contentbase 0.0.4 → 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/package.json +1 -1
- package/.mcp.json +0 -9
- package/MCP-DESCRIPTIONS-REVIEW.md +0 -122
- package/bun.lock +0 -512
- package/dist/cnotes +0 -0
- package/examples/sdlc-queries.ts +0 -161
- package/public/web-demo/index.html +0 -813
- 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/node_modules/.cache/.repl_history +0 -3
- 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/src/__tests__/semantic-search.integration.test.ts +0 -284
- package/test/ast-query.test.ts +0 -128
- package/test/collection.test.ts +0 -215
- 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/MODELS.md +0 -106
- package/test/fixtures/sdlc/SKILL.md +0 -404
- package/test/fixtures/sdlc/epics/authentication.mdx +0 -42
- package/test/fixtures/sdlc/epics/searching-and-browsing.mdx +0 -21
- package/test/fixtures/sdlc/index.ts +0 -9
- package/test/fixtures/sdlc/models.ts +0 -91
- package/test/fixtures/sdlc/stories/authentication/a-user-should-be-able-to-login.mdx +0 -20
- package/test/fixtures/sdlc/stories/authentication/a-user-should-be-able-to-register.mdx +0 -20
- package/test/fixtures/sdlc/templates/epic.md +0 -23
- package/test/fixtures/sdlc/templates/story.md +0 -19
- package/test/helpers.ts +0 -21
- package/test/model-instance.test.ts +0 -197
- package/test/pattern.test.ts +0 -191
- package/test/query-dsl.test.ts +0 -431
- package/test/query.test.ts +0 -196
- package/test/relationships.test.ts +0 -214
- package/test/section.test.ts +0 -160
- package/test/table-of-contents.test.ts +0 -135
- package/test/validator.test.ts +0 -62
- package/tsconfig.json +0 -18
- package/vitest.config.ts +0 -11
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
-
import { Collection } from "../src/collection";
|
|
3
|
-
import { createModelInstance } from "../src/model-instance";
|
|
4
|
-
import { defineModel, hasMany, belongsTo, z } from "../src/index";
|
|
5
|
-
import { createTestCollection } from "./helpers";
|
|
6
|
-
import { Epic, Story } from "./fixtures/sdlc/models";
|
|
7
|
-
|
|
8
|
-
describe("HasManyRelationship", () => {
|
|
9
|
-
let collection: Collection;
|
|
10
|
-
|
|
11
|
-
beforeEach(async () => {
|
|
12
|
-
collection = await createTestCollection();
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it("extracts child headings from parent section", () => {
|
|
16
|
-
const epic = collection.getModel("epics/authentication", Epic);
|
|
17
|
-
const stories = epic.relationships.stories.fetchAll();
|
|
18
|
-
expect(stories.length).toBe(2);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it("child instances have correct titles", () => {
|
|
22
|
-
const epic = collection.getModel("epics/authentication", Epic);
|
|
23
|
-
const stories = epic.relationships.stories.fetchAll();
|
|
24
|
-
expect(stories[0].title).toContain("register");
|
|
25
|
-
expect(stories[1].title).toContain("login");
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("first returns first child", () => {
|
|
29
|
-
const epic = collection.getModel("epics/authentication", Epic);
|
|
30
|
-
const first = epic.relationships.stories.first();
|
|
31
|
-
expect(first).toBeDefined();
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("last returns last child", () => {
|
|
35
|
-
const epic = collection.getModel("epics/authentication", Epic);
|
|
36
|
-
const last = epic.relationships.stories.last();
|
|
37
|
-
expect(last).toBeDefined();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("works with epic that has multiple stories", () => {
|
|
41
|
-
const epic = collection.getModel(
|
|
42
|
-
"epics/searching-and-browsing",
|
|
43
|
-
Epic
|
|
44
|
-
);
|
|
45
|
-
const stories = epic.relationships.stories.fetchAll();
|
|
46
|
-
expect(stories.length).toBe(3);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("computes IDs as targetPrefix/parentSlug/childSlug", () => {
|
|
50
|
-
const epic = collection.getModel("epics/authentication", Epic);
|
|
51
|
-
const stories = epic.relationships.stories.fetchAll();
|
|
52
|
-
expect(stories[0].id).toContain("stories/");
|
|
53
|
-
expect(stories[0].id).toContain("authentication");
|
|
54
|
-
});
|
|
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
|
-
|
|
186
|
-
describe("BelongsToRelationship", () => {
|
|
187
|
-
let collection: Collection;
|
|
188
|
-
|
|
189
|
-
beforeEach(async () => {
|
|
190
|
-
collection = await createTestCollection();
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it("resolves parent by foreign key", () => {
|
|
194
|
-
const story = collection.getModel(
|
|
195
|
-
"stories/authentication/a-user-should-be-able-to-register",
|
|
196
|
-
Story
|
|
197
|
-
);
|
|
198
|
-
const epic = story.relationships.epic.fetch();
|
|
199
|
-
expect(epic.title).toBe("Authentication");
|
|
200
|
-
expect(epic.id).toBe("epics/authentication");
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it("throws if parent not found", () => {
|
|
204
|
-
const doc = collection.createDocument({
|
|
205
|
-
id: "test/orphan",
|
|
206
|
-
content: "# Orphan Story\n",
|
|
207
|
-
meta: { epic: "nonexistent" },
|
|
208
|
-
});
|
|
209
|
-
const instance = createModelInstance(doc, Story, collection);
|
|
210
|
-
expect(() => instance.relationships.epic.fetch()).toThrow(
|
|
211
|
-
'Could not find Epic'
|
|
212
|
-
);
|
|
213
|
-
});
|
|
214
|
-
});
|
package/test/section.test.ts
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
-
import { section } from "../src/section";
|
|
3
|
-
import { Collection } from "../src/collection";
|
|
4
|
-
import { createModelInstance } from "../src/model-instance";
|
|
5
|
-
import { createTestCollection } from "./helpers";
|
|
6
|
-
import { Story } from "./fixtures/sdlc/models";
|
|
7
|
-
import { z } from "zod";
|
|
8
|
-
import { toString } from "mdast-util-to-string";
|
|
9
|
-
|
|
10
|
-
describe("section helper", () => {
|
|
11
|
-
it("creates a SectionDefinition with heading and extract", () => {
|
|
12
|
-
const sd = section("My Section", {
|
|
13
|
-
extract: (q) => q.selectAll("listItem"),
|
|
14
|
-
});
|
|
15
|
-
expect(sd.heading).toBe("My Section");
|
|
16
|
-
expect(sd.extract).toBeDefined();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("stores optional schema", () => {
|
|
20
|
-
const schema = z.array(z.string());
|
|
21
|
-
const sd = section("Items", {
|
|
22
|
-
extract: (q) =>
|
|
23
|
-
q.selectAll("listItem").map((n) => toString(n)),
|
|
24
|
-
schema,
|
|
25
|
-
});
|
|
26
|
-
expect(sd.schema).toBe(schema);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("extract receives AstQuery scoped to section content only", async () => {
|
|
30
|
-
const collection = await createTestCollection();
|
|
31
|
-
const doc = collection.document(
|
|
32
|
-
"stories/authentication/a-user-should-be-able-to-register"
|
|
33
|
-
);
|
|
34
|
-
const instance = createModelInstance(doc, Story, collection);
|
|
35
|
-
|
|
36
|
-
// acceptanceCriteria should only contain items from that section
|
|
37
|
-
const criteria = instance.sections.acceptanceCriteria;
|
|
38
|
-
expect(criteria.length).toBe(4);
|
|
39
|
-
expect(criteria[0]).toContain("signup form");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("section data is lazily computed", async () => {
|
|
43
|
-
const collection = await createTestCollection();
|
|
44
|
-
const doc = collection.document(
|
|
45
|
-
"stories/authentication/a-user-should-be-able-to-register"
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
let extractCalled = 0;
|
|
49
|
-
const TestModel = {
|
|
50
|
-
name: "Test",
|
|
51
|
-
prefix: "test",
|
|
52
|
-
meta: z.looseObject({}),
|
|
53
|
-
schema: z.looseObject({}),
|
|
54
|
-
sections: {
|
|
55
|
-
items: section("Acceptance Criteria", {
|
|
56
|
-
extract: (q) => {
|
|
57
|
-
extractCalled++;
|
|
58
|
-
return q.selectAll("listItem").map((n) => toString(n));
|
|
59
|
-
},
|
|
60
|
-
}),
|
|
61
|
-
},
|
|
62
|
-
relationships: {},
|
|
63
|
-
computed: {},
|
|
64
|
-
} as any;
|
|
65
|
-
|
|
66
|
-
const instance = createModelInstance(doc, TestModel, collection);
|
|
67
|
-
expect(extractCalled).toBe(0); // Not yet accessed
|
|
68
|
-
const _items = instance.sections.items;
|
|
69
|
-
expect(extractCalled).toBe(1); // Now extracted
|
|
70
|
-
const _items2 = instance.sections.items;
|
|
71
|
-
expect(extractCalled).toBe(1); // Cached
|
|
72
|
-
});
|
|
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
|
-
|
|
135
|
-
it("missing section returns empty data", async () => {
|
|
136
|
-
const collection = await createTestCollection();
|
|
137
|
-
const doc = collection.createDocument({
|
|
138
|
-
id: "test/no-section",
|
|
139
|
-
content: "# Just a title\n\nSome content.\n",
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
const TestModel = {
|
|
143
|
-
name: "Test",
|
|
144
|
-
prefix: "test",
|
|
145
|
-
meta: z.looseObject({}),
|
|
146
|
-
schema: z.looseObject({}),
|
|
147
|
-
sections: {
|
|
148
|
-
missing: section("Nonexistent Section", {
|
|
149
|
-
extract: (q) => q.selectAll("listItem").map((n) => toString(n)),
|
|
150
|
-
}),
|
|
151
|
-
},
|
|
152
|
-
relationships: {},
|
|
153
|
-
computed: {},
|
|
154
|
-
} as any;
|
|
155
|
-
|
|
156
|
-
const instance = createModelInstance(doc, TestModel, collection);
|
|
157
|
-
// Should not crash, just return empty
|
|
158
|
-
expect(instance.sections.missing).toEqual([]);
|
|
159
|
-
});
|
|
160
|
-
});
|
|
@@ -1,135 +0,0 @@
|
|
|
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
|
-
});
|
package/test/validator.test.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
-
import { validateDocument } from "../src/validator";
|
|
3
|
-
import { Collection } from "../src/collection";
|
|
4
|
-
import { createTestCollection } from "./helpers";
|
|
5
|
-
import { Epic, Story } from "./fixtures/sdlc/models";
|
|
6
|
-
|
|
7
|
-
describe("validateDocument", () => {
|
|
8
|
-
let collection: Collection;
|
|
9
|
-
|
|
10
|
-
beforeEach(async () => {
|
|
11
|
-
collection = await createTestCollection();
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it("returns valid for good meta", () => {
|
|
15
|
-
const doc = collection.document("epics/authentication");
|
|
16
|
-
const result = validateDocument(doc, Epic);
|
|
17
|
-
expect(result.valid).toBe(true);
|
|
18
|
-
expect(result.errors.length).toBe(0);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it("returns errors for invalid meta", () => {
|
|
22
|
-
const doc = collection.createDocument({
|
|
23
|
-
id: "test/bad",
|
|
24
|
-
content: "# Bad\n",
|
|
25
|
-
meta: { status: "BOGUS" },
|
|
26
|
-
});
|
|
27
|
-
const result = validateDocument(doc, Epic);
|
|
28
|
-
expect(result.valid).toBe(false);
|
|
29
|
-
expect(result.errors.length).toBeGreaterThan(0);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("applies defaults before validation", () => {
|
|
33
|
-
const doc = collection.createDocument({
|
|
34
|
-
id: "test/defaults",
|
|
35
|
-
content: "# Defaults\n",
|
|
36
|
-
meta: {},
|
|
37
|
-
});
|
|
38
|
-
const result = validateDocument(doc, Epic);
|
|
39
|
-
expect(result.valid).toBe(true);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("validates sections when schema is present", () => {
|
|
43
|
-
const doc = collection.document(
|
|
44
|
-
"stories/authentication/a-user-should-be-able-to-register"
|
|
45
|
-
);
|
|
46
|
-
const result = validateDocument(doc, Story);
|
|
47
|
-
expect(result.valid).toBe(true);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("section validation errors include section key in path", () => {
|
|
51
|
-
const doc = collection.createDocument({
|
|
52
|
-
id: "test/bad-section",
|
|
53
|
-
content: "# Bad\n\n## Acceptance Criteria\n\nNo list items here.\n",
|
|
54
|
-
meta: { status: "created" },
|
|
55
|
-
});
|
|
56
|
-
const result = validateDocument(doc, Story);
|
|
57
|
-
// acceptanceCriteria extracts listItems; paragraph text won't produce any
|
|
58
|
-
// The schema is z.array(z.string()) which allows empty arrays
|
|
59
|
-
// so this should actually pass - the extract returns []
|
|
60
|
-
expect(result.valid).toBe(true);
|
|
61
|
-
});
|
|
62
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ESNext",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"strict": true,
|
|
7
|
-
"esModuleInterop": true,
|
|
8
|
-
"declaration": true,
|
|
9
|
-
"declarationMap": true,
|
|
10
|
-
"sourceMap": true,
|
|
11
|
-
"outDir": "./dist",
|
|
12
|
-
"rootDir": "./src",
|
|
13
|
-
"types": ["bun-types"],
|
|
14
|
-
"skipLibCheck": true
|
|
15
|
-
},
|
|
16
|
-
"include": ["src/**/*.ts"],
|
|
17
|
-
"exclude": ["node_modules", "dist", "test"]
|
|
18
|
-
}
|