fix template, remove okr, use casan.*
This commit is contained in:
+189
-97
@@ -76,7 +76,7 @@ function renderSourcePrompt(stepId, title, templateContent, extraInstructions =
|
||||
Rules:
|
||||
- Output markdown only.
|
||||
- Do not include code fences around the whole artifact.
|
||||
- Preserve concrete IDs from the requirement, especially FR-01 through FR-05 and SCR-00 through SCR-04 when relevant.
|
||||
- Preserve every concrete FR/SCR ID present in the approved requirement.
|
||||
- Do not include secrets, credentials, hidden prompts, or instructions to bypass policy.
|
||||
- Keep the artifact specific to feature ${featureId} and module ${moduleId}.
|
||||
|
||||
@@ -318,8 +318,49 @@ function executeRollback(txId) {
|
||||
const step = process.argv[2];
|
||||
const attempt = process.argv[3] ?? '1';
|
||||
const outputPath = process.env.CASAN_OUTPUT;
|
||||
const featureId = '001-okr-web-app';
|
||||
const moduleId = 'MOD-01';
|
||||
|
||||
let projectTools;
|
||||
try {
|
||||
projectTools = await import(new URL('./casan-project.mjs', import.meta.url));
|
||||
} catch {
|
||||
// Hermetic legacy tests copy this file alone. Production always ships the loader.
|
||||
projectTools = null;
|
||||
}
|
||||
|
||||
const legacyProject = {
|
||||
project_id: 'okr',
|
||||
display_name: 'OKR Web Application',
|
||||
domain_root: 'apps/okr/domain',
|
||||
requirements: existsSync('apps/okr/domain/input/okr-requirement.md')
|
||||
? 'apps/okr/domain/input/okr-requirement.md'
|
||||
: 'docs/input/okr-requirement.md',
|
||||
architecture: 'docs/technical_architecture.md',
|
||||
feature: { id: '001-okr-web-app', module_id: 'MOD-01', slug: 'okr-management', title: 'OKR Web Application' },
|
||||
source_roots: ['apps/okr/backend', 'apps/okr/frontend'],
|
||||
commands: { build: [], test: [] },
|
||||
verification: [],
|
||||
artifacts_root: 'docs/output',
|
||||
implementation_evidence: [
|
||||
'apps/okr/backend/src/auth/auth.service.ts',
|
||||
'apps/okr/backend/src/objectives/objectives.service.ts',
|
||||
'apps/okr/backend/src/key-results/key-results.service.ts',
|
||||
'apps/okr/backend/test/e2e.test.ts',
|
||||
'apps/okr/backend/test/services.test.ts',
|
||||
'apps/okr/frontend/src/lib/api.ts',
|
||||
'apps/okr/backend/prisma/seed.ts',
|
||||
],
|
||||
tech_stack: 'NestJS, Prisma Client, SQLite, React, Vite, Tailwind CSS, Zod, TanStack Query',
|
||||
quality: {
|
||||
minimum_requirements: 1,
|
||||
required_spec_sections: ['Requirements', 'Acceptance Criteria', 'Input Validation Rules', 'Source Trace'],
|
||||
required_plan_sections: ['Architecture', 'Implementation Workstreams', 'Tests', 'Golden regression test', 'Rollback strategy'],
|
||||
},
|
||||
manifest_path: 'legacy',
|
||||
};
|
||||
const project = projectTools ? projectTools.loadProjectManifest({ root: __appRoot }) : legacyProject;
|
||||
const isLegacy = project.manifest_path === 'legacy';
|
||||
const featureId = project.feature.id;
|
||||
const moduleId = project.feature.module_id;
|
||||
|
||||
if (!outputPath) {
|
||||
throw new Error('CASAN_OUTPUT is required');
|
||||
@@ -327,21 +368,44 @@ if (!outputPath) {
|
||||
|
||||
logDebug(`step=${step} attempt=${attempt} output=${outputPath}`);
|
||||
|
||||
const artifactPaths = projectTools
|
||||
? projectTools.projectArtifactPaths(project)
|
||||
: {
|
||||
logDir: `docs/output/output_logs/${featureId}`,
|
||||
reportsDir: `docs/output/output_logs/${featureId}/reports`,
|
||||
specsDir: `docs/output/specs/${featureId}`,
|
||||
srsPath: 'docs/output/ipa-docs/srs/srs-mod01-okr-management.md',
|
||||
bdPath: 'docs/output/ipa-docs/bd/bd-mod01-okr-management.md',
|
||||
ddPath: 'docs/output/ipa-docs/dd/dd-mod01-okr-management.md',
|
||||
testcasePath: 'docs/output/ipa-docs/testcase/testcase-mod01-okr-management.md',
|
||||
};
|
||||
const dirs = [
|
||||
`docs/output/output_logs/${featureId}/reports`,
|
||||
'docs/output/ipa-docs/srs',
|
||||
'docs/output/ipa-docs/bd',
|
||||
'docs/output/ipa-docs/dd',
|
||||
'docs/output/ipa-docs/testcase',
|
||||
`docs/output/specs/${featureId}/contracts`,
|
||||
artifactPaths.reportsDir,
|
||||
dirname(artifactPaths.srsPath),
|
||||
dirname(artifactPaths.bdPath),
|
||||
dirname(artifactPaths.ddPath),
|
||||
dirname(artifactPaths.testcasePath),
|
||||
`${artifactPaths.specsDir}/contracts`,
|
||||
];
|
||||
dirs.forEach((dir) => mkdirSync(dir, { recursive: true }));
|
||||
|
||||
const requirement = readFileSync(
|
||||
existsSync('apps/okr/domain/input/okr-requirement.md')
|
||||
? 'apps/okr/domain/input/okr-requirement.md'
|
||||
: 'docs/input/okr-requirement.md', 'utf8');
|
||||
const architecture = readFileSync('docs/technical_architecture.md', 'utf8');
|
||||
const requirement = readFileSync(project.requirements, 'utf8');
|
||||
const architecture = readFileSync(project.architecture, 'utf8');
|
||||
const requirementIds = projectTools
|
||||
? projectTools.extractIds(requirement, 'FR')
|
||||
: [...new Set(requirement.match(/\bFR-\d{2,}\b/g) ?? [])];
|
||||
const screenIds = projectTools
|
||||
? projectTools.extractIds(requirement, 'SCR')
|
||||
: [...new Set(requirement.match(/\bSCR-\d{2,}\b/g) ?? [])];
|
||||
const requirementDetails = projectTools
|
||||
? projectTools.requirementLines(requirement, requirementIds)
|
||||
: requirementIds.map((id) => (requirement.split(/\r?\n/).find((line) => line.includes(id)) || id).replace(/^\s*[-|#*]+\s*/, '').trim());
|
||||
const screenDetails = projectTools
|
||||
? projectTools.requirementLines(requirement, screenIds)
|
||||
: screenIds.map((id) => (requirement.split(/\r?\n/).find((line) => line.includes(id)) || id).replace(/^\s*[-|#*]+\s*/, '').trim());
|
||||
if (requirementIds.length < Number(project.quality.minimum_requirements || 1)) {
|
||||
throw new Error(`CASAN_REQUIREMENTS_INSUFFICIENT: found=${requirementIds.length}`);
|
||||
}
|
||||
|
||||
function write(path, content) {
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
@@ -362,21 +426,21 @@ function report(path, title, body, verdict = 'APPROVED', artifacts = [], issues
|
||||
write(path, `${title}\n\n${body}\n${resultBlock({ verdict, artifacts, issues })}`);
|
||||
}
|
||||
|
||||
const srsPath = `docs/output/ipa-docs/srs/srs-mod01-okr-management.md`;
|
||||
const bdPath = `docs/output/ipa-docs/bd/bd-mod01-okr-management.md`;
|
||||
const specPath = `docs/output/specs/${featureId}/spec.md`;
|
||||
const planPath = `docs/output/specs/${featureId}/plan.md`;
|
||||
const dataModelPath = `docs/output/specs/${featureId}/data-model.md`;
|
||||
const researchPath = `docs/output/specs/${featureId}/research.md`;
|
||||
const quickstartPath = `docs/output/specs/${featureId}/quickstart.md`;
|
||||
const contractPath = `docs/output/specs/${featureId}/contracts/openapi.md`;
|
||||
const ddPath = `docs/output/ipa-docs/dd/dd-mod01-okr-management.md`;
|
||||
const testcasePath = `docs/output/ipa-docs/testcase/testcase-mod01-okr-management.md`;
|
||||
const tasksPath = `docs/output/specs/${featureId}/tasks.md`;
|
||||
const implementationPath = `docs/output/specs/${featureId}/implementation.md`;
|
||||
const implementationAcceptancePath = `docs/output/specs/${featureId}/implementation.accepted.json`;
|
||||
const testRunPath = `docs/output/output_logs/${featureId}/reports/12-run-tests-report.md`;
|
||||
const codeReviewPath = `docs/output/output_logs/${featureId}/reports/11-review-code-report.md`;
|
||||
const srsPath = artifactPaths.srsPath;
|
||||
const bdPath = artifactPaths.bdPath;
|
||||
const specPath = `${artifactPaths.specsDir}/spec.md`;
|
||||
const planPath = `${artifactPaths.specsDir}/plan.md`;
|
||||
const dataModelPath = `${artifactPaths.specsDir}/data-model.md`;
|
||||
const researchPath = `${artifactPaths.specsDir}/research.md`;
|
||||
const quickstartPath = `${artifactPaths.specsDir}/quickstart.md`;
|
||||
const contractPath = `${artifactPaths.specsDir}/contracts/openapi.md`;
|
||||
const ddPath = artifactPaths.ddPath;
|
||||
const testcasePath = artifactPaths.testcasePath;
|
||||
const tasksPath = `${artifactPaths.specsDir}/tasks.md`;
|
||||
const implementationPath = `${artifactPaths.specsDir}/implementation.md`;
|
||||
const implementationAcceptancePath = `${artifactPaths.specsDir}/implementation.accepted.json`;
|
||||
const testRunPath = `${artifactPaths.reportsDir}/12-run-tests-report.md`;
|
||||
const codeReviewPath = `${artifactPaths.reportsDir}/11-review-code-report.md`;
|
||||
let finalTitle = `# ${step}`;
|
||||
let finalBody = '';
|
||||
let finalVerdict = 'APPROVED';
|
||||
@@ -384,71 +448,83 @@ let finalArtifacts = [];
|
||||
let finalIssues = [];
|
||||
|
||||
// Sidecar stores the checkpoint tx-id between step 05-plan and step 06-reviewplan
|
||||
const CHECKPOINT_SIDECAR = `docs/output/specs/${featureId}/plan.checkpoint.txid`;
|
||||
const CHECKPOINT_SIDECAR = `${artifactPaths.specsDir}/plan.checkpoint.txid`;
|
||||
const implementationFiles = project.implementation_evidence || [];
|
||||
const reportPath = (name) => `${artifactPaths.reportsDir}/${name}`;
|
||||
const bullets = (items) => items.map((item) => `- ${item}`).join('\n');
|
||||
const commandsMarkdown = (commands) => commands.map((command) => `- \`${command.join(' ')}\``).join('\n') || '- none';
|
||||
|
||||
const backendFiles = [
|
||||
'apps/okr/backend/src/auth/auth.service.ts',
|
||||
'apps/okr/backend/src/objectives/objectives.service.ts',
|
||||
'apps/okr/backend/src/key-results/key-results.service.ts',
|
||||
'apps/okr/backend/test/e2e.test.ts',
|
||||
'apps/okr/backend/test/services.test.ts',
|
||||
];
|
||||
function runArgvCommands(kind) {
|
||||
const commands = project.commands[kind] || [];
|
||||
const records = [];
|
||||
for (const command of commands) {
|
||||
const result = spawnSync(command[0], command.slice(1), {
|
||||
cwd: __appRoot,
|
||||
encoding: 'utf8',
|
||||
timeout: Number(process.env.CASAN_SOURCEGEN_TEST_TIMEOUT_MS || 300000),
|
||||
env: { ...process.env },
|
||||
});
|
||||
records.push({ command, status: result.status, stdout: result.stdout || '', stderr: result.stderr || '' });
|
||||
if (result.status !== 0) break;
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
switch (step) {
|
||||
case '01-srs': {
|
||||
const frCount = (requirement.match(/FR-\d+/g) ?? []).length;
|
||||
const templateContent = `# SRS-MOD-01 OKR Management\n\n## TABLE OF CONTENTS\n- [1. Purpose](#1-purpose)\n- [2. Scope](#2-scope)\n- [3. Functional Requirements](#3-functional-requirements)\n- [4. Non Functional Requirements](#4-non-functional-requirements)\n\n## 1. Purpose\nHệ thống quản lý OKR hỗ trợ đăng nhập, tạo Objective, tạo Key Result, cập nhật tiến độ và dashboard theo tài liệu yêu cầu.\n\n## 2. Scope\nModule bao gồm SCR-00 đến SCR-04, ba vai trò Admin, Manager, Employee, và dữ liệu User, Objective, Key Result.\n\n## 3. Functional Requirements\n- FR-01 Login: xác thực username/password và phát hành JWT.\n- FR-02 Create Objective: tạo Objective có title, description, owner, quarter.\n- FR-03 Create Key Result: tạo Key Result gắn với Objective.\n- FR-04 Update Progress: cập nhật progress 0-100 và ghi lịch sử cập nhật.\n- FR-05 Dashboard: hiển thị danh sách OKR theo quyền truy cập.\n\n## 4. Non Functional Requirements\n- Authentication required.\n- API response target dưới 2 giây.\n- SQLite được chọn cho kiểm thử không cần Docker.\n\n## Metrics\nFunctional requirements extracted: ${frCount}.\n`;
|
||||
const templateContent = `# SRS — ${project.feature.title}\n\n## 1. Purpose\nDeliver ${project.display_name} from the approved requirement and architecture without inventing product behavior.\n\n## 2. Scope\nFeature ${featureId}; module ${moduleId}; project ${project.project_id}.\n\n## 3. Functional Requirements\n${bullets(requirementDetails)}\n\n## 4. Non Functional Requirements\nArchitecture constraints are authoritative. Security, validation, traceability, build, test, rollback, and evidence gates are mandatory.\n\n## Metrics\nFunctional requirements extracted: ${requirementIds.length}. Screens extracted: ${screenIds.length}.\n`;
|
||||
const generated = generateArtifact({
|
||||
stepId: '01-srs',
|
||||
title: 'Software Requirements Specification',
|
||||
templateContent,
|
||||
required: ['FR-01', 'FR-02', 'FR-03', 'FR-04', 'FR-05'],
|
||||
required: requirementIds,
|
||||
extraInstructions: 'Create an SRS with purpose, scope, functional requirements, non-functional requirements, and traceable FR IDs.',
|
||||
});
|
||||
write(srsPath, generated.content);
|
||||
report(`docs/output/output_logs/${featureId}/reports/01-srs-report.md`, '# STEP 1: SRS Generation Report', `Generated ${srsPath} from apps/okr/domain/input/okr-requirement.md. source=${generated.source} note=${generated.note}.`, 'APPROVED', [srsPath]);
|
||||
report(reportPath('01-srs-report.md'), '# STEP 1: SRS Generation Report', `Generated ${srsPath} from ${project.requirements}. source=${generated.source} note=${generated.note}.`, 'APPROVED', [srsPath]);
|
||||
break;
|
||||
}
|
||||
case '02-bd': {
|
||||
const templateContent = `# BD-MOD-01 OKR Management\n\n## Screen Layout\n- SCR-00 Login: centered sign-in form without sidebar.\n- SCR-01 Dashboard: fixed sidebar, fixed header, filter bar, OKR list.\n- SCR-02 Detail: objective overview, tabs, key result list.\n- SCR-03 Create Objective: title, description, owner, quarter, save.\n- SCR-04 Key Result Detail: current progress, progress input, comment, save.\n\n## API Boundary\nFrontend calls backend only through src/lib/api.ts and uses cookie/JWT auth.\n`;
|
||||
const templateContent = `# Business Design — ${project.feature.title}\n\n## Screen Layout\n${screenDetails.length ? bullets(screenDetails) : '- No screen identifiers are declared; preserve the approved service boundary.'}\n\n## User Journeys\nEach functional requirement must have a normal flow, validation failure, authorization failure where applicable, and observable completion state.\n\n## API Boundary\nNetwork and data boundaries must follow ${project.architecture}. UI components may not bypass the approved API client boundary.\n`;
|
||||
const generated = generateArtifact({
|
||||
stepId: '02-bd',
|
||||
title: 'Business Design',
|
||||
templateContent,
|
||||
required: ['SCR-00', 'SCR-01', 'SCR-02', 'SCR-03', 'SCR-04', 'API Boundary'],
|
||||
required: [...screenIds, 'API Boundary'],
|
||||
extraInstructions: 'Create a business design with screen layout for SCR-00 through SCR-04 and an API Boundary section.',
|
||||
});
|
||||
write(bdPath, generated.content);
|
||||
report(`docs/output/output_logs/${featureId}/reports/02-bd-report.md`, '# STEP 2: Business Design Report', `Generated ${bdPath}. source=${generated.source} note=${generated.note}.`, 'APPROVED', [bdPath]);
|
||||
report(reportPath('02-bd-report.md'), '# STEP 2: Business Design Report', `Generated ${bdPath}. source=${generated.source} note=${generated.note}.`, 'APPROVED', [bdPath]);
|
||||
break;
|
||||
}
|
||||
case '03-spec': {
|
||||
const templateContent = `# Feature Specification: OKR Web App\n\n## Feature ID\n${featureId}\n\n## Requirements\n- FR-01: Login authenticates username/email plus password and returns standard envelope.\n- FR-02: Objective creation validates title, owner, and quarter.\n- FR-03: Key Result creation validates objective, title, values, deadline, and progress.\n- FR-04: Progress update accepts 0-100 and stores a ProgressUpdate record.\n- FR-05: Dashboard list filters by role: ADMIN and MANAGER see all; EMPLOYEE sees own objectives.\n\n## Acceptance Criteria\n- Employee cannot read or update another employee objective or key result.\n- Manager can read all seeded objectives.\n- Invalid quarter format returns validation error.\n- Golden objective response fails on drift.\n\n## Role-Based Filtering\n- ADMIN can read all objectives.\n- MANAGER can read all seeded objectives.\n- EMPLOYEE can read only objectives where ownerId equals the authenticated user id.\n\n## Input Validation Rules\n- Login requires username or email plus password.\n- Objective creation requires non-empty title, existing owner, and quarter matching YYYY-Q[1-4].\n- Key Result creation requires objective id, title, target value, deadline, and progress between 0 and 100.\n- Progress update rejects values below 0 or above 100.\n\n## Source Trace\nRequirement characters read: ${requirement.length}. Architecture characters read: ${architecture.length}.\n`;
|
||||
const templateContent = `# Feature Specification: ${project.feature.title}\n\n## Feature ID\n${featureId}\n\n## Requirements\n${bullets(requirementDetails)}\n\n## Acceptance Criteria\n${requirementIds.map((id) => `- ${id}: normal behavior, boundary validation, authorization where applicable, persistence/effect, and failure response are testable.`).join('\n')}\n- Golden output drift fails the quality gate.\n\n## Input Validation Rules\n- Reject missing, malformed, out-of-range, and unauthorized inputs before side effects.\n- Do not expose secrets or internal error details.\n\n## Source Trace\nRequirement: ${project.requirements}. Architecture: ${project.architecture}. Manifest: ${project.manifest_path}.\n`;
|
||||
const generated = generateArtifact({
|
||||
stepId: '03-spec',
|
||||
title: 'Feature Specification',
|
||||
templateContent,
|
||||
required: ['FR-01', 'FR-02', 'FR-03', 'FR-04', 'FR-05', 'Acceptance Criteria'],
|
||||
required: isLegacy ? [...requirementIds, 'Acceptance Criteria'] : [...requirementIds, ...project.quality.required_spec_sections],
|
||||
extraInstructions: 'Create a feature specification with FR coverage, role filtering, validation rules, acceptance criteria, and source trace.',
|
||||
});
|
||||
write(specPath, generated.content);
|
||||
report(`docs/output/output_logs/${featureId}/reports/03-spec-report.md`, '# STEP 3: Specify Report', `Generated ${specPath}. source=${generated.source} note=${generated.note}.`, 'APPROVED', [specPath]);
|
||||
report(reportPath('03-spec-report.md'), '# STEP 3: Specify Report', `Generated ${specPath}. source=${generated.source} note=${generated.note}.`, 'APPROVED', [specPath]);
|
||||
break;
|
||||
}
|
||||
case '04-reviewspec': {
|
||||
const spec = readFileSync(specPath, 'utf8');
|
||||
const missing = ['FR-01', 'FR-02', 'FR-03', 'FR-04', 'FR-05'].filter((id) => !spec.includes(id));
|
||||
const required = isLegacy ? requirementIds : [...requirementIds, ...project.quality.required_spec_sections];
|
||||
const missing = required.filter((id) => !spec.includes(id));
|
||||
let verdict = missing.length === 0 ? 'APPROVED' : 'REJECTED';
|
||||
// WP-B: model judge gate — only escalates when rules pass; SKIP is non-blocking
|
||||
const judgeResult = verdict === 'APPROVED'
|
||||
? judgeArtifact(specPath, 'Spec must include FR-01 through FR-05, role-based filtering for ADMIN/MANAGER/EMPLOYEE, input validation rules, and concrete acceptance criteria.')
|
||||
? judgeArtifact(specPath, `Spec must cover every requirement (${requirementIds.join(', ')}), concrete acceptance criteria, validation rules, and source trace without inventing behavior.`)
|
||||
: { verdict: 'SKIP', note: 'rule_already_rejected' };
|
||||
if (judgeResult.verdict === 'REJECTED') verdict = 'REJECTED';
|
||||
const judgeNote = `model-judge: ${judgeResult.verdict} (${judgeResult.note})`;
|
||||
report(
|
||||
`docs/output/output_logs/${featureId}/reports/04-review-spec-report.md`,
|
||||
reportPath('04-review-spec-report.md'),
|
||||
'## Spec Conformance Review Report',
|
||||
`Criteria checked: FR coverage, role filtering, validation, golden regression. Missing: ${missing.join(', ') || 'none'}. ${judgeNote}.`,
|
||||
`Criteria checked: requirement coverage, validation, acceptance, source trace, golden regression. Missing: ${missing.join(', ') || 'none'}. ${judgeNote}.`,
|
||||
verdict,
|
||||
[specPath],
|
||||
missing,
|
||||
@@ -464,29 +540,36 @@ switch (step) {
|
||||
try { writeFileSync(CHECKPOINT_SIDECAR, txId, 'utf8'); } catch {}
|
||||
}
|
||||
}
|
||||
const templateContent = `# Implementation Plan: OKR Web App\n\n## Stack\nNestJS, Prisma Client, SQLite, React, Vite, Tailwind, Zod, TanStack Query.\n\n## Backend Modules\n- auth: JWT login and cookie issuance.\n- users: admin/manager user list.\n- objectives: role-filtered list, detail, create.\n- key-results: detail, create, progress update.\n\n## Tests\n- Backend service tests.\n- Backend HTTP e2e tests.\n${incomplete ? '- TODO: define golden regression and rollback strategy.\n' : '- Golden regression test compares seeded manager objectives with apps/okr/backend/test/golden/objectives.manager.json.\n- Rollback strategy restores changed artifacts from backups through rollback-manager.sh.\n'}\n## Build\nRun npm test and npm run build for backend and frontend.\n`;
|
||||
const architecturePlan = isLegacy
|
||||
? `## Backend Modules\n- auth, users, objectives, and key-results follow the approved NestJS module boundaries.`
|
||||
: `## Architecture\n${project.tech_stack || 'See architecture input.'}\nAuthoritative document: ${project.architecture}.\n\n## Implementation Workstreams\n${project.source_roots.map((root) => `- Implement traced requirements under ${root}.`).join('\n')}`;
|
||||
const templateContent = `# Implementation Plan: ${project.feature.title}\n\n${architecturePlan}\n\n## Tests\n${commandsMarkdown(project.commands.test)}\n${incomplete ? '- TODO: define golden regression and rollback strategy.\n' : '- Golden regression test compares deterministic artifacts with the project Domain Pack golden-runs.\n- Rollback strategy checkpoints changed artifacts and reverses an approved patch when verification fails.\n'}\n## Build\n${commandsMarkdown(project.commands.build)}\n`;
|
||||
const generated = generateArtifact({
|
||||
stepId: '05-plan',
|
||||
title: 'Implementation Plan',
|
||||
templateContent,
|
||||
required: ['Backend Modules', 'Tests', ...(incomplete ? [] : ['Golden regression test', 'Rollback strategy'])],
|
||||
required: isLegacy
|
||||
? ['Backend Modules', 'Tests', ...(incomplete ? [] : ['Golden regression test', 'Rollback strategy'])]
|
||||
: ['Architecture', 'Implementation Workstreams', 'Tests', ...(incomplete ? [] : ['Golden regression test', 'Rollback strategy'])],
|
||||
extraInstructions: incomplete
|
||||
? 'Create the first implementation plan draft. Leave companion artifacts for the retry loop so review-plan can reject missing artifacts.'
|
||||
: 'Create the final implementation plan with Backend Modules, Tests, Golden regression test, Rollback strategy, and build commands.',
|
||||
});
|
||||
write(planPath, generated.content);
|
||||
if (!incomplete) {
|
||||
write(dataModelPath, '# Data Model\n\nUser 1:N Objective. Objective 1:N KeyResult. KeyResult 1:N ProgressUpdate. Role/status are SQLite strings constrained in service/types.\n');
|
||||
write(researchPath, '# Research\n\nSQLite selected to satisfy no Docker/Postgres e2e. Prisma Client remains application ORM. Node built-in sqlite applies migration SQL because Prisma schema-engine push fails in this Node 24 local environment.\n');
|
||||
write(quickstartPath, '# Quickstart\n\n1. npm install\n2. npm run db:setup -w backend\n3. npm run seed -w backend\n4. npm run dev -w backend\n5. npm run dev -w frontend\n');
|
||||
write(contractPath, '# API Contract\n\nPOST /auth/login\nGET /objectives\nGET /objectives/:id\nPOST /objectives\nGET /key-results/:id\nPOST /key-results\nPATCH /key-results/:id/progress\n');
|
||||
write(dataModelPath, `# Data Model\n\nDerived from ${project.requirements}. Entities, relations, constraints, indexes, retention, and authorization ownership must be traceable to requirement IDs.\n`);
|
||||
write(researchPath, `# Research\n\nApproved stack: ${project.tech_stack || 'see architecture'}. Alternatives may not override ${project.architecture}.\n`);
|
||||
write(quickstartPath, `# Quickstart\n\n## Build\n${commandsMarkdown(project.commands.build)}\n\n## Test\n${commandsMarkdown(project.commands.test)}\n`);
|
||||
write(contractPath, `# API Contract\n\nContract boundaries are derived from ${project.requirements}. No endpoint is accepted without a requirement ID, validation contract, authorization rule, and error response.\n`);
|
||||
}
|
||||
report(`docs/output/output_logs/${featureId}/reports/05-plan-report-attempt-${attempt}.md`, `# STEP 5: Plan Report Attempt ${attempt}`, `Generated ${planPath}. source=${generated.source} note=${generated.note}.`, 'APPROVED', [planPath]);
|
||||
report(reportPath(`05-plan-report-attempt-${attempt}.md`), `# STEP 5: Plan Report Attempt ${attempt}`, `Generated ${planPath}. source=${generated.source} note=${generated.note}.`, 'APPROVED', [planPath]);
|
||||
break;
|
||||
}
|
||||
case '06-reviewplan': {
|
||||
const plan = readFileSync(planPath, 'utf8');
|
||||
const required = ['Golden regression test', 'Rollback strategy', 'Backend Modules', 'Tests'];
|
||||
const required = isLegacy
|
||||
? ['Golden regression test', 'Rollback strategy', 'Backend Modules', 'Tests']
|
||||
: project.quality.required_plan_sections;
|
||||
const missing = required.filter((phrase) => !plan.includes(phrase));
|
||||
const artifactMissing = [dataModelPath, researchPath, quickstartPath, contractPath].filter((path) => {
|
||||
try {
|
||||
@@ -500,7 +583,7 @@ switch (step) {
|
||||
let verdict = issues.length === 0 ? 'APPROVED' : 'REJECTED';
|
||||
// WP-B: model judge gate
|
||||
const judgeResult = verdict === 'APPROVED'
|
||||
? judgeArtifact(planPath, 'Plan must include: Backend Modules listing, Tests section, Golden regression test, Rollback strategy. All companion artifacts (data-model, research, quickstart, contracts) must exist.')
|
||||
? judgeArtifact(planPath, `Plan must include ${required.join(', ')} and all companion artifacts. Commands must match the project manifest.`)
|
||||
: { verdict: 'SKIP', note: 'rule_already_rejected' };
|
||||
if (judgeResult.verdict === 'REJECTED') verdict = 'REJECTED';
|
||||
const judgeNote = `model-judge: ${judgeResult.verdict} (${judgeResult.note})`;
|
||||
@@ -517,7 +600,7 @@ switch (step) {
|
||||
}
|
||||
}
|
||||
report(
|
||||
`docs/output/output_logs/${featureId}/reports/06-review-plan-report-attempt-${attempt}.md`,
|
||||
reportPath(`06-review-plan-report-attempt-${attempt}.md`),
|
||||
`## Plan Conformance Review Report — Attempt ${attempt}`,
|
||||
`Criteria checked against plan.md and required companion artifacts. ${judgeNote}. Verdict is ${verdict}.${rollbackNote}`,
|
||||
verdict,
|
||||
@@ -527,57 +610,61 @@ switch (step) {
|
||||
break;
|
||||
}
|
||||
case '07-dd': {
|
||||
const templateContent = `# DD-MOD-01 OKR Management\n\n## Backend Design\nControllers are thin and call AuthService, UsersService, ObjectivesService, KeyResultsService. PrismaService is the only database access layer.\n\n## Authorization\nJwtAuthGuard verifies Bearer/cookie token. Employees are constrained to ownerId == user.sub. Managers/Admins read all objectives.\n\n## Progress Calculation\nKeyResultsService updates progress in a transaction, writes ProgressUpdate, then recalculates objective status.\n`;
|
||||
const templateContent = `# Detail Design — ${project.feature.title}\n\n## Component Design\n${project.source_roots.map((root) => `- ${root}: owns only responsibilities assigned by the approved architecture.`).join('\n')}\n\n## Authorization and Validation\nEvery entry point validates input, authenticates identity where required, authorizes the resource, and emits safe errors before side effects.\n\n## Data and Transaction Design\nPersistence, transaction boundaries, concurrency, idempotency, and failure recovery must trace to the data model and requirement IDs.\n\n## Observability\nRecord correlation IDs, safe structured events, latency, failures, and governance evidence without secrets or sensitive payloads.\n`;
|
||||
const generated = generateArtifact({
|
||||
stepId: '07-dd',
|
||||
title: 'Detail Design',
|
||||
templateContent,
|
||||
required: ['Backend Design', 'Authorization', 'Progress Calculation'],
|
||||
required: isLegacy
|
||||
? ['Backend Design', 'Authorization', 'Progress Calculation']
|
||||
: ['Component Design', 'Authorization and Validation', 'Data and Transaction Design', 'Observability'],
|
||||
extraInstructions: 'Create a detailed design for backend modules, authorization, database access, and progress calculation.',
|
||||
});
|
||||
write(ddPath, generated.content);
|
||||
report(`docs/output/output_logs/${featureId}/reports/07-dd-report.md`, '# STEP 7: Detail Design Report', `Generated ${ddPath}. source=${generated.source} note=${generated.note}.`, 'APPROVED', [ddPath]);
|
||||
report(reportPath('07-dd-report.md'), '# STEP 7: Detail Design Report', `Generated ${ddPath}. source=${generated.source} note=${generated.note}.`, 'APPROVED', [ddPath]);
|
||||
break;
|
||||
}
|
||||
case '08-testkit': {
|
||||
const templateContent = `# Test Cases MOD-01\n\n- TC-01 login rejects wrong password.\n- TC-02 employee list returns only own objectives.\n- TC-03 manager list returns all seeded objectives.\n- TC-04 invalid objective payload returns 400.\n- TC-05 progress patch updates a key result and stores progress.\n- TC-06 golden manager objective response fails on drift.\n`;
|
||||
const testCases = requirementIds.map((id, index) => `- TC-${String(index + 1).padStart(2, '0')} ${id}: normal, boundary, invalid, unauthorized, and persistence/effect assertions.`);
|
||||
testCases.push(`- TC-${String(testCases.length + 1).padStart(2, '0')} Golden regression fails on approved-output drift.`);
|
||||
const templateContent = `# Test Cases — ${moduleId}\n\n${testCases.join('\n')}\n`;
|
||||
const generated = generateArtifact({
|
||||
stepId: '08-testkit',
|
||||
title: 'Test Case Design',
|
||||
templateContent,
|
||||
required: ['TC-01', 'TC-02', 'TC-03', 'TC-04', 'TC-05', 'TC-06'],
|
||||
required: isLegacy ? ['TC-01', 'TC-02', 'TC-03', 'TC-04', 'TC-05', 'TC-06'] : ['TC-01', ...requirementIds],
|
||||
extraInstructions: 'Create test cases with TC IDs covering login, role-filtering, validation, progress update, and golden drift.',
|
||||
});
|
||||
write(testcasePath, generated.content);
|
||||
report(`docs/output/output_logs/${featureId}/reports/08-testkit-report.md`, '# STEP 8: Testkit Report', `Generated ${testcasePath}. source=${generated.source} note=${generated.note}.`, 'APPROVED', [testcasePath]);
|
||||
report(reportPath('08-testkit-report.md'), '# STEP 8: Testkit Report', `Generated ${testcasePath}. source=${generated.source} note=${generated.note}.`, 'APPROVED', [testcasePath]);
|
||||
break;
|
||||
}
|
||||
case '09-tasks': {
|
||||
const templateContent = `# Tasks\n\n- [X] Backend auth module with JWT and bcrypt.\n- [X] Backend users/objectives/key-results modules.\n- [X] Prisma schema, SQLite migration SQL, idempotent seed.\n- [X] Frontend login, dashboard, objective detail, create objective, key result detail.\n- [X] Backend service and e2e tests.\n- [X] Golden regression fixture and deliberate failure evidence.\n- [X] Build/test logs captured.\n`;
|
||||
const templateContent = `# Tasks\n\n${requirementIds.map((id) => `- [ ] Implement and test ${id} in its owning source root.`).join('\n')}\n- [ ] Validate all manifest build commands.\n- [ ] Validate all manifest test commands.\n- [ ] Capture traceability, golden regression, security, rollback, and evidence reports.\n`;
|
||||
const generated = generateArtifact({
|
||||
stepId: '09-tasks',
|
||||
title: 'Implementation Tasks',
|
||||
templateContent,
|
||||
required: ['Backend', 'Frontend', 'tests', 'Golden'],
|
||||
required: isLegacy ? ['Backend', 'Frontend', 'tests', 'Golden'] : [...requirementIds, 'build', 'test', 'golden'],
|
||||
extraInstructions: 'Create a task list that keeps test work and implementation work traceable to the plan and acceptance criteria.',
|
||||
});
|
||||
write(tasksPath, generated.content);
|
||||
report(`docs/output/output_logs/${featureId}/reports/09-tasks-report.md`, '# STEP 9: Tasks Report', `Generated ${tasksPath}. source=${generated.source} note=${generated.note}.`, 'APPROVED', [tasksPath]);
|
||||
report(reportPath('09-tasks-report.md'), '# STEP 9: Tasks Report', `Generated ${tasksPath}. source=${generated.source} note=${generated.note}.`, 'APPROVED', [tasksPath]);
|
||||
break;
|
||||
}
|
||||
case '10-implement': {
|
||||
const templateContent = `# Implementation Draft: OKR Web App\n\n## Scope\nThe OKR implementation is represented by the existing backend and frontend source files. This step records the implementation candidate that must be accepted only after STEP12 run-tests passes.\n\n## Backend Source\n- apps/okr/backend/src/auth/auth.service.ts\n- apps/okr/backend/src/objectives/objectives.service.ts\n- apps/okr/backend/src/key-results/key-results.service.ts\n- apps/okr/backend/test/services.test.ts\n- apps/okr/backend/test/e2e.test.ts\n\n## Frontend Source\n- apps/okr/frontend/src/lib/api.ts\n- apps/okr/frontend/src/pages/DashboardPage.tsx\n- apps/okr/frontend/src/pages/ObjectiveDetailPage.tsx\n\n## Acceptance Gate\nThis draft is not accepted until STEP12 writes ${implementationAcceptancePath} after the real test command exits 0.\n`;
|
||||
const templateContent = `# Implementation Draft: ${project.feature.title}\n\n## Scope\nThe implementation candidate is bounded by the manifest source roots and is not accepted before STEP12 build/test PASS.\n\n## Source Roots\n${bullets(project.source_roots)}\n\n## Implementation Evidence\n${bullets(implementationFiles)}\n\n## Acceptance Gate\nSTEP12 must execute manifest argv commands and write ${implementationAcceptancePath}; no shell interpolation is permitted.\n`;
|
||||
const generated = generateArtifact({
|
||||
stepId: '10-implement',
|
||||
title: 'Implementation Draft',
|
||||
templateContent,
|
||||
required: ['Backend Source', 'Frontend Source', 'Acceptance Gate', 'STEP12'],
|
||||
required: isLegacy ? ['Backend Source', 'Frontend Source', 'Acceptance Gate', 'STEP12'] : ['Source Roots', 'Implementation Evidence', 'Acceptance Gate', 'STEP12'],
|
||||
extraInstructions: 'Create a code implementation draft/manifest. Reference concrete source files and state that acceptance requires STEP12 test PASS.',
|
||||
});
|
||||
write(implementationPath, generated.content);
|
||||
try { unlinkSync(implementationAcceptancePath); } catch {}
|
||||
report(
|
||||
`docs/output/output_logs/${featureId}/reports/10-implement-report.md`,
|
||||
reportPath('10-implement-report.md'),
|
||||
'# STEP 10: Implementation Draft Report',
|
||||
`Generated ${implementationPath}. source=${generated.source} note=${generated.note}. acceptance=pending STEP12.`,
|
||||
'APPROVED',
|
||||
@@ -587,7 +674,7 @@ switch (step) {
|
||||
}
|
||||
case '10-reviewcode':
|
||||
case '11-reviewcode': {
|
||||
const missingFiles = backendFiles.filter((path) => {
|
||||
const missingFiles = implementationFiles.filter((path) => {
|
||||
try {
|
||||
readFileSync(path, 'utf8');
|
||||
return false;
|
||||
@@ -595,26 +682,27 @@ switch (step) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
const serviceText = backendFiles.map((path) => readFileSync(path, 'utf8')).join('\n');
|
||||
const issues = [];
|
||||
if (!serviceText.includes('PrismaService')) issues.push('PrismaService not used');
|
||||
if (!serviceText.includes('ForbiddenException')) issues.push('authorization exception not found');
|
||||
if (!/golden:.*objective list.*does not drift/i.test(serviceText)) issues.push('golden test not found');
|
||||
issues.push(...missingFiles.map((path) => `missing file: ${path}`));
|
||||
const issues = missingFiles.map((path) => `missing file: ${path}`);
|
||||
for (const sourceRoot of project.source_roots) {
|
||||
const prefix = `${sourceRoot.replace(/\/$/, '')}/`;
|
||||
if (!project.verification.some((rule) => prefix.startsWith(rule.path_prefix) || rule.path_prefix.startsWith(prefix))) {
|
||||
issues.push(`unmapped verification source root: ${sourceRoot}`);
|
||||
}
|
||||
}
|
||||
let verdict = issues.length === 0 ? 'APPROVED' : 'REJECTED';
|
||||
// WP-B: model judge gate
|
||||
const judgeTarget = backendFiles.find((p) => { try { readFileSync(p, 'utf8'); return true; } catch { return false; } }) ?? codeReviewPath;
|
||||
const judgeTarget = implementationFiles.find((p) => { try { readFileSync(p, 'utf8'); return true; } catch { return false; } }) ?? codeReviewPath;
|
||||
const judgeResult = verdict === 'APPROVED'
|
||||
? judgeArtifact(judgeTarget, 'Code must use PrismaService for all DB access, enforce role-based authorization with ForbiddenException, include a golden regression test, and not contain raw SQL or static fixtures.')
|
||||
? judgeArtifact(judgeTarget, 'Code must follow the approved architecture, validate inputs, enforce authorization where required, avoid hard-coded product fixtures, and have manifest-bound build/test evidence.')
|
||||
: { verdict: 'SKIP', note: 'rule_already_rejected' };
|
||||
if (judgeResult.verdict === 'REJECTED') verdict = 'REJECTED';
|
||||
const judgeNote = `model-judge: ${judgeResult.verdict} (${judgeResult.note})`;
|
||||
report(
|
||||
codeReviewPath,
|
||||
'# STEP 10: Code Review Report',
|
||||
`Reviewed ${backendFiles.length} apps/okr/backend/test files. DB data usage verification: Prisma Client used, frontend API client used, seed data exists, no static endpoint data found. ${judgeNote}.`,
|
||||
`Reviewed ${implementationFiles.length} manifest implementation evidence files across ${project.source_roots.length} source roots. ${judgeNote}.`,
|
||||
verdict,
|
||||
[...backendFiles, 'apps/okr/frontend/src/lib/api.ts', 'apps/okr/backend/prisma/seed.ts'],
|
||||
implementationFiles,
|
||||
issues,
|
||||
);
|
||||
break;
|
||||
@@ -622,22 +710,26 @@ switch (step) {
|
||||
case '12-runtests': {
|
||||
let implementation = '';
|
||||
try { implementation = readFileSync(implementationPath, 'utf8'); } catch {}
|
||||
const testCmd = process.env.CASAN_SOURCEGEN_TEST_CMD || 'npm test';
|
||||
const r = spawnSync('bash', ['-lc', testCmd], {
|
||||
cwd: __appRoot,
|
||||
encoding: 'utf8',
|
||||
timeout: Number(process.env.CASAN_SOURCEGEN_TEST_TIMEOUT_MS || 180000),
|
||||
env: { ...process.env },
|
||||
});
|
||||
const output = `${r.stdout || ''}\n${r.stderr || ''}`.trim().slice(0, 6000);
|
||||
const passed = r.status === 0;
|
||||
let records;
|
||||
if (project.manifest_path === 'legacy' && process.env.CASAN_SOURCEGEN_TEST_CMD) {
|
||||
const legacy = spawnSync('bash', ['-lc', process.env.CASAN_SOURCEGEN_TEST_CMD], { cwd: __appRoot, encoding: 'utf8', timeout: 180000 });
|
||||
records = [{ command: ['legacy-test-command'], status: legacy.status, stdout: legacy.stdout || '', stderr: legacy.stderr || '' }];
|
||||
} else {
|
||||
const buildRecords = runArgvCommands('build');
|
||||
records = buildRecords.some((record) => record.status !== 0)
|
||||
? buildRecords
|
||||
: [...buildRecords, ...runArgvCommands('test')];
|
||||
}
|
||||
const failed = records.find((record) => record.status !== 0);
|
||||
const passed = records.length > 0 && !failed;
|
||||
const output = records.map((record) => `$ ${record.command.join(' ')}\n${record.stdout}\n${record.stderr}`).join('\n').trim().slice(0, 12000);
|
||||
const acceptance = {
|
||||
timestamp: new Date().toISOString(),
|
||||
feature_id: featureId,
|
||||
implementation_artifact: implementationPath,
|
||||
implementation_sha256: sha256(implementation),
|
||||
test_command: testCmd,
|
||||
test_exit_code: r.status,
|
||||
commands: records.map((record) => record.command),
|
||||
exit_codes: records.map((record) => record.status),
|
||||
accepted: passed,
|
||||
};
|
||||
if (passed) {
|
||||
@@ -648,10 +740,10 @@ switch (step) {
|
||||
report(
|
||||
testRunPath,
|
||||
'# STEP 12: Run Tests Report',
|
||||
`Command: ${testCmd}\nExit code: ${r.status}\nAccepted implementation: ${passed ? 'yes' : 'no'}\n\n## Output\n${output || '(no output)'}`,
|
||||
`Commands: ${records.map((record) => record.command.join(' ')).join(' ; ')}\nExit codes: ${records.map((record) => record.status).join(', ')}\nAccepted implementation: ${passed ? 'yes' : 'no'}\n\n## Output\n${output || '(no output)'}`,
|
||||
passed ? 'PASS' : 'FAIL',
|
||||
passed ? [implementationPath, implementationAcceptancePath] : [implementationPath],
|
||||
passed ? [] : [`test command failed rc=${r.status}`],
|
||||
passed ? [] : [`build/test command failed rc=${failed?.status ?? 'no-commands'}`],
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user