@polymorphism-tech/morph-spec 9.3.0 → 9.5.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 (107) hide show
  1. package/CLAUDE.md +1 -1
  2. package/README.md +2 -2
  3. package/claude-plugin.json +1 -1
  4. package/docs/CHEATSHEET.md +1 -1
  5. package/docs/QUICKSTART.md +1 -1
  6. package/framework/MORPH.md +23 -33
  7. package/framework/agents.json +10 -9
  8. package/framework/docs/CLI.md +3 -3
  9. package/framework/evals/review-checklist.md +1 -1
  10. package/framework/hooks/README.md +3 -2
  11. package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
  12. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -15
  13. package/framework/hooks/claude-code/core/comment-guard.js +1 -0
  14. package/framework/hooks/claude-code/core/conventional-commit.js +57 -0
  15. package/framework/hooks/claude-code/core/denylist.json +63 -1
  16. package/framework/hooks/claude-code/core/enforce-phase-writes.js +0 -8
  17. package/framework/hooks/claude-code/core/gate-guard.js +0 -8
  18. package/framework/hooks/claude-code/core/loop-detect.js +0 -3
  19. package/framework/hooks/claude-code/core/pre-bash-denylist.js +2 -1
  20. package/framework/hooks/claude-code/core/protect-spec-files.js +0 -7
  21. package/framework/hooks/claude-code/core/validate-completion.js +0 -4
  22. package/framework/hooks/claude-code/core/worktree-context.js +0 -2
  23. package/framework/hooks/claude-code/statusline.py +3 -44
  24. package/framework/hooks/shared/build-check.js +12 -19
  25. package/framework/hooks/shared/command-tokens.js +17 -0
  26. package/framework/hooks/shared/comment-rules.js +11 -1
  27. package/framework/hooks/shared/hook-response.js +0 -4
  28. package/framework/hooks/shared/payload-utils.js +1 -35
  29. package/framework/hooks/shared/phase-utils.js +0 -12
  30. package/framework/hooks/shared/slice-progress.js +0 -2
  31. package/framework/hooks/shared/state-reader.js +0 -19
  32. package/framework/hooks/shared/telemetry-logger.js +1 -1
  33. package/framework/rules/csharp-standards.md +5 -3
  34. package/framework/rules/dotnet-templates.md +2 -2
  35. package/framework/rules/frontend-standards.md +1 -2
  36. package/framework/rules/nextjs-standards.md +10 -8
  37. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  38. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  39. package/framework/skills/core/morph-apply/SKILL.md +1 -1
  40. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  41. package/framework/skills/core/morph-machine/SKILL.md +2 -2
  42. package/framework/skills/core/morph-media/SKILL.md +1 -1
  43. package/framework/skills/core/morph-plan/SKILL.md +3 -3
  44. package/framework/skills/core/morph-review/SKILL.md +1 -1
  45. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  46. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  47. package/framework/standards/STANDARDS.json +87 -37
  48. package/framework/standards/STANDARDS.meta.json +41 -15
  49. package/framework/standards/architecture/code-comments.md +1 -1
  50. package/framework/standards/architecture/engineering-lens.md +100 -0
  51. package/framework/standards/architecture/vertical-slice/vertical-slice.md +7 -9
  52. package/framework/standards/backend/dotnet/analyzers.md +59 -0
  53. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -12
  54. package/framework/standards/frontend/admin-app/data-table.md +1 -3
  55. package/framework/standards/frontend/admin-app/page-structure.md +8 -4
  56. package/framework/standards/frontend/nextjs/app-router.md +3 -4
  57. package/framework/standards/frontend/nextjs/components.md +2 -15
  58. package/framework/standards/frontend/nextjs/data-fetching.md +2 -3
  59. package/framework/standards/frontend/nextjs/lint.md +141 -0
  60. package/framework/standards/frontend/nextjs/motion-patterns.md +2 -7
  61. package/framework/standards/frontend/nextjs/naming-conventions.md +1 -2
  62. package/framework/standards/frontend/nextjs/nextjs-patterns.md +5 -7
  63. package/framework/standards/frontend/nextjs/project-structure.md +3 -2
  64. package/framework/standards/frontend/nextjs/state-management.md +6 -7
  65. package/framework/templates/README.md +4 -5
  66. package/framework/templates/REGISTRY.json +21 -516
  67. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +54 -56
  68. package/package.json +3 -2
  69. package/src/commands/project/worktree.js +1 -1
  70. package/src/commands/state/archive.js +1 -1
  71. package/src/commands/state/finish.js +1 -1
  72. package/src/core/paths/output-schema.js +0 -9
  73. package/src/core/state/feature-identity.js +0 -2
  74. package/src/core/state/state-manager.js +1 -169
  75. package/src/core/templates/template-registry.js +1 -43
  76. package/src/core/templates/template-renderer.js +0 -23
  77. package/src/dashboard/app/lib/format.js +0 -7
  78. package/src/dashboard/app/lib/phases.js +0 -7
  79. package/src/dashboard/scanner/activity-scanner.js +0 -4
  80. package/src/lib/ai/ai-project-scan.js +1 -6
  81. package/src/lib/graph/graphify-runtime.js +0 -1
  82. package/src/lib/mutation/snapshot.js +1 -5
  83. package/src/lib/risk-detector.js +0 -5
  84. package/src/lib/stack/stack-profile.js +0 -4
  85. package/src/lib/stack/stack-scan.js +18 -9
  86. package/src/lib/tasks/eval-node.js +1 -7
  87. package/src/lib/validators/validation-runner.js +0 -43
  88. package/src/lib/validators/verify-runner.js +18 -2
  89. package/src/utils/agents-installer.js +12 -19
  90. package/src/utils/claude-settings-manager.js +0 -28
  91. package/src/utils/file-copier.js +0 -53
  92. package/src/utils/hooks-installer.js +1 -1
  93. package/framework/hooks/shared/activity-logger.js +0 -131
  94. package/framework/standards/frontend/design-system/naming.md +0 -68
  95. package/framework/templates/meta-prompts/fusion/fusion-agent.md +0 -76
  96. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +0 -100
  97. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +0 -90
  98. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +0 -84
  99. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +0 -44
  100. package/framework/templates/meta-prompts/validators/checkpoint-validator.md +0 -107
  101. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +0 -94
  102. package/src/commands/validation/index.js +0 -2
  103. package/src/lib/validators/code-budget/index.js +0 -2
  104. package/src/lib/validators/contracts/index.js +0 -1
  105. package/src/lib/validators/nextjs/index.js +0 -1
  106. package/src/lib/validators/packages/index.js +0 -1
  107. package/src/lib/validators/reuse/index.js +0 -2
@@ -18,7 +18,6 @@ const PHASE_DIR_ENTRY_GATE = {
18
18
 
19
19
  const ALWAYS_WRITABLE_DIR = '2-plan';
20
20
  import { blockResult, runStandalone } from '../../shared/hook-response.js';
21
- import { logHookActivity } from '../../shared/activity-logger.js';
22
21
  import { getFilePath, resolveMorphRoot, resolveTargetPath } from '../../shared/payload-utils.js';
23
22
 
24
23
  export async function run(payload) {
@@ -57,7 +56,6 @@ export async function run(payload) {
57
56
  if (targetDir !== allowedDir) {
58
57
  const absolutePath = resolveTargetPath(filePath, root);
59
58
  if (existsSync(absolutePath)) {
60
- logHookActivity('enforce-phase-writes', 'PreToolUse', 'ok');
61
59
  return null;
62
60
  }
63
61
 
@@ -69,11 +67,8 @@ export async function run(payload) {
69
67
  && existsSync(resolveTargetPath(`.morph/features/${featureName}/1-design`, root));
70
68
 
71
69
  if (!tookDesignBranch || isGateApproved(featureName, 'uiux', root)) {
72
- logHookActivity('enforce-phase-writes', 'PreToolUse', 'ok');
73
70
  return null;
74
71
  }
75
-
76
- logHookActivity('enforce-phase-writes', 'PreToolUse', 'blocked');
77
72
  return blockResult(
78
73
  `MORPH-SPEC: Writing to '${targetDir}/' is not allowed yet.\n` +
79
74
  `This feature entered the uiux branch (1-design/ exists) and the 'uiux' gate is still pending.\n\n` +
@@ -87,7 +82,6 @@ export async function run(payload) {
87
82
  const treeNote = root !== process.cwd().replace(/\\/g, '/')
88
83
  ? `\n(phase read from the tree that owns the file: ${root})`
89
84
  : '';
90
- logHookActivity('enforce-phase-writes', 'PreToolUse', 'blocked');
91
85
  return blockResult(
92
86
  `MORPH-SPEC: Writing to '${targetDir}/' is not allowed during ${phaseLabel} phase.\n` +
93
87
  `Current phase '${phase}' only permits writes to '${allowedDir}/'.${treeNote}\n\n` +
@@ -96,8 +90,6 @@ export async function run(payload) {
96
90
  `(run it in that tree — the gate is recorded in its own feature.json).`
97
91
  );
98
92
  }
99
-
100
- logHookActivity('enforce-phase-writes', 'PreToolUse', 'ok');
101
93
  return null;
102
94
  } catch {
103
95
  return null;
@@ -4,7 +4,6 @@
4
4
  import { loadState, stateExists, getActiveFeature } from '../../shared/state-reader.js';
5
5
  import { contextResult, runStandalone } from '../../shared/hook-response.js';
6
6
  import { gateFromQuestion } from '../../shared/gate-question.js';
7
- import { logHookActivity } from '../../shared/activity-logger.js';
8
7
  import { resolveMorphRoot, resolveTargetPath } from '../../shared/payload-utils.js';
9
8
  import { locateFeatureForQuestion } from '../../shared/feature-locator.js';
10
9
 
@@ -61,7 +60,6 @@ function checkAskUserQuestion(input, sessionRoot) {
61
60
  const decision = getDecision(state, feature, gate);
62
61
  if (!decision) continue;
63
62
  if (decision.pause === false) {
64
- logHookActivity('gate-guard', 'PostToolUse', 'divergent', cwd);
65
63
  return contextResult(
66
64
  `⚠️ [morph-spec] GATE OVER-PAUSE — você apresentou o Gate "${gate}" via AskUserQuestion, ` +
67
65
  `mas a decisão registrada em state.json (gateDecisions.${gate}) era pause:false ` +
@@ -72,7 +70,6 @@ function checkAskUserQuestion(input, sessionRoot) {
72
70
  'PostToolUse'
73
71
  );
74
72
  }
75
- logHookActivity('gate-guard', 'PostToolUse', 'coherent', cwd);
76
73
  return null;
77
74
  }
78
75
  return null;
@@ -94,8 +91,6 @@ function checkStateEdit(input, sessionRoot) {
94
91
  if (!state) return null;
95
92
  const feature = featureFromStatePath(filePath) || getActiveFeature(cwd)?.name;
96
93
  if (!feature) return null;
97
-
98
- let checked = false;
99
94
  const gates = state.features?.[feature]?.approvalGates ?? {};
100
95
  for (const gate of ['plan', 'uiux', 'review']) {
101
96
  const g = gates[gate];
@@ -104,7 +99,6 @@ function checkStateEdit(input, sessionRoot) {
104
99
  const decision = getDecision(state, feature, gate);
105
100
  if (!decision) continue;
106
101
  if (decision.pause === true) {
107
- logHookActivity('gate-guard', 'PostToolUse', 'divergent', cwd);
108
102
  return contextResult(
109
103
  `⚠️ [morph-spec] GATE SKIPPED PAUSE — o Gate "${gate}" foi marcado approvedBy:"auto", ` +
110
104
  `mas a decisão registrada (gateDecisions.${gate}) era pause:true ` +
@@ -114,9 +108,7 @@ function checkStateEdit(input, sessionRoot) {
114
108
  'PostToolUse'
115
109
  );
116
110
  }
117
- checked = true;
118
111
  }
119
- if (checked) logHookActivity('gate-guard', 'PostToolUse', 'coherent', cwd);
120
112
  return null;
121
113
  }
122
114
 
@@ -5,7 +5,6 @@ import { join } from 'path';
5
5
  import { stateExists } from '../../shared/state-reader.js';
6
6
  import { contextResult, runStandalone } from '../../shared/hook-response.js';
7
7
  import { getCommandSignature } from '../../shared/payload-utils.js';
8
- import { logEvent } from '../../shared/activity-logger.js';
9
8
 
10
9
  const MAX_BUFFER = 15;
11
10
  const LOOP_WINDOW = 8;
@@ -60,8 +59,6 @@ export async function run(payload) {
60
59
 
61
60
  writeFileSync(cooldownPath, JSON.stringify({ lastDetected: new Date().toISOString() }), 'utf-8');
62
61
 
63
- logEvent('loop_detected', { signatures: uniqueSigs.size }, process.cwd());
64
-
65
62
  const sigList = [...uniqueSigs].join(', ');
66
63
  return contextResult(
67
64
  `⚠️ [morph-spec] LOOP DETECTED — the last ${LOOP_WINDOW} actions repeat only ${uniqueSigs.size} pattern(s): ${sigList}\n\n` +
@@ -54,8 +54,9 @@ export async function run(payload) {
54
54
  }
55
55
 
56
56
  for (const rule of denylist.executableArgs || []) {
57
+ if (rule.subagentOnly && !payload.agent_id) continue;
57
58
  if (matchesExecutableRule(command, rule)) {
58
- const shown = [rule.executable, ...(rule.flags || []).map((group) => group[0]), rule.args[0]].join(' ');
59
+ const shown = rule.display ?? [rule.executable, ...(rule.flags || []).map((group) => group[0]), rule.args[0]].join(' ');
59
60
  return blockResult(`Comando bloqueado pela denylist: \`${shown}\` ${rule.reason}.`);
60
61
  }
61
62
  }
@@ -9,7 +9,6 @@ import {
9
9
  PROTECTED_SPEC_FILES
10
10
  } from '../../shared/phase-utils.js';
11
11
  import { blockResult, runStandalone } from '../../shared/hook-response.js';
12
- import { logHookActivity } from '../../shared/activity-logger.js';
13
12
  import { getFilePath, getToolName, resolveMorphRoot, resolveTargetPath } from '../../shared/payload-utils.js';
14
13
  import { ERRATA_HEADING, errataViolation } from '../../shared/errata.js';
15
14
 
@@ -57,7 +56,6 @@ export async function run(payload) {
57
56
  // Generated only by the CLI: a hand-written one would carry checks the parser never judged.
58
57
  // Decided before the creation carve-out, since chore/hotfix are born with the plan gate stamped.
59
58
  if (filename === CHECKS_FILE) {
60
- logHookActivity('protect-spec-files', 'PreToolUse', 'blocked');
61
59
  return blockResult(
62
60
  `MORPH-SPEC: '${CHECKS_FILE}' is generated — never written by hand.\n` +
63
61
  `Edit 2-plan/plan.md and run \`morph-spec plan-check ${featureName}\`: it parses the plan and writes ${CHECKS_FILE}.\n` +
@@ -67,7 +65,6 @@ export async function run(payload) {
67
65
  }
68
66
 
69
67
  if (!isGateApproved(featureName, requiredGate, root)) {
70
- logHookActivity('protect-spec-files', 'PreToolUse', 'ok');
71
68
  return null;
72
69
  }
73
70
 
@@ -76,7 +73,6 @@ export async function run(payload) {
76
73
  // A file not yet on disk is its CREATION (chore/hotfix are born with `plan` stamped), not an edit of a frozen spec.
77
74
  // The criterion is existence, never `approvedBy`: by signature, deleting a spec first would license rewriting it.
78
75
  if (!existsSync(absPath)) {
79
- logHookActivity('protect-spec-files', 'PreToolUse', 'ok:absent');
80
76
  return null;
81
77
  }
82
78
 
@@ -90,11 +86,8 @@ export async function run(payload) {
90
86
  } catch {}
91
87
 
92
88
  if (errataCheck?.ok) {
93
- logHookActivity('protect-spec-files', 'PreToolUse', 'ok');
94
89
  return null;
95
90
  }
96
-
97
- logHookActivity('protect-spec-files', 'PreToolUse', 'blocked');
98
91
  return blockResult(
99
92
  `MORPH-SPEC: '${filename}' is locked — the '${requiredGate}' gate has been approved.\n` +
100
93
  `Editing an approved artifact breaks its contract.\n\n` +
@@ -7,7 +7,6 @@ import {
7
7
  stateExists, getActiveFeature, getMostRecentFeature, derivePhaseForFeature,
8
8
  } from '../../shared/state-reader.js';
9
9
  import { contextResult, runStandalone } from '../../shared/hook-response.js';
10
- import { logHookActivity, logEvent } from '../../shared/activity-logger.js';
11
10
  import { appendTelemetryEvent } from '../../shared/telemetry-logger.js';
12
11
  import { readTranscriptUsage, usageDelta } from '../../shared/transcript-usage.js';
13
12
  import { sliceProgress, sliceReportPath, inspectReport } from '../../shared/slice-progress.js';
@@ -145,9 +144,6 @@ export async function run(payload) {
145
144
  'MORPH-SPEC:',
146
145
  ...warnings.map(w => ` ${w}`),
147
146
  ].join('\n');
148
-
149
- logHookActivity('validate-completion', 'Stop', `warnings(${warnings.length})`);
150
- logEvent('session_end', { feature: name, phase: currentPhase, warnings: warnings.length });
151
147
  return contextResult(message, 'Stop');
152
148
  } catch {
153
149
  return null;
@@ -13,7 +13,6 @@ import { sliceProgress } from '../../shared/slice-progress.js';
13
13
  import { collectNextSignals, decideNext, formatNext } from '../../shared/next-step.js';
14
14
  import { contextResult, runStandalone } from '../../shared/hook-response.js';
15
15
  import { acquireClaim, foreignClaim, claimAge } from '../../shared/claim-registry.js';
16
- import { rotateActivityLogIfLarge } from '../../shared/activity-logger.js';
17
16
  import { rotateEventsLogIfLarge } from '../../shared/telemetry-logger.js';
18
17
  import { kickGraphRefresh, seedFromPrimary } from '../../shared/graph-refresh.js';
19
18
  import { kickAgentReap } from '../../shared/agent-reap.js';
@@ -226,7 +225,6 @@ export async function run(payload) {
226
225
  return null;
227
226
  }
228
227
 
229
- try { rotateActivityLogIfLarge(dir); } catch {}
230
228
  try { rotateEventsLogIfLarge(dir); } catch {}
231
229
  // Only the ROOT's graph is refreshed in background: a detached process holding a worktree directory made `finish` fail on win32.
232
230
  try { seedFromPrimary(dir); } catch {}
@@ -457,49 +457,13 @@ def get_active_features(cwd):
457
457
  return result
458
458
 
459
459
 
460
- def get_last_hook(cwd):
461
- """Name of the most recent hook from .morph/logs/activity.jsonl.
462
-
463
- activity-logger.js (R1.2) writes append-only JSONL now, one event per
464
- line (`{"kind": "hook", ...}` or `{"kind": "event", ...}`), instead of a
465
- single JSON doc rewritten whole on every call. The log can grow up to the
466
- rotation threshold (2 MB, see ROTATE_THRESHOLD_BYTES in activity-logger.js)
467
- between SessionStarts, and this function runs on every prompt redraw, so
468
- only the trailing slice is read — never the whole file.
469
-
470
- The age is deliberately not shown: activity-logger.js only records
471
- HH:MM:SS, so any age computed from it is wrong across midnight.
472
- """
473
- try:
474
- path = Path(cwd) / '.morph' / 'logs' / 'activity.jsonl'
475
- size = path.stat().st_size
476
- tail_bytes = 8192
477
- with path.open('rb') as f:
478
- if size > tail_bytes:
479
- f.seek(-tail_bytes, 2)
480
- tail = f.read().decode('utf-8', errors='ignore')
481
- for line in reversed(tail.splitlines()):
482
- line = line.strip()
483
- if not line:
484
- continue
485
- try:
486
- obj = json.loads(line)
487
- except Exception:
488
- continue
489
- if obj.get('kind') == 'hook':
490
- return obj.get('name') or None
491
- return None
492
- except Exception:
493
- return None
494
-
495
-
496
460
  # ── Line renderers ────────────────────────────────────────────────────────────
497
461
 
498
- def render_morph_line(feature, hook_name, columns):
462
+ def render_morph_line(feature, columns):
499
463
  """Line 0 — the active MORPH feature.
500
464
 
501
465
  Goes through join_to_width like the status line: a feature name plus phase,
502
- slice bar, gate and hook is the longest thing this script prints, and a
466
+ slice bar and gate is the longest thing this script prints, and a
503
467
  wrapping line 0 would defeat the whole point of the single-row layout.
504
468
  """
505
469
  parts = [(0, f"{CYAN}{BOLD}► {feature['name']}{R}")]
@@ -523,9 +487,6 @@ def render_morph_line(feature, hook_name, columns):
523
487
  if feature['pending']:
524
488
  parts.append((2, f"{RED}⛔ gate: {feature['pending']}{R}"))
525
489
 
526
- if hook_name:
527
- parts.append((4, f"{GRAY}🪝 {hook_name}{R}"))
528
-
529
490
  return join_to_width(parts, columns)
530
491
 
531
492
 
@@ -722,10 +683,8 @@ def main():
722
683
  # worktree. In the primary tree it resurrects stale feature names
723
684
  # during unrelated work, so there the status line is a single row.
724
685
  if get_worktree_label(payload, cwd):
725
- hook_name = get_last_hook(cwd)
726
686
  for feature in get_active_features(cwd):
727
- print(render_morph_line(feature, hook_name, columns))
728
- hook_name = None # annotate the first line only
687
+ print(render_morph_line(feature, columns))
729
688
 
730
689
  status_line = render_status_line(payload, cwd, settings, columns)
731
690
  if status_line:
@@ -4,6 +4,7 @@ import { join } from 'path';
4
4
 
5
5
  const DOTNET_BUILD_CMD = 'dotnet build --no-restore --nologo -v q';
6
6
  const TSC_BUILD_CMD = 'npx tsc --noEmit';
7
+ const ESLINT_CONFIGS = ['eslint.config.mjs', 'eslint.config.js', 'eslint.config.cjs'];
7
8
 
8
9
  export function isDotnetStack(s) {
9
10
  return s.includes('dotnet') || s.includes('aspnet');
@@ -13,26 +14,9 @@ function isFrontendStack(s) {
13
14
  return s.includes('nextjs') || s.includes('node') || s.includes('react') || s.includes('vue');
14
15
  }
15
16
 
16
- // Single stack only: a compound stack resolves to `dotnet build` alone. Use
17
- // `getBuildSteps()` for those.
18
- export function getBuildCommand(stack, hasTsConfig = false) {
19
- if (!stack) return null;
20
- const s = stack.toLowerCase();
21
-
22
- if (isDotnetStack(s)) {
23
- return DOTNET_BUILD_CMD;
24
- }
25
-
26
- if (isFrontendStack(s) && hasTsConfig) {
27
- return TSC_BUILD_CMD;
28
- }
29
-
30
- return null;
31
- }
32
-
33
17
  // A compound stack checks BOTH sides, each from its own directory.
34
18
  // `backendTarget` names the solution when the backend directory holds none.
35
- export function getBuildSteps(stack, { cwd, frontendPath, backendPath, backendTarget } = {}) {
19
+ export function getBuildSteps(stack, { cwd, frontendPath, backendPath, backendTarget, lintFiles = [] } = {}) {
36
20
  if (!stack || !cwd) return [];
37
21
  const s = stack.toLowerCase();
38
22
  const steps = [];
@@ -51,6 +35,15 @@ export function getBuildSteps(stack, { cwd, frontendPath, backendPath, backendTa
51
35
  ) {
52
36
  steps.push({ command: TSC_BUILD_CMD, cwd: frontendDir });
53
37
  }
38
+ // Only the files the feature touched: legacy lint debt never turns a feature's verify red.
39
+ if (
40
+ lintFiles.length > 0 &&
41
+ ESLINT_CONFIGS.some((name) => existsSync(join(frontendDir, name))) &&
42
+ existsSync(join(frontendDir, 'node_modules', 'eslint'))
43
+ ) {
44
+ const files = lintFiles.map((file) => JSON.stringify(file)).join(' ');
45
+ steps.push({ command: `npx eslint --quiet --no-warn-ignored ${files}`, cwd: frontendDir });
46
+ }
54
47
  }
55
48
 
56
49
  return steps;
@@ -68,7 +61,7 @@ export function parseBuildResult(exitCode, output) {
68
61
 
69
62
  /** @returns {{ status: 'pass'|'fail'|'skip', output: string }} */
70
63
  export function runBuildCheck(cwd, stack, timeout = 30000, paths = {}) {
71
- const steps = getBuildSteps(stack, { cwd, frontendPath: paths.frontendPath, backendPath: paths.backendPath, backendTarget: paths.backendTarget });
64
+ const steps = getBuildSteps(stack, { cwd, frontendPath: paths.frontendPath, backendPath: paths.backendPath, backendTarget: paths.backendTarget, lintFiles: paths.lintFiles });
72
65
  if (steps.length === 0) return { status: 'skip', output: '' };
73
66
 
74
67
  const failures = [];
@@ -108,6 +108,23 @@ function simpleCommands(head) {
108
108
  return commands.filter((c) => c.length > 0);
109
109
  }
110
110
 
111
+ const GIT_EXECUTABLE = /(^|[\\/])git(\.exe)?$/i;
112
+ const GIT_GLOBAL_WITH_VALUE = new Set(['-c', '-C', '--git-dir', '--work-tree', '--namespace', '--exec-path', '--config-env']);
113
+
114
+ /** Each `git <sub> …` the command runs, global options skipped, one `{ sub, args }` per simple command. */
115
+ export function gitInvocations(command) {
116
+ const invocations = [];
117
+ for (const tokens of simpleCommands(command)) {
118
+ const texts = tokens.map((t) => t.text);
119
+ const at = texts.findIndex((t) => GIT_EXECUTABLE.test(t));
120
+ if (at < 0) continue;
121
+ let j = at + 1;
122
+ while (j < texts.length && texts[j].startsWith('-')) j += GIT_GLOBAL_WITH_VALUE.has(texts[j]) ? 2 : 1;
123
+ if (j < texts.length) invocations.push({ sub: texts[j].toLowerCase(), args: texts.slice(j + 1) });
124
+ }
125
+ return invocations;
126
+ }
127
+
111
128
  function positionals(args, takesValue) {
112
129
  const out = [];
113
130
  const values = [];
@@ -37,6 +37,15 @@ function isBanner(text) {
37
37
  return words === '' || /^[-=*#~_+]{3,}.*[-=*#~_+]{3,}$/.test(body);
38
38
  }
39
39
 
40
+ const CODE_KEYWORD = /^(?:const|let|var|return|await|throw|if|else|for|foreach|while|switch|import|export|using|new|public|private|protected|internal|function|class)\b/;
41
+
42
+ function isCommentedCode(text) {
43
+ const body = bodyOf(text);
44
+ if (/^[}\])]+[;,)]*$/.test(body)) return true;
45
+ if (/\{$/.test(body)) return CODE_KEYWORD.test(body) || /[=(]/.test(body);
46
+ return /;$/.test(body) && (CODE_KEYWORD.test(body) || /[=(.]/.test(body));
47
+ }
48
+
40
49
  function isDocComment(text, isCs) {
41
50
  return isCs ? /^\/\/\/(?!\/)/.test(text) : /^\/\*\*(?!\*)/.test(text);
42
51
  }
@@ -103,7 +112,7 @@ function addedRegionLines(before, after) {
103
112
  return out;
104
113
  }
105
114
 
106
- /** @returns {Array<{ line: number, rule: 'process'|'banner'|'doc-comment'|'long-block', text: string }>} */
115
+ /** @returns {Array<{ line: number, rule: 'process'|'banner'|'commented-code'|'doc-comment'|'long-block', text: string }>} */
107
116
  export function commentFindings({ file, before, after, docCommentsForbidden: docForbidden = false }) {
108
117
  const isCs = /\.cs$/i.test(file);
109
118
  const all = commentLines(after, file);
@@ -120,6 +129,7 @@ export function commentFindings({ file, before, after, docCommentsForbidden: doc
120
129
  if (ALLOWED.some((re) => re.test(text))) continue;
121
130
  if (citesProcess(text)) flag(line, 'process', text);
122
131
  else if (isBanner(text)) flag(line, 'banner', text);
132
+ else if (!isDocComment(text, isCs) && isCommentedCode(text)) flag(line, 'commented-code', text);
123
133
  }
124
134
  for (const { line, text } of addedRegionLines(before, after)) flag(line, 'banner', text);
125
135
 
@@ -30,10 +30,6 @@ export function blockResult(reason) {
30
30
  return { decision: 'block', reason };
31
31
  }
32
32
 
33
- export function approveResult(context) {
34
- return context ? { decision: 'approve', additionalContext: context } : null;
35
- }
36
-
37
33
  export function contextResult(text, eventName) {
38
34
  return { hookSpecificOutput: { hookEventName: eventName, additionalContext: text } };
39
35
  }
@@ -1,29 +1,5 @@
1
1
  import { createHash } from 'crypto';
2
- import { existsSync } from 'fs';
3
- import { dirname, join, isAbsolute } from 'path';
4
-
5
- // A sub-agent may edit inside its own worktree: the build root comes from the
6
- // file, not from the session's cwd.
7
- export function resolveRootForFile(filePath, fallbackCwd, markers = ['package.json']) {
8
- try {
9
- if (!filePath) return fallbackCwd;
10
- let p = String(filePath).replace(/\\/g, '/');
11
- if (!isAbsolute(p)) p = join(fallbackCwd, p);
12
-
13
- let dir = dirname(p);
14
- let prev = null;
15
- while (dir && dir !== prev) {
16
- for (const marker of markers) {
17
- if (existsSync(join(dir, marker))) return dir;
18
- }
19
- prev = dir;
20
- dir = dirname(dir);
21
- }
22
- return fallbackCwd;
23
- } catch {
24
- return fallbackCwd;
25
- }
26
- }
2
+ import { join, isAbsolute } from 'path';
27
3
 
28
4
  // State is resolved from the target PATH, never the session: a session in the
29
5
  // primary root may write a feature that lives in a worktree. `lastIndexOf` picks
@@ -58,16 +34,6 @@ export function getFilePath(payload) {
58
34
  return raw.replace(/\\/g, '/');
59
35
  }
60
36
 
61
- export function getContentToValidate(payload) {
62
- const input = payload?.tool_input;
63
- if (!input) return '';
64
- return input.content || input.new_string || '';
65
- }
66
-
67
- export function isWriteOperation(payload) {
68
- return typeof payload?.tool_input?.content === 'string';
69
- }
70
-
71
37
  export function getToolName(payload) {
72
38
  return payload?.tool_name || 'unknown';
73
39
  }
@@ -2,8 +2,6 @@
2
2
  // Source of truth: src/core/paths/output-schema.js
3
3
  // Regenerate with: node scripts/generate-refs.js
4
4
 
5
- export const PHASE_ORDER = ["plan","uiux","implement","review"];
6
-
7
5
  export const PHASE_DIRS = {
8
6
  plan: '2-plan',
9
7
  uiux: '1-design',
@@ -63,16 +61,6 @@ export function isFeaturePath(filePath) {
63
61
  return normalized.includes('.morph/features/');
64
62
  }
65
63
 
66
- export function isFrameworkPath(filePath) {
67
- const normalized = filePath.replace(/\\/g, '/');
68
- return normalized.includes('.morph/framework/');
69
- }
70
-
71
- export function isStatePath(filePath) {
72
- const normalized = filePath.replace(/\\/g, '/');
73
- return normalized.endsWith('.morph/state.json');
74
- }
75
-
76
64
  export function getBasename(filePath) {
77
65
  const normalized = filePath.replace(/\\/g, '/');
78
66
  return normalized.split('/').pop();
@@ -7,8 +7,6 @@ export const PARTIAL_MARKER = /^[\s#>*_]*PARCIAL\b/;
7
7
  /** `BLOQUEADO`: the slice waits on a decision only the human can make; re-dispatching it repeats the question. */
8
8
  export const BLOCKED_MARKER = /^[\s#>*_]*BLOQUEADO\b/;
9
9
 
10
- export const SLICE_STATUSES = ['pendente', 'parcial', 'bloqueado', 'feito'];
11
-
12
10
  export function checksPath(featureDir) {
13
11
  return join(featureDir, '2-plan', 'checks.json');
14
12
  }
@@ -185,11 +185,6 @@ export function derivePhaseFrom(feature, hasArtifact) {
185
185
  return PHASE_INDEX[implied] > PHASE_INDEX[artifactPhase] ? implied : artifactPhase;
186
186
  }
187
187
 
188
- export function gateImpliedPhase(featureName, projectPath) {
189
- const designDir = join(projectPath || process.cwd(), '.morph/features', featureName, DESIGN_DIR);
190
- return gateImpliedPhaseFrom(getFeature(featureName, projectPath), existsSync(designDir));
191
- }
192
-
193
188
  export function derivePhaseForFeature(featureName, projectPath) {
194
189
  const basePath = join(projectPath || process.cwd(), '.morph/features', featureName);
195
190
  return derivePhaseFrom(getFeature(featureName, projectPath), diskArtifactProbe(basePath));
@@ -219,17 +214,3 @@ export function pendingGatesFrom(feature) {
219
214
  export function getPendingGates(featureName, projectPath) {
220
215
  return pendingGatesFrom(getFeature(featureName, projectPath));
221
216
  }
222
-
223
- export function getOutputs(featureName, projectPath) {
224
- const feature = getFeature(featureName, projectPath);
225
- return feature?.outputs || null;
226
- }
227
-
228
- export function getMissingOutputs(featureName, projectPath) {
229
- const feature = getFeature(featureName, projectPath);
230
- if (!feature?.outputs) return [];
231
-
232
- return Object.entries(feature.outputs)
233
- .filter(([, output]) => !output.created)
234
- .map(([type, output]) => ({ type, path: output.path }));
235
- }
@@ -70,7 +70,7 @@ export function readTelemetryEvents(projectPath) {
70
70
  }
71
71
  }
72
72
 
73
- // `events-` prefix: activity-logger archives share `sessions/` without one.
73
+ // `events-` prefix: older activity-log archives in `sessions/` carry none.
74
74
  function archiveEvents(projectPath) {
75
75
  try {
76
76
  const eventsPath = getEventsPath(projectPath);
@@ -8,13 +8,15 @@ stacks:
8
8
 
9
9
  # C# and .NET VSA Standards
10
10
 
11
- - **architecture-vertical-slice-vertical-slice** (`architecture/vertical-slice/vertical-slice.md`) — Uma slice por operação em `Features/{Entity}Feature/{Verb}{Entity}/`: Request/Response `sealed record`, Handler, Validator e Endpoint `IApiEndpoint` `internal sealed`. Decorators Scrutor: Logging → Validation → Handler. PROIBIDO: MediatR/`IRequest`, AggregateRoot, DomainEvent, CQRS formal, camada Application Service, DTO compartilhado entre features, `IRepository<T>`/`IUnitOfWork`.
12
- - **backend-dotnet-vsa-handler-patterns** (`backend/dotnet/vsa-handler-patterns.md`) — Handler `sealed class` com primary constructor implementando `IHandler<TRequest, TResponse>` (não MediatR): injeta `ApplicationDbContext` direto, usa `Guid.CreateVersion7()` (nunca `Guid.NewGuid()`), passa `CancellationToken` em tudo e devolve `Result<T>`. Erros em `{Feature}Errors.cs`. SQL cru ou store dedicado só com justificativa em `plan.md` (`## Decisões`).
11
+ - **architecture-vertical-slice-vertical-slice** (`architecture/vertical-slice/vertical-slice.md`) — Uma slice por operação em `Features/{Entity}Feature/{Verb}{Entity}/`: Request/Response `record`, Handler, Validator e Endpoint `IApiEndpoint`. Decorators Scrutor: Logging → Validation → Handler. PROIBIDO: MediatR/`IRequest`, AggregateRoot, DomainEvent, CQRS formal, camada Application Service, DTO compartilhado entre features, `IRepository<T>`/`IUnitOfWork`.
12
+ - **backend-dotnet-vsa-handler-patterns** (`backend/dotnet/vsa-handler-patterns.md`) — Handler implementando `IHandler<TRequest, TResponse>` (não MediatR): injeta `ApplicationDbContext` direto e devolve `Result<T>`. Erros em `{Feature}Errors.cs`. SQL cru ou store dedicado só com justificativa em `plan.md` (`## Decisões`).
13
13
  - **backend-dotnet-result-pattern** (`backend/dotnet/result-pattern.md`) — Falha esperada é `Result`/`Result<T>`, nunca exceção. Endpoint: `result.Match(value => Results.Ok(value), error => error.ToHttpResult())`; nunca leia `.Value` fora do `Match`. Status pelo `ErrorType` (Validation 400, NotFound 404, Conflict 409, Unauthorized 401, Forbidden 403, Unexpected 500), nunca BadRequest para tudo. PROIBIDO: try/catch no handler, retornar null, erro string sem código.
14
- - **backend-dotnet-fluent-validation-vsa** (`backend/dotnet/fluent-validation-vsa.md`) — Um `sealed AbstractValidator<TRequest>` (FluentValidation 12) por request, na pasta da slice; o `ValidationDecorator` roda antes do handler e devolve `Result.Failure(ValidationError)`. `AddValidatorsFromAssembly()` ANTES de `AddHandlersFromAssembly()`. Sempre `.WithMessage()`; tempo via `TimeProvider` lido DENTRO da regra. Nunca lance `ValidationException`.
14
+ - **backend-dotnet-fluent-validation-vsa** (`backend/dotnet/fluent-validation-vsa.md`) — Um `AbstractValidator<TRequest>` (FluentValidation 12) por request, na pasta da slice; o `ValidationDecorator` roda antes do handler e devolve `Result.Failure(ValidationError)`. `AddValidatorsFromAssembly()` ANTES de `AddHandlersFromAssembly()`. Sempre `.WithMessage()`; tempo via `TimeProvider` lido DENTRO da regra. Nunca lance `ValidationException`.
15
15
  - **backend-dotnet-scrutor** (`backend/dotnet/scrutor.md`) — `AddHandlersFromAssembly` registra cada `IHandler<,>` como Scoped (filtro `!t.ContainsGenericParameters`) e depois `Decorate` Validation e então Logging (o último registrado é o mais externo). Endpoints `IApiEndpoint` via `TryAddEnumerable` Transient + `MapApiEndpoints`. Nunca registre handler um a um.
16
16
  - **backend-dotnet-scalar-ui** (`backend/dotnet/scalar-ui.md`) — Scalar no lugar do Swagger: `Microsoft.AspNetCore.OpenApi` 10.0.0 + `Scalar.AspNetCore` 2.12.40, nunca Swashbuckle. `MapOpenApi()` só em `IsDevelopment()` e antes de `MapScalarApiReference`. Todo endpoint com `.WithTags(ApiTags.X)` (constantes), `.Produces<T>` e `.ProducesProblem(status)` por falha possível.
17
+ - **backend-dotnet-analyzers** (`backend/dotnet/analyzers.md`) — O build cobra, a prosa não repete: `Guid.NewGuid()` é erro RS0030 (use `Guid.CreateVersion7()`) e token recebido e não repassado é erro CA2016, por `Directory.Build.props` + `BannedSymbols.txt` + `morph.globalconfig` na raiz. `sealed`, primary constructor e `var` são gosto: siga o arquivo vizinho.
17
18
  - **backend-api-validation** (`backend/api/validation.md`) — O validator checa só FORMA (sem DbContext); existência, unicidade e estado ficam no handler com `{Entity}Errors` tipados. Checagem de estado valida o INVÁLIDO (enum ordenado, estados de erro em 100+). PROIBIDO: lançar exceção, validar na lambda do endpoint, misturar DataAnnotations, `if (status != Expected)`, switch exaustivo sobre estados válidos.
18
19
  - **architecture-code-comments** (`architecture/code-comments.md`) — Padrão é nenhum comentário: renomeie, extraia, tipe ou teste antes. Só o porquê que o código não mostra (invariante, ordem, quirk externo com link), até ~3 linhas. PROIBIDO: história de processo (task, UC, ADR, Gate), repetir o código, XML doc/JSDoc que repete a assinatura, banner, `#region`, código comentado, TODO solto. O hook `comment-guard` bloqueia.
20
+ - **architecture-engineering-lens** (`architecture/engineering-lens.md`) — Escreva para quem herda amanhã: convenção do projeto, o mais simples, causa raiz. PROIBIDO no que você adiciona: `catch` que engole, flag booleana que bifurca o método (são dois métodos), efeito colateral escondido, símbolo/flag/config sem consumidor, abstração ou camada sem segundo uso, literal repetido sem constante. Nomes de domínio buscáveis, funções pequenas, ≤ 3 parâmetros, condicional positiva. Commit em Conventional Commits (hook `conventional-commit`).
19
21
 
20
22
  > Resumo injetado quando você lê ou edita um arquivo casado por `paths:`. Texto completo: `morph-spec standards --find "<id>"` e leia só a seção necessária de `.morph/framework/standards/<path>`.
@@ -40,8 +40,8 @@ For each new feature slice, follow the patterns in these templates:
40
40
  |----------|------|---------|
41
41
  | Endpoint | `.morph/framework/templates/code/dotnet/feature/endpoint.cs.hbs` | IApiEndpoint with MapEndpoint() |
42
42
  | Handler | `.morph/framework/templates/code/dotnet/feature/handler.cs.hbs` | IHandler<TRequest, TResponse> |
43
- | Request | `.morph/framework/templates/code/dotnet/feature/request.cs.hbs` | Sealed record DTO |
44
- | Response | `.morph/framework/templates/code/dotnet/feature/response.cs.hbs` | Sealed record DTO |
43
+ | Request | `.morph/framework/templates/code/dotnet/feature/request.cs.hbs` | record DTO |
44
+ | Response | `.morph/framework/templates/code/dotnet/feature/response.cs.hbs` | record DTO |
45
45
  | Validator | `.morph/framework/templates/code/dotnet/feature/validator.cs.hbs` | FluentValidation AbstractValidator |
46
46
  | Entity | `.morph/framework/templates/code/dotnet/feature/entity.cs.hbs` | Domain entity |
47
47
  | Errors | `.morph/framework/templates/code/dotnet/feature/errors.cs.hbs` | Error factory |
@@ -9,8 +9,7 @@ stacks:
9
9
 
10
10
  # Frontend Standards
11
11
 
12
- - **frontend-design-system-naming** (`frontend/design-system/naming.md`) — BEM para componente reutilizável (`.card`, `.card__header`, `.card--featured`); prefixo de página para elemento específico (`.home-hero`). Custom properties em `:root` com namespace `--app-`. Estado por sufixo fixo: `--active`, `--disabled`, `--loading`, `--error`.
13
- - **frontend-design-system-animations** (`frontend/design-system/animations.md`) — Classes `.animate-*`, `.duration-fast/normal/slow`, `.stagger-1..10`. Lista escalonada exige `animation-delay` E `animation-fill-mode: backwards`. Sempre o bloco `prefers-reduced-motion: reduce`. Além do CSS: Motion (`motion/react`) para enter/exit/layout; GSAP só para ScrollTrigger, SVG morph e timelines.
12
+ - **frontend-design-system-animations** (`frontend/design-system/animations.md`) — Lista escalonada exige `animation-delay` E `animation-fill-mode: backwards` (senão pisca antes do atraso). Sempre o bloco `prefers-reduced-motion: reduce`. Além do CSS: Motion (`motion/react`) para enter/exit/layout; GSAP só para ScrollTrigger, SVG morph e timelines.
14
13
  - **frontend-design-system-art-direction** (`frontend/design-system/art-direction.md`) — Tela nova ou alterada segue a direção escrita em `1-design/direcao.md` e o protótipo de `1-design/prototipos/`: celular composto (folha inferior, alvos de 44px), movimento só em `transform`/`opacity` com versão estática em `prefers-reduced-motion`, nenhum número inventado em cena ilustrativa.
15
14
 
16
15
  > Resumo injetado quando você lê ou edita um arquivo casado por `paths:`. Texto completo: `morph-spec standards --find "<id>"` e leia só a seção necessária de `.morph/framework/standards/<path>`.
@@ -8,16 +8,18 @@ stacks:
8
8
 
9
9
  # Next.js Standards
10
10
 
11
- - **frontend-nextjs-naming-conventions** (`frontend/nextjs/naming-conventions.md`) — Arquivo SEMPRE kebab-case (`user-card.tsx`); export PascalCase para componente e `useX` para hook. Zod em `.schemas.ts`, tipos em `.types.ts` via `z.infer`. Named export (só `page.tsx` usa `default`). Nunca misture casos na mesma categoria.
12
- - **frontend-nextjs-project-structure** (`frontend/nextjs/project-structure.md`) — `src/app/` só roteia e importa de `features/`. Cada `src/features/{f}/` tem `components/`, `hooks/`, `types/` e `index.ts` como API pública (import de `@/features/users`, nunca deep path). NUNCA uma feature importa outra; `components/ui/` é shadcn e não se edita.
13
- - **frontend-nextjs-app-router** (`frontend/nextjs/app-router.md`) — Server Component por padrão; `'use client'` só para interação, browser API ou TanStack Query. Nunca fetch em `useEffect`. `loading.tsx`/`error.tsx` ao lado do `page.tsx` (`error.tsx` é client). Server Component chama o .NET com `BACKEND_API_PREFIX`; evite `route.ts`.
14
- - **frontend-nextjs-components** (`frontend/nextjs/components.md`) — `components/ui/` é saída do shadcn CLI (nunca editar; envolva em `components/`); `components/` sem regra de negócio e sem importar `features/`; `features/*/components/` conhece o domínio. Props tipadas e validadas com Zod, sempre com `className` opcional.
15
- - **frontend-nextjs-data-fetching** (`frontend/nextjs/data-fetching.md`) — Server Component com `BACKEND_API_PREFIX`; Client Component só pelo proxy same-origin com `PROXY_API_PREFIX`. TanStack Query 5 com key factory por feature e `invalidateQueries({ queryKey })`; toda resposta passa por Zod. PROIBIDO: `useEffect(() => fetch…)` e paginar/filtrar a coleção inteira no browser.
11
+ - **frontend-nextjs-lint** (`frontend/nextjs/lint.md`) — O lint cobra o mecânico: `framer-motion`, Zustand/Redux, driver de banco, Form legado, deep import de feature, fronteiras `components`/`features`/`app`/`hooks`, fetch em `useEffect`, `'use client'` no layout raiz, `export default` fora de rota, `any`. O `verify` roda o eslint nos arquivos da feature quando há `eslint.config.*`; sem `eslint.config.mjs`: `morph-spec standards --show frontend-nextjs-lint#files`. Regra mecânica nova vira linha nele, nunca prosa.
12
+ - **frontend-nextjs-naming-conventions** (`frontend/nextjs/naming-conventions.md`) — Arquivo SEMPRE kebab-case (`user-card.tsx`); componente PascalCase. Zod em `.schemas.ts`, tipos em `.types.ts` via `z.infer`. Nunca misture casos na mesma categoria.
13
+ - **frontend-nextjs-project-structure** (`frontend/nextjs/project-structure.md`) — `src/app/` só roteia; a lógica da tela mora em `src/features/{f}/` (`components/`, `hooks/`, `types/`, `index.ts` como API pública). NUNCA uma feature importa outra (extraia para `components/` ou `lib/`); `components/ui/` é shadcn e não se edita.
14
+ - **frontend-nextjs-app-router** (`frontend/nextjs/app-router.md`) — Server Component por padrão; `'use client'` só para interação, browser API ou TanStack Query. `loading.tsx`/`error.tsx` ao lado do `page.tsx`. Server Component chama o .NET com `BACKEND_API_PREFIX`; evite `route.ts`.
15
+ - **frontend-nextjs-components** (`frontend/nextjs/components.md`) — `components/ui/` é saída do shadcn CLI (nunca editar; envolva em `components/`); `components/` sem regra de negócio; `features/*/components/` conhece o domínio e entrega props tipadas, nunca a resposta crua da API.
16
+ - **frontend-nextjs-data-fetching** (`frontend/nextjs/data-fetching.md`) — Server Component com `BACKEND_API_PREFIX`; Client Component só pelo proxy same-origin com `PROXY_API_PREFIX`. TanStack Query 5 com key factory por feature e `invalidateQueries({ queryKey })`; toda resposta passa por Zod. PROIBIDO paginar/filtrar a coleção inteira no browser.
16
17
  - **frontend-nextjs-forms** (`frontend/nextjs/forms.md`) — react-hook-form + Zod (`zodResolver`) + Field do shadcn via `Controller`; tipo por `z.infer`; nunca `useState` por campo. Submit por `useMutation`, erro em `root`. Edição reusa o schema de criação (`.partial()`). Zod 4: `z.email()`.
17
- - **frontend-nextjs-state-management** (`frontend/nextjs/state-management.md`) — Estado de servidor no TanStack Query, nunca copiado para `useState`. Página, ordenação, filtros e aba ativa na URL (`searchParams`). Context só para tema, usuário ou locale. NUNCA Zustand/Redux antes de esgotar Server Components + TanStack Query.
18
+ - **frontend-nextjs-state-management** (`frontend/nextjs/state-management.md`) — Estado de servidor no TanStack Query, nunca copiado para `useState`. Página, ordenação, filtros e aba ativa na URL (`searchParams`). Context só para tema, usuário ou locale.
18
19
  - **frontend-nextjs-testing** (`frontend/nextjs/testing.md`) — Vitest + Testing Library; MSW para API; Playwright para e2e. Teste comportamento, não implementação. Orçamento por risco: um teste por cenário com `risco:` + o de regressão do bug, componente sem risco é `leitura`/`e2e`; cenários do mesmo comportamento em `it.each`; nada de teste que só confere string de copy.
19
- - **frontend-nextjs-nextjs-patterns** (`frontend/nextjs/nextjs-patterns.md`) — Route Handler é BFF: `auth.getSession()` (401 sem sessão) e delega ao .NET; banco NUNCA em route handler. Zod no client e no server (`safeParse`, 400). `strict: true` + `noUncheckedIndexedAccess`; sem `any` em resposta de API. shadcn via `npx shadcn@latest add`.
20
- - **frontend-nextjs-motion-patterns** (`frontend/nextjs/motion-patterns.md`) — Motion via `motion/react` (nunca `framer-motion` junto). Variants em `motion/variants.ts` com durations/easings de `@/lib/design-tokens`. `useReducedMotion()` obrigatório; o `layout.tsx` raiz segue Server Component. 3–5 momentos de motion por página; cena guiada por scroll é do GSAP, nunca no mesmo elemento.
20
+ - **frontend-nextjs-nextjs-patterns** (`frontend/nextjs/nextjs-patterns.md`) — Route Handler é BFF: `auth.getSession()` (401 sem sessão) e delega ao .NET. Zod no client e no server (`safeParse`, 400). `strict: true` + `noUncheckedIndexedAccess`. shadcn via `npx shadcn@latest add`.
21
+ - **frontend-nextjs-motion-patterns** (`frontend/nextjs/motion-patterns.md`) — Motion via `motion/react`. Variants em `motion/variants.ts` com durations/easings de `@/lib/design-tokens`. `useReducedMotion()` obrigatório. Cena guiada por scroll é do GSAP, nunca no mesmo elemento que o Motion.
21
22
  - **architecture-code-comments** (`architecture/code-comments.md`) — Padrão é nenhum comentário: renomeie, extraia, tipe ou teste antes. Só o porquê que o código não mostra (invariante, ordem, quirk externo com link), até ~3 linhas. PROIBIDO: história de processo (task, UC, ADR, Gate), repetir o código, XML doc/JSDoc que repete a assinatura, banner, `#region`, código comentado, TODO solto. O hook `comment-guard` bloqueia.
23
+ - **architecture-engineering-lens** (`architecture/engineering-lens.md`) — Escreva para quem herda amanhã: convenção do projeto, o mais simples, causa raiz. PROIBIDO no que você adiciona: `catch` que engole, flag booleana que bifurca o método (são dois métodos), efeito colateral escondido, símbolo/flag/config sem consumidor, abstração ou camada sem segundo uso, literal repetido sem constante. Nomes de domínio buscáveis, funções pequenas, ≤ 3 parâmetros, condicional positiva. Commit em Conventional Commits (hook `conventional-commit`).
22
24
 
23
25
  > Resumo injetado quando você lê ou edita um arquivo casado por `paths:`. Texto completo: `morph-spec standards --find "<id>"` e leia só a seção necessária de `.morph/framework/standards/<path>`.