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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/rules/000-session-orchestrator.mdc +3 -2
- package/.cursor/rules/030-wave-execution.mdc +10 -8
- package/.cursor/rules/040-discovery.mdc +6 -6
- package/.cursor/rules/050-plan.mdc +8 -8
- package/CHANGELOG.md +515 -0
- package/README.md +16 -11
- package/agents/analyst.md +1 -1
- package/agents/architect-reviewer.md +1 -1
- package/agents/code-implementer.md +4 -2
- package/agents/db-specialist.md +1 -1
- package/agents/dialectic-deriver.md +1 -1
- package/agents/docs-writer.md +1 -1
- package/agents/memory-proposal-collector.md +7 -5
- package/agents/qa-strategist.md +1 -1
- package/agents/security-reviewer.md +1 -1
- package/agents/session-reviewer.md +42 -1
- package/agents/skill-applied-judge.md +1 -1
- package/agents/test-writer.md +1 -1
- package/agents/ui-developer.md +1 -1
- package/agents/ux-evaluator.md +1 -1
- package/commands/eli5.md +33 -0
- package/commands/release.md +62 -0
- package/commands/test.md +2 -2
- package/docs/components.md +6 -5
- package/docs/migration-v3.md +9 -6
- package/docs/persona-panel.md +3 -1
- package/docs/scope-collision-guard.md +167 -0
- package/docs/session-config-reference.md +31 -8
- package/hooks/_lib/lock-bootstrap.mjs +19 -13
- package/hooks/enforce-scope.mjs +103 -3
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +21 -1
- package/hooks/on-session-end.mjs +76 -97
- package/hooks/on-session-start.mjs +195 -104
- package/hooks/on-stop.mjs +127 -12
- package/hooks/post-bash-write-verify.mjs +8 -32
- package/hooks/pre-auq-clarity.mjs +787 -0
- package/hooks/pre-bash-issue-budget.mjs +17 -18
- package/hooks/pre-task-scope-disjoint.mjs +1042 -0
- package/package.json +3 -1
- package/pi/prompts/eli5.md +12 -0
- package/pi/prompts/release.md +12 -0
- package/scripts/auq-audit.mjs +825 -0
- package/scripts/autopilot.mjs +10 -9
- package/scripts/emit-session.mjs +42 -0
- package/scripts/export-hw-learnings.mjs +61 -2
- package/scripts/lib/auq/clarity.mjs +1314 -0
- package/scripts/lib/auq/parse.mjs +1006 -0
- package/scripts/lib/auq/schema.mjs +1457 -0
- package/scripts/lib/autopilot/worktree-pipeline.mjs +5 -5
- package/scripts/lib/backlog-scan.mjs +106 -15
- package/scripts/lib/build-live-signals.mjs +7 -3
- package/scripts/lib/ci-status-banner.mjs +267 -77
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +32 -9
- package/scripts/lib/config/vault-integration.mjs +12 -1
- package/scripts/lib/dispatcher/rank.mjs +4 -7
- package/scripts/lib/gates/gate-full.mjs +3 -3
- package/scripts/lib/gates/gate-helpers.mjs +17 -6
- package/scripts/lib/git-config-drift.mjs +471 -0
- package/scripts/lib/harness-audit/categories/category6.mjs +65 -12
- package/scripts/lib/io.mjs +432 -7
- package/scripts/lib/issue-budget.mjs +63 -9
- package/scripts/lib/learnings/select.mjs +157 -3
- package/scripts/lib/memory-cleanup-stamp.mjs +132 -8
- package/scripts/lib/mirror-issues-banner.mjs +266 -0
- package/scripts/lib/named-vault-resolver.mjs +105 -16
- package/scripts/lib/owner-interview.mjs +78 -32
- package/scripts/lib/peer-cards/schema.mjs +6 -2
- package/scripts/lib/peer-discovery.mjs +73 -22
- package/scripts/lib/project-hygiene.mjs +64 -4
- package/scripts/lib/reconcile/renderer.mjs +17 -4
- package/scripts/lib/reconcile/writer.mjs +69 -30
- package/scripts/lib/redact-spans.mjs +89 -0
- package/scripts/lib/resource-probe/evaluate.mjs +330 -149
- package/scripts/lib/resource-probe/probe-platform.mjs +35 -0
- package/scripts/lib/resource-probe.mjs +18 -2
- package/scripts/lib/scope-baseline.mjs +77 -17
- package/scripts/lib/scope-gate.mjs +658 -0
- package/scripts/lib/secret-masker.mjs +262 -0
- package/scripts/lib/session-lock.mjs +34 -10
- package/scripts/lib/session-registry.mjs +9 -1
- package/scripts/lib/spiral-carryover.mjs +23 -2
- package/scripts/lib/state-md/mission-status.mjs +164 -58
- package/scripts/lib/tmux-layout/vcs-detector.mjs +108 -4
- package/scripts/lib/validate/check-agents.mjs +77 -5
- package/scripts/lib/validate/check-auq-clarity.mjs +274 -0
- package/scripts/lib/validate/check-commands.mjs +2 -20
- package/scripts/lib/validate/check-doc-cli-commands.mjs +514 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +48 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +185 -17
- package/scripts/lib/validate/check-rules.mjs +153 -9
- package/scripts/lib/validate/check-skills.mjs +191 -0
- package/scripts/lib/validate/check-test-git-config-target.mjs +665 -0
- package/scripts/lib/validate/check-unicode-safety.mjs +22 -2
- package/scripts/lib/validate/check-untracked-test-deps.mjs +925 -0
- package/scripts/lib/validate/check-unwired-features.mjs +219 -11
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +965 -0
- package/scripts/lib/validate/frontmatter-block.mjs +61 -0
- package/scripts/lib/validate/tier-inference.mjs +46 -8
- package/scripts/lib/vault-backfill/glab.mjs +91 -58
- package/scripts/lib/vault-backfill/manifest.mjs +28 -8
- package/scripts/lib/vault-mirror/namespace.mjs +146 -1
- package/scripts/lib/vault-mirror/process.mjs +264 -31
- package/scripts/lib/vault-mirror/render-sessions.mjs +115 -4
- package/scripts/lib/vault-status/board-writer.mjs +300 -56
- package/scripts/lib/vault-status/narrative-mirror.mjs +119 -5
- package/scripts/lib/vcs-repo-spec.mjs +680 -30
- package/scripts/lib/wave-resource-gate.mjs +67 -73
- package/scripts/materialize-wave-scope.mjs +281 -0
- package/scripts/print-learnings-index.mjs +30 -3
- package/scripts/release.mjs +983 -107
- package/scripts/run-quality-gate.mjs +14 -0
- package/scripts/site-numbers.mjs +1049 -0
- package/scripts/validate-plugin.mjs +64 -0
- package/scripts/validate-wave-scope.mjs +286 -12
- package/scripts/vault-backfill.mjs +32 -5
- package/scripts/vault-mirror.mjs +26 -1
- package/skills/_shared/monitor-patterns.md +24 -4
- package/skills/_shared/parallel-aware-auq.md +30 -24
- package/skills/_shared/parallel-aware-preamble.md +31 -2
- package/skills/_shared/state-ownership.md +49 -6
- package/skills/bootstrap/SKILL.md +2 -1
- package/skills/brainstorm/SKILL.md +18 -18
- package/skills/brainstorm/soul.md +12 -0
- package/skills/claude-md-drift-check/SKILL.md +9 -1
- package/skills/debug/SKILL.md +4 -1
- package/skills/discovery/SKILL.md +28 -24
- package/skills/discovery/issue-templates.md +4 -4
- package/skills/discovery/probes-code.md +2 -2
- package/skills/discovery/probes-feature.md +6 -6
- package/skills/discovery/probes-infra.md +2 -2
- package/skills/discovery/probes-session.md +5 -5
- package/skills/dispatcher/SKILL.md +10 -1
- package/skills/eli5/SKILL.md +43 -0
- package/skills/evolve/SKILL.md +8 -9
- package/skills/frontmatter-guard/SKILL.md +9 -1
- package/skills/gitlab-ops/SKILL.md +73 -59
- package/skills/gitlab-portfolio/SKILL.md +10 -1
- package/skills/grill/SKILL.md +6 -6
- package/skills/grill/soul.md +16 -0
- package/skills/memory-cleanup/SKILL.md +20 -7
- package/skills/npm-publish/SKILL.md +23 -51
- package/skills/peekaboo-driver/SKILL.md +3 -3
- package/skills/persona-panel/SKILL.md +3 -1
- package/skills/plan/SKILL.md +18 -16
- package/skills/plan/mode-feature.md +1 -1
- package/skills/plan/mode-new.md +42 -12
- package/skills/plan/soul.md +12 -0
- package/skills/reconcile/SKILL.md +3 -3
- package/skills/repo-audit/SKILL.md +10 -1
- package/skills/session-end/SKILL.md +97 -22
- package/skills/session-end/metrics-collection.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +37 -2
- package/skills/session-end/session-metrics-write.md +4 -10
- package/skills/session-plan/SKILL.md +2 -2
- package/skills/session-plan/wave-template.md +1 -1
- package/skills/session-start/SKILL.md +82 -36
- package/skills/session-start/phase-2-5-docs-planning.md +8 -8
- package/skills/session-start/phase-4-5-resource-health.md +82 -19
- package/skills/session-start/soul.md +110 -0
- package/skills/spinout/SKILL.md +5 -1
- package/skills/sunset-review/SKILL.md +11 -1
- package/skills/test-runner/SKILL.md +2 -2
- package/skills/tmux-layout/SKILL.md +7 -2
- package/skills/using-orchestrator/SKILL.md +1 -1
- package/skills/vault-mirror/SKILL.md +10 -1
- package/skills/vault-sync/SKILL.md +10 -1
- package/skills/vault-sync/validator.mjs +55 -6
- package/skills/wave-executor/wave-loop.md +64 -12
- package/skills/write-executable-plan/SKILL.md +6 -6
- package/scripts/lib/mission-status-schema.mjs +0 -114
- package/scripts/tests/fixtures/fetch-baseline/sample-rule.md +0 -8
- package/skills/vault-sync/tests/fixtures/archive-test-vault/90-archive/bad-archived.md +0 -8
- package/skills/vault-sync/tests/fixtures/archive-test-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/archive-test-vault/live-note.md +0 -8
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/bad-type.md +0 -8
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/good-note.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/.obsidian/config.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/01-projects/foo/projects-baseline.md +0 -10
- package/skills/vault-sync/tests/fixtures/clean-vault/03-daily/daily-2026-04-13.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/README.md +0 -3
- package/skills/vault-sync/tests/fixtures/clean-vault/hello-world.md +0 -11
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/has-dangling.md +0 -9
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/real-target.md +0 -8
- package/skills/vault-sync/tests/fixtures/empty-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/missing-id.md +0 -7
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/03-daily/daily-2026-04-13.md +0 -9
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/nested-tags-note.md +0 -11
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/README.md +0 -3
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_MOC.md +0 -3
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_MOC.md +0 -11
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/hello-world.md +0 -11
- 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
|
|
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
|
-
* @
|
|
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 (
|
|
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() &&
|
|
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');
|