@polymorphism-tech/morph-spec 9.3.0 → 9.5.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 (107) hide show
  1. package/CLAUDE.md +1 -1
  2. package/README.md +2 -2
  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 +23 -33
  7. package/framework/agents.json +10 -9
  8. package/framework/docs/CLI.md +3 -3
  9. package/framework/evals/review-checklist.md +1 -1
  10. package/framework/hooks/README.md +3 -2
  11. package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
  12. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -15
  13. package/framework/hooks/claude-code/core/comment-guard.js +1 -0
  14. package/framework/hooks/claude-code/core/conventional-commit.js +57 -0
  15. package/framework/hooks/claude-code/core/denylist.json +63 -1
  16. package/framework/hooks/claude-code/core/enforce-phase-writes.js +0 -8
  17. package/framework/hooks/claude-code/core/gate-guard.js +0 -8
  18. package/framework/hooks/claude-code/core/loop-detect.js +0 -3
  19. package/framework/hooks/claude-code/core/pre-bash-denylist.js +2 -1
  20. package/framework/hooks/claude-code/core/protect-spec-files.js +0 -7
  21. package/framework/hooks/claude-code/core/validate-completion.js +0 -4
  22. package/framework/hooks/claude-code/core/worktree-context.js +0 -2
  23. package/framework/hooks/claude-code/statusline.py +3 -44
  24. package/framework/hooks/shared/build-check.js +12 -19
  25. package/framework/hooks/shared/command-tokens.js +17 -0
  26. package/framework/hooks/shared/comment-rules.js +11 -1
  27. package/framework/hooks/shared/hook-response.js +0 -4
  28. package/framework/hooks/shared/payload-utils.js +1 -35
  29. package/framework/hooks/shared/phase-utils.js +0 -12
  30. package/framework/hooks/shared/slice-progress.js +0 -2
  31. package/framework/hooks/shared/state-reader.js +0 -19
  32. package/framework/hooks/shared/telemetry-logger.js +1 -1
  33. package/framework/rules/csharp-standards.md +5 -3
  34. package/framework/rules/dotnet-templates.md +2 -2
  35. package/framework/rules/frontend-standards.md +1 -2
  36. package/framework/rules/nextjs-standards.md +10 -8
  37. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  38. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  39. package/framework/skills/core/morph-apply/SKILL.md +1 -1
  40. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  41. package/framework/skills/core/morph-machine/SKILL.md +2 -2
  42. package/framework/skills/core/morph-media/SKILL.md +1 -1
  43. package/framework/skills/core/morph-plan/SKILL.md +3 -3
  44. package/framework/skills/core/morph-review/SKILL.md +1 -1
  45. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  46. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  47. package/framework/standards/STANDARDS.json +87 -37
  48. package/framework/standards/STANDARDS.meta.json +41 -15
  49. package/framework/standards/architecture/code-comments.md +1 -1
  50. package/framework/standards/architecture/engineering-lens.md +100 -0
  51. package/framework/standards/architecture/vertical-slice/vertical-slice.md +7 -9
  52. package/framework/standards/backend/dotnet/analyzers.md +59 -0
  53. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -12
  54. package/framework/standards/frontend/admin-app/data-table.md +1 -3
  55. package/framework/standards/frontend/admin-app/page-structure.md +8 -4
  56. package/framework/standards/frontend/nextjs/app-router.md +3 -4
  57. package/framework/standards/frontend/nextjs/components.md +2 -15
  58. package/framework/standards/frontend/nextjs/data-fetching.md +2 -3
  59. package/framework/standards/frontend/nextjs/lint.md +141 -0
  60. package/framework/standards/frontend/nextjs/motion-patterns.md +2 -7
  61. package/framework/standards/frontend/nextjs/naming-conventions.md +1 -2
  62. package/framework/standards/frontend/nextjs/nextjs-patterns.md +5 -7
  63. package/framework/standards/frontend/nextjs/project-structure.md +3 -2
  64. package/framework/standards/frontend/nextjs/state-management.md +6 -7
  65. package/framework/templates/README.md +4 -5
  66. package/framework/templates/REGISTRY.json +21 -516
  67. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +54 -56
  68. package/package.json +3 -2
  69. package/src/commands/project/worktree.js +1 -1
  70. package/src/commands/state/archive.js +1 -1
  71. package/src/commands/state/finish.js +1 -1
  72. package/src/core/paths/output-schema.js +0 -9
  73. package/src/core/state/feature-identity.js +0 -2
  74. package/src/core/state/state-manager.js +1 -169
  75. package/src/core/templates/template-registry.js +1 -43
  76. package/src/core/templates/template-renderer.js +0 -23
  77. package/src/dashboard/app/lib/format.js +0 -7
  78. package/src/dashboard/app/lib/phases.js +0 -7
  79. package/src/dashboard/scanner/activity-scanner.js +0 -4
  80. package/src/lib/ai/ai-project-scan.js +1 -6
  81. package/src/lib/graph/graphify-runtime.js +0 -1
  82. package/src/lib/mutation/snapshot.js +1 -5
  83. package/src/lib/risk-detector.js +0 -5
  84. package/src/lib/stack/stack-profile.js +0 -4
  85. package/src/lib/stack/stack-scan.js +18 -9
  86. package/src/lib/tasks/eval-node.js +1 -7
  87. package/src/lib/validators/validation-runner.js +0 -43
  88. package/src/lib/validators/verify-runner.js +18 -2
  89. package/src/utils/agents-installer.js +12 -19
  90. package/src/utils/claude-settings-manager.js +0 -28
  91. package/src/utils/file-copier.js +0 -53
  92. package/src/utils/hooks-installer.js +1 -1
  93. package/framework/hooks/shared/activity-logger.js +0 -131
  94. package/framework/standards/frontend/design-system/naming.md +0 -68
  95. package/framework/templates/meta-prompts/fusion/fusion-agent.md +0 -76
  96. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +0 -100
  97. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +0 -90
  98. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +0 -84
  99. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +0 -44
  100. package/framework/templates/meta-prompts/validators/checkpoint-validator.md +0 -107
  101. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +0 -94
  102. package/src/commands/validation/index.js +0 -2
  103. package/src/lib/validators/code-budget/index.js +0 -2
  104. package/src/lib/validators/contracts/index.js +0 -1
  105. package/src/lib/validators/nextjs/index.js +0 -1
  106. package/src/lib/validators/packages/index.js +0 -1
  107. package/src/lib/validators/reuse/index.js +0 -2
@@ -1,56 +1,54 @@
1
- // features/{{kebabCase featureName}}/components/create-{{kebabCase featureName}}-form.tsx
2
- 'use client';
3
-
4
- import { useForm } from 'react-hook-form';
5
- import { zodResolver } from '@hookform/resolvers/zod';
6
- import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
7
- import { Input } from '@/components/ui/input';
8
- import { Button } from '@/components/ui/button';
9
- import { useCreate{{pascalCase featureName}} } from '@/features/{{kebabCase featureName}}/hooks/use-create-{{kebabCase featureName}}';
10
- import { create{{pascalCase featureName}}Schema, type Create{{pascalCase featureName}}Input } from '@/features/{{kebabCase featureName}}/types/{{kebabCase featureName}}.schemas';
11
-
12
- interface Create{{pascalCase featureName}}FormProps {
13
- onSuccess?: () => void;
14
- }
15
-
16
- export function Create{{pascalCase featureName}}Form({ onSuccess }: Create{{pascalCase featureName}}FormProps) {
17
- const form = useForm<Create{{pascalCase featureName}}Input>({
18
- resolver: zodResolver(create{{pascalCase featureName}}Schema),
19
- defaultValues: { name: '' },
20
- });
21
-
22
- const { mutate: create{{pascalCase featureName}}, isPending } = useCreate{{pascalCase featureName}}();
23
-
24
- function onSubmit(values: Create{{pascalCase featureName}}Input) {
25
- create{{pascalCase featureName}}(values, {
26
- onSuccess: () => { form.reset(); onSuccess?.(); },
27
- onError: (error) => { form.setError('root', { message: error.message }); },
28
- });
29
- }
30
-
31
- return (
32
- <Form {...form}>
33
- <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
34
- <FormField
35
- control={form.control}
36
- name="name"
37
- render={({ field }) => (
38
- <FormItem>
39
- <FormLabel>Name</FormLabel>
40
- <FormControl>
41
- <Input placeholder="Enter name" {...field} />
42
- </FormControl>
43
- <FormMessage />
44
- </FormItem>
45
- )}
46
- />
47
- {form.formState.errors.root && (
48
- <p className="text-sm text-destructive">{form.formState.errors.root.message}</p>
49
- )}
50
- <Button type="submit" disabled={isPending}>
51
- {isPending ? 'Creating...' : 'Create {{pascalCase featureName}}'}
52
- </Button>
53
- </form>
54
- </Form>
55
- );
56
- }
1
+ // features/{{kebabCase featureName}}/components/create-{{kebabCase featureName}}-form.tsx
2
+ 'use client';
3
+
4
+ import { Controller, useForm } from 'react-hook-form';
5
+ import { zodResolver } from '@hookform/resolvers/zod';
6
+ import { Field, FieldError, FieldGroup, FieldLabel } from '@/components/ui/field';
7
+ import { Input } from '@/components/ui/input';
8
+ import { Button } from '@/components/ui/button';
9
+ import { useCreate{{pascalCase featureName}} } from '@/features/{{kebabCase featureName}}/hooks/use-create-{{kebabCase featureName}}';
10
+ import { create{{pascalCase featureName}}Schema, type Create{{pascalCase featureName}}Input } from '@/features/{{kebabCase featureName}}/types/{{kebabCase featureName}}.schemas';
11
+
12
+ interface Create{{pascalCase featureName}}FormProps {
13
+ onSuccess?: () => void;
14
+ }
15
+
16
+ export function Create{{pascalCase featureName}}Form({ onSuccess }: Create{{pascalCase featureName}}FormProps) {
17
+ const form = useForm<Create{{pascalCase featureName}}Input>({
18
+ resolver: zodResolver(create{{pascalCase featureName}}Schema),
19
+ defaultValues: { name: '' },
20
+ });
21
+
22
+ const { mutate: create{{pascalCase featureName}}, isPending } = useCreate{{pascalCase featureName}}();
23
+
24
+ function onSubmit(values: Create{{pascalCase featureName}}Input) {
25
+ create{{pascalCase featureName}}(values, {
26
+ onSuccess: () => { form.reset(); onSuccess?.(); },
27
+ onError: (error) => { form.setError('root', { message: error.message }); },
28
+ });
29
+ }
30
+
31
+ return (
32
+ <form onSubmit={form.handleSubmit(onSubmit)}>
33
+ <FieldGroup>
34
+ <Controller
35
+ name="name"
36
+ control={form.control}
37
+ render={({ field, fieldState }) => (
38
+ <Field data-invalid={fieldState.invalid}>
39
+ <FieldLabel htmlFor={field.name}>Name</FieldLabel>
40
+ <Input {...field} id={field.name} aria-invalid={fieldState.invalid} placeholder="Enter name" />
41
+ {fieldState.invalid && <FieldError errors={[fieldState.error]} />}
42
+ </Field>
43
+ )}
44
+ />
45
+ {form.formState.errors.root && (
46
+ <p className="text-sm text-destructive">{form.formState.errors.root.message}</p>
47
+ )}
48
+ <Button type="submit" disabled={isPending}>
49
+ {isPending ? 'Creating...' : 'Create {{pascalCase featureName}}'}
50
+ </Button>
51
+ </FieldGroup>
52
+ </form>
53
+ );
54
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polymorphism-tech/morph-spec",
3
- "version": "9.3.0",
3
+ "version": "9.5.0",
4
4
  "description": "MORPH-SPEC: NLH (Natural Language Harness) for spec-driven development with Claude Code",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -56,7 +56,8 @@
56
56
  "docs:serve": "npx http-server docs/api -p 8080 -o",
57
57
  "release": "node scripts/release.js",
58
58
  "prepublishOnly": "node scripts/prepublish-check.js",
59
- "postinstall": "node src/scripts/global-install.js"
59
+ "postinstall": "node src/scripts/global-install.js",
60
+ "publish:web": "node scripts/publish-web.mjs"
60
61
  },
61
62
  "dependencies": {
62
63
  "chalk": "^5.3.0",
@@ -743,7 +743,7 @@ export async function clearRootWorktreeRegistration(root, feature, { stamp } = {
743
743
  let unchanged = true;
744
744
  try { execFileSync('git', ['diff', '--quiet', 'HEAD', '--', featPathspec], gitOpts); } catch { unchanged = false; }
745
745
  if (!unchanged) {
746
- execFileSync('git', ['commit', '-m', `morph-spec finish: clear ${feature} worktree registration`, '--', featPathspec], gitOpts);
746
+ execFileSync('git', ['commit', '-m', `chore(morph): clear ${feature} worktree registration`, '--', featPathspec], gitOpts);
747
747
  }
748
748
  } catch { /* fail-open — a dirty root is a nuisance, not a reason to fail */ }
749
749
  return { shedBackup: null };
@@ -123,7 +123,7 @@ export function runArchive(feature, opts = {}) {
123
123
  ...(adrIndex.changed ? [ADR_INDEX_PATHSPEC] : []),
124
124
  ];
125
125
  try {
126
- execFileSync('git', ['commit', '-m', `morph-spec archive: ${feature}`, '--', ...pathspecs], { cwd, stdio: 'pipe' });
126
+ execFileSync('git', ['commit', '-m', `chore(morph): archive ${feature}`, '--', ...pathspecs], { cwd, stdio: 'pipe' });
127
127
  committed = true;
128
128
  } catch { /* nothing to commit / no identity — not fatal, the move + stage stand */ }
129
129
  }
@@ -494,7 +494,7 @@ export function finishPreflight({ feature, root, worktreePath, cwd, mode, branch
494
494
  }
495
495
 
496
496
  async function integrateMerge({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, arch, backupStamp }) {
497
- const mergeMsg = `morph-spec finish: merge ${expectedBranch}`;
497
+ const mergeMsg = `chore(morph): merge ${expectedBranch}`;
498
498
  const adrNote = adrIndexNote(arch);
499
499
 
500
500
  if (isWorktreeFlow) {
@@ -43,15 +43,6 @@ export function getFeatureDir(featureName) {
43
43
  return join('.morph', 'features', featureName).replace(/\\/g, '/');
44
44
  }
45
45
 
46
- export function getTaskReportPath(featureName, taskId) {
47
- return join('.morph', 'features', featureName, '3-implement', 'reports', `${taskId}.md`)
48
- .replace(/\\/g, '/');
49
- }
50
-
51
- export function getAbsoluteTaskReportPath(projectRoot, featureName, taskId) {
52
- return join(projectRoot, '.morph', 'features', featureName, '3-implement', 'reports', `${taskId}.md`);
53
- }
54
-
55
46
  /** `multi` types contribute their directory, so `created` means "the folder exists". */
56
47
  export function getAllOutputPaths(featureName) {
57
48
  const outputs = {};
@@ -1,5 +1,3 @@
1
- export const IDENTITY_FIELDS = ['name', 'description', 'workType', 'createdAt'];
2
-
3
1
  const blank = (v) => v === undefined || v === null || String(v).trim() === '';
4
2
 
5
3
  // `workType` is excluded on purpose: absent, it defaults to `feature`.
@@ -1,4 +1,4 @@
1
- import { readFileSync, writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync, rmdirSync, statSync, readdirSync } from 'fs';
1
+ import { readFileSync, writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync, readdirSync } from 'fs';
2
2
  import { join, dirname } from 'path';
3
3
  import { getAllOutputPaths, getOutputPath } from '../paths/output-schema.js';
4
4
  import { derivePhaseFrom, diskArtifactProbe } from '../../../framework/hooks/shared/state-reader.js';
@@ -248,41 +248,6 @@ export function saveState(state) {
248
248
  });
249
249
  }
250
250
 
251
- const LOCK_DIR_NAME = '.morph/state.json.lock';
252
- const LOCK_MAX_RETRIES = 5;
253
- const LOCK_BASE_MS = 100;
254
- const LOCK_STALE_MS = 30_000;
255
-
256
- export async function withStateLock(fn) {
257
- const lockPath = join(process.cwd(), LOCK_DIR_NAME);
258
-
259
- for (let attempt = 0; attempt <= LOCK_MAX_RETRIES; attempt++) {
260
- try {
261
- mkdirSync(lockPath);
262
- break;
263
- } catch (err) {
264
- if (err.code !== 'EEXIST') throw err;
265
- try {
266
- const lockStat = statSync(lockPath);
267
- if (Date.now() - lockStat.mtimeMs > LOCK_STALE_MS) {
268
- try { rmdirSync(lockPath); } catch { }
269
- continue;
270
- }
271
- } catch { }
272
- if (attempt === LOCK_MAX_RETRIES) {
273
- throw new Error('Could not acquire state lock after retries');
274
- }
275
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, LOCK_BASE_MS * (2 ** attempt));
276
- }
277
- }
278
-
279
- try {
280
- return await fn();
281
- } finally {
282
- try { rmdirSync(lockPath); } catch { }
283
- }
284
- }
285
-
286
251
  export function initState(options = {}) {
287
252
  const { force = false, projectName = '{PROJECT_NAME}', projectType = 'dotnet-nextjs' } = options;
288
253
 
@@ -402,29 +367,6 @@ export async function updateFeature(featureName, key, value) {
402
367
  saveState(state);
403
368
  }
404
369
 
405
- export async function updateFeatureMultiple(featureName, updates) {
406
- await ensureFeature(featureName);
407
- const state = loadState(); // Load AFTER ensuring feature exists
408
-
409
- for (const [key, value] of Object.entries(updates)) {
410
- const keys = key.split('.');
411
- let target = state.features[featureName];
412
-
413
- for (let i = 0; i < keys.length - 1; i++) {
414
- if (!target[keys[i]]) {
415
- target[keys[i]] = {};
416
- }
417
- target = target[keys[i]];
418
- }
419
-
420
- const finalKey = keys[keys.length - 1];
421
- target[finalKey] = value;
422
- }
423
-
424
- state.features[featureName].updatedAt = new Date().toISOString();
425
- saveState(state);
426
- }
427
-
428
370
  function normalizeOutputType(type) {
429
371
  if (!type.includes('-')) {
430
372
  return type;
@@ -544,91 +486,6 @@ export async function markOutput(featureName, outputType) {
544
486
  saveState(state);
545
487
  }
546
488
 
547
- export async function trackFileChange(featureName, filePath, action, phase) {
548
- await ensureFeature(featureName);
549
- const state = loadState();
550
-
551
- if (!Array.isArray(state.features[featureName].fileChanges)) {
552
- state.features[featureName].fileChanges = [];
553
- }
554
-
555
- state.features[featureName].fileChanges.push({
556
- path: filePath,
557
- action,
558
- phase: phase || state.features[featureName].phase || 'plan',
559
- timestamp: new Date().toISOString()
560
- });
561
-
562
- state.features[featureName].updatedAt = new Date().toISOString();
563
- saveState(state);
564
- }
565
-
566
- export function getFileChanges(featureName, options = {}) {
567
- const state = loadState();
568
- const feature = state.features[featureName];
569
-
570
- if (!feature) {
571
- throw new Error(`Feature '${featureName}' not found.`);
572
- }
573
-
574
- const changes = feature.fileChanges || [];
575
-
576
- if (options.groupByPhase) {
577
- const grouped = {};
578
- for (const change of changes) {
579
- const phase = change.phase || 'unknown';
580
- if (!grouped[phase]) grouped[phase] = [];
581
- grouped[phase].push(change);
582
- }
583
- return grouped;
584
- }
585
-
586
- return changes;
587
- }
588
-
589
- export async function skipPhase(featureName, phase, reason = '') {
590
- await ensureFeature(featureName);
591
- const state = loadState();
592
-
593
- if (!state.features[featureName].skippedPhases) {
594
- state.features[featureName].skippedPhases = [];
595
- }
596
-
597
- const existing = state.features[featureName].skippedPhases.find(s => s.phase === phase);
598
- if (!existing) {
599
- state.features[featureName].skippedPhases.push({
600
- phase,
601
- reason,
602
- timestamp: new Date().toISOString()
603
- });
604
- }
605
-
606
- state.features[featureName].updatedAt = new Date().toISOString();
607
- saveState(state);
608
- }
609
-
610
- export function getSkippedPhases(featureName) {
611
- const state = loadState();
612
- if (!state.features[featureName]) {
613
- return [];
614
- }
615
- return state.features[featureName].skippedPhases || [];
616
- }
617
-
618
- export function listFeatures() {
619
- const state = loadState();
620
- return Object.entries(state.features);
621
- }
622
-
623
- export function getSummary() {
624
- const state = loadState();
625
- return {
626
- project: state.project,
627
- metadata: state.metadata,
628
- featuresCount: Object.keys(state.features).length
629
- };
630
- }
631
-
632
489
  export async function setApprovalGate(featureName, gate, approved, metadata = {}) {
633
490
  const state = loadState();
634
491
  const feature = await ensureFeature(featureName);
@@ -666,31 +523,6 @@ export function getApprovalGate(featureName, gate) {
666
523
  return feature.approvalGates[gate] || null;
667
524
  }
668
525
 
669
- export function isPendingApproval(featureName) {
670
- const state = loadState();
671
- const feature = state.features[featureName];
672
-
673
- if (!feature || !feature.approvalGates) {
674
- return false;
675
- }
676
-
677
- const currentPhase = feature.phase || 'plan';
678
-
679
- const phaseGateMap = {
680
- 'plan': 'plan',
681
- 'review': 'review',
682
- 'uiux': 'uiux'
683
- };
684
-
685
- const relevantGate = phaseGateMap[currentPhase];
686
- if (!relevantGate) {
687
- return false;
688
- }
689
-
690
- const gate = feature.approvalGates[relevantGate];
691
- return gate && !gate.approved;
692
- }
693
-
694
526
  export function getApprovalHistory(featureName) {
695
527
  const state = loadState();
696
528
  const feature = state.features[featureName];
@@ -36,7 +36,7 @@ export function clearRegistryCache() {
36
36
  _registryCache.clear();
37
37
  }
38
38
 
39
- export function getAllTemplates(projectPath = null) {
39
+ function getAllTemplates(projectPath = null) {
40
40
  const registry = loadTemplateRegistry(projectPath);
41
41
  return registry.templates || [];
42
42
  }
@@ -46,48 +46,6 @@ export function getTemplateById(templateId, projectPath = null) {
46
46
  return templates.find(t => t.id === templateId) || null;
47
47
  }
48
48
 
49
- export function getTemplatesByCategory(category, projectPath = null) {
50
- const templates = getAllTemplates(projectPath);
51
- return templates.filter(t => t.category === category);
52
- }
53
-
54
- export function getTemplatesByStack(stackId, projectPath = null) {
55
- const templates = getAllTemplates(projectPath);
56
- return templates.filter(t => {
57
- if (!t.stackSpecific) return true;
58
-
59
- return t.stacks && t.stacks.includes(stackId);
60
- });
61
- }
62
-
63
- export function getRequiredTemplates(stackId = null, projectPath = null) {
64
- let templates = getAllTemplates(projectPath);
65
-
66
- templates = templates.filter(t => t.required === true);
67
-
68
- if (stackId) {
69
- templates = templates.filter(t => {
70
- if (!t.stackSpecific) return true;
71
- return t.stacks && t.stacks.includes(stackId);
72
- });
73
- }
74
-
75
- return templates;
76
- }
77
-
78
- export function searchTemplates(keyword, projectPath = null) {
79
- const templates = getAllTemplates(projectPath);
80
- const lowerKeyword = keyword.toLowerCase();
81
-
82
- return templates.filter(t => {
83
- return (
84
- t.id.toLowerCase().includes(lowerKeyword) ||
85
- t.name.toLowerCase().includes(lowerKeyword) ||
86
- (t.description && t.description.toLowerCase().includes(lowerKeyword))
87
- );
88
- });
89
- }
90
-
91
49
  const FRAMEWORK_TEMPLATES_DIR = join(__dirname, '../../../framework/templates');
92
50
 
93
51
  /** Path inside this package's `framework/templates/`, not a project's `.morph/`; throws when missing. */
@@ -340,15 +340,6 @@ export function renderTemplateString(templateString, variables, options = {}) {
340
340
  }
341
341
  }
342
342
 
343
- export function getTemplatePlaceholders(templatePath) {
344
- if (!existsSync(templatePath)) {
345
- throw new Error(`Template file not found: ${templatePath}`);
346
- }
347
-
348
- const templateContent = readFileSync(templatePath, 'utf-8');
349
- return getTemplatePlaceholdersFromString(templateContent);
350
- }
351
-
352
343
  export function getTemplatePlaceholdersFromString(templateString) {
353
344
  const placeholders = new Set();
354
345
 
@@ -380,18 +371,4 @@ export function getTemplatePlaceholdersFromString(templateString) {
380
371
  return Array.from(placeholders).sort();
381
372
  }
382
373
 
383
- export function validateTemplatePlaceholders(templatePath, variables, optionalPlaceholders = []) {
384
- const placeholders = getTemplatePlaceholders(templatePath);
385
- const providedKeys = Object.keys(variables);
386
-
387
- const missing = placeholders.filter(
388
- p => !providedKeys.includes(p) && !optionalPlaceholders.includes(p)
389
- );
390
-
391
- return {
392
- valid: missing.length === 0,
393
- missing
394
- };
395
- }
396
-
397
374
  export { Handlebars };
@@ -48,13 +48,6 @@ export function bytes(n) {
48
48
  return `${(n / 1024 / 1024).toFixed(1)} MB`;
49
49
  }
50
50
 
51
- export function duration(seconds) {
52
- if (typeof seconds !== 'number' || !Number.isFinite(seconds) || seconds <= 0) return '—';
53
- const m = Math.round(seconds / 60);
54
- if (m < 60) return `${m} min`;
55
- return `${Math.floor(m / 60)} h ${m % 60} min`;
56
- }
57
-
58
51
  export function splitFrontmatter(text) {
59
52
  const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text || '');
60
53
  if (!m) return { meta: null, body: text || '' };
@@ -36,13 +36,6 @@ export const STATUS_LABEL = {
36
36
  abandoned: 'Abandonada',
37
37
  };
38
38
 
39
- export const WORKTYPE_LABEL = {
40
- feature: 'feature',
41
- bug: 'bug',
42
- chore: 'chore',
43
- hotfix: 'hotfix',
44
- };
45
-
46
39
  export const LOCATION_LABEL = {
47
40
  root: 'raiz',
48
41
  worktree: 'worktree',
@@ -145,10 +145,6 @@ export function lastActivityByFeature(events) {
145
145
  return out;
146
146
  }
147
147
 
148
- export function loadProjectEvents(projectDir) {
149
- return dedupeEvents(loadTelemetryAllTrees(projectDir).flatMap((t) => t.events));
150
- }
151
-
152
148
  const LIVE_LOG = join('.morph', 'logs', 'events.jsonl');
153
149
 
154
150
  function eventKey(e) {
@@ -2,7 +2,7 @@
2
2
  // `[]` é "perguntei e não achei". Worktrees ficam fora: são o checkout de outra feature.
3
3
 
4
4
  import { readdirSync, readFileSync, statSync, existsSync } from 'node:fs';
5
- import { join, basename } from 'node:path';
5
+ import { join } from 'node:path';
6
6
  import { WORKTREE_DIR_NAMES } from '../worktree/dirs.js';
7
7
 
8
8
  export const SCAN_IGNORED_DIRS = new Set([
@@ -246,8 +246,3 @@ export function findPackage(scan, name) {
246
246
  if (!scan || !Array.isArray(scan.packages)) return null;
247
247
  return scan.packages.find((p) => p.name === name) ?? null;
248
248
  }
249
-
250
- export function projectFileNames(scan) {
251
- if (!scan) return [];
252
- return scan.projectFiles.map((f) => basename(f));
253
- }
@@ -7,7 +7,6 @@ import { execFileSync } from 'node:child_process';
7
7
  export const GRAPHIFY_VERSION = '0.9.50';
8
8
  export const GRAPHIFY_PACKAGE = 'graphifyy';
9
9
  export const GRAPHIFY_BIN = 'graphify';
10
- export const GRAPHIFY_MCP_BIN = 'graphify-mcp';
11
10
 
12
11
  const PREFERENCE = ['uv', 'pipx', 'python3', 'python'];
13
12
 
@@ -1,5 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { existsSync, mkdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from 'node:fs';
2
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
3
3
  import { join, dirname } from 'node:path';
4
4
 
5
5
  export const md5Of = (bytes) => createHash('md5').update(bytes).digest('hex');
@@ -63,7 +63,3 @@ export function restoreSnapshot(snapshot, { rewrite = true } = {}) {
63
63
  writeJournal(snapshot);
64
64
  return report;
65
65
  }
66
-
67
- export function discardSnapshot(snapshot) {
68
- rmSync(snapshot.dir, { recursive: true, force: true });
69
- }
@@ -379,8 +379,3 @@ export function getRiskSignals(signals) {
379
379
 
380
380
  return out;
381
381
  }
382
-
383
- export const RISK_CONSTANTS = {
384
- FORCED_PAUSE_WORK_TYPES,
385
- E2E_SKIP_CLASSES_THAT_PAUSE,
386
- };
@@ -9,17 +9,14 @@ const PROFILES = {
9
9
  dotnet: {
10
10
  isDotnet: true, isNextjs: false,
11
11
  defaultValidators: ['packages'],
12
- buildCommand: 'dotnet build --no-restore',
13
12
  },
14
13
  nextjs: {
15
14
  isDotnet: false, isNextjs: true,
16
15
  defaultValidators: ['nextjs-component'],
17
- buildCommand: 'npx tsc --noEmit',
18
16
  },
19
17
  empty: {
20
18
  isDotnet: false, isNextjs: false,
21
19
  defaultValidators: [],
22
- buildCommand: null,
23
20
  },
24
21
  };
25
22
 
@@ -38,7 +35,6 @@ function mergeProfiles(profiles) {
38
35
  isDotnet: profiles.some((p) => p.isDotnet),
39
36
  isNextjs: profiles.some((p) => p.isNextjs),
40
37
  defaultValidators: [...new Set(profiles.flatMap((p) => p.defaultValidators))],
41
- buildCommand: profiles.map((p) => p.buildCommand).find(Boolean) ?? null,
42
38
  };
43
39
  }
44
40
 
@@ -120,8 +120,11 @@ function auditDotnet(files, read) {
120
120
  if (major !== null && major < DOTNET_EXPECTED_MAJOR) add('Baixo', `Pacote desatualizado mas compatível (${p} ${packages.get(p)})`, [], `subir para ${DOTNET_EXPECTED_MAJOR}.x`);
121
121
  }
122
122
 
123
- const newGuid = grepFiles(source, read, /Guid\.NewGuid\(\)/);
124
- if (newGuid.length > 0) add('Alto', '`Guid.NewGuid()`', newGuid, 'trocar por `Guid.CreateVersion7()`');
123
+ const analyzers = packages.has('Microsoft.CodeAnalysis.BannedApiAnalyzers');
124
+ if (!analyzers) {
125
+ const newGuid = grepFiles(source, read, /Guid\.NewGuid\(\)/);
126
+ if (newGuid.length > 0) add('Alto', '`Guid.NewGuid()`', newGuid, 'trocar por `Guid.CreateVersion7()`');
127
+ }
125
128
  const repository = grepFiles(source, read, /interface\s+(IRepository\s*<|IUnitOfWork\b)/);
126
129
  if (repository.length > 0) {
127
130
  const di = grepFiles(source, read, /Add(Scoped|Transient|Singleton)\s*(<\s*(IRepository|IUnitOfWork)|\(\s*typeof\s*\(\s*(IRepository|IUnitOfWork))/);
@@ -135,17 +138,16 @@ function auditDotnet(files, read) {
135
138
  if (serviceLayer.length > 0) add('Alto', 'Camada de Service/Manager entre handler e dados', serviceLayer, 'o handler fala com o `ApplicationDbContext` direto');
136
139
  const rawSql = grepFiles(source, read, /\b(FromSqlRaw|ExecuteSqlRaw|SqlQueryRaw|ExecuteQuery)\s*\(/);
137
140
  if (rawSql.length > 0) add('Alto', 'SQL cru', rawSql, 'LINQ, ou store dedicado com justificativa em ADR');
138
- const notSealed = source.filter((f) => {
139
- const text = read(f) ?? '';
140
- return [...text.matchAll(/^\s*(?:public|internal)\s+((?:\w+\s+)*)class\s+\w+/gm)]
141
- .some((m) => !/\b(sealed|abstract|static|partial)\b/.test(m[1]));
142
- });
143
- if (notSealed.length > 0) add('Médio', 'Classes não-sealed', notSealed, '`sealed` por padrão');
141
+ if (projects.length > 0 && !analyzers) {
142
+ add('Médio', 'Analyzers do stack ausentes (o build não barra `Guid.NewGuid()` nem token não repassado)', [],
143
+ '`Directory.Build.props` + `BannedSymbols.txt` + `morph.globalconfig` na raiz: `morph-spec standards --show backend-dotnet-analyzers#files`');
144
+ }
144
145
  const controllers = grepFiles(source, read, /:\s*Controller(Base)?\b/);
145
146
  rows.push({ layer: 'Estrutura', detected: controllers.length > 0 ? `${controllers.length} controller(s) MVC` : 'sem controllers', expected: 'VSA (endpoint por slice)', ok: controllers.length === 0 });
146
147
  return { rows, deviations };
147
148
  }
148
149
 
150
+ const ESLINT_CONFIGS = ['eslint.config.mjs', 'eslint.config.js', 'eslint.config.cjs'];
149
151
  const UI_LIBRARIES = ['@mui/material', 'antd', '@chakra-ui/react', 'bootstrap', 'react-bootstrap', '@mantine/core'];
150
152
 
151
153
  function auditNextjs(root, read) {
@@ -177,11 +179,18 @@ function auditNextjs(root, read) {
177
179
  rows.push({ layer: 'Roteamento', detected: appRouter ? 'App Router' : (pagesRouter ? 'Pages Router' : 'não detectado'), expected: 'App Router', ok: appRouter || !pagesRouter });
178
180
  if (pagesRouter && !appRouter) add('Médio', 'Pages Router em vez de App Router', [next.file], 'migrar as rotas para `app/`');
179
181
  rows.push({ layer: 'Estilo', detected: next.deps.tailwindcss ? 'Tailwind' : 'ausente', expected: 'Tailwind', ok: Boolean(next.deps.tailwindcss) });
180
- rows.push({ layer: 'Motion', detected: next.deps['framer-motion'] || next.deps.motion ? 'Framer Motion' : 'ausente', expected: 'Framer Motion', ok: true });
182
+ const motionPackages = ['motion', 'framer-motion'].filter((p) => next.deps[p]);
183
+ rows.push({ layer: 'Motion', detected: motionPackages.join(' + ') || 'ausente', expected: 'motion (`motion/react`)', ok: motionPackages.length < 2 });
184
+ if (motionPackages.length === 2) add('Baixo', '`motion` e `framer-motion` instalados juntos', [next.file], 'remover `framer-motion`; o import é `motion/react`');
181
185
  const shadcn = existsSync(join(root, base, 'components.json'));
182
186
  const otherUi = UI_LIBRARIES.filter((lib) => next.deps[lib]);
183
187
  rows.push({ layer: 'UI', detected: otherUi.join(', ') || (shadcn ? 'shadcn' : 'ausente'), expected: 'shadcn', ok: otherUi.length === 0 });
184
188
  if (otherUi.length > 0) add('Médio', `Biblioteca de UI diferente (${otherUi.join(', ')})`, [next.file], 'shadcn sobre Tailwind');
189
+ const eslintConfig = ESLINT_CONFIGS.map((name) => `${base}${name}`).find((file) => existsSync(join(root, file)));
190
+ if (!eslintConfig || !/no-restricted-imports/.test(read(eslintConfig) ?? '')) {
191
+ add('Médio', 'ESLint do stack ausente (o lint não barra import proibido nem fetch em `useEffect`)', eslintConfig ? [eslintConfig] : [],
192
+ '`eslint.config.mjs` na raiz do app: `morph-spec standards --show frontend-nextjs-lint#files`');
193
+ }
185
194
  return { rows, deviations };
186
195
  }
187
196