@crewhaus/context-bundle 0.1.1 → 0.1.2
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 +5 -10
- package/src/index.test.ts +79 -1
- package/src/index.ts +4 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewhaus/context-bundle",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Builds a single-markdown manifest of CrewHaus docs + recipes + schema. Consumed by `crewhaus context --bundle` and the cloud demo prompt builder.",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"license": "Apache-2.0",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "Max Meier",
|
|
18
|
-
"email": "max@
|
|
19
|
-
"url": "https://
|
|
18
|
+
"email": "max@crewhaus.ai",
|
|
19
|
+
"url": "https://crewhaus.ai"
|
|
20
20
|
},
|
|
21
21
|
"repository": {
|
|
22
22
|
"type": "git",
|
|
@@ -28,12 +28,7 @@
|
|
|
28
28
|
"url": "https://github.com/crewhaus/factory/issues"
|
|
29
29
|
},
|
|
30
30
|
"publishConfig": {
|
|
31
|
-
"access": "
|
|
31
|
+
"access": "public"
|
|
32
32
|
},
|
|
33
|
-
"files": [
|
|
34
|
-
"src",
|
|
35
|
-
"README.md",
|
|
36
|
-
"LICENSE",
|
|
37
|
-
"NOTICE"
|
|
38
|
-
]
|
|
33
|
+
"files": ["src", "README.md", "LICENSE", "NOTICE"]
|
|
39
34
|
}
|
package/src/index.test.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
|
-
import { buildContextBundle, discoverRoots } from "./index";
|
|
5
|
+
import { buildContextBundle, discoverRoots, sliceSpecSchemaExcerpt } from "./index";
|
|
6
6
|
|
|
7
7
|
function makeFixtureTree(): {
|
|
8
8
|
factoryRoot: string;
|
|
@@ -144,4 +144,82 @@ describe("discoverRoots", () => {
|
|
|
144
144
|
test("throws when roots cannot be located", () => {
|
|
145
145
|
expect(() => discoverRoots({ startDir: tmpdir(), env: {} })).toThrow(/Could not locate/);
|
|
146
146
|
});
|
|
147
|
+
|
|
148
|
+
test("throws when an env-provided root is missing a required marker", () => {
|
|
149
|
+
const { factoryRoot, docsRoot } = makeFixtureTree();
|
|
150
|
+
// Point the demos root at a directory that exists but lacks recipes/INDEX.md.
|
|
151
|
+
const emptyDemos = join(
|
|
152
|
+
tmpdir(),
|
|
153
|
+
`crewhaus-context-bundle-empty-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
154
|
+
);
|
|
155
|
+
mkdirSync(emptyDemos, { recursive: true });
|
|
156
|
+
|
|
157
|
+
expect(() =>
|
|
158
|
+
discoverRoots({
|
|
159
|
+
startDir: "/tmp",
|
|
160
|
+
env: {
|
|
161
|
+
CREWHAUS_FACTORY_ROOT: factoryRoot,
|
|
162
|
+
CREWHAUS_DOCS_ROOT: docsRoot,
|
|
163
|
+
CREWHAUS_DEMOS_ROOT: emptyDemos,
|
|
164
|
+
},
|
|
165
|
+
}),
|
|
166
|
+
).toThrow(/missing required file: recipes\/INDEX\.md/);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("recipe heading fallback", () => {
|
|
171
|
+
test("falls back to the file basename when a recipe has no top-level heading", () => {
|
|
172
|
+
const { factoryRoot, docsRoot, demosRoot } = makeFixtureTree();
|
|
173
|
+
// A recipe whose only headings are deeper than h1 (no "# " line) must
|
|
174
|
+
// fall back to its basename in the recipe index.
|
|
175
|
+
writeFileSync(
|
|
176
|
+
join(demosRoot, "recipes/99-no-top-heading.md"),
|
|
177
|
+
"## Subheading only\n\nbody without an h1\n",
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const { markdown } = buildContextBundle({
|
|
181
|
+
factoryRoot,
|
|
182
|
+
docsRoot,
|
|
183
|
+
demosRoot,
|
|
184
|
+
bundledAt: "2026-05-21T00:00:00Z",
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
expect(markdown).toContain(
|
|
188
|
+
"- [99-no-top-heading.md](demos/walkthroughs/99-no-top-heading.md) — 99-no-top-heading",
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe("sliceSpecSchemaExcerpt", () => {
|
|
194
|
+
test("centers the excerpt on the discriminatedUnion marker", () => {
|
|
195
|
+
const before = "A".repeat(2000);
|
|
196
|
+
const after = "B".repeat(6000);
|
|
197
|
+
const source = `${before}discriminatedUnion${after}`;
|
|
198
|
+
|
|
199
|
+
const excerpt = sliceSpecSchemaExcerpt(source);
|
|
200
|
+
|
|
201
|
+
// Starts 800 chars before the marker and runs up to 5000 chars past it.
|
|
202
|
+
const markerIndex = source.indexOf("discriminatedUnion");
|
|
203
|
+
expect(excerpt).toBe(source.slice(markerIndex - 800, markerIndex + 5000));
|
|
204
|
+
expect(excerpt).toContain("discriminatedUnion");
|
|
205
|
+
expect(excerpt.length).toBe(800 + 5000);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("clamps the window to the source bounds when the marker is near the edges", () => {
|
|
209
|
+
const source = "discriminatedUnion then a short tail";
|
|
210
|
+
|
|
211
|
+
const excerpt = sliceSpecSchemaExcerpt(source);
|
|
212
|
+
|
|
213
|
+
// marker - 800 clamps to 0 and marker + 5000 clamps to source.length.
|
|
214
|
+
expect(excerpt).toBe(source);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("falls back to the first 8000 chars when no marker is present", () => {
|
|
218
|
+
const source = "C".repeat(10000);
|
|
219
|
+
|
|
220
|
+
const excerpt = sliceSpecSchemaExcerpt(source);
|
|
221
|
+
|
|
222
|
+
expect(excerpt.length).toBe(8000);
|
|
223
|
+
expect(excerpt).toBe(source.slice(0, 8000));
|
|
224
|
+
});
|
|
147
225
|
});
|
package/src/index.ts
CHANGED
|
@@ -220,13 +220,13 @@ function walkUpForSibling(
|
|
|
220
220
|
markers: readonly string[],
|
|
221
221
|
): string | undefined {
|
|
222
222
|
let current = resolve(from);
|
|
223
|
-
|
|
223
|
+
let parent = resolve(current, "..");
|
|
224
|
+
for (; ; current = parent, parent = resolve(current, "..")) {
|
|
224
225
|
const candidate = resolve(current, siblingName);
|
|
225
226
|
if (existsSync(candidate) && markers.every((m) => existsSync(resolve(candidate, m)))) {
|
|
226
227
|
return candidate;
|
|
227
228
|
}
|
|
228
|
-
|
|
229
|
-
if (parent === current) return undefined;
|
|
230
|
-
current = parent;
|
|
229
|
+
if (parent === current) break;
|
|
231
230
|
}
|
|
231
|
+
return undefined;
|
|
232
232
|
}
|