@polymorphism-tech/morph-spec 9.4.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.
- package/CLAUDE.md +1 -1
- package/README.md +2 -2
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/MORPH.md +23 -33
- package/framework/agents.json +6 -7
- package/framework/docs/CLI.md +3 -3
- package/framework/hooks/README.md +2 -2
- package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
- package/framework/hooks/claude-code/core/comment-guard.js +1 -0
- package/framework/hooks/claude-code/core/denylist.json +63 -1
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +0 -8
- package/framework/hooks/claude-code/core/gate-guard.js +0 -8
- package/framework/hooks/claude-code/core/loop-detect.js +0 -3
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +2 -1
- package/framework/hooks/claude-code/core/protect-spec-files.js +0 -7
- package/framework/hooks/claude-code/core/validate-completion.js +0 -4
- package/framework/hooks/claude-code/core/worktree-context.js +0 -2
- package/framework/hooks/claude-code/statusline.py +3 -44
- package/framework/hooks/shared/build-check.js +12 -19
- package/framework/hooks/shared/comment-rules.js +11 -1
- package/framework/hooks/shared/hook-response.js +0 -4
- package/framework/hooks/shared/payload-utils.js +1 -35
- package/framework/hooks/shared/phase-utils.js +0 -12
- package/framework/hooks/shared/slice-progress.js +0 -2
- package/framework/hooks/shared/state-reader.js +0 -19
- package/framework/hooks/shared/telemetry-logger.js +1 -1
- package/framework/rules/frontend-standards.md +1 -2
- package/framework/rules/nextjs-standards.md +9 -8
- 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 +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-machine/SKILL.md +2 -2
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +1 -1
- package/framework/standards/STANDARDS.json +32 -29
- package/framework/standards/STANDARDS.meta.json +13 -15
- package/framework/standards/architecture/code-comments.md +1 -1
- package/framework/standards/frontend/admin-app/data-table.md +1 -3
- package/framework/standards/frontend/admin-app/page-structure.md +8 -4
- package/framework/standards/frontend/nextjs/app-router.md +3 -4
- package/framework/standards/frontend/nextjs/components.md +2 -15
- package/framework/standards/frontend/nextjs/data-fetching.md +2 -3
- package/framework/standards/frontend/nextjs/lint.md +141 -0
- package/framework/standards/frontend/nextjs/motion-patterns.md +2 -7
- package/framework/standards/frontend/nextjs/naming-conventions.md +1 -2
- package/framework/standards/frontend/nextjs/nextjs-patterns.md +5 -7
- package/framework/standards/frontend/nextjs/project-structure.md +3 -2
- package/framework/standards/frontend/nextjs/state-management.md +6 -7
- package/framework/templates/README.md +4 -4
- package/framework/templates/REGISTRY.json +21 -309
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +54 -56
- package/package.json +3 -2
- package/src/core/paths/output-schema.js +0 -9
- package/src/core/state/feature-identity.js +0 -2
- package/src/core/state/state-manager.js +1 -169
- package/src/core/templates/template-renderer.js +0 -23
- package/src/dashboard/app/lib/format.js +0 -7
- package/src/dashboard/app/lib/phases.js +0 -7
- package/src/dashboard/scanner/activity-scanner.js +0 -4
- package/src/lib/ai/ai-project-scan.js +1 -6
- package/src/lib/graph/graphify-runtime.js +0 -1
- package/src/lib/mutation/snapshot.js +1 -5
- package/src/lib/risk-detector.js +0 -5
- package/src/lib/stack/stack-profile.js +0 -4
- package/src/lib/stack/stack-scan.js +15 -4
- package/src/lib/tasks/eval-node.js +1 -7
- package/src/lib/validators/validation-runner.js +0 -43
- package/src/lib/validators/verify-runner.js +18 -2
- package/src/utils/agents-installer.js +12 -19
- package/src/utils/claude-settings-manager.js +0 -28
- package/src/utils/file-copier.js +0 -53
- package/framework/hooks/shared/activity-logger.js +0 -131
- package/framework/standards/frontend/design-system/naming.md +0 -68
- package/src/commands/validation/index.js +0 -2
- package/src/lib/validators/code-budget/index.js +0 -2
- package/src/lib/validators/contracts/index.js +0 -1
- package/src/lib/validators/nextjs/index.js +0 -1
- package/src/lib/validators/packages/index.js +0 -1
- 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 {
|
|
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
|
-
<
|
|
33
|
-
<
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
render={({ field }) => (
|
|
38
|
-
<
|
|
39
|
-
<
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
+
"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",
|
|
@@ -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,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync,
|
|
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];
|
|
@@ -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
|
|
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,
|
|
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
|
-
}
|
package/src/lib/risk-detector.js
CHANGED
|
@@ -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
|
|
124
|
-
if (
|
|
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,7 +138,7 @@ 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
|
-
if (projects.length > 0 && !
|
|
141
|
+
if (projects.length > 0 && !analyzers) {
|
|
139
142
|
add('Médio', 'Analyzers do stack ausentes (o build não barra `Guid.NewGuid()` nem token não repassado)', [],
|
|
140
143
|
'`Directory.Build.props` + `BannedSymbols.txt` + `morph.globalconfig` na raiz: `morph-spec standards --show backend-dotnet-analyzers#files`');
|
|
141
144
|
}
|
|
@@ -144,6 +147,7 @@ function auditDotnet(files, read) {
|
|
|
144
147
|
return { rows, deviations };
|
|
145
148
|
}
|
|
146
149
|
|
|
150
|
+
const ESLINT_CONFIGS = ['eslint.config.mjs', 'eslint.config.js', 'eslint.config.cjs'];
|
|
147
151
|
const UI_LIBRARIES = ['@mui/material', 'antd', '@chakra-ui/react', 'bootstrap', 'react-bootstrap', '@mantine/core'];
|
|
148
152
|
|
|
149
153
|
function auditNextjs(root, read) {
|
|
@@ -175,11 +179,18 @@ function auditNextjs(root, read) {
|
|
|
175
179
|
rows.push({ layer: 'Roteamento', detected: appRouter ? 'App Router' : (pagesRouter ? 'Pages Router' : 'não detectado'), expected: 'App Router', ok: appRouter || !pagesRouter });
|
|
176
180
|
if (pagesRouter && !appRouter) add('Médio', 'Pages Router em vez de App Router', [next.file], 'migrar as rotas para `app/`');
|
|
177
181
|
rows.push({ layer: 'Estilo', detected: next.deps.tailwindcss ? 'Tailwind' : 'ausente', expected: 'Tailwind', ok: Boolean(next.deps.tailwindcss) });
|
|
178
|
-
|
|
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`');
|
|
179
185
|
const shadcn = existsSync(join(root, base, 'components.json'));
|
|
180
186
|
const otherUi = UI_LIBRARIES.filter((lib) => next.deps[lib]);
|
|
181
187
|
rows.push({ layer: 'UI', detected: otherUi.join(', ') || (shadcn ? 'shadcn' : 'ausente'), expected: 'shadcn', ok: otherUi.length === 0 });
|
|
182
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
|
+
}
|
|
183
194
|
return { rows, deviations };
|
|
184
195
|
}
|
|
185
196
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
|
|
2
|
-
import { join, resolve
|
|
2
|
+
import { join, resolve } from 'path';
|
|
3
3
|
import { tmpdir } from 'os';
|
|
4
4
|
|
|
5
5
|
import { isInside } from './test-runner.js';
|
|
@@ -284,12 +284,6 @@ export function evalColdCacheWarning(summary) {
|
|
|
284
284
|
return null;
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
-
export function relativeToProject(projectPath, target) {
|
|
288
|
-
const root = resolve(projectPath) + sep;
|
|
289
|
-
const full = resolve(target);
|
|
290
|
-
return full.startsWith(root) ? full.slice(root.length).replace(/\\/g, '/') : full;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
287
|
export function isDirectory(path) {
|
|
294
288
|
try {
|
|
295
289
|
return statSync(path).isDirectory();
|
|
@@ -186,46 +186,3 @@ async function runSingleValidator(validatorId, projectPath, featureName, options
|
|
|
186
186
|
};
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
|
-
|
|
190
|
-
export function formatValidationResults(result) {
|
|
191
|
-
if (result.passed && result.warnings === 0) {
|
|
192
|
-
const ranCount = Object.keys(result.results || {}).length;
|
|
193
|
-
const countLabel = ranCount > 0 ? ` (${ranCount} validator${ranCount !== 1 ? 's' : ''})` : '';
|
|
194
|
-
console.log(chalk.green(`\n ✅ All validations passed${countLabel}!\n`));
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (result.passed && result.warnings > 0) {
|
|
199
|
-
console.log(chalk.yellow(`\n ⚠️ Passed with ${result.warnings} warning(s)\n`));
|
|
200
|
-
} else {
|
|
201
|
-
console.log(chalk.red(`\n ❌ Validation FAILED: ${result.errors} error(s), ${result.warnings} warning(s)\n`));
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const errors = result.issues.filter(i => i.level === 'error');
|
|
205
|
-
const warnings = result.issues.filter(i => i.level === 'warning');
|
|
206
|
-
|
|
207
|
-
if (errors.length > 0) {
|
|
208
|
-
console.log(chalk.red.bold(' Errors:'));
|
|
209
|
-
for (const issue of errors.slice(0, 10)) {
|
|
210
|
-
console.log(chalk.red(` ✗ ${issue.message}`));
|
|
211
|
-
if (issue.file) console.log(chalk.gray(` ${issue.file}${issue.line ? ':' + issue.line : ''}`));
|
|
212
|
-
if (issue.solution) console.log(chalk.cyan(` Fix: ${issue.solution}`));
|
|
213
|
-
}
|
|
214
|
-
if (errors.length > 10) {
|
|
215
|
-
console.log(chalk.gray(` ... and ${errors.length - 10} more errors`));
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (warnings.length > 0) {
|
|
220
|
-
console.log(chalk.yellow('\n Warnings:'));
|
|
221
|
-
for (const issue of warnings.slice(0, 5)) {
|
|
222
|
-
console.log(chalk.yellow(` ⚠ ${issue.message}`));
|
|
223
|
-
if (issue.file) console.log(chalk.gray(` ${issue.file}`));
|
|
224
|
-
}
|
|
225
|
-
if (warnings.length > 5) {
|
|
226
|
-
console.log(chalk.gray(` ... and ${warnings.length - 5} more warnings`));
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
console.log('');
|
|
231
|
-
}
|