Skip to content

Commit 060467a

Browse files
xuyushun441-sysos-zhuangclaude
authored
feat(ADR-0046): add optional description to package docs (#1811)
* chore: bump objectui to 893e5302174c feat(ADR-0046): package documentation portal + nav entry (#1686) objectui@893e5302174c7cbf75a7bed9e8e6dcb935273363 * feat(objectql): ADR-0048 Phase 1+2 — namespace install gate + prefer-local resolution Phase 1 — install-time namespace gate. `SchemaRegistry.installPackage` refuses a package whose `manifest.namespace` is already owned by a DIFFERENT installed package (new `NamespaceConflictError`), making explicit and early the constraint the object/table layer already enforces implicitly (a duplicate `CREATE TABLE <ns>_<obj>` fails at the DB). Same-package reinstall and shareable platform namespaces (base/system/sys) are exempt; OS_METADATA_COLLISION=warn downgrades to a warning. Phase 2 — prefer-local (container-scoped) resolution. `getItem(type, name, ns?)` resolves a bare name to the item owned by `ns`'s package before any cross-package fallback, preserving ADR-0005 overlay precedence and remaining backward compatible (param optional). `getApp` resolves prefer-local against its own name (app.name ≡ namespace in v1). The per-item collision guard now narrows to the cases prefer-local CANNOT disambiguate (shared/missing namespace), so two DIFFERENT-namespace packages legitimately coexist on the same bare name — the marketplace coexistence the revised ADR-0048 targets. Every existing collision test still passes (namespace-less fixtures remain a hard error). Tests: registry-namespace-install-gate.test.ts (7), registry-prefer-local- resolution.test.ts (7). Full objectql suite green (600). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(ADR-0046): add optional `description` to package docs A doc can now carry a one-line `description` (frontmatter `description:`) — title / summary / body, the natural minimal model. - spec: DocSchema gains optional `description`. - cli: collect-docs reads `description:` from frontmatter (generalized the scalar extractor; title + description share it). Regression test added. - It travels in GET /meta/doc list (content is omitted there), so the docs portal shows summaries without fetching each body — verified live: showcase /meta/doc returns description, hasContent:false. - Example docs (app-showcase, app-todo) carry descriptions. - ADR-0046: schema + a P3 design note for doc TAGS (deferred) — tags are i18n-resolved keys, core protocol vocabulary + namespace-prefixed package tags; not a field to bolt on early. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent cad0aff commit 060467a

15 files changed

Lines changed: 425 additions & 18 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/cli": minor
4+
---
5+
6+
feat(ADR-0046): add optional `description` to package docs
7+
8+
A doc can now carry a one-line `description` (frontmatter `description:`),
9+
giving the natural minimal model: title / summary / body. `DocSchema` gains an
10+
optional `description`; `os build` reads it from frontmatter. It travels in the
11+
`GET /meta/doc` list response (unlike `content`, which the list omits), so a
12+
docs portal can show summaries without fetching each body. Example docs
13+
(app-showcase, app-todo) updated.
14+
15+
Also records the deferred-to-P3 design for doc **tags** in ADR-0046: tags are
16+
keys (i18n-resolved, never display strings), with a small protocol core
17+
vocabulary plus namespace-prefixed package tags — not a field to bolt on early.

.objectui-sha

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
6164366619a136ba301580322e72a99b8a19c738
1+
893e5302174c7cbf75a7bed9e8e6dcb935273363

content/docs/guides/metadata/doc.mdx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,19 +31,23 @@ stable (a link resolves by basename, never by path).
3131

3232
### Frontmatter
3333

34-
A leading YAML frontmatter block is optional. Only `title` is read:
34+
A leading YAML frontmatter block is optional. `title` and `description`
35+
are read:
3536

3637
```md
3738
---
3839
title: User Guide
40+
description: How to create, organize, and complete records in CRM.
3941
---
4042

4143
# Getting started with CRM
4244
...
4345
```
4446

4547
The display title resolves in order: frontmatter `title:` → the first
46-
`#` heading → the doc `name`.
48+
`#` heading → the doc `name`. The optional `description` is a one-line
49+
summary the docs portal renders under the title (it travels in the doc
50+
list, so the portal shows summaries without fetching each body).
4751

4852
## Doc Properties
4953

@@ -53,6 +57,7 @@ When a `*.md` file is collected, it becomes a `doc` item with this shape:
5357
| :--- | :--- | :--- | :--- |
5458
| `name` | `string` || Machine name (`snake_case`), from the filename stem. Must match `^[a-z][a-z0-9_]*$` and be **namespace-prefixed** |
5559
| `label` | `string` || Display title (from frontmatter `title`) |
60+
| `description` | `string` || One-line summary for listings (from frontmatter `description`) |
5661
| `content` | `string` || The Markdown body |
5762

5863
You can also declare a doc programmatically with `DocSchema` in a stack

docs/adr/0046-package-docs-as-metadata.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ export const DocSchema = z.object({
107107
.describe('Unique doc name; MUST carry the package namespace prefix (enforced by build/publish lint)'),
108108
label: z.string().optional()
109109
.describe('Display title; defaults to the first `#` heading, then the name'),
110+
description: z.string().optional()
111+
.describe('One-line summary for listings (frontmatter `description:`); travels in the list response'),
110112
content: z.string().describe('Raw Markdown (CommonMark + GFM)'),
111113
});
112114
```
@@ -144,6 +146,8 @@ unbounded, and manifest-size pressure lands here first.
144146
legacy, so day-one lint costs nothing — the same reasoning as the
145147
image ban.
146148
- `label` = frontmatter `title:` if present, else first `#` heading.
149+
- `description` = frontmatter `description:` if present (optional one-line
150+
summary; the docs portal renders it under the title).
147151
- `content` = the file body (frontmatter stripped).
148152
- **Subdirectories under `src/docs/` are a build error**, not silently
149153
flattened — flatness is the contract that keeps references stable.
@@ -240,3 +244,30 @@ maintained, so it cannot rot.
240244

241245
P0 conventions are isomorphic to the P1 schema by construction: when the
242246
compiler lands, pilot content migrates with zero edits.
247+
248+
### P3 design note — tags / categorization (deferred, not bolted on early)
249+
250+
Tags, categories, and ordering are **navigation-model** concerns and stay
251+
in P3 by design — adding them before there is enough doc volume to need
252+
filtering buys an i18n-bearing protocol field with no payoff. When they
253+
land, design the discovery surface (tags + category + search + cross-package
254+
aggregation) as one thing, not field-by-field. The agreed shape for tags:
255+
256+
- **A tag is a stable key, never a display string.** Display always resolves
257+
through the platform's existing label-key → i18n mechanism, exactly like
258+
every other label. Free-form display strings fail twice: cross-package
259+
fragmentation (`setup` vs `getting-started` vs `quickstart`) and no i18n
260+
owner. Keying fixes both.
261+
- **Layered vocabulary, not closed-vs-open.** The protocol blesses a *small
262+
core* of cross-cutting "purpose" tags (`getting-started`, `guide`,
263+
`reference`, `tutorial`, `api`, `migration`, `troubleshooting`) — central
264+
i18n, eligible for dedicated UI. Packages extend with **namespace-prefixed**
265+
tags (`crm_*`, same rule as doc names) for domain topics, shipping their
266+
translations in the package i18n bundle (same path as object/field labels).
267+
- Pure closed enum is too rigid (packages can't express domain topics); pure
268+
open free-form is too fragmented and has no i18n home. The layered
269+
key-based model is the resolution.
270+
271+
The current addition — `description` — deliberately stops short of this: it is
272+
a per-doc summary, not a taxonomy, so it carries no i18n-keying or
273+
cross-package-coherence burden.

examples/app-showcase/src/docs/showcase_docs_guide.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
---
22
title: Documentation Guide
3+
description: The authoring rules every package doc must follow — flat directory, namespace prefix, links, and the Markdown subset.
34
---
45

56
# Writing package docs

examples/app-showcase/src/docs/showcase_index.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
---
2+
title: Showcase
3+
description: Overview of the showcase package and the docs-as-metadata feature it demonstrates.
4+
---
5+
16
# Showcase
27

38
The living conformance fixture for the ObjectStack protocol: every field

examples/app-todo/src/docs/todo_index.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
---
2+
title: Todo App
3+
description: A minimal task-management app that demonstrates the ObjectStack metadata protocol end to end.
4+
---
5+
16
# Todo App
27

38
A minimal task-management app demonstrating the ObjectStack metadata

examples/app-todo/src/docs/todo_user_guide.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
---
22
title: User Guide
3+
description: How to create, organize, and complete tasks in the Todo app.
34
---
45

56
# Working with Tasks

packages/cli/src/utils/collect-docs.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,18 @@ describe('collectDocsFromSrc (ADR-0046 §3.2)', () => {
3838
expect(guide.content).not.toContain('title:'); // frontmatter stripped
3939
});
4040

41+
it('reads optional frontmatter `description:` (and omits it when absent)', () => {
42+
write('crm_index.md', '---\ntitle: CRM\ndescription: Start here.\n---\n\n# CRM\n\nBody.');
43+
write('crm_plain.md', '# Plain\n\nNo frontmatter.');
44+
const { docs, issues } = collectDocsFromSrc(configPath);
45+
expect(issues).toHaveLength(0);
46+
const index = docs.find((d) => d.name === 'crm_index')!;
47+
expect(index.description).toBe('Start here.');
48+
expect(index.content).not.toContain('description:'); // frontmatter stripped
49+
const plain = docs.find((d) => d.name === 'crm_plain')!;
50+
expect(plain.description).toBeUndefined(); // absent → omitted, not ''
51+
});
52+
4153
it('errors on subdirectories — flatness is the contract', () => {
4254
fs.mkdirSync(path.join(docsDir, 'user'));
4355
write('crm_index.md', '# x');

packages/cli/src/utils/collect-docs.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import path from 'path';
2929
export interface DocItem {
3030
name: string;
3131
label?: string;
32+
description?: string;
3233
content: string;
3334
}
3435

@@ -41,19 +42,31 @@ export interface DocIssue {
4142

4243
const DOC_NAME_RE = /^[a-z][a-z0-9_]*$/;
4344

44-
/** Strip a leading `---` frontmatter block; extract a `title:` if present. */
45-
function parseFrontmatter(raw: string): { title?: string; body: string } {
45+
/** Extract a single-line scalar `key: value` from a frontmatter block. */
46+
function frontmatterScalar(block: string, key: string): string | undefined {
47+
const re = new RegExp(`^${key}\\s*:`, 'i');
48+
const line = block.split(/\r?\n/).find((l) => re.test(l));
49+
if (!line) return undefined;
50+
const value = line.replace(re, '').trim().replace(/^['"]|['"]$/g, '');
51+
return value || undefined;
52+
}
53+
54+
/**
55+
* Strip a leading `---` frontmatter block; extract `title:` and
56+
* `description:` if present (both optional, single-line scalars).
57+
*/
58+
function parseFrontmatter(raw: string): { title?: string; description?: string; body: string } {
4659
if (!raw.startsWith('---\n') && !raw.startsWith('---\r\n')) return { body: raw };
4760
const end = raw.indexOf('\n---', 3);
4861
if (end === -1) return { body: raw };
4962
const block = raw.slice(raw.indexOf('\n') + 1, end);
5063
const bodyStart = raw.indexOf('\n', end + 1);
5164
const body = bodyStart === -1 ? '' : raw.slice(bodyStart + 1);
52-
const titleLine = block.split(/\r?\n/).find((l) => /^title\s*:/.test(l));
53-
const title = titleLine
54-
? titleLine.replace(/^title\s*:\s*/, '').trim().replace(/^['"]|['"]$/g, '')
55-
: undefined;
56-
return { title: title || undefined, body };
65+
return {
66+
title: frontmatterScalar(block, 'title'),
67+
description: frontmatterScalar(block, 'description'),
68+
body,
69+
};
5770
}
5871

5972
/** Remove fenced code blocks and inline code spans before content scans. */
@@ -105,8 +118,13 @@ export function collectDocsFromSrc(configPath: string): { docs: DocItem[]; issue
105118
}
106119

107120
const raw = fs.readFileSync(path.join(docsDir, entry.name), 'utf-8');
108-
const { title, body } = parseFrontmatter(raw);
109-
docs.push({ name: stem, label: title ?? firstHeading(body), content: body });
121+
const { title, description, body } = parseFrontmatter(raw);
122+
docs.push({
123+
name: stem,
124+
label: title ?? firstHeading(body),
125+
...(description ? { description } : {}),
126+
content: body,
127+
});
110128
}
111129
return { docs, issues };
112130
}

0 commit comments

Comments
 (0)