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,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time order in the program's placement inference (CR, D104 follow-up to CQ). Two countable facts the ledger already
|
|
3
|
+
* holds, checked by the program so the model's attention is not spent overriding them:
|
|
4
|
+
*
|
|
5
|
+
* - **A citation cannot precede what it cites.** A token a record names (a number of any family, a two-letter id, `#n`,
|
|
6
|
+
* an id in front matter, a row's batch) counts only when the item it names was first written on or before the citing
|
|
7
|
+
* record's date (one day of slack for time zones). The acceptance run of CQ placed E88 on K through "E87's entry names
|
|
8
|
+
* CN": E87 (2026-09-22) writes `智谱 CN` — China in a key name — and ticket CN was first written on 2026-10-01.
|
|
9
|
+
* - **A work item or an execution decision does not go to a plan written after it.** An execution decision records the
|
|
10
|
+
* execution of a plan that existed at that time; a plan first written after it cannot be the one it executed. Product
|
|
11
|
+
* decisions are exempt: a product decision may shape a later plan.
|
|
12
|
+
*
|
|
13
|
+
* Where either date is unknown the check keeps the inference as it was. The dates:
|
|
14
|
+
*
|
|
15
|
+
* - **When an item was first written** (`first`): its first appearance as the ledger dates its defining lines (`nums`
|
|
16
|
+
* definitions of its numbers, every version, deleted documents included), and for a Plan item the first version of the
|
|
17
|
+
* document whose heading or table row defines it (the date the ledger gives that line). A date is exact when it is
|
|
18
|
+
* written for the line or its entry, or is the commit of an ordinary commit; a file's time, a first observation, the
|
|
19
|
+
* commit of an import-style root commit, or a document's stated date only say the line existed by then. When such a
|
|
20
|
+
* bound is earlier than every exact date, the first appearance is unknown.
|
|
21
|
+
* - **The date of a citing record**: a decision's own entry date (the date its heading or bold entry carries, else the
|
|
22
|
+
* date its name writes; a later note in the entry that names the token and writes its own date cites as of that date);
|
|
23
|
+
* for a table row, a work item's row, prompt or words, and a line of a current document, the latest version of the
|
|
24
|
+
* document that holds it (the line was written by then, so a later date only keeps more).
|
|
25
|
+
*/
|
|
26
|
+
import type { Ledger } from '../../ledger/index.ts';
|
|
27
|
+
import { versionText } from '../../ledger/docs.ts';
|
|
28
|
+
import { dateContext, datesOnLine, dayOf } from '../../ledger/time.ts';
|
|
29
|
+
import type { ReferenceItem } from '../../model/types.ts';
|
|
30
|
+
import type { ProjectStore } from '../../store/project-store.ts';
|
|
31
|
+
|
|
32
|
+
const slash = (p: string) => p.replace(/\\/g, '/');
|
|
33
|
+
const upper = (s: string) => s.trim().toUpperCase();
|
|
34
|
+
const esc = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
35
|
+
const NAME_SEP = ' · ';
|
|
36
|
+
|
|
37
|
+
/** One day of slack for time zones: `a` is after `b` only when it is two calendar days later or more. */
|
|
38
|
+
export function laterThan(a: string, b: string): boolean {
|
|
39
|
+
const ms = (d: string) => Date.parse(`${d}T00:00:00Z`);
|
|
40
|
+
const x = ms(a);
|
|
41
|
+
const y = ms(b);
|
|
42
|
+
if (Number.isNaN(x) || Number.isNaN(y)) return false;
|
|
43
|
+
return x - y > 86_400_000;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The latest a ledger row says its line was written by: a commit (ordinary or import), a file's time, a first
|
|
48
|
+
* observation. A date written in the text is not one (a row may write a date it refers to, `会话边界(2026-09-18)`): for
|
|
49
|
+
* content of an import commit the import is (`other_at`); outside version control, none.
|
|
50
|
+
*/
|
|
51
|
+
export function writtenBy(r: { readonly occurred_at: string; readonly occurred_basis: string; readonly other_at?: string | null }): string | null {
|
|
52
|
+
return dayOf(r.occurred_basis === 'Written in text' ? r.other_at ?? null : r.occurred_at);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The latest full date written on a line of an entry that names `n` as a whole token, or null. An entry kept "in place and
|
|
57
|
+
* whole" may get a dated note later (`部分被 D67 改动(2026-09-21)`): that line cites as of its own date, not the entry's.
|
|
58
|
+
*/
|
|
59
|
+
export function lineDateNaming(text: string, n: string): string | null {
|
|
60
|
+
const re = new RegExp(`(?<![\\p{L}\\p{N}_-])${esc(n)}(?![\\p{L}\\p{N}_])`, 'iu');
|
|
61
|
+
let out: string | null = null;
|
|
62
|
+
for (const line of text.split(/\r?\n/)) {
|
|
63
|
+
if (!re.test(line)) continue;
|
|
64
|
+
for (const d of datesOnLine(line, null)) if (!out || d.at > out) out = d.at;
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Whether `first` (when the cited item was first written) is after the citing record's day, allowing a later dated note in its text that names it. */
|
|
70
|
+
export function citedAfter(first: string | null, day: string | null, text?: string, n?: string): boolean {
|
|
71
|
+
if (!first || !day || !laterThan(first, day)) return false;
|
|
72
|
+
const noted = text && n ? lineDateNaming(text, n) : null;
|
|
73
|
+
return !(noted && !laterThan(first, noted));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The layers whose documents are the product layer: a decision log beside them is a product log. */
|
|
77
|
+
const PRODUCT_LAYERS = new Set(['Plan', 'PRD', 'Spec', 'Product', 'Task contract']);
|
|
78
|
+
/** The layers of the execution layer: a decision log beside them is an execution log. */
|
|
79
|
+
const EXECUTION_LAYERS = new Set(['Execution arrangement', 'Task index']);
|
|
80
|
+
|
|
81
|
+
export interface Chronology {
|
|
82
|
+
/** When the item (a work item or a reference item) was first written, `YYYY-MM-DD`; null when the records cannot say. */
|
|
83
|
+
first(id: string): string | null;
|
|
84
|
+
/** The date of an item's own record for "after it" checks: a decision's entry date; a work item's first appearance. */
|
|
85
|
+
own(id: string): string | null;
|
|
86
|
+
/** The date a work item's records as they stand now were written by: the latest version of the documents that define it. */
|
|
87
|
+
current(id: string): string | null;
|
|
88
|
+
/** The date a line of a document as it stands now was written by: the document's latest version (else the ledger's rows of that line). */
|
|
89
|
+
line(path: string, line: number | null): string | null;
|
|
90
|
+
/** The date of a document's latest version. */
|
|
91
|
+
file(path: string): string | null;
|
|
92
|
+
/** When a token of a table was first written in that document (a batch row `B2` of an arrangement), or null. */
|
|
93
|
+
firstIn(token: string, path: string): string | null;
|
|
94
|
+
/** Whether a decision or boundary is an entry of an execution log (its log sits with the execution layer, not the product layer). */
|
|
95
|
+
executionEntry(d: ReferenceItem): boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Without a ledger nothing is dated: every check keeps the inference as it was. */
|
|
99
|
+
export const NO_CHRONOLOGY: Chronology = {
|
|
100
|
+
first: () => null, own: () => null, current: () => null, line: () => null, file: () => null, firstIn: () => null,
|
|
101
|
+
executionEntry: () => false,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
interface NumRow {
|
|
105
|
+
readonly num: string; readonly kind: string; readonly repo: string | null; readonly position: string | null; readonly path: string | null; readonly line: number | null; readonly commit_hash: string | null;
|
|
106
|
+
readonly occurred_at: string; readonly occurred_basis: string; readonly occurred_anchor: string | null; readonly other_at: string | null; readonly current: number;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function chronology(store: ProjectStore, ledger: Ledger | null): Chronology {
|
|
110
|
+
if (!ledger) return { ...NO_CHRONOLOGY, executionEntry: (d) => executionEntryByLayers(store, null, d) };
|
|
111
|
+
const db = ledger.db;
|
|
112
|
+
const all = <T>(sql: string, ...params: (string | number | null)[]): T[] => { try { return db.prepare(sql).all(...params) as T[]; } catch { return []; } };
|
|
113
|
+
const imports = all<{ hash: string }>('SELECT hash FROM commits WHERE import_root = 1').map((r) => r.hash);
|
|
114
|
+
const atImport = (r: NumRow): boolean => r.occurred_basis === 'Commit' && imports.some((h) => (r.commit_hash ?? '').startsWith(h.slice(0, 10)) || (r.occurred_anchor ?? '').includes(h.slice(0, 10)));
|
|
115
|
+
// A line dated only by its document's stated date (`日期:` in the preamble of content that predates version control):
|
|
116
|
+
// that is when the document's version was written, so the line existed by then, not since then.
|
|
117
|
+
const docDateLine = new Map<string, number | null>();
|
|
118
|
+
const documentDated = (r: NumRow): boolean => {
|
|
119
|
+
if (r.occurred_basis !== 'Written in text' || !r.occurred_anchor) return false;
|
|
120
|
+
const m = /^(.*):(\d+)$/.exec(r.occurred_anchor);
|
|
121
|
+
if (!m) return false;
|
|
122
|
+
const [, at, n] = m;
|
|
123
|
+
const key = `${r.repo ?? ''}|${at}`;
|
|
124
|
+
if (!docDateLine.has(key)) {
|
|
125
|
+
let text: string | null = null;
|
|
126
|
+
if (r.kind === 'loose') text = all<{ content: string }>('SELECT content FROM texts WHERE key = ?', `loose:${at}`)[0]?.content ?? null;
|
|
127
|
+
else {
|
|
128
|
+
const v = all<{ repo: string; blob: string }>('SELECT repo, blob FROM docs WHERE path = ? AND import_root = 1' + (r.repo ? ' AND repo = ?' : '') + ' ORDER BY at_ms LIMIT 1', ...(r.repo ? [at!, r.repo] : [at!]))[0];
|
|
129
|
+
if (v) { try { text = versionText(db, v.repo, v.blob); } catch { text = null; } }
|
|
130
|
+
}
|
|
131
|
+
docDateLine.set(key, text === null ? null : dateContext(text).document?.line ?? null);
|
|
132
|
+
}
|
|
133
|
+
const docLine = docDateLine.get(key);
|
|
134
|
+
return docLine !== null && docLine !== undefined && docLine === Number(n) && r.line !== Number(n);
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* Exact: a date written for the line itself or its entry, or the commit of an ordinary commit. Everything else — a
|
|
138
|
+
* file's time, a first observation, an import commit, a document's stated date — only says the line existed by then.
|
|
139
|
+
*/
|
|
140
|
+
const exact = (r: NumRow): boolean => (r.occurred_basis === 'Written in text' && !documentDated(r)) || (r.occurred_basis === 'Commit' && !atImport(r));
|
|
141
|
+
/** The earliest a row says its line existed by (its date, or the import's when that is earlier). */
|
|
142
|
+
const boundOf = (r: NumRow): string | null => { const a = dayOf(r.occurred_at); const b = dayOf(r.other_at); return a && b ? (a < b ? a : b) : a ?? b; };
|
|
143
|
+
let repos: { id: string; path: string }[] = [];
|
|
144
|
+
try { repos = ledger.repos().map((r) => ({ id: r.id, path: slash(r.path).replace(/\/+$/, '') })); } catch { repos = []; }
|
|
145
|
+
/** A path as the ledger keys it: repository-relative when it is in a repository. */
|
|
146
|
+
const relative = (p: string): { repo: string | null; path: string } => {
|
|
147
|
+
const s = slash(p);
|
|
148
|
+
const root = repos.filter((x) => s.toLowerCase().startsWith(`${x.path.toLowerCase()}/`)).sort((a, b) => b.path.length - a.path.length)[0];
|
|
149
|
+
return root ? { repo: root.id, path: s.slice(root.path.length + 1) } : { repo: null, path: s };
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const rowsCache = new Map<string, NumRow[]>();
|
|
153
|
+
const definitions = (num: string): NumRow[] => {
|
|
154
|
+
const key = upper(num);
|
|
155
|
+
let rows = rowsCache.get(key);
|
|
156
|
+
if (!rows) {
|
|
157
|
+
rows = all<NumRow>("SELECT num, kind, repo, position, path, line, commit_hash, occurred_at, occurred_basis, occurred_anchor, other_at, current FROM nums WHERE num = ? AND place = 'definition' AND confidence = 'stated'", key);
|
|
158
|
+
rowsCache.set(key, rows);
|
|
159
|
+
}
|
|
160
|
+
return rows;
|
|
161
|
+
};
|
|
162
|
+
/** The first appearance the rows support: the earliest exact date, unless a bound says the line existed before it. */
|
|
163
|
+
const firstOf = (rows: readonly NumRow[], extra: readonly { day: string; exact: boolean }[] = []): string | null => {
|
|
164
|
+
let exactMin: string | null = null;
|
|
165
|
+
let boundMin: string | null = null;
|
|
166
|
+
const take = (day: string | null, isExact: boolean) => {
|
|
167
|
+
if (!day) return;
|
|
168
|
+
if (isExact) { if (!exactMin || day < exactMin) exactMin = day; } else if (!boundMin || day < boundMin) boundMin = day;
|
|
169
|
+
};
|
|
170
|
+
for (const r of rows) { if (exact(r)) take(dayOf(r.occurred_at), true); else take(boundOf(r), false); }
|
|
171
|
+
for (const e of extra) take(e.day, e.exact);
|
|
172
|
+
if (!exactMin) return null;
|
|
173
|
+
return boundMin && boundMin < exactMin ? null : exactMin;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const numbersOf = (id: string): string[] => {
|
|
177
|
+
const t = store.threads.get(id);
|
|
178
|
+
const ids = t ? t.ids : store.reference.get(id)?.ids ?? [];
|
|
179
|
+
return [...new Set(ids.map(upper))].filter((n) => n.length >= 2 && n.length <= 24);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// A Plan item: the first version of its own document whose heading or table row defines it by its token.
|
|
183
|
+
const planTokens = (r: ReferenceItem): string[] => {
|
|
184
|
+
const lead = (r.name.split(NAME_SEP)[0] ?? '').trim();
|
|
185
|
+
return [...new Set([...r.ids, ...(lead && lead.length <= 16 ? [lead] : [])].map(upper))].filter(Boolean);
|
|
186
|
+
};
|
|
187
|
+
const definesLine = (line: string, tokens: readonly string[]): boolean => {
|
|
188
|
+
const heading = /^\s{0,3}#{1,6}\s+(.*)$/.exec(line);
|
|
189
|
+
const cell = /^\s*\|\s*([^|]*)\|/.exec(line);
|
|
190
|
+
const text = heading?.[1] ?? cell?.[1];
|
|
191
|
+
if (text === undefined) return false;
|
|
192
|
+
const bare = text.replace(/[*`[\]]/g, '').trim();
|
|
193
|
+
return tokens.some((t) => new RegExp(`^${esc(t)}(?![\\p{L}\\p{N}_-])`, 'iu').test(bare));
|
|
194
|
+
};
|
|
195
|
+
const planVersions = (r: ReferenceItem): { day: string; exact: boolean }[] => {
|
|
196
|
+
const tokens = planTokens(r);
|
|
197
|
+
if (!tokens.length) return [];
|
|
198
|
+
const out: { day: string; exact: boolean }[] = [];
|
|
199
|
+
const paths = new Set<string>();
|
|
200
|
+
for (const sid of r.sourceIds) { const a = store.sources.get(sid)?.anchor; if (a?.kind === 'file') paths.add(a.path); }
|
|
201
|
+
for (const p of paths) {
|
|
202
|
+
const rel = relative(p);
|
|
203
|
+
const versions = all<{ repo: string; blob: string; at: string; import_root: number }>(`SELECT repo, blob, at, import_root FROM docs WHERE path = ?${rel.repo ? ' AND repo = ?' : ''} ORDER BY at_ms`, ...(rel.repo ? [rel.path, rel.repo] : [rel.path]));
|
|
204
|
+
for (const v of versions) {
|
|
205
|
+
let text: string | null = null;
|
|
206
|
+
try { text = versionText(db, v.repo, v.blob); } catch { text = null; }
|
|
207
|
+
if (text === null) continue;
|
|
208
|
+
const lines = text.split(/\r?\n/);
|
|
209
|
+
const i = lines.findIndex((l) => definesLine(l, tokens));
|
|
210
|
+
if (i < 0) continue;
|
|
211
|
+
if (!v.import_root) out.push({ day: dayOf(v.at)!, exact: true });
|
|
212
|
+
else {
|
|
213
|
+
// Content that existed before version control: the date the ledger gives that line (its own, its entry's, else the
|
|
214
|
+
// date of the document's version that has it first), else only "by the import".
|
|
215
|
+
const written = dateContext(text).at(i + 1);
|
|
216
|
+
out.push(written ? { day: written.at.slice(0, 10), exact: true } : { day: dayOf(v.at)!, exact: false });
|
|
217
|
+
}
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return out;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const firstCache = new Map<string, string | null>();
|
|
225
|
+
const first = (id: string): string | null => {
|
|
226
|
+
if (firstCache.has(id)) return firstCache.get(id)!;
|
|
227
|
+
const rows = numbersOf(id).flatMap(definitions);
|
|
228
|
+
const r = store.reference.get(id);
|
|
229
|
+
const extra = r?.category === 'Plan' ? planVersions(r) : [];
|
|
230
|
+
const out = firstOf(rows, extra);
|
|
231
|
+
firstCache.set(id, out);
|
|
232
|
+
return out;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const ownSourcePaths = (r: ReferenceItem): string[] => [...new Set(r.sourceIds.flatMap((sid) => { const a = store.sources.get(sid)?.anchor; return a?.kind === 'file' ? [relative(a.path).path.toLowerCase()] : []; }))];
|
|
236
|
+
const ownCache = new Map<string, string | null>();
|
|
237
|
+
const own = (id: string): string | null => {
|
|
238
|
+
if (ownCache.has(id)) return ownCache.get(id)!;
|
|
239
|
+
let out: string | null = null;
|
|
240
|
+
const r = store.reference.get(id);
|
|
241
|
+
if (r && (r.category === 'Decision' || r.category === 'Boundary')) {
|
|
242
|
+
// Its entry in its own log: the date its heading (or bold entry) carries, or the commit that added it.
|
|
243
|
+
const paths = ownSourcePaths(r);
|
|
244
|
+
const mine = numbersOf(id).flatMap(definitions).filter((x) => x.path && paths.some((p) => slash(x.path!).toLowerCase() === p || slash(x.path!).toLowerCase().endsWith(`/${p}`)));
|
|
245
|
+
const heads = mine.filter((x) => x.position === 'heading' || x.position === 'bold entry');
|
|
246
|
+
for (const x of heads) if (exact(x)) { const d = dayOf(x.occurred_at); if (d && (!out || d < out)) out = d; }
|
|
247
|
+
// Else the date its name (its heading) writes, when it is an entry with a heading or none the ledger read.
|
|
248
|
+
if (!out && (heads.length || !mine.length)) out = /(\d{4}-\d{2}-\d{2})/.exec(r.name)?.[1] ?? null;
|
|
249
|
+
// Else (a row of a table, where a date written may be one it refers to, and a long row may change past what the ledger
|
|
250
|
+
// keeps of it) the latest version of the log that holds it: the row was written by then.
|
|
251
|
+
if (!out) for (const x of mine) { const d = (x.path && x.kind !== 'loose' ? fileIn(x.repo, x.path) : null) ?? writtenBy(x); if (d && (!out || d > out)) out = d; }
|
|
252
|
+
} else out = first(id);
|
|
253
|
+
ownCache.set(id, out);
|
|
254
|
+
return out;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const fileCache = new Map<string, string | null>();
|
|
258
|
+
/** The day of a document's latest version (a repository-relative path in a known repository, or any). */
|
|
259
|
+
function fileIn(repo: string | null, rel: string): string | null {
|
|
260
|
+
const key = `${repo ?? ''}:${rel.toLowerCase()}`;
|
|
261
|
+
if (fileCache.has(key)) return fileCache.get(key)!;
|
|
262
|
+
const v = all<{ at: string }>(`SELECT at FROM docs WHERE path = ?${repo ? ' AND repo = ?' : ''} ORDER BY at_ms DESC LIMIT 1`, ...(repo ? [rel, repo] : [rel]))[0];
|
|
263
|
+
const out = v ? dayOf(v.at) : null;
|
|
264
|
+
fileCache.set(key, out);
|
|
265
|
+
return out;
|
|
266
|
+
}
|
|
267
|
+
const file = (path: string): string | null => { const rel = relative(path); return fileIn(rel.repo, rel.path); };
|
|
268
|
+
// A line as it stands now was written by the document's latest version. (The ledger's rows of a line keep its first
|
|
269
|
+
// version, and a long line may change past what they keep of it: the document's version is the safe date.)
|
|
270
|
+
const line = (path: string, n: number | null): string | null => {
|
|
271
|
+
const out = file(path);
|
|
272
|
+
if (out || n === null) return out;
|
|
273
|
+
const rel = relative(path);
|
|
274
|
+
let latest: string | null = null;
|
|
275
|
+
for (const r of all<{ occurred_at: string; occurred_basis: string; other_at: string | null }>('SELECT occurred_at, occurred_basis, other_at FROM nums WHERE path = ? AND line = ? AND current = 1', rel.path, n)) { const d = writtenBy(r); if (d && (!latest || d > latest)) latest = d; }
|
|
276
|
+
return latest;
|
|
277
|
+
};
|
|
278
|
+
const current = (id: string): string | null => {
|
|
279
|
+
let out: string | null = null;
|
|
280
|
+
for (const r of numbersOf(id).flatMap(definitions)) if (r.current) { const d = (r.path && r.kind !== 'loose' ? fileIn(r.repo, r.path) : null) ?? writtenBy(r); if (d && (!out || d > out)) out = d; }
|
|
281
|
+
return out ?? own(id);
|
|
282
|
+
};
|
|
283
|
+
const firstIn = (token: string, path: string): string | null => {
|
|
284
|
+
const rel = relative(path).path.toLowerCase();
|
|
285
|
+
return firstOf(definitions(token).filter((r) => r.path && slash(r.path).toLowerCase() === rel));
|
|
286
|
+
};
|
|
287
|
+
return { first, own, current, line, file, firstIn, executionEntry: (d) => executionEntryByLayers(store, ledger, d, relative) };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Whether a decision or boundary is an entry of an execution log: the log's directory holds (or contains) the execution
|
|
292
|
+
* layer — an execution arrangement or a task index — and no document of the product layer (plan, PRD, Spec, product,
|
|
293
|
+
* contracts) sits beside it. Without a layer map: an index or a prompt the ledger read sits beside the log.
|
|
294
|
+
*/
|
|
295
|
+
function executionEntryByLayers(store: ProjectStore, ledger: Ledger | null, d: ReferenceItem, relative?: (p: string) => { repo: string | null; path: string }): boolean {
|
|
296
|
+
if (d.category !== 'Decision' && d.category !== 'Boundary') return false;
|
|
297
|
+
const rel = relative ?? ((p: string) => ({ repo: null, path: slash(p) }));
|
|
298
|
+
const logs = [...new Set(d.sourceIds.flatMap((sid) => { const a = store.sources.get(sid)?.anchor; return a?.kind === 'file' ? [a.path] : []; }))];
|
|
299
|
+
if (!logs.length) return false;
|
|
300
|
+
const layers = store.layers.all();
|
|
301
|
+
return logs.some((abs) => {
|
|
302
|
+
const s = slash(abs).toLowerCase();
|
|
303
|
+
const dirOf = (p: string) => p.split('/').slice(0, -1).join('/');
|
|
304
|
+
if (layers.length) {
|
|
305
|
+
// The layer map's paths are relative to their repository: joined to it, they compare with the log's absolute path.
|
|
306
|
+
const inRepo = layers.map((l) => ({ ...l, full: `${slash(l.repo ?? '').replace(/\/+$/, '').toLowerCase()}/${slash(l.path).replace(/\/+$/, '').toLowerCase()}` }));
|
|
307
|
+
const dir = dirOf(s);
|
|
308
|
+
const below = (p: string) => p.startsWith(`${dir}/`);
|
|
309
|
+
const execution = inRepo.some((l) => EXECUTION_LAYERS.has(l.layer) && below(l.full));
|
|
310
|
+
const product = inRepo.some((l) => PRODUCT_LAYERS.has(l.layer) && (dirOf(l.full) === dir || l.full === dir));
|
|
311
|
+
return execution && !product;
|
|
312
|
+
}
|
|
313
|
+
if (!ledger) return false;
|
|
314
|
+
const r = rel(abs);
|
|
315
|
+
const dir = dirOf(r.path.toLowerCase());
|
|
316
|
+
let files: { path: string; kind: string }[] = [];
|
|
317
|
+
try { files = ledger.db.prepare("SELECT DISTINCT path, kind FROM plans WHERE current = 1 AND kind IN ('index', 'prompt')").all() as typeof files; } catch { files = []; }
|
|
318
|
+
return files.some((f) => dirOf(rel(f.path).path.toLowerCase()) === dir);
|
|
319
|
+
});
|
|
320
|
+
}
|