@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.
- package/CLAUDE.md +6 -1
- package/README.md +13 -2
- package/bin/morph-spec.js +48 -0
- 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 +2 -0
- package/framework/MORPH.md +2 -1
- package/framework/agents.json +20 -9
- package/framework/commands/create-umbraco-lp.md +1 -1
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +18 -0
- 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 +97 -11
- 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 +13 -2
- package/framework/skills/core/create-umbraco-lp/references/umbraco-lp-patterns.md +598 -0
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +92 -7
- package/framework/skills/core/morph-apply/SKILL.md +12 -3
- 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 +1 -1
- 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 +11 -3
- 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/PROVENANCE.json +41 -0
- package/framework/standards/STANDARDS.json +218 -7
- 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/standards/frontend/design-system/ai-image-generation.md +43 -0
- package/framework/standards/frontend/design-system/ai-video-generation.md +113 -0
- package/framework/standards/frontend/design-system/premium-finish.md +160 -0
- package/framework/standards/frontend/gsap/gsap-core.md +1 -1
- package/framework/standards/frontend/gsap/gsap-plugins.md +1 -1
- package/framework/standards/frontend/gsap/gsap-scrolltrigger.md +3 -1
- package/framework/standards/frontend/nextjs/motion-patterns.md +3 -1
- package/framework/standards/frontend/scroll-driven/frame-scrub.md +287 -0
- package/framework/standards/frontend/scroll-driven/scroll-components.md +256 -0
- package/framework/standards/frontend/scroll-driven/smooth-scroll.md +148 -0
- package/framework/standards/frontend/seo/meta-schema-audit.md +134 -0
- 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/checkpoint.js +209 -209
- package/src/commands/project/doctor.js +11 -0
- package/src/core/paths/output-schema.js +7 -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/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/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,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
|
+
}
|