@polymorphism-tech/morph-spec 8.37.1 → 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.
- package/CLAUDE.md +19 -6
- package/README.md +2 -2
- package/bin/morph-spec.js +16 -7
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +3 -3
- package/framework/MORPH.md +25 -22
- package/framework/agents.json +76 -27
- package/framework/docs/CLI.md +54 -17
- package/framework/evals/rubrics/code-quality.md +5 -0
- package/framework/evals/rubrics/condensed.md +6 -2
- package/framework/evals/rubrics/contracts.md +20 -0
- package/framework/evals/rubrics/test-coverage.md +17 -6
- package/framework/hooks/README.md +9 -3
- package/framework/hooks/claude-code/core/graph-refresh-kick.js +52 -0
- package/framework/hooks/claude-code/core/telemetry-log.js +172 -87
- package/framework/hooks/claude-code/core/validate-completion.js +16 -90
- package/framework/hooks/claude-code/core/worktree-context.js +10 -0
- package/framework/hooks/shared/command-tokens.js +96 -0
- package/framework/hooks/shared/git-exclude.js +44 -0
- package/framework/hooks/shared/git-paths.js +52 -0
- package/framework/hooks/shared/graph-freshness.js +380 -0
- package/framework/hooks/shared/graph-refresh.js +416 -0
- package/framework/hooks/shared/morph-cli.js +17 -5
- package/framework/hooks/shared/redact-secrets.js +55 -0
- package/framework/hooks/shared/telemetry-logger.js +25 -2
- package/framework/hooks/shared/transcript-usage.js +80 -0
- package/framework/rubrics/backend.json +2 -2
- package/framework/rubrics/default.json +2 -2
- package/framework/rubrics/frontend.json +2 -2
- package/framework/rules/csharp-standards.md +1 -0
- package/framework/rules/morph-workflow.md +1 -0
- package/framework/rules/nextjs-standards.md +1 -0
- package/framework/schemas/config.schema.json +41 -1
- package/framework/schemas/feature.schema.json +5 -1
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +36 -11
- package/framework/skills/core/morph-brainstorming/SKILL.md +16 -7
- 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 +39 -10
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +72 -15
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +51 -13
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +34 -9
- package/framework/standards/STANDARDS.json +187 -12
- package/framework/standards/ai-agents/testing-ai.md +2 -1
- package/framework/standards/architecture/code-comments.md +64 -0
- package/framework/standards/backend/dotnet/testing.md +22 -0
- package/framework/standards/frontend/admin-app/data-table.md +562 -0
- package/framework/standards/frontend/admin-app/feedback.md +236 -0
- package/framework/standards/frontend/admin-app/forms.md +232 -0
- package/framework/standards/frontend/admin-app/page-structure.md +203 -0
- package/framework/standards/frontend/nextjs/components.md +37 -58
- package/framework/standards/frontend/nextjs/data-fetching.md +1 -0
- package/framework/standards/frontend/nextjs/forms.md +31 -30
- package/framework/standards/frontend/nextjs/state-management.md +2 -1
- package/framework/standards/frontend/nextjs/testing.md +18 -0
- package/framework/templates/context/design-system.md +3 -3
- package/framework/templates/docs/proposal.md +11 -0
- package/framework/templates/docs/spec.md +15 -0
- package/framework/templates/docs/ui-mockups.md +115 -98
- package/framework/templates/feature/decisions.md +124 -122
- package/package.json +1 -1
- package/src/commands/graph/affected.js +4 -3
- package/src/commands/graph/build.js +109 -18
- package/src/commands/graph/explain.js +8 -5
- package/src/commands/graph/status.js +14 -8
- package/src/commands/observability/cost.js +148 -18
- package/src/commands/project/doctor.js +82 -10
- package/src/commands/project/update.js +25 -0
- package/src/commands/project/worktree.js +73 -90
- package/src/commands/state/advance.js +18 -0
- package/src/commands/state/approve.js +15 -0
- package/src/commands/state/archive.js +62 -9
- package/src/commands/state/finish.js +62 -14
- package/src/commands/state/gate-check.js +8 -1
- package/src/commands/state/gate-decision.js +8 -4
- package/src/commands/state/report-check.js +90 -9
- package/src/commands/state/usecase.js +66 -0
- package/src/commands/tasks/dag.js +17 -2
- package/src/commands/validation/validate-feature.js +6 -1
- package/src/commands/validation/verify.js +20 -1
- package/src/dashboard/scanner/agents-scanner.js +5 -1
- package/src/dashboard/scanner/hooks-scanner.js +8 -1
- package/src/dashboard/scanner/standards-scanner.js +2 -1
- package/src/lib/ai/comment-stripper.js +101 -4
- package/src/lib/evaluator-report.js +25 -0
- package/src/lib/generators/handoff-generator.js +4 -18
- package/src/lib/git/default-branch.js +52 -0
- package/src/lib/git/tree-fingerprint.js +58 -0
- package/src/lib/git/tree-state.js +167 -0
- package/src/lib/graph/god-nodes.js +12 -3
- package/src/lib/graph/graph-health.js +48 -2
- package/src/lib/graph/graph-loader.js +99 -25
- package/src/lib/graph/graphifyignore.js +145 -28
- package/src/lib/graph/sanitize.js +117 -11
- package/src/lib/risk-detector.js +90 -0
- package/src/lib/specs/acervo-format.js +70 -0
- package/src/lib/specs/acervo-lifecycle.js +264 -0
- package/src/lib/specs/adr-index.js +169 -0
- package/src/lib/specs/revocations.js +156 -0
- package/src/lib/specs/screen-checklist.js +357 -0
- package/src/lib/specs/usecase-check.js +20 -16
- package/src/lib/specs/usecase-promoter.js +103 -38
- package/src/lib/specs/usecase-registry.js +7 -7
- package/src/lib/tasks/tasks-json-parser.js +2 -3
- package/src/lib/telemetry/metrics.js +386 -0
- package/src/lib/telemetry/telemetry-reader.js +161 -51
- package/src/lib/telemetry/verify-event.js +79 -0
- package/src/lib/validators/code-budget/code-budget-validator.js +172 -0
- package/src/lib/validators/code-budget/comment-scanner.js +75 -0
- package/src/lib/validators/code-budget/index.js +9 -0
- package/src/lib/validators/reuse/reuse-validator.js +4 -2
- package/src/lib/validators/reuse/symbol-scanner.js +27 -22
- package/src/lib/validators/validation-runner.js +21 -5
- package/src/lib/validators/verify-runner.js +11 -0
- package/src/lib/worktree/finish-backup.js +739 -0
- package/src/lib/worktree/ignored-triage.js +15 -8
- package/src/lib/worktree/worktree-audit.js +5 -0
- package/src/scripts/setup-infra.js +29 -18
- package/src/utils/agents-installer.js +281 -12
- 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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
- {Positive consequence
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
**
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
-
|
|
62
|
-
-
|
|
63
|
-
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
-
|
|
68
|
-
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
**
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
-
|
|
92
|
-
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
**
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
@@ -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(
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
12
|
+
import { GRAPHIFY_PACKAGE, GRAPHIFY_VERSION } from '../../lib/graph/graphify-runtime.js';
|
|
14
13
|
import { getProjectTags } from '../../lib/stack-filter.js';
|
|
15
|
-
import {
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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 {
|
|
12
|
-
|
|
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
|
|
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
|
-
|
|
76
|
-
|
|
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
|
}
|