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,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Execution arrangements (Spec §1.16 row 6; CKC-22 AC-7): what the orchestrator wrote down — the prompt index, each
|
|
3
|
+
* prompt's front matter, the execution plans, the run status files, the receipts — read into batches, dependencies,
|
|
4
|
+
* parallel candidates, what is running, the agent and worktree bound to a piece of work, and its baseline commit. Every
|
|
5
|
+
* committed version is read, so the history of a status field is kept; a file whose structure the program cannot read
|
|
6
|
+
* is recorded with `parsed: false`, meaning "read it whole and judge" (交判断读).
|
|
7
|
+
*
|
|
8
|
+
* Which files are arrangements, by the conventions of a `subagent/` folder:
|
|
9
|
+
* …/subagent/INDEX.md index (its tables: one row per work id)
|
|
10
|
+
* …/subagent/<ID>-….md prompt (front matter: id, executor, status, worktree, baseline …)
|
|
11
|
+
* …/execution-plan*.md, …/subagent/*plan*.md plan
|
|
12
|
+
* …/subagent/runs/<run>/status.json status (a headless run: agent, model, worktree, base commit, result)
|
|
13
|
+
* …/subagent/reports/….md, *-report*.md, 回执 receipt
|
|
14
|
+
*/
|
|
15
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
16
|
+
import { blobTextKey, getText, planKey, redact, tx } from './schema.ts';
|
|
17
|
+
import { catBlobs, isText } from './git-read.ts';
|
|
18
|
+
import { datesOf, occurredOfLine } from './docs.ts';
|
|
19
|
+
import { familyOf } from './numbering.ts';
|
|
20
|
+
import { materialTime, msOf, type Occurred } from './time.ts';
|
|
21
|
+
import type { RepoHandle } from './repo-scan.ts';
|
|
22
|
+
|
|
23
|
+
export type ArrangementKind = 'index' | 'prompt' | 'plan' | 'status' | 'receipt';
|
|
24
|
+
|
|
25
|
+
export function arrangementKind(path: string): ArrangementKind | null {
|
|
26
|
+
const p = path.split('\\').join('/');
|
|
27
|
+
const base = p.split('/').pop() ?? '';
|
|
28
|
+
if (/^execution-plan[^/]*\.md$/i.test(base)) return 'plan';
|
|
29
|
+
const m = /(?:^|\/)subagents?\/(.+)$/i.exec(p);
|
|
30
|
+
if (!m) return null;
|
|
31
|
+
const rest = m[1]!;
|
|
32
|
+
if (/^INDEX\.md$/i.test(rest)) return 'index';
|
|
33
|
+
if (/(?:^|\/)runs\/[^/]+\/status\.json$/i.test(rest)) return 'status';
|
|
34
|
+
if (/(?:^|\/)reports?\/[^/]+\.md$/i.test(rest) || /-report[^/]*\.md$/i.test(base) || /回执/.test(base)) return 'receipt';
|
|
35
|
+
if (/^[^/]*plan[^/]*\.md$/i.test(rest)) return 'plan';
|
|
36
|
+
if (/^(?:[A-Z]{1,6}-[A-Z]?\d{1,4}|[A-Z]\d{1,4}|[A-Z]{2})[-_][^/]*\.md$/.test(rest)) return 'prompt';
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The work id a file is about: its front-matter id, else its name's (or its run directory's) number prefix. */
|
|
41
|
+
export function identOf(path: string, fields: Record<string, string> | null): string | null {
|
|
42
|
+
if (fields?.id && familyOf(fields.id.trim())) return fields.id.trim();
|
|
43
|
+
const segs = path.split('/');
|
|
44
|
+
for (let i = segs.length - 1; i >= 0; i--) {
|
|
45
|
+
const m = /^(?:\d{4}-\d{2}-\d{2}[_-])?([A-Z]{1,6}-[A-Z]?\d{1,4}|[A-Z]\d{1,4}|[A-Z]{2})(?=[-_.]|$)/.exec(segs[i]!);
|
|
46
|
+
if (m && familyOf(m[1]!)) return m[1]!;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A small YAML-front-matter subset: `key: value` lines between `---` fences; quotes around a value are taken off. */
|
|
52
|
+
export function frontMatter(text: string): Record<string, string> | null {
|
|
53
|
+
const m = /^\uFEFF?---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/.exec(text);
|
|
54
|
+
if (!m) return null;
|
|
55
|
+
const out: Record<string, string> = {};
|
|
56
|
+
for (const line of m[1]!.split(/\r?\n/)) {
|
|
57
|
+
const kv = /^([A-Za-z_][\w.-]*)\s*:\s*(.*)$/.exec(line);
|
|
58
|
+
if (!kv) continue;
|
|
59
|
+
out[kv[1]!] = kv[2]!.trim().replace(/^"(.*)"$/, '$1').replace(/^'(.*)'$/, '$1');
|
|
60
|
+
}
|
|
61
|
+
return Object.keys(out).length ? out : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ArrangementData {
|
|
65
|
+
readonly title?: string;
|
|
66
|
+
readonly fields?: Record<string, string>;
|
|
67
|
+
readonly status?: string | null;
|
|
68
|
+
readonly running?: boolean;
|
|
69
|
+
readonly agent?: string | null;
|
|
70
|
+
readonly model?: string | null;
|
|
71
|
+
readonly worktree?: string | null;
|
|
72
|
+
readonly baseline?: string | null;
|
|
73
|
+
readonly accepted?: string | null;
|
|
74
|
+
/** Table rows keyed by their first column (an index, a plan's order table): the row's cells by column header. */
|
|
75
|
+
readonly rows?: readonly { readonly id: string; readonly cells: Readonly<Record<string, string>>; readonly line: number }[];
|
|
76
|
+
readonly batches?: readonly string[];
|
|
77
|
+
readonly dependencies?: readonly string[];
|
|
78
|
+
readonly parallel?: readonly string[];
|
|
79
|
+
readonly json?: Readonly<Record<string, unknown>>;
|
|
80
|
+
/**
|
|
81
|
+
* How many table rows and batch, dependency and parallel lines the file has beyond what is kept (`MAX_ROWS`,
|
|
82
|
+
* `MAX_LINES`): the list goes on and says by how much, instead of stopping without a word (QC AY, CKC-22 AC-7). The
|
|
83
|
+
* whole file is still read by `pk_ledger_doc_text` / `read`.
|
|
84
|
+
*/
|
|
85
|
+
readonly omitted?: Readonly<Partial<Record<'rows' | 'batches' | 'dependencies' | 'parallel', number>>>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const clip = (s: string, n = 240): string => (s.length > n ? `${s.slice(0, n)}…` : s);
|
|
89
|
+
const HASH = /\b[0-9a-f]{7,40}\b/;
|
|
90
|
+
const RUNNING = /^(running|in[ _-]?progress|dispatched|started|queued|进行中|正在跑|执行中)$/i;
|
|
91
|
+
/** Table rows kept per arrangement file; batch, dependency and parallel lines kept of each. The rest are counted. */
|
|
92
|
+
export const MAX_ROWS = 600;
|
|
93
|
+
export const MAX_LINES = 60;
|
|
94
|
+
|
|
95
|
+
/** One Markdown table as written: its header cells and each data row's cells, unclipped, with 1-based line numbers. */
|
|
96
|
+
export interface MarkdownTable {
|
|
97
|
+
/** The header row's line. */
|
|
98
|
+
readonly line: number;
|
|
99
|
+
/** The header cells, trimmed, with `*` and backticks taken out; an empty header cell is `col<n>`. */
|
|
100
|
+
readonly header: readonly string[];
|
|
101
|
+
readonly rows: readonly { readonly cells: readonly string[]; readonly line: number }[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The tables of a Markdown text as written (D99 fill-in tools, ledger arrangements): a header row, its separator row,
|
|
106
|
+
* then the data rows that follow; cells are trimmed and otherwise untouched.
|
|
107
|
+
*/
|
|
108
|
+
export function markdownTables(text: string): MarkdownTable[] {
|
|
109
|
+
const lines = text.split(/\r?\n/);
|
|
110
|
+
const out: MarkdownTable[] = [];
|
|
111
|
+
for (let i = 0; i + 1 < lines.length; i++) {
|
|
112
|
+
if (!/^\s*\|.*\|\s*$/.test(lines[i]!) || !/^\s*\|[\s:|-]+\|\s*$/.test(lines[i + 1]!)) continue;
|
|
113
|
+
const header = lines[i]!.split('|').slice(1, -1).map((c, k) => c.trim().replace(/[*`]/g, '') || `col${k + 1}`);
|
|
114
|
+
const rows: { cells: string[]; line: number }[] = [];
|
|
115
|
+
let j = i + 2;
|
|
116
|
+
for (; j < lines.length && /^\s*\|.*\|\s*$/.test(lines[j]!); j++) rows.push({ cells: lines[j]!.split('|').slice(1, -1).map((c) => c.trim()), line: j + 1 });
|
|
117
|
+
out.push({ line: i + 1, header, rows });
|
|
118
|
+
i = j - 1;
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Tables of a Markdown text: each data row keyed by its first cell, cells named by the header row; `more` counts the rows past the cap. */
|
|
124
|
+
export function tables(text: string): { rows: { id: string; cells: Record<string, string>; line: number }[]; more: number } {
|
|
125
|
+
const out: { id: string; cells: Record<string, string>; line: number }[] = [];
|
|
126
|
+
let more = 0;
|
|
127
|
+
for (const table of markdownTables(text)) {
|
|
128
|
+
for (const row of table.rows) {
|
|
129
|
+
const id = (row.cells[0] ?? '').replace(/[*`[\]]/g, '').replace(/\(.*\)$/, '').trim();
|
|
130
|
+
if (!id) continue;
|
|
131
|
+
if (out.length >= MAX_ROWS) { more += 1; continue; }
|
|
132
|
+
const named: Record<string, string> = {};
|
|
133
|
+
table.header.forEach((h, k) => { named[h] = clip(row.cells[k] ?? '', 300); });
|
|
134
|
+
out.push({ id: clip(id, 80), cells: named, line: row.line });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return { rows: out, more };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function parseArrangement(kind: ArrangementKind, text: string): { parsed: boolean; data: ArrangementData } {
|
|
141
|
+
if (kind === 'status') {
|
|
142
|
+
try {
|
|
143
|
+
const json = JSON.parse(text) as Record<string, unknown>;
|
|
144
|
+
const pre = (json.preflight ?? {}) as Record<string, unknown>;
|
|
145
|
+
const str = (v: unknown) => (typeof v === 'string' ? v : typeof v === 'number' || typeof v === 'boolean' ? String(v) : null);
|
|
146
|
+
const keep: Record<string, unknown> = {};
|
|
147
|
+
for (const k of ['status', 'agent', 'mode', 'model', 'effort', 'worktree', 'started_at', 'finished_at', 'exit_code', 'reason', 'head_after', 'new_commits', 'prompt', 'report', 'timeout_seconds']) if (json[k] !== undefined) keep[k] = json[k];
|
|
148
|
+
if (pre.base_commit !== undefined) keep.base_commit = pre.base_commit;
|
|
149
|
+
const status = str(json.status);
|
|
150
|
+
return {
|
|
151
|
+
parsed: true,
|
|
152
|
+
data: {
|
|
153
|
+
status, running: status !== null && RUNNING.test(status), agent: str(json.agent), model: str(json.model), worktree: str(json.worktree),
|
|
154
|
+
baseline: str(pre.base_commit) ?? str(json.base_commit), accepted: null, json: keep,
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
} catch {
|
|
158
|
+
return { parsed: false, data: {} };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const fields = frontMatter(text);
|
|
162
|
+
const title = /^\s{0,3}#\s+(.+)$/m.exec(text)?.[1]?.trim();
|
|
163
|
+
const table = kind === 'index' || kind === 'plan' ? tables(text) : { rows: [], more: 0 };
|
|
164
|
+
const rows = table.rows;
|
|
165
|
+
const lines = text.split(/\r?\n/);
|
|
166
|
+
const batches: string[] = [];
|
|
167
|
+
const dependencies: string[] = [];
|
|
168
|
+
const parallel: string[] = [];
|
|
169
|
+
const omitted: { rows?: number; batches?: number; dependencies?: number; parallel?: number } = {};
|
|
170
|
+
if (table.more) omitted.rows = table.more;
|
|
171
|
+
const keep = (list: string[], which: 'batches' | 'dependencies' | 'parallel', l: string) => {
|
|
172
|
+
if (list.length < MAX_LINES) list.push(clip(l));
|
|
173
|
+
else omitted[which] = (omitted[which] ?? 0) + 1;
|
|
174
|
+
};
|
|
175
|
+
for (const raw of lines) {
|
|
176
|
+
const l = raw.trim();
|
|
177
|
+
if (!l || l.length > 600) continue;
|
|
178
|
+
if (/批次\s*\d|第\s*\d+\s*批|batch\s*\d|^#{1,6}\s.*(批次|batch|阶段|phase)/i.test(l)) keep(batches, 'batches', l);
|
|
179
|
+
if (/依赖|depends on|blocked by|等.{0,10}完成|需要先|before it|after\s+[A-Z]{2}\b|upstream|blocks:/i.test(l)) keep(dependencies, 'dependencies', l);
|
|
180
|
+
if (/并行|可并行|同时|parallel|in parallel|concurrently/i.test(l)) keep(parallel, 'parallel', l);
|
|
181
|
+
}
|
|
182
|
+
const f = fields ?? {};
|
|
183
|
+
const pick = (...keys: string[]) => { for (const k of keys) if (f[k]) return f[k]!; return null; };
|
|
184
|
+
const status = pick('status', 'state');
|
|
185
|
+
const worktree = pick('worktree', 'worktree_path');
|
|
186
|
+
const baseline = pick('base_commit', 'baseline_commit', 'baseline', 'base');
|
|
187
|
+
const structured = Boolean(fields) || rows.length > 0 || (kind === 'receipt' && Boolean(title));
|
|
188
|
+
return {
|
|
189
|
+
parsed: structured,
|
|
190
|
+
data: {
|
|
191
|
+
...(title ? { title: clip(title, 200) } : {}),
|
|
192
|
+
...(fields ? { fields: Object.fromEntries(Object.entries(fields).map(([k, v]) => [k, clip(redact(v), 400)])) } : {}),
|
|
193
|
+
status, running: status !== null && RUNNING.test(status.split(/[\s((]/)[0] ?? ''),
|
|
194
|
+
agent: pick('executor', 'agent', 'worker'), model: pick('requested_model', 'model', 'signed_model'),
|
|
195
|
+
worktree, baseline: baseline && HASH.test(baseline) ? HASH.exec(baseline)![0] : baseline, accepted: pick('accepted_commit', 'merged', 'merge_commit'),
|
|
196
|
+
...(rows.length ? { rows } : {}),
|
|
197
|
+
...(batches.length ? { batches } : {}), ...(dependencies.length ? { dependencies } : {}), ...(parallel.length ? { parallel } : {}),
|
|
198
|
+
...(Object.keys(omitted).length ? { omitted } : {}),
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** The parsed fields as stored: credential values redacted (the markers keep the JSON valid; a value is never kept whole). */
|
|
204
|
+
function storedData(data: ArrangementData): string {
|
|
205
|
+
const red = redact(JSON.stringify(data));
|
|
206
|
+
try { JSON.parse(red); return red; } catch { return JSON.stringify({ title: data.title ?? null, status: data.status ?? null, note: 'fields left out: they held a credential' }); }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export interface ArrangementScanStats { readonly files: number; readonly versions: number; readonly versionsAdded: number; readonly unparsed: number }
|
|
210
|
+
|
|
211
|
+
/** Parse every committed version of every arrangement file not read yet; mark the versions the checkout has now. */
|
|
212
|
+
export function scanArrangements(db: DatabaseSync, repo: RepoHandle, now: string): ArrangementScanStats {
|
|
213
|
+
const wanted = (db.prepare(`
|
|
214
|
+
SELECT cf.hash, cf.path, cf.new_blob, c.author_at, c.author_ms, c.import_root FROM commit_files cf JOIN commits c ON c.repo = cf.repo AND c.hash = cf.hash
|
|
215
|
+
WHERE cf.repo = ? AND cf.status <> 'D' AND cf.new_blob IS NOT NULL
|
|
216
|
+
AND NOT EXISTS (SELECT 1 FROM plans p WHERE p.repo = cf.repo AND p.path = cf.path AND p.commit_hash = cf.hash)
|
|
217
|
+
ORDER BY c.author_ms, cf.hash`).all(repo.id) as { hash: string; path: string; new_blob: string; author_at: string; author_ms: number; import_root: number }[])
|
|
218
|
+
.filter((r) => arrangementKind(r.path) !== null);
|
|
219
|
+
const missingText = wanted.filter((w) => getText(db, blobTextKey(repo.id, w.new_blob)) === null).map((w) => w.new_blob);
|
|
220
|
+
const fetched = catBlobs(repo.path, missingText);
|
|
221
|
+
const ins = db.prepare(`INSERT OR IGNORE INTO plans (key, repo, path, commit_hash, blob, kind, ident, parsed, data, occurred_at, occurred_ms, occurred_basis, occurred_anchor, other_at, other_basis, undated)
|
|
222
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
223
|
+
let added = 0;
|
|
224
|
+
tx(db, () => {
|
|
225
|
+
for (const w of wanted) {
|
|
226
|
+
const stored = getText(db, blobTextKey(repo.id, w.new_blob));
|
|
227
|
+
const buf = stored === null ? fetched.get(w.new_blob) : null;
|
|
228
|
+
const text = stored ?? (buf && isText(buf) ? buf.toString('utf8') : null);
|
|
229
|
+
const kind = arrangementKind(w.path)!;
|
|
230
|
+
const { parsed, data } = text === null ? { parsed: false, data: {} as ArrangementData } : parseArrangement(kind, text);
|
|
231
|
+
const occ: Occurred = text === null
|
|
232
|
+
? { at: materialTime(w.author_at) ?? w.author_at, basis: 'Commit', anchor: `${w.path}@${w.hash.slice(0, 10)}` }
|
|
233
|
+
: occurredOfLine({ commit: w.hash, commitAt: w.author_at, importRoot: w.import_root === 1, path: w.path, firstSeen: w.author_at, fileTime: null }, datesOf(`${repo.id}:${w.new_blob}`, text), null);
|
|
234
|
+
const id = identOf(w.path, data.fields ?? null);
|
|
235
|
+
if (ins.run(planKey(w.path, w.hash), repo.id, w.path, w.hash, w.new_blob, kind, id, parsed ? 1 : 0, storedData(data), occ.at, msOf(occ.at), occ.basis, occ.anchor,
|
|
236
|
+
occ.other?.at ?? null, occ.other?.basis ?? null, occ.undated ? 1 : 0).changes > 0) added += 1;
|
|
237
|
+
}
|
|
238
|
+
db.prepare('UPDATE plans SET current = 0 WHERE repo = ?').run(repo.id);
|
|
239
|
+
db.prepare('UPDATE plans SET current = 1 WHERE repo = ? AND EXISTS (SELECT 1 FROM code_files cf WHERE cf.repo = plans.repo AND cf.path = plans.path AND cf.blob = plans.blob)').run(repo.id);
|
|
240
|
+
});
|
|
241
|
+
void now;
|
|
242
|
+
const totals = db.prepare('SELECT count(DISTINCT path) f, count(*) v, sum(parsed = 0) u FROM plans WHERE repo = ?').get(repo.id) as { f: number; v: number; u: number | null };
|
|
243
|
+
return { files: totals.f, versions: totals.v, versionsAdded: added, unparsed: totals.u ?? 0 };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** An arrangement file outside version control, or one not committed yet: dated by its written date, else its file time. */
|
|
247
|
+
export function scanLooseArrangement(db: DatabaseSync, path: string, rel: string, text: string, fileTime: string): void {
|
|
248
|
+
const kind = arrangementKind(rel);
|
|
249
|
+
if (!kind) return;
|
|
250
|
+
const { parsed, data } = parseArrangement(kind, text);
|
|
251
|
+
const w = datesOf(`loose:${path}:${fileTime}`, text).document;
|
|
252
|
+
const occ: Occurred = w ? { at: w.at, basis: 'Written in text', anchor: `${path}:${w.line}`, other: { at: fileTime, basis: 'File time', anchor: path } } : { at: fileTime, basis: 'File time', anchor: path };
|
|
253
|
+
db.prepare('DELETE FROM plans WHERE repo IS NULL AND path = ?').run(path);
|
|
254
|
+
db.prepare(`INSERT INTO plans (key, repo, path, commit_hash, blob, kind, ident, parsed, data, current, occurred_at, occurred_ms, occurred_basis, occurred_anchor, other_at, other_basis, undated)
|
|
255
|
+
VALUES (?, NULL, ?, NULL, NULL, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, 0)`)
|
|
256
|
+
.run(planKey(path, null), path, kind, identOf(rel, data.fields ?? null), parsed ? 1 : 0, storedData(data), occ.at, msOf(occ.at), occ.basis, occ.anchor, occ.other?.at ?? null, occ.other?.basis ?? null);
|
|
257
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Current source-line provenance. Git is read only; the cache stores ranges, never source text. Source is the code whose
|
|
3
|
+
* references a reader reads (code_files.reader: the TypeScript compiler, or the code engine for every other language).
|
|
4
|
+
*/
|
|
5
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
6
|
+
import { ledgerGit } from './git-read.ts';
|
|
7
|
+
import type { RepoHandle } from './repo-scan.ts';
|
|
8
|
+
import { tx } from './schema.ts';
|
|
9
|
+
|
|
10
|
+
export interface BlameSegment { readonly start: number; readonly end: number; readonly commit: string }
|
|
11
|
+
export interface BlameScanStats { readonly scanned: number; readonly reused: number; readonly failed: number; readonly ms: number }
|
|
12
|
+
|
|
13
|
+
/** `--incremental` prints one range header per contiguous run, in arbitrary range order. */
|
|
14
|
+
export function parseBlame(output: string, expectedLines: number): BlameSegment[] | null {
|
|
15
|
+
const segments: BlameSegment[] = [];
|
|
16
|
+
for (const line of output.split(/\r?\n/)) {
|
|
17
|
+
const m = /^\^?([0-9a-f]{40}) \d+ (\d+) (\d+)$/.exec(line);
|
|
18
|
+
if (!m) continue;
|
|
19
|
+
const start = Number(m[2]);
|
|
20
|
+
const count = Number(m[3]);
|
|
21
|
+
if (start < 1 || count < 1) return null;
|
|
22
|
+
segments.push({ start, end: start + count - 1, commit: m[1]! });
|
|
23
|
+
}
|
|
24
|
+
segments.sort((a, b) => a.start - b.start);
|
|
25
|
+
let next = 1;
|
|
26
|
+
for (const s of segments) { if (s.start !== next) return null; next = s.end + 1; }
|
|
27
|
+
return next - 1 === expectedLines ? segments : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A changed content id is blamed once; unchanged files survive an incremental rebuild untouched. */
|
|
31
|
+
export function scanBlame(db: DatabaseSync, repo: RepoHandle, force = false): BlameScanStats {
|
|
32
|
+
const started = Date.now();
|
|
33
|
+
const head = (db.prepare('SELECT head FROM repos WHERE id = ?').get(repo.id) as { head: string | null } | undefined)?.head;
|
|
34
|
+
const source = db.prepare('SELECT path, blob, lines FROM code_files WHERE repo = ? AND generated = 0 AND classification IS NULL AND blob IS NOT NULL AND lines IS NOT NULL AND reader IS NOT NULL')
|
|
35
|
+
.all(repo.id) as { path: string; blob: string; lines: number }[];
|
|
36
|
+
const existing = new Map((db.prepare('SELECT path, blob FROM code_blame WHERE repo = ?').all(repo.id) as { path: string; blob: string }[]).map((r) => [r.path, r.blob]));
|
|
37
|
+
const keep = new Set(source.map((f) => f.path));
|
|
38
|
+
let scanned = 0;
|
|
39
|
+
let reused = 0;
|
|
40
|
+
let failed = 0;
|
|
41
|
+
const updates: { path: string; blob: string; lines: number; segments: BlameSegment[] }[] = [];
|
|
42
|
+
if (head) for (const f of source) {
|
|
43
|
+
if (!force && existing.get(f.path) === f.blob) { reused++; continue; }
|
|
44
|
+
if (f.lines === 0) { updates.push({ ...f, segments: [] }); scanned++; continue; }
|
|
45
|
+
const result = ledgerGit(repo.path, ['blame', '--incremental', head, '--', f.path], { timeoutMs: 180_000 });
|
|
46
|
+
const segments = result.ok ? parseBlame(result.out, f.lines) : null;
|
|
47
|
+
if (!segments) { failed++; continue; }
|
|
48
|
+
updates.push({ ...f, segments });
|
|
49
|
+
scanned++;
|
|
50
|
+
}
|
|
51
|
+
tx(db, () => {
|
|
52
|
+
const put = db.prepare('INSERT OR REPLACE INTO code_blame (repo, path, blob, lines, segments, computed_at) VALUES (?, ?, ?, ?, ?, ?)');
|
|
53
|
+
for (const u of updates) put.run(repo.id, u.path, u.blob, u.lines, JSON.stringify(u.segments), new Date().toISOString());
|
|
54
|
+
const del = db.prepare('DELETE FROM code_blame WHERE repo = ? AND path = ?');
|
|
55
|
+
for (const path of existing.keys()) if (!keep.has(path)) del.run(repo.id, path);
|
|
56
|
+
});
|
|
57
|
+
return { scanned, reused, failed, ms: Date.now() - started };
|
|
58
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The code engine's child process (code-engine.ts). It is started with V8's `--liftoff-only`, `DO_NOT_TRACK=1` and git
|
|
3
|
+
* blind to any repository around it, never inside a project, and prints one JSON line:
|
|
4
|
+
*
|
|
5
|
+
* index <tree> index the snapshot, or bring its index up to date (`sync`); an interrupted or outdated index is made
|
|
6
|
+
* again from nothing
|
|
7
|
+
* imports <path> stdin: a JSON array of versions of one file (a version may be null); for each, the import specifiers
|
|
8
|
+
* the engine extracts from it
|
|
9
|
+
*
|
|
10
|
+
* It imports nothing of ProjectKeeper, so starting it costs Node and the engine only.
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
import type { CodeGraph as CodeGraphInstance } from '@colbymchenry/codegraph';
|
|
15
|
+
|
|
16
|
+
type EngineModule = typeof import('@colbymchenry/codegraph');
|
|
17
|
+
const lib = createRequire(import.meta.url)('@colbymchenry/codegraph') as EngineModule;
|
|
18
|
+
lib.setLogger(lib.silentLogger);
|
|
19
|
+
|
|
20
|
+
const print = (value: unknown): void => { process.stdout.write(`${JSON.stringify(value)}\n`); };
|
|
21
|
+
const peakMemoryMB = (): number => Math.round(process.resourceUsage().maxRSS / 1024);
|
|
22
|
+
|
|
23
|
+
async function index(tree: string): Promise<void> {
|
|
24
|
+
const started = performance.now();
|
|
25
|
+
const { CodeGraph } = lib;
|
|
26
|
+
let cg: CodeGraphInstance;
|
|
27
|
+
let mode: 'full' | 'sync' = 'full';
|
|
28
|
+
if (CodeGraph.isInitialized(tree)) {
|
|
29
|
+
cg = await CodeGraph.open(tree);
|
|
30
|
+
if (cg.getIndexState() === 'complete' && !cg.isIndexStale()) {
|
|
31
|
+
await cg.sync();
|
|
32
|
+
mode = 'sync';
|
|
33
|
+
} else {
|
|
34
|
+
cg.close();
|
|
35
|
+
cg = await CodeGraph.recreate(tree);
|
|
36
|
+
await cg.indexAll();
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
cg = await CodeGraph.init(tree);
|
|
40
|
+
await cg.indexAll();
|
|
41
|
+
}
|
|
42
|
+
const stats = cg.getStats();
|
|
43
|
+
const state = cg.getIndexState();
|
|
44
|
+
cg.close();
|
|
45
|
+
// `partial`: finished, some files dropped — usable, and said; `failed` or still `indexing`: not usable.
|
|
46
|
+
const usable = state !== 'failed' && state !== 'indexing';
|
|
47
|
+
print({ ok: usable, mode, state, ms: Math.round(performance.now() - started), peakMemoryMB: peakMemoryMB(), files: stats.fileCount, nodes: stats.nodeCount, edges: stats.edgeCount, ...(usable ? {} : { error: `the index ended ${state}` }) });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function imports(path: string): Promise<void> {
|
|
51
|
+
const sources = JSON.parse(readFileSync(0, 'utf8')) as (string | null)[];
|
|
52
|
+
const languages = [...new Set(sources.filter((s): s is string => typeof s === 'string').map((s) => lib.detectLanguage(path, s)))];
|
|
53
|
+
await lib.initGrammars();
|
|
54
|
+
await lib.loadGrammarsForLanguages(languages);
|
|
55
|
+
// `extractFromSource` reads nothing of the instance it hangs on; it parses one text.
|
|
56
|
+
const extract = lib.CodeGraph.prototype.extractFromSource;
|
|
57
|
+
const out = sources.map((s) => (typeof s === 'string' ? extract.call(null as unknown as CodeGraphInstance, path, s).nodes.filter((n) => n.kind === 'import').map((n) => n.name) : null));
|
|
58
|
+
print({ ok: true, imports: out, peakMemoryMB: peakMemoryMB() });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const [mode, arg] = process.argv.slice(2);
|
|
62
|
+
try {
|
|
63
|
+
if (mode === 'index' && arg) await index(arg);
|
|
64
|
+
else if (mode === 'imports' && arg) await imports(arg);
|
|
65
|
+
else { print({ ok: false, error: `usage: index <tree> | imports <path> (got ${mode ?? 'nothing'})` }); process.exitCode = 2; }
|
|
66
|
+
} catch (error) {
|
|
67
|
+
print({ ok: false, error: (error as Error).message });
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
}
|