@t3lnet/sceneforge 1.0.34 → 1.0.35

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.
@@ -113,6 +113,12 @@ async function writeSplitPointer(
113
113
  outputDir: string
114
114
  ): Promise<{ filePath: string; merged: boolean }> {
115
115
  const config = getToolConfig(tool);
116
+
117
+ // Base reference file
118
+ const baseFile = `${config.splitFilePrefix}base${config.fileExtension}`;
119
+ const baseRelativePath = path.posix.join(config.splitDir, baseFile);
120
+
121
+ // Stage files
116
122
  const stageLines = stageNames.map((stage) => {
117
123
  const friendly = formatStageName(stage);
118
124
  const stageFile = `${config.splitFilePrefix}${getStageFileName(stage)}${config.fileExtension}`;
@@ -121,11 +127,17 @@ async function writeSplitPointer(
121
127
  });
122
128
 
123
129
  const pointerBody = [
124
- "SceneForge context for this tool is split across the following stage files:",
130
+ "SceneForge context for this tool is split into modular files:",
125
131
  "",
132
+ "**Reference Documentation:**",
133
+ `- Base Reference (YAML schema, CLI, selectors): ${baseRelativePath}`,
134
+ "",
135
+ "**Stage-Specific Instructions:**",
126
136
  ...stageLines,
127
137
  "",
128
- `Open the stage file that matches the work you're doing, or run "npx sceneforge context preview --target ${tool} --stage <stage>" to inspect a specific stage.`,
138
+ `Open the stage file that matches the work you're doing. The base reference contains shared documentation used across all stages.`,
139
+ "",
140
+ `Run "npx sceneforge context preview --target ${tool} --stage <stage>" to inspect a specific stage.`,
129
141
  ].join("\n");
130
142
 
131
143
  const formattedPointer = formatForTool(tool, pointerBody, {
@@ -145,6 +157,8 @@ async function writeSplitPointer(
145
157
 
146
158
  /**
147
159
  * Build context content for a specific tool and stage.
160
+ * For combined format, includes both base and stage templates.
161
+ * For split format, use buildBaseContext and buildStageOnlyContext instead.
148
162
  */
149
163
  export async function buildContext(
150
164
  tool: TargetTool,
@@ -187,6 +201,58 @@ export async function buildContext(
187
201
  return formatForTool(tool, content, { stage: stageName });
188
202
  }
189
203
 
204
+ /**
205
+ * Build base context content only (for split format).
206
+ * Contains reference documentation shared across all stages.
207
+ */
208
+ export async function buildBaseContext(
209
+ tool: TargetTool,
210
+ variables?: TemplateVariables
211
+ ): Promise<string> {
212
+ const baseTemplates = await loadTemplatesByCategory("base");
213
+
214
+ let content = composeTemplates(baseTemplates, {
215
+ separator: "\n\n---\n\n",
216
+ includeHeaders: false,
217
+ });
218
+
219
+ if (variables) {
220
+ content = interpolateVariables(content, variables);
221
+ }
222
+
223
+ return formatForTool(tool, content, { stage: "Base Reference" });
224
+ }
225
+
226
+ /**
227
+ * Build stage-only context content (for split format).
228
+ * Contains only stage-specific instructions without base templates.
229
+ */
230
+ export async function buildStageOnlyContext(
231
+ tool: TargetTool,
232
+ stage: Stage,
233
+ variables?: TemplateVariables
234
+ ): Promise<string> {
235
+ if (stage === "all") {
236
+ throw new Error("buildStageOnlyContext does not support 'all' stage");
237
+ }
238
+
239
+ const stageFileName = getStageFileName(stage);
240
+ const stageTemplate = await loadTemplate("stages", stageFileName);
241
+
242
+ let content = stageTemplate.content;
243
+
244
+ if (variables) {
245
+ content = interpolateVariables(content, variables);
246
+ }
247
+
248
+ // Add a note pointing to base reference
249
+ const baseNote = `> **Note:** This file contains stage-specific instructions. See \`base.md\` in this directory for reference documentation (YAML schema, CLI commands, selectors guide, etc.).\n\n`;
250
+
251
+ return formatForTool(tool, baseNote + content, {
252
+ stage: formatStageName(stage),
253
+ });
254
+ }
255
+
190
256
  /**
191
257
  * Deploy context files to the target directory.
192
258
  */
@@ -245,11 +311,58 @@ export async function deployContext(
245
311
  });
246
312
  }
247
313
  } else {
248
- // Generate split files for each stage
314
+ // Generate split files: one base file + individual stage files
315
+ const config = getToolConfig(tool);
316
+
317
+ // First, deploy the base reference file
318
+ try {
319
+ const baseContent = await buildBaseContext(tool, variables);
320
+ const baseFilePath = path.join(
321
+ outputDir,
322
+ config.splitDir,
323
+ `${config.splitFilePrefix}base${config.fileExtension}`
324
+ );
325
+ const absoluteBasePath = path.resolve(baseFilePath);
326
+
327
+ await fs.mkdir(path.dirname(absoluteBasePath), { recursive: true });
328
+
329
+ const { content: finalBaseContent, merged: baseMerged } =
330
+ await mergeWithExisting(absoluteBasePath, baseContent);
331
+
332
+ await fs.writeFile(absoluteBasePath, finalBaseContent, "utf-8");
333
+
334
+ results.push({
335
+ tool,
336
+ filePath: absoluteBasePath,
337
+ stage: "base",
338
+ created: true,
339
+ skipped: false,
340
+ });
341
+
342
+ if (baseMerged) {
343
+ console.log(
344
+ ` [merged] ${path.relative(outputDir, absoluteBasePath)}`
345
+ );
346
+ }
347
+ } catch (error) {
348
+ results.push({
349
+ tool,
350
+ filePath: path.join(
351
+ outputDir,
352
+ config.splitDir,
353
+ `${config.splitFilePrefix}base${config.fileExtension}`
354
+ ),
355
+ stage: "base",
356
+ created: false,
357
+ error: error instanceof Error ? error.message : String(error),
358
+ });
359
+ }
360
+
361
+ // Then deploy individual stage files (without base content)
249
362
  const deployedStages: Stage[] = [];
250
363
  for (const stg of stages) {
251
364
  try {
252
- const content = await buildContext(tool, stg, variables);
365
+ const content = await buildStageOnlyContext(tool, stg, variables);
253
366
  const stageName = getStageFileName(stg);
254
367
  const filePath = getOutputPath(tool, format, outputDir, stageName);
255
368
  const absolutePath = path.resolve(filePath);
package/context/index.ts CHANGED
@@ -38,6 +38,8 @@ export {
38
38
  // Context builder
39
39
  export {
40
40
  buildContext,
41
+ buildBaseContext,
42
+ buildStageOnlyContext,
41
43
  deployContext,
42
44
  previewContext,
43
45
  listDeployedContext,
@@ -118,12 +118,14 @@ describe("context-builder", () => {
118
118
  outputDir: tempDir,
119
119
  });
120
120
 
121
- expect(results.length).toBe(2);
122
- expect(results[0].created).toBe(true);
121
+ // Split format now creates: base.md + stage file + pointer file
122
+ expect(results.length).toBe(3);
123
+ expect(results.every((r) => r.created)).toBe(true);
123
124
 
124
125
  const splitDir = path.join(tempDir, ".claude/rules");
125
126
  const files = await fs.readdir(splitDir);
126
- expect(files.length).toBeGreaterThan(0);
127
+ expect(files).toContain("base.md");
128
+ expect(files).toContain("stage1-actions.md");
127
129
  });
128
130
 
129
131
  it("deploys multiple stages in split format", async () => {
@@ -134,7 +136,8 @@ describe("context-builder", () => {
134
136
  outputDir: tempDir,
135
137
  });
136
138
 
137
- expect(results.length).toBe(5);
139
+ // Split format now creates: base.md + 4 stage files + pointer file = 6
140
+ expect(results.length).toBe(6);
138
141
  expect(results.every((r) => r.created)).toBe(true);
139
142
  });
140
143
 
@@ -147,7 +150,8 @@ describe("context-builder", () => {
147
150
  });
148
151
 
149
152
  const claudeFile = await fs.readFile(path.join(tempDir, "CLAUDE.md"), "utf-8");
150
- expect(claudeFile).toContain("split across the following stage files");
153
+ expect(claudeFile).toContain("split into modular files");
154
+ expect(claudeFile).toContain("Base Reference");
151
155
  expect(claudeFile).toContain(".claude/rules/stage1-actions.md");
152
156
  });
153
157
  });
package/dist/index.cjs CHANGED
@@ -2728,6 +2728,8 @@ async function mergeWithExisting(filePath, newContent) {
2728
2728
  }
2729
2729
  async function writeSplitPointer(tool, stageNames, outputDir) {
2730
2730
  const config = getToolConfig(tool);
2731
+ const baseFile = `${config.splitFilePrefix}base${config.fileExtension}`;
2732
+ const baseRelativePath = path6.posix.join(config.splitDir, baseFile);
2731
2733
  const stageLines = stageNames.map((stage) => {
2732
2734
  const friendly = formatStageName(stage);
2733
2735
  const stageFile = `${config.splitFilePrefix}${getStageFileName(stage)}${config.fileExtension}`;
@@ -2735,11 +2737,17 @@ async function writeSplitPointer(tool, stageNames, outputDir) {
2735
2737
  return `- ${friendly}: ${relativePath}`;
2736
2738
  });
2737
2739
  const pointerBody = [
2738
- "SceneForge context for this tool is split across the following stage files:",
2740
+ "SceneForge context for this tool is split into modular files:",
2739
2741
  "",
2742
+ "**Reference Documentation:**",
2743
+ `- Base Reference (YAML schema, CLI, selectors): ${baseRelativePath}`,
2744
+ "",
2745
+ "**Stage-Specific Instructions:**",
2740
2746
  ...stageLines,
2741
2747
  "",
2742
- `Open the stage file that matches the work you're doing, or run "npx sceneforge context preview --target ${tool} --stage <stage>" to inspect a specific stage.`
2748
+ `Open the stage file that matches the work you're doing. The base reference contains shared documentation used across all stages.`,
2749
+ "",
2750
+ `Run "npx sceneforge context preview --target ${tool} --stage <stage>" to inspect a specific stage.`
2743
2751
  ].join("\n");
2744
2752
  const formattedPointer = formatForTool(tool, pointerBody, {
2745
2753
  includeToolHeader: true
@@ -2778,6 +2786,34 @@ async function buildContext(tool, stage, variables) {
2778
2786
  const stageName = stage === "all" ? void 0 : formatStageName(stage);
2779
2787
  return formatForTool(tool, content, { stage: stageName });
2780
2788
  }
2789
+ async function buildBaseContext(tool, variables) {
2790
+ const baseTemplates = await loadTemplatesByCategory("base");
2791
+ let content = composeTemplates(baseTemplates, {
2792
+ separator: "\n\n---\n\n",
2793
+ includeHeaders: false
2794
+ });
2795
+ if (variables) {
2796
+ content = interpolateVariables(content, variables);
2797
+ }
2798
+ return formatForTool(tool, content, { stage: "Base Reference" });
2799
+ }
2800
+ async function buildStageOnlyContext(tool, stage, variables) {
2801
+ if (stage === "all") {
2802
+ throw new Error("buildStageOnlyContext does not support 'all' stage");
2803
+ }
2804
+ const stageFileName = getStageFileName(stage);
2805
+ const stageTemplate = await loadTemplate("stages", stageFileName);
2806
+ let content = stageTemplate.content;
2807
+ if (variables) {
2808
+ content = interpolateVariables(content, variables);
2809
+ }
2810
+ const baseNote = `> **Note:** This file contains stage-specific instructions. See \`base.md\` in this directory for reference documentation (YAML schema, CLI commands, selectors guide, etc.).
2811
+
2812
+ `;
2813
+ return formatForTool(tool, baseNote + content, {
2814
+ stage: formatStageName(stage)
2815
+ });
2816
+ }
2781
2817
  async function deployContext(options) {
2782
2818
  const { target, stage, format, outputDir, variables } = options;
2783
2819
  const results = [];
@@ -2813,10 +2849,47 @@ async function deployContext(options) {
2813
2849
  });
2814
2850
  }
2815
2851
  } else {
2852
+ const config = getToolConfig(tool);
2853
+ try {
2854
+ const baseContent = await buildBaseContext(tool, variables);
2855
+ const baseFilePath = path6.join(
2856
+ outputDir,
2857
+ config.splitDir,
2858
+ `${config.splitFilePrefix}base${config.fileExtension}`
2859
+ );
2860
+ const absoluteBasePath = path6.resolve(baseFilePath);
2861
+ await fs6.mkdir(path6.dirname(absoluteBasePath), { recursive: true });
2862
+ const { content: finalBaseContent, merged: baseMerged } = await mergeWithExisting(absoluteBasePath, baseContent);
2863
+ await fs6.writeFile(absoluteBasePath, finalBaseContent, "utf-8");
2864
+ results.push({
2865
+ tool,
2866
+ filePath: absoluteBasePath,
2867
+ stage: "base",
2868
+ created: true,
2869
+ skipped: false
2870
+ });
2871
+ if (baseMerged) {
2872
+ console.log(
2873
+ ` [merged] ${path6.relative(outputDir, absoluteBasePath)}`
2874
+ );
2875
+ }
2876
+ } catch (error) {
2877
+ results.push({
2878
+ tool,
2879
+ filePath: path6.join(
2880
+ outputDir,
2881
+ config.splitDir,
2882
+ `${config.splitFilePrefix}base${config.fileExtension}`
2883
+ ),
2884
+ stage: "base",
2885
+ created: false,
2886
+ error: error instanceof Error ? error.message : String(error)
2887
+ });
2888
+ }
2816
2889
  const deployedStages = [];
2817
2890
  for (const stg of stages) {
2818
2891
  try {
2819
- const content = await buildContext(tool, stg, variables);
2892
+ const content = await buildStageOnlyContext(tool, stg, variables);
2820
2893
  const stageName = getStageFileName(stg);
2821
2894
  const filePath = getOutputPath(tool, format, outputDir, stageName);
2822
2895
  const absolutePath = path6.resolve(filePath);