sf snapshot: pre-dispatch, uncommitted changes after 30m inactivity
This commit is contained in:
parent
e2c3d6542c
commit
6f32f9287a
10 changed files with 652 additions and 30 deletions
|
|
@ -1115,15 +1115,51 @@ export async function buildWorkflowPreferencesPrompt(
|
|||
* Project-level interview: produces .sf/PROJECT.md.
|
||||
* Fires before any milestone-level work when planning_depth === "deep"
|
||||
* and PROJECT.md is missing.
|
||||
*
|
||||
* #4782 phase 3: migrated through composeUnitContext v2. Declared inline
|
||||
* order: project, templates. Knowledge/graph are computed artifacts injected
|
||||
* after templates (prepend-less unit; knowledge/graph are not prepended,
|
||||
* they follow the templates block).
|
||||
*/
|
||||
export async function buildDiscussProjectPrompt(
|
||||
base,
|
||||
structuredQuestionsAvailable = "false",
|
||||
) {
|
||||
const inlinedTemplates = inlineTemplate("project", "Project");
|
||||
const { inline: composed } = await composeUnitContext("discuss-project", {
|
||||
base,
|
||||
resolveArtifact: async (key) => {
|
||||
switch (key) {
|
||||
case "project":
|
||||
return inlineProjectFromDb(base);
|
||||
case "templates":
|
||||
return inlineTemplate("project", "Project");
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
knowledge: {
|
||||
build: async (_, b) => inlineKnowledgeScoped(b, []),
|
||||
inputs: {},
|
||||
},
|
||||
graph: {
|
||||
build: async (_, b) => inlineGraphSubgraph(b, "project setup", { budget: 3000 }),
|
||||
inputs: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
const parts = [];
|
||||
if (composed) parts.push(composed);
|
||||
const knowledgeBlockDP = await inlineKnowledgeScoped(base, []);
|
||||
if (knowledgeBlockDP) parts.push(knowledgeBlockDP);
|
||||
const graphBlockDP = await inlineGraphSubgraph(base, "project setup", { budget: 3000 });
|
||||
if (graphBlockDP) parts.push(graphBlockDP);
|
||||
const inlinedContext = capPreamble(
|
||||
`## Inlined Context (preloaded — do not re-read these files)\n\n${parts.join("\n\n---\n\n")}`,
|
||||
);
|
||||
return loadPrompt("guided-discuss-project", {
|
||||
workingDirectory: base,
|
||||
inlinedTemplates,
|
||||
inlinedContext,
|
||||
structuredQuestionsAvailable,
|
||||
commitInstruction:
|
||||
"Do not commit planning artifacts — .sf/ is managed externally.",
|
||||
|
|
@ -1135,15 +1171,55 @@ export async function buildDiscussProjectPrompt(
|
|||
* structured R### format. Reads PROJECT.md as authoritative context.
|
||||
* Fires when planning_depth === "deep", PROJECT.md exists, and
|
||||
* REQUIREMENTS.md is missing.
|
||||
*
|
||||
* #4782 phase 3: migrated through composeUnitContext v2. Declared inline
|
||||
* order: project, requirements, templates. Knowledge/graph appended after
|
||||
* templates (knowledge is full-scope for requirements gathering).
|
||||
*/
|
||||
export async function buildDiscussRequirementsPrompt(
|
||||
base,
|
||||
structuredQuestionsAvailable = "false",
|
||||
) {
|
||||
const inlinedTemplates = inlineTemplate("requirements", "Requirements");
|
||||
const { inline: composed } = await composeUnitContext("discuss-requirements", {
|
||||
base,
|
||||
resolveArtifact: async (key) => {
|
||||
switch (key) {
|
||||
case "project":
|
||||
return inlineProjectFromDb(base);
|
||||
case "requirements":
|
||||
return inlineRequirementsFromDb(base);
|
||||
case "templates":
|
||||
return inlineTemplate("requirements", "Requirements");
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
knowledge: {
|
||||
build: async (_, b) => inlineKnowledgeScoped(b, []),
|
||||
inputs: {},
|
||||
},
|
||||
graph: {
|
||||
build: async (_, b) =>
|
||||
inlineGraphSubgraph(b, "project requirements", { budget: 3000 }),
|
||||
inputs: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
const parts = [];
|
||||
if (composed) parts.push(composed);
|
||||
const knowledgeBlockDR = await inlineKnowledgeScoped(base, []);
|
||||
if (knowledgeBlockDR) parts.push(knowledgeBlockDR);
|
||||
const graphBlockDR = await inlineGraphSubgraph(base, "project requirements", {
|
||||
budget: 3000,
|
||||
});
|
||||
if (graphBlockDR) parts.push(graphBlockDR);
|
||||
const inlinedContext = capPreamble(
|
||||
`## Inlined Context (preloaded — do not re-read these files)\n\n${parts.join("\n\n---\n\n")}`,
|
||||
);
|
||||
return loadPrompt("guided-discuss-requirements", {
|
||||
workingDirectory: base,
|
||||
inlinedTemplates,
|
||||
inlinedContext,
|
||||
structuredQuestionsAvailable,
|
||||
commitInstruction:
|
||||
"Do not commit planning artifacts — .sf/ is managed externally.",
|
||||
|
|
@ -1161,13 +1237,57 @@ export async function buildDiscussRequirementsPrompt(
|
|||
* architecture, and pitfalls. Each subagent writes its findings to
|
||||
* .sf/research/. Fires after research-decision marker says "research" and
|
||||
* project research files are missing. Skipped entirely if user picked "skip".
|
||||
*
|
||||
* #4782 phase 3: migrated through composeUnitContext v2. Declared inline
|
||||
* order: project, requirements, decisions, templates. Knowledge/graph
|
||||
* appended after templates.
|
||||
*/
|
||||
export async function buildResearchProjectPrompt(
|
||||
base,
|
||||
structuredQuestionsAvailable = "false",
|
||||
) {
|
||||
const { inline: composed } = await composeUnitContext("research-project", {
|
||||
base,
|
||||
resolveArtifact: async (key) => {
|
||||
switch (key) {
|
||||
case "project":
|
||||
return inlineProjectFromDb(base);
|
||||
case "requirements":
|
||||
return inlineRequirementsFromDb(base);
|
||||
case "decisions":
|
||||
return inlineDecisionsFromDb(base);
|
||||
case "templates":
|
||||
return inlineTemplate("research", "Research");
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
knowledge: {
|
||||
build: async (_, b) => inlineKnowledgeScoped(b, []),
|
||||
inputs: {},
|
||||
},
|
||||
graph: {
|
||||
build: async (_, b) =>
|
||||
inlineGraphSubgraph(b, "project research", { budget: 3000 }),
|
||||
inputs: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
const parts = [];
|
||||
if (composed) parts.push(composed);
|
||||
const knowledgeBlockRP = await inlineKnowledgeScoped(base, []);
|
||||
if (knowledgeBlockRP) parts.push(knowledgeBlockRP);
|
||||
const graphBlockRP = await inlineGraphSubgraph(base, "project research", {
|
||||
budget: 3000,
|
||||
});
|
||||
if (graphBlockRP) parts.push(graphBlockRP);
|
||||
const inlinedContext = capPreamble(
|
||||
`## Inlined Context (preloaded — do not re-read these files)\n\n${parts.join("\n\n---\n\n")}`,
|
||||
);
|
||||
return loadPrompt("guided-research-project", {
|
||||
workingDirectory: base,
|
||||
inlinedContext,
|
||||
structuredQuestionsAvailable,
|
||||
});
|
||||
}
|
||||
|
|
@ -1176,6 +1296,11 @@ export async function buildResearchProjectPrompt(
|
|||
* Loads the guided-discuss-milestone template and inlines the CONTEXT-DRAFT
|
||||
* as a seed when present. The discussion agent interviews the user, writes
|
||||
* a full CONTEXT.md, and the phase transitions to pre-planning automatically.
|
||||
*
|
||||
* #4782 phase 3: migrated through composeUnitContext v2. Declared inline
|
||||
* order: project, requirements, decisions, milestone-context, templates.
|
||||
* Knowledge/graph are computed artifacts appended after templates.
|
||||
* The CONTEXT-DRAFT seed is preserved as a post-composer imperative append.
|
||||
*/
|
||||
export async function buildDiscussMilestonePrompt(
|
||||
mid,
|
||||
|
|
@ -1183,11 +1308,57 @@ export async function buildDiscussMilestonePrompt(
|
|||
base,
|
||||
structuredQuestionsAvailable = "false",
|
||||
) {
|
||||
const discussTemplates = inlineTemplate("context", "Context");
|
||||
const contextPath = resolveMilestoneFile(base, mid, "CONTEXT");
|
||||
const contextRel = relMilestoneFile(base, mid, "CONTEXT");
|
||||
const { inline: composed } = await composeUnitContext("discuss-milestone", {
|
||||
base,
|
||||
resolveArtifact: async (key) => {
|
||||
switch (key) {
|
||||
case "project":
|
||||
return inlineProjectFromDb(base);
|
||||
case "requirements":
|
||||
return inlineRequirementsFromDb(base, mid);
|
||||
case "decisions":
|
||||
return inlineDecisionsFromDb(base, mid);
|
||||
case "milestone-context":
|
||||
return inlineFileOptional(
|
||||
contextPath,
|
||||
contextRel,
|
||||
"Milestone Context",
|
||||
);
|
||||
case "templates":
|
||||
return inlineTemplate("context", "Context");
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
knowledge: {
|
||||
build: async (_, b) => inlineKnowledgeScoped(b, []),
|
||||
inputs: {},
|
||||
},
|
||||
graph: {
|
||||
build: async (_, b) =>
|
||||
inlineGraphSubgraph(b, `${mid} ${midTitle}`, { budget: 3000 }),
|
||||
inputs: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
const parts = [];
|
||||
if (composed) parts.push(composed);
|
||||
const knowledgeBlockDM = await inlineKnowledgeScoped(base, []);
|
||||
if (knowledgeBlockDM) parts.push(knowledgeBlockDM);
|
||||
const graphBlockDM = await inlineGraphSubgraph(base, `${mid} ${midTitle}`, {
|
||||
budget: 3000,
|
||||
});
|
||||
if (graphBlockDM) parts.push(graphBlockDM);
|
||||
const inlinedContext = capPreamble(
|
||||
`## Inlined Context (preloaded — do not re-read these files)\n\n${parts.join("\n\n---\n\n")}`,
|
||||
);
|
||||
const basePrompt = loadPrompt("guided-discuss-milestone", {
|
||||
milestoneId: mid,
|
||||
milestoneTitle: midTitle,
|
||||
inlinedTemplates: discussTemplates,
|
||||
inlinedContext,
|
||||
structuredQuestionsAvailable,
|
||||
commitInstruction:
|
||||
"Do not commit planning artifacts — .sf/ is managed externally.",
|
||||
|
|
@ -1856,14 +2027,16 @@ export async function buildExecuteTaskPrompt(
|
|||
const knowledgeAbsPath = resolveSfRootFile(base, "KNOWLEDGE");
|
||||
const runtimePath = resolveRuntimeFile(base);
|
||||
// Fan out all independent I/O in parallel: task plan, slice plan, continue
|
||||
// file, runtime, knowledge, graph subgraph, overrides, prior summary paths.
|
||||
// file, runtime, overrides, prior summary paths.
|
||||
// #4782 phase 3: knowledge and graph moved to composeUnitContext computed registry
|
||||
// (manifest declares computed: ["knowledge", "graph"]). The template now includes
|
||||
// {{inlinedTemplates}} so composed output is visible. Knowledge/graph are no longer
|
||||
// fetched here — the composer handles them.
|
||||
const [
|
||||
taskPlanContent,
|
||||
slicePlanContent,
|
||||
continueContent,
|
||||
runtimeContent,
|
||||
knowledgeInlineET,
|
||||
graphBlockET,
|
||||
activeOverrides,
|
||||
priorSummaries,
|
||||
] = await Promise.all([
|
||||
|
|
@ -1871,15 +2044,6 @@ export async function buildExecuteTaskPrompt(
|
|||
slicePlanPath ? loadFile(slicePlanPath) : Promise.resolve(null),
|
||||
continueFile ? loadFile(continueFile) : Promise.resolve(null),
|
||||
existsSync(runtimePath) ? loadFile(runtimePath) : Promise.resolve(null),
|
||||
existsSync(knowledgeAbsPath)
|
||||
? inlineFileSmart(
|
||||
knowledgeAbsPath,
|
||||
relSfRootFile("KNOWLEDGE"),
|
||||
"Project Knowledge",
|
||||
`${tTitle} ${sTitle}`,
|
||||
)
|
||||
: Promise.resolve(null),
|
||||
inlineGraphSubgraph(base, `${tid} ${tTitle}`, { budget: 2000 }),
|
||||
loadActiveOverrides(base),
|
||||
opts.carryForwardPaths
|
||||
? Promise.resolve(opts.carryForwardPaths)
|
||||
|
|
@ -1943,18 +2107,61 @@ export async function buildExecuteTaskPrompt(
|
|||
base,
|
||||
);
|
||||
// Only include knowledge if it has content (not a "not found" result)
|
||||
const knowledgeContent =
|
||||
knowledgeInlineET && !knowledgeInlineET.includes("not found")
|
||||
? knowledgeInlineET
|
||||
: null;
|
||||
// #4782 phase 3: knowledge and graph moved to composer computed registry.
|
||||
// The composer produces inline blocks including the computed knowledge/graph entries.
|
||||
const { inline: composedInlines } = await composeUnitContext("execute-task", {
|
||||
base,
|
||||
resolveArtifact: async (key) => {
|
||||
switch (key) {
|
||||
case "task-plan":
|
||||
return taskPlanContent
|
||||
? [
|
||||
"## Inlined Task Plan (authoritative local execution contract)",
|
||||
`Source: \`${taskPlanRelPath}\``,
|
||||
"",
|
||||
taskPlanContent.trim(),
|
||||
].join("\n")
|
||||
: [
|
||||
"## Inlined Task Plan (authoritative local execution contract)",
|
||||
`Task plan not found at dispatch time. Read \`${taskPlanRelPath}\` before executing.`,
|
||||
].join("\n");
|
||||
case "slice-plan": {
|
||||
return slicePlanContent
|
||||
? extractSliceExecutionExcerpt(
|
||||
slicePlanContent,
|
||||
relSliceFile(base, mid, sid, "PLAN"),
|
||||
)
|
||||
: null;
|
||||
}
|
||||
case "prior-task-summaries": {
|
||||
if (priorSummaries.length === 0) return null;
|
||||
const lines = priorSummaries.map((p) => `- \`${p}\``).join("\n");
|
||||
return `### Prior Task Summaries\n\n${lines}`;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
knowledge: {
|
||||
build: async ({ kw }, b) => inlineKnowledgeScoped(b, kw),
|
||||
inputs: { kw: [tTitle, sTitle] },
|
||||
},
|
||||
graph: {
|
||||
build: async ({ query }, b) =>
|
||||
inlineGraphSubgraph(b, query, { budget: 2000 }),
|
||||
inputs: { query: `${tid} ${tTitle}` },
|
||||
},
|
||||
},
|
||||
});
|
||||
// Build inlinedTemplates from composer output + task-summary/decisions templates
|
||||
const inlinedTemplates =
|
||||
inlineLevel === "minimal"
|
||||
? inlineTemplate("task-summary", "Task Summary")
|
||||
: [
|
||||
inlineTemplate("task-summary", "Task Summary"),
|
||||
inlineTemplate("decisions", "Decisions"),
|
||||
...(knowledgeContent ? [knowledgeContent] : []),
|
||||
...(graphBlockET ? [graphBlockET] : []),
|
||||
...(composedInlines ? [composedInlines] : []),
|
||||
].join("\n\n---\n\n");
|
||||
const taskSummaryPath = join(
|
||||
base,
|
||||
|
|
@ -3216,6 +3423,9 @@ export async function buildRewriteDocsPrompt(
|
|||
) {
|
||||
const sid = activeSlice?.id;
|
||||
const sTitle = activeSlice?.title ?? "";
|
||||
// #4782 phase 3: migrated through composeUnitContext v2.
|
||||
// The docList accumulation stays imperative — it is a dynamic path enumeration,
|
||||
// not a static file inline. The composer handles the declared static inlines.
|
||||
const docList = [];
|
||||
if (sid) {
|
||||
const slicePlanPath = resolveSliceFile(base, mid, sid, "PLAN");
|
||||
|
|
@ -3291,6 +3501,52 @@ export async function buildRewriteDocsPrompt(
|
|||
docList.length > 0
|
||||
? docList.join("\n")
|
||||
: "- No active plan documents found.";
|
||||
|
||||
const keywords = extractKeywords(sTitle || midTitle);
|
||||
const { inline: composed } = await composeUnitContext("rewrite-docs", {
|
||||
base,
|
||||
resolveArtifact: async (key) => {
|
||||
switch (key) {
|
||||
case "project":
|
||||
return inlineProjectFromDb(base);
|
||||
case "requirements":
|
||||
return inlineRequirementsFromDb(base, mid);
|
||||
case "decisions":
|
||||
return inlineDecisionsFromDb(base, mid);
|
||||
case "templates":
|
||||
// No rewrite-docs.md in templates/ — the headless rewrite-docs prompt
|
||||
// template lives in prompts/. Templates are not inlined here.
|
||||
return null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
knowledge: {
|
||||
build: async ({ kw }, b) => inlineKnowledgeScoped(b, kw),
|
||||
inputs: { kw: keywords },
|
||||
},
|
||||
graph: {
|
||||
build: async ({ query }, b) =>
|
||||
inlineGraphSubgraph(b, query, { budget: 3000 }),
|
||||
inputs: { query: `${sid} ${sTitle} ${midTitle}` },
|
||||
},
|
||||
},
|
||||
});
|
||||
const parts = [];
|
||||
if (composed) parts.push(composed);
|
||||
const knowledgeBlockRD = await inlineKnowledgeScoped(base, keywords);
|
||||
if (knowledgeBlockRD) parts.push(knowledgeBlockRD);
|
||||
const graphBlockRD = await inlineGraphSubgraph(
|
||||
base,
|
||||
`${sid} ${sTitle} ${midTitle}`,
|
||||
{ budget: 3000 },
|
||||
);
|
||||
if (graphBlockRD) parts.push(graphBlockRD);
|
||||
const inlinedContext = capPreamble(
|
||||
`## Inlined Context (preloaded — do not re-read these files)\n\n${parts.join("\n\n---\n\n")}`,
|
||||
);
|
||||
|
||||
return loadPrompt("rewrite-docs", {
|
||||
milestoneId: mid,
|
||||
milestoneTitle: midTitle,
|
||||
|
|
@ -3299,6 +3555,7 @@ export async function buildRewriteDocsPrompt(
|
|||
overrideContent,
|
||||
documentList,
|
||||
overridesPath: relSfRootFile("OVERRIDES"),
|
||||
inlinedContext,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ A researcher explored the codebase and a planner decomposed the work — you are
|
|||
|
||||
{{gatesToClose}}
|
||||
|
||||
{{inlinedTemplates}}
|
||||
|
||||
## Backing Source Artifacts
|
||||
- Slice plan: `{{planPath}}`
|
||||
- Task plan source: `{{taskPlanPath}}`
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ Apply `pm-planning` skill thinking throughout: use Working Backwards to anchor o
|
|||
|
||||
**Structured questions available: {{structuredQuestionsAvailable}}**
|
||||
|
||||
{{inlinedTemplates}}
|
||||
{{inlinedContext}}
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ This stage runs ONCE per project, before any milestone-level discussion. It prod
|
|||
|
||||
**Structured questions available: {{structuredQuestionsAvailable}}**
|
||||
|
||||
{{inlinedTemplates}}
|
||||
{{inlinedContext}}
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ This stage runs ONCE per project, after `discuss-project` and before any milesto
|
|||
|
||||
**Structured questions available: {{structuredQuestionsAvailable}}**
|
||||
|
||||
{{inlinedTemplates}}
|
||||
{{inlinedContext}}
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ Run **project-level domain research** in 4 parallel dimensions. Read `.sf/PROJEC
|
|||
|
||||
**Structured questions available: {{structuredQuestionsAvailable}}**
|
||||
|
||||
{{inlinedContext}}
|
||||
|
||||
---
|
||||
|
||||
## Stage Banner
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ An override was issued by the user that changes a fundamental decision or approa
|
|||
|
||||
{{documentList}}
|
||||
|
||||
{{inlinedContext}}
|
||||
|
||||
## Instructions
|
||||
|
||||
1. Read each document listed above
|
||||
|
|
|
|||
|
|
@ -0,0 +1,346 @@
|
|||
/**
|
||||
* auto-prompts-m005-migration.test.mjs — M005 S01/S02: Phase 3 v2 migration contracts.
|
||||
*
|
||||
* Purpose:
|
||||
* 1. Prove that the 5 simple builders migrated in M005 S01
|
||||
* (discuss-project, discuss-requirements, research-project,
|
||||
* discuss-milestone, rewrite-docs) produce prompts containing the
|
||||
* expected artifact sections via composeUnitContext v2.
|
||||
* 2. Prove that all 26 declared unit types have manifests in UNIT_MANIFESTS
|
||||
* with valid inline artifact orderings.
|
||||
* 3. Prove that prompt-cache-optimizer.js is no longer importable (dead code
|
||||
* removal verification).
|
||||
*
|
||||
* Consumer: CI regression guard for M005 prompt modularization.
|
||||
*/
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import {
|
||||
buildDiscussMilestonePrompt,
|
||||
buildDiscussProjectPrompt,
|
||||
buildDiscussRequirementsPrompt,
|
||||
buildResearchProjectPrompt,
|
||||
buildRewriteDocsPrompt,
|
||||
} from "../auto-prompts.js";
|
||||
import {
|
||||
closeDatabase,
|
||||
insertMilestone,
|
||||
insertSlice,
|
||||
insertTask,
|
||||
openDatabase,
|
||||
} from "../sf-db.js";
|
||||
import {
|
||||
ARTIFACT_KEYS,
|
||||
KNOWN_UNIT_TYPES,
|
||||
resolveManifest,
|
||||
UNIT_MANIFESTS,
|
||||
} from "../unit-context-manifest.js";
|
||||
|
||||
let tempDirs = [];
|
||||
|
||||
function makeProject(opts = {}) {
|
||||
const dir = mkdtempSync(join(tmpdir(), "sf-m005-prompts-"));
|
||||
tempDirs.push(dir);
|
||||
const mid = opts.mid ?? "M910";
|
||||
const sid = opts.sid ?? "S01";
|
||||
const tid = opts.tid ?? "T01";
|
||||
mkdirSync(join(dir, ".sf", "milestones", mid, "slices", sid, "tasks"), {
|
||||
recursive: true,
|
||||
});
|
||||
writeFileSync(
|
||||
join(dir, ".sf", "milestones", mid, `${mid}-ROADMAP.md`),
|
||||
`# ${mid}: Test Milestone\n\n## ${sid}: Test Slice\n`,
|
||||
);
|
||||
writeFileSync(
|
||||
join(dir, ".sf", "milestones", mid, `${mid}-CONTEXT.md`),
|
||||
`# Context\n\nMilestone context for testing.\n`,
|
||||
);
|
||||
writeFileSync(
|
||||
join(dir, ".sf", "milestones", mid, "slices", sid, `${sid}-CONTEXT.md`),
|
||||
`# Slice Context\n\nSlice context for testing.\n`,
|
||||
);
|
||||
writeFileSync(
|
||||
join(dir, ".sf", "milestones", mid, "slices", sid, `${sid}-PLAN.md`),
|
||||
`# ${sid}: Test Slice\n\n## Tasks\n\n- ${tid}: Do the thing\n`,
|
||||
);
|
||||
return { dir, mid, sid, tid };
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
closeDatabase();
|
||||
for (const dir of tempDirs) {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
tempDirs = [];
|
||||
});
|
||||
|
||||
// ─── R007: knowledge and graph are in ARTIFACT_KEYS ───────────────────────
|
||||
describe("R007 — knowledge and graph in ARTIFACT_KEYS", () => {
|
||||
test("knowledge_is_declared_in_artifact_keys", () => {
|
||||
expect(ARTIFACT_KEYS).toContain("knowledge");
|
||||
});
|
||||
|
||||
test("graph_is_declared_in_artifact_keys", () => {
|
||||
expect(ARTIFACT_KEYS).toContain("graph");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── R009: Builder ordering safety — manifest coverage ─────────────────────
|
||||
describe("R009 — manifest coverage for all 26 unit types", () => {
|
||||
test("every_known_unit_type_has_a_manifest", () => {
|
||||
const missing = KNOWN_UNIT_TYPES.filter(
|
||||
(type) => resolveManifest(type) === null,
|
||||
);
|
||||
expect(missing).toHaveLength(0);
|
||||
expect(missing).toEqual([]);
|
||||
});
|
||||
|
||||
test("every_unit_type_in_unit_manifests_is_known", () => {
|
||||
const extra = Object.keys(UNIT_MANIFESTS).filter(
|
||||
(type) => !KNOWN_UNIT_TYPES.includes(type),
|
||||
);
|
||||
expect(extra).toHaveLength(0);
|
||||
expect(extra).toEqual([]);
|
||||
});
|
||||
|
||||
test("all_manifests_declare_inline_artifacts_array", () => {
|
||||
const withoutInline = KNOWN_UNIT_TYPES.filter((type) => {
|
||||
const m = resolveManifest(type);
|
||||
return !m?.artifacts?.inline || !Array.isArray(m.artifacts.inline);
|
||||
});
|
||||
expect(withoutInline).toHaveLength(0);
|
||||
expect(withoutInline).toEqual([]);
|
||||
});
|
||||
|
||||
test("inline_artifacts_do_not_contain_undeclared_keys", () => {
|
||||
// Every artifact key referenced in any manifest's inline array must be
|
||||
// a registered ARTIFACT_KEY.
|
||||
const undeclared = [];
|
||||
for (const type of KNOWN_UNIT_TYPES) {
|
||||
const m = resolveManifest(type);
|
||||
if (!m?.artifacts?.inline) continue;
|
||||
for (const key of m.artifacts.inline) {
|
||||
if (!ARTIFACT_KEYS.includes(key)) {
|
||||
undeclared.push({ type, key });
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(undeclared).toHaveLength(0);
|
||||
expect(undeclared).toEqual([]);
|
||||
});
|
||||
|
||||
test("inline_artifacts_declare_computed_keys_only_when_registered", () => {
|
||||
// For each manifest that declares computed entries, verify those keys
|
||||
// are registered in ARTIFACT_KEYS.
|
||||
const issues = [];
|
||||
for (const type of KNOWN_UNIT_TYPES) {
|
||||
const m = resolveManifest(type);
|
||||
if (!m?.artifacts?.computed) continue;
|
||||
for (const key of m.artifacts.computed) {
|
||||
if (!ARTIFACT_KEYS.includes(key)) {
|
||||
issues.push({ type, key });
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(issues).toHaveLength(0);
|
||||
expect(issues).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── S01 Simple Builder Migration Tests ──────────────────────────────────
|
||||
describe("buildDiscussProjectPrompt v2", () => {
|
||||
test("discuss_project_prompt_uses_composer_and_contains_inlined_context", async () => {
|
||||
const { dir } = makeProject({ mid: "M901" });
|
||||
openDatabase(join(dir, ".sf", "sf.db"));
|
||||
insertMilestone({
|
||||
id: "M901",
|
||||
title: "Discuss Project",
|
||||
status: "active",
|
||||
planning: { vision: "Test.", successCriteria: [] },
|
||||
});
|
||||
|
||||
const prompt = await buildDiscussProjectPrompt(dir, "false");
|
||||
|
||||
expect(prompt).toContain("## Inlined Context");
|
||||
// The composer wires project through inlineProjectFromDb
|
||||
expect(prompt).toContain("Project");
|
||||
});
|
||||
|
||||
test("discuss_project_prompt_passes_structured_questions_variable", async () => {
|
||||
const { dir } = makeProject({ mid: "M902" });
|
||||
openDatabase(join(dir, ".sf", "sf.db"));
|
||||
insertMilestone({
|
||||
id: "M902",
|
||||
title: "Discuss Project",
|
||||
status: "active",
|
||||
planning: { vision: "Test.", successCriteria: [] },
|
||||
});
|
||||
|
||||
const prompt = await buildDiscussProjectPrompt(dir, "false");
|
||||
|
||||
// Template substitution: structuredQuestionsAvailable is substituted with "false"
|
||||
expect(prompt).toContain("**Structured questions available: false**");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildDiscussRequirementsPrompt v2", () => {
|
||||
test("discuss_requirements_prompt_uses_composer_and_contains_inlined_context", async () => {
|
||||
const { dir } = makeProject({ mid: "M903" });
|
||||
openDatabase(join(dir, ".sf", "sf.db"));
|
||||
insertMilestone({
|
||||
id: "M903",
|
||||
title: "Discuss Requirements",
|
||||
status: "active",
|
||||
planning: { vision: "Test.", successCriteria: [] },
|
||||
});
|
||||
|
||||
const prompt = await buildDiscussRequirementsPrompt(dir, "false");
|
||||
|
||||
expect(prompt).toContain("## Inlined Context");
|
||||
// Composer wires requirements through inlineRequirementsFromDb
|
||||
expect(prompt).toContain("Requirements");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildResearchProjectPrompt v2", () => {
|
||||
test("research_project_prompt_uses_composer_and_contains_inlined_context", async () => {
|
||||
const { dir } = makeProject({ mid: "M904" });
|
||||
openDatabase(join(dir, ".sf", "sf.db"));
|
||||
insertMilestone({
|
||||
id: "M904",
|
||||
title: "Research Project",
|
||||
status: "active",
|
||||
planning: { vision: "Test.", successCriteria: [] },
|
||||
});
|
||||
|
||||
const prompt = await buildResearchProjectPrompt(dir, "false");
|
||||
|
||||
expect(prompt).toContain("## Inlined Context");
|
||||
// Composer wires project, requirements, decisions through DB helpers
|
||||
expect(prompt).toContain("Project");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildDiscussMilestonePrompt v2", () => {
|
||||
test("discuss_milestone_prompt_uses_composer_and_contains_inlined_context", async () => {
|
||||
const { dir, mid } = makeProject({ mid: "M905" });
|
||||
openDatabase(join(dir, ".sf", "sf.db"));
|
||||
insertMilestone({
|
||||
id: mid,
|
||||
title: "Discuss Milestone",
|
||||
status: "active",
|
||||
planning: { vision: "Test.", successCriteria: [] },
|
||||
});
|
||||
|
||||
const prompt = await buildDiscussMilestonePrompt(
|
||||
mid,
|
||||
"Discuss Milestone",
|
||||
dir,
|
||||
"false",
|
||||
);
|
||||
|
||||
expect(prompt).toContain("## Inlined Context");
|
||||
// Composer wires milestone-context
|
||||
expect(prompt).toContain("Milestone Context");
|
||||
});
|
||||
|
||||
test("discuss_milestone_prompt_appends_draft_seed_when_present", async () => {
|
||||
const { dir, mid } = makeProject({ mid: "M906" });
|
||||
openDatabase(join(dir, ".sf", "sf.db"));
|
||||
insertMilestone({
|
||||
id: mid,
|
||||
title: "Discuss Milestone",
|
||||
status: "active",
|
||||
planning: { vision: "Test.", successCriteria: [] },
|
||||
});
|
||||
// Write a CONTEXT-DRAFT
|
||||
writeFileSync(
|
||||
join(dir, ".sf", "milestones", mid, `${mid}-CONTEXT-DRAFT.md`),
|
||||
"# Draft Context\n\nThis is a draft.\n",
|
||||
);
|
||||
|
||||
const prompt = await buildDiscussMilestonePrompt(
|
||||
mid,
|
||||
"Discuss Milestone",
|
||||
dir,
|
||||
"false",
|
||||
);
|
||||
|
||||
expect(prompt).toContain("Prior Discussion (Draft Seed)");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildRewriteDocsPrompt v2", () => {
|
||||
test("rewrite_docs_prompt_uses_composer_and_includes_inlined_context_header", async () => {
|
||||
const { dir, mid, sid } = makeProject({ mid: "M907", sid: "S01" });
|
||||
openDatabase(join(dir, ".sf", "sf.db"));
|
||||
insertMilestone({
|
||||
id: mid,
|
||||
title: "Rewrite Docs",
|
||||
status: "active",
|
||||
planning: { vision: "Test.", successCriteria: [] },
|
||||
});
|
||||
insertSlice({
|
||||
milestoneId: mid,
|
||||
id: sid,
|
||||
title: "Docs Slice",
|
||||
status: "active",
|
||||
risk: "low",
|
||||
depends: [],
|
||||
demo: "Docs done.",
|
||||
sequence: 1,
|
||||
});
|
||||
|
||||
const prompt = await buildRewriteDocsPrompt(
|
||||
mid,
|
||||
"Rewrite Docs",
|
||||
{ id: sid, title: "Docs Slice" },
|
||||
dir,
|
||||
[],
|
||||
);
|
||||
|
||||
// Composer produces the inlined context section header
|
||||
expect(prompt).toContain("## Inlined Context");
|
||||
// The composeUnitContext produces the section even when resolvers return null
|
||||
// (empty project/requirements/decisions in test fixture — fine for structural test)
|
||||
});
|
||||
|
||||
test("rewrite_docs_prompt_includes_document_list_with_slice_plan_path", async () => {
|
||||
const { dir, mid, sid } = makeProject({ mid: "M908", sid: "S01" });
|
||||
openDatabase(join(dir, ".sf", "sf.db"));
|
||||
insertMilestone({
|
||||
id: mid,
|
||||
title: "Rewrite Docs",
|
||||
status: "active",
|
||||
planning: { vision: "Test.", successCriteria: [] },
|
||||
});
|
||||
insertSlice({
|
||||
milestoneId: mid,
|
||||
id: sid,
|
||||
title: "Docs Slice",
|
||||
status: "active",
|
||||
risk: "low",
|
||||
depends: [],
|
||||
demo: "Docs done.",
|
||||
sequence: 1,
|
||||
});
|
||||
|
||||
const prompt = await buildRewriteDocsPrompt(
|
||||
mid,
|
||||
"Rewrite Docs",
|
||||
{ id: sid, title: "Docs Slice" },
|
||||
dir,
|
||||
[],
|
||||
);
|
||||
|
||||
// The docList includes the slice plan path (rendered from {{documentList}})
|
||||
expect(prompt).toContain("S01-PLAN.md");
|
||||
// Document list section header is present
|
||||
expect(prompt).toContain("Documents to Review and Update");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── R010 verification deferred to S02 ─────────────────────────────────────
|
||||
// prompt-cache-optimizer.js removal is handled in M005/S02.
|
||||
|
|
@ -103,8 +103,8 @@ export async function composeUnitContext(unitType, opts) {
|
|||
runComputed(manifest.artifacts.computed ?? [], opts),
|
||||
]);
|
||||
const inlineBlocks = [
|
||||
...inlineResolved.filter((b) => !!b && b.length > 0),
|
||||
...excerptResolved.filter((b) => !!b && b.length > 0),
|
||||
...inlineResolved.filter((b) => b != null && b.length > 0),
|
||||
...excerptResolved.filter((b) => b != null && b.length > 0),
|
||||
...computedBlocks,
|
||||
];
|
||||
return {
|
||||
|
|
@ -128,5 +128,5 @@ async function runComputed(ids, opts) {
|
|||
return entry.build(entry.inputs, opts.base);
|
||||
}),
|
||||
);
|
||||
return results.filter((b) => !!b && b.length > 0);
|
||||
return results.filter((b) => b != null && b.length > 0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,6 +69,9 @@ export const ARTIFACT_KEYS = [
|
|||
"previous-validation", // prior VALIDATION file for re-validation rounds
|
||||
// History-scoped
|
||||
"prior-milestone-summary", // previous milestone's SUMMARY (plan-milestone)
|
||||
// Computed (async, budgeted) — these are not file paths but derived from DB/memory/graph
|
||||
"knowledge", // ranked knowledge entries scoped by keywords (knowledge-injector + DB memories)
|
||||
"graph", // code-graph subgraph scoped by milestone/slice/task identifiers
|
||||
];
|
||||
// ─── Manifests ────────────────────────────────────────────────────────────
|
||||
// Phase 1 policy: every manifest encodes today's behavior. Skills = "all"
|
||||
|
|
@ -217,6 +220,9 @@ export const UNIT_MANIFESTS = {
|
|||
preferences: "active-only",
|
||||
tools: TOOLS_PLANNING,
|
||||
artifacts: {
|
||||
// #4782 phase 3: Q1 decision — added computed entries to match
|
||||
// the pattern of similar discuss-type builders. Knowledge/graph
|
||||
// are appended after templates (not prepended).
|
||||
inline: [
|
||||
"project",
|
||||
"requirements",
|
||||
|
|
@ -226,6 +232,7 @@ export const UNIT_MANIFESTS = {
|
|||
],
|
||||
excerpt: [],
|
||||
onDemand: [],
|
||||
computed: ["knowledge", "graph"],
|
||||
},
|
||||
maxSystemPromptChars: COMMON_BUDGET_MEDIUM,
|
||||
},
|
||||
|
|
@ -520,9 +527,15 @@ export const UNIT_MANIFESTS = {
|
|||
preferences: "active-only",
|
||||
tools: TOOLS_DOCS,
|
||||
artifacts: {
|
||||
// #4782 phase 3: added computed entries for knowledge/graph.
|
||||
// Static inline: project, requirements, decisions, templates.
|
||||
// The document-list path accumulation (from the existing imperative
|
||||
// builder) is preserved as the primary onDemand path list in
|
||||
// the builder itself; the composer handles the structured inlines.
|
||||
inline: ["project", "requirements", "decisions", "templates"],
|
||||
excerpt: [],
|
||||
onDemand: [],
|
||||
computed: ["knowledge", "graph"],
|
||||
},
|
||||
maxSystemPromptChars: COMMON_BUDGET_MEDIUM,
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue