makersclaw 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -3
- package/package.json +1 -1
- package/r/activity-log.json +52 -0
- package/r/agent-changes.json +51 -0
- package/r/agent-chat.json +53 -0
- package/r/agent-column.json +85 -0
- package/r/agent-dock.json +57 -0
- package/r/agent-kit.json +49 -0
- package/r/agent-marks.json +49 -0
- package/r/agent-pane.json +93 -0
- package/r/ai-action.json +56 -0
- package/r/annotate.json +3 -3
- package/r/approval-request.json +2 -2
- package/r/approval.json +54 -0
- package/r/ask-bar.json +50 -0
- package/r/audio-player.json +2 -2
- package/r/auth-form.json +2 -2
- package/r/automation-editor.json +66 -0
- package/r/automation-list.json +57 -0
- package/r/automation-model.json +118 -0
- package/r/automations-screen.json +71 -0
- package/r/board.json +2 -2
- package/r/calendar-view.json +3 -3
- package/r/cart.json +2 -2
- package/r/chart.json +2 -2
- package/r/chat-pane.json +84 -0
- package/r/chat.json +2 -2
- package/r/code-editor.json +2 -2
- package/r/comment-box.json +3 -3
- package/r/connect-card.json +51 -0
- package/r/contact-section.json +2 -2
- package/r/core.json +13 -7
- package/r/cost.json +51 -0
- package/r/csv-import.json +3 -3
- package/r/data-picker.json +60 -0
- package/r/data-table.json +2 -2
- package/r/diagram.json +2 -2
- package/r/doc-editor.json +2 -2
- package/r/doc-page.json +3 -3
- package/r/email-render.json +2 -2
- package/r/faq.json +2 -2
- package/r/file-upload.json +2 -2
- package/r/first-run.json +63 -0
- package/r/fix-note.json +57 -0
- package/r/flow-canvas.json +2 -2
- package/r/flow-editor.json +79 -0
- package/r/footer.json +2 -2
- package/r/gantt.json +3 -3
- package/r/job-panel.json +2 -2
- package/r/json-editor.json +3 -3
- package/r/library-shelf.json +56 -0
- package/r/map.json +2 -2
- package/r/margin-notes.json +67 -0
- package/r/menubar.json +2 -2
- package/r/mind-map.json +3 -3
- package/r/navigation-menu.json +2 -2
- package/r/needs-you.json +55 -0
- package/r/order-detail.json +2 -2
- package/r/org-chart.json +3 -3
- package/r/pace-bar.json +69 -0
- package/r/page-builder.json +2 -2
- package/r/payment-methods.json +2 -2
- package/r/pdf-document.json +2 -2
- package/r/pdf-viewer.json +2 -2
- package/r/product-detail.json +2 -2
- package/r/product-grid.json +3 -3
- package/r/record-form.json +2 -2
- package/r/record-grid.json +52 -0
- package/r/record-notes.json +4 -4
- package/r/registry.json +4086 -1512
- package/r/run-history.json +52 -0
- package/r/run-view.json +80 -0
- package/r/schedule-card.json +54 -0
- package/r/schedule-engine.json +79 -0
- package/r/schedule-gantt.json +83 -0
- package/r/schedule-ruler.json +64 -0
- package/r/search-dialog.json +2 -2
- package/r/settings-list.json +2 -2
- package/r/setup-checklist.json +2 -2
- package/r/share-dialog.json +2 -2
- package/r/sidebar.json +2 -2
- package/r/signature-pad.json +2 -2
- package/r/sketch-canvas.json +2 -2
- package/r/source-receipt.json +52 -0
- package/r/spreadsheet.json +3 -3
- package/r/stat-cards.json +3 -3
- package/r/step-picker.json +53 -0
- package/r/step-settings.json +72 -0
- package/r/suggestion.json +58 -0
- package/r/sum-bar.json +64 -0
- package/r/task-list.json +3 -3
- package/r/task-model.json +2 -2
- package/r/testimonials.json +2 -2
- package/r/tier-map.json +67 -0
- package/r/time-grid.json +73 -0
- package/r/time-strip.json +64 -0
- package/r/top-bar.json +66 -0
- package/r/transaction-list.json +2 -2
- package/r/trigger-picker.json +61 -0
- package/r/whiteboard.json +3 -3
- package/r/workflow-view.json +55 -0
- package/registry/blocks/activity-log/README.md +112 -0
- package/registry/blocks/activity-log/activity-log.jsx +351 -0
- package/registry/blocks/activity-log/block.json +33 -0
- package/registry/blocks/activity-log/demo.jsx +48 -0
- package/registry/blocks/activity-log/index.js +4 -0
- package/registry/blocks/agent-changes/README.md +94 -0
- package/registry/blocks/agent-changes/agent-changes.jsx +118 -0
- package/registry/blocks/agent-changes/block.json +28 -0
- package/registry/blocks/agent-changes/demo.jsx +39 -0
- package/registry/blocks/agent-changes/index.js +3 -0
- package/registry/blocks/agent-chat/README.md +145 -0
- package/registry/blocks/agent-chat/agent-chat.jsx +415 -0
- package/registry/blocks/agent-chat/block.json +30 -0
- package/registry/blocks/agent-chat/demo.jsx +165 -0
- package/registry/blocks/agent-chat/index.js +3 -0
- package/registry/blocks/agent-column/README.md +197 -0
- package/registry/blocks/agent-column/agent-column.jsx +492 -0
- package/registry/blocks/agent-column/block.json +67 -0
- package/registry/blocks/agent-column/column-picker.jsx +163 -0
- package/registry/blocks/agent-column/demo.jsx +50 -0
- package/registry/blocks/agent-column/index.js +7 -0
- package/registry/blocks/agent-column/model.js +164 -0
- package/registry/blocks/agent-column/receipt.jsx +483 -0
- package/registry/blocks/agent-column/spreadsheet-type.jsx +24 -0
- package/registry/blocks/agent-dock/README.md +210 -0
- package/registry/blocks/agent-dock/agent-dock.jsx +356 -0
- package/registry/blocks/agent-dock/block.json +47 -0
- package/registry/blocks/agent-dock/demo.jsx +153 -0
- package/registry/blocks/agent-dock/dock-model.js +118 -0
- package/registry/blocks/agent-dock/index.js +5 -0
- package/registry/blocks/agent-kit/README.md +73 -0
- package/registry/blocks/agent-kit/agent-kit.jsx +86 -0
- package/registry/blocks/agent-kit/block.json +10 -0
- package/registry/blocks/agent-kit/demo.jsx +35 -0
- package/registry/blocks/agent-kit/index.js +3 -0
- package/registry/blocks/agent-marks/README.md +80 -0
- package/registry/blocks/agent-marks/agent-marks.jsx +133 -0
- package/registry/blocks/agent-marks/block.json +27 -0
- package/registry/blocks/agent-marks/demo.jsx +49 -0
- package/registry/blocks/agent-marks/index.js +3 -0
- package/registry/blocks/agent-pane/README.md +225 -0
- package/registry/blocks/agent-pane/agent-pane.jsx +244 -0
- package/registry/blocks/agent-pane/block.json +41 -0
- package/registry/blocks/agent-pane/composer.jsx +6 -0
- package/registry/blocks/agent-pane/demo.jsx +233 -0
- package/registry/blocks/agent-pane/dock-context.js +10 -0
- package/registry/blocks/agent-pane/index.js +4 -0
- package/registry/blocks/agent-pane/prose.jsx +84 -0
- package/registry/blocks/agent-pane/questions.jsx +134 -0
- package/registry/blocks/agent-pane/transcript.jsx +268 -0
- package/registry/blocks/agent-pane/work.jsx +368 -0
- package/registry/blocks/ai-action/README.md +94 -0
- package/registry/blocks/ai-action/ai-action.jsx +188 -0
- package/registry/blocks/ai-action/block.json +34 -0
- package/registry/blocks/ai-action/demo.jsx +93 -0
- package/registry/blocks/ai-action/index.js +3 -0
- package/registry/blocks/approval/README.md +145 -0
- package/registry/blocks/approval/approval.jsx +163 -0
- package/registry/blocks/approval/block.json +31 -0
- package/registry/blocks/approval/demo.jsx +121 -0
- package/registry/blocks/approval/index.js +3 -0
- package/registry/blocks/ask-bar/README.md +105 -0
- package/registry/blocks/ask-bar/ask-bar.jsx +166 -0
- package/registry/blocks/ask-bar/block.json +26 -0
- package/registry/blocks/ask-bar/demo.jsx +78 -0
- package/registry/blocks/ask-bar/index.js +3 -0
- package/registry/blocks/automation-editor/README.md +291 -0
- package/registry/blocks/automation-editor/automation-editor.jsx +656 -0
- package/registry/blocks/automation-editor/block.json +39 -0
- package/registry/blocks/automation-editor/demo.jsx +284 -0
- package/registry/blocks/automation-editor/index.js +4 -0
- package/registry/blocks/automation-editor/run-inputs.jsx +82 -0
- package/registry/blocks/automation-list/README.md +129 -0
- package/registry/blocks/automation-list/automation-list.jsx +476 -0
- package/registry/blocks/automation-list/block.json +35 -0
- package/registry/blocks/automation-list/demo.jsx +67 -0
- package/registry/blocks/automation-list/index.js +3 -0
- package/registry/blocks/automation-model/README.md +109 -0
- package/registry/blocks/automation-model/block.json +135 -0
- package/registry/blocks/automation-model/catalog.js +256 -0
- package/registry/blocks/automation-model/check.js +161 -0
- package/registry/blocks/automation-model/demo.jsx +42 -0
- package/registry/blocks/automation-model/edit.js +262 -0
- package/registry/blocks/automation-model/fixes.js +287 -0
- package/registry/blocks/automation-model/glyph.jsx +13 -0
- package/registry/blocks/automation-model/graph.js +410 -0
- package/registry/blocks/automation-model/index.js +33 -0
- package/registry/blocks/automation-model/overlay.js +175 -0
- package/registry/blocks/automation-model/parts.jsx +94 -0
- package/registry/blocks/automation-model/schedule.js +53 -0
- package/registry/blocks/automation-model/templates.js +128 -0
- package/registry/blocks/automation-model/values.js +29 -0
- package/registry/blocks/automation-model/words.js +439 -0
- package/registry/blocks/automations-screen/README.md +207 -0
- package/registry/blocks/automations-screen/automations-screen.jsx +452 -0
- package/registry/blocks/automations-screen/block.json +47 -0
- package/registry/blocks/automations-screen/demo.jsx +364 -0
- package/registry/blocks/automations-screen/index.js +4 -0
- package/registry/blocks/automations-screen/screen-model.js +129 -0
- package/registry/blocks/chat-pane/README.md +143 -0
- package/registry/blocks/chat-pane/block.json +48 -0
- package/registry/blocks/chat-pane/chat-attach.js +166 -0
- package/registry/blocks/chat-pane/chat-composer.jsx +195 -0
- package/registry/blocks/chat-pane/chat-context.js +17 -0
- package/registry/blocks/chat-pane/chat-model.js +43 -0
- package/registry/blocks/chat-pane/chat-pane.jsx +398 -0
- package/registry/blocks/chat-pane/chats-list.jsx +52 -0
- package/registry/blocks/chat-pane/demo.jsx +103 -0
- package/registry/blocks/chat-pane/index.js +9 -0
- package/registry/blocks/connect-card/README.md +99 -0
- package/registry/blocks/connect-card/block.json +10 -0
- package/registry/blocks/connect-card/connect-card.jsx +103 -0
- package/registry/blocks/connect-card/demo.jsx +49 -0
- package/registry/blocks/connect-card/index.js +3 -0
- package/registry/blocks/cost/README.md +115 -0
- package/registry/blocks/cost/block.json +10 -0
- package/registry/blocks/cost/cost.jsx +176 -0
- package/registry/blocks/cost/demo.jsx +66 -0
- package/registry/blocks/cost/index.js +3 -0
- package/registry/blocks/data-picker/README.md +81 -0
- package/registry/blocks/data-picker/block.json +38 -0
- package/registry/blocks/data-picker/data-picker.jsx +210 -0
- package/registry/blocks/data-picker/demo.jsx +34 -0
- package/registry/blocks/data-picker/fields.js +89 -0
- package/registry/blocks/data-picker/index.js +4 -0
- package/registry/blocks/data-table/demo.jsx +1 -1
- package/registry/blocks/first-run/README.md +118 -0
- package/registry/blocks/first-run/block.json +11 -0
- package/registry/blocks/first-run/demo.jsx +88 -0
- package/registry/blocks/first-run/first-run-model.js +51 -0
- package/registry/blocks/first-run/first-run.jsx +156 -0
- package/registry/blocks/first-run/index.js +5 -0
- package/registry/blocks/first-run/probe-rules.js +26 -0
- package/registry/blocks/fix-note/README.md +79 -0
- package/registry/blocks/fix-note/block.json +35 -0
- package/registry/blocks/fix-note/demo.jsx +30 -0
- package/registry/blocks/fix-note/fix-note.jsx +235 -0
- package/registry/blocks/fix-note/index.js +3 -0
- package/registry/blocks/flow-editor/README.md +108 -0
- package/registry/blocks/flow-editor/block.json +44 -0
- package/registry/blocks/flow-editor/demo.jsx +115 -0
- package/registry/blocks/flow-editor/flow-canvas.jsx +568 -0
- package/registry/blocks/flow-editor/flow-editor.jsx +154 -0
- package/registry/blocks/flow-editor/index.js +6 -0
- package/registry/blocks/flow-editor/minimap.jsx +78 -0
- package/registry/blocks/flow-editor/outline.jsx +134 -0
- package/registry/blocks/library-shelf/README.md +117 -0
- package/registry/blocks/library-shelf/block.json +28 -0
- package/registry/blocks/library-shelf/demo.jsx +68 -0
- package/registry/blocks/library-shelf/index.js +3 -0
- package/registry/blocks/library-shelf/library-shelf.jsx +211 -0
- package/registry/blocks/library-shelf/probe-rules.js +15 -0
- package/registry/blocks/margin-notes/README.md +222 -0
- package/registry/blocks/margin-notes/block.json +10 -0
- package/registry/blocks/margin-notes/demo.jsx +144 -0
- package/registry/blocks/margin-notes/index.js +20 -0
- package/registry/blocks/margin-notes/margin-model.js +224 -0
- package/registry/blocks/margin-notes/margin-notes.jsx +465 -0
- package/registry/blocks/needs-you/README.md +177 -0
- package/registry/blocks/needs-you/block.json +34 -0
- package/registry/blocks/needs-you/demo.jsx +143 -0
- package/registry/blocks/needs-you/index.js +3 -0
- package/registry/blocks/needs-you/needs-you.jsx +441 -0
- package/registry/blocks/pace-bar/README.md +230 -0
- package/registry/blocks/pace-bar/block.json +10 -0
- package/registry/blocks/pace-bar/demo.jsx +255 -0
- package/registry/blocks/pace-bar/index.js +22 -0
- package/registry/blocks/pace-bar/pace-bar.jsx +461 -0
- package/registry/blocks/pace-bar/pace-model.js +288 -0
- package/registry/blocks/record-grid/README.md +100 -0
- package/registry/blocks/record-grid/block.json +28 -0
- package/registry/blocks/record-grid/demo.jsx +43 -0
- package/registry/blocks/record-grid/index.js +3 -0
- package/registry/blocks/record-grid/record-grid.jsx +251 -0
- package/registry/blocks/run-history/README.md +138 -0
- package/registry/blocks/run-history/block.json +10 -0
- package/registry/blocks/run-history/demo.jsx +74 -0
- package/registry/blocks/run-history/index.js +3 -0
- package/registry/blocks/run-history/run-history.jsx +218 -0
- package/registry/blocks/run-view/README.md +174 -0
- package/registry/blocks/run-view/block.json +50 -0
- package/registry/blocks/run-view/demo.jsx +286 -0
- package/registry/blocks/run-view/index.js +5 -0
- package/registry/blocks/run-view/run-controls.jsx +266 -0
- package/registry/blocks/run-view/run-view.jsx +236 -0
- package/registry/blocks/run-view/runs-list.jsx +99 -0
- package/registry/blocks/schedule-card/README.md +121 -0
- package/registry/blocks/schedule-card/block.json +10 -0
- package/registry/blocks/schedule-card/demo.jsx +82 -0
- package/registry/blocks/schedule-card/index.js +3 -0
- package/registry/blocks/schedule-card/schedule-card.jsx +136 -0
- package/registry/blocks/schedule-engine/README.md +137 -0
- package/registry/blocks/schedule-engine/block.json +10 -0
- package/registry/blocks/schedule-engine/demo.jsx +82 -0
- package/registry/blocks/schedule-engine/engine.js +769 -0
- package/registry/blocks/schedule-engine/index.js +9 -0
- package/registry/blocks/schedule-engine/ops.js +71 -0
- package/registry/blocks/schedule-engine/place.js +294 -0
- package/registry/blocks/schedule-engine/tree.js +79 -0
- package/registry/blocks/schedule-engine/waits.js +80 -0
- package/registry/blocks/schedule-gantt/README.md +159 -0
- package/registry/blocks/schedule-gantt/block.json +10 -0
- package/registry/blocks/schedule-gantt/demo.jsx +223 -0
- package/registry/blocks/schedule-gantt/gantt-model.js +219 -0
- package/registry/blocks/schedule-gantt/geometry.js +176 -0
- package/registry/blocks/schedule-gantt/index.js +26 -0
- package/registry/blocks/schedule-gantt/schedule-gantt.jsx +882 -0
- package/registry/blocks/schedule-gantt/sheet.jsx +372 -0
- package/registry/blocks/schedule-gantt/track.jsx +190 -0
- package/registry/blocks/schedule-ruler/README.md +148 -0
- package/registry/blocks/schedule-ruler/block.json +10 -0
- package/registry/blocks/schedule-ruler/demo.jsx +181 -0
- package/registry/blocks/schedule-ruler/index.js +21 -0
- package/registry/blocks/schedule-ruler/ruler-model.js +239 -0
- package/registry/blocks/schedule-ruler/schedule-ruler.jsx +548 -0
- package/registry/blocks/source-receipt/README.md +97 -0
- package/registry/blocks/source-receipt/block.json +30 -0
- package/registry/blocks/source-receipt/demo.jsx +71 -0
- package/registry/blocks/source-receipt/index.js +3 -0
- package/registry/blocks/source-receipt/source-receipt.jsx +107 -0
- package/registry/blocks/spreadsheet/spreadsheet.jsx +15 -1
- package/registry/blocks/step-picker/README.md +78 -0
- package/registry/blocks/step-picker/block.json +31 -0
- package/registry/blocks/step-picker/demo.jsx +23 -0
- package/registry/blocks/step-picker/index.js +3 -0
- package/registry/blocks/step-picker/step-picker.jsx +144 -0
- package/registry/blocks/step-settings/README.md +100 -0
- package/registry/blocks/step-settings/block.json +44 -0
- package/registry/blocks/step-settings/demo.jsx +93 -0
- package/registry/blocks/step-settings/fields.jsx +58 -0
- package/registry/blocks/step-settings/flow-settings.jsx +115 -0
- package/registry/blocks/step-settings/index.js +5 -0
- package/registry/blocks/step-settings/step-settings.jsx +290 -0
- package/registry/blocks/suggestion/README.md +171 -0
- package/registry/blocks/suggestion/block.json +40 -0
- package/registry/blocks/suggestion/demo.jsx +91 -0
- package/registry/blocks/suggestion/index.js +3 -0
- package/registry/blocks/suggestion/suggestion.jsx +436 -0
- package/registry/blocks/sum-bar/README.md +122 -0
- package/registry/blocks/sum-bar/block.json +10 -0
- package/registry/blocks/sum-bar/demo.jsx +110 -0
- package/registry/blocks/sum-bar/index.js +5 -0
- package/registry/blocks/sum-bar/sum-bar.jsx +209 -0
- package/registry/blocks/sum-bar/sum-model.js +126 -0
- package/registry/blocks/tier-map/README.md +217 -0
- package/registry/blocks/tier-map/block.json +10 -0
- package/registry/blocks/tier-map/demo.jsx +222 -0
- package/registry/blocks/tier-map/index.js +24 -0
- package/registry/blocks/tier-map/tier-map.jsx +574 -0
- package/registry/blocks/tier-map/tier-model.js +318 -0
- package/registry/blocks/time-grid/README.md +305 -0
- package/registry/blocks/time-grid/block.json +10 -0
- package/registry/blocks/time-grid/demo.jsx +279 -0
- package/registry/blocks/time-grid/grid-model.js +322 -0
- package/registry/blocks/time-grid/index.js +25 -0
- package/registry/blocks/time-grid/time-grid.jsx +1325 -0
- package/registry/blocks/time-strip/README.md +141 -0
- package/registry/blocks/time-strip/block.json +10 -0
- package/registry/blocks/time-strip/demo.jsx +129 -0
- package/registry/blocks/time-strip/index.js +19 -0
- package/registry/blocks/time-strip/strip-model.js +178 -0
- package/registry/blocks/time-strip/time-strip.jsx +260 -0
- package/registry/blocks/top-bar/README.md +149 -0
- package/registry/blocks/top-bar/block.json +35 -0
- package/registry/blocks/top-bar/demo.jsx +78 -0
- package/registry/blocks/top-bar/index.js +5 -0
- package/registry/blocks/top-bar/probe-rules.js +74 -0
- package/registry/blocks/top-bar/top-bar-model.js +152 -0
- package/registry/blocks/top-bar/top-bar.jsx +244 -0
- package/registry/blocks/trigger-picker/README.md +98 -0
- package/registry/blocks/trigger-picker/block.json +38 -0
- package/registry/blocks/trigger-picker/demo.jsx +59 -0
- package/registry/blocks/trigger-picker/index.js +3 -0
- package/registry/blocks/trigger-picker/trigger-picker.jsx +374 -0
- package/registry/blocks/workflow-view/README.md +123 -0
- package/registry/blocks/workflow-view/block.json +10 -0
- package/registry/blocks/workflow-view/demo.jsx +90 -0
- package/registry/blocks/workflow-view/index.js +3 -0
- package/registry/blocks/workflow-view/workflow-view.jsx +203 -0
- package/src/install.mjs +77 -16
- package/src/registry.mjs +3 -0
- package/templates/next/browser/lib/data.js +6 -1
- package/templates/next/files/README.md +627 -24
- package/templates/next/files/check/automation-format.ts +2103 -0
- package/templates/next/files/check/automation-package.ts +142 -0
- package/templates/next/files/check/run.mjs +968 -9
- package/templates/next/files/components/ui/field.jsx +17 -4
- package/templates/next/files/components/ui/icon.jsx +13 -0
- package/templates/next/files/components/ui/index.js +2 -1
- package/templates/next/files/components/ui/layout.jsx +77 -2
- package/templates/next/files/components/ui/notice.jsx +70 -0
- package/templates/next/files/components/ui/page.jsx +124 -0
- package/templates/next/files/components/ui/stat.jsx +22 -7
- package/templates/next/files/components/ui/toast.jsx +6 -3
- package/templates/next/files/lib/automations.js +660 -0
- package/templates/next/files/lib/claw.js +241 -5
- package/templates/next/files/lib/door.js +92 -0
- package/templates/next/files/lib/hooks.js +1627 -8
- package/templates/next/files/lib/platform.js +529 -21
- package/templates/next/template.json +13 -3
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// The schedule-engine block — `makersclaw add schedule-engine`. Fixed:
|
|
2
|
+
// compose it through its functions (README.md beside this file). Pure
|
|
3
|
+
// JavaScript, no React: the browser, a script and an agent run the same
|
|
4
|
+
// rules.
|
|
5
|
+
export * from "./engine";
|
|
6
|
+
export { taskTree, descendantsOf, positionBetween, siblingOrder } from "./tree";
|
|
7
|
+
export { KIND_WORDS, lagWords, shortForm, waitsWords, parseWaits } from "./waits";
|
|
8
|
+
export { nextWorkday, summaryChanges, placeNew, familyLinkProblem, outlineChange, lengthChange, overlapsOf, snapshotOf } from "./place";
|
|
9
|
+
export { operate, plain, OPERATIONS } from "./ops";
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// One entry for the questions an agent (or a script) asks of the rules:
|
|
2
|
+
// the same functions a screen runs, over JSON. A package's
|
|
3
|
+
// `references/plan.mjs` reads `{ operation, tasks, links, … }` on stdin and
|
|
4
|
+
// prints `operate(input)` as JSON, so the dates an agent states or writes
|
|
5
|
+
// are the dates the chart shows.
|
|
6
|
+
//
|
|
7
|
+
// schedule the plan: rows, links, finish, critical path, stuck, violations
|
|
8
|
+
// move { moves: [{ id, start, due }] } what a move shifts
|
|
9
|
+
// replan { today } late work off the past
|
|
10
|
+
// mend {} broken links aligned
|
|
11
|
+
// place { add, today, project } where a new task goes
|
|
12
|
+
// outline { id, kind, parent_id } an outline change
|
|
13
|
+
// length { id, days, project } a length, dependents follow
|
|
14
|
+
// link { task_id, depends_on_id, kind, lag } can it be made; what it shifts
|
|
15
|
+
// snapshot {} the rows a baseline keeps
|
|
16
|
+
// load { owner_key } who is on two things at once
|
|
17
|
+
//
|
|
18
|
+
// Maps become arrays of [id, value] pairs' objects (`rows` an object keyed
|
|
19
|
+
// by id); Sets become arrays. Throws nothing: a bad input answers { ok: false, error }.
|
|
20
|
+
|
|
21
|
+
import { alignConflicts, linkKind, linkProblem, longPole, replanFromToday, schedule, shiftDependents } from "./engine";
|
|
22
|
+
import { lengthChange, outlineChange, overlapsOf, placeNew, snapshotOf, summaryChanges } from "./place";
|
|
23
|
+
|
|
24
|
+
/** Maps and Sets as plain JSON values. */
|
|
25
|
+
export function plain(value) {
|
|
26
|
+
return JSON.parse(JSON.stringify(value, (_k, v) => (v instanceof Map ? Object.fromEntries(v) : v instanceof Set ? [...v] : v)));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The operations an agent may ask for. */
|
|
30
|
+
export const OPERATIONS = ["schedule", "move", "replan", "mend", "place", "outline", "length", "link", "snapshot", "load"];
|
|
31
|
+
|
|
32
|
+
export function operate(input = {}) {
|
|
33
|
+
try {
|
|
34
|
+
const { tasks = [], links = [], operation = "schedule" } = input;
|
|
35
|
+
switch (operation) {
|
|
36
|
+
case "schedule": {
|
|
37
|
+
const plan = schedule(tasks, links);
|
|
38
|
+
return plain({ ok: true, ...plan, longPole: longPole(plan) });
|
|
39
|
+
}
|
|
40
|
+
case "move":
|
|
41
|
+
return plain({ ok: true, ...shiftDependents(tasks, links, input.moves ?? []) });
|
|
42
|
+
case "replan":
|
|
43
|
+
return plain({ ok: true, ...replanFromToday(tasks, links, input.today) });
|
|
44
|
+
case "mend":
|
|
45
|
+
return plain({ ok: true, ...alignConflicts(tasks, links) });
|
|
46
|
+
case "place": {
|
|
47
|
+
const placed = placeNew(tasks, { add: input.add ?? {}, today: input.today, project: input.project ?? null });
|
|
48
|
+
const next = [...tasks, { id: "__new", parent_id: placed.parent_id, start_date: placed.start_date, due_date: placed.due_date, is_milestone: placed.is_milestone, progress: 0, status: "todo" }];
|
|
49
|
+
return plain({ ...placed, summaries: placed.resum ? summaryChanges(next, links, ["__new"]) : [] });
|
|
50
|
+
}
|
|
51
|
+
case "outline":
|
|
52
|
+
return plain(outlineChange(tasks, links, { id: input.id, kind: input.kind, parent_id: input.parent_id ?? null }));
|
|
53
|
+
case "length":
|
|
54
|
+
return plain(lengthChange(tasks, links, { id: input.id, days: input.days, project: input.project ?? null }));
|
|
55
|
+
case "link": {
|
|
56
|
+
const problem = linkProblem(tasks, links, input.task_id, input.depends_on_id);
|
|
57
|
+
if (problem) return { ok: false, error: problem };
|
|
58
|
+
const next = [...links, { id: "__new", task_id: input.task_id, depends_on_id: input.depends_on_id, kind: input.kind ?? "finish_to_start", lag_days: Number(input.lag ?? 0) || 0 }];
|
|
59
|
+
return plain({ ok: true, kind: linkKind(input.kind), ...shiftDependents(tasks, links, [], { nextLinks: next }) });
|
|
60
|
+
}
|
|
61
|
+
case "snapshot":
|
|
62
|
+
return plain({ ok: true, ...snapshotOf(tasks, links) });
|
|
63
|
+
case "load":
|
|
64
|
+
return plain({ ok: true, overlaps: overlapsOf(tasks, schedule(tasks, links), { ownerOf: (t) => t[input.owner_key ?? "assignee_id"] }) });
|
|
65
|
+
default:
|
|
66
|
+
return { ok: false, error: `Unknown operation "${operation}". Use one of: ${OPERATIONS.join(", ")}.` };
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
return { ok: false, error: error?.message ?? String(error) };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// Planning a write: where a new task goes, what an outline change moves, which
|
|
2
|
+
// summaries must re-span. Pure functions over the same rows the engine takes,
|
|
3
|
+
// so a screen and an agent that ask the same question get the same answer.
|
|
4
|
+
//
|
|
5
|
+
// None of these writes anything. Each returns exactly the values a write
|
|
6
|
+
// needs ({ parent_id, sort_order, start_date, due_date, summaries, changes … }),
|
|
7
|
+
// and the caller hands them to its own save.
|
|
8
|
+
//
|
|
9
|
+
// Rows in (as the engine's):
|
|
10
|
+
// task { id, parent_id, title, start_date, due_date, is_milestone, progress, status, position }
|
|
11
|
+
// link { id, task_id, depends_on_id, kind, lag_days }
|
|
12
|
+
|
|
13
|
+
import { addWorkdays, datesOf, dayOf, isoOf, makeCalendar, schedule, shiftDependents, spanOf } from "./engine";
|
|
14
|
+
import { descendantsOf, positionBetween, siblingOrder, taskTree } from "./tree";
|
|
15
|
+
|
|
16
|
+
const ISO = /^\d{4}-\d{2}-\d{2}$/;
|
|
17
|
+
const same = (a, b) => String(a ?? "") === String(b ?? "");
|
|
18
|
+
const quote = (s) => `“${String(s ?? "").slice(0, 90)}”`;
|
|
19
|
+
|
|
20
|
+
/** The working day on or after `iso`, as a date. */
|
|
21
|
+
export function nextWorkday(iso, calendar = "weekdays") {
|
|
22
|
+
const cal = makeCalendar(calendar);
|
|
23
|
+
const d = dayOf(iso);
|
|
24
|
+
return d == null ? null : isoOf(cal.dayAt(cal.startIndex(d)));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Summaries whose stored dates differ from the span of the work under them:
|
|
29
|
+
* the ancestors of each id (and each id itself when it is a summary).
|
|
30
|
+
* → [{ id, start_date, due_date }]
|
|
31
|
+
*/
|
|
32
|
+
export function summaryChanges(tasks, links, ids) {
|
|
33
|
+
const plan = schedule(tasks, links);
|
|
34
|
+
const byId = new Map((tasks ?? []).map((t) => [String(t.id), t]));
|
|
35
|
+
const want = new Set();
|
|
36
|
+
for (const id of ids ?? []) {
|
|
37
|
+
for (let p = byId.get(String(id))?.parent_id; p; p = byId.get(String(p))?.parent_id) {
|
|
38
|
+
if (want.has(String(p))) break;
|
|
39
|
+
want.add(String(p));
|
|
40
|
+
}
|
|
41
|
+
if (id) want.add(String(id));
|
|
42
|
+
}
|
|
43
|
+
const out = [];
|
|
44
|
+
for (const id of want) {
|
|
45
|
+
const row = plan.rows.get(id);
|
|
46
|
+
const t = byId.get(id);
|
|
47
|
+
if (!row?.summary || !t) continue;
|
|
48
|
+
if (!same(t.start_date, row.start) || !same(t.due_date, row.due)) out.push({ id, start_date: row.start, due_date: row.due });
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Where a new task goes and the dates it starts with.
|
|
55
|
+
*
|
|
56
|
+
* add: { title, parent_id, after_id, start, due, days, milestone }
|
|
57
|
+
* today: 'YYYY-MM-DD' in the team's zone
|
|
58
|
+
* project: { start_date }
|
|
59
|
+
*
|
|
60
|
+
* It lands after `after_id` at the same level, or as the last sub-task of
|
|
61
|
+
* `parent_id`, or last in the project. Undated input gets dates: a first
|
|
62
|
+
* sub-task takes its parent's dates; a task added after another starts the
|
|
63
|
+
* next working day; otherwise the later of today and the project's start.
|
|
64
|
+
* Three working days long unless `days` says.
|
|
65
|
+
*
|
|
66
|
+
* → { ok, parent_id, sort_order, start_date, due_date, is_milestone,
|
|
67
|
+
* resum: boolean (the parent already had sub-tasks: re-span it after) }
|
|
68
|
+
*/
|
|
69
|
+
export function placeNew(tasks, { add = {}, today, project = null } = {}) {
|
|
70
|
+
const list = tasks ?? [];
|
|
71
|
+
const byId = new Map(list.map((t) => [String(t.id), t]));
|
|
72
|
+
const after = add.after_id && byId.has(String(add.after_id)) ? byId.get(String(add.after_id)) : null;
|
|
73
|
+
const parent = add.parent_id && byId.has(String(add.parent_id)) ? byId.get(String(add.parent_id)) : after?.parent_id ? byId.get(String(after.parent_id)) ?? null : null;
|
|
74
|
+
const siblings = list.filter((t) => same(t.parent_id, parent?.id)).sort(siblingOrder);
|
|
75
|
+
let sort_order;
|
|
76
|
+
if (after && same(after.parent_id, parent?.id)) {
|
|
77
|
+
const i = siblings.findIndex((t) => same(t.id, after.id));
|
|
78
|
+
sort_order = positionBetween(after, siblings[i + 1] ?? null);
|
|
79
|
+
} else sort_order = positionBetween(siblings[siblings.length - 1] ?? null, null);
|
|
80
|
+
|
|
81
|
+
let s = ISO.test(String(add.start ?? "")) ? add.start : null;
|
|
82
|
+
let d = ISO.test(String(add.due ?? "")) ? add.due : null;
|
|
83
|
+
if (!s) {
|
|
84
|
+
const parentLeaf = parent && !list.some((t) => same(t.parent_id, parent.id)) && parent.start_date;
|
|
85
|
+
if (parentLeaf) {
|
|
86
|
+
s = String(parent.start_date);
|
|
87
|
+
d = d ?? String(parent.due_date ?? parent.start_date);
|
|
88
|
+
} else if (after?.due_date) s = addWorkdays(String(after.due_date), 1);
|
|
89
|
+
else {
|
|
90
|
+
const now = ISO.test(String(today ?? "")) ? today : isoOf(Math.floor(Date.now() / 86400000));
|
|
91
|
+
const begins = project?.start_date ? String(project.start_date) : null;
|
|
92
|
+
s = nextWorkday(begins && begins > now ? begins : now);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const length = Math.max(1, Math.min(500, Math.trunc(Number(add.days) || 0)));
|
|
96
|
+
const milestone = Boolean(add.milestone);
|
|
97
|
+
if (milestone) d = s = d ?? s;
|
|
98
|
+
else if (Number(add.days) >= 1) d = addWorkdays(nextWorkday(s), length - 1);
|
|
99
|
+
else if (!d || d < s) d = addWorkdays(nextWorkday(s), 2);
|
|
100
|
+
return {
|
|
101
|
+
ok: true,
|
|
102
|
+
parent_id: parent ? String(parent.id) : null,
|
|
103
|
+
sort_order,
|
|
104
|
+
start_date: s,
|
|
105
|
+
due_date: d,
|
|
106
|
+
is_milestone: milestone,
|
|
107
|
+
resum: Boolean(parent && list.some((t) => same(t.parent_id, parent.id))),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** A link between a task (or anything under it) and a would-be parent can't stay. */
|
|
112
|
+
export function familyLinkProblem(tasks, links, id, parentId) {
|
|
113
|
+
const under = descendantsOf(tasks, id);
|
|
114
|
+
under.add(String(id));
|
|
115
|
+
const bad = (links ?? []).find((l) => (under.has(String(l.task_id)) && same(l.depends_on_id, parentId)) || (under.has(String(l.depends_on_id)) && same(l.task_id, parentId)));
|
|
116
|
+
if (!bad) return null;
|
|
117
|
+
const title = (x) => (tasks ?? []).find((t) => same(t.id, x))?.title ?? "a task";
|
|
118
|
+
return `Remove the link between ${quote(title(bad.task_id))} and ${quote(title(bad.depends_on_id))} first: a task can't wait on its own summary.`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* A change of outline. `kind`: "in" (a sub-task of the row above), "out"
|
|
123
|
+
* (up a level, just after its old parent), "up" / "down" (one place among
|
|
124
|
+
* its siblings), "under" (last child of `parent_id`, or the top level for
|
|
125
|
+
* null).
|
|
126
|
+
*
|
|
127
|
+
* → { ok: false, error } or { ok: true, id, parent_id, old_parent_id,
|
|
128
|
+
* sort_order, order_only, changes (date changes the outline binds, as
|
|
129
|
+
* shiftDependents), summaries (re-spans: the old parent, the new one),
|
|
130
|
+
* finishBefore, finishAfter, words }
|
|
131
|
+
*/
|
|
132
|
+
export function outlineChange(tasks, links, { id, kind, parent_id = null }) {
|
|
133
|
+
const list = tasks ?? [];
|
|
134
|
+
const task = list.find((t) => same(t.id, id));
|
|
135
|
+
if (!task) return { ok: false, error: "That task is gone." };
|
|
136
|
+
const level = (pid) => list.filter((t) => same(t.parent_id, pid)).sort(siblingOrder);
|
|
137
|
+
const siblings = level(task.parent_id);
|
|
138
|
+
const i = siblings.findIndex((t) => same(t.id, id));
|
|
139
|
+
let newParent = task.parent_id ?? null;
|
|
140
|
+
let position;
|
|
141
|
+
let words;
|
|
142
|
+
if (kind === "up" || kind === "down") {
|
|
143
|
+
const j = kind === "up" ? i - 1 : i + 1;
|
|
144
|
+
if (j < 0 || j >= siblings.length) return { ok: false, error: kind === "up" ? "It's already first in its phase." : "It's already last in its phase." };
|
|
145
|
+
position = kind === "up" ? positionBetween(siblings[j - 1] ?? null, siblings[j]) : positionBetween(siblings[j], siblings[j + 1] ?? null);
|
|
146
|
+
words = `${quote(task.title)} ${kind} one place`;
|
|
147
|
+
} else if (kind === "in") {
|
|
148
|
+
if (i <= 0) return { ok: false, error: "There's nothing above it at this level to put it under." };
|
|
149
|
+
const parent = siblings[i - 1];
|
|
150
|
+
const problem = familyLinkProblem(list, links, id, parent.id);
|
|
151
|
+
if (problem) return { ok: false, error: problem };
|
|
152
|
+
const kids = level(parent.id);
|
|
153
|
+
newParent = String(parent.id);
|
|
154
|
+
position = positionBetween(kids[kids.length - 1] ?? null, null);
|
|
155
|
+
words = `${quote(task.title)} under ${quote(parent.title)}`;
|
|
156
|
+
} else if (kind === "out") {
|
|
157
|
+
const parent = task.parent_id ? list.find((t) => same(t.id, task.parent_id)) : null;
|
|
158
|
+
if (!parent) return { ok: false, error: "It's already at the top level." };
|
|
159
|
+
const grand = parent.parent_id ?? null;
|
|
160
|
+
if (grand) {
|
|
161
|
+
const problem = familyLinkProblem(list, links, id, grand);
|
|
162
|
+
if (problem) return { ok: false, error: problem };
|
|
163
|
+
}
|
|
164
|
+
const above = level(grand);
|
|
165
|
+
const j = above.findIndex((t) => same(t.id, parent.id));
|
|
166
|
+
newParent = grand == null ? null : String(grand);
|
|
167
|
+
position = positionBetween(parent, above[j + 1] ?? null);
|
|
168
|
+
words = `${quote(task.title)} out of ${quote(parent.title)}`;
|
|
169
|
+
} else if (kind === "under") {
|
|
170
|
+
const parent = parent_id ? list.find((t) => same(t.id, parent_id)) : null;
|
|
171
|
+
if (parent_id && !parent) return { ok: false, error: "That phase is gone." };
|
|
172
|
+
if (parent && (same(parent.id, id) || descendantsOf(list, id).has(String(parent.id)))) return { ok: false, error: "A task can't go under itself or one of its own sub-tasks." };
|
|
173
|
+
if (same(task.parent_id, parent?.id)) return { ok: false, error: "It's there already." };
|
|
174
|
+
if (parent) {
|
|
175
|
+
const problem = familyLinkProblem(list, links, id, parent.id);
|
|
176
|
+
if (problem) return { ok: false, error: problem };
|
|
177
|
+
}
|
|
178
|
+
const kids = level(parent?.id ?? null);
|
|
179
|
+
newParent = parent ? String(parent.id) : null;
|
|
180
|
+
position = positionBetween(kids[kids.length - 1] ?? null, null);
|
|
181
|
+
words = `${quote(task.title)} under ${parent ? quote(parent.title) : "the top level"}`;
|
|
182
|
+
} else return { ok: false, error: "Unknown outline change." };
|
|
183
|
+
|
|
184
|
+
const moved = !same(newParent, task.parent_id);
|
|
185
|
+
let result = { changes: [], finishBefore: schedule(list, links).finish, finishAfter: schedule(list, links).finish };
|
|
186
|
+
const summaries = [];
|
|
187
|
+
if (moved) {
|
|
188
|
+
const nextTasks = list.map((t) => (same(t.id, id) ? { ...t, parent_id: newParent } : t));
|
|
189
|
+
result = shiftDependents(list, links, [], { nextTasks });
|
|
190
|
+
const afterTasks = nextTasks.map((t) => {
|
|
191
|
+
const c = result.changes.find((x) => same(x.id, t.id));
|
|
192
|
+
return c ? { ...t, start_date: c.start_date, due_date: c.due_date } : t;
|
|
193
|
+
});
|
|
194
|
+
// The summary it left is re-spanned. The one it joined keeps its own
|
|
195
|
+
// dates, unless the task was put there by name ("under"): putting a task
|
|
196
|
+
// under another with a key and taking it out again loses nothing.
|
|
197
|
+
if (task.parent_id) summaries.push(...summaryChanges(afterTasks, links, [task.parent_id]));
|
|
198
|
+
if (newParent && kind === "under") summaries.push(...summaryChanges(afterTasks, links, [newParent]).filter((s) => !summaries.some((x) => same(x.id, s.id))));
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
ok: true,
|
|
202
|
+
id: String(id),
|
|
203
|
+
parent_id: newParent,
|
|
204
|
+
old_parent_id: task.parent_id == null ? null : String(task.parent_id),
|
|
205
|
+
sort_order: position,
|
|
206
|
+
order_only: !moved,
|
|
207
|
+
changes: result.changes.filter((c) => !c.summary),
|
|
208
|
+
summaries,
|
|
209
|
+
finishBefore: result.finishBefore,
|
|
210
|
+
finishAfter: result.finishAfter,
|
|
211
|
+
words,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Give a task a length: it keeps its start (or takes the project's) and
|
|
217
|
+
* the dependents follow. → shiftDependents' result, or { ok: false, error }.
|
|
218
|
+
*/
|
|
219
|
+
export function lengthChange(tasks, links, { id, days, project = null }) {
|
|
220
|
+
const len = Math.trunc(Number(days));
|
|
221
|
+
if (!(len >= 1 && len <= 500)) return { ok: false, error: "A length is a whole number of working days, 1 or more." };
|
|
222
|
+
const task = (tasks ?? []).find((t) => same(t.id, id));
|
|
223
|
+
if (!task) return { ok: false, error: "That task is gone." };
|
|
224
|
+
if (task.is_milestone) return { ok: false, error: "A milestone has no length." };
|
|
225
|
+
const start = task.start_date ? String(task.start_date).slice(0, 10) : project?.start_date ? nextWorkday(String(project.start_date)) : null;
|
|
226
|
+
if (!start) return { ok: false, error: "Set the project's start date first: a length needs a day to count from." };
|
|
227
|
+
const s = spanOf({ start_date: start, due_date: start });
|
|
228
|
+
const m = datesOf(s.s, len, false);
|
|
229
|
+
return { ok: true, ...shiftDependents(tasks, links, [{ id: String(id), start: m.start, due: m.due }]), moves: [{ id: String(id), start: m.start, due: m.due }] };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Who is on two things at once: for each owner, the stretches of working
|
|
234
|
+
* days where two or more of their unfinished, dated tasks overlap.
|
|
235
|
+
* `ownerOf(task)` → a key (default `assignee_id`).
|
|
236
|
+
* → [{ owner, from, to, count, ids }] sorted by owner then date.
|
|
237
|
+
*/
|
|
238
|
+
export function overlapsOf(tasks, plan, { ownerOf = (t) => t.assignee_id } = {}) {
|
|
239
|
+
const cal = makeCalendar();
|
|
240
|
+
const by = new Map();
|
|
241
|
+
for (const t of tasks ?? []) {
|
|
242
|
+
const r = plan?.rows?.get(String(t.id));
|
|
243
|
+
const owner = ownerOf(t);
|
|
244
|
+
if (!r || !r.dated || r.summary || r.milestone || r.pinned || owner == null || owner === "") continue;
|
|
245
|
+
const a = dayOf(r.planStart);
|
|
246
|
+
const b = dayOf(r.planDue);
|
|
247
|
+
if (a == null || b == null) continue;
|
|
248
|
+
const k = String(owner);
|
|
249
|
+
if (!by.has(k)) by.set(k, []);
|
|
250
|
+
by.get(k).push({ id: String(t.id), s: cal.startIndex(a), e: cal.lastIndex(b) });
|
|
251
|
+
}
|
|
252
|
+
const out = [];
|
|
253
|
+
for (const [owner, spans] of by) {
|
|
254
|
+
const points = new Map();
|
|
255
|
+
for (const sp of spans) for (let x = sp.s; x <= sp.e; x += 1) points.set(x, (points.get(x) ?? 0) + 1);
|
|
256
|
+
let run = null;
|
|
257
|
+
const flush = () => {
|
|
258
|
+
if (!run) return;
|
|
259
|
+
const ids = spans.filter((sp) => sp.s <= run.e && sp.e >= run.s).map((sp) => sp.id);
|
|
260
|
+
out.push({ owner, from: isoOf(cal.dayAt(run.s)), to: isoOf(cal.dayAt(run.e)), count: run.n, ids });
|
|
261
|
+
run = null;
|
|
262
|
+
};
|
|
263
|
+
const days = [...points.keys()].sort((x, y) => x - y);
|
|
264
|
+
for (const x of days) {
|
|
265
|
+
const n = points.get(x);
|
|
266
|
+
if (n >= 2) {
|
|
267
|
+
if (run && run.e === x - 1) {
|
|
268
|
+
run.e = x;
|
|
269
|
+
run.n = Math.max(run.n, n);
|
|
270
|
+
} else {
|
|
271
|
+
flush();
|
|
272
|
+
run = { s: x, e: x, n };
|
|
273
|
+
}
|
|
274
|
+
} else flush();
|
|
275
|
+
}
|
|
276
|
+
flush();
|
|
277
|
+
}
|
|
278
|
+
return out.sort((a, b) => String(a.owner).localeCompare(String(b.owner)) || a.from.localeCompare(b.from));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** The rows a baseline keeps: every dated, live task as it is planned. → { finish_date, data } */
|
|
282
|
+
export function snapshotOf(tasks, links) {
|
|
283
|
+
const plan = schedule(tasks, links);
|
|
284
|
+
const data = [];
|
|
285
|
+
for (const t of tasks ?? []) {
|
|
286
|
+
if (!t || String(t.status ?? "") === "cancelled") continue;
|
|
287
|
+
const r = plan.rows.get(String(t.id));
|
|
288
|
+
if (!r?.dated) continue;
|
|
289
|
+
data.push({ task_id: String(t.id), title: String(t.title ?? ""), start_date: r.start, due_date: r.due, is_milestone: Boolean(t.is_milestone) });
|
|
290
|
+
}
|
|
291
|
+
return { finish_date: plan.finish, data };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export { taskTree };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// The task tree: outline order and outline numbers (1, 1.1, 1.2, 2 …).
|
|
2
|
+
// Pure, like engine.js: the chart, the server's writes and the tests use it.
|
|
3
|
+
|
|
4
|
+
const num = (v) => (Number.isFinite(Number(v)) ? Number(v) : 0);
|
|
5
|
+
|
|
6
|
+
/** Sibling order: position, then when it was made, then id. */
|
|
7
|
+
export function siblingOrder(a, b) {
|
|
8
|
+
return num(a.position) - num(b.position) || String(a.created_at ?? "").localeCompare(String(b.created_at ?? "")) || String(a.id).localeCompare(String(b.id));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* → { order: [{ id, parentId, depth, outline, childCount, index }], byId: Map(id → entry) }
|
|
13
|
+
* `order` walks the tree depth first. A parent outside `tasks` makes a
|
|
14
|
+
* root; a parent chain that loops is cut.
|
|
15
|
+
*/
|
|
16
|
+
export function taskTree(tasks) {
|
|
17
|
+
const list = (tasks ?? []).filter((t) => t && t.id != null);
|
|
18
|
+
const ids = new Set(list.map((t) => String(t.id)));
|
|
19
|
+
const kids = new Map();
|
|
20
|
+
for (const t of list) {
|
|
21
|
+
const pid = t.parent_id != null && ids.has(String(t.parent_id)) && String(t.parent_id) !== String(t.id) ? String(t.parent_id) : "";
|
|
22
|
+
if (!kids.has(pid)) kids.set(pid, []);
|
|
23
|
+
kids.get(pid).push(t);
|
|
24
|
+
}
|
|
25
|
+
for (const arr of kids.values()) arr.sort(siblingOrder);
|
|
26
|
+
const order = [];
|
|
27
|
+
const byId = new Map();
|
|
28
|
+
const seen = new Set();
|
|
29
|
+
const walk = (pid, depth, prefix) => {
|
|
30
|
+
(kids.get(pid) ?? []).forEach((t, i) => {
|
|
31
|
+
const id = String(t.id);
|
|
32
|
+
if (seen.has(id)) return;
|
|
33
|
+
seen.add(id);
|
|
34
|
+
const outline = prefix ? `${prefix}.${i + 1}` : String(i + 1);
|
|
35
|
+
const entry = { id, parentId: pid || null, depth, outline, childCount: (kids.get(id) ?? []).length, index: order.length };
|
|
36
|
+
order.push(entry);
|
|
37
|
+
byId.set(id, entry);
|
|
38
|
+
walk(id, depth + 1, outline);
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
walk("", 0, "");
|
|
42
|
+
// Anything a loop hid becomes a root at the end.
|
|
43
|
+
for (const t of list) {
|
|
44
|
+
const id = String(t.id);
|
|
45
|
+
if (seen.has(id)) continue;
|
|
46
|
+
seen.add(id);
|
|
47
|
+
const entry = { id, parentId: null, depth: 0, outline: String(order.filter((e) => e.depth === 0).length + 1), childCount: 0, index: order.length };
|
|
48
|
+
order.push(entry);
|
|
49
|
+
byId.set(id, entry);
|
|
50
|
+
}
|
|
51
|
+
return { order, byId };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Every id below `id` (not including it). */
|
|
55
|
+
export function descendantsOf(tasks, id) {
|
|
56
|
+
const out = new Set();
|
|
57
|
+
const want = [String(id)];
|
|
58
|
+
while (want.length) {
|
|
59
|
+
const p = want.pop();
|
|
60
|
+
for (const t of tasks ?? []) {
|
|
61
|
+
const tid = String(t.id);
|
|
62
|
+
if (String(t.parent_id ?? "") === p && !out.has(tid)) {
|
|
63
|
+
out.add(tid);
|
|
64
|
+
want.push(tid);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** A position between two siblings (or after the last one). */
|
|
72
|
+
export function positionBetween(before, after) {
|
|
73
|
+
const a = before == null ? null : num(before.position);
|
|
74
|
+
const b = after == null ? null : num(after.position);
|
|
75
|
+
if (a == null && b == null) return 1;
|
|
76
|
+
if (a == null) return b - 1;
|
|
77
|
+
if (b == null) return a + 1;
|
|
78
|
+
return (a + b) / 2;
|
|
79
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// The short form schedulers write for "what a task waits on": `2.5FS`,
|
|
2
|
+
// `6.3SS+5d`, `3.4SF-2d`, several joined with a semicolon. An outline
|
|
3
|
+
// number (the tree's 1, 1.1, 1.2.3) names the task, FS / SS / FF / SF the
|
|
4
|
+
// kind (finish-to-start when left out), and a signed number of working
|
|
5
|
+
// days the lag. Pure, like the engine.
|
|
6
|
+
|
|
7
|
+
import { LINK_KINDS, linkKind } from "./engine";
|
|
8
|
+
|
|
9
|
+
/** The four kinds, named in full and in a clause ("B starts after A finishes"). */
|
|
10
|
+
export const KIND_WORDS = {
|
|
11
|
+
FS: { name: "Finish to start", clause: (b, a) => `${b} starts after ${a} finishes` },
|
|
12
|
+
SS: { name: "Start to start", clause: (b, a) => `${b} can start once ${a} has started` },
|
|
13
|
+
FF: { name: "Finish to finish", clause: (b, a) => `${b} can finish once ${a} has finished` },
|
|
14
|
+
SF: { name: "Start to finish", clause: (b, a) => `${b} can finish once ${a} has started` },
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** `+2d`, `-1d` or "" for no lag. */
|
|
18
|
+
export function lagWords(lag) {
|
|
19
|
+
const n = Math.trunc(Number(lag) || 0);
|
|
20
|
+
return n ? `${n > 0 ? "+" : "-"}${Math.abs(n)}d` : "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** One link as the short form: `2.4FS+2d`. */
|
|
24
|
+
export function shortForm(outline, kind, lag) {
|
|
25
|
+
return `${outline}${linkKind(kind)}${lagWords(lag)}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* What a task waits on, in the short form.
|
|
30
|
+
* `links`: [{ task_id, depends_on_id, kind, lag_days }]; `outlineOf(id)` → "2.4".
|
|
31
|
+
* Only links of `taskId`; ordered by the predecessor's outline.
|
|
32
|
+
*/
|
|
33
|
+
export function waitsWords(links, taskId, outlineOf) {
|
|
34
|
+
return (links ?? [])
|
|
35
|
+
.filter((l) => String(l.task_id) === String(taskId))
|
|
36
|
+
.map((l) => ({ outline: outlineOf(String(l.depends_on_id)) ?? "", kind: l.kind, lag: l.lag_days }))
|
|
37
|
+
.filter((l) => l.outline)
|
|
38
|
+
.sort((a, b) => a.outline.localeCompare(b.outline, undefined, { numeric: true }))
|
|
39
|
+
.map((l) => shortForm(l.outline, l.kind, l.lag))
|
|
40
|
+
.join("; ");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const PART = /^\s*([0-9]+(?:\.[0-9]+)*)\s*(FS|SS|FF|SF)?\s*(?:([+-])\s*([0-9]+)\s*d?)?\s*$/i;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Read the short form back. `idOf(outline)` → the task's id, or null when
|
|
47
|
+
* the file / the screen has no such task.
|
|
48
|
+
* → { links: [{ depends_on_id, outline, kind ("finish_to_start" …), lag_days }],
|
|
49
|
+
* problems: [{ part, why }] }
|
|
50
|
+
*/
|
|
51
|
+
export function parseWaits(text, idOf) {
|
|
52
|
+
const links = [];
|
|
53
|
+
const problems = [];
|
|
54
|
+
for (const raw of String(text ?? "").split(/[;,]/)) {
|
|
55
|
+
const part = raw.trim();
|
|
56
|
+
if (!part) continue;
|
|
57
|
+
const m = PART.exec(part);
|
|
58
|
+
if (!m) {
|
|
59
|
+
problems.push({ part, why: `"${part}" isn't a task number with a kind and a lag, like 2.4FS+2d.` });
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const outline = m[1];
|
|
63
|
+
const id = idOf(outline);
|
|
64
|
+
if (id == null) {
|
|
65
|
+
problems.push({ part, why: `There is no task ${outline}.` });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (links.some((l) => l.depends_on_id === String(id))) {
|
|
69
|
+
problems.push({ part, why: `Task ${outline} is listed twice.` });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const lag = m[3] ? Number(m[4]) * (m[3] === "-" ? -1 : 1) : 0;
|
|
73
|
+
if (lag < -250 || lag > 250) {
|
|
74
|
+
problems.push({ part, why: "A lag is between -250 and 250 working days." });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
links.push({ depends_on_id: String(id), outline, kind: LINK_KINDS[linkKind(m[2] || "FS")], lag_days: lag });
|
|
78
|
+
}
|
|
79
|
+
return { links, problems };
|
|
80
|
+
}
|