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
|
@@ -4,19 +4,13 @@ import escomplex from 'typhonjs-escomplex';
|
|
|
4
4
|
import { canonicalise as canonicalisePath } from './baselines/path-canon.js';
|
|
5
5
|
import { findCoverageEntry } from './coverage-utils.js';
|
|
6
6
|
import { POOL_SERIAL_THRESHOLD, runOnPool } from './cpu-pool.js';
|
|
7
|
-
import { finalizeMethodRowsWithBaseline } from './crap-baseline-join.js';
|
|
8
7
|
import {
|
|
9
|
-
|
|
10
|
-
finalizeMethodRows,
|
|
11
|
-
methodRowsFromReport,
|
|
12
|
-
} from './crap-engine.js';
|
|
13
|
-
import {
|
|
14
|
-
resolvedFromBaselineFlag,
|
|
8
|
+
finalizeMethodRowsWithBaseline,
|
|
15
9
|
resolveIncrementalContext,
|
|
16
10
|
resolveQueueIncrementalFields,
|
|
17
|
-
shouldRunSerial,
|
|
18
11
|
shouldSkipFileForNoCoverage,
|
|
19
|
-
} from './crap-
|
|
12
|
+
} from './crap-baseline-join.js';
|
|
13
|
+
import { COORDINATE_ORIGINAL, methodRowsFromReport } from './crap-engine.js';
|
|
20
14
|
import { Logger } from './Logger.js';
|
|
21
15
|
import { scanDirectory } from './maintainability-utils.js';
|
|
22
16
|
import {
|
|
@@ -25,10 +19,6 @@ import {
|
|
|
25
19
|
} from './transpile.js';
|
|
26
20
|
|
|
27
21
|
const CRAP_WORKER_URL = new URL('./workers/crap-worker.js', import.meta.url);
|
|
28
|
-
const COMBINED_MI_CRAP_WORKER_URL = new URL(
|
|
29
|
-
'./workers/combined-mi-crap-worker.js',
|
|
30
|
-
import.meta.url,
|
|
31
|
-
);
|
|
32
22
|
|
|
33
23
|
// Pool-vs-serial cutover — single-sourced in cpu-pool.js (see the
|
|
34
24
|
// POOL_SERIAL_THRESHOLD docstring for the tuning rationale).
|
|
@@ -79,192 +69,6 @@ export function resolveEscomplexVersion(cwd = process.cwd()) {
|
|
|
79
69
|
return '0.0.0';
|
|
80
70
|
}
|
|
81
71
|
|
|
82
|
-
function resolveBaselinePath({ cwd = process.cwd(), baselinePath } = {}) {
|
|
83
|
-
if (typeof baselinePath !== 'string' || baselinePath.length === 0) {
|
|
84
|
-
throw new TypeError(
|
|
85
|
-
'crap-utils: opts.baselinePath is required (Epic #730 Story 5.5 — ' +
|
|
86
|
-
'callers resolve the path via getBaselines(config).crap.path).',
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
return path.isAbsolute(baselinePath)
|
|
90
|
-
? baselinePath
|
|
91
|
-
: path.join(cwd, baselinePath);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Load the CRAP baseline envelope from disk.
|
|
96
|
-
*
|
|
97
|
-
* Returns the parsed envelope on success, or `null` when the file is missing,
|
|
98
|
-
* unreadable, or structurally unusable. Version-mismatch detection is a
|
|
99
|
-
* caller concern — this loader never silently rescores or mutates the
|
|
100
|
-
* envelope.
|
|
101
|
-
*
|
|
102
|
-
* @param {{cwd?: string, baselinePath?: string}} [opts]
|
|
103
|
-
* @returns {{
|
|
104
|
-
* kernelVersion: string,
|
|
105
|
-
* escomplexVersion: string,
|
|
106
|
-
* rows: Array<{file: string, method: string, startLine: number, crap: number}>,
|
|
107
|
-
* }|null}
|
|
108
|
-
*/
|
|
109
|
-
/**
|
|
110
|
-
* The envelope-level fields the CRAP compat axes read off a *loaded* baseline
|
|
111
|
-
* — the set every read path owes `assertBaselineCompatible`.
|
|
112
|
-
*
|
|
113
|
-
* It exists because there are TWO read paths and they have now diverged three
|
|
114
|
-
* times. `check-baselines` loads through `baselines/reader.js`; the
|
|
115
|
-
* `quality-preview` pre-commit arm loads through `projectCrapEnvelopeToLegacy`
|
|
116
|
-
* below. Both are ALLOW-LISTS, both feed the same axes, and a stamp added to
|
|
117
|
-
* one and not the other yields two opposite verdicts on one file: Story #4866
|
|
118
|
-
* (`scoringSemantics`, `tsTranspilerVersion`), Story #4969 (`rows[].anonymous`)
|
|
119
|
-
* and Story #4986 (`provenanceStamped`, half-fixed by #4973) were each that
|
|
120
|
-
* same half-landing.
|
|
121
|
-
*
|
|
122
|
-
* Every one of those axes keys on a POSITIVE marker, so a dropped field reads
|
|
123
|
-
* `undefined` and fails the baseline closed with a remedy that cannot work —
|
|
124
|
-
* re-deriving it writes the stamp the read path then discards. The projection
|
|
125
|
-
* below is DERIVED from this list rather than repeating it, so a new stamp is
|
|
126
|
-
* carried here the moment it is named; a parity test holds the reader to the
|
|
127
|
-
* same set and names whichever path forgot one.
|
|
128
|
-
*
|
|
129
|
-
* Row-level markers are deliberately out: they are projected per-row, not as
|
|
130
|
-
* envelope stamps.
|
|
131
|
-
*
|
|
132
|
-
* Deliberately module-local, mirroring `SCORING_SEMANTICS` in
|
|
133
|
-
* `baselines/kinds/crap.js`: the writer's `envelopeExtras()` is the single
|
|
134
|
-
* production door to the stamp set, and exporting this list would add a second
|
|
135
|
-
* one that only a test reaches. The parity test holds this projection to
|
|
136
|
-
* `Object.keys(envelopeExtras())` instead, so a stamp the writer starts
|
|
137
|
-
* emitting fails the test here until it is named — which is the enforcement
|
|
138
|
-
* this list needs, not an export.
|
|
139
|
-
*/
|
|
140
|
-
const COMPAT_STAMP_FIELDS = Object.freeze([
|
|
141
|
-
'scoringSemantics',
|
|
142
|
-
'tsTranspilerVersion',
|
|
143
|
-
'provenanceStamped',
|
|
144
|
-
]);
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Per-stamp coercion applied on the way through the legacy projection. A field
|
|
148
|
-
* with no entry is carried VERBATIM, which is the correct default: the axes
|
|
149
|
-
* distinguish "stamped" from "absent", so inventing a value for a stamp the
|
|
150
|
-
* envelope never wrote is the one thing a read path must not do.
|
|
151
|
-
*/
|
|
152
|
-
const COMPAT_STAMP_NORMALIZERS = {
|
|
153
|
-
// `null` (not the running value) when unstamped, so `ts-transpiler-drift`
|
|
154
|
-
// can tell "written by a different transpiler" from "written before the
|
|
155
|
-
// stamp existed" instead of comparing a value against itself.
|
|
156
|
-
tsTranspilerVersion: (value) => (typeof value === 'string' ? value : null),
|
|
157
|
-
scoringSemantics: (value) => value ?? null,
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Project the compat stamps off a v2 envelope, driven by
|
|
162
|
-
* `COMPAT_STAMP_FIELDS`.
|
|
163
|
-
*
|
|
164
|
-
* @param {Record<string, unknown>} parsed
|
|
165
|
-
* @returns {Record<string, unknown>}
|
|
166
|
-
*/
|
|
167
|
-
function projectCompatStamps(parsed) {
|
|
168
|
-
const stamps = {};
|
|
169
|
-
for (const field of COMPAT_STAMP_FIELDS) {
|
|
170
|
-
const normalize = COMPAT_STAMP_NORMALIZERS[field];
|
|
171
|
-
stamps[field] = normalize ? normalize(parsed[field]) : parsed[field];
|
|
172
|
-
}
|
|
173
|
-
return stamps;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Story #1895: shipped baseline switched to the canonical envelope shape
|
|
178
|
-
* (`$schema`, `kernelVersion`, `generatedAt`, `rollup`, `rows` keyed on
|
|
179
|
-
* `path`). Backfill the legacy `escomplexVersion` field from the running
|
|
180
|
-
* scorer and re-key rows by `file` so existing comparators keep working
|
|
181
|
-
* until Story #1912 lands the unified gate. Detection probes the first row
|
|
182
|
-
* for the new `path` key — the legacy envelope also carries `$schema` but
|
|
183
|
-
* keys rows by `file`.
|
|
184
|
-
*
|
|
185
|
-
* **Compat stamps survive the projection (Story #4866).** `scoringSemantics`
|
|
186
|
-
* and `tsTranspilerVersion` are *baseline* facts, and the projection used to
|
|
187
|
-
* drop the first and overwrite the second with the RUNNING value — which made
|
|
188
|
-
* every compat axis reading them either unstamped or vacuously self-equal.
|
|
189
|
-
* They are carried verbatim now, `null` when the envelope never stamped them,
|
|
190
|
-
* so an axis can tell "written by a different transpiler" apart from "written
|
|
191
|
-
* before the stamp existed" instead of guessing.
|
|
192
|
-
*
|
|
193
|
-
* **This projection is one of TWO (Story #4986).** `check-baselines` reads a
|
|
194
|
-
* baseline through `baselines/reader.js`; `quality-preview` reads the same file
|
|
195
|
-
* through here. Both feed `assertBaselineCompatible`, so a stamp added to one
|
|
196
|
-
* allow-list and not the other produces two opposite verdicts on one envelope —
|
|
197
|
-
* which is what happened to `provenanceStamped`: #4973 added it to the reader
|
|
198
|
-
* and left this projection dropping it, so the pre-commit CRAP arm rejected
|
|
199
|
-
* every stamped baseline with an un-satisfiable "re-seed" remedy while the
|
|
200
|
-
* authoritative gate passed. The stamp block is derived from
|
|
201
|
-
* `COMPAT_STAMP_FIELDS` above so this projection cannot fall behind again.
|
|
202
|
-
*/
|
|
203
|
-
function projectCrapEnvelopeToLegacy(parsed) {
|
|
204
|
-
if (
|
|
205
|
-
!Array.isArray(parsed.rows) ||
|
|
206
|
-
parsed.rows.length === 0 ||
|
|
207
|
-
typeof parsed.rows[0]?.path !== 'string'
|
|
208
|
-
) {
|
|
209
|
-
return null;
|
|
210
|
-
}
|
|
211
|
-
return {
|
|
212
|
-
kernelVersion: parsed.kernelVersion,
|
|
213
|
-
escomplexVersion: resolveEscomplexVersion(),
|
|
214
|
-
...projectCompatStamps(parsed),
|
|
215
|
-
rows: parsed.rows.map((row) => ({
|
|
216
|
-
crap: row.crap,
|
|
217
|
-
file: row.path,
|
|
218
|
-
method: row.method,
|
|
219
|
-
startLine: row.startLine,
|
|
220
|
-
...(row.coordinateSystem === undefined
|
|
221
|
-
? {}
|
|
222
|
-
: { coordinateSystem: row.coordinateSystem }),
|
|
223
|
-
// Story #4969, same reason as the stamps above: `anonymous` is a
|
|
224
|
-
// BASELINE fact. Dropping it here left every re-keyed row looking like
|
|
225
|
-
// an unmarked anonymous one, which is precisely the shape the
|
|
226
|
-
// `anon-identity-unstamped` axis fails closed — the projection would
|
|
227
|
-
// have manufactured the very defect the axis exists to catch.
|
|
228
|
-
...(row.anonymous === undefined ? {} : { anonymous: row.anonymous }),
|
|
229
|
-
})),
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
export function getCrapBaseline(opts = {}) {
|
|
234
|
-
const filePath = resolveBaselinePath(opts);
|
|
235
|
-
if (!fs.existsSync(filePath)) return null;
|
|
236
|
-
let raw;
|
|
237
|
-
try {
|
|
238
|
-
raw = fs.readFileSync(filePath, 'utf-8');
|
|
239
|
-
} catch (err) {
|
|
240
|
-
Logger.warn(`[crap-utils] unable to read baseline: ${err.message}`);
|
|
241
|
-
return null;
|
|
242
|
-
}
|
|
243
|
-
let parsed;
|
|
244
|
-
try {
|
|
245
|
-
parsed = JSON.parse(raw);
|
|
246
|
-
} catch (err) {
|
|
247
|
-
Logger.warn(`[crap-utils] baseline is not valid JSON: ${err.message}`);
|
|
248
|
-
return null;
|
|
249
|
-
}
|
|
250
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
251
|
-
return null;
|
|
252
|
-
}
|
|
253
|
-
const projected = projectCrapEnvelopeToLegacy(parsed);
|
|
254
|
-
if (projected) return projected;
|
|
255
|
-
if (typeof parsed.kernelVersion !== 'string') return null;
|
|
256
|
-
if (typeof parsed.escomplexVersion !== 'string') return null;
|
|
257
|
-
if (!Array.isArray(parsed.rows)) return null;
|
|
258
|
-
// tsTranspilerVersion landed in kernel 1.1.0. Older envelopes (1.0.0)
|
|
259
|
-
// do not carry it; we surface that as the sentinel '0.0.0' so the
|
|
260
|
-
// version-drift detector can warn on first 1.1.0 check without
|
|
261
|
-
// crashing on a missing field.
|
|
262
|
-
if (typeof parsed.tsTranspilerVersion !== 'string') {
|
|
263
|
-
parsed.tsTranspilerVersion = '0.0.0';
|
|
264
|
-
}
|
|
265
|
-
return parsed;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
72
|
/**
|
|
269
73
|
* Project rich scan rows onto the minimal baseline row shape and assemble an
|
|
270
74
|
* envelope ready for the shared V2 writer.
|
|
@@ -428,9 +232,9 @@ export function checkResolutionFloor(resolution, floor) {
|
|
|
428
232
|
* Parse `source` exactly once with escomplex and derive both the
|
|
429
233
|
* maintainability score and the raw CRAP method rows from that single report.
|
|
430
234
|
*
|
|
431
|
-
* Callers that need both scores for the same source string
|
|
432
|
-
*
|
|
433
|
-
*
|
|
235
|
+
* Callers that need both scores for the same source string MUST use this
|
|
236
|
+
* helper rather than calling `calculateCrapForSource` and `calculateForSource`
|
|
237
|
+
* separately — doing so would parse the AST twice.
|
|
434
238
|
*
|
|
435
239
|
* Coverage-dependent CRAP values require a `coverageForFile` entry (the value
|
|
436
240
|
* from `coverage-final.json` for this file). Pass `null` when no coverage is
|
|
@@ -456,7 +260,7 @@ export function checkResolutionFloor(resolution, floor) {
|
|
|
456
260
|
* parseError: boolean,
|
|
457
261
|
* }}
|
|
458
262
|
*/
|
|
459
|
-
|
|
263
|
+
function analyzeOnce(source, coverageForFile, mapLine = null) {
|
|
460
264
|
let report;
|
|
461
265
|
try {
|
|
462
266
|
report = escomplex.analyzeModule(source);
|
|
@@ -469,6 +273,69 @@ export function analyzeOnce(source, coverageForFile, mapLine = null) {
|
|
|
469
273
|
return { report, miScore, crapRows, parseError: false };
|
|
470
274
|
}
|
|
471
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Build `scanAndScore`'s per-file work queue: canonicalise each discovered
|
|
278
|
+
* absolute path, drop everything outside `scopeSet`, and merge the
|
|
279
|
+
* incremental-join fields onto the surviving items.
|
|
280
|
+
*
|
|
281
|
+
* Story #2079: every relPath goes through path-canon so a scan from inside
|
|
282
|
+
* `.worktrees/<workspace>/` (with `cwd` pointing at the main checkout) cannot
|
|
283
|
+
* leak the worktree prefix into the on-disk baseline's `file` / `path` keys.
|
|
284
|
+
*
|
|
285
|
+
* @param {string[]} files Absolute paths, already sorted.
|
|
286
|
+
* @param {{
|
|
287
|
+
* cwd: string,
|
|
288
|
+
* scopeSet: Set<string>|null,
|
|
289
|
+
* requireCoverage: boolean,
|
|
290
|
+
* coverageAvailable: boolean,
|
|
291
|
+
* incrementalCtx: object,
|
|
292
|
+
* }} opts
|
|
293
|
+
* @returns {Array<object>}
|
|
294
|
+
*/
|
|
295
|
+
function buildScanQueue(
|
|
296
|
+
files,
|
|
297
|
+
{ cwd, scopeSet, requireCoverage, coverageAvailable, incrementalCtx },
|
|
298
|
+
) {
|
|
299
|
+
const queue = [];
|
|
300
|
+
for (const abs of files) {
|
|
301
|
+
const rawRel = path.relative(cwd, abs).replace(/\\/g, '/');
|
|
302
|
+
const relPath = canonicalisePath(rawRel);
|
|
303
|
+
if (scopeSet && !scopeSet.has(relPath)) continue;
|
|
304
|
+
queue.push(
|
|
305
|
+
resolveQueueIncrementalFields(
|
|
306
|
+
{ abs, relPath, requireCoverage, coverageAvailable },
|
|
307
|
+
incrementalCtx,
|
|
308
|
+
),
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
return queue;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Project one finalized method row onto the enriched scan-row shape
|
|
316
|
+
* `compareCrap` and the baseline writer consume.
|
|
317
|
+
*
|
|
318
|
+
* @param {string} relPath Canonical repo-relative path of the scanned file.
|
|
319
|
+
* @param {object} mr A row from `finalizeMethodRowsWithBaseline`.
|
|
320
|
+
* @returns {object}
|
|
321
|
+
*/
|
|
322
|
+
function projectScanRow(relPath, mr) {
|
|
323
|
+
return {
|
|
324
|
+
file: relPath,
|
|
325
|
+
method: mr.method,
|
|
326
|
+
// Story #4969: `method` may be a derived anonymous identity; the flag is
|
|
327
|
+
// what lets the persisted row say so.
|
|
328
|
+
anonymous: mr.anonymous === true,
|
|
329
|
+
startLine: mr.startLine,
|
|
330
|
+
cyclomatic: mr.cyclomatic,
|
|
331
|
+
coverage: mr.coverage,
|
|
332
|
+
crap: mr.crap,
|
|
333
|
+
coordinateSystem: mr.coordinateSystem ?? COORDINATE_ORIGINAL,
|
|
334
|
+
// Present only when true, so a full-scope scan's rows are unaffected.
|
|
335
|
+
...(mr.resolvedFromBaseline === true ? { resolvedFromBaseline: true } : {}),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
472
339
|
/**
|
|
473
340
|
* Scan `targetDirs` for JS files, score each method via the CRAP kernel, and
|
|
474
341
|
* return enriched rows plus skip counters. Does not write to disk.
|
|
@@ -551,26 +418,20 @@ export async function scanAndScore({
|
|
|
551
418
|
|
|
552
419
|
// Build the work-queue first so scopeFile filtering happens before
|
|
553
420
|
// any I/O / IPC. `scannedFiles` is the in-scope count.
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
const rawRel = path.relative(cwd, abs).replace(/\\/g, '/');
|
|
562
|
-
const relPath = canonicalisePath(rawRel);
|
|
563
|
-
if (scopeSet && !scopeSet.has(relPath)) continue;
|
|
564
|
-
queue.push(
|
|
565
|
-
resolveQueueIncrementalFields(
|
|
566
|
-
{ abs, relPath, requireCoverage, coverageAvailable },
|
|
567
|
-
incrementalCtx,
|
|
568
|
-
),
|
|
569
|
-
);
|
|
570
|
-
}
|
|
421
|
+
const queue = buildScanQueue(files, {
|
|
422
|
+
cwd,
|
|
423
|
+
scopeSet,
|
|
424
|
+
requireCoverage,
|
|
425
|
+
coverageAvailable: isCoverageArtifactPresent(coverage),
|
|
426
|
+
incrementalCtx,
|
|
427
|
+
});
|
|
571
428
|
const scannedFiles = queue.length;
|
|
572
429
|
|
|
573
|
-
|
|
430
|
+
// Serial below the pool cutover, and ALWAYS in incremental mode: the
|
|
431
|
+
// per-file baseline lookup Maps the join needs do not cross the worker
|
|
432
|
+
// boundary (Story #4981).
|
|
433
|
+
const runSerial = queue.length < SERIAL_THRESHOLD || Boolean(incremental);
|
|
434
|
+
const perFile = runSerial
|
|
574
435
|
? queue.map((item) => ({ item, result: scoreFileSerial(item, coverage) }))
|
|
575
436
|
: await scoreFilesViaPool(queue, coverage);
|
|
576
437
|
|
|
@@ -598,19 +459,7 @@ export async function scanAndScore({
|
|
|
598
459
|
skippedMethodsNoCoverage += result.skippedMethodsNoCoverage ?? 0;
|
|
599
460
|
accumulateResolution(resolution, item.relPath, result);
|
|
600
461
|
for (const mr of result.rows) {
|
|
601
|
-
rows.push(
|
|
602
|
-
file: item.relPath,
|
|
603
|
-
method: mr.method,
|
|
604
|
-
// Story #4969: `method` may be a derived anonymous identity; the flag
|
|
605
|
-
// is what lets the persisted row say so.
|
|
606
|
-
anonymous: mr.anonymous === true,
|
|
607
|
-
startLine: mr.startLine,
|
|
608
|
-
cyclomatic: mr.cyclomatic,
|
|
609
|
-
coverage: mr.coverage,
|
|
610
|
-
crap: mr.crap,
|
|
611
|
-
coordinateSystem: mr.coordinateSystem ?? COORDINATE_ORIGINAL,
|
|
612
|
-
...resolvedFromBaselineFlag(mr),
|
|
613
|
-
});
|
|
462
|
+
rows.push(projectScanRow(item.relPath, mr));
|
|
614
463
|
}
|
|
615
464
|
}
|
|
616
465
|
|
|
@@ -635,8 +484,7 @@ export async function scanAndScore({
|
|
|
635
484
|
* reference implementation against which the worker output is asserted
|
|
636
485
|
* byte-for-byte in the cpu-pool tests.
|
|
637
486
|
*
|
|
638
|
-
* Uses `analyzeOnce` so the source is parsed a single time
|
|
639
|
-
* CRAP rows and the MI score are derived from the same escomplex report.
|
|
487
|
+
* Uses `analyzeOnce` so the source is parsed a single time.
|
|
640
488
|
*/
|
|
641
489
|
function scoreFileSerial(
|
|
642
490
|
{
|
|
@@ -713,287 +561,3 @@ async function scoreFilesViaPool(queue, coverage) {
|
|
|
713
561
|
return { item, result: r };
|
|
714
562
|
});
|
|
715
563
|
}
|
|
716
|
-
|
|
717
|
-
/**
|
|
718
|
-
* In-process combined scorer: parse `abs` exactly once via `analyzeOnce` and
|
|
719
|
-
* derive BOTH the module MI score and the per-method CRAP rows. The reference
|
|
720
|
-
* implementation for the combined worker, used directly below
|
|
721
|
-
* `SERIAL_THRESHOLD` (matching the serial fast paths of `calculateAll` and
|
|
722
|
-
* `scanAndScore`).
|
|
723
|
-
*
|
|
724
|
-
* Return shape mirrors `combined-mi-crap-worker.js`:
|
|
725
|
-
* - `miScore` — `null` on read failure (MI dropped by the host), `0` on
|
|
726
|
-
* transpile-null / parse-error (parity with `calculateForFile` /
|
|
727
|
-
* `calculateForSource`), otherwise the module maintainability index.
|
|
728
|
-
* - `crapRows` — `null` on read/transpile/parse failure (CRAP drops the
|
|
729
|
-
* file), `[]` when coverage-skipped, otherwise the scored method rows.
|
|
730
|
-
* - `skippedFileNoCoverage` / `skippedMethodsNoCoverage` — CRAP counters.
|
|
731
|
-
*/
|
|
732
|
-
function scoreFileCombinedSerial(
|
|
733
|
-
{ abs, relPath, requireCoverage, coverageAvailable = true },
|
|
734
|
-
coverage,
|
|
735
|
-
) {
|
|
736
|
-
const entry = findCoverageEntry(coverage, relPath);
|
|
737
|
-
const prepared = prepareSourceForScoring(abs);
|
|
738
|
-
if (prepared.error) {
|
|
739
|
-
return {
|
|
740
|
-
relPath,
|
|
741
|
-
miScore: prepared.error === 'read' ? null : 0,
|
|
742
|
-
skippedFileNoCoverage: false,
|
|
743
|
-
crapRows: null,
|
|
744
|
-
skippedMethodsNoCoverage: 0,
|
|
745
|
-
hasCoverageEntry: entry !== null,
|
|
746
|
-
resolvedMethods: 0,
|
|
747
|
-
totalMethods: 0,
|
|
748
|
-
};
|
|
749
|
-
}
|
|
750
|
-
const {
|
|
751
|
-
miScore,
|
|
752
|
-
crapRows: rawCrapRows,
|
|
753
|
-
parseError,
|
|
754
|
-
} = analyzeOnce(prepared.code, entry, prepared.mapLine);
|
|
755
|
-
if (parseError) {
|
|
756
|
-
return {
|
|
757
|
-
relPath,
|
|
758
|
-
miScore: 0,
|
|
759
|
-
skippedFileNoCoverage: false,
|
|
760
|
-
crapRows: null,
|
|
761
|
-
skippedMethodsNoCoverage: 0,
|
|
762
|
-
hasCoverageEntry: entry !== null,
|
|
763
|
-
resolvedMethods: 0,
|
|
764
|
-
totalMethods: 0,
|
|
765
|
-
};
|
|
766
|
-
}
|
|
767
|
-
if (requireCoverage && entry === null) {
|
|
768
|
-
return {
|
|
769
|
-
relPath,
|
|
770
|
-
miScore,
|
|
771
|
-
skippedFileNoCoverage: true,
|
|
772
|
-
crapRows: [],
|
|
773
|
-
skippedMethodsNoCoverage: 0,
|
|
774
|
-
hasCoverageEntry: false,
|
|
775
|
-
resolvedMethods: 0,
|
|
776
|
-
totalMethods: 0,
|
|
777
|
-
};
|
|
778
|
-
}
|
|
779
|
-
const { rows, skippedMethodsNoCoverage, resolvedMethods, totalMethods } =
|
|
780
|
-
finalizeMethodRows(rawCrapRows, {
|
|
781
|
-
requireCoverage,
|
|
782
|
-
coverageAvailable,
|
|
783
|
-
});
|
|
784
|
-
return {
|
|
785
|
-
relPath,
|
|
786
|
-
miScore,
|
|
787
|
-
skippedFileNoCoverage: false,
|
|
788
|
-
crapRows: rows,
|
|
789
|
-
skippedMethodsNoCoverage,
|
|
790
|
-
hasCoverageEntry: entry !== null,
|
|
791
|
-
resolvedMethods,
|
|
792
|
-
totalMethods,
|
|
793
|
-
};
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
async function scoreFilesCombinedViaPool(queue, coverage) {
|
|
797
|
-
const enrichedQueue = queue.map((item) => ({
|
|
798
|
-
...item,
|
|
799
|
-
coverageEntry: findCoverageEntry(coverage, item.relPath),
|
|
800
|
-
}));
|
|
801
|
-
const results = await runOnPool(COMBINED_MI_CRAP_WORKER_URL, enrichedQueue, {
|
|
802
|
-
workerData: {},
|
|
803
|
-
});
|
|
804
|
-
return results.map((r, i) => {
|
|
805
|
-
const item = queue[i];
|
|
806
|
-
if (!r || r.__cpuPoolError) {
|
|
807
|
-
Logger.warn(
|
|
808
|
-
`[crap-utils] combined worker pool error for ${item.relPath}: ${r?.message ?? 'unknown'}`,
|
|
809
|
-
);
|
|
810
|
-
return { item, result: null };
|
|
811
|
-
}
|
|
812
|
-
return { item, result: r };
|
|
813
|
-
});
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
/**
|
|
817
|
-
* Combined MI + CRAP single-pass scan. Walks the shared `targetDirs` once (or
|
|
818
|
-
* reuses `preScannedFiles`), dispatches every file through ONE worker that
|
|
819
|
-
* calls `analyzeOnce` a single time, and returns BOTH the maintainability
|
|
820
|
-
* score map and the CRAP scan result.
|
|
821
|
-
*
|
|
822
|
-
* This collapses the two independent escomplex passes the full-tree baseline
|
|
823
|
-
* regenerator used to run (`calculateAll` → maintainability worker, then
|
|
824
|
-
* `scanAndScore` → CRAP worker) into one parse per file. The outputs are
|
|
825
|
-
* shaped to be drop-in equivalents of the two passes they replace, so the
|
|
826
|
-
* downstream envelope projection + writer logic stays byte-identical:
|
|
827
|
-
*
|
|
828
|
-
* - `miScores` — `Record<relPath, number>` keyed exactly as `calculateAll`
|
|
829
|
-
* keys its result (`path.relative(cwd, abs)`, POSIX-normalised), with
|
|
830
|
-
* read-failure files (`miScore === null`) dropped. Parity target:
|
|
831
|
-
* `calculateAll(files)`.
|
|
832
|
-
* - `crap` — `{ rows, scannedFiles, skippedFilesNoCoverage,
|
|
833
|
-
* skippedMethodsNoCoverage }`, identical in shape and content to
|
|
834
|
-
* `scanAndScore({ targetDirs, coverage, ... })`. The rows are
|
|
835
|
-
* CRAP-sorted (file → startLine → method) so the result matches
|
|
836
|
-
* `scanAndScore` even before the writer re-sorts.
|
|
837
|
-
*
|
|
838
|
-
* The `requireCoverage`, `scopeFiles`, `ignoreGlobs`, and `preScannedFiles`
|
|
839
|
-
* semantics match `scanAndScore` exactly — coverage gating, scope filtering,
|
|
840
|
-
* and the single-walk reuse path behave the same. Files dropped from CRAP by
|
|
841
|
-
* the coverage gate STILL contribute their MI score (the MI pass never
|
|
842
|
-
* required coverage), preserving the two-pass behaviour where MI scores every
|
|
843
|
-
* file in the target dirs.
|
|
844
|
-
*
|
|
845
|
-
* @param {{
|
|
846
|
-
* targetDirs: string[],
|
|
847
|
-
* coverage: object|null,
|
|
848
|
-
* requireCoverage?: boolean,
|
|
849
|
-
* cwd?: string,
|
|
850
|
-
* scopeFiles?: Set<string>|string[]|null,
|
|
851
|
-
* ignoreGlobs?: string[],
|
|
852
|
-
* preScannedFiles?: string[]|null,
|
|
853
|
-
* }} params
|
|
854
|
-
* @returns {Promise<{
|
|
855
|
-
* miScores: Record<string, number>,
|
|
856
|
-
* crap: {
|
|
857
|
-
* rows: Array<{
|
|
858
|
-
* file: string, method: string, startLine: number,
|
|
859
|
-
* cyclomatic: number, coverage: number, crap: number,
|
|
860
|
-
* }>,
|
|
861
|
-
* scannedFiles: number,
|
|
862
|
-
* skippedFilesNoCoverage: number,
|
|
863
|
-
* skippedMethodsNoCoverage: number,
|
|
864
|
-
* },
|
|
865
|
-
* }>}
|
|
866
|
-
*/
|
|
867
|
-
export async function scanAndScoreCombined({
|
|
868
|
-
targetDirs,
|
|
869
|
-
coverage,
|
|
870
|
-
requireCoverage = true,
|
|
871
|
-
cwd = process.cwd(),
|
|
872
|
-
scopeFiles = null,
|
|
873
|
-
ignoreGlobs = [],
|
|
874
|
-
preScannedFiles = null,
|
|
875
|
-
}) {
|
|
876
|
-
if (!Array.isArray(targetDirs)) {
|
|
877
|
-
throw new TypeError('scanAndScoreCombined: targetDirs must be an array');
|
|
878
|
-
}
|
|
879
|
-
const scopeSet =
|
|
880
|
-
scopeFiles == null
|
|
881
|
-
? null
|
|
882
|
-
: scopeFiles instanceof Set
|
|
883
|
-
? scopeFiles
|
|
884
|
-
: new Set(scopeFiles);
|
|
885
|
-
|
|
886
|
-
// Single directory walk (or reuse the caller's pre-walked list), mirroring
|
|
887
|
-
// scanAndScore so the file discovery is byte-identical between paths.
|
|
888
|
-
const files = preScannedFiles != null ? [...preScannedFiles] : [];
|
|
889
|
-
if (preScannedFiles == null) {
|
|
890
|
-
for (const dir of targetDirs) {
|
|
891
|
-
const abs = path.isAbsolute(dir) ? dir : path.resolve(cwd, dir);
|
|
892
|
-
scanDirectory(abs, files, { cwd, ignoreGlobs });
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
files.sort();
|
|
896
|
-
|
|
897
|
-
// Build the work queue. Each item carries both the canonicalised relPath
|
|
898
|
-
// (CRAP's key + scope filter, matching scanAndScore) and the raw relPath
|
|
899
|
-
// (MI's key, matching calculateAll's `path.relative(cwd, p)` shape).
|
|
900
|
-
const coverageAvailable = isCoverageArtifactPresent(coverage);
|
|
901
|
-
const queue = [];
|
|
902
|
-
for (const abs of files) {
|
|
903
|
-
const rawRel = path.relative(cwd, abs).replace(/\\/g, '/');
|
|
904
|
-
const relPath = canonicalisePath(rawRel);
|
|
905
|
-
if (scopeSet && !scopeSet.has(relPath)) continue;
|
|
906
|
-
queue.push({
|
|
907
|
-
abs,
|
|
908
|
-
relPath,
|
|
909
|
-
miRel: rawRel,
|
|
910
|
-
requireCoverage,
|
|
911
|
-
coverageAvailable,
|
|
912
|
-
});
|
|
913
|
-
}
|
|
914
|
-
const scannedFiles = queue.length;
|
|
915
|
-
|
|
916
|
-
const perFile =
|
|
917
|
-
queue.length < SERIAL_THRESHOLD
|
|
918
|
-
? queue.map((item) => ({
|
|
919
|
-
item,
|
|
920
|
-
result: scoreFileCombinedSerial(item, coverage),
|
|
921
|
-
}))
|
|
922
|
-
: await scoreFilesCombinedViaPool(queue, coverage);
|
|
923
|
-
|
|
924
|
-
// MI assembly — mirror calculateAll: drop read-failure files (miScore
|
|
925
|
-
// null), key by the raw relative path, then sort ascending so the returned
|
|
926
|
-
// object is insertion-order-stable.
|
|
927
|
-
const miEntries = [];
|
|
928
|
-
// CRAP assembly — mirror scanAndScore: file-level skip counter, drop
|
|
929
|
-
// read/transpile/parse failures, accumulate method rows.
|
|
930
|
-
const crapRows = [];
|
|
931
|
-
let skippedFilesNoCoverage = 0;
|
|
932
|
-
let skippedMethodsNoCoverage = 0;
|
|
933
|
-
const resolution = newResolutionAccumulator();
|
|
934
|
-
|
|
935
|
-
for (const { item, result } of perFile) {
|
|
936
|
-
if (!result) continue; // unrecoverable per-file failure: drop silently
|
|
937
|
-
|
|
938
|
-
// MI side.
|
|
939
|
-
if (result.miScore !== null) {
|
|
940
|
-
miEntries.push({ relPath: item.miRel, score: result.miScore });
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
// CRAP side.
|
|
944
|
-
if (result.skippedFileNoCoverage) {
|
|
945
|
-
skippedFilesNoCoverage += 1;
|
|
946
|
-
continue;
|
|
947
|
-
}
|
|
948
|
-
if (result.crapRows === null) {
|
|
949
|
-
if (result.error) {
|
|
950
|
-
Logger.warn(
|
|
951
|
-
`[crap-utils] failed to score ${item.relPath}: ${result.error}`,
|
|
952
|
-
);
|
|
953
|
-
}
|
|
954
|
-
continue;
|
|
955
|
-
}
|
|
956
|
-
skippedMethodsNoCoverage += result.skippedMethodsNoCoverage ?? 0;
|
|
957
|
-
accumulateResolution(resolution, item.relPath, result);
|
|
958
|
-
for (const mr of result.crapRows) {
|
|
959
|
-
crapRows.push({
|
|
960
|
-
file: item.relPath,
|
|
961
|
-
method: mr.method,
|
|
962
|
-
// Story #4969: `method` may be a derived anonymous identity; the flag
|
|
963
|
-
// is what lets the persisted row say so.
|
|
964
|
-
anonymous: mr.anonymous === true,
|
|
965
|
-
startLine: mr.startLine,
|
|
966
|
-
cyclomatic: mr.cyclomatic,
|
|
967
|
-
coverage: mr.coverage,
|
|
968
|
-
crap: mr.crap,
|
|
969
|
-
coordinateSystem: mr.coordinateSystem ?? COORDINATE_ORIGINAL,
|
|
970
|
-
});
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
miEntries.sort((a, b) =>
|
|
975
|
-
a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0,
|
|
976
|
-
);
|
|
977
|
-
const miScores = {};
|
|
978
|
-
for (const { relPath, score } of miEntries) {
|
|
979
|
-
miScores[relPath] = score;
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
crapRows.sort((a, b) => {
|
|
983
|
-
if (a.file !== b.file) return a.file < b.file ? -1 : 1;
|
|
984
|
-
if (a.startLine !== b.startLine) return a.startLine - b.startLine;
|
|
985
|
-
if (a.method !== b.method) return a.method < b.method ? -1 : 1;
|
|
986
|
-
return 0;
|
|
987
|
-
});
|
|
988
|
-
|
|
989
|
-
return {
|
|
990
|
-
miScores,
|
|
991
|
-
crap: {
|
|
992
|
-
rows: crapRows,
|
|
993
|
-
scannedFiles,
|
|
994
|
-
skippedFilesNoCoverage,
|
|
995
|
-
skippedMethodsNoCoverage,
|
|
996
|
-
resolution: summarizeResolution(resolution),
|
|
997
|
-
},
|
|
998
|
-
};
|
|
999
|
-
}
|