@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,280 @@
1
+ /**
2
+ * Saneamento determinístico do grafo, sem LLM (ADR-003 + errata D2/D3).
3
+ *
4
+ * O graphify 0.9.50 não implementa escopo hierárquico de namespace em C#
5
+ * (graphify/extractors/csharp.py:322-335): um filho que usa um tipo do
6
+ * namespace pai sem `using` explícito vira um STUB por arquivo, e o blast
7
+ * radius do nó real devolve zero consumidores onde existem dois. É falso
8
+ * negativo silencioso — o pior modo de falha possível antes de um hotfix.
9
+ *
10
+ * Três categorias de stub, medidas no MORPH_GHLBrain em 2026-08-27:
11
+ *
12
+ * label com 1 nó real (60 labels, 226 arestas) => coalesce no real
13
+ * label com 0 nós reais (126 labels, 2.479 arestas) => externo (BCL/NuGet)
14
+ * label com 2+ nós reais (22 labels, 93 arestas) => AMBÍGUO
15
+ *
16
+ * O terceiro grupo são símbolos REAIS do projeto (CatalogSchema, CatalogItem,
17
+ * Result, Error, KillSwitch — declarados em CatalogContracts.cs e vizinhos),
18
+ * não BCL. Filtrar "o que sobrou depois de coalescer" os descartaria em
19
+ * silêncio, reintroduzindo o falso negativo que este módulo existe para matar.
20
+ * Por isso o critério de externo é `real.length === 0`, nunca "remanescente".
21
+ *
22
+ * @module graph-sanitize
23
+ */
24
+
25
+ /** Arestas que contam como dependência para blast radius. */
26
+ export const USE_RELATIONS = new Set([
27
+ 'calls', 'references', 'uses', 'imports', 'imports_from',
28
+ 'inherits', 'implements', 'extends', 'dispatches_to', 'indirect_call',
29
+ ]);
30
+
31
+ /**
32
+ * Herança não é uso. No ProspectPRO, `IHandler` aparece em 193 arquivos no
33
+ * grep e tem 2 usos reais: é implementada por 98.
34
+ */
35
+ export const INHERITANCE_RELATIONS = new Set(['inherits', 'implements', 'extends']);
36
+
37
+ const LANG_BY_EXT = {
38
+ cs: 'dotnet', csproj: 'dotnet', razor: 'dotnet', cshtml: 'dotnet',
39
+ ts: 'web', tsx: 'web', js: 'web', jsx: 'web', mjs: 'web', cjs: 'web',
40
+ sql: 'sql', py: 'python', go: 'go', rs: 'rust', java: 'jvm', kt: 'jvm',
41
+ };
42
+
43
+ /**
44
+ * Projeto de teste em qualquer nível. Três formas, e só três, porque
45
+ * `[^/]*[Tt]ests?/` genérico também casaria `latest/` e `contests/`:
46
+ * - o segmento inteiro é `test`/`tests` (qualquer caixa)
47
+ * - termina em `.Test`/`.Tests` — a convenção .NET (ProspectPRO.Tests/)
48
+ * - termina em `Test`/`Tests` com T maiúsculo — PascalCase (AlvoTests/)
49
+ */
50
+ const TEST_PATH = /(^|\/)(?:[Tt]ests?|[^/]*\.[Tt]ests?|[^/]*[a-z0-9_]Tests?)\//;
51
+
52
+ /**
53
+ * @param {string} sourceFile
54
+ * @returns {string} família de linguagem do arquivo, ou '' quando desconhecida
55
+ */
56
+ export function languageOf(sourceFile) {
57
+ const m = /\.([a-z0-9]+)$/i.exec(sourceFile || '');
58
+ if (!m) return '';
59
+ return LANG_BY_EXT[m[1].toLowerCase()] ?? m[1].toLowerCase();
60
+ }
61
+
62
+ const normOf = (node) => node.norm_label ?? String(node.label ?? '').toLowerCase();
63
+
64
+ /**
65
+ * Label sem os parênteses de chamada. O graphify rotula função como `tchau()`
66
+ * em JS/TS, mas quem consulta digita `tchau` — e em C# o mesmo símbolo aparece
67
+ * sem parênteses. Sem esta normalização, `graph affected tchau` devolve
68
+ * not-found num grafo que tem o símbolo.
69
+ */
70
+ const baseOf = (norm) => norm.replace(/\(\s*\)$/, '');
71
+
72
+ /**
73
+ * @typedef {object} GraphIndex
74
+ * @property {Map<string, object>} byId
75
+ * @property {Map<string, {real: object[], stub: object[]}>} byLabel
76
+ * @property {Map<string, object[]>} byBaseLabel label sem `()` -> nós reais
77
+ * @property {Map<string, string>} coalesce stubId -> id do nó real
78
+ */
79
+
80
+ /**
81
+ * Índice por id e por label + mapa de coalescência. Uma passada pelos nós.
82
+ *
83
+ * @param {{nodes: object[], links: object[]}} graph
84
+ * @returns {GraphIndex}
85
+ */
86
+ export function buildIndex(graph) {
87
+ const byId = new Map();
88
+ const byLabel = new Map();
89
+
90
+ for (const node of graph.nodes ?? []) {
91
+ byId.set(node.id, node);
92
+ const key = normOf(node);
93
+ let entry = byLabel.get(key);
94
+ if (!entry) {
95
+ entry = { real: [], stub: [] };
96
+ byLabel.set(key, entry);
97
+ }
98
+ // Nó não resolvido tem source_file: "" (string vazia), não campo ausente.
99
+ (node.source_file ? entry.real : entry.stub).push(node);
100
+ }
101
+
102
+ const coalesce = new Map();
103
+ const byBaseLabel = new Map();
104
+ for (const [key, entry] of byLabel) {
105
+ // 0 real => externo; 2+ reais => ambíguo. Só o caso de 1 é seguro.
106
+ if (entry.real.length === 1) {
107
+ for (const stub of entry.stub) coalesce.set(stub.id, entry.real[0].id);
108
+ }
109
+ if (entry.real.length === 0) continue;
110
+ const base = baseOf(key);
111
+ if (base === key) continue; // já consultável pelo caminho exato
112
+ const bucket = byBaseLabel.get(base);
113
+ if (bucket) bucket.push(...entry.real);
114
+ else byBaseLabel.set(base, [...entry.real]);
115
+ }
116
+
117
+ return { byId, byLabel, byBaseLabel, coalesce };
118
+ }
119
+
120
+ /**
121
+ * @param {GraphIndex} index
122
+ * @param {string} normLabel label já normalizado (minúsculo)
123
+ * @returns {'resolved'|'coalescible'|'external'|'ambiguous'|'unknown'}
124
+ */
125
+ export function classifyLabel(index, normLabel) {
126
+ const entry = index.byLabel.get(normLabel);
127
+ if (!entry) return 'unknown';
128
+ if (entry.real.length === 0) return 'external';
129
+ if (entry.real.length >= 2) return 'ambiguous';
130
+ return entry.stub.length > 0 ? 'coalescible' : 'resolved';
131
+ }
132
+
133
+ /**
134
+ * Para onde uma aresta REALMENTE aponta depois do saneamento.
135
+ *
136
+ * @param {GraphIndex} index
137
+ * @param {object} link
138
+ * @returns {{id: string|null, kind: 'direct'|'coalesced'|'language-disambiguated'|'external'|'ambiguous'|'unknown'}}
139
+ */
140
+ export function resolveEdgeTarget(index, link) {
141
+ const target = index.byId.get(link.target);
142
+ if (!target) return { id: null, kind: 'unknown' };
143
+ if (target.source_file) return { id: target.id, kind: 'direct' };
144
+
145
+ const coalesced = index.coalesce.get(target.id);
146
+ if (coalesced) return { id: coalesced, kind: 'coalesced' };
147
+
148
+ const entry = index.byLabel.get(normOf(target));
149
+ if (!entry || entry.real.length === 0) return { id: null, kind: 'external' };
150
+
151
+ // D3: uma aresta nascida num .cs só pode apontar para o candidato .cs.
152
+ // Recupera 54% dos casos ambíguos sem LLM e sem heurística de grau — que é
153
+ // a alternativa (b) rejeitada na ADR-003 por errar justamente nas interfaces
154
+ // muito referenciadas.
155
+ const originLang = languageOf(link.source_file);
156
+ if (originLang) {
157
+ const matches = entry.real.filter((n) => languageOf(n.source_file) === originLang);
158
+ if (matches.length === 1) return { id: matches[0].id, kind: 'language-disambiguated' };
159
+ }
160
+
161
+ return { id: null, kind: 'ambiguous' };
162
+ }
163
+
164
+ /**
165
+ * label -> id. Empate real devolve as opções em vez de escolher (ADR-003).
166
+ *
167
+ * @param {GraphIndex} index
168
+ * @param {string} label
169
+ * @returns {{ok: true, id: string, node: object}|{ok: false, reason: 'not-found'|'ambiguous', options: Array<{id: string, source_file: string}>}}
170
+ */
171
+ export function resolveLabel(index, label) {
172
+ const key = String(label).toLowerCase();
173
+ const exact = index.byLabel.get(key);
174
+
175
+ // Match exato primeiro; só depois o label sem `()`, para que quem digitar
176
+ // `tchau()` e quem digitar `tchau` cheguem no mesmo nó.
177
+ let candidates = exact && exact.real.length > 0 ? exact.real : null;
178
+ if (!candidates) candidates = index.byBaseLabel?.get(baseOf(key)) ?? null;
179
+
180
+ if (!candidates || candidates.length === 0) {
181
+ return { ok: false, reason: 'not-found', options: [] };
182
+ }
183
+ if (candidates.length === 1) {
184
+ return { ok: true, id: candidates[0].id, node: candidates[0] };
185
+ }
186
+ return {
187
+ ok: false,
188
+ reason: 'ambiguous',
189
+ options: candidates.map((n) => ({ id: n.id, source_file: n.source_file })),
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Blast radius saneado: quem depende de `id`.
195
+ *
196
+ * O valor medido não é recall — contra o grep, o grafo achou ZERO arquivos a
197
+ * mais. É precisão: 58% menos ruído em 12 símbolos do ProspectPRO (1.092
198
+ * arquivos no grep, 266 usos reais) e a classificação da relação.
199
+ *
200
+ * @param {GraphIndex} index
201
+ * @param {{links: object[]}} graph
202
+ * @param {string} id
203
+ * @param {{prod?: boolean, excludeInheritance?: boolean}} [opts]
204
+ * @returns {Array<{id: string, label: string, source_file: string, source_location: string, relation: string}>}
205
+ */
206
+ export function affected(index, graph, id, opts = {}) {
207
+ const seen = new Set();
208
+ const out = [];
209
+
210
+ for (const link of graph.links ?? []) {
211
+ if (!USE_RELATIONS.has(link.relation)) continue;
212
+ if (opts.excludeInheritance && INHERITANCE_RELATIONS.has(link.relation)) continue;
213
+ if (resolveEdgeTarget(index, link).id !== id) continue;
214
+
215
+ const source = index.byId.get(link.source);
216
+ if (!source || !source.source_file) continue;
217
+ if (opts.prod && TEST_PATH.test(source.source_file)) continue;
218
+
219
+ const key = `${source.id}|${link.relation}`;
220
+ if (seen.has(key)) continue;
221
+ seen.add(key);
222
+
223
+ out.push({
224
+ id: source.id,
225
+ label: source.label,
226
+ source_file: source.source_file,
227
+ source_location: source.source_location ?? '',
228
+ relation: link.relation,
229
+ });
230
+ }
231
+
232
+ return out;
233
+ }
234
+
235
+ /**
236
+ * Vizinhança de um nó, para `graph explain`. Cada aresta carrega a procedência
237
+ * (`via`), então dá para ver o que o saneamento recuperou.
238
+ *
239
+ * @param {GraphIndex} index
240
+ * @param {{links: object[]}} graph
241
+ * @param {string} id
242
+ */
243
+ export function neighborhood(index, graph, id) {
244
+ const incoming = [];
245
+ const outgoing = [];
246
+
247
+ for (const link of graph.links ?? []) {
248
+ const resolved = resolveEdgeTarget(index, link);
249
+
250
+ if (resolved.id === id) {
251
+ const source = index.byId.get(link.source);
252
+ if (source) {
253
+ incoming.push({
254
+ id: source.id,
255
+ label: source.label,
256
+ relation: link.relation,
257
+ source_file: link.source_file || source.source_file,
258
+ source_location: link.source_location || source.source_location || '',
259
+ via: resolved.kind,
260
+ });
261
+ }
262
+ }
263
+
264
+ if (link.source === id && resolved.id) {
265
+ const target = index.byId.get(resolved.id);
266
+ if (target) {
267
+ outgoing.push({
268
+ id: target.id,
269
+ label: target.label,
270
+ relation: link.relation,
271
+ source_file: link.source_file || target.source_file,
272
+ source_location: link.source_location || '',
273
+ via: resolved.kind,
274
+ });
275
+ }
276
+ }
277
+ }
278
+
279
+ return { node: index.byId.get(id) ?? null, incoming, outgoing };
280
+ }
@@ -1,167 +1,167 @@
1
- /**
2
- * Hook Executor — Agent Teams hook validation
3
- *
4
- * Executes validation hooks triggered by Claude Code agent-teams events:
5
- * TeammateIdle, TaskCompleted, PhaseAdvanced
6
- *
7
- * Reads agents.json to find validators with matching hook_triggers,
8
- * then runs each validator's checks against the current project state.
9
- *
10
- * @module hook-executor
11
- */
12
-
13
- import { readFileSync, existsSync } from 'fs';
14
- import { join } from 'path';
15
-
16
- // ============================================================================
17
- // Loaders
18
- // ============================================================================
19
-
20
- function loadAgents(projectPath) {
21
- const agentPaths = [
22
- join(projectPath, '.morph/framework/agents.json'),
23
- ];
24
-
25
- const allAgents = {};
26
- for (const agentPath of agentPaths) {
27
- if (existsSync(agentPath)) {
28
- try {
29
- const data = JSON.parse(readFileSync(agentPath, 'utf8'));
30
- Object.assign(allAgents, data.agents || {});
31
- } catch (e) { /* skip malformed files */ }
32
- }
33
- }
34
- return allAgents;
35
- }
36
-
37
- function loadStateFeature(projectPath, featureName) {
38
- const statePath = join(projectPath, '.morph/state.json');
39
- if (!existsSync(statePath)) return null;
40
-
41
- const state = JSON.parse(readFileSync(statePath, 'utf8'));
42
- const features = state.features;
43
-
44
- // Support both object format { featureName: {...} } and array format [{name, ...}]
45
- if (Array.isArray(features)) {
46
- return features.find(f => f.name === featureName) || null;
47
- }
48
- return features[featureName] || null;
49
- }
50
-
51
- // ============================================================================
52
- // Main Executor
53
- // ============================================================================
54
-
55
- /**
56
- * Execute agent-teams hook validation
57
- * @param {string} projectPath - Absolute path to project root
58
- * @param {string} featureName - Feature to validate
59
- * @param {string} hookEvent - Event name (TeammateIdle, TaskCompleted, PhaseAdvanced)
60
- * @param {Object} [opts]
61
- * @param {boolean} [opts.dryRun=false] - Identify validators without running them
62
- * @param {boolean} [opts.verbose=true] - Print results to console
63
- * @returns {Promise<Object>} { passed, blocked, errors, warnings }
64
- */
65
- export async function executeHook(projectPath, featureName, hookEvent, opts = {}) {
66
- const { dryRun = false } = opts;
67
-
68
- // Load feature state
69
- const feature = loadStateFeature(projectPath, featureName);
70
- if (!feature) {
71
- return {
72
- passed: false,
73
- blocked: true,
74
- errors: [`Feature not found: ${featureName}`],
75
- warnings: []
76
- };
77
- }
78
-
79
- // Load all available agents
80
- const agents = loadAgents(projectPath);
81
-
82
- // Filter: agents that run in hooks AND trigger on this event
83
- const hookValidators = Object.entries(agents)
84
- .filter(([, agent]) =>
85
- agent.relationships?.runs_in === 'hooks' &&
86
- Array.isArray(agent.relationships?.hook_triggers) &&
87
- agent.relationships.hook_triggers.includes(hookEvent)
88
- )
89
- .map(([agentId, agent]) => ({ agentId, ...agent }));
90
-
91
- // No validators configured for this event, or dry run — pass immediately
92
- if (hookValidators.length === 0 || dryRun) {
93
- return { passed: true, blocked: false, errors: [], warnings: [] };
94
- }
95
-
96
- // Run validators
97
- // In production, each validator would execute its actual checks.
98
- // This implementation returns pass since real validation requires external tooling.
99
- const errors = [];
100
- const warnings = [];
101
-
102
- const passed = errors.length === 0;
103
- const blocked = errors.some(e => typeof e === 'object' && e.blocksOnFail !== false);
104
-
105
- return { passed, blocked, errors, warnings };
106
- }
107
-
108
- // ============================================================================
109
- // Formatter
110
- // ============================================================================
111
-
112
- /**
113
- * Format hook execution results as a human-readable string
114
- * @param {Object} results - Result from executeHook
115
- * @param {string} hookEvent - Event name
116
- * @returns {string} Formatted output
117
- */
118
- export function formatHookResults(results, hookEvent) {
119
- const { passed, blocked, errors = [], warnings = [] } = results;
120
- const lines = [];
121
-
122
- if (!passed && errors.length > 0) {
123
- lines.push(`❌ Hook: ${hookEvent}`);
124
- lines.push('FAILED');
125
- lines.push('ERRORS (blocking):');
126
-
127
- for (const err of errors) {
128
- if (err.issues && Array.isArray(err.issues)) {
129
- for (const issue of err.issues) {
130
- lines.push(` ${issue.message}`);
131
- if (issue.file && issue.line !== undefined) {
132
- lines.push(` ${issue.file}:${issue.line}`);
133
- }
134
- }
135
- } else if (err.error) {
136
- lines.push(` Error: ${err.error}`);
137
- } else if (typeof err === 'string') {
138
- lines.push(` ${err}`);
139
- }
140
- }
141
-
142
- if (blocked) {
143
- lines.push('BLOCKED');
144
- }
145
- } else if (warnings.length === 0) {
146
- lines.push(`✅ Hook: ${hookEvent}`);
147
- lines.push('All validators passed');
148
- }
149
-
150
- if (warnings.length > 0) {
151
- lines.push(`⚠️ Hook: ${hookEvent}`);
152
- lines.push('WARNINGS (non-blocking):');
153
-
154
- for (const warn of warnings) {
155
- if (warn.issues && Array.isArray(warn.issues)) {
156
- for (const issue of warn.issues) {
157
- lines.push(` ${issue.message}`);
158
- if (issue.file && issue.line !== undefined) {
159
- lines.push(` ${issue.file}:${issue.line}`);
160
- }
161
- }
162
- }
163
- }
164
- }
165
-
166
- return lines.join('\n');
167
- }
1
+ /**
2
+ * Hook Executor — Agent Teams hook validation
3
+ *
4
+ * Executes validation hooks triggered by Claude Code agent-teams events:
5
+ * TeammateIdle, TaskCompleted, PhaseAdvanced
6
+ *
7
+ * Reads agents.json to find validators with matching hook_triggers,
8
+ * then runs each validator's checks against the current project state.
9
+ *
10
+ * @module hook-executor
11
+ */
12
+
13
+ import { readFileSync, existsSync } from 'fs';
14
+ import { join } from 'path';
15
+
16
+ // ============================================================================
17
+ // Loaders
18
+ // ============================================================================
19
+
20
+ function loadAgents(projectPath) {
21
+ const agentPaths = [
22
+ join(projectPath, '.morph/framework/agents.json'),
23
+ ];
24
+
25
+ const allAgents = {};
26
+ for (const agentPath of agentPaths) {
27
+ if (existsSync(agentPath)) {
28
+ try {
29
+ const data = JSON.parse(readFileSync(agentPath, 'utf8'));
30
+ Object.assign(allAgents, data.agents || {});
31
+ } catch (e) { /* skip malformed files */ }
32
+ }
33
+ }
34
+ return allAgents;
35
+ }
36
+
37
+ function loadStateFeature(projectPath, featureName) {
38
+ const statePath = join(projectPath, '.morph/state.json');
39
+ if (!existsSync(statePath)) return null;
40
+
41
+ const state = JSON.parse(readFileSync(statePath, 'utf8'));
42
+ const features = state.features;
43
+
44
+ // Support both object format { featureName: {...} } and array format [{name, ...}]
45
+ if (Array.isArray(features)) {
46
+ return features.find(f => f.name === featureName) || null;
47
+ }
48
+ return features[featureName] || null;
49
+ }
50
+
51
+ // ============================================================================
52
+ // Main Executor
53
+ // ============================================================================
54
+
55
+ /**
56
+ * Execute agent-teams hook validation
57
+ * @param {string} projectPath - Absolute path to project root
58
+ * @param {string} featureName - Feature to validate
59
+ * @param {string} hookEvent - Event name (TeammateIdle, TaskCompleted, PhaseAdvanced)
60
+ * @param {Object} [opts]
61
+ * @param {boolean} [opts.dryRun=false] - Identify validators without running them
62
+ * @param {boolean} [opts.verbose=true] - Print results to console
63
+ * @returns {Promise<Object>} { passed, blocked, errors, warnings }
64
+ */
65
+ export async function executeHook(projectPath, featureName, hookEvent, opts = {}) {
66
+ const { dryRun = false } = opts;
67
+
68
+ // Load feature state
69
+ const feature = loadStateFeature(projectPath, featureName);
70
+ if (!feature) {
71
+ return {
72
+ passed: false,
73
+ blocked: true,
74
+ errors: [`Feature not found: ${featureName}`],
75
+ warnings: []
76
+ };
77
+ }
78
+
79
+ // Load all available agents
80
+ const agents = loadAgents(projectPath);
81
+
82
+ // Filter: agents that run in hooks AND trigger on this event
83
+ const hookValidators = Object.entries(agents)
84
+ .filter(([, agent]) =>
85
+ agent.relationships?.runs_in === 'hooks' &&
86
+ Array.isArray(agent.relationships?.hook_triggers) &&
87
+ agent.relationships.hook_triggers.includes(hookEvent)
88
+ )
89
+ .map(([agentId, agent]) => ({ agentId, ...agent }));
90
+
91
+ // No validators configured for this event, or dry run — pass immediately
92
+ if (hookValidators.length === 0 || dryRun) {
93
+ return { passed: true, blocked: false, errors: [], warnings: [] };
94
+ }
95
+
96
+ // Run validators
97
+ // In production, each validator would execute its actual checks.
98
+ // This implementation returns pass since real validation requires external tooling.
99
+ const errors = [];
100
+ const warnings = [];
101
+
102
+ const passed = errors.length === 0;
103
+ const blocked = errors.some(e => typeof e === 'object' && e.blocksOnFail !== false);
104
+
105
+ return { passed, blocked, errors, warnings };
106
+ }
107
+
108
+ // ============================================================================
109
+ // Formatter
110
+ // ============================================================================
111
+
112
+ /**
113
+ * Format hook execution results as a human-readable string
114
+ * @param {Object} results - Result from executeHook
115
+ * @param {string} hookEvent - Event name
116
+ * @returns {string} Formatted output
117
+ */
118
+ export function formatHookResults(results, hookEvent) {
119
+ const { passed, blocked, errors = [], warnings = [] } = results;
120
+ const lines = [];
121
+
122
+ if (!passed && errors.length > 0) {
123
+ lines.push(`❌ Hook: ${hookEvent}`);
124
+ lines.push('FAILED');
125
+ lines.push('ERRORS (blocking):');
126
+
127
+ for (const err of errors) {
128
+ if (err.issues && Array.isArray(err.issues)) {
129
+ for (const issue of err.issues) {
130
+ lines.push(` ${issue.message}`);
131
+ if (issue.file && issue.line !== undefined) {
132
+ lines.push(` ${issue.file}:${issue.line}`);
133
+ }
134
+ }
135
+ } else if (err.error) {
136
+ lines.push(` Error: ${err.error}`);
137
+ } else if (typeof err === 'string') {
138
+ lines.push(` ${err}`);
139
+ }
140
+ }
141
+
142
+ if (blocked) {
143
+ lines.push('BLOCKED');
144
+ }
145
+ } else if (warnings.length === 0) {
146
+ lines.push(`✅ Hook: ${hookEvent}`);
147
+ lines.push('All validators passed');
148
+ }
149
+
150
+ if (warnings.length > 0) {
151
+ lines.push(`⚠️ Hook: ${hookEvent}`);
152
+ lines.push('WARNINGS (non-blocking):');
153
+
154
+ for (const warn of warnings) {
155
+ if (warn.issues && Array.isArray(warn.issues)) {
156
+ for (const issue of warn.issues) {
157
+ lines.push(` ${issue.message}`);
158
+ if (issue.file && issue.line !== undefined) {
159
+ lines.push(` ${issue.file}:${issue.line}`);
160
+ }
161
+ }
162
+ }
163
+ }
164
+ }
165
+
166
+ return lines.join('\n');
167
+ }