@sprig-and-prose/sprig 0.9.1 → 0.10.0

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 (48) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +1 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +22 -129
  5. package/dist/cli.js.map +1 -1
  6. package/dist/compiler.d.ts +1 -3
  7. package/dist/compiler.d.ts.map +1 -1
  8. package/dist/compiler.js +30 -63
  9. package/dist/compiler.js.map +1 -1
  10. package/dist/diagnostics.d.ts +7 -0
  11. package/dist/diagnostics.d.ts.map +1 -0
  12. package/dist/diagnostics.js +69 -0
  13. package/dist/diagnostics.js.map +1 -0
  14. package/dist/prose.d.ts +1 -1
  15. package/dist/prose.d.ts.map +1 -1
  16. package/dist/prose.js +2 -5
  17. package/dist/prose.js.map +1 -1
  18. package/dist/ui.d.ts +1 -1
  19. package/dist/ui.d.ts.map +1 -1
  20. package/dist/ui.js +11 -12
  21. package/dist/ui.js.map +1 -1
  22. package/package.json +4 -7
  23. package/src/cli.ts +21 -168
  24. package/src/compiler.ts +40 -76
  25. package/src/diagnostics.ts +100 -0
  26. package/src/prose.ts +2 -5
  27. package/src/ui.ts +9 -12
  28. package/tests/compile.test.js +44 -17
  29. package/tests/compile.test.ts +65 -14
  30. package/tests/init.test.ts +8 -0
  31. package/tests/root.test.js +1 -1
  32. package/scripts/switch-deps.js +0 -44
  33. package/src/scene-compiler.ts +0 -192
  34. package/src/scene-discovery.ts +0 -51
  35. package/tests/compile-scene-only.test.js +0 -109
  36. package/tests/fixtures/scene-only-invalid/.sprig-out/bad.error.txt +0 -2
  37. package/tests/fixtures/scene-only-invalid/bad.scene.prose +0 -10
  38. package/tests/fixtures/scene-only-valid/.sprig-emit/AnotherScene.scene.json +0 -15
  39. package/tests/fixtures/scene-only-valid/.sprig-emit/SimpleScene.scene.json +0 -15
  40. package/tests/fixtures/scene-only-valid/.sprig-out/AnotherScene.scene.json +0 -15
  41. package/tests/fixtures/scene-only-valid/.sprig-out/SimpleScene.scene.json +0 -15
  42. package/tests/fixtures/scene-only-valid/another.scene.prose +0 -3
  43. package/tests/fixtures/scene-only-valid/simple.scene.prose +0 -3
  44. package/tests/fixtures/universe-unchanged/.sprig/TestScene.scene.json +0 -15
  45. package/tests/fixtures/universe-unchanged/custom-out/TestScene.scene.json +0 -15
  46. package/tests/fixtures/universe-unchanged/scene.scene.prose +0 -3
  47. package/tests/fixtures/universe-upstream/subdir/.sprig/OneScene.scene.json +0 -15
  48. package/tests/fixtures/universe-upstream/subdir/one.scene.prose +0 -3
package/src/compiler.ts CHANGED
@@ -1,12 +1,15 @@
1
1
  import { readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
2
2
  import { join, dirname } from "node:path";
3
3
  import { execSync } from "node:child_process";
4
- // @ts-expect-error - sprig-universe doesn't have TypeScript types
5
- import { parseFiles, parseFilesLegacy } from "@sprig-and-prose/sprig-universe";
4
+ // @ts-expect-error - prose-parser package does not ship TS types
5
+ import { compileFiles as compileProseFiles } from "@sprig-and-prose/prose-parser";
6
6
 
7
7
  export interface CompileResult {
8
8
  success: boolean;
9
- manifest?: Record<string, unknown> & { repositories?: Record<string, unknown>; generatedAt: string; meta?: { generatedAt: string; manifestId: string } };
9
+ manifest?: Record<string, unknown> & {
10
+ generatedAt: string;
11
+ meta?: { generatedAt: string; manifestId: string };
12
+ };
10
13
  diagnostics: Array<{ severity: string; message: string; source?: unknown }>;
11
14
  }
12
15
 
@@ -25,64 +28,22 @@ function generateManifestId(): string {
25
28
  return `time:${new Date().toISOString()}`;
26
29
  }
27
30
 
28
- /**
29
- * Validates that exactly one universe declaration exists in the parsed graph
30
- */
31
- function validateUniverseCount(graph: Record<string, unknown>): {
32
- valid: boolean;
33
- universeName?: string;
34
- error?: string;
35
- } {
36
- const universes = graph.universes as Record<string, unknown> | undefined;
37
- const universeNames = Object.keys(universes || {});
38
-
39
- if (universeNames.length === 0) {
40
- return {
41
- valid: false,
42
- error: "No universe declaration found. At least one universe declaration is required.",
43
- };
44
- }
45
-
46
- if (universeNames.length > 1) {
47
- const nodes = graph.nodes as Record<string, { source?: { file?: string } }> | undefined;
48
- const fileList = Array.from(universeNames)
49
- .map((name) => {
50
- const universe = universes?.[name] as { root?: string } | undefined;
51
- const rootNode = universe?.root && nodes ? nodes[universe.root] : null;
52
- return rootNode?.source?.file || "unknown";
53
- })
54
- .filter((file, index, arr) => arr.indexOf(file) === index) // unique files
55
- .sort()
56
- .join(", ");
57
- return {
58
- valid: false,
59
- error: `Multiple distinct universes found: ${universeNames.join(", ")}. Files: ${fileList}. Exactly one universe declaration is required.`,
60
- };
61
- }
62
-
63
- return {
64
- valid: true,
65
- universeName: universeNames[0],
66
- };
67
- }
68
-
69
31
  /**
70
32
  * Compiles prose files into a manifest
71
33
  * @param universeRoot - Universe root directory
72
34
  * @param files - Array of prose file paths
73
35
  * @param writeManifest - If true, write manifest to disk
74
- * @param options - Compilation options (legacy, outputDir)
36
+ * @param options - Compilation options (outputDir)
75
37
  * @returns Compile result with success status, manifest, and diagnostics
76
38
  */
77
39
  export async function compileUniverse(
78
40
  universeRoot: string,
79
41
  files: string[],
80
42
  writeManifest = true,
81
- options?: { legacy?: boolean; outputDir?: string },
43
+ options?: { outputDir?: string },
82
44
  ): Promise<CompileResult> {
83
45
  const outputDir = options?.outputDir || join(universeRoot, ".sprig");
84
46
  const manifestPath = join(outputDir, "manifest.json");
85
- const useLegacy = options?.legacy ?? false;
86
47
 
87
48
  // Read and parse files
88
49
  const fileContents = files.map((file) => ({
@@ -91,43 +52,46 @@ export async function compileUniverse(
91
52
  }));
92
53
 
93
54
  try {
94
- const graph = useLegacy ? parseFilesLegacy(fileContents) : parseFiles(fileContents);
95
-
96
- // Validate exactly one universe
97
- const validation = validateUniverseCount(graph);
98
- if (!validation.valid) {
99
- return {
100
- success: false,
101
- diagnostics: [
102
- {
103
- severity: "error",
104
- message: validation.error || "Unknown validation error",
105
- },
106
- ],
107
- };
108
- }
55
+ const result = compileProseFiles(fileContents);
56
+ const baseDiagnostics = result.diagnostics || [];
109
57
 
110
- // Add repositories and metadata to manifest
58
+ // Add metadata to manifest
111
59
  const generatedAt = new Date().toISOString();
112
60
  const manifestId = generateManifestId();
113
-
114
- const manifest = {
115
- ...graph,
116
- repositories: graph.repositories || {},
117
- generatedAt,
118
- meta: {
119
- generatedAt,
120
- manifestId,
121
- },
122
- };
61
+
62
+ const manifest = result.manifest
63
+ ? {
64
+ ...result.manifest,
65
+ generatedAt,
66
+ meta: {
67
+ generatedAt,
68
+ manifestId,
69
+ },
70
+ }
71
+ : undefined;
123
72
 
124
73
  // Check for parsing errors
125
- const hasErrors = graph.diagnostics.some((d: { severity: string }) => d.severity === "error");
74
+ const hasErrors = baseDiagnostics.some(
75
+ (d: { severity: string }) => d.severity === "error",
76
+ );
126
77
  if (hasErrors) {
127
78
  return {
128
79
  success: false,
129
80
  manifest,
130
- diagnostics: graph.diagnostics,
81
+ diagnostics: baseDiagnostics,
82
+ };
83
+ }
84
+
85
+ if (!manifest) {
86
+ return {
87
+ success: false,
88
+ diagnostics: [
89
+ ...baseDiagnostics,
90
+ {
91
+ severity: "error",
92
+ message: "Compilation produced no manifest output.",
93
+ },
94
+ ],
131
95
  };
132
96
  }
133
97
 
@@ -151,7 +115,7 @@ export async function compileUniverse(
151
115
  return {
152
116
  success: true,
153
117
  manifest,
154
- diagnostics: graph.diagnostics,
118
+ diagnostics: baseDiagnostics,
155
119
  };
156
120
  } catch (error) {
157
121
  const message = error instanceof Error ? error.message : String(error);
@@ -0,0 +1,100 @@
1
+ import { basename } from "node:path";
2
+
3
+ export type Diagnostic = { severity: string; message: string; source?: unknown };
4
+
5
+ export function printDiagnostics(diagnostics: Diagnostic[], label: "Error" | "Warning"): void {
6
+ const grouped = new Map<
7
+ string,
8
+ { count: number; diagnostic: Diagnostic; diagnosticWithSource: Diagnostic | null }
9
+ >();
10
+
11
+ for (const diag of diagnostics) {
12
+ const key = diag.message;
13
+ const entry = grouped.get(key) || { count: 0, diagnostic: diag, diagnosticWithSource: null };
14
+ entry.count += 1;
15
+ if (hasSource(diag.source) && !entry.diagnosticWithSource) {
16
+ entry.diagnosticWithSource = diag;
17
+ }
18
+ grouped.set(key, entry);
19
+ }
20
+
21
+ const entries = Array.from(grouped.values());
22
+ entries.forEach(({ count, diagnostic, diagnosticWithSource }, index) => {
23
+ const source = formatSource(diagnosticWithSource?.source || diagnostic.source);
24
+ const { text, code } = extractMessageAndCode(diagnostic.message);
25
+ const countText = count > 1 ? ` (and ${count - 1} more)` : "";
26
+ const codeLabel = label === "Warning" ? "Warning Code" : "Error Code";
27
+ console.error(`${text}${countText}`);
28
+ console.error("");
29
+ console.error(source);
30
+ console.error(`${codeLabel}: ${code}`);
31
+ if (index < entries.length - 1) {
32
+ console.error("");
33
+ }
34
+ });
35
+ }
36
+
37
+ function hasSource(source: unknown): boolean {
38
+ if (!source) {
39
+ return false;
40
+ }
41
+
42
+ if (typeof source === "string") {
43
+ return source.trim().length > 0;
44
+ }
45
+
46
+ if (typeof source !== "object") {
47
+ return false;
48
+ }
49
+
50
+ const candidate = source as {
51
+ file?: string;
52
+ source?: { file?: string };
53
+ location?: { file?: string };
54
+ };
55
+
56
+ const span = candidate.source || candidate.location || candidate;
57
+ return typeof span.file === "string" && span.file.length > 0;
58
+ }
59
+
60
+ function extractMessageAndCode(message: string): { text: string; code: string } {
61
+ const match = message.match(/^\[(SP\d{3})\]\s*(.*)$/);
62
+ if (!match) {
63
+ return { text: message, code: "UNKNOWN" };
64
+ }
65
+
66
+ return {
67
+ code: match[1],
68
+ text: match[2] || "Unknown error.",
69
+ };
70
+ }
71
+
72
+ function formatSource(source: unknown): string {
73
+ if (!source) {
74
+ return "<unknown>:0:0";
75
+ }
76
+
77
+ if (typeof source === "string") {
78
+ return `${basename(source)}:0:0`;
79
+ }
80
+
81
+ if (typeof source !== "object") {
82
+ return "<unknown>:0:0";
83
+ }
84
+
85
+ const candidate = source as {
86
+ file?: string;
87
+ line?: number;
88
+ col?: number;
89
+ start?: { line?: number; col?: number };
90
+ source?: { file?: string; start?: { line?: number; col?: number } };
91
+ location?: { file?: string; start?: { line?: number; col?: number } };
92
+ };
93
+
94
+ const span = candidate.source || candidate.location || candidate;
95
+ const file = span.file ? basename(span.file) : "<unknown>";
96
+ const line = span.start?.line ?? candidate.line ?? 0;
97
+ const col = span.start?.col ?? candidate.col ?? 0;
98
+
99
+ return `${file}:${line}:${col}`;
100
+ }
package/src/prose.ts CHANGED
@@ -4,7 +4,7 @@ import fastGlob from "fast-glob";
4
4
  const DEFAULT_EXCLUDES = [".sprig/**", "dist/**", "node_modules/**", ".git/**"];
5
5
 
6
6
  /**
7
- * Loads all .prose files under root (excluding .scene.prose files) with default excludes
7
+ * Loads all .prose files under root with default excludes
8
8
  * @param root - Universe root directory
9
9
  * @returns Array of prose file paths, sorted deterministically
10
10
  */
@@ -12,10 +12,7 @@ export async function loadProseFiles(root: string): Promise<string[]> {
12
12
  const pattern = join(root, "**/*.prose");
13
13
  const allFiles = await fastGlob(pattern, {
14
14
  absolute: true,
15
- ignore: [
16
- ...DEFAULT_EXCLUDES.map((exclude) => join(root, exclude)),
17
- join(root, "**/*.scene.prose"), // Exclude scene files
18
- ],
15
+ ignore: [...DEFAULT_EXCLUDES.map((exclude) => join(root, exclude))],
19
16
  });
20
17
 
21
18
  // Sort for deterministic ordering
package/src/ui.ts CHANGED
@@ -7,6 +7,7 @@ import chokidar from "chokidar";
7
7
  import { discoverUniverseRoot } from "./root.js";
8
8
  import { loadProseFiles } from "./prose.js";
9
9
  import { compileUniverse } from "./compiler.js";
10
+ import { printDiagnostics } from "./diagnostics.js";
10
11
 
11
12
  const __filename = fileURLToPath(import.meta.url);
12
13
  const __dirname = dirname(__filename);
@@ -29,14 +30,14 @@ const mimeTypes: Record<string, string> = {
29
30
  };
30
31
 
31
32
  /**
32
- * Resolves the dist directory path for sprig-ui-csr
33
+ * Resolves the dist directory path for sprig-ui
33
34
  * Works for both local and global installations
34
35
  */
35
36
  function resolveUIDist(): string {
36
37
  try {
37
38
  // Use Node's module resolution to find the package location
38
39
  // This works for both local (node_modules) and global installations
39
- const packageJsonPath = require.resolve("@sprig-and-prose/sprig-ui-csr/package.json");
40
+ const packageJsonPath = require.resolve("@sprig-and-prose/sprig-ui/package.json");
40
41
  const packageDir = dirname(packageJsonPath);
41
42
  const distPath = join(packageDir, "dist");
42
43
 
@@ -47,13 +48,13 @@ function resolveUIDist(): string {
47
48
  throw new Error(`Found package at ${packageDir} but dist directory does not exist`);
48
49
  } catch (error) {
49
50
  // Fallback: try relative to current package (for development)
50
- const fallbackPath = resolve(__dirname, "../../sprig-ui-csr/dist");
51
+ const fallbackPath = resolve(__dirname, "../../sprig-ui/dist");
51
52
  if (existsSync(fallbackPath)) {
52
53
  return fallbackPath;
53
54
  }
54
55
 
55
56
  const errorMessage = error instanceof Error ? error.message : String(error);
56
- throw new Error(`Could not locate sprig-ui-csr dist directory: ${errorMessage}`);
57
+ throw new Error(`Could not locate sprig-ui dist directory: ${errorMessage}`);
57
58
  }
58
59
  }
59
60
 
@@ -196,11 +197,10 @@ function createServerHandler(
196
197
  async function rebuildManifest(
197
198
  universeRoot: string,
198
199
  manifestPath: string,
199
- legacy: boolean,
200
200
  ): Promise<boolean> {
201
201
  try {
202
202
  const files = await loadProseFiles(universeRoot);
203
- const result = await compileUniverse(universeRoot, files, true, { legacy });
203
+ const result = await compileUniverse(universeRoot, files, true);
204
204
 
205
205
  // Only broadcast if compilation succeeded (manifest was atomically written)
206
206
  if (result.success) {
@@ -219,7 +219,6 @@ async function rebuildManifest(
219
219
  export async function startUIServer(
220
220
  universeRoot: string,
221
221
  port: number = 6336,
222
- legacy = false,
223
222
  ): Promise<void> {
224
223
  const uiDistDir = resolveUIDist();
225
224
  const manifestPath = join(universeRoot, ".sprig", "manifest.json");
@@ -227,14 +226,12 @@ export async function startUIServer(
227
226
  // Compile once on startup
228
227
  console.log("Compiling universe...");
229
228
  const files = await loadProseFiles(universeRoot);
230
- const result = await compileUniverse(universeRoot, files, true, { legacy });
229
+ const result = await compileUniverse(universeRoot, files, true);
231
230
 
232
231
  if (!result.success) {
233
232
  const errors = result.diagnostics.filter((d) => d.severity === "error");
234
233
  if (errors.length > 0) {
235
- for (const error of errors) {
236
- console.error(`Error: ${error.message}`);
237
- }
234
+ printDiagnostics(errors, "Error");
238
235
  }
239
236
  process.exit(1);
240
237
  }
@@ -292,7 +289,7 @@ export async function startUIServer(
292
289
  }
293
290
 
294
291
  rebuildTimeout = setTimeout(async () => {
295
- const success = await rebuildManifest(universeRoot, manifestPath, legacy);
292
+ const success = await rebuildManifest(universeRoot, manifestPath);
296
293
  if (success) {
297
294
  // One-line status per rebuild (calm output)
298
295
  process.stdout.write("\r✓ Rebuilt\n");
@@ -1,34 +1,33 @@
1
1
  import { test } from "node:test";
2
2
  import { strict as assert } from "node:assert";
3
3
  import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from "node:fs";
4
- import { join, tmpdir } from "node:path";
5
- import { compileUniverse } from "../src/compiler.js";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { compileUniverse } from "../dist/compiler.js";
6
7
  test("compileUniverse writes .sprig/manifest.json", async () => {
7
8
  const testDir = join(tmpdir(), `sprig-test-${Date.now()}`);
8
9
  mkdirSync(testDir, { recursive: true });
9
- // Create universe.prose marker
10
- writeFileSync(join(testDir, "universe.prose"), `universe TestUniverse {
10
+ const universeFile = join(testDir, "universe.prose");
11
+ const proseFile = join(testDir, "flora.prose");
12
+ writeFileSync(universeFile, `universe TestUniverse {
11
13
  describe {
12
14
  A test universe.
13
15
  }
14
16
  }`);
15
- // Create a simple prose file
16
- const proseFile = join(testDir, "test.prose");
17
- writeFileSync(proseFile, `series TestSeries {
17
+ writeFileSync(proseFile, `concept Rose in TestUniverse {
18
18
  describe {
19
- A test series.
19
+ A flower.
20
20
  }
21
21
  }`);
22
22
  try {
23
- const result = await compileUniverse(testDir, [proseFile], true);
23
+ const result = await compileUniverse(testDir, [universeFile, proseFile], true);
24
24
  assert.strictEqual(result.success, true);
25
25
  assert.ok(result.manifest);
26
- // Check that manifest.json was written
27
26
  const manifestPath = join(testDir, ".sprig", "manifest.json");
28
27
  assert.ok(existsSync(manifestPath), "manifest.json should exist");
29
- // Check manifest content
30
28
  const manifestContent = JSON.parse(readFileSync(manifestPath, "utf-8"));
31
- assert.strictEqual(manifestContent.universes?.TestUniverse?.name, "TestUniverse");
29
+ assert.strictEqual(manifestContent.universe?.name, "TestUniverse");
30
+ assert.ok(manifestContent.concepts?.["TestUniverse.Rose"]);
32
31
  assert.ok(manifestContent.generatedAt);
33
32
  }
34
33
  finally {
@@ -38,22 +37,22 @@ test("compileUniverse writes .sprig/manifest.json", async () => {
38
37
  test("compileUniverse does not write manifest when writeManifest is false", async () => {
39
38
  const testDir = join(tmpdir(), `sprig-test-${Date.now()}`);
40
39
  mkdirSync(testDir, { recursive: true });
41
- writeFileSync(join(testDir, "universe.prose"), `universe TestUniverse {
40
+ const universeFile = join(testDir, "universe.prose");
41
+ writeFileSync(universeFile, `universe TestUniverse {
42
42
  describe {
43
43
  A test universe.
44
44
  }
45
45
  }`);
46
46
  const proseFile = join(testDir, "test.prose");
47
- writeFileSync(proseFile, `series TestSeries {
47
+ writeFileSync(proseFile, `concept Lily in TestUniverse {
48
48
  describe {
49
- A test series.
49
+ A test concept.
50
50
  }
51
51
  }`);
52
52
  try {
53
- const result = await compileUniverse(testDir, [proseFile], false);
53
+ const result = await compileUniverse(testDir, [universeFile, proseFile], false);
54
54
  assert.strictEqual(result.success, true);
55
55
  assert.ok(result.manifest);
56
- // Check that manifest.json was NOT written
57
56
  const manifestPath = join(testDir, ".sprig", "manifest.json");
58
57
  assert.ok(!existsSync(manifestPath), "manifest.json should not exist when writeManifest is false");
59
58
  }
@@ -61,4 +60,32 @@ test("compileUniverse does not write manifest when writeManifest is false", asyn
61
60
  rmSync(testDir, { recursive: true, force: true });
62
61
  }
63
62
  });
63
+ test("compileUniverse supports declarations split across files", async () => {
64
+ const testDir = join(tmpdir(), `sprig-test-${Date.now()}`);
65
+ mkdirSync(testDir, { recursive: true });
66
+ const universeFile = join(testDir, "universe.prose");
67
+ const relationshipsFile = join(testDir, "relationships.prose");
68
+ const conceptsFile = join(testDir, "concepts.prose");
69
+ writeFileSync(universeFile, `universe TestUniverse {
70
+ concept Fox {}
71
+ concept Chicken {}
72
+ }`);
73
+ writeFileSync(relationshipsFile, `relationship hunts and isHuntedBy in TestUniverse {}`);
74
+ writeFileSync(conceptsFile, `concept Raid in TestUniverse {
75
+ relationships {
76
+ hunts {
77
+ TestUniverse.Fox
78
+ TestUniverse.Chicken
79
+ }
80
+ }
81
+ }`);
82
+ try {
83
+ const result = await compileUniverse(testDir, [universeFile, relationshipsFile, conceptsFile], false);
84
+ assert.strictEqual(result.success, true);
85
+ assert.deepEqual(result.manifest?.concepts?.["TestUniverse.Raid"]?.relationships?.map((reference) => reference.to), ["TestUniverse.Fox", "TestUniverse.Chicken"]);
86
+ }
87
+ finally {
88
+ rmSync(testDir, { recursive: true, force: true });
89
+ }
90
+ });
64
91
  //# sourceMappingURL=compile.test.js.map
@@ -1,36 +1,36 @@
1
1
  import { test } from "node:test";
2
2
  import { strict as assert } from "node:assert";
3
3
  import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from "node:fs";
4
- import { join, tmpdir } from "node:path";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
5
6
  import { compileUniverse } from "../src/compiler.js";
6
7
 
7
8
  test("compileUniverse writes .sprig/manifest.json", async () => {
8
9
  const testDir = join(tmpdir(), `sprig-test-${Date.now()}`);
9
10
  mkdirSync(testDir, { recursive: true });
10
11
 
11
- // Create universe.prose marker
12
+ const universeFile = join(testDir, "universe.prose");
13
+ const proseFile = join(testDir, "flora.prose");
14
+
12
15
  writeFileSync(
13
- join(testDir, "universe.prose"),
16
+ universeFile,
14
17
  `universe TestUniverse {
15
18
  describe {
16
19
  A test universe.
17
20
  }
18
21
  }`,
19
22
  );
20
-
21
- // Create a simple prose file
22
- const proseFile = join(testDir, "test.prose");
23
23
  writeFileSync(
24
24
  proseFile,
25
- `series TestSeries {
25
+ `concept Rose in TestUniverse {
26
26
  describe {
27
- A test series.
27
+ A flower.
28
28
  }
29
29
  }`,
30
30
  );
31
31
 
32
32
  try {
33
- const result = await compileUniverse(testDir, [proseFile], true);
33
+ const result = await compileUniverse(testDir, [universeFile, proseFile], true);
34
34
 
35
35
  assert.strictEqual(result.success, true);
36
36
  assert.ok(result.manifest);
@@ -41,7 +41,8 @@ test("compileUniverse writes .sprig/manifest.json", async () => {
41
41
 
42
42
  // Check manifest content
43
43
  const manifestContent = JSON.parse(readFileSync(manifestPath, "utf-8"));
44
- assert.strictEqual(manifestContent.universes?.TestUniverse?.name, "TestUniverse");
44
+ assert.strictEqual(manifestContent.universe?.name, "TestUniverse");
45
+ assert.ok(manifestContent.concepts?.["TestUniverse.Rose"]);
45
46
  assert.ok(manifestContent.generatedAt);
46
47
  } finally {
47
48
  rmSync(testDir, { recursive: true, force: true });
@@ -52,8 +53,9 @@ test("compileUniverse does not write manifest when writeManifest is false", asyn
52
53
  const testDir = join(tmpdir(), `sprig-test-${Date.now()}`);
53
54
  mkdirSync(testDir, { recursive: true });
54
55
 
56
+ const universeFile = join(testDir, "universe.prose");
55
57
  writeFileSync(
56
- join(testDir, "universe.prose"),
58
+ universeFile,
57
59
  `universe TestUniverse {
58
60
  describe {
59
61
  A test universe.
@@ -64,15 +66,15 @@ test("compileUniverse does not write manifest when writeManifest is false", asyn
64
66
  const proseFile = join(testDir, "test.prose");
65
67
  writeFileSync(
66
68
  proseFile,
67
- `series TestSeries {
69
+ `concept Lily in TestUniverse {
68
70
  describe {
69
- A test series.
71
+ A test concept.
70
72
  }
71
73
  }`,
72
74
  );
73
75
 
74
76
  try {
75
- const result = await compileUniverse(testDir, [proseFile], false);
77
+ const result = await compileUniverse(testDir, [universeFile, proseFile], false);
76
78
 
77
79
  assert.strictEqual(result.success, true);
78
80
  assert.ok(result.manifest);
@@ -85,3 +87,52 @@ test("compileUniverse does not write manifest when writeManifest is false", asyn
85
87
  }
86
88
  });
87
89
 
90
+ test("compileUniverse supports declarations split across files", async () => {
91
+ const testDir = join(tmpdir(), `sprig-test-${Date.now()}`);
92
+ mkdirSync(testDir, { recursive: true });
93
+
94
+ const universeFile = join(testDir, "universe.prose");
95
+ const relationshipsFile = join(testDir, "relationships.prose");
96
+ const conceptsFile = join(testDir, "concepts.prose");
97
+
98
+ writeFileSync(
99
+ universeFile,
100
+ `universe TestUniverse {
101
+ concept Fox {}
102
+ concept Chicken {}
103
+ }`,
104
+ );
105
+ writeFileSync(
106
+ relationshipsFile,
107
+ `relationship hunts and isHuntedBy in TestUniverse {}`,
108
+ );
109
+ writeFileSync(
110
+ conceptsFile,
111
+ `concept Raid in TestUniverse {
112
+ relationships {
113
+ hunts {
114
+ TestUniverse.Fox
115
+ TestUniverse.Chicken
116
+ }
117
+ }
118
+ }`,
119
+ );
120
+
121
+ try {
122
+ const result = await compileUniverse(
123
+ testDir,
124
+ [universeFile, relationshipsFile, conceptsFile],
125
+ false,
126
+ );
127
+ assert.strictEqual(result.success, true);
128
+ assert.deepEqual(
129
+ result.manifest?.concepts?.["TestUniverse.Raid"]?.relationships?.map(
130
+ (reference: { to: string }) => reference.to,
131
+ ),
132
+ ["TestUniverse.Fox", "TestUniverse.Chicken"],
133
+ );
134
+ } finally {
135
+ rmSync(testDir, { recursive: true, force: true });
136
+ }
137
+ });
138
+
@@ -20,6 +20,10 @@ test("init creates both files when directory is empty", async () => {
20
20
 
21
21
  const universeProseContent = readFileSync(universeProsePath, "utf-8");
22
22
  assert.ok(universeProseContent.includes("universe Primer"), "universe.prose should contain universe name");
23
+ assert.ok(
24
+ universeProseContent.includes("describe {"),
25
+ "universe.prose should use describe block syntax",
26
+ );
23
27
 
24
28
  const readmeContent = readFileSync(readmePath, "utf-8");
25
29
  assert.ok(readmeContent.includes("# Primer"), "readme.md should contain universe name");
@@ -102,6 +106,10 @@ test("init handles reverse partial file existence correctly", async () => {
102
106
  assert.ok(existsSync(universeProsePath), "universe.prose should be created");
103
107
  const universeProseContent = readFileSync(universeProsePath, "utf-8");
104
108
  assert.ok(universeProseContent.includes("universe ReversePartialTest"), "universe.prose should contain new universe name");
109
+ assert.ok(
110
+ universeProseContent.includes("describe {"),
111
+ "universe.prose should use describe block syntax",
112
+ );
105
113
  } finally {
106
114
  rmSync(testDir, { recursive: true, force: true });
107
115
  }
@@ -3,7 +3,7 @@ import { strict as assert } from "node:assert";
3
3
  import { mkdirSync, writeFileSync, rmSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
- import { discoverUniverseRoot } from "../src/root.js";
6
+ import { discoverUniverseRoot } from "../dist/root.js";
7
7
  test("discoverUniverseRoot finds universe.prose in current directory", () => {
8
8
  const testDir = join(tmpdir(), `sprig-test-${Date.now()}`);
9
9
  mkdirSync(testDir, { recursive: true });