mandrel 2.54.0 → 2.56.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 +8 -2
- package/.agents/docs/configuration.md +5 -0
- package/.agents/rules/ci-remediation.md +39 -21
- package/.agents/schemas/agentrc.schema.json +34 -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 +374 -76
- 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/file-ci-gap.js +306 -0
- 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/audit-label-taxonomy.js +25 -1
- package/.agents/scripts/lib/audit-to-stories/dedupe-against-github.js +80 -29
- 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/issue-index.js +83 -0
- package/.agents/scripts/lib/audit-to-stories/issues-file.js +121 -0
- package/.agents/scripts/lib/audit-to-stories/ledger-commit.js +61 -115
- package/.agents/scripts/lib/audit-to-stories/ledger-pr.js +347 -0
- package/.agents/scripts/lib/audit-to-stories/ledger-record.js +126 -0
- package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +169 -44
- package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +11 -0
- 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 +52 -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/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 +42 -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/github/framework-repo.js +148 -2
- package/.agents/scripts/lib/label-constants.js +6 -1
- package/.agents/scripts/lib/observability/source-classifier.js +2 -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/ci-gap-intake.js +605 -0
- package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +13 -8
- 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 +233 -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/audit-provenance.js +197 -0
- 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 +39 -3
- 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 +63 -0
- 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 +63 -42
- 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/story-follow-ups.js +32 -20
- package/.agents/scripts/lib/orchestration/ticket-validator.js +25 -14
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +30 -0
- 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/pr-watch-with-update.js +3 -2
- 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/audit-to-stories.md +63 -27
- 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 +35 -14
- package/.agents/workflows/helpers/deliver-story-reference.md +26 -8
- package/.agents/workflows/helpers/deliver-story.md +15 -12
- package/.agents/workflows/helpers/plan-reference.md +30 -0
- package/.agents/workflows/mandrel-plan.md +10 -13
- package/.agents/workflows/memory-consolidate.md +14 -9
- package/docs/CHANGELOG.md +37 -0
- package/lib/cli/registry.js +64 -21
- package/lib/cli/sync.js +27 -2
- package/package.json +7 -4
|
@@ -36,27 +36,39 @@ import { parseArgs } from 'node:util';
|
|
|
36
36
|
import { buildStoryBody } from './lib/audit-to-stories/build-story-body.js';
|
|
37
37
|
import { classifyGroupsAgainstGitHub } from './lib/audit-to-stories/dedupe-against-github.js';
|
|
38
38
|
import { formatEpicGrouping } from './lib/audit-to-stories/epic-grouping-directive.js';
|
|
39
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
toCanonicalFinding,
|
|
41
|
+
withFingerprints,
|
|
42
|
+
} from './lib/audit-to-stories/finding-adapter.js';
|
|
40
43
|
import { groupFindings } from './lib/audit-to-stories/group-findings.js';
|
|
41
44
|
import {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
writeLedger,
|
|
46
|
-
} from './lib/audit-to-stories/ledger.js';
|
|
45
|
+
loadIssuesFile,
|
|
46
|
+
normaliseIssueHit,
|
|
47
|
+
} from './lib/audit-to-stories/issues-file.js';
|
|
47
48
|
import {
|
|
48
49
|
resolveLedgerSummary,
|
|
49
50
|
runLedgerCommit,
|
|
50
51
|
} from './lib/audit-to-stories/ledger-commit.js';
|
|
52
|
+
import { recordFiledIssues } from './lib/audit-to-stories/ledger-record.js';
|
|
51
53
|
import {
|
|
52
54
|
parseAuditReports,
|
|
53
|
-
|
|
55
|
+
readSeverityTally,
|
|
54
56
|
} from './lib/audit-to-stories/parse-audit-md.js';
|
|
55
57
|
import { buildPlanSeedMarkdown } from './lib/audit-to-stories/seed-from-findings.js';
|
|
56
58
|
import { wireAuditStoryEdges } from './lib/audit-to-stories/wire-dependencies.js';
|
|
57
59
|
import { runAsCli } from './lib/cli-utils.js';
|
|
60
|
+
import {
|
|
61
|
+
DEFAULT_LEDGER_PATH,
|
|
62
|
+
readLedger,
|
|
63
|
+
reconcileLedger,
|
|
64
|
+
writeLedger,
|
|
65
|
+
} from './lib/findings/audit-ledger.js';
|
|
58
66
|
import { searchSemanticCandidates } from './lib/findings/semantic-issue-search.js';
|
|
59
|
-
import {
|
|
67
|
+
import {
|
|
68
|
+
normalizeSeverity,
|
|
69
|
+
SEVERITIES,
|
|
70
|
+
SEVERITY_RANK,
|
|
71
|
+
} from './lib/findings/severity.js';
|
|
60
72
|
import { Logger } from './lib/Logger.js';
|
|
61
73
|
import { parse as parseStoryBody } from './lib/story-body/story-body.js';
|
|
62
74
|
|
|
@@ -209,6 +221,11 @@ function auditReportFailures({
|
|
|
209
221
|
* hand-written report). `allowMissingTally` downgrades ONLY this kind to a
|
|
210
222
|
* warning, for an interactive `--scan` over legacy reports.
|
|
211
223
|
* - `tally-mismatch` — the report says one thing and the parse says another.
|
|
224
|
+
* - `duplicate-tally` — the report declares the tally more than once, so there
|
|
225
|
+
* is no single number to check against. Adopting whichever line the scan
|
|
226
|
+
* reached first would compare the parse against an arbitrary one of two
|
|
227
|
+
* declarations, which is a cross-check in name only. `allowMissingTally`
|
|
228
|
+
* does NOT downgrade it: the report is contradictory, not merely old.
|
|
212
229
|
* - `unresolved-severity` — a finding parsed with no resolvable severity. It
|
|
213
230
|
* is a report defect, never an `unknown` group.
|
|
214
231
|
*
|
|
@@ -232,7 +249,11 @@ function crossCheckReports({ reports, findings, allowMissingTally }) {
|
|
|
232
249
|
for (const report of reports) {
|
|
233
250
|
const own = byReport.get(report.sourceReport) ?? [];
|
|
234
251
|
const parsed = comparableTally(own);
|
|
235
|
-
const
|
|
252
|
+
const {
|
|
253
|
+
tally: reported,
|
|
254
|
+
matches: tallyLines,
|
|
255
|
+
duplicate,
|
|
256
|
+
} = readSeverityTally(report.markdown);
|
|
236
257
|
const sourceReport = report.sourceReport;
|
|
237
258
|
const unresolved = own.filter((f) => !f.severity);
|
|
238
259
|
if (unresolved.length > 0) {
|
|
@@ -244,6 +265,16 @@ function crossCheckReports({ reports, findings, allowMissingTally }) {
|
|
|
244
265
|
titles: unresolved.map((f) => f.title),
|
|
245
266
|
});
|
|
246
267
|
}
|
|
268
|
+
if (duplicate) {
|
|
269
|
+
failures.push({
|
|
270
|
+
sourceReport,
|
|
271
|
+
kind: 'duplicate-tally',
|
|
272
|
+
reported: null,
|
|
273
|
+
parsed,
|
|
274
|
+
tallyLines,
|
|
275
|
+
});
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
247
278
|
if (!reported) {
|
|
248
279
|
const failure = {
|
|
249
280
|
sourceReport,
|
|
@@ -282,7 +313,7 @@ function missingTallyWarning(failure) {
|
|
|
282
313
|
*
|
|
283
314
|
* Pure: returns the message string so the caller owns the single `Logger.warn`.
|
|
284
315
|
*
|
|
285
|
-
* @param {Array<{ sourceReport: string, kind: string, reported: object|null, parsed: object, titles?: string[] }>} failures
|
|
316
|
+
* @param {Array<{ sourceReport: string, kind: string, reported: object|null, parsed: object, titles?: string[], tallyLines?: string[] }>} failures
|
|
286
317
|
* @returns {string}
|
|
287
318
|
*/
|
|
288
319
|
function reportFailureWarning(failures) {
|
|
@@ -290,7 +321,13 @@ function reportFailureWarning(failures) {
|
|
|
290
321
|
const titles = f.titles?.length
|
|
291
322
|
? ` findings=${f.titles.map((t) => `"${t}"`).join(', ')}`
|
|
292
323
|
: '';
|
|
293
|
-
|
|
324
|
+
// A duplicate names the competing lines rather than a tally: there is no
|
|
325
|
+
// single `reported` number to print, and "which two lines" is the whole
|
|
326
|
+
// remedy.
|
|
327
|
+
const declared = f.tallyLines?.length
|
|
328
|
+
? ` declared=${f.tallyLines.map((t) => `"${t}"`).join(' | ')}`
|
|
329
|
+
: '';
|
|
330
|
+
return ` - ${f.sourceReport} [${f.kind}] reported=${formatTally(f.reported)} parsed=${formatTally(f.parsed)}${titles}${declared}`;
|
|
294
331
|
});
|
|
295
332
|
return [
|
|
296
333
|
`audit report cross-check FAILED for ${failures.length} report(s) — the declared severity tally does not match the parsed findings:`,
|
|
@@ -338,25 +375,31 @@ class ProviderUnavailableError extends Error {
|
|
|
338
375
|
}
|
|
339
376
|
|
|
340
377
|
/**
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
378
|
+
* Walk the list endpoint once per label and merge the pages into one
|
|
379
|
+
* deduplicated, normalised issue list.
|
|
380
|
+
*
|
|
381
|
+
* `labels` is an OR across the run's lenses, which the REST list endpoint
|
|
382
|
+
* cannot express in one query (its `labels` parameter is an AND), so one call
|
|
383
|
+
* per label is the narrowest honest read. Each is a paginated **list**, not a
|
|
384
|
+
* search — a different, far larger rate-limit budget.
|
|
344
385
|
*
|
|
345
|
-
* @param {object}
|
|
346
|
-
* @
|
|
386
|
+
* @param {object} provider
|
|
387
|
+
* @param {string[]} labels
|
|
388
|
+
* @returns {Promise<Array<object>>}
|
|
347
389
|
*/
|
|
348
|
-
function
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
390
|
+
async function listIssuesForLabels(provider, labels) {
|
|
391
|
+
const seen = new Map();
|
|
392
|
+
for (const label of labels) {
|
|
393
|
+
const issues = await provider.listIssuesByLabel({
|
|
394
|
+
state: 'all',
|
|
395
|
+
labels: label,
|
|
396
|
+
});
|
|
397
|
+
for (const raw of issues ?? []) {
|
|
398
|
+
const hit = normaliseIssueHit(raw);
|
|
399
|
+
if (!seen.has(hit.number)) seen.set(hit.number, hit);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return [...seen.values()];
|
|
360
403
|
}
|
|
361
404
|
|
|
362
405
|
/**
|
|
@@ -368,7 +411,8 @@ function normaliseIssueHit(hit) {
|
|
|
368
411
|
*
|
|
369
412
|
* @param {object} provider
|
|
370
413
|
* @param {{ owner: string, repo: string }} coords
|
|
371
|
-
* @returns {{ findIssuesByFingerprint: Function,
|
|
414
|
+
* @returns {{ findIssuesByFingerprint: Function, listAuditIssues: Function,
|
|
415
|
+
* searchCandidates: Function }}
|
|
372
416
|
*/
|
|
373
417
|
function buildDedupPorts(provider, { owner, repo }) {
|
|
374
418
|
return {
|
|
@@ -376,6 +420,21 @@ function buildDedupPorts(provider, { owner, repo }) {
|
|
|
376
420
|
const hits = await provider.searchIssues({ query: sha, owner, repo });
|
|
377
421
|
return (hits ?? []).map(normaliseIssueHit);
|
|
378
422
|
},
|
|
423
|
+
/**
|
|
424
|
+
* List every Issue carrying one of the run's `audit::*` labels, once, off
|
|
425
|
+
* the REST list endpoint. The dedup module indexes the result and answers
|
|
426
|
+
* every exact-fingerprint lookup from it, so the rate-limited search API is
|
|
427
|
+
* spent only on findings it has never seen. A provider without the list
|
|
428
|
+
* port yields `null`, which returns dedup to the per-finding search path
|
|
429
|
+
* rather than silently skipping it.
|
|
430
|
+
*
|
|
431
|
+
* @param {string[]} labels
|
|
432
|
+
* @returns {Promise<Array<object>|null>}
|
|
433
|
+
*/
|
|
434
|
+
async listAuditIssues(labels) {
|
|
435
|
+
if (typeof provider.listIssuesByLabel !== 'function') return null;
|
|
436
|
+
return listIssuesForLabels(provider, labels);
|
|
437
|
+
},
|
|
379
438
|
async searchCandidates(finding) {
|
|
380
439
|
// Wire the shared semantic search onto the provider's full-text
|
|
381
440
|
// issue search (open + closed) so route-finding's Stage-1 pass runs.
|
|
@@ -576,6 +635,136 @@ function dedupDegradedWarning(entries) {
|
|
|
576
635
|
);
|
|
577
636
|
}
|
|
578
637
|
|
|
638
|
+
/**
|
|
639
|
+
* Render the operator-visible line naming a host-supplied index and its size.
|
|
640
|
+
*
|
|
641
|
+
* Always emitted for a `--issues-file` run, because "how many issues did you
|
|
642
|
+
* actually check against" is the one number that separates a real dedup from a
|
|
643
|
+
* plan that merely looks checked. At zero it is the load-bearing case: an empty
|
|
644
|
+
* corpus is a legitimate first sweep AND exactly what a broken fetch writes, so
|
|
645
|
+
* the operator — not the run — decides which this was. Deliberately distinct in
|
|
646
|
+
* wording from both `dedupSkippedWarning` and `dedupDegradedWarning` so the
|
|
647
|
+
* three are never confused in a scrollback.
|
|
648
|
+
*
|
|
649
|
+
* @param {{ source?: string, size?: number }} dedupIndex
|
|
650
|
+
* @returns {string}
|
|
651
|
+
*/
|
|
652
|
+
function dedupIndexWarning({ size = 0 } = {}) {
|
|
653
|
+
if (size === 0) {
|
|
654
|
+
return (
|
|
655
|
+
'dedup index: 0 issues supplied via --issues-file. Dedup DID run and ' +
|
|
656
|
+
'every group is correctly "create" — but that is also what a fetch that ' +
|
|
657
|
+
'returned nothing looks like. If audit issues already exist, the fetch ' +
|
|
658
|
+
'that wrote this file is broken and this run will re-file them.'
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
return `dedup index: ${size} issue(s) supplied via --issues-file; every exact-fingerprint lookup was answered from it.`;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Render the warning for a pre-fetch of the issue index that could not
|
|
666
|
+
* complete. The run still dedups — it falls back to a per-finding search — but
|
|
667
|
+
* it loses the one-list saving, and until Story #5301 this failure was
|
|
668
|
+
* swallowed whole: the operator saw only the downstream per-group degradation
|
|
669
|
+
* and could not tell that the pre-fetch itself was the cause.
|
|
670
|
+
*
|
|
671
|
+
* @param {string} reason
|
|
672
|
+
* @returns {string}
|
|
673
|
+
*/
|
|
674
|
+
function dedupIndexDegradedWarning(reason) {
|
|
675
|
+
return (
|
|
676
|
+
`dedup index unavailable: ${reason}. Dedup fell back to a per-finding ` +
|
|
677
|
+
'search, which is slower and rate-limited — if those searches also fail, ' +
|
|
678
|
+
'every affected group is classified "create" WITHOUT a dedup check.'
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Phase 6: classify every group against GitHub, and say loudly whichever way
|
|
684
|
+
* it went.
|
|
685
|
+
*
|
|
686
|
+
* Extracted from `buildPlan` because the gate has three outcomes, not two, and
|
|
687
|
+
* inlining them pushed the caller past its complexity ceiling. The three:
|
|
688
|
+
*
|
|
689
|
+
* - **deduped** — a provider resolved, or the host supplied a corpus, or
|
|
690
|
+
* both. A host-supplied corpus is a dedup source in its own right, which is
|
|
691
|
+
* the whole point: the gate asks "can we dedup at all", not "did a provider
|
|
692
|
+
* resolve". While it asked the latter, `--no-provider --issues-file` — the
|
|
693
|
+
* one invocation a `gh`-less host can run — short-circuited to the seeded
|
|
694
|
+
* all-`create` classifications however well the dedupe module worked.
|
|
695
|
+
* - **skipped, no port** — a provider was wanted but could not be adapted.
|
|
696
|
+
* - **skipped, disabled** — `--no-provider` with no corpus to fall back on.
|
|
697
|
+
*
|
|
698
|
+
* Every outcome warns on stderr, so the `--scan` JSON on stdout stays clean and
|
|
699
|
+
* a create-only plan is never read as "checked, found nothing".
|
|
700
|
+
*
|
|
701
|
+
* @param {{ groups: Array<object>, useProvider?: boolean,
|
|
702
|
+
* issues?: Array<object>|null }} params
|
|
703
|
+
* @param {{ loadProviderImpl: Function, classifyGroupsImpl: Function,
|
|
704
|
+
* logger: { warn: Function } }} deps
|
|
705
|
+
* @returns {Promise<{ classifications: Array<object>, summary: object,
|
|
706
|
+
* dedupApplied: boolean }>}
|
|
707
|
+
*/
|
|
708
|
+
async function runDedupPhase(
|
|
709
|
+
{ groups, useProvider, issues },
|
|
710
|
+
{ loadProviderImpl, classifyGroupsImpl, logger },
|
|
711
|
+
) {
|
|
712
|
+
const provider = useProvider ? await loadProviderImpl() : null;
|
|
713
|
+
if (!provider && !issues) {
|
|
714
|
+
logger.warn(
|
|
715
|
+
dedupSkippedWarning(useProvider ? 'no-provider-port' : 'disabled'),
|
|
716
|
+
);
|
|
717
|
+
return {
|
|
718
|
+
classifications: groups.map((group) => ({
|
|
719
|
+
group,
|
|
720
|
+
action: 'create',
|
|
721
|
+
matchedIssues: [],
|
|
722
|
+
matchedFingerprints: [],
|
|
723
|
+
})),
|
|
724
|
+
summary: { create: groups.length, skipOpen: 0, skipReoccurring: 0 },
|
|
725
|
+
dedupApplied: false,
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const { classifications, summary } = await classifyGroupsImpl({
|
|
730
|
+
groups,
|
|
731
|
+
provider,
|
|
732
|
+
searchCandidates: provider?.searchCandidates,
|
|
733
|
+
listAuditIssues: provider?.listAuditIssues,
|
|
734
|
+
issues,
|
|
735
|
+
});
|
|
736
|
+
for (const warning of dedupPhaseWarnings({ issues, summary })) {
|
|
737
|
+
logger.warn(warning);
|
|
738
|
+
}
|
|
739
|
+
return { classifications, summary, dedupApplied: true };
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Every warning a completed dedup pass owes the operator, in order. Pure, so
|
|
744
|
+
* the wording stays unit-testable and `runDedupPhase` keeps one write site.
|
|
745
|
+
*
|
|
746
|
+
* @param {{ issues?: Array<object>|null, summary: object }} params
|
|
747
|
+
* @returns {string[]}
|
|
748
|
+
*/
|
|
749
|
+
function dedupPhaseWarnings({ issues, summary }) {
|
|
750
|
+
const warnings = [];
|
|
751
|
+
if (issues) warnings.push(dedupIndexWarning(summary.dedupIndex));
|
|
752
|
+
// The pre-fetch failing is a distinct fact from any group's lookup failing,
|
|
753
|
+
// and used to be invisible: the operator saw only the downstream per-group
|
|
754
|
+
// degradation and could not tell what had caused it.
|
|
755
|
+
if (summary.dedupDegraded?.indexPrefetch) {
|
|
756
|
+
warnings.push(
|
|
757
|
+
dedupIndexDegradedWarning(summary.dedupDegraded.indexPrefetch),
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
// A partially-checked plan is a useful result — name the groups that degraded
|
|
761
|
+
// to create because their lookup could not complete (Story #4678).
|
|
762
|
+
if (summary.dedupDegraded?.count > 0) {
|
|
763
|
+
warnings.push(dedupDegradedWarning(summary.dedupDegraded.groups));
|
|
764
|
+
}
|
|
765
|
+
return warnings;
|
|
766
|
+
}
|
|
767
|
+
|
|
579
768
|
/**
|
|
580
769
|
* Scan → group → dedup → (optionally) reconcile the cross-run ledger, and
|
|
581
770
|
* return the plan envelope.
|
|
@@ -584,12 +773,14 @@ function dedupDegradedWarning(entries) {
|
|
|
584
773
|
* implementation (`.agents/rules/test-seams.md` rules 1-2, 4), so `main`,
|
|
585
774
|
* `runAuto`, and every production caller are unchanged.
|
|
586
775
|
*
|
|
587
|
-
* @param {{ glob?: string, severity?: string, useProvider?: boolean,
|
|
776
|
+
* @param {{ glob?: string, severity?: string, useProvider?: boolean,
|
|
777
|
+
* issuesFile?: string, ledger?: object }} params
|
|
588
778
|
* @param {{
|
|
589
779
|
* collectReportPathsImpl?: typeof collectReportPaths,
|
|
590
780
|
* readReportsImpl?: typeof readReports,
|
|
591
781
|
* loadProviderImpl?: typeof loadProviderOrNull,
|
|
592
782
|
* classifyGroupsImpl?: typeof classifyGroupsAgainstGitHub,
|
|
783
|
+
* loadIssuesFileImpl?: typeof loadIssuesFile,
|
|
593
784
|
* reconcileScanLedgerImpl?: typeof reconcileScanLedger,
|
|
594
785
|
* logger?: { warn: Function },
|
|
595
786
|
* }} [deps]
|
|
@@ -600,6 +791,7 @@ async function buildPlan(
|
|
|
600
791
|
glob: pattern,
|
|
601
792
|
severity,
|
|
602
793
|
useProvider,
|
|
794
|
+
issuesFile,
|
|
603
795
|
ledger,
|
|
604
796
|
allowMissingTally,
|
|
605
797
|
failOnReportFailures,
|
|
@@ -611,9 +803,14 @@ async function buildPlan(
|
|
|
611
803
|
readReportsImpl = readReports,
|
|
612
804
|
loadProviderImpl = loadProviderOrNull,
|
|
613
805
|
classifyGroupsImpl = classifyGroupsAgainstGitHub,
|
|
806
|
+
loadIssuesFileImpl = loadIssuesFile,
|
|
614
807
|
reconcileScanLedgerImpl = reconcileScanLedger,
|
|
615
808
|
logger = Logger,
|
|
616
809
|
} = deps;
|
|
810
|
+
// Deliberately BEFORE the reports are read: an unusable corpus is a usage
|
|
811
|
+
// error, and failing fast costs the operator nothing, where failing late
|
|
812
|
+
// would tempt a fallback that silently dedups nothing.
|
|
813
|
+
const issues = issuesFile ? loadIssuesFileImpl(issuesFile) : null;
|
|
617
814
|
const reportPaths = await collectReportPathsImpl(pattern ?? DEFAULT_GLOB);
|
|
618
815
|
if (reportPaths.length === 0) {
|
|
619
816
|
return {
|
|
@@ -651,44 +848,10 @@ async function buildPlan(
|
|
|
651
848
|
const stamped = withFingerprints(filtered.filter((f) => Boolean(f.severity)));
|
|
652
849
|
const { groups, edges } = groupFindings(stamped);
|
|
653
850
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
matchedFingerprints: [],
|
|
659
|
-
}));
|
|
660
|
-
let summary = { create: groups.length, skipOpen: 0, skipReoccurring: 0 };
|
|
661
|
-
let dedupApplied = false;
|
|
662
|
-
|
|
663
|
-
if (useProvider) {
|
|
664
|
-
const provider = await loadProviderImpl();
|
|
665
|
-
if (provider) {
|
|
666
|
-
const result = await classifyGroupsImpl({
|
|
667
|
-
groups,
|
|
668
|
-
provider,
|
|
669
|
-
searchCandidates: provider.searchCandidates,
|
|
670
|
-
});
|
|
671
|
-
classifications = result.classifications;
|
|
672
|
-
summary = result.summary;
|
|
673
|
-
dedupApplied = true;
|
|
674
|
-
// A partially-checked plan is a useful result — warn loudly (stderr, so
|
|
675
|
-
// the --scan JSON on stdout stays clean) naming the groups that degraded
|
|
676
|
-
// to create because their lookup could not complete (Story #4678).
|
|
677
|
-
if (summary.dedupDegraded?.count > 0) {
|
|
678
|
-
logger.warn(dedupDegradedWarning(summary.dedupDegraded.groups));
|
|
679
|
-
}
|
|
680
|
-
} else {
|
|
681
|
-
// The provider could not resolve a searchIssues port — the dedup gate
|
|
682
|
-
// is silently a no-op without this. Surface it loudly (stderr, so the
|
|
683
|
-
// --scan JSON on stdout stays clean) so the operator does not read a
|
|
684
|
-
// create-only plan as "no duplicates found".
|
|
685
|
-
logger.warn(dedupSkippedWarning('no-provider-port'));
|
|
686
|
-
}
|
|
687
|
-
} else {
|
|
688
|
-
// Operator explicitly opted out via --no-provider. Still warn so a
|
|
689
|
-
// duplicate-opening re-run is never a surprise.
|
|
690
|
-
logger.warn(dedupSkippedWarning('disabled'));
|
|
691
|
-
}
|
|
851
|
+
const { classifications, summary, dedupApplied } = await runDedupPhase(
|
|
852
|
+
{ groups, useProvider, issues },
|
|
853
|
+
{ loadProviderImpl, classifyGroupsImpl, logger },
|
|
854
|
+
);
|
|
692
855
|
|
|
693
856
|
// Cross-run ledger (Story #4626): fold this scan onto the committed memory,
|
|
694
857
|
// suppress findings a prior run recorded as accepted-risk, and (unless the
|
|
@@ -758,6 +921,9 @@ function reconcileScanLedger({ ledgerPath, findings, classifications, write }) {
|
|
|
758
921
|
ledger: prior,
|
|
759
922
|
findings,
|
|
760
923
|
issueStates,
|
|
924
|
+
// The ledger lives in the shared findings layer and cannot import the
|
|
925
|
+
// audit adapter without closing a cycle, so the projection is ours to pass.
|
|
926
|
+
toCanonical: toCanonicalFinding,
|
|
761
927
|
});
|
|
762
928
|
if (write !== false) writeLedger(ledgerPath, next);
|
|
763
929
|
return new Set(
|
|
@@ -844,6 +1010,7 @@ async function runAuto({
|
|
|
844
1010
|
severity,
|
|
845
1011
|
dryRun,
|
|
846
1012
|
useProvider,
|
|
1013
|
+
issuesFile,
|
|
847
1014
|
ledgerPath,
|
|
848
1015
|
ledgerCommit,
|
|
849
1016
|
git,
|
|
@@ -856,6 +1023,7 @@ async function runAuto({
|
|
|
856
1023
|
glob,
|
|
857
1024
|
severity: floor,
|
|
858
1025
|
useProvider,
|
|
1026
|
+
issuesFile,
|
|
859
1027
|
ledger: { path: resolvedLedgerPath, write: !dryRun },
|
|
860
1028
|
// `--auto` never accepts `--allow-missing-tally`: an unattended sweep has
|
|
861
1029
|
// no operator to read a warning, so every report failure is fatal here.
|
|
@@ -893,6 +1061,11 @@ async function runAuto({
|
|
|
893
1061
|
skipReoccurring: byAction.skipReoccurring.length,
|
|
894
1062
|
suppressedByLedger: byAction.suppressed.length,
|
|
895
1063
|
},
|
|
1064
|
+
// `--auto` opens no Issues itself — the caller does, from the `--emit-stories`
|
|
1065
|
+
// drafts — so these keys are the only thing standing between its summary and
|
|
1066
|
+
// the `--wire-edges --ids` map. Without them an unattended sweep cannot
|
|
1067
|
+
// record what it filed, and the ledger stays empty however well it works.
|
|
1068
|
+
createGroupKeys: eligible.map((g) => g?.groupKey).filter(Boolean),
|
|
896
1069
|
// Re-detected open Issues the operator may want a "re-detected" comment on.
|
|
897
1070
|
reDetected: byAction.skipOpen
|
|
898
1071
|
.flatMap((c) => c.matchedIssues ?? [])
|
|
@@ -1002,20 +1175,50 @@ function wireEdgesPreconditionError(reason, detail) {
|
|
|
1002
1175
|
* re-rendered with a canonical `blocked by #N` footer and the same edges are
|
|
1003
1176
|
* mirrored as native `blocked_by` relations.
|
|
1004
1177
|
*
|
|
1178
|
+
* The same map is what the cross-run ledger needs to record what this run
|
|
1179
|
+
* filed, so the record rides along here rather than arriving as a second
|
|
1180
|
+
* command an operator must remember (Story #5305).
|
|
1181
|
+
*
|
|
1005
1182
|
* @param {object} params
|
|
1006
1183
|
* @param {object} params.plan A `--scan` plan envelope.
|
|
1007
1184
|
* @param {Record<string, number>} params.issueByGroupKey
|
|
1185
|
+
* @param {string} [params.ledgerPath] — ledger to record into; defaults to
|
|
1186
|
+
* `DEFAULT_LEDGER_PATH` inside the record.
|
|
1187
|
+
* @param {boolean} [params.write] — `false` computes the record without
|
|
1188
|
+
* persisting it (what `--dry-run` passes).
|
|
1008
1189
|
* @param {object} [deps]
|
|
1009
1190
|
* @param {Function} [deps.loadProviderImpl]
|
|
1010
1191
|
* @param {Function} [deps.wireImpl]
|
|
1011
|
-
* @
|
|
1192
|
+
* @param {Function} [deps.recordFiledIssuesImpl]
|
|
1193
|
+
* @returns {Promise<object>} the wiring summary, with the ledger record on
|
|
1194
|
+
* `ledger`.
|
|
1012
1195
|
*/
|
|
1013
|
-
async function wireEdges(
|
|
1014
|
-
|
|
1015
|
-
|
|
1196
|
+
async function wireEdges(
|
|
1197
|
+
{ plan, issueByGroupKey, ledgerPath, write },
|
|
1198
|
+
deps = {},
|
|
1199
|
+
) {
|
|
1200
|
+
const {
|
|
1201
|
+
loadProviderImpl = loadProvider,
|
|
1202
|
+
wireImpl = wireAuditStoryEdges,
|
|
1203
|
+
recordFiledIssuesImpl = recordFiledIssues,
|
|
1204
|
+
} = deps;
|
|
1016
1205
|
const groups = (plan.classifications ?? [])
|
|
1017
1206
|
.filter((c) => c.action === 'create')
|
|
1018
1207
|
.map((c) => c.group);
|
|
1208
|
+
|
|
1209
|
+
// Record BEFORE the provider is loaded. The record is pure local filesystem
|
|
1210
|
+
// work, while the wiring below needs a provider exposing `updateTicket` — and
|
|
1211
|
+
// the host most likely to lack one is the `gh`-less host where the ledger is
|
|
1212
|
+
// the only duplicate protection there is. Recording first means such a run
|
|
1213
|
+
// still remembers what it filed, and the precondition error below still
|
|
1214
|
+
// surfaces unchanged afterwards.
|
|
1215
|
+
const ledger = recordFiledIssuesImpl({
|
|
1216
|
+
ledgerPath,
|
|
1217
|
+
groups,
|
|
1218
|
+
issueByGroupKey,
|
|
1219
|
+
write,
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1019
1222
|
let provider;
|
|
1020
1223
|
try {
|
|
1021
1224
|
provider = await loadProviderImpl();
|
|
@@ -1025,7 +1228,7 @@ async function wireEdges({ plan, issueByGroupKey }, deps = {}) {
|
|
|
1025
1228
|
if (typeof provider?.updateTicket !== 'function') {
|
|
1026
1229
|
throw wireEdgesPreconditionError('fixture-no-write-port');
|
|
1027
1230
|
}
|
|
1028
|
-
|
|
1231
|
+
const wired = await wireImpl({
|
|
1029
1232
|
groups,
|
|
1030
1233
|
edges: plan.edges ?? [],
|
|
1031
1234
|
issueByGroupKey,
|
|
@@ -1033,6 +1236,7 @@ async function wireEdges({ plan, issueByGroupKey }, deps = {}) {
|
|
|
1033
1236
|
updateBody: (issueNumber, body) =>
|
|
1034
1237
|
provider.updateTicket(issueNumber, { body }),
|
|
1035
1238
|
});
|
|
1239
|
+
return { ...wired, ledger };
|
|
1036
1240
|
}
|
|
1037
1241
|
|
|
1038
1242
|
/**
|
|
@@ -1087,6 +1291,8 @@ export const __testing = {
|
|
|
1087
1291
|
loadProviderOrNull,
|
|
1088
1292
|
dedupSkippedWarning,
|
|
1089
1293
|
dedupDegradedWarning,
|
|
1294
|
+
dedupIndexWarning,
|
|
1295
|
+
dedupIndexDegradedWarning,
|
|
1090
1296
|
buildAndGateStories,
|
|
1091
1297
|
runAuto,
|
|
1092
1298
|
resolveSeverityFloor,
|
|
@@ -1117,6 +1323,73 @@ export const __testing = {
|
|
|
1117
1323
|
* }} [deps]
|
|
1118
1324
|
* @returns {Promise<void>}
|
|
1119
1325
|
*/
|
|
1326
|
+
/**
|
|
1327
|
+
* The one-line `--ledger-commit` outcome, for stderr.
|
|
1328
|
+
*
|
|
1329
|
+
* Names the branch and the PR on success — the two things an operator needs to
|
|
1330
|
+
* go look at it — and the skip reason otherwise, because "nothing happened" and
|
|
1331
|
+
* "the ledger was already clean" are different facts and only one of them is
|
|
1332
|
+
* fine.
|
|
1333
|
+
*
|
|
1334
|
+
* @param {{ committed?: boolean, reason?: string, branch?: string,
|
|
1335
|
+
* prUrl?: string|null, resumed?: boolean, ledgerPath?: string }} [result]
|
|
1336
|
+
* @returns {string}
|
|
1337
|
+
*/
|
|
1338
|
+
function describeLedgerCommit(result) {
|
|
1339
|
+
return result?.committed
|
|
1340
|
+
? ledgerCommittedLine(result)
|
|
1341
|
+
: ledgerSkippedLine(result);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
/**
|
|
1345
|
+
* The success half: the branch, whether it resumed a half-finished one, and
|
|
1346
|
+
* the PR to go look at.
|
|
1347
|
+
* @param {object} result
|
|
1348
|
+
* @returns {string}
|
|
1349
|
+
*/
|
|
1350
|
+
function ledgerCommittedLine(result) {
|
|
1351
|
+
const resumed = result.resumed ? ' (resumed an unpushed ledger branch)' : '';
|
|
1352
|
+
const pr = result.prUrl ?? '(no URL reported by gh)';
|
|
1353
|
+
return `--ledger-commit: pushed ${result.branch}${resumed} and opened ${pr}.`;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
/**
|
|
1357
|
+
* The skip half. It names the ledger file, because the fact that matters is
|
|
1358
|
+
* which state is still only in the working tree.
|
|
1359
|
+
* @param {object} [result]
|
|
1360
|
+
* @returns {string}
|
|
1361
|
+
*/
|
|
1362
|
+
function ledgerSkippedLine(result) {
|
|
1363
|
+
const reason = result?.reason ?? 'no result';
|
|
1364
|
+
const ledgerPath = result?.ledgerPath ?? 'the ledger';
|
|
1365
|
+
return `--ledger-commit: skipped (${reason}) — ${ledgerPath} was not committed.`;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
/**
|
|
1369
|
+
* Validate `--severity` against the canonical scale, failing on a value the
|
|
1370
|
+
* filter would silently ignore.
|
|
1371
|
+
*
|
|
1372
|
+
* `meetsSeverity` reads an unknown threshold as rank `0`, so a typo — the
|
|
1373
|
+
* classic being `--severity Hgh` — quietly widened the run to every finding
|
|
1374
|
+
* instead of narrowing it. On an unattended sweep that is the difference
|
|
1375
|
+
* between filing a batch and filing the backlog, with nothing on stderr to say
|
|
1376
|
+
* so. An absent flag stays absent: the floor then resolves from config.
|
|
1377
|
+
*
|
|
1378
|
+
* @param {string|undefined} raw
|
|
1379
|
+
* @returns {string|undefined} the canonical level.
|
|
1380
|
+
*/
|
|
1381
|
+
function validateSeverityFlag(raw) {
|
|
1382
|
+
if (raw === undefined || raw === null || raw === '') return undefined;
|
|
1383
|
+
if (String(raw).toLowerCase() === 'all') return 'all';
|
|
1384
|
+
const level = normalizeSeverity(String(raw), null);
|
|
1385
|
+
if (!level) {
|
|
1386
|
+
throw new Error(
|
|
1387
|
+
`[audit-to-stories] --severity "${raw}" is not a severity. Accepted: ${SEVERITIES.join(', ')} (or "all").`,
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
return level;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1120
1393
|
export async function runAuditToStories(
|
|
1121
1394
|
argv = process.argv.slice(2),
|
|
1122
1395
|
deps = {},
|
|
@@ -1150,12 +1423,15 @@ export async function runAuditToStories(
|
|
|
1150
1423
|
plan: { type: 'string' },
|
|
1151
1424
|
out: { type: 'string' },
|
|
1152
1425
|
'no-provider': { type: 'boolean' },
|
|
1426
|
+
'issues-file': { type: 'string' },
|
|
1153
1427
|
'allow-missing-tally': { type: 'boolean' },
|
|
1154
1428
|
json: { type: 'boolean' },
|
|
1155
1429
|
},
|
|
1156
1430
|
strict: false,
|
|
1157
1431
|
});
|
|
1158
1432
|
|
|
1433
|
+
values.severity = validateSeverityFlag(values.severity);
|
|
1434
|
+
|
|
1159
1435
|
const json = (value) => JSON.stringify(value, null, 2);
|
|
1160
1436
|
|
|
1161
1437
|
const runAutoSummary = async () =>
|
|
@@ -1165,6 +1441,7 @@ export async function runAuditToStories(
|
|
|
1165
1441
|
severity: values.severity,
|
|
1166
1442
|
dryRun: values['dry-run'],
|
|
1167
1443
|
useProvider: !values['no-provider'],
|
|
1444
|
+
issuesFile: values['issues-file'],
|
|
1168
1445
|
ledgerPath: values.ledger,
|
|
1169
1446
|
ledgerCommit: values['ledger-commit'],
|
|
1170
1447
|
})
|
|
@@ -1175,7 +1452,15 @@ export async function runAuditToStories(
|
|
|
1175
1452
|
// findings, so the PR attempt is the last thing the run does (Story #5145).
|
|
1176
1453
|
const commitLedger = async () => {
|
|
1177
1454
|
if (!values['ledger-commit'] || values['dry-run']) return;
|
|
1178
|
-
|
|
1455
|
+
// Say what happened. The tail used to run silently, so an operator could
|
|
1456
|
+
// not tell a ledger PR from a skip without going to look for the branch —
|
|
1457
|
+
// and a skip is the outcome that matters, because it means the sweep's
|
|
1458
|
+
// memory is still only in the working tree.
|
|
1459
|
+
Logger.warn(
|
|
1460
|
+
describeLedgerCommit(
|
|
1461
|
+
await runLedgerCommitImpl({ ledgerPath: values.ledger }),
|
|
1462
|
+
),
|
|
1463
|
+
);
|
|
1179
1464
|
};
|
|
1180
1465
|
|
|
1181
1466
|
const scanPlan = () =>
|
|
@@ -1183,6 +1468,7 @@ export async function runAuditToStories(
|
|
|
1183
1468
|
glob: values.glob,
|
|
1184
1469
|
severity: values.severity,
|
|
1185
1470
|
useProvider: !values['no-provider'],
|
|
1471
|
+
issuesFile: values['issues-file'],
|
|
1186
1472
|
allowMissingTally: values['allow-missing-tally'],
|
|
1187
1473
|
});
|
|
1188
1474
|
|
|
@@ -1208,6 +1494,11 @@ export async function runAuditToStories(
|
|
|
1208
1494
|
wireEdgesImpl({
|
|
1209
1495
|
plan: loadPlanImpl(values.plan),
|
|
1210
1496
|
issueByGroupKey: parseIssueMapImpl(values.ids),
|
|
1497
|
+
ledgerPath: values.ledger,
|
|
1498
|
+
// `--scan` still never writes the ledger; `--wire-edges` runs only after
|
|
1499
|
+
// the Issues were really opened, where recording is never wrong — so the
|
|
1500
|
+
// record is on by default here and `--dry-run` is what suppresses it.
|
|
1501
|
+
write: !values['dry-run'],
|
|
1211
1502
|
});
|
|
1212
1503
|
|
|
1213
1504
|
// One table, not a chain of `if (values.X) { …; return; }`. Each entry
|
|
@@ -1284,7 +1575,7 @@ runAsCli(import.meta.url, main, {
|
|
|
1284
1575
|
['--emit-stories', 'Emit the Story drafts as JSON.'],
|
|
1285
1576
|
[
|
|
1286
1577
|
'--wire-edges',
|
|
1287
|
-
'Second pass: resolve the detected group edges to blocked by #N footers plus native blocked_by relations. Needs --plan and --ids.',
|
|
1578
|
+
'Second pass: resolve the detected group edges to blocked by #N footers plus native blocked_by relations, and record the mapped issues in the cross-run ledger as filed. Needs --plan and --ids; --dry-run suppresses the ledger write.',
|
|
1288
1579
|
],
|
|
1289
1580
|
[
|
|
1290
1581
|
'--ids <json|path>',
|
|
@@ -1292,7 +1583,10 @@ runAsCli(import.meta.url, main, {
|
|
|
1292
1583
|
],
|
|
1293
1584
|
['--glob <pattern>', 'Override the audit-results glob.'],
|
|
1294
1585
|
['--severity <level>', 'Lowest severity to include (high|medium|low).'],
|
|
1295
|
-
[
|
|
1586
|
+
[
|
|
1587
|
+
'--ledger <path>',
|
|
1588
|
+
`Path to the cross-run dedup ledger (default ${DEFAULT_LEDGER_PATH}).`,
|
|
1589
|
+
],
|
|
1296
1590
|
[
|
|
1297
1591
|
'--ledger-commit',
|
|
1298
1592
|
'After the --auto summary prints, commit a changed ledger onto chore/audit-ledger-<date>, push it, and open a PR against the base branch (never auto-merged). Ignored under --dry-run.',
|
|
@@ -1303,6 +1597,10 @@ runAsCli(import.meta.url, main, {
|
|
|
1303
1597
|
],
|
|
1304
1598
|
['--out <path>', 'Write output to a file instead of stdout.'],
|
|
1305
1599
|
['--no-provider', 'Skip live GitHub dedup lookups (offline).'],
|
|
1600
|
+
[
|
|
1601
|
+
'--issues-file <path>',
|
|
1602
|
+
'Dedup against a JSON array of issues the host already fetched (every issue labelled audit::*, state all) instead of listing them through the provider. Lets dedup run where there is no gh CLI; composes with --no-provider.',
|
|
1603
|
+
],
|
|
1306
1604
|
[
|
|
1307
1605
|
'--allow-missing-tally',
|
|
1308
1606
|
'Downgrade a missing "Severity tally:" line to a warning (--scan only; --auto ignores it).',
|