toolroll 0.9.7 → 0.9.9
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 +48 -1
- package/dist/assignment-adapters.d.ts +1 -0
- package/dist/assignment-adapters.js +2 -0
- package/dist/assignment-brief.d.ts +2 -0
- package/dist/assignment-brief.js +2 -1
- package/dist/assignment-presentation.js +3 -1
- package/dist/assignment-status.d.ts +1 -1
- package/dist/assignment-ui.js +5 -0
- package/dist/assignment.d.ts +15 -0
- package/dist/assignment.js +56 -9
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +18 -18
- package/dist/browser-crew.js +4 -0
- package/dist/browser-workspace.d.ts +40 -0
- package/dist/build-review.d.ts +38 -0
- package/dist/build-review.js +341 -0
- package/dist/builder.d.ts +2 -0
- package/dist/builder.js +114 -8
- package/dist/chat-controls.d.ts +1 -1
- package/dist/chat-controls.js +1 -1
- package/dist/check-levels-ui.d.ts +5 -0
- package/dist/check-levels-ui.js +16 -2
- package/dist/check-progress.js +4 -3
- package/dist/checkout-cleanup.d.ts +40 -2
- package/dist/checkout-cleanup.js +129 -9
- package/dist/cli.js +2 -0
- package/dist/desktop-update.d.ts +52 -0
- package/dist/desktop-update.js +147 -5
- package/dist/dispatch.js +2 -1
- package/dist/dispose.d.ts +4 -3
- package/dist/dispose.js +12 -4
- package/dist/evidence.d.ts +6 -0
- package/dist/evidence.js +8 -0
- package/dist/exec.d.ts +5 -0
- package/dist/exec.js +30 -4
- package/dist/first-run.d.ts +1 -1
- package/dist/first-run.js +8 -6
- package/dist/invoke.js +5 -0
- package/dist/lead-context.d.ts +1 -1
- package/dist/lead-context.js +4 -2
- package/dist/lead-status.d.ts +10 -1
- package/dist/lead-status.js +21 -24
- package/dist/modes.d.ts +3 -1
- package/dist/modes.js +3 -1
- package/dist/needs-you.d.ts +55 -0
- package/dist/needs-you.js +83 -0
- package/dist/operate.d.ts +2 -0
- package/dist/operate.js +231 -36
- package/dist/plane-review.js +4 -0
- package/dist/process-custody.js +8 -0
- package/dist/process-recovery-eligibility.d.ts +1 -0
- package/dist/project-learning.d.ts +3 -3
- package/dist/project-learning.js +28 -17
- package/dist/proof.d.ts +3 -1
- package/dist/proof.js +4 -2
- package/dist/provider.js +16 -0
- package/dist/result-completion.d.ts +4 -0
- package/dist/result-completion.js +11 -0
- package/dist/retention-ui.d.ts +3 -1
- package/dist/retention-ui.js +4 -4
- package/dist/retention.d.ts +13 -6
- package/dist/retention.js +50 -12
- package/dist/review-context.d.ts +3 -0
- package/dist/review-context.js +45 -5
- package/dist/review-switch.d.ts +41 -0
- package/dist/review-switch.js +88 -0
- package/dist/reviewer.d.ts +24 -2
- package/dist/reviewer.js +49 -1
- package/dist/run-closeout.d.ts +89 -0
- package/dist/run-closeout.js +302 -0
- package/dist/serve.d.ts +15 -1
- package/dist/serve.js +158 -18
- package/dist/setup-guide.d.ts +2 -2
- package/dist/shared-deps.d.ts +94 -0
- package/dist/shared-deps.js +365 -0
- package/dist/storage.js +2 -0
- package/dist/store.d.ts +72 -1
- package/dist/store.js +258 -31
- package/dist/surface.js +5 -1
- package/dist/task-status.d.ts +22 -5
- package/dist/task-status.js +45 -24
- package/dist/telegram-progress.js +2 -1
- package/dist/telegram-status.js +3 -3
- package/dist/test-temp.d.ts +19 -0
- package/dist/test-temp.js +98 -0
- package/dist/toolroll-update-ui.js +7 -2
- package/dist/toolroll-update.d.ts +21 -1
- package/dist/toolroll-update.js +135 -5
- package/dist/work-index.d.ts +11 -0
- package/dist/work-index.js +103 -16
- package/dist/work-summary.d.ts +3 -1
- package/dist/work-summary.js +2 -1
- package/dist/workspace-ui.d.ts +3 -1
- package/dist/workspace-ui.js +10 -9
- package/dist/worktree.d.ts +16 -0
- package/dist/worktree.js +47 -0
- package/package.json +1 -1
package/dist/project-learning.js
CHANGED
|
@@ -44,12 +44,21 @@ CREATE TRIGGER IF NOT EXISTS learning_capture_no_delete BEFORE DELETE ON learnin
|
|
|
44
44
|
export const learningSha = (text) => createHash('sha256').update(text).digest('hex');
|
|
45
45
|
const text = (x, max) => typeof x === 'string' && x.trim().length > 0 && Buffer.byteLength(x) <= max && !/[\x00-\x1f\x7f]/.test(x);
|
|
46
46
|
const pathOk = (x) => text(x, 300) && !x.startsWith('/') && !x.includes('\\') && !/^[A-Za-z]:/.test(x) && x.split('/').every(p => p !== '.' && p !== '..' && p !== '') && !/[\*?\[\]:]/.test(x);
|
|
47
|
-
|
|
47
|
+
/** One read per distinct command within a single selection pass (`seen`), so a pass over many lessons reads the repository once. */
|
|
48
|
+
const git = (repo, args, seen) => {
|
|
49
|
+
const key = JSON.stringify([repo, args]);
|
|
50
|
+
const known = seen?.get(key);
|
|
51
|
+
if (known !== undefined)
|
|
52
|
+
return known;
|
|
53
|
+
const out = execFileSync('git', ['--no-optional-locks', '-C', repo, ...args], { encoding: 'utf8', maxBuffer: 2 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] }).trimEnd();
|
|
54
|
+
seen?.set(key, out);
|
|
55
|
+
return out;
|
|
56
|
+
};
|
|
48
57
|
/** Path plus the physical shared Git database: replacing a repository does not inherit its lessons. */
|
|
49
|
-
export function learningIdentity(repo) {
|
|
58
|
+
export function learningIdentity(repo, seen) {
|
|
50
59
|
if (canonicalProject(repo) !== repo)
|
|
51
60
|
throw new Error('Project identity changed.');
|
|
52
|
-
const common = realpathSync(resolve(repo, git(repo, ['rev-parse', '--git-common-dir'])));
|
|
61
|
+
const common = realpathSync(resolve(repo, git(repo, ['rev-parse', '--git-common-dir'], seen)));
|
|
53
62
|
const st = statSync(common);
|
|
54
63
|
return learningSha(JSON.stringify([repo, common, st.dev, st.ino, st.birthtimeMs]));
|
|
55
64
|
}
|
|
@@ -59,10 +68,10 @@ function environmentFingerprint(store, repo) {
|
|
|
59
68
|
}
|
|
60
69
|
const configPath = (p) => /(^|\/)(AGENTS\.md|package(-lock)?\.json|[^/]*lock[^/]*|[^/]*config[^/]*|Cargo\.toml|go\.mod|pyproject\.toml|\.tool-versions|\.nvmrc)$/.test(p);
|
|
61
70
|
/** Compare the applicable code and configuration, not model confidence or a claimed passing command. */
|
|
62
|
-
export function learningFingerprint(repo, head, paths) {
|
|
71
|
+
export function learningFingerprint(repo, head, paths, seen) {
|
|
63
72
|
if (!/^[a-f0-9]{40,64}$/.test(head))
|
|
64
73
|
throw new Error('No exact source revision.');
|
|
65
|
-
const entries = git(repo, ['ls-tree', '-r', head]).split('\n').filter(line => {
|
|
74
|
+
const entries = git(repo, ['ls-tree', '-r', head], seen).split('\n').filter(line => {
|
|
66
75
|
const p = line.slice(line.indexOf('\t') + 1);
|
|
67
76
|
return paths.includes(p) || configPath(p);
|
|
68
77
|
});
|
|
@@ -198,7 +207,7 @@ function verifyEvidence(store, root, source, evidence, catalog) {
|
|
|
198
207
|
// missing work, so newer reviews cannot starve an older recovery.
|
|
199
208
|
const recoveryCursors = new WeakMap();
|
|
200
209
|
/** Validate after core commit; both suggestions commit or neither. */
|
|
201
|
-
export function recoverLearning(store, root, repo, now = new Date()) {
|
|
210
|
+
export function recoverLearning(store, root, repo, now = new Date(), seen = new Map()) {
|
|
202
211
|
let projects = recoveryCursors.get(store);
|
|
203
212
|
if (!projects)
|
|
204
213
|
recoveryCursors.set(store, projects = new Map());
|
|
@@ -225,7 +234,7 @@ export function recoverLearning(store, root, repo, now = new Date()) {
|
|
|
225
234
|
if (!source || !reviewer || source.taskRef !== reviewer.taskRef)
|
|
226
235
|
continue;
|
|
227
236
|
const snapshot = reviewSnapshot(store, reviewer.id);
|
|
228
|
-
if (!snapshot || snapshot['identity'] !== learningIdentity(repo))
|
|
237
|
+
if (!snapshot || snapshot['identity'] !== learningIdentity(repo, seen))
|
|
229
238
|
continue;
|
|
230
239
|
const response = store.artifactsFor(reviewer.id).find(a => a.kind === 'structured-output' && a.captureStatus === 'ok');
|
|
231
240
|
const read = response && readVerifiedArtifact(root, response);
|
|
@@ -260,7 +269,7 @@ export function recoverLearning(store, root, repo, now = new Date()) {
|
|
|
260
269
|
if (!run || !reviewed || reviewed.outcome !== 'no-change' || reviewed.taskRef !== run.taskRef || reviewed.role !== 'reviewer' || reviewedSource(store, reviewer) !== source || store.refForId(run.taskRef)?.repo !== repo)
|
|
261
270
|
throw new Error('Learning review provenance is unavailable.');
|
|
262
271
|
const candidates = parseLearning(JSON.parse(String(row['payload'])));
|
|
263
|
-
const identity = learningIdentity(repo);
|
|
272
|
+
const identity = learningIdentity(repo, seen);
|
|
264
273
|
if (identity !== row['identity'])
|
|
265
274
|
throw new Error('Learning project identity changed.');
|
|
266
275
|
const head = run.headRevision ?? run.baseRevision ?? '';
|
|
@@ -273,7 +282,7 @@ export function recoverLearning(store, root, repo, now = new Date()) {
|
|
|
273
282
|
verifyEvidence(store, root, source, c.evidence, JSON.parse(String(row['catalog'])));
|
|
274
283
|
if (c.paths.some(p => !patch.split('\n').some(l => l === `+++ b/${p}` || l === `--- a/${p}`)))
|
|
275
284
|
throw new Error('Learning applicability must name reviewed files.');
|
|
276
|
-
const payload = JSON.stringify({ ...c, environment: String(row['environment']), fingerprint: learningFingerprint(repo, head, c.paths), platform: process.platform, head });
|
|
285
|
+
const payload = JSON.stringify({ ...c, environment: String(row['environment']), fingerprint: learningFingerprint(repo, head, c.paths, seen), platform: process.platform, head });
|
|
277
286
|
const finding = learningSha(JSON.stringify(c));
|
|
278
287
|
store.handle.prepare("INSERT OR IGNORE INTO project_lesson(repo,identity,source,reviewer,finding,payload,sha,status) VALUES (?,?,?,?,?,?,?,'proposed')").run(repo, identity, source, reviewer, finding, payload, learningSha(payload));
|
|
279
288
|
const id = Number(store.handle.prepare('SELECT id FROM project_lesson WHERE source=? AND finding=?').get(source, finding)['id']);
|
|
@@ -302,15 +311,15 @@ function lessonOf(row) {
|
|
|
302
311
|
parseLearning([payload]);
|
|
303
312
|
return { id: Number(row['id']), repo: String(row['repo']), source: Number(row['source']), reviewer: Number(row['reviewer']), status: String(row['status']), version: Number(row['version']), payload, sha: String(row['sha']), identity: String(row['identity']), adoptedBy: row['adopted_by'] == null ? null : String(row['adopted_by']) };
|
|
304
313
|
}
|
|
305
|
-
function supported(store, root, lesson) {
|
|
306
|
-
if (lesson.identity !== learningIdentity(lesson.repo))
|
|
314
|
+
function supported(store, root, lesson, seen) {
|
|
315
|
+
if (lesson.identity !== learningIdentity(lesson.repo, seen))
|
|
307
316
|
throw new Error('Project identity changed.');
|
|
308
317
|
const capture = store.handle.prepare('SELECT catalog FROM learning_capture WHERE source=? AND reviewer=? AND repo=?').get(lesson.source, lesson.reviewer, lesson.repo);
|
|
309
318
|
if (!capture || reviewedSource(store, lesson.reviewer) !== lesson.source)
|
|
310
319
|
throw new Error('Learning source is unavailable.');
|
|
311
320
|
verifyEvidence(store, root, lesson.source, lesson.payload.evidence, JSON.parse(String(capture['catalog'])));
|
|
312
321
|
const run = store.getRun(lesson.source);
|
|
313
|
-
if ((run?.headRevision ?? run?.baseRevision) !== lesson.payload.head || learningFingerprint(lesson.repo, lesson.payload.head, lesson.payload.paths) !== lesson.payload.fingerprint)
|
|
322
|
+
if ((run?.headRevision ?? run?.baseRevision) !== lesson.payload.head || learningFingerprint(lesson.repo, lesson.payload.head, lesson.payload.paths, seen) !== lesson.payload.fingerprint)
|
|
314
323
|
throw new Error('Learning source changed.');
|
|
315
324
|
}
|
|
316
325
|
export function learningView(store, root, repo, actor, before = 0) {
|
|
@@ -377,15 +386,17 @@ export function learningContext(store, root, runId, phase, now = new Date(), val
|
|
|
377
386
|
const run = store.getRun(runId), ref = run && store.refForId(run.taskRef), repo = ref?.repo;
|
|
378
387
|
if (!run || !repo)
|
|
379
388
|
return '';
|
|
389
|
+
// One read of the repository per command for this whole pass, recovery included.
|
|
390
|
+
const seen = new Map();
|
|
380
391
|
try {
|
|
381
|
-
recoverLearning(store, root, repo, now);
|
|
392
|
+
recoverLearning(store, root, repo, now, seen);
|
|
382
393
|
return store.transact(() => {
|
|
383
394
|
if (!store.schemaCurrent() || run.outcome !== null || !['planner', 'builder', 'repair', 'reviewer'].includes(run.role))
|
|
384
395
|
return '';
|
|
385
396
|
const runner = store.getRunner(run.runner)?.runner;
|
|
386
397
|
if (!runner || runner.retiredAt !== null || !runner.repos.includes(repo) || (phase === 'review') !== (run.role === 'reviewer') || (phase === 'plan') !== (run.role === 'planner'))
|
|
387
398
|
return '';
|
|
388
|
-
const identity = learningIdentity(repo);
|
|
399
|
+
const identity = learningIdentity(repo, seen);
|
|
389
400
|
const existing = store.handle.prepare('SELECT * FROM learning_snapshot WHERE run=?').get(runId);
|
|
390
401
|
if (existing) {
|
|
391
402
|
if (existing['repo'] !== repo || existing['identity'] !== identity || learningSha(String(existing['payload'])) !== existing['sha'])
|
|
@@ -409,11 +420,11 @@ export function learningContext(store, root, runId, phase, now = new Date(), val
|
|
|
409
420
|
const l = lessonOf(row);
|
|
410
421
|
if (!store.handle.prepare("SELECT 1 FROM learning_event WHERE lesson=? AND action='adopt' AND actor=?").get(l.id, l.adoptedBy ?? '') || l.source === source?.id || l.payload.kind !== 'project' || !l.adoptedBy || !store.accountCanAccess(l.adoptedBy, repo) || store.accountOf(l.adoptedBy)?.role !== 'approver' || !l.payload.phases.includes(phase) || l.payload.platform !== process.platform || (!unscopedPlan && !l.payload.paths.some(p => paths.some(t => p === t || p.startsWith(t.replace(/\/$/, '') + '/')))))
|
|
411
422
|
continue;
|
|
412
|
-
supported(store, root, l);
|
|
413
|
-
if (l.payload.environment !== environmentFingerprint(store, repo) || learningFingerprint(repo, head, l.payload.paths) !== l.payload.fingerprint)
|
|
423
|
+
supported(store, root, l, seen);
|
|
424
|
+
if (l.payload.environment !== environmentFingerprint(store, repo) || learningFingerprint(repo, head, l.payload.paths, seen) !== l.payload.fingerprint)
|
|
414
425
|
continue;
|
|
415
426
|
const cwd = run.worktree ?? repo;
|
|
416
|
-
const dirty = git(cwd, ['status', '--porcelain', '--untracked-files=all']).split('\n').some(line => { const p = line.slice(3); return configPath(p) || l.payload.paths.includes(p); });
|
|
427
|
+
const dirty = git(cwd, ['status', '--porcelain', '--untracked-files=all'], seen).split('\n').some(line => { const p = line.slice(3); return configPath(p) || l.payload.paths.includes(p); });
|
|
417
428
|
if (!dirty)
|
|
418
429
|
eligible.push(l);
|
|
419
430
|
}
|
package/dist/proof.d.ts
CHANGED
|
@@ -266,7 +266,7 @@ export type VerifyCommandFacts = {
|
|
|
266
266
|
configured: true;
|
|
267
267
|
ran: false;
|
|
268
268
|
attemptFailed: true;
|
|
269
|
-
failure?: "spawn-failed" | "timed-out" | "dependency-missing" | "setup-stale" | "setup-failed" | "tracked-files-changed" | "setup-changed-files" | "checkout-moved" | "cleanliness-unavailable" | "dependency-still-missing" | "retry-spawn-failed" | "retry-timed-out" | "custody-lost";
|
|
269
|
+
failure?: "spawn-failed" | "timed-out" | "dependency-missing" | "setup-stale" | "setup-failed" | "own-install-failed" | "tracked-files-changed" | "setup-changed-files" | "checkout-moved" | "cleanliness-unavailable" | "dependency-still-missing" | "retry-spawn-failed" | "retry-timed-out" | "custody-lost";
|
|
270
270
|
} | {
|
|
271
271
|
configured: true;
|
|
272
272
|
ran: true;
|
|
@@ -390,6 +390,8 @@ export type CriterionCoverage = {
|
|
|
390
390
|
* context: eligible only with the same text, verified ancestry, and
|
|
391
391
|
* byte-identical relevant code. Never a judgement on this run. */
|
|
392
392
|
priorSupport: "eligible" | "invalid" | "none";
|
|
393
|
+
/** The change's images and other binary files in one line, when it has any. */
|
|
394
|
+
assets?: string;
|
|
393
395
|
};
|
|
394
396
|
/** v40: the three words an evidence reviewer can say about one signed
|
|
395
397
|
* criterion, and nothing else — `cannot-tell` is a CORRECT answer whenever
|
package/dist/proof.js
CHANGED
|
@@ -722,7 +722,7 @@ function assessCapturedEvidence(input) {
|
|
|
722
722
|
id: criterion.id, statement: criterion.statement, requiredEvidence: criterion.evidence,
|
|
723
723
|
assessment: { evidenceState, detail }, state: evidenceState === "pass" ? "missing" : evidenceState,
|
|
724
724
|
detail: [...detail, `criterion "${criterion.id}" awaits independent goal assessment`], answered, review: null,
|
|
725
|
-
...(coverage === undefined ? {} : { coverage: { state: coverage.state, inherited: coverage.inherited, items: [...coverage.items], gaps: [...coverage.gaps], priorSupport: coverage.priorSupport } }),
|
|
725
|
+
...(coverage === undefined ? {} : { coverage: { state: coverage.state, inherited: coverage.inherited, items: [...coverage.items], gaps: [...coverage.gaps], priorSupport: coverage.priorSupport, ...(coverage.assets === undefined ? {} : { assets: coverage.assets }) } }),
|
|
726
726
|
};
|
|
727
727
|
});
|
|
728
728
|
return { machineVerdict, verdict: machineVerdict === "refuted" ? "refuted" : "short", matrix,
|
|
@@ -761,6 +761,8 @@ function verificationFailureWords(failure) {
|
|
|
761
761
|
return "automatic recovery stopped because the project setup or check changed";
|
|
762
762
|
case "setup-failed":
|
|
763
763
|
return "the approved setup command failed during automatic recovery";
|
|
764
|
+
case "own-install-failed":
|
|
765
|
+
return "the checkout's own dependency install failed after its package.json or lockfile changed";
|
|
764
766
|
case "tracked-files-changed":
|
|
765
767
|
return "automatic recovery stopped because tracked files no longer matched the built result";
|
|
766
768
|
case "setup-changed-files":
|
|
@@ -828,7 +830,7 @@ export function adjudicate(input) {
|
|
|
828
830
|
// must show, never a silent "judged from the patch".
|
|
829
831
|
const coverage = found === undefined
|
|
830
832
|
? { state: "gap", inherited: false, items: [], gaps: ["the sealed review context carries no coverage entry for this criterion"], priorSupport: "none" }
|
|
831
|
-
: { state: found.state, inherited: found.inherited, items: [...found.items], gaps: [...found.gaps], priorSupport: found.priorSupport };
|
|
833
|
+
: { state: found.state, inherited: found.inherited, items: [...found.items], gaps: [...found.gaps], priorSupport: found.priorSupport, ...(found.assets === undefined ? {} : { assets: found.assets }) };
|
|
832
834
|
return { ...row, coverage };
|
|
833
835
|
});
|
|
834
836
|
if (!input.proofArtifactPresent) {
|
package/dist/provider.js
CHANGED
|
@@ -169,6 +169,22 @@ const CLAUDE_REVIEW_JSON_SCHEMA = {
|
|
|
169
169
|
additionalProperties: false,
|
|
170
170
|
},
|
|
171
171
|
},
|
|
172
|
+
// The automatic build review's reply (build-review.ts). Shape only;
|
|
173
|
+
// parseBuildFindings is the validator.
|
|
174
|
+
findings: {
|
|
175
|
+
type: "array",
|
|
176
|
+
items: {
|
|
177
|
+
type: "object",
|
|
178
|
+
properties: {
|
|
179
|
+
severity: { type: "string", enum: ["HIGH", "MEDIUM", "LOW"] },
|
|
180
|
+
file: { type: "string" },
|
|
181
|
+
line: { type: "integer", minimum: 1 },
|
|
182
|
+
scenario: { type: "string" },
|
|
183
|
+
},
|
|
184
|
+
required: ["severity", "file", "line", "scenario"],
|
|
185
|
+
additionalProperties: false,
|
|
186
|
+
},
|
|
187
|
+
},
|
|
172
188
|
// The read request rides the same structured reply channel and
|
|
173
189
|
// session. The machine validates the exact allowlist/hash/range
|
|
174
190
|
// before supplying bytes; this only shapes the request.
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
import type { Store } from "./store.js";
|
|
7
7
|
/** The ledger action a person's (or lead's) completion of a result writes. */
|
|
8
8
|
export declare const COMPLETION_ACTION = "assignment handoff checked";
|
|
9
|
+
/** Whether a task's family is marked complete. Completion is recorded on the
|
|
10
|
+
* family's root against its current version's latest result, so the root, a
|
|
11
|
+
* superseded revision and the current version all read Complete together. */
|
|
12
|
+
export declare function familyCompleted(store: Store, taskId: string): boolean;
|
|
9
13
|
/** The files a build changed, renames on both sides; null when the inventory can't be read whole. */
|
|
10
14
|
export declare function changedFilesOf(store: Store, runId: number, root: string): string[] | null;
|
|
11
15
|
/** Everything a result's branch carries: this build's changes and every build of every task it revises (a
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { readVerifiedArtifact } from "./evidence.js";
|
|
2
2
|
/** The ledger action a person's (or lead's) completion of a result writes. */
|
|
3
3
|
export const COMPLETION_ACTION = "assignment handoff checked";
|
|
4
|
+
/** Whether a task's family is marked complete. Completion is recorded on the
|
|
5
|
+
* family's root against its current version's latest result, so the root, a
|
|
6
|
+
* superseded revision and the current version all read Complete together. */
|
|
7
|
+
export function familyCompleted(store, taskId) {
|
|
8
|
+
const family = store.taskFamilyOf(taskId, null, true);
|
|
9
|
+
return store.handle.prepare(`SELECT 1 AS hit FROM action_ledger INDEXED BY work_completion
|
|
10
|
+
WHERE task_id IN (?, ?) AND action = ? AND source = 'work'
|
|
11
|
+
AND run_id = (SELECT run.id FROM run INDEXED BY work_result WHERE run.task_ref = ?
|
|
12
|
+
AND run.finished_at IS NOT NULL AND run.role IN ('builder','scout') ORDER BY run.id DESC LIMIT 1) LIMIT 1`)
|
|
13
|
+
.get(taskId, family?.root.id ?? taskId, COMPLETION_ACTION, family?.current.refId ?? store.lookupRef(taskId)?.id ?? -1) !== undefined;
|
|
14
|
+
}
|
|
4
15
|
/** The files a build changed, renames on both sides; null when the inventory can't be read whole. */
|
|
5
16
|
export function changedFilesOf(store, runId, root) {
|
|
6
17
|
const stat = store.artifactsFor(runId).find(one => one.kind === "diff-stat");
|
package/dist/retention-ui.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { type RetentionCount, type RetentionPeriods } from "./retention.js";
|
|
1
|
+
import { type RetentionCount, type RetentionKind, type RetentionPeriods } from "./retention.js";
|
|
2
2
|
export declare const RETENTION_CSS: string;
|
|
3
|
+
/** `chosen`: the kinds someone chose a period for; the rest keep their default. */
|
|
3
4
|
export type RetentionView = {
|
|
4
5
|
periods: RetentionPeriods;
|
|
6
|
+
chosen: readonly RetentionKind[];
|
|
5
7
|
next: RetentionCount[];
|
|
6
8
|
lastSweep: string | null;
|
|
7
9
|
csrf: string;
|
package/dist/retention-ui.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* take a step-up.
|
|
5
5
|
*/
|
|
6
6
|
import { bytesWords } from "./storage.js";
|
|
7
|
-
import {
|
|
7
|
+
import { RETENTION_KINDS, countWords, periodChoices, periodLabel } from "./retention.js";
|
|
8
8
|
const e = (value) => String(value ?? "").replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
9
9
|
export const RETENTION_CSS = `.retention{max-width:720px;min-width:0}.retention-next{margin:0 0 16px}` +
|
|
10
10
|
`.retention form{display:grid;gap:0}.retention .kind{display:grid;grid-template-columns:minmax(0,1fr) 11rem;gap:4px 16px;align-items:center;padding:12px 0;border-bottom:1px solid var(--border)}` +
|
|
@@ -26,8 +26,8 @@ export function retentionHtml(view, notice) {
|
|
|
26
26
|
const note = notice.problem ? `<p class="problem" role="alert">${e(notice.problem)}</p>` : notice.said ? `<p role="status">${e(notice.said)}</p>` : "";
|
|
27
27
|
const rows = RETENTION_KINDS.map(({ kind, label, detail }) => {
|
|
28
28
|
const current = view.periods[kind];
|
|
29
|
-
const
|
|
30
|
-
const options =
|
|
29
|
+
const chosen = view.chosen.includes(kind);
|
|
30
|
+
const options = periodChoices(kind, current).map(days => `<option value="${days === null ? "forever" : days}"${days === current ? " selected" : ""}>${e(periodLabel(kind, days, chosen || days !== current))}</option>`).join("");
|
|
31
31
|
return `<div class="kind" data-kind="${kind}"><label class="name" for="keep-${kind}">${e(label)}</label><p class="meta">${e(detail)}</p><select id="keep-${kind}" name="${kind}">${options}</select></div>`;
|
|
32
32
|
}).join("");
|
|
33
33
|
const last = view.lastSweep === null ? "" : ` Last sweep ${e(view.lastSweep.slice(0, 10))}.`;
|
|
@@ -35,5 +35,5 @@ export function retentionHtml(view, notice) {
|
|
|
35
35
|
`<form method="post" action="/settings/retention"><input type="hidden" name="csrf" value="${e(view.csrf)}">${rows}` +
|
|
36
36
|
`<label class="step-up">Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>` +
|
|
37
37
|
`<button type="submit">Save</button></form>` +
|
|
38
|
-
`<p class="meta">Never removed: the action ledger, and anything a task still needs (unfinished tasks, results
|
|
38
|
+
`<p class="meta">Never removed: the action ledger, and anything a task still needs (unfinished tasks, results Ready for review, anything on hold).</p></article>`;
|
|
39
39
|
}
|
package/dist/retention.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import type { Store } from "./store.js";
|
|
2
|
-
export declare const RETENTION_SCHEMA
|
|
1
|
+
import type { Database, Store } from "./store.js";
|
|
2
|
+
export declare const RETENTION_SCHEMA: string;
|
|
3
|
+
/** A file from before 1-day evidence keeps `days >= 7` in its CHECK: rebuild the table with the wider one, keeping
|
|
4
|
+
* every row, in one transaction. A file already widened is untouched. */
|
|
5
|
+
export declare function widenRetentionSchema(db: Database): void;
|
|
3
6
|
export type RetentionKind = "evidence" | "checkouts" | "chat" | "notifications";
|
|
4
7
|
export type RetentionPeriods = Record<RetentionKind, number | null>;
|
|
5
8
|
export declare const RETENTION_KINDS: readonly {
|
|
@@ -7,19 +10,23 @@ export declare const RETENTION_KINDS: readonly {
|
|
|
7
10
|
label: string;
|
|
8
11
|
detail: string;
|
|
9
12
|
}[];
|
|
10
|
-
|
|
11
|
-
export declare const
|
|
12
|
-
export declare const MIN_DAYS = 7;
|
|
13
|
+
export declare const PERIOD_CHOICES: Readonly<Record<RetentionKind, readonly (number | null)[]>>;
|
|
14
|
+
export declare const MIN_DAYS = 1;
|
|
13
15
|
export declare const MAX_DAYS = 3650;
|
|
14
16
|
/** A sweep is due once a day. */
|
|
15
17
|
export declare const SWEEP_EVERY_MS = 86400000;
|
|
16
18
|
/** At most this many runs, messages, notifications or records go per kind per sweep; the next sweep takes the rest. */
|
|
17
19
|
export declare const SWEEP_BATCH = 2000;
|
|
18
|
-
|
|
20
|
+
/** What each kind keeps until someone chooses: evidence 28 days, everything else forever. */
|
|
21
|
+
export declare const DEFAULT_PERIODS: RetentionPeriods;
|
|
19
22
|
export declare function isRetentionKind(value: string): value is RetentionKind;
|
|
20
23
|
/** "forever", or a number of days ("90", "90d"), weeks ("12w") or years ("1y"); undefined when it's none of those or out of range. */
|
|
21
24
|
export declare function parsePeriod(text: string): number | null | undefined;
|
|
22
25
|
export declare function periodWords(days: number | null): string;
|
|
26
|
+
/** What the page offers for `kind`, with `current` among them (in order) when it was set some other way. */
|
|
27
|
+
export declare function periodChoices(kind: RetentionKind, current: number | null): readonly (number | null)[];
|
|
28
|
+
/** A period as the page and `retention show` say it: "28 days (default)" when nobody chose, "30 days (custom)" when it's not one the page offers. */
|
|
29
|
+
export declare function periodLabel(kind: RetentionKind, days: number | null, chosen: boolean): string;
|
|
23
30
|
export type RetentionCount = {
|
|
24
31
|
kind: RetentionKind;
|
|
25
32
|
days: number | null;
|
package/dist/retention.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Retention (v105): how long Toolroll keeps run evidence and logs,
|
|
3
|
-
* finished checkouts' records, chat messages and notifications.
|
|
4
|
-
*
|
|
5
|
-
* a daily sweep
|
|
6
|
-
* ledger entry saying what went and about how much
|
|
3
|
+
* finished checkouts' records, chat messages and notifications. Until an
|
|
4
|
+
* instance operator chooses a period, evidence is kept 28 days and every
|
|
5
|
+
* other kind forever; a daily sweep deletes what is older than its period
|
|
6
|
+
* and writes one action ledger entry saying what went and about how much
|
|
7
|
+
* space it freed.
|
|
7
8
|
*
|
|
8
9
|
* Never deleted, whatever the settings: the action ledger (it is append-only),
|
|
9
10
|
* and anything a task still needs. A task still needs its things until it is
|
|
@@ -21,24 +22,47 @@ import { join } from "node:path";
|
|
|
21
22
|
import { RETENTION_NOTE } from "./evidence.js";
|
|
22
23
|
import { COMPLETION_ACTION } from "./result-completion.js";
|
|
23
24
|
import { bytesWords, treeBytes } from "./storage.js";
|
|
24
|
-
|
|
25
|
-
CREATE TABLE IF NOT EXISTS
|
|
25
|
+
const RETENTION_TABLE = (name) => `
|
|
26
|
+
CREATE TABLE IF NOT EXISTS ${name} (
|
|
26
27
|
kind TEXT PRIMARY KEY CHECK (kind IN ('evidence', 'checkouts', 'chat', 'notifications')),
|
|
27
28
|
-- NULL is forever; a row only exists once someone chose.
|
|
28
|
-
days INTEGER CHECK (days IS NULL OR (days >=
|
|
29
|
+
days INTEGER CHECK (days IS NULL OR (days >= 1 AND days <= 3650)),
|
|
29
30
|
updated_by TEXT NOT NULL,
|
|
30
31
|
updated_at TEXT NOT NULL
|
|
31
32
|
);
|
|
32
33
|
`;
|
|
34
|
+
export const RETENTION_SCHEMA = RETENTION_TABLE("retention_setting");
|
|
35
|
+
/** A file from before 1-day evidence keeps `days >= 7` in its CHECK: rebuild the table with the wider one, keeping
|
|
36
|
+
* every row, in one transaction. A file already widened is untouched. */
|
|
37
|
+
export function widenRetentionSchema(db) {
|
|
38
|
+
const row = db.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'retention_setting'").get();
|
|
39
|
+
if (row === undefined || !/days\s*>=\s*7\b/.test(String(row["sql"])))
|
|
40
|
+
return;
|
|
41
|
+
db.exec("BEGIN IMMEDIATE");
|
|
42
|
+
try {
|
|
43
|
+
db.exec(RETENTION_TABLE("retention_setting_next"));
|
|
44
|
+
db.exec("INSERT INTO retention_setting_next (kind, days, updated_by, updated_at) SELECT kind, days, updated_by, updated_at FROM retention_setting");
|
|
45
|
+
db.exec("DROP TABLE retention_setting");
|
|
46
|
+
db.exec("ALTER TABLE retention_setting_next RENAME TO retention_setting");
|
|
47
|
+
db.exec("COMMIT");
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
db.exec("ROLLBACK");
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
33
54
|
export const RETENTION_KINDS = [
|
|
34
55
|
{ kind: "evidence", label: "Run evidence and logs", detail: "Diffs, check logs, screenshots and reports saved for each run" },
|
|
35
56
|
{ kind: "checkouts", label: "Finished checkout records", detail: "Records of build checkouts that were let go and are no longer on disk" },
|
|
36
57
|
{ kind: "chat", label: "Chat messages", detail: "Messages in lead and team chats" },
|
|
37
58
|
{ kind: "notifications", label: "Notifications", detail: "Delivered or dismissed notifications" },
|
|
38
59
|
];
|
|
39
|
-
/** The periods the page offers; the command line takes any number of days from
|
|
40
|
-
|
|
41
|
-
export const
|
|
60
|
+
/** The periods the page offers for each kind; the command line takes any number of days from 1 to 3650. */
|
|
61
|
+
const LONG_CHOICES = [30, 90, 180, 365, 730, null];
|
|
62
|
+
export const PERIOD_CHOICES = {
|
|
63
|
+
evidence: [1, 7, 14, 28, null], checkouts: LONG_CHOICES, chat: LONG_CHOICES, notifications: LONG_CHOICES,
|
|
64
|
+
};
|
|
65
|
+
export const MIN_DAYS = 1;
|
|
42
66
|
export const MAX_DAYS = 3650;
|
|
43
67
|
const DAY_MS = 86_400_000;
|
|
44
68
|
/** A sweep is due once a day. */
|
|
@@ -46,7 +70,8 @@ export const SWEEP_EVERY_MS = DAY_MS;
|
|
|
46
70
|
/** At most this many runs, messages, notifications or records go per kind per sweep; the next sweep takes the rest. */
|
|
47
71
|
export const SWEEP_BATCH = 2000;
|
|
48
72
|
const SWEEP_CURSOR = "retention:last-sweep";
|
|
49
|
-
|
|
73
|
+
/** What each kind keeps until someone chooses: evidence 28 days, everything else forever. */
|
|
74
|
+
export const DEFAULT_PERIODS = { evidence: 28, checkouts: null, chat: null, notifications: null };
|
|
50
75
|
export function isRetentionKind(value) {
|
|
51
76
|
return RETENTION_KINDS.some(one => one.kind === value);
|
|
52
77
|
}
|
|
@@ -67,7 +92,20 @@ export function periodWords(days) {
|
|
|
67
92
|
return "forever";
|
|
68
93
|
if (days % 365 === 0)
|
|
69
94
|
return days === 365 ? "1 year" : `${days / 365} years`;
|
|
70
|
-
return `${days} days`;
|
|
95
|
+
return days === 1 ? "1 day" : `${days} days`;
|
|
96
|
+
}
|
|
97
|
+
/** What the page offers for `kind`, with `current` among them (in order) when it was set some other way. */
|
|
98
|
+
export function periodChoices(kind, current) {
|
|
99
|
+
const offered = PERIOD_CHOICES[kind];
|
|
100
|
+
return offered.includes(current) ? offered : [...offered, current].sort((a, b) => (a ?? Infinity) - (b ?? Infinity));
|
|
101
|
+
}
|
|
102
|
+
/** A period as the page and `retention show` say it: "28 days (default)" when nobody chose, "30 days (custom)" when it's not one the page offers. */
|
|
103
|
+
export function periodLabel(kind, days, chosen) {
|
|
104
|
+
const words = periodWords(days);
|
|
105
|
+
const label = `${words[0].toUpperCase()}${words.slice(1)}`;
|
|
106
|
+
if (!chosen)
|
|
107
|
+
return `${label} (default)`;
|
|
108
|
+
return PERIOD_CHOICES[kind].includes(days) ? label : `${label} (custom)`;
|
|
71
109
|
}
|
|
72
110
|
/** Tasks that no longer need anything kept: cancelled, or done with their result marked complete; not on hold, not
|
|
73
111
|
* running, not a release candidate, and no revision below it (at any depth) still unfinished — cancelled or completed,
|
package/dist/review-context.d.ts
CHANGED
|
@@ -121,6 +121,9 @@ export type CriterionContextCoverage = {
|
|
|
121
121
|
items: string[];
|
|
122
122
|
gaps: string[];
|
|
123
123
|
priorSupport: "eligible" | "invalid" | "none";
|
|
124
|
+
/** One line for the change's images and other binary files, which are
|
|
125
|
+
* never read as text and so are neither items nor gaps. */
|
|
126
|
+
assets?: string;
|
|
124
127
|
};
|
|
125
128
|
export type ReviewContextInventory = {
|
|
126
129
|
schema: 1 | 2 | 3;
|
package/dist/review-context.js
CHANGED
|
@@ -43,7 +43,7 @@ import { isVerificationReceipt } from "./verification-evidence.js";
|
|
|
43
43
|
*/
|
|
44
44
|
import { createHash } from "node:crypto";
|
|
45
45
|
import { hasForbiddenControls } from "./decision.js";
|
|
46
|
-
import { EVIDENCE_CAPS, readVerifiedArtifact, redactSecretLines, scanForSecrets, storeEvidence } from "./evidence.js";
|
|
46
|
+
import { EVIDENCE_CAPS, isBinaryAssetPath, isImagePath, readVerifiedArtifact, redactSecretLines, scanForSecrets, storeEvidence } from "./evidence.js";
|
|
47
47
|
import { parseProof } from "./proof.js";
|
|
48
48
|
/** Explicit bounds. Content bytes are UTF-8 of the stored (possibly
|
|
49
49
|
* redacted) text; the aggregate is the sum over every stored item. */
|
|
@@ -420,7 +420,8 @@ export function parseReviewContext(raw) {
|
|
|
420
420
|
const c = one;
|
|
421
421
|
if (!str(c["id"], 64) || coverageIds.has(c["id"]) || (c["state"] !== "patch" && c["state"] !== "context" && c["state"] !== "gap") ||
|
|
422
422
|
typeof c["inherited"] !== "boolean" || !strList(c["paths"], 200) || !strList(c["items"], r["schema"] === 3 ? contentLimits.items : 64) || !strList(c["gaps"], r["schema"] === 3 ? 400 : 64) ||
|
|
423
|
-
(c["priorSupport"] !== "eligible" && c["priorSupport"] !== "invalid" && c["priorSupport"] !== "none")
|
|
423
|
+
(c["priorSupport"] !== "eligible" && c["priorSupport"] !== "invalid" && c["priorSupport"] !== "none") ||
|
|
424
|
+
(c["assets"] !== undefined && !str(c["assets"], 300))) {
|
|
424
425
|
return { ok: false, problem: `coverage ${index}: malformed` };
|
|
425
426
|
}
|
|
426
427
|
if (c["items"].some(id => !ids.has(id) || !items.find(item => item.id === id)?.criteria.includes(String(c["id"]))))
|
|
@@ -431,6 +432,7 @@ export function parseReviewContext(raw) {
|
|
|
431
432
|
coverage.push({
|
|
432
433
|
id: c["id"], state: c["state"], inherited: c["inherited"], paths: [...c["paths"]],
|
|
433
434
|
items: [...c["items"]], gaps: [...c["gaps"]], priorSupport: c["priorSupport"],
|
|
435
|
+
...(c["assets"] === undefined ? {} : { assets: c["assets"] }),
|
|
434
436
|
});
|
|
435
437
|
}
|
|
436
438
|
return {
|
|
@@ -499,6 +501,32 @@ async function treeEntries(git, cwd, revision, paths) {
|
|
|
499
501
|
}
|
|
500
502
|
return entries;
|
|
501
503
|
}
|
|
504
|
+
/** "12 images, 3.4 MB, in the run's evidence": every binary file at the head
|
|
505
|
+
* in one line. Files deleted by the change are not counted. */
|
|
506
|
+
async function assetSummary(git, cwd, head, assets) {
|
|
507
|
+
const sizes = new Map();
|
|
508
|
+
const paths = [...assets].filter(safeContextPath);
|
|
509
|
+
for (let at = 0; at < paths.length; at += 500) {
|
|
510
|
+
const listed = await treeEntries(git, cwd, head, paths.slice(at, at + 500));
|
|
511
|
+
for (const [path, entry] of listed ?? [])
|
|
512
|
+
if (regular(entry))
|
|
513
|
+
sizes.set(path, entry.size);
|
|
514
|
+
}
|
|
515
|
+
if (sizes.size === 0)
|
|
516
|
+
return null;
|
|
517
|
+
const images = [...sizes.keys()].filter(isImagePath).length;
|
|
518
|
+
const others = sizes.size - images;
|
|
519
|
+
const plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
520
|
+
const counted = [...(images > 0 ? [plural(images, "image")] : []), ...(others > 0 ? [plural(others, images > 0 ? "other binary file" : "binary file")] : [])].join(", ");
|
|
521
|
+
return `${counted}, ${assetBytesWords([...sizes.values()].reduce((sum, one) => sum + one, 0))}, in the run's evidence`;
|
|
522
|
+
}
|
|
523
|
+
function assetBytesWords(bytes) {
|
|
524
|
+
if (bytes >= 1024 * 1024)
|
|
525
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
526
|
+
if (bytes >= 1024)
|
|
527
|
+
return `${Math.round(bytes / 1024)} KB`;
|
|
528
|
+
return `${bytes} bytes`;
|
|
529
|
+
}
|
|
502
530
|
function regular(entry) {
|
|
503
531
|
return entry !== undefined && entry.type === "blob" && ["100644", "100755"].includes(entry.mode) && SHA1.test(entry.object) && Number.isSafeInteger(entry.size) && entry.size >= 0;
|
|
504
532
|
}
|
|
@@ -822,11 +850,22 @@ export async function deriveReviewContext(store, git, args, lineage) {
|
|
|
822
850
|
gapAll("capture-failed", "the committed source paths in the approved scope could not be listed");
|
|
823
851
|
}
|
|
824
852
|
}
|
|
853
|
+
// Files named as images or other binaries are never text context: they are
|
|
854
|
+
// counted in one summary line, never listed as gaps or held against the
|
|
855
|
+
// bounds. A text-named file that turns out not to be text stays a gap.
|
|
856
|
+
const assets = new Set();
|
|
857
|
+
const textOnly = (paths) => paths.filter(path => {
|
|
858
|
+
if (!isBinaryAssetPath(path))
|
|
859
|
+
return true;
|
|
860
|
+
assets.add(path);
|
|
861
|
+
return false;
|
|
862
|
+
});
|
|
863
|
+
generalPaths.splice(0, generalPaths.length, ...textOnly(generalPaths));
|
|
825
864
|
const specificFor = new Map();
|
|
826
865
|
for (const criterion of sourceProof?.criteria ?? []) {
|
|
827
866
|
const cited = [...new Set(criterion.evidence.filter(one => one.kind === "changed-path").map(one => one.ref))].filter(safeContextPath).sort();
|
|
828
867
|
const inherited = parentContext?.coverage.find(one => one.id === criterion.id)?.paths ?? [];
|
|
829
|
-
const combined = [...new Set([...cited, ...inherited])].filter(safeContextPath).sort();
|
|
868
|
+
const combined = textOnly([...new Set([...cited, ...inherited])].filter(safeContextPath).sort());
|
|
830
869
|
if (combined.length > 0)
|
|
831
870
|
specificFor.set(criterion.id, combined);
|
|
832
871
|
}
|
|
@@ -852,7 +891,7 @@ export async function deriveReviewContext(store, git, args, lineage) {
|
|
|
852
891
|
// A proof's path citations are relevance hints, not permission to omit other
|
|
853
892
|
// changed files. Deliver new revision files and tests in full as supplemental
|
|
854
893
|
// context without inventing criterion ownership or inheriting a judgement.
|
|
855
|
-
for (const path of args.patchPaths) {
|
|
894
|
+
for (const path of textOnly([...args.patchPaths])) {
|
|
856
895
|
if (safeContextPath(path) && !relevance.has(path))
|
|
857
896
|
relevance.set(path, { criteria: new Set(), why: new Set(["candidate-changed-path"]) });
|
|
858
897
|
}
|
|
@@ -965,6 +1004,7 @@ export async function deriveReviewContext(store, git, args, lineage) {
|
|
|
965
1004
|
}
|
|
966
1005
|
}
|
|
967
1006
|
inventory.items = [...itemsByPath.values()];
|
|
1007
|
+
const assetLine = await assetSummary(git, args.worktree, args.head, assets);
|
|
968
1008
|
// 6. Prior review provenance — context, proved per criterion, never a verdict.
|
|
969
1009
|
const sourceDiff = sourceArtifacts.find(one => one.kind === "terminal-diff") ?? null;
|
|
970
1010
|
const sourceProofArtifact = sourceArtifacts.find(one => one.kind === "proof") ?? null;
|
|
@@ -1039,5 +1079,5 @@ export async function deriveReviewContext(store, git, args, lineage) {
|
|
|
1039
1079
|
const gapsFor = state === "gap" && relevantGaps.length === 0 ? [plan.paths.length === 0 ? "no relevant source paths are known" : "inherited context is incomplete"] : relevantGaps;
|
|
1040
1080
|
return { id: plan.id, state, inherited: plan.inherited && lineage.sourceRun !== args.runId, paths: plan.paths, items, gaps: gapsFor, priorSupport };
|
|
1041
1081
|
});
|
|
1042
|
-
return finish(coverage);
|
|
1082
|
+
return finish(assetLine === null ? coverage : coverage.map(one => ({ ...one, assets: assetLine })));
|
|
1043
1083
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** The per-project automatic review switch and the one review each finished
|
|
2
|
+
* build gets (build-review.ts runs it): tables, and the read-only views every
|
|
3
|
+
* surface shows. No schema version bump. Kept free of heavy imports so the
|
|
4
|
+
* status readers can use it without an import cycle. */
|
|
5
|
+
import type { Store } from "./store.js";
|
|
6
|
+
export type FindingSeverity = "HIGH" | "MEDIUM" | "LOW";
|
|
7
|
+
/** One automatic-review finding: how bad, where, and how it fails. */
|
|
8
|
+
export type BuildFinding = {
|
|
9
|
+
severity: FindingSeverity;
|
|
10
|
+
file: string;
|
|
11
|
+
line: number;
|
|
12
|
+
scenario: string;
|
|
13
|
+
};
|
|
14
|
+
export declare const REVIEW_SCHEMA = "\n-- A project's explicit review switch. No row: on while the project is under a hands-off mode, off otherwise.\nCREATE TABLE IF NOT EXISTS review_switch (\n repo TEXT PRIMARY KEY,\n enabled INTEGER NOT NULL CHECK (enabled IN (0, 1)),\n changed_by TEXT NOT NULL,\n changed_at TEXT NOT NULL\n);\n-- One row per reviewed build run: pending until its one reviewer run settles it.\nCREATE TABLE IF NOT EXISTS build_review (\n run INTEGER PRIMARY KEY,\n task_id TEXT NOT NULL,\n repo TEXT NOT NULL,\n state TEXT NOT NULL CHECK (state IN ('pending', 'reviewed', 'not-reviewed')),\n request INTEGER,\n reviewer_run INTEGER,\n findings_json TEXT,\n reason TEXT,\n sent_back_as TEXT,\n queued_at TEXT NOT NULL,\n finished_at TEXT\n);\nCREATE INDEX IF NOT EXISTS build_review_pending ON build_review (repo) WHERE state = 'pending';\nCREATE INDEX IF NOT EXISTS build_review_sent_back ON build_review (sent_back_as) WHERE sent_back_as IS NOT NULL;\n";
|
|
15
|
+
export type ReviewSwitch = {
|
|
16
|
+
on: boolean;
|
|
17
|
+
source: "project" | "hands-off" | "default";
|
|
18
|
+
changedBy: string | null;
|
|
19
|
+
changedAt: string | null;
|
|
20
|
+
};
|
|
21
|
+
/** How the policy log words a switch state. */
|
|
22
|
+
export declare function reviewSwitchWords(state: ReviewSwitch): string;
|
|
23
|
+
export type BuildReviewView = {
|
|
24
|
+
run: number;
|
|
25
|
+
state: "pending" | "reviewed" | "not-reviewed";
|
|
26
|
+
reviewerRun: number | null;
|
|
27
|
+
/** Why it was not reviewed, in words. */
|
|
28
|
+
reason: string | null;
|
|
29
|
+
/** HIGH findings; they sent the task back when `sentBackAs` is set. */
|
|
30
|
+
high: BuildFinding[];
|
|
31
|
+
/** MEDIUM and LOW findings: suggested follow-ups, never blocking. */
|
|
32
|
+
followUps: BuildFinding[];
|
|
33
|
+
sentBackAs: string | null;
|
|
34
|
+
finishedAt: string | null;
|
|
35
|
+
};
|
|
36
|
+
export declare function buildReviewOf(store: Pick<Store, "handle">, runId: number): BuildReviewView | null;
|
|
37
|
+
export declare const findingWords: (finding: BuildFinding) => string;
|
|
38
|
+
/** The review's standing in one line, or null while nothing was asked. */
|
|
39
|
+
export declare function buildReviewHeadline(view: BuildReviewView | null): string | null;
|
|
40
|
+
/** Plain lines for `task show` and `assignment show`. */
|
|
41
|
+
export declare function buildReviewLines(view: BuildReviewView | null): string[];
|