@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
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
// Judged from observed evidence, never the agent's word: did the WORK land, independently of
|
|
2
|
-
// the REPORT? The `tree` block is information only and never changes the verdict.
|
|
3
|
-
|
|
4
1
|
import chalk from 'chalk';
|
|
5
|
-
import { existsSync, readFileSync
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
6
3
|
import { join } from 'path';
|
|
7
|
-
import {
|
|
8
|
-
import { sliceReportPath, inspectReport, REPORT_MARKERS } from '../../../framework/hooks/shared/slice-progress.js';
|
|
9
|
-
import { loadTelemetryAllTrees, dedupeEvents } from '../../lib/telemetry/telemetry-reader.js';
|
|
4
|
+
import { implementProgress, parseReport, reportPath } from '../../../framework/hooks/shared/report-progress.js';
|
|
10
5
|
import { treeState, snapshotTree } from '../../lib/git/tree-state.js';
|
|
11
6
|
import { atomicWriteJson } from '../../core/state/state-manager.js';
|
|
12
7
|
import { minimatch } from 'minimatch';
|
|
@@ -16,9 +11,9 @@ import { changedPathsAgainst, detectDefaultBranch } from '../../lib/git/default-
|
|
|
16
11
|
import { readMorphConfig } from '../../lib/tasks/test-runner.js';
|
|
17
12
|
import { readPlanChecks } from '../../lib/plan/checks.js';
|
|
18
13
|
import { isTestPath } from '../../lib/graph/sanitize.js';
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
|
|
15
|
+
import { collectReviewRequirement } from './gate-check.js';
|
|
16
|
+
import { featureGeneratesMedia } from '../../lib/plan/media.js';
|
|
22
17
|
|
|
23
18
|
/** Each review report: complete when it carries a readable verdict and is not partial. */
|
|
24
19
|
export function inspectReviewReports(cwd, feature) {
|
|
@@ -31,8 +26,12 @@ export function inspectReviewReports(cwd, feature) {
|
|
|
31
26
|
return out;
|
|
32
27
|
}
|
|
33
28
|
|
|
29
|
+
function readOrEmpty(path) {
|
|
30
|
+
try { return existsSync(path) ? readFileSync(path, 'utf8') : ''; } catch { return ''; }
|
|
31
|
+
}
|
|
32
|
+
|
|
34
33
|
// The verifier routes to opus on prompt/model or media; security is dispatched only when required.
|
|
35
|
-
function reviewDispatch(cwd, feature
|
|
34
|
+
function reviewDispatch(cwd, feature) {
|
|
36
35
|
let changedFiles = [];
|
|
37
36
|
try { changedFiles = changedPathsAgainst(detectDefaultBranch(cwd), cwd); } catch { changedFiles = []; }
|
|
38
37
|
const watch = readMorphConfig(cwd)?.project?.evals?.watch;
|
|
@@ -44,172 +43,45 @@ function reviewDispatch(cwd, feature, slices) {
|
|
|
44
43
|
required,
|
|
45
44
|
verifier: verifierModelFor({
|
|
46
45
|
promptModelTouched: changedFiles.some((f) => globs.some((g) => minimatch(f, g))),
|
|
47
|
-
generatesMedia: featureGeneratesMedia(
|
|
46
|
+
generatesMedia: featureGeneratesMedia(readOrEmpty(getAbsoluteOutputPath(cwd, feature, 'plan'))),
|
|
48
47
|
}),
|
|
49
48
|
reviewer: { model: 'sonnet', reasons: [] },
|
|
50
49
|
security: { required: security.required, model: 'opus', reasons: security.reasons },
|
|
51
50
|
};
|
|
52
51
|
}
|
|
53
52
|
|
|
54
|
-
// Two clocks: the report mtime comes from the filesystem, the event `ts` from the hook. Two
|
|
55
|
-
// seconds absorb their skew and are far less than any real work done after reporting.
|
|
56
|
-
export const STALE_TOLERANCE_MS = 2000;
|
|
57
|
-
|
|
58
53
|
const slash = (p) => String(p || '').replace(/\\/g, '/');
|
|
59
|
-
|
|
60
|
-
function toTreeRelative(target, treePath) {
|
|
61
|
-
const norm = slash(target);
|
|
62
|
-
const root = slash(treePath).replace(/\/+$/, '');
|
|
63
|
-
if (!root) return norm;
|
|
64
|
-
// Case-insensitive prefix: on Windows the same root arrives as `R:/repo` or `r:/repo`.
|
|
65
|
-
return norm.toLowerCase().startsWith(`${root.toLowerCase()}/`) ? norm.slice(root.length + 1) : norm;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const reportSuffix = (feature, sliceId) => `.morph/features/${feature}/3-implement/reports/${sliceId}.md`.toLowerCase();
|
|
69
|
-
|
|
70
|
-
function loadWrites(cwd) {
|
|
71
|
-
let trees = [];
|
|
72
|
-
try { trees = loadTelemetryAllTrees(cwd); } catch { trees = []; }
|
|
73
|
-
const events = [];
|
|
74
|
-
for (const tree of trees) {
|
|
75
|
-
for (const e of tree.events) {
|
|
76
|
-
if (e?.tool !== 'Write' && e?.tool !== 'Edit') continue;
|
|
77
|
-
if (isFailedToolCall(e)) continue;
|
|
78
|
-
const ts = Date.parse(e?.ts ?? '');
|
|
79
|
-
if (!Number.isFinite(ts)) continue;
|
|
80
|
-
events.push({ ...e, rel: toTreeRelative(e.target, tree.path), tsMs: ts });
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return { available: trees.some((t) => t.events.length > 0), writes: dedupeEvents(events) };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// The owning dispatch is the latest sub-agent write of the report; a later orchestrator edit
|
|
87
|
-
// does not hide it. Without a report, any write to one of the slice files says the work landed.
|
|
88
|
-
function observedEvidence(telemetry, feature, slice) {
|
|
89
|
-
if (!telemetry.available) return { available: false };
|
|
90
|
-
const suffix = reportSuffix(feature, slice.id);
|
|
91
|
-
const isReport = (w) => slash(w.target).toLowerCase().endsWith(suffix);
|
|
92
|
-
const declared = new Set((slice.files ?? []).map((f) => slash(f).replace(/^\.\//, '').toLowerCase()));
|
|
93
|
-
const isSliceFile = (w) => declared.has(w.rel.toLowerCase());
|
|
94
|
-
|
|
95
|
-
const reportWrites = telemetry.writes.filter((w) => isReport(w) && w.agentId);
|
|
96
|
-
const agentId = reportWrites.length ? reportWrites[reportWrites.length - 1].agentId : null;
|
|
97
|
-
if (!agentId) {
|
|
98
|
-
return { available: true, agentId: null, sliceWrites: telemetry.writes.filter(isSliceFile).length };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const work = telemetry.writes.filter((w) => w.agentId === agentId && !isReport(w) && !w.rel.includes('.morph/'));
|
|
102
|
-
const observed = [...new Set(work.map((w) => w.rel))];
|
|
103
|
-
const observedKeys = new Set(observed.map((f) => f.toLowerCase()));
|
|
104
|
-
return {
|
|
105
|
-
available: true,
|
|
106
|
-
agentId,
|
|
107
|
-
observed,
|
|
108
|
-
lastWriteAt: work.length ? new Date(Math.max(...work.map((w) => w.tsMs))).toISOString() : null,
|
|
109
|
-
undeclared: observed.filter((f) => !declared.has(f.toLowerCase())),
|
|
110
|
-
unobserved: (slice.files ?? []).filter((f) => !observedKeys.has(slash(f).replace(/^\.\//, '').toLowerCase())),
|
|
111
|
-
sliceWrites: work.filter(isSliceFile).length,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function isReportStale(reportFsPath, observed) {
|
|
116
|
-
if (!observed?.agentId || !observed.lastWriteAt) return false;
|
|
117
|
-
try {
|
|
118
|
-
return statSync(reportFsPath).mtimeMs + STALE_TOLERANCE_MS < Date.parse(observed.lastWriteAt);
|
|
119
|
-
} catch {
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function decide(report, observed, stale) {
|
|
125
|
-
if (report.exists && report.complete) {
|
|
126
|
-
// Structural integrity answers "is the report complete?", never "is it
|
|
127
|
-
// still true?": an agent can write it early, keep working and be cut off.
|
|
128
|
-
return stale
|
|
129
|
-
? { verdict: 'report-stale', action: 'refresh-from-evidence' }
|
|
130
|
-
: { verdict: 'ok', action: 'proceed' };
|
|
131
|
-
}
|
|
132
|
-
// PARCIAL: the agent stopped short (turn ceiling) and resumes from its last
|
|
133
|
-
// `wip` commit — completing it DEGRADADO would stamp unfinished work as done.
|
|
134
|
-
if (report.exists && report.blocked) return { verdict: 'report-blocked', action: 'ask-human' };
|
|
135
|
-
if (report.exists && report.partial) return { verdict: 'report-incomplete', action: 'resume' };
|
|
136
|
-
if (report.exists) return { verdict: 'report-incomplete', action: 'complete-degraded' };
|
|
137
|
-
if (!observed.available) return { verdict: 'report-missing-evidence-unknown', action: 'redispatch' };
|
|
138
|
-
return observed.sliceWrites > 0
|
|
139
|
-
? { verdict: 'work-landed-report-missing', action: 'accept-degraded' }
|
|
140
|
-
: { verdict: 'no-work-no-report', action: 'redispatch' };
|
|
141
|
-
}
|
|
142
|
-
|
|
143
54
|
const isTestRef = (p) => isTestPath(p) || /\.(test|spec)\.[a-z]+$|Tests?\.cs$/i.test(p);
|
|
144
55
|
|
|
145
|
-
/** Cn whose
|
|
146
|
-
export function checksSpread(
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
for (const l of lines.slice(start + 1)) {
|
|
152
|
-
if (/^\s*(\*\*|#)/.test(l)) break;
|
|
153
|
-
block.push(l);
|
|
56
|
+
/** Cn whose evidence in the report cites tests in more than one file, as `{ id, files }`. */
|
|
57
|
+
export function checksSpread(reportText) {
|
|
58
|
+
const spread = [];
|
|
59
|
+
for (const [id, { evidence }] of parseReport(reportText)) {
|
|
60
|
+
const files = [...new Set((evidence.match(/[\w@./\\-]+\.[A-Za-z]+/g) ?? []).map(slash).filter(isTestRef))];
|
|
61
|
+
if (files.length > 1) spread.push({ id, files });
|
|
154
62
|
}
|
|
155
|
-
|
|
156
|
-
let current = null;
|
|
157
|
-
for (const piece of block.join('\n').split(/[;\n]/)) {
|
|
158
|
-
const lead = /^\s*(?:[-*]\s*)?\(?(C\d+)\b/.exec(piece);
|
|
159
|
-
if (lead) current = lead[1];
|
|
160
|
-
if (!current) continue;
|
|
161
|
-
const refs = (piece.match(/[\w@.\/\\-]+\.[A-Za-z]+/g) ?? []).map(slash).filter(isTestRef);
|
|
162
|
-
if (!filesByCn.has(current)) filesByCn.set(current, new Set());
|
|
163
|
-
for (const f of refs) filesByCn.get(current).add(f);
|
|
164
|
-
}
|
|
165
|
-
return [...filesByCn].filter(([, files]) => files.size > 1).map(([id, files]) => ({ id, files: [...files] }));
|
|
63
|
+
return spread;
|
|
166
64
|
}
|
|
167
65
|
|
|
168
|
-
function spreadWarnings(fsPath) {
|
|
169
|
-
let body = '';
|
|
170
|
-
try { body = readFileSync(fsPath, 'utf8'); } catch { return []; }
|
|
171
|
-
return checksSpread(body).map(({ id, files }) =>
|
|
172
|
-
`${id} cita testes em ${files.length} arquivos (${files.join(', ')}): um teste por Cn, na camada mais baixa que o prova`);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function checkSlice(cwd, feature, slice, telemetry) {
|
|
176
|
-
const fsPath = sliceReportPath(join(cwd, '.morph', 'features', feature), slice.id);
|
|
177
|
-
const report = inspectReport(fsPath, { covers: slice.covers ?? [] });
|
|
178
|
-
const observed = observedEvidence(telemetry, feature, slice);
|
|
179
|
-
const stale = report.exists && isReportStale(fsPath, observed);
|
|
180
|
-
const warnings = report.exists ? spreadWarnings(fsPath) : [];
|
|
181
|
-
return { slice: slice.id, report, observed, warnings, ...decide(report, observed, stale) };
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const ACTION_HINT = {
|
|
185
|
-
proceed: 'relatório íntegro — siga o fluxo normal.',
|
|
186
|
-
'refresh-from-evidence': 'o relatório está COMPLETO mas VELHO: o agente da fatia seguiu escrevendo arquivos depois de gravá-lo, então ele descreve um estado já ultrapassado. NÃO re-despache e não confie no texto — confira os arquivos em `observed` (evidência real, não auto-declarada) e atualize o relatório a partir deles.',
|
|
187
|
-
'complete-degraded': 'complete os itens faltantes a partir da mensagem de retorno do sub-agent, carimbe o relatório como DEGRADADO e registre nele a falha de protocolo.',
|
|
188
|
-
resume: 'relatório PARCIAL: o sub-agent parou antes do fim (limite de turnos) e a fatia NÃO está pronta. Não carimbe DEGRADADO — retome o MESMO agente por `SendMessage` (cole no recado `tree.dirty` e `tree.commits` desta saída e o que falta) ou re-despache só o que falta, a partir do último commit `wip`.',
|
|
189
|
-
'ask-human': 'relatório BLOQUEADO: o sub-agent parou numa decisão que não é dele (requisito que contradiz o plano ou um ADR, escolha do humano) e o relatório traz a pergunta e as opções. NÃO re-despache — a mesma pergunta volta. Se o plano ou um ADR já decide, decida você e registre no relatório; senão, faça a pergunta por `AskUserQuestion`. Depois retome o MESMO agente por `SendMessage` com a resposta.',
|
|
190
|
-
'accept-degraded':'o TRABALHO entrou — não re-despache. Grave você mesmo o relatório da fatia a partir da mensagem de retorno, carimbado como DEGRADADO, com a falha de protocolo registrada nele.',
|
|
191
|
-
redispatch: 're-despache a fatia UMA vez com o mesmo prompt. Segunda ausência sem trabalho entregue = falha real: escale ao humano.',
|
|
192
|
-
};
|
|
193
|
-
|
|
194
66
|
function snapshotPath(cwd, feature) {
|
|
195
67
|
return join(cwd, '.morph', 'features', feature, '4-review', 'tree-before-eval.json');
|
|
196
68
|
}
|
|
197
69
|
|
|
198
70
|
// `beforeEval` snapshots after comparing, so the output still shows what changed since the last one.
|
|
199
|
-
function treeFor(cwd, feature, beforeEval
|
|
71
|
+
function treeFor(cwd, feature, beforeEval) {
|
|
200
72
|
const path = snapshotPath(cwd, feature);
|
|
201
73
|
let snapshot = null;
|
|
202
74
|
try { if (existsSync(path)) snapshot = JSON.parse(readFileSync(path, 'utf8')); } catch { snapshot = null; }
|
|
203
75
|
const tree = treeState(cwd, { snapshot });
|
|
204
76
|
if (!beforeEval) return { tree };
|
|
205
|
-
const reviewModels = reviewDispatch(cwd, feature
|
|
77
|
+
const reviewModels = reviewDispatch(cwd, feature);
|
|
206
78
|
const next = snapshotTree(cwd);
|
|
207
79
|
if (!next) return { tree, beforeEval: null, reviewModels };
|
|
208
80
|
atomicWriteJson(path, next);
|
|
209
81
|
return { tree, beforeEval: { at: next.at, files: Object.keys(next.files).length }, reviewModels };
|
|
210
82
|
}
|
|
211
83
|
|
|
212
|
-
export function reportCheckCommand(feature,
|
|
84
|
+
export function reportCheckCommand(feature, options = {}) {
|
|
213
85
|
const cwd = options.cwd || process.cwd();
|
|
214
86
|
const emit = (obj, code) => {
|
|
215
87
|
if (options.json) console.log(JSON.stringify(obj, null, 2));
|
|
@@ -221,20 +93,15 @@ export function reportCheckCommand(feature, sliceIds, options = {}) {
|
|
|
221
93
|
if (!checks) {
|
|
222
94
|
emit({ error: `feature "${feature}" sem 2-plan/checks.json legível (${status}) — rode morph-spec plan-check ${feature}`, feature }, 1);
|
|
223
95
|
}
|
|
224
|
-
const
|
|
225
|
-
const
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
return { ...r, hint: ACTION_HINT[r.action] };
|
|
234
|
-
});
|
|
235
|
-
const clean = results.every((r) => r.verdict === 'ok');
|
|
236
|
-
const reviews = ids.length ? undefined : inspectReviewReports(cwd, feature);
|
|
237
|
-
emit({ feature, results, ...(reviews ? { reviews } : {}), clean, ...treeFor(cwd, feature, options.beforeEval, slices) }, clean ? 0 : 1);
|
|
96
|
+
const featureDir = join(cwd, '.morph', 'features', feature);
|
|
97
|
+
const progress = implementProgress(featureDir);
|
|
98
|
+
const warnings = checksSpread(readOrEmpty(reportPath(featureDir))).map(({ id, files }) =>
|
|
99
|
+
`${id} cita testes em ${files.length} arquivos (${files.join(', ')}): um teste por Cn, na camada mais baixa que o prova`);
|
|
100
|
+
const implement = progress
|
|
101
|
+
? { total: progress.total, done: progress.done, pending: progress.checks.filter((c) => c.status === 'pendente').map((c) => c.id),
|
|
102
|
+
blocked: progress.checks.filter((c) => c.status === 'bloqueado').map((c) => c.id), legacy: progress.legacy, warnings }
|
|
103
|
+
: null;
|
|
104
|
+
emit({ feature, implement, reviews: inspectReviewReports(cwd, feature), ...treeFor(cwd, feature, options.beforeEval) }, 0);
|
|
238
105
|
}
|
|
239
106
|
|
|
240
107
|
function printHuman(obj) {
|
|
@@ -243,18 +110,12 @@ function printHuman(obj) {
|
|
|
243
110
|
return;
|
|
244
111
|
}
|
|
245
112
|
console.log(chalk.bold(`\n📄 report-check ${obj.feature}`));
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (r.report.exists && r.report.missing.length) {
|
|
253
|
-
console.log(chalk.gray(` faltando no relatório: ${r.report.missing.join(', ')}`));
|
|
254
|
-
}
|
|
255
|
-
console.log(chalk.gray(` → ${r.hint}`));
|
|
256
|
-
}
|
|
257
|
-
for (const w of r.warnings ?? []) console.log(chalk.yellow(` ⚠ ${w}`));
|
|
113
|
+
const impl = obj.implement;
|
|
114
|
+
if (impl) {
|
|
115
|
+
const open = [...impl.pending, ...impl.blocked];
|
|
116
|
+
console.log(` ${open.length ? chalk.yellow('⚠') : chalk.green('✓')} report.md ${impl.done}/${impl.total} cenários feitos`
|
|
117
|
+
+ `${impl.pending.length ? ` · pendentes: ${impl.pending.join(', ')}` : ''}${impl.blocked.length ? ` · bloqueados: ${impl.blocked.join(', ')}` : ''}`);
|
|
118
|
+
for (const w of impl.warnings) console.log(chalk.yellow(` ⚠ ${w}`));
|
|
258
119
|
}
|
|
259
120
|
for (const [role, r] of Object.entries(obj.reviews ?? {})) {
|
|
260
121
|
if (!r.exists && role === 'security') continue;
|
|
@@ -280,9 +141,6 @@ function printTree(tree) {
|
|
|
280
141
|
console.log(` árvore ${tree.branch ?? '-'}@${tree.head ?? '-'}: ${tree.dirtyCount} arquivo(s) não commitado(s) fora de .morph/${stat}`);
|
|
281
142
|
for (const d of tree.dirty.slice(0, 10)) console.log(chalk.gray(` ${d.status.padEnd(2)} ${d.path}`));
|
|
282
143
|
if (tree.dirtyCount > 10) console.log(chalk.gray(` … e mais ${tree.dirtyCount - 10}`));
|
|
283
|
-
if (tree.markers.length) {
|
|
284
|
-
console.log(chalk.yellow(` ⚠ marcador de mutação esquecido: ${tree.markers.map((m) => `${m.file}:${m.line}`).join(', ')}`));
|
|
285
|
-
}
|
|
286
144
|
if (tree.commits?.length) console.log(chalk.gray(` commits sobre a default: ${tree.commits.join(' · ')}`));
|
|
287
145
|
const since = tree.sinceBeforeEval;
|
|
288
146
|
if (since) {
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { atomicWriteJson } from '../../core/state/state-manager.js';
|
|
5
5
|
import { isGateApproved } from '../../../framework/hooks/shared/state-reader.js';
|
|
6
6
|
import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
|
|
7
7
|
import { parsePlan } from '../../lib/plan/plan-parser.js';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { renderPlanHtml } from '../../lib/plan/plan-html.js';
|
|
9
|
+
import { buildChecks, planHash, planRuleOptions, validatePlan } from '../../lib/plan/checks.js';
|
|
10
10
|
import { revocationWarnings } from '../../lib/specs/adr-index.js';
|
|
11
11
|
import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
|
|
12
12
|
import { seedDotnetBuildIgnores } from '../../lib/stack/dotnet-build-ignores.js';
|
|
13
13
|
import { reportDotnetIgnores } from '../state/advance.js';
|
|
14
|
+
import { syncStackRules } from '../../lib/installers/stack-sync.js';
|
|
14
15
|
|
|
15
16
|
function readJson(path) {
|
|
16
17
|
try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; }
|
|
@@ -21,17 +22,14 @@ export function runPlanCheck(featureName, cwd = process.cwd()) {
|
|
|
21
22
|
const planPath = getAbsoluteOutputPath(cwd, featureName, 'plan');
|
|
22
23
|
const checksPath = getAbsoluteOutputPath(cwd, featureName, 'checks');
|
|
23
24
|
const base = { feature: featureName, plan: planPath, checksPath };
|
|
24
|
-
if (!existsSync(planPath)) return { ...base, ok: false, exit: 2, error: 'plan-not-found', errors: [], warnings: []
|
|
25
|
+
if (!existsSync(planPath)) return { ...base, ok: false, exit: 2, error: 'plan-not-found', errors: [], warnings: [] };
|
|
25
26
|
|
|
26
27
|
const text = readFileSync(planPath, 'utf8');
|
|
27
28
|
const plan = parsePlan(text);
|
|
28
|
-
const { errors, warnings } = validatePlan(plan, {
|
|
29
|
-
personas: knownPersonas(cwd), surfaces: knownSurfaces(cwd), ...planRuleOptions(cwd, featureName),
|
|
30
|
-
});
|
|
29
|
+
const { errors, warnings } = validatePlan(plan, { ...planRuleOptions(cwd, featureName), text });
|
|
31
30
|
warnings.push(...revocationWarnings(cwd, plan));
|
|
32
|
-
const groups = errors.length === 0 ? parallelGroups(plan.slices) : [];
|
|
33
31
|
const hash = planHash(text);
|
|
34
|
-
const result = { ...base, planHash: hash, errors, warnings,
|
|
32
|
+
const result = { ...base, planHash: hash, errors, warnings, written: false };
|
|
35
33
|
if (errors.length > 0) return { ...result, ok: false, exit: 1 };
|
|
36
34
|
|
|
37
35
|
const approved = isGateApproved(featureName, 'plan', cwd);
|
|
@@ -41,7 +39,9 @@ export function runPlanCheck(featureName, cwd = process.cwd()) {
|
|
|
41
39
|
}
|
|
42
40
|
|
|
43
41
|
atomicWriteJson(checksPath, buildChecks(featureName, text, plan));
|
|
44
|
-
|
|
42
|
+
const viewPath = getAbsoluteOutputPath(cwd, featureName, 'planView');
|
|
43
|
+
writeFileSync(viewPath, renderPlanHtml(featureName, plan, { planHash: hash }));
|
|
44
|
+
return { ...result, ok: true, exit: 0, written: true, viewPath };
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export function planCheckCommand(featureName, options = {}) {
|
|
@@ -54,8 +54,13 @@ export function planCheckCommand(featureName, options = {}) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
const result = runPlanCheck(featureName);
|
|
57
|
+
// After the plan validates: in a greenfield project the plan is where the stack is born.
|
|
58
|
+
const stackSync = result.written
|
|
59
|
+
? syncStackRules(process.cwd(), { log: (line) => console.error(chalk.gray(line)) })
|
|
60
|
+
: null;
|
|
57
61
|
const gitignoreAdded = result.written ? seedDotnetBuildIgnores(process.cwd(), { featureName }) : [];
|
|
58
62
|
if (options.json) {
|
|
63
|
+
if (stackSync?.added.length) result.rulesAdded = stackSync.added;
|
|
59
64
|
if (gitignoreAdded.length) result.gitignoreAdded = gitignoreAdded;
|
|
60
65
|
if (gitignoreAdded.skipped) result.gitignoreSkipped = gitignoreAdded.skipped;
|
|
61
66
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -77,7 +82,7 @@ export function planCheckCommand(featureName, options = {}) {
|
|
|
77
82
|
console.log(chalk.red(`✗ plan-check: ${result.errors.length} erro(s) em ${result.plan} — checks.json não foi gravado`));
|
|
78
83
|
} else {
|
|
79
84
|
console.log(chalk.green(`✓ plan-check: ${join('2-plan', 'checks.json')} gravado (${result.planHash.slice(0, 12)})`));
|
|
80
|
-
|
|
85
|
+
console.log(chalk.gray(` visão do plano: ${result.viewPath}`));
|
|
81
86
|
reportDotnetIgnores(gitignoreAdded);
|
|
82
87
|
}
|
|
83
88
|
process.exit(result.exit);
|
|
@@ -2,7 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { parsePlan } from '../../lib/plan/plan-parser.js';
|
|
5
|
-
import {
|
|
5
|
+
import { planHash, planRuleOptions, validatePlan } from '../../lib/plan/checks.js';
|
|
6
6
|
|
|
7
7
|
// Matched against section markers (headings and line-start bold labels), never whole-file
|
|
8
8
|
// substrings. required → ERROR, recommended → WARNING, optional → never penalized; an array is any-of.
|
|
@@ -117,7 +117,7 @@ function validatePlanOutput(content, planDir, result) {
|
|
|
117
117
|
const plan = parsePlan(content);
|
|
118
118
|
const root = path.resolve(planDir, '..', '..', '..', '..');
|
|
119
119
|
const feature = path.basename(path.resolve(planDir, '..'));
|
|
120
|
-
const { errors, warnings } = validatePlan(plan, {
|
|
120
|
+
const { errors, warnings } = validatePlan(plan, { ...planRuleOptions(root, feature), text: content });
|
|
121
121
|
result.errors.push(...errors);
|
|
122
122
|
result.warnings.push(...warnings);
|
|
123
123
|
result.sections.found.push(...plan.sections.filter((x) => x.key).map((x) => x.name));
|
|
@@ -137,7 +137,7 @@ function validateChecksOutput(content, planDir, result) {
|
|
|
137
137
|
result.errors.push('checks.json is stale — plan.md changed after it was generated; run `morph-spec plan-check`');
|
|
138
138
|
return;
|
|
139
139
|
}
|
|
140
|
-
result.sections.found.push(`${doc.checks?.length ?? 0} check(s)
|
|
140
|
+
result.sections.found.push(`${doc.checks?.length ?? 0} check(s)`);
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
function validateMarkdownSections(content, sections, result) {
|
|
@@ -14,6 +14,7 @@ import { afterHeavyRun } from '../../lib/heavy/cleanup.js';
|
|
|
14
14
|
import { memoryBusyHint } from '../../lib/heavy/live-stacks.js';
|
|
15
15
|
import { readHeavyConfig } from '../../../framework/hooks/shared/heavy-config.js';
|
|
16
16
|
import { runCiVerify, CiUsageError, reusableLocalStamp } from '../../lib/ci/ci-verify.js';
|
|
17
|
+
import { syncStackRules } from '../../lib/installers/stack-sync.js';
|
|
17
18
|
|
|
18
19
|
function statusIcon(status) {
|
|
19
20
|
if (status === 'pass') return chalk.green('✓');
|
|
@@ -57,12 +58,6 @@ function formatVerifyResult(result) {
|
|
|
57
58
|
for (const line of result.feedback) lines.push(` ${line}`);
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
if (result.traceability?.warnings?.length > 0) {
|
|
61
|
-
lines.push('');
|
|
62
|
-
lines.push(chalk.yellow('Traceability warnings (advisory — never fails verify):'));
|
|
63
|
-
for (const w of result.traceability.warnings) lines.push(` ⚠ ${w}`);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
61
|
lines.push('');
|
|
67
62
|
return lines.join('\n');
|
|
68
63
|
}
|
|
@@ -180,6 +175,8 @@ export async function verifyCommand(featureName, options = {}) {
|
|
|
180
175
|
process.exit(2);
|
|
181
176
|
}
|
|
182
177
|
|
|
178
|
+
syncStackRules(process.cwd(), { persistTags: false, log: (line) => console.error(chalk.gray(line)) });
|
|
179
|
+
|
|
183
180
|
if (options.ci) {
|
|
184
181
|
const conflict = CI_EXCLUSIVE_FLAGS.find(([flag]) => options[flag])?.[1];
|
|
185
182
|
if (conflict) {
|
|
@@ -3,12 +3,12 @@ import { join } from 'path';
|
|
|
3
3
|
export const OUTPUT_SCHEMA = {
|
|
4
4
|
plan: { filename: 'plan.md', phaseDir: '2-plan', phase: 'plan', protected: true, approvalGate: 'plan' },
|
|
5
5
|
checks: { filename: 'checks.json', phaseDir: '2-plan', phase: 'plan', protected: true, approvalGate: 'plan' },
|
|
6
|
+
planCritique: { filename: 'critica.md', phaseDir: '2-plan', phase: 'plan', protected: false },
|
|
7
|
+
planView: { filename: 'plan.html', phaseDir: '2-plan', phase: 'plan', protected: false },
|
|
6
8
|
|
|
7
|
-
uiDesignSystem: { filename: 'design-system.md', phaseDir: '1-design', phase: 'uiux', protected: true, approvalGate: 'uiux' },
|
|
8
9
|
uiMockups: { filename: 'mockups.md', phaseDir: '1-design', phase: 'uiux', protected: true, approvalGate: 'uiux' },
|
|
9
|
-
uiComponents: { filename: 'components.md', phaseDir: '1-design', phase: 'uiux', protected: true, approvalGate: 'uiux' },
|
|
10
|
-
uiFlows: { filename: 'flows.md', phaseDir: '1-design', phase: 'uiux', protected: true, approvalGate: 'uiux' },
|
|
11
10
|
|
|
11
|
+
implementReport: { filename: 'report.md', phaseDir: '3-implement', phase: 'implement', protected: false },
|
|
12
12
|
evidencias: { dirname: 'evidencias', filenamePattern: '^C\\d+\\.md$', displayFilename: '{Cn}.md',
|
|
13
13
|
phaseDir: '3-implement', phase: 'implement', protected: false, multi: true },
|
|
14
14
|
|
|
@@ -450,10 +450,8 @@ export async function markOutput(featureName, outputType) {
|
|
|
450
450
|
if (suggestion) {
|
|
451
451
|
errorMsg += `\n\nDid you mean '${suggestion}'?`;
|
|
452
452
|
const kebabAlternative = {
|
|
453
|
-
'uiDesignSystem': 'ui-design-system',
|
|
454
453
|
'uiMockups': 'ui-mockups',
|
|
455
|
-
'
|
|
456
|
-
'uiFlows': 'ui-flows'
|
|
454
|
+
'implementReport': 'implement-report'
|
|
457
455
|
}[suggestion];
|
|
458
456
|
if (kebabAlternative) {
|
|
459
457
|
errorMsg += ` (also accepts '${kebabAlternative}')`;
|
|
@@ -462,7 +460,7 @@ export async function markOutput(featureName, outputType) {
|
|
|
462
460
|
|
|
463
461
|
errorMsg += `\n\nValid types:\n`;
|
|
464
462
|
errorMsg += ` ${validCamelCaseTypes.join(', ')}\n`;
|
|
465
|
-
errorMsg += `\nNote: kebab-case is also accepted (e.g., 'ui-
|
|
463
|
+
errorMsg += `\nNote: kebab-case is also accepted (e.g., 'ui-mockups', 'implement-report')`;
|
|
466
464
|
|
|
467
465
|
throw new Error(errorMsg);
|
|
468
466
|
}
|
|
@@ -8,7 +8,7 @@ import { useMorphQuery } from '../hooks/useMorphQuery.js';
|
|
|
8
8
|
import { apiBase, projectHref, featureHref } from '../lib/project.js';
|
|
9
9
|
|
|
10
10
|
const KIND_LABEL = {
|
|
11
|
-
page: 'Página', project: 'Projeto', feature: 'Feature',
|
|
11
|
+
page: 'Página', project: 'Projeto', feature: 'Feature', skill: 'Skill', persona: 'Persona',
|
|
12
12
|
standard: 'Standard', rule: 'Rule', hook: 'Hook', guide: 'Guia',
|
|
13
13
|
};
|
|
14
14
|
|
|
@@ -46,9 +46,6 @@ function featureIndex(project, features, projectName = null) {
|
|
|
46
46
|
const where = (text) => (projectName ? (text ? `${projectName}: ${text}` : projectName) : text);
|
|
47
47
|
for (const f of features || []) {
|
|
48
48
|
out.push({ kind: 'feature', title: f.name, hint: where(f.description), to: featureHref(project, f.name), hay: `${f.name} ${f.description || ''} ${projectName || ''}` });
|
|
49
|
-
for (const sl of f.sliceIndex || []) {
|
|
50
|
-
out.push({ kind: 'slice', title: `${sl.id} ${sl.title}`, hint: where(f.name), to: featureHref(project, f.name, '?tab=slices'), hay: `${sl.id} ${sl.title} ${f.name} ${projectName || ''}` });
|
|
51
|
-
}
|
|
52
49
|
}
|
|
53
50
|
return out;
|
|
54
51
|
}
|
|
@@ -112,7 +109,7 @@ export default function CommandPalette({ open, onClose, portfolio, project }) {
|
|
|
112
109
|
value={query}
|
|
113
110
|
onChange={(e) => setQuery(e.target.value)}
|
|
114
111
|
onKeyDown={onKey}
|
|
115
|
-
placeholder="Buscar features,
|
|
112
|
+
placeholder="Buscar features, skills, personas, standards, seções do guia…"
|
|
116
113
|
className="w-full bg-transparent text-base text-[var(--color-fg)] outline-none focus-visible:outline-none placeholder:text-[var(--color-faint)]"
|
|
117
114
|
aria-controls="palette-results"
|
|
118
115
|
aria-activedescendant={results[active] ? `palette-${active}` : undefined}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { useMorphQuery } from '../hooks/useMorphQuery.js';
|
|
2
|
+
import { apiBase } from '../lib/project.js';
|
|
3
|
+
|
|
4
|
+
// Validated with the dataviz palette checker against --color-core (#12151b), dark mode.
|
|
5
|
+
const SESSION = '#26a69a';
|
|
6
|
+
const HUMAN = '#d07f27';
|
|
7
|
+
// Not a series: pauses logged before the resolver existed, so nobody knows who cleared them.
|
|
8
|
+
const UNKNOWN = 'var(--color-gray-500)';
|
|
9
|
+
const GATE_ORDER = ['plan', 'uiux', 'review'];
|
|
10
|
+
const GATE_LABEL = { plan: 'Plano', uiux: 'Design', review: 'Gate 3' };
|
|
11
|
+
|
|
12
|
+
function split(s = {}) {
|
|
13
|
+
const unknown = s.pausedUnattributed ?? 0;
|
|
14
|
+
const human = Math.max(0, (s.pausedForHuman ?? 0) - unknown);
|
|
15
|
+
return { human, unknown, session: Math.max(0, (s.paused ?? 0) - human - unknown) };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function gateRows(gates = {}) {
|
|
19
|
+
const known = GATE_ORDER.filter((g) => gates[g]);
|
|
20
|
+
const extra = Object.keys(gates).filter((g) => !GATE_ORDER.includes(g)).sort();
|
|
21
|
+
return [...known, ...extra].map((g) => ({ gate: g, label: GATE_LABEL[g] ?? g, decisions: gates[g].decisions ?? 0, ...split(gates[g]) }));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function humanSignals(gates = {}, top = 5) {
|
|
25
|
+
return Object.entries(gates)
|
|
26
|
+
.flatMap(([gate, slot]) => Object.entries(slot.bySignal ?? {}).map(([signal, s]) => ({ gate, signal, ...split(s) })))
|
|
27
|
+
.filter((r) => r.human > 0 || r.unknown > 0)
|
|
28
|
+
.sort((a, b) => b.human - a.human || b.unknown - a.unknown || a.signal.localeCompare(b.signal))
|
|
29
|
+
.slice(0, top);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function Swatch({ color, children }) {
|
|
33
|
+
return <span className="flex items-center gap-1.5"><span className="h-2.5 w-2.5 rounded-sm" style={{ background: color }} />{children}</span>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function Bar({ row, max }) {
|
|
37
|
+
const parts = [
|
|
38
|
+
['session', row.session, SESSION, 'a sessão resolveu'],
|
|
39
|
+
['human', row.human, HUMAN, 'foi para você'],
|
|
40
|
+
['unknown', row.unknown, UNKNOWN, 'antes do registro'],
|
|
41
|
+
].filter(([, n]) => n > 0);
|
|
42
|
+
const total = row.session + row.human + row.unknown;
|
|
43
|
+
const describe = parts.map(([, n, , what]) => `${what} ${n}`).join(', ');
|
|
44
|
+
return (
|
|
45
|
+
<div className="grid grid-cols-[4.5rem_1fr_auto] items-center gap-3">
|
|
46
|
+
<span className="text-sm text-[var(--color-fg)]">{row.label}</span>
|
|
47
|
+
<div className="flex h-3 gap-[2px]" role="img" aria-label={`${row.label}: ${describe || 'nenhuma pausa'}`}>
|
|
48
|
+
{parts.map(([key, n, color, what], i) => (
|
|
49
|
+
<span key={key} className={`h-full ${i === 0 ? 'rounded-l-[4px]' : ''} ${i === parts.length - 1 ? 'rounded-r-[4px]' : ''}`}
|
|
50
|
+
style={{ width: `${(n / max) * 100}%`, background: color }} title={`${row.label} · ${what}: ${n}`} />
|
|
51
|
+
))}
|
|
52
|
+
</div>
|
|
53
|
+
<span className="text-right text-xs tabular-nums text-[var(--color-dim)]">
|
|
54
|
+
{total === 0 ? `nenhuma pausa em ${row.decisions}` : `${row.human} de ${total} para você`}
|
|
55
|
+
</span>
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function GatePausesView({ gates }) {
|
|
61
|
+
const rows = gateRows(gates);
|
|
62
|
+
if (rows.length === 0) {
|
|
63
|
+
return <p className="text-sm text-[var(--color-faint)]">Nenhuma decisão de gate na telemetria ainda.</p>;
|
|
64
|
+
}
|
|
65
|
+
const max = Math.max(1, ...rows.map((r) => r.session + r.human + r.unknown));
|
|
66
|
+
const signals = humanSignals(gates);
|
|
67
|
+
const anyUnknown = rows.some((r) => r.unknown > 0);
|
|
68
|
+
return (
|
|
69
|
+
<div className="space-y-5">
|
|
70
|
+
<div className="flex flex-wrap items-baseline justify-between gap-2">
|
|
71
|
+
<h2 className="text-base font-semibold">Pausas por gate</h2>
|
|
72
|
+
<div className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-[var(--color-dim)]" aria-hidden="true">
|
|
73
|
+
<Swatch color={SESSION}>a sessão resolveu</Swatch>
|
|
74
|
+
<Swatch color={HUMAN}>foi para você</Swatch>
|
|
75
|
+
{anyUnknown && <Swatch color={UNKNOWN}>antes do registro</Swatch>}
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
<div className="space-y-2.5">{rows.map((r) => <Bar key={r.gate} row={r} max={max} />)}</div>
|
|
79
|
+
{anyUnknown && (
|
|
80
|
+
<p className="text-xs text-[var(--color-faint)]">Pausas gravadas antes de o morph-spec registrar quem resolve cada uma: não entram na conta de "para você".</p>
|
|
81
|
+
)}
|
|
82
|
+
{signals.length > 0 && (
|
|
83
|
+
<table className="w-full text-sm">
|
|
84
|
+
<caption className="mb-2 text-left text-xs text-[var(--color-dim)]">O que mais te chamou</caption>
|
|
85
|
+
<thead className="text-xs text-[var(--color-faint)]">
|
|
86
|
+
<tr>
|
|
87
|
+
<th className="pb-1 text-left font-normal">sinal</th>
|
|
88
|
+
<th className="pb-1 text-left font-normal">gate</th>
|
|
89
|
+
<th className="pb-1 text-right font-normal">para você</th>
|
|
90
|
+
<th className="pb-1 text-right font-normal">sessão</th>
|
|
91
|
+
{anyUnknown && <th className="pb-1 text-right font-normal">antes</th>}
|
|
92
|
+
</tr>
|
|
93
|
+
</thead>
|
|
94
|
+
<tbody>
|
|
95
|
+
{signals.map((s) => (
|
|
96
|
+
<tr key={`${s.gate}:${s.signal}`} className="border-t border-[var(--color-hair)]">
|
|
97
|
+
<td className="py-1.5 font-mono text-xs text-[var(--color-fg)]">{s.signal}</td>
|
|
98
|
+
<td className="py-1.5 text-[var(--color-dim)]">{GATE_LABEL[s.gate] ?? s.gate}</td>
|
|
99
|
+
<td className="py-1.5 text-right tabular-nums text-[var(--color-fg)]">{s.human}</td>
|
|
100
|
+
<td className="py-1.5 text-right tabular-nums text-[var(--color-dim)]">{s.session}</td>
|
|
101
|
+
{anyUnknown && <td className="py-1.5 text-right tabular-nums text-[var(--color-faint)]">{s.unknown}</td>}
|
|
102
|
+
</tr>
|
|
103
|
+
))}
|
|
104
|
+
</tbody>
|
|
105
|
+
</table>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export default function GatePauses({ project }) {
|
|
112
|
+
const { data, error } = useMorphQuery(`${apiBase(project)}/gates`, { scope: 'activity', project });
|
|
113
|
+
if (error) return <p className="text-sm text-[var(--color-bad)]">Não consegui ler os gates: {error}</p>;
|
|
114
|
+
if (!data) return <p className="text-sm text-[var(--color-faint)]">Lendo a telemetria…</p>;
|
|
115
|
+
return <GatePausesView gates={data.gates} />;
|
|
116
|
+
}
|