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,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Keeper's ledger tools (Spec §1.16; CKC-22 AC-13; CKC-03 AC-26, AC-27): exact answers from the project's ledger —
|
|
3
|
+
* the history map, commits and merges, document versions and their section diffs, deleted documents and cleanups, the
|
|
4
|
+
* lines that say something was superseded, numbering, verdicts, execution arrangements, words across all history, file
|
|
5
|
+
* and symbol references, code sizes, sessions and the owner's words, how something got here, and what the ledger covers —
|
|
6
|
+
* instead of counting with the shell. Every tool is offered to every step of a round (roles.ts `pk_ledger_*`).
|
|
7
|
+
*
|
|
8
|
+
* The tools are a view on the ledger, not an interpretation: it says "this line reads superseded by", never "this design
|
|
9
|
+
* is superseded"; "no file references this file", never "this is residue". Every row carries its entry id — cite it as
|
|
10
|
+
* evidence `{ kind: 'ledger', id }` and the program reads its label and time back — and when it happened with the basis
|
|
11
|
+
* (§2.11). Lists are paged (limit, offset; `next` says where the next page starts). The ledger is brought up to date by
|
|
12
|
+
* the round's own program step; these tools only read it.
|
|
13
|
+
*/
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { Type } from 'typebox';
|
|
16
|
+
import { defineTool, type ToolDefinition } from '@earendil-works/pi-coding-agent';
|
|
17
|
+
import { Ledger, type LedgerEntry } from '../ledger/index.ts';
|
|
18
|
+
import type { Occurred } from '../model/k-types.ts';
|
|
19
|
+
import type { ToolContext } from './tools.ts';
|
|
20
|
+
|
|
21
|
+
const MAX_CHARS = 120_000;
|
|
22
|
+
|
|
23
|
+
function ok(value: unknown) {
|
|
24
|
+
const body = typeof value === 'string' ? value : JSON.stringify(compact(value), null, 1);
|
|
25
|
+
return { content: [{ type: 'text' as const, text: body.length > MAX_CHARS ? `${body.slice(0, MAX_CHARS)}\n…(cut at ${MAX_CHARS} characters: ask for a smaller page with limit)` : body }], details: {} };
|
|
26
|
+
}
|
|
27
|
+
function fail(message: string) {
|
|
28
|
+
return { content: [{ type: 'text' as const, text: `ERROR: ${message}` }], details: {}, isError: true };
|
|
29
|
+
}
|
|
30
|
+
const str = (v: unknown): string => (typeof v === 'string' ? v.trim() : '');
|
|
31
|
+
const opt = (v: unknown): string | undefined => str(v) || undefined;
|
|
32
|
+
const num = (v: unknown): number | undefined => (typeof v === 'number' && Number.isFinite(v) ? v : typeof v === 'string' && /^\d+$/.test(v) ? Number(v) : undefined);
|
|
33
|
+
const strs = (v: unknown): string[] => (Array.isArray(v) ? v.filter((x): x is string => typeof x === 'string' && x.trim() !== '').map((x) => x.trim()) : []);
|
|
34
|
+
|
|
35
|
+
/** When it happened, in one line: `2026-09-11T20:41:22Z Commit` (+ the other time, + undated). */
|
|
36
|
+
export function when(o: Occurred | null | undefined): string | null {
|
|
37
|
+
if (!o) return null;
|
|
38
|
+
const at = o.at.replace(/\.000Z$/, 'Z');
|
|
39
|
+
const other = o.other ? `; ${o.other.basis} ${o.other.at.replace(/\.000Z$/, 'Z')}` : '';
|
|
40
|
+
return o.undated ? `Undated · first seen ${at}${other}` : `${at} ${o.basis}${other}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Rows as the model reads them: `occurred` in one line, empty fields left out. */
|
|
44
|
+
function compact(value: unknown): unknown {
|
|
45
|
+
if (Array.isArray(value)) return value.map(compact);
|
|
46
|
+
if (value === null || typeof value !== 'object') return value;
|
|
47
|
+
const o = value as Record<string, unknown>;
|
|
48
|
+
if (typeof o.at === 'string' && typeof o.basis === 'string' && ('anchor' in o || 'undated' in o || 'other' in o)) return when(o as unknown as Occurred);
|
|
49
|
+
const out: Record<string, unknown> = {};
|
|
50
|
+
for (const [k, v] of Object.entries(o)) {
|
|
51
|
+
if (v === null || v === undefined || (Array.isArray(v) && v.length === 0)) continue;
|
|
52
|
+
out[k === 'occurred' ? 'when' : k] = compact(v);
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const NO_LEDGER = 'This project has no ledger yet: it is built by the program step that opens each round (step 0). Until then, read git and the files directly.';
|
|
58
|
+
|
|
59
|
+
export function ledgerOf(ctx: Pick<ToolContext, 'store'>): Ledger | null {
|
|
60
|
+
return Ledger.openDir(ctx.store.dir);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function withLedger(ctx: ToolContext, fn: (ledger: Ledger) => unknown) {
|
|
64
|
+
const ledger = ledgerOf(ctx);
|
|
65
|
+
if (!ledger) return fail(NO_LEDGER);
|
|
66
|
+
try {
|
|
67
|
+
const r = fn(ledger);
|
|
68
|
+
return typeof r === 'string' ? fail(r) : ok(r);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
return fail(`The ledger could not answer: ${(error as Error).message}`);
|
|
71
|
+
} finally {
|
|
72
|
+
ledger.close();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const REPO = { repo: Type.Optional(Type.String({ description: 'a repository of the project: its scope item id or path; default all (or the main one where one is needed)' })) };
|
|
77
|
+
const PAGE = {
|
|
78
|
+
limit: Type.Optional(Type.Number({ description: 'rows per page (default 50)' })),
|
|
79
|
+
offset: Type.Optional(Type.Number({ description: 'rows to skip: the `next` of the previous page' })),
|
|
80
|
+
};
|
|
81
|
+
const RANGE = {
|
|
82
|
+
since: Type.Optional(Type.String({ description: 'from this date or time (a date: the start of that day, UTC)' })),
|
|
83
|
+
until: Type.Optional(Type.String({ description: 'up to this date or time (a date: the end of that day, UTC)' })),
|
|
84
|
+
};
|
|
85
|
+
const CITE = 'Each row has an id: cite it as evidence { kind: "ledger", id } (with line when you quote one line of it); the program reads its label and when it happened.';
|
|
86
|
+
|
|
87
|
+
export function ledgerTools(ctx: ToolContext): ToolDefinition[] {
|
|
88
|
+
return [
|
|
89
|
+
defineTool({
|
|
90
|
+
name: 'pk_ledger_overview', label: 'History map',
|
|
91
|
+
description: `The orientation map of the whole history, per repository, in one call: commits on all refs and on the trunk, merges, refs by namespace (heads, tags, remotes, stash, others such as refs/codex/…), the time span and commits per day, every branch that never entered the trunk (with how far ahead it is), the worktrees and their uncommitted changes, cleanup commits, deleted documents by directory, import-style root commits and the day document version history starts. Exact and complete — no recent-N cap. ${CITE}`,
|
|
92
|
+
parameters: Type.Object({ ...REPO }),
|
|
93
|
+
execute: async (_id, p) => withLedger(ctx, (l) => l.overview({ repo: opt(p.repo) })),
|
|
94
|
+
}),
|
|
95
|
+
defineTool({
|
|
96
|
+
name: 'pk_ledger_commits', label: 'Find commits',
|
|
97
|
+
description: `Commits of the whole history (every ref), newest first unless oldestFirst. Filters: path (a file, or a directory and everything under it; a move counts), keyword in the message, num (a project number the message names: D83, T-22, AB …), author, since/until, merges, trunk (reachable from the trunk), sideOnly (never reached the trunk), importRoots. Each row: id, subject, author, when (author time; the committer time too when it differs), parents, merge, onTrunk, which ref namespaces reach it, files changed. ${CITE}`,
|
|
98
|
+
parameters: Type.Object({
|
|
99
|
+
...REPO, ...PAGE, ...RANGE,
|
|
100
|
+
path: Type.Optional(Type.String()), keyword: Type.Optional(Type.String()), num: Type.Optional(Type.String()), author: Type.Optional(Type.String()),
|
|
101
|
+
merges: Type.Optional(Type.Boolean()), trunk: Type.Optional(Type.Boolean()), sideOnly: Type.Optional(Type.Boolean()), importRoots: Type.Optional(Type.Boolean()),
|
|
102
|
+
oldestFirst: Type.Optional(Type.Boolean()),
|
|
103
|
+
}),
|
|
104
|
+
execute: async (_id, p) => withLedger(ctx, (l) => l.commits({
|
|
105
|
+
repo: opt(p.repo), path: opt(p.path), keyword: opt(p.keyword), num: opt(p.num), author: opt(p.author), since: opt(p.since), until: opt(p.until),
|
|
106
|
+
merges: p.merges === true, trunk: p.trunk === true, sideOnly: p.sideOnly === true, importRoots: p.importRoots === true, oldestFirst: p.oldestFirst === true,
|
|
107
|
+
limit: num(p.limit), offset: num(p.offset),
|
|
108
|
+
})),
|
|
109
|
+
}),
|
|
110
|
+
defineTool({
|
|
111
|
+
name: 'pk_ledger_commit', label: 'One commit',
|
|
112
|
+
description: `One commit in full: its message, author and committer time, parents, the project numbers it names, every file it changed against its first parent (status, move source, lines added and deleted — for a merge, what it brought in; paged), the branches that contain it, and the merge that brought it into the trunk. ${CITE}`,
|
|
113
|
+
parameters: Type.Object({ hash: Type.String({ description: 'full or short hash, or its ledger id commit:<hash>' }), ...REPO, ...PAGE }),
|
|
114
|
+
execute: async (_id, p) => withLedger(ctx, (l) => l.commit(str(p.hash), { repo: opt(p.repo), limit: num(p.limit) ?? 300, offset: num(p.offset) })),
|
|
115
|
+
}),
|
|
116
|
+
defineTool({
|
|
117
|
+
name: 'pk_ledger_doc_versions', label: 'Versions of documents',
|
|
118
|
+
description: `With path: every version of that document, oldest first, followed across moves — the commit and when, Added / Modified / Renamed / Copied, which Markdown sections (by heading path) that version added, removed or changed against the version before it, whether it is on the trunk, which one is current — and each deletion with the commit whose tree still has the full text. With compare: [from, to] (two version ids, or commits of the path): the sections that differ between any two versions. Without path: the document versions made in a period (since/until) or under a directory (dir), oldest first, paged. Read a version with pk_ledger_doc_read. ${CITE}`,
|
|
119
|
+
parameters: Type.Object({
|
|
120
|
+
path: Type.Optional(Type.String({ description: 'repository-relative path' })), ...REPO, ...PAGE, ...RANGE,
|
|
121
|
+
dir: Type.Optional(Type.String()), change: Type.Optional(Type.String({ description: 'Added | Modified | Renamed | Copied' })),
|
|
122
|
+
compare: Type.Optional(Type.Array(Type.String(), { description: 'two versions: ids (doc:<path>@<commit>) or commits of path' })),
|
|
123
|
+
}),
|
|
124
|
+
execute: async (_id, p) => withLedger(ctx, (l) => {
|
|
125
|
+
const cmp = strs(p.compare);
|
|
126
|
+
const ref = (v: string) => (v.startsWith('doc:') ? { id: v } : { path: str(p.path), commit: v, repo: opt(p.repo) });
|
|
127
|
+
if (cmp.length === 2) return l.docCompare(ref(cmp[0]!), ref(cmp[1]!));
|
|
128
|
+
if (cmp.length) return 'compare takes two versions.';
|
|
129
|
+
if (opt(p.path)) return l.docVersions(str(p.path), { repo: opt(p.repo) });
|
|
130
|
+
return l.docChanges({ repo: opt(p.repo), dir: opt(p.dir), since: opt(p.since), until: opt(p.until), change: opt(p.change), limit: num(p.limit), offset: num(p.offset) });
|
|
131
|
+
}),
|
|
132
|
+
}),
|
|
133
|
+
defineTool({
|
|
134
|
+
name: 'pk_ledger_doc_read', label: 'Read a document version',
|
|
135
|
+
description: `The full text of one version of a document — a deleted design, an earlier plan, a version on a side branch — by its version id (doc:<path>@<commit>) or by path and commit (for a deleted document, its readableAt commit). Paged by lines: fromLine, toLine; nextFromLine says where the next page starts. Says whether it is the current version. What only history has is history: mark it so wherever you use it. Cite a line of it as { kind: "ledger", id, line }.`,
|
|
136
|
+
parameters: Type.Object({
|
|
137
|
+
id: Type.Optional(Type.String()), path: Type.Optional(Type.String()), commit: Type.Optional(Type.String()), ...REPO,
|
|
138
|
+
fromLine: Type.Optional(Type.Number()), toLine: Type.Optional(Type.Number()),
|
|
139
|
+
}),
|
|
140
|
+
execute: async (_id, p) => withLedger(ctx, (l) => l.docText({ id: opt(p.id), path: opt(p.path), commit: opt(p.commit), repo: opt(p.repo) }, { from: num(p.fromLine), to: num(p.toLine) })),
|
|
141
|
+
}),
|
|
142
|
+
defineTool({
|
|
143
|
+
name: 'pk_ledger_deleted', label: 'Deleted documents and cleanups',
|
|
144
|
+
description: `Documents the history deleted that the current version does not have again, newest deletion first (paged; dir narrows to a directory), each with the deleting commit and readableAt, the commit whose tree still has the full text. With cleanups: true, the cleanup commits instead (one commit deleting many documents), each with the documents removed and the catalogue size before and after; with cleanup set to one cleanup's id, its whole catalogue before and after. ${CITE}`,
|
|
145
|
+
parameters: Type.Object({ ...REPO, ...PAGE, dir: Type.Optional(Type.String()), cleanups: Type.Optional(Type.Boolean()), cleanup: Type.Optional(Type.String()), onlyInCleanups: Type.Optional(Type.Boolean()) }),
|
|
146
|
+
execute: async (_id, p) => withLedger(ctx, (l) => (p.cleanups === true || opt(p.cleanup)
|
|
147
|
+
? l.cleanups({ repo: opt(p.repo), id: opt(p.cleanup) })
|
|
148
|
+
: l.deletedDocs({ repo: opt(p.repo), dir: opt(p.dir), cleanupsOnly: p.onlyInCleanups === true, limit: num(p.limit), offset: num(p.offset) }))),
|
|
149
|
+
}),
|
|
150
|
+
defineTool({
|
|
151
|
+
name: 'pk_ledger_supersessions', label: 'Lines that say superseded',
|
|
152
|
+
description: `Every line of the material that says something was superseded, replaced, deprecated, withdrawn — superseded by, replaced by, deprecated, 取代, 被…取代, 作废, 废弃, 不再使用, 撤回, V1 → V2 — and every row of the project's own obsolete lists (a section headed or introduced as obsolete, e.g. 已纠正的旧规则), in document bodies (every version), file and directory names, and commit messages. Each with where it is, the first version it appears in (its time), the last, and whether the current version still has it. It records what the line says; whether it really supersedes something, and what, is your judgement. Filters: path, pattern, obsoleteOnly, currentOnly, source (content | commit | filename | loose), keyword. ${CITE}`,
|
|
153
|
+
parameters: Type.Object({
|
|
154
|
+
...REPO, ...PAGE, ...RANGE, path: Type.Optional(Type.String()), pattern: Type.Optional(Type.String()), obsoleteOnly: Type.Optional(Type.Boolean()),
|
|
155
|
+
currentOnly: Type.Optional(Type.Boolean()), source: Type.Optional(Type.String()), keyword: Type.Optional(Type.String()),
|
|
156
|
+
}),
|
|
157
|
+
execute: async (_id, p) => withLedger(ctx, (l) => l.supersessions({
|
|
158
|
+
repo: opt(p.repo), path: opt(p.path), pattern: opt(p.pattern), obsoleteOnly: p.obsoleteOnly === true, currentOnly: p.currentOnly === true,
|
|
159
|
+
source: opt(p.source), keyword: opt(p.keyword), since: opt(p.since), until: opt(p.until), limit: num(p.limit), offset: num(p.offset),
|
|
160
|
+
})),
|
|
161
|
+
}),
|
|
162
|
+
defineTool({
|
|
163
|
+
name: 'pk_ledger_numbers', label: 'Numbering',
|
|
164
|
+
description: `The project's numbering. Without num: the rules the program recognised (D<n>, CKC-<n>, two-letter prompt or task numbers …), each with the basis it was recognised by and how often it occurs. With num: every place that number shows up — documents (each line once, with the first and last version it is in and whether the current version has it; place definition or mention), commit messages, branch, worktree and file names — oldest first. Filters: kind (doc | commit | branch | worktree | file-name | loose), place, path, currentOnly, since/until. A two-letter number that is also an ordinary word (AI, AM …) is a candidate in prose. ${CITE}`,
|
|
165
|
+
parameters: Type.Object({
|
|
166
|
+
num: Type.Optional(Type.String({ description: 'the number as written: D83, T-22, AB' })), rule: Type.Optional(Type.String()), ...REPO, ...PAGE, ...RANGE,
|
|
167
|
+
kind: Type.Optional(Type.String()), place: Type.Optional(Type.String()), path: Type.Optional(Type.String()), currentOnly: Type.Optional(Type.Boolean()),
|
|
168
|
+
}),
|
|
169
|
+
execute: async (_id, p) => withLedger(ctx, (l) => (!opt(p.num) && !opt(p.rule) && !opt(p.path)
|
|
170
|
+
? l.numRules()
|
|
171
|
+
: l.nums({ num: opt(p.num), rule: opt(p.rule), repo: opt(p.repo), kind: opt(p.kind), place: opt(p.place), path: opt(p.path), currentOnly: p.currentOnly === true, since: opt(p.since), until: opt(p.until), limit: num(p.limit), offset: num(p.offset) }))),
|
|
172
|
+
}),
|
|
173
|
+
defineTool({
|
|
174
|
+
name: 'pk_ledger_verdicts', label: 'Verdicts in reports',
|
|
175
|
+
description: `What QC, review, walkthrough and test reports state: verdicts (pass, fail, incomplete, needs-repair, partial …, as normalised from 通过 / 不通过 / 需修复 / 做到 / 不成立 …), test counts (511/511, "12 passed"), and numbered findings (F-1, D-3 …), each with its line, where it is, the first version it appears in and whether the current version still has it. confidence: stated (a verdict field such as 结论:fail) or candidate (a verdict word in a table row or a heading — yours to confirm). Filters: path, verdict, kind (verdict | count | finding), confidence, keyword (a work number, a scenario), currentOnly. ${CITE}`,
|
|
176
|
+
parameters: Type.Object({
|
|
177
|
+
...REPO, ...PAGE, ...RANGE, path: Type.Optional(Type.String()), verdict: Type.Optional(Type.String()), kind: Type.Optional(Type.String()),
|
|
178
|
+
confidence: Type.Optional(Type.String()), keyword: Type.Optional(Type.String()), currentOnly: Type.Optional(Type.Boolean()),
|
|
179
|
+
}),
|
|
180
|
+
execute: async (_id, p) => withLedger(ctx, (l) => l.verdicts({
|
|
181
|
+
repo: opt(p.repo), path: opt(p.path), verdict: opt(p.verdict), kind: opt(p.kind), confidence: opt(p.confidence), keyword: opt(p.keyword),
|
|
182
|
+
currentOnly: p.currentOnly === true, since: opt(p.since), until: opt(p.until), limit: num(p.limit), offset: num(p.offset),
|
|
183
|
+
})),
|
|
184
|
+
}),
|
|
185
|
+
defineTool({
|
|
186
|
+
name: 'pk_ledger_arrangements', label: 'Execution arrangements',
|
|
187
|
+
description: `What the orchestrator wrote down, every committed version: the prompt index (its rows by work number), each prompt's front matter (status, executor, model, worktree, baseline, accepted commit), execution plans (tables, batches, dependencies, what may run in parallel), run status files (agent, model, worktree, base commit, result), receipts. parsed:false means the program could not read its structure — read the file whole and judge it. With ident (a work number such as AK), how that work's fields changed version by version and its rows in the index. Filters: kind (index | prompt | plan | status | receipt), path, status, currentOnly, unparsedOnly. ${CITE}`,
|
|
188
|
+
parameters: Type.Object({
|
|
189
|
+
...REPO, ...PAGE, ...RANGE, ident: Type.Optional(Type.String()), kind: Type.Optional(Type.String()), path: Type.Optional(Type.String()),
|
|
190
|
+
status: Type.Optional(Type.String()), currentOnly: Type.Optional(Type.Boolean()), unparsedOnly: Type.Optional(Type.Boolean()),
|
|
191
|
+
}),
|
|
192
|
+
execute: async (_id, p) => withLedger(ctx, (l) => (opt(p.ident) && !opt(p.kind) && !opt(p.path)
|
|
193
|
+
? l.arrangementHistory(str(p.ident), opt(p.repo))
|
|
194
|
+
: l.arrangements({ repo: opt(p.repo), ident: opt(p.ident), kind: opt(p.kind), path: opt(p.path), status: opt(p.status), currentOnly: p.currentOnly === true, unparsedOnly: p.unparsedOnly === true, since: opt(p.since), until: opt(p.until), limit: num(p.limit), offset: num(p.offset) }))),
|
|
195
|
+
}),
|
|
196
|
+
defineTool({
|
|
197
|
+
name: 'pk_ledger_word', label: 'A word across all history',
|
|
198
|
+
description: `Where a word or phrase appears across everything the ledger read — every version of every document (per document: the first and last version that has it, and whether the current version does), every commit message, the owner's words and the agent messages they answered, documents outside version control — oldest first. span: true gives only the first and the last appearance and the count. Three characters or more: case-insensitive; shorter (a two-character Chinese word): exact. Filters: kinds (doc | commit | owner | agent | loose), since/until. ${CITE}`,
|
|
199
|
+
parameters: Type.Object({ word: Type.String(), span: Type.Optional(Type.Boolean()), kinds: Type.Optional(Type.Array(Type.String())), ...REPO, ...PAGE, ...RANGE }),
|
|
200
|
+
execute: async (_id, p) => withLedger(ctx, (l) => (p.span === true
|
|
201
|
+
? l.wordSpan(str(p.word), { repo: opt(p.repo) })
|
|
202
|
+
: l.word(str(p.word), { kinds: strs(p.kinds), repo: opt(p.repo), since: opt(p.since), until: opt(p.until), limit: num(p.limit), offset: num(p.offset) }))),
|
|
203
|
+
}),
|
|
204
|
+
defineTool({
|
|
205
|
+
name: 'pk_ledger_refs', label: 'File references',
|
|
206
|
+
description: 'File-level references in the current version: who references a file and what it references, with the kind (import, calls, references, instantiates, extends …), its size and last change. TypeScript and JavaScript are read by the compiler\'s pre-parser; every other language by the ledger\'s code engine, counting only references it resolved through an import, a path, a qualified name, a function reference or an instance method (never a name alone); its unresolved imports are listed as external. A file whose references no reader reads says so in level. Without path: the code files no other file references — a fact; an entry point, a test or a script is referenced by nothing and is not residue, the judgement is yours (dir narrows it). namedBy on a file: other files name it though no counted reference reaches it — entryPoint when a build or manifest file names it (AndroidManifest.xml, CMakeLists.txt, a Gradle or Xcode project …: a named entry point, not residue), otherwise code that names it (an include directive, a worker started by its path); mentionedBy lists documents and data that only mention it. Read them before calling it unreferenced. Symbols: pk_ledger_symbol; since when a reference exists: pk_ledger_ref_history.',
|
|
207
|
+
parameters: Type.Object({ path: Type.Optional(Type.String()), dir: Type.Optional(Type.String()), includeTests: Type.Optional(Type.Boolean()), ...REPO, ...PAGE }),
|
|
208
|
+
execute: async (_id, p) => withLedger(ctx, (l) => (opt(p.path)
|
|
209
|
+
? l.fileRefs(str(p.path), { repo: opt(p.repo) })
|
|
210
|
+
: l.unreferenced({ repo: opt(p.repo), dir: opt(p.dir), includeTests: p.includeTests === true, limit: num(p.limit), offset: num(p.offset) }))),
|
|
211
|
+
}),
|
|
212
|
+
defineTool({
|
|
213
|
+
name: 'pk_ledger_ref_history', label: 'Since when a file references another',
|
|
214
|
+
description: 'Follows one file back through every commit that changed it and says in which commits its reference to another file appears and disappears (the target matched by file name — by its stem in an import specifier — so a moved target is still found), the first such commit on the trunk, and whether it references it now. Each version is read the way the file\'s language is read now: TypeScript and JavaScript by the compiler\'s pre-parser, every other language by the code engine\'s extraction of that version\'s imports. Read-only git.',
|
|
215
|
+
parameters: Type.Object({ src: Type.String({ description: 'the referencing file, repository-relative' }), dst: Type.String({ description: 'the referenced file (its name is enough)' }), ...REPO }),
|
|
216
|
+
execute: async (_id, p) => withLedger(ctx, (l) => l.refHistory(str(p.src), str(p.dst), { repo: opt(p.repo) })),
|
|
217
|
+
}),
|
|
218
|
+
defineTool({
|
|
219
|
+
name: 'pk_ledger_symbol', label: 'Symbol references',
|
|
220
|
+
description: 'Symbol level in every language whose references the ledger reads: kind references (every place the symbol is used), implementations, callers or callees (the call hierarchy). TypeScript and JavaScript are answered by the TypeScript language service on the checkout, compiler-exact; every other language by the ledger\'s code engine on the current version. Every hit says how it was resolved (how, confidence) and whether it counts — a hit the engine matched only by a name counts when its file reaches the declaring file through imports (through). Locate the symbol by name (its first declaration in the file; a member as Class.member) or by 1-based line and character.',
|
|
221
|
+
parameters: Type.Object({
|
|
222
|
+
file: Type.String({ description: 'repository-relative file of the current version' }),
|
|
223
|
+
kind: Type.String({ description: 'references | implementations | callers | callees' }),
|
|
224
|
+
name: Type.Optional(Type.String()), line: Type.Optional(Type.Number()), character: Type.Optional(Type.Number()), ...REPO,
|
|
225
|
+
}),
|
|
226
|
+
execute: async (_id, p) => {
|
|
227
|
+
const kind = str(p.kind);
|
|
228
|
+
if (!['references', 'implementations', 'callers', 'callees'].includes(kind)) return fail('kind must be references, implementations, callers or callees.');
|
|
229
|
+
return withLedger(ctx, (l) => l.symbol(kind as 'references', { file: str(p.file), name: opt(p.name), line: num(p.line), character: num(p.character) }, { repo: opt(p.repo) }));
|
|
230
|
+
},
|
|
231
|
+
}),
|
|
232
|
+
defineTool({
|
|
233
|
+
name: 'pk_ledger_code', label: 'Code sizes, tests and merges',
|
|
234
|
+
description: 'The current version\'s directories: files and lines split into product code, generated code (how it was recognised) and other classified material (third-party), test files and test cases, and the last change of each (under narrows to a directory, depth limits how deep). With merges: true, which directories each merge changed instead (dir narrows it; trunkOnly). The numbers a code territory is made of.',
|
|
235
|
+
parameters: Type.Object({ ...REPO, ...PAGE, under: Type.Optional(Type.String()), depth: Type.Optional(Type.Number()), merges: Type.Optional(Type.Boolean()), dir: Type.Optional(Type.String()), trunkOnly: Type.Optional(Type.Boolean()) }),
|
|
236
|
+
execute: async (_id, p) => withLedger(ctx, (l) => (p.merges === true
|
|
237
|
+
? l.merges({ repo: opt(p.repo), dir: opt(p.dir), trunkOnly: p.trunkOnly === true, limit: num(p.limit), offset: num(p.offset) })
|
|
238
|
+
: l.dirs({ repo: opt(p.repo), under: opt(p.under), depth: num(p.depth), limit: num(p.limit), offset: num(p.offset) }))),
|
|
239
|
+
}),
|
|
240
|
+
defineTool({
|
|
241
|
+
name: 'pk_ledger_sessions', label: 'Sessions',
|
|
242
|
+
description: `The sessions of the project's working directories the ledger read: host, native id, start and end, messages, the owner's messages, headless or subagent, and what cannot be read (a log gone since it was read, a log that fails to parse — missingOnly lists those). With session (its id, native id or a prefix): its messages in order, every speaker as the session's structure says (owner, agent, subagent, host), with the text of the owner's messages and of the agent messages they answer. ${CITE}`,
|
|
243
|
+
parameters: Type.Object({ session: Type.Optional(Type.String()), host: Type.Optional(Type.String()), speaker: Type.Optional(Type.String()), fromIndex: Type.Optional(Type.Number()), withOwnerWords: Type.Optional(Type.Boolean()), missingOnly: Type.Optional(Type.Boolean()), ...PAGE, ...RANGE }),
|
|
244
|
+
execute: async (_id, p) => withLedger(ctx, (l) => (opt(p.session)
|
|
245
|
+
? l.sessionMessages(str(p.session), { host: opt(p.host), speaker: opt(p.speaker), fromIndex: num(p.fromIndex), limit: num(p.limit), offset: num(p.offset) })
|
|
246
|
+
: l.sessions({ host: opt(p.host), withOwnerWords: p.withOwnerWords === true, missingOnly: p.missingOnly === true, since: opt(p.since), until: opt(p.until), limit: num(p.limit), offset: num(p.offset) }))),
|
|
247
|
+
}),
|
|
248
|
+
defineTool({
|
|
249
|
+
name: 'pk_ledger_owner_words', label: 'The owner\'s own words',
|
|
250
|
+
description: `Every message the owner typed in the project's sessions, verbatim (credentials redacted), oldest first, each with the agent message right before it that it answers — the owner's "ok" or "可以" is read against what it said yes to; the agent's text is only there to understand what the owner confirmed. Filters: since/until, keyword, session, host; headless runs (a program's prompts) are left out unless includeHeadless. ${CITE}`,
|
|
251
|
+
parameters: Type.Object({ keyword: Type.Optional(Type.String()), session: Type.Optional(Type.String()), host: Type.Optional(Type.String()), includeHeadless: Type.Optional(Type.Boolean()), ...PAGE, ...RANGE }),
|
|
252
|
+
execute: async (_id, p) => withLedger(ctx, (l) => l.ownerWords({ keyword: opt(p.keyword), session: opt(p.session), host: opt(p.host), includeHeadless: p.includeHeadless === true, since: opt(p.since), until: opt(p.until), limit: num(p.limit), offset: num(p.offset) })),
|
|
253
|
+
}),
|
|
254
|
+
defineTool({
|
|
255
|
+
name: 'pk_ledger_provenance', label: 'How it got here',
|
|
256
|
+
description: `The dated steps of how something got here, assembled from the ledger, oldest first: where it first appeared, each version that changed it (with the sections), lines that say it was superseded or list it as obsolete, the plans and arrangements that name it (status by version), commits and merges, verdicts, the owner's words that name it, and where it stands now. Anchors: nums (project numbers), paths (documents or files), commits, words, entries (ledger ids). Steps whose material exists only in history are marked history: write them as history, with what replaced them. ${CITE}`,
|
|
257
|
+
parameters: Type.Object({
|
|
258
|
+
nums: Type.Optional(Type.Array(Type.String())), paths: Type.Optional(Type.Array(Type.String())), commits: Type.Optional(Type.Array(Type.String())),
|
|
259
|
+
words: Type.Optional(Type.Array(Type.String())), entries: Type.Optional(Type.Array(Type.String())), ...REPO,
|
|
260
|
+
}),
|
|
261
|
+
execute: async (_id, p) => {
|
|
262
|
+
const input = { nums: strs(p.nums), paths: strs(p.paths), commits: strs(p.commits), words: strs(p.words), entries: strs(p.entries), repo: opt(p.repo) };
|
|
263
|
+
if (!input.nums.length && !input.paths.length && !input.commits.length && !input.words.length && !input.entries.length) return fail('Give at least one anchor: nums, paths, commits, words or entries.');
|
|
264
|
+
return withLedger(ctx, (l) => l.provenance(input));
|
|
265
|
+
},
|
|
266
|
+
}),
|
|
267
|
+
defineTool({
|
|
268
|
+
name: 'pk_ledger_entry', label: 'Read a ledger entry',
|
|
269
|
+
description: 'One entry of the ledger by its id (commit:…, doc:…, del:…, cleanup:…, sup:…, num:…, rule:…, verdict:…, plan:…, session:…, msg:…, file:…): its label, when it happened and its text (a document version\'s whole text, paged by lines with fromLine / toLine), exactly as a citation of it is read.',
|
|
270
|
+
parameters: Type.Object({ id: Type.String(), fromLine: Type.Optional(Type.Number()), toLine: Type.Optional(Type.Number()) }),
|
|
271
|
+
execute: async (_id, p) => withLedger(ctx, (l) => {
|
|
272
|
+
const e: LedgerEntry | null = l.resolve(str(p.id));
|
|
273
|
+
if (!e) return `${str(p.id)} is not an entry of the ledger.`;
|
|
274
|
+
if (e.text === undefined) return e;
|
|
275
|
+
const lines = e.text.split(/\r?\n/);
|
|
276
|
+
const from = Math.max(1, Math.floor(num(p.fromLine) ?? 1));
|
|
277
|
+
const to = Math.min(lines.length, Math.floor(num(p.toLine) ?? lines.length));
|
|
278
|
+
let size = 0;
|
|
279
|
+
let end = from - 1;
|
|
280
|
+
const out: string[] = [];
|
|
281
|
+
for (let i = from - 1; i < to; i++) { if (out.length && size + lines[i]!.length > 60_000) break; out.push(lines[i]!); size += lines[i]!.length + 1; end = i + 1; }
|
|
282
|
+
return { ...e, text: out.join('\n'), lines: lines.length, fromLine: from, toLine: end, ...(end < to ? { nextFromLine: end + 1 } : {}) };
|
|
283
|
+
}),
|
|
284
|
+
}),
|
|
285
|
+
defineTool({
|
|
286
|
+
name: 'pk_ledger_coverage', label: 'What the ledger covers',
|
|
287
|
+
description: 'What the ledger covers and what it does not: per repository the commits, branches, merges, the time span and the day document version history starts (content before an import commit is dated by what it says, else file time); per language who reads its code references (the TypeScript compiler, the code engine, or no one) and what that reached, measured — imports resolved and not, references by resolution method, counted and matched by a name only, and where references went unresolved (gaps); the code engine\'s last run (time, peak memory, index size); generated and third-party material kept apart; sessions read, the ones that cannot be read and the commit days no session covers; documents outside version control; the rules the program used (import-style root commit, cleanup commit, numbering, verdicts, occurred time); the last rebuild and how long it took.',
|
|
288
|
+
parameters: Type.Object({}),
|
|
289
|
+
execute: async () => withLedger(ctx, (l) => l.coverage()),
|
|
290
|
+
}),
|
|
291
|
+
];
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Where a project's ledger file is, for a caller that has the store's directory. */
|
|
295
|
+
export const ledgerFileOf = (projectDirPath: string): string => join(projectDirPath, 'ledger.sqlite');
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The program's check of a process link's evidence, as the link is written (CM, E151; CK fix 6). The cross-check used to
|
|
3
|
+
* confirm every link by hand: on the gated run 82 links were "confirmed" in 26 seconds without one file opened, after a
|
|
4
|
+
* 25K-character turn spent untangling link ids. What can be checked is checked here, at no cost to the model:
|
|
5
|
+
*
|
|
6
|
+
* - the fact resolves (evidence.ts already refuses a commit that is not there and a line its source does not hold);
|
|
7
|
+
* - a commit is in the ledger, and touches what the link claims: its message names the work item's number (a range
|
|
8
|
+
* written in it counts, `AC-25-AC-31`), a file it changed is the work item's own (its path carries the number, or the
|
|
9
|
+
* work item's material is that file), a file the link's why names is among its changes, or a parent it merged names the
|
|
10
|
+
* number;
|
|
11
|
+
* - a line of a document names the work item's number, or the document is the work item's own.
|
|
12
|
+
*
|
|
13
|
+
* A link that passes is lane-checked: it counts like a confirmed one, the distinction kept visible (Spec §2.12; the process
|
|
14
|
+
* view says `Lane-checked`). One that fails is suspect: the cross-check rejects it, or confirms it after reading the
|
|
15
|
+
* original. The synthesis gate counts the suspect links neither confirmed nor rejected, not every unconfirmed link.
|
|
16
|
+
*/
|
|
17
|
+
import { Ledger } from '../ledger/index.ts';
|
|
18
|
+
import { definitionsInPath } from '../ledger/numbering.ts';
|
|
19
|
+
import { rangeMembers } from '../ledger/ranges.ts';
|
|
20
|
+
import type { EvidenceRef, LinkCheck } from '../model/k-types.ts';
|
|
21
|
+
import type { WorkThread } from '../model/types.ts';
|
|
22
|
+
import type { ProjectStore } from '../store/project-store.ts';
|
|
23
|
+
import { pathKey } from '../util/paths.ts';
|
|
24
|
+
import { isNumberShaped } from './numbers-check.ts';
|
|
25
|
+
import { primaryIdentifier } from './tools.ts';
|
|
26
|
+
|
|
27
|
+
/** A work item's own numbers: its ids, the identifier its title starts with, an internal id that is a project number. */
|
|
28
|
+
export function workNumbers(t: WorkThread): Set<string> {
|
|
29
|
+
const out = new Set<string>([...t.ids, ...(isNumberShaped(t.id.toUpperCase()) ? [t.id] : [])].map((x) => x.trim().toUpperCase()).filter(Boolean));
|
|
30
|
+
const p = primaryIdentifier(t.ids, t.title);
|
|
31
|
+
if (p) out.add(p.toUpperCase());
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const esc = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
36
|
+
|
|
37
|
+
/** The numbers of `nums` a text names, ranges included (`CKC-03 AC-25-AC-31` names AC-28). */
|
|
38
|
+
export function namesNumber(text: string, nums: ReadonlySet<string>): string | null {
|
|
39
|
+
if (!text) return null;
|
|
40
|
+
const upper = text.toUpperCase();
|
|
41
|
+
for (const n of nums) if (new RegExp(`(?<![A-Z0-9_])${esc(n)}(?![A-Z0-9_]|\\.\\d)`).test(upper)) return n;
|
|
42
|
+
for (const m of rangeMembers(text)) if (nums.has(m.toUpperCase())) return m;
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The repository-relative files a work item's material is in (its fact records' sources, and the reference items it serves). */
|
|
47
|
+
function workFiles(store: ProjectStore, t: WorkThread): Set<string> {
|
|
48
|
+
const ids = [
|
|
49
|
+
...t.factRecordIds.flatMap((f) => store.facts.get(f)?.aboutSourceIds ?? []),
|
|
50
|
+
...[...t.executionFacts, ...t.qcFacts].flatMap((s) => s.sourceIds),
|
|
51
|
+
];
|
|
52
|
+
const out = new Set<string>();
|
|
53
|
+
for (const id of ids) {
|
|
54
|
+
const a = store.sources.get(id)?.anchor;
|
|
55
|
+
if (a?.kind === 'file') out.add(pathKey(a.path));
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const ok = (why: string): LinkCheck => ({ passed: true, why, at: new Date().toISOString() });
|
|
61
|
+
const no = (why: string): LinkCheck => ({ passed: false, why, at: new Date().toISOString() });
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Check the links written before the program checked links (or by a writer that did not): each gets its check now, from
|
|
65
|
+
* its evidence as recorded. Returns how many were checked. The synthesis gate and `pk_round_state` run it first.
|
|
66
|
+
*/
|
|
67
|
+
export function checkUncheckedLinks(store: ProjectStore, jobId: string | null = null): number {
|
|
68
|
+
const todo = store.links.filter((l) => l.check == null && !l.confirmed && l.evidence != null);
|
|
69
|
+
if (!todo.length) return 0;
|
|
70
|
+
let ledger: Ledger | null = null;
|
|
71
|
+
try { ledger = Ledger.openDir(store.dir); } catch { ledger = null; }
|
|
72
|
+
try {
|
|
73
|
+
for (const l of todo) {
|
|
74
|
+
const work = store.threads.get(l.workId);
|
|
75
|
+
if (!work) continue;
|
|
76
|
+
const check = checkLink(store, work, l.evidence!, l.why, ledger);
|
|
77
|
+
store.links.put({ ...l, check }, { jobId, basisSourceIds: [], summary: `Link ${l.stepKind} → ${work.title}: ${check.passed ? 'lane-checked' : 'suspect'} (${check.why})` });
|
|
78
|
+
}
|
|
79
|
+
} finally { ledger?.close(); }
|
|
80
|
+
return todo.length;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The commit a fact is, when it is one: a commit evidence, or a ledger entry of a commit. */
|
|
84
|
+
function commitOf(fact: EvidenceRef): string | null {
|
|
85
|
+
if (fact.kind === 'commit') return fact.id.toLowerCase();
|
|
86
|
+
if (fact.kind === 'ledger' && /^commit:[0-9a-f]{7,40}$/i.test(fact.id)) return fact.id.slice(7).toLowerCase();
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The numbers that stand for the same work beside its own (CM): the tickets that implement it when it is a contract, and
|
|
92
|
+
* the contracts it implements when it is a ticket — a delivery of CKC-22 is the merge that names AP, the ticket that
|
|
93
|
+
* implements it.
|
|
94
|
+
*/
|
|
95
|
+
function relatedNumbers(store: ProjectStore, work: WorkThread): Map<string, string> {
|
|
96
|
+
const out = new Map<string, string>();
|
|
97
|
+
const add = (t: WorkThread | undefined, how: string) => { if (t) for (const n of workNumbers(t)) if (!out.has(n)) out.set(n, how); };
|
|
98
|
+
const byRef = (id: string): WorkThread | undefined => {
|
|
99
|
+
const direct = store.threads.get(id);
|
|
100
|
+
if (direct) return direct;
|
|
101
|
+
const r = store.reference.get(id);
|
|
102
|
+
const ids = (r?.ids ?? []).map((i) => i.toUpperCase());
|
|
103
|
+
return ids.length ? store.threads.find((x) => x.ids.some((i) => ids.includes(i.toUpperCase()))) : undefined;
|
|
104
|
+
};
|
|
105
|
+
for (const r of store.relations.filter((x) => x.type === 'implements')) {
|
|
106
|
+
if (byRef(r.to)?.id === work.id) add(store.threads.get(r.from), `a ticket implementing ${work.ids[0] ?? work.title}`);
|
|
107
|
+
if (r.from === work.id) add(byRef(r.to), `the contract ${work.ids[0] ?? work.title} implements`);
|
|
108
|
+
}
|
|
109
|
+
for (const n of workNumbers(work)) out.delete(n);
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** A path as the ledger writes it: relative to the repository that holds it. */
|
|
114
|
+
function relOf(ledger: Ledger, path: string): string {
|
|
115
|
+
const norm = path.replace(/\\/g, '/');
|
|
116
|
+
for (const r of ledger.repos()) {
|
|
117
|
+
const root = r.path.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
118
|
+
if (norm.toLowerCase().startsWith(`${root.toLowerCase()}/`)) return norm.slice(root.length + 1);
|
|
119
|
+
}
|
|
120
|
+
return norm;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The check (see the module comment). `ledger` is the project's ledger, or null when this build has none: a commit is then
|
|
125
|
+
* checked by its message alone, as far as the evidence's label gives it.
|
|
126
|
+
*/
|
|
127
|
+
export function checkLink(store: ProjectStore, work: WorkThread, fact: EvidenceRef, why: string, ledger: Ledger | null): LinkCheck {
|
|
128
|
+
const nums = workNumbers(work);
|
|
129
|
+
const label = work.ids[0] ?? work.title;
|
|
130
|
+
if (nums.size === 0) return no(`${work.title} has no number of its own, so the program cannot tell whether the fact is its step: check it against the original.`);
|
|
131
|
+
const related = relatedNumbers(store, work);
|
|
132
|
+
const all = new Set([...nums, ...related.keys()]);
|
|
133
|
+
const said = (n: string) => (nums.has(n.toUpperCase()) ? n : `${n}, ${related.get(n.toUpperCase()) ?? 'related work'}`);
|
|
134
|
+
const files = workFiles(store, work);
|
|
135
|
+
const ownFile = (path: string) => definitionsInPath(path.replace(/\\/g, '/')).some((d) => nums.has(d.num.toUpperCase())) || [...files].some((f) => f.endsWith(`/${pathKey(path)}`) || f === pathKey(path));
|
|
136
|
+
const list = [...all];
|
|
137
|
+
const marks = list.map(() => '?').join(',');
|
|
138
|
+
const hash = commitOf(fact);
|
|
139
|
+
if (hash) {
|
|
140
|
+
if (!ledger) {
|
|
141
|
+
const n = namesNumber(`${fact.label}\n${fact.line ?? ''}`, all);
|
|
142
|
+
return n ? ok(`the commit's subject names ${said(n)}`) : no(`the commit's subject names no number of ${label}, and the ledger is not available to read its files`);
|
|
143
|
+
}
|
|
144
|
+
const row = ledger.db.prepare('SELECT repo, hash, subject, body, parents FROM commits WHERE hash LIKE ? LIMIT 2').all(`${hash}%`) as { repo: string; hash: string; subject: string; body: string; parents: string }[];
|
|
145
|
+
if (row.length === 0) return no(`commit ${hash.slice(0, 12)} is not in the ledger`);
|
|
146
|
+
const c = row[0]!;
|
|
147
|
+
const named = namesNumber(`${c.subject}\n${c.body}`, all);
|
|
148
|
+
if (named) return ok(`the commit's message names ${said(named)}`);
|
|
149
|
+
const changed = (ledger.db.prepare('SELECT path FROM commit_files WHERE repo = ? AND hash = ?').all(c.repo, c.hash) as { path: string }[]).map((r) => r.path);
|
|
150
|
+
const own = changed.find(ownFile);
|
|
151
|
+
if (own) return ok(`the commit changed ${own}, ${label}'s own material`);
|
|
152
|
+
const inWhy = changed.find((x) => why.includes(x));
|
|
153
|
+
if (inWhy) return ok(`the commit changed ${inWhy}, which the link's why names`);
|
|
154
|
+
for (const parent of c.parents.split(' ').slice(1).filter(Boolean)) {
|
|
155
|
+
const pr = ledger.db.prepare('SELECT subject, body FROM commits WHERE repo = ? AND hash = ?').get(c.repo, parent) as { subject: string; body: string } | undefined;
|
|
156
|
+
const n = pr ? namesNumber(`${pr.subject}\n${pr.body}`, all) : null;
|
|
157
|
+
if (n) return ok(`the merge brought in ${parent.slice(0, 7)}, whose message names ${said(n)}`);
|
|
158
|
+
}
|
|
159
|
+
// The project's own records tie them: a line of a document that names the work item's number and this commit.
|
|
160
|
+
const short = c.hash.slice(0, 7);
|
|
161
|
+
const line = ledger.db.prepare(`SELECT num, path, line FROM nums WHERE num IN (${marks}) AND kind IN ('doc', 'loose') AND context LIKE ? ORDER BY current DESC LIMIT 1`).get(...list, `%${short}%`) as { num: string; path: string | null; line: number | null } | undefined;
|
|
162
|
+
if (line) return ok(`${line.path ?? 'a document'}${line.line ? `:${line.line}` : ''} names ${said(line.num)} and ${short} on one line`);
|
|
163
|
+
return no(`commit ${short} “${c.subject.slice(0, 80)}” names no number of ${label}, changed none of its files, and no line of the project's documents names both${changed.length ? ` (it changed ${changed.slice(0, 3).join(', ')}${changed.length > 3 ? ` and ${changed.length - 3} more` : ''})` : ''}`);
|
|
164
|
+
}
|
|
165
|
+
// A line of a document, a file, a source.
|
|
166
|
+
const path = fact.kind === 'file' ? fact.id
|
|
167
|
+
: fact.kind === 'source' ? (() => { const x = store.sources.get(fact.id)?.anchor; return x?.kind === 'file' ? x.path : null; })()
|
|
168
|
+
: fact.kind === 'ledger' && ledger ? ledger.pathOfEntry(fact.id)?.path ?? null : null;
|
|
169
|
+
if (fact.line) {
|
|
170
|
+
const n = namesNumber(fact.line, all);
|
|
171
|
+
if (n) return ok(`the cited line names ${said(n)}`);
|
|
172
|
+
}
|
|
173
|
+
if (path && ownFile(path)) return ok(`${path.replace(/\\/g, '/').split('/').slice(-2).join('/')} is ${label}'s own material`);
|
|
174
|
+
if (fact.kind === 'source' && fact.line) {
|
|
175
|
+
const n = namesNumber(store.sources.get(fact.id)?.title ?? '', all);
|
|
176
|
+
if (n) return ok(`the cited line stands in a section titled with ${said(n)}`);
|
|
177
|
+
}
|
|
178
|
+
// The document the line stands in is about the work item: it names its number (a QC report's verdict line seldom does).
|
|
179
|
+
if (path && fact.line && ledger) {
|
|
180
|
+
const rel = relOf(ledger, path);
|
|
181
|
+
const row = ledger.db.prepare(`SELECT num, line FROM nums WHERE num IN (${marks}) AND path = ? AND kind IN ('doc', 'loose') ORDER BY current DESC, line LIMIT 1`).get(...list, rel) as { num: string; line: number | null } | undefined;
|
|
182
|
+
if (row) return ok(`the line is cited from ${rel.split('/').slice(-2).join('/')}, which names ${said(row.num)}${row.line ? ` (line ${row.line})` : ''}`);
|
|
183
|
+
}
|
|
184
|
+
return no(fact.line ? `the cited line names no number of ${label}, and ${path ? 'the document does not name it either' : 'it names no file of its own'}` : `no line is cited, and ${path ? `${path.replace(/\\/g, '/').split('/').pop()} is not ${label}'s own material` : 'the fact names no file'}`);
|
|
185
|
+
}
|