@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.
- package/CLAUDE.md +10 -10
- package/README.md +4 -4
- package/bin/morph-spec.js +40 -43
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +24 -30
- package/framework/MORPH.md +47 -123
- package/framework/agents.json +6 -374
- package/framework/commands/morph-apply.md +1 -1
- package/framework/commands/morph-archive.md +4 -3
- package/framework/commands/morph-hotfix.md +8 -30
- package/framework/commands/morph-infra.md +2 -2
- package/framework/commands/morph-media.md +10 -9
- package/framework/commands/morph-review.md +15 -0
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +83 -94
- package/framework/evals/review-checklist.md +45 -53
- package/framework/hooks/README.md +4 -4
- package/framework/hooks/claude-code/core/README.md +1 -1
- package/framework/hooks/claude-code/core/denylist.json +4 -4
- package/framework/hooks/claude-code/core/gate-guard.js +9 -0
- package/framework/hooks/claude-code/core/masked-exit.js +49 -0
- package/framework/hooks/claude-code/core/state-sync.js +4 -1
- package/framework/hooks/claude-code/core/telemetry-log.js +1 -2
- package/framework/hooks/claude-code/core/test-budget.js +10 -70
- package/framework/hooks/claude-code/core/validate-completion.js +11 -24
- package/framework/hooks/claude-code/core/worktree-context.js +5 -22
- package/framework/hooks/claude-code/core/worktree-create.js +6 -45
- package/framework/hooks/claude-code/core/worktree-env-inject.js +2 -10
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/gate-event.js +2 -2
- package/framework/hooks/shared/graph-refresh.js +20 -3
- package/framework/hooks/shared/heavy-command.js +32 -1
- package/framework/hooks/shared/heavy-config.js +1 -1
- package/framework/hooks/shared/next-step.js +63 -24
- package/framework/hooks/shared/phase-utils.js +6 -9
- package/framework/hooks/shared/report-progress.js +86 -0
- package/framework/hooks/shared/state-reader.js +3 -2
- package/framework/hooks/statusline.js +332 -0
- package/framework/phases.json +32 -10
- package/framework/rules/ai-agents-standards.md +9 -1
- package/framework/rules/csharp-standards.md +4 -0
- package/framework/rules/ef-standards.md +20 -0
- package/framework/rules/frontend-standards.md +1 -1
- package/framework/rules/implement-report.md +25 -0
- package/framework/rules/infrastructure-standards.md +2 -0
- package/framework/rules/nextjs-standards.md +7 -0
- package/framework/rules/plan-grammar.md +53 -0
- package/framework/rules/ui-design.md +27 -0
- package/framework/schemas/config.schema.json +11 -18
- package/framework/schemas/feature.schema.json +7 -1
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/create-umbraco-lp/references/umbraco-lp-patterns.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +2 -2
- package/framework/skills/core/morph-apply/SKILL.md +38 -166
- package/framework/skills/core/morph-apply/references/debugging.md +8 -0
- package/framework/skills/core/morph-apply/references/hotfix.md +42 -0
- package/framework/skills/core/morph-apply/references/machine.md +29 -0
- package/framework/skills/core/{morph-media/SKILL.md → morph-apply/references/media.md} +11 -74
- package/framework/skills/core/morph-apply/references/review.md +65 -0
- package/framework/skills/core/morph-plan/SKILL.md +42 -108
- package/framework/skills/core/morph-plan/references/critique.md +40 -0
- package/framework/skills/core/morph-plan/references/interview.md +25 -0
- package/framework/skills/core/morph-uiux/SKILL.md +47 -64
- package/framework/skills/core/morph-uiux/surfaces.json +32 -0
- package/framework/standards/STANDARDS.json +32 -5
- package/framework/standards/STANDARDS.meta.json +5 -1
- package/framework/standards/ai-agents/agent-session.md +2 -3
- package/framework/standards/ai-agents/context-providers.md +2 -2
- package/framework/standards/ai-agents/durable-workflows-hitl.md +2 -2
- package/framework/standards/ai-agents/providers/model-registry.md +2 -2
- package/framework/standards/ai-agents/rag-custom-pgvector.md +6 -6
- package/framework/standards/ai-agents/setup.md +1 -1
- package/framework/standards/ai-agents/tool-adapters.md +2 -2
- package/framework/standards/architecture/stack-baseline.md +47 -0
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -4
- package/framework/standards/frontend/design-system/ai-image-generation.md +51 -12
- package/framework/standards/frontend/design-system/ai-video-generation.md +50 -4
- package/framework/standards/frontend/design-system/art-direction.md +2 -2
- package/framework/standards/frontend/scroll-driven/frame-scrub.md +12 -2
- package/framework/standards/frontend/scroll-driven/scroll-components.md +76 -4
- package/framework/templates/REGISTRY.json +1 -2
- package/framework/templates/context/ARCHITECTURE.md +1 -1
- package/framework/templates/docs/plan.md +12 -12
- package/framework/templates/docs/ui-mockups.md +17 -105
- package/package.json +1 -1
- package/src/commands/e2e/e2e.js +88 -23
- package/src/commands/graph/diff.js +74 -0
- package/src/commands/graph/status.js +1 -1
- package/src/commands/observability/cost.js +2 -4
- package/src/commands/observability/telemetry.js +1 -1
- package/src/commands/project/doctor.js +23 -39
- package/src/commands/project/fleet.js +5 -6
- package/src/commands/project/init.js +63 -36
- package/src/commands/project/stack-scan.js +3 -42
- package/src/commands/project/stack-set.js +26 -0
- package/src/commands/project/status.js +12 -13
- package/src/commands/project/update.js +39 -3
- package/src/commands/project/worktree.js +3 -14
- package/src/commands/state/advance.js +17 -17
- package/src/commands/state/approve.js +0 -6
- package/src/commands/state/archive.js +27 -1
- package/src/commands/state/auto-approval.js +1 -1
- package/src/commands/state/create.js +0 -5
- package/src/commands/state/gate-check.js +37 -30
- package/src/commands/state/gate-decision.js +3 -2
- package/src/commands/state/next.js +1 -0
- package/src/commands/state/recap.js +67 -0
- package/src/commands/state/report-check.js +36 -178
- package/src/commands/validation/plan-check.js +16 -11
- package/src/commands/validation/validate-feature.js +3 -3
- package/src/commands/validation/verify.js +3 -6
- package/src/core/paths/output-schema.js +3 -3
- package/src/core/state/state-manager.js +2 -4
- package/src/dashboard/app/components/CommandPalette.jsx +2 -5
- package/src/dashboard/app/components/GatePauses.jsx +116 -0
- package/src/dashboard/app/components/SessionMap.jsx +77 -0
- package/src/dashboard/app/pages/Activity.jsx +4 -0
- package/src/dashboard/app/pages/AgentMap.jsx +1 -2
- package/src/dashboard/app/pages/FeatureDetail.jsx +129 -186
- package/src/dashboard/app/pages/Features.jsx +7 -7
- package/src/dashboard/app/pages/PhasePipeline.jsx +1 -1
- package/src/dashboard/app/pages/Portfolio.jsx +40 -5
- package/src/dashboard/scanner/activity-scanner.js +29 -0
- package/src/dashboard/scanner/agents-scanner.js +11 -14
- package/src/dashboard/scanner/features-scanner.js +45 -24
- package/src/dashboard/scanner/project-api.js +11 -2
- package/src/dashboard/scanner/skills-scanner.js +11 -1
- package/src/lib/e2e/applicability.js +3 -1
- package/src/lib/git/tree-state.js +1 -16
- package/src/lib/graph/diff-tree.js +127 -0
- package/src/lib/graph/graphify-runtime.js +12 -5
- package/src/lib/installers/memory-installer.js +2 -2
- package/src/lib/installers/rules-installer.js +57 -0
- package/src/lib/installers/stack-assets.js +17 -0
- package/src/lib/installers/stack-sync.js +219 -0
- package/src/lib/plan/checks.js +55 -110
- package/src/lib/plan/critique.js +28 -0
- package/src/lib/plan/media.js +42 -0
- package/src/lib/plan/plan-html.js +125 -0
- package/src/lib/plan/plan-parser.js +34 -45
- package/src/lib/recap/piper.js +36 -0
- package/src/lib/recap/recap.js +232 -0
- package/src/lib/review-reports.js +28 -4
- package/src/lib/risk-detector.js +90 -11
- package/src/lib/specs/screen-checklist.js +33 -48
- package/src/lib/stack/dotnet-build-ignores.js +8 -9
- package/src/lib/telemetry/metrics.js +27 -29
- package/src/lib/validators/reuse/reuse-validator.js +1 -9
- package/src/lib/validators/verify-runner.js +0 -7
- package/src/lib/worktree/fleet-scanner.js +7 -20
- package/src/lib/worktree/worktree-audit.js +0 -37
- package/src/scripts/setup-infra.js +15 -50
- package/src/utils/agents-installer.js +27 -201
- package/src/utils/hooks-installer.js +11 -95
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +0 -23
- package/framework/hooks/claude-code/statusline.py +0 -699
- package/framework/hooks/claude-code/statusline.sh +0 -11
- package/framework/hooks/shared/agent-reap.js +0 -74
- package/framework/hooks/shared/slice-progress.js +0 -110
- package/framework/skills/core/morph-hotfix/SKILL.md +0 -133
- package/framework/skills/core/morph-machine/SKILL.md +0 -46
- package/framework/skills/core/morph-review/SKILL.md +0 -129
- package/framework/skills/core/morph-standards/SKILL.md +0 -52
- package/framework/standards/PROVENANCE.json +0 -119
- package/src/commands/project/worktree-reap.js +0 -34
- package/src/commands/state/handoff.js +0 -65
- package/src/commands/validation/mutate.js +0 -263
- package/src/commands/validation/scout-check.js +0 -28
- package/src/lib/generators/handoff-generator.js +0 -172
- package/src/lib/mutation/adapters.js +0 -79
- package/src/lib/mutation/commands.js +0 -61
- package/src/lib/mutation/dotnet-introspection.js +0 -281
- package/src/lib/mutation/manifest.js +0 -144
- package/src/lib/mutation/plan.js +0 -44
- package/src/lib/mutation/report.js +0 -68
- package/src/lib/mutation/runner.js +0 -301
- package/src/lib/mutation/schemata.js +0 -60
- package/src/lib/mutation/snapshot.js +0 -65
- package/src/lib/mutation/subset.js +0 -109
- package/src/lib/mutation/trx.js +0 -50
- package/src/lib/scout/scout-check.js +0 -293
- package/src/lib/validators/traceability-check.js +0 -103
- 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)
|
|
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,
|
|
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 {
|
|
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
|
|
27
|
-
feito: { label: '
|
|
28
|
-
|
|
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.
|
|
71
|
-
const blocked = f.
|
|
72
|
-
if (blocked) return `${blocked.id} está
|
|
73
|
-
const open = f.
|
|
74
|
-
if (open) return `${f.
|
|
75
|
-
return '
|
|
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
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
|
334
|
-
|
|
335
|
-
{
|
|
336
|
-
{
|
|
337
|
-
{
|
|
338
|
-
]
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
|
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.
|
|
387
|
-
return <Panel><p className="text-sm text-[var(--color-dim)]">
|
|
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
|
|
390
|
-
const
|
|
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-
|
|
393
|
-
<
|
|
394
|
-
<
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
<
|
|
402
|
-
{
|
|
403
|
-
<
|
|
404
|
-
|
|
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
|
-
</
|
|
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,
|
|
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.
|
|
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 === '
|
|
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
|
-
|
|
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
|
|
44
|
-
if (!
|
|
45
|
-
const pct = Math.round((
|
|
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={`${
|
|
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)]">{
|
|
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
|
-
<
|
|
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"><
|
|
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
|
|
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
|
};
|