contentbase 0.0.2 → 0.0.4
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/.mcp.json +9 -0
- package/CLI.md +593 -0
- package/MCP-DESCRIPTIONS-REVIEW.md +122 -0
- package/MCP-SERVER-SPEC.md +453 -0
- package/PRIMER.md +540 -0
- package/README.md +289 -13
- package/bun.lock +43 -4
- package/dist/cnotes +0 -0
- 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/public/web-demo/index.html +813 -0
- package/showcases/node_modules/.cache/.repl_history +3 -0
- package/showcases/vinyl-collection/artists/nina-simone.mdx +1 -1
- package/src/__tests__/semantic-search.integration.test.ts +284 -0
- 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/test/collection.test.ts +118 -2
- package/test/fixtures/sdlc/MODELS.md +106 -0
- package/test/fixtures/sdlc/SKILL.md +404 -0
- package/test/fixtures/sdlc/index.ts +9 -0
- package/test/fixtures/sdlc/models.ts +8 -6
- package/test/fixtures/sdlc/stories/authentication/a-user-should-be-able-to-login.mdx +20 -0
- package/test/fixtures/sdlc/templates/epic.md +23 -0
- package/test/fixtures/sdlc/templates/story.md +19 -0
- package/test/pattern.test.ts +191 -0
- package/test/query-dsl.test.ts +431 -0
- package/test/query.test.ts +29 -0
- package/test/relationships.test.ts +130 -0
- package/test/section.test.ts +61 -0
- package/test/table-of-contents.test.ts +49 -5
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
2
|
import { Collection } from "../src/collection";
|
|
3
3
|
import { createModelInstance } from "../src/model-instance";
|
|
4
|
+
import { defineModel, hasMany, belongsTo, z } from "../src/index";
|
|
4
5
|
import { createTestCollection } from "./helpers";
|
|
5
6
|
import { Epic, Story } from "./fixtures/sdlc/models";
|
|
6
7
|
|
|
@@ -53,6 +54,135 @@ describe("HasManyRelationship", () => {
|
|
|
53
54
|
});
|
|
54
55
|
});
|
|
55
56
|
|
|
57
|
+
describe("HasMany with foreignKey", () => {
|
|
58
|
+
let collection: Collection;
|
|
59
|
+
|
|
60
|
+
beforeEach(async () => {
|
|
61
|
+
collection = await createTestCollection();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("finds children by matching meta field to parent slug", () => {
|
|
65
|
+
// Define a version of Epic that uses foreignKey instead of heading
|
|
66
|
+
const EpicFK = defineModel("Epic", {
|
|
67
|
+
prefix: "epics",
|
|
68
|
+
meta: z.object({
|
|
69
|
+
priority: z.enum(["low", "medium", "high"]).optional(),
|
|
70
|
+
status: z.enum(["created", "in-progress", "complete"]).default("created"),
|
|
71
|
+
}),
|
|
72
|
+
relationships: {
|
|
73
|
+
stories: hasMany(() => Story, { foreignKey: "epic" }),
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const doc = collection.document("epics/authentication");
|
|
78
|
+
const epic = createModelInstance(doc, EpicFK, collection);
|
|
79
|
+
const stories = epic.relationships.stories.fetchAll();
|
|
80
|
+
|
|
81
|
+
expect(stories.length).toBe(2);
|
|
82
|
+
expect(stories.map((s: any) => s.meta.epic)).toEqual(["authentication", "authentication"]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("infers foreignKey from parent prefix when omitted", () => {
|
|
86
|
+
// hasMany with empty options — should infer foreignKey as "epic" from "epics/" prefix
|
|
87
|
+
const EpicInferred = defineModel("Epic", {
|
|
88
|
+
prefix: "epics",
|
|
89
|
+
meta: z.object({
|
|
90
|
+
priority: z.enum(["low", "medium", "high"]).optional(),
|
|
91
|
+
status: z.enum(["created", "in-progress", "complete"]).default("created"),
|
|
92
|
+
}),
|
|
93
|
+
relationships: {
|
|
94
|
+
stories: hasMany(() => Story, {}),
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const doc = collection.document("epics/authentication");
|
|
99
|
+
const epic = createModelInstance(doc, EpicInferred, collection);
|
|
100
|
+
const stories = epic.relationships.stories.fetchAll();
|
|
101
|
+
|
|
102
|
+
expect(stories.length).toBe(2);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("returns empty array when no children match", () => {
|
|
106
|
+
const EpicFK = defineModel("Epic", {
|
|
107
|
+
prefix: "epics",
|
|
108
|
+
meta: z.object({
|
|
109
|
+
priority: z.enum(["low", "medium", "high"]).optional(),
|
|
110
|
+
status: z.enum(["created", "in-progress", "complete"]).default("created"),
|
|
111
|
+
}),
|
|
112
|
+
relationships: {
|
|
113
|
+
stories: hasMany(() => Story, { foreignKey: "epic" }),
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const doc = collection.document("epics/searching-and-browsing");
|
|
118
|
+
const epic = createModelInstance(doc, EpicFK, collection);
|
|
119
|
+
const stories = epic.relationships.stories.fetchAll();
|
|
120
|
+
|
|
121
|
+
// No story files have epic: "searching-and-browsing" in their meta
|
|
122
|
+
expect(stories.length).toBe(0);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("children have correct titles and IDs", () => {
|
|
126
|
+
const EpicFK = defineModel("Epic", {
|
|
127
|
+
prefix: "epics",
|
|
128
|
+
meta: z.object({
|
|
129
|
+
priority: z.enum(["low", "medium", "high"]).optional(),
|
|
130
|
+
status: z.enum(["created", "in-progress", "complete"]).default("created"),
|
|
131
|
+
}),
|
|
132
|
+
relationships: {
|
|
133
|
+
stories: hasMany(() => Story, { foreignKey: "epic" }),
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const doc = collection.document("epics/authentication");
|
|
138
|
+
const epic = createModelInstance(doc, EpicFK, collection);
|
|
139
|
+
const stories = epic.relationships.stories.fetchAll();
|
|
140
|
+
|
|
141
|
+
const ids = stories.map((s: any) => s.id).sort();
|
|
142
|
+
expect(ids).toEqual([
|
|
143
|
+
"stories/authentication/a-user-should-be-able-to-login",
|
|
144
|
+
"stories/authentication/a-user-should-be-able-to-register",
|
|
145
|
+
]);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("CollectionQuery include()", () => {
|
|
150
|
+
let collection: Collection;
|
|
151
|
+
|
|
152
|
+
beforeEach(async () => {
|
|
153
|
+
collection = await createTestCollection();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("includes relationships in toJSON output", async () => {
|
|
157
|
+
const results = await collection.query(Epic).include("stories").fetchAll();
|
|
158
|
+
const auth = results.find((e: any) => e.id === "epics/authentication");
|
|
159
|
+
const json = auth!.toJSON();
|
|
160
|
+
|
|
161
|
+
expect(json.stories).toBeDefined();
|
|
162
|
+
expect(Array.isArray(json.stories)).toBe(true);
|
|
163
|
+
expect((json.stories as any[]).length).toBe(2);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("toJSON without include does not contain relationships", async () => {
|
|
167
|
+
const results = await collection.query(Epic).fetchAll();
|
|
168
|
+
const auth = results.find((e: any) => e.id === "epics/authentication");
|
|
169
|
+
const json = auth!.toJSON();
|
|
170
|
+
|
|
171
|
+
expect(json.stories).toBeUndefined();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("include can be combined with where", async () => {
|
|
175
|
+
const results = await collection.query(Epic)
|
|
176
|
+
.where("meta.priority", "high")
|
|
177
|
+
.include("stories")
|
|
178
|
+
.fetchAll();
|
|
179
|
+
|
|
180
|
+
expect(results.length).toBeGreaterThan(0);
|
|
181
|
+
const json = results[0].toJSON();
|
|
182
|
+
expect(json.stories).toBeDefined();
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
56
186
|
describe("BelongsToRelationship", () => {
|
|
57
187
|
let collection: Collection;
|
|
58
188
|
|
package/test/section.test.ts
CHANGED
|
@@ -71,6 +71,67 @@ describe("section helper", () => {
|
|
|
71
71
|
expect(extractCalled).toBe(1); // Cached
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
+
it("stores alternatives in the definition", () => {
|
|
75
|
+
const sd = section("Acceptance Criteria", {
|
|
76
|
+
alternatives: ["AC", "ACs"],
|
|
77
|
+
extract: (q) => q.selectAll("listItem"),
|
|
78
|
+
});
|
|
79
|
+
expect(sd.alternatives).toEqual(["AC", "ACs"]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("uses alternative heading when primary is missing", async () => {
|
|
83
|
+
const collection = await createTestCollection();
|
|
84
|
+
const doc = collection.createDocument({
|
|
85
|
+
id: "test/alt-heading",
|
|
86
|
+
content: "# Title\n\n## AC\n\n- item one\n- item two\n",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const TestModel = {
|
|
90
|
+
name: "Test",
|
|
91
|
+
prefix: "test",
|
|
92
|
+
meta: z.looseObject({}),
|
|
93
|
+
schema: z.looseObject({}),
|
|
94
|
+
sections: {
|
|
95
|
+
criteria: section("Acceptance Criteria", {
|
|
96
|
+
alternatives: ["AC", "ACs"],
|
|
97
|
+
extract: (q) => q.selectAll("listItem").map((n) => toString(n)),
|
|
98
|
+
}),
|
|
99
|
+
},
|
|
100
|
+
relationships: {},
|
|
101
|
+
computed: {},
|
|
102
|
+
} as any;
|
|
103
|
+
|
|
104
|
+
const instance = createModelInstance(doc, TestModel, collection);
|
|
105
|
+
expect(instance.sections.criteria).toEqual(["item one", "item two"]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("primary heading takes precedence over alternatives", async () => {
|
|
109
|
+
const collection = await createTestCollection();
|
|
110
|
+
const doc = collection.createDocument({
|
|
111
|
+
id: "test/both-headings",
|
|
112
|
+
content:
|
|
113
|
+
"# Title\n\n## Acceptance Criteria\n\n- primary item\n\n## AC\n\n- alt item\n",
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const TestModel = {
|
|
117
|
+
name: "Test",
|
|
118
|
+
prefix: "test",
|
|
119
|
+
meta: z.looseObject({}),
|
|
120
|
+
schema: z.looseObject({}),
|
|
121
|
+
sections: {
|
|
122
|
+
criteria: section("Acceptance Criteria", {
|
|
123
|
+
alternatives: ["AC"],
|
|
124
|
+
extract: (q) => q.selectAll("listItem").map((n) => toString(n)),
|
|
125
|
+
}),
|
|
126
|
+
},
|
|
127
|
+
relationships: {},
|
|
128
|
+
computed: {},
|
|
129
|
+
} as any;
|
|
130
|
+
|
|
131
|
+
const instance = createModelInstance(doc, TestModel, collection);
|
|
132
|
+
expect(instance.sections.criteria).toEqual(["primary item"]);
|
|
133
|
+
});
|
|
134
|
+
|
|
74
135
|
it("missing section returns empty data", async () => {
|
|
75
136
|
const collection = await createTestCollection();
|
|
76
137
|
const doc = collection.createDocument({
|
|
@@ -12,8 +12,8 @@ describe("Collection.tableOfContents", () => {
|
|
|
12
12
|
it("generates markdown with model group headings", () => {
|
|
13
13
|
const toc = collection.tableOfContents();
|
|
14
14
|
|
|
15
|
-
expect(toc).toContain("
|
|
16
|
-
expect(toc).toContain("
|
|
15
|
+
expect(toc).toContain("## Epics");
|
|
16
|
+
expect(toc).toContain("## Stories");
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
it("includes document titles as link text", () => {
|
|
@@ -41,8 +41,8 @@ describe("Collection.tableOfContents", () => {
|
|
|
41
41
|
|
|
42
42
|
expect(toc).toContain("# Project Docs");
|
|
43
43
|
// Model group headings shift to h2
|
|
44
|
-
expect(toc).toContain("##
|
|
45
|
-
expect(toc).toContain("##
|
|
44
|
+
expect(toc).toContain("## Epics");
|
|
45
|
+
expect(toc).toContain("## Stories");
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
it("sorts items alphabetically within groups", () => {
|
|
@@ -70,7 +70,7 @@ describe("Collection.tableOfContents", () => {
|
|
|
70
70
|
});
|
|
71
71
|
|
|
72
72
|
it("works without models registered (flat list)", async () => {
|
|
73
|
-
const bare = new Collection({ rootPath: FIXTURES_PATH });
|
|
73
|
+
const bare = new Collection({ rootPath: FIXTURES_PATH, autoDiscover: false });
|
|
74
74
|
await bare.load();
|
|
75
75
|
|
|
76
76
|
const toc = bare.tableOfContents();
|
|
@@ -89,3 +89,47 @@ describe("Collection.tableOfContents", () => {
|
|
|
89
89
|
);
|
|
90
90
|
});
|
|
91
91
|
});
|
|
92
|
+
|
|
93
|
+
describe("Collection.renderFileTree", () => {
|
|
94
|
+
it("renders a tree with directory structure", () => {
|
|
95
|
+
const tree = collection.renderFileTree();
|
|
96
|
+
|
|
97
|
+
expect(tree).toContain("epics/");
|
|
98
|
+
expect(tree).toContain("stories/");
|
|
99
|
+
expect(tree).toContain("authentication.mdx");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("nests files under their directories", () => {
|
|
103
|
+
const tree = collection.renderFileTree();
|
|
104
|
+
const lines = tree.split("\n");
|
|
105
|
+
|
|
106
|
+
// Find the stories directory line and the nested file
|
|
107
|
+
const storiesLine = lines.findIndex((l) => l.includes("stories/"));
|
|
108
|
+
const nestedFile = lines.findIndex((l) =>
|
|
109
|
+
l.includes("a-user-should-be-able-to-register.mdx")
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
expect(storiesLine).toBeGreaterThanOrEqual(0);
|
|
113
|
+
expect(nestedFile).toBeGreaterThan(storiesLine);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("uses tree connector characters", () => {
|
|
117
|
+
const tree = collection.renderFileTree();
|
|
118
|
+
|
|
119
|
+
expect(tree).toMatch(/[├└]/);
|
|
120
|
+
expect(tree).toMatch(/──/);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("ends with a newline", () => {
|
|
124
|
+
const tree = collection.renderFileTree();
|
|
125
|
+
expect(tree.endsWith("\n")).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("throws if collection not loaded", () => {
|
|
129
|
+
const unloaded = new Collection({ rootPath: FIXTURES_PATH });
|
|
130
|
+
|
|
131
|
+
expect(() => unloaded.renderFileTree()).toThrow(
|
|
132
|
+
"Collection has not been loaded"
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
});
|