projectkeeper 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/LICENSE +21 -0
- package/README.md +283 -2
- package/README.zh-CN.md +285 -0
- package/THIRD_PARTY_NOTICES.md +53 -0
- package/bin/pk.mjs +6 -0
- package/bin/postinstall.mjs +5 -0
- package/bin/strip-types.mjs +28 -0
- package/package.json +68 -4
- package/patches/README.md +111 -0
- package/patches/pi-ai-0.85.0.ts +122 -0
- package/patches/pi-ai-0.87.1.ts +122 -0
- package/scripts/apply-pi-streaming-patch.ts +86 -0
- package/skills/clerk/coverage/SKILL.md +35 -0
- package/skills/clerk/cross-check/SKILL.md +118 -0
- package/skills/clerk/deepen-dispatch/SKILL.md +69 -0
- package/skills/clerk/lane-follow-up/SKILL.md +94 -0
- package/skills/clerk/lane-plan/SKILL.md +107 -0
- package/skills/clerk/lane-slot/SKILL.md +113 -0
- package/skills/clerk/lane-topic/SKILL.md +116 -0
- package/skills/clerk/orientation/SKILL.md +160 -0
- package/skills/clerk/reconcile/SKILL.md +77 -0
- package/skills/clerk/skeleton-dispatch/SKILL.md +43 -0
- package/skills/clerk/spot-check/SKILL.md +52 -0
- package/skills/clerk/synthesis/SKILL.md +65 -0
- package/skills/table-follow-up/SKILL.md +42 -0
- package/src/cli.ts +319 -0
- package/src/codemap/README.md +13 -0
- package/src/codemap/engines.ts +102 -0
- package/src/codemap/facts.ts +504 -0
- package/src/context/agent-entry.ts +232 -0
- package/src/context/ask.ts +97 -0
- package/src/context/assemble.ts +1058 -0
- package/src/context/code-source.ts +103 -0
- package/src/context/k-briefs.ts +114 -0
- package/src/context/lineage.ts +100 -0
- package/src/context/object-brief.ts +300 -0
- package/src/context/owner-words.ts +191 -0
- package/src/context/pack-fixture.ts +342 -0
- package/src/context/project-rules.ts +160 -0
- package/src/intake/intake.ts +392 -0
- package/src/intake/material-rules.ts +166 -0
- package/src/intake/removal.ts +316 -0
- package/src/intake/skipped.ts +72 -0
- package/src/keeper/adjustment.ts +565 -0
- package/src/keeper/bounds/boundary.ts +509 -0
- package/src/keeper/bounds/command.ts +1665 -0
- package/src/keeper/bounds/paths.ts +164 -0
- package/src/keeper/bounds/reads.ts +718 -0
- package/src/keeper/bounds/shell-write-guard.ts +521 -0
- package/src/keeper/capabilities.ts +136 -0
- package/src/keeper/clerk-steps.ts +151 -0
- package/src/keeper/clerk-tools.ts +1424 -0
- package/src/keeper/conversation.ts +253 -0
- package/src/keeper/evidence.ts +662 -0
- package/src/keeper/fake-provider.ts +132 -0
- package/src/keeper/held.ts +15 -0
- package/src/keeper/history-tools.ts +163 -0
- package/src/keeper/jobs/answering.ts +53 -0
- package/src/keeper/key-check.ts +105 -0
- package/src/keeper/ledger-tools.ts +295 -0
- package/src/keeper/link-check.ts +185 -0
- package/src/keeper/merge.ts +260 -0
- package/src/keeper/note-origin.ts +56 -0
- package/src/keeper/numbers-check.ts +102 -0
- package/src/keeper/open-in-pi.ts +57 -0
- package/src/keeper/organize/absence.ts +266 -0
- package/src/keeper/organize/carried-on.ts +214 -0
- package/src/keeper/organize/clerk-coverage.ts +293 -0
- package/src/keeper/organize/clerk-prompts.ts +132 -0
- package/src/keeper/organize/clerk.ts +1395 -0
- package/src/keeper/organize/coverage-gate.ts +19 -0
- package/src/keeper/organize/coverage-tools.ts +765 -0
- package/src/keeper/organize/entries.ts +153 -0
- package/src/keeper/organize/entry-gate.ts +289 -0
- package/src/keeper/organize/fill-tools.ts +718 -0
- package/src/keeper/organize/follow-up.ts +280 -0
- package/src/keeper/organize/generation-check.ts +504 -0
- package/src/keeper/organize/graph.ts +230 -0
- package/src/keeper/organize/lane-tools.ts +289 -0
- package/src/keeper/organize/looked-tools.ts +54 -0
- package/src/keeper/organize/materials.ts +243 -0
- package/src/keeper/organize/owner-lines.ts +493 -0
- package/src/keeper/organize/place-tools.ts +216 -0
- package/src/keeper/organize/placement-inference.ts +1045 -0
- package/src/keeper/organize/placement-time.ts +320 -0
- package/src/keeper/organize/placing.ts +821 -0
- package/src/keeper/organize/positioning.ts +33 -0
- package/src/keeper/organize/readiness.ts +47 -0
- package/src/keeper/organize/reading.ts +401 -0
- package/src/keeper/organize/round-blocks.ts +65 -0
- package/src/keeper/organize/round-news.ts +164 -0
- package/src/keeper/organize/round-open.ts +586 -0
- package/src/keeper/organize/schedule-state.ts +39 -0
- package/src/keeper/organize/schedule.ts +137 -0
- package/src/keeper/organize/service.ts +409 -0
- package/src/keeper/organize/skills.ts +116 -0
- package/src/keeper/organize/slots.ts +85 -0
- package/src/keeper/organize/stage-gate.ts +175 -0
- package/src/keeper/organize/stage-tools.ts +471 -0
- package/src/keeper/organize/standing-notes.ts +141 -0
- package/src/keeper/organize/table-follow-up.ts +330 -0
- package/src/keeper/organize/takeover-clerk.ts +832 -0
- package/src/keeper/organize/takeover-state.ts +103 -0
- package/src/keeper/organize/takeover.ts +216 -0
- package/src/keeper/organize/update-pending.ts +535 -0
- package/src/keeper/organize/workbench-placement.ts +92 -0
- package/src/keeper/own-keys.ts +114 -0
- package/src/keeper/owner-text.ts +165 -0
- package/src/keeper/project-folder.ts +334 -0
- package/src/keeper/prompts.ts +101 -0
- package/src/keeper/references.ts +177 -0
- package/src/keeper/relook/compose.ts +120 -0
- package/src/keeper/relook/prompts.ts +36 -0
- package/src/keeper/roles.ts +259 -0
- package/src/keeper/round-tools.ts +227 -0
- package/src/keeper/route.ts +148 -0
- package/src/keeper/rules.ts +97 -0
- package/src/keeper/runtime.ts +1858 -0
- package/src/keeper/scope-tools.ts +194 -0
- package/src/keeper/sendback-text.ts +83 -0
- package/src/keeper/step-timing.ts +207 -0
- package/src/keeper/testing/clerk-script.ts +212 -0
- package/src/keeper/testing/streaming-arguments.ts +57 -0
- package/src/keeper/tools.ts +2340 -0
- package/src/keeper/utterance-tools.ts +64 -0
- package/src/keeper/verbatim.ts +397 -0
- package/src/ledger/adapters.ts +186 -0
- package/src/ledger/arrangements.ts +257 -0
- package/src/ledger/blame.ts +58 -0
- package/src/ledger/code-engine-child.ts +69 -0
- package/src/ledger/code-engine.ts +548 -0
- package/src/ledger/code.ts +607 -0
- package/src/ledger/docs.ts +294 -0
- package/src/ledger/git-read.ts +321 -0
- package/src/ledger/index.ts +1850 -0
- package/src/ledger/item-range.ts +16 -0
- package/src/ledger/lines.ts +328 -0
- package/src/ledger/numbering.ts +269 -0
- package/src/ledger/ranges.ts +74 -0
- package/src/ledger/rebuild-worker.ts +15 -0
- package/src/ledger/rebuild.ts +387 -0
- package/src/ledger/repo-scan.ts +181 -0
- package/src/ledger/schema.ts +450 -0
- package/src/ledger/sessions.ts +134 -0
- package/src/ledger/text-scan.ts +466 -0
- package/src/ledger/time.ts +185 -0
- package/src/ledger/ts-symbols.ts +180 -0
- package/src/ledger/views.ts +377 -0
- package/src/mcp/server.ts +127 -0
- package/src/mcp/tools.ts +187 -0
- package/src/model/ids.ts +23 -0
- package/src/model/k-types.ts +912 -0
- package/src/model/time.ts +85 -0
- package/src/model/types.ts +1382 -0
- package/src/model/views-k.ts +707 -0
- package/src/model/vocab.ts +269 -0
- package/src/process/analysis.ts +79 -0
- package/src/process/breakpoint-candidates.ts +383 -0
- package/src/process/breakpoints.ts +746 -0
- package/src/process/claim-check.ts +295 -0
- package/src/process/delivery-links.ts +52 -0
- package/src/process/facts.ts +487 -0
- package/src/process/generations.ts +138 -0
- package/src/process/index.ts +243 -0
- package/src/process/integration.ts +128 -0
- package/src/process/placement.ts +107 -0
- package/src/process/plan.ts +276 -0
- package/src/process/sendbacks.ts +244 -0
- package/src/process/text.ts +185 -0
- package/src/process/units.ts +543 -0
- package/src/process/unnumbered.ts +431 -0
- package/src/process/work.ts +884 -0
- package/src/scope/candidates.ts +86 -0
- package/src/scope/decisions.ts +229 -0
- package/src/scope/discover.ts +741 -0
- package/src/scope/ignore.ts +126 -0
- package/src/scope/ignored-place.ts +93 -0
- package/src/scope/skip.ts +161 -0
- package/src/scope/toolchain.ts +180 -0
- package/src/scope/worktree.ts +187 -0
- package/src/server/api.ts +549 -0
- package/src/server/app.ts +595 -0
- package/src/server/drafts-api.ts +170 -0
- package/src/server/folders-api.ts +243 -0
- package/src/server/graph-view.ts +639 -0
- package/src/server/http.ts +224 -0
- package/src/server/k-agent-api.ts +56 -0
- package/src/server/k-rounds-d99.ts +91 -0
- package/src/server/k-views.ts +405 -0
- package/src/server/keeper-page.ts +446 -0
- package/src/server/keys-api.ts +239 -0
- package/src/server/project-folder-api.ts +104 -0
- package/src/server/vendor.ts +14 -0
- package/src/server/workbench-content.ts +561 -0
- package/src/sources/anchor.ts +129 -0
- package/src/sources/files.ts +267 -0
- package/src/sources/gitobs.ts +41 -0
- package/src/sources/history.ts +231 -0
- package/src/sources/scan-worker.ts +10 -0
- package/src/sources/sessions/locate.ts +292 -0
- package/src/sources/sessions/read.ts +346 -0
- package/src/sources/sessions/scope.ts +42 -0
- package/src/sources/sessions/utterances.ts +223 -0
- package/src/sources/watch.ts +215 -0
- package/src/store/json-file.ts +50 -0
- package/src/store/paths.ts +39 -0
- package/src/store/project-store.ts +295 -0
- package/src/store/versions.ts +172 -0
- package/src/store/workspace.ts +216 -0
- package/src/table/check.ts +333 -0
- package/src/table/follow-up-tools.ts +30 -0
- package/src/table/follow-up.ts +344 -0
- package/src/table/import.ts +610 -0
- package/src/table/project-home.ts +61 -0
- package/src/table/sessions.ts +168 -0
- package/src/table/takeover.ts +483 -0
- package/src/util/git-check.ts +29 -0
- package/src/util/git.ts +411 -0
- package/src/util/own-ts.ts +15 -0
- package/src/util/paths.ts +225 -0
- package/template/FILLING-IN.md +148 -0
- package/template/examples/papertrail.json +747 -0
- package/template/projectkeeper.schema.json +1001 -0
- package/ui/app.js +1135 -0
- package/ui/failures.js +82 -0
- package/ui/flyout.js +120 -0
- package/ui/folder-chooser.js +264 -0
- package/ui/glossary.js +156 -0
- package/ui/graph-fit.js +157 -0
- package/ui/graph-material.js +61 -0
- package/ui/graph-palette.js +109 -0
- package/ui/graph-tools.js +62 -0
- package/ui/graph.js +1712 -0
- package/ui/index.html +31 -0
- package/ui/k/code-view.js +230 -0
- package/ui/k/draft-view.js +93 -0
- package/ui/k/k-views.css +224 -0
- package/ui/k/round-tree.js +450 -0
- package/ui/k/scope-k.js +159 -0
- package/ui/k-data.js +118 -0
- package/ui/k-fold.js +106 -0
- package/ui/k-process.css +302 -0
- package/ui/k-process.js +934 -0
- package/ui/keeper-activity.js +175 -0
- package/ui/keeper-view.js +801 -0
- package/ui/keeper.css +101 -0
- package/ui/key-order.js +62 -0
- package/ui/list-fold.js +67 -0
- package/ui/markdown.d.ts +21 -0
- package/ui/markdown.js +472 -0
- package/ui/placement.d.ts +92 -0
- package/ui/placement.js +445 -0
- package/ui/popover-place.d.ts +16 -0
- package/ui/popover-place.js +89 -0
- package/ui/popover.js +198 -0
- package/ui/project-folder.js +37 -0
- package/ui/shell-state.js +40 -0
- package/ui/strip-rows.js +171 -0
- package/ui/styles.css +492 -0
- package/ui/theme-pick.js +36 -0
- package/ui/theme.js +74 -0
- package/ui/themes/LICENSE.md +13 -0
- package/ui/themes/README.md +44 -0
- package/ui/themes/TOKENS.md +291 -0
- package/ui/themes/a1/board.css +1557 -0
- package/ui/themes/a1/pk.css +392 -0
- package/ui/themes/b1/board.css +1163 -0
- package/ui/themes/b1/pk.css +218 -0
- package/ui/themes/d2/board.css +518 -0
- package/ui/themes/d2/pk.css +243 -0
- package/ui/themes/guard.css +73 -0
- package/ui/themes/i1/board.css +776 -0
- package/ui/themes/i1/pk.css +223 -0
- package/ui/themes/manifest.json +13 -0
- package/ui/views.js +2796 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The schedule of daily organizing (Spec §3.8, §6.10; D105; CKC-07 AC-18, AC-28~AC-30).
|
|
3
|
+
*
|
|
4
|
+
* The owner sets how often a round starts by itself and at what time of day, on the `Daily` page: `Off`, `Every day`,
|
|
5
|
+
* `On selected days`, `Every N days` — each of the last three with a time — or `Continuous`. Times are this machine's
|
|
6
|
+
* local time. A project whose schedule was never saved runs `Every day` at the time of day its takeover finished.
|
|
7
|
+
*
|
|
8
|
+
* Everything here is computed: which scheduled time comes next, which one was the last to pass, and whether one is due.
|
|
9
|
+
* The planner (clerk.ts) asks `scheduledDue` and stamps what it did with the time on the project (`scheduleHandled`), so
|
|
10
|
+
* a time that passed while ProjectKeeper was not running, or the machine slept, is made up for with one round however
|
|
11
|
+
* many were missed, and no time is dealt with twice.
|
|
12
|
+
*/
|
|
13
|
+
import type { OrganizeSchedule, Project, ScheduleFrequency } from '../../model/types.ts';
|
|
14
|
+
|
|
15
|
+
export const SCHEDULE_FREQUENCIES: readonly ScheduleFrequency[] = ['Off', 'Every day', 'On selected days', 'Every N days', 'Continuous'];
|
|
16
|
+
/** The frequencies that start a round at a time of day. */
|
|
17
|
+
export const TIMED_FREQUENCIES: readonly ScheduleFrequency[] = ['Every day', 'On selected days', 'Every N days'];
|
|
18
|
+
export const DAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] as const;
|
|
19
|
+
const DAY_MS = 24 * 60 * 60_000;
|
|
20
|
+
/** How far a search for a scheduled time looks: more than the longest `Every N days` allowed. */
|
|
21
|
+
const MAX_EVERY_DAYS = 365;
|
|
22
|
+
|
|
23
|
+
const pad = (n: number) => String(n).padStart(2, '0');
|
|
24
|
+
/** The local time of day of an instant, `HH:MM`. */
|
|
25
|
+
export const localTime = (at: Date): string => `${pad(at.getHours())}:${pad(at.getMinutes())}`;
|
|
26
|
+
/** The local date of an instant, `YYYY-MM-DD`. */
|
|
27
|
+
export const localDate = (at: Date): string => `${at.getFullYear()}-${pad(at.getMonth() + 1)}-${pad(at.getDate())}`;
|
|
28
|
+
|
|
29
|
+
const parseTime = (time: string): { h: number; m: number } | null => {
|
|
30
|
+
const x = /^([01]?\d|2[0-3]):([0-5]\d)$/.exec(time.trim());
|
|
31
|
+
return x ? { h: Number(x[1]), m: Number(x[2]) } : null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** What the owner sent from the `Daily` page, checked: the schedule to save, or why it cannot be saved. */
|
|
35
|
+
export function parseSchedule(input: unknown, now = new Date()): { schedule: OrganizeSchedule } | { error: string } {
|
|
36
|
+
const b = (input ?? {}) as Record<string, unknown>;
|
|
37
|
+
const frequency = b.frequency;
|
|
38
|
+
if (typeof frequency !== 'string' || !(SCHEDULE_FREQUENCIES as readonly string[]).includes(frequency)) return { error: `frequency must be one of ${SCHEDULE_FREQUENCIES.join(', ')}` };
|
|
39
|
+
const f = frequency as ScheduleFrequency;
|
|
40
|
+
const timed = TIMED_FREQUENCIES.includes(f);
|
|
41
|
+
const time = typeof b.time === 'string' ? b.time.trim() : '';
|
|
42
|
+
const t = parseTime(time);
|
|
43
|
+
if (timed && !t) return { error: 'time must be a time of day, HH:MM' };
|
|
44
|
+
const base = { frequency: f, time: t ? `${pad(t.h)}:${pad(t.m)}` : '00:00', savedAt: now.toISOString() };
|
|
45
|
+
if (f === 'On selected days') {
|
|
46
|
+
const days = Array.isArray(b.days) ? [...new Set(b.days.map(Number).filter((d) => Number.isInteger(d) && d >= 0 && d <= 6))].sort((x, y) => x - y) : [];
|
|
47
|
+
if (!days.length) return { error: 'Choose at least one day of the week' };
|
|
48
|
+
return { schedule: { ...base, days } };
|
|
49
|
+
}
|
|
50
|
+
if (f === 'Every N days') {
|
|
51
|
+
const n = Number(b.everyDays);
|
|
52
|
+
if (!Number.isInteger(n) || n < 1 || n > MAX_EVERY_DAYS) return { error: `everyDays must be a whole number from 1 to ${MAX_EVERY_DAYS}` };
|
|
53
|
+
return { schedule: { ...base, everyDays: n, anchor: localDate(now) } };
|
|
54
|
+
}
|
|
55
|
+
return { schedule: base };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The schedule in force for a project. One the owner saved stands as saved. Otherwise the default (§3.8): `Every day` at
|
|
60
|
+
* the time of day the takeover finished — which is also what a home from before D105 gets from its `Daily` rhythm (its
|
|
61
|
+
* last round's end, when there is one: a day after the last round is what `Daily` meant); its `Continuous` stays.
|
|
62
|
+
* `takeoverDoneAt` is when the takeover finished, `lastRoundEnd` when the project's latest round ended.
|
|
63
|
+
*/
|
|
64
|
+
export function scheduleOf(project: Pick<Project, 'schedule' | 'organizeRhythm'>, when: { readonly takeoverDoneAt?: string | null; readonly lastRoundEnd?: string | null } = {}): OrganizeSchedule {
|
|
65
|
+
if (project.schedule) return project.schedule;
|
|
66
|
+
if (project.organizeRhythm === 'Continuous') return { frequency: 'Continuous', time: '00:00', savedAt: null };
|
|
67
|
+
const from = when.lastRoundEnd ?? when.takeoverDoneAt ?? null;
|
|
68
|
+
return { frequency: 'Every day', time: from ? localTime(new Date(from)) : '09:00', savedAt: null };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Whether a local calendar day is one the schedule starts a round on. */
|
|
72
|
+
function onDay(s: OrganizeSchedule, day: Date): boolean {
|
|
73
|
+
if (s.frequency === 'Every day') return true;
|
|
74
|
+
if (s.frequency === 'On selected days') return (s.days ?? []).includes(day.getDay());
|
|
75
|
+
if (s.frequency === 'Every N days') {
|
|
76
|
+
const n = Math.max(1, s.everyDays ?? 1);
|
|
77
|
+
const a = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s.anchor ?? '');
|
|
78
|
+
const anchor = a ? new Date(Number(a[1]), Number(a[2]) - 1, Number(a[3])) : new Date(day.getFullYear(), day.getMonth(), day.getDate());
|
|
79
|
+
// Whole local days between the two dates; rounding absorbs the hour a daylight-saving change adds or removes.
|
|
80
|
+
const days = Math.round((new Date(day.getFullYear(), day.getMonth(), day.getDate()).getTime() - anchor.getTime()) / DAY_MS);
|
|
81
|
+
return days >= 0 && days % n === 0;
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The scheduled time on a local calendar day. */
|
|
87
|
+
function slotOn(s: OrganizeSchedule, day: Date): Date {
|
|
88
|
+
const t = parseTime(s.time) ?? { h: 0, m: 0 };
|
|
89
|
+
return new Date(day.getFullYear(), day.getMonth(), day.getDate(), t.h, t.m, 0, 0);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The first scheduled time strictly after `after`; null when the schedule starts nothing by the clock. */
|
|
93
|
+
export function nextSlot(s: OrganizeSchedule, after: Date): Date | null {
|
|
94
|
+
if (!TIMED_FREQUENCIES.includes(s.frequency)) return null;
|
|
95
|
+
for (let i = 0; i <= MAX_EVERY_DAYS + 7; i++) {
|
|
96
|
+
const day = new Date(after.getFullYear(), after.getMonth(), after.getDate() + i);
|
|
97
|
+
if (!onDay(s, day)) continue;
|
|
98
|
+
const slot = slotOn(s, day);
|
|
99
|
+
if (slot.getTime() > after.getTime()) return slot;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The latest scheduled time at or before `now`, no earlier than `notBefore`; null when none has passed since then. */
|
|
105
|
+
export function lastSlot(s: OrganizeSchedule, now: Date, notBefore: Date): Date | null {
|
|
106
|
+
if (!TIMED_FREQUENCIES.includes(s.frequency)) return null;
|
|
107
|
+
for (let i = 0; i <= MAX_EVERY_DAYS + 7; i++) {
|
|
108
|
+
const day = new Date(now.getFullYear(), now.getMonth(), now.getDate() - i);
|
|
109
|
+
const slot = slotOn(s, day);
|
|
110
|
+
if (slot.getTime() <= notBefore.getTime()) return null;
|
|
111
|
+
if (onDay(s, day) && slot.getTime() <= now.getTime()) return slot;
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Whether a scheduled time has come that was not dealt with yet (§3.8 到了时间). `since` is the later of: when the
|
|
118
|
+
* schedule was saved, when the takeover finished, and the last scheduled time already dealt with — times before it are
|
|
119
|
+
* not made up for. `missed` counts the scheduled times that passed since then; however many, one round makes up for them.
|
|
120
|
+
*/
|
|
121
|
+
export function scheduledDue(s: OrganizeSchedule, now: Date, since: Date): { readonly slot: Date; readonly missed: number } | null {
|
|
122
|
+
const slot = lastSlot(s, now, since);
|
|
123
|
+
if (!slot) return null;
|
|
124
|
+
let missed = 0;
|
|
125
|
+
for (let at = nextSlot(s, since); at && at.getTime() <= now.getTime() && missed < 1000; at = nextSlot(s, at)) missed += 1;
|
|
126
|
+
return { slot, missed: Math.max(1, missed) };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The schedule in one line, as the `Daily` page and the context say it. */
|
|
130
|
+
export function scheduleText(s: OrganizeSchedule): string {
|
|
131
|
+
if (s.frequency === 'Off') return 'Off — a round runs only when you press Follow up';
|
|
132
|
+
if (s.frequency === 'Continuous') return 'Continuous — a round after each stretch of work';
|
|
133
|
+
if (s.frequency === 'Every day') return `Every day at ${s.time}`;
|
|
134
|
+
if (s.frequency === 'On selected days') return `On ${(s.days ?? []).map((d) => DAY_NAMES[d]).join(', ')} at ${s.time}`;
|
|
135
|
+
const n = s.everyDays ?? 1;
|
|
136
|
+
return `Every ${n === 1 ? 'day' : `${n} days`} at ${s.time}`;
|
|
137
|
+
}
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Organizing service (CKC-05, CKC-06, CKC-07, CKC-13): plans and queues the Keeper's automatic work from the state of
|
|
3
|
+
* the assets, keeps the coverage record honest, marks entries whose material changed (`Update pending`), and derives
|
|
4
|
+
* the graph after every job.
|
|
5
|
+
*
|
|
6
|
+
* Since increment K every round is a round of the clerk method (Spec v3.0 §3.3; clerk.ts). Planning is a pull: after
|
|
7
|
+
* each job (and after intake, settled changes, resume) the service lets the clerk planner advance the round under way or
|
|
8
|
+
* start the one that is due, and brings coverage and the takeover's progress up to date from the rounds, so the Keeper
|
|
9
|
+
* activity list shows what is happening now.
|
|
10
|
+
*/
|
|
11
|
+
import type { Coverage, CoverageScope, DeepeningPlan, KeeperJob, TakeoverStatus } from '../../model/types.ts';
|
|
12
|
+
import { organizingHeld } from '../held.ts';
|
|
13
|
+
import type { ProjectStore } from '../../store/project-store.ts';
|
|
14
|
+
import type { PendingChange } from '../../sources/watch.ts';
|
|
15
|
+
import type { IntakeResult } from '../../intake/intake.ts';
|
|
16
|
+
import { excludedLocations, splitTooLarge } from '../../intake/skipped.ts';
|
|
17
|
+
import type { App } from '../../server/app.ts';
|
|
18
|
+
import type { KeeperEvent } from '../runtime.ts';
|
|
19
|
+
import { gitHead } from '../../util/git.ts';
|
|
20
|
+
import { listMaterials } from './materials.ts';
|
|
21
|
+
import { areaOf, deriveGraph } from './graph.ts';
|
|
22
|
+
import { positioningBlock } from './positioning.ts';
|
|
23
|
+
import { composeRelook, scopeLabel, type RelookScope } from '../relook/compose.ts';
|
|
24
|
+
import { relookPrompt } from '../relook/prompts.ts';
|
|
25
|
+
import { takeoverState } from './takeover-state.ts';
|
|
26
|
+
import { scheduleDueFor } from './schedule-state.ts';
|
|
27
|
+
import { clerkDepthOptions, deepeningAsRun, deepeningPlanned, deepeningReads, firstUsableFigures, focusedCovers } from './takeover-clerk.ts';
|
|
28
|
+
import { readingBasisText } from './reading.ts';
|
|
29
|
+
import { isRoundMain } from '../roles.ts';
|
|
30
|
+
import { ACTIVE_STATUSES, ClerkPlanner, stepFailureText, type LedgerRunner, type ProcessRunner } from './clerk.ts';
|
|
31
|
+
import { clerkLevels, clerkPending } from './clerk-coverage.ts';
|
|
32
|
+
import { refreshUpdatePending } from './update-pending.ts';
|
|
33
|
+
import { isTableJob, tableJobs, tablePending, TableFollowUp } from './table-follow-up.ts';
|
|
34
|
+
|
|
35
|
+
/** What the service reads from a job's task, and writes into a re-look's: its kind, judgement record and scope. */
|
|
36
|
+
interface TaskExtra { readonly kind: string; readonly judgementId?: string; readonly scopeKey?: string }
|
|
37
|
+
|
|
38
|
+
const ACTIVE: readonly string[] = ['Queued', 'Running', 'Waiting for quota', 'Paused'];
|
|
39
|
+
const extraOf = (job: KeeperJob): TaskExtra | null => ((job.task as { extra?: TaskExtra } | null)?.extra ?? null);
|
|
40
|
+
|
|
41
|
+
/** What an owner's request left to re-look at, kept on the request's job until the re-looks open (§3.3, §5.6). */
|
|
42
|
+
interface DeferredExtra { readonly deferredRelooks?: readonly RelookScope[]; readonly relooksOpenedAt?: string | null }
|
|
43
|
+
const deferredOf = (job: KeeperJob): DeferredExtra | null => ((job.task as { extra?: DeferredExtra } | null)?.extra ?? null);
|
|
44
|
+
|
|
45
|
+
/** A round under way: a round of the clerk method running, or — left by a build before it — a round's main job still active. */
|
|
46
|
+
function roundUnderWay(store: ProjectStore): boolean {
|
|
47
|
+
return store.clerkRounds.find((r) => r.status === 'Running') !== undefined || store.jobs.find((j) => ACTIVE.includes(j.status) && isRoundMain(j)) !== undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class OrganizingService {
|
|
51
|
+
private readonly app: App;
|
|
52
|
+
private readonly enabled: boolean;
|
|
53
|
+
private readonly replanning = new Map<string, { running: boolean; again: boolean }>();
|
|
54
|
+
|
|
55
|
+
/** The clerk method (Spec v3.0 §3.3): plans every round since increment K. */
|
|
56
|
+
private clerk: ClerkPlanner;
|
|
57
|
+
/** D107: a project taken over by the ProjectKeeper table is followed up by carrying the table on, not by rounds. */
|
|
58
|
+
private table: TableFollowUp;
|
|
59
|
+
|
|
60
|
+
constructor(app: App, enabled = true) { this.app = app; this.enabled = enabled; this.clerk = new ClerkPlanner(app, null); this.table = new TableFollowUp(app); }
|
|
61
|
+
|
|
62
|
+
/** The program step of every round: bring the ledger up to date (src/ledger). Set once the build carries it. */
|
|
63
|
+
setLedgerRunner(runner: LedgerRunner | null): void { this.clerk.setLedgerRunner(runner); }
|
|
64
|
+
/** The program step that computes each work item's process and breakpoints (§2.12). Set once the build carries it. */
|
|
65
|
+
setProcessRunner(runner: ProcessRunner | null): void { this.clerk.setProcessRunner(runner); }
|
|
66
|
+
|
|
67
|
+
start(): void {
|
|
68
|
+
this.app.keeper.on('event', (event: KeeperEvent) => { if (event.kind === 'done') void this.afterJob(event.projectId, event.jobId); });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The clock the schedule is read against (§3.8); a test sets it. */
|
|
72
|
+
setClock(clock: () => number): void { this.clerk.clock = clock; this.table.clock = clock; }
|
|
73
|
+
|
|
74
|
+
private ticker: NodeJS.Timeout | null = null;
|
|
75
|
+
/**
|
|
76
|
+
* Watch the clock for the scheduled times (§3.8; CKC-07 AC-29, AC-30): twice a minute, a project whose scheduled time
|
|
77
|
+
* has come is planned again, and the planner starts a round when something changed. After the machine slept, or
|
|
78
|
+
* ProjectKeeper was not running, the first pass finds the time that was missed and makes up for it with one round.
|
|
79
|
+
*/
|
|
80
|
+
watchSchedule(everyMs = 30_000): void {
|
|
81
|
+
if (!this.enabled || this.ticker) return;
|
|
82
|
+
this.ticker = setInterval(() => { void this.tick(); }, everyMs);
|
|
83
|
+
this.ticker.unref?.();
|
|
84
|
+
}
|
|
85
|
+
stopWatchingSchedule(): void { if (this.ticker) { clearInterval(this.ticker); this.ticker = null; } }
|
|
86
|
+
/** One pass over the projects: one whose scheduled time has come is planned again. A paused project waits for its resume. */
|
|
87
|
+
async tick(): Promise<void> {
|
|
88
|
+
for (const p of this.app.workspace.list()) {
|
|
89
|
+
try {
|
|
90
|
+
if (organizingHeld(p)) continue;
|
|
91
|
+
const store = this.app.store(p.id);
|
|
92
|
+
const state = takeoverState(store, p);
|
|
93
|
+
if (state.phase === 'Done' && scheduleDueFor(store, p, state.completedAt, this.clerk.clock())) await this.replan(p.id);
|
|
94
|
+
} catch (e) { console.warn(`[schedule ${p.id}] ${(e as Error).message}`); }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* When ProjectKeeper starts, each project's organizing goes on from its saved progress (Spec §3.10 接着做): the round
|
|
100
|
+
* under way advances — what the restart cut short runs again (clerk.ts `stepEnding`) — and a step of a round that was
|
|
101
|
+
* waiting for quota is queued again, because what would have queued it (the runtime's quota and rate-limit timers) did
|
|
102
|
+
* not outlive the last run. If the quota is still out, it waits again.
|
|
103
|
+
*/
|
|
104
|
+
resumeAll(): void {
|
|
105
|
+
for (const p of this.app.workspace.list()) {
|
|
106
|
+
if (this.enabled) {
|
|
107
|
+
try {
|
|
108
|
+
const store = this.app.store(p.id);
|
|
109
|
+
for (const j of store.jobs.filter((x) => x.status === 'Waiting for quota' && store.clerkRounds.get(x.step?.roundId ?? '')?.status === 'Running')) this.app.keeper.restartJob(p.id, j.id);
|
|
110
|
+
} catch (e) { console.warn(`[organize ${p.id}] ${(e as Error).message}`); }
|
|
111
|
+
}
|
|
112
|
+
void this.replan(p.id);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
onIntake(projectId: string): void { void this.replan(projectId); }
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The owner's Retry or Continue on a job (Spec §3.10, §6.9). A job of a round's step is the planner's to run again — a
|
|
119
|
+
* job the program did itself as well (the ledger, the process, the program's part of a step), which the runtime does
|
|
120
|
+
* not run; any other job goes back on the runtime's queue.
|
|
121
|
+
*/
|
|
122
|
+
runAgain(projectId: string, jobId: string): boolean {
|
|
123
|
+
const store = this.app.store(projectId);
|
|
124
|
+
const job = store.jobs.get(jobId);
|
|
125
|
+
if (!job) return false;
|
|
126
|
+
return job.step ? this.clerk.ownerRunAgain(store, this.app.project(projectId), job) : this.app.keeper.restartJob(projectId, jobId);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
onSettled(projectId: string, changes: readonly PendingChange[], _result: IntakeResult): void {
|
|
130
|
+
this.markPending(projectId, changes);
|
|
131
|
+
void this.replan(projectId);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Settled changes were just read (§3.2): the entries they affect show `Update pending` (§1.11), each naming the changes it
|
|
136
|
+
* waits for and how they reach it (update-pending.ts). The marks follow the coverage's list of what waits for the next
|
|
137
|
+
* round, so only material that will be organized again marks anything: what is History only or Reference only, what the
|
|
138
|
+
* scope does not read, what the project's rules settle and the Keeper's own commits of its project folder never do.
|
|
139
|
+
*
|
|
140
|
+
* A changed material does not modify the work items built on it, so their propagation judgements stand (§2.10,
|
|
141
|
+
* §5.5): a work item is modified when organizing brings it up to date, and then the next round judges what it still
|
|
142
|
+
* lacks, while what already has a conclusion keeps it. Its own time is left alone as well: when an object last
|
|
143
|
+
* changed is evidence a judgement weighs, and the change of a material is not a change of the object. Resetting the
|
|
144
|
+
* entries to `Not yet checked` here left objects in the change's detail that no round would judge — a concluded
|
|
145
|
+
* item is not judged again, and the refreshed time made the entry read as answered (batch D2, AK P4).
|
|
146
|
+
*/
|
|
147
|
+
markPending(projectId: string, changes: readonly PendingChange[]): void {
|
|
148
|
+
if (changes.length === 0) return;
|
|
149
|
+
try { refreshUpdatePending(this.app, projectId); } catch (e) { console.warn(`[update pending ${projectId}] ${(e as Error).message}`); }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private async afterJob(projectId: string, jobId: string): Promise<void> {
|
|
153
|
+
const store = this.app.store(projectId);
|
|
154
|
+
const job = store.jobs.get(jobId);
|
|
155
|
+
if (job && extraOf(job)?.kind === 'relook') this.finalizeRelook(store, job);
|
|
156
|
+
try { deriveGraph(store, this.app.project(projectId)); } catch (e) { console.warn(`[graph ${projectId}] ${(e as Error).message}`); }
|
|
157
|
+
if (job && job.kind === 'Your request' && job.status === 'Done') this.relookAfterRequest(store, job);
|
|
158
|
+
await this.replan(projectId);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** §5.6: after a delegated or authorized adjustment, the affected scope is re-looked at; changing files alone resolves nothing. */
|
|
162
|
+
private relookAfterRequest(store: ProjectStore, job: KeeperJob): void {
|
|
163
|
+
const areaIds = new Set<string>();
|
|
164
|
+
const consider = (id: string) => { const a = store.reference.has(id) ? areaOf(store, id) : store.threads.has(id) ? this.threadArea(store, id) : null; if (a) areaIds.add(a); };
|
|
165
|
+
for (const r of job.savedResults) consider(r.id);
|
|
166
|
+
const ctx = (job.task as { extra?: { context?: { kind: string; id: string } } } | null)?.extra?.context;
|
|
167
|
+
if (ctx?.kind === 'note') { const n = store.notes.get(ctx.id); for (const id of n?.mount.ids ?? []) consider(id); }
|
|
168
|
+
else if (ctx) consider(ctx.id);
|
|
169
|
+
const scopes: RelookScope[] = areaIds.size === 0 ? [{ kind: 'project', id: null, label: 'Whole project' }] : [...areaIds].slice(0, 3).map((id) => ({ kind: 'area', id, label: store.reference.get(id)?.name ?? id }));
|
|
170
|
+
// Like every automatic re-look, it opens after the round under way has ended, not alongside it (§3.3): the round's
|
|
171
|
+
// synthesis writes the notes then. What waits is kept on the request's own job, not in memory, so a restart in between
|
|
172
|
+
// does not lose it (C1's hand-back item 2), and `openDeferredRelooks` opens it once no round is under way.
|
|
173
|
+
if (roundUnderWay(store)) {
|
|
174
|
+
const current = store.jobs.get(job.id) ?? job;
|
|
175
|
+
const task = (current.task ?? {}) as { extra?: Record<string, unknown> | null };
|
|
176
|
+
const extra = task.extra ?? {};
|
|
177
|
+
const waiting = [...((extra.deferredRelooks as RelookScope[] | undefined) ?? []), ...scopes];
|
|
178
|
+
store.jobs.put({ ...current, task: { ...task, extra: { ...extra, deferredRelooks: waiting, relooksOpenedAt: null } } }, { jobId: job.id, summary: `Re-look after the request waits for the round under way: ${scopes.map((sc) => sc.label).join(', ')}` });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
for (const scope of scopes) this.enqueueRelook(job.projectId, scope, false);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The re-looks an owner's request left for after the round under way (`relookAfterRequest`), opened once no round is
|
|
186
|
+
* under way — each scope once — and marked opened on the request's job, so they open once and a restart before then
|
|
187
|
+
* does not lose them. The reader of what waits went with the material rounds' planner (06bccd4), which left the owner's
|
|
188
|
+
* request stored and never opened (explore of test-C-1, §5.6 item 5).
|
|
189
|
+
*/
|
|
190
|
+
openDeferredRelooks(projectId: string): number {
|
|
191
|
+
const store = this.app.store(projectId);
|
|
192
|
+
if (roundUnderWay(store)) return 0;
|
|
193
|
+
const waiting = store.jobs.filter((j) => (deferredOf(j)?.deferredRelooks?.length ?? 0) > 0 && !deferredOf(j)?.relooksOpenedAt).sort((a, b) => a.queuedAt.localeCompare(b.queuedAt));
|
|
194
|
+
if (!waiting.length) return 0;
|
|
195
|
+
const now = new Date().toISOString();
|
|
196
|
+
const seen = new Set<string>();
|
|
197
|
+
for (const j of waiting) {
|
|
198
|
+
const task = (j.task ?? {}) as { extra?: Record<string, unknown> | null };
|
|
199
|
+
store.jobs.put({ ...j, task: { ...task, extra: { ...(task.extra ?? {}), relooksOpenedAt: now } } }, { jobId: j.id, summary: 'The re-looks the request left for after the round are opened now' });
|
|
200
|
+
for (const scope of deferredOf(j)!.deferredRelooks!) {
|
|
201
|
+
const key = `${scope.kind}:${scope.id ?? ''}`;
|
|
202
|
+
if (seen.has(key)) continue;
|
|
203
|
+
seen.add(key);
|
|
204
|
+
this.enqueueRelook(projectId, scope, false);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return seen.size;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private threadArea(store: ProjectStore, threadId: string): string | null {
|
|
211
|
+
const t = store.threads.get(threadId);
|
|
212
|
+
if (!t) return null;
|
|
213
|
+
for (const s of t.serves) { const a = areaOf(store, s.referenceId); if (a) return a; }
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** After a re-look: a `Questioned` without a note is not allowed (§2.5); record whether anything was found. */
|
|
218
|
+
private finalizeRelook(store: ProjectStore, job: KeeperJob): void {
|
|
219
|
+
const judgementId = extraOf(job)?.judgementId;
|
|
220
|
+
const j = judgementId ? store.judgements.get(judgementId) : undefined;
|
|
221
|
+
if (!j) return;
|
|
222
|
+
const now = new Date().toISOString();
|
|
223
|
+
const assessments = j.outcome.assessments.filter((a) => {
|
|
224
|
+
if (a.assessment !== 'Questioned') return true;
|
|
225
|
+
const r = store.relations.get(a.relationId);
|
|
226
|
+
if (!r) return false;
|
|
227
|
+
const hasNote = store.notes.find((n) => n.status === 'Current' && (n.mount.ids.includes(r.id) || n.mount.ids.includes(r.from) || n.mount.ids.includes(r.to) || n.versions.some((v) => v.judgementRecordId === j.id && (v.preview.includes(r.id) || JSON.stringify(v.body).includes(r.id)))));
|
|
228
|
+
if (hasNote) return true;
|
|
229
|
+
store.relations.put({ ...r, assessment: 'Not assessed', assessedAt: now, assessedInJobId: job.id, updatedAt: now }, { jobId: job.id, summary: 'Questioned without a note; assessment reverted to Not assessed' });
|
|
230
|
+
return false;
|
|
231
|
+
});
|
|
232
|
+
const reconsideredOnly = job.status === 'Done' && j.outcome.noteIds.length === 0 && assessments.length === 0;
|
|
233
|
+
store.judgements.put({ ...j, outcome: { ...j.outcome, assessments, reconsideredOnly } });
|
|
234
|
+
// The notes this re-look touched keep the course of events (§4.5, CKC-11 AC-16).
|
|
235
|
+
for (const noteId of j.outcome.noteIds) {
|
|
236
|
+
const n = store.notes.get(noteId);
|
|
237
|
+
if (!n || n.followUps.some((f) => f.jobId === job.id)) continue;
|
|
238
|
+
store.notes.put({ ...n, followUps: [...n.followUps, { kind: 'relook', jobId: job.id, at: now, summary: `Product re-look: ${j.scope.label}${n.status !== 'Current' ? ` → ${n.status}` : ''}` }] });
|
|
239
|
+
}
|
|
240
|
+
if (job.status === 'Done') {
|
|
241
|
+
const scopes = store.coverage.scopes.map((s) => (s.id === 'project' ? { ...s, lastRelookAt: now } : s));
|
|
242
|
+
store.setCoverage({ ...store.coverage, scopes }, { jobId: job.id, summary: `Product re-look done: ${j.scope.label}${reconsideredOnly ? ' (reconsidered; no new finding)' : ''}` });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Owner-requested re-look (§3.4 "owner 要求"): runs in the owner lane ahead of backlog. */
|
|
247
|
+
requestRelook(projectId: string, scope: RelookScope): KeeperJob {
|
|
248
|
+
return this.enqueueRelook(projectId, scope, true);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private enqueueRelook(projectId: string, scope: RelookScope, byOwner: boolean): KeeperJob {
|
|
252
|
+
const store = this.app.store(projectId);
|
|
253
|
+
const project = this.app.project(projectId);
|
|
254
|
+
const composed = composeRelook(store, project, scope, '');
|
|
255
|
+
store.judgements.put(composed.judgement, { jobId: null, summary: `Re-look inputs composed: ${composed.judgement.scope.label}` });
|
|
256
|
+
const first = !store.judgements.find((j) => j.scope.kind === 'project' && j.id !== composed.judgement.id);
|
|
257
|
+
const job = this.app.keeper.enqueue(projectId, {
|
|
258
|
+
kind: 'Product re-look', initiator: byOwner ? 'owner' : 'auto', priority: byOwner ? 0 : 2, timeoutMs: 45 * 60_000,
|
|
259
|
+
scope: { kind: scope.kind, ids: scope.id ? [scope.id] : [], label: scopeLabel(store, scope) },
|
|
260
|
+
prompt: relookPrompt(positioningBlock(store, project), composed.judgement.scope.label, composed.judgement.id, composed.text, first, byOwner),
|
|
261
|
+
judgementId: composed.judgement.id,
|
|
262
|
+
task: { kind: 'relook', judgementId: composed.judgement.id, scopeKey: `${scope.kind}:${scope.id ?? ''}` } satisfies TaskExtra,
|
|
263
|
+
});
|
|
264
|
+
store.judgements.put({ ...composed.judgement, jobId: job.id });
|
|
265
|
+
return job;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Re-evaluate what to queue next; serialized per project. */
|
|
269
|
+
async replan(projectId: string): Promise<Coverage> {
|
|
270
|
+
if (!this.enabled || this.app.isClearing(projectId)) return this.app.store(projectId).coverage;
|
|
271
|
+
let st = this.replanning.get(projectId);
|
|
272
|
+
if (!st) { st = { running: false, again: false }; this.replanning.set(projectId, st); }
|
|
273
|
+
if (st.running) { st.again = true; return this.app.store(projectId).coverage; }
|
|
274
|
+
st.running = true;
|
|
275
|
+
try {
|
|
276
|
+
do { st.again = false; await this.plan(projectId); } while (st.again);
|
|
277
|
+
} catch (e) {
|
|
278
|
+
console.warn(`[organize ${projectId}] ${(e as Error).message}`);
|
|
279
|
+
} finally {
|
|
280
|
+
st.running = false;
|
|
281
|
+
}
|
|
282
|
+
return this.app.store(projectId).coverage;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Since increment K every round is a round of the clerk method (Spec v3.0 §3.3; CKC-23): the planner below advances the
|
|
287
|
+
* round under way or starts the one that is due, and the coverage and the depth question follow from its rounds. The
|
|
288
|
+
* material rounds it replaced (D59; CKC-06) are gone.
|
|
289
|
+
*/
|
|
290
|
+
private async plan(projectId: string): Promise<void> {
|
|
291
|
+
// D107: a project taken over by the table never starts the clerk method's rounds; its Follow up is a table follow-up.
|
|
292
|
+
if (this.app.project(projectId).takenOverByTable) await this.table.plan(projectId);
|
|
293
|
+
else await this.clerk.plan(projectId);
|
|
294
|
+
this.openDeferredRelooks(projectId);
|
|
295
|
+
this.clerkCoverage(projectId);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** The coverage and the takeover's progress for the clerk method's rounds (`writeClerkCoverage`). */
|
|
299
|
+
private clerkCoverage(projectId: string): void {
|
|
300
|
+
writeClerkCoverage(this.app, projectId);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Coverage and takeover progress for the clerk method's rounds (Spec §3.7, §1.11; CKC-13 AC-1, AC-10, AC-19, AC-20):
|
|
306
|
+
* the first usable round's time as evidence (D37), where the takeover stands (not started, under way, done), what waits and what is
|
|
307
|
+
* running or failed, every material's organizing level, and the last Follow up. The depth options are sized from the
|
|
308
|
+
* project's own material. Exported so a fixture that plants rounds gets the coverage the product computes from them.
|
|
309
|
+
*/
|
|
310
|
+
export function writeClerkCoverage(app: App, projectId: string): void {
|
|
311
|
+
const store = app.store(projectId);
|
|
312
|
+
const project = app.project(projectId);
|
|
313
|
+
const rounds = store.clerkRounds.all().sort((a, b) => a.startedAt.localeCompare(b.startedAt));
|
|
314
|
+
const first = rounds.find((r) => r.kind === 'First usable');
|
|
315
|
+
const firstDone = first?.status === 'Done';
|
|
316
|
+
// Where the takeover stands is read from the rounds (takeover-state.ts): the depth is the one the takeover runs to, and
|
|
317
|
+
// once it is done, the one that ran — never the last button pressed (D105).
|
|
318
|
+
const takeover = takeoverState(store, project);
|
|
319
|
+
const notStarted = takeover.phase === 'Not started';
|
|
320
|
+
const depth = takeover.phase === 'Done' ? takeover.ran : takeover.chosen;
|
|
321
|
+
const complete = takeover.phase === 'Done';
|
|
322
|
+
const stage: TakeoverStatus['stage'] = notStarted ? 'Not started' : !firstDone ? 'First usable' : complete ? 'Daily' : 'Deepening';
|
|
323
|
+
// §3.7 by the clerk method: the first usable round's own figures, what a deepening reads path by path (counted from
|
|
324
|
+
// the ledger), the three options on a stated basis, and the deepening's progress path by path — with what each path
|
|
325
|
+
// actually read against its plan, and the round against the whole plan, beside the estimate (CKC-13 AC-8).
|
|
326
|
+
const ledger = app.ledger.ledger(projectId);
|
|
327
|
+
const figures = firstUsableFigures(store, first, ledger, project);
|
|
328
|
+
// The plan is counted once for the options, the paths and the deepening as it ran: counting it reads the ledger's totals.
|
|
329
|
+
const planned = deepeningPlanned(ledger, store);
|
|
330
|
+
const { options, basis, perMaterial } = clerkDepthOptions(figures, ledger, store, app.keeper.autoLaneCountFor(projectId), planned, project);
|
|
331
|
+
const asRun = deepeningAsRun(ledger, store, project, planned);
|
|
332
|
+
// A deepening read by reading assignments (reading.ts): how its reading is counted and its assignments sized.
|
|
333
|
+
const reading = rounds.filter((r) => r.kind === 'Deepen' && r.reading).pop()?.reading ?? null;
|
|
334
|
+
const deepening: DeepeningPlan = { paths: deepeningReads(ledger, store, planned), focused: focusedCovers(store), basis, progress: asRun.progress, actual: asRun.actual, ...(reading ? { readingBasis: readingBasisText(reading) } : {}) };
|
|
335
|
+
const chosen = options.find((o) => o.depth === depth);
|
|
336
|
+
// Under way, what remains of a deepening read by assignments is what its paths have not accounted for yet.
|
|
337
|
+
const open = reading ? asRun.progress.reduce((n, p) => n + (p.reading?.open ?? 0), 0) : null;
|
|
338
|
+
// What waits for the next round, and every material's organizing level (§1.11; CKC-07 AC-1, AC-10; CKC-13 AC-8): from
|
|
339
|
+
// the rounds, the jobs' recorded calls, the drafts and the rules. Both used to be written empty on every pass, which
|
|
340
|
+
// wiped what the watcher and intake had listed and left the level table blank (QC AY B1, B8).
|
|
341
|
+
const materials = listMaterials(store, project);
|
|
342
|
+
// Before `Start` nothing is taken in and nothing counts as waiting (D105; CKC-13 AC-20).
|
|
343
|
+
// D107: what waits for a table project is what its table is behind: the commits since its asOf and the owner's messages
|
|
344
|
+
// since it was filled in (table-follow-up.ts `tablePending`); no material level is pending, so no entry shows `Update pending`.
|
|
345
|
+
const found = project.takenOverByTable ? { pending: tablePending(app, project), keys: new Set<string>(), fresh: new Map() } : clerkPending(store, project, app.pendingChanges(projectId), materials);
|
|
346
|
+
const waiting = notStarted ? { ...found, pending: [], keys: new Set<string>() } : found;
|
|
347
|
+
const { pending, keys: pendingKeys } = waiting;
|
|
348
|
+
const { levels, sampled, readInPart, historyNotOrganized } = clerkLevels(store, project, pendingKeys, materials);
|
|
349
|
+
// The entries what waits affects show `Update pending` from the same list (§1.11; CKC-07 AC-10), so the marks and the
|
|
350
|
+
// coverage agree: what a round took in leaves both at once. A failure here never keeps the coverage from being written.
|
|
351
|
+
try { refreshUpdatePending(app, projectId, waiting); } catch (e) { console.warn(`[update pending ${projectId}] ${(e as Error).message}`); }
|
|
352
|
+
const status: TakeoverStatus = {
|
|
353
|
+
depth: depth ?? 'Depth not chosen', stage,
|
|
354
|
+
firstUsable: { startedAt: first?.startedAt ?? project.createdAt, completedAt: firstDone ? first!.endedAt : null, minutes: figures.minutes, byKind: figures.byKind, ownerWords: figures.ownerWords },
|
|
355
|
+
levels, readInPart, sampled,
|
|
356
|
+
remaining: stage === 'Deepening' && chosen && chosen.depth !== 'First picture only'
|
|
357
|
+
? open !== null && perMaterial
|
|
358
|
+
? { materials: open, minutes: Math.round((open * perMaterial.minutes) / Math.max(1, app.keeper.autoLaneCountFor(projectId))), cost: perMaterial.cost == null ? null : Math.round(open * perMaterial.cost * 100) / 100 }
|
|
359
|
+
: { materials: chosen.toOrganize, minutes: chosen.minutes, cost: chosen.cost }
|
|
360
|
+
: null,
|
|
361
|
+
options, rates: { basis, msPerChar: 0, costPerChar: null, perMaterial },
|
|
362
|
+
// What history is still not read for meaning, counted from the levels of the history-tier materials whatever the
|
|
363
|
+
// depth (§3.7 stage 5 lists the rest). It used to be 0 by definition once a Full deepening was done.
|
|
364
|
+
historyNotOrganized,
|
|
365
|
+
noteId: null, deepening,
|
|
366
|
+
};
|
|
367
|
+
// A table follow-up's session (D107) is organizing too, though it is no step of a round.
|
|
368
|
+
const running = store.jobs.filter((j) => (Boolean(j.step) || isTableJob(j)) && ACTIVE_STATUSES.includes(j.status));
|
|
369
|
+
// Every job of a round's step that did not finish, whether the program runs it again, it waits for the owner, or it is
|
|
370
|
+
// listed and the round went on — or closed — without it (Spec §3.10: 列入覆盖情况的失败清单并写明原因,不静默跳过). It
|
|
371
|
+
// leaves the list when a run again finishes it.
|
|
372
|
+
const failed = store.jobs.filter((j) => Boolean(j.step) && (j.status === 'Failed' || j.status === 'Stopped') && store.clerkRounds.has(j.step!.roundId)).sort((a, b) => a.queuedAt.localeCompare(b.queuedAt));
|
|
373
|
+
// The last table follow-up, when it did not finish (D107): listed until a later one does.
|
|
374
|
+
const lastTable = tableJobs(store).pop();
|
|
375
|
+
const tableFailed = lastTable && (lastTable.status === 'Failed' || lastTable.status === 'Stopped') ? [{ ref: lastTable.id, reason: `${lastTable.scope.label}: ${lastTable.status === 'Failed' ? 'failed' : 'was stopped'} — ${lastTable.error ?? 'no reason recorded'}. Retry or Follow up runs it again`, at: lastTable.endedAt ?? lastTable.queuedAt }] : [];
|
|
376
|
+
const previous = store.coverage.scopes.find((s) => s.id === 'project');
|
|
377
|
+
const mainRepo = project.scope.find((i) => i.category === 'Repository' && i.relation !== 'Excluded' && !i.missing);
|
|
378
|
+
// §1.11's four labels: paused holds the rest; what runs is organizing; what waits with nothing running is pending.
|
|
379
|
+
const label: CoverageScope['coverage'] = organizingHeld(project) ? 'Organizing paused' : running.length ? 'Organizing' : pending.length ? 'Changes pending' : 'Up to date';
|
|
380
|
+
// What intake could not take in is carried over; a file too large to read is skipped, not failed (D105) — also one a
|
|
381
|
+
// home written before that still holds among its failures, which moves over here with no step by the owner.
|
|
382
|
+
const intake = splitTooLarge((previous?.failed ?? []).filter((f) => !store.jobs.has(f.ref)), previous?.skipped ?? [], excludedLocations(project));
|
|
383
|
+
const projectScope: CoverageScope = {
|
|
384
|
+
id: 'project', kind: 'project', label: project.name, coverage: label, asOf: latest(rounds.filter((r) => r.endedAt).pop()?.endedAt, project.takenOverByTable?.at) ?? previous?.asOf ?? null,
|
|
385
|
+
commit: mainRepo ? gitHead(mainRepo.path) : null, pending, organizing: running.map((j) => ({ kind: 'other', ref: j.id, label: j.scope.label, since: j.startedAt ?? j.queuedAt })),
|
|
386
|
+
// A round's step that did not finish, and what intake could not take in with its reason (CKC-07 AC-10): intake
|
|
387
|
+
// writes its own failures, so they are carried over, not wiped by the next pass.
|
|
388
|
+
failed: notStarted ? [] : [...failed.map((j) => ({ ref: j.id, reason: stepFailureText(store, j, organizingHeld(project)), at: j.endedAt ?? j.queuedAt })), ...tableFailed, ...intake.failed], skipped: notStarted ? [] : intake.skipped, lastRelookAt: previous?.lastRelookAt ?? null,
|
|
389
|
+
};
|
|
390
|
+
const pendingByKind: Record<string, number> = {};
|
|
391
|
+
for (const p of pending) pendingByKind[p.kind] = (pendingByKind[p.kind] ?? 0) + 1;
|
|
392
|
+
store.setCoverage({ ...store.coverage, scopes: [projectScope, ...store.coverage.scopes.filter((s) => s.id !== 'project')], pendingByKind, takeover: status, lastFollowUp: lastFollowUp(store), state: notStarted ? 'Not organized yet' : complete ? 'Takeover complete' : 'Initial model', updatedAt: new Date().toISOString() }, { jobId: null, summary: 'Coverage updated' });
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** The later of two times, either of which may be absent: a table's filling-in counts as the picture's time (D107). */
|
|
396
|
+
const latest = (a: string | null | undefined, b: string | null | undefined): string | null => (a && b ? (a > b ? a : b) : a ?? b ?? null);
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* The last Follow up round that closed, for the top bar (§6.2): whether it found anything new, in one line. When it did
|
|
400
|
+
* not, this is where "the last round found nothing new" is said (CKC-07 AC-27, CKC-24 AC-15); when it did, its item is
|
|
401
|
+
* in `Notes (attention)`. A result closed before rounds counted their news says nothing here.
|
|
402
|
+
*/
|
|
403
|
+
function lastFollowUp(store: ProjectStore): NonNullable<Coverage['lastFollowUp']> | null {
|
|
404
|
+
const record = store.rounds.filter((r) => r.endedAt !== null && r.result !== null).sort((a, b) => (b.endedAt ?? '').localeCompare(a.endedAt ?? ''))[0];
|
|
405
|
+
if (!record?.result?.news) return null;
|
|
406
|
+
const news = record.result!.news!;
|
|
407
|
+
const clerk = store.clerkRounds.find((c) => c.followUpRoundId === record.id);
|
|
408
|
+
return { recordId: record.id, round: clerk?.number ?? record.number, endedAt: record.endedAt!, nothingNew: news.nothingNew, statement: news.nothingNew ? 'The last Follow up round found nothing new' : `The last Follow up round: ${news.statement}` };
|
|
409
|
+
}
|