Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681
  1. ---
  2. name: 'step-05-gate-decision'
  3. description: 'Phase 2: Apply gate decision logic and generate outputs'
  4. outputFile: '{test_artifacts}/traceability-matrix.md'
  5. ---
  6. # Step 5: Phase 2 - Gate Decision
  7. ## STEP GOAL
  8. **Phase 2:** Read coverage matrix from Phase 1, apply deterministic gate decision logic when gate-eligible, and generate the traceability report plus machine-readable outputs.
  9. ---
  10. ## MANDATORY EXECUTION RULES
  11. - 📖 Read the entire step file before acting
  12. - ✅ Speak in `{communication_language}`
  13. - ✅ Read coverage matrix from Phase 1 temp file
  14. - ✅ Resolve collection status and gate eligibility before applying gate decision logic
  15. - ❌ Do NOT regenerate coverage matrix (use Phase 1 output)
  16. ---
  17. ## EXECUTION PROTOCOLS:
  18. - 🎯 Follow the MANDATORY SEQUENCE exactly
  19. - 💾 Record outputs before proceeding
  20. - 📖 This is the FINAL step
  21. ## CONTEXT BOUNDARIES:
  22. - Available context: Coverage matrix from Phase 1 temp file
  23. - Focus: gate decision logic only
  24. - Dependencies: Phase 1 complete (coverage matrix exists)
  25. ---
  26. ## MANDATORY SEQUENCE
  27. ### 1. Read Phase 1 Coverage Matrix
  28. Read `{outputFile}` frontmatter for `tempCoverageMatrixPath`. Halt when missing — the fallback timestamp cannot be reconstructed reliably in a different execution context:
  29. ```javascript
  30. const progressDoc = fs.readFileSync('{outputFile}', 'utf8');
  31. const frontmatterMatch = progressDoc.match(/^---\n([\s\S]*?)\n---/);
  32. const frontmatter = frontmatterMatch ? yaml.parse(frontmatterMatch[1]) : {};
  33. const matrixPath = frontmatter.tempCoverageMatrixPath;
  34. if (!matrixPath) {
  35. throw new Error(
  36. '❌ tempCoverageMatrixPath not found in progress frontmatter. ' +
  37. 'Step 4 must record the resolved temp file path before Step 5 can proceed.',
  38. );
  39. }
  40. const coverageMatrix = JSON.parse(fs.readFileSync(matrixPath, 'utf8'));
  41. console.log('✅ Phase 1 coverage matrix loaded');
  42. ```
  43. **Verify Phase 1 complete:**
  44. ```javascript
  45. if (coverageMatrix.phase !== 'PHASE_1_COMPLETE') {
  46. throw new Error('Phase 1 not complete - cannot proceed to gate decision');
  47. }
  48. ```
  49. ---
  50. ### 2. Apply Gate Decision Logic
  51. **Decision Tree:**
  52. ```javascript
  53. const stats = coverageMatrix.coverage_statistics;
  54. if (
  55. !stats ||
  56. typeof stats !== 'object' ||
  57. !stats.priority_breakdown ||
  58. !stats.priority_breakdown.P0 ||
  59. !stats.priority_breakdown.P1 ||
  60. !stats.priority_breakdown.P2 ||
  61. !stats.priority_breakdown.P3
  62. ) {
  63. throw new Error(
  64. 'Phase 1 coverage_statistics.priority_breakdown is missing or incomplete. ' +
  65. 'Step 4 must emit P0-P3 totals and coverage percentages before Step 5 can proceed.',
  66. );
  67. }
  68. const priorityBreakdown = stats.priority_breakdown;
  69. const p0Coverage = priorityBreakdown.P0.percentage;
  70. const p1Coverage = priorityBreakdown.P1.percentage;
  71. const hasP1Requirements = (priorityBreakdown.P1.total || 0) > 0;
  72. const effectiveP1Coverage = hasP1Requirements ? p1Coverage : 100;
  73. const overallCoverage = stats.overall_coverage_percentage;
  74. const criticalGaps = (coverageMatrix.gap_analysis?.critical_gaps || []).length;
  75. const isUnresolved = (value) => typeof value === 'string' && value.startsWith('{') && value.endsWith('}');
  76. const normalizeResolvedToken = (value) => {
  77. if (value === undefined || value === null) return null;
  78. const normalized = String(value).trim().toLowerCase();
  79. if (!normalized || normalized === 'auto' || isUnresolved(normalized)) return null;
  80. return normalized;
  81. };
  82. const oracleResolutionMode = normalizeResolvedToken(coverageMatrix.oracle?.resolution_mode) || 'formal_requirements';
  83. const coverageBasis =
  84. normalizeResolvedToken(coverageMatrix.coverage_basis) ||
  85. {
  86. formal_requirements: 'acceptance_criteria',
  87. spec_artifact: 'openapi_endpoints',
  88. external_pointer: 'acceptance_criteria',
  89. synthetic_source: 'user_journeys',
  90. }[oracleResolutionMode] ||
  91. 'acceptance_criteria';
  92. const oracleConfidence =
  93. normalizeResolvedToken(coverageMatrix.oracle?.confidence || coverageMatrix.summary_confidence) ||
  94. {
  95. formal_requirements: 'high',
  96. spec_artifact: 'high',
  97. external_pointer: 'medium',
  98. synthetic_source: 'medium',
  99. }[oracleResolutionMode] ||
  100. 'medium';
  101. const syntheticOracle = coverageMatrix.oracle?.synthetic === true || ['synthetic_requirements', 'user_journeys'].includes(coverageBasis);
  102. const deriveActiveTestCasesFromRequirements = (requirements) => {
  103. const uniqueTests = new Map();
  104. (requirements || []).forEach((req) => {
  105. (req.tests || []).forEach((test) => {
  106. const stableId =
  107. test.id ||
  108. [test.file, test.title || test.name, test.line]
  109. .filter((value) => value !== undefined && value !== null && value !== '')
  110. .join(':') ||
  111. null;
  112. if (stableId === null || uniqueTests.has(stableId)) return;
  113. const explicitStatus = String(test.status || '')
  114. .trim()
  115. .toLowerCase();
  116. const status = ['skipped', 'pending', 'fixme'].includes(explicitStatus)
  117. ? explicitStatus
  118. : test.fixme === true
  119. ? 'fixme'
  120. : test.pending === true
  121. ? 'pending'
  122. : test.skipped === true
  123. ? 'skipped'
  124. : 'active';
  125. uniqueTests.set(stableId, status);
  126. });
  127. });
  128. return [...uniqueTests.values()].filter((status) => status === 'active').length;
  129. };
  130. const summarizedTestInventory = coverageMatrix.test_inventory?.summary || null;
  131. const activeTestCases =
  132. summarizedTestInventory === null
  133. ? deriveActiveTestCasesFromRequirements(coverageMatrix.requirements)
  134. : Math.max(
  135. 0,
  136. (summarizedTestInventory.cases || 0) -
  137. (summarizedTestInventory.skipped_cases || 0) -
  138. (summarizedTestInventory.fixme_cases || 0) -
  139. (summarizedTestInventory.pending_cases || 0),
  140. );
  141. let effectiveOracleConfidence = oracleConfidence;
  142. if (effectiveOracleConfidence === 'high' && activeTestCases === 0) {
  143. effectiveOracleConfidence = 'medium';
  144. }
  145. const normalizeBoolean = (value, defaultValue = true) => {
  146. if (typeof value === 'string') {
  147. const normalized = value.trim().toLowerCase();
  148. if (['false', '0', 'off', 'no'].includes(normalized)) return false;
  149. if (['true', '1', 'on', 'yes'].includes(normalized)) return true;
  150. }
  151. if (value === undefined || value === null) return defaultValue;
  152. return Boolean(value);
  153. };
  154. const collectionMode = String(!isUnresolved(coverageMatrix.collection_mode) ? coverageMatrix.collection_mode : 'contract_static')
  155. .trim()
  156. .toLowerCase();
  157. const rawAllowGate = !isUnresolved(coverageMatrix.allow_gate) ? coverageMatrix.allow_gate : true;
  158. const allowGate = normalizeBoolean(rawAllowGate, true);
  159. const rawCollectionStatus =
  160. coverageMatrix.collection_status ||
  161. {
  162. waived: 'WAIVED',
  163. restricted: 'RESTRICTED',
  164. inaccessible: 'INACCESSIBLE',
  165. deferred_shared: 'DEFERRED_SHARED',
  166. }[collectionMode] ||
  167. 'COLLECTED';
  168. // Normalize to UPPER_CASE + trimmed so comparisons are whitespace/case-safe.
  169. const collectionStatus = String(rawCollectionStatus).trim().toUpperCase();
  170. const gateEligible = allowGate && collectionStatus === 'COLLECTED';
  171. let gateDecision = 'NOT_EVALUATED'; // default; overwritten when gateEligible
  172. let rationale;
  173. if (!gateEligible) {
  174. rationale = `Gate decision skipped because allow_gate=${allowGate} and collection_status=${collectionStatus}.`;
  175. } else {
  176. // Rule 1: P0 coverage must be 100%
  177. if (p0Coverage < 100) {
  178. gateDecision = 'FAIL';
  179. rationale = `P0 coverage is ${p0Coverage}% (required: 100%). ${criticalGaps} critical requirements uncovered.`;
  180. }
  181. // Rule 2: Overall coverage must be >= 80%
  182. else if (overallCoverage < 80) {
  183. gateDecision = 'FAIL';
  184. rationale = `Overall coverage is ${overallCoverage}% (minimum: 80%). Significant gaps exist.`;
  185. }
  186. // Rule 3: P1 coverage < 80% → FAIL
  187. else if (effectiveP1Coverage < 80) {
  188. gateDecision = 'FAIL';
  189. rationale = hasP1Requirements
  190. ? `P1 coverage is ${effectiveP1Coverage}% (minimum: 80%). High-priority gaps must be addressed.`
  191. : `P1 requirements are not present; continuing with remaining gate criteria.`;
  192. }
  193. // Rule 4: P1 coverage >= 90% and overall >= 80% with P0 at 100% → PASS
  194. else if (effectiveP1Coverage >= 90) {
  195. gateDecision = 'PASS';
  196. rationale = hasP1Requirements
  197. ? `P0 coverage is 100%, P1 coverage is ${effectiveP1Coverage}% (target: 90%), and overall coverage is ${overallCoverage}% (minimum: 80%).`
  198. : `P0 coverage is 100% and overall coverage is ${overallCoverage}% (minimum: 80%). No P1 requirements detected.`;
  199. }
  200. // Rule 5: P1 coverage 80-89% with P0 at 100% and overall >= 80% → CONCERNS
  201. else if (effectiveP1Coverage >= 80) {
  202. gateDecision = 'CONCERNS';
  203. rationale = hasP1Requirements
  204. ? `P0 coverage is 100% and overall coverage is ${overallCoverage}% (minimum: 80%), but P1 coverage is ${effectiveP1Coverage}% (target: 90%).`
  205. : `P0 coverage is 100% and overall coverage is ${overallCoverage}% (minimum: 80%), but additional non-P1 gaps need mitigation.`;
  206. }
  207. // Rule 6: Manual waiver — set gateDecision = 'WAIVED' and update rationale here
  208. // if a stakeholder-approved waiver applies (wired through config or user input upstream).
  209. // Oracle confidence overlay
  210. if (syntheticOracle && gateDecision === 'PASS' && effectiveOracleConfidence !== 'high') {
  211. gateDecision = 'CONCERNS';
  212. rationale =
  213. `Coverage traced against inferred ${coverageBasis.replace('_', ' ')} with ${effectiveOracleConfidence} confidence. ` +
  214. `Base coverage meets PASS thresholds, but confidence is not high enough for an unconditional PASS.`;
  215. } else if (syntheticOracle && effectiveOracleConfidence === 'low' && gateDecision === 'NOT_EVALUATED') {
  216. gateDecision = 'CONCERNS';
  217. rationale =
  218. `Coverage traced against inferred ${coverageBasis.replace('_', ' ')} with low confidence. ` +
  219. `Treat this result as advisory until the inferred journeys are confirmed or formalized.`;
  220. }
  221. }
  222. ```
  223. ---
  224. ### 3. Generate Gate Report
  225. ```javascript
  226. const gateReport = {
  227. gate_eligible: gateEligible,
  228. collection_status: collectionStatus,
  229. decision: gateEligible ? gateDecision : 'NOT_EVALUATED',
  230. rationale: rationale,
  231. decision_date: new Date().toISOString(),
  232. coverage_matrix: coverageMatrix,
  233. gate_criteria: gateEligible
  234. ? {
  235. p0_coverage_required: '100%',
  236. p0_coverage_actual: `${p0Coverage}%`,
  237. p0_status: p0Coverage === 100 ? 'MET' : 'NOT_MET',
  238. p1_coverage_target: '90%',
  239. p1_coverage_minimum: '80%',
  240. p1_coverage_actual: `${effectiveP1Coverage}%`,
  241. p1_status: effectiveP1Coverage >= 90 ? 'MET' : effectiveP1Coverage >= 80 ? 'PARTIAL' : 'NOT_MET',
  242. overall_coverage_minimum: '80%',
  243. overall_coverage_actual: `${overallCoverage}%`,
  244. overall_status: overallCoverage >= 80 ? 'MET' : 'NOT_MET',
  245. }
  246. : null,
  247. uncovered_requirements: (coverageMatrix.gap_analysis?.critical_gaps || []).concat(coverageMatrix.gap_analysis?.high_gaps || []),
  248. recommendations: coverageMatrix.recommendations,
  249. };
  250. ```
  251. ---
  252. ### 3b. Emit `e2e-trace-summary.json`
  253. **After the gate report is assembled, write the machine-readable summary to `{e2e_trace_summary_output}`.**
  254. This file is the portable, automation-friendly companion to the markdown report. Any CI/CD pipeline, reporting dashboard, or LLM agent can consume it without parsing markdown.
  255. ```javascript
  256. const buildFallbackInventory = () => {
  257. const byLevel = {
  258. e2e: { tests: 0, criteria_covered: 0 },
  259. api: { tests: 0, criteria_covered: 0 },
  260. component: { tests: 0, criteria_covered: 0 },
  261. unit: { tests: 0, criteria_covered: 0 },
  262. other: { tests: 0, criteria_covered: 0 }, // captures tests with unrecognized or empty level
  263. };
  264. const coverageEligibleStatuses = new Set(['FULL', 'PARTIAL', 'UNIT-ONLY', 'INTEGRATION-ONLY']);
  265. const uniqueTests = new Map();
  266. (coverageMatrix.requirements || []).forEach((req) => {
  267. (req.tests || []).forEach((test) => {
  268. const stableId =
  269. test.id ||
  270. [test.file, test.title || test.name, test.line]
  271. .filter((value) => value !== undefined && value !== null && value !== '')
  272. .join(':') ||
  273. null; // unresolvable — skip rather than manufacture a key
  274. if (stableId === null || uniqueTests.has(stableId)) return;
  275. const explicitStatus = String(test.status || '')
  276. .trim()
  277. .toLowerCase();
  278. const status = ['skipped', 'pending', 'fixme'].includes(explicitStatus)
  279. ? explicitStatus
  280. : test.fixme === true
  281. ? 'fixme'
  282. : test.pending === true
  283. ? 'pending'
  284. : test.skipped === true
  285. ? 'skipped'
  286. : 'active';
  287. uniqueTests.set(stableId, {
  288. id: stableId,
  289. file: test.file || '',
  290. title: test.title || test.name || stableId,
  291. level: String(test.level || '')
  292. .trim()
  293. .toLowerCase(),
  294. skipped: status === 'skipped',
  295. fixme: status === 'fixme',
  296. pending: status === 'pending',
  297. status: status,
  298. blocker_reason: test.skip_reason || test.blocker_reason || test.fixme_reason || test.pending_reason || '',
  299. });
  300. });
  301. if (!coverageEligibleStatuses.has(req.coverage)) return;
  302. const requirementLevels = new Set(
  303. (req.tests || []).map((test) => {
  304. const level = String(test.level || '')
  305. .trim()
  306. .toLowerCase();
  307. return byLevel[level] ? level : 'other';
  308. }),
  309. );
  310. requirementLevels.forEach((level) => {
  311. byLevel[level].criteria_covered += 1;
  312. });
  313. });
  314. const deduplicatedTests = [...uniqueTests.values()];
  315. deduplicatedTests.forEach((test) => {
  316. const bucket = byLevel[test.level] ? test.level : 'other';
  317. byLevel[bucket].tests += 1;
  318. });
  319. return {
  320. summary: {
  321. files: [...new Set(deduplicatedTests.map((test) => test.file).filter(Boolean))].length,
  322. cases: deduplicatedTests.length,
  323. skipped_cases: deduplicatedTests.filter((test) => test.skipped).length,
  324. fixme_cases: deduplicatedTests.filter((test) => test.fixme).length,
  325. pending_cases: deduplicatedTests.filter((test) => test.pending).length,
  326. by_level: byLevel,
  327. },
  328. blockers: deduplicatedTests
  329. .filter((test) => ['skipped', 'pending', 'fixme'].includes(test.status))
  330. .map((test) => ({
  331. id: test.id,
  332. severity: test.status === 'skipped' ? 'high' : 'medium',
  333. reason: test.blocker_reason || `Test marked ${test.status} during trace collection`,
  334. test_file: test.file,
  335. test_title: test.title,
  336. })),
  337. };
  338. };
  339. const fallbackInventory = buildFallbackInventory();
  340. const testInventory = coverageMatrix.test_inventory?.summary || fallbackInventory.summary;
  341. const blockers = coverageMatrix.blockers || coverageMatrix.test_inventory?.blockers || fallbackInventory.blockers;
  342. const heuristicCounts = coverageMatrix.coverage_heuristics?.counts || {};
  343. const endpointGapCount = heuristicCounts.endpoints_without_tests ?? 0;
  344. const authGapCount = heuristicCounts.auth_missing_negative_paths ?? 0;
  345. const errorPathGapCount = heuristicCounts.happy_path_only_criteria ?? 0;
  346. const uiJourneyGapCount = heuristicCounts.ui_journeys_without_e2e;
  347. const uiStateGapCount = heuristicCounts.ui_states_missing_coverage;
  348. const sourceSha = process.env.GITHUB_SHA || runtime.getSourceSha?.() || '';
  349. const mapOptionalHeuristicStatus = (count, applicable) => {
  350. if (!applicable) return 'not_applicable';
  351. if (typeof count !== 'number' || Number.isNaN(count)) return 'unknown';
  352. if (count === 0) return 'present';
  353. return count <= 2 ? 'partial' : 'none';
  354. };
  355. const gateBasis = gateEligible ? 'priority_thresholds' : 'none';
  356. const e2eTraceSummary = {
  357. schema_version: '0.1.0',
  358. snapshot_at: new Date().toISOString(),
  359. repo: '{project_name}',
  360. collection_mode: collectionMode,
  361. collection_status: collectionStatus,
  362. inventory_basis: coverageBasis,
  363. gate_basis: gateBasis,
  364. source_sha: sourceSha || '',
  365. target: coverageMatrix.trace_target || { type: '{gate_type}', id: null, label: null },
  366. decision_mode: '{decision_mode}',
  367. evaluator: '{user_name}',
  368. confidence: effectiveOracleConfidence,
  369. oracle: {
  370. resolution_mode: oracleResolutionMode,
  371. confidence: effectiveOracleConfidence,
  372. sources: coverageMatrix.oracle?.sources || [],
  373. external_pointer_status: coverageMatrix.oracle?.external_pointer_status || 'not_used',
  374. synthetic: syntheticOracle,
  375. },
  376. coverage: {
  377. inventory: {
  378. covered: stats.fully_covered,
  379. total: stats.total_requirements,
  380. pct: stats.overall_coverage_percentage,
  381. },
  382. priority_breakdown: {
  383. P0: {
  384. total: priorityBreakdown.P0.total,
  385. covered: priorityBreakdown.P0.covered,
  386. pct: priorityBreakdown.P0.percentage,
  387. },
  388. P1: {
  389. total: priorityBreakdown.P1.total,
  390. covered: priorityBreakdown.P1.covered,
  391. pct: priorityBreakdown.P1.percentage,
  392. },
  393. P2: {
  394. total: priorityBreakdown.P2.total,
  395. covered: priorityBreakdown.P2.covered,
  396. pct: priorityBreakdown.P2.percentage,
  397. },
  398. P3: {
  399. total: priorityBreakdown.P3.total,
  400. covered: priorityBreakdown.P3.covered,
  401. pct: priorityBreakdown.P3.percentage,
  402. },
  403. },
  404. by_level: testInventory.by_level,
  405. },
  406. tests: {
  407. files: testInventory.files || 0,
  408. cases: testInventory.cases || 0,
  409. skipped_cases: testInventory.skipped_cases || 0,
  410. fixme_cases: testInventory.fixme_cases || 0,
  411. pending_cases: testInventory.pending_cases || 0,
  412. },
  413. risk_summary: {
  414. critical_open: (coverageMatrix.gap_analysis?.critical_gaps || []).length,
  415. high_open: (coverageMatrix.gap_analysis?.high_gaps || []).length,
  416. medium_open: (coverageMatrix.gap_analysis?.medium_gaps || []).length,
  417. low_open: (coverageMatrix.gap_analysis?.low_gaps || []).length,
  418. },
  419. heuristics: {
  420. endpoint_gaps: endpointGapCount,
  421. auth_negative_path_status: authGapCount === 0 ? 'present' : authGapCount <= 2 ? 'partial' : 'none',
  422. error_path_status: errorPathGapCount === 0 ? 'present' : errorPathGapCount <= 2 ? 'partial' : 'none',
  423. ui_journey_status: mapOptionalHeuristicStatus(uiJourneyGapCount, syntheticOracle),
  424. ui_state_status: mapOptionalHeuristicStatus(uiStateGapCount, syntheticOracle),
  425. },
  426. blockers: blockers,
  427. recommendations: coverageMatrix.recommendations,
  428. links: {
  429. trace_report_path: '{outputFile}',
  430. trace_report_url: '', // populated by CI/CD runner after artifact upload
  431. artifact_url: '',
  432. journey_evidence_url: '',
  433. },
  434. };
  435. if (gateEligible) {
  436. e2eTraceSummary.gate_status = gateDecision;
  437. e2eTraceSummary.gate_criteria = {
  438. p0_coverage_required: '100%',
  439. p0_coverage_actual: `${p0Coverage}%`,
  440. p0_status: p0Coverage === 100 ? 'MET' : 'NOT_MET',
  441. p1_coverage_target: '90%',
  442. p1_coverage_minimum: '80%',
  443. p1_coverage_actual: `${effectiveP1Coverage}%`,
  444. p1_status: effectiveP1Coverage >= 90 ? 'MET' : effectiveP1Coverage >= 80 ? 'PARTIAL' : 'NOT_MET',
  445. overall_coverage_minimum: '80%',
  446. overall_coverage_actual: `${overallCoverage}%`,
  447. overall_status: overallCoverage >= 80 ? 'MET' : 'NOT_MET',
  448. };
  449. }
  450. fs.writeFileSync('{e2e_trace_summary_output}', JSON.stringify(e2eTraceSummary, null, 2), 'utf8');
  451. console.log(`✅ e2e-trace-summary.json written to {e2e_trace_summary_output}`);
  452. ```
  453. **Optional: emit `gate-decision.json`** for pipelines that only need the gate signal without the full summary:
  454. ```javascript
  455. // Construct and write only when gate evaluation was performed and produced a meaningful decision.
  456. // gateDecisionSlim is intentionally inside this guard: e2eTraceSummary.gate_criteria is only
  457. // populated when gateEligible is true, so constructing it outside would throw when !gateEligible.
  458. if (gateEligible && ['PASS', 'CONCERNS', 'FAIL', 'WAIVED'].includes(gateDecision)) {
  459. const gateDecisionSlim = {
  460. schema_version: '0.1.0',
  461. evaluated_at: e2eTraceSummary.snapshot_at,
  462. repo: e2eTraceSummary.repo,
  463. target: e2eTraceSummary.target,
  464. collection_status: e2eTraceSummary.collection_status,
  465. gate_basis: e2eTraceSummary.gate_basis,
  466. gate_status: gateDecision,
  467. rationale: rationale,
  468. p0_status: e2eTraceSummary.gate_criteria.p0_status,
  469. p1_status: e2eTraceSummary.gate_criteria.p1_status,
  470. overall_status: e2eTraceSummary.gate_criteria.overall_status,
  471. critical_open: e2eTraceSummary.risk_summary.critical_open,
  472. links: e2eTraceSummary.links,
  473. };
  474. fs.writeFileSync('{gate_decision_output}', JSON.stringify(gateDecisionSlim, null, 2), 'utf8');
  475. console.log(`✅ gate-decision.json written to {gate_decision_output}`);
  476. }
  477. ```
  478. ---
  479. ### 4. Generate Traceability Report
  480. **Use trace-template.md to generate:**
  481. ```markdown
  482. # Traceability Report
  483. ## Gate Decision: {gateDecision}
  484. **Rationale:** {rationale}
  485. ## Coverage Summary
  486. - Total Requirements: {totalRequirements}
  487. - Covered: {fullyCovered} ({coveragePercentage}%)
  488. - P0 Coverage: {p0CoveragePercentage}%
  489. ## Traceability Matrix
  490. [Full matrix with requirement → test mappings]
  491. ## Gaps & Recommendations
  492. [List of uncovered requirements with recommended actions]
  493. ## Next Actions
  494. {recommendations}
  495. ```
  496. **Save to:**
  497. ```javascript
  498. fs.writeFileSync('{outputFile}', reportContent, 'utf8');
  499. ```
  500. ---
  501. ### 5. Display Gate Decision
  502. ```
  503. 🚨 GATE DECISION: {gateDecision}
  504. 📊 Coverage Analysis:
  505. - P0 Coverage: {p0Coverage}% (Required: 100%) → {p0_status}
  506. - P1 Coverage: {effectiveP1Coverage}% (PASS target: 90%, minimum: 80%) → {p1_status}
  507. - Overall Coverage: {overallCoverage}% (Minimum: 80%) → {overall_status}
  508. ✅ Decision Rationale:
  509. {rationale}
  510. ⚠️ Critical Gaps: {criticalGaps.length}
  511. 📝 Recommended Actions:
  512. {list top 3 recommendations}
  513. 📂 Full Report: {outputFile}
  514. {if !gateEligible}
  515. ℹ️ GATE: NOT EVALUATED - collection status is {collectionStatus}; machine-readable summary still emitted
  516. {endif}
  517. {if FAIL}
  518. 🚫 GATE: FAIL - Release BLOCKED until coverage improves
  519. {endif}
  520. {if CONCERNS}
  521. ⚠️ GATE: CONCERNS - Proceed with caution, address gaps soon
  522. {endif}
  523. {if PASS}
  524. ✅ GATE: PASS - Release approved, coverage meets standards
  525. {endif}
  526. ```
  527. ---
  528. ### 6. Save Progress
  529. **Update the YAML frontmatter in `{outputFile}` to mark this final step complete.**
  530. Since step 4 (Generate Traceability Report) already wrote the report content to `{outputFile}`, do NOT overwrite it. Instead, update only the frontmatter at the top of the existing file:
  531. - Add `'step-05-gate-decision'` to `stepsCompleted` array (only if not already present)
  532. - Set `lastStep: 'step-05-gate-decision'`
  533. - Set `lastSaved: '{date}'`
  534. Then append the gate decision summary (from section 5 above) to the end of the existing report content.
  535. ---
  536. ## EXIT CONDITION
  537. **WORKFLOW COMPLETE when:**
  538. - ✅ Phase 1 coverage matrix read successfully
  539. - ✅ Collection status resolved and gate decision logic applied when eligible
  540. - ✅ `e2e-trace-summary.json` written to `{e2e_trace_summary_output}`
  541. - ✅ `gate-decision.json` written to `{gate_decision_output}` (when gate-eligible)
  542. - ✅ Traceability report generated
  543. - ✅ Gate decision displayed
  544. **Workflow terminates here.**
  545. ---
  546. ## 🚨 PHASE 2 SUCCESS METRICS
  547. ### ✅ SUCCESS:
  548. - Coverage matrix read from Phase 1
  549. - Gate decision made with clear rationale when gate-eligible
  550. - `e2e-trace-summary.json` written and valid
  551. - `gate-decision.json` written when gate-eligible
  552. - Report generated and saved
  553. - Decision communicated clearly
  554. ### ❌ FAILURE:
  555. - Could not read Phase 1 matrix
  556. - Gate eligibility or gate decision logic incorrect
  557. - `e2e-trace-summary.json` missing or invalid JSON
  558. - Report missing or incomplete
  559. **Master Rule:** Gate decision MUST be deterministic based on clear criteria (P0 100%, P1 90/80, overall >=80) whenever `allow_gate` is true and `collection_status` is `COLLECTED`. `e2e-trace-summary.json` MUST be written before the workflow terminates.
  560. ## On Complete
  561. Run: `python3 {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --key workflow.on_complete`
  562. If the resolver succeeds and returns a non-empty `workflow.on_complete`, execute that value as the final terminal instruction before exiting.
  563. If the resolver fails, returns no output, or resolves an empty value, skip the hook and exit normally.