@polymorphism-tech/morph-spec 8.28.0 → 8.31.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 (185) hide show
  1. package/CLAUDE.md +6 -1
  2. package/README.md +13 -2
  3. package/bin/morph-spec.js +48 -0
  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 +2 -0
  9. package/framework/MORPH.md +2 -1
  10. package/framework/agents.json +20 -9
  11. package/framework/commands/create-umbraco-lp.md +1 -1
  12. package/framework/commands/morph-status.md +1 -1
  13. package/framework/docs/CLI.md +18 -0
  14. package/framework/evals/rubrics/code-quality.md +17 -0
  15. package/framework/evals/rubrics/condensed.md +3 -1
  16. package/framework/hooks/claude-code/statusline.sh +11 -11
  17. package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
  18. package/framework/hooks/git/pre-commit/agents.sh +25 -25
  19. package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
  20. package/framework/hooks/git/pre-commit/specs.sh +50 -50
  21. package/framework/hooks/git/pre-push/run-tests.sh +44 -44
  22. package/framework/hooks/shared/phase-utils.js +2 -0
  23. package/framework/hooks/shared/stdin-reader.js +26 -26
  24. package/framework/mcp/registry.json +97 -11
  25. package/framework/rubrics/backend.json +2 -2
  26. package/framework/rubrics/default.json +2 -2
  27. package/framework/rubrics/frontend.json +2 -2
  28. package/framework/schemas/tasks.schema.json +25 -0
  29. package/framework/skills/core/create-umbraco-lp/SKILL.md +13 -2
  30. package/framework/skills/core/create-umbraco-lp/references/umbraco-lp-patterns.md +598 -0
  31. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +92 -7
  32. package/framework/skills/core/morph-apply/SKILL.md +12 -3
  33. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  34. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  35. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  36. package/framework/skills/core/morph-implement/SKILL.md +12 -2
  37. package/framework/skills/core/morph-plan/SKILL.md +45 -3
  38. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  39. package/framework/skills/core/morph-review/SKILL.md +11 -3
  40. package/framework/skills/core/morph-stack-scan/SKILL.md +13 -1
  41. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  42. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  43. package/framework/standards/PROVENANCE.json +41 -0
  44. package/framework/standards/STANDARDS.json +218 -7
  45. package/framework/standards/ai-agents/agent-session.md +160 -160
  46. package/framework/standards/ai-agents/batching.md +193 -193
  47. package/framework/standards/ai-agents/context-providers.md +188 -188
  48. package/framework/standards/ai-agents/mcp-server.md +250 -250
  49. package/framework/standards/ai-agents/mcp-tools.md +197 -197
  50. package/framework/standards/ai-agents/middleware-patterns.md +207 -207
  51. package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
  52. package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
  53. package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
  54. package/framework/standards/ai-agents/modalities-vision.md +201 -201
  55. package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
  56. package/framework/standards/ai-agents/production.md +231 -231
  57. package/framework/standards/ai-agents/prompts-in-db.md +214 -214
  58. package/framework/standards/ai-agents/providers/model-registry.md +158 -158
  59. package/framework/standards/ai-agents/service-tier-flex.md +135 -135
  60. package/framework/standards/ai-agents/structured-output.md +249 -249
  61. package/framework/standards/ai-agents/sweet-spot.md +109 -109
  62. package/framework/standards/frontend/design-system/ai-image-generation.md +43 -0
  63. package/framework/standards/frontend/design-system/ai-video-generation.md +113 -0
  64. package/framework/standards/frontend/design-system/premium-finish.md +160 -0
  65. package/framework/standards/frontend/gsap/gsap-core.md +1 -1
  66. package/framework/standards/frontend/gsap/gsap-plugins.md +1 -1
  67. package/framework/standards/frontend/gsap/gsap-scrolltrigger.md +3 -1
  68. package/framework/standards/frontend/nextjs/motion-patterns.md +3 -1
  69. package/framework/standards/frontend/scroll-driven/frame-scrub.md +287 -0
  70. package/framework/standards/frontend/scroll-driven/scroll-components.md +256 -0
  71. package/framework/standards/frontend/scroll-driven/smooth-scroll.md +148 -0
  72. package/framework/standards/frontend/seo/meta-schema-audit.md +134 -0
  73. package/framework/templates/.idea/morph-templates.xml +92 -0
  74. package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  75. package/framework/templates/IDE-SNIPPETS.md +262 -262
  76. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  77. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
  78. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
  79. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
  80. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
  81. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
  82. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
  83. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
  84. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
  85. package/framework/templates/code/dotnet/database/migration.cs +83 -83
  86. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
  87. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  88. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  89. package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
  90. package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
  91. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  92. package/framework/templates/code/typescript/contracts.ts +167 -167
  93. package/framework/templates/context/ARCHITECTURE.md +20 -0
  94. package/framework/templates/docs/clarifications.md +253 -253
  95. package/framework/templates/docs/schema-analysis.md +119 -119
  96. package/framework/templates/docs/ui-components.md +52 -52
  97. package/framework/templates/docs/ui-design-system.md +76 -76
  98. package/framework/templates/docs/ui-flows.md +167 -167
  99. package/framework/templates/docs/ui-mockups.md +98 -98
  100. package/framework/templates/feature/decisions.md +122 -122
  101. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
  102. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  103. package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
  104. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  105. package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
  106. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  107. package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
  108. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  109. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  110. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  111. package/framework/templates/infrastructure/github/README.md +56 -56
  112. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  113. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  114. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  115. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  116. package/framework/templates/integrations/asaas-client.cs +387 -387
  117. package/framework/templates/integrations/asaas-webhook.cs +351 -351
  118. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  119. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  120. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
  121. package/framework/templates/project/validate.js +120 -120
  122. package/framework/templates/state.template.json +17 -17
  123. package/framework/templates/ui/design-system.css +226 -226
  124. package/package.json +1 -1
  125. package/src/commands/dashboard/dashboard.js +53 -53
  126. package/src/commands/graph/affected.js +89 -0
  127. package/src/commands/graph/build.js +94 -0
  128. package/src/commands/graph/explain.js +96 -0
  129. package/src/commands/graph/status.js +93 -0
  130. package/src/commands/index.js +13 -13
  131. package/src/commands/project/checkpoint.js +209 -209
  132. package/src/commands/project/doctor.js +11 -0
  133. package/src/core/paths/output-schema.js +7 -0
  134. package/src/core/state/phase-state-machine.js +197 -197
  135. package/src/core/templates/template-renderer.js +477 -477
  136. package/src/dashboard/app/components/DetailPanel.jsx +11 -11
  137. package/src/dashboard/app/components/FlowGraph.jsx +17 -17
  138. package/src/dashboard/app/components/SearchFilter.jsx +22 -22
  139. package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
  140. package/src/dashboard/app/main.jsx +13 -13
  141. package/src/dashboard/app/pages/AgentMap.jsx +370 -370
  142. package/src/dashboard/app/pages/HookInspector.jsx +418 -418
  143. package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
  144. package/src/dashboard/app/pages/RulesMap.jsx +273 -273
  145. package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
  146. package/src/dashboard/app/pages/Standards.jsx +332 -332
  147. package/src/dashboard/app/styles/index.css +47 -47
  148. package/src/dashboard/index.html +12 -12
  149. package/src/dashboard/package.json +24 -24
  150. package/src/dashboard/scanner/phases-scanner.js +15 -15
  151. package/src/dashboard/scanner/rubrics-scanner.js +12 -12
  152. package/src/dashboard/scanner/rules-scanner.js +37 -37
  153. package/src/dashboard/scanner/skills-scanner.js +43 -43
  154. package/src/dashboard/scanner/squads-scanner.js +13 -13
  155. package/src/dashboard/vite-plugin-morph-data.js +17 -17
  156. package/src/dashboard/vite.config.js +24 -24
  157. package/src/lib/detectors/design-system-detector.js +188 -188
  158. package/src/lib/detectors/index.js +217 -217
  159. package/src/lib/generators/architecture-scanner.js +45 -4
  160. package/src/lib/generators/design-system-generator.js +508 -508
  161. package/src/lib/generators/settings-generator.js +209 -209
  162. package/src/lib/graph/god-nodes.js +75 -0
  163. package/src/lib/graph/graph-health.js +61 -0
  164. package/src/lib/graph/graph-loader.js +86 -0
  165. package/src/lib/graph/graphify-runtime.js +100 -0
  166. package/src/lib/graph/graphifyignore.js +86 -0
  167. package/src/lib/graph/sanitize.js +280 -0
  168. package/src/lib/hooks/hook-executor.js +167 -167
  169. package/src/lib/installers/mcp-installer.js +719 -719
  170. package/src/lib/tasks/task-classifier.js +30 -30
  171. package/src/lib/tasks/task-parser.js +209 -209
  172. package/src/lib/tasks/tasks-json-parser.js +26 -0
  173. package/src/lib/validators/checkpoint-persistence.js +39 -39
  174. package/src/lib/validators/css/css-validator.js +340 -340
  175. package/src/lib/validators/reuse/index.js +10 -0
  176. package/src/lib/validators/reuse/reuse-validator.js +324 -0
  177. package/src/lib/validators/reuse/symbol-scanner.js +144 -0
  178. package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
  179. package/src/lib/validators/shared/issue-counter.js +14 -14
  180. package/src/lib/validators/validation-runner.js +40 -1
  181. package/src/scripts/setup-infra.js +60 -1
  182. package/src/utils/agent-id-mapper.js +11 -11
  183. package/src/utils/claude-settings-manager.js +300 -300
  184. package/src/utils/file-copier.js +40 -0
  185. package/src/utils/skills-installer.js +187 -187
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Descoberta do runtime Python e argv de instalação do graphify.
3
+ *
4
+ * O binário instalado chama-se `graphify` (e `graphify-mcp` para o servidor
5
+ * MCP). NUNCA use `python -m graphify.serve`, que é o que o README oficial
6
+ * manda: com `uv tool install` o pacote vive num venv isolado e o Python do
7
+ * sistema não o enxerga (ModuleNotFoundError) — medido no eval, Eixo 2.
8
+ *
9
+ * Toda descoberta é injetável (`_which`, `_run`) para que o teste cubra Windows
10
+ * e POSIX sem depender do PATH da máquina que roda o CI.
11
+ *
12
+ * @module graphify-runtime
13
+ */
14
+ import { execFileSync } from 'node:child_process';
15
+
16
+ /**
17
+ * Pin exato: 0.9.50 é a única versão medida no eval e a base do fixture de
18
+ * regressão. O graphify é pré-1.0 com release quase diária, então semver não
19
+ * garante nada — bump é decisão consciente, com re-run do fixture.
20
+ */
21
+ export const GRAPHIFY_VERSION = '0.9.50';
22
+ export const GRAPHIFY_PACKAGE = 'graphifyy';
23
+ export const GRAPHIFY_BIN = 'graphify';
24
+ export const GRAPHIFY_MCP_BIN = 'graphify-mcp';
25
+
26
+ const PREFERENCE = ['uv', 'pipx', 'python3', 'python'];
27
+
28
+ function whichSync(bin) {
29
+ const cmd = process.platform === 'win32' ? 'where' : 'which';
30
+ try {
31
+ const out = execFileSync(cmd, [bin], {
32
+ encoding: 'utf-8',
33
+ stdio: ['ignore', 'pipe', 'ignore'],
34
+ }).split(/\r?\n/)[0].trim();
35
+ return out || null;
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * uv > pipx > python3 > python. `kind: null` significa "sem Python": o step do
43
+ * setup-infra registra e segue, e o framework continua funcionando por grep
44
+ * (ADR-002).
45
+ *
46
+ * @param {{_which?: (bin: string) => string|null}} [deps]
47
+ * @returns {{kind: 'uv'|'pipx'|'python3'|'python'|null, bin: string|null}}
48
+ */
49
+ export function detectInstaller(deps = {}) {
50
+ const which = deps._which ?? whichSync;
51
+ for (const kind of PREFERENCE) {
52
+ let bin = null;
53
+ try {
54
+ bin = which(kind);
55
+ } catch {
56
+ bin = null;
57
+ }
58
+ if (bin) return { kind, bin };
59
+ }
60
+ return { kind: null, bin: null };
61
+ }
62
+
63
+ /**
64
+ * @param {'uv'|'pipx'|'python3'|'python'} kind
65
+ * @param {{withSql: boolean}} opts sem o extra `[sql]` os arquivos .sql são
66
+ * ignorados em silêncio (92 no ProspectPRO) — só um aviso no meio do output
67
+ * denuncia
68
+ * @returns {{cmd: string, args: string[]}}
69
+ */
70
+ export function installArgs(kind, { withSql }) {
71
+ const spec = `${GRAPHIFY_PACKAGE}${withSql ? '[sql]' : ''}==${GRAPHIFY_VERSION}`;
72
+ switch (kind) {
73
+ case 'uv':
74
+ return { cmd: 'uv', args: ['tool', 'install', spec] };
75
+ case 'pipx':
76
+ return { cmd: 'pipx', args: ['install', spec] };
77
+ case 'python3':
78
+ case 'python':
79
+ return { cmd: kind, args: ['-m', 'pip', 'install', '--user', spec] };
80
+ default:
81
+ throw new Error(`installArgs: gerenciador desconhecido "${kind}"`);
82
+ }
83
+ }
84
+
85
+ /**
86
+ * @param {{_run?: () => string}} [deps]
87
+ * @returns {string|null} versão instalada, ou null se o binário não responde
88
+ */
89
+ export function detectGraphifyVersion(deps = {}) {
90
+ const run = deps._run ?? (() => execFileSync(GRAPHIFY_BIN, ['--version'], {
91
+ encoding: 'utf-8',
92
+ stdio: ['ignore', 'pipe', 'ignore'],
93
+ }));
94
+ try {
95
+ const m = /(\d+\.\d+\.\d+)/.exec(run());
96
+ return m ? m[1] : null;
97
+ } catch {
98
+ return null;
99
+ }
100
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Geração stack-aware do .graphifyignore (ADR-004).
3
+ *
4
+ * Filtrar teste não é cosmético: sem isso o god node #1 do MORPH_GHLBrain é
5
+ * `FakeGhlToolbelt`, um dublê. Pior, os dublês criam um segundo implementador e
6
+ * quebram o guard de "implementador único" do `dispatches_to` — a aresta que
7
+ * atravessa a injeção de dependência cai de 109 para 1.
8
+ *
9
+ * E o padrão genérico `tests/` NÃO serve em .NET: no ProspectPRO os testes
10
+ * vivem em `src/backend/ProspectPRO.Tests/`. Com `**\/*.Tests/` o grafo caiu de
11
+ * 10.450 para 7.384 nós — 30% era ruído de teste.
12
+ *
13
+ * Em Next.js o problema é outro: metade dos nós vinha de JSON de configuração,
14
+ * e os god nodes viravam `dependencies` e `compilerOptions`.
15
+ *
16
+ * @module graphifyignore
17
+ */
18
+ import { writeFileSync, existsSync } from 'node:fs';
19
+ import { join } from 'node:path';
20
+
21
+ export const ALWAYS_IGNORED = [
22
+ '.git/', '.morph/', 'artifacts/', 'worktrees/', '.worktrees/',
23
+ 'coverage/', 'graphify-out/', 'dist/', 'out/',
24
+ ];
25
+
26
+ const DOTNET_IGNORED = [
27
+ '**/*.Tests/', '**/*Tests/', '**/*.Test/', 'bin/', 'obj/', 'TestResults/',
28
+ ];
29
+
30
+ const WEB_IGNORED = [
31
+ 'node_modules/', '.next/', '.turbo/', 'build/',
32
+ // Só os JSON da RAIZ, para não esconder JSON que seja dado do projeto.
33
+ '/package.json', '/package-lock.json', '/pnpm-lock.yaml', '/tsconfig.json',
34
+ '/next.config.js', '/next.config.mjs', '/tailwind.config.js', '/tailwind.config.ts',
35
+ ];
36
+
37
+ const DOTNET_TAGS = new Set(['dotnet', 'aspnet']);
38
+ const WEB_TAGS = new Set([
39
+ 'nextjs', 'next', 'next.js', 'react', 'vue', 'nodejs', 'nodejs-cli', 'node-cli',
40
+ ]);
41
+
42
+ /**
43
+ * @param {string[]} [projectTags] saída de `getProjectTags(config)` — stack
44
+ * splitado por `-`/`+` mais as integrations
45
+ * @returns {string} conteúdo completo do .graphifyignore
46
+ */
47
+ export function buildGraphifyignore(projectTags = []) {
48
+ const tags = new Set(projectTags);
49
+ const hasDotnet = [...tags].some((t) => DOTNET_TAGS.has(t));
50
+ const hasWeb = [...tags].some((t) => WEB_TAGS.has(t));
51
+
52
+ const sections = [
53
+ ['# Gerado por morph-spec (ADR-004). Editável: o CLI não sobrescreve.', ''],
54
+ ['# Sempre', ...ALWAYS_IGNORED],
55
+ ];
56
+ if (hasDotnet) {
57
+ sections.push(['', '# .NET — testes em qualquer nível (tests/ genérico não serve)', ...DOTNET_IGNORED]);
58
+ }
59
+ if (hasWeb) {
60
+ sections.push(['', '# Web — dependências e JSON de configuração de raiz', ...WEB_IGNORED]);
61
+ }
62
+
63
+ const seen = new Set();
64
+ const lines = [];
65
+ for (const line of sections.flat()) {
66
+ const isPattern = line && !line.startsWith('#');
67
+ if (isPattern && seen.has(line)) continue;
68
+ if (isPattern) seen.add(line);
69
+ lines.push(line);
70
+ }
71
+ return `${lines.join('\n')}\n`;
72
+ }
73
+
74
+ /**
75
+ * Escreve o .graphifyignore. Arquivo já existente é do usuário: não é tocado.
76
+ *
77
+ * @param {string} projectPath
78
+ * @param {string[]} [projectTags]
79
+ * @returns {{written: boolean, path: string}}
80
+ */
81
+ export function writeGraphifyignore(projectPath, projectTags = []) {
82
+ const path = join(projectPath, '.graphifyignore');
83
+ if (existsSync(path)) return { written: false, path };
84
+ writeFileSync(path, buildGraphifyignore(projectTags), 'utf-8');
85
+ return { written: true, path };
86
+ }
@@ -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
+ }