Skip to content

Commit 5fcb2ba

Browse files
os-zhuangclaude
andcommitted
fix(ADR-0046): serve package docs at runtime, not just in the compiled artifact
Package docs (src/docs/*.md) compiled into a bundle never reached the runtime: GET /meta/doc returned [] and docs were invisible in the console even though os build produced them and a pure-artifact boot served them. Root cause (two gaps on the two load paths): 1. os dev / os serve load metadata from objectstack.config.ts via defineStack(), which never carries the markdown docs — those are a compile-time augmentation (compile.ts step 3d). serve.ts now collects src/docs/*.md into the stack on the config-load path as well (collection only, additive, never blocks boot), so docs serve in dev exactly as from a built artifact. Verified: showcase /meta/doc returns showcase_index + showcase_docs_guide on a config boot. 2. The MetadataPlugin artifact loader's ARTIFACT_FIELD_TO_TYPE omitted docs -> doc, so the bundle's docs array was dropped on that path. Added the mapping for parity with the ObjectQL engine's metadataArrayKeys, plus a _parseAndRegisterArtifact regression test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c865eaf commit 5fcb2ba

4 files changed

Lines changed: 92 additions & 0 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
"@objectstack/cli": patch
3+
"@objectstack/metadata": patch
4+
---
5+
6+
fix(ADR-0046): serve package docs at runtime, not just in the compiled artifact
7+
8+
Package docs (`src/docs/*.md`) compiled into a bundle were never reaching the
9+
runtime, so `GET /meta/doc` returned an empty list and the docs were invisible
10+
even though `os build` produced them.
11+
12+
Two gaps:
13+
14+
- **`os dev` / `os serve` (config-load path)** re-derives metadata from
15+
`defineStack(...)`, which never carries the markdown docs — those are
16+
collected only at compile time. `serve.ts` now collects `src/docs/*.md` into
17+
the stack on the config-load path too (collection only — additive, never
18+
blocks boot), so docs serve in dev exactly as from a built artifact.
19+
- **The MetadataPlugin artifact loader** (`ARTIFACT_FIELD_TO_TYPE`) omitted the
20+
`docs``doc` mapping, so the bundle's `docs` array was skipped when loading
21+
through that path. Added the mapping (with a regression test) for parity with
22+
the ObjectQL engine's `metadataArrayKeys`.

packages/cli/src/commands/serve.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,31 @@ export default class Serve extends Command {
383383
config = merged;
384384
}
385385

386+
// Package docs (ADR-0046): flat `src/docs/*.md` are collected into the
387+
// stack at COMPILE time (compile.ts step 3d). The config-load path here
388+
// re-derives metadata from `defineStack(...)`, which never carries the
389+
// markdown docs — so without this, `os dev`/`os serve` against a config
390+
// serves ZERO docs (GET /meta/doc empty) even though `os build` produces
391+
// them and an artifact boot serves them. Mirror compile's collection so
392+
// docs render under /docs/<name> in dev exactly as from a built artifact.
393+
// Collection only (no lint-fail): docs are additive; never block boot.
394+
if (!useArtifactFallback) {
395+
try {
396+
const { collectDocsFromSrc } = await import('../utils/collect-docs.js');
397+
const collected = collectDocsFromSrc(absolutePath);
398+
if (collected.docs.length > 0) {
399+
const byName = new Map<string, any>();
400+
for (const d of (Array.isArray((config as any).docs) ? (config as any).docs : [])) {
401+
if (d?.name) byName.set(d.name, d);
402+
}
403+
for (const d of collected.docs) byName.set(d.name, d);
404+
config = { ...config, docs: Array.from(byName.values()) };
405+
}
406+
} catch {
407+
/* docs are additive — never block boot on collection */
408+
}
409+
}
410+
386411
// Boot-mode dispatch: this open-core CLI only supports `standalone`
387412
// (and the artifact-fallback shortcut). Cloud / multi-environment
388413
// boot modes live in a separate distribution and are no longer

packages/metadata/src/plugin.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,50 @@ describe('MetadataPlugin._parseAndRegisterArtifact — view name resolution (PR-
111111
});
112112
});
113113

114+
// ─────────────────────────────────────────────────────────────────────────
115+
// ADR-0046 regression: a compiled artifact carries package docs in a
116+
// top-level `docs: DocSchema[]` array. The artifact loader registers only
117+
// the metadata fields enumerated in ARTIFACT_FIELD_TO_TYPE; `docs` was
118+
// omitted, so the bundle's docs were silently dropped and GET /meta/doc
119+
// returned an empty list even though the package shipped docs. The field
120+
// must map to the `doc` type so docs register like any other item.
121+
// ─────────────────────────────────────────────────────────────────────────
122+
describe('MetadataPlugin._parseAndRegisterArtifact — package docs (ADR-0046)', () => {
123+
it('registers `doc` items from the artifact `docs` array', async () => {
124+
const plugin = new MetadataPlugin({
125+
watch: false,
126+
config: { bootstrap: 'eager' },
127+
environmentId: 'proj_test',
128+
});
129+
const mgr = (plugin as any).manager as NodeMetadataManager;
130+
const fakeCtx = {
131+
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
132+
} as any;
133+
134+
const artifact = {
135+
id: 'com.example.docs',
136+
name: 'test',
137+
version: '0.0.0',
138+
type: 'app',
139+
scope: 'app',
140+
namespace: 'test',
141+
defaultDatasource: 'memory',
142+
docs: [
143+
{ name: 'test_index', label: 'Overview', content: '# Overview\n' },
144+
{ name: 'test_guide', content: '# Guide\n' },
145+
],
146+
};
147+
148+
await (plugin as any)._parseAndRegisterArtifact(fakeCtx, artifact, 'test-artifact');
149+
150+
const index = await mgr.get('doc', 'test_index');
151+
expect(index).toBeDefined();
152+
expect((index as any)?.content).toContain('# Overview');
153+
const guide = await mgr.get('doc', 'test_guide');
154+
expect(guide).toBeDefined();
155+
});
156+
});
157+
114158
// ─────────────────────────────────────────────────────────────────────────
115159
// Filesystem-scanner provenance: when the host declares its package id
116160
// (options.packageId — the project's `defineStack` manifest id), scanned

packages/metadata/src/plugin.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ const ARTIFACT_FIELD_TO_TYPE: Record<string, string> = {
7171
analyticsCubes: 'analytics_cube',
7272
connectors: 'connector',
7373
emailTemplates: 'email_template',
74+
docs: 'doc',
7475
data: 'dataset',
7576
};
7677

0 commit comments

Comments
 (0)