mandrel 2.56.0 → 2.57.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/.agents/agents/plan-critic.md +13 -18
- package/.agents/agents/story-worker.md +25 -34
- package/.agents/docs/agentrc-reference.json +0 -30
- package/.agents/docs/configuration.md +8 -28
- package/.agents/docs/execution-reference.md +5 -5
- package/.agents/docs/quality-gates.md +8 -7
- package/.agents/instructions.md +9 -10
- package/.agents/schemas/agentrc.schema.json +9 -185
- package/.agents/schemas/story-deliver-terminal.schema.json +1 -1
- package/.agents/scripts/acceptance-eval.js +107 -17
- package/.agents/scripts/ceremony-derive.js +191 -0
- package/.agents/scripts/check-context-budget.js +28 -33
- package/.agents/scripts/check-cyclomatic.js +4 -3
- package/.agents/scripts/deliver-light.js +31 -94
- package/.agents/scripts/lib/audit-suite/checklist-threading.js +15 -2
- package/.agents/scripts/lib/baselines/coverage-updater-cli.js +110 -0
- package/.agents/scripts/lib/baselines/crap-preview-scan.js +25 -0
- package/.agents/scripts/lib/baselines/crap-updater-cli.js +223 -0
- package/.agents/scripts/lib/bdd-scenario-budget.js +21 -3
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -1
- package/.agents/scripts/lib/close-validation/gates.js +52 -1
- package/.agents/scripts/lib/config/acceptance-eval.js +25 -57
- package/.agents/scripts/lib/config/delivery-routing.js +7 -33
- package/.agents/scripts/lib/config/explain.js +0 -19
- package/.agents/scripts/lib/config/limits.js +18 -78
- package/.agents/scripts/lib/config/quality.js +6 -3
- package/.agents/scripts/lib/config/runners.js +3 -2
- package/.agents/scripts/lib/config-settings-schema-delivery.js +15 -68
- package/.agents/scripts/lib/config-settings-schema-quality.js +0 -14
- package/.agents/scripts/lib/config-settings-schema.js +16 -143
- package/.agents/scripts/lib/crap-engine.js +35 -4
- package/.agents/scripts/lib/crap-utils.js +17 -1
- package/.agents/scripts/lib/cyclomatic-ceiling.js +19 -7
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/observability/runtime-friction.js +1 -1
- package/.agents/scripts/lib/observability/source-classifier.js +1 -0
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +5 -4
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +19 -73
- package/.agents/scripts/lib/orchestration/complexity-gate.js +46 -212
- package/.agents/scripts/lib/orchestration/file-assumptions.js +32 -17
- package/.agents/scripts/lib/orchestration/light-escalation.js +3 -3
- package/.agents/scripts/lib/orchestration/light-suitability.js +66 -233
- package/.agents/scripts/lib/orchestration/plan-context.js +181 -387
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +42 -153
- package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +14 -70
- package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +300 -0
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +131 -168
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +118 -297
- package/.agents/scripts/lib/orchestration/plan-persist/soft-findings.js +55 -0
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +16 -65
- package/.agents/scripts/lib/orchestration/plan-persist/wave-serialisation.js +22 -35
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +30 -139
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +61 -223
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +5 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pre-gate-steps.js +46 -16
- package/.agents/scripts/lib/orchestration/story-close/context-budget-writeback.js +213 -0
- package/.agents/scripts/lib/orchestration/task-body-validator.js +10 -63
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +33 -539
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +21 -414
- package/.agents/scripts/lib/orchestration/ticket-validator.js +54 -118
- package/.agents/scripts/lib/orchestration/verify-credit.js +69 -24
- package/.agents/scripts/lib/story-body/body-format-lints.js +15 -85
- package/.agents/scripts/lib/story-body/story-body.js +17 -237
- package/.agents/scripts/lib/templates/decomposer-prompts.js +84 -121
- package/.agents/scripts/lib/test-isolate/cli-options.js +93 -0
- package/.agents/scripts/lib/test-isolate/progress-log.js +45 -0
- package/.agents/scripts/lib/test-isolate/render-report.js +97 -0
- package/.agents/scripts/lib/test-isolate/run-isolate.js +87 -0
- package/.agents/scripts/lib/test-run-credit.js +266 -0
- package/.agents/scripts/lib/wave-runner/footprint.js +48 -358
- package/.agents/scripts/lib/wave-runner/ready-set.js +6 -5
- package/.agents/scripts/lib/workers/crap-worker.js +32 -41
- package/.agents/scripts/plan-context.js +7 -9
- package/.agents/scripts/plan-critics.js +28 -54
- package/.agents/scripts/plan-persist.js +25 -68
- package/.agents/scripts/quality-preview.js +51 -0
- package/.agents/scripts/run-tests.js +12 -0
- package/.agents/scripts/stories-wave-tick.js +23 -45
- package/.agents/scripts/test-isolate.js +13 -180
- package/.agents/scripts/update-coverage-baseline.js +25 -70
- package/.agents/scripts/update-crap-baseline.js +19 -123
- package/.agents/skills/core/scope-triage/SKILL.md +3 -3
- package/.agents/workflows/audit-clean-code.md +4 -3
- package/.agents/workflows/helpers/acceptance-self-eval.md +41 -41
- package/.agents/workflows/helpers/code-quality-guardrails.md +4 -4
- package/.agents/workflows/helpers/code-review.md +2 -3
- package/.agents/workflows/helpers/deliver-digest.md +41 -57
- package/.agents/workflows/helpers/deliver-light.md +40 -105
- package/.agents/workflows/helpers/deliver-reference.md +1 -1
- package/.agents/workflows/helpers/deliver-story-reference.md +37 -58
- package/.agents/workflows/helpers/deliver-story.md +9 -13
- package/.agents/workflows/helpers/plan-reference.md +132 -219
- package/.agents/workflows/mandrel-plan.md +27 -40
- package/.agents/workflows/memory-consolidate.md +9 -13
- package/docs/CHANGELOG.md +23 -0
- package/lib/migrations/index.js +4 -0
- package/lib/migrations/steps/2.57.0-retire-delivery-limit-knobs.js +45 -0
- package/lib/migrations/steps/2.57.0-retire-planning-limit-knobs.js +59 -0
- package/package.json +1 -1
- package/.agents/scripts/lib/framework-version.js +0 -39
- package/.agents/scripts/lib/orchestration/consolidation-precondition.js +0 -223
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +0 -97
- package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +0 -26
- package/.agents/scripts/lib/orchestration/spec-budget.js +0 -89
- package/.agents/scripts/lib/orchestration/spec-spill.js +0 -74
- package/.agents/scripts/lib/orchestration/verify-tier-repair.js +0 -107
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/test-isolate/run-isolate.js — the `test-isolate` orchestration.
|
|
3
|
+
*
|
|
4
|
+
* Story #5316: moved out of `.agents/scripts/test-isolate.js` (CRAP 56 at
|
|
5
|
+
* cyclomatic 7, 0% coverage) so it can be driven from a test. The CLI shell
|
|
6
|
+
* that remains does nothing but call this and translate the exit code.
|
|
7
|
+
*
|
|
8
|
+
* `resolveFiles` and `diagnose` are named seams defaulting to the real
|
|
9
|
+
* implementations, per [`rules/test-seams.md`](../../../rules/test-seams.md):
|
|
10
|
+
* a unit test substitutes them rather than running real suites in child
|
|
11
|
+
* processes, and the production caller passes neither.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { parseIsolateArgv } from './cli-options.js';
|
|
15
|
+
import { resolveTestFiles as defaultResolveTestFiles } from './list-files.js';
|
|
16
|
+
import { createProgressLogger } from './progress-log.js';
|
|
17
|
+
import { renderReport } from './render-report.js';
|
|
18
|
+
import { diagnoseIsolation as defaultDiagnoseIsolation } from './runner.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The report shape returned when no file matched the pattern — a real report
|
|
22
|
+
* with nothing in it, so callers never branch on null.
|
|
23
|
+
*
|
|
24
|
+
* @returns {import('./runner.js').IsolateReport}
|
|
25
|
+
*/
|
|
26
|
+
function emptyReport() {
|
|
27
|
+
return {
|
|
28
|
+
pattern: null,
|
|
29
|
+
files: [],
|
|
30
|
+
isolated: [],
|
|
31
|
+
suite: [],
|
|
32
|
+
flippers: [],
|
|
33
|
+
bisections: [],
|
|
34
|
+
envMutators: [],
|
|
35
|
+
durationMs: 0,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Run the isolation diagnosis end to end.
|
|
41
|
+
*
|
|
42
|
+
* Exit code is 1 when the run found anything worth an operator's attention —
|
|
43
|
+
* a flipper OR an env mutator — and 0 otherwise. An empty match set is not a
|
|
44
|
+
* failure: nothing was asked of the suite, so nothing can be wrong with it.
|
|
45
|
+
*
|
|
46
|
+
* @param {object} [opts]
|
|
47
|
+
* @param {string[]} [opts.argv]
|
|
48
|
+
* @param {string} [opts.repoRoot]
|
|
49
|
+
* @param {(line: string) => void} [opts.onLog]
|
|
50
|
+
* @param {(args: {pattern: string|undefined, repoRoot: string}) => string[]} [opts.resolveFiles]
|
|
51
|
+
* @param {(args: object) => Promise<import('./runner.js').IsolateReport>} [opts.diagnose]
|
|
52
|
+
* @returns {Promise<{ exitCode: number, report: import('./runner.js').IsolateReport }>}
|
|
53
|
+
*/
|
|
54
|
+
export async function runTestIsolate({
|
|
55
|
+
argv = [],
|
|
56
|
+
repoRoot,
|
|
57
|
+
onLog = (s) => process.stdout.write(`${s}\n`),
|
|
58
|
+
resolveFiles = defaultResolveTestFiles,
|
|
59
|
+
diagnose = defaultDiagnoseIsolation,
|
|
60
|
+
} = {}) {
|
|
61
|
+
const options = parseIsolateArgv(argv);
|
|
62
|
+
const files = resolveFiles({ pattern: options.pattern, repoRoot });
|
|
63
|
+
if (files.length === 0) {
|
|
64
|
+
onLog(
|
|
65
|
+
`[test-isolate] no test files matched pattern: ${options.pattern ?? '<default>'}`,
|
|
66
|
+
);
|
|
67
|
+
return { exitCode: 0, report: emptyReport() };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!options.quiet) {
|
|
71
|
+
onLog(`[test-isolate] scanning ${files.length} file(s)...`);
|
|
72
|
+
}
|
|
73
|
+
const report = await diagnose({
|
|
74
|
+
repoRoot,
|
|
75
|
+
files,
|
|
76
|
+
workers: options.workers,
|
|
77
|
+
suiteConcurrency: options.suiteConcurrency,
|
|
78
|
+
maxBisectDepth: options.maxBisectDepth,
|
|
79
|
+
maxBisectTargets: options.maxBisectTargets,
|
|
80
|
+
onProgress: options.quiet ? undefined : createProgressLogger(onLog),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
onLog(options.json ? JSON.stringify(report, null, 2) : renderReport(report));
|
|
84
|
+
|
|
85
|
+
const clean = report.flippers.length === 0 && report.envMutators.length === 0;
|
|
86
|
+
return { exitCode: clean ? 0 : 1, report };
|
|
87
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/test-run-credit.js — let a green bare `npm test` earn the credit close
|
|
3
|
+
* reads (Story #5313).
|
|
4
|
+
*
|
|
5
|
+
* Until this module the only suite run that deposited credit was the one
|
|
6
|
+
* shaped exactly like the close gate — `coverage-capture.js --cwd <worktree>`
|
|
7
|
+
* or `evidence-gate.js --standalone … -- npm test` — and the digest, the
|
|
8
|
+
* worker boot context and the reference all carried prose explaining which
|
|
9
|
+
* invocation to type. A worker that ran the project's own test runner paid
|
|
10
|
+
* for the suite and then close paid for it again. The runner is the natural
|
|
11
|
+
* depositor: it knows the tree it ran against, whether the run was green,
|
|
12
|
+
* and whether it ran the whole suite.
|
|
13
|
+
*
|
|
14
|
+
* On a green **full-tier** run inside a `story-<id>` checkout the runner
|
|
15
|
+
* records the `test` gate's evidence in the same keyspace
|
|
16
|
+
* `close-validation/runner.js` consults — keyed on HEAD and the tree
|
|
17
|
+
* fingerprint, hashed on the exact `{ cmd: 'npm', args: ['test'], cwd }`
|
|
18
|
+
* close will spawn — so close's `test` gate short-circuits at unchanged HEAD.
|
|
19
|
+
* The freshness keying is untouched: a later commit voids the record exactly
|
|
20
|
+
* as it voids one `evidence-gate.js` wrote.
|
|
21
|
+
*
|
|
22
|
+
* What it deliberately does **not** do is write the coverage capture stamp:
|
|
23
|
+
* that stamp is a claim that `coverage/coverage-final.json` describes this
|
|
24
|
+
* tree, and a bare `npm test` produces no such artifact. The CRAP gate still
|
|
25
|
+
* runs `coverage-capture.js` when it needs one.
|
|
26
|
+
*
|
|
27
|
+
* Total: every failure — not a Story branch, no git, an unwritable evidence
|
|
28
|
+
* file — is reported by reason and never fails the test run that earned it.
|
|
29
|
+
*
|
|
30
|
+
* @module lib/test-run-credit
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import path from 'node:path';
|
|
34
|
+
|
|
35
|
+
import { gitSpawn as defaultGitSpawn } from './git-utils.js';
|
|
36
|
+
import {
|
|
37
|
+
recordPass as defaultRecordPass,
|
|
38
|
+
shouldSkip as defaultShouldSkip,
|
|
39
|
+
hashCommandConfig,
|
|
40
|
+
treeFingerprint,
|
|
41
|
+
} from './validation-evidence.js';
|
|
42
|
+
|
|
43
|
+
/** The gate name close's runner looks the record up under. */
|
|
44
|
+
const GATE_NAME = 'test';
|
|
45
|
+
|
|
46
|
+
/** The exact command close spawns for that gate — the hash must match it. */
|
|
47
|
+
const GATE_COMMAND = Object.freeze({
|
|
48
|
+
cmd: 'npm',
|
|
49
|
+
args: Object.freeze(['test']),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Read one trimmed git stdout line, or `null` on any failure.
|
|
54
|
+
*
|
|
55
|
+
* @param {Function} gitSpawnFn
|
|
56
|
+
* @param {string} cwd
|
|
57
|
+
* @param {string[]} args
|
|
58
|
+
* @returns {string|null}
|
|
59
|
+
*/
|
|
60
|
+
function gitLine(gitSpawnFn, cwd, args) {
|
|
61
|
+
try {
|
|
62
|
+
const res = gitSpawnFn(cwd, ...args);
|
|
63
|
+
if (res?.status !== 0) return null;
|
|
64
|
+
const line = String(res.stdout ?? '').trim();
|
|
65
|
+
return line.length > 0 ? line : null;
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The Story id a checkout's branch names, or `null` off a Story branch.
|
|
73
|
+
*
|
|
74
|
+
* @param {string|null} branch
|
|
75
|
+
* @returns {number|null}
|
|
76
|
+
*/
|
|
77
|
+
export function storyIdFromBranch(branch) {
|
|
78
|
+
const match = /^story-(\d+)$/.exec(branch ?? '');
|
|
79
|
+
if (!match) return null;
|
|
80
|
+
const id = Number.parseInt(match[1], 10);
|
|
81
|
+
return Number.isInteger(id) && id > 0 ? id : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The checkout whose temp tree holds the evidence keyspace: the **main**
|
|
86
|
+
* checkout, which is where close runs with `--cwd <main-repo>`. From a
|
|
87
|
+
* worktree `git rev-parse --git-common-dir` names the main `.git`; from the
|
|
88
|
+
* main checkout it names its own. Either way the parent is the checkout.
|
|
89
|
+
*
|
|
90
|
+
* @param {string} cwd
|
|
91
|
+
* @param {Function} gitSpawnFn
|
|
92
|
+
* @returns {string|null}
|
|
93
|
+
*/
|
|
94
|
+
export function resolveEvidenceRoot(cwd, gitSpawnFn) {
|
|
95
|
+
const common = gitLine(gitSpawnFn, cwd, ['rev-parse', '--git-common-dir']);
|
|
96
|
+
if (!common) return null;
|
|
97
|
+
const absolute = path.resolve(cwd, common);
|
|
98
|
+
return path.basename(absolute) === '.git' ? path.dirname(absolute) : null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Deposit the `test` gate's evidence for a green full-suite run.
|
|
103
|
+
*
|
|
104
|
+
* @param {{
|
|
105
|
+
* cwd: string,
|
|
106
|
+
* tier?: string,
|
|
107
|
+
* status?: number,
|
|
108
|
+
* durationMs?: number|null,
|
|
109
|
+
* gitSpawnFn?: typeof defaultGitSpawn,
|
|
110
|
+
* recordPassFn?: typeof defaultRecordPass,
|
|
111
|
+
* }} args
|
|
112
|
+
* @returns {{ deposited: boolean, reason: string, storyId?: number, sha?: string }}
|
|
113
|
+
*/
|
|
114
|
+
export function depositTestRunCredit({
|
|
115
|
+
cwd,
|
|
116
|
+
tier = 'full',
|
|
117
|
+
status = 0,
|
|
118
|
+
durationMs = null,
|
|
119
|
+
gitSpawnFn = defaultGitSpawn,
|
|
120
|
+
recordPassFn = defaultRecordPass,
|
|
121
|
+
} = {}) {
|
|
122
|
+
if (status !== 0) return { deposited: false, reason: 'run-not-green' };
|
|
123
|
+
if (tier !== 'full') return { deposited: false, reason: 'not-full-tier' };
|
|
124
|
+
const storyId = storyIdFromBranch(
|
|
125
|
+
gitLine(gitSpawnFn, cwd, ['rev-parse', '--abbrev-ref', 'HEAD']),
|
|
126
|
+
);
|
|
127
|
+
if (storyId === null)
|
|
128
|
+
return { deposited: false, reason: 'not-a-story-branch' };
|
|
129
|
+
const sha = gitLine(gitSpawnFn, cwd, ['rev-parse', 'HEAD']);
|
|
130
|
+
const evidenceRoot = resolveEvidenceRoot(cwd, gitSpawnFn);
|
|
131
|
+
if (!sha || !evidenceRoot) {
|
|
132
|
+
return { deposited: false, reason: 'tree-unreadable', storyId };
|
|
133
|
+
}
|
|
134
|
+
return writeRecord({
|
|
135
|
+
storyId,
|
|
136
|
+
sha,
|
|
137
|
+
cwd,
|
|
138
|
+
evidenceRoot,
|
|
139
|
+
durationMs,
|
|
140
|
+
gitSpawnFn,
|
|
141
|
+
recordPassFn,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Deposit and say so on stderr — the runner's one-line hook. The line is
|
|
147
|
+
* the only surface a worker sees, so it names the outcome by reason.
|
|
148
|
+
*
|
|
149
|
+
* @param {Parameters<typeof depositTestRunCredit>[0] & { log?: (line: string) => void }} args
|
|
150
|
+
* @returns {ReturnType<typeof depositTestRunCredit>}
|
|
151
|
+
*/
|
|
152
|
+
export function reportTestRunCredit({
|
|
153
|
+
log = (line) => process.stderr.write(`${line}\n`),
|
|
154
|
+
...args
|
|
155
|
+
} = {}) {
|
|
156
|
+
const credit = depositTestRunCredit(args);
|
|
157
|
+
log(
|
|
158
|
+
credit.deposited
|
|
159
|
+
? `[run-tests] ✓ deposited the close test credit for story #${credit.storyId} at ${credit.sha.slice(0, 7)}`
|
|
160
|
+
: `[run-tests] no close test credit deposited (${credit.reason})`,
|
|
161
|
+
);
|
|
162
|
+
return credit;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Is the `test` gate already credited for this tree — did a green bare
|
|
167
|
+
* `npm test` in the Story worktree deposit its evidence? The read side of
|
|
168
|
+
* {@link depositTestRunCredit}, consulted by `close-validation/gates.js`.
|
|
169
|
+
*
|
|
170
|
+
* When it did, close registers the plain `test` gate beside the capture gate
|
|
171
|
+
* even though coverage-capture is the active test runner: the runner's
|
|
172
|
+
* evidence check then skips it as credited, so close REPORTS the suite the
|
|
173
|
+
* worker already ran instead of silently folding it into the capture. It is
|
|
174
|
+
* never a second spend — an uncredited tree resolves `false` and the
|
|
175
|
+
* pre-#5313 shape (capture alone runs the suite) is unchanged. Every
|
|
176
|
+
* uncertainty resolves `false`.
|
|
177
|
+
*
|
|
178
|
+
* @param {{
|
|
179
|
+
* storyId?: number|null,
|
|
180
|
+
* cwd?: string,
|
|
181
|
+
* evidenceCwd?: string|null,
|
|
182
|
+
* gitSpawnImpl?: typeof defaultGitSpawn,
|
|
183
|
+
* shouldSkipImpl?: typeof defaultShouldSkip,
|
|
184
|
+
* log?: (line: string) => void,
|
|
185
|
+
* }} opts
|
|
186
|
+
* @returns {boolean}
|
|
187
|
+
*/
|
|
188
|
+
export function predictsTestEvidenceCredit({
|
|
189
|
+
storyId,
|
|
190
|
+
cwd,
|
|
191
|
+
evidenceCwd,
|
|
192
|
+
gitSpawnImpl = defaultGitSpawn,
|
|
193
|
+
shouldSkipImpl = defaultShouldSkip,
|
|
194
|
+
log,
|
|
195
|
+
} = {}) {
|
|
196
|
+
if (!Number.isInteger(storyId) || storyId <= 0) return false;
|
|
197
|
+
if (typeof cwd !== 'string' || cwd.length === 0) return false;
|
|
198
|
+
try {
|
|
199
|
+
const sha = gitLine(gitSpawnImpl, cwd, ['rev-parse', 'HEAD']);
|
|
200
|
+
if (!sha) return false;
|
|
201
|
+
const verdict = shouldSkipImpl(
|
|
202
|
+
{
|
|
203
|
+
storyId,
|
|
204
|
+
gateName: GATE_NAME,
|
|
205
|
+
currentSha: sha,
|
|
206
|
+
configHash: hashCommandConfig({
|
|
207
|
+
cmd: GATE_COMMAND.cmd,
|
|
208
|
+
args: [...GATE_COMMAND.args],
|
|
209
|
+
cwd: path.resolve(cwd),
|
|
210
|
+
}),
|
|
211
|
+
inputFingerprint: treeFingerprint(cwd, gitSpawnImpl),
|
|
212
|
+
},
|
|
213
|
+
{ cwd: evidenceCwd ?? cwd, standalone: true },
|
|
214
|
+
);
|
|
215
|
+
if (verdict.skip !== true) return false;
|
|
216
|
+
log?.(
|
|
217
|
+
'[close-validation] a green `npm test` already deposited the test credit for this tree — registering the plain `test` gate so close reports it as credited.',
|
|
218
|
+
);
|
|
219
|
+
return true;
|
|
220
|
+
} catch {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* The write itself, split out so the guard chain above stays flat.
|
|
227
|
+
*
|
|
228
|
+
* @param {{ storyId: number, sha: string, cwd: string, evidenceRoot: string, durationMs: number|null, gitSpawnFn: Function, recordPassFn: Function }} args
|
|
229
|
+
* @returns {{ deposited: boolean, reason: string, storyId: number, sha: string }}
|
|
230
|
+
*/
|
|
231
|
+
function writeRecord({
|
|
232
|
+
storyId,
|
|
233
|
+
sha,
|
|
234
|
+
cwd,
|
|
235
|
+
evidenceRoot,
|
|
236
|
+
durationMs,
|
|
237
|
+
gitSpawnFn,
|
|
238
|
+
recordPassFn,
|
|
239
|
+
}) {
|
|
240
|
+
try {
|
|
241
|
+
recordPassFn(
|
|
242
|
+
{
|
|
243
|
+
storyId,
|
|
244
|
+
gateName: GATE_NAME,
|
|
245
|
+
sha,
|
|
246
|
+
configHash: hashCommandConfig({
|
|
247
|
+
cmd: GATE_COMMAND.cmd,
|
|
248
|
+
args: [...GATE_COMMAND.args],
|
|
249
|
+
cwd: path.resolve(cwd),
|
|
250
|
+
}),
|
|
251
|
+
exitCode: 0,
|
|
252
|
+
durationMs,
|
|
253
|
+
inputFingerprint: treeFingerprint(cwd, gitSpawnFn),
|
|
254
|
+
},
|
|
255
|
+
{ cwd: evidenceRoot, standalone: true },
|
|
256
|
+
);
|
|
257
|
+
return { deposited: true, reason: 'recorded', storyId, sha };
|
|
258
|
+
} catch (err) {
|
|
259
|
+
return {
|
|
260
|
+
deposited: false,
|
|
261
|
+
reason: `record-failed: ${err?.message ?? err}`,
|
|
262
|
+
storyId,
|
|
263
|
+
sha,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
}
|