@polymorphism-tech/morph-spec 8.37.0 → 8.38.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 (130) hide show
  1. package/CLAUDE.md +19 -6
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +16 -7
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +3 -3
  8. package/framework/MORPH.md +25 -22
  9. package/framework/agents.json +76 -27
  10. package/framework/docs/CLI.md +55 -18
  11. package/framework/evals/rubrics/code-quality.md +5 -0
  12. package/framework/evals/rubrics/condensed.md +6 -2
  13. package/framework/evals/rubrics/contracts.md +20 -0
  14. package/framework/evals/rubrics/test-coverage.md +17 -6
  15. package/framework/hooks/README.md +9 -3
  16. package/framework/hooks/claude-code/core/graph-refresh-kick.js +52 -0
  17. package/framework/hooks/claude-code/core/telemetry-log.js +172 -87
  18. package/framework/hooks/claude-code/core/validate-completion.js +16 -90
  19. package/framework/hooks/claude-code/core/worktree-context.js +10 -0
  20. package/framework/hooks/shared/command-tokens.js +96 -0
  21. package/framework/hooks/shared/git-exclude.js +44 -0
  22. package/framework/hooks/shared/git-paths.js +52 -0
  23. package/framework/hooks/shared/graph-freshness.js +380 -0
  24. package/framework/hooks/shared/graph-refresh.js +416 -0
  25. package/framework/hooks/shared/morph-cli.js +17 -5
  26. package/framework/hooks/shared/redact-secrets.js +55 -0
  27. package/framework/hooks/shared/telemetry-logger.js +25 -2
  28. package/framework/hooks/shared/transcript-usage.js +80 -0
  29. package/framework/rubrics/backend.json +2 -2
  30. package/framework/rubrics/default.json +2 -2
  31. package/framework/rubrics/frontend.json +2 -2
  32. package/framework/rules/csharp-standards.md +1 -0
  33. package/framework/rules/morph-workflow.md +1 -0
  34. package/framework/rules/nextjs-standards.md +1 -0
  35. package/framework/schemas/config.schema.json +41 -1
  36. package/framework/schemas/feature.schema.json +5 -1
  37. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  38. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  39. package/framework/skills/core/morph-apply/SKILL.md +36 -11
  40. package/framework/skills/core/morph-brainstorming/SKILL.md +16 -7
  41. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  42. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  43. package/framework/skills/core/morph-implement/SKILL.md +39 -10
  44. package/framework/skills/core/morph-media/SKILL.md +1 -1
  45. package/framework/skills/core/morph-plan/SKILL.md +72 -15
  46. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  47. package/framework/skills/core/morph-review/SKILL.md +51 -13
  48. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  49. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  50. package/framework/skills/core/morph-uiux/SKILL.md +34 -9
  51. package/framework/standards/STANDARDS.json +187 -12
  52. package/framework/standards/ai-agents/rag-custom-pgvector.md +7 -3
  53. package/framework/standards/ai-agents/testing-ai.md +2 -1
  54. package/framework/standards/architecture/code-comments.md +64 -0
  55. package/framework/standards/backend/dotnet/testing.md +22 -0
  56. package/framework/standards/frontend/admin-app/data-table.md +562 -0
  57. package/framework/standards/frontend/admin-app/feedback.md +236 -0
  58. package/framework/standards/frontend/admin-app/forms.md +232 -0
  59. package/framework/standards/frontend/admin-app/page-structure.md +203 -0
  60. package/framework/standards/frontend/nextjs/components.md +37 -58
  61. package/framework/standards/frontend/nextjs/data-fetching.md +1 -0
  62. package/framework/standards/frontend/nextjs/forms.md +31 -30
  63. package/framework/standards/frontend/nextjs/state-management.md +2 -1
  64. package/framework/standards/frontend/nextjs/testing.md +18 -0
  65. package/framework/templates/context/design-system.md +3 -3
  66. package/framework/templates/docs/proposal.md +11 -0
  67. package/framework/templates/docs/spec.md +15 -0
  68. package/framework/templates/docs/ui-mockups.md +115 -98
  69. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +12 -6
  70. package/framework/templates/feature/decisions.md +124 -122
  71. package/package.json +1 -1
  72. package/src/commands/graph/affected.js +4 -3
  73. package/src/commands/graph/build.js +109 -18
  74. package/src/commands/graph/explain.js +8 -5
  75. package/src/commands/graph/status.js +14 -8
  76. package/src/commands/observability/cost.js +148 -18
  77. package/src/commands/project/doctor.js +82 -10
  78. package/src/commands/project/update.js +25 -0
  79. package/src/commands/project/worktree.js +73 -90
  80. package/src/commands/state/advance.js +18 -0
  81. package/src/commands/state/approve.js +15 -0
  82. package/src/commands/state/archive.js +62 -9
  83. package/src/commands/state/finish.js +62 -14
  84. package/src/commands/state/gate-check.js +8 -1
  85. package/src/commands/state/gate-decision.js +8 -4
  86. package/src/commands/state/report-check.js +90 -9
  87. package/src/commands/state/usecase.js +66 -0
  88. package/src/commands/tasks/dag.js +17 -2
  89. package/src/commands/validation/validate-feature.js +6 -1
  90. package/src/commands/validation/verify.js +20 -1
  91. package/src/dashboard/scanner/agents-scanner.js +5 -1
  92. package/src/dashboard/scanner/hooks-scanner.js +8 -1
  93. package/src/dashboard/scanner/standards-scanner.js +2 -1
  94. package/src/lib/ai/comment-stripper.js +101 -4
  95. package/src/lib/evaluator-report.js +25 -0
  96. package/src/lib/generators/handoff-generator.js +4 -18
  97. package/src/lib/git/default-branch.js +52 -0
  98. package/src/lib/git/tree-fingerprint.js +58 -0
  99. package/src/lib/git/tree-state.js +167 -0
  100. package/src/lib/graph/god-nodes.js +12 -3
  101. package/src/lib/graph/graph-health.js +48 -2
  102. package/src/lib/graph/graph-loader.js +99 -25
  103. package/src/lib/graph/graphifyignore.js +145 -28
  104. package/src/lib/graph/sanitize.js +117 -11
  105. package/src/lib/risk-detector.js +90 -0
  106. package/src/lib/specs/acervo-format.js +70 -0
  107. package/src/lib/specs/acervo-lifecycle.js +264 -0
  108. package/src/lib/specs/adr-index.js +169 -0
  109. package/src/lib/specs/revocations.js +156 -0
  110. package/src/lib/specs/screen-checklist.js +357 -0
  111. package/src/lib/specs/usecase-check.js +20 -16
  112. package/src/lib/specs/usecase-promoter.js +103 -38
  113. package/src/lib/specs/usecase-registry.js +7 -7
  114. package/src/lib/tasks/tasks-json-parser.js +2 -3
  115. package/src/lib/telemetry/metrics.js +386 -0
  116. package/src/lib/telemetry/telemetry-reader.js +161 -51
  117. package/src/lib/telemetry/verify-event.js +79 -0
  118. package/src/lib/validators/code-budget/code-budget-validator.js +172 -0
  119. package/src/lib/validators/code-budget/comment-scanner.js +75 -0
  120. package/src/lib/validators/code-budget/index.js +9 -0
  121. package/src/lib/validators/reuse/reuse-validator.js +4 -2
  122. package/src/lib/validators/reuse/symbol-scanner.js +27 -22
  123. package/src/lib/validators/validation-runner.js +21 -5
  124. package/src/lib/validators/verify-runner.js +11 -0
  125. package/src/lib/worktree/finish-backup.js +739 -0
  126. package/src/lib/worktree/ignored-triage.js +15 -8
  127. package/src/lib/worktree/worktree-audit.js +5 -0
  128. package/src/scripts/setup-infra.js +29 -18
  129. package/src/utils/agents-installer.js +281 -12
  130. package/src/utils/hooks-installer.js +75 -15
@@ -1,122 +1,124 @@
1
- # Architecture Decision Records - {{titleCase FEATURE_NAME}}
2
-
3
- ## ADR-001: {Decision Title}
4
-
5
- **Date:** {{DATE}}
6
- **Status:** Proposed | Accepted | Deprecated | Superseded
7
- **Deciders:** MORPH Agents, Developer
8
-
9
- ### Context
10
-
11
- {Describe the context and problem that led to this decision}
12
-
13
- ### Decision
14
-
15
- {Describe the decision made}
16
-
17
- ### Consequences
18
-
19
- **Pros:**
20
- - {Positive consequence 1}
21
- - {Positive consequence 2}
22
-
23
- **Cons:**
24
- - {Negative consequence 1}
25
-
26
- **Risks:**
27
- - {Identified risk}
28
-
29
- ### Alternatives Considered
30
-
31
- #### Option A: {Name}
32
- - Pros: {advantages}
33
- - Cons: {disadvantages}
34
- - **Rejected because:** {reason}
35
-
36
- #### Option B: {Name}
37
- - Pros: {advantages}
38
- - Cons: {disadvantages}
39
- - **Rejected because:** {reason}
40
-
41
- ---
42
-
43
- {{#if (eq STACK "nextjs-neon")}}
44
- ## ADR-002: Database Access Strategy
45
-
46
- **Date:** {{DATE}}
47
- **Status:** Accepted
48
-
49
- ### Context
50
-
51
- Need to decide how the .NET API accesses Neon PostgreSQL.
52
-
53
- ### Decision
54
-
55
- Use Dapper with Npgsql for direct SQL queries. No EF Core.
56
-
57
- ### Consequences
58
-
59
- **Pros:**
60
- - Full control over SQL queries
61
- - Better performance (no ORM overhead)
62
- - Direct access to PostgreSQL features (jsonb, pgvector, functions)
63
- - RLS policies apply naturally through connection
64
-
65
- **Cons:**
66
- - No automatic migrations (use Neon CLI / dotnet ef instead)
67
- - Manual SQL query writing
68
- - No change tracking
69
-
70
- ---
71
-
72
- ## ADR-003: Auth Strategy
73
-
74
- **Date:** {{DATE}}
75
- **Status:** Accepted
76
-
77
- ### Context
78
-
79
- Need authentication for both Next.js frontend and .NET API.
80
-
81
- ### Decision
82
-
83
- Use Neon Auth (Better Auth) for authentication. Auth data lives in the Neon database. JWT validated by .NET API middleware.
84
-
85
- ### Consequences
86
-
87
- **Pros:**
88
- - Auth data in same database — no external provider, no sync delays
89
- - RLS policies work with Neon Auth JWT via auth.user_id() / pg_session_jwt
90
- - Built-in social login, email/password via @neondatabase/auth SDK
91
- - Branch isolation — each Neon branch gets isolated auth data
92
- - No webhooks needed — query neon_auth schema directly
93
-
94
- **Cons:**
95
- - Neon Auth is currently in Beta
96
- - JWT must be forwarded to .NET API for backend auth
97
- {{/if}}
98
-
99
- ---
100
-
101
- ## Template for New ADRs
102
-
103
- ```markdown
104
- ## ADR-XXX: {Title}
105
-
106
- **Date:**
107
- **Status:** Proposed | Accepted | Deprecated | Superseded
108
-
109
- ### Context
110
- {Why is this decision needed?}
111
-
112
- ### Decision
113
- {What was decided?}
114
-
115
- ### Consequences
116
- **Pros:** {list}
117
- **Cons:** {list}
118
- ```
119
-
120
- ---
121
-
122
- *Generated by MORPH Framework*
1
+ # Architecture Decision Records - {{titleCase FEATURE_NAME}}
2
+
3
+ ## ADR-001: {Decision Title}
4
+
5
+ **Date:** {{DATE}}
6
+ **Status:** Proposed | Accepted | Deprecated | Superseded
7
+ **Deciders:** MORPH Agents, Developer
8
+ **Alcance:** feature
9
+
10
+ ### Context
11
+
12
+ {Describe the context and problem that led to this decision}
13
+
14
+ ### Decision
15
+
16
+ {Describe the decision made}
17
+
18
+ ### Consequences
19
+
20
+ **Pros:**
21
+ - {Positive consequence 1}
22
+ - {Positive consequence 2}
23
+
24
+ **Cons:**
25
+ - {Negative consequence 1}
26
+
27
+ **Risks:**
28
+ - {Identified risk}
29
+
30
+ ### Alternatives Considered
31
+
32
+ #### Option A: {Name}
33
+ - Pros: {advantages}
34
+ - Cons: {disadvantages}
35
+ - **Rejected because:** {reason}
36
+
37
+ #### Option B: {Name}
38
+ - Pros: {advantages}
39
+ - Cons: {disadvantages}
40
+ - **Rejected because:** {reason}
41
+
42
+ ---
43
+
44
+ {{#if (eq STACK "nextjs-neon")}}
45
+ ## ADR-002: Database Access Strategy
46
+
47
+ **Date:** {{DATE}}
48
+ **Status:** Accepted
49
+
50
+ ### Context
51
+
52
+ Need to decide how the .NET API accesses Neon PostgreSQL.
53
+
54
+ ### Decision
55
+
56
+ Use Dapper with Npgsql for direct SQL queries. No EF Core.
57
+
58
+ ### Consequences
59
+
60
+ **Pros:**
61
+ - Full control over SQL queries
62
+ - Better performance (no ORM overhead)
63
+ - Direct access to PostgreSQL features (jsonb, pgvector, functions)
64
+ - RLS policies apply naturally through connection
65
+
66
+ **Cons:**
67
+ - No automatic migrations (use Neon CLI / dotnet ef instead)
68
+ - Manual SQL query writing
69
+ - No change tracking
70
+
71
+ ---
72
+
73
+ ## ADR-003: Auth Strategy
74
+
75
+ **Date:** {{DATE}}
76
+ **Status:** Accepted
77
+
78
+ ### Context
79
+
80
+ Need authentication for both Next.js frontend and .NET API.
81
+
82
+ ### Decision
83
+
84
+ Use Neon Auth (Better Auth) for authentication. Auth data lives in the Neon database. JWT validated by .NET API middleware.
85
+
86
+ ### Consequences
87
+
88
+ **Pros:**
89
+ - Auth data in same database — no external provider, no sync delays
90
+ - RLS policies work with Neon Auth JWT via auth.user_id() / pg_session_jwt
91
+ - Built-in social login, email/password via @neondatabase/auth SDK
92
+ - Branch isolation — each Neon branch gets isolated auth data
93
+ - No webhooks needed — query neon_auth schema directly
94
+
95
+ **Cons:**
96
+ - Neon Auth is currently in Beta
97
+ - JWT must be forwarded to .NET API for backend auth
98
+ {{/if}}
99
+
100
+ ---
101
+
102
+ ## Template for New ADRs
103
+
104
+ ```markdown
105
+ ## ADR-XXX: {Title}
106
+
107
+ **Date:**
108
+ **Status:** Proposed | Accepted | Deprecated | Superseded
109
+ **Alcance:** feature | projeto — `projeto` when the rule holds beyond this feature: `morph-spec finish` copies it to `.morph/specs/adrs/`, the folder brainstorming and plan read instead of archived decisions.md
110
+
111
+ ### Context
112
+ {Why is this decision needed?}
113
+
114
+ ### Decision
115
+ {What was decided?}
116
+
117
+ ### Consequences
118
+ **Pros:** {list}
119
+ **Cons:** {list}
120
+ ```
121
+
122
+ ---
123
+
124
+ *Generated by MORPH Framework*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polymorphism-tech/morph-spec",
3
- "version": "8.37.0",
3
+ "version": "8.38.0",
4
4
  "description": "MORPH-SPEC: NLH (Natural Language Harness) for spec-driven development with Claude Code",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -9,7 +9,7 @@
9
9
  * @module graph-affected
10
10
  */
11
11
  import chalk from 'chalk';
12
- import { loadGraph, getFreshness } from '../../lib/graph/graph-loader.js';
12
+ import { loadGraph, getFreshness, describeStale } from '../../lib/graph/graph-loader.js';
13
13
  import { buildIndex, resolveLabel, affected } from '../../lib/graph/sanitize.js';
14
14
 
15
15
  const FALLBACK_HINT = 'Sem resposta do grafo, caia para grep/Glob — o grafo é um subconjunto estrito do grep, então cair para grep não perde recall.';
@@ -79,11 +79,12 @@ export async function graphAffectedCommand(symbol, options = {}) {
79
79
  console.log(chalk.green(`\n✅ ${symbol} — ${consumers.length} consumidor(es)`));
80
80
  console.log(chalk.gray(` declarado em ${resolved.node.source_file}`));
81
81
  if (freshness.state === 'stale') {
82
- console.log(chalk.yellow(' ⚠ grafo stale (o rebuild do hook é assíncrono) — confirme no código antes de agir'));
82
+ console.log(chalk.yellow(` ⚠ grafo stale, confirme no código antes de agir — ${describeStale(projectPath, freshness)}`));
83
83
  }
84
84
  for (const c of consumers) {
85
85
  const loc = c.source_location ? `:${c.source_location}` : '';
86
- console.log(` ${chalk.cyan(c.relation.padEnd(14))} ${c.source_file}${loc} ${chalk.gray(c.label)}`);
86
+ const test = c.test ? chalk.gray(' [teste]') : '';
87
+ console.log(` ${chalk.cyan(c.relation.padEnd(14))} ${c.source_file}${loc} ${chalk.gray(c.label)}${test}`);
87
88
  }
88
89
  console.log('');
89
90
  }
@@ -7,12 +7,20 @@
7
7
  * @module graph-build
8
8
  */
9
9
  import chalk from 'chalk';
10
- import { spawnSync } from 'node:child_process';
11
- import { readFileSync, existsSync } from 'node:fs';
10
+ import { readFileSync, existsSync, unlinkSync, writeFileSync } from 'node:fs';
12
11
  import { join } from 'node:path';
13
- import { GRAPHIFY_BIN, GRAPHIFY_VERSION, detectGraphifyVersion } from '../../lib/graph/graphify-runtime.js';
12
+ import { GRAPHIFY_PACKAGE, GRAPHIFY_VERSION } from '../../lib/graph/graphify-runtime.js';
14
13
  import { getProjectTags } from '../../lib/stack-filter.js';
15
- import { writeGraphifyignore } from '../../lib/graph/graphifyignore.js';
14
+ import {
15
+ buildGraphifyignore, writeGraphifyignore, describeOutdatedIgnore,
16
+ } from '../../lib/graph/graphifyignore.js';
17
+ import {
18
+ acquireRefreshLock, isPrimaryTree, readRefreshLock, releaseRefreshLock, resolveGraphify, runGraphify,
19
+ } from '../../../framework/hooks/shared/graph-refresh.js';
20
+ import { isSecondaryWorktree } from '../../../framework/hooks/shared/git-paths.js';
21
+ import {
22
+ TEMP_IGNORE_MARKER, ignoreRulesHash, ignoreTextOf, isTemporaryIgnore, readHead, recordRefreshRun,
23
+ } from '../../../framework/hooks/shared/graph-freshness.js';
16
24
  import { resolveGraphPath } from '../../lib/graph/graph-loader.js';
17
25
 
18
26
  function readProjectTags(projectPath) {
@@ -25,6 +33,44 @@ function readProjectTags(projectPath) {
25
33
  }
26
34
  }
27
35
 
36
+ /**
37
+ * O `.graphifyignore` com que o graphify vai rodar.
38
+ *
39
+ * Na raiz: escrito ou migrado (writeGraphifyignore). Numa worktree o arquivo é
40
+ * do checkout e nunca é reescrito — seria uma mudança na branch da feature.
41
+ * Ausente ali (a raiz ainda não o versionou), as regras — as da raiz
42
+ * (`ignoreTextOf`), ou as geradas — entram só durante a rodada e saem depois
43
+ * (`cleanup`): um `?? .graphifyignore` deixado na worktree faz o `finish` e
44
+ * o `worktree remove --if-clean` recusarem com worktree-dirty (medido), e pôr
45
+ * o arquivo no exclude do common dir esconderia também o da raiz. O arquivo leva
46
+ * `TEMP_IGNORE_MARKER` na 1ª linha: se este processo morrer antes do
47
+ * `cleanup`, a sobra é reconhecida pelo conteúdo — a próxima rodada a reusa e o
48
+ * `finish` a apaga (`dropLeftoverIgnore`). O graphify não herda o da raiz: o
49
+ * ignore dele para na raiz de VCS mais próxima, que numa worktree é ela mesma
50
+ * (detect.py `_load_graphifyignore`).
51
+ *
52
+ * @param {string} projectPath
53
+ * @param {string[]} tags
54
+ * @returns {{written: boolean, migrated?: boolean, outdated?: object, temporary?: boolean, path: string, cleanup: () => void}}
55
+ */
56
+ function prepareIgnore(projectPath, tags) {
57
+ const none = () => {};
58
+ if (!isSecondaryWorktree(projectPath)) return { ...writeGraphifyignore(projectPath, tags), cleanup: none };
59
+ const path = join(projectPath, '.graphifyignore');
60
+ if (existsSync(path) && !isTemporaryIgnore(readFileSync(path, 'utf-8'))) {
61
+ const preview = writeGraphifyignore(projectPath, tags, { fx: { writeFile: none } });
62
+ return { written: false, path, ...(preview.outdated ? { outdated: preview.outdated } : {}), cleanup: none };
63
+ }
64
+ const content = ignoreTextOf(projectPath) ?? buildGraphifyignore(tags);
65
+ writeFileSync(path, `${TEMP_IGNORE_MARKER}\n${content.replace(/^/, '')}`, 'utf-8');
66
+ return {
67
+ written: false,
68
+ temporary: true,
69
+ path,
70
+ cleanup: () => { try { unlinkSync(path); } catch { /* já saiu */ } },
71
+ };
72
+ }
73
+
28
74
  /**
29
75
  * @param {{path?: string, json?: boolean, refresh?: boolean}} [options]
30
76
  */
@@ -32,8 +78,10 @@ export async function graphBuildCommand(options = {}) {
32
78
  const projectPath = options.path || process.cwd();
33
79
  const refresh = options.refresh === true;
34
80
 
35
- if (!detectGraphifyVersion()) {
36
- const hint = `graphify não está no PATH. Instale com: uv tool install ${'graphifyy'}==${GRAPHIFY_VERSION}`;
81
+ // O mesmo resolvedor do refresh em background: no win32 um shim `.cmd` só
82
+ // é achado pelo shell, e os dois caminhos precisam ver o mesmo binário.
83
+ if (!resolveGraphify(process.env)) {
84
+ const hint = `graphify não está no PATH. Instale com: uv tool install ${GRAPHIFY_PACKAGE}==${GRAPHIFY_VERSION}`;
37
85
  const payload = { ok: false, reason: 'graphify-unavailable', hint };
38
86
  if (options.json) {
39
87
  console.log(JSON.stringify(payload, null, 2));
@@ -47,7 +95,33 @@ export async function graphBuildCommand(options = {}) {
47
95
 
48
96
  // O ignore precisa existir ANTES da extração, senão o grafo nasce com o
49
97
  // ruído que a ADR-004 existe para tirar.
50
- writeGraphifyignore(projectPath, readProjectTags(projectPath));
98
+ const ignore = prepareIgnore(projectPath, readProjectTags(projectPath));
99
+ if (ignore.outdated && !options.json) {
100
+ console.error(chalk.yellow(`\n⚠ ${describeOutdatedIgnore(ignore.outdated)}`));
101
+ }
102
+
103
+ // Na árvore principal o refresh em background (framework/hooks/shared/
104
+ // graph-refresh.js) escreve o mesmo graph.json: dois graphify ao mesmo tempo
105
+ // disputam o arquivo e todos os núcleos. Numa worktree ninguém mais escreve
106
+ // o graph.json dela — o background só toca a raiz —, e esperar o lock da
107
+ // raiz ali prometeria "o grafo chega ao HEAD" sobre um refresh que nunca a
108
+ // alcança. Sem git não há lock, e o comando segue sozinho.
109
+ const primary = isPrimaryTree(projectPath);
110
+ const lock = primary ? acquireRefreshLock(projectPath) : null;
111
+ const running = primary && !lock ? readRefreshLock(projectPath) : null;
112
+ if (running?.live) {
113
+ ignore.cleanup();
114
+ const since = new Date(running.since).toLocaleTimeString();
115
+ if (options.json) {
116
+ console.log(JSON.stringify({
117
+ ok: true, running: true, since: running.since, pid: running.pid, mode: refresh ? 'refresh' : 'build',
118
+ }, null, 2));
119
+ } else {
120
+ console.log(chalk.yellow(`\n⟳ Refresh em background já em andamento desde ${since} (pid ${running.pid}).`));
121
+ console.log(chalk.gray(' O grafo chega ao HEAD quando ele terminar — confira com: morph-spec graph status\n'));
122
+ }
123
+ return;
124
+ }
51
125
 
52
126
  const args = refresh
53
127
  ? ['update', projectPath]
@@ -56,19 +130,33 @@ export async function graphBuildCommand(options = {}) {
56
130
  // Capturamos o output em vez de herdar o terminal porque precisamos
57
131
  // inspecioná-lo: o graphify sai 1 tanto quando quebra quanto quando não há
58
132
  // código para indexar, e essas duas coisas não podem virar a mesma mensagem.
59
- const run = spawnSync(GRAPHIFY_BIN, args, {
60
- cwd: projectPath,
61
- encoding: 'utf-8',
62
- stdio: ['ignore', 'pipe', 'pipe'],
63
- });
133
+ // HEAD e regras lidos ANTES: são o que o graphify leu.
134
+ const head = readHead(projectPath);
135
+ const ignoreHash = ignoreRulesHash(projectPath);
136
+ let run;
137
+ let empty = false;
138
+ try {
139
+ run = runGraphify(projectPath, args, { timeout: null });
140
+ // Projeto sem código é o estado normal logo depois de um `init`, não uma
141
+ // falha: o graphify diz "found 0 code" / "graph is empty", sai 1 e não
142
+ // escreve graph.json. Tratar isso como erro faria todo init greenfield
143
+ // terminar em vermelho.
144
+ empty = run.status !== 0 && (/found 0 code/i.test(run.output) || /graph is empty/i.test(run.output));
145
+ // O registro é o que torna "untouched" em dia: sem ele, um refresh que não
146
+ // mudou a topologia deixa o carimbo no commit antigo e o grafo parece
147
+ // velho para sempre (framework/hooks/shared/graph-freshness.js).
148
+ if (!empty) {
149
+ recordRefreshRun(projectPath, {
150
+ head, exit: run.status ?? null, ignoreHash, unavailable: Boolean(run.unavailable),
151
+ });
152
+ }
153
+ } finally {
154
+ releaseRefreshLock(lock);
155
+ ignore.cleanup();
156
+ }
64
157
 
65
- const output = `${run.stdout ?? ''}${run.stderr ?? ''}`;
158
+ const { output } = run;
66
159
  const ok = run.status === 0;
67
- // Projeto sem código é o estado normal logo depois de um `init`, não uma
68
- // falha: o graphify diz "found 0 code" / "graph is empty", sai 1 e não
69
- // escreve graph.json. Tratar isso como erro faria todo init greenfield
70
- // terminar em vermelho.
71
- const empty = !ok && (/found 0 code/i.test(output) || /graph is empty/i.test(output));
72
160
 
73
161
  if (options.json) {
74
162
  console.log(JSON.stringify({
@@ -77,6 +165,9 @@ export async function graphBuildCommand(options = {}) {
77
165
  mode: refresh ? 'refresh' : 'build',
78
166
  path: resolveGraphPath(projectPath),
79
167
  exitCode: run.status,
168
+ ...(ignore.migrated ? { ignoreMigrated: true } : {}),
169
+ ...(ignore.outdated ? { ignoreOutdated: ignore.outdated } : {}),
170
+ ...(ignore.temporary ? { ignoreTemporary: true } : {}),
80
171
  }, null, 2));
81
172
  } else {
82
173
  if (!empty && output.trim()) process.stdout.write(`${output.trimEnd()}\n`);
@@ -11,8 +11,8 @@
11
11
  * @module graph-explain
12
12
  */
13
13
  import chalk from 'chalk';
14
- import { loadGraph, getFreshness } from '../../lib/graph/graph-loader.js';
15
- import { buildIndex, resolveLabel, neighborhood } from '../../lib/graph/sanitize.js';
14
+ import { loadGraph, getFreshness, describeStale } from '../../lib/graph/graph-loader.js';
15
+ import { buildIndex, resolveLabel, neighborhood, isTestPath } from '../../lib/graph/sanitize.js';
16
16
 
17
17
  const FALLBACK_HINT = 'Sem resposta do grafo, caia para grep/Glob.';
18
18
 
@@ -62,6 +62,7 @@ export async function graphExplainCommand(symbol, options = {}) {
62
62
  label: node.label,
63
63
  source_file: node.source_file,
64
64
  source_location: node.source_location ?? '',
65
+ test: isTestPath(node.source_file),
65
66
  },
66
67
  incoming,
67
68
  outgoing,
@@ -74,16 +75,18 @@ export async function graphExplainCommand(symbol, options = {}) {
74
75
  }
75
76
 
76
77
  const nodeLoc = node.source_location ? `:${node.source_location}` : '';
77
- console.log(chalk.green(`\n${node.label}`) + chalk.gray(` ${node.source_file}${nodeLoc}`));
78
+ const testTag = payload.node.test ? chalk.gray(' [teste]') : '';
79
+ console.log(chalk.green(`\n${node.label}`) + chalk.gray(` ${node.source_file}${nodeLoc}`) + testTag);
78
80
  if (freshness.state === 'stale') {
79
- console.log(chalk.yellow(' ⚠ grafo stale — o rebuild do hook é assíncrono'));
81
+ console.log(chalk.yellow(` ⚠ grafo stale — ${describeStale(projectPath, freshness)}`));
80
82
  }
81
83
 
82
84
  console.log(chalk.bold('\n quem depende dele:'));
83
85
  for (const e of incoming) {
84
86
  const loc = e.source_location ? `:${e.source_location}` : '';
85
87
  const via = e.via === 'direct' ? '' : chalk.gray(` [${e.via}]`);
86
- console.log(` ${chalk.cyan(e.relation.padEnd(14))} ${e.source_file}${loc}${via}`);
88
+ const test = e.test ? chalk.gray(' [teste]') : '';
89
+ console.log(` ${chalk.cyan(e.relation.padEnd(14))} ${e.source_file}${loc}${via}${test}`);
87
90
  }
88
91
  if (incoming.length === 0) console.log(chalk.gray(' (nenhum)'));
89
92
 
@@ -8,8 +8,10 @@
8
8
  * @module graph-status
9
9
  */
10
10
  import chalk from 'chalk';
11
- import { loadGraph, getFreshness } from '../../lib/graph/graph-loader.js';
12
- import { buildIndex, classifyLabel } from '../../lib/graph/sanitize.js';
11
+ import {
12
+ loadGraph, getFreshness, describeStale, refreshState, staleCause,
13
+ } from '../../lib/graph/graph-loader.js';
14
+ import { buildIndex, classifyLabel, isTestPath } from '../../lib/graph/sanitize.js';
13
15
  import { GRAPHIFY_VERSION, detectGraphifyVersion } from '../../lib/graph/graphify-runtime.js';
14
16
 
15
17
  function summarizeSanitize(index) {
@@ -52,13 +54,16 @@ export async function graphStatusCommand(options = {}) {
52
54
 
53
55
  const { graph } = loaded;
54
56
  const freshness = getFreshness(projectPath, graph);
55
- const sanitize = summarizeSanitize(buildIndex(graph));
57
+ const index = buildIndex(graph);
58
+ const sanitize = { ...summarizeSanitize(index), recoveredDispatch: index.recovered.length };
56
59
  const payload = {
57
60
  available: true,
58
61
  path: loaded.path,
59
62
  nodes: graph.nodes.length,
63
+ testNodes: graph.nodes.filter((n) => isTestPath(n.source_file)).length,
60
64
  links: graph.links.length,
61
65
  freshness,
66
+ refresh: refreshState(projectPath),
62
67
  version,
63
68
  sanitize,
64
69
  };
@@ -72,10 +77,8 @@ export async function graphStatusCommand(options = {}) {
72
77
  console.log(chalk.gray(` ${loaded.path}`));
73
78
 
74
79
  if (freshness.state === 'stale') {
75
- // O hook post-commit rebuilda em BACKGROUND: logo após um commit o grafo
76
- // ainda é o do commit anterior.
77
- console.log(chalk.yellow(` ⚠ stale — construído em ${freshness.builtAtCommit.slice(0, 8)}, HEAD é ${freshness.headCommit.slice(0, 8)}`));
78
- console.log(chalk.gray(' Atualize com: morph-spec graph refresh'));
80
+ console.log(chalk.yellow(` ⚠ stale — ${staleCause(freshness)}`));
81
+ console.log(chalk.gray(` ${describeStale(projectPath, freshness)}`));
79
82
  } else if (freshness.state === 'fresh') {
80
83
  console.log(chalk.gray(` fresco (${freshness.headCommit.slice(0, 8)})`));
81
84
  } else {
@@ -88,6 +91,9 @@ export async function graphStatusCommand(options = {}) {
88
91
  console.log(chalk.yellow(` ⚠ graphify ${installed} instalado; ${GRAPHIFY_VERSION} é a versão pinada e medida`));
89
92
  }
90
93
 
91
- console.log(chalk.gray(` saneamento: ${sanitize.coalescible} coalescível · ${sanitize.external} externo · ${sanitize.ambiguous} ambíguo`));
94
+ console.log(chalk.gray(` saneamento: ${sanitize.coalescible} coalescível · ${sanitize.external} externo · ${sanitize.ambiguous} ambíguo · ${sanitize.recoveredDispatch} dispatch recuperado`));
95
+ if (payload.testNodes) {
96
+ console.log(chalk.gray(` ${payload.testNodes} nós de teste (dublês, fixtures, helpers) — achados pelo explain, fora dos god nodes`));
97
+ }
92
98
  console.log('');
93
99
  }