mandrel 2.53.0 → 2.55.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/story-worker.md +24 -23
- package/.agents/audit-checklists/accessibility.md +0 -3
- package/.agents/audit-checklists/mobile.md +0 -4
- package/.agents/docs/agentrc-reference.json +4 -2
- package/.agents/docs/configuration.md +2 -0
- package/.agents/schemas/agentrc.schema.json +15 -1
- package/.agents/schemas/lifecycle/merge.unlanded.schema.json +2 -1
- package/.agents/schemas/story-deliver-terminal.schema.json +1 -0
- package/.agents/scripts/audit-to-stories.js +158 -7
- package/.agents/scripts/check-audit-attribution.js +119 -62
- package/.agents/scripts/check-test-portability.js +512 -0
- package/.agents/scripts/coverage-capture.js +17 -10
- package/.agents/scripts/evidence-gate.js +31 -4
- package/.agents/scripts/generate-workflows-doc.js +65 -14
- package/.agents/scripts/git-cleanup.js +4 -0
- package/.agents/scripts/lib/ITicketingProvider.js +78 -0
- package/.agents/scripts/lib/audit-advisories.js +195 -0
- package/.agents/scripts/lib/audit-attribution.js +22 -0
- package/.agents/scripts/lib/audit-to-stories/dedupe-against-github.js +68 -5
- package/.agents/scripts/lib/audit-to-stories/issue-index.js +83 -0
- package/.agents/scripts/lib/audit-to-stories/ledger-commit.js +60 -114
- package/.agents/scripts/lib/audit-to-stories/ledger-pr.js +347 -0
- package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +169 -44
- package/.agents/scripts/lib/baselines/merge-envelopes.js +298 -32
- package/.agents/scripts/lib/bootstrap/baseline-merge-driver.js +180 -14
- package/.agents/scripts/lib/cli-args.js +26 -0
- package/.agents/scripts/lib/close-validation/gates.js +113 -7
- package/.agents/scripts/lib/close-validation/process.js +7 -3
- package/.agents/scripts/lib/close-validation/runner.js +62 -11
- package/.agents/scripts/lib/config/ci.js +28 -9
- package/.agents/scripts/lib/config-settings-schema-delivery.js +7 -0
- package/.agents/scripts/lib/config-settings-schema.js +19 -1
- package/.agents/scripts/lib/coverage-capture-fullscope.js +23 -11
- package/.agents/scripts/lib/coverage-capture-incremental.js +22 -16
- package/.agents/scripts/lib/coverage-capture-usage.js +5 -1
- package/.agents/scripts/lib/coverage-capture.js +77 -3
- package/.agents/scripts/lib/findings/route-finding.js +4 -2
- package/.agents/scripts/lib/full-suite-lock.js +232 -6
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/git/sync-from-base.js +130 -13
- package/.agents/scripts/lib/observability/source-classifier.js +1 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +10 -2
- package/.agents/scripts/lib/orchestration/check-baselines/phases/refresh-ack.js +75 -15
- package/.agents/scripts/lib/orchestration/deliver-recover.js +82 -43
- package/.agents/scripts/lib/orchestration/dependency-candidates.js +8 -4
- package/.agents/scripts/lib/orchestration/epic-candidates.js +9 -4
- package/.agents/scripts/lib/orchestration/epic-container.js +66 -4
- package/.agents/scripts/lib/orchestration/epic-rollup.js +241 -84
- package/.agents/scripts/lib/orchestration/file-assumptions.js +218 -16
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/branches.js +93 -7
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/git-probes.js +22 -6
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/parse-args.js +26 -5
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/phase-drivers.js +13 -2
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +35 -5
- package/.agents/scripts/lib/orchestration/merge-block-class.js +18 -3
- package/.agents/scripts/lib/orchestration/merge-poll.js +284 -40
- package/.agents/scripts/lib/orchestration/plan-persist/epic-adoption.js +49 -2
- package/.agents/scripts/lib/orchestration/plan-persist/epic-ops.js +43 -7
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +24 -1
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +5 -0
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +3 -0
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +119 -6
- package/.agents/scripts/lib/orchestration/plan-persist/wave-serialisation.js +110 -0
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +130 -40
- package/.agents/scripts/lib/orchestration/resolve-stories.js +44 -1
- package/.agents/scripts/lib/orchestration/review-providers/native.js +31 -11
- package/.agents/scripts/lib/orchestration/review-providers/scoped-lint.js +27 -24
- package/.agents/scripts/lib/orchestration/run-epilogue.js +59 -38
- package/.agents/scripts/lib/orchestration/single-story-close/close-note.js +81 -0
- package/.agents/scripts/lib/orchestration/single-story-close/failed-terminal.js +40 -51
- package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +10 -2
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +101 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +351 -28
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +27 -6
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +117 -22
- package/.agents/scripts/lib/orchestration/story-close/baseline-upward-writeback.js +94 -12
- package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +6 -1
- package/.agents/scripts/lib/orchestration/ticket-validator.js +25 -14
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +70 -6
- package/.agents/scripts/lib/orchestration/verify-credit.js +37 -0
- package/.agents/scripts/lib/pinned-override-notes.js +41 -53
- package/.agents/scripts/lib/pinned-override-resolve.js +212 -0
- package/.agents/scripts/lib/qa/resolve-qa-contract.js +18 -0
- package/.agents/scripts/lib/single-story-sweep/sweep-lock.js +173 -9
- package/.agents/scripts/lib/skills/walk-skill-files.js +24 -7
- package/.agents/scripts/lib/test-temp.js +167 -30
- package/.agents/scripts/lib/validation-evidence.js +37 -0
- package/.agents/scripts/lib/wave-runner/footprint.js +167 -14
- package/.agents/scripts/lib/wave-runner/live-probe.js +7 -1
- package/.agents/scripts/lib/wave-runner/ready-set.js +1 -1
- package/.agents/scripts/merge-baseline.js +175 -21
- package/.agents/scripts/providers/github/errors.js +22 -1
- package/.agents/scripts/providers/github/issues.js +106 -1
- package/.agents/scripts/providers/github/sub-issue-add.js +18 -1
- package/.agents/scripts/providers/github.js +6 -0
- package/.agents/scripts/resolve-stories.js +44 -34
- package/.agents/scripts/single-story-close.js +5 -0
- package/.agents/scripts/stories-wave-tick.js +37 -13
- package/.agents/templates/docs/audit-sweep-runbook.md +41 -7
- package/.agents/workflows/audit-accessibility.md +16 -31
- package/.agents/workflows/audit-mobile.md +20 -37
- package/.agents/workflows/git-cleanup.md +17 -3
- package/.agents/workflows/helpers/audit-lens-core.md +45 -0
- package/.agents/workflows/helpers/deliver-digest.md +7 -6
- package/.agents/workflows/helpers/deliver-reference.md +40 -16
- package/.agents/workflows/helpers/deliver-story-reference.md +7 -4
- package/.agents/workflows/helpers/deliver-story.md +15 -12
- package/.agents/workflows/helpers/plan-reference.md +8 -1
- package/.agents/workflows/mandrel-plan.md +4 -7
- package/.agents/workflows/memory-consolidate.md +14 -9
- package/docs/CHANGELOG.md +34 -0
- package/lib/cli/registry.js +64 -21
- package/lib/cli/sync.js +27 -2
- package/package.json +7 -4
|
@@ -12,6 +12,13 @@
|
|
|
12
12
|
* `blockOnAdvisoryFailure: false` to restore the pre-#5096 behaviour verbatim,
|
|
13
13
|
* or list a job name in `advisoryAllowlist` to exempt just that one.
|
|
14
14
|
*
|
|
15
|
+
* Story #5266 added `rerunAdvisory`, **default `0`**. It is the number of
|
|
16
|
+
* times close may re-run a failed advisory workflow run before blocking on it.
|
|
17
|
+
* Zero is the deliberate default and not a placeholder: a rerun spends the
|
|
18
|
+
* consumer's CI minutes and mutates GitHub state, and close must never do
|
|
19
|
+
* either unasked. Raise it (or pass `--rerun-advisory <n>`) on a repository
|
|
20
|
+
* whose advisory scans time out transiently.
|
|
21
|
+
*
|
|
15
22
|
* Retired (no production readers on v2 Story-only delivery): `earlyPr`
|
|
16
23
|
* (Epic early-PR warmup) and `requireChecks` (AutomergePredicate escape hatch
|
|
17
24
|
* whose listener was never landed).
|
|
@@ -21,6 +28,20 @@ export const CI_DELIVERY_DEFAULTS = Object.freeze({
|
|
|
21
28
|
autoMerge: 'trust-ci',
|
|
22
29
|
blockOnAdvisoryFailure: true,
|
|
23
30
|
advisoryAllowlist: Object.freeze([]),
|
|
31
|
+
rerunAdvisory: 0,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Per-knob validators for the scalar `delivery.ci` settings. A value that
|
|
36
|
+
* fails its own test is not an instruction to guess — it degrades to the
|
|
37
|
+
* framework default beside it in {@link CI_DELIVERY_DEFAULTS}, which is why
|
|
38
|
+
* the two objects are keyed alike and read as a pair.
|
|
39
|
+
*/
|
|
40
|
+
const CI_KNOB_VALIDATORS = Object.freeze({
|
|
41
|
+
autoMerge: (value) => value === 'trust-ci' || value === 'strict',
|
|
42
|
+
blockOnAdvisoryFailure: (value) => typeof value === 'boolean',
|
|
43
|
+
// Story #5266 — a negative or non-integer allowance spends nothing.
|
|
44
|
+
rerunAdvisory: (value) => Number.isInteger(value) && value >= 0,
|
|
24
45
|
});
|
|
25
46
|
|
|
26
47
|
/**
|
|
@@ -31,19 +52,17 @@ export const CI_DELIVERY_DEFAULTS = Object.freeze({
|
|
|
31
52
|
* defaults; only the scalar knobs carry framework defaults here.
|
|
32
53
|
*
|
|
33
54
|
* @param {object | null | undefined} config
|
|
34
|
-
* @returns {{ autoMerge: 'trust-ci' | 'strict',
|
|
55
|
+
* @returns {{ autoMerge: 'trust-ci' | 'strict', blockOnAdvisoryFailure: boolean,
|
|
56
|
+
* advisoryAllowlist: string[], rerunAdvisory: number, watch: object | undefined }}
|
|
35
57
|
*/
|
|
36
58
|
export function getCiDelivery(config) {
|
|
37
59
|
const ci = config?.delivery?.ci ?? config?.ci ?? config ?? {};
|
|
60
|
+
const knobs = {};
|
|
61
|
+
for (const [key, isValid] of Object.entries(CI_KNOB_VALIDATORS)) {
|
|
62
|
+
knobs[key] = isValid(ci[key]) ? ci[key] : CI_DELIVERY_DEFAULTS[key];
|
|
63
|
+
}
|
|
38
64
|
return {
|
|
39
|
-
|
|
40
|
-
ci.autoMerge === 'trust-ci' || ci.autoMerge === 'strict'
|
|
41
|
-
? ci.autoMerge
|
|
42
|
-
: CI_DELIVERY_DEFAULTS.autoMerge,
|
|
43
|
-
blockOnAdvisoryFailure:
|
|
44
|
-
typeof ci.blockOnAdvisoryFailure === 'boolean'
|
|
45
|
-
? ci.blockOnAdvisoryFailure
|
|
46
|
-
: CI_DELIVERY_DEFAULTS.blockOnAdvisoryFailure,
|
|
65
|
+
...knobs,
|
|
47
66
|
advisoryAllowlist: Array.isArray(ci.advisoryAllowlist)
|
|
48
67
|
? ci.advisoryAllowlist.filter(
|
|
49
68
|
(entry) => typeof entry === 'string' && entry,
|
|
@@ -414,6 +414,13 @@ const CI_DELIVERY_SCHEMA = {
|
|
|
414
414
|
'Story #5096. Check-run names exempt from blockOnAdvisoryFailure — a red run whose name matches exactly never blocks arming. Matching is exact; an unnamed run can never match and always blocks.',
|
|
415
415
|
default: [...CI_DELIVERY_DEFAULTS.advisoryAllowlist],
|
|
416
416
|
},
|
|
417
|
+
rerunAdvisory: {
|
|
418
|
+
type: 'integer',
|
|
419
|
+
minimum: 0,
|
|
420
|
+
description:
|
|
421
|
+
'Story #5266. How many times close may re-run a failed advisory workflow run before blocking on it, per close invocation. Default 0: close spends no CI minutes and issues no GitHub mutation on an advisory red unless asked. At n > 0 the failed run(s) are re-run within that allowance and the merge wait re-polls inside its existing budget, landing or blocking on the re-run verdict. Overridden per invocation by --rerun-advisory <n>.',
|
|
422
|
+
default: CI_DELIVERY_DEFAULTS.rerunAdvisory,
|
|
423
|
+
},
|
|
417
424
|
},
|
|
418
425
|
additionalProperties: false,
|
|
419
426
|
};
|
|
@@ -16,6 +16,7 @@ import { SHELL_INJECTION_PATTERN_STRING } from './config-schema-shared.js';
|
|
|
16
16
|
// resolved AGENTRC_SCHEMA is unchanged.
|
|
17
17
|
import { DELIVERY_SCHEMA } from './config-settings-schema-delivery.js';
|
|
18
18
|
import compiledAgentrcValidator from './generated/agentrc-validator.js';
|
|
19
|
+
import { SKILL_ID_RE } from './skills/walk-skill-files.js';
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Annotation contract (Story #5007). These schema literals are the SINGLE
|
|
@@ -529,6 +530,13 @@ const PLANNING_SCHEMA = {
|
|
|
529
530
|
'Recommend a consolidation pass once this many entries have been written since the last one. Measured against the entry count the last pass stamped, so a stamp predating that field leaves growth unmeasured and only the age threshold applies. Default 25.',
|
|
530
531
|
default: 25,
|
|
531
532
|
},
|
|
533
|
+
indexByteCeiling: {
|
|
534
|
+
type: 'integer',
|
|
535
|
+
minimum: 1,
|
|
536
|
+
description:
|
|
537
|
+
"Recommend a consolidation pass once the pool's `MEMORY.md` index exceeds this many bytes. Independent of the age and growth thresholds: the harness truncates the index it loads into each session at its own byte cap, so an oversized index is a loss already happening — every entry listed after the cut is invisible — rather than a hygiene forecast. Default 24576, the harness cap itself.",
|
|
538
|
+
default: 24576,
|
|
539
|
+
},
|
|
532
540
|
},
|
|
533
541
|
additionalProperties: false,
|
|
534
542
|
},
|
|
@@ -681,7 +689,17 @@ const QA_SIGN_IN_SEAM_SCHEMA = {
|
|
|
681
689
|
{
|
|
682
690
|
type: 'object',
|
|
683
691
|
properties: {
|
|
684
|
-
|
|
692
|
+
// The id is joined onto a skills root to reach a `SKILL.md`, so the
|
|
693
|
+
// shape is validated here rather than at the path join (Story #5285).
|
|
694
|
+
// `SKILL_ID_RE` is imported, never restated: one regex, two
|
|
695
|
+
// enforcement points — this schema and `resolveSkillFile`.
|
|
696
|
+
skill: {
|
|
697
|
+
...SAFE_STRING,
|
|
698
|
+
minLength: 1,
|
|
699
|
+
pattern: SKILL_ID_RE.source,
|
|
700
|
+
description:
|
|
701
|
+
'Tier-relative skill id, e.g. `stack/qa/acme-sso`: lowercase segments of letters, digits, `.`, `_` or `-`, at least two of them, separated by `/`. A traversal (`../..`), an absolute path, a backslash or an uppercase segment is rejected here rather than normalized.',
|
|
702
|
+
},
|
|
685
703
|
},
|
|
686
704
|
required: ['skill'],
|
|
687
705
|
additionalProperties: false,
|
|
@@ -11,6 +11,7 @@ import path from 'node:path';
|
|
|
11
11
|
import {
|
|
12
12
|
anyChangedUnderTargets,
|
|
13
13
|
describeFreshness,
|
|
14
|
+
stampCapturedTree,
|
|
14
15
|
} from './coverage-capture.js';
|
|
15
16
|
|
|
16
17
|
/**
|
|
@@ -77,10 +78,23 @@ export function runFullScopeCapture({
|
|
|
77
78
|
logger.info(
|
|
78
79
|
`[coverage-capture] Coverage at ${crap.coveragePath} is ${describeFreshness(freshness, crap.targetDirs)}; running npm run test:coverage…`,
|
|
79
80
|
);
|
|
81
|
+
// Story #5278 — the digest of the tree the suite is about to measure, taken
|
|
82
|
+
// BEFORE the spawn. That is the value the stamp claims; see
|
|
83
|
+
// `stampCapturedTree`.
|
|
84
|
+
const preDigest = computeContentDigestImpl(args.cwd, crap.targetDirs);
|
|
80
85
|
const code = runCaptureImpl({
|
|
81
86
|
cwd: args.cwd,
|
|
82
87
|
timeoutMs: coverage?.timeoutMs,
|
|
83
88
|
log: (m) => logger.info(m),
|
|
89
|
+
// Story #5278 — consulted only if this capture had to queue behind
|
|
90
|
+
// another full suite on this host. Whoever we waited for may have just
|
|
91
|
+
// stamped this exact tree.
|
|
92
|
+
recheckFresh: () =>
|
|
93
|
+
isCoverageFreshImpl({
|
|
94
|
+
coveragePath: crap.coveragePath,
|
|
95
|
+
targetDirs: crap.targetDirs,
|
|
96
|
+
cwd: args.cwd,
|
|
97
|
+
}).fresh === true,
|
|
84
98
|
});
|
|
85
99
|
if (code !== 0) {
|
|
86
100
|
logger.error(
|
|
@@ -93,16 +107,14 @@ export function runFullScopeCapture({
|
|
|
93
107
|
// freshness checks are content-aware (mtime churn from branch switches no
|
|
94
108
|
// longer invalidates). Best-effort — a missing stamp just means the next
|
|
95
109
|
// check falls back to the mtime heuristic.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
)
|
|
105
|
-
logger.info('[coverage-capture] Wrote content-digest capture stamp.');
|
|
106
|
-
}
|
|
110
|
+
stampCapturedTree({
|
|
111
|
+
preDigest,
|
|
112
|
+
cwd: args.cwd,
|
|
113
|
+
targetDirs: crap.targetDirs,
|
|
114
|
+
coveragePath: crap.coveragePath,
|
|
115
|
+
computeContentDigestImpl,
|
|
116
|
+
writeCaptureStampImpl,
|
|
117
|
+
logger,
|
|
118
|
+
});
|
|
107
119
|
return code;
|
|
108
120
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* parameter (`.agents/rules/test-seams.md` rules 1-2, 4).
|
|
10
10
|
*/
|
|
11
11
|
import path from 'node:path';
|
|
12
|
+
import { stampCapturedTree } from './coverage-capture.js';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Run the skip-aware capture path when
|
|
@@ -97,10 +98,19 @@ export function tryIncrementalCapture({
|
|
|
97
98
|
logger.info(
|
|
98
99
|
`[coverage-capture] Incremental mode: ${scopedFiles.length} changed file(s) under [${crap.targetDirs.join(', ')}] — capturing…`,
|
|
99
100
|
);
|
|
101
|
+
// Story #5278 — pre-spawn digest; see `stampCapturedTree`.
|
|
102
|
+
const preDigest = computeContentDigestImpl(args.cwd, crap.targetDirs);
|
|
100
103
|
const code = runCaptureImpl({
|
|
101
104
|
cwd: args.cwd,
|
|
102
105
|
timeoutMs: coverage?.timeoutMs,
|
|
103
106
|
log: (m) => logger.info(m),
|
|
107
|
+
recheckFresh: () =>
|
|
108
|
+
isCoverageFreshImpl({
|
|
109
|
+
coveragePath: crap.coveragePath,
|
|
110
|
+
targetDirs: crap.targetDirs,
|
|
111
|
+
cwd: args.cwd,
|
|
112
|
+
requireScope: 'incremental',
|
|
113
|
+
}).fresh === true,
|
|
104
114
|
});
|
|
105
115
|
if (code !== 0) {
|
|
106
116
|
logger.error(
|
|
@@ -109,21 +119,17 @@ export function tryIncrementalCapture({
|
|
|
109
119
|
return code;
|
|
110
120
|
}
|
|
111
121
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
)
|
|
124
|
-
logger.info(
|
|
125
|
-
'[coverage-capture] Wrote content-digest capture stamp (incremental scope).',
|
|
126
|
-
);
|
|
127
|
-
}
|
|
122
|
+
stampCapturedTree({
|
|
123
|
+
preDigest,
|
|
124
|
+
cwd: args.cwd,
|
|
125
|
+
targetDirs: crap.targetDirs,
|
|
126
|
+
coveragePath: crap.coveragePath,
|
|
127
|
+
scope: 'incremental',
|
|
128
|
+
files: scopedFiles,
|
|
129
|
+
ref,
|
|
130
|
+
computeContentDigestImpl,
|
|
131
|
+
writeCaptureStampImpl,
|
|
132
|
+
logger,
|
|
133
|
+
});
|
|
128
134
|
return code;
|
|
129
135
|
}
|
|
@@ -27,7 +27,7 @@ import { respondToHelp } from './cli-usage.js';
|
|
|
27
27
|
*/
|
|
28
28
|
const COVERAGE_CAPTURE_USAGE = {
|
|
29
29
|
invocation:
|
|
30
|
-
'node .agents/scripts/coverage-capture.js [--skip-when-no-crap-files] [--ref <git-ref>] [--cwd <path>]',
|
|
30
|
+
'node .agents/scripts/coverage-capture.js [--skip-when-no-crap-files] [--require-credited] [--ref <git-ref>] [--cwd <path>]',
|
|
31
31
|
summary:
|
|
32
32
|
'Ensure coverage/coverage-final.json is present and fresh before the CRAP gate fires, spawning `npm run test:coverage` only when it is stale. Writes a content-digest capture stamp that close-validation reads to skip a redundant re-run.',
|
|
33
33
|
flags: [
|
|
@@ -35,6 +35,10 @@ const COVERAGE_CAPTURE_USAGE = {
|
|
|
35
35
|
'--skip-when-no-crap-files',
|
|
36
36
|
'Exit 0 without capturing when no changed file under the CRAP target dirs differs from --ref.',
|
|
37
37
|
],
|
|
38
|
+
[
|
|
39
|
+
'--require-credited',
|
|
40
|
+
'Refuse (exit 1) instead of spawning when no credited capture stamp covers this tree. Passed by the close gate when delivery.execution.requireCreditedCapture is set; a bare invocation always runs, so the deposit path stays open.',
|
|
41
|
+
],
|
|
38
42
|
['--ref <git-ref>', 'Git ref the changed-file set is computed against.'],
|
|
39
43
|
['--cwd <path>', 'Repository root the capture runs in.'],
|
|
40
44
|
],
|
|
@@ -400,8 +400,12 @@ const CREDITING_INVOCATION =
|
|
|
400
400
|
* @param {{
|
|
401
401
|
* requireCredited?: boolean,
|
|
402
402
|
* logger: { info: Function, warn: Function, error: Function },
|
|
403
|
-
* }} opts `requireCredited`
|
|
404
|
-
*
|
|
403
|
+
* }} opts `requireCredited` comes from the CLI's `--require-credited`
|
|
404
|
+
* argument, which the close gate passes when
|
|
405
|
+
* `delivery.execution.requireCreditedCapture` is set (default false →
|
|
406
|
+
* announce and run). Story #5278: it is deliberately NOT read from config
|
|
407
|
+
* here — that made the refusal cover the depositing invocation too, leaving
|
|
408
|
+
* no way to earn the credit it demanded.
|
|
405
409
|
* @returns {number | null} A non-zero exit code the caller MUST return
|
|
406
410
|
* without spawning the suite, or `null` to proceed with the capture.
|
|
407
411
|
*/
|
|
@@ -411,7 +415,7 @@ function announceUncreditedCapture({ requireCredited = false, logger }) {
|
|
|
411
415
|
`the full suite is about to run. Deposit credit before the push with: ${CREDITING_INVOCATION}`;
|
|
412
416
|
if (requireCredited) {
|
|
413
417
|
logger.error(
|
|
414
|
-
`[coverage-capture] ✖ ${preamble} (
|
|
418
|
+
`[coverage-capture] ✖ ${preamble} (--require-credited was passed, so this run is refused instead of paid for).`,
|
|
415
419
|
);
|
|
416
420
|
return 1;
|
|
417
421
|
}
|
|
@@ -445,6 +449,76 @@ export function creditedCapture(runCaptureFn, { requireCredited, logger }) {
|
|
|
445
449
|
};
|
|
446
450
|
}
|
|
447
451
|
|
|
452
|
+
/**
|
|
453
|
+
* Write the capture stamp for a run that has just finished — but only when
|
|
454
|
+
* the tree it measured is still the tree on disk (Story #5278).
|
|
455
|
+
*
|
|
456
|
+
* The stamp is a claim about content: "coverage/coverage-final.json reflects
|
|
457
|
+
* sources digesting to X". Computing X *after* the suite finishes makes that
|
|
458
|
+
* claim false whenever anything moved while the suite ran — a sibling
|
|
459
|
+
* worktree's write, a rebase, an editor save minutes into a ten-minute run.
|
|
460
|
+
* The digest taken **before** the spawn is the one the run actually measured,
|
|
461
|
+
* so that is the value written, and a post-run digest that disagrees means
|
|
462
|
+
* the artifact describes a tree nobody has any more: no stamp is written at
|
|
463
|
+
* all, and the next reader captures rather than crediting a run against
|
|
464
|
+
* sources it never saw.
|
|
465
|
+
*
|
|
466
|
+
* A `null` on either digest is "unavailable", not "changed" — the same
|
|
467
|
+
* fail-open the pre-#5278 code had, since without a digest there is nothing
|
|
468
|
+
* to stamp.
|
|
469
|
+
*
|
|
470
|
+
* @param {{
|
|
471
|
+
* preDigest: string|null,
|
|
472
|
+
* cwd: string,
|
|
473
|
+
* targetDirs: string[],
|
|
474
|
+
* coveragePath: string,
|
|
475
|
+
* scope?: 'full' | 'incremental',
|
|
476
|
+
* files?: string[],
|
|
477
|
+
* ref?: string,
|
|
478
|
+
* computeContentDigestImpl: typeof computeContentDigest,
|
|
479
|
+
* writeCaptureStampImpl: typeof writeCaptureStamp,
|
|
480
|
+
* logger: { info: Function, warn: Function, error: Function },
|
|
481
|
+
* }} opts
|
|
482
|
+
* @returns {boolean} Whether a stamp was written.
|
|
483
|
+
*/
|
|
484
|
+
export function stampCapturedTree({
|
|
485
|
+
preDigest,
|
|
486
|
+
cwd,
|
|
487
|
+
targetDirs,
|
|
488
|
+
coveragePath,
|
|
489
|
+
scope,
|
|
490
|
+
files,
|
|
491
|
+
ref,
|
|
492
|
+
computeContentDigestImpl,
|
|
493
|
+
writeCaptureStampImpl,
|
|
494
|
+
logger,
|
|
495
|
+
}) {
|
|
496
|
+
if (!preDigest) return false;
|
|
497
|
+
const postDigest = computeContentDigestImpl(cwd, targetDirs);
|
|
498
|
+
if (postDigest && postDigest !== preDigest) {
|
|
499
|
+
logger.warn(
|
|
500
|
+
'[coverage-capture] ⚠ the tree moved while the suite ran — the coverage ' +
|
|
501
|
+
'artifact measures sources that are no longer on disk, so no capture ' +
|
|
502
|
+
'stamp was written. The next capture will re-run against the current tree.',
|
|
503
|
+
);
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
const written = writeCaptureStampImpl({
|
|
507
|
+
cwd,
|
|
508
|
+
coveragePath,
|
|
509
|
+
digest: preDigest,
|
|
510
|
+
...(scope === undefined ? {} : { scope }),
|
|
511
|
+
...(files === undefined ? {} : { files }),
|
|
512
|
+
...(ref === undefined ? {} : { ref }),
|
|
513
|
+
});
|
|
514
|
+
if (written) {
|
|
515
|
+
logger.info(
|
|
516
|
+
`[coverage-capture] Wrote content-digest capture stamp${scope ? ` (${scope} scope)` : ''}.`,
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
return written;
|
|
520
|
+
}
|
|
521
|
+
|
|
448
522
|
/**
|
|
449
523
|
* Narrow `changedFiles` to the subset that lives under one of `targetDirs`.
|
|
450
524
|
*
|
|
@@ -160,12 +160,14 @@ export function semanticKeyFooter(keys) {
|
|
|
160
160
|
/**
|
|
161
161
|
* Extract semantic keys from an Issue body carrying the semantic-key footer.
|
|
162
162
|
* The audit filers stamp the footer via {@link semanticKeyFooter}; the
|
|
163
|
-
* confirmation path here
|
|
163
|
+
* confirmation path here, {@link carryProvenanceFooters} and the audit dedup's
|
|
164
|
+
* local issue index read it back. Exported alongside its writer so an indexer
|
|
165
|
+
* cannot drift into a second parse of the same footer.
|
|
164
166
|
*
|
|
165
167
|
* @param {string} body
|
|
166
168
|
* @returns {string[]}
|
|
167
169
|
*/
|
|
168
|
-
function parseSemanticKeyFooter(body) {
|
|
170
|
+
export function parseSemanticKeyFooter(body) {
|
|
169
171
|
return parseAllFooterValues(
|
|
170
172
|
body,
|
|
171
173
|
/<!--\s*audit-semantic-keys:\s*([^>]*?)\s*-->/g,
|