@cleocode/core 2026.10.25 → 2026.10.26-rc.ci.697
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/dist/build-provenance.json +2 -2
- package/dist/dispatch/contracts/output-contracts.d.ts +13 -0
- package/dist/dispatch/contracts/output-contracts.d.ts.map +1 -1
- package/dist/dispatch/contracts/output-contracts.js +29 -0
- package/dist/dispatch/contracts/output-contracts.js.map +1 -1
- package/dist/docs/docs-ops.d.ts +8 -0
- package/dist/docs/docs-ops.d.ts.map +1 -1
- package/dist/docs/docs-ops.js +24 -1
- package/dist/docs/docs-ops.js.map +1 -1
- package/dist/docs/docs-update.d.ts +8 -5
- package/dist/docs/docs-update.d.ts.map +1 -1
- package/dist/docs/docs-update.js +16 -5
- package/dist/docs/docs-update.js.map +1 -1
- package/dist/gateway-client/generated/types.gen.d.ts +27 -0
- package/dist/gateway-client/generated/types.gen.d.ts.map +1 -1
- package/dist/hooks/handlers/watchdog-hooks.d.ts.map +1 -1
- package/dist/hooks/handlers/watchdog-hooks.js +5 -0
- package/dist/hooks/handlers/watchdog-hooks.js.map +1 -1
- package/dist/internal.d.ts +3 -2
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +5 -2
- package/dist/internal.js.map +1 -1
- package/dist/lifecycle/verification/satisfies-validator.d.ts.map +1 -1
- package/dist/lifecycle/verification/satisfies-validator.js +22 -10
- package/dist/lifecycle/verification/satisfies-validator.js.map +1 -1
- package/dist/memory/brain-writer-thread.d.ts +30 -0
- package/dist/memory/brain-writer-thread.d.ts.map +1 -1
- package/dist/memory/brain-writer-thread.js +46 -1
- package/dist/memory/brain-writer-thread.js.map +1 -1
- package/dist/memory/brain-writer-worker.js +13 -2
- package/dist/memory/brain-writer-worker.js.map +1 -1
- package/dist/memory/embedding-backfill.d.ts +12 -0
- package/dist/memory/embedding-backfill.d.ts.map +1 -1
- package/dist/memory/embedding-backfill.js +51 -31
- package/dist/memory/embedding-backfill.js.map +1 -1
- package/dist/orchestrate/include-deps.d.ts +58 -0
- package/dist/orchestrate/include-deps.d.ts.map +1 -0
- package/dist/orchestrate/include-deps.js +138 -0
- package/dist/orchestrate/include-deps.js.map +1 -0
- package/dist/orchestrate/spawn-ops.d.ts +14 -33
- package/dist/orchestrate/spawn-ops.d.ts.map +1 -1
- package/dist/orchestrate/spawn-ops.js +49 -61
- package/dist/orchestrate/spawn-ops.js.map +1 -1
- package/dist/orchestrate/worktree-complete.d.ts +15 -1
- package/dist/orchestrate/worktree-complete.d.ts.map +1 -1
- package/dist/orchestrate/worktree-complete.js +36 -3
- package/dist/orchestrate/worktree-complete.js.map +1 -1
- package/dist/release/ci-evidence.d.ts +142 -4
- package/dist/release/ci-evidence.d.ts.map +1 -1
- package/dist/release/ci-evidence.js +298 -48
- package/dist/release/ci-evidence.js.map +1 -1
- package/dist/release/open.d.ts +25 -1
- package/dist/release/open.d.ts.map +1 -1
- package/dist/release/open.js +63 -11
- package/dist/release/open.js.map +1 -1
- package/dist/resources/admission-ledger.d.ts +83 -11
- package/dist/resources/admission-ledger.d.ts.map +1 -1
- package/dist/resources/admission-ledger.js +138 -39
- package/dist/resources/admission-ledger.js.map +1 -1
- package/dist/resources/governor.js +1 -1
- package/dist/resources/governor.js.map +1 -1
- package/dist/resources/hook-footprint.d.ts +24 -0
- package/dist/resources/hook-footprint.d.ts.map +1 -1
- package/dist/resources/hook-footprint.js +38 -1
- package/dist/resources/hook-footprint.js.map +1 -1
- package/dist/resources/run-class.d.ts +38 -5
- package/dist/resources/run-class.d.ts.map +1 -1
- package/dist/resources/run-class.js +60 -11
- package/dist/resources/run-class.js.map +1 -1
- package/dist/resources/run-governed.d.ts +13 -3
- package/dist/resources/run-governed.d.ts.map +1 -1
- package/dist/resources/run-governed.js +42 -9
- package/dist/resources/run-governed.js.map +1 -1
- package/dist/resources/slot-holder.d.ts +3 -3
- package/dist/resources/slot-holder.js +3 -3
- package/dist/resources/tool-groups.d.ts +11 -7
- package/dist/resources/tool-groups.d.ts.map +1 -1
- package/dist/resources/tool-groups.js +24 -13
- package/dist/resources/tool-groups.js.map +1 -1
- package/dist/spawn/branch-integration.d.ts +281 -0
- package/dist/spawn/branch-integration.d.ts.map +1 -0
- package/dist/spawn/branch-integration.js +715 -0
- package/dist/spawn/branch-integration.js.map +1 -0
- package/dist/spawn/branch-lock.d.ts +32 -270
- package/dist/spawn/branch-lock.d.ts.map +1 -1
- package/dist/spawn/branch-lock.js +59 -770
- package/dist/spawn/branch-lock.js.map +1 -1
- package/dist/spawn/index.d.ts +3 -1
- package/dist/spawn/index.d.ts.map +1 -1
- package/dist/spawn/index.js +5 -1
- package/dist/spawn/index.js.map +1 -1
- package/dist/store/exodus/recovery.d.ts.map +1 -1
- package/dist/store/exodus/recovery.js +9 -4
- package/dist/store/exodus/recovery.js.map +1 -1
- package/dist/system/health.d.ts.map +1 -1
- package/dist/system/health.js +3 -1
- package/dist/system/health.js.map +1 -1
- package/dist/tasks/ac-coverage-gate.d.ts +18 -2
- package/dist/tasks/ac-coverage-gate.d.ts.map +1 -1
- package/dist/tasks/ac-coverage-gate.js +40 -6
- package/dist/tasks/ac-coverage-gate.js.map +1 -1
- package/dist/tasks/ac-table.d.ts.map +1 -1
- package/dist/tasks/ac-table.js +3 -10
- package/dist/tasks/ac-table.js.map +1 -1
- package/dist/tasks/affected-packages.d.ts.map +1 -1
- package/dist/tasks/affected-packages.js +54 -10
- package/dist/tasks/affected-packages.js.map +1 -1
- package/dist/tasks/affected-scope.d.ts +10 -1
- package/dist/tasks/affected-scope.d.ts.map +1 -1
- package/dist/tasks/affected-scope.js +13 -3
- package/dist/tasks/affected-scope.js.map +1 -1
- package/dist/tasks/change-set.d.ts +9 -6
- package/dist/tasks/change-set.d.ts.map +1 -1
- package/dist/tasks/change-set.js +75 -97
- package/dist/tasks/change-set.js.map +1 -1
- package/dist/tasks/command-line.d.ts.map +1 -1
- package/dist/tasks/command-line.js +6 -2
- package/dist/tasks/command-line.js.map +1 -1
- package/dist/tasks/complete.d.ts.map +1 -1
- package/dist/tasks/complete.js +7 -2
- package/dist/tasks/complete.js.map +1 -1
- package/dist/tasks/done-plan.d.ts.map +1 -1
- package/dist/tasks/done-plan.js +111 -29
- package/dist/tasks/done-plan.js.map +1 -1
- package/dist/tasks/done-record.d.ts +14 -0
- package/dist/tasks/done-record.d.ts.map +1 -1
- package/dist/tasks/done-record.js +17 -10
- package/dist/tasks/done-record.js.map +1 -1
- package/dist/tasks/evidence-tree.d.ts +33 -0
- package/dist/tasks/evidence-tree.d.ts.map +1 -0
- package/dist/tasks/evidence-tree.js +132 -0
- package/dist/tasks/evidence-tree.js.map +1 -0
- package/dist/tasks/evidence.d.ts +42 -6
- package/dist/tasks/evidence.d.ts.map +1 -1
- package/dist/tasks/evidence.js +175 -79
- package/dist/tasks/evidence.js.map +1 -1
- package/dist/tasks/gate-preview.d.ts.map +1 -1
- package/dist/tasks/gate-preview.js +3 -2
- package/dist/tasks/gate-preview.js.map +1 -1
- package/dist/tasks/gate-runner.d.ts +11 -3
- package/dist/tasks/gate-runner.d.ts.map +1 -1
- package/dist/tasks/gate-runner.js +156 -37
- package/dist/tasks/gate-runner.js.map +1 -1
- package/dist/tasks/gate-staleness.d.ts +58 -0
- package/dist/tasks/gate-staleness.d.ts.map +1 -0
- package/dist/tasks/gate-staleness.js +78 -0
- package/dist/tasks/gate-staleness.js.map +1 -0
- package/dist/tasks/heavy-tool-env.d.ts +26 -1
- package/dist/tasks/heavy-tool-env.d.ts.map +1 -1
- package/dist/tasks/heavy-tool-env.js +92 -5
- package/dist/tasks/heavy-tool-env.js.map +1 -1
- package/dist/tasks/heavy-tool-limit.d.ts +24 -0
- package/dist/tasks/heavy-tool-limit.d.ts.map +1 -1
- package/dist/tasks/heavy-tool-limit.js +44 -1
- package/dist/tasks/heavy-tool-limit.js.map +1 -1
- package/dist/tasks/show.d.ts.map +1 -1
- package/dist/tasks/show.js +43 -0
- package/dist/tasks/show.js.map +1 -1
- package/dist/tasks/test-run-binding.d.ts +48 -3
- package/dist/tasks/test-run-binding.d.ts.map +1 -1
- package/dist/tasks/test-run-binding.js +111 -3
- package/dist/tasks/test-run-binding.js.map +1 -1
- package/dist/tasks/tool-cache.d.ts +58 -7
- package/dist/tasks/tool-cache.d.ts.map +1 -1
- package/dist/tasks/tool-cache.js +216 -16
- package/dist/tasks/tool-cache.js.map +1 -1
- package/dist/tasks/tool-resolver.d.ts +13 -0
- package/dist/tasks/tool-resolver.d.ts.map +1 -1
- package/dist/tasks/tool-resolver.js +34 -0
- package/dist/tasks/tool-resolver.js.map +1 -1
- package/dist/tasks/tool-runner-guard.d.ts +6 -0
- package/dist/tasks/tool-runner-guard.d.ts.map +1 -1
- package/dist/tasks/tool-runner-guard.js.map +1 -1
- package/dist/tasks/tool-semaphore.js +1 -1
- package/dist/tasks/tool-semaphore.js.map +1 -1
- package/dist/tasks/verification-policy.d.ts +26 -3
- package/dist/tasks/verification-policy.d.ts.map +1 -1
- package/dist/tasks/verification-policy.js +76 -11
- package/dist/tasks/verification-policy.js.map +1 -1
- package/dist/validation/doctor/checks.d.ts +12 -0
- package/dist/validation/doctor/checks.d.ts.map +1 -1
- package/dist/validation/doctor/checks.js +33 -0
- package/dist/validation/doctor/checks.js.map +1 -1
- package/dist/validation/engine-ops.d.ts +6 -0
- package/dist/validation/engine-ops.d.ts.map +1 -1
- package/dist/validation/engine-ops.js +51 -1
- package/dist/validation/engine-ops.js.map +1 -1
- package/dist/worktree/force-unlock.js +2 -2
- package/dist/worktree/force-unlock.js.map +1 -1
- package/dist/worktree/list.d.ts +7 -7
- package/dist/worktree/list.d.ts.map +1 -1
- package/dist/worktree/list.js +11 -56
- package/dist/worktree/list.js.map +1 -1
- package/dist/worktree/prune.d.ts +2 -2
- package/dist/worktree/prune.d.ts.map +1 -1
- package/dist/worktree/prune.js +4 -4
- package/dist/worktree/prune.js.map +1 -1
- package/package.json +13 -13
package/dist/tasks/evidence.js
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* - {@link resolveToolCommand} maps `tool:<name>` to a runnable command
|
|
11
11
|
* using `.cleo/project-context.json` and per-`primaryType` fallbacks.
|
|
12
12
|
* - {@link runToolCached} memoises results per
|
|
13
|
-
* `(canonical, cmd, args, treeHash, envFingerprint)`
|
|
13
|
+
* `(canonical, cmd, args, treeHash, envFingerprint, resourceEnv)` (the
|
|
14
|
+
* heap and worker limits the run gets, T12989) and serialises
|
|
14
15
|
* concurrent identical runs via a cross-process lock,
|
|
15
16
|
* preventing the resource thrash observed when multiple `cleo verify`
|
|
16
17
|
* invocations spawned full toolchains in parallel.
|
|
@@ -36,11 +37,11 @@ import { HEAVY_HEAP_ENV, HEAVY_WORKERS_ENV, isHeavyTool } from './heavy-tool-env
|
|
|
36
37
|
import { DISABLE_ENV, describeMemoryLimit } from './heavy-tool-limit.js';
|
|
37
38
|
import { qaRunUnionGapReason, typecheckCoverageGap } from './qa-run-binding.js';
|
|
38
39
|
import { computeCommitRevalidationKey, readCommitRevalidationEntry, writeCommitRevalidationEntry, } from './revalidation-cache.js';
|
|
39
|
-
import { bindTestRunReport, configSchemaCompositionGap, coveredTestFiles, TEST_RUN_MAX_RECORDED_FILES, } from './test-run-binding.js';
|
|
40
|
+
import { bindTestRunReport, configSchemaCompositionGap, coveredTestFiles, playwrightTestRun, TEST_RUN_MAX_RECORDED_FILES, } from './test-run-binding.js';
|
|
40
41
|
import { captureHead, captureTree, resolveSpawnTimeoutMs, runToolCached, } from './tool-cache.js';
|
|
41
42
|
import { CANONICAL_TOOLS, canonicalToolName, listValidToolNames, resolveToolCommand, } from './tool-resolver.js';
|
|
42
43
|
import { detectStaticVacuity, probeTscFileCount, VACUOUS_TOOL_FIX } from './tool-vacuity.js';
|
|
43
|
-
import { classifyEvidenceTask, isDocumentArtifact, isDocumentEvidencePath, } from './verification-policy.js';
|
|
44
|
+
import { classifyEvidenceTask, declaredEvidenceKind, isDocumentArtifact, isDocumentEvidencePath, } from './verification-policy.js';
|
|
44
45
|
/**
|
|
45
46
|
* Valid tool names recognised by the `tool:<name>` evidence atom.
|
|
46
47
|
*
|
|
@@ -229,7 +230,7 @@ siblingCommitSha, context) {
|
|
|
229
230
|
case 'commit':
|
|
230
231
|
return validateCommit(parsed.sha, roots, taskId);
|
|
231
232
|
case 'files': {
|
|
232
|
-
const result = await validateFiles(parsed.paths, roots, taskId, anchorFor, Boolean(artifactPrs ?? context?.artifactCommitSha));
|
|
233
|
+
const result = await validateFiles(parsed.paths, roots, taskId, anchorFor, Boolean(artifactPrs ?? context?.artifactCommitSha), (path) => artifactPrs?.find((pr) => pr.deletedPaths?.some((deleted) => normalizeEvidencePath(deleted) === normalizeEvidencePath(path))));
|
|
233
234
|
if (!result.ok || result.atom.kind !== 'files' || (artifactPrs?.length ?? 0) < 2)
|
|
234
235
|
return result;
|
|
235
236
|
return {
|
|
@@ -238,7 +239,7 @@ siblingCommitSha, context) {
|
|
|
238
239
|
};
|
|
239
240
|
}
|
|
240
241
|
case 'test-run':
|
|
241
|
-
return validateTestRun(parsed.path, roots, taskId ?? context?.task.id);
|
|
242
|
+
return validateTestRun(parsed.path, roots, taskId ?? context?.task.id, context?.testRunPaths ?? []);
|
|
242
243
|
case 'qa-run':
|
|
243
244
|
return validateQaRun(parsed.path, roots, taskId ?? context?.task.id);
|
|
244
245
|
case 'tool':
|
|
@@ -875,7 +876,13 @@ async function commitFilesOrNull(sha, projectRoot) {
|
|
|
875
876
|
export function diffIntersectsAc(diffFiles, acFiles) {
|
|
876
877
|
const norm = (s) => s.replace(/^\.\//, '').replace(/\/+$/, '').toLowerCase();
|
|
877
878
|
const acSet = new Set(acFiles.map(norm));
|
|
878
|
-
|
|
879
|
+
// A directory entry: declared with a trailing slash, or whose last segment
|
|
880
|
+
// has no extension. Judged on the raw entry and its last segment (T14073):
|
|
881
|
+
// the old test ran on the normalised path, whose trailing slash is already
|
|
882
|
+
// stripped, so any dot earlier in it (`.cleo/rcasd/T1/`) hid the directory.
|
|
883
|
+
const acPrefixes = acFiles
|
|
884
|
+
.filter((raw) => /\/\s*$/.test(raw) || !(norm(raw).split('/').pop() ?? '').includes('.'))
|
|
885
|
+
.map(norm);
|
|
879
886
|
for (const f of diffFiles) {
|
|
880
887
|
const nf = norm(f);
|
|
881
888
|
if (acSet.has(nf))
|
|
@@ -1161,6 +1168,8 @@ async function gitShowFileContentAtCommit(relPath, sha, projectRoot) {
|
|
|
1161
1168
|
* enables the commit-tree-first check (T12107).
|
|
1162
1169
|
* @param requireCommitArtifact - Refuse when the anchor tree lacks the path
|
|
1163
1170
|
* (a PR merge anchor: checkout bytes cannot substitute).
|
|
1171
|
+
* @param deletedBy - The PR of the write that deleted a path, if any: such a
|
|
1172
|
+
* path is refused by name rather than as an unfetched merge (T14030).
|
|
1164
1173
|
* @returns Validated atom on success, error on failure.
|
|
1165
1174
|
*
|
|
1166
1175
|
* @internal
|
|
@@ -1168,7 +1177,7 @@ async function gitShowFileContentAtCommit(relPath, sha, projectRoot) {
|
|
|
1168
1177
|
* @task T11959
|
|
1169
1178
|
* @task T12107
|
|
1170
1179
|
*/
|
|
1171
|
-
async function validateFiles(paths, roots, taskId, commitShaFor, requireCommitArtifact = false) {
|
|
1180
|
+
async function validateFiles(paths, roots, taskId, commitShaFor, requireCommitArtifact = false, deletedBy = () => undefined) {
|
|
1172
1181
|
const { storeRoot: projectRoot, executionRoot } = roots;
|
|
1173
1182
|
if (paths.length === 0) {
|
|
1174
1183
|
return {
|
|
@@ -1195,6 +1204,16 @@ async function validateFiles(paths, roots, taskId, commitShaFor, requireCommitAr
|
|
|
1195
1204
|
content = await gitShowFileContentAtCommit(p, commitSha, executionRoot);
|
|
1196
1205
|
}
|
|
1197
1206
|
if (requireCommitArtifact && content === null) {
|
|
1207
|
+
const deleter = deletedBy(p);
|
|
1208
|
+
if (deleter !== undefined)
|
|
1209
|
+
// @sync-invariant none:input-shape a files: path the PR deleted is refused before anything is written
|
|
1210
|
+
return {
|
|
1211
|
+
ok: false,
|
|
1212
|
+
codeName: 'E_EVIDENCE_INVALID',
|
|
1213
|
+
reason: `${p} was deleted by ${deleter.prNumber !== undefined ? `PR #${deleter.prNumber}` : 'the PR'}, ` +
|
|
1214
|
+
`so its merge ${deleter.mergeCommitSha} holds no bytes to inspect. Leave it out of files: ` +
|
|
1215
|
+
'(name the deletion in note: if it matters); files: lists artifacts the PR changed and kept.',
|
|
1216
|
+
};
|
|
1198
1217
|
return {
|
|
1199
1218
|
ok: false,
|
|
1200
1219
|
codeName: 'E_EVIDENCE_INSUFFICIENT',
|
|
@@ -1389,12 +1408,16 @@ async function runAffectedTests(tool, run, roots, taskId) {
|
|
|
1389
1408
|
executionRoot,
|
|
1390
1409
|
semaphoreOptions: { scope: 'affected', ...(taskId !== undefined ? { task: taskId } : {}) },
|
|
1391
1410
|
});
|
|
1411
|
+
const label = `tool:${tool} (affected: ${[run.command.cmd, ...run.command.args].join(' ')})`;
|
|
1412
|
+
// T13013: an OOM or signal kill is reported as a kill, never as failing tests.
|
|
1413
|
+
const killed = result.timedOut ? null : killedValidation(label, run.command.canonical, result);
|
|
1414
|
+
if (killed !== null)
|
|
1415
|
+
return killed;
|
|
1392
1416
|
if (result.exitCode !== 0) {
|
|
1393
1417
|
return {
|
|
1394
1418
|
ok: false,
|
|
1395
1419
|
codeName: result.timedOut ? 'E_EVIDENCE_TOOL_TIMEOUT' : 'E_EVIDENCE_TOOL_FAILED',
|
|
1396
|
-
reason:
|
|
1397
|
-
(result.signal !== null || result.resourceKill !== null ? resourcePlanNote(result) : ''),
|
|
1420
|
+
reason: `${label} exited ${result.exitCode}: ${(result.stderrTail || result.stdoutTail).trim().slice(-300)}`,
|
|
1398
1421
|
};
|
|
1399
1422
|
}
|
|
1400
1423
|
return {
|
|
@@ -1414,7 +1437,7 @@ async function runAffectedTests(tool, run, roots, taskId) {
|
|
|
1414
1437
|
...(run.testFiles ? { affectedTestFiles: run.testFiles } : {}),
|
|
1415
1438
|
...(run.configPaths ? { configPaths: run.configPaths } : {}),
|
|
1416
1439
|
...(run.untested.length > 0 ? { untestedPackages: run.untested } : {}),
|
|
1417
|
-
...toolRunAtomFields(result),
|
|
1440
|
+
...(await toolRunAtomFields(result)),
|
|
1418
1441
|
},
|
|
1419
1442
|
};
|
|
1420
1443
|
}
|
|
@@ -1543,14 +1566,13 @@ hints = {}) {
|
|
|
1543
1566
|
}
|
|
1544
1567
|
return projectRoot;
|
|
1545
1568
|
}
|
|
1546
|
-
|
|
1569
|
+
/**
|
|
1570
|
+
* Resolve, read and parse a `test-run:` report. gh#1226: a relative path
|
|
1571
|
+
* names a report the caller just wrote, in the caller's tree, so the
|
|
1572
|
+
* execution root is tried before the store root.
|
|
1573
|
+
*/
|
|
1574
|
+
async function readTestRunReport(path, roots) {
|
|
1547
1575
|
const { storeRoot: projectRoot, executionRoot } = roots;
|
|
1548
|
-
// gh#1226: a relative test-run path names a report the caller just wrote,
|
|
1549
|
-
// in the caller's tree. Resolving it against the shared store root made a
|
|
1550
|
-
// report written in a worktree report "file does not exist". Try the
|
|
1551
|
-
// execution root first, then fall back to the store root so an absolute or
|
|
1552
|
-
// store-relative path keeps working. gh#1365: the root arrives as a
|
|
1553
|
-
// parameter now; it is NOT re-resolved here.
|
|
1554
1576
|
let abs;
|
|
1555
1577
|
if (isAbsolute(path)) {
|
|
1556
1578
|
abs = path;
|
|
@@ -1578,10 +1600,8 @@ async function validateTestRun(path, roots, taskId) {
|
|
|
1578
1600
|
codeName: 'E_EVIDENCE_INVALID',
|
|
1579
1601
|
};
|
|
1580
1602
|
}
|
|
1581
|
-
const sha256 = createHash('sha256').update(content).digest('hex');
|
|
1582
|
-
let parsed;
|
|
1583
1603
|
try {
|
|
1584
|
-
parsed
|
|
1604
|
+
return { ok: true, abs, content, parsed: JSON.parse(content.toString('utf-8')) };
|
|
1585
1605
|
}
|
|
1586
1606
|
catch (err) {
|
|
1587
1607
|
return {
|
|
@@ -1590,6 +1610,22 @@ async function validateTestRun(path, roots, taskId) {
|
|
|
1590
1610
|
codeName: 'E_EVIDENCE_INVALID',
|
|
1591
1611
|
};
|
|
1592
1612
|
}
|
|
1613
|
+
}
|
|
1614
|
+
/**
|
|
1615
|
+
* Validate a `test-run:` report: its counts, then bound to the change.
|
|
1616
|
+
* `siblings` are every `test-run:` path of the same evidence write (T14031):
|
|
1617
|
+
* the change's coverage is judged over the test files all of them passed, so
|
|
1618
|
+
* a vitest report and a Playwright report together cover unit and e2e files.
|
|
1619
|
+
* Each sibling is still validated on its own by its own atom.
|
|
1620
|
+
*/
|
|
1621
|
+
async function validateTestRun(path, roots, taskId, siblings = []) {
|
|
1622
|
+
const { storeRoot: projectRoot, executionRoot } = roots;
|
|
1623
|
+
const read = await readTestRunReport(path, roots);
|
|
1624
|
+
if (!read.ok)
|
|
1625
|
+
return read;
|
|
1626
|
+
const { abs, content } = read;
|
|
1627
|
+
let parsed = read.parsed;
|
|
1628
|
+
const sha256 = createHash('sha256').update(content).digest('hex');
|
|
1593
1629
|
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
1594
1630
|
return {
|
|
1595
1631
|
ok: false,
|
|
@@ -1597,6 +1633,12 @@ async function validateTestRun(path, roots, taskId) {
|
|
|
1597
1633
|
codeName: 'E_EVIDENCE_INVALID',
|
|
1598
1634
|
};
|
|
1599
1635
|
}
|
|
1636
|
+
// T14031: a Playwright JSON report is read in the vitest shape.
|
|
1637
|
+
const playwright = playwrightTestRun(parsed);
|
|
1638
|
+
if (playwright !== null && 'refusal' in playwright)
|
|
1639
|
+
return { ok: false, reason: playwright.refusal, codeName: playwright.codeName };
|
|
1640
|
+
if (playwright !== null)
|
|
1641
|
+
parsed = playwright;
|
|
1600
1642
|
if (present(parsed.testResults) &&
|
|
1601
1643
|
(!Array.isArray(parsed.testResults) ||
|
|
1602
1644
|
parsed.testResults.some((tr) => tr === null || typeof tr !== 'object' || Array.isArray(tr)))) {
|
|
@@ -1614,7 +1656,7 @@ async function validateTestRun(path, roots, taskId) {
|
|
|
1614
1656
|
return {
|
|
1615
1657
|
ok: false,
|
|
1616
1658
|
reason: `test-run report has no test counts. Expected one of: ${TEST_RUN_SHAPE_HINT}; ` +
|
|
1617
|
-
"e.g. vitest's --reporter=json --outputFile, or " +
|
|
1659
|
+
"e.g. vitest's --reporter=json --outputFile, Playwright's --reporter=json, or " +
|
|
1618
1660
|
'{"total":43,"passed":42,"failed":0,"skipped":1}',
|
|
1619
1661
|
codeName: 'E_EVIDENCE_INVALID',
|
|
1620
1662
|
};
|
|
@@ -1694,6 +1736,19 @@ async function validateTestRun(path, roots, taskId) {
|
|
|
1694
1736
|
if (broken)
|
|
1695
1737
|
return { ok: false, reason: broken, codeName: E_EVIDENCE_GIT_ROOT };
|
|
1696
1738
|
const testFiles = coveredTestFiles(parsed, bindRoot);
|
|
1739
|
+
// T14031: the other reports of this write cover the change with this one.
|
|
1740
|
+
const siblingFiles = [];
|
|
1741
|
+
for (const sibling of siblings) {
|
|
1742
|
+
if (sibling === path)
|
|
1743
|
+
continue;
|
|
1744
|
+
const other = await readTestRunReport(sibling, roots);
|
|
1745
|
+
if (!other.ok || other.parsed === null || typeof other.parsed !== 'object')
|
|
1746
|
+
continue;
|
|
1747
|
+
const adapted = playwrightTestRun(other.parsed);
|
|
1748
|
+
if (adapted !== null && 'refusal' in adapted)
|
|
1749
|
+
continue;
|
|
1750
|
+
siblingFiles.push(...coveredTestFiles(adapted ?? other.parsed, bindRoot));
|
|
1751
|
+
}
|
|
1697
1752
|
// The tree hash is the tool cache's (T12958), so a bound test-run and a
|
|
1698
1753
|
// cached tool run of the same content agree.
|
|
1699
1754
|
const capture = await captureTree(bindRoot, await typedGateInputs(projectRoot, taskId, bindRoot));
|
|
@@ -1709,7 +1764,7 @@ async function validateTestRun(path, roots, taskId) {
|
|
|
1709
1764
|
let typecheckDirs = [];
|
|
1710
1765
|
let untrackedSetAside = [];
|
|
1711
1766
|
if (identity) {
|
|
1712
|
-
const binding = bindTestRunReport(parsed, abs, bindRoot, testFiles);
|
|
1767
|
+
const binding = bindTestRunReport(parsed, abs, bindRoot, siblingFiles.length > 0 ? [...new Set([...testFiles, ...siblingFiles])].sort() : testFiles);
|
|
1713
1768
|
if (!binding.ok)
|
|
1714
1769
|
return { ok: false, reason: binding.reason, codeName: binding.codeName };
|
|
1715
1770
|
untestedPackages = binding.untestedPackages;
|
|
@@ -1995,6 +2050,12 @@ async function validateConfigSchema(roots, context, siblingCommitSha) {
|
|
|
1995
2050
|
const schema = JSON.parse(await readFile(schemaPath, 'utf-8'));
|
|
1996
2051
|
for (const error of checkSchema(data, schema))
|
|
1997
2052
|
failures.push(`${path}: ${error}`);
|
|
2053
|
+
// T14039: a tool command that cannot run without a shell is a config error.
|
|
2054
|
+
if (schemaName === 'project-context.schema.json') {
|
|
2055
|
+
const { projectContextCommandProblems } = await import('./tool-resolver.js');
|
|
2056
|
+
for (const problem of projectContextCommandProblems(data))
|
|
2057
|
+
failures.push(`${path}: ${problem}`);
|
|
2058
|
+
}
|
|
1998
2059
|
}
|
|
1999
2060
|
if (failures.length > 0)
|
|
2000
2061
|
return {
|
|
@@ -2129,7 +2190,8 @@ async function validateTool(tool, roots, context, siblingCommitSha) {
|
|
|
2129
2190
|
return {
|
|
2130
2191
|
ok: false,
|
|
2131
2192
|
reason: resolution.reason,
|
|
2132
|
-
|
|
2193
|
+
// T14039: a declared command with shell syntax is a config error, not a missing tool.
|
|
2194
|
+
codeName: resolution.codeName === 'E_TOOL_UNKNOWN' || resolution.codeName === 'E_TOOL_COMMAND_INVALID'
|
|
2133
2195
|
? 'E_EVIDENCE_INVALID'
|
|
2134
2196
|
: 'E_EVIDENCE_TOOL_UNAVAILABLE',
|
|
2135
2197
|
};
|
|
@@ -2189,49 +2251,10 @@ async function validateTool(tool, roots, context, siblingCommitSha) {
|
|
|
2189
2251
|
codeName: 'E_EVIDENCE_TOOL_BUSY',
|
|
2190
2252
|
};
|
|
2191
2253
|
}
|
|
2192
|
-
// gh#1381
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
// suite sent three separate operators hunting a binary that was present, and
|
|
2197
|
-
// the one diagnostic that would have helped (you exceeded the memory
|
|
2198
|
-
// ceiling) was the one fact discarded.
|
|
2199
|
-
if (result.signal !== null) {
|
|
2200
|
-
const limit = describeMemoryLimit(resolution.command.canonical);
|
|
2201
|
-
return {
|
|
2202
|
-
ok: false,
|
|
2203
|
-
reason: `Tool "${tool}" → ${resolution.command.cmd} ${resolution.command.args.join(' ')} ` +
|
|
2204
|
-
`RAN for ${Math.round(result.durationMs / 1000)}s in ${result.executionRoot} and was ` +
|
|
2205
|
-
`KILLED by ${result.signal}. The binary is present and the command started — ` +
|
|
2206
|
-
`this is not a resolution problem.${limit}${resourcePlanNote(result)} ` +
|
|
2207
|
-
`Nothing was cached, so a retry re-runs the tool from scratch and will be killed ` +
|
|
2208
|
-
`identically unless the cause is addressed.` +
|
|
2209
|
-
(result.stdoutTail || result.stderrTail
|
|
2210
|
-
? ` Last output before the kill: ${tailString(`${result.stdoutTail}\n${result.stderrTail}`, 512)}`
|
|
2211
|
-
: ''),
|
|
2212
|
-
codeName: 'E_EVIDENCE_TOOL_KILLED',
|
|
2213
|
-
};
|
|
2214
|
-
}
|
|
2215
|
-
// T12989: killed for resources with no signal reaching CLEO — a wrapper
|
|
2216
|
-
// (`sh`, `pnpm`) reported the kill as exit 128 + n, or the runner caught a
|
|
2217
|
-
// worker's heap OOM and exited 1. Reported as a failing suite, and cached
|
|
2218
|
-
// as one, it replayed a 3 GB OOM to a retry given 6 GB.
|
|
2219
|
-
if (result.resourceKill !== null) {
|
|
2220
|
-
return {
|
|
2221
|
-
ok: false,
|
|
2222
|
-
reason: `Tool "${tool}" → ${resolution.command.cmd} ${resolution.command.args.join(' ')} ` +
|
|
2223
|
-
`RAN for ${Math.round(result.durationMs / 1000)}s in ${result.executionRoot} and was ` +
|
|
2224
|
-
`KILLED for resources (${result.resourceKill}). This is not a verdict on the code, ` +
|
|
2225
|
-
`and nothing was cached.${resourcePlanNote(result)} Raise the heap with ` +
|
|
2226
|
-
`${HEAVY_HEAP_ENV}=<MiB> (an inherited NODE_OPTIONS heap above the run's budget is ` +
|
|
2227
|
-
`clamped, T13122) or lower the worker count with ${HEAVY_WORKERS_ENV}=<n>, and verify ` +
|
|
2228
|
-
`again — both are part of the cache key, so the retry runs fresh.` +
|
|
2229
|
-
(result.stdoutTail || result.stderrTail
|
|
2230
|
-
? ` Last output: ${tailString(`${result.stdoutTail}\n${result.stderrTail}`, 512)}`
|
|
2231
|
-
: ''),
|
|
2232
|
-
codeName: 'E_EVIDENCE_TOOL_KILLED',
|
|
2233
|
-
};
|
|
2234
|
-
}
|
|
2254
|
+
// gh#1381 / T12989: killed after it started, for resources or by a signal.
|
|
2255
|
+
const killed = killedValidation(`Tool "${tool}" → ${resolution.command.cmd} ${resolution.command.args.join(' ')}`, resolution.command.canonical, result);
|
|
2256
|
+
if (killed !== null)
|
|
2257
|
+
return killed;
|
|
2235
2258
|
// gh#1397: CLEO wraps `test` and `build` in a systemd scope of its own
|
|
2236
2259
|
// making (T12116). When that wrapper cannot start, `systemd-run` exits 1
|
|
2237
2260
|
// WITHOUT ever running the tool — and 1 is also what a suite with a failing
|
|
@@ -2295,7 +2318,7 @@ async function validateTool(tool, roots, context, siblingCommitSha) {
|
|
|
2295
2318
|
tool,
|
|
2296
2319
|
exitCode: 0,
|
|
2297
2320
|
stdoutTail: result.stdoutTail,
|
|
2298
|
-
...toolRunAtomFields(result),
|
|
2321
|
+
...(await toolRunAtomFields(result)),
|
|
2299
2322
|
...(fullScopeReason !== undefined
|
|
2300
2323
|
? { scope: 'full', ...(fullScopeReason ? { scopeReason: fullScopeReason } : {}) }
|
|
2301
2324
|
: {}),
|
|
@@ -2308,9 +2331,13 @@ async function validateTool(tool, roots, context, siblingCommitSha) {
|
|
|
2308
2331
|
* needed its one full retry — the `flaky` files, so a flaky pass stays
|
|
2309
2332
|
* visible to gates instead of reading as a clean one.
|
|
2310
2333
|
*/
|
|
2311
|
-
function toolRunAtomFields(result) {
|
|
2334
|
+
async function toolRunAtomFields(result) {
|
|
2335
|
+
// T13003: the HEAD the run measured, so boundHeadGapReason can refuse a run
|
|
2336
|
+
// on a checkout that lacks the task's implemented commits.
|
|
2337
|
+
const headSha = result.treeHash ? await captureHead(result.executionRoot) : null;
|
|
2312
2338
|
return {
|
|
2313
2339
|
...(result.treeHash ? { treeHash: result.treeHash } : {}),
|
|
2340
|
+
...(headSha ? { headSha } : {}),
|
|
2314
2341
|
cacheHit: result.cacheHit,
|
|
2315
2342
|
...(result.flaky ? { flaky: result.flaky } : {}),
|
|
2316
2343
|
// T13122: the heap and worker count the run got, and why — including an
|
|
@@ -2322,6 +2349,48 @@ function toolRunAtomFields(result) {
|
|
|
2322
2349
|
* One sentence naming the heap and worker plan a memory-bound tool ran under
|
|
2323
2350
|
* (T13122), for a message about a kill; `''` for any other tool.
|
|
2324
2351
|
*/
|
|
2352
|
+
/**
|
|
2353
|
+
* The `E_EVIDENCE_TOOL_KILLED` validation for a tool run that started and was
|
|
2354
|
+
* killed, by a signal or for resources (a wrapper's exit 128 + n, a heap OOM
|
|
2355
|
+
* the runner caught), else `null` (T13013). One message for every path that
|
|
2356
|
+
* runs a tool (`tool:<name>`, the affected-tests run, `cleo done`), naming the
|
|
2357
|
+
* systemd memory ceiling when one applies ({@link describeMemoryLimit}) and
|
|
2358
|
+
* where the heap is really set.
|
|
2359
|
+
*
|
|
2360
|
+
* gh#1381: a tool killed after it started is a different fact from a tool
|
|
2361
|
+
* that never started; "binary missing" sent operators hunting a binary that
|
|
2362
|
+
* was present.
|
|
2363
|
+
*
|
|
2364
|
+
* @param label - How the run is named: `Tool "test" → pnpm run test`.
|
|
2365
|
+
* @param canonical - The canonical tool, for the memory ceiling.
|
|
2366
|
+
* @param result - The run.
|
|
2367
|
+
* @returns The failed validation, or `null` when the run was not killed.
|
|
2368
|
+
*
|
|
2369
|
+
* @task T13013
|
|
2370
|
+
*/
|
|
2371
|
+
export function killedValidation(label, canonical, result) {
|
|
2372
|
+
const how = result.signal !== null
|
|
2373
|
+
? `by ${result.signal}`
|
|
2374
|
+
: result.resourceKill !== null
|
|
2375
|
+
? `for resources (${result.resourceKill})`
|
|
2376
|
+
: null;
|
|
2377
|
+
if (how === null)
|
|
2378
|
+
return null;
|
|
2379
|
+
const output = `${result.stdoutTail}\n${result.stderrTail}`.trim();
|
|
2380
|
+
return {
|
|
2381
|
+
ok: false,
|
|
2382
|
+
reason: `${label} RAN for ${Math.round(result.durationMs / 1000)}s in ${result.executionRoot} and was ` +
|
|
2383
|
+
`KILLED ${how}. The command started, so this is not a resolution problem and not a ` +
|
|
2384
|
+
`verdict on the code; nothing was cached.${describeMemoryLimit(canonical)}` +
|
|
2385
|
+
`${resourcePlanNote(result)} Raise the heap with ${HEAVY_HEAP_ENV}=<MiB> or lower the ` +
|
|
2386
|
+
`worker count with ${HEAVY_WORKERS_ENV}=<n> (both are in the cache key, so the retry ` +
|
|
2387
|
+
`runs fresh). A heap set by the test config (a vitest execArgv such as ` +
|
|
2388
|
+
`--max-old-space-size in vitest.memory-safe) binds each worker and NODE_OPTIONS does ` +
|
|
2389
|
+
`not override it: raise it there.` +
|
|
2390
|
+
(output ? ` Last output: ${tailString(output, 512)}` : ''),
|
|
2391
|
+
codeName: 'E_EVIDENCE_TOOL_KILLED',
|
|
2392
|
+
};
|
|
2393
|
+
}
|
|
2325
2394
|
function resourcePlanNote(result) {
|
|
2326
2395
|
return result.resources ? ` It ran with ${result.resources.summary}.` : '';
|
|
2327
2396
|
}
|
|
@@ -2738,12 +2807,17 @@ async function validatePrAtom(prNumber, roots, context, componentPrNumber) {
|
|
|
2738
2807
|
restoredPaths = direct.restored;
|
|
2739
2808
|
}
|
|
2740
2809
|
if (context.gates.includes('implemented') &&
|
|
2741
|
-
classifyEvidenceTask(context) === 'code' &&
|
|
2810
|
+
classifyEvidenceTask({ ...context, changedPaths }) === 'code' &&
|
|
2742
2811
|
changedPaths.every(isDocumentArtifact)) {
|
|
2812
|
+
// T14065: name the supported reclassifications, not just the refusal.
|
|
2743
2813
|
return {
|
|
2744
2814
|
ok: false,
|
|
2745
2815
|
codeName: 'E_EVIDENCE_CONTENT_MISMATCH',
|
|
2746
|
-
reason: `PR #${linked.prNumber} changes only documentation and cannot implement code task ${context.task.id}.
|
|
2816
|
+
reason: `PR #${linked.prNumber} changes only documentation and cannot implement code task ${context.task.id}. ` +
|
|
2817
|
+
`Changed artifacts: ${changedPaths.join(', ')}. If ${context.task.id} is a documentation task, ` +
|
|
2818
|
+
`declare its documents with \`cleo update ${context.task.id} --files ${changedPaths.join(',')}\` ` +
|
|
2819
|
+
`(declared documents decide), or label it with \`cleo update ${context.task.id} --add-labels docs\` ` +
|
|
2820
|
+
'when it is not a bug and no declared file or acceptance criterion names code; then re-verify.',
|
|
2747
2821
|
};
|
|
2748
2822
|
}
|
|
2749
2823
|
return {
|
|
@@ -2987,7 +3061,7 @@ export function checkTaskEvidenceContext(context, gate, atoms) {
|
|
|
2987
3061
|
return refusal;
|
|
2988
3062
|
}
|
|
2989
3063
|
if (gate === 'implemented' &&
|
|
2990
|
-
classifyEvidenceTask(context) === 'code' &&
|
|
3064
|
+
classifyEvidenceTask({ ...context, changedPaths: atom.changedPaths }) === 'code' &&
|
|
2991
3065
|
atom.changedPaths.every(isDocumentArtifact))
|
|
2992
3066
|
return 'Documentation-only PR evidence cannot implement the current code task.';
|
|
2993
3067
|
}
|
|
@@ -3263,9 +3337,10 @@ export function composeGateEvidence(atoms, capturedBy, override, overrideReason,
|
|
|
3263
3337
|
return result;
|
|
3264
3338
|
}
|
|
3265
3339
|
/**
|
|
3266
|
-
* Why a `test-run:`/`qa-run:` result
|
|
3267
|
-
* (T14004), or null. The binding records the HEAD it
|
|
3268
|
-
*
|
|
3340
|
+
* Why a `test-run:`/`qa-run:` result, or a `tool:` run (T13003), does not
|
|
3341
|
+
* describe this task's work (T14004), or null. The binding records the HEAD it
|
|
3342
|
+
* was bound to (a tool run, the HEAD it measured), and that HEAD must have seen
|
|
3343
|
+
* the task's `implemented` work:
|
|
3269
3344
|
*
|
|
3270
3345
|
* - `commit:` — the run head is that commit or contains it. An older commit
|
|
3271
3346
|
* (the branch base, main before the work) never saw the work.
|
|
@@ -3273,6 +3348,10 @@ export function composeGateEvidence(atoms, capturedBy, override, overrideReason,
|
|
|
3273
3348
|
* head: its recorded `headSha` (a squash merge too), else the merge's second
|
|
3274
3349
|
* parent. An earlier branch commit never saw the whole change.
|
|
3275
3350
|
*
|
|
3351
|
+
* "Seen" also holds when the HEAD carries a patch-equivalent commit
|
|
3352
|
+
* (`git cherry`): a rebase or a message-only amend keeps the patch, not the
|
|
3353
|
+
* SHA (T13003).
|
|
3354
|
+
*
|
|
3276
3355
|
* Evidence bound in another task's worktree is refused even when `cleo
|
|
3277
3356
|
* complete` runs from that same worktree. A commit git cannot read (a shallow
|
|
3278
3357
|
* clone) is refused and said so. Skipped while no implemented commit is
|
|
@@ -3283,9 +3362,11 @@ export function composeGateEvidence(atoms, capturedBy, override, overrideReason,
|
|
|
3283
3362
|
* @param projectRoot - The CLEO store root (the task's checkout is resolved from it).
|
|
3284
3363
|
* @returns The refusal, or null.
|
|
3285
3364
|
* @task T14004
|
|
3365
|
+
* @task T13003
|
|
3286
3366
|
*/
|
|
3287
3367
|
export async function boundHeadGapReason(task, atoms, projectRoot) {
|
|
3288
|
-
const runs = atoms.filter((a) => (a.kind === 'test-run' || a.kind === 'qa-run'
|
|
3368
|
+
const runs = atoms.filter((a) => (a.kind === 'test-run' || a.kind === 'qa-run' || a.kind === 'tool') &&
|
|
3369
|
+
typeof a.headSha === 'string');
|
|
3289
3370
|
if (runs.length === 0)
|
|
3290
3371
|
return null;
|
|
3291
3372
|
const implemented = (task.verification?.evidence?.implemented?.atoms ?? []).filter((a) => a.kind === 'commit' || a.kind === 'pr');
|
|
@@ -3326,7 +3407,14 @@ export async function boundHeadGapReason(task, atoms, projectRoot) {
|
|
|
3326
3407
|
};
|
|
3327
3408
|
// A prefix match needs at least 7 hex characters on both sides: '' matches everything.
|
|
3328
3409
|
const sameCommit = (a, b) => a.length >= 7 && b.length >= 7 && (a.startsWith(b) || b.startsWith(a));
|
|
3329
|
-
|
|
3410
|
+
// T13003: a rebased or amended copy of the commit (same patch) in the run's
|
|
3411
|
+
// history. `git cherry` marks each commit of `sha^..sha` with `-` when the
|
|
3412
|
+
// head has its patch; every one must be marked (merges have no patch).
|
|
3413
|
+
const patchEquivalent = (head, sha) => {
|
|
3414
|
+
const marks = (gitOut(['cherry', head, sha, `${sha}^`]) ?? '').split('\n').filter(Boolean);
|
|
3415
|
+
return marks.length > 0 && marks.every((line) => line.startsWith('-'));
|
|
3416
|
+
};
|
|
3417
|
+
const reached = (head, sha) => sameCommit(head, sha) || contains(head, sha) || patchEquivalent(head, sha);
|
|
3330
3418
|
// Only positive checks accept: the run head is, or contains, the work's tip.
|
|
3331
3419
|
const sawWork = (head, atom) => {
|
|
3332
3420
|
if (atom.kind === 'commit')
|
|
@@ -3348,7 +3436,9 @@ export async function boundHeadGapReason(task, atoms, projectRoot) {
|
|
|
3348
3436
|
missing = null;
|
|
3349
3437
|
if (implemented.every((atom) => sawWork(head, atom)))
|
|
3350
3438
|
continue;
|
|
3351
|
-
const bound =
|
|
3439
|
+
const bound = run.kind === 'tool'
|
|
3440
|
+
? `tool:${run.tool} ran at ${head.slice(0, 12)}`
|
|
3441
|
+
: `${run.kind}:${run.path} was bound at ${head.slice(0, 12)}`;
|
|
3352
3442
|
if (missing !== null) {
|
|
3353
3443
|
return (`${bound}, and git cannot read the commits that relate it to ${task.id}'s implemented ` +
|
|
3354
3444
|
`commit ${shas} (missing object, or a shallow clone: ${missing}). ` +
|
|
@@ -3379,6 +3469,8 @@ export async function boundHeadGapReason(task, atoms, projectRoot) {
|
|
|
3379
3469
|
* on the default branch whose subject cites it, and its merged PRs). A kind
|
|
3380
3470
|
* or label set by any agent never lets a code change skip its tests, and
|
|
3381
3471
|
* work that cannot be inspected fails closed to `code`, with the reason.
|
|
3472
|
+
* A task {@link declaredEvidenceKind} leaves undecided is classified by that
|
|
3473
|
+
* same change set (T14048): documents only make it documentation.
|
|
3382
3474
|
*
|
|
3383
3475
|
* @param task - The task (kind, files, labels and recorded evidence are read).
|
|
3384
3476
|
* @param projectRoot - The CLEO store root (commits are read in the task's checkout).
|
|
@@ -3390,9 +3482,11 @@ export async function boundHeadGapReason(task, atoms, projectRoot) {
|
|
|
3390
3482
|
*/
|
|
3391
3483
|
export async function taskEvidenceKindReport(task, projectRoot, extraPaths = [], cwd, deps = {}) {
|
|
3392
3484
|
const kind = classifyEvidenceTask({ task });
|
|
3393
|
-
|
|
3485
|
+
// T14048: a task its record leaves undecided is classified by what it changed.
|
|
3486
|
+
const undecided = kind === 'code' && declaredEvidenceKind(task) === null;
|
|
3487
|
+
if (kind === 'code' && !undecided)
|
|
3394
3488
|
return { kind };
|
|
3395
|
-
const code = (reason) =>
|
|
3489
|
+
const code = (reason) => undecided ? { kind: 'code' } : { kind: 'code', reason };
|
|
3396
3490
|
const { resolveChangeSetRoot, taskOwnWorkPaths } = await import('./change-set.js');
|
|
3397
3491
|
// The task's own branches, commits and merged PRs, whatever its evidence
|
|
3398
3492
|
// cites: a spike that commits code and records `decision:` owes tests.
|
|
@@ -3420,6 +3514,8 @@ export async function taskEvidenceKindReport(task, projectRoot, extraPaths = [],
|
|
|
3420
3514
|
paths.push(...files);
|
|
3421
3515
|
}
|
|
3422
3516
|
}
|
|
3517
|
+
if (undecided)
|
|
3518
|
+
return { kind: classifyEvidenceTask({ task, changedPaths: paths }) };
|
|
3423
3519
|
const codePath = paths.find((path) => !isDocumentEvidencePath(path));
|
|
3424
3520
|
return codePath === undefined ? { kind } : code(`its work touches ${codePath}`);
|
|
3425
3521
|
}
|