session-orchestrator 3.20.0 → 3.22.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 (202) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor/rules/000-session-orchestrator.mdc +3 -2
  5. package/.cursor/rules/030-wave-execution.mdc +10 -8
  6. package/.cursor/rules/040-discovery.mdc +6 -6
  7. package/.cursor/rules/050-plan.mdc +8 -8
  8. package/CHANGELOG.md +515 -0
  9. package/README.md +16 -11
  10. package/agents/analyst.md +1 -1
  11. package/agents/architect-reviewer.md +1 -1
  12. package/agents/code-implementer.md +4 -2
  13. package/agents/db-specialist.md +1 -1
  14. package/agents/dialectic-deriver.md +1 -1
  15. package/agents/docs-writer.md +1 -1
  16. package/agents/memory-proposal-collector.md +7 -5
  17. package/agents/qa-strategist.md +1 -1
  18. package/agents/security-reviewer.md +1 -1
  19. package/agents/session-reviewer.md +42 -1
  20. package/agents/skill-applied-judge.md +1 -1
  21. package/agents/test-writer.md +1 -1
  22. package/agents/ui-developer.md +1 -1
  23. package/agents/ux-evaluator.md +1 -1
  24. package/commands/eli5.md +33 -0
  25. package/commands/release.md +62 -0
  26. package/commands/test.md +2 -2
  27. package/docs/components.md +6 -5
  28. package/docs/migration-v3.md +9 -6
  29. package/docs/persona-panel.md +3 -1
  30. package/docs/scope-collision-guard.md +167 -0
  31. package/docs/session-config-reference.md +31 -8
  32. package/hooks/_lib/lock-bootstrap.mjs +19 -13
  33. package/hooks/enforce-scope.mjs +103 -3
  34. package/hooks/hooks-codex.json +1 -1
  35. package/hooks/hooks.json +21 -1
  36. package/hooks/on-session-end.mjs +76 -97
  37. package/hooks/on-session-start.mjs +195 -104
  38. package/hooks/on-stop.mjs +127 -12
  39. package/hooks/post-bash-write-verify.mjs +8 -32
  40. package/hooks/pre-auq-clarity.mjs +787 -0
  41. package/hooks/pre-bash-issue-budget.mjs +17 -18
  42. package/hooks/pre-task-scope-disjoint.mjs +1042 -0
  43. package/package.json +3 -1
  44. package/pi/prompts/eli5.md +12 -0
  45. package/pi/prompts/release.md +12 -0
  46. package/scripts/auq-audit.mjs +825 -0
  47. package/scripts/autopilot.mjs +10 -9
  48. package/scripts/emit-session.mjs +42 -0
  49. package/scripts/export-hw-learnings.mjs +61 -2
  50. package/scripts/lib/auq/clarity.mjs +1314 -0
  51. package/scripts/lib/auq/parse.mjs +1006 -0
  52. package/scripts/lib/auq/schema.mjs +1457 -0
  53. package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
  54. package/scripts/lib/backlog-scan.mjs +106 -15
  55. package/scripts/lib/build-live-signals.mjs +7 -3
  56. package/scripts/lib/ci-status-banner.mjs +267 -77
  57. package/scripts/lib/config/dispatcher-autonomy-capture.mjs +32 -9
  58. package/scripts/lib/config/vault-integration.mjs +12 -1
  59. package/scripts/lib/dispatcher/rank.mjs +4 -7
  60. package/scripts/lib/gates/gate-full.mjs +3 -3
  61. package/scripts/lib/gates/gate-helpers.mjs +17 -6
  62. package/scripts/lib/git-config-drift.mjs +471 -0
  63. package/scripts/lib/harness-audit/categories/category6.mjs +65 -12
  64. package/scripts/lib/io.mjs +432 -7
  65. package/scripts/lib/issue-budget.mjs +63 -9
  66. package/scripts/lib/learnings/select.mjs +157 -3
  67. package/scripts/lib/memory-cleanup-stamp.mjs +132 -8
  68. package/scripts/lib/mirror-issues-banner.mjs +266 -0
  69. package/scripts/lib/named-vault-resolver.mjs +105 -16
  70. package/scripts/lib/owner-interview.mjs +78 -32
  71. package/scripts/lib/peer-cards/schema.mjs +6 -2
  72. package/scripts/lib/peer-discovery.mjs +73 -22
  73. package/scripts/lib/project-hygiene.mjs +64 -4
  74. package/scripts/lib/reconcile/renderer.mjs +17 -4
  75. package/scripts/lib/reconcile/writer.mjs +69 -30
  76. package/scripts/lib/redact-spans.mjs +89 -0
  77. package/scripts/lib/resource-probe/evaluate.mjs +330 -149
  78. package/scripts/lib/resource-probe/probe-platform.mjs +35 -0
  79. package/scripts/lib/resource-probe.mjs +18 -2
  80. package/scripts/lib/scope-baseline.mjs +77 -17
  81. package/scripts/lib/scope-gate.mjs +658 -0
  82. package/scripts/lib/secret-masker.mjs +262 -0
  83. package/scripts/lib/session-lock.mjs +34 -10
  84. package/scripts/lib/session-registry.mjs +9 -1
  85. package/scripts/lib/spiral-carryover.mjs +23 -2
  86. package/scripts/lib/state-md/mission-status.mjs +164 -58
  87. package/scripts/lib/tmux-layout/vcs-detector.mjs +108 -4
  88. package/scripts/lib/validate/check-agents.mjs +77 -5
  89. package/scripts/lib/validate/check-auq-clarity.mjs +274 -0
  90. package/scripts/lib/validate/check-commands.mjs +2 -20
  91. package/scripts/lib/validate/check-doc-cli-commands.mjs +514 -0
  92. package/scripts/lib/validate/check-hooks-symmetry.mjs +48 -0
  93. package/scripts/lib/validate/check-owner-leakage.mjs +185 -17
  94. package/scripts/lib/validate/check-rules.mjs +153 -9
  95. package/scripts/lib/validate/check-skills.mjs +191 -0
  96. package/scripts/lib/validate/check-test-git-config-target.mjs +665 -0
  97. package/scripts/lib/validate/check-unicode-safety.mjs +22 -2
  98. package/scripts/lib/validate/check-untracked-test-deps.mjs +925 -0
  99. package/scripts/lib/validate/check-unwired-features.mjs +219 -11
  100. package/scripts/lib/validate/check-vcs-repo-flag.mjs +965 -0
  101. package/scripts/lib/validate/frontmatter-block.mjs +61 -0
  102. package/scripts/lib/validate/tier-inference.mjs +46 -8
  103. package/scripts/lib/vault-backfill/glab.mjs +91 -58
  104. package/scripts/lib/vault-backfill/manifest.mjs +28 -8
  105. package/scripts/lib/vault-mirror/namespace.mjs +146 -1
  106. package/scripts/lib/vault-mirror/process.mjs +264 -31
  107. package/scripts/lib/vault-mirror/render-sessions.mjs +115 -4
  108. package/scripts/lib/vault-status/board-writer.mjs +300 -56
  109. package/scripts/lib/vault-status/narrative-mirror.mjs +119 -5
  110. package/scripts/lib/vcs-repo-spec.mjs +680 -30
  111. package/scripts/lib/wave-resource-gate.mjs +67 -73
  112. package/scripts/materialize-wave-scope.mjs +281 -0
  113. package/scripts/print-learnings-index.mjs +30 -3
  114. package/scripts/release.mjs +983 -107
  115. package/scripts/run-quality-gate.mjs +14 -0
  116. package/scripts/site-numbers.mjs +1049 -0
  117. package/scripts/validate-plugin.mjs +64 -0
  118. package/scripts/validate-wave-scope.mjs +286 -12
  119. package/scripts/vault-backfill.mjs +32 -5
  120. package/scripts/vault-mirror.mjs +26 -1
  121. package/skills/_shared/monitor-patterns.md +24 -4
  122. package/skills/_shared/parallel-aware-auq.md +30 -24
  123. package/skills/_shared/parallel-aware-preamble.md +31 -2
  124. package/skills/_shared/state-ownership.md +49 -6
  125. package/skills/bootstrap/SKILL.md +2 -1
  126. package/skills/brainstorm/SKILL.md +18 -18
  127. package/skills/brainstorm/soul.md +12 -0
  128. package/skills/claude-md-drift-check/SKILL.md +9 -1
  129. package/skills/debug/SKILL.md +4 -1
  130. package/skills/discovery/SKILL.md +28 -24
  131. package/skills/discovery/issue-templates.md +4 -4
  132. package/skills/discovery/probes-code.md +2 -2
  133. package/skills/discovery/probes-feature.md +6 -6
  134. package/skills/discovery/probes-infra.md +2 -2
  135. package/skills/discovery/probes-session.md +5 -5
  136. package/skills/dispatcher/SKILL.md +10 -1
  137. package/skills/eli5/SKILL.md +43 -0
  138. package/skills/evolve/SKILL.md +8 -9
  139. package/skills/frontmatter-guard/SKILL.md +9 -1
  140. package/skills/gitlab-ops/SKILL.md +73 -59
  141. package/skills/gitlab-portfolio/SKILL.md +10 -1
  142. package/skills/grill/SKILL.md +6 -6
  143. package/skills/grill/soul.md +16 -0
  144. package/skills/memory-cleanup/SKILL.md +20 -7
  145. package/skills/npm-publish/SKILL.md +23 -51
  146. package/skills/peekaboo-driver/SKILL.md +3 -3
  147. package/skills/persona-panel/SKILL.md +3 -1
  148. package/skills/plan/SKILL.md +18 -16
  149. package/skills/plan/mode-feature.md +1 -1
  150. package/skills/plan/mode-new.md +42 -12
  151. package/skills/plan/soul.md +12 -0
  152. package/skills/reconcile/SKILL.md +3 -3
  153. package/skills/repo-audit/SKILL.md +10 -1
  154. package/skills/session-end/SKILL.md +97 -22
  155. package/skills/session-end/metrics-collection.md +1 -1
  156. package/skills/session-end/phase-3-6-tail.md +37 -2
  157. package/skills/session-end/session-metrics-write.md +4 -10
  158. package/skills/session-plan/SKILL.md +2 -2
  159. package/skills/session-plan/wave-template.md +1 -1
  160. package/skills/session-start/SKILL.md +82 -36
  161. package/skills/session-start/phase-2-5-docs-planning.md +8 -8
  162. package/skills/session-start/phase-4-5-resource-health.md +82 -19
  163. package/skills/session-start/soul.md +110 -0
  164. package/skills/spinout/SKILL.md +5 -1
  165. package/skills/sunset-review/SKILL.md +11 -1
  166. package/skills/test-runner/SKILL.md +2 -2
  167. package/skills/tmux-layout/SKILL.md +7 -2
  168. package/skills/using-orchestrator/SKILL.md +1 -1
  169. package/skills/vault-mirror/SKILL.md +10 -1
  170. package/skills/vault-sync/SKILL.md +10 -1
  171. package/skills/vault-sync/validator.mjs +55 -6
  172. package/skills/wave-executor/wave-loop.md +64 -12
  173. package/skills/write-executable-plan/SKILL.md +6 -6
  174. package/scripts/lib/mission-status-schema.mjs +0 -114
  175. package/scripts/tests/fixtures/fetch-baseline/sample-rule.md +0 -8
  176. package/skills/vault-sync/tests/fixtures/archive-test-vault/90-archive/bad-archived.md +0 -8
  177. package/skills/vault-sync/tests/fixtures/archive-test-vault/_meta/.gitkeep +0 -0
  178. package/skills/vault-sync/tests/fixtures/archive-test-vault/live-note.md +0 -8
  179. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/_meta/.gitkeep +0 -0
  180. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/bad-type.md +0 -8
  181. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/good-note.md +0 -8
  182. package/skills/vault-sync/tests/fixtures/clean-vault/.obsidian/config.md +0 -8
  183. package/skills/vault-sync/tests/fixtures/clean-vault/01-projects/foo/projects-baseline.md +0 -10
  184. package/skills/vault-sync/tests/fixtures/clean-vault/03-daily/daily-2026-04-13.md +0 -8
  185. package/skills/vault-sync/tests/fixtures/clean-vault/README.md +0 -3
  186. package/skills/vault-sync/tests/fixtures/clean-vault/hello-world.md +0 -11
  187. package/skills/vault-sync/tests/fixtures/dangling-link-vault/_meta/.gitkeep +0 -0
  188. package/skills/vault-sync/tests/fixtures/dangling-link-vault/has-dangling.md +0 -9
  189. package/skills/vault-sync/tests/fixtures/dangling-link-vault/real-target.md +0 -8
  190. package/skills/vault-sync/tests/fixtures/empty-vault/_meta/.gitkeep +0 -0
  191. package/skills/vault-sync/tests/fixtures/missing-field-vault/_meta/.gitkeep +0 -0
  192. package/skills/vault-sync/tests/fixtures/missing-field-vault/missing-id.md +0 -7
  193. package/skills/vault-sync/tests/fixtures/nested-tag-vault/03-daily/daily-2026-04-13.md +0 -9
  194. package/skills/vault-sync/tests/fixtures/nested-tag-vault/_meta/.gitkeep +0 -0
  195. package/skills/vault-sync/tests/fixtures/nested-tag-vault/nested-tags-note.md +0 -11
  196. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/README.md +0 -3
  197. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_MOC.md +0 -3
  198. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_meta/.gitkeep +0 -0
  199. package/skills/vault-sync/tests/fixtures/with-moc-vault/_MOC.md +0 -11
  200. package/skills/vault-sync/tests/fixtures/with-moc-vault/_meta/.gitkeep +0 -0
  201. package/skills/vault-sync/tests/fixtures/with-moc-vault/hello-world.md +0 -11
  202. package/skills/vault-sync/tests/schema-drift.test.mjs +0 -133
@@ -50,6 +50,65 @@
50
50
  * through the parser layer to become a value. Measured 2026-08-08: 84 of 89
51
51
  * top-level keys satisfy it, so the 5 that do not are signal, not noise.
52
52
  *
53
+ * ## S3 `orphaned-prose-module` — the same disease, one level out
54
+ *
55
+ * A config key is not the only thing prose can promise. A DOCUMENT can also
56
+ * assert that a module does a job that nothing calls. S3 reports a module under
57
+ * `scripts/`/`hooks/` that satisfies ALL of:
58
+ *
59
+ * 1. its basename is named in tracked, non-historical prose,
60
+ * 2. NO other production module references that basename on a non-comment line,
61
+ * 3. it is not a CLI entrypoint (no shebang, no main-guard),
62
+ * 4. it exports at least one named symbol, and
63
+ * 5. the prose naming it names NONE of those exported symbols.
64
+ *
65
+ * Condition 5 is the discriminator, and it is a claim about GRAMMAR. "dispatch
66
+ * via `runWavePool()`" names a symbol: it is an INSTRUCTION addressed to a reader
67
+ * who will execute it, which is legitimate prose-wiring. "transitions **are
68
+ * validated** against `foo.mjs`" names only the file, in the passive voice: it
69
+ * ASSERTS that something happens by itself. Nobody is addressed, so nobody does
70
+ * it. Passive + bare filename + zero symbols is the signature of a dead promise.
71
+ *
72
+ * ### Why this is a narrow rule and not an export census
73
+ *
74
+ * The obvious broader check — "report every export with no non-test importer" —
75
+ * was measured on 2026-08-14 and is NOT buildable: 1366 exports, 779 without a
76
+ * non-test importer, a false-positive rate of 93.2% naive and still 81.2% after
77
+ * four exclusion rules. A gate that prints 282 lines gets switched off in week
78
+ * two, which is this file's own disease one level up. S3 trades that recall for
79
+ * precision: it only fires where prose made a CLAIM, so every hit has a document
80
+ * to correct.
81
+ *
82
+ * ### Honest limitation: the population is tiny, by construction
83
+ *
84
+ * The measured cascade on 2026-08-14 was 452 production modules → 329 named in
85
+ * prose → 95 with no production reference → 57 non-entrypoint → 56 with a named
86
+ * export → **2**. Do not read a near-empty report as a broken check: S3 is a
87
+ * RELAPSE GUARD, not a cleanup tool. Its value is catching the NEXT false
88
+ * promise on the day it is written, not finding mass today.
89
+ *
90
+ * ### Two false-positive classes this rule was calibrated against
91
+ *
92
+ * Both were live hits in the first draft, and both are now excluded by
93
+ * construction — reintroducing either would be a regression:
94
+ *
95
+ * - **Dynamic-import consumers.** `scripts/lib/skill-health/join.mjs` looks
96
+ * orphaned to any `from '…join.mjs'` regex: `harness-audit/categories/
97
+ * category9.mjs` resolves it via `new URL('../../skill-health/join.mjs',
98
+ * import.meta.url)` and imports the resulting VARIABLE inside a generated
99
+ * child-process source string. Condition 2 therefore counts any non-comment
100
+ * mention of the basename as a reference, not just a static import specifier.
101
+ * - **Re-export shims.** `scripts/lib/autopilot-telemetry.mjs` is
102
+ * `export * from './autopilot/telemetry.mjs'` — zero NAMED exports, so
103
+ * condition 5 ("prose names none of its exports") is vacuously true and the
104
+ * module is reported for having no symbols to name. Condition 4 excludes it.
105
+ *
106
+ * `CHANGELOG.md` is excluded from the prose corpus for the same reason: it is an
107
+ * append-only record of what a PAST release shipped, so it names the symbols of
108
+ * code that may since have died. Counting it silenced a true positive
109
+ * (`soul-resolve.mjs`, whose only live claim is in `.claude/rules/owner-persona.md`
110
+ * but whose symbols appear in a 2026-06 changelog entry).
111
+ *
53
112
  * ## Consumer scope, and why "prose-only" is a finding rather than an error
54
113
  *
55
114
  * Read sites are counted in `scripts/**` and `hooks/**` (`.mjs`/`.js`/`.cjs`),
@@ -129,6 +188,29 @@ const CODE_EXTENSIONS = Object.freeze(['.mjs', '.js', '.cjs']);
129
188
  /** Directory names excluded from the consumer scan at any depth. */
130
189
  const EXCLUDED_DIRS = Object.freeze(['node_modules', '.git', 'tests', 'test', '__tests__']);
131
190
 
191
+ /** Extension carrying prose claims (signal S3). */
192
+ const PROSE_EXTENSIONS = Object.freeze(['.md']);
193
+
194
+ /**
195
+ * Additionally excluded from the S3 PROSE corpus. `.orchestrator/` is generated
196
+ * telemetry and audit output — machine-written, so it asserts nothing.
197
+ */
198
+ const PROSE_EXCLUDED_DIRS = Object.freeze([...EXCLUDED_DIRS, '.orchestrator']);
199
+
200
+ /**
201
+ * Prose files excluded by basename.
202
+ *
203
+ * `CHANGELOG.md` is a HISTORICAL record: it describes what a past release
204
+ * shipped, so it keeps naming symbols of code that has since been deleted. See
205
+ * the header for the true positive this masked.
206
+ *
207
+ * `STATE.md` is per-session MUTABLE state, not documentation. A module named in
208
+ * a wave plan is not a durable promise, and counting it would make this check's
209
+ * output depend on whichever session happens to be open — a repo-wide census
210
+ * must not change because a task description mentioned a filename.
211
+ */
212
+ const PROSE_EXCLUDED_FILES = Object.freeze(['CHANGELOG.md', 'STATE.md']);
213
+
132
214
  /**
133
215
  * This file excludes ITSELF from the consumer corpus. Load-bearing: every
134
216
  * `ALLOWLIST` key is a string literal here, so without the exclusion each
@@ -179,27 +261,29 @@ const ALLOWLIST = Object.freeze({
179
261
  /**
180
262
  * @typedef {{
181
263
  * kind: 'unwired-config-key' | 'parser-orphan-config-key' | 'allowlist-missing-reason'
182
- * | 'allowlist-stale' | 'tool-error',
264
+ * | 'allowlist-stale' | 'orphaned-prose-module' | 'tool-error',
183
265
  * key: string,
184
266
  * message: string,
185
267
  * }} Finding
186
268
  */
187
269
 
188
270
  /**
189
- * Recursively collect code files, skipping symlinks and excluded directories.
271
+ * Recursively collect files, skipping symlinks and excluded directories.
190
272
  *
191
273
  * @param {string} directory absolute directory path
192
274
  * @param {string[]} [acc]
193
- * @returns {string[]} absolute file paths, sorted
275
+ * @param {readonly string[]} [extensions] extensions to keep
276
+ * @param {readonly string[]} [excludedDirs] directory names pruned at any depth
277
+ * @returns {string[]} absolute file paths
194
278
  */
195
- function walkCode(directory, acc = []) {
279
+ function walkCode(directory, acc = [], extensions = CODE_EXTENSIONS, excludedDirs = EXCLUDED_DIRS) {
196
280
  if (!existsSync(directory)) return acc;
197
281
  for (const entry of readdirSync(directory, { withFileTypes: true })) {
198
282
  if (entry.isSymbolicLink()) continue;
199
- if (EXCLUDED_DIRS.includes(entry.name)) continue;
283
+ if (excludedDirs.includes(entry.name)) continue;
200
284
  const fullPath = path.join(directory, entry.name);
201
- if (entry.isDirectory()) walkCode(fullPath, acc);
202
- else if (entry.isFile() && CODE_EXTENSIONS.includes(path.extname(entry.name))) acc.push(fullPath);
285
+ if (entry.isDirectory()) walkCode(fullPath, acc, extensions, excludedDirs);
286
+ else if (entry.isFile() && extensions.includes(path.extname(entry.name))) acc.push(fullPath);
203
287
  }
204
288
  return acc;
205
289
  }
@@ -383,13 +467,127 @@ export function countReadSites(declared, corpus) {
383
467
  return { code, comment, files };
384
468
  }
385
469
 
470
+ /**
471
+ * Extract the NAMED symbols a module exports.
472
+ *
473
+ * Deliberately named-only: `export * from './x.mjs'` yields nothing, which is
474
+ * what marks a re-export shim as unjudgeable by S3 (see header, FP class 2).
475
+ *
476
+ * @param {string} body module source
477
+ * @returns {string[]} exported symbol names
478
+ */
479
+ export function collectExportedSymbols(body) {
480
+ /** @type {Set<string>} */
481
+ const names = new Set();
482
+ const declaration = /^export\s+(?:async\s+)?(?:function\*?|class|const|let|var)\s+([A-Za-z0-9_$]+)/gm;
483
+ for (const match of body.matchAll(declaration)) names.add(match[1]);
484
+ for (const match of body.matchAll(/^export\s*\{([^}]*)\}/gm)) {
485
+ for (const clause of match[1].split(',')) {
486
+ const name = clause.trim().split(/\s+as\s+/).pop()?.trim();
487
+ if (name && /^[A-Za-z0-9_$]+$/.test(name)) names.add(name);
488
+ }
489
+ }
490
+ return [...names];
491
+ }
492
+
493
+ /**
494
+ * Whether a module is a CLI entrypoint rather than a library.
495
+ *
496
+ * An entrypoint is invoked by path (npm script, hook wiring, CI job), so having
497
+ * no importer is its normal state and says nothing about being wired.
498
+ *
499
+ * @param {string} body module source
500
+ * @returns {boolean}
501
+ */
502
+ export function isCliEntrypoint(body) {
503
+ return (
504
+ body.startsWith('#!') ||
505
+ /import\.meta\.url\s*===|require\.main\s*===\s*module|process\.argv\[1\]/.test(body)
506
+ );
507
+ }
508
+
509
+ /**
510
+ * Signal S3 — modules a document promises but nothing calls.
511
+ *
512
+ * See the header for the five conditions, the grammar discriminator, and the two
513
+ * false-positive classes this is calibrated against. Condition 2 counts ANY
514
+ * non-comment mention of the basename as a reference (not just a static import
515
+ * specifier) because a real consumer can reach a module through
516
+ * `new URL(…, import.meta.url)` + dynamic `import()`.
517
+ *
518
+ * @param {string} pluginRoot absolute plugin root
519
+ * @returns {{findings: Finding[], scanned: {modules: number, prose: number}}}
520
+ */
521
+ export function collectOrphanedProseModules(pluginRoot) {
522
+ /** @type {Finding[]} */
523
+ const findings = [];
524
+
525
+ const modules = CONSUMER_DIRS.flatMap((dir) => walkCode(path.join(pluginRoot, dir)))
526
+ .sort()
527
+ .map((absolute) => {
528
+ const body = readFileSync(absolute, 'utf8');
529
+ return {
530
+ relative: path.relative(pluginRoot, absolute),
531
+ base: path.basename(absolute),
532
+ body,
533
+ lines: body.split('\n'),
534
+ };
535
+ });
536
+
537
+ const prose = walkCode(pluginRoot, [], PROSE_EXTENSIONS, PROSE_EXCLUDED_DIRS)
538
+ .filter((absolute) => !PROSE_EXCLUDED_FILES.includes(path.basename(absolute)))
539
+ .sort()
540
+ .map((absolute) => ({
541
+ relative: path.relative(pluginRoot, absolute),
542
+ body: readFileSync(absolute, 'utf8'),
543
+ }));
544
+
545
+ for (const module of modules) {
546
+ // (1) named by a live document
547
+ const claims = prose.filter((doc) => doc.body.includes(module.base));
548
+ if (claims.length === 0) continue;
549
+
550
+ // (2) no production module references it outside a comment
551
+ const referenced = modules.some(
552
+ (other) =>
553
+ other.relative !== module.relative &&
554
+ other.lines.some((line) => line.includes(module.base) && !isCommentLine(line)),
555
+ );
556
+ if (referenced) continue;
557
+
558
+ // (3) not invoked by path
559
+ if (isCliEntrypoint(module.body)) continue;
560
+
561
+ // (4) has symbols the prose could have named
562
+ const symbols = collectExportedSymbols(module.body);
563
+ if (symbols.length === 0) continue;
564
+
565
+ // (5) the prose names none of them → nobody is addressed, so nobody acts
566
+ const naming = claims.filter((doc) => symbols.some((symbol) => tokenMatcher(symbol).test(doc.body)));
567
+ if (naming.length > 0) continue;
568
+
569
+ findings.push({
570
+ kind: 'orphaned-prose-module',
571
+ key: module.relative,
572
+ message:
573
+ `named in ${claims.map((doc) => doc.relative).join(' + ')} but no .mjs under ` +
574
+ `${CONSUMER_DIRS.join('/ or ')}/ references it, and that prose names none of its ` +
575
+ `export(s) (${symbols.join(', ')}) — the document promises behaviour nothing performs; ` +
576
+ 'wire it, delete it, or reword the prose to describe what actually happens',
577
+ });
578
+ }
579
+
580
+ return { findings, scanned: { modules: modules.length, prose: prose.length } };
581
+ }
582
+
386
583
  /**
387
584
  * Run the full census.
388
585
  *
389
586
  * @param {string} pluginRoot absolute plugin root
390
587
  * @returns {{
391
588
  * ok: boolean,
392
- * summary: {declaredKeys: number, consumerFiles: number, unwired: number, allowlisted: number},
589
+ * summary: {declaredKeys: number, consumerFiles: number, unwired: number, allowlisted: number,
590
+ * orphanedModules: number},
393
591
  * sourcesScanned: string[],
394
592
  * findings: Finding[],
395
593
  * toolError: boolean,
@@ -400,7 +598,7 @@ export function inspectUnwiredFeatures(pluginRoot) {
400
598
  const findings = [];
401
599
  const result = {
402
600
  ok: false,
403
- summary: { declaredKeys: 0, consumerFiles: 0, unwired: 0, allowlisted: 0 },
601
+ summary: { declaredKeys: 0, consumerFiles: 0, unwired: 0, allowlisted: 0, orphanedModules: 0 },
404
602
  /** @type {string[]} */
405
603
  sourcesScanned: [],
406
604
  findings,
@@ -413,6 +611,8 @@ export function inspectUnwiredFeatures(pluginRoot) {
413
611
  let corpus;
414
612
  /** @type {string} */
415
613
  let parserBody;
614
+ /** @type {ReturnType<typeof collectOrphanedProseModules>} */
615
+ let orphans;
416
616
  try {
417
617
  declared = collectDeclaredKeys(pluginRoot);
418
618
  corpus = CONSUMER_DIRS.flatMap((dir) => walkCode(path.join(pluginRoot, dir)))
@@ -429,6 +629,7 @@ export function inspectUnwiredFeatures(pluginRoot) {
429
629
  })
430
630
  .map((absolute) => readFileSync(absolute, 'utf8'))
431
631
  .join('\n');
632
+ orphans = collectOrphanedProseModules(pluginRoot);
432
633
  } catch (error) {
433
634
  result.toolError = true;
434
635
  findings.push({
@@ -501,6 +702,12 @@ export function inspectUnwiredFeatures(pluginRoot) {
501
702
  });
502
703
  }
503
704
 
705
+ // S3 — prose promises a module nothing calls. Reported alongside the config
706
+ // census because it is the same defect class one level out: a claim with no
707
+ // mechanism behind it.
708
+ result.summary.orphanedModules = orphans.findings.length;
709
+ findings.push(...orphans.findings);
710
+
504
711
  result.ok = !result.toolError && findings.length === 0;
505
712
  return result;
506
713
  }
@@ -525,13 +732,14 @@ export function runCheckUnwiredFeatures(pluginRoot) {
525
732
  return 2;
526
733
  }
527
734
 
528
- const { declaredKeys, consumerFiles, unwired, allowlisted } = inspection.summary;
735
+ const { declaredKeys, consumerFiles, unwired, allowlisted, orphanedModules } = inspection.summary;
529
736
  for (const item of inspection.findings) {
530
737
  console.log(` WARN: [${item.kind}] ${item.key} — ${item.message}`);
531
738
  }
532
739
  console.log(
533
740
  ` PASS: censused ${declaredKeys} declared key(s) from ${inspection.sourcesScanned.join(' + ') || '(no source)'} ` +
534
- `against ${consumerFiles} consumer file(s) — ${unwired} unwired, ${allowlisted} allowlisted`,
741
+ `against ${consumerFiles} consumer file(s) — ${unwired} unwired, ${allowlisted} allowlisted, ` +
742
+ `${orphanedModules} prose-orphaned module(s)`,
535
743
  );
536
744
  console.log('');
537
745
  console.log('Results: 1 passed, 0 failed');