mandrel 2.58.0 → 2.60.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 +17 -12
- package/.agents/agents/acceptance-critic.md +24 -43
- package/.agents/agents/story-worker.md +18 -19
- package/.agents/docs/SDLC.md +12 -13
- package/.agents/docs/agentrc-reference.json +1 -2
- package/.agents/docs/configuration.md +29 -46
- package/.agents/docs/quality-gates.md +9 -5
- package/.agents/docs/workflows.md +1 -1
- package/.agents/instructions.md +5 -7
- package/.agents/rules/ci-remediation.md +41 -8
- package/.agents/rules/known-tooling-behavior.md +65 -15
- package/.agents/runtime-deps.json +7 -2
- package/.agents/schemas/acceptance-eval-verdict.schema.json +1 -1
- package/.agents/schemas/agentrc.schema.json +6 -11
- package/.agents/schemas/crap-baseline.schema.json +1 -1
- package/.agents/schemas/crap-report.schema.json +1 -1
- package/.agents/schemas/story-deliver-terminal.schema.json +3 -3
- package/.agents/scripts/README.md +11 -1
- package/.agents/scripts/acceptance-eval.js +25 -27
- package/.agents/scripts/ceremony-derive.js +15 -10
- package/.agents/scripts/check-context-budget.js +148 -228
- package/.agents/scripts/check-schema-references.js +5 -3
- package/.agents/scripts/check-workflow-citations.js +33 -147
- package/.agents/scripts/coverage-capture.js +7 -4
- package/.agents/scripts/deliver-light.js +41 -100
- package/.agents/scripts/deliver-run.js +631 -0
- package/.agents/scripts/file-ci-gap.js +59 -11
- package/.agents/scripts/install-matrix-assert.js +48 -3
- package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +51 -33
- package/.agents/scripts/lib/baselines/crap-preview-incremental.js +6 -2
- package/.agents/scripts/lib/baselines/kinds/_crap-read.js +0 -8
- package/.agents/scripts/lib/baselines/kinds/crap.js +35 -18
- package/.agents/scripts/lib/changed-files.js +30 -0
- package/.agents/scripts/lib/config/delivery-routing.js +5 -4
- package/.agents/scripts/lib/config/explain.js +1 -3
- package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +1 -1
- package/.agents/scripts/lib/config-resolver.js +1 -0
- package/.agents/scripts/lib/config-settings-schema-delivery.js +28 -21
- package/.agents/scripts/lib/coverage-capture-fullscope.js +10 -2
- package/.agents/scripts/lib/coverage-capture-incremental.js +3 -2
- package/.agents/scripts/lib/coverage-capture-usage.js +4 -1
- package/.agents/scripts/lib/crap-engine.js +2 -2
- package/.agents/scripts/lib/crap-utils.js +21 -5
- package/.agents/scripts/lib/doc-tiers.js +4 -2
- package/.agents/scripts/lib/escomplex-ast-compat.js +39 -17
- package/.agents/scripts/lib/escomplex-kernel.js +298 -0
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +7 -6
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +7 -5
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/gh-exec.js +160 -0
- package/.agents/scripts/lib/maintainability-engine.js +3 -3
- package/.agents/scripts/lib/observability/source-classifier.js +1 -0
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +74 -132
- package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +123 -12
- package/.agents/scripts/lib/orchestration/complexity-gate.js +180 -352
- package/.agents/scripts/lib/orchestration/light-suitability.js +71 -136
- package/.agents/scripts/lib/orchestration/plan-context.js +44 -50
- package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +8 -6
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +104 -119
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +41 -25
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +11 -11
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +63 -29
- package/.agents/scripts/lib/orchestration/plan-persist/wave-collision-gate.js +107 -0
- package/.agents/scripts/lib/orchestration/review-depth.js +14 -11
- package/.agents/scripts/lib/orchestration/run-epilogue.js +260 -182
- package/.agents/scripts/lib/orchestration/run-scoped-config.js +63 -99
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +3 -3
- package/.agents/scripts/lib/orchestration/single-story-close/phases/graphql-preflight.js +137 -0
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +105 -18
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +4 -3
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +156 -39
- package/.agents/scripts/lib/orchestration/story-init-envelope.js +71 -0
- package/.agents/scripts/lib/orchestration/task-body-validator.js +8 -17
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +25 -209
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +8 -5
- package/.agents/scripts/lib/orchestration/ticket-validator.js +44 -183
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +14 -25
- package/.agents/scripts/lib/runtime-deps/dep-resolution.js +155 -0
- package/.agents/scripts/lib/runtime-deps/ensure-installed.js +44 -9
- package/.agents/scripts/lib/runtime-deps/parser-major.js +110 -0
- package/.agents/scripts/lib/runtime-deps/preflight.js +6 -25
- package/.agents/scripts/lib/runtime-deps/scan-imports.js +46 -1
- package/.agents/scripts/lib/skills/walk-skill-files.js +1 -1
- package/.agents/scripts/lib/story-body/body-format-lints.js +58 -12
- package/.agents/scripts/lib/story-body/story-body.js +83 -29
- package/.agents/scripts/lib/templates/decomposer-prompts.js +28 -33
- package/.agents/scripts/lib/wave-runner/live-probe.js +31 -5
- package/.agents/scripts/merge-baseline.js +4 -5
- package/.agents/scripts/plan-context.js +117 -28
- package/.agents/scripts/plan-persist.js +79 -39
- package/.agents/scripts/plan-run-epilogue.js +11 -8
- package/.agents/scripts/pr-watch-with-update.js +9 -2
- package/.agents/scripts/run-verify.js +13 -6
- package/.agents/scripts/single-story-init.js +7 -57
- package/.agents/scripts/stories-wave-tick.js +160 -26
- package/.agents/skills/core/gates-and-baselines/reference.md +0 -1
- package/.agents/skills/skills.index.json +2 -12
- package/.agents/skills/stack/qa/playwright/SKILL.md +26 -0
- package/.agents/workflows/audit-to-stories.md +14 -11
- package/.agents/workflows/helpers/acceptance-self-eval.md +84 -157
- package/.agents/workflows/helpers/code-review.md +4 -2
- package/.agents/workflows/helpers/deliver-digest.md +31 -24
- package/.agents/workflows/helpers/deliver-light.md +92 -101
- package/.agents/workflows/helpers/deliver-reference.md +116 -100
- package/.agents/workflows/helpers/deliver-story-reference.md +58 -124
- package/.agents/workflows/helpers/deliver-story.md +17 -18
- package/.agents/workflows/helpers/plan-reference.md +82 -60
- package/.agents/workflows/mandrel-deliver.md +47 -31
- package/.agents/workflows/mandrel-plan.md +32 -30
- package/.agents/workflows/mandrel-update.md +36 -21
- package/README.md +3 -3
- package/docs/CHANGELOG.md +43 -0
- package/lib/cli/registry.js +45 -25
- package/lib/cli/update.js +376 -17
- package/lib/migrations/index.js +2 -0
- package/lib/migrations/steps/2.60.0-retire-audit-results-autofile.js +40 -0
- package/package.json +8 -2
- package/.agents/schemas/model-attribution.schema.json +0 -53
- package/.agents/scripts/lib/orchestration/model-attribution.js +0 -418
- package/.agents/scripts/lib/orchestration/split-policy-validator.js +0 -188
- package/.agents/scripts/lib/orchestration/story-plan-state.js +0 -33
- package/.agents/scripts/lib/orchestration/structured-comment-parser.js +0 -67
- package/.agents/scripts/lib/templates/spec-author-prompts.js +0 -76
- package/.agents/skills/core/scope-triage/SKILL.md +0 -48
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* logic; behaviour is byte-for-byte the pre-#4981 body.
|
|
9
9
|
*/
|
|
10
10
|
import path from 'node:path';
|
|
11
|
+
import { resolveChangedFilesRef } from './changed-files.js';
|
|
11
12
|
import {
|
|
12
13
|
anyChangedUnderTargets,
|
|
13
14
|
describeFreshness,
|
|
@@ -19,10 +20,14 @@ import {
|
|
|
19
20
|
* content-digest freshness probe, and — when stale — the full-repo
|
|
20
21
|
* `npm run test:coverage` capture + stamp write.
|
|
21
22
|
*
|
|
23
|
+
* The skip check scores the ref `resolveChangedFilesRef` resolves — the rule,
|
|
24
|
+
* and the same answer, incremental mode applies, so a fall-through from it
|
|
25
|
+
* cannot change scope mid-run (Story #5365).
|
|
26
|
+
*
|
|
22
27
|
* @param {{
|
|
23
28
|
* crap: object,
|
|
24
29
|
* coverage: object,
|
|
25
|
-
* args: { skipWhenNoCrapFiles: boolean, ref: string, cwd: string },
|
|
30
|
+
* args: { skipWhenNoCrapFiles: boolean, ref: string | null, cwd: string },
|
|
26
31
|
* getChangedFilesImpl: Function,
|
|
27
32
|
* isCoverageFreshImpl: Function,
|
|
28
33
|
* runCaptureImpl: Function,
|
|
@@ -46,7 +51,10 @@ export function runFullScopeCapture({
|
|
|
46
51
|
if (args.skipWhenNoCrapFiles) {
|
|
47
52
|
let changed;
|
|
48
53
|
try {
|
|
49
|
-
changed = getChangedFilesImpl({
|
|
54
|
+
changed = getChangedFilesImpl({
|
|
55
|
+
ref: resolveChangedFilesRef({ crap, ref: args.ref }),
|
|
56
|
+
cwd: args.cwd,
|
|
57
|
+
});
|
|
50
58
|
} catch (err) {
|
|
51
59
|
// A bad ref must not silently relax the gate. Fall through to the
|
|
52
60
|
// freshness check so coverage still gets captured if needed.
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* parameter (`.agents/rules/test-seams.md` rules 1-2, 4).
|
|
10
10
|
*/
|
|
11
11
|
import path from 'node:path';
|
|
12
|
+
import { resolveChangedFilesRef } from './changed-files.js';
|
|
12
13
|
import { stampCapturedTree } from './coverage-capture.js';
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -38,7 +39,7 @@ import { stampCapturedTree } from './coverage-capture.js';
|
|
|
38
39
|
* @param {{
|
|
39
40
|
* crap: object,
|
|
40
41
|
* coverage: object,
|
|
41
|
-
* args: { ref: string, cwd: string },
|
|
42
|
+
* args: { ref: string | null, cwd: string },
|
|
42
43
|
* getChangedFilesImpl: Function,
|
|
43
44
|
* filterFilesUnderTargetsImpl: Function,
|
|
44
45
|
* isCoverageFreshImpl: Function,
|
|
@@ -63,7 +64,7 @@ export function tryIncrementalCapture({
|
|
|
63
64
|
}) {
|
|
64
65
|
if (crap.incrementalCoverage?.skipWhenUnchanged !== true) return null;
|
|
65
66
|
|
|
66
|
-
const ref = crap
|
|
67
|
+
const ref = resolveChangedFilesRef({ crap, ref: args.ref });
|
|
67
68
|
let changed = null;
|
|
68
69
|
try {
|
|
69
70
|
changed = getChangedFilesImpl({ ref, cwd: args.cwd });
|
|
@@ -39,7 +39,10 @@ const COVERAGE_CAPTURE_USAGE = {
|
|
|
39
39
|
'--require-credited',
|
|
40
40
|
'Refuse (exit 1) instead of spawning when no credited capture stamp covers this tree. Passed by the close gate when delivery.execution.requireCreditedCapture is set; a bare invocation always runs, so the deposit path stays open.',
|
|
41
41
|
],
|
|
42
|
-
[
|
|
42
|
+
[
|
|
43
|
+
'--ref <git-ref>',
|
|
44
|
+
'Git ref the changed-file set is computed against. Passing it wins over delivery.quality.gates.crap.incrementalCoverage.baseRef, so a caller that anchors another gate on the same ref gets one scope for both.',
|
|
45
|
+
],
|
|
43
46
|
['--cwd <path>', 'Repository root the capture runs in.'],
|
|
44
47
|
],
|
|
45
48
|
};
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import escomplex from 'typhonjs-escomplex';
|
|
2
1
|
import { coverageForMethodInEntry } from './coverage-utils.js';
|
|
3
2
|
// `finalizeMethodRowsWithBaseline` (Story #4981) lives in
|
|
4
3
|
// crap-baseline-join.js — `resolveRawRow`, the per-row policy it shares with
|
|
@@ -15,6 +14,7 @@ import {
|
|
|
15
14
|
} from './crap-coordinates.js';
|
|
16
15
|
import { deriveMethodIdentities } from './crap-method-identity.js';
|
|
17
16
|
import { install as installAstCompat } from './escomplex-ast-compat.js';
|
|
17
|
+
import { analyzeModule } from './escomplex-kernel.js';
|
|
18
18
|
|
|
19
19
|
export { COORDINATE_ORIGINAL, COORDINATE_TRANSPILED, crapFormula };
|
|
20
20
|
|
|
@@ -256,7 +256,7 @@ export function calculateCrapForSource(
|
|
|
256
256
|
) {
|
|
257
257
|
let report;
|
|
258
258
|
try {
|
|
259
|
-
report =
|
|
259
|
+
report = analyzeModule(source);
|
|
260
260
|
} catch {
|
|
261
261
|
return UNSCORABLE;
|
|
262
262
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import escomplex from 'typhonjs-escomplex';
|
|
4
3
|
import { canonicalise as canonicalisePath } from './baselines/path-canon.js';
|
|
5
4
|
import { findCoverageEntry } from './coverage-utils.js';
|
|
6
5
|
import { POOL_SERIAL_THRESHOLD, runOnPool } from './cpu-pool.js';
|
|
@@ -11,6 +10,7 @@ import {
|
|
|
11
10
|
shouldSkipFileForNoCoverage,
|
|
12
11
|
} from './crap-baseline-join.js';
|
|
13
12
|
import { COORDINATE_ORIGINAL, methodRowsFromReport } from './crap-engine.js';
|
|
13
|
+
import { analyzeModule } from './escomplex-kernel.js';
|
|
14
14
|
import { Logger } from './Logger.js';
|
|
15
15
|
import { scanDirectory } from './maintainability-utils.js';
|
|
16
16
|
import {
|
|
@@ -36,8 +36,24 @@ export { resolveTsTranspilerVersion };
|
|
|
36
36
|
const SCHEMA_REF = '.agents/schemas/crap-baseline.schema.json';
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
39
|
+
* Package whose resolved version is stamped as the scorer identity.
|
|
40
|
+
*
|
|
41
|
+
* `escomplex-plugin-metrics-module` computes the metrics. The retired
|
|
42
|
+
* `typhonjs-escomplex` facade did not, so stamping it described the shell
|
|
43
|
+
* rather than the scorer.
|
|
44
|
+
*/
|
|
45
|
+
const SCORER_PACKAGE = 'escomplex-plugin-metrics-module';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the running scorer's version by walking up from `cwd` and reading the
|
|
49
|
+
* nearest `node_modules/<SCORER_PACKAGE>/package.json`.
|
|
50
|
+
*
|
|
51
|
+
* The package read is `escomplex-plugin-metrics-module`, which owns the
|
|
52
|
+
* Halstead and maintainability math — not the displaced `typhonjs-escomplex`
|
|
53
|
+
* shell, which contributed a parser and a plugin bus and never a metric. The
|
|
54
|
+
* stamp is supposed to answer "could this scorer have produced different
|
|
55
|
+
* numbers", so it has to name the package that computes them.
|
|
56
|
+
*
|
|
41
57
|
* Returns `'0.0.0'` when the dependency cannot be found — callers treat that
|
|
42
58
|
* sentinel as "unknown environment" and may refuse to persist a baseline.
|
|
43
59
|
*
|
|
@@ -51,7 +67,7 @@ export function resolveEscomplexVersion(cwd = process.cwd()) {
|
|
|
51
67
|
const pkgPath = path.join(
|
|
52
68
|
dir,
|
|
53
69
|
'node_modules',
|
|
54
|
-
|
|
70
|
+
SCORER_PACKAGE,
|
|
55
71
|
'package.json',
|
|
56
72
|
);
|
|
57
73
|
if (fs.existsSync(pkgPath)) {
|
|
@@ -266,7 +282,7 @@ export function checkResolutionFloor(resolution, floor) {
|
|
|
266
282
|
function analyzeOnce(source, coverageForFile, mapLine = null) {
|
|
267
283
|
let report;
|
|
268
284
|
try {
|
|
269
|
-
report =
|
|
285
|
+
report = analyzeModule(source);
|
|
270
286
|
} catch {
|
|
271
287
|
return { report: null, miScore: 0, crapRows: [], parseError: true };
|
|
272
288
|
}
|
|
@@ -27,8 +27,10 @@
|
|
|
27
27
|
* - `agentBoot` — the role-scoped boot contexts `.agents/agents/*.md`
|
|
28
28
|
* (issue #4478). Each is a standalone system prompt a
|
|
29
29
|
* converted spawn boots on **instead of** the always-loaded
|
|
30
|
-
* closure, so it is
|
|
31
|
-
* ceiling
|
|
30
|
+
* closure, so it is measured independently. Its per-file
|
|
31
|
+
* 8 KB ceiling was deleted by Story #5340;
|
|
32
|
+
* `check-context-budget.js` now reports these sizes and
|
|
33
|
+
* gates nothing on them.
|
|
32
34
|
* - `workflow` / — the `.agents/workflows/**` read-tier (Story #4752),
|
|
33
35
|
* `workflowOnDemand` resolved as each entry point's transitive
|
|
34
36
|
* markdown-link closure by
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* ## The upstream defect
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* The kernel parses with `@babel/parser`, so every AST it analyses is a
|
|
8
|
+
* **Babel** AST. But `typhonjs-escomplex-commons`'
|
|
9
9
|
* `utils/ast/astSyntax.js` — the code generator that `ASTGenerator` drives —
|
|
10
10
|
* was written against **ESTree**. The two disagree on node names
|
|
11
11
|
* (`OptionalMemberExpression` vs a `MemberExpression` with `optional: true`)
|
|
@@ -62,6 +62,21 @@ import { createRequire } from 'node:module';
|
|
|
62
62
|
|
|
63
63
|
const require = createRequire(import.meta.url);
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* The package whose resolution anchors every `typhonjs-escomplex-commons` deep
|
|
67
|
+
* import in this module.
|
|
68
|
+
*
|
|
69
|
+
* `escomplex-plugin-syntax-babylon` is the package that actually reads the
|
|
70
|
+
* `astSyntax` table during a metric traversal, so its copy of `commons` is the
|
|
71
|
+
* only one worth patching.
|
|
72
|
+
*
|
|
73
|
+
* Module-local on purpose. A test proves the binding by resolving from this
|
|
74
|
+
* package's own root itself — which is the assertion worth making, since our
|
|
75
|
+
* copy and the plugin's coincide under a hoisting installer and an
|
|
76
|
+
* "ours === theirs" check would pass vacuously.
|
|
77
|
+
*/
|
|
78
|
+
const ANCHOR_PACKAGE = 'escomplex-plugin-syntax-babylon';
|
|
79
|
+
|
|
65
80
|
/** Marker set on every function this module installs, for idempotency. */
|
|
66
81
|
const PATCH_MARKER = Symbol.for('mandrel.escomplexAstCompat');
|
|
67
82
|
|
|
@@ -77,25 +92,32 @@ let installResult = null;
|
|
|
77
92
|
* back to today's behaviour (unscorable files, now reported explicitly by
|
|
78
93
|
* the engine rather than silently scored 0).
|
|
79
94
|
*
|
|
80
|
-
* The patch must land on the *same* `
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
95
|
+
* The patch must land on the *same* `astSyntax` table the metric traversal
|
|
96
|
+
* reads, and that table is resolved by **`escomplex-plugin-syntax-babylon`**,
|
|
97
|
+
* from its own location — `PluginSyntaxBabylon` requires
|
|
98
|
+
* `typhonjs-escomplex-commons/dist/utils/ast/ASTGenerator` and `ASTState` binds
|
|
99
|
+
* the table it finds. So `commons` is anchored through the syntax plugin's
|
|
100
|
+
* resolution, not through this module's and not through the kernel's.
|
|
101
|
+
*
|
|
102
|
+
* Resolving it from here, or from the kernel, would be a coin flip: under a
|
|
103
|
+
* hoisting installer every copy usually coincides, but under pnpm's isolated
|
|
104
|
+
* layout — or as soon as anything declares `commons` at a different version —
|
|
105
|
+
* the plugin can read a *different* physical copy, and the patch then lands on
|
|
106
|
+
* a table nobody reads while `install()` cheerfully reports success. Anchoring
|
|
107
|
+
* on the reader makes that unreachable. Note this is why a test asserting
|
|
108
|
+
* "our copy === the patched copy" proves nothing: under hoisting it passes
|
|
109
|
+
* vacuously — a test must resolve from the plugin's own root instead.
|
|
88
110
|
*
|
|
89
111
|
* `requireFn` is the test seam: a cross-checkout verification harness passes
|
|
90
|
-
* its own `createRequire` so the anchor starts from that checkout's
|
|
112
|
+
* its own `createRequire` so the anchor starts from that checkout's plugin.
|
|
91
113
|
*
|
|
92
114
|
* @param {NodeJS.Require} [requireFn]
|
|
93
115
|
* @returns {Record<string, Function>|null}
|
|
94
116
|
*/
|
|
95
117
|
function resolveSyntaxTable(requireFn = require) {
|
|
96
118
|
try {
|
|
97
|
-
const
|
|
98
|
-
const mod =
|
|
119
|
+
const fromReader = createRequire(requireFn.resolve(ANCHOR_PACKAGE));
|
|
120
|
+
const mod = fromReader(
|
|
99
121
|
'typhonjs-escomplex-commons/dist/utils/ast/astSyntax.js',
|
|
100
122
|
);
|
|
101
123
|
const table = mod?.default ?? mod;
|
|
@@ -337,8 +359,8 @@ export function install(options = {}) {
|
|
|
337
359
|
/**
|
|
338
360
|
* `ASTUtil` is only needed by the `OptionalCallExpression` handler, and only at
|
|
339
361
|
* call time — resolving it lazily keeps `install()` free of a second deep
|
|
340
|
-
* import that could fail at module load. Anchored through the
|
|
341
|
-
* same reason as {@link resolveSyntaxTable}.
|
|
362
|
+
* import that could fail at module load. Anchored through the syntax plugin
|
|
363
|
+
* for the same reason as {@link resolveSyntaxTable}.
|
|
342
364
|
*
|
|
343
365
|
* `formatSequence` is a pure helper that takes the traveler and state as
|
|
344
366
|
* arguments, so which copy answers is immaterial — but resolving it the same
|
|
@@ -347,8 +369,8 @@ export function install(options = {}) {
|
|
|
347
369
|
* @returns {{ formatSequence: Function }}
|
|
348
370
|
*/
|
|
349
371
|
function ASTUtil() {
|
|
350
|
-
const
|
|
351
|
-
const mod =
|
|
372
|
+
const fromReader = createRequire(require.resolve(ANCHOR_PACKAGE));
|
|
373
|
+
const mod = fromReader(
|
|
352
374
|
'typhonjs-escomplex-commons/dist/utils/ast/ASTUtil.js',
|
|
353
375
|
);
|
|
354
376
|
return mod?.default ?? mod;
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* escomplex-kernel.js — the complexity kernel's parse and dispatch layers,
|
|
3
|
+
* in-repo.
|
|
4
|
+
*
|
|
5
|
+
* ## Why this file exists
|
|
6
|
+
*
|
|
7
|
+
* `typhonjs-escomplex` was a thin shell around four packages that do all the
|
|
8
|
+
* actual work. The shell contributed two things: a parser front-end
|
|
9
|
+
* (`@typhonjs/babel-parser`, a ~100-LOC shim over `@babel/parser`) and a
|
|
10
|
+
* generic plugin bus (`typhonjs-plugin-manager`, used as a hardcoded
|
|
11
|
+
* two-plugin synchronous dispatcher). Nine packages of plumbing hang off
|
|
12
|
+
* those two, and none of it computes anything.
|
|
13
|
+
*
|
|
14
|
+
* What this does **not** do is remove `core-js@2`. Four of the retained
|
|
15
|
+
* metric-core packages `require('babel-runtime/core-js/*')` themselves, so it
|
|
16
|
+
* is load-bearing for the code that stays; a change that claimed otherwise
|
|
17
|
+
* would be unshippable. What it buys instead is an honest closure —
|
|
18
|
+
* `babel-runtime` is required by those packages and declared by none of them,
|
|
19
|
+
* so today it resolves only because the removed plumbing hoists it. Declaring
|
|
20
|
+
* it turns an accident into a contract.
|
|
21
|
+
*
|
|
22
|
+
* This module reimplements exactly those two layers over the retained metric
|
|
23
|
+
* core — `typhonjs-escomplex-commons`, `escomplex-plugin-metrics-module`,
|
|
24
|
+
* `escomplex-plugin-syntax-babylon`, `typhonjs-ast-walker` — which compute
|
|
25
|
+
* every score. Nothing here computes a metric; the scores come from the same
|
|
26
|
+
* packages as before, which is why they do not move.
|
|
27
|
+
*
|
|
28
|
+
* ## The equivalence contract
|
|
29
|
+
*
|
|
30
|
+
* Reproducing the displaced shell's *behaviour* means reproducing three
|
|
31
|
+
* details it never documented:
|
|
32
|
+
*
|
|
33
|
+
* 1. **The parser's fixed plugin list**, verbatim and in order, with
|
|
34
|
+
* `sourceType: 'unambiguous'` — see `PARSER_PLUGINS` below. The list is
|
|
35
|
+
* what makes a `.ts` file, a decorator or a pipeline operator parse at
|
|
36
|
+
* all, and `unambiguous` is what lets a CommonJS script and an ES module
|
|
37
|
+
* both score.
|
|
38
|
+
* 2. **Both plugin instances, in registration order** — syntax first, then
|
|
39
|
+
* metrics. The metrics plugin reads trait tables the syntax plugin put on
|
|
40
|
+
* the event.
|
|
41
|
+
* 3. **One mutable event object per dispatch**, threaded through every plugin
|
|
42
|
+
* in turn, with the caller reading the mutations back off it. The displaced
|
|
43
|
+
* bus also stamped `$$plugin_invoke_count` / `$$plugin_invoke_names` onto
|
|
44
|
+
* every event's data; no plugin and no report reads them, so they are not
|
|
45
|
+
* reproduced.
|
|
46
|
+
*
|
|
47
|
+
* Equivalence is not asserted by reasoning: `tests/lib/escomplex-kernel.test.js`
|
|
48
|
+
* replays a corpus captured under the displaced kernel *before* it left the
|
|
49
|
+
* tree (`tests/fixtures/escomplex-kernel-parity/`), because afterwards there is
|
|
50
|
+
* nothing left to compare against.
|
|
51
|
+
*
|
|
52
|
+
* ## The one uncontrolled input
|
|
53
|
+
*
|
|
54
|
+
* `.agents/` materializes into a consumer's repository root, so `@babel/parser`
|
|
55
|
+
* resolves from **their** `node_modules`. The preflight guard checks presence,
|
|
56
|
+
* not range, and `@babel/parser@8` is GA and rejects several names in the
|
|
57
|
+
* fixed plugin list. A manifest range documents the requirement; it does not
|
|
58
|
+
* enforce it. So the resolved major is asserted here, at load, with an error
|
|
59
|
+
* that names the problem — rather than surfacing as an opaque plugin-list
|
|
60
|
+
* syntax error partway through a scan.
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
// Every metric-core package is reached by a STATIC import specifier with an
|
|
64
|
+
// explicit `.js` extension. That is not style: `tests/scripts/
|
|
65
|
+
// runtime-deps-drift.test.js` scans for literal `import`/`require` callees, so
|
|
66
|
+
// a package reached through an aliased `createRequire` would be a runtime
|
|
67
|
+
// dependency that is neither declared nor preflighted. The extensions are
|
|
68
|
+
// mandatory because `typhonjs-escomplex-commons` ships an empty
|
|
69
|
+
// `package.json` — no `main`, no `exports` — so a deep path is the only door.
|
|
70
|
+
import { parse as babelParse } from '@babel/parser';
|
|
71
|
+
import PluginMetricsModule from 'escomplex-plugin-metrics-module/dist/PluginMetricsModule.js';
|
|
72
|
+
import PluginSyntaxBabylon from 'escomplex-plugin-syntax-babylon/dist/PluginSyntaxBabylon.js';
|
|
73
|
+
import ASTWalker from 'typhonjs-ast-walker/dist/ASTWalker.js';
|
|
74
|
+
import ModuleScopeControl from 'typhonjs-escomplex-commons/dist/module/report/control/ModuleScopeControl.js';
|
|
75
|
+
import ModuleReport from 'typhonjs-escomplex-commons/dist/module/report/ModuleReport.js';
|
|
76
|
+
import { install as installAstCompat } from './escomplex-ast-compat.js';
|
|
77
|
+
import { describeParserMajorError } from './runtime-deps/parser-major.js';
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The displaced parser shim's plugin list, verbatim and in order.
|
|
81
|
+
*
|
|
82
|
+
* Order is preserved because it is cheap to preserve, not because a
|
|
83
|
+
* reordering is known to matter. The two entries with options
|
|
84
|
+
* (`decorators`, `pipelineOperator`) carry the shim's exact settings —
|
|
85
|
+
* `decoratorsBeforeExport: false` and the `minimal` pipeline proposal — which
|
|
86
|
+
* decide whether decorated classes and `|>` parse. Re-cloned per parse so a
|
|
87
|
+
* parser that mutated its options could not poison a later call.
|
|
88
|
+
*/
|
|
89
|
+
const PARSER_PLUGINS = [
|
|
90
|
+
'asyncGenerators',
|
|
91
|
+
'bigInt',
|
|
92
|
+
'classProperties',
|
|
93
|
+
'classPrivateProperties',
|
|
94
|
+
'classPrivateMethods',
|
|
95
|
+
['decorators', { decoratorsBeforeExport: false }],
|
|
96
|
+
'doExpressions',
|
|
97
|
+
'dynamicImport',
|
|
98
|
+
'exportDefaultFrom',
|
|
99
|
+
'exportNamespaceFrom',
|
|
100
|
+
'functionBind',
|
|
101
|
+
'functionSent',
|
|
102
|
+
'importMeta',
|
|
103
|
+
'jsx',
|
|
104
|
+
'logicalAssignment',
|
|
105
|
+
'nullishCoalescingOperator',
|
|
106
|
+
'numericSeparator',
|
|
107
|
+
'objectRestSpread',
|
|
108
|
+
'optionalCatchBinding',
|
|
109
|
+
'optionalChaining',
|
|
110
|
+
['pipelineOperator', { proposal: 'minimal' }],
|
|
111
|
+
'throwExpressions',
|
|
112
|
+
'typescript',
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The two plugins, in registration order: syntax populates the trait tables
|
|
117
|
+
* the metrics plugin then reads. Neither defines `onPluginLoad`, and the
|
|
118
|
+
* displaced bus was constructed without an eventbus, so there is no plugin
|
|
119
|
+
* lifecycle or eventbus coupling to reproduce — only this list.
|
|
120
|
+
*/
|
|
121
|
+
const PLUGINS = [
|
|
122
|
+
['escomplex-plugin-syntax-babylon', new PluginSyntaxBabylon()],
|
|
123
|
+
['escomplex-plugin-metrics-module', new PluginMetricsModule()],
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
// The kernel's code generator predates the Babel AST its own parser emits, so
|
|
127
|
+
// ordinary modern syntax aborts a WHOLE module — see `escomplex-ast-compat.js`
|
|
128
|
+
// for the defect and the upstream status. Installing at the kernel rather than
|
|
129
|
+
// at each caller makes the next scoring entrypoint correct by construction.
|
|
130
|
+
installAstCompat();
|
|
131
|
+
|
|
132
|
+
const parserProblem = describeParserMajorError();
|
|
133
|
+
if (parserProblem !== null) {
|
|
134
|
+
throw new Error(`[escomplex-kernel] ${parserProblem}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Run one synchronous plugin dispatch.
|
|
139
|
+
*
|
|
140
|
+
* Reproduces the displaced bus's `invokeSyncEvents` for the degenerate shape
|
|
141
|
+
* this kernel uses: no `copyProps` (the shell passed `void 0` at every call
|
|
142
|
+
* site, so the merge base was always `{}`), no eventbus, and the caller
|
|
143
|
+
* reading its results back off the same mutated `data` object every plugin
|
|
144
|
+
* saw.
|
|
145
|
+
*
|
|
146
|
+
* @param {string} method Plugin method name, e.g. `onEnterNode`.
|
|
147
|
+
* @param {object} passthru Properties placed on the event's `data`.
|
|
148
|
+
* @returns {object} The event `data`, after every plugin has mutated it.
|
|
149
|
+
*/
|
|
150
|
+
function dispatch(method, passthru) {
|
|
151
|
+
const event = {
|
|
152
|
+
data: { ...passthru },
|
|
153
|
+
extra: undefined,
|
|
154
|
+
eventbus: undefined,
|
|
155
|
+
pluginName: undefined,
|
|
156
|
+
pluginOptions: undefined,
|
|
157
|
+
};
|
|
158
|
+
for (const [name, instance] of PLUGINS) {
|
|
159
|
+
if (typeof instance[method] !== 'function') continue;
|
|
160
|
+
event.pluginName = name;
|
|
161
|
+
instance[method](event);
|
|
162
|
+
}
|
|
163
|
+
return event.data;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The `ignoreKeys` a syntax trait wants withheld from the walker, if any.
|
|
168
|
+
*
|
|
169
|
+
* @param {object|undefined} syntax The trait entry for this node type.
|
|
170
|
+
* @param {object} node
|
|
171
|
+
* @param {object} parent
|
|
172
|
+
* @returns {string[]}
|
|
173
|
+
*/
|
|
174
|
+
function traitIgnoreKeys(syntax, node, parent) {
|
|
175
|
+
return typeof syntax === 'object' && syntax?.ignoreKeys
|
|
176
|
+
? syntax.ignoreKeys.valueOf(node, parent)
|
|
177
|
+
: [];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The new scope a syntax trait opens at this node, if any.
|
|
182
|
+
*
|
|
183
|
+
* @param {object|undefined} syntax The trait entry for this node type.
|
|
184
|
+
* @param {object} node
|
|
185
|
+
* @param {object} parent
|
|
186
|
+
* @returns {object|null}
|
|
187
|
+
*/
|
|
188
|
+
function traitNewScope(syntax, node, parent) {
|
|
189
|
+
if (typeof syntax !== 'object' || !syntax?.newScope) return null;
|
|
190
|
+
return syntax.newScope.valueOf(node, parent) ?? null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Build the walker visitor for one module traversal.
|
|
195
|
+
*
|
|
196
|
+
* Split out of {@link analyzeModule} so the enter/exit symmetry is readable
|
|
197
|
+
* side by side: each resolves the trait's scope, brackets the `scopeControl`
|
|
198
|
+
* mutation with a pre/post dispatch, and straddles it with the node dispatch
|
|
199
|
+
* in opposite order on the way in and out.
|
|
200
|
+
*
|
|
201
|
+
* The two event shapes are not interchangeable, and the difference is the
|
|
202
|
+
* displaced shell's, not a simplification available here: node events carry
|
|
203
|
+
* `syntaxes` and scope events do not, and a scope event names its scope
|
|
204
|
+
* `newScope` on the way in but `scope` on the way out.
|
|
205
|
+
*
|
|
206
|
+
* @param {{
|
|
207
|
+
* moduleReport: object,
|
|
208
|
+
* scopeControl: object,
|
|
209
|
+
* syntaxes: Record<string, object>,
|
|
210
|
+
* settings: object,
|
|
211
|
+
* }} context
|
|
212
|
+
* @returns {{enterNode: Function, exitNode: Function}}
|
|
213
|
+
*/
|
|
214
|
+
function buildVisitor({ moduleReport, scopeControl, syntaxes, settings }) {
|
|
215
|
+
const nodeBase = { moduleReport, scopeControl, syntaxes, settings };
|
|
216
|
+
const scopeBase = { moduleReport, scopeControl, settings };
|
|
217
|
+
return {
|
|
218
|
+
enterNode(node, parent) {
|
|
219
|
+
const syntax = syntaxes[node.type];
|
|
220
|
+
const event = dispatch('onEnterNode', {
|
|
221
|
+
...nodeBase,
|
|
222
|
+
ignoreKeys: traitIgnoreKeys(syntax, node, parent),
|
|
223
|
+
node,
|
|
224
|
+
parent,
|
|
225
|
+
});
|
|
226
|
+
const ignoreKeys = event !== null ? event.ignoreKeys : [];
|
|
227
|
+
const newScope = traitNewScope(syntax, node, parent);
|
|
228
|
+
if (newScope) {
|
|
229
|
+
const scoped = { ...scopeBase, newScope, node, parent };
|
|
230
|
+
dispatch('onModulePreScopeCreated', scoped);
|
|
231
|
+
scopeControl.createScope(newScope);
|
|
232
|
+
dispatch('onModulePostScopeCreated', scoped);
|
|
233
|
+
}
|
|
234
|
+
return ignoreKeys;
|
|
235
|
+
},
|
|
236
|
+
exitNode(node, parent) {
|
|
237
|
+
const syntax = syntaxes[node.type];
|
|
238
|
+
const newScope = traitNewScope(syntax, node, parent);
|
|
239
|
+
if (newScope) {
|
|
240
|
+
const scoped = { ...scopeBase, scope: newScope, node, parent };
|
|
241
|
+
dispatch('onModulePreScopePopped', scoped);
|
|
242
|
+
scopeControl.popScope(newScope);
|
|
243
|
+
dispatch('onModulePostScopePopped', scoped);
|
|
244
|
+
}
|
|
245
|
+
dispatch('onExitNode', { ...nodeBase, node, parent });
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Parse and score one module.
|
|
252
|
+
*
|
|
253
|
+
* Drop-in replacement for the displaced `escomplex.analyzeModule(source)`:
|
|
254
|
+
* same report object, same `finalize()` shape, same thrown errors for source
|
|
255
|
+
* the kernel cannot handle.
|
|
256
|
+
*
|
|
257
|
+
* @param {string} source JavaScript (or TypeScript) source text.
|
|
258
|
+
* @param {object} [options] Passed to the plugins' `onConfigure`, as before.
|
|
259
|
+
* @returns {object} The finalized module report.
|
|
260
|
+
* @throws {SyntaxError} Propagated from the parser, as before.
|
|
261
|
+
*/
|
|
262
|
+
export function analyzeModule(source, options = {}) {
|
|
263
|
+
const ast = babelParse(source, {
|
|
264
|
+
plugins: structuredClone(PARSER_PLUGINS),
|
|
265
|
+
sourceType: 'unambiguous',
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const settings = dispatch('onConfigure', { options, settings: {} }).settings;
|
|
269
|
+
Object.freeze(settings);
|
|
270
|
+
const syntaxes = dispatch('onLoadSyntax', {
|
|
271
|
+
settings,
|
|
272
|
+
syntaxes: {},
|
|
273
|
+
}).syntaxes;
|
|
274
|
+
|
|
275
|
+
const moduleReport = new ModuleReport(
|
|
276
|
+
ast.loc.start.line,
|
|
277
|
+
ast.loc.end.line,
|
|
278
|
+
settings,
|
|
279
|
+
);
|
|
280
|
+
dispatch('onModuleStart', { ast, moduleReport, syntaxes, settings });
|
|
281
|
+
|
|
282
|
+
const scopeControl = new ModuleScopeControl(moduleReport);
|
|
283
|
+
new ASTWalker().traverse(
|
|
284
|
+
ast,
|
|
285
|
+
buildVisitor({ moduleReport, scopeControl, syntaxes, settings }),
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
for (const phase of [
|
|
289
|
+
'onModuleCalculate',
|
|
290
|
+
'onModuleAverage',
|
|
291
|
+
'onModulePostAverage',
|
|
292
|
+
'onModuleEnd',
|
|
293
|
+
]) {
|
|
294
|
+
dispatch(phase, { moduleReport, syntaxes, settings });
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return moduleReport.finalize();
|
|
298
|
+
}
|
|
@@ -290,18 +290,19 @@ export function runChild({
|
|
|
290
290
|
|
|
291
291
|
/**
|
|
292
292
|
* Build an `isAutoFileEnabled(config)` reader bound to a specific
|
|
293
|
-
* `delivery.feedbackLoop.<key>` toggle. The feature is opt-
|
|
294
|
-
* toggle defaults to `
|
|
293
|
+
* `delivery.feedbackLoop.<key>` toggle. The feature is **opt-in** since Story
|
|
294
|
+
* #5341: the toggle defaults to `false` and only an explicit `true` enables
|
|
295
|
+
* it, because the filings the channel produced unattended were dominated by
|
|
296
|
+
* noise (see `config-settings-schema-delivery.js` for the measured record).
|
|
295
297
|
*
|
|
296
298
|
* @param {string} toggleKey — key under `config.delivery.feedbackLoop`
|
|
297
|
-
* (
|
|
299
|
+
* (today, only "retroProposals" — the factory stays keyed so the next
|
|
300
|
+
* graduator does not have to reinvent the resolution)
|
|
298
301
|
* @returns {(config: object|undefined|null) => boolean}
|
|
299
302
|
*/
|
|
300
303
|
export function makeIsAutoFileEnabled(toggleKey) {
|
|
301
304
|
return function isAutoFileEnabled(config) {
|
|
302
|
-
|
|
303
|
-
if (value === false) return false;
|
|
304
|
-
return true;
|
|
305
|
+
return config?.delivery?.feedbackLoop?.[toggleKey] === true;
|
|
305
306
|
};
|
|
306
307
|
}
|
|
307
308
|
|
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
* is gone from the whole path, not just from this module. The `meta::<framework-gap|consumer-improvement>` +
|
|
29
29
|
* `friction::<category>` labels are lifted verbatim from the routed item.
|
|
30
30
|
*
|
|
31
|
-
* Behind the `delivery.feedbackLoop.retroProposals` toggle (default
|
|
32
|
-
* per `graduator-core.js#makeIsAutoFileEnabled`). NEVER
|
|
33
|
-
* failure path is captured in `errors[]`.
|
|
31
|
+
* Behind the `delivery.feedbackLoop.retroProposals` toggle (default OFF
|
|
32
|
+
* since Story #5341, per `graduator-core.js#makeIsAutoFileEnabled`). NEVER
|
|
33
|
+
* throws — every failure path is captured in `errors[]`.
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
36
|
import {
|
|
@@ -46,8 +46,10 @@ import {
|
|
|
46
46
|
} from './graduator-core.js';
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
|
-
* Resolve the toggle from the resolved agentrc config. Defaults to `
|
|
50
|
-
* — the feature is opt-
|
|
49
|
+
* Resolve the toggle from the resolved agentrc config. Defaults to `false`
|
|
50
|
+
* — the feature is opt-in (Story #5341). It is now the only such toggle: the
|
|
51
|
+
* audit-results sibling was removed by Story #5366, its graduator having been
|
|
52
|
+
* deleted two releases earlier.
|
|
51
53
|
*
|
|
52
54
|
* @param {object|undefined|null} config
|
|
53
55
|
* @returns {boolean}
|