Skip to content

Commit 9aab4bd

Browse files
committed
feat: add instruction loader for template loading and change context
Add the instruction-loader module that provides: - loadTemplate: Load templates from schema directories - loadChangeContext: Combine artifact graph with completion state - generateInstructions: Enrich templates with change-specific context - formatChangeStatus: Format change status as readable output This is Slice 3 of the artifact-graph system, building on the graph operations (Slice 1) and change creation utilities (Slice 2).
1 parent ab47cc6 commit 9aab4bd

File tree

5 files changed

+630
-0
lines changed

5 files changed

+630
-0
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Tasks
2+
3+
## Implementation Tasks
4+
5+
- [x] Create `instruction-loader` spec in `openspec/specs/instruction-loader/spec.md`
6+
- [x] Implement `loadTemplate` function to load templates from schema directories
7+
- [x] Implement `loadChangeContext` function to combine graph and completion state
8+
- [x] Implement `generateInstructions` function to enrich templates with change context
9+
- [x] Implement `formatChangeStatus` function for readable status output
10+
- [x] Export new functions from `src/core/artifact-graph/index.ts`
11+
- [x] Add comprehensive tests in `test/core/artifact-graph/instruction-loader.test.ts`
12+
- [x] Verify build passes
13+
- [x] Verify all tests pass
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# instruction-loader Specification
2+
3+
## Purpose
4+
Load templates from schema directories and enrich them with change-specific context for guiding artifact creation.
5+
6+
## Requirements
7+
8+
### Requirement: Template Loading
9+
The system SHALL load templates from schema directories.
10+
11+
#### Scenario: Load template from schema directory
12+
- **WHEN** `loadTemplate(schemaName, templatePath)` is called
13+
- **THEN** the system loads the template from `schemas/<schemaName>/templates/<templatePath>`
14+
15+
#### Scenario: Template file not found
16+
- **WHEN** a template file does not exist in the schema's templates directory
17+
- **THEN** the system throws an error with the template path
18+
19+
### Requirement: Change Context Loading
20+
The system SHALL load change context combining graph and completion state.
21+
22+
#### Scenario: Load context for existing change
23+
- **WHEN** `loadChangeContext(projectRoot, changeName)` is called for an existing change
24+
- **THEN** the system returns a context with graph, completed set, schema name, and change info
25+
26+
#### Scenario: Load context with custom schema
27+
- **WHEN** `loadChangeContext(projectRoot, changeName, schemaName)` is called
28+
- **THEN** the system uses the specified schema instead of default
29+
30+
#### Scenario: Load context for non-existent change directory
31+
- **WHEN** `loadChangeContext` is called for a non-existent change directory
32+
- **THEN** the system returns context with empty completed set
33+
34+
### Requirement: Template Enrichment
35+
The system SHALL enrich templates with change-specific context.
36+
37+
#### Scenario: Include artifact metadata
38+
- **WHEN** instructions are generated for an artifact
39+
- **THEN** the output includes change name, artifact ID, schema name, and output path
40+
41+
#### Scenario: Include dependency status
42+
- **WHEN** an artifact has dependencies
43+
- **THEN** the output shows each dependency with completion status (done/missing)
44+
45+
#### Scenario: Include unlocked artifacts
46+
- **WHEN** instructions are generated
47+
- **THEN** the output includes which artifacts become available after this one
48+
49+
#### Scenario: Root artifact indicator
50+
- **WHEN** an artifact has no dependencies
51+
- **THEN** the dependency section indicates this is a root artifact
52+
53+
### Requirement: Status Formatting
54+
The system SHALL format change status as readable output.
55+
56+
#### Scenario: All artifacts completed
57+
- **WHEN** all artifacts are completed
58+
- **THEN** status shows all artifacts as "done"
59+
60+
#### Scenario: Mixed completion status
61+
- **WHEN** some artifacts are completed
62+
- **THEN** status shows completed as "done", ready as "ready", blocked as "blocked"
63+
64+
#### Scenario: Blocked artifact details
65+
- **WHEN** an artifact is blocked
66+
- **THEN** status shows which dependencies are missing
67+
68+
#### Scenario: Include output paths
69+
- **WHEN** status is formatted
70+
- **THEN** each artifact shows its output path pattern

src/core/artifact-graph/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,17 @@ export {
2626
getUserSchemasDir,
2727
SchemaLoadError,
2828
} from './resolver.js';
29+
30+
// Instruction loading
31+
export {
32+
loadTemplate,
33+
loadChangeContext,
34+
generateInstructions,
35+
formatChangeStatus,
36+
TemplateLoadError,
37+
type ChangeContext,
38+
type ArtifactInstructions,
39+
type DependencyStatus,
40+
type ArtifactStatus,
41+
type ChangeStatus,
42+
} from './instruction-loader.js';
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
import * as fs from 'node:fs';
2+
import * as path from 'node:path';
3+
import { getSchemaDir, resolveSchema } from './resolver.js';
4+
import { ArtifactGraph } from './graph.js';
5+
import { detectCompleted } from './state.js';
6+
import type { Artifact, CompletedSet } from './types.js';
7+
8+
/**
9+
* Error thrown when loading a template fails.
10+
*/
11+
export class TemplateLoadError extends Error {
12+
constructor(
13+
message: string,
14+
public readonly templatePath: string
15+
) {
16+
super(message);
17+
this.name = 'TemplateLoadError';
18+
}
19+
}
20+
21+
/**
22+
* Change context containing graph, completion state, and metadata.
23+
*/
24+
export interface ChangeContext {
25+
/** The artifact dependency graph */
26+
graph: ArtifactGraph;
27+
/** Set of completed artifact IDs */
28+
completed: CompletedSet;
29+
/** Schema name being used */
30+
schemaName: string;
31+
/** Change name */
32+
changeName: string;
33+
/** Path to the change directory */
34+
changeDir: string;
35+
}
36+
37+
/**
38+
* Enriched instructions for creating an artifact.
39+
*/
40+
export interface ArtifactInstructions {
41+
/** Change name */
42+
changeName: string;
43+
/** Artifact ID */
44+
artifactId: string;
45+
/** Schema name */
46+
schemaName: string;
47+
/** Output path pattern (e.g., "proposal.md") */
48+
outputPath: string;
49+
/** Artifact description */
50+
description: string;
51+
/** Template content */
52+
template: string;
53+
/** Dependencies with completion status */
54+
dependencies: DependencyStatus[];
55+
/** Artifacts that become available after completing this one */
56+
unlocks: string[];
57+
}
58+
59+
/**
60+
* Dependency status information.
61+
*/
62+
export interface DependencyStatus {
63+
/** Artifact ID */
64+
id: string;
65+
/** Whether the dependency is completed */
66+
done: boolean;
67+
}
68+
69+
/**
70+
* Status of a single artifact in the workflow.
71+
*/
72+
export interface ArtifactStatus {
73+
/** Artifact ID */
74+
id: string;
75+
/** Output path pattern */
76+
outputPath: string;
77+
/** Status: done, ready, or blocked */
78+
status: 'done' | 'ready' | 'blocked';
79+
/** Missing dependencies (only for blocked) */
80+
missingDeps?: string[];
81+
}
82+
83+
/**
84+
* Formatted change status.
85+
*/
86+
export interface ChangeStatus {
87+
/** Change name */
88+
changeName: string;
89+
/** Schema name */
90+
schemaName: string;
91+
/** Whether all artifacts are complete */
92+
isComplete: boolean;
93+
/** Status of each artifact */
94+
artifacts: ArtifactStatus[];
95+
}
96+
97+
/**
98+
* Loads a template from a schema's templates directory.
99+
*
100+
* @param schemaName - Schema name (e.g., "spec-driven")
101+
* @param templatePath - Relative path within the templates directory (e.g., "proposal.md")
102+
* @returns The template content
103+
* @throws TemplateLoadError if the template cannot be loaded
104+
*/
105+
export function loadTemplate(schemaName: string, templatePath: string): string {
106+
const schemaDir = getSchemaDir(schemaName);
107+
if (!schemaDir) {
108+
throw new TemplateLoadError(
109+
`Schema '${schemaName}' not found`,
110+
templatePath
111+
);
112+
}
113+
114+
const fullPath = path.join(schemaDir, 'templates', templatePath);
115+
116+
if (!fs.existsSync(fullPath)) {
117+
throw new TemplateLoadError(
118+
`Template not found: ${fullPath}`,
119+
fullPath
120+
);
121+
}
122+
123+
try {
124+
return fs.readFileSync(fullPath, 'utf-8');
125+
} catch (err) {
126+
const ioError = err instanceof Error ? err : new Error(String(err));
127+
throw new TemplateLoadError(
128+
`Failed to read template: ${ioError.message}`,
129+
fullPath
130+
);
131+
}
132+
}
133+
134+
/**
135+
* Loads change context combining graph and completion state.
136+
*
137+
* @param projectRoot - Project root directory
138+
* @param changeName - Change name
139+
* @param schemaName - Optional schema name (defaults to "spec-driven")
140+
* @returns Change context with graph, completed set, and metadata
141+
*/
142+
export function loadChangeContext(
143+
projectRoot: string,
144+
changeName: string,
145+
schemaName: string = 'spec-driven'
146+
): ChangeContext {
147+
const schema = resolveSchema(schemaName);
148+
const graph = ArtifactGraph.fromSchema(schema);
149+
const changeDir = path.join(projectRoot, 'openspec', 'changes', changeName);
150+
const completed = detectCompleted(graph, changeDir);
151+
152+
return {
153+
graph,
154+
completed,
155+
schemaName,
156+
changeName,
157+
changeDir,
158+
};
159+
}
160+
161+
/**
162+
* Generates enriched instructions for creating an artifact.
163+
*
164+
* @param context - Change context
165+
* @param artifactId - Artifact ID to generate instructions for
166+
* @returns Enriched artifact instructions
167+
* @throws Error if artifact not found
168+
*/
169+
export function generateInstructions(
170+
context: ChangeContext,
171+
artifactId: string
172+
): ArtifactInstructions {
173+
const artifact = context.graph.getArtifact(artifactId);
174+
if (!artifact) {
175+
throw new Error(`Artifact '${artifactId}' not found in schema '${context.schemaName}'`);
176+
}
177+
178+
const template = loadTemplate(context.schemaName, artifact.template);
179+
const dependencies = getDependencyStatus(artifact, context.completed);
180+
const unlocks = getUnlockedArtifacts(context.graph, artifactId);
181+
182+
return {
183+
changeName: context.changeName,
184+
artifactId: artifact.id,
185+
schemaName: context.schemaName,
186+
outputPath: artifact.generates,
187+
description: artifact.description,
188+
template,
189+
dependencies,
190+
unlocks,
191+
};
192+
}
193+
194+
/**
195+
* Gets dependency status for an artifact.
196+
*/
197+
function getDependencyStatus(
198+
artifact: Artifact,
199+
completed: CompletedSet
200+
): DependencyStatus[] {
201+
return artifact.requires.map(id => ({
202+
id,
203+
done: completed.has(id),
204+
}));
205+
}
206+
207+
/**
208+
* Gets artifacts that become available after completing the given artifact.
209+
*/
210+
function getUnlockedArtifacts(graph: ArtifactGraph, artifactId: string): string[] {
211+
const unlocks: string[] = [];
212+
213+
for (const artifact of graph.getAllArtifacts()) {
214+
if (artifact.requires.includes(artifactId)) {
215+
unlocks.push(artifact.id);
216+
}
217+
}
218+
219+
return unlocks.sort();
220+
}
221+
222+
/**
223+
* Formats the status of all artifacts in a change.
224+
*
225+
* @param context - Change context
226+
* @returns Formatted change status
227+
*/
228+
export function formatChangeStatus(context: ChangeContext): ChangeStatus {
229+
const artifacts = context.graph.getAllArtifacts();
230+
const ready = new Set(context.graph.getNextArtifacts(context.completed));
231+
const blocked = context.graph.getBlocked(context.completed);
232+
233+
const artifactStatuses: ArtifactStatus[] = artifacts.map(artifact => {
234+
if (context.completed.has(artifact.id)) {
235+
return {
236+
id: artifact.id,
237+
outputPath: artifact.generates,
238+
status: 'done' as const,
239+
};
240+
}
241+
242+
if (ready.has(artifact.id)) {
243+
return {
244+
id: artifact.id,
245+
outputPath: artifact.generates,
246+
status: 'ready' as const,
247+
};
248+
}
249+
250+
return {
251+
id: artifact.id,
252+
outputPath: artifact.generates,
253+
status: 'blocked' as const,
254+
missingDeps: blocked[artifact.id] ?? [],
255+
};
256+
});
257+
258+
// Sort by build order for consistent output
259+
const buildOrder = context.graph.getBuildOrder();
260+
const orderMap = new Map(buildOrder.map((id, idx) => [id, idx]));
261+
artifactStatuses.sort((a, b) => (orderMap.get(a.id) ?? 0) - (orderMap.get(b.id) ?? 0));
262+
263+
return {
264+
changeName: context.changeName,
265+
schemaName: context.schemaName,
266+
isComplete: context.graph.isComplete(context.completed),
267+
artifacts: artifactStatuses,
268+
};
269+
}

0 commit comments

Comments
 (0)