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,233 @@
|
|
|
1
|
+
// .agents/scripts/lib/baselines/orphan-pruner.js
|
|
2
|
+
//
|
|
3
|
+
// Story #5012 — the measurement-free remedy that makes hard-failing on a stale
|
|
4
|
+
// baseline row defensible.
|
|
5
|
+
//
|
|
6
|
+
// The scope gate (`check-baseline-scope.js`) blocks a PR that leaves a row
|
|
7
|
+
// pointing at a file it deleted. A hard gate is only fair while its remedy is
|
|
8
|
+
// cheap, and the existing remedy — re-run the scorer, re-derive the baseline —
|
|
9
|
+
// costs a full coverage run or a full-tree MI re-score for what is arithmetic
|
|
10
|
+
// on a row set. This pruner is that arithmetic.
|
|
11
|
+
//
|
|
12
|
+
// ## Measurement-free by contract
|
|
13
|
+
//
|
|
14
|
+
// Three prohibitions, each guarding a way the file could start lying:
|
|
15
|
+
//
|
|
16
|
+
// 1. **Never add a row.** Adding one means claiming a measurement nobody
|
|
17
|
+
// took. Producing rows stays the producers' exclusive job.
|
|
18
|
+
// 2. **Never restamp `generatedAt`.** A fresh stamp over rows nobody
|
|
19
|
+
// re-measured is precisely the failure an age check exists to catch — the
|
|
20
|
+
// envelope would claim to describe today's tree on the strength of a
|
|
21
|
+
// deletion. The pruner carries the original stamp through untouched.
|
|
22
|
+
// 3. **Never delete a row it cannot prove inert.** Only two classes qualify:
|
|
23
|
+
// the file is absent from disk, or the file is no longer matched by the
|
|
24
|
+
// gate's own `targetDirs` / `ignoreGlobs`. Everything else survives.
|
|
25
|
+
//
|
|
26
|
+
// ## Degrade, never guess
|
|
27
|
+
//
|
|
28
|
+
// A degraded inventory (`files: null` — unreadable `.c8rc.cjs`, absent
|
|
29
|
+
// `targetDirs`) means scope is unknown, not empty. Treating it as empty would
|
|
30
|
+
// make every row look out-of-scope and hand the pruner the whole baseline. So
|
|
31
|
+
// an unreadable scope config falls back to **orphan-only** pruning: the
|
|
32
|
+
// absent-from-disk class still resolves off the filesystem and is still safe,
|
|
33
|
+
// while the out-of-scope class is suspended until scope can be read again.
|
|
34
|
+
|
|
35
|
+
import fs from 'node:fs';
|
|
36
|
+
import path from 'node:path';
|
|
37
|
+
|
|
38
|
+
import { getKindModule } from './kernel.js';
|
|
39
|
+
import { _internals as readerInternals } from './reader.js';
|
|
40
|
+
import { EXTRA_REASONS } from './scope-assert.js';
|
|
41
|
+
import {
|
|
42
|
+
buildScopeInventory,
|
|
43
|
+
isFileKeyed,
|
|
44
|
+
SCOPE_KINDS,
|
|
45
|
+
} from './scope-inventory.js';
|
|
46
|
+
import { writeFile } from './writer.js';
|
|
47
|
+
|
|
48
|
+
/** Every kind whose rows key on a repo-relative file path. */
|
|
49
|
+
export const PRUNABLE_KINDS = Object.freeze(SCOPE_KINDS.filter(isFileKeyed));
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Partition a row set into the rows to keep and the rows to drop.
|
|
53
|
+
*
|
|
54
|
+
* Pure given its injected `existsOnDisk` predicate. `inScope` of `null` is the
|
|
55
|
+
* degraded case: only the absent-from-disk class is prunable.
|
|
56
|
+
*
|
|
57
|
+
* @param {{
|
|
58
|
+
* rows: Array<Record<string, unknown>>,
|
|
59
|
+
* inScope: Set<string> | null,
|
|
60
|
+
* existsOnDisk: (relPath: string) => boolean,
|
|
61
|
+
* }} params
|
|
62
|
+
* @returns {{ keep: Array<object>, removed: Array<{ path: string, reason: string }> }}
|
|
63
|
+
*/
|
|
64
|
+
export function planPrune({ rows, inScope, existsOnDisk } = {}) {
|
|
65
|
+
const keep = [];
|
|
66
|
+
const removed = [];
|
|
67
|
+
for (const row of Array.isArray(rows) ? rows : []) {
|
|
68
|
+
const key = row?.path;
|
|
69
|
+
if (typeof key !== 'string' || key.length === 0) {
|
|
70
|
+
keep.push(row);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (!existsOnDisk(key)) {
|
|
74
|
+
removed.push({ path: key, reason: EXTRA_REASONS.ABSENT });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (inScope !== null && !inScope.has(key)) {
|
|
78
|
+
removed.push({ path: key, reason: EXTRA_REASONS.OUT_OF_SCOPE });
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
keep.push(row);
|
|
82
|
+
}
|
|
83
|
+
return { keep, removed };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Prune one parsed envelope and recompute its `rollup` through the kind's own
|
|
88
|
+
* arithmetic (`kinds/<kind>.js#rollup`), so the pruned file stays internally
|
|
89
|
+
* consistent and still validates against its schema. Recomputing by a private
|
|
90
|
+
* formula here would let the rollup and the rows describe different trees.
|
|
91
|
+
*
|
|
92
|
+
* A rollup carrying component buckets beyond `*` is refused rather than
|
|
93
|
+
* recomputed: the component globs that produced those buckets live in gate
|
|
94
|
+
* config this module is not handed, and emitting a `*`-only rollup would
|
|
95
|
+
* silently delete them.
|
|
96
|
+
*
|
|
97
|
+
* @param {{
|
|
98
|
+
* kind: string,
|
|
99
|
+
* envelope: object,
|
|
100
|
+
* inventory: { files: string[] | null },
|
|
101
|
+
* existsOnDisk?: (relPath: string) => boolean,
|
|
102
|
+
* }} params
|
|
103
|
+
* @returns {{
|
|
104
|
+
* envelope: object | null,
|
|
105
|
+
* removed: Array<{ path: string, reason: string }>,
|
|
106
|
+
* skipped: boolean,
|
|
107
|
+
* reason: string | null,
|
|
108
|
+
* }}
|
|
109
|
+
*/
|
|
110
|
+
export function pruneEnvelope({
|
|
111
|
+
kind,
|
|
112
|
+
envelope,
|
|
113
|
+
inventory,
|
|
114
|
+
existsOnDisk,
|
|
115
|
+
} = {}) {
|
|
116
|
+
const rollupKeys = Object.keys(envelope?.rollup ?? {});
|
|
117
|
+
if (rollupKeys.some((key) => key !== '*')) {
|
|
118
|
+
return {
|
|
119
|
+
envelope: null,
|
|
120
|
+
removed: [],
|
|
121
|
+
skipped: true,
|
|
122
|
+
reason: `rollup carries component buckets (${rollupKeys.join(', ')}); prune them with the producer`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const inScope = inventory?.files === null ? null : new Set(inventory.files);
|
|
126
|
+
const { keep, removed } = planPrune({
|
|
127
|
+
rows: envelope?.rows ?? [],
|
|
128
|
+
inScope,
|
|
129
|
+
existsOnDisk: existsOnDisk ?? (() => true),
|
|
130
|
+
});
|
|
131
|
+
if (removed.length === 0) {
|
|
132
|
+
return { envelope: null, removed: [], skipped: false, reason: null };
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
envelope: {
|
|
136
|
+
...envelope,
|
|
137
|
+
// `generatedAt` is carried by the spread, deliberately unmodified.
|
|
138
|
+
rollup: getKindModule(kind).rollup(keep, []),
|
|
139
|
+
rows: keep,
|
|
140
|
+
},
|
|
141
|
+
removed,
|
|
142
|
+
skipped: false,
|
|
143
|
+
reason: null,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Read and JSON-parse a baseline file. Returns `null` when the file is absent
|
|
149
|
+
* (a kind this repository does not use) and throws only on malformed JSON,
|
|
150
|
+
* which is a real defect the caller should surface rather than swallow.
|
|
151
|
+
*
|
|
152
|
+
* @param {string} absPath
|
|
153
|
+
* @param {typeof fs} fsImpl
|
|
154
|
+
* @returns {object | null}
|
|
155
|
+
*/
|
|
156
|
+
function readEnvelope(absPath, fsImpl) {
|
|
157
|
+
let raw;
|
|
158
|
+
try {
|
|
159
|
+
raw = fsImpl.readFileSync(absPath, 'utf8');
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
return JSON.parse(raw);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Prune one kind end to end: resolve its baseline path, read it, build the
|
|
168
|
+
* scope inventory, plan, and (unless `check`) write.
|
|
169
|
+
*
|
|
170
|
+
* @param {object} params
|
|
171
|
+
* @returns {object} One entry of the report's `kinds` array.
|
|
172
|
+
*/
|
|
173
|
+
function pruneKind({ kind, cwd, quality, check, fsImpl, requireFn }) {
|
|
174
|
+
const absPath = readerInternals.resolveBaselinePath(kind, { cwd });
|
|
175
|
+
const relPath = path.relative(cwd, absPath).split(path.sep).join('/');
|
|
176
|
+
const envelope = readEnvelope(absPath, fsImpl);
|
|
177
|
+
if (envelope === null) {
|
|
178
|
+
return { kind, path: relPath, present: false, removed: [], written: false };
|
|
179
|
+
}
|
|
180
|
+
const inventory = buildScopeInventory({ kind, cwd, quality, requireFn });
|
|
181
|
+
const result = pruneEnvelope({
|
|
182
|
+
kind,
|
|
183
|
+
envelope,
|
|
184
|
+
inventory,
|
|
185
|
+
existsOnDisk: (rel) => fsImpl.existsSync(path.resolve(cwd, rel)),
|
|
186
|
+
});
|
|
187
|
+
const base = {
|
|
188
|
+
kind,
|
|
189
|
+
path: relPath,
|
|
190
|
+
present: true,
|
|
191
|
+
degraded: inventory.degraded,
|
|
192
|
+
degradedReason: inventory.degraded ? inventory.reason : null,
|
|
193
|
+
skipped: result.skipped,
|
|
194
|
+
skipReason: result.reason,
|
|
195
|
+
removed: result.removed,
|
|
196
|
+
written: false,
|
|
197
|
+
};
|
|
198
|
+
if (result.envelope === null || check) return base;
|
|
199
|
+
writeFile(absPath, result.envelope, { fsImpl });
|
|
200
|
+
return { ...base, written: true };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Prune every file-keyed baseline under `cwd`.
|
|
205
|
+
*
|
|
206
|
+
* @param {{
|
|
207
|
+
* cwd?: string,
|
|
208
|
+
* kinds?: string[],
|
|
209
|
+
* check?: boolean,
|
|
210
|
+
* quality?: object,
|
|
211
|
+
* fsImpl?: typeof fs,
|
|
212
|
+
* requireFn?: (id: string) => object,
|
|
213
|
+
* }} params
|
|
214
|
+
* @returns {{ kinds: Array<object>, removedCount: number, writtenCount: number, check: boolean }}
|
|
215
|
+
*/
|
|
216
|
+
export function runPrune({
|
|
217
|
+
cwd = process.cwd(),
|
|
218
|
+
kinds = PRUNABLE_KINDS,
|
|
219
|
+
check = false,
|
|
220
|
+
quality,
|
|
221
|
+
fsImpl = fs,
|
|
222
|
+
requireFn,
|
|
223
|
+
} = {}) {
|
|
224
|
+
const results = kinds.map((kind) =>
|
|
225
|
+
pruneKind({ kind, cwd, quality, check, fsImpl, requireFn }),
|
|
226
|
+
);
|
|
227
|
+
return {
|
|
228
|
+
check,
|
|
229
|
+
kinds: results,
|
|
230
|
+
removedCount: results.reduce((sum, r) => sum + r.removed.length, 0),
|
|
231
|
+
writtenCount: results.filter((r) => r.written).length,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
@@ -76,11 +76,10 @@
|
|
|
76
76
|
* @module .agents/scripts/lib/baselines/refresh-service
|
|
77
77
|
*/
|
|
78
78
|
|
|
79
|
-
import { execFile as nodeExecFile } from 'node:child_process';
|
|
80
79
|
import nodeFs from 'node:fs';
|
|
81
80
|
import { createRequire } from 'node:module';
|
|
82
81
|
import path from 'node:path';
|
|
83
|
-
import {
|
|
82
|
+
import { execFileCaptureAsync } from '../child-exec.js';
|
|
84
83
|
import { getQuality, resolveConfig } from '../config-resolver.js';
|
|
85
84
|
import {
|
|
86
85
|
buildScopePredicate,
|
|
@@ -107,8 +106,6 @@ import {
|
|
|
107
106
|
|
|
108
107
|
const nodeRequire = createRequire(import.meta.url);
|
|
109
108
|
|
|
110
|
-
const execFileAsync = promisify(nodeExecFile);
|
|
111
|
-
|
|
112
109
|
/**
|
|
113
110
|
* Kinds the refresh service knows how to dispatch. Stays in lockstep with
|
|
114
111
|
* the per-kind modules under `.agents/scripts/lib/baselines/kinds/`.
|
|
@@ -574,8 +571,9 @@ function assertRequiredScopeRows({
|
|
|
574
571
|
}
|
|
575
572
|
|
|
576
573
|
/**
|
|
577
|
-
* Default git-diff derivation for the diff-scope path.
|
|
578
|
-
*
|
|
574
|
+
* Default git-diff derivation for the diff-scope path. Runs through the shared
|
|
575
|
+
* child-process surface ([`child-exec.js`](../child-exec.js)) — `execFile`,
|
|
576
|
+
* never a shell — and only the canonical two-dot range `baseRef..headRef`:
|
|
579
577
|
* triple-dot is intentionally avoided so the result reflects exactly the
|
|
580
578
|
* files that differ between the two refs at the time of the call.
|
|
581
579
|
*
|
|
@@ -585,10 +583,10 @@ function assertRequiredScopeRows({
|
|
|
585
583
|
async function defaultGitDiff({ baseRef, headRef, cwd }) {
|
|
586
584
|
const range = `${baseRef}..${headRef}`;
|
|
587
585
|
try {
|
|
588
|
-
const { stdout } = await
|
|
586
|
+
const { stdout } = await execFileCaptureAsync(
|
|
589
587
|
'git',
|
|
590
588
|
['diff', '--name-only', range],
|
|
591
|
-
{ cwd
|
|
589
|
+
{ cwd },
|
|
592
590
|
);
|
|
593
591
|
return stdout
|
|
594
592
|
.split(/\r?\n/)
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// .agents/scripts/lib/baselines/scope-assert.js
|
|
2
|
+
//
|
|
3
|
+
// Story #5012 — the pure both-directions row-set assertion, plus the
|
|
4
|
+
// merge-base attribution that decides which half of a divergence is this
|
|
5
|
+
// change set's fault.
|
|
6
|
+
//
|
|
7
|
+
// `check-baselines.js` answers "did a measured value regress?". Nothing
|
|
8
|
+
// answered "does this baseline still describe the tree?" — so a row could
|
|
9
|
+
// point at a file deleted six months ago, or an in-scope file could carry no
|
|
10
|
+
// row at all, and every gate stayed green. Both directions are checked here:
|
|
11
|
+
//
|
|
12
|
+
// - **missing** — an in-scope file with no row. Only asserted for kinds
|
|
13
|
+
// whose producer emits one row per in-scope file
|
|
14
|
+
// (`scope-inventory.js#KIND_SCOPE_POLICY`).
|
|
15
|
+
// - **extra** — a row whose file is gone from disk (`absent`) or no longer
|
|
16
|
+
// matched by the gate's own `targetDirs` / `ignoreGlobs` (`out-of-scope`).
|
|
17
|
+
//
|
|
18
|
+
// ## Why attribution exists
|
|
19
|
+
//
|
|
20
|
+
// Whole-tree equality on the PR path is unusable: the moment anyone lands an
|
|
21
|
+
// in-scope file, every open PR reds on divergence its author did not create
|
|
22
|
+
// and cannot fix from their branch. So the gate blocks only on divergence
|
|
23
|
+
// attributable to `merge-base(base, HEAD)..HEAD`, and reports the inherited
|
|
24
|
+
// remainder as a warning that costs nothing to carry.
|
|
25
|
+
//
|
|
26
|
+
// Attribution can be wrong in one safe direction only, so the resolution
|
|
27
|
+
// **fails towards strict**: no resolvable base, a HEAD not ahead of its base,
|
|
28
|
+
// or a change set that edits a baseline or the config defining its scope all
|
|
29
|
+
// promote every finding to fatal. The last case is the load-bearing one — once
|
|
30
|
+
// a branch has rewritten the scope rules themselves, "which side of the
|
|
31
|
+
// merge-base introduced this row" is no longer a question the diff can answer.
|
|
32
|
+
//
|
|
33
|
+
// Pure module: no filesystem, no git, no config resolution. Every input is
|
|
34
|
+
// handed in already resolved, which is what lets the whole attribution matrix
|
|
35
|
+
// be unit-tested without a fixture repository.
|
|
36
|
+
|
|
37
|
+
/** A row whose keyed file no longer exists on disk. */
|
|
38
|
+
const REASON_ABSENT = 'absent';
|
|
39
|
+
/** A row whose file exists but is no longer inside the gate's scope. */
|
|
40
|
+
const REASON_OUT_OF_SCOPE = 'out-of-scope';
|
|
41
|
+
|
|
42
|
+
export const EXTRA_REASONS = Object.freeze({
|
|
43
|
+
ABSENT: REASON_ABSENT,
|
|
44
|
+
OUT_OF_SCOPE: REASON_OUT_OF_SCOPE,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Strictness reasons, in resolution order. Exported so the CLI can render the
|
|
49
|
+
* cause without restating the strings.
|
|
50
|
+
*/
|
|
51
|
+
export const STRICT_REASONS = Object.freeze({
|
|
52
|
+
NO_BASE: 'no-resolvable-base',
|
|
53
|
+
NOT_AHEAD: 'head-not-ahead-of-base',
|
|
54
|
+
BASELINE_EDITED: 'change-set-edits-a-baseline',
|
|
55
|
+
SCOPE_CONFIG_EDITED: 'change-set-edits-a-scope-config',
|
|
56
|
+
ATTRIBUTABLE: 'attributable-to-merge-base-range',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Collect the distinct key values a row set carries, skipping malformed rows.
|
|
61
|
+
*
|
|
62
|
+
* @param {Array<Record<string, unknown>>} rows
|
|
63
|
+
* @param {string} keyField
|
|
64
|
+
* @returns {Set<string>}
|
|
65
|
+
*/
|
|
66
|
+
function rowKeys(rows, keyField) {
|
|
67
|
+
const keys = new Set();
|
|
68
|
+
for (const row of Array.isArray(rows) ? rows : []) {
|
|
69
|
+
const value = row?.[keyField];
|
|
70
|
+
if (typeof value === 'string' && value.length > 0) keys.add(value);
|
|
71
|
+
}
|
|
72
|
+
return keys;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Compare a baseline's row set against its in-scope inventory.
|
|
77
|
+
*
|
|
78
|
+
* Direction gating is the inventory's call, not this function's: a kind whose
|
|
79
|
+
* policy omits `missing` produces no missing findings even when its row set is
|
|
80
|
+
* far sparser than the tree, because for that kind sparseness is correct.
|
|
81
|
+
*
|
|
82
|
+
* @param {{
|
|
83
|
+
* inventory: { kind: string, keyField: string, directions: readonly string[],
|
|
84
|
+
* files: string[] | null, degraded: boolean, reason: string | null },
|
|
85
|
+
* rows: Array<Record<string, unknown>>,
|
|
86
|
+
* existsOnDisk?: (relPath: string) => boolean,
|
|
87
|
+
* }} params
|
|
88
|
+
* @returns {{
|
|
89
|
+
* kind: string,
|
|
90
|
+
* skipped: boolean,
|
|
91
|
+
* reason: string | null,
|
|
92
|
+
* missing: string[],
|
|
93
|
+
* extra: Array<{ path: string, reason: string }>,
|
|
94
|
+
* }}
|
|
95
|
+
*/
|
|
96
|
+
export function assertScope({ inventory, rows, existsOnDisk } = {}) {
|
|
97
|
+
const kind = inventory?.kind ?? 'unknown';
|
|
98
|
+
const directions = inventory?.directions ?? [];
|
|
99
|
+
if (directions.length === 0 || inventory?.files === null) {
|
|
100
|
+
return {
|
|
101
|
+
kind,
|
|
102
|
+
skipped: true,
|
|
103
|
+
reason: inventory?.reason ?? `${kind} declares no assertable direction`,
|
|
104
|
+
missing: [],
|
|
105
|
+
extra: [],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const keyField = inventory.keyField;
|
|
110
|
+
const inScope = new Set(inventory.files);
|
|
111
|
+
const keyed = rowKeys(rows, keyField);
|
|
112
|
+
const onDisk = typeof existsOnDisk === 'function' ? existsOnDisk : () => true;
|
|
113
|
+
|
|
114
|
+
const missing = directions.includes('missing')
|
|
115
|
+
? inventory.files.filter((file) => !keyed.has(file)).sort()
|
|
116
|
+
: [];
|
|
117
|
+
|
|
118
|
+
const extra = [];
|
|
119
|
+
if (directions.includes('extra')) {
|
|
120
|
+
for (const key of [...keyed].sort()) {
|
|
121
|
+
if (inScope.has(key)) continue;
|
|
122
|
+
extra.push({
|
|
123
|
+
path: key,
|
|
124
|
+
reason: onDisk(key) ? REASON_OUT_OF_SCOPE : REASON_ABSENT,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { kind, skipped: false, reason: null, missing, extra };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Decide whether this run blocks on every finding or only on the attributable
|
|
134
|
+
* ones. Fails towards strict — each early return below is a case where the
|
|
135
|
+
* merge-base range cannot be trusted to explain the divergence.
|
|
136
|
+
*
|
|
137
|
+
* @param {{
|
|
138
|
+
* base?: string | null,
|
|
139
|
+
* aheadOfBase?: boolean,
|
|
140
|
+
* changedFiles?: string[],
|
|
141
|
+
* baselinePaths?: string[],
|
|
142
|
+
* scopeConfigPaths?: string[],
|
|
143
|
+
* }} params
|
|
144
|
+
* @returns {{ strict: boolean, reason: string }}
|
|
145
|
+
*/
|
|
146
|
+
export function resolveStrictness({
|
|
147
|
+
base,
|
|
148
|
+
aheadOfBase,
|
|
149
|
+
changedFiles = [],
|
|
150
|
+
baselinePaths = [],
|
|
151
|
+
scopeConfigPaths = [],
|
|
152
|
+
} = {}) {
|
|
153
|
+
if (typeof base !== 'string' || base.length === 0) {
|
|
154
|
+
return { strict: true, reason: STRICT_REASONS.NO_BASE };
|
|
155
|
+
}
|
|
156
|
+
if (aheadOfBase !== true) {
|
|
157
|
+
return { strict: true, reason: STRICT_REASONS.NOT_AHEAD };
|
|
158
|
+
}
|
|
159
|
+
const changed = new Set(changedFiles);
|
|
160
|
+
if (baselinePaths.some((file) => changed.has(file))) {
|
|
161
|
+
return { strict: true, reason: STRICT_REASONS.BASELINE_EDITED };
|
|
162
|
+
}
|
|
163
|
+
if (scopeConfigPaths.some((file) => changed.has(file))) {
|
|
164
|
+
return { strict: true, reason: STRICT_REASONS.SCOPE_CONFIG_EDITED };
|
|
165
|
+
}
|
|
166
|
+
return { strict: false, reason: STRICT_REASONS.ATTRIBUTABLE };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Split a kind's findings into the fatal set and the warning set.
|
|
171
|
+
*
|
|
172
|
+
* Under `strict`, everything is fatal. Otherwise a finding is fatal only when
|
|
173
|
+
* this change set created it:
|
|
174
|
+
*
|
|
175
|
+
* - a **missing** row for a file the range ADDED — the branch introduced an
|
|
176
|
+
* in-scope file and left it unmeasured;
|
|
177
|
+
* - an **extra** row for a file the range DELETED or renamed away — the
|
|
178
|
+
* branch removed the file and left its row behind.
|
|
179
|
+
*
|
|
180
|
+
* Everything else is inherited: real, worth reporting, and not this author's
|
|
181
|
+
* to fix from this branch.
|
|
182
|
+
*
|
|
183
|
+
* @param {{
|
|
184
|
+
* missing?: string[],
|
|
185
|
+
* extra?: Array<{ path: string, reason: string }>,
|
|
186
|
+
* added?: string[],
|
|
187
|
+
* removed?: string[],
|
|
188
|
+
* strict?: boolean,
|
|
189
|
+
* }} params
|
|
190
|
+
* @returns {{
|
|
191
|
+
* fatal: { missing: string[], extra: Array<{ path: string, reason: string }> },
|
|
192
|
+
* warning: { missing: string[], extra: Array<{ path: string, reason: string }> },
|
|
193
|
+
* fatalCount: number,
|
|
194
|
+
* warningCount: number,
|
|
195
|
+
* }}
|
|
196
|
+
*/
|
|
197
|
+
export function attributeDivergence({
|
|
198
|
+
missing = [],
|
|
199
|
+
extra = [],
|
|
200
|
+
added = [],
|
|
201
|
+
removed = [],
|
|
202
|
+
strict = false,
|
|
203
|
+
} = {}) {
|
|
204
|
+
const addedSet = new Set(added);
|
|
205
|
+
const removedSet = new Set(removed);
|
|
206
|
+
const isFatalMissing = (file) => strict || addedSet.has(file);
|
|
207
|
+
const isFatalExtra = (finding) => strict || removedSet.has(finding.path);
|
|
208
|
+
|
|
209
|
+
const fatal = {
|
|
210
|
+
missing: missing.filter(isFatalMissing),
|
|
211
|
+
extra: extra.filter(isFatalExtra),
|
|
212
|
+
};
|
|
213
|
+
const warning = {
|
|
214
|
+
missing: missing.filter((file) => !isFatalMissing(file)),
|
|
215
|
+
extra: extra.filter((finding) => !isFatalExtra(finding)),
|
|
216
|
+
};
|
|
217
|
+
return {
|
|
218
|
+
fatal,
|
|
219
|
+
warning,
|
|
220
|
+
fatalCount: fatal.missing.length + fatal.extra.length,
|
|
221
|
+
warningCount: warning.missing.length + warning.extra.length,
|
|
222
|
+
};
|
|
223
|
+
}
|