PRD 1.20 REQ-TASK-1, REQ-PLUG-1, REQ-WS-8. Each organization keeps its tasks in its own bus through the new task-tracker plug-in kind. The default module is the built-in store; Vikunja is the optional module and a Vikunja organization keeps today's path. - bus schema 3u to 3v (migration 3u-3v.sql): store_tasks, append-only, one row per write, each citing its events row; seven guard triggers. - broker: the eight role verbs on the store with the slice 1 field table checked twice (module, then a before/after field diff); the person's tasks.create/edit/archive/unarchive/delete as human.input events; the tasks read returns store rows in board order (group, position), filtered and paged like tracker rows. - runtime: each business gets its module at start (organization file plugin, else vikunja with a tracker entry, else store); a store business with a Vikunja tracker refuses the boot (tracker-config). - plugins: task-tracker kind with store and vikunja modules. - cli: the launcher's tracker flag covers store organizations; trail prints store writes. - pins: schema.test counts, the bus migration tests, stack-release #811 and T46 (store_tasks on the always list; COUNTED unchanged), budget-supervisor accepts 3v. Rebased onto SYS1 by the lead: SYS1 took 3t-3u, so this step is 3u-3v.
102 lines
4.5 KiB
JavaScript
102 lines
4.5 KiB
JavaScript
// Plain-text views of broker results. Every function takes what the broker
|
|
// returned and keeps its order: the broker sorts, this file never re-sorts.
|
|
|
|
const one = (s, max = 160) => {
|
|
const flat = String(s ?? "").replace(/\s+/g, " ").trim();
|
|
return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
|
|
};
|
|
|
|
export const shortId = (id) => String(id).slice(0, 8);
|
|
|
|
export function optionsLine(d) {
|
|
return d.options.map((o) => `${o.key} = ${one(o.text, 80)}`).join("; ");
|
|
}
|
|
|
|
// What approving means, when the decision carries authorization context.
|
|
export function authorizationLines(d) {
|
|
const a = d.authorization;
|
|
if (!a) return [];
|
|
const lines = [`action: ${a.action}${a.target ? ` on ${a.target}` : ""}`];
|
|
lines.push(`choosing "${a.approvalChoice}" authorizes it; any other choice declines`);
|
|
return lines;
|
|
}
|
|
|
|
export function formatInbox(list) {
|
|
if (list.length === 0) return "inbox: empty\n";
|
|
const out = [`inbox: ${list.length} open decision(s)`];
|
|
for (const d of list) {
|
|
const flags = [d.class, d.blocking ? "blocking" : null].filter(Boolean).join(", ");
|
|
out.push("", `${shortId(d.id)} ${d.action} (${flags}) raised ${d.at} by ${d.raised_by_role}`);
|
|
out.push(` ${one(d.question, 400)}`);
|
|
out.push(` options: ${optionsLine(d)} (recommended: ${d.recommendation})`);
|
|
for (const l of authorizationLines(d)) out.push(` ${l}`);
|
|
if (d.task_ref) out.push(` task: ${d.task_ref}`);
|
|
out.push(` decide: mosaic decide ${shortId(d.id)} <option>`);
|
|
}
|
|
return `${out.join("\n")}\n`;
|
|
}
|
|
|
|
export function formatDecision(d) {
|
|
const out = [
|
|
`decision ${d.id}`,
|
|
` ${d.class}${d.blocking ? ", blocking" : ""}, raised ${d.at} by ${d.raised_by_role} (${d.raised_by_run})`,
|
|
` question: ${d.question}`,
|
|
` options: ${optionsLine(d)}`,
|
|
` recommended: ${d.recommendation}`,
|
|
];
|
|
for (const l of authorizationLines(d)) out.push(` ${l}`);
|
|
if (d.task_ref) out.push(` task: ${d.task_ref}`);
|
|
return `${out.join("\n")}\n`;
|
|
}
|
|
|
|
// Row 277 (#1782): the held thread claims follow the role lines, each marked `thread`.
|
|
export function formatAgents(list, threads = []) {
|
|
const roles = list.length === 0 ? "agents: no role is claimed\n" : `${list.map((c) => `${c.role} ${c.holder_run} ${c.harness ?? "-"} since ${c.at}`).join("\n")}\n`;
|
|
return roles + threads.map((c) => `thread ${c.thread} ${c.instance} ${c.holder_run} ${c.harness} since ${c.at}\n`).join("");
|
|
}
|
|
|
|
export function formatTasks(list) {
|
|
if (list.length === 0) return "tasks: none\n";
|
|
return `${list
|
|
.map((t) => {
|
|
const f = t.fields ?? {};
|
|
const title = f.title ?? f.name ?? "";
|
|
const state = f.status ?? f.state ?? (f.done === true ? "done" : f.done === false ? "open" : "");
|
|
return [t.task_ref, state, one(title, 100)].filter(Boolean).join(" ");
|
|
})
|
|
.join("\n")}\n`;
|
|
}
|
|
|
|
export function trailSummary(r) {
|
|
switch (r.table) {
|
|
case "events":
|
|
return [r.kind, r.actor_role ?? null, r.body?.operation ?? null, r.body?.decision ? `decision ${shortId(r.body.decision)}` : null].filter(Boolean).join(" ");
|
|
case "decisions":
|
|
return `${r.class} ${r.action} → ${r.route_to}${r.blocking ? " (blocking)" : ""}: ${one(r.question, 120)}`;
|
|
case "decision_events":
|
|
return [r.op, r.choice ? `choice ${r.choice}` : null, `by ${r.by}`, r.via ? `via ${r.via}` : null].filter(Boolean).join(" ");
|
|
case "messages":
|
|
return `${r.from_role} → ${r.to_role}: ${one(r.body, 120)}`;
|
|
case "deliveries":
|
|
return [r.op, r.transport, r.holder_run].filter(Boolean).join(" ");
|
|
case "role_claims":
|
|
return `${r.op} ${r.role} by ${r.holder_run}`;
|
|
case "task_snapshots":
|
|
return `snapshot ${r.task_ref ?? ""}`.trim();
|
|
// U4a (#1873): a built-in store task's write: what it did, under which verb, by which role or person.
|
|
case "store_tasks":
|
|
return [r.op, r.verb, `r${r.revision}`, `by ${r.by_role ?? r.by_human}`, r.fields?.title ? one(r.fields.title, 100) : null].filter(Boolean).join(" ");
|
|
default:
|
|
return "";
|
|
}
|
|
}
|
|
|
|
// Rows print in the order the broker returned them (at, table, seq).
|
|
export function formatTrail(subject, rows) {
|
|
const out = [`trail ${subject}: ${rows.length} row(s)`];
|
|
for (const r of rows) out.push(`${r.at} ${r.table}#${r.seq} ${trailSummary(r)}`.trimEnd());
|
|
const decision = rows.find((r) => r.table === "decisions" && r.id === subject);
|
|
if (decision?.task_ref) out.push("", `task: ${decision.task_ref}`, `follow with: mosaic trail ${decision.task_ref}`);
|
|
return `${out.join("\n")}\n`;
|
|
}
|