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,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version history (Spec §1.16 row 1; CKC-22 AC-1, AC-2): every commit on every ref — heads, tags, remotes, the stash and
|
|
3
|
+
* any other namespace (`refs/codex/turn-diffs/…`) — with no cap. Per commit: hash, parents, author and committer
|
|
4
|
+
* time (both kept; they differ after a rebase or a cherry-pick), author, subject and body, and the files it changed against
|
|
5
|
+
* its first parent with status, rename source, content ids and line counts. Also: which ref namespaces reach each commit,
|
|
6
|
+
* the trunk and each commit's place on it, every branch (local and remote-tracking) and whether its tip is in the trunk,
|
|
7
|
+
* and the worktrees with their uncommitted changes.
|
|
8
|
+
*
|
|
9
|
+
* Incremental (AC-2): only commits reachable from a ref that moved, and from none of the tips already recorded, are read;
|
|
10
|
+
* what is recorded stays recorded (a force-pushed or deleted ref keeps its commits, which were real history when read).
|
|
11
|
+
*/
|
|
12
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
13
|
+
import { commitKey, putText, redact, tx } from './schema.ts';
|
|
14
|
+
import { materialTime, msOf } from './time.ts';
|
|
15
|
+
import {
|
|
16
|
+
defaultBranch, firstParentLine, headOf, isAncestor, isHash, ledgerGit, listRefs, logDetails, revList, uncommitted, worktreeList,
|
|
17
|
+
type RefInfo, type RefNamespace,
|
|
18
|
+
} from './git-read.ts';
|
|
19
|
+
|
|
20
|
+
export interface RepoHandle {
|
|
21
|
+
/** Scope item id. */
|
|
22
|
+
readonly id: string;
|
|
23
|
+
/** The repository's own checkout; a worktree folds into its main repository. */
|
|
24
|
+
readonly path: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface RepoScanStats {
|
|
28
|
+
readonly repo: string;
|
|
29
|
+
readonly refs: number;
|
|
30
|
+
readonly refsByNamespace: Readonly<Record<string, number>>;
|
|
31
|
+
readonly commits: number;
|
|
32
|
+
readonly commitsAdded: number;
|
|
33
|
+
readonly unreadCommits: number;
|
|
34
|
+
readonly merges: number;
|
|
35
|
+
readonly trunk: string | null;
|
|
36
|
+
readonly trunkCommits: number;
|
|
37
|
+
readonly trunkMerges: number;
|
|
38
|
+
readonly branches: number;
|
|
39
|
+
readonly branchesMerged: number;
|
|
40
|
+
readonly worktrees: number;
|
|
41
|
+
readonly head: string | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The time a commit gives, kept as git wrote it (the author's own offset, so the local day stays readable), and its milliseconds. */
|
|
45
|
+
function commitTime(raw: string): { at: string; ms: number } {
|
|
46
|
+
return { at: raw, ms: msOf(materialTime(raw) ?? raw) ?? 0 };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Scan one repository's refs, commits, branches, merges and worktrees into the ledger. */
|
|
50
|
+
export function scanRepo(db: DatabaseSync, repo: RepoHandle, now: string): RepoScanStats {
|
|
51
|
+
// Every ref is recorded; only refs to commits have a history to walk.
|
|
52
|
+
const allRefs = listRefs(repo.path);
|
|
53
|
+
const refs = allRefs.filter((r) => r.type === 'commit');
|
|
54
|
+
const head = headOf(repo.path);
|
|
55
|
+
const previous = new Map((db.prepare('SELECT refname, tip, present FROM refs WHERE repo = ?').all(repo.id) as { refname: string; tip: string; present: number }[])
|
|
56
|
+
.map((r) => [r.refname, r]));
|
|
57
|
+
const known = new Set((db.prepare('SELECT hash FROM commits WHERE repo = ?').all(repo.id) as { hash: string }[]).map((r) => r.hash));
|
|
58
|
+
|
|
59
|
+
// What is new: reachable from a moved ref (or a detached HEAD), and from none of the tips already recorded.
|
|
60
|
+
const recordedTips = [...new Set([...previous.values()].filter((r) => isHash(r.tip) && known.has(r.tip)).map((r) => r.tip))];
|
|
61
|
+
const moved = refs.filter((r) => previous.get(r.refname)?.tip !== r.tip || !known.has(r.tip)).map((r) => r.tip);
|
|
62
|
+
if (head.hash && !known.has(head.hash)) moved.push(head.hash);
|
|
63
|
+
const fresh = moved.length ? (revList(repo.path, [...new Set(moved)], recordedTips) ?? []) : [];
|
|
64
|
+
const toRead = fresh.filter((h) => !known.has(h));
|
|
65
|
+
const { details, failed } = logDetails(repo.path, toRead);
|
|
66
|
+
|
|
67
|
+
const insCommit = db.prepare(`INSERT OR IGNORE INTO commits
|
|
68
|
+
(key, repo, hash, parents, author_at, author_ms, committer_at, committer_ms, author, subject, body, merge, first_seen)
|
|
69
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
70
|
+
const insFile = db.prepare(`INSERT OR REPLACE INTO commit_files (repo, hash, status, path, old_path, old_blob, new_blob, added, deleted)
|
|
71
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
72
|
+
let added = 0;
|
|
73
|
+
tx(db, () => {
|
|
74
|
+
for (const d of details) {
|
|
75
|
+
const a = commitTime(d.authorAt);
|
|
76
|
+
const c = commitTime(d.committerAt || d.authorAt);
|
|
77
|
+
// A credential in a commit message never enters the ledger (Spec §3.1): the message is kept redacted.
|
|
78
|
+
const w = insCommit.run(commitKey(d.hash), repo.id, d.hash, d.parents.join(' '), a.at, a.ms, c.at, c.ms, d.author, redact(d.subject), redact(d.body), d.parents.length >= 2 ? 1 : 0, now);
|
|
79
|
+
if (w.changes === 0) continue;
|
|
80
|
+
added += 1;
|
|
81
|
+
for (const f of d.files) insFile.run(repo.id, d.hash, f.status, f.path, f.from, f.oldBlob, f.newBlob, f.added, f.deleted);
|
|
82
|
+
putText(db, `commit:${repo.id}:${d.hash}`, 'commit', repo.id, `${d.subject}\n\n${d.body}`.trim());
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const refsChanged = added > 0 || allRefs.length !== [...previous.values()].filter((r) => r.present === 1).length || allRefs.some((r) => previous.get(r.refname)?.tip !== r.tip || previous.get(r.refname)?.present !== 1);
|
|
87
|
+
tx(db, () => {
|
|
88
|
+
const upRef = db.prepare('INSERT OR REPLACE INTO refs (repo, refname, namespace, type, tip, present, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)');
|
|
89
|
+
const seen = new Set(allRefs.map((r) => r.refname));
|
|
90
|
+
for (const [name, old] of previous) if (!seen.has(name) && old.present === 1) upRef.run(repo.id, name, namespaceLabel(name), 'commit', old.tip, 0, now);
|
|
91
|
+
for (const r of allRefs) if (previous.get(r.refname)?.tip !== r.tip || previous.get(r.refname)?.present !== 1) upRef.run(repo.id, r.refname, r.namespace, r.type, r.tip, 1, now);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Trunk and the namespaces each commit is reachable from: recomputed when any ref moved.
|
|
95
|
+
const trunk = defaultBranch(repo.path, refs);
|
|
96
|
+
const trunkTip = trunk ? refs.find((r) => r.refname === `refs/heads/${trunk}`)?.tip ?? null : head.hash;
|
|
97
|
+
const trunkList = trunkTip ? revList(repo.path, [trunkTip]) : null;
|
|
98
|
+
const trunkSet = new Set(trunkList ?? []);
|
|
99
|
+
/** Whether the trunk reaches a commit; null when there is no trunk or git could not list it. */
|
|
100
|
+
const inTrunk = (h: string | null): boolean | null => (h && trunkList ? trunkSet.has(h) : null);
|
|
101
|
+
const stored = db.prepare('SELECT trunk_tip FROM repos WHERE id = ?').get(repo.id) as { trunk_tip: string | null } | undefined;
|
|
102
|
+
if (refsChanged || stored?.trunk_tip !== trunkTip) {
|
|
103
|
+
const firstParent = new Set(trunkTip ? firstParentLine(repo.path, trunkTip) : []);
|
|
104
|
+
const byNs = new Map<RefNamespace, string[]>();
|
|
105
|
+
for (const r of refs) byNs.set(r.namespace, [...(byNs.get(r.namespace) ?? []), r.tip]);
|
|
106
|
+
const reach = new Map<string, string[]>();
|
|
107
|
+
for (const [ns, tips] of byNs) {
|
|
108
|
+
for (const h of revList(repo.path, [...new Set(tips)]) ?? []) reach.set(h, [...(reach.get(h) ?? []), ns]);
|
|
109
|
+
}
|
|
110
|
+
if (head.hash && !head.branch) for (const h of revList(repo.path, [head.hash]) ?? []) if (!reach.has(h)) reach.set(h, ['HEAD']);
|
|
111
|
+
const order: readonly string[] = ['heads', 'remotes', 'tags', 'stash', 'other', 'HEAD'];
|
|
112
|
+
const up = db.prepare('UPDATE commits SET on_trunk = ?, first_parent_trunk = ?, reach = ? WHERE repo = ? AND hash = ?');
|
|
113
|
+
const all = db.prepare('SELECT hash, on_trunk, first_parent_trunk, reach FROM commits WHERE repo = ?').all(repo.id) as { hash: string; on_trunk: number; first_parent_trunk: number; reach: string }[];
|
|
114
|
+
tx(db, () => {
|
|
115
|
+
for (const c of all) {
|
|
116
|
+
const t = trunkSet.has(c.hash) ? 1 : 0;
|
|
117
|
+
const f = firstParent.has(c.hash) ? 1 : 0;
|
|
118
|
+
const r = (reach.get(c.hash) ?? []).sort((a, b) => order.indexOf(a) - order.indexOf(b)).join(',');
|
|
119
|
+
if (t !== c.on_trunk || f !== c.first_parent_trunk || r !== c.reach) up.run(t, f, r, repo.id, c.hash);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Branches: local and remote-tracking; merged when the trunk reaches the tip.
|
|
125
|
+
const branchRefs = refs.filter((r) => r.namespace === 'heads' || r.namespace === 'remotes');
|
|
126
|
+
const tipAt = db.prepare('SELECT author_at FROM commits WHERE repo = ? AND hash = ?');
|
|
127
|
+
const oldBranches = new Map((db.prepare('SELECT name, tip, merged, ahead FROM branches WHERE repo = ?').all(repo.id) as { name: string; tip: string; merged: number | null; ahead: number | null }[]).map((b) => [b.name, b]));
|
|
128
|
+
let merged = 0;
|
|
129
|
+
tx(db, () => {
|
|
130
|
+
db.prepare('DELETE FROM branches WHERE repo = ?').run(repo.id);
|
|
131
|
+
const ins = db.prepare('INSERT INTO branches (repo, name, namespace, tip, tip_at, merged, ahead, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)');
|
|
132
|
+
for (const b of branchRefs) {
|
|
133
|
+
const name = b.refname.replace(/^refs\/(heads|remotes)\//, '');
|
|
134
|
+
const old = oldBranches.get(name);
|
|
135
|
+
let isMerged: boolean | null = inTrunk(b.tip);
|
|
136
|
+
let ahead: number | null = isMerged ? 0 : null;
|
|
137
|
+
if (trunkTip && isMerged === false) {
|
|
138
|
+
if (old && old.tip === b.tip && stored?.trunk_tip === trunkTip) ahead = old.ahead;
|
|
139
|
+
else {
|
|
140
|
+
const r = ledgerGit(repo.path, ['rev-list', '--count', b.tip, `^${trunkTip}`], { timeoutMs: 60_000 });
|
|
141
|
+
ahead = r.ok ? Number(r.out.trim()) : null;
|
|
142
|
+
if (!r.ok) isMerged = null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (isMerged) merged += 1;
|
|
146
|
+
ins.run(repo.id, name, b.namespace, b.tip, (tipAt.get(repo.id, b.tip) as { author_at: string } | undefined)?.author_at ?? null,
|
|
147
|
+
isMerged === null ? null : isMerged ? 1 : 0, ahead, now);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Worktrees, with their uncommitted changes and whether the trunk already has their HEAD.
|
|
152
|
+
const wts = worktreeList(repo.path).filter((w) => !w.bare);
|
|
153
|
+
tx(db, () => {
|
|
154
|
+
db.prepare('DELETE FROM worktrees WHERE repo = ?').run(repo.id);
|
|
155
|
+
const ins = db.prepare('INSERT OR REPLACE INTO worktrees (repo, path, branch, head, merged, detached, uncommitted, uncommitted_list, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
|
156
|
+
for (const w of wts) {
|
|
157
|
+
const dirty = w.prunable ? null : uncommitted(w.path);
|
|
158
|
+
const wtMerged = inTrunk(w.head) ?? (w.head && trunkTip ? isAncestor(repo.path, w.head, trunkTip) : null);
|
|
159
|
+
ins.run(repo.id, w.path, w.branch, w.head, wtMerged === null ? null : wtMerged ? 1 : 0, w.detached ? 1 : 0,
|
|
160
|
+
// Redacted like everything else the ledger keeps (QC AY): a file name or a status line never carries a credential out.
|
|
161
|
+
dirty ? dirty.length : 0, JSON.stringify(dirty === null ? ['(the worktree cannot be read: its directory is gone or git cannot open it)'] : dirty.slice(0, 500).map((line) => redact(line))), now);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const totals = db.prepare('SELECT count(*) c, sum(merge) m, sum(on_trunk) t, sum(on_trunk * merge) tm, min(author_at) first FROM commits WHERE repo = ?').get(repo.id) as { c: number; m: number | null; t: number | null; tm: number | null; first: string | null };
|
|
166
|
+
db.prepare(`INSERT INTO repos (id, path, trunk, trunk_tip, head, head_branch, version_from) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
167
|
+
ON CONFLICT(id) DO UPDATE SET path = excluded.path, trunk = excluded.trunk, trunk_tip = excluded.trunk_tip, head = excluded.head,
|
|
168
|
+
head_branch = excluded.head_branch, version_from = excluded.version_from`)
|
|
169
|
+
.run(repo.id, repo.path, trunk, trunkTip, head.hash, head.branch, totals.first);
|
|
170
|
+
|
|
171
|
+
const byNamespace: Record<string, number> = {};
|
|
172
|
+
for (const r of allRefs) byNamespace[r.namespace] = (byNamespace[r.namespace] ?? 0) + 1;
|
|
173
|
+
return {
|
|
174
|
+
repo: repo.id, refs: allRefs.length, refsByNamespace: byNamespace, commits: totals.c, commitsAdded: added, unreadCommits: failed,
|
|
175
|
+
merges: totals.m ?? 0, trunk, trunkCommits: totals.t ?? 0, trunkMerges: totals.tm ?? 0,
|
|
176
|
+
branches: branchRefs.length, branchesMerged: merged, worktrees: wts.length, head: head.hash,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const namespaceLabel = (refname: string): RefInfo['namespace'] =>
|
|
181
|
+
refname.startsWith('refs/heads/') ? 'heads' : refname.startsWith('refs/tags/') ? 'tags' : refname.startsWith('refs/remotes/') ? 'remotes' : refname === 'refs/stash' ? 'stash' : 'other';
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ledger storage (CKC-22, Spec §1.16): one `ledger.sqlite` per project in its ProjectKeeper project directory, in WAL
|
|
3
|
+
* mode so the read-only connections the workbench and the Keeper's tools query through never wait on a recompute. No
|
|
4
|
+
* dependency beyond Node's own `node:sqlite`. Full-text search is FTS5 with the trigram tokenizer: a query of three
|
|
5
|
+
* characters or more uses the index; a shorter one (a two-character Chinese word) is answered by a substring scan.
|
|
6
|
+
*
|
|
7
|
+
* Every row a query hands out carries an entry id (`key`) derived from what the row is — repository, commit, path, the
|
|
8
|
+
* line's words, the session and message — never from a row number, so the same fact keeps the same id across
|
|
9
|
+
* incremental and full rebuilds and a judgement that cites it stays resolvable (keeper/evidence.ts `LedgerHook`).
|
|
10
|
+
*
|
|
11
|
+
* Credential values never enter the ledger (Spec §3.1): every text is redacted on the way in (`putText`, `redact`).
|
|
12
|
+
*/
|
|
13
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
15
|
+
import { createHash } from 'node:crypto';
|
|
16
|
+
import { existsSync, mkdirSync, renameSync } from 'node:fs';
|
|
17
|
+
import { dirname } from 'node:path';
|
|
18
|
+
import { redactCredentials } from '../sources/anchor.ts';
|
|
19
|
+
|
|
20
|
+
/** node:sqlite loads when a ledger is first opened: a process that never opens one — the CLI's client commands, which
|
|
21
|
+
* print what an agent reads — prints no experimental-feature warning. */
|
|
22
|
+
const requireBuiltin = createRequire(import.meta.url);
|
|
23
|
+
const sqlite = (): typeof import('node:sqlite') => requireBuiltin('node:sqlite') as typeof import('node:sqlite');
|
|
24
|
+
|
|
25
|
+
/** Bumped when the tables change shape; an older ledger file is set aside and rebuilt in full. */
|
|
26
|
+
export const SCHEMA_VERSION = 3;
|
|
27
|
+
|
|
28
|
+
const DDL = `
|
|
29
|
+
CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
30
|
+
CREATE TABLE IF NOT EXISTS state (key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
31
|
+
|
|
32
|
+
-- §1.16 版本历史 --------------------------------------------------------------------------------------------------
|
|
33
|
+
CREATE TABLE IF NOT EXISTS repos (
|
|
34
|
+
id TEXT PRIMARY KEY, -- scope item id
|
|
35
|
+
path TEXT NOT NULL, -- the repository's own checkout (a worktree folds into its main repository)
|
|
36
|
+
trunk TEXT, trunk_tip TEXT, -- default branch and its tip; null when git gives none
|
|
37
|
+
head TEXT, head_branch TEXT, -- what the checkout has now (the code structure is read from it)
|
|
38
|
+
version_from TEXT, -- author time of the earliest commit
|
|
39
|
+
import_rule TEXT -- why a root commit was (not) taken as import-style (AC-15), in words
|
|
40
|
+
);
|
|
41
|
+
CREATE TABLE IF NOT EXISTS refs (
|
|
42
|
+
repo TEXT NOT NULL, refname TEXT NOT NULL,
|
|
43
|
+
namespace TEXT NOT NULL, -- heads | tags | remotes | stash | other (e.g. refs/codex/turn-diffs/…)
|
|
44
|
+
type TEXT NOT NULL DEFAULT 'commit', -- what it names: commit, or tree / blob (no history to walk)
|
|
45
|
+
tip TEXT NOT NULL, present INTEGER NOT NULL DEFAULT 1, updated_at TEXT NOT NULL,
|
|
46
|
+
PRIMARY KEY (repo, refname)
|
|
47
|
+
);
|
|
48
|
+
CREATE TABLE IF NOT EXISTS commits (
|
|
49
|
+
key TEXT NOT NULL, -- commit:<hash12> (repositories that share history share the entry)
|
|
50
|
+
repo TEXT NOT NULL, hash TEXT NOT NULL,
|
|
51
|
+
parents TEXT NOT NULL, -- space-separated full hashes
|
|
52
|
+
author_at TEXT NOT NULL, author_ms INTEGER NOT NULL,
|
|
53
|
+
committer_at TEXT NOT NULL, committer_ms INTEGER NOT NULL,
|
|
54
|
+
author TEXT NOT NULL, subject TEXT NOT NULL, body TEXT NOT NULL,
|
|
55
|
+
merge INTEGER NOT NULL DEFAULT 0, -- two parents or more
|
|
56
|
+
on_trunk INTEGER NOT NULL DEFAULT 0, -- reachable from the trunk tip
|
|
57
|
+
first_parent_trunk INTEGER NOT NULL DEFAULT 0, -- on the trunk's first-parent line (a merge into the trunk is one)
|
|
58
|
+
reach TEXT NOT NULL DEFAULT '', -- ref namespaces it is reachable from, e.g. "heads,remotes"
|
|
59
|
+
import_root INTEGER NOT NULL DEFAULT 0,
|
|
60
|
+
first_seen TEXT NOT NULL,
|
|
61
|
+
scanned INTEGER NOT NULL DEFAULT 0, -- the text scans have read its message
|
|
62
|
+
PRIMARY KEY (repo, hash)
|
|
63
|
+
);
|
|
64
|
+
CREATE INDEX IF NOT EXISTS commits_ms ON commits (repo, author_ms);
|
|
65
|
+
CREATE INDEX IF NOT EXISTS commits_key ON commits (key);
|
|
66
|
+
CREATE TABLE IF NOT EXISTS commit_files (
|
|
67
|
+
repo TEXT NOT NULL, hash TEXT NOT NULL,
|
|
68
|
+
status TEXT NOT NULL, -- A M D R C T (against the first parent; a merge: what it brought into that line)
|
|
69
|
+
path TEXT NOT NULL, old_path TEXT,
|
|
70
|
+
old_blob TEXT, new_blob TEXT, -- null on the side that does not exist
|
|
71
|
+
added INTEGER, deleted INTEGER, -- null for binary
|
|
72
|
+
PRIMARY KEY (repo, hash, path)
|
|
73
|
+
);
|
|
74
|
+
CREATE INDEX IF NOT EXISTS commit_files_path ON commit_files (repo, path);
|
|
75
|
+
CREATE INDEX IF NOT EXISTS commit_files_old ON commit_files (repo, old_path);
|
|
76
|
+
CREATE TABLE IF NOT EXISTS branches (
|
|
77
|
+
repo TEXT NOT NULL, name TEXT NOT NULL, namespace TEXT NOT NULL, -- heads | remotes
|
|
78
|
+
tip TEXT NOT NULL, tip_at TEXT,
|
|
79
|
+
merged INTEGER, -- tip is an ancestor of the trunk tip; null when git could not tell
|
|
80
|
+
ahead INTEGER, -- commits it has that the trunk does not
|
|
81
|
+
updated_at TEXT NOT NULL,
|
|
82
|
+
PRIMARY KEY (repo, name)
|
|
83
|
+
);
|
|
84
|
+
CREATE TABLE IF NOT EXISTS worktrees (
|
|
85
|
+
repo TEXT NOT NULL, path TEXT NOT NULL,
|
|
86
|
+
branch TEXT, head TEXT, merged INTEGER, detached INTEGER NOT NULL DEFAULT 0,
|
|
87
|
+
uncommitted INTEGER NOT NULL DEFAULT 0, uncommitted_list TEXT NOT NULL DEFAULT '[]',
|
|
88
|
+
updated_at TEXT NOT NULL,
|
|
89
|
+
PRIMARY KEY (repo, path)
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
-- §1.16 文档的每一版 ------------------------------------------------------------------------------------------------
|
|
93
|
+
CREATE TABLE IF NOT EXISTS docs (
|
|
94
|
+
key TEXT NOT NULL, -- doc:<path>@<hash10>
|
|
95
|
+
repo TEXT NOT NULL, path TEXT NOT NULL, commit_hash TEXT NOT NULL,
|
|
96
|
+
blob TEXT NOT NULL, prev_blob TEXT, prev_path TEXT,
|
|
97
|
+
at TEXT NOT NULL, at_ms INTEGER NOT NULL,
|
|
98
|
+
change TEXT NOT NULL, -- Added | Modified | Renamed | Copied
|
|
99
|
+
sections INTEGER NOT NULL DEFAULT 0, bytes INTEGER NOT NULL DEFAULT 0, lines INTEGER NOT NULL DEFAULT 0,
|
|
100
|
+
import_root INTEGER NOT NULL DEFAULT 0,
|
|
101
|
+
added TEXT, removed TEXT, changed TEXT, -- section diff against the previous version (JSON heading paths)
|
|
102
|
+
scanned INTEGER NOT NULL DEFAULT 0,
|
|
103
|
+
UNIQUE (repo, path, commit_hash)
|
|
104
|
+
);
|
|
105
|
+
CREATE INDEX IF NOT EXISTS docs_path ON docs (repo, path, at_ms);
|
|
106
|
+
CREATE INDEX IF NOT EXISTS docs_blob ON docs (repo, blob);
|
|
107
|
+
CREATE INDEX IF NOT EXISTS docs_key ON docs (key);
|
|
108
|
+
CREATE TABLE IF NOT EXISTS deleted_docs (
|
|
109
|
+
key TEXT NOT NULL, -- del:<path>@<hash10>
|
|
110
|
+
repo TEXT NOT NULL, path TEXT NOT NULL,
|
|
111
|
+
deleted_commit TEXT NOT NULL, deleted_at TEXT NOT NULL, deleted_ms INTEGER NOT NULL,
|
|
112
|
+
readable_at TEXT, -- the commit whose tree still has the full text (the deleting commit's first parent)
|
|
113
|
+
blob TEXT, -- the last version's content
|
|
114
|
+
cleanup INTEGER NOT NULL DEFAULT 0,
|
|
115
|
+
UNIQUE (repo, path, deleted_commit)
|
|
116
|
+
);
|
|
117
|
+
CREATE INDEX IF NOT EXISTS deleted_docs_key ON deleted_docs (key);
|
|
118
|
+
CREATE TABLE IF NOT EXISTS cleanups (
|
|
119
|
+
key TEXT NOT NULL, -- cleanup:<hash10>
|
|
120
|
+
repo TEXT NOT NULL, commit_hash TEXT NOT NULL, at TEXT NOT NULL, at_ms INTEGER NOT NULL,
|
|
121
|
+
deleted_docs INTEGER NOT NULL, deleted_total INTEGER NOT NULL,
|
|
122
|
+
catalog_before TEXT NOT NULL, catalog_after TEXT NOT NULL, -- JSON: document paths before / after
|
|
123
|
+
UNIQUE (repo, commit_hash)
|
|
124
|
+
);
|
|
125
|
+
CREATE INDEX IF NOT EXISTS cleanups_key ON cleanups (key);
|
|
126
|
+
|
|
127
|
+
-- A line of the material, kept once per document and followed through its versions: the first version it appears in
|
|
128
|
+
-- (its occurred time), the last, and whether the current version still has it. Used by the three line scans below:
|
|
129
|
+
-- doc_items says which content of which document has which line item, at which line.
|
|
130
|
+
CREATE TABLE IF NOT EXISTS doc_items (
|
|
131
|
+
repo TEXT NOT NULL, path TEXT NOT NULL, blob TEXT NOT NULL,
|
|
132
|
+
tbl TEXT NOT NULL, -- supersedes | verdicts | nums
|
|
133
|
+
key TEXT NOT NULL, line INTEGER NOT NULL,
|
|
134
|
+
PRIMARY KEY (repo, path, blob, key)
|
|
135
|
+
);
|
|
136
|
+
CREATE INDEX IF NOT EXISTS doc_items_key ON doc_items (key);
|
|
137
|
+
CREATE TABLE IF NOT EXISTS doc_scans (
|
|
138
|
+
repo TEXT NOT NULL, path TEXT NOT NULL, blob TEXT NOT NULL,
|
|
139
|
+
what TEXT NOT NULL, -- lines (supersessions, verdicts, number definitions) | mentions (numbers, per rule set)
|
|
140
|
+
PRIMARY KEY (repo, path, blob, what)
|
|
141
|
+
);
|
|
142
|
+
-- §1.16 明写的取代
|
|
143
|
+
CREATE TABLE IF NOT EXISTS supersedes (
|
|
144
|
+
key TEXT NOT NULL UNIQUE, -- sup:<hex12>
|
|
145
|
+
repo TEXT, source TEXT NOT NULL, -- content | filename | commit | loose (outside version control)
|
|
146
|
+
path TEXT, pattern TEXT NOT NULL, target TEXT, text TEXT NOT NULL,
|
|
147
|
+
replaced TEXT, replacement TEXT, syntax TEXT,
|
|
148
|
+
ident TEXT NOT NULL, -- the line in its normal form: the same line in another document or at the old path of a move
|
|
149
|
+
obsolete_list INTEGER NOT NULL DEFAULT 0, list_heading TEXT,
|
|
150
|
+
first_commit TEXT, first_line INTEGER, first_ms INTEGER,
|
|
151
|
+
occurred_at TEXT NOT NULL, occurred_basis TEXT NOT NULL, occurred_anchor TEXT,
|
|
152
|
+
other_at TEXT, other_basis TEXT, undated INTEGER NOT NULL DEFAULT 0,
|
|
153
|
+
last_commit TEXT, last_ms INTEGER, current INTEGER NOT NULL DEFAULT 0, current_line INTEGER
|
|
154
|
+
);
|
|
155
|
+
CREATE INDEX IF NOT EXISTS supersedes_path ON supersedes (repo, path);
|
|
156
|
+
-- §1.16 编号
|
|
157
|
+
CREATE TABLE IF NOT EXISTS num_rules (
|
|
158
|
+
rule TEXT PRIMARY KEY, -- the family, e.g. "D<n>", "CKC-<n>", "two letters"
|
|
159
|
+
shape TEXT NOT NULL, prefix TEXT NOT NULL,
|
|
160
|
+
basis TEXT NOT NULL, -- where the program saw it defined, and how often
|
|
161
|
+
examples TEXT NOT NULL, defined INTEGER NOT NULL, occurrences INTEGER NOT NULL DEFAULT 0
|
|
162
|
+
);
|
|
163
|
+
CREATE TABLE IF NOT EXISTS nums (
|
|
164
|
+
key TEXT NOT NULL UNIQUE, -- num:<hex12>
|
|
165
|
+
num TEXT NOT NULL, rule TEXT NOT NULL,
|
|
166
|
+
kind TEXT NOT NULL, -- doc | commit | branch | worktree | file-name | loose
|
|
167
|
+
place TEXT NOT NULL, -- definition | mention
|
|
168
|
+
position TEXT, -- where a definition stands: heading, bold entry, table first column, front matter …
|
|
169
|
+
repo TEXT, path TEXT, commit_hash TEXT, line INTEGER, context TEXT NOT NULL,
|
|
170
|
+
ident TEXT NOT NULL,
|
|
171
|
+
confidence TEXT NOT NULL DEFAULT 'stated', -- candidate: a two-letter id that is also an ordinary word, in prose
|
|
172
|
+
first_ms INTEGER, occurred_at TEXT NOT NULL, occurred_basis TEXT NOT NULL, occurred_anchor TEXT,
|
|
173
|
+
other_at TEXT, other_basis TEXT, undated INTEGER NOT NULL DEFAULT 0,
|
|
174
|
+
last_commit TEXT, last_ms INTEGER, current INTEGER NOT NULL DEFAULT 0
|
|
175
|
+
);
|
|
176
|
+
CREATE INDEX IF NOT EXISTS nums_num ON nums (num);
|
|
177
|
+
CREATE INDEX IF NOT EXISTS nums_path ON nums (repo, path);
|
|
178
|
+
CREATE INDEX IF NOT EXISTS nums_commit ON nums (kind, commit_hash);
|
|
179
|
+
-- §1.16 判定
|
|
180
|
+
CREATE TABLE IF NOT EXISTS verdicts (
|
|
181
|
+
key TEXT NOT NULL UNIQUE, -- verdict:<hex12>
|
|
182
|
+
repo TEXT, path TEXT NOT NULL,
|
|
183
|
+
kind TEXT NOT NULL, -- verdict | count | finding
|
|
184
|
+
verdict TEXT, -- normalised: pass | fail | incomplete | needs-repair | partial | done | not-done; count: "511/511"
|
|
185
|
+
confidence TEXT NOT NULL, -- stated | candidate (AC-6)
|
|
186
|
+
text TEXT NOT NULL,
|
|
187
|
+
ident TEXT NOT NULL,
|
|
188
|
+
first_commit TEXT, first_line INTEGER, first_ms INTEGER,
|
|
189
|
+
occurred_at TEXT NOT NULL, occurred_basis TEXT NOT NULL, occurred_anchor TEXT,
|
|
190
|
+
other_at TEXT, other_basis TEXT, undated INTEGER NOT NULL DEFAULT 0,
|
|
191
|
+
last_commit TEXT, last_ms INTEGER, current INTEGER NOT NULL DEFAULT 0, current_line INTEGER
|
|
192
|
+
);
|
|
193
|
+
CREATE INDEX IF NOT EXISTS verdicts_path ON verdicts (repo, path);
|
|
194
|
+
-- §1.16 执行安排: every committed version of every arrangement file, parsed.
|
|
195
|
+
CREATE TABLE IF NOT EXISTS plans (
|
|
196
|
+
key TEXT NOT NULL, -- plan:<path>@<hash10>, or plan:<path> outside version control
|
|
197
|
+
repo TEXT, path TEXT NOT NULL, commit_hash TEXT, blob TEXT,
|
|
198
|
+
kind TEXT NOT NULL, -- index | prompt | plan | status | receipt
|
|
199
|
+
ident TEXT, -- the work id it is about (AP, T5 …) when its name or fields say
|
|
200
|
+
parsed INTEGER NOT NULL, -- 0: structure unreadable — 交判断读 (AC-7)
|
|
201
|
+
data TEXT NOT NULL, -- JSON
|
|
202
|
+
current INTEGER NOT NULL DEFAULT 0,
|
|
203
|
+
occurred_at TEXT NOT NULL, occurred_ms INTEGER, occurred_basis TEXT NOT NULL, occurred_anchor TEXT,
|
|
204
|
+
other_at TEXT, other_basis TEXT, undated INTEGER NOT NULL DEFAULT 0
|
|
205
|
+
);
|
|
206
|
+
CREATE INDEX IF NOT EXISTS plans_path ON plans (repo, path);
|
|
207
|
+
CREATE INDEX IF NOT EXISTS plans_key ON plans (key);
|
|
208
|
+
CREATE UNIQUE INDEX IF NOT EXISTS plans_version ON plans (ifnull(repo, ''), path, ifnull(commit_hash, ''));
|
|
209
|
+
|
|
210
|
+
-- §1.16 代码结构 (the version the checkout has) -----------------------------------------------------------------------
|
|
211
|
+
CREATE TABLE IF NOT EXISTS code_files (
|
|
212
|
+
repo TEXT NOT NULL, path TEXT NOT NULL,
|
|
213
|
+
bytes INTEGER NOT NULL, lines INTEGER, -- null lines: binary or not read
|
|
214
|
+
lang TEXT,
|
|
215
|
+
generated INTEGER NOT NULL DEFAULT 0, generated_how TEXT,
|
|
216
|
+
blob TEXT, -- content id: the documents' current version is the one with this content
|
|
217
|
+
classification TEXT, -- the scope's classification (third-party, generated …): not product code
|
|
218
|
+
test INTEGER NOT NULL DEFAULT 0, test_cases INTEGER NOT NULL DEFAULT 0,
|
|
219
|
+
last_commit TEXT, last_at TEXT,
|
|
220
|
+
file_at TEXT, -- outside version control: the file's own time when the rebuild read it (File time)
|
|
221
|
+
first_seen TEXT, -- when a rebuild first read the file (Undated · first seen)
|
|
222
|
+
reader TEXT, -- who reads its references: compiler (TypeScript, JavaScript) | engine | null (none)
|
|
223
|
+
ref_lang TEXT, -- its language as that reader names it (the engine's for a file the engine tracks)
|
|
224
|
+
named_by TEXT, -- JSON: files naming it though no counted reference reaches it, and how (code.ts NamingKind)
|
|
225
|
+
PRIMARY KEY (repo, path)
|
|
226
|
+
);
|
|
227
|
+
CREATE TABLE IF NOT EXISTS code_deps (
|
|
228
|
+
repo TEXT NOT NULL, src TEXT NOT NULL, dst TEXT NOT NULL,
|
|
229
|
+
kind TEXT NOT NULL, -- import | reference (TypeScript); the code engine's: import | calls | references | instantiates | extends | implements …
|
|
230
|
+
external INTEGER NOT NULL DEFAULT 0, -- dst is a package or a path outside the repository's files
|
|
231
|
+
PRIMARY KEY (repo, src, dst, kind)
|
|
232
|
+
);
|
|
233
|
+
CREATE INDEX IF NOT EXISTS code_deps_dst ON code_deps (repo, dst);
|
|
234
|
+
-- Current tracked source lines: compact contiguous blame ranges, keyed by the content fingerprint.
|
|
235
|
+
-- No source text is stored here. A changed blob invalidates only its own file's ranges.
|
|
236
|
+
CREATE TABLE IF NOT EXISTS code_blame (
|
|
237
|
+
repo TEXT NOT NULL, path TEXT NOT NULL, blob TEXT NOT NULL,
|
|
238
|
+
lines INTEGER NOT NULL, segments TEXT NOT NULL, computed_at TEXT NOT NULL,
|
|
239
|
+
PRIMARY KEY (repo, path)
|
|
240
|
+
);
|
|
241
|
+
CREATE TABLE IF NOT EXISTS dir_stats (
|
|
242
|
+
repo TEXT NOT NULL, dir TEXT NOT NULL,
|
|
243
|
+
files INTEGER NOT NULL, lines INTEGER NOT NULL,
|
|
244
|
+
product_files INTEGER NOT NULL, product_lines INTEGER NOT NULL,
|
|
245
|
+
generated_files INTEGER NOT NULL, generated_lines INTEGER NOT NULL,
|
|
246
|
+
other_files INTEGER NOT NULL, other_lines INTEGER NOT NULL, -- third-party and other classified material
|
|
247
|
+
tests INTEGER NOT NULL, test_cases INTEGER NOT NULL,
|
|
248
|
+
last_at TEXT, last_commit TEXT,
|
|
249
|
+
PRIMARY KEY (repo, dir)
|
|
250
|
+
);
|
|
251
|
+
CREATE TABLE IF NOT EXISTS merge_dirs (
|
|
252
|
+
repo TEXT NOT NULL, commit_hash TEXT NOT NULL, at TEXT NOT NULL, on_trunk INTEGER NOT NULL,
|
|
253
|
+
dirs TEXT NOT NULL, -- JSON: [dir, files changed]
|
|
254
|
+
PRIMARY KEY (repo, commit_hash)
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
-- §1.16 会话 ---------------------------------------------------------------------------------------------------------
|
|
258
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
259
|
+
key TEXT NOT NULL UNIQUE, -- session:<hex12> of host + native session id + log file
|
|
260
|
+
host TEXT NOT NULL, session_id TEXT NOT NULL, file TEXT NOT NULL,
|
|
261
|
+
cwd TEXT, started_at TEXT, ended_at TEXT,
|
|
262
|
+
messages INTEGER NOT NULL DEFAULT 0, owner_messages INTEGER NOT NULL DEFAULT 0,
|
|
263
|
+
headless INTEGER NOT NULL DEFAULT 0, subagent INTEGER NOT NULL DEFAULT 0,
|
|
264
|
+
bytes INTEGER NOT NULL DEFAULT 0, mtime_ms INTEGER NOT NULL DEFAULT 0, broken_lines INTEGER NOT NULL DEFAULT 0,
|
|
265
|
+
missing INTEGER NOT NULL DEFAULT 0, -- read before, the log is gone now (§3.11)
|
|
266
|
+
unreadable TEXT, -- why it could not be read, when it could not
|
|
267
|
+
first_seen TEXT NOT NULL
|
|
268
|
+
);
|
|
269
|
+
CREATE TABLE IF NOT EXISTS session_messages (
|
|
270
|
+
key TEXT NOT NULL UNIQUE, -- msg:<hex12> of the session key + message position
|
|
271
|
+
session TEXT NOT NULL, -- sessions.key
|
|
272
|
+
idx INTEGER NOT NULL, -- position in the native log, as the session transcripts number it
|
|
273
|
+
role TEXT NOT NULL, -- user | assistant
|
|
274
|
+
speaker TEXT NOT NULL, -- owner | agent | subagent | host (by the session's structure: read.ts isOwnerMessage)
|
|
275
|
+
at TEXT,
|
|
276
|
+
text TEXT, -- verbatim (redacted) for the owner's words and the agent message each answers
|
|
277
|
+
chars INTEGER NOT NULL DEFAULT 0,
|
|
278
|
+
tools TEXT, -- the agent's tool calls in one line each (JSON), for agent messages
|
|
279
|
+
answers INTEGER, -- owner message: the position of the agent message right before it
|
|
280
|
+
UNIQUE (session, idx)
|
|
281
|
+
);
|
|
282
|
+
CREATE INDEX IF NOT EXISTS session_messages_at ON session_messages (at);
|
|
283
|
+
|
|
284
|
+
-- Everything the ledger read that a word can be searched in: document contents (once per distinct content), commit
|
|
285
|
+
-- messages, the owner's words and the agent messages they answer, documents outside version control.
|
|
286
|
+
CREATE TABLE IF NOT EXISTS texts (
|
|
287
|
+
id INTEGER PRIMARY KEY,
|
|
288
|
+
key TEXT NOT NULL UNIQUE, -- blob:<repo>:<blob> | commit:<hash12> | msg:<hex12> | loose:<path>
|
|
289
|
+
kind TEXT NOT NULL, -- doc | commit | owner | agent | loose
|
|
290
|
+
repo TEXT,
|
|
291
|
+
content TEXT NOT NULL
|
|
292
|
+
);
|
|
293
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS texts_fts USING fts5(content, tokenize='trigram');
|
|
294
|
+
|
|
295
|
+
CREATE TABLE IF NOT EXISTS rebuilds (
|
|
296
|
+
id INTEGER PRIMARY KEY,
|
|
297
|
+
started_at TEXT NOT NULL, ended_at TEXT NOT NULL, ms INTEGER NOT NULL,
|
|
298
|
+
kind TEXT NOT NULL, -- full | incremental
|
|
299
|
+
commits_added INTEGER NOT NULL DEFAULT 0,
|
|
300
|
+
stats TEXT NOT NULL
|
|
301
|
+
);
|
|
302
|
+
`;
|
|
303
|
+
|
|
304
|
+
/** Open the ledger for writing: created when missing, WAL, and rebuilt from nothing when its schema is older. */
|
|
305
|
+
export function openLedgerForWrite(path: string): DatabaseSync {
|
|
306
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
307
|
+
const db = new (sqlite().DatabaseSync)(path);
|
|
308
|
+
db.exec('PRAGMA journal_mode = WAL');
|
|
309
|
+
db.exec('PRAGMA synchronous = NORMAL');
|
|
310
|
+
db.exec('PRAGMA busy_timeout = 10000');
|
|
311
|
+
const hasMeta = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'meta'").get();
|
|
312
|
+
const version = hasMeta ? (db.prepare('SELECT value FROM meta WHERE key = ?').get('schemaVersion') as { value: string } | undefined) : undefined;
|
|
313
|
+
const tables = (db.prepare("SELECT count(*) c FROM sqlite_master WHERE type = 'table'").get() as { c: number }).c;
|
|
314
|
+
if (tables > 0 && (!version || Number(version.value) !== SCHEMA_VERSION)) {
|
|
315
|
+
// A ledger of another shape is set aside (never deleted) and this one is built in full.
|
|
316
|
+
db.close();
|
|
317
|
+
for (const suffix of ['', '-wal', '-shm']) {
|
|
318
|
+
if (existsSync(path + suffix)) renameSync(path + suffix, `${path}.schema-${version?.value ?? 'unknown'}-${Date.now()}.bak${suffix}`);
|
|
319
|
+
}
|
|
320
|
+
return openLedgerForWrite(path);
|
|
321
|
+
}
|
|
322
|
+
db.exec(DDL);
|
|
323
|
+
migrate(db);
|
|
324
|
+
db.prepare('INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)').run('schemaVersion', String(SCHEMA_VERSION));
|
|
325
|
+
return db;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Additive changes to a ledger of this schema version, made in place — nothing is set aside or rebuilt from nothing — and
|
|
330
|
+
* filled from what the ledger holds:
|
|
331
|
+
* - `code_files.file_at` and `code_files.first_seen` (CKC-22 AC-10, Spec §2.11): a code file outside version control is
|
|
332
|
+
* dated by its own time, else by the rebuild that first read it, never by the moment of a query. A file read before
|
|
333
|
+
* they were kept was read by the ledger's last rebuild at the latest, which stands as its first reading; the
|
|
334
|
+
* directories outside version control are read again at this rebuild, which records their files' own times.
|
|
335
|
+
* - `supersedes.replaced`, `replacement`, and `syntax`: old word-match rows are removed and saved document blobs and
|
|
336
|
+
* commit messages are scanned again at the next rebuild; their two sides cannot be inferred from a legacy `target`.
|
|
337
|
+
* - `code_files.reader`, `ref_lang` and `named_by` (D98: the code engine reads every language the TypeScript path does
|
|
338
|
+
* not): the code of every repository and directory is read again at the next rebuild, which fills them.
|
|
339
|
+
* Readers of a ledger not yet migrated see none of these columns and fall back (index.ts `codeFileOccurred`, `hasReader`).
|
|
340
|
+
*/
|
|
341
|
+
function migrate(db: DatabaseSync): void {
|
|
342
|
+
const codeColumns = new Set((db.prepare('PRAGMA table_info(code_files)').all() as { name: string }[]).map((c) => c.name));
|
|
343
|
+
const supColumns = new Set((db.prepare('PRAGMA table_info(supersedes)').all() as { name: string }[]).map((c) => c.name));
|
|
344
|
+
const readerColumns = ['reader', 'ref_lang', 'named_by'];
|
|
345
|
+
if (codeColumns.has('file_at') && codeColumns.has('first_seen') && supColumns.has('replaced') && supColumns.has('replacement') && supColumns.has('syntax') && readerColumns.every((c) => codeColumns.has(c))) return;
|
|
346
|
+
tx(db, () => {
|
|
347
|
+
if (!readerColumns.every((c) => codeColumns.has(c))) {
|
|
348
|
+
for (const c of readerColumns) if (!codeColumns.has(c)) db.exec(`ALTER TABLE code_files ADD COLUMN ${c} TEXT`);
|
|
349
|
+
db.prepare("DELETE FROM state WHERE key LIKE 'code:%' OR key LIKE 'loosecode:%'").run();
|
|
350
|
+
}
|
|
351
|
+
if (!codeColumns.has('file_at') || !codeColumns.has('first_seen')) {
|
|
352
|
+
if (!codeColumns.has('file_at')) db.exec('ALTER TABLE code_files ADD COLUMN file_at TEXT');
|
|
353
|
+
if (!codeColumns.has('first_seen')) db.exec('ALTER TABLE code_files ADD COLUMN first_seen TEXT');
|
|
354
|
+
const last = getState(db, 'lastRebuildAt') ?? (db.prepare('SELECT max(ended_at) at FROM rebuilds').get() as { at: string | null } | undefined)?.at ?? null;
|
|
355
|
+
if (last) db.prepare('UPDATE code_files SET first_seen = ? WHERE first_seen IS NULL').run(last);
|
|
356
|
+
db.prepare("DELETE FROM state WHERE key LIKE 'loosecode:%'").run();
|
|
357
|
+
}
|
|
358
|
+
if (!supColumns.has('replaced') || !supColumns.has('replacement') || !supColumns.has('syntax')) {
|
|
359
|
+
if (!supColumns.has('replaced')) db.exec('ALTER TABLE supersedes ADD COLUMN replaced TEXT');
|
|
360
|
+
if (!supColumns.has('replacement')) db.exec('ALTER TABLE supersedes ADD COLUMN replacement TEXT');
|
|
361
|
+
if (!supColumns.has('syntax')) db.exec('ALTER TABLE supersedes ADD COLUMN syntax TEXT');
|
|
362
|
+
// The previous word-match rows cannot be migrated by guessing sides. Re-read saved document blobs and commits.
|
|
363
|
+
db.prepare("DELETE FROM doc_items WHERE tbl = 'supersedes'").run();
|
|
364
|
+
db.prepare('DELETE FROM supersedes').run();
|
|
365
|
+
db.prepare("DELETE FROM doc_scans WHERE what = 'lines'").run();
|
|
366
|
+
db.prepare('UPDATE commits SET scanned = 0').run();
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** Open the ledger read-only for queries; null when it has never been built or has another shape. */
|
|
372
|
+
export function openLedgerReadOnly(path: string): DatabaseSync | null {
|
|
373
|
+
if (!existsSync(path)) return null;
|
|
374
|
+
try {
|
|
375
|
+
const db = new (sqlite().DatabaseSync)(path, { readOnly: true });
|
|
376
|
+
db.exec('PRAGMA busy_timeout = 10000');
|
|
377
|
+
const v = db.prepare("SELECT value FROM meta WHERE key = 'schemaVersion'").get() as { value: string } | undefined;
|
|
378
|
+
if (!v || Number(v.value) !== SCHEMA_VERSION) { db.close(); return null; }
|
|
379
|
+
return db;
|
|
380
|
+
} catch {
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export const getState = (db: DatabaseSync, key: string): string | null =>
|
|
386
|
+
(db.prepare('SELECT value FROM state WHERE key = ?').get(key) as { value: string } | undefined)?.value ?? null;
|
|
387
|
+
|
|
388
|
+
export const setState = (db: DatabaseSync, key: string, value: string): void => {
|
|
389
|
+
db.prepare('INSERT OR REPLACE INTO state (key, value) VALUES (?, ?)').run(key, value);
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
/** Run `fn` in one transaction (the rebuild writes in large batches; one fsync per batch). */
|
|
393
|
+
export function tx<T>(db: DatabaseSync, fn: () => T): T {
|
|
394
|
+
db.exec('BEGIN');
|
|
395
|
+
try {
|
|
396
|
+
const out = fn();
|
|
397
|
+
db.exec('COMMIT');
|
|
398
|
+
return out;
|
|
399
|
+
} catch (error) {
|
|
400
|
+
db.exec('ROLLBACK');
|
|
401
|
+
throw error;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ───────────────────────── entry ids ─────────────────────────
|
|
406
|
+
|
|
407
|
+
/** Twelve hex digits of a SHA-1: enough to keep a project's entries apart, short enough to cite. */
|
|
408
|
+
export const hex12 = (...parts: readonly (string | number | null | undefined)[]): string =>
|
|
409
|
+
createHash('sha1').update(parts.map((p) => (p === null || p === undefined ? '' : String(p))).join('\x1f')).digest('hex').slice(0, 12);
|
|
410
|
+
|
|
411
|
+
export const commitKey = (hash: string): string => `commit:${hash.slice(0, 12)}`;
|
|
412
|
+
export const docKey = (path: string, commit: string): string => `doc:${path}@${commit.slice(0, 10)}`;
|
|
413
|
+
export const delKey = (path: string, commit: string): string => `del:${path}@${commit.slice(0, 10)}`;
|
|
414
|
+
export const cleanupKey = (commit: string): string => `cleanup:${commit.slice(0, 10)}`;
|
|
415
|
+
export const blobTextKey = (repo: string, blob: string): string => `blob:${repo}:${blob}`;
|
|
416
|
+
export const planKey = (path: string, commit: string | null): string => (commit ? `plan:${path}@${commit.slice(0, 10)}` : `plan:${path}`);
|
|
417
|
+
|
|
418
|
+
/** A line of the material in its normal form: what makes two versions' lines the same line. */
|
|
419
|
+
export const lineIdentity = (line: string): string => line.normalize('NFC').replace(/\s+/g, ' ').trim();
|
|
420
|
+
|
|
421
|
+
// ───────────────────────── searchable text ─────────────────────────
|
|
422
|
+
|
|
423
|
+
/** Credential values replaced by a marker; the fact that one was there stays visible (Spec §3.1). */
|
|
424
|
+
export const redact = (text: string): string => redactCredentials(text).text;
|
|
425
|
+
|
|
426
|
+
/** Store a searchable text under its key (redacted), keeping the FTS index in step. Unchanged text is not rewritten. */
|
|
427
|
+
export function putText(db: DatabaseSync, key: string, kind: string, repo: string | null, content: string): void {
|
|
428
|
+
const red = redact(content);
|
|
429
|
+
const existing = db.prepare('SELECT id, content FROM texts WHERE key = ?').get(key) as { id: number; content: string } | undefined;
|
|
430
|
+
if (existing) {
|
|
431
|
+
if (existing.content === red) return;
|
|
432
|
+
db.prepare('DELETE FROM texts_fts WHERE rowid = ?').run(existing.id);
|
|
433
|
+
db.prepare('UPDATE texts SET kind = ?, repo = ?, content = ? WHERE id = ?').run(kind, repo, red, existing.id);
|
|
434
|
+
db.prepare('INSERT INTO texts_fts (rowid, content) VALUES (?, ?)').run(existing.id, red);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const w = db.prepare('INSERT INTO texts (key, kind, repo, content) VALUES (?, ?, ?, ?)').run(key, kind, repo, red);
|
|
438
|
+
db.prepare('INSERT INTO texts_fts (rowid, content) VALUES (?, ?)').run(Number(w.lastInsertRowid), red);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export function getText(db: DatabaseSync, key: string): string | null {
|
|
442
|
+
return (db.prepare('SELECT content FROM texts WHERE key = ?').get(key) as { content: string } | undefined)?.content ?? null;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export function deleteText(db: DatabaseSync, key: string): void {
|
|
446
|
+
const row = db.prepare('SELECT id FROM texts WHERE key = ?').get(key) as { id: number } | undefined;
|
|
447
|
+
if (!row) return;
|
|
448
|
+
db.prepare('DELETE FROM texts_fts WHERE rowid = ?').run(row.id);
|
|
449
|
+
db.prepare('DELETE FROM texts WHERE id = ?').run(row.id);
|
|
450
|
+
}
|