@chatpanel/events 0.101.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.
Files changed (138) 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 +1 -4
  62. package/pdf-layout.js +7 -253
  63. package/project-run.js +20 -444
  64. package/project.js +1 -170
  65. package/promotion.js +1 -171
  66. package/queue.js +1 -98
  67. package/reach.js +1 -23
  68. package/recipe-tool.js +2 -161
  69. package/recipe.js +1 -264
  70. package/record-list.js +2 -110
  71. package/recruit.js +3 -456
  72. package/redaction-tokens.js +1 -61
  73. package/ref.js +1 -55
  74. package/registry.js +1 -240
  75. package/route-graph.js +1 -115
  76. package/route-strategies.js +2 -232
  77. package/router.js +2 -880
  78. package/rrf.js +1 -89
  79. package/rules.js +1 -142
  80. package/schedule.js +1 -663
  81. package/scm-connection.js +1 -180
  82. package/scopes.js +1 -12
  83. package/scorecard.js +1 -335
  84. package/search-engines.js +1 -81
  85. package/skill-manifest.js +1 -330
  86. package/skill-scan.js +3 -209
  87. package/skill-sources.js +1 -162
  88. package/skill-vars.js +2 -252
  89. package/slash-commands.js +1 -158
  90. package/source-gate.js +1 -92
  91. package/sources-retrieval.js +5 -186
  92. package/sources.js +1 -273
  93. package/store.js +1 -171
  94. package/structured.js +4 -901
  95. package/subject-kinds.js +1 -5
  96. package/subject-name.js +1 -96
  97. package/sync-plan.js +1 -213
  98. package/synthesis.js +3 -123
  99. package/tags.js +1 -203
  100. package/team-board.js +6 -337
  101. package/team-cache.js +2 -63
  102. package/team-observe.js +1 -118
  103. package/team-org.js +1 -348
  104. package/team-plan.js +4 -131
  105. package/team-record.js +1 -225
  106. package/team-run.js +9 -652
  107. package/team-subtask.js +2 -191
  108. package/team-task.js +2 -124
  109. package/team-tool.js +2 -196
  110. package/team-trail.js +2 -73
  111. package/team-worklog.js +2 -149
  112. package/team.js +1 -302
  113. package/text-search.js +1 -155
  114. package/theme.js +17 -154
  115. package/titles.js +4 -285
  116. package/tool-discovery.js +1 -87
  117. package/tool-dispatch.js +5 -230
  118. package/tool-groups.js +1 -81
  119. package/tool-hints.js +10 -185
  120. package/tool-loop-guard.js +4 -182
  121. package/tool-need.js +2 -124
  122. package/tool-result.js +2 -438
  123. package/tool-round.js +0 -0
  124. package/tool-schema.js +1 -155
  125. package/tool-traits.js +1 -182
  126. package/toolset.js +2 -88
  127. package/trajectory.js +1 -530
  128. package/turn-loop.js +4 -440
  129. package/upcast.js +1 -37
  130. package/vault.js +2 -250
  131. package/view.js +1 -88
  132. package/voice-intents.js +2 -1488
  133. package/voice-speaker.js +1 -98
  134. package/weather-tool.js +1 -65
  135. package/weather.js +2 -211
  136. package/web-search-tool.js +14 -102
  137. package/web-search.js +1 -160
  138. package/widget.js +1 -153
package/project.js CHANGED
@@ -1,170 +1 @@
1
- // A project the page a goal starts on, and the record that folds everything done for it.
2
- //
3
- // Every job or goal starts here (F8 §12): the goal, defined by its stakeholder — the *chief
4
- // executive*: a person by default, an agent from the pool when the person delegates it —
5
- // with a done-when a run can be checked against, a budget the jobs are carved from, the
6
- // repos the work happens in, and the gate that says how far a team may go without a person.
7
- // A project is data (a `projects` prefs section, both clients) and a gateway record that
8
- // folds its jobs, runs and spend from events, the way a run folds (team-record.js).
9
- //
10
- // A project's status is a small machine: draft (a goal being written) → open (jobs may be
11
- // posted) → active (a job was recruited) → done (done-when held, a person closed it) or
12
- // closed (abandoned). Nothing here runs anything: project-run.js is the executive loop.
13
-
14
- import { validateBudget, normalizeBudget } from './budget.js';
15
- import { normalizeGate, validateGate } from './gate.js';
16
-
17
- export const PROJECT_ID_RE = /^[a-z][a-z0-9_-]{0,63}$/i;
18
- export const PROJECT_STATUSES = Object.freeze(['draft', 'open', 'active', 'done', 'closed']);
19
- const NEXT = Object.freeze({ draft: ['open', 'closed'], open: ['active', 'closed', 'draft'], active: ['done', 'closed', 'open'], done: ['closed', 'open'], closed: ['open'] });
20
- export const MAX_REPOS = 16;
21
-
22
- export class ProjectError extends Error {
23
- constructor(code, message) { super(message); this.name = 'ProjectError'; this.code = code; }
24
- }
25
-
26
- const isRecord = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
27
- const clip = (s, n) => String(s || '').trim().slice(0, n);
28
-
29
- export function validateProject(p) {
30
- const errors = [];
31
- if (!isRecord(p)) return { ok: false, errors: ['project must be an object'] };
32
- if (!PROJECT_ID_RE.test(String(p.id || ''))) errors.push('id: a short identifier (letters, digits, _ -)');
33
- if (!clip(p.title, 200)) errors.push('title: what the project is called');
34
- if (!clip(p.goal, 4000)) errors.push('goal: what done looks like, in the stakeholder\'s words');
35
- if (p.doneWhen !== undefined && !clip(p.doneWhen, 2000)) errors.push('doneWhen: a check a run can be held to, or leave it out');
36
- if (p.stakeholder !== undefined && p.stakeholder !== 'person' && !/^[a-z][a-z0-9_-]{0,63}$/i.test(String(p.stakeholder))) errors.push('stakeholder: "person" or an agent id');
37
- if (p.status !== undefined && !PROJECT_STATUSES.includes(p.status)) errors.push(`status: one of ${PROJECT_STATUSES.join(', ')}`);
38
- const b = validateBudget(p.budget);
39
- if (!b.ok) errors.push(...b.errors.map((e) => `budget: ${e}`));
40
- if (p.repos !== undefined) {
41
- if (!Array.isArray(p.repos)) errors.push('repos: a list of repo ids');
42
- else if (p.repos.length > MAX_REPOS) errors.push(`repos: at most ${MAX_REPOS}`);
43
- }
44
- if (p.gate !== undefined && p.gate !== null) errors.push(...validateGate(p.gate, { partial: true }).errors.map((e) => `gate: ${e}`));
45
- return { ok: errors.length === 0, errors };
46
- }
47
-
48
- /** The stored form: defaults filled, the budget normalised, the gate (if any) normalised. */
49
- export function normalizeProject(p) {
50
- const v = validateProject(p);
51
- if (!v.ok) throw new ProjectError('INVALID', v.errors.join('; '));
52
- return {
53
- id: String(p.id),
54
- title: clip(p.title, 200),
55
- goal: clip(p.goal, 4000),
56
- doneWhen: clip(p.doneWhen, 2000),
57
- stakeholder: p.stakeholder ? String(p.stakeholder) : 'person',
58
- budget: normalizeBudget(p.budget),
59
- status: PROJECT_STATUSES.includes(p.status) ? p.status : 'draft',
60
- repos: Array.isArray(p.repos) ? [...new Set(p.repos.map((r) => clip(r, 120)).filter(Boolean))].slice(0, MAX_REPOS) : [],
61
- ...(p.gate ? { gate: normalizeGate(p.gate, { partial: true }) } : {}),
62
- tags: Array.isArray(p.tags) ? [...new Set(p.tags.map((t) => clip(t, 40)).filter(Boolean))].slice(0, 12) : [],
63
- createdBy: clip(p.createdBy, 80) || 'person',
64
- createdAt: Number(p.createdAt) || Date.now(),
65
- ...(p.updatedAt ? { updatedAt: Number(p.updatedAt) } : {}),
66
- };
67
- }
68
-
69
- export function defineProject(p) { return Object.freeze(normalizeProject(p)); }
70
-
71
- /** May the project move from `from` to `to`? The machine above; a person's close is always allowed. */
72
- export function canTransition(from, to) {
73
- return (NEXT[from] || []).includes(to);
74
- }
75
-
76
- /** A blank page for the form. */
77
- export function blankProject() {
78
- return { id: '', title: '', goal: '', doneWhen: '', stakeholder: 'person', budget: { tokens: 200000, ms: 3600000 }, status: 'draft', repos: [], tags: [] };
79
- }
80
-
81
- /** The form → a project, or the errors (the same shaping both clients use). */
82
- export function projectFromForm(form) {
83
- const budget = {};
84
- for (const k of ['tokens', 'calls', 'ms', 'usd']) {
85
- const v = Number(form?.budget?.[k]);
86
- if (form?.budget?.[k] !== '' && form?.budget?.[k] != null && Number.isFinite(v) && v > 0) budget[k] = v;
87
- }
88
- const p = {
89
- id: String(form?.id || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^[^a-z]+/, '').replace(/-+$/, '').slice(0, 64),
90
- title: form?.title, goal: form?.goal, doneWhen: form?.doneWhen, stakeholder: form?.stakeholder || 'person',
91
- budget, status: form?.status || 'draft',
92
- repos: String(Array.isArray(form?.repos) ? form.repos.join(',') : form?.repos || '').split(/[,\s]+/).filter(Boolean),
93
- tags: String(Array.isArray(form?.tags) ? form.tags.join(',') : form?.tags || '').split(/[,\s]+/).filter(Boolean),
94
- ...(form?.gate ? { gate: form.gate } : {}), ...(form?.createdAt ? { createdAt: form.createdAt } : {}),
95
- };
96
- const v = validateProject(p);
97
- return v.ok ? { ok: true, project: normalizeProject(p) } : { ok: false, errors: v.errors };
98
- }
99
-
100
- // ── the record: a project folded from its events ────────────────────────────────────────
101
-
102
- /** The empty record — what the gateway holds per project and both clients read. */
103
- export function emptyProjectRecord({ id, now = Date.now() } = {}) {
104
- return { id, page: null, status: 'draft', jobs: [], runs: [], spend: { tokens: 0, calls: 0, usd: 0, ms: 0 }, decisions: [], report: null, createdAt: now, lastEventAt: now };
105
- }
106
-
107
- /**
108
- * Fold one event into the record. Events: `project.created` `{ project }` · `project.updated`
109
- * `{ project }` · `project.status` `{ status, by, note }` · `job.posted` `{ job }` ·
110
- * `job.updated` `{ job }` (any field: applications, recruited, status) · `run.linked`
111
- * `{ runId, jobId }` · `run.spent` `{ runId, spent }` · `project.decision` `{ by, kind, text,
112
- * refs }` · `project.report` `{ text, by }`. Idempotent by job id and run id.
113
- */
114
- export function foldProject(rec, ev) {
115
- const type = String(ev?.type || '');
116
- const p = ev?.payload && typeof ev.payload === 'object' ? ev.payload : (ev || {});
117
- const at = Number(ev?.at) || Date.now();
118
- rec.lastEventAt = at;
119
- switch (type) {
120
- case 'project.created':
121
- case 'project.updated':
122
- if (p.project && typeof p.project === 'object') { rec.page = { ...p.project, updatedAt: at }; rec.status = p.project.status || rec.status; }
123
- break;
124
- case 'project.status':
125
- if (PROJECT_STATUSES.includes(p.status)) { rec.status = p.status; if (rec.page) rec.page.status = p.status; rec.decisions.push({ at, by: p.by || 'person', kind: 'status', text: `${p.status}${p.note ? ` — ${p.note}` : ''}` }); }
126
- break;
127
- case 'job.posted':
128
- if (p.job?.id && !rec.jobs.some((j) => j.id === p.job.id)) rec.jobs.push({ ...p.job, postedAt: at });
129
- if (rec.status === 'draft') rec.status = 'open';
130
- break;
131
- case 'job.updated': {
132
- const i = rec.jobs.findIndex((j) => j.id === p.job?.id);
133
- if (i >= 0) rec.jobs[i] = { ...rec.jobs[i], ...p.job, updatedAt: at };
134
- if (p.job?.status === 'recruited' || p.job?.status === 'in-progress') { if (rec.status === 'open' || rec.status === 'draft') rec.status = 'active'; }
135
- break;
136
- }
137
- case 'run.linked':
138
- if (p.runId && !rec.runs.some((r) => r.runId === p.runId)) rec.runs.push({ runId: p.runId, jobId: p.jobId || null, at });
139
- break;
140
- case 'run.spent': {
141
- const r = rec.runs.find((x) => x.runId === p.runId);
142
- const prev = r?.spent || { tokens: 0, calls: 0, usd: 0, ms: 0 };
143
- const next = { tokens: Number(p.spent?.tokens) || 0, calls: Number(p.spent?.calls) || 0, usd: Number(p.spent?.usd) || 0, ms: Number(p.spent?.ms) || 0 };
144
- // A run reports its running total; the project's spend is the sum of every run's latest.
145
- for (const k of Object.keys(next)) rec.spend[k] = Math.max(0, (rec.spend[k] || 0) - (prev[k] || 0) + next[k]);
146
- if (r) r.spent = next; else rec.runs.push({ runId: p.runId, jobId: null, at, spent: next });
147
- break;
148
- }
149
- case 'project.decision':
150
- rec.decisions.push({ at, by: p.by || 'person', kind: p.kind || 'note', text: String(p.text || '').slice(0, 2000), refs: Array.isArray(p.refs) ? p.refs.slice(0, 8) : [] });
151
- break;
152
- case 'project.report':
153
- rec.report = { text: String(p.text || ''), by: p.by || 'runner', at };
154
- break;
155
- default: break;
156
- }
157
- return rec;
158
- }
159
-
160
- /** How far along: jobs by status, spend against the budget, whether done-when is claimed. */
161
- export function projectProgress(rec) {
162
- const by = {};
163
- for (const j of rec?.jobs || []) by[j.status] = (by[j.status] || 0) + 1;
164
- const cap = rec?.page?.budget || {};
165
- const spend = rec?.spend || {};
166
- const pct = cap.tokens ? Math.min(1, (spend.tokens || 0) / cap.tokens) : cap.usd ? Math.min(1, (spend.usd || 0) / cap.usd) : cap.ms ? Math.min(1, (spend.ms || 0) / cap.ms) : null;
167
- const total = (rec?.jobs || []).length;
168
- const done = by.done || 0;
169
- return { jobs: { total, by }, done, open: (by.open || 0) + (by.evaluating || 0) + (by.recruited || 0) + (by['in-progress'] || 0), spend, cap, budgetUsed: pct, hasReport: !!rec?.report, status: rec?.status || 'draft' };
170
- }
1
+ import{validateBudget as g,normalizeBudget as f}from"./budget.js";import{normalizeGate as j,validateGate as h}from"./gate.js";const k=/^[a-z][a-z0-9_-]{0,63}$/i,l=Object.freeze(["draft","open","active","done","closed"]),y=Object.freeze({draft:["open","closed"],open:["active","closed","draft"],active:["done","closed","open"],done:["closed","open"],closed:["open"]}),c=16;class A extends Error{constructor(s,o){super(o),this.name="ProjectError",this.code=s}}const m=t=>t!==null&&typeof t=="object"&&!Array.isArray(t),a=(t,s)=>String(t||"").trim().slice(0,s);function p(t){const s=[];if(!m(t))return{ok:!1,errors:["project must be an object"]};k.test(String(t.id||""))||s.push("id: a short identifier (letters, digits, _ -)"),a(t.title,200)||s.push("title: what the project is called"),a(t.goal,4e3)||s.push("goal: what done looks like, in the stakeholder's words"),t.doneWhen!==void 0&&!a(t.doneWhen,2e3)&&s.push("doneWhen: a check a run can be held to, or leave it out"),t.stakeholder!==void 0&&t.stakeholder!=="person"&&!/^[a-z][a-z0-9_-]{0,63}$/i.test(String(t.stakeholder))&&s.push('stakeholder: "person" or an agent id'),t.status!==void 0&&!l.includes(t.status)&&s.push(`status: one of ${l.join(", ")}`);const o=g(t.budget);return o.ok||s.push(...o.errors.map(e=>`budget: ${e}`)),t.repos!==void 0&&(Array.isArray(t.repos)?t.repos.length>c&&s.push(`repos: at most ${c}`):s.push("repos: a list of repo ids")),t.gate!==void 0&&t.gate!==null&&s.push(...h(t.gate,{partial:!0}).errors.map(e=>`gate: ${e}`)),{ok:s.length===0,errors:s}}function b(t){const s=p(t);if(!s.ok)throw new A("INVALID",s.errors.join("; "));return{id:String(t.id),title:a(t.title,200),goal:a(t.goal,4e3),doneWhen:a(t.doneWhen,2e3),stakeholder:t.stakeholder?String(t.stakeholder):"person",budget:f(t.budget),status:l.includes(t.status)?t.status:"draft",repos:Array.isArray(t.repos)?[...new Set(t.repos.map(o=>a(o,120)).filter(Boolean))].slice(0,c):[],...t.gate?{gate:j(t.gate,{partial:!0})}:{},tags:Array.isArray(t.tags)?[...new Set(t.tags.map(o=>a(o,40)).filter(Boolean))].slice(0,12):[],createdBy:a(t.createdBy,80)||"person",createdAt:Number(t.createdAt)||Date.now(),...t.updatedAt?{updatedAt:Number(t.updatedAt)}:{}}}function I(t){return Object.freeze(b(t))}function z(t,s){return(y[t]||[]).includes(s)}function P(){return{id:"",title:"",goal:"",doneWhen:"",stakeholder:"person",budget:{tokens:2e5,ms:36e5},status:"draft",repos:[],tags:[]}}function w(t){const s={};for(const n of["tokens","calls","ms","usd"]){const r=Number(t?.budget?.[n]);t?.budget?.[n]!==""&&t?.budget?.[n]!=null&&Number.isFinite(r)&&r>0&&(s[n]=r)}const o={id:String(t?.id||"").trim().toLowerCase().replace(/[^a-z0-9_-]+/g,"-").replace(/^[^a-z]+/,"").replace(/-+$/,"").slice(0,64),title:t?.title,goal:t?.goal,doneWhen:t?.doneWhen,stakeholder:t?.stakeholder||"person",budget:s,status:t?.status||"draft",repos:String(Array.isArray(t?.repos)?t.repos.join(","):t?.repos||"").split(/[,\s]+/).filter(Boolean),tags:String(Array.isArray(t?.tags)?t.tags.join(","):t?.tags||"").split(/[,\s]+/).filter(Boolean),...t?.gate?{gate:t.gate}:{},...t?.createdAt?{createdAt:t.createdAt}:{}},e=p(o);return e.ok?{ok:!0,project:b(o)}:{ok:!1,errors:e.errors}}function E({id:t,now:s=Date.now()}={}){return{id:t,page:null,status:"draft",jobs:[],runs:[],spend:{tokens:0,calls:0,usd:0,ms:0},decisions:[],report:null,createdAt:s,lastEventAt:s}}function N(t,s){const o=String(s?.type||""),e=s?.payload&&typeof s.payload=="object"?s.payload:s||{},n=Number(s?.at)||Date.now();switch(t.lastEventAt=n,o){case"project.created":case"project.updated":e.project&&typeof e.project=="object"&&(t.page={...e.project,updatedAt:n},t.status=e.project.status||t.status);break;case"project.status":l.includes(e.status)&&(t.status=e.status,t.page&&(t.page.status=e.status),t.decisions.push({at:n,by:e.by||"person",kind:"status",text:`${e.status}${e.note?` — ${e.note}`:""}`}));break;case"job.posted":e.job?.id&&!t.jobs.some(r=>r.id===e.job.id)&&t.jobs.push({...e.job,postedAt:n}),t.status==="draft"&&(t.status="open");break;case"job.updated":{const r=t.jobs.findIndex(u=>u.id===e.job?.id);r>=0&&(t.jobs[r]={...t.jobs[r],...e.job,updatedAt:n}),(e.job?.status==="recruited"||e.job?.status==="in-progress")&&(t.status==="open"||t.status==="draft")&&(t.status="active");break}case"run.linked":e.runId&&!t.runs.some(r=>r.runId===e.runId)&&t.runs.push({runId:e.runId,jobId:e.jobId||null,at:n});break;case"run.spent":{const r=t.runs.find(i=>i.runId===e.runId),u=r?.spent||{tokens:0,calls:0,usd:0,ms:0},d={tokens:Number(e.spent?.tokens)||0,calls:Number(e.spent?.calls)||0,usd:Number(e.spent?.usd)||0,ms:Number(e.spent?.ms)||0};for(const i of Object.keys(d))t.spend[i]=Math.max(0,(t.spend[i]||0)-(u[i]||0)+d[i]);r?r.spent=d:t.runs.push({runId:e.runId,jobId:null,at:n,spent:d});break}case"project.decision":t.decisions.push({at:n,by:e.by||"person",kind:e.kind||"note",text:String(e.text||"").slice(0,2e3),refs:Array.isArray(e.refs)?e.refs.slice(0,8):[]});break;case"project.report":t.report={text:String(e.text||""),by:e.by||"runner",at:n};break;default:break}return t}function $(t){const s={};for(const d of t?.jobs||[])s[d.status]=(s[d.status]||0)+1;const o=t?.page?.budget||{},e=t?.spend||{},n=o.tokens?Math.min(1,(e.tokens||0)/o.tokens):o.usd?Math.min(1,(e.usd||0)/o.usd):o.ms?Math.min(1,(e.ms||0)/o.ms):null,r=(t?.jobs||[]).length,u=s.done||0;return{jobs:{total:r,by:s},done:u,open:(s.open||0)+(s.evaluating||0)+(s.recruited||0)+(s["in-progress"]||0),spend:e,cap:o,budgetUsed:n,hasReport:!!t?.report,status:t?.status||"draft"}}export{c as MAX_REPOS,k as PROJECT_ID_RE,l as PROJECT_STATUSES,A as ProjectError,P as blankProject,z as canTransition,I as defineProject,E as emptyProjectRecord,N as foldProject,b as normalizeProject,w as projectFromForm,$ as projectProgress,p as validateProject};
package/promotion.js CHANGED
@@ -1,171 +1 @@
1
- // PROMOTION the gate between "a model wrote this" and "the brief says this".
2
- //
3
- // The single most repeated finding in the community threads was that unreviewed agent
4
- // writing compounds into confident nonsense (C2), and the convergent answer was to separate
5
- // CAPTURE from PROMOTION: agents draft freely, promotion needs review. This module is that
6
- // separation as code. Class-R claims never pass through it — a backlink is not an opinion —
7
- // and class-C claims never get around it.
8
- //
9
- // proposed → promoted only via accept()
10
- // proposed → rejected via reject()
11
- //
12
- // A proposal is its own object rather than a mutation of the brief, so the queue survives a
13
- // rebuild (I-K2 throws briefs away; it must not throw away the user's pending decisions),
14
- // and so accepting shows a DIFF against what the brief said before rather than a fait
15
- // accompli.
16
- //
17
- // `converge()` is W7's rule written before W7 exists: N independent drafts agree on a claim
18
- // ⇒ it may be auto-proposed; disagreement ⇒ the human queue. The reviewer is a different
19
- // appointment from the drafters or convergence measures nothing.
20
-
21
- import { contentHash } from './knowledge.js';
22
- import { normalizeSubject } from './subject-name.js';
23
-
24
- export const PROPOSAL_STATES = Object.freeze(['proposed', 'accepted', 'rejected']);
25
-
26
- /** A new claim replaces an old one when this much of its vocabulary overlaps. */
27
- export const SUPERSEDE_OVERLAP = 0.5;
28
-
29
- /**
30
- * BACKLINKS — the deterministic post-pass that turns cited prose into a linked wiki.
31
- *
32
- * A synthesised claim cites RECORDS, never another brief, so an accepted "Jordan Blake owns
33
- * the Atlas rollback plan" told a reader nothing about where Jordan's page was. This finds
34
- * every other subject named in a claim's text — whole-word, by canonical name or alias — and
35
- * adds a `brief:` ref beside the record refs. No model: the names are known and the text is
36
- * in front of us, which is exactly the kind of work the design says must stay class R.
37
- *
38
- * `subjects` is the brief index: `[{ id, name, aliases?, kind? }]`. The claim's own brief is
39
- * never linked to itself. Refs already present are kept and not duplicated.
40
- */
41
- export function linkClaims(claims = [], subjects = [], { selfId = '' } = {}) {
42
- const targets = [];
43
- for (const s of subjects) {
44
- if (!s?.id || s.id === selfId) continue;
45
- const names = [s.name, ...(s.aliases || [])].map(normalizeSubject).filter((n) => n && n.length >= 3);
46
- if (names.length) targets.push({ id: s.id, names });
47
- }
48
- if (!targets.length) return claims;
49
- return claims.map((c) => {
50
- const hay = ` ${normalizeSubject(c.text)} `;
51
- const have = new Set((c.refs || []).map((r) => `${r.kind}:${r.id}`));
52
- const added = [];
53
- for (const t of targets) {
54
- if (have.has(`brief:${t.id}`)) continue;
55
- if (t.names.some((n) => hay.includes(` ${n} `))) {
56
- added.push({ kind: 'brief', id: t.id, hash: contentHash(t.id) });
57
- have.add(`brief:${t.id}`);
58
- }
59
- }
60
- return added.length ? { ...c, refs: [...(c.refs || []), ...added] } : c;
61
- });
62
- }
63
-
64
- /** A pending synthesis for one brief. Never carries `promoted`. */
65
- export function propose({ briefId, claims = [], summary = '', by = 'model', now = 0, newId = null } = {}) {
66
- if (!briefId) throw new TypeError('propose: briefId required');
67
- const bad = claims.find((c) => c?.cls !== 'C' || !c?.refs?.length);
68
- if (bad) throw new TypeError('propose: only class-C claims with refs may be proposed');
69
- return {
70
- id: newId ? newId() : `p-${briefId}-${now}`,
71
- briefId,
72
- by: String(by),
73
- at: now,
74
- state: 'proposed',
75
- claims: claims.map((c) => ({ ...c, state: 'proposed' })),
76
- summary: String(summary || ''),
77
- };
78
- }
79
-
80
- /**
81
- * Accept: the proposal's claims join the brief as promoted class-C claims. The ONLY path.
82
- * Returns a new brief and the settled proposal; mutates neither input.
83
- */
84
- export function accept(brief, proposal, { now = 0, subjects = [] } = {}) {
85
- if (!brief || !proposal || proposal.briefId !== brief.id) throw new TypeError('accept: proposal does not belong to this brief');
86
- if (proposal.state !== 'proposed') throw new TypeError(`accept: proposal is ${proposal.state}`);
87
- let promoted = proposal.claims.map((c) => ({ ...c, state: 'promoted', lastConfirmed: now }));
88
- // Backlinks, at the moment of acceptance — the one point where the claim is final and the
89
- // set of other pages is known.
90
- promoted = linkClaims(promoted, subjects, { selfId: brief.id });
91
-
92
- // SUPERSESSION. "Cutover is planned for Q3" and "Cutover moved to Q4" must not sit on one
93
- // page as two contradicting lines. The old claim is not deleted — that would lose "when did
94
- // this change" — it is marked superseded by the new one, and keeps its refs, so the brief
95
- // reads as a history rather than an argument with itself. The rule is the same overlap
96
- // diffProposal shows the reviewer, so what they accepted is what happens.
97
- const diff = diffProposal(brief, proposal);
98
- const superseded = new Map(); // old text -> new claim id
99
- diff.forEach((d, i) => { if (d.replaces && promoted[i]) superseded.set(d.replaces, promoted[i].id); });
100
- const kept = brief.claims.map((c) => (superseded.has(c.text) && !c.supersededBy
101
- ? { ...c, supersededBy: superseded.get(c.text), supersededAt: now }
102
- : c));
103
- promoted = promoted.map((c, i) => (diff[i]?.replaces ? { ...c, supersedes: brief.claims.find((o) => o.text === diff[i].replaces)?.id || null } : c));
104
-
105
- const next = {
106
- ...brief,
107
- claims: [...kept, ...promoted],
108
- summary: proposal.summary || brief.summary || '',
109
- updatedAt: now,
110
- };
111
- return { brief: next, proposal: { ...proposal, state: 'accepted', settledAt: now, claims: promoted } };
112
- }
113
-
114
- export function reject(proposal, { now = 0, why = '' } = {}) {
115
- if (!proposal || proposal.state !== 'proposed') throw new TypeError('reject: not a pending proposal');
116
- return { ...proposal, state: 'rejected', settledAt: now, why: String(why || '') };
117
- }
118
-
119
- /**
120
- * The diff a reviewer sees: what is new against the brief, and what a new claim
121
- * contradicts or supersedes. Deterministic and cheap — same-subject, same-`when`-or-later
122
- * claims that share enough words are shown as "replaces", which is a suggestion the
123
- * reviewer confirms, never an edit.
124
- */
125
- export function diffProposal(brief, proposal) {
126
- const existing = (brief?.claims || []).map((c) => c.text);
127
- const tokens = (t) => new Set(String(t).toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((w) => w.length > 3));
128
- return proposal.claims.map((c) => {
129
- const mine = tokens(c.text);
130
- let best = null; let bestScore = 0;
131
- for (const e of existing) {
132
- const theirs = tokens(e);
133
- let hit = 0; for (const w of mine) if (theirs.has(w)) hit += 1;
134
- const score = mine.size ? hit / mine.size : 0;
135
- if (score > bestScore) { bestScore = score; best = e; }
136
- }
137
- return { claim: c, replaces: bestScore >= SUPERSEDE_OVERLAP ? best : null, overlap: bestScore };
138
- });
139
- }
140
-
141
- /**
142
- * W7's rule. Given several INDEPENDENT drafts of the same subject, the claims that at least
143
- * `minAgree` drafts made (by near-identical text) may be proposed automatically; the rest go
144
- * to the human queue. "Agree" is a text-overlap test, not equality — two models never phrase
145
- * a thing identically, and requiring it would make convergence measure nothing.
146
- */
147
- export function converge(drafts = [], { minAgree = 2, threshold = 0.6 } = {}) {
148
- const tokens = (t) => new Set(String(t).toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((w) => w.length > 3));
149
- const all = drafts.flatMap((d, i) => (d?.claims || []).map((c) => ({ c, draft: i, t: tokens(c.text) })));
150
- const agreed = [];
151
- const disputed = [];
152
- const used = new Set();
153
- for (let i = 0; i < all.length; i += 1) {
154
- if (used.has(i)) continue;
155
- const group = [i];
156
- for (let j = i + 1; j < all.length; j += 1) {
157
- if (used.has(j) || all[j].draft === all[i].draft) continue;
158
- let hit = 0; for (const w of all[i].t) if (all[j].t.has(w)) hit += 1;
159
- const score = all[i].t.size ? hit / all[i].t.size : 0;
160
- if (score >= threshold) group.push(j);
161
- }
162
- const drafters = new Set(group.map((g) => all[g].draft));
163
- for (const g of group) used.add(g);
164
- // The union of refs across agreeing drafts: agreement on the claim is evidence, and so
165
- // is every record any of them cited for it.
166
- const refs = [...new Map(group.flatMap((g) => all[g].c.refs || []).map((r) => [`${r.kind}:${r.id}`, r])).values()];
167
- const merged = { ...all[i].c, refs, agreedBy: drafters.size };
168
- (drafters.size >= minAgree ? agreed : disputed).push(merged);
169
- }
170
- return { agreed, disputed };
171
- }
1
+ import{contentHash as w}from"./knowledge.js";import{normalizeSubject as h}from"./subject-name.js";const b=Object.freeze(["proposed","accepted","rejected"]),x=.5;function y(r=[],n=[],{selfId:c=""}={}){const f=[];for(const e of n){if(!e?.id||e.id===c)continue;const o=[e.name,...e.aliases||[]].map(h).filter(a=>a&&a.length>=3);o.length&&f.push({id:e.id,names:o})}return f.length?r.map(e=>{const o=` ${h(e.text)} `,a=new Set((e.refs||[]).map(t=>`${t.kind}:${t.id}`)),i=[];for(const t of f)a.has(`brief:${t.id}`)||t.names.some(s=>o.includes(` ${s} `))&&(i.push({kind:"brief",id:t.id,hash:w(t.id)}),a.add(`brief:${t.id}`));return i.length?{...e,refs:[...e.refs||[],...i]}:e}):r}function j({briefId:r,claims:n=[],summary:c="",by:f="model",now:e=0,newId:o=null}={}){if(!r)throw new TypeError("propose: briefId required");if(n.find(i=>i?.cls!=="C"||!i?.refs?.length))throw new TypeError("propose: only class-C claims with refs may be proposed");return{id:o?o():`p-${r}-${e}`,briefId:r,by:String(f),at:e,state:"proposed",claims:n.map(i=>({...i,state:"proposed"})),summary:String(c||"")}}function k(r,n,{now:c=0,subjects:f=[]}={}){if(!r||!n||n.briefId!==r.id)throw new TypeError("accept: proposal does not belong to this brief");if(n.state!=="proposed")throw new TypeError(`accept: proposal is ${n.state}`);let e=n.claims.map(s=>({...s,state:"promoted",lastConfirmed:c}));e=y(e,f,{selfId:r.id});const o=S(r,n),a=new Map;o.forEach((s,d)=>{s.replaces&&e[d]&&a.set(s.replaces,e[d].id)});const i=r.claims.map(s=>a.has(s.text)&&!s.supersededBy?{...s,supersededBy:a.get(s.text),supersededAt:c}:s);return e=e.map((s,d)=>o[d]?.replaces?{...s,supersedes:r.claims.find(l=>l.text===o[d].replaces)?.id||null}:s),{brief:{...r,claims:[...i,...e],summary:n.summary||r.summary||"",updatedAt:c},proposal:{...n,state:"accepted",settledAt:c,claims:e}}}function A(r,{now:n=0,why:c=""}={}){if(!r||r.state!=="proposed")throw new TypeError("reject: not a pending proposal");return{...r,state:"rejected",settledAt:n,why:String(c||"")}}function S(r,n){const c=(r?.claims||[]).map(e=>e.text),f=e=>new Set(String(e).toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(o=>o.length>3));return n.claims.map(e=>{const o=f(e.text);let a=null,i=0;for(const t of c){const s=f(t);let d=0;for(const m of o)s.has(m)&&(d+=1);const l=o.size?d/o.size:0;l>i&&(i=l,a=t)}return{claim:e,replaces:i>=x?a:null,overlap:i}})}function T(r=[],{minAgree:n=2,threshold:c=.6}={}){const f=t=>new Set(String(t).toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(s=>s.length>3)),e=r.flatMap((t,s)=>(t?.claims||[]).map(d=>({c:d,draft:s,t:f(d.text)}))),o=[],a=[],i=new Set;for(let t=0;t<e.length;t+=1){if(i.has(t))continue;const s=[t];for(let p=t+1;p<e.length;p+=1){if(i.has(p)||e[p].draft===e[t].draft)continue;let u=0;for(const g of e[t].t)e[p].t.has(g)&&(u+=1);(e[t].t.size?u/e[t].t.size:0)>=c&&s.push(p)}const d=new Set(s.map(p=>e[p].draft));for(const p of s)i.add(p);const l=[...new Map(s.flatMap(p=>e[p].c.refs||[]).map(p=>[`${p.kind}:${p.id}`,p])).values()],m={...e[t].c,refs:l,agreedBy:d.size};(d.size>=n?o:a).push(m)}return{agreed:o,disputed:a}}export{b as PROPOSAL_STATES,x as SUPERSEDE_OVERLAP,k as accept,T as converge,S as diffProposal,y as linkClaims,j as propose,A as reject};
package/queue.js CHANGED
@@ -1,98 +1 @@
1
- // THE SEND QUEUE what someone typed while a reply was still streaming, and what they are
2
- // allowed to do about it before it is answered.
3
- //
4
- // Typing over a running answer is not a mistake to be prevented; it is how people think. But
5
- // a queue you can only ADD to is a trap, and in three specific ways:
6
- //
7
- // • the thought that arrives mid-answer is often the URGENT one — it should not have to
8
- // wait out a reply it was meant to redirect (steer);
9
- // • the reply frequently answers a queued question before it is ever sent, and sending it
10
- // anyway spends a turn on something nobody wants any more (dequeue);
11
- // • the order things occur to you is not the order they should be asked in (reorder).
12
- //
13
- // The queue is deliberately NOT a second list living beside the transcript. It IS the
14
- // trailing run of user messages that has no reply after it — exactly the set a client
15
- // answers when the current stream ends. One definition means what is drawn and what is sent
16
- // can never disagree, and a message cannot be "in the queue" but missing from the turn.
17
- //
18
- // Every function here is a pure transform: it returns a NEW array, or the array it was given
19
- // (by identity) when the operation was a no-op, so a caller can tell "nothing changed" from
20
- // "changed" without diffing. No clock, no ids minted, no DOM, no storage — the panel, a
21
- // mobile client, the gateway and the bridge can all reason about the same queue.
22
- //
23
- // STEERING, honestly: no engine we speak to accepts new input into a run already in flight
24
- // (the bridge closes a CLI's stdin with the prompt; HTTP streaming has no upstream channel).
25
- // So "send now" is promote-then-interrupt: the partial answer stays in the transcript as
26
- // context, and the next turn opens with the message the user wanted read first. This module
27
- // owns the ORDER half of that — the client owns the abort.
28
-
29
- const isUser = (m) => !!m && m.role === 'user';
30
-
31
- /**
32
- * The pending queue: the trailing run of user messages with no reply after it.
33
- *
34
- * Returned oldest-first — the order they will be sent in — as
35
- * `{ message, id, index, position }`, where `index` is the position in the whole
36
- * transcript and `position` the position within the queue.
37
- */
38
- export function pendingQueue(messages) {
39
- const list = Array.isArray(messages) ? messages : [];
40
- let start = list.length;
41
- while (start > 0 && isUser(list[start - 1])) start--;
42
- const out = [];
43
- for (let i = start; i < list.length; i++) {
44
- out.push({ message: list[i], id: list[i].id, index: i, position: i - start });
45
- }
46
- return out;
47
- }
48
-
49
- /** Is this message currently in the queue (rather than already answered)? */
50
- export function isQueued(messages, id) {
51
- return pendingQueue(messages).some((e) => e.id === id);
52
- }
53
-
54
- /**
55
- * Drop a queued message — the question the running reply already answered.
56
- *
57
- * Only ever removes from the pending run: an id from further up the transcript is a
58
- * no-op, so a stale click can never delete a turn that has already been answered.
59
- */
60
- export function dequeue(messages, id) {
61
- const queue = pendingQueue(messages);
62
- const hit = queue.find((e) => e.id === id);
63
- if (!hit) return messages;
64
- const next = messages.slice();
65
- next.splice(hit.index, 1);
66
- return next;
67
- }
68
-
69
- /** Move a queued message by `delta` places (-1 up, +1 down). Clamped: past either end is a no-op. */
70
- export function moveQueued(messages, id, delta) {
71
- const queue = pendingQueue(messages);
72
- const from = queue.findIndex((e) => e.id === id);
73
- if (from < 0) return messages;
74
- const step = Math.trunc(Number(delta) || 0);
75
- const to = from + step;
76
- if (!step || to < 0 || to >= queue.length) return messages;
77
- return reordered(messages, queue, from, to);
78
- }
79
-
80
- /**
81
- * Put a queued message at the FRONT — the ordering half of "send now".
82
- *
83
- * The rest of the queue keeps its relative order behind it: steering is a statement about
84
- * what to read first, not an instruction to throw away everything else that was typed.
85
- */
86
- export function promoteQueued(messages, id) {
87
- const queue = pendingQueue(messages);
88
- const from = queue.findIndex((e) => e.id === id);
89
- if (from <= 0) return messages; // absent, or already first
90
- return reordered(messages, queue, from, 0);
91
- }
92
-
93
- function reordered(messages, queue, from, to) {
94
- const block = queue.map((e) => e.message);
95
- const [moved] = block.splice(from, 1);
96
- block.splice(to, 0, moved);
97
- return messages.slice(0, queue[0].index).concat(block);
98
- }
1
+ const l=n=>!!n&&n.role==="user";function c(n){const e=Array.isArray(n)?n:[];let r=e.length;for(;r>0&&l(e[r-1]);)r--;const o=[];for(let t=r;t<e.length;t++)o.push({message:e[t],id:e[t].id,index:t,position:t-r});return o}function p(n,e){return c(n).some(r=>r.id===e)}function x(n,e){const o=c(n).find(i=>i.id===e);if(!o)return n;const t=n.slice();return t.splice(o.index,1),t}function s(n,e,r){const o=c(n),t=o.findIndex(f=>f.id===e);if(t<0)return n;const i=Math.trunc(Number(r)||0),u=t+i;return!i||u<0||u>=o.length?n:d(n,o,t,u)}function h(n,e){const r=c(n),o=r.findIndex(t=>t.id===e);return o<=0?n:d(n,r,o,0)}function d(n,e,r,o){const t=e.map(u=>u.message),[i]=t.splice(r,1);return t.splice(o,0,i),n.slice(0,e[0].index).concat(t)}export{x as dequeue,p as isQueued,s as moveQueued,c as pendingQueue,h as promoteQueued};
package/reach.js CHANGED
@@ -1,23 +1 @@
1
- // reach.js — how far a request may travel, on its own so it can travel alone.
2
- //
3
- // One ordered vocabulary answers "how far may this go" for three very different callers: the
4
- // model router ranks a model's reach against a turn's requirement, pairing gives a phone a
5
- // ceiling, and the bridge maps that ceiling to a toolset. Three declarations, one vocabulary,
6
- // or "how far may this reach" gets three answers that drift.
7
- //
8
- // A separate module rather than a constant inside router.js for the reason scopes.js exists:
9
- // the consumers have wildly different weights. The bridge has zero runtime dependencies by
10
- // design and vendors what it needs — pulling the 50 KB model router in to reach a
11
- // three-element array is exactly the transitive-graph mistake that split DATA_SCOPES out of
12
- // capability.js.
13
- //
14
- // ORDERED, least to most: a ceiling comparison is an index comparison.
15
- export const REACH = Object.freeze(['device', 'trusted', 'any']);
16
-
17
- /** Position in the ladder; an unknown tier ranks LOWEST, so a typo never widens reach. */
18
- export const reachRank = (r) => Math.max(0, REACH.indexOf(r));
19
-
20
- /** Does `have` satisfy `need`? Fails closed — an unknown `have` is treated as 'device'. */
21
- export function reachSatisfies(have, need) {
22
- return reachRank(have) >= reachRank(need);
23
- }
1
+ const n=Object.freeze(["device","trusted","any"]),t=e=>Math.max(0,n.indexOf(e));function a(e,r){return t(e)>=t(r)}export{n as REACH,t as reachRank,a as reachSatisfies};