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,169 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* lib/workers/combined-mi-crap-worker.js ā CPU-pool worker entry for the
|
|
3
|
-
* combined MI + CRAP single-pass scan (`scanAndScoreCombined`).
|
|
4
|
-
*
|
|
5
|
-
* One file in, BOTH the maintainability score and the per-method CRAP rows
|
|
6
|
-
* out ā derived from a SINGLE `escomplex.analyzeModule` parse via
|
|
7
|
-
* `analyzeOnce`. This collapses the two independent worker-pool passes the
|
|
8
|
-
* full-tree baseline regenerator used to run (the MI worker parsed the AST
|
|
9
|
-
* once for the module score, the CRAP worker parsed the same file's AST
|
|
10
|
-
* again for the method rows) into one parse per file.
|
|
11
|
-
*
|
|
12
|
-
* The MI score and the CRAP rows have independent skip policies, mirroring
|
|
13
|
-
* the two separate passes this worker replaces:
|
|
14
|
-
* - **MI** never requires coverage. The module score is emitted for every
|
|
15
|
-
* file that reads + transpiles + parses. A read failure yields
|
|
16
|
-
* `miScore: null` (the host drops the file from the MI map, matching
|
|
17
|
-
* `calculateAll`'s `score === null` filter). A transpile failure or a
|
|
18
|
-
* parse error yields `miScore: 0` (matching `calculateForFile` /
|
|
19
|
-
* `calculateForSource`, which return 0 on transpile-null / parse-error).
|
|
20
|
-
* - **CRAP** honours `requireCoverage`. A file with no coverage entry is
|
|
21
|
-
* reported as `skippedFileNoCoverage: true` (the host increments its own
|
|
22
|
-
* counter and emits no CRAP rows for it) ā but the MI score is STILL
|
|
23
|
-
* computed and returned, because the MI pass would have scored it.
|
|
24
|
-
*
|
|
25
|
-
* Message contract ā see lib/cpu-pool.js:
|
|
26
|
-
* IN : { item: { abs: string, relPath: string, requireCoverage: boolean,
|
|
27
|
-
* coverageAvailable?: boolean, coverageEntry: object | null } }
|
|
28
|
-
* { exit: true }
|
|
29
|
-
* OUT : { ok: true, result: {
|
|
30
|
-
* relPath,
|
|
31
|
-
* miScore: number | null,
|
|
32
|
-
* skippedFileNoCoverage: boolean,
|
|
33
|
-
* crapRows: Array<{ method, startLine, cyclomatic, coverage, crap }> | null,
|
|
34
|
-
* skippedMethodsNoCoverage: number,
|
|
35
|
-
* hasCoverageEntry: boolean,
|
|
36
|
-
* resolvedMethods: number,
|
|
37
|
-
* totalMethods: number,
|
|
38
|
-
* } }
|
|
39
|
-
*
|
|
40
|
-
* A read/transpile/parse failure surfaces as `crapRows: null` so the host
|
|
41
|
-
* loop drops the file's CRAP contribution (matching the crap-worker's
|
|
42
|
-
* `rows: null` contract) ā never aborts the whole scan. On a read failure
|
|
43
|
-
* `miScore` is `null`; on a transpile/parse failure `miScore` is `0`.
|
|
44
|
-
*/
|
|
45
|
-
|
|
46
|
-
import { parentPort } from 'node:worker_threads';
|
|
47
|
-
import { finalizeMethodRows } from '../crap-engine.js';
|
|
48
|
-
import { analyzeOnce } from '../crap-utils.js';
|
|
49
|
-
import { prepareSourceForScoring } from '../transpile.js';
|
|
50
|
-
import { serveWorkerMessages } from './serve-worker-messages.js';
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Pure handler for a single inbound worker message. Exported so unit tests
|
|
54
|
-
* can exercise every branch (bad-shape rejection, coverage gate, read /
|
|
55
|
-
* transpile / parse failures, success rows, skipped methods, and the
|
|
56
|
-
* MI-computed-even-when-coverage-skipped invariant) without spawning a real
|
|
57
|
-
* `Worker` thread.
|
|
58
|
-
*
|
|
59
|
-
* Side effects (fs, transpile, analyzeOnce) are wired through `deps` so
|
|
60
|
-
* tests pass deterministic stubs.
|
|
61
|
-
*
|
|
62
|
-
* @param {unknown} msg
|
|
63
|
-
* @param {{
|
|
64
|
-
* readFile?: (abs: string) => string,
|
|
65
|
-
* transpile?: (abs: string, source: string, opts?: object) => unknown,
|
|
66
|
-
* prepare?: (abs: string, deps: object) => object,
|
|
67
|
-
* analyze?: (source: string, entry: object|null, mapLine: Function|null) => {
|
|
68
|
-
* miScore: number,
|
|
69
|
-
* crapRows: Array<object>,
|
|
70
|
-
* parseError: boolean,
|
|
71
|
-
* },
|
|
72
|
-
* }} [deps]
|
|
73
|
-
* @returns {{kind: 'exit'} | {kind: 'reply', message: object}}
|
|
74
|
-
*/
|
|
75
|
-
export function handleCombinedMiCrapWorkerMessage(msg, deps = {}) {
|
|
76
|
-
if (msg && msg.exit === true) return { kind: 'exit' };
|
|
77
|
-
|
|
78
|
-
const item = msg?.item;
|
|
79
|
-
if (
|
|
80
|
-
!item ||
|
|
81
|
-
typeof item.abs !== 'string' ||
|
|
82
|
-
typeof item.relPath !== 'string'
|
|
83
|
-
) {
|
|
84
|
-
return {
|
|
85
|
-
kind: 'reply',
|
|
86
|
-
message: {
|
|
87
|
-
ok: false,
|
|
88
|
-
error: `bad worker message: ${JSON.stringify(msg)}`,
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
const { abs, relPath, requireCoverage, coverageAvailable = true } = item;
|
|
93
|
-
const prepare = deps.prepare ?? prepareSourceForScoring;
|
|
94
|
-
const analyze = deps.analyze ?? analyzeOnce;
|
|
95
|
-
|
|
96
|
-
// Coverage entry is pre-resolved on the host and attached to the item.
|
|
97
|
-
// `item.coverageEntry` may be explicitly `null` when the file has no
|
|
98
|
-
// coverage, or `undefined` when the caller did not supply it (treat as null).
|
|
99
|
-
const entry = item.coverageEntry ?? null;
|
|
100
|
-
|
|
101
|
-
const reply = (result) => ({
|
|
102
|
-
kind: 'reply',
|
|
103
|
-
message: {
|
|
104
|
-
ok: true,
|
|
105
|
-
result: {
|
|
106
|
-
relPath,
|
|
107
|
-
skippedFileNoCoverage: false,
|
|
108
|
-
skippedMethodsNoCoverage: 0,
|
|
109
|
-
hasCoverageEntry: entry !== null,
|
|
110
|
-
resolvedMethods: 0,
|
|
111
|
-
totalMethods: 0,
|
|
112
|
-
...result,
|
|
113
|
-
},
|
|
114
|
-
},
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
// Read + transpile once, carrying the transpiled -> original-source line
|
|
118
|
-
// map the per-method coverage join needs (Story #4775). A read failure
|
|
119
|
-
// means neither MI nor CRAP can be computed (MI drops as null, matching
|
|
120
|
-
// `calculateAll`'s filter); a transpile failure scores MI 0 (matching
|
|
121
|
-
// `calculateForFile`). CRAP drops the file either way.
|
|
122
|
-
const prepared = prepare(abs, deps);
|
|
123
|
-
if (prepared.error) {
|
|
124
|
-
return reply({
|
|
125
|
-
miScore: prepared.error === 'read' ? null : 0,
|
|
126
|
-
crapRows: null,
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// ONE parse: analyzeOnce derives both the module MI score and the raw
|
|
131
|
-
// per-method CRAP rows from a single escomplex report. On a parse error it
|
|
132
|
-
// returns miScore 0 and an empty crapRows with parseError true.
|
|
133
|
-
const {
|
|
134
|
-
miScore,
|
|
135
|
-
crapRows: rawCrapRows,
|
|
136
|
-
parseError,
|
|
137
|
-
} = analyze(prepared.code, entry, prepared.mapLine);
|
|
138
|
-
if (parseError) {
|
|
139
|
-
// Parse error: MI scores 0 (parity with calculateForSource's catch ->
|
|
140
|
-
// returns 0), CRAP drops the file (rows null, parity with the crap
|
|
141
|
-
// worker's calculateCrap-throw branch).
|
|
142
|
-
return reply({ miScore: 0, crapRows: null });
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// CRAP coverage gate runs AFTER the parse so the MI score is always
|
|
146
|
-
// available. When the file has no coverage under requireCoverage, the CRAP
|
|
147
|
-
// pass would have skipped it at the file level (no rows, counted) ā but the
|
|
148
|
-
// MI pass would still have scored it, so miScore is returned regardless.
|
|
149
|
-
if (requireCoverage && entry === null) {
|
|
150
|
-
return reply({ miScore, skippedFileNoCoverage: true, crapRows: [] });
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const { rows, skippedMethodsNoCoverage, resolvedMethods, totalMethods } =
|
|
154
|
-
finalizeMethodRows(rawCrapRows, {
|
|
155
|
-
requireCoverage,
|
|
156
|
-
coverageAvailable,
|
|
157
|
-
});
|
|
158
|
-
return reply({
|
|
159
|
-
miScore,
|
|
160
|
-
crapRows: rows,
|
|
161
|
-
skippedMethodsNoCoverage,
|
|
162
|
-
resolvedMethods,
|
|
163
|
-
totalMethods,
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
serveWorkerMessages(parentPort, (msg) =>
|
|
168
|
-
handleCombinedMiCrapWorkerMessage(msg),
|
|
169
|
-
);
|
|
@@ -1,507 +0,0 @@
|
|
|
1
|
-
/* node:coverage ignore file -- top-level CLI gate; tested logic lives in lib/gates/baseline-store.js */
|
|
2
|
-
|
|
3
|
-
import { spawnSync } from 'node:child_process';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { parseArgsStringToArgv } from 'string-argv';
|
|
7
|
-
import { runAsCli } from './lib/cli-utils.js';
|
|
8
|
-
import {
|
|
9
|
-
getBaselines,
|
|
10
|
-
getCommands,
|
|
11
|
-
getLimits,
|
|
12
|
-
resolveConfig,
|
|
13
|
-
} from './lib/config-resolver.js';
|
|
14
|
-
import { isDegraded, softFailOrThrow } from './lib/degraded-mode.js';
|
|
15
|
-
import {
|
|
16
|
-
BaselineNotFoundError,
|
|
17
|
-
loadBaseline,
|
|
18
|
-
writeBaseline,
|
|
19
|
-
} from './lib/gates/baseline-store.js';
|
|
20
|
-
import { Logger } from './lib/Logger.js';
|
|
21
|
-
|
|
22
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
-
const PROJECT_ROOT = path.resolve(__dirname, '../..');
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Hardcoded Node `spawnSync` buffer ceiling (Epic #1720 Story #1739 ā
|
|
27
|
-
* `limits.executionMaxBuffer` moved from `.agentrc.json` to a
|
|
28
|
-
* framework-internal constant). 10 MiB matches the legacy default;
|
|
29
|
-
* lint output that exceeds this much JSON is a symptom of misconfigured
|
|
30
|
-
* scopes, not a tuning case.
|
|
31
|
-
*/
|
|
32
|
-
const EXECUTION_MAX_BUFFER = 10485760;
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Allowlist of command names that resolve via shell-launcher shims on
|
|
36
|
-
* Windows (`.cmd` / `.bat`). Node's `spawnSync({ shell: false })` cannot
|
|
37
|
-
* execute `.cmd` files (post CVE-2024-27980), so commands beginning with
|
|
38
|
-
* one of these names are invoked through the platform shell. Restricting
|
|
39
|
-
* `shell: true` to this fixed set keeps the command-injection surface
|
|
40
|
-
* minimal while making the default `npx eslint ā¦` invocation portable.
|
|
41
|
-
*/
|
|
42
|
-
const SHIM_LAUNCHERS = new Set(['npx', 'npm', 'pnpm', 'pnpx', 'yarn']);
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Decide how `spawnSync` should be invoked for `cmdConfig`. Returns the
|
|
46
|
-
* full argument tuple ā exported for unit testing the shim-detection
|
|
47
|
-
* logic without a real process spawn.
|
|
48
|
-
*
|
|
49
|
-
* @param {string} cmdConfig ā operator-configured command string.
|
|
50
|
-
* @returns {{ shell: boolean, command: string, args: string[] }}
|
|
51
|
-
*/
|
|
52
|
-
export function pickSpawnShape(cmdConfig) {
|
|
53
|
-
const parsedArgs = parseArgsStringToArgv(cmdConfig);
|
|
54
|
-
if (parsedArgs.length === 0) {
|
|
55
|
-
return { shell: false, command: '', args: [] };
|
|
56
|
-
}
|
|
57
|
-
const head = parsedArgs[0];
|
|
58
|
-
if (SHIM_LAUNCHERS.has(head)) {
|
|
59
|
-
return { shell: true, command: cmdConfig, args: [] };
|
|
60
|
-
}
|
|
61
|
-
return { shell: false, command: head, args: parsedArgs.slice(1) };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Shared core: extract the JSON-array tail from shell output, then tally
|
|
65
|
-
// errors/warnings. `{ detailed: true }` also returns per-file counts + rule
|
|
66
|
-
// histogram (used by `diff` + `captureBaseline` to attribute regressions).
|
|
67
|
-
function parseLintShared(jsonStr, { detailed = false } = {}) {
|
|
68
|
-
const empty = detailed
|
|
69
|
-
? { errorCount: 0, warningCount: 0, byFile: {} }
|
|
70
|
-
: { errorCount: 0, warningCount: 0 };
|
|
71
|
-
const startIndex = jsonStr.indexOf('[');
|
|
72
|
-
const endIndex = jsonStr.lastIndexOf(']');
|
|
73
|
-
if (startIndex === -1 || endIndex === -1) {
|
|
74
|
-
if (jsonStr === '') return empty;
|
|
75
|
-
throw new Error(
|
|
76
|
-
`Could not find JSON array in output. Output: ${jsonStr.substring(0, 100)}`,
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
const output = JSON.parse(jsonStr.substring(startIndex, endIndex + 1));
|
|
80
|
-
let totalErrors = 0;
|
|
81
|
-
let totalWarnings = 0;
|
|
82
|
-
const byFile = detailed ? {} : null;
|
|
83
|
-
for (const file of output) {
|
|
84
|
-
const errorCount = file.errorCount || 0;
|
|
85
|
-
const warningCount = file.warningCount || 0;
|
|
86
|
-
totalErrors += errorCount;
|
|
87
|
-
totalWarnings += warningCount;
|
|
88
|
-
if (!detailed) continue;
|
|
89
|
-
if (errorCount === 0 && warningCount === 0) continue;
|
|
90
|
-
const filePath = file.filePath || file.file || '<unknown>';
|
|
91
|
-
const rules = {};
|
|
92
|
-
if (Array.isArray(file.messages)) {
|
|
93
|
-
for (const msg of file.messages) {
|
|
94
|
-
const ruleId = msg.ruleId || msg.rule || '<unknown>';
|
|
95
|
-
rules[ruleId] = (rules[ruleId] || 0) + 1;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
byFile[filePath] = { errorCount, warningCount, rules };
|
|
99
|
-
}
|
|
100
|
-
return detailed
|
|
101
|
-
? { errorCount: totalErrors, warningCount: totalWarnings, byFile }
|
|
102
|
-
: { errorCount: totalErrors, warningCount: totalWarnings };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function parseLintOutput(jsonStr, _cmdConfig) {
|
|
106
|
-
return parseLintShared(jsonStr, { detailed: false });
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Detailed twin of `parseLintOutput`: returns `byFile` with per-file
|
|
111
|
-
* counts and a rule histogram keyed by ruleId (or `<unknown>`).
|
|
112
|
-
*/
|
|
113
|
-
export function parseLintOutputDetailed(jsonStr, _cmdConfig) {
|
|
114
|
-
return parseLintShared(jsonStr, { detailed: true });
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Soft-fail contract (Tech Spec #819): a JSON-parse failure emits the
|
|
118
|
-
// degraded envelope (or hard-fails under `--gate-mode`) so callers see
|
|
119
|
-
// the explicit signal rather than a silent zero-error fallback.
|
|
120
|
-
function runLintShared(
|
|
121
|
-
cmdConfig,
|
|
122
|
-
executionTimeoutMs,
|
|
123
|
-
executionMaxBuffer,
|
|
124
|
-
gateModeOpts,
|
|
125
|
-
{ detailed = false } = {},
|
|
126
|
-
) {
|
|
127
|
-
const shape = pickSpawnShape(cmdConfig);
|
|
128
|
-
if (shape.command === '') {
|
|
129
|
-
Logger.warn(`ā ļø [lint-baseline] Empty command configuration provided.`);
|
|
130
|
-
return detailed
|
|
131
|
-
? { errorCount: 0, warningCount: 0, byFile: {} }
|
|
132
|
-
: { errorCount: 0, warningCount: 0 };
|
|
133
|
-
}
|
|
134
|
-
const result = spawnSync(shape.command, shape.args, {
|
|
135
|
-
cwd: PROJECT_ROOT,
|
|
136
|
-
encoding: 'utf-8',
|
|
137
|
-
timeout: executionTimeoutMs,
|
|
138
|
-
maxBuffer: executionMaxBuffer,
|
|
139
|
-
shell: shape.shell,
|
|
140
|
-
});
|
|
141
|
-
try {
|
|
142
|
-
return parseLintShared(result.stdout.trim(), { detailed });
|
|
143
|
-
} catch (err) {
|
|
144
|
-
return softFailOrThrow(
|
|
145
|
-
'LINT_OUTPUT_PARSE_FAILED',
|
|
146
|
-
`lint-baseline: failed to parse JSON from \`${cmdConfig}\`: ${err.message}`,
|
|
147
|
-
gateModeOpts,
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export function runLintCommand(
|
|
153
|
-
cmdConfig,
|
|
154
|
-
executionTimeoutMs,
|
|
155
|
-
executionMaxBuffer,
|
|
156
|
-
gateModeOpts,
|
|
157
|
-
) {
|
|
158
|
-
return runLintShared(
|
|
159
|
-
cmdConfig,
|
|
160
|
-
executionTimeoutMs,
|
|
161
|
-
executionMaxBuffer,
|
|
162
|
-
gateModeOpts,
|
|
163
|
-
{
|
|
164
|
-
detailed: false,
|
|
165
|
-
},
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function runLintCommandDetailed(
|
|
170
|
-
cmdConfig,
|
|
171
|
-
executionTimeoutMs,
|
|
172
|
-
executionMaxBuffer,
|
|
173
|
-
gateModeOpts,
|
|
174
|
-
) {
|
|
175
|
-
return runLintShared(
|
|
176
|
-
cmdConfig,
|
|
177
|
-
executionTimeoutMs,
|
|
178
|
-
executionMaxBuffer,
|
|
179
|
-
gateModeOpts,
|
|
180
|
-
{
|
|
181
|
-
detailed: true,
|
|
182
|
-
},
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export function captureBaseline(
|
|
187
|
-
cmdConfig,
|
|
188
|
-
executionTimeoutMs,
|
|
189
|
-
executionMaxBuffer,
|
|
190
|
-
baselinePath,
|
|
191
|
-
baselinePathRel,
|
|
192
|
-
gateModeOpts,
|
|
193
|
-
) {
|
|
194
|
-
Logger.info(`ā¶ [lint-baseline] Capturing lint baseline...`);
|
|
195
|
-
const detailed = runLintCommandDetailed(
|
|
196
|
-
cmdConfig,
|
|
197
|
-
executionTimeoutMs,
|
|
198
|
-
executionMaxBuffer,
|
|
199
|
-
gateModeOpts,
|
|
200
|
-
);
|
|
201
|
-
if (isDegraded(detailed)) return detailed;
|
|
202
|
-
writeBaseline({ baselinePath, data: detailed });
|
|
203
|
-
Logger.info(
|
|
204
|
-
`ā
Baseline captured: ${detailed.errorCount} errors, ${detailed.warningCount} warnings.`,
|
|
205
|
-
);
|
|
206
|
-
Logger.info(` Saved to: ${baselinePathRel}`);
|
|
207
|
-
return detailed;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Pure: compute per-file regressions between a baseline and a current
|
|
212
|
-
* detailed snapshot. Returns rows for files where current warnings or errors
|
|
213
|
-
* exceed baseline. Each row carries the delta and the rules contributing the
|
|
214
|
-
* most issues in the current snapshot. Sorted by descending warning delta
|
|
215
|
-
* then descending error delta.
|
|
216
|
-
*
|
|
217
|
-
* Exported for testing.
|
|
218
|
-
*
|
|
219
|
-
* @param {object} baseline Object loaded from baseline.json (may lack `byFile`).
|
|
220
|
-
* @param {object} current Detailed snapshot from `runLintCommandDetailed`.
|
|
221
|
-
* @returns {{ file: string, errorDelta: number, warningDelta: number, rules: string[] }[]}
|
|
222
|
-
*/
|
|
223
|
-
export function diffPerFile(baseline, current) {
|
|
224
|
-
const baseFiles = baseline?.byFile ?? {};
|
|
225
|
-
const curFiles = current?.byFile ?? {};
|
|
226
|
-
const rows = [];
|
|
227
|
-
for (const [filePath, cur] of Object.entries(curFiles)) {
|
|
228
|
-
const base = baseFiles[filePath] ?? { errorCount: 0, warningCount: 0 };
|
|
229
|
-
const errorDelta = cur.errorCount - (base.errorCount || 0);
|
|
230
|
-
const warningDelta = cur.warningCount - (base.warningCount || 0);
|
|
231
|
-
if (errorDelta <= 0 && warningDelta <= 0) continue;
|
|
232
|
-
const rules = Object.entries(cur.rules || {})
|
|
233
|
-
.sort((a, b) => b[1] - a[1])
|
|
234
|
-
.map(([rule]) => rule);
|
|
235
|
-
rows.push({ file: filePath, errorDelta, warningDelta, rules });
|
|
236
|
-
}
|
|
237
|
-
rows.sort((a, b) => {
|
|
238
|
-
if (b.warningDelta !== a.warningDelta) {
|
|
239
|
-
return b.warningDelta - a.warningDelta;
|
|
240
|
-
}
|
|
241
|
-
return b.errorDelta - a.errorDelta;
|
|
242
|
-
});
|
|
243
|
-
return rows;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Pure: render the diff rows as a fixed-width table suitable for terminal
|
|
248
|
-
* output. When there are no regressions, emits a single line. When the
|
|
249
|
-
* baseline lacks `byFile`, prepends a one-line note so operators understand
|
|
250
|
-
* why every currently-warning file shows as "new".
|
|
251
|
-
*
|
|
252
|
-
* Exported for testing.
|
|
253
|
-
*/
|
|
254
|
-
export function formatDiffTable(rows, { baselineHasByFile } = {}) {
|
|
255
|
-
if (rows.length === 0) {
|
|
256
|
-
return 'ā
No per-file regressions detected.';
|
|
257
|
-
}
|
|
258
|
-
const FILE_HEADER = 'File';
|
|
259
|
-
const DELTA_HEADER = 'Ī warn/err';
|
|
260
|
-
const RULES_HEADER = 'rules';
|
|
261
|
-
const fileWidth = Math.max(
|
|
262
|
-
FILE_HEADER.length,
|
|
263
|
-
...rows.map((r) => r.file.length),
|
|
264
|
-
);
|
|
265
|
-
const deltaCells = rows.map((r) => `+${r.warningDelta}w / +${r.errorDelta}e`);
|
|
266
|
-
const deltaWidth = Math.max(
|
|
267
|
-
DELTA_HEADER.length,
|
|
268
|
-
...deltaCells.map((c) => c.length),
|
|
269
|
-
);
|
|
270
|
-
const lines = [];
|
|
271
|
-
if (!baselineHasByFile) {
|
|
272
|
-
lines.push(
|
|
273
|
-
'ā¹ļø Baseline has no per-file data; treating every regression as "new since baseline".',
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
lines.push(
|
|
277
|
-
`${FILE_HEADER.padEnd(fileWidth)} ${DELTA_HEADER.padEnd(deltaWidth)} ${RULES_HEADER}`,
|
|
278
|
-
);
|
|
279
|
-
lines.push(
|
|
280
|
-
`${'-'.repeat(fileWidth)} ${'-'.repeat(deltaWidth)} ${'-'.repeat(RULES_HEADER.length)}`,
|
|
281
|
-
);
|
|
282
|
-
for (let i = 0; i < rows.length; i += 1) {
|
|
283
|
-
const r = rows[i];
|
|
284
|
-
const ruleStr = r.rules.length > 0 ? r.rules.join(', ') : '(no ruleId)';
|
|
285
|
-
lines.push(
|
|
286
|
-
`${r.file.padEnd(fileWidth)} ${deltaCells[i].padEnd(deltaWidth)} ${ruleStr}`,
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
|
-
return lines.join('\n');
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Load the baseline at `baselinePath` and return an empty-shape fallback when
|
|
294
|
-
* it is missing. `BaselineNotFoundError` is the only error class we tolerate
|
|
295
|
-
* ā anything else propagates. Pure-ish (warns via Logger).
|
|
296
|
-
*
|
|
297
|
-
* @param {{ baselinePath: string, baselinePathRel: string, includeByFile: boolean }} args
|
|
298
|
-
* @returns {{ baseline: object, baselineHasByFile: boolean }}
|
|
299
|
-
*/
|
|
300
|
-
export function loadBaselineWithFallback({
|
|
301
|
-
baselinePath,
|
|
302
|
-
baselinePathRel,
|
|
303
|
-
includeByFile,
|
|
304
|
-
}) {
|
|
305
|
-
const emptyBaseline = includeByFile
|
|
306
|
-
? { errorCount: 0, warningCount: 0, byFile: {} }
|
|
307
|
-
: { errorCount: 0, warningCount: 0 };
|
|
308
|
-
try {
|
|
309
|
-
const baseline = loadBaseline({ baselinePath });
|
|
310
|
-
const baselineHasByFile =
|
|
311
|
-
includeByFile &&
|
|
312
|
-
baseline &&
|
|
313
|
-
typeof baseline.byFile === 'object' &&
|
|
314
|
-
baseline.byFile !== null;
|
|
315
|
-
return { baseline, baselineHasByFile };
|
|
316
|
-
} catch (err) {
|
|
317
|
-
if (!(err instanceof BaselineNotFoundError)) throw err;
|
|
318
|
-
const msg = includeByFile
|
|
319
|
-
? `ā ļø No baseline found at ${baselinePathRel}. Treating baseline as empty.`
|
|
320
|
-
: `ā ļø No baseline found at ${baselinePathRel}. Assuming 0 baseline.`;
|
|
321
|
-
Logger.warn(msg);
|
|
322
|
-
return { baseline: emptyBaseline, baselineHasByFile: false };
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
export function diffBaseline(
|
|
327
|
-
cmdConfig,
|
|
328
|
-
executionTimeoutMs,
|
|
329
|
-
executionMaxBuffer,
|
|
330
|
-
baselinePath,
|
|
331
|
-
baselinePathRel,
|
|
332
|
-
gateModeOpts,
|
|
333
|
-
) {
|
|
334
|
-
Logger.info(`ā¶ [lint-baseline] Computing per-file regressions...`);
|
|
335
|
-
const current = runLintCommandDetailed(
|
|
336
|
-
cmdConfig,
|
|
337
|
-
executionTimeoutMs,
|
|
338
|
-
executionMaxBuffer,
|
|
339
|
-
gateModeOpts,
|
|
340
|
-
);
|
|
341
|
-
if (isDegraded(current)) return current;
|
|
342
|
-
|
|
343
|
-
const { baseline, baselineHasByFile } = loadBaselineWithFallback({
|
|
344
|
-
baselinePath,
|
|
345
|
-
baselinePathRel,
|
|
346
|
-
includeByFile: true,
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
Logger.info(
|
|
350
|
-
` Baseline: ${baseline.errorCount ?? 0} errors, ${baseline.warningCount ?? 0} warnings`,
|
|
351
|
-
);
|
|
352
|
-
Logger.info(
|
|
353
|
-
` Current: ${current.errorCount} errors, ${current.warningCount} warnings`,
|
|
354
|
-
);
|
|
355
|
-
Logger.info('');
|
|
356
|
-
|
|
357
|
-
const rows = diffPerFile(baseline, current);
|
|
358
|
-
Logger.info(formatDiffTable(rows, { baselineHasByFile }));
|
|
359
|
-
return { ...current, regressions: rows };
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/** Pure predicate: current snapshot has more errors or warnings than baseline. */
|
|
363
|
-
export function hasDegraded(current, baseline) {
|
|
364
|
-
return (
|
|
365
|
-
current.errorCount > baseline.errorCount ||
|
|
366
|
-
current.warningCount > baseline.warningCount
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/** Pure predicate: current snapshot has strictly fewer of either count. */
|
|
371
|
-
export function hasImproved(current, baseline) {
|
|
372
|
-
return (
|
|
373
|
-
current.errorCount < baseline.errorCount ||
|
|
374
|
-
current.warningCount < baseline.warningCount
|
|
375
|
-
);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
export function checkBaseline(
|
|
379
|
-
cmdConfig,
|
|
380
|
-
executionTimeoutMs,
|
|
381
|
-
executionMaxBuffer,
|
|
382
|
-
baselinePath,
|
|
383
|
-
baselinePathRel,
|
|
384
|
-
gateModeOpts,
|
|
385
|
-
) {
|
|
386
|
-
Logger.info(`ā¶ [lint-baseline] Checking lint against baseline...`);
|
|
387
|
-
const current = runLintCommand(
|
|
388
|
-
cmdConfig,
|
|
389
|
-
executionTimeoutMs,
|
|
390
|
-
executionMaxBuffer,
|
|
391
|
-
gateModeOpts,
|
|
392
|
-
);
|
|
393
|
-
if (isDegraded(current)) return current;
|
|
394
|
-
|
|
395
|
-
const { baseline } = loadBaselineWithFallback({
|
|
396
|
-
baselinePath,
|
|
397
|
-
baselinePathRel,
|
|
398
|
-
includeByFile: false,
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
Logger.info(
|
|
402
|
-
` Baseline: ${baseline.errorCount} errors, ${baseline.warningCount} warnings`,
|
|
403
|
-
);
|
|
404
|
-
Logger.info(
|
|
405
|
-
` Current: ${current.errorCount} errors, ${current.warningCount} warnings`,
|
|
406
|
-
);
|
|
407
|
-
|
|
408
|
-
if (hasDegraded(current, baseline)) {
|
|
409
|
-
throw new Error(
|
|
410
|
-
'\nšØ LINT DEGRADATION DETECTED! You have introduced new lint issues compared to the baseline.',
|
|
411
|
-
);
|
|
412
|
-
}
|
|
413
|
-
if (hasImproved(current, baseline)) {
|
|
414
|
-
writeBaseline({ baselinePath, data: current });
|
|
415
|
-
Logger.info(
|
|
416
|
-
`š Lint health improved! Ratcheted baseline down to current levels.`,
|
|
417
|
-
);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
Logger.info(`ā
Lint check passed.`);
|
|
421
|
-
return current;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
/**
|
|
425
|
-
* Orchestration body of `main` extracted as a sibling exported function so
|
|
426
|
-
* the validate / dispatch / classify-degraded ladder is unit-testable
|
|
427
|
-
* without spawning a process. `main` becomes a thin shell: parse ā call
|
|
428
|
-
* this ā render ā exit. CLI surface unchanged (same modes, same exit
|
|
429
|
-
* codes, same stdout JSON schema for degraded envelopes).
|
|
430
|
-
*
|
|
431
|
-
* Note: `checkBaseline` itself still throws on real lint degradation
|
|
432
|
-
* (preserved CLI behaviour ā `runAsCli` maps the throw to exit 1). Tests
|
|
433
|
-
* for the validation-error branch use the explicit `'invalid'` mode which
|
|
434
|
-
* never reaches the runner.
|
|
435
|
-
*
|
|
436
|
-
* @param {{ mode: string, gateModeArgv?: string[] }} values
|
|
437
|
-
* @param {{
|
|
438
|
-
* resolveConfig?: typeof resolveConfig,
|
|
439
|
-
* runners?: { capture?: Function, check?: Function, diff?: Function },
|
|
440
|
-
* env?: Record<string, string|undefined>,
|
|
441
|
-
* projectRoot?: string,
|
|
442
|
-
* }} [deps]
|
|
443
|
-
* @returns {Promise<{ exitCode: number, result: object }>}
|
|
444
|
-
* `result.kind` is one of: `'validation-error'`, `'envelope'`. Envelopes
|
|
445
|
-
* carry the raw runner output; `exitCode === 1` iff `isDegraded(envelope)`.
|
|
446
|
-
*/
|
|
447
|
-
export async function runLintBaselineCli(values, deps = {}) {
|
|
448
|
-
const { mode, gateModeArgv = [] } = values;
|
|
449
|
-
if (mode !== 'capture' && mode !== 'check' && mode !== 'diff') {
|
|
450
|
-
return {
|
|
451
|
-
exitCode: 1,
|
|
452
|
-
result: {
|
|
453
|
-
kind: 'validation-error',
|
|
454
|
-
message:
|
|
455
|
-
'Usage: node lint-baseline.js <capture|check|diff> [--gate-mode]',
|
|
456
|
-
},
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
const cfg = deps.resolveConfig ? deps.resolveConfig() : resolveConfig();
|
|
461
|
-
const cmdConfig = getCommands(cfg).lintBaseline;
|
|
462
|
-
const baselinePathRel = getBaselines(cfg).lint.path;
|
|
463
|
-
const projectRoot = deps.projectRoot ?? PROJECT_ROOT;
|
|
464
|
-
const baselinePath = path.resolve(projectRoot, baselinePathRel);
|
|
465
|
-
const limits = getLimits(cfg);
|
|
466
|
-
const env = deps.env ?? process.env;
|
|
467
|
-
|
|
468
|
-
const gateModeOpts = { argv: gateModeArgv, env };
|
|
469
|
-
|
|
470
|
-
const runners = deps.runners ?? {
|
|
471
|
-
capture: captureBaseline,
|
|
472
|
-
check: checkBaseline,
|
|
473
|
-
diff: diffBaseline,
|
|
474
|
-
};
|
|
475
|
-
const runner = runners[mode];
|
|
476
|
-
// Hardcoded post-reshape (Epic #1720 Story #1739) ā see EXECUTION_MAX_BUFFER.
|
|
477
|
-
const envelope = runner(
|
|
478
|
-
cmdConfig,
|
|
479
|
-
limits.executionTimeoutMs,
|
|
480
|
-
EXECUTION_MAX_BUFFER,
|
|
481
|
-
baselinePath,
|
|
482
|
-
baselinePathRel,
|
|
483
|
-
gateModeOpts,
|
|
484
|
-
);
|
|
485
|
-
|
|
486
|
-
return {
|
|
487
|
-
exitCode: isDegraded(envelope) ? 1 : 0,
|
|
488
|
-
result: { kind: 'envelope', envelope },
|
|
489
|
-
};
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
export async function main(args = process.argv) {
|
|
493
|
-
const values = { mode: args[2], gateModeArgv: args.slice(3) };
|
|
494
|
-
const { exitCode, result } = await runLintBaselineCli(values);
|
|
495
|
-
|
|
496
|
-
if (result.kind === 'validation-error') {
|
|
497
|
-
throw new Error(result.message);
|
|
498
|
-
}
|
|
499
|
-
// kind === 'envelope': only print on degraded soft-fail (preserves
|
|
500
|
-
// pre-refactor stdout contract ā happy paths stay quiet on stdout).
|
|
501
|
-
if (exitCode === 1 && isDegraded(result.envelope)) {
|
|
502
|
-
process.stdout.write(`${JSON.stringify(result.envelope)}\n`);
|
|
503
|
-
}
|
|
504
|
-
process.exit(exitCode);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
runAsCli(import.meta.url, main, { source: 'LintBaseline' });
|