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.
Files changed (124) hide show
  1. package/.agents/README.md +17 -12
  2. package/.agents/agents/acceptance-critic.md +24 -43
  3. package/.agents/agents/story-worker.md +18 -19
  4. package/.agents/docs/SDLC.md +12 -13
  5. package/.agents/docs/agentrc-reference.json +1 -2
  6. package/.agents/docs/configuration.md +29 -46
  7. package/.agents/docs/quality-gates.md +9 -5
  8. package/.agents/docs/workflows.md +1 -1
  9. package/.agents/instructions.md +5 -7
  10. package/.agents/rules/ci-remediation.md +41 -8
  11. package/.agents/rules/known-tooling-behavior.md +65 -15
  12. package/.agents/runtime-deps.json +7 -2
  13. package/.agents/schemas/acceptance-eval-verdict.schema.json +1 -1
  14. package/.agents/schemas/agentrc.schema.json +6 -11
  15. package/.agents/schemas/crap-baseline.schema.json +1 -1
  16. package/.agents/schemas/crap-report.schema.json +1 -1
  17. package/.agents/schemas/story-deliver-terminal.schema.json +3 -3
  18. package/.agents/scripts/README.md +11 -1
  19. package/.agents/scripts/acceptance-eval.js +25 -27
  20. package/.agents/scripts/ceremony-derive.js +15 -10
  21. package/.agents/scripts/check-context-budget.js +148 -228
  22. package/.agents/scripts/check-schema-references.js +5 -3
  23. package/.agents/scripts/check-workflow-citations.js +33 -147
  24. package/.agents/scripts/coverage-capture.js +7 -4
  25. package/.agents/scripts/deliver-light.js +41 -100
  26. package/.agents/scripts/deliver-run.js +631 -0
  27. package/.agents/scripts/file-ci-gap.js +59 -11
  28. package/.agents/scripts/install-matrix-assert.js +48 -3
  29. package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +51 -33
  30. package/.agents/scripts/lib/baselines/crap-preview-incremental.js +6 -2
  31. package/.agents/scripts/lib/baselines/kinds/_crap-read.js +0 -8
  32. package/.agents/scripts/lib/baselines/kinds/crap.js +35 -18
  33. package/.agents/scripts/lib/changed-files.js +30 -0
  34. package/.agents/scripts/lib/config/delivery-routing.js +5 -4
  35. package/.agents/scripts/lib/config/explain.js +1 -3
  36. package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +1 -1
  37. package/.agents/scripts/lib/config-resolver.js +1 -0
  38. package/.agents/scripts/lib/config-settings-schema-delivery.js +28 -21
  39. package/.agents/scripts/lib/coverage-capture-fullscope.js +10 -2
  40. package/.agents/scripts/lib/coverage-capture-incremental.js +3 -2
  41. package/.agents/scripts/lib/coverage-capture-usage.js +4 -1
  42. package/.agents/scripts/lib/crap-engine.js +2 -2
  43. package/.agents/scripts/lib/crap-utils.js +21 -5
  44. package/.agents/scripts/lib/doc-tiers.js +4 -2
  45. package/.agents/scripts/lib/escomplex-ast-compat.js +39 -17
  46. package/.agents/scripts/lib/escomplex-kernel.js +298 -0
  47. package/.agents/scripts/lib/feedback-loop/graduator-core.js +7 -6
  48. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +7 -5
  49. package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
  50. package/.agents/scripts/lib/gh-exec.js +160 -0
  51. package/.agents/scripts/lib/maintainability-engine.js +3 -3
  52. package/.agents/scripts/lib/observability/source-classifier.js +1 -0
  53. package/.agents/scripts/lib/orchestration/ceremony-routing.js +74 -132
  54. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +123 -12
  55. package/.agents/scripts/lib/orchestration/complexity-gate.js +180 -352
  56. package/.agents/scripts/lib/orchestration/light-suitability.js +71 -136
  57. package/.agents/scripts/lib/orchestration/plan-context.js +44 -50
  58. package/.agents/scripts/lib/orchestration/plan-persist/changes-repair.js +8 -6
  59. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +104 -119
  60. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +41 -25
  61. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +11 -11
  62. package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +63 -29
  63. package/.agents/scripts/lib/orchestration/plan-persist/wave-collision-gate.js +107 -0
  64. package/.agents/scripts/lib/orchestration/review-depth.js +14 -11
  65. package/.agents/scripts/lib/orchestration/run-epilogue.js +260 -182
  66. package/.agents/scripts/lib/orchestration/run-scoped-config.js +63 -99
  67. package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +3 -3
  68. package/.agents/scripts/lib/orchestration/single-story-close/phases/graphql-preflight.js +137 -0
  69. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +105 -18
  70. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +4 -3
  71. package/.agents/scripts/lib/orchestration/story-follow-ups.js +156 -39
  72. package/.agents/scripts/lib/orchestration/story-init-envelope.js +71 -0
  73. package/.agents/scripts/lib/orchestration/task-body-validator.js +8 -17
  74. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +25 -209
  75. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +8 -5
  76. package/.agents/scripts/lib/orchestration/ticket-validator.js +44 -183
  77. package/.agents/scripts/lib/orchestration/ticketing/reads.js +14 -25
  78. package/.agents/scripts/lib/runtime-deps/dep-resolution.js +155 -0
  79. package/.agents/scripts/lib/runtime-deps/ensure-installed.js +44 -9
  80. package/.agents/scripts/lib/runtime-deps/parser-major.js +110 -0
  81. package/.agents/scripts/lib/runtime-deps/preflight.js +6 -25
  82. package/.agents/scripts/lib/runtime-deps/scan-imports.js +46 -1
  83. package/.agents/scripts/lib/skills/walk-skill-files.js +1 -1
  84. package/.agents/scripts/lib/story-body/body-format-lints.js +58 -12
  85. package/.agents/scripts/lib/story-body/story-body.js +83 -29
  86. package/.agents/scripts/lib/templates/decomposer-prompts.js +28 -33
  87. package/.agents/scripts/lib/wave-runner/live-probe.js +31 -5
  88. package/.agents/scripts/merge-baseline.js +4 -5
  89. package/.agents/scripts/plan-context.js +117 -28
  90. package/.agents/scripts/plan-persist.js +79 -39
  91. package/.agents/scripts/plan-run-epilogue.js +11 -8
  92. package/.agents/scripts/pr-watch-with-update.js +9 -2
  93. package/.agents/scripts/run-verify.js +13 -6
  94. package/.agents/scripts/single-story-init.js +7 -57
  95. package/.agents/scripts/stories-wave-tick.js +160 -26
  96. package/.agents/skills/core/gates-and-baselines/reference.md +0 -1
  97. package/.agents/skills/skills.index.json +2 -12
  98. package/.agents/skills/stack/qa/playwright/SKILL.md +26 -0
  99. package/.agents/workflows/audit-to-stories.md +14 -11
  100. package/.agents/workflows/helpers/acceptance-self-eval.md +84 -157
  101. package/.agents/workflows/helpers/code-review.md +4 -2
  102. package/.agents/workflows/helpers/deliver-digest.md +31 -24
  103. package/.agents/workflows/helpers/deliver-light.md +92 -101
  104. package/.agents/workflows/helpers/deliver-reference.md +116 -100
  105. package/.agents/workflows/helpers/deliver-story-reference.md +58 -124
  106. package/.agents/workflows/helpers/deliver-story.md +17 -18
  107. package/.agents/workflows/helpers/plan-reference.md +82 -60
  108. package/.agents/workflows/mandrel-deliver.md +47 -31
  109. package/.agents/workflows/mandrel-plan.md +32 -30
  110. package/.agents/workflows/mandrel-update.md +36 -21
  111. package/README.md +3 -3
  112. package/docs/CHANGELOG.md +43 -0
  113. package/lib/cli/registry.js +45 -25
  114. package/lib/cli/update.js +376 -17
  115. package/lib/migrations/index.js +2 -0
  116. package/lib/migrations/steps/2.60.0-retire-audit-results-autofile.js +40 -0
  117. package/package.json +8 -2
  118. package/.agents/schemas/model-attribution.schema.json +0 -53
  119. package/.agents/scripts/lib/orchestration/model-attribution.js +0 -418
  120. package/.agents/scripts/lib/orchestration/split-policy-validator.js +0 -188
  121. package/.agents/scripts/lib/orchestration/story-plan-state.js +0 -33
  122. package/.agents/scripts/lib/orchestration/structured-comment-parser.js +0 -67
  123. package/.agents/scripts/lib/templates/spec-author-prompts.js +0 -76
  124. 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({ ref: args.ref, cwd: args.cwd });
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.incrementalCoverage.baseRef || args.ref;
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
- ['--ref <git-ref>', 'Git ref the changed-file set is computed against.'],
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 = escomplex.analyzeModule(source);
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
- * Resolve the running `typhonjs-escomplex` version by walking up from `cwd`
40
- * and reading the nearest `node_modules/typhonjs-escomplex/package.json`.
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
- 'typhonjs-escomplex',
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 = escomplex.analyzeModule(source);
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 budgeted independently (per-file ≤8KB
31
- * ceiling gated by `check-context-budget.js`).
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
- * `typhonjs-escomplex` parses with `@typhonjs/babel-parser`, so every AST it
8
- * analyses is a **Babel** AST. But `typhonjs-escomplex-commons`'
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* `typhonjs-escomplex-commons` instance the
81
- * kernel loads, so `commons` is resolved **through `typhonjs-escomplex`'s own
82
- * resolution** rather than from here. Resolving it directly would be a coin
83
- * flip: under a hoisting installer it usually finds the same copy, but under
84
- * pnpm's isolated layout — or as soon as anything declares `commons` directly —
85
- * it can find a *different* physical copy, and the patch then lands on a table
86
- * nobody reads while `install()` cheerfully reports success. Anchoring makes
87
- * that failure mode unreachable.
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 escomplex.
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 fromKernel = createRequire(requireFn.resolve('typhonjs-escomplex'));
98
- const mod = fromKernel(
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 kernel for 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 fromKernel = createRequire(require.resolve('typhonjs-escomplex'));
351
- const mod = fromKernel(
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-out: the
294
- * toggle defaults to `true` and only an explicit `false` disables it.
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
- * (e.g. "auditResultsAutoFile", "retroProposals")
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
- const value = config?.delivery?.feedbackLoop?.[toggleKey];
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 ON,
32
- * per `graduator-core.js#makeIsAutoFileEnabled`). NEVER throws — every
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 `true`
50
- * — the feature is opt-out (mirrors auditResultsAutoFile).
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}