Skip to main content
← Back to list
01Issue
BugClosedSwamp CLI
Assigneesstack72

User report extensions registered lazily are silently skipped during method execution

Opened by 4chems · 4/24/2026

Description

When the extension catalog (_extension_catalog.db) is already populated for the report kind, user-defined report extensions are registered as lazy entries (via registerLazyFromCatalog) rather than being fully imported. Subsequent method invocations silently skip these reports because executeReports calls registry.getAll(), which only returns fully-loaded (non-lazy) reports. The user sees only @swamp/method-summary run; their custom report produces no new data version and no Running report: log line.

Steps to Reproduce

  1. Create a user report extension at extensions/reports/my_report.ts with export const report = { name: "@org/my-report", scope: "method", ... }.
  2. Declare it on the model: reports: ["@org/my-report"] in the extension TypeScript, and in the instance YAML under reports.require.
  3. Run swamp model method run <model> <method> once — the report fires (bootstrap path: catalog not yet populated, full eager import).
  4. Run swamp model method run <model> <method> again — the report is silently skipped. Only @swamp/method-summary runs.

Root Cause (traced in swamp source 20260409.030033.0-sha.07dae003)

The bug spans two files:

src/domain/reports/user_report_loader.tsbuildIndex() (line 271) When catalog.isPopulated("report") is true and no source files are stale, the method calls registerLazyFromCatalog(catalog) and returns immediately. This registers report types as lazy entries only — their bundles are not imported.

src/domain/reports/report_execution_service.tsexecuteReports() (line 335)

const allReports = registry.getAll();

registry.getAll() (report_registry.ts line 152) only returns fully-loaded reports (this.reports map). Lazy entries in this.lazyTypes are invisible. So the candidate set built by filterReports() never contains user reports after the first run.

src/libswamp/models/run.ts — guard (line 612)

if (!input.skipAllReports && reportRegistry.getAll().length > 0) {

This guard passes (builtin reports like @swamp/method-summary are always eagerly registered), so execution enters the report block — but executeReports still only sees the builtins.

Compare with src/libswamp/reports/describe.tscreateReportDescribeDeps() (line 36-43) correctly calls reportRegistry.ensureLoaded() and reportRegistry.ensureTypeLoaded(name) before using the registry. The run path never does this.

Expected Behavior

All reports declared in modelDef.reports (and selection.require) should fire on every method run, regardless of whether the extension catalog was warm or cold at startup.

Proposed Fix

In src/domain/reports/report_execution_service.tsexecuteReports(), before calling registry.getAll(), add:

// Build candidate names first (from modelTypeReports + selection.require)
const candidateNames = [
  ...(modelTypeReports ?? []),
  ...(selection?.require ?? []).map(r => typeof r === "string" ? r : r.name),
];
// Eagerly load any lazy candidates so they appear in getAll()
await Promise.all(candidateNames.map(n => registry.ensureTypeLoaded(n)));
const allReports = registry.getAll();

Alternatively, add await registry.ensureLoaded() in executeReports() before getAll() — this is simpler but loads all report types rather than just the candidates.

Environment

  • swamp version: 20260409.030033.0-sha.07dae003
  • OS: macOS Darwin 25.4.0
  • Repo has one user report extension, one extension catalog DB, PostgreSQL datastore

Observed vs Expected

  • Observed: Data version for custom report stays at v2 after every subsequent queryErrors run. No Running report: "@org/my-report" log line. No report-4chems-loki-errors-report output.
  • Expected: Custom report produces a new data version on each method run.

Workaround

Manually delete the populated:report key from .swamp/_extension_catalog.db before each run (sqlite3 .swamp/_extension_catalog.db "DELETE FROM bundle_meta WHERE key='populated:report'"). This forces the bootstrap path (full eager import) on the next run. The problem recurs after the first subsequent run re-populates the catalog.

02Bog Flow
OPENTRIAGEDIN PROGRESSCLOSED+ 1 MOREASSIGNED+ 1 MOREPLANNING

Closed

4/24/2026, 1:55:21 PM

No activity in this phase yet.

03Sludge Pulse
stack72 assigned stack724/24/2026, 1:52:08 PM
Editable. Press Enter to edit.

stack72 commented 4/24/2026, 1:55:14 PM

hey @4chems

Thanks for the extremely thorough trace — the root cause analysis, file:line references, and proposed fix are all spot-on.

Good news: this bug has already been fixed. The version you're running (20260409.030033.0-sha.07dae003, built 2026-04-09) predates the fix by two days.

  • Fix: PR #1161 — fix: promote lazy-loaded reports in executeReports() before filtering (merged 2026-04-11)
  • Duplicate of: swamp-club issue #81 (same regression, originally introduced by #1089)
  • Approach shipped: the candidate-name variant you proposed (ensureTypeLoaded per required/modelType report name before registry.getAll()), applied centrally in executeReports() so it covers workflow, method, model, and failed-method summary scopes. 5 regression tests in src/domain/reports/report_execution_service_test.ts guard it.

Could you run swamp update and confirm your custom report fires on both the first and second invocations? If you still see the silent-skip on the latest build, let us know and we'll reopen for investigation.

Paul

Sign in to post a ripple.