Add source generation mode for first artifacts

This commit is contained in:
thanhnv
2026-07-06 12:38:03 +09:00
parent d0bde2a8b3
commit c49ab251df
5 changed files with 224 additions and 24 deletions
@@ -0,0 +1,98 @@
#!/usr/bin/env bash
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
PASS=0; FAIL=0
pass(){ echo "PASS: $1"; PASS=$((PASS+1)); }
fail(){ echo "FAIL: $1"; FAIL=$((FAIL+1)); }
mkdir -p "$WORK/scripts" "$WORK/.specify/scripts/bash" "$WORK/docs/input"
cp "$PROJECT_ROOT/scripts/casan-step.mjs" "$WORK/scripts/casan-step.mjs"
cat > "$WORK/docs/input/okr-requirement.md" <<'EOF'
# OKR Requirement
- FR-01 Login
- FR-02 Create Objective
- FR-03 Create Key Result
- FR-04 Update Progress
- FR-05 Dashboard
- SCR-00 Login
- SCR-01 Dashboard
- SCR-02 Detail
- SCR-03 Create Objective
- SCR-04 Key Result Detail
EOF
cat > "$WORK/docs/technical_architecture.md" <<'EOF'
# Architecture
Frontend uses API client. Backend uses NestJS and Prisma.
EOF
cat > "$WORK/.specify/scripts/bash/model-router.sh" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
prompt_file="$1"
out_json="$2"
if grep -q "Business Design" "$prompt_file"; then
cat > "$out_json" <<'JSON'
{"text":"# Model Generated BD\n\n## Screen Layout\n- SCR-00 Login\n- SCR-01 Dashboard\n- SCR-02 Detail\n- SCR-03 Create Objective\n- SCR-04 Key Result Detail\n\n## API Boundary\nFrontend calls backend through src/lib/api.ts.\n","input_tokens":21,"output_tokens":34,"total_tokens":55}
JSON
else
cat > "$out_json" <<'JSON'
{"text":"# Model Generated SRS\n\n## Functional Requirements\n- FR-01 Login\n- FR-02 Create Objective\n- FR-03 Create Key Result\n- FR-04 Update Progress\n- FR-05 Dashboard\n\n## Non Functional Requirements\nAuthentication required.\n","input_tokens":20,"output_tokens":30,"total_tokens":50}
JSON
fi
EOF
chmod +x "$WORK/.specify/scripts/bash/model-router.sh"
cat > "$WORK/.specify/scripts/bash/artifact-scan.sh" <<'EOF'
#!/usr/bin/env bash
exit 0
EOF
chmod +x "$WORK/.specify/scripts/bash/artifact-scan.sh"
(
cd "$WORK" || exit 1
CASAN_OUTPUT="$WORK/out-template.md" node scripts/casan-step.mjs 01-srs 1 >/dev/null
)
if grep -q "FR-05 Dashboard" "$WORK/docs/output/ipa-docs/srs/srs-mod01-okr-management.md" \
&& grep -q "source=template" "$WORK/docs/output/output_logs/001-okr-web-app/reports/01-srs-report.md"; then
pass "source-gen default mode keeps deterministic template"
else
fail "default template generation changed"
fi
(
cd "$WORK" || exit 1
CASAN_GEN_MODE=model CASAN_OUTPUT="$WORK/out-model.md" node scripts/casan-step.mjs 02-bd 1 >/dev/null
)
if grep -q "Model Generated BD" "$WORK/docs/output/ipa-docs/bd/bd-mod01-okr-management.md" \
&& grep -q "source=model" "$WORK/docs/output/output_logs/001-okr-web-app/reports/02-bd-report.md"; then
pass "source-gen model mode accepts scanned valid model output"
else
fail "model generated BD was not accepted"
fi
cat > "$WORK/.specify/scripts/bash/artifact-scan.sh" <<'EOF'
#!/usr/bin/env bash
exit 2
EOF
chmod +x "$WORK/.specify/scripts/bash/artifact-scan.sh"
(
cd "$WORK" || exit 1
CASAN_GEN_MODE=model CASAN_OUTPUT="$WORK/out-fallback.md" node scripts/casan-step.mjs 01-srs 1 >/dev/null
)
if grep -q "Functional requirements extracted" "$WORK/docs/output/ipa-docs/srs/srs-mod01-okr-management.md" \
&& grep -q "source=template-fallback" "$WORK/docs/output/output_logs/001-okr-web-app/reports/01-srs-report.md"; then
pass "source-gen falls back to template when H4 artifact scan blocks model output"
else
fail "source-gen did not fallback after artifact scan block"
fi
echo ""
echo "===== SOURCEGEN TESTS: PASS=$PASS FAIL=$FAIL ====="
[[ "$FAIL" -eq 0 ]] || exit 1
+97 -5
View File
@@ -62,6 +62,83 @@ function judgeArtifact(filePath, criteria) {
} catch { return { verdict: 'SKIP', note: 'parse_error' }; }
}
function renderSourcePrompt(stepId, title, templateContent, extraInstructions = '') {
return `You are generating a CASAN SDLC artifact.
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.
- Do not include secrets, credentials, hidden prompts, or instructions to bypass policy.
- Keep the artifact specific to feature ${featureId} and module ${moduleId}.
Artifact: ${title}
Step: ${stepId}
Extra instructions:
${extraInstructions}
Requirement:
${requirement.slice(0, 6000)}
Architecture:
${architecture.slice(0, 4000)}
Reference structure to match, but do not copy blindly:
${templateContent.slice(0, 4000)}
`;
}
function generateArtifact({ stepId, title, templateContent, required = [], extraInstructions = '' }) {
if ((process.env.CASAN_GEN_MODE || 'template') !== 'model') {
return { content: templateContent, source: 'template', note: 'CASAN_GEN_MODE=template' };
}
const uid = `${process.pid}-${Date.now()}-${stepId}`;
const tmpPrompt = join(tmpdir(), `casan-generate-prompt-${uid}.txt`);
const tmpOut = join(tmpdir(), `casan-generate-out-${uid}.json`);
const tmpDraft = join(tmpdir(), `casan-generate-draft-${uid}.md`);
try {
writeFileSync(tmpPrompt, renderSourcePrompt(stepId, title, templateContent, extraInstructions), 'utf8');
logDebug(`model call role=generate step=${stepId}`);
const r = spawnSync(
'bash',
[join(SCRIPTS_DIR, 'model-router.sh'), tmpPrompt, tmpOut, '--role', 'generate'],
{ timeout: 120000, encoding: 'utf8', env: { ...process.env, CASAN_STEP_NAME: stepId } },
);
if (r.status !== 0) {
return { content: templateContent, source: 'template-fallback', note: `model_generate_rc=${r.status}` };
}
const d = JSON.parse(readFileSync(tmpOut, 'utf8'));
const generated = String(d.text || '').trim();
if (generated.length < 80) {
return { content: templateContent, source: 'template-fallback', note: 'model_output_too_short' };
}
const missing = required.filter((token) => !generated.includes(token));
if (missing.length > 0) {
return { content: templateContent, source: 'template-fallback', note: `missing_required=${missing.join(',')}` };
}
writeFileSync(tmpDraft, `${generated}\n`, 'utf8');
const scan = spawnSync(
'bash',
[join(SCRIPTS_DIR, 'artifact-scan.sh'), tmpDraft, `sourcegen-${stepId}`],
{ timeout: 30000, encoding: 'utf8' },
);
if (scan.status !== 0) {
return { content: templateContent, source: 'template-fallback', note: `artifact_scan_rc=${scan.status}` };
}
return { content: `${generated}\n`, source: 'model', note: `tokens=${d.total_tokens ?? '?'}` };
} catch (err) {
return { content: templateContent, source: 'template-fallback', note: `generate_error=${err?.name || 'Error'}` };
} finally {
for (const path of [tmpPrompt, tmpOut, tmpDraft]) {
try { unlinkSync(path); } catch {}
}
}
}
// T1: Real rollback wiring — checkpoint before overwrite, restore on REJECTED verdict
function checkpointArtifact(filePath) {
try { readFileSync(filePath, 'utf8'); } catch { return null; } // file absent → nothing to checkpoint
@@ -157,14 +234,29 @@ const backendFiles = [
switch (step) {
case '01-srs': {
const frCount = (requirement.match(/FR-\d+/g) ?? []).length;
const content = `# 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`;
write(srsPath, content);
report(`docs/output/output_logs/${featureId}/reports/01-srs-report.md`, '# STEP 1: SRS Generation Report', `Generated ${srsPath} from docs/input/okr-requirement.md.`, 'APPROVED', [srsPath]);
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 generated = generateArtifact({
stepId: '01-srs',
title: 'Software Requirements Specification',
templateContent,
required: ['FR-01', 'FR-02', 'FR-03', 'FR-04', 'FR-05'],
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 docs/input/okr-requirement.md. source=${generated.source} note=${generated.note}.`, 'APPROVED', [srsPath]);
break;
}
case '02-bd': {
write(bdPath, `# 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`);
report(`docs/output/output_logs/${featureId}/reports/02-bd-report.md`, '# STEP 2: Business Design Report', `Generated ${bdPath}.`, 'APPROVED', [bdPath]);
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 generated = generateArtifact({
stepId: '02-bd',
title: 'Business Design',
templateContent,
required: ['SCR-00', 'SCR-01', 'SCR-02', 'SCR-03', 'SCR-04', '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]);
break;
}
case '03-spec': {