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,450 @@
|
|
|
1
|
+
// §6.9, §3.3, §3.10 a round in the Keeper view: the round is one item; below it the ledger, the session drafts, the
|
|
2
|
+
// main agent — the stage it is in and each stage's time — with the lanes it sent (what each answers, its slots, status,
|
|
3
|
+
// what it read, its brief and report) and the coverage check with each account, then the spot check and the process
|
|
4
|
+
// (D99). A round from before D99 reads as its steps: orientation, skeleton, the deep sweeps and their reading
|
|
5
|
+
// assignments, cross-check, synthesis and spot check — each with its scope, status, time split, tokens and openable
|
|
6
|
+
// documents (CKC-23). Pure rendering: data comes in as parameters.
|
|
7
|
+
import { h, fmtTime } from '../app.js';
|
|
8
|
+
import { fillMarkdown } from '../markdown.js';
|
|
9
|
+
|
|
10
|
+
const ensureStyle = () => {
|
|
11
|
+
if (document.getElementById('k-views-css')) return;
|
|
12
|
+
const link = document.createElement('link');
|
|
13
|
+
link.id = 'k-views-css';
|
|
14
|
+
link.rel = 'stylesheet';
|
|
15
|
+
link.href = '/k/k-views.css';
|
|
16
|
+
document.head.append(link);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const num = (n) => Number(n).toLocaleString('en-US');
|
|
20
|
+
const ms = (v) => {
|
|
21
|
+
if (v == null) return '—';
|
|
22
|
+
if (v < 60000) return `${Math.round(v / 1000)} s`;
|
|
23
|
+
const m = v / 60000;
|
|
24
|
+
return `${m >= 20 ? Math.round(m) : m.toFixed(1)} min`;
|
|
25
|
+
};
|
|
26
|
+
const tok = (n) => (n >= 1e6 ? `${(n / 1e6).toFixed(2)}M` : n >= 1e3 ? `${(n / 1e3).toFixed(1)}k` : String(n));
|
|
27
|
+
const hm = (iso) => (iso ? iso.slice(11, 16) : '—');
|
|
28
|
+
|
|
29
|
+
export const STEP_LABEL = {
|
|
30
|
+
ledger: 'Ledger (program)', 'session-drafts': 'Session drafts', orientation: 'Orientation',
|
|
31
|
+
skeleton: 'Skeleton', dig: 'Deep sweep', 'cross-check': 'Cross-check', process: 'Process and breakpoints', synthesis: 'Synthesis', 'spot-check': 'Spot check',
|
|
32
|
+
// D99: one main agent through the stages, and the lanes it sends.
|
|
33
|
+
main: 'Main agent', lane: 'Lanes',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// ── D99: the main agent's stages, its lanes and the coverage check (Spec §3.3, §3.10, §6.9) ──
|
|
37
|
+
|
|
38
|
+
/** The stages of the main agent's session, by the words the Keeper view uses (ClerkStage). */
|
|
39
|
+
export const STAGE_LABEL = {
|
|
40
|
+
orientation: 'Orientation', skeleton: 'Skeleton', reconcile: 'Reconcile the lanes', dig: 'Dig by question',
|
|
41
|
+
coverage: 'Coverage check', 'cross-check': 'Cross-check', synthesis: 'Synthesis',
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* The main agent's stages in each kind of round, in order (clerk-steps.ts ROUND_STAGES). D103: the synthesis is a step of
|
|
45
|
+
* its own after the main agent's handover; a round recorded before still lists it as the last stage it ran.
|
|
46
|
+
*/
|
|
47
|
+
const ROUND_STAGES = {
|
|
48
|
+
'First usable': ['orientation', 'skeleton', 'reconcile'],
|
|
49
|
+
Deepen: ['orientation', 'dig', 'coverage', 'cross-check'],
|
|
50
|
+
'Follow up': ['orientation', 'skeleton', 'reconcile', 'dig', 'coverage', 'cross-check'],
|
|
51
|
+
};
|
|
52
|
+
/** What a lane answers (LaneKind). */
|
|
53
|
+
export const LANE_KIND_LABEL = { slot: 'slots', plan: 'plan or stage', topic: 'topic', 'follow-up': 'follow-up' };
|
|
54
|
+
/** A round the main agent runs carries `main`; a round from before D99 reads as its steps. */
|
|
55
|
+
export const isMainAgentRound = (round) => Boolean(round && round.main);
|
|
56
|
+
|
|
57
|
+
const plural = (n, one, many = `${one}s`) => `${num(n)} ${n === 1 ? one : many}`;
|
|
58
|
+
/** What a lane read, each material once (RoundLaneView.read), in one line. */
|
|
59
|
+
export const readText = (r) => {
|
|
60
|
+
const parts = [r.files ? plural(r.files, 'file') : null, r.versions ? plural(r.versions, 'version') : null, r.commits ? plural(r.commits, 'commit') : null, r.sessions ? plural(r.sessions, 'session') : null].filter(Boolean);
|
|
61
|
+
return parts.length ? parts.join(' · ') : 'nothing recorded';
|
|
62
|
+
};
|
|
63
|
+
/** The most lanes that ran at the same time, from their start and end. */
|
|
64
|
+
export const lanesAtOnce = (lanes) => {
|
|
65
|
+
const edges = lanes.filter((l) => l.startedAt).flatMap((l) => [[Date.parse(l.startedAt), 1], [l.endedAt ? Date.parse(l.endedAt) : Infinity, -1]]).sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
|
66
|
+
let n = 0, most = 0;
|
|
67
|
+
for (const [, d] of edges) { n += d; most = Math.max(most, n); }
|
|
68
|
+
return most;
|
|
69
|
+
};
|
|
70
|
+
/** The coverage check in one line: untouched, accounted for, settled (RoundCoverageView). */
|
|
71
|
+
export const coverageText = (c) => {
|
|
72
|
+
if (!c) return 'not run yet';
|
|
73
|
+
const materials = (c.untouched ?? []).reduce((n, g) => n + g.count, 0);
|
|
74
|
+
const notNeeded = c.accounted.filter((a) => a.outcome === 'not needed').length;
|
|
75
|
+
const part = c.accounted.filter((a) => a.outcome === 'part').length;
|
|
76
|
+
return [
|
|
77
|
+
c.untouched === null ? 'its listing was not recorded' : c.untouched.length ? `${plural(materials, 'material')} in ${plural(c.untouched.length, 'group')} untouched by any lane` : 'every planned material touched by a lane',
|
|
78
|
+
c.accounted.length ? `${plural(c.accounted.length, 'account')} (${num(notNeeded)} not needed, ${num(part)} in part)` : 'no account written',
|
|
79
|
+
c.settled ? 'settled' : 'not settled',
|
|
80
|
+
].join(' · ');
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const SEGMENTS = [
|
|
84
|
+
['generationMs', 'kv-seg-gen', 'model generation'],
|
|
85
|
+
['toolMs', 'kv-seg-tool', 'tool execution'],
|
|
86
|
+
['queueMs', 'kv-seg-queue', 'queue'],
|
|
87
|
+
['parseRetryMs', 'kv-seg-parse', 'parse & retry'],
|
|
88
|
+
['otherMs', 'kv-seg-other', 'other'],
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
function timingRow(t) {
|
|
92
|
+
if (!t) return null;
|
|
93
|
+
return h('div', {
|
|
94
|
+
class: 'kv-timing',
|
|
95
|
+
dataset: { wall: t.wallMs, gen: t.generationMs, tool: t.toolMs, queue: t.queueMs, parse: t.parseRetryMs, other: t.otherMs },
|
|
96
|
+
},
|
|
97
|
+
h('span', { class: 'kv-bar' }, SEGMENTS.map(([k, cls, label]) =>
|
|
98
|
+
h('span', { class: cls, title: label, style: `width:${t.wallMs ? (t[k] / t.wallMs) * 100 : 0}%` }))),
|
|
99
|
+
SEGMENTS.map(([k, cls, label]) => h('span', { class: 'kv-legend' }, h('i', { class: cls }), `${label} ${ms(t[k])}`)),
|
|
100
|
+
h('span', { class: 'kv-strong' }, `wall ${ms(t.wallMs)}`));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function usageRow(u) {
|
|
104
|
+
if (!u) return null;
|
|
105
|
+
return h('div', { class: 'kv-usage' },
|
|
106
|
+
`tokens in ${tok(u.input)} · out ${tok(u.output)} · cache read ${tok(u.cacheRead)}`,
|
|
107
|
+
u.cost != null ? ` · $${u.cost.toFixed(3)}` : '');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function docButton(roundId, d, ctx) {
|
|
111
|
+
let panel = null;
|
|
112
|
+
const btn = h('button', {
|
|
113
|
+
class: 'btn small kv-btn', type: 'button', dataset: { doc: d.id, kind: d.kind },
|
|
114
|
+
onClick: async () => {
|
|
115
|
+
if (panel) { panel.remove(); panel = null; return; }
|
|
116
|
+
panel = h('div', { class: 'kv-doc-panel' }, h('div', { class: 'kv-doc-loading' }, 'Loading…'));
|
|
117
|
+
// Under the step it belongs to, or — a sweep's own document, such as a brief the program composed — under the sweep.
|
|
118
|
+
(btn.closest('.kv-step') ?? btn.closest('.kv-dig-path'))?.append(panel);
|
|
119
|
+
try {
|
|
120
|
+
const doc = await ctx.fetchDoc(roundId, d.id);
|
|
121
|
+
panel.replaceChildren();
|
|
122
|
+
if (!doc) { panel.append(h('div', { class: 'kv-none' }, 'Document not found.')); return; }
|
|
123
|
+
fillMarkdown(panel, doc.markdown);
|
|
124
|
+
} catch (e) {
|
|
125
|
+
panel.replaceChildren(h('div', { class: 'kv-none' }, `Could not load: ${e.message}`));
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
}, `${d.kind} · ${d.title}`);
|
|
129
|
+
// When the document was written (RoundDocRef.at), beside its button.
|
|
130
|
+
return d.at ? h('span', { class: 'kv-docitem' }, btn, h('span', { class: 'kv-faint kv-doc-at', title: 'When the document was written' }, fmtTime(d.at))) : btn;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The session drafts a session-drafts step wrote (RoundStepView.drafts; CKC-23 AC-18): one line per session — host,
|
|
135
|
+
* session, when, how many owner lines and how they were read — each opening the draft (ctx.openDraft).
|
|
136
|
+
*/
|
|
137
|
+
function draftsRow(drafts, ctx) {
|
|
138
|
+
if (!drafts) return null;
|
|
139
|
+
if (!drafts.length) return h('div', { class: 'kv-drafts kv-none' }, 'No session drafted in this step.');
|
|
140
|
+
const count = (d) => [`${d.ownerLines} owner line${d.ownerLines === 1 ? '' : 's'}`, d.decisions ? `${d.decisions} decision${d.decisions === 1 ? '' : 's'}` : null,
|
|
141
|
+
d.confirmations ? `${d.confirmations} confirmation${d.confirmations === 1 ? '' : 's'}` : null, d.unjudged ? `${d.unjudged} not yet classified` : null].filter(Boolean).join(' · ');
|
|
142
|
+
return h('details', { class: 'kv-drafts' },
|
|
143
|
+
h('summary', { class: 'kv-text-btn' }, `Session drafts written · ${drafts.length}`),
|
|
144
|
+
h('ul', { class: 'kv-draft-list' }, drafts.map((d) => h('li', { dataset: { draft: d.id ?? '' } },
|
|
145
|
+
ctx.openDraft && d.id
|
|
146
|
+
? h('button', { class: 'btn small kv-btn kv-draft', type: 'button', title: 'Open the draft: the owner’s words verbatim with their kind, and what the agents said, as claims', onClick: () => ctx.openDraft(d.id) }, `${d.host} ${d.sessionId.slice(0, 8)}`)
|
|
147
|
+
: h('span', { class: 'kv-mono' }, `${d.host} ${d.sessionId.slice(0, 8)}`),
|
|
148
|
+
h('span', { class: 'kv-faint' }, ` ${d.startedAt ? `${d.startedAt.slice(0, 10)} ${hm(d.startedAt)}` : ''} · ${count(d)}`)))));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The sweep the program added for this deep-sweep step, when it is one (RoundView.sweepsAdded; CKC-23 AC-4). */
|
|
152
|
+
const addedSweep = (s, round) => (s.kind === 'dig' && s.path ? (round?.sweepsAdded ?? []).find((x) => x.path === s.path) ?? null : null);
|
|
153
|
+
|
|
154
|
+
/** A reading assignment's number and whether it is a follow-up, from its job's label ("… · assignment 3 (follow-up)"). */
|
|
155
|
+
const assignmentOf = (s) => {
|
|
156
|
+
const m = /· assignment (\d+)( \(follow-up\))?$/.exec(s.label || '');
|
|
157
|
+
return m ? { n: Number(m[1]), followUp: Boolean(m[2]) } : null;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* What of a round did not finish (Spec §3.10): the coverage's failure list (ctx.failures) holds every job of a round's
|
|
162
|
+
* step that did not finish, with why and where it stands — run again by the program, waiting for the owner, or listed
|
|
163
|
+
* with the round going on without it. A round's are the ones whose job is one of its steps.
|
|
164
|
+
*/
|
|
165
|
+
function failuresOf(round, ctx) {
|
|
166
|
+
const steps = new Map(round.steps.map((s) => [s.jobId, s]));
|
|
167
|
+
return (ctx.failures ?? []).filter((f) => steps.has(f.ref)).map((f) => ({ ...f, step: steps.get(f.ref) }));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The round's note of what did not finish, each with its reason and a Retry — Continue for a stopped one (ctx.runAgain). */
|
|
171
|
+
function failuresNote(round, failures, ctx) {
|
|
172
|
+
if (!failures.length) return null;
|
|
173
|
+
const head = round.status === 'Running'
|
|
174
|
+
? `${failures.length === 1 ? 'A job has' : `${failures.length} jobs have`} not finished in this round:`
|
|
175
|
+
: `${failures.length === 1 ? 'A job' : `${failures.length} jobs`} did not finish; the round closed without ${failures.length === 1 ? 'it' : 'them'}:`;
|
|
176
|
+
return h('div', { class: 'kv-round-note kv-failures', dataset: { failures: failures.length } },
|
|
177
|
+
h('div', {}, head),
|
|
178
|
+
h('ul', {}, failures.map((f) => {
|
|
179
|
+
const action = f.step.status === 'Stopped' ? 'continue' : 'retry';
|
|
180
|
+
return h('li', { dataset: { job: f.ref, kind: f.step.kind } },
|
|
181
|
+
h('span', { class: 'kv-fail-reason' }, f.reason),
|
|
182
|
+
ctx.runAgain ? h('button', { class: 'btn small kv-btn', type: 'button', title: action === 'retry' ? 'Run this job again: it goes on from what it saved, told why it ended' : 'Continue this job from what it saved', onClick: () => ctx.runAgain(f.ref, action) }, action === 'retry' ? 'Retry' : 'Continue') : null);
|
|
183
|
+
})));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function stepView(roundId, s, ctx, round, inPath = false) {
|
|
187
|
+
const failed = /fail/i.test(s.status || '') || (ctx.failures ?? []).some((f) => f.ref === s.jobId);
|
|
188
|
+
const added = inPath ? null : addedSweep(s, round);
|
|
189
|
+
const asg = inPath ? assignmentOf(s) : null;
|
|
190
|
+
// An added sweep's brief is the one the program composed: a document of the round no job wrote, so it rides here.
|
|
191
|
+
const docs = added?.doc && !s.docs.some((d) => d.id === added.doc.id) ? [added.doc, ...s.docs] : s.docs;
|
|
192
|
+
return h('li', { class: `kv-step${failed ? ' kv-failed' : ''}${added ? ' kv-added' : ''}${asg ? ' kv-assignment' : ''}`, dataset: { kind: s.kind, job: s.jobId, path: s.path ?? '', ...(asg ? { assignment: asg.n } : {}) } },
|
|
193
|
+
h('div', { class: 'kv-step-head' },
|
|
194
|
+
h('span', { class: 'kv-step-label' }, asg ? `Reading assignment ${asg.n}` : STEP_LABEL[s.kind] ?? s.label),
|
|
195
|
+
// The main agent says which stage it has reached (Spec §6.9).
|
|
196
|
+
// D103: once it handed the round over to the synthesis, it says so; its Handover opens among its documents.
|
|
197
|
+
s.kind === 'main' && round?.main ? h('span', { class: 'kv-tag kv-blue kv-main-stage', dataset: { stage: round.main.stage ?? '', handover: round.main.handover ? 'yes' : 'no' } }, round.main.handover ? `handed over to the synthesis ${hm(round.main.handover.at)}` : round.main.stage ? STAGE_LABEL[round.main.stage] ?? round.main.stage : 'not started') : null,
|
|
198
|
+
asg?.followUp ? h('span', { class: 'kv-tag kv-blue', title: 'Takes up what an earlier assignment of this sweep left unread' }, 'follow-up') : null,
|
|
199
|
+
s.path && !inPath ? h('span', { class: 'kv-step-path' }, `— ${s.path}`) : null,
|
|
200
|
+
added ? h('span', { class: 'kv-tag kv-amber kv-added-tag', title: added.why }, 'added by the program') : null,
|
|
201
|
+
s.model ? h('span', { class: 'kv-model' }, `${s.model.id} · thinking ${s.model.thinking ?? 'default'}`) : null,
|
|
202
|
+
h('span', { class: `kv-status${failed ? ' kv-bad' : ''}` }, s.status),
|
|
203
|
+
s.startedAt ? h('span', { class: 'kv-faint' }, `${hm(s.startedAt)}–${hm(s.endedAt)}`) : null),
|
|
204
|
+
timingRow(s.timing),
|
|
205
|
+
usageRow(s.usage),
|
|
206
|
+
s.kind === 'ledger' && round?.ledger ? h('div', { class: 'kv-usage' }, `${num(round.ledger.commitsAdded)} commits added`) : null,
|
|
207
|
+
docs.length ? h('div', { class: 'kv-docs' }, docs.map((d) => docButton(roundId, d, ctx))) : null,
|
|
208
|
+
s.kind === 'session-drafts' ? draftsRow(s.drafts, ctx) : null,
|
|
209
|
+
s.children?.length ? h('ol', { class: 'kv-steps' }, s.children.map((c) => stepView(roundId, c, ctx, round))) : null);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function roundSum(round) {
|
|
213
|
+
const cell = (title, lit, ...kids) => h('div', { class: `kv-sum-cell${lit ? ' kv-lit' : ''}`, dataset: { sum: title } },
|
|
214
|
+
h('div', { class: 'kv-sum-title' }, title), ...kids);
|
|
215
|
+
return h('div', { class: 'kv-round-sum' },
|
|
216
|
+
cell('Outputs by position', false,
|
|
217
|
+
h('ul', {}, round.outputs.map((o) => h('li', {}, `${o.position} × ${num(o.count)}`)))),
|
|
218
|
+
cell('Groundwork for later steps & recall', false,
|
|
219
|
+
h('ul', {}, round.groundwork.map((g) => h('li', {}, `${g.kind} × ${num(g.count)}`)))),
|
|
220
|
+
cell(`Unplaced × ${round.unplaced.count}`, round.unplaced.count > 0,
|
|
221
|
+
round.unplaced.count === 0
|
|
222
|
+
? h('div', { class: 'kv-muted' }, 'everything landed somewhere')
|
|
223
|
+
: h('ul', {}, round.unplaced.reasons.map((r) => h('li', {}, r)))),
|
|
224
|
+
cell('Spot check', round.spotCheck && round.spotCheck.wrong > 0,
|
|
225
|
+
round.spotCheck
|
|
226
|
+
? h('div', {},
|
|
227
|
+
// CM: a wrong by timing (right when written, overtaken by a later write or recompute) apart from a wrong of substance.
|
|
228
|
+
h('div', {}, `${round.spotCheck.sampled} ${round.spotCheck.synthesis ? 'checked' : 'sampled'} · ${round.spotCheck.wrong} wrong${round.spotCheck.wrongByKind && round.spotCheck.wrong ? ` (${round.spotCheck.wrongByKind.substance} of substance, ${round.spotCheck.wrongByKind.timing} by timing)` : ''} · ${round.spotCheck.corrected} corrected`),
|
|
229
|
+
// D103: what the synthesis wrote and every current note are checked in full, and counted apart from the sample.
|
|
230
|
+
round.spotCheck.synthesis ? h('div', { class: 'kv-muted kv-spot-full', dataset: { outputs: round.spotCheck.synthesis.outputs, checked: round.spotCheck.synthesis.checked, wrong: round.spotCheck.synthesis.wrong } },
|
|
231
|
+
`the synthesis and the notes, in full: ${round.spotCheck.synthesis.checked} of ${round.spotCheck.synthesis.outputs} checked · ${round.spotCheck.synthesis.wrong} wrong; the rest, sampled: ${round.spotCheck.sampled - round.spotCheck.synthesis.checked} checked · ${round.spotCheck.wrong - round.spotCheck.synthesis.wrong} wrong`) : null,
|
|
232
|
+
// CS (D104): the placement readings on a line of their own — the reasons that left an item unplaced, whichever
|
|
233
|
+
// round wrote them, and the program's placements nobody had reviewed.
|
|
234
|
+
round.spotCheck.placement ? h('div', { class: 'kv-muted kv-spot-placement', dataset: { reasons: round.spotCheck.placement.reasons, checked: round.spotCheck.placement.reasonsChecked, wrong: round.spotCheck.placement.reasonsWrong, sampled: round.spotCheck.placement.programPlacementsSampled, placedWrong: round.spotCheck.placement.programPlacementsWrong } },
|
|
235
|
+
`placement: ${round.spotCheck.placement.reasonsChecked} of ${round.spotCheck.placement.reasons} reason${round.spotCheck.placement.reasons === 1 ? '' : 's'} for leaving an item unplaced checked · ${round.spotCheck.placement.reasonsWrong} wrong; ${round.spotCheck.placement.programPlacementsSampled} program placement${round.spotCheck.placement.programPlacementsSampled === 1 ? '' : 's'} sampled · ${round.spotCheck.placement.programPlacementsWrong} wrong`) : null,
|
|
236
|
+
round.standingNotes?.standing ? h('div', { class: 'kv-muted kv-standing-notes', dataset: { standing: round.standingNotes.standing, open: round.standingNotes.open } },
|
|
237
|
+
`notes from earlier rounds: ${round.standingNotes.standing} — ${round.standingNotes.confirmed} confirmed, ${round.standingNotes.updated} updated, ${round.standingNotes.withdrawn} withdrawn${round.standingNotes.open ? `, ${round.standingNotes.open} with no result` : ''}`) : null,
|
|
238
|
+
Object.keys(round.spotCheck.byKind).length
|
|
239
|
+
? h('div', { class: 'kv-muted' }, `wrong by kind: ${Object.entries(round.spotCheck.byKind).map(([k, n]) => `${k} ×${n}`).join(', ')}`)
|
|
240
|
+
: null,
|
|
241
|
+
// D99: the missing steps the lanes looked for, and those the spot check confirmed and lit (Spec §2.12).
|
|
242
|
+
round.missing ? h('div', { class: 'kv-muted kv-missing-steps', dataset: { looked: round.missing.looked, checked: round.missing.checked } }, `missing steps: ${round.missing.looked} looked for by the lanes · ${round.missing.checked} checked and lit`) : null)
|
|
243
|
+
: h('div', { class: 'kv-muted' }, 'not run')),
|
|
244
|
+
cell('Ledger (program)', false,
|
|
245
|
+
round.ledger
|
|
246
|
+
? h('div', {}, `${ms(round.ledger.ms)} · ${num(round.ledger.commitsAdded)} commits added`)
|
|
247
|
+
: h('div', { class: 'kv-muted' }, '—')));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* What a sweep read of its plan (PathReading, from the coverage: ctx.deepening), in one line — planned, read whole, in
|
|
252
|
+
* part, not read, still open, and its assignments — with each material recorded as read in part or not read, and why.
|
|
253
|
+
*/
|
|
254
|
+
function readingLine(r) {
|
|
255
|
+
if (!r) return null;
|
|
256
|
+
const a = r.assignments;
|
|
257
|
+
const going = [a.running ? `${a.running} running` : null, a.queued ? `${a.queued} queued` : null, a.failed ? `${a.failed} failed or stopped` : null].filter(Boolean).join(', ');
|
|
258
|
+
return h('div', { class: 'kv-reading', dataset: { planned: r.planned, whole: r.whole, part: r.part, notRead: r.notRead, open: r.open } },
|
|
259
|
+
h('span', {}, `${num(r.planned)} planned · ${num(r.whole)} read whole · ${num(r.part)} in part · ${num(r.notRead)} not read${r.open ? ` · ${num(r.open)} still open` : ''}`),
|
|
260
|
+
h('span', { class: 'kv-faint' }, ` — ${a.total} assignment${a.total === 1 ? '' : 's'}${a.followUps ? ` (${a.followUps} follow-up${a.followUps === 1 ? '' : 's'})` : ''}: ${a.done} done${going ? `, ${going}` : ''}`),
|
|
261
|
+
r.accounted.length ? h('details', { class: 'kv-accounted' },
|
|
262
|
+
h('summary', { class: 'kv-text-btn' }, `Read in part or not read, with why · ${r.accounted.length}`),
|
|
263
|
+
h('ul', {}, r.accounted.map((x) => h('li', { dataset: { key: x.key, outcome: x.outcome } },
|
|
264
|
+
h('span', { class: `kv-tag ${x.outcome === 'none' ? 'kv-red' : 'kv-amber'}` }, x.outcome === 'none' ? 'not read' : 'in part'), ' ',
|
|
265
|
+
h('span', { class: 'kv-mono' }, x.label), h('span', { class: 'kv-faint' }, ` — ${x.why}${x.by === 'program' ? ' (the program)' : ''}`))))) : null);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** A deepening read by reading assignments: its sweeps, each with its reading line and its assignments under it. */
|
|
269
|
+
function sweepsByPath(round, digs, ctx) {
|
|
270
|
+
const dp = ctx.deepening;
|
|
271
|
+
const rows = dp && dp.actual && dp.actual.roundId === round.id ? dp.progress.filter((p) => p.reading) : [];
|
|
272
|
+
const byPath = new Map();
|
|
273
|
+
for (const d of digs) { const k = d.path ?? d.label; byPath.set(k, [...(byPath.get(k) ?? []), d]); }
|
|
274
|
+
if (!rows.length && [...byPath.values()].every((list) => list.length === 1)) return null;
|
|
275
|
+
const readingOf = new Map(rows.map((p) => [p.path, p.reading]));
|
|
276
|
+
return h('div', { class: 'kv-digs', dataset: { assignments: digs.length } },
|
|
277
|
+
h('div', { class: 'kv-digs-head' }, `Deep sweeps — ${byPath.size} path${byPath.size === 1 ? '' : 's'} in parallel, read in ${digs.length} reading assignment${digs.length === 1 ? '' : 's'} on the lanes`),
|
|
278
|
+
...[...byPath].map(([path, list]) => {
|
|
279
|
+
const added = addedSweep({ kind: 'dig', path }, round);
|
|
280
|
+
const docs = added?.doc ? [added.doc] : [];
|
|
281
|
+
return h('div', { class: 'kv-dig-path', dataset: { path } },
|
|
282
|
+
h('div', { class: 'kv-dig-path-head' }, h('span', { class: 'kv-step-path' }, path),
|
|
283
|
+
added ? h('span', { class: 'kv-tag kv-amber kv-added-tag', title: added.why }, 'added by the program') : null),
|
|
284
|
+
readingLine(readingOf.get(path)),
|
|
285
|
+
docs.length ? h('div', { class: 'kv-docs' }, docs.map((d) => docButton(round.id, d, ctx))) : null,
|
|
286
|
+
h('ol', { class: 'kv-steps' }, list.map((d) => stepView(round.id, d, ctx, round, true))));
|
|
287
|
+
}));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* The main agent's stages (RoundView.main): the stages of this kind of round in order, each with when it ran and how
|
|
292
|
+
* long; the one it is in is tagged, the ones not reached yet are listed faint. A stage it skipped stays faint too.
|
|
293
|
+
*/
|
|
294
|
+
function stagesBlock(round) {
|
|
295
|
+
const m = round.main;
|
|
296
|
+
const ran = new Map();
|
|
297
|
+
for (const e of m.stages) ran.set(e.stage, e);
|
|
298
|
+
const order = [...new Set([...(ROUND_STAGES[round.kind] ?? []), ...m.stages.map((e) => e.stage)])];
|
|
299
|
+
const split = (t) => (t ? SEGMENTS.map(([k, , label]) => `${label} ${ms(t[k])}`).join(' · ') : '');
|
|
300
|
+
return h('table', { class: 'kv-mini kv-stages', dataset: { stage: m.stage ?? '' } },
|
|
301
|
+
h('thead', {}, h('tr', {}, h('th', {}, 'Stage'), h('th', {}, 'From'), h('th', {}, 'To'), h('th', {}, 'Time'))),
|
|
302
|
+
h('tbody', {}, order.map((stage) => {
|
|
303
|
+
const e = ran.get(stage);
|
|
304
|
+
// A stage not run while a later one did was skipped (a Follow up whose document chain had not changed, §3.3).
|
|
305
|
+
const skipped = !e && order.slice(order.indexOf(stage) + 1).some((x) => ran.has(x));
|
|
306
|
+
const time = e ? e.timing?.wallMs ?? (e.endedAt ? Date.parse(e.endedAt) - Date.parse(e.startedAt) : null) : null;
|
|
307
|
+
return h('tr', { class: e ? '' : 'kv-faint', dataset: { stage, ran: e ? 'yes' : skipped ? 'skipped' : 'no' } },
|
|
308
|
+
h('td', {}, STAGE_LABEL[stage] ?? stage, e?.current ? h('span', { class: 'kv-tag kv-blue', style: 'margin-left:6px' }, 'now') : null),
|
|
309
|
+
h('td', {}, e ? hm(e.startedAt) : '—'), h('td', {}, e ? hm(e.endedAt) : '—'),
|
|
310
|
+
h('td', { title: split(e?.timing) }, e ? ms(time) : skipped ? 'skipped' : 'not reached'));
|
|
311
|
+
})));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** One lane (RoundLaneView): what it answers, its slots, the stage it was sent in, status, what it read, brief and report. */
|
|
315
|
+
function laneView(round, l, ctx) {
|
|
316
|
+
const failed = /fail|stop/i.test(l.status || '') || (ctx.failures ?? []).some((f) => f.ref === l.jobId);
|
|
317
|
+
const docs = [l.brief, l.report].filter(Boolean);
|
|
318
|
+
return h('li', { class: `kv-step kv-lane${failed ? ' kv-failed' : ''}`, dataset: { kind: 'lane', lane: l.name, laneKind: l.kind, job: l.jobId ?? '', status: l.status } },
|
|
319
|
+
h('div', { class: 'kv-step-head' },
|
|
320
|
+
h('span', { class: 'kv-step-label' }, l.name),
|
|
321
|
+
h('span', { class: `kv-tag ${l.kind === 'follow-up' ? 'kv-amber' : 'kv-blue'}`, title: 'What the lane answers' }, LANE_KIND_LABEL[l.kind] ?? l.kind),
|
|
322
|
+
l.stage ? h('span', { class: 'kv-faint' }, `sent in ${STAGE_LABEL[l.stage] ?? l.stage}`) : null,
|
|
323
|
+
l.model ? h('span', { class: 'kv-model' }, `${l.model.id} · thinking ${l.model.thinking ?? 'default'}`) : null,
|
|
324
|
+
h('span', { class: `kv-status${failed ? ' kv-bad' : ''}` }, l.status),
|
|
325
|
+
l.startedAt ? h('span', { class: 'kv-faint' }, `${hm(l.startedAt)}–${hm(l.endedAt)}`) : null),
|
|
326
|
+
l.question ? h('div', { class: 'kv-reading kv-question', style: 'white-space:pre-line' }, l.question) : h('div', { class: 'kv-reading kv-none' }, 'No brief found for this lane.'),
|
|
327
|
+
h('div', { class: 'kv-usage kv-slots', dataset: { slots: l.slots.join(' ') } }, `Writes: ${l.slots.length ? l.slots.join(', ') : 'its report only'}`),
|
|
328
|
+
h('div', { class: 'kv-usage kv-lane-read', dataset: { files: l.read.files, versions: l.read.versions, commits: l.read.commits, sessions: l.read.sessions } }, `Read: ${readText(l.read)}`),
|
|
329
|
+
timingRow(l.timing),
|
|
330
|
+
usageRow(l.usage),
|
|
331
|
+
docs.length ? h('div', { class: 'kv-docs' }, docs.map((d) => docButton(round.id, d, ctx))) : null);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** The lanes the main agent sent (RoundView.lanes), in the order sent, with how many ran at once. */
|
|
335
|
+
function lanesBlock(round, ctx) {
|
|
336
|
+
const lanes = round.lanes ?? [];
|
|
337
|
+
if (!lanes.length) return h('div', { class: 'kv-digs kv-lanes', dataset: { lanes: 0 } }, h('div', { class: 'kv-digs-head' }, 'Lanes — none sent yet'));
|
|
338
|
+
const most = lanesAtOnce(lanes);
|
|
339
|
+
return h('div', { class: 'kv-digs kv-lanes', dataset: { lanes: lanes.length, atOnce: most } },
|
|
340
|
+
h('div', { class: 'kv-digs-head' }, `Lanes — ${plural(lanes.length, 'lane')} sent by the main agent${most > 1 ? `, up to ${most} at once` : ''}`),
|
|
341
|
+
h('ol', { class: 'kv-steps' }, lanes.map((l) => laneView(round, l, ctx))));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** The coverage check (RoundView.coverage): what it listed as untouched by any lane, and the main agent's account of each. */
|
|
345
|
+
function coverageBlock(round) {
|
|
346
|
+
const c = round.coverage ?? null;
|
|
347
|
+
// A round whose kind has no coverage stage (First usable) shows none; a deepening shows it before the check has run too.
|
|
348
|
+
if (!c && !(ROUND_STAGES[round.kind] ?? []).includes('coverage')) return null;
|
|
349
|
+
const followUps = (round.lanes ?? []).filter((l) => l.kind === 'follow-up').length;
|
|
350
|
+
return h('div', { class: 'kv-digs kv-coverage', dataset: { settled: c ? String(c.settled) : 'none' } },
|
|
351
|
+
h('div', { class: 'kv-digs-head' }, 'Coverage check',
|
|
352
|
+
c ? h('span', { class: `kv-tag ${c.settled ? 'kv-green' : 'kv-amber'}`, style: 'margin-left:8px' }, c.settled ? 'settled' : 'not settled') : null,
|
|
353
|
+
c ? h('span', { class: 'kv-faint' }, ` ${fmtTime(c.at)}`) : null),
|
|
354
|
+
h('div', { class: 'kv-coverage-body' },
|
|
355
|
+
h('div', { class: 'kv-reading' }, coverageText(c), followUps ? ` · ${plural(followUps, 'follow-up lane')} sent` : ''),
|
|
356
|
+
c?.untouched?.length ? h('details', { class: 'kv-accounted kv-untouched' },
|
|
357
|
+
h('summary', { class: 'kv-text-btn' }, `Untouched by any lane · ${plural(c.untouched.length, 'group')}`),
|
|
358
|
+
h('ul', {}, c.untouched.map((g) => h('li', { dataset: { group: g.group } },
|
|
359
|
+
h('span', { class: 'kv-mono' }, g.group), h('span', { class: 'kv-faint' }, ` — ${plural(g.count, 'material')}, ${g.category}${g.dir ? ` in ${g.dir}` : ''}`),
|
|
360
|
+
g.keys.length ? h('div', { class: 'kv-faint kv-mono' }, `${g.keys.slice(0, 5).join(', ')}${g.count > 5 ? ', …' : ''}`) : null)))) : null,
|
|
361
|
+
c?.accounted.length ? h('details', { class: 'kv-accounted' },
|
|
362
|
+
h('summary', { class: 'kv-text-btn' }, `Accounted for, with why · ${c.accounted.length}`),
|
|
363
|
+
h('ul', {}, c.accounted.map((a) => h('li', { dataset: { outcome: a.outcome, group: a.group ?? '' } },
|
|
364
|
+
h('span', { class: `kv-tag${a.outcome === 'part' ? ' kv-amber' : ''}` }, a.outcome === 'part' ? 'in part' : 'not needed'), ' ',
|
|
365
|
+
h('span', { class: 'kv-mono' }, a.group ?? (a.keys?.length === 1 ? a.keys[0] : `${plural(a.keys?.length ?? 0, 'material')}`)),
|
|
366
|
+
h('span', { class: 'kv-faint' }, ` — ${a.why}${a.by === 'program' ? ' (the program)' : ''}`))))) : null));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** The main agent's item: its job as a step, then its stages, its lanes and the coverage check under it. */
|
|
370
|
+
function mainView(round, s, ctx) {
|
|
371
|
+
const item = s ? stepView(round.id, s, ctx, round)
|
|
372
|
+
: h('li', { class: 'kv-step', dataset: { kind: 'main', job: round.main.jobId ?? '' } }, h('div', { class: 'kv-step-head' }, h('span', { class: 'kv-step-label' }, STEP_LABEL.main), h('span', { class: 'kv-status' }, 'Not started')));
|
|
373
|
+
item.classList.add('kv-main');
|
|
374
|
+
item.append(h('div', { class: 'kv-main-body' }, stagesBlock(round), lanesBlock(round, ctx), coverageBlock(round)));
|
|
375
|
+
return item;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** A round the main agent runs: the ledger and session drafts, the main agent with all under it, the spot check, the process. */
|
|
379
|
+
function mainAgentSteps(round, ctx) {
|
|
380
|
+
const steps = round.steps.filter((s) => s.kind !== 'lane');
|
|
381
|
+
const out = steps.map((s) => (s.kind === 'main' ? mainView(round, s, ctx) : stepView(round.id, s, ctx, round)));
|
|
382
|
+
if (!steps.some((s) => s.kind === 'main')) {
|
|
383
|
+
const at = steps.findIndex((s) => !['ledger', 'session-drafts'].includes(s.kind));
|
|
384
|
+
out.splice(at < 0 ? out.length : at, 0, mainView(round, null, ctx));
|
|
385
|
+
}
|
|
386
|
+
return out;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function roundView(round, ctx) {
|
|
390
|
+
const d99 = isMainAgentRound(round);
|
|
391
|
+
const steps = d99 ? mainAgentSteps(round, ctx) : [];
|
|
392
|
+
let i = d99 ? round.steps.length : 0;
|
|
393
|
+
while (i < round.steps.length) {
|
|
394
|
+
const s = round.steps[i];
|
|
395
|
+
if (s.kind === 'dig') {
|
|
396
|
+
const digs = [];
|
|
397
|
+
while (i < round.steps.length && round.steps[i].kind === 'dig') { digs.push(round.steps[i]); i += 1; }
|
|
398
|
+
steps.push(sweepsByPath(round, digs, ctx) ?? h('div', { class: 'kv-digs' },
|
|
399
|
+
h('div', { class: 'kv-digs-head' }, `Deep sweeps — ${digs.length} paths in parallel`),
|
|
400
|
+
h('ol', { class: 'kv-steps' }, digs.map((d) => stepView(round.id, d, ctx, round)))));
|
|
401
|
+
} else {
|
|
402
|
+
steps.push(stepView(round.id, s, ctx, round));
|
|
403
|
+
i += 1;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const digsCount = d99 ? (round.lanes ?? []).length : new Set(round.steps.filter((s) => s.kind === 'dig').map((s) => s.path ?? s.label)).size;
|
|
407
|
+
// A round that ran through closes Done; what did not finish is said with its status, never left out of it.
|
|
408
|
+
const failures = failuresOf(round, ctx);
|
|
409
|
+
return h('section', { class: 'kv-round kv-card', dataset: { round: round.id, kind: round.kind, number: round.number } },
|
|
410
|
+
h('div', { class: 'kv-round-head' },
|
|
411
|
+
h('span', { class: 'kv-tag kv-blue' }, round.kind),
|
|
412
|
+
h('span', { class: 'kv-round-title' }, `Round ${round.number}`),
|
|
413
|
+
h('span', { class: `kv-tag ${failures.length ? 'kv-red' : 'kv-green'}`, dataset: { status: round.status, failures: failures.length } }, failures.length ? `${round.status} · ${failures.length} not finished` : round.status),
|
|
414
|
+
h('span', { class: 'kv-round-meta' }, `${fmtTime(round.startedAt)} → ${hm(round.endedAt)}`),
|
|
415
|
+
h('span', { class: 'kv-round-meta kv-strong' }, `wall ${ms(round.wallMs)}`)),
|
|
416
|
+
round.longestPathMs != null
|
|
417
|
+
? h('div', { class: 'kv-round-note' }, `longest path ${ms(round.longestPathMs)}${digsCount > 1 ? (d99 ? ` — ${digsCount} lanes, the main agent waiting while they run: the round lasts as long as its longest lane` : ` — ${digsCount} paths in parallel: the round lasts as long as its longest path`) : ''}`)
|
|
418
|
+
: null,
|
|
419
|
+
failuresNote(round, failures, ctx),
|
|
420
|
+
// What the round's end did with the project's `projectkeeper/` folder, or why it could not (RoundView.folder).
|
|
421
|
+
round.folder ? h('div', { class: 'kv-round-note kv-round-folder', dataset: { folder: 'yes' } }, round.folder) : null,
|
|
422
|
+
// The sweeps the program added for kinds of question orientation set none for, each with why (CKC-23 AC-4); the
|
|
423
|
+
// step of each carries the tag and opens the brief the program composed.
|
|
424
|
+
round.sweepsAdded?.length ? h('div', { class: 'kv-round-note kv-sweeps-added', dataset: { added: round.sweepsAdded.length } },
|
|
425
|
+
h('div', {}, round.sweepsAdded.length === 1 ? 'The program added a sweep for a kind of question orientation left out:' : `The program added ${round.sweepsAdded.length} sweeps for kinds of question orientation left out:`),
|
|
426
|
+
h('ul', {}, round.sweepsAdded.map((x) => h('li', { dataset: { path: x.path } }, h('span', { class: 'kv-step-path' }, x.path), ` — ${x.why}`)))) : null,
|
|
427
|
+
resultNote(round, ctx),
|
|
428
|
+
h('ol', { class: 'kv-steps' }, steps),
|
|
429
|
+
roundSum(round));
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* A finished Follow up round's one result (§5.5, §6.9): what it found, in one line, and the result itself — which stays
|
|
434
|
+
* reachable here after the owner has opened it from Notes (attention). `ctx.resultOf` gives it; none for other rounds.
|
|
435
|
+
*/
|
|
436
|
+
function resultNote(round, ctx) {
|
|
437
|
+
const result = round.kind === 'Follow up' ? ctx.resultOf?.(round) ?? null : null;
|
|
438
|
+
if (!result) return null;
|
|
439
|
+
return h('div', { class: 'kv-round-note kv-round-result', dataset: { result: result.id } },
|
|
440
|
+
h('span', {}, `Result: ${result.line}`), ' ',
|
|
441
|
+
h('button', { class: 'btn small kv-btn', type: 'button', title: `Open the result of ${result.name}`, onClick: () => ctx.openResult?.(result) }, 'Open the result'));
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export function renderRoundTree(container, rounds, ctx) {
|
|
445
|
+
ensureStyle();
|
|
446
|
+
container.replaceChildren();
|
|
447
|
+
container.append(h('div', { class: 'kv-rounds', dataset: { kv: 'rounds' } },
|
|
448
|
+
rounds.length ? rounds.map((r) => roundView(r, ctx)) : h('div', { class: 'kv-none' }, 'No rounds yet.')));
|
|
449
|
+
return container;
|
|
450
|
+
}
|
package/ui/k/scope-k.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// §6.7 the Project scope additions: ledger coverage (§1.16, CKC-22 AC-15/16), the layer mapping,
|
|
2
|
+
// earlier generations, the deepening questions, and the `Project folder` standing authorization
|
|
3
|
+
// (§1.14, CKC-26 AC-7/8). Pure rendering: the ScopeKView comes in as a parameter.
|
|
4
|
+
import { h, fmtRel } from '../app.js';
|
|
5
|
+
|
|
6
|
+
const ensureStyle = () => {
|
|
7
|
+
if (document.getElementById('k-views-css')) return;
|
|
8
|
+
const link = document.createElement('link');
|
|
9
|
+
link.id = 'k-views-css';
|
|
10
|
+
link.rel = 'stylesheet';
|
|
11
|
+
link.href = '/k/k-views.css';
|
|
12
|
+
document.head.append(link);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const num = (n) => Number(n).toLocaleString('en-US');
|
|
16
|
+
const day = (at) => (at ? at.slice(0, 10) : '—');
|
|
17
|
+
const sec = (ms) => `${(ms / 1000).toFixed(ms < 10000 ? 1 : 0)} s`;
|
|
18
|
+
|
|
19
|
+
const section = (title, ...kids) => h('section', { class: 'kv-section kv-card', dataset: { section: title } },
|
|
20
|
+
h('h3', {}, title), ...kids);
|
|
21
|
+
const plural = (n, one, many = `${one}s`) => `${num(n)} ${n === 1 ? one : many}`;
|
|
22
|
+
const mb = (bytes) => (bytes ? `${Math.round(bytes / 100_000) / 10} MB` : null);
|
|
23
|
+
const pathList = (paths, more, kind) => h('ul', { class: 'kv-notread-paths', dataset: { list: kind } },
|
|
24
|
+
paths.map((p) => h('li', { class: 'kv-mono' }, p)),
|
|
25
|
+
more ? h('li', { class: 'kv-faint' }, `and ${num(more)} more`) : null);
|
|
26
|
+
|
|
27
|
+
// How deep the ledger reads one language of the code, in the words the Code view says it (code-view.js levelText; the
|
|
28
|
+
// same CodeLanguageView, ScopeKView ledger.languages[].code; §1.16, §6.7, §6.17; D98 补): read by the TypeScript compiler,
|
|
29
|
+
// to symbols; read by the code engine, to symbols, with the imports it resolved to a file of the repository and those it
|
|
30
|
+
// left unresolved naming one; not read, its files and sizes only. Where a file is not read or a reference was left
|
|
31
|
+
// unresolved, residual calls there are Inferred; the ledger's own words for that are on hover.
|
|
32
|
+
function readText(l) {
|
|
33
|
+
if (!l.readBy) return { text: 'not read: files and sizes only', inferred: true };
|
|
34
|
+
const measured = [
|
|
35
|
+
l.read < l.files ? `${num(l.read)} of ${num(l.files)} files read` : null,
|
|
36
|
+
l.imports ? `${plural(l.imports.resolved, 'import')} resolved` : null,
|
|
37
|
+
l.imports ? `${num(l.imports.unresolvedNamingRepoFiles)} left unresolved naming a repository file` : null,
|
|
38
|
+
l.namedButUnreferenced ? `${plural(l.namedButUnreferenced, 'file')} named by other files but reached by no counted reference` : null,
|
|
39
|
+
l.parseErrors ? `${plural(l.parseErrors, 'file')} not parsed in full` : null,
|
|
40
|
+
].filter(Boolean);
|
|
41
|
+
return { text: `read by the ${l.readBy === 'compiler' ? 'TypeScript compiler' : 'code engine'}: symbols${measured.length ? `, ${measured.join(', ')}` : ''}`, inferred: !l.complete };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** One language's row: the code as the Code view says it, prose and data as no code (their files and sizes). */
|
|
45
|
+
function languageCell(l) {
|
|
46
|
+
if (!l.code) return h('td', { class: 'kv-reads', dataset: { readBy: 'not-code' } }, 'not code: files and sizes only');
|
|
47
|
+
const r = readText(l.code);
|
|
48
|
+
return h('td', { class: 'kv-reads', dataset: { readBy: l.code.readBy ?? 'none' }, title: l.code.gaps.length ? l.code.gaps.join('\n') : null },
|
|
49
|
+
r.text, r.inferred ? [' ', h('span', { class: 'kv-faint' }, '(residual calls here are Inferred)')] : null);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* What the ledger keeps without reading it (ScopeKView ledger.notRead; QC AY): the document versions it holds without
|
|
54
|
+
* their text — over the size it reads to, or not text — with the reports among them, whose verdicts are not read; the
|
|
55
|
+
* files outside version control too large to read; and what that means, in the ledger's own words. Nothing when it read
|
|
56
|
+
* everything.
|
|
57
|
+
*/
|
|
58
|
+
function notReadRows(nr) {
|
|
59
|
+
if (!nr) return [];
|
|
60
|
+
const docLimit = mb(nr.documentLimitBytes), looseLimit = mb(nr.looseLimitBytes);
|
|
61
|
+
return [
|
|
62
|
+
h('dt', {}, 'Kept without reading'),
|
|
63
|
+
h('dd', { class: 'kv-notread' },
|
|
64
|
+
nr.versionsWithoutText ? h('div', { dataset: { part: 'versions' } },
|
|
65
|
+
h('div', {}, `${plural(nr.versionsWithoutText, 'document version')} kept without ${nr.versionsWithoutText === 1 ? 'its' : 'their'} text — ${docLimit ? `over ${docLimit}` : 'over the size the ledger reads'}, or not text:`),
|
|
66
|
+
pathList(nr.paths, nr.morePaths, 'versions'),
|
|
67
|
+
nr.reports?.length ? h('div', { dataset: { part: 'reports' } }, `${nr.reports.length === 1 ? 'A report' : `${num(nr.reports.length)} reports`} among them, whose verdicts are not read: `, nr.reports.map((p, i) => [i ? ', ' : '', h('span', { class: 'kv-mono' }, p)])) : null) : null,
|
|
68
|
+
nr.unversionedTooLarge ? h('div', { dataset: { part: 'loose' } },
|
|
69
|
+
h('div', {}, `${plural(nr.unversionedTooLarge, 'file')} outside version control ${looseLimit ? `over ${looseLimit}` : 'too large to read'}, not read:`),
|
|
70
|
+
pathList(nr.unversionedPaths, Math.max(0, nr.unversionedTooLarge - nr.unversionedPaths.length), 'loose')) : null,
|
|
71
|
+
nr.effect ? h('div', { class: 'kv-faint', dataset: { part: 'effect' } }, nr.effect) : null),
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function ledgerSection(ledger) {
|
|
76
|
+
if (!ledger) return section('Ledger coverage', h('div', { class: 'kv-note-block' }, 'The ledger has not been built yet.'));
|
|
77
|
+
return section('Ledger coverage',
|
|
78
|
+
h('table', { class: 'kv-mini' },
|
|
79
|
+
h('thead', {}, h('tr', {}, h('th', {}, 'Repo'), h('th', {}, 'Commits'), h('th', {}, 'Branches'), h('th', {}, 'Merges'), h('th', {}, 'Span'), h('th', {}, 'Version history'))),
|
|
80
|
+
h('tbody', {}, ledger.repos.map((r) => h('tr', {},
|
|
81
|
+
h('td', { class: 'kv-mono' }, r.repo),
|
|
82
|
+
// On the trunk alone and over all refs, when the two differ (ScopeKView ledger.repos[].trunkCommits).
|
|
83
|
+
h('td', {}, r.trunkCommits != null && r.trunkCommits !== r.commits
|
|
84
|
+
? h('span', { title: 'commits on the trunk · commits on all refs' }, `${num(r.trunkCommits)} trunk · ${num(r.commits)} all`) : num(r.commits)),
|
|
85
|
+
h('td', {}, num(r.branches)),
|
|
86
|
+
h('td', {}, num(r.merges)),
|
|
87
|
+
h('td', {}, `${day(r.from)} → ${day(r.to)}`),
|
|
88
|
+
// Every repo here is under version control: `historyFrom` is set when documents entered it later than the first
|
|
89
|
+
// commit (an import root, CKC-22 AC-15); otherwise the history starts at the first commit.
|
|
90
|
+
h('td', {}, r.historyFrom || r.from ? h('span', {}, `since ${day(r.historyFrom ?? r.from)}`) : '—'))))),
|
|
91
|
+
h('table', { class: 'kv-mini', style: 'margin-top:8px' },
|
|
92
|
+
h('thead', {}, h('tr', {}, h('th', {}, 'Language'), h('th', {}, 'Code structure reaches'), h('th', {}, 'Files'))),
|
|
93
|
+
h('tbody', {}, ledger.languages.map((l) => h('tr', { dataset: { lang: l.language } },
|
|
94
|
+
h('td', {}, l.language),
|
|
95
|
+
// What the Code view says of the language (CKC-03 AC-27): a language the ledger reads no references of is not
|
|
96
|
+
// called read, and the engine's languages are read to symbols with what it left unresolved said.
|
|
97
|
+
languageCell(l),
|
|
98
|
+
h('td', {}, num(l.files)))))),
|
|
99
|
+
h('dl', { class: 'kv-kv', style: 'margin-top:8px' },
|
|
100
|
+
h('dt', {}, 'Sessions read'), h('dd', {}, `${num(ledger.sessions.read)} · hosts: ${ledger.sessions.hosts.join(', ') || '—'}`),
|
|
101
|
+
ledger.sessions.missing.length ? h('dt', {}, 'Unreadable sessions') : null,
|
|
102
|
+
ledger.sessions.missing.length ? h('dd', {}, ledger.sessions.missing.map((m) => h('div', { class: 'kv-missing' }, `⚠ ${m.host} · ${day(m.from)} → ${day(m.to)} — ${m.why}`))) : null,
|
|
103
|
+
h('dt', {}, 'Docs outside version control'), h('dd', {}, num(ledger.unversionedDocs)),
|
|
104
|
+
notReadRows(ledger.notRead),
|
|
105
|
+
h('dt', {}, 'Last rebuild'), h('dd', {}, ledger.lastRebuild ? `${fmtRel(ledger.lastRebuild.at)} · took ${sec(ledger.lastRebuild.ms)}` : '—')));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const layersSection = (layers) => section('Layers',
|
|
109
|
+
h('table', { class: 'kv-mini' },
|
|
110
|
+
h('thead', {}, h('tr', {}, h('th', {}, 'File / directory'), h('th', {}, 'Layer'), h('th', {}, 'Note'), h('th', {}, 'Current'))),
|
|
111
|
+
h('tbody', {}, layers.map((l) => h('tr', {},
|
|
112
|
+
h('td', { class: `kv-mono${l.current ? '' : ' kv-history'}` }, l.path),
|
|
113
|
+
h('td', {}, l.layer),
|
|
114
|
+
h('td', { class: 'kv-muted' }, l.note ?? '—'),
|
|
115
|
+
h('td', {}, l.current ? h('span', { class: 'kv-tag kv-green' }, 'current') : h('span', { class: 'kv-tag' }, 'history')))))));
|
|
116
|
+
|
|
117
|
+
const generationsSection = (gens) => section('Earlier generations',
|
|
118
|
+
gens.length
|
|
119
|
+
? h('table', { class: 'kv-mini' },
|
|
120
|
+
h('thead', {}, h('tr', {}, h('th', {}, 'Generation'), h('th', {}, 'Ended'), h('th', {}, 'Ended by'))),
|
|
121
|
+
h('tbody', {}, gens.map((g) => h('tr', {},
|
|
122
|
+
h('td', {}, g.name),
|
|
123
|
+
h('td', {}, day(g.ended.at)),
|
|
124
|
+
h('td', { class: 'kv-muted' }, g.endedBy.label)))))
|
|
125
|
+
: h('div', { class: 'kv-note-block' }, 'The material names no earlier generations.'));
|
|
126
|
+
|
|
127
|
+
const questionsSection = (questions, ctx) => section('Questions the deepening asks',
|
|
128
|
+
questions
|
|
129
|
+
? h('div', {},
|
|
130
|
+
h('ul', { style: 'margin:0 0 6px; padding-left:18px' }, questions.paths.map((p) => h('li', {}, p))),
|
|
131
|
+
h('button', { class: 'btn small kv-btn', type: 'button', onClick: () => ctx.navigate('keeper', { roundId: questions.roundId, docId: questions.docId }) }, 'Open the question list in the round'))
|
|
132
|
+
: h('div', { class: 'kv-note-block' }, 'No question list yet — it is written by the first orientation.'));
|
|
133
|
+
|
|
134
|
+
const folderSection = (pf) => {
|
|
135
|
+
if (!pf || !pf.granted) {
|
|
136
|
+
return section('Project folder',
|
|
137
|
+
h('div', { class: 'kv-note-block', dataset: { granted: 'no' } },
|
|
138
|
+
h('p', {}, h('b', {}, 'Not granted.'), ' Keeper writes nothing into the project: semantic patches, Keeper-given numbers and distilled decisions live only on this workbench.'),
|
|
139
|
+
h('p', {}, 'This authorization can be granted on the Keeper page, under Project folder. With it, Keeper writes exactly those files into one folder you choose (for example ', h('span', { class: 'kv-mono' }, '<project>/projectkeeper/'), '); by default Keeper commits them itself — every commit contains only that folder, says it comes from ProjectKeeper, and is never pushed. You can also choose write-only: the files then appear as uncommitted changes.')));
|
|
140
|
+
}
|
|
141
|
+
return section('Project folder',
|
|
142
|
+
h('dl', { class: 'kv-kv', dataset: { granted: 'yes' } },
|
|
143
|
+
h('dt', {}, 'Authorization'), h('dd', {}, h('span', { class: 'kv-tag kv-green' }, 'granted'), pf.authorizationId ? ` · ${pf.authorizationId}` : ''),
|
|
144
|
+
h('dt', {}, 'Folder'), h('dd', { class: 'kv-mono' }, pf.path ?? '—'),
|
|
145
|
+
h('dt', {}, 'Committed by Keeper'), h('dd', {}, pf.commits ? 'yes — each commit contains only this folder, never pushed' : 'no — writes appear as uncommitted changes'),
|
|
146
|
+
h('dt', {}, 'Last write'), h('dd', {}, pf.lastWrite ? `${fmtRel(pf.lastWrite.at)}${pf.lastWrite.commit ? ` · ${pf.lastWrite.commit.slice(0, 7)}` : ''}` : 'never')));
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export function renderScopeK(container, scopeK, ctx) {
|
|
150
|
+
ensureStyle();
|
|
151
|
+
container.replaceChildren();
|
|
152
|
+
container.append(h('div', { class: 'kv-scope', dataset: { kv: 'scope-k' } },
|
|
153
|
+
ledgerSection(scopeK.ledger),
|
|
154
|
+
layersSection(scopeK.layers),
|
|
155
|
+
generationsSection(scopeK.generations),
|
|
156
|
+
questionsSection(scopeK.questions, ctx),
|
|
157
|
+
folderSection(scopeK.projectFolder)));
|
|
158
|
+
return container;
|
|
159
|
+
}
|