@chatpanel/events 0.102.0 → 0.103.1
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/adapters.js +1 -83
- package/adaptive-tool-policy.js +1 -45
- package/agent.js +2 -255
- package/attribution.js +1 -132
- package/backup-envelope.js +1 -221
- package/board-tool.js +2 -165
- package/budget.js +1 -116
- package/capability.js +1 -126
- package/citations.js +5 -127
- package/client-prefs.js +1 -148
- package/context-attachments.js +8 -144
- package/cowriter-router.js +1 -83
- package/cowriter-writer.js +19 -193
- package/cowriter.js +1 -190
- package/curate.js +3 -509
- package/distance.js +1 -124
- package/engine.js +1 -131
- package/entitlement.js +1 -332
- package/entity.js +1 -354
- package/event.js +1 -175
- package/extraction.js +4 -293
- package/failover.js +1 -93
- package/find-tool.js +1 -66
- package/flowchart.js +2 -644
- package/gate.js +1 -74
- package/harness.js +4 -101
- package/index.js +1 -345
- package/invariants.js +1 -174
- package/job.js +3 -147
- package/kernel.js +1 -255
- package/knowledge-derive.js +1 -267
- package/knowledge.js +3 -230
- package/library.js +11 -280
- package/loop.js +1 -132
- package/manifest.js +1 -107
- package/markdown-authoring.js +11 -288
- package/markdown-render.js +0 -0
- package/mcp-client.js +8 -358
- package/mcp-dispatch.js +1 -52
- package/mcp-errors.js +1 -106
- package/mcp-manager.js +1 -127
- package/media-transcript.js +8 -531
- package/meeting-analyzers.js +1 -83
- package/meeting-insights.js +7 -164
- package/meeting-shape.js +1 -282
- package/meeting-text.js +5 -94
- package/memory.js +2 -638
- package/model-candidates.js +1 -358
- package/model-health.js +1 -182
- package/model-ledger.js +1 -228
- package/model-picker.js +1 -188
- package/note-actions.js +15 -246
- package/note-graph.js +1 -158
- package/note-links.js +1 -68
- package/note-mentions.js +8 -114
- package/note-plan.js +9 -107
- package/note-research.js +1 -135
- package/observability.js +1 -119
- package/omni.js +1 -125
- package/order.js +1 -78
- package/package.json +1 -4
- package/pdf-layout.js +7 -253
- package/project-run.js +20 -444
- package/project.js +1 -170
- package/promotion.js +1 -171
- package/queue.js +1 -98
- package/reach.js +1 -23
- package/recipe-tool.js +2 -161
- package/recipe.js +1 -264
- package/record-list.js +2 -110
- package/recruit.js +3 -456
- package/redaction-tokens.js +1 -61
- package/ref.js +1 -55
- package/registry.js +1 -240
- package/route-graph.js +1 -115
- package/route-strategies.js +2 -232
- package/router.js +2 -880
- package/rrf.js +1 -89
- package/rules.js +1 -142
- package/schedule.js +1 -663
- package/scm-connection.js +1 -180
- package/scopes.js +1 -12
- package/scorecard.js +1 -335
- package/search-engines.js +1 -81
- package/skill-manifest.js +1 -330
- package/skill-scan.js +3 -209
- package/skill-sources.js +1 -162
- package/skill-vars.js +2 -252
- package/slash-commands.js +1 -158
- package/source-gate.js +1 -92
- package/sources-retrieval.js +5 -186
- package/sources.js +1 -273
- package/store.js +1 -171
- package/structured.js +4 -901
- package/subject-kinds.js +1 -5
- package/subject-name.js +1 -96
- package/sync-plan.js +1 -213
- package/synthesis.js +3 -123
- package/tags.js +1 -203
- package/team-board.js +6 -337
- package/team-cache.js +2 -63
- package/team-observe.js +1 -118
- package/team-org.js +1 -442
- package/team-plan.js +4 -131
- package/team-record.js +1 -225
- package/team-run.js +9 -652
- package/team-subtask.js +2 -191
- package/team-task.js +2 -124
- package/team-tool.js +2 -196
- package/team-trail.js +2 -73
- package/team-worklog.js +2 -149
- package/team.js +1 -302
- package/text-search.js +1 -155
- package/theme.js +17 -154
- package/titles.js +4 -285
- package/tool-discovery.js +1 -87
- package/tool-dispatch.js +5 -230
- package/tool-groups.js +1 -81
- package/tool-hints.js +10 -185
- package/tool-loop-guard.js +4 -182
- package/tool-need.js +2 -124
- package/tool-result.js +2 -438
- package/tool-round.js +0 -0
- package/tool-schema.js +1 -155
- package/tool-traits.js +1 -182
- package/toolset.js +2 -88
- package/trajectory.js +1 -530
- package/turn-loop.js +4 -440
- package/upcast.js +1 -37
- package/vault.js +2 -250
- package/view.js +1 -88
- package/voice-intents.js +2 -1488
- package/voice-speaker.js +1 -98
- package/weather-tool.js +1 -65
- package/weather.js +2 -211
- package/web-search-tool.js +14 -102
- package/web-search.js +1 -160
- package/widget.js +1 -153
package/knowledge-derive.js
CHANGED
|
@@ -1,267 +1 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// Separate from `knowledge.js` (the model) because the costs are not alike: reading a brief
|
|
4
|
-
// needs its shape and its renderer; building one walks every record, resolves entities and
|
|
5
|
-
// runs the deterministic maintenance passes. Only pages ever build. The MV3 service worker
|
|
6
|
-
// only ever reads — it syncs stored briefs onward — so keeping this out of `knowledge.js`
|
|
7
|
-
// keeps it off the worker's cold start entirely, rather than relying on nobody importing it.
|
|
8
|
-
//
|
|
9
|
-
// Everything here is class R: no model, no network, and `now` is injected rather than read,
|
|
10
|
-
// so a rebuild is reproducible. That is what makes invariant I-K2 true — deriveBriefs() is
|
|
11
|
-
// the ONLY way a brief is ever created, so throwing them all away can never lose anything.
|
|
12
|
-
|
|
13
|
-
import { makeRef } from './ref.js';
|
|
14
|
-
import {
|
|
15
|
-
DEFAULT_THRESHOLD, MAX_SUBJECTS, normalizeSubject, rankSubjects, resolveSubjects,
|
|
16
|
-
} from './entity.js';
|
|
17
|
-
import { mentionsFrom, normalizeRecords, wantedPages } from './curate.js';
|
|
18
|
-
import { MAX_CLAIMS, MAX_CLAIM_REFS, MAX_BRIEF_RECORDS, briefId, contentHash } from './knowledge.js';
|
|
19
|
-
|
|
20
|
-
/** Records are addressed `chat:x` / `meeting:y` / `note:z` — the ref kind is the prefix. */
|
|
21
|
-
function refForRecord(rec) {
|
|
22
|
-
const [kind, ...rest] = String(rec.id).split(':');
|
|
23
|
-
const id = rest.join(':') || rec.id;
|
|
24
|
-
const known = kind === 'chat' || kind === 'meeting' || kind === 'note' || kind === 'page';
|
|
25
|
-
return makeRef({ kind: known ? kind : 'result', id: known ? id : rec.id, hash: contentHash(rec.text) });
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function claim({ id, kind, text, refs, at = 0, confidence = 1 }) {
|
|
29
|
-
return {
|
|
30
|
-
id,
|
|
31
|
-
kind,
|
|
32
|
-
text,
|
|
33
|
-
// I-K1 lives here: a claim is CONSTRUCTED with its refs, and `checkKnowledgeInvariants`
|
|
34
|
-
// refuses one that arrives without them. There is no path that writes a claim first and
|
|
35
|
-
// attaches provenance later, because that path is how provenance goes missing.
|
|
36
|
-
refs: refs.slice(0, MAX_CLAIM_REFS),
|
|
37
|
-
firstSeen: at,
|
|
38
|
-
lastConfirmed: at,
|
|
39
|
-
confidence,
|
|
40
|
-
cls: 'R', // class R — derived, not written. W3's prose claims carry 'C'.
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
45
|
-
const isoDay = (ms) => (ms ? new Date(ms).toISOString().slice(0, 10) : '');
|
|
46
|
-
|
|
47
|
-
// How many co-occurring subjects a `together` claim names. Past a handful it stops being a
|
|
48
|
-
// statement and becomes a tag cloud.
|
|
49
|
-
const TOGETHER_LIMIT = 5;
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Memories that are ABOUT this subject become its highest-confidence claims.
|
|
53
|
-
*
|
|
54
|
-
* This is how the existing memory layer folds in, and the direction matters. `identity` and
|
|
55
|
-
* `preference` memories are AMBIENT — they ride every turn already, and copying them onto a
|
|
56
|
-
* brief would say them twice. The other three kinds (`project`, `fact`, `reference`) are
|
|
57
|
-
* retrieved rather than ambient, and a retrieved durable statement about a subject is
|
|
58
|
-
* exactly what a claim is — except the user said it themselves, so it outranks anything
|
|
59
|
-
* derived from co-occurrence.
|
|
60
|
-
*
|
|
61
|
-
* The reverse never happens automatically: a brief must not write itself into memory. Memory
|
|
62
|
-
* is bounded because it is ambient; a corpus flowing into it would unbound the one thing in
|
|
63
|
-
* the product that is deliberately small.
|
|
64
|
-
*/
|
|
65
|
-
function statedClaims(subject, memories, seq) {
|
|
66
|
-
const out = [];
|
|
67
|
-
const names = [subject.canonical, ...subject.aliases].filter(Boolean);
|
|
68
|
-
for (const mem of memories) {
|
|
69
|
-
if (!mem?.text || !mem.id) continue;
|
|
70
|
-
if (mem.kind === 'identity' || mem.kind === 'preference') continue; // already ambient
|
|
71
|
-
const hay = normalizeSubject(mem.text);
|
|
72
|
-
// Whole-token containment, so "pricing" does not match "repricing" and a one-word
|
|
73
|
-
// subject cannot claim every memory that happens to contain it as a substring.
|
|
74
|
-
const hit = names.some((n) => n && (hay === n || hay.includes(` ${n} `) || hay.startsWith(`${n} `) || hay.endsWith(` ${n}`)));
|
|
75
|
-
if (!hit) continue;
|
|
76
|
-
out.push(claim({
|
|
77
|
-
id: `${seq()}`,
|
|
78
|
-
kind: 'stated',
|
|
79
|
-
text: mem.text,
|
|
80
|
-
refs: [makeRef({ kind: 'memory', id: mem.id, hash: contentHash(mem.text) })],
|
|
81
|
-
at: Number(mem.updatedAt || mem.createdAt) || 0,
|
|
82
|
-
confidence: Number(mem.confidence) || 1,
|
|
83
|
-
}));
|
|
84
|
-
}
|
|
85
|
-
return out;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* One subject + the records that mention it → a brief.
|
|
90
|
-
*
|
|
91
|
-
* Every claim here is a restatement of something the corpus already holds, which is what
|
|
92
|
-
* makes the phase free and what makes I-K3's auto-promotion defensible: none of it is an
|
|
93
|
-
* opinion, so there is nothing for a reviewer to review.
|
|
94
|
-
*/
|
|
95
|
-
export function deriveBrief(subject, { records, byId, cooccurring = [], memories = [], wanted = null, now = 0 } = {}) {
|
|
96
|
-
const recs = [...(subject.records instanceof Set ? subject.records : subject.records || [])]
|
|
97
|
-
.map((id) => byId.get(id)).filter(Boolean)
|
|
98
|
-
.sort((a, b) => (a.date || 0) - (b.date || 0));
|
|
99
|
-
if (!recs.length && !wanted) return null;
|
|
100
|
-
|
|
101
|
-
let n = 0;
|
|
102
|
-
const seq = () => { n += 1; return `c${n}`; };
|
|
103
|
-
const claims = [];
|
|
104
|
-
|
|
105
|
-
if (recs.length) {
|
|
106
|
-
const byType = {};
|
|
107
|
-
for (const r of recs) byType[r.type] = (byType[r.type] || 0) + 1;
|
|
108
|
-
const parts = Object.entries(byType).sort((a, b) => b[1] - a[1]).map(([t, c]) => plural(c, t));
|
|
109
|
-
claims.push(claim({
|
|
110
|
-
id: seq(),
|
|
111
|
-
kind: 'presence',
|
|
112
|
-
text: `Appears in ${plural(recs.length, 'record')} — ${parts.join(', ')}.`,
|
|
113
|
-
// The most recent records, because that is what a reader checks first — and the
|
|
114
|
-
// newest is also the one most likely to still resolve.
|
|
115
|
-
refs: recs.slice(-MAX_CLAIM_REFS).reverse().map(refForRecord),
|
|
116
|
-
at: now,
|
|
117
|
-
}));
|
|
118
|
-
|
|
119
|
-
const first = recs.find((r) => r.date);
|
|
120
|
-
const last = [...recs].reverse().find((r) => r.date);
|
|
121
|
-
if (first && last && first !== last) {
|
|
122
|
-
claims.push(claim({
|
|
123
|
-
id: seq(),
|
|
124
|
-
kind: 'timeline',
|
|
125
|
-
text: `Runs from ${isoDay(first.date)} to ${isoDay(last.date)} — ${first.title || 'untitled'} → ${last.title || 'untitled'}.`,
|
|
126
|
-
refs: [refForRecord(first), refForRecord(last)],
|
|
127
|
-
at: now,
|
|
128
|
-
}));
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (cooccurring.length) {
|
|
133
|
-
claims.push(claim({
|
|
134
|
-
id: seq(),
|
|
135
|
-
kind: 'together',
|
|
136
|
-
text: `Usually alongside ${cooccurring.slice(0, TOGETHER_LIMIT).map((c) => c.name).join(', ')}.`,
|
|
137
|
-
refs: recs.slice(-MAX_CLAIM_REFS).map(refForRecord),
|
|
138
|
-
at: now,
|
|
139
|
-
}));
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (wanted) {
|
|
143
|
-
// A [[link]] that resolves to nothing is the corpus asking for a page: a human already
|
|
144
|
-
// decided the subject was worth naming. Saying so on the brief is more useful than
|
|
145
|
-
// hiding it, because it tells the reader the page is thin BECAUSE nobody wrote the
|
|
146
|
-
// source, not because the derivation failed.
|
|
147
|
-
claims.push(claim({
|
|
148
|
-
id: seq(),
|
|
149
|
-
kind: 'wanted',
|
|
150
|
-
text: `Linked from ${plural(wanted.recordCount, 'record')} but no record carries this title.`,
|
|
151
|
-
refs: recs.slice(0, MAX_CLAIM_REFS).map(refForRecord),
|
|
152
|
-
at: now,
|
|
153
|
-
}));
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
claims.push(...statedClaims(subject, memories, seq));
|
|
157
|
-
|
|
158
|
-
const capped = claims.slice(0, MAX_CLAIMS);
|
|
159
|
-
const dates = recs.map((r) => r.date || 0).filter(Boolean);
|
|
160
|
-
return {
|
|
161
|
-
id: briefId(subject.key),
|
|
162
|
-
key: subject.key,
|
|
163
|
-
kind: subject.kind,
|
|
164
|
-
subject: { name: subject.name, aliases: [...(subject.aliases || [])] },
|
|
165
|
-
// Class R may auto-promote (I-K3): nothing above is an opinion, so there is nothing a
|
|
166
|
-
// reviewer could accept or reject. W3's prose arrives as `proposed` beside it.
|
|
167
|
-
state: 'promoted',
|
|
168
|
-
cls: 'R',
|
|
169
|
-
claims: capped,
|
|
170
|
-
records: recs.slice(-MAX_BRIEF_RECORDS).map((r) => ({ id: r.id, type: r.type, title: r.title, date: r.date })),
|
|
171
|
-
stats: {
|
|
172
|
-
records: recs.length,
|
|
173
|
-
mentions: subject.mentions,
|
|
174
|
-
claims: capped.length,
|
|
175
|
-
first: dates.length ? Math.min(...dates) : 0,
|
|
176
|
-
last: dates.length ? Math.max(...dates) : 0,
|
|
177
|
-
wanted: !!wanted,
|
|
178
|
-
},
|
|
179
|
-
createdAt: now,
|
|
180
|
-
updatedAt: now,
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* The whole corpus → the briefs it earns. This IS invariant I-K2: it is the only way a
|
|
186
|
-
* brief is ever created, so throwing them all away can never lose anything.
|
|
187
|
-
*
|
|
188
|
-
* `now` is injected rather than read, so a rebuild is reproducible and testable — the same
|
|
189
|
-
* rule loop.js and schedule.js follow.
|
|
190
|
-
*/
|
|
191
|
-
export function deriveBriefs(records = [], {
|
|
192
|
-
memories = [], merges = null, self = '', threshold = DEFAULT_THRESHOLD,
|
|
193
|
-
limit = MAX_SUBJECTS, now = 0,
|
|
194
|
-
} = {}) {
|
|
195
|
-
const recs = normalizeRecords(records);
|
|
196
|
-
const byId = new Map(recs.map((r) => [r.id, r]));
|
|
197
|
-
const mentions = mentionsFrom(recs);
|
|
198
|
-
// `merges` and `self` are corrections the USER made, and they are applied here — as inputs
|
|
199
|
-
// to the pass, never as edits to its output. That is what keeps I-K2 true: a rebuild
|
|
200
|
-
// re-applies them, where a rebuild that erased them would teach the user not to bother.
|
|
201
|
-
const subjects = resolveSubjects(mentions, { merges, self });
|
|
202
|
-
const ranked = rankSubjects(subjects, { threshold, limit });
|
|
203
|
-
|
|
204
|
-
// Which subjects share records with which — the `together` claim, computed once for the
|
|
205
|
-
// whole corpus rather than per subject, so this stays linear in mentions rather than
|
|
206
|
-
// quadratic in subjects.
|
|
207
|
-
const perRecord = new Map();
|
|
208
|
-
for (const s of ranked) {
|
|
209
|
-
for (const id of s.records) {
|
|
210
|
-
if (!perRecord.has(id)) perRecord.set(id, []);
|
|
211
|
-
perRecord.get(id).push(s.key);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
const pairs = new Map();
|
|
215
|
-
for (const keys of perRecord.values()) {
|
|
216
|
-
for (const a of keys) for (const b of keys) {
|
|
217
|
-
if (a === b) continue;
|
|
218
|
-
const m = pairs.get(a) || new Map();
|
|
219
|
-
m.set(b, (m.get(b) || 0) + 1);
|
|
220
|
-
pairs.set(a, m);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
const byKey = new Map(ranked.map((s) => [s.key, s]));
|
|
224
|
-
const wantedByNorm = new Map(wantedPages(recs).map((w) => [w.norm, w]));
|
|
225
|
-
|
|
226
|
-
const out = [];
|
|
227
|
-
for (const s of ranked) {
|
|
228
|
-
const co = [...(pairs.get(s.key) || new Map()).entries()]
|
|
229
|
-
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
230
|
-
.map(([key, count]) => ({ key, count, name: byKey.get(key)?.name || key }))
|
|
231
|
-
.filter((c) => c.count > 1);
|
|
232
|
-
const brief = deriveBrief(s, {
|
|
233
|
-
byId,
|
|
234
|
-
cooccurring: co,
|
|
235
|
-
memories,
|
|
236
|
-
wanted: s.kind === 'title' ? wantedByNorm.get(s.canonical) || null : null,
|
|
237
|
-
now,
|
|
238
|
-
});
|
|
239
|
-
if (brief) out.push(brief);
|
|
240
|
-
}
|
|
241
|
-
return out;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Which refs no longer resolve to what they cited.
|
|
246
|
-
*
|
|
247
|
-
* This is where `ref.js`'s `drifted` finally earns its existence: a claim saying "cutover
|
|
248
|
-
* moved to Q4, see meeting m_8812" is worthless if m_8812 has since been edited into
|
|
249
|
-
* something else, and a maintenance pass that cannot tell is a maintenance pass that lies.
|
|
250
|
-
*/
|
|
251
|
-
export function driftedRefs(brief, records = []) {
|
|
252
|
-
const hashes = new Map();
|
|
253
|
-
for (const r of normalizeRecords(records)) {
|
|
254
|
-
const [kind, ...rest] = r.id.split(':');
|
|
255
|
-
hashes.set(`${kind}:${rest.join(':') || r.id}`, contentHash(r.text));
|
|
256
|
-
}
|
|
257
|
-
const out = [];
|
|
258
|
-
for (const c of brief?.claims || []) {
|
|
259
|
-
for (const ref of c.refs || []) {
|
|
260
|
-
if (ref.kind === 'memory') continue; // memories are not records; they drift by edit, not by hash
|
|
261
|
-
const now = hashes.get(`${ref.kind}:${ref.id}`);
|
|
262
|
-
if (now === undefined) out.push({ claim: c.id, ref, resolution: 'verified-but-unavailable' });
|
|
263
|
-
else if (now !== ref.hash) out.push({ claim: c.id, ref, resolution: 'drifted' });
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
return out;
|
|
267
|
-
}
|
|
1
|
+
import{makeRef as C}from"./ref.js";import{DEFAULT_THRESHOLD as _,MAX_SUBJECTS as w,normalizeSubject as T,rankSubjects as b,resolveSubjects as L}from"./entity.js";import{mentionsFrom as D,normalizeRecords as E,wantedPages as F}from"./curate.js";import{MAX_CLAIMS as O,MAX_CLAIM_REFS as A,MAX_BRIEF_RECORDS as H,briefId as X,contentHash as v}from"./knowledge.js";function x(e){const[r,...d]=String(e.id).split(":"),c=d.join(":")||e.id,o=r==="chat"||r==="meeting"||r==="note"||r==="page";return C({kind:o?r:"result",id:o?c:e.id,hash:v(e.text)})}function R({id:e,kind:r,text:d,refs:c,at:o=0,confidence:n=1}){return{id:e,kind:r,text:d,refs:c.slice(0,A),firstSeen:o,lastConfirmed:o,confidence:n,cls:"R"}}const B=(e,r,d=`${r}s`)=>`${e} ${e===1?r:d}`,I=e=>e?new Date(e).toISOString().slice(0,10):"",N=5;function U(e,r,d){const c=[],o=[e.canonical,...e.aliases].filter(Boolean);for(const n of r){if(!n?.text||!n.id||n.kind==="identity"||n.kind==="preference")continue;const s=T(n.text);o.some(m=>m&&(s===m||s.includes(` ${m} `)||s.startsWith(`${m} `)||s.endsWith(` ${m}`)))&&c.push(R({id:`${d()}`,kind:"stated",text:n.text,refs:[C({kind:"memory",id:n.id,hash:v(n.text)})],at:Number(n.updatedAt||n.createdAt)||0,confidence:Number(n.confidence)||1}))}return c}function z(e,{records:r,byId:d,cooccurring:c=[],memories:o=[],wanted:n=null,now:s=0}={}){const a=[...e.records instanceof Set?e.records:e.records||[]].map(i=>d.get(i)).filter(Boolean).sort((i,M)=>(i.date||0)-(M.date||0));if(!a.length&&!n)return null;let m=0;const k=()=>(m+=1,`c${m}`),u=[];if(a.length){const i={};for(const t of a)i[t.type]=(i[t.type]||0)+1;const M=Object.entries(i).sort((t,f)=>f[1]-t[1]).map(([t,f])=>B(f,t));u.push(R({id:k(),kind:"presence",text:`Appears in ${B(a.length,"record")} — ${M.join(", ")}.`,refs:a.slice(-A).reverse().map(x),at:s}));const g=a.find(t=>t.date),h=[...a].reverse().find(t=>t.date);g&&h&&g!==h&&u.push(R({id:k(),kind:"timeline",text:`Runs from ${I(g.date)} to ${I(h.date)} — ${g.title||"untitled"} → ${h.title||"untitled"}.`,refs:[x(g),x(h)],at:s}))}c.length&&u.push(R({id:k(),kind:"together",text:`Usually alongside ${c.slice(0,N).map(i=>i.name).join(", ")}.`,refs:a.slice(-A).map(x),at:s})),n&&u.push(R({id:k(),kind:"wanted",text:`Linked from ${B(n.recordCount,"record")} but no record carries this title.`,refs:a.slice(0,A).map(x),at:s})),u.push(...U(e,o,k));const y=u.slice(0,O),p=a.map(i=>i.date||0).filter(Boolean);return{id:X(e.key),key:e.key,kind:e.kind,subject:{name:e.name,aliases:[...e.aliases||[]]},state:"promoted",cls:"R",claims:y,records:a.slice(-H).map(i=>({id:i.id,type:i.type,title:i.title,date:i.date})),stats:{records:a.length,mentions:e.mentions,claims:y.length,first:p.length?Math.min(...p):0,last:p.length?Math.max(...p):0,wanted:!!n},createdAt:s,updatedAt:s}}function K(e=[],{memories:r=[],merges:d=null,self:c="",threshold:o=_,limit:n=w,now:s=0}={}){const a=E(e),m=new Map(a.map(t=>[t.id,t])),k=D(a),u=L(k,{merges:d,self:c}),y=b(u,{threshold:o,limit:n}),p=new Map;for(const t of y)for(const f of t.records)p.has(f)||p.set(f,[]),p.get(f).push(t.key);const i=new Map;for(const t of p.values())for(const f of t)for(const $ of t){if(f===$)continue;const l=i.get(f)||new Map;l.set($,(l.get($)||0)+1),i.set(f,l)}const M=new Map(y.map(t=>[t.key,t])),g=new Map(F(a).map(t=>[t.norm,t])),h=[];for(const t of y){const f=[...(i.get(t.key)||new Map).entries()].sort((l,S)=>S[1]-l[1]||l[0].localeCompare(S[0])).map(([l,S])=>({key:l,count:S,name:M.get(l)?.name||l})).filter(l=>l.count>1),$=z(t,{byId:m,cooccurring:f,memories:r,wanted:t.kind==="title"&&g.get(t.canonical)||null,now:s});$&&h.push($)}return h}function P(e,r=[]){const d=new Map;for(const o of E(r)){const[n,...s]=o.id.split(":");d.set(`${n}:${s.join(":")||o.id}`,v(o.text))}const c=[];for(const o of e?.claims||[])for(const n of o.refs||[]){if(n.kind==="memory")continue;const s=d.get(`${n.kind}:${n.id}`);s===void 0?c.push({claim:o.id,ref:n,resolution:"verified-but-unavailable"}):s!==n.hash&&c.push({claim:o.id,ref:n,resolution:"drifted"})}return c}export{z as deriveBrief,K as deriveBriefs,P as driftedRefs};
|
package/knowledge.js
CHANGED
|
@@ -1,230 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
// happened, a human wrote a note. Nothing is a synthesis, so every answer is re-derived
|
|
5
|
-
// from scratch, every session, forever — and a multi-agent run's findings die with the run.
|
|
6
|
-
// A brief is the thing that compounds.
|
|
7
|
-
//
|
|
8
|
-
// FOUR INVARIANTS, and they are the whole defence (see docs/knowledge-compounding.md §5.1):
|
|
9
|
-
//
|
|
10
|
-
// I-K1 Every claim cites raw. A claim with no ref is a BUG, not a weak claim — it is
|
|
11
|
-
// refused on write. Derived text can then always be checked against, or rebuilt
|
|
12
|
-
// from, the immutable records under it.
|
|
13
|
-
// I-K2 A brief is REBUILDABLE. Delete every brief and this module reconstructs them from
|
|
14
|
-
// the record store. That makes a brief a projection — the same guarantee replay()
|
|
15
|
-
// gives the event log — and it makes "rebuild all" a cache clear, not data loss.
|
|
16
|
-
// I-K3 Nothing self-promotes. `draft` is free; `promoted` needs a gate. Class-R
|
|
17
|
-
// derivations may auto-promote (a backlink is not an opinion); model-written prose
|
|
18
|
-
// may not. Unreviewed agent writing compounding into confident nonsense is the
|
|
19
|
-
// failure mode that is undetectable six months later.
|
|
20
|
-
// I-K4 Bounded, or it is a second corpus. A brief has a size ceiling and the SET of
|
|
21
|
-
// briefs has a count ceiling driven by evidence — memory.js already made this
|
|
22
|
-
// argument for memory, and the same sentence applies here.
|
|
23
|
-
//
|
|
24
|
-
// This phase (W1) is entirely class R: no model, no network, no clock of its own. Every
|
|
25
|
-
// claim is something the corpus already states — who was present, when, what co-occurs,
|
|
26
|
-
// what the user themselves told us. Prose synthesis arrives in W3, behind the gate, and
|
|
27
|
-
// lands as `proposed` beside these rather than replacing them.
|
|
28
|
-
//
|
|
29
|
-
// WHY THIS FILE IS THE MODEL AND `knowledge-derive.js` IS THE PASS. Reading a brief and
|
|
30
|
-
// BUILDING one have very different costs: reading needs the shape and the renderer, while
|
|
31
|
-
// building walks the whole corpus and needs entity resolution and the maintenance passes.
|
|
32
|
-
// The MV3 service worker only ever reads — it syncs stored briefs to the gateway — so
|
|
33
|
-
// putting both halves in one module would have put 60 KB of derivation on its cold start
|
|
34
|
-
// for code it never runs. The split is what keeps that honest rather than remembered.
|
|
35
|
-
|
|
36
|
-
// From subject-name.js, not entity.js: this module is on the MV3 service worker's graph and
|
|
37
|
-
// needs exactly one string function, where entity.js also carries alias resolution and merge
|
|
38
|
-
// suggestion. Same argument as the knowledge/knowledge-derive split, one level down.
|
|
39
|
-
import { normalizeSubject } from './subject-name.js';
|
|
40
|
-
|
|
41
|
-
/** draft → proposed → promoted → archived. `promotion.js` (W3) owns the transitions. */
|
|
42
|
-
export const BRIEF_STATES = Object.freeze(['draft', 'proposed', 'promoted', 'archived']);
|
|
43
|
-
|
|
44
|
-
/** What a claim is derived FROM. Class R throughout this phase. */
|
|
45
|
-
export const CLAIM_KINDS = Object.freeze(['presence', 'timeline', 'together', 'wanted', 'stated']);
|
|
46
|
-
|
|
47
|
-
// I-K4, made concrete. A brief that grows without bound is a document, and a document
|
|
48
|
-
// needs its own summary, and then nothing has been gained.
|
|
49
|
-
export const MAX_CLAIMS = 12;
|
|
50
|
-
export const MAX_CLAIM_REFS = 8;
|
|
51
|
-
export const MAX_BRIEF_RECORDS = 200;
|
|
52
|
-
export const MAX_BRIEF_CHARS = 4000;
|
|
53
|
-
|
|
54
|
-
// How many co-occurring subjects a `together` claim names. Past a handful it stops being a
|
|
55
|
-
// statement and becomes a tag cloud.
|
|
56
|
-
const TOGETHER_LIMIT = 5;
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* A stable, non-cryptographic content hash, computed SYNCHRONOUSLY.
|
|
60
|
-
*
|
|
61
|
-
* Deliberately not SHA-256, which `store.js` uses and which is async: derivation walks
|
|
62
|
-
* every record in the corpus and runs in an MV3 service worker, so a hash per record has to
|
|
63
|
-
* be synchronous or the whole pass becomes a promise storm. The job here is DRIFT
|
|
64
|
-
* DETECTION — "has the record this claim cites changed since the claim was made" — not
|
|
65
|
-
* tamper resistance, and FNV-1a answers that exactly as well while staying pure.
|
|
66
|
-
*/
|
|
67
|
-
export function contentHash(text) {
|
|
68
|
-
const s = String(text ?? '');
|
|
69
|
-
let h1 = 0x811c9dc5, h2 = 0x01000193;
|
|
70
|
-
for (let i = 0; i < s.length; i += 1) {
|
|
71
|
-
const c = s.charCodeAt(i);
|
|
72
|
-
h1 = Math.imul(h1 ^ c, 0x01000193) >>> 0;
|
|
73
|
-
h2 = Math.imul(h2 ^ (c + i), 0x85ebca6b) >>> 0;
|
|
74
|
-
}
|
|
75
|
-
return `f${h1.toString(16).padStart(8, '0')}${h2.toString(16).padStart(8, '0')}`;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** `person:alex rivera` → `brief:person-alex-rivera`. Safe as a storage key and a URL hash. */
|
|
79
|
-
export function briefId(subjectKey) {
|
|
80
|
-
const [kind, ...rest] = String(subjectKey || '').split(':');
|
|
81
|
-
const slug = normalizeSubject(rest.join(':')).replace(/\s+/g, '-').replace(/-+/g, '-');
|
|
82
|
-
if (!kind || !slug) return '';
|
|
83
|
-
// The slug is TRUNCATED, so two long subjects can share one, and it is lossy, so two
|
|
84
|
-
// kinds can too. A hash of the CANONICAL key rides along to separate them. Canonical, not
|
|
85
|
-
// raw: "Alex Rivera" and "alex rivera" are one subject and must land on one id, or a
|
|
86
|
-
// rebuild would fork the page in two — the exact failure I-K2 exists to make impossible.
|
|
87
|
-
const canonical = `${kind}:${normalizeSubject(rest.join(':'))}`;
|
|
88
|
-
return `brief:${kind}-${slug.slice(0, 48)}-${contentHash(canonical).slice(1, 7)}`;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** Records are addressed `chat:x` / `meeting:y` / `note:z` — the ref kind is the prefix. */
|
|
92
|
-
function refForRecord(rec) {
|
|
93
|
-
const [kind, ...rest] = String(rec.id).split(':');
|
|
94
|
-
const id = rest.join(':') || rec.id;
|
|
95
|
-
const known = kind === 'chat' || kind === 'meeting' || kind === 'note' || kind === 'page';
|
|
96
|
-
return makeRef({ kind: known ? kind : 'result', id: known ? id : rec.id, hash: contentHash(rec.text) });
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function claim({ id, kind, text, refs, at = 0, confidence = 1 }) {
|
|
100
|
-
return {
|
|
101
|
-
id,
|
|
102
|
-
kind,
|
|
103
|
-
text,
|
|
104
|
-
// I-K1 lives here: a claim is CONSTRUCTED with its refs, and `checkKnowledgeInvariants`
|
|
105
|
-
// refuses one that arrives without them. There is no path that writes a claim first and
|
|
106
|
-
// attaches provenance later, because that path is how provenance goes missing.
|
|
107
|
-
refs: refs.slice(0, MAX_CLAIM_REFS),
|
|
108
|
-
firstSeen: at,
|
|
109
|
-
lastConfirmed: at,
|
|
110
|
-
confidence,
|
|
111
|
-
cls: 'R', // class R — derived, not written. W3's prose claims carry 'C'.
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
116
|
-
const isoDay = (ms) => (ms ? new Date(ms).toISOString().slice(0, 10) : '');
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* The searchable body. A brief is a SOURCE, ranked by the same engine as everything else
|
|
120
|
-
* (design §7.6 — no second retrieval stack), so it has to render to text like one.
|
|
121
|
-
*/
|
|
122
|
-
export function briefToText(brief) {
|
|
123
|
-
if (!brief?.subject?.name) return '';
|
|
124
|
-
// Every collection is read DEFENSIVELY. This used to assume the shape its own deriver
|
|
125
|
-
// produces, which was true while the only caller was that deriver — and stopped being true
|
|
126
|
-
// the moment briefs started arriving from a backup file, where a field can be absent
|
|
127
|
-
// because it was written by an older build. A missing `records` threw and took the whole
|
|
128
|
-
// restore's brief section with it.
|
|
129
|
-
const L = [`BRIEF: ${brief.subject.name}`];
|
|
130
|
-
const aliases = brief.subject.aliases;
|
|
131
|
-
if (Array.isArray(aliases) && aliases.length) L.push(`Also known as: ${aliases.join(', ')}`);
|
|
132
|
-
L.push(`Kind: ${brief.kind || 'topic'}`);
|
|
133
|
-
L.push('');
|
|
134
|
-
for (const c of brief.claims || []) {
|
|
135
|
-
if (!c?.text) continue;
|
|
136
|
-
L.push(`- ${c.supersededBy ? '[superseded] ' : ''}${c.text}`);
|
|
137
|
-
const refs = Array.isArray(c.refs) ? c.refs : [];
|
|
138
|
-
if (refs.length) L.push(` (${refs.map((r) => `${r.kind}:${r.id}`).join(', ')})`);
|
|
139
|
-
}
|
|
140
|
-
const records = Array.isArray(brief.records) ? brief.records : [];
|
|
141
|
-
if (records.length) {
|
|
142
|
-
L.push('', 'RECORDS:');
|
|
143
|
-
for (const r of records.slice(-40).reverse()) L.push(`- ${r.type}: ${r.title || 'untitled'}`);
|
|
144
|
-
}
|
|
145
|
-
return L.join('\n').slice(0, MAX_BRIEF_CHARS);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* The inverse of `briefToText` — a brief's claims and refs read back out of the text form.
|
|
150
|
-
*
|
|
151
|
-
* Exists because the warm store holds RECORDS: `{ id, title, type, date, text }`, nothing
|
|
152
|
-
* else. Briefs cross to the gateway as that shape, so an agent asking `get_brief` over MCP
|
|
153
|
-
* can only be handed structure if the text form is stable enough to parse. It is: this
|
|
154
|
-
* module writes both ends, and the claim line (`- text`) followed by its refs
|
|
155
|
-
* (` (kind:id, kind:id)`) is a grammar, not a rendering. Round-trips in the tests.
|
|
156
|
-
*
|
|
157
|
-
* Returns `null` for text that is not a brief, so a caller can tell "not a brief" from
|
|
158
|
-
* "a brief with no claims".
|
|
159
|
-
*/
|
|
160
|
-
export function parseBriefText(text) {
|
|
161
|
-
const lines = String(text ?? '').split('\n');
|
|
162
|
-
if (!/^BRIEF: /.test(lines[0] || '')) return null;
|
|
163
|
-
const out = { name: lines[0].slice('BRIEF: '.length).trim(), aliases: [], kind: '', claims: [], records: [] };
|
|
164
|
-
let section = 'head';
|
|
165
|
-
for (let i = 1; i < lines.length; i += 1) {
|
|
166
|
-
const line = lines[i];
|
|
167
|
-
if (section === 'head') {
|
|
168
|
-
if (line.startsWith('Also known as: ')) out.aliases = line.slice(15).split(',').map((a) => a.trim()).filter(Boolean);
|
|
169
|
-
else if (line.startsWith('Kind: ')) out.kind = line.slice(6).trim();
|
|
170
|
-
else if (line === '') section = 'claims';
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
if (line === 'RECORDS:') { section = 'records'; continue; }
|
|
174
|
-
if (section === 'claims' && line.startsWith('- ')) {
|
|
175
|
-
const claim = { text: line.slice(2), refs: [] };
|
|
176
|
-
const next = lines[i + 1] || '';
|
|
177
|
-
const m = /^ \((.*)\)$/.exec(next);
|
|
178
|
-
if (m) {
|
|
179
|
-
claim.refs = m[1].split(', ').map((r) => {
|
|
180
|
-
const idx = r.indexOf(':');
|
|
181
|
-
return idx > 0 ? { kind: r.slice(0, idx), id: r.slice(idx + 1) } : null;
|
|
182
|
-
}).filter(Boolean);
|
|
183
|
-
i += 1;
|
|
184
|
-
}
|
|
185
|
-
out.claims.push(claim);
|
|
186
|
-
} else if (section === 'records' && line.startsWith('- ')) {
|
|
187
|
-
const idx = line.indexOf(': ');
|
|
188
|
-
out.records.push(idx > 0 ? { type: line.slice(2, idx), title: line.slice(idx + 2) } : { type: '', title: line.slice(2) });
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
return out;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/** The other briefs this brief's accepted claims link to — the backlink graph's edges. */
|
|
195
|
-
export function briefLinks(brief) {
|
|
196
|
-
const out = new Set();
|
|
197
|
-
for (const c of brief?.claims || []) for (const r of c.refs || []) if (r.kind === 'brief' && r.id) out.add(r.id);
|
|
198
|
-
return [...out];
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/** Terms the graph and the search index rank a brief by — its subject and its neighbours. */
|
|
202
|
-
export function briefTerms(brief) {
|
|
203
|
-
if (!brief) return [];
|
|
204
|
-
const together = brief.claims.find((c) => c.kind === 'together');
|
|
205
|
-
const names = together ? together.text.replace(/^Usually alongside /, '').replace(/\.$/, '').split(', ') : [];
|
|
206
|
-
return [...new Set([brief.subject.name, ...(brief.subject.aliases || []), ...names])].filter(Boolean);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* The invariants, as a check rather than a promise. Returns the failures; empty means clean.
|
|
211
|
-
* Same shape as `invariants.js checkInvariants()`, for the same reason: an invariant nobody
|
|
212
|
-
* can run is a comment.
|
|
213
|
-
*/
|
|
214
|
-
export function checkKnowledgeInvariants(brief) {
|
|
215
|
-
const fail = [];
|
|
216
|
-
if (!brief || typeof brief !== 'object') return [{ invariant: 'I-K1', detail: 'not a brief' }];
|
|
217
|
-
if (!BRIEF_STATES.includes(brief.state)) fail.push({ invariant: 'I-K3', detail: `unknown state ${brief.state}` });
|
|
218
|
-
if (brief.state === 'promoted' && brief.cls === 'C') {
|
|
219
|
-
fail.push({ invariant: 'I-K3', detail: 'model-written prose cannot be promoted without the gate' });
|
|
220
|
-
}
|
|
221
|
-
for (const c of brief.claims || []) {
|
|
222
|
-
if (!c.refs?.length) fail.push({ invariant: 'I-K1', detail: `claim ${c.id} cites nothing` });
|
|
223
|
-
if (!CLAIM_KINDS.includes(c.kind) && c.cls !== 'C') {
|
|
224
|
-
fail.push({ invariant: 'I-K1', detail: `claim ${c.id} has unknown kind ${c.kind}` });
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
if ((brief.claims || []).length > MAX_CLAIMS) fail.push({ invariant: 'I-K4', detail: `${brief.claims.length} claims exceeds ${MAX_CLAIMS}` });
|
|
228
|
-
if (briefToText(brief).length >= MAX_BRIEF_CHARS) fail.push({ invariant: 'I-K4', detail: 'brief text is at the ceiling' });
|
|
229
|
-
return fail;
|
|
230
|
-
}
|
|
1
|
+
import{normalizeSubject as d}from"./subject-name.js";const m=Object.freeze(["draft","proposed","promoted","archived"]),x=Object.freeze(["presence","timeline","together","wanted","stated"]),u=12,g=8,I=200,f=4e3,k=5;function p(e){const t=String(e??"");let n=2166136261,s=16777619;for(let i=0;i<t.length;i+=1){const o=t.charCodeAt(i);n=Math.imul(n^o,16777619)>>>0,s=Math.imul(s^o+i,2246822507)>>>0}return`f${n.toString(16).padStart(8,"0")}${s.toString(16).padStart(8,"0")}`}function A(e){const[t,...n]=String(e||"").split(":"),s=d(n.join(":")).replace(/\s+/g,"-").replace(/-+/g,"-");if(!t||!s)return"";const i=`${t}:${d(n.join(":"))}`;return`brief:${t}-${s.slice(0,48)}-${p(i).slice(1,7)}`}function R(e){const[t,...n]=String(e.id).split(":"),s=n.join(":")||e.id,i=t==="chat"||t==="meeting"||t==="note"||t==="page";return makeRef({kind:i?t:"result",id:i?s:e.id,hash:p(e.text)})}function j({id:e,kind:t,text:n,refs:s,at:i=0,confidence:o=1}){return{id:e,kind:t,text:n,refs:s.slice(0,g),firstSeen:i,lastConfirmed:i,confidence:o,cls:"R"}}const y=(e,t,n=`${t}s`)=>`${e} ${e===1?t:n}`,E=e=>e?new Date(e).toISOString().slice(0,10):"";function $(e){if(!e?.subject?.name)return"";const t=[`BRIEF: ${e.subject.name}`],n=e.subject.aliases;Array.isArray(n)&&n.length&&t.push(`Also known as: ${n.join(", ")}`),t.push(`Kind: ${e.kind||"topic"}`),t.push("");for(const i of e.claims||[]){if(!i?.text)continue;t.push(`- ${i.supersededBy?"[superseded] ":""}${i.text}`);const o=Array.isArray(i.refs)?i.refs:[];o.length&&t.push(` (${o.map(r=>`${r.kind}:${r.id}`).join(", ")})`)}const s=Array.isArray(e.records)?e.records:[];if(s.length){t.push("","RECORDS:");for(const i of s.slice(-40).reverse())t.push(`- ${i.type}: ${i.title||"untitled"}`)}return t.join(`
|
|
2
|
+
`).slice(0,f)}function w(e){const t=String(e??"").split(`
|
|
3
|
+
`);if(!/^BRIEF: /.test(t[0]||""))return null;const n={name:t[0].slice(7).trim(),aliases:[],kind:"",claims:[],records:[]};let s="head";for(let i=1;i<t.length;i+=1){const o=t[i];if(s==="head"){o.startsWith("Also known as: ")?n.aliases=o.slice(15).split(",").map(r=>r.trim()).filter(Boolean):o.startsWith("Kind: ")?n.kind=o.slice(6).trim():o===""&&(s="claims");continue}if(o==="RECORDS:"){s="records";continue}if(s==="claims"&&o.startsWith("- ")){const r={text:o.slice(2),refs:[]},h=t[i+1]||"",l=/^ \((.*)\)$/.exec(h);l&&(r.refs=l[1].split(", ").map(c=>{const a=c.indexOf(":");return a>0?{kind:c.slice(0,a),id:c.slice(a+1)}:null}).filter(Boolean),i+=1),n.claims.push(r)}else if(s==="records"&&o.startsWith("- ")){const r=o.indexOf(": ");n.records.push(r>0?{type:o.slice(2,r),title:o.slice(r+2)}:{type:"",title:o.slice(2)})}}return n}function B(e){const t=new Set;for(const n of e?.claims||[])for(const s of n.refs||[])s.kind==="brief"&&s.id&&t.add(s.id);return[...t]}function C(e){if(!e)return[];const t=e.claims.find(s=>s.kind==="together"),n=t?t.text.replace(/^Usually alongside /,"").replace(/\.$/,"").split(", "):[];return[...new Set([e.subject.name,...e.subject.aliases||[],...n])].filter(Boolean)}function K(e){const t=[];if(!e||typeof e!="object")return[{invariant:"I-K1",detail:"not a brief"}];m.includes(e.state)||t.push({invariant:"I-K3",detail:`unknown state ${e.state}`}),e.state==="promoted"&&e.cls==="C"&&t.push({invariant:"I-K3",detail:"model-written prose cannot be promoted without the gate"});for(const n of e.claims||[])n.refs?.length||t.push({invariant:"I-K1",detail:`claim ${n.id} cites nothing`}),!x.includes(n.kind)&&n.cls!=="C"&&t.push({invariant:"I-K1",detail:`claim ${n.id} has unknown kind ${n.kind}`});return(e.claims||[]).length>u&&t.push({invariant:"I-K4",detail:`${e.claims.length} claims exceeds ${u}`}),$(e).length>=f&&t.push({invariant:"I-K4",detail:"brief text is at the ceiling"}),t}export{m as BRIEF_STATES,x as CLAIM_KINDS,f as MAX_BRIEF_CHARS,I as MAX_BRIEF_RECORDS,u as MAX_CLAIMS,g as MAX_CLAIM_REFS,A as briefId,B as briefLinks,C as briefTerms,$ as briefToText,K as checkKnowledgeInvariants,p as contentHash,w as parseBriefText};
|