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/testpassswamp extension quality manifest.yaml→ 100% (12/12 client-earnable)swamp extension push manifest.yaml --dry-run→ exit 0swamp model type search acme→ all types load- The local loader no longer warns about
typeextraction (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
- Push-time diagnostic (highest value).
swamp extension pushand--dry-runalready scan model sources. IfextractModelFromSourcewould returnnullfor a file listed inmanifest.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. - Make the warning match the gate. The loader warns when
typecan't be extracted, but there is no equivalent warning whenversioncan't. Emit the sameemitTypeExtractionFailure-style warning for a missing version so localswamp model type searchand the registry catalog agree. - Consider supporting non-literal construction.
extractModelTypeNamealready special-cases/ModelType\.create\(\s*["']([^"']+)["']\s*\)/, so there is precedent for recognising factory-built models. If the source is loadable, derivingtype/versionfrom the loaded module (or accepting an explicit manifest-level declaration) would let shared-factory extensions catalogue without literal duplication. - Document the contract.
publishing.md/ the extension guide should state explicitly that theexport const modelblock must contain literaltypeandversionstrings for registry cataloguing, and that factory/spread patterns must repeat those literals in the export block.
Related: globalArguments metadata is also empty for imported schemas
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.2were catalogued skills-only;2026.09.25.1(with literaltype+version) catalogued correctly
Open
No activity in this phase yet.
Sign in to post a ripple.