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,543 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The units of work the ledger shows (Spec v3.0 §2.12 "按项目的编号"; §1.16 row 4 编号, row 6 执行安排): each number the
|
|
3
|
+
* project gives a piece of work, with what the ledger ties to it by that number —
|
|
4
|
+
*
|
|
5
|
+
* - its arrangement versions: the prompt or task file (front matter: executor, status, worktree, the fields that name
|
|
6
|
+
* other work), the receipts and reports named after it, the run status files;
|
|
7
|
+
* - its commits: a commit whose subject opens with the number (`AB: …`), else a side commit on a branch named after it
|
|
8
|
+
* (`wip/AK-…`), else a side commit brought in by a merge that names it where a merge names what it merges (`Merge AI: …`,
|
|
9
|
+
* `Merge branch 'k-ledger' (AP: …)`) — the innermost such merge, for a branch merged into an integration branch
|
|
10
|
+
* (integration.ts); the merges that brought them in. A trunk commit that only mentions a number (`subagent: dispatch
|
|
11
|
+
* AK`), or a merge that mentions one elsewhere in its subject (`…, QC AW/AX/AY fixes)`), is not the unit's delivery;
|
|
12
|
+
* - what it is by its own words: a fix (`修复 AB`, `fix`, `repair`), a check (`QC`, `review`, `走查`, a report with a
|
|
13
|
+
* verdict and no code of its own), or work;
|
|
14
|
+
* - which units it fixes or checks: named in its title, in a front-matter field that says so (`upstream`, `verifies`,
|
|
15
|
+
* `reviewed_commit` …), or named by the other unit (`resolved_by`, `milestone_qc`).
|
|
16
|
+
*
|
|
17
|
+
* A number the project gave to more than one task (ContextKeeper's AJ: an exam, then a review) is one unit per task —
|
|
18
|
+
* the latest keeps the number, an earlier one is `AJ#1` — told apart by the prompt files, what names which report, and
|
|
19
|
+
* when each was written (`tasksOfNumber`); a work item carrying the number is the task its sources cite.
|
|
20
|
+
*
|
|
21
|
+
* A related task that names the work's number is how a step joins a work item's process (the orchestrator's matching
|
|
22
|
+
* rule): a fix prompt that names the task it repairs, a QC that names what it checks. Only these relations chain; a
|
|
23
|
+
* dependency (`依赖 AK`, `blocks`) does not make one task a step of another.
|
|
24
|
+
*/
|
|
25
|
+
import type { WorkThread } from '../model/types.ts';
|
|
26
|
+
import type { ProjectStore } from '../store/project-store.ts';
|
|
27
|
+
import type { ArrangementFact, CommitFact, Facts, NumFact } from './facts.ts';
|
|
28
|
+
import { integrationsOf, type Integrations } from './integration.ts';
|
|
29
|
+
import { definitionsInSubject } from '../ledger/numbering.ts';
|
|
30
|
+
import { isNumberShaped, otherObjectOf } from '../keeper/numbers-check.ts';
|
|
31
|
+
import { earlierGenerationOf } from '../keeper/organize/carried-on.ts';
|
|
32
|
+
import { fullDatesIn, materialTime, msOf } from '../ledger/time.ts';
|
|
33
|
+
|
|
34
|
+
export type UnitNature = 'work' | 'fix' | 'check';
|
|
35
|
+
export type CheckKind = 'QC' | 'Review' | 'Walkthrough';
|
|
36
|
+
|
|
37
|
+
export interface Relation {
|
|
38
|
+
/** The unit this one fixes or checks. */
|
|
39
|
+
readonly num: string;
|
|
40
|
+
readonly as: 'fix' | 'check';
|
|
41
|
+
/** Where the relation is written: the title, a field (`upstream: "AB"`), the reviewed commit, or the other unit's field. */
|
|
42
|
+
readonly via: { readonly entryId: string; readonly line: string };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface Unit {
|
|
46
|
+
readonly num: string;
|
|
47
|
+
/** Prompt / task-file versions, oldest first. */
|
|
48
|
+
readonly prompts: readonly ArrangementFact[];
|
|
49
|
+
/** Receipts and reports named after it (its own account of what it did, or a check's report), oldest first. */
|
|
50
|
+
readonly receipts: readonly ArrangementFact[];
|
|
51
|
+
/** Evidence the host collected about it (`AB-evidence-grok.md`): not its own account, and not an independent check. */
|
|
52
|
+
readonly evidence: readonly ArrangementFact[];
|
|
53
|
+
/** Run status files (a headless run: agent, model, worktree, base commit). */
|
|
54
|
+
readonly runs: readonly ArrangementFact[];
|
|
55
|
+
readonly title: string | null;
|
|
56
|
+
readonly fields: Readonly<Record<string, string>>;
|
|
57
|
+
readonly nature: UnitNature;
|
|
58
|
+
readonly checkKind: CheckKind | null;
|
|
59
|
+
/** Delivery commits (not merges), oldest first. */
|
|
60
|
+
readonly commits: readonly CommitFact[];
|
|
61
|
+
/** The merges into the trunk that brought its commits in (or merge commits named after it), oldest first. */
|
|
62
|
+
readonly merges: readonly CommitFact[];
|
|
63
|
+
readonly branches: readonly string[];
|
|
64
|
+
readonly relations: readonly Relation[];
|
|
65
|
+
/** The executor the arrangement names, and the model when it says. */
|
|
66
|
+
readonly executor: string | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ───────────────────────── words ─────────────────────────
|
|
70
|
+
|
|
71
|
+
/** Fix words in a title or a `kind` field. `修` alone only right before a number (`修 AF`), since it is part of many words. */
|
|
72
|
+
export const FIX_WORDS = /修复|修补|修正|返工|修掉|修好|修\s*(?=[A-Z]{1,6}(?:-?\d)?\b)|\b(?:fix|fixes|fixing|repair|repairs|repairing|rework|hotfix|remediate|remediation)\b/i;
|
|
73
|
+
/** Check words: QC, review, audit, walkthrough, independent verification. */
|
|
74
|
+
export const CHECK_WORDS = /\bQC\b|\breview\b|\baudit\b|walk-?through|走查|审阅|审核|复核|评审|核查|独立.{0,8}(?:QC|复核|审|核|验)/i;
|
|
75
|
+
const WALK_WORDS = /walk-?through|走查|真机|模拟器/i;
|
|
76
|
+
const REVIEW_WORDS = /\breview\b|\baudit\b|审阅|审核|评审/i;
|
|
77
|
+
|
|
78
|
+
/** Front-matter fields that say which unit this one fixes or checks. */
|
|
79
|
+
const FORWARD_FIELDS = /^(upstream|fixes|fix_of|repairs|repair_of|resolves|reworks|verifies|verified|reviews|reviewed|reviewed_work|reviewed_task|checks|checked|qc_of|audits|target|targets|for|parent|follows)$/i;
|
|
80
|
+
/** Front-matter fields of the *other* unit that name this one as its fixer or checker. */
|
|
81
|
+
const REVERSE_FIX_FIELDS = /^(resolved_by|fixed_by|repaired_by|reworked_by|fix|fixed_in)$/i;
|
|
82
|
+
const REVERSE_CHECK_FIELDS = /^(milestone_qc|qc|qc_by|reviewed_by|checked_by|verified_by|audited_by|independent_qc)$/i;
|
|
83
|
+
/** Fields naming the commit or revision a check reviewed. */
|
|
84
|
+
const COMMIT_FIELDS = /^(reviewed_commit|reviewed_revision|reviewed_rev|verifies|verified_commit|checked_commit|qc_commit|commit_under_review)$/i;
|
|
85
|
+
|
|
86
|
+
const escapeRe = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
87
|
+
|
|
88
|
+
/** Status words of an arrangement, by what they say about the dispatch (§2.12 `Dispatched`). */
|
|
89
|
+
export const RUNNING_STATUS = /^(?:running|in[ _-]?progress|dispatched|started|active|执行中|进行中|正在)/i;
|
|
90
|
+
export const PLANNED_STATUS = /^(?:ready|queued|planned|todo|to[ _-]?do|pending|blocked|draft|not[ _-]?dispatched|waiting|待|排队|未派发)/i;
|
|
91
|
+
export const DONE_STATUS = /^(?:complete|completed|done|delivered|submitted|finished|merged|accepted|closed|完成|已完成|已合入|已交付)/i;
|
|
92
|
+
/** A host review that sent the delivery back (§1.18: the result has gaps). `…-qc-failed` records a check's verdict, not a review. */
|
|
93
|
+
export const REPAIR_STATUS = /^(?:needs?[ _-]?(?:repair|fix|fixes|rework)|rework|rejected|returned|sent[ _-]?back|返工|打回|需修复|待修复|需要修复)/i;
|
|
94
|
+
|
|
95
|
+
// ───────────────────────── building the units ─────────────────────────
|
|
96
|
+
|
|
97
|
+
export interface UnitIndex {
|
|
98
|
+
readonly units: ReadonlyMap<string, Unit>;
|
|
99
|
+
/** Units by their thread: the thread's numbers that are units. */
|
|
100
|
+
readonly threadNums: ReadonlyMap<string, readonly string[]>;
|
|
101
|
+
/** The thread a number belongs to. */
|
|
102
|
+
readonly threadOf: ReadonlyMap<string, string>;
|
|
103
|
+
/** Where every unit number shows up (§1.16 row 4). */
|
|
104
|
+
readonly nums: ReadonlyMap<string, readonly NumFact[]>;
|
|
105
|
+
/** Unit numbers named in each document path or commit (for "does this text name the unit"). */
|
|
106
|
+
readonly namedIn: ReadonlyMap<string, ReadonlySet<string>>;
|
|
107
|
+
/** Which units each commit is attributed to. */
|
|
108
|
+
readonly commitUnits: ReadonlyMap<string, readonly string[]>;
|
|
109
|
+
/** The merges that brought each commit in, innermost first (integration.ts). */
|
|
110
|
+
readonly integrations: Integrations;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const MERGE_ID = '[A-Z]{1,6}-[A-Z]?\\d{1,4}|[A-Z]\\d{1,4}|[A-Z]{2}';
|
|
114
|
+
const MERGE_SUBJECT = new RegExp(`^Merge\\s+(?:branch\\s+'[^']*'\\s*\\()?((?:${MERGE_ID})(?:\\s*[+&,]\\s*(?:${MERGE_ID}))*)\\s*[::)]`);
|
|
115
|
+
const MERGE_OPENS = new RegExp(`^Merge\\s+((?:${MERGE_ID})(?:\\s*[+&,]\\s*(?:${MERGE_ID}))*)(?![A-Za-z0-9_-])`);
|
|
116
|
+
const MERGE_PAREN = new RegExp(`\\(((?:${MERGE_ID})(?:\\s*[+&,]\\s*(?:${MERGE_ID}))*)\\s*[::]`);
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The numbers a merge's subject names as what it merges, in the places a merge says so: right after `Merge`
|
|
120
|
+
* (`Merge AB+AD: …`, `Merge T-2 conveyor`), or opening its parenthesis (`Merge branch 'k-ledger' (AP: …)`). Numbers the
|
|
121
|
+
* subject mentions anywhere else are not what it merged. Any shape of number: the caller keeps the ones it knows.
|
|
122
|
+
*/
|
|
123
|
+
export function mergeNamesIn(subject: string): string[] {
|
|
124
|
+
const m = MERGE_SUBJECT.exec(subject) ?? MERGE_OPENS.exec(subject) ?? MERGE_PAREN.exec(subject);
|
|
125
|
+
return m ? m[1]!.split(/\s*[+&,]\s*/) : [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The numbers of a work item: the project's own, from its record (CJ) — its ids, and an internal id that is a project
|
|
130
|
+
* number (`AP`: before CJ, `pk_write_thread({ id: "AP" })` made the number the item's id and left its ids empty) — less
|
|
131
|
+
* another object's number (`isOther`): a decision's (`D99`, `D100`) is not the ticket's, and tying work by it showed the
|
|
132
|
+
* decision record's commits as the ticket's delivery.
|
|
133
|
+
*/
|
|
134
|
+
export function ownNumbers(t: WorkThread, isOther: (num: string) => boolean = () => false): string[] {
|
|
135
|
+
const own = [...t.ids, ...(isNumberShaped(t.id) ? [t.id] : [])].map((x) => x.trim()).filter(Boolean);
|
|
136
|
+
return [...new Set(own)].filter((n) => !isOther(n));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function buildUnits(store: ProjectStore, facts: Facts): UnitIndex {
|
|
140
|
+
const threads = store.threads.all();
|
|
141
|
+
const otherCache = new Map<string, boolean>();
|
|
142
|
+
const isOther = (n: string): boolean => {
|
|
143
|
+
const key = n.toUpperCase();
|
|
144
|
+
if (!otherCache.has(key)) otherCache.set(key, otherObjectOf(store, facts.ledger, key) !== null);
|
|
145
|
+
return otherCache.get(key)!;
|
|
146
|
+
};
|
|
147
|
+
const threadNumbers = (t: WorkThread) => ownNumbers(t, isOther);
|
|
148
|
+
const threadOf = new Map<string, string>();
|
|
149
|
+
const threadNums = new Map<string, string[]>();
|
|
150
|
+
for (const t of threads) {
|
|
151
|
+
const nums = threadNumbers(t);
|
|
152
|
+
threadNums.set(t.id, nums);
|
|
153
|
+
for (const n of nums) if (!threadOf.has(n)) threadOf.set(n, t.id);
|
|
154
|
+
}
|
|
155
|
+
// The unit numbers: every number a work item carries, and every number an arrangement file is about.
|
|
156
|
+
const known = new Set<string>([...threadOf.keys()]);
|
|
157
|
+
for (const a of facts.arrangements) if (a.ident && (a.kind === 'prompt' || a.kind === 'receipt' || a.kind === 'status')) known.add(a.ident);
|
|
158
|
+
const nums = facts.numsOf(known);
|
|
159
|
+
const namedIn = new Map<string, Set<string>>();
|
|
160
|
+
const note = (key: string, num: string) => { const s = namedIn.get(key) ?? new Set<string>(); s.add(num); namedIn.set(key, s); };
|
|
161
|
+
const branchUnits = new Map<string, Set<string>>();
|
|
162
|
+
const subjectDefs = new Map<string, Set<string>>();
|
|
163
|
+
const subjectNames = new Map<string, Set<string>>();
|
|
164
|
+
for (const [num, rows] of nums) {
|
|
165
|
+
for (const r of rows) {
|
|
166
|
+
if (r.kind === 'doc' || r.kind === 'loose') { if (r.path) note(`path:${r.path}`, num); }
|
|
167
|
+
else if (r.kind === 'commit' && r.commit) {
|
|
168
|
+
note(`commit:${r.commit}`, num);
|
|
169
|
+
if (r.line === 1) {
|
|
170
|
+
if (r.place === 'definition') subjectDefs.set(r.commit, new Set([...(subjectDefs.get(r.commit) ?? []), num]));
|
|
171
|
+
if (r.confidence === 'stated' || r.place === 'definition') subjectNames.set(r.commit, new Set([...(subjectNames.get(r.commit) ?? []), num]));
|
|
172
|
+
}
|
|
173
|
+
} else if (r.kind === 'branch') {
|
|
174
|
+
const name = r.path ?? r.context;
|
|
175
|
+
const plain = name.replace(/^(?:origin|upstream|remotes\/[^/]+)\//, '');
|
|
176
|
+
branchUnits.set(plain, new Set([...(branchUnits.get(plain) ?? []), num]));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// A subject that opens with a unit's number defines it (`AN: scanner`), and a merge's subject names what it brings in
|
|
181
|
+
// (`Merge AB+AD: …`, `Merge branch 'k-ledger' (AP: …)`, `Merge T-2 conveyor`), read here for every known unit: the
|
|
182
|
+
// ledger leaves out two capitals that are also ordinary words (AN, AT, AS …), which in these positions are the number.
|
|
183
|
+
// A number a merge's subject only mentions elsewhere is not what it merges: `Merge k-clerk: increment K (…, QC AW/AX/AY
|
|
184
|
+
// fixes)` brought in increment K, not three QC tasks (test-C-1, where it had made every commit of increment K AW's).
|
|
185
|
+
const mergeNames = new Map<string, Set<string>>();
|
|
186
|
+
for (const c of facts.commits.values()) {
|
|
187
|
+
const defs = definitionsInSubject(c.subject).map((d) => d.num).filter((n) => known.has(n));
|
|
188
|
+
if (defs.length) subjectDefs.set(c.hash, new Set([...(subjectDefs.get(c.hash) ?? []), ...defs]));
|
|
189
|
+
if (!c.merge) continue;
|
|
190
|
+
const named = mergeNamesIn(c.subject).filter((n) => known.has(n));
|
|
191
|
+
if (named.length) mergeNames.set(c.hash, new Set(named));
|
|
192
|
+
}
|
|
193
|
+
const integrations = integrationsOf(facts);
|
|
194
|
+
// Commits → units (see the module comment for the order of the rules).
|
|
195
|
+
const commitUnits = new Map<string, string[]>();
|
|
196
|
+
const byUnit = new Map<string, CommitFact[]>();
|
|
197
|
+
const mergesOf = new Map<string, Map<string, CommitFact>>();
|
|
198
|
+
const addMerge = (num: string, m: CommitFact) => { const x = mergesOf.get(num) ?? new Map<string, CommitFact>(); x.set(m.hash, m); mergesOf.set(num, x); };
|
|
199
|
+
for (const c of facts.commits.values()) {
|
|
200
|
+
if (c.merge) {
|
|
201
|
+
for (const n of mergeNames.get(c.hash) ?? []) if (known.has(n) && c.fpTrunk) addMerge(n, c);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
let units = [...(subjectDefs.get(c.hash) ?? [])].filter((n) => known.has(n));
|
|
205
|
+
if (units.length === 0 && !c.fpTrunk) {
|
|
206
|
+
const fromBranches = new Set<string>();
|
|
207
|
+
const onBranches = (facts.branchesOf.get(c.hash) ?? []).filter((b) => [...(branchUnits.get(b) ?? [])].some((n) => known.has(n)));
|
|
208
|
+
const smallest = Math.min(...onBranches.map((b) => facts.branchCommits.get(b)?.size ?? Infinity));
|
|
209
|
+
for (const b of onBranches.filter((x) => (facts.branchCommits.get(x)?.size ?? Infinity) === smallest)) for (const n of branchUnits.get(b) ?? []) if (known.has(n)) fromBranches.add(n);
|
|
210
|
+
units = [...fromBranches];
|
|
211
|
+
// The merges that brought it in, innermost first: the first that names a unit in its merge position (a branch
|
|
212
|
+
// merged into an integration branch is named by its own merge, `Merge branch 'k-ledger' (AP: …)`, not by the
|
|
213
|
+
// trunk's merge of the integration branch).
|
|
214
|
+
for (const m of units.length === 0 ? integrations.chainOf(c.hash) : []) {
|
|
215
|
+
const named = [...(mergeNames.get(m.merge.hash) ?? [])].filter((n) => known.has(n));
|
|
216
|
+
if (named.length) { units = named; break; }
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (units.length === 0) continue;
|
|
220
|
+
commitUnits.set(c.hash, units);
|
|
221
|
+
for (const n of units) {
|
|
222
|
+
byUnit.set(n, [...(byUnit.get(n) ?? []), c]);
|
|
223
|
+
const m = facts.introducedBy.get(c.hash);
|
|
224
|
+
const mc = m ? facts.commits.get(m) : undefined;
|
|
225
|
+
if (mc && mc.merge && m !== c.hash) addMerge(n, mc);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// Runs name the prompt they ran and the report they wrote (`runs/AJ-1/status.json`: prompt …, report …).
|
|
229
|
+
const runPairs = facts.arrangements.filter((a) => a.kind === 'status' && a.data.json)
|
|
230
|
+
.map((a) => ({ prompt: pathKeyOf(a.data.json!.prompt), report: pathKeyOf(a.data.json!.report) }))
|
|
231
|
+
.filter((x): x is { prompt: string; report: string } => x.prompt !== null && x.report !== null);
|
|
232
|
+
const units = new Map<string, Unit>();
|
|
233
|
+
/** The numbers the project gave to more than one task, with those tasks, oldest first. */
|
|
234
|
+
const tasksOf = new Map<string, readonly Task[]>();
|
|
235
|
+
for (const num of known) {
|
|
236
|
+
// A number the ledger never saw anywhere (no arrangement, no commit, no line of any document or message) is no unit
|
|
237
|
+
// the program can say anything about: its work item's process is left to the round's judged links.
|
|
238
|
+
const own = facts.arrangementsByIdent.get(num) ?? [];
|
|
239
|
+
if (own.length === 0 && !byUnit.has(num) && !mergesOf.has(num) && (nums.get(num)?.length ?? 0) === 0) continue;
|
|
240
|
+
const tasks = tasksOfNumber(facts, num, own, (byUnit.get(num) ?? []).sort((a, b) => a.ms - b.ms), [...(mergesOf.get(num)?.values() ?? [])].sort((a, b) => a.ms - b.ms), runPairs);
|
|
241
|
+
for (const task of tasks) units.set(task.key, unitOf(facts, task, branchUnits, num));
|
|
242
|
+
if (tasks.length < 2) continue;
|
|
243
|
+
tasksOf.set(num, tasks);
|
|
244
|
+
// A commit of an earlier task with the number is that task's.
|
|
245
|
+
for (const task of tasks) for (const c of task.commits) commitUnits.set(c.hash, (commitUnits.get(c.hash) ?? []).map((n) => (n === num ? task.key : n)));
|
|
246
|
+
}
|
|
247
|
+
// A work item carrying a number given to several tasks is the task its sources cite, else the one of its generation,
|
|
248
|
+
// else the latest.
|
|
249
|
+
if (tasksOf.size) {
|
|
250
|
+
for (const t of threads) threadNums.set(t.id, threadNumbers(t).map((n) => { const tasks = tasksOf.get(n); return tasks ? taskOfThread(store, facts, t, tasks) : n; }));
|
|
251
|
+
threadOf.clear();
|
|
252
|
+
for (const t of threads) for (const n of threadNums.get(t.id)!) if (!threadOf.has(n)) threadOf.set(n, t.id);
|
|
253
|
+
}
|
|
254
|
+
/** A number named in a text written at `ms`: the task that had the number then. */
|
|
255
|
+
const taskAt = (num: string, ms: number): string => {
|
|
256
|
+
const tasks = tasksOf.get(num);
|
|
257
|
+
if (!tasks) return num;
|
|
258
|
+
return (tasks.filter((k) => k.start <= ms).pop() ?? tasks[0]!).key;
|
|
259
|
+
};
|
|
260
|
+
// Relations: only a fix or a check chains (a dependency does not).
|
|
261
|
+
const unitNames = [...new Set([...units.keys()].map(baseNumber))].sort((a, b) => b.length - a.length);
|
|
262
|
+
const nameRe = unitNames.length ? new RegExp(`(?<![A-Za-z0-9_])(${unitNames.map(escapeRe).join('|')})(?![A-Za-z0-9_]|-\\d|\\.\\d)`, 'g') : null;
|
|
263
|
+
const namedAt = (text: string, ms: number): string[] => (nameRe ? [...new Set([...text.matchAll(nameRe)].map((m) => taskAt(m[1]!, ms)))] : []);
|
|
264
|
+
const unitsOfCommit = (hash: string): string[] => {
|
|
265
|
+
const c = facts.commitByPrefix(hash);
|
|
266
|
+
if (!c) return [];
|
|
267
|
+
const direct = commitUnits.get(c.hash);
|
|
268
|
+
if (direct?.length) return [...direct];
|
|
269
|
+
const subject = [...((c.merge ? mergeNames : subjectNames).get(c.hash) ?? [])].map((n) => taskAt(n, c.ms)).filter((u) => units.has(u));
|
|
270
|
+
if (c.merge) {
|
|
271
|
+
// What the merge brought in: its own integration's commits (a merge nested under the trunk's one included).
|
|
272
|
+
const own = integrations.merges.find((m) => m.merge.hash === c.hash);
|
|
273
|
+
const brought = new Set<string>();
|
|
274
|
+
for (const [h, us] of commitUnits) if (own ? own.brought.has(h) : facts.introducedBy.get(h) === c.hash) for (const u of us) brought.add(u);
|
|
275
|
+
for (const u of subject) brought.add(u);
|
|
276
|
+
if (brought.size) return [...brought];
|
|
277
|
+
}
|
|
278
|
+
return subject;
|
|
279
|
+
};
|
|
280
|
+
const rel = new Map<string, Map<string, Relation>>();
|
|
281
|
+
const addRel = (from: string, r: Relation) => {
|
|
282
|
+
if (from === r.num || !units.has(r.num)) return;
|
|
283
|
+
const m = rel.get(from) ?? new Map<string, Relation>();
|
|
284
|
+
if (!m.has(r.num)) m.set(r.num, r);
|
|
285
|
+
rel.set(from, m);
|
|
286
|
+
};
|
|
287
|
+
for (const u of units.values()) {
|
|
288
|
+
const as: 'fix' | 'check' | null = u.nature === 'fix' ? 'fix' : u.nature === 'check' ? 'check' : null;
|
|
289
|
+
const latest = u.prompts[u.prompts.length - 1] ?? u.receipts[u.receipts.length - 1];
|
|
290
|
+
if (as && latest) {
|
|
291
|
+
if (u.title) for (const n of namedAt(u.title, latest.ms)) addRel(u.num, { num: n, as, via: { entryId: latest.id, line: u.title } });
|
|
292
|
+
for (const [k, v] of Object.entries(u.fields)) {
|
|
293
|
+
if (FORWARD_FIELDS.test(k)) for (const n of namedAt(v, latest.ms)) addRel(u.num, { num: n, as, via: { entryId: latest.id, line: `${k}: ${v}` } });
|
|
294
|
+
if (COMMIT_FIELDS.test(k)) {
|
|
295
|
+
for (const h of v.match(/\b[0-9a-f]{7,40}\b/g) ?? []) for (const n of unitsOfCommit(h)) addRel(u.num, { num: n, as, via: { entryId: latest.id, line: `${k}: ${v}` } });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// The other direction: this unit's fields name its fixer or checker.
|
|
300
|
+
const mine = u.prompts[u.prompts.length - 1];
|
|
301
|
+
if (!mine) continue;
|
|
302
|
+
for (const [k, v] of Object.entries(u.fields)) {
|
|
303
|
+
const fixBy = REVERSE_FIX_FIELDS.test(k);
|
|
304
|
+
const checkBy = REVERSE_CHECK_FIELDS.test(k);
|
|
305
|
+
if (!fixBy && !checkBy) continue;
|
|
306
|
+
for (const n of namedAt(v, mine.ms)) {
|
|
307
|
+
const other = units.get(n);
|
|
308
|
+
if (!other || other.num === u.num) continue;
|
|
309
|
+
const kind: 'fix' | 'check' = other.nature === 'check' ? 'check' : other.nature === 'fix' ? 'fix' : checkBy ? 'check' : 'fix';
|
|
310
|
+
addRel(n, { num: u.num, as: kind, via: { entryId: mine.id, line: `${k}: ${v}` } });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
for (const [num, m] of rel) {
|
|
315
|
+
const u = units.get(num)!;
|
|
316
|
+
units.set(num, { ...u, relations: [...m.values()] });
|
|
317
|
+
}
|
|
318
|
+
return { units, threadNums, threadOf, nums, namedIn, commitUnits, integrations };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ───────────────────────── one number, several tasks ─────────────────────────
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* One task a number was given to. A project sometimes gives a number again: ContextKeeper's AJ was first an exam (甲 ·
|
|
325
|
+
* 19 题, its report `reports/AJ-report.md`, dispatched 2026-09-20 from outside the repository) and then the Keeper's
|
|
326
|
+
* read-boundary review (`AJ-sol-b2-read-boundary.md`, created 2026-09-21).
|
|
327
|
+
*/
|
|
328
|
+
interface Task {
|
|
329
|
+
/** The number for the latest task, `NUM#k` for an earlier one (k counts the number's tasks from the oldest). */
|
|
330
|
+
readonly key: string;
|
|
331
|
+
/** When it was given out: its prompt's first version or `created_at`; for a task known only by its receipts, their date. */
|
|
332
|
+
readonly start: number;
|
|
333
|
+
readonly prompts: ArrangementFact[];
|
|
334
|
+
readonly receipts: ArrangementFact[];
|
|
335
|
+
readonly runs: ArrangementFact[];
|
|
336
|
+
readonly commits: CommitFact[];
|
|
337
|
+
readonly merges: CommitFact[];
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** The number a unit key stands for: `AJ#1` → `AJ`. */
|
|
341
|
+
export const baseNumber = (key: string): string => key.replace(/#\d+$/, '');
|
|
342
|
+
|
|
343
|
+
/** A path as a run file or a front-matter field writes it (`D:\\pk\\subagent\\reports\\AJ-report.md`), for comparing ends. */
|
|
344
|
+
function pathKeyOf(v: unknown): string | null {
|
|
345
|
+
return typeof v === 'string' && v.trim() ? v.trim().replace(/[\\/]+/g, '/').toLowerCase() : null;
|
|
346
|
+
}
|
|
347
|
+
/** Whether a written path (absolute, or relative to somewhere) is this repository path. */
|
|
348
|
+
const names = (written: string | null, rel: string): boolean => {
|
|
349
|
+
if (written === null) return false;
|
|
350
|
+
const r = rel.toLowerCase();
|
|
351
|
+
return written === r || written.endsWith(`/${r}`) || r.endsWith(`/${written}`);
|
|
352
|
+
};
|
|
353
|
+
const stemOf = (path: string): string => (path.split('/').pop() ?? path).replace(/\.[^.]+$/, '').toLowerCase();
|
|
354
|
+
const dayOfMs = (ms: number): string => new Date(ms).toISOString().slice(0, 10);
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* The tasks a number was given to, oldest first. Each prompt file of the number is a task of its own. A receipt goes to
|
|
358
|
+
* the task whose prompt or run names it (`report: …`, a run's `report`), else whose prompt file its name continues
|
|
359
|
+
* (`AJ-sol-b2-read-boundary-report.md` ↔ `AJ-sol-b2-read-boundary.md`), else by when it was written: one dated before
|
|
360
|
+
* the number's first prompt was given out — every date written in it earlier, or, with none written, its first version
|
|
361
|
+
* earlier — belongs to an earlier task the repository has no prompt for. Runs go by the prompt they name, commits and
|
|
362
|
+
* merges by when they happened. A number with one task is one unit, as it always was.
|
|
363
|
+
*/
|
|
364
|
+
function tasksOfNumber(facts: Facts, num: string, own: readonly ArrangementFact[], commits: readonly CommitFact[], merges: readonly CommitFact[], runPairs: readonly { prompt: string; report: string }[]): Task[] {
|
|
365
|
+
const prompts = own.filter((a) => a.kind === 'prompt' || (a.kind === 'plan' && a.data.fields?.id === num));
|
|
366
|
+
const receipts = own.filter((a) => a.kind === 'receipt');
|
|
367
|
+
const runs = own.filter((a) => a.kind === 'status');
|
|
368
|
+
const byPath = new Map<string, ArrangementFact[]>();
|
|
369
|
+
for (const p of prompts) byPath.set(p.path, [...(byPath.get(p.path) ?? []), p]);
|
|
370
|
+
const created = (a: ArrangementFact) => msOf(materialTime(a.data.fields?.created_at ?? a.data.fields?.created ?? null)) ?? Infinity;
|
|
371
|
+
const given = [...byPath].map(([path, versions]) => ({ path, versions, start: Math.min(...versions.map((v) => Math.min(v.ms, created(v)))) })).sort((a, b) => a.start - b.start);
|
|
372
|
+
const all = (): Task[] => [{ key: num, start: given[0]?.start ?? 0, prompts: [...prompts], receipts: [...receipts], runs: [...runs], commits: [...commits], merges: [...merges] }];
|
|
373
|
+
if (given.length === 0) return all();
|
|
374
|
+
const out = given.map((g) => ({ path: g.path, start: g.start, prompts: g.versions, receipts: [] as ArrangementFact[], runs: [] as ArrangementFact[], commits: [] as CommitFact[], merges: [] as CommitFact[] }));
|
|
375
|
+
const earlier: ArrangementFact[] = [];
|
|
376
|
+
let earlierAt = Infinity;
|
|
377
|
+
const at = (ms: number) => out.filter((t) => t.start <= ms).pop() ?? out[0]!;
|
|
378
|
+
const byReceipt = new Map<string, ArrangementFact[]>();
|
|
379
|
+
for (const r of receipts) byReceipt.set(r.path, [...(byReceipt.get(r.path) ?? []), r]);
|
|
380
|
+
for (const [path, versions] of byReceipt) {
|
|
381
|
+
const named = out.find((t) => t.prompts.some((v) => names(pathKeyOf(v.data.fields?.report ?? v.data.fields?.receipt), path)) || runPairs.some((x) => names(x.prompt, t.path) && names(x.report, path)));
|
|
382
|
+
const stem = stemOf(path);
|
|
383
|
+
const continued = out.filter((t) => { const s = stemOf(t.path); return stem.startsWith(`${s}-`) || stem.startsWith(`${s}_`); }).sort((x, y) => stemOf(y.path).length - stemOf(x.path).length)[0];
|
|
384
|
+
let task = named ?? continued;
|
|
385
|
+
if (!task) {
|
|
386
|
+
const first = versions[0]!;
|
|
387
|
+
const days = fullDatesIn(facts.textOf(first.id) ?? '').sort();
|
|
388
|
+
const day = days[days.length - 1] ?? null;
|
|
389
|
+
if (day ? day < dayOfMs(out[0]!.start) : first.ms < out[0]!.start) {
|
|
390
|
+
earlier.push(...versions);
|
|
391
|
+
earlierAt = Math.min(earlierAt, day ? msOf(day)! : first.ms);
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
task = day ? out.filter((t) => dayOfMs(t.start) <= day).pop() ?? out[0]! : at(first.ms);
|
|
395
|
+
}
|
|
396
|
+
task.receipts.push(...versions);
|
|
397
|
+
}
|
|
398
|
+
for (const r of runs) {
|
|
399
|
+
const prompt = pathKeyOf(r.data.json?.prompt);
|
|
400
|
+
(out.find((t) => names(prompt, t.path)) ?? at(r.ms)).runs.push(r);
|
|
401
|
+
}
|
|
402
|
+
for (const c of commits) at(c.ms).commits.push(c);
|
|
403
|
+
for (const m of merges) {
|
|
404
|
+
const brought = out.find((t) => t.commits.some((c) => facts.introducedBy.get(c.hash) === m.hash));
|
|
405
|
+
(brought ?? at(m.ms)).merges.push(m);
|
|
406
|
+
}
|
|
407
|
+
if (out.length === 1 && earlier.length === 0) return all();
|
|
408
|
+
const byTime = (x: ArrangementFact, y: ArrangementFact) => x.ms - y.ms || x.path.localeCompare(y.path);
|
|
409
|
+
const tasks = [
|
|
410
|
+
...(earlier.length ? [{ start: earlierAt, prompts: [], receipts: earlier, runs: [], commits: [], merges: [] }] : []),
|
|
411
|
+
...out,
|
|
412
|
+
];
|
|
413
|
+
return tasks.map((t, i) => ({ ...t, receipts: [...t.receipts].sort(byTime), runs: [...t.runs].sort(byTime), key: i === tasks.length - 1 ? num : `${num}#${i + 1}` }));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** A unit from one task: its arrangements, commits and merges, and what it is by its own words. */
|
|
417
|
+
function unitOf(facts: Facts, task: Task, branchUnits: ReadonlyMap<string, ReadonlySet<string>>, num: string): Unit {
|
|
418
|
+
const { prompts, runs, commits } = task;
|
|
419
|
+
const receipts = task.receipts.filter((a) => !isHostEvidence(a.path));
|
|
420
|
+
const evidence = task.receipts.filter((a) => isHostEvidence(a.path));
|
|
421
|
+
const latest = prompts[prompts.length - 1];
|
|
422
|
+
const title = latest?.data.title ?? receipts[receipts.length - 1]?.data.title ?? null;
|
|
423
|
+
const fields = latest?.data.fields ?? {};
|
|
424
|
+
const codeCommits = commits.filter((c) => facts.filesOf(c).some((f) => !isArrangementPath(f.path)));
|
|
425
|
+
const words = `${title ?? ''} ${fields.kind ?? ''} ${fields.type ?? ''}`;
|
|
426
|
+
const nature: UnitNature = natureOf(fields.kind ?? fields.type ?? '', title ?? '') ?? (codeCommits.length === 0 && receipts.some((r) => hasVerdict(facts, r.path)) ? 'check' : 'work');
|
|
427
|
+
const checkKind: CheckKind | null = nature !== 'check' ? null : WALK_WORDS.test(words) ? 'Walkthrough' : REVIEW_WORDS.test(words) ? 'Review' : 'QC';
|
|
428
|
+
// Its branches, the one named after it first, then the one it was made on (the smallest that holds its commits).
|
|
429
|
+
const namedAfter = (b: string) => Number((branchUnits.get(b) ?? new Set()).has(num));
|
|
430
|
+
const branches = [...new Set(commits.flatMap((c) => facts.branchesOf.get(c.hash) ?? []))]
|
|
431
|
+
.sort((x, y) => namedAfter(y) - namedAfter(x) || (facts.branchCommits.get(x)?.size ?? 0) - (facts.branchCommits.get(y)?.size ?? 0) || x.localeCompare(y));
|
|
432
|
+
return {
|
|
433
|
+
num: task.key, prompts, receipts, evidence, runs, title, fields, nature, checkKind, commits, branches, merges: task.merges,
|
|
434
|
+
relations: [], executor: fields.executor ?? fields.agent ?? fields.worker ?? latest?.data.agent ?? runs[runs.length - 1]?.data.agent ?? null,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Which of a number's tasks a work item is: the one whose files or commits its sources cite most; else the one given out
|
|
440
|
+
* within its generation; else the latest.
|
|
441
|
+
*/
|
|
442
|
+
function taskOfThread(store: ProjectStore, facts: Facts, t: WorkThread, tasks: readonly Task[]): string {
|
|
443
|
+
const paths = new Set<string>();
|
|
444
|
+
const hashes = new Set<string>();
|
|
445
|
+
const sourceIds = new Set([
|
|
446
|
+
...(t.inputs?.sourceIds ?? []), ...t.pendingSourceIds, ...t.factRecordIds.flatMap((f) => store.facts.get(f)?.aboutSourceIds ?? []),
|
|
447
|
+
...[...t.executionFacts, ...t.qcFacts].flatMap((s) => s.sourceIds),
|
|
448
|
+
]);
|
|
449
|
+
for (const sid of sourceIds) {
|
|
450
|
+
const an = store.sources.get(sid)?.anchor;
|
|
451
|
+
if (an?.kind === 'file') { const at = facts.relPathOf(an.path); if (at) paths.add(at.path); }
|
|
452
|
+
if (an?.kind === 'revision') paths.add(an.path);
|
|
453
|
+
if (an?.kind === 'commit' || an?.kind === 'revision') { const c = facts.commitByPrefix(an.commit); if (c) hashes.add(c.hash); }
|
|
454
|
+
}
|
|
455
|
+
const cited = (k: Task) => new Set([...k.prompts, ...k.receipts, ...k.runs].map((a) => a.path).filter((p) => paths.has(p))).size + k.commits.filter((c) => hashes.has(c.hash)).length;
|
|
456
|
+
const best = tasks.map((k) => ({ k, n: cited(k) })).filter((x) => x.n > 0).sort((x, y) => y.n - x.n || y.k.start - x.k.start)[0];
|
|
457
|
+
if (best) return best.k.key;
|
|
458
|
+
const g = earlierGenerationOf(store, t.id); // CZ: work carried on under the same number is not tied to its generation's time
|
|
459
|
+
if (g) {
|
|
460
|
+
const from = msOf(g.started?.at) ?? -Infinity;
|
|
461
|
+
const to = msOf(g.ended.at) ?? Infinity;
|
|
462
|
+
const inside = tasks.filter((k) => k.start >= from && k.start <= to);
|
|
463
|
+
if (inside.length) return inside[inside.length - 1]!.key;
|
|
464
|
+
}
|
|
465
|
+
return tasks[tasks.length - 1]!.key;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* What a unit is by its own words: a `kind` field decides; else, in the title, whichever comes first of the fix words and
|
|
470
|
+
* the check words (`独立里程碑 QC:AK(BYOK 缺陷修复)` is a check of a fix; `修 AG 走查查出来的问题` is a fix).
|
|
471
|
+
*/
|
|
472
|
+
export function natureOf(kind: string, title: string): UnitNature | null {
|
|
473
|
+
for (const text of [kind, title]) {
|
|
474
|
+
if (!text) continue;
|
|
475
|
+
const f = FIX_WORDS.exec(text);
|
|
476
|
+
const c = CHECK_WORDS.exec(text);
|
|
477
|
+
if (f && (!c || f.index <= c.index)) return 'fix';
|
|
478
|
+
if (c) return 'check';
|
|
479
|
+
}
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** A receipt that is the host's evidence about a unit, not the unit's own account (`AB-evidence-grok.md`, `…-证据.md`). */
|
|
484
|
+
export function isHostEvidence(path: string): boolean {
|
|
485
|
+
const base = path.split('/').pop() ?? path;
|
|
486
|
+
return /(?:^|[-_.])(?:evidence|证据)(?:[-_.]|$)/i.test(base);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/** Whether a report states a verdict (a stated one, or verdicts in its headings). */
|
|
490
|
+
function hasVerdict(facts: Facts, path: string): boolean {
|
|
491
|
+
return (facts.verdictsByPath.get(path) ?? []).some((v) => v.kind === 'verdict' && (v.confidence === 'stated' || /^#{1,6}\s/.test(v.text)));
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** Files that are the project's execution arrangements (prompts, receipts, the index, run files), not its product. */
|
|
495
|
+
export function isArrangementPath(path: string): boolean {
|
|
496
|
+
return /(?:^|\/)subagents?\//i.test(path) || /(?:^|\/)execution-plan[^/]*\.md$/i.test(path);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* The work item a work item is a step of (§2.12): when every task of it fixes or checks another (it has relations), the
|
|
501
|
+
* work its fixes and checks lead up to — AD, AF, AK and AO all lead to AB, through AF and AK where they fix or check
|
|
502
|
+
* a check or a fix. The nearest work items reached, going up one relation at a time; exactly one, or null: a work item
|
|
503
|
+
* with a task of its own is a piece of work of its own, and one that fixes or checks several separate pieces of work
|
|
504
|
+
* (a milestone QC over AB and AC) belongs to none of them alone.
|
|
505
|
+
*/
|
|
506
|
+
export function stepOfThread(index: UnitIndex, threadId: string): string | null {
|
|
507
|
+
const own = (index.threadNums.get(threadId) ?? []).map((n) => index.units.get(n)).filter((u): u is Unit => u !== undefined);
|
|
508
|
+
if (own.length === 0 || own.some((u) => u.relations.length === 0)) return null;
|
|
509
|
+
const seen = new Set(own.map((u) => u.num));
|
|
510
|
+
const roots = new Set<string>();
|
|
511
|
+
let level = own.flatMap((u) => u.relations.map((r) => r.num));
|
|
512
|
+
while (level.length && roots.size === 0) {
|
|
513
|
+
const next: string[] = [];
|
|
514
|
+
for (const n of level) {
|
|
515
|
+
if (seen.has(n)) continue;
|
|
516
|
+
seen.add(n);
|
|
517
|
+
const u = index.units.get(n);
|
|
518
|
+
if (!u) continue;
|
|
519
|
+
if (u.relations.length) { next.push(...u.relations.map((r) => r.num)); continue; }
|
|
520
|
+
const t = index.threadOf.get(n);
|
|
521
|
+
if (t && t !== threadId) roots.add(t);
|
|
522
|
+
}
|
|
523
|
+
level = next;
|
|
524
|
+
}
|
|
525
|
+
return roots.size === 1 ? [...roots][0]! : null;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* The units whose process a work item shows (§2.12): its own numbers, and, following the fix and check relations, every
|
|
530
|
+
* unit that fixes or checks one of them — an AB → AD → AF → AK → AO chain. Oldest relation first.
|
|
531
|
+
*/
|
|
532
|
+
export function chainOf(index: UnitIndex, own: readonly string[]): string[] {
|
|
533
|
+
const inChain = new Set(own.filter((n) => index.units.has(n)));
|
|
534
|
+
let grew = true;
|
|
535
|
+
while (grew) {
|
|
536
|
+
grew = false;
|
|
537
|
+
for (const u of index.units.values()) {
|
|
538
|
+
if (inChain.has(u.num)) continue;
|
|
539
|
+
if (u.relations.some((r) => inChain.has(r.num))) { inChain.add(u.num); grew = true; }
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return [...inChain];
|
|
543
|
+
}
|