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,535 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Update pending` by the clerk method (Spec v3.0 §1.11, §7.1, §7.6, §9; CKC-07 AC-10): an entry — a work item, a product
|
|
3
|
+
* reference item, an area, a fact record kept from before increment K — shows `Update pending` while a change that affects
|
|
4
|
+
* it waits for the next round, and it names each such change and how the change reaches it.
|
|
5
|
+
*
|
|
6
|
+
* What waits is the coverage's own list (clerk-coverage.ts `clerkPending`): what was read since the latest round started,
|
|
7
|
+
* less what the project's rules settle, what is history or reference only, what the scope does not read and the Keeper's
|
|
8
|
+
* own commits of its project folder. The marks are computed from that list on every pass that writes the coverage, never
|
|
9
|
+
* accumulated, so they agree with it: every change a mark names is on the list, and once a round starts — its start is the
|
|
10
|
+
* list's watermark — what it took in leaves the list, and every mark that waited for it is gone at the next pass.
|
|
11
|
+
*
|
|
12
|
+
* How a change reaches an entry. The clerk method's rounds write no fact records (D88), so the old chain — material →
|
|
13
|
+
* fact records → work items → areas — carries nothing for them. These are the links the clerk method's assets hold:
|
|
14
|
+
* - Number: the change names the entry's number — the project's own (`ids`) or the Keeper's (`KeeperNumber`, and the
|
|
15
|
+
* project's number it later took) — where the ledger indexes numbers: a document's text (in the sections the change
|
|
16
|
+
* added or changed, or in what a section it changed or removed said before), a commit's message and the paths it
|
|
17
|
+
* changed, a file's path, the worktree and branch a session ran in (a worktree named after the work); and, beyond what
|
|
18
|
+
* the ledger indexes, a session's messages. A pending version is not in the ledger yet — the ledger is brought up to
|
|
19
|
+
* date by the round that takes the change in — so its text is read as intake read it, and numbers are recognised the
|
|
20
|
+
* ledger's way (ledger/numbering.ts `mentionMatcher`, `nameMatcher`): its families, its word boundaries, its caution
|
|
21
|
+
* about two capitals that are also ordinary words. Code is read for its path only: the ledger indexes no number in it.
|
|
22
|
+
* - Cited source: the entry cites a part of the material that changed — a work item's statements, what it was written
|
|
23
|
+
* from, the fact records it rests on; a reference item's own sources and the evidence of its carry-out; a relation that
|
|
24
|
+
* ties the entry to a source or rests on one; the clue of a mark on it.
|
|
25
|
+
* - Code territory: the change is in a code file of a territory the work item changed — its trunk commits, counted the
|
|
26
|
+
* way `Built by` counts them (codemap `workTerritories`).
|
|
27
|
+
* - Process: a step of the work item's process (a confirmed or judged link to a dispatch, receipt, report, session), one
|
|
28
|
+
* of its breakpoints or one of its open send-backs rests on the material.
|
|
29
|
+
* - Recorded change: a change the Change log recorded from this material reached the entry (the item's `affects`, or the
|
|
30
|
+
* record's own when its items name no sources): the material has changed it before.
|
|
31
|
+
* - Contributing work: an area waits for what the work items that contribute to its understanding wait for.
|
|
32
|
+
*
|
|
33
|
+
* Which parts of a document changed. A changed file is read again whole, so every section of it reads as new. When the
|
|
34
|
+
* ledger holds the version the latest round took in, the new version is compared with it section by section (the
|
|
35
|
+
* ledger's own `diffSections`): a section the change did not touch reaches nothing through its text or its citations; a
|
|
36
|
+
* section it removed still reaches what cited it. Before the first round, without that version, or when the comparison
|
|
37
|
+
* finds no section that differs (the file was read again, so it did change: an edit the round took in and then undone,
|
|
38
|
+
* a file that came back, blank lines or line endings), which part changed is not known and every section counts.
|
|
39
|
+
*
|
|
40
|
+
* Left out: notes (a note is not marked, §1.11 — its re-look updates it); earlier generations' plan documents and
|
|
41
|
+
* semantic patches' anchors (history, and a patch's objects are reached through their own links); a reference item's
|
|
42
|
+
* downstream work (propagation judges that, §5.5). A change only the watcher has seen and intake has not read yet — a
|
|
43
|
+
* file still being written, a session still going — reaches nothing until it is read: what it touches is known only from
|
|
44
|
+
* its content. It is on the coverage's list meanwhile.
|
|
45
|
+
*/
|
|
46
|
+
import { join, relative } from 'node:path';
|
|
47
|
+
import type { App } from '../../server/app.ts';
|
|
48
|
+
import type { PendingLink, PendingMaterial, PendingWait, Project, Source } from '../../model/types.ts';
|
|
49
|
+
import type { EvidenceRef } from '../../model/k-types.ts';
|
|
50
|
+
import type { WorkTerritoryView } from '../../model/views-k.ts';
|
|
51
|
+
import type { ProjectStore } from '../../store/project-store.ts';
|
|
52
|
+
import type { Ledger } from '../../ledger/index.ts';
|
|
53
|
+
import { diffSections, keyedSections, sectionBase } from '../../ledger/docs.ts';
|
|
54
|
+
import { familyOf, mentionMatcher, nameMatcher, type Rule } from '../../ledger/numbering.ts';
|
|
55
|
+
import { within } from '../../codemap/facts.ts';
|
|
56
|
+
import { isDocumentPath } from '../../scope/skip.ts';
|
|
57
|
+
import { canonicalPath, isWithin, normalizePath, pathKey, relativeDisplay, samePath } from '../../util/paths.ts';
|
|
58
|
+
import { clerkPending, takenInAt, type ClerkPending } from './clerk-coverage.ts';
|
|
59
|
+
import { projectRelPath } from './materials.ts';
|
|
60
|
+
|
|
61
|
+
export interface UpdatePendingInput {
|
|
62
|
+
readonly store: ProjectStore;
|
|
63
|
+
readonly project: Project;
|
|
64
|
+
/** What waits for the next round, with the sources behind each entry (`clerkPending`). */
|
|
65
|
+
readonly pending: ClerkPending;
|
|
66
|
+
/** The list's watermark: when the latest round started; '' before the first round. */
|
|
67
|
+
readonly takenInAt: string;
|
|
68
|
+
/** The project's ledger (read-only), for the version the latest round took in, commits and worktrees; null without one. */
|
|
69
|
+
readonly ledger: Ledger | null;
|
|
70
|
+
/** The territories each work item changed (codemap `workTerritories`); called only when a change is in code. */
|
|
71
|
+
readonly workTerritories?: (() => Readonly<Record<string, readonly WorkTerritoryView[]>> | null) | null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The changes each entry waits for, by collection and id (areas by their area's reference id). */
|
|
75
|
+
export interface UpdatePendingMarks {
|
|
76
|
+
readonly threads: ReadonlyMap<string, readonly PendingWait[]>;
|
|
77
|
+
readonly references: ReadonlyMap<string, readonly PendingWait[]>;
|
|
78
|
+
readonly areas: ReadonlyMap<string, readonly PendingWait[]>;
|
|
79
|
+
readonly facts: ReadonlyMap<string, readonly PendingWait[]>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
type ObjectKey = string; // `thread:<id>` · `ref:<id>` · `fact:<id>`
|
|
83
|
+
interface Holder { readonly key: ObjectKey; readonly link: PendingLink; readonly how: string }
|
|
84
|
+
|
|
85
|
+
/** One change as the marks read it: the coverage's entry and what it is made of. */
|
|
86
|
+
interface ChangeView {
|
|
87
|
+
readonly entry: PendingMaterial;
|
|
88
|
+
readonly fresh: readonly Source[];
|
|
89
|
+
/** The fresh sources whose content changed: all of them unless a section diff said which. */
|
|
90
|
+
readonly changed: readonly Source[];
|
|
91
|
+
/** A section diff against the version the latest round took in said which parts changed. */
|
|
92
|
+
readonly precise: boolean;
|
|
93
|
+
/** The sections the change removed, by their base key (`sectionBase`), when precise. */
|
|
94
|
+
readonly removed: ReadonlySet<string>;
|
|
95
|
+
/** A file's path on disk. */
|
|
96
|
+
readonly path: string | null;
|
|
97
|
+
/** Where numbers are read: new text, and what changed or removed sections said before (`gone`). */
|
|
98
|
+
readonly texts: readonly { readonly text: string; readonly where: string; readonly sourceIds: readonly string[]; readonly gone?: 'removed' | 'changed'; readonly now?: string }[];
|
|
99
|
+
/** Names numbers are read in: a path, a commit's files, a session's working directory and branch. */
|
|
100
|
+
readonly names: readonly { readonly name: string; readonly where: string }[];
|
|
101
|
+
/** The files on disk the change is in: a file itself; a commit's changed files. */
|
|
102
|
+
readonly files: readonly string[];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const THREAD = (id: string): ObjectKey => `thread:${id}`;
|
|
106
|
+
const REF = (id: string): ObjectKey => `ref:${id}`;
|
|
107
|
+
const FACT = (id: string): ObjectKey => `fact:${id}`;
|
|
108
|
+
const push = <K, V>(map: Map<K, V[]>, key: K, value: V) => { const list = map.get(key); if (list) list.push(value); else map.set(key, [value]); };
|
|
109
|
+
const escapeRe = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
110
|
+
const slash = (p: string) => p.split('\\').join('/');
|
|
111
|
+
/** A split long section's source ends its heading path in `part n` (sources/files.ts `splitLong`). */
|
|
112
|
+
const baseOf = (headingPath: readonly string[]) => sectionBase(headingPath.length && /^part \d+$/.test(headingPath[headingPath.length - 1]!) ? headingPath.slice(0, -1) : headingPath);
|
|
113
|
+
const noDuplicateMark = (key: string) => key.replace(/ #\d+$/, '');
|
|
114
|
+
|
|
115
|
+
/** A part of the material as a reader finds it: `docs/PLAN.md › W-2 Count by ear`, a session's messages, a commit. */
|
|
116
|
+
function partLabel(project: Project, s: Source): string {
|
|
117
|
+
const a = s.anchor;
|
|
118
|
+
if (a.kind === 'file') return `${slash(projectRelPath(project, a.path))}${a.headingPath.length ? ` › ${a.headingPath.join(' › ')}` : ''}`;
|
|
119
|
+
if (a.kind === 'session') return `${a.host} session ${a.sessionId.slice(0, 8)} · messages ${a.messageStart}–${a.messageEnd}`;
|
|
120
|
+
if (a.kind === 'commit') return `commit ${a.commit.slice(0, 8)}`;
|
|
121
|
+
return s.title;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The document as intake read it, from its sections, each at its own lines (sections cover the file from the first heading on). */
|
|
125
|
+
function textOfSections(fresh: readonly Source[]): string {
|
|
126
|
+
const lines: string[] = [];
|
|
127
|
+
for (const s of [...fresh].sort((a, b) => (a.anchor.kind === 'file' ? a.anchor.lineStart : 0) - (b.anchor.kind === 'file' ? b.anchor.lineStart : 0))) {
|
|
128
|
+
if (s.anchor.kind !== 'file') continue;
|
|
129
|
+
const at = Math.max(0, s.anchor.lineStart - 1);
|
|
130
|
+
while (lines.length < at) lines.push('');
|
|
131
|
+
s.excerpt.split('\n').forEach((l, i) => { lines[at + i] = l; });
|
|
132
|
+
}
|
|
133
|
+
return lines.join('\n');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ───────────────────────── numbers, recognised the ledger's way ─────────────────────────
|
|
137
|
+
|
|
138
|
+
interface NumberIndex {
|
|
139
|
+
/** The numbers an entry is known by, as the project writes them, and the entries each names. */
|
|
140
|
+
readonly holders: ReadonlyMap<string, readonly ObjectKey[]>;
|
|
141
|
+
inText(text: string): string[];
|
|
142
|
+
inName(name: string): string[];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function numberIndex(store: ProjectStore): NumberIndex | null {
|
|
146
|
+
const holders = new Map<string, ObjectKey[]>();
|
|
147
|
+
const add = (num: string | null | undefined, key: ObjectKey) => {
|
|
148
|
+
const n = (num ?? '').trim();
|
|
149
|
+
if (!n || /^v\d/i.test(n)) return; // a version label is not a number of the work (tools.ts reads it the same way)
|
|
150
|
+
const written = familyOf(n.toUpperCase()) ? n.toUpperCase() : n;
|
|
151
|
+
const list = holders.get(written) ?? [];
|
|
152
|
+
if (!list.includes(key)) list.push(key);
|
|
153
|
+
holders.set(written, list);
|
|
154
|
+
};
|
|
155
|
+
for (const t of store.threads.all()) if (t.validity !== 'Removed') for (const id of t.ids ?? []) add(id, THREAD(t.id));
|
|
156
|
+
for (const r of store.reference.all()) if (r.validity !== 'Removed') for (const id of r.ids ?? []) add(id, REF(r.id));
|
|
157
|
+
for (const n of store.numbers.all()) {
|
|
158
|
+
const key = store.threads.has(n.objectId) ? THREAD(n.objectId) : store.reference.has(n.objectId) ? REF(n.objectId) : null;
|
|
159
|
+
if (!key) continue;
|
|
160
|
+
add(n.number, key);
|
|
161
|
+
add(n.projectNumber, key);
|
|
162
|
+
}
|
|
163
|
+
if (holders.size === 0) return null;
|
|
164
|
+
// The families of the numbers entries are known by, as the ledger defines a family (two capitals: only the ones named).
|
|
165
|
+
const byFamily = new Map<string, { shape: Rule['shape']; defined: Set<string> }>();
|
|
166
|
+
const literal: string[] = [];
|
|
167
|
+
for (const num of holders.keys()) {
|
|
168
|
+
const f = familyOf(num);
|
|
169
|
+
if (!f) { literal.push(num); continue; }
|
|
170
|
+
const r = byFamily.get(f.family) ?? { shape: f.shape, defined: new Set<string>() };
|
|
171
|
+
if (f.shape === 'two-letters') r.defined.add(num);
|
|
172
|
+
byFamily.set(f.family, r);
|
|
173
|
+
}
|
|
174
|
+
const rules: Rule[] = [...byFamily].map(([family, r]) => ({ family, shape: r.shape, defined: r.defined }));
|
|
175
|
+
const mentions = mentionMatcher(rules);
|
|
176
|
+
const names = rules.length ? nameMatcher(rules) : null;
|
|
177
|
+
const literalRe = literal.length ? new RegExp(`(?<![\\p{L}\\p{N}_-])(${literal.sort((a, b) => b.length - a.length).map(escapeRe).join('|')})(?![\\p{L}\\p{N}_])`, 'gu') : null;
|
|
178
|
+
const known = (nums: Iterable<string>) => [...new Set([...nums].filter((n) => holders.has(n)))];
|
|
179
|
+
const literals = (text: string) => (literalRe ? [...text.matchAll(literalRe)].map((m) => m[1]!) : []);
|
|
180
|
+
return {
|
|
181
|
+
holders,
|
|
182
|
+
inText: (text) => known([...(mentions ? mentions(text).filter((m) => m.confidence === 'stated').map((m) => m.num) : []), ...literals(text)]),
|
|
183
|
+
inName: (name) => known([...(names ? names(name) : []), ...literals(name)]),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ───────────────────────── what each change is ─────────────────────────
|
|
188
|
+
|
|
189
|
+
function fileView(input: UpdatePendingInput, entry: PendingMaterial, fresh: readonly Source[]): ChangeView {
|
|
190
|
+
const { project, ledger } = input;
|
|
191
|
+
const path = entry.ref;
|
|
192
|
+
const rel = slash(projectRelPath(project, path));
|
|
193
|
+
const document = isDocumentPath(path);
|
|
194
|
+
let changed: readonly Source[] = fresh;
|
|
195
|
+
let precise = false;
|
|
196
|
+
const removed = new Set<string>();
|
|
197
|
+
const was: { text: string; where: string; sourceIds: string[]; gone: 'removed' | 'changed'; now?: string }[] = [];
|
|
198
|
+
const before = /\.(md|markdown)$/i.test(path) && input.takenInAt && ledger ? ledger.currentText(path) : null;
|
|
199
|
+
const after = before !== null ? textOfSections(fresh) : '';
|
|
200
|
+
const diff = before !== null ? diffSections(before, after) : null;
|
|
201
|
+
// What the latest round took in against what intake read since: which sections were added, changed or removed. A
|
|
202
|
+
// comparison that finds none cannot say which part changed. The file was read again after the round started because
|
|
203
|
+
// its content differed from what intake last read, so the difference lies outside what the ledger's version shows: an
|
|
204
|
+
// edit the round took in and that was then undone (the committed version is what the ledger holds), a file that came
|
|
205
|
+
// back, a change of blank lines or line endings. Every section then counts, as with no version to compare with.
|
|
206
|
+
if (before !== null && diff && (diff.added.length || diff.changed.length || diff.removed.length)) {
|
|
207
|
+
const touched = new Set([...diff.added, ...diff.changed]);
|
|
208
|
+
const sections = keyedSections(after);
|
|
209
|
+
const keyOf = (s: Source) => (s.anchor.kind === 'file' ? sections.find((k) => s.anchor.kind === 'file' && s.anchor.lineStart >= k.lineStart && s.anchor.lineStart <= k.lineEnd)?.key ?? null : null);
|
|
210
|
+
changed = fresh.filter((s) => { const k = keyOf(s); return k === null || touched.has(k); });
|
|
211
|
+
precise = true;
|
|
212
|
+
for (const k of diff.removed) removed.add(noDuplicateMark(k));
|
|
213
|
+
if (document) {
|
|
214
|
+
const newText = new Map(sections.map((k) => [k.key, k.text]));
|
|
215
|
+
for (const k of keyedSections(before)) {
|
|
216
|
+
if (diff.removed.includes(k.key)) was.push({ text: k.text, where: `${rel} › ${k.base}`, sourceIds: [], gone: 'removed' });
|
|
217
|
+
else if (diff.changed.includes(k.key)) was.push({ text: k.text, where: `${rel} › ${k.base}`, sourceIds: [], gone: 'changed', now: newText.get(k.key) ?? '' });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// A document's text is where the ledger reads numbers; code is read for its path only (the ledger indexes no numbers in it).
|
|
222
|
+
const texts = document ? [...changed.map((s) => ({ text: s.excerpt, where: partLabel(project, s), sourceIds: [s.id] })), ...was] : [];
|
|
223
|
+
return { entry, fresh, changed, precise, removed, path, texts, names: [{ name: rel, where: `its path ${rel}` }], files: [path] };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function commitView(input: UpdatePendingInput, entry: PendingMaterial, fresh: readonly Source[]): ChangeView {
|
|
227
|
+
const texts: { text: string; where: string; sourceIds: string[] }[] = [];
|
|
228
|
+
const names: { name: string; where: string }[] = [];
|
|
229
|
+
const files: string[] = [];
|
|
230
|
+
for (const s of fresh) {
|
|
231
|
+
if (s.anchor.kind !== 'commit') continue;
|
|
232
|
+
const lines = s.excerpt.split('\n');
|
|
233
|
+
const short = s.anchor.commit.slice(0, 8);
|
|
234
|
+
// The message as the ledger keeps it (subject and body) when it has read the commit; else the subject intake read.
|
|
235
|
+
const message = input.ledger?.resolve(`commit:${s.anchor.commit.slice(0, 12)}`)?.text ?? lines[0] ?? '';
|
|
236
|
+
texts.push({ text: message, where: `the message of commit ${short}`, sourceIds: [s.id] });
|
|
237
|
+
const listed = lines.indexOf('files:');
|
|
238
|
+
for (const f of listed >= 0 ? lines.slice(listed + 1).map((l) => l.trim()).filter(Boolean) : []) {
|
|
239
|
+
names.push({ name: f, where: `${f}, a file commit ${short} changed` });
|
|
240
|
+
files.push(join(s.anchor.repo, ...f.split('/')));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return { entry, fresh, changed: fresh, precise: true, removed: new Set(), path: null, texts, names, files };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function sessionView(input: UpdatePendingInput, entry: PendingMaterial, fresh: readonly Source[]): ChangeView {
|
|
247
|
+
const { project, ledger } = input;
|
|
248
|
+
const texts = fresh.map((s) => ({ text: s.excerpt, where: partLabel(project, s), sourceIds: [s.id] }));
|
|
249
|
+
const names: { name: string; where: string }[] = [];
|
|
250
|
+
const recorded = fresh.map((s) => (s.anchor.kind === 'session' ? s.anchor.cwd : null)).find((c): c is string => Boolean(c)) ?? null;
|
|
251
|
+
// The log records its directory in the spelling the agent ran under (through a junction, say): it is told from the
|
|
252
|
+
// project's locations as the file system spells both, or the project's own directory would pass for a worktree.
|
|
253
|
+
const cwd = recorded ? canonicalPath(recorded) : null;
|
|
254
|
+
const locations = project.locations.map(canonicalPath);
|
|
255
|
+
if (cwd && !locations.some((l) => samePath(l, cwd))) {
|
|
256
|
+
// A session in a worktree named after the work (`.worktrees/AB-byok`), on a branch named after it (`wip/AB-byok`).
|
|
257
|
+
const home = locations.filter((l) => isWithin(l, cwd)).sort((a, b) => b.length - a.length)[0];
|
|
258
|
+
const dir = home ? slash(relative(normalizePath(home), normalizePath(cwd))) : slash(normalizePath(cwd)).split('/').slice(-2).join('/');
|
|
259
|
+
names.push({ name: dir, where: `its working directory ${dir}` });
|
|
260
|
+
const branch = ledger?.worktreeBranch(cwd) ?? null;
|
|
261
|
+
if (branch) names.push({ name: branch, where: `the branch ${branch} checked out there` });
|
|
262
|
+
}
|
|
263
|
+
return { entry, fresh, changed: fresh, precise: true, removed: new Set(), path: null, texts, names, files: [] };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ───────────────────────── the marks ─────────────────────────
|
|
267
|
+
|
|
268
|
+
/** The changes each entry waits for, computed from the pending list (see the module comment). */
|
|
269
|
+
export function updatePendingMarks(input: UpdatePendingInput): UpdatePendingMarks {
|
|
270
|
+
const { store, project, pending, ledger } = input;
|
|
271
|
+
const waits = new Map<ObjectKey, Map<string, { entry: PendingMaterial; sourceIds: Set<string>; reasons: Map<string, { link: PendingLink; detail: string }> }>>();
|
|
272
|
+
const reach = (key: ObjectKey, entry: PendingMaterial, link: PendingLink, detail: string, sourceIds: readonly string[]) => {
|
|
273
|
+
const byRef = waits.get(key) ?? new Map();
|
|
274
|
+
const w = byRef.get(entry.ref) ?? { entry, sourceIds: new Set<string>(), reasons: new Map() };
|
|
275
|
+
for (const id of sourceIds) w.sourceIds.add(id);
|
|
276
|
+
w.reasons.set(`${link}|${detail}`, { link, detail });
|
|
277
|
+
byRef.set(entry.ref, w);
|
|
278
|
+
waits.set(key, byRef);
|
|
279
|
+
};
|
|
280
|
+
const live = (key: ObjectKey) => {
|
|
281
|
+
const [kind, id] = [key.slice(0, key.indexOf(':')), key.slice(key.indexOf(':') + 1)];
|
|
282
|
+
if (kind === 'thread') return (store.threads.get(id)?.validity ?? 'Removed') !== 'Removed';
|
|
283
|
+
if (kind === 'ref') return (store.reference.get(id)?.validity ?? 'Removed') !== 'Removed';
|
|
284
|
+
return store.facts.has(id);
|
|
285
|
+
};
|
|
286
|
+
const entries = new Map(pending.pending.map((p) => [p.ref, p]));
|
|
287
|
+
const changes: ChangeView[] = [];
|
|
288
|
+
for (const [ref, fresh] of pending.fresh) {
|
|
289
|
+
const entry = entries.get(ref);
|
|
290
|
+
if (!entry || fresh.length === 0) continue;
|
|
291
|
+
changes.push(entry.kind === 'commit' ? commitView(input, entry, fresh) : entry.kind === 'session' ? sessionView(input, entry, fresh) : fileView(input, entry, fresh));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (changes.length) {
|
|
295
|
+
// ── what holds each source: citations, recorded changes, process evidence given as a source ──
|
|
296
|
+
const holdersOf = new Map<string, Holder[]>();
|
|
297
|
+
const cite = (sourceId: string, key: ObjectKey, link: PendingLink, how: string) => push(holdersOf, sourceId, { key, link, how });
|
|
298
|
+
for (const t of store.threads.all()) {
|
|
299
|
+
const key = THREAD(t.id);
|
|
300
|
+
for (const s of [...(t.executionFacts ?? []), ...(t.qcFacts ?? [])]) for (const id of s.sourceIds ?? []) cite(id, key, 'Cited source', `a ${s.type} statement of it cites`);
|
|
301
|
+
for (const id of t.inputs?.sourceIds ?? []) cite(id, key, 'Cited source', 'it was written from');
|
|
302
|
+
for (const f of t.factRecordIds ?? []) for (const id of store.facts.get(f)?.aboutSourceIds ?? []) cite(id, key, 'Cited source', `the fact record ${f} it rests on is about`);
|
|
303
|
+
}
|
|
304
|
+
for (const r of store.reference.all()) {
|
|
305
|
+
const key = REF(r.id);
|
|
306
|
+
for (const id of r.sourceIds ?? []) cite(id, key, 'Cited source', 'it rests on');
|
|
307
|
+
for (const id of r.inputs?.sourceIds ?? []) cite(id, key, 'Cited source', 'it was written from');
|
|
308
|
+
for (const id of r.carryOut?.evidenceSourceIds ?? []) cite(id, key, 'Cited source', 'the evidence of its carry-out is');
|
|
309
|
+
}
|
|
310
|
+
for (const f of store.facts.all()) for (const id of f.aboutSourceIds ?? []) cite(id, FACT(f.id), 'Cited source', 'it is about');
|
|
311
|
+
const objectKey = (id: string): ObjectKey | null => (store.threads.has(id) ? THREAD(id) : store.reference.has(id) ? REF(id) : null);
|
|
312
|
+
for (const rel of store.relations.all()) {
|
|
313
|
+
if (rel.type === 'affects') continue; // a change record's own relation: its sources come in as a recorded change
|
|
314
|
+
for (const [end, other] of [[rel.from, rel.to], [rel.to, rel.from]] as const) {
|
|
315
|
+
const key = objectKey(end);
|
|
316
|
+
if (!key) continue;
|
|
317
|
+
if (store.sources.has(other)) cite(other, key, 'Cited source', `the relation “${rel.type}” ties it to`);
|
|
318
|
+
for (const id of rel.evidence?.sourceIds ?? []) cite(id, key, 'Cited source', `the relation “${rel.type}” rests on`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
for (const m of store.marks.all()) {
|
|
322
|
+
const key = m.closed ? null : objectKey(m.targetId);
|
|
323
|
+
if (key) for (const id of m.clueSourceIds ?? []) cite(id, key, 'Cited source', `the clue of its mark ${m.kind} is`);
|
|
324
|
+
}
|
|
325
|
+
for (const c of store.changes.all()) {
|
|
326
|
+
const items = (c.items ?? []).filter((it) => (it.sourceIds ?? []).length);
|
|
327
|
+
const recorded = items.length ? items.map((it) => ({ sources: it.sourceIds, affects: it.affects ?? [], title: it.title })) : [{ sources: c.sourceIds ?? [], affects: c.affects ?? [], title: c.title }];
|
|
328
|
+
for (const r of recorded) for (const target of r.affects) {
|
|
329
|
+
const key = objectKey(target);
|
|
330
|
+
if (key) for (const id of r.sources) cite(id, key, 'Recorded change', `the change “${r.title}” (${c.id}) that reached it came from`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ── what a work item's process rests on: its steps, breakpoints and open send-backs (a plan's too) ──
|
|
335
|
+
const byPlace = new Map<string, Holder[]>(); // pathKey of a file or a session log
|
|
336
|
+
const roots = [...new Set([...(ledger?.repos().map((r) => r.path) ?? []), ...project.scope.filter((i) => !i.missing && i.category !== 'Session source').map((i) => i.path), ...project.locations])];
|
|
337
|
+
const evidencePlaces = (e: EvidenceRef): string[] => {
|
|
338
|
+
if (e.kind === 'file') {
|
|
339
|
+
const rel = e.id.replace(/^file:/, '');
|
|
340
|
+
const own = e.repo ? ledger?.repos().find((x) => x.id === e.repo)?.path ?? roots.find((r) => samePath(r, e.repo!)) : undefined;
|
|
341
|
+
return (own ? [own] : roots).map((r) => join(r, ...rel.split('/')));
|
|
342
|
+
}
|
|
343
|
+
if (e.kind !== 'ledger') return [];
|
|
344
|
+
const place = ledger?.entryPlace(e.id);
|
|
345
|
+
if (place) return ['file' in place ? place.file : place.sessionFile];
|
|
346
|
+
// Without the ledger, what the id itself says: `doc:<path>@<commit>`, `plan:<path>`, `file:<path>`, `loose:<path>`.
|
|
347
|
+
const m = /^(doc|del|plan|file|loose):(.+?)(?:@[0-9a-f]{6,40})?$/i.exec(e.id);
|
|
348
|
+
if (!m) return [];
|
|
349
|
+
return /^[A-Za-z]:[\\/]|^\//.test(m[2]!) ? [m[2]!] : roots.map((r) => join(r, ...m[2]!.split('/')));
|
|
350
|
+
};
|
|
351
|
+
const onProcess = (targetId: string, e: EvidenceRef | null | undefined, how: string) => {
|
|
352
|
+
const key = e ? objectKey(targetId) : null;
|
|
353
|
+
if (!e || !key) return;
|
|
354
|
+
if (e.kind === 'source') { cite(e.id, key, 'Process', how); return; }
|
|
355
|
+
for (const p of evidencePlaces(e)) push(byPlace, pathKey(p), { key, link: 'Process', how });
|
|
356
|
+
};
|
|
357
|
+
// A link records its evidence; one written before it did names it by `ledgerRef` alone (`source:<id>` for a source).
|
|
358
|
+
const linkEvidence = (ref: string): EvidenceRef => (ref.startsWith('source:') ? { kind: 'source', id: ref.slice(7), label: ref } : ref.startsWith('file:') && !ledger ? { kind: 'file', id: ref.slice(5), label: ref } : { kind: 'ledger', id: ref, label: ref });
|
|
359
|
+
for (const l of store.links.all()) onProcess(l.workId, l.evidence ?? linkEvidence(l.ledgerRef), `its ${l.stepKind} step${l.confirmed ? '' : l.check?.passed ? ' (lane-checked)' : ' (not yet confirmed)'} rests on`);
|
|
360
|
+
// A breakpoint candidate rests on its evidence as a lit one does (D99: nothing is lit after the first usable round).
|
|
361
|
+
for (const b of store.breakpoints.all()) if (!b.out) for (const e of b.evidence ?? []) onProcess(b.targetId, e, `its breakpoint ${b.lit ? '' : 'candidate '}${b.kind} rests on`);
|
|
362
|
+
for (const sb of store.sendbacks.all()) if (sb.stage !== 'Closed') for (const e of sb.evidence ?? []) onProcess(sb.targetId, e, `its send-back to ${sb.to} rests on`);
|
|
363
|
+
|
|
364
|
+
// ── the cited sources of each file, to find what a change removed or could not tell apart ──
|
|
365
|
+
const heldByPath = new Map<string, Source[]>();
|
|
366
|
+
for (const id of holdersOf.keys()) {
|
|
367
|
+
const s = store.sources.get(id);
|
|
368
|
+
if (s?.anchor.kind === 'file') push(heldByPath, pathKey(s.anchor.path), s);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ── territories: which works changed which, and where each one's code is ──
|
|
372
|
+
let territoryWorks: Map<string, string[]> | null | undefined;
|
|
373
|
+
const territoriesOf = () => {
|
|
374
|
+
if (territoryWorks !== undefined) return territoryWorks;
|
|
375
|
+
const byWork = store.territories.size ? input.workTerritories?.() ?? null : null;
|
|
376
|
+
if (!byWork) return (territoryWorks = null);
|
|
377
|
+
territoryWorks = new Map();
|
|
378
|
+
for (const [workId, rows] of Object.entries(byWork)) for (const r of rows) push(territoryWorks, r.territoryId, workId);
|
|
379
|
+
return territoryWorks;
|
|
380
|
+
};
|
|
381
|
+
const repos = ledger?.repos() ?? [];
|
|
382
|
+
const rootOf = (repo: string) => (repos.find((r) => r.id === repo || samePath(r.path, repo) || slash(r.path).toLowerCase().endsWith(`/${repo.toLowerCase()}`)) ?? repos[0])?.path ?? null;
|
|
383
|
+
|
|
384
|
+
const numbers = numberIndex(store);
|
|
385
|
+
for (const change of changes) {
|
|
386
|
+
const { entry } = change;
|
|
387
|
+
const parts = (change.changed.length ? change.changed : change.fresh).map((s) => s.id);
|
|
388
|
+
const freshIds = new Set(change.fresh.map((s) => s.id));
|
|
389
|
+
|
|
390
|
+
// Number
|
|
391
|
+
if (numbers) {
|
|
392
|
+
for (const t of change.texts) {
|
|
393
|
+
const found = numbers.inText(t.text);
|
|
394
|
+
if (t.gone === 'changed') {
|
|
395
|
+
const still = new Set(numbers.inText(t.now ?? ''));
|
|
396
|
+
for (const num of found) if (!still.has(num)) for (const key of numbers.holders.get(num) ?? []) reach(key, entry, 'Number', `no longer names ${num} in ${t.where}`, parts);
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
for (const num of found) for (const key of numbers.holders.get(num) ?? []) {
|
|
400
|
+
reach(key, entry, 'Number', t.gone === 'removed' ? `names ${num} in ${t.where}, a section the change removed` : `names ${num} in ${t.where}`, t.sourceIds.length ? t.sourceIds : parts);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
for (const n of change.names) for (const num of numbers.inName(n.name)) for (const key of numbers.holders.get(num) ?? []) reach(key, entry, 'Number', `names ${num} in ${n.where}`, parts);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Cited source, recorded change, and process evidence given as a source: the parts that changed
|
|
407
|
+
const reached = new Map<string, string>(); // source id → where it is, as the reason says it
|
|
408
|
+
for (const s of change.changed) reached.set(s.id, partLabel(project, s));
|
|
409
|
+
if (change.path) {
|
|
410
|
+
for (const s of heldByPath.get(pathKey(change.path)) ?? []) {
|
|
411
|
+
if (reached.has(s.id)) continue;
|
|
412
|
+
if (!change.precise) reached.set(s.id, partLabel(project, s));
|
|
413
|
+
else if (!freshIds.has(s.id) && s.anchor.kind === 'file' && change.removed.has(baseOf(s.anchor.headingPath))) reached.set(s.id, `${partLabel(project, s)}, which the change removed`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
for (const [id, where] of reached) for (const h of holdersOf.get(id) ?? []) reach(h.key, entry, h.link, `${h.how} ${where}`, freshIds.has(id) ? [id] : parts);
|
|
417
|
+
|
|
418
|
+
// Process: a step, breakpoint or send-back resting on the file or the session that changed
|
|
419
|
+
const places = change.path ? [change.path] : entry.kind === 'session' ? [entry.ref] : [];
|
|
420
|
+
for (const place of places) for (const h of byPlace.get(pathKey(place)) ?? []) reach(h.key, entry, h.link, `${h.how} ${entry.kind === 'session' ? entry.label : slash(projectRelPath(project, place))}`, parts);
|
|
421
|
+
|
|
422
|
+
// Code territory: a code file of a territory the work changed
|
|
423
|
+
const code = change.files.filter((f) => !isDocumentPath(f));
|
|
424
|
+
if (code.length && store.territories.size) {
|
|
425
|
+
const works = territoriesOf();
|
|
426
|
+
if (works) for (const t of store.territories.all()) {
|
|
427
|
+
const root = rootOf(t.repo);
|
|
428
|
+
const doers = works.get(t.id);
|
|
429
|
+
if (!root || !doers?.length) continue;
|
|
430
|
+
for (const f of code) {
|
|
431
|
+
if (!isWithin(root, f)) continue;
|
|
432
|
+
const rel = relativeDisplay(root, f);
|
|
433
|
+
if (!within(t.paths, rel)) continue;
|
|
434
|
+
for (const w of doers) reach(THREAD(w), entry, 'Code territory', `${rel} is in the code territory “${t.name}”, which it changed`, parts);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const out = (key: ObjectKey): PendingWait[] => {
|
|
442
|
+
if (!live(key)) return [];
|
|
443
|
+
return [...(waits.get(key)?.values() ?? [])]
|
|
444
|
+
.sort((a, b) => a.entry.since.localeCompare(b.entry.since) || a.entry.label.localeCompare(b.entry.label))
|
|
445
|
+
.map((w) => ({ ref: w.entry.ref, kind: w.entry.kind, label: w.entry.label, since: w.entry.since, sourceIds: [...w.sourceIds], reasons: [...w.reasons.values()] }));
|
|
446
|
+
};
|
|
447
|
+
const threads = new Map<string, PendingWait[]>();
|
|
448
|
+
for (const t of store.threads.all()) { const w = out(THREAD(t.id)); if (w.length) threads.set(t.id, w); }
|
|
449
|
+
const references = new Map<string, PendingWait[]>();
|
|
450
|
+
for (const r of store.reference.all()) { const w = out(REF(r.id)); if (w.length) references.set(r.id, w); }
|
|
451
|
+
const facts = new Map<string, PendingWait[]>();
|
|
452
|
+
for (const f of store.facts.all()) { const w = out(FACT(f.id)); if (w.length) facts.set(f.id, w); }
|
|
453
|
+
// An area waits for what its own item waits for, and for what the work items its understanding is made of wait for.
|
|
454
|
+
const areas = new Map<string, PendingWait[]>();
|
|
455
|
+
for (const a of store.areas.all()) {
|
|
456
|
+
const merged = new Map<string, { w: PendingWait; sourceIds: Set<string>; reasons: Map<string, { link: PendingLink; detail: string }> }>();
|
|
457
|
+
const add = (w: PendingWait, reasons: readonly { link: PendingLink; detail: string }[]) => {
|
|
458
|
+
const m = merged.get(w.ref) ?? { w, sourceIds: new Set<string>(), reasons: new Map() };
|
|
459
|
+
for (const id of w.sourceIds) m.sourceIds.add(id);
|
|
460
|
+
for (const r of reasons) m.reasons.set(`${r.link}|${r.detail}`, r);
|
|
461
|
+
merged.set(w.ref, m);
|
|
462
|
+
};
|
|
463
|
+
for (const w of references.get(a.referenceId) ?? []) add(w, w.reasons);
|
|
464
|
+
for (const c of a.contributions ?? []) {
|
|
465
|
+
const t = store.threads.get(c.threadId);
|
|
466
|
+
for (const w of threads.get(c.threadId) ?? []) add(w, [{ link: 'Contributing work', detail: `its work ${t?.title ?? c.threadId} (${c.threadId}) waits for it` }]);
|
|
467
|
+
}
|
|
468
|
+
if (merged.size) areas.set(a.referenceId, [...merged.values()].sort((x, y) => x.w.since.localeCompare(y.w.since) || x.w.label.localeCompare(y.w.label)).map((m) => ({ ...m.w, sourceIds: [...m.sourceIds], reasons: [...m.reasons.values()] })));
|
|
469
|
+
}
|
|
470
|
+
return { threads, references, areas, facts };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// ───────────────────────── writing them ─────────────────────────
|
|
474
|
+
|
|
475
|
+
const idsOf = (waits: readonly PendingWait[]) => [...new Set(waits.flatMap((w) => w.sourceIds))];
|
|
476
|
+
const sameMark = (ids: readonly string[] | undefined, had: readonly PendingWait[] | undefined, waits: readonly PendingWait[]) =>
|
|
477
|
+
JSON.stringify(ids ?? []) === JSON.stringify(idsOf(waits)) && JSON.stringify(had ?? []) === JSON.stringify(waits);
|
|
478
|
+
const traceOf = (name: string, waits: readonly PendingWait[]) => ({
|
|
479
|
+
jobId: null,
|
|
480
|
+
basisSourceIds: idsOf(waits).slice(0, 50),
|
|
481
|
+
summary: waits.length ? `Update pending: ${name} waits for ${waits.map((w) => w.label).join(', ')}` : `Update pending cleared: ${name}; what it waited for was taken in by a round`,
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Write the marks: every entry's `pendingSourceIds` and `waitsFor` become what the pending list gives it now, cleared
|
|
486
|
+
* where it gives nothing. Only what differs is written; an entry's own time is left alone, since a change it waits for is
|
|
487
|
+
* not a change of the entry (§2.10). Returns how many entries changed.
|
|
488
|
+
*/
|
|
489
|
+
export function writeUpdatePending(store: ProjectStore, marks: UpdatePendingMarks): number {
|
|
490
|
+
let n = 0;
|
|
491
|
+
for (const t of store.threads.all()) {
|
|
492
|
+
const waits = marks.threads.get(t.id) ?? [];
|
|
493
|
+
if (sameMark(t.pendingSourceIds, t.waitsFor, waits)) continue;
|
|
494
|
+
const { waitsFor: _drop, ...rest } = t;
|
|
495
|
+
store.threads.put({ ...rest, pendingSourceIds: idsOf(waits), ...(waits.length ? { waitsFor: waits } : {}) }, traceOf(t.title, waits));
|
|
496
|
+
n += 1;
|
|
497
|
+
}
|
|
498
|
+
for (const r of store.reference.all()) {
|
|
499
|
+
const waits = marks.references.get(r.id) ?? [];
|
|
500
|
+
if (sameMark(r.pendingSourceIds, r.waitsFor, waits)) continue;
|
|
501
|
+
const { waitsFor: _drop, pendingSourceIds: _ids, ...rest } = r;
|
|
502
|
+
store.reference.put({ ...rest, ...(waits.length ? { pendingSourceIds: idsOf(waits), waitsFor: waits } : {}) }, traceOf(r.name, waits));
|
|
503
|
+
n += 1;
|
|
504
|
+
}
|
|
505
|
+
for (const a of store.areas.all()) {
|
|
506
|
+
const waits = marks.areas.get(a.referenceId) ?? [];
|
|
507
|
+
if (sameMark(a.pendingSourceIds, a.waitsFor, waits)) continue;
|
|
508
|
+
const { waitsFor: _drop, ...rest } = a;
|
|
509
|
+
store.areas.put({ ...rest, pendingSourceIds: idsOf(waits), ...(waits.length ? { waitsFor: waits } : {}) }, traceOf(`the area ${store.reference.get(a.referenceId)?.name ?? a.referenceId}`, waits));
|
|
510
|
+
n += 1;
|
|
511
|
+
}
|
|
512
|
+
for (const f of store.facts.all()) {
|
|
513
|
+
const ids = idsOf(marks.facts.get(f.id) ?? []);
|
|
514
|
+
if (JSON.stringify(f.pendingSourceIds ?? []) === JSON.stringify(ids)) continue;
|
|
515
|
+
store.facts.put({ ...f, pendingSourceIds: ids }, { jobId: null, basisSourceIds: ids.slice(0, 50), summary: ids.length ? `Update pending: ${f.title} waits for changed material` : `Update pending cleared: ${f.title}` });
|
|
516
|
+
n += 1;
|
|
517
|
+
}
|
|
518
|
+
return n;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Bring a project's `Update pending` marks in line with what waits for the next round: the list the coverage shows
|
|
523
|
+
* (`pending`, when the caller computed it; else computed now, the watcher's changes included), the project's ledger and
|
|
524
|
+
* the code map's territories. Returns how many entries changed.
|
|
525
|
+
*/
|
|
526
|
+
export function refreshUpdatePending(app: App, projectId: string, pending?: ClerkPending): number {
|
|
527
|
+
const store = app.store(projectId);
|
|
528
|
+
const project = app.project(projectId);
|
|
529
|
+
const list = pending ?? clerkPending(store, project, app.pendingChanges(projectId));
|
|
530
|
+
const marks = updatePendingMarks({
|
|
531
|
+
store, project, pending: list, takenInAt: takenInAt(store), ledger: app.ledger.ledger(projectId),
|
|
532
|
+
workTerritories: () => app.kEngines.codemap?.workTerritories(store, project) ?? null,
|
|
533
|
+
});
|
|
534
|
+
return writeUpdatePending(store, marks);
|
|
535
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The workbench's own placement, read by the program (DB; Spec §1.4 "落位", §6.3).
|
|
3
|
+
*
|
|
4
|
+
* One definition of "unplaced": the story map's placement model (`ui/placement.js`) decides where every object sits and
|
|
5
|
+
* what is not placed yet, and the program's counts for the Keeper — `pk_round_state`, the handover note, the reasons a
|
|
6
|
+
* writer accepts — are read from that same model, never from a second rule. On the DeepSeek run (2026-10-04) the
|
|
7
|
+
* program counted 0 unplaced while the workbench showed 2 work items in no band (their only plan was written `Proposed`,
|
|
8
|
+
* which has no band) and 8 Requirements on nothing or on the Product with no reason: the program's rule covered
|
|
9
|
+
* decisions, boundaries, designs and work items, and took any Plan item as a plan.
|
|
10
|
+
*
|
|
11
|
+
* The model is given what the workbench is given: each object's node as the graph derivation writes it (graph.ts
|
|
12
|
+
* `referenceNode`, `threadNode`), the fields its placement reads (server/graph-view.ts `placementFields`), the earlier
|
|
13
|
+
* generations (`generationsView`) and the relations. They are built from the assets as they stand now — the claims the
|
|
14
|
+
* items carry (`claimedRelations`) together with the relations already written — because the graph is derived again
|
|
15
|
+
* only when a job ends, and the main agent counts while its job runs.
|
|
16
|
+
*/
|
|
17
|
+
import { placementOf, unplacedOf, type Placement, type PlacementData, type PlacementNode, type UnplacedLists } from '../../../ui/placement.js';
|
|
18
|
+
import type { GraphRelation, ReferenceItem, WorkThread } from '../../model/types.ts';
|
|
19
|
+
import type { ProjectStore } from '../../store/project-store.ts';
|
|
20
|
+
import { generationsView, placementFields, recentChanges, recentlyChanged } from '../../server/graph-view.ts';
|
|
21
|
+
import { claimedRelations, referenceNode, threadNode } from './graph.ts';
|
|
22
|
+
|
|
23
|
+
/** A work item as a write is about to leave it, read in place of the stored one: the writer checks its reasons before it writes. */
|
|
24
|
+
export interface PlacementOverride { readonly thread?: WorkThread }
|
|
25
|
+
|
|
26
|
+
/** What the placement model is given: the objects still in the project's current version, as the graph view gives them. */
|
|
27
|
+
export function placementData(store: ProjectStore, override: PlacementOverride = {}): PlacementData {
|
|
28
|
+
const own = override.thread;
|
|
29
|
+
const threads = own ? [...store.threads.filter((t) => t.id !== own.id), own] : store.threads.all();
|
|
30
|
+
const derived = [...store.reference.filter((r) => r.validity !== 'Removed').map((r) => referenceNode(store, r)), ...threads.filter((t) => t.validity !== 'Removed').map((t) => threadNode(store, t))];
|
|
31
|
+
const ids = new Set(derived.map((n) => n.id));
|
|
32
|
+
const seen = new Set<string>();
|
|
33
|
+
const relations: Pick<GraphRelation, 'type' | 'from' | 'to'>[] = [];
|
|
34
|
+
// The item being written states its own serves; the relations already written stay, as the next derivation keeps them.
|
|
35
|
+
const stated = own ? own.serves.filter((s) => store.reference.has(s.referenceId)).map((s) => ({ type: 'serves' as const, from: own.id, to: s.referenceId })) : [];
|
|
36
|
+
for (const r of [...stated, ...claimedRelations(store), ...store.relations.all()]) {
|
|
37
|
+
const key = `${r.type}\u0000${r.from}\u0000${r.to}`;
|
|
38
|
+
if (seen.has(key) || !ids.has(r.from) || !ids.has(r.to)) continue;
|
|
39
|
+
seen.add(key);
|
|
40
|
+
relations.push({ type: r.type, from: r.from, to: r.to });
|
|
41
|
+
}
|
|
42
|
+
const recent = recentlyChanged(recentChanges(store), relations);
|
|
43
|
+
const fields = (n: (typeof derived)[number]) => (own && n.id === own.id
|
|
44
|
+
? { servesOrder: own.serves.map((s) => s.referenceId), ...(own.wholePlanWhy?.trim() ? { wholePlanWhy: own.wholePlanWhy.trim() } : {}), ...(own.noPlanWhy?.trim() ? { noPlanWhy: own.noPlanWhy.trim() } : {}), ...(own.noAreaWhy?.trim() ? { noAreaWhy: own.noAreaWhy.trim() } : {}) }
|
|
45
|
+
: placementFields(store, n));
|
|
46
|
+
const nodes: PlacementNode[] = derived.map((n) => ({ id: n.id, category: n.category, label: n.label, validity: n.validity, progress: n.progress, areaId: n.areaId, parentId: null, replacedBy: n.replacedBy, recentChange: recent.has(n.id), ...fields(n) }));
|
|
47
|
+
return { nodes, relations, generations: generationsView(store) };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The workbench's placement of the project as its assets stand now — or as they would with the work item a write is about to store. */
|
|
51
|
+
export function workbenchPlacement(store: ProjectStore, override: PlacementOverride = {}): Placement {
|
|
52
|
+
return placementOf(placementData(store, override));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface WorkbenchUnplaced {
|
|
56
|
+
readonly placement: Placement;
|
|
57
|
+
/** What is not placed yet, by kind (ui/placement.js `unplacedOf`). */
|
|
58
|
+
readonly lists: UnplacedLists;
|
|
59
|
+
/** The plans the workbench draws a band for: the current ones. */
|
|
60
|
+
readonly drawnPlans: ReadonlySet<string>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The workbench's placement together with its lists of what is not placed. */
|
|
64
|
+
export function workbenchUnplaced(store: ProjectStore): WorkbenchUnplaced {
|
|
65
|
+
const placement = workbenchPlacement(store);
|
|
66
|
+
return { placement, lists: unplacedOf(placement), drawnPlans: new Set(placement.plans.map((p) => p.id)) };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A plan in words for a list: its name, and why the workbench draws no band for it. */
|
|
70
|
+
export function planNotDrawnLine(store: ProjectStore, placement: Placement, planId: string): string {
|
|
71
|
+
const plan = store.reference.get(planId);
|
|
72
|
+
if (!plan) return planId;
|
|
73
|
+
const gen = placement.generations.find((g) => g.planIds.includes(planId));
|
|
74
|
+
const name = plan.name.length > 60 ? `${plan.name.slice(0, 59)}…` : plan.name;
|
|
75
|
+
return `${name} (${plan.validity}${gen ? `; held in the earlier generation ${gen.name}` : ''})`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The plans a work item serves that the workbench draws no band for, each in words — empty when it serves none, or
|
|
80
|
+
* stands in a band. A work item whose only plans are these is in no plan (ui/placement.js `plansNotDrawn`).
|
|
81
|
+
*/
|
|
82
|
+
export function plansNotDrawnOf(store: ProjectStore, placement: Placement, thread: Pick<WorkThread, 'id'>): string[] {
|
|
83
|
+
return (placement.place.get(thread.id)?.plansNotDrawn ?? []).map((id) => planNotDrawnLine(store, placement, id));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Where a requirement, design or decision stands when it is not placed: on nothing, or only on the Product (or a Goal) with no reason. */
|
|
87
|
+
export function unplacedIntent(store: ProjectStore, lists: UnplacedLists): { readonly item: ReferenceItem; readonly on: 'nothing' | 'the Product only' }[] {
|
|
88
|
+
const out: { item: ReferenceItem; on: 'nothing' | 'the Product only' }[] = [];
|
|
89
|
+
for (const id of lists.intentNowhere) { const item = store.reference.get(id); if (item) out.push({ item, on: 'nothing' }); }
|
|
90
|
+
for (const id of lists.intentProductOnly) { const item = store.reference.get(id); if (item) out.push({ item, on: 'the Product only' }); }
|
|
91
|
+
return out;
|
|
92
|
+
}
|