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,504 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The generation check (CM, E151; CK fixes 2–4; Spec §2.12, D100 「上几代写去向、不标废弃」; CKC-24 AC-18).
|
|
3
|
+
*
|
|
4
|
+
* **Destinations.** Every item of a recognised generation has a destination: it was replaced by something (`replacedBy`:
|
|
5
|
+
* an id or the project's own number), a current item depends on it or carries it on, it moved into a current plan, it was
|
|
6
|
+
* deferred, or it was abandoned. On the gated run the only check was "generations with no planned item" — it read 0, the
|
|
7
|
+
* deepening wrote 「去向已记全」, and the 20 CKT items of the first generation had no destination at all.
|
|
8
|
+
* `itemsWithoutDestination` lists them; `pk_round_state` shows them (`withoutDestination`), and a deepening enters its
|
|
9
|
+
* synthesis only once each has one, or the main agent says why.
|
|
10
|
+
*
|
|
11
|
+
* **Candidates.** The program lists what looks like an earlier generation, for orientation to accept or reject
|
|
12
|
+
* (`pk_generation_candidate`): the archived or superseded sets of plan, contract and module documents — a directory the
|
|
13
|
+
* project keeps under an archive that holds such documents, a plan document the layer map marks not current — and the
|
|
14
|
+
* owner's lines that name generations. Whether a candidate is a generation is orientation's judgement from the documents
|
|
15
|
+
* (whether the current contracts' earlier versions count as one is not decided here); accepting writes the generation with
|
|
16
|
+
* its plan documents, what ended it and its work items, in one call.
|
|
17
|
+
*
|
|
18
|
+
* **What a set's work is (CZ).** The block the main agent reads decides the reading: on the flash run it called the 60 rows
|
|
19
|
+
* of an archived README's table of readings "the work items carrying U1, U2 …", and the main agent made them work items.
|
|
20
|
+
* So a candidate says three things apart, each read off the ledger:
|
|
21
|
+
* - `numbers` — the rows of its plan-like documents (a plan's tables, a task index, a contract set, an execution
|
|
22
|
+
* arrangement — `listsWork`) that no current document lists: its own work. A row stands at a heading or in the first
|
|
23
|
+
* cell of a table row. A document named by a number (a contract file) is that number's document: the number is the
|
|
24
|
+
* row, and the rows inside it are its points.
|
|
25
|
+
* - `carriedOn` — the rows of those documents that a current document listing work has as rows too, or a current
|
|
26
|
+
* contract is named by: carried on under the same number. They are not dropped (the second generation of ContextKeeper
|
|
27
|
+
* is the 27 contracts that went on); the current items of those numbers are the generation's items, with that
|
|
28
|
+
* destination. A report that tabulates a number, or is named after it, does not carry it on.
|
|
29
|
+
* - `alsoDefined` — the number families its other documents define (a README's table, a PRD's requirements), each with
|
|
30
|
+
* its document and whether a current document continues the family. They are not its work, and nobody is told to fill
|
|
31
|
+
* them as work.
|
|
32
|
+
* Left out of all three: a number a decision record lists among its own entries (a reference to that decision wherever
|
|
33
|
+
* it stands), and a point of numbered items (an acceptance criterion: no file is named by it, and the current documents
|
|
34
|
+
* that list it are each named by another number).
|
|
35
|
+
*
|
|
36
|
+
* **Which items join (CZ).** A work item joins an accepted generation when it carries one of those rows' numbers and it was
|
|
37
|
+
* written from a document of that set (`originOf`: the source its fill cited, its written status' source) — never by its
|
|
38
|
+
* number alone: on the flash run U1–U16 stood in two generations because both archives define them. An item of a
|
|
39
|
+
* carried-on number written from a current document joins too (that is what carried on means), unless another set's plan
|
|
40
|
+
* documents list the number as well. An item that names no source joins none by itself; the accept reply lists it
|
|
41
|
+
* (`notJoined`) for the main agent to add (`pk_write_generation`).
|
|
42
|
+
*
|
|
43
|
+
* A carried-on item is current work listed in an earlier generation: it stays live. Every reader of `Generation.workIds`
|
|
44
|
+
* that means "earlier work" — the process engine, the unplaced counts, the code map, the views — asks carried-on.ts
|
|
45
|
+
* (`earlierWork`), which leaves those items out by their state, whoever listed them.
|
|
46
|
+
*/
|
|
47
|
+
import { Ledger } from '../../ledger/index.ts';
|
|
48
|
+
import type { ClerkRound, Generation, GenerationVerdict, LayerKind } from '../../model/k-types.ts';
|
|
49
|
+
import type { WorkThread } from '../../model/types.ts';
|
|
50
|
+
import type { ProjectStore } from '../../store/project-store.ts';
|
|
51
|
+
import { familyOf } from '../../ledger/numbering.ts';
|
|
52
|
+
import { isWithin, pathKey, relativeDisplay } from '../../util/paths.ts';
|
|
53
|
+
import { ROW_POSITIONS, archiveRootOf, carriedOnItems, currentEntries, currentRows, layerEntryOver, listsWork, nameSays, slash, type DefRow } from './carried-on.ts';
|
|
54
|
+
import { GENERATION_NAMED, generationExcerpts } from './owner-lines.ts';
|
|
55
|
+
import { arrangementEvidence, inferenceContext, type InferenceContext } from './placement-inference.ts';
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
// ───────────────────────── destinations ─────────────────────────
|
|
59
|
+
|
|
60
|
+
export interface GenerationItem {
|
|
61
|
+
readonly id: string;
|
|
62
|
+
/** The project's own number, when it has one. */
|
|
63
|
+
readonly number: string;
|
|
64
|
+
readonly name: string;
|
|
65
|
+
readonly generationId: string;
|
|
66
|
+
readonly generation: string;
|
|
67
|
+
readonly validity: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type Destination =
|
|
71
|
+
| { readonly kind: 'replaced'; readonly by: string }
|
|
72
|
+
| { readonly kind: 'carried'; readonly by: string; readonly how: string }
|
|
73
|
+
| { readonly kind: 'moved'; readonly to: string }
|
|
74
|
+
| { readonly kind: 'deferred' }
|
|
75
|
+
| { readonly kind: 'abandoned' };
|
|
76
|
+
|
|
77
|
+
/** The item a `replacedBy` names: an id of the assets, or the project's own number of a work item or a reference item. */
|
|
78
|
+
export function successorOf(store: ProjectStore, ref: string | null | undefined): { readonly id: string; readonly label: string } | null {
|
|
79
|
+
const key = (ref ?? '').trim();
|
|
80
|
+
if (!key) return null;
|
|
81
|
+
const t = store.threads.get(key);
|
|
82
|
+
if (t) return { id: t.id, label: t.ids[0] ?? t.title };
|
|
83
|
+
const r = store.reference.get(key);
|
|
84
|
+
if (r) return { id: r.id, label: r.ids[0] ?? r.name };
|
|
85
|
+
const up = key.toUpperCase();
|
|
86
|
+
const byNumber = store.threads.find((x) => x.validity !== 'Removed' && x.ids.some((i) => i.toUpperCase() === up));
|
|
87
|
+
if (byNumber) return { id: byNumber.id, label: byNumber.ids[0] ?? byNumber.title };
|
|
88
|
+
const refByNumber = store.reference.find((x) => x.validity !== 'Removed' && x.ids.some((i) => i.toUpperCase() === up));
|
|
89
|
+
return refByNumber ? { id: refByNumber.id, label: refByNumber.ids[0] ?? refByNumber.name } : null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const CARRY: ReadonlySet<string> = new Set(['replaces', 'depends on', 'carries out', 'refines', 'serves', 'implements']);
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Where an item of an earlier generation went, or null when nothing recorded says (see the module comment). `inGeneration`
|
|
96
|
+
* are the ids every generation lists: an item of the same generation does not carry another on. `carried` (CZ) are the
|
|
97
|
+
* items listed because current documents define their number too: each went on under its own number, and — being current
|
|
98
|
+
* work — it can carry another item on.
|
|
99
|
+
*/
|
|
100
|
+
export function destinationOf(store: ProjectStore, t: WorkThread, inGeneration: ReadonlySet<string>, carried: ReadonlySet<string> = new Set()): Destination | null {
|
|
101
|
+
if (t.validity === 'Abandoned') return { kind: 'abandoned' };
|
|
102
|
+
// Current work the current plan lists under the same number (carried-on.ts) — a deferred contract among it.
|
|
103
|
+
if (carried.has(t.id) && t.validity !== 'Replaced') return { kind: 'carried', by: t.ids[0] ?? t.title, how: 'carried on under the same number' };
|
|
104
|
+
if (t.validity === 'Deferred') return { kind: 'deferred' };
|
|
105
|
+
const successor = successorOf(store, t.replacedBy);
|
|
106
|
+
if (successor && successor.id !== t.id) return { kind: 'replaced', by: successor.label };
|
|
107
|
+
// A replacement the project writes by a number no item carries yet still says where it went.
|
|
108
|
+
if (t.replacedBy?.trim() && !successor) return { kind: 'replaced', by: t.replacedBy.trim() };
|
|
109
|
+
const current = (id: string): boolean => {
|
|
110
|
+
if (inGeneration.has(id) && !carried.has(id)) return false;
|
|
111
|
+
const v = store.threads.get(id)?.validity ?? store.reference.get(id)?.validity;
|
|
112
|
+
return v === 'Current' || v === 'Proposed' || v === 'Deferred';
|
|
113
|
+
};
|
|
114
|
+
if (t.validity !== 'Replaced') {
|
|
115
|
+
const plan = t.serves.map((s) => store.reference.get(s.referenceId)).find((r) => r?.category === 'Plan' && r.validity === 'Current');
|
|
116
|
+
if (plan) return { kind: 'moved', to: plan.ids[0] ?? plan.name };
|
|
117
|
+
}
|
|
118
|
+
const dependent = store.threads.find((x) => x.id !== t.id && current(x.id) && x.dependsOn.some((d) => d.threadId === t.id));
|
|
119
|
+
if (dependent) return { kind: 'carried', by: dependent.ids[0] ?? dependent.title, how: 'depends on' };
|
|
120
|
+
const carrier = store.relations.find((r) => r.to === t.id && CARRY.has(r.type) && current(r.from));
|
|
121
|
+
if (carrier) return { kind: 'carried', by: store.threads.get(carrier.from)?.ids[0] ?? store.reference.get(carrier.from)?.ids[0] ?? store.threads.get(carrier.from)?.title ?? store.reference.get(carrier.from)?.name ?? carrier.from, how: carrier.type };
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The items of every recorded generation that have no destination yet. The ledger, when not given, is opened here. */
|
|
126
|
+
export function itemsWithoutDestination(store: ProjectStore, ledger?: Ledger | null): GenerationItem[] {
|
|
127
|
+
const gens = store.generations.all();
|
|
128
|
+
if (!gens.some((g) => g.workIds.length)) return [];
|
|
129
|
+
const inGeneration = new Set(gens.flatMap((g) => g.workIds));
|
|
130
|
+
let own: Ledger | null = null;
|
|
131
|
+
if (ledger === undefined) { try { own = Ledger.openDir(store.dir); } catch { own = null; } }
|
|
132
|
+
let carried = new Set<string>();
|
|
133
|
+
try { carried = carriedOnItems(store, ledger ?? own); } catch { carried = new Set(); } finally { own?.close(); }
|
|
134
|
+
const out: GenerationItem[] = [];
|
|
135
|
+
for (const g of gens) {
|
|
136
|
+
for (const id of g.workIds) {
|
|
137
|
+
const t = store.threads.get(id);
|
|
138
|
+
if (!t || t.validity === 'Removed') continue;
|
|
139
|
+
if (destinationOf(store, t, inGeneration, carried)) continue;
|
|
140
|
+
out.push({ id: t.id, number: t.ids[0] ?? '', name: t.title, generationId: g.id, generation: g.name, validity: t.validity });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ───────────────────────── candidates ─────────────────────────
|
|
147
|
+
|
|
148
|
+
export interface GenerationCandidate {
|
|
149
|
+
/** What names it: the set's directory or document (`set:<path>`), or the owner's line (`line:<draft>:<ref>`). */
|
|
150
|
+
readonly key: string;
|
|
151
|
+
readonly kind: 'set' | 'owner-line';
|
|
152
|
+
readonly name: string;
|
|
153
|
+
/** Why the program lists it, in one sentence. */
|
|
154
|
+
readonly why: string;
|
|
155
|
+
/** A set: its plan, contract and module documents (repository-relative), at most `CANDIDATE_DOCS`. */
|
|
156
|
+
readonly planRefs: readonly string[];
|
|
157
|
+
/** A set: the rows of its plan-like documents that no current document defines — the generation's own work. */
|
|
158
|
+
readonly numbers: readonly string[];
|
|
159
|
+
/** CZ, a set: the rows of its plan-like documents that current documents define too — carried on under the same number. */
|
|
160
|
+
readonly carriedOn: readonly string[];
|
|
161
|
+
/** CZ, a set: the number families its other documents define (not work of the generation), largest first. */
|
|
162
|
+
readonly alsoDefined: readonly AlsoDefined[];
|
|
163
|
+
/** A set: the commit that put it there — what ended it, as far as the program can tell. */
|
|
164
|
+
readonly endedCommit: string | null;
|
|
165
|
+
/** The generation that already holds its documents, when one does. */
|
|
166
|
+
readonly recordedAs: string | null;
|
|
167
|
+
/**
|
|
168
|
+
* CQ (D104): for a not-current execution arrangement, the program's own evidence of what its rows execute — "its rows name
|
|
169
|
+
* work of plan(s) P1 (23 tickets, 69 log entries)" — so orientation accepts or rejects it as a generation with that in
|
|
170
|
+
* hand. Its rows place the work they list whatever the verdict (placement-inference.ts).
|
|
171
|
+
*/
|
|
172
|
+
readonly executes?: string | null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** A number family a set's other documents define: how many numbers, in which document, and the current document that goes on with it. */
|
|
176
|
+
export interface AlsoDefined {
|
|
177
|
+
readonly family: string;
|
|
178
|
+
readonly count: number;
|
|
179
|
+
/** The documents of the set that define it (repository-relative), the one with the most numbers first. */
|
|
180
|
+
readonly documents: readonly string[];
|
|
181
|
+
/** The current document that defines numbers of the family, or null when none does. */
|
|
182
|
+
readonly continuedIn: string | null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export const CANDIDATE_DOCS = 12;
|
|
186
|
+
/** How many families of a set's other documents a candidate lists. */
|
|
187
|
+
export const ALSO_DEFINED = 6;
|
|
188
|
+
/** How many of the owner's lines that name generations are listed as candidates (newest first). */
|
|
189
|
+
export const OWNER_LINE_CANDIDATES = 12;
|
|
190
|
+
const CANDIDATE_NUMBERS = 80;
|
|
191
|
+
|
|
192
|
+
/** The layers a generation's plan documents are of. */
|
|
193
|
+
const PLAN_LAYERS: ReadonlySet<LayerKind> = new Set<LayerKind>(['Product', 'PRD', 'Spec', 'Plan', 'Task index', 'Task contract', 'Execution arrangement']);
|
|
194
|
+
const DECISION_NAME = nameSays('decisions?|adrs?', '决定|决策');
|
|
195
|
+
|
|
196
|
+
/** Whether a document is a decision record: mapped so, or named so. */
|
|
197
|
+
function isDecisionRecord(store: ProjectStore, path: string): boolean {
|
|
198
|
+
const over = layerEntryOver(store, path);
|
|
199
|
+
return over?.layer === 'Decision record' || DECISION_NAME.test(path.split('/').pop() ?? '');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
interface SetNumbers { readonly numbers: string[]; readonly carriedOn: string[]; readonly alsoDefined: AlsoDefined[] }
|
|
203
|
+
const byNumber = (a: string, b: string): number => a.localeCompare(b, undefined, { numeric: true });
|
|
204
|
+
|
|
205
|
+
/** CZ: what a set's documents define, read off the ledger (see the module comment). `roots` are every candidate's root. */
|
|
206
|
+
function setNumbers(store: ProjectStore, ledger: Ledger, root: string, roots: readonly string[]): SetNumbers {
|
|
207
|
+
const { defs, setAside, numberNamed, namedByOwnNumber, isCurrent } = currentRows(store, ledger, roots);
|
|
208
|
+
const rows = defs.rows.filter((r) => r.path === root || r.path.startsWith(`${root}/`));
|
|
209
|
+
if (!rows.length) return { numbers: [], carriedOn: [], alsoDefined: [] };
|
|
210
|
+
const byDoc = new Map<string, DefRow[]>();
|
|
211
|
+
for (const r of rows) byDoc.set(r.path, [...(byDoc.get(r.path) ?? []), r]);
|
|
212
|
+
/** A document's rows: its own entries at row positions; one no longer in the checkout, as the ledger first read it. */
|
|
213
|
+
const rowsIn = (path: string, docRows: readonly DefRow[]): { num: string; rule: string }[] => {
|
|
214
|
+
const now = currentEntries(ledger, defs, docRows.find((r) => r.repo)?.repo ?? null, path);
|
|
215
|
+
return (now ?? docRows.filter((r) => r.kind === 'doc').map((r) => ({ num: r.num, rule: r.rule, position: r.position ?? '' }))).filter((e) => ROW_POSITIONS.has(e.position));
|
|
216
|
+
};
|
|
217
|
+
const rowsOf = new Map<string, string>(); // number → the plan-like document that lists it
|
|
218
|
+
const others = new Map<string, Map<string, Set<string>>>(); // family → document → its numbers
|
|
219
|
+
for (const [path, docRows] of byDoc) {
|
|
220
|
+
const named = docRows.filter(namedByOwnNumber);
|
|
221
|
+
if (listsWork(store, root, path)) {
|
|
222
|
+
// A document named by a number is that number's own document; otherwise its entries are the rows.
|
|
223
|
+
for (const e of named.length ? named : rowsIn(path, docRows)) if (!rowsOf.has(e.num)) rowsOf.set(e.num, path);
|
|
224
|
+
} else if (!named.length) {
|
|
225
|
+
for (const e of rowsIn(path, docRows)) {
|
|
226
|
+
const docs = others.get(e.rule) ?? new Map<string, Set<string>>();
|
|
227
|
+
docs.set(path, (docs.get(path) ?? new Set<string>()).add(e.num));
|
|
228
|
+
others.set(e.rule, docs);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/** A decision's number: a decision record lists it among its own entries — a reference to that decision wherever else it stands. */
|
|
233
|
+
const isDecision = (n: string): boolean => (defs.byNum.get(n) ?? []).some((d) => {
|
|
234
|
+
if (d.kind !== 'doc' || !isDecisionRecord(store, d.path)) return false;
|
|
235
|
+
const own = currentEntries(ledger, defs, d.repo, d.path);
|
|
236
|
+
return own === null ? true : own.some((e) => e.num === n);
|
|
237
|
+
});
|
|
238
|
+
/**
|
|
239
|
+
* A point of numbered items, not a row of a plan: no file was ever named by it, and the current documents that list it
|
|
240
|
+
* are each named by another number (an acceptance criterion every contract has its own of).
|
|
241
|
+
*/
|
|
242
|
+
const isPoint = (n: string): boolean => {
|
|
243
|
+
if ((defs.byNum.get(n) ?? []).some((r) => r.kind === 'file-name')) return false;
|
|
244
|
+
const now = (defs.byNum.get(n) ?? []).filter((d) => d.kind === 'doc' && d.current === 1 && !setAside(d.path) && ROW_POSITIONS.has(d.position ?? ''));
|
|
245
|
+
return now.length > 0 && now.every((d) => numberNamed(d.path));
|
|
246
|
+
};
|
|
247
|
+
const plan = [...rowsOf.keys()].filter((n) => !isDecision(n) && !isPoint(n)).sort(byNumber);
|
|
248
|
+
const alsoDefined: AlsoDefined[] = [];
|
|
249
|
+
for (const [family, docs] of others) {
|
|
250
|
+
const nums = new Set([...docs.values()].flatMap((x) => [...x]).filter((n) => !rowsOf.has(n) && !isDecision(n)));
|
|
251
|
+
if (nums.size < 2) continue;
|
|
252
|
+
const documents = [...docs].map(([path, x]) => ({ path, n: [...x].filter((y) => nums.has(y)).length })).filter((d) => d.n > 0).sort((x, y) => y.n - x.n || x.path.localeCompare(y.path)).map((d) => d.path);
|
|
253
|
+
// The current document that goes on with the family: the one listing the most numbers of it as rows.
|
|
254
|
+
const counts = new Map<string, Set<string>>();
|
|
255
|
+
for (const d of defs.rows) if (d.rule === family && d.kind === 'doc' && d.current === 1 && ROW_POSITIONS.has(d.position ?? '') && !setAside(d.path)) counts.set(d.path, (counts.get(d.path) ?? new Set<string>()).add(d.num));
|
|
256
|
+
const continuedIn = [...counts].sort((x, y) => y[1].size - x[1].size || x[0].localeCompare(y[0]))[0]?.[0] ?? null;
|
|
257
|
+
alsoDefined.push({ family, count: nums.size, documents, continuedIn });
|
|
258
|
+
}
|
|
259
|
+
const own = plan.filter((n) => !isCurrent(n));
|
|
260
|
+
return {
|
|
261
|
+
numbers: own.slice(0, CANDIDATE_NUMBERS),
|
|
262
|
+
carriedOn: plan.filter((n) => !own.includes(n)).slice(0, CANDIDATE_NUMBERS),
|
|
263
|
+
alsoDefined: alsoDefined.sort((x, y) => y.count - x.count || x.family.localeCompare(y.family)).slice(0, ALSO_DEFINED),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** A document named like a plan, a contract or a module set. */
|
|
268
|
+
const PLAN_NAME = /(?:^|[\/_.-])(?:plan|plans|prd|spec|roadmap|backlog|milestones?|contracts?|modules?|product|requirements?|计划|合同|模块|需求|路线)(?:[\/_.-]|$)/i;
|
|
269
|
+
|
|
270
|
+
/** The verdicts the main agent gave on candidates, in every round (a rejected one is not listed again). */
|
|
271
|
+
export function generationVerdicts(store: ProjectStore): Map<string, GenerationVerdict> {
|
|
272
|
+
const out = new Map<string, GenerationVerdict>();
|
|
273
|
+
for (const r of store.clerkRounds.all().sort((a, b) => a.startedAt.localeCompare(b.startedAt))) for (const v of r.generationVerdicts ?? []) out.set(v.key, v);
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* What looks like an earlier generation (see the module comment), every candidate with whether a generation already holds
|
|
279
|
+
* it. Candidates the main agent gave a verdict on are left out unless `all`.
|
|
280
|
+
*/
|
|
281
|
+
export function generationCandidates(store: ProjectStore, ledger: Ledger | null, opts: { readonly all?: boolean } = {}): GenerationCandidate[] {
|
|
282
|
+
const sets = new Map<string, { docs: Set<string>; notes: string[] }>();
|
|
283
|
+
const add = (root: string, doc: string | null, note: string | null) => {
|
|
284
|
+
const s = sets.get(root) ?? { docs: new Set<string>(), notes: [] };
|
|
285
|
+
if (doc) s.docs.add(doc);
|
|
286
|
+
if (note && !s.notes.includes(note)) s.notes.push(note);
|
|
287
|
+
sets.set(root, s);
|
|
288
|
+
};
|
|
289
|
+
// 1 · the layer map: a plan-like document or directory the orientation marked not current.
|
|
290
|
+
const layerRoots: string[] = [];
|
|
291
|
+
for (const l of store.layers.all()) {
|
|
292
|
+
if (l.current) continue;
|
|
293
|
+
const path = slash(l.path);
|
|
294
|
+
const isFile = /\.[A-Za-z0-9]{1,6}$/.test(path);
|
|
295
|
+
const root = archiveRootOf(path, !isFile) ?? path;
|
|
296
|
+
if (!isFile && !layerRoots.includes(path)) layerRoots.push(path);
|
|
297
|
+
if (PLAN_LAYERS.has(l.layer)) add(root, isFile ? path : null, `${l.layer}, not current${l.note ? `: ${l.note}` : ''}`);
|
|
298
|
+
else if (archiveRootOf(path, !isFile)) add(root, null, l.note ?? null);
|
|
299
|
+
}
|
|
300
|
+
// 2 · the ledger: documents named like plans, contracts or modules kept under an archive.
|
|
301
|
+
if (ledger) {
|
|
302
|
+
let paths: { path: string }[] = [];
|
|
303
|
+
try { paths = ledger.db.prepare('SELECT DISTINCT path FROM docs').all() as { path: string }[]; } catch { paths = []; }
|
|
304
|
+
for (const { path } of paths) {
|
|
305
|
+
// A directory the layer map set aside whole is one set, whatever it holds below.
|
|
306
|
+
const root = layerRoots.filter((r) => path.startsWith(`${r}/`)).sort((a, b) => a.length - b.length)[0] ?? archiveRootOf(path);
|
|
307
|
+
if (!root) continue;
|
|
308
|
+
const inside = path.slice(root.length + 1);
|
|
309
|
+
if (PLAN_NAME.test(`/${inside}`) || sets.has(root)) add(root, PLAN_NAME.test(`/${inside}`) ? path : null, null);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// A set with no plan-like document is an archive of something else (reports, history logs): not a generation's plans.
|
|
313
|
+
// CQ (D104): what an archived execution arrangement's rows execute, read once when a candidate needs it.
|
|
314
|
+
let ctx: InferenceContext | null = null;
|
|
315
|
+
const evidence = (root: string): string | null => { try { ctx ??= inferenceContext(store, ledger); return arrangementEvidence(ctx, root); } catch { return null; } };
|
|
316
|
+
const verdicts = generationVerdicts(store);
|
|
317
|
+
const gens = store.generations.all();
|
|
318
|
+
const recordedAs = (root: string, docs: readonly string[]): string | null => {
|
|
319
|
+
const g = gens.find((x) => x.planRefs.some((p) => { const id = slash(p.id); return id === root || id.startsWith(`${root}/`) || docs.includes(id); }));
|
|
320
|
+
return g ? g.name : null;
|
|
321
|
+
};
|
|
322
|
+
const roots = [...sets.keys()];
|
|
323
|
+
const out: GenerationCandidate[] = [];
|
|
324
|
+
for (const [root, s] of [...sets].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
325
|
+
const docs = [...s.docs].sort();
|
|
326
|
+
if (!docs.length && !s.notes.some((n) => /not current/.test(n))) continue;
|
|
327
|
+
let defined: SetNumbers = { numbers: [], carriedOn: [], alsoDefined: [] };
|
|
328
|
+
let endedCommit: string | null = null;
|
|
329
|
+
if (ledger) {
|
|
330
|
+
try {
|
|
331
|
+
defined = setNumbers(store, ledger, root, roots);
|
|
332
|
+
const first = ledger.db.prepare('SELECT c.hash FROM commit_files f JOIN commits c ON c.repo = f.repo AND c.hash = f.hash WHERE f.path = ? OR f.path LIKE ? ORDER BY c.author_ms ASC LIMIT 1').get(root, `${root}/%`) as { hash: string } | undefined;
|
|
333
|
+
endedCommit = first?.hash ?? null;
|
|
334
|
+
} catch { /* the ledger has no such table in this build: the candidate stands without numbers */ }
|
|
335
|
+
}
|
|
336
|
+
const key = `set:${root}`;
|
|
337
|
+
// CQ (D104): a not-current execution arrangement is listed only with the program's own evidence of what its rows execute.
|
|
338
|
+
const executes = s.notes.some((n) => n.startsWith('Execution arrangement')) ? evidence(root) : null;
|
|
339
|
+
const { numbers, carriedOn, alsoDefined } = defined;
|
|
340
|
+
const rowsSaid = [
|
|
341
|
+
numbers.length ? `${numbers.length} row${numbers.length === 1 ? '' : 's'} no current document lists` : '',
|
|
342
|
+
carriedOn.length ? `${carriedOn.length} carried on under the same number` : '',
|
|
343
|
+
].filter(Boolean).join(' and ');
|
|
344
|
+
const c: GenerationCandidate = {
|
|
345
|
+
key, kind: 'set', name: root.split('/').slice(-1)[0] ?? root,
|
|
346
|
+
why: `${root} holds ${docs.length ? `${docs.length} plan, contract or module document${docs.length === 1 ? '' : 's'}` : 'documents'} set aside${s.notes.length ? ` (${s.notes.slice(0, 2).join('; ')})` : ''}${rowsSaid ? `; its plan documents list ${rowsSaid}` : ''}${executes ? `; ${executes}` : ''}`,
|
|
347
|
+
planRefs: docs.slice(0, CANDIDATE_DOCS), numbers, carriedOn, alsoDefined, endedCommit, recordedAs: recordedAs(root, docs), ...(executes ? { executes } : {}),
|
|
348
|
+
};
|
|
349
|
+
if (!opts.all && verdicts.has(key)) continue;
|
|
350
|
+
out.push(c);
|
|
351
|
+
}
|
|
352
|
+
// 3 · the owner's lines that name generations, whatever the session draft labelled them.
|
|
353
|
+
const lines: (GenerationCandidate & { at: string })[] = [];
|
|
354
|
+
for (const d of store.drafts.all()) {
|
|
355
|
+
for (const l of d.ownerLines) {
|
|
356
|
+
const m = GENERATION_NAMED.exec(l.text);
|
|
357
|
+
if (!m) continue;
|
|
358
|
+
const key = `line:${d.id}:${l.ref}`;
|
|
359
|
+
if (!opts.all && verdicts.has(key)) continue;
|
|
360
|
+
const flat = l.text.replace(/\s+/g, ' ');
|
|
361
|
+
const at = flat.indexOf(m[0]);
|
|
362
|
+
const excerpt = `${at > 60 ? '…' : ''}${flat.slice(Math.max(0, at - 60), at + m[0].length + 100)}${at + m[0].length + 100 < flat.length ? '…' : ''}`;
|
|
363
|
+
lines.push({ key, kind: 'owner-line', name: `the owner, ${l.at.slice(0, 10)}`, why: `the owner's line names generations or versions: 「${excerpt}」`, planRefs: [], numbers: [], carriedOn: [], alsoDefined: [], endedCommit: null, recordedAs: null, at: l.at });
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// The newest lines first; a long history of sessions has many, and the newest say how the owner sees the generations now.
|
|
367
|
+
for (const { at: _at, ...c } of lines.sort((a, b) => b.at.localeCompare(a.at)).slice(0, OWNER_LINE_CANDIDATES)) out.push(c);
|
|
368
|
+
return out;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** The document sets not yet judged and not yet held by a recorded generation: what orientation has to accept or reject. */
|
|
372
|
+
export function openCandidates(store: ProjectStore, ledger: Ledger | null): GenerationCandidate[] {
|
|
373
|
+
return generationCandidates(store, ledger).filter((c) => c.kind === 'set' && !c.recordedAs);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** The block orientation is given: the candidate generations to accept or reject, each with why and what accepting writes. */
|
|
377
|
+
export function generationCandidatesBlock(store: ProjectStore, ledger: Ledger | null): string {
|
|
378
|
+
const head = '=== Candidate generations (the program’s list: archived or superseded plan, contract and module sets, and the owner’s lines that name generations — accept or reject each with pk_generation_candidate)';
|
|
379
|
+
const all = generationCandidates(store, ledger);
|
|
380
|
+
const open = all.filter((c) => !c.recordedAs);
|
|
381
|
+
const recorded = all.filter((c) => c.recordedAs);
|
|
382
|
+
if (!all.length) return `${head}\nNone: no plan, contract or module set is kept under an archive or marked not current, and no owner's line names a generation.`;
|
|
383
|
+
const lines = [head];
|
|
384
|
+
const sets = open.filter((c) => c.kind === 'set');
|
|
385
|
+
for (const c of sets) {
|
|
386
|
+
lines.push(`- ${c.key} · ${c.name}: ${c.why}`);
|
|
387
|
+
if (c.executes) lines.push(` the program's evidence: ${c.executes} — an execution arrangement of a current plan is the record of how that plan was carried out, not a generation; its rows place the work they list whatever your verdict`);
|
|
388
|
+
lines.push(` accepting records it with ${c.planRefs.length} plan document${c.planRefs.length === 1 ? '' : 's'}${c.endedCommit ? `, ended by commit ${c.endedCommit.slice(0, 7)} unless you give what ended it` : ''}`);
|
|
389
|
+
lines.push(...candidateWorkLines(c).map((l) => ` ${l}`));
|
|
390
|
+
}
|
|
391
|
+
if (!sets.length) lines.push('No document set waits for a verdict.');
|
|
392
|
+
if (recorded.length) lines.push(`Already recorded: ${recorded.map((c) => `${c.name} → ${c.recordedAs}`).join('; ')}.`);
|
|
393
|
+
const said = open.filter((c) => c.kind === 'owner-line');
|
|
394
|
+
if (said.length) {
|
|
395
|
+
lines.push(`The owner's lines that name generations or versions (${said.length}, newest first; read them before you count the generations — they are not judged one by one):`);
|
|
396
|
+
for (const c of said) lines.push(`- ${c.key.slice(5)} · ${c.name}: ${c.why.replace(/^the owner's line names generations or versions: /, '')}`);
|
|
397
|
+
}
|
|
398
|
+
lines.push('Whether each set is a generation is yours to judge from the documents (a version history moved to an archive is not a generation of plans; whether an earlier version of the current contracts counts as one is for you to read off the documents and the owner’s lines): reject with why, or accept — one call each.');
|
|
399
|
+
return lines.join('\n');
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** Numbers said by family, the largest first: `CKC-01, CKC-02, CKC-03 … (27); AC-15, AC-30 (2)` — or plainly when they are one family. */
|
|
403
|
+
function byFamily(list: readonly string[], n = 4): string {
|
|
404
|
+
const groups = new Map<string, string[]>();
|
|
405
|
+
for (const x of list) { const f = familyOf(x)?.family ?? x.replace(/\d+$/, '<n>'); groups.set(f, [...(groups.get(f) ?? []), x]); }
|
|
406
|
+
const said = [...groups.values()].sort((x, y) => y.length - x.length).map((g) => `${g.length > n + 1 ? `${g.slice(0, n).join(', ')} … ${g[g.length - 1]}` : g.join(', ')}${groups.size > 1 ? ` (${g.length})` : ''}`);
|
|
407
|
+
return said.join('; ');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* CZ: what a candidate's documents define, in the words the main agent reads — its work (the rows of its plan documents),
|
|
412
|
+
* what went on under the same number, and what its other documents define, each kept apart.
|
|
413
|
+
*/
|
|
414
|
+
export function candidateWorkLines(c: GenerationCandidate): string[] {
|
|
415
|
+
const out: string[] = [];
|
|
416
|
+
out.push(c.numbers.length
|
|
417
|
+
? `its work — the rows of its plan documents that no current document lists (${c.numbers.length}): ${byFamily(c.numbers)}`
|
|
418
|
+
: `its plan documents list no row that only they define${c.carriedOn.length ? '' : ' (the program found no plan table, task index or contract set in it)'}`);
|
|
419
|
+
if (c.carriedOn.length) out.push(`carried on under the same number (${c.carriedOn.length}): ${byFamily(c.carriedOn)} — current documents list these rows too; the current items of those numbers are its items, with that destination`);
|
|
420
|
+
if (c.alsoDefined.length) {
|
|
421
|
+
out.push(`also defined in this set, not in a plan document: ${c.alsoDefined.map((a) => `${a.family} (${a.count}) in ${a.documents[0]}${a.documents.length > 1 ? ` and ${a.documents.length - 1} more` : ''} — ${a.continuedIn ? `${a.continuedIn} continues the family` : 'no current document continues the family'}`).join('; ')}. These are not its work items: what they are is read from their document`);
|
|
422
|
+
}
|
|
423
|
+
return out;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** A verdict recorded on the round, replacing an earlier one on the same candidate. */
|
|
427
|
+
export function withVerdict(round: ClerkRound, verdict: GenerationVerdict): ClerkRound {
|
|
428
|
+
return { ...round, generationVerdicts: [...(round.generationVerdicts ?? []).filter((v) => v.key !== verdict.key), verdict], updatedAt: verdict.at };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* CZ: the repository-relative paths of the documents a work item was written from — the source its fill cited
|
|
433
|
+
* (`inputs.sourceIds`) and the source of its written status. Empty when nothing recorded says (an item written by hand
|
|
434
|
+
* before the fill recorded it).
|
|
435
|
+
*/
|
|
436
|
+
export function originOf(store: ProjectStore, ledger: Ledger | null, t: WorkThread): string[] {
|
|
437
|
+
const ids = [...new Set([...(t.writtenStatus ? [t.writtenStatus.sourceId] : []), ...(t.inputs?.sourceIds ?? [])])];
|
|
438
|
+
if (!ids.length) return [];
|
|
439
|
+
const repos = (ledger?.repos() ?? []).map((r) => r.path.replace(/\\/g, '/').replace(/\/+$/, '')).sort((x, y) => y.length - x.length);
|
|
440
|
+
const out: string[] = [];
|
|
441
|
+
for (const id of ids) {
|
|
442
|
+
const a = store.sources.get(id)?.anchor;
|
|
443
|
+
if (!a) continue;
|
|
444
|
+
if (a.kind === 'revision') { out.push(slash(a.path)); continue; }
|
|
445
|
+
if (a.kind !== 'file') continue;
|
|
446
|
+
const abs = a.path.replace(/\\/g, '/');
|
|
447
|
+
const repo = repos.find((r) => (isWithin(r, a.path) && pathKey(r) !== pathKey(a.path)) || abs.toLowerCase().startsWith(`${r.toLowerCase()}/`));
|
|
448
|
+
out.push(repo ? (isWithin(repo, a.path) ? relativeDisplay(repo, a.path) : abs.slice(repo.length + 1)) : abs);
|
|
449
|
+
}
|
|
450
|
+
return [...new Set(out)];
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const under = (root: string, path: string): boolean => path === root || path.startsWith(`${root}/`) || path.endsWith(`/${root}`) || path.includes(`/${root}/`);
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* CZ: the work items of a candidate generation (see the module comment, "Which items join"). `all` are every candidate
|
|
457
|
+
* set, so a number two sets' plan documents define is never settled by the number.
|
|
458
|
+
*/
|
|
459
|
+
export function generationItems(store: ProjectStore, ledger: Ledger | null, c: GenerationCandidate, all: readonly GenerationCandidate[]): string[] {
|
|
460
|
+
const own = new Set(c.numbers.map((n) => n.toUpperCase()));
|
|
461
|
+
const carried = new Set(c.carriedOn.map((n) => n.toUpperCase()));
|
|
462
|
+
if (!own.size && !carried.size) return [];
|
|
463
|
+
const root = c.key.replace(/^set:/, '');
|
|
464
|
+
const otherSets = all.filter((x) => x.kind === 'set' && x.key !== c.key);
|
|
465
|
+
const elsewhere = new Set(otherSets.flatMap((x) => [...x.numbers, ...x.carriedOn]).map((n) => n.toUpperCase()));
|
|
466
|
+
const otherRoots = otherSets.map((x) => x.key.replace(/^set:/, ''));
|
|
467
|
+
const out: string[] = [];
|
|
468
|
+
for (const t of store.threads.filter((x) => x.validity !== 'Removed')) {
|
|
469
|
+
const n = t.ids.map((i) => i.toUpperCase()).find((i) => own.has(i) || carried.has(i));
|
|
470
|
+
if (!n) continue;
|
|
471
|
+
const from = originOf(store, ledger, t);
|
|
472
|
+
if (from.some((p) => under(root, p))) { out.push(t.id); continue; }
|
|
473
|
+
// Written from a current document that defines the number too: carried on — unless another set claims the number.
|
|
474
|
+
if (carried.has(n) && from.length && !elsewhere.has(n) && !from.some((p) => otherRoots.some((r) => under(r, p)) || archiveRootOf(p) !== null)) out.push(t.id);
|
|
475
|
+
}
|
|
476
|
+
return out;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* The work items of an accepted candidate join its generation as they appear (CM): orientation accepts a set before the
|
|
481
|
+
* skeleton's lanes have filled its items, and nobody should have to add them one by one afterwards. CZ: which items those
|
|
482
|
+
* are is `generationItems` — by the document they were written from, never by the number alone. Returns how many were
|
|
483
|
+
* added. Run where the round's state is read (`pk_round_state`, `pk_stage`).
|
|
484
|
+
*/
|
|
485
|
+
export function attachAcceptedItems(store: ProjectStore, ledger: Ledger | null, jobId: string | null = null): number {
|
|
486
|
+
const accepted = [...generationVerdicts(store).values()].filter((v) => v.accepted && v.generationId);
|
|
487
|
+
if (!accepted.length) return 0;
|
|
488
|
+
const all = generationCandidates(store, ledger, { all: true });
|
|
489
|
+
const byKey = new Map(all.map((c) => [c.key, c]));
|
|
490
|
+
let added = 0;
|
|
491
|
+
for (const v of accepted) {
|
|
492
|
+
const g = store.generations.get(v.generationId!);
|
|
493
|
+
const c = byKey.get(v.key);
|
|
494
|
+
if (!g || !c || (!c.numbers.length && !c.carriedOn.length)) continue;
|
|
495
|
+
const fresh = generationItems(store, ledger, c, all).filter((id) => !g.workIds.includes(id));
|
|
496
|
+
if (!fresh.length) continue;
|
|
497
|
+
store.generations.put({ ...g, workIds: [...g.workIds, ...fresh], updatedAt: new Date().toISOString() }, { jobId, basisSourceIds: [], summary: `Generation “${g.name}”: ${fresh.length} work item${fresh.length === 1 ? '' : 's'} written from its documents, or carried on under the same number, joined it` });
|
|
498
|
+
added += fresh.length;
|
|
499
|
+
}
|
|
500
|
+
return added;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export { archiveRootOf, carriedOnItems, listsWork };
|
|
504
|
+
export type { Generation };
|