@chatpanel/events 0.33.1 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/library.js ADDED
@@ -0,0 +1,265 @@
1
+ // THE LIBRARY — one record model for chats, notes, meetings and briefs.
2
+ //
3
+ // WHY THIS IS IN THE SHARED PACKAGE. "What is a record" is currently answered three times
4
+ // and the three answers already disagree:
5
+ //
6
+ // • the extension builds rich source records (conversationSource / meetingSource /
7
+ // noteSource / briefSource), each with its own title, date and body rules;
8
+ // • the gateway re-derives them in `backup-ingest.js`, whose own comment calls it "a
9
+ // SIMPLIFIED MIRROR of the extension's" — a mirror is a copy that drifts;
10
+ // • a desktop or mobile client would make a third, and three implementations of "what is
11
+ // the title of a meeting" become three different titles for the same meeting.
12
+ //
13
+ // The projection is pure: records in, a normalized record out. No storage, no crypto, no
14
+ // platform. Each host keeps its own persistence (IndexedDB, SQLite, Core Data) and calls
15
+ // this to decide WHAT it is storing — the P8 split the store already uses.
16
+ //
17
+ // THE ID GRAMMAR IS THE CONTRACT. `chat:<id>`, `note:<id>`, `meeting:<id>`, `brief:<id>`.
18
+ // The gateway's warm index, the MCP tools, `find_related`, the omni bar and every citation
19
+ // already speak it; writing it down here is what stops a fourth client inventing
20
+ // `chats/<id>` and silently failing to join.
21
+ //
22
+ // NAMED `normalizeStoredRecord`, NOT `normalizeStoredRecord`, on purpose: `curate.js` already
23
+ // exports the latter and it does the OTHER job — coercing anything into the flat survey
24
+ // shape a curation pass reads. Two functions named the same in one package, one lossy and
25
+ // one lossless, is a trap for whoever imports the wrong one.
26
+ //
27
+ // LOSSY AND LOSSLESS ARE DIFFERENT QUESTIONS, and conflating them is what made the gateway's
28
+ // mirror drift. `toSearchRecord` is deliberately lossy — it is the flat {id,title,type,date,
29
+ // text} the BM25 index wants. `normalizeStoredRecord` is lossless — it is the full record a client
30
+ // stores and round-trips. A client that needs to SHOW a chat must never reconstruct it from
31
+ // a search record.
32
+
33
+ export class LibraryError extends Error {
34
+ constructor(message) { super(message); this.name = 'LibraryError'; }
35
+ }
36
+
37
+ /** The four record kinds. Ordered as the UI orders them, not alphabetically. */
38
+ export const RECORD_KINDS = Object.freeze(['chat', 'note', 'meeting', 'brief']);
39
+
40
+ const KIND_SET = new Set(RECORD_KINDS);
41
+
42
+ /** `chat:abc` → { kind:'chat', localId:'abc' }; anything else → null. */
43
+ export function parseRecordId(recordId) {
44
+ const s = String(recordId || '');
45
+ const colon = s.indexOf(':');
46
+ if (colon <= 0) return null;
47
+ const kind = s.slice(0, colon);
48
+ const localId = s.slice(colon + 1);
49
+ if (!KIND_SET.has(kind) || !localId) return null;
50
+ return { kind, localId };
51
+ }
52
+
53
+ /** The inverse. Throws rather than producing an id nothing else will match. */
54
+ export function makeRecordId(kind, localId) {
55
+ if (!KIND_SET.has(kind)) throw new LibraryError(`unknown record kind: ${kind}`);
56
+ const id = String(localId || '');
57
+ if (!id) throw new LibraryError('a record id needs a local id');
58
+ if (id.includes(':')) throw new LibraryError(`local id may not contain ":" — got ${id}`);
59
+ return `${kind}:${id}`;
60
+ }
61
+
62
+ export function isRecordId(value) {
63
+ return parseRecordId(value) !== null;
64
+ }
65
+
66
+ // --------------------------------------------------------------------------
67
+ // Titles
68
+ // --------------------------------------------------------------------------
69
+
70
+ /** The single length a record title may be — the extension's MAX_TITLE_LEN, shared. */
71
+ export const MAX_TITLE_LEN = 48;
72
+
73
+ /**
74
+ * First non-empty line, stripped of markdown furniture, clamped.
75
+ *
76
+ * Notes derive their title from the body when the user has not set one, and the meeting
77
+ * autotitler falls back here too. Keeping it in one place is what stops "# Rollback runbook"
78
+ * becoming the title in one client and "Rollback runbook" in another.
79
+ */
80
+ export function deriveTitle(body, fallback = 'Untitled') {
81
+ const first = String(body || '')
82
+ .split('\n')
83
+ .map((l) => l.replace(/^\s*#{1,6}\s*/, '').replace(/[*_`>~]+/g, '').trim())
84
+ .find((l) => l.length > 0);
85
+ if (!first) return fallback;
86
+ return first.length > MAX_TITLE_LEN ? `${first.slice(0, MAX_TITLE_LEN - 1).trimEnd()}…` : first;
87
+ }
88
+
89
+ // --------------------------------------------------------------------------
90
+ // Normalization — the lossless shape
91
+ // --------------------------------------------------------------------------
92
+
93
+ const num = (v, dflt = 0) => (Number.isFinite(Number(v)) ? Number(v) : dflt);
94
+ const str = (v) => (typeof v === 'string' ? v : v == null ? '' : String(v));
95
+
96
+ /**
97
+ * A stored record, whatever client wrote it.
98
+ *
99
+ * `body` is the kind-specific payload and is carried VERBATIM — messages for a chat,
100
+ * segments for a meeting, markdown for a note. This module refuses to flatten it, because
101
+ * flattening is exactly the lossy step that belongs in `toSearchRecord` and nowhere else.
102
+ */
103
+ export function normalizeStoredRecord(input = {}, { now = 0 } = {}) {
104
+ const parsed = parseRecordId(input.id);
105
+ if (!parsed) throw new LibraryError(`not a record id: ${JSON.stringify(input.id)}`);
106
+ const createdAt = num(input.createdAt, num(input.date, now));
107
+ return {
108
+ id: input.id,
109
+ kind: parsed.kind,
110
+ localId: parsed.localId,
111
+ title: str(input.title).trim() || defaultTitleFor(parsed.kind, input),
112
+ tags: normalizeTagList(input.tags),
113
+ createdAt,
114
+ // A record with no updatedAt sorts by when it was made, not to the bottom of the list.
115
+ updatedAt: num(input.updatedAt, createdAt),
116
+ // Soft delete. A removal has to be REPRESENTABLE or it cannot replicate: a record that
117
+ // is merely absent from one side is indistinguishable from one that side has not seen.
118
+ deletedAt: num(input.deletedAt, 0) || 0,
119
+ body: input.body ?? null,
120
+ meta: input.meta && typeof input.meta === 'object' ? input.meta : {},
121
+ };
122
+ }
123
+
124
+ function defaultTitleFor(kind, input) {
125
+ if (kind === 'note') return deriveTitle(input?.body?.markdown ?? input?.body, 'Untitled note');
126
+ if (kind === 'meeting') return 'Untitled meeting';
127
+ if (kind === 'brief') return 'Untitled brief';
128
+ return 'New chat';
129
+ }
130
+
131
+ /**
132
+ * Tags are a shared vocabulary — `tags.js` owns the rules. This is the shallow guard for
133
+ * callers that hand us junk; it deliberately does NOT re-implement normalization, so a
134
+ * client that cares passes tags through `normalizeTags` first.
135
+ */
136
+ function normalizeTagList(tags) {
137
+ if (!Array.isArray(tags)) return [];
138
+ const out = [];
139
+ for (const t of tags) {
140
+ const s = str(t).trim();
141
+ if (s && !out.includes(s)) out.push(s);
142
+ }
143
+ return out;
144
+ }
145
+
146
+ export function isValidStoredRecord(rec) {
147
+ try { normalizeStoredRecord(rec); return true; } catch { return false; }
148
+ }
149
+
150
+ // --------------------------------------------------------------------------
151
+ // Projection — the lossy shape the search index wants
152
+ // --------------------------------------------------------------------------
153
+
154
+ const ROLE_LABEL = { assistant: 'Assistant', system: 'System', user: 'You' };
155
+
156
+ /**
157
+ * A record → the flat `{ id, type, title, date, text }` the warm index stores.
158
+ *
159
+ * This is the function the gateway's `backupToRecords` was a copy of. Note what it does NOT
160
+ * do: it never invents an id, never guesses a kind, and never returns a record for something
161
+ * it does not understand — a silent wrong answer in a search index is worse than a gap,
162
+ * because the gap is visible.
163
+ */
164
+ export function toSearchRecord(record) {
165
+ const rec = normalizeStoredRecord(record);
166
+ if (rec.deletedAt) return null;
167
+ return {
168
+ id: rec.id,
169
+ type: rec.kind,
170
+ title: rec.title,
171
+ date: rec.updatedAt || rec.createdAt,
172
+ text: searchTextFor(rec),
173
+ };
174
+ }
175
+
176
+ /** The body of a record as one searchable string. Exported because callers index in batches. */
177
+ export function searchTextFor(record) {
178
+ const rec = record.kind ? record : normalizeStoredRecord(record);
179
+ const head = `${kindLabel(rec.kind)}: ${rec.title}`;
180
+ const tagLine = rec.tags.length ? `\nTags: ${rec.tags.join(', ')}` : '';
181
+ return `${head}${tagLine}\n\n${bodyText(rec)}`.trim();
182
+ }
183
+
184
+ function kindLabel(kind) {
185
+ return kind === 'chat' ? 'CHAT' : kind === 'note' ? 'NOTE' : kind === 'meeting' ? 'MEETING' : 'BRIEF';
186
+ }
187
+
188
+ function bodyText(rec) {
189
+ const b = rec.body;
190
+ if (b == null) return '';
191
+ if (typeof b === 'string') return b;
192
+
193
+ if (rec.kind === 'chat') {
194
+ return (b.messages || [])
195
+ .filter((m) => m && m.content)
196
+ .map((m) => `${ROLE_LABEL[m.role] || 'You'}: ${textOfContent(m.content)}`)
197
+ .join('\n\n');
198
+ }
199
+ if (rec.kind === 'note') return str(b.markdown ?? b.text);
200
+ if (rec.kind === 'meeting') {
201
+ const notes = str(b.notes);
202
+ const segs = (b.segments || [])
203
+ .map((s) => `${str(s.speaker) || '?'}: ${str(s.text)}`)
204
+ .join('\n');
205
+ return [notes, segs].filter(Boolean).join('\n\n');
206
+ }
207
+ if (rec.kind === 'brief') {
208
+ const claims = (b.claims || []).map((c) => str(c.text)).filter(Boolean).join('\n');
209
+ return [str(b.summary), claims].filter(Boolean).join('\n\n');
210
+ }
211
+ return '';
212
+ }
213
+
214
+ /**
215
+ * A message's content may be a string or a multimodal part list. An image part contributes
216
+ * nothing to a text index, and stringifying the object would put `[object Object]` into the
217
+ * corpus — which is not a hypothetical, it is what naive JSON handling does here.
218
+ */
219
+ function textOfContent(content) {
220
+ if (typeof content === 'string') return content;
221
+ if (!Array.isArray(content)) return '';
222
+ return content
223
+ .map((part) => (typeof part === 'string' ? part : part && part.type === 'text' ? str(part.text) : ''))
224
+ .filter(Boolean)
225
+ .join(' ');
226
+ }
227
+
228
+ // --------------------------------------------------------------------------
229
+ // Counting — so every surface reports the same number
230
+ // --------------------------------------------------------------------------
231
+
232
+ /** Words in a note body, by the same rule everywhere. */
233
+ export function wordCount(text) {
234
+ const t = str(text).trim();
235
+ return t ? t.split(/\s+/).length : 0;
236
+ }
237
+
238
+ /** A short preview for a list row, so a list renders without opening every body. */
239
+ export function snippetOf(text, max = 110) {
240
+ const b = str(text);
241
+ const nl = b.indexOf('\n');
242
+ const rest = nl >= 0 ? b.slice(nl + 1) : b;
243
+ return rest.replace(/[#*_`>~]+/g, '').replace(/\s+/g, ' ').trim().slice(0, max);
244
+ }
245
+
246
+ /**
247
+ * The index entry a list view renders from — everything needed to draw a row and nothing
248
+ * that requires reading the body a second time.
249
+ */
250
+ export function toIndexEntry(record) {
251
+ const rec = normalizeStoredRecord(record);
252
+ const text = bodyText(rec);
253
+ return {
254
+ id: rec.id,
255
+ kind: rec.kind,
256
+ title: rec.title,
257
+ tags: rec.tags,
258
+ snippet: snippetOf(text),
259
+ words: wordCount(text),
260
+ chars: text.length,
261
+ createdAt: rec.createdAt,
262
+ updatedAt: rec.updatedAt,
263
+ deletedAt: rec.deletedAt,
264
+ };
265
+ }
package/omni.js ADDED
@@ -0,0 +1,125 @@
1
+ // THE COMMAND BAR — what one line of typing means.
2
+ //
3
+ // Every ChatPanel surface is growing the same input: a single field that searches the
4
+ // corpus, asks the model, jumps to a subject, filters by tag, or runs a command. The
5
+ // extension has an omni modal, the desktop has an ambient composer, mobile will have a
6
+ // search sheet. If each decides on its own what `#atlas` means, muscle memory stops
7
+ // transferring between them — which is the whole value of a command bar.
8
+ //
9
+ // So the GRAMMAR lives here and the surfaces only decide how to paint it. Pure string in,
10
+ // structured intent out. No storage, no search, no model.
11
+ //
12
+ // WHY PREFIXES RATHER THAN A MODE PICKER: a picker costs a click before every query and has
13
+ // to be reset afterwards. A prefix is typed in the same keystroke as the query and is
14
+ // self-evident on screen. The set is deliberately tiny — five modes, each one character —
15
+ // because a grammar nobody can recall is a grammar nobody uses.
16
+ //
17
+ // AMBIGUITY RESOLVES TOWARD SEARCH. Anything unrecognised is a search, never an error: a bar
18
+ // that refuses input is worse than one that searches for a literal "?" .
19
+
20
+ export const OMNI_MODES = Object.freeze(['search', 'ask', 'subject', 'tag', 'command']);
21
+
22
+ /**
23
+ * The grammar. `prefix` is what the user types; `hint` is what a surface shows in the
24
+ * mode strip. Order is display order.
25
+ */
26
+ export const OMNI_GRAMMAR = Object.freeze([
27
+ Object.freeze({ mode: 'search', prefix: '', hint: 'search' }),
28
+ Object.freeze({ mode: 'ask', prefix: '?', hint: 'ask' }),
29
+ Object.freeze({ mode: 'subject', prefix: '@', hint: 'subject' }),
30
+ Object.freeze({ mode: 'tag', prefix: '#', hint: 'tag' }),
31
+ Object.freeze({ mode: 'command', prefix: '>', hint: 'command' }),
32
+ ]);
33
+
34
+ const BY_PREFIX = new Map(OMNI_GRAMMAR.filter((g) => g.prefix).map((g) => [g.prefix, g.mode]));
35
+
36
+ /**
37
+ * Filters a surface may apply inside a search. Kept here so `type:note since:7d` means the
38
+ * same thing in the extension's omni and the desktop's bar — and the same thing the
39
+ * gateway's `search_history` already accepts.
40
+ */
41
+ const FILTER_KEYS = new Set(['type', 'since', 'before', 'in', 'limit']);
42
+
43
+ /**
44
+ * Parse one line.
45
+ *
46
+ * Returns `{ mode, query, prefix, filters, raw }`. `query` has the prefix and any recognised
47
+ * `key:value` filters removed, so it is ready to hand to a search or a model verbatim.
48
+ */
49
+ export function parseOmni(input) {
50
+ const raw = String(input ?? '');
51
+ const trimmed = raw.trimStart();
52
+ const first = trimmed.slice(0, 1);
53
+ const mode = BY_PREFIX.get(first) || 'search';
54
+ const prefix = mode === 'search' ? '' : first;
55
+
56
+ // Only strip the prefix when it IS one — a search for "#" alone should still search.
57
+ let rest = prefix ? trimmed.slice(1) : trimmed;
58
+ rest = rest.replace(/^\s+/, '');
59
+
60
+ const { query, filters } = extractFilters(rest);
61
+ return { mode, prefix, query, filters, raw };
62
+ }
63
+
64
+ /**
65
+ * Pull `key:value` pairs out of a query.
66
+ *
67
+ * A value may be quoted (`in:"cutover review"`). Unknown keys are LEFT IN the query rather
68
+ * than dropped, because `http://example.com` and `note:` typed by mistake are both far more
69
+ * likely than a user inventing a filter we forgot to implement.
70
+ */
71
+ export function extractFilters(text) {
72
+ const filters = {};
73
+ const kept = [];
74
+ // The `key:` prefix has to be part of the quoted alternative. Without it `\S+` matches
75
+ // `in:"cutover` first and the value loses everything after its first space.
76
+ const tokens = String(text || '').match(/(?:[A-Za-z]+:)?"[^"]*"|\S+/g) || [];
77
+
78
+ for (const tok of tokens) {
79
+ const m = /^([a-z]+):(.*)$/i.exec(tok);
80
+ if (m && FILTER_KEYS.has(m[1].toLowerCase())) {
81
+ const key = m[1].toLowerCase();
82
+ const value = m[2].replace(/^"|"$/g, '');
83
+ if (value) filters[key] = value;
84
+ continue;
85
+ }
86
+ kept.push(tok.replace(/^"|"$/g, ''));
87
+ }
88
+ return { query: kept.join(' ').trim(), filters };
89
+ }
90
+
91
+ /**
92
+ * A relative duration (`7d`, `24h`, `30m`) or a date, resolved against an injected `now`.
93
+ *
94
+ * `now` is a parameter for the reason it is one in `loop.js`: a function that reads the
95
+ * clock cannot be tested, and "since 7d" must mean the same span in every client.
96
+ */
97
+ export function resolveSince(value, now = Date.now()) {
98
+ const s = String(value || '').trim().toLowerCase();
99
+ if (!s) return 0;
100
+ if (s === 'today') { const d = new Date(now); d.setHours(0, 0, 0, 0); return d.getTime(); }
101
+ if (s === 'yesterday') { const d = new Date(now); d.setHours(0, 0, 0, 0); return d.getTime() - 86_400_000; }
102
+ const rel = /^(\d+)\s*([mhdw])$/.exec(s);
103
+ if (rel) {
104
+ const n = Number(rel[1]);
105
+ const unit = { m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000 }[rel[2]];
106
+ return now - n * unit;
107
+ }
108
+ const t = Date.parse(s);
109
+ return Number.isFinite(t) ? t : 0;
110
+ }
111
+
112
+ /** Does this line ask for a model turn? Surfaces use it to decide whether to warm a target. */
113
+ export function wantsModel(parsed) {
114
+ return !!parsed && parsed.mode === 'ask';
115
+ }
116
+
117
+ /**
118
+ * Is the line worth acting on yet? Guards the "search on every keystroke" path so a bare
119
+ * prefix does not run an empty query against the whole corpus.
120
+ */
121
+ export function isActionable(parsed, { minChars = 2 } = {}) {
122
+ if (!parsed) return false;
123
+ if (parsed.mode === 'command') return parsed.query.length >= 1;
124
+ return parsed.query.length >= minChars;
125
+ }
package/package.json CHANGED
@@ -1,33 +1,48 @@
1
1
  {
2
2
  "name": "@chatpanel/events",
3
- "version": "0.33.1",
3
+ "version": "0.47.0",
4
4
  "description": "The canonical ChatPanel event-log and capability contracts — typed durable facts, clock-free deterministic linearization, schema upcasting, and the invariants the replay harness asserts. Pure, dependency-free ESM shared by the ChatPanel extension, gateway and bridge.",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "exports": {
8
8
  ".": "./index.js",
9
9
  "./adapters.js": "./adapters.js",
10
+ "./backup-envelope.js": "./backup-envelope.js",
10
11
  "./capability.js": "./capability.js",
11
12
  "./citations.js": "./citations.js",
13
+ "./curate.js": "./curate.js",
14
+ "./distance.js": "./distance.js",
15
+ "./entitlement.js": "./entitlement.js",
16
+ "./entity.js": "./entity.js",
12
17
  "./event.js": "./event.js",
18
+ "./extraction.js": "./extraction.js",
19
+ "./flowchart.js": "./flowchart.js",
13
20
  "./harness.js": "./harness.js",
14
21
  "./invariants.js": "./invariants.js",
15
22
  "./kernel.js": "./kernel.js",
23
+ "./knowledge-derive.js": "./knowledge-derive.js",
24
+ "./knowledge.js": "./knowledge.js",
25
+ "./library.js": "./library.js",
16
26
  "./loop.js": "./loop.js",
17
27
  "./manifest.js": "./manifest.js",
18
28
  "./markdown-authoring.js": "./markdown-authoring.js",
19
- "./media-transcript.js": "./media-transcript.js",
20
29
  "./mcp-errors.js": "./mcp-errors.js",
30
+ "./media-transcript.js": "./media-transcript.js",
21
31
  "./meeting-analyzers.js": "./meeting-analyzers.js",
22
32
  "./memory.js": "./memory.js",
33
+ "./observability.js": "./observability.js",
34
+ "./omni.js": "./omni.js",
23
35
  "./order.js": "./order.js",
24
36
  "./pdf-layout.js": "./pdf-layout.js",
37
+ "./promotion.js": "./promotion.js",
25
38
  "./queue.js": "./queue.js",
26
39
  "./reach.js": "./reach.js",
40
+ "./redaction-tokens.js": "./redaction-tokens.js",
27
41
  "./ref.js": "./ref.js",
28
42
  "./registry.js": "./registry.js",
29
43
  "./route-graph.js": "./route-graph.js",
30
44
  "./router.js": "./router.js",
45
+ "./rrf.js": "./rrf.js",
31
46
  "./rules.js": "./rules.js",
32
47
  "./schedule.js": "./schedule.js",
33
48
  "./scopes.js": "./scopes.js",
@@ -39,30 +54,36 @@
39
54
  "./sources-retrieval.js": "./sources-retrieval.js",
40
55
  "./sources.js": "./sources.js",
41
56
  "./store.js": "./store.js",
57
+ "./structured.js": "./structured.js",
58
+ "./subject-kinds.js": "./subject-kinds.js",
59
+ "./subject-name.js": "./subject-name.js",
60
+ "./sync-plan.js": "./sync-plan.js",
61
+ "./synthesis.js": "./synthesis.js",
62
+ "./tags.js": "./tags.js",
42
63
  "./text-search.js": "./text-search.js",
64
+ "./theme.js": "./theme.js",
65
+ "./titles.js": "./titles.js",
43
66
  "./tool-groups.js": "./tool-groups.js",
44
67
  "./tool-need.js": "./tool-need.js",
45
68
  "./trajectory.js": "./trajectory.js",
46
69
  "./upcast.js": "./upcast.js",
47
70
  "./vault.js": "./vault.js",
48
- "./voice-intents.js": "./voice-intents.js",
49
- "./observability.js": "./observability.js",
50
- "./flowchart.js": "./flowchart.js",
51
- "./rrf.js": "./rrf.js",
52
71
  "./view.js": "./view.js",
72
+ "./voice-intents.js": "./voice-intents.js",
53
73
  "./weather.js": "./weather.js",
54
- "./widget.js": "./widget.js",
55
- "./tags.js": "./tags.js",
56
- "./titles.js": "./titles.js",
57
- "./structured.js": "./structured.js",
58
- "./extraction.js": "./extraction.js"
74
+ "./widget.js": "./widget.js"
59
75
  },
60
76
  "files": [
61
77
  "LICENSE",
62
78
  "README.md",
63
79
  "adapters.js",
80
+ "backup-envelope.js",
64
81
  "capability.js",
65
82
  "citations.js",
83
+ "curate.js",
84
+ "distance.js",
85
+ "entitlement.js",
86
+ "entity.js",
66
87
  "event.js",
67
88
  "extraction.js",
68
89
  "flowchart.js",
@@ -70,6 +91,9 @@
70
91
  "index.js",
71
92
  "invariants.js",
72
93
  "kernel.js",
94
+ "knowledge-derive.js",
95
+ "knowledge.js",
96
+ "library.js",
73
97
  "loop.js",
74
98
  "manifest.js",
75
99
  "markdown-authoring.js",
@@ -78,10 +102,13 @@
78
102
  "meeting-analyzers.js",
79
103
  "memory.js",
80
104
  "observability.js",
105
+ "omni.js",
81
106
  "order.js",
82
107
  "pdf-layout.js",
108
+ "promotion.js",
83
109
  "queue.js",
84
110
  "reach.js",
111
+ "redaction-tokens.js",
85
112
  "ref.js",
86
113
  "registry.js",
87
114
  "route-graph.js",
@@ -99,8 +126,13 @@
99
126
  "sources.js",
100
127
  "store.js",
101
128
  "structured.js",
129
+ "subject-kinds.js",
130
+ "subject-name.js",
131
+ "sync-plan.js",
132
+ "synthesis.js",
102
133
  "tags.js",
103
134
  "text-search.js",
135
+ "theme.js",
104
136
  "titles.js",
105
137
  "tool-groups.js",
106
138
  "tool-need.js",