Skip to main content
← Back to list
01Issue
BugOpenSwamp CLIPublic
AssigneesNone

Relationships

#2486 Registry catalog silently drops models whose version is not a literal in the export const model block

Opened by shelson · 9/24/2026

Summary

swamp extension push accepts a models-only extension and reports a non-zero modelCount, but the swamp-club registry catalog can end up with zero model types — no error, no warning at push time. The extension is catalogued as contentTypes: ["skills"] and is invisible to swamp extension search --content-type models, even though swamp extension pull installs it and the types work at runtime.

Found while publishing @shelson/newrelic (10 model types produced by a shared factory).

Reproduction

Model definitions are produced by a shared factory, so the export block spreads the factory result and only sets type as a literal:

const definition = nrModel({ type: "@acme/thing", version: "...", /* … */ });

export const model = {
  ...definition,
  type: "@acme/thing",
};

Everything local passes:

  • deno check / lint / fmt / test pass
  • swamp extension quality manifest.yaml → 100% (12/12 client-earnable)
  • swamp extension push manifest.yaml --dry-run → exit 0
  • swamp model type search acme → all types load
  • The local loader no longer warns about type extraction (the literal is visible)
  • swamp extension push manifest.yaml --yes → succeeds, modelCount: 10, bundleCount: 10

But the registry then reports:

$ swamp extension info @acme/thing --json
contentTypes: ["skills"]
contentMetadata.models: []
$ swamp extension search acme --content-type models --json
{ "extensions": [], "meta": { "total": 0 } }

Root cause

The model kind adapter extracts type and version with separate regexes, but the catalog builder hard-requires a version and silently returns null when it is missing.

From domain/extensions/model_kind_adapter.ts:

extractTypeFromSource(source) {
  const modelMatch = /export\s+const\s+model\s*[=:]/.test(source);
  const extensionMatch = /export\s+const\s+extension\s*[=:]/.test(source);
  if (!modelMatch && !extensionMatch) return null;

  const typeMatch = source.match(
    /export\s+const\s+(?:model|extension)\b[\s\S]*?=\s*\{[\s\S]*?type\s*:\s*["']([^"']+)["']/,
  );
  if (!typeMatch) return null;

  const versionMatch = source.match(
    /export\s+const\s+(?:model|extension)\b[\s\S]*?=\s*\{[\s\S]*?version\s*:\s*["']([^"']+)["']/,
  );

  return { typeNormalized, version: versionMatch?.[1] ?? "", /* … */ };
}

and in the catalog builder:

function extractModelFromSource(content, filePath, modelsDir) {
  const type = extractModelType(content);
  if (!type) return null;
  const version = extractModelVersion(content);
  if (!version) return null;   // <-- model silently dropped, no diagnostic
  // …
}

So a model with a literal type but a non-literal (or absent) version passes the exportRegex gate and the local type warning, then extractModelFromSource returns null with no diagnostic. The archive still contains every bundle (runtime works after pull), but the registry catalog lists none of them.

The inverse gap also exists: a literal version with no literal type triggers the local loader warning, but a missing version does not warn anywhere.

Workaround used

Repeat both literals in the export block:

export const model = {
  ...definition,
  type: "@acme/thing",
  version: "2026.09.25.1",
};

After republishing, contentTypes became ["models", "skills"] and contentMetadata.models listed all 10 types.

Suggested improvements

  1. Push-time diagnostic (highest value). swamp extension push and --dry-run already scan model sources. If extractModelFromSource would return null for a file listed in manifest.models, emit a review/push warning naming the file and the missing literal (type and/or version). This is the check that would have caught this before publishing a catalog-empty extension.
  2. Make the warning match the gate. The loader warns when type can't be extracted, but there is no equivalent warning when version can't. Emit the same emitTypeExtractionFailure-style warning for a missing version so local swamp model type search and the registry catalog agree.
  3. Consider supporting non-literal construction. extractModelTypeName already special-cases /ModelType\.create\(\s*["']([^"']+)["']\s*\)/, so there is precedent for recognising factory-built models. If the source is loadable, deriving type/version from the loaded module (or accepting an explicit manifest-level declaration) would let shared-factory extensions catalogue without literal duplication.
  4. Document the contract. publishing.md / the extension guide should state explicitly that the export const model block must contain literal type and version strings for registry cataloguing, and that factory/spread patterns must repeat those literals in the export block.

extractGlobalArguments resolves a named schema only when it is declared in the same file:

const refMatch = content.match(/(?<![.\w])globalArguments:\s*(\w+)/);
// then searches the same `content` for: const <SchemaName> = z.object({

A schema imported from a shared module (as ours is, via nrModel) yields []. Runtime swamp model type describe shows the full global args, so this is catalogue-only, but it means shared-schema extensions get a degraded catalogue entry. Worth documenting or resolving through the import.

Environment

  • swamp CLI: 20260911.215321.0-sha.d18f2d86
  • Registry: swamp-club.com
  • Extension: @shelson/newrelic — 10 model types via a shared factory, published to stable
  • Repro versions: 2026.09.24.1/2026.09.24.2 were catalogued skills-only; 2026.09.25.1 (with literal type + version) catalogued correctly
02Bog Flow
◉OPEN○TRIAGED○IN PROGRESS○SHIPPED

Open

9/24/2026, 3:22:10 PM

No activity in this phase yet.

03Sludge Pulse

Sign in to post a ripple.