@polymorphism-tech/morph-spec 8.29.0 → 8.32.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 (183) hide show
  1. package/CLAUDE.md +11 -1
  2. package/README.md +13 -2
  3. package/bin/morph-spec.js +67 -1
  4. package/bin/validate.js +379 -379
  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 +4 -1
  9. package/framework/MORPH.md +4 -3
  10. package/framework/agents.json +6 -6
  11. package/framework/commands/morph-status.md +1 -1
  12. package/framework/docs/CLI.md +68 -45
  13. package/framework/evals/rubrics/code-quality.md +17 -0
  14. package/framework/evals/rubrics/condensed.md +3 -1
  15. package/framework/hooks/claude-code/statusline.sh +11 -11
  16. package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
  17. package/framework/hooks/git/pre-commit/agents.sh +25 -25
  18. package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
  19. package/framework/hooks/git/pre-commit/specs.sh +50 -50
  20. package/framework/hooks/git/pre-push/run-tests.sh +44 -44
  21. package/framework/hooks/shared/phase-utils.js +2 -0
  22. package/framework/hooks/shared/stdin-reader.js +26 -26
  23. package/framework/mcp/registry.json +39 -4
  24. package/framework/rubrics/backend.json +2 -2
  25. package/framework/rubrics/default.json +2 -2
  26. package/framework/rubrics/frontend.json +2 -2
  27. package/framework/schemas/tasks.schema.json +25 -0
  28. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  29. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  30. package/framework/skills/core/morph-apply/SKILL.md +14 -4
  31. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  32. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  33. package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
  34. package/framework/skills/core/morph-implement/SKILL.md +12 -2
  35. package/framework/skills/core/morph-plan/SKILL.md +45 -3
  36. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  37. package/framework/skills/core/morph-review/SKILL.md +12 -4
  38. package/framework/skills/core/morph-stack-scan/SKILL.md +13 -1
  39. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  40. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  41. package/framework/standards/ai-agents/agent-session.md +160 -160
  42. package/framework/standards/ai-agents/batching.md +193 -193
  43. package/framework/standards/ai-agents/context-providers.md +188 -188
  44. package/framework/standards/ai-agents/mcp-server.md +250 -250
  45. package/framework/standards/ai-agents/mcp-tools.md +197 -197
  46. package/framework/standards/ai-agents/middleware-patterns.md +207 -207
  47. package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
  48. package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
  49. package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
  50. package/framework/standards/ai-agents/modalities-vision.md +201 -201
  51. package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
  52. package/framework/standards/ai-agents/production.md +231 -231
  53. package/framework/standards/ai-agents/prompts-in-db.md +214 -214
  54. package/framework/standards/ai-agents/providers/model-registry.md +158 -158
  55. package/framework/standards/ai-agents/service-tier-flex.md +135 -135
  56. package/framework/standards/ai-agents/structured-output.md +249 -249
  57. package/framework/standards/ai-agents/sweet-spot.md +109 -109
  58. package/framework/templates/.idea/morph-templates.xml +92 -0
  59. package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  60. package/framework/templates/IDE-SNIPPETS.md +262 -262
  61. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  62. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
  63. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
  64. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
  65. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
  66. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
  67. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
  68. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
  69. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
  70. package/framework/templates/code/dotnet/database/migration.cs +83 -83
  71. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
  72. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  73. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  74. package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
  75. package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
  76. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  77. package/framework/templates/code/typescript/contracts.ts +167 -167
  78. package/framework/templates/context/ARCHITECTURE.md +20 -0
  79. package/framework/templates/docs/clarifications.md +253 -253
  80. package/framework/templates/docs/schema-analysis.md +119 -119
  81. package/framework/templates/docs/ui-components.md +52 -52
  82. package/framework/templates/docs/ui-design-system.md +76 -76
  83. package/framework/templates/docs/ui-flows.md +167 -167
  84. package/framework/templates/docs/ui-mockups.md +98 -98
  85. package/framework/templates/feature/decisions.md +122 -122
  86. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
  87. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  88. package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
  89. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  90. package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
  91. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  92. package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
  93. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  94. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  95. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  96. package/framework/templates/infrastructure/github/README.md +56 -56
  97. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  98. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  99. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  100. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  101. package/framework/templates/integrations/asaas-client.cs +387 -387
  102. package/framework/templates/integrations/asaas-webhook.cs +351 -351
  103. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  104. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  105. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
  106. package/framework/templates/project/validate.js +120 -120
  107. package/framework/templates/state.template.json +17 -17
  108. package/framework/templates/ui/design-system.css +226 -226
  109. package/package.json +1 -1
  110. package/src/commands/dashboard/dashboard.js +53 -53
  111. package/src/commands/graph/affected.js +89 -0
  112. package/src/commands/graph/build.js +94 -0
  113. package/src/commands/graph/explain.js +96 -0
  114. package/src/commands/graph/status.js +93 -0
  115. package/src/commands/index.js +13 -13
  116. package/src/commands/project/branch.js +170 -0
  117. package/src/commands/project/checkpoint.js +209 -209
  118. package/src/commands/project/doctor.js +148 -9
  119. package/src/commands/project/fleet.js +10 -3
  120. package/src/commands/project/worktree.js +73 -50
  121. package/src/commands/state/finish.js +41 -26
  122. package/src/commands/state/list.js +79 -6
  123. package/src/core/paths/output-schema.js +7 -0
  124. package/src/core/state/lifecycle-audit.js +127 -0
  125. package/src/core/state/phase-state-machine.js +197 -197
  126. package/src/core/templates/template-renderer.js +477 -477
  127. package/src/dashboard/app/components/DetailPanel.jsx +11 -11
  128. package/src/dashboard/app/components/FlowGraph.jsx +17 -17
  129. package/src/dashboard/app/components/SearchFilter.jsx +22 -22
  130. package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
  131. package/src/dashboard/app/main.jsx +13 -13
  132. package/src/dashboard/app/pages/AgentMap.jsx +370 -370
  133. package/src/dashboard/app/pages/HookInspector.jsx +418 -418
  134. package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
  135. package/src/dashboard/app/pages/RulesMap.jsx +273 -273
  136. package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
  137. package/src/dashboard/app/pages/Standards.jsx +332 -332
  138. package/src/dashboard/app/styles/index.css +47 -47
  139. package/src/dashboard/index.html +12 -12
  140. package/src/dashboard/package.json +24 -24
  141. package/src/dashboard/scanner/phases-scanner.js +15 -15
  142. package/src/dashboard/scanner/rubrics-scanner.js +12 -12
  143. package/src/dashboard/scanner/rules-scanner.js +37 -37
  144. package/src/dashboard/scanner/skills-scanner.js +43 -43
  145. package/src/dashboard/scanner/squads-scanner.js +13 -13
  146. package/src/dashboard/vite-plugin-morph-data.js +17 -17
  147. package/src/dashboard/vite.config.js +24 -24
  148. package/src/lib/detectors/design-system-detector.js +188 -188
  149. package/src/lib/detectors/index.js +217 -217
  150. package/src/lib/generators/architecture-scanner.js +45 -4
  151. package/src/lib/generators/design-system-generator.js +508 -508
  152. package/src/lib/generators/settings-generator.js +209 -209
  153. package/src/lib/git/branch-classifier.js +177 -0
  154. package/src/lib/git/gh.js +30 -0
  155. package/src/lib/git/pending-prs.js +89 -0
  156. package/src/lib/git/pr-index.js +70 -0
  157. package/src/lib/graph/god-nodes.js +75 -0
  158. package/src/lib/graph/graph-health.js +61 -0
  159. package/src/lib/graph/graph-loader.js +86 -0
  160. package/src/lib/graph/graphify-runtime.js +100 -0
  161. package/src/lib/graph/graphifyignore.js +86 -0
  162. package/src/lib/graph/sanitize.js +280 -0
  163. package/src/lib/hooks/hook-executor.js +167 -167
  164. package/src/lib/installers/mcp-installer.js +719 -719
  165. package/src/lib/tasks/task-classifier.js +30 -30
  166. package/src/lib/tasks/task-parser.js +209 -209
  167. package/src/lib/tasks/tasks-json-parser.js +26 -0
  168. package/src/lib/validators/checkpoint-persistence.js +39 -39
  169. package/src/lib/validators/css/css-validator.js +340 -340
  170. package/src/lib/validators/reuse/index.js +10 -0
  171. package/src/lib/validators/reuse/reuse-validator.js +324 -0
  172. package/src/lib/validators/reuse/symbol-scanner.js +144 -0
  173. package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
  174. package/src/lib/validators/shared/issue-counter.js +14 -14
  175. package/src/lib/validators/validation-runner.js +40 -1
  176. package/src/lib/worktree/fleet-scanner.js +51 -21
  177. package/src/lib/worktree/ignored-triage.js +197 -0
  178. package/src/lib/worktree/worktree-audit.js +200 -35
  179. package/src/scripts/setup-infra.js +60 -1
  180. package/src/utils/agent-id-mapper.js +11 -11
  181. package/src/utils/claude-settings-manager.js +300 -300
  182. package/src/utils/file-copier.js +40 -0
  183. package/src/utils/skills-installer.js +187 -187
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Reuse Validators
3
+ *
4
+ * Cobrança determinística de reuso de código: símbolo novo cujo nome já existe
5
+ * no grafo do codebase, na mesma linguagem. É a terceira camada do ciclo
6
+ * PROSA → ARTEFATO → VERIFICAÇÃO (ver reuse-validator.js).
7
+ */
8
+
9
+ export * from './reuse-validator.js';
10
+ export * from './symbol-scanner.js';
@@ -0,0 +1,324 @@
1
+ /**
2
+ * Reuse Validator — símbolo novo cujo nome já existe no codebase.
3
+ *
4
+ * A terceira camada do ciclo de reuso, no molde do `design-system-validator`:
5
+ *
6
+ * PROSA (morph-plan §1c, personas) ──► ARTEFATO (2-plan/reuse-map.md,
7
+ * `reuse` em tasks.json) ──► COBRANÇA (este arquivo)
8
+ *
9
+ * Prosa sozinha não segura — `morph-brainstorming/SKILL.md:39` já manda olhar
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.
13
+ *
14
+ * Regras, na ordem em que reduzem ruído:
15
+ *
16
+ * 1. Só tipo (class/record/interface/enum/struct/type) — não função nem
17
+ * variável. É a categoria que o grafo casa com confiança.
18
+ * 2. Só MESMA LINGUAGEM. Contrato espelhado entre C# e TS (110 casos no
19
+ * ProspectPRO) é o escopo F da proposta: exige gerar tipos do contrato,
20
+ * não se resolve por aviso, e fica FORA daqui de propósito.
21
+ * 3. Mesma feature = achado (`error`); feature diferente = aviso
22
+ * (`warning`), porque VSA prescreve DTO homônimo entre slices.
23
+ * 4. Veredito `CRIAR` declarado no plano (tasks.json `reuse`) rebaixa o
24
+ * achado a aviso: a decisão foi tomada no Gate 2, com justificativa no
25
+ * reuse-map. O validador cobra a decisão AUSENTE, não a decisão tomada.
26
+ *
27
+ * Sem grafo o validador não roda (ADR-002: ausência de grafo nunca bloqueia).
28
+ *
29
+ * @module reuse-validator
30
+ */
31
+
32
+ import { existsSync, readFileSync } from 'fs';
33
+ import { execFileSync } from 'child_process';
34
+ import { join, isAbsolute } from 'path';
35
+
36
+ import { loadGraph } from '../../graph/graph-loader.js';
37
+ import { languageOf } from '../../graph/sanitize.js';
38
+ import { getAbsoluteOutputPath } from '../../../core/paths/output-schema.js';
39
+ import {
40
+ featureOf,
41
+ isIdiomaticLocalName,
42
+ isScannableFile,
43
+ scanDeclarations,
44
+ } from './symbol-scanner.js';
45
+
46
+ /** Quantos locais citar por achado antes de resumir com "(+N)". */
47
+ const MAX_CITED = 4;
48
+
49
+ /**
50
+ * Índice "o que já existe": nome em minúsculas -> ocorrências reais do grafo.
51
+ *
52
+ * Só nós com `source_file` preenchido entram — nó com `source_file: ""` é stub
53
+ * não resolvido (BCL, NuGet, pacote externo), e apontar `Task` do .NET como
54
+ * duplicação seria ruído garantido. É o mesmo critério do `buildIndex`.
55
+ *
56
+ * @param {{nodes: object[]}} graph
57
+ * @returns {Map<string, Array<{name: string, file: string, language: string, feature: string}>>}
58
+ */
59
+ export function buildExistingIndex(graph) {
60
+ const index = new Map();
61
+
62
+ for (const node of graph?.nodes ?? []) {
63
+ if (!node.source_file) continue;
64
+ // `_callable_class` é a marca de tipo do graphify; nós de ARQUIVO também a
65
+ // carregam, e se distinguem pelo label com extensão.
66
+ if (node._callable_class !== true) continue;
67
+ if (/\.[a-z0-9]+$/i.test(node.label)) continue;
68
+ if (!isScannableFile(node.source_file)) continue;
69
+
70
+ const key = String(node.label).toLowerCase();
71
+ const entry = {
72
+ name: node.label,
73
+ file: node.source_file.replace(/\\/g, '/'),
74
+ language: languageOf(node.source_file),
75
+ feature: featureOf(node.source_file),
76
+ };
77
+ const bucket = index.get(key);
78
+ if (bucket) bucket.push(entry);
79
+ else index.set(key, [entry]);
80
+ }
81
+
82
+ return index;
83
+ }
84
+
85
+ /**
86
+ * Casa as declarações desta feature contra o que já existe.
87
+ *
88
+ * Puro e sem I/O — é aqui que a regra mora, e é isto que o teste roda contra o
89
+ * grafo real do ProspectPRO.
90
+ *
91
+ * @param {Array<{name: string, kind: string, line: number, file: string}>} declarations
92
+ * @param {Map<string, Array<{name: string, file: string, language: string, feature: string}>>} existing
93
+ * @param {{verdicts?: Map<string, {verdict: string, target?: string}>}} [opts]
94
+ * @returns {Array<object>} issues, uma por símbolo duplicado
95
+ */
96
+ export function findDuplicateSymbols(declarations, existing, opts = {}) {
97
+ const verdicts = opts.verdicts ?? new Map();
98
+ const issues = [];
99
+
100
+ // Uma declaração por (nome, arquivo); o mesmo nome em N arquivos da feature
101
+ // vira UM achado com N locais, não N achados.
102
+ const byName = new Map();
103
+ for (const decl of declarations) {
104
+ const key = decl.name.toLowerCase();
105
+ const bucket = byName.get(key);
106
+ if (bucket) bucket.push(decl);
107
+ else byName.set(key, [decl]);
108
+ }
109
+
110
+ for (const [key, decls] of byName) {
111
+ if (isIdiomaticLocalName(decls[0].name)) continue;
112
+
113
+ const declaredFiles = new Set(decls.map((d) => d.file));
114
+ const language = languageOf(decls[0].file);
115
+
116
+ // Onde mais esse nome vive: no grafo (código que já existia) e nos OUTROS
117
+ // arquivos desta mesma leva (código novo que o grafo ainda não viu — o
118
+ // rebuild é assíncrono, e é exatamente o caso SimpleBuilding).
119
+ const fromGraph = (existing.get(key) ?? []).filter(
120
+ (e) => e.language === language && !declaredFiles.has(e.file),
121
+ );
122
+ // O filtro de linguagem vale AQUI TAMBÉM, não só contra o grafo: uma leva
123
+ // que toca `CatalogContracts.cs` e `catalog.types.ts` declara `CatalogItem`
124
+ // nos dois, na mesma feature `Catalog/` — e sem este filtro isso vira erro,
125
+ // que é precisamente o contrato espelhado C#↔TS que a proposta separa no
126
+ // escopo F por não se resolver com aviso. Medido no MORPH_GHLBrain.
127
+ const fromBatch = decls
128
+ .slice(1)
129
+ .filter((d) => languageOf(d.file) === language)
130
+ .map((d) => ({ name: d.name, file: d.file, language, feature: featureOf(d.file) }));
131
+
132
+ const elsewhere = [...fromGraph, ...fromBatch];
133
+ if (elsewhere.length === 0) continue;
134
+
135
+ const homeFeature = featureOf(decls[0].file);
136
+ const intra = elsewhere.some((e) => e.feature === homeFeature);
137
+ const verdict = verdicts.get(key);
138
+ const planned = verdict?.verdict === 'CRIAR';
139
+
140
+ const locations = elsewhere.slice(0, MAX_CITED).map((e) => e.file);
141
+ const more = elsewhere.length - locations.length;
142
+
143
+ issues.push({
144
+ level: intra && !planned ? 'error' : 'warning',
145
+ scope: intra ? 'intra-feature' : 'cross-feature',
146
+ symbol: decls[0].name,
147
+ file: decls[0].file,
148
+ line: decls[0].line,
149
+ planned,
150
+ message:
151
+ `${decls[0].kind} "${decls[0].name}" já existe ${intra ? 'na MESMA feature' : 'no projeto'} ` +
152
+ `(${elsewhere.length}× em ${locations.join(', ')}${more > 0 ? ` (+${more})` : ''})` +
153
+ (planned ? ' — o plano declarou CRIAR, mas confira se a justificativa ainda vale' : ''),
154
+ solution: intra
155
+ ? `Reuse ou estenda o tipo existente em vez de redeclarar. Confirme com ` +
156
+ `\`morph-spec graph explain ${decls[0].name}\` (sem grafo: ` +
157
+ `\`grep -rn "${decls[0].name}"\`) e, se a criação for mesmo necessária, ` +
158
+ `registre o veredito CRIAR com justificativa no reuse-map da feature.`
159
+ : `VSA tolera DTO homônimo entre slices — confirme que são conceitos ` +
160
+ `diferentes e não a mesma coisa escrita duas vezes.`,
161
+ });
162
+ }
163
+
164
+ return issues.sort((a, b) => {
165
+ if (a.level !== b.level) return a.level === 'error' ? -1 : 1;
166
+ return a.symbol.localeCompare(b.symbol);
167
+ });
168
+ }
169
+
170
+ /**
171
+ * Vereditos de reuso carimbados no plano: `tasks[].reuse[]`.
172
+ *
173
+ * Ler do `tasks.json` — e não do `reuse-map.md` — é deliberado: o veredito é
174
+ * um dado estruturado, e o markdown existe para o humano ler no Gate 2. Duas
175
+ * leituras do mesmo fato, cada uma na forma certa para o seu consumidor.
176
+ *
177
+ * @param {string} projectPath
178
+ * @param {string} featureName
179
+ * @returns {Map<string, {verdict: string, target?: string}>}
180
+ */
181
+ export function readReuseVerdicts(projectPath, featureName) {
182
+ const verdicts = new Map();
183
+ if (!featureName) return verdicts;
184
+
185
+ try {
186
+ const tasksPath = getAbsoluteOutputPath(projectPath, featureName, 'tasks');
187
+ if (!existsSync(tasksPath)) return verdicts;
188
+ const doc = JSON.parse(readFileSync(tasksPath, 'utf8').replace(/^/, ''));
189
+ for (const task of doc.tasks ?? []) {
190
+ for (const entry of task.reuse ?? []) {
191
+ if (!entry?.concept) continue;
192
+ verdicts.set(String(entry.concept).toLowerCase(), {
193
+ verdict: String(entry.verdict || '').toUpperCase(),
194
+ target: entry.target,
195
+ });
196
+ }
197
+ }
198
+ } catch {
199
+ // Plano ilegível não é problema deste validador — o tasks-json-guard já
200
+ // cobra o schema, e aqui a ausência de veredito só torna a cobrança mais
201
+ // rígida, nunca mais frouxa.
202
+ }
203
+ return verdicts;
204
+ }
205
+
206
+ /**
207
+ * Arquivos que esta feature criou ou alterou.
208
+ *
209
+ * Duas fontes, unidas, porque cada uma é cega no momento em que a outra
210
+ * enxerga: `outputs` do `tasks.json` é o contrato do framework (morph-implement
211
+ * §8 exige listar TODO arquivo produzido) e sobrevive ao commit; `git status`
212
+ * pega o que ainda não foi registrado numa task. Sem git, sem drama.
213
+ *
214
+ * @param {string} projectPath
215
+ * @param {string} featureName
216
+ * @returns {string[]} paths relativos ao projeto, com barra normal
217
+ */
218
+ export function collectChangedFiles(projectPath, featureName) {
219
+ const files = new Set();
220
+
221
+ try {
222
+ const tasksPath = getAbsoluteOutputPath(projectPath, featureName, 'tasks');
223
+ if (existsSync(tasksPath)) {
224
+ const doc = JSON.parse(readFileSync(tasksPath, 'utf8').replace(/^/, ''));
225
+ for (const task of doc.tasks ?? []) {
226
+ for (const out of task.outputs ?? []) {
227
+ if (typeof out === 'string') files.add(out.replace(/\\/g, '/'));
228
+ }
229
+ }
230
+ }
231
+ } catch {
232
+ // tasks.json ausente/ilegível: sobra o git.
233
+ }
234
+
235
+ try {
236
+ const out = execFileSync('git', ['status', '--porcelain', '--untracked-files=all'], {
237
+ cwd: projectPath,
238
+ encoding: 'utf-8',
239
+ stdio: ['ignore', 'pipe', 'ignore'],
240
+ windowsHide: true,
241
+ });
242
+ for (const line of out.split(/\r?\n/)) {
243
+ if (!line.trim()) continue;
244
+ // "XY path" e, em rename, "XY antigo -> novo": só o destino interessa.
245
+ // Os paths vêm relativos à RAIZ do repositório — que no morph-spec é a
246
+ // própria árvore da feature (cada worktree é a sua raiz). Se algum dia
247
+ // não coincidir com `projectPath`, o `existsSync` adiante descarta o
248
+ // path: perde-se cobertura, nunca se acusa um arquivo que não existe.
249
+ const path = line.slice(3).split(' -> ').pop().replace(/^"|"$/g, '');
250
+ if (path) files.add(path.replace(/\\/g, '/'));
251
+ }
252
+ } catch {
253
+ // Fora de um repo git, ou git indisponível: o tasks.json já respondeu.
254
+ }
255
+
256
+ return [...files];
257
+ }
258
+
259
+ /**
260
+ * Validador plugado no `validation-runner` (e por ele no nó `validators` do
261
+ * `morph-spec verify`).
262
+ *
263
+ * @param {string} projectPath
264
+ * @param {string} featureName
265
+ * @param {object} [options]
266
+ * @param {string[]} [options.changedFiles] - override da coleta (testes)
267
+ * @param {object} [options._graph] - grafo já carregado (testes)
268
+ * @returns {Promise<{errors: number, warnings: number, issues: object[]}>}
269
+ */
270
+ export async function validateReuse(projectPath, featureName = null, options = {}) {
271
+ const result = { errors: 0, warnings: 0, issues: [] };
272
+
273
+ const graph = options._graph ?? (() => {
274
+ const loaded = loadGraph(projectPath);
275
+ return loaded.ok ? loaded.graph : null;
276
+ })();
277
+
278
+ if (!graph) {
279
+ // ADR-002: projeto sem grafo (sem Python, ou grafo ainda não construído)
280
+ // segue em frente. Um validador que reprovasse aqui transformaria uma
281
+ // dependência opcional em pré-requisito.
282
+ result._skipReason = 'sem grafo de codebase — `morph-spec graph build` habilita esta verificação';
283
+ return result;
284
+ }
285
+
286
+ const changed = options.changedFiles ?? collectChangedFiles(projectPath, featureName);
287
+ const declarations = [];
288
+
289
+ for (const rel of changed) {
290
+ if (!isScannableFile(rel)) continue;
291
+ const abs = isAbsolute(rel) ? rel : join(projectPath, rel);
292
+ if (!existsSync(abs)) continue;
293
+ let content;
294
+ try {
295
+ content = readFileSync(abs, 'utf8');
296
+ } catch {
297
+ continue;
298
+ }
299
+ for (const decl of scanDeclarations(content, rel)) {
300
+ declarations.push({ ...decl, file: rel.replace(/\\/g, '/') });
301
+ }
302
+ }
303
+
304
+ if (declarations.length === 0) {
305
+ result._skipReason = 'nenhum tipo novo declarado nos arquivos desta feature';
306
+ return result;
307
+ }
308
+
309
+ const existing = buildExistingIndex(graph);
310
+ const verdicts = readReuseVerdicts(projectPath, featureName);
311
+ const issues = findDuplicateSymbols(declarations, existing, { verdicts });
312
+
313
+ result.issues = issues.map((i) => ({
314
+ level: i.level,
315
+ message: i.message,
316
+ file: i.file,
317
+ line: i.line,
318
+ solution: i.solution,
319
+ }));
320
+ result.errors = issues.filter((i) => i.level === 'error').length;
321
+ result.warnings = issues.filter((i) => i.level === 'warning').length;
322
+
323
+ return result;
324
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Extração determinística de declarações de tipo a partir do TEXTO do arquivo.
3
+ *
4
+ * Por que não ler as declarações do grafo: o grafo é reconstruído em background
5
+ * pelo hook post-commit do graphify, então o código que acabou de ser escrito
6
+ * (o caso que este validador existe para pegar) ainda NÃO está nele. O grafo
7
+ * responde "o que já existia"; o disco responde "o que esta feature acabou de
8
+ * declarar". Inverter os dois papéis produziria um validador cego justamente na
9
+ * janela em que ele precisa morder — ver graph-loader.js `getFreshness`.
10
+ *
11
+ * A varredura é por linha, com regex ancorada no início da declaração. Mesma
12
+ * limitação (e mesmo grau de tolerância) do design-system-validator: uma
13
+ * declaração citada dentro de uma string ou de um comentário de bloco pode
14
+ * passar. O custo de errar é um aviso a mais, nunca um bloqueio silencioso.
15
+ *
16
+ * @module reuse-symbol-scanner
17
+ */
18
+
19
+ import { languageOf } from '../../graph/sanitize.js';
20
+
21
+ /** Extensões que este scanner sabe ler. Fora daqui, o arquivo é ignorado. */
22
+ const SCANNABLE = /\.(cs|ts|tsx|js|jsx|mjs|cjs)$/i;
23
+
24
+ /**
25
+ * Arquivo de teste, em qualquer das convenções que os dois stacks usam.
26
+ * Duplicar um stub entre três arquivos de teste (`ResizeObserverStub` no
27
+ * ProspectPRO) é ruído conhecido: teste é código descartável por natureza e a
28
+ * regra de reuso não se aplica a ele. Cobre `__tests__/`, `.test.`, `.spec.`,
29
+ * e os projetos `*.Tests/` do .NET.
30
+ */
31
+ const TEST_FILE = /(^|\/)__tests__\/|\.(test|spec)\.[jt]sx?$|(^|\/)(?:[Tt]ests?|[^/]*\.[Tt]ests?|[^/]*[a-z0-9_]Tests?)\//;
32
+
33
+ /**
34
+ * Diretórios que contêm arquivos com extensão de código mas que não são o
35
+ * código-fonte do projeto. `.morph/` é o caso que apareceu de verdade: o
36
+ * `archive/{feature}/1-design/contracts.cs` de uma feature arquivada entra no
37
+ * grafo do ProspectPRO como um tipo `RoulettePriority` a mais, e acusaria a
38
+ * entidade real de duplicar o próprio artefato de planejamento.
39
+ */
40
+ const NOT_SOURCE = /(^|\/)(?:\.morph|node_modules|dist|build|out|\.next|bin|obj|coverage)\//;
41
+
42
+ /**
43
+ * Declaration file do TypeScript. `auth-debug.d.ts` + `auth-debug.ts`
44
+ * declarando o mesmo tipo é o mecanismo da linguagem, não duplicação.
45
+ */
46
+ const DECLARATION_FILE = /\.d\.ts$/i;
47
+
48
+ /**
49
+ * Nomes que a convenção usa como tipo LOCAL de arquivo, não como conceito do
50
+ * domínio. Todo componente React declara o seu `Props`; dois arquivos com
51
+ * `interface Props` não são o mesmo conceito escrito duas vezes — são dois
52
+ * conceitos diferentes com o nome que a convenção manda usar.
53
+ *
54
+ * A lista é curta de propósito e só admite nome genérico de UMA palavra: assim
55
+ * que há qualificador de domínio (`BuildingFiltersProps`, `FilterOption`) o
56
+ * nome volta a ser um conceito e a regra vale. Mesmo papel da allowlist de
57
+ * cores de sistema no design-system-validator.
58
+ */
59
+ const IDIOMATIC_LOCAL_NAMES = new Set([
60
+ 'props', 'state', 'row', 'item', 'column', 'context', 'params', 'options',
61
+ ]);
62
+
63
+ /** Declaração de tipo em C#. `partial` é capturado para ser excluído depois. */
64
+ const CSHARP_DECL =
65
+ /^\s*(?:\[[^\]]*\]\s*)*(?:(?:public|internal|private|protected|sealed|abstract|static|readonly|new|file|partial)\s+)*(class|record|interface|enum|struct)\s+([A-Za-z_]\w*)/;
66
+
67
+ /** Declaração de tipo em TS/JS. */
68
+ const WEB_DECL =
69
+ /^\s*(?:export\s+(?:default\s+)?)?(?:declare\s+)?(?:abstract\s+)?(interface|type|class|enum)\s+([A-Za-z_$][\w$]*)/;
70
+
71
+ /** Linha que é claramente comentário — não vale a pena parsear. */
72
+ const COMMENT_LINE = /^\s*(?:\/\/|\/\*|\*|<!--)/;
73
+
74
+ /**
75
+ * @param {string} sourceFile
76
+ * @returns {boolean} o arquivo entra na varredura de reuso
77
+ */
78
+ export function isScannableFile(sourceFile) {
79
+ const f = String(sourceFile || '').replace(/\\/g, '/');
80
+ if (!SCANNABLE.test(f)) return false;
81
+ if (DECLARATION_FILE.test(f)) return false;
82
+ if (TEST_FILE.test(f)) return false;
83
+ if (NOT_SOURCE.test(f)) return false;
84
+ return true;
85
+ }
86
+
87
+ /**
88
+ * "Feature" de um path, com a mesma heurística do baseline
89
+ * (docs/specs/code-reuse-gap/measure-duplication.mjs): o segmento após
90
+ * `features/`/`Features/` quando existe, senão a pasta do arquivo. É o que
91
+ * separa o achado indefensável (mesma feature) do aviso tolerado por VSA.
92
+ *
93
+ * @param {string} sourceFile
94
+ * @returns {string}
95
+ */
96
+ export function featureOf(sourceFile) {
97
+ const f = String(sourceFile || '').replace(/\\/g, '/');
98
+ const m = /(?:^|\/)(?:features|Features)\/([^/]+)/.exec(f);
99
+ if (m) return m[1].toLowerCase();
100
+ return f.split('/').slice(0, -1).join('/').toLowerCase();
101
+ }
102
+
103
+ /**
104
+ * @param {string} name
105
+ * @returns {boolean} nome genérico que a convenção usa como tipo local
106
+ */
107
+ export function isIdiomaticLocalName(name) {
108
+ return IDIOMATIC_LOCAL_NAMES.has(String(name).toLowerCase());
109
+ }
110
+
111
+ /**
112
+ * Declarações de tipo de um arquivo.
113
+ *
114
+ * `partial` (C#) é reconhecido e DESCARTADO: declarar a mesma classe em dois
115
+ * arquivos é o propósito da palavra-chave, e sinalizá-la seria um falso
116
+ * positivo garantido em qualquer projeto .NET com código gerado.
117
+ *
118
+ * @param {string} content - conteúdo do arquivo
119
+ * @param {string} sourceFile - path, usado só para decidir a linguagem
120
+ * @returns {Array<{name: string, kind: string, line: number}>}
121
+ */
122
+ export function scanDeclarations(content, sourceFile) {
123
+ const language = languageOf(sourceFile);
124
+ const re = language === 'dotnet' ? CSHARP_DECL : WEB_DECL;
125
+ const out = [];
126
+ const seen = new Set();
127
+
128
+ const lines = String(content || '').split(/\r?\n/);
129
+ for (let i = 0; i < lines.length; i++) {
130
+ const line = lines[i];
131
+ if (COMMENT_LINE.test(line)) continue;
132
+ if (language === 'dotnet' && /\bpartial\s+(class|record|interface|struct)\b/.test(line)) continue;
133
+
134
+ const m = re.exec(line);
135
+ if (!m) continue;
136
+
137
+ const [, kind, name] = m;
138
+ if (seen.has(name)) continue;
139
+ seen.add(name);
140
+ out.push({ name, kind, line: i + 1 });
141
+ }
142
+
143
+ return out;
144
+ }
@@ -1,14 +1,14 @@
1
- /**
2
- * Stub: validation dispatch was driven by tier-4 agents in v4.
3
- * In v5, validation is handled by morph-eval skill invoked directly
4
- * by the Evaluator role. This function is kept for call-site compatibility.
5
- *
6
- * @module emit-validator-dispatch
7
- */
8
-
9
- /**
10
- * No-op in v5. Validation dispatch is now handled by morph-eval skill.
11
- */
12
- export async function emitValidatorDispatch(_featureName, _phase, _cwd) {
13
- // v5: evaluation driven by morph-eval skill, not CLI dispatch
14
- }
1
+ /**
2
+ * Stub: validation dispatch was driven by tier-4 agents in v4.
3
+ * In v5, validation is handled by morph-eval skill invoked directly
4
+ * by the Evaluator role. This function is kept for call-site compatibility.
5
+ *
6
+ * @module emit-validator-dispatch
7
+ */
8
+
9
+ /**
10
+ * No-op in v5. Validation dispatch is now handled by morph-eval skill.
11
+ */
12
+ export async function emitValidatorDispatch(_featureName, _phase, _cwd) {
13
+ // v5: evaluation driven by morph-eval skill, not CLI dispatch
14
+ }
@@ -1,14 +1,14 @@
1
- /**
2
- * Shared issue counting utility for validators
3
- *
4
- *
5
- * @param {Array} issues - Array of validation issues with type property
6
- * @returns {{errors: number, warnings: number, infos: number}} Count by type
7
- */
8
- export function countIssues(issues) {
9
- return {
10
- errors: issues.filter((i) => i.type === 'error').length,
11
- warnings: issues.filter((i) => i.type === 'warning').length,
12
- infos: issues.filter((i) => i.type === 'info').length,
13
- };
14
- }
1
+ /**
2
+ * Shared issue counting utility for validators
3
+ *
4
+ *
5
+ * @param {Array} issues - Array of validation issues with type property
6
+ * @returns {{errors: number, warnings: number, infos: number}} Count by type
7
+ */
8
+ export function countIssues(issues) {
9
+ return {
10
+ errors: issues.filter((i) => i.type === 'error').length,
11
+ warnings: issues.filter((i) => i.type === 'warning').length,
12
+ infos: issues.filter((i) => i.type === 'info').length,
13
+ };
14
+ }
@@ -24,7 +24,7 @@ import { aggregateScores, loadRubric } from './score-aggregator.js';
24
24
  */
25
25
  const HEAVY_VALIDATORS = new Set([
26
26
  'architecture', 'packages', 'contract-compliance',
27
- 'css', 'design-system'
27
+ 'css', 'design-system', 'reuse'
28
28
  ]);
29
29
 
30
30
  /**
@@ -74,6 +74,22 @@ export async function runValidation(projectPath, featureName, options = {}) {
74
74
  // Merge constraint-enabled validators
75
75
  validatorIds.push(...constraints.enabledValidators);
76
76
 
77
+ // Reuse is stack-agnostic: "isto já existe?" é a mesma pergunta em C# e em
78
+ // TypeScript, e o validador se cala sozinho quando não há grafo (ADR-002).
79
+ // Por isso ele não entra no `defaultValidators` de stack profile nenhum —
80
+ // entra aqui, ANTES do early-return de "nenhum validador": num projeto de
81
+ // stack `empty`/`nodejs-cli` não há outro validador para carregá-lo, e é
82
+ // exatamente lá que ele seria a única verificação a rodar.
83
+ // Em `light` (checkpoint) ele fica de fora, como todo validador pesado — é
84
+ // uma leitura de graph.json (11-24 MB nos repos medidos) — e por isso está
85
+ // em HEAVY_VALIDATORS, que é onde essa política se declara. Não entra em
86
+ // `droppedByMode`: essa lista responde "o que o modo tirou da SUA lista", e
87
+ // é ela que distingue "light pulou os pesados" de "este stack não tem
88
+ // validador nenhum" na mensagem final.
89
+ if (!options.skipReuse && mode !== 'light') {
90
+ validatorIds.push('reuse');
91
+ }
92
+
77
93
  if (validatorIds.length === 0) {
78
94
  // Say which of the two it is. On a pure `dotnet` project every default
79
95
  // validator is heavy, so a light run legitimately runs none — but the old
@@ -115,6 +131,11 @@ export async function runValidation(projectPath, featureName, options = {}) {
115
131
  // Deduplicate
116
132
  const uniqueValidators = [...new Set(validatorIds)];
117
133
 
134
+ // Validadores que rodaram mas se declararam inaplicáveis (ex.: `reuse` num
135
+ // projeto sem grafo). Um validador assim não acha nada — e "não achou nada"
136
+ // é indistinguível de "verificou e está limpo" se ninguém contar.
137
+ const selfSkipped = [];
138
+
118
139
  // Run each validator
119
140
  for (const validatorId of uniqueValidators) {
120
141
  try {
@@ -127,6 +148,9 @@ export async function runValidation(projectPath, featureName, options = {}) {
127
148
  const validatorResult = await runSingleValidator(validatorId, projectPath, featureName, validatorOptions);
128
149
 
129
150
  if (validatorResult) {
151
+ if (validatorResult._skipReason) {
152
+ selfSkipped.push(`${validatorId}: ${validatorResult._skipReason}`);
153
+ }
130
154
  const { errors, warnings, issues } = normalizeValidatorResult(validatorId, validatorResult);
131
155
  result.errors += errors;
132
156
  result.warnings += warnings;
@@ -158,6 +182,16 @@ export async function runValidation(projectPath, featureName, options = {}) {
158
182
  }
159
183
  }
160
184
 
185
+ // Todos os validadores se declararam inaplicáveis: o resultado é SKIP, não um
186
+ // verde. Sem isto, um projeto de stack sem validadores e sem grafo receberia
187
+ // `validators: pass` do `morph-spec verify` tendo verificado exatamente nada
188
+ // — o modo de falha que este repositório já pagou caro (guardrail inerte).
189
+ if (selfSkipped.length > 0 && selfSkipped.length === uniqueValidators.length) {
190
+ result._skipReason = `No validators ran — ${selfSkipped.join('; ')}`;
191
+ result.summary = result._skipReason;
192
+ return result;
193
+ }
194
+
161
195
  result.passed = result.errors === 0;
162
196
  result.summary = result.passed
163
197
  ? `✅ All validations passed (${result.warnings} warnings)`
@@ -286,6 +320,11 @@ async function runSingleValidator(validatorId, projectPath, featureName, options
286
320
  return await validateDesignSystem(projectPath, featureName, options);
287
321
  }
288
322
 
323
+ case 'reuse': {
324
+ const { validateReuse } = await import('./reuse/reuse-validator.js');
325
+ return await validateReuse(projectPath, featureName, options);
326
+ }
327
+
289
328
  case 'nextjs-component': {
290
329
  const { validateNextComponentFiles } = await import('./nextjs/next-component-validator.js');
291
330
  return await validateNextComponentFiles(projectPath, options);