@agimon-ai/doompi-task 0.0.1-alpha.0 → 0.0.1-alpha.2
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/dist/delegation/manager.cjs +2 -1
- package/dist/delegation/manager.cjs.map +1 -1
- package/dist/delegation/manager.d.cts.map +1 -1
- package/dist/delegation/manager.d.mts.map +1 -1
- package/dist/delegation/manager.mjs +2 -1
- package/dist/delegation/manager.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +1 -1
- package/dist/store/reducer.cjs +1 -1
- package/dist/store/reducer.cjs.map +1 -1
- package/dist/store/reducer.mjs +1 -1
- package/dist/store/reducer.mjs.map +1 -1
- package/dist/tool/promptGuidelines.cjs +1 -1
- package/dist/tool/promptGuidelines.cjs.map +1 -1
- package/dist/tool/promptGuidelines.mjs +1 -1
- package/dist/tool/promptGuidelines.mjs.map +1 -1
- package/dist/tui/selectors.cjs +1 -1
- package/dist/tui/selectors.cjs.map +1 -1
- package/dist/tui/selectors.d.cts +16 -8
- package/dist/tui/selectors.d.cts.map +1 -1
- package/dist/tui/selectors.d.mts +16 -8
- package/dist/tui/selectors.d.mts.map +1 -1
- package/dist/tui/selectors.mjs +1 -1
- package/dist/tui/selectors.mjs.map +1 -1
- package/dist/tui/taskOverlay.cjs +1 -1
- package/dist/tui/taskOverlay.cjs.map +1 -1
- package/dist/tui/taskOverlay.d.cts.map +1 -1
- package/dist/tui/taskOverlay.d.mts.map +1 -1
- package/dist/tui/taskOverlay.mjs +1 -1
- package/dist/tui/taskOverlay.mjs.map +1 -1
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{COLLAPSE_KEY_ENV as e,COLLAPSE_KEY_OFF as t,DEFAULT_COLLAPSE_KEY as n,DEFAULT_DELEGATION_TIMEOUT_MS as r,DEFAULT_MAX_WIDGET_LINES as i,DEFAULT_STORE_TTL_MS as a,DELEGATION_TIMEOUT_MS_ENV as o,MAX_WIDGET_LINES_ENV as s,STORE_TTL_MS_ENV as c,getDelegationTimeoutMs as l,getMaxWidgetLines as u,getStoreTtlMs as d,parsePiTaskConfig as f,resolveCollapseKey as p}from"./config.mjs";import{DELETED_STATUS as m,STORE_SCHEMA_VERSION as h,emptyDocument as g,isDelegationActive as _}from"./store/types.mjs";import{ERR_ORPHANED_BY_RESTART as v,ERR_ORPHANED_BY_SESSION as y,reconcileOrphanedDelegations as b}from"./store/reconcile.mjs";import{deriveBlocks as x,detectCycle as S,isBlocked as C,isTaskListComplete as w,unresolvedBlockers as T}from"./store/taskGraph.mjs";import{BACKGROUND_WORK_PROVIDER as E,DelegationManager as D,ERR_CANCEL_UNACKNOWLEDGED as O,ERR_CHILD_SESSION as k,ERR_NO_RUNTIME as A,MAX_BRIEF_FILES as j,NOTIFY_CUSTOM_TYPE as M}from"./delegation/manager.mjs";import{COMMAND_NAME as N,ERR_REQUIRES_INTERACTIVE as P,MSG_NO_TASKS as F,MSG_UPSERT_FIELD_MISPLACED as I,TASK_ACTIONS as L,TASK_ACTION_FIELDS as R,TASK_CONTEXTS as z,TASK_STATUSES as B,TOOL_LABEL as V,TOOL_NAME as H,TaskItemSchema as U,TaskParamsSchema as W,taskActionAcceptsField as G}from"./tool/schema.mjs";import{MAX_UPSERT_ITEMS as K,REF_PATTERN as q,applyTaskMutation as J,formatUpsertFailure as Y,isCommittingOp as X,singleItemOutcome as Z}from"./store/reducer.mjs";import{countTasks as Q,
|
|
1
|
+
import{COLLAPSE_KEY_ENV as e,COLLAPSE_KEY_OFF as t,DEFAULT_COLLAPSE_KEY as n,DEFAULT_DELEGATION_TIMEOUT_MS as r,DEFAULT_MAX_WIDGET_LINES as i,DEFAULT_STORE_TTL_MS as a,DELEGATION_TIMEOUT_MS_ENV as o,MAX_WIDGET_LINES_ENV as s,STORE_TTL_MS_ENV as c,getDelegationTimeoutMs as l,getMaxWidgetLines as u,getStoreTtlMs as d,parsePiTaskConfig as f,resolveCollapseKey as p}from"./config.mjs";import{DELETED_STATUS as m,STORE_SCHEMA_VERSION as h,emptyDocument as g,isDelegationActive as _}from"./store/types.mjs";import{ERR_ORPHANED_BY_RESTART as v,ERR_ORPHANED_BY_SESSION as y,reconcileOrphanedDelegations as b}from"./store/reconcile.mjs";import{deriveBlocks as x,detectCycle as S,isBlocked as C,isTaskListComplete as w,unresolvedBlockers as T}from"./store/taskGraph.mjs";import{BACKGROUND_WORK_PROVIDER as E,DelegationManager as D,ERR_CANCEL_UNACKNOWLEDGED as O,ERR_CHILD_SESSION as k,ERR_NO_RUNTIME as A,MAX_BRIEF_FILES as j,NOTIFY_CUSTOM_TYPE as M}from"./delegation/manager.mjs";import{COMMAND_NAME as N,ERR_REQUIRES_INTERACTIVE as P,MSG_NO_TASKS as F,MSG_UPSERT_FIELD_MISPLACED as I,TASK_ACTIONS as L,TASK_ACTION_FIELDS as R,TASK_CONTEXTS as z,TASK_STATUSES as B,TOOL_LABEL as V,TOOL_NAME as H,TaskItemSchema as U,TaskParamsSchema as W,taskActionAcceptsField as G}from"./tool/schema.mjs";import{MAX_UPSERT_ITEMS as K,REF_PATTERN as q,applyTaskMutation as J,formatUpsertFailure as Y,isCommittingOp as X,singleItemOutcome as Z}from"./store/reducer.mjs";import{countTasks as Q,deriveTaskProjection as $,groupTasks as ee,hasActiveWork as te,selectOverlayLayout as ne,selectOverlayLayoutFromProjection as re,shouldShowIds as ie,visibleTasks as ae}from"./tui/selectors.mjs";import{STORE_PATH_ENV as oe,hasStorePathOverride as se,lockPathFor as ce,removeLegacyStoreDirectory as le,resolveLegacyStoreDirectory as ue,resolveSessionKey as de,resolveStorePath as fe,sweepStoreFiles as pe,tempPathFor as me}from"./store/paths.mjs";import{TaskStore as he}from"./store/taskStore.mjs";import{MSG_ALL_COMPLETE_CLEAR as ge,MSG_UPSERT_NONE_APPLIED as _e,MSG_UPSERT_PARTIAL as ve,buildTextResult as ye,buildToolResult as be,formatContent as xe,formatUpsertFailureText as Se}from"./tool/responseEnvelope.mjs";import{DEFAULT_PROMPT_GUIDELINES as Ce}from"./tool/promptGuidelines.mjs";import{DEFAULT_PROMPT_SNIPPET as we,registerTaskTool as Te}from"./tool/taskTool.mjs";import Ee from"./extension.mjs";export{E as BACKGROUND_WORK_PROVIDER,e as COLLAPSE_KEY_ENV,t as COLLAPSE_KEY_OFF,N as COMMAND_NAME,n as DEFAULT_COLLAPSE_KEY,r as DEFAULT_DELEGATION_TIMEOUT_MS,i as DEFAULT_MAX_WIDGET_LINES,Ce as DEFAULT_PROMPT_GUIDELINES,we as DEFAULT_PROMPT_SNIPPET,a as DEFAULT_STORE_TTL_MS,o as DELEGATION_TIMEOUT_MS_ENV,m as DELETED_STATUS,D as DelegationManager,O as ERR_CANCEL_UNACKNOWLEDGED,k as ERR_CHILD_SESSION,A as ERR_NO_RUNTIME,v as ERR_ORPHANED_BY_RESTART,y as ERR_ORPHANED_BY_SESSION,P as ERR_REQUIRES_INTERACTIVE,j as MAX_BRIEF_FILES,K as MAX_UPSERT_ITEMS,s as MAX_WIDGET_LINES_ENV,ge as MSG_ALL_COMPLETE_CLEAR,F as MSG_NO_TASKS,I as MSG_UPSERT_FIELD_MISPLACED,_e as MSG_UPSERT_NONE_APPLIED,ve as MSG_UPSERT_PARTIAL,M as NOTIFY_CUSTOM_TYPE,q as REF_PATTERN,oe as STORE_PATH_ENV,h as STORE_SCHEMA_VERSION,c as STORE_TTL_MS_ENV,L as TASK_ACTIONS,R as TASK_ACTION_FIELDS,z as TASK_CONTEXTS,B as TASK_STATUSES,V as TOOL_LABEL,H as TOOL_NAME,U as TaskItemSchema,W as TaskParamsSchema,he as TaskStore,J as applyTaskMutation,ye as buildTextResult,be as buildToolResult,Q as countTasks,x as deriveBlocks,$ as deriveTaskProjection,S as detectCycle,g as emptyDocument,xe as formatContent,Y as formatUpsertFailure,Se as formatUpsertFailureText,l as getDelegationTimeoutMs,u as getMaxWidgetLines,d as getStoreTtlMs,ee as groupTasks,te as hasActiveWork,se as hasStorePathOverride,C as isBlocked,X as isCommittingOp,_ as isDelegationActive,w as isTaskListComplete,ce as lockPathFor,f as parsePiTaskConfig,b as reconcileOrphanedDelegations,Te as registerTaskTool,le as removeLegacyStoreDirectory,p as resolveCollapseKey,ue as resolveLegacyStoreDirectory,de as resolveSessionKey,fe as resolveStorePath,ne as selectOverlayLayout,re as selectOverlayLayoutFromProjection,ie as shouldShowIds,Z as singleItemOutcome,pe as sweepStoreFiles,G as taskActionAcceptsField,Ee as taskExtension,me as tempPathFor,T as unresolvedBlockers,ae as visibleTasks};
|
package/dist/store/reducer.cjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./types.cjs"),t=require("./taskGraph.cjs"),n=require("./invariants.cjs"),r=/^[A-Za-z][A-Za-z0-9_-]{0,31}$/;function i(e){switch(e.kind){case`error`:case`list`:case`get`:return!1;case`upsert`:return e.
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./types.cjs"),t=require("./taskGraph.cjs"),n=require("./invariants.cjs"),r=/^[A-Za-z][A-Za-z0-9_-]{0,31}$/;function i(e){switch(e.kind){case`error`:case`list`:case`get`:return!1;case`upsert`:return e.items.some(e=>e.kind===`created`||e.kind===`updated`);case`delete`:case`clear`:return!0}}function a(e){return e.items.filter(e=>e.kind===`failed`).map(e=>`item[${e.index}]: ${e.message}`).join(`
|
|
2
2
|
`)}function o(e){return e.kind===`upsert`?e.items[0]:void 0}function s(e,t){return{document:e,op:{kind:`error`,message:t}}}function c(e,t){let n=e??[],r=t??[];return n.length===r.length&&n.every((e,t)=>e===r[t])}function l(e,t){return JSON.stringify(e??null)===JSON.stringify(t??null)}function u(e,t){return e.subject!==t.subject||e.status!==t.status||e.description!==t.description||e.activeForm!==t.activeForm||e.owner!==t.owner||!c(e.blockedBy,t.blockedBy)||!l(e.metadata,t.metadata)}function d(e,t,n=e.nextId){return{...e,tasks:t,nextId:n}}function f(e,t){let n={...e};for(let[e,r]of Object.entries(t))r===null?delete n[e]:n[e]=r;return Object.keys(n).length?n:void 0}function p(e,t){if(t.subject!==void 0&&(e.subject=t.subject),t.description!==void 0&&(e.description=t.description),t.activeForm!==void 0&&(e.activeForm=t.activeForm),t.owner!==void 0&&(e.owner=t.owner),t.metadata!==void 0){let n=f(e.metadata,t.metadata);n===void 0?delete e.metadata:e.metadata=n}}function m(e,t,n){if(typeof n==`number`)return{ok:!0,id:n};if(!r.test(n))return{ok:!1,message:`${t}: "${n}" is not a valid ref; pass an existing task id as a number`};let i=e.refIds.get(n);if(i!==void 0)return{ok:!0,id:i};let a=e.failedRefs.get(n);return a===void 0?{ok:!1,message:`${t}: unknown ref "${n}" — a ref must be declared by an earlier entry in the same call`}:{ok:!1,message:`${t}: ref "${n}" refers to item[${a}], which failed`}}function h(e){return{ok:!1,message:e}}function g(e,t){if(t.ref!==void 0){if(t.id!==void 0)return`ref is only valid when creating a task; drop the id, or drop the ref`;if(!r.test(t.ref))return`ref "${t.ref}" is invalid: a ref must start with a letter and contain only letters, digits, - or _`;if(e.refIds.get(t.ref)!==void 0||e.failedRefs.has(t.ref))return`duplicate ref "${t.ref}": it was already declared by an earlier entry`}}function _(e,t,n,r){if(t.subject===void 0)return h(`subject is required when the entry has no id`);if(!t.subject.trim())return h(`subject must not be blank`);if(t.addBlockedBy!==void 0||t.removeBlockedBy!==void 0)return h(`addBlockedBy / removeBlockedBy are only for an entry with an id; a new task uses blockedBy`);if(t.status===`deleted`)return h(`cannot create a task with status deleted; create it, then use action delete`);let i=[];for(let n of t.blockedBy??[]){let t=m(e,`blockedBy`,n);if(!t.ok)return h(t.message);let r=e.tasks.find(e=>e.id===t.id);if(!r)return h(`blockedBy: #${t.id} not found`);if(r.status===`deleted`)return h(`blockedBy: #${t.id} is deleted`);i.includes(t.id)||i.push(t.id)}let a={id:e.nextId,subject:t.subject,status:t.status??`pending`,createdAt:r,updatedAt:r};return p(a,t),i.length&&(a.blockedBy=i),e.nextId+=1,e.tasks.push(a),t.ref&&e.refIds.set(t.ref,a.id),{ok:!0,outcome:{index:n,kind:`created`,id:a.id,subject:a.subject,status:a.status,...t.ref?{ref:t.ref}:{},...i.length?{blockedBy:i}:{}}}}function v(e,r,i,a){let o=r.id,s=e.tasks.findIndex(e=>e.id===o);if(s===-1)return h(`#${o} not found`);if(r.blockedBy!==void 0)return h(`blockedBy is only for a new entry; use addBlockedBy / removeBlockedBy on an entry that has an id`);if(!(r.subject!==void 0||r.description!==void 0||r.activeForm!==void 0||r.status!==void 0||r.owner!==void 0||r.metadata!==void 0||r.addBlockedBy?.length||r.removeBlockedBy?.length))return h(`nothing to change: provide at least one of subject, description, activeForm, status, owner, metadata, addBlockedBy, or removeBlockedBy`);let c=e.tasks[s];if(r.subject!==void 0&&!r.subject.trim())return h(`subject must not be blank`);let l=c.status;if(r.status!==void 0){if(!n.isTransitionValid(c.status,r.status))return h(`illegal transition ${c.status} -> ${r.status}`);l=r.status}let d=c.blockedBy?[...c.blockedBy]:[];for(let t of r.removeBlockedBy??[]){let n=m(e,`removeBlockedBy`,t);if(!n.ok)return h(n.message);d=d.filter(e=>e!==n.id)}for(let t of r.addBlockedBy??[]){let n=m(e,`addBlockedBy`,t);if(!n.ok)return h(n.message);if(n.id===o)return h(`cannot block #${o} on itself`);let r=e.tasks.find(e=>e.id===n.id);if(!r)return h(`addBlockedBy: #${n.id} not found`);if(r.status===`deleted`)return h(`addBlockedBy: #${n.id} is deleted`);d.includes(n.id)||d.push(n.id)}let f={...c,status:l};p(f,r),d.length?f.blockedBy=d:delete f.blockedBy;let g=[...e.tasks];if(g[s]=f,r.addBlockedBy?.length&&t.detectCycle(g,o,d))return h(`addBlockedBy would create a cycle in the blockedBy graph`);let _=u(c,f);return _&&(f.updatedAt=a),e.tasks=g,{ok:!0,outcome:_?{index:i,kind:`updated`,id:o,fromStatus:c.status,toStatus:l}:{index:i,kind:`unchanged`,id:o,status:l}}}function y(e,t,n){let i={tasks:[...e.tasks],nextId:e.nextId,refIds:new Map,failedRefs:new Map},a=[],o=0;for(let[e,s]of t.entries()){let t=g(i,s),c=t?h(t):s.id===void 0?_(i,s,e,n):v(i,s,e,n);if(c.ok){a.push(c.outcome),o+=1;continue}s.ref&&r.test(s.ref)&&!i.refIds.has(s.ref)&&i.failedRefs.set(s.ref,e),a.push({index:e,kind:`failed`,message:c.message,...s.id===void 0?{}:{id:s.id},...s.ref?{ref:s.ref}:{}})}return{document:o>0?d(e,i.tasks,i.nextId):e,op:{kind:`upsert`,items:a,applied:o,failed:a.length-o}}}function b(t,n,r,i=new Date().toISOString()){switch(n){case`upsert`:{let e=r.tasks;return!Array.isArray(e)||e.length===0?s(t,`upsert requires a non-empty tasks array, e.g. {"action":"upsert","tasks":[{"subject":"Write tests"}]}`):e.length>100?s(t,`upsert accepts at most 100 entries per call (received ${e.length})`):y(t,e,i)}case`list`:return{document:t,op:{kind:`list`,includeDeleted:r.includeDeleted===!0,...r.status===void 0?{}:{statusFilter:r.status}}};case`get`:{if(r.id===void 0)return s(t,`id required for get`);let e=t.tasks.find(e=>e.id===r.id);return e?{document:t,op:{kind:`get`,task:e}}:s(t,`#${r.id} not found`)}case`delete`:{if(r.id===void 0)return s(t,`id required for delete`);let n=t.tasks.findIndex(e=>e.id===r.id);if(n===-1)return s(t,`#${r.id} not found`);let a=t.tasks[n];if(a.status===`deleted`)return s(t,`#${a.id} is already deleted`);if(e.isDelegationActive(a))return s(t,`#${a.id} has a running delegation — cancel it before deleting`);let o=[...t.tasks];return o[n]={...a,status:e.DELETED_STATUS,updatedAt:i},{document:d(t,o),op:{kind:`delete`,id:a.id,subject:a.subject}}}case`clear`:{let n=t.tasks.filter(e.isDelegationActive);if(n.length>0)return s(t,`cannot clear while delegations are running (${n.map(e=>`#${e.id}`).join(`, `)}) — cancel them first`);let r=t.tasks.length;return{document:d(t,[],1),op:{kind:`clear`,count:r}}}}}exports.MAX_UPSERT_ITEMS=100,exports.REF_PATTERN=r,exports.applyTaskMutation=b,exports.formatUpsertFailure=a,exports.isCommittingOp=i,exports.singleItemOutcome=o;
|
|
3
3
|
//# sourceMappingURL=reducer.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reducer.cjs","names":["isTransitionValid","detectCycle","isDelegationActive","DELETED_STATUS"],"sources":["../../src/store/reducer.ts"],"sourcesContent":["import { isTransitionValid } from './invariants.ts';\nimport { detectCycle } from './taskGraph.ts';\nimport {\n DELETED_STATUS,\n type DepToken,\n isDelegationActive,\n type Task,\n type TaskAction,\n type TaskDocument,\n type TaskItemMutation,\n type TaskMutationParams,\n type TaskStatus,\n type UpsertItemOutcome,\n} from './types.ts';\n\n/** Bounds the work one call can do, and with it the size of one committed write. */\nexport const MAX_UPSERT_ITEMS = 100;\n\n/**\n * A ref must start with a letter, which makes it structurally unconfusable with\n * a stringified id. That is why resolving a dependency token is just a `typeof`\n * check with no coercion heuristics.\n */\nexport const REF_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,31}$/;\n\n/**\n * Reducer outcome. Closed tagged union: adding an action requires extending\n * this union and the response formatter, which the compiler enforces.\n *\n * `upsert` carries one outcome per request item. It is returned even when every\n * item failed — `error` is reserved for whole-call faults — so the tool layer\n * still has the per-item messages to report.\n */\nexport type Op =\n | { kind: 'upsert'; items: UpsertItemOutcome[]; applied: number; failed: number }\n | { kind: 'delete'; id: number; subject: string }\n | { kind: 'list'; statusFilter?: TaskStatus; includeDeleted: boolean }\n | { kind: 'get'; task: Task }\n | { kind: 'clear'; count: number }\n | { kind: 'error'; message: string };\n\nexport interface ApplyResult {\n document: TaskDocument;\n op: Op;\n}\n\n/** Actions the reducer owns. `assign`/`cancel` are handled by the delegation manager. */\nexport type ReducerAction = Exclude<TaskAction, 'assign' | 'cancel'>;\n\n/**\n * Does this op represent state that must reach disk?\n *\n * The exhaustive switch is the point: a future `Op` variant cannot be added\n * without declaring its persistence intent, so nothing silently starts or stops\n * bumping `rev`.\n */\nexport function isCommittingOp(op: Op): boolean {\n switch (op.kind) {\n case 'error':\n case 'list':\n case 'get':\n return false;\n case 'upsert':\n return op.applied > 0;\n case 'delete':\n case 'clear':\n return true;\n }\n}\n\n/** Joined per-item failures, for the thrown error when an upsert applied nothing. */\nexport function formatUpsertFailure(op: Extract<Op, { kind: 'upsert' }>): string {\n return op.items\n .filter((item) => item.kind === 'failed')\n .map((item) => `item[${item.index}]: ${item.message}`)\n .join('\\n');\n}\n\n/** The single item's outcome, for callers that only ever send a batch of one. */\nexport function singleItemOutcome(op: Op): UpsertItemOutcome | undefined {\n return op.kind === 'upsert' ? op.items[0] : undefined;\n}\n\nfunction errorResult(document: TaskDocument, message: string): ApplyResult {\n return { document, op: { kind: 'error', message } };\n}\n\nfunction sameNumberList(a: number[] | undefined, b: number[] | undefined): boolean {\n const x = a ?? [];\n const y = b ?? [];\n return x.length === y.length && x.every((value, index) => value === y[index]);\n}\n\nfunction sameRecord(a: Record<string, unknown> | undefined, b: Record<string, unknown> | undefined): boolean {\n return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);\n}\n\n/**\n * Did this update change anything? A no-effect update (status re-set to its\n * current value, fields re-sent unchanged) reports \"No change\" rather than\n * \"Updated #N\" — without the distinction a model can loop re-issuing the same\n * call believing it never landed.\n */\nfunction taskChanged(before: Task, after: Task): boolean {\n return (\n before.subject !== after.subject ||\n before.status !== after.status ||\n before.description !== after.description ||\n before.activeForm !== after.activeForm ||\n before.owner !== after.owner ||\n !sameNumberList(before.blockedBy, after.blockedBy) ||\n !sameRecord(before.metadata, after.metadata)\n );\n}\n\nfunction withTasks(document: TaskDocument, tasks: Task[], nextId = document.nextId): TaskDocument {\n return { ...document, tasks, nextId };\n}\n\n/** Merge incoming metadata over a base; a `null` value deletes its key. */\nfunction mergeMetadata(\n base: Record<string, unknown> | undefined,\n incoming: Record<string, unknown>,\n): Record<string, unknown> | undefined {\n const merged: Record<string, unknown> = { ...base };\n for (const [key, value] of Object.entries(incoming)) {\n if (value === null) delete merged[key];\n else merged[key] = value;\n }\n return Object.keys(merged).length ? merged : undefined;\n}\n\n/** Copy the optional scalar fields an item may set onto a task, in place. */\nfunction assignScalars(target: Task, item: TaskItemMutation): void {\n if (item.subject !== undefined) target.subject = item.subject;\n if (item.description !== undefined) target.description = item.description;\n if (item.activeForm !== undefined) target.activeForm = item.activeForm;\n if (item.owner !== undefined) target.owner = item.owner;\n if (item.metadata !== undefined) {\n const merged = mergeMetadata(target.metadata, item.metadata);\n if (merged === undefined) delete target.metadata;\n else target.metadata = merged;\n }\n}\n\n/**\n * Working state threaded item to item, so entry N observes every effect of\n * entries 1..N-1. `refIds` and `failedRefs` are what make the cascade work:\n * refs resolve backward only, so an entry that names a failed sibling is always\n * processed after the failure that killed it.\n */\ninterface UpsertContext {\n tasks: Task[];\n nextId: number;\n refIds: Map<string, number>;\n failedRefs: Map<string, number>;\n}\n\ntype Resolved = { ok: true; id: number } | { ok: false; message: string };\n\nfunction resolveDep(context: UpsertContext, field: string, token: DepToken): Resolved {\n if (typeof token === 'number') return { ok: true, id: token };\n if (!REF_PATTERN.test(token)) {\n return { ok: false, message: `${field}: \"${token}\" is not a valid ref; pass an existing task id as a number` };\n }\n const id = context.refIds.get(token);\n if (id !== undefined) return { ok: true, id };\n const failedAt = context.failedRefs.get(token);\n if (failedAt !== undefined) {\n return { ok: false, message: `${field}: ref \"${token}\" refers to item[${failedAt}], which failed` };\n }\n return {\n ok: false,\n message: `${field}: unknown ref \"${token}\" — a ref must be declared by an earlier entry in the same call`,\n };\n}\n\ntype ItemResult = { ok: true; outcome: UpsertItemOutcome } | { ok: false; message: string };\n\nfunction fail(message: string): ItemResult {\n return { ok: false, message };\n}\n\n/** Structural checks that apply to every entry, whichever kind it is. */\nfunction checkShape(context: UpsertContext, item: TaskItemMutation): string | undefined {\n if (item.ref !== undefined) {\n if (item.id !== undefined) return 'ref is only valid when creating a task; drop the id, or drop the ref';\n if (!REF_PATTERN.test(item.ref)) {\n return `ref \"${item.ref}\" is invalid: a ref must start with a letter and contain only letters, digits, - or _`;\n }\n const owner = context.refIds.get(item.ref) !== undefined || context.failedRefs.has(item.ref);\n if (owner) return `duplicate ref \"${item.ref}\": it was already declared by an earlier entry`;\n }\n return undefined;\n}\n\nfunction applyCreateItem(context: UpsertContext, item: TaskItemMutation, index: number, now: string): ItemResult {\n if (item.subject === undefined) return fail('subject is required when the entry has no id');\n if (!item.subject.trim()) return fail('subject must not be blank');\n if (item.addBlockedBy !== undefined || item.removeBlockedBy !== undefined) {\n return fail('addBlockedBy / removeBlockedBy are only for an entry with an id; a new task uses blockedBy');\n }\n // A task created as a tombstone is invisible to every view and can never\n // leave `deleted`, so it would be a write-only black hole.\n if (item.status === DELETED_STATUS) {\n return fail('cannot create a task with status deleted; create it, then use action delete');\n }\n\n const blockedBy: number[] = [];\n for (const token of item.blockedBy ?? []) {\n const resolved = resolveDep(context, 'blockedBy', token);\n if (!resolved.ok) return fail(resolved.message);\n const dep = context.tasks.find((task) => task.id === resolved.id);\n if (!dep) return fail(`blockedBy: #${resolved.id} not found`);\n if (dep.status === DELETED_STATUS) return fail(`blockedBy: #${resolved.id} is deleted`);\n if (!blockedBy.includes(resolved.id)) blockedBy.push(resolved.id);\n }\n\n // No cycle check is needed: the id is freshly allocated, so nothing already\n // in the document can point at it yet.\n const created: Task = {\n id: context.nextId,\n subject: item.subject,\n status: item.status ?? 'pending',\n createdAt: now,\n updatedAt: now,\n };\n assignScalars(created, item);\n if (blockedBy.length) created.blockedBy = blockedBy;\n\n context.nextId += 1;\n context.tasks.push(created);\n if (item.ref) context.refIds.set(item.ref, created.id);\n\n return {\n ok: true,\n outcome: {\n index,\n kind: 'created',\n id: created.id,\n subject: created.subject,\n status: created.status,\n ...(item.ref ? { ref: item.ref } : {}),\n ...(blockedBy.length ? { blockedBy } : {}),\n },\n };\n}\n\nfunction applyUpdateItem(context: UpsertContext, item: TaskItemMutation, index: number, now: string): ItemResult {\n const id = item.id as number;\n const at = context.tasks.findIndex((task) => task.id === id);\n if (at === -1) return fail(`#${id} not found`);\n if (item.blockedBy !== undefined) {\n return fail('blockedBy is only for a new entry; use addBlockedBy / removeBlockedBy on an entry that has an id');\n }\n\n const hasMutation =\n item.subject !== undefined ||\n item.description !== undefined ||\n item.activeForm !== undefined ||\n item.status !== undefined ||\n item.owner !== undefined ||\n item.metadata !== undefined ||\n Boolean(item.addBlockedBy?.length) ||\n Boolean(item.removeBlockedBy?.length);\n if (!hasMutation) {\n return fail(\n 'nothing to change: provide at least one of subject, description, activeForm, status, owner, metadata, addBlockedBy, or removeBlockedBy',\n );\n }\n\n const current = context.tasks[at];\n\n // The Task Space overlay commits free text from an inline editor, so a blank\n // subject reaches the reducer as a real update rather than as a missing\n // field. Persisting it would leave an unidentifiable row.\n if (item.subject !== undefined && !item.subject.trim()) return fail('subject must not be blank');\n\n let newStatus = current.status;\n if (item.status !== undefined) {\n if (!isTransitionValid(current.status, item.status)) {\n return fail(`illegal transition ${current.status} -> ${item.status}`);\n }\n newStatus = item.status;\n }\n\n let newBlockedBy = current.blockedBy ? [...current.blockedBy] : [];\n for (const token of item.removeBlockedBy ?? []) {\n const resolved = resolveDep(context, 'removeBlockedBy', token);\n if (!resolved.ok) return fail(resolved.message);\n // Removing an id that is not there is a well-defined no-op, so unlike the\n // add path this deliberately skips the existence check.\n newBlockedBy = newBlockedBy.filter((dep) => dep !== resolved.id);\n }\n for (const token of item.addBlockedBy ?? []) {\n const resolved = resolveDep(context, 'addBlockedBy', token);\n if (!resolved.ok) return fail(resolved.message);\n if (resolved.id === id) return fail(`cannot block #${id} on itself`);\n const dep = context.tasks.find((task) => task.id === resolved.id);\n if (!dep) return fail(`addBlockedBy: #${resolved.id} not found`);\n if (dep.status === DELETED_STATUS) return fail(`addBlockedBy: #${resolved.id} is deleted`);\n if (!newBlockedBy.includes(resolved.id)) newBlockedBy.push(resolved.id);\n }\n\n const updated: Task = { ...current, status: newStatus };\n assignScalars(updated, item);\n if (newBlockedBy.length) updated.blockedBy = newBlockedBy;\n else delete updated.blockedBy;\n\n // The candidate is written before the check because `detectCycle` unions the\n // node's stored edges with the ones passed in: checking against the old array\n // would resurrect an edge `removeBlockedBy` just stripped and reject a\n // remove+add that legitimately breaks a cycle.\n const candidate = [...context.tasks];\n candidate[at] = updated;\n if (item.addBlockedBy?.length && detectCycle(candidate, id, newBlockedBy)) {\n return fail('addBlockedBy would create a cycle in the blockedBy graph');\n }\n\n const changed = taskChanged(current, updated);\n if (changed) updated.updatedAt = now;\n context.tasks = candidate;\n\n return {\n ok: true,\n outcome: changed\n ? { index, kind: 'updated', id, fromStatus: current.status, toStatus: newStatus }\n : { index, kind: 'unchanged', id, status: newStatus },\n };\n}\n\n/**\n * Apply every entry in request order, threading the document.\n *\n * Each entry lands completely or not at all: a create whose dependencies fail\n * is not kept dependency-free, because handing back an unblocked task the\n * caller asked to be blocked is a lie it may immediately act on.\n */\nfunction applyUpsert(document: TaskDocument, items: TaskItemMutation[], now: string): ApplyResult {\n const context: UpsertContext = {\n tasks: [...document.tasks],\n nextId: document.nextId,\n refIds: new Map(),\n failedRefs: new Map(),\n };\n const outcomes: UpsertItemOutcome[] = [];\n let applied = 0;\n\n for (const [index, item] of items.entries()) {\n const shapeError = checkShape(context, item);\n const result = shapeError\n ? fail(shapeError)\n : item.id === undefined\n ? applyCreateItem(context, item, index, now)\n : applyUpdateItem(context, item, index, now);\n\n if (result.ok) {\n outcomes.push(result.outcome);\n applied += 1;\n continue;\n }\n // A ref whose owner failed must not read as a typo to the entries that\n // depend on it, so the dead ref is recorded rather than left unknown.\n if (item.ref && REF_PATTERN.test(item.ref) && !context.refIds.has(item.ref)) {\n context.failedRefs.set(item.ref, index);\n }\n outcomes.push({\n index,\n kind: 'failed',\n message: result.message,\n ...(item.id === undefined ? {} : { id: item.id }),\n ...(item.ref ? { ref: item.ref } : {}),\n });\n }\n\n return {\n // Nothing applied means nothing to write: returning the original object by\n // reference keeps the caller's write-skip path identical to a hard error.\n document: applied > 0 ? withTasks(document, context.tasks, context.nextId) : document,\n op: { kind: 'upsert', items: outcomes, applied, failed: outcomes.length - applied },\n };\n}\n\n/**\n * Pure reducer: (document, action, params) -> (document, op).\n *\n * All validation is in-line: structural guards plus state-aware checks\n * (transition legality, dangling or deleted blockedBy, self-block, cycles,\n * in-flight delegations). The caller owns persistence and formatting.\n */\nexport function applyTaskMutation(\n document: TaskDocument,\n action: ReducerAction,\n params: TaskMutationParams,\n now: string = new Date().toISOString(),\n): ApplyResult {\n switch (action) {\n case 'upsert': {\n const items = params.tasks;\n if (!Array.isArray(items) || items.length === 0) {\n return errorResult(\n document,\n 'upsert requires a non-empty tasks array, e.g. {\"action\":\"upsert\",\"tasks\":[{\"subject\":\"Write tests\"}]}',\n );\n }\n if (items.length > MAX_UPSERT_ITEMS) {\n return errorResult(\n document,\n `upsert accepts at most ${MAX_UPSERT_ITEMS} entries per call (received ${items.length})`,\n );\n }\n return applyUpsert(document, items, now);\n }\n\n case 'list': {\n return {\n document,\n op: {\n kind: 'list',\n includeDeleted: params.includeDeleted === true,\n ...(params.status !== undefined ? { statusFilter: params.status } : {}),\n },\n };\n }\n\n case 'get': {\n if (params.id === undefined) return errorResult(document, 'id required for get');\n const task = document.tasks.find((candidate) => candidate.id === params.id);\n if (!task) return errorResult(document, `#${params.id} not found`);\n return { document, op: { kind: 'get', task } };\n }\n\n case 'delete': {\n if (params.id === undefined) return errorResult(document, 'id required for delete');\n const index = document.tasks.findIndex((task) => task.id === params.id);\n if (index === -1) return errorResult(document, `#${params.id} not found`);\n const current = document.tasks[index];\n if (current.status === DELETED_STATUS) return errorResult(document, `#${current.id} is already deleted`);\n if (isDelegationActive(current)) {\n return errorResult(document, `#${current.id} has a running delegation — cancel it before deleting`);\n }\n\n const tasks = [...document.tasks];\n tasks[index] = { ...current, status: DELETED_STATUS, updatedAt: now };\n return {\n document: withTasks(document, tasks),\n op: { kind: 'delete', id: current.id, subject: current.subject },\n };\n }\n\n case 'clear': {\n const active = document.tasks.filter(isDelegationActive);\n if (active.length > 0) {\n const ids = active.map((task) => `#${task.id}`).join(', ');\n return errorResult(document, `cannot clear while delegations are running (${ids}) — cancel them first`);\n }\n const count = document.tasks.length;\n return {\n document: withTasks(document, [], 1),\n op: { kind: 'clear', count },\n };\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"reducer.cjs","names":["isTransitionValid","detectCycle","isDelegationActive","DELETED_STATUS"],"sources":["../../src/store/reducer.ts"],"sourcesContent":["import { isTransitionValid } from './invariants.ts';\nimport { detectCycle } from './taskGraph.ts';\nimport {\n DELETED_STATUS,\n type DepToken,\n isDelegationActive,\n type Task,\n type TaskAction,\n type TaskDocument,\n type TaskItemMutation,\n type TaskMutationParams,\n type TaskStatus,\n type UpsertItemOutcome,\n} from './types.ts';\n\n/** Bounds the work one call can do, and with it the size of one committed write. */\nexport const MAX_UPSERT_ITEMS = 100;\n\n/**\n * A ref must start with a letter, which makes it structurally unconfusable with\n * a stringified id. That is why resolving a dependency token is just a `typeof`\n * check with no coercion heuristics.\n */\nexport const REF_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,31}$/;\n\n/**\n * Reducer outcome. Closed tagged union: adding an action requires extending\n * this union and the response formatter, which the compiler enforces.\n *\n * `upsert` carries one outcome per request item. It is returned even when every\n * item failed — `error` is reserved for whole-call faults — so the tool layer\n * still has the per-item messages to report.\n */\nexport type Op =\n | { kind: 'upsert'; items: UpsertItemOutcome[]; applied: number; failed: number }\n | { kind: 'delete'; id: number; subject: string }\n | { kind: 'list'; statusFilter?: TaskStatus; includeDeleted: boolean }\n | { kind: 'get'; task: Task }\n | { kind: 'clear'; count: number }\n | { kind: 'error'; message: string };\n\nexport interface ApplyResult {\n document: TaskDocument;\n op: Op;\n}\n\n/** Actions the reducer owns. `assign`/`cancel` are handled by the delegation manager. */\nexport type ReducerAction = Exclude<TaskAction, 'assign' | 'cancel'>;\n\n/**\n * Does this op represent state that must reach disk?\n *\n * The exhaustive switch is the point: a future `Op` variant cannot be added\n * without declaring its persistence intent, so nothing silently starts or stops\n * bumping `rev`.\n */\nexport function isCommittingOp(op: Op): boolean {\n switch (op.kind) {\n case 'error':\n case 'list':\n case 'get':\n return false;\n case 'upsert':\n return op.items.some((item) => item.kind === 'created' || item.kind === 'updated');\n case 'delete':\n case 'clear':\n return true;\n }\n}\n\n/** Joined per-item failures, for the thrown error when an upsert applied nothing. */\nexport function formatUpsertFailure(op: Extract<Op, { kind: 'upsert' }>): string {\n return op.items\n .filter((item) => item.kind === 'failed')\n .map((item) => `item[${item.index}]: ${item.message}`)\n .join('\\n');\n}\n\n/** The single item's outcome, for callers that only ever send a batch of one. */\nexport function singleItemOutcome(op: Op): UpsertItemOutcome | undefined {\n return op.kind === 'upsert' ? op.items[0] : undefined;\n}\n\nfunction errorResult(document: TaskDocument, message: string): ApplyResult {\n return { document, op: { kind: 'error', message } };\n}\n\nfunction sameNumberList(a: number[] | undefined, b: number[] | undefined): boolean {\n const x = a ?? [];\n const y = b ?? [];\n return x.length === y.length && x.every((value, index) => value === y[index]);\n}\n\nfunction sameRecord(a: Record<string, unknown> | undefined, b: Record<string, unknown> | undefined): boolean {\n return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);\n}\n\n/**\n * Did this update change anything? A no-effect update (status re-set to its\n * current value, fields re-sent unchanged) reports \"No change\" rather than\n * \"Updated #N\" — without the distinction a model can loop re-issuing the same\n * call believing it never landed.\n */\nfunction taskChanged(before: Task, after: Task): boolean {\n return (\n before.subject !== after.subject ||\n before.status !== after.status ||\n before.description !== after.description ||\n before.activeForm !== after.activeForm ||\n before.owner !== after.owner ||\n !sameNumberList(before.blockedBy, after.blockedBy) ||\n !sameRecord(before.metadata, after.metadata)\n );\n}\n\nfunction withTasks(document: TaskDocument, tasks: Task[], nextId = document.nextId): TaskDocument {\n return { ...document, tasks, nextId };\n}\n\n/** Merge incoming metadata over a base; a `null` value deletes its key. */\nfunction mergeMetadata(\n base: Record<string, unknown> | undefined,\n incoming: Record<string, unknown>,\n): Record<string, unknown> | undefined {\n const merged: Record<string, unknown> = { ...base };\n for (const [key, value] of Object.entries(incoming)) {\n if (value === null) delete merged[key];\n else merged[key] = value;\n }\n return Object.keys(merged).length ? merged : undefined;\n}\n\n/** Copy the optional scalar fields an item may set onto a task, in place. */\nfunction assignScalars(target: Task, item: TaskItemMutation): void {\n if (item.subject !== undefined) target.subject = item.subject;\n if (item.description !== undefined) target.description = item.description;\n if (item.activeForm !== undefined) target.activeForm = item.activeForm;\n if (item.owner !== undefined) target.owner = item.owner;\n if (item.metadata !== undefined) {\n const merged = mergeMetadata(target.metadata, item.metadata);\n if (merged === undefined) delete target.metadata;\n else target.metadata = merged;\n }\n}\n\n/**\n * Working state threaded item to item, so entry N observes every effect of\n * entries 1..N-1. `refIds` and `failedRefs` are what make the cascade work:\n * refs resolve backward only, so an entry that names a failed sibling is always\n * processed after the failure that killed it.\n */\ninterface UpsertContext {\n tasks: Task[];\n nextId: number;\n refIds: Map<string, number>;\n failedRefs: Map<string, number>;\n}\n\ntype Resolved = { ok: true; id: number } | { ok: false; message: string };\n\nfunction resolveDep(context: UpsertContext, field: string, token: DepToken): Resolved {\n if (typeof token === 'number') return { ok: true, id: token };\n if (!REF_PATTERN.test(token)) {\n return { ok: false, message: `${field}: \"${token}\" is not a valid ref; pass an existing task id as a number` };\n }\n const id = context.refIds.get(token);\n if (id !== undefined) return { ok: true, id };\n const failedAt = context.failedRefs.get(token);\n if (failedAt !== undefined) {\n return { ok: false, message: `${field}: ref \"${token}\" refers to item[${failedAt}], which failed` };\n }\n return {\n ok: false,\n message: `${field}: unknown ref \"${token}\" — a ref must be declared by an earlier entry in the same call`,\n };\n}\n\ntype ItemResult = { ok: true; outcome: UpsertItemOutcome } | { ok: false; message: string };\n\nfunction fail(message: string): ItemResult {\n return { ok: false, message };\n}\n\n/** Structural checks that apply to every entry, whichever kind it is. */\nfunction checkShape(context: UpsertContext, item: TaskItemMutation): string | undefined {\n if (item.ref !== undefined) {\n if (item.id !== undefined) return 'ref is only valid when creating a task; drop the id, or drop the ref';\n if (!REF_PATTERN.test(item.ref)) {\n return `ref \"${item.ref}\" is invalid: a ref must start with a letter and contain only letters, digits, - or _`;\n }\n const owner = context.refIds.get(item.ref) !== undefined || context.failedRefs.has(item.ref);\n if (owner) return `duplicate ref \"${item.ref}\": it was already declared by an earlier entry`;\n }\n return undefined;\n}\n\nfunction applyCreateItem(context: UpsertContext, item: TaskItemMutation, index: number, now: string): ItemResult {\n if (item.subject === undefined) return fail('subject is required when the entry has no id');\n if (!item.subject.trim()) return fail('subject must not be blank');\n if (item.addBlockedBy !== undefined || item.removeBlockedBy !== undefined) {\n return fail('addBlockedBy / removeBlockedBy are only for an entry with an id; a new task uses blockedBy');\n }\n // A task created as a tombstone is invisible to every view and can never\n // leave `deleted`, so it would be a write-only black hole.\n if (item.status === DELETED_STATUS) {\n return fail('cannot create a task with status deleted; create it, then use action delete');\n }\n\n const blockedBy: number[] = [];\n for (const token of item.blockedBy ?? []) {\n const resolved = resolveDep(context, 'blockedBy', token);\n if (!resolved.ok) return fail(resolved.message);\n const dep = context.tasks.find((task) => task.id === resolved.id);\n if (!dep) return fail(`blockedBy: #${resolved.id} not found`);\n if (dep.status === DELETED_STATUS) return fail(`blockedBy: #${resolved.id} is deleted`);\n if (!blockedBy.includes(resolved.id)) blockedBy.push(resolved.id);\n }\n\n // No cycle check is needed: the id is freshly allocated, so nothing already\n // in the document can point at it yet.\n const created: Task = {\n id: context.nextId,\n subject: item.subject,\n status: item.status ?? 'pending',\n createdAt: now,\n updatedAt: now,\n };\n assignScalars(created, item);\n if (blockedBy.length) created.blockedBy = blockedBy;\n\n context.nextId += 1;\n context.tasks.push(created);\n if (item.ref) context.refIds.set(item.ref, created.id);\n\n return {\n ok: true,\n outcome: {\n index,\n kind: 'created',\n id: created.id,\n subject: created.subject,\n status: created.status,\n ...(item.ref ? { ref: item.ref } : {}),\n ...(blockedBy.length ? { blockedBy } : {}),\n },\n };\n}\n\nfunction applyUpdateItem(context: UpsertContext, item: TaskItemMutation, index: number, now: string): ItemResult {\n const id = item.id as number;\n const at = context.tasks.findIndex((task) => task.id === id);\n if (at === -1) return fail(`#${id} not found`);\n if (item.blockedBy !== undefined) {\n return fail('blockedBy is only for a new entry; use addBlockedBy / removeBlockedBy on an entry that has an id');\n }\n\n const hasMutation =\n item.subject !== undefined ||\n item.description !== undefined ||\n item.activeForm !== undefined ||\n item.status !== undefined ||\n item.owner !== undefined ||\n item.metadata !== undefined ||\n Boolean(item.addBlockedBy?.length) ||\n Boolean(item.removeBlockedBy?.length);\n if (!hasMutation) {\n return fail(\n 'nothing to change: provide at least one of subject, description, activeForm, status, owner, metadata, addBlockedBy, or removeBlockedBy',\n );\n }\n\n const current = context.tasks[at];\n\n // The Task Space overlay commits free text from an inline editor, so a blank\n // subject reaches the reducer as a real update rather than as a missing\n // field. Persisting it would leave an unidentifiable row.\n if (item.subject !== undefined && !item.subject.trim()) return fail('subject must not be blank');\n\n let newStatus = current.status;\n if (item.status !== undefined) {\n if (!isTransitionValid(current.status, item.status)) {\n return fail(`illegal transition ${current.status} -> ${item.status}`);\n }\n newStatus = item.status;\n }\n\n let newBlockedBy = current.blockedBy ? [...current.blockedBy] : [];\n for (const token of item.removeBlockedBy ?? []) {\n const resolved = resolveDep(context, 'removeBlockedBy', token);\n if (!resolved.ok) return fail(resolved.message);\n // Removing an id that is not there is a well-defined no-op, so unlike the\n // add path this deliberately skips the existence check.\n newBlockedBy = newBlockedBy.filter((dep) => dep !== resolved.id);\n }\n for (const token of item.addBlockedBy ?? []) {\n const resolved = resolveDep(context, 'addBlockedBy', token);\n if (!resolved.ok) return fail(resolved.message);\n if (resolved.id === id) return fail(`cannot block #${id} on itself`);\n const dep = context.tasks.find((task) => task.id === resolved.id);\n if (!dep) return fail(`addBlockedBy: #${resolved.id} not found`);\n if (dep.status === DELETED_STATUS) return fail(`addBlockedBy: #${resolved.id} is deleted`);\n if (!newBlockedBy.includes(resolved.id)) newBlockedBy.push(resolved.id);\n }\n\n const updated: Task = { ...current, status: newStatus };\n assignScalars(updated, item);\n if (newBlockedBy.length) updated.blockedBy = newBlockedBy;\n else delete updated.blockedBy;\n\n // The candidate is written before the check because `detectCycle` unions the\n // node's stored edges with the ones passed in: checking against the old array\n // would resurrect an edge `removeBlockedBy` just stripped and reject a\n // remove+add that legitimately breaks a cycle.\n const candidate = [...context.tasks];\n candidate[at] = updated;\n if (item.addBlockedBy?.length && detectCycle(candidate, id, newBlockedBy)) {\n return fail('addBlockedBy would create a cycle in the blockedBy graph');\n }\n\n const changed = taskChanged(current, updated);\n if (changed) updated.updatedAt = now;\n context.tasks = candidate;\n\n return {\n ok: true,\n outcome: changed\n ? { index, kind: 'updated', id, fromStatus: current.status, toStatus: newStatus }\n : { index, kind: 'unchanged', id, status: newStatus },\n };\n}\n\n/**\n * Apply every entry in request order, threading the document.\n *\n * Each entry lands completely or not at all: a create whose dependencies fail\n * is not kept dependency-free, because handing back an unblocked task the\n * caller asked to be blocked is a lie it may immediately act on.\n */\nfunction applyUpsert(document: TaskDocument, items: TaskItemMutation[], now: string): ApplyResult {\n const context: UpsertContext = {\n tasks: [...document.tasks],\n nextId: document.nextId,\n refIds: new Map(),\n failedRefs: new Map(),\n };\n const outcomes: UpsertItemOutcome[] = [];\n let applied = 0;\n\n for (const [index, item] of items.entries()) {\n const shapeError = checkShape(context, item);\n const result = shapeError\n ? fail(shapeError)\n : item.id === undefined\n ? applyCreateItem(context, item, index, now)\n : applyUpdateItem(context, item, index, now);\n\n if (result.ok) {\n outcomes.push(result.outcome);\n applied += 1;\n continue;\n }\n // A ref whose owner failed must not read as a typo to the entries that\n // depend on it, so the dead ref is recorded rather than left unknown.\n if (item.ref && REF_PATTERN.test(item.ref) && !context.refIds.has(item.ref)) {\n context.failedRefs.set(item.ref, index);\n }\n outcomes.push({\n index,\n kind: 'failed',\n message: result.message,\n ...(item.id === undefined ? {} : { id: item.id }),\n ...(item.ref ? { ref: item.ref } : {}),\n });\n }\n\n return {\n // Nothing applied means nothing to write: returning the original object by\n // reference keeps the caller's write-skip path identical to a hard error.\n document: applied > 0 ? withTasks(document, context.tasks, context.nextId) : document,\n op: { kind: 'upsert', items: outcomes, applied, failed: outcomes.length - applied },\n };\n}\n\n/**\n * Pure reducer: (document, action, params) -> (document, op).\n *\n * All validation is in-line: structural guards plus state-aware checks\n * (transition legality, dangling or deleted blockedBy, self-block, cycles,\n * in-flight delegations). The caller owns persistence and formatting.\n */\nexport function applyTaskMutation(\n document: TaskDocument,\n action: ReducerAction,\n params: TaskMutationParams,\n now: string = new Date().toISOString(),\n): ApplyResult {\n switch (action) {\n case 'upsert': {\n const items = params.tasks;\n if (!Array.isArray(items) || items.length === 0) {\n return errorResult(\n document,\n 'upsert requires a non-empty tasks array, e.g. {\"action\":\"upsert\",\"tasks\":[{\"subject\":\"Write tests\"}]}',\n );\n }\n if (items.length > MAX_UPSERT_ITEMS) {\n return errorResult(\n document,\n `upsert accepts at most ${MAX_UPSERT_ITEMS} entries per call (received ${items.length})`,\n );\n }\n return applyUpsert(document, items, now);\n }\n\n case 'list': {\n return {\n document,\n op: {\n kind: 'list',\n includeDeleted: params.includeDeleted === true,\n ...(params.status !== undefined ? { statusFilter: params.status } : {}),\n },\n };\n }\n\n case 'get': {\n if (params.id === undefined) return errorResult(document, 'id required for get');\n const task = document.tasks.find((candidate) => candidate.id === params.id);\n if (!task) return errorResult(document, `#${params.id} not found`);\n return { document, op: { kind: 'get', task } };\n }\n\n case 'delete': {\n if (params.id === undefined) return errorResult(document, 'id required for delete');\n const index = document.tasks.findIndex((task) => task.id === params.id);\n if (index === -1) return errorResult(document, `#${params.id} not found`);\n const current = document.tasks[index];\n if (current.status === DELETED_STATUS) return errorResult(document, `#${current.id} is already deleted`);\n if (isDelegationActive(current)) {\n return errorResult(document, `#${current.id} has a running delegation — cancel it before deleting`);\n }\n\n const tasks = [...document.tasks];\n tasks[index] = { ...current, status: DELETED_STATUS, updatedAt: now };\n return {\n document: withTasks(document, tasks),\n op: { kind: 'delete', id: current.id, subject: current.subject },\n };\n }\n\n case 'clear': {\n const active = document.tasks.filter(isDelegationActive);\n if (active.length > 0) {\n const ids = active.map((task) => `#${task.id}`).join(', ');\n return errorResult(document, `cannot clear while delegations are running (${ids}) — cancel them first`);\n }\n const count = document.tasks.length;\n return {\n document: withTasks(document, [], 1),\n op: { kind: 'clear', count },\n };\n }\n }\n}\n"],"mappings":"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"}
|
package/dist/store/reducer.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{DELETED_STATUS as e,isDelegationActive as t}from"./types.mjs";import{detectCycle as n}from"./taskGraph.mjs";import{isTransitionValid as r}from"./invariants.mjs";const i=100,a=/^[A-Za-z][A-Za-z0-9_-]{0,31}$/;function o(e){switch(e.kind){case`error`:case`list`:case`get`:return!1;case`upsert`:return e.
|
|
1
|
+
import{DELETED_STATUS as e,isDelegationActive as t}from"./types.mjs";import{detectCycle as n}from"./taskGraph.mjs";import{isTransitionValid as r}from"./invariants.mjs";const i=100,a=/^[A-Za-z][A-Za-z0-9_-]{0,31}$/;function o(e){switch(e.kind){case`error`:case`list`:case`get`:return!1;case`upsert`:return e.items.some(e=>e.kind===`created`||e.kind===`updated`);case`delete`:case`clear`:return!0}}function s(e){return e.items.filter(e=>e.kind===`failed`).map(e=>`item[${e.index}]: ${e.message}`).join(`
|
|
2
2
|
`)}function c(e){return e.kind===`upsert`?e.items[0]:void 0}function l(e,t){return{document:e,op:{kind:`error`,message:t}}}function u(e,t){let n=e??[],r=t??[];return n.length===r.length&&n.every((e,t)=>e===r[t])}function d(e,t){return JSON.stringify(e??null)===JSON.stringify(t??null)}function f(e,t){return e.subject!==t.subject||e.status!==t.status||e.description!==t.description||e.activeForm!==t.activeForm||e.owner!==t.owner||!u(e.blockedBy,t.blockedBy)||!d(e.metadata,t.metadata)}function p(e,t,n=e.nextId){return{...e,tasks:t,nextId:n}}function m(e,t){let n={...e};for(let[e,r]of Object.entries(t))r===null?delete n[e]:n[e]=r;return Object.keys(n).length?n:void 0}function h(e,t){if(t.subject!==void 0&&(e.subject=t.subject),t.description!==void 0&&(e.description=t.description),t.activeForm!==void 0&&(e.activeForm=t.activeForm),t.owner!==void 0&&(e.owner=t.owner),t.metadata!==void 0){let n=m(e.metadata,t.metadata);n===void 0?delete e.metadata:e.metadata=n}}function g(e,t,n){if(typeof n==`number`)return{ok:!0,id:n};if(!a.test(n))return{ok:!1,message:`${t}: "${n}" is not a valid ref; pass an existing task id as a number`};let r=e.refIds.get(n);if(r!==void 0)return{ok:!0,id:r};let i=e.failedRefs.get(n);return i===void 0?{ok:!1,message:`${t}: unknown ref "${n}" — a ref must be declared by an earlier entry in the same call`}:{ok:!1,message:`${t}: ref "${n}" refers to item[${i}], which failed`}}function _(e){return{ok:!1,message:e}}function v(e,t){if(t.ref!==void 0){if(t.id!==void 0)return`ref is only valid when creating a task; drop the id, or drop the ref`;if(!a.test(t.ref))return`ref "${t.ref}" is invalid: a ref must start with a letter and contain only letters, digits, - or _`;if(e.refIds.get(t.ref)!==void 0||e.failedRefs.has(t.ref))return`duplicate ref "${t.ref}": it was already declared by an earlier entry`}}function y(e,t,n,r){if(t.subject===void 0)return _(`subject is required when the entry has no id`);if(!t.subject.trim())return _(`subject must not be blank`);if(t.addBlockedBy!==void 0||t.removeBlockedBy!==void 0)return _(`addBlockedBy / removeBlockedBy are only for an entry with an id; a new task uses blockedBy`);if(t.status===`deleted`)return _(`cannot create a task with status deleted; create it, then use action delete`);let i=[];for(let n of t.blockedBy??[]){let t=g(e,`blockedBy`,n);if(!t.ok)return _(t.message);let r=e.tasks.find(e=>e.id===t.id);if(!r)return _(`blockedBy: #${t.id} not found`);if(r.status===`deleted`)return _(`blockedBy: #${t.id} is deleted`);i.includes(t.id)||i.push(t.id)}let a={id:e.nextId,subject:t.subject,status:t.status??`pending`,createdAt:r,updatedAt:r};return h(a,t),i.length&&(a.blockedBy=i),e.nextId+=1,e.tasks.push(a),t.ref&&e.refIds.set(t.ref,a.id),{ok:!0,outcome:{index:n,kind:`created`,id:a.id,subject:a.subject,status:a.status,...t.ref?{ref:t.ref}:{},...i.length?{blockedBy:i}:{}}}}function b(e,t,i,a){let o=t.id,s=e.tasks.findIndex(e=>e.id===o);if(s===-1)return _(`#${o} not found`);if(t.blockedBy!==void 0)return _(`blockedBy is only for a new entry; use addBlockedBy / removeBlockedBy on an entry that has an id`);if(!(t.subject!==void 0||t.description!==void 0||t.activeForm!==void 0||t.status!==void 0||t.owner!==void 0||t.metadata!==void 0||t.addBlockedBy?.length||t.removeBlockedBy?.length))return _(`nothing to change: provide at least one of subject, description, activeForm, status, owner, metadata, addBlockedBy, or removeBlockedBy`);let c=e.tasks[s];if(t.subject!==void 0&&!t.subject.trim())return _(`subject must not be blank`);let l=c.status;if(t.status!==void 0){if(!r(c.status,t.status))return _(`illegal transition ${c.status} -> ${t.status}`);l=t.status}let u=c.blockedBy?[...c.blockedBy]:[];for(let n of t.removeBlockedBy??[]){let t=g(e,`removeBlockedBy`,n);if(!t.ok)return _(t.message);u=u.filter(e=>e!==t.id)}for(let n of t.addBlockedBy??[]){let t=g(e,`addBlockedBy`,n);if(!t.ok)return _(t.message);if(t.id===o)return _(`cannot block #${o} on itself`);let r=e.tasks.find(e=>e.id===t.id);if(!r)return _(`addBlockedBy: #${t.id} not found`);if(r.status===`deleted`)return _(`addBlockedBy: #${t.id} is deleted`);u.includes(t.id)||u.push(t.id)}let d={...c,status:l};h(d,t),u.length?d.blockedBy=u:delete d.blockedBy;let p=[...e.tasks];if(p[s]=d,t.addBlockedBy?.length&&n(p,o,u))return _(`addBlockedBy would create a cycle in the blockedBy graph`);let m=f(c,d);return m&&(d.updatedAt=a),e.tasks=p,{ok:!0,outcome:m?{index:i,kind:`updated`,id:o,fromStatus:c.status,toStatus:l}:{index:i,kind:`unchanged`,id:o,status:l}}}function x(e,t,n){let r={tasks:[...e.tasks],nextId:e.nextId,refIds:new Map,failedRefs:new Map},i=[],o=0;for(let[e,s]of t.entries()){let t=v(r,s),c=t?_(t):s.id===void 0?y(r,s,e,n):b(r,s,e,n);if(c.ok){i.push(c.outcome),o+=1;continue}s.ref&&a.test(s.ref)&&!r.refIds.has(s.ref)&&r.failedRefs.set(s.ref,e),i.push({index:e,kind:`failed`,message:c.message,...s.id===void 0?{}:{id:s.id},...s.ref?{ref:s.ref}:{}})}return{document:o>0?p(e,r.tasks,r.nextId):e,op:{kind:`upsert`,items:i,applied:o,failed:i.length-o}}}function S(n,r,i,a=new Date().toISOString()){switch(r){case`upsert`:{let e=i.tasks;return!Array.isArray(e)||e.length===0?l(n,`upsert requires a non-empty tasks array, e.g. {"action":"upsert","tasks":[{"subject":"Write tests"}]}`):e.length>100?l(n,`upsert accepts at most 100 entries per call (received ${e.length})`):x(n,e,a)}case`list`:return{document:n,op:{kind:`list`,includeDeleted:i.includeDeleted===!0,...i.status===void 0?{}:{statusFilter:i.status}}};case`get`:{if(i.id===void 0)return l(n,`id required for get`);let e=n.tasks.find(e=>e.id===i.id);return e?{document:n,op:{kind:`get`,task:e}}:l(n,`#${i.id} not found`)}case`delete`:{if(i.id===void 0)return l(n,`id required for delete`);let r=n.tasks.findIndex(e=>e.id===i.id);if(r===-1)return l(n,`#${i.id} not found`);let o=n.tasks[r];if(o.status===`deleted`)return l(n,`#${o.id} is already deleted`);if(t(o))return l(n,`#${o.id} has a running delegation — cancel it before deleting`);let s=[...n.tasks];return s[r]={...o,status:e,updatedAt:a},{document:p(n,s),op:{kind:`delete`,id:o.id,subject:o.subject}}}case`clear`:{let e=n.tasks.filter(t);if(e.length>0)return l(n,`cannot clear while delegations are running (${e.map(e=>`#${e.id}`).join(`, `)}) — cancel them first`);let r=n.tasks.length;return{document:p(n,[],1),op:{kind:`clear`,count:r}}}}}export{i as MAX_UPSERT_ITEMS,a as REF_PATTERN,S as applyTaskMutation,s as formatUpsertFailure,o as isCommittingOp,c as singleItemOutcome};
|
|
3
3
|
//# sourceMappingURL=reducer.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reducer.mjs","names":[],"sources":["../../src/store/reducer.ts"],"sourcesContent":["import { isTransitionValid } from './invariants.ts';\nimport { detectCycle } from './taskGraph.ts';\nimport {\n DELETED_STATUS,\n type DepToken,\n isDelegationActive,\n type Task,\n type TaskAction,\n type TaskDocument,\n type TaskItemMutation,\n type TaskMutationParams,\n type TaskStatus,\n type UpsertItemOutcome,\n} from './types.ts';\n\n/** Bounds the work one call can do, and with it the size of one committed write. */\nexport const MAX_UPSERT_ITEMS = 100;\n\n/**\n * A ref must start with a letter, which makes it structurally unconfusable with\n * a stringified id. That is why resolving a dependency token is just a `typeof`\n * check with no coercion heuristics.\n */\nexport const REF_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,31}$/;\n\n/**\n * Reducer outcome. Closed tagged union: adding an action requires extending\n * this union and the response formatter, which the compiler enforces.\n *\n * `upsert` carries one outcome per request item. It is returned even when every\n * item failed — `error` is reserved for whole-call faults — so the tool layer\n * still has the per-item messages to report.\n */\nexport type Op =\n | { kind: 'upsert'; items: UpsertItemOutcome[]; applied: number; failed: number }\n | { kind: 'delete'; id: number; subject: string }\n | { kind: 'list'; statusFilter?: TaskStatus; includeDeleted: boolean }\n | { kind: 'get'; task: Task }\n | { kind: 'clear'; count: number }\n | { kind: 'error'; message: string };\n\nexport interface ApplyResult {\n document: TaskDocument;\n op: Op;\n}\n\n/** Actions the reducer owns. `assign`/`cancel` are handled by the delegation manager. */\nexport type ReducerAction = Exclude<TaskAction, 'assign' | 'cancel'>;\n\n/**\n * Does this op represent state that must reach disk?\n *\n * The exhaustive switch is the point: a future `Op` variant cannot be added\n * without declaring its persistence intent, so nothing silently starts or stops\n * bumping `rev`.\n */\nexport function isCommittingOp(op: Op): boolean {\n switch (op.kind) {\n case 'error':\n case 'list':\n case 'get':\n return false;\n case 'upsert':\n return op.applied > 0;\n case 'delete':\n case 'clear':\n return true;\n }\n}\n\n/** Joined per-item failures, for the thrown error when an upsert applied nothing. */\nexport function formatUpsertFailure(op: Extract<Op, { kind: 'upsert' }>): string {\n return op.items\n .filter((item) => item.kind === 'failed')\n .map((item) => `item[${item.index}]: ${item.message}`)\n .join('\\n');\n}\n\n/** The single item's outcome, for callers that only ever send a batch of one. */\nexport function singleItemOutcome(op: Op): UpsertItemOutcome | undefined {\n return op.kind === 'upsert' ? op.items[0] : undefined;\n}\n\nfunction errorResult(document: TaskDocument, message: string): ApplyResult {\n return { document, op: { kind: 'error', message } };\n}\n\nfunction sameNumberList(a: number[] | undefined, b: number[] | undefined): boolean {\n const x = a ?? [];\n const y = b ?? [];\n return x.length === y.length && x.every((value, index) => value === y[index]);\n}\n\nfunction sameRecord(a: Record<string, unknown> | undefined, b: Record<string, unknown> | undefined): boolean {\n return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);\n}\n\n/**\n * Did this update change anything? A no-effect update (status re-set to its\n * current value, fields re-sent unchanged) reports \"No change\" rather than\n * \"Updated #N\" — without the distinction a model can loop re-issuing the same\n * call believing it never landed.\n */\nfunction taskChanged(before: Task, after: Task): boolean {\n return (\n before.subject !== after.subject ||\n before.status !== after.status ||\n before.description !== after.description ||\n before.activeForm !== after.activeForm ||\n before.owner !== after.owner ||\n !sameNumberList(before.blockedBy, after.blockedBy) ||\n !sameRecord(before.metadata, after.metadata)\n );\n}\n\nfunction withTasks(document: TaskDocument, tasks: Task[], nextId = document.nextId): TaskDocument {\n return { ...document, tasks, nextId };\n}\n\n/** Merge incoming metadata over a base; a `null` value deletes its key. */\nfunction mergeMetadata(\n base: Record<string, unknown> | undefined,\n incoming: Record<string, unknown>,\n): Record<string, unknown> | undefined {\n const merged: Record<string, unknown> = { ...base };\n for (const [key, value] of Object.entries(incoming)) {\n if (value === null) delete merged[key];\n else merged[key] = value;\n }\n return Object.keys(merged).length ? merged : undefined;\n}\n\n/** Copy the optional scalar fields an item may set onto a task, in place. */\nfunction assignScalars(target: Task, item: TaskItemMutation): void {\n if (item.subject !== undefined) target.subject = item.subject;\n if (item.description !== undefined) target.description = item.description;\n if (item.activeForm !== undefined) target.activeForm = item.activeForm;\n if (item.owner !== undefined) target.owner = item.owner;\n if (item.metadata !== undefined) {\n const merged = mergeMetadata(target.metadata, item.metadata);\n if (merged === undefined) delete target.metadata;\n else target.metadata = merged;\n }\n}\n\n/**\n * Working state threaded item to item, so entry N observes every effect of\n * entries 1..N-1. `refIds` and `failedRefs` are what make the cascade work:\n * refs resolve backward only, so an entry that names a failed sibling is always\n * processed after the failure that killed it.\n */\ninterface UpsertContext {\n tasks: Task[];\n nextId: number;\n refIds: Map<string, number>;\n failedRefs: Map<string, number>;\n}\n\ntype Resolved = { ok: true; id: number } | { ok: false; message: string };\n\nfunction resolveDep(context: UpsertContext, field: string, token: DepToken): Resolved {\n if (typeof token === 'number') return { ok: true, id: token };\n if (!REF_PATTERN.test(token)) {\n return { ok: false, message: `${field}: \"${token}\" is not a valid ref; pass an existing task id as a number` };\n }\n const id = context.refIds.get(token);\n if (id !== undefined) return { ok: true, id };\n const failedAt = context.failedRefs.get(token);\n if (failedAt !== undefined) {\n return { ok: false, message: `${field}: ref \"${token}\" refers to item[${failedAt}], which failed` };\n }\n return {\n ok: false,\n message: `${field}: unknown ref \"${token}\" — a ref must be declared by an earlier entry in the same call`,\n };\n}\n\ntype ItemResult = { ok: true; outcome: UpsertItemOutcome } | { ok: false; message: string };\n\nfunction fail(message: string): ItemResult {\n return { ok: false, message };\n}\n\n/** Structural checks that apply to every entry, whichever kind it is. */\nfunction checkShape(context: UpsertContext, item: TaskItemMutation): string | undefined {\n if (item.ref !== undefined) {\n if (item.id !== undefined) return 'ref is only valid when creating a task; drop the id, or drop the ref';\n if (!REF_PATTERN.test(item.ref)) {\n return `ref \"${item.ref}\" is invalid: a ref must start with a letter and contain only letters, digits, - or _`;\n }\n const owner = context.refIds.get(item.ref) !== undefined || context.failedRefs.has(item.ref);\n if (owner) return `duplicate ref \"${item.ref}\": it was already declared by an earlier entry`;\n }\n return undefined;\n}\n\nfunction applyCreateItem(context: UpsertContext, item: TaskItemMutation, index: number, now: string): ItemResult {\n if (item.subject === undefined) return fail('subject is required when the entry has no id');\n if (!item.subject.trim()) return fail('subject must not be blank');\n if (item.addBlockedBy !== undefined || item.removeBlockedBy !== undefined) {\n return fail('addBlockedBy / removeBlockedBy are only for an entry with an id; a new task uses blockedBy');\n }\n // A task created as a tombstone is invisible to every view and can never\n // leave `deleted`, so it would be a write-only black hole.\n if (item.status === DELETED_STATUS) {\n return fail('cannot create a task with status deleted; create it, then use action delete');\n }\n\n const blockedBy: number[] = [];\n for (const token of item.blockedBy ?? []) {\n const resolved = resolveDep(context, 'blockedBy', token);\n if (!resolved.ok) return fail(resolved.message);\n const dep = context.tasks.find((task) => task.id === resolved.id);\n if (!dep) return fail(`blockedBy: #${resolved.id} not found`);\n if (dep.status === DELETED_STATUS) return fail(`blockedBy: #${resolved.id} is deleted`);\n if (!blockedBy.includes(resolved.id)) blockedBy.push(resolved.id);\n }\n\n // No cycle check is needed: the id is freshly allocated, so nothing already\n // in the document can point at it yet.\n const created: Task = {\n id: context.nextId,\n subject: item.subject,\n status: item.status ?? 'pending',\n createdAt: now,\n updatedAt: now,\n };\n assignScalars(created, item);\n if (blockedBy.length) created.blockedBy = blockedBy;\n\n context.nextId += 1;\n context.tasks.push(created);\n if (item.ref) context.refIds.set(item.ref, created.id);\n\n return {\n ok: true,\n outcome: {\n index,\n kind: 'created',\n id: created.id,\n subject: created.subject,\n status: created.status,\n ...(item.ref ? { ref: item.ref } : {}),\n ...(blockedBy.length ? { blockedBy } : {}),\n },\n };\n}\n\nfunction applyUpdateItem(context: UpsertContext, item: TaskItemMutation, index: number, now: string): ItemResult {\n const id = item.id as number;\n const at = context.tasks.findIndex((task) => task.id === id);\n if (at === -1) return fail(`#${id} not found`);\n if (item.blockedBy !== undefined) {\n return fail('blockedBy is only for a new entry; use addBlockedBy / removeBlockedBy on an entry that has an id');\n }\n\n const hasMutation =\n item.subject !== undefined ||\n item.description !== undefined ||\n item.activeForm !== undefined ||\n item.status !== undefined ||\n item.owner !== undefined ||\n item.metadata !== undefined ||\n Boolean(item.addBlockedBy?.length) ||\n Boolean(item.removeBlockedBy?.length);\n if (!hasMutation) {\n return fail(\n 'nothing to change: provide at least one of subject, description, activeForm, status, owner, metadata, addBlockedBy, or removeBlockedBy',\n );\n }\n\n const current = context.tasks[at];\n\n // The Task Space overlay commits free text from an inline editor, so a blank\n // subject reaches the reducer as a real update rather than as a missing\n // field. Persisting it would leave an unidentifiable row.\n if (item.subject !== undefined && !item.subject.trim()) return fail('subject must not be blank');\n\n let newStatus = current.status;\n if (item.status !== undefined) {\n if (!isTransitionValid(current.status, item.status)) {\n return fail(`illegal transition ${current.status} -> ${item.status}`);\n }\n newStatus = item.status;\n }\n\n let newBlockedBy = current.blockedBy ? [...current.blockedBy] : [];\n for (const token of item.removeBlockedBy ?? []) {\n const resolved = resolveDep(context, 'removeBlockedBy', token);\n if (!resolved.ok) return fail(resolved.message);\n // Removing an id that is not there is a well-defined no-op, so unlike the\n // add path this deliberately skips the existence check.\n newBlockedBy = newBlockedBy.filter((dep) => dep !== resolved.id);\n }\n for (const token of item.addBlockedBy ?? []) {\n const resolved = resolveDep(context, 'addBlockedBy', token);\n if (!resolved.ok) return fail(resolved.message);\n if (resolved.id === id) return fail(`cannot block #${id} on itself`);\n const dep = context.tasks.find((task) => task.id === resolved.id);\n if (!dep) return fail(`addBlockedBy: #${resolved.id} not found`);\n if (dep.status === DELETED_STATUS) return fail(`addBlockedBy: #${resolved.id} is deleted`);\n if (!newBlockedBy.includes(resolved.id)) newBlockedBy.push(resolved.id);\n }\n\n const updated: Task = { ...current, status: newStatus };\n assignScalars(updated, item);\n if (newBlockedBy.length) updated.blockedBy = newBlockedBy;\n else delete updated.blockedBy;\n\n // The candidate is written before the check because `detectCycle` unions the\n // node's stored edges with the ones passed in: checking against the old array\n // would resurrect an edge `removeBlockedBy` just stripped and reject a\n // remove+add that legitimately breaks a cycle.\n const candidate = [...context.tasks];\n candidate[at] = updated;\n if (item.addBlockedBy?.length && detectCycle(candidate, id, newBlockedBy)) {\n return fail('addBlockedBy would create a cycle in the blockedBy graph');\n }\n\n const changed = taskChanged(current, updated);\n if (changed) updated.updatedAt = now;\n context.tasks = candidate;\n\n return {\n ok: true,\n outcome: changed\n ? { index, kind: 'updated', id, fromStatus: current.status, toStatus: newStatus }\n : { index, kind: 'unchanged', id, status: newStatus },\n };\n}\n\n/**\n * Apply every entry in request order, threading the document.\n *\n * Each entry lands completely or not at all: a create whose dependencies fail\n * is not kept dependency-free, because handing back an unblocked task the\n * caller asked to be blocked is a lie it may immediately act on.\n */\nfunction applyUpsert(document: TaskDocument, items: TaskItemMutation[], now: string): ApplyResult {\n const context: UpsertContext = {\n tasks: [...document.tasks],\n nextId: document.nextId,\n refIds: new Map(),\n failedRefs: new Map(),\n };\n const outcomes: UpsertItemOutcome[] = [];\n let applied = 0;\n\n for (const [index, item] of items.entries()) {\n const shapeError = checkShape(context, item);\n const result = shapeError\n ? fail(shapeError)\n : item.id === undefined\n ? applyCreateItem(context, item, index, now)\n : applyUpdateItem(context, item, index, now);\n\n if (result.ok) {\n outcomes.push(result.outcome);\n applied += 1;\n continue;\n }\n // A ref whose owner failed must not read as a typo to the entries that\n // depend on it, so the dead ref is recorded rather than left unknown.\n if (item.ref && REF_PATTERN.test(item.ref) && !context.refIds.has(item.ref)) {\n context.failedRefs.set(item.ref, index);\n }\n outcomes.push({\n index,\n kind: 'failed',\n message: result.message,\n ...(item.id === undefined ? {} : { id: item.id }),\n ...(item.ref ? { ref: item.ref } : {}),\n });\n }\n\n return {\n // Nothing applied means nothing to write: returning the original object by\n // reference keeps the caller's write-skip path identical to a hard error.\n document: applied > 0 ? withTasks(document, context.tasks, context.nextId) : document,\n op: { kind: 'upsert', items: outcomes, applied, failed: outcomes.length - applied },\n };\n}\n\n/**\n * Pure reducer: (document, action, params) -> (document, op).\n *\n * All validation is in-line: structural guards plus state-aware checks\n * (transition legality, dangling or deleted blockedBy, self-block, cycles,\n * in-flight delegations). The caller owns persistence and formatting.\n */\nexport function applyTaskMutation(\n document: TaskDocument,\n action: ReducerAction,\n params: TaskMutationParams,\n now: string = new Date().toISOString(),\n): ApplyResult {\n switch (action) {\n case 'upsert': {\n const items = params.tasks;\n if (!Array.isArray(items) || items.length === 0) {\n return errorResult(\n document,\n 'upsert requires a non-empty tasks array, e.g. {\"action\":\"upsert\",\"tasks\":[{\"subject\":\"Write tests\"}]}',\n );\n }\n if (items.length > MAX_UPSERT_ITEMS) {\n return errorResult(\n document,\n `upsert accepts at most ${MAX_UPSERT_ITEMS} entries per call (received ${items.length})`,\n );\n }\n return applyUpsert(document, items, now);\n }\n\n case 'list': {\n return {\n document,\n op: {\n kind: 'list',\n includeDeleted: params.includeDeleted === true,\n ...(params.status !== undefined ? { statusFilter: params.status } : {}),\n },\n };\n }\n\n case 'get': {\n if (params.id === undefined) return errorResult(document, 'id required for get');\n const task = document.tasks.find((candidate) => candidate.id === params.id);\n if (!task) return errorResult(document, `#${params.id} not found`);\n return { document, op: { kind: 'get', task } };\n }\n\n case 'delete': {\n if (params.id === undefined) return errorResult(document, 'id required for delete');\n const index = document.tasks.findIndex((task) => task.id === params.id);\n if (index === -1) return errorResult(document, `#${params.id} not found`);\n const current = document.tasks[index];\n if (current.status === DELETED_STATUS) return errorResult(document, `#${current.id} is already deleted`);\n if (isDelegationActive(current)) {\n return errorResult(document, `#${current.id} has a running delegation — cancel it before deleting`);\n }\n\n const tasks = [...document.tasks];\n tasks[index] = { ...current, status: DELETED_STATUS, updatedAt: now };\n return {\n document: withTasks(document, tasks),\n op: { kind: 'delete', id: current.id, subject: current.subject },\n };\n }\n\n case 'clear': {\n const active = document.tasks.filter(isDelegationActive);\n if (active.length > 0) {\n const ids = active.map((task) => `#${task.id}`).join(', ');\n return errorResult(document, `cannot clear while delegations are running (${ids}) — cancel them first`);\n }\n const count = document.tasks.length;\n return {\n document: withTasks(document, [], 1),\n op: { kind: 'clear', count },\n };\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"reducer.mjs","names":[],"sources":["../../src/store/reducer.ts"],"sourcesContent":["import { isTransitionValid } from './invariants.ts';\nimport { detectCycle } from './taskGraph.ts';\nimport {\n DELETED_STATUS,\n type DepToken,\n isDelegationActive,\n type Task,\n type TaskAction,\n type TaskDocument,\n type TaskItemMutation,\n type TaskMutationParams,\n type TaskStatus,\n type UpsertItemOutcome,\n} from './types.ts';\n\n/** Bounds the work one call can do, and with it the size of one committed write. */\nexport const MAX_UPSERT_ITEMS = 100;\n\n/**\n * A ref must start with a letter, which makes it structurally unconfusable with\n * a stringified id. That is why resolving a dependency token is just a `typeof`\n * check with no coercion heuristics.\n */\nexport const REF_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,31}$/;\n\n/**\n * Reducer outcome. Closed tagged union: adding an action requires extending\n * this union and the response formatter, which the compiler enforces.\n *\n * `upsert` carries one outcome per request item. It is returned even when every\n * item failed — `error` is reserved for whole-call faults — so the tool layer\n * still has the per-item messages to report.\n */\nexport type Op =\n | { kind: 'upsert'; items: UpsertItemOutcome[]; applied: number; failed: number }\n | { kind: 'delete'; id: number; subject: string }\n | { kind: 'list'; statusFilter?: TaskStatus; includeDeleted: boolean }\n | { kind: 'get'; task: Task }\n | { kind: 'clear'; count: number }\n | { kind: 'error'; message: string };\n\nexport interface ApplyResult {\n document: TaskDocument;\n op: Op;\n}\n\n/** Actions the reducer owns. `assign`/`cancel` are handled by the delegation manager. */\nexport type ReducerAction = Exclude<TaskAction, 'assign' | 'cancel'>;\n\n/**\n * Does this op represent state that must reach disk?\n *\n * The exhaustive switch is the point: a future `Op` variant cannot be added\n * without declaring its persistence intent, so nothing silently starts or stops\n * bumping `rev`.\n */\nexport function isCommittingOp(op: Op): boolean {\n switch (op.kind) {\n case 'error':\n case 'list':\n case 'get':\n return false;\n case 'upsert':\n return op.items.some((item) => item.kind === 'created' || item.kind === 'updated');\n case 'delete':\n case 'clear':\n return true;\n }\n}\n\n/** Joined per-item failures, for the thrown error when an upsert applied nothing. */\nexport function formatUpsertFailure(op: Extract<Op, { kind: 'upsert' }>): string {\n return op.items\n .filter((item) => item.kind === 'failed')\n .map((item) => `item[${item.index}]: ${item.message}`)\n .join('\\n');\n}\n\n/** The single item's outcome, for callers that only ever send a batch of one. */\nexport function singleItemOutcome(op: Op): UpsertItemOutcome | undefined {\n return op.kind === 'upsert' ? op.items[0] : undefined;\n}\n\nfunction errorResult(document: TaskDocument, message: string): ApplyResult {\n return { document, op: { kind: 'error', message } };\n}\n\nfunction sameNumberList(a: number[] | undefined, b: number[] | undefined): boolean {\n const x = a ?? [];\n const y = b ?? [];\n return x.length === y.length && x.every((value, index) => value === y[index]);\n}\n\nfunction sameRecord(a: Record<string, unknown> | undefined, b: Record<string, unknown> | undefined): boolean {\n return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);\n}\n\n/**\n * Did this update change anything? A no-effect update (status re-set to its\n * current value, fields re-sent unchanged) reports \"No change\" rather than\n * \"Updated #N\" — without the distinction a model can loop re-issuing the same\n * call believing it never landed.\n */\nfunction taskChanged(before: Task, after: Task): boolean {\n return (\n before.subject !== after.subject ||\n before.status !== after.status ||\n before.description !== after.description ||\n before.activeForm !== after.activeForm ||\n before.owner !== after.owner ||\n !sameNumberList(before.blockedBy, after.blockedBy) ||\n !sameRecord(before.metadata, after.metadata)\n );\n}\n\nfunction withTasks(document: TaskDocument, tasks: Task[], nextId = document.nextId): TaskDocument {\n return { ...document, tasks, nextId };\n}\n\n/** Merge incoming metadata over a base; a `null` value deletes its key. */\nfunction mergeMetadata(\n base: Record<string, unknown> | undefined,\n incoming: Record<string, unknown>,\n): Record<string, unknown> | undefined {\n const merged: Record<string, unknown> = { ...base };\n for (const [key, value] of Object.entries(incoming)) {\n if (value === null) delete merged[key];\n else merged[key] = value;\n }\n return Object.keys(merged).length ? merged : undefined;\n}\n\n/** Copy the optional scalar fields an item may set onto a task, in place. */\nfunction assignScalars(target: Task, item: TaskItemMutation): void {\n if (item.subject !== undefined) target.subject = item.subject;\n if (item.description !== undefined) target.description = item.description;\n if (item.activeForm !== undefined) target.activeForm = item.activeForm;\n if (item.owner !== undefined) target.owner = item.owner;\n if (item.metadata !== undefined) {\n const merged = mergeMetadata(target.metadata, item.metadata);\n if (merged === undefined) delete target.metadata;\n else target.metadata = merged;\n }\n}\n\n/**\n * Working state threaded item to item, so entry N observes every effect of\n * entries 1..N-1. `refIds` and `failedRefs` are what make the cascade work:\n * refs resolve backward only, so an entry that names a failed sibling is always\n * processed after the failure that killed it.\n */\ninterface UpsertContext {\n tasks: Task[];\n nextId: number;\n refIds: Map<string, number>;\n failedRefs: Map<string, number>;\n}\n\ntype Resolved = { ok: true; id: number } | { ok: false; message: string };\n\nfunction resolveDep(context: UpsertContext, field: string, token: DepToken): Resolved {\n if (typeof token === 'number') return { ok: true, id: token };\n if (!REF_PATTERN.test(token)) {\n return { ok: false, message: `${field}: \"${token}\" is not a valid ref; pass an existing task id as a number` };\n }\n const id = context.refIds.get(token);\n if (id !== undefined) return { ok: true, id };\n const failedAt = context.failedRefs.get(token);\n if (failedAt !== undefined) {\n return { ok: false, message: `${field}: ref \"${token}\" refers to item[${failedAt}], which failed` };\n }\n return {\n ok: false,\n message: `${field}: unknown ref \"${token}\" — a ref must be declared by an earlier entry in the same call`,\n };\n}\n\ntype ItemResult = { ok: true; outcome: UpsertItemOutcome } | { ok: false; message: string };\n\nfunction fail(message: string): ItemResult {\n return { ok: false, message };\n}\n\n/** Structural checks that apply to every entry, whichever kind it is. */\nfunction checkShape(context: UpsertContext, item: TaskItemMutation): string | undefined {\n if (item.ref !== undefined) {\n if (item.id !== undefined) return 'ref is only valid when creating a task; drop the id, or drop the ref';\n if (!REF_PATTERN.test(item.ref)) {\n return `ref \"${item.ref}\" is invalid: a ref must start with a letter and contain only letters, digits, - or _`;\n }\n const owner = context.refIds.get(item.ref) !== undefined || context.failedRefs.has(item.ref);\n if (owner) return `duplicate ref \"${item.ref}\": it was already declared by an earlier entry`;\n }\n return undefined;\n}\n\nfunction applyCreateItem(context: UpsertContext, item: TaskItemMutation, index: number, now: string): ItemResult {\n if (item.subject === undefined) return fail('subject is required when the entry has no id');\n if (!item.subject.trim()) return fail('subject must not be blank');\n if (item.addBlockedBy !== undefined || item.removeBlockedBy !== undefined) {\n return fail('addBlockedBy / removeBlockedBy are only for an entry with an id; a new task uses blockedBy');\n }\n // A task created as a tombstone is invisible to every view and can never\n // leave `deleted`, so it would be a write-only black hole.\n if (item.status === DELETED_STATUS) {\n return fail('cannot create a task with status deleted; create it, then use action delete');\n }\n\n const blockedBy: number[] = [];\n for (const token of item.blockedBy ?? []) {\n const resolved = resolveDep(context, 'blockedBy', token);\n if (!resolved.ok) return fail(resolved.message);\n const dep = context.tasks.find((task) => task.id === resolved.id);\n if (!dep) return fail(`blockedBy: #${resolved.id} not found`);\n if (dep.status === DELETED_STATUS) return fail(`blockedBy: #${resolved.id} is deleted`);\n if (!blockedBy.includes(resolved.id)) blockedBy.push(resolved.id);\n }\n\n // No cycle check is needed: the id is freshly allocated, so nothing already\n // in the document can point at it yet.\n const created: Task = {\n id: context.nextId,\n subject: item.subject,\n status: item.status ?? 'pending',\n createdAt: now,\n updatedAt: now,\n };\n assignScalars(created, item);\n if (blockedBy.length) created.blockedBy = blockedBy;\n\n context.nextId += 1;\n context.tasks.push(created);\n if (item.ref) context.refIds.set(item.ref, created.id);\n\n return {\n ok: true,\n outcome: {\n index,\n kind: 'created',\n id: created.id,\n subject: created.subject,\n status: created.status,\n ...(item.ref ? { ref: item.ref } : {}),\n ...(blockedBy.length ? { blockedBy } : {}),\n },\n };\n}\n\nfunction applyUpdateItem(context: UpsertContext, item: TaskItemMutation, index: number, now: string): ItemResult {\n const id = item.id as number;\n const at = context.tasks.findIndex((task) => task.id === id);\n if (at === -1) return fail(`#${id} not found`);\n if (item.blockedBy !== undefined) {\n return fail('blockedBy is only for a new entry; use addBlockedBy / removeBlockedBy on an entry that has an id');\n }\n\n const hasMutation =\n item.subject !== undefined ||\n item.description !== undefined ||\n item.activeForm !== undefined ||\n item.status !== undefined ||\n item.owner !== undefined ||\n item.metadata !== undefined ||\n Boolean(item.addBlockedBy?.length) ||\n Boolean(item.removeBlockedBy?.length);\n if (!hasMutation) {\n return fail(\n 'nothing to change: provide at least one of subject, description, activeForm, status, owner, metadata, addBlockedBy, or removeBlockedBy',\n );\n }\n\n const current = context.tasks[at];\n\n // The Task Space overlay commits free text from an inline editor, so a blank\n // subject reaches the reducer as a real update rather than as a missing\n // field. Persisting it would leave an unidentifiable row.\n if (item.subject !== undefined && !item.subject.trim()) return fail('subject must not be blank');\n\n let newStatus = current.status;\n if (item.status !== undefined) {\n if (!isTransitionValid(current.status, item.status)) {\n return fail(`illegal transition ${current.status} -> ${item.status}`);\n }\n newStatus = item.status;\n }\n\n let newBlockedBy = current.blockedBy ? [...current.blockedBy] : [];\n for (const token of item.removeBlockedBy ?? []) {\n const resolved = resolveDep(context, 'removeBlockedBy', token);\n if (!resolved.ok) return fail(resolved.message);\n // Removing an id that is not there is a well-defined no-op, so unlike the\n // add path this deliberately skips the existence check.\n newBlockedBy = newBlockedBy.filter((dep) => dep !== resolved.id);\n }\n for (const token of item.addBlockedBy ?? []) {\n const resolved = resolveDep(context, 'addBlockedBy', token);\n if (!resolved.ok) return fail(resolved.message);\n if (resolved.id === id) return fail(`cannot block #${id} on itself`);\n const dep = context.tasks.find((task) => task.id === resolved.id);\n if (!dep) return fail(`addBlockedBy: #${resolved.id} not found`);\n if (dep.status === DELETED_STATUS) return fail(`addBlockedBy: #${resolved.id} is deleted`);\n if (!newBlockedBy.includes(resolved.id)) newBlockedBy.push(resolved.id);\n }\n\n const updated: Task = { ...current, status: newStatus };\n assignScalars(updated, item);\n if (newBlockedBy.length) updated.blockedBy = newBlockedBy;\n else delete updated.blockedBy;\n\n // The candidate is written before the check because `detectCycle` unions the\n // node's stored edges with the ones passed in: checking against the old array\n // would resurrect an edge `removeBlockedBy` just stripped and reject a\n // remove+add that legitimately breaks a cycle.\n const candidate = [...context.tasks];\n candidate[at] = updated;\n if (item.addBlockedBy?.length && detectCycle(candidate, id, newBlockedBy)) {\n return fail('addBlockedBy would create a cycle in the blockedBy graph');\n }\n\n const changed = taskChanged(current, updated);\n if (changed) updated.updatedAt = now;\n context.tasks = candidate;\n\n return {\n ok: true,\n outcome: changed\n ? { index, kind: 'updated', id, fromStatus: current.status, toStatus: newStatus }\n : { index, kind: 'unchanged', id, status: newStatus },\n };\n}\n\n/**\n * Apply every entry in request order, threading the document.\n *\n * Each entry lands completely or not at all: a create whose dependencies fail\n * is not kept dependency-free, because handing back an unblocked task the\n * caller asked to be blocked is a lie it may immediately act on.\n */\nfunction applyUpsert(document: TaskDocument, items: TaskItemMutation[], now: string): ApplyResult {\n const context: UpsertContext = {\n tasks: [...document.tasks],\n nextId: document.nextId,\n refIds: new Map(),\n failedRefs: new Map(),\n };\n const outcomes: UpsertItemOutcome[] = [];\n let applied = 0;\n\n for (const [index, item] of items.entries()) {\n const shapeError = checkShape(context, item);\n const result = shapeError\n ? fail(shapeError)\n : item.id === undefined\n ? applyCreateItem(context, item, index, now)\n : applyUpdateItem(context, item, index, now);\n\n if (result.ok) {\n outcomes.push(result.outcome);\n applied += 1;\n continue;\n }\n // A ref whose owner failed must not read as a typo to the entries that\n // depend on it, so the dead ref is recorded rather than left unknown.\n if (item.ref && REF_PATTERN.test(item.ref) && !context.refIds.has(item.ref)) {\n context.failedRefs.set(item.ref, index);\n }\n outcomes.push({\n index,\n kind: 'failed',\n message: result.message,\n ...(item.id === undefined ? {} : { id: item.id }),\n ...(item.ref ? { ref: item.ref } : {}),\n });\n }\n\n return {\n // Nothing applied means nothing to write: returning the original object by\n // reference keeps the caller's write-skip path identical to a hard error.\n document: applied > 0 ? withTasks(document, context.tasks, context.nextId) : document,\n op: { kind: 'upsert', items: outcomes, applied, failed: outcomes.length - applied },\n };\n}\n\n/**\n * Pure reducer: (document, action, params) -> (document, op).\n *\n * All validation is in-line: structural guards plus state-aware checks\n * (transition legality, dangling or deleted blockedBy, self-block, cycles,\n * in-flight delegations). The caller owns persistence and formatting.\n */\nexport function applyTaskMutation(\n document: TaskDocument,\n action: ReducerAction,\n params: TaskMutationParams,\n now: string = new Date().toISOString(),\n): ApplyResult {\n switch (action) {\n case 'upsert': {\n const items = params.tasks;\n if (!Array.isArray(items) || items.length === 0) {\n return errorResult(\n document,\n 'upsert requires a non-empty tasks array, e.g. {\"action\":\"upsert\",\"tasks\":[{\"subject\":\"Write tests\"}]}',\n );\n }\n if (items.length > MAX_UPSERT_ITEMS) {\n return errorResult(\n document,\n `upsert accepts at most ${MAX_UPSERT_ITEMS} entries per call (received ${items.length})`,\n );\n }\n return applyUpsert(document, items, now);\n }\n\n case 'list': {\n return {\n document,\n op: {\n kind: 'list',\n includeDeleted: params.includeDeleted === true,\n ...(params.status !== undefined ? { statusFilter: params.status } : {}),\n },\n };\n }\n\n case 'get': {\n if (params.id === undefined) return errorResult(document, 'id required for get');\n const task = document.tasks.find((candidate) => candidate.id === params.id);\n if (!task) return errorResult(document, `#${params.id} not found`);\n return { document, op: { kind: 'get', task } };\n }\n\n case 'delete': {\n if (params.id === undefined) return errorResult(document, 'id required for delete');\n const index = document.tasks.findIndex((task) => task.id === params.id);\n if (index === -1) return errorResult(document, `#${params.id} not found`);\n const current = document.tasks[index];\n if (current.status === DELETED_STATUS) return errorResult(document, `#${current.id} is already deleted`);\n if (isDelegationActive(current)) {\n return errorResult(document, `#${current.id} has a running delegation — cancel it before deleting`);\n }\n\n const tasks = [...document.tasks];\n tasks[index] = { ...current, status: DELETED_STATUS, updatedAt: now };\n return {\n document: withTasks(document, tasks),\n op: { kind: 'delete', id: current.id, subject: current.subject },\n };\n }\n\n case 'clear': {\n const active = document.tasks.filter(isDelegationActive);\n if (active.length > 0) {\n const ids = active.map((task) => `#${task.id}`).join(', ');\n return errorResult(document, `cannot clear while delegations are running (${ids}) — cancel them first`);\n }\n const count = document.tasks.length;\n return {\n document: withTasks(document, [], 1),\n op: { kind: 'clear', count },\n };\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=["Use `task` for work with 3+ steps, when the user gives you a list of tasks, or immediately after receiving new instructions to capture requirements. Skip it for single trivial tasks and purely conversational requests.",`Capture a whole plan in one call: {"action":"upsert","tasks":[{"subject":"Research existing tool"},{"subject":"Write the reducer"},{"subject":"Add tests"}]}. Batch whenever you are writing the plan down — the initial list, a revised plan, or wiring dependencies across several tasks — because one call keeps ids and blockers consistent and shows the user the whole plan at once.`,`Do not batch progress. Mark a task in_progress with its own call BEFORE beginning work, and completed with its own call IMMEDIATELY when it is done. upsert accepts many tasks, but a batch of completions sent at the end of a turn is still wrong: batching is for writing a plan down, single calls are for reporting progress, and the user watches the task list to see where you are. Exactly one task should be in_progress for your own work; delegated tasks run in parallel and do not count against that.`,`Never mark a task completed if tests are failing, the implementation is partial, or you hit unresolved errors, use status "failed" or keep it in_progress and add a new task for the blocker in the same upsert.`,`When every non-deleted task is completed, review the full task list once more and call task {"action":"clear"} to close it. Do not leave an all-completed list open.`,`Status is a state machine: pending → in_progress → completed, plus failed (recoverable, can go back to pending or in_progress) and deleted as a tombstone. Pass activeForm (present-continuous, e.g. 'writing tests') when marking in_progress.`,`One call does both: an entry with an id changes that task, an entry without an id creates one. To change status, upsert the task by id, e.g. {"action":"upsert","tasks":[{"id":3,"status":"completed"}]}. Never omit the id when you meant to change an existing task — an entry with no id creates a new one instead. An entry that carries an id and no other field is rejected.`,`Use blockedBy for dependencies (A is blocked by B). On an entry with no id pass blockedBy as the initial set; on an entry with an id use addBlockedBy / removeBlockedBy (additive merge, do not resend the full array). To depend on a task created earlier in the SAME call, give that earlier entry a "ref" and list the ref string in place of an id: [{"ref":"api","subject":"Design the API"},{"subject":"Implement the API","blockedBy":["api"]}]. A ref must be declared before it is used. Cycles are rejected.`,`upsert applies each entry independently: the entries that pass are committed and the failures are reported with their array index. A failed entry changed nothing, so resend only the corrected failures — never resend the whole call, because an already-applied entry with no id would create a duplicate task.`,`Before assigning work, call subagent with {"action":"agents"} and match each task to an exact discovered agent name. Prefer a discovered general-purpose write-capable agent such as delegate or worker when no specialist fits. Use a focused inlineAgent only for read-only work.`,`Assign every unblocked pending task with task {"action":"assign","id":3,"agent":"reviewer"}, including implementation tasks. Do not use a direct subagent run for work already represented in the shared task list. Use task assign so ownership and terminal results stay visible.`,`Hand over what you already know when you assign. Pass relevantFiles with the paths you have actually read or located for that task, and priorFindings with the facts you established: symbol names, call sites, the approach you picked, what you ruled out.
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=["Use `task` for work with 3+ steps, when the user gives you a list of tasks, or immediately after receiving new instructions to capture requirements. Skip it for single trivial tasks and purely conversational requests.",`Capture a whole plan in one call: {"action":"upsert","tasks":[{"subject":"Research existing tool"},{"subject":"Write the reducer"},{"subject":"Add tests"}]}. Batch whenever you are writing the plan down — the initial list, a revised plan, or wiring dependencies across several tasks — because one call keeps ids and blockers consistent and shows the user the whole plan at once.`,`Do not batch progress. Mark a task in_progress with its own call BEFORE beginning work, and completed with its own call IMMEDIATELY when it is done. upsert accepts many tasks, but a batch of completions sent at the end of a turn is still wrong: batching is for writing a plan down, single calls are for reporting progress, and the user watches the task list to see where you are. Exactly one task should be in_progress for your own work; delegated tasks run in parallel and do not count against that.`,`Never mark a task completed if tests are failing, the implementation is partial, or you hit unresolved errors, use status "failed" or keep it in_progress and add a new task for the blocker in the same upsert.`,`When every non-deleted task is completed, review the full task list once more and call task {"action":"clear"} to close it. Do not leave an all-completed list open.`,`Status is a state machine: pending → in_progress → completed, plus failed (recoverable, can go back to pending or in_progress) and deleted as a tombstone. Pass activeForm (present-continuous, e.g. 'writing tests') when marking in_progress.`,`One call does both: an entry with an id changes that task, an entry without an id creates one. To change status, upsert the task by id, e.g. {"action":"upsert","tasks":[{"id":3,"status":"completed"}]}. Never omit the id when you meant to change an existing task — an entry with no id creates a new one instead. An entry that carries an id and no other field is rejected.`,`Use blockedBy for dependencies (A is blocked by B). On an entry with no id pass blockedBy as the initial set; on an entry with an id use addBlockedBy / removeBlockedBy (additive merge, do not resend the full array). To depend on a task created earlier in the SAME call, give that earlier entry a "ref" and list the ref string in place of an id: [{"ref":"api","subject":"Design the API"},{"subject":"Implement the API","blockedBy":["api"]}]. A ref must be declared before it is used. Cycles are rejected.`,`upsert applies each entry independently: the entries that pass are committed and the failures are reported with their array index. A failed entry changed nothing, so resend only the corrected failures — never resend the whole call, because an already-applied entry with no id would create a duplicate task.`,`Before assigning work, call subagent with {"action":"agents"} and match each task to an exact discovered agent name. Prefer a discovered general-purpose write-capable agent such as delegate or worker when no specialist fits. Use a focused inlineAgent only for read-only work.`,`Assign every unblocked pending task with task {"action":"assign","id":3,"agent":"reviewer"}, including implementation tasks. Do not use a direct subagent run for work already represented in the shared task list. Use task assign so ownership and terminal results stay visible.`,`Hand over what you already know when you assign. Pass relevantFiles with the paths you have actually read or located for that task, and priorFindings with the facts you established: symbol names, call sites, the approach you picked, what you ruled out. A fresh-context child starts with none of your context and will otherwise spend most of its run re-reading the repo. It treats this verified pack as satisfying initial context exploration, reads known paths directly, and widens discovery only for a concrete missing dependency. Send at most a dozen files and only ones you really opened — a guessed path costs the child more than an omitted one — and keep priorFindings to a few lines of facts, not directives.`,`After assigning work, continue non-overlapping parent work or end the turn and wait. Do not poll or duplicate delegated scope.`,`When a delegated task completes and unblocks another task, repeat agent discovery if needed and assign that newly eligible task promptly. Do not poll in a loop. Completion notifications wake the parent session.`,`If a child asks for a decision through intercom, or an assignment fails to start for another concrete issue, respond, rescope, or retry explicitly. Do not silently leave the task pending.`,`assign refuses tasks that are blocked, already delegated, or completed. Resolve blockers first. Use {"action":"cancel","id":3} to stop a running delegation; the task returns to pending.`,`Doom Task records delegated lifecycle and results on the shared task. The child does not update task status directly. Read the completion or failure result before deciding what to do next.`,`The task list is scoped to this session tree: delegated subagents share it, while unrelated sessions start empty. It persists for later inspection until retention cleanup.`,`Subject must be short and imperative (e.g. 'Research existing tool'); description is long-form detail written when you plan the task, and the delegated brief is that description plus the assign-time instructions, relevantFiles and priorFindings. Put durable scope in description and what you learned while exploring in the assign-time pack — do not repeat one in the other.`];exports.DEFAULT_PROMPT_GUIDELINES=e;
|
|
2
2
|
//# sourceMappingURL=promptGuidelines.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"promptGuidelines.cjs","names":[],"sources":["../../src/tool/promptGuidelines.ts"],"sourcesContent":["export const DEFAULT_PROMPT_GUIDELINES: string[] = [\n 'Use `task` for work with 3+ steps, when the user gives you a list of tasks, or immediately after receiving new instructions to capture requirements. Skip it for single trivial tasks and purely conversational requests.',\n 'Capture a whole plan in one call: {\"action\":\"upsert\",\"tasks\":[{\"subject\":\"Research existing tool\"},{\"subject\":\"Write the reducer\"},{\"subject\":\"Add tests\"}]}. Batch whenever you are writing the plan down — the initial list, a revised plan, or wiring dependencies across several tasks — because one call keeps ids and blockers consistent and shows the user the whole plan at once.',\n 'Do not batch progress. Mark a task in_progress with its own call BEFORE beginning work, and completed with its own call IMMEDIATELY when it is done. upsert accepts many tasks, but a batch of completions sent at the end of a turn is still wrong: batching is for writing a plan down, single calls are for reporting progress, and the user watches the task list to see where you are. Exactly one task should be in_progress for your own work; delegated tasks run in parallel and do not count against that.',\n 'Never mark a task completed if tests are failing, the implementation is partial, or you hit unresolved errors, use status \"failed\" or keep it in_progress and add a new task for the blocker in the same upsert.',\n 'When every non-deleted task is completed, review the full task list once more and call task {\"action\":\"clear\"} to close it. Do not leave an all-completed list open.',\n \"Status is a state machine: pending → in_progress → completed, plus failed (recoverable, can go back to pending or in_progress) and deleted as a tombstone. Pass activeForm (present-continuous, e.g. 'writing tests') when marking in_progress.\",\n 'One call does both: an entry with an id changes that task, an entry without an id creates one. To change status, upsert the task by id, e.g. {\"action\":\"upsert\",\"tasks\":[{\"id\":3,\"status\":\"completed\"}]}. Never omit the id when you meant to change an existing task — an entry with no id creates a new one instead. An entry that carries an id and no other field is rejected.',\n 'Use blockedBy for dependencies (A is blocked by B). On an entry with no id pass blockedBy as the initial set; on an entry with an id use addBlockedBy / removeBlockedBy (additive merge, do not resend the full array). To depend on a task created earlier in the SAME call, give that earlier entry a \"ref\" and list the ref string in place of an id: [{\"ref\":\"api\",\"subject\":\"Design the API\"},{\"subject\":\"Implement the API\",\"blockedBy\":[\"api\"]}]. A ref must be declared before it is used. Cycles are rejected.',\n 'upsert applies each entry independently: the entries that pass are committed and the failures are reported with their array index. A failed entry changed nothing, so resend only the corrected failures — never resend the whole call, because an already-applied entry with no id would create a duplicate task.',\n 'Before assigning work, call subagent with {\"action\":\"agents\"} and match each task to an exact discovered agent name. Prefer a discovered general-purpose write-capable agent such as delegate or worker when no specialist fits. Use a focused inlineAgent only for read-only work.',\n 'Assign every unblocked pending task with task {\"action\":\"assign\",\"id\":3,\"agent\":\"reviewer\"}, including implementation tasks. Do not use a direct subagent run for work already represented in the shared task list. Use task assign so ownership and terminal results stay visible.',\n 'Hand over what you already know when you assign. Pass relevantFiles with the paths you have actually read or located for that task, and priorFindings with the facts you established: symbol names, call sites, the approach you picked, what you ruled out.
|
|
1
|
+
{"version":3,"file":"promptGuidelines.cjs","names":[],"sources":["../../src/tool/promptGuidelines.ts"],"sourcesContent":["export const DEFAULT_PROMPT_GUIDELINES: string[] = [\n 'Use `task` for work with 3+ steps, when the user gives you a list of tasks, or immediately after receiving new instructions to capture requirements. Skip it for single trivial tasks and purely conversational requests.',\n 'Capture a whole plan in one call: {\"action\":\"upsert\",\"tasks\":[{\"subject\":\"Research existing tool\"},{\"subject\":\"Write the reducer\"},{\"subject\":\"Add tests\"}]}. Batch whenever you are writing the plan down — the initial list, a revised plan, or wiring dependencies across several tasks — because one call keeps ids and blockers consistent and shows the user the whole plan at once.',\n 'Do not batch progress. Mark a task in_progress with its own call BEFORE beginning work, and completed with its own call IMMEDIATELY when it is done. upsert accepts many tasks, but a batch of completions sent at the end of a turn is still wrong: batching is for writing a plan down, single calls are for reporting progress, and the user watches the task list to see where you are. Exactly one task should be in_progress for your own work; delegated tasks run in parallel and do not count against that.',\n 'Never mark a task completed if tests are failing, the implementation is partial, or you hit unresolved errors, use status \"failed\" or keep it in_progress and add a new task for the blocker in the same upsert.',\n 'When every non-deleted task is completed, review the full task list once more and call task {\"action\":\"clear\"} to close it. Do not leave an all-completed list open.',\n \"Status is a state machine: pending → in_progress → completed, plus failed (recoverable, can go back to pending or in_progress) and deleted as a tombstone. Pass activeForm (present-continuous, e.g. 'writing tests') when marking in_progress.\",\n 'One call does both: an entry with an id changes that task, an entry without an id creates one. To change status, upsert the task by id, e.g. {\"action\":\"upsert\",\"tasks\":[{\"id\":3,\"status\":\"completed\"}]}. Never omit the id when you meant to change an existing task — an entry with no id creates a new one instead. An entry that carries an id and no other field is rejected.',\n 'Use blockedBy for dependencies (A is blocked by B). On an entry with no id pass blockedBy as the initial set; on an entry with an id use addBlockedBy / removeBlockedBy (additive merge, do not resend the full array). To depend on a task created earlier in the SAME call, give that earlier entry a \"ref\" and list the ref string in place of an id: [{\"ref\":\"api\",\"subject\":\"Design the API\"},{\"subject\":\"Implement the API\",\"blockedBy\":[\"api\"]}]. A ref must be declared before it is used. Cycles are rejected.',\n 'upsert applies each entry independently: the entries that pass are committed and the failures are reported with their array index. A failed entry changed nothing, so resend only the corrected failures — never resend the whole call, because an already-applied entry with no id would create a duplicate task.',\n 'Before assigning work, call subagent with {\"action\":\"agents\"} and match each task to an exact discovered agent name. Prefer a discovered general-purpose write-capable agent such as delegate or worker when no specialist fits. Use a focused inlineAgent only for read-only work.',\n 'Assign every unblocked pending task with task {\"action\":\"assign\",\"id\":3,\"agent\":\"reviewer\"}, including implementation tasks. Do not use a direct subagent run for work already represented in the shared task list. Use task assign so ownership and terminal results stay visible.',\n 'Hand over what you already know when you assign. Pass relevantFiles with the paths you have actually read or located for that task, and priorFindings with the facts you established: symbol names, call sites, the approach you picked, what you ruled out. A fresh-context child starts with none of your context and will otherwise spend most of its run re-reading the repo. It treats this verified pack as satisfying initial context exploration, reads known paths directly, and widens discovery only for a concrete missing dependency. Send at most a dozen files and only ones you really opened — a guessed path costs the child more than an omitted one — and keep priorFindings to a few lines of facts, not directives.',\n 'After assigning work, continue non-overlapping parent work or end the turn and wait. Do not poll or duplicate delegated scope.',\n 'When a delegated task completes and unblocks another task, repeat agent discovery if needed and assign that newly eligible task promptly. Do not poll in a loop. Completion notifications wake the parent session.',\n 'If a child asks for a decision through intercom, or an assignment fails to start for another concrete issue, respond, rescope, or retry explicitly. Do not silently leave the task pending.',\n 'assign refuses tasks that are blocked, already delegated, or completed. Resolve blockers first. Use {\"action\":\"cancel\",\"id\":3} to stop a running delegation; the task returns to pending.',\n 'Doom Task records delegated lifecycle and results on the shared task. The child does not update task status directly. Read the completion or failure result before deciding what to do next.',\n 'The task list is scoped to this session tree: delegated subagents share it, while unrelated sessions start empty. It persists for later inspection until retention cleanup.',\n \"Subject must be short and imperative (e.g. 'Research existing tool'); description is long-form detail written when you plan the task, and the delegated brief is that description plus the assign-time instructions, relevantFiles and priorFindings. Put durable scope in description and what you learned while exploring in the assign-time pack — do not repeat one in the other.\",\n];\n"],"mappings":"mEAAA,MAAa,EAAsC,CACjD,4NACA,6XACA,ufACA,mNACA,uKACA,kPACA,qXACA,0fACA,qTACA,sRACA,sRACA,4sBACA,iIACA,qNACA,8LACA,4LACA,+LACA,8KACA,uXACF"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=["Use `task` for work with 3+ steps, when the user gives you a list of tasks, or immediately after receiving new instructions to capture requirements. Skip it for single trivial tasks and purely conversational requests.",`Capture a whole plan in one call: {"action":"upsert","tasks":[{"subject":"Research existing tool"},{"subject":"Write the reducer"},{"subject":"Add tests"}]}. Batch whenever you are writing the plan down — the initial list, a revised plan, or wiring dependencies across several tasks — because one call keeps ids and blockers consistent and shows the user the whole plan at once.`,`Do not batch progress. Mark a task in_progress with its own call BEFORE beginning work, and completed with its own call IMMEDIATELY when it is done. upsert accepts many tasks, but a batch of completions sent at the end of a turn is still wrong: batching is for writing a plan down, single calls are for reporting progress, and the user watches the task list to see where you are. Exactly one task should be in_progress for your own work; delegated tasks run in parallel and do not count against that.`,`Never mark a task completed if tests are failing, the implementation is partial, or you hit unresolved errors, use status "failed" or keep it in_progress and add a new task for the blocker in the same upsert.`,`When every non-deleted task is completed, review the full task list once more and call task {"action":"clear"} to close it. Do not leave an all-completed list open.`,`Status is a state machine: pending → in_progress → completed, plus failed (recoverable, can go back to pending or in_progress) and deleted as a tombstone. Pass activeForm (present-continuous, e.g. 'writing tests') when marking in_progress.`,`One call does both: an entry with an id changes that task, an entry without an id creates one. To change status, upsert the task by id, e.g. {"action":"upsert","tasks":[{"id":3,"status":"completed"}]}. Never omit the id when you meant to change an existing task — an entry with no id creates a new one instead. An entry that carries an id and no other field is rejected.`,`Use blockedBy for dependencies (A is blocked by B). On an entry with no id pass blockedBy as the initial set; on an entry with an id use addBlockedBy / removeBlockedBy (additive merge, do not resend the full array). To depend on a task created earlier in the SAME call, give that earlier entry a "ref" and list the ref string in place of an id: [{"ref":"api","subject":"Design the API"},{"subject":"Implement the API","blockedBy":["api"]}]. A ref must be declared before it is used. Cycles are rejected.`,`upsert applies each entry independently: the entries that pass are committed and the failures are reported with their array index. A failed entry changed nothing, so resend only the corrected failures — never resend the whole call, because an already-applied entry with no id would create a duplicate task.`,`Before assigning work, call subagent with {"action":"agents"} and match each task to an exact discovered agent name. Prefer a discovered general-purpose write-capable agent such as delegate or worker when no specialist fits. Use a focused inlineAgent only for read-only work.`,`Assign every unblocked pending task with task {"action":"assign","id":3,"agent":"reviewer"}, including implementation tasks. Do not use a direct subagent run for work already represented in the shared task list. Use task assign so ownership and terminal results stay visible.`,`Hand over what you already know when you assign. Pass relevantFiles with the paths you have actually read or located for that task, and priorFindings with the facts you established: symbol names, call sites, the approach you picked, what you ruled out.
|
|
1
|
+
const e=["Use `task` for work with 3+ steps, when the user gives you a list of tasks, or immediately after receiving new instructions to capture requirements. Skip it for single trivial tasks and purely conversational requests.",`Capture a whole plan in one call: {"action":"upsert","tasks":[{"subject":"Research existing tool"},{"subject":"Write the reducer"},{"subject":"Add tests"}]}. Batch whenever you are writing the plan down — the initial list, a revised plan, or wiring dependencies across several tasks — because one call keeps ids and blockers consistent and shows the user the whole plan at once.`,`Do not batch progress. Mark a task in_progress with its own call BEFORE beginning work, and completed with its own call IMMEDIATELY when it is done. upsert accepts many tasks, but a batch of completions sent at the end of a turn is still wrong: batching is for writing a plan down, single calls are for reporting progress, and the user watches the task list to see where you are. Exactly one task should be in_progress for your own work; delegated tasks run in parallel and do not count against that.`,`Never mark a task completed if tests are failing, the implementation is partial, or you hit unresolved errors, use status "failed" or keep it in_progress and add a new task for the blocker in the same upsert.`,`When every non-deleted task is completed, review the full task list once more and call task {"action":"clear"} to close it. Do not leave an all-completed list open.`,`Status is a state machine: pending → in_progress → completed, plus failed (recoverable, can go back to pending or in_progress) and deleted as a tombstone. Pass activeForm (present-continuous, e.g. 'writing tests') when marking in_progress.`,`One call does both: an entry with an id changes that task, an entry without an id creates one. To change status, upsert the task by id, e.g. {"action":"upsert","tasks":[{"id":3,"status":"completed"}]}. Never omit the id when you meant to change an existing task — an entry with no id creates a new one instead. An entry that carries an id and no other field is rejected.`,`Use blockedBy for dependencies (A is blocked by B). On an entry with no id pass blockedBy as the initial set; on an entry with an id use addBlockedBy / removeBlockedBy (additive merge, do not resend the full array). To depend on a task created earlier in the SAME call, give that earlier entry a "ref" and list the ref string in place of an id: [{"ref":"api","subject":"Design the API"},{"subject":"Implement the API","blockedBy":["api"]}]. A ref must be declared before it is used. Cycles are rejected.`,`upsert applies each entry independently: the entries that pass are committed and the failures are reported with their array index. A failed entry changed nothing, so resend only the corrected failures — never resend the whole call, because an already-applied entry with no id would create a duplicate task.`,`Before assigning work, call subagent with {"action":"agents"} and match each task to an exact discovered agent name. Prefer a discovered general-purpose write-capable agent such as delegate or worker when no specialist fits. Use a focused inlineAgent only for read-only work.`,`Assign every unblocked pending task with task {"action":"assign","id":3,"agent":"reviewer"}, including implementation tasks. Do not use a direct subagent run for work already represented in the shared task list. Use task assign so ownership and terminal results stay visible.`,`Hand over what you already know when you assign. Pass relevantFiles with the paths you have actually read or located for that task, and priorFindings with the facts you established: symbol names, call sites, the approach you picked, what you ruled out. A fresh-context child starts with none of your context and will otherwise spend most of its run re-reading the repo. It treats this verified pack as satisfying initial context exploration, reads known paths directly, and widens discovery only for a concrete missing dependency. Send at most a dozen files and only ones you really opened — a guessed path costs the child more than an omitted one — and keep priorFindings to a few lines of facts, not directives.`,`After assigning work, continue non-overlapping parent work or end the turn and wait. Do not poll or duplicate delegated scope.`,`When a delegated task completes and unblocks another task, repeat agent discovery if needed and assign that newly eligible task promptly. Do not poll in a loop. Completion notifications wake the parent session.`,`If a child asks for a decision through intercom, or an assignment fails to start for another concrete issue, respond, rescope, or retry explicitly. Do not silently leave the task pending.`,`assign refuses tasks that are blocked, already delegated, or completed. Resolve blockers first. Use {"action":"cancel","id":3} to stop a running delegation; the task returns to pending.`,`Doom Task records delegated lifecycle and results on the shared task. The child does not update task status directly. Read the completion or failure result before deciding what to do next.`,`The task list is scoped to this session tree: delegated subagents share it, while unrelated sessions start empty. It persists for later inspection until retention cleanup.`,`Subject must be short and imperative (e.g. 'Research existing tool'); description is long-form detail written when you plan the task, and the delegated brief is that description plus the assign-time instructions, relevantFiles and priorFindings. Put durable scope in description and what you learned while exploring in the assign-time pack — do not repeat one in the other.`];export{e as DEFAULT_PROMPT_GUIDELINES};
|
|
2
2
|
//# sourceMappingURL=promptGuidelines.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"promptGuidelines.mjs","names":[],"sources":["../../src/tool/promptGuidelines.ts"],"sourcesContent":["export const DEFAULT_PROMPT_GUIDELINES: string[] = [\n 'Use `task` for work with 3+ steps, when the user gives you a list of tasks, or immediately after receiving new instructions to capture requirements. Skip it for single trivial tasks and purely conversational requests.',\n 'Capture a whole plan in one call: {\"action\":\"upsert\",\"tasks\":[{\"subject\":\"Research existing tool\"},{\"subject\":\"Write the reducer\"},{\"subject\":\"Add tests\"}]}. Batch whenever you are writing the plan down — the initial list, a revised plan, or wiring dependencies across several tasks — because one call keeps ids and blockers consistent and shows the user the whole plan at once.',\n 'Do not batch progress. Mark a task in_progress with its own call BEFORE beginning work, and completed with its own call IMMEDIATELY when it is done. upsert accepts many tasks, but a batch of completions sent at the end of a turn is still wrong: batching is for writing a plan down, single calls are for reporting progress, and the user watches the task list to see where you are. Exactly one task should be in_progress for your own work; delegated tasks run in parallel and do not count against that.',\n 'Never mark a task completed if tests are failing, the implementation is partial, or you hit unresolved errors, use status \"failed\" or keep it in_progress and add a new task for the blocker in the same upsert.',\n 'When every non-deleted task is completed, review the full task list once more and call task {\"action\":\"clear\"} to close it. Do not leave an all-completed list open.',\n \"Status is a state machine: pending → in_progress → completed, plus failed (recoverable, can go back to pending or in_progress) and deleted as a tombstone. Pass activeForm (present-continuous, e.g. 'writing tests') when marking in_progress.\",\n 'One call does both: an entry with an id changes that task, an entry without an id creates one. To change status, upsert the task by id, e.g. {\"action\":\"upsert\",\"tasks\":[{\"id\":3,\"status\":\"completed\"}]}. Never omit the id when you meant to change an existing task — an entry with no id creates a new one instead. An entry that carries an id and no other field is rejected.',\n 'Use blockedBy for dependencies (A is blocked by B). On an entry with no id pass blockedBy as the initial set; on an entry with an id use addBlockedBy / removeBlockedBy (additive merge, do not resend the full array). To depend on a task created earlier in the SAME call, give that earlier entry a \"ref\" and list the ref string in place of an id: [{\"ref\":\"api\",\"subject\":\"Design the API\"},{\"subject\":\"Implement the API\",\"blockedBy\":[\"api\"]}]. A ref must be declared before it is used. Cycles are rejected.',\n 'upsert applies each entry independently: the entries that pass are committed and the failures are reported with their array index. A failed entry changed nothing, so resend only the corrected failures — never resend the whole call, because an already-applied entry with no id would create a duplicate task.',\n 'Before assigning work, call subagent with {\"action\":\"agents\"} and match each task to an exact discovered agent name. Prefer a discovered general-purpose write-capable agent such as delegate or worker when no specialist fits. Use a focused inlineAgent only for read-only work.',\n 'Assign every unblocked pending task with task {\"action\":\"assign\",\"id\":3,\"agent\":\"reviewer\"}, including implementation tasks. Do not use a direct subagent run for work already represented in the shared task list. Use task assign so ownership and terminal results stay visible.',\n 'Hand over what you already know when you assign. Pass relevantFiles with the paths you have actually read or located for that task, and priorFindings with the facts you established: symbol names, call sites, the approach you picked, what you ruled out.
|
|
1
|
+
{"version":3,"file":"promptGuidelines.mjs","names":[],"sources":["../../src/tool/promptGuidelines.ts"],"sourcesContent":["export const DEFAULT_PROMPT_GUIDELINES: string[] = [\n 'Use `task` for work with 3+ steps, when the user gives you a list of tasks, or immediately after receiving new instructions to capture requirements. Skip it for single trivial tasks and purely conversational requests.',\n 'Capture a whole plan in one call: {\"action\":\"upsert\",\"tasks\":[{\"subject\":\"Research existing tool\"},{\"subject\":\"Write the reducer\"},{\"subject\":\"Add tests\"}]}. Batch whenever you are writing the plan down — the initial list, a revised plan, or wiring dependencies across several tasks — because one call keeps ids and blockers consistent and shows the user the whole plan at once.',\n 'Do not batch progress. Mark a task in_progress with its own call BEFORE beginning work, and completed with its own call IMMEDIATELY when it is done. upsert accepts many tasks, but a batch of completions sent at the end of a turn is still wrong: batching is for writing a plan down, single calls are for reporting progress, and the user watches the task list to see where you are. Exactly one task should be in_progress for your own work; delegated tasks run in parallel and do not count against that.',\n 'Never mark a task completed if tests are failing, the implementation is partial, or you hit unresolved errors, use status \"failed\" or keep it in_progress and add a new task for the blocker in the same upsert.',\n 'When every non-deleted task is completed, review the full task list once more and call task {\"action\":\"clear\"} to close it. Do not leave an all-completed list open.',\n \"Status is a state machine: pending → in_progress → completed, plus failed (recoverable, can go back to pending or in_progress) and deleted as a tombstone. Pass activeForm (present-continuous, e.g. 'writing tests') when marking in_progress.\",\n 'One call does both: an entry with an id changes that task, an entry without an id creates one. To change status, upsert the task by id, e.g. {\"action\":\"upsert\",\"tasks\":[{\"id\":3,\"status\":\"completed\"}]}. Never omit the id when you meant to change an existing task — an entry with no id creates a new one instead. An entry that carries an id and no other field is rejected.',\n 'Use blockedBy for dependencies (A is blocked by B). On an entry with no id pass blockedBy as the initial set; on an entry with an id use addBlockedBy / removeBlockedBy (additive merge, do not resend the full array). To depend on a task created earlier in the SAME call, give that earlier entry a \"ref\" and list the ref string in place of an id: [{\"ref\":\"api\",\"subject\":\"Design the API\"},{\"subject\":\"Implement the API\",\"blockedBy\":[\"api\"]}]. A ref must be declared before it is used. Cycles are rejected.',\n 'upsert applies each entry independently: the entries that pass are committed and the failures are reported with their array index. A failed entry changed nothing, so resend only the corrected failures — never resend the whole call, because an already-applied entry with no id would create a duplicate task.',\n 'Before assigning work, call subagent with {\"action\":\"agents\"} and match each task to an exact discovered agent name. Prefer a discovered general-purpose write-capable agent such as delegate or worker when no specialist fits. Use a focused inlineAgent only for read-only work.',\n 'Assign every unblocked pending task with task {\"action\":\"assign\",\"id\":3,\"agent\":\"reviewer\"}, including implementation tasks. Do not use a direct subagent run for work already represented in the shared task list. Use task assign so ownership and terminal results stay visible.',\n 'Hand over what you already know when you assign. Pass relevantFiles with the paths you have actually read or located for that task, and priorFindings with the facts you established: symbol names, call sites, the approach you picked, what you ruled out. A fresh-context child starts with none of your context and will otherwise spend most of its run re-reading the repo. It treats this verified pack as satisfying initial context exploration, reads known paths directly, and widens discovery only for a concrete missing dependency. Send at most a dozen files and only ones you really opened — a guessed path costs the child more than an omitted one — and keep priorFindings to a few lines of facts, not directives.',\n 'After assigning work, continue non-overlapping parent work or end the turn and wait. Do not poll or duplicate delegated scope.',\n 'When a delegated task completes and unblocks another task, repeat agent discovery if needed and assign that newly eligible task promptly. Do not poll in a loop. Completion notifications wake the parent session.',\n 'If a child asks for a decision through intercom, or an assignment fails to start for another concrete issue, respond, rescope, or retry explicitly. Do not silently leave the task pending.',\n 'assign refuses tasks that are blocked, already delegated, or completed. Resolve blockers first. Use {\"action\":\"cancel\",\"id\":3} to stop a running delegation; the task returns to pending.',\n 'Doom Task records delegated lifecycle and results on the shared task. The child does not update task status directly. Read the completion or failure result before deciding what to do next.',\n 'The task list is scoped to this session tree: delegated subagents share it, while unrelated sessions start empty. It persists for later inspection until retention cleanup.',\n \"Subject must be short and imperative (e.g. 'Research existing tool'); description is long-form detail written when you plan the task, and the delegated brief is that description plus the assign-time instructions, relevantFiles and priorFindings. Put durable scope in description and what you learned while exploring in the assign-time pack — do not repeat one in the other.\",\n];\n"],"mappings":"AAAA,MAAa,EAAsC,CACjD,4NACA,6XACA,ufACA,mNACA,uKACA,kPACA,qXACA,0fACA,qTACA,sRACA,sRACA,4sBACA,iIACA,qNACA,8LACA,4LACA,+LACA,8KACA,uXACF"}
|
package/dist/tui/selectors.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});function e(e){return e.filter(e=>e.status!==`deleted`)}function t(e){let t=new Map;for(let n of e)t.has(n.id)||t.set(n.id,n.status);return t}function n(e,t){return e.blockedBy?.some(e=>{let n=t.get(e);return n!==void 0&&n!==`completed`&&n!==`deleted`})??!1}function r(e){let r=t(e),i=[],a={total:0,completed:0,inProgress:0,pending:0,failed:0,blocked:0},o={inProgress:[],blocked:[],pending:[],completed:[],failed:[]},s=!1;for(let t of e)if(t.status!==`deleted`)switch(i.push(t),a.total+=1,s||=!!t.blockedBy?.length,t.status){case`in_progress`:a.inProgress+=1,o.inProgress.push(t);break;case`pending`:a.pending+=1,n(t,r)?(a.blocked+=1,o.blocked.push(t)):o.pending.push(t);break;case`failed`:a.failed+=1,o.failed.push(t);break;case`completed`:a.completed+=1,o.completed.push(t)}return{visible:i,counts:a,groups:o,showIds:s,hasActiveWork:a.inProgress>0}}function i(e){return r(e).counts}function a(e){return r(e).groups}function o(e){return e.some(e=>e.status!==`deleted`&&!!e.blockedBy?.length)}function s(e){return e.some(e=>e.status===`in_progress`)}function c(e,t){let n=[...e.inProgress,...e.failed,...e.blocked,...e.pending],r=[...n,...e.completed];if(t<=0)return{visible:[],hiddenCompleted:e.completed.length,truncatedTail:n.length};if(r.length<=t)return{visible:r,hiddenCompleted:0,truncatedTail:0};if(n.length<=t){let r=t-n.length,i=e.completed.slice(0,r);return{visible:[...n,...i],hiddenCompleted:e.completed.length-i.length,truncatedTail:0}}return{visible:n.slice(0,t),hiddenCompleted:e.completed.length,truncatedTail:n.length-t}}function l(e,t){return c(e.groups,t)}function u(e,t){return l(r(e),t)}exports.countTasks=i,exports.deriveTaskProjection=r,exports.groupTasks=a,exports.hasActiveWork=s,exports.selectOverlayLayout=u,exports.selectOverlayLayoutFromProjection=l,exports.shouldShowIds=o,exports.visibleTasks=e;
|
|
2
2
|
//# sourceMappingURL=selectors.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"selectors.cjs","names":[
|
|
1
|
+
{"version":3,"file":"selectors.cjs","names":[],"sources":["../../src/tui/selectors.ts"],"sourcesContent":["import type { Task } from '../store/types.ts';\n\nexport interface TaskCounts {\n total: number;\n completed: number;\n inProgress: number;\n pending: number;\n failed: number;\n blocked: number;\n}\n\nexport interface TaskGroups {\n inProgress: Task[];\n blocked: Task[];\n pending: Task[];\n completed: Task[];\n failed: Task[];\n}\n\nexport interface TaskProjection {\n visible: Task[];\n counts: TaskCounts;\n groups: TaskGroups;\n showIds: boolean;\n hasActiveWork: boolean;\n}\n\nexport function visibleTasks(tasks: readonly Task[]): Task[] {\n return tasks.filter((task) => task.status !== 'deleted');\n}\n\nfunction indexStatuses(tasks: readonly Task[]): Map<number, Task['status']> {\n const statuses = new Map<number, Task['status']>();\n for (const task of tasks) {\n // Valid stores have unique ids. Keeping the first entry also preserves the\n // former `Array.find` behavior if a corrupt document contains duplicates.\n if (!statuses.has(task.id)) statuses.set(task.id, task.status);\n }\n return statuses;\n}\n\nfunction hasUnresolvedBlocker(task: Task, statuses: ReadonlyMap<number, Task['status']>): boolean {\n return (\n task.blockedBy?.some((id) => {\n const status = statuses.get(id);\n return status !== undefined && status !== 'completed' && status !== 'deleted';\n }) ?? false\n );\n}\n\n/**\n * Derive all overlay-facing state in O(tasks + dependency edges).\n *\n * The id index replaces one full task-list scan per dependency, while the\n * ordered pass keeps group ordering identical to the source document.\n */\nexport function deriveTaskProjection(tasks: readonly Task[]): TaskProjection {\n const statuses = indexStatuses(tasks);\n const visible: Task[] = [];\n const counts: TaskCounts = { total: 0, completed: 0, inProgress: 0, pending: 0, failed: 0, blocked: 0 };\n const groups: TaskGroups = { inProgress: [], blocked: [], pending: [], completed: [], failed: [] };\n let showIds = false;\n\n for (const task of tasks) {\n if (task.status === 'deleted') continue;\n\n visible.push(task);\n counts.total += 1;\n showIds ||= Boolean(task.blockedBy?.length);\n\n switch (task.status) {\n case 'in_progress':\n counts.inProgress += 1;\n groups.inProgress.push(task);\n break;\n case 'pending': {\n counts.pending += 1;\n if (hasUnresolvedBlocker(task, statuses)) {\n counts.blocked += 1;\n groups.blocked.push(task);\n } else {\n groups.pending.push(task);\n }\n break;\n }\n case 'failed':\n counts.failed += 1;\n groups.failed.push(task);\n break;\n case 'completed':\n counts.completed += 1;\n groups.completed.push(task);\n break;\n }\n }\n\n return { visible, counts, groups, showIds, hasActiveWork: counts.inProgress > 0 };\n}\n\nexport function countTasks(tasks: readonly Task[]): TaskCounts {\n return deriveTaskProjection(tasks).counts;\n}\n\n/**\n * Group tasks for display.\n *\n * Blocked tasks split out of `pending` because a reader scanning the overlay\n * needs to know at a glance what is actually actionable right now.\n */\nexport function groupTasks(tasks: readonly Task[]): TaskGroups {\n return deriveTaskProjection(tasks).groups;\n}\n\n/** Ids are only worth the visual noise once dependencies are in play. */\nexport function shouldShowIds(tasks: readonly Task[]): boolean {\n return tasks.some((task) => task.status !== 'deleted' && Boolean(task.blockedBy?.length));\n}\n\nexport function hasActiveWork(tasks: readonly Task[]): boolean {\n return tasks.some((task) => task.status === 'in_progress');\n}\n\nexport interface OverlayLayout {\n visible: Task[];\n hiddenCompleted: number;\n truncatedTail: number;\n}\n\n/**\n * Choose which rows fit the overlay budget.\n *\n * Active work wins the space: in-progress first, then blocked and pending, and\n * completed rows are dropped first when the budget runs short since they carry\n * the least information about what happens next.\n */\nfunction selectLayout(groups: TaskGroups, budget: number): OverlayLayout {\n const active = [...groups.inProgress, ...groups.failed, ...groups.blocked, ...groups.pending];\n const ordered = [...active, ...groups.completed];\n if (budget <= 0) {\n return {\n visible: [],\n hiddenCompleted: groups.completed.length,\n truncatedTail: active.length,\n };\n }\n if (ordered.length <= budget) {\n return { visible: ordered, hiddenCompleted: 0, truncatedTail: 0 };\n }\n\n if (active.length <= budget) {\n const remaining = budget - active.length;\n const shownCompleted = groups.completed.slice(0, remaining);\n return {\n visible: [...active, ...shownCompleted],\n hiddenCompleted: groups.completed.length - shownCompleted.length,\n truncatedTail: 0,\n };\n }\n\n return {\n visible: active.slice(0, budget),\n hiddenCompleted: groups.completed.length,\n truncatedTail: active.length - budget,\n };\n}\n\nexport function selectOverlayLayoutFromProjection(projection: TaskProjection, budget: number): OverlayLayout {\n return selectLayout(projection.groups, budget);\n}\n\nexport function selectOverlayLayout(tasks: readonly Task[], budget: number): OverlayLayout {\n return selectOverlayLayoutFromProjection(deriveTaskProjection(tasks), budget);\n}\n"],"mappings":"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"}
|
package/dist/tui/selectors.d.cts
CHANGED
|
@@ -15,7 +15,21 @@ interface TaskGroups {
|
|
|
15
15
|
completed: Task[];
|
|
16
16
|
failed: Task[];
|
|
17
17
|
}
|
|
18
|
+
interface TaskProjection {
|
|
19
|
+
visible: Task[];
|
|
20
|
+
counts: TaskCounts;
|
|
21
|
+
groups: TaskGroups;
|
|
22
|
+
showIds: boolean;
|
|
23
|
+
hasActiveWork: boolean;
|
|
24
|
+
}
|
|
18
25
|
declare function visibleTasks(tasks: readonly Task[]): Task[];
|
|
26
|
+
/**
|
|
27
|
+
* Derive all overlay-facing state in O(tasks + dependency edges).
|
|
28
|
+
*
|
|
29
|
+
* The id index replaces one full task-list scan per dependency, while the
|
|
30
|
+
* ordered pass keeps group ordering identical to the source document.
|
|
31
|
+
*/
|
|
32
|
+
declare function deriveTaskProjection(tasks: readonly Task[]): TaskProjection;
|
|
19
33
|
declare function countTasks(tasks: readonly Task[]): TaskCounts;
|
|
20
34
|
/**
|
|
21
35
|
* Group tasks for display.
|
|
@@ -32,14 +46,8 @@ interface OverlayLayout {
|
|
|
32
46
|
hiddenCompleted: number;
|
|
33
47
|
truncatedTail: number;
|
|
34
48
|
}
|
|
35
|
-
|
|
36
|
-
* Choose which rows fit the overlay budget.
|
|
37
|
-
*
|
|
38
|
-
* Active work wins the space: in-progress first, then blocked and pending, and
|
|
39
|
-
* completed rows are dropped first when the budget runs short since they carry
|
|
40
|
-
* the least information about what happens next.
|
|
41
|
-
*/
|
|
49
|
+
declare function selectOverlayLayoutFromProjection(projection: TaskProjection, budget: number): OverlayLayout;
|
|
42
50
|
declare function selectOverlayLayout(tasks: readonly Task[], budget: number): OverlayLayout;
|
|
43
51
|
//#endregion
|
|
44
|
-
export { OverlayLayout, TaskCounts, TaskGroups, countTasks, groupTasks, hasActiveWork, selectOverlayLayout, shouldShowIds, visibleTasks };
|
|
52
|
+
export { OverlayLayout, TaskCounts, TaskGroups, TaskProjection, countTasks, deriveTaskProjection, groupTasks, hasActiveWork, selectOverlayLayout, selectOverlayLayoutFromProjection, shouldShowIds, visibleTasks };
|
|
45
53
|
//# sourceMappingURL=selectors.d.cts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"selectors.d.cts","names":[],"sources":["../../src/tui/selectors.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"selectors.d.cts","names":[],"sources":["../../src/tui/selectors.ts"],"mappings":";;UAEiB;EACf;EACA;EACA;EACA;EACA;EACA;;UAGe;EACf,YAAY;EACZ,SAAS;EACT,SAAS;EACT,WAAW;EACX,QAAQ;;UAGO;EACf,SAAS;EACT,QAAQ;EACR,QAAQ;EACR;EACA;;iBAGc,aAAa,gBAAgB,SAAS;;;;;;;iBA6BtC,qBAAqB,gBAAgB,SAAS;iBA2C9C,WAAW,gBAAgB,SAAS;;;;;;;iBAUpC,WAAW,gBAAgB,SAAS;;iBAKpC,cAAc,gBAAgB;iBAI9B,cAAc,gBAAgB;UAI7B;EACf,SAAS;EACT;EACA;;iBAyCc,kCAAkC,YAAY,gBAAgB,iBAAiB;iBAI/E,oBAAoB,gBAAgB,QAAQ,iBAAiB"}
|