mandrel 2.8.0 → 2.10.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/.markdownlint.json +4 -0
- package/.agents/agents/acceptance-critic.md +30 -5
- package/.agents/agents/auditor.md +36 -19
- package/.agents/agents/plan-critic.md +31 -5
- package/.agents/agents/story-worker.md +91 -100
- package/.agents/docs/configuration.md +39 -1
- package/.agents/docs/execution-reference.md +13 -0
- package/.agents/docs/workflows.md +1 -1
- package/.agents/instructions.md +131 -265
- package/.agents/rules/git-conventions.md +47 -83
- package/.agents/rules/orchestration-error-handling.md +28 -0
- package/.agents/schemas/agentrc.schema.json +44 -1
- package/.agents/schemas/validation-evidence.schema.json +3 -1
- package/.agents/scripts/acceptance-eval.js +1 -1
- package/.agents/scripts/apply-quality-bootstrap.js +1 -1
- package/.agents/scripts/audit-to-stories.js +51 -0
- package/.agents/scripts/check-test-temp-hygiene.js +438 -0
- package/.agents/scripts/deliver-recover.js +23 -6
- package/.agents/scripts/lib/audit-suite/index.js +5 -0
- package/.agents/scripts/lib/audit-suite/lens-diff-floor.js +179 -0
- package/.agents/scripts/lib/audit-suite/selector.js +1 -1
- package/.agents/scripts/lib/audit-to-stories/dedupe-against-github.js +120 -55
- package/.agents/scripts/lib/config/temp-paths.js +121 -1
- package/.agents/scripts/lib/config-settings-schema-delivery.js +30 -0
- package/.agents/scripts/lib/config-settings-schema.js +32 -0
- package/.agents/scripts/lib/findings/semantic-issue-search.js +43 -5
- package/.agents/scripts/lib/observability/metrics-ledger.js +217 -0
- package/.agents/scripts/lib/observability/runtime-friction.js +7 -0
- package/.agents/scripts/lib/observability/terse-result.js +114 -0
- package/.agents/scripts/lib/orchestration/complexity-gate.js +318 -0
- package/.agents/scripts/lib/orchestration/deliver-recover.js +137 -10
- package/.agents/scripts/lib/orchestration/merge-block-class.js +36 -15
- package/.agents/scripts/lib/orchestration/merge-poll.js +213 -0
- package/.agents/scripts/lib/orchestration/plan-context.js +60 -0
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +182 -9
- package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +29 -2
- package/.agents/scripts/lib/orchestration/plan-metrics.js +31 -82
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +102 -2
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +215 -14
- package/.agents/scripts/lib/orchestration/resolve-stories.js +7 -0
- package/.agents/scripts/lib/orchestration/review-providers/native.js +34 -16
- package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +221 -8
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +8 -3
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +230 -79
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +55 -14
- package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +9 -3
- package/.agents/scripts/lib/orchestration/story-close/phases/local-lens-review.js +89 -1
- package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +73 -0
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +4 -1
- package/.agents/scripts/lib/orchestration/task-body-validator.js +13 -40
- package/.agents/scripts/lib/story-body/body-format-lints.js +215 -0
- package/.agents/scripts/lib/story-body/story-body.js +18 -2
- package/.agents/scripts/lib/templates/decomposer-prompts.js +29 -6
- package/.agents/scripts/lib/test-env.js +65 -0
- package/.agents/scripts/plan-context.js +66 -9
- package/.agents/scripts/plan-critics.js +115 -3
- package/.agents/scripts/plan-persist.js +11 -1
- package/.agents/scripts/plan-run-epilogue.js +1 -1
- package/.agents/scripts/providers/github/issues.js +54 -7
- package/.agents/scripts/providers/github/search-budget.js +124 -0
- package/.agents/scripts/providers/github/search-query.js +71 -0
- package/.agents/scripts/single-story-confirm-merge.js +79 -10
- package/.agents/scripts/single-story-init.js +19 -3
- package/.agents/scripts/stories-wave-tick.js +1 -1
- package/.agents/scripts/sync-branch-from-base.js +9 -3
- package/.agents/workflows/deliver.md +86 -230
- package/.agents/workflows/helpers/deliver-reference.md +167 -0
- package/.agents/workflows/helpers/deliver-story-reference.md +203 -0
- package/.agents/workflows/helpers/deliver-story.md +114 -422
- package/.agents/workflows/helpers/plan-reference.md +211 -0
- package/.agents/workflows/plan.md +107 -279
- package/docs/CHANGELOG.md +47 -0
- package/package.json +1 -1
|
@@ -1,99 +1,63 @@
|
|
|
1
1
|
# Git & Version Control Conventions
|
|
2
2
|
|
|
3
|
-
This rule applies globally to all repository changes
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
**before** doing the matching work; the core below is sufficient for every
|
|
13
|
-
routine commit, branch, and PR.
|
|
3
|
+
This rule applies globally to all repository changes. It is the **always-on
|
|
4
|
+
core**: branch shapes, the Conventional-Commit subject contract, issue
|
|
5
|
+
references, and the non-negotiable push/hygiene MUSTs. Deeper mechanics —
|
|
6
|
+
the hard-cutover contract policy, the full push-validation procedure and
|
|
7
|
+
the push-hook false-negative signature, checkout-hygiene scope rules,
|
|
8
|
+
shared-checkout merge contention, the documentation-freshness gate, and
|
|
9
|
+
the `meta::*` labels — live in the on-demand
|
|
10
|
+
[`git-conventions-reference.md`](git-conventions-reference.md); read it
|
|
11
|
+
**before** doing the matching work.
|
|
14
12
|
|
|
15
13
|
## Canonical Branching (v2)
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
> **Commit subjects.** Stories have no child tickets. Commits land on
|
|
27
|
-
> `story-<storyId>` directly from the agent and the Conventional Commit
|
|
28
|
-
> subject references the Story via `(refs #<storyId>)`. See
|
|
29
|
-
> [`.agents/instructions.md` § 5.D](../instructions.md) for the hierarchy
|
|
30
|
-
> contract.
|
|
15
|
+
Every Story lands on a dedicated **Story branch** named
|
|
16
|
+
`story-<storyId>`, seeded from `project.baseBranch` (`main` by default).
|
|
17
|
+
The runtime owns branch creation via `single-story-init.js`; agents
|
|
18
|
+
commit on that branch only. Close opens a PR against `main` (squash +
|
|
19
|
+
required checks). No `epic/<id>` integration branch, no `--no-ff` wave
|
|
20
|
+
merge, no child tickets: commits land on `story-<storyId>` directly, the
|
|
21
|
+
subject referencing the Story via `(refs #<storyId>)` — see
|
|
22
|
+
[`.agents/instructions.md` § 5.D](../instructions.md).
|
|
31
23
|
|
|
32
24
|
## Conventional Commits
|
|
33
25
|
|
|
34
|
-
- MUST
|
|
35
|
-
|
|
36
|
-
- Types
|
|
37
|
-
`style:`, `chore:`, `test:`, `build:`, `ci
|
|
38
|
-
`changelog-sections` in `release-please-config.json`; keep
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
(`agentSettings.commands.validate` and `agentSettings.commands.test` in
|
|
57
|
-
`.agentrc.json`, or the equivalent format-check command) locally
|
|
58
|
-
_before_ attempting a `git push`.
|
|
59
|
-
2. **Verify Push Output**: Do NOT assume a push succeeded unless the output
|
|
60
|
-
explicitly confirms the remote ref was updated (`[new branch]`,
|
|
61
|
-
`[up to date]`, or `... -> ...`).
|
|
62
|
-
3. **Handle Rejections**: If a push is rejected by a `pre-push` hook, fix
|
|
63
|
-
the underlying issue (usually formatting or linting) and create a NEW
|
|
64
|
-
follow-up commit. Do **not** amend the rejected commit — amending makes
|
|
65
|
-
diffs harder to review and can lose work if the original commit
|
|
66
|
-
contained more than the linting fix.
|
|
67
|
-
4. **Never bypass hooks**: Do not use `--no-verify`, `--no-gpg-sign`, or
|
|
68
|
-
other hook-skipping flags unless the operator explicitly authorizes it.
|
|
69
|
-
If a hook fails, investigate the underlying cause. One recognized
|
|
70
|
-
exception — a Biome _zero-match_ failure under a harness-managed
|
|
71
|
-
worktree path the consumer's lint config ignores — is a consumer-tooling
|
|
72
|
-
gap, **not** authorization to skip the hook; its recognition signature
|
|
73
|
-
and sanctioned fix live in
|
|
74
|
-
[`git-conventions-reference.md` § Push Validation](git-conventions-reference.md).
|
|
26
|
+
- MUST follow `<type>(<optional scope>): <description>`, imperative
|
|
27
|
+
mood.
|
|
28
|
+
- Types: `feat:`, `fix:`, `perf:`, `refactor:`, `revert:`, `docs:`,
|
|
29
|
+
`style:`, `chore:`, `test:`, `build:`, `ci:` — mirrors
|
|
30
|
+
`changelog-sections` in `release-please-config.json`; keep in sync.
|
|
31
|
+
- The `commit-msg` Husky hook runs `commitlint` on every local commit.
|
|
32
|
+
It does **not** run on squash-merge titles edited in the GitHub UI —
|
|
33
|
+
author the PR title in conventional form so the squash commit parses
|
|
34
|
+
for release-please.
|
|
35
|
+
|
|
36
|
+
## Push Validation & Reliability (MUSTs)
|
|
37
|
+
|
|
38
|
+
1. Run the configured validation commands locally **before** `git push`.
|
|
39
|
+
2. Do NOT assume a push succeeded unless the output confirms the remote
|
|
40
|
+
ref was updated (`[new branch]`, `[up to date]`, `... -> ...`).
|
|
41
|
+
3. If a `pre-push` hook rejects, fix the cause and create a NEW follow-up
|
|
42
|
+
commit — never amend the rejected commit.
|
|
43
|
+
4. **Never bypass hooks** (`--no-verify`, `--no-gpg-sign`, …) without
|
|
44
|
+
explicit operator authorization. The one recognized exception — a
|
|
45
|
+
Biome zero-match failure under a harness-managed worktree path — is a
|
|
46
|
+
consumer-tooling gap, **not** authorization; see
|
|
47
|
+
[`git-conventions-reference.md` § Push Validation](git-conventions-reference.md).
|
|
75
48
|
|
|
76
49
|
## Local checkout hygiene
|
|
77
50
|
|
|
78
|
-
**
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
intervention: it fast-forwards the base branch itself, and reaps its own
|
|
85
|
-
merged local refs on the next workflow boot (the `boot-sweep.js` protected
|
|
86
|
-
sweep). Do **not** end a workflow by telling the operator to run
|
|
87
|
-
`/git-cleanup` to catch up; `/git-cleanup` is for recovering an unusual state
|
|
88
|
-
the automated hygiene does not cover. The boot-sweep scope rules, the
|
|
89
|
-
content-merged report-only case, and the shared-checkout merge-contention
|
|
90
|
-
guard are detailed in
|
|
51
|
+
**The delivering flow owns tidying the local checkout** — it
|
|
52
|
+
fast-forwards the base branch itself and reaps its own merged refs on
|
|
53
|
+
the next workflow boot (the `boot-sweep.js` protected sweep).
|
|
54
|
+
`/git-cleanup` is a recovery tool, not a routine chore — never end a
|
|
55
|
+
workflow by telling the operator to run it. Scope rules and the
|
|
56
|
+
shared-checkout contention guard:
|
|
91
57
|
[`git-conventions-reference.md` § Local checkout hygiene](git-conventions-reference.md).
|
|
92
58
|
|
|
93
59
|
## Pull Requests
|
|
94
60
|
|
|
95
61
|
- Never commit `.env` or hardcoded secrets.
|
|
96
|
-
- Always include a short description of _why_ the change was made
|
|
97
|
-
|
|
98
|
-
- **Reference Issues**: Use "Resolves #109" or "Closes #114" to link
|
|
99
|
-
tickets.
|
|
62
|
+
- Always include a short description of _why_ the change was made.
|
|
63
|
+
- Reference issues: "Resolves #109" / "Closes #114".
|
|
@@ -23,3 +23,31 @@ stubbed, letting execution continue past the intended hard-stop.
|
|
|
23
23
|
Non-orchestration scripts (one-shot utilities, audit reporters, doc
|
|
24
24
|
generators) may continue to use `Logger.fatal` where the lifetime guarantees
|
|
25
25
|
are simpler.
|
|
26
|
+
|
|
27
|
+
## Output Contract — compact digest + artifact path (Story #4708)
|
|
28
|
+
|
|
29
|
+
An orchestration script's stdout rides resident in the invoking agent's
|
|
30
|
+
transcript and is re-read as prompt-cache input on every later turn, so fat
|
|
31
|
+
success output is a per-turn tax on the whole session — not a one-time cost.
|
|
32
|
+
|
|
33
|
+
- **Budget.** A script's **default success-path stdout MUST stay under
|
|
34
|
+
~2KB**. Error paths and opt-in verbose modes are exempt; the budget is
|
|
35
|
+
about what every routine invocation pays.
|
|
36
|
+
- **Digest + artifact path.** When a script produces a large result (a full
|
|
37
|
+
envelope, a per-phase result object, a report), it MUST write the full
|
|
38
|
+
payload to an on-disk artifact (under `temp/`, or an operator-named
|
|
39
|
+
`--out` path) and emit a **compact machine-parseable digest** on stdout
|
|
40
|
+
that names the artifact path plus the handful of fields the caller acts
|
|
41
|
+
on. The shared helper
|
|
42
|
+
[`lib/observability/terse-result.js`](../scripts/lib/observability/terse-result.js)
|
|
43
|
+
(`emitTerseResult`, Story #4685) implements this for hot-path result
|
|
44
|
+
dumps; `plan-context.js --out` shows the envelope-capture variant.
|
|
45
|
+
- **Compact JSON.** Machine-parsed stdout envelopes are `JSON.parse`d by
|
|
46
|
+
their drivers — emit them single-line (`JSON.stringify(x)`), never
|
|
47
|
+
pretty-printed (`null, 2` only adds resident bytes). Pretty output is
|
|
48
|
+
reserved for explicit opt-in flags (`--pretty`).
|
|
49
|
+
- **Escape hatch.** `MANDREL_RESULT_DETAIL=inline` restores inline full
|
|
50
|
+
detail for interactive debugging; scripts using `emitTerseResult` honor it
|
|
51
|
+
automatically.
|
|
52
|
+
- **stdout purity is unchanged.** Scripts whose stdout is a machine contract
|
|
53
|
+
(Story #2278) keep logs on stderr; the digest is the *only* stdout line.
|
|
@@ -327,6 +327,27 @@
|
|
|
327
327
|
"codebaseSnapshot": {
|
|
328
328
|
"$ref": "#/$defs/codebaseSnapshot"
|
|
329
329
|
},
|
|
330
|
+
"complexityGate": {
|
|
331
|
+
"type": "object",
|
|
332
|
+
"description": "Plan-time ceremony-lite complexity gate. Routes trivial single-artifact seeds onto a collapsed plan/deliver path; conservative (full on any doubt). Never relaxes the Story-ticket / PR-to-main / repo-gates / security-baseline non-negotiables.",
|
|
333
|
+
"properties": {
|
|
334
|
+
"enabled": {
|
|
335
|
+
"type": "boolean",
|
|
336
|
+
"description": "Master switch. When false, every seed takes the full plan/deliver ceremony. Default true."
|
|
337
|
+
},
|
|
338
|
+
"maxSeedWords": {
|
|
339
|
+
"type": "integer",
|
|
340
|
+
"minimum": 0,
|
|
341
|
+
"description": "Seed prose word ceiling for the lite path. A seed above this many words is not trivial and takes the full path. Default 150."
|
|
342
|
+
},
|
|
343
|
+
"maxArtifacts": {
|
|
344
|
+
"type": "integer",
|
|
345
|
+
"minimum": 0,
|
|
346
|
+
"description": "Enumerated-artifact ceiling for the lite path. A seed enumerating more than this many candidate artifacts is multi-capability and takes the full path. Default 1."
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
"additionalProperties": false
|
|
350
|
+
},
|
|
330
351
|
"failOnSharedEditors": {
|
|
331
352
|
"type": "boolean",
|
|
332
353
|
"description": "When true, upgrade shared-editor conflict findings to hard errors (default false — advisory soft findings only)."
|
|
@@ -1150,8 +1171,14 @@
|
|
|
1150
1171
|
},
|
|
1151
1172
|
"mergeWatch": {
|
|
1152
1173
|
"type": "object",
|
|
1153
|
-
"description": "Knobs consumed by the close-and-land merge wait (Story #4543; defaults in `lib/orchestration/merge-poll.js`). `intervalSeconds` is the poll cadence between `gh pr view` probes after the arm. `maxWaitSeconds` bounds ONE invocation of the merge wait and its expiry returns a resumable `pending` terminal with no label mutation; `maxBudgetSeconds` bounds the CUMULATIVE wait across resumes (anchored at the PR's createdAt, so a resume does not restart the clock) and exhausting it is the genuine give-up that classifies and blocks. `updateAttempts` caps the bounded update of a behind-the-base PR.",
|
|
1174
|
+
"description": "Knobs consumed by the close-and-land merge wait (Story #4543; defaults in `lib/orchestration/merge-poll.js`). `mode` (Story #4698) selects the close-time merge posture. `intervalSeconds` is the poll cadence between `gh pr view` probes after the arm. `maxWaitSeconds` bounds ONE invocation of the merge wait and its expiry returns a resumable `pending` terminal with no label mutation; `maxBudgetSeconds` bounds the CUMULATIVE wait across resumes (anchored at the PR's createdAt, so a resume does not restart the clock) and exhausting it is the genuine give-up that classifies and blocks. `updateAttempts` caps the bounded update of a behind-the-base PR.",
|
|
1154
1175
|
"properties": {
|
|
1176
|
+
"mode": {
|
|
1177
|
+
"type": "string",
|
|
1178
|
+
"enum": ["sync", "async"],
|
|
1179
|
+
"default": "sync",
|
|
1180
|
+
"description": "Close-time merge-wait posture (Story #4698). `sync` (default) keeps the in-close foreground merge wait unchanged. `async` caps the per-invocation wait to a short ~60s probe window — long enough to catch an instant merge and, via the head-anchored required-check predicate, an instantly-red required check — then returns the resumable `pending` terminal (exit 3) with a `nextCommand`. Opt in when slow CI makes the foreground wait routinely expire: the worker launches `nextCommand` in the background instead of burning the host tool slot polling. `maxBudgetSeconds` (the cumulative give-up) is unchanged."
|
|
1181
|
+
},
|
|
1155
1182
|
"intervalSeconds": {
|
|
1156
1183
|
"type": "integer",
|
|
1157
1184
|
"minimum": 1,
|
|
@@ -1179,6 +1206,19 @@
|
|
|
1179
1206
|
},
|
|
1180
1207
|
"additionalProperties": false
|
|
1181
1208
|
},
|
|
1209
|
+
"review": {
|
|
1210
|
+
"type": "object",
|
|
1211
|
+
"description": "Close-scope review tuning (Story #4699). Governs the Story-scope local-lens pass that runs inside the close subprocess; the maker-blind code-review pass and all hard gates are unaffected.",
|
|
1212
|
+
"properties": {
|
|
1213
|
+
"lensDiffFloor": {
|
|
1214
|
+
"type": "integer",
|
|
1215
|
+
"minimum": 0,
|
|
1216
|
+
"default": 40,
|
|
1217
|
+
"description": "Changed-line floor for the close-scope lens walk (Story #4699). A diff strictly below this many changed lines (additions + deletions) with zero sensitive-path hits skips lens materialization and records the skip in the findings-yield ledger. Default 40; 0 disables the skip."
|
|
1218
|
+
}
|
|
1219
|
+
},
|
|
1220
|
+
"additionalProperties": false
|
|
1221
|
+
},
|
|
1182
1222
|
"quality": {
|
|
1183
1223
|
"type": "object",
|
|
1184
1224
|
"properties": {
|
|
@@ -1291,6 +1331,9 @@
|
|
|
1291
1331
|
"codeReview": {
|
|
1292
1332
|
"$ref": "#/$defs/codeReview"
|
|
1293
1333
|
},
|
|
1334
|
+
"review": {
|
|
1335
|
+
"$ref": "#/$defs/review"
|
|
1336
|
+
},
|
|
1294
1337
|
"refactorStage": {
|
|
1295
1338
|
"$ref": "#/$defs/refactorStage"
|
|
1296
1339
|
},
|
|
@@ -36,10 +36,12 @@
|
|
|
36
36
|
"lint",
|
|
37
37
|
"test",
|
|
38
38
|
"format",
|
|
39
|
+
"coverage-capture",
|
|
40
|
+
"check-baselines",
|
|
39
41
|
"check-maintainability",
|
|
40
42
|
"check-crap"
|
|
41
43
|
],
|
|
42
|
-
"description": "Stable gate identifier. Closed enum — additions require a schema bump."
|
|
44
|
+
"description": "Stable gate identifier. Closed enum — additions require a schema bump. Must be a superset of every gate name buildDefaultGates() can emit (lib/close-validation/gates.js); tests/close-validation-gates-enum.test.js pins that gate-list ⊆ enum invariant. `coverage-capture` and `check-baselines` are the real close-validation gates (Story #4697); `check-maintainability` / `check-crap` are the retired per-kind gates (Story #2210) kept so historical evidence records still validate."
|
|
43
45
|
},
|
|
44
46
|
"commitSha": {
|
|
45
47
|
"type": "string",
|
|
@@ -283,7 +283,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
283
283
|
emitSignal,
|
|
284
284
|
});
|
|
285
285
|
|
|
286
|
-
Logger.info(JSON.stringify(envelope
|
|
286
|
+
Logger.info(JSON.stringify(envelope));
|
|
287
287
|
|
|
288
288
|
if (exitCode !== 0) {
|
|
289
289
|
const names = envelope.unmetCriteria
|
|
@@ -69,7 +69,7 @@ async function main() {
|
|
|
69
69
|
// Mirror the retired heredoc's output: pretty-printed `{ quality, baselines }`
|
|
70
70
|
// to stdout. Use process.stdout.write (not console.log) per the no-console
|
|
71
71
|
// enforcement boundary.
|
|
72
|
-
process.stdout.write(`${JSON.stringify(result
|
|
72
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
73
73
|
return 0;
|
|
74
74
|
}
|
|
75
75
|
|
|
@@ -31,6 +31,7 @@ import fs from 'node:fs';
|
|
|
31
31
|
import { glob } from 'node:fs/promises';
|
|
32
32
|
import path from 'node:path';
|
|
33
33
|
import process from 'node:process';
|
|
34
|
+
import { pathToFileURL } from 'node:url';
|
|
34
35
|
import { parseArgs } from 'node:util';
|
|
35
36
|
import { buildStoryBody } from './lib/audit-to-stories/build-story-body.js';
|
|
36
37
|
import { classifyGroupsAgainstGitHub } from './lib/audit-to-stories/dedupe-against-github.js';
|
|
@@ -85,12 +86,30 @@ function tallyBySeverity(findings) {
|
|
|
85
86
|
return t;
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Test-only seam: when `AUDIT_TO_STORIES_PROVIDER_FIXTURE` names a module, load
|
|
91
|
+
* its default export as the dedup provider (ports) instead of the live GitHub
|
|
92
|
+
* provider. This lets the soft-fail contract be exercised end-to-end through
|
|
93
|
+
* the real `--scan` CLI with a search port that fails for a subset of groups
|
|
94
|
+
* (Story #4678, AC-8), with no network. Returns null when the env var is unset.
|
|
95
|
+
*
|
|
96
|
+
* @returns {Promise<object|null>}
|
|
97
|
+
*/
|
|
98
|
+
async function loadFixtureProvider() {
|
|
99
|
+
const fixturePath = process.env.AUDIT_TO_STORIES_PROVIDER_FIXTURE;
|
|
100
|
+
if (!fixturePath) return null;
|
|
101
|
+
const mod = await import(pathToFileURL(fixturePath).href);
|
|
102
|
+
return mod.default ?? null;
|
|
103
|
+
}
|
|
104
|
+
|
|
88
105
|
async function loadProvider({ createProviderImpl, resolveConfigImpl } = {}) {
|
|
89
106
|
// The provider is optional — when missing, the dedupe step emits a
|
|
90
107
|
// create-only classification and the workflow operator is informed. The
|
|
91
108
|
// `createProviderImpl` / `resolveConfigImpl` seams let a contract test drive
|
|
92
109
|
// this exact adapter (fingerprint + semantic-candidate ports) with an
|
|
93
110
|
// in-memory issue store instead of the live GitHub provider.
|
|
111
|
+
const fixture = await loadFixtureProvider();
|
|
112
|
+
if (fixture) return fixture;
|
|
94
113
|
try {
|
|
95
114
|
const resolveConfig =
|
|
96
115
|
resolveConfigImpl ??
|
|
@@ -180,6 +199,31 @@ function dedupSkippedWarning(reason) {
|
|
|
180
199
|
);
|
|
181
200
|
}
|
|
182
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Render the loud, operator-visible warning emitted when Phase 6 dedup ran but
|
|
204
|
+
* one or more groups' lookups could not complete (an HTTP 422, or a rate limit
|
|
205
|
+
* still exhausted after the endpoint budget's cooldown). Those groups degrade
|
|
206
|
+
* to `create` rather than aborting the whole scan (Story #4678); this warning
|
|
207
|
+
* names each affected group so the operator knows exactly which to check by
|
|
208
|
+
* hand. Mirrors the `dedupSkippedWarning` shape so a partially-checked plan
|
|
209
|
+
* reads as clearly as a wholly-unchecked one.
|
|
210
|
+
*
|
|
211
|
+
* Pure: returns the message string so `buildPlan` owns the single `Logger.warn`
|
|
212
|
+
* write site (stderr) and the text stays unit-testable.
|
|
213
|
+
*
|
|
214
|
+
* @param {Array<{ group: string, reason: string }>} entries
|
|
215
|
+
* @returns {string}
|
|
216
|
+
*/
|
|
217
|
+
function dedupDegradedWarning(entries) {
|
|
218
|
+
const lines = (entries ?? []).map((e) => ` - ${e.group}: ${e.reason}`);
|
|
219
|
+
return (
|
|
220
|
+
`dedup degraded for ${lines.length} group(s): their GitHub lookup could ` +
|
|
221
|
+
'not complete, so they are classified "create" WITHOUT a dedup check. A ' +
|
|
222
|
+
'run that creates Stories from this plan may open duplicates of these ' +
|
|
223
|
+
`groups — verify each by hand before opening:\n${lines.join('\n')}`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
183
227
|
async function buildPlan({ glob: pattern, severity, useProvider, ledger }) {
|
|
184
228
|
const reportPaths = await collectReportPaths(pattern ?? DEFAULT_GLOB);
|
|
185
229
|
if (reportPaths.length === 0) {
|
|
@@ -227,6 +271,12 @@ async function buildPlan({ glob: pattern, severity, useProvider, ledger }) {
|
|
|
227
271
|
classifications = result.classifications;
|
|
228
272
|
summary = result.summary;
|
|
229
273
|
dedupApplied = true;
|
|
274
|
+
// A partially-checked plan is a useful result — warn loudly (stderr, so
|
|
275
|
+
// the --scan JSON on stdout stays clean) naming the groups that degraded
|
|
276
|
+
// to create because their lookup could not complete (Story #4678).
|
|
277
|
+
if (summary.dedupDegraded?.count > 0) {
|
|
278
|
+
Logger.warn(dedupDegradedWarning(summary.dedupDegraded.groups));
|
|
279
|
+
}
|
|
230
280
|
} else {
|
|
231
281
|
// The provider could not resolve a searchIssues port — the dedup gate
|
|
232
282
|
// is silently a no-op without this. Surface it loudly (stderr, so the
|
|
@@ -488,6 +538,7 @@ export const __testing = {
|
|
|
488
538
|
buildPlan,
|
|
489
539
|
loadProvider,
|
|
490
540
|
dedupSkippedWarning,
|
|
541
|
+
dedupDegradedWarning,
|
|
491
542
|
buildAndGateStories,
|
|
492
543
|
runAuto,
|
|
493
544
|
resolveSeverityFloor,
|