mandrel 2.30.0 → 2.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/README.md +13 -17
- package/.agents/agents/acceptance-critic.md +1 -2
- package/.agents/docs/SDLC.md +4 -4
- package/.agents/docs/agentrc-reference.json +61 -57
- package/.agents/docs/configuration.md +274 -227
- package/.agents/docs/execution-reference.md +13 -14
- package/.agents/docs/quality-gates.md +195 -23
- package/.agents/instructions.md +2 -5
- package/.agents/rules/ci-remediation.md +44 -1
- package/.agents/rules/git-conventions-reference.md +27 -27
- package/.agents/rules/git-conventions.md +4 -2
- package/.agents/rules/known-tooling-behavior.md +66 -30
- package/.agents/rules/testing-standards.md +35 -71
- package/.agents/runtime-deps.json +0 -1
- package/.agents/schemas/agentrc.schema.json +1939 -1400
- package/.agents/schemas/lifecycle/README.md +21 -14
- package/.agents/schemas/lifecycle/ledger-record.schema.json +76 -22
- package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
- package/.agents/scripts/README.md +7 -29
- package/.agents/scripts/apply-quality-bootstrap.js +27 -34
- package/.agents/scripts/bootstrap.js +28 -26
- package/.agents/scripts/check-baseline-drift.js +73 -13
- package/.agents/scripts/check-baseline-scope.js +362 -0
- package/.agents/scripts/check-dead-exports.js +9 -1
- package/.agents/scripts/check-gherkin-corpus.js +508 -0
- package/.agents/scripts/check-knip-entries.js +136 -0
- package/.agents/scripts/check-lifecycle-lint.js +36 -112
- package/.agents/scripts/check-schema-references.js +1 -1
- package/.agents/scripts/diagnose-friction.js +7 -4
- package/.agents/scripts/generate-config-docs.js +263 -171
- package/.agents/scripts/install-matrix-assert.js +0 -1
- package/.agents/scripts/lib/ITicketingProvider.js +0 -58
- package/.agents/scripts/lib/audit-baselines/staleness.js +6 -6
- package/.agents/scripts/lib/audit-baselines/trend.js +7 -8
- package/.agents/scripts/lib/audit-baselines/weights.js +4 -5
- package/.agents/scripts/lib/audit-suite/checklist-threading.js +1 -1
- package/.agents/scripts/lib/audit-to-stories/build-story-body.js +0 -1
- package/.agents/scripts/lib/baselines/envelope.js +41 -60
- package/.agents/scripts/lib/baselines/git-base.js +30 -37
- package/.agents/scripts/lib/baselines/kinds/_crap-new-method-gate.js +103 -0
- package/.agents/scripts/lib/baselines/kinds/_crap-read.js +150 -0
- package/.agents/scripts/lib/baselines/kinds/crap.js +25 -65
- package/.agents/scripts/lib/baselines/orphan-pruner.js +233 -0
- package/.agents/scripts/lib/baselines/refresh-service.js +6 -8
- package/.agents/scripts/lib/baselines/scope-assert.js +223 -0
- package/.agents/scripts/lib/baselines/scope-inventory.js +314 -0
- package/.agents/scripts/lib/bdd-step-index.js +326 -0
- package/.agents/scripts/lib/bootstrap/install-ledger.js +5 -3
- package/.agents/scripts/lib/bootstrap/issue-forms-template.js +4 -6
- package/.agents/scripts/lib/bootstrap/manifest.js +17 -40
- package/.agents/scripts/lib/bootstrap/project-bootstrap.js +12 -59
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +62 -2
- package/.agents/scripts/lib/checks/loop-health.js +9 -37
- package/.agents/scripts/lib/child-exec.js +193 -0
- package/.agents/scripts/lib/cli/standard-args.js +1 -1
- package/.agents/scripts/lib/cli-args.js +64 -0
- package/.agents/scripts/lib/close-validation/gates.js +2 -2
- package/.agents/scripts/lib/close-validation/runner.js +3 -3
- package/.agents/scripts/lib/config/acceptance-eval.js +5 -52
- package/.agents/scripts/lib/config/commands.js +3 -5
- package/.agents/scripts/lib/config/explain.js +5 -7
- package/.agents/scripts/lib/config/gates/bundle-size.schema.js +32 -6
- package/.agents/scripts/lib/config/gates/coverage.schema.js +25 -5
- package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +12 -2
- package/.agents/scripts/lib/config/gates/crap.schema.js +68 -23
- package/.agents/scripts/lib/config/gates/duplication.schema.js +29 -17
- package/.agents/scripts/lib/config/gates/index.js +5 -2
- package/.agents/scripts/lib/config/gates/lighthouse.schema.js +34 -6
- package/.agents/scripts/lib/config/gates/lint.schema.js +11 -2
- package/.agents/scripts/lib/config/gates/maintainability.schema.js +37 -15
- package/.agents/scripts/lib/config/gates/mutation.schema.js +15 -3
- package/.agents/scripts/lib/config/gates/shared.js +58 -9
- package/.agents/scripts/lib/config/github.js +0 -1
- package/.agents/scripts/lib/config/limits.js +3 -48
- package/.agents/scripts/lib/config/qa.js +105 -0
- package/.agents/scripts/lib/config/temp-paths.js +6 -5
- package/.agents/scripts/lib/config-settings-schema-delivery.js +237 -56
- package/.agents/scripts/lib/config-settings-schema-quality.js +209 -29
- package/.agents/scripts/lib/config-settings-schema.js +386 -39
- package/.agents/scripts/lib/crap-baseline-join.js +126 -9
- package/.agents/scripts/lib/crap-utils.js +84 -520
- package/.agents/scripts/lib/dead-exports-knip.js +79 -10
- package/.agents/scripts/lib/degraded-mode.js +2 -2
- package/.agents/scripts/lib/doc-tiers.js +3 -3
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +46 -104
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +10 -8
- package/.agents/scripts/lib/fs-walk.js +52 -0
- package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
- package/.agents/scripts/lib/git-utils.js +16 -36
- package/.agents/scripts/lib/knip-entry-sync.js +469 -0
- package/.agents/scripts/lib/observability/metrics-ledger.js +1 -1
- package/.agents/scripts/lib/observability/runtime-friction.js +10 -0
- package/.agents/scripts/lib/observability/signal-validator.js +5 -85
- package/.agents/scripts/lib/observability/signals-writer.js +19 -62
- package/.agents/scripts/lib/observability/source-classifier.js +5 -7
- package/.agents/scripts/lib/observability/terse-result.js +3 -3
- package/.agents/scripts/lib/orchestration/behind-recovery.js +114 -0
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +7 -8
- package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +34 -33
- package/.agents/scripts/lib/orchestration/code-review.js +2 -2
- package/.agents/scripts/lib/orchestration/complexity-gate.js +43 -161
- package/.agents/scripts/lib/orchestration/diff-magnitude.js +4 -4
- package/.agents/scripts/lib/orchestration/label-transitions.js +3 -2
- package/.agents/scripts/lib/orchestration/lease-guard-shared.js +12 -38
- package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +5 -6
- package/.agents/scripts/lib/orchestration/plan-metrics.js +2 -3
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +6 -0
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +0 -1
- package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
- package/.agents/scripts/lib/orchestration/resolve-stories.js +5 -15
- package/.agents/scripts/lib/orchestration/review-providers/codex.js +1 -1
- package/.agents/scripts/lib/orchestration/review-providers/mi-exemptions.js +130 -0
- package/.agents/scripts/lib/orchestration/review-providers/native.js +30 -16
- package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +37 -26
- package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +376 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +161 -151
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +15 -3
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +10 -15
- package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +17 -2
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +5 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +157 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +0 -14
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +65 -25
- package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +20 -31
- package/.agents/scripts/lib/orchestration/spec-spill.js +17 -3
- package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +7 -6
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +2 -1
- package/.agents/scripts/lib/orchestration/task-body-validator.js +4 -1
- package/.agents/scripts/lib/orchestration/ticket-lease.js +28 -127
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -1
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +5 -5
- package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -4
- package/.agents/scripts/lib/orchestration/verify-tier-repair.js +107 -0
- package/.agents/scripts/lib/qa/coverage-verdict.js +5 -87
- package/.agents/scripts/lib/signals/detectors/common.js +1 -1
- package/.agents/scripts/lib/signals/index.js +8 -6
- package/.agents/scripts/lib/signals/schema.js +20 -25
- package/.agents/scripts/lib/signals/write.js +8 -8
- package/.agents/scripts/lib/story-body/story-body.js +12 -59
- package/.agents/scripts/lib/temp-retention.js +1 -1
- package/.agents/scripts/lib/templates/decomposer-prompts.js +16 -14
- package/.agents/scripts/lib/ticket-body-sections.js +4 -5
- package/.agents/scripts/lib/worktree/lifecycle/merge-reachability.js +13 -45
- package/.agents/scripts/lib/worktree/lifecycle/reap.js +4 -5
- package/.agents/scripts/lib/worktree-manager.js +2 -3
- package/.agents/scripts/lint-label-vocabulary.js +2 -24
- package/.agents/scripts/pr-watch-with-update.js +7 -5
- package/.agents/scripts/providers/github/cache.js +2 -2
- package/.agents/scripts/providers/github/comments.js +6 -28
- package/.agents/scripts/providers/github/compose.js +0 -15
- package/.agents/scripts/providers/github/errors.js +10 -27
- package/.agents/scripts/providers/github/request-helpers.js +1 -2
- package/.agents/scripts/providers/github/sub-issues.js +10 -218
- package/.agents/scripts/providers/github.js +4 -7
- package/.agents/scripts/prune-baseline-orphans.js +181 -0
- package/.agents/scripts/resolve-stories.js +0 -2
- package/.agents/scripts/run-lint.js +61 -61
- package/.agents/scripts/run-test-profile.js +6 -6
- package/.agents/scripts/run-verify.js +48 -30
- package/.agents/scripts/single-story-close.js +20 -0
- package/.agents/scripts/single-story-init.js +12 -35
- package/.agents/scripts/update-dead-exports-baseline.js +321 -0
- package/.agents/skills/core/gates-and-baselines/SKILL.md +2 -2
- package/.agents/skills/skills.index.json +2 -12
- package/.agents/skills/stack/qa/playwright/SKILL.md +48 -0
- package/.agents/workflows/audit-documentation.md +5 -6
- package/.agents/workflows/audit-to-stories.md +2 -2
- package/.agents/workflows/helpers/audit-lens-core.md +11 -12
- package/.agents/workflows/helpers/code-quality-guardrails.md +15 -14
- package/.agents/workflows/helpers/code-review.md +3 -8
- package/.agents/workflows/helpers/deliver-reference.md +2 -1
- package/.agents/workflows/helpers/deliver-story-reference.md +27 -16
- package/.agents/workflows/helpers/worktree-lifecycle.md +1 -2
- package/.agents/workflows/mandrel-update.md +10 -10
- package/.agents/workflows/qa-assist.md +15 -20
- package/.agents/workflows/qa-explore.md +9 -8
- package/README.md +1 -1
- package/docs/CHANGELOG.md +49 -0
- package/lib/migrations/index.js +2 -0
- package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +127 -0
- package/package.json +12 -3
- package/.agents/schemas/lifecycle/checkpoint.written.schema.json +0 -13
- package/.agents/schemas/lifecycle/close-validate.end.schema.json +0 -18
- package/.agents/schemas/lifecycle/close-validate.start.schema.json +0 -13
- package/.agents/schemas/lifecycle/code-review.end.schema.json +0 -30
- package/.agents/schemas/lifecycle/code-review.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/intervention.recorded.schema.json +0 -15
- package/.agents/schemas/lifecycle/loop.tick.schema.json +0 -20
- package/.agents/schemas/lifecycle/notification.emitted.schema.json +0 -18
- package/.agents/schemas/lifecycle/pr.created.schema.json +0 -14
- package/.agents/schemas/lifecycle/retro.end.schema.json +0 -16
- package/.agents/schemas/lifecycle/retro.start.schema.json +0 -12
- package/.agents/schemas/lifecycle/story.blocked.schema.json +0 -13
- package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +0 -17
- package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +0 -15
- package/.agents/schemas/lifecycle/story.merged.schema.json +0 -13
- package/.agents/scripts/check-gherkin-placeholders.js +0 -663
- package/.agents/scripts/check-lifecycle-doc-drift.js +0 -411
- package/.agents/scripts/lib/audit-suite/cli.js +0 -64
- package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +0 -202
- package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +0 -212
- package/.agents/scripts/lib/checks/baseline-drift-main-checkout.js +0 -104
- package/.agents/scripts/lib/checks/push-hook-parity.js +0 -106
- package/.agents/scripts/lib/checks/windows-coverage-noise-floor.js +0 -92
- package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +0 -81
- package/.agents/scripts/lib/checks/worktree-residue-biome.js +0 -55
- package/.agents/scripts/lib/crap-baseline-index.js +0 -46
- package/.agents/scripts/lib/crap-utils-incremental.js +0 -113
- package/.agents/scripts/lib/dynamic-workflow/capability.js +0 -396
- package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -335
- package/.agents/scripts/lib/mutation/baseline-snapshot.js +0 -239
- package/.agents/scripts/lib/mutation/config-detector.js +0 -119
- package/.agents/scripts/lib/mutation/stryker-runner.js +0 -306
- package/.agents/scripts/lib/mutation/survivor-report.js +0 -160
- package/.agents/scripts/lib/observability/active-story-env.js +0 -170
- package/.agents/scripts/lib/observability/tool-trace-hook.js +0 -456
- package/.agents/scripts/lib/orchestration/acceptance-clusters.js +0 -111
- package/.agents/scripts/lib/orchestration/context-envelope.js +0 -277
- package/.agents/scripts/lib/orchestration/detectors-phase.js +0 -194
- package/.agents/scripts/lib/orchestration/lifecycle/bus.js +0 -309
- package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +0 -181
- package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +0 -229
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +0 -54
- package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -344
- package/.agents/scripts/lib/orchestration/lint-baseline-service.js +0 -114
- package/.agents/scripts/lib/orchestration/pr-base-guard.js +0 -37
- package/.agents/scripts/lib/orchestration/resolves-token.js +0 -127
- package/.agents/scripts/lib/orchestration/spec-section-validator.js +0 -130
- package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +0 -55
- package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +0 -211
- package/.agents/scripts/lib/planning-corpus.js +0 -37
- package/.agents/scripts/lib/qa/coverage-report.js +0 -181
- package/.agents/scripts/lib/qa/propose-missing-test.js +0 -95
- package/.agents/scripts/lib/qa/qa-context-hydrator.js +0 -217
- package/.agents/scripts/lib/signals/detectors/index.js +0 -14
- package/.agents/scripts/lib/signals/detectors/retry.js +0 -253
- package/.agents/scripts/lib/signals/detectors/rework.js +0 -167
- package/.agents/scripts/lib/signals/read.js +0 -268
- package/.agents/scripts/lib/signals/span-tree.js +0 -291
- package/.agents/scripts/lib/story-lifecycle.js +0 -194
- package/.agents/scripts/lib/story-plan.js +0 -379
- package/.agents/scripts/lib/util/phase-timer-state.js +0 -72
- package/.agents/scripts/lib/util/phase-timer.js +0 -163
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +0 -169
- package/.agents/scripts/lint-baseline.js +0 -507
- package/.agents/scripts/providers/github/prs.js +0 -103
- package/.agents/scripts/signals-view.js +0 -309
- package/.agents/scripts/story-plan.js +0 -370
- package/.agents/scripts/sync-branch-from-base.js +0 -149
- package/.agents/scripts/validate-docs-freshness.js +0 -314
- package/.agents/skills/core/diagnose-friction/SKILL.md +0 -78
- package/.agents/workflows/helpers/signals.md +0 -112
|
@@ -1,306 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* lib/mutation/stryker-runner.js — Invoke Stryker and parse the mutation
|
|
3
|
-
* report (Story #1736, Task #1754).
|
|
4
|
-
*
|
|
5
|
-
* The runner is dependency-injection-friendly so unit tests never have to
|
|
6
|
-
* spawn a real Stryker process. The default `spawnFn` is
|
|
7
|
-
* `child_process.spawnSync`, the default `fsImpl` is `node:fs`, and the
|
|
8
|
-
* default `clock` is `Date.now`.
|
|
9
|
-
*
|
|
10
|
-
* Output contract:
|
|
11
|
-
*
|
|
12
|
-
* { ok: true, mutationScore: <number>, byWorkspace: { "*": <number>, ... },
|
|
13
|
-
* reportPath, durationMs }
|
|
14
|
-
* { ok: false, skipped: true, reason: <string> }
|
|
15
|
-
* { ok: false, skipped: false, error: <string>, durationMs }
|
|
16
|
-
*
|
|
17
|
-
* Stryker writes its JSON report by default to `reports/mutation/mutation.json`.
|
|
18
|
-
* Consumers that relocate the report can pass `reportPath`. We never parse
|
|
19
|
-
* the much larger per-mutant HTML report — only the JSON summary.
|
|
20
|
-
*
|
|
21
|
-
* `timeoutMs` is honoured via `spawnSync`'s `timeout` option; on TIMEOUT
|
|
22
|
-
* we surface an `ok: false` result rather than throwing so the caller can
|
|
23
|
-
* fold the failure into a single uniform "[mutation] failed" gate line.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
import { spawnSync as defaultSpawnSync } from 'node:child_process';
|
|
27
|
-
import fs from 'node:fs';
|
|
28
|
-
import path from 'node:path';
|
|
29
|
-
|
|
30
|
-
import { detectStrykerConfig as defaultDetectStrykerConfig } from './config-detector.js';
|
|
31
|
-
|
|
32
|
-
/** Default per-gate timeout when none is supplied (15 minutes). */
|
|
33
|
-
export const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000;
|
|
34
|
-
|
|
35
|
-
/** Default Stryker JSON report path (mirrors the Stryker default). */
|
|
36
|
-
export const DEFAULT_REPORT_PATH = 'reports/mutation/mutation.json';
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* @typedef {Object} RunResult
|
|
40
|
-
* @property {boolean} ok
|
|
41
|
-
* @property {boolean} [skipped]
|
|
42
|
-
* @property {string} [reason]
|
|
43
|
-
* @property {string} [error]
|
|
44
|
-
* @property {number} [mutationScore]
|
|
45
|
-
* @property {Record<string, number>} [byWorkspace]
|
|
46
|
-
* @property {string} [reportPath]
|
|
47
|
-
* @property {number} [durationMs]
|
|
48
|
-
* @property {number} [exitCode]
|
|
49
|
-
*/
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Run Stryker and return a normalised summary.
|
|
53
|
-
*
|
|
54
|
-
* @param {{
|
|
55
|
-
* cwd?: string,
|
|
56
|
-
* configPath?: string | null,
|
|
57
|
-
* reportPath?: string,
|
|
58
|
-
* timeoutMs?: number,
|
|
59
|
-
* workspace?: string,
|
|
60
|
-
* spawnFn?: typeof defaultSpawnSync,
|
|
61
|
-
* fsImpl?: typeof fs,
|
|
62
|
-
* clock?: () => number,
|
|
63
|
-
* strykerCmd?: string,
|
|
64
|
-
* strykerArgs?: string[],
|
|
65
|
-
* detectFn?: typeof defaultDetectStrykerConfig,
|
|
66
|
-
* skipDetect?: boolean,
|
|
67
|
-
* }} [opts]
|
|
68
|
-
* @returns {Promise<RunResult>}
|
|
69
|
-
*/
|
|
70
|
-
export async function runStryker(opts = {}) {
|
|
71
|
-
const cwd = opts.cwd ?? process.cwd();
|
|
72
|
-
const reportPath = opts.reportPath ?? DEFAULT_REPORT_PATH;
|
|
73
|
-
const timeoutMs = Number.isFinite(opts.timeoutMs)
|
|
74
|
-
? opts.timeoutMs
|
|
75
|
-
: DEFAULT_TIMEOUT_MS;
|
|
76
|
-
const workspace = opts.workspace ?? '*';
|
|
77
|
-
const spawnFn = opts.spawnFn ?? defaultSpawnSync;
|
|
78
|
-
const fsImpl = opts.fsImpl ?? fs;
|
|
79
|
-
const clock = opts.clock ?? (() => Date.now());
|
|
80
|
-
const detectFn = opts.detectFn ?? defaultDetectStrykerConfig;
|
|
81
|
-
|
|
82
|
-
// Detection short-circuit: the runner is the single point of "no
|
|
83
|
-
// Stryker config → skip" so callers can fold the same result into a
|
|
84
|
-
// uniform "[mutation] skipped" gate line. `skipDetect` is the escape
|
|
85
|
-
// hatch for tests and for callers that have already detected.
|
|
86
|
-
let resolvedConfigPath = opts.configPath ?? null;
|
|
87
|
-
if (!opts.skipDetect) {
|
|
88
|
-
const detection = detectFn({
|
|
89
|
-
cwd,
|
|
90
|
-
configPath: resolvedConfigPath,
|
|
91
|
-
fsImpl,
|
|
92
|
-
});
|
|
93
|
-
if (!detection.found) {
|
|
94
|
-
return {
|
|
95
|
-
ok: false,
|
|
96
|
-
skipped: true,
|
|
97
|
-
reason:
|
|
98
|
-
detection.reason ??
|
|
99
|
-
'no Stryker config found. Run `npx stryker init` to enable.',
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
resolvedConfigPath = detection.path;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Default to `npx stryker run` — works whether Stryker is a local devDep
|
|
106
|
-
// or a one-shot install. Consumers with a pinned executable can override.
|
|
107
|
-
const cmd = opts.strykerCmd ?? 'npx';
|
|
108
|
-
const args =
|
|
109
|
-
opts.strykerArgs ??
|
|
110
|
-
(resolvedConfigPath
|
|
111
|
-
? ['stryker', 'run', '--configFile', resolvedConfigPath]
|
|
112
|
-
: ['stryker', 'run']);
|
|
113
|
-
|
|
114
|
-
const startedAt = clock();
|
|
115
|
-
const spawnResult = spawnFn(cmd, args, {
|
|
116
|
-
cwd,
|
|
117
|
-
encoding: 'utf-8',
|
|
118
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
119
|
-
timeout: timeoutMs,
|
|
120
|
-
shell: process.platform === 'win32',
|
|
121
|
-
});
|
|
122
|
-
const durationMs = clock() - startedAt;
|
|
123
|
-
|
|
124
|
-
// spawnSync sets `signal` to 'SIGTERM' when the timeout fires.
|
|
125
|
-
if (
|
|
126
|
-
spawnResult.signal === 'SIGTERM' ||
|
|
127
|
-
spawnResult.error?.code === 'ETIMEDOUT'
|
|
128
|
-
) {
|
|
129
|
-
return {
|
|
130
|
-
ok: false,
|
|
131
|
-
skipped: false,
|
|
132
|
-
error: `Stryker timed out after ${timeoutMs}ms`,
|
|
133
|
-
durationMs,
|
|
134
|
-
exitCode: spawnResult.status ?? -1,
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (spawnResult.error) {
|
|
139
|
-
return {
|
|
140
|
-
ok: false,
|
|
141
|
-
skipped: false,
|
|
142
|
-
error: `failed to invoke Stryker: ${spawnResult.error.message}`,
|
|
143
|
-
durationMs,
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (spawnResult.status !== 0) {
|
|
148
|
-
return {
|
|
149
|
-
ok: false,
|
|
150
|
-
skipped: false,
|
|
151
|
-
error: `Stryker exited with status ${spawnResult.status}: ${
|
|
152
|
-
spawnResult.stderr?.trim() || spawnResult.stdout?.trim() || 'no output'
|
|
153
|
-
}`,
|
|
154
|
-
durationMs,
|
|
155
|
-
exitCode: spawnResult.status,
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const absReportPath = path.isAbsolute(reportPath)
|
|
160
|
-
? reportPath
|
|
161
|
-
: path.resolve(cwd, reportPath);
|
|
162
|
-
|
|
163
|
-
if (!fsImpl.existsSync(absReportPath)) {
|
|
164
|
-
return {
|
|
165
|
-
ok: false,
|
|
166
|
-
skipped: false,
|
|
167
|
-
error: `Stryker report not found at ${reportPath} — expected JSON reporter to be enabled`,
|
|
168
|
-
durationMs,
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
let parsed;
|
|
173
|
-
try {
|
|
174
|
-
parsed = JSON.parse(fsImpl.readFileSync(absReportPath, 'utf8'));
|
|
175
|
-
} catch (err) {
|
|
176
|
-
return {
|
|
177
|
-
ok: false,
|
|
178
|
-
skipped: false,
|
|
179
|
-
error: `failed to parse Stryker report at ${reportPath}: ${err instanceof Error ? err.message : String(err)}`,
|
|
180
|
-
durationMs,
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const summary = summariseReport(parsed, { workspace });
|
|
185
|
-
if (!summary.ok) {
|
|
186
|
-
return {
|
|
187
|
-
ok: false,
|
|
188
|
-
skipped: false,
|
|
189
|
-
error: summary.error,
|
|
190
|
-
durationMs,
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return {
|
|
195
|
-
ok: true,
|
|
196
|
-
mutationScore: summary.mutationScore,
|
|
197
|
-
byWorkspace: summary.byWorkspace,
|
|
198
|
-
reportPath: absReportPath,
|
|
199
|
-
durationMs,
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Pure: parse a Stryker mutation-report payload and return the overall
|
|
205
|
-
* mutation score plus per-workspace breakdown. Exposed for unit testing.
|
|
206
|
-
*
|
|
207
|
-
* The Stryker JSON report shape includes a top-level `files` map keyed by
|
|
208
|
-
* source path, where each entry carries an array of `mutants` with a
|
|
209
|
-
* `status` field. The "mutation score" is the percentage of killed +
|
|
210
|
-
* covered+failed-by-test mutants over the total tested mutants.
|
|
211
|
-
*
|
|
212
|
-
* Status values that count toward the score numerator: `Killed`.
|
|
213
|
-
* Status values that count toward the denominator: `Killed`, `Survived`,
|
|
214
|
-
* `Timeout`, `CompileError`, `RuntimeError`, `NoCoverage`.
|
|
215
|
-
*
|
|
216
|
-
* Stryker also exposes a precomputed `schema.MutationScoreResult` /
|
|
217
|
-
* `metrics.mutationScore` in its `mutation-testing-elements` reports;
|
|
218
|
-
* when present, prefer it over a hand-roll so we never disagree with the
|
|
219
|
-
* Stryker dashboard. The hand-roll is the fallback for older reports
|
|
220
|
-
* that omit the metrics block.
|
|
221
|
-
*
|
|
222
|
-
* @param {unknown} report
|
|
223
|
-
* @param {{ workspace?: string }} [opts]
|
|
224
|
-
* @returns {{ ok: true, mutationScore: number, byWorkspace: Record<string, number> } | { ok: false, error: string }}
|
|
225
|
-
*/
|
|
226
|
-
export function summariseReport(report, opts = {}) {
|
|
227
|
-
const workspace = opts.workspace ?? '*';
|
|
228
|
-
|
|
229
|
-
if (!report || typeof report !== 'object' || Array.isArray(report)) {
|
|
230
|
-
return { ok: false, error: 'Stryker report must be a JSON object' };
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const reportObj = /** @type {Record<string, unknown>} */ (report);
|
|
234
|
-
|
|
235
|
-
// Prefer Stryker's precomputed metrics block when present.
|
|
236
|
-
const metrics = /** @type {Record<string, unknown> | undefined} */ (
|
|
237
|
-
reportObj.metrics
|
|
238
|
-
);
|
|
239
|
-
if (
|
|
240
|
-
metrics &&
|
|
241
|
-
typeof metrics === 'object' &&
|
|
242
|
-
typeof metrics.mutationScore === 'number' &&
|
|
243
|
-
Number.isFinite(metrics.mutationScore)
|
|
244
|
-
) {
|
|
245
|
-
return {
|
|
246
|
-
ok: true,
|
|
247
|
-
mutationScore: metrics.mutationScore,
|
|
248
|
-
byWorkspace: { [workspace]: metrics.mutationScore },
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const files = /** @type {Record<string, unknown> | undefined} */ (
|
|
253
|
-
reportObj.files
|
|
254
|
-
);
|
|
255
|
-
if (!files || typeof files !== 'object') {
|
|
256
|
-
return {
|
|
257
|
-
ok: false,
|
|
258
|
-
error: "Stryker report missing 'files' map",
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
let killed = 0;
|
|
263
|
-
let total = 0;
|
|
264
|
-
for (const entry of Object.values(files)) {
|
|
265
|
-
if (!entry || typeof entry !== 'object') continue;
|
|
266
|
-
const mutants = /** @type {{ mutants?: Array<{ status?: string }> }} */ (
|
|
267
|
-
entry
|
|
268
|
-
).mutants;
|
|
269
|
-
if (!Array.isArray(mutants)) continue;
|
|
270
|
-
for (const m of mutants) {
|
|
271
|
-
const status = m?.status;
|
|
272
|
-
if (typeof status !== 'string') continue;
|
|
273
|
-
if (countsTowardDenominator(status)) total += 1;
|
|
274
|
-
if (status === 'Killed') killed += 1;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
if (total === 0) {
|
|
279
|
-
return {
|
|
280
|
-
ok: false,
|
|
281
|
-
error: 'Stryker report has no scored mutants',
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const score = (killed / total) * 100;
|
|
286
|
-
return {
|
|
287
|
-
ok: true,
|
|
288
|
-
mutationScore: round2(score),
|
|
289
|
-
byWorkspace: { [workspace]: round2(score) },
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function countsTowardDenominator(status) {
|
|
294
|
-
return (
|
|
295
|
-
status === 'Killed' ||
|
|
296
|
-
status === 'Survived' ||
|
|
297
|
-
status === 'Timeout' ||
|
|
298
|
-
status === 'CompileError' ||
|
|
299
|
-
status === 'RuntimeError' ||
|
|
300
|
-
status === 'NoCoverage'
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function round2(n) {
|
|
305
|
-
return Math.round(n * 100) / 100;
|
|
306
|
-
}
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* lib/mutation/survivor-report.js — enumerate actionable surviving mutants
|
|
3
|
-
* from a Stryker JSON report (Story #3428).
|
|
4
|
-
*
|
|
5
|
-
* The mutation gate (`stryker-runner.js` → `summariseReport`) collapses the
|
|
6
|
-
* report into a single score. That score tells you *whether* the suite is
|
|
7
|
-
* leaky, not *where*. This helper does the complementary job: it walks the
|
|
8
|
-
* same `files[].mutants[].status` surface and enumerates the mutants that
|
|
9
|
-
* the suite failed to kill — `Survived` (a mutation that the tests ran over
|
|
10
|
-
* but never caught) and `NoCoverage` (a mutation in a line no test exercises
|
|
11
|
-
* at all) — grouped per file so the survivor-kill loop has concrete targets.
|
|
12
|
-
*
|
|
13
|
-
* It is **pure**: no network, no child process, no filesystem reads. The
|
|
14
|
-
* caller hands in an already-parsed report object (the same JSON that
|
|
15
|
-
* `stryker-runner.js` reads from `reports/mutation/mutation.json`). This
|
|
16
|
-
* keeps the helper trivially unit-testable with inline fixtures and lets
|
|
17
|
-
* remediation tooling reuse a report it already loaded for scoring.
|
|
18
|
-
*
|
|
19
|
-
* Output contract:
|
|
20
|
-
*
|
|
21
|
-
* {
|
|
22
|
-
* ok: true,
|
|
23
|
-
* totals: { survived, noCoverage, actionable, files },
|
|
24
|
-
* files: [
|
|
25
|
-
* {
|
|
26
|
-
* file: "src/a.js",
|
|
27
|
-
* survived: [{ id, line, mutatorName, status, ... }],
|
|
28
|
-
* noCoverage: [{ ... }],
|
|
29
|
-
* count: <survived.length + noCoverage.length>,
|
|
30
|
-
* },
|
|
31
|
-
* ...
|
|
32
|
-
* ],
|
|
33
|
-
* }
|
|
34
|
-
* { ok: false, error: <string> }
|
|
35
|
-
*
|
|
36
|
-
* Files with no actionable survivors are omitted from `files`. The `files`
|
|
37
|
-
* array is sorted by descending actionable count (worst offenders first),
|
|
38
|
-
* ties broken by file path for stable output.
|
|
39
|
-
*/
|
|
40
|
-
|
|
41
|
-
/** Mutant statuses this helper treats as actionable survivors. */
|
|
42
|
-
export const ACTIONABLE_STATUSES = Object.freeze(['Survived', 'NoCoverage']);
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Fields copied verbatim from each Stryker mutant onto the enumerated
|
|
46
|
-
* survivor record. We deliberately keep a fixed allowlist (rather than
|
|
47
|
-
* spreading the raw mutant) so the output shape is stable and never leaks
|
|
48
|
-
* unexpected report internals.
|
|
49
|
-
*/
|
|
50
|
-
const MUTANT_FIELDS = Object.freeze([
|
|
51
|
-
'id',
|
|
52
|
-
'mutatorName',
|
|
53
|
-
'status',
|
|
54
|
-
'location',
|
|
55
|
-
'replacement',
|
|
56
|
-
]);
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Pure: enumerate `Survived` and `NoCoverage` mutants per file from a
|
|
60
|
-
* parsed Stryker mutation report.
|
|
61
|
-
*
|
|
62
|
-
* @param {unknown} report Parsed Stryker JSON report (an object with a
|
|
63
|
-
* top-level `files` map). Passing the raw JSON string is **not**
|
|
64
|
-
* supported — parse it first (mirrors `summariseReport`).
|
|
65
|
-
* @returns {(
|
|
66
|
-
* { ok: true, totals: { survived: number, noCoverage: number, actionable: number, files: number }, files: Array<{ file: string, survived: object[], noCoverage: object[], count: number }> }
|
|
67
|
-
* | { ok: false, error: string }
|
|
68
|
-
* )}
|
|
69
|
-
*/
|
|
70
|
-
export function enumerateSurvivors(report) {
|
|
71
|
-
if (!report || typeof report !== 'object' || Array.isArray(report)) {
|
|
72
|
-
return { ok: false, error: 'Stryker report must be a JSON object' };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const files = /** @type {Record<string, unknown> | undefined} */ (
|
|
76
|
-
/** @type {Record<string, unknown>} */ (report).files
|
|
77
|
-
);
|
|
78
|
-
if (!files || typeof files !== 'object' || Array.isArray(files)) {
|
|
79
|
-
return { ok: false, error: "Stryker report missing 'files' map" };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const perFile = [];
|
|
83
|
-
let survivedTotal = 0;
|
|
84
|
-
let noCoverageTotal = 0;
|
|
85
|
-
|
|
86
|
-
for (const [filePath, entry] of Object.entries(files)) {
|
|
87
|
-
if (!entry || typeof entry !== 'object') continue;
|
|
88
|
-
const mutants = /** @type {{ mutants?: unknown }} */ (entry).mutants;
|
|
89
|
-
if (!Array.isArray(mutants)) continue;
|
|
90
|
-
|
|
91
|
-
const survived = [];
|
|
92
|
-
const noCoverage = [];
|
|
93
|
-
for (const mutant of mutants) {
|
|
94
|
-
if (!mutant || typeof mutant !== 'object') continue;
|
|
95
|
-
const status = /** @type {{ status?: unknown }} */ (mutant).status;
|
|
96
|
-
if (status === 'Survived') {
|
|
97
|
-
survived.push(pickMutant(mutant));
|
|
98
|
-
} else if (status === 'NoCoverage') {
|
|
99
|
-
noCoverage.push(pickMutant(mutant));
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (survived.length === 0 && noCoverage.length === 0) continue;
|
|
104
|
-
|
|
105
|
-
survivedTotal += survived.length;
|
|
106
|
-
noCoverageTotal += noCoverage.length;
|
|
107
|
-
perFile.push({
|
|
108
|
-
file: filePath,
|
|
109
|
-
survived,
|
|
110
|
-
noCoverage,
|
|
111
|
-
count: survived.length + noCoverage.length,
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
perFile.sort((a, b) => b.count - a.count || a.file.localeCompare(b.file));
|
|
116
|
-
|
|
117
|
-
return {
|
|
118
|
-
ok: true,
|
|
119
|
-
totals: {
|
|
120
|
-
survived: survivedTotal,
|
|
121
|
-
noCoverage: noCoverageTotal,
|
|
122
|
-
actionable: survivedTotal + noCoverageTotal,
|
|
123
|
-
files: perFile.length,
|
|
124
|
-
},
|
|
125
|
-
files: perFile,
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Copy the stable allowlist of fields off a raw Stryker mutant. The `line`
|
|
131
|
-
* convenience field is derived from `location.start.line` when present so
|
|
132
|
-
* callers can sort/print without re-walking the nested location shape.
|
|
133
|
-
*
|
|
134
|
-
* @param {Record<string, unknown>} mutant
|
|
135
|
-
* @returns {Record<string, unknown>}
|
|
136
|
-
*/
|
|
137
|
-
function pickMutant(mutant) {
|
|
138
|
-
const out = {};
|
|
139
|
-
for (const field of MUTANT_FIELDS) {
|
|
140
|
-
if (mutant[field] !== undefined) out[field] = mutant[field];
|
|
141
|
-
}
|
|
142
|
-
const line = extractLine(mutant.location);
|
|
143
|
-
if (line !== null) out.line = line;
|
|
144
|
-
return out;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Pull the 1-based start line out of a Stryker `location` object, or null
|
|
149
|
-
* when the shape is missing or malformed.
|
|
150
|
-
*
|
|
151
|
-
* @param {unknown} location
|
|
152
|
-
* @returns {number | null}
|
|
153
|
-
*/
|
|
154
|
-
function extractLine(location) {
|
|
155
|
-
if (!location || typeof location !== 'object') return null;
|
|
156
|
-
const start = /** @type {{ start?: unknown }} */ (location).start;
|
|
157
|
-
if (!start || typeof start !== 'object') return null;
|
|
158
|
-
const line = /** @type {{ line?: unknown }} */ (start).line;
|
|
159
|
-
return typeof line === 'number' && Number.isFinite(line) ? line : null;
|
|
160
|
-
}
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Active-Story env-var propagation (Epic #1030 Story #1043 / Task #1061).
|
|
3
|
-
*
|
|
4
|
-
* The PreToolUse / PostToolUse trace hook in
|
|
5
|
-
* `lib/observability/tool-trace-hook.js` resolves the active Story from
|
|
6
|
-
* `process.env.CC_STORY_ID`. This module is the single writer/clearer of
|
|
7
|
-
* that var:
|
|
8
|
-
*
|
|
9
|
-
* - `setActiveStoryEnv({ storyId, workCwd })` is called from
|
|
10
|
-
* `single-story-init.js` after the worktree is materialised. It sets the
|
|
11
|
-
* var on the current `process.env` (so any child commands the
|
|
12
|
-
* orchestrator spawns inherit it) and exports it to a sibling
|
|
13
|
-
* `.env.local` inside the worktree. The harness re-spawns the
|
|
14
|
-
* agent with that file's contents loaded, so the trace hook fires
|
|
15
|
-
* with the right ids on the *next* tool call after init returns.
|
|
16
|
-
*
|
|
17
|
-
* - `clearActiveStoryEnv({ workCwd })` is called from
|
|
18
|
-
* `story-close/post-merge-close.js` after the merge lands. It
|
|
19
|
-
* deletes the env vars from `process.env` and removes the
|
|
20
|
-
* `.env.local` file. Tooling invoked outside an active Story —
|
|
21
|
-
* planning, dispatch, ad-hoc CLI — must NOT pollute random
|
|
22
|
-
* `traces.ndjson` files; the hook's no-op contract relies on the
|
|
23
|
-
* vars being absent at that point.
|
|
24
|
-
*
|
|
25
|
-
* Both functions are best-effort and never throw on fs failures. The
|
|
26
|
-
* trace hook is itself best-effort, so a stale `.env.local` would at
|
|
27
|
-
* worst cause one extra trace line to land in a stale `temp/.../`
|
|
28
|
-
* directory — annoying but not a correctness bug. We log warnings via
|
|
29
|
-
* the caller's logger when provided.
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
import nodeFs from 'node:fs';
|
|
33
|
-
import nodePath from 'node:path';
|
|
34
|
-
|
|
35
|
-
const ENV_LOCAL_BASENAME = '.env.local';
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Names of the env vars we own. Keeping the list central makes the
|
|
39
|
-
* round-trip (set on init, clear on close) trivially auditable —
|
|
40
|
-
* grep `CC_STORY_ID` / `CC_EPIC_ID` to find every read site.
|
|
41
|
-
*
|
|
42
|
-
* `CC_STORY_ID` is the one var this module writes. `CC_EPIC_ID` is retained
|
|
43
|
-
* on the CLEAR path only: v2 has no Epics and nothing here sets it, but
|
|
44
|
-
* `tool-trace-hook.js`'s `resolveActiveStory` still reads it, so wiping it at
|
|
45
|
-
* close keeps the trace path's "no stale context past close" contract honest
|
|
46
|
-
* against a value that leaked in from outside (a pre-upgrade `.env.local` the
|
|
47
|
-
* harness already loaded, or an operator's shell).
|
|
48
|
-
*
|
|
49
|
-
* `CC_SLICE_ID` / `CC_OPERATOR` were removed with the heartbeat substrate:
|
|
50
|
-
* their only writer was the never-called `setActiveSliceEnv` and their only
|
|
51
|
-
* reader the deleted `hook-heartbeat.js`.
|
|
52
|
-
*/
|
|
53
|
-
export const ACTIVE_STORY_ENV_KEYS = ['CC_EPIC_ID', 'CC_STORY_ID'];
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Render the `.env.local` body. One `KEY=value` line per var, LF
|
|
57
|
-
* line endings (the harness's dotenv parser tolerates CRLF too but LF
|
|
58
|
-
* keeps the file deterministic across Windows / macOS / Linux).
|
|
59
|
-
*
|
|
60
|
-
* Only `CC_STORY_ID` is emitted: v2 Stories are standalone, so there is no
|
|
61
|
-
* parent Epic to key. The trace hook's no-op contract reads "var absent from
|
|
62
|
-
* env" as the signal, so no `CC_EPIC_ID=` line is written at all — an empty
|
|
63
|
-
* one would set the var to the empty string and change the contract.
|
|
64
|
-
*
|
|
65
|
-
* Exported for testing.
|
|
66
|
-
*
|
|
67
|
-
* @param {{ storyId: number }} input
|
|
68
|
-
* @returns {string}
|
|
69
|
-
*/
|
|
70
|
-
export function renderActiveStoryEnvFile({ storyId }) {
|
|
71
|
-
return [
|
|
72
|
-
'# Auto-managed by .agents/scripts/lib/observability/active-story-env.js',
|
|
73
|
-
'# Re-generated on every story-init; deleted on story-close.',
|
|
74
|
-
`CC_STORY_ID=${storyId}`,
|
|
75
|
-
'',
|
|
76
|
-
].join('\n');
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Set `CC_STORY_ID` on the current process and (when `workCwd` is provided)
|
|
81
|
-
* export it to `<workCwd>/.env.local`.
|
|
82
|
-
*
|
|
83
|
-
* Idempotent: re-running with the same id is a no-op on disk; with a
|
|
84
|
-
* different id the `.env.local` is overwritten.
|
|
85
|
-
*
|
|
86
|
-
* v2 Stories are standalone — there is no parent Epic — so `CC_EPIC_ID` is
|
|
87
|
-
* never set, and any pre-existing value is removed: the trace hook's
|
|
88
|
-
* `resolveActiveStory` no-op contract is keyed on the var's absence, and a
|
|
89
|
-
* stale epic id would key this Story's traces to a foreign Epic directory.
|
|
90
|
-
*
|
|
91
|
-
* @param {{ storyId: number, workCwd?: string,
|
|
92
|
-
* env?: NodeJS.ProcessEnv, fs?: typeof nodeFs,
|
|
93
|
-
* logger?: { warn?: (m: string) => void } }} args
|
|
94
|
-
* @returns {{ envSet: boolean, fileWritten: boolean, filePath: string|null }}
|
|
95
|
-
*/
|
|
96
|
-
export function setActiveStoryEnv({
|
|
97
|
-
storyId,
|
|
98
|
-
workCwd,
|
|
99
|
-
env = process.env,
|
|
100
|
-
fs = nodeFs,
|
|
101
|
-
logger,
|
|
102
|
-
} = {}) {
|
|
103
|
-
if (!Number.isInteger(storyId) || storyId <= 0) {
|
|
104
|
-
throw new Error(
|
|
105
|
-
`[active-story-env] storyId must be a positive integer; got ${storyId}`,
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if ('CC_EPIC_ID' in env) delete env.CC_EPIC_ID;
|
|
110
|
-
env.CC_STORY_ID = String(storyId);
|
|
111
|
-
|
|
112
|
-
let fileWritten = false;
|
|
113
|
-
let filePath = null;
|
|
114
|
-
if (typeof workCwd === 'string' && workCwd.length > 0) {
|
|
115
|
-
filePath = nodePath.join(workCwd, ENV_LOCAL_BASENAME);
|
|
116
|
-
try {
|
|
117
|
-
fs.writeFileSync(filePath, renderActiveStoryEnvFile({ storyId }), {
|
|
118
|
-
encoding: 'utf8',
|
|
119
|
-
});
|
|
120
|
-
fileWritten = true;
|
|
121
|
-
} catch (err) {
|
|
122
|
-
logger?.warn?.(
|
|
123
|
-
`[active-story-env] Failed to write ${filePath}: ${
|
|
124
|
-
err instanceof Error ? err.message : String(err)
|
|
125
|
-
}`,
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return { envSet: true, fileWritten, filePath };
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Delete `CC_EPIC_ID` / `CC_STORY_ID` from the current process env and
|
|
134
|
-
* remove `<workCwd>/.env.local` when present. Best-effort; a missing
|
|
135
|
-
* file is not an error.
|
|
136
|
-
*
|
|
137
|
-
* @param {{ workCwd?: string, env?: NodeJS.ProcessEnv,
|
|
138
|
-
* fs?: typeof nodeFs,
|
|
139
|
-
* logger?: { warn?: (m: string) => void } }} args
|
|
140
|
-
* @returns {{ envCleared: boolean, fileRemoved: boolean, filePath: string|null }}
|
|
141
|
-
*/
|
|
142
|
-
export function clearActiveStoryEnv({
|
|
143
|
-
workCwd,
|
|
144
|
-
env = process.env,
|
|
145
|
-
fs = nodeFs,
|
|
146
|
-
logger,
|
|
147
|
-
} = {}) {
|
|
148
|
-
for (const k of ACTIVE_STORY_ENV_KEYS) {
|
|
149
|
-
if (k in env) delete env[k];
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
let fileRemoved = false;
|
|
153
|
-
let filePath = null;
|
|
154
|
-
if (typeof workCwd === 'string' && workCwd.length > 0) {
|
|
155
|
-
filePath = nodePath.join(workCwd, ENV_LOCAL_BASENAME);
|
|
156
|
-
try {
|
|
157
|
-
if (fs.existsSync(filePath)) {
|
|
158
|
-
fs.unlinkSync(filePath);
|
|
159
|
-
fileRemoved = true;
|
|
160
|
-
}
|
|
161
|
-
} catch (err) {
|
|
162
|
-
logger?.warn?.(
|
|
163
|
-
`[active-story-env] Failed to remove ${filePath}: ${
|
|
164
|
-
err instanceof Error ? err.message : String(err)
|
|
165
|
-
}`,
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
return { envCleared: true, fileRemoved, filePath };
|
|
170
|
-
}
|