@polymorphism-tech/morph-spec 8.46.0 → 8.47.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 (140) hide show
  1. package/CLAUDE.md +5 -5
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +1 -4
  4. package/bin/validate.js +4 -25
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +2 -2
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +1 -1
  9. package/framework/MORPH.md +8 -8
  10. package/framework/commands/create-umbraco-lp.md +2 -0
  11. package/framework/commands/migrate-lp-to-umbraco.md +2 -0
  12. package/framework/commands/morph-archive.md +1 -1
  13. package/framework/commands/morph-hotfix.md +1 -1
  14. package/framework/commands/morph-infra.md +17 -203
  15. package/framework/commands/morph-media.md +3 -0
  16. package/framework/commands/morph-preflight.md +5 -28
  17. package/framework/commands/morph-proposal.md +2 -2
  18. package/framework/commands/morph-troubleshoot.md +2 -14
  19. package/framework/docs/CLI.md +3 -3
  20. package/framework/hooks/claude-code/core/state-sync.js +13 -1
  21. package/framework/hooks/shared/e2e-check.js +3 -4
  22. package/framework/hooks/shared/state-writer.js +1 -0
  23. package/framework/rules/ai-agents-standards.md +18 -16
  24. package/framework/rules/asaas-standards.md +3 -1
  25. package/framework/rules/csharp-standards.md +10 -8
  26. package/framework/rules/data-standards.md +5 -5
  27. package/framework/rules/dotnet-templates.md +1 -1
  28. package/framework/rules/frontend-standards.md +4 -2
  29. package/framework/rules/gsap-standards.md +10 -5
  30. package/framework/rules/hangfire-standards.md +3 -1
  31. package/framework/rules/infrastructure-standards.md +4 -2
  32. package/framework/rules/morph-workflow.md +2 -2
  33. package/framework/rules/neon-auth-standards.md +3 -1
  34. package/framework/rules/neon-standards.md +6 -4
  35. package/framework/rules/nextjs-standards.md +13 -11
  36. package/framework/rules/nextjs-templates.md +1 -1
  37. package/framework/rules/resend-standards.md +3 -1
  38. package/framework/schemas/feature.schema.json +1 -3
  39. package/framework/skills/README.md +1 -1
  40. package/framework/skills/core/create-umbraco-lp/SKILL.md +3 -3
  41. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +3 -3
  42. package/framework/skills/core/morph-apply/SKILL.md +3 -3
  43. package/framework/skills/core/morph-brainstorming/SKILL.md +3 -3
  44. package/framework/skills/core/morph-hotfix/SKILL.md +4 -4
  45. package/framework/skills/core/morph-implement/SKILL.md +3 -3
  46. package/framework/skills/core/morph-machine/SKILL.md +3 -3
  47. package/framework/skills/core/morph-media/SKILL.md +3 -3
  48. package/framework/skills/core/morph-plan/SKILL.md +3 -3
  49. package/framework/skills/core/morph-proposal/SKILL.md +3 -3
  50. package/framework/skills/core/morph-review/SKILL.md +3 -3
  51. package/framework/skills/core/morph-stack-scan/SKILL.md +3 -3
  52. package/framework/skills/core/morph-standards/SKILL.md +6 -6
  53. package/framework/skills/core/morph-uiux/SKILL.md +3 -3
  54. package/framework/standards/STANDARDS.json +59 -56
  55. package/framework/standards/ai-agents/evals-with-cache.md +1 -1
  56. package/framework/standards/ai-agents/llm-runtime-defaults.md +1 -1
  57. package/framework/standards/ai-agents/modalities-speech-to-text.md +36 -26
  58. package/framework/standards/ai-agents/prompt-sources.md +6 -4
  59. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  60. package/framework/standards/ai-agents/rag-custom-pgvector.md +67 -39
  61. package/framework/standards/ai-agents/setup.md +1 -1
  62. package/framework/standards/ai-agents/structured-output.md +1 -1
  63. package/framework/standards/ai-agents/sweet-spot.md +1 -1
  64. package/framework/standards/architecture/code-comments.md +2 -2
  65. package/framework/standards/architecture/vertical-slice/vertical-slice.md +18 -9
  66. package/framework/standards/backend/api/validation.md +135 -47
  67. package/framework/standards/backend/database/postgresql/database.md +11 -7
  68. package/framework/standards/backend/database/vector-search-rag.md +207 -324
  69. package/framework/standards/backend/dotnet/fluent-validation-vsa.md +14 -8
  70. package/framework/standards/backend/dotnet/result-pattern.md +40 -6
  71. package/framework/standards/backend/dotnet/scalar-ui.md +13 -7
  72. package/framework/standards/backend/dotnet/scrutor.md +1 -1
  73. package/framework/standards/backend/dotnet/testing.md +1 -1
  74. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +7 -7
  75. package/framework/standards/backend/integrations/asaas/asaas-api.md +309 -165
  76. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +134 -154
  77. package/framework/standards/data/nosql/cache/redis.md +120 -44
  78. package/framework/standards/data/nosql/cosmos-db.md +114 -71
  79. package/framework/standards/frontend/design-system/animations.md +1 -1
  80. package/framework/standards/frontend/design-system/naming.md +1 -1
  81. package/framework/standards/frontend/nextjs/motion-patterns.md +45 -29
  82. package/framework/standards/frontend/nextjs/naming-conventions.md +1 -1
  83. package/framework/standards/infrastructure/docker/coolify-deploy.md +8 -6
  84. package/framework/standards/infrastructure/neon/neon-pgvector.md +120 -75
  85. package/framework/standards/infrastructure/neon/neon-setup.md +53 -31
  86. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +2 -2
  87. package/framework/templates/code/dotnet/extensions/database-extensions.cs +1 -1
  88. package/framework/templates/code/dotnet/extensions/result-extensions.cs +27 -0
  89. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +2 -2
  90. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +1 -1
  91. package/framework/templates/infrastructure/docker/coolify.template.json +3 -3
  92. package/framework/templates/infrastructure/github/README.md +2 -1
  93. package/framework/templates/infrastructure/github/workflows/deploy-coolify.yml.hbs +2 -1
  94. package/package.json +1 -1
  95. package/src/commands/observability/telemetry.js +3 -5
  96. package/src/commands/project/init.js +2 -1
  97. package/src/commands/project/update.js +16 -18
  98. package/src/commands/state/approve.js +17 -0
  99. package/src/commands/state/gate-check.js +4 -14
  100. package/src/commands/validation/verify.js +3 -12
  101. package/src/core/paths/output-schema.js +1 -1
  102. package/src/dashboard/app/App.jsx +0 -2
  103. package/src/dashboard/app/components/CommandPalette.jsx +2 -3
  104. package/src/dashboard/app/components/EntityLink.jsx +0 -1
  105. package/src/dashboard/app/components/Markdown.jsx +1 -1
  106. package/src/dashboard/app/components/Sidebar.jsx +1 -2
  107. package/src/dashboard/app/pages/RulesMap.jsx +3 -3
  108. package/src/dashboard/app/pages/SkillsLibrary.jsx +3 -3
  109. package/src/dashboard/scanner/activity-scanner.js +1 -1
  110. package/src/dashboard/scanner/docs-scanner.js +5 -23
  111. package/src/dashboard/scanner/features-scanner.js +1 -1
  112. package/src/dashboard/scanner/index.js +0 -2
  113. package/src/dashboard/scanner/rules-scanner.js +15 -10
  114. package/src/dashboard/scanner/skills-scanner.js +2 -3
  115. package/src/lib/evaluator-report.js +13 -0
  116. package/src/lib/risk-detector.js +8 -0
  117. package/src/lib/stack/stack-profile.js +1 -1
  118. package/src/lib/validators/reuse/reuse-validator.js +2 -3
  119. package/src/lib/validators/reuse/symbol-scanner.js +2 -4
  120. package/src/lib/validators/validation-runner.js +8 -88
  121. package/src/lib/validators/verify-runner.js +22 -72
  122. package/src/scripts/setup-infra.js +33 -22
  123. package/src/utils/agents-installer.js +20 -8
  124. package/src/utils/file-copier.js +7 -5
  125. package/framework/commands/commit.md +0 -166
  126. package/framework/rubrics/backend.json +0 -41
  127. package/framework/rubrics/default.json +0 -36
  128. package/framework/rubrics/frontend.json +0 -36
  129. package/framework/templates/project/validate.js +0 -97
  130. package/src/dashboard/app/pages/Rubrics.jsx +0 -97
  131. package/src/dashboard/scanner/rubrics-scanner.js +0 -12
  132. package/src/lib/validators/architecture/architecture-validator.js +0 -270
  133. package/src/lib/validators/architecture/index.js +0 -7
  134. package/src/lib/validators/css/css-validator.js +0 -340
  135. package/src/lib/validators/css/index.js +0 -7
  136. package/src/lib/validators/design-system/design-system-validator.js +0 -237
  137. package/src/lib/validators/design-system/index.js +0 -7
  138. package/src/lib/validators/score-aggregator.js +0 -244
  139. package/src/lib/validators/ui/index.js +0 -7
  140. package/src/lib/validators/ui/ui-contrast-validator.js +0 -422
@@ -39,7 +39,8 @@ infrastructure/github/
39
39
  - Push to container registry (GHCR or Docker Hub)
40
40
 
41
41
  ### Deploy Coolify (`deploy-coolify.yml.hbs`)
42
- - SSHes into the VPS and calls the local Coolify API (`localhost:8000/api/v1/deploy?uuid=...`) after Docker push
42
+ - SSHes into the VPS and calls the local Coolify API (`POST localhost:8000/api/v1/deploy?uuid=...`) after Docker push; the step fails unless the response carries a `deployment_uuid`
43
+ - The token is read on the VPS from `~/<token-file>` (secret `COOLIFY_TOKEN_FILE` holds the file name) and never leaves the SSH session
43
44
  - Runs health check after deployment
44
45
  - Supports staging + production environments
45
46
 
@@ -25,7 +25,8 @@ jobs:
25
25
  - name: Trigger Coolify Deploy (via SSH — the Coolify API is not publicly exposed)
26
26
  run: |
27
27
  ssh -i ~/.ssh/id_ed25519 ${{{{ secrets.COOLIFY_SSH_USER }}}}@${{{{ secrets.COOLIFY_SSH_HOST }}}} \
28
- 'T=$(cat ~/.coolify-token); curl -s -X GET -H "Authorization: Bearer $T" "http://localhost:8000/api/v1/deploy?uuid=${{{{ secrets.COOLIFY_APPLICATION_UUID }}}}"'
28
+ 'T=$(cat ~/${{{{ secrets.COOLIFY_TOKEN_FILE }}}}); curl -s --fail-with-body -X POST -H "Authorization: Bearer $T" "http://localhost:8000/api/v1/deploy?uuid=${{{{ secrets.COOLIFY_APPLICATION_UUID }}}}"' \
29
+ | grep -q deployment_uuid
29
30
 
30
31
  - name: Health Check
31
32
  run: |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polymorphism-tech/morph-spec",
3
- "version": "8.46.0",
3
+ "version": "8.47.0",
4
4
  "description": "MORPH-SPEC: NLH (Natural Language Harness) for spec-driven development with Claude Code",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -12,12 +12,10 @@
12
12
  import chalk from 'chalk';
13
13
  import { loadTelemetry, aggregateTelemetry } from '../../lib/telemetry/telemetry-reader.js';
14
14
 
15
- // The knowledge-base count reflects EXPLICIT reads only. The core standards
16
- // load via @-refs in rules (Layer 0/1 auto-load) — those are injected by
17
- // Claude Code's rules system, not via the Read tool, so no hook ever sees them.
18
- // A low count here means "few on-demand reads", NOT "standards barely used".
15
+ // A rule injects its standards' digests through Claude Code's rules system,
16
+ // not the Read tool, so no hook sees them: a low count means "few full-text reads".
19
17
  const STANDARDS_CAVEAT =
20
- 'Só reads explícitos (via Read) são observáveis; standards auto-carregados por @-ref nas rules (Layer 0/1) não passam por tool call e ficam invisíveis aqui.';
18
+ 'Só reads explícitos (via Read) são observáveis; o resumo que uma rule injeta ao tocar arquivo casado por `paths:` não passa por tool call: não observável aqui.';
21
19
 
22
20
  function section(title, rows, fmt) {
23
21
  if (!rows.length) return;
@@ -28,7 +28,7 @@ import {
28
28
  setupStitch,
29
29
  loadMcpRegistry
30
30
  } from '../../lib/installers/mcp-installer.js';
31
- import { setupInfra, installRulesForStack } from '../../scripts/setup-infra.js';
31
+ import { setupInfra, installRulesForStack, installCommandsForStack } from '../../scripts/setup-infra.js';
32
32
  import { installSkills } from '../../utils/skills-installer.js';
33
33
  import { installAgents, installDomainAgents } from '../../utils/agents-installer.js';
34
34
  import { getProjectTags } from '../../lib/stack-filter.js';
@@ -263,6 +263,7 @@ export async function initCommand(options) {
263
263
  const rulesSrc = join(fwSrcDir, 'rules');
264
264
  const rulesDest = join(targetPath, '.claude', 'rules');
265
265
  installRulesForStack(rulesSrc, rulesDest, stackTags);
266
+ installCommandsForStack(join(fwSrcDir, 'commands'), join(targetPath, '.claude', 'commands'), stackTags);
266
267
 
267
268
  // Skills
268
269
  await installSkills(targetPath, { projectTags: stackTags });
@@ -30,7 +30,7 @@ import { installSkills } from '../../utils/skills-installer.js';
30
30
  import { installAgents, installDomainAgents } from '../../utils/agents-installer.js';
31
31
  import { injectMorphImport } from '../../utils/claude-md-injector.js';
32
32
  import { getProjectTags } from '../../lib/stack-filter.js';
33
- import { updateClaudeMdAgentsBlock, installRulesForStack } from '../../scripts/setup-infra.js';
33
+ import { updateClaudeMdAgentsBlock, installRulesForStack, installCommandsForStack } from '../../scripts/setup-infra.js';
34
34
  import { reconcileLegacyTemplatesCopy } from '../../lib/installers/templates-reconcile.js';
35
35
  import { createEffects } from '../../lib/installers/effects.js';
36
36
  import { applyEditorSettings } from '../../lib/worktree/vscode-settings.js';
@@ -84,6 +84,7 @@ export function frameworkDirsToClean(morphPath, targetPath) {
84
84
  join(morphPath, 'framework', 'standards'),
85
85
  join(morphPath, 'framework', 'hooks'),
86
86
  join(morphPath, 'framework', 'schemas'),
87
+ // Não é mais instalado: fica na lista para apagar o de instalações antigas.
87
88
  join(morphPath, 'framework', 'rubrics'),
88
89
  join(morphPath, 'framework', 'evals'),
89
90
  join(morphPath, 'config'),
@@ -594,10 +595,8 @@ export async function updateCommand(options = {}) {
594
595
  fx.copyFile(agentsSrc, agentsDest);
595
596
  }
596
597
 
597
- // Update the remaining framework payloads (parity with setup-infra 7b/7c/8b/8c/8d) —
598
- // MORPH.md, phases.json, schemas/, rubrics/, evals/. Without this, upgrades left
599
- // the installed charter/rubrics/eval assets stale forever.
600
- updateSpinner.text = 'Updating charter, schemas, rubrics and evals...';
598
+ // Paridade com setup-infra 7c/8b/8c/8d: sem isto o charter e os evals instalados ficam velhos.
599
+ updateSpinner.text = 'Updating charter, schemas and evals...';
601
600
  const canonicalFrameworkDir = join(__dirname, '..', '..', '..', 'framework');
602
601
  for (const file of ['MORPH.md', 'phases.json']) {
603
602
  const src = join(canonicalFrameworkDir, file);
@@ -605,7 +604,7 @@ export async function updateCommand(options = {}) {
605
604
  fx.copyFile(src, join(morphPath, 'framework', file));
606
605
  }
607
606
  }
608
- for (const dir of ['schemas', 'rubrics', 'evals']) {
607
+ for (const dir of ['schemas', 'evals']) {
609
608
  const src = join(canonicalFrameworkDir, dir);
610
609
  if (await pathExists(src)) {
611
610
  fx.copyDir(src, join(morphPath, 'framework', dir));
@@ -644,18 +643,6 @@ export async function updateCommand(options = {}) {
644
643
  const frameworkDir = join(__dirname, '..', '..', '..', 'framework');
645
644
  const claudeDest = join(targetPath, '.claude');
646
645
 
647
- let commandsCopied = false;
648
-
649
- // Copy commands directory (slash commands): framework/commands/ → .claude/commands/
650
- const commandsSrc = join(frameworkDir, 'commands');
651
- const commandsDest = join(claudeDest, 'commands');
652
- if (await pathExists(commandsSrc)) {
653
- fx.copyDir(commandsSrc, commandsDest);
654
- commandsCopied = true;
655
- } else {
656
- logger.warn(' ⚠ framework/commands/ source missing — commands not updated');
657
- }
658
-
659
646
  // Compute project tags for stack-aware installation of skills, agents, and rules.
660
647
  // MUST come from the pre-clean backup: cleanFrameworkDirs() deleted .morph/config/
661
648
  // above and restoreUserConfig() only runs at the end — re-reading the file here
@@ -666,6 +653,17 @@ export async function updateCommand(options = {}) {
666
653
  if (configBackup) projectTags = getProjectTags(configBackup);
667
654
  } catch { /* ignore — malformed config */ }
668
655
 
656
+ let commandsCopied = false;
657
+ const commandsSrc = join(frameworkDir, 'commands');
658
+ if (await pathExists(commandsSrc)) {
659
+ const commandsDest = join(claudeDest, 'commands');
660
+ fx.ensureDir(commandsDest);
661
+ installCommandsForStack(commandsSrc, commandsDest, projectTags, fx);
662
+ commandsCopied = true;
663
+ } else {
664
+ logger.warn(' ⚠ framework/commands/ source missing — commands not updated');
665
+ }
666
+
669
667
  // Sync morph skills to .claude/skills/ for native Claude Code discovery
670
668
  updateSpinner.text = 'Syncing morph skills to .claude/skills/...';
671
669
  const skillsResult = await installSkills(targetPath, { projectTags, fx });
@@ -6,6 +6,9 @@ import { join } from 'path';
6
6
  import { getFeature, setApprovalGate, getApprovalGate, getApprovalHistory, derivePhase } from '../../core/state/state-manager.js';
7
7
  import { unmetPrereqs } from '../../core/state/gate-order.js';
8
8
  import { evaluatorReportState, VERDICT_TOKEN } from '../../lib/evaluator-report.js';
9
+ import { shouldPauseGate } from '../../lib/risk-detector.js';
10
+ import { collectSignals } from './gate-check.js';
11
+ import { readTrustFromConfig } from './gate-decision.js';
9
12
  import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
10
13
  import { uiuxAutoApprovalBlock } from '../../lib/specs/screen-checklist.js';
11
14
  import { treeMovedSince } from '../../lib/git/evidence-staleness.js';
@@ -351,6 +354,20 @@ export async function approveCommand(featureName, gateName, options = {}) {
351
354
  selfAssessed = true;
352
355
  }
353
356
 
357
+ // Sem gate-check prévio não há `gateDecisions.review` para o backstop acima morder; em
358
+ // campo, ships passaram assim com verify vermelho ou ausente. A auto-aprovação recalcula
359
+ // a decisão pela MESMA régua do gate-check. --self-assessed já dispensou o veredito.
360
+ if (approvedBy === 'auto') {
361
+ const signals = collectSignals(featureName, feature, process.cwd(), 'review');
362
+ if (selfAssessed) signals.evaluatorVerdict = { status: 'ok', verdict: 'SHIP', blockers: 0, untyped: 0 };
363
+ const { pause, reason } = shouldPauseGate({ trust: readTrustFromConfig() }, signals);
364
+ if (pause) {
365
+ console.error(chalk.red(`\n❌ Gate 3 não pode ser auto-aprovado — ${reason}.`));
366
+ console.error(chalk.gray(' Corrija e rode de novo, ou leve a decisão ao humano (--approver <nome>).\n'));
367
+ process.exit(1);
368
+ }
369
+ }
370
+
354
371
  if (report.status === 'ok' && report.superseded.length > 0) {
355
372
  console.log(chalk.yellow(`\n⚠️ O relatório do avaliador tem ${report.superseded.length + 1} vereditos (append-only).`));
356
373
  console.log(chalk.gray(` Vigente: ${report.verdict}, ${report.blockers.length} bloqueador(es). Superados: ${report.superseded.join(', ')}\n`));
@@ -3,7 +3,7 @@ import { existsSync, readFileSync } from 'fs';
3
3
  import { getFeature } from '../../core/state/state-manager.js';
4
4
  import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
5
5
  import { shouldPauseGate, getRiskSignals, classifyE2eSkip } from '../../lib/risk-detector.js';
6
- import { evaluatorReportState, evaluatorMediaSection } from '../../lib/evaluator-report.js';
6
+ import { evaluatorReportState, evaluatorMediaSection, verdictSignal } from '../../lib/evaluator-report.js';
7
7
  import { collectScreenChecklist } from '../../lib/specs/screen-checklist.js';
8
8
  import { persistGateDecision, VALID_GATES, readTrustFromConfig } from './gate-decision.js';
9
9
  import { minimatch } from 'minimatch';
@@ -151,7 +151,7 @@ function collectPromptModel(cwd, defaultBranch) {
151
151
  * @param {string} gateName
152
152
  * @returns {object}
153
153
  */
154
- function collectSignals(featureName, feature, cwd, gateName) {
154
+ export function collectSignals(featureName, feature, cwd, gateName) {
155
155
  const tasks = readTasks(featureName);
156
156
  const defaultBranch = detectDefaultBranch(cwd);
157
157
  const sha = getHeadSha(cwd);
@@ -179,17 +179,7 @@ function collectSignals(featureName, feature, cwd, gateName) {
179
179
  }
180
180
 
181
181
  if (gateName === 'review') {
182
- const report = evaluatorReportState(cwd, featureName);
183
- signals.evaluatorVerdict = report.status === 'ok'
184
- ? {
185
- status: 'ok',
186
- verdict: report.verdict,
187
- blockers: report.blockers.length,
188
- blockerIds: report.blockers.map((b) => b.id),
189
- untyped: report.untyped.length,
190
- superseded: report.superseded,
191
- }
192
- : { status: report.status };
182
+ signals.evaluatorVerdict = verdictSignal(evaluatorReportState(cwd, featureName));
193
183
  signals.mediaClaims = {
194
184
  section: evaluatorMediaSection(cwd, featureName),
195
185
  featureGeneratesMedia: featureGeneratesMedia(tasks),
@@ -269,7 +259,7 @@ export async function gateCheckCommand(featureName, gateName, options = {}) {
269
259
  process.exit(1);
270
260
  }
271
261
 
272
- // Owner-tree guard: gate-check reads workType/tasks/scores/git off cwd
262
+ // Owner-tree guard: gate-check reads workType/tasks/verdict/git off cwd
273
263
  // AND writes the resulting decision through persistGateDecision — both
274
264
  // halves are wrong from a tree that isn't this feature's own, so refuse
275
265
  // rather than compute a decision from stale reads and then fail to save it.
@@ -43,18 +43,13 @@ function selectionLines(runs = []) {
43
43
  });
44
44
  }
45
45
 
46
- /**
47
- * Human-readable rendering of a runVerify() result — mirrors the
48
- * ✓/✗-per-node + feedback-list style of formatScoreBreakdown() in
49
- * score-aggregator.js, adapted to verify's build/tests/validators nodes.
50
- */
51
46
  function formatVerifyResult(result) {
52
47
  const lines = [];
53
48
  const scopeLabel = result.scope === 'task' ? `${result.feature}/${result.task}` : result.feature;
54
49
 
55
50
  lines.push('');
56
51
  lines.push(chalk.bold(`Verify — ${scopeLabel}`));
57
- lines.push(chalk.gray(`rubric: ${result.rubric}${result.stack ? ` | stack: ${result.stack}` : ''}`));
52
+ if (result.stack) lines.push(chalk.gray(`stack: ${result.stack}`));
58
53
  lines.push('');
59
54
  lines.push(result.status === 'pass' ? chalk.green.bold('✓ PASS') : chalk.red.bold('✗ FAIL'));
60
55
  lines.push('');
@@ -69,8 +64,6 @@ function formatVerifyResult(result) {
69
64
  let detail;
70
65
  if (node.status === 'skip') {
71
66
  detail = node.reason || '';
72
- } else if (name === 'validators' && node.score != null) {
73
- detail = `score ${node.score}/10`;
74
67
  } else if (node.reason) {
75
68
  // A green e2e still carries meaning worth showing: smoke-only vs specs,
76
69
  // warm reuse vs rebuild. Without it, "pass" hides how much was proven.
@@ -200,7 +193,6 @@ const CI_EXCLUSIVE_FLAGS = [
200
193
  async function runLocalVerify(featureName, taskId, options) {
201
194
  try {
202
195
  return await runVerify(process.cwd(), featureName, taskId || null, {
203
- rubric: options.rubric,
204
196
  timeout: options.timeout,
205
197
  skipTests: options.skipTests,
206
198
  skipBuild: options.skipBuild,
@@ -251,8 +243,8 @@ async function runLocalVerify(featureName, taskId, options) {
251
243
  * Exit 2 is "the command cannot mean what you typed", and there are four
252
244
  * paths to it — never a verdict, because a verdict about nothing is worse
253
245
  * than a refusal:
254
- * - unknown feature, or unknown task id (the task's `group` decides the
255
- * rubric, so it is a hard usage error here);
246
+ * - unknown feature, or unknown task id (the task's `group` and `outputs`
247
+ * decide the scope, so it is a hard usage error here);
256
248
  * - `--filter` that matches no declared node (a filtered-to-empty run is
257
249
  * green and proves nothing);
258
250
  * - `--record-baseline` with nowhere to write (no `config.json`), or with a
@@ -345,7 +337,6 @@ export async function verifyCommand(featureName, taskId, options = {}) {
345
337
  compact = {
346
338
  status: result.status,
347
339
  nodes: nodeStatuses(result),
348
- score: result.nodes.validators.score ?? null,
349
340
  attempts: (prevEntry?.attempts ?? 0) + 1,
350
341
  updatedAt: now,
351
342
  };
@@ -74,7 +74,7 @@ export const OUTPUT_SCHEMA = {
74
74
  // (morph-plan §5). Runtime evidence: input, model, output, verdict — pointing
75
75
  // at the `runner: "evals"` node when the stack declares one. CONDITIONAL by
76
76
  // construction: never in phases.json requiredOutputs. Readers: morph-review
77
- // dispatch prompt, proof rubric eval-de-modelo, test-coverage Q7.
77
+ // dispatch prompt.
78
78
  evidencias: { dirname: 'evidencias', filenamePattern: '^T\\d+\\.md$', displayFilename: '{taskId}.md',
79
79
  phaseDir: '3-implement', phase: 'implement', protected: false, multi: true },
80
80
 
@@ -14,7 +14,6 @@ import SkillsLibrary from './pages/SkillsLibrary.jsx';
14
14
  import RulesMap from './pages/RulesMap.jsx';
15
15
  import Standards from './pages/Standards.jsx';
16
16
  import Guide from './pages/Guide.jsx';
17
- import Rubrics from './pages/Rubrics.jsx';
18
17
  import { useNow } from './hooks/usePref.js';
19
18
  import { useProjects, useCurrentProject, projectHref } from './lib/project.js';
20
19
 
@@ -77,7 +76,6 @@ export default function App() {
77
76
  <Route path="/p/:project/activity" element={<Activity />} />
78
77
  <Route path="/guide" element={<Guide />} />
79
78
  <Route path="/guide/:doc" element={<Guide />} />
80
- <Route path="/rubrics" element={<Rubrics />} />
81
79
  <Route path="/agents" element={<AgentMap />} />
82
80
  <Route path="/phases" element={<PhasePipeline />} />
83
81
  <Route path="/hooks" element={<HookInspector />} />
@@ -9,12 +9,12 @@ import { apiBase, projectHref, featureHref } from '../lib/project.js';
9
9
 
10
10
  const KIND_LABEL = {
11
11
  page: 'Página', project: 'Projeto', feature: 'Feature', task: 'Task', skill: 'Skill', persona: 'Persona',
12
- standard: 'Standard', rule: 'Rule', hook: 'Hook', guide: 'Guia', rubric: 'Rubrica',
12
+ standard: 'Standard', rule: 'Rule', hook: 'Hook', guide: 'Guia',
13
13
  };
14
14
 
15
15
  const PAGES = [
16
16
  ['Portfólio', '/'], ['Guia', '/guide'], ['Fases', '/phases'], ['Skills', '/skills'],
17
- ['Personas', '/agents'], ['Standards', '/standards'], ['Rules', '/rules'], ['Hooks', '/hooks'], ['Rubricas', '/rubrics'],
17
+ ['Personas', '/agents'], ['Standards', '/standards'], ['Rules', '/rules'], ['Hooks', '/hooks'],
18
18
  ];
19
19
 
20
20
  /** Everything the framework knows that has a page — built once. */
@@ -35,7 +35,6 @@ function knowledgeIndex() {
35
35
  out.push({ kind: 'guide', title: h.text, hint: d.title, to: `/guide/${d.id}#${h.slug}`, hay: `${h.text} ${d.title}` });
36
36
  }
37
37
  }
38
- for (const r of morphData.docs?.rubrics || []) out.push({ kind: 'rubric', title: r.title, to: `/rubrics?sel=${encodeURIComponent(`dim:${r.id}`)}`, hay: r.title });
39
38
  return out;
40
39
  }
41
40
 
@@ -16,7 +16,6 @@ export function entityHref(kind, id) {
16
16
  case 'hook': return q('hooks');
17
17
  case 'phase': return q('phases');
18
18
  case 'guide': return `/guide/${encodeURIComponent(id)}`;
19
- case 'rubric': return q('rubrics');
20
19
  default: return '#';
21
20
  }
22
21
  }
@@ -7,7 +7,7 @@ const REMARK = [remarkGfm];
7
7
  const REHYPE = [rehypeSlug];
8
8
 
9
9
  /** Frontmatter keys worth showing, in the order a reader wants them. */
10
- const META_ORDER = ['name', 'description', 'when-to-invoke', 'user-invokable', 'stacks', 'version', 'cliVersion'];
10
+ const META_ORDER = ['name', 'description', 'when_to_use', 'user-invocable', 'stacks', 'version', 'cliVersion'];
11
11
 
12
12
  function MetaBlock({ meta }) {
13
13
  const keys = Object.keys(meta).sort((a, b) => {
@@ -1,6 +1,6 @@
1
1
  import { NavLink, useLocation, useNavigate } from 'react-router-dom';
2
2
  import {
3
- MagnifyingGlass, SquaresFour, Kanban, Pulse, BookOpen, FlowArrow, PuzzlePiece, UsersThree, Books, Ruler, Plug, Scales,
3
+ MagnifyingGlass, SquaresFour, Kanban, Pulse, BookOpen, FlowArrow, PuzzlePiece, UsersThree, Books, Ruler, Plug,
4
4
  } from '@phosphor-icons/react';
5
5
  import { projectHref } from '../lib/project.js';
6
6
 
@@ -12,7 +12,6 @@ const KNOWLEDGE = [
12
12
  { to: '/standards', label: 'Standards', icon: Books },
13
13
  { to: '/rules', label: 'Rules', icon: Ruler },
14
14
  { to: '/hooks', label: 'Hooks', icon: Plug },
15
- { to: '/rubrics', label: 'Rubricas', icon: Scales },
16
15
  ];
17
16
 
18
17
  function Badge({ pulse }) {
@@ -15,7 +15,7 @@ const NODE_W = 260;
15
15
  // ─── Standard resolution ──────────────────────────────────────────────────────
16
16
 
17
17
  /**
18
- * A rule's @-import → the standard it names in the registry. `id` is what the
18
+ * A rule's route → the standard it names in the registry. `id` is what the
19
19
  * Standards page selects by; with no registry match the path itself is passed,
20
20
  * so the link still says which file the rule meant.
21
21
  */
@@ -196,7 +196,7 @@ function RuleDetail({ rule, byPath }) {
196
196
  )}
197
197
  </Section>
198
198
 
199
- <Section title="Standards importados">
199
+ <Section title="Standards roteados (resumo)">
200
200
  {rule.standardRefs.length > 0 ? (
201
201
  <div className="flex flex-wrap gap-1.5">
202
202
  {rule.standardRefs.map((ref) => {
@@ -209,7 +209,7 @@ function RuleDetail({ rule, byPath }) {
209
209
  })}
210
210
  </div>
211
211
  ) : (
212
- <p className="text-xs">Esta regra não importa nenhum standard.</p>
212
+ <p className="text-xs">Esta regra não roteia nenhum standard.</p>
213
213
  )}
214
214
  </Section>
215
215
 
@@ -315,10 +315,10 @@ export function SkillDetail({ skill, phases = morphData.phases?.list || [] }) {
315
315
  {skill.description && <p className="leading-relaxed text-[var(--color-fg)]">{skill.description}</p>}
316
316
 
317
317
  <dl className="grid grid-cols-[max-content_1fr] gap-x-4 gap-y-2 rounded-xl bg-white/[0.03] px-4 py-3 shadow-[inset_0_0_0_1px_var(--color-hair)]">
318
- {skill.whenToInvoke && (
318
+ {skill.whenToUse && (
319
319
  <>
320
320
  <dt className="text-xs leading-6 text-[var(--color-faint)]">Quando usar</dt>
321
- <dd className="leading-6">{skill.whenToInvoke}</dd>
321
+ <dd className="leading-6">{skill.whenToUse}</dd>
322
322
  </>
323
323
  )}
324
324
  <dt className="text-xs leading-6 text-[var(--color-faint)]">Fase</dt>
@@ -389,7 +389,7 @@ export default function SkillsLibrary() {
389
389
  const isMatch = useCallback((s) => {
390
390
  const q = search.trim().toLowerCase();
391
391
  if (q) {
392
- const hay = [s.id, s.name, s.description, s.whenToInvoke].filter(Boolean).join(' ').toLowerCase();
392
+ const hay = [s.id, s.name, s.description, s.whenToUse].filter(Boolean).join(' ').toLowerCase();
393
393
  if (!hay.includes(q)) return false;
394
394
  }
395
395
  if (phaseFilter) {
@@ -18,7 +18,7 @@ import { isFailedToolCall } from '../../../framework/hooks/shared/telemetry-logg
18
18
  export const DEFAULT_KINDS = ['prompt', 'question', 'skill', 'dispatch', 'handback', 'task', 'verify', 'gate', 'failure'];
19
19
  export const ALL_KINDS = [...DEFAULT_KINDS, 'write', 'read', 'command', 'tool'];
20
20
 
21
- const GATE_COMMAND = /\bmorph-spec\s+(approve|gate-check|gate-decision|advance|score|finish)\b/;
21
+ const GATE_COMMAND = /\bmorph-spec\s+(approve|gate-check|gate-decision|advance|finish)\b/;
22
22
  const MORPH_PERSONA = /^morph-(.+)$/;
23
23
  const SHELL_TOOLS = new Set(['Bash', 'PowerShell']);
24
24
  const SILENT_EVENTS = new Set(['ToolBatch', 'SessionUsage', 'AgentUsage']);
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * The reading material of the framework — the charter, the runtime
3
- * instructions, the CLI reference and the rubrics the evaluator scores with —
4
- * for the dashboard's Guide and Rubrics pages.
3
+ * instructions, the CLI reference and the Gate 3 evaluator checklist — for the
4
+ * dashboard's Guide page.
5
5
  */
6
- import { readFileSync, readdirSync, existsSync } from 'fs';
6
+ import { readFileSync } from 'fs';
7
7
  import { join } from 'path';
8
8
 
9
9
  function read(path) {
@@ -23,26 +23,11 @@ export function headingsOf(content) {
23
23
  return out;
24
24
  }
25
25
 
26
- function titleOf(content, fallback) {
27
- const h = headingsOf(content || '').find((x) => x.depth === 1);
28
- return h?.text || fallback;
29
- }
30
-
31
- function mdDir(dir) {
32
- if (!existsSync(dir)) return [];
33
- return readdirSync(dir)
34
- .filter((f) => f.endsWith('.md'))
35
- .sort()
36
- .map((f) => {
37
- const content = read(join(dir, f)) || '';
38
- return { id: f.replace(/\.md$/, ''), file: f, title: titleOf(content, f.replace(/\.md$/, '')), content };
39
- });
40
- }
41
-
42
26
  const GUIDE = [
43
27
  { id: 'morph', path: 'MORPH.md', title: 'Carta do harness (MORPH.md)', summary: 'Como o Claude trabalha: papéis, fases, gates, sub-agents. É a fonte única do comportamento.' },
44
28
  { id: 'runtime', path: 'CLAUDE.md', title: 'Instruções de runtime (CLAUDE.md)', summary: 'O que é injetado no CLAUDE.md do projeto: como começar cada sessão, regras críticas, comandos.' },
45
29
  { id: 'cli', path: 'docs/CLI.md', title: 'Referência do CLI', summary: 'Todos os comandos morph-spec: gates, verify, worktrees, grafo, telemetria.' },
30
+ { id: 'review-checklist', path: 'evals/review-checklist.md', title: 'Checklist do avaliador (Gate 3)', summary: 'O que o avaliador independente procura antes do ship: veredito SHIP/FIX e bloqueador com evidência.' },
46
31
  ];
47
32
 
48
33
  export function scanDocs(frameworkDir) {
@@ -51,8 +36,5 @@ export function scanDocs(frameworkDir) {
51
36
  return { ...g, content, headings: headingsOf(content).filter((h) => h.depth <= 3) };
52
37
  }).filter((g) => g.content);
53
38
 
54
- return {
55
- guide,
56
- rubrics: mdDir(join(frameworkDir, 'evals')).filter((d) => d.id === 'review-checklist'),
57
- };
39
+ return { guide };
58
40
  }
@@ -310,7 +310,7 @@ export function scanFeatures(projectDir, { resolveTree = resolveFeatureTree } =
310
310
  }
311
311
 
312
312
  /**
313
- * One feature in full: summary + tasks with their verify/score, the gate
313
+ * One feature in full: summary + tasks with their verify, the gate
314
314
  * decisions, the verify nodes and the document tree.
315
315
  */
316
316
  export function readFeatureDetail(projectDir, name, { resolveTree = resolveFeatureTree, known } = {}) {
@@ -4,7 +4,6 @@ import { scanHooks } from './hooks-scanner.js';
4
4
  import { scanSkills } from './skills-scanner.js';
5
5
  import { scanStandards } from './standards-scanner.js';
6
6
  import { scanRules } from './rules-scanner.js';
7
- import { scanRubrics } from './rubrics-scanner.js';
8
7
  import { scanDocs } from './docs-scanner.js';
9
8
 
10
9
  /**
@@ -21,7 +20,6 @@ export function scanAll(frameworkDir) {
21
20
  skills: scanSkills(frameworkDir, phases.list),
22
21
  standards: scanStandards(frameworkDir, agents.list),
23
22
  rules: scanRules(frameworkDir),
24
- rubrics: scanRubrics(frameworkDir),
25
23
  docs: scanDocs(frameworkDir),
26
24
  };
27
25
  }
@@ -18,17 +18,22 @@ function parseFrontmatter(content) {
18
18
  return { paths, stacks };
19
19
  }
20
20
 
21
- /**
22
- * Standards a rule imports, each once. RulesMap keys an edge per ref, so a
23
- * rule that imports the same standard twice (infrastructure-standards.md did,
24
- * up to 8.38.0) drew two edges under one React key.
25
- */
21
+ const ROUTE_LINE = /^- \*\*([a-z0-9-]+)\*\* \(`([^`]+\.md)`\)/;
22
+
23
+ // Bullet form: - **<id>** (`<path under standards/>`) — <digest>. An `@path`
24
+ // import inside .claude/rules resolves relative to the rule file and never loads.
25
+ export function parseStandardRoutes(content) {
26
+ const routes = [];
27
+ for (const line of content.split(/\r?\n/)) {
28
+ const m = ROUTE_LINE.exec(line);
29
+ if (m) routes.push({ id: m[1], path: m[2] });
30
+ }
31
+ return routes;
32
+ }
33
+
34
+ // RulesMap keys one edge per ref, so a repeated path would collide.
26
35
  export function extractStandardRefs(content) {
27
- const refs = new Set();
28
- const regex = /@\.morph\/framework\/standards\/(.+\.md)/g;
29
- let m;
30
- while ((m = regex.exec(content)) !== null) refs.add(m[1]);
31
- return [...refs];
36
+ return [...new Set(parseStandardRoutes(content).map((r) => r.path))];
32
37
  }
33
38
 
34
39
  export function scanRules(frameworkDir) {
@@ -84,13 +84,12 @@ export function scanSkills(frameworkDir, phases = []) {
84
84
  if (!existsSync(skillPath)) continue;
85
85
  const content = readFileSync(skillPath, 'utf8');
86
86
  const fm = parseFrontmatter(content);
87
- // The framework spells it `user-invokable`; the Claude Code docs say `user-invocable`.
88
- const invocable = fm['user-invokable'] ?? fm['user-invocable'];
87
+ const invocable = fm['user-invocable'];
89
88
  skills.push({
90
89
  id: skillDir.name,
91
90
  name: fm.name || skillDir.name,
92
91
  description: fm.description || '',
93
- whenToInvoke: fm['when-to-invoke'] || '',
92
+ whenToUse: fm.when_to_use || '',
94
93
  level: levelDir.name,
95
94
  userInvocable: invocable === true || invocable === 'true',
96
95
  stacks: typeof fm.stacks === 'string' ? fm.stacks.replace(/[[\]]/g, '').split(',').map((s) => s.trim()).filter(Boolean) : [],
@@ -79,6 +79,19 @@ export function evaluatorReportState(cwd, featureName) {
79
79
  return { status: 'ok', path, ...parsed };
80
80
  }
81
81
 
82
+ /** O sinal `evaluatorVerdict` que `verdictPause` (risk-detector) lê — gate-check e approve leem o mesmo. */
83
+ export function verdictSignal(report) {
84
+ if (report.status !== 'ok') return { status: report.status };
85
+ return {
86
+ status: 'ok',
87
+ verdict: report.verdict,
88
+ blockers: report.blockers.length,
89
+ blockerIds: report.blockers.map((b) => b.id),
90
+ untyped: report.untyped.length,
91
+ superseded: report.superseded,
92
+ };
93
+ }
94
+
82
95
  export function evaluatorMediaSection(cwd, featureName) {
83
96
  const text = readReport(cwd, featureName);
84
97
  return text !== null && MEDIA_SECTION_RE.test(text);
@@ -244,6 +244,14 @@ export function shouldPauseGate(config, signals) {
244
244
  };
245
245
  }
246
246
 
247
+ // No ship, ausência de stamp não é "verde": uma feature foi aprovada em campo sem verify nenhum.
248
+ if (signals.gate === 'review' && !signals.verificationStatus) {
249
+ return {
250
+ pause: true,
251
+ reason: 'nenhum `morph-spec verify` carimbado nesta feature — rode `morph-spec verify {feature}` antes do ship',
252
+ };
253
+ }
254
+
247
255
  // No gate de SHIP: um `verification` PRESENTE cujo escopo não é o da feature
248
256
  // é um recorte parcial — de UMA task, nunca da app inteira. O stamp é
249
257
  // SINGULAR, sobrescrito por qualquer verify de qualquer escopo, e vale
@@ -19,7 +19,7 @@ const NO_VALIDATOR_STACKS = new Set(['nodejs-cli', 'node-cli', 'cli']);
19
19
  const PROFILES = {
20
20
  dotnet: {
21
21
  isDotnet: true, isNextjs: false,
22
- defaultValidators: ['architecture', 'packages'],
22
+ defaultValidators: ['packages'],
23
23
  buildCommand: 'dotnet build --no-restore',
24
24
  },
25
25
  nextjs: {
@@ -1,15 +1,14 @@
1
1
  /**
2
2
  * Reuse Validator — símbolo novo cujo nome já existe no codebase.
3
3
  *
4
- * A terceira camada do ciclo de reuso, no molde do `design-system-validator`:
4
+ * A terceira camada do ciclo de reuso:
5
5
  *
6
6
  * PROSA (morph-plan, personas) ──► ARTEFATO (`reuse` em tasks.json)
7
7
  * ──► COBRANÇA (este arquivo)
8
8
  *
9
9
  * Prosa sozinha não segura — `morph-brainstorming/SKILL.md:39` já manda olhar
10
10
  * o que existe, e o ProspectPRO redefiniu `SimpleBuilding` em cinco arquivos da
11
- * MESMA pasta assim mesmo. O único ciclo que funciona no framework (design
12
- * tokens) tem verificação em código; este é o equivalente para código.
11
+ * MESMA pasta assim mesmo: só a verificação em código segura o ciclo.
13
12
  *
14
13
  * Regras, na ordem em que reduzem ruído:
15
14
  *