@crewhaus/context-bundle 0.1.0 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crewhaus/context-bundle",
3
- "version": "0.1.0",
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@studiomax.io",
19
- "url": "https://studiomax.io"
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": "restricted"
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
- while (true) {
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
- const parent = resolve(current, "..");
229
- if (parent === current) return undefined;
230
- current = parent;
229
+ if (parent === current) break;
231
230
  }
231
+ return undefined;
232
232
  }