@polymorphism-tech/morph-spec 10.7.0 → 10.8.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 (59) hide show
  1. package/README.md +2 -2
  2. package/bin/morph-spec.js +17 -1
  3. package/claude-plugin.json +1 -1
  4. package/docs/CHEATSHEET.md +1 -1
  5. package/docs/QUICKSTART.md +1 -1
  6. package/framework/MORPH.md +4 -2
  7. package/framework/agents.json +1 -1
  8. package/framework/docs/CLI.md +32 -17
  9. package/framework/hooks/README.md +2 -2
  10. package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +12 -4
  11. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +43 -4
  12. package/framework/hooks/claude-code/core/conventional-commit.js +1 -1
  13. package/framework/hooks/claude-code/core/protect-spec-files.js +8 -1
  14. package/framework/hooks/claude-code/core/worktree-context.js +5 -1
  15. package/framework/hooks/shared/command-tokens.js +1 -1
  16. package/framework/hooks/shared/e2e-check.js +16 -0
  17. package/framework/hooks/shared/heavy-command.js +3 -2
  18. package/framework/hooks/shared/state-reader.js +3 -0
  19. package/framework/hooks/statusline.js +3 -1
  20. package/framework/rules/implement-report.md +1 -1
  21. package/framework/rules/plan-grammar.md +3 -2
  22. package/framework/schemas/config.schema.json +6 -1
  23. package/framework/schemas/feature.schema.json +4 -2
  24. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  25. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  26. package/framework/skills/core/morph-apply/SKILL.md +8 -7
  27. package/framework/skills/core/morph-apply/references/hotfix.md +1 -1
  28. package/framework/skills/core/morph-apply/references/review.md +9 -3
  29. package/framework/skills/core/morph-plan/SKILL.md +3 -3
  30. package/framework/skills/core/morph-plan/references/critique.md +26 -18
  31. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  32. package/framework/templates/context/ARCHITECTURE.md +0 -3
  33. package/package.json +1 -1
  34. package/src/commands/graph/diff.js +9 -2
  35. package/src/commands/project/worktree.js +86 -7
  36. package/src/commands/state/approve.js +4 -7
  37. package/src/commands/state/create.js +3 -3
  38. package/src/commands/state/finish.js +75 -4
  39. package/src/commands/state/gate-check.js +7 -2
  40. package/src/commands/state/report-check.js +33 -6
  41. package/src/commands/validation/plan-check.js +11 -2
  42. package/src/commands/validation/prove-red.js +95 -0
  43. package/src/commands/validation/verify.js +56 -40
  44. package/src/lib/ci/ci-verify.js +1 -106
  45. package/src/lib/ci/github-ci.js +5 -117
  46. package/src/lib/generators/architecture-scanner.js +1 -14
  47. package/src/lib/git/default-branch.js +28 -1
  48. package/src/lib/plan/artifact-vocabulary.js +7 -3
  49. package/src/lib/plan/critique.js +17 -0
  50. package/src/lib/review-confirmation.js +10 -11
  51. package/src/lib/review-reports.js +12 -3
  52. package/src/lib/risk/diff-risk.js +116 -0
  53. package/src/lib/risk-detector.js +1 -1
  54. package/src/lib/validators/code-budget/code-budget-validator.js +16 -4
  55. package/src/lib/validators/verify-runner.js +24 -2
  56. package/src/lib/verify/affected.js +3 -1
  57. package/src/lib/verify/detached.js +3 -0
  58. package/src/utils/refusal-line.js +37 -0
  59. package/src/lib/ci/workflow-scaffold.js +0 -126
@@ -51,6 +51,9 @@ const removeRun = (paths) => {
51
51
  for (const file of Object.values(paths)) rmSync(file, { force: true });
52
52
  };
53
53
 
54
+ // launch and watchdog run without a console on win32: every git the view spawns there opens a window.
55
+ export const refreshesFeatureView = (env = process.env) => !['launch', 'watchdog'].includes(env[DETACH_STAGE_ENV]);
56
+
54
57
  export function matchesRun(record, { key, args }) {
55
58
  return record?.key === key && JSON.stringify(record.args) === JSON.stringify(args);
56
59
  }
@@ -0,0 +1,37 @@
1
+ // eslint-disable-next-line no-control-regex
2
+ const ANSI = /\x1b\[[0-9;]*m/g;
3
+
4
+ const clean = (text) => String(text).replace(ANSI, '').replace(/^[\s❌✗⚠️]+/u, '').split(/\r?\n/).find((l) => l.trim())?.trim() ?? '';
5
+
6
+ function reasonFromJson(text) {
7
+ try {
8
+ const obj = JSON.parse(text);
9
+ if (!obj || typeof obj !== 'object' || !obj.error) return null;
10
+ return obj.hint ? `${obj.error} — ${clean(obj.hint)}` : String(obj.error);
11
+ } catch {
12
+ return null;
13
+ }
14
+ }
15
+
16
+ /**
17
+ * On any non-zero exit, the last line on stderr names the command, the code and the reason: a `| tail`
18
+ * of long JSON or of a hint must never read as success. Stdout is untouched, so `--json` stays parseable.
19
+ */
20
+ export function installRefusalLine(command) {
21
+ let reason = null;
22
+ const log = console.log;
23
+ const error = console.error;
24
+ console.log = (...args) => {
25
+ if (args.length === 1 && typeof args[0] === 'string') reason = reasonFromJson(args[0]) ?? reason;
26
+ log(...args);
27
+ };
28
+ console.error = (...args) => {
29
+ const text = args.map(String).join(' ');
30
+ if (!reason && /❌/u.test(text)) reason = clean(text);
31
+ error(...args);
32
+ };
33
+ process.on('exit', (code) => {
34
+ if (!code) return;
35
+ process.stderr.write(`\n✗ morph-spec ${command} recusou (exit ${code}): ${reason || 'veja a saída acima'}\n`);
36
+ });
37
+ }
@@ -1,126 +0,0 @@
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
- import { discoverDotnetSolution } from '../tasks/test-runner.js';
6
-
7
- const LOCKFILES = [
8
- ['package-lock.json', 'npm ci'],
9
- ['pnpm-lock.yaml', 'corepack enable && pnpm install --frozen-lockfile'],
10
- ['yarn.lock', 'corepack enable && yarn install --frozen-lockfile'],
11
- ];
12
- const PLAYWRIGHT_CONFIGS = ['playwright.config.ts', 'playwright.config.js', 'playwright.config.mjs'];
13
-
14
- function packageDirs(root, config) {
15
- const dirs = [...PACKAGE_LOCATIONS];
16
- if (typeof config?.project?.frontendPath === 'string') dirs.push(config.project.frontendPath);
17
- for (const node of Array.isArray(config?.project?.tests) ? config.project.tests : []) {
18
- if (typeof node?.cwd === 'string') dirs.push(node.cwd);
19
- }
20
- const seen = new Set();
21
- const out = [];
22
- for (const dir of dirs) {
23
- const abs = resolve(root, dir.trim() || '.');
24
- const rel = relative(root, abs).split('\\').join('/') || '.';
25
- if (rel.startsWith('..') || seen.has(rel)) continue;
26
- seen.add(rel);
27
- const lock = LOCKFILES.find(([file]) => existsSync(join(abs, file)));
28
- if (lock) out.push({ dir: rel, install: lock[1], playwright: PLAYWRIGHT_CONFIGS.some((f) => existsSync(join(abs, f))) });
29
- }
30
- return out;
31
- }
32
-
33
- function usesDotnet(root, config) {
34
- if (existsSync(join(root, 'global.json'))) return true;
35
- if (/dotnet/i.test(String(config?.project?.stack ?? ''))) return true;
36
- if ((config?.project?.tests ?? []).some?.((node) => node?.runner === 'dotnet' || /\bdotnet\b/.test(node?.command ?? ''))) return true;
37
- try {
38
- return readdirSync(root).some((f) => /\.(sln|slnx|csproj)$/i.test(f));
39
- } catch {
40
- return false;
41
- }
42
- }
43
-
44
- function dotnetSetup(root, config) {
45
- if (!usesDotnet(root, config)) return [];
46
- const hasGlobalJson = existsSync(join(root, 'global.json'));
47
- return [
48
- ' - uses: actions/setup-dotnet@v4',
49
- ' with:',
50
- hasGlobalJson ? ' global-json-file: global.json' : " dotnet-version: '10.0.x'",
51
- ];
52
- }
53
-
54
- // The verify build node runs `dotnet build --no-restore` (right on a warm tree): a fresh checkout restores first,
55
- // against the same target the build node resolves.
56
- function dotnetRestore(root, config) {
57
- if (!usesDotnet(root, config)) return [];
58
- const backend = config?.project?.backendPath;
59
- if (typeof backend === 'string') return step('dotnet restore', backend, 'dotnet restore');
60
- const { target } = discoverDotnetSolution(root);
61
- return step('dotnet restore', '.', target && target !== '.' ? `dotnet restore "${target}"` : 'dotnet restore');
62
- }
63
-
64
- const step = (name, dir, run) => [
65
- ` - name: ${name}`,
66
- ...(dir === '.' ? [] : [` working-directory: ${posix.normalize(dir)}`]),
67
- ` run: ${run}`,
68
- ];
69
-
70
- // Calls the project's pinned morph-spec, so the suite is whatever `project.tests` says. `.morph/` is out of
71
- // the trigger (bookkeeping commits would cancel the run over the code) except the config.
72
- export function renderCiWorkflow(root, config) {
73
- const packages = packageDirs(root, config);
74
- const lines = [
75
- '# Gerado por morph-spec: a prova do Gate 3 roda aqui (verify {feature} --ci).',
76
- 'name: morph-verify',
77
- '',
78
- 'on:',
79
- ' push:',
80
- " branches: ['morph/**']",
81
- ' paths:',
82
- " - '**'",
83
- " - '!.morph/**'",
84
- " - '.morph/config/config.json'",
85
- ' workflow_dispatch:',
86
- '',
87
- 'concurrency:',
88
- ' group: morph-verify-${{ github.ref }}',
89
- ' cancel-in-progress: true',
90
- '',
91
- 'jobs:',
92
- ' verify:',
93
- ' runs-on: ubuntu-latest',
94
- ' timeout-minutes: 45',
95
- ' env:',
96
- " MORPH_HEAVY: 'off'",
97
- ' steps:',
98
- ' - uses: actions/checkout@v4',
99
- ' - uses: actions/setup-node@v4',
100
- ' with:',
101
- " node-version: '22'",
102
- ...dotnetSetup(root, config),
103
- ];
104
- for (const pkg of packages) {
105
- lines.push(...step(`install (${pkg.dir})`, pkg.dir, pkg.install));
106
- if (pkg.playwright) lines.push(...step(`playwright browsers (${pkg.dir})`, pkg.dir, 'npx playwright install --with-deps chromium'));
107
- }
108
- lines.push(...dotnetRestore(root, config));
109
- lines.push(
110
- ' - name: morph-spec verify',
111
- ' shell: bash',
112
- ' run: |',
113
- ' feature="${GITHUB_REF_NAME#morph/}"',
114
- " version=$(node -p \"try { require('./.morph/.morphversion').morphVersion } catch { 'latest' }\")",
115
- ` npx -y "@polymorphism-tech/morph-spec@\${version}" verify "$feature" --json --no-persist > ${CI_RESULT_FILE}`,
116
- ' - uses: actions/upload-artifact@v4',
117
- ' if: always()',
118
- ' with:',
119
- ` name: ${CI_ARTIFACT}`,
120
- ` path: ${CI_RESULT_FILE}`,
121
- ' if-no-files-found: ignore',
122
- ' retention-days: 7',
123
- '',
124
- );
125
- return lines.join('\n');
126
- }