@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.
- package/CLAUDE.md +11 -1
- package/README.md +13 -2
- package/bin/morph-spec.js +67 -1
- package/bin/validate.js +379 -379
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +4 -1
- package/framework/MORPH.md +4 -3
- package/framework/agents.json +6 -6
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +68 -45
- package/framework/evals/rubrics/code-quality.md +17 -0
- package/framework/evals/rubrics/condensed.md +3 -1
- package/framework/hooks/claude-code/statusline.sh +11 -11
- package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
- package/framework/hooks/git/pre-commit/agents.sh +25 -25
- package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
- package/framework/hooks/git/pre-commit/specs.sh +50 -50
- package/framework/hooks/git/pre-push/run-tests.sh +44 -44
- package/framework/hooks/shared/phase-utils.js +2 -0
- package/framework/hooks/shared/stdin-reader.js +26 -26
- package/framework/mcp/registry.json +39 -4
- package/framework/rubrics/backend.json +2 -2
- package/framework/rubrics/default.json +2 -2
- package/framework/rubrics/frontend.json +2 -2
- package/framework/schemas/tasks.schema.json +25 -0
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +14 -4
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
- package/framework/skills/core/morph-implement/SKILL.md +12 -2
- package/framework/skills/core/morph-plan/SKILL.md +45 -3
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +12 -4
- package/framework/skills/core/morph-stack-scan/SKILL.md +13 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +1 -1
- package/framework/standards/ai-agents/agent-session.md +160 -160
- package/framework/standards/ai-agents/batching.md +193 -193
- package/framework/standards/ai-agents/context-providers.md +188 -188
- package/framework/standards/ai-agents/mcp-server.md +250 -250
- package/framework/standards/ai-agents/mcp-tools.md +197 -197
- package/framework/standards/ai-agents/middleware-patterns.md +207 -207
- package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
- package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
- package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
- package/framework/standards/ai-agents/modalities-vision.md +201 -201
- package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
- package/framework/standards/ai-agents/production.md +231 -231
- package/framework/standards/ai-agents/prompts-in-db.md +214 -214
- package/framework/standards/ai-agents/providers/model-registry.md +158 -158
- package/framework/standards/ai-agents/service-tier-flex.md +135 -135
- package/framework/standards/ai-agents/structured-output.md +249 -249
- package/framework/standards/ai-agents/sweet-spot.md +109 -109
- package/framework/templates/.idea/morph-templates.xml +92 -0
- package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
- package/framework/templates/code/dotnet/database/migration.cs +83 -83
- package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
- package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
- package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/code/typescript/contracts.ts +167 -167
- package/framework/templates/context/ARCHITECTURE.md +20 -0
- package/framework/templates/docs/clarifications.md +253 -253
- package/framework/templates/docs/schema-analysis.md +119 -119
- package/framework/templates/docs/ui-components.md +52 -52
- package/framework/templates/docs/ui-design-system.md +76 -76
- package/framework/templates/docs/ui-flows.md +167 -167
- package/framework/templates/docs/ui-mockups.md +98 -98
- package/framework/templates/feature/decisions.md +122 -122
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
- package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/github/README.md +56 -56
- package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
- package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
- package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
- package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
- package/framework/templates/integrations/asaas-client.cs +387 -387
- package/framework/templates/integrations/asaas-webhook.cs +351 -351
- package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
- package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
- package/framework/templates/project/validate.js +120 -120
- package/framework/templates/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- package/package.json +1 -1
- package/src/commands/dashboard/dashboard.js +53 -53
- package/src/commands/graph/affected.js +89 -0
- package/src/commands/graph/build.js +94 -0
- package/src/commands/graph/explain.js +96 -0
- package/src/commands/graph/status.js +93 -0
- package/src/commands/index.js +13 -13
- package/src/commands/project/branch.js +170 -0
- package/src/commands/project/checkpoint.js +209 -209
- package/src/commands/project/doctor.js +148 -9
- package/src/commands/project/fleet.js +10 -3
- package/src/commands/project/worktree.js +73 -50
- package/src/commands/state/finish.js +41 -26
- package/src/commands/state/list.js +79 -6
- package/src/core/paths/output-schema.js +7 -0
- package/src/core/state/lifecycle-audit.js +127 -0
- package/src/core/state/phase-state-machine.js +197 -197
- package/src/core/templates/template-renderer.js +477 -477
- package/src/dashboard/app/components/DetailPanel.jsx +11 -11
- package/src/dashboard/app/components/FlowGraph.jsx +17 -17
- package/src/dashboard/app/components/SearchFilter.jsx +22 -22
- package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/AgentMap.jsx +370 -370
- package/src/dashboard/app/pages/HookInspector.jsx +418 -418
- package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
- package/src/dashboard/app/pages/RulesMap.jsx +273 -273
- package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
- package/src/dashboard/app/pages/Standards.jsx +332 -332
- package/src/dashboard/app/styles/index.css +47 -47
- package/src/dashboard/index.html +12 -12
- package/src/dashboard/package.json +24 -24
- package/src/dashboard/scanner/phases-scanner.js +15 -15
- package/src/dashboard/scanner/rubrics-scanner.js +12 -12
- package/src/dashboard/scanner/rules-scanner.js +37 -37
- package/src/dashboard/scanner/skills-scanner.js +43 -43
- package/src/dashboard/scanner/squads-scanner.js +13 -13
- package/src/dashboard/vite-plugin-morph-data.js +17 -17
- package/src/dashboard/vite.config.js +24 -24
- package/src/lib/detectors/design-system-detector.js +188 -188
- package/src/lib/detectors/index.js +217 -217
- package/src/lib/generators/architecture-scanner.js +45 -4
- package/src/lib/generators/design-system-generator.js +508 -508
- package/src/lib/generators/settings-generator.js +209 -209
- package/src/lib/git/branch-classifier.js +177 -0
- package/src/lib/git/gh.js +30 -0
- package/src/lib/git/pending-prs.js +89 -0
- package/src/lib/git/pr-index.js +70 -0
- package/src/lib/graph/god-nodes.js +75 -0
- package/src/lib/graph/graph-health.js +61 -0
- package/src/lib/graph/graph-loader.js +86 -0
- package/src/lib/graph/graphify-runtime.js +100 -0
- package/src/lib/graph/graphifyignore.js +86 -0
- package/src/lib/graph/sanitize.js +280 -0
- package/src/lib/hooks/hook-executor.js +167 -167
- package/src/lib/installers/mcp-installer.js +719 -719
- package/src/lib/tasks/task-classifier.js +30 -30
- package/src/lib/tasks/task-parser.js +209 -209
- package/src/lib/tasks/tasks-json-parser.js +26 -0
- package/src/lib/validators/checkpoint-persistence.js +39 -39
- package/src/lib/validators/css/css-validator.js +340 -340
- package/src/lib/validators/reuse/index.js +10 -0
- package/src/lib/validators/reuse/reuse-validator.js +324 -0
- package/src/lib/validators/reuse/symbol-scanner.js +144 -0
- package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
- package/src/lib/validators/shared/issue-counter.js +14 -14
- package/src/lib/validators/validation-runner.js +40 -1
- package/src/lib/worktree/fleet-scanner.js +51 -21
- package/src/lib/worktree/ignored-triage.js +197 -0
- package/src/lib/worktree/worktree-audit.js +200 -35
- package/src/scripts/setup-infra.js +60 -1
- package/src/utils/agent-id-mapper.js +11 -11
- package/src/utils/claude-settings-manager.js +300 -300
- package/src/utils/file-copier.js +40 -0
- 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
|
+
}
|