@polymorphism-tech/morph-spec 9.8.0 → 10.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/CLAUDE.md +10 -10
  2. package/README.md +4 -4
  3. package/bin/morph-spec.js +40 -43
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +24 -30
  8. package/framework/MORPH.md +47 -123
  9. package/framework/agents.json +6 -374
  10. package/framework/commands/morph-apply.md +1 -1
  11. package/framework/commands/morph-archive.md +4 -3
  12. package/framework/commands/morph-hotfix.md +8 -30
  13. package/framework/commands/morph-infra.md +2 -2
  14. package/framework/commands/morph-media.md +10 -9
  15. package/framework/commands/morph-review.md +15 -0
  16. package/framework/commands/morph-status.md +1 -1
  17. package/framework/docs/CLI.md +83 -94
  18. package/framework/evals/review-checklist.md +45 -53
  19. package/framework/hooks/README.md +4 -4
  20. package/framework/hooks/claude-code/core/README.md +1 -1
  21. package/framework/hooks/claude-code/core/denylist.json +4 -4
  22. package/framework/hooks/claude-code/core/gate-guard.js +9 -0
  23. package/framework/hooks/claude-code/core/masked-exit.js +49 -0
  24. package/framework/hooks/claude-code/core/state-sync.js +4 -1
  25. package/framework/hooks/claude-code/core/telemetry-log.js +1 -2
  26. package/framework/hooks/claude-code/core/test-budget.js +10 -70
  27. package/framework/hooks/claude-code/core/validate-completion.js +11 -24
  28. package/framework/hooks/claude-code/core/worktree-context.js +5 -22
  29. package/framework/hooks/claude-code/core/worktree-create.js +6 -45
  30. package/framework/hooks/claude-code/core/worktree-env-inject.js +2 -10
  31. package/framework/hooks/shared/e2e-check.js +0 -0
  32. package/framework/hooks/shared/gate-event.js +2 -2
  33. package/framework/hooks/shared/graph-refresh.js +20 -3
  34. package/framework/hooks/shared/heavy-command.js +32 -1
  35. package/framework/hooks/shared/heavy-config.js +1 -1
  36. package/framework/hooks/shared/next-step.js +63 -24
  37. package/framework/hooks/shared/phase-utils.js +6 -9
  38. package/framework/hooks/shared/report-progress.js +86 -0
  39. package/framework/hooks/shared/state-reader.js +3 -2
  40. package/framework/hooks/statusline.js +332 -0
  41. package/framework/phases.json +32 -10
  42. package/framework/rules/ai-agents-standards.md +9 -1
  43. package/framework/rules/csharp-standards.md +4 -0
  44. package/framework/rules/ef-standards.md +20 -0
  45. package/framework/rules/frontend-standards.md +1 -1
  46. package/framework/rules/implement-report.md +25 -0
  47. package/framework/rules/infrastructure-standards.md +2 -0
  48. package/framework/rules/nextjs-standards.md +7 -0
  49. package/framework/rules/plan-grammar.md +53 -0
  50. package/framework/rules/ui-design.md +27 -0
  51. package/framework/schemas/config.schema.json +11 -18
  52. package/framework/schemas/feature.schema.json +7 -1
  53. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  54. package/framework/skills/core/create-umbraco-lp/references/umbraco-lp-patterns.md +1 -1
  55. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +2 -2
  56. package/framework/skills/core/morph-apply/SKILL.md +38 -166
  57. package/framework/skills/core/morph-apply/references/debugging.md +8 -0
  58. package/framework/skills/core/morph-apply/references/hotfix.md +42 -0
  59. package/framework/skills/core/morph-apply/references/machine.md +29 -0
  60. package/framework/skills/core/{morph-media/SKILL.md → morph-apply/references/media.md} +11 -74
  61. package/framework/skills/core/morph-apply/references/review.md +65 -0
  62. package/framework/skills/core/morph-plan/SKILL.md +42 -108
  63. package/framework/skills/core/morph-plan/references/critique.md +40 -0
  64. package/framework/skills/core/morph-plan/references/interview.md +25 -0
  65. package/framework/skills/core/morph-uiux/SKILL.md +47 -64
  66. package/framework/skills/core/morph-uiux/surfaces.json +32 -0
  67. package/framework/standards/STANDARDS.json +32 -5
  68. package/framework/standards/STANDARDS.meta.json +5 -1
  69. package/framework/standards/ai-agents/agent-session.md +2 -3
  70. package/framework/standards/ai-agents/context-providers.md +2 -2
  71. package/framework/standards/ai-agents/durable-workflows-hitl.md +2 -2
  72. package/framework/standards/ai-agents/providers/model-registry.md +2 -2
  73. package/framework/standards/ai-agents/rag-custom-pgvector.md +6 -6
  74. package/framework/standards/ai-agents/setup.md +1 -1
  75. package/framework/standards/ai-agents/tool-adapters.md +2 -2
  76. package/framework/standards/architecture/stack-baseline.md +47 -0
  77. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -4
  78. package/framework/standards/frontend/design-system/ai-image-generation.md +51 -12
  79. package/framework/standards/frontend/design-system/ai-video-generation.md +50 -4
  80. package/framework/standards/frontend/design-system/art-direction.md +2 -2
  81. package/framework/standards/frontend/scroll-driven/frame-scrub.md +12 -2
  82. package/framework/standards/frontend/scroll-driven/scroll-components.md +76 -4
  83. package/framework/templates/REGISTRY.json +1 -2
  84. package/framework/templates/context/ARCHITECTURE.md +1 -1
  85. package/framework/templates/docs/plan.md +12 -12
  86. package/framework/templates/docs/ui-mockups.md +17 -105
  87. package/package.json +1 -1
  88. package/src/commands/e2e/e2e.js +88 -23
  89. package/src/commands/graph/diff.js +74 -0
  90. package/src/commands/graph/status.js +1 -1
  91. package/src/commands/observability/cost.js +2 -4
  92. package/src/commands/observability/telemetry.js +1 -1
  93. package/src/commands/project/doctor.js +23 -39
  94. package/src/commands/project/fleet.js +5 -6
  95. package/src/commands/project/init.js +63 -36
  96. package/src/commands/project/stack-scan.js +3 -42
  97. package/src/commands/project/stack-set.js +26 -0
  98. package/src/commands/project/status.js +12 -13
  99. package/src/commands/project/update.js +39 -3
  100. package/src/commands/project/worktree.js +3 -14
  101. package/src/commands/state/advance.js +17 -17
  102. package/src/commands/state/approve.js +0 -6
  103. package/src/commands/state/archive.js +27 -1
  104. package/src/commands/state/auto-approval.js +1 -1
  105. package/src/commands/state/create.js +0 -5
  106. package/src/commands/state/gate-check.js +37 -30
  107. package/src/commands/state/gate-decision.js +3 -2
  108. package/src/commands/state/next.js +1 -0
  109. package/src/commands/state/recap.js +67 -0
  110. package/src/commands/state/report-check.js +36 -178
  111. package/src/commands/validation/plan-check.js +16 -11
  112. package/src/commands/validation/validate-feature.js +3 -3
  113. package/src/commands/validation/verify.js +3 -6
  114. package/src/core/paths/output-schema.js +3 -3
  115. package/src/core/state/state-manager.js +2 -4
  116. package/src/dashboard/app/components/CommandPalette.jsx +2 -5
  117. package/src/dashboard/app/components/GatePauses.jsx +116 -0
  118. package/src/dashboard/app/components/SessionMap.jsx +77 -0
  119. package/src/dashboard/app/pages/Activity.jsx +4 -0
  120. package/src/dashboard/app/pages/AgentMap.jsx +1 -2
  121. package/src/dashboard/app/pages/FeatureDetail.jsx +129 -186
  122. package/src/dashboard/app/pages/Features.jsx +7 -7
  123. package/src/dashboard/app/pages/PhasePipeline.jsx +1 -1
  124. package/src/dashboard/app/pages/Portfolio.jsx +40 -5
  125. package/src/dashboard/scanner/activity-scanner.js +29 -0
  126. package/src/dashboard/scanner/agents-scanner.js +11 -14
  127. package/src/dashboard/scanner/features-scanner.js +45 -24
  128. package/src/dashboard/scanner/project-api.js +11 -2
  129. package/src/dashboard/scanner/skills-scanner.js +11 -1
  130. package/src/lib/e2e/applicability.js +3 -1
  131. package/src/lib/git/tree-state.js +1 -16
  132. package/src/lib/graph/diff-tree.js +127 -0
  133. package/src/lib/graph/graphify-runtime.js +12 -5
  134. package/src/lib/installers/memory-installer.js +2 -2
  135. package/src/lib/installers/rules-installer.js +57 -0
  136. package/src/lib/installers/stack-assets.js +17 -0
  137. package/src/lib/installers/stack-sync.js +219 -0
  138. package/src/lib/plan/checks.js +55 -110
  139. package/src/lib/plan/critique.js +28 -0
  140. package/src/lib/plan/media.js +42 -0
  141. package/src/lib/plan/plan-html.js +125 -0
  142. package/src/lib/plan/plan-parser.js +34 -45
  143. package/src/lib/recap/piper.js +36 -0
  144. package/src/lib/recap/recap.js +232 -0
  145. package/src/lib/review-reports.js +28 -4
  146. package/src/lib/risk-detector.js +90 -11
  147. package/src/lib/specs/screen-checklist.js +33 -48
  148. package/src/lib/stack/dotnet-build-ignores.js +8 -9
  149. package/src/lib/telemetry/metrics.js +27 -29
  150. package/src/lib/validators/reuse/reuse-validator.js +1 -9
  151. package/src/lib/validators/verify-runner.js +0 -7
  152. package/src/lib/worktree/fleet-scanner.js +7 -20
  153. package/src/lib/worktree/worktree-audit.js +0 -37
  154. package/src/scripts/setup-infra.js +15 -50
  155. package/src/utils/agents-installer.js +27 -201
  156. package/src/utils/hooks-installer.js +11 -95
  157. package/framework/hooks/claude-code/core/warn-bash-source-write.js +0 -23
  158. package/framework/hooks/claude-code/statusline.py +0 -699
  159. package/framework/hooks/claude-code/statusline.sh +0 -11
  160. package/framework/hooks/shared/agent-reap.js +0 -74
  161. package/framework/hooks/shared/slice-progress.js +0 -110
  162. package/framework/skills/core/morph-hotfix/SKILL.md +0 -133
  163. package/framework/skills/core/morph-machine/SKILL.md +0 -46
  164. package/framework/skills/core/morph-review/SKILL.md +0 -129
  165. package/framework/skills/core/morph-standards/SKILL.md +0 -52
  166. package/framework/standards/PROVENANCE.json +0 -119
  167. package/src/commands/project/worktree-reap.js +0 -34
  168. package/src/commands/state/handoff.js +0 -65
  169. package/src/commands/validation/mutate.js +0 -263
  170. package/src/commands/validation/scout-check.js +0 -28
  171. package/src/lib/generators/handoff-generator.js +0 -172
  172. package/src/lib/mutation/adapters.js +0 -79
  173. package/src/lib/mutation/commands.js +0 -61
  174. package/src/lib/mutation/dotnet-introspection.js +0 -281
  175. package/src/lib/mutation/manifest.js +0 -144
  176. package/src/lib/mutation/plan.js +0 -44
  177. package/src/lib/mutation/report.js +0 -68
  178. package/src/lib/mutation/runner.js +0 -301
  179. package/src/lib/mutation/schemata.js +0 -60
  180. package/src/lib/mutation/snapshot.js +0 -65
  181. package/src/lib/mutation/subset.js +0 -109
  182. package/src/lib/mutation/trx.js +0 -50
  183. package/src/lib/scout/scout-check.js +0 -293
  184. package/src/lib/validators/traceability-check.js +0 -103
  185. package/src/lib/worktree/agent-reaper.js +0 -247
@@ -0,0 +1,77 @@
1
+ import { clock, timeAgo } from '../lib/format.js';
2
+
3
+ const MARK = {
4
+ verify: { label: 'verify', color: (m) => (m.status === 'fail' ? 'var(--color-bad)' : 'var(--color-ok)') },
5
+ failure: { label: 'falha', color: () => 'var(--color-bad)' },
6
+ gate: { label: 'gate', color: () => 'var(--color-fg)' },
7
+ dispatch: { label: 'sub-agent', color: () => 'var(--color-phase-uiux)' },
8
+ handback: { label: 'sub-agent', color: () => 'var(--color-phase-uiux)' },
9
+ prompt: { label: 'você', color: () => 'var(--color-dim)' },
10
+ question: { label: 'você', color: () => 'var(--color-dim)' },
11
+ };
12
+ const LEGEND = [
13
+ ['verify verde', 'var(--color-ok)'], ['verify vermelho ou falha', 'var(--color-bad)'],
14
+ ['gate', 'var(--color-fg)'], ['sub-agent', 'var(--color-phase-uiux)'], ['você', 'var(--color-dim)'],
15
+ ];
16
+
17
+ function Strip({ session }) {
18
+ const start = Date.parse(session.start);
19
+ const span = Math.max(1, Date.parse(session.end) - start);
20
+ return (
21
+ <div className="relative h-6" role="img"
22
+ aria-label={`${session.marks.length} marcos entre ${clock(session.start)} e ${clock(session.end)}`}>
23
+ <div className="absolute inset-x-0 top-1/2 h-px -translate-y-1/2 bg-[var(--color-hair-strong)]" />
24
+ {session.marks.map((m, i) => {
25
+ const at = ((Date.parse(m.ts) - start) / span) * 100;
26
+ return (
27
+ <span key={`${m.ts}:${i}`} title={`${clock(m.ts)} · ${m.title}`}
28
+ className="absolute top-1/2 h-2.5 w-2.5 -translate-x-1/2 -translate-y-1/2 rounded-full ring-2 ring-[var(--color-core)]"
29
+ style={{ left: `${at}%`, background: (MARK[m.kind] ?? MARK.gate).color(m) }} />
30
+ );
31
+ })}
32
+ </div>
33
+ );
34
+ }
35
+
36
+ export default function SessionMap({ session, now = Date.now() }) {
37
+ if (session === undefined) return <p className="text-sm text-[var(--color-faint)]">Lendo a telemetria…</p>;
38
+ if (!session) return <p className="text-sm text-[var(--color-faint)]">Nenhuma ação registrada para esta feature ainda.</p>;
39
+ return (
40
+ <div className="space-y-4">
41
+ <p className="text-sm text-[var(--color-dim)]">
42
+ Última sessão: <span className="text-[var(--color-fg)]">{clock(session.start)}–{clock(session.end)}</span>,
43
+ {' '}{timeAgo(session.end, now)} · {session.actions} ações
44
+ </p>
45
+ <div>
46
+ <Strip session={session} />
47
+ <div className="mt-1 flex justify-between font-mono text-[11px] text-[var(--color-faint)]">
48
+ <span>{clock(session.start)}</span><span>{clock(session.end)}</span>
49
+ </div>
50
+ </div>
51
+ <div className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-[var(--color-dim)]" aria-hidden="true">
52
+ {LEGEND.map(([label, color]) => (
53
+ <span key={label} className="flex items-center gap-1.5"><span className="h-2 w-2 rounded-full" style={{ background: color }} />{label}</span>
54
+ ))}
55
+ </div>
56
+ {session.lastFailure && (
57
+ <p className="text-sm text-[var(--color-bad)]">
58
+ Parou em: {session.lastFailure.title}, {clock(session.lastFailure.ts)}
59
+ {session.lastFailure.detail && <span className="mt-0.5 block truncate font-mono text-xs text-[var(--color-dim)]">{session.lastFailure.detail}</span>}
60
+ </p>
61
+ )}
62
+ {session.files.length > 0 && (
63
+ <div>
64
+ <p className="mb-1.5 text-xs text-[var(--color-faint)]">Mexeu mais em</p>
65
+ <ul className="space-y-1">
66
+ {session.files.map((f) => (
67
+ <li key={f.path} className="flex items-baseline justify-between gap-3 text-sm">
68
+ <span className="min-w-0 truncate font-mono text-xs text-[var(--color-fg)]" title={f.path}>{f.path}</span>
69
+ <span className="shrink-0 tabular-nums text-xs text-[var(--color-dim)]">{f.edits}×</span>
70
+ </li>
71
+ ))}
72
+ </ul>
73
+ </div>
74
+ )}
75
+ </div>
76
+ );
77
+ }
@@ -1,5 +1,6 @@
1
1
  import { useParams } from 'react-router-dom';
2
2
  import ActivityFeed from '../components/ActivityFeed.jsx';
3
+ import GatePauses from '../components/GatePauses.jsx';
3
4
  import { useProjects } from '../lib/project.js';
4
5
 
5
6
  export default function Activity() {
@@ -16,6 +17,9 @@ export default function Activity() {
16
17
  os sub-agents despachados, os verifies e os gates. Atualiza sozinho enquanto ele trabalha.
17
18
  </p>
18
19
  </header>
20
+ <div className="rise-2 bezel mb-6">
21
+ <div className="bezel-core px-5 py-5"><GatePauses project={project} /></div>
22
+ </div>
19
23
  <div className="rise-2 bezel">
20
24
  <div className="bezel-core px-5 py-5"><ActivityFeed project={project} showFeature /></div>
21
25
  </div>
@@ -76,9 +76,8 @@ function buildLayout(nodes, edges) {
76
76
  }
77
77
 
78
78
  function dispatchingSkills(agent) {
79
- const implementer = (agent.activePhases || []).includes('implement');
80
79
  return (morphData.skills || []).filter((s) => (s.dispatches || []).some(
81
- (d) => (d.personas || []).includes(agent.id) || (d.generic && implementer),
80
+ (d) => (d.personas || []).includes(agent.id),
82
81
  ));
83
82
  }
84
83
 
@@ -1,40 +1,32 @@
1
1
  import { useCallback, useState } from 'react';
2
2
  import { Link, useParams, useSearchParams } from 'react-router-dom';
3
3
  import {
4
- ArrowLeft, CheckCircle, CircleDashed, PauseCircle, Circle, WarningCircle, FileText, FileCode, CaretDown, CaretRight,
5
- Rows, Kanban, Copy, Check, ArrowSquareOut,
4
+ ArrowLeft, CheckCircle, PauseCircle, Circle, WarningCircle, FileText, FileCode, Copy, Check, ArrowSquareOut,
6
5
  } from '@phosphor-icons/react';
7
6
  import PhaseRail, { segmentStates } from '../components/PhaseRail.jsx';
8
7
  import KnowledgeMarkdown from '../components/KnowledgeMarkdown.jsx';
9
- import EntityLink from '../components/EntityLink.jsx';
10
8
  import ActivityFeed from '../components/ActivityFeed.jsx';
9
+ import SessionMap from '../components/SessionMap.jsx';
11
10
  import { useMorphQuery } from '../hooks/useMorphQuery.js';
12
11
  import { usePref, useNow } from '../hooks/usePref.js';
13
- import { Segmented, isWorking } from './Features.jsx';
12
+ import { isWorking } from './Features.jsx';
14
13
  import { apiBase, projectHref } from '../lib/project.js';
15
14
  import { PHASES, GATE_LABEL, STATUS_LABEL, LOCATION_LABEL, phaseMeta } from '../lib/phases.js';
16
15
  import { timeAgo, dateTime, num, bytes, verdictLabel } from '../lib/format.js';
17
16
 
18
17
  const TABS = [
19
18
  { id: 'overview', label: 'Visão geral' },
20
- { id: 'slices', label: 'Fatias' },
21
19
  { id: 'checks', label: 'Cenários' },
22
20
  { id: 'docs', label: 'Documentos' },
23
21
  { id: 'activity', label: 'Atividade' },
24
22
  ];
25
23
 
26
- const SLICE_STATUS = {
27
- feito: { label: 'feita', tone: 'text-[var(--color-ok)]', icon: CheckCircle },
28
- parcial: { label: 'parcial', tone: 'text-[var(--color-phase-implement)]', icon: CircleDashed },
29
- bloqueado: { label: 'bloqueada', tone: 'text-[var(--color-warn)]', icon: PauseCircle },
24
+ const IMPLEMENT_STATUS = {
25
+ feito: { label: 'feito', tone: 'text-[var(--color-ok)]', icon: CheckCircle },
26
+ bloqueado: { label: 'bloqueado', tone: 'text-[var(--color-warn)]', icon: PauseCircle },
30
27
  pendente: { label: 'pendente', tone: 'text-[var(--color-faint)]', icon: Circle },
31
28
  };
32
29
 
33
- function isReady(sl, byId) {
34
- if (sl.status === 'feito' || sl.status === 'bloqueado') return false;
35
- return (sl.depends || []).every((d) => byId[d]?.status === 'feito');
36
- }
37
-
38
30
  export function editorLink(absPath) {
39
31
  return `vscode://file/${encodeURI(String(absPath).replace(/\\/g, '/'))}`;
40
32
  }
@@ -67,15 +59,15 @@ export function nextAction(f) {
67
59
  const paused = f.gates.find((g) => g.paused);
68
60
  if (paused) return `O ${GATE_LABEL[paused.gate] || paused.gate} pausou${paused.reason ? `: ${paused.reason}` : ''}. A decisão é sua, no Claude Code.`;
69
61
  if (f.inEvaluation) return 'Os relatórios do Gate 3 chegaram. A decisão sai dos vereditos, dos bloqueadores e dos cenários provados.';
70
- if (f.phase === 'implement' && f.slicesList?.length) {
71
- const blocked = f.slicesList.find((sl) => sl.status === 'bloqueado');
72
- if (blocked) return `${blocked.id} está bloqueada: o relatório traz uma pergunta que só você responde, no Claude Code.`;
73
- const open = f.slicesList.find((sl) => sl.status === 'parcial') || f.slicesList.find((sl) => sl.status === 'pendente');
74
- if (open) return `${f.slices.done} de ${f.slices.total} fatias feitas. Próxima: ${open.id}, ${open.title}.`;
75
- return 'Todas as fatias feitas. Falta o verify da feature e a revisão.';
62
+ if (f.phase === 'implement' && f.checksList?.length) {
63
+ const blocked = f.checksList.find((c) => c.implement === 'bloqueado');
64
+ if (blocked) return `${blocked.id} está bloqueado: o report.md traz uma pergunta que só você responde, no Claude Code.`;
65
+ const open = f.checksList.find((c) => c.implement !== 'feito');
66
+ if (open) return `${f.scenarios.done} de ${f.scenarios.total} cenários feitos. Próximo: ${open.id}.`;
67
+ return 'Todos os cenários feitos. Falta o verify da feature e a revisão.';
76
68
  }
77
69
  const text = {
78
- plan: 'Plano sendo escrito: cenários, decisões e fatias. O gate do plano aprova.',
70
+ plan: 'Plano sendo escrito: cenários, decisões e arquitetura. O gate do plano aprova.',
79
71
  uiux: 'Desenho de UI/UX em andamento.',
80
72
  implement: 'Implementação em andamento.',
81
73
  review: 'Em revisão.',
@@ -213,9 +205,12 @@ function CostBlock({ cost }) {
213
205
  );
214
206
  }
215
207
 
216
- function OverviewTab({ f, cost }) {
208
+ function OverviewTab({ f, cost, session, now }) {
217
209
  return (
218
210
  <div className="grid gap-5 lg:grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)]">
211
+ {f.status !== 'completed' && (
212
+ <Panel title="Onde parou" className="min-w-0 lg:col-span-2"><SessionMap session={session} now={now} /></Panel>
213
+ )}
219
214
  <div className="space-y-5">
220
215
  {f.description && (
221
216
  <Panel title="Descrição">
@@ -238,173 +233,122 @@ function OverviewTab({ f, cost }) {
238
233
  );
239
234
  }
240
235
 
241
- const CHECK_STATUS_TONE = {
242
- PROVADO: 'text-[var(--color-ok)]',
243
- FALHA: 'text-[var(--color-bad)]',
244
- 'NÃO PROVADO': 'text-[var(--color-warn)]',
245
- };
246
-
247
- function ChecksTab({ f }) {
248
- const cov = f.checksCoverage;
249
- if (!f.checksList?.length) {
250
- return <Panel><p className="text-sm text-[var(--color-dim)]">Sem cenários extraídos: o `plan-check` ainda não gravou o `checks.json`.</p></Panel>;
251
- }
252
- const proven = cov ? cov.total - cov.missing.length - cov.notProven.length : null;
253
- return (
254
- <Panel title={cov ? `${proven} de ${cov.total} provados pelo verificador` : 'Aguardando o relatório do verificador'}>
255
- <ul className="divide-y divide-[var(--color-hair)]">
256
- {f.checksList.map((c) => (
257
- <li key={c.id} className="grid grid-cols-[3rem_minmax(0,1fr)_7rem] gap-3 py-3 text-sm">
258
- <span className="font-mono text-[var(--color-dim)]">{c.id}</span>
259
- <div className="min-w-0">
260
- <p className="text-[var(--color-fg)]">{c.text}</p>
261
- {c.evidence && <p className="mt-1 truncate text-xs text-[var(--color-faint)]" title={c.evidence}>{c.evidence}</p>}
262
- </div>
263
- <div className="text-right">
264
- <p className={CHECK_STATUS_TONE[c.status] || 'text-[var(--color-faint)]'}>{c.status ?? (cov ? 'sem linha' : '—')}</p>
265
- {c.proof && <p className="text-xs text-[var(--color-faint)]">prova: {c.proof}</p>}
266
- </div>
267
- </li>
268
- ))}
269
- </ul>
270
- </Panel>
271
- );
272
- }
273
-
274
- function reportPathFor(f, sliceId) {
275
- const path = `3-implement/reports/${sliceId}.md`;
276
- return f.documents.some((d) => d.path === path) ? path : null;
277
- }
278
-
279
- function SliceRow({ sl, f, open, onToggle, ready }) {
280
- const st = SLICE_STATUS[sl.status] || SLICE_STATUS.pendente;
281
- const StIcon = st.icon;
282
- const report = reportPathFor(f, sl.id);
283
- return (
284
- <li>
285
- <button
286
- type="button"
287
- onClick={onToggle}
288
- aria-expanded={open}
289
- className="fluid grid w-full grid-cols-[1.25rem_2.75rem_minmax(0,1fr)] items-center gap-3 px-5 py-3 text-left transition-colors hover:bg-white/[0.025] md:grid-cols-[1.25rem_2.75rem_minmax(0,1fr)_9rem_1rem]"
290
- >
291
- <StIcon weight="light" size={17} className={st.tone} aria-label={st.label} />
292
- <span className="font-mono text-xs text-[var(--color-dim)]">{sl.id}</span>
293
- <span className="flex min-w-0 items-center gap-2">
294
- <span className="truncate text-sm text-[var(--color-fg)]">{sl.title}</span>
295
- {ready && <span className="shrink-0 text-xs text-[var(--color-phase-implement)]">pronta</span>}
296
- </span>
297
- <span className="hidden truncate text-xs text-[var(--color-dim)] md:block">{sl.persona ? `morph-${sl.persona}` : '—'}</span>
298
- <span className="hidden text-[var(--color-faint)] md:block">{open ? <CaretDown weight="light" size={14} /> : <CaretRight weight="light" size={14} />}</span>
299
- </button>
300
- {open && (
301
- <div className="grid gap-5 px-5 pb-5 pl-[4.75rem] md:grid-cols-2">
302
- <dl className="space-y-3 text-sm">
303
- {sl.persona && (
304
- <div><dt className="text-xs text-[var(--color-faint)]">Persona</dt>
305
- <dd><EntityLink kind="persona" id={sl.persona}>morph-{sl.persona}</EntityLink></dd></div>
306
- )}
307
- {sl.covers?.length > 0 && (
308
- <div><dt className="text-xs text-[var(--color-faint)]">Cobre</dt><dd className="font-mono text-xs text-[var(--color-dim)]">{sl.covers.join(', ')}</dd></div>
309
- )}
310
- {sl.depends?.length > 0 && (
311
- <div><dt className="text-xs text-[var(--color-faint)]">Depende de</dt><dd className="font-mono text-xs text-[var(--color-dim)]">{sl.depends.join(', ')}</dd></div>
312
- )}
313
- </dl>
314
- <dl className="space-y-3 text-sm">
315
- {sl.files?.length > 0 && (
316
- <div><dt className="text-xs text-[var(--color-faint)]">Arquivos</dt>
317
- <dd className="mt-1 space-y-0.5 font-mono text-[11px] text-[var(--color-dim)]">{sl.files.map((o) => <p key={o} className="truncate" title={o}>{o}</p>)}</dd></div>
318
- )}
319
- {report && (
320
- <div className="flex flex-wrap gap-2 pt-1">
321
- <Link to={`?tab=docs&doc=${encodeURIComponent(report)}`} className="fluid rounded-full bg-white/[0.06] px-3 py-1.5 text-xs text-[var(--color-fg)] transition-colors hover:bg-white/[0.1]">
322
- Abrir relatório
323
- </Link>
324
- </div>
325
- )}
326
- </dl>
327
- </div>
328
- )}
329
- </li>
330
- );
236
+ function ImplementIcon({ status }) {
237
+ const st = IMPLEMENT_STATUS[status] || IMPLEMENT_STATUS.pendente;
238
+ const Icon = st.icon;
239
+ return <Icon weight="light" size={17} className={st.tone} aria-label={`implementação: ${st.label}`} />;
331
240
  }
332
241
 
333
- const BOARD_COLUMNS = [
334
- { id: 'pendente', label: 'Pendentes' },
335
- { id: 'parcial', label: 'Parciais' },
336
- { id: 'bloqueado', label: 'Bloqueadas' },
337
- { id: 'feito', label: 'Feitas' },
338
- ];
339
-
340
- function SliceCard({ sl, f, ready }) {
341
- const report = reportPathFor(f, sl.id);
342
- return (
343
- <li className="rounded-xl bg-[var(--color-raised)] px-3.5 py-3 shadow-[inset_0_0_0_1px_var(--color-hair),inset_0_1px_0_rgb(255_255_255_/_0.05)]">
344
- <p className="flex items-baseline gap-2">
345
- <span className="font-mono text-xs text-[var(--color-dim)]">{sl.id}</span>
346
- {ready && <span className="text-xs text-[var(--color-phase-implement)]">pronta</span>}
347
- </p>
348
- <p className="mt-1 text-sm leading-snug text-[var(--color-fg)]">{sl.title}</p>
349
- {sl.persona && (
350
- <EntityLink kind="persona" id={sl.persona} className="mt-2 inline-block text-xs">morph-{sl.persona}</EntityLink>
351
- )}
352
- {sl.depends?.length > 0 && <p className="mt-1 text-xs text-[var(--color-faint)]">depende de {sl.depends.join(', ')}</p>}
353
- {report && (
354
- <Link to={`?tab=docs&doc=${encodeURIComponent(report)}`} className="mt-2.5 inline-block text-xs text-[var(--color-dim)] underline decoration-[var(--color-hair-strong)] underline-offset-2 hover:text-[var(--color-fg)]">abrir relatório</Link>
355
- )}
356
- </li>
357
- );
242
+ const SCORE_ROLES = ['verifier', 'reviewer', 'security'];
243
+ const CELL = {
244
+ ok: { icon: '✓', tone: 'bg-[var(--color-ok)]/15 text-[var(--color-ok)]' },
245
+ bad: { icon: '✗', tone: 'bg-[var(--color-bad)]/15 text-[var(--color-bad)]' },
246
+ warn: { icon: '?', tone: 'bg-[var(--color-warn)]/15 text-[var(--color-warn)]' },
247
+ na: { icon: '·', tone: 'bg-white/[0.04] text-[var(--color-faint)]' },
248
+ };
249
+ const CHECK_CELL = { PROVADO: 'ok', FALHA: 'bad', 'NÃO PROVADO': 'warn' };
250
+
251
+ export function scoreCell(round, id, role) {
252
+ const r = round.roles[role];
253
+ if (!r) return { kind: 'na', text: 'sem relatório' };
254
+ const cited = r.blockers.filter((b) => b.checks.includes(id));
255
+ if (cited.length) return { kind: 'bad', text: cited.map((b) => `${b.id}: ${b.evidence}`).join(' · ') };
256
+ if (role === 'verifier') {
257
+ const row = round.checks.find((c) => c.id === id);
258
+ if (!row) return { kind: 'warn', text: 'sem linha no relatório' };
259
+ return { kind: CHECK_CELL[row.status] ?? 'warn', text: `${row.status}. ${row.evidence}` };
260
+ }
261
+ return { kind: 'na', text: `não julga por cenário; veredito ${r.verdict ?? '—'}` };
358
262
  }
359
263
 
360
- function SlicesBoard({ f, byId }) {
361
- return (
362
- <div className="-mx-4 overflow-x-auto px-4 pb-2 md:mx-0 md:px-0">
363
- <div className="grid auto-cols-[16rem] grid-flow-col gap-3 md:auto-cols-[minmax(12rem,1fr)]">
364
- {BOARD_COLUMNS.map((c) => {
365
- const items = f.slicesList.filter((sl) => sl.status === c.id);
366
- return (
367
- <section key={c.id} className="bezel min-w-0" aria-label={`${c.label}: ${items.length}`}>
368
- <div className="bezel-core flex h-full min-h-[12rem] flex-col p-2.5">
369
- <h3 className="flex items-center justify-between px-1.5 pb-2.5 pt-1 text-sm font-medium">
370
- {c.label}<span className="font-mono text-xs text-[var(--color-faint)]">{items.length}</span>
371
- </h3>
372
- {items.length === 0
373
- ? <p className="px-1.5 text-xs text-[var(--color-faint)]">Nenhuma.</p>
374
- : <ul className="space-y-2">{items.map((sl) => <SliceCard key={sl.id} sl={sl} f={f} ready={isReady(sl, byId)} />)}</ul>}
375
- </div>
376
- </section>
377
- );
378
- })}
379
- </div>
380
- </div>
381
- );
382
- }
264
+ // Same rule as the gate: a Cn the verifier did not prove keeps the round at FIX, whatever the reports say.
265
+ const roundVerdict = (round, ids) => (Object.values(round.roles).some((r) => r.verdict !== 'SHIP' || r.blockers.length)
266
+ || (round.roles.verifier && ids.some((id) => scoreCell(round, id, 'verifier').kind !== 'ok')) ? 'FIX' : 'SHIP');
267
+ const roundSha = (round) => (round.roles.verifier ?? Object.values(round.roles)[0])?.reviewedSha?.slice(0, 7) ?? null;
268
+ const blockersOf = (round) => SCORE_ROLES.flatMap((role) => (round.roles[role]?.carried ? [] : round.roles[role]?.blockers ?? []).map((b) => ({ ...b, role })));
383
269
 
384
- function SlicesTab({ f, view = 'list', onView = () => {} }) {
270
+ function ChecksTab({ f }) {
271
+ const rounds = f.rounds ?? [];
272
+ const [pick, setPick] = useState(null);
385
273
  const [open, setOpen] = useState(null);
386
- if (!f.slicesList.length) {
387
- return <Panel><p className="text-sm text-[var(--color-dim)]">O plano desta feature ainda não foi conferido pelo <code className="font-mono">morph-spec plan-check</code>. As fatias nascem na fase de plano.</p></Panel>;
274
+ if (!f.checksList?.length) {
275
+ return <Panel><p className="text-sm text-[var(--color-dim)]">Sem cenários extraídos: o `plan-check` ainda não gravou o `checks.json`.</p></Panel>;
388
276
  }
389
- const byId = Object.fromEntries(f.slicesList.map((sl) => [sl.id, sl]));
390
- const ready = f.slicesList.filter((sl) => isReady(sl, byId)).length;
277
+ const i = pick ?? rounds.length - 1;
278
+ const round = rounds[i];
279
+ const proven = round ? f.checksList.filter((c) => scoreCell(round, c.id, 'verifier').kind === 'ok').length : null;
280
+ const blockers = round ? blockersOf(round) : [];
281
+ const closed = round && i > 0 && !blockers.length ? blockersOf(rounds[i - 1]) : [];
282
+ const ids = f.checksList.map((c) => c.id);
283
+ const verdict = round ? roundVerdict(round, ids) : null;
391
284
  return (
392
- <div className="space-y-4">
393
- <div className="flex flex-wrap items-center justify-between gap-3">
394
- <Segmented label="Visualização das fatias" value={view} onChange={onView} options={[{ id: 'list', label: 'Lista', icon: Rows }, { id: 'board', label: 'Quadro', icon: Kanban }]} />
395
- <p className="text-sm text-[var(--color-dim)]">
396
- {f.slices.done} de {f.slices.total} feitas{f.slices.partial ? `, ${f.slices.partial} parciais` : ''}{f.slices.blocked ? `, ${f.slices.blocked} bloqueadas` : ''}{ready ? `, ${ready} prontas para começar` : ''}
397
- </p>
398
- </div>
399
- {view === 'board' ? <SlicesBoard f={f} byId={byId} /> : (
400
- <div className="bezel">
401
- <ul className="bezel-core divide-y divide-[var(--color-hair)] overflow-hidden">
402
- {f.slicesList.map((sl) => (
403
- <SliceRow key={sl.id} sl={sl} f={f} ready={isReady(sl, byId)}
404
- open={open === sl.id} onToggle={() => setOpen(open === sl.id ? null : sl.id)} />
285
+ <div className="space-y-5">
286
+ <Panel>
287
+ <div className="flex flex-wrap items-baseline gap-4">
288
+ <span className={`text-4xl font-semibold tracking-tight ${verdict === 'SHIP' ? 'text-[var(--color-ok)]' : verdict === 'FIX' ? 'text-[var(--color-bad)]' : ''}`}>{verdict ?? '—'}</span>
289
+ <span className="text-sm text-[var(--color-dim)]">
290
+ {round ? `${proven} de ${f.checksList.length} cenários provados · ${blockers.length} bloqueador(es)` : 'Aguardando os relatórios do Gate 3'}
291
+ </span>
292
+ </div>
293
+ {rounds.length > 1 && (
294
+ <div className="mt-4 flex flex-wrap gap-2">
295
+ {rounds.map((r, k) => (
296
+ <button key={k} type="button" aria-pressed={k === i} onClick={() => setPick(k)}
297
+ className={`fluid inline-flex items-center gap-2 rounded-full px-3 py-1 text-xs shadow-[inset_0_0_0_1px_var(--color-hair)] ${k === i ? 'bg-white/[0.1] text-[var(--color-fg)]' : 'text-[var(--color-dim)] hover:text-[var(--color-fg)]'}`}>
298
+ <span className={`h-2 w-2 rounded-full ${roundVerdict(r, ids) === 'SHIP' ? 'bg-[var(--color-ok)]' : 'bg-[var(--color-bad)]'}`} />
299
+ Rodada {k + 1} · {roundVerdict(r, ids)}{roundSha(r) && <span className="font-mono text-[var(--color-faint)]">{roundSha(r)}</span>}
300
+ </button>
405
301
  ))}
406
- </ul>
302
+ </div>
303
+ )}
304
+ </Panel>
305
+ <Panel>
306
+ <div className="grid grid-cols-[1.25rem_3rem_minmax(0,1fr)_repeat(3,3.5rem)] gap-3 pb-2 text-xs text-[var(--color-faint)]">
307
+ <span /><span>Cn</span><span>cenário</span>
308
+ {SCORE_ROLES.map((role) => <span key={role} className="text-center">{REVIEW_ROLE_LABEL[role]}</span>)}
407
309
  </div>
310
+ <ul className="divide-y divide-[var(--color-hair)]">
311
+ {f.checksList.map((c) => {
312
+ const cells = SCORE_ROLES.map((role) => (round ? scoreCell(round, c.id, role) : { kind: 'na', text: '' }));
313
+ const expanded = open === c.id;
314
+ return (
315
+ <li key={c.id} className="py-3 text-sm">
316
+ <button type="button" onClick={() => setOpen(expanded ? null : c.id)} aria-expanded={expanded}
317
+ className="grid w-full grid-cols-[1.25rem_3rem_minmax(0,1fr)_repeat(3,3.5rem)] items-center gap-3 text-left">
318
+ <ImplementIcon status={c.implement} />
319
+ <span className="font-mono text-[var(--color-dim)]">{c.id}</span>
320
+ <span className={`min-w-0 text-[var(--color-fg)] ${expanded ? '' : 'truncate'}`} title={c.text}>{c.text}</span>
321
+ {cells.map((cell, k) => (
322
+ <span key={k} title={cell.text} className={`mx-auto grid h-8 w-8 place-items-center rounded-lg font-semibold ${CELL[cell.kind].tone}`}>{CELL[cell.kind].icon}</span>
323
+ ))}
324
+ </button>
325
+ {expanded && (
326
+ <div className="mt-3 space-y-2 pl-[4.25rem] text-xs">
327
+ {c.proof && <p className="text-[var(--color-faint)]">prova: {c.proof}</p>}
328
+ {cells.map((cell, k) => cell.kind !== 'na' && (
329
+ <p key={k}><span className="mr-2 text-[var(--color-faint)]">{REVIEW_ROLE_LABEL[SCORE_ROLES[k]]}</span><span className="text-[var(--color-dim)]">{cell.text}</span></p>
330
+ ))}
331
+ </div>
332
+ )}
333
+ </li>
334
+ );
335
+ })}
336
+ </ul>
337
+ </Panel>
338
+ {round && (
339
+ <Panel title={blockers.length ? 'Bloqueadores desta rodada' : closed.length ? 'Fechados nesta rodada' : 'Nenhum bloqueador'}>
340
+ <ul className="space-y-2 text-xs">
341
+ {(blockers.length ? blockers : closed).map((b) => (
342
+ <li key={`${b.role}-${b.id}`} className="grid grid-cols-[3rem_minmax(0,1fr)] gap-3">
343
+ <span className={`font-mono ${blockers.length ? 'text-[var(--color-bad)]' : 'text-[var(--color-ok)]'}`}>{b.id}</span>
344
+ <div className="min-w-0">
345
+ <p className="text-[var(--color-faint)]">{REVIEW_ROLE_LABEL[b.role]} · {b.type} · {b.checks.length ? b.checks.join(', ') : 'sem Cn citado'} · <span className="font-mono">{b.where}</span></p>
346
+ <p className="mt-0.5 truncate text-[var(--color-dim)]" title={b.evidence}>{b.evidence}</p>
347
+ </div>
348
+ </li>
349
+ ))}
350
+ </ul>
351
+ </Panel>
408
352
  )}
409
353
  </div>
410
354
  );
@@ -491,7 +435,7 @@ function DocsTab({ f, doc, onDoc, project }) {
491
435
  );
492
436
  }
493
437
 
494
- export function FeatureDetailView({ f, tab, onTab, doc, onDoc, cost, latest = null, slicesView = 'list', onSlicesView = () => {}, now = Date.now(), project, projectName }) {
438
+ export function FeatureDetailView({ f, tab, onTab, doc, onDoc, cost, session, latest = null, now = Date.now(), project, projectName }) {
495
439
  const verdict = verdictLabel(f);
496
440
  const working = latest && isWorking({ ...f, lastActivity: latest }, now);
497
441
  return (
@@ -510,7 +454,7 @@ export function FeatureDetailView({ f, tab, onTab, doc, onDoc, cost, latest = nu
510
454
  </p>
511
455
  </div>
512
456
  <div className="flex shrink-0 gap-6 text-right">
513
- <div><p className="text-2xl font-semibold tabular-nums">{f.slices.total ? `${f.slices.done}/${f.slices.total}` : '—'}</p><p className="text-xs text-[var(--color-faint)]">fatias</p></div>
457
+ <div><p className="text-2xl font-semibold tabular-nums">{f.scenarios.total ? `${f.scenarios.done}/${f.scenarios.total}` : '—'}</p><p className="text-xs text-[var(--color-faint)]">cenários</p></div>
514
458
  <div><p className="text-2xl font-semibold">{verdict ?? '—'}</p><p className="text-xs text-[var(--color-faint)]">veredito</p></div>
515
459
  </div>
516
460
  </div>
@@ -556,14 +500,13 @@ export function FeatureDetailView({ f, tab, onTab, doc, onDoc, cost, latest = nu
556
500
  tab === t.id ? 'bg-white/[0.1] text-[var(--color-fg)] shadow-[inset_0_1px_0_rgb(255_255_255_/_0.08)]' : 'text-[var(--color-dim)] hover:text-[var(--color-fg)]',
557
501
  ].join(' ')}
558
502
  >
559
- {t.label}{t.id === 'slices' && f.slices.total ? ` ${f.slices.total}` : ''}{t.id === 'checks' && f.checksList?.length ? ` ${f.checksList.length}` : ''}{t.id === 'docs' ? ` ${f.documents.length}` : ''}
503
+ {t.label}{t.id === 'checks' && f.checksList?.length ? ` ${f.checksList.length}` : ''}{t.id === 'docs' ? ` ${f.documents.length}` : ''}
560
504
  </button>
561
505
  ))}
562
506
  </div>
563
507
 
564
508
  <div className="rise-3">
565
- {tab === 'overview' && <OverviewTab f={f} cost={cost} />}
566
- {tab === 'slices' && <SlicesTab f={f} view={slicesView} onView={onSlicesView} />}
509
+ {tab === 'overview' && <OverviewTab f={f} cost={cost} session={session} now={now} />}
567
510
  {tab === 'checks' && <ChecksTab f={f} />}
568
511
  {tab === 'docs' && <DocsTab f={f} doc={doc} onDoc={onDoc} project={project} />}
569
512
  {tab === 'activity' && (
@@ -582,7 +525,6 @@ export default function FeatureDetail() {
582
525
  const { data, error, loading } = useMorphQuery(`${apiBase(project)}/features/${encodeURIComponent(name)}`, { project });
583
526
  // The cost rides on the activity endpoint (it owns the event stream).
584
527
  const costQuery = useMorphQuery(`${apiBase(project)}/activity?feature=${encodeURIComponent(name)}&limit=1`, { scope: 'activity', project });
585
- const [slicesView, setSlicesView] = usePref('slices.view', 'list');
586
528
  const now = useNow();
587
529
 
588
530
  const onTab = useCallback((id) => setParams((p) => { const n = new URLSearchParams(p); n.set('tab', id); if (id !== 'docs') n.delete('doc'); return n; }), [setParams]);
@@ -602,7 +544,8 @@ export default function FeatureDetail() {
602
544
  <FeatureDetailView
603
545
  f={data} tab={tab} onTab={onTab} doc={doc} onDoc={onDoc}
604
546
  cost={costQuery.data?.cost ?? null} latest={costQuery.data?.items?.[0] ?? null}
605
- slicesView={slicesView} onSlicesView={setSlicesView} now={now} project={project}
547
+ session={costQuery.data ? costQuery.data.session : undefined}
548
+ now={now} project={project}
606
549
  />
607
550
  );
608
551
  }
@@ -40,15 +40,15 @@ function VerifyMark({ verification }) {
40
40
  : <XCircle weight="light" className="text-[var(--color-bad)]" size={16} aria-label="último verify falhou" />;
41
41
  }
42
42
 
43
- function SliceMeter({ slices, wide = false }) {
44
- if (!slices?.total) return <span className="text-xs text-[var(--color-faint)]">sem fatias</span>;
45
- const pct = Math.round((slices.done / slices.total) * 100);
43
+ function ScenarioMeter({ scenarios, wide = false }) {
44
+ if (!scenarios?.total) return <span className="text-xs text-[var(--color-faint)]">sem cenários</span>;
45
+ const pct = Math.round((scenarios.done / scenarios.total) * 100);
46
46
  return (
47
- <div className="flex items-center gap-2" title={`${slices.done} de ${slices.total} fatias feitas`}>
47
+ <div className="flex items-center gap-2" title={`${scenarios.done} de ${scenarios.total} cenários feitos`}>
48
48
  <div className={`h-1 overflow-hidden rounded-full bg-white/[0.07] ${wide ? 'flex-1' : 'w-12'}`}>
49
49
  <div className="h-full rounded-full bg-[var(--color-fg)]/70" style={{ width: `${pct}%` }} />
50
50
  </div>
51
- <span className="font-mono text-xs tabular-nums text-[var(--color-dim)]">{slices.done}/{slices.total}</span>
51
+ <span className="font-mono text-xs tabular-nums text-[var(--color-dim)]">{scenarios.done}/{scenarios.total}</span>
52
52
  </div>
53
53
  );
54
54
  }
@@ -104,7 +104,7 @@ export function FeatureRow({ f, now, project, projectName }) {
104
104
  <PhaseRail feature={f} />
105
105
  {/* One line on a phone; on desktop the wrapper dissolves into the row's grid. */}
106
106
  <div className="flex items-center gap-5 md:contents">
107
- <SliceMeter slices={f.slices} />
107
+ <ScenarioMeter scenarios={f.scenarios} />
108
108
  <VerifyMark verification={f.verification} />
109
109
  <span className="font-mono text-sm tabular-nums text-[var(--color-dim)]" title={verdict ? 'veredito do Gate 3' : 'sem veredito'}>
110
110
  {verdict ?? '—'}
@@ -163,7 +163,7 @@ function BoardCard({ f, now, project }) {
163
163
  {f.lastActivity?.title && !isFinished(f) && (
164
164
  <p className="mt-1.5 line-clamp-2 text-xs text-[var(--color-dim)]">{f.lastActivity.title}</p>
165
165
  )}
166
- <div className="mt-3"><SliceMeter slices={f.slices} wide /></div>
166
+ <div className="mt-3"><ScenarioMeter scenarios={f.scenarios} wide /></div>
167
167
  <div className="mt-2.5 flex items-center justify-between text-xs text-[var(--color-faint)]">
168
168
  <span className="flex items-center gap-2">
169
169
  <VerifyMark verification={f.verification} />
@@ -10,7 +10,7 @@ import { phaseMeta, GATE_LABEL } from '../lib/phases.js';
10
10
  const phaseColor = (id) => `var(--color-phase-${id})`;
11
11
 
12
12
  const GATE_CHECKS = {
13
- plan: 'Confere o plano: o problema, o que fica de fora, os cenários com a prova de cada um e as fatias. Depois dele o plan.md congela (só a errata cresce).',
13
+ plan: 'Confere o plano: o problema, o que fica de fora, os cenários com a prova de cada um, o reuso e as decisões. Depois dele o plan.md congela (só a errata cresce).',
14
14
  uiux: 'Confere o design das telas (mockups e checklist de tela). É opcional: só existe quando a feature passa pela fase de UI/UX, depois do plano.',
15
15
  review: 'Confere a avaliação independente em contexto novo: verificador e revisor (e segurança, quando o diff toca auth, segredo ou dado pessoal), cada um com veredito SHIP ou FIX sobre o mesmo commit. Só bloqueia com evidência, com o verify verde e todo cenário provado.',
16
16
  };