@polymorphism-tech/morph-spec 8.47.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/CLAUDE.md +48 -39
  2. package/README.md +3 -4
  3. package/bin/morph-spec.js +37 -51
  4. package/bin/validate.js +3 -111
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +1 -1
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +41 -101
  9. package/framework/MORPH.md +95 -252
  10. package/framework/agents.json +75 -18
  11. package/framework/commands/morph-apply.md +5 -28
  12. package/framework/commands/morph-archive.md +1 -1
  13. package/framework/commands/morph-hotfix.md +8 -8
  14. package/framework/commands/morph-infra.md +1 -1
  15. package/framework/commands/morph-preflight.md +2 -3
  16. package/framework/commands/morph-proposal.md +2 -50
  17. package/framework/commands/morph-status.md +3 -3
  18. package/framework/docs/CLI.md +585 -286
  19. package/framework/evals/review-checklist.md +82 -41
  20. package/framework/hooks/README.md +10 -11
  21. package/framework/hooks/claude-code/core/comment-guard.js +1 -1
  22. package/framework/hooks/claude-code/core/denylist.json +33 -3
  23. package/framework/hooks/claude-code/core/enforce-phase-writes.js +16 -20
  24. package/framework/hooks/claude-code/core/gate-guard.js +2 -11
  25. package/framework/hooks/claude-code/core/pre-bash-denylist.js +38 -0
  26. package/framework/hooks/claude-code/core/protect-spec-files.js +30 -242
  27. package/framework/hooks/claude-code/core/state-sync.js +7 -21
  28. package/framework/hooks/claude-code/core/telemetry-log.js +3 -3
  29. package/framework/hooks/claude-code/core/validate-completion.js +33 -97
  30. package/framework/hooks/claude-code/core/warn-bash-source-write.js +31 -0
  31. package/framework/hooks/claude-code/core/worktree-context.js +5 -4
  32. package/framework/hooks/claude-code/statusline.py +55 -41
  33. package/framework/hooks/package.json +3 -0
  34. package/framework/hooks/shared/activity-logger.js +1 -1
  35. package/framework/hooks/shared/build-check.js +10 -6
  36. package/framework/hooks/shared/command-tokens.js +169 -0
  37. package/framework/hooks/shared/comment-rules.js +1 -1
  38. package/framework/hooks/shared/comment-scanner.js +10 -23
  39. package/framework/hooks/shared/comment-stripper.js +1 -1
  40. package/framework/hooks/shared/e2e-check.js +64 -27
  41. package/framework/hooks/shared/errata.js +72 -0
  42. package/framework/hooks/shared/feature-locator.js +1 -1
  43. package/framework/hooks/shared/frontend-locations.js +6 -0
  44. package/framework/hooks/shared/gate-question.js +18 -0
  45. package/framework/hooks/shared/phase-utils.js +15 -32
  46. package/framework/hooks/shared/slice-progress.js +118 -0
  47. package/framework/hooks/shared/spawn-command.js +23 -0
  48. package/framework/hooks/shared/state-reader.js +61 -87
  49. package/framework/hooks/shared/state-writer.js +2 -3
  50. package/framework/mcp/registry.json +3 -4
  51. package/framework/phases.json +23 -39
  52. package/framework/schemas/config.schema.json +14 -9
  53. package/framework/schemas/feature.schema.json +15 -50
  54. package/framework/skills/README.md +3 -4
  55. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  56. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  57. package/framework/skills/core/morph-apply/SKILL.md +149 -222
  58. package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
  59. package/framework/skills/core/morph-machine/SKILL.md +2 -2
  60. package/framework/skills/core/morph-media/SKILL.md +1 -1
  61. package/framework/skills/core/morph-plan/SKILL.md +137 -299
  62. package/framework/skills/core/morph-review/SKILL.md +111 -136
  63. package/framework/skills/core/morph-standards/SKILL.md +27 -39
  64. package/framework/skills/core/morph-uiux/SKILL.md +77 -152
  65. package/framework/standards/STANDARDS.json +86 -1218
  66. package/framework/standards/STANDARDS.meta.json +1305 -0
  67. package/framework/standards/ai-agents/evals-with-cache.md +1 -2
  68. package/framework/standards/ai-agents/setup.md +1 -1
  69. package/framework/standards/architecture/code-comments.md +6 -6
  70. package/framework/standards/integration/mcp/mcp-tools.md +7 -7
  71. package/framework/templates/README.md +2 -3
  72. package/framework/templates/REGISTRY.json +6 -170
  73. package/framework/templates/docs/plan.md +72 -0
  74. package/framework/templates/dotnet/ai-kit/README.md +1 -9
  75. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
  76. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
  77. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
  78. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
  79. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
  80. package/package.json +1 -1
  81. package/src/commands/e2e/e2e.js +2 -2
  82. package/src/commands/observability/cost.js +18 -60
  83. package/src/commands/project/doctor-procs.js +1 -1
  84. package/src/commands/project/doctor.js +9 -9
  85. package/src/commands/project/fleet.js +4 -4
  86. package/src/commands/project/heavy.js +3 -3
  87. package/src/commands/project/init.js +4 -0
  88. package/src/commands/project/setup-infra-cmd.js +2 -1
  89. package/src/commands/project/stack-scan.js +58 -0
  90. package/src/commands/project/standards.js +79 -25
  91. package/src/commands/project/status.js +29 -69
  92. package/src/commands/project/update.js +17 -1
  93. package/src/commands/project/worktree.js +1 -1
  94. package/src/commands/state/advance.js +62 -46
  95. package/src/commands/state/approve.js +81 -141
  96. package/src/commands/state/archive.js +20 -84
  97. package/src/commands/state/auto-approval.js +68 -0
  98. package/src/commands/state/create.js +28 -1
  99. package/src/commands/state/delete.js +1 -1
  100. package/src/commands/state/finish.js +11 -23
  101. package/src/commands/state/gate-check.js +85 -84
  102. package/src/commands/state/gate-decision.js +6 -4
  103. package/src/commands/state/handoff.js +1 -1
  104. package/src/commands/state/list.js +9 -9
  105. package/src/commands/state/report-check.js +157 -311
  106. package/src/commands/state/retype.js +9 -3
  107. package/src/commands/validation/mutate.js +77 -14
  108. package/src/commands/validation/plan-check.js +81 -0
  109. package/src/commands/validation/validate-feature.js +60 -135
  110. package/src/commands/validation/verify.js +40 -72
  111. package/src/core/paths/output-schema.js +19 -66
  112. package/src/core/state/gate-order.js +5 -6
  113. package/src/core/state/lifecycle-audit.js +2 -2
  114. package/src/core/state/state-manager.js +17 -98
  115. package/src/core/templates/template-registry.js +2 -2
  116. package/src/core/templates/template-seed.js +22 -0
  117. package/src/core/work-types.js +10 -10
  118. package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
  119. package/src/dashboard/app/components/CommandPalette.jsx +5 -5
  120. package/src/dashboard/app/components/EntityLink.jsx +1 -1
  121. package/src/dashboard/app/hooks/useMorphQuery.js +1 -1
  122. package/src/dashboard/app/lib/phases.js +2 -4
  123. package/src/dashboard/app/pages/Activity.jsx +1 -1
  124. package/src/dashboard/app/pages/FeatureDetail.jsx +139 -129
  125. package/src/dashboard/app/pages/Features.jsx +9 -9
  126. package/src/dashboard/app/pages/Guide.jsx +2 -2
  127. package/src/dashboard/app/pages/PhasePipeline.jsx +3 -4
  128. package/src/dashboard/app/pages/Standards.jsx +1 -1
  129. package/src/dashboard/scanner/activity-scanner.js +5 -15
  130. package/src/dashboard/scanner/agents-scanner.js +1 -1
  131. package/src/dashboard/scanner/docs-scanner.js +2 -2
  132. package/src/dashboard/scanner/features-scanner.js +95 -60
  133. package/src/dashboard/scanner/project-api.js +1 -1
  134. package/src/dashboard/scanner/standards-scanner.js +2 -1
  135. package/src/lib/ci/ci-verify.js +17 -1
  136. package/src/lib/ci/github-ci.js +1 -1
  137. package/src/lib/detectors/index.js +3 -8
  138. package/src/lib/generators/handoff-generator.js +34 -65
  139. package/src/lib/generators/settings-generator.js +11 -89
  140. package/src/lib/git/evidence-staleness.js +1 -1
  141. package/src/lib/git/tree-fingerprint.js +19 -7
  142. package/src/lib/git/tree-state.js +2 -2
  143. package/src/lib/heavy/cleanup.js +1 -5
  144. package/src/lib/installers/memory-installer.js +6 -8
  145. package/src/lib/mutation/commands.js +3 -2
  146. package/src/lib/{tasks/dotnet-test-introspection.js → mutation/dotnet-introspection.js} +17 -282
  147. package/src/lib/mutation/plan.js +14 -6
  148. package/src/lib/mutation/runner.js +1 -1
  149. package/src/lib/mutation/subset.js +46 -9
  150. package/src/lib/plan/artifact-vocabulary.js +60 -0
  151. package/src/lib/plan/checks.js +234 -0
  152. package/src/lib/plan/plan-parser.js +209 -0
  153. package/src/lib/plan/plan-template.js +18 -0
  154. package/src/lib/review-reports.js +222 -0
  155. package/src/lib/risk-detector.js +131 -140
  156. package/src/lib/specs/adr-index.js +50 -69
  157. package/src/lib/specs/design-seed.js +18 -0
  158. package/src/lib/specs/frontmatter.js +44 -0
  159. package/src/lib/specs/gfm-table.js +17 -0
  160. package/src/lib/specs/old-format.js +68 -0
  161. package/src/lib/specs/revocations.js +37 -95
  162. package/src/lib/specs/screen-checklist.js +18 -17
  163. package/src/lib/stack/dotnet-build-ignores.js +47 -0
  164. package/src/lib/stack/stack-scan.js +267 -0
  165. package/src/lib/standards/registry.js +43 -0
  166. package/src/lib/standards/sections.js +58 -0
  167. package/src/lib/tasks/eval-node.js +4 -12
  168. package/src/lib/tasks/test-nodes.js +1 -74
  169. package/src/lib/tasks/test-runner.js +75 -117
  170. package/src/lib/telemetry/metrics.js +8 -69
  171. package/src/lib/telemetry/telemetry-reader.js +5 -118
  172. package/src/lib/telemetry/verify-event.js +5 -7
  173. package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
  174. package/src/lib/validators/contracts/contract-compliance-validator.js +2 -2
  175. package/src/lib/validators/reuse/reuse-validator.js +22 -45
  176. package/src/lib/validators/traceability-check.js +85 -129
  177. package/src/lib/validators/verify-runner.js +118 -235
  178. package/src/lib/worktree/docker-audit.js +1 -2
  179. package/src/lib/worktree/fleet-scanner.js +25 -38
  180. package/src/scripts/setup-infra.js +9 -0
  181. package/src/utils/agents-installer.js +76 -9
  182. package/src/utils/claude-settings-manager.js +0 -35
  183. package/src/utils/file-copier.js +27 -1
  184. package/src/utils/hooks-installer.js +16 -9
  185. package/src/utils/skills-installer.js +1 -2
  186. package/framework/evals/trace-schema.md +0 -67
  187. package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
  188. package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
  189. package/framework/hooks/git/pre-commit/specs.sh +0 -50
  190. package/framework/hooks/shared/tasks-validator.js +0 -111
  191. package/framework/rules/morph-task-tracking.md +0 -54
  192. package/framework/rules/morph-workflow.md +0 -244
  193. package/framework/schemas/tasks.schema.json +0 -79
  194. package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
  195. package/framework/skills/core/morph-implement/SKILL.md +0 -194
  196. package/framework/skills/core/morph-proposal/SKILL.md +0 -152
  197. package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
  198. package/framework/templates/docs/proposal.md +0 -192
  199. package/framework/templates/docs/spec.md +0 -57
  200. package/framework/templates/docs/ui-components.md +0 -52
  201. package/framework/templates/docs/ui-design-system.md +0 -76
  202. package/framework/templates/docs/ui-flows.md +0 -167
  203. package/framework/templates/examples/spec-examples.md +0 -91
  204. package/framework/templates/feature/decisions.md +0 -124
  205. package/framework/templates/feature/recap.md +0 -145
  206. package/framework/templates/tasks.template.json +0 -17
  207. package/src/commands/state/usecase.js +0 -143
  208. package/src/commands/tasks/dag.js +0 -181
  209. package/src/lib/agents/output-collector.js +0 -76
  210. package/src/lib/evaluator-report.js +0 -104
  211. package/src/lib/specs/acervo-format.js +0 -70
  212. package/src/lib/specs/acervo-lifecycle.js +0 -264
  213. package/src/lib/specs/done-criteria-check.js +0 -200
  214. package/src/lib/specs/usecase-check.js +0 -301
  215. package/src/lib/specs/usecase-promoter.js +0 -301
  216. package/src/lib/specs/usecase-registry.js +0 -237
  217. package/src/lib/tasks/dispatch-planner.js +0 -319
  218. package/src/lib/tasks/mermaid-renderer.js +0 -132
  219. package/src/lib/tasks/task-classifier.js +0 -30
  220. package/src/lib/tasks/task-parser.js +0 -209
  221. package/src/lib/tasks/tasks-json-parser.js +0 -296
  222. package/src/lib/tasks/test-scope.js +0 -597
@@ -44,8 +44,8 @@ import {
44
44
  getActiveFeature,
45
45
  getFeaturePhase,
46
46
  getPendingGates,
47
- getTaskStats,
48
47
  } from '../../shared/state-reader.js';
48
+ import { sliceProgress } from '../../shared/slice-progress.js';
49
49
  import { contextResult, runStandalone } from '../../shared/hook-response.js';
50
50
  import { acquireClaim, foreignClaim, claimAge } from '../../shared/claim-registry.js';
51
51
  import { rotateActivityLogIfLarge } from '../../shared/activity-logger.js';
@@ -232,10 +232,11 @@ function buildActiveFeatureSummary(dir) {
232
232
  if (!active) return null;
233
233
 
234
234
  const phase = getFeaturePhase(active.name, dir);
235
- const stats = getTaskStats(active.feature);
235
+ const slices = sliceProgress(join(dir, '.morph', 'features', active.name));
236
236
  const pending = getPendingGates(active.name, dir);
237
237
 
238
- let text = `MORPH-SPEC: feature ativa ${active.name} (fase ${phase}) · tasks ${stats.completed}/${stats.total}`;
238
+ let text = `MORPH-SPEC: feature ativa ${active.name} (fase ${phase})`;
239
+ if (slices) text += ` · fatias ${slices.done}/${slices.total}`;
239
240
  if (pending.length) {
240
241
  text += ` · gates pendentes: ${pending.join(', ')}`;
241
242
  }
@@ -264,7 +265,7 @@ function claimBlock(dir, feature, sessionId) {
264
265
  if (!other) return '';
265
266
  return `\n⚠️ ATENÇÃO — a feature ${feature} tem claim VIVO de OUTRA sessão`
266
267
  + `\n pid ${other.pid}${other.path ? ` · ${other.path}` : ''} · visto há ${claimAge(other)}`
267
- + '\n Duas sessões na mesma feature já custaram um recap.md inteiro e um sub-agent'
268
+ + '\n Duas sessões na mesma feature já custaram um relatório de fatia inteiro e um sub-agent'
268
269
  + '\n (o worktree sumiu debaixo dele). Confirme com a outra sessão ANTES de agir —'
269
270
  + '\n principalmente antes de `finish`, `archive` ou `delete`.'
270
271
  + `\n Se aquela sessão morreu: morph-spec claim ${feature} --release`;
@@ -56,28 +56,26 @@ SEP = f" {GRAY}│{R} "
56
56
 
57
57
  # ── MORPH framework constants ────────────────────────────────────────────────
58
58
 
59
- # Mirrors derivePhase() in src/core/state/state-manager.js — the first phase
60
- # (scanning backwards) whose CANONICAL artifact exists wins. An empty folder
61
- # is not enough.
59
+ # Mirrors derivePhaseFrom() in framework/hooks/shared/state-reader.js — the
60
+ # first phase (scanning backwards) whose CANONICAL artifact exists wins. An
61
+ # empty folder is not enough; a canonical ending in '/' is any .md under it.
62
62
  PHASE_MAP = [
63
- ('4-review', 'review', 'review-report.md'),
64
- ('3-implement', 'implement', 'recap.md'),
65
- ('2-plan', 'plan', 'spec.md'),
63
+ ('4-review', 'review', 'verifier.md'),
64
+ ('3-implement', 'implement', 'reports/'),
66
65
  ('1-design', 'uiux', 'design-system.md'),
67
- ('0-proposal', 'proposal', 'proposal.md'),
66
+ ('2-plan', 'plan', 'plan.md'),
68
67
  ]
69
- PHASE_INDEX = {'proposal': 0, 'uiux': 1, 'plan': 2, 'implement': 3, 'review': 4}
68
+ PHASE_INDEX = {'plan': 0, 'uiux': 1, 'implement': 2, 'review': 3}
70
69
  PHASE_LABELS = {
71
- 'proposal': 'proposal',
72
- 'uiux': 'UI/UX',
73
70
  'plan': 'planning',
71
+ 'uiux': 'UI/UX',
74
72
  'implement': 'implement',
75
73
  'review': 'review',
76
74
  }
77
- PIPELINE_TOTAL = 5
75
+ PIPELINE_TOTAL = 4
78
76
 
79
77
  # Gates that can block; `implement` has none. Mirrors feature.schema.json.
80
- GATE_PHASES = ('proposal', 'uiux', 'plan', 'review')
78
+ GATE_PHASES = ('plan', 'uiux', 'review')
81
79
 
82
80
 
83
81
  # ── Auto-compact threshold ───────────────────────────────────────────────────
@@ -358,49 +356,65 @@ def get_worktree_label(payload, cwd):
358
356
  return None
359
357
 
360
358
 
361
- def _gate_implied_phase(gates):
362
- """Phase implied purely by approved gates (mirrors state-manager.js)."""
359
+ def _gate_implied_phase(gates, design_started):
360
+ """Phase implied purely by approved gates (mirrors state-reader.js)."""
363
361
  def approved(gate):
364
362
  return (gates.get(gate) or {}).get('approved') is True
365
363
  if approved('review'):
366
364
  return 'review'
367
365
  if approved('plan'):
368
- return 'implement'
369
- if approved('uiux'):
370
- return 'plan'
371
- return 'proposal'
366
+ return 'uiux' if design_started and not approved('uiux') else 'implement'
367
+ return 'plan'
368
+
369
+
370
+ def _has_artifact(folder, canonical):
371
+ if not canonical.endswith('/'):
372
+ return (folder / canonical).exists()
373
+ try:
374
+ return any(p.is_file() for p in (folder / canonical).glob('*.md'))
375
+ except OSError:
376
+ return False
372
377
 
373
378
 
374
379
  def derive_phase(feature_dir, gates):
375
380
  """Phase from the filesystem, bumped by approved gates."""
376
- artifact_phase = 'proposal'
381
+ artifact_phase = 'plan'
377
382
  for folder, phase, canonical in PHASE_MAP:
378
- if (feature_dir / folder / canonical).exists():
383
+ if _has_artifact(feature_dir / folder, canonical):
379
384
  artifact_phase = phase
380
385
  break
381
- implied = _gate_implied_phase(gates)
386
+ implied = _gate_implied_phase(gates, (feature_dir / '1-design').exists())
382
387
  if PHASE_INDEX[implied] > PHASE_INDEX[artifact_phase]:
383
388
  return implied
384
389
  return artifact_phase
385
390
 
386
391
 
387
- def count_tasks(feature_dir, feature_json):
388
- """(done, total) from 2-plan/tasks.json — the declared source of truth.
392
+ PARTIAL_MARKER = re.compile(r'^[\s#>*_]*PARCIAL\b')
389
393
 
390
- `skipped` counts as resolved: it will never be worked on, and leaving it
391
- out would pin the bar below 100%. Falls back to feature.json's aggregate
392
- counters, which the schema marks as informational.
394
+
395
+ def count_slices(feature_dir):
396
+ """(done, total) of the slices in 2-plan/checks.json.
397
+
398
+ Same rule as framework/hooks/shared/slice-progress.js: a slice is done when
399
+ 3-implement/reports/{id}.md exists and its first line is not PARCIAL.
393
400
  """
394
401
  try:
395
- raw = (feature_dir / '2-plan' / 'tasks.json').read_text(encoding='utf-8')
396
- tasks = json.loads(raw).get('tasks')
397
- if isinstance(tasks, list) and tasks:
398
- done = sum(1 for task in tasks if task.get('status') in ('done', 'skipped'))
399
- return done, len(tasks)
402
+ slices = json.loads((feature_dir / '2-plan' / 'checks.json').read_text(encoding='utf-8')).get('slices')
400
403
  except Exception:
401
- pass
402
- aggregate = feature_json.get('tasks') or {}
403
- return safe_int(aggregate.get('completed')), safe_int(aggregate.get('total'))
404
+ return 0, 0
405
+ if not isinstance(slices, list):
406
+ return 0, 0
407
+ ids = [s.get('id') for s in slices if isinstance(s, dict) and isinstance(s.get('id'), str)]
408
+ done = 0
409
+ for slice_id in ids:
410
+ try:
411
+ text = (feature_dir / '3-implement' / 'reports' / f'{slice_id}.md').read_text(encoding='utf-8')
412
+ except Exception:
413
+ continue
414
+ first = next((line for line in text.splitlines() if line.strip()), '')
415
+ if not PARTIAL_MARKER.match(first):
416
+ done += 1
417
+ return done, len(ids)
404
418
 
405
419
 
406
420
  def get_active_features(cwd):
@@ -428,15 +442,15 @@ def get_active_features(cwd):
428
442
  feature_dir = path.parent
429
443
  gates = data.get('approvalGates') or {}
430
444
  phase = derive_phase(feature_dir, gates)
431
- done, total = count_tasks(feature_dir, data)
445
+ done, total = count_slices(feature_dir)
432
446
  pending = None
433
447
  if phase in GATE_PHASES and (gates.get(phase) or {}).get('approved') is not True:
434
448
  pending = phase
435
449
  result.append({
436
450
  'name': feature_dir.name,
437
451
  'phase': phase,
438
- 'tasks_done': done,
439
- 'tasks_total': total,
452
+ 'slices_done': done,
453
+ 'slices_total': total,
440
454
  'pending': pending,
441
455
  })
442
456
  return result
@@ -484,7 +498,7 @@ def render_morph_line(feature, hook_name, columns):
484
498
  """Line 0 — the active MORPH feature.
485
499
 
486
500
  Goes through join_to_width like the status line: a feature name plus phase,
487
- task bar, gate and hook is the longest thing this script prints, and a
501
+ slice bar, gate and hook is the longest thing this script prints, and a
488
502
  wrapping line 0 would defeat the whole point of the single-row layout.
489
503
  """
490
504
  parts = [(0, f"{CYAN}{BOLD}► {feature['name']}{R}")]
@@ -497,12 +511,12 @@ def render_morph_line(feature, hook_name, columns):
497
511
  else:
498
512
  parts.append((1, f"{CYAN}{label} {position + 1}/{PIPELINE_TOTAL}{R}"))
499
513
 
500
- if feature['tasks_total'] > 0:
501
- pct = feature['tasks_done'] / feature['tasks_total'] * 100
514
+ if feature['slices_total'] > 0:
515
+ pct = feature['slices_done'] / feature['slices_total'] * 100
502
516
  parts.append((
503
517
  3,
504
518
  f"{GREEN}{progress_bar(pct, 6)} "
505
- f"{feature['tasks_done']}/{feature['tasks_total']} tasks{R}"
519
+ f"{feature['slices_done']}/{feature['slices_total']} fatias{R}"
506
520
  ))
507
521
 
508
522
  if feature['pending']:
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -18,7 +18,7 @@
18
18
  *
19
19
  * Exported names/signatures are unchanged from the previous doc-based
20
20
  * implementation on purpose — hooks outside this file's ownership
21
- * (gate-guard.js, tasks-json-guard.js, enforce-phase-writes.js,
21
+ * (gate-guard.js, enforce-phase-writes.js,
22
22
  * loop-detect.js, protect-spec-files.js, validate-completion.js) call
23
23
  * logHookActivity/logEvent and must keep working untouched. Only the
24
24
  * on-disk format changed.
@@ -14,7 +14,7 @@ import { join } from 'path';
14
14
  const DOTNET_BUILD_CMD = 'dotnet build --no-restore --nologo -v q';
15
15
  const TSC_BUILD_CMD = 'npx tsc --noEmit';
16
16
 
17
- function isDotnetStack(s) {
17
+ export function isDotnetStack(s) {
18
18
  return s.includes('dotnet') || s.includes('aspnet');
19
19
  }
20
20
 
@@ -70,16 +70,20 @@ export function getBuildCommand(stack, hasTsConfig = false) {
70
70
  * `getBuildCommand()`'s existing single-stack behavior.
71
71
  *
72
72
  * @param {string} stack
73
- * @param {{ cwd: string, frontendPath?: string, backendPath?: string }} opts
73
+ * `backendTarget` names the solution/project to build (relative to the backend
74
+ * directory) when that directory holds none itself.
75
+ *
76
+ * @param {{ cwd: string, frontendPath?: string, backendPath?: string, backendTarget?: string }} opts
74
77
  * @returns {Array<{ command: string, cwd: string }>}
75
78
  */
76
- export function getBuildSteps(stack, { cwd, frontendPath, backendPath } = {}) {
79
+ export function getBuildSteps(stack, { cwd, frontendPath, backendPath, backendTarget } = {}) {
77
80
  if (!stack || !cwd) return [];
78
81
  const s = stack.toLowerCase();
79
82
  const steps = [];
80
83
 
81
84
  if (isDotnetStack(s)) {
82
- steps.push({ command: DOTNET_BUILD_CMD, cwd: backendPath ? join(cwd, backendPath) : cwd });
85
+ const command = backendTarget ? DOTNET_BUILD_CMD.replace(/^dotnet build/, `dotnet build "${backendTarget}"`) : DOTNET_BUILD_CMD;
86
+ steps.push({ command, cwd: backendPath ? join(cwd, backendPath) : cwd });
83
87
  }
84
88
 
85
89
  if (isFrontendStack(s)) {
@@ -129,11 +133,11 @@ export function parseBuildResult(exitCode, output) {
129
133
  * @param {string} cwd - Project root directory
130
134
  * @param {string} stack - Project stack string
131
135
  * @param {number} [timeout=30000] - Timeout in ms
132
- * @param {{ frontendPath?: string, backendPath?: string }} [paths] - Monorepo sub-paths, from config.json → project
136
+ * @param {{ frontendPath?: string, backendPath?: string, backendTarget?: string }} [paths] - Monorepo sub-paths, from config.json → project
133
137
  * @returns {{ status: 'pass'|'fail'|'skip', output: string }}
134
138
  */
135
139
  export function runBuildCheck(cwd, stack, timeout = 30000, paths = {}) {
136
- const steps = getBuildSteps(stack, { cwd, frontendPath: paths.frontendPath, backendPath: paths.backendPath });
140
+ const steps = getBuildSteps(stack, { cwd, frontendPath: paths.frontendPath, backendPath: paths.backendPath, backendTarget: paths.backendTarget });
137
141
  if (steps.length === 0) return { status: 'skip', output: '' };
138
142
 
139
143
  const failures = [];
@@ -117,3 +117,172 @@ export function exitCodeMasked(command) {
117
117
  const last = segments[segments.length - 1] || '';
118
118
  return (segments.length > 1 && REPORTER.test(last)) || /\|\|/.test(last.replace(/"[^"]*"|'[^']*'/g, ''));
119
119
  }
120
+
121
+ const WRITE_REDIRECT = /^(?:[1&])?>>?\|?/;
122
+
123
+ function quotedTokens(text) {
124
+ const tokens = [];
125
+ const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
126
+ let m;
127
+ while ((m = re.exec(text)) !== null) {
128
+ tokens.push({ text: m[1] ?? m[2] ?? m[3], quoted: m[3] === undefined, single: m[2] !== undefined });
129
+ }
130
+ return tokens;
131
+ }
132
+
133
+ const CHAIN = /^(?:&&|\|\||\||\|&)$/;
134
+ const ASSIGNMENT = /^([A-Za-z_]\w*)=(.*)$/;
135
+ const REDIRECT = /^(?:\d|&)?[<>]/;
136
+ const SCRIPT_RUNNER = /^(?:python[\d.]*|py|node|bun|deno)$/;
137
+
138
+ function simpleCommands(head) {
139
+ const commands = [];
140
+ for (const segment of topLevelSegments(head).segments) {
141
+ let current = [];
142
+ for (const token of quotedTokens(segment)) {
143
+ if (!token.quoted && CHAIN.test(token.text)) { commands.push(current); current = []; } else current.push(token);
144
+ }
145
+ commands.push(current);
146
+ }
147
+ return commands.filter((c) => c.length > 0);
148
+ }
149
+
150
+ function positionals(args, takesValue) {
151
+ const out = [];
152
+ const values = [];
153
+ for (let i = 0; i < args.length; i++) {
154
+ const { text, quoted } = args[i];
155
+ if (!quoted && REDIRECT.test(text)) { if (/[<>]$/.test(text)) i++; continue; }
156
+ if (!quoted && text.startsWith('-') && text !== '-') {
157
+ if (takesValue(text) && i + 1 < args.length) values.push(args[++i].text);
158
+ continue;
159
+ }
160
+ out.push(text);
161
+ }
162
+ return { files: out, values };
163
+ }
164
+
165
+ function sedTargets(args) {
166
+ const inPlace = args.some(({ text, quoted }) => !quoted && (/^-[A-Za-z]*i/.test(text) || text.startsWith('--in-place')));
167
+ if (!inPlace) return [];
168
+ const { files, values } = positionals(args, (o) => /^-[A-Za-z]*[ef]$/.test(o) || o === '--expression' || o === '--file');
169
+ return values.length > 0 ? files : files.slice(1);
170
+ }
171
+
172
+ function perlTargets(args) {
173
+ if (!args.some(({ text, quoted }) => !quoted && /^-[A-Za-z]*i/.test(text))) return [];
174
+ const { files, values } = positionals(args, (o) => /^-[A-Za-z.]*[eE]$/.test(o));
175
+ return values.length > 0 ? files : files.slice(1);
176
+ }
177
+
178
+ function copyTargets(args) {
179
+ if (args.some(({ text }) => text === '-t' || text.startsWith('--target-directory'))) return [];
180
+ const { files } = positionals(args, () => false);
181
+ if (files.length < 2) return [];
182
+ const dest = files[files.length - 1];
183
+ if (!dest.endsWith('/')) return [dest];
184
+ return files.slice(0, -1).map((src) => dest + src.replace(/^.*\//, ''));
185
+ }
186
+
187
+ const PY_OPEN_WRITE = /\bopen\(\s*([^,()]+?)\s*,\s*(?:mode\s*=\s*)?(['"])([^'"]*)\2/g;
188
+ const JS_WRITE = /\b(?:writeFileSync|writeFile|appendFileSync|appendFile)\(\s*([^,()]+?)\s*,/g;
189
+ const PY_PATH_WRITE = /\.write_(?:text|bytes)\(/;
190
+ const STRING_LITERAL = /(['"`])((?:(?!\1)[^\\\n]|\\.)*)\1/g;
191
+ const PATH_LITERAL = /^[\w@./-]*[\w-]\.[A-Za-z]{1,5}$/;
192
+
193
+ function literalOf(expr) {
194
+ const m = /^(['"`])([^'"`]*)\1$/.exec(expr.trim());
195
+ return m ? m[2] : null;
196
+ }
197
+
198
+ // A write call with a computed path (`open(p, "w")`) counts every path-shaped literal in the script.
199
+ function scriptWrites(script) {
200
+ const literals = [];
201
+ let computed = PY_PATH_WRITE.test(script);
202
+ for (const m of script.matchAll(PY_OPEN_WRITE)) {
203
+ if (!/[wax+]/.test(m[3])) continue;
204
+ const lit = literalOf(m[1]);
205
+ if (lit === null) computed = true; else literals.push(lit);
206
+ }
207
+ for (const m of script.matchAll(JS_WRITE)) {
208
+ const lit = literalOf(m[1]);
209
+ if (lit === null) computed = true; else literals.push(lit);
210
+ }
211
+ if (!computed) return literals;
212
+ const mentioned = [...script.matchAll(STRING_LITERAL)].map((m) => m[2]).filter((s) => PATH_LITERAL.test(s));
213
+ return [...new Set([...literals, ...mentioned])];
214
+ }
215
+
216
+ function runnerScript(args, body) {
217
+ for (let i = 0; i < args.length; i++) {
218
+ if (['-c', '-e', '-p', '--eval', '--print'].includes(args[i].text) && !args[i].quoted) return args[i + 1]?.text ?? null;
219
+ }
220
+ const { files } = positionals(args, () => false);
221
+ return (files.length === 0 || files[0] === '-') ? body : null;
222
+ }
223
+
224
+ function heredocBody(lines, at) {
225
+ if (at < 0) return null;
226
+ const delim = /<<-?\s*(['"]?)([\w.-]+)\1/.exec(lines[at])?.[2];
227
+ if (!delim) return null;
228
+ const end = lines.findIndex((line, i) => i > at && line.trim() === delim);
229
+ return lines.slice(at + 1, end < 0 ? undefined : end).join('\n');
230
+ }
231
+
232
+ function commandWriteTargets(head, body) {
233
+ const vars = new Map();
234
+ const expand = ({ text, single }) => (single ? text : text.replace(/\$\{?([A-Za-z_]\w*)\}?/g, (all, name) => vars.get(name) ?? all));
235
+ const targets = [];
236
+ for (const tokens of simpleCommands(head)) {
237
+ let i = 0;
238
+ for (; i < tokens.length && !tokens[i].quoted && ASSIGNMENT.test(tokens[i].text); i++) {
239
+ const [, name, value] = ASSIGNMENT.exec(tokens[i].text);
240
+ vars.set(name, value);
241
+ }
242
+ if (i >= tokens.length) continue;
243
+ const word = tokens[i].text.replace(/^.*[\\/]/, '').replace(/\.exe$/i, '');
244
+ const args = tokens.slice(i + 1).map((t) => ({ ...t, text: expand(t) }));
245
+ if (word === 'sed') targets.push(...sedTargets(args));
246
+ else if (word === 'perl') targets.push(...perlTargets(args));
247
+ else if (word === 'cp' || word === 'mv') targets.push(...copyTargets(args));
248
+ else if (SCRIPT_RUNNER.test(word)) {
249
+ const script = runnerScript(args, body);
250
+ if (script) targets.push(...scriptWrites(script));
251
+ }
252
+ }
253
+ return targets;
254
+ }
255
+
256
+ /**
257
+ * Files a command writes: stdout redirection (`>`, `>>`, `1>`, `&>`), `tee`, `sed -i`,
258
+ * `perl -i`, the `cp`/`mv` destination, and a python/node `-c`/`-e`/heredoc script's writes.
259
+ * The redirect scan stops at the line that opens a heredoc: the body is data, and an
260
+ * apostrophe in it would derail the quote-aware split. Quoted operators are text.
261
+ * @param {string} command
262
+ * @returns {string[]}
263
+ */
264
+ export function writeTargets(command) {
265
+ const lines = String(command || '').split('\n');
266
+ const heredocAt = lines.findIndex((line) => line.includes('<<'));
267
+ const head = (heredocAt >= 0 ? lines.slice(0, heredocAt + 1) : lines).join('\n');
268
+ const tokens = quotedTokens(head);
269
+
270
+ const targets = [];
271
+ for (let i = 0; i < tokens.length; i++) {
272
+ const { text, quoted } = tokens[i];
273
+ if (quoted) continue;
274
+ const op = text.match(WRITE_REDIRECT);
275
+ if (op) {
276
+ const rest = text.slice(op[0].length);
277
+ const target = rest || tokens[i + 1]?.text;
278
+ if (target && !target.startsWith('&')) targets.push(target);
279
+ continue;
280
+ }
281
+ if (text.replace(/^.*[\/]/, '') === 'tee') {
282
+ for (let j = i + 1; j < tokens.length && !/^[;&|]/.test(tokens[j].text); j++) {
283
+ if (!tokens[j].text.startsWith('-')) targets.push(tokens[j].text);
284
+ }
285
+ }
286
+ }
287
+ return [...new Set([...targets, ...commandWriteTargets(head, heredocBody(lines, heredocAt))])];
288
+ }
@@ -2,7 +2,7 @@
2
2
  * The comment shapes the `architecture-code-comments` standard forbids in
3
3
  * every case, judged only on the comments an edit ADDED. Whether a short
4
4
  * comment restates the code is not decidable here; that stays with the
5
- * Gate 3 evaluator and the `code-budget` density.
5
+ * Gate 3 reviewer and the `code-budget` density.
6
6
  *
7
7
  * @module comment-rules
8
8
  */
@@ -10,42 +10,29 @@
10
10
 
11
11
  import { stripComments } from './comment-stripper.js';
12
12
 
13
- /** Vocabulário de processo do morph-spec, fora o id de task (abaixo). */
13
+ /**
14
+ * Id de cenário (`C{n}`) e de fatia (`F{n}`) do plano. A mesma grafia também é
15
+ * tecla de função, métrica, célula de planilha ou o modelo C-quatro: esses
16
+ * vizinhos não contam (casos em comment-scanner.test.js).
17
+ */
18
+ const PLAN_ID = new RegExp(String.raw`(?<!\b(?:key|tecla|press|aperte|cell|c[eé]lula|column|coluna)\s+)(?<![\w.#$/-])[CF]\d{1,2}(?![\w.(-]|\s+(?:key|tecla|score|model|modelo|diagram|diagrama)\b)`, 'i');
19
+
14
20
  const PROCESS_CITATION = [
15
21
  /\bUC[-\s]?\d+/,
16
22
  /\bADR[-\s]?\d+/,
17
23
  /\bFR[-\s]?\d+/,
18
24
  /\bgate\s?[1-3]\b/i,
19
- /\bspec\.md\b/i,
25
+ /\bplan\.md\b/i,
26
+ /\bchecks\.json\b/i,
20
27
  /\bmockups?\b/i,
21
28
  ];
22
29
 
23
- /**
24
- * `T{n}` também é nome de parâmetro genérico. Não casa depois de `template`/
25
- * `typename`/`generic`, nem colado a letra ou dígito (a hora ISO `24T10`).
26
- */
27
- const TASK_ID = /(?<!\b(?:[Tt]emplate|[Tt]ypename|[Gg]eneric)\s+)(?<![\w"])T\d{1,3}\b(?!")/;
28
-
29
- /** Lista de tipos `<…>` e `{…}` (`Func<T1, T2>`, `cref="Func{T1,T2}"`, XML doc), de dentro para fora. */
30
- const TYPE_LIST = /<[^<>]*>|\{[^{}]*\}/g;
31
-
32
- function withoutTypeLists(text) {
33
- let previous;
34
- let current = text;
35
- do {
36
- previous = current;
37
- current = current.replace(TYPE_LIST, ' ');
38
- } while (current !== previous);
39
- return current;
40
- }
41
-
42
30
  /**
43
31
  * @param {string} commentText
44
32
  * @returns {boolean} o comentário usa o vocabulário de processo acima
45
33
  */
46
34
  export function citesProcess(commentText) {
47
- return PROCESS_CITATION.some((re) => re.test(commentText))
48
- || TASK_ID.test(withoutTypeLists(commentText));
35
+ return PROCESS_CITATION.some((re) => re.test(commentText)) || PLAN_ID.test(commentText);
49
36
  }
50
37
 
51
38
  /**
@@ -10,7 +10,7 @@
10
10
  * - `registry-audit.js` — precisa ler **JSONC**, a forma que o exemplar
11
11
  * canônico `model-registry.v2.json` usa e que o leitor C# do kit aceita de
12
12
  * propósito (`JsonCommentHandling.Skip`, `AllowTrailingCommas`);
13
- * - `dotnet-test-introspection.js` — lê o `global.json` por `jsoncToJson`;
13
+ * - `mutation/dotnet-introspection.js` — lê o `global.json` por `jsoncToJson`;
14
14
  * - `comment-scanner.js` (ao lado; lido pelo `code-budget` e pelo hook
15
15
  * `comment-guard`) — acha as linhas de
16
16
  * comentário de `.cs`/`.ts`/`.js`, e é o único que liga a opção `js`.