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
|
@@ -1,356 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
-
import { toString } from "mdast-util-to-string";
|
|
3
|
-
import type { Heading } from "mdast";
|
|
4
|
-
import { Collection } from "../src/collection";
|
|
5
|
-
import { parse, extractSections } from "../src/index";
|
|
6
|
-
import { createTestCollection, FIXTURES_PATH } from "./helpers";
|
|
7
|
-
import path from "path";
|
|
8
|
-
|
|
9
|
-
let collection: Collection;
|
|
10
|
-
|
|
11
|
-
beforeEach(async () => {
|
|
12
|
-
collection = await createTestCollection();
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
describe("extractSections", () => {
|
|
16
|
-
describe("grouped mode (default)", () => {
|
|
17
|
-
it("groups sections under source document titles", () => {
|
|
18
|
-
const doc1 = collection.document("epics/authentication");
|
|
19
|
-
const doc2 = collection.document("epics/searching-and-browsing");
|
|
20
|
-
|
|
21
|
-
const combined = extractSections([
|
|
22
|
-
{ source: doc1, sections: "Stories" },
|
|
23
|
-
{ source: doc2, sections: "Stories" },
|
|
24
|
-
]);
|
|
25
|
-
|
|
26
|
-
const headings = combined.astQuery.selectAll("heading") as Heading[];
|
|
27
|
-
|
|
28
|
-
// Source titles become h1
|
|
29
|
-
expect(headings[0].depth).toBe(1);
|
|
30
|
-
expect(toString(headings[0])).toBe("Authentication");
|
|
31
|
-
|
|
32
|
-
// "Stories" section headings become h2
|
|
33
|
-
expect(headings[1].depth).toBe(2);
|
|
34
|
-
expect(toString(headings[1])).toBe("Stories");
|
|
35
|
-
|
|
36
|
-
// Sub-headings within Stories shift accordingly
|
|
37
|
-
expect(headings[2].depth).toBe(3);
|
|
38
|
-
expect(toString(headings[2])).toBe("A User should be able to register.");
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("with title, nests source titles under it", () => {
|
|
42
|
-
const doc1 = collection.document("epics/authentication");
|
|
43
|
-
const doc2 = collection.document("epics/searching-and-browsing");
|
|
44
|
-
|
|
45
|
-
const combined = extractSections(
|
|
46
|
-
[
|
|
47
|
-
{ source: doc1, sections: "Stories" },
|
|
48
|
-
{ source: doc2, sections: "Stories" },
|
|
49
|
-
],
|
|
50
|
-
{ title: "All Stories" },
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
const headings = combined.astQuery.selectAll("heading") as Heading[];
|
|
54
|
-
|
|
55
|
-
// Title is h1
|
|
56
|
-
expect(headings[0].depth).toBe(1);
|
|
57
|
-
expect(toString(headings[0])).toBe("All Stories");
|
|
58
|
-
|
|
59
|
-
// Source titles become h2
|
|
60
|
-
expect(headings[1].depth).toBe(2);
|
|
61
|
-
expect(toString(headings[1])).toBe("Authentication");
|
|
62
|
-
|
|
63
|
-
// Section headings become h3
|
|
64
|
-
expect(headings[2].depth).toBe(3);
|
|
65
|
-
expect(toString(headings[2])).toBe("Stories");
|
|
66
|
-
|
|
67
|
-
// Sub-headings shift to h4
|
|
68
|
-
expect(headings[3].depth).toBe(4);
|
|
69
|
-
expect(toString(headings[3])).toBe("A User should be able to register.");
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("extracts multiple sections from one source", () => {
|
|
73
|
-
const doc = collection.document("epics/authentication");
|
|
74
|
-
|
|
75
|
-
// The Authentication epic has "Stories" as a section.
|
|
76
|
-
// Within that, individual stories have "Acceptance Criteria" and "Mockups".
|
|
77
|
-
// Let's extract the top-level "Stories" section.
|
|
78
|
-
const combined = extractSections([
|
|
79
|
-
{ source: doc, sections: "Stories" },
|
|
80
|
-
]);
|
|
81
|
-
|
|
82
|
-
const headings = combined.astQuery.selectAll("heading") as Heading[];
|
|
83
|
-
|
|
84
|
-
// Source title is h1
|
|
85
|
-
expect(toString(headings[0])).toBe("Authentication");
|
|
86
|
-
expect(headings[0].depth).toBe(1);
|
|
87
|
-
|
|
88
|
-
// "Stories" is h2
|
|
89
|
-
expect(toString(headings[1])).toBe("Stories");
|
|
90
|
-
expect(headings[1].depth).toBe(2);
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
describe("flat mode", () => {
|
|
95
|
-
it("places sections sequentially without source grouping", () => {
|
|
96
|
-
const doc1 = collection.document("epics/authentication");
|
|
97
|
-
const doc2 = collection.document("epics/searching-and-browsing");
|
|
98
|
-
|
|
99
|
-
const combined = extractSections(
|
|
100
|
-
[
|
|
101
|
-
{ source: doc1, sections: "Stories" },
|
|
102
|
-
{ source: doc2, sections: "Stories" },
|
|
103
|
-
],
|
|
104
|
-
{ mode: "flat" },
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
const headings = combined.astQuery.selectAll("heading") as Heading[];
|
|
108
|
-
|
|
109
|
-
// Both "Stories" sections become h1 (no source grouping)
|
|
110
|
-
expect(headings[0].depth).toBe(1);
|
|
111
|
-
expect(toString(headings[0])).toBe("Stories");
|
|
112
|
-
|
|
113
|
-
// Sub-headings shift accordingly
|
|
114
|
-
expect(headings[1].depth).toBe(2);
|
|
115
|
-
expect(toString(headings[1])).toBe("A User should be able to register.");
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("with title, sections start at h2", () => {
|
|
119
|
-
const doc1 = collection.document("epics/authentication");
|
|
120
|
-
const doc2 = collection.document("epics/searching-and-browsing");
|
|
121
|
-
|
|
122
|
-
const combined = extractSections(
|
|
123
|
-
[
|
|
124
|
-
{ source: doc1, sections: "Stories" },
|
|
125
|
-
{ source: doc2, sections: "Stories" },
|
|
126
|
-
],
|
|
127
|
-
{ mode: "flat", title: "Combined Stories" },
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
const headings = combined.astQuery.selectAll("heading") as Heading[];
|
|
131
|
-
|
|
132
|
-
// Title is h1
|
|
133
|
-
expect(headings[0].depth).toBe(1);
|
|
134
|
-
expect(toString(headings[0])).toBe("Combined Stories");
|
|
135
|
-
|
|
136
|
-
// Section headings are h2
|
|
137
|
-
expect(headings[1].depth).toBe(2);
|
|
138
|
-
expect(toString(headings[1])).toBe("Stories");
|
|
139
|
-
|
|
140
|
-
// Sub-headings are h3
|
|
141
|
-
expect(headings[2].depth).toBe(3);
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
describe("works with ParsedDocument", () => {
|
|
146
|
-
it("accepts ParsedDocument as source", async () => {
|
|
147
|
-
const parsed = await parse(
|
|
148
|
-
path.join(FIXTURES_PATH, "epics/authentication.mdx"),
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
const combined = extractSections([
|
|
152
|
-
{ source: parsed, sections: "Stories" },
|
|
153
|
-
]);
|
|
154
|
-
|
|
155
|
-
const headings = combined.astQuery.selectAll("heading") as Heading[];
|
|
156
|
-
expect(toString(headings[0])).toBe("Authentication");
|
|
157
|
-
expect(headings[0].depth).toBe(1);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it("accepts mixed Document and ParsedDocument sources", async () => {
|
|
161
|
-
const doc = collection.document("epics/authentication");
|
|
162
|
-
const parsed = await parse(
|
|
163
|
-
path.join(FIXTURES_PATH, "epics/searching-and-browsing.mdx"),
|
|
164
|
-
);
|
|
165
|
-
|
|
166
|
-
const combined = extractSections([
|
|
167
|
-
{ source: doc, sections: "Stories" },
|
|
168
|
-
{ source: parsed, sections: "Stories" },
|
|
169
|
-
]);
|
|
170
|
-
|
|
171
|
-
const headings = combined.astQuery.selectAll("heading") as Heading[];
|
|
172
|
-
|
|
173
|
-
// Both source titles present
|
|
174
|
-
const titles = headings
|
|
175
|
-
.filter((h) => h.depth === 1)
|
|
176
|
-
.map((h) => toString(h));
|
|
177
|
-
expect(titles).toContain("Authentication");
|
|
178
|
-
expect(titles).toContain("Searching And Browsing");
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
describe("onMissing", () => {
|
|
183
|
-
it("skips missing sections by default", () => {
|
|
184
|
-
const doc = collection.document("epics/authentication");
|
|
185
|
-
|
|
186
|
-
const combined = extractSections([
|
|
187
|
-
{ source: doc, sections: "Nonexistent Section" },
|
|
188
|
-
]);
|
|
189
|
-
|
|
190
|
-
// Should have only the source title heading, no section content
|
|
191
|
-
const headings = combined.astQuery.selectAll("heading") as Heading[];
|
|
192
|
-
expect(headings).toHaveLength(1);
|
|
193
|
-
expect(toString(headings[0])).toBe("Authentication");
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it("throws on missing section when onMissing is 'throw'", () => {
|
|
197
|
-
const doc = collection.document("epics/authentication");
|
|
198
|
-
|
|
199
|
-
expect(() =>
|
|
200
|
-
extractSections(
|
|
201
|
-
[{ source: doc, sections: "Nonexistent Section" }],
|
|
202
|
-
{ onMissing: "throw" },
|
|
203
|
-
),
|
|
204
|
-
).toThrow("Heading not found");
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
describe("edge cases", () => {
|
|
209
|
-
it("returns empty document for empty entries", () => {
|
|
210
|
-
const combined = extractSections([]);
|
|
211
|
-
|
|
212
|
-
expect(combined.content).toBe("");
|
|
213
|
-
expect(combined.ast.children).toHaveLength(0);
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
it("returns document with only title for empty entries with title", () => {
|
|
217
|
-
const combined = extractSections([], { title: "Empty Doc" });
|
|
218
|
-
|
|
219
|
-
const headings = combined.astQuery.selectAll("heading") as Heading[];
|
|
220
|
-
expect(headings).toHaveLength(1);
|
|
221
|
-
expect(toString(headings[0])).toBe("Empty Doc");
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it("clamps heading depths to max 6", async () => {
|
|
225
|
-
// Create a document with deeply nested headings (h4+ subsections)
|
|
226
|
-
// In grouped mode with title, h4 sections would shift by +3 → h7, should clamp to h6
|
|
227
|
-
const deepDoc = await parse(
|
|
228
|
-
"# Doc\n## Section\n### Sub\n#### Deep\n##### Deeper\n###### Deepest\n\nContent here.",
|
|
229
|
-
);
|
|
230
|
-
|
|
231
|
-
const combined = extractSections(
|
|
232
|
-
[{ source: deepDoc, sections: "Section" }],
|
|
233
|
-
{ title: "Wrapper" },
|
|
234
|
-
);
|
|
235
|
-
|
|
236
|
-
const headings = combined.astQuery.selectAll("heading") as Heading[];
|
|
237
|
-
// All heading depths should be <= 6
|
|
238
|
-
for (const h of headings) {
|
|
239
|
-
expect(h.depth).toBeLessThanOrEqual(6);
|
|
240
|
-
expect(h.depth).toBeGreaterThanOrEqual(1);
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
it("handles source with no title using (Untitled)", async () => {
|
|
245
|
-
// A document with no heading — just content
|
|
246
|
-
const noTitleDoc = await parse("## Section\n\nSome content.");
|
|
247
|
-
|
|
248
|
-
const combined = extractSections([
|
|
249
|
-
{ source: noTitleDoc, sections: "Section" },
|
|
250
|
-
]);
|
|
251
|
-
|
|
252
|
-
const headings = combined.astQuery.selectAll("heading") as Heading[];
|
|
253
|
-
// In grouped mode, source title should be "(Untitled)" since doc title comes from first heading
|
|
254
|
-
// But actually, parse() sets title from first heading which is "Section" here
|
|
255
|
-
// Let's use a truly headingless doc
|
|
256
|
-
const bareDoc = await parse("Just a paragraph, no headings at all.");
|
|
257
|
-
const combined2 = extractSections([
|
|
258
|
-
{ source: bareDoc, sections: "Nonexistent" },
|
|
259
|
-
]);
|
|
260
|
-
const headings2 = combined2.astQuery.selectAll("heading") as Heading[];
|
|
261
|
-
expect(toString(headings2[0])).toBe("(Untitled)");
|
|
262
|
-
});
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
describe("returned ParsedDocument", () => {
|
|
266
|
-
it("has working content and stringify", () => {
|
|
267
|
-
const doc = collection.document("epics/authentication");
|
|
268
|
-
|
|
269
|
-
const combined = extractSections(
|
|
270
|
-
[{ source: doc, sections: "Stories" }],
|
|
271
|
-
{ title: "Test" },
|
|
272
|
-
);
|
|
273
|
-
|
|
274
|
-
expect(combined.content).toContain("# Test");
|
|
275
|
-
expect(combined.stringify()).toBe(combined.content);
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
it("has working extractSection", () => {
|
|
279
|
-
const doc = collection.document("epics/authentication");
|
|
280
|
-
|
|
281
|
-
const combined = extractSections([
|
|
282
|
-
{ source: doc, sections: "Stories" },
|
|
283
|
-
]);
|
|
284
|
-
|
|
285
|
-
// The combined doc should have "Stories" as an h2 under "Authentication"
|
|
286
|
-
const section = combined.extractSection("Stories");
|
|
287
|
-
expect(section.length).toBeGreaterThan(0);
|
|
288
|
-
expect(section[0].type).toBe("heading");
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
it("has working querySection", () => {
|
|
292
|
-
const doc = collection.document("epics/authentication");
|
|
293
|
-
|
|
294
|
-
const combined = extractSections([
|
|
295
|
-
{ source: doc, sections: "Stories" },
|
|
296
|
-
]);
|
|
297
|
-
|
|
298
|
-
const query = combined.querySection("Stories");
|
|
299
|
-
const subHeadings = query.selectAll("heading") as Heading[];
|
|
300
|
-
expect(subHeadings.length).toBeGreaterThan(0);
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
it("has working nodes accessor", () => {
|
|
304
|
-
const doc = collection.document("epics/authentication");
|
|
305
|
-
|
|
306
|
-
const combined = extractSections(
|
|
307
|
-
[{ source: doc, sections: "Stories" }],
|
|
308
|
-
{ title: "Test" },
|
|
309
|
-
);
|
|
310
|
-
|
|
311
|
-
expect(combined.nodes).toBeDefined();
|
|
312
|
-
expect(combined.title).toBe("Test");
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
it("has empty meta", () => {
|
|
316
|
-
const doc = collection.document("epics/authentication");
|
|
317
|
-
|
|
318
|
-
const combined = extractSections([
|
|
319
|
-
{ source: doc, sections: "Stories" },
|
|
320
|
-
]);
|
|
321
|
-
|
|
322
|
-
expect(combined.meta).toEqual({});
|
|
323
|
-
});
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
describe("sections as array", () => {
|
|
327
|
-
it("accepts string for single section", () => {
|
|
328
|
-
const doc = collection.document("epics/authentication");
|
|
329
|
-
|
|
330
|
-
const combined = extractSections([
|
|
331
|
-
{ source: doc, sections: "Stories" },
|
|
332
|
-
]);
|
|
333
|
-
|
|
334
|
-
const headings = combined.astQuery.selectAll("heading") as Heading[];
|
|
335
|
-
expect(headings.length).toBeGreaterThan(1);
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
it("accepts array for multiple sections", async () => {
|
|
339
|
-
// Use a parsed doc with distinct top-level sections
|
|
340
|
-
const doc = await parse(
|
|
341
|
-
"# My Doc\n\n## Section A\n\nContent A.\n\n## Section B\n\nContent B.\n\n## Section C\n\nContent C.",
|
|
342
|
-
);
|
|
343
|
-
|
|
344
|
-
const combined = extractSections([
|
|
345
|
-
{ source: doc, sections: ["Section A", "Section B"] },
|
|
346
|
-
]);
|
|
347
|
-
|
|
348
|
-
const headings = combined.astQuery.selectAll("heading") as Heading[];
|
|
349
|
-
const headingTexts = headings.map((h) => toString(h));
|
|
350
|
-
|
|
351
|
-
expect(headingTexts).toContain("Section A");
|
|
352
|
-
expect(headingTexts).toContain("Section B");
|
|
353
|
-
expect(headingTexts).not.toContain("Section C");
|
|
354
|
-
});
|
|
355
|
-
});
|
|
356
|
-
});
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
priority: high
|
|
3
|
-
status: created
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Authentication
|
|
7
|
-
|
|
8
|
-
The Authentication stories cover users logging in and out of the application, as well as the roles and permissions granted to these users and how they are enforced in the application.
|
|
9
|
-
|
|
10
|
-
## Stories
|
|
11
|
-
|
|
12
|
-
### A User should be able to register.
|
|
13
|
-
|
|
14
|
-
As a User I would like to register so that I can use the application.
|
|
15
|
-
|
|
16
|
-
#### Acceptance Criteria
|
|
17
|
-
|
|
18
|
-
- A user can visit the signup form, supply their name, email, and password
|
|
19
|
-
- The signup form should validate the user's information and supply errors
|
|
20
|
-
- The user should receive a confirmation email
|
|
21
|
-
- The user should show up in our database as confirmed after clicking the confirmation link
|
|
22
|
-
|
|
23
|
-
#### Mockups
|
|
24
|
-
|
|
25
|
-
- [Invision: Registration Form](https://invisionapp.com)
|
|
26
|
-
- [Invision: Registration Form Error State](https://invisionapp.com)
|
|
27
|
-
|
|
28
|
-
### A User should be able to login.
|
|
29
|
-
|
|
30
|
-
As a User I would like to login so that I can use the application.
|
|
31
|
-
|
|
32
|
-
#### Acceptance Criteria
|
|
33
|
-
|
|
34
|
-
- A user can visit the signup form, supply their name, email, and password
|
|
35
|
-
- The signup form should validate the user's information and supply errors
|
|
36
|
-
- The user should receive a confirmation email
|
|
37
|
-
- The user should show up in our database as confirmed after clicking the confirmation link
|
|
38
|
-
|
|
39
|
-
#### Mockups
|
|
40
|
-
|
|
41
|
-
- [Invision: Login Form](https://invisionapp.com)
|
|
42
|
-
- [Invision: Login Form Error State ](https://invisionapp.com)
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
status: created
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# Searching And Browsing
|
|
6
|
-
|
|
7
|
-
This epic covers the stories related to searching for a specific item.
|
|
8
|
-
|
|
9
|
-
## Stories
|
|
10
|
-
|
|
11
|
-
### Searching for a product by category
|
|
12
|
-
|
|
13
|
-
As a user I want to be able to search for a product by category so that I can find the right product for me.
|
|
14
|
-
|
|
15
|
-
### Searching for a product by manufacturer
|
|
16
|
-
|
|
17
|
-
As a user I want to be able to search for a product by manufacturer so that I can find the right product for me.
|
|
18
|
-
|
|
19
|
-
### Searching for a product by name
|
|
20
|
-
|
|
21
|
-
As a user I want to be able to search for a product by name so that I can find the right product for me.
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
defineModel,
|
|
3
|
-
section,
|
|
4
|
-
hasMany,
|
|
5
|
-
belongsTo,
|
|
6
|
-
z,
|
|
7
|
-
type ModelDefinition,
|
|
8
|
-
type HasManyDefinition,
|
|
9
|
-
type BelongsToDefinition,
|
|
10
|
-
type SectionDefinition,
|
|
11
|
-
} from "../../../src/index";
|
|
12
|
-
import { toString } from "mdast-util-to-string";
|
|
13
|
-
|
|
14
|
-
const epicMeta = z.object({
|
|
15
|
-
priority: z.enum(["low", "medium", "high"]).optional(),
|
|
16
|
-
status: z
|
|
17
|
-
.enum(["created", "in-progress", "complete"])
|
|
18
|
-
.default("created"),
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
const storyMeta = z.object({
|
|
22
|
-
status: z
|
|
23
|
-
.enum(["created", "in-progress", "complete"])
|
|
24
|
-
.default("created"),
|
|
25
|
-
epic: z.string().optional(),
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
/** Explicit type for Epic so circular Epic↔Story inference doesn’t collapse to never */
|
|
29
|
-
export type EpicDef = ModelDefinition<
|
|
30
|
-
"Epic",
|
|
31
|
-
typeof epicMeta,
|
|
32
|
-
Record<string, never>,
|
|
33
|
-
{ stories: HasManyDefinition<StoryDef> },
|
|
34
|
-
{ isComplete: (self: any) => boolean }
|
|
35
|
-
>;
|
|
36
|
-
|
|
37
|
-
/** Explicit type for Story so circular Epic↔Story inference doesn’t collapse to never */
|
|
38
|
-
export type StoryDef = ModelDefinition<
|
|
39
|
-
"Story",
|
|
40
|
-
typeof storyMeta,
|
|
41
|
-
Record<string, SectionDefinition<any>>,
|
|
42
|
-
{ epic: BelongsToDefinition<EpicDef> },
|
|
43
|
-
{ isComplete: (self: any) => boolean }
|
|
44
|
-
>;
|
|
45
|
-
|
|
46
|
-
export const Epic: EpicDef = defineModel("Epic", {
|
|
47
|
-
prefix: "epics",
|
|
48
|
-
meta: epicMeta,
|
|
49
|
-
relationships: {
|
|
50
|
-
stories: hasMany(() => Story, {
|
|
51
|
-
heading: "Stories",
|
|
52
|
-
}),
|
|
53
|
-
},
|
|
54
|
-
computed: {
|
|
55
|
-
isComplete: (self: any) => self.meta.status === "complete",
|
|
56
|
-
},
|
|
57
|
-
defaults: {
|
|
58
|
-
status: "created",
|
|
59
|
-
},
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
export const Story: StoryDef = defineModel("Story", {
|
|
63
|
-
prefix: "stories",
|
|
64
|
-
meta: storyMeta,
|
|
65
|
-
sections: {
|
|
66
|
-
acceptanceCriteria: section("Acceptance Criteria", {
|
|
67
|
-
extract: (query) =>
|
|
68
|
-
query.selectAll("listItem").map((n) => toString(n)),
|
|
69
|
-
schema: z.array(z.string()),
|
|
70
|
-
}),
|
|
71
|
-
mockups: section("Mockups", {
|
|
72
|
-
extract: (query) =>
|
|
73
|
-
Object.fromEntries(
|
|
74
|
-
query
|
|
75
|
-
.selectAll("link")
|
|
76
|
-
.map((l: any) => [toString(l), l.url])
|
|
77
|
-
),
|
|
78
|
-
schema: z.record(z.string(), z.string()),
|
|
79
|
-
}),
|
|
80
|
-
},
|
|
81
|
-
relationships: {
|
|
82
|
-
epic: belongsTo(() => Epic, {
|
|
83
|
-
foreignKey: (doc) => doc.meta.epic as string,
|
|
84
|
-
}),
|
|
85
|
-
},
|
|
86
|
-
computed: {
|
|
87
|
-
isComplete: (self: any) => self.meta.status === "complete",
|
|
88
|
-
},
|
|
89
|
-
});
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
status: created
|
|
3
|
-
epic: authentication
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# A User should be able to register.
|
|
7
|
-
|
|
8
|
-
As a User I would like to register so that I can use the application.
|
|
9
|
-
|
|
10
|
-
## Acceptance Criteria
|
|
11
|
-
|
|
12
|
-
- A user can visit the signup form, supply their name, email, and password
|
|
13
|
-
- The signup form should validate the user's information and supply errors
|
|
14
|
-
- The user should receive a confirmation email
|
|
15
|
-
- The user should show up in our database as confirmed after clicking the confirmation link
|
|
16
|
-
|
|
17
|
-
## Mockups
|
|
18
|
-
|
|
19
|
-
- [Invision: Registration Form](https://invisionapp.com)
|
|
20
|
-
- [Invision: Registration Form Error State](https://invisionapp.com)
|
package/test/helpers.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import { Collection } from "../src/collection";
|
|
3
|
-
import { Epic, Story } from "./fixtures/sdlc/models";
|
|
4
|
-
|
|
5
|
-
const dir = import.meta.dirname ?? new URL(".", import.meta.url).pathname;
|
|
6
|
-
|
|
7
|
-
export const FIXTURES_PATH = path.resolve(
|
|
8
|
-
dir,
|
|
9
|
-
"fixtures/sdlc"
|
|
10
|
-
);
|
|
11
|
-
|
|
12
|
-
export async function createTestCollection(): Promise<Collection> {
|
|
13
|
-
const collection = new Collection({
|
|
14
|
-
rootPath: FIXTURES_PATH,
|
|
15
|
-
name: "test-sdlc",
|
|
16
|
-
});
|
|
17
|
-
collection.register(Epic);
|
|
18
|
-
collection.register(Story);
|
|
19
|
-
await collection.load();
|
|
20
|
-
return collection;
|
|
21
|
-
}
|