update first - 84
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { dirname } from 'node:path';
|
||||
|
||||
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';
|
||||
|
||||
if (!outputPath) {
|
||||
throw new Error('CASAN_OUTPUT is required');
|
||||
}
|
||||
|
||||
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`,
|
||||
];
|
||||
dirs.forEach((dir) => mkdirSync(dir, { recursive: true }));
|
||||
|
||||
const requirement = readFileSync('docs/input/okr-requirement.md', 'utf8');
|
||||
const architecture = readFileSync('docs/technical_architecture.md', 'utf8');
|
||||
|
||||
function write(path, content) {
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
writeFileSync(path, content, 'utf8');
|
||||
return path;
|
||||
}
|
||||
|
||||
function resultBlock({ status = 'COMPLETE', verdict = 'APPROVED', artifacts = [], issues = [] }) {
|
||||
return `\n<!-- STEP-RESULT\nstatus: ${status}\nverdict: ${verdict}\nartifacts:\n${artifacts.map((artifact) => ` - ${artifact}`).join('\n')}\ncritical-issues:\n${issues.length === 0 ? ' - none' : issues.map((issue) => ` - ${issue}`).join('\n')}\n/STEP-RESULT -->\n`;
|
||||
}
|
||||
|
||||
function report(path, title, body, verdict = 'APPROVED', artifacts = [], issues = []) {
|
||||
finalTitle = title;
|
||||
finalBody = body;
|
||||
finalVerdict = verdict;
|
||||
finalArtifacts = artifacts;
|
||||
finalIssues = 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 codeReviewPath = `docs/output/output_logs/${featureId}/reports/11-review-code-report.md`;
|
||||
let finalTitle = `# ${step}`;
|
||||
let finalBody = '';
|
||||
let finalVerdict = 'APPROVED';
|
||||
let finalArtifacts = [];
|
||||
let finalIssues = [];
|
||||
|
||||
const backendFiles = [
|
||||
'backend/src/auth/auth.service.ts',
|
||||
'backend/src/objectives/objectives.service.ts',
|
||||
'backend/src/key-results/key-results.service.ts',
|
||||
'backend/test/e2e.test.ts',
|
||||
'backend/test/services.test.ts',
|
||||
];
|
||||
|
||||
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]);
|
||||
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]);
|
||||
break;
|
||||
}
|
||||
case '03-spec': {
|
||||
write(specPath, `# 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## Source Trace\nRequirement characters read: ${requirement.length}. Architecture characters read: ${architecture.length}.\n`);
|
||||
report(`docs/output/output_logs/${featureId}/reports/03-spec-report.md`, '# STEP 3: Specify Report', `Generated ${specPath}.`, '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 verdict = missing.length === 0 ? 'APPROVED' : 'REJECTED';
|
||||
report(
|
||||
`docs/output/output_logs/${featureId}/reports/04-review-spec-report.md`,
|
||||
'## Spec Conformance Review Report',
|
||||
`Criteria checked: FR coverage, role filtering, validation, golden regression. Missing: ${missing.join(', ') || 'none'}.`,
|
||||
verdict,
|
||||
[specPath],
|
||||
missing,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case '05-plan': {
|
||||
const incomplete = attempt === '1';
|
||||
write(planPath, `# 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 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`);
|
||||
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');
|
||||
}
|
||||
report(`docs/output/output_logs/${featureId}/reports/05-plan-report-attempt-${attempt}.md`, `# STEP 5: Plan Report Attempt ${attempt}`, `Generated ${planPath}.`, 'APPROVED', [planPath]);
|
||||
break;
|
||||
}
|
||||
case '06-reviewplan': {
|
||||
const plan = readFileSync(planPath, 'utf8');
|
||||
const required = ['Golden regression test', 'Rollback strategy', 'Backend Modules', 'Tests'];
|
||||
const missing = required.filter((phrase) => !plan.includes(phrase));
|
||||
const artifactMissing = [dataModelPath, researchPath, quickstartPath, contractPath].filter((path) => {
|
||||
try {
|
||||
readFileSync(path, 'utf8');
|
||||
return false;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
const issues = [...missing.map((item) => `missing plan criterion: ${item}`), ...artifactMissing.map((item) => `missing artifact: ${item}`)];
|
||||
const verdict = issues.length === 0 ? 'APPROVED' : 'REJECTED';
|
||||
report(
|
||||
`docs/output/output_logs/${featureId}/reports/06-review-plan-report-attempt-${attempt}.md`,
|
||||
`## Plan Conformance Review Report — Attempt ${attempt}`,
|
||||
`Criteria checked against plan.md and required companion artifacts. Verdict is ${verdict}.`,
|
||||
verdict,
|
||||
[planPath, dataModelPath, researchPath, quickstartPath, contractPath],
|
||||
issues,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case '07-dd': {
|
||||
write(ddPath, `# 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`);
|
||||
report(`docs/output/output_logs/${featureId}/reports/07-dd-report.md`, '# STEP 7: Detail Design Report', `Generated ${ddPath}.`, 'APPROVED', [ddPath]);
|
||||
break;
|
||||
}
|
||||
case '08-testkit': {
|
||||
write(testcasePath, `# 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`);
|
||||
report(`docs/output/output_logs/${featureId}/reports/08-testkit-report.md`, '# STEP 8: Testkit Report', `Generated ${testcasePath}.`, 'APPROVED', [testcasePath]);
|
||||
break;
|
||||
}
|
||||
case '09-tasks': {
|
||||
write(tasksPath, `# 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`);
|
||||
report(`docs/output/output_logs/${featureId}/reports/09-tasks-report.md`, '# STEP 9: Tasks Report', `Generated ${tasksPath}.`, 'APPROVED', [tasksPath]);
|
||||
break;
|
||||
}
|
||||
case '10-reviewcode': {
|
||||
const missingFiles = backendFiles.filter((path) => {
|
||||
try {
|
||||
readFileSync(path, 'utf8');
|
||||
return false;
|
||||
} catch {
|
||||
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 (!serviceText.includes('golden objective list response does not drift')) issues.push('golden test not found');
|
||||
issues.push(...missingFiles.map((path) => `missing file: ${path}`));
|
||||
const verdict = issues.length === 0 ? 'APPROVED' : 'REJECTED';
|
||||
report(
|
||||
codeReviewPath,
|
||||
'# STEP 10: Code Review Report',
|
||||
`Reviewed ${backendFiles.length} backend/test files. DB data usage verification: Prisma Client used, frontend API client used, seed data exists, no static endpoint data found.`,
|
||||
verdict,
|
||||
[...backendFiles, 'frontend/src/lib/api.ts', 'backend/prisma/seed.ts'],
|
||||
issues,
|
||||
);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown step: ${step}`);
|
||||
}
|
||||
|
||||
writeFileSync(
|
||||
outputPath,
|
||||
`${finalTitle}\n\n${finalBody}\n\nGenerated by ${step} attempt ${attempt} for ${featureId}.\n${resultBlock({
|
||||
verdict: finalVerdict,
|
||||
artifacts: finalArtifacts,
|
||||
issues: finalIssues,
|
||||
})}`,
|
||||
'utf8',
|
||||
);
|
||||
@@ -0,0 +1,158 @@
|
||||
import { execFileSync, execSync } from 'node:child_process';
|
||||
import { copyFileSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
||||
import { basename, join } from 'node:path';
|
||||
|
||||
const featureId = '001-okr-web-app';
|
||||
const root = process.cwd();
|
||||
const logDir = `docs/output/output_logs/${featureId}`;
|
||||
const casanDir = `${logDir}/casan`;
|
||||
const reportsDir = `${logDir}/reports`;
|
||||
const contextPath = `${logDir}/pipeline-context.yaml`;
|
||||
const bossLog = `${logDir}/00-boss.log.md`;
|
||||
|
||||
mkdirSync(casanDir, { recursive: true });
|
||||
mkdirSync(reportsDir, { recursive: true });
|
||||
writeFileSync(
|
||||
contextPath,
|
||||
`feature-id: ${featureId}\nmodule-id: MOD-01\nmodule-keyword: okr-management\ntech-stack: NestJS + Prisma + SQLite + React + Vite + Tailwind\nsteps:\n`,
|
||||
'utf8',
|
||||
);
|
||||
writeFileSync(bossLog, `# Boss Log ${featureId}\n\n`, 'utf8');
|
||||
|
||||
function appendBoss(line) {
|
||||
const ts = new Date().toISOString();
|
||||
writeFileSync(bossLog, `${readFileSync(bossLog, 'utf8')}- ${ts} ${line}\n`, 'utf8');
|
||||
}
|
||||
|
||||
function latestAgentTrace(stepName) {
|
||||
const traceDir = '.specify/logs/trace';
|
||||
const files = readdirSync(traceDir)
|
||||
.filter((file) => file.startsWith('agentops-') && file.endsWith('.json'))
|
||||
.map((file) => join(traceDir, file))
|
||||
.sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
|
||||
for (const file of files) {
|
||||
const record = JSON.parse(readFileSync(file, 'utf8'));
|
||||
if (record.step === stepName) {
|
||||
return { traceId: record.trace_id, path: file, status: record.status };
|
||||
}
|
||||
}
|
||||
throw new Error(`No trace found for ${stepName}`);
|
||||
}
|
||||
|
||||
function verdictFromOutput(path) {
|
||||
const text = readFileSync(path, 'utf8');
|
||||
const match = text.match(/verdict:\s*([A-Z_]+)/);
|
||||
return match?.[1] ?? 'UNKNOWN';
|
||||
}
|
||||
|
||||
function appendContext({ id, agent, output, trace }) {
|
||||
const verdict = verdictFromOutput(output);
|
||||
const existing = readFileSync(contextPath, 'utf8');
|
||||
writeFileSync(
|
||||
contextPath,
|
||||
`${existing} - id: ${id}\n agent: ${agent}\n artifact: ${output}\n verdict: ${verdict}\n trace_id: ${trace.traceId}\n trace_file: ${trace.path}\n status: ${trace.status}\n`,
|
||||
'utf8',
|
||||
);
|
||||
}
|
||||
|
||||
function runHarness({ id, agent, step, attempt = '1' }) {
|
||||
const input = `${casanDir}/${id}-input.txt`;
|
||||
const output = `${casanDir}/${id}-output.md`;
|
||||
const payload = `feature ${featureId}\nstep ${id}\nagent ${agent}\nattempt ${attempt}\nsource docs/input/okr-requirement.md\n`;
|
||||
writeFileSync(input, payload, 'utf8');
|
||||
appendBoss(`START ${id} ${agent} attempt ${attempt}`);
|
||||
execFileSync(
|
||||
'.specify/scripts/bash/casan-harness.sh',
|
||||
[input, output, `agent_step_${id}`, '--', 'node', 'scripts/casan-step.mjs', step, attempt],
|
||||
{
|
||||
cwd: root,
|
||||
stdio: 'inherit',
|
||||
env: {
|
||||
...process.env,
|
||||
CASAN_AGENT: agent,
|
||||
CASAN_AGENT_NAME: agent,
|
||||
CASAN_STEP_NAME: id,
|
||||
},
|
||||
},
|
||||
);
|
||||
const trace = latestAgentTrace(id);
|
||||
appendContext({ id, agent, output, trace });
|
||||
appendBoss(`END ${id} verdict ${verdictFromOutput(output)} trace ${trace.traceId}`);
|
||||
return { output, verdict: verdictFromOutput(output), trace };
|
||||
}
|
||||
|
||||
const sequence = [
|
||||
{ id: '01-srs', agent: 'okr.srs', step: '01-srs' },
|
||||
{ id: '02-bd', agent: 'okr.bd', step: '02-bd' },
|
||||
{ id: '03-spec', agent: 'speckit.specify', step: '03-spec' },
|
||||
{ id: '04-reviewspec', agent: 'okr.reviewspec', step: '04-reviewspec' },
|
||||
{ id: '05-plan-attempt-1', agent: 'speckit.plan', step: '05-plan', attempt: '1' },
|
||||
{ id: '06-reviewplan-attempt-1', agent: 'okr.reviewplan', step: '06-reviewplan', attempt: '1' },
|
||||
];
|
||||
|
||||
for (const item of sequence) {
|
||||
runHarness(item);
|
||||
}
|
||||
|
||||
appendBoss('BACK-TO-PLAN triggered by reviewplan rejection; retrying plan with missing criteria fixed.');
|
||||
runHarness({ id: '07-plan-attempt-2', agent: 'speckit.plan', step: '05-plan', attempt: '2' });
|
||||
|
||||
const fallbackOut = `${casanDir}/model-fallback-output.txt`;
|
||||
execFileSync(
|
||||
'.specify/scripts/bash/model-fallback.sh',
|
||||
[
|
||||
fallbackOut,
|
||||
'--primary',
|
||||
'bash -c "exit 9"',
|
||||
'--fallback',
|
||||
'printf "Generate a safe OKR plan for employee ***MASKED_EMAIL***.\\nExpected sections:\\n- Objective\\n- Key Results\\n- Security gate\\n- Governance decision\\n- AgentOps metrics\\n"',
|
||||
],
|
||||
{ cwd: root, stdio: 'inherit' },
|
||||
);
|
||||
appendBoss(`Model fallback invoked; output ${fallbackOut}`);
|
||||
|
||||
const driftCandidate = `${casanDir}/drift-plan-candidate.txt`;
|
||||
copyFileSync(fallbackOut, driftCandidate);
|
||||
execFileSync('.specify/scripts/bash/drift-detect.sh', [
|
||||
'.specify/level5/golden-runs/okr-plan.golden.txt',
|
||||
driftCandidate,
|
||||
'.specify/logs/level5/okr-plan-drift-report.json',
|
||||
], { cwd: root, stdio: 'inherit' });
|
||||
appendBoss('Drift detection invoked after fixed plan.');
|
||||
|
||||
runHarness({ id: '08-reviewplan-attempt-2', agent: 'okr.reviewplan', step: '06-reviewplan', attempt: '2' });
|
||||
runHarness({ id: '09-dd', agent: 'okr.dd', step: '07-dd' });
|
||||
runHarness({ id: '10-testkit', agent: 'okr.testkit', step: '08-testkit' });
|
||||
runHarness({ id: '11-tasks', agent: 'speckit.tasks', step: '09-tasks' });
|
||||
runHarness({ id: '12-reviewcode', agent: 'okr.reviewcode', step: '10-reviewcode' });
|
||||
|
||||
const rollbackDir = 'docs/output/casan/app-evidence';
|
||||
mkdirSync(rollbackDir, { recursive: true });
|
||||
const rollbackTarget = `${rollbackDir}/rollback-target.txt`;
|
||||
const rollbackBackup = `${rollbackDir}/rollback-target.backup.txt`;
|
||||
writeFileSync(rollbackTarget, 'original pipeline rollback content\n', 'utf8');
|
||||
copyFileSync(rollbackTarget, rollbackBackup);
|
||||
writeFileSync(`${rollbackDir}/rollback-before.txt`, readFileSync(rollbackTarget, 'utf8'), 'utf8');
|
||||
writeFileSync(rollbackTarget, 'changed content that must be undone\n', 'utf8');
|
||||
writeFileSync(`${rollbackDir}/rollback-changed.txt`, readFileSync(rollbackTarget, 'utf8'), 'utf8');
|
||||
const record = execFileSync('.specify/scripts/bash/rollback-manager.sh', [
|
||||
'record',
|
||||
'restore rollback target evidence file',
|
||||
`cp ${rollbackBackup} ${rollbackTarget}`,
|
||||
], { cwd: root, encoding: 'utf8' });
|
||||
writeFileSync(`${rollbackDir}/rollback-record.stdout`, record, 'utf8');
|
||||
const tx = record.match(/transaction_id=([^\s]+)/)?.[1];
|
||||
if (!tx) {
|
||||
throw new Error('rollback transaction id not found');
|
||||
}
|
||||
const execute = execFileSync('.specify/scripts/bash/rollback-manager.sh', ['execute', tx], {
|
||||
cwd: root,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
writeFileSync(`${rollbackDir}/rollback-execute.stdout`, execute, 'utf8');
|
||||
writeFileSync(`${rollbackDir}/rollback-after.txt`, readFileSync(rollbackTarget, 'utf8'), 'utf8');
|
||||
appendBoss(`Rollback transaction ${tx} executed; before/changed/after evidence captured.`);
|
||||
|
||||
const summary = `Pipeline complete. Context: ${contextPath}. Boss log: ${bossLog}. Last step artifacts under ${reportsDir}.\n`;
|
||||
writeFileSync(`${rollbackDir}/pipeline-summary.txt`, summary, 'utf8');
|
||||
console.log(summary);
|
||||
Reference in New Issue
Block a user