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,561 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the workbench shows of the assets Spec v2.8 adds (CKC-09 AC-32, AC-33; CKC-21 AC-11, AC-12; Spec §6.3, §6.4,
|
|
3
|
+
* §6.7, §6.9): which objects were removed, the authority layer, a decision's carry-out, who claimed what and when and
|
|
4
|
+
* what the code shows, the project's rules and the organizing plan. It is worked out here, on the server, so the
|
|
5
|
+
* browser only draws it and a new skin does not have to work any of it out again. The fixed words are English and
|
|
6
|
+
* the same for every project (Spec §6.13). Pure reads; nothing here changes an asset.
|
|
7
|
+
*/
|
|
8
|
+
import type { Attribution, EntryMark, KeeperJob, PendingMaterial, Project, ProjectRule, ScopeClassification, ScopeItem, Source, Statement, WorktreeSummary } from '../model/types.ts';
|
|
9
|
+
import { ORGANIZING_LEVEL, OWNER_WORDS, type Basis, type CarryOut, type Identity, type MarkKind, type OrganizingLevel, type Validity } from '../model/vocab.ts';
|
|
10
|
+
import type { ProjectStore } from '../store/project-store.ts';
|
|
11
|
+
import { anchorLabel } from '../sources/anchor.ts';
|
|
12
|
+
import { displayMaterialTime, reportedBy } from '../model/time.ts';
|
|
13
|
+
import { rulesByGroup } from '../keeper/rules.ts';
|
|
14
|
+
import { worktreeSentence } from '../scope/worktree.ts';
|
|
15
|
+
import { ignoredPlaceOf, ruleText, type IgnoredPlace } from '../scope/ignored-place.ts';
|
|
16
|
+
import { listMaterials, materialKeyOf, ruleSettlement, settledAwayMaterials } from '../keeper/organize/materials.ts';
|
|
17
|
+
import { clerkLevels, clerkPending } from '../keeper/organize/clerk-coverage.ts';
|
|
18
|
+
|
|
19
|
+
/** The one group the owner's words are drawn in on top of the graph, and listed in above every area (§6.3, D63). */
|
|
20
|
+
export const OWNER_WORDS_GROUP = OWNER_WORDS;
|
|
21
|
+
|
|
22
|
+
/** The organizing plan's id: one plan per project (§3.7). */
|
|
23
|
+
const PLAN_ID = 'organizing-plan';
|
|
24
|
+
|
|
25
|
+
export interface SourceRow { readonly id: string; readonly title: string; readonly label: string }
|
|
26
|
+
|
|
27
|
+
export function sourceRow(store: ProjectStore, id: string): SourceRow {
|
|
28
|
+
const s = store.sources.get(id);
|
|
29
|
+
return s ? { id, title: s.title, label: anchorLabel(s.anchor) } : { id, title: id, label: id };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** An object's name as the workbench shows it: the graph's label, else the item's own name. */
|
|
33
|
+
export function objectLabel(store: ProjectStore, id: string): string {
|
|
34
|
+
const source = store.sources.get(id);
|
|
35
|
+
return store.nodes.get(id)?.label ?? store.reference.get(id)?.name ?? store.threads.get(id)?.title ?? store.rules.get(id)?.summary ?? (source ? anchorLabel(source.anchor) : id);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ───────────────────────── Removed (§2.1, D61) ─────────────────────────
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Whether the object behind an id was removed from the project's current version. The item's own validity decides;
|
|
42
|
+
* the graph's copy of it is only read for an object that is not a reference item or a work item.
|
|
43
|
+
*/
|
|
44
|
+
export function isRemoved(store: ProjectStore, id: string): boolean {
|
|
45
|
+
const validity: Validity | undefined = store.reference.get(id)?.validity ?? store.threads.get(id)?.validity ?? store.nodes.get(id)?.validity;
|
|
46
|
+
return validity === 'Removed';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ───────────────────────── the authority layer (§1.9, §6.4) ─────────────────────────
|
|
50
|
+
|
|
51
|
+
export interface AuthorityView {
|
|
52
|
+
/** The three layers of §1.9: the owner decided it; a role set it within its responsibilities; a role set it in the owner's place. Null for what is none of them (a proposal, a report, an interpretation). */
|
|
53
|
+
readonly layer: 'Owner' | 'Role' | 'Decided without owner' | null;
|
|
54
|
+
readonly label: string;
|
|
55
|
+
/** For `Decided without owner`: whether it is in force now, and that it waits for the owner. */
|
|
56
|
+
readonly detail: string | null;
|
|
57
|
+
readonly identity: Identity | null;
|
|
58
|
+
readonly who: string | null;
|
|
59
|
+
/** When it was set, as the material gives it (a date stays a date, §1.8). */
|
|
60
|
+
readonly at: string | null;
|
|
61
|
+
readonly inForce: boolean;
|
|
62
|
+
readonly markId: string | null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function authorName(a: Attribution): string | null {
|
|
66
|
+
if (a.author.kind === 'owner') return 'the owner';
|
|
67
|
+
return a.author.name?.trim() || a.holder?.role || null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function openMark(store: ProjectStore, targetId: string, kind: MarkKind): EntryMark | undefined {
|
|
71
|
+
return store.marks.find((m) => m.targetId === targetId && m.kind === kind && !m.closed);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The authority layer of a product reference item (§1.3: every item carries it; §1.9, §6.4). */
|
|
75
|
+
export function authorityOf(store: ProjectStore, referenceId: string): AuthorityView | null {
|
|
76
|
+
const item = store.reference.get(referenceId);
|
|
77
|
+
if (!item) return null;
|
|
78
|
+
// An item written without its attribution (older assets) has no layer to show, unless a mark says who set it.
|
|
79
|
+
const identity = item.attribution?.identity ?? null;
|
|
80
|
+
const author = item.attribution ? authorName(item.attribution) : null;
|
|
81
|
+
const inForce = item.validity === 'Current';
|
|
82
|
+
const mark = openMark(store, referenceId, 'Decided without owner');
|
|
83
|
+
if (mark) {
|
|
84
|
+
const who = mark.decidedBy?.who ?? author;
|
|
85
|
+
const at = mark.decidedBy?.at ?? null;
|
|
86
|
+
return {
|
|
87
|
+
layer: 'Decided without owner', identity, who, at, inForce, markId: mark.id,
|
|
88
|
+
label: `Decided without owner: set by ${who ?? 'a role'}${at ? `, ${displayMaterialTime(at)}` : ''}`,
|
|
89
|
+
detail: inForce ? 'In force now. Not decided by the owner; pending the owner’s decision.' : `Not in force: ${item.validity}.`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (identity === 'Decision') {
|
|
93
|
+
return { layer: 'Owner', label: item.category === OWNER_WORDS ? "The owner's own words" : 'Decided by the owner', detail: null, identity, who: 'the owner', at: null, inForce, markId: null };
|
|
94
|
+
}
|
|
95
|
+
if (identity === 'Artifact') {
|
|
96
|
+
return { layer: 'Role', label: `Set by ${author ?? 'a role'} within its responsibilities`, detail: null, identity, who: author, at: null, inForce, markId: null };
|
|
97
|
+
}
|
|
98
|
+
if (!identity) return null;
|
|
99
|
+
return { layer: null, label: author ? `${identity} · ${author}` : identity, detail: null, identity, who: author, at: null, inForce, markId: null };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ───────────────────────── a decision's carry-out (§2.2, §6.4) ─────────────────────────
|
|
103
|
+
|
|
104
|
+
export interface CarryOutView {
|
|
105
|
+
readonly status: CarryOut | null; // null when work is linked by `carries out` but no carry-out was recorded
|
|
106
|
+
readonly remaining: string | null;
|
|
107
|
+
readonly work: readonly { readonly id: string; readonly label: string; readonly progress: string | null; readonly validity: string | null; readonly removed: boolean }[];
|
|
108
|
+
readonly evidence: readonly SourceRow[];
|
|
109
|
+
readonly at: string | null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Whether a decision that asks for something was carried out, and the work that carries it out (each opens by id). */
|
|
113
|
+
export function carryOutOf(store: ProjectStore, referenceId: string): CarryOutView | null {
|
|
114
|
+
const item = store.reference.get(referenceId);
|
|
115
|
+
if (!item) return null;
|
|
116
|
+
const recorded = item.carryOut ?? null;
|
|
117
|
+
const linked = store.relations.filter((r) => r.type === 'carries out' && r.to === referenceId).map((r) => r.from);
|
|
118
|
+
const workIds = [...new Set([...(recorded?.workIds ?? []), ...linked])];
|
|
119
|
+
if (!recorded && workIds.length === 0) return null;
|
|
120
|
+
return {
|
|
121
|
+
status: recorded?.status ?? null,
|
|
122
|
+
remaining: recorded?.remaining ?? null,
|
|
123
|
+
work: workIds.map((id) => {
|
|
124
|
+
const t = store.threads.get(id);
|
|
125
|
+
return { id, label: objectLabel(store, id), progress: t?.progress ?? null, validity: t?.validity ?? null, removed: isRemoved(store, id) };
|
|
126
|
+
}),
|
|
127
|
+
evidence: (recorded?.evidenceSourceIds ?? []).map((id) => sourceRow(store, id)),
|
|
128
|
+
at: recorded?.at ?? null,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ───────────────────────── claims and what the code shows (§2.4, §6.4) ─────────────────────────
|
|
133
|
+
|
|
134
|
+
export interface StatementView extends Statement {
|
|
135
|
+
/** `Reported by <who>, <date>` for a claim that says whose it is (§2.4); null for anything else. */
|
|
136
|
+
readonly reportedBy: string | null;
|
|
137
|
+
/** The project's `Untrusted` rule that covers the claim's source (§1.15). */
|
|
138
|
+
readonly untrusted: { readonly ruleId: string; readonly summary: string } | null;
|
|
139
|
+
/** For an observation: the code it was observed in, with file, lines and commit. */
|
|
140
|
+
readonly code: readonly { readonly id: string; readonly label: string }[];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* A check that found a material and something else disagreeing — the code, above all (§1.6 `contradicts`, §2.4). It
|
|
145
|
+
* is recorded between the material and the code, not on one statement, so it is shown with the material the claims
|
|
146
|
+
* rest on (`on`), in the check's own words: a receipt usually holds several claims, and the check says which one.
|
|
147
|
+
*/
|
|
148
|
+
export interface CheckView {
|
|
149
|
+
readonly relationId: string;
|
|
150
|
+
readonly claim: string;
|
|
151
|
+
readonly on: SourceRow;
|
|
152
|
+
readonly otherId: string;
|
|
153
|
+
readonly otherLabel: string;
|
|
154
|
+
readonly otherIsSource: boolean;
|
|
155
|
+
/** Whether the other end is code: this is the check against the code (§2.4). */
|
|
156
|
+
readonly code: boolean;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isCodeSource(store: ProjectStore, id: string): boolean {
|
|
160
|
+
const s = store.sources.get(id);
|
|
161
|
+
return s !== undefined && (s.usedAs === 'Code' || s.anchor.kind === 'commit');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function codeLocation(store: ProjectStore, id: string): string {
|
|
165
|
+
const s = store.sources.get(id)!;
|
|
166
|
+
return `${anchorLabel(s.anchor)}${s.version.commit && s.anchor.kind !== 'commit' ? ` @ ${s.version.commit.slice(0, 10)}` : ''}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* One statement of a fact record or a work item as the details show it: a claim says who made it and when, and that
|
|
171
|
+
* its source is one the project marks untrusted; an observation made in the code carries where. A claim nothing was
|
|
172
|
+
* checked against stays as it was reported (§2.4).
|
|
173
|
+
*/
|
|
174
|
+
export function statementView(store: ProjectStore, s: Statement): StatementView {
|
|
175
|
+
const ruleId = s.claimedBy?.untrustedRuleId ?? null;
|
|
176
|
+
return {
|
|
177
|
+
...s,
|
|
178
|
+
reportedBy: s.type === 'Claimed' && s.claimedBy ? reportedBy(s.claimedBy) : null,
|
|
179
|
+
untrusted: ruleId ? { ruleId, summary: store.rules.get(ruleId)?.summary ?? ruleId } : null,
|
|
180
|
+
code: s.type === 'Observed' ? s.sourceIds.filter((id) => isCodeSource(store, id)).map((id) => ({ id, label: codeLocation(store, id) })) : [],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The checks recorded on the materials claims rest on (`contradicts`, §1.6, §2.4), each shown once. When both ends are
|
|
186
|
+
* among them, the claim's side is the one that is not code.
|
|
187
|
+
*/
|
|
188
|
+
export function checksOn(store: ProjectStore, materialIds: readonly string[]): CheckView[] {
|
|
189
|
+
const materials = new Set(materialIds);
|
|
190
|
+
return store.relations
|
|
191
|
+
.filter((r) => r.type === 'contradicts' && (materials.has(r.from) || materials.has(r.to)))
|
|
192
|
+
.map((r) => {
|
|
193
|
+
const fromIn = materials.has(r.from);
|
|
194
|
+
const toIn = materials.has(r.to);
|
|
195
|
+
const onId = fromIn && !toIn ? r.from : toIn && !fromIn ? r.to : isCodeSource(store, r.from) ? r.to : r.from;
|
|
196
|
+
const otherId = onId === r.from ? r.to : r.from;
|
|
197
|
+
const other = store.sources.get(otherId);
|
|
198
|
+
return { relationId: r.id, claim: r.claim, on: sourceRow(store, onId), otherId, otherLabel: other ? codeLocation(store, otherId) : objectLabel(store, otherId), otherIsSource: other !== undefined, code: isCodeSource(store, otherId) };
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** The materials the claims among these statements rest on. */
|
|
203
|
+
export function claimMaterials(statements: readonly Statement[]): string[] {
|
|
204
|
+
return [...new Set(statements.filter((s) => s.type === 'Claimed').flatMap((s) => s.sourceIds))];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ───────────────────────── How this project works (§1.15, §6.7; CKC-21 AC-11) ─────────────────────────
|
|
208
|
+
|
|
209
|
+
export const CONFIRMED = 'Confirmed by the owner';
|
|
210
|
+
export const WAITING = 'Waiting for the owner to confirm';
|
|
211
|
+
export const WRITTEN = 'Written in the project';
|
|
212
|
+
|
|
213
|
+
/** A mark on a rule, as the rule's row shows it; for `Decided without owner`, who set it, when, and whether it is in force. */
|
|
214
|
+
export interface RuleMarkView { readonly id: string; readonly kind: MarkKind; readonly clue: string; readonly who: string | null; readonly at: string | null; readonly inForce: boolean; readonly label: string; readonly detail: string }
|
|
215
|
+
export interface RuleView {
|
|
216
|
+
readonly id: string;
|
|
217
|
+
readonly group: ProjectRule['group'];
|
|
218
|
+
readonly category: ProjectRule['category'];
|
|
219
|
+
readonly summary: string;
|
|
220
|
+
readonly excerpt: string | null;
|
|
221
|
+
readonly sources: readonly SourceRow[];
|
|
222
|
+
readonly appliesTo: readonly string[];
|
|
223
|
+
readonly basis: ProjectRule['basis'];
|
|
224
|
+
readonly validity: Validity;
|
|
225
|
+
/** Whether the owner confirmed it, it waits for them (inferred), or the project writes it down. */
|
|
226
|
+
readonly confirmation: { readonly state: typeof CONFIRMED | typeof WAITING | typeof WRITTEN; readonly quote: string | null; readonly at: string | null; readonly source: SourceRow | null };
|
|
227
|
+
readonly ownerSystem: string | null;
|
|
228
|
+
readonly differsInPractice: readonly { readonly text: string; readonly sources: readonly SourceRow[] }[];
|
|
229
|
+
readonly replacedBy: { readonly id: string; readonly summary: string } | null;
|
|
230
|
+
readonly marks: readonly RuleMarkView[];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function ruleMark(m: EntryMark, validity: Validity): RuleMarkView {
|
|
234
|
+
const inForce = validity === 'Current';
|
|
235
|
+
const who = m.decidedBy?.who ?? null;
|
|
236
|
+
const at = m.decidedBy?.at ?? null;
|
|
237
|
+
if (m.kind !== 'Decided without owner') return { id: m.id, kind: m.kind, clue: m.clue, who, at, inForce, label: m.kind, detail: m.clue };
|
|
238
|
+
return {
|
|
239
|
+
id: m.id, kind: m.kind, clue: m.clue, who, at, inForce,
|
|
240
|
+
label: `Decided without owner: set by ${who ?? 'a role'}${at ? `, ${displayMaterialTime(at)}` : ''}`,
|
|
241
|
+
detail: inForce ? 'In force now. Not decided by the owner; pending the owner’s decision.' : `Not in force: ${validity}.`,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function ruleView(store: ProjectStore, r: ProjectRule): RuleView {
|
|
246
|
+
return {
|
|
247
|
+
id: r.id, group: r.group, category: r.category, summary: r.summary, excerpt: r.excerpt,
|
|
248
|
+
sources: r.sourceIds.map((id) => sourceRow(store, id)),
|
|
249
|
+
appliesTo: r.appliesTo, basis: r.basis, validity: r.validity,
|
|
250
|
+
confirmation: r.ownerConfirmation
|
|
251
|
+
? { state: CONFIRMED, quote: r.ownerConfirmation.quote, at: r.ownerConfirmation.at, source: sourceRow(store, r.ownerConfirmation.sourceId) }
|
|
252
|
+
: { state: r.basis === 'Inferred' ? WAITING : WRITTEN, quote: null, at: null, source: null },
|
|
253
|
+
ownerSystem: r.ownerSystem,
|
|
254
|
+
differsInPractice: r.differsInPractice.map((d) => ({ text: d.text, sources: d.sourceIds.map((id) => sourceRow(store, id)) })),
|
|
255
|
+
replacedBy: r.replacedBy ? { id: r.replacedBy, summary: store.rules.get(r.replacedBy)?.summary ?? r.replacedBy } : null,
|
|
256
|
+
marks: store.marks.filter((m) => m.targetId === r.id && !m.closed).map((m) => ruleMark(m, r.validity)),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export interface HowThisProjectWorksView {
|
|
261
|
+
/** The ways of working the owner summarized that the rules come from, listed first (§1.15, §6.7). */
|
|
262
|
+
readonly ownerSystems: readonly string[];
|
|
263
|
+
readonly groups: readonly { readonly group: ProjectRule['group']; readonly rules: readonly RuleView[]; readonly replaced: readonly RuleView[] }[];
|
|
264
|
+
readonly counts: { readonly rules: number; readonly waiting: number; readonly replaced: number };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Every rule of the project in the three fixed groups (§1.15, §6.7; CKC-21 AC-11): what the owner summarized first in
|
|
269
|
+
* its group, each with its sources, what it applies to, its basis and whether the owner confirmed it; replaced rules
|
|
270
|
+
* apart, each pointing to the one that replaced it. A group the project has no rule in is left out.
|
|
271
|
+
*/
|
|
272
|
+
export function howThisProjectWorks(store: ProjectStore): HowThisProjectWorksView {
|
|
273
|
+
const groups = rulesByGroup(store.rules.all()).map((g) => ({ group: g.group, rules: g.rules.map((r) => ruleView(store, r)), replaced: g.replaced.map((r) => ruleView(store, r)) }));
|
|
274
|
+
const shown = groups.flatMap((g) => g.rules);
|
|
275
|
+
return {
|
|
276
|
+
ownerSystems: [...new Set(shown.map((r) => r.ownerSystem).filter((s): s is string => Boolean(s)))],
|
|
277
|
+
groups,
|
|
278
|
+
counts: { rules: shown.length, waiting: shown.filter((r) => r.confirmation.state === WAITING).length, replaced: groups.reduce((n, g) => n + g.replaced.length, 0) },
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ───────────────────────── the organizing plan and focus (§3.7, §6.7) ─────────────────────────
|
|
283
|
+
|
|
284
|
+
export interface OrganizingPlanView {
|
|
285
|
+
readonly byRule: readonly { readonly what: string; readonly targets: readonly string[]; readonly treatment: string; readonly rule: { readonly id: string; readonly summary: string; readonly category: string | null; readonly validity: Validity | null } }[];
|
|
286
|
+
readonly readClosely: readonly { readonly what: string; readonly targets: readonly string[]; readonly why: string }[];
|
|
287
|
+
readonly focus: readonly { readonly what: string; readonly why: string; readonly sources: readonly SourceRow[] }[];
|
|
288
|
+
readonly order: readonly string[];
|
|
289
|
+
readonly corrections: readonly { readonly at: string; readonly quote: string; readonly changed: string; readonly source: SourceRow; readonly previous: { readonly byRule: readonly { readonly what: string; readonly targets: readonly string[]; readonly treatment: string }[]; readonly readClosely: readonly { readonly what: string }[]; readonly focus: readonly { readonly what: string }[]; readonly order: readonly string[] } }[];
|
|
290
|
+
readonly jobId: string | null;
|
|
291
|
+
readonly asOf: string;
|
|
292
|
+
readonly updatedAt: string;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** The framing round's plan for the organizing that follows, with the owner's corrections and what each replaced (§3.7). */
|
|
296
|
+
export function organizingPlanView(store: ProjectStore): OrganizingPlanView | null {
|
|
297
|
+
const plan = store.plans.get(PLAN_ID) ?? store.plans.all()[0];
|
|
298
|
+
if (!plan) return null;
|
|
299
|
+
const rule = (id: string) => { const r = store.rules.get(id); return { id, summary: r?.summary ?? id, category: r?.category ?? null, validity: r?.validity ?? null }; };
|
|
300
|
+
return {
|
|
301
|
+
byRule: plan.byRule.map((e) => ({ what: e.what, targets: e.targets, treatment: e.treatment, rule: rule(e.ruleId) })),
|
|
302
|
+
readClosely: plan.readClosely.map((e) => ({ what: e.what, targets: e.targets, why: e.why })),
|
|
303
|
+
focus: plan.focus.map((f) => ({ what: f.what, why: f.why, sources: f.sourceIds.map((id) => sourceRow(store, id)) })),
|
|
304
|
+
order: plan.order,
|
|
305
|
+
corrections: plan.corrections.map((c) => ({ at: c.at, quote: c.quote, changed: c.changed, source: sourceRow(store, c.sourceId), previous: c.previous })),
|
|
306
|
+
jobId: plan.jobId, asOf: plan.asOf, updatedAt: plan.updatedAt,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* What the assets say about a rule or about the plan, for the conversation opened on it (§6.8): the owner confirms or
|
|
312
|
+
* corrects it there, and the Keeper starts from what is recorded.
|
|
313
|
+
*/
|
|
314
|
+
export function ruleOrPlanSummary(store: ProjectStore, kind: string, id: string): { text: string; sourceIds: string[] } | null {
|
|
315
|
+
if (kind === 'rule') {
|
|
316
|
+
const r = store.rules.get(id);
|
|
317
|
+
if (!r) return null;
|
|
318
|
+
const v = ruleView(store, r);
|
|
319
|
+
const lines = [
|
|
320
|
+
v.summary,
|
|
321
|
+
[v.group, v.category, v.basis, v.confirmation.state, v.validity === 'Current' ? null : v.validity].filter(Boolean).join(' · '),
|
|
322
|
+
v.excerpt ? `The project’s words: “${v.excerpt}”` : null,
|
|
323
|
+
v.appliesTo.length ? `Applies to: ${v.appliesTo.join(', ')}` : null,
|
|
324
|
+
v.ownerSystem ? `From the way of working the owner summarized: ${v.ownerSystem}` : null,
|
|
325
|
+
...v.differsInPractice.map((d) => `Differs in practice: ${d.text}`),
|
|
326
|
+
v.confirmation.quote ? `The owner confirmed it: “${v.confirmation.quote}”` : null,
|
|
327
|
+
v.replacedBy ? `Replaced by ${v.replacedBy.id}: ${v.replacedBy.summary}` : null,
|
|
328
|
+
...v.marks.map((m) => `${m.label}. ${m.detail}`),
|
|
329
|
+
];
|
|
330
|
+
return { text: lines.filter((l): l is string => Boolean(l)).join('\n'), sourceIds: [...r.sourceIds] };
|
|
331
|
+
}
|
|
332
|
+
if (kind === 'plan') {
|
|
333
|
+
const p = organizingPlanView(store);
|
|
334
|
+
if (!p) return null;
|
|
335
|
+
const lines = [
|
|
336
|
+
...p.byRule.map((e) => `Settled by a rule: ${e.what} (${e.targets.join(', ')}) → ${e.treatment}, by “${e.rule.summary}”`),
|
|
337
|
+
...p.readClosely.map((e) => `Read closely: ${e.what}${e.targets.length ? ` (${e.targets.join(', ')})` : ''}${e.why ? ` — ${e.why}` : ''}`),
|
|
338
|
+
...p.focus.map((f) => `Focus: ${f.what}${f.why ? ` — ${f.why}` : ''}`),
|
|
339
|
+
p.order.length ? `Order: ${p.order.join(' → ')}` : null,
|
|
340
|
+
...p.corrections.map((c) => `The owner corrected it (${displayMaterialTime(c.at)}): “${c.quote}” — ${c.changed}`),
|
|
341
|
+
];
|
|
342
|
+
return { text: lines.filter((l): l is string => Boolean(l)).join('\n'), sourceIds: [...new Set(p.focus.flatMap((f) => f.sources.map((s) => s.id)))] };
|
|
343
|
+
}
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ───────────────────────── the scope list (§1.1, §6.7; CKC-04 AC-13–AC-17) ─────────────────────────
|
|
348
|
+
|
|
349
|
+
const plural = (n: number, word: string) => `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
350
|
+
|
|
351
|
+
export interface WorktreeView {
|
|
352
|
+
readonly merged: boolean | null;
|
|
353
|
+
/** `Merged into main · 15 files: 14 the same as main, skipped · took 1: src/a.ts (uncommitted change)` — the facts AC-15 asks the listing to give, in one line. */
|
|
354
|
+
readonly sentence: string;
|
|
355
|
+
readonly taken: readonly { readonly path: string; readonly kind: string; readonly deleted: boolean }[];
|
|
356
|
+
readonly takenMore: number;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* A worktree in a place the project's ignore rules leave out (§1.1, D105; CKC-04 AC-17): its working files are not
|
|
361
|
+
* pending — how many there are, and how many of them are uncommitted changes, as counts — and its branch and commits are
|
|
362
|
+
* still read from version control.
|
|
363
|
+
*/
|
|
364
|
+
function ignoredPlaceSentence(w: WorktreeSummary, place: IgnoredPlace): string {
|
|
365
|
+
const uncommitted = w.taken.filter((t) => t.kind === 'Uncommitted change').length;
|
|
366
|
+
return `In a directory the project’s ignore rules leave out (${ruleText(place.rule)}): its ${plural(w.files, 'working file')} (${plural(uncommitted, 'uncommitted change')} among them) are not counted as pending; its branch and commits are read from version control`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function worktreeViewOf(w: WorktreeSummary, place: IgnoredPlace | null = null): WorktreeView {
|
|
370
|
+
if (place && !w.error) { const plain = worktreeViewOf(w); return { ...plain, sentence: `${plain.sentence} · ${ignoredPlaceSentence(w, place)}` }; }
|
|
371
|
+
if (w.error) return { merged: w.merged, sentence: `Could not be measured against the trunk: ${w.error}; nothing taken from it`, taken: [], takenMore: 0 };
|
|
372
|
+
const trunk = w.trunk?.ref ?? 'the trunk';
|
|
373
|
+
const state = w.merged === null ? `Merge with ${trunk} unknown (git could not tell)`
|
|
374
|
+
: w.merged ? `Merged into ${trunk}`
|
|
375
|
+
: `Not merged into ${trunk}: ${plural(w.uniqueCommits, 'commit')} ${trunk} does not have (work in progress, not ${trunk}’s current state)`;
|
|
376
|
+
const skipped = `${plural(w.files, 'file')}: ${w.sameAsTrunk} the same as ${trunk}, skipped${w.olderVersions ? `; ${plural(w.olderVersions, 'older version')} ${trunk} has moved past, left to the version history` : ''}`;
|
|
377
|
+
const taken = w.taken.slice(0, 12).map((t) => ({ path: t.path, kind: t.kind as string, deleted: t.deleted === true }));
|
|
378
|
+
const takenText = w.taken.length === 0 ? 'nothing taken'
|
|
379
|
+
: `took ${w.taken.length}: ${taken.map((t) => `${t.path} (${t.kind.toLowerCase()}${t.deleted ? ', removed' : ''})`).join(', ')}${w.taken.length > 12 ? `, and ${w.taken.length - 12} more` : ''}`;
|
|
380
|
+
return { merged: w.merged, sentence: `${state} · ${skipped} · ${takenText}`, taken, takenMore: Math.max(0, w.taken.length - taken.length) };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export interface IgnoredView {
|
|
384
|
+
/** Which ignore file, line and pattern leaves this out: `.gitignore line 4: drafts/`. */
|
|
385
|
+
readonly rule: string;
|
|
386
|
+
/** The directory's contents summed up, with any documents named: those may carry intent, so the owner decides (AC-17). */
|
|
387
|
+
readonly sentence: string;
|
|
388
|
+
readonly files: number;
|
|
389
|
+
readonly documents: number;
|
|
390
|
+
readonly documentNames: readonly string[];
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function ignoredViewOf(ig: NonNullable<ScopeItem['ignoredBy']>): IgnoredView {
|
|
394
|
+
const rule = ig.file === 'git' && ig.line === 0 ? 'git’s ignore rules' : `${ig.file} line ${ig.line}: ${ig.pattern}`;
|
|
395
|
+
const docs = ig.documents
|
|
396
|
+
? `, ${ig.documents} of them ${ig.documents === 1 ? 'a document' : 'documents'} (${ig.documentNames.join(', ')}${ig.documents > ig.documentNames.length ? ', …' : ''}) — the owner decides whether to include ${ig.documents === 1 ? 'it' : 'them'}`
|
|
397
|
+
: '';
|
|
398
|
+
// What the ignore rules leave out is not pending (§1.1, D105): said with the count, in the same line.
|
|
399
|
+
return { rule, files: ig.files, documents: ig.documents, documentNames: ig.documentNames, sentence: `${plural(ig.files, 'file')} left out${docs} · not counted as pending` };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export interface ClassificationView {
|
|
403
|
+
readonly by: 'program' | 'keeper' | 'owner';
|
|
404
|
+
readonly basis: Basis;
|
|
405
|
+
readonly kind: string | null;
|
|
406
|
+
/** Who classified the location, marked when it is an inference: a program candidate waits for the Keeper's judgement. */
|
|
407
|
+
readonly sentence: string;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function classificationViewOf(c: ScopeClassification): ClassificationView {
|
|
411
|
+
const who = c.by === 'owner' ? 'Classified by the owner' : c.by === 'keeper' ? 'Classified by the Keeper' : 'Offered by the program — not yet judged by the Keeper';
|
|
412
|
+
return { by: c.by, basis: c.basis, kind: c.kind, sentence: `${who} (${c.basis})${c.kind ? `: ${c.kind}` : ''}${c.evidence.length ? ` — ${c.evidence.join('; ')}` : ''}` };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export interface ScopeItemView {
|
|
416
|
+
readonly item: ScopeItem;
|
|
417
|
+
/** The item's reason as shown: for a measured worktree, without the measured sentence the discovery appended to it, which `worktree.sentence` says in its own line (V15). */
|
|
418
|
+
readonly reason: string;
|
|
419
|
+
readonly worktree: WorktreeView | null;
|
|
420
|
+
readonly ignored: IgnoredView | null;
|
|
421
|
+
readonly classification: ClassificationView | null;
|
|
422
|
+
/** The project's material rules that cover this location, with their words; each opens its rule in How this project works. */
|
|
423
|
+
readonly covers: readonly { readonly ruleId: string; readonly summary: string; readonly category: string | null; readonly excerpt: string | null }[];
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export interface ScopeListView {
|
|
427
|
+
readonly inScope: readonly ScopeItemView[];
|
|
428
|
+
/** What the project uses, not its own intent: listed apart (AC-13). */
|
|
429
|
+
readonly thirdParty: readonly ScopeItemView[];
|
|
430
|
+
/** Build output, caches and exports: listed apart (AC-13). */
|
|
431
|
+
readonly generated: readonly ScopeItemView[];
|
|
432
|
+
/** Directories the ignore rules leave out that hold documents: intent may be in them, so the owner decides (AC-17). */
|
|
433
|
+
readonly ignoredDocuments: readonly ScopeItemView[];
|
|
434
|
+
readonly excluded: readonly ScopeItemView[];
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* A worktree's reason without the measured sentence the discovery puts at its end (`… · merged into main · 34 files: …`,
|
|
439
|
+
* src/scope/discover.ts): the listing says those facts in the worktree's own line, so the reason keeps what comes before.
|
|
440
|
+
* A reason that does not end that way (an older store, one the owner wrote) is left whole.
|
|
441
|
+
*/
|
|
442
|
+
export function reasonShown(item: ScopeItem): string {
|
|
443
|
+
if (!item.worktree) return item.reason;
|
|
444
|
+
const tail = ` · ${worktreeSentence(item.worktree)}`;
|
|
445
|
+
return item.reason.endsWith(tail) ? item.reason.slice(0, -tail.length) : item.reason;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** The scope list sectioned for Project scope: third-party material and generated output apart, ignored directories holding documents apart (§6.7; CKC-04 AC-13, AC-17). */
|
|
449
|
+
export function scopeListView(project: Project): ScopeListView {
|
|
450
|
+
const view = (item: ScopeItem): ScopeItemView => ({
|
|
451
|
+
item,
|
|
452
|
+
reason: reasonShown(item),
|
|
453
|
+
worktree: item.worktree ? worktreeViewOf(item.worktree, ignoredPlaceOf(project.scope, item)) : null,
|
|
454
|
+
ignored: item.ignoredBy ? ignoredViewOf(item.ignoredBy) : null,
|
|
455
|
+
classification: item.classification ? classificationViewOf(item.classification) : null,
|
|
456
|
+
covers: (item.coveredBy ?? []).map((c) => ({ ruleId: c.ruleId, summary: c.summary, category: c.category, excerpt: c.excerpt })),
|
|
457
|
+
});
|
|
458
|
+
const all = project.scope.map(view);
|
|
459
|
+
const holdsDocuments = (x: ScopeItemView) => x.item.relation === 'Excluded' && (x.item.ignoredBy?.documents ?? 0) > 0;
|
|
460
|
+
return {
|
|
461
|
+
inScope: all.filter((x) => x.item.relation !== 'Excluded' && x.item.relation !== 'Third-party material' && x.item.relation !== 'Generated'),
|
|
462
|
+
thirdParty: all.filter((x) => x.item.relation === 'Third-party material'),
|
|
463
|
+
generated: all.filter((x) => x.item.relation === 'Generated'),
|
|
464
|
+
ignoredDocuments: all.filter(holdsDocuments),
|
|
465
|
+
excluded: all.filter((x) => x.item.relation === 'Excluded' && !holdsDocuments(x)),
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ───────────────────────── the toolchain (§3.1, §6.7; CKC-03 AC-23) ─────────────────────────
|
|
470
|
+
|
|
471
|
+
export interface ToolchainView {
|
|
472
|
+
readonly entries: readonly { readonly path: string; readonly reason: string; readonly configPath: string; readonly used: boolean; readonly notUsedReason: string | null }[];
|
|
473
|
+
/** What the Keeper uses these locations for, and how the owner corrects a wrong one. */
|
|
474
|
+
readonly note: string;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** The toolchain locations the project's own configuration points at (§6.7); a location too broad to open is listed with why, never used. */
|
|
478
|
+
export function toolchainView(project: Project): ToolchainView | null {
|
|
479
|
+
const entries = (project.toolchain ?? []).map((t) => ({ path: t.path, reason: t.reason, configPath: t.configPath, used: t.used !== false, notUsedReason: t.notUsedReason ?? null }));
|
|
480
|
+
if (entries.length === 0) return null;
|
|
481
|
+
return {
|
|
482
|
+
entries,
|
|
483
|
+
note: 'Locations the project’s own configuration points at (an SDK, a compiler). The Keeper may read them to make sense of the build environment; they are not project material. A wrong entry is corrected through Ask Keeper.',
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ───────────────────────── what the project's rules settle directly (§1.11, §3.7) ─────────────────────────
|
|
488
|
+
|
|
489
|
+
export interface SettledByRuleView {
|
|
490
|
+
readonly materials: number;
|
|
491
|
+
readonly byRule: readonly {
|
|
492
|
+
readonly ruleId: string;
|
|
493
|
+
readonly summary: string;
|
|
494
|
+
/** `plan`: the organizing plan names the material under the rule; `sources`: the rule set how every one of its sources is used; `away`: the rule took it out of what is organized altogether. */
|
|
495
|
+
readonly via: 'plan' | 'sources' | 'away';
|
|
496
|
+
readonly count: number;
|
|
497
|
+
readonly names: readonly string[];
|
|
498
|
+
}[];
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* The materials the project's own rules settle directly — judged, never read closely (Spec §3.7; CKC-13 AC-30) —
|
|
503
|
+
* grouped by the rule that settles them, so the coverage's `Settled by rule` level says which rule or plan decided it.
|
|
504
|
+
* Derived from the assets the way the planner derives it; null when no rule settles anything.
|
|
505
|
+
*/
|
|
506
|
+
export function settledByRuleView(store: ProjectStore, project: Project): SettledByRuleView | null {
|
|
507
|
+
const materials = listMaterials(store, project);
|
|
508
|
+
const plan = store.plans.get(PLAN_ID) ?? null;
|
|
509
|
+
const groups = new Map<string, { via: 'plan' | 'sources' | 'away'; count: number; names: string[] }>();
|
|
510
|
+
const add = (ruleId: string, via: 'plan' | 'sources' | 'away', name: string) => {
|
|
511
|
+
const g = groups.get(ruleId) ?? { via, count: 0, names: [] };
|
|
512
|
+
g.count += 1;
|
|
513
|
+
if (g.names.length < 4 && !g.names.includes(name)) g.names.push(name);
|
|
514
|
+
groups.set(ruleId, g);
|
|
515
|
+
};
|
|
516
|
+
for (const m of materials) { const s = ruleSettlement(store, plan, m); if (s) add(s.ruleId, s.via, m.rel); }
|
|
517
|
+
for (const m of settledAwayMaterials(store, project, new Set(materials.map((x) => x.key)))) add(m.ruleId, 'away', m.rel);
|
|
518
|
+
if (groups.size === 0) return null;
|
|
519
|
+
const byRule = [...groups]
|
|
520
|
+
.map(([ruleId, g]) => ({ ruleId, summary: store.rules.get(ruleId)?.summary ?? ruleId, via: g.via, count: g.count, names: g.names }))
|
|
521
|
+
.sort((a, b) => b.count - a.count || a.ruleId.localeCompare(b.ruleId));
|
|
522
|
+
return { materials: byRule.reduce((n, r) => n + r.count, 0), byRule };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// ───────────────────────── each material's organizing level (§1.11, §6.7) ─────────────────────────
|
|
526
|
+
|
|
527
|
+
export interface OrganizingLevels {
|
|
528
|
+
/** The fixed level names, for the sources list's filter: from the vocabulary, not written into the interface. */
|
|
529
|
+
readonly levels: readonly OrganizingLevel[];
|
|
530
|
+
/** The level of one source, or null when it is not an organized material (a command run, a status record, a moved-away or deleted file). */
|
|
531
|
+
readonly ofSource: (source: Source) => OrganizingLevel | null;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* The organizing level of every material, computed the way the coverage table counts it (`clerkLevels`, fed the same
|
|
536
|
+
* pending list), so the sources list's level filter and the coverage table always agree: settled by a rule, not
|
|
537
|
+
* organized while a change waits, read in full by a step, conclusions only for a drafted session, sampled through a
|
|
538
|
+
* read series, indexed otherwise. What a rule took out of the material altogether is settled by that rule.
|
|
539
|
+
*/
|
|
540
|
+
export function organizingLevels(store: ProjectStore, project: Project, watcher: readonly PendingMaterial[] = []): OrganizingLevels {
|
|
541
|
+
const materials = listMaterials(store, project);
|
|
542
|
+
const { keys: pendingKeys } = clerkPending(store, project, watcher, materials);
|
|
543
|
+
const { levelOf } = clerkLevels(store, project, pendingKeys, materials);
|
|
544
|
+
const awayPaths = new Set(settledAwayMaterials(store, project, new Set(materials.map((m) => m.key))).map((m) => m.key.slice('file:'.length)));
|
|
545
|
+
const ofSource = (source: Source): OrganizingLevel | null => {
|
|
546
|
+
if (source.anchor.kind === 'file' && awayPaths.has(source.anchor.path)) return 'Settled by rule';
|
|
547
|
+
const key = materialKeyOf(source);
|
|
548
|
+
return key ? levelOf.get(key) ?? null : null;
|
|
549
|
+
};
|
|
550
|
+
return { levels: ORGANIZING_LEVEL, ofSource };
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// ───────────────────────── Keeper activity (§6.9) ─────────────────────────
|
|
554
|
+
|
|
555
|
+
/** What a job's saved results open in Project scope: the rules it recorded and the plan and focus (§6.9, §3.7). */
|
|
556
|
+
export function jobOpens(job: KeeperJob): { rules: number; plan: boolean } {
|
|
557
|
+
return {
|
|
558
|
+
rules: new Set(job.savedResults.filter((r) => r.collection === 'rules').map((r) => r.id)).size,
|
|
559
|
+
plan: job.savedResults.some((r) => r.collection === 'plans'),
|
|
560
|
+
};
|
|
561
|
+
}
|