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,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source code does not reach an execution agent through ProjectKeeper (D65; Spec §7.1, §7.4, §7.10; CKC-12 AC-43).
|
|
3
|
+
* What goes instead is where the code is — repository, commit, file, lines — and the agent opens the current version.
|
|
4
|
+
*
|
|
5
|
+
* A "code source" is a code file: a file the Keeper judged `Code`, or one whose name says it is program text or build
|
|
6
|
+
* and tool configuration. A configuration-format file the Keeper judged to be one of the project's intent or record
|
|
7
|
+
* materials (a plan kept in YAML, a status table in JSON) is that material, not code. Markdown sections, sessions,
|
|
8
|
+
* commits and command results are never code files; a code block a design document or QC report contains is that
|
|
9
|
+
* document's own text (Spec §7.1).
|
|
10
|
+
*
|
|
11
|
+
* The Keeper's own reading of code is not touched by any of this (Spec §7.1): only what is handed to an agent.
|
|
12
|
+
*/
|
|
13
|
+
import type { FileAnchor, Project, Source } from '../model/types.ts';
|
|
14
|
+
import { isWithin, relativeDisplay } from '../util/paths.ts';
|
|
15
|
+
|
|
16
|
+
const CODE_EXT = new Set([
|
|
17
|
+
'.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs', '.py', '.pyi', '.rb', '.go', '.rs', '.java', '.kt', '.kts',
|
|
18
|
+
'.swift', '.dart', '.c', '.cc', '.cpp', '.cxx', '.h', '.hh', '.hpp', '.hxx', '.m', '.mm', '.cs', '.fs', '.vb', '.php',
|
|
19
|
+
'.pl', '.pm', '.lua', '.r', '.scala', '.clj', '.ex', '.exs', '.erl', '.hs', '.ml', '.sql', '.sh', '.bash', '.zsh', '.fish',
|
|
20
|
+
'.ps1', '.psm1', '.bat', '.cmd', '.css', '.scss', '.sass', '.less', '.vue', '.svelte', '.gradle', '.groovy', '.cmake',
|
|
21
|
+
]);
|
|
22
|
+
const CODE_NAMES = new Set(['makefile', 'dockerfile', 'cmakelists.txt', 'gemfile', 'rakefile', 'podfile', 'jenkinsfile', 'build.bazel', 'workspace']);
|
|
23
|
+
/** Configuration formats: code, unless the Keeper judged the file to be one of the materials below. */
|
|
24
|
+
const CONFIG_EXT = new Set(['.json', '.json5', '.jsonc', '.yaml', '.yml', '.toml', '.xml', '.ini', '.cfg', '.conf', '.properties', '.plist', '.lock', '.env', '.html', '.htm']);
|
|
25
|
+
const CONFIG_NAMES = new Set(['.gitignore', '.gitattributes', '.editorconfig', '.npmrc', '.env.example', '.dockerignore']);
|
|
26
|
+
const MATERIAL_USE = new Set(['Purpose', 'Decision', 'Requirement', 'Design', 'Plan', 'Task', 'Status', 'QC', 'Run result', 'Session']);
|
|
27
|
+
|
|
28
|
+
/** Whether a file at this path, used as this, is a code file. */
|
|
29
|
+
export function isCodeFile(path: string, usedAs: string | null | undefined): boolean {
|
|
30
|
+
if (usedAs === 'Code') return true;
|
|
31
|
+
const base = (path.split(/[\\/]/).pop() ?? '').toLowerCase();
|
|
32
|
+
const dot = base.lastIndexOf('.');
|
|
33
|
+
const ext = dot > 0 ? base.slice(dot) : '';
|
|
34
|
+
if (CODE_EXT.has(ext) || CODE_NAMES.has(base)) return true;
|
|
35
|
+
if (CONFIG_EXT.has(ext) || CONFIG_NAMES.has(base)) return !(usedAs && MATERIAL_USE.has(usedAs));
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isCodeSource(s: Pick<Source, 'anchor' | 'usedAs'>): boolean {
|
|
40
|
+
if (s.anchor.kind !== 'file') return false;
|
|
41
|
+
if (s.usedAs === 'Code') return true;
|
|
42
|
+
// A section under a heading is a document's section (the reader splits only Markdown by heading).
|
|
43
|
+
if (s.anchor.headingPath.length > 0) return false;
|
|
44
|
+
return isCodeFile(s.anchor.path, s.usedAs);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The same test on what an object page receives for a source: its label (`<path>[ › heading] (Lx–Ly)` for a file) and
|
|
49
|
+
* how it is used. Anything that is not a file's label is not a code file.
|
|
50
|
+
*/
|
|
51
|
+
export function isCodeLabel(label: string, usedAs: string | null | undefined): boolean {
|
|
52
|
+
const m = /^(.*) \(L\d+–L\d+\)$/.exec(label);
|
|
53
|
+
if (!m) return false;
|
|
54
|
+
if (usedAs === 'Code') return true;
|
|
55
|
+
if (m[1]!.includes(' › ')) return false;
|
|
56
|
+
return isCodeFile(m[1]!, usedAs);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface CodeLocation {
|
|
60
|
+
/** The repository or directory of the project scope the file is in, when it is in one. */
|
|
61
|
+
readonly repo: string | null;
|
|
62
|
+
/** The file, relative to `repo` with forward slashes, or the whole path when it is in no scope item. */
|
|
63
|
+
readonly file: string;
|
|
64
|
+
readonly lines: string;
|
|
65
|
+
/** The commit the file was read at, when the project has version control. */
|
|
66
|
+
readonly commit: string | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Where a file source is: repository, file, lines, and the commit it was read at (Spec §7.1, §7.10). */
|
|
70
|
+
export function fileLocation(project: Project, s: Source): CodeLocation {
|
|
71
|
+
const a = s.anchor as FileAnchor;
|
|
72
|
+
const item = project.scope.find((i) => i.id === s.scopeItemId) ?? project.scope.find((i) => i.relation !== 'Excluded' && i.category !== 'Session source' && isWithin(i.path, a.path));
|
|
73
|
+
const inRepo = item !== undefined && isWithin(item.path, a.path);
|
|
74
|
+
return { repo: inRepo ? item.path : null, file: inRepo ? relativeDisplay(item.path, a.path) : a.path, lines: `lines ${a.lineStart}–${a.lineEnd}`, commit: s.version.commit };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** `D:\repo · app/x.ts, lines 3–9, as read at commit abc1234567` */
|
|
78
|
+
export function locationText(loc: CodeLocation): string {
|
|
79
|
+
return `${loc.repo ? `${loc.repo} · ` : ''}${loc.file}, ${loc.lines}${loc.commit ? `, as read at commit ${loc.commit.slice(0, 10)}` : ''}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Just the file and lines, for a statement observed in the code: `app/x.ts, lines 3–9`. */
|
|
83
|
+
export function shortLocationText(loc: CodeLocation): string {
|
|
84
|
+
return `${loc.file}, ${loc.lines}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface CommitFacts { readonly subject: string; readonly at: string | null; readonly files: readonly string[] }
|
|
88
|
+
/**
|
|
89
|
+
* What a commit source records: its message, when, and the files it changed. When it was made is the time its anchor
|
|
90
|
+
* records (sources/gitobs.ts); a commit source written before the anchor carried it gives the `at:` line its excerpt
|
|
91
|
+
* has. The message and the files are read from the source as the commit reader wrote it (message, then an indented
|
|
92
|
+
* `files:` list); what is not there is simply left out.
|
|
93
|
+
*/
|
|
94
|
+
export function commitFacts(s: Source): CommitFacts {
|
|
95
|
+
const lines = s.excerpt.split('\n');
|
|
96
|
+
const at = s.anchor.kind === 'commit' && s.anchor.at !== undefined ? s.anchor.at : lines.find((l) => l.startsWith('at: '))?.slice(4).trim() ?? null;
|
|
97
|
+
const start = lines.findIndex((l) => l.trim() === 'files:');
|
|
98
|
+
const files = start < 0 ? [] : lines.slice(start + 1).filter((l) => /^\s+\S/.test(l)).map((l) => l.trim());
|
|
99
|
+
return { subject: (lines[0] ?? s.title).trim(), at, files };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** How an agent is told to read a code file itself (D65). */
|
|
103
|
+
export const OPEN_CURRENT = 'ProjectKeeper gives where code is, not the code: open the current version in the repository and read it there.';
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/** Text for the agent entry's increment K objects. The server hands these views to the CLI as-is. */
|
|
2
|
+
import type { CodeTerritory, EvidenceRef, Occurred, SemanticPatch } from '../model/k-types.ts';
|
|
3
|
+
import type { BreakpointView, CodeView, SendBackView, TerritoryView } from '../model/views-k.ts';
|
|
4
|
+
import { redactCredentials } from '../sources/anchor.ts';
|
|
5
|
+
|
|
6
|
+
function safe(text: string): string { return redactCredentials(text).text; }
|
|
7
|
+
|
|
8
|
+
export function occurredText(value: Occurred): string {
|
|
9
|
+
if (value.undated) return `Undated · first seen ${value.at.slice(0, 10)}`;
|
|
10
|
+
const one = (at: string, basis: string, anchor: string | null) => `${basis} ${at}${anchor ? ` (${anchor})` : ''}`;
|
|
11
|
+
const primary = one(value.at, value.basis, value.anchor);
|
|
12
|
+
return value.other ? `${primary} · ${one(value.other.at, value.other.basis, value.other.anchor)}` : primary;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function evidenceLines(evidence: readonly EvidenceRef[]): string[] {
|
|
16
|
+
if (!evidence.length) return ['- None recorded'];
|
|
17
|
+
return evidence.flatMap((e) => [
|
|
18
|
+
`- ${e.kind} · ${e.label} (${e.id})${e.occurred ? ` · ${occurredText(e.occurred)}` : ''}`,
|
|
19
|
+
...(e.line == null ? [] : [` Original line: ${e.line}`]),
|
|
20
|
+
]);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function finish(lines: (string | null)[]): string { return safe(lines.filter((line) => line !== null).join('\n')); }
|
|
24
|
+
|
|
25
|
+
export function breakpointBrief(view: BreakpointView, targetName: string): string {
|
|
26
|
+
return finish([
|
|
27
|
+
`# Breakpoint · ${view.kind} (${view.id})`,
|
|
28
|
+
`Where: ${targetName} (${view.targetId})`,
|
|
29
|
+
`What: ${view.why}`,
|
|
30
|
+
`Status: ${view.lit ? 'Lit' : view.state === 'Candidate' ? 'Candidate (a lead, not a finding: it lights only after a lane looked for the step and the spot-check confirmed it)' : 'Out'}${view.ownerResponse ? ` · No action needed at ${view.ownerResponse.at}: ${view.ownerResponse.reason}` : ''}`,
|
|
31
|
+
view.looked ? `Looked for, not found: ${view.looked.where.join('; ')} (${view.looked.at.slice(0, 10)})` : null,
|
|
32
|
+
`Occurred: ${occurredText(view.since)} (next step became due)`,
|
|
33
|
+
`Basis: ${view.basis}`,
|
|
34
|
+
view.sixThing ? `Six thing: ${view.sixThing}` : null,
|
|
35
|
+
view.sendBackId ? `Send-back: ${view.sendBackId}` : null,
|
|
36
|
+
'', '## Evidence', ...evidenceLines(view.evidence),
|
|
37
|
+
]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function sendBackBrief(view: SendBackView, targetName: string): string {
|
|
41
|
+
return finish([
|
|
42
|
+
`# Send-back · ${view.id}`,
|
|
43
|
+
`Where: ${targetName} (${view.targetId})`,
|
|
44
|
+
`What: ${view.what}`,
|
|
45
|
+
`Send back to: ${view.to} · ${view.suggestion}`,
|
|
46
|
+
`Status: ${view.stage}${view.lit ? ' · Open' : ' · Not lit'}${view.ownerResponse ? ` · No action needed at ${view.ownerResponse.at}: ${view.ownerResponse.reason}` : ''}`,
|
|
47
|
+
`Occurred: ${occurredText(view.occurred)}`,
|
|
48
|
+
view.returned ? `Returned: ${view.returned.label} · ${occurredText(view.returned.occurred)}` : null,
|
|
49
|
+
view.closed ? `Closed: ${view.closed.label} · ${occurredText(view.closed.occurred)}` : null,
|
|
50
|
+
view.sixThing ? `Six thing: ${view.sixThing}` : null,
|
|
51
|
+
'', '## Evidence', ...evidenceLines(view.evidence),
|
|
52
|
+
'', '## Copy for agent', view.copyForAgent,
|
|
53
|
+
]);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function patchBrief(patch: SemanticPatch, objectName: (id: string) => string): string {
|
|
57
|
+
return finish([
|
|
58
|
+
`# Semantic patch · ${patch.title} (${patch.number}; ${patch.id})`,
|
|
59
|
+
`Where: ${patch.writtenToFolder ? patch.writtenToFolder.path : 'Workbench only; not written to the project folder'}`,
|
|
60
|
+
`Status: ${patch.status}${patch.partial ? ' · Partial replacement' : ''}`,
|
|
61
|
+
`Occurred: ${occurredText(patch.occurred)}`,
|
|
62
|
+
`Invalidated: ${patch.invalidated}`,
|
|
63
|
+
`Replaced by: ${patch.replacedBy}`,
|
|
64
|
+
`Affects: ${patch.affectsText}`,
|
|
65
|
+
...patch.affects.map((id) => `- ${objectName(id)} (${id})`),
|
|
66
|
+
`Must not pass as current: ${patch.mustNotPassAsCurrent}`,
|
|
67
|
+
'', '## Evidence',
|
|
68
|
+
'Old anchor:', ...evidenceLines([patch.oldAnchor]),
|
|
69
|
+
'New anchor:', ...evidenceLines(patch.newAnchor ? [patch.newAnchor] : []),
|
|
70
|
+
'Decision or commit:', ...evidenceLines(patch.decision ? [patch.decision] : []),
|
|
71
|
+
'Ledger supersession:', ...evidenceLines([patch.candidate]),
|
|
72
|
+
]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function territoryBrief(
|
|
76
|
+
territory: CodeTerritory,
|
|
77
|
+
objectName: (id: string) => string,
|
|
78
|
+
ledger: { view: TerritoryView; version: CodeView['version'] } | null,
|
|
79
|
+
firstSeen: string,
|
|
80
|
+
): string {
|
|
81
|
+
const view = ledger?.view;
|
|
82
|
+
const areaId = view?.row.kind === 'area' ? view.row.areaId : territory.areaId;
|
|
83
|
+
const also = view?.alsoServes.map((a) => ({ id: a.areaId, name: a.areaName })) ?? territory.alsoServes.map((id) => ({ id, name: objectName(id) }));
|
|
84
|
+
const anomalies = view?.anomalies ?? territory.anomalies;
|
|
85
|
+
const lines: (string | null)[] = [
|
|
86
|
+
`# Code territory · ${view?.name ?? territory.name} (${territory.id})`,
|
|
87
|
+
`What: ${view?.summary ?? territory.summary}`,
|
|
88
|
+
`Where: ${view?.repo ?? territory.repo}`,
|
|
89
|
+
...(view?.paths ?? territory.paths).map((path) => `- ${path}`),
|
|
90
|
+
`Kind: ${view?.row.kind ?? territory.kind}`,
|
|
91
|
+
`Main area: ${areaId ? `${objectName(areaId)} (${areaId})` : 'None'}`,
|
|
92
|
+
`Also serves: ${also.length ? also.map((a) => `${a.name} (${a.id})`).join('; ') : 'None'}`,
|
|
93
|
+
`Occurred: ${ledger ? occurredText(ledger.version.occurred) : `Undated · first seen ${firstSeen}`}`,
|
|
94
|
+
`Current state: ${view ? view.currentUse : 'the ledger is not connected yet'}`,
|
|
95
|
+
];
|
|
96
|
+
if (view) {
|
|
97
|
+
lines.push(
|
|
98
|
+
`Size: ${view.size.files} files, ${view.size.lines} lines; generated ${view.size.generatedFiles} files, ${view.size.generatedLines} lines`,
|
|
99
|
+
`Depends on: ${view.dependsOn.join(', ') || 'None'}`,
|
|
100
|
+
`Depended on by: ${view.dependedBy.join(', ') || 'None'}`,
|
|
101
|
+
`Built by: ${view.builtBy.map((b) => `${b.label} (${b.workId ?? 'unattributed'}): ${b.files} files; ${b.commits.join(', ')}`).join('; ') || 'None recorded'}`,
|
|
102
|
+
`Generations: ${view.generations.map((g) => `${g.name}: ${g.share}`).join('; ') || 'None recorded'}`,
|
|
103
|
+
`Tests: ${view.tests}`,
|
|
104
|
+
view.lastChange ? `Last change: ${view.lastChange.commit} · ${occurredText(view.lastChange.occurred)} · ${view.lastChange.subject}` : 'Last change: None recorded',
|
|
105
|
+
);
|
|
106
|
+
} else lines.push('Size, dependencies, who built it, generations, tests and last change: the ledger is not connected yet');
|
|
107
|
+
lines.push('', '## Anomalies');
|
|
108
|
+
if (!anomalies.length) lines.push('- None recorded');
|
|
109
|
+
else for (const anomaly of anomalies) lines.push(
|
|
110
|
+
`- ${anomaly.kind} · ${anomaly.text} · ${anomaly.basis}${anomaly.sendBackId ? ` · send-back ${anomaly.sendBackId}` : ''}`,
|
|
111
|
+
...evidenceLines(anomaly.evidence).map((line) => ` ${line}`),
|
|
112
|
+
);
|
|
113
|
+
return finish(lines);
|
|
114
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* History in a context pack, given as lineage (Spec §2.11, §7.1; CKC-12 AC-35). D82 withdrew D61's "what exists only in
|
|
3
|
+
* history enters no context": history is a first-hand source, and what bears on the work is given — marked as history,
|
|
4
|
+
* saying when, and what replaced it or why it was removed. What remains of D61: what exists only in history (old versions,
|
|
5
|
+
* deleted files, side branches, material kept for recovery) and objects `Removed` from the project's current version never
|
|
6
|
+
* appear as current content; replaced and abandoned directions still go to `Do not revive`; a current object that still
|
|
7
|
+
* rests on a removed one says so on its own row.
|
|
8
|
+
*
|
|
9
|
+
* Everything here is what the program already has, read and never inferred:
|
|
10
|
+
* - the assets: the change records (the program records one per deletion, with the commit that deleted the material —
|
|
11
|
+
* intake/removal.ts), the round judgements that closed an item as superseded by a later change (§2.10), the sources a
|
|
12
|
+
* rule or the history reading made `History only`, and the objects that became `Removed`;
|
|
13
|
+
* - the ledger's `How it got here` (ledger/views.ts `lineageView`): versions, the lines that say something was superseded,
|
|
14
|
+
* deletions, side branches, the plans and commits that name the object, where it stands now — when the project has a
|
|
15
|
+
* ledger.
|
|
16
|
+
*/
|
|
17
|
+
import type { ChangeItem, ChangeRecord } from '../model/types.ts';
|
|
18
|
+
import type { LineageStepView } from '../model/views-k.ts';
|
|
19
|
+
import type { ProjectStore } from '../store/project-store.ts';
|
|
20
|
+
import type { Ledger } from '../ledger/index.ts';
|
|
21
|
+
import { lineageView } from '../ledger/views.ts';
|
|
22
|
+
import { itemsOf } from '../keeper/adjustment.ts';
|
|
23
|
+
|
|
24
|
+
/** How an object left the project's current version, as the change records give it. */
|
|
25
|
+
export interface Removal {
|
|
26
|
+
readonly at: string;
|
|
27
|
+
readonly changeId: string;
|
|
28
|
+
readonly effect: string;
|
|
29
|
+
readonly title: string;
|
|
30
|
+
readonly why: string | null;
|
|
31
|
+
readonly sourceIds: readonly string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The change that took an object out of the current version: the item that recorded it `Removed` (the program writes one
|
|
36
|
+
* per deletion, dated by the commit that deleted the material and giving that commit's reason), else the latest item that
|
|
37
|
+
* gave it up or replaced it. Null when the records hold neither — the pack then says the records do not say when.
|
|
38
|
+
*/
|
|
39
|
+
export function removalOf(store: ProjectStore, id: string): Removal | null {
|
|
40
|
+
let removed: { c: ChangeRecord; it: ChangeItem } | null = null;
|
|
41
|
+
let other: { c: ChangeRecord; it: ChangeItem } | null = null;
|
|
42
|
+
for (const c of store.changes.all()) {
|
|
43
|
+
for (const it of itemsOf(c)) {
|
|
44
|
+
if (!it.affects.includes(id)) continue;
|
|
45
|
+
if (it.effect === 'Removed') { if (!removed || it.at >= removed.it.at) removed = { c, it }; }
|
|
46
|
+
else if ((it.effect === 'Abandoned' || it.effect === 'Replaced') && (!other || it.at >= other.it.at)) other = { c, it };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const hit = removed ?? other;
|
|
50
|
+
return hit ? { at: hit.it.at, changeId: hit.c.id, effect: hit.it.effect, title: hit.it.title, why: hit.it.why ?? null, sourceIds: hit.it.sourceIds } : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Why a change item that bears on the work is history rather than a change in force. */
|
|
54
|
+
export type HistoryReason =
|
|
55
|
+
/** A later change superseded it for an object it reached (§2.10): the round closed the item with that change's id. */
|
|
56
|
+
| { readonly kind: 'superseded'; readonly byChangeId: string | null; readonly reason: string }
|
|
57
|
+
/** Every source it rests on exists only in history (§1.2 `History only`). */
|
|
58
|
+
| { readonly kind: 'history only' }
|
|
59
|
+
/** Everything it set down has since been replaced, given up or removed. */
|
|
60
|
+
| { readonly kind: 'overtaken' };
|
|
61
|
+
|
|
62
|
+
export interface HistoryChange { readonly change: ChangeRecord; readonly item: ChangeItem; readonly why: HistoryReason }
|
|
63
|
+
|
|
64
|
+
const GONE = new Set(['Removed', 'Replaced', 'Abandoned']);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The change items that reached one of `ids` and are history now, oldest first: superseded for one of them by a later
|
|
68
|
+
* change, written only from material that exists in history, or everything they set down since replaced, given up or
|
|
69
|
+
* removed. A proposal nobody adopted changed nothing and is not history of the work.
|
|
70
|
+
*/
|
|
71
|
+
export function historyChanges(store: ProjectStore, ids: ReadonlySet<string>): HistoryChange[] {
|
|
72
|
+
const superseded = new Map<string, { byChangeId: string | null; reason: string }>();
|
|
73
|
+
for (const j of store.propagation.filter((x) => ids.has(x.nodeId))) {
|
|
74
|
+
for (const c of j.closed) if (c.close === 'Superseded by a later change') superseded.set(`${c.changeId}|${c.itemId}`, { byChangeId: c.supersededByChangeId ?? null, reason: c.reason });
|
|
75
|
+
}
|
|
76
|
+
const validity = (id: string) => store.reference.get(id)?.validity ?? store.threads.get(id)?.validity ?? store.nodes.get(id)?.validity ?? null;
|
|
77
|
+
const out: HistoryChange[] = [];
|
|
78
|
+
for (const change of store.changes.all()) {
|
|
79
|
+
for (const item of itemsOf(change)) {
|
|
80
|
+
if (!item.affects.some((id) => ids.has(id)) || item.by?.identity === 'Proposal') continue;
|
|
81
|
+
const over = superseded.get(`${change.id}|${item.id}`);
|
|
82
|
+
if (over) { out.push({ change, item, why: { kind: 'superseded', ...over } }); continue; }
|
|
83
|
+
const sources = item.sourceIds.map((id) => store.sources.get(id)).filter((s) => s !== undefined);
|
|
84
|
+
if (sources.length && sources.every((s) => s.usedAs === 'History only')) { out.push({ change, item, why: { kind: 'history only' } }); continue; }
|
|
85
|
+
const states = item.affects.map(validity).filter((v): v is NonNullable<typeof v> => v !== null);
|
|
86
|
+
if (states.length && states.every((v) => GONE.has(v))) out.push({ change, item, why: { kind: 'overtaken' } });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return out.sort((a, b) => a.item.at.localeCompare(b.item.at));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The ledger's `How it got here` for one object, or why it could not be read; empty without a ledger. */
|
|
93
|
+
export function ledgerLineage(ledger: Ledger | null, store: ProjectStore, objectId: string): { readonly steps: readonly LineageStepView[]; readonly error: string | null } {
|
|
94
|
+
if (!ledger) return { steps: [], error: null };
|
|
95
|
+
try {
|
|
96
|
+
return { steps: lineageView(ledger, store, objectId)?.steps ?? [], error: null };
|
|
97
|
+
} catch (e) {
|
|
98
|
+
return { steps: [], error: (e as Error).message };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `pk get <id>` returns for an object that is not a context pack, a source or a note (Spec §7.10; CKC-12 AC-23):
|
|
3
|
+
* the same facts the workbench's object detail shows (§6.4), written for an agent to read.
|
|
4
|
+
*
|
|
5
|
+
* The rule (D59): the object's own content goes in whole — its explanation, the owner's words, its validity, progress
|
|
6
|
+
* and acceptance. Everything around it — relations, the change records that reach it, notes and marks — is named by
|
|
7
|
+
* the other end's name and id, and fetched by id when the agent wants it. A change record does not carry its whole
|
|
8
|
+
* propagation list: the objects that have not followed it are named, the rest are counted. None of the workbench's own
|
|
9
|
+
* bookkeeping (the job trace, node flags, internal timestamps) is returned.
|
|
10
|
+
*
|
|
11
|
+
* Basis: on 2026-09-18 `pk get` on one reference item returned 82 KB of raw workbench JSON, 63% of it the full records
|
|
12
|
+
* of the changes that reached it, each with its whole propagation list; the item itself was about 3%. About 70% of the
|
|
13
|
+
* 18.1 MB the CLI produced for a subagent that only used the CLI came from fetching reference items (D59).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { isCodeLabel } from './code-source.ts';
|
|
17
|
+
|
|
18
|
+
export interface BriefSource { readonly id: string; readonly title: string; readonly label: string; readonly excerpt?: string; readonly usedAs?: string | null; readonly availability?: string | null }
|
|
19
|
+
export interface BriefRelation { readonly id: string; readonly type: string; readonly from: string; readonly to: string; readonly fromLabel?: string; readonly toLabel?: string; readonly claim?: string; readonly basis?: string; readonly assessment?: string }
|
|
20
|
+
export interface BriefNote { readonly id: string; readonly title: string; readonly ask?: string }
|
|
21
|
+
export interface BriefMark { readonly id: string; readonly kind: string; readonly clue?: string; readonly targetId?: string }
|
|
22
|
+
export interface BriefPropagation { readonly nodeId: string; readonly state: string; readonly sourceOrReason?: string }
|
|
23
|
+
/** One net change of a piece of work (Spec §1.8): what changed, why, and what it changed. */
|
|
24
|
+
export interface BriefChangeItem {
|
|
25
|
+
readonly id: string; readonly at: string; readonly effect: string; readonly material?: string; readonly title: string;
|
|
26
|
+
readonly summary?: string; readonly before?: string | null; readonly after?: string | null; readonly why?: string | null;
|
|
27
|
+
readonly affects?: readonly string[];
|
|
28
|
+
}
|
|
29
|
+
export interface BriefChange {
|
|
30
|
+
readonly id: string; readonly at: string; readonly effect: string; readonly material?: string; readonly title: string;
|
|
31
|
+
readonly summary?: string; readonly before?: string | null; readonly after?: string | null;
|
|
32
|
+
readonly segment?: { readonly name: string } | null;
|
|
33
|
+
/** The piece of work the record is (Spec §1.8, D56), and its net changes. A record from before pieces of work has neither. */
|
|
34
|
+
readonly work?: { readonly kind: string; readonly label: string; readonly startedAt?: string; readonly endedAt?: string; readonly openEnded?: boolean } | null;
|
|
35
|
+
readonly items?: readonly BriefChangeItem[];
|
|
36
|
+
readonly affectsLabels?: readonly { readonly id: string; readonly label: string }[];
|
|
37
|
+
readonly propagationLabels?: readonly { readonly id: string; readonly label: string }[];
|
|
38
|
+
readonly propagation?: readonly BriefPropagation[];
|
|
39
|
+
/** What the piece of work recorded and no one judges: a finished work item, a session, a commit (§2.10). */
|
|
40
|
+
readonly notJudgedLabels?: readonly { readonly id: string; readonly label: string }[];
|
|
41
|
+
/** What it reached that has since been removed from the current version. */
|
|
42
|
+
readonly removed?: readonly { readonly id: string; readonly label: string }[];
|
|
43
|
+
readonly sources?: readonly { readonly id: string; readonly title: string; readonly label: string }[];
|
|
44
|
+
}
|
|
45
|
+
export interface BriefNode {
|
|
46
|
+
readonly node?: { readonly id: string; readonly label: string; readonly category: string; readonly refKind: string; readonly refId: string; readonly validity?: string; readonly progress?: string | null; readonly acceptance?: string; readonly basis?: string; readonly attribution?: { readonly holder?: { readonly role: string } | null; readonly author?: { readonly kind: string; readonly name: string | null } | null; readonly identity?: string } | null; readonly updatedAt?: string };
|
|
47
|
+
readonly reference?: { readonly name: string; readonly ids?: readonly string[]; readonly category: string; readonly text: string; readonly quote?: string | null; readonly answers?: string | null; readonly validity?: string; readonly basis?: string; readonly asOf?: string; readonly replacedBy?: string | null; readonly refines?: readonly string[] } | null;
|
|
48
|
+
readonly area?: { readonly effectNow: string; readonly gaps: string; readonly asOf?: string } | null;
|
|
49
|
+
readonly thread?: { readonly title: string; readonly ids?: readonly string[]; readonly doing?: string; readonly changed?: string; readonly results?: string; readonly unresolved?: string; readonly doneMeans?: string; readonly acceptanceMeans?: string; readonly progress?: string; readonly validity?: string; readonly acceptance?: string; readonly servesLabels?: readonly { readonly referenceId: string; readonly name: string; readonly claim: string }[]; readonly dependsOn?: readonly { readonly threadId: string; readonly claim: string }[]; readonly executionFacts?: readonly { readonly type: string; readonly text: string }[]; readonly qcFacts?: readonly { readonly type: string; readonly text: string }[]; readonly facts?: readonly { readonly id: string; readonly title: string }[] } | null;
|
|
50
|
+
readonly change?: BriefChange | null;
|
|
51
|
+
readonly relations?: readonly BriefRelation[];
|
|
52
|
+
readonly notes?: readonly BriefNote[];
|
|
53
|
+
readonly changes?: readonly BriefChange[];
|
|
54
|
+
readonly marks?: readonly BriefMark[];
|
|
55
|
+
readonly sources?: readonly BriefSource[];
|
|
56
|
+
readonly updatePending?: boolean;
|
|
57
|
+
/** The changes `Update pending` waits for (Spec §7.1), each with the parts to read and how it reaches the object. */
|
|
58
|
+
readonly waitsFor?: readonly BriefWait[];
|
|
59
|
+
/** When they will be taken in (§3.8). */
|
|
60
|
+
readonly pendingWhen?: string | null;
|
|
61
|
+
}
|
|
62
|
+
export interface BriefWait { readonly ref: string; readonly label: string; readonly sourceIds: readonly string[]; readonly reasons: readonly { readonly link: string; readonly detail: string }[] }
|
|
63
|
+
export interface BriefFact {
|
|
64
|
+
readonly id: string; readonly title: string; readonly asOf?: string;
|
|
65
|
+
readonly statements?: readonly { readonly type: string; readonly text: string; readonly sourceIds?: readonly string[] }[];
|
|
66
|
+
readonly openQuestions?: readonly string[];
|
|
67
|
+
readonly aboutSourceIds?: readonly string[];
|
|
68
|
+
readonly sources?: readonly BriefSource[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const text = (s: string | null | undefined) => (s ?? '').trim();
|
|
72
|
+
const line = (label: string, value: string | null | undefined) => (text(value) ? `${label}: ${text(value)}` : null);
|
|
73
|
+
const keep = (lines: (string | null | undefined | false)[]) => lines.filter((l): l is string => typeof l === 'string' && l.length > 0).join('\n');
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* An object's sources are part of its own content: each with its id and its excerpt, whole (Spec §7.10; CKC-12 AC-23,
|
|
77
|
+
* AC-31). A code file is named by where it is, never quoted (D65; AC-43): the agent opens the current version. The
|
|
78
|
+
* excerpt used to be shown only under 400 characters, when the workbench cut its excerpts at that length; the workbench
|
|
79
|
+
* gives them whole now, so a longer one no longer turned into a bare id (QC AH #12).
|
|
80
|
+
*/
|
|
81
|
+
function sourceLines(sources: readonly BriefSource[] | undefined, how: string): string[] {
|
|
82
|
+
if (!sources || !sources.length) return [];
|
|
83
|
+
return [`\n## Sources (${sources.length})`, ...sources.map((s) => {
|
|
84
|
+
const code = isCodeLabel(s.label, s.usedAs);
|
|
85
|
+
const excerpt = code ? '' : text(s.excerpt);
|
|
86
|
+
const history = s.usedAs === 'History only' ? ' · history only, not the current version' : '';
|
|
87
|
+
return `- \`${s.id}\` ${s.title} — ${s.label}${s.availability ? ` · ${s.availability}` : ''}${history}${code ? ' · a code file: open the current version and read it there' : ''}${excerpt ? `\n “${excerpt.split('\n').join('\n ')}”` : ''}`;
|
|
88
|
+
}), how];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** What the agent entry receives for a source id (Spec §7.10 item 3; CKC-12 AC-22, AC-43). */
|
|
92
|
+
export interface OriginalData {
|
|
93
|
+
readonly title: string;
|
|
94
|
+
readonly kind: string; // the anchor's kind: file, session, commit …
|
|
95
|
+
readonly label: string;
|
|
96
|
+
readonly readAt: string;
|
|
97
|
+
readonly commit: string | null;
|
|
98
|
+
readonly fingerprint: string;
|
|
99
|
+
readonly usedAs: string | null;
|
|
100
|
+
readonly origin: { readonly relation: string; readonly path: string } | null;
|
|
101
|
+
readonly availabilityNow: string | null;
|
|
102
|
+
readonly currentState: 'same' | 'changed' | 'missing';
|
|
103
|
+
readonly hasCredential: boolean;
|
|
104
|
+
readonly excerpt: string;
|
|
105
|
+
readonly lineStart: number;
|
|
106
|
+
/** Set for a code file: where it is — repository, file, lines, commit — given in place of its lines (D65). */
|
|
107
|
+
readonly code: { readonly location: string; readonly file: string } | null;
|
|
108
|
+
readonly otherParts: readonly { readonly id: string; readonly label: string }[];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* A source as the agent entry gives it: the same as the workbench's original reading — title, kind, location, when it
|
|
113
|
+
* was read, version, the excerpt with line numbers, and what changed since — except for a code file, which gives where
|
|
114
|
+
* it is, the version read and whether it changed since, and tells the agent to open the current version (D65).
|
|
115
|
+
*/
|
|
116
|
+
export function sourceOriginal(d: OriginalData): string {
|
|
117
|
+
const version = `${d.commit ? `${d.commit.slice(0, 10)} · ` : ''}${d.fingerprint.slice(0, 23)}`;
|
|
118
|
+
const credential = d.hasCredential ? 'Credential: this source contains a credential; its value is not shown or stored.' : null;
|
|
119
|
+
const origin = d.origin ? `Origin: ${d.origin.relation} · ${d.origin.path}` : null;
|
|
120
|
+
const parts = d.otherParts.length ? ['', `Other parts of this document (${d.otherParts.length}), by id:`, ...d.otherParts.map((x) => `- ${x.id} ${x.label}`)] : [];
|
|
121
|
+
if (d.code) {
|
|
122
|
+
return keep([
|
|
123
|
+
`# ${d.title}`, `Kind: ${d.kind} · code`, `Location: ${d.code.location}`, `Read at: ${d.readAt}`, `Version when read: ${version}`, `Used as: ${d.usedAs ?? 'Not yet judged'}`, origin,
|
|
124
|
+
d.currentState === 'same' ? 'Unchanged since it was read.' : d.currentState === 'changed' ? 'Changed since it was read: the file at this path is no longer the version above.' : 'No longer at this path: it was moved or deleted after it was read.',
|
|
125
|
+
credential,
|
|
126
|
+
`\nThis is a code file. ProjectKeeper gives where code is, not the code: open the current version of ${d.code.file} in the repository and read it there.`,
|
|
127
|
+
...parts,
|
|
128
|
+
]);
|
|
129
|
+
}
|
|
130
|
+
const lines = d.excerpt.split('\n');
|
|
131
|
+
const width = String(d.lineStart + lines.length - 1).length;
|
|
132
|
+
return [
|
|
133
|
+
`# ${d.title}`, `Kind: ${d.kind}`, `Location: ${d.label}`, `Read at: ${d.readAt}`, `Version: ${version}`, `Used as: ${d.usedAs ?? 'Not yet judged'}`,
|
|
134
|
+
origin, d.availabilityNow ? `Availability: ${d.availabilityNow}` : null, credential,
|
|
135
|
+
d.currentState === 'changed' ? 'This source changed after it was read; the excerpt below is what was read.' : d.currentState === 'missing' ? 'This source is no longer available at its path; the excerpt below is what was read.' : null,
|
|
136
|
+
'', ...(d.excerpt ? lines.map((l, i) => `${String(d.lineStart + i).padStart(width)} | ${l}`) : ['(No text was read from this source: it is binary or could not be read as text; nothing is reproduced.)']),
|
|
137
|
+
'', 'Excerpts keep the original text. Interpretations and relations built on them can be corrected; the original is never changed.',
|
|
138
|
+
...parts,
|
|
139
|
+
].filter((l) => l !== null).join('\n');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function relationLines(relations: readonly BriefRelation[] | undefined, selfId: string, alreadyListed: ReadonlySet<string>): string[] {
|
|
143
|
+
// Every change record that reaches an object also hangs off it as an `affects` relation; it is listed once, under
|
|
144
|
+
// the change records, not twice.
|
|
145
|
+
const shown = (relations ?? []).filter((r) => !(r.type === 'affects' && alreadyListed.has(r.from === selfId ? r.to : r.from)));
|
|
146
|
+
if (!shown.length) return [];
|
|
147
|
+
return [`\n## Relations (${shown.length})`, ...shown.map((r) => {
|
|
148
|
+
const outgoing = r.from === selfId;
|
|
149
|
+
const otherId = outgoing ? r.to : r.from;
|
|
150
|
+
const otherLabel = (outgoing ? r.toLabel : r.fromLabel) ?? otherId;
|
|
151
|
+
// Only a questioned assessment is written; `Holds` and `Not assessed` are normal states and stay in the assets (D59).
|
|
152
|
+
return `- ${outgoing ? '' : '← '}${r.type} ${otherLabel} (\`${otherId}\`)${r.basis === 'Inferred' ? ' · Inferred' : ''}${r.assessment === 'Questioned' ? ' · Questioned' : ''}${text(r.claim) ? `: ${text(r.claim)}` : ''}`;
|
|
153
|
+
})];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* `Update pending` says which changes the object waits for and when they are taken in (Spec §7.1, §7.6): each change by
|
|
158
|
+
* name with the id to read it now and how it reaches the object. Nobody has to wait for the round to check.
|
|
159
|
+
*/
|
|
160
|
+
function pendingLines(d: BriefNode): string[] {
|
|
161
|
+
const waits = d.waitsFor ?? [];
|
|
162
|
+
if (!waits.length) return [];
|
|
163
|
+
return [
|
|
164
|
+
`\n## Update pending (${waits.length})`,
|
|
165
|
+
...waits.map((w) => `- ${w.label} (${w.sourceIds.length ? w.sourceIds.map((id) => `\`${id}\``).join(', ') : `\`${w.ref}\``})${w.reasons.length ? ` — ${w.reasons.map((r) => r.detail).join('; ')}` : ''}`),
|
|
166
|
+
`What is written above may be out of date until these are taken in${d.pendingWhen ? `, ${d.pendingWhen}` : ''}. Read any of them now with \`pk get <id>\`; asking the Keeper about this object reads them in.`,
|
|
167
|
+
];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function neighbourLines(d: BriefNode): string[] {
|
|
171
|
+
const out: string[] = [];
|
|
172
|
+
if (d.changes && d.changes.length) {
|
|
173
|
+
out.push(`\n## Change records that reach it (${d.changes.length})`, ...d.changes.map((c) => `- ${c.at.slice(0, 10)} · ${c.effect} · ${c.title} (\`${c.id}\`)`), 'Each by id with `pk get`; the record says what changed, why, and which objects have not followed it.');
|
|
174
|
+
}
|
|
175
|
+
if (d.notes && d.notes.length) out.push(`\n## Notes on it (${d.notes.length})`, ...d.notes.map((n) => `- ${n.title} (\`${n.id}\`${n.ask ? ` · ${n.ask}` : ''})`), 'The whole note: `pk get <note id>`.');
|
|
176
|
+
const marks = (d.marks ?? []).filter((m) => text(m.clue).length > 0);
|
|
177
|
+
if (marks.length) out.push(`\n## Entry marks on it (${marks.length})`, ...marks.map((m) => `- ${m.kind} (\`${m.id}\`): ${text(m.clue)}`));
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* A change record, without its whole propagation list (D59; Spec §7.10): the piece of work it is and each of its net
|
|
183
|
+
* changes — what changed, why, what it changed (QC AH #4: only the record's first line reached the agent) — then what
|
|
184
|
+
* has not followed it, and the rest counted.
|
|
185
|
+
*/
|
|
186
|
+
export function changeBrief(c: BriefChange): string {
|
|
187
|
+
const prop = c.propagation ?? [];
|
|
188
|
+
// An object that has not followed the change is usually not one the change itself altered, so its name comes from
|
|
189
|
+
// the propagation labels; `affects` covers the ones it did alter.
|
|
190
|
+
const labelOf = (id: string) => c.propagationLabels?.find((a) => a.id === id)?.label ?? c.affectsLabels?.find((a) => a.id === id)?.label ?? c.removed?.find((a) => a.id === id)?.label ?? id;
|
|
191
|
+
// What it reached that has since been removed from the current version is named as removed (CKC-12 AC-35).
|
|
192
|
+
const named = (id: string) => `${labelOf(id)} (\`${id}\`${c.removed?.some((r) => r.id === id) ? ', removed from the current version' : ''})`;
|
|
193
|
+
const behind = prop.filter((p) => p.state === 'Still on old understanding');
|
|
194
|
+
const rest = new Map<string, number>();
|
|
195
|
+
for (const p of prop) if (p.state !== 'Still on old understanding') rest.set(p.state, (rest.get(p.state) ?? 0) + 1);
|
|
196
|
+
const items = c.items ?? [];
|
|
197
|
+
const byWork = c.work && items.length > 0;
|
|
198
|
+
const itemLines = (it: BriefChangeItem) => keep([
|
|
199
|
+
`- ${it.at} · ${it.effect}${it.material ? ` · ${it.material}` : ''} · **${text(it.title)}**`,
|
|
200
|
+
text(it.summary) && text(it.summary) !== text(it.title) ? ` ${text(it.summary).split('\n').join('\n ')}` : null,
|
|
201
|
+
it.before || it.after ? ` Before: ${text(it.before) || '—'} → after: ${text(it.after) || '—'}` : null,
|
|
202
|
+
text(it.why) ? ` Why: ${text(it.why)}` : null,
|
|
203
|
+
it.affects && it.affects.length ? ` Affects: ${it.affects.map(named).join('; ')}` : null,
|
|
204
|
+
]);
|
|
205
|
+
return keep([
|
|
206
|
+
`# ${c.title} (\`${c.id}\`)`,
|
|
207
|
+
byWork
|
|
208
|
+
? `Change record of one piece of work · ${c.work!.kind}: ${c.work!.label}${c.work!.startedAt ? ` · ${c.work!.startedAt}${c.work!.endedAt && c.work!.endedAt !== c.work!.startedAt ? ` → ${c.work!.endedAt}` : ''}` : ''}${c.work!.openEnded ? ' · still going when its round began' : ''}`
|
|
209
|
+
: `Change · ${c.effect}${c.material ? ` · ${c.material}` : ''} · ${c.at}${c.segment ? ` · from ${c.segment.name}` : ''}`,
|
|
210
|
+
'',
|
|
211
|
+
...(byWork
|
|
212
|
+
? [`## Its net changes (${items.length})`, ...items.map(itemLines)]
|
|
213
|
+
: [
|
|
214
|
+
text(c.summary),
|
|
215
|
+
c.before || c.after ? `\nBefore: ${text(c.before) || '—'} → after: ${text(c.after) || '—'}` : null,
|
|
216
|
+
c.affectsLabels && c.affectsLabels.length ? `\nAffects: ${[...new Map(c.affectsLabels.map((a) => [a.id, a])).values()].map((a) => named(a.id)).join('; ')}` : null,
|
|
217
|
+
]),
|
|
218
|
+
c.notJudgedLabels && c.notJudgedLabels.length ? `\nRecorded at the time, not judged: ${c.notJudgedLabels.map((a) => named(a.id)).join('; ')}` : null,
|
|
219
|
+
c.sources && c.sources.length ? `${byWork ? '\n' : ''}Sources: ${c.sources.map((s) => `${s.title} (\`${s.id}\`) — ${s.label}`).join('; ')}` : null,
|
|
220
|
+
behind.length ? `\n## Not followed yet (${behind.length})\n${behind.map((p) => `- ${labelOf(p.nodeId)} (\`${p.nodeId}\`)${text(p.sourceOrReason) ? ` — ${text(p.sourceOrReason)}` : ''}`).join('\n')}` : null,
|
|
221
|
+
rest.size ? `\nThe other ${[...rest.values()].reduce((a, b) => a + b, 0)} object(s) this record reached: ${[...rest].map(([state, n]) => `${state} ${n}`).join(', ')}. Those are the normal states; they stay in the assets, and \`pk get <id>\` reads any one of them.` : (behind.length ? null : '\nNo object is recorded as still on the old understanding of this change.'),
|
|
222
|
+
]);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** A fact record: its statements and open questions in full, its sources by id. */
|
|
226
|
+
export function factBrief(f: BriefFact): string {
|
|
227
|
+
return keep([
|
|
228
|
+
`# ${f.title} (\`${f.id}\`)`,
|
|
229
|
+
`Fact record${f.asOf ? ` · as of ${f.asOf}` : ''}`,
|
|
230
|
+
'',
|
|
231
|
+
...(f.statements ?? []).map((s) => `- ${s.type}: ${text(s.text)}${s.sourceIds && s.sourceIds.length ? ` [${s.sourceIds.join(', ')}]` : ''}`),
|
|
232
|
+
(f.openQuestions ?? []).length ? `\n## Still open\n${(f.openQuestions ?? []).map((q) => `- ${q}`).join('\n')}` : null,
|
|
233
|
+
...sourceLines(f.sources, 'Each source by id with `pk get`, which gives the original with line numbers.'),
|
|
234
|
+
]);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** A relation on its own: what it claims, on what evidence, and both ends by name and id. */
|
|
238
|
+
export function relationBrief(r: BriefRelation & { readonly evidence?: { readonly factsSoFar?: string; readonly sourceIds?: readonly string[] }; readonly evidenceSources?: readonly BriefSource[]; readonly notes?: readonly BriefNote[]; readonly assessedAt?: string | null }): string {
|
|
239
|
+
return keep([
|
|
240
|
+
`# ${r.fromLabel ?? r.from} ${r.type} ${r.toLabel ?? r.to} (\`${r.id}\`)`,
|
|
241
|
+
`From ${r.fromLabel ?? r.from} (\`${r.from}\`) to ${r.toLabel ?? r.to} (\`${r.to}\`)${r.basis === 'Inferred' ? ' · Inferred' : ''}${r.assessment === 'Questioned' ? ` · Questioned${r.assessedAt ? ` on ${r.assessedAt}` : ''}` : ''}`,
|
|
242
|
+
'',
|
|
243
|
+
text(r.claim),
|
|
244
|
+
line('So far', r.evidence?.factsSoFar),
|
|
245
|
+
...sourceLines(r.evidenceSources, 'Each source by id with `pk get`.'),
|
|
246
|
+
(r.notes ?? []).length ? `\n## Notes on it (${(r.notes ?? []).length})\n${(r.notes ?? []).map((n) => `- ${n.title} (\`${n.id}\`)`).join('\n')}` : null,
|
|
247
|
+
]);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Any other object of the graph — a product reference item, an area, a work item, a change, a session or a result —
|
|
252
|
+
* as the workbench's object detail says it, in prose.
|
|
253
|
+
*/
|
|
254
|
+
export function nodeBrief(d: BriefNode): string {
|
|
255
|
+
const n = d.node;
|
|
256
|
+
const selfId = n?.id ?? '';
|
|
257
|
+
if (d.change) return changeBrief(d.change);
|
|
258
|
+
const r = d.reference;
|
|
259
|
+
const t = d.thread;
|
|
260
|
+
const name = r?.name ?? t?.title ?? n?.label ?? selfId;
|
|
261
|
+
const ids = r?.ids ?? t?.ids ?? [];
|
|
262
|
+
const category = r?.category ?? n?.category ?? 'Object';
|
|
263
|
+
const validity = r?.validity ?? t?.validity ?? n?.validity ?? '';
|
|
264
|
+
const progress = t?.progress ?? n?.progress ?? '';
|
|
265
|
+
const acceptance = t?.acceptance ?? n?.acceptance ?? '';
|
|
266
|
+
const holder = n?.attribution?.holder?.role ?? (n?.attribution?.author?.kind === 'role' ? n.attribution.author.name : null);
|
|
267
|
+
const head = [
|
|
268
|
+
category,
|
|
269
|
+
validity && validity !== 'Current' ? validity : '', // `Current` is the normal state and is not written (D59)
|
|
270
|
+
progress,
|
|
271
|
+
acceptance,
|
|
272
|
+
holder ? `held by ${holder}` : '',
|
|
273
|
+
(r?.basis ?? n?.basis) === 'Inferred' ? 'Inferred' : '',
|
|
274
|
+
d.updatePending ? 'Update pending' : '',
|
|
275
|
+
].filter((x) => text(x).length).join(' · ');
|
|
276
|
+
return keep([
|
|
277
|
+
`# ${name}${ids.length ? ` (${ids.join(', ')})` : ''} (\`${selfId}\`)`,
|
|
278
|
+
head,
|
|
279
|
+
r?.asOf || n?.updatedAt ? `As of ${r?.asOf ?? n?.updatedAt}` : null,
|
|
280
|
+
'',
|
|
281
|
+
// The object's own content, whole (AC-31).
|
|
282
|
+
text(r?.text),
|
|
283
|
+
r?.quote ? `Owner’s words: “${text(r.quote)}”${r.answers ? ` — in answer to: “${text(r.answers)}”` : ''}` : null,
|
|
284
|
+
r?.replacedBy ? `Replaced by \`${r.replacedBy}\` — \`pk get ${r.replacedBy}\` reads it.` : null,
|
|
285
|
+
d.area ? keep(['\n## Area understanding', line('Now', d.area.effectNow), line('Still missing', d.area.gaps)]) : null,
|
|
286
|
+
t ? keep([
|
|
287
|
+
line('Doing', t.doing), line('Done means', t.doneMeans), line('Owner acceptance means', t.acceptanceMeans),
|
|
288
|
+
line('Changed on the way', t.changed), line('Results', t.results), line('Unresolved', t.unresolved),
|
|
289
|
+
(t.servesLabels ?? []).length ? `Serves: ${(t.servesLabels ?? []).map((s) => `${s.name} (\`${s.referenceId}\`)${text(s.claim) ? ` — ${text(s.claim)}` : ''}`).join('; ')}` : null,
|
|
290
|
+
(t.dependsOn ?? []).length ? `Depends on: ${(t.dependsOn ?? []).map((x) => `\`${x.threadId}\`${text(x.claim) ? ` — ${text(x.claim)}` : ''}`).join('; ')}` : null,
|
|
291
|
+
[...(t.executionFacts ?? []), ...(t.qcFacts ?? [])].length ? `\n## Results and failures recorded\n${[...(t.executionFacts ?? []), ...(t.qcFacts ?? [])].map((s) => `- ${s.type}: ${text(s.text)}`).join('\n')}` : null,
|
|
292
|
+
(t.facts ?? []).length ? `\nFact records behind it: ${(t.facts ?? []).map((f) => `${f.title} (\`${f.id}\`)`).join('; ')}` : null,
|
|
293
|
+
]) : null,
|
|
294
|
+
...pendingLines(d),
|
|
295
|
+
...sourceLines(d.sources, 'Each source by id with `pk get`, which gives the original with line numbers and says whether it changed since it was read.'),
|
|
296
|
+
...relationLines(d.relations, selfId, new Set((d.changes ?? []).map((c) => c.id))),
|
|
297
|
+
...neighbourLines(d),
|
|
298
|
+
'\nEvery id above is read with `pk get <id>`. A work item or an area gives its context pack; anything else gives a page like this one.',
|
|
299
|
+
]);
|
|
300
|
+
}
|