@chatpanel/events 0.102.0 → 0.104.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.
Files changed (139) hide show
  1. package/adapters.js +1 -83
  2. package/adaptive-tool-policy.js +1 -45
  3. package/agent.js +2 -255
  4. package/attribution.js +1 -132
  5. package/backup-envelope.js +1 -221
  6. package/board-tool.js +2 -165
  7. package/budget.js +1 -116
  8. package/capability.js +1 -126
  9. package/citations.js +5 -127
  10. package/client-prefs.js +1 -148
  11. package/context-attachments.js +8 -144
  12. package/cowriter-router.js +1 -83
  13. package/cowriter-writer.js +19 -193
  14. package/cowriter.js +1 -190
  15. package/curate.js +3 -509
  16. package/distance.js +1 -124
  17. package/engine.js +1 -131
  18. package/entitlement.js +1 -332
  19. package/entity.js +1 -354
  20. package/event.js +1 -175
  21. package/extraction.js +4 -293
  22. package/failover.js +1 -93
  23. package/find-tool.js +1 -66
  24. package/flowchart.js +2 -644
  25. package/gate.js +1 -74
  26. package/harness.js +4 -101
  27. package/index.js +1 -345
  28. package/invariants.js +1 -174
  29. package/job.js +3 -147
  30. package/kernel.js +1 -255
  31. package/knowledge-derive.js +1 -267
  32. package/knowledge.js +3 -230
  33. package/library.js +11 -280
  34. package/loop.js +1 -132
  35. package/manifest.js +1 -107
  36. package/markdown-authoring.js +11 -288
  37. package/markdown-render.js +0 -0
  38. package/mcp-client.js +8 -358
  39. package/mcp-dispatch.js +1 -52
  40. package/mcp-errors.js +1 -106
  41. package/mcp-manager.js +1 -127
  42. package/media-transcript.js +8 -531
  43. package/meeting-analyzers.js +1 -83
  44. package/meeting-insights.js +7 -164
  45. package/meeting-shape.js +1 -282
  46. package/meeting-text.js +5 -94
  47. package/memory.js +2 -638
  48. package/model-candidates.js +1 -358
  49. package/model-health.js +1 -182
  50. package/model-ledger.js +1 -228
  51. package/model-picker.js +1 -188
  52. package/note-actions.js +15 -246
  53. package/note-graph.js +1 -158
  54. package/note-links.js +1 -68
  55. package/note-mentions.js +8 -114
  56. package/note-plan.js +9 -107
  57. package/note-research.js +1 -135
  58. package/observability.js +1 -119
  59. package/omni.js +1 -125
  60. package/order.js +1 -78
  61. package/package.json +4 -5
  62. package/pdf-layout.js +7 -253
  63. package/policy.js +1 -0
  64. package/project-run.js +20 -444
  65. package/project.js +1 -170
  66. package/promotion.js +1 -171
  67. package/queue.js +1 -98
  68. package/reach.js +1 -23
  69. package/recipe-tool.js +2 -161
  70. package/recipe.js +1 -264
  71. package/record-list.js +2 -110
  72. package/recruit.js +3 -456
  73. package/redaction-tokens.js +1 -61
  74. package/ref.js +1 -55
  75. package/registry.js +1 -240
  76. package/route-graph.js +1 -115
  77. package/route-strategies.js +2 -232
  78. package/router.js +2 -880
  79. package/rrf.js +1 -89
  80. package/rules.js +1 -142
  81. package/schedule.js +1 -663
  82. package/scm-connection.js +1 -180
  83. package/scopes.js +1 -12
  84. package/scorecard.js +1 -335
  85. package/search-engines.js +1 -81
  86. package/skill-manifest.js +1 -330
  87. package/skill-scan.js +3 -209
  88. package/skill-sources.js +1 -162
  89. package/skill-vars.js +2 -252
  90. package/slash-commands.js +1 -158
  91. package/source-gate.js +1 -92
  92. package/sources-retrieval.js +5 -186
  93. package/sources.js +1 -273
  94. package/store.js +1 -171
  95. package/structured.js +4 -901
  96. package/subject-kinds.js +1 -5
  97. package/subject-name.js +1 -96
  98. package/sync-plan.js +1 -213
  99. package/synthesis.js +3 -123
  100. package/tags.js +1 -203
  101. package/team-board.js +6 -337
  102. package/team-cache.js +2 -63
  103. package/team-observe.js +1 -118
  104. package/team-org.js +1 -442
  105. package/team-plan.js +4 -131
  106. package/team-record.js +1 -225
  107. package/team-run.js +9 -652
  108. package/team-subtask.js +2 -191
  109. package/team-task.js +2 -124
  110. package/team-tool.js +2 -196
  111. package/team-trail.js +2 -73
  112. package/team-worklog.js +2 -149
  113. package/team.js +1 -302
  114. package/text-search.js +1 -155
  115. package/theme.js +17 -154
  116. package/titles.js +4 -285
  117. package/tool-discovery.js +1 -87
  118. package/tool-dispatch.js +5 -230
  119. package/tool-groups.js +1 -81
  120. package/tool-hints.js +10 -185
  121. package/tool-loop-guard.js +4 -182
  122. package/tool-need.js +2 -124
  123. package/tool-result.js +2 -438
  124. package/tool-round.js +0 -0
  125. package/tool-schema.js +1 -155
  126. package/tool-traits.js +1 -182
  127. package/toolset.js +2 -88
  128. package/trajectory.js +1 -530
  129. package/turn-loop.js +4 -440
  130. package/upcast.js +1 -37
  131. package/vault.js +2 -250
  132. package/view.js +1 -88
  133. package/voice-intents.js +2 -1488
  134. package/voice-speaker.js +1 -98
  135. package/weather-tool.js +1 -65
  136. package/weather.js +2 -211
  137. package/web-search-tool.js +14 -102
  138. package/web-search.js +1 -160
  139. package/widget.js +1 -153
package/subject-kinds.js CHANGED
@@ -1,5 +1 @@
1
- // The subject vocabulary, alone in a file so both halves of subject identity can name it
2
- // without either importing the other.
3
-
4
- /** What a subject can be. `title` is a record title someone linked to with [[…]]. */
5
- export const SUBJECT_KINDS = Object.freeze(['person', 'topic', 'tag', 'title']);
1
+ const e=Object.freeze(["person","topic","tag","title"]);export{e as SUBJECT_KINDS};
package/subject-name.js CHANGED
@@ -1,96 +1 @@
1
- // Naming a subject, and when one is big enough to deserve a page.
2
- //
3
- // The small, dependency-free half of subject identity: fold a name to its canonical form,
4
- // strip the decoration a directory hangs off it, and hold the evidence thresholds. Nothing
5
- // here resolves aliases, proposes merges or reaches for a Levenshtein — that is `entity.js`,
6
- // which imports this.
7
- //
8
- // The split is a load-time one, and it is the third time the lesson has come up here (see
9
- // `distance.js` and `redaction-tokens.js`). `knowledge.js` needs exactly `normalizeSubject`
10
- // to build a brief id, and the extension's brief store needs exactly `DEFAULT_THRESHOLD` —
11
- // and both are on the MV3 service worker's graph. Reaching them through `entity.js` put
12
- // entity resolution and merge suggestion on a worker that will never run either.
13
-
14
- import { SUBJECT_KINDS } from './subject-kinds.js';
15
-
16
- /**
17
- * A subject earns a brief with EVIDENCE, not on first sight (I-K4).
18
- *
19
- * PROVISIONAL. These numbers are the W0 measurement's whole point: `surveyCorpus()` reports
20
- * how many subjects clear them so they can be set from a real corpus instead of taste. Do
21
- * not treat them as decided until that report has been run.
22
- */
23
- export const DEFAULT_THRESHOLD = Object.freeze({ records: 3, mentions: 5 });
24
-
25
- /** Ceiling on the set of briefs, for the same reason memory.js caps memories. Provisional. */
26
- export const MAX_SUBJECTS = 500;
27
- /** Longest name we will treat as a subject — past this it is a sentence, not a subject. */
28
- export const MAX_SUBJECT_CHARS = 60;
29
- /**
30
- * Labels a meeting platform uses for the person holding the microphone.
31
- *
32
- * Zoom, Meet and Teams all write the local participant as "You" — so the user appears in
33
- * their own corpus under a name that is not a name, alongside however their colleagues'
34
- * clients spelled them. Resolving these needs one fact only the host has: who "you" IS.
35
- * `resolveSubjects` takes it rather than guessing, and with no `self` supplied these stay
36
- * unresolved instead of collapsing every meeting's local speaker into one fictional person.
37
- */
38
- export const SELF_LABELS = Object.freeze(['you', 'me', 'myself', 'yourself', 'i']);
39
-
40
- /**
41
- * Is this the platform's label for the local participant?
42
- *
43
- * Exported because two layers need the SAME exception and getting the order wrong is subtle:
44
- * a self-label fails `isSubjectCandidate` (it is a pronoun), so any pass that filters
45
- * candidacy BEFORE `resolveSubjects` can fold it has already thrown the user away. `curate.js
46
- * mentionsFrom` keeps them for exactly this reason and lets resolution decide.
47
- */
48
- export function isSelfLabel(name) {
49
- return SELF_LABELS.includes(normalizeSubject(name));
50
- }
51
-
52
- /**
53
- * Strip the decoration a directory or a conference client hangs off a person's name.
54
- *
55
- * The same human arrives as "Alex Rivera", "Alex Rivera (ACME)", "Alex Rivera - Host" and
56
- * "Alex Rivera (he/him)" depending on which client wrote the label. The part in parentheses
57
- * or after a dash is an org, a role or a pronoun set — decoration, never identity — so it is
58
- * removed before folding.
59
- *
60
- * NOT removed for non-person subjects: "Migration (Phase 2)" is a different topic from
61
- * "Migration", where "Alex Rivera (ACME)" is not a different person from "Alex Rivera".
62
- */
63
- export function stripQualifiers(name) {
64
- return String(name ?? '')
65
- .replace(/\s*[([{][^)\]}]*[)\]}]\s*/g, ' ') // (ACME), [external], {guest}
66
- .replace(/\s+[-–—|·,]\s+.*$/, '') // - Host, — Guest, | ACME
67
- .replace(/\s+/g, ' ')
68
- .trim();
69
- }
70
- /**
71
- * Fold a name to its canonical form: lowercase, Unicode-aware, separators collapsed.
72
- *
73
- * Spaces survive as spaces (unlike normalizeTag, which folds them to '-') because a person's
74
- * name is read back to the user and "alex rivera" has to be recognisable as one.
75
- */
76
- export function normalizeSubject(name) {
77
- const raw = String(name ?? '').normalize('NFKC').trim().replace(/^[#@]+/, '');
78
- if (!raw) return '';
79
- return raw
80
- .toLowerCase()
81
- .replace(/[^\p{L}\p{N}]+/gu, ' ')
82
- .trim()
83
- .slice(0, MAX_SUBJECT_CHARS)
84
- .trim();
85
- }
86
- /** `person:alex rivera` — the identity a brief is filed under. '' when nothing survives. */
87
- export function subjectKey(kind, name) {
88
- const norm = normalizeSubject(name);
89
- if (!norm || !SUBJECT_KINDS.includes(kind)) return '';
90
- return `${kind}:${norm}`;
91
- }
92
- /** Tokens of a canonical name. */
93
- export function subjectTokens(name) {
94
- const norm = normalizeSubject(name);
95
- return norm ? norm.split(' ').filter(Boolean) : [];
96
- }
1
+ import{SUBJECT_KINDS as n}from"./subject-kinds.js";const u=Object.freeze({records:3,mentions:5}),p=500,s=60,c=Object.freeze(["you","me","myself","yourself","i"]);function l(e){return c.includes(t(e))}function f(e){return String(e??"").replace(/\s*[([{][^)\]}]*[)\]}]\s*/g," ").replace(/\s+[-–—|·,]\s+.*$/,"").replace(/\s+/g," ").trim()}function t(e){const r=String(e??"").normalize("NFKC").trim().replace(/^[#@]+/,"");return r?r.toLowerCase().replace(/[^\p{L}\p{N}]+/gu," ").trim().slice(0,s).trim():""}function m(e,r){const o=t(r);return!o||!n.includes(e)?"":`${e}:${o}`}function S(e){const r=t(e);return r?r.split(" ").filter(Boolean):[]}export{u as DEFAULT_THRESHOLD,p as MAX_SUBJECTS,s as MAX_SUBJECT_CHARS,c as SELF_LABELS,l as isSelfLabel,t as normalizeSubject,f as stripQualifiers,m as subjectKey,S as subjectTokens};
package/sync-plan.js CHANGED
@@ -1,213 +1 @@
1
- // RECONCILING TWO COPIES OF THE LIBRARY the pure half of two-way sync.
2
- //
3
- // The extension holds the corpus in chrome.storage; a desktop app holds it in SQLite; the
4
- // gateway holds a warm copy. Each pair of those needs the same question answered: given what
5
- // I have and what you have, what do I pull, what do I push, and what genuinely conflicts.
6
- //
7
- // It lives here because the answer must not depend on WHO is asking. If the extension
8
- // decided ties one way and the desktop the other, two clients syncing the same record would
9
- // flap — each would keep "winning" and re-pushing, forever. One rule, both sides.
10
- //
11
- // WHAT MAKES THIS SAFE TO RUN REPEATEDLY: the plan is a pure function of two index lists, so
12
- // applying it and re-planning yields an empty plan. That is the property to hold on to —
13
- // every bug in a sync engine eventually shows up as "running it twice does something".
14
- //
15
- // TOMBSTONES ARE NOT OPTIONAL, and this is the sharpest edge in the module. A record that is
16
- // merely ABSENT from one side is indistinguishable from one that side has never seen. So a
17
- // deletion has to be representable as a value — `deletedAt` — or "delete" and "not synced
18
- // yet" are the same input with opposite correct answers. The extension's warm sync already
19
- // hit this and chose, deliberately, not to tombstone (a missing browser record may simply
20
- // have aged out of IndexedDB while remaining in a backup). That choice is why warm sync is
21
- // one-way. A two-way sync cannot make it.
22
- //
23
- // Pure over arrays of `{ id, updatedAt, deletedAt }`. No storage, no network, no clock —
24
- // `now` is never consulted, because a plan that depends on when you asked is not a plan.
25
-
26
- export class SyncError extends Error {
27
- constructor(message) { super(message); this.name = 'SyncError'; }
28
- }
29
-
30
- /**
31
- * How far apart two timestamps may be and still count as "the same edit".
32
- *
33
- * Clocks on two machines are not identical, and a record copied between them can come back
34
- * with a millisecond of drift. Without a tolerance every round trip looks like a fresh edit
35
- * and the two sides push at each other indefinitely.
36
- */
37
- export const CLOCK_TOLERANCE_MS = 1000;
38
-
39
- const num = (v) => (Number.isFinite(Number(v)) ? Number(v) : 0);
40
-
41
- /** The comparable stamp of a record: when it last changed, deleted or not. */
42
- export function stampOf(entry) {
43
- if (!entry) return 0;
44
- return Math.max(num(entry.updatedAt), num(entry.deletedAt));
45
- }
46
-
47
- function indexById(entries) {
48
- const m = new Map();
49
- for (const e of entries || []) {
50
- if (!e || !e.id) continue;
51
- // Later duplicates win: a caller concatenating pages should not be punished for it.
52
- m.set(e.id, e);
53
- }
54
- return m;
55
- }
56
-
57
- /**
58
- * Decide one record. Exported because it is the whole rule, and a caller that streams
59
- * records one at a time should not have to build two full indexes to use it.
60
- *
61
- * Returns one of:
62
- * 'none' — the two sides agree
63
- * 'pull' — remote is newer; take theirs
64
- * 'push' — local is newer, or remote has never seen it; send ours
65
- * 'conflict' — both changed since the last common state and neither is clearly newer
66
- */
67
- /**
68
- * Is this entry the COMPLETE record, or a flattened stand-in for one?
69
- *
70
- * A warm/indexed copy of a chat is its transcript as one blob of text: enough to search,
71
- * read and cite, and not enough to open as a conversation. Callers stamp that with
72
- * `meta.lossy` (or `meta.origin: 'warm'`); a stamp row may carry `lossy` directly, because a
73
- * sync plan works on stamps and should not have to load bodies to make this decision.
74
- *
75
- * Returns 1 for full fidelity and 0 for lossy — an ordering, so the rule below is a
76
- * comparison rather than a pile of branches.
77
- */
78
- export function fidelityOf(entry) {
79
- if (!entry) return 0;
80
- if (entry.lossy === true) return 0;
81
- const meta = entry.meta || null;
82
- if (meta && (meta.lossy === true || meta.origin === 'warm')) return 0;
83
- return 1;
84
- }
85
-
86
- export function decide(local, remote, { tolerance = CLOCK_TOLERANCE_MS, base = null } = {}) {
87
- if (!local && !remote) return 'none';
88
- if (!remote) return 'push';
89
- if (!local) return 'pull';
90
-
91
- const l = stampOf(local);
92
- const r = stampOf(remote);
93
-
94
- // FIDELITY BEATS RECENCY, BOTH WAYS.
95
- //
96
- // The two sides are not always describing the same kind of thing. One may hold the whole
97
- // record and the other a flattened stand-in for it — and those two arrive with the SAME
98
- // timestamp, because they describe the same moment. Last-write-wins then reads them as
99
- // equal and answers 'none', which is how a complete conversation restored from a backup
100
- // gets silently refused in favour of a search-index summary of itself that is already
101
- // there. The user restores, is told it worked, and still sees the flattened copy.
102
- //
103
- // So a complete record always wins over a partial one whatever the clocks say. The
104
- // converse — never let a partial overwrite a complete one — is the same rule read the
105
- // other way, and it is the one callers usually remember to enforce by hand.
106
- //
107
- // DELETIONS ARE EXEMPT. A tombstone carries no body, so it is 'lossy' by any measure, and
108
- // resurrecting deleted records because their replacement looks fuller would be a far worse
109
- // bug than the one this fixes. When either side is deleted, the stamps decide.
110
- const deleted = num(local.deletedAt) > 0 || num(remote.deletedAt) > 0;
111
- if (!deleted) {
112
- const lf = fidelityOf(local);
113
- const rf = fidelityOf(remote);
114
- if (lf !== rf) return rf > lf ? 'pull' : 'push';
115
- }
116
-
117
- const delta = l - r;
118
- if (Math.abs(delta) <= tolerance) return 'none';
119
-
120
- // With a recorded base (what both sides last agreed on) we can tell a genuine divergence
121
- // from a simple fast-forward: a conflict is when BOTH moved. Without a base the newer
122
- // stamp wins, which is last-write-wins and is all a first sync can honestly offer.
123
- if (base) {
124
- const b = stampOf(base);
125
- const localMoved = l - b > tolerance;
126
- const remoteMoved = r - b > tolerance;
127
- if (localMoved && remoteMoved) return 'conflict';
128
- }
129
- return delta > 0 ? 'push' : 'pull';
130
- }
131
-
132
- /**
133
- * Plan a full two-way reconcile.
134
- *
135
- * `bases` is optional — a map (or array) of the stamps both sides last agreed on, which is
136
- * what upgrades last-write-wins into real conflict detection. Callers that keep a sync
137
- * journal pass it; callers that do not get LWW and no false conflicts.
138
- *
139
- * Returns `{ pull, push, conflicts, unchanged }` as arrays of ids, plus `counts`.
140
- */
141
- export function planSync(localEntries, remoteEntries, {
142
- tolerance = CLOCK_TOLERANCE_MS, bases = null,
143
- } = {}) {
144
- const local = indexById(localEntries);
145
- const remote = indexById(remoteEntries);
146
- const baseMap = bases instanceof Map ? bases : indexById(bases);
147
-
148
- const pull = [];
149
- const push = [];
150
- const conflicts = [];
151
- let unchanged = 0;
152
-
153
- for (const id of new Set([...local.keys(), ...remote.keys()])) {
154
- const verdict = decide(local.get(id), remote.get(id), { tolerance, base: baseMap.get(id) || null });
155
- if (verdict === 'pull') pull.push(id);
156
- else if (verdict === 'push') push.push(id);
157
- else if (verdict === 'conflict') conflicts.push(id);
158
- else unchanged += 1;
159
- }
160
-
161
- // Stable order so two runs over the same input produce byte-identical plans — which is
162
- // what makes a plan diffable in a log and testable without sorting at every assertion.
163
- pull.sort();
164
- push.sort();
165
- conflicts.sort();
166
-
167
- return {
168
- pull, push, conflicts, unchanged,
169
- counts: { pull: pull.length, push: push.length, conflicts: conflicts.length, unchanged },
170
- };
171
- }
172
-
173
- /** Nothing to do — the property a correct sync reaches and stays at. */
174
- export function isSettled(plan) {
175
- return !!plan && plan.pull.length === 0 && plan.push.length === 0 && plan.conflicts.length === 0;
176
- }
177
-
178
- /**
179
- * Resolve a conflict by keeping both: the loser is preserved under a new id rather than
180
- * overwritten.
181
- *
182
- * Silently discarding one side of a conflict is how a sync engine loses the paragraph
183
- * someone wrote on a plane. The caller supplies `newId` because id minting is a host
184
- * concern; this only decides WHAT the two resulting records are.
185
- */
186
- export function forkConflict(localRecord, remoteRecord, { newId, label = 'conflicted copy' } = {}) {
187
- if (typeof newId !== 'function') throw new SyncError('forkConflict needs a newId() function');
188
- const keep = stampOf(localRecord) >= stampOf(remoteRecord) ? localRecord : remoteRecord;
189
- const fork = keep === localRecord ? remoteRecord : localRecord;
190
- const parsedKind = String(fork.id || '').split(':')[0];
191
- return {
192
- keep,
193
- fork: {
194
- ...fork,
195
- id: `${parsedKind}:${newId()}`,
196
- title: `${fork.title || 'Untitled'} (${label})`,
197
- meta: { ...(fork.meta || {}), conflictOf: keep.id },
198
- },
199
- };
200
- }
201
-
202
- /**
203
- * Fold applied ids back into a base map, so the NEXT plan can tell a fast-forward from a
204
- * divergence. Returns a new Map; the input is not mutated.
205
- */
206
- export function advanceBases(bases, applied = []) {
207
- const next = bases instanceof Map ? new Map(bases) : indexById(bases);
208
- for (const entry of applied) {
209
- if (!entry || !entry.id) continue;
210
- next.set(entry.id, { id: entry.id, updatedAt: num(entry.updatedAt), deletedAt: num(entry.deletedAt) });
211
- }
212
- return next;
213
- }
1
+ class m extends Error{constructor(n){super(n),this.name="SyncError"}}const g=1e3,l=t=>Number.isFinite(Number(t))?Number(t):0;function p(t){return t?Math.max(l(t.updatedAt),l(t.deletedAt)):0}function a(t){const n=new Map;for(const e of t||[])!e||!e.id||n.set(e.id,e);return n}function M(t){if(!t||t.lossy===!0)return 0;const n=t.meta||null;return n&&(n.lossy===!0||n.origin==="warm")?0:1}function A(t,n,{tolerance:e=g,base:o=null}={}){if(!t&&!n)return"none";if(!n)return"push";if(!t)return"pull";const s=p(t),i=p(n);if(!(l(t.deletedAt)>0||l(n.deletedAt)>0)){const r=M(t),u=M(n);if(r!==u)return u>r?"pull":"push"}const c=s-i;if(Math.abs(c)<=e)return"none";if(o){const r=p(o),u=s-r>e,d=i-r>e;if(u&&d)return"conflict"}return c>0?"push":"pull"}function y(t,n,{tolerance:e=g,bases:o=null}={}){const s=a(t),i=a(n),h=o instanceof Map?o:a(o),c=[],r=[],u=[];let d=0;for(const f of new Set([...s.keys(),...i.keys()])){const x=A(s.get(f),i.get(f),{tolerance:e,base:h.get(f)||null});x==="pull"?c.push(f):x==="push"?r.push(f):x==="conflict"?u.push(f):d+=1}return c.sort(),r.sort(),u.sort(),{pull:c,push:r,conflicts:u,unchanged:d,counts:{pull:c.length,push:r.length,conflicts:u.length,unchanged:d}}}function k(t){return!!t&&t.pull.length===0&&t.push.length===0&&t.conflicts.length===0}function S(t,n,{newId:e,label:o="conflicted copy"}={}){if(typeof e!="function")throw new m("forkConflict needs a newId() function");const s=p(t)>=p(n)?t:n,i=s===t?n:t,h=String(i.id||"").split(":")[0];return{keep:s,fork:{...i,id:`${h}:${e()}`,title:`${i.title||"Untitled"} (${o})`,meta:{...i.meta||{},conflictOf:s.id}}}}function w(t,n=[]){const e=t instanceof Map?new Map(t):a(t);for(const o of n)!o||!o.id||e.set(o.id,{id:o.id,updatedAt:l(o.updatedAt),deletedAt:l(o.deletedAt)});return e}export{g as CLOCK_TOLERANCE_MS,m as SyncError,w as advanceBases,A as decide,M as fidelityOf,S as forkConflict,k as isSettled,y as planSync,p as stampOf};
package/synthesis.js CHANGED
@@ -1,123 +1,3 @@
1
- // SYNTHESIS the first class-C claims: prose about a subject, written by a model.
2
- //
3
- // Everything a brief said until now was class R: a restatement of something a record
4
- // already held, with nothing to review. This is the layer the wiki pattern is actually
5
- // about — "what was decided about Atlas, and when did it change" — and it is exactly the
6
- // layer the community warned about: agent writing nobody reviewed compounds into confident
7
- // nonsense that a lint pass cannot tell apart from truth six months on (C2).
8
- //
9
- // So two rules do all the defending, and both are enforced here rather than promised:
10
- //
11
- // I-K1 EVERY CLAIM CITES A RECORD IT WAS SHOWN. The model sees excerpts labelled with
12
- // their ids and must attach at least one to each claim. A claim citing nothing, or
13
- // citing an id that was not in the excerpt set, is REFUSED — not downgraded, not
14
- // flagged, refused — because a citation a reader cannot open is worse than no claim.
15
- // I-K3 NOTHING HERE PROMOTES. `claimsFromSynthesis` produces claims in state `proposed`
16
- // and `promotion.js` is the only path to `promoted`. There is no flag to skip it.
17
- //
18
- // The model call itself is not made here (no network, per the package rule). This module
19
- // owns the SHAPE — one schema, one prompt, one parser — so the extension, the gateway and a
20
- // desktop client ask the same question and read the answer the same way. It is written as
21
- // a single appointment on purpose: a team appointment (W7 — drafters that must agree) has
22
- // the same contract, with `converge()` in promotion.js deciding what to propose.
23
-
24
- import { defineSchema, describeSchema } from './structured.js';
25
-
26
- export const MAX_SYNTHESIS_CLAIMS = 8;
27
- export const MAX_CLAIM_CHARS = 240;
28
- export const MAX_EXCERPT_CHARS = 1200;
29
- export const MAX_EXCERPTS = 24;
30
-
31
- export const SYNTHESIS_SCHEMA = defineSchema({
32
- name: 'brief_synthesis',
33
- purpose: 'What the records establish about one subject, as separate cited claims.',
34
- fields: {
35
- claims: {
36
- type: 'object[]', maxItems: MAX_SYNTHESIS_CLAIMS,
37
- describe: 'the things the records establish about the subject — decisions, roles, status, changes — one per entry, each cited',
38
- fields: {
39
- text: { type: 'string', required: true, max: MAX_CLAIM_CHARS, describe: 'one specific claim in plain prose, past tense for events, present for standing facts' },
40
- refs: { type: 'string[]', required: true, maxItems: 6, describe: 'the record ids (exactly as labelled, e.g. meeting:m_12) that support this claim — at least one, only ones you were shown' },
41
- when: { type: 'string', max: 10, describe: 'YYYY-MM-DD of the record that establishes it, if one does' },
42
- },
43
- },
44
- summary: { type: 'string', max: 600, describe: 'two or three sentences of what a colleague should know about this subject right now' },
45
- },
46
- // "The records do not establish anything beyond what is already listed" is a legitimate
47
- // answer and must not be read as a failure — read as one, a caller might fall back to the
48
- // model's prose without the schema, which is the exact unstructured path this replaces.
49
- nothing: { claims: [], summary: '' },
50
- });
51
-
52
- /**
53
- * The prompt. The subject, what the deterministic layer already says (so the model adds to
54
- * it rather than restating it), and the excerpts it may cite — each labelled with the id it
55
- * must use. Excerpts are capped per record and in count, because this is a bounded call by
56
- * design (I-K4): a subject with two hundred records gets its most recent two dozen, and a
57
- * later pass can take the rest.
58
- */
59
- export function synthesisPrompt({ subject, existing = [], excerpts = [] } = {}) {
60
- const name = String(subject?.name || 'the subject');
61
- const kind = String(subject?.kind || 'subject');
62
- const shown = excerpts.slice(0, MAX_EXCERPTS);
63
- const lines = [
64
- `Subject: ${name} (${kind})${subject?.aliases?.length ? ` — also written as ${subject.aliases.join(', ')}` : ''}.`,
65
- '',
66
- 'Already established (do not repeat these):',
67
- ...(existing.length ? existing.map((c) => `- ${c}`) : ['- nothing yet']),
68
- '',
69
- `Records you may cite (${shown.length}). Cite ONLY these ids, exactly as written:`,
70
- ...shown.map((e) => `[${e.id}] ${e.title || ''}${e.date ? ` (${e.date})` : ''}\n${String(e.text || '').slice(0, MAX_EXCERPT_CHARS)}`),
71
- '',
72
- 'Write what these records ESTABLISH about the subject: decisions, ownership, status, and',
73
- 'anything that changed over time (say what it was before and after). Every claim must cite',
74
- 'at least one of the ids above. If the records establish nothing beyond what is already',
75
- 'listed, return no claims.',
76
- '',
77
- describeSchema(SYNTHESIS_SCHEMA),
78
- ];
79
- return lines.join('\n');
80
- }
81
-
82
- /**
83
- * The model's answer → claims a brief could carry, with the refusals listed.
84
- *
85
- * `knownIds` is the set of ids the model was shown. A claim citing anything outside it is
86
- * refused whole — the model invented or misremembered a citation, and a claim we cannot
87
- * trace is a claim we cannot keep (I-K1). `hashOf(id)` gives the record's drift hash so the
88
- * ref can later say "this record changed since I cited it".
89
- *
90
- * Returns claims in state `proposed`, class C. There is no argument that produces
91
- * `promoted`; that is promotion.js's job and nobody else's (I-K3).
92
- */
93
- export function claimsFromSynthesis(value, { knownIds, hashOf = () => 'unhashed', now = 0, newId = null } = {}) {
94
- const known = knownIds instanceof Set ? knownIds : new Set(knownIds || []);
95
- const out = [];
96
- const refused = [];
97
- let n = 0;
98
- const id = () => (newId ? newId() : `s${now}-${(n += 1)}`);
99
- for (const raw of value?.claims || []) {
100
- const text = String(raw?.text || '').trim();
101
- const refs = [...new Set((raw?.refs || []).map((r) => String(r || '').trim()).filter(Boolean))];
102
- if (!text) { refused.push({ text, why: 'empty' }); continue; }
103
- if (!refs.length) { refused.push({ text, why: 'no citation' }); continue; }
104
- const unknown = refs.filter((r) => !known.has(r));
105
- if (unknown.length) { refused.push({ text, why: `cites a record it was not shown: ${unknown.join(', ')}` }); continue; }
106
- out.push({
107
- id: id(),
108
- kind: 'synthesis',
109
- text: text.slice(0, MAX_CLAIM_CHARS),
110
- refs: refs.map((r) => {
111
- const idx = r.indexOf(':');
112
- return { kind: r.slice(0, idx), id: r.slice(idx + 1), hash: hashOf(r) };
113
- }),
114
- when: /^\d{4}-\d{2}-\d{2}$/.test(String(raw?.when || '')) ? raw.when : '',
115
- firstSeen: now,
116
- lastConfirmed: now,
117
- confidence: 0.6, // a model's read, not a record's word — lower than any class-R claim
118
- cls: 'C',
119
- state: 'proposed',
120
- });
121
- }
122
- return { claims: out, summary: String(value?.summary || '').trim().slice(0, 600), refused };
123
- }
1
+ import{defineSchema as S,describeSchema as g}from"./structured.js";const b=8,p=240,w=1200,x=24,$=S({name:"brief_synthesis",purpose:"What the records establish about one subject, as separate cited claims.",fields:{claims:{type:"object[]",maxItems:b,describe:"the things the records establish about the subject — decisions, roles, status, changes — one per entry, each cited",fields:{text:{type:"string",required:!0,max:p,describe:"one specific claim in plain prose, past tense for events, present for standing facts"},refs:{type:"string[]",required:!0,maxItems:6,describe:"the record ids (exactly as labelled, e.g. meeting:m_12) that support this claim — at least one, only ones you were shown"},when:{type:"string",max:10,describe:"YYYY-MM-DD of the record that establishes it, if one does"}}},summary:{type:"string",max:600,describe:"two or three sentences of what a colleague should know about this subject right now"}},nothing:{claims:[],summary:""}});function C({subject:s,existing:n=[],excerpts:h=[]}={}){const i=String(s?.name||"the subject"),r=String(s?.kind||"subject"),a=h.slice(0,x);return[`Subject: ${i} (${r})${s?.aliases?.length?` — also written as ${s.aliases.join(", ")}`:""}.`,"","Already established (do not repeat these):",...n.length?n.map(e=>`- ${e}`):["- nothing yet"],"",`Records you may cite (${a.length}). Cite ONLY these ids, exactly as written:`,...a.map(e=>`[${e.id}] ${e.title||""}${e.date?` (${e.date})`:""}
2
+ ${String(e.text||"").slice(0,w)}`),"","Write what these records ESTABLISH about the subject: decisions, ownership, status, and","anything that changed over time (say what it was before and after). Every claim must cite","at least one of the ids above. If the records establish nothing beyond what is already","listed, return no claims.","",g($)].join(`
3
+ `)}function _(s,{knownIds:n,hashOf:h=()=>"unhashed",now:i=0,newId:r=null}={}){const a=n instanceof Set?n:new Set(n||[]),l=[],e=[];let f=0;const y=()=>r?r():`s${i}-${f+=1}`;for(const c of s?.claims||[]){const o=String(c?.text||"").trim(),d=[...new Set((c?.refs||[]).map(t=>String(t||"").trim()).filter(Boolean))];if(!o){e.push({text:o,why:"empty"});continue}if(!d.length){e.push({text:o,why:"no citation"});continue}const m=d.filter(t=>!a.has(t));if(m.length){e.push({text:o,why:`cites a record it was not shown: ${m.join(", ")}`});continue}l.push({id:y(),kind:"synthesis",text:o.slice(0,p),refs:d.map(t=>{const u=t.indexOf(":");return{kind:t.slice(0,u),id:t.slice(u+1),hash:h(t)}}),when:/^\d{4}-\d{2}-\d{2}$/.test(String(c?.when||""))?c.when:"",firstSeen:i,lastConfirmed:i,confidence:.6,cls:"C",state:"proposed"})}return{claims:l,summary:String(s?.summary||"").trim().slice(0,600),refused:e}}export{p as MAX_CLAIM_CHARS,x as MAX_EXCERPTS,w as MAX_EXCERPT_CHARS,b as MAX_SYNTHESIS_CLAIMS,$ as SYNTHESIS_SCHEMA,_ as claimsFromSynthesis,C as synthesisPrompt};
package/tags.js CHANGED
@@ -1,203 +1 @@
1
- // The tag vocabulary one filing system across notes, chats and meetings.
2
- //
3
- // A tag typed in the notes editor and a tag typed on the meetings page have to BE the
4
- // same tag, and `tag:design` has to select it from any list. That is a pure input →
5
- // output question about normalization and matching, so it lives here: the extension,
6
- // the gateway's warm index and any future mobile client all have to agree on what "the
7
- // same tag" is, and three normalizers would mean three answers to that.
8
- //
9
- // Design notes:
10
- // • Normalization is lossy on purpose — case, punctuation and spacing are filing
11
- // noise. "Design Review", "design-review" and "#DesignReview!" are one tag.
12
- // • Unicode-aware. Stripping everything outside [a-z0-9-] silently erases a tag
13
- // written in Japanese, Greek or Hindi — it becomes '' and vanishes. Letters and
14
- // numbers in ANY script are kept; only separators and punctuation fold to '-'.
15
- // • Order is the user's, not ours: tags keep insertion order rather than sorting,
16
- // because the first tag someone adds is usually the one they think in.
17
-
18
- export const MAX_TAG_LENGTH = 32;
19
- export const MAX_TAGS = 24;
20
-
21
- /**
22
- * Fold one user-typed value into its canonical tag, or '' when nothing survives.
23
- * Idempotent: normalizeTag(normalizeTag(x)) === normalizeTag(x).
24
- */
25
- export function normalizeTag(value) {
26
- const raw = String(value ?? '').trim().replace(/^#+/, '');
27
- if (!raw) return '';
28
- return raw
29
- .toLowerCase()
30
- // Any run of things that aren't letters/numbers becomes a single separator.
31
- .replace(/[^\p{L}\p{N}]+/gu, '-')
32
- .replace(/^-+|-+$/g, '')
33
- .slice(0, MAX_TAG_LENGTH)
34
- // The slice can land mid-separator; don't leave a trailing dash behind.
35
- .replace(/-+$/, '');
36
- }
37
-
38
- /** Normalize a list: drop blanks, dedupe (first wins), cap the count. */
39
- export function normalizeTags(list, { max = MAX_TAGS } = {}) {
40
- const out = [];
41
- for (const value of Array.isArray(list) ? list : []) {
42
- const tag = normalizeTag(value);
43
- if (tag && !out.includes(tag)) out.push(tag);
44
- if (out.length >= max) break;
45
- }
46
- return out;
47
- }
48
-
49
- export function hasTag(tags, value) {
50
- const tag = normalizeTag(value);
51
- return !!tag && normalizeTags(tags).includes(tag);
52
- }
53
-
54
- /** Append a tag (no-op when blank, already present, or at the cap). */
55
- export function addTag(tags, value, { max = MAX_TAGS } = {}) {
56
- const current = normalizeTags(tags, { max });
57
- const tag = normalizeTag(value);
58
- if (!tag || current.includes(tag) || current.length >= max) return current;
59
- return [...current, tag];
60
- }
61
-
62
- export function removeTag(tags, value) {
63
- const tag = normalizeTag(value);
64
- return normalizeTags(tags).filter((t) => t !== tag);
65
- }
66
-
67
- export function toggleTag(tags, value, { max = MAX_TAGS } = {}) {
68
- return hasTag(tags, value) ? removeTag(tags, value) : addTag(tags, value, { max });
69
- }
70
-
71
- /** Set equality, order-insensitive — so a save can skip a write that changes nothing. */
72
- export function sameTags(a, b) {
73
- const x = normalizeTags(a);
74
- const y = normalizeTags(b);
75
- return x.length === y.length && x.every((t) => y.includes(t));
76
- }
77
-
78
- /** `#design` — the one display form, so chips read the same on every surface. */
79
- export function formatTag(tag) {
80
- const t = normalizeTag(tag);
81
- return t ? `#${t}` : '';
82
- }
83
-
84
- // --------------------------------------------------------------------------
85
- // Query language
86
- // --------------------------------------------------------------------------
87
-
88
- // One search box, two jobs: free text and tag selection. Rather than a second input
89
- // per page, a query may carry tag terms inline —
90
- // tag:design include
91
- // #design include (the shorthand people already type)
92
- // -tag:done / -#done exclude
93
- // tag:"deep work" quoted, folded to deep-work
94
- // …and everything left over is the free-text part the ranker sees. A `#` that is part
95
- // of a word ("C#", "issue #12") is left alone: only a leading, standalone one counts.
96
- const TERM_RE = /(^|\s)(-)?(?:tag:|#)("([^"]*)"|[^\s"]+)/giu;
97
-
98
- /**
99
- * Split a raw query into { include, exclude, text }.
100
- * `text` is the query with the tag terms removed (whitespace collapsed).
101
- */
102
- export function parseTagQuery(query) {
103
- const raw = String(query ?? '');
104
- const include = [];
105
- const exclude = [];
106
- let text = raw.replace(TERM_RE, (match, lead, minus, bare, quoted) => {
107
- const tag = normalizeTag(quoted !== undefined ? quoted : bare);
108
- if (!tag) return match; // nothing usable — leave it in the free text
109
- const bucket = minus ? exclude : include;
110
- if (!bucket.includes(tag)) bucket.push(tag);
111
- return lead || '';
112
- });
113
- text = text.replace(/\s+/g, ' ').trim();
114
- return { include, exclude, text };
115
- }
116
-
117
- /** True when `query` carries at least one tag term. */
118
- export function hasTagTerms(query) {
119
- const { include, exclude } = parseTagQuery(query);
120
- return include.length > 0 || exclude.length > 0;
121
- }
122
-
123
- /** Render a filter back into query syntax — the inverse of parseTagQuery. */
124
- export function formatTagQuery({ include = [], exclude = [], text = '' } = {}) {
125
- return [
126
- ...normalizeTags(include).map((t) => `tag:${t}`),
127
- ...normalizeTags(exclude).map((t) => `-tag:${t}`),
128
- String(text || '').trim(),
129
- ].filter(Boolean).join(' ');
130
- }
131
-
132
- /**
133
- * Does one record's tags satisfy a filter?
134
- * Include terms are ANDed (narrowing is what filters are for); `mode:'any'` ORs them.
135
- * Exclusions always win.
136
- */
137
- export function matchesTagFilter(tags, { include = [], exclude = [] } = {}, { mode = 'all' } = {}) {
138
- const own = normalizeTags(tags);
139
- const wanted = normalizeTags(include);
140
- const banned = normalizeTags(exclude);
141
- if (banned.some((t) => own.includes(t))) return false;
142
- if (!wanted.length) return true;
143
- return mode === 'any' ? wanted.some((t) => own.includes(t)) : wanted.every((t) => own.includes(t));
144
- }
145
-
146
- const defaultGetTags = (entry) => entry?.tags;
147
-
148
- /** Filter a list of records by a parsed filter. Order is preserved. */
149
- export function filterByTags(entries, filter, getTags = defaultGetTags, opts) {
150
- const f = filter || {};
151
- if (!(f.include?.length || f.exclude?.length)) return [...(entries || [])];
152
- return (entries || []).filter((e) => matchesTagFilter(getTags(e), f, opts));
153
- }
154
-
155
- /**
156
- * Tag facets for a filter bar: every tag in the corpus with its count, most-used
157
- * first then alphabetical (so the bar is stable as counts tie). `selected` tags are
158
- * always included even at count 0, so a filter that empties the list can be undone.
159
- */
160
- export function tagFacets(entries, getTags = defaultGetTags, { limit = 0, selected = [] } = {}) {
161
- const counts = new Map();
162
- for (const entry of entries || []) {
163
- for (const tag of normalizeTags(getTags(entry))) counts.set(tag, (counts.get(tag) || 0) + 1);
164
- }
165
- for (const tag of normalizeTags(selected)) if (!counts.has(tag)) counts.set(tag, 0);
166
- const facets = [...counts.entries()]
167
- .map(([tag, count]) => ({ tag, count }))
168
- .sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag));
169
- const keep = new Set(normalizeTags(selected));
170
- if (!limit || facets.length <= limit) return facets;
171
- // Truncation must never hide an active selection — keep those, then fill by rank.
172
- const picked = facets.filter((f) => keep.has(f.tag));
173
- for (const f of facets) {
174
- if (picked.length >= limit) break;
175
- if (!keep.has(f.tag)) picked.push(f);
176
- }
177
- return picked.sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag));
178
- }
179
-
180
- /**
181
- * Suggest tags a record doesn't have yet, drawn from what's already in use —
182
- * `existing` facets ranked by count, minus what's on the record. Reusing a tag the
183
- * user coined beats inventing a synonym for it.
184
- */
185
- export function suggestExistingTags(facets, tags, { limit = 6 } = {}) {
186
- const own = new Set(normalizeTags(tags));
187
- return (facets || [])
188
- .map((f) => (typeof f === 'string' ? { tag: normalizeTag(f), count: 0 } : f))
189
- .filter((f) => f.tag && !own.has(f.tag))
190
- .slice(0, limit)
191
- .map((f) => f.tag);
192
- }
193
-
194
- /**
195
- * The line a full-text index should carry for tags. Both forms are emitted — `#design`
196
- * so an exact-text search for what the chip shows hits, and `design` so a plain word
197
- * query does too.
198
- */
199
- export function tagsSearchText(tags) {
200
- const list = normalizeTags(tags);
201
- if (!list.length) return '';
202
- return [...list.map((t) => `#${t}`), ...list].join(' ');
203
- }
1
+ const S=32,w=24;function g(t){const n=String(t??"").trim().replace(/^#+/,"");return n?n.toLowerCase().replace(/[^\p{L}\p{N}]+/gu,"-").replace(/^-+|-+$/g,"").slice(0,32).replace(/-+$/,""):""}function a(t,{max:n=24}={}){const e=[];for(const o of Array.isArray(t)?t:[]){const r=g(o);if(r&&!e.includes(r)&&e.push(r),e.length>=n)break}return e}function x(t,n){const e=g(n);return!!e&&a(t).includes(e)}function h(t,n,{max:e=24}={}){const o=a(t,{max:e}),r=g(n);return!r||o.includes(r)||o.length>=e?o:[...o,r]}function d(t,n){const e=g(n);return a(t).filter(o=>o!==e)}function G(t,n,{max:e=24}={}){return x(t,n)?d(t,n):h(t,n,{max:e})}function _(t,n){const e=a(t),o=a(n);return e.length===o.length&&e.every(r=>o.includes(r))}function M(t){const n=g(t);return n?`#${n}`:""}const m=/(^|\s)(-)?(?:tag:|#)("([^"]*)"|[^\s"]+)/giu;function A(t){const n=String(t??""),e=[],o=[];let r=n.replace(m,(u,f,i,c,s)=>{const l=g(s!==void 0?s:c);if(!l)return u;const p=i?o:e;return p.includes(l)||p.push(l),f||""});return r=r.replace(/\s+/g," ").trim(),{include:e,exclude:o,text:r}}function X(t){const{include:n,exclude:e}=A(t);return n.length>0||e.length>0}function $({include:t=[],exclude:n=[],text:e=""}={}){return[...a(t).map(o=>`tag:${o}`),...a(n).map(o=>`-tag:${o}`),String(e||"").trim()].filter(Boolean).join(" ")}function y(t,{include:n=[],exclude:e=[]}={},{mode:o="all"}={}){const r=a(t),u=a(n);return a(e).some(i=>r.includes(i))?!1:u.length?o==="any"?u.some(i=>r.includes(i)):u.every(i=>r.includes(i)):!0}const T=t=>t?.tags;function k(t,n,e=T,o){const r=n||{};return r.include?.length||r.exclude?.length?(t||[]).filter(u=>y(e(u),r,o)):[...t||[]]}function E(t,n=T,{limit:e=0,selected:o=[]}={}){const r=new Map;for(const c of t||[])for(const s of a(n(c)))r.set(s,(r.get(s)||0)+1);for(const c of a(o))r.has(c)||r.set(c,0);const u=[...r.entries()].map(([c,s])=>({tag:c,count:s})).sort((c,s)=>s.count-c.count||c.tag.localeCompare(s.tag)),f=new Set(a(o));if(!e||u.length<=e)return u;const i=u.filter(c=>f.has(c.tag));for(const c of u){if(i.length>=e)break;f.has(c.tag)||i.push(c)}return i.sort((c,s)=>s.count-c.count||c.tag.localeCompare(s.tag))}function L(t,n,{limit:e=6}={}){const o=new Set(a(n));return(t||[]).map(r=>typeof r=="string"?{tag:g(r),count:0}:r).filter(r=>r.tag&&!o.has(r.tag)).slice(0,e).map(r=>r.tag)}function b(t){const n=a(t);return n.length?[...n.map(e=>`#${e}`),...n].join(" "):""}export{w as MAX_TAGS,S as MAX_TAG_LENGTH,h as addTag,k as filterByTags,M as formatTag,$ as formatTagQuery,x as hasTag,X as hasTagTerms,y as matchesTagFilter,g as normalizeTag,a as normalizeTags,A as parseTagQuery,d as removeTag,_ as sameTags,L as suggestExistingTags,E as tagFacets,b as tagsSearchText,G as toggleTag};