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,487 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ledger's facts the process engine reads (Spec v3.0 §1.16, §2.12), loaded once per computation in a few batched
|
|
3
|
+
* reads, with the indexes the engine needs: every commit with its parents and the merge that brought it into the trunk,
|
|
4
|
+
* the branches each side commit lies on, the execution arrangements (§1.16 row 6), the verdicts, findings and test counts
|
|
5
|
+
* (row 5), the numbers of the units of work (row 4), the document versions (row 2) and the explicit supersessions
|
|
6
|
+
* (row 3).
|
|
7
|
+
*
|
|
8
|
+
* Everything here is a fact of the ledger; nothing is judged. The engine never re-reads git: what the ledger records is
|
|
9
|
+
* read from its tables (read-only, the way `ledger/views.ts` reads code files), and an entry is cited by its ledger id,
|
|
10
|
+
* resolved through `Ledger.resolve` for the label and the time the workbench shows (§2.11).
|
|
11
|
+
*
|
|
12
|
+
* Why direct table reads and not the paged queries: the `Ledger` class has no query for a commit's parent hashes or for
|
|
13
|
+
* the branch rows, and its paged queries cap a page at 500 rows, so a project-wide pass would need hundreds of calls
|
|
14
|
+
* (reported as a query gap).
|
|
15
|
+
*/
|
|
16
|
+
import { realpathSync } from 'node:fs';
|
|
17
|
+
import type { Ledger } from '../ledger/index.ts';
|
|
18
|
+
import { isWithin, normalizePath } from '../util/paths.ts';
|
|
19
|
+
import { materialTime, msOf } from '../ledger/time.ts';
|
|
20
|
+
import type { EvidenceRef, Occurred, OccurredBasis } from '../model/k-types.ts';
|
|
21
|
+
import { familyPrefix, rangesIn } from '../ledger/ranges.ts';
|
|
22
|
+
|
|
23
|
+
// ───────────────────────── row shapes ─────────────────────────
|
|
24
|
+
|
|
25
|
+
export interface CommitFact {
|
|
26
|
+
readonly repo: string;
|
|
27
|
+
readonly hash: string;
|
|
28
|
+
/** The ledger entry id: `commit:<12>`. */
|
|
29
|
+
readonly id: string;
|
|
30
|
+
readonly short: string;
|
|
31
|
+
readonly parents: readonly string[];
|
|
32
|
+
readonly ms: number;
|
|
33
|
+
readonly occurred: Occurred;
|
|
34
|
+
readonly author: string;
|
|
35
|
+
readonly subject: string;
|
|
36
|
+
readonly body: string;
|
|
37
|
+
readonly merge: boolean;
|
|
38
|
+
readonly onTrunk: boolean;
|
|
39
|
+
/** On the trunk's first-parent line: a direct trunk commit or the merge that brought a branch in. */
|
|
40
|
+
readonly fpTrunk: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface BranchFact {
|
|
44
|
+
readonly repo: string;
|
|
45
|
+
readonly name: string;
|
|
46
|
+
readonly namespace: string;
|
|
47
|
+
readonly tip: string;
|
|
48
|
+
readonly merged: boolean | null;
|
|
49
|
+
readonly ahead: number | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ArrangementFact {
|
|
53
|
+
readonly id: string;
|
|
54
|
+
readonly repo: string | null;
|
|
55
|
+
readonly path: string;
|
|
56
|
+
readonly commit: string | null;
|
|
57
|
+
readonly kind: 'index' | 'prompt' | 'plan' | 'status' | 'receipt' | string;
|
|
58
|
+
readonly ident: string | null;
|
|
59
|
+
readonly parsed: boolean;
|
|
60
|
+
readonly current: boolean;
|
|
61
|
+
readonly ms: number;
|
|
62
|
+
readonly occurred: Occurred;
|
|
63
|
+
readonly data: ArrangementData;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The parsed fields the ledger keeps for an arrangement version (ledger/arrangements.ts `ArrangementData`). */
|
|
67
|
+
export interface ArrangementData {
|
|
68
|
+
readonly title?: string;
|
|
69
|
+
readonly fields?: Readonly<Record<string, string>>;
|
|
70
|
+
readonly status?: string | null;
|
|
71
|
+
readonly agent?: string | null;
|
|
72
|
+
readonly model?: string | null;
|
|
73
|
+
readonly worktree?: string | null;
|
|
74
|
+
readonly baseline?: string | null;
|
|
75
|
+
readonly accepted?: string | null;
|
|
76
|
+
readonly rows?: readonly { readonly id: string; readonly cells: Readonly<Record<string, string>>; readonly line: number }[];
|
|
77
|
+
readonly batches?: readonly string[];
|
|
78
|
+
readonly dependencies?: readonly string[];
|
|
79
|
+
readonly parallel?: readonly string[];
|
|
80
|
+
readonly json?: Readonly<Record<string, unknown>>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface VerdictFact {
|
|
84
|
+
readonly id: string;
|
|
85
|
+
readonly repo: string | null;
|
|
86
|
+
readonly path: string;
|
|
87
|
+
readonly kind: 'verdict' | 'count' | 'finding' | string;
|
|
88
|
+
/** Normalised verdict (pass, fail …), a count ("14/14"), or a finding's number ("F-1"). */
|
|
89
|
+
readonly verdict: string | null;
|
|
90
|
+
readonly confidence: 'stated' | 'candidate' | string;
|
|
91
|
+
readonly text: string;
|
|
92
|
+
readonly line: number | null;
|
|
93
|
+
readonly firstCommit: string | null;
|
|
94
|
+
readonly ms: number;
|
|
95
|
+
readonly occurred: Occurred;
|
|
96
|
+
readonly current: boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface NumFact {
|
|
100
|
+
readonly id: string;
|
|
101
|
+
readonly num: string;
|
|
102
|
+
readonly kind: 'doc' | 'commit' | 'branch' | 'worktree' | 'file-name' | 'loose' | string;
|
|
103
|
+
readonly place: 'definition' | 'mention' | string;
|
|
104
|
+
readonly repo: string | null;
|
|
105
|
+
readonly path: string | null;
|
|
106
|
+
readonly commit: string | null;
|
|
107
|
+
readonly line: number | null;
|
|
108
|
+
readonly context: string;
|
|
109
|
+
readonly confidence: string;
|
|
110
|
+
readonly ms: number;
|
|
111
|
+
readonly occurred: Occurred;
|
|
112
|
+
readonly current: boolean;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const NUM_COLUMNS = 'key, num, kind, place, repo, path, commit_hash, line, context, confidence, first_ms, occurred_at, occurred_basis, occurred_anchor, other_at, other_basis, undated, current';
|
|
116
|
+
|
|
117
|
+
/** A row of the ledger's `nums` table as a fact. */
|
|
118
|
+
function numFactOf(r: Record<string, unknown>): NumFact {
|
|
119
|
+
const occurred = rowOccurred(r);
|
|
120
|
+
return {
|
|
121
|
+
id: String(r.key), num: String(r.num), kind: String(r.kind), place: String(r.place), repo: str(r.repo), path: str(r.path), commit: str(r.commit_hash),
|
|
122
|
+
line: (r.line ?? null) as number | null, context: String(r.context ?? ''), confidence: String(r.confidence),
|
|
123
|
+
ms: r.first_ms === null || r.first_ms === undefined ? msOf(occurred.at) ?? 0 : Number(r.first_ms), occurred, current: Number(r.current) === 1,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface DocVersionFact {
|
|
128
|
+
readonly id: string;
|
|
129
|
+
readonly repo: string;
|
|
130
|
+
readonly path: string;
|
|
131
|
+
readonly commit: string;
|
|
132
|
+
readonly ms: number;
|
|
133
|
+
readonly change: string;
|
|
134
|
+
readonly added: readonly string[];
|
|
135
|
+
readonly changed: readonly string[];
|
|
136
|
+
readonly removed: readonly string[];
|
|
137
|
+
readonly current: boolean;
|
|
138
|
+
readonly onTrunk: boolean;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface SupersessionFact {
|
|
142
|
+
readonly id: string;
|
|
143
|
+
readonly repo: string | null;
|
|
144
|
+
readonly source: string;
|
|
145
|
+
readonly path: string | null;
|
|
146
|
+
readonly pattern: string;
|
|
147
|
+
readonly target: string | null;
|
|
148
|
+
readonly replaced: string | null;
|
|
149
|
+
readonly replacement: string | null;
|
|
150
|
+
readonly syntax: string | null;
|
|
151
|
+
readonly text: string;
|
|
152
|
+
readonly obsoleteList: boolean;
|
|
153
|
+
readonly line: number | null;
|
|
154
|
+
readonly firstCommit: string | null;
|
|
155
|
+
readonly ms: number;
|
|
156
|
+
readonly occurred: Occurred;
|
|
157
|
+
readonly current: boolean;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ───────────────────────── helpers ─────────────────────────
|
|
161
|
+
|
|
162
|
+
const json = <T>(s: unknown, fallback: T): T => { try { return typeof s === 'string' && s ? (JSON.parse(s) as T) : fallback; } catch { return fallback; } };
|
|
163
|
+
const str = (v: unknown): string | null => (typeof v === 'string' ? v : v === null || v === undefined ? null : String(v));
|
|
164
|
+
|
|
165
|
+
/** When a row happened, the way the ledger's queries give it (`Ledger.rowOccurred`). */
|
|
166
|
+
export function rowOccurred(r: Record<string, unknown>): Occurred {
|
|
167
|
+
const at = String(r.occurred_at ?? '');
|
|
168
|
+
return {
|
|
169
|
+
at, basis: String(r.occurred_basis ?? 'First observed') as OccurredBasis, anchor: str(r.occurred_anchor),
|
|
170
|
+
...(r.other_at ? { other: { at: String(r.other_at), basis: String(r.other_basis ?? 'Commit') as OccurredBasis, anchor: null } } : {}),
|
|
171
|
+
...(r.undated ? { undated: true } : {}),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** A commit's time: the author time, with the committer time beside it when it differs (`Ledger.commitOccurred`). */
|
|
176
|
+
export function commitOccurred(hash: string, authorAt: string, committerAt: string | null): Occurred {
|
|
177
|
+
const a = materialTime(authorAt) ?? authorAt;
|
|
178
|
+
const c = committerAt ? materialTime(committerAt) ?? committerAt : null;
|
|
179
|
+
return { at: a, basis: 'Commit', anchor: `commit:${hash.slice(0, 12)}`, ...(c && c !== a ? { other: { at: c, basis: 'Commit', anchor: 'committer time' } } : {}) };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export const msOfOccurred = (o: Occurred | null | undefined): number => (o ? msOf(o.at) ?? 0 : 0);
|
|
183
|
+
|
|
184
|
+
function chunks<T>(xs: readonly T[], n: number): T[][] {
|
|
185
|
+
const out: T[][] = [];
|
|
186
|
+
for (let i = 0; i < xs.length; i += n) out.push(xs.slice(i, i + n));
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ───────────────────────── the facts ─────────────────────────
|
|
191
|
+
|
|
192
|
+
export class Facts {
|
|
193
|
+
readonly ledger: Ledger;
|
|
194
|
+
readonly commits = new Map<string, CommitFact>(); // by full hash
|
|
195
|
+
readonly commitsByRepo = new Map<string, CommitFact[]>(); // oldest first
|
|
196
|
+
/** The trunk commit on the first-parent line that brought a commit into the trunk (itself for a direct trunk commit). */
|
|
197
|
+
readonly introducedBy = new Map<string, string>();
|
|
198
|
+
/** Side commits (not on the trunk's first-parent line) of each branch, and the branches each side commit is on. */
|
|
199
|
+
readonly branchCommits = new Map<string, Set<string>>(); // branch name → hashes
|
|
200
|
+
readonly branchesOf = new Map<string, string[]>(); // hash → branch names
|
|
201
|
+
readonly branches: BranchFact[] = [];
|
|
202
|
+
readonly trunkTip = new Map<string, string | null>(); // repo → trunk tip
|
|
203
|
+
readonly arrangements: ArrangementFact[] = [];
|
|
204
|
+
readonly arrangementsByIdent = new Map<string, ArrangementFact[]>();
|
|
205
|
+
readonly arrangementsByPath = new Map<string, ArrangementFact[]>();
|
|
206
|
+
readonly verdicts: VerdictFact[] = [];
|
|
207
|
+
readonly verdictsByPath = new Map<string, VerdictFact[]>();
|
|
208
|
+
readonly docs: DocVersionFact[] = [];
|
|
209
|
+
readonly docsByPath = new Map<string, DocVersionFact[]>();
|
|
210
|
+
readonly supersessions: SupersessionFact[] = [];
|
|
211
|
+
/** The latest commit time in the ledger: "now" as far as the project's material goes. */
|
|
212
|
+
readonly latestMs: number;
|
|
213
|
+
private readonly numsCache = new Map<string, NumFact[]>();
|
|
214
|
+
private readonly rangeCache = new Map<string, { row: NumFact; members: Set<string> }[]>();
|
|
215
|
+
private readonly entryCache = new Map<string, { label: string; occurred: Occurred; text?: string } | null>();
|
|
216
|
+
private readonly filesCache = new Map<string, { path: string; status: string }[]>();
|
|
217
|
+
|
|
218
|
+
constructor(ledger: Ledger) {
|
|
219
|
+
this.ledger = ledger;
|
|
220
|
+
const db = ledger.db;
|
|
221
|
+
for (const r of ledger.repos()) this.trunkTip.set(r.id, r.trunkTip);
|
|
222
|
+
// Commits, oldest first per repository.
|
|
223
|
+
const rows = db.prepare('SELECT repo, hash, parents, author_at, author_ms, committer_at, author, subject, body, merge, on_trunk, first_parent_trunk FROM commits ORDER BY author_ms, hash').all() as Record<string, unknown>[];
|
|
224
|
+
let latest = 0;
|
|
225
|
+
for (const r of rows) {
|
|
226
|
+
const hash = String(r.hash);
|
|
227
|
+
const c: CommitFact = {
|
|
228
|
+
repo: String(r.repo), hash, id: `commit:${hash.slice(0, 12)}`, short: hash.slice(0, 7), parents: String(r.parents).split(' ').filter(Boolean),
|
|
229
|
+
ms: Number(r.author_ms), occurred: commitOccurred(hash, String(r.author_at), str(r.committer_at)), author: String(r.author ?? ''),
|
|
230
|
+
subject: String(r.subject ?? ''), body: String(r.body ?? ''), merge: Number(r.merge) === 1, onTrunk: Number(r.on_trunk) === 1, fpTrunk: Number(r.first_parent_trunk) === 1,
|
|
231
|
+
};
|
|
232
|
+
this.commits.set(hash, c);
|
|
233
|
+
const list = this.commitsByRepo.get(c.repo) ?? [];
|
|
234
|
+
list.push(c);
|
|
235
|
+
this.commitsByRepo.set(c.repo, list);
|
|
236
|
+
if (c.ms > latest) latest = c.ms;
|
|
237
|
+
}
|
|
238
|
+
this.latestMs = latest;
|
|
239
|
+
this.computeIntroducedBy();
|
|
240
|
+
for (const b of db.prepare('SELECT repo, name, namespace, tip, merged, ahead FROM branches ORDER BY name').all() as Record<string, unknown>[]) {
|
|
241
|
+
this.branches.push({ repo: String(b.repo), name: String(b.name), namespace: String(b.namespace), tip: String(b.tip), merged: b.merged === null ? null : Number(b.merged) === 1, ahead: b.ahead === null ? null : Number(b.ahead) });
|
|
242
|
+
}
|
|
243
|
+
this.computeBranchCommits();
|
|
244
|
+
for (const r of db.prepare('SELECT key, repo, path, commit_hash, kind, ident, parsed, data, current, occurred_at, occurred_ms, occurred_basis, occurred_anchor, other_at, other_basis, undated FROM plans').all() as Record<string, unknown>[]) {
|
|
245
|
+
const occurred = rowOccurred(r);
|
|
246
|
+
const a: ArrangementFact = {
|
|
247
|
+
id: String(r.key), repo: str(r.repo), path: String(r.path), commit: str(r.commit_hash), kind: String(r.kind), ident: str(r.ident),
|
|
248
|
+
parsed: Number(r.parsed) === 1, current: Number(r.current) === 1, ms: r.occurred_ms === null || r.occurred_ms === undefined ? msOf(occurred.at) ?? 0 : Number(r.occurred_ms),
|
|
249
|
+
occurred, data: json<ArrangementData>(r.data, {}),
|
|
250
|
+
};
|
|
251
|
+
this.arrangements.push(a);
|
|
252
|
+
if (a.ident) this.arrangementsByIdent.set(a.ident, [...(this.arrangementsByIdent.get(a.ident) ?? []), a]);
|
|
253
|
+
this.arrangementsByPath.set(a.path, [...(this.arrangementsByPath.get(a.path) ?? []), a]);
|
|
254
|
+
}
|
|
255
|
+
const byTime = (x: { ms: number; path?: string }, y: { ms: number; path?: string }) => x.ms - y.ms || (x.path ?? '').localeCompare(y.path ?? '');
|
|
256
|
+
this.arrangements.sort(byTime);
|
|
257
|
+
for (const list of this.arrangementsByIdent.values()) list.sort(byTime);
|
|
258
|
+
for (const list of this.arrangementsByPath.values()) list.sort(byTime);
|
|
259
|
+
for (const r of db.prepare('SELECT key, repo, path, kind, verdict, confidence, text, first_commit, first_line, current_line, first_ms, occurred_at, occurred_basis, occurred_anchor, other_at, other_basis, undated, current FROM verdicts').all() as Record<string, unknown>[]) {
|
|
260
|
+
const occurred = rowOccurred(r);
|
|
261
|
+
const v: VerdictFact = {
|
|
262
|
+
id: String(r.key), repo: str(r.repo), path: String(r.path), kind: String(r.kind), verdict: str(r.verdict), confidence: String(r.confidence), text: String(r.text ?? ''),
|
|
263
|
+
line: (r.current_line ?? r.first_line ?? null) as number | null, firstCommit: str(r.first_commit),
|
|
264
|
+
ms: r.first_ms === null || r.first_ms === undefined ? msOf(occurred.at) ?? 0 : Number(r.first_ms), occurred, current: Number(r.current) === 1,
|
|
265
|
+
};
|
|
266
|
+
this.verdicts.push(v);
|
|
267
|
+
this.verdictsByPath.set(v.path, [...(this.verdictsByPath.get(v.path) ?? []), v]);
|
|
268
|
+
}
|
|
269
|
+
for (const list of this.verdictsByPath.values()) list.sort((a, b) => (a.line ?? 0) - (b.line ?? 0) || a.ms - b.ms);
|
|
270
|
+
for (const r of db.prepare(`SELECT d.key, d.repo, d.path, d.commit_hash, d.at_ms, d.change, d.added, d.removed, d.changed, c.on_trunk,
|
|
271
|
+
(SELECT count(*) FROM code_files f WHERE f.repo = d.repo AND f.path = d.path AND f.blob = d.blob) cur
|
|
272
|
+
FROM docs d LEFT JOIN commits c ON c.repo = d.repo AND c.hash = d.commit_hash ORDER BY d.at_ms, d.commit_hash`).all() as Record<string, unknown>[]) {
|
|
273
|
+
const d: DocVersionFact = {
|
|
274
|
+
id: String(r.key), repo: String(r.repo), path: String(r.path), commit: String(r.commit_hash), ms: Number(r.at_ms), change: String(r.change),
|
|
275
|
+
added: json<string[]>(r.added, []), changed: json<string[]>(r.changed, []), removed: json<string[]>(r.removed, []), current: Number(r.cur) > 0, onTrunk: Number(r.on_trunk) === 1,
|
|
276
|
+
};
|
|
277
|
+
this.docs.push(d);
|
|
278
|
+
this.docsByPath.set(d.path, [...(this.docsByPath.get(d.path) ?? []), d]);
|
|
279
|
+
}
|
|
280
|
+
for (const r of db.prepare('SELECT * FROM supersedes ORDER BY first_ms IS NULL, first_ms, path, first_line').all() as Record<string, unknown>[]) {
|
|
281
|
+
const occurred = rowOccurred(r);
|
|
282
|
+
this.supersessions.push({
|
|
283
|
+
id: String(r.key), repo: str(r.repo), source: String(r.source), path: str(r.path), pattern: String(r.pattern), target: str(r.target),
|
|
284
|
+
replaced: str(r.replaced), replacement: str(r.replacement), syntax: str(r.syntax), text: String(r.text ?? ''),
|
|
285
|
+
obsoleteList: Number(r.obsolete_list) === 1, line: (r.current_line ?? r.first_line ?? null) as number | null, firstCommit: str(r.first_commit),
|
|
286
|
+
ms: r.first_ms === null || r.first_ms === undefined ? msOf(occurred.at) ?? 0 : Number(r.first_ms), occurred, current: Number(r.current) === 1,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* For every trunk commit, the commit of the trunk's first-parent line that brought it in: walking that line from its
|
|
293
|
+
* oldest commit, a merge brings in every commit its other parents reach that no earlier commit of the line reached.
|
|
294
|
+
*/
|
|
295
|
+
private computeIntroducedBy(): void {
|
|
296
|
+
for (const tip of this.trunkTip.values()) {
|
|
297
|
+
if (!tip || !this.commits.has(tip)) continue;
|
|
298
|
+
const line: string[] = [];
|
|
299
|
+
for (let h: string | undefined = tip; h && this.commits.has(h); h = this.commits.get(h)!.parents[0]) {
|
|
300
|
+
line.push(h);
|
|
301
|
+
if (line.length > 1_000_000) break;
|
|
302
|
+
}
|
|
303
|
+
line.reverse();
|
|
304
|
+
const seen = new Set<string>();
|
|
305
|
+
for (const fp of line) {
|
|
306
|
+
seen.add(fp);
|
|
307
|
+
this.introducedBy.set(fp, fp);
|
|
308
|
+
const stack = [...(this.commits.get(fp)?.parents.slice(1) ?? [])];
|
|
309
|
+
while (stack.length) {
|
|
310
|
+
const h = stack.pop()!;
|
|
311
|
+
if (seen.has(h) || !this.commits.has(h)) continue;
|
|
312
|
+
seen.add(h);
|
|
313
|
+
this.introducedBy.set(h, fp);
|
|
314
|
+
stack.push(...this.commits.get(h)!.parents);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Each branch's own commits: from its tip back to where it meets the trunk's first-parent line. */
|
|
321
|
+
private computeBranchCommits(): void {
|
|
322
|
+
const tips = new Map<string, BranchFact>();
|
|
323
|
+
for (const b of this.branches) {
|
|
324
|
+
const plain = b.namespace === 'remotes' ? b.name.replace(/^[^/]+\//, '') : b.name;
|
|
325
|
+
if (plain === 'HEAD') continue;
|
|
326
|
+
const tip = this.commits.get(b.tip);
|
|
327
|
+
if (!tip || tip.fpTrunk) continue; // the trunk itself, or a branch whose tip is on the trunk's line
|
|
328
|
+
if (!tips.has(`${b.repo}\x1f${plain}`)) tips.set(`${b.repo}\x1f${plain}`, { ...b, name: plain });
|
|
329
|
+
}
|
|
330
|
+
for (const b of tips.values()) {
|
|
331
|
+
const own = new Set<string>();
|
|
332
|
+
const stack = [b.tip];
|
|
333
|
+
while (stack.length) {
|
|
334
|
+
const h = stack.pop()!;
|
|
335
|
+
const c = this.commits.get(h);
|
|
336
|
+
if (!c || own.has(h) || c.fpTrunk) continue;
|
|
337
|
+
own.add(h);
|
|
338
|
+
stack.push(...c.parents);
|
|
339
|
+
if (own.size > 20_000) break;
|
|
340
|
+
}
|
|
341
|
+
this.branchCommits.set(b.name, own);
|
|
342
|
+
for (const h of own) this.branchesOf.set(h, [...(this.branchesOf.get(h) ?? []), b.name]);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Every place the given numbers appear (documents, commit messages, branch, worktree and file names), batched. */
|
|
347
|
+
numsOf(numbers: Iterable<string>): Map<string, NumFact[]> {
|
|
348
|
+
const list = [...numbers];
|
|
349
|
+
const want = [...new Set(list)].filter((n) => !this.numsCache.has(n));
|
|
350
|
+
for (const part of chunks(want, 300)) {
|
|
351
|
+
for (const n of part) this.numsCache.set(n, []);
|
|
352
|
+
const rows = this.ledger.db.prepare(`SELECT ${NUM_COLUMNS} FROM nums WHERE num IN (${part.map(() => '?').join(',')})`).all(...part) as Record<string, unknown>[];
|
|
353
|
+
for (const r of rows) {
|
|
354
|
+
const f = numFactOf(r);
|
|
355
|
+
this.numsCache.get(f.num)!.push(f);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const out = new Map<string, NumFact[]>();
|
|
359
|
+
for (const n of list) out.set(n, this.numsCache.get(n) ?? []);
|
|
360
|
+
return out;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* `numsOf`, and also every place a range names the number (CM, E151; ledger/ranges.ts): the place where the range's
|
|
365
|
+
* first number stands — a commit message, a line of a document — counts as naming each of its members, under the
|
|
366
|
+
* same ledger entry. `3431e93` 「(CKC-03 AC-25-AC-31)」 names AC-26 … AC-31 too. For the lookups of what later names an
|
|
367
|
+
* item; the ledger's own index is unchanged.
|
|
368
|
+
*/
|
|
369
|
+
numsWithRanges(numbers: Iterable<string>): Map<string, NumFact[]> {
|
|
370
|
+
const list = [...numbers];
|
|
371
|
+
const base = this.numsOf(list);
|
|
372
|
+
const out = new Map(base);
|
|
373
|
+
const placeKey = (r: NumFact) => `${r.kind}\x1f${r.commit ?? ''}\x1f${r.path ?? ''}\x1f${r.line ?? ''}`;
|
|
374
|
+
for (const n of new Set(list)) {
|
|
375
|
+
const prefix = familyPrefix(n);
|
|
376
|
+
if (!prefix) continue;
|
|
377
|
+
const have = new Set((base.get(n) ?? []).map(placeKey));
|
|
378
|
+
const extra: NumFact[] = [];
|
|
379
|
+
for (const x of this.rangeRowsOf(prefix)) {
|
|
380
|
+
if (!x.members.has(n) || have.has(placeKey(x.row))) continue;
|
|
381
|
+
have.add(placeKey(x.row));
|
|
382
|
+
extra.push({ ...x.row, num: n, place: 'mention' });
|
|
383
|
+
}
|
|
384
|
+
if (extra.length) out.set(n, [...(base.get(n) ?? []), ...extra]);
|
|
385
|
+
}
|
|
386
|
+
return out;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** The rows of a family whose context writes a range of it, with the members each names (cached per family). */
|
|
390
|
+
private rangeRowsOf(prefix: string): { row: NumFact; members: Set<string> }[] {
|
|
391
|
+
const hit = this.rangeCache.get(prefix);
|
|
392
|
+
if (hit) return hit;
|
|
393
|
+
const like = `${prefix.replace(/[\\%_]/g, (c) => `\\${c}`)}%`;
|
|
394
|
+
const rows = this.ledger.db.prepare(`SELECT ${NUM_COLUMNS} FROM nums WHERE num LIKE ? ESCAPE '\\'`).all(like) as Record<string, unknown>[];
|
|
395
|
+
const out: { row: NumFact; members: Set<string> }[] = [];
|
|
396
|
+
for (const r of rows) {
|
|
397
|
+
const f = numFactOf(r);
|
|
398
|
+
if (familyPrefix(f.num) !== prefix) continue;
|
|
399
|
+
const members = rangesIn(f.context).filter((x) => x.prefix === prefix && x.members.includes(f.num)).flatMap((x) => x.members);
|
|
400
|
+
if (members.length) out.push({ row: f, members: new Set(members) });
|
|
401
|
+
}
|
|
402
|
+
this.rangeCache.set(prefix, out);
|
|
403
|
+
return out;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** The files a commit changed (against its first parent). */
|
|
407
|
+
filesOf(c: CommitFact): { path: string; status: string }[] {
|
|
408
|
+
const key = `${c.repo}\x1f${c.hash}`;
|
|
409
|
+
if (!this.filesCache.has(key)) {
|
|
410
|
+
this.filesCache.set(key, this.ledger.db.prepare('SELECT path, status FROM commit_files WHERE repo = ? AND hash = ? ORDER BY path').all(c.repo, c.hash) as { path: string; status: string }[]);
|
|
411
|
+
}
|
|
412
|
+
return this.filesCache.get(key)!;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** The ledger's own label, time and text of an entry (cached). */
|
|
416
|
+
entry(id: string): { label: string; occurred: Occurred; text?: string } | null {
|
|
417
|
+
if (!this.entryCache.has(id)) {
|
|
418
|
+
const e = this.ledger.resolve(id);
|
|
419
|
+
this.entryCache.set(id, e ? { label: e.label, occurred: e.occurred, ...(e.text !== undefined ? { text: e.text } : {}) } : null);
|
|
420
|
+
}
|
|
421
|
+
return this.entryCache.get(id)!;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** Evidence as the workbench names it: a ledger entry with its label, time and (for one line) the original line. */
|
|
425
|
+
ref(id: string, line: string | null = null, fallback?: { label: string; occurred: Occurred }): EvidenceRef {
|
|
426
|
+
const e = this.entry(id);
|
|
427
|
+
return { kind: 'ledger', id, label: e?.label ?? fallback?.label ?? id, line: line ?? null, occurred: e?.occurred ?? fallback?.occurred ?? null };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** The commit a (short) hash names, when the ledger has exactly one. */
|
|
431
|
+
commitByPrefix(prefix: string): CommitFact | null {
|
|
432
|
+
const p = prefix.trim().toLowerCase();
|
|
433
|
+
if (!/^[0-9a-f]{7,40}$/.test(p)) return null;
|
|
434
|
+
const exact = this.commits.get(p);
|
|
435
|
+
if (exact) return exact;
|
|
436
|
+
let hit: CommitFact | null = null;
|
|
437
|
+
for (const c of this.commits.values()) {
|
|
438
|
+
if (!c.hash.startsWith(p)) continue;
|
|
439
|
+
if (hit && hit.hash !== c.hash) return null;
|
|
440
|
+
hit = c;
|
|
441
|
+
}
|
|
442
|
+
return hit;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* An absolute path as the ledger names it: its repository and the path inside it (forward slashes). A path written with
|
|
447
|
+
* a different spelling of the same directory (a Windows short name, a junction) is compared by its real path.
|
|
448
|
+
*/
|
|
449
|
+
relPathOf(absPath: string): { readonly repo: string; readonly path: string } | null {
|
|
450
|
+
const repos = this.ledger.repos();
|
|
451
|
+
const within = (root: string, p: string) => repos.length && isWithin(root, p);
|
|
452
|
+
const pick = (p: string, rootOf: (r: { path: string }) => string) => {
|
|
453
|
+
const r = repos.filter((x) => within(rootOf(x), p)).sort((x, y) => rootOf(y).length - rootOf(x).length)[0];
|
|
454
|
+
return r ? { repo: r.id, path: normalizePath(p).slice(normalizePath(rootOf(r)).length + 1).split(/[\\/]/).join('/') } : null;
|
|
455
|
+
};
|
|
456
|
+
return pick(absPath, (r) => r.path) ?? pick(this.real(absPath), (r) => this.real(r.path));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
private readonly realCache = new Map<string, string>();
|
|
460
|
+
private real(p: string): string {
|
|
461
|
+
if (!this.realCache.has(p)) {
|
|
462
|
+
let out = p;
|
|
463
|
+
try { out = realpathSync.native(p); } catch {
|
|
464
|
+
// A path that no longer exists: resolve its nearest existing parent and keep the rest.
|
|
465
|
+
const parts = normalizePath(p).split(/[\\/]/);
|
|
466
|
+
for (let i = parts.length - 1; i > 0; i--) {
|
|
467
|
+
try { out = [realpathSync.native(parts.slice(0, i).join('/')), ...parts.slice(i)].join('/'); break; } catch { /* go up */ }
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
this.realCache.set(p, out);
|
|
471
|
+
}
|
|
472
|
+
return this.realCache.get(p)!;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Whether the ledger ever saw a document at this path (any version, any repository). */
|
|
476
|
+
hasDocument(path: string): boolean {
|
|
477
|
+
const p = path.replace(/\\/g, '/');
|
|
478
|
+
if (this.docsByPath.has(p)) return true;
|
|
479
|
+
for (const k of this.docsByPath.keys()) if (p.endsWith(`/${k}`) || k.endsWith(`/${p}`)) return true;
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** The text of an entry (a document version, an arrangement version), as the ledger keeps it. */
|
|
484
|
+
textOf(id: string): string | null {
|
|
485
|
+
return this.entry(id)?.text ?? null;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plans of an earlier generation (Spec v3.0 §2.12 "上几代的计划", D85, D82; CKC-24 AC-18): which `Plan` objects a
|
|
3
|
+
* rolled-up band holds, and its plan documents as they stood — a document since deleted is read from the version before
|
|
4
|
+
* its deletion, one rewritten in place from the version it had when the generation ended.
|
|
5
|
+
*
|
|
6
|
+
* The generation names its plan documents itself (`Generation.planRefs`, written by orientation or the skeleton from
|
|
7
|
+
* what the material says); the program only locates them in the ledger's document history and reads them. Nothing here
|
|
8
|
+
* decides what a generation is.
|
|
9
|
+
*/
|
|
10
|
+
import type { Ledger } from '../ledger/index.ts';
|
|
11
|
+
import { msOf } from '../ledger/time.ts';
|
|
12
|
+
import type { EvidenceRef, Generation } from '../model/k-types.ts';
|
|
13
|
+
import type { ReferenceItem } from '../model/types.ts';
|
|
14
|
+
import type { GenerationPlanDocView, GenerationPlanTextView } from '../model/views-k.ts';
|
|
15
|
+
import type { ProjectStore } from '../store/project-store.ts';
|
|
16
|
+
import { isWithin, pathKey, relativeDisplay, samePath } from '../util/paths.ts';
|
|
17
|
+
import { plansOf } from './placement.ts';
|
|
18
|
+
|
|
19
|
+
const slash = (p: string) => p.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '');
|
|
20
|
+
|
|
21
|
+
/** Where a plan reference points in a repository: its path (repository-relative when the reference says so, else as the
|
|
22
|
+
* source anchors it) and the document version, when the reference names one (`doc:<path>@<commit>`). */
|
|
23
|
+
interface PlanPlace { readonly path: string; readonly absolute: boolean; readonly repo: string | null; readonly version: string | null }
|
|
24
|
+
|
|
25
|
+
function placeOf(store: ProjectStore, ref: EvidenceRef): PlanPlace | null {
|
|
26
|
+
if (ref.kind === 'file') return { path: slash(ref.id), absolute: false, repo: ref.repo ?? null, version: null };
|
|
27
|
+
if (ref.kind === 'ledger') {
|
|
28
|
+
const doc = /^doc:(.+)@([0-9a-f]{4,40})$/.exec(ref.id);
|
|
29
|
+
if (doc) return { path: slash(doc[1]!), absolute: false, repo: ref.repo ?? null, version: ref.id };
|
|
30
|
+
const file = /^(?:file|del):(.+?)(?:@[0-9a-f]{4,40})?$/.exec(ref.id);
|
|
31
|
+
if (file) return { path: slash(file[1]!), absolute: false, repo: ref.repo ?? null, version: null };
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (ref.kind === 'source') {
|
|
35
|
+
const a = store.sources.get(ref.id)?.anchor;
|
|
36
|
+
if (a?.kind === 'revision') return { path: slash(a.path), absolute: false, repo: a.repo, version: null };
|
|
37
|
+
if (a?.kind === 'file') return { path: slash(a.path), absolute: true, repo: null, version: null };
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A source is at a place: the same document, by repository-relative path or by the absolute path's tail. */
|
|
43
|
+
function sourceAt(store: ProjectStore, sourceId: string, place: PlanPlace): boolean {
|
|
44
|
+
const a = store.sources.get(sourceId)?.anchor;
|
|
45
|
+
if (!a || (a.kind !== 'file' && a.kind !== 'revision')) return false;
|
|
46
|
+
const p = slash(a.path);
|
|
47
|
+
if (place.absolute) return a.kind === 'file' && pathKey(p) === pathKey(place.path);
|
|
48
|
+
return pathKey(p) === pathKey(place.path) || pathKey(p).endsWith(`/${pathKey(place.path)}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The plan object's document is gone from the current version: every source of it is history only, or no longer there. */
|
|
52
|
+
function onlyInHistory(store: ProjectStore, plan: ReferenceItem): boolean {
|
|
53
|
+
const sources = plan.sourceIds.flatMap((id) => store.sources.get(id) ?? []);
|
|
54
|
+
return sources.length > 0 && sources.every((s) => s.anchor.kind === 'revision' || s.availability === 'No longer available');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A generation's plan objects (graph node ids): the `Plan` items read from its plan documents that are no longer the
|
|
59
|
+
* current plan (not in force, or read from a document gone from the current version), and the plans its work items were
|
|
60
|
+
* planned in that are no longer in force. A plan item still in force and read from a document still there is the
|
|
61
|
+
* current plan's, even when an earlier generation's plan was once in that same document.
|
|
62
|
+
*/
|
|
63
|
+
export function generationPlanIds(store: ProjectStore, g: Generation): string[] {
|
|
64
|
+
const places = g.planRefs.flatMap((ref) => placeOf(store, ref) ?? []);
|
|
65
|
+
const sources = new Set(g.planRefs.filter((r) => r.kind === 'source').map((r) => r.id));
|
|
66
|
+
const earlier = (plan: ReferenceItem) => plan.validity !== 'Current' || onlyInHistory(store, plan);
|
|
67
|
+
const ids: string[] = [];
|
|
68
|
+
const add = (id: string) => { if (!ids.includes(id)) ids.push(id); };
|
|
69
|
+
for (const plan of store.reference.filter((r) => r.category === 'Plan')) {
|
|
70
|
+
const named = plan.sourceIds.some((sid) => sources.has(sid) || places.some((p) => sourceAt(store, sid, p)));
|
|
71
|
+
if (named && earlier(plan)) add(plan.id);
|
|
72
|
+
}
|
|
73
|
+
for (const workId of g.workIds) {
|
|
74
|
+
const thread = store.threads.get(workId);
|
|
75
|
+
if (thread) for (const plan of plansOf(store, thread)) if (earlier(plan)) add(plan.id);
|
|
76
|
+
}
|
|
77
|
+
return ids;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** A generation's plan documents as its band lists them, in the order the generation names them. */
|
|
81
|
+
export function generationPlanDocs(store: ProjectStore, g: Generation): GenerationPlanDocView[] {
|
|
82
|
+
return g.planRefs.map((ref, index) => {
|
|
83
|
+
const place = placeOf(store, ref);
|
|
84
|
+
return { index, label: ref.label, path: place ? place.path : null, ref };
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The end of a generation in milliseconds: a date alone ends at the end of that day. */
|
|
89
|
+
const endMs = (at: string): number => (/^\d{4}-\d\d-\d\d$/.test(at) ? Date.parse(`${at}T23:59:59.999Z`) : msOf(at) ?? Date.parse(at));
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* One plan document of an earlier generation as it stood (D82): the version the generation's reference names; else the
|
|
93
|
+
* last version the ledger has of it up to the generation's end — for a document since deleted that is the version before
|
|
94
|
+
* its deletion — else its first version. `fromLine` pages a long document (the ledger's own paging).
|
|
95
|
+
*/
|
|
96
|
+
export function generationPlanText(ledger: Ledger, store: ProjectStore, g: Generation, index: number, fromLine = 1): GenerationPlanTextView | null {
|
|
97
|
+
const ref = g.planRefs[index];
|
|
98
|
+
if (!ref) return null;
|
|
99
|
+
const base = { generationId: g.id, index, label: ref.label };
|
|
100
|
+
const none = (why: string, path: string | null = null, repo: string | null = null): GenerationPlanTextView => ({
|
|
101
|
+
...base, repo, path, version: null, deleted: null, current: false, text: null, lines: 0, fromLine: 1, nextFromLine: null, truncated: false, why,
|
|
102
|
+
});
|
|
103
|
+
const place = placeOf(store, ref);
|
|
104
|
+
if (!place) return none(`${ref.label} names no document (${ref.kind} ${ref.id}); it is shown as the generation cites it.`);
|
|
105
|
+
// An absolute path (a file source) is made relative to the ledger repository it lies in.
|
|
106
|
+
let path = place.path;
|
|
107
|
+
let repoId = place.repo ? ledger.repoId(place.repo) : null;
|
|
108
|
+
if (place.absolute) {
|
|
109
|
+
// Under a repository as the system's own paths are: asked as text with `/` after a key, nothing is under anything
|
|
110
|
+
// on Windows, where a key has `\`.
|
|
111
|
+
const repo = ledger.repos().filter((r) => isWithin(r.path, path) && !samePath(r.path, path)).sort((a, b) => b.path.length - a.path.length)[0];
|
|
112
|
+
if (!repo) return none(`${path} is in no repository of the ledger.`, path);
|
|
113
|
+
repoId = repo.id;
|
|
114
|
+
path = relativeDisplay(repo.path, path);
|
|
115
|
+
}
|
|
116
|
+
const repoPath = repoId ? ledger.repos().find((r) => r.id === repoId)?.path ?? null : null;
|
|
117
|
+
let versionId = place.version;
|
|
118
|
+
let deleted: GenerationPlanTextView['deleted'] = null;
|
|
119
|
+
let current = false;
|
|
120
|
+
if (!versionId) {
|
|
121
|
+
const v = ledger.docVersions(path, repoId ? { repo: repoId } : {});
|
|
122
|
+
if (typeof v === 'string') return none(v, path, repoPath);
|
|
123
|
+
const end = endMs(g.ended.at);
|
|
124
|
+
const upToEnd = v.versions.filter((x) => (msOf(x.occurred.at) ?? 0) <= end);
|
|
125
|
+
const pick = upToEnd[upToEnd.length - 1] ?? v.versions[0];
|
|
126
|
+
if (!pick) return none(`The ledger has no version of ${path}.`, path, repoPath);
|
|
127
|
+
versionId = pick.id;
|
|
128
|
+
const gone = v.inCurrentVersion ? undefined : v.deletions[v.deletions.length - 1];
|
|
129
|
+
if (gone?.commit) deleted = { commit: gone.commit, occurred: gone.occurred };
|
|
130
|
+
}
|
|
131
|
+
const page = ledger.docText({ id: versionId }, { from: fromLine });
|
|
132
|
+
if (typeof page === 'string') return none(page, path, repoPath);
|
|
133
|
+
current = page.current;
|
|
134
|
+
return {
|
|
135
|
+
...base, repo: repoPath ?? page.repo, path: page.path, version: { commit: page.commit, occurred: page.occurred }, deleted, current,
|
|
136
|
+
text: page.text, lines: page.lines, fromLine: page.fromLine, nextFromLine: page.nextFromLine ?? null, truncated: page.nextFromLine !== undefined || page.fromLine > 1, why: null,
|
|
137
|
+
};
|
|
138
|
+
}
|