contentbase 0.0.1 → 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.
Files changed (97) hide show
  1. package/.mcp.json +9 -0
  2. package/CLI.md +593 -0
  3. package/MCP-DESCRIPTIONS-REVIEW.md +122 -0
  4. package/MCP-SERVER-SPEC.md +453 -0
  5. package/PRIMER.md +540 -0
  6. package/README.md +406 -13
  7. package/bun.lock +45 -6
  8. package/dist/cnotes +0 -0
  9. package/docs/README.md +110 -0
  10. package/docs/TABLE-OF-CONTENTS.md +7 -0
  11. package/docs/models.ts +38 -0
  12. package/models.ts +38 -0
  13. package/package.json +14 -4
  14. package/public/web-demo/index.html +813 -0
  15. package/scripts/examples/01-collection-setup.ts +46 -0
  16. package/scripts/examples/02-querying.ts +67 -0
  17. package/scripts/examples/03-sections.ts +36 -0
  18. package/scripts/examples/04-relationships.ts +54 -0
  19. package/scripts/examples/05-document-api.ts +54 -0
  20. package/scripts/examples/06-extract-sections.ts +55 -0
  21. package/scripts/examples/07-validation.ts +46 -0
  22. package/scripts/examples/08-serialization.ts +51 -0
  23. package/scripts/examples/lib/format.ts +87 -0
  24. package/scripts/examples/lib/setup.ts +21 -0
  25. package/scripts/examples/run-all.ts +43 -0
  26. package/showcases/node_modules/.cache/.repl_history +3 -0
  27. package/showcases/vinyl-collection/artists/nina-simone.mdx +1 -1
  28. package/src/__tests__/semantic-search.integration.test.ts +284 -0
  29. package/src/api/endpoints/actions.ts +34 -0
  30. package/src/api/endpoints/doc.ts +187 -0
  31. package/src/api/endpoints/docs-index.ts +26 -0
  32. package/src/api/endpoints/document.ts +171 -0
  33. package/src/api/endpoints/documents.ts +71 -0
  34. package/src/api/endpoints/inspect.ts +16 -0
  35. package/src/api/endpoints/models.ts +10 -0
  36. package/src/api/endpoints/query.ts +88 -0
  37. package/src/api/endpoints/root.ts +7 -0
  38. package/src/api/endpoints/search-reindex.ts +65 -0
  39. package/src/api/endpoints/search-status.ts +55 -0
  40. package/src/api/endpoints/search.ts +104 -0
  41. package/src/api/endpoints/semantic-search.ts +120 -0
  42. package/src/api/endpoints/text-search.ts +63 -0
  43. package/src/api/endpoints/validate.ts +34 -0
  44. package/src/api/helpers.ts +97 -0
  45. package/src/base-model.ts +12 -0
  46. package/src/cli/commands/action.ts +82 -44
  47. package/src/cli/commands/console.ts +124 -0
  48. package/src/cli/commands/create.ts +179 -53
  49. package/src/cli/commands/embed.ts +323 -0
  50. package/src/cli/commands/export.ts +58 -24
  51. package/src/cli/commands/extract.ts +174 -0
  52. package/src/cli/commands/help.ts +81 -0
  53. package/src/cli/commands/index.ts +17 -0
  54. package/src/cli/commands/init.ts +72 -48
  55. package/src/cli/commands/inspect.ts +56 -46
  56. package/src/cli/commands/mcp.ts +1219 -0
  57. package/src/cli/commands/search.ts +285 -0
  58. package/src/cli/commands/serve.ts +348 -0
  59. package/src/cli/commands/summary.ts +60 -0
  60. package/src/cli/commands/teach.ts +86 -0
  61. package/src/cli/commands/text-search.ts +134 -0
  62. package/src/cli/commands/validate.ts +126 -64
  63. package/src/cli/index.ts +88 -19
  64. package/src/cli/load-collection.ts +144 -17
  65. package/src/cli/registry.ts +28 -0
  66. package/src/collection.ts +455 -6
  67. package/src/define-model.ts +104 -7
  68. package/src/document.ts +47 -1
  69. package/src/extract-sections.ts +222 -0
  70. package/src/index.ts +20 -2
  71. package/src/model-instance.ts +35 -5
  72. package/src/node-shortcuts.ts +1 -1
  73. package/src/query/collection-query.ts +96 -9
  74. package/src/query/index.ts +7 -0
  75. package/src/query/query-dsl.ts +259 -0
  76. package/src/relationships/has-many.ts +39 -0
  77. package/src/relationships/index.ts +7 -2
  78. package/src/section.ts +2 -0
  79. package/src/types.ts +24 -2
  80. package/src/utils/index.ts +1 -0
  81. package/src/utils/match-pattern.ts +65 -0
  82. package/src/validator.ts +18 -1
  83. package/test/collection.test.ts +118 -2
  84. package/test/extract-sections.test.ts +356 -0
  85. package/test/fixtures/sdlc/MODELS.md +106 -0
  86. package/test/fixtures/sdlc/SKILL.md +404 -0
  87. package/test/fixtures/sdlc/index.ts +9 -0
  88. package/test/fixtures/sdlc/models.ts +8 -6
  89. package/test/fixtures/sdlc/stories/authentication/a-user-should-be-able-to-login.mdx +20 -0
  90. package/test/fixtures/sdlc/templates/epic.md +23 -0
  91. package/test/fixtures/sdlc/templates/story.md +19 -0
  92. package/test/pattern.test.ts +191 -0
  93. package/test/query-dsl.test.ts +431 -0
  94. package/test/query.test.ts +29 -0
  95. package/test/relationships.test.ts +130 -0
  96. package/test/section.test.ts +65 -4
  97. package/test/table-of-contents.test.ts +135 -0
@@ -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
 
@@ -49,8 +49,8 @@ describe("section helper", () => {
49
49
  const TestModel = {
50
50
  name: "Test",
51
51
  prefix: "test",
52
- meta: z.object({}).passthrough(),
53
- schema: z.object({}).passthrough(),
52
+ meta: z.looseObject({}),
53
+ schema: z.looseObject({}),
54
54
  sections: {
55
55
  items: section("Acceptance Criteria", {
56
56
  extract: (q) => {
@@ -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({
@@ -81,8 +142,8 @@ describe("section helper", () => {
81
142
  const TestModel = {
82
143
  name: "Test",
83
144
  prefix: "test",
84
- meta: z.object({}).passthrough(),
85
- schema: z.object({}).passthrough(),
145
+ meta: z.looseObject({}),
146
+ schema: z.looseObject({}),
86
147
  sections: {
87
148
  missing: section("Nonexistent Section", {
88
149
  extract: (q) => q.selectAll("listItem").map((n) => toString(n)),
@@ -0,0 +1,135 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { Collection } from "../src/collection";
3
+ import { createTestCollection, FIXTURES_PATH } from "./helpers";
4
+
5
+ let collection: Collection;
6
+
7
+ beforeEach(async () => {
8
+ collection = await createTestCollection();
9
+ });
10
+
11
+ describe("Collection.tableOfContents", () => {
12
+ it("generates markdown with model group headings", () => {
13
+ const toc = collection.tableOfContents();
14
+
15
+ expect(toc).toContain("## Epics");
16
+ expect(toc).toContain("## Stories");
17
+ });
18
+
19
+ it("includes document titles as link text", () => {
20
+ const toc = collection.tableOfContents();
21
+
22
+ expect(toc).toContain("[Authentication]");
23
+ expect(toc).toContain("[Searching And Browsing]");
24
+ });
25
+
26
+ it("generates relative links with file extensions", () => {
27
+ const toc = collection.tableOfContents();
28
+
29
+ expect(toc).toContain("(./epics/authentication.mdx)");
30
+ expect(toc).toContain("(./epics/searching-and-browsing.mdx)");
31
+ });
32
+
33
+ it("uses custom basePath for links", () => {
34
+ const toc = collection.tableOfContents({ basePath: "./content" });
35
+
36
+ expect(toc).toContain("(./content/epics/authentication.mdx)");
37
+ });
38
+
39
+ it("adds a title heading when provided", () => {
40
+ const toc = collection.tableOfContents({ title: "Project Docs" });
41
+
42
+ expect(toc).toContain("# Project Docs");
43
+ // Model group headings shift to h2
44
+ expect(toc).toContain("## Epics");
45
+ expect(toc).toContain("## Stories");
46
+ });
47
+
48
+ it("sorts items alphabetically within groups", () => {
49
+ const toc = collection.tableOfContents();
50
+ const authIndex = toc.indexOf("[Authentication]");
51
+ const searchIndex = toc.indexOf("[Searching And Browsing]");
52
+
53
+ expect(authIndex).toBeLessThan(searchIndex);
54
+ });
55
+
56
+ it("formats entries as markdown list items", () => {
57
+ const toc = collection.tableOfContents();
58
+ const lines = toc.split("\n");
59
+ const listLines = lines.filter((l) => l.startsWith("- ["));
60
+
61
+ expect(listLines.length).toBeGreaterThanOrEqual(3);
62
+ for (const line of listLines) {
63
+ expect(line).toMatch(/^- \[.+\]\(.+\.mdx?\)$/);
64
+ }
65
+ });
66
+
67
+ it("ends with a newline", () => {
68
+ const toc = collection.tableOfContents();
69
+ expect(toc.endsWith("\n")).toBe(true);
70
+ });
71
+
72
+ it("works without models registered (flat list)", async () => {
73
+ const bare = new Collection({ rootPath: FIXTURES_PATH, autoDiscover: false });
74
+ await bare.load();
75
+
76
+ const toc = bare.tableOfContents();
77
+
78
+ // No group headings — just list items
79
+ expect(toc).not.toContain("# Epic");
80
+ expect(toc).toContain("- [");
81
+ expect(toc).toContain("(./epics/authentication.mdx)");
82
+ });
83
+
84
+ it("throws if collection not loaded", () => {
85
+ const unloaded = new Collection({ rootPath: FIXTURES_PATH });
86
+
87
+ expect(() => unloaded.tableOfContents()).toThrow(
88
+ "Collection has not been loaded"
89
+ );
90
+ });
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
+ });