@polymorphism-tech/morph-spec 8.44.0 → 8.46.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 (199) hide show
  1. package/CLAUDE.md +11 -9
  2. package/README.md +10 -10
  3. package/bin/morph-spec.js +11 -13
  4. package/bin/validate.js +379 -379
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +2 -4
  7. package/docs/QUICKSTART.md +6 -6
  8. package/framework/CLAUDE.md +6 -8
  9. package/framework/MORPH.md +39 -45
  10. package/framework/agents.json +8 -9
  11. package/framework/commands/morph-apply.md +1 -3
  12. package/framework/commands/morph-hotfix.md +1 -1
  13. package/framework/commands/morph-proposal.md +2 -2
  14. package/framework/docs/CLI.md +15 -14
  15. package/framework/evals/review-checklist.md +90 -0
  16. package/framework/evals/trace-schema.md +2 -12
  17. package/framework/hooks/README.md +2 -3
  18. package/framework/hooks/claude-code/core/heavy-gate.js +1 -1
  19. package/framework/hooks/claude-code/core/protect-spec-files.js +2 -2
  20. package/framework/hooks/claude-code/core/telemetry-log.js +2 -2
  21. package/framework/hooks/claude-code/core/trace-autogen.js +1 -22
  22. package/framework/hooks/claude-code/core/validate-completion.js +3 -56
  23. package/framework/hooks/claude-code/statusline.sh +11 -11
  24. package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
  25. package/framework/hooks/git/pre-commit/agents.sh +25 -25
  26. package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
  27. package/framework/hooks/git/pre-commit/specs.sh +50 -50
  28. package/framework/hooks/git/pre-push/run-tests.sh +44 -44
  29. package/framework/hooks/shared/build-check.js +1 -1
  30. package/framework/hooks/shared/comment-scanner.js +1 -1
  31. package/framework/hooks/shared/phase-utils.js +0 -5
  32. package/framework/hooks/shared/stdin-reader.js +26 -26
  33. package/framework/phases.json +4 -6
  34. package/framework/rubrics/backend.json +1 -1
  35. package/framework/rubrics/default.json +1 -1
  36. package/framework/rubrics/frontend.json +1 -1
  37. package/framework/rules/morph-workflow.md +6 -6
  38. package/framework/schemas/config.schema.json +4 -0
  39. package/framework/schemas/feature.schema.json +5 -29
  40. package/framework/schemas/tasks.schema.json +6 -2
  41. package/framework/skills/README.md +1 -2
  42. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  43. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  44. package/framework/skills/core/morph-apply/SKILL.md +145 -246
  45. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  46. package/framework/skills/core/morph-hotfix/SKILL.md +5 -3
  47. package/framework/skills/core/morph-implement/SKILL.md +101 -201
  48. package/framework/skills/core/morph-machine/SKILL.md +3 -3
  49. package/framework/skills/core/morph-media/SKILL.md +1 -1
  50. package/framework/skills/core/morph-plan/SKILL.md +171 -287
  51. package/framework/skills/core/morph-proposal/SKILL.md +4 -5
  52. package/framework/skills/core/morph-review/SKILL.md +122 -430
  53. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  54. package/framework/skills/core/morph-standards/SKILL.md +2 -2
  55. package/framework/skills/core/morph-uiux/SKILL.md +2 -3
  56. package/framework/standards/STANDARDS.json +10 -10
  57. package/framework/standards/ai-agents/agent-archetypes.md +1 -1
  58. package/framework/standards/ai-agents/agent-session.md +1 -1
  59. package/framework/standards/ai-agents/agent-spec.md +1 -1
  60. package/framework/standards/ai-agents/batching.md +1 -1
  61. package/framework/standards/ai-agents/context-providers.md +1 -1
  62. package/framework/standards/ai-agents/conversational-agent-with-phases.md +1 -1
  63. package/framework/standards/ai-agents/cost-and-budget.md +1 -1
  64. package/framework/standards/ai-agents/durable-workflows-hitl.md +1 -1
  65. package/framework/standards/ai-agents/guardrails.md +1 -1
  66. package/framework/standards/ai-agents/llm-runtime-defaults.md +1 -1
  67. package/framework/standards/ai-agents/mcp-server.md +1 -1
  68. package/framework/standards/ai-agents/mcp-tools.md +1 -1
  69. package/framework/standards/ai-agents/media-pipeline.md +1 -1
  70. package/framework/standards/ai-agents/media-video.md +1 -1
  71. package/framework/standards/ai-agents/middleware-patterns.md +1 -1
  72. package/framework/standards/ai-agents/modalities-image-gen.md +1 -1
  73. package/framework/standards/ai-agents/modalities-speech-to-text.md +1 -1
  74. package/framework/standards/ai-agents/modalities-text-to-speech.md +1 -1
  75. package/framework/standards/ai-agents/modalities-vision.md +1 -1
  76. package/framework/standards/ai-agents/multi-agent-patterns.md +1 -1
  77. package/framework/standards/ai-agents/observability-patterns.md +3 -3
  78. package/framework/standards/ai-agents/production.md +2 -2
  79. package/framework/standards/ai-agents/prompt-sources.md +1 -1
  80. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  81. package/framework/standards/ai-agents/rag-custom-pgvector.md +1 -1
  82. package/framework/standards/ai-agents/service-tier-flex.md +1 -1
  83. package/framework/standards/ai-agents/setup.md +1 -1
  84. package/framework/standards/ai-agents/structured-output.md +1 -1
  85. package/framework/standards/ai-agents/sweet-spot.md +1 -1
  86. package/framework/standards/ai-agents/testing-ai.md +1 -1
  87. package/framework/standards/ai-agents/tool-adapters.md +1 -1
  88. package/framework/standards/ai-agents/vector-data-extensions.md +1 -1
  89. package/framework/standards/ai-agents/workflows.md +1 -1
  90. package/framework/templates/CLAUDE.md.core.template +1 -1
  91. package/framework/templates/IDE-SNIPPETS.md +262 -262
  92. package/framework/templates/README.md +1 -1
  93. package/framework/templates/REGISTRY.json +0 -81
  94. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  95. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
  96. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  97. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  98. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  99. package/framework/templates/docs/spec.md +19 -205
  100. package/framework/templates/docs/ui-components.md +52 -52
  101. package/framework/templates/docs/ui-design-system.md +76 -76
  102. package/framework/templates/docs/ui-flows.md +167 -167
  103. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  104. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  105. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  106. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  107. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  108. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  109. package/framework/templates/infrastructure/github/README.md +56 -56
  110. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  111. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  112. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  113. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  114. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  115. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  116. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
  117. package/framework/templates/state.template.json +17 -17
  118. package/framework/templates/ui/design-system.css +226 -226
  119. package/package.json +2 -2
  120. package/src/commands/e2e/e2e.js +1 -1
  121. package/src/commands/project/heavy.js +1 -1
  122. package/src/commands/project/status.js +3 -3
  123. package/src/commands/state/approve.js +7 -49
  124. package/src/commands/state/gate-check.js +16 -106
  125. package/src/commands/state/gate-decision.js +1 -1
  126. package/src/commands/state/report-check.js +42 -17
  127. package/src/commands/validation/mutate.js +1 -1
  128. package/src/commands/validation/scout-check.js +28 -0
  129. package/src/commands/validation/validate-feature.js +5 -32
  130. package/src/commands/validation/verify.js +98 -48
  131. package/src/core/paths/output-schema.js +0 -2
  132. package/src/core/state/state-manager.js +1 -2
  133. package/src/core/templates/template-renderer.js +477 -477
  134. package/src/dashboard/app/lib/format.js +5 -0
  135. package/src/dashboard/app/main.jsx +13 -13
  136. package/src/dashboard/app/pages/FeatureDetail.jsx +16 -22
  137. package/src/dashboard/app/pages/Features.jsx +7 -7
  138. package/src/dashboard/app/pages/PhasePipeline.jsx +1 -1
  139. package/src/dashboard/app/pages/Rubrics.jsx +7 -11
  140. package/src/dashboard/scanner/docs-scanner.js +1 -2
  141. package/src/dashboard/scanner/features-scanner.js +3 -4
  142. package/src/dashboard/scanner/rubrics-scanner.js +12 -12
  143. package/src/dashboard/vite.config.js +24 -24
  144. package/src/lib/agents/output-collector.js +0 -1
  145. package/src/lib/ci/ci-verify.js +118 -0
  146. package/src/lib/ci/github-ci.js +192 -0
  147. package/src/lib/ci/workflow-scaffold.js +122 -0
  148. package/src/lib/detectors/design-system-detector.js +188 -188
  149. package/src/lib/evaluator-report.js +52 -334
  150. package/src/lib/generators/design-system-generator.js +508 -508
  151. package/src/lib/generators/handoff-generator.js +4 -5
  152. package/src/lib/installers/memory-installer.js +2 -2
  153. package/src/lib/mutation/manifest.js +8 -2
  154. package/src/lib/mutation/report.js +5 -3
  155. package/src/lib/mutation/runner.js +9 -7
  156. package/src/lib/risk-detector.js +60 -122
  157. package/src/lib/scout/scout-check.js +293 -0
  158. package/src/lib/tasks/task-classifier.js +30 -30
  159. package/src/lib/tasks/task-parser.js +209 -209
  160. package/src/lib/tasks/tasks-json-parser.js +7 -5
  161. package/src/lib/tasks/test-nodes.js +1 -1
  162. package/src/lib/tasks/test-runner.js +1 -1
  163. package/src/lib/telemetry/verify-event.js +3 -2
  164. package/src/lib/validators/css/css-validator.js +340 -340
  165. package/src/lib/validators/reuse/reuse-validator.js +6 -8
  166. package/src/lib/validators/verify-runner.js +1 -1
  167. package/src/scripts/setup-infra.js +1 -1
  168. package/src/utils/agent-id-mapper.js +11 -11
  169. package/src/utils/agents-installer.js +5 -9
  170. package/src/utils/claude-settings-manager.js +300 -300
  171. package/src/utils/hooks-installer.js +3 -10
  172. package/src/utils/skills-installer.js +1 -1
  173. package/framework/evals/proof-rubrics/README.md +0 -76
  174. package/framework/evals/proof-rubrics/eval-de-modelo.md +0 -80
  175. package/framework/evals/proof-rubrics/fiacao.md +0 -54
  176. package/framework/evals/proof-rubrics/guard-cego.md +0 -52
  177. package/framework/evals/proof-rubrics/media-claims.md +0 -109
  178. package/framework/evals/proof-rubrics/mutation-proof.md +0 -107
  179. package/framework/evals/proof-rubrics/observabilidade-genai.md +0 -119
  180. package/framework/evals/proof-rubrics/verde-nao-e-coberto.md +0 -48
  181. package/framework/evals/rubrics/README.md +0 -45
  182. package/framework/evals/rubrics/architecture.md +0 -103
  183. package/framework/evals/rubrics/code-quality.md +0 -98
  184. package/framework/evals/rubrics/condensed.md +0 -57
  185. package/framework/evals/rubrics/contracts.md +0 -67
  186. package/framework/evals/rubrics/test-coverage.md +0 -99
  187. package/framework/evals/scenarios/dotnet-crud.yaml +0 -42
  188. package/framework/evals/scenarios/nextjs-auth.yaml +0 -38
  189. package/framework/evals/suites/standard.yaml +0 -5
  190. package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
  191. package/framework/hooks/claude-code/core/post-edit-typecheck.js +0 -127
  192. package/framework/skills/core/morph-eval/SKILL.md +0 -64
  193. package/framework/templates/.idea/morph-templates.xml +0 -92
  194. package/framework/templates/.vscode/morph-templates.code-snippets +0 -186
  195. package/framework/templates/docs/clarifications.md +0 -253
  196. package/framework/templates/docs/onboarding.md +0 -122
  197. package/framework/templates/docs/schema-analysis.md +0 -119
  198. package/framework/templates/docs/user-stories.md +0 -34
  199. package/src/commands/state/score.js +0 -147
@@ -0,0 +1,192 @@
1
+ import { execFileSync, spawnSync } from 'child_process';
2
+ import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync } from 'fs';
3
+ import { tmpdir } from 'os';
4
+ import { join } from 'path';
5
+ import { resolveGh } from '../git/gh.js';
6
+
7
+ export const CI_WORKFLOW_DIR = '.github/workflows';
8
+ export const CI_WORKFLOW_FILE = 'morph-verify.yml';
9
+ export const CI_ARTIFACT = 'morph-verify';
10
+ export const CI_RESULT_FILE = 'morph-verify.json';
11
+
12
+ // A run that never judged the code (superseded, filtered out, quota) is no proof either way.
13
+ const NO_VERDICT = new Set(['cancelled', 'skipped', 'startup_failure', 'stale', 'neutral']);
14
+
15
+ const VERIFY_CALL = /morph-spec\S*\s+verify\b/;
16
+
17
+ const git = (cwd, args) => execFileSync('git', args, { cwd, stdio: 'pipe' }).toString();
18
+
19
+ function gh(cwd, args, timeout = 15000) {
20
+ const { bin, prefix } = resolveGh();
21
+ return execFileSync(bin, [...prefix, ...args], { cwd, stdio: 'pipe', timeout }).toString();
22
+ }
23
+
24
+ /**
25
+ * The workflow that can stand in for `verify {feature}`: it must call `morph-spec verify` AND
26
+ * publish its JSON, because only that artifact becomes a stamp. A green run of any other
27
+ * workflow (lint, deploy) proves nothing about the suite.
28
+ *
29
+ * @param {string} root
30
+ * @returns {string|null} workflow file name, as `gh run list --workflow` takes it
31
+ */
32
+ export function findCiWorkflow(root) {
33
+ const dir = join(root, CI_WORKFLOW_DIR);
34
+ if (!existsSync(dir)) return null;
35
+ const files = readdirSync(dir).filter((f) => /\.ya?ml$/i.test(f)).sort((a, b) =>
36
+ (a === CI_WORKFLOW_FILE ? -1 : b === CI_WORKFLOW_FILE ? 1 : a.localeCompare(b)));
37
+ for (const file of files) {
38
+ let text;
39
+ try {
40
+ text = readFileSync(join(dir, file), 'utf8');
41
+ } catch {
42
+ continue;
43
+ }
44
+ if (VERIFY_CALL.test(text) && text.includes(CI_RESULT_FILE)) return file;
45
+ }
46
+ return null;
47
+ }
48
+
49
+ export function currentBranch(root) {
50
+ try {
51
+ return git(root, ['rev-parse', '--abbrev-ref', 'HEAD']).trim();
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ function originUrl(root) {
58
+ try {
59
+ return git(root, ['config', '--get', 'remote.origin.url']).trim();
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ function ghAuthText(root) {
66
+ const { bin, prefix } = resolveGh();
67
+ const res = spawnSync(bin, [...prefix, 'auth', 'status'], { cwd: root, stdio: 'pipe', timeout: 10000 });
68
+ if (res.error || res.status !== 0) return null;
69
+ return `${res.stdout ?? ''}${res.stderr ?? ''}`;
70
+ }
71
+
72
+ /**
73
+ * Whether CI can answer for this feature, and why not. Never throws: every "no" is a reason the
74
+ * caller prints before falling back to the local verify.
75
+ *
76
+ * @param {string} root - the feature's tree
77
+ * @param {string} featureName
78
+ * @param {object|null} config - `.morph/config/config.json`
79
+ * @returns {{ ok: true, branch: string, https: boolean, scopes: string|null } | { ok: false, reason: string }}
80
+ */
81
+ export function ciAvailability(root, featureName, config) {
82
+ if (config?.project?.ci === false) return { ok: false, reason: 'project.ci = false no config.json' };
83
+ const url = originUrl(root);
84
+ if (!url) return { ok: false, reason: 'sem remote origin' };
85
+ if (!/github\.com/i.test(url)) return { ok: false, reason: 'origin não é GitHub' };
86
+ const branch = currentBranch(root);
87
+ if (branch !== `morph/${featureName}`) {
88
+ return { ok: false, reason: `branch atual "${branch}" não é morph/${featureName}` };
89
+ }
90
+ const auth = ghAuthText(root);
91
+ if (auth === null) return { ok: false, reason: 'gh ausente ou não autenticado' };
92
+ const scopes = auth.match(/Token scopes:\s*(.*)/i)?.[1] ?? null;
93
+ return { ok: true, branch, https: /^https?:/i.test(url), scopes };
94
+ }
95
+
96
+ /**
97
+ * Creating a workflow file over HTTPS needs the token's `workflow` scope; without it GitHub
98
+ * rejects the whole push. Unknown scopes (fine-grained token, SSH remote) are not a refusal.
99
+ */
100
+ export function workflowPushProblem({ https, scopes }) {
101
+ if (!https || scopes === null) return null;
102
+ return /\bworkflow\b/.test(scopes) ? null : 'o token do gh não tem o escopo "workflow" (gh auth refresh -s workflow)';
103
+ }
104
+
105
+ /**
106
+ * Paths CI would not see: uncommitted or untracked files outside `.morph/`, plus the config,
107
+ * which names the test commands.
108
+ *
109
+ * @returns {string[]|null} null when git did not answer
110
+ */
111
+ export function uncommittedCode(root) {
112
+ let out;
113
+ try {
114
+ out = git(root, ['status', '--porcelain', '-z', '--untracked-files=all', '--no-renames']);
115
+ } catch {
116
+ return null;
117
+ }
118
+ return out.split('\0').filter(Boolean).map((line) => line.slice(3))
119
+ .filter((path) => !path.startsWith('.morph/') || path === '.morph/config/config.json');
120
+ }
121
+
122
+ export function pushBranch(root, branch) {
123
+ try {
124
+ git(root, ['push', '-u', 'origin', branch]);
125
+ return null;
126
+ } catch (err) {
127
+ return (err.stderr?.toString() || err.message).trim().split('\n').slice(-3).join(' ');
128
+ }
129
+ }
130
+
131
+ /** @returns {Array<object>|null} newest first; null = gh did not answer */
132
+ export function listRuns(root, branch, workflow) {
133
+ try {
134
+ return JSON.parse(gh(root, ['run', 'list', '--branch', branch, '--workflow', workflow, '--limit', '20',
135
+ '--json', 'databaseId,status,conclusion,headSha,url,createdAt']));
136
+ } catch {
137
+ return null;
138
+ }
139
+ }
140
+
141
+ /**
142
+ * The newest run over the same code as HEAD. `isCurrent` is a tree comparison, not SHA equality:
143
+ * the evaluator and the archive commit only `.morph/`, and the workflow does not re-run for them.
144
+ */
145
+ export function pickRun(runs, isCurrent) {
146
+ return (runs || []).find((run) => !(run.status === 'completed' && NO_VERDICT.has(run.conclusion))
147
+ && run.headSha && isCurrent(run.headSha)) ?? null;
148
+ }
149
+
150
+ /** @returns {object|null} the parsed `verify --json` the run uploaded; null when absent or unreadable */
151
+ export function downloadResult(root, runId) {
152
+ const dir = mkdtempSync(join(tmpdir(), 'morph-ci-'));
153
+ try {
154
+ gh(root, ['run', 'download', String(runId), '--name', CI_ARTIFACT, '--dir', dir], 120000);
155
+ return JSON.parse(readFileSync(join(dir, CI_RESULT_FILE), 'utf8'));
156
+ } catch {
157
+ return null;
158
+ } finally {
159
+ rmSync(dir, { recursive: true, force: true });
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Polls until the run over the current tree completes. Never decides: the caller turns
165
+ * `timeout`/`no-run`/`unknown` into "no CI proof", never into red.
166
+ *
167
+ * @returns {Promise<{ state: 'completed'|'timeout'|'no-run'|'unknown', run?: object }>}
168
+ */
169
+ export async function waitForRun(root, { branch, workflow, isCurrent, timeoutMs, appearMs, pollMs = 15000,
170
+ sleep = (ms) => new Promise((r) => setTimeout(r, ms)), now = Date.now, onPending = () => {}, list = listRuns }) {
171
+ const start = now();
172
+ let seen = false;
173
+ let misses = 0;
174
+ for (;;) {
175
+ const runs = list(root, branch, workflow);
176
+ if (runs === null) {
177
+ if (++misses >= 3) return { state: 'unknown' };
178
+ } else {
179
+ misses = 0;
180
+ const run = pickRun(runs, isCurrent);
181
+ if (run?.status === 'completed') return { state: 'completed', run };
182
+ if (run) {
183
+ if (!seen) onPending(run);
184
+ seen = true;
185
+ }
186
+ }
187
+ const elapsed = now() - start;
188
+ if (!seen && elapsed >= appearMs) return { state: 'no-run' };
189
+ if (elapsed >= timeoutMs) return { state: 'timeout' };
190
+ await sleep(pollMs);
191
+ }
192
+ }
@@ -0,0 +1,122 @@
1
+ import { existsSync, readdirSync } from 'fs';
2
+ import { join, posix, relative, resolve } from 'path';
3
+ import { PACKAGE_LOCATIONS } from '../detectors/index.js';
4
+ import { CI_ARTIFACT, CI_RESULT_FILE } from './github-ci.js';
5
+
6
+ const LOCKFILES = [
7
+ ['package-lock.json', 'npm ci'],
8
+ ['pnpm-lock.yaml', 'corepack enable && pnpm install --frozen-lockfile'],
9
+ ['yarn.lock', 'corepack enable && yarn install --frozen-lockfile'],
10
+ ];
11
+ const PLAYWRIGHT_CONFIGS = ['playwright.config.ts', 'playwright.config.js', 'playwright.config.mjs'];
12
+
13
+ function packageDirs(root, config) {
14
+ const dirs = [...PACKAGE_LOCATIONS];
15
+ if (typeof config?.project?.frontendPath === 'string') dirs.push(config.project.frontendPath);
16
+ for (const node of Array.isArray(config?.project?.tests) ? config.project.tests : []) {
17
+ if (typeof node?.cwd === 'string') dirs.push(node.cwd);
18
+ }
19
+ const seen = new Set();
20
+ const out = [];
21
+ for (const dir of dirs) {
22
+ const abs = resolve(root, dir.trim() || '.');
23
+ const rel = relative(root, abs).split('\\').join('/') || '.';
24
+ if (rel.startsWith('..') || seen.has(rel)) continue;
25
+ seen.add(rel);
26
+ const lock = LOCKFILES.find(([file]) => existsSync(join(abs, file)));
27
+ if (lock) out.push({ dir: rel, install: lock[1], playwright: PLAYWRIGHT_CONFIGS.some((f) => existsSync(join(abs, f))) });
28
+ }
29
+ return out;
30
+ }
31
+
32
+ function usesDotnet(root, config) {
33
+ if (existsSync(join(root, 'global.json'))) return true;
34
+ if (/dotnet/i.test(String(config?.project?.stack ?? ''))) return true;
35
+ if ((config?.project?.tests ?? []).some?.((node) => node?.runner === 'dotnet' || /\bdotnet\b/.test(node?.command ?? ''))) return true;
36
+ try {
37
+ return readdirSync(root).some((f) => /\.(sln|slnx|csproj)$/i.test(f));
38
+ } catch {
39
+ return false;
40
+ }
41
+ }
42
+
43
+ function dotnetSetup(root, config) {
44
+ if (!usesDotnet(root, config)) return [];
45
+ const hasGlobalJson = existsSync(join(root, 'global.json'));
46
+ return [
47
+ ' - uses: actions/setup-dotnet@v4',
48
+ ' with:',
49
+ hasGlobalJson ? ' global-json-file: global.json' : " dotnet-version: '10.0.x'",
50
+ ];
51
+ }
52
+
53
+ const step = (name, dir, run) => [
54
+ ` - name: ${name}`,
55
+ ...(dir === '.' ? [] : [` working-directory: ${posix.normalize(dir)}`]),
56
+ ` run: ${run}`,
57
+ ];
58
+
59
+ /**
60
+ * The workflow `verify {f} --ci` relies on. Same judge as the local run: it calls the project's
61
+ * pinned morph-spec, so the suite is whatever `project.tests` says, not a second definition here.
62
+ * `.morph/` is out of the trigger (bookkeeping commits would cancel the run over the code) except
63
+ * the config, which names the commands.
64
+ *
65
+ * @param {string} root
66
+ * @param {object|null} config
67
+ * @returns {string}
68
+ */
69
+ export function renderCiWorkflow(root, config) {
70
+ const packages = packageDirs(root, config);
71
+ const lines = [
72
+ '# Gerado por morph-spec: a prova do Gate 3 roda aqui (verify {feature} --ci).',
73
+ 'name: morph-verify',
74
+ '',
75
+ 'on:',
76
+ ' push:',
77
+ " branches: ['morph/**']",
78
+ ' paths:',
79
+ " - '**'",
80
+ " - '!.morph/**'",
81
+ " - '.morph/config/config.json'",
82
+ ' workflow_dispatch:',
83
+ '',
84
+ 'concurrency:',
85
+ ' group: morph-verify-${{ github.ref }}',
86
+ ' cancel-in-progress: true',
87
+ '',
88
+ 'jobs:',
89
+ ' verify:',
90
+ ' runs-on: ubuntu-latest',
91
+ ' timeout-minutes: 45',
92
+ ' env:',
93
+ " MORPH_HEAVY: 'off'",
94
+ ' steps:',
95
+ ' - uses: actions/checkout@v4',
96
+ ' - uses: actions/setup-node@v4',
97
+ ' with:',
98
+ " node-version: '22'",
99
+ ...dotnetSetup(root, config),
100
+ ];
101
+ for (const pkg of packages) {
102
+ lines.push(...step(`install (${pkg.dir})`, pkg.dir, pkg.install));
103
+ if (pkg.playwright) lines.push(...step(`playwright browsers (${pkg.dir})`, pkg.dir, 'npx playwright install --with-deps chromium'));
104
+ }
105
+ lines.push(
106
+ ' - name: morph-spec verify',
107
+ ' shell: bash',
108
+ ' run: |',
109
+ ' feature="${GITHUB_REF_NAME#morph/}"',
110
+ " version=$(node -p \"try { require('./.morph/.morphversion').morphVersion } catch { 'latest' }\")",
111
+ ` npx -y "@polymorphism-tech/morph-spec@\${version}" verify "$feature" --json --no-persist > ${CI_RESULT_FILE}`,
112
+ ' - uses: actions/upload-artifact@v4',
113
+ ' if: always()',
114
+ ' with:',
115
+ ` name: ${CI_ARTIFACT}`,
116
+ ` path: ${CI_RESULT_FILE}`,
117
+ ' if-no-files-found: ignore',
118
+ ' retention-days: 7',
119
+ '',
120
+ );
121
+ return lines.join('\n');
122
+ }