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,884 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Each piece of work's process as the ledger shows it (Spec v3.0 §2.12 工作的过程): the steps in the order they happened,
|
|
3
|
+
* what was done on the left and what reality gave on the right, and the four things every piece of work shows at a
|
|
4
|
+
* glance (execution, progress, check, what is still open).
|
|
5
|
+
*
|
|
6
|
+
* The steps come from the ledger by the project's numbers (units.ts), then from the links the round confirmed:
|
|
7
|
+
*
|
|
8
|
+
* | step | from | result |
|
|
9
|
+
* | --- | --- | --- |
|
|
10
|
+
* | `Planned` | the plan version it entered (the plan's section in its document), or its task written and queued | plan and time |
|
|
11
|
+
* | `Dispatched` | the prompt's first running version (else its first version, when it went on to be worked), a run file | to whom, when |
|
|
12
|
+
* | `Delivered` | its own commits, branch and receipts; a receipt's account is a claim (§2.4) | commits, merged or not, tests |
|
|
13
|
+
* | `Review` | the host's review written into the arrangement (`status: needs-repair`) | the status word as written |
|
|
14
|
+
* | `QC` / `Review` / `Walkthrough` | a check's report | the verdict as written, findings counted |
|
|
15
|
+
* | `Fix` | a unit that fixes this work, or a check of it | as `Delivered` |
|
|
16
|
+
* | `Merged` | the merge that brought its commits into the trunk | the merge commit |
|
|
17
|
+
* | `Accepted` | an owner's acceptance written in the arrangement | the record |
|
|
18
|
+
* | `Handed to` / `Handed in` | a finding its report hands to another unit, or one handed to it | from where, to where |
|
|
19
|
+
*
|
|
20
|
+
* No step the project does not have is added; the order is what happened, not what should have (§2.12).
|
|
21
|
+
*/
|
|
22
|
+
import type { Occurred, EvidenceRef, ProcessLink, StepKind } from '../model/k-types.ts';
|
|
23
|
+
import { linkHolds, linkStanding } from '../model/k-types.ts';
|
|
24
|
+
import type { FourThingsView, ProcessStepView, ExecutionState } from '../model/views-k.ts';
|
|
25
|
+
import type { Project, WorkThread } from '../model/types.ts';
|
|
26
|
+
import type { ProjectStore } from '../store/project-store.ts';
|
|
27
|
+
import { stableId } from '../model/ids.ts';
|
|
28
|
+
import type { ArrangementFact, CommitFact, Facts, VerdictFact } from './facts.ts';
|
|
29
|
+
import { msOfOccurred } from './facts.ts';
|
|
30
|
+
import {
|
|
31
|
+
DONE_STATUS, PLANNED_STATUS, REPAIR_STATUS, RUNNING_STATUS, baseNumber, chainOf, isArrangementPath, isHostEvidence, ownNumbers, type CheckKind, type Unit, type UnitIndex,
|
|
32
|
+
} from './units.ts';
|
|
33
|
+
import { definitionsInPath } from '../ledger/numbering.ts';
|
|
34
|
+
import { batchOf, planDocument, plansOf, versionOccurred } from './placement.ts';
|
|
35
|
+
import { earlierWork } from '../keeper/organize/carried-on.ts';
|
|
36
|
+
import {
|
|
37
|
+
DISPOSE_WORDS, FAILING, IN_PROGRESS, SELF_DISPOSE_WORDS, claimLineOf, clausesOf, clip, deferLinesOf, evidenceLineOf, isTestCount, openItemsOf,
|
|
38
|
+
rootCommentLineOf, severityOf, standsAlone, tokensOf, verdictAsWritten, withGloss, type OpenItem,
|
|
39
|
+
} from './text.ts';
|
|
40
|
+
|
|
41
|
+
// ───────────────────────── records the steps, the four things and the breakpoints share ─────────────────────────
|
|
42
|
+
|
|
43
|
+
export interface CheckRecord {
|
|
44
|
+
readonly unit: string;
|
|
45
|
+
readonly kind: CheckKind;
|
|
46
|
+
readonly reportPath: string;
|
|
47
|
+
/** The latest version of the report (its text is read through the ledger). */
|
|
48
|
+
readonly report: ArrangementFact | null;
|
|
49
|
+
/** The first version of the report. */
|
|
50
|
+
readonly firstReport: ArrangementFact | null;
|
|
51
|
+
/** The line that states the overall verdict. */
|
|
52
|
+
readonly verdict: VerdictFact | null;
|
|
53
|
+
readonly verdictWord: string | null;
|
|
54
|
+
/** pass, fail, needs-repair …, or null when the report states none. */
|
|
55
|
+
readonly normalized: string | null;
|
|
56
|
+
/** The verdict rests on what the ledger lists only as candidates (section headings), not on a stated verdict. */
|
|
57
|
+
readonly fromCandidates: boolean;
|
|
58
|
+
readonly occurred: Occurred;
|
|
59
|
+
readonly ms: number;
|
|
60
|
+
readonly by: 'Independent QC' | 'Self-reported';
|
|
61
|
+
/** The report is a check unit's (a QC, review or walkthrough task), not a delivery's own receipt: only such a verdict
|
|
62
|
+
* starts a send-back (§1.18 "QC 与走查的判定"). */
|
|
63
|
+
readonly fromCheckUnit: boolean;
|
|
64
|
+
/** The check has not concluded (`incomplete(审核进行中)`, `blocked`): shown as a check in progress, never as a failure. */
|
|
65
|
+
readonly inProgress: boolean;
|
|
66
|
+
readonly findings: readonly VerdictFact[];
|
|
67
|
+
readonly counts: readonly VerdictFact[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A check whose verdict sends the work back (§1.18): a check unit's failing verdict (fail, 不通过, needs repair …). Not a
|
|
72
|
+
* delivery's own account (CKC-27 AC-11), and not a check still in progress (`incomplete(审核进行中)`), which has judged
|
|
73
|
+
* nothing yet.
|
|
74
|
+
*/
|
|
75
|
+
export const startsSendBack = (c: CheckRecord): boolean =>
|
|
76
|
+
c.fromCheckUnit && c.verdict !== null && c.normalized !== null && FAILING.has(c.normalized);
|
|
77
|
+
|
|
78
|
+
export interface ReviewRecord {
|
|
79
|
+
readonly unit: string;
|
|
80
|
+
/** The status word as the arrangement wrote it (`needs-repair`). */
|
|
81
|
+
readonly status: string;
|
|
82
|
+
readonly entry: ArrangementFact;
|
|
83
|
+
readonly receipts: readonly ArrangementFact[];
|
|
84
|
+
readonly occurred: Occurred;
|
|
85
|
+
readonly ms: number;
|
|
86
|
+
readonly who: string | null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface Handling {
|
|
90
|
+
readonly how: 'disposed' | 'followed' | 'sent back' | 'handed';
|
|
91
|
+
readonly evidence: EvidenceRef;
|
|
92
|
+
/** The unit it was handed to. */
|
|
93
|
+
readonly to?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface FindingState {
|
|
97
|
+
readonly check: CheckRecord;
|
|
98
|
+
readonly finding: VerdictFact;
|
|
99
|
+
readonly num: string;
|
|
100
|
+
readonly handled: Handling | null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface OpenItemState {
|
|
104
|
+
readonly check: CheckRecord;
|
|
105
|
+
readonly item: OpenItem;
|
|
106
|
+
readonly handled: Handling | null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface FixRecord {
|
|
110
|
+
readonly unit: string;
|
|
111
|
+
readonly ms: number;
|
|
112
|
+
readonly occurred: Occurred;
|
|
113
|
+
readonly evidence: readonly EvidenceRef[];
|
|
114
|
+
/** When the fix was handed out (its dispatch). */
|
|
115
|
+
readonly dispatched: { readonly ms: number; readonly occurred: Occurred; readonly evidence: EvidenceRef } | null;
|
|
116
|
+
/** When its task was first written, queued or not: the task that catches a send-back (§1.18 `Returned`). */
|
|
117
|
+
readonly taskWritten: { readonly ms: number; readonly occurred: Occurred; readonly evidence: EvidenceRef } | null;
|
|
118
|
+
/** It delivered something: commits or a receipt (a fix only dispatched has nothing to re-check yet). */
|
|
119
|
+
readonly delivered: boolean;
|
|
120
|
+
/** Its code reached the trunk (its merge, or its commits on the trunk). */
|
|
121
|
+
readonly landed: { readonly ms: number; readonly occurred: Occurred; readonly evidence: EvidenceRef } | null;
|
|
122
|
+
readonly merges: readonly CommitFact[];
|
|
123
|
+
readonly targets: readonly string[];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface Delivery {
|
|
127
|
+
readonly commits: readonly CommitFact[];
|
|
128
|
+
readonly codeCommits: readonly CommitFact[];
|
|
129
|
+
readonly merges: readonly CommitFact[];
|
|
130
|
+
readonly receipts: readonly ArrangementFact[];
|
|
131
|
+
/** A receipt the arrangement names (`report: …`) that the ledger never saw committed. */
|
|
132
|
+
readonly missingReceipt: string | null;
|
|
133
|
+
readonly dispatched: { readonly occurred: Occurred; readonly ms: number; readonly entry: ArrangementFact } | null;
|
|
134
|
+
readonly planned: { readonly occurred: Occurred; readonly ms: number; readonly evidence: EvidenceRef; readonly text: string; readonly basis: 'Explicit' | 'Inferred'; readonly unit: string | null } | null;
|
|
135
|
+
/** The material says it is done: its arrangement's status, or the work item's progress. */
|
|
136
|
+
readonly reportedDone: { readonly occurred: Occurred; readonly ms: number; readonly evidence: EvidenceRef | null } | null;
|
|
137
|
+
/** Delivered code commits are there and none of them reached the trunk. */
|
|
138
|
+
readonly unmerged: boolean;
|
|
139
|
+
readonly branches: readonly string[];
|
|
140
|
+
/** The work says it is an experiment or abandoned, or its worktree is an experiment of the scope (§1.1). */
|
|
141
|
+
readonly experiment: EvidenceRef | null;
|
|
142
|
+
/** When it was delivered: its last code commit, else its first receipt. */
|
|
143
|
+
readonly deliveredAt: { readonly ms: number; readonly occurred: Occurred } | null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface WorkProcess {
|
|
147
|
+
readonly thread: WorkThread;
|
|
148
|
+
readonly own: readonly string[];
|
|
149
|
+
readonly chain: readonly string[];
|
|
150
|
+
readonly steps: readonly ProcessStepView[];
|
|
151
|
+
readonly delivery: Delivery;
|
|
152
|
+
readonly checks: readonly CheckRecord[];
|
|
153
|
+
readonly reviews: readonly ReviewRecord[];
|
|
154
|
+
readonly fixes: readonly FixRecord[];
|
|
155
|
+
readonly findings: readonly FindingState[];
|
|
156
|
+
readonly openItems: readonly OpenItemState[];
|
|
157
|
+
/** Null when neither the ids nor the round's links say how the work was carried out (§2.12: a lookup that finds nothing is not absence). */
|
|
158
|
+
readonly execution: ExecutionState | null;
|
|
159
|
+
readonly latestCheck: { readonly verdict: string; readonly by: 'Independent QC' | 'Self-reported' | 'Test'; readonly occurred: Occurred } | null;
|
|
160
|
+
/** The confirmed links this process uses (a step on one of them is `Inferred`). */
|
|
161
|
+
readonly linked: readonly ProcessLink[];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const STEP_ORDER: Record<StepKind, number> = { Planned: 0, Dispatched: 1, Delivered: 2, Review: 3, Fix: 4, Merged: 5, QC: 6, Walkthrough: 6, Accepted: 8, 'Handed to': 9, 'Handed in': 9 };
|
|
165
|
+
|
|
166
|
+
const short = (h: string): string => h.slice(0, 7);
|
|
167
|
+
export const statusOf = (a: ArrangementFact): string => (a.data.status ?? a.data.fields?.status ?? '').trim();
|
|
168
|
+
const baseName = (p: string): string => p.split(/[\\/]/).pop() ?? p;
|
|
169
|
+
|
|
170
|
+
/** The id of the send-back a failed check or a host's `needs-repair` review starts (§1.18); sendbacks.ts records it. */
|
|
171
|
+
export const sendBackIdFor = (projectId: string, originEntryId: string): string => stableId('sb', projectId, 'origin', originEntryId);
|
|
172
|
+
|
|
173
|
+
export interface WorkContext {
|
|
174
|
+
readonly store: ProjectStore;
|
|
175
|
+
readonly project: Project;
|
|
176
|
+
readonly facts: Facts;
|
|
177
|
+
readonly index: UnitIndex;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** A step to push: every step names the task that made it (`unit`, e.g. `AD` for AB's fix step), or null for a judged link. */
|
|
181
|
+
type StepInput = Omit<ProcessStepView, 'id' | 'unit'> & { readonly key: string; readonly unit: string | null };
|
|
182
|
+
|
|
183
|
+
// ───────────────────────── building a work item's process ─────────────────────────
|
|
184
|
+
|
|
185
|
+
export function processOf(ctx: WorkContext, thread: WorkThread): WorkProcess {
|
|
186
|
+
const { facts, index, store } = ctx;
|
|
187
|
+
const own = (index.threadNums.get(thread.id) ?? []).filter((n) => index.units.has(n));
|
|
188
|
+
const chain = chainOf(index, own);
|
|
189
|
+
const steps: ProcessStepView[] = [];
|
|
190
|
+
const cited = new Set<string>();
|
|
191
|
+
const push = (s: StepInput) => {
|
|
192
|
+
const { key, ...rest } = s;
|
|
193
|
+
for (const e of rest.evidence) cited.add(e.id);
|
|
194
|
+
if (steps.some((x) => x.id === stableId('step', thread.id, rest.kind, key))) return;
|
|
195
|
+
steps.push({ id: stableId('step', thread.id, rest.kind, key), ...rest });
|
|
196
|
+
};
|
|
197
|
+
// Work of an earlier generation, or no longer current, lies in history (D82). CZ: not work that was carried on under
|
|
198
|
+
// the same number — that is the current plan's.
|
|
199
|
+
const history = thread.validity !== 'Current' || earlierWork(store).has(thread.id);
|
|
200
|
+
|
|
201
|
+
// ── the work's own units ──
|
|
202
|
+
const ownUnits = own.map((n) => index.units.get(n)!);
|
|
203
|
+
const delivery = deliveryOf(ctx, thread, ownUnits);
|
|
204
|
+
const reviews: ReviewRecord[] = [];
|
|
205
|
+
const checks: CheckRecord[] = [];
|
|
206
|
+
const fixes: FixRecord[] = [];
|
|
207
|
+
|
|
208
|
+
if (delivery.planned) {
|
|
209
|
+
push({ key: 'planned', kind: 'Planned', occurred: delivery.planned.occurred, did: delivery.planned.text, unit: delivery.planned.unit, result: delivery.planned.evidence.label, verdict: null, who: null, evidence: [delivery.planned.evidence], basis: delivery.planned.basis, history, sendBackId: null });
|
|
210
|
+
}
|
|
211
|
+
for (const u of ownUnits) {
|
|
212
|
+
const d = dispatchOf(u);
|
|
213
|
+
if (d) {
|
|
214
|
+
const who = whoOf(u, d);
|
|
215
|
+
const status = statusOf(d);
|
|
216
|
+
push({
|
|
217
|
+
key: `dispatch:${u.num}`, kind: 'Dispatched', occurred: d.occurred, did: `${u.num} dispatched${u.title ? ` — ${clip(stripNum(u.title, u.num), 80)}` : ''}`, unit: u.num,
|
|
218
|
+
result: [who ? `to ${who}` : null, status ? `status ${status}` : null].filter(Boolean).join(' · ') || 'task written',
|
|
219
|
+
verdict: null, who, evidence: [facts.ref(d.id, status ? `status: ${status}` : d.data.title ?? null)], basis: 'Explicit', history: history || !arrangementStillThere(facts, d.path), sendBackId: null,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
for (const r of reviewsOf(ctx, u)) {
|
|
223
|
+
reviews.push(r);
|
|
224
|
+
pushReview(push, ctx, u, r, history);
|
|
225
|
+
}
|
|
226
|
+
if (u.nature === 'check') {
|
|
227
|
+
// A check unit that is itself the work item: independent of what it checks when it checks another unit.
|
|
228
|
+
for (const c of checksOf(ctx, u, u.relations.length ? 'Independent QC' : 'Self-reported')) { checks.push(c); pushCheck(push, ctx, c, history); }
|
|
229
|
+
} else {
|
|
230
|
+
const mine = delivery.commits.filter((c) => (index.commitUnits.get(c.hash) ?? []).includes(u.num));
|
|
231
|
+
if (mine.length || u.receipts.length || missingReceiptOf(facts, u)) pushDelivery(push, ctx, u, mine, 'Delivered', history);
|
|
232
|
+
for (const c of checksOf(ctx, u, 'Self-reported')) checks.push(c);
|
|
233
|
+
}
|
|
234
|
+
if (u.merges.length) pushMerges(push, ctx, u, u.merges);
|
|
235
|
+
const accepted = acceptanceOf(u);
|
|
236
|
+
if (accepted) push({ key: `accepted:${accepted.entry.id}`, kind: 'Accepted', occurred: accepted.entry.occurred, did: `${u.num} accepted`, unit: u.num, result: accepted.line, verdict: null, who: null, evidence: [facts.ref(accepted.entry.id, accepted.line)], basis: 'Explicit', history, sendBackId: null });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ── the units that fix or check it (§2.12: related tasks name the work's number) ──
|
|
240
|
+
for (const n of chain) {
|
|
241
|
+
if (own.includes(n)) continue;
|
|
242
|
+
const u = index.units.get(n)!;
|
|
243
|
+
const targets = u.relations.filter((r) => chain.includes(r.num)).map((r) => r.num);
|
|
244
|
+
for (const r of reviewsOf(ctx, u)) { reviews.push(r); pushReview(push, ctx, u, r, history); }
|
|
245
|
+
const isCheck = u.nature === 'check' || (u.nature === 'work' && u.relations.some((r) => r.as === 'check'));
|
|
246
|
+
if (isCheck) {
|
|
247
|
+
const found = checksOf(ctx, u, 'Independent QC');
|
|
248
|
+
for (const c of found) { checks.push(c); pushCheck(push, ctx, c, history, targets); }
|
|
249
|
+
if (found.length === 0) {
|
|
250
|
+
const d = dispatchOf(u);
|
|
251
|
+
if (d) push({ key: `check-dispatched:${u.num}`, kind: u.checkKind ?? 'QC', occurred: d.occurred, did: `${u.num} ${checkWord(u.checkKind ?? 'QC')} of ${targets.join(', ') || 'it'} dispatched`, unit: u.num, result: 'no report yet', verdict: null, who: whoOf(u, d), evidence: [facts.ref(d.id, statusOf(d) ? `status: ${statusOf(d)}` : null)], basis: 'Explicit', history, sendBackId: null });
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
const at = pushDelivery(push, ctx, u, u.commits.filter((c) => !c.merge), 'Fix', history, targets);
|
|
255
|
+
const d = dispatchOf(u);
|
|
256
|
+
const task = u.prompts[0] ?? d;
|
|
257
|
+
const merge = u.merges[0];
|
|
258
|
+
const onTrunk = merge ? null : u.commits.filter((c) => !c.merge && c.onTrunk).sort((x, y) => x.ms - y.ms)[0];
|
|
259
|
+
if (at) {
|
|
260
|
+
fixes.push({
|
|
261
|
+
unit: u.num, ms: at.ms, occurred: at.occurred, evidence: at.evidence, merges: u.merges, targets, delivered: u.commits.some((c) => !c.merge) || u.receipts.length > 0,
|
|
262
|
+
dispatched: d ? { ms: d.ms, occurred: d.occurred, evidence: facts.ref(d.id, statusOf(d) ? `status: ${statusOf(d)}` : d.data.title ?? null) } : null,
|
|
263
|
+
taskWritten: task ? { ms: task.ms, occurred: task.occurred, evidence: facts.ref(task.id, task.data.title ?? (statusOf(task) ? `status: ${statusOf(task)}` : null)) } : null,
|
|
264
|
+
landed: merge ? { ms: merge.ms, occurred: merge.occurred, evidence: facts.ref(merge.id) } : onTrunk ? { ms: onTrunk.ms, occurred: onTrunk.occurred, evidence: facts.ref(onTrunk.id) } : null,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ── findings of the checks: handled, handed to another unit, or still open ──
|
|
271
|
+
const findings = findingsOf(ctx, checks, chain);
|
|
272
|
+
for (const f of findings) {
|
|
273
|
+
if (f.handled?.how !== 'handed' || !f.handled.to) continue;
|
|
274
|
+
push({ key: `handed:${f.finding.id}:${f.handled.to}`, kind: 'Handed to', occurred: f.handled.evidence.occurred ?? f.check.occurred, did: `${f.num} (${f.check.unit}) handed to ${f.handled.to}`, unit: f.check.unit, result: clip(f.handled.evidence.line ?? f.handled.evidence.label, 120), verdict: null, who: null, evidence: [facts.ref(f.finding.id, f.finding.text), f.handled.evidence], basis: 'Explicit', history, sendBackId: null });
|
|
275
|
+
}
|
|
276
|
+
for (const h of handedIn(ctx, own)) push({ ...h, history });
|
|
277
|
+
const openItems = openItemsOfChecks(ctx, checks);
|
|
278
|
+
|
|
279
|
+
// ── links the round confirmed (§1.4: what the program could not tie by ids) ──
|
|
280
|
+
// CJ: a link not confirmed yet is shown too, as Inferred and saying so — not as "no delivery linked yet" — but only a
|
|
281
|
+
// confirmed one counts for the execution and the breakpoints. One step per work item, step and file; a check whose
|
|
282
|
+
// file is the work item's own is not its check (a work item is never the QC of itself).
|
|
283
|
+
const linked: ProcessLink[] = [];
|
|
284
|
+
const mine = new Set(ownNumbersOf(thread));
|
|
285
|
+
const shownFiles = new Set<string>();
|
|
286
|
+
// A report the chain's own checks already show is not shown again as a link or a relation.
|
|
287
|
+
for (const c of checks) for (const k of [c.kind, 'QC']) shownFiles.add(`${k}\u0000${c.reportPath.toLowerCase()}`);
|
|
288
|
+
const links = store.links.filter((x) => x.workId === thread.id).sort((a, b) => Number(linkHolds(b)) - Number(linkHolds(a)) || Number(b.confirmed) - Number(a.confirmed) || a.at.localeCompare(b.at));
|
|
289
|
+
for (const l of links) {
|
|
290
|
+
if (cited.has(l.ledgerRef) || (l.evidence && cited.has(l.evidence.id))) continue;
|
|
291
|
+
const checkKind = l.stepKind === 'QC' || l.stepKind === 'Review' || l.stepKind === 'Walkthrough';
|
|
292
|
+
const file = linkFile(facts, l);
|
|
293
|
+
if (file && checkKind && fileOwners(facts, file).some((n) => mine.has(n))) continue;
|
|
294
|
+
const fileKey = file ? `${l.stepKind}\u0000${file.toLowerCase()}` : null;
|
|
295
|
+
if (fileKey && shownFiles.has(fileKey)) continue;
|
|
296
|
+
if (fileKey) shownFiles.add(fileKey);
|
|
297
|
+
// CM: a lane-checked link (its evidence passed the program's check when written) counts like a confirmed one.
|
|
298
|
+
if (linkHolds(l)) linked.push(l);
|
|
299
|
+
const ev: EvidenceRef = l.evidence ?? facts.ref(l.ledgerRef);
|
|
300
|
+
const occurred = ev.occurred ?? { at: l.at, basis: 'First observed' as const, anchor: l.ledgerRef, undated: true };
|
|
301
|
+
const result = clip(ev.line ?? ev.label, 120);
|
|
302
|
+
push({
|
|
303
|
+
key: `link:${l.id}`, kind: l.stepKind, occurred, did: clip(l.why, 120), unit: null,
|
|
304
|
+
result: l.confirmed ? result : l.check?.passed ? `Lane-checked · ${result}` : l.check ? `Suspect, not confirmed · ${result}` : `Inferred, not confirmed yet · ${result}`,
|
|
305
|
+
verdict: checkKind && ev.line ? verdictAsWritten(ev.line) : null, who: null, evidence: [ev], basis: 'Inferred', history, sendBackId: null, link: linkStanding(l),
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
// CJ: a check recorded as a `verifies` relation — from the report (its source) or a Review or Test item to this work
|
|
309
|
+
// item, or to the Requirement of the same number — is its QC step, unless its report already made one.
|
|
310
|
+
for (const r of store.relations.filter((x) => x.type === 'verifies' && verifiesThis(store, thread, x.to))) {
|
|
311
|
+
const src = store.sources.get(r.from);
|
|
312
|
+
const item = src ? null : store.reference.get(r.from);
|
|
313
|
+
const file = src && src.anchor.kind === 'file' ? relativeFile(facts, src.anchor.path) : null;
|
|
314
|
+
if (file && fileOwners(facts, file).some((n) => mine.has(n))) continue;
|
|
315
|
+
const fileKey = file ? `QC\u0000${file.toLowerCase()}` : null;
|
|
316
|
+
if (fileKey && shownFiles.has(fileKey)) continue;
|
|
317
|
+
if (fileKey) shownFiles.add(fileKey);
|
|
318
|
+
if (src && cited.has(src.id)) continue;
|
|
319
|
+
const ev: EvidenceRef = src ? { kind: 'source', id: src.id, label: file ?? src.title, line: null, occurred: null } : { kind: 'object', id: r.from, label: item?.name ?? r.from, line: null, occurred: null };
|
|
320
|
+
const firstVersion = src && file ? (facts.docsByPath.get(file) ?? [])[0] : undefined;
|
|
321
|
+
const occurred = firstVersion ? versionOccurred(facts, firstVersion) : { at: r.updatedAt, basis: 'First observed' as const, anchor: r.id, undated: true };
|
|
322
|
+
push({ key: `verifies:${r.id}`, kind: 'QC', occurred, did: clip(r.claim, 120), unit: null, result: clip(file ?? ev.label, 120), verdict: null, who: null, evidence: [ev], basis: r.basis === 'Explicit' ? 'Explicit' : 'Inferred', history, sendBackId: null });
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
steps.sort((a, b) => msOfOccurred(a.occurred) - msOfOccurred(b.occurred) || STEP_ORDER[a.kind] - STEP_ORDER[b.kind] || a.id.localeCompare(b.id));
|
|
326
|
+
const execution = executionOf(thread, delivery, ownUnits, checks, linked, (l) => linkedOnTrunk(facts, l));
|
|
327
|
+
const latestCheck = latestCheckOf(checks, ownUnits);
|
|
328
|
+
return { thread, own, chain, steps, delivery, checks, reviews, fixes, findings, openItems, execution, latestCheck, linked };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const stripNum = (title: string, num: string): string => title.replace(new RegExp(`^${baseNumber(num).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*[—–·::-]*\\s*`), '');
|
|
332
|
+
const checkWord = (k: CheckKind): string => (k === 'QC' ? 'QC' : k.toLowerCase());
|
|
333
|
+
|
|
334
|
+
function arrangementStillThere(facts: Facts, path: string): boolean {
|
|
335
|
+
return (facts.arrangementsByPath.get(path) ?? []).some((a) => a.current) || (facts.docsByPath.get(path) ?? []).some((d) => d.current);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ───────────────────────── the own unit: planned, dispatched, delivered ─────────────────────────
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* When the work was handed out: the prompt's first running version; when no version says running but the work went on
|
|
342
|
+
* (a later status, commits, a receipt), its first version — the task written is the task handed out; a task only ever
|
|
343
|
+
* ready or queued was not dispatched. A run status file counts when it is earlier.
|
|
344
|
+
*/
|
|
345
|
+
export function dispatchOf(u: Unit): ArrangementFact | null {
|
|
346
|
+
const running = u.prompts.find((p) => RUNNING_STATUS.test(statusOf(p)));
|
|
347
|
+
const wentOn = u.prompts.some((p) => statusOf(p) && !PLANNED_STATUS.test(statusOf(p))) || u.commits.length > 0 || u.receipts.length > 0;
|
|
348
|
+
const fromPrompt = running ?? (wentOn ? u.prompts[0] ?? null : null);
|
|
349
|
+
const run = u.runs[0] ?? null;
|
|
350
|
+
if (run && (!fromPrompt || run.ms < fromPrompt.ms)) return run;
|
|
351
|
+
return fromPrompt;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function whoOf(u: Unit, d: ArrangementFact): string | null {
|
|
355
|
+
const f = d.data.fields ?? {};
|
|
356
|
+
const executor = f.executor ?? f.agent ?? d.data.agent ?? u.executor;
|
|
357
|
+
const model = f.requested_model ?? f.model ?? d.data.model ?? null;
|
|
358
|
+
const worktree = f.worktree ?? d.data.worktree ?? null;
|
|
359
|
+
const parts = [executor ? `${executor}${model && !executor.toLowerCase().includes(model.toLowerCase()) ? ` (${model})` : ''}` : null, worktree ? `worktree ${baseName(worktree.split(' ')[0]!)}` : null];
|
|
360
|
+
return parts.filter(Boolean).join(' · ') || null;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** A receipt the prompt names (`report: …`) that the ledger never saw in any version. */
|
|
364
|
+
export function missingReceiptOf(facts: Facts, u: Unit): string | null {
|
|
365
|
+
const named = u.fields.report ?? u.fields.receipt ?? null;
|
|
366
|
+
if (!named || u.receipts.length) return null;
|
|
367
|
+
const rel = named.replace(/\\/g, '/').trim();
|
|
368
|
+
return facts.hasDocument(rel) ? null : rel.replace(/^[A-Za-z]:\/[^/]+\//, '');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function deliveryOf(ctx: WorkContext, thread: WorkThread, units: readonly Unit[]): Delivery {
|
|
372
|
+
const { facts, store, project } = ctx;
|
|
373
|
+
const commits = [...new Map(units.flatMap((u) => u.commits).map((c) => [c.hash, c])).values()].sort((a, b) => a.ms - b.ms);
|
|
374
|
+
const codeCommits = commits.filter((c) => facts.filesOf(c).some((f) => !isArrangementPath(f.path)));
|
|
375
|
+
const merges = [...new Map(units.flatMap((u) => u.merges).map((c) => [c.hash, c])).values()].sort((a, b) => a.ms - b.ms);
|
|
376
|
+
const receipts = units.flatMap((u) => u.receipts).sort((a, b) => a.ms - b.ms);
|
|
377
|
+
const dispatches = units.map(dispatchOf).filter((d): d is ArrangementFact => d !== null).sort((a, b) => a.ms - b.ms);
|
|
378
|
+
const dispatched = dispatches[0] ? { occurred: dispatches[0].occurred, ms: dispatches[0].ms, entry: dispatches[0] } : null;
|
|
379
|
+
// Planned: the plan version it entered, or its task written and queued before it was dispatched.
|
|
380
|
+
let planned: Delivery['planned'] = null;
|
|
381
|
+
const plan = plansOf(store, thread)[0];
|
|
382
|
+
if (plan) {
|
|
383
|
+
const doc = planDocument(store, facts, plan);
|
|
384
|
+
const batched = units.find((u) => batchOf(u) !== null) ?? null;
|
|
385
|
+
const batch = batched ? batchOf(batched) : null;
|
|
386
|
+
if (doc?.first) {
|
|
387
|
+
const occurred = versionOccurred(facts, doc.first);
|
|
388
|
+
const saysSo = units.some((u) => u.prompts.some((p) => `${p.data.title ?? ''} ${Object.values(p.data.fields ?? {}).join(' ')}`.includes(doc.path)));
|
|
389
|
+
planned = {
|
|
390
|
+
occurred, ms: msOfOccurred(occurred), basis: saysSo ? 'Explicit' : 'Inferred',
|
|
391
|
+
text: `Planned in ${plan.name}${batch ? ` as batch ${batch.batch}${batch.part ?? ''}` : ''}`,
|
|
392
|
+
evidence: facts.ref(doc.first.id, doc.section ? doc.headingPath[doc.headingPath.length - 1]! : null),
|
|
393
|
+
unit: batched?.num ?? units[0]?.num ?? null,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const queued = units.flatMap((u) => {
|
|
398
|
+
const firstRunning = u.prompts.find((p) => RUNNING_STATUS.test(statusOf(p)));
|
|
399
|
+
const q = u.prompts.find((p) => PLANNED_STATUS.test(statusOf(p)));
|
|
400
|
+
return q && firstRunning && q.ms < firstRunning.ms ? [{ q, u }] : [];
|
|
401
|
+
}).sort((a, b) => a.q.ms - b.q.ms)[0];
|
|
402
|
+
if (queued && (!planned || queued.q.ms < planned.ms)) {
|
|
403
|
+
const { q, u } = queued;
|
|
404
|
+
planned = { occurred: q.occurred, ms: q.ms, basis: 'Explicit', text: `${u.num} task written, not dispatched yet`, evidence: facts.ref(q.id, `status: ${statusOf(q)}`), unit: u.num };
|
|
405
|
+
}
|
|
406
|
+
if (planned && dispatched && planned.ms >= dispatched.ms) planned = null;
|
|
407
|
+
// Reported done: the arrangement's status first, else the work item's progress.
|
|
408
|
+
const doneVersion = units.flatMap((u) => u.prompts.filter((p) => DONE_STATUS.test(statusOf(p)))).sort((a, b) => a.ms - b.ms)[0];
|
|
409
|
+
let reportedDone: Delivery['reportedDone'] = null;
|
|
410
|
+
if (doneVersion) reportedDone = { occurred: doneVersion.occurred, ms: doneVersion.ms, evidence: facts.ref(doneVersion.id, `status: ${statusOf(doneVersion)}`) };
|
|
411
|
+
else if (thread.progress === 'Done') {
|
|
412
|
+
const r = receipts[0];
|
|
413
|
+
reportedDone = r ? { occurred: r.occurred, ms: r.ms, evidence: facts.ref(r.id) } : { occurred: { at: thread.asOf || thread.updatedAt, basis: 'First observed', anchor: thread.id, undated: true }, ms: Date.parse(thread.asOf || thread.updatedAt) || 0, evidence: null };
|
|
414
|
+
}
|
|
415
|
+
const unmerged = codeCommits.length > 0 && !codeCommits.some((c) => c.onTrunk);
|
|
416
|
+
const branches = [...new Set(units.flatMap((u) => u.branches))];
|
|
417
|
+
const lastCode = codeCommits[codeCommits.length - 1];
|
|
418
|
+
const deliveredAt = lastCode ? { ms: lastCode.ms, occurred: lastCode.occurred } : receipts[0] ? { ms: receipts[0].ms, occurred: receipts[0].occurred } : null;
|
|
419
|
+
return {
|
|
420
|
+
commits, codeCommits, merges, receipts, missingReceipt: units.map((u) => missingReceiptOf(facts, u)).find((x) => x !== null) ?? null, dispatched, planned, reportedDone,
|
|
421
|
+
unmerged, branches, experiment: experimentOf(ctx, project, units, branches), deliveredAt,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const EXPERIMENT_WORDS = /\bexperiment(?:al)?\b|实验|试验性|\babandon(?:ed)?\b|放弃|\bwithdrawn\b|撤回|\bsuperseded\b|作废|废弃/i;
|
|
426
|
+
|
|
427
|
+
/** §2.12 `Not merged` does not light when the project wrote that the work is an experiment or abandoned. */
|
|
428
|
+
function experimentOf(ctx: WorkContext, project: Project, units: readonly Unit[], branches: readonly string[]): EvidenceRef | null {
|
|
429
|
+
for (const u of units) {
|
|
430
|
+
const p = u.prompts[u.prompts.length - 1];
|
|
431
|
+
if (!p) continue;
|
|
432
|
+
for (const k of ['status', 'kind', 'note', 'result']) {
|
|
433
|
+
const v = u.fields[k];
|
|
434
|
+
if (v && EXPERIMENT_WORDS.test(v)) return ctx.facts.ref(p.id, `${k}: ${v}`);
|
|
435
|
+
}
|
|
436
|
+
const wt = u.fields.worktree ?? p.data.worktree ?? null;
|
|
437
|
+
if (wt) {
|
|
438
|
+
const name = baseName(wt.split(' ')[0]!).toLowerCase();
|
|
439
|
+
const item = project.scope.find((i) => i.relation === 'Experiment' && baseName(i.path).toLowerCase() === name);
|
|
440
|
+
if (item) return { kind: 'object', id: item.id, label: `${item.path} (scope: Experiment)`, line: item.reason || null, occurred: null };
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const b = branches.find((x) => /(?:^|\/)(?:exp|experiment|experiments|spike|poc|scratch)[-_/]/i.test(x));
|
|
444
|
+
if (b) return { kind: 'object', id: `branch:${b}`, label: `branch ${b}`, line: null, occurred: null };
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function pushDelivery(push: (s: StepInput) => void, ctx: WorkContext, u: Unit, commits: readonly CommitFact[], kind: 'Delivered' | 'Fix', history: boolean, targets: readonly string[] = []): { ms: number; occurred: Occurred; evidence: EvidenceRef[] } | null {
|
|
449
|
+
const { facts } = ctx;
|
|
450
|
+
const code = commits.filter((c) => facts.filesOf(c).some((f) => !isArrangementPath(f.path)));
|
|
451
|
+
const shown = code.length ? code : commits;
|
|
452
|
+
const receipts = u.receipts;
|
|
453
|
+
const d = dispatchOf(u);
|
|
454
|
+
const missing = missingReceiptOf(facts, u);
|
|
455
|
+
if (shown.length === 0 && receipts.length === 0 && !d) return null;
|
|
456
|
+
const last = shown[shown.length - 1];
|
|
457
|
+
const at = last ? { occurred: last.occurred, ms: last.ms } : receipts[0] ? { occurred: receipts[0].occurred, ms: receipts[0].ms } : { occurred: d!.occurred, ms: d!.ms };
|
|
458
|
+
const onTrunk = shown.filter((c) => c.onTrunk);
|
|
459
|
+
const merge = u.merges[u.merges.length - 1];
|
|
460
|
+
const branch = u.branches[0] ?? null;
|
|
461
|
+
const parts: string[] = [];
|
|
462
|
+
if (shown.length) parts.push(`${shown.length} commit${shown.length === 1 ? '' : 's'} ${shown.length === 1 ? short(shown[0]!.hash) : `${short(shown[0]!.hash)}…${short(last!.hash)}`}${branch ? ` on ${branch}` : ''}`);
|
|
463
|
+
if (merge) parts.push(`merged ${short(merge.hash)}`);
|
|
464
|
+
else if (shown.length && onTrunk.length === shown.length) parts.push('on the trunk');
|
|
465
|
+
else if (shown.length && onTrunk.length === 0) parts.push('not merged');
|
|
466
|
+
const counts = receipts.flatMap((r) => (facts.verdictsByPath.get(r.path) ?? []).filter((v) => v.kind === 'count' && v.confidence === 'stated' && isTestCount(v.text, v.verdict)));
|
|
467
|
+
const uniqueCounts = [...new Map(counts.map((c) => [c.id, c])).values()];
|
|
468
|
+
if (uniqueCounts[0]) parts.push(`tests ${uniqueCounts[0].verdict}${uniqueCounts.length > 1 ? ` (+${uniqueCounts.length - 1} more counts)` : ''}`);
|
|
469
|
+
const receipt = receipts[receipts.length - 1];
|
|
470
|
+
const claim = receipt ? claimLineOf(facts.textOf(receipt.id)) : null;
|
|
471
|
+
if (missing) parts.push(`no receipt: ${baseName(missing)}, which the prompt names, was never committed`);
|
|
472
|
+
const hostEvidence = !receipts.length ? u.evidence[0] : undefined;
|
|
473
|
+
const hostLine = hostEvidence ? evidenceLineOf(facts.textOf(hostEvidence.id), u.num) : null;
|
|
474
|
+
if (hostEvidence) parts.push(`the host's evidence ${baseName(hostEvidence.path)}${hostLine ? `: ${clip(hostLine, 80)}` : ''}`);
|
|
475
|
+
if (receipt) parts.push(`${baseName(receipt.path)}${claim ? ` (${clip(claim, 80)})` : ''}`);
|
|
476
|
+
if (shown.length === 0 && receipts.length === 0) parts.push('dispatched, nothing delivered yet');
|
|
477
|
+
const evidence: EvidenceRef[] = [
|
|
478
|
+
...(shown.length ? [facts.ref(shown[0]!.id)] : []),
|
|
479
|
+
...(shown.length > 1 ? [facts.ref(last!.id)] : []),
|
|
480
|
+
...(merge ? [facts.ref(merge.id)] : []),
|
|
481
|
+
...(receipt ? [facts.ref(receipt.id, claim)] : []),
|
|
482
|
+
...(hostEvidence ? [facts.ref(hostEvidence.id, hostLine)] : []),
|
|
483
|
+
...(uniqueCounts[0] ? [facts.ref(uniqueCounts[0].id, uniqueCounts[0].text)] : []),
|
|
484
|
+
...(!shown.length && !receipts.length && d ? [facts.ref(d.id)] : []),
|
|
485
|
+
];
|
|
486
|
+
const fixOf = kind === 'Fix' && targets.length ? ` (fixes ${targets.join(', ')})` : '';
|
|
487
|
+
push({
|
|
488
|
+
key: `${kind}:${u.num}`, kind, occurred: at.occurred,
|
|
489
|
+
did: `${u.num} ${kind === 'Fix' ? 'fix' : 'delivered'}${fixOf}${u.title ? ` — ${clip(stripNum(u.title, u.num), 70)}` : ''}`, unit: u.num,
|
|
490
|
+
result: parts.join(' · '), verdict: null, who: d ? whoOf(u, d) : u.executor, evidence, basis: 'Explicit',
|
|
491
|
+
history: history || (shown.length > 0 && onTrunk.length === 0 && u.merges.length === 0 && u.branches.length === 0), sendBackId: null,
|
|
492
|
+
});
|
|
493
|
+
return { ms: at.ms, occurred: at.occurred, evidence };
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** Its merges into the trunk as one step, when the work landed: a branch merged several times as it went is one landing. */
|
|
497
|
+
function pushMerges(push: (s: StepInput) => void, ctx: WorkContext, u: Unit, merges: readonly CommitFact[]): void {
|
|
498
|
+
const { facts, index } = ctx;
|
|
499
|
+
const last = merges[merges.length - 1]!;
|
|
500
|
+
const others = [...new Set([...index.commitUnits.entries()].filter(([h]) => merges.some((m) => facts.introducedBy.get(h) === m.hash)).flatMap(([, us]) => us))].filter((n) => n !== u.num);
|
|
501
|
+
push({
|
|
502
|
+
key: `merge:${last.hash}`, kind: 'Merged', occurred: last.occurred,
|
|
503
|
+
did: `${u.num} merged into the trunk${others.length ? ` together with ${others.join(', ')}` : ''}${merges.length > 1 ? ` (${merges.length} merges)` : ''}`, unit: u.num,
|
|
504
|
+
result: merges.length > 1 ? `merges ${merges.map((m) => short(m.hash)).join(', ')} · last: ${clip(last.subject, 70)}` : `merge ${short(last.hash)} · ${clip(last.subject, 90)}`,
|
|
505
|
+
verdict: null, who: null, evidence: merges.map((m) => facts.ref(m.id)), basis: 'Explicit', history: false, sendBackId: null,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function pushReview(push: (s: StepInput) => void, ctx: WorkContext, u: Unit, r: ReviewRecord, history: boolean): void {
|
|
510
|
+
const { facts, store, project } = ctx;
|
|
511
|
+
const said = r.receipts.map((x) => ({ x, line: isHostEvidence(x.path) ? evidenceLineOf(facts.textOf(x.id), u.num) : rootCommentLineOf(facts.textOf(x.id)) ?? evidenceLineOf(facts.textOf(x.id), u.num) }));
|
|
512
|
+
const first = said.find((x) => x.line) ?? said[0];
|
|
513
|
+
push({
|
|
514
|
+
key: `review:${r.entry.id}`, kind: 'Review', occurred: r.occurred, did: `${r.who ?? 'The host'} reviewed ${u.num}`, unit: u.num, verdict: r.status,
|
|
515
|
+
result: [r.status, first ? `${baseName(first.x.path)}${first.line ? `: ${clip(first.line, 100)}` : ''}` : null].filter(Boolean).join(' · '),
|
|
516
|
+
who: r.who, evidence: [facts.ref(r.entry.id, `status: ${r.status}`), ...said.map((x) => facts.ref(x.x.id, x.line))], basis: 'Explicit', history,
|
|
517
|
+
sendBackId: sendBackOf(store, project.id, r.entry.id),
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// ───────────────────────── reviews by the host, and checks ─────────────────────────
|
|
522
|
+
|
|
523
|
+
/** The host's reviews written into the arrangement: each change of the status to `needs-repair` or the like. */
|
|
524
|
+
export function reviewsOf(ctx: WorkContext, u: Unit): ReviewRecord[] {
|
|
525
|
+
const out: ReviewRecord[] = [];
|
|
526
|
+
let previous = '';
|
|
527
|
+
for (const p of u.prompts) {
|
|
528
|
+
const s = statusOf(p);
|
|
529
|
+
if (s && REPAIR_STATUS.test(s) && s !== previous) {
|
|
530
|
+
// Who reviewed: the review field's `root = …`, else the host that dispatched it (`dispatched_by: root (…)`).
|
|
531
|
+
const rr = p.data.fields?.root_review ?? p.data.fields?.review ?? '';
|
|
532
|
+
const by = p.data.fields?.dispatched_by ?? '';
|
|
533
|
+
const who = /root\s*=\s*([^;,]+)/i.exec(rr)?.[1]?.trim() ?? /\broot\s*\(([^)]+)\)/i.exec(by)?.[1]?.trim() ?? null;
|
|
534
|
+
// What was written with the review: this task's receipts and the host's evidence in the same commit.
|
|
535
|
+
const receipts = [...u.receipts, ...u.evidence].filter((a) => a.commit === p.commit).sort((x, y) => x.ms - y.ms || x.path.localeCompare(y.path));
|
|
536
|
+
out.push({ unit: u.num, status: s, entry: p, receipts, occurred: p.occurred, ms: p.ms, who: who ? `root (${who})` : rr || /\broot\b/i.test(by) ? 'root' : null });
|
|
537
|
+
}
|
|
538
|
+
previous = s;
|
|
539
|
+
}
|
|
540
|
+
return out;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/** The checks one unit's reports record: the overall verdict, its findings and counts. */
|
|
544
|
+
export function checksOf(ctx: WorkContext, u: Unit, by: 'Independent QC' | 'Self-reported'): CheckRecord[] {
|
|
545
|
+
const { facts } = ctx;
|
|
546
|
+
const out: CheckRecord[] = [];
|
|
547
|
+
const paths = [...new Set(u.receipts.map((r) => r.path))];
|
|
548
|
+
for (const path of paths) {
|
|
549
|
+
const rows = facts.verdictsByPath.get(path) ?? [];
|
|
550
|
+
const stated = rows.filter((v) => v.kind === 'verdict' && v.confidence === 'stated').sort((a, b) => a.ms - b.ms || (a.line ?? 0) - (b.line ?? 0));
|
|
551
|
+
// Section-heading verdicts (`## 1. 登录 — pass`) are how a walkthrough reports; in a delivery's own receipt they are
|
|
552
|
+
// as often the state before the fix, so they count only for a check unit.
|
|
553
|
+
const headings = u.nature === 'check' ? rows.filter((v) => v.kind === 'verdict' && v.confidence === 'candidate' && /^#{1,6}\s/.test(v.text)) : [];
|
|
554
|
+
const overall = stated[0] ?? null;
|
|
555
|
+
const fromCandidates = !overall && headings.length > 0;
|
|
556
|
+
// From section headings: a failing section fails the report; all passing, it passes; a section not finished leaves
|
|
557
|
+
// the whole in progress.
|
|
558
|
+
const fromHeadings = (): string | null => {
|
|
559
|
+
if (headings.some((h) => FAILING.has(h.verdict ?? ''))) return 'fail';
|
|
560
|
+
if (headings.every((h) => h.verdict === 'pass')) return 'pass';
|
|
561
|
+
if (headings.some((h) => IN_PROGRESS.has(h.verdict ?? ''))) return 'incomplete';
|
|
562
|
+
return headings[0]!.verdict;
|
|
563
|
+
};
|
|
564
|
+
const normalized = overall?.verdict ?? (fromCandidates ? fromHeadings() : null);
|
|
565
|
+
const inProgress = normalized !== null && IN_PROGRESS.has(normalized);
|
|
566
|
+
const findings = rows.filter((v) => v.kind === 'finding' && v.confidence === 'stated' && /^[A-Z]{1,3}-\d{1,3}$/.test(v.verdict ?? ''));
|
|
567
|
+
const counts = rows.filter((v) => v.kind === 'count' && v.confidence === 'stated');
|
|
568
|
+
if (!overall && !fromCandidates && findings.length === 0) continue;
|
|
569
|
+
const receipts = u.receipts.filter((r) => r.path === path);
|
|
570
|
+
const verdictRow = overall ?? headings[0] ?? null;
|
|
571
|
+
const occurred = verdictRow?.occurred ?? receipts[0]!.occurred;
|
|
572
|
+
const written = overall ? verdictAsWritten(overall.text) ?? overall.verdict : null;
|
|
573
|
+
out.push({
|
|
574
|
+
unit: u.num, kind: u.checkKind ?? 'QC', reportPath: path, report: receipts[receipts.length - 1] ?? null, firstReport: receipts[0] ?? null, verdict: verdictRow,
|
|
575
|
+
// A check still in progress keeps the words it said so with (`incomplete(审核进行中)`).
|
|
576
|
+
verdictWord: overall ? (written && inProgress ? withGloss(overall.text, written) : written) : fromCandidates ? (normalized === 'pass' ? `pass ×${headings.length}` : normalized) : null,
|
|
577
|
+
normalized, fromCandidates, occurred, ms: msOfOccurred(occurred), by, fromCheckUnit: u.nature === 'check', inProgress, findings, counts,
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
return out;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function pushCheck(push: (s: StepInput) => void, ctx: WorkContext, c: CheckRecord, history: boolean, targets: readonly string[] = []): void {
|
|
584
|
+
const { facts, store, project } = ctx;
|
|
585
|
+
const own = ownFindings(c);
|
|
586
|
+
const failing = startsSendBack(c);
|
|
587
|
+
const severity = own.map((f) => severityOf(f.text)).filter((x): x is string => Boolean(x));
|
|
588
|
+
const bySeverity = [...new Set(severity)].map((s) => `${severity.filter((x) => x === s).length} ${s}`).join(' / ');
|
|
589
|
+
const result = [
|
|
590
|
+
c.verdictWord ?? 'no verdict stated',
|
|
591
|
+
c.inProgress ? 'check in progress, no verdict yet' : null,
|
|
592
|
+
own.length ? `${own.length} finding${own.length === 1 ? '' : 's'}${bySeverity ? ` (${bySeverity})` : ''}` : null,
|
|
593
|
+
own[0] && failing ? clip(own[0].text.replace(/^#+\s*/, ''), 90) : null,
|
|
594
|
+
c.by === 'Self-reported' ? 'self-reported' : null,
|
|
595
|
+
].filter(Boolean).join(' · ');
|
|
596
|
+
push({
|
|
597
|
+
key: `check:${c.reportPath}`, kind: c.kind, occurred: c.occurred,
|
|
598
|
+
did: `${c.unit} ${c.by === 'Independent QC' ? 'independent ' : ''}${checkWord(c.kind)}${targets.length ? ` of ${targets.join(', ')}` : ''}`, unit: c.unit,
|
|
599
|
+
result, verdict: c.verdictWord, who: null,
|
|
600
|
+
evidence: [...(c.verdict ? [facts.ref(c.verdict.id, c.verdict.text)] : []), ...own.slice(0, 12).map((f) => facts.ref(f.id, f.text))],
|
|
601
|
+
basis: c.fromCandidates ? 'Inferred' : 'Explicit', history, sendBackId: failing && c.verdict ? sendBackOf(store, project.id, c.verdict.id) : null,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/** The findings a report introduces: the first line of each number, except a check re-reading an earlier finding (`→ 已修`). */
|
|
606
|
+
export function ownFindings(c: CheckRecord): VerdictFact[] {
|
|
607
|
+
const seen = new Set<string>();
|
|
608
|
+
const out: VerdictFact[] = [];
|
|
609
|
+
for (const f of [...c.findings].sort((a, b) => (a.line ?? 0) - (b.line ?? 0))) {
|
|
610
|
+
const n = f.verdict!;
|
|
611
|
+
if (seen.has(n)) continue;
|
|
612
|
+
seen.add(n);
|
|
613
|
+
if (/已修|已解决|已关闭|已处理|→\s*已|\bfixed\b|\bresolved\b|\bclosed\b/i.test(f.text)) continue;
|
|
614
|
+
out.push(f);
|
|
615
|
+
}
|
|
616
|
+
return out;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/** An existing send-back that started from this entry (a verdict, a review), whoever recorded it. */
|
|
620
|
+
export function sendBackOf(store: ProjectStore, projectId: string, originEntryId: string): string | null {
|
|
621
|
+
const id = sendBackIdFor(projectId, originEntryId);
|
|
622
|
+
if (store.sendbacks.has(id)) return id;
|
|
623
|
+
return store.sendbacks.find((s) => s.from.id === originEntryId)?.id ?? null;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function acceptanceOf(u: Unit): { entry: ArrangementFact; line: string } | null {
|
|
627
|
+
for (const p of u.prompts) {
|
|
628
|
+
for (const [k, v] of Object.entries(p.data.fields ?? {})) {
|
|
629
|
+
if (/^(owner_accept(?:ed|ance)?|accepted_by_owner|owner_sign_?off)$/i.test(k) && v.trim()) return { entry: p, line: `${k}: ${v}` };
|
|
630
|
+
if (/^(acceptance|accepted_by)$/i.test(k) && /\bowner\b|owner\s*验收/i.test(v)) return { entry: p, line: `${k}: ${v}` };
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// ───────────────────────── findings: handled or still open (§2.13 row 5) ─────────────────────────
|
|
637
|
+
|
|
638
|
+
/** A section of a report where it is dealt with what the report found: the root comment, the disposition. */
|
|
639
|
+
const DISPOSITION_HEADING = /root comment|root disposition|处置|disposition|response|回应|root 的|决定/i;
|
|
640
|
+
/** A line that does something with a finding: accepts it, fixes it, sends it on, leaves it for later. */
|
|
641
|
+
const DISPOSITION_VERB = /处置|接受|不处理|不修|修(?:复|掉|好)?|进下一批|下一批|并入|留给|留到|记下|延后|推迟|转给|交给|送回|\baccept|\bfix|\bdefer|\bwon'?t\b|\bhandled?\b|\bmoved? to\b|\bgoes? to\b/i;
|
|
642
|
+
|
|
643
|
+
function findingsOf(ctx: WorkContext, checks: readonly CheckRecord[], chain: readonly string[]): FindingState[] {
|
|
644
|
+
const { facts, index, store } = ctx;
|
|
645
|
+
const out: FindingState[] = [];
|
|
646
|
+
const definedAt = new Map<string, number>();
|
|
647
|
+
for (const c of [...checks].sort((a, b) => a.ms - b.ms)) {
|
|
648
|
+
const own = ownFindings(c).filter((f) => { const t = definedAt.get(f.verdict!); return t === undefined || t >= f.ms; });
|
|
649
|
+
for (const f of own) if (!definedAt.has(f.verdict!)) definedAt.set(f.verdict!, f.ms);
|
|
650
|
+
if (own.length === 0) continue;
|
|
651
|
+
const reportRows = facts.verdictsByPath.get(c.reportPath) ?? [];
|
|
652
|
+
const text = c.report ? facts.textOf(c.report.id) : null;
|
|
653
|
+
const lines = text ? text.split(/\r?\n/) : [];
|
|
654
|
+
const headingAt: string[] = [];
|
|
655
|
+
let heading = '';
|
|
656
|
+
lines.forEach((l, i) => { const m = /^\s{0,3}#{1,6}\s+(.*)$/.exec(l); if (m) heading = m[1]!; headingAt[i] = heading; });
|
|
657
|
+
const numRows = facts.numsOf(own.map((f) => f.verdict!));
|
|
658
|
+
const context = new Set<string>([c.unit, ...chain]);
|
|
659
|
+
for (const f of own) {
|
|
660
|
+
const num = f.verdict!;
|
|
661
|
+
let handled: Handling | null = null;
|
|
662
|
+
// 1. The same report deals with it further down: its root comment, its disposition. A clause that hands it to
|
|
663
|
+
// another unit (`F-4 并入正在跑的 AJ`) says where it went; else the first line that disposes of it.
|
|
664
|
+
const dispositions: Handling[] = [];
|
|
665
|
+
for (let i = f.line ?? 0; i < lines.length; i++) {
|
|
666
|
+
const l = lines[i]!;
|
|
667
|
+
if (!standsAlone(l, num) || /^\s{0,3}#/.test(l)) continue;
|
|
668
|
+
for (const clause of l.split(/[,,;;。]/).filter((x) => standsAlone(x, num))) {
|
|
669
|
+
if (!DISPOSITION_HEADING.test(headingAt[i] ?? '') && !DISPOSITION_VERB.test(clause)) continue;
|
|
670
|
+
const others = [...index.units.keys()].filter((n) => !context.has(n) && standsAlone(clause, n));
|
|
671
|
+
// Cite the ledger's own entry for that line (a verdict row or the number's row), which dates it by the version it first appeared in.
|
|
672
|
+
const row = reportRows.find((r) => r.line === i + 1 && r.id !== f.id) ?? (numRows.get(num) ?? []).find((r) => r.path === c.reportPath && r.line === i + 1);
|
|
673
|
+
const ev: EvidenceRef = row ? facts.ref(row.id, l.trim()) : { kind: 'file', id: c.reportPath, label: `${c.reportPath}:${i + 1}`, line: l.trim(), occurred: c.report?.occurred ?? c.occurred };
|
|
674
|
+
dispositions.push(others.length ? { how: 'handed', evidence: ev, to: others[0]! } : { how: 'disposed', evidence: ev });
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
handled = dispositions.find((d) => d.how === 'handed') ?? dispositions[0] ?? null;
|
|
678
|
+
// 2. A later document or commit of the chain names it (a fix prompt, the next QC).
|
|
679
|
+
if (!handled) {
|
|
680
|
+
const rows = numRows.get(num) ?? [];
|
|
681
|
+
const later = rows.filter((r) => r.ms >= f.ms && r.path !== c.reportPath && standsAlone(r.context, num))
|
|
682
|
+
.filter((r) => { const named = index.namedIn.get(r.kind === 'commit' ? `commit:${r.commit}` : `path:${r.path}`); return named !== undefined && [...context].some((n) => named.has(n)); })
|
|
683
|
+
.sort((a, b) => a.ms - b.ms)[0];
|
|
684
|
+
if (later) handled = { how: 'followed', evidence: facts.ref(later.id, later.context) };
|
|
685
|
+
else if (rows.length === 0) handled = followedLater(ctx, c, [num], context);
|
|
686
|
+
}
|
|
687
|
+
// 3. A send-back suggested for it carries it — not the one the failed check itself started, which covers the
|
|
688
|
+
// check as a whole and closes when a re-check passes, whatever became of this finding.
|
|
689
|
+
if (!handled) {
|
|
690
|
+
const sb = store.sendbacks.find((s) => s.from.id !== c.verdict?.id && s.evidence.some((e) => e.id === f.id));
|
|
691
|
+
if (sb) handled = { how: 'sent back', evidence: { kind: 'object', id: sb.id, label: `Send-back to ${sb.to}: ${sb.what}`, occurred: sb.occurred } };
|
|
692
|
+
}
|
|
693
|
+
out.push({ check: c, finding: f, num, handled });
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return out;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/** Findings of other units' checks that a report hands to this work (`F-4 并入正在跑的 AJ`): `Handed in`. */
|
|
700
|
+
function handedIn(ctx: WorkContext, own: readonly string[]): (Omit<StepInput, 'history'>)[] {
|
|
701
|
+
const { facts, index } = ctx;
|
|
702
|
+
const out: (Omit<StepInput, 'history'>)[] = [];
|
|
703
|
+
if (own.length === 0) return out;
|
|
704
|
+
const nums = facts.numsOf(own);
|
|
705
|
+
const seen = new Set<string>();
|
|
706
|
+
for (const n of own) {
|
|
707
|
+
for (const r of nums.get(n) ?? []) {
|
|
708
|
+
if (r.kind !== 'doc' || !r.path) continue;
|
|
709
|
+
const reportRows = facts.verdictsByPath.get(r.path);
|
|
710
|
+
if (!reportRows) continue;
|
|
711
|
+
const clause = r.context.split(/[,,;;。]/).find((x) => standsAlone(x, n)) ?? r.context;
|
|
712
|
+
if (!DISPOSITION_VERB.test(clause)) continue;
|
|
713
|
+
const finding = reportRows.find((v) => v.kind === 'finding' && v.confidence === 'stated' && v.verdict && standsAlone(clause, v.verdict) && (v.line ?? 0) < (r.line ?? 0));
|
|
714
|
+
if (!finding) continue;
|
|
715
|
+
const from = facts.arrangementsByPath.get(r.path)?.[0]?.ident ?? null;
|
|
716
|
+
if (!from || own.includes(from) || !index.units.has(from) || seen.has(finding.id)) continue;
|
|
717
|
+
seen.add(finding.id);
|
|
718
|
+
out.push({ key: `handed-in:${finding.id}`, kind: 'Handed in', occurred: r.occurred, did: `${finding.verdict} from ${from} handed in`, unit: from, result: clip(r.context, 120), verdict: null, who: null, evidence: [facts.ref(finding.id, finding.text), facts.ref(r.id, r.context)], basis: 'Explicit', sendBackId: null });
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return out;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// ───────────────────────── a passing report's items left for later ─────────────────────────
|
|
725
|
+
|
|
726
|
+
function openItemsOfChecks(ctx: WorkContext, checks: readonly CheckRecord[]): OpenItemState[] {
|
|
727
|
+
const { facts } = ctx;
|
|
728
|
+
const out: OpenItemState[] = [];
|
|
729
|
+
for (const c of checks) {
|
|
730
|
+
if (c.normalized !== 'pass' || !c.report) continue;
|
|
731
|
+
const text = facts.textOf(c.report.id);
|
|
732
|
+
if (!text) continue;
|
|
733
|
+
const lines = text.split(/\r?\n/);
|
|
734
|
+
const rows = facts.verdictsByPath.get(c.reportPath) ?? [];
|
|
735
|
+
const findingNums = [...new Set(rows.filter((v) => v.kind === 'finding' && /^[A-Z]{1,3}-\d{1,3}$/.test(v.verdict ?? '')).map((v) => v.verdict!))];
|
|
736
|
+
const items = [...openItemsOf(text), ...deferLinesOf(text).filter((d) => findingNums.some((n) => standsAlone(d.text, n)))];
|
|
737
|
+
for (const item of items) {
|
|
738
|
+
// The item's own tokens, and those of the finding it names (whose line says where in the code it is).
|
|
739
|
+
const named = findingNums.filter((n) => standsAlone(item.text, n));
|
|
740
|
+
const defLines = named.flatMap((n) => rows.filter((v) => v.kind === 'finding' && v.verdict === n).slice(0, 1).map((v) => v.text));
|
|
741
|
+
const tokens = [...new Set([...tokensOf(item.text), ...defLines.flatMap(tokensOf)])].filter((t) => !named.includes(t));
|
|
742
|
+
let handled: Handling | null = null;
|
|
743
|
+
if (SELF_DISPOSE_WORDS.test(item.text)) handled = { how: 'disposed', evidence: lineRef(c, item.line, item.text) };
|
|
744
|
+
for (let i = item.line; i < lines.length && !handled && tokens.length; i++) {
|
|
745
|
+
const clause = clausesOf(lines[i]!).find((x) => tokens.some((t) => x.includes(t)) && DISPOSE_WORDS.test(x));
|
|
746
|
+
if (clause) handled = { how: 'disposed', evidence: lineRef(c, i + 1, lines[i]!.trim()) };
|
|
747
|
+
}
|
|
748
|
+
if (!handled) handled = followedLater(ctx, c, tokens, null);
|
|
749
|
+
out.push({ check: c, item, handled });
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return out;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function lineRef(c: CheckRecord, line: number, text: string): EvidenceRef {
|
|
756
|
+
return { kind: 'file', id: c.reportPath, label: `${c.reportPath}:${line}`, line: text, occurred: c.report?.occurred ?? c.occurred };
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* A later document version or commit (after the report first appeared) that carries one of the tokens — not the report
|
|
761
|
+
* itself, and not the checking unit's own prompt or receipts. With `context`, only material that names one of those units.
|
|
762
|
+
*/
|
|
763
|
+
function followedLater(ctx: WorkContext, c: CheckRecord, tokens: readonly string[], context: ReadonlySet<string> | null): Handling | null {
|
|
764
|
+
const { facts, index } = ctx;
|
|
765
|
+
const since = c.firstReport?.ms ?? c.ms;
|
|
766
|
+
const ownPaths = new Set([...(index.units.get(c.unit)?.prompts ?? []), ...(index.units.get(c.unit)?.receipts ?? [])].map((a) => a.path));
|
|
767
|
+
for (const t of tokens) {
|
|
768
|
+
const hits = facts.ledger.word(t, { limit: 300, oldestFirst: true });
|
|
769
|
+
if (typeof hits === 'string') continue;
|
|
770
|
+
for (const h of hits.rows) {
|
|
771
|
+
if (h.kind !== 'doc' && h.kind !== 'commit' && h.kind !== 'loose') continue;
|
|
772
|
+
const path = h.kind === 'doc' ? h.label.split(' — ')[0]! : null;
|
|
773
|
+
if (path && (path === c.reportPath || ownPaths.has(path))) continue;
|
|
774
|
+
const first = msOfOccurred(h.occurred);
|
|
775
|
+
const last = h.last ? Date.parse(h.last.at) || first : first;
|
|
776
|
+
if (Math.max(first, last) <= since) continue;
|
|
777
|
+
if (context) {
|
|
778
|
+
const named = index.namedIn.get(h.kind === 'commit' ? `commit:${facts.commitByPrefix(h.id.replace(/^commit:/, ''))?.hash ?? ''}` : `path:${path}`);
|
|
779
|
+
if (!named || ![...context].some((n) => named.has(n))) continue;
|
|
780
|
+
}
|
|
781
|
+
const id = h.kind === 'doc' && h.last && last > since && first <= since ? h.last.id : h.id;
|
|
782
|
+
return { how: 'followed', evidence: facts.ref(id, h.snippet) };
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return null;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// ───────────────────────── the four things ─────────────────────────
|
|
789
|
+
|
|
790
|
+
/** A confirmed link whose fact is a commit on the trunk. */
|
|
791
|
+
function linkedOnTrunk(facts: Facts, l: ProcessLink): boolean {
|
|
792
|
+
const hash = l.ledgerRef.startsWith('commit:') ? l.ledgerRef.slice('commit:'.length) : l.evidence?.kind === 'commit' ? l.evidence.id : null;
|
|
793
|
+
return hash ? facts.commitByPrefix(hash)?.onTrunk === true : false;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function executionOf(thread: WorkThread, d: Delivery, units: readonly Unit[], checks: readonly CheckRecord[], linked: readonly ProcessLink[], onTrunk: (l: ProcessLink) => boolean): ExecutionState | null {
|
|
797
|
+
const hasCode = d.codeCommits.length > 0;
|
|
798
|
+
if (hasCode && d.codeCommits.some((c) => c.onTrunk)) return 'Merged';
|
|
799
|
+
// A check or a report-only piece of work lands as its report on the trunk.
|
|
800
|
+
if (!hasCode && units.some((u) => u.nature === 'check') && checks.some((c) => c.report?.current)) return 'Merged';
|
|
801
|
+
if (!hasCode && d.reportedDone && d.receipts.some((r) => r.current)) return 'Merged';
|
|
802
|
+
if (hasCode && d.reportedDone) return 'Not merged';
|
|
803
|
+
// What the round confirmed where the ids could not tie it (§1.4): the work's delivered or merged commits.
|
|
804
|
+
const delivered = linked.filter((l) => l.stepKind === 'Delivered' || l.stepKind === 'Fix' || l.stepKind === 'Merged');
|
|
805
|
+
if (delivered.length) return delivered.some((l) => l.stepKind === 'Merged' || onTrunk(l)) ? 'Merged' : 'In progress';
|
|
806
|
+
if (hasCode || d.dispatched || d.receipts.length || thread.progress === 'In progress' || thread.progress === 'On hold') return 'In progress';
|
|
807
|
+
// Said to be done, and nothing ties its execution to it yet: that is not evidence it never started (§2.12). The
|
|
808
|
+
// deepening's reading ties it, or leaves a finding that says what is missing; until then the execution is not shown.
|
|
809
|
+
if (thread.progress === 'Done') return null;
|
|
810
|
+
return 'Not started';
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/** A run status file's recorded outcome (a headless run's result or exit code): the one test record the ledger holds. */
|
|
814
|
+
function latestRunResult(units: readonly Unit[]): { verdict: string; occurred: Occurred; ms: number } | null {
|
|
815
|
+
const runs = units.flatMap((u) => u.runs).filter((r) => r.data.json && (r.data.json.exit_code !== undefined || typeof r.data.json.result === 'string')).sort((a, b) => b.ms - a.ms);
|
|
816
|
+
const r = runs[0];
|
|
817
|
+
if (!r) return null;
|
|
818
|
+
const j = r.data.json!;
|
|
819
|
+
return { verdict: typeof j.result === 'string' ? j.result : `exit ${String(j.exit_code)}`, occurred: r.occurred, ms: r.ms };
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/** The latest check and who made it (CKC-27 AC-11: a self-report is never written as a check someone else made). */
|
|
823
|
+
function latestCheckOf(checks: readonly CheckRecord[], units: readonly Unit[]): WorkProcess['latestCheck'] {
|
|
824
|
+
const latest = [...checks].filter((c) => c.normalized || c.verdictWord).sort((a, b) => b.ms - a.ms)[0] ?? null;
|
|
825
|
+
const run = latestRunResult(units);
|
|
826
|
+
if (latest && (!run || latest.ms >= run.ms)) {
|
|
827
|
+
const word = latest.verdictWord ?? latest.normalized!;
|
|
828
|
+
return { verdict: latest.inProgress ? `${word} · in progress` : word, by: latest.by, occurred: latest.occurred };
|
|
829
|
+
}
|
|
830
|
+
return run ? { verdict: run.verdict, by: 'Test', occurred: run.occurred } : null;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/** The four things of one work item (§2.12); the open counts read what the runner last wrote on the workbench. */
|
|
834
|
+
export function fourThingsOf(store: ProjectStore, p: WorkProcess, expectsCheck: boolean): FourThingsView {
|
|
835
|
+
const breakpoints = store.breakpoints.filter((b) => b.targetId === p.thread.id && b.lit && !b.ownerResponse).length;
|
|
836
|
+
const sendBacks = store.sendbacks.filter((s) => s.targetId === p.thread.id && s.stage !== 'Closed' && !s.ownerResponse).length;
|
|
837
|
+
const findings = p.findings.filter((f) => !f.handled).length + p.openItems.filter((i) => !i.handled).length;
|
|
838
|
+
return {
|
|
839
|
+
execution: p.execution, progress: p.thread.progress,
|
|
840
|
+
check: p.latestCheck ?? (expectsCheck ? 'Not checked' : null),
|
|
841
|
+
open: { findings, sendBacks, breakpoints },
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// ───────────────────────── CJ: which file a link or a check is, and whose ─────────────────────────
|
|
846
|
+
|
|
847
|
+
/** A work item's own numbers, as the process engine reads them (units.ts). */
|
|
848
|
+
function ownNumbersOf(t: WorkThread): string[] {
|
|
849
|
+
return ownNumbers(t).map((n) => n.toUpperCase());
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/** The repository-relative file of an absolute path under one of the ledger's repositories, or null. */
|
|
853
|
+
function relativeFile(facts: Facts, abs: string): string | null {
|
|
854
|
+
const a = abs.replace(/\\/g, '/');
|
|
855
|
+
for (const r of facts.ledger.repos()) {
|
|
856
|
+
const root = r.path.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
857
|
+
if (a.toLowerCase().startsWith(`${root.toLowerCase()}/`)) return a.slice(root.length + 1);
|
|
858
|
+
}
|
|
859
|
+
return null;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/** The file a link's fact is about (a file, a source's file, a ledger entry's document), or null for a commit. */
|
|
863
|
+
function linkFile(facts: Facts, l: ProcessLink): string | null {
|
|
864
|
+
const ev = l.evidence;
|
|
865
|
+
if (ev?.kind === 'file') return ev.id.replace(/\\/g, '/');
|
|
866
|
+
if (l.ledgerRef.startsWith('file:')) return l.ledgerRef.slice('file:'.length).replace(/\\/g, '/');
|
|
867
|
+
if (ev?.kind === 'ledger' || (!ev && !l.ledgerRef.includes(':'))) return facts.ledger.pathOfEntry(ev?.id ?? l.ledgerRef)?.path ?? null;
|
|
868
|
+
return null;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/** The numbers a file belongs to: the work ids its arrangement versions are about, and the numbers its path defines. */
|
|
872
|
+
function fileOwners(facts: Facts, path: string): string[] {
|
|
873
|
+
const kinds = facts.ledger.arrangementKinds(path);
|
|
874
|
+
return [...new Set([...kinds.map((k) => k.ident ?? ''), ...definitionsInPath(path).map((d) => d.num)].filter(Boolean).map((x) => x.toUpperCase()))];
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/** A `verifies` relation's target is this work item, or the Requirement of the same number it stands for. */
|
|
878
|
+
function verifiesThis(store: ProjectStore, t: WorkThread, to: string): boolean {
|
|
879
|
+
if (to === t.id) return true;
|
|
880
|
+
const r = store.reference.get(to);
|
|
881
|
+
if (!r || r.category !== 'Requirement') return false;
|
|
882
|
+
const nums = new Set(t.ids.map((i) => i.toUpperCase()));
|
|
883
|
+
return r.ids.some((i) => nums.has(i.toUpperCase())) || t.serves.some((s) => s.referenceId === to && /the same row/.test(s.claim));
|
|
884
|
+
}
|