mandrel 2.36.0 → 2.38.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 +16 -16
- package/.agents/agents/plan-critic.md +1 -1
- package/.agents/docs/SDLC.md +40 -40
- package/.agents/docs/configuration.md +41 -16
- package/.agents/docs/execution-reference.md +3 -3
- package/.agents/docs/quality-gates.md +2 -2
- package/.agents/docs/workflows.md +6 -6
- package/.agents/instructions.md +5 -5
- package/.agents/rules/changelog-style.md +3 -3
- package/.agents/rules/ci-remediation.md +1 -1
- package/.agents/rules/git-conventions-reference.md +4 -4
- package/.agents/rules/testing-standards.md +56 -0
- package/.agents/schemas/agentrc.schema.json +9 -9
- package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
- package/.agents/schemas/validation-evidence.schema.json +1 -1
- package/.agents/scripts/acceptance-eval.js +1 -1
- package/.agents/scripts/apply-quality-bootstrap.js +1 -1
- package/.agents/scripts/audit-to-stories.js +2 -2
- package/.agents/scripts/boot-sweep.js +7 -1
- package/.agents/scripts/check-context-budget.js +195 -103
- package/.agents/scripts/check-cyclomatic.js +112 -42
- package/.agents/scripts/check-doc-links.js +32 -4
- package/.agents/scripts/check-generated-validator.js +202 -0
- package/.agents/scripts/check-lifecycle-lint.js +2 -68
- package/.agents/scripts/check-schema-references.js +2 -28
- package/.agents/scripts/check-test-temp-hygiene.js +1 -1
- package/.agents/scripts/check-workflow-cli-lint.js +1 -1
- package/.agents/scripts/deliver-light.js +10 -10
- package/.agents/scripts/deliver-recover.js +1 -1
- package/.agents/scripts/drain-pending-cleanup.js +19 -2
- package/.agents/scripts/evidence-gate.js +1 -1
- package/.agents/scripts/generate-workflows-doc.js +1 -1
- package/.agents/scripts/lib/audit-suite/selector.js +1 -1
- package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +1 -1
- package/.agents/scripts/lib/audit-to-stories/build-story-body.js +1 -1
- package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +5 -5
- package/.agents/scripts/lib/audit-to-stories/wire-dependencies.js +1 -1
- package/.agents/scripts/lib/baselines/components.js +32 -2
- package/.agents/scripts/lib/baselines/env-overrides.js +1 -1
- package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
- package/.agents/scripts/lib/bdd-scenario-budget.js +1 -1
- package/.agents/scripts/lib/bdd-scenario-scanner.js +3 -3
- package/.agents/scripts/lib/bdd-step-index.js +1 -1
- package/.agents/scripts/lib/bootstrap/branch-protection.js +2 -2
- package/.agents/scripts/lib/bootstrap/commit-push.js +2 -2
- package/.agents/scripts/lib/bootstrap/install-ledger.js +1 -1
- package/.agents/scripts/lib/bootstrap/issue-forms-template.js +1 -1
- package/.agents/scripts/lib/bootstrap/prompt.js +1 -1
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +2 -2
- package/.agents/scripts/lib/checks/core-bare-clean.js +1 -1
- package/.agents/scripts/lib/checks/loop-health.js +1 -1
- package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +1 -1
- package/.agents/scripts/lib/cli-args.js +1 -3
- package/.agents/scripts/lib/close-validation/gates.js +1 -1
- package/.agents/scripts/lib/close-validation/process.js +1 -1
- package/.agents/scripts/lib/command-header.js +1 -1
- package/.agents/scripts/lib/config/delivery-routing.js +1 -1
- package/.agents/scripts/lib/config/explain.js +1 -1
- package/.agents/scripts/lib/config/runners.js +2 -2
- package/.agents/scripts/lib/config/runtime.js +1 -1
- package/.agents/scripts/lib/config/sync-agentrc.js +1 -1
- package/.agents/scripts/lib/config/temp-paths.js +2 -2
- package/.agents/scripts/lib/config-settings-schema-delivery.js +3 -3
- package/.agents/scripts/lib/config-settings-schema-quality.js +6 -6
- package/.agents/scripts/lib/config-settings-schema.js +49 -9
- package/.agents/scripts/lib/coverage-baseline.js +2 -2
- package/.agents/scripts/lib/cpu-pool.js +90 -10
- package/.agents/scripts/lib/crap-utils.js +6 -2
- package/.agents/scripts/lib/cyclomatic-ceiling.js +28 -6
- package/.agents/scripts/lib/cyclomatic-scope.js +144 -0
- package/.agents/scripts/lib/dependency-version.js +86 -0
- package/.agents/scripts/lib/duplicate-search.js +2 -2
- package/.agents/scripts/lib/errors/index.js +1 -1
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +2 -2
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +5 -5
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +2 -2
- package/.agents/scripts/lib/findings/classify-finding.js +1 -1
- package/.agents/scripts/lib/findings/promote-finding.js +12 -12
- package/.agents/scripts/lib/findings/route-finding.js +2 -2
- package/.agents/scripts/lib/generated/agentrc-validator.js +16 -0
- package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
- package/.agents/scripts/lib/install-cmd-parser.js +1 -1
- package/.agents/scripts/lib/knip-entry-sync.js +1 -57
- package/.agents/scripts/lib/label-constants.js +2 -2
- package/.agents/scripts/lib/label-taxonomy.js +1 -1
- package/.agents/scripts/lib/maintainability-utils.js +108 -10
- package/.agents/scripts/lib/observability/source-classifier.js +1 -0
- package/.agents/scripts/lib/onboard/init-tail.js +4 -4
- package/.agents/scripts/lib/onboard/scaffold-docs.js +2 -2
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +1 -1
- package/.agents/scripts/lib/orchestration/code-review.js +4 -4
- package/.agents/scripts/lib/orchestration/complexity-gate.js +5 -5
- package/.agents/scripts/lib/orchestration/deliver-recover.js +3 -3
- package/.agents/scripts/lib/orchestration/docs-digest.js +3 -3
- package/.agents/scripts/lib/orchestration/lease-guard-shared.js +2 -2
- package/.agents/scripts/lib/orchestration/light-backstop.js +1 -1
- package/.agents/scripts/lib/orchestration/light-escalation.js +6 -6
- package/.agents/scripts/lib/orchestration/light-suitability.js +19 -19
- package/.agents/scripts/lib/orchestration/plan-context.js +4 -4
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +2 -2
- package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +2 -2
- package/.agents/scripts/lib/orchestration/plan-metrics.js +1 -1
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +1 -1
- package/.agents/scripts/lib/orchestration/plan-persist/plan-context-source.js +3 -3
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +6 -6
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +86 -25
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +3 -3
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +6 -6
- package/.agents/scripts/lib/orchestration/planning/authoring-context.js +7 -7
- package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +1 -1
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +3 -3
- package/.agents/scripts/lib/orchestration/pr-watch.js +1 -30
- package/.agents/scripts/lib/orchestration/reassert-status-column.js +2 -2
- package/.agents/scripts/lib/orchestration/remote-verifier.js +2 -2
- package/.agents/scripts/lib/orchestration/resolve-stories.js +4 -4
- package/.agents/scripts/lib/orchestration/review-providers/codex.js +3 -3
- package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +1 -1
- package/.agents/scripts/lib/orchestration/review-providers/native.js +9 -2
- package/.agents/scripts/lib/orchestration/review-providers/review-depth.js +2 -2
- package/.agents/scripts/lib/orchestration/review-providers/security-review.js +3 -3
- package/.agents/scripts/lib/orchestration/run-epilogue.js +2 -2
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +2 -2
- package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +5 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +52 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +3 -3
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +2 -2
- package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +1 -1
- package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +2 -5
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +14 -12
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +1 -1
- package/.agents/scripts/lib/orchestration/ticket-lease.js +34 -9
- package/.agents/scripts/lib/orchestration/ticket-validator.js +1 -1
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +6 -4
- package/.agents/scripts/lib/plan-phase-cleanup.js +1 -1
- package/.agents/scripts/lib/preflight-runner.js +3 -3
- package/.agents/scripts/lib/qa/qa-session.js +1 -1
- package/.agents/scripts/lib/runtime-deps/manifest.js +1 -7
- package/.agents/scripts/lib/runtime-deps/scan-imports.js +3 -58
- package/.agents/scripts/lib/signals/schema.js +1 -1
- package/.agents/scripts/lib/single-story-sweep/sweep-lock.js +284 -25
- package/.agents/scripts/lib/skills/walk-skill-files.js +1 -1
- package/.agents/scripts/lib/source-text/strip-js-comments.js +115 -0
- package/.agents/scripts/lib/story-adjacency.js +2 -2
- package/.agents/scripts/lib/templates/decomposer-prompts.js +1 -1
- package/.agents/scripts/lib/test-isolate/runner.js +3 -3
- package/.agents/scripts/lib/test-runner-contract.js +14 -6
- package/.agents/scripts/lib/test-tiers.js +135 -18
- package/.agents/scripts/lib/ticket-body-sections.js +1 -1
- package/.agents/scripts/lib/transpile.js +16 -3
- package/.agents/scripts/lib/util/concurrent-map.js +1 -1
- package/.agents/scripts/lib/util/parse-id-list.js +1 -1
- package/.agents/scripts/lib/wave-runner/live-probe.js +4 -4
- package/.agents/scripts/lib/wave-runner/ready-set.js +1 -1
- package/.agents/scripts/lib/workflow-closure.js +1 -1
- package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +170 -32
- package/.agents/scripts/lib/worktree/node-modules-strategy.js +2 -5
- package/.agents/scripts/lint-issue-body.js +3 -3
- package/.agents/scripts/plan-context.js +4 -4
- package/.agents/scripts/plan-critics.js +2 -2
- package/.agents/scripts/plan-persist.js +2 -2
- package/.agents/scripts/plan-run-epilogue.js +2 -2
- package/.agents/scripts/pr-watch-with-update.js +305 -137
- package/.agents/scripts/providers/github/board-add.js +1 -1
- package/.agents/scripts/providers/github/errors.js +1 -1
- package/.agents/scripts/providers/github/tickets.js +114 -21
- package/.agents/scripts/quality-preview.js +162 -70
- package/.agents/scripts/resolve-stories.js +3 -3
- package/.agents/scripts/resync-status-column.js +1 -1
- package/.agents/scripts/run-lint.js +1 -1
- package/.agents/scripts/run-test-profile.js +8 -5
- package/.agents/scripts/run-tests.js +79 -14
- package/.agents/scripts/single-story-close.js +2 -4
- package/.agents/scripts/single-story-init.js +10 -6
- package/.agents/scripts/stories-wave-tick.js +8 -8
- package/.agents/skills/core/idea-refinement/SKILL.md +5 -5
- package/.agents/skills/core/scope-triage/SKILL.md +3 -3
- package/.agents/skills/skills.index.json +1 -1
- package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -1
- package/.agents/workflows/audit-data-model.md +1 -1
- package/.agents/workflows/audit-documentation.md +2 -2
- package/.agents/workflows/audit-to-stories.md +11 -11
- package/.agents/workflows/git-deliver.md +4 -4
- package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
- package/.agents/workflows/helpers/audit-lens-core.md +1 -1
- package/.agents/workflows/helpers/code-review.md +4 -4
- package/.agents/workflows/helpers/deliver-light.md +26 -26
- package/.agents/workflows/helpers/deliver-reference.md +15 -7
- package/.agents/workflows/helpers/deliver-story-reference.md +9 -4
- package/.agents/workflows/helpers/deliver-story.md +6 -7
- package/.agents/workflows/helpers/diagnose.md +2 -2
- package/.agents/workflows/helpers/parallel-tooling.md +1 -1
- package/.agents/workflows/helpers/plan-reference.md +9 -9
- package/.agents/workflows/helpers/qa-core.md +8 -8
- package/.agents/workflows/helpers/worktree-lifecycle.md +6 -6
- package/.agents/workflows/{deliver.md → mandrel-deliver.md} +15 -19
- package/.agents/workflows/{plan.md → mandrel-plan.md} +9 -9
- package/.agents/workflows/memory-consolidate.md +4 -4
- package/.agents/workflows/prototype.md +3 -3
- package/.agents/workflows/qa-assist.md +12 -12
- package/.agents/workflows/qa-explore.md +6 -6
- package/.agents/workflows/qa-run.md +1 -1
- package/README.md +7 -7
- package/docs/CHANGELOG.md +43 -0
- package/lib/cli/doctor.js +1 -1
- package/lib/cli/init.js +1 -1
- package/lib/cli/migrate.js +100 -94
- package/lib/migrations/helpers/retire-agentrc-key.js +200 -0
- package/lib/migrations/index.js +32 -33
- package/lib/migrations/steps/2.1.0-retire-mi-drop-knobs.js +21 -80
- package/lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js +15 -87
- package/lib/migrations/steps/2.11.0-retire-max-seed-words.js +13 -71
- package/lib/migrations/steps/2.2.0-retire-epic-ac-tags.js +1 -1
- package/lib/migrations/steps/2.20.0-retire-codebase-snapshot.js +13 -101
- package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +13 -100
- package/package.json +7 -3
- package/.agents/scripts/lib/audit-suite/frontmatter-lint.js +0 -32
- package/.agents/scripts/lib/baselines/maintainability-baseline-save.js +0 -37
- package/.agents/scripts/lib/cli/parse-numeric.js +0 -60
- package/.agents/scripts/lib/close-validation/telemetry.js +0 -79
- package/.agents/scripts/lib/orchestration/label-transitions.js +0 -44
- package/.agents/scripts/lib/orchestration/parked-follow-ons.js +0 -147
- package/.agents/scripts/lib/orchestration/phase-runner.js +0 -88
- package/.agents/scripts/lib/orchestration/recut.js +0 -56
|
@@ -223,7 +223,7 @@ export const QUALITY_SCHEMA = {
|
|
|
223
223
|
navigability: {
|
|
224
224
|
type: 'object',
|
|
225
225
|
description:
|
|
226
|
-
"Navigability lens + journey-suite config (Epic #4131, F2/F3/F1/F4). Read by audit-suite/selector.js (route globs) and /deliver's per-Story ceremony (journey suite). Opt-in: absent or empty routeGlobs degrades to a silent no-op.",
|
|
226
|
+
"Navigability lens + journey-suite config (Epic #4131, F2/F3/F1/F4). Read by audit-suite/selector.js (route globs) and /mandrel-deliver's per-Story ceremony (journey suite). Opt-in: absent or empty routeGlobs degrades to a silent no-op.",
|
|
227
227
|
properties: {
|
|
228
228
|
routeGlobs: {
|
|
229
229
|
type: 'array',
|
|
@@ -242,7 +242,7 @@ export const QUALITY_SCHEMA = {
|
|
|
242
242
|
journeySuite: {
|
|
243
243
|
type: 'string',
|
|
244
244
|
description:
|
|
245
|
-
"Path or command for the per-persona journey suite /deliver's per-Story ceremony runs.",
|
|
245
|
+
"Path or command for the per-persona journey suite /mandrel-deliver's per-Story ceremony runs.",
|
|
246
246
|
},
|
|
247
247
|
},
|
|
248
248
|
additionalProperties: false,
|
|
@@ -253,7 +253,7 @@ export const QUALITY_SCHEMA = {
|
|
|
253
253
|
|
|
254
254
|
/**
|
|
255
255
|
* `delivery.codeReview` — review-provider chain + bounded-retry knobs for
|
|
256
|
-
* the /deliver code-review ceremony (Story-close and plan-run close).
|
|
256
|
+
* the /mandrel-deliver code-review ceremony (Story-close and plan-run close).
|
|
257
257
|
*
|
|
258
258
|
* `autoFixSeverity` (Story #4399) is the threshold that governs which
|
|
259
259
|
* findings the host-LLM focused-fix routing remediates on-branch —
|
|
@@ -263,7 +263,7 @@ export const QUALITY_SCHEMA = {
|
|
|
263
263
|
export const CODE_REVIEW_SCHEMA = {
|
|
264
264
|
type: 'object',
|
|
265
265
|
description:
|
|
266
|
-
'Review-provider chain plus bounded-retry knobs for the /deliver code-review ceremony.',
|
|
266
|
+
'Review-provider chain plus bounded-retry knobs for the /mandrel-deliver code-review ceremony.',
|
|
267
267
|
properties: {
|
|
268
268
|
// Story #2825 (Epic #2815) seeded the pluggable review backend
|
|
269
269
|
// with `native`; Story #2830 added `codex` (the
|
|
@@ -353,7 +353,7 @@ export const CODE_REVIEW_SCHEMA = {
|
|
|
353
353
|
type: 'integer',
|
|
354
354
|
minimum: 0,
|
|
355
355
|
description:
|
|
356
|
-
'Maximum auto-fix retry attempts per finding in /deliver Phase 5 (code-review). 0 disables auto-fix. Default 3.',
|
|
356
|
+
'Maximum auto-fix retry attempts per finding in /mandrel-deliver Phase 5 (code-review). 0 disables auto-fix. Default 3.',
|
|
357
357
|
default: DEFAULT_CODE_REVIEW.maxFixAttempts,
|
|
358
358
|
},
|
|
359
359
|
maxFixScopeFiles: {
|
|
@@ -367,7 +367,7 @@ export const CODE_REVIEW_SCHEMA = {
|
|
|
367
367
|
type: 'string',
|
|
368
368
|
enum: ['high', 'medium'],
|
|
369
369
|
description:
|
|
370
|
-
'Severity threshold for on-branch remediation in /deliver Phase 5 (code-review). `medium` (default) routes 🔴/🟠/🟡 findings into the host-LLM focused-fix routing (Mediums batched per lens: one commit per lens, a single validation + rescan at the end) while 🟢 suggestions still graduate to follow-up issues; `high` reproduces the pre-4399 Critical/High-only routing. Hard cutover — no back-compat flag.',
|
|
370
|
+
'Severity threshold for on-branch remediation in /mandrel-deliver Phase 5 (code-review). `medium` (default) routes 🔴/🟠/🟡 findings into the host-LLM focused-fix routing (Mediums batched per lens: one commit per lens, a single validation + rescan at the end) while 🟢 suggestions still graduate to follow-up issues; `high` reproduces the pre-4399 Critical/High-only routing. Hard cutover — no back-compat flag.',
|
|
371
371
|
default: DEFAULT_CODE_REVIEW.autoFixSeverity,
|
|
372
372
|
},
|
|
373
373
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* node:coverage ignore file -- AJV schema declaration (data-as-code); MI < 70 is inherent to large flat schema literals, no business logic to test */
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import process from 'node:process';
|
|
5
5
|
import { COMMANDS_DEFAULTS } from './config/commands.js';
|
|
6
6
|
import {
|
|
7
7
|
BRANCH_PROTECTION_DEFAULTS,
|
|
@@ -15,6 +15,7 @@ import { SHELL_INJECTION_PATTERN_STRING } from './config-schema-shared.js';
|
|
|
15
15
|
// to keep this aggregate module above the maintainability floor. The
|
|
16
16
|
// resolved AGENTRC_SCHEMA is unchanged.
|
|
17
17
|
import { DELIVERY_SCHEMA } from './config-settings-schema-delivery.js';
|
|
18
|
+
import compiledAgentrcValidator from './generated/agentrc-validator.js';
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Annotation contract (Story #5007). These schema literals are the SINGLE
|
|
@@ -431,7 +432,7 @@ const GITHUB_SCHEMA = {
|
|
|
431
432
|
};
|
|
432
433
|
|
|
433
434
|
// ---------------------------------------------------------------------------
|
|
434
|
-
// planning.* — inputs to /plan
|
|
435
|
+
// planning.* — inputs to /mandrel-plan
|
|
435
436
|
// ---------------------------------------------------------------------------
|
|
436
437
|
|
|
437
438
|
// Story #4541: `planning.context.{maxBytes, summaryMode}` was retired. The
|
|
@@ -455,7 +456,7 @@ const GITHUB_SCHEMA = {
|
|
|
455
456
|
const PLANNING_SCHEMA = {
|
|
456
457
|
type: 'object',
|
|
457
458
|
description:
|
|
458
|
-
'Inputs to `/plan`: risk escalation heuristics, ceremony-lite routing, and the cross-Story conflict-finding severity gates.',
|
|
459
|
+
'Inputs to `/mandrel-plan`: risk escalation heuristics, ceremony-lite routing, and the cross-Story conflict-finding severity gates.',
|
|
459
460
|
properties: {
|
|
460
461
|
riskHeuristics: {
|
|
461
462
|
...LIST_OR_EXTENDER_OF_STRINGS,
|
|
@@ -590,7 +591,7 @@ const PLANNING_SCHEMA = {
|
|
|
590
591
|
};
|
|
591
592
|
|
|
592
593
|
// ---------------------------------------------------------------------------
|
|
593
|
-
// delivery.* — /deliver + story-deliver consume. The full block of
|
|
594
|
+
// delivery.* — /mandrel-deliver + story-deliver consume. The full block of
|
|
594
595
|
// per-key sub-schemas lives in `config-settings-schema-delivery.js` (refs
|
|
595
596
|
// #3457); DELIVERY_SCHEMA is imported above and referenced unchanged below.
|
|
596
597
|
// ---------------------------------------------------------------------------
|
|
@@ -861,10 +862,49 @@ export const AGENTRC_SCHEMA = {
|
|
|
861
862
|
};
|
|
862
863
|
|
|
863
864
|
let _agentrcValidator = null;
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Compile `AGENTRC_SCHEMA` with a live AJV instance.
|
|
868
|
+
*
|
|
869
|
+
* `ajv` is pulled in through `createRequire` rather than a top-level
|
|
870
|
+
* `import` so the module never enters the graph on the fast path
|
|
871
|
+
* (Story #5109). Loading AJV was itself a measurable share of the ~35 ms
|
|
872
|
+
* this function used to cost in each of the 36 config-touching entry
|
|
873
|
+
* scripts, and the precompiled validator needs none of it.
|
|
874
|
+
*
|
|
875
|
+
* @returns {import('ajv').ValidateFunction}
|
|
876
|
+
*/
|
|
877
|
+
function compileAgentrcValidatorDynamically() {
|
|
878
|
+
const require = createRequire(import.meta.url);
|
|
879
|
+
const ajvModule = require('ajv');
|
|
880
|
+
const Ajv = ajvModule.default ?? ajvModule;
|
|
881
|
+
const ajv = new Ajv({ allErrors: true });
|
|
882
|
+
return ajv.compile(AGENTRC_SCHEMA);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* The `.agentrc.json` validator.
|
|
887
|
+
*
|
|
888
|
+
* Returns the **precompiled** validator committed at
|
|
889
|
+
* `lib/generated/agentrc-validator.js` — AJV's standalone emit for the exact
|
|
890
|
+
* `AGENTRC_SCHEMA` literal above, kept in step by
|
|
891
|
+
* `check-generated-validator.js --check` inside `npm run lint`. The verdicts
|
|
892
|
+
* are AJV's own, produced by AJV's own generated code with the same
|
|
893
|
+
* `allErrors: true` option the dynamic path uses, so nothing about what a
|
|
894
|
+
* config is allowed to contain changes — only when the codegen runs.
|
|
895
|
+
*
|
|
896
|
+
* Set `MANDREL_AGENTRC_VALIDATOR=dynamic` to compile at runtime instead. That
|
|
897
|
+
* escape hatch exists so a consumer who has hand-edited the schema (or hit a
|
|
898
|
+
* platform where the generated module will not load) is never stuck with a
|
|
899
|
+
* validator they cannot regenerate; it costs the ~35 ms the artifact removes.
|
|
900
|
+
*
|
|
901
|
+
* @returns {import('ajv').ValidateFunction}
|
|
902
|
+
*/
|
|
864
903
|
export function getAgentrcValidator() {
|
|
865
|
-
if (
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
904
|
+
if (_agentrcValidator) return _agentrcValidator;
|
|
905
|
+
_agentrcValidator =
|
|
906
|
+
process.env.MANDREL_AGENTRC_VALIDATOR === 'dynamic'
|
|
907
|
+
? compileAgentrcValidatorDynamically()
|
|
908
|
+
: compiledAgentrcValidator;
|
|
869
909
|
return _agentrcValidator;
|
|
870
910
|
}
|
|
@@ -22,7 +22,7 @@ import path from 'node:path';
|
|
|
22
22
|
import picomatch from 'picomatch';
|
|
23
23
|
import { write, writeFile } from './baselines/writer.js';
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
const COVERAGE_FINAL_PATH = 'coverage/coverage-final.json';
|
|
26
26
|
export const COVERAGE_BASELINE_PATH = 'baselines/coverage.json';
|
|
27
27
|
// Absolute floating-point tolerance (percentage points). Values in the
|
|
28
28
|
// baseline are stored to two decimals, so anything below 0.01 is noise.
|
|
@@ -32,7 +32,7 @@ export const COVERAGE_TOLERANCE = 0.01;
|
|
|
32
32
|
// branch flipping covered↔uncovered between runs is the natural noise floor
|
|
33
33
|
// under non-deterministic Windows/Node 22 V8 instrumentation. We absorb up
|
|
34
34
|
// to one event of slack per axis. Anything beyond one event is real signal.
|
|
35
|
-
|
|
35
|
+
const NOISE_EVENT_HEADROOM = 1.0;
|
|
36
36
|
|
|
37
37
|
function toForwardSlash(p) {
|
|
38
38
|
return p.replace(/\\/g, '/');
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* lib/cpu-pool.js — Generic worker_threads pool for CPU-bound work.
|
|
3
3
|
*
|
|
4
4
|
* `runOnPool(workerScript, items, opts)` spawns up to
|
|
5
|
-
* `opts.concurrency
|
|
5
|
+
* `resolvePoolConcurrency(opts.concurrency)` persistent workers — an
|
|
6
|
+
* explicit option, else `MANDREL_POOL_CONCURRENCY`, else a clamp of 4 under
|
|
7
|
+
* `node:test`, else `os.availableParallelism()` —
|
|
6
8
|
* dispatches `items` to whichever worker is idle, and resolves with an
|
|
7
9
|
* array of per-item results in input order. The queue is bounded by
|
|
8
10
|
* worker count — we only have N items in flight at once, where N =
|
|
@@ -58,15 +60,93 @@ const defaultWorkerFactory = (script, options) => new Worker(script, options);
|
|
|
58
60
|
* Pool-vs-serial cutover for `runOnPool` callers.
|
|
59
61
|
*
|
|
60
62
|
* Below this batch size the pool's worker spawn overhead dominates, so
|
|
61
|
-
* callers fall back to in-process serial scoring.
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
63
|
+
* callers fall back to in-process serial scoring. Single-sourced here so the
|
|
64
|
+
* maintainability baseline scan (`maintainability-utils.js`), the CRAP
|
|
65
|
+
* scanner (`crap-utils.js`), and the native review provider
|
|
66
|
+
* (`review-providers/native.js`) cannot silently desynchronize on a retune.
|
|
67
|
+
*
|
|
68
|
+
* **Retuned from 8 to 256 in Story #5109.** The original 8 was set against
|
|
69
|
+
* tmpdir fixtures and never measured against a real batch, so every
|
|
70
|
+
* pre-commit preview — the most frequent path in the framework — paid for a
|
|
71
|
+
* pool it could not amortise. Measured on this repository (18 logical cores,
|
|
72
|
+
* `.agents/scripts` + `bin` + `lib`, MI scoring):
|
|
73
|
+
*
|
|
74
|
+
* ```text
|
|
75
|
+
* n=16 serial 125 ms pooled 444 ms
|
|
76
|
+
* n=64 serial 175 ms pooled 535 ms
|
|
77
|
+
* n=128 serial 241 ms pooled 422 ms
|
|
78
|
+
* n=256 serial 440 ms pooled 707 ms <- still serial-favourable
|
|
79
|
+
* n=384 serial 943 ms pooled 782 ms <- crossover
|
|
80
|
+
* n=619 serial 1054 ms pooled 710 ms
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* The crossover sits between 256 and 384, so 256 is the last power-of-two
|
|
84
|
+
* step the serial path provably wins. A whole-tree baseline refresh
|
|
85
|
+
* (n≈619) still takes the pool; a diff-scoped preview (n≈50) no longer
|
|
86
|
+
* spawns a worker at all. Scoring output is identical on either path — the
|
|
87
|
+
* worker runs the same scorer — so this is a cost retune, never a verdict
|
|
88
|
+
* change.
|
|
89
|
+
*/
|
|
90
|
+
export const POOL_SERIAL_THRESHOLD = 256;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Hard ceiling on pool width when the process is a `node:test` child.
|
|
94
|
+
*
|
|
95
|
+
* Node's test runner already fans test *files* out across processes, so a
|
|
96
|
+
* pool inside one of them multiplies against that fan-out and can oversubscribe
|
|
97
|
+
* the host by an order of magnitude. Four is wide enough to keep the pooled
|
|
98
|
+
* code path genuinely concurrent (so the scheduling branches under test stay
|
|
99
|
+
* exercised) without letting a suite of parallel test processes each claim
|
|
100
|
+
* every core.
|
|
101
|
+
*/
|
|
102
|
+
const NODE_TEST_CONCURRENCY_CLAMP = 4;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* True when this process was spawned by Node's own test runner, which sets
|
|
106
|
+
* `NODE_TEST_CONTEXT` in every test child.
|
|
107
|
+
*
|
|
108
|
+
* @param {NodeJS.ProcessEnv} env
|
|
109
|
+
* @returns {boolean}
|
|
110
|
+
*/
|
|
111
|
+
function inNodeTestContext(env) {
|
|
112
|
+
return (
|
|
113
|
+
typeof env.NODE_TEST_CONTEXT === 'string' && env.NODE_TEST_CONTEXT !== ''
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Resolve the requested pool width, in strict precedence order:
|
|
119
|
+
*
|
|
120
|
+
* 1. an explicit `opts.concurrency` — the caller knows its own budget;
|
|
121
|
+
* 2. `MANDREL_POOL_CONCURRENCY` — the operator/CI override, so a
|
|
122
|
+
* constrained runner can bound every pool in the process tree at once;
|
|
123
|
+
* 3. `NODE_TEST_CONCURRENCY_CLAMP` when running under `node:test`;
|
|
124
|
+
* 4. `os.availableParallelism()`.
|
|
125
|
+
*
|
|
126
|
+
* Only *finite, positive* values are honoured at each step; anything else
|
|
127
|
+
* falls through to the next, so a typo in the env var degrades to the
|
|
128
|
+
* default rather than collapsing the pool to a single worker.
|
|
129
|
+
*
|
|
130
|
+
* Module-private on purpose: `runOnPool` is the only caller, and the
|
|
131
|
+
* precedence is pinned through its observable worker count rather than by
|
|
132
|
+
* an export whose sole importer would be a test.
|
|
133
|
+
*
|
|
134
|
+
* @param {number|undefined} explicit
|
|
135
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
136
|
+
* @returns {number}
|
|
68
137
|
*/
|
|
69
|
-
|
|
138
|
+
function resolvePoolConcurrency(explicit, env = process.env) {
|
|
139
|
+
const positive = (value) => {
|
|
140
|
+
const n = Number(value);
|
|
141
|
+
return Number.isFinite(n) && n >= 1 ? Math.floor(n) : null;
|
|
142
|
+
};
|
|
143
|
+
return (
|
|
144
|
+
positive(explicit) ??
|
|
145
|
+
positive(env.MANDREL_POOL_CONCURRENCY) ??
|
|
146
|
+
(inNodeTestContext(env) ? NODE_TEST_CONCURRENCY_CLAMP : null) ??
|
|
147
|
+
os.availableParallelism()
|
|
148
|
+
);
|
|
149
|
+
}
|
|
70
150
|
|
|
71
151
|
/**
|
|
72
152
|
* @template TItem, TResult
|
|
@@ -90,7 +170,7 @@ export async function runOnPool(workerScript, items, opts = {}) {
|
|
|
90
170
|
const itemsArr = [...items];
|
|
91
171
|
if (itemsArr.length === 0) return [];
|
|
92
172
|
|
|
93
|
-
const requested = opts.concurrency
|
|
173
|
+
const requested = resolvePoolConcurrency(opts.concurrency);
|
|
94
174
|
const concurrency = Math.max(1, Math.min(requested, itemsArr.length));
|
|
95
175
|
const workerData = opts.workerData;
|
|
96
176
|
const throwOnItemError = opts.throwOnItemError === true;
|
|
@@ -21,7 +21,10 @@ import {
|
|
|
21
21
|
const CRAP_WORKER_URL = new URL('./workers/crap-worker.js', import.meta.url);
|
|
22
22
|
|
|
23
23
|
// Pool-vs-serial cutover — single-sourced in cpu-pool.js (see the
|
|
24
|
-
// POOL_SERIAL_THRESHOLD docstring for the tuning rationale).
|
|
24
|
+
// POOL_SERIAL_THRESHOLD docstring for the tuning rationale). Callers may
|
|
25
|
+
// override it per scan via `serialThreshold`, which is how the parity tests
|
|
26
|
+
// drive the pooled path without materialising a batch bigger than the
|
|
27
|
+
// cutover (Story #5109 raised it from 8 to 256).
|
|
25
28
|
const SERIAL_THRESHOLD = POOL_SERIAL_THRESHOLD;
|
|
26
29
|
// 1.1.0 — TypeScript support landed in 5.29.0. Bumped from 1.0.0 because
|
|
27
30
|
// the scanner now emits CRAP rows for TS/TSX paths that the previous
|
|
@@ -393,6 +396,7 @@ export async function scanAndScore({
|
|
|
393
396
|
ignoreGlobs = [],
|
|
394
397
|
preScannedFiles = null,
|
|
395
398
|
incremental = null,
|
|
399
|
+
serialThreshold = SERIAL_THRESHOLD,
|
|
396
400
|
}) {
|
|
397
401
|
if (!Array.isArray(targetDirs)) {
|
|
398
402
|
throw new TypeError('scanAndScore: targetDirs must be an array');
|
|
@@ -430,7 +434,7 @@ export async function scanAndScore({
|
|
|
430
434
|
// Serial below the pool cutover, and ALWAYS in incremental mode: the
|
|
431
435
|
// per-file baseline lookup Maps the join needs do not cross the worker
|
|
432
436
|
// boundary (Story #4981).
|
|
433
|
-
const runSerial = queue.length <
|
|
437
|
+
const runSerial = queue.length < serialThreshold || Boolean(incremental);
|
|
434
438
|
const perFile = runSerial
|
|
435
439
|
? queue.map((item) => ({ item, result: scoreFileSerial(item, coverage) }))
|
|
436
440
|
: await scoreFilesViaPool(queue, coverage);
|
|
@@ -30,8 +30,9 @@
|
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
32
|
import path from 'node:path';
|
|
33
|
+
import { selectFilesToScore } from './cyclomatic-scope.js';
|
|
33
34
|
import { calculateReportForFile } from './maintainability-engine.js';
|
|
34
|
-
import {
|
|
35
|
+
import { scanDirectory } from './maintainability-utils.js';
|
|
35
36
|
|
|
36
37
|
/** Default location of the committed breach baseline. */
|
|
37
38
|
export const DEFAULT_CYCLOMATIC_BASELINE = 'baselines/cyclomatic.json';
|
|
@@ -96,20 +97,37 @@ function breachRowFor(file, methods, ceiling) {
|
|
|
96
97
|
* `scoreFile` is a seam so tests can drive the reduction without the kernel;
|
|
97
98
|
* production callers omit it.
|
|
98
99
|
*
|
|
100
|
+
* **Scoped scoring (Story #5109).** The directory walk is cheap — a few
|
|
101
|
+
* `readdir` calls — but scoring is an escomplex parse per file, and re-parsing
|
|
102
|
+
* the whole tree on every CI run cost 0.94 s to re-derive rows that could not
|
|
103
|
+
* have moved. `scopeFiles`, when supplied, is the set of repo-relative POSIX
|
|
104
|
+
* paths that are still *scored*; everything else is walked, counted, and
|
|
105
|
+
* skipped. That is sound precisely because this gate is a ratchet: a verdict
|
|
106
|
+
* exists only for a file that either changed (so it must be re-scored) or is
|
|
107
|
+
* already recorded in the baseline (so its recorded count must be re-derived
|
|
108
|
+
* to detect an improvement or a removal). A file that is neither cannot
|
|
109
|
+
* produce `added` or `worsened`, and `removed` is computed from baseline files
|
|
110
|
+
* — all of which are in scope by construction. `scannedFiles` therefore keeps
|
|
111
|
+
* reporting the **whole walk**, so the reported scan surface does not shrink
|
|
112
|
+
* just because the scoring did. Callers pass `null` (the default) for the
|
|
113
|
+
* whole-tree scan, which `--update` and `BASELINE_SCOPE=full` always do.
|
|
114
|
+
*
|
|
99
115
|
* @param {{
|
|
100
116
|
* targetDirs: string[],
|
|
101
117
|
* ignoreGlobs?: string[],
|
|
102
118
|
* ceiling: number,
|
|
103
119
|
* cwd?: string,
|
|
120
|
+
* scopeFiles?: Set<string> | null,
|
|
104
121
|
* scoreFile?: (absPath: string) => { methods?: Array<{ cyclomatic?: number }>, parseError?: boolean },
|
|
105
122
|
* }} args
|
|
106
|
-
* @returns {{ rows: Array<object>, scannedFiles: number, parseErrors: number }}
|
|
123
|
+
* @returns {{ rows: Array<object>, scannedFiles: number, scoredFiles: number, parseErrors: number }}
|
|
107
124
|
*/
|
|
108
125
|
export function scanCyclomatic({
|
|
109
126
|
targetDirs,
|
|
110
127
|
ignoreGlobs = [],
|
|
111
128
|
ceiling,
|
|
112
129
|
cwd = process.cwd(),
|
|
130
|
+
scopeFiles = null,
|
|
113
131
|
scoreFile = calculateReportForFile,
|
|
114
132
|
}) {
|
|
115
133
|
const files = [];
|
|
@@ -118,21 +136,25 @@ export function scanCyclomatic({
|
|
|
118
136
|
scanDirectory(abs, files, { cwd, ignoreGlobs });
|
|
119
137
|
}
|
|
120
138
|
files.sort();
|
|
139
|
+
const selected = selectFilesToScore(files, { cwd, ignoreGlobs, scopeFiles });
|
|
121
140
|
const rows = [];
|
|
122
141
|
let parseErrors = 0;
|
|
123
|
-
for (const abs of
|
|
124
|
-
if (isIgnoredByGlobs(abs, ignoreGlobs, cwd)) continue;
|
|
142
|
+
for (const { abs, rel } of selected) {
|
|
125
143
|
const report = scoreFile(abs);
|
|
126
144
|
if (!report || report.parseError) {
|
|
127
145
|
parseErrors += 1;
|
|
128
146
|
continue;
|
|
129
147
|
}
|
|
130
|
-
const rel = path.relative(cwd, abs).split(path.sep).join('/');
|
|
131
148
|
const row = breachRowFor(rel, report.methods, ceiling);
|
|
132
149
|
if (row) rows.push(row);
|
|
133
150
|
}
|
|
134
151
|
rows.sort((a, b) => a.file.localeCompare(b.file));
|
|
135
|
-
return {
|
|
152
|
+
return {
|
|
153
|
+
rows,
|
|
154
|
+
scannedFiles: files.length,
|
|
155
|
+
scoredFiles: selected.length,
|
|
156
|
+
parseErrors,
|
|
157
|
+
};
|
|
136
158
|
}
|
|
137
159
|
|
|
138
160
|
/**
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cyclomatic-scope.js — which files the cyclomatic ratchet still has to score
|
|
3
|
+
* (Story #5109).
|
|
4
|
+
*
|
|
5
|
+
* `check-cyclomatic.js` re-parsed the whole tree on every run — 0.94 s of
|
|
6
|
+
* escomplex work to re-derive rows that could not have moved. The scan surface
|
|
7
|
+
* is unchanged; only the *scoring* is narrowed, and it is narrowed to exactly
|
|
8
|
+
* the two sets a ratchet verdict can come from:
|
|
9
|
+
*
|
|
10
|
+
* - **every file already recorded in the baseline**, because `improved`,
|
|
11
|
+
* `worsened` and `removed` are all computed by re-deriving a recorded
|
|
12
|
+
* file's current count;
|
|
13
|
+
* - **everything this branch changed** against `project.baseBranch`,
|
|
14
|
+
* including the staged, unstaged and untracked working tree, because
|
|
15
|
+
* `added` is what a change introduces — and a pre-push gate that only
|
|
16
|
+
* looked at committed work would wave through the breach an operator is
|
|
17
|
+
* about to commit.
|
|
18
|
+
*
|
|
19
|
+
* A file in neither set is unchanged and unrecorded: it cannot produce a
|
|
20
|
+
* verdict in any bucket.
|
|
21
|
+
*
|
|
22
|
+
* @module lib/cyclomatic-scope
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import { createGitInterface } from './git-utils.js';
|
|
27
|
+
import { isIgnoredByGlobs } from './maintainability-utils.js';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Git probes whose union is "everything this working copy has touched".
|
|
31
|
+
*
|
|
32
|
+
* Three, not four: `git diff --name-only HEAD` already reports the staged
|
|
33
|
+
* *and* unstaged deltas, so a separate `--cached` pass would only re-list
|
|
34
|
+
* paths this one has, at the cost of another process spawn.
|
|
35
|
+
*
|
|
36
|
+
* @type {ReadonlyArray<ReadonlyArray<string>>}
|
|
37
|
+
*/
|
|
38
|
+
const CHANGE_PROBES = Object.freeze([
|
|
39
|
+
Object.freeze(['diff', '--name-only', '__BASE__...HEAD']),
|
|
40
|
+
Object.freeze(['diff', '--name-only', 'HEAD']),
|
|
41
|
+
Object.freeze(['ls-files', '--others', '--exclude-standard']),
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the set of repo-relative POSIX paths the ratchet still needs to
|
|
46
|
+
* score, or `null` when it must scan the whole tree.
|
|
47
|
+
*
|
|
48
|
+
* **Fails open, never closed.** Any git failure — a shallow clone, a missing
|
|
49
|
+
* base ref, a fixture directory that is not a repository at all — returns
|
|
50
|
+
* `null`, and the caller scans everything. Narrowing on an unreadable scope
|
|
51
|
+
* is how a ratchet silently stops ratcheting; paying for a whole-tree scan is
|
|
52
|
+
* not.
|
|
53
|
+
*
|
|
54
|
+
* @param {{
|
|
55
|
+
* cwd: string,
|
|
56
|
+
* baseRef: string,
|
|
57
|
+
* baselineRows?: Array<{ file?: string }>,
|
|
58
|
+
* git?: ReturnType<typeof createGitInterface>,
|
|
59
|
+
* }} args
|
|
60
|
+
* Module-private: `resolveScanScope` below is the only caller, and it is the
|
|
61
|
+
* one the CLI actually asks. Exporting both would ship a seam whose only
|
|
62
|
+
* importer is a test, which the `--production` dead-export pass discounts.
|
|
63
|
+
*
|
|
64
|
+
* @returns {Set<string> | null}
|
|
65
|
+
*/
|
|
66
|
+
function resolveCyclomaticScope({ cwd, baseRef, baselineRows = [], git }) {
|
|
67
|
+
const gitIface = git ?? createGitInterface({});
|
|
68
|
+
const scope = new Set();
|
|
69
|
+
for (const row of baselineRows) {
|
|
70
|
+
if (typeof row?.file === 'string' && row.file) scope.add(row.file);
|
|
71
|
+
}
|
|
72
|
+
for (const probe of CHANGE_PROBES) {
|
|
73
|
+
const argv = probe.map((arg) =>
|
|
74
|
+
arg === '__BASE__...HEAD' ? `${baseRef}...HEAD` : arg,
|
|
75
|
+
);
|
|
76
|
+
const res = gitIface.gitSpawn(cwd, ...argv);
|
|
77
|
+
if (res?.status !== 0) return null;
|
|
78
|
+
for (const line of String(res.stdout ?? '').split('\n')) {
|
|
79
|
+
const file = line.trim().replace(/\\/g, '/');
|
|
80
|
+
if (file) scope.add(file);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return scope;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Decide the scan scope for one `check-cyclomatic` invocation.
|
|
88
|
+
*
|
|
89
|
+
* `--update` rewrites the baseline from the current tree, and
|
|
90
|
+
* `BASELINE_SCOPE=full` is the operator's explicit "re-derive everything" —
|
|
91
|
+
* both must see the whole tree, so both return `null`.
|
|
92
|
+
*
|
|
93
|
+
* @param {{
|
|
94
|
+
* cwd: string,
|
|
95
|
+
* config?: { project?: { baseBranch?: string } } | null,
|
|
96
|
+
* update?: boolean,
|
|
97
|
+
* env?: NodeJS.ProcessEnv,
|
|
98
|
+
* baselineRows?: Array<{ file?: string }>,
|
|
99
|
+
* git?: ReturnType<typeof createGitInterface>,
|
|
100
|
+
* }} args
|
|
101
|
+
* @returns {Set<string> | null} `null` means "scan everything".
|
|
102
|
+
*/
|
|
103
|
+
export function resolveScanScope({
|
|
104
|
+
cwd,
|
|
105
|
+
config = null,
|
|
106
|
+
update = false,
|
|
107
|
+
env = process.env,
|
|
108
|
+
baselineRows = [],
|
|
109
|
+
git,
|
|
110
|
+
}) {
|
|
111
|
+
if (update || env?.BASELINE_SCOPE === 'full') return null;
|
|
112
|
+
return resolveCyclomaticScope({
|
|
113
|
+
cwd,
|
|
114
|
+
baseRef: config?.project?.baseBranch ?? 'main',
|
|
115
|
+
baselineRows,
|
|
116
|
+
git,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Reduce a walked file list to the `{ abs, rel }` pairs that will actually be
|
|
122
|
+
* scored, applying the gate's `ignoreGlobs` and then the diff scope.
|
|
123
|
+
*
|
|
124
|
+
* Kept here rather than inside `scanCyclomatic` so "which files count" lives
|
|
125
|
+
* in one module: the ignore rule and the scope rule are the same decision
|
|
126
|
+
* asked twice, and splitting them across two files is how they drift.
|
|
127
|
+
*
|
|
128
|
+
* @param {string[]} files Absolute paths, as walked.
|
|
129
|
+
* @param {{ cwd: string, ignoreGlobs?: string[], scopeFiles?: Set<string> | null }} args
|
|
130
|
+
* @returns {Array<{ abs: string, rel: string }>}
|
|
131
|
+
*/
|
|
132
|
+
export function selectFilesToScore(
|
|
133
|
+
files,
|
|
134
|
+
{ cwd, ignoreGlobs = [], scopeFiles = null },
|
|
135
|
+
) {
|
|
136
|
+
const selected = [];
|
|
137
|
+
for (const abs of files) {
|
|
138
|
+
if (isIgnoredByGlobs(abs, ignoreGlobs, cwd)) continue;
|
|
139
|
+
const rel = path.relative(cwd, abs).split(path.sep).join('/');
|
|
140
|
+
if (scopeFiles && !scopeFiles.has(rel)) continue;
|
|
141
|
+
selected.push({ abs, rel });
|
|
142
|
+
}
|
|
143
|
+
return selected;
|
|
144
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dependency-version.js — read an installed package's version without
|
|
3
|
+
* evaluating the package (Story #5109).
|
|
4
|
+
*
|
|
5
|
+
* Baselines stamp the versions of the tools that produced them so a consumer
|
|
6
|
+
* can detect scorer drift. The obvious way to get one — `require(pkg).version`
|
|
7
|
+
* — pays the package's whole module-initialisation cost for a string that is
|
|
8
|
+
* sitting in a manifest a few kilobytes long. For `typescript` that was ~65 ms
|
|
9
|
+
* of CPU and ~75 MB of RSS on every `check-baselines --gate crap` and every
|
|
10
|
+
* `quality-preview` run, in a repository with no TypeScript in it at all.
|
|
11
|
+
*
|
|
12
|
+
* `require.resolve` walks the resolver without reading or executing the
|
|
13
|
+
* target, so everything here is path arithmetic plus one small `JSON.parse`.
|
|
14
|
+
*
|
|
15
|
+
* This deliberately does **not** replace `crap-utils.js#resolveEscomplexVersion`,
|
|
16
|
+
* which answers a different question: it walks `node_modules` upward from a
|
|
17
|
+
* caller-supplied `cwd` rather than resolving from this module's own location,
|
|
18
|
+
* because the escomplex stamp must describe the tree being scanned.
|
|
19
|
+
*
|
|
20
|
+
* @module lib/dependency-version
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import fs from 'node:fs';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Locate a package's manifest via the module resolver, without loading it.
|
|
28
|
+
*
|
|
29
|
+
* The `<pkg>/package.json` subpath is tried first because it is a single
|
|
30
|
+
* resolver call. A package whose `exports` map withholds `./package.json`
|
|
31
|
+
* falls back to resolving the main entry and walking up to the nearest
|
|
32
|
+
* `package.json`, which is still evaluation-free.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} name Package name, e.g. `'typescript'`.
|
|
35
|
+
* @param {NodeJS.Require} requireFn A `createRequire`-produced require whose
|
|
36
|
+
* resolution base is the calling module.
|
|
37
|
+
* @returns {string | null} Absolute manifest path, or `null` when unresolvable.
|
|
38
|
+
*/
|
|
39
|
+
function resolveManifestPath(name, requireFn) {
|
|
40
|
+
try {
|
|
41
|
+
return requireFn.resolve(`${name}/package.json`);
|
|
42
|
+
} catch {
|
|
43
|
+
// fall through to the main-entry walk
|
|
44
|
+
}
|
|
45
|
+
let dir;
|
|
46
|
+
try {
|
|
47
|
+
dir = path.dirname(requireFn.resolve(name));
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const { root } = path.parse(dir);
|
|
52
|
+
while (true) {
|
|
53
|
+
const candidate = path.join(dir, 'package.json');
|
|
54
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
55
|
+
if (dir === root) return null;
|
|
56
|
+
const parent = path.dirname(dir);
|
|
57
|
+
if (parent === dir) return null;
|
|
58
|
+
dir = parent;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Resolve an installed package's declared version.
|
|
64
|
+
*
|
|
65
|
+
* Returns `null` — never a guess and never a throw — when the package cannot
|
|
66
|
+
* be resolved, its manifest cannot be read or parsed, or it declares no
|
|
67
|
+
* usable `version`. Callers map that onto their own sentinel (the baseline
|
|
68
|
+
* writers use `'0.0.0'`, meaning "unknown environment").
|
|
69
|
+
*
|
|
70
|
+
* @param {string} name Package name.
|
|
71
|
+
* @param {NodeJS.Require} requireFn A `createRequire`-produced require.
|
|
72
|
+
* @returns {string | null}
|
|
73
|
+
*/
|
|
74
|
+
export function resolveDependencyVersion(name, requireFn) {
|
|
75
|
+
const manifest = resolveManifestPath(name, requireFn);
|
|
76
|
+
if (!manifest) return null;
|
|
77
|
+
try {
|
|
78
|
+
const parsed = JSON.parse(fs.readFileSync(manifest, 'utf-8'));
|
|
79
|
+
if (parsed && typeof parsed.version === 'string' && parsed.version) {
|
|
80
|
+
return parsed.version;
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
// unreadable / unparseable manifest
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* duplicate-search.js — Cross-Story Duplicate Detection
|
|
3
3
|
*
|
|
4
|
-
* Used by `/plan` to surface open Stories whose scope overlaps with a
|
|
4
|
+
* Used by `/mandrel-plan` to surface open Stories whose scope overlaps with a
|
|
5
5
|
* seed / seed-file / tickets corpus before new Stories are created.
|
|
6
6
|
* Returns ranked candidates with an overlap score and URL so the host
|
|
7
7
|
* LLM can pause for HITL confirmation.
|
|
@@ -207,7 +207,7 @@ function normalizeIssue(issue) {
|
|
|
207
207
|
* }} args
|
|
208
208
|
* @returns {Array<{ id: number, title: string, score: number, url: string }>}
|
|
209
209
|
*/
|
|
210
|
-
|
|
210
|
+
function rankOpenStoryDuplicates({
|
|
211
211
|
seed,
|
|
212
212
|
openStories,
|
|
213
213
|
minScore = DEFAULT_MIN_SCORE,
|