mandrel 2.63.0 → 2.65.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/acceptance-critic.md +3 -2
- package/.agents/agents/auditor.md +3 -2
- package/.agents/agents/plan-critic.md +3 -2
- package/.agents/agents/story-worker.md +2 -2
- package/.agents/audit-checklists/quality.md +3 -0
- package/.agents/docs/agentrc-reference.json +1 -9
- package/.agents/docs/configuration.md +8 -7
- package/.agents/schemas/agentrc.schema.json +6 -13
- package/.agents/schemas/audit-rules.schema.json +1 -1
- package/.agents/schemas/story-deliver-terminal.schema.json +5 -0
- package/.agents/scripts/bootstrap.js +8 -2
- package/.agents/scripts/check-context-budget.js +1 -1
- package/.agents/scripts/lib/ITicketingProvider.js +1 -3
- package/.agents/scripts/lib/audit-suite/findings.js +1 -17
- package/.agents/scripts/lib/audit-suite/frontmatter.js +0 -28
- package/.agents/scripts/lib/audit-suite/index.js +0 -6
- package/.agents/scripts/lib/audit-suite/selector.js +0 -31
- package/.agents/scripts/lib/bootstrap/agents-md-fold.js +156 -0
- package/.agents/scripts/lib/bootstrap/commit-push.js +1 -1
- package/.agents/scripts/lib/bootstrap/manifest.js +2 -2
- package/.agents/scripts/lib/bootstrap/project-bootstrap.js +13 -29
- package/.agents/scripts/lib/config/review-chain-default.js +13 -0
- package/.agents/scripts/lib/config-settings-schema-delivery.js +2 -2
- package/.agents/scripts/lib/config-settings-schema-quality.js +11 -13
- package/.agents/scripts/lib/doc-tiers.js +25 -6
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/observability/metrics-ledger.js +0 -72
- package/.agents/scripts/lib/orchestration/ci-red-handling.js +73 -0
- package/.agents/scripts/lib/orchestration/code-review.js +11 -6
- package/.agents/scripts/lib/orchestration/deliver-recover.js +56 -11
- package/.agents/scripts/lib/orchestration/epic-rollup.js +29 -12
- package/.agents/scripts/lib/orchestration/merge-block-class.js +20 -4
- package/.agents/scripts/lib/orchestration/merge-poll.js +41 -22
- package/.agents/scripts/lib/orchestration/required-checks.js +147 -0
- package/.agents/scripts/lib/orchestration/review-providers/code-review.js +203 -0
- package/.agents/scripts/lib/orchestration/review-providers/review-provider-factory.js +6 -4
- package/.agents/scripts/lib/orchestration/review-providers/security-review.js +3 -2
- package/.agents/scripts/lib/orchestration/single-story-close/failed-terminal.js +1 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +0 -12
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +135 -20
- package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +112 -82
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +72 -5
- package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +12 -87
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +3 -0
- package/.agents/scripts/lib/templates/decomposer-prompts.js +5 -24
- package/.agents/scripts/pr-watch-with-update.js +13 -19
- package/.agents/scripts/providers/github/issues.js +14 -23
- package/.agents/scripts/sync-claude-agents.js +1 -1
- package/.agents/workflows/audit-quality.md +42 -7
- package/.agents/workflows/helpers/acceptance-self-eval.md +1 -1
- package/.agents/workflows/helpers/code-review.md +15 -38
- package/.agents/workflows/helpers/deliver-reference.md +4 -2
- package/.agents/workflows/helpers/deliver-story.md +3 -0
- package/.agents/workflows/helpers/plan-reference.md +9 -8
- package/.agents/workflows/mandrel-deliver.md +2 -1
- package/.agents/workflows/mandrel-plan.md +10 -7
- package/.agents/workflows/mandrel-update.md +5 -3
- package/docs/CHANGELOG.md +38 -0
- package/lib/cli/claude-code-version.js +73 -0
- package/lib/cli/doctor.js +2 -2
- package/lib/cli/registry.js +9 -0
- package/lib/cli/uninstall.js +37 -9
- package/lib/migrations/index.js +2 -0
- package/lib/migrations/steps/2.65.0-fold-claude-md-into-agents-md.js +38 -0
- package/package.json +2 -1
- package/.agents/scripts/lib/audit-suite/lens-diff-floor.js +0 -99
- package/.agents/scripts/lib/audit-suite/runner.js +0 -205
- package/.agents/scripts/lib/audit-suite/substitutions.js +0 -96
- package/.agents/scripts/lib/audit-suite/workflow-loader.js +0 -37
- package/.agents/scripts/lib/orchestration/story-close/phases/local-lens-review.js +0 -234
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// lib/migrations/steps/2.65.0-fold-claude-md-into-agents-md.js
|
|
2
|
+
/**
|
|
3
|
+
* Fold a consumer's root CLAUDE.md into AGENTS.md and delete CLAUDE.md.
|
|
4
|
+
* The host reads CLAUDE.md exclusively whenever it exists, so a surviving one
|
|
5
|
+
* would shadow the AGENTS.md wiring. Shares the bootstrap fold contract.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import nodeFs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
foldClaudeMdIntoAgentsMd,
|
|
13
|
+
LEGACY_ENTRY_DOC,
|
|
14
|
+
} from '../../../.agents/scripts/lib/bootstrap/agents-md-fold.js';
|
|
15
|
+
|
|
16
|
+
export const foldClaudeMdIntoAgentsMdStep = {
|
|
17
|
+
version: '2.65.0',
|
|
18
|
+
description:
|
|
19
|
+
'fold the root CLAUDE.md into AGENTS.md and delete it — AGENTS.md is ' +
|
|
20
|
+
'now the entry doc (Story #5410)',
|
|
21
|
+
/**
|
|
22
|
+
* @param {{ projectRoot?: string, fs?: typeof nodeFs }} [ctx]
|
|
23
|
+
* @param {typeof nodeFs} [fsImpl]
|
|
24
|
+
* @returns {boolean}
|
|
25
|
+
*/
|
|
26
|
+
detect(ctx, fsImpl = ctx?.fs ?? nodeFs) {
|
|
27
|
+
const root = ctx?.projectRoot ?? process.cwd();
|
|
28
|
+
return fsImpl.existsSync(path.join(root, LEGACY_ENTRY_DOC));
|
|
29
|
+
},
|
|
30
|
+
/**
|
|
31
|
+
* @param {{ projectRoot?: string, fs?: typeof nodeFs }} [ctx]
|
|
32
|
+
* @param {typeof nodeFs} [fsImpl]
|
|
33
|
+
* @returns {void}
|
|
34
|
+
*/
|
|
35
|
+
apply(ctx, fsImpl = ctx?.fs ?? nodeFs) {
|
|
36
|
+
foldClaudeMdIntoAgentsMd(ctx?.projectRoot ?? process.cwd(), fsImpl);
|
|
37
|
+
},
|
|
38
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mandrel",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.65.0",
|
|
4
4
|
"description": "Claude Code-first opinionated workflow framework: instructions, skills, rules, and SDLC workflows that govern AI coding assistants.",
|
|
5
5
|
"files": [
|
|
6
6
|
".agents/",
|
|
@@ -102,6 +102,7 @@
|
|
|
102
102
|
"@cucumber/gherkin": "^42.0.1",
|
|
103
103
|
"c8": "^11.0.0",
|
|
104
104
|
"chokidar": "^5.0.0",
|
|
105
|
+
"fast-check": "^4.10.2",
|
|
105
106
|
"husky": "^9.1.7",
|
|
106
107
|
"jscpd": "^4.2.5",
|
|
107
108
|
"knip": "^6.17.1",
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Close-scope lens diff-floor: a diff under the changed-line floor that
|
|
3
|
-
* touches no sensitive path skips lens materialization. Fail-open — every
|
|
4
|
-
* degraded input (unknown count, unreadable manifest, floor `0`) means "do
|
|
5
|
-
* not skip". Sensitive classes come from the same selector as review depth,
|
|
6
|
-
* so the two never disagree. Total; no I/O beyond the injected git spawn.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { gitSpawn } from '../git-utils.js';
|
|
10
|
-
import { readNumstatRows } from '../orchestration/diff-magnitude.js';
|
|
11
|
-
import { selectSensitivePathClasses } from './selector.js';
|
|
12
|
-
|
|
13
|
-
/** Measured: zero-yield closes clustered well under this size. */
|
|
14
|
-
export const DEFAULT_LENS_DIFF_FLOOR = 40;
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Whole-diff additions + deletions (no companion exemption: the question is
|
|
18
|
-
* "is this diff small"). Shares the numstat parse with the light path's
|
|
19
|
-
* backstop. `null` on any git failure; binary rows count 0.
|
|
20
|
-
*
|
|
21
|
-
* @param {{
|
|
22
|
-
* baseRef: string,
|
|
23
|
-
* headRef: string,
|
|
24
|
-
* cwd?: string,
|
|
25
|
-
* gitSpawnFn?: typeof gitSpawn,
|
|
26
|
-
* }} args
|
|
27
|
-
* @returns {number|null}
|
|
28
|
-
*/
|
|
29
|
-
export function countChangedLines({
|
|
30
|
-
baseRef,
|
|
31
|
-
headRef,
|
|
32
|
-
cwd = process.cwd(),
|
|
33
|
-
gitSpawnFn = gitSpawn,
|
|
34
|
-
} = {}) {
|
|
35
|
-
const rows = readNumstatRows({ baseRef, headRef, cwd, gitSpawnFn });
|
|
36
|
-
if (rows === null) return null;
|
|
37
|
-
return rows.reduce((total, row) => total + row.additions + row.deletions, 0);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Skip only when the floor is enabled, the count is known and below it, and
|
|
42
|
-
* no sensitive-path class matches; every other state names its reason.
|
|
43
|
-
*
|
|
44
|
-
* @param {{
|
|
45
|
-
* changedFiles?: string[]|null,
|
|
46
|
-
* changedLineCount?: number|null,
|
|
47
|
-
* floor?: number,
|
|
48
|
-
* injectedRules?: object,
|
|
49
|
-
* selectSensitivePathClassesFn?: typeof selectSensitivePathClasses,
|
|
50
|
-
* }} [input]
|
|
51
|
-
* @returns {{
|
|
52
|
-
* skip: boolean,
|
|
53
|
-
* reason: 'floor-disabled'|'line-count-unknown'|'at-or-above-floor'|'sensitive-classes-unknown'|'sensitive-path-hit'|'below-floor',
|
|
54
|
-
* floor: number,
|
|
55
|
-
* changedLineCount: number|null,
|
|
56
|
-
* sensitiveClasses: string[],
|
|
57
|
-
* }}
|
|
58
|
-
*/
|
|
59
|
-
export function evaluateLensDiffFloor(input = {}) {
|
|
60
|
-
const floorRaw = input.floor;
|
|
61
|
-
const floor =
|
|
62
|
-
typeof floorRaw === 'number' && Number.isInteger(floorRaw) && floorRaw >= 0
|
|
63
|
-
? floorRaw
|
|
64
|
-
: DEFAULT_LENS_DIFF_FLOOR;
|
|
65
|
-
const count =
|
|
66
|
-
typeof input.changedLineCount === 'number' &&
|
|
67
|
-
Number.isFinite(input.changedLineCount) &&
|
|
68
|
-
input.changedLineCount >= 0
|
|
69
|
-
? Math.floor(input.changedLineCount)
|
|
70
|
-
: null;
|
|
71
|
-
const verdict = (skip, reason, sensitiveClasses = []) => ({
|
|
72
|
-
skip,
|
|
73
|
-
reason,
|
|
74
|
-
floor,
|
|
75
|
-
changedLineCount: count,
|
|
76
|
-
sensitiveClasses,
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
if (floor <= 0) return verdict(false, 'floor-disabled');
|
|
80
|
-
if (count === null) return verdict(false, 'line-count-unknown');
|
|
81
|
-
if (count >= floor) return verdict(false, 'at-or-above-floor');
|
|
82
|
-
|
|
83
|
-
const select =
|
|
84
|
-
input.selectSensitivePathClassesFn ?? selectSensitivePathClasses;
|
|
85
|
-
let classes;
|
|
86
|
-
try {
|
|
87
|
-
classes = select({
|
|
88
|
-
changedFiles: Array.isArray(input.changedFiles) ? input.changedFiles : [],
|
|
89
|
-
injectedRules: input.injectedRules,
|
|
90
|
-
});
|
|
91
|
-
} catch {
|
|
92
|
-
return verdict(false, 'sensitive-classes-unknown');
|
|
93
|
-
}
|
|
94
|
-
const matched = Array.isArray(classes) ? classes : [];
|
|
95
|
-
if (matched.length > 0) {
|
|
96
|
-
return verdict(false, 'sensitive-path-hit', matched);
|
|
97
|
-
}
|
|
98
|
-
return verdict(true, 'below-floor');
|
|
99
|
-
}
|
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `runAuditSuite`: owns the audit envelope shape (`metadata`, `findings`,
|
|
3
|
-
* `workflows`) and the per-audit fan-out; invoked via the barrel.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import fs from 'node:fs/promises';
|
|
7
|
-
import path from 'node:path';
|
|
8
|
-
import { getPaths, PROJECT_ROOT, resolveConfig } from '../config-resolver.js';
|
|
9
|
-
import { ValidationError } from '../errors/index.js';
|
|
10
|
-
import { aggregateSummary } from './findings.js';
|
|
11
|
-
import { summarizeWorkflow } from './frontmatter.js';
|
|
12
|
-
import { applySubstitutions, computeAllowedKeys } from './substitutions.js';
|
|
13
|
-
import { defaultWriteArtifact, loadWorkflow } from './workflow-loader.js';
|
|
14
|
-
|
|
15
|
-
async function loadRules(paths) {
|
|
16
|
-
const rulesPath = path.join(
|
|
17
|
-
PROJECT_ROOT,
|
|
18
|
-
paths.schemasRoot,
|
|
19
|
-
'audit-rules.json',
|
|
20
|
-
);
|
|
21
|
-
const rulesContent = await fs.readFile(rulesPath, 'utf8');
|
|
22
|
-
return JSON.parse(rulesContent);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function rejectUnknownKeys(allowedKeys, callerSubstitutions) {
|
|
26
|
-
const unknownKeys = Object.keys(callerSubstitutions).filter(
|
|
27
|
-
(k) => !allowedKeys.has(k),
|
|
28
|
-
);
|
|
29
|
-
if (unknownKeys.length === 0) return;
|
|
30
|
-
const allowedList = [...allowedKeys].sort().join(', ');
|
|
31
|
-
throw new ValidationError(
|
|
32
|
-
`Unknown substitution key(s): ${unknownKeys.join(', ')}. Allowed for this call: ${allowedList}.`,
|
|
33
|
-
{ unknownKeys, allowedKeys: [...allowedKeys] },
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function emptyEnvelope(auditWorkflows) {
|
|
38
|
-
return {
|
|
39
|
-
metadata: {
|
|
40
|
-
timestamp: new Date().toISOString(),
|
|
41
|
-
auditsRequested: auditWorkflows,
|
|
42
|
-
auditsRun: [],
|
|
43
|
-
summary: { critical: 0, high: 0, medium: 0, low: 0 },
|
|
44
|
-
},
|
|
45
|
-
findings: [],
|
|
46
|
-
workflows: [],
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function notDefinedFinding(auditName) {
|
|
51
|
-
return {
|
|
52
|
-
error: true,
|
|
53
|
-
finding: {
|
|
54
|
-
audit: auditName,
|
|
55
|
-
severity: 'low',
|
|
56
|
-
message: `Requested audit workflow '${auditName}' is not defined in audit-rules.json.`,
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function notFoundFinding(auditName) {
|
|
62
|
-
return {
|
|
63
|
-
error: true,
|
|
64
|
-
finding: {
|
|
65
|
-
audit: auditName,
|
|
66
|
-
severity: 'low',
|
|
67
|
-
message: `Audit workflow '${auditName}.md' not found in workflows directory.`,
|
|
68
|
-
},
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async function processAudit({
|
|
73
|
-
auditName,
|
|
74
|
-
validAudits,
|
|
75
|
-
loader,
|
|
76
|
-
workflowsDir,
|
|
77
|
-
effectiveSubstitutions,
|
|
78
|
-
}) {
|
|
79
|
-
if (!validAudits.includes(auditName)) {
|
|
80
|
-
return notDefinedFinding(auditName);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const workflow = await loader(auditName, workflowsDir);
|
|
84
|
-
if (!workflow) {
|
|
85
|
-
return notFoundFinding(auditName);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const substituted = applySubstitutions(
|
|
89
|
-
workflow.content,
|
|
90
|
-
effectiveSubstitutions,
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
return {
|
|
94
|
-
success: true,
|
|
95
|
-
auditName,
|
|
96
|
-
workflowPath: workflow.path ?? null,
|
|
97
|
-
workflowContent: substituted,
|
|
98
|
-
summary: summarizeWorkflow(workflow.content),
|
|
99
|
-
byteSize: Buffer.byteLength(substituted, 'utf8'),
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async function reduceResults({
|
|
104
|
-
results,
|
|
105
|
-
envelope,
|
|
106
|
-
artifactPrefix,
|
|
107
|
-
effectiveArtifactsDir,
|
|
108
|
-
writeArtifact,
|
|
109
|
-
}) {
|
|
110
|
-
for (const result of results) {
|
|
111
|
-
if (result.error) {
|
|
112
|
-
envelope.findings.push(result.finding);
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
if (!result.success) continue;
|
|
116
|
-
|
|
117
|
-
envelope.metadata.auditsRun.push(result.auditName);
|
|
118
|
-
let artifactPath = null;
|
|
119
|
-
if (artifactPrefix) {
|
|
120
|
-
const fileName = `audit-${artifactPrefix}-${result.auditName}.md`;
|
|
121
|
-
artifactPath = await writeArtifact(
|
|
122
|
-
effectiveArtifactsDir,
|
|
123
|
-
fileName,
|
|
124
|
-
result.workflowContent,
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
envelope.workflows.push({
|
|
128
|
-
audit: result.auditName,
|
|
129
|
-
path: result.workflowPath,
|
|
130
|
-
summary: result.summary,
|
|
131
|
-
byteSize: result.byteSize,
|
|
132
|
-
artifactPath,
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Resolve registered audit workflows into slim descriptors; with
|
|
139
|
-
* `artifactPrefix`, full bodies are written to disk instead of travelling in
|
|
140
|
-
* the envelope. Substitution keys beyond the built-ins must be declared as
|
|
141
|
-
* `substitutionKeys` in audit-rules.json, else ValidationError.
|
|
142
|
-
*
|
|
143
|
-
* @param {object} opts
|
|
144
|
-
* @param {string[]} opts.auditWorkflows
|
|
145
|
-
* @param {Record<string,string>} [opts.substitutions]
|
|
146
|
-
* @param {string} [opts.artifactPrefix] - writes `audit-<prefix>-<audit>.md`.
|
|
147
|
-
* @param {string} [opts.artifactsDir] - defaults to `<auditOutputDir>`.
|
|
148
|
-
* @param {Function} [opts.injectedLoadWorkflow]
|
|
149
|
-
* @param {object} [opts.injectedRules]
|
|
150
|
-
* @param {Function} [opts.injectedWriteArtifact]
|
|
151
|
-
* @returns {Promise<object>} Aggregated audit results.
|
|
152
|
-
*/
|
|
153
|
-
export async function runAuditSuite({
|
|
154
|
-
auditWorkflows,
|
|
155
|
-
substitutions,
|
|
156
|
-
artifactPrefix,
|
|
157
|
-
artifactsDir,
|
|
158
|
-
injectedLoadWorkflow,
|
|
159
|
-
injectedRules,
|
|
160
|
-
injectedWriteArtifact,
|
|
161
|
-
}) {
|
|
162
|
-
const config = resolveConfig();
|
|
163
|
-
const paths = getPaths(config);
|
|
164
|
-
const callerSubstitutions = substitutions ?? {};
|
|
165
|
-
const rules = injectedRules ?? (await loadRules(paths));
|
|
166
|
-
|
|
167
|
-
const allowedKeys = computeAllowedKeys(rules, auditWorkflows);
|
|
168
|
-
rejectUnknownKeys(allowedKeys, callerSubstitutions);
|
|
169
|
-
|
|
170
|
-
const effectiveSubstitutions = {
|
|
171
|
-
auditOutputDir: paths.auditOutputDir,
|
|
172
|
-
...callerSubstitutions,
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
const validAudits = Object.keys(rules.audits || {});
|
|
176
|
-
const envelope = emptyEnvelope(auditWorkflows);
|
|
177
|
-
const workflowsDir = path.join(PROJECT_ROOT, paths.workflowsRoot);
|
|
178
|
-
const effectiveArtifactsDir =
|
|
179
|
-
artifactsDir ?? path.join(PROJECT_ROOT, paths.auditOutputDir);
|
|
180
|
-
const writeArtifact = injectedWriteArtifact ?? defaultWriteArtifact;
|
|
181
|
-
const loader = injectedLoadWorkflow ?? loadWorkflow;
|
|
182
|
-
|
|
183
|
-
const results = await Promise.all(
|
|
184
|
-
auditWorkflows.map((auditName) =>
|
|
185
|
-
processAudit({
|
|
186
|
-
auditName,
|
|
187
|
-
validAudits,
|
|
188
|
-
loader,
|
|
189
|
-
workflowsDir,
|
|
190
|
-
effectiveSubstitutions,
|
|
191
|
-
}),
|
|
192
|
-
),
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
await reduceResults({
|
|
196
|
-
results,
|
|
197
|
-
envelope,
|
|
198
|
-
artifactPrefix,
|
|
199
|
-
effectiveArtifactsDir,
|
|
200
|
-
writeArtifact,
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
envelope.metadata.summary = aggregateSummary(envelope.findings);
|
|
204
|
-
return envelope;
|
|
205
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `{{key}}` substitution for audit workflow bodies, plus the CLI glue and the
|
|
3
|
-
* per-run allowed-key set.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { ValidationError } from '../errors/index.js';
|
|
7
|
-
|
|
8
|
-
export const BUILT_IN_SUBSTITUTION_KEYS = Object.freeze([
|
|
9
|
-
'auditOutputDir',
|
|
10
|
-
'ticketId',
|
|
11
|
-
'baseBranch',
|
|
12
|
-
// Newline-joined change set; when unsubstituted, lens templates read the
|
|
13
|
-
// literal as "no scope filter".
|
|
14
|
-
'changedFiles',
|
|
15
|
-
]);
|
|
16
|
-
|
|
17
|
-
function escapeRegExp(str) {
|
|
18
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Unrecognised placeholders are left intact; keys are validated upstream.
|
|
23
|
-
*
|
|
24
|
-
* @param {string} content
|
|
25
|
-
* @param {Record<string, string>} substitutions
|
|
26
|
-
* @returns {string}
|
|
27
|
-
*/
|
|
28
|
-
export function applySubstitutions(content, substitutions) {
|
|
29
|
-
let out = content;
|
|
30
|
-
for (const [key, value] of Object.entries(substitutions)) {
|
|
31
|
-
out = out.replace(
|
|
32
|
-
new RegExp(`\\{\\{${escapeRegExp(key)}\\}\\}`, 'g'),
|
|
33
|
-
value,
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
return out;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Throws {@link ValidationError} on a missing `=` or empty key.
|
|
41
|
-
*
|
|
42
|
-
* @param {string[]} [pairs]
|
|
43
|
-
* @returns {Record<string, string>}
|
|
44
|
-
*/
|
|
45
|
-
export function parseSubstitutionPairs(pairs = []) {
|
|
46
|
-
const out = {};
|
|
47
|
-
for (const entry of pairs) {
|
|
48
|
-
const eq = entry.indexOf('=');
|
|
49
|
-
if (eq <= 0) {
|
|
50
|
-
throw new ValidationError(
|
|
51
|
-
`Invalid --substitution "${entry}"; expected key=value.`,
|
|
52
|
-
{ entry },
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
const key = entry.slice(0, eq);
|
|
56
|
-
const value = entry.slice(eq + 1);
|
|
57
|
-
out[key] = value;
|
|
58
|
-
}
|
|
59
|
-
return out;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Fill `ticketId`/`baseBranch` from flags in place, unless an explicit
|
|
64
|
-
* `--substitution` already set them.
|
|
65
|
-
*
|
|
66
|
-
* @param {Record<string, unknown>} values
|
|
67
|
-
* @param {Record<string, string|undefined>} substitutions
|
|
68
|
-
*/
|
|
69
|
-
export function applyImplicitSubstitutions(values, substitutions) {
|
|
70
|
-
if (values.ticket && substitutions.ticketId === undefined) {
|
|
71
|
-
substitutions.ticketId = String(values.ticket);
|
|
72
|
-
}
|
|
73
|
-
if (values['base-branch'] && substitutions.baseBranch === undefined) {
|
|
74
|
-
substitutions.baseBranch = values['base-branch'];
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Built-ins plus each requested audit's declared keys; unregistered audits
|
|
80
|
-
* are rejected elsewhere.
|
|
81
|
-
*
|
|
82
|
-
* @param {{ audits?: Record<string, { substitutionKeys?: string[] }> }} rules
|
|
83
|
-
* @param {string[]} auditWorkflows
|
|
84
|
-
* @returns {Set<string>}
|
|
85
|
-
*/
|
|
86
|
-
export function computeAllowedKeys(rules, auditWorkflows) {
|
|
87
|
-
const allowed = new Set(BUILT_IN_SUBSTITUTION_KEYS);
|
|
88
|
-
for (const auditName of auditWorkflows) {
|
|
89
|
-
const entry = rules.audits?.[auditName];
|
|
90
|
-
if (!entry) continue;
|
|
91
|
-
for (const k of entry.substitutionKeys ?? []) {
|
|
92
|
-
allowed.add(k);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return allowed;
|
|
96
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Filesystem IO for audit workflows, kept out of the runner so tests can
|
|
3
|
-
* inject stubs.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import fs from 'node:fs/promises';
|
|
7
|
-
import path from 'node:path';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* `null` when missing; the runner records a finding rather than failing.
|
|
11
|
-
*
|
|
12
|
-
* @param {string} auditName
|
|
13
|
-
* @param {string} workflowsDir absolute path to the workflows root
|
|
14
|
-
* @returns {Promise<{ path: string, content: string } | null>}
|
|
15
|
-
*/
|
|
16
|
-
export async function loadWorkflow(auditName, workflowsDir) {
|
|
17
|
-
const workflowPath = path.join(workflowsDir, `${auditName}.md`);
|
|
18
|
-
try {
|
|
19
|
-
const content = await fs.readFile(workflowPath, 'utf8');
|
|
20
|
-
return { path: workflowPath, content };
|
|
21
|
-
} catch {
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* @param {string} artifactsDir
|
|
28
|
-
* @param {string} fileName
|
|
29
|
-
* @param {string} content
|
|
30
|
-
* @returns {Promise<string>}
|
|
31
|
-
*/
|
|
32
|
-
export async function defaultWriteArtifact(artifactsDir, fileName, content) {
|
|
33
|
-
await fs.mkdir(artifactsDir, { recursive: true });
|
|
34
|
-
const fullPath = path.join(artifactsDir, fileName);
|
|
35
|
-
await fs.writeFile(fullPath, content, 'utf8');
|
|
36
|
-
return fullPath;
|
|
37
|
-
}
|