@polycode-projects/the-mechanical-code-talker 4.1.1 → 4.1.3
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 +31 -18
- package/bin/tmct.mjs +3 -0
- package/data/templates/responses.jsonl +3 -0
- package/package.json +2 -1
- package/src/adapters/memory/core.mjs +1358 -196
- package/src/adapters/memory/inspect.mjs +11 -0
- package/src/adapters/memory/shacl.mjs +38 -0
- package/src/adapters/p2p/webrtc-transport.mjs +28 -5
- package/src/domain/ask-vocab.mjs +39 -0
- package/src/domain/ask.mjs +183 -34
- package/src/domain/grammar/assert.mjs +8 -2
- package/src/domain/hanoi-board.mjs +232 -0
- package/src/domain/ingest-facts.mjs +120 -0
- package/src/domain/interpret/normalize.mjs +49 -0
- package/src/domain/memory/compaction.mjs +284 -0
- package/src/domain/memory/resolution.mjs +171 -0
- package/src/domain/memory/trust.mjs +175 -5
- package/src/domain/memory-facts.mjs +139 -0
- package/src/domain/p2p/facts.mjs +21 -0
- package/src/domain/p2p/peer-id.mjs +15 -0
- package/src/domain/p2p/provenance-relabel.mjs +13 -2
- package/src/domain/p2p/sync-filter.mjs +5 -1
- package/src/domain/p2p/wire.mjs +7 -4
- package/src/domain/scene-compose.mjs +2 -2
- package/src/domain/sprite-facts.mjs +0 -0
- package/src/services/adventure-viz.mjs +5 -1
- package/src/services/adventure.mjs +70 -44
- package/src/services/chat-page-viz.mjs +381 -310
- package/src/services/chat.mjs +273 -155
- package/src/services/code-explorer-viz.mjs +141 -54
- package/src/services/index.mjs +1 -1
- package/src/services/ingest-viz.mjs +134 -9
- package/src/services/ledger-viz.mjs +7 -4
- package/src/services/memory-panel-viz.mjs +8 -3
- package/src/services/mud-turn.mjs +11 -8
- package/src/services/mud-viz.mjs +441 -206
- package/src/services/p2p-room.mjs +110 -23
- package/src/services/plan-viz.mjs +63 -4
- package/src/services/research-viz.mjs +18 -7
- package/src/services/share-overlay-viz.mjs +623 -0
- package/src/services/spider-fly-viz.mjs +2 -2
- package/src/services/sprite-catalog-viz.mjs +303 -78
- package/src/surfaces/web/adventure-browser-entry.mjs +27 -5
- package/src/surfaces/web/chat-browser-entry.mjs +37 -10
- package/src/surfaces/web/code-explorer-browser-entry.mjs +4 -3
- package/src/surfaces/web/ingest-browser-entry.mjs +73 -12
- package/src/surfaces/web/ledger-browser-entry.mjs +32 -7
- package/src/surfaces/web/memory-ask-browser.bundle.js +149 -116
- package/src/surfaces/web/mud-browser-entry.mjs +38 -7
- package/src/surfaces/web/p2p-browser-entry.mjs +1 -1
- package/src/surfaces/web/plan-browser-entry.mjs +33 -2
- package/src/surfaces/web/research-browser-entry.mjs +11 -19
- package/src/surfaces/web/sprites-browser-entry.mjs +39 -8
- package/src/surfaces/web/tmct-surface.mjs +12 -0
- package/src/surfaces/web/turn-session.mjs +10 -3
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// hanoi-board.mjs — the one pure walk from a solved towers-of-hanoi plan to
|
|
2
|
+
// board-shaped fact rows, and from those rows to the `{individuals,
|
|
3
|
+
// objectProperties}` payload parseEntities consumes. The plan page's
|
|
4
|
+
// counterpart to sprite-facts.mjs, and to ask.mjs's own
|
|
5
|
+
// worldRelationGraphPayload.
|
|
6
|
+
//
|
|
7
|
+
// The plan lane already hands the page everything a board question needs:
|
|
8
|
+
// `plan.states[step]` is the position after `step` moves, one `mgx:rest-on`
|
|
9
|
+
// row per disk; `plan.actions` is the move list; `plan.domain` carries the
|
|
10
|
+
// taught taxonomy (`classMembers`), the taught size order (`ordering`) and
|
|
11
|
+
// the taught render hints. None of that is a graph, so `tmct.ask` over a plan
|
|
12
|
+
// session had nothing to traverse. These two functions close that.
|
|
13
|
+
//
|
|
14
|
+
// A row set is taken AT A STEP, because that is the question a visitor asks
|
|
15
|
+
// on that page: the transport bar scrubs the board, and "where are the disks"
|
|
16
|
+
// means where they are on the board in front of them.
|
|
17
|
+
//
|
|
18
|
+
// What travels, and why each row earns its place:
|
|
19
|
+
// - `<piece> rdf:type <class>` — the taught board taxonomy (disk, peg),
|
|
20
|
+
// so ask()'s dynamic class lane answers "how many disks are there" and
|
|
21
|
+
// "list the pegs" straight off `individual.class`, with no vocabulary
|
|
22
|
+
// table to edit.
|
|
23
|
+
// - `<disk> mgx:rest-on <support>` — the position itself, copied from the
|
|
24
|
+
// state's own rows. The support is a disk or a peg.
|
|
25
|
+
// - `<disk> mgx:currently-in <peg>`— the same position chased down the
|
|
26
|
+
// support chain to the peg the disk is standing on. This is
|
|
27
|
+
// ask-vocab.mjs's WORLD_RELATIONS.placement predicate, so "where are the
|
|
28
|
+
// disks" and "list the locations of disks" answer over the live board the
|
|
29
|
+
// way spider-fly's own board answers them.
|
|
30
|
+
// - `<peg> mgx:top-disk <disk>` — the topmost disk on a peg, the only one
|
|
31
|
+
// a legal move can lift. Derived, not restated: a peg holding nothing
|
|
32
|
+
// emits no row.
|
|
33
|
+
// - `<disk> mgx:smaller-than <disk>` — the taught size order, straight from
|
|
34
|
+
// `plan.domain.ordering`.
|
|
35
|
+
// - `move-N rdf:type move`, `move-N mgx:moves-disk <disk>`,
|
|
36
|
+
// `move-N mgx:moves-onto <target>` — the solution's own move list, so
|
|
37
|
+
// "how many moves are there" and "list the moves" answer about the
|
|
38
|
+
// puzzle the page is showing.
|
|
39
|
+
//
|
|
40
|
+
// The move rows cover the WHOLE solution rather than only the moves played so
|
|
41
|
+
// far, because that is what the movelist beside the board shows and what "how
|
|
42
|
+
// many moves does this take" means. The step is what the position rows read;
|
|
43
|
+
// the move list is the plan, and the plan does not change as you scrub it.
|
|
44
|
+
//
|
|
45
|
+
// Which classes count as board furniture follows computeBlocksLayout's own
|
|
46
|
+
// rule, for the same reason: `plan.domain.classMembers` is the whole memory's
|
|
47
|
+
// taxonomy, not just this puzzle's, so a session that has been taught a
|
|
48
|
+
// vocabulary carries hundreds of individuals with nothing to do with the game.
|
|
49
|
+
// A class DECLARED in the render hints contributes every member, empty or not
|
|
50
|
+
// (an unused peg is still a peg); an undeclared class contributes only the
|
|
51
|
+
// members the plan itself names.
|
|
52
|
+
|
|
53
|
+
/** The board relations a projected hanoi graph carries. `placement` is
|
|
54
|
+
* ask-vocab.mjs's own WORLD_RELATIONS.placement predicate, restated here
|
|
55
|
+
* rather than imported so this module stays a leaf with no imports — the
|
|
56
|
+
* spelling is checked against that table by test. */
|
|
57
|
+
export const HANOI_BOARD_RELATIONS = Object.freeze({
|
|
58
|
+
placement: Object.freeze({
|
|
59
|
+
predicate: "mgx:currently-in",
|
|
60
|
+
comment: "disk -> peg: the peg this disk is standing on, chased down its own support chain.",
|
|
61
|
+
}),
|
|
62
|
+
support: Object.freeze({
|
|
63
|
+
predicate: "mgx:rest-on",
|
|
64
|
+
comment: "disk -> disk or peg: what this disk is resting directly on.",
|
|
65
|
+
}),
|
|
66
|
+
topDisk: Object.freeze({
|
|
67
|
+
predicate: "mgx:top-disk",
|
|
68
|
+
comment: "peg -> disk: the topmost disk on that peg, the only one a legal move can lift.",
|
|
69
|
+
}),
|
|
70
|
+
size: Object.freeze({
|
|
71
|
+
predicate: "mgx:smaller-than",
|
|
72
|
+
comment: "disk -> disk: the taught size order between two disks.",
|
|
73
|
+
}),
|
|
74
|
+
movesDisk: Object.freeze({
|
|
75
|
+
predicate: "mgx:moves-disk",
|
|
76
|
+
comment: "move -> disk: the disk that move lifts.",
|
|
77
|
+
}),
|
|
78
|
+
movesOnto: Object.freeze({
|
|
79
|
+
predicate: "mgx:moves-onto",
|
|
80
|
+
comment: "move -> disk or peg: what that move sets the disk down on.",
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/** The class every solution move is filed under, so a count/list question
|
|
85
|
+
* reaches the move list by the word a visitor would use. */
|
|
86
|
+
export const HANOI_MOVE_CLASS = "move";
|
|
87
|
+
|
|
88
|
+
/** `move-1` … `move-N`, numbered the way the movelist beside the board
|
|
89
|
+
* numbers them. */
|
|
90
|
+
export const hanoiMoveId = (index) => `move-${index + 1}`;
|
|
91
|
+
|
|
92
|
+
const isThanPredicate = (predicate) => /-than$/.test(String(predicate || ""));
|
|
93
|
+
|
|
94
|
+
/** Every label the plan itself names, across its states and its actions —
|
|
95
|
+
* the same set computeBlocksLayout draws its undeclared anchors from. */
|
|
96
|
+
function namesInPlan(plan) {
|
|
97
|
+
const named = new Set();
|
|
98
|
+
for (const rows of plan?.states || []) {
|
|
99
|
+
for (const r of rows || []) {
|
|
100
|
+
if (r?.subject != null) named.add(r.subject);
|
|
101
|
+
if (r?.object != null) named.add(r.object);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
for (const a of plan?.actions || []) {
|
|
105
|
+
if (a?.subject != null) named.add(a.subject);
|
|
106
|
+
if (a?.target != null) named.add(a.target);
|
|
107
|
+
}
|
|
108
|
+
return named;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** class -> the members of it this board carries, applying the declared/
|
|
112
|
+
* undeclared rule from this module's header. */
|
|
113
|
+
function boardMembersByClass(plan) {
|
|
114
|
+
const classMembers = plan?.domain?.classMembers || {};
|
|
115
|
+
const renderHints = plan?.domain?.renderHints || {};
|
|
116
|
+
const named = namesInPlan(plan);
|
|
117
|
+
const out = new Map();
|
|
118
|
+
for (const cls of Object.keys(classMembers).sort()) {
|
|
119
|
+
const declared = Object.prototype.hasOwnProperty.call(renderHints, cls);
|
|
120
|
+
const members = [...(classMembers[cls] || [])]
|
|
121
|
+
.filter((m) => declared || named.has(m))
|
|
122
|
+
.sort();
|
|
123
|
+
if (members.length) out.set(cls, members);
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Follow `rest-on` from a piece down to whatever it finally stands on. Returns
|
|
129
|
+
* null when the piece rests on nothing, and when the chain loops back on
|
|
130
|
+
* itself — a cycle is a position no board can be in, and inventing a peg for
|
|
131
|
+
* it would be a guess. */
|
|
132
|
+
function groundUnder(piece, supportOf) {
|
|
133
|
+
let here = piece;
|
|
134
|
+
const seen = new Set([piece]);
|
|
135
|
+
while (supportOf.has(here)) {
|
|
136
|
+
const next = supportOf.get(here);
|
|
137
|
+
if (seen.has(next)) return null;
|
|
138
|
+
seen.add(next);
|
|
139
|
+
here = next;
|
|
140
|
+
}
|
|
141
|
+
return here === piece ? null : here;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The board rows for one position of a solved plan, as plain
|
|
146
|
+
* `{ subject, predicate, object }` triples — deterministic for identical
|
|
147
|
+
* inputs, deduplicated, and drawn only from the plan handed in. Pure.
|
|
148
|
+
*
|
|
149
|
+
* `step` is the number of moves played, so `0` is the starting position and
|
|
150
|
+
* `plan.actions.length` is the solved one. Out-of-range steps clamp.
|
|
151
|
+
*/
|
|
152
|
+
export function hanoiBoardRows({ plan, step = 0 } = {}) {
|
|
153
|
+
const states = plan?.states || [];
|
|
154
|
+
const actions = plan?.actions || [];
|
|
155
|
+
const rows = [];
|
|
156
|
+
const seen = new Set();
|
|
157
|
+
const add = (subject, predicate, object) => {
|
|
158
|
+
if (subject == null || object == null || !predicate) return;
|
|
159
|
+
const key = `${subject} ${predicate} ${object}`;
|
|
160
|
+
if (seen.has(key)) return;
|
|
161
|
+
seen.add(key);
|
|
162
|
+
rows.push({ subject: String(subject), predicate, object: String(object) });
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
for (const [cls, members] of boardMembersByClass(plan)) {
|
|
166
|
+
for (const m of members) add(m, "rdf:type", cls);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const at = states.length
|
|
170
|
+
? states[Math.max(0, Math.min(states.length - 1, Math.floor(Number(step) || 0)))]
|
|
171
|
+
: [];
|
|
172
|
+
const supportOf = new Map();
|
|
173
|
+
const supporting = new Set();
|
|
174
|
+
for (const r of at || []) {
|
|
175
|
+
if (r?.subject == null || r.object == null) continue;
|
|
176
|
+
add(r.subject, r.predicate || HANOI_BOARD_RELATIONS.support.predicate, r.object);
|
|
177
|
+
supportOf.set(String(r.subject), String(r.object));
|
|
178
|
+
supporting.add(String(r.object));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const topOf = new Map();
|
|
182
|
+
for (const piece of [...supportOf.keys()].sort()) {
|
|
183
|
+
const ground = groundUnder(piece, supportOf);
|
|
184
|
+
if (!ground) continue;
|
|
185
|
+
add(piece, HANOI_BOARD_RELATIONS.placement.predicate, ground);
|
|
186
|
+
if (!supporting.has(piece)) topOf.set(ground, piece);
|
|
187
|
+
}
|
|
188
|
+
for (const [ground, top] of [...topOf.entries()].sort()) {
|
|
189
|
+
add(ground, HANOI_BOARD_RELATIONS.topDisk.predicate, top);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
for (const row of plan?.domain?.ordering || []) {
|
|
193
|
+
if (!isThanPredicate(row?.predicate)) continue;
|
|
194
|
+
add(row.subject, HANOI_BOARD_RELATIONS.size.predicate, row.object);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
actions.forEach((action, i) => {
|
|
198
|
+
const id = hanoiMoveId(i);
|
|
199
|
+
add(id, "rdf:type", HANOI_MOVE_CLASS);
|
|
200
|
+
add(id, HANOI_BOARD_RELATIONS.movesDisk.predicate, action?.subject);
|
|
201
|
+
add(id, HANOI_BOARD_RELATIONS.movesOnto.predicate, action?.target);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
return rows;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Project hanoiBoardRows' own rows into the `{individuals, objectProperties}`
|
|
208
|
+
* payload parseEntities consumes, so a page holding a live puzzle can hand
|
|
209
|
+
* `ask()` a real graph of the position instead of an empty one.
|
|
210
|
+
*
|
|
211
|
+
* Every `rdf:type` row makes its subject an individual of that class, and
|
|
212
|
+
* that is the whole individuals set — a term the board only ever mentions as
|
|
213
|
+
* the object of an edge is not a board piece and gets no class invented for
|
|
214
|
+
* it. Every other row travels as an edge under its own predicate. Pure. */
|
|
215
|
+
export function hanoiBoardGraphPayload(rows) {
|
|
216
|
+
const individuals = new Map();
|
|
217
|
+
for (const row of rows || []) {
|
|
218
|
+
if (row?.predicate !== "rdf:type" || !row.subject || !row.object) continue;
|
|
219
|
+
if (!individuals.has(row.subject)) {
|
|
220
|
+
individuals.set(row.subject, { id: row.subject, label: row.subject, class: row.object });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const groups = new Map();
|
|
224
|
+
for (const row of rows || []) {
|
|
225
|
+
if (!row?.subject || !row.predicate || row.predicate === "rdf:type") continue;
|
|
226
|
+
if (!groups.has(row.predicate)) {
|
|
227
|
+
groups.set(row.predicate, { prop: row.predicate, predicate: row.predicate, examples: [] });
|
|
228
|
+
}
|
|
229
|
+
groups.get(row.predicate).examples.push({ subject: row.subject, object: row.object });
|
|
230
|
+
}
|
|
231
|
+
return { individuals: [...individuals.values()], objectProperties: [...groups.values()] };
|
|
232
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// ingest-facts.mjs — the projection from an ingest session's stored fact rows
|
|
2
|
+
// to the `{individuals, objectProperties}` payload parseEntities consumes, so
|
|
3
|
+
// the ingest page can hand `ask()` a real graph of what a visitor just pasted
|
|
4
|
+
// instead of an empty one. The ingest counterpart to ask.mjs's
|
|
5
|
+
// worldRelationGraphPayload and sprite-facts.mjs's spriteFactGraphPayload.
|
|
6
|
+
//
|
|
7
|
+
// It is shaped differently from both, because the domain is different.
|
|
8
|
+
//
|
|
9
|
+
// A world knows its own predicates ahead of time (ask-vocab.mjs's
|
|
10
|
+
// WORLD_PREDICATES) and a sprite catalog knows its own subjects ahead of time
|
|
11
|
+
// (one per template class), so each of those projections carries a curated
|
|
12
|
+
// predicate or class table. Ingest knows neither: the vocabulary is whatever
|
|
13
|
+
// the visitor's own prose grounded. So nothing here is curated by predicate
|
|
14
|
+
// name at all — every row travels as an edge under its own predicate, and the
|
|
15
|
+
// only rule with a table behind it is which predicates carry a CLASS.
|
|
16
|
+
//
|
|
17
|
+
// The class rule is what makes the graph answerable. `ask()`'s own
|
|
18
|
+
// dynamicClassQuery ("how many dogs are there", "list dogs") resolves a class
|
|
19
|
+
// noun straight against `individual.class`, with no vocabulary-table entry
|
|
20
|
+
// needed, and that is the one lane an open-vocabulary graph can reach. So a
|
|
21
|
+
// term's class is the object of its own taxonomy row: "a beagle is a kind of
|
|
22
|
+
// dog" stores `beagle rdfs:subClassOf dog`, which classes the individual
|
|
23
|
+
// `beagle` as a `dog`, which is what makes "list dogs" answer with the real
|
|
24
|
+
// beagles on record. A term with no taxonomy row of its own still becomes an
|
|
25
|
+
// individual (so a relation edge has both its ends) but carries no class, and
|
|
26
|
+
// no class is ever minted for it — a listing it cannot ground lands on the
|
|
27
|
+
// honest miss wall, the same standard every other projection holds.
|
|
28
|
+
//
|
|
29
|
+
// The class is the DIRECT taxonomy parent, not a closure over it: with
|
|
30
|
+
// `fido -> beagle` and `beagle -> dog` on record, "list dogs" answers `beagle`
|
|
31
|
+
// and "list beagles" answers `fido`. That is what the store actually says.
|
|
32
|
+
//
|
|
33
|
+
// Which rows to project is a separate decision from how to project them, and
|
|
34
|
+
// the session makes it: `ingestedFactRows` keeps the facts a session brought
|
|
35
|
+
// in itself (taught, operator-asserted, extracted, or the low-trust optimistic
|
|
36
|
+
// tier) and drops the seeded corpus bands, which run to tens of thousands of
|
|
37
|
+
// rows the visitor never typed. Both functions are pure.
|
|
38
|
+
import { provenanceTagToSource } from "./memory/trust.mjs";
|
|
39
|
+
|
|
40
|
+
/** The Source kinds that mean "this session brought this fact in", as opposed
|
|
41
|
+
* to the corpus bands it booted with. Read off the same
|
|
42
|
+
* `provenanceTagToSource` tags memory-stats.mjs's own taught/band split uses,
|
|
43
|
+
* so the ask route and the stats panel can never disagree about which facts
|
|
44
|
+
* are the visitor's own. */
|
|
45
|
+
export const INGESTED_SOURCE_KINDS = Object.freeze(["teach", "operator", "extracted", "optimisticExtract"]);
|
|
46
|
+
|
|
47
|
+
const INGESTED_KINDS = new Set(INGESTED_SOURCE_KINDS);
|
|
48
|
+
|
|
49
|
+
/** True where this row's provenance names a source kind the visitor's own
|
|
50
|
+
* session produced. A row whose provenance parses to nothing is not claimed. */
|
|
51
|
+
export function isIngestedFactRow(row) {
|
|
52
|
+
for (const tag of String(row?.provenance || "").split(" | ")) {
|
|
53
|
+
if (!tag) continue;
|
|
54
|
+
const source = provenanceTagToSource(tag);
|
|
55
|
+
if (source && INGESTED_KINDS.has(source.kind)) return true;
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The subset of `rows` (readFactRows' shape) this session grounded itself. */
|
|
61
|
+
export function ingestedFactRows(rows) {
|
|
62
|
+
return (rows || []).filter(isIngestedFactRow);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The predicates whose object names the subject's CLASS. `rdfs:subClassOf` is
|
|
66
|
+
* what the teach lane canonicalizes both "a beagle is a kind of dog" and
|
|
67
|
+
* "Fido is a beagle" to; `rdf:type` is what a loaded world and an imported
|
|
68
|
+
* pack write instead, and either can reach this store. */
|
|
69
|
+
export const CLASSING_PREDICATES = Object.freeze(["rdfs:subClassOf", "rdf:type"]);
|
|
70
|
+
|
|
71
|
+
const CLASSING = new Set(CLASSING_PREDICATES);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Project plain ingest fact rows (`{subject, predicate, object}`, the shape
|
|
75
|
+
* memory/core.mjs's readFactRows returns) into the `{individuals,
|
|
76
|
+
* objectProperties}` payload parseEntities consumes.
|
|
77
|
+
*
|
|
78
|
+
* Every term that appears as a subject or an object becomes an individual, so
|
|
79
|
+
* an edge always has both its ends in `byId`. A term with a taxonomy row of
|
|
80
|
+
* its own is classed by that row's object — the FIRST such row in reading
|
|
81
|
+
* order, so a term taught two parents keeps the one the visitor stated first
|
|
82
|
+
* rather than a silently reordered pick. Every row, taxonomy rows included,
|
|
83
|
+
* also travels as an edge under its own predicate, deduplicated.
|
|
84
|
+
*
|
|
85
|
+
* Deterministic for identical input: individuals come out in first-seen order,
|
|
86
|
+
* predicate groups in first-seen predicate order. Pure.
|
|
87
|
+
*/
|
|
88
|
+
export function ingestFactGraphPayload(rows) {
|
|
89
|
+
const classByTerm = new Map();
|
|
90
|
+
for (const row of rows || []) {
|
|
91
|
+
if (!row?.subject || !row.object || !CLASSING.has(row.predicate)) continue;
|
|
92
|
+
const subject = String(row.subject);
|
|
93
|
+
if (!classByTerm.has(subject)) classByTerm.set(subject, String(row.object));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const individuals = new Map();
|
|
97
|
+
const noteTerm = (id) => {
|
|
98
|
+
if (individuals.has(id)) return;
|
|
99
|
+
const cls = classByTerm.get(id);
|
|
100
|
+
individuals.set(id, cls ? { id, label: id, class: cls } : { id, label: id });
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const groups = new Map();
|
|
104
|
+
const seenEdges = new Set();
|
|
105
|
+
for (const row of rows || []) {
|
|
106
|
+
if (!row?.subject || !row.predicate || !row.object) continue;
|
|
107
|
+
const subject = String(row.subject);
|
|
108
|
+
const predicate = String(row.predicate);
|
|
109
|
+
const object = String(row.object);
|
|
110
|
+
noteTerm(subject);
|
|
111
|
+
noteTerm(object);
|
|
112
|
+
const edgeKey = `${predicate} ${subject} ${object}`;
|
|
113
|
+
if (seenEdges.has(edgeKey)) continue;
|
|
114
|
+
seenEdges.add(edgeKey);
|
|
115
|
+
if (!groups.has(predicate)) groups.set(predicate, { prop: predicate, predicate, examples: [] });
|
|
116
|
+
groups.get(predicate).examples.push({ subject, object });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { individuals: [...individuals.values()], objectProperties: [...groups.values()] };
|
|
120
|
+
}
|
|
@@ -62,6 +62,55 @@ export function kindNounAnaphoraHint(text) {
|
|
|
62
62
|
return m ? (ENTITY_TO_TYPE[m[2].toLowerCase()] || null) : null;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// ---- the dated teach frame: "<sentence> as of <date>" ----
|
|
66
|
+
|
|
67
|
+
const MONTH_NAMES = [
|
|
68
|
+
"january", "february", "march", "april", "may", "june",
|
|
69
|
+
"july", "august", "september", "october", "november", "december",
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
/** A closed trailing suffix, not a grammar change: three accepted date forms
|
|
73
|
+
* ("as of 2019", "as of 2019-03-01", "as of march 2019"), anchored to the
|
|
74
|
+
* end of input with trailing punctuation tolerated. "as of" only — "as at",
|
|
75
|
+
* "back in", and bare "in 2019" stay out (the last is ambiguous with
|
|
76
|
+
* locatives: "the dog is in 2019" vs "the meeting is in room 4"). */
|
|
77
|
+
const DATED_TEACH_SUFFIX_RE =
|
|
78
|
+
/\s+as\s+of\s+((?:19|20)\d{2}(?:-(?:0[1-9]|1[0-2])(?:-(?:0[1-9]|[12]\d|3[01]))?)?|(?:january|february|march|april|may|june|july|august|september|october|november|december)\s+(?:19|20)\d{2})\s*[.!?]*$/i;
|
|
79
|
+
|
|
80
|
+
/** Read-only probe: does `text` carry a trailing "as of <date>" suffix? A hit
|
|
81
|
+
* returns `{ stripped, observedAt, dateText }` — `stripped` is the sentence
|
|
82
|
+
* with the suffix removed (exactly the shape the teach lanes already parse),
|
|
83
|
+
* `dateText` the matched date phrase verbatim (for the acknowledgment echo),
|
|
84
|
+
* and `observedAt` the ISO instant the date names, always Date.parse-able.
|
|
85
|
+
* No hit, or a suffix with nothing left to teach, returns null. Same
|
|
86
|
+
* read-only-probe shape as kindNounAnaphoraHint above — a helper a teach
|
|
87
|
+
* lane consults, never a mutation of normalizeQuery's own text->text path.
|
|
88
|
+
*
|
|
89
|
+
* The stored instant is the START of the named period (a bare year ->
|
|
90
|
+
* <yyyy>-01-01T00:00:00.000Z; month+year -> the 1st of that month; a full
|
|
91
|
+
* date -> that day, all at midnight UTC) — the CONSERVATIVE reading for
|
|
92
|
+
* latest-observation-wins: it can under-claim how recent the observation
|
|
93
|
+
* was, never over-claim it. */
|
|
94
|
+
export function datedTeachSuffix(text) {
|
|
95
|
+
const s = String(text || "");
|
|
96
|
+
const m = DATED_TEACH_SUFFIX_RE.exec(s);
|
|
97
|
+
if (!m) return null;
|
|
98
|
+
const stripped = s.slice(0, m.index).trim();
|
|
99
|
+
if (!stripped) return null;
|
|
100
|
+
const dateText = m[1];
|
|
101
|
+
const isoForm = /^(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?$/.exec(dateText);
|
|
102
|
+
let observedAt;
|
|
103
|
+
if (isoForm) {
|
|
104
|
+
const [, year, month, day] = isoForm;
|
|
105
|
+
observedAt = `${year}-${month || "01"}-${day || "01"}T00:00:00.000Z`;
|
|
106
|
+
} else {
|
|
107
|
+
const [, monthName, year] = /^([a-z]+)\s+(\d{4})$/i.exec(dateText);
|
|
108
|
+
const monthNum = String(MONTH_NAMES.indexOf(monthName.toLowerCase()) + 1).padStart(2, "0");
|
|
109
|
+
observedAt = `${year}-${monthNum}-01T00:00:00.000Z`;
|
|
110
|
+
}
|
|
111
|
+
return { stripped, observedAt, dateText };
|
|
112
|
+
}
|
|
113
|
+
|
|
65
114
|
// Every relation verb phrase, as one longest-first alternation — feeds the
|
|
66
115
|
// DOES-X-VERB-ANYTHING-ELSE frame below without hardcoding a parallel list.
|
|
67
116
|
const VERB_ALTERNATION = Object.keys(VERB_TO_KIND).sort((a, b) => b.length - a.length).map(escapeRegex).join("|");
|