mandrel 2.31.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/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/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 +59 -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 +1 -11
- 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 +42 -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,335 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* audit-results-graduator.js — Auto-graduate non-blocking audit findings
|
|
3
|
-
* from the Epic's unified `verification-results` structured comment into
|
|
4
|
-
* routed GitHub follow-up issues. Story #2615 / Epic #2586.
|
|
5
|
-
*
|
|
6
|
-
* Story #4411 (Epic #4405) unified the former `audit-results` and
|
|
7
|
-
* `code-review` structured-comment contracts into one `verification-results`
|
|
8
|
-
* contract: this graduator now reads the shared
|
|
9
|
-
* {@link VERIFICATION_RESULTS_MARKER} comment rather than the retired
|
|
10
|
-
* audit-results structured-comment marker. The lens-aware finding
|
|
11
|
-
* parser, the `audit-results::<severity>` + `domain::<lens>` label shape,
|
|
12
|
-
* and the audit idempotency marker are unchanged.
|
|
13
|
-
*
|
|
14
|
-
* As of Story #3845 / Epic #3823 the spawn helper, the path/idempotency
|
|
15
|
-
* probes, the `gh issue create` filer, the toggle reader, and the
|
|
16
|
-
* route → probe → file walk all live in the shared
|
|
17
|
-
* [`graduator-core.js`](./graduator-core.js). This module is the thin
|
|
18
|
-
* audit-specific shell: it owns the audit finding parser (lens detection
|
|
19
|
-
* plus the 🔵→low / 🟢→suggestion severity mapping), the audit label /
|
|
20
|
-
* title / body shape, and the audit idempotency marker. Behaviour is
|
|
21
|
-
* identical to the pre-consolidation graduator.
|
|
22
|
-
*
|
|
23
|
-
* - Read the unified `verification-results` structured comment off the
|
|
24
|
-
* Epic ticket via the injected provider (`getTicketComments`).
|
|
25
|
-
* - For each non-blocking finding (severity high/medium/low/suggestion
|
|
26
|
-
* — i.e. anything that is NOT a 🔴 Critical Blocker), check that
|
|
27
|
-
* the cited file still exists in the merged tree.
|
|
28
|
-
* - Route by source classification (framework vs consumer) using
|
|
29
|
-
* `classifyPathSource`. Cross-repo findings are recorded under
|
|
30
|
-
* `skipped: 'cross-repo-deferred'` and the would-be `gh issue
|
|
31
|
-
* create` invocation is logged — never shelled out across repos.
|
|
32
|
-
* - File a follow-up issue with `gh issue create` carrying:
|
|
33
|
-
* - `meta::audit-finding`
|
|
34
|
-
* - `meta::framework-gap` or `meta::consumer-improvement`
|
|
35
|
-
* - `audit-results::<severity>` (high|medium|low|suggestion)
|
|
36
|
-
* - `domain::<lens-name>` (e.g. `domain::audit-security`)
|
|
37
|
-
* - Embed an idempotency marker in each body:
|
|
38
|
-
* <!-- audit-results-followup: epic-<id>-finding-<idx> -->
|
|
39
|
-
* Before filing, probe via `gh search issues "<marker>"` and skip
|
|
40
|
-
* findings whose marker is already present.
|
|
41
|
-
* - Short-circuit when
|
|
42
|
-
* `config.delivery.feedbackLoop.auditResultsAutoFile` is `false` —
|
|
43
|
-
* return `{ filed: [], skipped: [{ reason: 'toggle-disabled' }],
|
|
44
|
-
* errors: [] }`.
|
|
45
|
-
* - NEVER throw. Every failure path is captured in `errors[]`.
|
|
46
|
-
*/
|
|
47
|
-
|
|
48
|
-
import {
|
|
49
|
-
fingerprintFinding,
|
|
50
|
-
fingerprintFooter,
|
|
51
|
-
semanticKeyFooter,
|
|
52
|
-
semanticKeyFor,
|
|
53
|
-
} from '../findings/route-finding.js';
|
|
54
|
-
import {
|
|
55
|
-
contentFingerprint,
|
|
56
|
-
graduate,
|
|
57
|
-
makeIsAutoFileEnabled,
|
|
58
|
-
NO_VERIFICATION_RESULTS_COMMENT_REASON,
|
|
59
|
-
VERIFICATION_RESULTS_MARKER,
|
|
60
|
-
} from './graduator-core.js';
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Resolve the toggle from the resolved agentrc config. Defaults to `true`
|
|
64
|
-
* — the feature is opt-out, not opt-in.
|
|
65
|
-
*
|
|
66
|
-
* @param {object|undefined|null} config
|
|
67
|
-
* @returns {boolean}
|
|
68
|
-
*/
|
|
69
|
-
export const isAutoFileEnabled = makeIsAutoFileEnabled('auditResultsAutoFile');
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Severity → label mapping. Only non-blocking severities have a route;
|
|
73
|
-
* 🔴 Critical Blocker is explicitly filtered out upstream.
|
|
74
|
-
*/
|
|
75
|
-
const SEVERITY_LABEL = Object.freeze({
|
|
76
|
-
high: 'audit-results::high',
|
|
77
|
-
medium: 'audit-results::medium',
|
|
78
|
-
low: 'audit-results::low',
|
|
79
|
-
suggestion: 'audit-results::suggestion',
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Map a classification source to its meta routing label.
|
|
84
|
-
*
|
|
85
|
-
* @param {string} source
|
|
86
|
-
* @returns {string}
|
|
87
|
-
*/
|
|
88
|
-
function metaSourceLabel(source) {
|
|
89
|
-
return source === 'framework'
|
|
90
|
-
? 'meta::framework-gap'
|
|
91
|
-
: 'meta::consumer-improvement';
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Build the **legacy** idempotency marker for a given epicId / finding
|
|
96
|
-
* index. Superseded by the content-hash marker
|
|
97
|
-
* ({@link buildContentMarker}) at the Story #4415 cutover, but still
|
|
98
|
-
* probed for so follow-ups filed before the cutover are recognized and
|
|
99
|
-
* not re-filed. An HTML comment so it survives markdown rendering without
|
|
100
|
-
* leaking into the visible body; the idempotency probe strips the comment
|
|
101
|
-
* delimiters before querying `gh search` (the raw `<!-- … -->` form never
|
|
102
|
-
* matches the index — Story #4657).
|
|
103
|
-
*
|
|
104
|
-
* @param {number} epicId
|
|
105
|
-
* @param {number} index — zero-based finding ordinal within the Epic.
|
|
106
|
-
* @returns {string}
|
|
107
|
-
*/
|
|
108
|
-
export function buildIdempotencyMarker(epicId, index) {
|
|
109
|
-
return `<!-- audit-results-followup: epic-${epicId}-finding-${index} -->`;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Build the content-hash idempotency marker embedded in freshly filed
|
|
114
|
-
* follow-up bodies. Derived from the finding's `lens|path|summary` triple
|
|
115
|
-
* so the marker is stable across sibling insert/remove/reorder churn in
|
|
116
|
-
* the source `audit-results` comment (Story #4415). An HTML comment so it
|
|
117
|
-
* survives markdown rendering without leaking into the visible body; the
|
|
118
|
-
* idempotency probe strips the comment delimiters before querying
|
|
119
|
-
* `gh search` (the raw `<!-- … -->` form never matches the index —
|
|
120
|
-
* Story #4657).
|
|
121
|
-
*
|
|
122
|
-
* @param {number} epicId
|
|
123
|
-
* @param {{ lens?: string, path?: string, summary?: string }} finding
|
|
124
|
-
* @returns {string}
|
|
125
|
-
*/
|
|
126
|
-
export function buildContentMarker(epicId, finding) {
|
|
127
|
-
const fp = contentFingerprint({
|
|
128
|
-
category: finding.lens,
|
|
129
|
-
path: finding.path,
|
|
130
|
-
title: finding.summary,
|
|
131
|
-
});
|
|
132
|
-
return `<!-- audit-results-followup: epic-${epicId}-${fp} -->`;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Project a graduator audit finding onto the canonical identity the shared
|
|
137
|
-
* dedup helper (`lib/findings/route-finding.js`) fingerprints over, so a
|
|
138
|
-
* close-time graduator filing and a sweep-time `/audit-to-stories` filing
|
|
139
|
-
* share ONE identity namespace (Story #4626). The graduator's `lens`
|
|
140
|
-
* (`audit-<dimension>`) maps to the identity `area`; its cited `path` is the
|
|
141
|
-
* primary file; its `summary` is the title. Historically the graduator only
|
|
142
|
-
* stamped its own content-hash `audit-results-followup` marker, disjoint from
|
|
143
|
-
* route-finding's `audit-fingerprints` footer — so a `/audit-to-stories`
|
|
144
|
-
* sweep could not recognize a graduator-filed issue and would re-file it.
|
|
145
|
-
*
|
|
146
|
-
* @param {{ lens?: string, path?: string, summary?: string }} finding
|
|
147
|
-
* @returns {{ title: string, area: string, primaryFile: string, severity: string, labels: string[] }}
|
|
148
|
-
*/
|
|
149
|
-
export function toCanonicalFinding(finding) {
|
|
150
|
-
const lens = typeof finding?.lens === 'string' ? finding.lens : '';
|
|
151
|
-
return {
|
|
152
|
-
title: typeof finding?.summary === 'string' ? finding.summary : '',
|
|
153
|
-
area: lens,
|
|
154
|
-
primaryFile: typeof finding?.path === 'string' ? finding.path : '',
|
|
155
|
-
severity: '',
|
|
156
|
-
labels: lens ? [lens] : [],
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Render the canonical `audit-fingerprints` (and location-based
|
|
162
|
-
* `audit-semantic-keys`) footer for a graduator finding, computed off the
|
|
163
|
-
* shared helper's canonical hash. Stamped into every filed follow-up body so
|
|
164
|
-
* the `/audit-to-stories` dedup probe recognizes a graduator-filed issue
|
|
165
|
-
* (Story #4626).
|
|
166
|
-
*
|
|
167
|
-
* @param {{ lens?: string, path?: string, summary?: string }} finding
|
|
168
|
-
* @returns {string}
|
|
169
|
-
*/
|
|
170
|
-
export function canonicalFingerprintFooter(finding) {
|
|
171
|
-
const canonical = toCanonicalFinding(finding);
|
|
172
|
-
const { full } = fingerprintFinding(canonical);
|
|
173
|
-
const key = semanticKeyFor(canonical);
|
|
174
|
-
const lines = [fingerprintFooter(full)];
|
|
175
|
-
if (key) lines.push(semanticKeyFooter(key));
|
|
176
|
-
return lines.join('\n');
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Parse the rendered audit-results markdown into a list of findings.
|
|
181
|
-
* The format produced by `epic-audit.md` Step 4 groups findings under
|
|
182
|
-
* `#### <lens-name>` headings; each finding line begins with a severity
|
|
183
|
-
* emoji and embeds the cited path inside backticks. 🔴 critical findings
|
|
184
|
-
* are filtered out (they're blocking — the Epic stops on those).
|
|
185
|
-
*
|
|
186
|
-
* Findings the Phase 4 remediation loop already fixed on-branch are
|
|
187
|
-
* rendered under a **"Fixed on-branch"** heading (Story #4399) with a ✅
|
|
188
|
-
* prefix so they no longer parse as open findings. As a belt-and-suspenders
|
|
189
|
-
* guard the parser also skips every line inside a Fixed-on-branch section
|
|
190
|
-
* outright, so a remediated 🟡 Medium never spawns a ghost follow-up issue
|
|
191
|
-
* even if its line retains its original severity emoji.
|
|
192
|
-
*
|
|
193
|
-
* Pure. Exported so the parser can be unit-tested in isolation.
|
|
194
|
-
*
|
|
195
|
-
* @param {string} body
|
|
196
|
-
* @returns {Array<{ severity: 'high'|'medium'|'low'|'suggestion', lens: string, path: string, summary: string, index: number }>}
|
|
197
|
-
*/
|
|
198
|
-
export function parseFindings(body) {
|
|
199
|
-
if (typeof body !== 'string' || body.length === 0) return [];
|
|
200
|
-
const findings = [];
|
|
201
|
-
const lines = body.split(/\r?\n/);
|
|
202
|
-
let idx = 0;
|
|
203
|
-
let lens = 'unknown';
|
|
204
|
-
let inFixedSection = false;
|
|
205
|
-
for (const rawLine of lines) {
|
|
206
|
-
const trimmed = rawLine.trim();
|
|
207
|
-
if (trimmed.length === 0) continue;
|
|
208
|
-
|
|
209
|
-
// Any markdown heading resets the Fixed-on-branch guard and, when it
|
|
210
|
-
// names a known audit family (`audit-*`), sets the active lens. A
|
|
211
|
-
// "Fixed on-branch" heading opens a section whose entries never
|
|
212
|
-
// graduate (Story #4399).
|
|
213
|
-
const headingMatch = trimmed.match(/^#{2,6}\s+(.+)$/);
|
|
214
|
-
if (headingMatch) {
|
|
215
|
-
inFixedSection = /fixed on-branch/i.test(headingMatch[1]);
|
|
216
|
-
const lensMatch = headingMatch[1].match(/^(audit-[a-z0-9-]+)/i);
|
|
217
|
-
if (lensMatch) lens = lensMatch[1];
|
|
218
|
-
continue;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (inFixedSection) continue;
|
|
222
|
-
|
|
223
|
-
let severity = null;
|
|
224
|
-
if (trimmed.startsWith('🔴')) {
|
|
225
|
-
// Critical Blocker — skip; never graduates.
|
|
226
|
-
continue;
|
|
227
|
-
}
|
|
228
|
-
if (trimmed.startsWith('🟠')) severity = 'high';
|
|
229
|
-
else if (trimmed.startsWith('🟡')) severity = 'medium';
|
|
230
|
-
else if (trimmed.startsWith('🟢')) severity = 'suggestion';
|
|
231
|
-
else if (trimmed.startsWith('🔵')) severity = 'low';
|
|
232
|
-
else continue;
|
|
233
|
-
|
|
234
|
-
const pathMatch = trimmed.match(/`([^`]+)`/);
|
|
235
|
-
if (!pathMatch) continue;
|
|
236
|
-
const path = pathMatch[1];
|
|
237
|
-
|
|
238
|
-
findings.push({
|
|
239
|
-
severity,
|
|
240
|
-
lens,
|
|
241
|
-
path,
|
|
242
|
-
summary: trimmed,
|
|
243
|
-
index: idx,
|
|
244
|
-
});
|
|
245
|
-
idx += 1;
|
|
246
|
-
}
|
|
247
|
-
return findings;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Auto-graduate non-blocking audit-results findings into routed
|
|
252
|
-
* follow-up issues. Never throws. Thin wrapper around the shared
|
|
253
|
-
* `graduate()` walk with the audit-specific behaviour bundle.
|
|
254
|
-
*
|
|
255
|
-
* @param {object} opts
|
|
256
|
-
* @param {number} opts.epicId
|
|
257
|
-
* @param {object} opts.provider — ticketing provider exposing
|
|
258
|
-
* `getTicketComments(ticketId)`.
|
|
259
|
-
* @param {object} [opts.config] — resolved agentrc.
|
|
260
|
-
* @param {{owner: string, repo: string}} opts.currentRepo — repo the
|
|
261
|
-
* listener is running inside; used for the cross-repo guard.
|
|
262
|
-
* @param {{owner: string, repo: string}} [opts.frameworkRepo] — where
|
|
263
|
-
* framework-tagged findings route. Defaults to `currentRepo`.
|
|
264
|
-
* @param {string} [opts.gitRef='HEAD']
|
|
265
|
-
* @param {Function} [opts.classifier=classifyPathSource]
|
|
266
|
-
* @param {string} [opts.ghPath='gh']
|
|
267
|
-
* @param {Function} [opts.spawnImpl]
|
|
268
|
-
* @param {string} [opts.cwd]
|
|
269
|
-
* @param {{info?: Function, warn?: Function, debug?: Function}} [opts.logger]
|
|
270
|
-
* @returns {Promise<{
|
|
271
|
-
* filed: Array<{ index: number, severity: string, lens: string, path: string, source: string, repo: string, url: string|null }>,
|
|
272
|
-
* skipped: Array<{ index?: number, reason: string, path?: string, severity?: string, lens?: string }>,
|
|
273
|
-
* errors: string[],
|
|
274
|
-
* }>}
|
|
275
|
-
*/
|
|
276
|
-
export async function graduateAuditResults(opts = {}) {
|
|
277
|
-
return graduate({
|
|
278
|
-
...opts,
|
|
279
|
-
spec: {
|
|
280
|
-
fnName: 'graduateAuditResults',
|
|
281
|
-
isAutoFileEnabled,
|
|
282
|
-
commentMarker: VERIFICATION_RESULTS_MARKER,
|
|
283
|
-
noCommentReason: NO_VERIFICATION_RESULTS_COMMENT_REASON,
|
|
284
|
-
parseFindings,
|
|
285
|
-
buildContentMarker,
|
|
286
|
-
buildLegacyMarker: buildIdempotencyMarker,
|
|
287
|
-
crossRepoCommentAttrs: { graduator: 'audit-results' },
|
|
288
|
-
decorateRecord: (record, finding) => {
|
|
289
|
-
record.lens = finding.lens;
|
|
290
|
-
return record;
|
|
291
|
-
},
|
|
292
|
-
buildCrossRepoLog: ({ finding, routedRepo, source }) => {
|
|
293
|
-
const labels = [
|
|
294
|
-
'meta::audit-finding',
|
|
295
|
-
metaSourceLabel(source),
|
|
296
|
-
SEVERITY_LABEL[finding.severity],
|
|
297
|
-
`domain::${finding.lens}`,
|
|
298
|
-
];
|
|
299
|
-
return `[audit-results-graduator] cross-repo skip (would file in ${routedRepo.owner}/${routedRepo.repo}): gh issue create --repo ${routedRepo.owner}/${routedRepo.repo} --title "Audit follow-up (${finding.lens}): ${finding.path}" --label "${labels.join(',')}"`;
|
|
300
|
-
},
|
|
301
|
-
buildFollowUp: ({ finding, source, epicId, idMarker }) => {
|
|
302
|
-
const title = `Audit follow-up (${finding.lens}): ${finding.path}`;
|
|
303
|
-
const body = [
|
|
304
|
-
idMarker,
|
|
305
|
-
'',
|
|
306
|
-
`Auto-filed from the Epic #${epicId} audit-results pass.`,
|
|
307
|
-
'',
|
|
308
|
-
`**Lens**: ${finding.lens}`,
|
|
309
|
-
`**Severity**: ${finding.severity}`,
|
|
310
|
-
`**Source**: ${source}`,
|
|
311
|
-
`**Path**: \`${finding.path}\``,
|
|
312
|
-
'',
|
|
313
|
-
'### Finding',
|
|
314
|
-
'',
|
|
315
|
-
finding.summary,
|
|
316
|
-
'',
|
|
317
|
-
`_See Epic #${epicId} for the full audit-results report._`,
|
|
318
|
-
'',
|
|
319
|
-
// Canonical dedup identity so the `/audit-to-stories` sweep-time
|
|
320
|
-
// probe recognizes this close-time filing and never re-files it
|
|
321
|
-
// (Story #4626). The content-hash `audit-results-followup` marker
|
|
322
|
-
// above stays the graduator's own re-file guard.
|
|
323
|
-
canonicalFingerprintFooter(finding),
|
|
324
|
-
].join('\n');
|
|
325
|
-
const labels = [
|
|
326
|
-
'meta::audit-finding',
|
|
327
|
-
metaSourceLabel(source),
|
|
328
|
-
SEVERITY_LABEL[finding.severity],
|
|
329
|
-
`domain::${finding.lens}`,
|
|
330
|
-
];
|
|
331
|
-
return { title, body, labels };
|
|
332
|
-
},
|
|
333
|
-
},
|
|
334
|
-
});
|
|
335
|
-
}
|
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* lib/mutation/baseline-snapshot.js — Read/write the mutation baseline file
|
|
3
|
-
* consumed by the story-close mutation gate (Story #1736, Epic #1720).
|
|
4
|
-
*
|
|
5
|
-
* The baseline file lives at the path configured by
|
|
6
|
-
* `delivery.quality.gates.mutation.baselinePath` (default
|
|
7
|
-
* `baselines/mutation.json`) and conforms to:
|
|
8
|
-
*
|
|
9
|
-
* {
|
|
10
|
-
* "generatedAt": "<ISO-8601 timestamp>",
|
|
11
|
-
* "tolerancePct": <number, mutation-score percent points>,
|
|
12
|
-
* "workspaces": {
|
|
13
|
-
* "*": <number, mutation-score percent points 0–100>,
|
|
14
|
-
* "<workspace-name>": <number>
|
|
15
|
-
* }
|
|
16
|
-
* }
|
|
17
|
-
*
|
|
18
|
-
* The `"*"` catch-all key handles single-workspace consumers; real
|
|
19
|
-
* workspace names handle monorepo consumers. Generic applier code reads
|
|
20
|
-
* `workspaces[name] ?? workspaces['*']`.
|
|
21
|
-
*
|
|
22
|
-
* `tolerancePct` carries the per-workspace mutation-score drop tolerance
|
|
23
|
-
* in percent points (matches the `delivery.quality.gates.mutation.tolerance`
|
|
24
|
-
* resolver default of 0). When a workspace's measured score is below
|
|
25
|
-
* `baseline - tolerancePct`, the gate fails.
|
|
26
|
-
*
|
|
27
|
-
* This module mirrors the dependency-injection style used by
|
|
28
|
-
* `lib/baseline-snapshot.js`: the fs surface and the clock are injected so
|
|
29
|
-
* unit tests never touch real files. The two writer paths
|
|
30
|
-
* (`readBaseline`, `writeBaseline`) are split because the typical caller
|
|
31
|
-
* sequence is read-then-merge-then-write — a refresh CLI reads the prior
|
|
32
|
-
* baseline to preserve the configured `tolerancePct` if the Stryker run
|
|
33
|
-
* does not override it (the former `update-mutation-baseline.js` wrapper
|
|
34
|
-
* was retired in #4482; the gate ships dormant).
|
|
35
|
-
*/
|
|
36
|
-
|
|
37
|
-
import fs from 'node:fs';
|
|
38
|
-
import path from 'node:path';
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Default tolerance in percent points. Matches the framework-default
|
|
42
|
-
* tolerance value resolved from `delivery.quality.gates.mutation.tolerance`
|
|
43
|
-
* (`{ kind: 'absolute', value: 0 }`). A drop of 0% pts means any
|
|
44
|
-
* regression below the recorded baseline trips the gate.
|
|
45
|
-
*/
|
|
46
|
-
export const DEFAULT_TOLERANCE_PCT = 0;
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Default baseline file path, relative to the repo root. Mirrors the
|
|
50
|
-
* resolver default in `lib/config/quality.js`.
|
|
51
|
-
*/
|
|
52
|
-
export const DEFAULT_BASELINE_PATH = 'baselines/mutation.json';
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Read the mutation baseline at `baselinePath`. Returns `null` when the
|
|
56
|
-
* file does not exist so the caller can distinguish "no baseline yet"
|
|
57
|
-
* (the gate self-skips with an explanatory line) from a parse error.
|
|
58
|
-
*
|
|
59
|
-
* Throws when the file exists but is malformed (invalid JSON, missing
|
|
60
|
-
* required keys, wrong types) — silent corruption must not relax the gate.
|
|
61
|
-
*
|
|
62
|
-
* @param {string} baselinePath Absolute or cwd-relative path.
|
|
63
|
-
* @param {{ cwd?: string, fsImpl?: { existsSync: typeof fs.existsSync, readFileSync: typeof fs.readFileSync } }} [opts]
|
|
64
|
-
* @returns {{ generatedAt: string, tolerancePct: number, workspaces: Record<string, number> } | null}
|
|
65
|
-
*/
|
|
66
|
-
export function readBaseline(baselinePath, opts = {}) {
|
|
67
|
-
const cwd = opts.cwd ?? process.cwd();
|
|
68
|
-
const fsImpl = opts.fsImpl ?? fs;
|
|
69
|
-
const abs = path.isAbsolute(baselinePath)
|
|
70
|
-
? baselinePath
|
|
71
|
-
: path.resolve(cwd, baselinePath);
|
|
72
|
-
if (!fsImpl.existsSync(abs)) return null;
|
|
73
|
-
const raw = fsImpl.readFileSync(abs, 'utf8');
|
|
74
|
-
let parsed;
|
|
75
|
-
try {
|
|
76
|
-
parsed = JSON.parse(raw);
|
|
77
|
-
} catch (err) {
|
|
78
|
-
throw new Error(
|
|
79
|
-
`[mutation/baseline-snapshot] failed to parse ${baselinePath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
return validateBaseline(parsed, baselinePath);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Atomically rewrite the mutation baseline at `baselinePath` from
|
|
87
|
-
* `payload`. "Atomic" here means write-temp-then-rename, so a SIGKILL
|
|
88
|
-
* mid-write never leaves a half-written baseline file on disk.
|
|
89
|
-
*
|
|
90
|
-
* The on-disk shape is canonicalised: `workspaces` keys are sorted
|
|
91
|
-
* alphabetically (with `"*"` always first) and the file ends with a
|
|
92
|
-
* trailing newline. This produces a byte-stable serialisation, which
|
|
93
|
-
* lets a refresh caller detect "no change" runs (skip the
|
|
94
|
-
* baseline-refresh commit when re-running Stryker produces an identical
|
|
95
|
-
* baseline).
|
|
96
|
-
*
|
|
97
|
-
* @param {string} baselinePath
|
|
98
|
-
* @param {{ generatedAt?: string, tolerancePct?: number, workspaces: Record<string, number> }} payload
|
|
99
|
-
* @param {{ cwd?: string, fsImpl?: object, clock?: () => Date }} [opts]
|
|
100
|
-
* @returns {{ path: string, bytes: string, didChange: boolean }}
|
|
101
|
-
*/
|
|
102
|
-
export function writeBaseline(baselinePath, payload, opts = {}) {
|
|
103
|
-
const cwd = opts.cwd ?? process.cwd();
|
|
104
|
-
const fsImpl = opts.fsImpl ?? fs;
|
|
105
|
-
const clock = opts.clock ?? (() => new Date());
|
|
106
|
-
const abs = path.isAbsolute(baselinePath)
|
|
107
|
-
? baselinePath
|
|
108
|
-
: path.resolve(cwd, baselinePath);
|
|
109
|
-
const generatedAt = payload.generatedAt ?? clock().toISOString();
|
|
110
|
-
const tolerancePct = Number.isFinite(payload.tolerancePct)
|
|
111
|
-
? payload.tolerancePct
|
|
112
|
-
: DEFAULT_TOLERANCE_PCT;
|
|
113
|
-
if (
|
|
114
|
-
!payload ||
|
|
115
|
-
typeof payload.workspaces !== 'object' ||
|
|
116
|
-
payload.workspaces === null
|
|
117
|
-
) {
|
|
118
|
-
throw new TypeError(
|
|
119
|
-
'[mutation/baseline-snapshot] writeBaseline: payload.workspaces must be an object',
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
const workspaces = canonicaliseWorkspaces(payload.workspaces);
|
|
123
|
-
const envelope = { generatedAt, tolerancePct, workspaces };
|
|
124
|
-
const bytes = `${JSON.stringify(envelope, null, 2)}\n`;
|
|
125
|
-
let prior = null;
|
|
126
|
-
if (fsImpl.existsSync(abs)) {
|
|
127
|
-
try {
|
|
128
|
-
prior = fsImpl.readFileSync(abs, 'utf8');
|
|
129
|
-
} catch {
|
|
130
|
-
prior = null;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
if (prior === bytes) {
|
|
134
|
-
return { path: abs, bytes, didChange: false };
|
|
135
|
-
}
|
|
136
|
-
fsImpl.mkdirSync?.(path.dirname(abs), { recursive: true });
|
|
137
|
-
// Atomic write: write to a sibling temp file then rename. `renameSync`
|
|
138
|
-
// is atomic on POSIX and best-effort on Windows (which is good enough —
|
|
139
|
-
// the previous file is never half-overwritten).
|
|
140
|
-
const tmp = `${abs}.tmp-${process.pid}`;
|
|
141
|
-
fsImpl.writeFileSync(tmp, bytes);
|
|
142
|
-
try {
|
|
143
|
-
fsImpl.renameSync?.(tmp, abs);
|
|
144
|
-
} catch {
|
|
145
|
-
// `fs` shims in tests may not implement renameSync — fall back to a
|
|
146
|
-
// direct write + best-effort unlink so the in-memory store still
|
|
147
|
-
// converges to the right state.
|
|
148
|
-
fsImpl.writeFileSync(abs, bytes);
|
|
149
|
-
try {
|
|
150
|
-
fsImpl.unlinkSync?.(tmp);
|
|
151
|
-
} catch {
|
|
152
|
-
/* ignore */
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
return { path: abs, bytes, didChange: true };
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Validate that `parsed` matches the baseline envelope contract. Returns
|
|
160
|
-
* the parsed object unchanged when it conforms; throws otherwise.
|
|
161
|
-
*
|
|
162
|
-
* Exposed for unit testing the validator independently of fs I/O.
|
|
163
|
-
*
|
|
164
|
-
* @param {unknown} parsed
|
|
165
|
-
* @param {string} sourceLabel
|
|
166
|
-
* @returns {{ generatedAt: string, tolerancePct: number, workspaces: Record<string, number> }}
|
|
167
|
-
*/
|
|
168
|
-
export function validateBaseline(parsed, sourceLabel = '<input>') {
|
|
169
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
170
|
-
throw new Error(
|
|
171
|
-
`[mutation/baseline-snapshot] ${sourceLabel}: expected an object, got ${Array.isArray(parsed) ? 'array' : typeof parsed}`,
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
const { generatedAt, tolerancePct, workspaces } =
|
|
175
|
-
/** @type {Record<string, unknown>} */ (parsed);
|
|
176
|
-
if (typeof generatedAt !== 'string' || generatedAt.length === 0) {
|
|
177
|
-
throw new Error(
|
|
178
|
-
`[mutation/baseline-snapshot] ${sourceLabel}: 'generatedAt' must be a non-empty string`,
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
if (
|
|
182
|
-
!Number.isFinite(tolerancePct) ||
|
|
183
|
-
/** @type {number} */ (tolerancePct) < 0
|
|
184
|
-
) {
|
|
185
|
-
throw new Error(
|
|
186
|
-
`[mutation/baseline-snapshot] ${sourceLabel}: 'tolerancePct' must be a non-negative number`,
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
if (
|
|
190
|
-
!workspaces ||
|
|
191
|
-
typeof workspaces !== 'object' ||
|
|
192
|
-
Array.isArray(workspaces)
|
|
193
|
-
) {
|
|
194
|
-
throw new Error(
|
|
195
|
-
`[mutation/baseline-snapshot] ${sourceLabel}: 'workspaces' must be an object`,
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
for (const [name, score] of Object.entries(workspaces)) {
|
|
199
|
-
if (typeof name !== 'string' || name.length === 0) {
|
|
200
|
-
throw new Error(
|
|
201
|
-
`[mutation/baseline-snapshot] ${sourceLabel}: workspace name must be a non-empty string`,
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
if (
|
|
205
|
-
!Number.isFinite(score) ||
|
|
206
|
-
/** @type {number} */ (score) < 0 ||
|
|
207
|
-
/** @type {number} */ (score) > 100
|
|
208
|
-
) {
|
|
209
|
-
throw new Error(
|
|
210
|
-
`[mutation/baseline-snapshot] ${sourceLabel}: workspace "${name}" score must be a number in [0, 100], got ${JSON.stringify(score)}`,
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
return /** @type {{ generatedAt: string, tolerancePct: number, workspaces: Record<string, number> }} */ (
|
|
215
|
-
parsed
|
|
216
|
-
);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Sort `workspaces` keys deterministically so the on-disk serialisation
|
|
221
|
-
* is byte-stable. The `"*"` catch-all key is pinned to the first slot;
|
|
222
|
-
* the remaining keys sort alphabetically.
|
|
223
|
-
*
|
|
224
|
-
* @param {Record<string, number>} workspaces
|
|
225
|
-
* @returns {Record<string, number>}
|
|
226
|
-
*/
|
|
227
|
-
function canonicaliseWorkspaces(workspaces) {
|
|
228
|
-
const keys = Object.keys(workspaces);
|
|
229
|
-
const sorted = keys.slice().sort((a, b) => {
|
|
230
|
-
if (a === '*') return -1;
|
|
231
|
-
if (b === '*') return 1;
|
|
232
|
-
return a.localeCompare(b);
|
|
233
|
-
});
|
|
234
|
-
const out = /** @type {Record<string, number>} */ ({});
|
|
235
|
-
for (const k of sorted) {
|
|
236
|
-
out[k] = workspaces[k];
|
|
237
|
-
}
|
|
238
|
-
return out;
|
|
239
|
-
}
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* lib/mutation/config-detector.js — Detect a Stryker config in a repo
|
|
3
|
-
* (Story #1736, Task #1754).
|
|
4
|
-
*
|
|
5
|
-
* The mutation gate is on-by-default but self-skips when Stryker is not
|
|
6
|
-
* configured. The detector recognises the canonical Stryker config
|
|
7
|
-
* surfaces:
|
|
8
|
-
*
|
|
9
|
-
* - `stryker.conf.js`
|
|
10
|
-
* - `stryker.conf.cjs`
|
|
11
|
-
* - `stryker.conf.mjs`
|
|
12
|
-
* - `stryker.conf.json`
|
|
13
|
-
* - `stryker.config.js` / `.cjs` / `.mjs` / `.json` (Stryker's alternate name)
|
|
14
|
-
* - a `"stryker"` key in the repo-root `package.json`
|
|
15
|
-
*
|
|
16
|
-
* When an explicit `configPath` is supplied (from the gate's
|
|
17
|
-
* `strykerConfigPath` block), that path takes precedence — the file is
|
|
18
|
-
* checked for existence and returned without re-walking the canonical
|
|
19
|
-
* surfaces.
|
|
20
|
-
*
|
|
21
|
-
* Pure with respect to the injected fs surface; never invokes Stryker
|
|
22
|
-
* or any child process.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import fs from 'node:fs';
|
|
26
|
-
import path from 'node:path';
|
|
27
|
-
|
|
28
|
-
const CANONICAL_FILES = Object.freeze([
|
|
29
|
-
'stryker.conf.js',
|
|
30
|
-
'stryker.conf.cjs',
|
|
31
|
-
'stryker.conf.mjs',
|
|
32
|
-
'stryker.conf.json',
|
|
33
|
-
'stryker.config.js',
|
|
34
|
-
'stryker.config.cjs',
|
|
35
|
-
'stryker.config.mjs',
|
|
36
|
-
'stryker.config.json',
|
|
37
|
-
]);
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* @typedef {Object} DetectionResult
|
|
41
|
-
* @property {boolean} found
|
|
42
|
-
* @property {'config-file' | 'package-json' | 'explicit' | null} via
|
|
43
|
-
* @property {string | null} path Absolute path to the config artifact (or
|
|
44
|
-
* the package.json that carries the `stryker` key).
|
|
45
|
-
* @property {string} [reason] Populated when `found` is false.
|
|
46
|
-
*/
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Detect a Stryker configuration in `cwd`.
|
|
50
|
-
*
|
|
51
|
-
* @param {{
|
|
52
|
-
* cwd?: string,
|
|
53
|
-
* configPath?: string | null,
|
|
54
|
-
* fsImpl?: { existsSync: typeof fs.existsSync, readFileSync: typeof fs.readFileSync },
|
|
55
|
-
* }} [opts]
|
|
56
|
-
* @returns {DetectionResult}
|
|
57
|
-
*/
|
|
58
|
-
export function detectStrykerConfig(opts = {}) {
|
|
59
|
-
const cwd = opts.cwd ?? process.cwd();
|
|
60
|
-
const fsImpl = opts.fsImpl ?? fs;
|
|
61
|
-
const explicit = opts.configPath;
|
|
62
|
-
|
|
63
|
-
if (typeof explicit === 'string' && explicit.length > 0) {
|
|
64
|
-
const abs = path.isAbsolute(explicit)
|
|
65
|
-
? explicit
|
|
66
|
-
: path.resolve(cwd, explicit);
|
|
67
|
-
if (fsImpl.existsSync(abs)) {
|
|
68
|
-
return { found: true, via: 'explicit', path: abs };
|
|
69
|
-
}
|
|
70
|
-
return {
|
|
71
|
-
found: false,
|
|
72
|
-
via: null,
|
|
73
|
-
path: null,
|
|
74
|
-
reason: `explicit strykerConfigPath '${explicit}' does not exist`,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
for (const filename of CANONICAL_FILES) {
|
|
79
|
-
const abs = path.resolve(cwd, filename);
|
|
80
|
-
if (fsImpl.existsSync(abs)) {
|
|
81
|
-
return { found: true, via: 'config-file', path: abs };
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const pkgPath = path.resolve(cwd, 'package.json');
|
|
86
|
-
if (fsImpl.existsSync(pkgPath)) {
|
|
87
|
-
try {
|
|
88
|
-
const raw = fsImpl.readFileSync(pkgPath, 'utf8');
|
|
89
|
-
const parsed = JSON.parse(raw);
|
|
90
|
-
if (
|
|
91
|
-
parsed &&
|
|
92
|
-
typeof parsed === 'object' &&
|
|
93
|
-
parsed.stryker &&
|
|
94
|
-
typeof parsed.stryker === 'object'
|
|
95
|
-
) {
|
|
96
|
-
return { found: true, via: 'package-json', path: pkgPath };
|
|
97
|
-
}
|
|
98
|
-
} catch {
|
|
99
|
-
// Malformed package.json — fall through to "not found". The repo
|
|
100
|
-
// is already broken in other ways; the mutation gate skip is a
|
|
101
|
-
// benign symptom, not the cause.
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return {
|
|
106
|
-
found: false,
|
|
107
|
-
via: null,
|
|
108
|
-
path: null,
|
|
109
|
-
reason: 'no Stryker config found',
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Exposed for testing — the canonical filename list, frozen.
|
|
115
|
-
* @returns {readonly string[]}
|
|
116
|
-
*/
|
|
117
|
-
export function getCanonicalConfigFilenames() {
|
|
118
|
-
return CANONICAL_FILES;
|
|
119
|
-
}
|