mandrel 2.30.0 → 2.32.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/README.md +13 -17
- package/.agents/agents/acceptance-critic.md +1 -2
- package/.agents/docs/SDLC.md +4 -4
- package/.agents/docs/agentrc-reference.json +61 -57
- package/.agents/docs/configuration.md +274 -227
- package/.agents/docs/execution-reference.md +13 -14
- package/.agents/docs/quality-gates.md +195 -23
- package/.agents/instructions.md +2 -5
- package/.agents/rules/ci-remediation.md +44 -1
- package/.agents/rules/git-conventions-reference.md +27 -27
- package/.agents/rules/git-conventions.md +4 -2
- package/.agents/rules/known-tooling-behavior.md +66 -30
- package/.agents/rules/testing-standards.md +35 -71
- package/.agents/runtime-deps.json +0 -1
- package/.agents/schemas/agentrc.schema.json +1939 -1400
- package/.agents/schemas/lifecycle/README.md +21 -14
- package/.agents/schemas/lifecycle/ledger-record.schema.json +76 -22
- package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
- package/.agents/scripts/README.md +7 -29
- package/.agents/scripts/apply-quality-bootstrap.js +27 -34
- package/.agents/scripts/bootstrap.js +28 -26
- package/.agents/scripts/check-baseline-drift.js +73 -13
- package/.agents/scripts/check-baseline-scope.js +362 -0
- package/.agents/scripts/check-dead-exports.js +9 -1
- package/.agents/scripts/check-gherkin-corpus.js +508 -0
- package/.agents/scripts/check-knip-entries.js +136 -0
- package/.agents/scripts/check-lifecycle-lint.js +36 -112
- package/.agents/scripts/check-schema-references.js +1 -1
- package/.agents/scripts/diagnose-friction.js +7 -4
- package/.agents/scripts/generate-config-docs.js +263 -171
- package/.agents/scripts/install-matrix-assert.js +0 -1
- package/.agents/scripts/lib/ITicketingProvider.js +0 -58
- package/.agents/scripts/lib/audit-baselines/staleness.js +6 -6
- package/.agents/scripts/lib/audit-baselines/trend.js +7 -8
- package/.agents/scripts/lib/audit-baselines/weights.js +4 -5
- package/.agents/scripts/lib/audit-suite/checklist-threading.js +1 -1
- package/.agents/scripts/lib/audit-to-stories/build-story-body.js +0 -1
- package/.agents/scripts/lib/baselines/envelope.js +41 -60
- package/.agents/scripts/lib/baselines/git-base.js +30 -37
- package/.agents/scripts/lib/baselines/kinds/_crap-new-method-gate.js +103 -0
- package/.agents/scripts/lib/baselines/kinds/_crap-read.js +150 -0
- package/.agents/scripts/lib/baselines/kinds/crap.js +25 -65
- package/.agents/scripts/lib/baselines/orphan-pruner.js +233 -0
- package/.agents/scripts/lib/baselines/refresh-service.js +6 -8
- package/.agents/scripts/lib/baselines/scope-assert.js +223 -0
- package/.agents/scripts/lib/baselines/scope-inventory.js +314 -0
- package/.agents/scripts/lib/bdd-step-index.js +326 -0
- package/.agents/scripts/lib/bootstrap/install-ledger.js +5 -3
- package/.agents/scripts/lib/bootstrap/issue-forms-template.js +4 -6
- package/.agents/scripts/lib/bootstrap/manifest.js +17 -40
- package/.agents/scripts/lib/bootstrap/project-bootstrap.js +12 -59
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +62 -2
- package/.agents/scripts/lib/checks/loop-health.js +9 -37
- package/.agents/scripts/lib/child-exec.js +193 -0
- package/.agents/scripts/lib/cli/standard-args.js +1 -1
- package/.agents/scripts/lib/cli-args.js +64 -0
- package/.agents/scripts/lib/close-validation/gates.js +2 -2
- package/.agents/scripts/lib/close-validation/runner.js +3 -3
- package/.agents/scripts/lib/config/acceptance-eval.js +5 -52
- package/.agents/scripts/lib/config/commands.js +3 -5
- package/.agents/scripts/lib/config/explain.js +5 -7
- package/.agents/scripts/lib/config/gates/bundle-size.schema.js +32 -6
- package/.agents/scripts/lib/config/gates/coverage.schema.js +25 -5
- package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +12 -2
- package/.agents/scripts/lib/config/gates/crap.schema.js +68 -23
- package/.agents/scripts/lib/config/gates/duplication.schema.js +29 -17
- package/.agents/scripts/lib/config/gates/index.js +5 -2
- package/.agents/scripts/lib/config/gates/lighthouse.schema.js +34 -6
- package/.agents/scripts/lib/config/gates/lint.schema.js +11 -2
- package/.agents/scripts/lib/config/gates/maintainability.schema.js +37 -15
- package/.agents/scripts/lib/config/gates/mutation.schema.js +15 -3
- package/.agents/scripts/lib/config/gates/shared.js +58 -9
- package/.agents/scripts/lib/config/github.js +0 -1
- package/.agents/scripts/lib/config/limits.js +3 -48
- package/.agents/scripts/lib/config/qa.js +105 -0
- package/.agents/scripts/lib/config/temp-paths.js +6 -5
- package/.agents/scripts/lib/config-settings-schema-delivery.js +237 -56
- package/.agents/scripts/lib/config-settings-schema-quality.js +209 -29
- package/.agents/scripts/lib/config-settings-schema.js +386 -39
- package/.agents/scripts/lib/crap-baseline-join.js +126 -9
- package/.agents/scripts/lib/crap-utils.js +84 -520
- package/.agents/scripts/lib/dead-exports-knip.js +79 -10
- package/.agents/scripts/lib/degraded-mode.js +2 -2
- package/.agents/scripts/lib/doc-tiers.js +3 -3
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +46 -104
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +10 -8
- package/.agents/scripts/lib/fs-walk.js +52 -0
- package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
- package/.agents/scripts/lib/git-utils.js +16 -36
- package/.agents/scripts/lib/knip-entry-sync.js +469 -0
- package/.agents/scripts/lib/observability/metrics-ledger.js +1 -1
- package/.agents/scripts/lib/observability/runtime-friction.js +10 -0
- package/.agents/scripts/lib/observability/signal-validator.js +5 -85
- package/.agents/scripts/lib/observability/signals-writer.js +19 -62
- package/.agents/scripts/lib/observability/source-classifier.js +5 -7
- package/.agents/scripts/lib/observability/terse-result.js +3 -3
- package/.agents/scripts/lib/orchestration/behind-recovery.js +114 -0
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +7 -8
- package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +34 -33
- package/.agents/scripts/lib/orchestration/code-review.js +2 -2
- package/.agents/scripts/lib/orchestration/complexity-gate.js +43 -161
- package/.agents/scripts/lib/orchestration/diff-magnitude.js +4 -4
- package/.agents/scripts/lib/orchestration/label-transitions.js +3 -2
- package/.agents/scripts/lib/orchestration/lease-guard-shared.js +12 -38
- package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +5 -6
- package/.agents/scripts/lib/orchestration/plan-metrics.js +2 -3
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +6 -0
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +0 -1
- package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
- package/.agents/scripts/lib/orchestration/resolve-stories.js +5 -15
- package/.agents/scripts/lib/orchestration/review-providers/codex.js +1 -1
- package/.agents/scripts/lib/orchestration/review-providers/mi-exemptions.js +130 -0
- package/.agents/scripts/lib/orchestration/review-providers/native.js +30 -16
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +37 -26
- package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +376 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +161 -151
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +15 -3
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +10 -15
- package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +17 -2
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +5 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +157 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +0 -14
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +65 -25
- package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +20 -31
- package/.agents/scripts/lib/orchestration/spec-spill.js +17 -3
- package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +7 -6
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +2 -1
- package/.agents/scripts/lib/orchestration/task-body-validator.js +4 -1
- package/.agents/scripts/lib/orchestration/ticket-lease.js +28 -127
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -1
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +5 -5
- package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -4
- package/.agents/scripts/lib/orchestration/verify-tier-repair.js +107 -0
- package/.agents/scripts/lib/qa/coverage-verdict.js +5 -87
- package/.agents/scripts/lib/signals/detectors/common.js +1 -1
- package/.agents/scripts/lib/signals/index.js +8 -6
- package/.agents/scripts/lib/signals/schema.js +20 -25
- package/.agents/scripts/lib/signals/write.js +8 -8
- package/.agents/scripts/lib/story-body/story-body.js +12 -59
- package/.agents/scripts/lib/temp-retention.js +1 -1
- package/.agents/scripts/lib/templates/decomposer-prompts.js +16 -14
- package/.agents/scripts/lib/ticket-body-sections.js +4 -5
- package/.agents/scripts/lib/worktree/lifecycle/merge-reachability.js +13 -45
- package/.agents/scripts/lib/worktree/lifecycle/reap.js +4 -5
- package/.agents/scripts/lib/worktree-manager.js +2 -3
- package/.agents/scripts/lint-label-vocabulary.js +2 -24
- package/.agents/scripts/pr-watch-with-update.js +7 -5
- package/.agents/scripts/providers/github/cache.js +2 -2
- package/.agents/scripts/providers/github/comments.js +6 -28
- package/.agents/scripts/providers/github/compose.js +0 -15
- package/.agents/scripts/providers/github/errors.js +10 -27
- package/.agents/scripts/providers/github/request-helpers.js +1 -2
- package/.agents/scripts/providers/github/sub-issues.js +10 -218
- package/.agents/scripts/providers/github.js +4 -7
- package/.agents/scripts/prune-baseline-orphans.js +181 -0
- package/.agents/scripts/resolve-stories.js +0 -2
- package/.agents/scripts/run-lint.js +61 -61
- package/.agents/scripts/run-test-profile.js +6 -6
- package/.agents/scripts/run-verify.js +48 -30
- package/.agents/scripts/single-story-close.js +20 -0
- package/.agents/scripts/single-story-init.js +12 -35
- package/.agents/scripts/update-dead-exports-baseline.js +321 -0
- package/.agents/skills/core/gates-and-baselines/SKILL.md +2 -2
- package/.agents/skills/skills.index.json +2 -12
- package/.agents/skills/stack/qa/playwright/SKILL.md +48 -0
- package/.agents/workflows/audit-documentation.md +5 -6
- package/.agents/workflows/audit-to-stories.md +2 -2
- package/.agents/workflows/helpers/audit-lens-core.md +11 -12
- package/.agents/workflows/helpers/code-quality-guardrails.md +15 -14
- package/.agents/workflows/helpers/code-review.md +3 -8
- package/.agents/workflows/helpers/deliver-reference.md +2 -1
- package/.agents/workflows/helpers/deliver-story-reference.md +27 -16
- package/.agents/workflows/helpers/worktree-lifecycle.md +1 -2
- package/.agents/workflows/mandrel-update.md +10 -10
- package/.agents/workflows/qa-assist.md +15 -20
- package/.agents/workflows/qa-explore.md +9 -8
- package/README.md +1 -1
- package/docs/CHANGELOG.md +49 -0
- package/lib/migrations/index.js +2 -0
- package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +127 -0
- package/package.json +12 -3
- package/.agents/schemas/lifecycle/checkpoint.written.schema.json +0 -13
- package/.agents/schemas/lifecycle/close-validate.end.schema.json +0 -18
- package/.agents/schemas/lifecycle/close-validate.start.schema.json +0 -13
- package/.agents/schemas/lifecycle/code-review.end.schema.json +0 -30
- package/.agents/schemas/lifecycle/code-review.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/intervention.recorded.schema.json +0 -15
- package/.agents/schemas/lifecycle/loop.tick.schema.json +0 -20
- package/.agents/schemas/lifecycle/notification.emitted.schema.json +0 -18
- package/.agents/schemas/lifecycle/pr.created.schema.json +0 -14
- package/.agents/schemas/lifecycle/retro.end.schema.json +0 -16
- package/.agents/schemas/lifecycle/retro.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/story.blocked.schema.json +0 -13
- package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +0 -17
- package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +0 -15
- package/.agents/schemas/lifecycle/story.merged.schema.json +0 -13
- package/.agents/scripts/check-gherkin-placeholders.js +0 -663
- package/.agents/scripts/check-lifecycle-doc-drift.js +0 -411
- package/.agents/scripts/lib/audit-suite/cli.js +0 -64
- package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +0 -202
- package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +0 -212
- package/.agents/scripts/lib/checks/baseline-drift-main-checkout.js +0 -104
- package/.agents/scripts/lib/checks/push-hook-parity.js +0 -106
- package/.agents/scripts/lib/checks/windows-coverage-noise-floor.js +0 -92
- package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +0 -81
- package/.agents/scripts/lib/checks/worktree-residue-biome.js +0 -55
- package/.agents/scripts/lib/crap-baseline-index.js +0 -46
- package/.agents/scripts/lib/crap-utils-incremental.js +0 -113
- package/.agents/scripts/lib/dynamic-workflow/capability.js +0 -396
- package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -335
- package/.agents/scripts/lib/mutation/baseline-snapshot.js +0 -239
- package/.agents/scripts/lib/mutation/config-detector.js +0 -119
- package/.agents/scripts/lib/mutation/stryker-runner.js +0 -306
- package/.agents/scripts/lib/mutation/survivor-report.js +0 -160
- package/.agents/scripts/lib/observability/active-story-env.js +0 -170
- package/.agents/scripts/lib/observability/tool-trace-hook.js +0 -456
- package/.agents/scripts/lib/orchestration/acceptance-clusters.js +0 -111
- package/.agents/scripts/lib/orchestration/context-envelope.js +0 -277
- package/.agents/scripts/lib/orchestration/detectors-phase.js +0 -194
- package/.agents/scripts/lib/orchestration/lifecycle/bus.js +0 -309
- package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +0 -181
- package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +0 -229
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +0 -54
- package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -344
- package/.agents/scripts/lib/orchestration/lint-baseline-service.js +0 -114
- package/.agents/scripts/lib/orchestration/pr-base-guard.js +0 -37
- package/.agents/scripts/lib/orchestration/resolves-token.js +0 -127
- package/.agents/scripts/lib/orchestration/spec-section-validator.js +0 -130
- package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +0 -55
- package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +0 -211
- package/.agents/scripts/lib/planning-corpus.js +0 -37
- package/.agents/scripts/lib/qa/coverage-report.js +0 -181
- package/.agents/scripts/lib/qa/propose-missing-test.js +0 -95
- package/.agents/scripts/lib/qa/qa-context-hydrator.js +0 -217
- package/.agents/scripts/lib/signals/detectors/index.js +0 -14
- package/.agents/scripts/lib/signals/detectors/retry.js +0 -253
- package/.agents/scripts/lib/signals/detectors/rework.js +0 -167
- package/.agents/scripts/lib/signals/read.js +0 -268
- package/.agents/scripts/lib/signals/span-tree.js +0 -291
- package/.agents/scripts/lib/story-lifecycle.js +0 -194
- package/.agents/scripts/lib/story-plan.js +0 -379
- package/.agents/scripts/lib/util/phase-timer-state.js +0 -72
- package/.agents/scripts/lib/util/phase-timer.js +0 -163
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +0 -169
- package/.agents/scripts/lint-baseline.js +0 -507
- package/.agents/scripts/providers/github/prs.js +0 -103
- package/.agents/scripts/signals-view.js +0 -309
- package/.agents/scripts/story-plan.js +0 -370
- package/.agents/scripts/sync-branch-from-base.js +0 -149
- package/.agents/scripts/validate-docs-freshness.js +0 -314
- package/.agents/skills/core/diagnose-friction/SKILL.md +0 -78
- package/.agents/workflows/helpers/signals.md +0 -112
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
// .agents/scripts/lib/orchestration/
|
|
1
|
+
// .agents/scripts/lib/orchestration/pr-watch.js
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* pr-watch.js — the required-check poll loop for an open PR.
|
|
4
|
+
* Story #2256 (Epic #2172).
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* **Not a listener, and no longer shelved among them.** This shipped as the
|
|
7
|
+
* `Watcher` lifecycle listener subscribing to `pr.created`. Story #5006
|
|
8
|
+
* deleted the class but left the plain primitive at
|
|
9
|
+
* `lifecycle/listeners/watcher.js` so its CLI consumer's import stayed
|
|
10
|
+
* stable. Story #5024 then retired the bus itself, which took the listener
|
|
11
|
+
* concept — and the `listeners/` directory — with it, so the primitive moved
|
|
12
|
+
* here beside [`merge-poll.js`](./merge-poll.js), the home Story #4545 chose
|
|
13
|
+
* for `MergeWatcher`'s surviving parts. The only production consumer,
|
|
14
|
+
* `pr-watch-with-update.js`, has always driven {@link watchPrToTerminal}
|
|
15
|
+
* directly, with no bus.
|
|
11
16
|
*
|
|
12
17
|
* Critical contract:
|
|
13
18
|
* - Required-check **names** are resolved from `gh pr checks` at
|
|
@@ -16,29 +21,18 @@
|
|
|
16
21
|
* branch-protection ruleset on GitHub is the source of truth at
|
|
17
22
|
* watch time. This guards against config drift (a config file that
|
|
18
23
|
* hasn't been updated after a protection rule changed on GitHub
|
|
19
|
-
* would otherwise cause the
|
|
24
|
+
* would otherwise cause the watch to either skip a required check
|
|
20
25
|
* or wait for a removed one indefinitely).
|
|
21
26
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* replay defence, this is sufficient — re-running `/deliver` after
|
|
26
|
-
* a crash will produce a NEW seqId and the listener legitimately
|
|
27
|
-
* re-runs the poll loop (which is itself idempotent: the outcome map
|
|
28
|
-
* always reflects the live GitHub state).
|
|
29
|
-
*
|
|
30
|
-
* Side-effect firewall: the listener shells out to `gh` and records
|
|
31
|
-
* its outcome in the in-memory `classifications` log. It does NOT
|
|
32
|
-
* mutate ticket labels, post comments, call `notify`, or emit on the
|
|
33
|
-
* bus — the production consumer (`pr-watch-with-update.js`) drives
|
|
34
|
-
* `watchPrToTerminal` directly without a bus, and the retired
|
|
35
|
-
* `epic.watch.start` / `epic.watch.end` bus events went with the
|
|
36
|
-
* Epic-orchestration stratum.
|
|
27
|
+
* Side-effect firewall: the loop shells out to `gh` through injectable
|
|
28
|
+
* ports and returns a verdict. It does NOT mutate ticket labels, post
|
|
29
|
+
* comments, call `notify`, or write any ledger.
|
|
37
30
|
*/
|
|
38
31
|
|
|
39
32
|
import { spawnSync } from 'node:child_process';
|
|
40
33
|
|
|
41
|
-
import { parsePrNumberFromUrl } from '
|
|
34
|
+
import { parsePrNumberFromUrl } from '../github-url.js';
|
|
35
|
+
import { applyBehindUpdate } from './behind-recovery.js';
|
|
42
36
|
|
|
43
37
|
/**
|
|
44
38
|
* Map `gh pr checks` `state` values to the canonical lowercase outcome
|
|
@@ -121,8 +115,8 @@ export function normalizeCheckState(raw) {
|
|
|
121
115
|
}
|
|
122
116
|
|
|
123
117
|
/**
|
|
124
|
-
* Parse a PR number out of a PR URL.
|
|
125
|
-
* `
|
|
118
|
+
* Parse a PR number out of a PR URL. Callers hand in the URL `gh pr
|
|
119
|
+
* create` returned; `gh pr checks` accepts either the URL or the
|
|
126
120
|
* number — we pass the URL through verbatim, but the helper still
|
|
127
121
|
* exists for tests asserting we never silently coerce a malformed URL.
|
|
128
122
|
*
|
|
@@ -184,9 +178,9 @@ function ghPrChecks({ prUrl, cwd, repo, spawnFn = spawnSync }) {
|
|
|
184
178
|
}
|
|
185
179
|
|
|
186
180
|
/**
|
|
187
|
-
* Default `gh pr view` spawn — probes `mergeStateStatus` so the
|
|
181
|
+
* Default `gh pr view` spawn — probes `mergeStateStatus` so the watch loop
|
|
188
182
|
* can detect the BEHIND condition (PR head is behind its base branch)
|
|
189
|
-
* AFTER every required check is green.
|
|
183
|
+
* AFTER every required check is green. Injectable so tests can stub.
|
|
190
184
|
*/
|
|
191
185
|
function ghPrView({ prUrl, cwd, repo, spawnFn = spawnSync }) {
|
|
192
186
|
const result = spawnFn(
|
|
@@ -377,8 +371,8 @@ function defaultSleep(ms) {
|
|
|
377
371
|
* an empty stdout) are logged and skipped — the outer cap eventually
|
|
378
372
|
* short-circuits if `gh` is unrecoverably broken.
|
|
379
373
|
*
|
|
380
|
-
* Exported so
|
|
381
|
-
* call
|
|
374
|
+
* Exported so {@link watchPrToTerminal}'s BEHIND-recovery outer loop can
|
|
375
|
+
* call it once per CI cycle without duplicating the inner logic.
|
|
382
376
|
*
|
|
383
377
|
* @param {object} opts
|
|
384
378
|
* @param {string} opts.prUrl
|
|
@@ -434,9 +428,10 @@ export async function pollUntilTerminal({
|
|
|
434
428
|
* commit after each. Plain async function with NO bus coupling: it
|
|
435
429
|
* shells out to `gh` (via injectable spawns) and returns the verdict.
|
|
436
430
|
*
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
*
|
|
431
|
+
* The load-bearing primitive the `pr-watch-with-update.js` CLI drives.
|
|
432
|
+
* Story #3902 introduced it so the (since-retired) `Watcher` listener and
|
|
433
|
+
* the CLI could not drift apart on polling or BEHIND-recovery; the CLI is
|
|
434
|
+
* now its only caller.
|
|
440
435
|
*
|
|
441
436
|
* @param {object} opts
|
|
442
437
|
* @param {string} opts.prUrl PR URL or number (passed to `gh` verbatim).
|
|
@@ -463,11 +458,10 @@ export async function pollUntilTerminal({
|
|
|
463
458
|
* real `setTimeout`-backed sleep; tests override with a no-op.
|
|
464
459
|
* @param {{ info?: Function, warn?: Function, debug?: Function }} opts.logger
|
|
465
460
|
* @param {{status:number,stdout:string,stderr:string}} [opts.firstProbe]
|
|
466
|
-
* Optional already-issued `gh pr checks` result.
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
* and the loop issues the first probe itself.
|
|
461
|
+
* Optional already-issued `gh pr checks` result. A caller that has
|
|
462
|
+
* already probed once to resolve the required-check names threads it
|
|
463
|
+
* here so the loop does not double-spend the first `gh pr checks` call.
|
|
464
|
+
* Omit it (the CLI path) and the loop issues the first probe itself.
|
|
471
465
|
* @returns {Promise<{
|
|
472
466
|
* outcomes: object,
|
|
473
467
|
* requiredChecks: string[],
|
|
@@ -502,8 +496,8 @@ export async function watchPrToTerminal({
|
|
|
502
496
|
firstProbe,
|
|
503
497
|
}) {
|
|
504
498
|
// First probe: resolve the required-check name set at runtime. Reuse a
|
|
505
|
-
// caller-supplied probe (
|
|
506
|
-
//
|
|
499
|
+
// caller-supplied probe (issued to resolve the required-check names) so
|
|
500
|
+
// we never double-spend the first `gh` call.
|
|
507
501
|
const first = firstProbe ?? ghPrChecksFn({ prUrl, cwd, repo });
|
|
508
502
|
// `gh` exits 8 when checks are still pending; this is expected and
|
|
509
503
|
// does not indicate failure. Any other non-zero status with no
|
|
@@ -579,6 +573,9 @@ export async function watchPrToTerminal({
|
|
|
579
573
|
// Bounded by `maxUpdates` so a racing base branch can't ping-pong
|
|
580
574
|
// indefinitely.
|
|
581
575
|
if (!allTerminal(outcomes) || !allGreen(outcomes)) break;
|
|
576
|
+
// Budget is checked before the `gh pr view` spawn so an exhausted arm
|
|
577
|
+
// costs no extra round-trip; the shared helper re-checks it as a
|
|
578
|
+
// fail-safe, and its callback therefore never fires on this path.
|
|
582
579
|
if (updatesApplied >= maxUpdates) break;
|
|
583
580
|
const view = ghPrViewFn({ prUrl, cwd, repo });
|
|
584
581
|
if (view.status !== 0) {
|
|
@@ -587,15 +584,26 @@ export async function watchPrToTerminal({
|
|
|
587
584
|
);
|
|
588
585
|
break;
|
|
589
586
|
}
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
587
|
+
const recovery = await applyBehindUpdate({
|
|
588
|
+
mergeStateStatus: parseMergeStateStatus(view.stdout),
|
|
589
|
+
updatesUsed: updatesApplied,
|
|
590
|
+
maxUpdates,
|
|
591
|
+
updateBranch: async () => {
|
|
592
|
+
const update = ghPrUpdateBranchFn({ prUrl, cwd, repo });
|
|
593
|
+
return update.status === 0
|
|
594
|
+
? { ok: true }
|
|
595
|
+
: {
|
|
596
|
+
ok: false,
|
|
597
|
+
detail: `status=${update.status}: ${update.stderr}`,
|
|
598
|
+
};
|
|
599
|
+
},
|
|
600
|
+
onUpdateFailed: (detail) =>
|
|
601
|
+
logger.warn?.(`[Watcher] gh pr update-branch failed (${detail})`),
|
|
602
|
+
});
|
|
603
|
+
// Anything but a landed fast-forward ends the arm: not BEHIND means
|
|
604
|
+
// there is nothing to recover, and a failed update must not silently
|
|
605
|
+
// re-poll as though the head moved.
|
|
606
|
+
if (!recovery.updated) break;
|
|
599
607
|
updatesApplied += 1;
|
|
600
608
|
logger.info?.(
|
|
601
609
|
`[Watcher] PR BEHIND base — issued gh pr update-branch (#${updatesApplied}/${maxUpdates}); re-polling required checks.`,
|
|
@@ -643,161 +651,3 @@ export async function watchPrToTerminal({
|
|
|
643
651
|
stillRunning,
|
|
644
652
|
};
|
|
645
653
|
}
|
|
646
|
-
|
|
647
|
-
/**
|
|
648
|
-
* Watcher listener.
|
|
649
|
-
*/
|
|
650
|
-
export class Watcher {
|
|
651
|
-
/**
|
|
652
|
-
* @param {object} opts
|
|
653
|
-
* @param {object} opts.bus
|
|
654
|
-
* @param {string} [opts.cwd]
|
|
655
|
-
* @param {number} [opts.pollIntervalMs] default 10_000.
|
|
656
|
-
* @param {number} [opts.maxPolls] safety cap on iterations; default
|
|
657
|
-
* 180 (≈30 min @ 10s).
|
|
658
|
-
* @param {number} [opts.maxUpdates] cap on `gh pr update-branch`
|
|
659
|
-
* recovery calls per `pr.created` event; default 3. Mirrors the
|
|
660
|
-
* legacy `pr-watch-with-update` cap so a racing base branch
|
|
661
|
-
* can't induce an infinite update-branch ping-pong.
|
|
662
|
-
* @param {number} [opts.maxResumes] Story #4358: how many times to
|
|
663
|
-
* re-arm the poll loop after the cap fires with checks still pending
|
|
664
|
-
* (and none failed) before declaring `still-running`; default 0.
|
|
665
|
-
* @param {Function} [opts.ghPrChecksFn] override for tests.
|
|
666
|
-
* @param {Function} [opts.ghPrViewFn] override for tests; resolves
|
|
667
|
-
* `mergeStateStatus` for the BEHIND-recovery gate.
|
|
668
|
-
* @param {Function} [opts.ghPrUpdateBranchFn] override for tests;
|
|
669
|
-
* issues the fast-forward update on the PR.
|
|
670
|
-
* @param {Function} [opts.sleepFn] override for tests.
|
|
671
|
-
* @param {{ info?: Function, warn?: Function, debug?: Function }} [opts.logger]
|
|
672
|
-
*/
|
|
673
|
-
constructor(opts = {}) {
|
|
674
|
-
if (!opts.bus || typeof opts.bus.on !== 'function') {
|
|
675
|
-
throw new TypeError('Watcher requires a bus with on()');
|
|
676
|
-
}
|
|
677
|
-
this.bus = opts.bus;
|
|
678
|
-
this.cwd = opts.cwd ?? process.cwd();
|
|
679
|
-
this.pollIntervalMs = Number.isInteger(opts.pollIntervalMs)
|
|
680
|
-
? opts.pollIntervalMs
|
|
681
|
-
: 10_000;
|
|
682
|
-
this.maxPolls = Number.isInteger(opts.maxPolls) ? opts.maxPolls : 180;
|
|
683
|
-
this.maxUpdates =
|
|
684
|
-
Number.isInteger(opts.maxUpdates) && opts.maxUpdates >= 0
|
|
685
|
-
? opts.maxUpdates
|
|
686
|
-
: 3;
|
|
687
|
-
this.maxResumes =
|
|
688
|
-
Number.isInteger(opts.maxResumes) && opts.maxResumes >= 0
|
|
689
|
-
? opts.maxResumes
|
|
690
|
-
: 0;
|
|
691
|
-
this.ghPrChecksFn = opts.ghPrChecksFn ?? ghPrChecks;
|
|
692
|
-
this.ghPrViewFn = opts.ghPrViewFn ?? ghPrView;
|
|
693
|
-
this.ghPrUpdateBranchFn = opts.ghPrUpdateBranchFn ?? ghPrUpdateBranch;
|
|
694
|
-
this.sleepFn = opts.sleepFn ?? defaultSleep;
|
|
695
|
-
this.logger = opts.logger ?? console;
|
|
696
|
-
/** @type {Set<string>} `${event}:${seqId}` idempotency cache. */
|
|
697
|
-
this._seen = new Set();
|
|
698
|
-
/**
|
|
699
|
-
* Classification log — every `pr.created` we observe lands here
|
|
700
|
-
* with the outcome (`watched`, `failed`, `skipped-duplicate`,
|
|
701
|
-
* `still-running`, `timed-out`). Mirrors the Finalizer / Reconciler
|
|
702
|
-
* "no silent skip" surface.
|
|
703
|
-
*/
|
|
704
|
-
this.classifications = [];
|
|
705
|
-
this.events = Object.freeze(['pr.created']);
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
register() {
|
|
709
|
-
return this.events.map((event) =>
|
|
710
|
-
this.bus.on(event, async (ctx) => this.handle(ctx)),
|
|
711
|
-
);
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
async handle({ event, seqId, payload }) {
|
|
715
|
-
const key = `${event}:${seqId}`;
|
|
716
|
-
if (this._seen.has(key)) {
|
|
717
|
-
this.classifications.push({
|
|
718
|
-
event,
|
|
719
|
-
seqId,
|
|
720
|
-
outcome: 'skipped',
|
|
721
|
-
reason: 'duplicate-seqId',
|
|
722
|
-
});
|
|
723
|
-
this.logger.debug?.(`[Watcher] skip duplicate ${key} (idempotent)`);
|
|
724
|
-
return;
|
|
725
|
-
}
|
|
726
|
-
this._seen.add(key);
|
|
727
|
-
|
|
728
|
-
const prUrl = payload?.prUrl;
|
|
729
|
-
if (typeof prUrl !== 'string' || prUrl.length === 0) {
|
|
730
|
-
this.classifications.push({
|
|
731
|
-
event,
|
|
732
|
-
seqId,
|
|
733
|
-
outcome: 'failed',
|
|
734
|
-
reason: 'no-pr-url',
|
|
735
|
-
});
|
|
736
|
-
return;
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
// First probe: resolve the required-check name set at runtime BEFORE
|
|
740
|
-
// the (potentially long) poll loop runs. We thread this probe into
|
|
741
|
-
// `watchPrToTerminal` (via `firstProbe`) so the shared loop reuses it
|
|
742
|
-
// instead of double-spending the first `gh pr checks` call.
|
|
743
|
-
const first = this.ghPrChecksFn({ prUrl, cwd: this.cwd });
|
|
744
|
-
const firstEntries = parseGhPrChecks(first.stdout);
|
|
745
|
-
if (firstEntries.length === 0 && first.status !== 0 && first.status !== 8) {
|
|
746
|
-
this.classifications.push({
|
|
747
|
-
event,
|
|
748
|
-
seqId,
|
|
749
|
-
outcome: 'failed',
|
|
750
|
-
reason: `gh-checks-failed:status=${first.status}`,
|
|
751
|
-
});
|
|
752
|
-
this.logger.warn?.(
|
|
753
|
-
`[Watcher] gh pr checks failed (status=${first.status}): ${first.stderr}`,
|
|
754
|
-
);
|
|
755
|
-
return;
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
const requiredChecks = firstEntries.map((e) => e.name);
|
|
759
|
-
|
|
760
|
-
// Delegate the poll + BEHIND-recovery loop to the shared plain
|
|
761
|
-
// primitive so the CLI (`pr-watch-with-update.js`) and this listener
|
|
762
|
-
// run identical logic.
|
|
763
|
-
const { polls, updatesApplied, resumesApplied, terminal, stillRunning } =
|
|
764
|
-
await watchPrToTerminal({
|
|
765
|
-
prUrl,
|
|
766
|
-
cwd: this.cwd,
|
|
767
|
-
maxPolls: this.maxPolls,
|
|
768
|
-
maxUpdates: this.maxUpdates,
|
|
769
|
-
maxResumes: this.maxResumes,
|
|
770
|
-
pollIntervalMs: this.pollIntervalMs,
|
|
771
|
-
ghPrChecksFn: this.ghPrChecksFn,
|
|
772
|
-
ghPrViewFn: this.ghPrViewFn,
|
|
773
|
-
ghPrUpdateBranchFn: this.ghPrUpdateBranchFn,
|
|
774
|
-
sleepFn: this.sleepFn,
|
|
775
|
-
logger: this.logger,
|
|
776
|
-
firstProbe: first,
|
|
777
|
-
});
|
|
778
|
-
|
|
779
|
-
// `still-running` (slow CI, not red) is a distinct classification from
|
|
780
|
-
// a genuine `timed-out` — reserved for a check that never went
|
|
781
|
-
// terminal within the poll cap AND the resume budget while none
|
|
782
|
-
// failed. `watched` covers every terminal arm (green or red).
|
|
783
|
-
const outcome = terminal
|
|
784
|
-
? 'watched'
|
|
785
|
-
: stillRunning
|
|
786
|
-
? 'still-running'
|
|
787
|
-
: 'timed-out';
|
|
788
|
-
this.classifications.push({
|
|
789
|
-
event,
|
|
790
|
-
seqId,
|
|
791
|
-
outcome,
|
|
792
|
-
polls,
|
|
793
|
-
updatesApplied,
|
|
794
|
-
resumesApplied,
|
|
795
|
-
requiredChecks: requiredChecks.length,
|
|
796
|
-
});
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
reset() {
|
|
800
|
-
this._seen.clear();
|
|
801
|
-
this.classifications = [];
|
|
802
|
-
}
|
|
803
|
-
}
|
|
@@ -301,9 +301,6 @@ export async function readNativeBlockedBy({
|
|
|
301
301
|
* @param {Map<number, number[]>} nativeEdges
|
|
302
302
|
* @param {number[]} foreignDone Ids outside the set already satisfied.
|
|
303
303
|
* @param {(msg: string) => void} [warn]
|
|
304
|
-
* @param {object} [injectedRules] Test seam forwarded to the shape
|
|
305
|
-
* derivation — skips the `audit-rules.json` disk read. Production callers
|
|
306
|
-
* omit it (the real manifest, memoized per process, is the default).
|
|
307
304
|
* @returns {{ kind: string, stories: object[], dag: object[], done: number[] }}
|
|
308
305
|
*/
|
|
309
306
|
export function buildStoriesEnvelope({
|
|
@@ -311,8 +308,6 @@ export function buildStoriesEnvelope({
|
|
|
311
308
|
nativeEdges = new Map(),
|
|
312
309
|
foreignDone = [],
|
|
313
310
|
warn,
|
|
314
|
-
config,
|
|
315
|
-
injectedRules,
|
|
316
311
|
}) {
|
|
317
312
|
const sorted = [...stories].sort((a, b) => a.id - b.id);
|
|
318
313
|
const inSetDone = sorted.filter(isSatisfiedBlocker).map((s) => s.id);
|
|
@@ -324,8 +319,8 @@ export function buildStoriesEnvelope({
|
|
|
324
319
|
// acceptance-critic sub-agent boots) or `subagent` (the conservative
|
|
325
320
|
// default). Model-side fan-out only; close gates are untouched.
|
|
326
321
|
//
|
|
327
|
-
// `storyCount` is the premise that decides it, and it is this call
|
|
328
|
-
//
|
|
322
|
+
// `storyCount` is the ONLY premise that decides it, and it is this call
|
|
323
|
+
// site's whole argument: `inline` names the router's ONE session, so it is
|
|
329
324
|
// granted only to a run resolving exactly ONE Story, which has no
|
|
330
325
|
// concurrent sibling to share that session with. Passing the resolved set
|
|
331
326
|
// size here is therefore what makes the envelope self-consistent with the
|
|
@@ -336,19 +331,14 @@ export function buildStoriesEnvelope({
|
|
|
336
331
|
// a caller reads for a given `--ids` list never changes as siblings land
|
|
337
332
|
// mid-run. The `route::lite` label is a human-visible hint only, never the
|
|
338
333
|
// control signal.
|
|
339
|
-
stories: sorted.map(({ id, title,
|
|
334
|
+
stories: sorted.map(({ id, title, url, labels, state }) => ({
|
|
340
335
|
id,
|
|
341
336
|
title,
|
|
342
337
|
url,
|
|
343
338
|
labels,
|
|
344
339
|
state,
|
|
345
|
-
dispatchMode: resolveStoryDispatchMode({
|
|
346
|
-
|
|
347
|
-
labels,
|
|
348
|
-
config,
|
|
349
|
-
storyCount: sorted.length,
|
|
350
|
-
injectedRules,
|
|
351
|
-
}).mode,
|
|
340
|
+
dispatchMode: resolveStoryDispatchMode({ storyCount: sorted.length })
|
|
341
|
+
.mode,
|
|
352
342
|
})),
|
|
353
343
|
dag: storiesToDag(sorted, nativeEdges, warn),
|
|
354
344
|
done: [...new Set([...inSetDone, ...foreignDone])].sort((a, b) => a - b),
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* `provider: native` explicitly; the probe is the only thing that
|
|
20
20
|
* routes between "configured backend present" and "configured backend
|
|
21
21
|
* missing". The adapter never consults a GitHub provider — the
|
|
22
|
-
* orchestrator owns posting
|
|
22
|
+
* orchestrator owns posting and upserting.
|
|
23
23
|
*
|
|
24
24
|
* @typedef {import('./types.js').Finding} Finding
|
|
25
25
|
* @typedef {import('./types.js').ReviewInput} ReviewInput
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* review-providers/mi-exemptions.js — the maintainability gate's exemption list,
|
|
3
|
+
* as the native review provider reads it.
|
|
4
|
+
*
|
|
5
|
+
* `delivery.quality.gates.maintainability.ignoreGlobs` is the one declared
|
|
6
|
+
* answer to "which files is the maintainability index meaningless for" —
|
|
7
|
+
* declarative schema blobs, generated code, vendored trees. Every other MI
|
|
8
|
+
* consumer already honours it: the baseline writer excludes those files
|
|
9
|
+
* (`update-maintainability-baseline.js`), so the `check-baselines.js` ratchet
|
|
10
|
+
* and the pre-merge MI advisory never see them either.
|
|
11
|
+
*
|
|
12
|
+
* `review-providers/native.js` did not, and the split produced a live
|
|
13
|
+
* contradiction on Story #5007 / PR #5022: the ratchet PASSED while the review
|
|
14
|
+
* lens raised a **critical blocker** on three exempted
|
|
15
|
+
* `config-settings-schema*.js` modules in the same close run. Because a critical
|
|
16
|
+
* finding halts `single-story-close.js` before auto-merge, the only way to land
|
|
17
|
+
* legitimate work was to merge the PR by hand. A gate that must be
|
|
18
|
+
* hand-bypassed to ship is not a gate.
|
|
19
|
+
*
|
|
20
|
+
* This module is that reconciliation, kept separate from the provider so the
|
|
21
|
+
* exemption concern has one home and the provider keeps one reason to change.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { getQuality } from '../../config/quality.js';
|
|
25
|
+
import { resolveConfig } from '../../config-resolver.js';
|
|
26
|
+
import { isIgnoredByGlobs } from '../../maintainability-utils.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Read `delivery.quality.gates.maintainability.ignoreGlobs` so a review's
|
|
30
|
+
* maintainability dimension scores the same file set the ratchet does.
|
|
31
|
+
*
|
|
32
|
+
* Best-effort and total. A config that cannot be resolved yields `[]`, which
|
|
33
|
+
* scores every changed JS file. That direction is deliberate: degrading to
|
|
34
|
+
* "score everything" can only produce an advisory the operator must read,
|
|
35
|
+
* whereas degrading to "score nothing" would silently retire the dimension.
|
|
36
|
+
*
|
|
37
|
+
* @param {{ resolveConfigFn?: typeof resolveConfig, getQualityFn?: typeof getQuality }} [deps]
|
|
38
|
+
* @returns {string[]} minimatch patterns; `[]` when unset or unresolvable.
|
|
39
|
+
*/
|
|
40
|
+
export function resolveMaintainabilityIgnoreGlobs({
|
|
41
|
+
resolveConfigFn = resolveConfig,
|
|
42
|
+
getQualityFn = getQuality,
|
|
43
|
+
} = {}) {
|
|
44
|
+
try {
|
|
45
|
+
const globs = getQualityFn(resolveConfigFn())?.maintainability?.ignoreGlobs;
|
|
46
|
+
return Array.isArray(globs) ? globs.slice() : [];
|
|
47
|
+
} catch {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Pure: split a changed-file list into the set to score and the set the
|
|
54
|
+
* maintainability gate exempts.
|
|
55
|
+
*
|
|
56
|
+
* Matching funnels through `maintainability-utils.js#isIgnoredByGlobs` — the
|
|
57
|
+
* declared single source of truth for how the MI scorer decides a file is
|
|
58
|
+
* ignored — so an exempted file is excluded here by exactly the same rule that
|
|
59
|
+
* kept it out of the baseline. Re-implementing the match with a local
|
|
60
|
+
* `minimatch` call is what let the two surfaces disagree in the first place.
|
|
61
|
+
*
|
|
62
|
+
* Module-local: {@link scopeMaintainabilityFiles} is the single door, and its
|
|
63
|
+
* `scored` / `ignored` split is where this behaviour is observable.
|
|
64
|
+
*
|
|
65
|
+
* @param {string[]} files
|
|
66
|
+
* @param {string[]} ignoreGlobs
|
|
67
|
+
* @param {string} cwd root for repo-relative glob resolution
|
|
68
|
+
* @returns {{ scored: string[], ignored: string[] }}
|
|
69
|
+
*/
|
|
70
|
+
function partitionByIgnoreGlobs(files, ignoreGlobs, cwd) {
|
|
71
|
+
if (!Array.isArray(ignoreGlobs) || ignoreGlobs.length === 0) {
|
|
72
|
+
return { scored: files, ignored: [] };
|
|
73
|
+
}
|
|
74
|
+
const scored = [];
|
|
75
|
+
const ignored = [];
|
|
76
|
+
for (const relPath of files) {
|
|
77
|
+
if (isIgnoredByGlobs(relPath, ignoreGlobs, cwd)) ignored.push(relPath);
|
|
78
|
+
else scored.push(relPath);
|
|
79
|
+
}
|
|
80
|
+
return { scored, ignored };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Render the operator-facing notice naming the files the gate exempted, or
|
|
85
|
+
* `null` when nothing was exempted so the caller can `if` past the log call.
|
|
86
|
+
*
|
|
87
|
+
* The notice exists because silence is ambiguous: an operator reading a review
|
|
88
|
+
* that says nothing about three changed schema modules cannot tell "scored and
|
|
89
|
+
* healthy" from "never scored".
|
|
90
|
+
*
|
|
91
|
+
* Module-local: reached through {@link scopeMaintainabilityFiles}'s `notice`.
|
|
92
|
+
*
|
|
93
|
+
* @param {string[]|undefined} ignoredFiles
|
|
94
|
+
* @returns {string|null}
|
|
95
|
+
*/
|
|
96
|
+
function formatExemptionNotice(ignoredFiles) {
|
|
97
|
+
const files = Array.isArray(ignoredFiles) ? ignoredFiles : [];
|
|
98
|
+
if (files.length === 0) return null;
|
|
99
|
+
return (
|
|
100
|
+
`[native-review] Maintainability: ${files.length} changed file(s) exempt via ` +
|
|
101
|
+
`delivery.quality.gates.maintainability.ignoreGlobs — not scored: ${files.join(', ')}.`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Resolve the exemption list and split a review's changed-file set into the
|
|
107
|
+
* paths whose maintainability should be scored and the paths the gate exempts.
|
|
108
|
+
*
|
|
109
|
+
* This is the one door the native provider uses: it keeps the resolve → match →
|
|
110
|
+
* report sequence here rather than spread across the provider, so the provider
|
|
111
|
+
* carries no knowledge of how an exemption is decided.
|
|
112
|
+
*
|
|
113
|
+
* @param {string[]} changedFiles
|
|
114
|
+
* @param {{
|
|
115
|
+
* cwd: string,
|
|
116
|
+
* resolveIgnoreGlobsFn?: typeof resolveMaintainabilityIgnoreGlobs,
|
|
117
|
+
* }} opts
|
|
118
|
+
* @returns {{ scored: string[], ignored: string[], notice: string|null }}
|
|
119
|
+
*/
|
|
120
|
+
export function scopeMaintainabilityFiles(
|
|
121
|
+
changedFiles,
|
|
122
|
+
{ cwd, resolveIgnoreGlobsFn = resolveMaintainabilityIgnoreGlobs } = {},
|
|
123
|
+
) {
|
|
124
|
+
const { scored, ignored } = partitionByIgnoreGlobs(
|
|
125
|
+
changedFiles,
|
|
126
|
+
resolveIgnoreGlobsFn(),
|
|
127
|
+
cwd,
|
|
128
|
+
);
|
|
129
|
+
return { scored, ignored, notice: formatExemptionNotice(ignored) };
|
|
130
|
+
}
|
|
@@ -7,14 +7,24 @@
|
|
|
7
7
|
*
|
|
8
8
|
* 1. Diffs `headRef` against `baseRef` to enumerate changed files.
|
|
9
9
|
* 2. Runs scoped lint (biome + markdownlint) over the changed surface.
|
|
10
|
-
* 3. Computes per-file maintainability reports for changed JS files
|
|
10
|
+
* 3. Computes per-file maintainability reports for changed JS files, minus
|
|
11
|
+
* the files the maintainability gate exempts (see below).
|
|
11
12
|
* 4. Maps each signal to a `Finding` with a `severity` ∈ {critical, high,
|
|
12
13
|
* medium, suggestion}.
|
|
13
14
|
*
|
|
14
|
-
* The
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* **The maintainability dimension honours the gate's exemption list**, read via
|
|
16
|
+
* [`mi-exemptions.js`](mi-exemptions.js) — see that module for why this
|
|
17
|
+
* provider disagreeing with the ratchet was a live delivery blocker. Exempted
|
|
18
|
+
* files are named on the log rather than silently dropped.
|
|
19
|
+
*
|
|
20
|
+
* The lint dimension is deliberately NOT filtered through the same list:
|
|
21
|
+
* lint carries its own exclusion surface (biome's `files.includes`,
|
|
22
|
+
* `.markdownlintignore`), and a quality-gate ignore glob makes no claim about
|
|
23
|
+
* whether a file should parse or format cleanly.
|
|
24
|
+
*
|
|
25
|
+
* The adapter does NOT post to GitHub and does NOT render a markdown body.
|
|
26
|
+
* Those concerns belong to `runCodeReview()`, which calls the renderer + the
|
|
27
|
+
* structured-comment upserter.
|
|
18
28
|
*
|
|
19
29
|
* Construction is intentionally zero-arg so the factory can instantiate
|
|
20
30
|
* it without threading config through every call. Per-invocation config
|
|
@@ -53,6 +63,10 @@ import {
|
|
|
53
63
|
} from '../../observability/runtime-friction.js';
|
|
54
64
|
import { PROJECT_ROOT } from '../../project-root.js';
|
|
55
65
|
import { transpileIfNeeded } from '../../transpile.js';
|
|
66
|
+
import {
|
|
67
|
+
resolveMaintainabilityIgnoreGlobs,
|
|
68
|
+
scopeMaintainabilityFiles,
|
|
69
|
+
} from './mi-exemptions.js';
|
|
56
70
|
import {
|
|
57
71
|
parseLintOutput,
|
|
58
72
|
partitionFilesForLint,
|
|
@@ -389,16 +403,6 @@ export function buildLintFindings(lintSummary) {
|
|
|
389
403
|
return findings;
|
|
390
404
|
}
|
|
391
405
|
|
|
392
|
-
function _emptyResults() {
|
|
393
|
-
return {
|
|
394
|
-
totalFiles: 0,
|
|
395
|
-
jsFiles: 0,
|
|
396
|
-
maintainability: [],
|
|
397
|
-
criticalFindings: [],
|
|
398
|
-
mediumFindings: [],
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
|
|
402
406
|
async function runLintPhase({
|
|
403
407
|
scopeLint,
|
|
404
408
|
changedFiles,
|
|
@@ -467,6 +471,7 @@ function buildLintDegradations(lintSummary) {
|
|
|
467
471
|
* analyzeChangedFilesFn?: typeof analyzeChangedFiles,
|
|
468
472
|
* buildLintFindingsFn?: typeof buildLintFindings,
|
|
469
473
|
* emitToolDegradationFn?: typeof emitRuntimeFriction,
|
|
474
|
+
* resolveIgnoreGlobsFn?: typeof resolveMaintainabilityIgnoreGlobs,
|
|
470
475
|
* logger?: { info?: Function, warn?: Function, error?: Function },
|
|
471
476
|
* scopeLint?: 'changed-only'|'off',
|
|
472
477
|
* }} [deps]
|
|
@@ -479,6 +484,10 @@ export function createNativeProvider(deps = {}) {
|
|
|
479
484
|
analyzeChangedFilesFn = analyzeChangedFiles,
|
|
480
485
|
buildLintFindingsFn = buildLintFindings,
|
|
481
486
|
emitToolDegradationFn = emitRuntimeFriction,
|
|
487
|
+
// The maintainability-gate exemption seam. The resolution itself — gate-key
|
|
488
|
+
// read and fail-open — is unit-tested in `mi-exemptions.js`; this dep is
|
|
489
|
+
// here so a provider test can pin the WIRING without a config on disk.
|
|
490
|
+
resolveIgnoreGlobsFn = resolveMaintainabilityIgnoreGlobs,
|
|
482
491
|
logger,
|
|
483
492
|
scopeLint = 'changed-only',
|
|
484
493
|
} = deps;
|
|
@@ -551,7 +560,12 @@ export function createNativeProvider(deps = {}) {
|
|
|
551
560
|
logger?.info?.(
|
|
552
561
|
`[native-review] Analyzing ${changedFiles.length} changed file(s)...`,
|
|
553
562
|
);
|
|
554
|
-
const
|
|
563
|
+
const mi = scopeMaintainabilityFiles(changedFiles, {
|
|
564
|
+
cwd: PROJECT_ROOT,
|
|
565
|
+
resolveIgnoreGlobsFn,
|
|
566
|
+
});
|
|
567
|
+
if (mi.notice) logger?.info?.(mi.notice);
|
|
568
|
+
const results = await analyzeChangedFilesFn(mi.scored, {
|
|
555
569
|
headRef,
|
|
556
570
|
gitSpawnFn,
|
|
557
571
|
});
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* the caller raises that to a thrown error so auto-merge is not enabled.
|
|
22
22
|
*
|
|
23
23
|
* Delegates the `runCodeReview` invocation to `runStoryReviewCore`
|
|
24
|
-
* (exported from `story-close/phases/
|
|
24
|
+
* (exported from `story-close/phases/review-core.js`) so the close path
|
|
25
25
|
* shares a single invocation pattern (Story #3653). Review depth needs no
|
|
26
26
|
* input here: it is derived from this Story's own diff inside `runCodeReview`
|
|
27
27
|
* (Story #4542).
|