@polymorphism-tech/morph-spec 9.7.0 → 10.0.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 (211) hide show
  1. package/CLAUDE.md +11 -11
  2. package/README.md +4 -4
  3. package/bin/morph-spec.js +41 -44
  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 +24 -30
  8. package/framework/MORPH.md +47 -123
  9. package/framework/agents.json +6 -374
  10. package/framework/commands/morph-apply.md +1 -1
  11. package/framework/commands/morph-archive.md +4 -3
  12. package/framework/commands/morph-hotfix.md +8 -30
  13. package/framework/commands/morph-infra.md +2 -2
  14. package/framework/commands/morph-media.md +10 -9
  15. package/framework/commands/morph-review.md +15 -0
  16. package/framework/commands/morph-status.md +1 -1
  17. package/framework/docs/CLI.md +100 -102
  18. package/framework/evals/review-checklist.md +46 -54
  19. package/framework/hooks/README.md +4 -4
  20. package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
  21. package/framework/hooks/claude-code/core/README.md +1 -1
  22. package/framework/hooks/claude-code/core/denylist.json +4 -4
  23. package/framework/hooks/claude-code/core/gate-guard.js +9 -0
  24. package/framework/hooks/claude-code/core/masked-exit.js +49 -0
  25. package/framework/hooks/claude-code/core/state-sync.js +4 -1
  26. package/framework/hooks/claude-code/core/telemetry-log.js +1 -2
  27. package/framework/hooks/claude-code/core/test-budget.js +10 -70
  28. package/framework/hooks/claude-code/core/validate-completion.js +11 -24
  29. package/framework/hooks/claude-code/core/worktree-context.js +5 -22
  30. package/framework/hooks/claude-code/core/worktree-create.js +6 -45
  31. package/framework/hooks/claude-code/core/worktree-env-inject.js +2 -10
  32. package/framework/hooks/shared/e2e-check.js +0 -0
  33. package/framework/hooks/shared/gate-event.js +2 -2
  34. package/framework/hooks/shared/graph-refresh.js +20 -3
  35. package/framework/hooks/shared/heavy-command.js +32 -1
  36. package/framework/hooks/shared/heavy-config.js +1 -1
  37. package/framework/hooks/shared/heavy-slots.js +41 -4
  38. package/framework/hooks/shared/next-step.js +63 -24
  39. package/framework/hooks/shared/phase-utils.js +6 -9
  40. package/framework/hooks/shared/report-progress.js +86 -0
  41. package/framework/hooks/shared/state-reader.js +3 -2
  42. package/framework/hooks/statusline.js +332 -0
  43. package/framework/phases.json +32 -10
  44. package/framework/rules/ai-agents-standards.md +9 -1
  45. package/framework/rules/csharp-standards.md +4 -0
  46. package/framework/rules/ef-standards.md +20 -0
  47. package/framework/rules/frontend-standards.md +1 -1
  48. package/framework/rules/implement-report.md +25 -0
  49. package/framework/rules/infrastructure-standards.md +2 -0
  50. package/framework/rules/nextjs-standards.md +7 -0
  51. package/framework/rules/plan-grammar.md +53 -0
  52. package/framework/rules/ui-design.md +27 -0
  53. package/framework/schemas/config.schema.json +12 -19
  54. package/framework/schemas/feature.schema.json +7 -1
  55. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  56. package/framework/skills/core/create-umbraco-lp/references/umbraco-lp-patterns.md +1 -1
  57. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +2 -2
  58. package/framework/skills/core/morph-apply/SKILL.md +38 -166
  59. package/framework/skills/core/morph-apply/references/debugging.md +8 -0
  60. package/framework/skills/core/morph-apply/references/hotfix.md +42 -0
  61. package/framework/skills/core/morph-apply/references/machine.md +29 -0
  62. package/framework/skills/core/{morph-media/SKILL.md → morph-apply/references/media.md} +11 -74
  63. package/framework/skills/core/morph-apply/references/review.md +65 -0
  64. package/framework/skills/core/morph-plan/SKILL.md +42 -108
  65. package/framework/skills/core/morph-plan/references/critique.md +40 -0
  66. package/framework/skills/core/morph-plan/references/interview.md +25 -0
  67. package/framework/skills/core/morph-uiux/SKILL.md +47 -64
  68. package/framework/skills/core/morph-uiux/surfaces.json +32 -0
  69. package/framework/standards/STANDARDS.json +32 -5
  70. package/framework/standards/STANDARDS.meta.json +5 -1
  71. package/framework/standards/ai-agents/agent-session.md +2 -3
  72. package/framework/standards/ai-agents/context-providers.md +2 -2
  73. package/framework/standards/ai-agents/durable-workflows-hitl.md +2 -2
  74. package/framework/standards/ai-agents/providers/model-registry.md +2 -2
  75. package/framework/standards/ai-agents/rag-custom-pgvector.md +6 -6
  76. package/framework/standards/ai-agents/setup.md +1 -1
  77. package/framework/standards/ai-agents/tool-adapters.md +2 -2
  78. package/framework/standards/architecture/stack-baseline.md +47 -0
  79. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -4
  80. package/framework/standards/frontend/design-system/ai-image-generation.md +51 -12
  81. package/framework/standards/frontend/design-system/ai-video-generation.md +50 -4
  82. package/framework/standards/frontend/design-system/art-direction.md +2 -2
  83. package/framework/standards/frontend/scroll-driven/frame-scrub.md +12 -2
  84. package/framework/standards/frontend/scroll-driven/scroll-components.md +76 -4
  85. package/framework/templates/.idea/morph-templates.xml +92 -0
  86. package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  87. package/framework/templates/IDE-SNIPPETS.md +262 -262
  88. package/framework/templates/REGISTRY.json +1 -2
  89. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  90. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  91. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  92. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  93. package/framework/templates/context/ARCHITECTURE.md +1 -1
  94. package/framework/templates/docs/plan.md +12 -12
  95. package/framework/templates/docs/ui-mockups.md +17 -105
  96. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  97. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  98. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  99. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  100. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  101. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  102. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  103. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  104. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  105. package/framework/templates/state.template.json +17 -17
  106. package/framework/templates/ui/design-system.css +226 -226
  107. package/package.json +1 -1
  108. package/src/commands/e2e/e2e.js +88 -23
  109. package/src/commands/graph/diff.js +74 -0
  110. package/src/commands/graph/status.js +1 -1
  111. package/src/commands/observability/cost.js +2 -4
  112. package/src/commands/observability/telemetry.js +1 -1
  113. package/src/commands/project/doctor.js +23 -39
  114. package/src/commands/project/fleet.js +5 -6
  115. package/src/commands/project/init.js +63 -36
  116. package/src/commands/project/stack-scan.js +3 -42
  117. package/src/commands/project/stack-set.js +26 -0
  118. package/src/commands/project/status.js +12 -13
  119. package/src/commands/project/update.js +39 -3
  120. package/src/commands/project/worktree.js +27 -16
  121. package/src/commands/state/advance.js +17 -17
  122. package/src/commands/state/approve.js +0 -6
  123. package/src/commands/state/archive.js +27 -1
  124. package/src/commands/state/auto-approval.js +1 -1
  125. package/src/commands/state/create.js +0 -5
  126. package/src/commands/state/finish.js +43 -1
  127. package/src/commands/state/gate-check.js +37 -30
  128. package/src/commands/state/gate-decision.js +3 -2
  129. package/src/commands/state/next.js +1 -0
  130. package/src/commands/state/recap.js +67 -0
  131. package/src/commands/state/report-check.js +39 -146
  132. package/src/commands/validation/plan-check.js +16 -11
  133. package/src/commands/validation/validate-feature.js +3 -3
  134. package/src/commands/validation/verify.js +6 -6
  135. package/src/core/paths/output-schema.js +3 -3
  136. package/src/core/state/state-manager.js +2 -4
  137. package/src/dashboard/app/components/CommandPalette.jsx +2 -5
  138. package/src/dashboard/app/components/GatePauses.jsx +116 -0
  139. package/src/dashboard/app/components/SessionMap.jsx +77 -0
  140. package/src/dashboard/app/main.jsx +13 -13
  141. package/src/dashboard/app/pages/Activity.jsx +4 -0
  142. package/src/dashboard/app/pages/AgentMap.jsx +1 -2
  143. package/src/dashboard/app/pages/FeatureDetail.jsx +129 -186
  144. package/src/dashboard/app/pages/Features.jsx +7 -7
  145. package/src/dashboard/app/pages/PhasePipeline.jsx +1 -1
  146. package/src/dashboard/app/pages/Portfolio.jsx +40 -5
  147. package/src/dashboard/scanner/activity-scanner.js +29 -0
  148. package/src/dashboard/scanner/agents-scanner.js +11 -14
  149. package/src/dashboard/scanner/features-scanner.js +45 -24
  150. package/src/dashboard/scanner/project-api.js +11 -2
  151. package/src/dashboard/scanner/skills-scanner.js +11 -1
  152. package/src/dashboard/vite.config.js +24 -24
  153. package/src/lib/e2e/applicability.js +3 -1
  154. package/src/lib/git/tree-state.js +1 -16
  155. package/src/lib/graph/diff-tree.js +127 -0
  156. package/src/lib/graph/graphify-runtime.js +12 -5
  157. package/src/lib/heavy/gate.js +22 -1
  158. package/src/lib/installers/memory-installer.js +2 -2
  159. package/src/lib/installers/rules-installer.js +57 -0
  160. package/src/lib/installers/stack-assets.js +17 -0
  161. package/src/lib/installers/stack-sync.js +219 -0
  162. package/src/lib/plan/checks.js +55 -110
  163. package/src/lib/plan/critique.js +28 -0
  164. package/src/lib/plan/media.js +42 -0
  165. package/src/lib/plan/plan-html.js +125 -0
  166. package/src/lib/plan/plan-parser.js +34 -45
  167. package/src/lib/recap/piper.js +36 -0
  168. package/src/lib/recap/recap.js +232 -0
  169. package/src/lib/review-reports.js +33 -7
  170. package/src/lib/risk-detector.js +90 -11
  171. package/src/lib/specs/screen-checklist.js +33 -48
  172. package/src/lib/stack/dotnet-build-ignores.js +8 -9
  173. package/src/lib/tasks/test-nodes.js +24 -6
  174. package/src/lib/tasks/test-runner.js +40 -1
  175. package/src/lib/telemetry/metrics.js +27 -29
  176. package/src/lib/validators/reuse/reuse-validator.js +1 -9
  177. package/src/lib/validators/verify-runner.js +50 -19
  178. package/src/lib/worktree/fleet-scanner.js +7 -20
  179. package/src/lib/worktree/worktree-audit.js +0 -37
  180. package/src/scripts/setup-infra.js +15 -50
  181. package/src/utils/agents-installer.js +27 -201
  182. package/src/utils/hooks-installer.js +11 -95
  183. package/framework/hooks/claude-code/core/warn-bash-source-write.js +0 -23
  184. package/framework/hooks/claude-code/statusline.py +0 -699
  185. package/framework/hooks/claude-code/statusline.sh +0 -11
  186. package/framework/hooks/shared/agent-reap.js +0 -74
  187. package/framework/hooks/shared/slice-progress.js +0 -110
  188. package/framework/skills/core/morph-hotfix/SKILL.md +0 -133
  189. package/framework/skills/core/morph-machine/SKILL.md +0 -46
  190. package/framework/skills/core/morph-review/SKILL.md +0 -129
  191. package/framework/skills/core/morph-standards/SKILL.md +0 -52
  192. package/framework/standards/PROVENANCE.json +0 -119
  193. package/src/commands/project/worktree-reap.js +0 -34
  194. package/src/commands/state/handoff.js +0 -65
  195. package/src/commands/validation/mutate.js +0 -263
  196. package/src/commands/validation/scout-check.js +0 -28
  197. package/src/lib/generators/handoff-generator.js +0 -172
  198. package/src/lib/mutation/adapters.js +0 -79
  199. package/src/lib/mutation/commands.js +0 -61
  200. package/src/lib/mutation/dotnet-introspection.js +0 -281
  201. package/src/lib/mutation/manifest.js +0 -144
  202. package/src/lib/mutation/plan.js +0 -44
  203. package/src/lib/mutation/report.js +0 -68
  204. package/src/lib/mutation/runner.js +0 -301
  205. package/src/lib/mutation/schemata.js +0 -60
  206. package/src/lib/mutation/snapshot.js +0 -65
  207. package/src/lib/mutation/subset.js +0 -109
  208. package/src/lib/mutation/trx.js +0 -50
  209. package/src/lib/scout/scout-check.js +0 -293
  210. package/src/lib/validators/traceability-check.js +0 -103
  211. package/src/lib/worktree/agent-reaper.js +0 -247
@@ -1,172 +0,0 @@
1
- // Derived only from committed sources, so it is regenerated, never hand-edited or merged.
2
- // Every section is fail-open.
3
-
4
- import { existsSync, readFileSync, writeFileSync } from 'fs';
5
- import { join } from 'path';
6
- import { ensureCommonExclude } from '../../../framework/hooks/shared/git-exclude.js';
7
- import { derivePhaseForFeature, getPendingGates } from '../../../framework/hooks/shared/state-reader.js';
8
- import { sliceProgress, readySlices } from '../../../framework/hooks/shared/slice-progress.js';
9
- import { formatNext } from '../../../framework/hooks/shared/next-step.js';
10
- import { nextStepFor } from '../next-step.js';
11
-
12
- const POINTER_CANDIDATES = [
13
- ['2-plan/plan.md', 'plano: problema, cenários com prova, decisões e fatias'],
14
- ['2-plan/checks.json', 'cenários e fatias extraídos pelo plan-check'],
15
- ['1-design/design-system.md', 'design system da feature'],
16
- ['3-implement/reports', 'relatórios das fatias (**Arquivos:**/**Standards:**): o registro da implementação'],
17
- ['4-review/verifier.md', 'verificador do Gate 3: cada cenário provado'],
18
- ['4-review/reviewer.md', 'revisor do Gate 3: escopo, arquitetura, regressão'],
19
- ['4-review/security.md', 'segurança do Gate 3 (só com caminho sensível)'],
20
- ];
21
-
22
- function readJson(path) {
23
- return JSON.parse(readFileSync(path, 'utf8').replace(/^/, ''));
24
- }
25
-
26
- function decisionTitles(featureDir, limit = 5) {
27
- try {
28
- const path = join(featureDir, '2-plan', 'checks.json');
29
- if (!existsSync(path)) return [];
30
- const adrs = readJson(path).adrs;
31
- if (!Array.isArray(adrs)) return [];
32
- return adrs.map((a) => `${a.id} — ${a.title}`).slice(-limit);
33
- } catch {
34
- return [];
35
- }
36
- }
37
-
38
- function readWorktreeEnv(cwd) {
39
- try {
40
- const path = join(cwd, '.morph', 'worktree.env');
41
- if (!existsSync(path)) return null;
42
- const content = readFileSync(path, 'utf8');
43
- const get = (key) => content.match(new RegExp(`^${key}=(.+)$`, 'm'))?.[1] || null;
44
- const port = get('PORT');
45
- return port ? { port, range: get('MORPH_PORT_RANGE') } : null;
46
- } catch {
47
- return null;
48
- }
49
- }
50
-
51
- function nextLine(featureName, cwd) {
52
- const fallback = `Rode \`morph-spec next ${featureName}\`.`;
53
- try {
54
- const next = nextStepFor(featureName, cwd);
55
- return next ? formatNext(next) : fallback;
56
- } catch {
57
- return fallback;
58
- }
59
- }
60
-
61
- // `opts.worktree` overrides `feature.worktree`: that field is root-owned and stripped from branch-local
62
- // copies, so a handoff built inside a worktree would otherwise say "no worktree registered".
63
- export function buildHandoff(featureName, cwd = process.cwd(), opts = {}) {
64
- const featureDir = join(cwd, '.morph', 'features', featureName);
65
- if (!existsSync(join(featureDir, 'feature.json'))) return null;
66
-
67
- let feature;
68
- try {
69
- feature = readJson(join(featureDir, 'feature.json'));
70
- } catch {
71
- return null;
72
- }
73
-
74
- let phase = 'plan';
75
- let pendingGates = [];
76
- try {
77
- phase = derivePhaseForFeature(featureName, cwd) || 'plan';
78
- pendingGates = getPendingGates(featureName, cwd) || [];
79
- } catch {}
80
-
81
- const generatedAt = new Date().toISOString();
82
- const slices = sliceProgress(featureDir);
83
- const env = readWorktreeEnv(cwd);
84
- const decisions = decisionTitles(featureDir);
85
- const worktree = opts.worktree !== undefined ? opts.worktree : (feature.worktree || null);
86
-
87
- const gates = feature.approvals || feature.approvalGates || {};
88
- const gateLine = Object.entries(gates)
89
- .map(([name, g]) => (g?.approved ? `${name} ✔${g.approvedBy ? ` (${g.approvedBy})` : ''}` : `${name} pendente`))
90
- .join(' · ') || '—';
91
-
92
- const relFeatureDir = `.morph/features/${featureName}`;
93
- const pointers = POINTER_CANDIDATES
94
- .filter(([rel]) => existsSync(join(featureDir, rel)))
95
- .map(([rel, why], i) => `${i + 1}. \`${relFeatureDir}/${rel}\` — ${why}`);
96
-
97
- const lines = [
98
- `# Handoff — ${featureName}`,
99
- '',
100
- `> Documento derivado, gerado em ${generatedAt} por \`morph-spec handoff\`.`,
101
- '> Regenere com o comando — nunca edite à mão. Aponta para os arquivos da',
102
- '> feature em vez de copiá-los: leia os ponteiros, não confie em resumo velho.',
103
- '',
104
- '## Identidade',
105
- '',
106
- `- Feature: **${featureName}** (${feature.workType || 'feature'}) · fase: **${phase}** · status: ${feature.status || '—'}`,
107
- worktree?.path
108
- ? `- Local: worktree \`${worktree.path}\` · branch \`${worktree.branch || `morph/${featureName}`}\``
109
- : '- Local: raiz primária (sem worktree registrado)',
110
- env
111
- ? `- Ambiente: PORT ${env.port}${env.range ? ` (bloco ${env.range})` : ''} — fonte \`.morph/worktree.env\`; suba dev servers SEMPRE nessas portas`
112
- : null,
113
- '',
114
- '## Estado',
115
- '',
116
- `- Gates: ${gateLine}`,
117
- slices
118
- ? `- Fatias: ${slices.done}/${slices.total} feitas${slices.partial ? ` · ${slices.partial} parcial(is)` : ''}${slices.blocked ? ` · ${slices.blocked} bloqueada(s)` : ''} · prontas agora: ${readySlices(slices).map((s) => `**${s.id}** "${s.title}"`).join(', ') || 'nenhuma'}`
119
- : '- Fatias: sem checks.json ainda (rode plan-check)',
120
- pendingGates.length ? `- Gates pendentes: ${pendingGates.join(', ')}` : null,
121
- '',
122
- '## Leia nesta ordem (ponteiros — nada copiado)',
123
- '',
124
- ...(pointers.length ? pointers : ['1. (nenhum artefato ainda — feature recém-criada)']),
125
- decisions.length ? '' : null,
126
- decisions.length ? `Últimas decisões: ${decisions.map((d) => `"${d}"`).join(' · ')}` : null,
127
- '',
128
- '## Próxima ação',
129
- '',
130
- nextLine(featureName, cwd),
131
- '',
132
- '## Retomar',
133
- '',
134
- worktree?.path ? `- \`cd ${worktree.path}\` e abra o Claude Code (uma instância por worktree).` : '- Abra o Claude Code na raiz do projeto.',
135
- `- Visão da frota: \`morph-spec fleet\` · status: \`morph-spec status ${featureName}\``,
136
- '',
137
- ].filter((l) => l !== null);
138
-
139
- return {
140
- markdown: `${lines.join('\n')}`,
141
- meta: {
142
- generatedAt,
143
- phase,
144
- slicesDone: slices?.done ?? 0,
145
- slicesTotal: slices?.total ?? 0,
146
- },
147
- };
148
- }
149
-
150
- /** Marks the exclude block this generator owns in `.git/info/exclude`. */
151
- const HANDOFF_EXCLUDE_MARKER = '# morph-spec: derived handoff docs (managed — see handoff-generator)';
152
- const HANDOFF_EXCLUDE_PATTERN = '/.morph/**/handoff.md';
153
-
154
- // Never committed: left visible it lands in the `git status` the finish gates read. The common-dir
155
- // exclude hides it in every worktree; `**` also covers a feature folder after `archive` moves it.
156
- function ensureHandoffExcluded(cwd) {
157
- ensureCommonExclude(cwd, [HANDOFF_EXCLUDE_PATTERN], HANDOFF_EXCLUDE_MARKER);
158
- }
159
-
160
- /** The written path, or null when the feature does not exist (nothing is created). */
161
- export function writeHandoff(featureName, cwd = process.cwd(), opts = {}) {
162
- const result = buildHandoff(featureName, cwd, opts);
163
- if (!result) return null;
164
- const path = join(cwd, '.morph', 'features', featureName, 'handoff.md');
165
- try {
166
- ensureHandoffExcluded(cwd);
167
- writeFileSync(path, result.markdown, 'utf8');
168
- return path;
169
- } catch {
170
- return null;
171
- }
172
- }
@@ -1,79 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync } from 'node:fs';
2
- import { join, resolve } from 'node:path';
3
- import { spawn } from 'node:child_process';
4
- import { dotnetBuildCommands, withFilter, withTrx, withoutBuild } from './commands.js';
5
- import { parseTrx } from './trx.js';
6
- import { stripAnsi } from '../tasks/test-runner.js';
7
-
8
- const MAX_CAPTURE = 16 * 1024 * 1024;
9
-
10
- export function spawnExec(command, { cwd, env, timeoutMs }) {
11
- return new Promise((done) => {
12
- const start = Date.now();
13
- let stdout = '';
14
- let stderr = '';
15
- let timedOut = false;
16
- const child = spawn(command, { cwd, env, shell: true, windowsHide: true });
17
- child.stdout.setEncoding('utf8');
18
- child.stderr.setEncoding('utf8');
19
- child.stdout.on('data', (chunk) => { if (stdout.length < MAX_CAPTURE) stdout += chunk; });
20
- child.stderr.on('data', (chunk) => { if (stderr.length < MAX_CAPTURE) stderr += chunk; });
21
- const timer = setTimeout(() => { timedOut = true; child.kill(); }, timeoutMs);
22
- const finish = (status) => { clearTimeout(timer); done({ status, stdout, stderr, durationMs: Date.now() - start, timedOut }); };
23
- child.on('error', (err) => { stderr += String(err.message); finish(null); });
24
- child.on('close', (status) => finish(status));
25
- });
26
- }
27
-
28
- const FAIL_LINE = /^\s*(?:FAIL|✗|×|✖|not ok)\b|AssertionError|Error:|Expected/i;
29
-
30
- function genericFailures(output) {
31
- const lines = stripAnsi(output).split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
32
- const first = lines.findIndex((l) => FAIL_LINE.test(l));
33
- if (first === -1) return lines.slice(-3).map((l) => ({ name: l, message: '' }));
34
- return [{ name: lines[first], message: lines.slice(first + 1, first + 3).join(' ⏎ ') }];
35
- }
36
-
37
- export function dotnetAdapter({ node, root, workDir, exec, subset, timeoutMs }) {
38
- const cwd = resolve(root, node.cwd || '.');
39
- const trxDir = join(workDir, 'trx');
40
- mkdirSync(trxDir, { recursive: true });
41
- const base = withoutBuild(node.command);
42
- const commandFor = (label) => withTrx(withFilter(base, subset.filter), { dir: trxDir, name: `${label}.trx` });
43
- const runTests = async ({ label, env }) => {
44
- const trxPath = join(trxDir, `${label}.trx`);
45
- const r = await exec(commandFor(label), { cwd, env, timeoutMs, label });
46
- if (!existsSync(trxPath)) {
47
- return { outcome: 'crash', executed: null, total: null, failed: 0, failedTests: [], durationMs: r.durationMs, note: r.timedOut ? `timeout de ${timeoutMs} ms` : `sem TRX (exit ${r.status})` };
48
- }
49
- const t = parseTrx(readFileSync(trxPath, 'utf8'));
50
- if (!t.parsed || t.aborted > 0 || /abort/i.test(t.runOutcome ?? '')) {
51
- return { outcome: 'crash', executed: t.executed, total: t.total, failed: t.failed, failedTests: t.failedTests, durationMs: r.durationMs, note: 'a rodada foi abortada (testhost caiu ou estourou o tempo)' };
52
- }
53
- return { outcome: t.failed > 0 ? 'red' : 'green', executed: t.executed, total: t.total, failed: t.failed, failedTests: t.failedTests, durationMs: r.durationMs };
54
- };
55
- return {
56
- kind: 'dotnet',
57
- hasSubset: Boolean(subset?.usable),
58
- buildCommands: dotnetBuildCommands(node, root),
59
- cwd,
60
- runTests,
61
- };
62
- }
63
-
64
- export function genericAdapter({ node, root, exec, subset, timeoutMs }) {
65
- const cwd = resolve(root, node.cwd || '.');
66
- return {
67
- kind: node.runner,
68
- hasSubset: Boolean(subset?.usable),
69
- buildCommands: [],
70
- cwd,
71
- async runTests({ label, env }) {
72
- const r = await exec(subset.command, { cwd, env, timeoutMs, label });
73
- if (r.timedOut) return { outcome: 'crash', executed: null, total: null, failed: 0, failedTests: [], durationMs: r.durationMs, note: `timeout de ${timeoutMs} ms` };
74
- if (r.status === 0) return { outcome: 'green', executed: null, total: null, failed: 0, failedTests: [], durationMs: r.durationMs };
75
- const failedTests = genericFailures(`${r.stdout}\n${r.stderr}`);
76
- return { outcome: 'red', executed: null, total: null, failed: failedTests.length, failedTests, durationMs: r.durationMs };
77
- },
78
- };
79
- }
@@ -1,61 +0,0 @@
1
- import { resolve } from 'node:path';
2
- import { discoverDotnetBuildTargets, dotnetBuildCommandFor } from '../tasks/test-runner.js';
3
-
4
- const FILTER = /--filter(?:[ =:])(?:"([^"]*)"|'([^']*)'|(\S+))/;
5
- const quote = (s) => `"${String(s).replace(/"/g, '\\"')}"`;
6
-
7
- export function declaredFilter(command) {
8
- const m = FILTER.exec(command);
9
- return m ? (m[1] ?? m[2] ?? m[3]) : null;
10
- }
11
-
12
- export function withFilter(command, expression) {
13
- const declared = declaredFilter(command);
14
- if (declared === null) return `${command} --filter ${quote(expression)}`;
15
- return command.replace(FILTER, () => `--filter ${quote(`(${declared})&(${expression})`)}`);
16
- }
17
-
18
- export function withTrx(command, { dir, name }) {
19
- return `${command} --results-directory ${quote(dir)} --logger ${quote(`trx;LogFileName=${name}`)}`;
20
- }
21
-
22
- export function withoutBuild(command) {
23
- return /(?:^|\s)--no-build(?:\s|$)/.test(command) ? command : `${command} --no-build`;
24
- }
25
-
26
- const CONFIGURATION = /(?:^|\s)(?:-c|--configuration)[ =:]?(?:"([^"]*)"|(\S+))/;
27
- const VERBOSITY = /\s(?:-v|--verbosity)[ =:]?\S+/g;
28
-
29
- /**
30
- * The verify's build command per target. The test command's `-c` follows it, and
31
- * verbosity is minimal: at `quiet` the `Project -> dll` line the binary proof reads is gone.
32
- */
33
- export function dotnetBuildCommands(node, projectPath) {
34
- const configuration = CONFIGURATION.exec(node.command ?? '');
35
- const flag = configuration ? ` -c ${configuration[1] ?? configuration[2]}` : '';
36
- return discoverDotnetBuildTargets(node, resolve(projectPath || '.', node.cwd || '.'))
37
- .map((target) => dotnetBuildCommandFor(target, projectPath))
38
- .filter(Boolean)
39
- .map((command) => `${command.replace(VERBOSITY, '')} -v m${flag}`);
40
- }
41
-
42
- const BUILT = /^\s*(?<project>\S.*?) -> (?<dll>.+\.dll)\s*$/gm;
43
-
44
- export function parseBuiltDlls(output) {
45
- const dlls = new Map();
46
- for (const m of String(output).matchAll(BUILT)) dlls.set(m.groups.project, m.groups.dll.trim());
47
- return dlls;
48
- }
49
-
50
- const VITEST_RUN = /(vitest(?:\.mjs|\.js)?)\s+run\b/;
51
-
52
- export function relatedCommand(runner, command, files) {
53
- const list = files.map(quote).join(' ');
54
- if (runner === 'vitest' && VITEST_RUN.test(command)) {
55
- return `${command.replace(/\s+run\b/, ' related')} ${list} --run`;
56
- }
57
- if (runner === 'jest' && /\bjest\b/.test(command) && !/[;&|]/.test(command)) {
58
- return `${command} --findRelatedTests ${list}`;
59
- }
60
- return null;
61
- }
@@ -1,281 +0,0 @@
1
- import { existsSync, readFileSync, readdirSync } from 'fs';
2
- import { dirname, join, resolve } from 'path';
3
-
4
- import { isInside } from '../tasks/test-runner.js';
5
- import { jsoncToJson } from '../../../framework/hooks/shared/comment-stripper.js';
6
-
7
- const MTP_RUNNER = 'Microsoft.Testing.Platform';
8
- const TYPE_HEADER = /\b(?:class|struct|record|interface)\b/;
9
- // `@` = verbatim identifier, `\` = unicode escape.
10
- const NAME_START = '[\\p{L}\\p{Nl}_@\\\\]';
11
- // TRIPWIRE with NO context rule: any exclusion based on what precedes the keyword can be
12
- // forged by a comment or exposed string. The quote in the lookahead makes every keyword
13
- // the blanked code counts also counted in the raw text, at the same offset.
14
- const TYPE_KEYWORD = new RegExp(`\\b(?:record\\s+(?:class|struct)|class|record|struct|interface)\\b(?=\\s*(?:${NAME_START}|["']|\\/[/*]|#))`, 'gu');
15
- const TYPE_DECL = new RegExp(`(?:^|[;{}\\]\\n])[ \\t]*(?:\\[[^\\]\\n]*\\][ \\t]*)*(?:(?:public|internal|private|protected|sealed|static|partial|abstract|unsafe|file|new|readonly|ref)\\s+)*(record\\s+(?:class|struct)|class|record|struct|interface)\\s+(${NAME_START}[^\\s:<({;,=]*)`, 'gu');
16
- const IDENT = '[\\p{L}_][\\p{L}\\p{N}_]*';
17
- const QUALIFIER = `(?:${IDENT}\\s*(?:\\.|::)\\s*)*`;
18
- const ATTRIBUTE_ARGS = '(?:\\((?:[^()\\]]|\\([^()\\]]*\\))*\\))?';
19
- const OTHER_ATTRIBUTE = `${QUALIFIER}${IDENT}\\s*${ATTRIBUTE_ARGS}`;
20
- const TEST_ATTRIBUTE_NAME = '(?:Fact|Theory|Test|TestMethod|DataTestMethod|TestCase|TestCaseSource)(?:Attribute)?';
21
- // A false POSITIVE is the unsafe side (a helper taken for a test file hides the tests that
22
- // use it), so the whole name must sit in an attribute section that CLOSES: a C# 12
23
- // collection such as `[\n Env.Test,\n];` must not match. Residual cases: csharpClasses.
24
- export const CS_TEST_ATTRIBUTE = new RegExp(
25
- '(?:^|[;{}\\]]|\\*\\/)[ \\t]*\\['
26
- + `\\s*(?:${IDENT}\\s*:(?!:)\\s*)?`
27
- + `(?:${OTHER_ATTRIBUTE}\\s*,\\s*)*`
28
- + `${QUALIFIER}${TEST_ATTRIBUTE_NAME}(?![\\p{L}\\p{N}_])\\s*${ATTRIBUTE_ARGS}`
29
- + `(?:\\s*,\\s*${OTHER_ATTRIBUTE})*`
30
- + '\\s*\\]\\s*(?:[\\[\\p{L}_@/#]|(?![\\s\\S]))',
31
- 'mu',
32
- );
33
-
34
- // Roslyn reads CR/NEL/LS/PS as new-lines and U+001A as whitespace; JS `\s` misses NEL
35
- // and U+001A, which hid `class<NEL>Name`. Mapped 1:1 so offsets stay aligned with the code.
36
- const readText = (path) => {
37
- try { return readFileSync(path, 'utf8').replace(/\x1A/g, ' ').replace(/^\uFEFF/, '').replace(/[\r\u0085\u2028\u2029]/g, '\n'); } catch { return null; }
38
- };
39
-
40
- function nearestProjectFile(fileAbs, rootAbs, projectName) {
41
- for (let dir = dirname(fileAbs); isInside(dir, rootAbs); dir = dirname(dir)) {
42
- let names = [];
43
- try { names = readdirSync(dir).filter((n) => projectName.test(n)).sort(); } catch { /* unreadable dir */ }
44
- if (names.length > 0) return join(dir, names[0]);
45
- if (dirname(dir) === dir) break;
46
- }
47
- return null;
48
- }
49
-
50
- export function nearestProjectOf(fileAbs, rootAbs) {
51
- return nearestProjectFile(fileAbs, rootAbs, /\.(?:cs|fs|vb)proj$/i);
52
- }
53
-
54
- const NON_CSHARP_SOURCE = /\.(?:razor|cshtml|fs|fsi|fsx|vb)$/i;
55
-
56
- /** No type scan, hence no class selector, for these. */
57
- export const isNonCSharpSource = (path) => NON_CSHARP_SOURCE.test(String(path));
58
-
59
- export const isDotnetSource = (path) => /\.cs$/i.test(String(path)) || isNonCSharpSource(path);
60
-
61
- /**
62
- * Nearest global.json walking up from the CWD, as the SDK resolves it (never from the
63
- * `.sln` argument). null when one exists but cannot be read — never a guess.
64
- */
65
- export function dotnetTestMode(absCwd) {
66
- for (let dir = resolve(absCwd); ; dir = dirname(dir)) {
67
- const file = join(dir, 'global.json');
68
- if (existsSync(file)) {
69
- try {
70
- const parsed = JSON.parse(jsoncToJson(readFileSync(file, 'utf8')));
71
- return parsed?.test?.runner === MTP_RUNNER ? 'mtp' : 'vstest';
72
- } catch {
73
- return null;
74
- }
75
- }
76
- if (dirname(dir) === dir) return 'vstest';
77
- }
78
- }
79
-
80
- const lineEnd = (text, i) => { const e = text.indexOf('\n', i); return e < 0 ? text.length : e; };
81
-
82
- function closeOnLine(text, start, quote) {
83
- for (let j = start + 1; j < text.length && text[j] !== '\n'; j++) {
84
- if (text[j] === '\\') j++;
85
- else if (text[j] === quote) return j + 1;
86
- }
87
- return -1;
88
- }
89
-
90
- // A `"` inside a hole (`$"{d["k"]}"`) ends the literal as the scanner reads it,
91
- // leaving the hole open. `{{`/`}}` outside a hole are escapes.
92
- function holeLeftOpen(content) {
93
- let depth = 0;
94
- for (let k = 0; k < content.length; k++) {
95
- const c = content[k];
96
- if (depth === 0 && (c === '{' || c === '}') && content[k + 1] === c) { k++; continue; }
97
- if (c === '{') depth++;
98
- else if (c === '}' && depth > 0) depth--;
99
- }
100
- return depth > 0;
101
- }
102
-
103
- /**
104
- * Comments, preprocessor lines and literals blanked, length and newlines kept; null when
105
- * a literal never ends or an interpolation hole is cut. A heuristic lexer that CAN misread:
106
- * csharpClasses never trusts it alone (it checks against the raw text).
107
- */
108
- export function blankCSharpLiterals(text) {
109
- const out = text.split('');
110
- const blank = (from, to) => { for (let k = from; k < to; k++) if (out[k] !== '\n') out[k] = ' '; };
111
- for (let i = 0; i < text.length;) {
112
- const c = text[i];
113
- let end = -1;
114
- if (c === '#' && /^[ \t]*$/.test(text.slice(text.lastIndexOf('\n', i - 1) + 1, i))) {
115
- end = lineEnd(text, i);
116
- } else if (c === '/' && text[i + 1] === '/') {
117
- end = lineEnd(text, i);
118
- } else if (c === '/' && text[i + 1] === '*') {
119
- const close = text.indexOf('*/', i + 2);
120
- if (close < 0) return null;
121
- end = close + 2;
122
- } else if (c === '"' && (text[i - 1] === '@' || (text[i - 1] === '$' && text[i - 2] === '@'))) {
123
- let j = i + 1;
124
- while (j < text.length && !(text[j] === '"' && text[j + 1] !== '"')) j += text[j] === '"' ? 2 : 1;
125
- if (j >= text.length) return null;
126
- end = j + 1;
127
- const interpolated = text[i - 1] === '$' || text[i - 2] === '$';
128
- if (interpolated && holeLeftOpen(text.slice(i + 1, end - 1))) return null;
129
- } else if (c === '"') {
130
- let quotes = 0;
131
- while (text[i + quotes] === '"') quotes++;
132
- if (quotes >= 3) {
133
- const close = text.indexOf('"'.repeat(quotes), i + quotes);
134
- if (close < 0) return null;
135
- end = close + quotes;
136
- } else {
137
- end = closeOnLine(text, i, '"');
138
- if (end < 0) return null;
139
- if (text[i - 1] === '$' && holeLeftOpen(text.slice(i + 1, end - 1))) return null;
140
- }
141
- } else if (c === "'") {
142
- end = closeOnLine(text, i, "'");
143
- if (end < 0) return null;
144
- }
145
- if (end < 0) { i++; continue; }
146
- blank(i, end);
147
- i = end;
148
- }
149
- return out.join('');
150
- }
151
-
152
- const typeKeywordCount = (text) => [...text.matchAll(TYPE_KEYWORD)].length;
153
-
154
- /**
155
- * The types of a C# file, read FAIL-SAFE.
156
- *
157
- * `types` lists the BARE name of EVERY type declaration (class, record, struct,
158
- * interface) at ANY depth, in source order, with no namespace. A selection built
159
- * from these names is namespace-free (`--filter-class "*T*"`, measured, or the
160
- * substring `FullyQualifiedName~T`): any namespace, `Outer+Inner`, `Name`1` — a
161
- * selection that can only run MORE tests than the file's, never fewer.
162
- *
163
- * THE GUARANTEE (what `scannable: true` means), and why it holds:
164
- * every type-keyword token of the RAW text is a declaration in `types`.
165
- * - R = keyword tokens of the raw text (TYPE_KEYWORD, no context rule); C = the
166
- * same on the blanked code; D = the declarations named (TYPE_DECL on the code).
167
- * `scannable` requires |R| = |D| and |C| = |D|. By construction D ⊆ C ⊆ R as
168
- * sets of offsets: blanking keeps length and only replaces characters by spaces,
169
- * a blanked region starts at `"` `'` `/` `#` and ends at `"` `'` `*` `/` or a
170
- * line end (never inside an identifier), and the raw lookahead accepts every
171
- * character the code lookahead can be looking past (a name, a comment, `#`, a
172
- * quote). So |R| = |D| forces R = D. A real declaration's keyword is always in R
173
- * (it is followed by its name, or by a comment/directive before the name) —
174
- * UNDER ONE ASSUMPTION: every character Roslyn reads as whitespace or a new-line
175
- * between a keyword and its name is whitespace to JS `\s` once readText has run.
176
- * Roslyn's whitespace is Unicode Zs, TAB, VT, FF and U+001A; its new-lines are
177
- * CR, LF, NEL, LS and PS. JS `\s` already covers Zs, TAB, VT, FF and LF, and
178
- * readText maps CR/NEL/LS/PS to `\n` and U+001A to a space. A separator outside
179
- * that shared set would hide a declaration from R; none is known, and that is a
180
- * statement about the two lexers as read, not a proof.
181
- * Hence: a declaration swallowed by ANY misread — `"/*"` taken for a comment
182
- * opener, a quote inside an interpolation hole, a char literal — is still in R
183
- * and cannot be compensated by anything (a fake the code exposes is in R too).
184
- * - `abstract` lists the abstract types: their tests run under the names of DERIVED
185
- * classes, possibly in other files.
186
- * - `hasTestAttribute`: a test attribute (CS_TEST_ATTRIBUTE: a whole name inside an
187
- * attribute section) is present in the blanked code (a file without one is a
188
- * helper/fake/fixture: the tests using it are elsewhere).
189
- * - kept fail-safes: braces balance and blankCSharpLiterals returning null → unscannable.
190
- *
191
- * NOT guaranteed (stated, not hidden):
192
- * - completeness: keyword-shaped text in a real comment or string (`// the
193
- * class under test`), a constraint keyword followed by a name-shaped token
194
- * (`where T : class where …`, `: class // note`), `record with`, a directive
195
- * splitting a name — all make the file unscannable;
196
- * - which scope a type was read in;
197
- * - a NON-abstract class whose tests are inherited by derived classes declared in
198
- * OTHER files: the touched class runs; the derived copies only if their names
199
- * contain it;
200
- * - a test attribute reached through an alias (`using F = Xunit.FactAttribute; [F]`)
201
- * or a custom one (`[SkippableFact]`) is not seen: the file counts as a helper and
202
- * it is left out of a selection;
203
- * - `hasTestAttribute` is still a heuristic, and ITS false positive is the unsafe
204
- * side. The section must close; what still matches in a
205
- * helper — probed — is a LIST PATTERN opening a line (or after `;` `{` `}` `]`)
206
- * whose element is named exactly `Test`/`Fact`/… (qualified or not) and whose `]`
207
- * is followed by a combinator: `[Env.Test] or [Env.Dev] => true`. Such a helper is
208
- * taken for a test file. (`[Env.Test] => 1` does not match: `=` starts no declaration.)
209
- *
210
- * @param {string} csFileAbs
211
- * @returns {{ names: string[], types: string[], abstract: string[], hasTestClass: boolean, hasTestAttribute: boolean, scannable: boolean, why: string|null }}
212
- * `names`: top-level `class` names (classNamesOf). `abstract`: the abstract
213
- * types. `hasTestClass`: some plain `class` is declared (records, structs and
214
- * interfaces hold no tests). `hasTestAttribute`: see above. `why`: the reason
215
- * when `scannable` is false.
216
- */
217
- export function csharpClasses(csFileAbs) {
218
- const unscannable = (why) => ({ names: [], types: [], abstract: [], hasTestClass: false, hasTestAttribute: false, scannable: false, why });
219
- const text = readText(csFileAbs);
220
- if (text === null) return { names: [], types: [], abstract: [], hasTestClass: false, hasTestAttribute: false, scannable: true, why: null };
221
- const code = blankCSharpLiterals(text);
222
- if (code === null) return unscannable('a literal or comment never ends, or an interpolation hole is cut by an inner quote');
223
-
224
- const decls = [...code.matchAll(TYPE_DECL)].map((m) => ({
225
- at: m.index + m[0].length - m[2].length,
226
- kind: m[1].split(/\s+/)[0],
227
- name: m[2],
228
- abstract: /\babstract\b/.test(m[0]),
229
- gap: m[0].slice(0, m[0].length - m[2].length).match(/\s*$/)[0].length,
230
- }));
231
- // A preprocessor line between a keyword and its name (`class\n#if X\nA\n#else\nB…`)
232
- // makes the compiled name depend on a symbol this scan cannot see. Blanking keeps
233
- // length, so the raw text at the same offsets shows the directive.
234
- const split = decls.find((d) => /(^|\n)[ \t]*#/.test(text.slice(d.at - d.gap, d.at)));
235
- if (split) return unscannable(`a preprocessor directive stands between a type keyword and the name "${split.name}"`);
236
- // The raw text is the lexer-independent truth about how many type keywords the file
237
- // carries (the JSDoc explains why equal counts mean equal sets).
238
- const rawKeywords = typeKeywordCount(text);
239
- if (rawKeywords !== decls.length) {
240
- return unscannable(`${rawKeywords} type keyword(s) in the file but ${decls.length} declaration(s) recognised`);
241
- }
242
- const codeKeywords = typeKeywordCount(code);
243
- if (codeKeywords !== decls.length) {
244
- return unscannable(`${codeKeywords} type keyword(s) in the code but ${decls.length} declaration(s) recognised`);
245
- }
246
-
247
- const names = [];
248
- const types = [];
249
- const abstractTypes = [];
250
- const scopes = [];
251
- let hasTestClass = false;
252
- let header = 0;
253
- for (let i = 0, d = 0; i < code.length; i++) {
254
- for (; d < decls.length && decls[d].at === i; d++) {
255
- types.push(decls[d].name);
256
- if (decls[d].abstract) abstractTypes.push(decls[d].name);
257
- if (decls[d].kind === 'class') {
258
- hasTestClass = true;
259
- if (!scopes.some((s) => s.type)) names.push(decls[d].name);
260
- }
261
- }
262
- const c = code[i];
263
- if (c === '{') {
264
- scopes.push({ type: TYPE_HEADER.test(code.slice(header, i)) });
265
- } else if (c === '}' && scopes.pop() === undefined) {
266
- return unscannable('a `}` closes nothing');
267
- }
268
- if (c === '{' || c === '}' || c === ';') header = i + 1;
269
- }
270
- if (scopes.length > 0) return unscannable('braces left open at end of file');
271
- return {
272
- names: [...new Set(names)],
273
- types: [...new Set(types)],
274
- abstract: [...new Set(abstractTypes)],
275
- hasTestClass,
276
- hasTestAttribute: CS_TEST_ATTRIBUTE.test(code),
277
- scannable: true,
278
- why: null,
279
- };
280
- }
281
-