@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.
- package/adapters.js +1 -83
- package/adaptive-tool-policy.js +1 -45
- package/agent.js +2 -255
- package/attribution.js +1 -132
- package/backup-envelope.js +1 -221
- package/board-tool.js +2 -165
- package/budget.js +1 -116
- package/capability.js +1 -126
- package/citations.js +5 -127
- package/client-prefs.js +1 -148
- package/context-attachments.js +8 -144
- package/cowriter-router.js +1 -83
- package/cowriter-writer.js +19 -193
- package/cowriter.js +1 -190
- package/curate.js +3 -509
- package/distance.js +1 -124
- package/engine.js +1 -131
- package/entitlement.js +1 -332
- package/entity.js +1 -354
- package/event.js +1 -175
- package/extraction.js +4 -293
- package/failover.js +1 -93
- package/find-tool.js +1 -66
- package/flowchart.js +2 -644
- package/gate.js +1 -74
- package/harness.js +4 -101
- package/index.js +1 -345
- package/invariants.js +1 -174
- package/job.js +3 -147
- package/kernel.js +1 -255
- package/knowledge-derive.js +1 -267
- package/knowledge.js +3 -230
- package/library.js +11 -280
- package/loop.js +1 -132
- package/manifest.js +1 -107
- package/markdown-authoring.js +11 -288
- package/markdown-render.js +0 -0
- package/mcp-client.js +8 -358
- package/mcp-dispatch.js +1 -52
- package/mcp-errors.js +1 -106
- package/mcp-manager.js +1 -127
- package/media-transcript.js +8 -531
- package/meeting-analyzers.js +1 -83
- package/meeting-insights.js +7 -164
- package/meeting-shape.js +1 -282
- package/meeting-text.js +5 -94
- package/memory.js +2 -638
- package/model-candidates.js +1 -358
- package/model-health.js +1 -182
- package/model-ledger.js +1 -228
- package/model-picker.js +1 -188
- package/note-actions.js +15 -246
- package/note-graph.js +1 -158
- package/note-links.js +1 -68
- package/note-mentions.js +8 -114
- package/note-plan.js +9 -107
- package/note-research.js +1 -135
- package/observability.js +1 -119
- package/omni.js +1 -125
- package/order.js +1 -78
- package/package.json +4 -5
- package/pdf-layout.js +7 -253
- package/policy.js +1 -0
- package/project-run.js +20 -444
- package/project.js +1 -170
- package/promotion.js +1 -171
- package/queue.js +1 -98
- package/reach.js +1 -23
- package/recipe-tool.js +2 -161
- package/recipe.js +1 -264
- package/record-list.js +2 -110
- package/recruit.js +3 -456
- package/redaction-tokens.js +1 -61
- package/ref.js +1 -55
- package/registry.js +1 -240
- package/route-graph.js +1 -115
- package/route-strategies.js +2 -232
- package/router.js +2 -880
- package/rrf.js +1 -89
- package/rules.js +1 -142
- package/schedule.js +1 -663
- package/scm-connection.js +1 -180
- package/scopes.js +1 -12
- package/scorecard.js +1 -335
- package/search-engines.js +1 -81
- package/skill-manifest.js +1 -330
- package/skill-scan.js +3 -209
- package/skill-sources.js +1 -162
- package/skill-vars.js +2 -252
- package/slash-commands.js +1 -158
- package/source-gate.js +1 -92
- package/sources-retrieval.js +5 -186
- package/sources.js +1 -273
- package/store.js +1 -171
- package/structured.js +4 -901
- package/subject-kinds.js +1 -5
- package/subject-name.js +1 -96
- package/sync-plan.js +1 -213
- package/synthesis.js +3 -123
- package/tags.js +1 -203
- package/team-board.js +6 -337
- package/team-cache.js +2 -63
- package/team-observe.js +1 -118
- package/team-org.js +1 -442
- package/team-plan.js +4 -131
- package/team-record.js +1 -225
- package/team-run.js +9 -652
- package/team-subtask.js +2 -191
- package/team-task.js +2 -124
- package/team-tool.js +2 -196
- package/team-trail.js +2 -73
- package/team-worklog.js +2 -149
- package/team.js +1 -302
- package/text-search.js +1 -155
- package/theme.js +17 -154
- package/titles.js +4 -285
- package/tool-discovery.js +1 -87
- package/tool-dispatch.js +5 -230
- package/tool-groups.js +1 -81
- package/tool-hints.js +10 -185
- package/tool-loop-guard.js +4 -182
- package/tool-need.js +2 -124
- package/tool-result.js +2 -438
- package/tool-round.js +0 -0
- package/tool-schema.js +1 -155
- package/tool-traits.js +1 -182
- package/toolset.js +2 -88
- package/trajectory.js +1 -530
- package/turn-loop.js +4 -440
- package/upcast.js +1 -37
- package/vault.js +2 -250
- package/view.js +1 -88
- package/voice-intents.js +2 -1488
- package/voice-speaker.js +1 -98
- package/weather-tool.js +1 -65
- package/weather.js +2 -211
- package/web-search-tool.js +14 -102
- package/web-search.js +1 -160
- package/widget.js +1 -153
package/project.js
CHANGED
|
@@ -1,170 +1 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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};
|