toolroll 0.9.93 → 0.9.95
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/CHANGELOG.md +79 -0
- package/dist/activity-line.d.ts +7 -4
- package/dist/activity-line.js +9 -7
- package/dist/agent-fence.d.ts +10 -0
- package/dist/agent-fence.js +6 -3
- package/dist/approval-authority.d.ts +7 -1
- package/dist/approval-authority.js +11 -2
- package/dist/approval-password.d.ts +31 -0
- package/dist/approval-password.js +26 -0
- package/dist/assignment-status.d.ts +1 -1
- package/dist/assignment-ui.js +3 -1
- package/dist/assignment.js +5 -0
- package/dist/attest.d.ts +3 -0
- package/dist/attest.js +10 -0
- package/dist/boot-identity.d.ts +2 -1
- package/dist/boot-identity.js +6 -4
- package/dist/bounded-spawn.d.ts +57 -0
- package/dist/bounded-spawn.js +220 -0
- package/dist/browser/terminal.js +42 -42
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +32 -32
- package/dist/browser-crew.js +2 -0
- package/dist/browser-shell.d.ts +1 -1
- package/dist/browser-shell.js +2 -2
- package/dist/browser-workspace.d.ts +50 -2
- package/dist/browser-workspace.js +3 -3
- package/dist/build-review.js +2 -0
- package/dist/builder-restart.d.ts +21 -0
- package/dist/builder-restart.js +50 -0
- package/dist/builder.d.ts +11 -2
- package/dist/builder.js +234 -101
- package/dist/card-catalog.d.ts +209 -0
- package/dist/card-catalog.js +133 -0
- package/dist/card-cli.d.ts +43 -0
- package/dist/card-cli.js +44 -0
- package/dist/card-render.d.ts +26 -0
- package/dist/card-render.js +223 -0
- package/dist/card-sources.d.ts +68 -0
- package/dist/card-sources.js +234 -0
- package/dist/chat-acceptance.js +1 -1
- package/dist/chat-actions.d.ts +106 -9
- package/dist/chat-actions.js +473 -46
- package/dist/chat-approval.d.ts +13 -0
- package/dist/chat-approval.js +39 -9
- package/dist/chat-card-part.d.ts +11 -0
- package/dist/chat-card-part.js +18 -0
- package/dist/chat-card.d.ts +46 -0
- package/dist/chat-card.js +99 -0
- package/dist/chat-channel.d.ts +2 -0
- package/dist/chat-channel.js +37 -14
- package/dist/chat-controls.d.ts +4 -0
- package/dist/chat-controls.js +4 -0
- package/dist/chat-decide.d.ts +10 -16
- package/dist/chat-decide.js +79 -62
- package/dist/chat-delivery.js +13 -4
- package/dist/chat-flow.d.ts +0 -27
- package/dist/chat-flow.js +31 -2
- package/dist/chat-quiet.js +4 -4
- package/dist/chat-task-actions.d.ts +3 -2
- package/dist/chat-task-actions.js +19 -5
- package/dist/chat-terms.d.ts +14 -2
- package/dist/chat-terms.js +35 -0
- package/dist/chat-voice.d.ts +1 -1
- package/dist/chat-voice.js +2 -2
- package/dist/check-facts.d.ts +3 -1
- package/dist/check-facts.js +57 -11
- package/dist/check-failure.d.ts +24 -3
- package/dist/check-failure.js +89 -13
- package/dist/check-levels-ui.d.ts +4 -0
- package/dist/check-levels-ui.js +10 -1
- package/dist/check-load.d.ts +85 -0
- package/dist/check-load.js +147 -0
- package/dist/check-only-standing.d.ts +36 -0
- package/dist/check-only-standing.js +39 -0
- package/dist/check-only.d.ts +15 -0
- package/dist/check-only.js +99 -0
- package/dist/check-repair.d.ts +47 -0
- package/dist/check-repair.js +240 -0
- package/dist/check-report.d.ts +2 -0
- package/dist/check-report.js +1 -1
- package/dist/check-stamp.d.ts +46 -0
- package/dist/check-stamp.js +93 -0
- package/dist/checkout-cleanup.d.ts +36 -5
- package/dist/checkout-cleanup.js +166 -14
- package/dist/claim.d.ts +6 -0
- package/dist/claim.js +13 -2
- package/dist/cli-help.d.ts +4 -1
- package/dist/cli-help.js +49 -30
- package/dist/cli.js +15 -1
- package/dist/code-extract.d.ts +44 -0
- package/dist/code-extract.js +88 -0
- package/dist/code-map-acts.d.ts +24 -0
- package/dist/code-map-acts.js +53 -0
- package/dist/code-map-admission.d.ts +52 -0
- package/dist/code-map-admission.js +295 -0
- package/dist/code-map-build.d.ts +63 -0
- package/dist/code-map-build.js +415 -0
- package/dist/code-map-cli.d.ts +153 -0
- package/dist/code-map-cli.js +103 -0
- package/dist/code-map-main.d.ts +1 -0
- package/dist/code-map-main.js +48 -0
- package/dist/code-map-read.d.ts +106 -0
- package/dist/code-map-read.js +171 -0
- package/dist/code-map-report.d.ts +37 -0
- package/dist/code-map-report.js +95 -0
- package/dist/code-map-shape.d.ts +172 -0
- package/dist/code-map-shape.js +327 -0
- package/dist/code-map-store.d.ts +136 -0
- package/dist/code-map-store.js +233 -0
- package/dist/coding-provider.d.ts +3 -1
- package/dist/coding-provider.js +12 -7
- package/dist/coding-update.js +3 -2
- package/dist/coding-workspace.d.ts +1 -1
- package/dist/coding-workspace.js +8 -8
- package/dist/console-chrome.js +2 -2
- package/dist/containment.d.ts +5 -0
- package/dist/containment.js +14 -1
- package/dist/contracts/chat-actions.d.ts +92 -0
- package/dist/contracts/chat-actions.js +20 -0
- package/dist/contracts/chat-card.d.ts +91 -0
- package/dist/contracts/chat-card.js +54 -0
- package/dist/contracts/chat-content.d.ts +191 -0
- package/dist/contracts/chat-content.js +4 -0
- package/dist/contracts/cli.d.ts +52 -0
- package/dist/contracts/cli.js +4 -0
- package/dist/contracts/console-api.d.ts +12 -3
- package/dist/contracts/console-api.js +12 -3
- package/dist/contracts/flow-propose.d.ts +8 -1
- package/dist/contracts/flow-propose.js +3 -1
- package/dist/contracts/flow-send.d.ts +126 -0
- package/dist/contracts/flow-send.js +3 -0
- package/dist/contracts/gateway-tools.d.ts +17 -0
- package/dist/contracts/gateway-tools.js +3 -1
- package/dist/contracts/lead-tools.d.ts +234 -0
- package/dist/contracts/lead-tools.js +25 -1
- package/dist/contracts/project-knowledge.d.ts +38 -5
- package/dist/contracts/project-knowledge.js +11 -5
- package/dist/contracts/project-memory.d.ts +3 -1
- package/dist/contracts/project-memory.js +3 -2
- package/dist/contracts/storage.d.ts +14 -0
- package/dist/contracts/storage.js +3 -1
- package/dist/contracts/store-json.d.ts +18 -0
- package/dist/contracts/youiyoux-request.d.ts +1 -1
- package/dist/contracts/youiyoux-request.js +1 -1
- package/dist/desktop-access.js +6 -2
- package/dist/discord-chat.js +14 -0
- package/dist/dispatch.d.ts +1 -1
- package/dist/dispatch.js +25 -16
- package/dist/dispose.d.ts +19 -0
- package/dist/dispose.js +55 -9
- package/dist/easy-start.js +2 -3
- package/dist/eval-oracle.js +4 -1
- package/dist/evals.js +2 -1
- package/dist/evidence-pair.d.ts +53 -0
- package/dist/evidence-pair.js +128 -0
- package/dist/exhaustion.d.ts +17 -4
- package/dist/exhaustion.js +44 -4
- package/dist/export.js +1 -1
- package/dist/failing-first.d.ts +31 -0
- package/dist/failing-first.js +44 -0
- package/dist/fallback-schema.d.ts +8 -0
- package/dist/fallback-schema.js +40 -0
- package/dist/fallback.d.ts +138 -0
- package/dist/fallback.js +605 -0
- package/dist/fast-start.d.ts +136 -0
- package/dist/fast-start.js +431 -0
- package/dist/fast-terms.d.ts +9 -0
- package/dist/fast-terms.js +23 -0
- package/dist/fenced-check-coverage.d.ts +62 -0
- package/dist/fenced-check-coverage.js +54 -0
- package/dist/findings.d.ts +224 -0
- package/dist/findings.js +449 -0
- package/dist/first-run.js +1 -1
- package/dist/flaky-ci.d.ts +31 -0
- package/dist/flaky-ci.js +149 -0
- package/dist/flaky-cli.d.ts +60 -0
- package/dist/flaky-cli.js +60 -0
- package/dist/flaky.d.ts +75 -9
- package/dist/flaky.js +255 -0
- package/dist/fleet-schema.d.ts +17 -0
- package/dist/fleet-schema.js +135 -0
- package/dist/flow-browser.js +2 -2
- package/dist/flow-card-task.js +3 -0
- package/dist/flow-code.d.ts +2 -2
- package/dist/flow-code.js +9 -5
- package/dist/flow-result-card.d.ts +22 -0
- package/dist/flow-result-card.js +55 -0
- package/dist/flow-send.js +12 -9
- package/dist/flow-steps.js +1 -1
- package/dist/flow-triggers.d.ts +1 -1
- package/dist/flow-triggers.js +2 -2
- package/dist/flow-unattended.d.ts +26 -0
- package/dist/flow-unattended.js +12 -3
- package/dist/flows-ui.js +10 -2
- package/dist/follow-through-brief.d.ts +12 -0
- package/dist/follow-through-brief.js +31 -0
- package/dist/follow-through-cli.d.ts +23 -0
- package/dist/follow-through-cli.js +153 -0
- package/dist/git-incident.d.ts +16 -0
- package/dist/git-incident.js +69 -6
- package/dist/github-import.d.ts +62 -0
- package/dist/github-import.js +146 -0
- package/dist/guides.js +15 -10
- package/dist/handover-note.d.ts +33 -0
- package/dist/handover-note.js +106 -0
- package/dist/invoke.d.ts +25 -2
- package/dist/invoke.js +157 -58
- package/dist/job-object-helper.ps1 +10 -3
- package/dist/keymap.d.ts +41 -0
- package/dist/keymap.js +51 -0
- package/dist/knowledge-cli.js +1 -1
- package/dist/knowledge-ui.d.ts +21 -4
- package/dist/knowledge-ui.js +75 -7
- package/dist/laptop-background.d.ts +16 -0
- package/dist/laptop-background.js +108 -2
- package/dist/lead-cli.js +26 -24
- package/dist/lead-context.js +1 -1
- package/dist/lead-contract.d.ts +1 -1
- package/dist/lead-contract.js +3 -2
- package/dist/lead-doors.d.ts +4 -0
- package/dist/lead-doors.js +2 -2
- package/dist/lead-progress.js +5 -0
- package/dist/lead-status.d.ts +2 -0
- package/dist/lead-status.js +6 -2
- package/dist/lead-tools.js +162 -34
- package/dist/lead.js +2 -2
- package/dist/lesson-notes.d.ts +81 -0
- package/dist/lesson-notes.js +336 -0
- package/dist/limits-ui.js +1 -1
- package/dist/local-owner.d.ts +1 -1
- package/dist/local-owner.js +16 -0
- package/dist/machine-check.d.ts +16 -0
- package/dist/machine-check.js +26 -0
- package/dist/machine-identity.d.ts +75 -0
- package/dist/machine-identity.js +170 -0
- package/dist/mcp-core.js +11 -1
- package/dist/memory-cli.d.ts +300 -1
- package/dist/memory-cli.js +62 -9
- package/dist/memory-pass.d.ts +16 -0
- package/dist/memory-pass.js +67 -3
- package/dist/memory-tidy.d.ts +181 -0
- package/dist/memory-tidy.js +523 -0
- package/dist/memory-use.d.ts +35 -0
- package/dist/memory-use.js +109 -0
- package/dist/merge-checklist.d.ts +55 -0
- package/dist/merge-checklist.js +182 -0
- package/dist/modes.d.ts +6 -6
- package/dist/modes.js +3 -1
- package/dist/needs-you.d.ts +31 -9
- package/dist/needs-you.js +110 -41
- package/dist/nightly-github.d.ts +2 -0
- package/dist/nightly-github.js +4 -4
- package/dist/operate-remote-arguments.js +7 -1
- package/dist/operate-remote.js +3 -1
- package/dist/operate.d.ts +6 -1
- package/dist/operate.js +565 -64
- package/dist/phase-routing.js +2 -2
- package/dist/plain-schema.d.ts +23 -0
- package/dist/plain-schema.js +94 -0
- package/dist/plain-words.d.ts +20 -0
- package/dist/plain-words.js +66 -0
- package/dist/plan-login.d.ts +14 -0
- package/dist/plan-login.js +58 -0
- package/dist/planner.d.ts +1 -1
- package/dist/planner.js +4 -2
- package/dist/process-liveness.d.ts +2 -0
- package/dist/process-liveness.js +61 -17
- package/dist/project-delete-ui.d.ts +19 -2
- package/dist/project-delete-ui.js +29 -1
- package/dist/project-delete.js +14 -1
- package/dist/project-knowledge.d.ts +18 -11
- package/dist/project-knowledge.js +39 -9
- package/dist/project-learning.d.ts +2 -1
- package/dist/project-learning.js +15 -45
- package/dist/project-memory.d.ts +5 -0
- package/dist/project-memory.js +22 -2
- package/dist/project-tools.d.ts +4 -4
- package/dist/project-tools.js +23 -7
- package/dist/proof.js +30 -5
- package/dist/protected-storage-look.d.ts +1 -0
- package/dist/protected-storage-look.js +13 -0
- package/dist/protected-storage.d.ts +70 -39
- package/dist/protected-storage.js +196 -147
- package/dist/provider-auth.js +1 -1
- package/dist/provider-capture.d.ts +34 -0
- package/dist/provider-capture.js +53 -0
- package/dist/publish.js +9 -1
- package/dist/pull-request-flow.js +5 -0
- package/dist/push.d.ts +18 -2
- package/dist/push.js +40 -16
- package/dist/release-adapters.d.ts +9 -0
- package/dist/release-adapters.js +76 -0
- package/dist/release-auto.js +13 -13
- package/dist/release-card.d.ts +1 -2
- package/dist/release-card.js +11 -0
- package/dist/release-carry.d.ts +7 -2
- package/dist/release-carry.js +49 -10
- package/dist/release-cli.d.ts +4 -0
- package/dist/release-cli.js +29 -22
- package/dist/release-coverage.js +1 -1
- package/dist/release-next.d.ts +48 -0
- package/dist/release-next.js +161 -0
- package/dist/release.d.ts +20 -1
- package/dist/release.js +85 -9
- package/dist/remote-destination.d.ts +36 -0
- package/dist/remote-destination.js +217 -0
- package/dist/remote-main.d.ts +48 -0
- package/dist/remote-main.js +84 -5
- package/dist/repository-context.d.ts +3 -2
- package/dist/repository-context.js +112 -70
- package/dist/restart-certification.d.ts +1 -0
- package/dist/restart-certification.js +8 -6
- package/dist/result-acts.d.ts +27 -19
- package/dist/result-acts.js +36 -25
- package/dist/result-follow-ups.d.ts +2 -2
- package/dist/result-follow-ups.js +18 -4
- package/dist/result-review.d.ts +1 -1
- package/dist/route-change-check.d.ts +4 -0
- package/dist/route-change-check.js +13 -0
- package/dist/route-change.d.ts +22 -0
- package/dist/route-change.js +89 -0
- package/dist/route-hooks.js +3 -2
- package/dist/route-results.d.ts +6 -1
- package/dist/route-results.js +14 -5
- package/dist/run-closeout.d.ts +1 -1
- package/dist/run-closeout.js +7 -5
- package/dist/runner-process.js +3 -2
- package/dist/runner.js +2 -1
- package/dist/scope.d.ts +28 -4
- package/dist/scope.js +92 -28
- package/dist/scout.d.ts +1 -1
- package/dist/scout.js +2 -1
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +26 -18
- package/dist/server/chat.js +16 -14
- package/dist/server/chrome.d.ts +1 -1
- package/dist/server/chrome.js +26 -35
- package/dist/server/console.css +73 -0
- package/dist/server/guards.js +3 -0
- package/dist/server/home.d.ts +6 -0
- package/dist/server/home.js +21 -13
- package/dist/server/http.js +1 -1
- package/dist/server/kit.js +1 -1
- package/dist/server/merge-ready-view.d.ts +10 -0
- package/dist/server/merge-ready-view.js +30 -0
- package/dist/server/pages-assets.d.ts +1 -0
- package/dist/server/pages-assets.js +8 -5
- package/dist/server/pages.js +103 -82
- package/dist/server/render-chat.js +16 -3
- package/dist/server/render-code-map.d.ts +30 -0
- package/dist/server/render-code-map.js +137 -0
- package/dist/server/render-flaky.d.ts +20 -0
- package/dist/server/render-flaky.js +59 -0
- package/dist/server/render-follow-through.d.ts +11 -0
- package/dist/server/render-follow-through.js +68 -0
- package/dist/server/render-history.js +1 -1
- package/dist/server/render-new-task.d.ts +26 -1
- package/dist/server/render-new-task.js +30 -5
- package/dist/server/render-pages.d.ts +1 -175
- package/dist/server/render-pages.js +38 -313
- package/dist/server/render-projects.d.ts +2 -1
- package/dist/server/render-projects.js +9 -7
- package/dist/server/render-release.d.ts +2 -0
- package/dist/server/render-release.js +3 -3
- package/dist/server/render-results.d.ts +2 -0
- package/dist/server/render-results.js +68 -68
- package/dist/server/render-tasks.d.ts +16 -2
- package/dist/server/render-tasks.js +177 -141
- package/dist/server/retired-routes.d.ts +17 -0
- package/dist/server/retired-routes.js +53 -0
- package/dist/server/route-table.d.ts +3 -0
- package/dist/server/route-table.js +48 -12
- package/dist/server/settings.js +166 -34
- package/dist/server/task-views.js +66 -26
- package/dist/server/tasks.d.ts +2 -6
- package/dist/server/tasks.js +250 -321
- package/dist/server/welcome.js +1 -1
- package/dist/settings-groups-ui.d.ts +1 -0
- package/dist/settings-groups-ui.js +4 -2
- package/dist/settings-kit.js +3 -2
- package/dist/settings-proofs.js +3 -1
- package/dist/slack-chat.js +14 -1
- package/dist/steer-mailbox.d.ts +8 -1
- package/dist/steer-mailbox.js +164 -6
- package/dist/stop-proof.js +6 -4
- package/dist/storage-sweep-process.js +1 -1
- package/dist/storage-sweep.d.ts +5 -3
- package/dist/storage-sweep.js +30 -9
- package/dist/storage-ui.js +28 -17
- package/dist/storage.d.ts +27 -2
- package/dist/storage.js +39 -2
- package/dist/store.d.ts +89 -7
- package/dist/store.js +386 -114
- package/dist/surface.js +67 -6
- package/dist/sweep-quarantine.d.ts +1 -0
- package/dist/sweep-quarantine.js +6 -4
- package/dist/tamper-scan.d.ts +4 -0
- package/dist/tamper-scan.js +90 -0
- package/dist/task-aliases.d.ts +5 -0
- package/dist/task-aliases.js +9 -0
- package/dist/task-control.d.ts +2 -1
- package/dist/task-control.js +4 -1
- package/dist/task-outcome-cli.d.ts +2 -0
- package/dist/task-outcome-cli.js +17 -17
- package/dist/task-sizing.d.ts +5 -0
- package/dist/task-sizing.js +2 -0
- package/dist/task-source.d.ts +7 -0
- package/dist/task-source.js +14 -0
- package/dist/task-status.d.ts +9 -0
- package/dist/task-status.js +45 -49
- package/dist/teardown-git.d.ts +18 -0
- package/dist/teardown-git.js +105 -21
- package/dist/telegram-lead.js +23 -6
- package/dist/telegram-progress.js +5 -3
- package/dist/telegram-status.js +6 -4
- package/dist/telegram.js +12 -1
- package/dist/test-infra.d.ts +8 -0
- package/dist/test-infra.js +18 -0
- package/dist/tidy.js +1 -1
- package/dist/toolroll-update.js +19 -5
- package/dist/tui-contract.d.ts +24 -0
- package/dist/tui-source.js +67 -8
- package/dist/tui.js +51 -51
- package/dist/vanished-retry.d.ts +30 -0
- package/dist/vanished-retry.js +174 -0
- package/dist/work-index.d.ts +2 -0
- package/dist/work-index.js +95 -66
- package/dist/work-summary.d.ts +1 -1
- package/dist/work-summary.js +7 -2
- package/dist/workspace-ui.d.ts +7 -3
- package/dist/workspace-ui.js +38 -23
- package/dist/worktree.d.ts +12 -3
- package/dist/worktree.js +91 -14
- package/package.json +2 -1
- package/dist/server/render-next.d.ts +0 -63
- package/dist/server/render-next.js +0 -109
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tidying project memory (docs/plans/learning-loops.md, "Tidy-up pass"): proposals to retire, merge or settle entries,
|
|
3
|
+
* with explicit restore. Proposes; never changes. Nothing happens until a person accepts, and every accept re-reads every
|
|
4
|
+
* affected item in one transaction: any difference and the proposal goes stale.
|
|
5
|
+
*
|
|
6
|
+
* - The scheduled pass (a quiet project, at most weekly, 10 entries or more) is deterministic: it takes no analyzer.
|
|
7
|
+
* A model is asked for contradictions only on a person's request (`proposeTidy` with an analyzer, D4).
|
|
8
|
+
* - The successor chain of a decision is every decision reachable through ordinary supersession and not-undone merges,
|
|
9
|
+
* and its active tail the active ones in it. Restoring past an active tail, or undoing a merge built on, is refused
|
|
10
|
+
* until a person reviews the expanded change: a proposal that names every affected item and says what happens to the
|
|
11
|
+
* tail. Accepting compare-and-sets each one.
|
|
12
|
+
* - Contradictions and a note that keeps failing have no default: a person's choice is required.
|
|
13
|
+
*
|
|
14
|
+
* The only writer of `memory_tidy_proposal` and `decision_merge`. Decisions change only through project-memory.ts, which
|
|
15
|
+
* keeps their revisions and history. A policy path (release-auto.ts EVAL_POLICY). The lead has no tool for any of this.
|
|
16
|
+
*/
|
|
17
|
+
import { createHash } from "node:crypto";
|
|
18
|
+
import { lessonNoteOf, noteAfterwards, watchNote } from "./lesson-notes.js";
|
|
19
|
+
import { currentLocalOwner } from "./local-owner.js";
|
|
20
|
+
import { lastIncludedWords, memoryUseOf } from "./memory-use.js";
|
|
21
|
+
import { personProof } from "./person-proof.js";
|
|
22
|
+
import { changeKnowledge, knowledgeView } from "./project-knowledge.js";
|
|
23
|
+
import { changeLearning, learningIdentity } from "./project-learning.js";
|
|
24
|
+
import { changeDecisionStatus, getDecision, reactivateDecision, recordDecision, refuseLeadMemoryWrite, reindexProjectMemory, retireDecision } from "./project-memory.js";
|
|
25
|
+
import { scanForSecrets } from "./evidence.js";
|
|
26
|
+
const DAY_MS = 86_400_000;
|
|
27
|
+
export const UNUSED_DAYS = 60, UNUSED_RUNS = 20, FAILING_NOTE = 3, TIDY_MIN_ENTRIES = 10;
|
|
28
|
+
export const TIDY_EVERY_MS = 7 * DAY_MS, TIDY_QUIET_MS = DAY_MS;
|
|
29
|
+
const MAX_MODEL_PAIRS = 5;
|
|
30
|
+
/** What a stale proposal says. */
|
|
31
|
+
export const TIDY_STALE = "Changed since proposed. Run Clean up again.";
|
|
32
|
+
const sha = (text) => createHash("sha256").update(text).digest("hex");
|
|
33
|
+
function proposalOf(store, row) {
|
|
34
|
+
const items = JSON.parse(String(row["items_json"]));
|
|
35
|
+
const titles = {};
|
|
36
|
+
for (const item of items)
|
|
37
|
+
if (item.kind === "decision") {
|
|
38
|
+
const found = store.handle.prepare("SELECT claim FROM project_decision WHERE id = ?").get(item.id);
|
|
39
|
+
if (found !== undefined)
|
|
40
|
+
titles[`decision:${item.id}`] = String(found["claim"]);
|
|
41
|
+
}
|
|
42
|
+
return { id: Number(row["id"]), repo: String(row["repo"]), kind: String(row["kind"]), items, afterText: row["after_text"] == null ? null : String(row["after_text"]), rationale: String(row["rationale"]), evidence: JSON.parse(String(row["evidence_json"])),
|
|
43
|
+
source: String(row["source"]), status: String(row["status"]), createdAt: String(row["created_at"]), decidedBy: row["decided_by"] == null ? null : String(row["decided_by"]), titles };
|
|
44
|
+
}
|
|
45
|
+
export function listTidy(store, repo, status = "pending") {
|
|
46
|
+
return store.handle.prepare("SELECT * FROM memory_tidy_proposal WHERE repo = ? AND (? = 'all' OR status = ?) ORDER BY id DESC LIMIT 100").all(repo, status, status).map(row => proposalOf(store, row));
|
|
47
|
+
}
|
|
48
|
+
// ---- current items -------------------------------------------------------------------------------------------------
|
|
49
|
+
function decisionItem(store, repo, id) {
|
|
50
|
+
const row = store.handle.prepare("SELECT revision, status, sha FROM project_decision WHERE id = ? AND repo = ?").get(id, repo);
|
|
51
|
+
return row === undefined ? null : { kind: "decision", id, revision: Number(row["revision"]), status: String(row["status"]), sha: String(row["sha"]) };
|
|
52
|
+
}
|
|
53
|
+
function savedKnowledge(store, repo) {
|
|
54
|
+
const row = store.handle.prepare("SELECT revision, payload FROM project_knowledge WHERE repo = ?").get(repo);
|
|
55
|
+
if (row === undefined)
|
|
56
|
+
return { revision: 0, references: [] };
|
|
57
|
+
try {
|
|
58
|
+
return { revision: Number(row["revision"]), references: JSON.parse(String(row["payload"])).references ?? [] };
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return { revision: Number(row["revision"]), references: [] };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const referenceSha = (ref) => sha(JSON.stringify(ref));
|
|
65
|
+
function lessonItem(store, repo, id) {
|
|
66
|
+
const row = store.handle.prepare("SELECT version, sha FROM project_lesson WHERE id = ? AND repo = ?").get(id, repo);
|
|
67
|
+
return row === undefined ? null : { kind: "lesson", id, version: Number(row["version"]), sha: String(row["sha"]) };
|
|
68
|
+
}
|
|
69
|
+
/** Whether an item reads now exactly as it was bound. */
|
|
70
|
+
function itemStands(store, repo, item) {
|
|
71
|
+
if (item.kind === "decision") {
|
|
72
|
+
const now = decisionItem(store, repo, item.id);
|
|
73
|
+
return now !== null && now.kind === "decision" && now.revision === item.revision && now.status === item.status && now.sha === item.sha;
|
|
74
|
+
}
|
|
75
|
+
if (item.kind === "lesson") {
|
|
76
|
+
const now = lessonItem(store, repo, item.id);
|
|
77
|
+
return now !== null && now.kind === "lesson" && now.version === item.version && now.sha === item.sha;
|
|
78
|
+
}
|
|
79
|
+
const saved = savedKnowledge(store, repo), ref = saved.references.find(one => one.id === item.id);
|
|
80
|
+
return ref !== undefined && saved.revision === item.revision && referenceSha(ref) === item.sha;
|
|
81
|
+
}
|
|
82
|
+
const itemKey = (item) => `${item.kind}:${item.id}`;
|
|
83
|
+
// ---- finding -------------------------------------------------------------------------------------------------------
|
|
84
|
+
const normal = (text) => text.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").trim();
|
|
85
|
+
const dayWords = (iso) => iso.slice(0, 10);
|
|
86
|
+
/** Everything the deterministic pass would propose now. Reads only. */
|
|
87
|
+
function deterministicDrafts(store, repo, now) {
|
|
88
|
+
const drafts = [];
|
|
89
|
+
const cutoff = new Date(now.getTime() - UNUSED_DAYS * DAY_MS).toISOString();
|
|
90
|
+
const use = memoryUseOf(store, repo);
|
|
91
|
+
const runs = Number(store.handle.prepare("SELECT COUNT(DISTINCT run) AS n FROM memory_use WHERE repo = ? AND included_at >= ?").get(repo, cutoff)?.["n"] ?? 0);
|
|
92
|
+
const active = store.handle.prepare("SELECT id, claim, decided_at, source_ref FROM project_decision WHERE repo = ? AND status = 'active' ORDER BY id").all(repo);
|
|
93
|
+
/** Unused: a time older than the window, or never given to a run and older than the window. A row with no time is unknown, so no proposal. */
|
|
94
|
+
const unused = (key, since) => {
|
|
95
|
+
const one = use.get(key);
|
|
96
|
+
if (one === undefined)
|
|
97
|
+
return since < cutoff;
|
|
98
|
+
return one.lastAt !== null && one.lastAt < cutoff;
|
|
99
|
+
};
|
|
100
|
+
const retire = (item, key) => {
|
|
101
|
+
if (item !== null)
|
|
102
|
+
drafts.push({ kind: "retire", items: [item], afterText: null, rationale: `${lastIncludedWords(use.get(key))}, while ${runs} runs got context in the last ${UNUSED_DAYS} days.`, evidence: { runs, days: UNUSED_DAYS }, source: "usage" });
|
|
103
|
+
};
|
|
104
|
+
if (runs >= UNUSED_RUNS) {
|
|
105
|
+
for (const row of active)
|
|
106
|
+
if (unused(`decision:${String(row["id"])}`, String(row["decided_at"])))
|
|
107
|
+
retire(decisionItem(store, repo, Number(row["id"])), `decision:${String(row["id"])}`);
|
|
108
|
+
const saved = savedKnowledge(store, repo);
|
|
109
|
+
const firstSeen = store.handle.prepare("SELECT MIN(at) AS at FROM knowledge_change WHERE repo = ?").get(repo)?.["at"];
|
|
110
|
+
for (const ref of saved.references)
|
|
111
|
+
if (unused(`reference:${ref.id}`, firstSeen == null ? now.toISOString() : String(firstSeen))) {
|
|
112
|
+
retire({ kind: "reference", id: ref.id, revision: saved.revision, sha: referenceSha(ref) }, `reference:${ref.id}`);
|
|
113
|
+
}
|
|
114
|
+
for (const row of store.handle.prepare("SELECT l.id, (SELECT MAX(e.at) FROM learning_event e WHERE e.lesson = l.id AND e.action IN ('adopt','readopt')) AS since FROM project_lesson l WHERE l.repo = ? AND l.status = 'adopted'").all(repo)) {
|
|
115
|
+
if (unused(`lesson:${String(row["id"])}`, row["since"] == null ? now.toISOString() : String(row["since"])))
|
|
116
|
+
retire(lessonItem(store, repo, Number(row["id"])), `lesson:${String(row["id"])}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const groups = new Map();
|
|
120
|
+
for (const row of active) {
|
|
121
|
+
const key = normal(String(row["claim"]));
|
|
122
|
+
if (key !== "")
|
|
123
|
+
groups.set(key, [...(groups.get(key) ?? []), Number(row["id"])]);
|
|
124
|
+
}
|
|
125
|
+
for (const ids of groups.values()) {
|
|
126
|
+
if (ids.length < 2)
|
|
127
|
+
continue;
|
|
128
|
+
const items = ids.flatMap(id => { const item = decisionItem(store, repo, id); return item === null ? [] : [item]; });
|
|
129
|
+
const newest = active.find(row => Number(row["id"]) === Math.max(...ids));
|
|
130
|
+
drafts.push({ kind: "merge", items, afterText: String(newest?.["claim"] ?? ""), rationale: `${ids.length} decisions say the same thing.`, evidence: {}, source: "duplicate" });
|
|
131
|
+
}
|
|
132
|
+
for (const row of active) {
|
|
133
|
+
const after = noteAfterwards(store, { id: Number(row["id"]), decidedAt: String(row["decided_at"]), sourceRef: row["source_ref"] == null ? null : String(row["source_ref"]) });
|
|
134
|
+
const item = decisionItem(store, repo, Number(row["id"]));
|
|
135
|
+
if (after === null || item === null || after.failures - after.mixed < FAILING_NOTE)
|
|
136
|
+
continue;
|
|
137
|
+
drafts.push({ kind: "note-failing", items: [item], afterText: null, rationale: `This note's test failed ${after.failures - after.mixed} times since it was accepted on ${dayWords(after.since)}.`, evidence: { failures: after.failures, mixed: after.mixed, executions: after.executions }, source: "note" });
|
|
138
|
+
}
|
|
139
|
+
return drafts;
|
|
140
|
+
}
|
|
141
|
+
/** Save drafts that aren't waiting already and weren't rejected in this same state. */
|
|
142
|
+
function saveDrafts(store, repo, drafts, now) {
|
|
143
|
+
return store.transact(() => {
|
|
144
|
+
const seen = store.handle.prepare("SELECT kind, status, items_json FROM memory_tidy_proposal WHERE repo = ? AND status IN ('pending','rejected')").all(repo);
|
|
145
|
+
const keyOf = (kind, items) => `${kind}|${items.map(itemKey).sort().join(",")}`;
|
|
146
|
+
const exactOf = (kind, items) => `${kind}|${JSON.stringify([...items].sort((a, b) => itemKey(a).localeCompare(itemKey(b))))}`;
|
|
147
|
+
const waiting = new Set(seen.filter(row => row["status"] === "pending").map(row => keyOf(String(row["kind"]), JSON.parse(String(row["items_json"])))));
|
|
148
|
+
const rejected = new Set(seen.filter(row => row["status"] === "rejected").map(row => exactOf(String(row["kind"]), JSON.parse(String(row["items_json"])))));
|
|
149
|
+
let made = 0;
|
|
150
|
+
for (const draft of drafts) {
|
|
151
|
+
const key = keyOf(draft.kind, draft.items);
|
|
152
|
+
if (waiting.has(key) || rejected.has(exactOf(draft.kind, draft.items)))
|
|
153
|
+
continue;
|
|
154
|
+
store.handle.prepare("INSERT INTO memory_tidy_proposal (repo, kind, items_json, after_text, rationale, evidence_json, source, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?)")
|
|
155
|
+
.run(repo, draft.kind, JSON.stringify(draft.items), draft.afterText, draft.rationale, JSON.stringify(draft.evidence), draft.source, now.toISOString());
|
|
156
|
+
waiting.add(key);
|
|
157
|
+
made++;
|
|
158
|
+
}
|
|
159
|
+
return made;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The scheduled pass: deterministic, no analyzer in reach. Runs when the project has been quiet for a day, at most
|
|
164
|
+
* weekly, with at least 10 entries. Proposes only.
|
|
165
|
+
*/
|
|
166
|
+
export function scheduledTidy(store, repo, now) {
|
|
167
|
+
const key = `memory-tidy:${sha(repo).slice(0, 32)}`;
|
|
168
|
+
if (now.getTime() - store.serviceCursor(key) < TIDY_EVERY_MS)
|
|
169
|
+
return 0;
|
|
170
|
+
const quiet = new Date(now.getTime() - TIDY_QUIET_MS).toISOString();
|
|
171
|
+
if (store.handle.prepare("SELECT 1 FROM run r JOIN task_ref t ON t.id = r.task_ref WHERE t.repo = ? AND (r.finished_at IS NULL OR r.finished_at >= ?) LIMIT 1").get(repo, quiet) !== undefined)
|
|
172
|
+
return 0;
|
|
173
|
+
const entries = Number(store.handle.prepare("SELECT COUNT(*) AS n FROM project_decision WHERE repo = ? AND status = 'active'").get(repo)?.["n"] ?? 0)
|
|
174
|
+
+ savedKnowledge(store, repo).references.length + Number(store.handle.prepare("SELECT COUNT(*) AS n FROM project_lesson WHERE repo = ? AND status = 'adopted'").get(repo)?.["n"] ?? 0);
|
|
175
|
+
if (entries < TIDY_MIN_ENTRIES)
|
|
176
|
+
return 0;
|
|
177
|
+
store.setServiceCursor(key, now.getTime(), now);
|
|
178
|
+
return saveDrafts(store, repo, deterministicDrafts(store, repo, now), now);
|
|
179
|
+
}
|
|
180
|
+
export const CONTRADICTION_SCHEMA = { type: "object", additionalProperties: false, required: ["contradictions"], properties: { contradictions: { type: "array", items: { type: "object", additionalProperties: false, required: ["a", "b", "why"], properties: { a: { type: "integer" }, b: { type: "integer" }, why: { type: "string" } } } } } };
|
|
181
|
+
const CONTRADICTION_PROMPT = `You read a project's settled decisions. Find pairs that can't both hold. Say nothing about style or overlap. Refer to decisions only by id. Return one JSON object and nothing else, matching this JSON Schema: ${JSON.stringify(CONTRADICTION_SCHEMA)}`;
|
|
182
|
+
/** Read a model's pairs: ids of active decisions only, a clean reason, each pair once. Everything else is dropped. */
|
|
183
|
+
export function parseContradictions(text, active) {
|
|
184
|
+
const start = text.indexOf("{"), end = text.lastIndexOf("}");
|
|
185
|
+
if (start < 0 || end <= start)
|
|
186
|
+
return [];
|
|
187
|
+
let raw;
|
|
188
|
+
try {
|
|
189
|
+
raw = JSON.parse(text.slice(start, end + 1));
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
const list = raw?.contradictions;
|
|
195
|
+
if (!Array.isArray(list))
|
|
196
|
+
return [];
|
|
197
|
+
const known = new Set(active), seen = new Set(), pairs = [];
|
|
198
|
+
for (const one of list) {
|
|
199
|
+
const { a, b, why } = (one ?? {});
|
|
200
|
+
if (typeof a !== "number" || typeof b !== "number" || a === b || !known.has(a) || !known.has(b) || typeof why !== "string")
|
|
201
|
+
continue;
|
|
202
|
+
// eslint-disable-next-line no-control-regex
|
|
203
|
+
const reason = why.replace(/[\u0000-\u001f\u007f]+/g, " ").trim().slice(0, 300);
|
|
204
|
+
const key = [a, b].sort((x, y) => x - y).join(",");
|
|
205
|
+
if (reason === "" || scanForSecrets(reason).length > 0 || seen.has(key))
|
|
206
|
+
continue;
|
|
207
|
+
seen.add(key);
|
|
208
|
+
pairs.push({ a, b, why: reason });
|
|
209
|
+
if (pairs.length >= MAX_MODEL_PAIRS)
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
return pairs;
|
|
213
|
+
}
|
|
214
|
+
/** Clean up, on a person's request: the deterministic pass with no quiet gate, and contradictions when an analyzer is given. */
|
|
215
|
+
export async function proposeTidy(store, args, now) {
|
|
216
|
+
refuseLeadMemoryWrite();
|
|
217
|
+
if (!store.accountCanAccess(args.actor, args.repo) || store.accountOf(args.actor)?.role !== "approver")
|
|
218
|
+
throw Error("Project memory is outside your access.");
|
|
219
|
+
const drafts = deterministicDrafts(store, args.repo, now);
|
|
220
|
+
let problem = null;
|
|
221
|
+
if (args.analyzer !== undefined) {
|
|
222
|
+
const active = store.handle.prepare("SELECT id, claim FROM project_decision WHERE repo = ? AND status = 'active' ORDER BY id DESC LIMIT 60").all(args.repo);
|
|
223
|
+
if (active.length >= 2) {
|
|
224
|
+
const answer = await args.analyzer({ system: CONTRADICTION_PROMPT, data: active.map(row => `[${String(row["id"])}] ${String(row["claim"])}`).join("\n") });
|
|
225
|
+
if (!answer.ok)
|
|
226
|
+
problem = answer.problem;
|
|
227
|
+
else
|
|
228
|
+
for (const pair of parseContradictions(answer.text, active.map(row => Number(row["id"])))) {
|
|
229
|
+
const items = [decisionItem(store, args.repo, pair.a), decisionItem(store, args.repo, pair.b)].filter((one) => one !== null);
|
|
230
|
+
drafts.push({ kind: "contradiction", items, afterText: null, rationale: pair.why, evidence: {}, source: "model" });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return { made: saveDrafts(store, args.repo, drafts, now), problem };
|
|
235
|
+
}
|
|
236
|
+
// ---- chains --------------------------------------------------------------------------------------------------------
|
|
237
|
+
/** Every decision reachable from `id` through ordinary supersession and not-undone merges, transitively (never `id`). */
|
|
238
|
+
export function successorChain(store, repo, id) {
|
|
239
|
+
const seen = new Set([id]), order = [], queue = [id];
|
|
240
|
+
while (queue.length > 0) {
|
|
241
|
+
const at = queue.shift();
|
|
242
|
+
const next = [...store.handle.prepare("SELECT id FROM project_decision WHERE repo = ? AND supersedes = ?").all(repo, at).map(row => Number(row["id"])),
|
|
243
|
+
...store.handle.prepare("SELECT successor FROM decision_merge WHERE predecessor = ? AND undone_at IS NULL").all(at).map(row => Number(row["successor"]))];
|
|
244
|
+
for (const one of next)
|
|
245
|
+
if (!seen.has(one)) {
|
|
246
|
+
seen.add(one);
|
|
247
|
+
order.push(one);
|
|
248
|
+
queue.push(one);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return order;
|
|
252
|
+
}
|
|
253
|
+
const statusOf = (store, repo, id) => { const item = decisionItem(store, repo, id); return item === null || item.kind !== "decision" ? null : item.status; };
|
|
254
|
+
/** The active decisions in `id`'s chain. */
|
|
255
|
+
export function activeTail(store, repo, id) {
|
|
256
|
+
return successorChain(store, repo, id).filter(one => statusOf(store, repo, one) === "active");
|
|
257
|
+
}
|
|
258
|
+
// ---- who, and the writes -------------------------------------------------------------------------------------------
|
|
259
|
+
/** A person who may change this project's memory, re-read inside the write. Lead and remote refused; revoked or lost
|
|
260
|
+
* access refused; the laptop's local owner only in its own act; the proof must be a person's. */
|
|
261
|
+
function proveMemoryPerson(store, repo, actor, proof) {
|
|
262
|
+
refuseLeadMemoryWrite();
|
|
263
|
+
if (!store.accountCanAccess(actor, repo) || store.accountOf(actor)?.role !== "approver")
|
|
264
|
+
throw Error("Project memory is outside your access.");
|
|
265
|
+
const local = currentLocalOwner();
|
|
266
|
+
if (local !== null && local.act !== "memory-review")
|
|
267
|
+
throw Error("Sign in with your password to do this.");
|
|
268
|
+
const said = local !== null ? "local-owner" : proof;
|
|
269
|
+
if (!personProof(said))
|
|
270
|
+
throw Error("Project memory is changed by a person with their own sign-in.");
|
|
271
|
+
return said;
|
|
272
|
+
}
|
|
273
|
+
const needDecision = (store, repo, actor, id) => {
|
|
274
|
+
const found = getDecision(store, repo, actor, id);
|
|
275
|
+
if (found === null)
|
|
276
|
+
throw Error("That decision is unavailable.");
|
|
277
|
+
return found;
|
|
278
|
+
};
|
|
279
|
+
/** Back to active: its note, if it had one, is tracked again. */
|
|
280
|
+
function bringBack(store, repo, actor, id, action, reason, now) {
|
|
281
|
+
const back = reactivateDecision(store, repo, actor, id, action, reason, now);
|
|
282
|
+
const note = /^lesson-note:(\d+)$/.exec(back.sourceRef ?? "");
|
|
283
|
+
if (note !== null) {
|
|
284
|
+
const facts = lessonNoteOf(store, Number(note[1]));
|
|
285
|
+
if (facts !== null)
|
|
286
|
+
watchNote(store, { repo, proposal: facts.proposal, identities: facts.identities, decisionRevision: back.revision, now });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/** One new decision from several; each old one superseded with a "merged into" history row; every member listed. */
|
|
290
|
+
export function mergeDecisions(store, args, now) {
|
|
291
|
+
if (new Set(args.ids).size < 2)
|
|
292
|
+
throw Error("Choose at least two decisions to merge.");
|
|
293
|
+
return store.transact(() => {
|
|
294
|
+
proveMemoryPerson(store, args.repo, args.actor, args.proof);
|
|
295
|
+
const olds = args.ids.map(id => needDecision(store, args.repo, args.actor, id));
|
|
296
|
+
if (olds.some(one => one.status !== "active"))
|
|
297
|
+
throw Error("Every decision being merged must be active.");
|
|
298
|
+
const merge = Number(store.handle.prepare("SELECT COALESCE(MAX(merge), 0) + 1 AS next FROM decision_merge").get()["next"]);
|
|
299
|
+
const made = recordDecision(store, { repo: args.repo, actor: args.actor, draft: { claim: args.claim, why: args.why, sourceKind: "manual", sourceRef: `merge:${merge}` } }, now);
|
|
300
|
+
for (const old of olds) {
|
|
301
|
+
changeDecisionStatus(store, args.repo, args.actor, old.id, "superseded", `Merged into decision ${made.id}.`, now);
|
|
302
|
+
store.handle.prepare("INSERT INTO decision_merge (merge, successor, successor_revision, predecessor, predecessor_revision, at, by) VALUES (?, ?, ?, ?, ?, ?, ?)").run(merge, made.id, made.revision, old.id, old.revision, now.toISOString(), args.actor);
|
|
303
|
+
}
|
|
304
|
+
reindexProjectMemory(store, args.repo);
|
|
305
|
+
return made;
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Restore a retired or superseded decision. With no active tail it goes straight back to active. With one, nothing
|
|
310
|
+
* changes: a proposal names every affected item, and a person chooses what happens to the tail.
|
|
311
|
+
*/
|
|
312
|
+
export function restoreDecision(store, args, now) {
|
|
313
|
+
return store.transact(() => {
|
|
314
|
+
proveMemoryPerson(store, args.repo, args.actor, args.proof);
|
|
315
|
+
const decision = needDecision(store, args.repo, args.actor, args.id);
|
|
316
|
+
if (decision.status === "active")
|
|
317
|
+
throw Error("That decision is already active.");
|
|
318
|
+
const tail = activeTail(store, args.repo, args.id);
|
|
319
|
+
if (tail.length === 0) {
|
|
320
|
+
bringBack(store, args.repo, args.actor, args.id, "restored", "Restored by a person.", now);
|
|
321
|
+
reindexProjectMemory(store, args.repo);
|
|
322
|
+
return { restored: true, decision: needDecision(store, args.repo, args.actor, args.id) };
|
|
323
|
+
}
|
|
324
|
+
const members = [args.id, ...successorChain(store, args.repo, args.id)];
|
|
325
|
+
const items = members.flatMap(id => { const item = decisionItem(store, args.repo, id); return item === null ? [] : [item]; });
|
|
326
|
+
const inserted = store.handle.prepare("INSERT INTO memory_tidy_proposal (repo, kind, items_json, after_text, rationale, evidence_json, source, status, created_at) VALUES (?, 'restore', ?, NULL, ?, ?, 'person', 'pending', ?)")
|
|
327
|
+
.run(args.repo, JSON.stringify(items), `Restoring #${args.id} past what replaced it. Choose what happens to ${tail.map(one => `#${one}`).join(", ")}.`, JSON.stringify({ decision: args.id, tail }), now.toISOString());
|
|
328
|
+
return { restored: false, proposal: Number(inserted.lastInsertRowid), items };
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
function mergeRows(store, merge) {
|
|
332
|
+
return store.handle.prepare("SELECT * FROM decision_merge WHERE merge = ?").all(merge).map(row => ({ merge, successor: Number(row["successor"]), successorRevision: Number(row["successor_revision"]), predecessor: Number(row["predecessor"]), predecessorRevision: Number(row["predecessor_revision"]), undoneAt: row["undone_at"] == null ? null : String(row["undone_at"]) }));
|
|
333
|
+
}
|
|
334
|
+
function finishUnmerge(store, repo, actor, rows, now) {
|
|
335
|
+
const successor = rows[0].successor;
|
|
336
|
+
if (statusOf(store, repo, successor) === "active")
|
|
337
|
+
changeDecisionStatus(store, repo, actor, successor, "retired", "Unmerged.", now);
|
|
338
|
+
for (const row of rows)
|
|
339
|
+
if (statusOf(store, repo, row.predecessor) !== "active")
|
|
340
|
+
bringBack(store, repo, actor, row.predecessor, "unmerged", `Unmerged from decision ${successor}.`, now);
|
|
341
|
+
store.handle.prepare("UPDATE decision_merge SET undone_at = ?, undone_by = ? WHERE merge = ? AND undone_at IS NULL").run(now.toISOString(), actor, rows[0].merge);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Undo a merge. Direct only while its successor is active, at the revision recorded at the merge, and not merged on.
|
|
345
|
+
* Otherwise nothing changes: a proposal names every affected item, the later merge included. A merge already undone is
|
|
346
|
+
* refused and changes nothing.
|
|
347
|
+
*/
|
|
348
|
+
export function unmergeDecisions(store, args, now) {
|
|
349
|
+
return store.transact(() => {
|
|
350
|
+
proveMemoryPerson(store, args.repo, args.actor, args.proof);
|
|
351
|
+
const rows = mergeRows(store, args.merge);
|
|
352
|
+
if (rows.length === 0 || decisionItem(store, args.repo, rows[0].successor) === null)
|
|
353
|
+
throw Error("That merge is unavailable.");
|
|
354
|
+
const undone = rows.find(row => row.undoneAt !== null);
|
|
355
|
+
if (undone !== undefined)
|
|
356
|
+
throw Error(`This merge was already undone on ${dayWords(undone.undoneAt)}.`);
|
|
357
|
+
const first = rows[0], successor = decisionItem(store, args.repo, first.successor);
|
|
358
|
+
const mergedOn = store.handle.prepare("SELECT 1 FROM decision_merge WHERE predecessor = ? AND undone_at IS NULL LIMIT 1").get(first.successor) !== undefined;
|
|
359
|
+
const direct = successor !== null && successor.kind === "decision" && successor.status === "active" && successor.revision === first.successorRevision && !mergedOn
|
|
360
|
+
&& rows.every(row => { const one = decisionItem(store, args.repo, row.predecessor); return one !== null && one.kind === "decision" && one.status === "superseded" && one.revision === row.predecessorRevision + 1; });
|
|
361
|
+
if (direct) {
|
|
362
|
+
finishUnmerge(store, args.repo, args.actor, rows, now);
|
|
363
|
+
reindexProjectMemory(store, args.repo);
|
|
364
|
+
return { restored: true, successor: first.successor };
|
|
365
|
+
}
|
|
366
|
+
const tail = activeTail(store, args.repo, first.successor);
|
|
367
|
+
const members = [first.successor, ...rows.map(row => row.predecessor), ...successorChain(store, args.repo, first.successor)];
|
|
368
|
+
const items = [...new Set(members)].flatMap(id => { const item = decisionItem(store, args.repo, id); return item === null ? [] : [item]; });
|
|
369
|
+
const later = store.handle.prepare("SELECT DISTINCT merge FROM decision_merge WHERE predecessor = ? AND undone_at IS NULL").all(first.successor).map(row => Number(row["merge"]));
|
|
370
|
+
const inserted = store.handle.prepare("INSERT INTO memory_tidy_proposal (repo, kind, items_json, after_text, rationale, evidence_json, source, status, created_at) VALUES (?, 'unmerge', ?, NULL, ?, ?, 'person', 'pending', ?)")
|
|
371
|
+
.run(args.repo, JSON.stringify(items), `Decision #${first.successor} changed since the merge${later.length === 0 ? "" : ` and was merged on (${later.map(one => `merge ${one}`).join(", ")})`}.${tail.length === 0 ? "" : ` Choose what happens to ${tail.map(one => `#${one}`).join(", ")}.`}`, JSON.stringify({ merge: args.merge, tail, later }), now.toISOString());
|
|
372
|
+
return { restored: false, proposal: Number(inserted.lastInsertRowid), items };
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
/** A disabled lesson back in use (re-checks its support, bound to its version and sha). */
|
|
376
|
+
export function restoreLesson(store, root, args, now) {
|
|
377
|
+
store.transact(() => {
|
|
378
|
+
proveMemoryPerson(store, args.repo, args.actor, args.proof);
|
|
379
|
+
const item = lessonItem(store, args.repo, args.id);
|
|
380
|
+
if (item === null || item.kind !== "lesson")
|
|
381
|
+
throw Error("Learning lesson is unavailable.");
|
|
382
|
+
const policy = store.handle.prepare("SELECT revision FROM learning_policy WHERE repo = ?").get(args.repo);
|
|
383
|
+
changeLearning(store, root, { repo: args.repo, actor: args.actor, identity: learningIdentity(args.repo), revision: Number(policy?.["revision"] ?? 0), action: "readopt", lesson: args.id, version: item.version, sha: item.sha }, now);
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
/** A reference back from history, as a new knowledge revision bound to the current one. */
|
|
387
|
+
export function restoreReference(store, args, now) {
|
|
388
|
+
store.transact(() => {
|
|
389
|
+
proveMemoryPerson(store, args.repo, args.actor, args.proof);
|
|
390
|
+
const view = knowledgeView(store, args.repo, args.actor);
|
|
391
|
+
changeKnowledge(store, { repo: args.repo, actor: args.actor, identity: view.identity, revision: view.revision, action: "readd", draft: { id: args.id }, restore: args.fromRevision }, now);
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
const required = (value, words) => { const text = (value ?? "").trim(); if (text === "")
|
|
395
|
+
throw Error(words); return text; };
|
|
396
|
+
const tailOf = (proposal) => Array.isArray(proposal.evidence["tail"]) ? proposal.evidence["tail"] : [];
|
|
397
|
+
function applyTidy(store, root, proposal, args, proof, now) {
|
|
398
|
+
const { repo, actor } = args;
|
|
399
|
+
const decisions = proposal.items.filter((one) => one.kind === "decision");
|
|
400
|
+
const reason = proposal.rationale.slice(0, 450);
|
|
401
|
+
switch (proposal.kind) {
|
|
402
|
+
case "retire": {
|
|
403
|
+
const item = proposal.items[0];
|
|
404
|
+
if (item.kind === "decision")
|
|
405
|
+
retireDecision(store, { repo, actor, id: item.id, reason }, now);
|
|
406
|
+
else if (item.kind === "reference") {
|
|
407
|
+
const view = knowledgeView(store, repo, actor);
|
|
408
|
+
changeKnowledge(store, { repo, actor, identity: view.identity, revision: view.revision, action: "remove", draft: { id: item.id } }, now);
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
const policy = store.handle.prepare("SELECT revision FROM learning_policy WHERE repo = ?").get(repo);
|
|
412
|
+
changeLearning(store, root, { repo, actor, identity: learningIdentity(repo), revision: Number(policy?.["revision"] ?? 0), action: "disable", lesson: item.id, version: item.version, sha: item.sha }, now);
|
|
413
|
+
}
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
case "merge":
|
|
417
|
+
mergeDecisions(store, { repo, actor, ids: decisions.map(one => one.id), claim: (args.claim ?? "").trim() || proposal.afterText || "", why: (args.why ?? "").trim() || proposal.rationale, proof }, now);
|
|
418
|
+
return;
|
|
419
|
+
case "contradiction": {
|
|
420
|
+
const choice = required(args.choice, "Choose which stays, keep both, or write a new one.");
|
|
421
|
+
if (choice === "both")
|
|
422
|
+
return;
|
|
423
|
+
if (choice === "new") {
|
|
424
|
+
const claim = required(args.claim, "Write the new decision."), why = required(args.why, "Say why.");
|
|
425
|
+
for (const one of decisions)
|
|
426
|
+
changeDecisionStatus(store, repo, actor, one.id, "retired", "Replaced after a contradiction.", now);
|
|
427
|
+
recordDecision(store, { repo, actor, draft: { claim, why, sourceKind: "manual", sourceRef: `cleanup:${proposal.id}` } }, now);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const kept = /^keep:(\d+)$/.exec(choice);
|
|
431
|
+
if (kept === null || !decisions.some(one => one.id === Number(kept[1])))
|
|
432
|
+
throw Error("Choose which stays, keep both, or write a new one.");
|
|
433
|
+
for (const one of decisions)
|
|
434
|
+
if (one.id !== Number(kept[1]))
|
|
435
|
+
retireDecision(store, { repo, actor, id: one.id, reason: `Contradicted decision ${kept[1]}.` }, now);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
case "note-failing": {
|
|
439
|
+
const choice = required(args.choice, "Choose to rewrite or retire it.");
|
|
440
|
+
const item = decisions[0];
|
|
441
|
+
if (choice === "retire")
|
|
442
|
+
retireDecision(store, { repo, actor, id: item.id, reason }, now);
|
|
443
|
+
else if (choice === "rewrite")
|
|
444
|
+
recordDecision(store, { repo, actor, draft: { claim: required(args.claim, "Write the new note."), why: required(args.why, "Say why."), sourceKind: "manual", sourceRef: `cleanup:${proposal.id}`, supersedes: item.id } }, now);
|
|
445
|
+
else
|
|
446
|
+
throw Error("Choose to rewrite or retire it.");
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
case "restore":
|
|
450
|
+
case "unmerge": {
|
|
451
|
+
const tail = tailOf(proposal);
|
|
452
|
+
const choice = tail.length === 0 ? "keep" : required(args.choice, "Choose what happens to the decisions that replaced it.");
|
|
453
|
+
if (choice !== "retire" && choice !== "keep")
|
|
454
|
+
throw Error("Choose to retire them or keep both active.");
|
|
455
|
+
if (choice === "retire")
|
|
456
|
+
for (const id of tail)
|
|
457
|
+
changeDecisionStatus(store, repo, actor, id, "retired", "Retired by a reviewed restore.", now);
|
|
458
|
+
if (proposal.kind === "restore")
|
|
459
|
+
bringBack(store, repo, actor, Number(proposal.evidence["decision"]), "restored", "Restored after review.", now);
|
|
460
|
+
else {
|
|
461
|
+
const rows = mergeRows(store, Number(proposal.evidence["merge"]));
|
|
462
|
+
if (rows.length === 0 || rows.some(row => row.undoneAt !== null))
|
|
463
|
+
throw Error("That merge is unavailable.");
|
|
464
|
+
finishUnmerge(store, repo, actor, rows, now);
|
|
465
|
+
}
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Accept or reject one proposal: the one writer of a proposal's outcome. In one transaction, a person's standing, the
|
|
472
|
+
* proposal's state and every bound item are re-read; any difference marks it stale and changes nothing.
|
|
473
|
+
*/
|
|
474
|
+
export function decideTidy(store, root, args, now) {
|
|
475
|
+
refuseLeadMemoryWrite();
|
|
476
|
+
const outcome = store.transact(() => {
|
|
477
|
+
const proof = proveMemoryPerson(store, args.repo, args.actor, args.proof);
|
|
478
|
+
const row = store.handle.prepare("SELECT * FROM memory_tidy_proposal WHERE id = ? AND repo = ?").get(args.id, args.repo);
|
|
479
|
+
if (row === undefined)
|
|
480
|
+
throw Error("That proposal is unavailable.");
|
|
481
|
+
const proposal = proposalOf(store, row);
|
|
482
|
+
if (proposal.status !== "pending")
|
|
483
|
+
throw Error(proposal.status === "stale" ? TIDY_STALE : "That proposal was already decided.");
|
|
484
|
+
const at = now.toISOString();
|
|
485
|
+
if (args.decision === "reject") {
|
|
486
|
+
store.handle.prepare("UPDATE memory_tidy_proposal SET status = 'rejected', decided_by = ?, decided_proof = ?, decided_at = ? WHERE id = ?").run(args.actor, proof, at, proposal.id);
|
|
487
|
+
return { ...proposal, status: "rejected", decidedBy: args.actor };
|
|
488
|
+
}
|
|
489
|
+
if (!proposal.items.every(item => itemStands(store, args.repo, item))) {
|
|
490
|
+
store.handle.prepare("UPDATE memory_tidy_proposal SET status = 'stale', decided_at = ? WHERE id = ?").run(at, proposal.id);
|
|
491
|
+
return "stale";
|
|
492
|
+
}
|
|
493
|
+
applyTidy(store, root, proposal, args, proof, now);
|
|
494
|
+
reindexProjectMemory(store, args.repo);
|
|
495
|
+
store.handle.prepare("UPDATE memory_tidy_proposal SET status = 'accepted', decided_by = ?, decided_proof = ?, decided_at = ? WHERE id = ?").run(args.actor, proof, at, proposal.id);
|
|
496
|
+
return { ...proposal, status: "accepted", decidedBy: args.actor };
|
|
497
|
+
});
|
|
498
|
+
if (outcome === "stale")
|
|
499
|
+
throw Error(TIDY_STALE);
|
|
500
|
+
return outcome;
|
|
501
|
+
}
|
|
502
|
+
/** Merges that can still be undone, for the page. */
|
|
503
|
+
export function undoableMerges(store, repo) {
|
|
504
|
+
const rows = store.handle.prepare("SELECT m.merge, m.successor, m.predecessor FROM decision_merge m JOIN project_decision d ON d.id = m.successor WHERE d.repo = ? AND m.undone_at IS NULL ORDER BY m.merge DESC LIMIT 200").all(repo);
|
|
505
|
+
const merges = new Map();
|
|
506
|
+
for (const row of rows) {
|
|
507
|
+
const id = Number(row["merge"]), one = merges.get(id) ?? { merge: id, successor: Number(row["successor"]), predecessors: [] };
|
|
508
|
+
one.predecessors.push(Number(row["predecessor"]));
|
|
509
|
+
merges.set(id, one);
|
|
510
|
+
}
|
|
511
|
+
return [...merges.values()];
|
|
512
|
+
}
|
|
513
|
+
/** Lessons that were disabled and can be used again, for the page. */
|
|
514
|
+
export function disabledLessons(store, repo) {
|
|
515
|
+
return store.handle.prepare("SELECT id, payload FROM project_lesson WHERE repo = ? AND status = 'disabled' ORDER BY id DESC LIMIT 50").all(repo).flatMap(row => {
|
|
516
|
+
try {
|
|
517
|
+
return [{ id: Number(row["id"]), observation: String(JSON.parse(String(row["payload"])).observation ?? "") }];
|
|
518
|
+
}
|
|
519
|
+
catch {
|
|
520
|
+
return [];
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type KnowledgeSelection } from "./contracts/project-knowledge.js";
|
|
2
|
+
import type { Store } from "./store.js";
|
|
3
|
+
export type MemoryKind = "lesson" | "reference" | "decision";
|
|
4
|
+
export type MemoryUse = {
|
|
5
|
+
lastRun: number;
|
|
6
|
+
lastAt: string | null;
|
|
7
|
+
runs: number;
|
|
8
|
+
};
|
|
9
|
+
/** A run's role as a phase of the work. */
|
|
10
|
+
export declare const phaseOfRole: (role: string) => "plan" | "review" | "build";
|
|
11
|
+
/** Every reference and decision in a run's frozen knowledge. Called in the transaction that freezes it; advisory, so a
|
|
12
|
+
* failure leaves the run's context exactly as it was. */
|
|
13
|
+
export declare function recordKnowledgeUse(store: Store, input: {
|
|
14
|
+
run: number;
|
|
15
|
+
repo: string;
|
|
16
|
+
role: string;
|
|
17
|
+
selection: KnowledgeSelection;
|
|
18
|
+
}): void;
|
|
19
|
+
/** Every lesson in a run's frozen learning context. As recordKnowledgeUse. */
|
|
20
|
+
export declare function recordLessonUse(store: Store, input: {
|
|
21
|
+
run: number;
|
|
22
|
+
repo: string;
|
|
23
|
+
payload: string;
|
|
24
|
+
at: string;
|
|
25
|
+
}): void;
|
|
26
|
+
/**
|
|
27
|
+
* Backfill from snapshots older than this feature, a bounded batch at a time (the watch loop's learning pass): each
|
|
28
|
+
* snapshot's sha is verified first. Lesson rows take the snapshot's own time and version; knowledge rows from a snapshot
|
|
29
|
+
* with no `capturedAt` have no version and no time. Returns how many snapshots were read.
|
|
30
|
+
*/
|
|
31
|
+
export declare function backfillMemoryUse(store: Store, now: Date, limit?: number): number;
|
|
32
|
+
/** A project's usage, by `<kind>:<item>`: its newest run, that run's time and how many runs had it. */
|
|
33
|
+
export declare function memoryUseOf(store: Store, repo: string): Map<string, MemoryUse>;
|
|
34
|
+
/** "Last included in a run: #4410, Nov 3 · 41 runs"; "#2210 (time not recorded)" for a backfilled row; "Not included yet". */
|
|
35
|
+
export declare function lastIncludedWords(use: MemoryUse | undefined): string;
|