mandrel 2.31.0 → 2.33.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 +12 -9
- package/.agents/docs/agentrc-reference.json +63 -58
- package/.agents/docs/configuration.md +275 -227
- package/.agents/docs/execution-reference.md +13 -14
- package/.agents/docs/quality-gates.md +195 -23
- package/.agents/instructions.md +2 -5
- package/.agents/rules/git-conventions-reference.md +27 -27
- package/.agents/rules/git-conventions.md +4 -2
- package/.agents/rules/known-tooling-behavior.md +66 -30
- package/.agents/rules/testing-standards.md +35 -71
- package/.agents/runtime-deps.json +2 -2
- package/.agents/schemas/agentrc.schema.json +1955 -1410
- 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 +16 -29
- package/.agents/scripts/apply-quality-bootstrap.js +27 -34
- package/.agents/scripts/audit-to-stories.js +160 -41
- 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 +159 -0
- package/.agents/scripts/check-lifecycle-lint.js +108 -124
- 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 +81 -35
- package/.agents/scripts/lib/audit-to-stories/wire-dependencies.js +185 -0
- 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/runners.js +38 -16
- package/.agents/scripts/lib/config/temp-paths.js +6 -5
- package/.agents/scripts/lib/config-settings-schema-delivery.js +245 -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/dependency-parser.js +20 -7
- 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/findings/provenance-field.js +135 -0
- package/.agents/scripts/lib/findings/route-finding.js +57 -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-config-resolver.js +181 -0
- package/.agents/scripts/lib/knip-entry-sync.js +508 -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 +7 -26
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +69 -5
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +69 -13
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +49 -0
- package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
- package/.agents/scripts/lib/orchestration/resolve-stories.js +77 -50
- package/.agents/scripts/lib/orchestration/review-providers/codex.js +1 -1
- package/.agents/scripts/lib/orchestration/review-providers/mi-exemptions.js +130 -0
- package/.agents/scripts/lib/orchestration/review-providers/native.js +30 -16
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +37 -26
- package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +376 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +161 -151
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +15 -3
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +10 -15
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +5 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +157 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +0 -14
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +59 -25
- package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +20 -31
- package/.agents/scripts/lib/orchestration/spec-spill.js +17 -3
- package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +7 -6
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +2 -1
- package/.agents/scripts/lib/orchestration/task-body-validator.js +4 -1
- package/.agents/scripts/lib/orchestration/ticket-lease.js +28 -127
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +116 -1
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -1
- package/.agents/scripts/lib/orchestration/ticket-validator.js +38 -0
- 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/footer-block.js +97 -0
- package/.agents/scripts/lib/story-body/story-body.js +18 -81
- 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/wave-runner/footprint.js +306 -0
- package/.agents/scripts/lib/wave-runner/ready-set.js +198 -181
- 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/blocked-by-add.js +25 -10
- 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 +21 -7
- 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/stories-wave-tick.js +192 -9
- package/.agents/scripts/update-dead-exports-baseline.js +321 -0
- package/.agents/skills/core/gates-and-baselines/SKILL.md +2 -2
- package/.agents/skills/skills.index.json +1 -11
- package/.agents/workflows/audit-documentation.md +5 -6
- package/.agents/workflows/audit-to-stories.md +28 -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 +30 -2
- package/.agents/workflows/helpers/deliver-story-reference.md +84 -16
- package/.agents/workflows/helpers/plan-reference.md +76 -0
- 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 +58 -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 +14 -5
- 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
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
// .agents/scripts/lib/baselines/scope-inventory.js
|
|
2
|
+
//
|
|
3
|
+
// Story #5012 — the ONE in-scope file inventory the honesty surface reads.
|
|
4
|
+
//
|
|
5
|
+
// A ratchet gate can be green while measuring almost nothing: nothing checks
|
|
6
|
+
// that a committed baseline's row set still describes the tree it claims to
|
|
7
|
+
// describe. Both instruments that now do — `check-baseline-scope.js` (assert)
|
|
8
|
+
// and `prune-baseline-orphans.js` (remedy) — resolve "which files is this kind
|
|
9
|
+
// supposed to have rows for?" through this module, so the gate and the pruner
|
|
10
|
+
// cannot disagree about scope and start fighting each other.
|
|
11
|
+
//
|
|
12
|
+
// ## Recompute from the gate's own config, never from a second walker
|
|
13
|
+
//
|
|
14
|
+
// The inventory is derived from **the same configuration the kind's refresh
|
|
15
|
+
// scorer reads**, through the same helpers:
|
|
16
|
+
//
|
|
17
|
+
// - `coverage` — `.c8rc.cjs` `include` / `exclude`, applied with
|
|
18
|
+
// `coverage-baseline.js#buildScopePredicate`, exactly as
|
|
19
|
+
// `refresh-service.js#buildDefaultCoverageScorer` does.
|
|
20
|
+
// - every other file-keyed kind — `delivery.quality.gates.<kind>.targetDirs`
|
|
21
|
+
// / `.ignoreGlobs`, walked with `maintainability-utils.js#scanDirectory`,
|
|
22
|
+
// exactly as `refresh-service.js#buildDefaultMaintainabilityScorer` does.
|
|
23
|
+
//
|
|
24
|
+
// A second walker written here would report the two implementations
|
|
25
|
+
// disagreeing as divergence — the same failure mode
|
|
26
|
+
// `check-baseline-drift.js` avoids by re-scoring through
|
|
27
|
+
// `resolveDefaultScorer` rather than a private scorer.
|
|
28
|
+
//
|
|
29
|
+
// ## Measurement-free by contract
|
|
30
|
+
//
|
|
31
|
+
// Nothing in this module runs a scorer, reads `coverage/coverage-final.json`,
|
|
32
|
+
// or needs a test run to have happened. It walks directories and matches
|
|
33
|
+
// globs. That is what makes the gate runnable on any checkout, which is in
|
|
34
|
+
// turn what makes hard-failing on a stale row defensible.
|
|
35
|
+
|
|
36
|
+
import { createRequire } from 'node:module';
|
|
37
|
+
import path from 'node:path';
|
|
38
|
+
|
|
39
|
+
import { buildScopePredicate } from '../coverage-baseline.js';
|
|
40
|
+
import { scanDirectory } from '../maintainability-utils.js';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Per-kind classification of which divergence directions may be asserted.
|
|
44
|
+
*
|
|
45
|
+
* **Only a kind whose producer emits one row per in-scope file may assert the
|
|
46
|
+
* MISSING direction.** This is not a conservatism knob — it is arithmetic.
|
|
47
|
+
* Measured against this repository on 2026-08-05, asserting MISSING densely
|
|
48
|
+
* yields 128 phantom rows for `crap` (rows are per-method and coverage-gated,
|
|
49
|
+
* so a file with no scorable method legitimately has none) and 510 for
|
|
50
|
+
* `duplication` (a row exists only where a clone was found). A gate that
|
|
51
|
+
* reports 638 phantom findings on a healthy tree is a gate nobody reads.
|
|
52
|
+
*
|
|
53
|
+
* `lint` and `mutation` are sparse for the same reason — a clean file has no
|
|
54
|
+
* lint row, an untested file has no mutation row. `lighthouse` (keyed on
|
|
55
|
+
* `route`) and `bundle-size` (keyed on `bundle`) are not file-keyed at all, so
|
|
56
|
+
* neither direction is meaningful: their keys name artefacts, not paths on
|
|
57
|
+
* disk, and a directory walk can say nothing about them.
|
|
58
|
+
*
|
|
59
|
+
* @type {Readonly<Record<string, { keyField: string, directions: readonly string[] }>>}
|
|
60
|
+
*/
|
|
61
|
+
export const KIND_SCOPE_POLICY = Object.freeze({
|
|
62
|
+
coverage: Object.freeze({
|
|
63
|
+
keyField: 'path',
|
|
64
|
+
directions: Object.freeze(['missing', 'extra']),
|
|
65
|
+
}),
|
|
66
|
+
maintainability: Object.freeze({
|
|
67
|
+
keyField: 'path',
|
|
68
|
+
directions: Object.freeze(['missing', 'extra']),
|
|
69
|
+
}),
|
|
70
|
+
crap: Object.freeze({
|
|
71
|
+
keyField: 'path',
|
|
72
|
+
directions: Object.freeze(['extra']),
|
|
73
|
+
}),
|
|
74
|
+
duplication: Object.freeze({
|
|
75
|
+
keyField: 'path',
|
|
76
|
+
directions: Object.freeze(['extra']),
|
|
77
|
+
}),
|
|
78
|
+
lint: Object.freeze({
|
|
79
|
+
keyField: 'path',
|
|
80
|
+
directions: Object.freeze(['extra']),
|
|
81
|
+
}),
|
|
82
|
+
mutation: Object.freeze({
|
|
83
|
+
keyField: 'path',
|
|
84
|
+
directions: Object.freeze(['extra']),
|
|
85
|
+
}),
|
|
86
|
+
lighthouse: Object.freeze({
|
|
87
|
+
keyField: 'route',
|
|
88
|
+
directions: Object.freeze([]),
|
|
89
|
+
}),
|
|
90
|
+
'bundle-size': Object.freeze({
|
|
91
|
+
keyField: 'bundle',
|
|
92
|
+
directions: Object.freeze([]),
|
|
93
|
+
}),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
/** Every kind the honesty surface knows about, in report order. */
|
|
97
|
+
export const SCOPE_KINDS = Object.freeze(Object.keys(KIND_SCOPE_POLICY));
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The directions `kind` may assert. Unknown kinds assert nothing — a kind this
|
|
101
|
+
* module has never heard of has no scope model, and inventing one would be the
|
|
102
|
+
* phantom-row failure above with extra steps.
|
|
103
|
+
*
|
|
104
|
+
* @param {string} kind
|
|
105
|
+
* @returns {readonly string[]}
|
|
106
|
+
*/
|
|
107
|
+
export function directionsFor(kind) {
|
|
108
|
+
return KIND_SCOPE_POLICY[kind]?.directions ?? [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The row field `kind` keys on — `path` for the file-keyed kinds, `route` for
|
|
113
|
+
* lighthouse, `bundle` for bundle-size. Mirrors `kinds/<kind>.js#keyField`;
|
|
114
|
+
* kept here so a caller can classify without importing the kernel.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} kind
|
|
117
|
+
* @returns {string}
|
|
118
|
+
*/
|
|
119
|
+
export function keyFieldFor(kind) {
|
|
120
|
+
return KIND_SCOPE_POLICY[kind]?.keyField ?? 'path';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* True when `kind` keys its rows on a repo-relative file path, so a directory
|
|
125
|
+
* walk can be compared against its row set at all.
|
|
126
|
+
*
|
|
127
|
+
* @param {string} kind
|
|
128
|
+
* @returns {boolean}
|
|
129
|
+
*/
|
|
130
|
+
export function isFileKeyed(kind) {
|
|
131
|
+
return keyFieldFor(kind) === 'path' && Object.hasOwn(KIND_SCOPE_POLICY, kind);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Glob metacharacters that end a pattern's literal prefix. */
|
|
135
|
+
const GLOB_META = /[*?[\]{}!+@()]/;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Derive directory roots to walk from a list of include globs, by taking each
|
|
139
|
+
* pattern's literal leading path segments.
|
|
140
|
+
*
|
|
141
|
+
* The coverage gate's schema is closed and declares no `targetDirs` — its
|
|
142
|
+
* scope lives entirely in `.c8rc.cjs`. Without this derivation the coverage
|
|
143
|
+
* walk has no root to start from, so there is nothing to compare the row set
|
|
144
|
+
* against and the whole MISSING direction goes silently inert for the one kind
|
|
145
|
+
* whose producer is densest.
|
|
146
|
+
*
|
|
147
|
+
* `'.agents/scripts/**'` yields `.agents/scripts`; a pattern that is glob from
|
|
148
|
+
* its first segment (`'**/*.js'`) yields `.`, the repo root.
|
|
149
|
+
*
|
|
150
|
+
* @param {string[]} globs
|
|
151
|
+
* @returns {string[]} De-duplicated roots, repo-relative POSIX.
|
|
152
|
+
*/
|
|
153
|
+
export function deriveWalkRoots(globs) {
|
|
154
|
+
const roots = new Set();
|
|
155
|
+
for (const glob of Array.isArray(globs) ? globs : []) {
|
|
156
|
+
if (typeof glob !== 'string' || glob.length === 0) continue;
|
|
157
|
+
const literal = [];
|
|
158
|
+
for (const segment of glob.split('/')) {
|
|
159
|
+
if (GLOB_META.test(segment)) break;
|
|
160
|
+
literal.push(segment);
|
|
161
|
+
}
|
|
162
|
+
roots.add(literal.join('/') || '.');
|
|
163
|
+
}
|
|
164
|
+
return [...roots];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Walk `roots` under `cwd` and return canonical repo-relative POSIX paths.
|
|
169
|
+
*
|
|
170
|
+
* Delegates to `scanDirectory`, which already owns the supported-extension set
|
|
171
|
+
* and the ignored-directory set (`node_modules`, `.git`, `temp`, `.worktrees`,
|
|
172
|
+
* …) that every scorer walks with. Passing `ignoreGlobs` straight through
|
|
173
|
+
* means an ignored file is dropped by the identical matcher the scorer uses.
|
|
174
|
+
*
|
|
175
|
+
* @param {{ cwd: string, roots: string[], ignoreGlobs?: string[] }} params
|
|
176
|
+
* @returns {string[]}
|
|
177
|
+
*/
|
|
178
|
+
function walkRoots({ cwd, roots, ignoreGlobs = [] }) {
|
|
179
|
+
const absolute = [];
|
|
180
|
+
for (const root of roots) {
|
|
181
|
+
const abs = path.isAbsolute(root) ? root : path.resolve(cwd, root);
|
|
182
|
+
scanDirectory(abs, absolute, { cwd, ignoreGlobs });
|
|
183
|
+
}
|
|
184
|
+
return absolute.map((abs) =>
|
|
185
|
+
path.relative(cwd, abs).split(path.sep).join('/'),
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Build the coverage inventory from `.c8rc.cjs`.
|
|
191
|
+
*
|
|
192
|
+
* Roots come from the literal prefixes of `include`; the walked set is then
|
|
193
|
+
* filtered through the same `buildScopePredicate` the coverage scorer applies,
|
|
194
|
+
* so `exclude` is honoured by one implementation rather than two.
|
|
195
|
+
*
|
|
196
|
+
* @param {{ cwd: string, requireFn?: (id: string) => object }} params
|
|
197
|
+
* @returns {{ files: string[] | null, roots: string[], degraded: boolean, reason: string | null }}
|
|
198
|
+
*/
|
|
199
|
+
function coverageInventory({ cwd, requireFn }) {
|
|
200
|
+
const load = requireFn ?? createRequire(path.join(cwd, 'noop.cjs'));
|
|
201
|
+
let config;
|
|
202
|
+
try {
|
|
203
|
+
config = load(path.resolve(cwd, '.c8rc.cjs'));
|
|
204
|
+
} catch (err) {
|
|
205
|
+
return {
|
|
206
|
+
files: null,
|
|
207
|
+
roots: [],
|
|
208
|
+
degraded: true,
|
|
209
|
+
reason: `.c8rc.cjs unreadable: ${err?.message ?? err}`,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const include = Array.isArray(config?.include) ? config.include : [];
|
|
213
|
+
const exclude = Array.isArray(config?.exclude) ? config.exclude : [];
|
|
214
|
+
if (include.length === 0) {
|
|
215
|
+
return {
|
|
216
|
+
files: null,
|
|
217
|
+
roots: [],
|
|
218
|
+
degraded: true,
|
|
219
|
+
reason: '.c8rc.cjs declares no include globs',
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const roots = deriveWalkRoots(include);
|
|
223
|
+
const inScope = buildScopePredicate({ include, exclude });
|
|
224
|
+
return {
|
|
225
|
+
files: walkRoots({ cwd, roots }).filter(inScope),
|
|
226
|
+
roots,
|
|
227
|
+
degraded: false,
|
|
228
|
+
reason: null,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Build the inventory for a `targetDirs`-configured kind (everything except
|
|
234
|
+
* coverage). Reads the gate's own `targetDirs` / `ignoreGlobs` off the
|
|
235
|
+
* normalized quality block — the same two keys its refresh scorer reads.
|
|
236
|
+
*
|
|
237
|
+
* @param {{ kind: string, cwd: string, gate: object }} params
|
|
238
|
+
* @returns {{ files: string[] | null, roots: string[], degraded: boolean, reason: string | null }}
|
|
239
|
+
*/
|
|
240
|
+
function targetDirsInventory({ kind, cwd, gate }) {
|
|
241
|
+
const roots = Array.isArray(gate?.targetDirs) ? gate.targetDirs : [];
|
|
242
|
+
const ignoreGlobs = Array.isArray(gate?.ignoreGlobs) ? gate.ignoreGlobs : [];
|
|
243
|
+
if (roots.length === 0) {
|
|
244
|
+
return {
|
|
245
|
+
files: null,
|
|
246
|
+
roots: [],
|
|
247
|
+
degraded: true,
|
|
248
|
+
reason: `delivery.quality.gates.${kind}.targetDirs is empty or unset`,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
files: walkRoots({ cwd, roots, ignoreGlobs }),
|
|
253
|
+
roots,
|
|
254
|
+
degraded: false,
|
|
255
|
+
reason: null,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Resolve the set of files `kind` is configured to have rows for.
|
|
261
|
+
*
|
|
262
|
+
* Never throws: an unreadable or absent scope config degrades to
|
|
263
|
+
* `{ files: null, degraded: true, reason }`. Callers treat a degraded
|
|
264
|
+
* inventory as "scope unknown" — the gate reports it and skips that kind's
|
|
265
|
+
* scope directions, the pruner falls back to orphan-only pruning. Neither may
|
|
266
|
+
* infer emptiness from it: a `files: []` inventory would make every committed
|
|
267
|
+
* row look out-of-scope and hand the pruner a licence to delete the baseline.
|
|
268
|
+
*
|
|
269
|
+
* @param {{
|
|
270
|
+
* kind: string,
|
|
271
|
+
* cwd?: string,
|
|
272
|
+
* quality?: object,
|
|
273
|
+
* requireFn?: (id: string) => object,
|
|
274
|
+
* }} params
|
|
275
|
+
* @returns {{
|
|
276
|
+
* kind: string,
|
|
277
|
+
* keyField: string,
|
|
278
|
+
* directions: readonly string[],
|
|
279
|
+
* files: string[] | null,
|
|
280
|
+
* roots: string[],
|
|
281
|
+
* degraded: boolean,
|
|
282
|
+
* reason: string | null,
|
|
283
|
+
* }}
|
|
284
|
+
*/
|
|
285
|
+
export function buildScopeInventory({
|
|
286
|
+
kind,
|
|
287
|
+
cwd = process.cwd(),
|
|
288
|
+
quality,
|
|
289
|
+
requireFn,
|
|
290
|
+
} = {}) {
|
|
291
|
+
const base = {
|
|
292
|
+
kind,
|
|
293
|
+
keyField: keyFieldFor(kind),
|
|
294
|
+
directions: directionsFor(kind),
|
|
295
|
+
};
|
|
296
|
+
if (!isFileKeyed(kind)) {
|
|
297
|
+
return {
|
|
298
|
+
...base,
|
|
299
|
+
files: null,
|
|
300
|
+
roots: [],
|
|
301
|
+
degraded: false,
|
|
302
|
+
reason: `${kind} is not file-keyed (rows key on "${base.keyField}")`,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
const resolved =
|
|
306
|
+
kind === 'coverage'
|
|
307
|
+
? coverageInventory({ cwd, requireFn })
|
|
308
|
+
: targetDirsInventory({
|
|
309
|
+
kind,
|
|
310
|
+
cwd,
|
|
311
|
+
gate: quality?.gates?.[kind] ?? {},
|
|
312
|
+
});
|
|
313
|
+
return { ...base, ...resolved };
|
|
314
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bdd-step-index.js — scoped discovery and matching for BDD step definitions.
|
|
3
|
+
*
|
|
4
|
+
* `check-gherkin-corpus.js` answers one question per Gherkin step: does a step
|
|
5
|
+
* definition **under this step's own scope** claim it? That question has two
|
|
6
|
+
* halves, and this module owns both so the corpus gate never re-implements
|
|
7
|
+
* either inline:
|
|
8
|
+
*
|
|
9
|
+
* 1. **Discovery** — which files under a scope's `stepRoots` hold step
|
|
10
|
+
* definitions, and which `.feature` files sit under its `featureRoots`.
|
|
11
|
+
* Feature discovery is delegated to `listFeatureFiles` in
|
|
12
|
+
* `bdd-scenario-scanner.js` rather than copied: /plan's scenario index and
|
|
13
|
+
* this gate must agree on what counts as a feature file, and two walkers
|
|
14
|
+
* would eventually disagree.
|
|
15
|
+
* 2. **Matching** — turn each definition's Cucumber expression or regular
|
|
16
|
+
* expression into one `RegExp`, and test a step's text against the index.
|
|
17
|
+
*
|
|
18
|
+
* ## Heuristic index, exact parser
|
|
19
|
+
*
|
|
20
|
+
* The parser half of the gate is exact: `@cucumber/gherkin` decides what
|
|
21
|
+
* compiles. This half is deliberately **not**. Reading step definitions
|
|
22
|
+
* without executing them means a regex scan over source text, and a scan
|
|
23
|
+
* cannot see a definition assembled at runtime, registered through a wrapper,
|
|
24
|
+
* or parameterised by a custom `defineParameterType`. That asymmetry is why
|
|
25
|
+
* the gate ships a step-waiver list: a false "unbound" must always have an
|
|
26
|
+
* escape that does not require switching the whole gate off.
|
|
27
|
+
*
|
|
28
|
+
* Supported Cucumber-expression constructs are the ones the built-in parameter
|
|
29
|
+
* types and the optional/alternation syntax cover — `{int}`, `{float}`,
|
|
30
|
+
* `{word}`, `{string}`, an anonymous `{}`, `text(s)` optionals, and `a/an`
|
|
31
|
+
* word alternation. An unrecognised `{custom}` degrades to `(.*)` rather than
|
|
32
|
+
* failing to match, because over-matching produces a missed finding while
|
|
33
|
+
* under-matching produces a false one, and a false one blocks a delivery.
|
|
34
|
+
*
|
|
35
|
+
* Nothing here reads configuration or exits a process; the CLI owns both.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
39
|
+
import path from 'node:path';
|
|
40
|
+
|
|
41
|
+
import { listFeatureFiles } from './bdd-scenario-scanner.js';
|
|
42
|
+
|
|
43
|
+
export { listFeatureFiles };
|
|
44
|
+
|
|
45
|
+
/** Extensions a step-definition module may carry. */
|
|
46
|
+
const STEP_FILE_EXTENSIONS = Object.freeze([
|
|
47
|
+
'.js',
|
|
48
|
+
'.mjs',
|
|
49
|
+
'.cjs',
|
|
50
|
+
'.ts',
|
|
51
|
+
'.mts',
|
|
52
|
+
'.cts',
|
|
53
|
+
'.tsx',
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
/** Directory names never walked when looking for step definitions. */
|
|
57
|
+
const SKIPPED_DIRECTORIES = Object.freeze(
|
|
58
|
+
new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage']),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Regular expressions for the built-in Cucumber parameter types, keyed by the
|
|
63
|
+
* name inside the braces. The empty key is the anonymous `{}` parameter and
|
|
64
|
+
* doubles as the fallback for a custom type this scan cannot resolve.
|
|
65
|
+
*
|
|
66
|
+
* @type {Readonly<Record<string, string>>}
|
|
67
|
+
*/
|
|
68
|
+
const PARAMETER_PATTERNS = Object.freeze({
|
|
69
|
+
'': '(.*)',
|
|
70
|
+
int: '(-?\\d+)',
|
|
71
|
+
float: '(-?\\d*\\.?\\d+)',
|
|
72
|
+
word: '([^\\s]+)',
|
|
73
|
+
string: '("[^"]*"|\'[^\']*\')',
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Step-registration call sites this scan recognises. `Step` and `defineStep`
|
|
78
|
+
* cover the generic registrars playwright-bdd and cucumber-js both expose.
|
|
79
|
+
*
|
|
80
|
+
* Group 2/3 capture a quoted expression, group 4/5 a regular-expression
|
|
81
|
+
* literal with its flags.
|
|
82
|
+
*/
|
|
83
|
+
const STEP_CALL_PATTERN =
|
|
84
|
+
/\b(Given|When|Then|And|But|Step|defineStep)\s*\(\s*(?:(['"`])((?:\\.|(?!\2)[^\\])*)\2|\/((?:\\.|\[(?:\\.|[^\]\\])*\]|[^/\\[])+)\/([dgimsuvy]*))/g;
|
|
85
|
+
|
|
86
|
+
/** Escape one character for literal use inside a regular expression. */
|
|
87
|
+
function escapeRegExp(text) {
|
|
88
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Render one alternation-free segment of a Cucumber expression: parameter
|
|
93
|
+
* placeholders become capture groups, `(optional)` runs become optional
|
|
94
|
+
* groups, and everything else is escaped literal text.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} segment
|
|
97
|
+
* @returns {string}
|
|
98
|
+
*/
|
|
99
|
+
function renderSegment(segment) {
|
|
100
|
+
let out = '';
|
|
101
|
+
let i = 0;
|
|
102
|
+
while (i < segment.length) {
|
|
103
|
+
const ch = segment[i];
|
|
104
|
+
if (ch === '\\' && i + 1 < segment.length) {
|
|
105
|
+
out += escapeRegExp(segment[i + 1]);
|
|
106
|
+
i += 2;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (ch === '{') {
|
|
110
|
+
const end = segment.indexOf('}', i);
|
|
111
|
+
if (end !== -1) {
|
|
112
|
+
const name = segment.slice(i + 1, end);
|
|
113
|
+
out += PARAMETER_PATTERNS[name] ?? PARAMETER_PATTERNS[''];
|
|
114
|
+
i = end + 1;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (ch === '(') {
|
|
119
|
+
const end = segment.indexOf(')', i);
|
|
120
|
+
if (end !== -1) {
|
|
121
|
+
out += `(?:${renderSegment(segment.slice(i + 1, end))})?`;
|
|
122
|
+
i = end + 1;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
out += escapeRegExp(ch);
|
|
127
|
+
i += 1;
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Split a word on unescaped `/` so `a/an` becomes two alternatives. Escaped
|
|
134
|
+
* `\/` stays literal.
|
|
135
|
+
*
|
|
136
|
+
* @param {string} word
|
|
137
|
+
* @returns {string[]}
|
|
138
|
+
*/
|
|
139
|
+
function splitAlternation(word) {
|
|
140
|
+
const parts = [];
|
|
141
|
+
let current = '';
|
|
142
|
+
for (let i = 0; i < word.length; i += 1) {
|
|
143
|
+
const ch = word[i];
|
|
144
|
+
if (ch === '\\' && i + 1 < word.length) {
|
|
145
|
+
current += ch + word[i + 1];
|
|
146
|
+
i += 1;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (ch === '/') {
|
|
150
|
+
parts.push(current);
|
|
151
|
+
current = '';
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
current += ch;
|
|
155
|
+
}
|
|
156
|
+
parts.push(current);
|
|
157
|
+
return parts;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Compile a Cucumber expression into an anchored `RegExp`.
|
|
162
|
+
*
|
|
163
|
+
* Alternation is resolved per whitespace-separated word, which is what the
|
|
164
|
+
* real expression grammar does. Resolving it across the whole string instead
|
|
165
|
+
* would turn `I have a/an apple` into `^I have a|an apple$` — two anchored
|
|
166
|
+
* alternatives, neither of them the intended step.
|
|
167
|
+
*
|
|
168
|
+
* @param {string} expression
|
|
169
|
+
* @returns {RegExp}
|
|
170
|
+
*/
|
|
171
|
+
function expressionToRegExp(expression) {
|
|
172
|
+
const tokens = String(expression).split(/(\s+)/);
|
|
173
|
+
const body = tokens
|
|
174
|
+
.map((token) => {
|
|
175
|
+
if (token.length === 0) return '';
|
|
176
|
+
if (/^\s+$/.test(token)) return escapeRegExp(token);
|
|
177
|
+
const alternatives = splitAlternation(token);
|
|
178
|
+
const rendered = alternatives.map(renderSegment);
|
|
179
|
+
return rendered.length > 1 ? `(?:${rendered.join('|')})` : rendered[0];
|
|
180
|
+
})
|
|
181
|
+
.join('');
|
|
182
|
+
return new RegExp(`^${body}$`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Recursively list step-definition source files under the given roots.
|
|
187
|
+
*
|
|
188
|
+
* Deliberately not `walkFilesByExtension` from `fs-walk.js`, which is the
|
|
189
|
+
* shared walker for the lint surfaces. That one matches a single extension and
|
|
190
|
+
* walks everything below the root, so reusing it here would mean seven passes
|
|
191
|
+
* — one per accepted extension — each of them descending into a `node_modules`
|
|
192
|
+
* a consumer's step root may well contain. It also rethrows every non-ENOENT
|
|
193
|
+
* `readdir` failure, where this walker must skip an unreadable directory: an
|
|
194
|
+
* unreadable *scope* has to surface as "zero step definitions", the gate's
|
|
195
|
+
* fail-closed path, which names the scope and its step roots.
|
|
196
|
+
*
|
|
197
|
+
* @param {string[]} roots absolute or cwd-relative directories
|
|
198
|
+
* @returns {string[]} absolute paths, sorted
|
|
199
|
+
*/
|
|
200
|
+
export function listStepFiles(roots) {
|
|
201
|
+
const found = [];
|
|
202
|
+
for (const root of roots ?? []) {
|
|
203
|
+
walkStepDir(path.resolve(root), found);
|
|
204
|
+
}
|
|
205
|
+
return found.sort();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function walkStepDir(dir, acc) {
|
|
209
|
+
let entries;
|
|
210
|
+
try {
|
|
211
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
212
|
+
} catch {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
for (const entry of entries) {
|
|
216
|
+
const full = path.join(dir, entry.name);
|
|
217
|
+
// `withFileTypes` reports a symlink as neither file nor directory, so
|
|
218
|
+
// fall back to a stat for those rather than dropping them silently.
|
|
219
|
+
const isDir =
|
|
220
|
+
entry.isDirectory() || (entry.isSymbolicLink() && isDirAt(full));
|
|
221
|
+
if (isDir) {
|
|
222
|
+
if (!SKIPPED_DIRECTORIES.has(entry.name)) walkStepDir(full, acc);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (STEP_FILE_EXTENSIONS.includes(path.extname(entry.name))) acc.push(full);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function isDirAt(target) {
|
|
230
|
+
try {
|
|
231
|
+
return statSync(target).isDirectory();
|
|
232
|
+
} catch {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Extract every step registration in one source file.
|
|
239
|
+
*
|
|
240
|
+
* @param {string} source file contents
|
|
241
|
+
* @param {string} file absolute path, recorded on each entry
|
|
242
|
+
* @returns {Array<{ file: string, line: number, source: string, regex: RegExp }>}
|
|
243
|
+
*/
|
|
244
|
+
function parseStepDefinitions(source, file) {
|
|
245
|
+
const entries = [];
|
|
246
|
+
STEP_CALL_PATTERN.lastIndex = 0;
|
|
247
|
+
let match = STEP_CALL_PATTERN.exec(source);
|
|
248
|
+
while (match !== null) {
|
|
249
|
+
const [, , , quoted, pattern, flags] = match;
|
|
250
|
+
const line = source.slice(0, match.index).split('\n').length;
|
|
251
|
+
const regex = compileMatcher({ quoted, pattern, flags });
|
|
252
|
+
if (regex) entries.push({ file, line, source: quoted ?? pattern, regex });
|
|
253
|
+
match = STEP_CALL_PATTERN.exec(source);
|
|
254
|
+
}
|
|
255
|
+
return entries;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Build one matcher from a captured registration. A malformed regular
|
|
260
|
+
* expression yields `null` — the definition is skipped rather than crashing
|
|
261
|
+
* the scan, and the steps it would have claimed surface as unbound, which is
|
|
262
|
+
* the safe direction.
|
|
263
|
+
*/
|
|
264
|
+
function compileMatcher({ quoted, pattern, flags }) {
|
|
265
|
+
if (typeof quoted === 'string') {
|
|
266
|
+
try {
|
|
267
|
+
return expressionToRegExp(quoted);
|
|
268
|
+
} catch {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
// `g` and `y` are stateful across `.test()` calls; strip them so the index
|
|
274
|
+
// cannot depend on how many times it has been consulted.
|
|
275
|
+
return new RegExp(pattern, (flags ?? '').replace(/[gy]/g, ''));
|
|
276
|
+
} catch {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Build the step index for one scope.
|
|
283
|
+
*
|
|
284
|
+
* @param {{ files: string[], readFile?: (p: string) => string }} params
|
|
285
|
+
* @returns {{ entries: Array<{ file: string, line: number, source: string, regex: RegExp }>, files: string[] }}
|
|
286
|
+
*/
|
|
287
|
+
export function buildStepIndex({ files, readFile }) {
|
|
288
|
+
const read = readFile ?? ((p) => readFileSync(p, 'utf8'));
|
|
289
|
+
const entries = [];
|
|
290
|
+
for (const file of files ?? []) {
|
|
291
|
+
let source;
|
|
292
|
+
try {
|
|
293
|
+
source = read(file);
|
|
294
|
+
} catch {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
entries.push(...parseStepDefinitions(source, file));
|
|
298
|
+
}
|
|
299
|
+
return { entries, files: [...(files ?? [])] };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Find the first definition in the index claiming `text`.
|
|
304
|
+
*
|
|
305
|
+
* @param {{ entries: Array<{ regex: RegExp }> }} index
|
|
306
|
+
* @param {string} text the step text, keyword already stripped by the parser
|
|
307
|
+
* @returns {object | null} the matching entry, or `null` when nothing claims it
|
|
308
|
+
*/
|
|
309
|
+
export function matchStep(index, text) {
|
|
310
|
+
for (const entry of index?.entries ?? []) {
|
|
311
|
+
if (entry.regex.test(text)) return entry;
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Module-private helpers the suite drives directly. Bundled rather than
|
|
318
|
+
* exported individually — the same seam `knip-entry-sync.js` and
|
|
319
|
+
* `source-classifier.js` use — so test-only symbols cost one production
|
|
320
|
+
* dead-export row instead of one each, and so private helpers do not read as
|
|
321
|
+
* API.
|
|
322
|
+
*/
|
|
323
|
+
export const __testing = Object.freeze({
|
|
324
|
+
expressionToRegExp,
|
|
325
|
+
parseStepDefinitions,
|
|
326
|
+
});
|
|
@@ -105,9 +105,11 @@ export function ledgerPath(projectRoot) {
|
|
|
105
105
|
*
|
|
106
106
|
* @param {object} args
|
|
107
107
|
* @param {import('./manifest.js').MutationManifestEntry[]} args.entries
|
|
108
|
-
* — the
|
|
109
|
-
* @param {string[]} args.approvedGroups — the phase groups
|
|
110
|
-
*
|
|
108
|
+
* — the subset of the mutation manifest that was applied.
|
|
109
|
+
* @param {string[]} args.approvedGroups — the phase groups whose mutations
|
|
110
|
+
* landed (sorted for stable output). Field name predates Story #5007's
|
|
111
|
+
* removal of the phased-approval gate; the ledger record shape is a
|
|
112
|
+
* consumer contract read back by `mandrel uninstall`, so it is unchanged.
|
|
111
113
|
* @param {{ owner?: string, repo?: string }} [args.answers]
|
|
112
114
|
* @param {string} [args.appliedAt] — ISO-8601 timestamp (default: now).
|
|
113
115
|
* @param {Record<string, { action?: string }>} [args.report] — the live
|
|
@@ -15,8 +15,7 @@
|
|
|
15
15
|
* `HUMAN_INTENT_FIELDS` table here, and each field's heading is the exact
|
|
16
16
|
* section name the parser maps (`goal` → `## Goal`, etc.). The CI
|
|
17
17
|
* conformance lint (`lint-issue-body.js`) runs the real `parse()` against
|
|
18
|
-
* human-opened issues so the form and the parser cannot silently drift
|
|
19
|
-
* the same model `ci-workflow-template.js` follows for `ci.yml`.
|
|
18
|
+
* human-opened issues so the form and the parser cannot silently drift.
|
|
20
19
|
*
|
|
21
20
|
* ## Form fields ⊆ body schema
|
|
22
21
|
*
|
|
@@ -70,9 +69,8 @@ export const STORY_FORM_RELATIVE_PATH = `${ISSUE_TEMPLATE_RELATIVE_DIR}/story.ym
|
|
|
70
69
|
* generated YAML uses it verbatim as the field `label` so GitHub's
|
|
71
70
|
* `### {label}` render produces a heading the parser recognises.
|
|
72
71
|
*
|
|
73
|
-
* Machine-managed body fields (`wide`, `reason_to_exist`,
|
|
74
|
-
*
|
|
75
|
-
* the runtime fills those. `depends_on` is exposed as a free-text input
|
|
72
|
+
* Machine-managed body fields (`wide`, `reason_to_exist`, `depends_on`
|
|
73
|
+
* meta) are intentionally absent — the runtime fills those. `depends_on` is exposed as a free-text input
|
|
76
74
|
* that serializes to the `blocked by #N` footer `parse()` already reads.
|
|
77
75
|
*
|
|
78
76
|
* @type {Array<{
|
|
@@ -367,7 +365,7 @@ jobs:
|
|
|
367
365
|
|
|
368
366
|
/**
|
|
369
367
|
* Write (or refresh) the Story issue form into a project checkout. Idempotent at
|
|
370
|
-
* the byte level
|
|
368
|
+
* the byte level:
|
|
371
369
|
*
|
|
372
370
|
* - file absent → `created`
|
|
373
371
|
* - byte-identical → `unchanged`
|