contentbase 0.0.2 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLI.md +593 -0
- package/MCP-SERVER-SPEC.md +453 -0
- package/PRIMER.md +540 -0
- package/README.md +289 -13
- package/docs/README.md +110 -0
- package/docs/TABLE-OF-CONTENTS.md +7 -0
- package/docs/models.ts +38 -0
- package/models.ts +38 -0
- package/package.json +12 -3
- package/src/api/endpoints/actions.ts +34 -0
- package/src/api/endpoints/doc.ts +187 -0
- package/src/api/endpoints/docs-index.ts +26 -0
- package/src/api/endpoints/document.ts +171 -0
- package/src/api/endpoints/documents.ts +71 -0
- package/src/api/endpoints/inspect.ts +16 -0
- package/src/api/endpoints/models.ts +10 -0
- package/src/api/endpoints/query.ts +88 -0
- package/src/api/endpoints/root.ts +7 -0
- package/src/api/endpoints/search-reindex.ts +65 -0
- package/src/api/endpoints/search-status.ts +55 -0
- package/src/api/endpoints/search.ts +104 -0
- package/src/api/endpoints/semantic-search.ts +120 -0
- package/src/api/endpoints/text-search.ts +63 -0
- package/src/api/endpoints/validate.ts +34 -0
- package/src/api/helpers.ts +97 -0
- package/src/base-model.ts +12 -0
- package/src/cli/commands/action.ts +82 -44
- package/src/cli/commands/console.ts +124 -0
- package/src/cli/commands/create.ts +179 -53
- package/src/cli/commands/embed.ts +323 -0
- package/src/cli/commands/export.ts +58 -24
- package/src/cli/commands/extract.ts +174 -0
- package/src/cli/commands/help.ts +81 -0
- package/src/cli/commands/index.ts +17 -0
- package/src/cli/commands/init.ts +72 -48
- package/src/cli/commands/inspect.ts +56 -46
- package/src/cli/commands/mcp.ts +1219 -0
- package/src/cli/commands/search.ts +285 -0
- package/src/cli/commands/serve.ts +348 -0
- package/src/cli/commands/summary.ts +60 -0
- package/src/cli/commands/teach.ts +86 -0
- package/src/cli/commands/text-search.ts +134 -0
- package/src/cli/commands/validate.ts +126 -64
- package/src/cli/index.ts +88 -19
- package/src/cli/load-collection.ts +144 -17
- package/src/cli/registry.ts +28 -0
- package/src/collection.ts +361 -10
- package/src/define-model.ts +101 -4
- package/src/document.ts +47 -1
- package/src/extract-sections.ts +1 -1
- package/src/index.ts +14 -2
- package/src/model-instance.ts +35 -5
- package/src/node-shortcuts.ts +1 -1
- package/src/query/collection-query.ts +96 -9
- package/src/query/index.ts +7 -0
- package/src/query/query-dsl.ts +259 -0
- package/src/relationships/has-many.ts +39 -0
- package/src/relationships/index.ts +7 -2
- package/src/section.ts +2 -0
- package/src/types.ts +23 -1
- package/src/utils/index.ts +1 -0
- package/src/utils/match-pattern.ts +65 -0
- package/src/validator.ts +18 -1
- package/bun.lock +0 -473
- package/examples/sdlc-queries.ts +0 -161
- package/scripts/examples/01-collection-setup.ts +0 -46
- package/scripts/examples/02-querying.ts +0 -67
- package/scripts/examples/03-sections.ts +0 -36
- package/scripts/examples/04-relationships.ts +0 -54
- package/scripts/examples/05-document-api.ts +0 -54
- package/scripts/examples/06-extract-sections.ts +0 -55
- package/scripts/examples/07-validation.ts +0 -46
- package/scripts/examples/08-serialization.ts +0 -51
- package/scripts/examples/lib/format.ts +0 -87
- package/scripts/examples/lib/setup.ts +0 -21
- package/scripts/examples/run-all.ts +0 -43
- package/showcases/national-parks/models.ts +0 -74
- package/showcases/national-parks/parks/acadia.mdx +0 -40
- package/showcases/national-parks/parks/yosemite.mdx +0 -44
- package/showcases/national-parks/parks/zion.mdx +0 -44
- package/showcases/national-parks/queries.ts +0 -103
- package/showcases/national-parks/trails/angels-landing.mdx +0 -19
- package/showcases/national-parks/trails/cathedral-lakes.mdx +0 -19
- package/showcases/national-parks/trails/half-dome.mdx +0 -19
- package/showcases/national-parks/trails/jordan-pond-path.mdx +0 -19
- package/showcases/national-parks/trails/mist-trail.mdx +0 -19
- package/showcases/national-parks/trails/observation-point.mdx +0 -19
- package/showcases/national-parks/trails/precipice-trail.mdx +0 -19
- package/showcases/national-parks/trails/the-narrows.mdx +0 -19
- package/showcases/recipes/cuisines/chinese.mdx +0 -28
- package/showcases/recipes/cuisines/italian.mdx +0 -32
- package/showcases/recipes/cuisines/mexican.mdx +0 -28
- package/showcases/recipes/models.ts +0 -77
- package/showcases/recipes/queries.ts +0 -89
- package/showcases/recipes/recipes/chinese/egg-fried-rice.mdx +0 -43
- package/showcases/recipes/recipes/chinese/mapo-tofu.mdx +0 -47
- package/showcases/recipes/recipes/italian/bruschetta.mdx +0 -38
- package/showcases/recipes/recipes/italian/cacio-e-pepe.mdx +0 -39
- package/showcases/recipes/recipes/italian/tiramisu.mdx +0 -43
- package/showcases/recipes/recipes/mexican/chicken-tinga.mdx +0 -44
- package/showcases/recipes/recipes/mexican/guacamole.mdx +0 -39
- package/showcases/vinyl-collection/albums/bitches-brew.mdx +0 -36
- package/showcases/vinyl-collection/albums/i-put-a-spell-on-you.mdx +0 -35
- package/showcases/vinyl-collection/albums/in-rainbows.mdx +0 -35
- package/showcases/vinyl-collection/albums/kind-of-blue.mdx +0 -32
- package/showcases/vinyl-collection/albums/ok-computer.mdx +0 -37
- package/showcases/vinyl-collection/albums/wild-is-the-wind.mdx +0 -35
- package/showcases/vinyl-collection/artists/miles-davis.mdx +0 -27
- package/showcases/vinyl-collection/artists/nina-simone.mdx +0 -26
- package/showcases/vinyl-collection/artists/radiohead.mdx +0 -27
- package/showcases/vinyl-collection/models.ts +0 -73
- package/showcases/vinyl-collection/queries.ts +0 -87
- package/test/ast-query.test.ts +0 -128
- package/test/collection.test.ts +0 -99
- package/test/define-model.test.ts +0 -78
- package/test/document.test.ts +0 -225
- package/test/extract-sections.test.ts +0 -356
- package/test/fixtures/sdlc/epics/authentication.mdx +0 -42
- package/test/fixtures/sdlc/epics/searching-and-browsing.mdx +0 -21
- package/test/fixtures/sdlc/models.ts +0 -89
- package/test/fixtures/sdlc/stories/authentication/a-user-should-be-able-to-register.mdx +0 -20
- package/test/helpers.ts +0 -21
- package/test/model-instance.test.ts +0 -197
- package/test/query.test.ts +0 -167
- package/test/relationships.test.ts +0 -84
- package/test/section.test.ts +0 -99
- package/test/table-of-contents.test.ts +0 -91
- package/test/validator.test.ts +0 -62
- package/tsconfig.json +0 -18
- package/vitest.config.ts +0 -11
|
@@ -51,6 +51,7 @@ export class HasManyRelationship<
|
|
|
51
51
|
*/
|
|
52
52
|
private extractChildNodes(): ChildNode[] {
|
|
53
53
|
const { astQuery } = this.#document;
|
|
54
|
+
if (!this.#definition.heading) return [];
|
|
54
55
|
const parentHeading = astQuery.findHeadingByText(this.#definition.heading);
|
|
55
56
|
if (!parentHeading) return [];
|
|
56
57
|
|
|
@@ -100,6 +101,14 @@ export class HasManyRelationship<
|
|
|
100
101
|
|
|
101
102
|
fetchAll(): InferModelInstance<TTarget>[] {
|
|
102
103
|
const targetDef = this.#definition.target();
|
|
104
|
+
|
|
105
|
+
// Foreign key mode: query for target documents where meta[foreignKey] matches this document's slug
|
|
106
|
+
// Used when foreignKey is explicitly set, or when no heading is specified (convention-based)
|
|
107
|
+
if (this.#definition.foreignKey || !this.#definition.heading) {
|
|
108
|
+
return this.#fetchByForeignKey(targetDef);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Heading mode: extract child nodes from parent document AST
|
|
103
112
|
const childNodes = this.extractChildNodes();
|
|
104
113
|
|
|
105
114
|
return childNodes.map(({ id, ast }) => {
|
|
@@ -119,6 +128,36 @@ export class HasManyRelationship<
|
|
|
119
128
|
});
|
|
120
129
|
}
|
|
121
130
|
|
|
131
|
+
#fetchByForeignKey(targetDef: TTarget): InferModelInstance<TTarget>[] {
|
|
132
|
+
// If foreignKey is explicitly set, use it. Otherwise infer from the parent document's model.
|
|
133
|
+
// Convention: look for meta[lowercase(parentModelName)] on target documents.
|
|
134
|
+
// e.g. Project hasMany Plans → looks for meta.project on Plan documents.
|
|
135
|
+
const fk = this.#definition.foreignKey || this.#inferForeignKey();
|
|
136
|
+
const slug = this.#document.slug;
|
|
137
|
+
const prefix = targetDef.prefix;
|
|
138
|
+
const results: InferModelInstance<TTarget>[] = [];
|
|
139
|
+
|
|
140
|
+
for (const pathId of this.#collection.available) {
|
|
141
|
+
if (!pathId.startsWith(prefix + "/")) continue;
|
|
142
|
+
const doc = this.#collection.document(pathId);
|
|
143
|
+
if (doc.meta[fk] === slug) {
|
|
144
|
+
results.push(this.#factory(doc, targetDef, this.#collection));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return results;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Infer the foreign key name from the parent document's prefix.
|
|
153
|
+
* e.g. if the parent is in "projects/", the FK is "project" (singularized, lowercased).
|
|
154
|
+
*/
|
|
155
|
+
#inferForeignKey(): string {
|
|
156
|
+
const parentPrefix = this.#document.id.split("/")[0];
|
|
157
|
+
// Simple singularize: strip trailing "s"
|
|
158
|
+
return parentPrefix.replace(/s$/, "");
|
|
159
|
+
}
|
|
160
|
+
|
|
122
161
|
first(): InferModelInstance<TTarget> | undefined {
|
|
123
162
|
return this.fetchAll()[0];
|
|
124
163
|
}
|
|
@@ -7,7 +7,10 @@ import type {
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Declare a hasMany relationship.
|
|
10
|
-
*
|
|
10
|
+
*
|
|
11
|
+
* Two modes:
|
|
12
|
+
* - `heading`: Children are extracted from sub-headings under a parent heading in the document.
|
|
13
|
+
* - `foreignKey`: Children are found by querying target documents where meta[foreignKey] matches this document's slug.
|
|
11
14
|
*
|
|
12
15
|
* The target parameter is a thunk (() => ModelDef) to allow circular references.
|
|
13
16
|
*/
|
|
@@ -16,7 +19,8 @@ export function hasMany<
|
|
|
16
19
|
>(
|
|
17
20
|
target: () => TTarget,
|
|
18
21
|
options: {
|
|
19
|
-
heading
|
|
22
|
+
heading?: string;
|
|
23
|
+
foreignKey?: string;
|
|
20
24
|
meta?: (self: any) => Record<string, unknown>;
|
|
21
25
|
id?: (slug: string) => string;
|
|
22
26
|
}
|
|
@@ -25,6 +29,7 @@ export function hasMany<
|
|
|
25
29
|
type: "hasMany",
|
|
26
30
|
target,
|
|
27
31
|
heading: options.heading,
|
|
32
|
+
foreignKey: options.foreignKey,
|
|
28
33
|
meta: options.meta,
|
|
29
34
|
id: options.id,
|
|
30
35
|
};
|
package/src/section.ts
CHANGED
|
@@ -19,10 +19,12 @@ export function section<T>(
|
|
|
19
19
|
options: {
|
|
20
20
|
extract: (query: AstQuery) => T;
|
|
21
21
|
schema?: z.ZodType<T>;
|
|
22
|
+
alternatives?: string[];
|
|
22
23
|
}
|
|
23
24
|
): SectionDefinition<T> {
|
|
24
25
|
return {
|
|
25
26
|
heading,
|
|
27
|
+
alternatives: options.alternatives,
|
|
26
28
|
extract: options.extract,
|
|
27
29
|
schema: options.schema,
|
|
28
30
|
};
|
package/src/types.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface CollectionItem {
|
|
|
12
12
|
path: string;
|
|
13
13
|
createdAt: Date;
|
|
14
14
|
updatedAt: Date;
|
|
15
|
+
size: number;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
/** Options when constructing a Collection */
|
|
@@ -19,6 +20,8 @@ export interface CollectionOptions {
|
|
|
19
20
|
rootPath: string;
|
|
20
21
|
extensions?: string[];
|
|
21
22
|
name?: string;
|
|
23
|
+
/** When true (default), load() looks for a models.{ts,js,mjs} in rootPath and auto-registers exported model definitions. */
|
|
24
|
+
autoDiscover?: boolean;
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
// ─── Section system ───
|
|
@@ -30,6 +33,8 @@ export interface CollectionOptions {
|
|
|
30
33
|
export interface SectionDefinition<T = unknown> {
|
|
31
34
|
/** The heading text to find in the document */
|
|
32
35
|
heading: string;
|
|
36
|
+
/** Alternative heading texts to try if the primary heading is not found */
|
|
37
|
+
alternatives?: string[];
|
|
33
38
|
/** Extract structured data from the section's AST query */
|
|
34
39
|
extract: (query: AstQuery) => T;
|
|
35
40
|
/** Optional Zod schema to validate the extracted value */
|
|
@@ -43,7 +48,10 @@ export interface HasManyDefinition<
|
|
|
43
48
|
> {
|
|
44
49
|
type: "hasMany";
|
|
45
50
|
target: () => TTarget;
|
|
46
|
-
heading
|
|
51
|
+
/** Extract children from sub-headings under this heading in the parent document */
|
|
52
|
+
heading?: string;
|
|
53
|
+
/** Find children by querying for target documents where meta[foreignKey] matches this document's slug */
|
|
54
|
+
foreignKey?: string;
|
|
47
55
|
meta?: (self: any) => Record<string, unknown>;
|
|
48
56
|
id?: (slug: string) => string;
|
|
49
57
|
}
|
|
@@ -100,8 +108,17 @@ export interface ModelDefinition<
|
|
|
100
108
|
sections: TSections;
|
|
101
109
|
relationships: TRelationships;
|
|
102
110
|
computed: TComputed;
|
|
111
|
+
/** Human-readable description of this model, auto-generated if not provided */
|
|
112
|
+
description: string;
|
|
113
|
+
/** Named scopes — reusable query presets */
|
|
114
|
+
scopes: Record<string, (query: any) => any>;
|
|
103
115
|
match?: (doc: DocumentRef) => boolean;
|
|
104
116
|
defaults?: Partial<z.input<TMeta>>;
|
|
117
|
+
pattern?: string | string[];
|
|
118
|
+
/** Glob patterns or RegExp to exclude matching pathIds from queries, listings, and TOC */
|
|
119
|
+
exclude?: (string | RegExp)[];
|
|
120
|
+
/** When true, documents are not required to have an H1 title */
|
|
121
|
+
titleOptional?: boolean;
|
|
105
122
|
|
|
106
123
|
/** The inferred Zod schema for convenience (same as meta) */
|
|
107
124
|
schema: TMeta;
|
|
@@ -131,6 +148,11 @@ export type InferModelInstance<
|
|
|
131
148
|
readonly document: import("./document.js").Document;
|
|
132
149
|
readonly collection: import("./collection.js").Collection;
|
|
133
150
|
|
|
151
|
+
// File metadata
|
|
152
|
+
readonly createdAt: Date | undefined;
|
|
153
|
+
readonly updatedAt: Date | undefined;
|
|
154
|
+
readonly size: number | undefined;
|
|
155
|
+
|
|
134
156
|
// Typed meta from Zod schema
|
|
135
157
|
readonly meta: z.infer<TMeta>;
|
|
136
158
|
|
package/src/utils/index.ts
CHANGED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express-style path pattern matching for extracting named parameters
|
|
3
|
+
* from document pathIds.
|
|
4
|
+
*
|
|
5
|
+
* Patterns use `:param` syntax for named segments and literal segments
|
|
6
|
+
* for exact matching. Segment counts must match exactly.
|
|
7
|
+
*
|
|
8
|
+
* Examples:
|
|
9
|
+
* matchPattern("plans/:project/:slug", "plans/acme/launch")
|
|
10
|
+
* // => { project: "acme", slug: "launch" }
|
|
11
|
+
*
|
|
12
|
+
* matchPattern("plans/:project/:slug", "stories/acme/launch")
|
|
13
|
+
* // => null (literal mismatch)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Match a single pattern against a pathId.
|
|
18
|
+
* Returns extracted params on match, null on mismatch.
|
|
19
|
+
*/
|
|
20
|
+
export function matchPattern(
|
|
21
|
+
pattern: string,
|
|
22
|
+
pathId: string
|
|
23
|
+
): Record<string, string> | null {
|
|
24
|
+
const patternParts = pattern.split("/").filter(Boolean);
|
|
25
|
+
const pathParts = pathId.split("/").filter(Boolean);
|
|
26
|
+
|
|
27
|
+
if (patternParts.length !== pathParts.length) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const params: Record<string, string> = {};
|
|
32
|
+
|
|
33
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
34
|
+
const pat = patternParts[i];
|
|
35
|
+
const val = pathParts[i];
|
|
36
|
+
|
|
37
|
+
if (pat.startsWith(":")) {
|
|
38
|
+
params[pat.slice(1)] = val;
|
|
39
|
+
} else if (pat !== val) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return params;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Try multiple patterns against a pathId, returning the first match.
|
|
49
|
+
* Accepts a single pattern string or an array of patterns.
|
|
50
|
+
*/
|
|
51
|
+
export function matchPatterns(
|
|
52
|
+
patterns: string | string[],
|
|
53
|
+
pathId: string
|
|
54
|
+
): Record<string, string> | null {
|
|
55
|
+
const list = Array.isArray(patterns) ? patterns : [patterns];
|
|
56
|
+
|
|
57
|
+
for (const pattern of list) {
|
|
58
|
+
const result = matchPattern(pattern, pathId);
|
|
59
|
+
if (result !== null) {
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return null;
|
|
65
|
+
}
|
package/src/validator.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
ValidationResult,
|
|
6
6
|
} from "./types";
|
|
7
7
|
import type { Document } from "./document";
|
|
8
|
+
import { matchPatterns } from "./utils/match-pattern";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Standalone validator that checks a document against a model definition's
|
|
@@ -19,12 +20,28 @@ export function validateDocument(
|
|
|
19
20
|
const errors: z.ZodIssue[] = [];
|
|
20
21
|
|
|
21
22
|
// Validate meta
|
|
22
|
-
const
|
|
23
|
+
const patternMeta = definition.pattern
|
|
24
|
+
? matchPatterns(definition.pattern, document.id) ?? {}
|
|
25
|
+
: {};
|
|
26
|
+
const rawMeta = { ...(definition.defaults ?? {}), ...patternMeta, ...document.meta };
|
|
23
27
|
const metaResult = definition.meta.safeParse(rawMeta);
|
|
24
28
|
if (!metaResult.success) {
|
|
25
29
|
errors.push(...metaResult.error.issues);
|
|
26
30
|
}
|
|
27
31
|
|
|
32
|
+
// Validate title: require H1 heading unless titleOptional or meta.title exists
|
|
33
|
+
if (!definition.titleOptional) {
|
|
34
|
+
const hasMetaTitle = !!document.meta.title;
|
|
35
|
+
const hasH1 = !!document.astQuery.select("heading");
|
|
36
|
+
if (!hasMetaTitle && !hasH1) {
|
|
37
|
+
errors.push({
|
|
38
|
+
code: "custom",
|
|
39
|
+
path: ["title"],
|
|
40
|
+
message: `Document "${document.id}" is missing a title. Add an H1 heading or set meta.title.`,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
28
45
|
// Validate sections
|
|
29
46
|
if (definition.sections) {
|
|
30
47
|
for (const [key, sd] of Object.entries(definition.sections)) {
|