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,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A plan's execution shape (Spec v3.0 §6.3 执行形状, D72; CKC-24 AC-2): its batches as the plan wrote them, the order they
|
|
3
|
+
* actually ran in — by when their work was dispatched and delivered — which ran in parallel, the merge commit of each,
|
|
4
|
+
* and where the actual order differs from the planned one, in one sentence. As the orchestrator wrote it and as git shows
|
|
5
|
+
* it; ProjectKeeper schedules nothing.
|
|
6
|
+
*
|
|
7
|
+
* The plan's own batches are read from its section: numbered items (`1. **地基**:…`), or lines and headings naming a batch
|
|
8
|
+
* (`批次 3`, `Batch 3`), with the parallel it states (`和 3、4 并行`, `in parallel with 3`). Which batch a piece of work is
|
|
9
|
+
* comes from its own arrangement (`批次 5b`, `batch 6 of 7`; placement.ts).
|
|
10
|
+
*/
|
|
11
|
+
import type { Occurred } from '../model/k-types.ts';
|
|
12
|
+
import type { PlanShapeView } from '../model/views-k.ts';
|
|
13
|
+
import type { WorkThread } from '../model/types.ts';
|
|
14
|
+
import { splitMarkdown } from '../sources/files.ts';
|
|
15
|
+
import type { Analysis } from './analysis.ts';
|
|
16
|
+
import { batchOf, planDocument, workOfPlan } from './placement.ts';
|
|
17
|
+
import type { Unit } from './units.ts';
|
|
18
|
+
import type { WorkProcess } from './work.ts';
|
|
19
|
+
|
|
20
|
+
export interface PlannedBatch {
|
|
21
|
+
readonly label: string;
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly text: string;
|
|
24
|
+
/** Other batches the plan says this one runs in parallel with. */
|
|
25
|
+
readonly parallelWith: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The batches a plan section names, in its order. */
|
|
29
|
+
export function plannedBatches(text: string): PlannedBatch[] {
|
|
30
|
+
const out: PlannedBatch[] = [];
|
|
31
|
+
let inFence = false;
|
|
32
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
33
|
+
if (/^\s*(```|~~~)/.test(raw)) { inFence = !inFence; continue; }
|
|
34
|
+
if (inFence) continue;
|
|
35
|
+
const item = /^\s{0,3}(\d{1,2})[.)、]\s+(.+)$/.exec(raw);
|
|
36
|
+
const named = /^\s{0,3}(?:#{1,6}\s+|[-*+]\s+)?(?:\*\*)?(?:批次|第)\s*(\d{1,2})\s*批?\s*[::·—-]?\s*(.*)$/.exec(raw) ?? /^\s{0,3}(?:#{1,6}\s+|[-*+]\s+)?(?:\*\*)?batch\s*(\d{1,2})\b\s*[::·—-]?\s*(.*)$/i.exec(raw);
|
|
37
|
+
const m = item ?? named;
|
|
38
|
+
if (!m) continue;
|
|
39
|
+
const label = m[1]!;
|
|
40
|
+
if (out.some((b) => b.label === label)) continue;
|
|
41
|
+
const body = m[2]!.trim();
|
|
42
|
+
const bold = /\*\*([^*]+)\*\*/.exec(body)?.[1];
|
|
43
|
+
const name = (bold ?? body).replace(/[::((。].*$/, '').replace(/[*_`]/g, '').trim() || body.slice(0, 30);
|
|
44
|
+
const hint = /(?:和|与|跟|同)\s*((?:\d{1,2}\s*[、,,和与及]\s*)*\d{1,2})\s*(?:同时|一起)?\s*并行|(?:in )?parallel with\s+(?:batch(?:es)?\s*)?((?:\d{1,2}\s*(?:,|and|&)\s*)*\d{1,2})/i.exec(body);
|
|
45
|
+
const parallelWith = hint ? [...(hint[1] ?? hint[2] ?? '').matchAll(/\d{1,2}/g)].map((x) => x[0]) : [];
|
|
46
|
+
out.push({ label, name, text: body, parallelWith });
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The text of a document's section (the heading path) in a version's text; the whole text when no section is given. */
|
|
52
|
+
export function sectionText(text: string, headingPath: readonly string[]): string {
|
|
53
|
+
if (headingPath.length === 0) return text;
|
|
54
|
+
const want = headingPath.join(' › ');
|
|
55
|
+
const sections = splitMarkdown(text);
|
|
56
|
+
const own = sections.filter((s) => s.headingPath.join(' › ') === want || s.headingPath.join(' › ').startsWith(`${want} › `));
|
|
57
|
+
return own.length ? own.map((s) => s.text).join('\n') : text;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface WorkRun {
|
|
61
|
+
readonly thread: WorkThread;
|
|
62
|
+
readonly process: WorkProcess;
|
|
63
|
+
readonly units: readonly Unit[];
|
|
64
|
+
readonly batch: string | null;
|
|
65
|
+
readonly part: string | null;
|
|
66
|
+
readonly start: { readonly ms: number; readonly occurred: Occurred } | null;
|
|
67
|
+
readonly end: number | null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** One time a plan's batch ran: all of it, or — when its work ran at different times — one part of it. */
|
|
71
|
+
interface ActualBatch {
|
|
72
|
+
readonly label: string;
|
|
73
|
+
readonly runs: readonly WorkRun[];
|
|
74
|
+
/** Work of the batch not started yet (shown with its first part). */
|
|
75
|
+
readonly waiting: readonly WorkRun[];
|
|
76
|
+
readonly start: number;
|
|
77
|
+
readonly end: number;
|
|
78
|
+
readonly startOccurred: Occurred | null;
|
|
79
|
+
/** Which part of the batch this is (1-based), and of how many. */
|
|
80
|
+
readonly part: number;
|
|
81
|
+
readonly parts: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const joinList = (xs: readonly string[]): string => (xs.length <= 1 ? xs.join('') : `${xs.slice(0, -1).join(', ')} and ${xs[xs.length - 1]}`);
|
|
85
|
+
const byLabelOrder = (x: string, y: string): number => x.localeCompare(y, 'en', { numeric: true });
|
|
86
|
+
|
|
87
|
+
export function planShapeOf(a: Analysis, planId: string): PlanShapeView | null {
|
|
88
|
+
const { store, facts, index } = a;
|
|
89
|
+
const plan = store.reference.get(planId);
|
|
90
|
+
if (!plan || plan.category !== 'Plan') return null;
|
|
91
|
+
const doc = planDocument(store, facts, plan);
|
|
92
|
+
const version = doc?.current ?? doc?.first ?? null;
|
|
93
|
+
const text = version ? facts.textOf(version.id) : null;
|
|
94
|
+
const planned = text ? plannedBatches(sectionText(text, doc?.headingPath ?? [])) : [];
|
|
95
|
+
// The work of the plan, each with its batch and when it ran (dispatch or first commit, to its last merge, fix or check).
|
|
96
|
+
const runs: WorkRun[] = workOfPlan(store, planId).map((thread) => {
|
|
97
|
+
const process = a.process(thread.id)!;
|
|
98
|
+
const units = process.own.map((n) => index.units.get(n)!).filter(Boolean);
|
|
99
|
+
const b = units.map(batchOf).find((x) => x !== null) ?? null;
|
|
100
|
+
const d = process.delivery;
|
|
101
|
+
const start = d.dispatched ? { ms: d.dispatched.ms, occurred: d.dispatched.occurred } : d.codeCommits[0] ? { ms: d.codeCommits[0].ms, occurred: d.codeCommits[0].occurred } : null;
|
|
102
|
+
const endCandidates = [...d.merges.map((m) => m.ms), d.deliveredAt?.ms ?? 0, ...process.checks.map((c) => c.ms), ...process.fixes.flatMap((f) => [f.ms, ...f.merges.map((m) => m.ms)])].filter((x) => x > 0);
|
|
103
|
+
const end = endCandidates.length ? Math.max(...endCandidates) : null;
|
|
104
|
+
return { thread, process, units, batch: b?.batch ?? null, part: b?.part ?? null, start, end };
|
|
105
|
+
});
|
|
106
|
+
// Batches as they ran: by the plan's batch number (work with no batch of its own is a batch of its own); a batch whose
|
|
107
|
+
// work ran at different times is split into the parts that ran together.
|
|
108
|
+
const byLabel = new Map<string, WorkRun[]>();
|
|
109
|
+
for (const r of runs) {
|
|
110
|
+
const label = r.batch ?? r.thread.ids[0] ?? r.thread.id;
|
|
111
|
+
byLabel.set(label, [...(byLabel.get(label) ?? []), r]);
|
|
112
|
+
}
|
|
113
|
+
const actual: ActualBatch[] = [];
|
|
114
|
+
for (const [label, rs] of byLabel) {
|
|
115
|
+
const ran = rs.filter((r) => r.start).sort((x, y) => x.start!.ms - y.start!.ms);
|
|
116
|
+
const waiting = rs.filter((r) => !r.start);
|
|
117
|
+
const clusters: WorkRun[][] = [];
|
|
118
|
+
let until = -Infinity;
|
|
119
|
+
for (const r of ran) {
|
|
120
|
+
const e = r.end ?? r.start!.ms;
|
|
121
|
+
const c = clusters[clusters.length - 1];
|
|
122
|
+
if (c && r.start!.ms < until) { c.push(r); until = Math.max(until, e); }
|
|
123
|
+
else { clusters.push([r]); until = e; }
|
|
124
|
+
}
|
|
125
|
+
if (clusters.length === 0) { actual.push({ label, runs: [], waiting, start: Number.MAX_SAFE_INTEGER, end: 0, startOccurred: null, part: 1, parts: 1 }); continue; }
|
|
126
|
+
clusters.forEach((c, i) => actual.push({
|
|
127
|
+
label, runs: c, waiting: i === 0 ? waiting : [], start: c[0]!.start!.ms, end: Math.max(...c.map((r) => r.end ?? r.start!.ms)),
|
|
128
|
+
startOccurred: c[0]!.start!.occurred, part: i + 1, parts: clusters.length,
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
const started = actual.filter((b) => b.runs.length).sort((x, y) => x.start - y.start || byLabelOrder(x.label, y.label));
|
|
132
|
+
const notStarted = actual.filter((b) => b.runs.length === 0).sort((x, y) => byLabelOrder(x.label, y.label));
|
|
133
|
+
// Batches that ran at the same time: one that starts before the ones before it ended joins them.
|
|
134
|
+
const groups: ActualBatch[][] = [];
|
|
135
|
+
for (const b of started) {
|
|
136
|
+
const g = groups[groups.length - 1];
|
|
137
|
+
if (g && b.start < Math.max(...g.map((x) => x.end))) g.push(b);
|
|
138
|
+
else groups.push([b]);
|
|
139
|
+
}
|
|
140
|
+
const nameOfRun = (b: ActualBatch, r: WorkRun) => (r.part ? `${b.label}${r.part}` : r.thread.ids[0] ?? r.thread.id);
|
|
141
|
+
const partsOf = (b: ActualBatch): string => {
|
|
142
|
+
const names = b.runs.map((r) => nameOfRun(b, r));
|
|
143
|
+
const pieces = b.parts > 1 || b.runs.length > 1 ? [names.join(runsOverlap(b.runs) ? ' ∥ ' : ' → ')] : [];
|
|
144
|
+
const waiting = b.waiting.map((r) => `${nameOfRun(b, r)} not started`);
|
|
145
|
+
return pieces.length || waiting.length ? ` (${[...pieces, ...waiting].join('; ')})` : '';
|
|
146
|
+
};
|
|
147
|
+
const actualOrder = groups.length
|
|
148
|
+
? [...groups.map((g) => (g.length > 1 ? `(${g.map((b) => `${b.label}${partsOf(b)}`).join(' ∥ ')})` : `${g[0]!.label}${partsOf(g[0]!)}`)), ...notStarted.map((b) => `${b.label} (not started)`)].join(' → ')
|
|
149
|
+
: null;
|
|
150
|
+
// The planned order, with the parallel the plan states.
|
|
151
|
+
const plannedGroups: string[][] = [];
|
|
152
|
+
for (const p of planned) {
|
|
153
|
+
if (plannedGroups.some((g) => g.includes(p.label))) continue;
|
|
154
|
+
const mates = new Set([p.label, ...p.parallelWith.filter((x) => planned.some((q) => q.label === x))]);
|
|
155
|
+
for (const q of planned) if (q.parallelWith.includes(p.label)) mates.add(q.label);
|
|
156
|
+
plannedGroups.push([...mates].sort(byLabelOrder));
|
|
157
|
+
}
|
|
158
|
+
const plannedOrder = plannedGroups.length ? plannedGroups.map((g) => (g.length > 1 ? `(${g.join(' ∥ ')})` : g[0]!)).join(' → ') : null;
|
|
159
|
+
const differs = differences(plannedGroups, groups, actual, planned, nameOfRun);
|
|
160
|
+
const nameOf = (label: string) => planned.find((p) => p.label === label)?.name ?? null;
|
|
161
|
+
const batches: PlanShapeView['batches'] = [...groups.flat(), ...notStarted].map((b) => {
|
|
162
|
+
const group = groups.find((g) => g.includes(b));
|
|
163
|
+
const members = [...b.runs, ...b.waiting];
|
|
164
|
+
// The merge commits that brought the batch in; work committed straight to the trunk has none, and its last commit stands in.
|
|
165
|
+
const merged = [...new Map(b.runs.flatMap((r) => [...r.process.delivery.merges, ...r.process.fixes.flatMap((f) => f.merges)]).map((m) => [m.hash, m])).values()];
|
|
166
|
+
const direct = b.runs.flatMap((r) => (r.process.delivery.merges.length ? [] : r.process.delivery.codeCommits.filter((c) => c.fpTrunk).slice(-1)));
|
|
167
|
+
const merges = [...merged, ...direct].sort((x, y) => x.ms - y.ms);
|
|
168
|
+
const agents = [...new Set(members.flatMap((r) => r.units.map((u) => u.executor).filter((x): x is string => Boolean(x))))];
|
|
169
|
+
const worktrees = [...new Set(b.runs.flatMap((r) => r.units.map((u) => (u.fields.worktree ?? u.branches[0] ?? '').split(/[\\/ ]/).filter(Boolean).pop() ?? '').filter(Boolean)))];
|
|
170
|
+
const name = nameOf(b.label);
|
|
171
|
+
return {
|
|
172
|
+
label: `Batch ${b.label}${name ? ` · ${name}` : ''}${b.parts > 1 ? ` (part ${b.part} of ${b.parts})` : ''}`,
|
|
173
|
+
workIds: members.map((r) => r.thread.id),
|
|
174
|
+
parallel: (group?.length ?? 1) > 1 || (b.runs.length > 1 && runsOverlap(b.runs)),
|
|
175
|
+
running: b.runs.some((r) => r.process.execution === 'In progress' && r.process.delivery.dispatched !== null),
|
|
176
|
+
agent: agents.length ? agents.join(', ') : null,
|
|
177
|
+
worktree: worktrees.length ? worktrees.join(', ') : null,
|
|
178
|
+
mergeCommit: merges.length ? merges.map((m) => m.short).join(', ') : null,
|
|
179
|
+
occurred: b.startOccurred,
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
return { planId, actualOrder, plannedOrder, differs, batches, dependsOn: dependenciesOf(a, runs) };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function runsOverlap(runs: readonly WorkRun[]): boolean {
|
|
186
|
+
const spans = runs.filter((r) => r.start).map((r) => ({ s: r.start!.ms, e: r.end ?? r.start!.ms })).sort((x, y) => x.s - y.s);
|
|
187
|
+
for (let i = 1; i < spans.length; i++) if (spans[i]!.s < Math.max(...spans.slice(0, i).map((x) => x.e))) return true;
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Where the actual order differs from the plan, in one sentence; null when it does not (or the plan names no batches). */
|
|
192
|
+
function differences(
|
|
193
|
+
plannedGroups: readonly string[][], groups: readonly ActualBatch[][], actual: readonly ActualBatch[], planned: readonly PlannedBatch[],
|
|
194
|
+
nameOfRun: (b: ActualBatch, r: WorkRun) => string,
|
|
195
|
+
): string | null {
|
|
196
|
+
if (plannedGroups.length === 0 || groups.length === 0) return null;
|
|
197
|
+
const rank = new Map<string, number>();
|
|
198
|
+
plannedGroups.forEach((g, i) => { for (const l of g) rank.set(l, i); });
|
|
199
|
+
const sequence = groups.flat().filter((b) => rank.has(b.label));
|
|
200
|
+
const said = (b: ActualBatch) => (b.parts > 1 ? `${b.runs.map((r) => nameOfRun(b, r)).join(', ')} of batch ${b.label}` : `batch ${b.label}`);
|
|
201
|
+
// The batches that keep the planned order are the longest run of non-decreasing planned ranks; the others moved.
|
|
202
|
+
const n = sequence.length;
|
|
203
|
+
const len = new Array<number>(n).fill(1);
|
|
204
|
+
const prev = new Array<number>(n).fill(-1);
|
|
205
|
+
for (let i = 0; i < n; i++) for (let j = 0; j < i; j++) if (rank.get(sequence[j]!.label)! <= rank.get(sequence[i]!.label)! && len[j]! + 1 > len[i]!) { len[i] = len[j]! + 1; prev[i] = j; }
|
|
206
|
+
let best = 0;
|
|
207
|
+
for (let i = 1; i < n; i++) if (len[i]! > len[best]!) best = i;
|
|
208
|
+
const kept = new Set<number>();
|
|
209
|
+
for (let i = n ? best : -1; i >= 0; i = prev[i]!) kept.add(i);
|
|
210
|
+
const parts: string[] = [];
|
|
211
|
+
const unique = (xs: string[]) => [...new Set(xs)];
|
|
212
|
+
sequence.forEach((b, i) => {
|
|
213
|
+
if (kept.has(i)) return;
|
|
214
|
+
const r = rank.get(b.label)!;
|
|
215
|
+
// Said against the batches that kept their order.
|
|
216
|
+
const jumped = unique(sequence.flatMap((x, j) => (j > i && kept.has(j) && rank.get(x.label)! < r ? [x.label] : [])));
|
|
217
|
+
const after = unique(sequence.flatMap((x, j) => (j < i && kept.has(j) && rank.get(x.label)! > r ? [x.label] : [])));
|
|
218
|
+
if (jumped.length) parts.push(`${said(b)} ran before batch${jumped.length > 1 ? 'es' : ''} ${joinList(jumped)}`);
|
|
219
|
+
else if (after.length) parts.push(`${said(b)} ran after batch${after.length > 1 ? 'es' : ''} ${joinList(after)}`);
|
|
220
|
+
});
|
|
221
|
+
// A batch the plan wrote as one, run as several pieces of work together, or in parts at different times.
|
|
222
|
+
for (const b of actual) {
|
|
223
|
+
if (b.runs.length < 2 || !rank.has(b.label)) continue;
|
|
224
|
+
const pieces = b.runs.map((r) => nameOfRun(b, r));
|
|
225
|
+
parts.push(`batch ${b.label} ran as ${b.runs.length === 2 ? 'two halves' : `${b.runs.length} parts`} ${runsOverlap(b.runs) ? 'in parallel' : 'one after the other'} (${pieces.join(', ')})`);
|
|
226
|
+
}
|
|
227
|
+
for (const label of unique(actual.filter((b) => b.parts > 1 && b.part === 1).map((b) => b.label))) {
|
|
228
|
+
const pieces = actual.filter((b) => b.label === label).sort((x, y) => x.part - y.part).map((b) => b.runs.map((r) => nameOfRun(b, r)).join(', '));
|
|
229
|
+
parts.push(`batch ${label} ran in ${pieces.length} parts at different times (${pieces.join('; then ')})`);
|
|
230
|
+
}
|
|
231
|
+
// Parallel the plan stated and the run did not keep, or the other way round.
|
|
232
|
+
const groupOf = (label: string) => groups.findIndex((g) => g.some((b) => b.label === label && b.part === 1));
|
|
233
|
+
for (const g of plannedGroups) {
|
|
234
|
+
if (g.length < 2) continue;
|
|
235
|
+
const ran = g.map(groupOf).filter((i) => i >= 0);
|
|
236
|
+
if (ran.length === g.length && new Set(ran).size > 1) parts.push(`batches ${joinList(g)}, planned in parallel, ran one after another`);
|
|
237
|
+
}
|
|
238
|
+
for (const grp of groups) {
|
|
239
|
+
const labels = unique(grp.map((b) => b.label).filter((l) => rank.has(l)));
|
|
240
|
+
if (labels.length < 2) continue;
|
|
241
|
+
if (new Set(labels.map((l) => rank.get(l))).size > 1) parts.push(`batches ${joinList(labels)} ran in parallel, planned one after another`);
|
|
242
|
+
}
|
|
243
|
+
const missing = planned.filter((p) => !actual.some((b) => b.label === p.label)).map((p) => p.label);
|
|
244
|
+
if (missing.length) parts.push(`no work item is recorded for batch${missing.length > 1 ? 'es' : ''} ${joinList(missing)}`);
|
|
245
|
+
return parts.length ? `Execution differs from the plan: ${parts.join('; ')}.` : null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Dependencies between the plan's work: the round's `dependsOn`, and the arrangement's own fields (`upstream`, `blocks`). */
|
|
249
|
+
function dependenciesOf(a: Analysis, runs: readonly WorkRun[]): PlanShapeView['dependsOn'] {
|
|
250
|
+
const { index } = a;
|
|
251
|
+
const byNum = new Map<string, string>();
|
|
252
|
+
for (const t of a.store.threads.all()) for (const n of t.ids) if (!byNum.has(n)) byNum.set(n, t.id);
|
|
253
|
+
for (const r of runs) for (const n of r.thread.ids) byNum.set(n, r.thread.id);
|
|
254
|
+
const byBatch = new Map<string, string[]>();
|
|
255
|
+
for (const r of runs) if (r.batch) byBatch.set(r.batch, [...(byBatch.get(r.batch) ?? []), r.thread.id]);
|
|
256
|
+
const edges = new Map<string, { from: string; to: string }>();
|
|
257
|
+
const add = (from: string | undefined, to: string | undefined) => { if (from && to && from !== to) edges.set(`${from}\x1f${to}`, { from, to }); };
|
|
258
|
+
const unitNames = [...index.units.keys()];
|
|
259
|
+
const named = (v: string) => unitNames.filter((n) => new RegExp(`(?<![A-Za-z0-9_])${n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![A-Za-z0-9_]|-\\d)`).test(v));
|
|
260
|
+
for (const r of runs) {
|
|
261
|
+
for (const d of r.thread.dependsOn) if (runs.some((x) => x.thread.id === d.threadId)) add(r.thread.id, d.threadId);
|
|
262
|
+
for (const u of r.units) {
|
|
263
|
+
for (const [k, v] of Object.entries(u.fields)) {
|
|
264
|
+
if (/^(upstream|depends_on|depends|after|blocked_by|requires|prerequisites?)$/i.test(k)) for (const n of named(v)) add(r.thread.id, byNum.get(n));
|
|
265
|
+
if (/^blocks$/i.test(k)) {
|
|
266
|
+
for (const n of named(v)) add(byNum.get(n), r.thread.id);
|
|
267
|
+
// `batches 2 (B runner), 3 (A assembly), 4 (memory rooms)`: every batch number after the word.
|
|
268
|
+
const after = /(?:batch(?:es)?|批次)([\s\S]*)$/i.exec(v)?.[1] ?? '';
|
|
269
|
+
for (const b of after.replace(/\([^)]*\)|([^)]*)/g, ' ').match(/(?<![\d.])\d{1,2}(?![\d.])/g) ?? []) for (const t of byBatch.get(b) ?? []) add(t, r.thread.id);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return [...edges.values()];
|
|
275
|
+
}
|
|
276
|
+
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Send-backs (Spec v3.0 §1.18 送回): a relation, not a ticket. `Suggested → Returned → Closed`, recognised from the
|
|
3
|
+
* ledger at each round; nobody changes a state by hand, and the owner's `No action needed` stays.
|
|
4
|
+
*
|
|
5
|
+
* - **Where they come from.** A check that failed (QC, review or walkthrough judged `fail`, `needs repair` …; a check
|
|
6
|
+
* still in progress, `incomplete(审核进行中)`, has judged nothing and starts none) and a
|
|
7
|
+
* host's review that sent a delivery back (`status: needs-repair`) are send-backs to Work the project itself wrote
|
|
8
|
+
* (§1.18 "QC 与走查的判定"); the program records each once, keyed by the ledger entry it started from. The round's
|
|
9
|
+
* synthesis suggests the others — from breakpoints, the six things, code anomalies (clerk-tools `pk_suggest_sendback`).
|
|
10
|
+
* - **`Returned`**: a later task, commit or document version picks it up — a fix whose task names the work or the check
|
|
11
|
+
* (by number), a commit or document that cites the send-back's own id (the `Copy for agent` text carries it), a later
|
|
12
|
+
* version of the document a send-back to Plan hangs on, or a confirmed link (then `Inferred`).
|
|
13
|
+
* - **`Closed`**: later evidence shows it fixed — a check after the fix that passed; the fix merged (for a host's review,
|
|
14
|
+
* or where the project does not ask for a re-check); the breakpoint it came from put out by evidence; the note or mark
|
|
15
|
+
* it came from resolved — always with that evidence.
|
|
16
|
+
*/
|
|
17
|
+
import { linkHolds } from '../model/k-types.ts';
|
|
18
|
+
import type { Basis } from '../model/vocab.ts';
|
|
19
|
+
import type { EvidenceRef, Occurred, SendBack } from '../model/k-types.ts';
|
|
20
|
+
import type { ProjectStore } from '../store/project-store.ts';
|
|
21
|
+
import type { Analysis } from './analysis.ts';
|
|
22
|
+
import { msOfOccurred } from './facts.ts';
|
|
23
|
+
import { FIX_WORDS } from './units.ts';
|
|
24
|
+
import { clip, standsAlone } from './text.ts';
|
|
25
|
+
import { earlierWork } from '../keeper/organize/carried-on.ts';
|
|
26
|
+
import { ownFindings, sendBackIdFor, startsSendBack, type CheckRecord, type FixRecord, type ReviewRecord, type WorkProcess } from './work.ts';
|
|
27
|
+
|
|
28
|
+
export interface SendBackResult {
|
|
29
|
+
readonly created: number;
|
|
30
|
+
readonly returned: number;
|
|
31
|
+
readonly closed: number;
|
|
32
|
+
readonly open: number;
|
|
33
|
+
readonly written: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface Origin {
|
|
37
|
+
readonly id: string;
|
|
38
|
+
readonly kind: 'check' | 'review';
|
|
39
|
+
readonly targetId: string;
|
|
40
|
+
readonly occurred: Occurred;
|
|
41
|
+
readonly ms: number;
|
|
42
|
+
readonly what: string;
|
|
43
|
+
readonly suggestion: string;
|
|
44
|
+
readonly evidence: readonly EvidenceRef[];
|
|
45
|
+
readonly process: WorkProcess;
|
|
46
|
+
readonly check: CheckRecord | null;
|
|
47
|
+
readonly review: ReviewRecord | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type Stage = { readonly stage: SendBack['stage']; readonly returned: SendBack['returned']; readonly closed: SendBack['closed'] };
|
|
51
|
+
|
|
52
|
+
const RANK: Record<SendBack['stage'], number> = { Suggested: 0, Returned: 1, Closed: 2 };
|
|
53
|
+
|
|
54
|
+
/** The failed checks and the host's send-back reviews of every current piece of work, once each (a chain is seen from several works). */
|
|
55
|
+
function originsOf(a: Analysis): Origin[] {
|
|
56
|
+
const { store, facts, index } = a;
|
|
57
|
+
const gen = earlierWork(store); // CZ: an item carried on under the same number is current work
|
|
58
|
+
const out = new Map<string, Origin>();
|
|
59
|
+
for (const t of store.threads.filter((x) => x.validity === 'Current' && !gen.has(x.id))) {
|
|
60
|
+
const p = a.process(t.id)!;
|
|
61
|
+
for (const c of p.checks) {
|
|
62
|
+
// A check unit's failing verdict; a "fail" in a delivery's own receipt is its account, not a check (CKC-27 AC-11), and
|
|
63
|
+
// a check still in progress (`incomplete(审核进行中)`) has judged nothing yet.
|
|
64
|
+
if (!startsSendBack(c) || !c.verdict || out.has(c.verdict.id)) continue;
|
|
65
|
+
const checked = index.units.get(c.unit)?.relations.map((r) => r.num) ?? [];
|
|
66
|
+
const targetNum = checked.find((n) => index.threadOf.has(n)) ?? null;
|
|
67
|
+
const targetId = targetNum ? index.threadOf.get(targetNum)! : t.id;
|
|
68
|
+
const findings = ownFindings(c);
|
|
69
|
+
out.set(c.verdict.id, {
|
|
70
|
+
id: c.verdict.id, kind: 'check', targetId, occurred: c.occurred, ms: c.ms, process: targetId === t.id ? p : a.process(targetId) ?? p, check: c, review: null,
|
|
71
|
+
what: `${c.unit}'s ${c.by === 'Independent QC' ? 'independent ' : ''}${c.kind === 'QC' ? 'QC' : c.kind.toLowerCase()} judged ${checked.filter((n) => index.units.has(n)).join(', ') || (t.ids[0] ?? t.title)} ${c.verdictWord ?? c.normalized}${findings.length ? `: ${findings.length} finding${findings.length === 1 ? '' : 's'}, ${clip(findings[0]!.text.replace(/^#+\s*/, ''), 80)}` : ''}`,
|
|
72
|
+
suggestion: `Fix what ${c.unit} found${findings.length ? ` (${findings.map((f) => f.verdict).join(', ')})` : ''} in new or reopened work, then have it checked again`,
|
|
73
|
+
evidence: [facts.ref(c.verdict.id, c.verdict.text), ...findings.slice(0, 8).map((f) => facts.ref(f.id, f.text))],
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
for (const r of p.reviews) {
|
|
77
|
+
if (out.has(r.entry.id)) continue;
|
|
78
|
+
const targetId = index.threadOf.get(r.unit) ?? t.id;
|
|
79
|
+
out.set(r.entry.id, {
|
|
80
|
+
id: r.entry.id, kind: 'review', targetId, occurred: r.occurred, ms: r.ms, process: targetId === t.id ? p : a.process(targetId) ?? p, check: null, review: r,
|
|
81
|
+
what: `${r.who ?? 'The host'} reviewed ${r.unit}'s delivery: ${r.status}`,
|
|
82
|
+
suggestion: `Repair ${r.unit}'s delivery in new or reopened work`,
|
|
83
|
+
evidence: [facts.ref(r.entry.id, `status: ${r.status}`), ...r.receipts.map((x) => facts.ref(x.id))],
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return [...out.values()];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The stage of a send-back that a failed check or a host's review started, from its work's process. */
|
|
91
|
+
function stageOfOrigin(a: Analysis, o: Origin): Stage {
|
|
92
|
+
const p = o.process;
|
|
93
|
+
// The fix that caught it: the first fix whose task was written at or after the failure and that fixes the checked
|
|
94
|
+
// unit or the check itself.
|
|
95
|
+
const failedUnit = o.check ? o.check.unit : o.review!.unit;
|
|
96
|
+
const checked = o.check ? a.index.units.get(o.check.unit)?.relations.map((r) => r.num) ?? [] : [o.review!.unit];
|
|
97
|
+
const fix: FixRecord | undefined = p.fixes
|
|
98
|
+
.filter((f) => (f.taskWritten?.ms ?? f.dispatched?.ms ?? f.ms) >= o.ms && f.targets.some((n) => n === failedUnit || checked.includes(n)))
|
|
99
|
+
.sort((x, y) => (x.taskWritten?.ms ?? x.ms) - (y.taskWritten?.ms ?? y.ms))[0];
|
|
100
|
+
if (!fix) return { stage: 'Suggested', returned: null, closed: null };
|
|
101
|
+
const by = fix.taskWritten?.evidence ?? fix.dispatched?.evidence ?? fix.evidence[0]!;
|
|
102
|
+
const returned = { by, at: by.occurred?.at ?? new Date().toISOString(), basis: 'Explicit' as Basis };
|
|
103
|
+
// Fixed: the earliest of a check after the fix that passed, and the fix landing on the trunk — the latter counts
|
|
104
|
+
// for a host's review, and for a failed check only where the project does not ask for a re-check after a fix.
|
|
105
|
+
const recheck = p.checks.filter((c) => c.ms > fix.ms && c.normalized === 'pass' && c.verdict).sort((x, y) => x.ms - y.ms)[0];
|
|
106
|
+
const needsRecheck = o.kind === 'check' && a.expecting(p.thread, 'Re-check after fix') !== null;
|
|
107
|
+
const closings = [
|
|
108
|
+
...(recheck ? [{ ms: recheck.ms, by: a.facts.ref(recheck.verdict!.id, recheck.verdict!.text), at: recheck.occurred.at }] : []),
|
|
109
|
+
...(!needsRecheck && fix.landed ? [{ ms: fix.landed.ms, by: fix.landed.evidence, at: fix.landed.occurred.at }] : []),
|
|
110
|
+
].sort((x, y) => x.ms - y.ms);
|
|
111
|
+
const c = closings[0];
|
|
112
|
+
return c ? { stage: 'Closed', returned, closed: { by: c.by, at: c.at } } : { stage: 'Returned', returned, closed: null };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The numbers of a send-back's target (the work item, or what the reference item or territory names). */
|
|
116
|
+
function targetNumbers(store: ProjectStore, sb: SendBack): string[] {
|
|
117
|
+
return store.threads.get(sb.targetId)?.ids.slice() ?? store.reference.get(sb.targetId)?.ids.slice() ?? [];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The stage a suggested send-back has reached by what came later (§1.18), whoever suggested it. */
|
|
121
|
+
function stageOfSuggested(a: Analysis, sb: SendBack): Stage {
|
|
122
|
+
const { store, facts, index } = a;
|
|
123
|
+
const since = msOfOccurred(sb.occurred);
|
|
124
|
+
let returned: SendBack['returned'] = sb.returned;
|
|
125
|
+
const candidates: { ms: number; by: EvidenceRef; basis: Basis }[] = [];
|
|
126
|
+
// 1. A later commit or document that cites the send-back's own id (the text `Copy for agent` hands over carries it).
|
|
127
|
+
const cited = facts.ledger.word(sb.id, { limit: 20, oldestFirst: true });
|
|
128
|
+
if (typeof cited !== 'string') for (const h of cited.rows) if (h.kind === 'doc' || h.kind === 'commit' || h.kind === 'loose') candidates.push({ ms: msOfOccurred(h.occurred), by: facts.ref(h.id, h.snippet), basis: 'Explicit' });
|
|
129
|
+
const nums = targetNumbers(store, sb);
|
|
130
|
+
if (sb.to === 'Work') {
|
|
131
|
+
// 2. A later fix task that names the work, or a later commit that says it fixes it.
|
|
132
|
+
for (const u of index.units.values()) {
|
|
133
|
+
if (u.nature !== 'fix' || !u.relations.some((r) => nums.includes(r.num))) continue;
|
|
134
|
+
const task = u.prompts[0];
|
|
135
|
+
if (task && task.ms > since) candidates.push({ ms: task.ms, by: facts.ref(task.id, task.data.title ?? null), basis: 'Explicit' });
|
|
136
|
+
}
|
|
137
|
+
for (const c of facts.commits.values()) {
|
|
138
|
+
if (c.ms <= since || c.merge || !FIX_WORDS.test(c.subject) || !nums.some((n) => standsAlone(c.subject, n))) continue;
|
|
139
|
+
candidates.push({ ms: c.ms, by: facts.ref(c.id), basis: 'Explicit' });
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
// 2'. A later version of the document the send-back to Plan hangs on.
|
|
143
|
+
for (const path of documentPaths(a, sb.targetId)) {
|
|
144
|
+
const v = (facts.docsByPath.get(path) ?? []).find((d) => d.ms > since);
|
|
145
|
+
if (v) candidates.push({ ms: v.ms, by: facts.ref(v.id), basis: 'Explicit' });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// 3. A confirmed link on the target, later than the problem.
|
|
149
|
+
for (const l of store.links.filter((x) => x.workId === sb.targetId && linkHolds(x) && (x.stepKind === 'Fix' || x.stepKind === 'Delivered' || x.stepKind === 'Dispatched'))) {
|
|
150
|
+
const ms = msOfOccurred(l.evidence?.occurred);
|
|
151
|
+
if (ms > since) candidates.push({ ms, by: l.evidence ?? facts.ref(l.ledgerRef), basis: 'Inferred' });
|
|
152
|
+
}
|
|
153
|
+
const first = candidates.filter((c) => c.ms > 0).sort((x, y) => x.ms - y.ms)[0];
|
|
154
|
+
if (!returned && first) returned = { by: first.by, at: first.by.occurred?.at ?? new Date(first.ms).toISOString(), basis: first.basis };
|
|
155
|
+
// Closed: the breakpoint it came from went out on evidence; the note or mark it came from was resolved; for Work, a
|
|
156
|
+
// passing check or a merge of the work after it was returned.
|
|
157
|
+
let closed: SendBack['closed'] = null;
|
|
158
|
+
if (sb.from.kind === 'breakpoint') {
|
|
159
|
+
// Put out by evidence of the step — not by this send-back itself, which only says the problem went back.
|
|
160
|
+
const bp = store.breakpoints.get(sb.from.id);
|
|
161
|
+
const ev = bp && !bp.lit && bp.out?.by === 'evidence' ? bp.out.evidence.find((e) => e.id !== sb.id && !(e.kind === 'object' && store.sendbacks.has(e.id))) : undefined;
|
|
162
|
+
if (ev) closed = { by: ev, at: bp!.out!.at };
|
|
163
|
+
} else if (sb.from.kind === 'owner-judgement') {
|
|
164
|
+
const mark = store.marks.get(sb.from.id);
|
|
165
|
+
const note = store.notes.get(sb.from.id);
|
|
166
|
+
if (mark?.closed) closed = { by: { kind: 'object', id: mark.id, label: `${mark.kind} closed: ${mark.closed.result}`, line: mark.closed.reason, occurred: null }, at: mark.closed.at };
|
|
167
|
+
else if (note?.status === 'Resolved') closed = { by: { kind: 'object', id: note.id, label: `Note resolved: ${note.versions[note.versions.length - 1]?.title ?? note.id}`, line: note.resolvedReason, occurred: null }, at: note.updatedAt };
|
|
168
|
+
}
|
|
169
|
+
if (!closed && returned && sb.to === 'Work') {
|
|
170
|
+
const thread = store.threads.get(sb.targetId);
|
|
171
|
+
const p = thread ? a.process(thread.id) : null;
|
|
172
|
+
const after = msOfOccurred(returned.by.occurred) || Date.parse(returned.at) || since;
|
|
173
|
+
const pass = p?.checks.filter((c) => c.ms > after && c.normalized === 'pass').sort((x, y) => x.ms - y.ms)[0];
|
|
174
|
+
const merge = p ? [...p.delivery.merges, ...p.fixes.flatMap((f) => f.merges)].filter((m) => m.ms > after).sort((x, y) => x.ms - y.ms)[0] : undefined;
|
|
175
|
+
if (pass?.verdict) closed = { by: facts.ref(pass.verdict.id, pass.verdict.text), at: pass.occurred.at };
|
|
176
|
+
else if (merge && !(thread && a.expecting(thread, 'Re-check after fix'))) closed = { by: facts.ref(merge.id), at: merge.occurred.at };
|
|
177
|
+
}
|
|
178
|
+
if (closed && !returned) returned = { by: closed.by, at: closed.at, basis: 'Explicit' };
|
|
179
|
+
return { stage: closed ? 'Closed' : returned ? 'Returned' : 'Suggested', returned, closed };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function documentPaths(a: Analysis, id: string): string[] {
|
|
183
|
+
const { store, facts } = a;
|
|
184
|
+
const ref = store.reference.get(id);
|
|
185
|
+
const out = new Set<string>();
|
|
186
|
+
for (const sid of ref?.sourceIds ?? []) {
|
|
187
|
+
const an = store.sources.get(sid)?.anchor;
|
|
188
|
+
if (an?.kind !== 'file') continue;
|
|
189
|
+
for (const path of facts.docsByPath.keys()) if (an.path.replace(/\\/g, '/').endsWith(`/${path}`)) out.add(path);
|
|
190
|
+
}
|
|
191
|
+
return [...out];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Record the send-backs the project's own failed checks and reviews started, and move every send-back on by what the
|
|
196
|
+
* ledger shows (never back, never by hand). Returns what moved.
|
|
197
|
+
*/
|
|
198
|
+
export function advance(a: Analysis, roundId: string | null, jobId: string | null): SendBackResult {
|
|
199
|
+
const { store, project } = a;
|
|
200
|
+
const now = new Date().toISOString();
|
|
201
|
+
const trace = (summary: string, sourceIds: readonly string[] = []) => ({ jobId, summary, basisSourceIds: sourceIds });
|
|
202
|
+
let created = 0; let returned = 0; let closed = 0; let written = 0;
|
|
203
|
+
const handled = new Set<string>();
|
|
204
|
+
for (const o of originsOf(a)) {
|
|
205
|
+
const existing = store.sendbacks.get(sendBackIdFor(project.id, o.id)) ?? store.sendbacks.find((s) => s.from.id === o.id);
|
|
206
|
+
const st = stageOfOrigin(a, o);
|
|
207
|
+
if (existing) {
|
|
208
|
+
handled.add(existing.id);
|
|
209
|
+
const next = moveOn(existing, st, now);
|
|
210
|
+
if (next) { store.sendbacks.put(next, trace(`Send-back ${existing.id}: ${existing.stage} → ${next.stage} (program)`)); written += 1; if (next.stage !== existing.stage) (next.stage === 'Closed' ? closed++ : returned++); }
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const record: SendBack = {
|
|
214
|
+
id: sendBackIdFor(project.id, o.id), projectId: project.id, to: 'Work', stage: st.stage, targetId: o.targetId, what: o.what, suggestion: o.suggestion,
|
|
215
|
+
evidence: o.evidence, from: { kind: 'verdict', id: o.id }, returned: st.returned, closed: st.closed, ownerResponse: null, sixThing: null,
|
|
216
|
+
occurred: o.occurred, roundId, updatedAt: now,
|
|
217
|
+
};
|
|
218
|
+
store.sendbacks.put(record, trace(`Send-back to Work (program, from ${o.kind === 'check' ? 'a failed check' : "the host's review"}): ${clip(o.what, 120)} — ${st.stage}`));
|
|
219
|
+
handled.add(record.id);
|
|
220
|
+
created += 1; written += 1;
|
|
221
|
+
if (st.stage === 'Returned') returned += 1;
|
|
222
|
+
if (st.stage === 'Closed') closed += 1;
|
|
223
|
+
}
|
|
224
|
+
for (const sb of store.sendbacks.filter((s) => s.projectId === project.id && !handled.has(s.id) && s.stage !== 'Closed')) {
|
|
225
|
+
const next = moveOn(sb, stageOfSuggested(a, sb), now);
|
|
226
|
+
if (!next) continue;
|
|
227
|
+
store.sendbacks.put(next, trace(`Send-back ${sb.id}: ${sb.stage} → ${next.stage} (program): ${next.stage === 'Closed' ? next.closed!.by.label : next.returned!.by.label}`));
|
|
228
|
+
written += 1;
|
|
229
|
+
if (next.stage !== sb.stage) (next.stage === 'Closed' ? closed++ : returned++);
|
|
230
|
+
}
|
|
231
|
+
const open = store.sendbacks.filter((s) => s.projectId === project.id && s.stage !== 'Closed' && !s.ownerResponse).length;
|
|
232
|
+
return { created, returned, closed, open, written };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** The send-back moved on to a computed stage, never back; null when nothing changes. The owner's answer stays. */
|
|
236
|
+
function moveOn(sb: SendBack, st: Stage, now: string): SendBack | null {
|
|
237
|
+
if (RANK[st.stage] < RANK[sb.stage]) return null;
|
|
238
|
+
const returned = sb.returned ?? st.returned;
|
|
239
|
+
const closed = sb.closed ?? st.closed;
|
|
240
|
+
const stage = closed ? 'Closed' : returned ? 'Returned' : 'Suggested';
|
|
241
|
+
if (stage === sb.stage && returned === sb.returned && closed === sb.closed) return null;
|
|
242
|
+
return { ...sb, stage, returned, closed, updatedAt: now };
|
|
243
|
+
}
|
|
244
|
+
|