mandrel 2.55.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 +4 -30
- package/.agents/docs/configuration.md +11 -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/rules/ci-remediation.md +39 -21
- package/.agents/schemas/agentrc.schema.json +28 -185
- package/.agents/schemas/story-deliver-terminal.schema.json +1 -1
- package/.agents/scripts/acceptance-eval.js +107 -17
- package/.agents/scripts/audit-to-stories.js +222 -75
- 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/file-ci-gap.js +306 -0
- package/.agents/scripts/lib/audit-suite/checklist-threading.js +15 -2
- package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +25 -1
- package/.agents/scripts/lib/audit-to-stories/dedupe-against-github.js +40 -52
- package/.agents/scripts/lib/audit-to-stories/finding-adapter.js +5 -1
- package/.agents/scripts/lib/audit-to-stories/issue-corpus.js +162 -0
- package/.agents/scripts/lib/audit-to-stories/issues-file.js +121 -0
- package/.agents/scripts/lib/audit-to-stories/ledger-commit.js +1 -1
- package/.agents/scripts/lib/audit-to-stories/ledger-record.js +126 -0
- package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +11 -0
- 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 +49 -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/feedback-loop/graduator-core.js +53 -13
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +71 -25
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +18 -25
- package/.agents/scripts/lib/{audit-to-stories/ledger.js → findings/audit-ledger.js} +131 -24
- package/.agents/scripts/lib/findings/route-finding.js +38 -0
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/github/framework-repo.js +148 -2
- package/.agents/scripts/lib/label-constants.js +6 -1
- package/.agents/scripts/lib/observability/runtime-friction.js +1 -1
- package/.agents/scripts/lib/observability/source-classifier.js +2 -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/ci-gap-intake.js +605 -0
- package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +13 -8
- 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/audit-provenance.js +197 -0
- 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 +133 -299
- 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/run-epilogue.js +4 -4
- 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/story-follow-ups.js +32 -20
- 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/pr-watch-with-update.js +3 -2
- 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/audit-to-stories.md +63 -27
- 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 +56 -62
- package/.agents/workflows/helpers/deliver-story.md +9 -13
- package/.agents/workflows/helpers/plan-reference.md +132 -196
- package/.agents/workflows/mandrel-plan.md +28 -41
- package/.agents/workflows/memory-consolidate.md +9 -13
- package/docs/CHANGELOG.md +33 -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,306 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* file-ci-gap.js — the CI-remediation Option-2 filing command.
|
|
4
|
+
*
|
|
5
|
+
* `rules/ci-remediation.md` sends three verdicts here — `pre-existing`,
|
|
6
|
+
* `capacity`, `unreproducible-tier` — each meaning "this red check is real,
|
|
7
|
+
* and fixing it is not this delivery's job". The rule used to say "file a
|
|
8
|
+
* `meta::framework-gap` issue" and stop, leaving the agent to hand-run
|
|
9
|
+
* `gh issue create` wherever it was standing. This is the mechanism behind
|
|
10
|
+
* that sentence: evidence from the CI digest, ownership routing, fingerprint
|
|
11
|
+
* dedup, the `friction` comment, and the `agent::blocked` flip in one call.
|
|
12
|
+
*
|
|
13
|
+
* It files an **intake** issue, never a Story: `/mandrel-plan <id>` graduates
|
|
14
|
+
* it on the next planning pass. Delivery never blocks on planning — see
|
|
15
|
+
* `lib/orchestration/ci-gap-intake.js` for why that split is load-bearing.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { parseArgs } from 'node:util';
|
|
19
|
+
|
|
20
|
+
import { runAsCli } from './lib/cli-utils.js';
|
|
21
|
+
import { resolveConfig } from './lib/config-resolver.js';
|
|
22
|
+
import {
|
|
23
|
+
createFollowUpIssue,
|
|
24
|
+
ensureIssueLabels,
|
|
25
|
+
updateFollowUpIssue,
|
|
26
|
+
} from './lib/feedback-loop/graduator-core.js';
|
|
27
|
+
import { issueNumberFromUrl } from './lib/feedback-loop/retro-proposals-graduator.js';
|
|
28
|
+
import {
|
|
29
|
+
resolveOwnershipRepos,
|
|
30
|
+
routeOwnership,
|
|
31
|
+
} from './lib/github/framework-repo.js';
|
|
32
|
+
import { Logger } from './lib/Logger.js';
|
|
33
|
+
import {
|
|
34
|
+
fileCiGapIntake,
|
|
35
|
+
INTAKE_VERDICTS,
|
|
36
|
+
REFUSED_VERDICT,
|
|
37
|
+
} from './lib/orchestration/ci-gap-intake.js';
|
|
38
|
+
import { readCiDigest } from './lib/orchestration/ci-rerun-guard.js';
|
|
39
|
+
import {
|
|
40
|
+
STATE_LABELS,
|
|
41
|
+
transitionTicketState,
|
|
42
|
+
upsertStructuredComment,
|
|
43
|
+
} from './lib/orchestration/ticketing.js';
|
|
44
|
+
import { createProvider } from './lib/provider-factory.js';
|
|
45
|
+
|
|
46
|
+
const USAGE = {
|
|
47
|
+
invocation:
|
|
48
|
+
'node .agents/scripts/file-ci-gap.js --story <id> --verdict <verdict> --owner <bucket> [--evidence "<proof reading>"] [--pr <n>] [--block] [--dry-run]',
|
|
49
|
+
summary:
|
|
50
|
+
'File (or update) the CI-gap intake issue for an Option-2 verdict in .agents/rules/ci-remediation.md, routed to the repository that owns the fault and deduped by failure signature.',
|
|
51
|
+
flags: [
|
|
52
|
+
[
|
|
53
|
+
'--story <id>',
|
|
54
|
+
'Story the red check blocked (required — keys the CI digest).',
|
|
55
|
+
],
|
|
56
|
+
[
|
|
57
|
+
'--verdict <verdict>',
|
|
58
|
+
`One of ${INTAKE_VERDICTS.join(' | ')}. "${REFUSED_VERDICT}" is refused: it routes to Option 1, fix at source.`,
|
|
59
|
+
],
|
|
60
|
+
[
|
|
61
|
+
'--owner <bucket>',
|
|
62
|
+
'Who owns the fault: consumer | framework | platform. Resolves through github.followUpRepos.*.',
|
|
63
|
+
],
|
|
64
|
+
[
|
|
65
|
+
'--evidence <text>',
|
|
66
|
+
"The verdict's proof reading (the exhausted-limit log line, the failed attach). Recorded in the body.",
|
|
67
|
+
],
|
|
68
|
+
['--pr <n>', 'PR number the red check ran on; recorded as an occurrence.'],
|
|
69
|
+
[
|
|
70
|
+
'--block',
|
|
71
|
+
'Also flip the Story to agent::blocked. Without it the friction comment is posted but the Story is left where it is.',
|
|
72
|
+
],
|
|
73
|
+
['--dry-run', 'Compose the filing and print it; write nothing.'],
|
|
74
|
+
],
|
|
75
|
+
notes: [
|
|
76
|
+
'Requires a CI digest at temp/story-<id>-ci-digest.json — pr-watch-with-update.js --story <id> writes it on the first red.',
|
|
77
|
+
'A repeat occurrence of a known signature UPDATES the existing intake issue rather than opening a second one.',
|
|
78
|
+
'The issue it files is intake, not an executable Story: graduate it with /mandrel-plan <issue number>.',
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Wire the live GitHub ports the intake filer writes through.
|
|
84
|
+
*
|
|
85
|
+
* `gh` is the transport rather than the provider facade because a filing can
|
|
86
|
+
* target a repository other than the configured one, and `gh issue create
|
|
87
|
+
* --repo` is the surface that already does that (the graduators file
|
|
88
|
+
* cross-repo the same way).
|
|
89
|
+
*
|
|
90
|
+
* @param {object} opts
|
|
91
|
+
* @returns {object} ports for `fileCiGapIntake`
|
|
92
|
+
*/
|
|
93
|
+
function liveIntakePorts({ provider, searchRepo, cwd, logger }) {
|
|
94
|
+
const labelCache = new Map();
|
|
95
|
+
return {
|
|
96
|
+
searchIssues: (query) =>
|
|
97
|
+
provider.searchIssues({
|
|
98
|
+
query,
|
|
99
|
+
owner: searchRepo.owner,
|
|
100
|
+
repo: searchRepo.repo,
|
|
101
|
+
}),
|
|
102
|
+
createIssue: async ({ owner, repo, title, body, labels }) => {
|
|
103
|
+
// `gh issue create --label <absent>` fails outright, so a brand-new
|
|
104
|
+
// routing label (meta::platform-gap, friction::unreproducible-tier)
|
|
105
|
+
// has to exist before the create — not after it errors.
|
|
106
|
+
const ensured = await ensureIssueLabels({
|
|
107
|
+
owner,
|
|
108
|
+
repo,
|
|
109
|
+
labels,
|
|
110
|
+
labelCache,
|
|
111
|
+
cwd,
|
|
112
|
+
});
|
|
113
|
+
for (const err of ensured.errors) logger?.warn?.(`[file-ci-gap] ${err}`);
|
|
114
|
+
const created = await createFollowUpIssue({
|
|
115
|
+
owner,
|
|
116
|
+
repo,
|
|
117
|
+
title,
|
|
118
|
+
body,
|
|
119
|
+
labels,
|
|
120
|
+
ghPath: 'gh',
|
|
121
|
+
cwd,
|
|
122
|
+
});
|
|
123
|
+
return {
|
|
124
|
+
url: created.url,
|
|
125
|
+
number: created.url ? issueNumberFromUrl(created.url) : null,
|
|
126
|
+
error: created.error,
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
updateIssue: ({ owner, repo, number, body }) =>
|
|
130
|
+
updateFollowUpIssue({ owner, repo, number, body, ghPath: 'gh', cwd }),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Render the `friction` comment the Story carries so the blocker is legible
|
|
136
|
+
* on the ticket itself, not only in the intake issue.
|
|
137
|
+
*
|
|
138
|
+
* @param {object} opts
|
|
139
|
+
* @returns {string}
|
|
140
|
+
*/
|
|
141
|
+
export function renderFrictionComment({ verdict, result, digest }) {
|
|
142
|
+
const target = result.issue?.url ?? result.issue?.number ?? '(not filed)';
|
|
143
|
+
const lines = [
|
|
144
|
+
`### CI gap filed — verdict \`${verdict}\``,
|
|
145
|
+
'',
|
|
146
|
+
`- **Failing check:** \`${digest?.failingCheck ?? 'unknown'}\``,
|
|
147
|
+
`- **Run:** ${digest?.runUrl ?? `run id ${digest?.runId ?? 'unresolved'}`}`,
|
|
148
|
+
`- **Intake issue:** ${target} (${result.decision})`,
|
|
149
|
+
`- **Owner:** \`${result.routing.bucket}\` → \`${result.routing.routedRepo.owner}/${result.routing.routedRepo.repo}\``,
|
|
150
|
+
];
|
|
151
|
+
if (!result.routing.routable) {
|
|
152
|
+
lines.push(
|
|
153
|
+
`- **Routing:** \`unroutable\` — \`${result.routing.missingKey}\` is unset, so the intake issue was filed locally.`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
if (result.routing.deferredFrom) {
|
|
157
|
+
lines.push(
|
|
158
|
+
`- **Routing:** deferred from \`${result.routing.deferredFrom}\` (${result.routing.deferralReason}) — filed locally instead.`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
lines.push(
|
|
162
|
+
'',
|
|
163
|
+
'This verdict does **not** license a re-run of the failed job. Graduate the',
|
|
164
|
+
'intake issue with `/mandrel-plan <issue number>` to turn it into a Story.',
|
|
165
|
+
);
|
|
166
|
+
return lines.join('\n');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* File the CI-gap intake issue for one Story, post the `friction` comment,
|
|
171
|
+
* and optionally flip the Story to `agent::blocked`.
|
|
172
|
+
*
|
|
173
|
+
* Every port is injectable so the unit tests exercise the whole command with
|
|
174
|
+
* no network and no live tracker.
|
|
175
|
+
*
|
|
176
|
+
* @param {object} opts
|
|
177
|
+
* @returns {Promise<object>} the intake result, plus what the command did.
|
|
178
|
+
*/
|
|
179
|
+
export async function runFileCiGap({
|
|
180
|
+
storyId,
|
|
181
|
+
verdict,
|
|
182
|
+
owner: bucket,
|
|
183
|
+
evidence = '',
|
|
184
|
+
prNumber = null,
|
|
185
|
+
dryRun = false,
|
|
186
|
+
block = false,
|
|
187
|
+
config,
|
|
188
|
+
provider,
|
|
189
|
+
ports,
|
|
190
|
+
digest,
|
|
191
|
+
tempRoot,
|
|
192
|
+
cwd = process.cwd(),
|
|
193
|
+
logger = Logger,
|
|
194
|
+
now,
|
|
195
|
+
} = {}) {
|
|
196
|
+
const sid = Number(storyId);
|
|
197
|
+
if (!Number.isInteger(sid) || sid <= 0) {
|
|
198
|
+
throw new Error('--story <id> is required (a positive issue number).');
|
|
199
|
+
}
|
|
200
|
+
const resolved = config ?? resolveConfig();
|
|
201
|
+
const ciDigest = digest ?? readCiDigest({ storyId: sid, tempRoot, cwd });
|
|
202
|
+
if (!ciDigest) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`no CI digest for Story #${sid}. The digest is written by \`pr-watch-with-update.js --story ${sid}\` on the first red; without it there is no run link or failure signature to file.`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const repos = resolveOwnershipRepos(resolved);
|
|
209
|
+
const currentRepo = repos.consumer ?? { owner: 'unknown', repo: 'unknown' };
|
|
210
|
+
// Dedup searches the repository the filing will land in — routing is a pure
|
|
211
|
+
// function, so computing it here and inside the filer cannot disagree.
|
|
212
|
+
const routed = routeOwnership({ bucket, repos, currentRepo });
|
|
213
|
+
const searchRepo = routed.routable ? routed.routedRepo : currentRepo;
|
|
214
|
+
|
|
215
|
+
const ticketing =
|
|
216
|
+
provider ?? (dryRun ? null : (createProvider(resolved) ?? null));
|
|
217
|
+
const livePorts =
|
|
218
|
+
ports ??
|
|
219
|
+
liveIntakePorts({
|
|
220
|
+
provider: ticketing ?? createProvider(resolved),
|
|
221
|
+
searchRepo,
|
|
222
|
+
cwd,
|
|
223
|
+
logger,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const result = await fileCiGapIntake({
|
|
227
|
+
digest: ciDigest,
|
|
228
|
+
verdict,
|
|
229
|
+
bucket,
|
|
230
|
+
evidence,
|
|
231
|
+
repos,
|
|
232
|
+
currentRepo,
|
|
233
|
+
prNumber,
|
|
234
|
+
dryRun,
|
|
235
|
+
ports: livePorts,
|
|
236
|
+
logger,
|
|
237
|
+
now,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const actions = { commented: false, blocked: false };
|
|
241
|
+
if (!dryRun && ticketing) {
|
|
242
|
+
const body = renderFrictionComment({ verdict, result, digest: ciDigest });
|
|
243
|
+
try {
|
|
244
|
+
await upsertStructuredComment(ticketing, sid, 'friction', body);
|
|
245
|
+
actions.commented = true;
|
|
246
|
+
} catch (err) {
|
|
247
|
+
result.errors.push(`friction comment failed: ${err?.message ?? err}`);
|
|
248
|
+
}
|
|
249
|
+
if (block) {
|
|
250
|
+
try {
|
|
251
|
+
await transitionTicketState(ticketing, sid, STATE_LABELS.BLOCKED, {});
|
|
252
|
+
actions.blocked = true;
|
|
253
|
+
} catch (err) {
|
|
254
|
+
result.errors.push(
|
|
255
|
+
`agent::blocked transition failed: ${err?.message ?? err}`,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return { storyId: sid, verdict, ...result, actions };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* CLI entrypoint.
|
|
266
|
+
*
|
|
267
|
+
* @returns {Promise<void>}
|
|
268
|
+
*/
|
|
269
|
+
async function main() {
|
|
270
|
+
const { values } = parseArgs({
|
|
271
|
+
args: process.argv.slice(2),
|
|
272
|
+
options: {
|
|
273
|
+
story: { type: 'string' },
|
|
274
|
+
verdict: { type: 'string' },
|
|
275
|
+
owner: { type: 'string' },
|
|
276
|
+
evidence: { type: 'string' },
|
|
277
|
+
pr: { type: 'string' },
|
|
278
|
+
block: { type: 'boolean', default: false },
|
|
279
|
+
'dry-run': { type: 'boolean', default: false },
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const result = await runFileCiGap({
|
|
284
|
+
storyId: values.story,
|
|
285
|
+
verdict: values.verdict,
|
|
286
|
+
owner: values.owner,
|
|
287
|
+
evidence: values.evidence ?? '',
|
|
288
|
+
prNumber: values.pr ? Number(values.pr) : null,
|
|
289
|
+
dryRun: values['dry-run'],
|
|
290
|
+
block: values.block,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Single-line JSON per the script-output contract — an orchestrator parses
|
|
294
|
+
// this, and a pretty dump is noise in a delivery transcript.
|
|
295
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
296
|
+
for (const err of result.errors) {
|
|
297
|
+
Logger.error(`[file-ci-gap] ${err}`);
|
|
298
|
+
}
|
|
299
|
+
if (result.errors.length > 0) process.exitCode = 1;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
runAsCli(import.meta.url, main, {
|
|
303
|
+
source: 'file-ci-gap',
|
|
304
|
+
errorPrefix: '[file-ci-gap]',
|
|
305
|
+
usage: USAGE,
|
|
306
|
+
});
|
|
@@ -41,16 +41,29 @@ import path from 'node:path';
|
|
|
41
41
|
import { AUDIT_LENSES } from '../audit-to-stories/audit-lenses.js';
|
|
42
42
|
import { getPaths, PROJECT_ROOT, resolveConfig } from '../config-resolver.js';
|
|
43
43
|
import { Logger } from '../Logger.js';
|
|
44
|
-
import { estimateTokens } from '../orchestration/spec-spill.js';
|
|
45
44
|
import {
|
|
46
45
|
changeSetLacksSiblingTest,
|
|
47
46
|
matchesAnyFilePattern,
|
|
48
47
|
resolveLensTier,
|
|
49
48
|
} from './selector.js';
|
|
50
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Rough token estimate: ~4 characters per token. Deliberately cheap and
|
|
52
|
+
* deterministic — this budget is the one surviving ceiling that speaks in
|
|
53
|
+
* tokens (Story #5312 deleted the plan-time sizing ceilings that shared the
|
|
54
|
+
* estimator), so the approximation matters far less than the payload and
|
|
55
|
+
* the cap agreeing on one number.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} text
|
|
58
|
+
* @returns {number}
|
|
59
|
+
*/
|
|
60
|
+
function estimateTokens(text) {
|
|
61
|
+
return Math.ceil(String(text ?? '').length / 4);
|
|
62
|
+
}
|
|
63
|
+
|
|
51
64
|
/**
|
|
52
65
|
* Hard cap on the assembled checklist payload, in the ≈4-char/token estimate
|
|
53
|
-
*
|
|
66
|
+
* above ({@link estimateTokens}). Generous
|
|
54
67
|
* relative to the real checklist sizes (each distilled lens checklist is
|
|
55
68
|
* ~130–190 tokens, and at most the seven local lenses can match), so a normal
|
|
56
69
|
* Story is never truncated — the cap is a safety ceiling against a pathological
|
|
@@ -26,13 +26,14 @@
|
|
|
26
26
|
* label spelling — so a rename still lands in one place.
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
+
import { auditLabelFooter } from '../findings/route-finding.js';
|
|
29
30
|
import {
|
|
30
31
|
AGENT_LABELS,
|
|
31
32
|
LABEL_COLORS,
|
|
32
33
|
RISK_LABELS,
|
|
33
34
|
TYPE_LABELS,
|
|
34
35
|
} from '../label-constants.js';
|
|
35
|
-
import { AUDIT_LENSES } from './audit-lenses.js';
|
|
36
|
+
import { AUDIT_LENSES, auditLabelsForFindings } from './audit-lenses.js';
|
|
36
37
|
|
|
37
38
|
/**
|
|
38
39
|
* Per-lens label presentation, keyed by canonical lens name. A lens absent from
|
|
@@ -183,3 +184,26 @@ const DEFINED_NAMES = new Set(AUDIT_LABEL_TAXONOMY.map((l) => l.name));
|
|
|
183
184
|
export function definesAuditLabel(name) {
|
|
184
185
|
return typeof name === 'string' && DEFINED_NAMES.has(name);
|
|
185
186
|
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Render the `audit-labels` footer for a group of findings.
|
|
190
|
+
*
|
|
191
|
+
* The dedup corpus is listed by `audit::*` label, so a Story filed without one
|
|
192
|
+
* is absent from the pool an indexed run matches against — and with an index in
|
|
193
|
+
* play the exact lookup is answered locally and never reaches the provider, so
|
|
194
|
+
* a fingerprint footer alone cannot rescue it. Carrying the labels through the
|
|
195
|
+
* seed is what lets the chained planning path stamp them without the authoring
|
|
196
|
+
* agent being asked to notice them (Story #5307).
|
|
197
|
+
*
|
|
198
|
+
* Lives here rather than beside {@link auditLabelsForFindings} because it needs
|
|
199
|
+
* {@link definesAuditLabel}, and the taxonomy already depends on the lens list —
|
|
200
|
+
* the reverse edge would be a cycle.
|
|
201
|
+
*
|
|
202
|
+
* @param {Array<object>} findings
|
|
203
|
+
* @returns {string} the footer, or '' when no finding resolves to a label.
|
|
204
|
+
*/
|
|
205
|
+
export function auditLabelFooterForFindings(findings) {
|
|
206
|
+
return auditLabelFooter(
|
|
207
|
+
auditLabelsForFindings(findings).filter(definesAuditLabel),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
@@ -27,13 +27,21 @@
|
|
|
27
27
|
* both provenance footers, so `findIssuesByFingerprint` is answered locally and
|
|
28
28
|
* the rate-limited search API is spent only on findings with no exact hit.
|
|
29
29
|
*
|
|
30
|
-
*
|
|
30
|
+
* A caller that already holds the corpus injects it directly as `issues`
|
|
31
|
+
* instead (Story #5301) — the host fetched it by whatever access path it has,
|
|
32
|
+
* which is what lets dedup run on a host with no `gh` CLI at all. That source
|
|
33
|
+
* needs no provider: with an index in play the exact lookup is answered from
|
|
34
|
+
* memory and `findIssuesByFingerprint` is never called, so the port is required
|
|
35
|
+
* only on the un-indexed path where it is genuinely used.
|
|
36
|
+
*
|
|
37
|
+
* Pure orchestration: this module performs no network I/O itself, and reads no
|
|
38
|
+
* file — the caller hands over an array, never a path.
|
|
31
39
|
*/
|
|
32
40
|
|
|
33
41
|
import { routeFinding, semanticKeyFor } from '../findings/route-finding.js';
|
|
34
|
-
import { auditLabelsForFindings } from './audit-lenses.js';
|
|
35
42
|
import { toCanonicalFinding } from './finding-adapter.js';
|
|
36
|
-
import {
|
|
43
|
+
import { prepareDedupRouting } from './issue-corpus.js';
|
|
44
|
+
import { lookupLocally } from './issue-index.js';
|
|
37
45
|
|
|
38
46
|
/**
|
|
39
47
|
* @typedef {object} GroupClassification
|
|
@@ -46,6 +54,11 @@ import { buildIssueIndex, lookupLocally } from './issue-index.js';
|
|
|
46
54
|
/**
|
|
47
55
|
* Render a short, operator-legible reason from a dedup-lookup failure. Pure —
|
|
48
56
|
* no imports, no I/O — so the module stays pure orchestration (Story #4678).
|
|
57
|
+
*
|
|
58
|
+
* Both degrade paths run through here, so the wording an operator reads for a
|
|
59
|
+
* failed index pre-fetch matches the wording for a failed per-group lookup:
|
|
60
|
+
* one vocabulary for "the GitHub read did not complete", whichever read it was.
|
|
61
|
+
*
|
|
49
62
|
* @param {unknown} err
|
|
50
63
|
* @returns {string}
|
|
51
64
|
*/
|
|
@@ -154,31 +167,6 @@ function portsFor(canonical, sha, { searchIssues, semanticPort, index }) {
|
|
|
154
167
|
return exact.length > 0 ? local : withSemantic(local);
|
|
155
168
|
}
|
|
156
169
|
|
|
157
|
-
/**
|
|
158
|
-
* Pre-fetch and index every Issue carrying one of the run's `audit::*` labels.
|
|
159
|
-
*
|
|
160
|
-
* Returns `null` — the un-indexed, per-finding-search path — when no list port
|
|
161
|
-
* is wired, when the run's findings resolve to no canonical lens label, or when
|
|
162
|
-
* the list itself fails. A degraded pre-fetch must cost the run its saving, not
|
|
163
|
-
* its dedup.
|
|
164
|
-
*
|
|
165
|
-
* @param {{ listAuditIssues?: Function, groups: Array<object>,
|
|
166
|
-
* onDegraded?: Function }} params
|
|
167
|
-
* @returns {Promise<object|null>}
|
|
168
|
-
*/
|
|
169
|
-
async function prefetchIssueIndex({ listAuditIssues, groups }) {
|
|
170
|
-
if (typeof listAuditIssues !== 'function') return null;
|
|
171
|
-
const labels = auditLabelsForFindings(
|
|
172
|
-
groups.flatMap((group) => group?.findings ?? []),
|
|
173
|
-
);
|
|
174
|
-
if (labels.length === 0) return null;
|
|
175
|
-
try {
|
|
176
|
-
return buildIssueIndex(await listAuditIssues(labels));
|
|
177
|
-
} catch (_) {
|
|
178
|
-
return null;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
170
|
/**
|
|
183
171
|
* @param {object} params
|
|
184
172
|
* @param {Array<object>} params.groups — output of `groupFindings`.
|
|
@@ -191,6 +179,12 @@ async function prefetchIssueIndex({ listAuditIssues, groups }) {
|
|
|
191
179
|
* Optional list port over the run's `audit::*` labels. When wired, its result
|
|
192
180
|
* is fetched once and indexed, and `provider.findIssuesByFingerprint` is not
|
|
193
181
|
* called at all — the exact lookup is answered from that index.
|
|
182
|
+
* @param {Array<object>} [params.issues]
|
|
183
|
+
* Optional pre-fetched corpus the caller already holds, used in preference to
|
|
184
|
+
* `listAuditIssues`. Supplying it makes `provider` optional: with an index in
|
|
185
|
+
* play no provider read port is ever invoked, which is what lets a host with
|
|
186
|
+
* no `gh` CLI dedup at all (Story #5301). An empty array is a valid corpus —
|
|
187
|
+
* a first sweep — and is NOT read as "no index".
|
|
194
188
|
* @param {(entry: { group: object, reason: string }) => void} [params.onDegraded]
|
|
195
189
|
* Optional sink notified once per group whose dedup lookup could not complete
|
|
196
190
|
* (Story #4678). The group is then classified `create` — a soft-fail, never
|
|
@@ -204,37 +198,31 @@ export async function classifyGroupsAgainstGitHub({
|
|
|
204
198
|
searchCandidates,
|
|
205
199
|
onDegraded,
|
|
206
200
|
listAuditIssues,
|
|
201
|
+
issues,
|
|
207
202
|
}) {
|
|
208
203
|
if (!Array.isArray(groups)) {
|
|
209
204
|
throw new Error('classifyGroupsAgainstGitHub: groups must be an array');
|
|
210
205
|
}
|
|
211
|
-
if (!provider || typeof provider.findIssuesByFingerprint !== 'function') {
|
|
212
|
-
throw new Error(
|
|
213
|
-
'classifyGroupsAgainstGitHub: provider.findIssuesByFingerprint is required',
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
206
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
207
|
+
const { routing, summary, error } = await prepareDedupRouting({
|
|
208
|
+
groups,
|
|
209
|
+
provider,
|
|
210
|
+
searchCandidates,
|
|
211
|
+
listAuditIssues,
|
|
212
|
+
issues,
|
|
213
|
+
});
|
|
214
|
+
if (error) {
|
|
215
|
+
// Same vocabulary as a per-group failure, but deliberately NOT counted as
|
|
216
|
+
// a degraded group: the count names groups classified without a check, and
|
|
217
|
+
// every group still gets one here, off the per-finding search path. Until
|
|
218
|
+
// Story #5301 this failure was swallowed whole, so the operator saw only
|
|
219
|
+
// the downstream per-group degradation and could not tell what caused it.
|
|
220
|
+
const reason = `issue-index pre-fetch failed: ${describeDegradeReason(error)}`;
|
|
221
|
+
summary.dedupDegraded.indexPrefetch = reason;
|
|
222
|
+
if (typeof onDegraded === 'function') onDegraded({ group: null, reason });
|
|
223
|
+
}
|
|
230
224
|
|
|
231
225
|
const classifications = [];
|
|
232
|
-
const summary = {
|
|
233
|
-
create: 0,
|
|
234
|
-
skipOpen: 0,
|
|
235
|
-
skipReoccurring: 0,
|
|
236
|
-
dedupDegraded: { count: 0, groups: [] },
|
|
237
|
-
};
|
|
238
226
|
|
|
239
227
|
for (const group of groups) {
|
|
240
228
|
let result;
|
|
@@ -62,10 +62,14 @@ export function fingerprintAuditFinding(finding) {
|
|
|
62
62
|
* shared helper. Stable across a reworded title; used to confirm a dedup
|
|
63
63
|
* match when the fingerprint has drifted (Story #4626).
|
|
64
64
|
*
|
|
65
|
+
* Module-internal since the ledger moved to the shared findings layer and takes
|
|
66
|
+
* its projection injected: `renderSemanticKeyFooter` below is the only caller,
|
|
67
|
+
* and re-exporting it for none would trip `dead-exports:production`.
|
|
68
|
+
*
|
|
65
69
|
* @param {object} finding
|
|
66
70
|
* @returns {string}
|
|
67
71
|
*/
|
|
68
|
-
|
|
72
|
+
function semanticKeyForAuditFinding(finding) {
|
|
69
73
|
return semanticKeyFor(toCanonicalFinding(finding));
|
|
70
74
|
}
|
|
71
75
|
|