@polymorphism-tech/morph-spec 8.29.0 → 8.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/CLAUDE.md +11 -1
  2. package/README.md +13 -2
  3. package/bin/morph-spec.js +67 -1
  4. package/bin/validate.js +379 -379
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +1 -1
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +4 -1
  9. package/framework/MORPH.md +4 -3
  10. package/framework/agents.json +6 -6
  11. package/framework/commands/morph-status.md +1 -1
  12. package/framework/docs/CLI.md +68 -45
  13. package/framework/evals/rubrics/code-quality.md +17 -0
  14. package/framework/evals/rubrics/condensed.md +3 -1
  15. package/framework/hooks/claude-code/statusline.sh +11 -11
  16. package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
  17. package/framework/hooks/git/pre-commit/agents.sh +25 -25
  18. package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
  19. package/framework/hooks/git/pre-commit/specs.sh +50 -50
  20. package/framework/hooks/git/pre-push/run-tests.sh +44 -44
  21. package/framework/hooks/shared/phase-utils.js +2 -0
  22. package/framework/hooks/shared/stdin-reader.js +26 -26
  23. package/framework/mcp/registry.json +39 -4
  24. package/framework/rubrics/backend.json +2 -2
  25. package/framework/rubrics/default.json +2 -2
  26. package/framework/rubrics/frontend.json +2 -2
  27. package/framework/schemas/tasks.schema.json +25 -0
  28. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  29. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  30. package/framework/skills/core/morph-apply/SKILL.md +14 -4
  31. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  32. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  33. package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
  34. package/framework/skills/core/morph-implement/SKILL.md +12 -2
  35. package/framework/skills/core/morph-plan/SKILL.md +45 -3
  36. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  37. package/framework/skills/core/morph-review/SKILL.md +12 -4
  38. package/framework/skills/core/morph-stack-scan/SKILL.md +13 -1
  39. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  40. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  41. package/framework/standards/ai-agents/agent-session.md +160 -160
  42. package/framework/standards/ai-agents/batching.md +193 -193
  43. package/framework/standards/ai-agents/context-providers.md +188 -188
  44. package/framework/standards/ai-agents/mcp-server.md +250 -250
  45. package/framework/standards/ai-agents/mcp-tools.md +197 -197
  46. package/framework/standards/ai-agents/middleware-patterns.md +207 -207
  47. package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
  48. package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
  49. package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
  50. package/framework/standards/ai-agents/modalities-vision.md +201 -201
  51. package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
  52. package/framework/standards/ai-agents/production.md +231 -231
  53. package/framework/standards/ai-agents/prompts-in-db.md +214 -214
  54. package/framework/standards/ai-agents/providers/model-registry.md +158 -158
  55. package/framework/standards/ai-agents/service-tier-flex.md +135 -135
  56. package/framework/standards/ai-agents/structured-output.md +249 -249
  57. package/framework/standards/ai-agents/sweet-spot.md +109 -109
  58. package/framework/templates/.idea/morph-templates.xml +92 -0
  59. package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  60. package/framework/templates/IDE-SNIPPETS.md +262 -262
  61. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  62. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
  63. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
  64. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
  65. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
  66. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
  67. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
  68. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
  69. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
  70. package/framework/templates/code/dotnet/database/migration.cs +83 -83
  71. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
  72. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  73. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  74. package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
  75. package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
  76. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  77. package/framework/templates/code/typescript/contracts.ts +167 -167
  78. package/framework/templates/context/ARCHITECTURE.md +20 -0
  79. package/framework/templates/docs/clarifications.md +253 -253
  80. package/framework/templates/docs/schema-analysis.md +119 -119
  81. package/framework/templates/docs/ui-components.md +52 -52
  82. package/framework/templates/docs/ui-design-system.md +76 -76
  83. package/framework/templates/docs/ui-flows.md +167 -167
  84. package/framework/templates/docs/ui-mockups.md +98 -98
  85. package/framework/templates/feature/decisions.md +122 -122
  86. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
  87. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  88. package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
  89. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  90. package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
  91. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  92. package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
  93. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  94. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  95. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  96. package/framework/templates/infrastructure/github/README.md +56 -56
  97. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  98. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  99. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  100. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  101. package/framework/templates/integrations/asaas-client.cs +387 -387
  102. package/framework/templates/integrations/asaas-webhook.cs +351 -351
  103. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  104. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  105. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
  106. package/framework/templates/project/validate.js +120 -120
  107. package/framework/templates/state.template.json +17 -17
  108. package/framework/templates/ui/design-system.css +226 -226
  109. package/package.json +1 -1
  110. package/src/commands/dashboard/dashboard.js +53 -53
  111. package/src/commands/graph/affected.js +89 -0
  112. package/src/commands/graph/build.js +94 -0
  113. package/src/commands/graph/explain.js +96 -0
  114. package/src/commands/graph/status.js +93 -0
  115. package/src/commands/index.js +13 -13
  116. package/src/commands/project/branch.js +170 -0
  117. package/src/commands/project/checkpoint.js +209 -209
  118. package/src/commands/project/doctor.js +148 -9
  119. package/src/commands/project/fleet.js +10 -3
  120. package/src/commands/project/worktree.js +73 -50
  121. package/src/commands/state/finish.js +41 -26
  122. package/src/commands/state/list.js +79 -6
  123. package/src/core/paths/output-schema.js +7 -0
  124. package/src/core/state/lifecycle-audit.js +127 -0
  125. package/src/core/state/phase-state-machine.js +197 -197
  126. package/src/core/templates/template-renderer.js +477 -477
  127. package/src/dashboard/app/components/DetailPanel.jsx +11 -11
  128. package/src/dashboard/app/components/FlowGraph.jsx +17 -17
  129. package/src/dashboard/app/components/SearchFilter.jsx +22 -22
  130. package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
  131. package/src/dashboard/app/main.jsx +13 -13
  132. package/src/dashboard/app/pages/AgentMap.jsx +370 -370
  133. package/src/dashboard/app/pages/HookInspector.jsx +418 -418
  134. package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
  135. package/src/dashboard/app/pages/RulesMap.jsx +273 -273
  136. package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
  137. package/src/dashboard/app/pages/Standards.jsx +332 -332
  138. package/src/dashboard/app/styles/index.css +47 -47
  139. package/src/dashboard/index.html +12 -12
  140. package/src/dashboard/package.json +24 -24
  141. package/src/dashboard/scanner/phases-scanner.js +15 -15
  142. package/src/dashboard/scanner/rubrics-scanner.js +12 -12
  143. package/src/dashboard/scanner/rules-scanner.js +37 -37
  144. package/src/dashboard/scanner/skills-scanner.js +43 -43
  145. package/src/dashboard/scanner/squads-scanner.js +13 -13
  146. package/src/dashboard/vite-plugin-morph-data.js +17 -17
  147. package/src/dashboard/vite.config.js +24 -24
  148. package/src/lib/detectors/design-system-detector.js +188 -188
  149. package/src/lib/detectors/index.js +217 -217
  150. package/src/lib/generators/architecture-scanner.js +45 -4
  151. package/src/lib/generators/design-system-generator.js +508 -508
  152. package/src/lib/generators/settings-generator.js +209 -209
  153. package/src/lib/git/branch-classifier.js +177 -0
  154. package/src/lib/git/gh.js +30 -0
  155. package/src/lib/git/pending-prs.js +89 -0
  156. package/src/lib/git/pr-index.js +70 -0
  157. package/src/lib/graph/god-nodes.js +75 -0
  158. package/src/lib/graph/graph-health.js +61 -0
  159. package/src/lib/graph/graph-loader.js +86 -0
  160. package/src/lib/graph/graphify-runtime.js +100 -0
  161. package/src/lib/graph/graphifyignore.js +86 -0
  162. package/src/lib/graph/sanitize.js +280 -0
  163. package/src/lib/hooks/hook-executor.js +167 -167
  164. package/src/lib/installers/mcp-installer.js +719 -719
  165. package/src/lib/tasks/task-classifier.js +30 -30
  166. package/src/lib/tasks/task-parser.js +209 -209
  167. package/src/lib/tasks/tasks-json-parser.js +26 -0
  168. package/src/lib/validators/checkpoint-persistence.js +39 -39
  169. package/src/lib/validators/css/css-validator.js +340 -340
  170. package/src/lib/validators/reuse/index.js +10 -0
  171. package/src/lib/validators/reuse/reuse-validator.js +324 -0
  172. package/src/lib/validators/reuse/symbol-scanner.js +144 -0
  173. package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
  174. package/src/lib/validators/shared/issue-counter.js +14 -14
  175. package/src/lib/validators/validation-runner.js +40 -1
  176. package/src/lib/worktree/fleet-scanner.js +51 -21
  177. package/src/lib/worktree/ignored-triage.js +197 -0
  178. package/src/lib/worktree/worktree-audit.js +200 -35
  179. package/src/scripts/setup-infra.js +60 -1
  180. package/src/utils/agent-id-mapper.js +11 -11
  181. package/src/utils/claude-settings-manager.js +300 -300
  182. package/src/utils/file-copier.js +40 -0
  183. package/src/utils/skills-installer.js +187 -187
@@ -0,0 +1,89 @@
1
+ /**
2
+ * pending-prs.js — as features que existem SÓ como PR aberto.
3
+ *
4
+ * `finish --pr` deixa o root primário sem vestígio da feature: o
5
+ * `shedRootFeatureScope` apaga `.morph/features/{f}` e o archive foi commitado
6
+ * na branch `morph/{f}`, chegando na default apenas no merge. Entre abrir e
7
+ * mergear, portanto, nenhuma superfície local sabe que aquela branch tem dono —
8
+ * foi assim que uma branch com PR #51 aberto quase foi varrida.
9
+ *
10
+ * Este módulo devolve o dono. Não há estado persistido a consultar, e é de
11
+ * propósito: o PR aberto no GitHub É o registro, a branch é sempre
12
+ * `morph/{feature}`, e o `pr-index.js` já é o leitor único desse fato. Lembrar
13
+ * o número numa segunda cópia só criaria como divergir dele.
14
+ *
15
+ * `null` (do índice ou daqui) significa DESCONHECIDO e jamais "não há
16
+ * pendência" — a mesma regra que torna seguros os caminhos destrutivos que
17
+ * leem o índice.
18
+ */
19
+
20
+ import { existsSync } from 'fs';
21
+ import { join } from 'path';
22
+ import { buildPrIndex } from './pr-index.js';
23
+
24
+ /** As branches que o framework cria por feature. */
25
+ export const MORPH_BRANCH_RE = /^morph\/(.+)$/;
26
+
27
+ const DAY_MS = 86400000;
28
+
29
+ /**
30
+ * O PR da branch canônica de uma feature.
31
+ *
32
+ * @param {Map<string,object>|null} index - de buildPrIndex
33
+ * @param {string} feature
34
+ * @returns {{number:number,state:string,mergedAt:string|null,url:string|null,createdAt:string|null}|null}
35
+ * null = desconhecido OU sem PR; quem precisa distinguir os dois olha o índice.
36
+ */
37
+ export function prForFeature(index, feature) {
38
+ if (!index) return null;
39
+ return index.get(`morph/${feature}`) || null;
40
+ }
41
+
42
+ /** A feature tem pasta (ativa ou arquivada) nesta árvore? */
43
+ function hasLocalFolder(root, feature) {
44
+ return existsSync(join(root, '.morph', 'features', feature))
45
+ || existsSync(join(root, '.morph', 'archive', feature));
46
+ }
47
+
48
+ /**
49
+ * As features aguardando merge: PR OPEN numa branch `morph/*` cujo slug não
50
+ * tem pasta nesta árvore.
51
+ *
52
+ * @param {string} root - a raiz a inspecionar (e de onde o `gh` roda)
53
+ * @param {{ index?: Map<string,object>|null, now?: Date }} [opts]
54
+ * `index` injeta um índice já construído (o `list`/`fleet` constroem um por
55
+ * processo e reaproveitam; os testes passam um sintético). Ausente = constrói.
56
+ * @returns {Array<{feature:string,branch:string,number:number,state:string,url:string|null,createdAt:string|null,ageDays:number|null}>|null}
57
+ * null = DESCONHECIDO. Nunca lista vazia por falta de informação.
58
+ */
59
+ export function pendingPrFeatures(root, opts = {}) {
60
+ const index = opts.index !== undefined ? opts.index : buildPrIndex(root);
61
+ if (!index) return null;
62
+
63
+ const now = opts.now instanceof Date ? opts.now : new Date();
64
+ const out = [];
65
+ for (const [branch, pr] of index) {
66
+ const m = branch.match(MORPH_BRANCH_RE);
67
+ if (!m) continue;
68
+ if (pr.state !== 'OPEN') continue;
69
+ const feature = m[1];
70
+ if (hasLocalFolder(root, feature)) continue;
71
+
72
+ let ageDays = null;
73
+ if (pr.createdAt) {
74
+ const t = Date.parse(pr.createdAt);
75
+ if (!Number.isNaN(t)) ageDays = Math.floor((now.getTime() - t) / DAY_MS);
76
+ }
77
+ out.push({
78
+ feature,
79
+ branch,
80
+ number: pr.number,
81
+ state: pr.state,
82
+ url: pr.url ?? null,
83
+ createdAt: pr.createdAt ?? null,
84
+ ageDays,
85
+ });
86
+ }
87
+ out.sort((a, b) => a.feature.localeCompare(b.feature));
88
+ return out;
89
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * pr-index.js — one `gh pr list` per process, shared by every caller that needs
3
+ * to answer "does a branch have a PR, and in what state".
4
+ *
5
+ * Extracted from `fleet-scanner.js` because three independent leaders now ask
6
+ * the SAME question and must get the SAME answer:
7
+ *
8
+ * • `morph-spec fleet` — the PR column
9
+ * • `morph-spec branch prune` — a branch with an OPEN PR is never deleted,
10
+ * and a MERGED one is deletable even when git
11
+ * cannot see the merge (squash-merge)
12
+ * • `morph-spec doctor --worktrees` — a worktree whose branch carries an open
13
+ * PR must NOT be offered up for removal
14
+ *
15
+ * Fail-open by construction: no `gh`, not authenticated, not GitHub, or a
16
+ * timeout all yield `null`. `null` means "unknown", NEVER "no PR" — every
17
+ * caller must treat the two differently, because the destructive paths
18
+ * (prune, remove) are only safe when the answer is a positive fact.
19
+ */
20
+
21
+ import { execFileSync } from 'child_process';
22
+ import { resolveGh } from './gh.js';
23
+
24
+ /**
25
+ * Build the branch → PR index for a repository.
26
+ *
27
+ * Gated on the origin URL actually pointing at github.com: `gh` against any
28
+ * other host is a guaranteed error, and paying its start-up cost to learn that
29
+ * on every call is pure latency.
30
+ *
31
+ * `--state all` on purpose: a MERGED PR is exactly the evidence a squash-merge
32
+ * erases from git history, and it is the whole reason `branch prune` exists.
33
+ * First wins — `gh` lists newest-first, so an OPEN PR shadows an older closed
34
+ * one on the same branch.
35
+ *
36
+ * @param {string} root - repository root to run `gh` from
37
+ * @param {{ limit?: number, timeout?: number }} [opts]
38
+ * @returns {Map<string,{number:number,state:string,mergedAt:string|null,url:string|null,createdAt:string|null}>|null}
39
+ * null = DESCONHECIDO (sem gh / não é GitHub / falhou), não "sem PRs"
40
+ */
41
+ export function buildPrIndex(root, opts = {}) {
42
+ const limit = String(opts.limit ?? 200);
43
+ const timeout = opts.timeout ?? 8000;
44
+ try {
45
+ const url = execFileSync('git', ['remote', 'get-url', 'origin'], { cwd: root, stdio: 'pipe' }).toString();
46
+ if (!/github\.com/i.test(url)) return null;
47
+ const { bin, prefix } = resolveGh();
48
+ const out = execFileSync(
49
+ bin,
50
+ [...prefix, 'pr', 'list', '--state', 'all', '--limit', limit,
51
+ '--json', 'number,state,headRefName,mergedAt,url,createdAt'],
52
+ { cwd: root, stdio: 'pipe', timeout },
53
+ ).toString();
54
+ const map = new Map();
55
+ for (const pr of JSON.parse(out)) {
56
+ if (pr?.headRefName && !map.has(pr.headRefName)) {
57
+ map.set(pr.headRefName, {
58
+ number: pr.number,
59
+ state: pr.state,
60
+ mergedAt: pr.mergedAt ?? null,
61
+ url: pr.url ?? null,
62
+ createdAt: pr.createdAt ?? null,
63
+ });
64
+ }
65
+ }
66
+ return map;
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * God nodes e comunidades para o contexto de arquitetura.
3
+ *
4
+ * O inventário do architecture-scanner diz o que EXISTE; os god nodes dizem o
5
+ * que é CENTRAL. No ProspectPRO, seis linhas (`Result` 223, `Abstractions` 217,
6
+ * `Repository` 156, `cn()` 141) orientam melhor que 665 de inventário — sem
7
+ * substituí-lo, porque exaustividade é um valor que o grafo não tem.
8
+ *
9
+ * Só entram nós COM source_file: um god node sem arquivo não orienta ninguém.
10
+ *
11
+ * @module god-nodes
12
+ */
13
+ import { loadGraph } from './graph-loader.js';
14
+ import { buildIndex, resolveEdgeTarget } from './sanitize.js';
15
+
16
+ /** Sem `graphify label`, as comunidades se chamam Community_N — ruído puro. */
17
+ const PLACEHOLDER_LABEL = /^Community[_ ]?\d+$/i;
18
+
19
+ /**
20
+ * @param {string} projectPath
21
+ * @param {{top?: number, topCommunities?: number}} [opts]
22
+ * @returns {Promise<{available: boolean, godNodes: Array<{label: string, source_file: string, degree: number}>, communities: Array<{id: number, label: string, size: number, labeled: boolean}>}>}
23
+ */
24
+ export async function scanGraph(projectPath, opts = {}) {
25
+ const top = opts.top ?? 10;
26
+ const topCommunities = opts.topCommunities ?? 10;
27
+
28
+ const loaded = loadGraph(projectPath);
29
+ if (!loaded.ok) return { available: false, godNodes: [], communities: [] };
30
+
31
+ const { graph } = loaded;
32
+ const index = buildIndex(graph);
33
+
34
+ // Grau depois do saneamento: a coalescência muda quem é central, porque as
35
+ // arestas que apontavam para stub passam a contar no nó real.
36
+ const degree = new Map();
37
+ const bump = (id) => {
38
+ if (id) degree.set(id, (degree.get(id) ?? 0) + 1);
39
+ };
40
+ for (const link of graph.links ?? []) {
41
+ bump(link.source);
42
+ bump(resolveEdgeTarget(index, link).id);
43
+ }
44
+
45
+ const godNodes = [...degree.entries()]
46
+ .map(([id, d]) => ({ node: index.byId.get(id), degree: d }))
47
+ .filter(({ node }) => node?.source_file)
48
+ .sort((a, b) => b.degree - a.degree || a.node.label.localeCompare(b.node.label))
49
+ .slice(0, top)
50
+ .map(({ node, degree: d }) => ({
51
+ label: node.label,
52
+ source_file: node.source_file,
53
+ degree: d,
54
+ }));
55
+
56
+ const sizes = new Map();
57
+ const labels = new Map();
58
+ for (const node of graph.nodes ?? []) {
59
+ if (node.community === undefined || node.community === null) continue;
60
+ sizes.set(node.community, (sizes.get(node.community) ?? 0) + 1);
61
+ if (node.community_label && !labels.has(node.community)) {
62
+ labels.set(node.community, node.community_label);
63
+ }
64
+ }
65
+
66
+ const communities = [...sizes.entries()]
67
+ .sort((a, b) => b[1] - a[1] || a[0] - b[0])
68
+ .slice(0, topCommunities)
69
+ .map(([id, size]) => {
70
+ const label = labels.get(id) ?? `Community_${id}`;
71
+ return { id, label, size, labeled: !PLACEHOLDER_LABEL.test(label) };
72
+ });
73
+
74
+ return { available: true, godNodes, communities };
75
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Auditoria do grafo para o `morph-spec doctor`.
3
+ *
4
+ * Nunca devolve estado de erro: o grafo é opcional (ADR-002) e não pode pintar
5
+ * de vermelho o doctor de um projeto que não o usa. O pior veredito é `warn`.
6
+ *
7
+ * @module graph-health
8
+ */
9
+ import { loadGraph, getFreshness } from './graph-loader.js';
10
+ import { GRAPHIFY_VERSION, detectGraphifyVersion } from './graphify-runtime.js';
11
+
12
+ /**
13
+ * @param {string} projectPath
14
+ * @param {{_version?: () => string|null}} [deps]
15
+ * @returns {{status: 'ok'|'warn'|'absent', msg: string, details: string[]}}
16
+ */
17
+ export function auditGraph(projectPath, deps = {}) {
18
+ const installed = (deps._version ?? detectGraphifyVersion)();
19
+ const loaded = loadGraph(projectPath);
20
+
21
+ if (!loaded.ok && loaded.reason === 'missing') {
22
+ return {
23
+ status: 'absent',
24
+ msg: 'opcional, não gerado — morph-spec graph build',
25
+ details: [],
26
+ };
27
+ }
28
+ if (!loaded.ok) {
29
+ return {
30
+ status: 'warn',
31
+ msg: `graph.json ${loaded.reason} — regenere com: morph-spec graph build`,
32
+ details: [loaded.path],
33
+ };
34
+ }
35
+
36
+ const { graph } = loaded;
37
+ const details = [`${graph.nodes.length} nós, ${graph.links.length} arestas`];
38
+ let status = 'ok';
39
+
40
+ const freshness = getFreshness(projectPath, graph);
41
+ if (freshness.state === 'stale') {
42
+ status = 'warn';
43
+ details.push(
44
+ `stale: construído em ${freshness.builtAtCommit.slice(0, 8)}, HEAD é ${freshness.headCommit.slice(0, 8)} — morph-spec graph refresh`,
45
+ );
46
+ }
47
+
48
+ if (!installed) {
49
+ status = 'warn';
50
+ details.push(`graphify não está no PATH (pinado: ${GRAPHIFY_VERSION})`);
51
+ } else if (installed !== GRAPHIFY_VERSION) {
52
+ status = 'warn';
53
+ details.push(`graphify ${installed} instalado; ${GRAPHIFY_VERSION} é a versão pinada e medida`);
54
+ }
55
+
56
+ return {
57
+ status,
58
+ msg: status === 'ok' ? details[0] : details[details.length - 1],
59
+ details,
60
+ };
61
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Leitura fail-open do graph.json produzido pelo graphify.
3
+ *
4
+ * Medido em 2026-08-27 sobre o MORPH_GHLBrain (11,46 MB, 6.829 nós, 15.668
5
+ * arestas): JSON.parse leva 52 ms e 28 MB de heap. Streaming e cache são
6
+ * desnecessários nesse porte — se um dia deixarem de ser, a troca acontece só
7
+ * aqui, porque este módulo é a única porta de leitura do grafo.
8
+ *
9
+ * Nada aqui lança: ausência de grafo é um estado válido do projeto (ADR-002).
10
+ *
11
+ * @module graph-loader
12
+ */
13
+ import { readFileSync, existsSync } from 'node:fs';
14
+ import { join } from 'node:path';
15
+ import { execFileSync } from 'node:child_process';
16
+
17
+ /**
18
+ * @param {string} projectPath
19
+ * @returns {string} caminho canônico do graph.json do projeto
20
+ */
21
+ export function resolveGraphPath(projectPath) {
22
+ return join(projectPath, 'graphify-out', 'graph.json');
23
+ }
24
+
25
+ /**
26
+ * @param {string} projectPath
27
+ * @returns {{ok: boolean, reason?: 'missing'|'unreadable'|'malformed', graph?: object, path: string}}
28
+ */
29
+ export function loadGraph(projectPath) {
30
+ const path = resolveGraphPath(projectPath);
31
+ if (!existsSync(path)) return { ok: false, reason: 'missing', path };
32
+
33
+ let raw;
34
+ try {
35
+ raw = readFileSync(path, 'utf-8');
36
+ } catch {
37
+ return { ok: false, reason: 'unreadable', path };
38
+ }
39
+
40
+ try {
41
+ const graph = JSON.parse(raw.replace(/^/, ''));
42
+ // A chave das arestas é `links`, não `edges`. Um JSON válido sem uma das
43
+ // duas listas não é um grafo utilizável.
44
+ if (!graph || !Array.isArray(graph.nodes) || !Array.isArray(graph.links)) {
45
+ return { ok: false, reason: 'malformed', path };
46
+ }
47
+ return { ok: true, graph, path };
48
+ } catch {
49
+ return { ok: false, reason: 'malformed', path };
50
+ }
51
+ }
52
+
53
+ function readHead(projectPath) {
54
+ try {
55
+ return execFileSync('git', ['rev-parse', 'HEAD'], {
56
+ cwd: projectPath,
57
+ encoding: 'utf-8',
58
+ stdio: ['ignore', 'pipe', 'ignore'],
59
+ }).trim() || null;
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * O hook post-commit do graphify rebuilda em BACKGROUND: logo depois de um
67
+ * commit o grafo ainda é o do commit anterior. Quem consome precisa saber, ou
68
+ * conclui "ninguém usa isso" a partir de um grafo velho.
69
+ *
70
+ * @param {string} projectPath
71
+ * @param {object} graph
72
+ * @param {{_head?: (projectPath: string) => string|null}} [deps]
73
+ * @returns {{builtAtCommit: string|null, headCommit: string|null, state: 'fresh'|'stale'|'unknown'}}
74
+ */
75
+ export function getFreshness(projectPath, graph, deps = {}) {
76
+ const headCommit = (deps._head ?? readHead)(projectPath);
77
+ const builtAtCommit = graph?.built_at_commit ?? null;
78
+ if (!headCommit || !builtAtCommit) {
79
+ return { builtAtCommit, headCommit, state: 'unknown' };
80
+ }
81
+ return {
82
+ builtAtCommit,
83
+ headCommit,
84
+ state: builtAtCommit === headCommit ? 'fresh' : 'stale',
85
+ };
86
+ }
@@ -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
+ }