@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.
- package/CLAUDE.md +11 -9
- package/README.md +10 -10
- package/bin/morph-spec.js +11 -13
- package/bin/validate.js +379 -379
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +2 -4
- package/docs/QUICKSTART.md +6 -6
- package/framework/CLAUDE.md +6 -8
- package/framework/MORPH.md +39 -45
- package/framework/agents.json +8 -9
- package/framework/commands/morph-apply.md +1 -3
- package/framework/commands/morph-hotfix.md +1 -1
- package/framework/commands/morph-proposal.md +2 -2
- package/framework/docs/CLI.md +15 -14
- package/framework/evals/review-checklist.md +90 -0
- package/framework/evals/trace-schema.md +2 -12
- package/framework/hooks/README.md +2 -3
- package/framework/hooks/claude-code/core/heavy-gate.js +1 -1
- package/framework/hooks/claude-code/core/protect-spec-files.js +2 -2
- package/framework/hooks/claude-code/core/telemetry-log.js +2 -2
- package/framework/hooks/claude-code/core/trace-autogen.js +1 -22
- package/framework/hooks/claude-code/core/validate-completion.js +3 -56
- package/framework/hooks/claude-code/statusline.sh +11 -11
- package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
- package/framework/hooks/git/pre-commit/agents.sh +25 -25
- package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
- package/framework/hooks/git/pre-commit/specs.sh +50 -50
- package/framework/hooks/git/pre-push/run-tests.sh +44 -44
- package/framework/hooks/shared/build-check.js +1 -1
- package/framework/hooks/shared/comment-scanner.js +1 -1
- package/framework/hooks/shared/phase-utils.js +0 -5
- package/framework/hooks/shared/stdin-reader.js +26 -26
- package/framework/phases.json +4 -6
- package/framework/rubrics/backend.json +1 -1
- package/framework/rubrics/default.json +1 -1
- package/framework/rubrics/frontend.json +1 -1
- package/framework/rules/morph-workflow.md +6 -6
- package/framework/schemas/config.schema.json +4 -0
- package/framework/schemas/feature.schema.json +5 -29
- package/framework/schemas/tasks.schema.json +6 -2
- package/framework/skills/README.md +1 -2
- 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 +145 -246
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +5 -3
- package/framework/skills/core/morph-implement/SKILL.md +101 -201
- package/framework/skills/core/morph-machine/SKILL.md +3 -3
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +171 -287
- package/framework/skills/core/morph-proposal/SKILL.md +4 -5
- package/framework/skills/core/morph-review/SKILL.md +122 -430
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +2 -2
- package/framework/skills/core/morph-uiux/SKILL.md +2 -3
- package/framework/standards/STANDARDS.json +10 -10
- package/framework/standards/ai-agents/agent-archetypes.md +1 -1
- package/framework/standards/ai-agents/agent-session.md +1 -1
- package/framework/standards/ai-agents/agent-spec.md +1 -1
- package/framework/standards/ai-agents/batching.md +1 -1
- package/framework/standards/ai-agents/context-providers.md +1 -1
- package/framework/standards/ai-agents/conversational-agent-with-phases.md +1 -1
- package/framework/standards/ai-agents/cost-and-budget.md +1 -1
- package/framework/standards/ai-agents/durable-workflows-hitl.md +1 -1
- package/framework/standards/ai-agents/guardrails.md +1 -1
- package/framework/standards/ai-agents/llm-runtime-defaults.md +1 -1
- package/framework/standards/ai-agents/mcp-server.md +1 -1
- package/framework/standards/ai-agents/mcp-tools.md +1 -1
- package/framework/standards/ai-agents/media-pipeline.md +1 -1
- package/framework/standards/ai-agents/media-video.md +1 -1
- package/framework/standards/ai-agents/middleware-patterns.md +1 -1
- package/framework/standards/ai-agents/modalities-image-gen.md +1 -1
- package/framework/standards/ai-agents/modalities-speech-to-text.md +1 -1
- package/framework/standards/ai-agents/modalities-text-to-speech.md +1 -1
- package/framework/standards/ai-agents/modalities-vision.md +1 -1
- package/framework/standards/ai-agents/multi-agent-patterns.md +1 -1
- package/framework/standards/ai-agents/observability-patterns.md +3 -3
- package/framework/standards/ai-agents/production.md +2 -2
- package/framework/standards/ai-agents/prompt-sources.md +1 -1
- package/framework/standards/ai-agents/providers/model-registry.md +1 -1
- package/framework/standards/ai-agents/rag-custom-pgvector.md +1 -1
- package/framework/standards/ai-agents/service-tier-flex.md +1 -1
- package/framework/standards/ai-agents/setup.md +1 -1
- package/framework/standards/ai-agents/structured-output.md +1 -1
- package/framework/standards/ai-agents/sweet-spot.md +1 -1
- package/framework/standards/ai-agents/testing-ai.md +1 -1
- package/framework/standards/ai-agents/tool-adapters.md +1 -1
- package/framework/standards/ai-agents/vector-data-extensions.md +1 -1
- package/framework/standards/ai-agents/workflows.md +1 -1
- package/framework/templates/CLAUDE.md.core.template +1 -1
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/README.md +1 -1
- package/framework/templates/REGISTRY.json +0 -81
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
- package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/docs/spec.md +19 -205
- package/framework/templates/docs/ui-components.md +52 -52
- package/framework/templates/docs/ui-design-system.md +76 -76
- package/framework/templates/docs/ui-flows.md +167 -167
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/github/README.md +56 -56
- package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
- package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
- package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
- package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
- package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
- package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
- package/framework/templates/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- package/package.json +2 -2
- package/src/commands/e2e/e2e.js +1 -1
- package/src/commands/project/heavy.js +1 -1
- package/src/commands/project/status.js +3 -3
- package/src/commands/state/approve.js +7 -49
- package/src/commands/state/gate-check.js +16 -106
- package/src/commands/state/gate-decision.js +1 -1
- package/src/commands/state/report-check.js +42 -17
- package/src/commands/validation/mutate.js +1 -1
- package/src/commands/validation/scout-check.js +28 -0
- package/src/commands/validation/validate-feature.js +5 -32
- package/src/commands/validation/verify.js +98 -48
- package/src/core/paths/output-schema.js +0 -2
- package/src/core/state/state-manager.js +1 -2
- package/src/core/templates/template-renderer.js +477 -477
- package/src/dashboard/app/lib/format.js +5 -0
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/FeatureDetail.jsx +16 -22
- package/src/dashboard/app/pages/Features.jsx +7 -7
- package/src/dashboard/app/pages/PhasePipeline.jsx +1 -1
- package/src/dashboard/app/pages/Rubrics.jsx +7 -11
- package/src/dashboard/scanner/docs-scanner.js +1 -2
- package/src/dashboard/scanner/features-scanner.js +3 -4
- package/src/dashboard/scanner/rubrics-scanner.js +12 -12
- package/src/dashboard/vite.config.js +24 -24
- package/src/lib/agents/output-collector.js +0 -1
- package/src/lib/ci/ci-verify.js +118 -0
- package/src/lib/ci/github-ci.js +192 -0
- package/src/lib/ci/workflow-scaffold.js +122 -0
- package/src/lib/detectors/design-system-detector.js +188 -188
- package/src/lib/evaluator-report.js +52 -334
- package/src/lib/generators/design-system-generator.js +508 -508
- package/src/lib/generators/handoff-generator.js +4 -5
- package/src/lib/installers/memory-installer.js +2 -2
- package/src/lib/mutation/manifest.js +8 -2
- package/src/lib/mutation/report.js +5 -3
- package/src/lib/mutation/runner.js +9 -7
- package/src/lib/risk-detector.js +60 -122
- package/src/lib/scout/scout-check.js +293 -0
- package/src/lib/tasks/task-classifier.js +30 -30
- package/src/lib/tasks/task-parser.js +209 -209
- package/src/lib/tasks/tasks-json-parser.js +7 -5
- package/src/lib/tasks/test-nodes.js +1 -1
- package/src/lib/tasks/test-runner.js +1 -1
- package/src/lib/telemetry/verify-event.js +3 -2
- package/src/lib/validators/css/css-validator.js +340 -340
- package/src/lib/validators/reuse/reuse-validator.js +6 -8
- package/src/lib/validators/verify-runner.js +1 -1
- package/src/scripts/setup-infra.js +1 -1
- package/src/utils/agent-id-mapper.js +11 -11
- package/src/utils/agents-installer.js +5 -9
- package/src/utils/claude-settings-manager.js +300 -300
- package/src/utils/hooks-installer.js +3 -10
- package/src/utils/skills-installer.js +1 -1
- package/framework/evals/proof-rubrics/README.md +0 -76
- package/framework/evals/proof-rubrics/eval-de-modelo.md +0 -80
- package/framework/evals/proof-rubrics/fiacao.md +0 -54
- package/framework/evals/proof-rubrics/guard-cego.md +0 -52
- package/framework/evals/proof-rubrics/media-claims.md +0 -109
- package/framework/evals/proof-rubrics/mutation-proof.md +0 -107
- package/framework/evals/proof-rubrics/observabilidade-genai.md +0 -119
- package/framework/evals/proof-rubrics/verde-nao-e-coberto.md +0 -48
- package/framework/evals/rubrics/README.md +0 -45
- package/framework/evals/rubrics/architecture.md +0 -103
- package/framework/evals/rubrics/code-quality.md +0 -98
- package/framework/evals/rubrics/condensed.md +0 -57
- package/framework/evals/rubrics/contracts.md +0 -67
- package/framework/evals/rubrics/test-coverage.md +0 -99
- package/framework/evals/scenarios/dotnet-crud.yaml +0 -42
- package/framework/evals/scenarios/nextjs-auth.yaml +0 -38
- package/framework/evals/suites/standard.yaml +0 -5
- package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
- package/framework/hooks/claude-code/core/post-edit-typecheck.js +0 -127
- package/framework/skills/core/morph-eval/SKILL.md +0 -64
- package/framework/templates/.idea/morph-templates.xml +0 -92
- package/framework/templates/.vscode/morph-templates.code-snippets +0 -186
- package/framework/templates/docs/clarifications.md +0 -253
- package/framework/templates/docs/onboarding.md +0 -122
- package/framework/templates/docs/schema-analysis.md +0 -119
- package/framework/templates/docs/user-stories.md +0 -34
- 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
|
+
}
|