@polymorphism-tech/morph-spec 8.45.0 → 8.47.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 +6 -5
- package/README.md +2 -2
- package/bin/morph-spec.js +2 -4
- package/bin/validate.js +4 -25
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +2 -2
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +2 -2
- package/framework/MORPH.md +9 -9
- package/framework/commands/create-umbraco-lp.md +2 -0
- package/framework/commands/migrate-lp-to-umbraco.md +2 -0
- package/framework/commands/morph-archive.md +1 -1
- package/framework/commands/morph-hotfix.md +1 -1
- package/framework/commands/morph-infra.md +17 -203
- package/framework/commands/morph-media.md +3 -0
- package/framework/commands/morph-preflight.md +5 -28
- package/framework/commands/morph-proposal.md +2 -2
- package/framework/commands/morph-troubleshoot.md +2 -14
- package/framework/docs/CLI.md +8 -7
- package/framework/hooks/claude-code/core/state-sync.js +13 -1
- package/framework/hooks/shared/e2e-check.js +3 -4
- package/framework/hooks/shared/state-writer.js +1 -0
- package/framework/rules/ai-agents-standards.md +18 -16
- package/framework/rules/asaas-standards.md +3 -1
- package/framework/rules/csharp-standards.md +10 -8
- package/framework/rules/data-standards.md +5 -5
- package/framework/rules/dotnet-templates.md +1 -1
- package/framework/rules/frontend-standards.md +4 -2
- package/framework/rules/gsap-standards.md +10 -5
- package/framework/rules/hangfire-standards.md +3 -1
- package/framework/rules/infrastructure-standards.md +4 -2
- package/framework/rules/morph-workflow.md +2 -2
- package/framework/rules/neon-auth-standards.md +3 -1
- package/framework/rules/neon-standards.md +6 -4
- package/framework/rules/nextjs-standards.md +13 -11
- package/framework/rules/nextjs-templates.md +1 -1
- package/framework/rules/resend-standards.md +3 -1
- package/framework/schemas/config.schema.json +4 -0
- package/framework/schemas/feature.schema.json +4 -4
- package/framework/skills/README.md +1 -1
- package/framework/skills/core/create-umbraco-lp/SKILL.md +3 -3
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +3 -3
- package/framework/skills/core/morph-apply/SKILL.md +3 -3
- package/framework/skills/core/morph-brainstorming/SKILL.md +3 -3
- package/framework/skills/core/morph-hotfix/SKILL.md +4 -4
- package/framework/skills/core/morph-implement/SKILL.md +3 -3
- package/framework/skills/core/morph-machine/SKILL.md +3 -3
- package/framework/skills/core/morph-media/SKILL.md +3 -3
- package/framework/skills/core/morph-plan/SKILL.md +3 -3
- package/framework/skills/core/morph-proposal/SKILL.md +3 -3
- package/framework/skills/core/morph-review/SKILL.md +30 -17
- package/framework/skills/core/morph-stack-scan/SKILL.md +3 -3
- package/framework/skills/core/morph-standards/SKILL.md +6 -6
- package/framework/skills/core/morph-uiux/SKILL.md +3 -3
- package/framework/standards/STANDARDS.json +59 -56
- package/framework/standards/ai-agents/evals-with-cache.md +1 -1
- package/framework/standards/ai-agents/llm-runtime-defaults.md +1 -1
- package/framework/standards/ai-agents/modalities-speech-to-text.md +36 -26
- package/framework/standards/ai-agents/prompt-sources.md +6 -4
- package/framework/standards/ai-agents/providers/model-registry.md +1 -1
- package/framework/standards/ai-agents/rag-custom-pgvector.md +67 -39
- 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/architecture/code-comments.md +2 -2
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +18 -9
- package/framework/standards/backend/api/validation.md +135 -47
- package/framework/standards/backend/database/postgresql/database.md +11 -7
- package/framework/standards/backend/database/vector-search-rag.md +207 -324
- package/framework/standards/backend/dotnet/fluent-validation-vsa.md +14 -8
- package/framework/standards/backend/dotnet/result-pattern.md +40 -6
- package/framework/standards/backend/dotnet/scalar-ui.md +13 -7
- package/framework/standards/backend/dotnet/scrutor.md +1 -1
- package/framework/standards/backend/dotnet/testing.md +1 -1
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +7 -7
- package/framework/standards/backend/integrations/asaas/asaas-api.md +309 -165
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +134 -154
- package/framework/standards/data/nosql/cache/redis.md +120 -44
- package/framework/standards/data/nosql/cosmos-db.md +114 -71
- package/framework/standards/frontend/design-system/animations.md +1 -1
- package/framework/standards/frontend/design-system/naming.md +1 -1
- package/framework/standards/frontend/nextjs/motion-patterns.md +45 -29
- package/framework/standards/frontend/nextjs/naming-conventions.md +1 -1
- package/framework/standards/infrastructure/docker/coolify-deploy.md +8 -6
- package/framework/standards/infrastructure/neon/neon-pgvector.md +120 -75
- package/framework/standards/infrastructure/neon/neon-setup.md +53 -31
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +2 -2
- package/framework/templates/code/dotnet/extensions/database-extensions.cs +1 -1
- package/framework/templates/code/dotnet/extensions/result-extensions.cs +27 -0
- package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +2 -2
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +1 -1
- package/framework/templates/infrastructure/docker/coolify.template.json +3 -3
- package/framework/templates/infrastructure/github/README.md +2 -1
- package/framework/templates/infrastructure/github/workflows/deploy-coolify.yml.hbs +2 -1
- package/package.json +1 -1
- package/src/commands/observability/telemetry.js +3 -5
- package/src/commands/project/init.js +2 -1
- package/src/commands/project/update.js +16 -18
- package/src/commands/state/approve.js +17 -0
- package/src/commands/state/gate-check.js +4 -14
- package/src/commands/validation/verify.js +99 -57
- package/src/core/paths/output-schema.js +1 -1
- package/src/dashboard/app/App.jsx +0 -2
- package/src/dashboard/app/components/CommandPalette.jsx +2 -3
- package/src/dashboard/app/components/EntityLink.jsx +0 -1
- package/src/dashboard/app/components/Markdown.jsx +1 -1
- package/src/dashboard/app/components/Sidebar.jsx +1 -2
- package/src/dashboard/app/pages/RulesMap.jsx +3 -3
- package/src/dashboard/app/pages/SkillsLibrary.jsx +3 -3
- package/src/dashboard/scanner/activity-scanner.js +1 -1
- package/src/dashboard/scanner/docs-scanner.js +5 -23
- package/src/dashboard/scanner/features-scanner.js +1 -1
- package/src/dashboard/scanner/index.js +0 -2
- package/src/dashboard/scanner/rules-scanner.js +15 -10
- package/src/dashboard/scanner/skills-scanner.js +2 -3
- 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/evaluator-report.js +13 -0
- package/src/lib/risk-detector.js +8 -0
- package/src/lib/stack/stack-profile.js +1 -1
- package/src/lib/telemetry/verify-event.js +3 -2
- package/src/lib/validators/reuse/reuse-validator.js +2 -3
- package/src/lib/validators/reuse/symbol-scanner.js +2 -4
- package/src/lib/validators/validation-runner.js +8 -88
- package/src/lib/validators/verify-runner.js +22 -72
- package/src/scripts/setup-infra.js +33 -22
- package/src/utils/agents-installer.js +20 -8
- package/src/utils/file-copier.js +7 -5
- package/framework/commands/commit.md +0 -166
- package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
- package/framework/rubrics/backend.json +0 -41
- package/framework/rubrics/default.json +0 -36
- package/framework/rubrics/frontend.json +0 -36
- package/framework/templates/project/validate.js +0 -97
- package/src/dashboard/app/pages/Rubrics.jsx +0 -97
- package/src/dashboard/scanner/rubrics-scanner.js +0 -12
- package/src/lib/validators/architecture/architecture-validator.js +0 -270
- package/src/lib/validators/architecture/index.js +0 -7
- package/src/lib/validators/css/css-validator.js +0 -340
- package/src/lib/validators/css/index.js +0 -7
- package/src/lib/validators/design-system/design-system-validator.js +0 -237
- package/src/lib/validators/design-system/index.js +0 -7
- package/src/lib/validators/score-aggregator.js +0 -244
- package/src/lib/validators/ui/index.js +0 -7
- package/src/lib/validators/ui/ui-contrast-validator.js +0 -422
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The reading material of the framework — the charter, the runtime
|
|
3
|
-
* instructions, the CLI reference and the
|
|
4
|
-
*
|
|
3
|
+
* instructions, the CLI reference and the Gate 3 evaluator checklist — for the
|
|
4
|
+
* dashboard's Guide page.
|
|
5
5
|
*/
|
|
6
|
-
import { readFileSync
|
|
6
|
+
import { readFileSync } from 'fs';
|
|
7
7
|
import { join } from 'path';
|
|
8
8
|
|
|
9
9
|
function read(path) {
|
|
@@ -23,26 +23,11 @@ export function headingsOf(content) {
|
|
|
23
23
|
return out;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
function titleOf(content, fallback) {
|
|
27
|
-
const h = headingsOf(content || '').find((x) => x.depth === 1);
|
|
28
|
-
return h?.text || fallback;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function mdDir(dir) {
|
|
32
|
-
if (!existsSync(dir)) return [];
|
|
33
|
-
return readdirSync(dir)
|
|
34
|
-
.filter((f) => f.endsWith('.md'))
|
|
35
|
-
.sort()
|
|
36
|
-
.map((f) => {
|
|
37
|
-
const content = read(join(dir, f)) || '';
|
|
38
|
-
return { id: f.replace(/\.md$/, ''), file: f, title: titleOf(content, f.replace(/\.md$/, '')), content };
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
26
|
const GUIDE = [
|
|
43
27
|
{ id: 'morph', path: 'MORPH.md', title: 'Carta do harness (MORPH.md)', summary: 'Como o Claude trabalha: papéis, fases, gates, sub-agents. É a fonte única do comportamento.' },
|
|
44
28
|
{ id: 'runtime', path: 'CLAUDE.md', title: 'Instruções de runtime (CLAUDE.md)', summary: 'O que é injetado no CLAUDE.md do projeto: como começar cada sessão, regras críticas, comandos.' },
|
|
45
29
|
{ id: 'cli', path: 'docs/CLI.md', title: 'Referência do CLI', summary: 'Todos os comandos morph-spec: gates, verify, worktrees, grafo, telemetria.' },
|
|
30
|
+
{ id: 'review-checklist', path: 'evals/review-checklist.md', title: 'Checklist do avaliador (Gate 3)', summary: 'O que o avaliador independente procura antes do ship: veredito SHIP/FIX e bloqueador com evidência.' },
|
|
46
31
|
];
|
|
47
32
|
|
|
48
33
|
export function scanDocs(frameworkDir) {
|
|
@@ -51,8 +36,5 @@ export function scanDocs(frameworkDir) {
|
|
|
51
36
|
return { ...g, content, headings: headingsOf(content).filter((h) => h.depth <= 3) };
|
|
52
37
|
}).filter((g) => g.content);
|
|
53
38
|
|
|
54
|
-
return {
|
|
55
|
-
guide,
|
|
56
|
-
rubrics: mdDir(join(frameworkDir, 'evals')).filter((d) => d.id === 'review-checklist'),
|
|
57
|
-
};
|
|
39
|
+
return { guide };
|
|
58
40
|
}
|
|
@@ -310,7 +310,7 @@ export function scanFeatures(projectDir, { resolveTree = resolveFeatureTree } =
|
|
|
310
310
|
}
|
|
311
311
|
|
|
312
312
|
/**
|
|
313
|
-
* One feature in full: summary + tasks with their verify
|
|
313
|
+
* One feature in full: summary + tasks with their verify, the gate
|
|
314
314
|
* decisions, the verify nodes and the document tree.
|
|
315
315
|
*/
|
|
316
316
|
export function readFeatureDetail(projectDir, name, { resolveTree = resolveFeatureTree, known } = {}) {
|
|
@@ -4,7 +4,6 @@ import { scanHooks } from './hooks-scanner.js';
|
|
|
4
4
|
import { scanSkills } from './skills-scanner.js';
|
|
5
5
|
import { scanStandards } from './standards-scanner.js';
|
|
6
6
|
import { scanRules } from './rules-scanner.js';
|
|
7
|
-
import { scanRubrics } from './rubrics-scanner.js';
|
|
8
7
|
import { scanDocs } from './docs-scanner.js';
|
|
9
8
|
|
|
10
9
|
/**
|
|
@@ -21,7 +20,6 @@ export function scanAll(frameworkDir) {
|
|
|
21
20
|
skills: scanSkills(frameworkDir, phases.list),
|
|
22
21
|
standards: scanStandards(frameworkDir, agents.list),
|
|
23
22
|
rules: scanRules(frameworkDir),
|
|
24
|
-
rubrics: scanRubrics(frameworkDir),
|
|
25
23
|
docs: scanDocs(frameworkDir),
|
|
26
24
|
};
|
|
27
25
|
}
|
|
@@ -18,17 +18,22 @@ function parseFrontmatter(content) {
|
|
|
18
18
|
return { paths, stacks };
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
const ROUTE_LINE = /^- \*\*([a-z0-9-]+)\*\* \(`([^`]+\.md)`\)/;
|
|
22
|
+
|
|
23
|
+
// Bullet form: - **<id>** (`<path under standards/>`) — <digest>. An `@path`
|
|
24
|
+
// import inside .claude/rules resolves relative to the rule file and never loads.
|
|
25
|
+
export function parseStandardRoutes(content) {
|
|
26
|
+
const routes = [];
|
|
27
|
+
for (const line of content.split(/\r?\n/)) {
|
|
28
|
+
const m = ROUTE_LINE.exec(line);
|
|
29
|
+
if (m) routes.push({ id: m[1], path: m[2] });
|
|
30
|
+
}
|
|
31
|
+
return routes;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// RulesMap keys one edge per ref, so a repeated path would collide.
|
|
26
35
|
export function extractStandardRefs(content) {
|
|
27
|
-
|
|
28
|
-
const regex = /@\.morph\/framework\/standards\/(.+\.md)/g;
|
|
29
|
-
let m;
|
|
30
|
-
while ((m = regex.exec(content)) !== null) refs.add(m[1]);
|
|
31
|
-
return [...refs];
|
|
36
|
+
return [...new Set(parseStandardRoutes(content).map((r) => r.path))];
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
export function scanRules(frameworkDir) {
|
|
@@ -84,13 +84,12 @@ export function scanSkills(frameworkDir, phases = []) {
|
|
|
84
84
|
if (!existsSync(skillPath)) continue;
|
|
85
85
|
const content = readFileSync(skillPath, 'utf8');
|
|
86
86
|
const fm = parseFrontmatter(content);
|
|
87
|
-
|
|
88
|
-
const invocable = fm['user-invokable'] ?? fm['user-invocable'];
|
|
87
|
+
const invocable = fm['user-invocable'];
|
|
89
88
|
skills.push({
|
|
90
89
|
id: skillDir.name,
|
|
91
90
|
name: fm.name || skillDir.name,
|
|
92
91
|
description: fm.description || '',
|
|
93
|
-
|
|
92
|
+
whenToUse: fm.when_to_use || '',
|
|
94
93
|
level: levelDir.name,
|
|
95
94
|
userInvocable: invocable === true || invocable === 'true',
|
|
96
95
|
stacks: typeof fm.stacks === 'string' ? fm.stacks.replace(/[[\]]/g, '').split(',').map((s) => s.trim()).filter(Boolean) : [],
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { getHeadSha } from '../git/default-branch.js';
|
|
5
|
+
import { treeMovedSince } from '../git/evidence-staleness.js';
|
|
6
|
+
import { resolveGh } from '../git/gh.js';
|
|
7
|
+
import {
|
|
8
|
+
CI_WORKFLOW_DIR, CI_WORKFLOW_FILE, ciAvailability, downloadResult, findCiWorkflow, pushBranch,
|
|
9
|
+
uncommittedCode, waitForRun, workflowPushProblem,
|
|
10
|
+
} from './github-ci.js';
|
|
11
|
+
import { renderCiWorkflow } from './workflow-scaffold.js';
|
|
12
|
+
|
|
13
|
+
export const CI_WAIT_MS = 50 * 60 * 1000;
|
|
14
|
+
const APPEAR_MS = Number(process.env.MORPH_CI_APPEAR_MS ?? 90 * 1000);
|
|
15
|
+
|
|
16
|
+
export class CiUsageError extends Error {}
|
|
17
|
+
|
|
18
|
+
const NODE_KEYS = ['build', 'tests', 'validators', 'e2e'];
|
|
19
|
+
|
|
20
|
+
function isVerifyResult(result) {
|
|
21
|
+
return Boolean(result) && ['pass', 'fail'].includes(result.status) && result.scope === 'feature'
|
|
22
|
+
&& NODE_KEYS.every((k) => typeof result.nodes?.[k]?.status === 'string');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function scaffoldWorkflow(root, config) {
|
|
26
|
+
const rel = `${CI_WORKFLOW_DIR}/${CI_WORKFLOW_FILE}`;
|
|
27
|
+
const abs = join(root, rel);
|
|
28
|
+
if (existsSync(abs)) return { error: `${rel} já existe e não chama morph-spec verify publicando o JSON` };
|
|
29
|
+
try {
|
|
30
|
+
mkdirSync(join(root, CI_WORKFLOW_DIR), { recursive: true });
|
|
31
|
+
writeFileSync(abs, renderCiWorkflow(root, config));
|
|
32
|
+
execFileSync('git', ['add', '--', rel], { cwd: root, stdio: 'pipe' });
|
|
33
|
+
execFileSync('git', ['commit', '-m', 'ci: workflow morph-verify (prova do Gate 3 no CI)', '--', rel], { cwd: root, stdio: 'pipe' });
|
|
34
|
+
return { path: rel };
|
|
35
|
+
} catch (err) {
|
|
36
|
+
return { error: `não consegui criar ${rel}: ${(err.stderr?.toString() || err.message).trim()}` };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function dispatchWorkflow(root, workflow, branch) {
|
|
41
|
+
try {
|
|
42
|
+
const { bin, prefix } = resolveGh();
|
|
43
|
+
execFileSync(bin, [...prefix, 'workflow', 'run', workflow, '--ref', branch], { cwd: root, stdio: 'pipe', timeout: 15000 });
|
|
44
|
+
return true;
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* `verify {f} --ci`: the feature-scope proof, run by the project's CI over the pushed commit.
|
|
52
|
+
* Every way CI cannot answer is `mode: 'local'` with the reason, and the caller runs the local
|
|
53
|
+
* verify: no CI is never a pause. Only the uploaded `verify --json` becomes a result, so the
|
|
54
|
+
* gate judges a CI run by the same stamp and the same rules as a local one.
|
|
55
|
+
*
|
|
56
|
+
* @returns {Promise<
|
|
57
|
+
* { mode: 'local', reason: string } |
|
|
58
|
+
* { mode: 'pending', reason: string, run: object|null } |
|
|
59
|
+
* { mode: 'ci', result: object, run: object, created: string|null }>}
|
|
60
|
+
* @throws {CiUsageError} code CI would not see (uncommitted work)
|
|
61
|
+
*/
|
|
62
|
+
export async function runCiVerify(root, featureName, config, { log = () => {}, waitMs = CI_WAIT_MS, wait = waitForRun } = {}) {
|
|
63
|
+
const avail = ciAvailability(root, featureName, config);
|
|
64
|
+
if (!avail.ok) return { mode: 'local', reason: avail.reason };
|
|
65
|
+
|
|
66
|
+
const dirty = uncommittedCode(root);
|
|
67
|
+
if (dirty === null) return { mode: 'local', reason: 'git status não respondeu' };
|
|
68
|
+
if (dirty.length > 0) {
|
|
69
|
+
const shown = dirty.slice(0, 5).join(', ') + (dirty.length > 5 ? ` (+${dirty.length - 5})` : '');
|
|
70
|
+
throw new CiUsageError(`o CI só vê commits e há trabalho fora deles: ${shown}. Commite na branch da feature (git add <paths> && git commit) e rode de novo, ou rode sem --ci`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let workflow = findCiWorkflow(root);
|
|
74
|
+
let created = null;
|
|
75
|
+
if (!workflow) {
|
|
76
|
+
const problem = workflowPushProblem(avail);
|
|
77
|
+
if (problem) return { mode: 'local', reason: problem };
|
|
78
|
+
const scaffold = scaffoldWorkflow(root, config);
|
|
79
|
+
if (scaffold.error) return { mode: 'local', reason: scaffold.error };
|
|
80
|
+
created = scaffold.path;
|
|
81
|
+
workflow = CI_WORKFLOW_FILE;
|
|
82
|
+
log(`workflow criado e commitado: ${created}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const pushError = pushBranch(root, avail.branch);
|
|
86
|
+
if (pushError) return { mode: 'local', reason: `push de ${avail.branch} falhou: ${pushError}` };
|
|
87
|
+
|
|
88
|
+
const head = getHeadSha(root);
|
|
89
|
+
const waitOpts = {
|
|
90
|
+
branch: avail.branch,
|
|
91
|
+
workflow,
|
|
92
|
+
isCurrent: (sha) => !treeMovedSince(root, sha, head),
|
|
93
|
+
timeoutMs: waitMs,
|
|
94
|
+
appearMs: APPEAR_MS,
|
|
95
|
+
onPending: (run) => log(`CI rodando: ${run.url}`),
|
|
96
|
+
};
|
|
97
|
+
let outcome = await wait(root, waitOpts);
|
|
98
|
+
if (outcome.state === 'no-run' && dispatchWorkflow(root, workflow, avail.branch)) {
|
|
99
|
+
log(`nenhum run para este commit; disparei ${workflow} manualmente`);
|
|
100
|
+
outcome = await wait(root, waitOpts);
|
|
101
|
+
}
|
|
102
|
+
if (outcome.state === 'no-run') return { mode: 'local', reason: `${workflow} não rodou para este commit` };
|
|
103
|
+
if (outcome.state === 'unknown') return { mode: 'local', reason: 'gh run list não respondeu' };
|
|
104
|
+
if (outcome.state === 'timeout') {
|
|
105
|
+
return { mode: 'pending', reason: `o CI não terminou em ${Math.round(waitMs / 60000)} min`, run: outcome.run ?? null };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const { run } = outcome;
|
|
109
|
+
const result = downloadResult(root, run.databaseId);
|
|
110
|
+
if (!isVerifyResult(result)) {
|
|
111
|
+
return {
|
|
112
|
+
mode: 'local',
|
|
113
|
+
reason: `run ${run.conclusion} sem o JSON do verify (${run.url}; gh run view ${run.databaseId} --log-failed)`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const { persisted, reason, repeatOf, cleanup, ...verdict } = result;
|
|
117
|
+
return { mode: 'ci', result: verdict, run, created };
|
|
118
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -79,6 +79,19 @@ export function evaluatorReportState(cwd, featureName) {
|
|
|
79
79
|
return { status: 'ok', path, ...parsed };
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/** O sinal `evaluatorVerdict` que `verdictPause` (risk-detector) lê — gate-check e approve leem o mesmo. */
|
|
83
|
+
export function verdictSignal(report) {
|
|
84
|
+
if (report.status !== 'ok') return { status: report.status };
|
|
85
|
+
return {
|
|
86
|
+
status: 'ok',
|
|
87
|
+
verdict: report.verdict,
|
|
88
|
+
blockers: report.blockers.length,
|
|
89
|
+
blockerIds: report.blockers.map((b) => b.id),
|
|
90
|
+
untyped: report.untyped.length,
|
|
91
|
+
superseded: report.superseded,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
82
95
|
export function evaluatorMediaSection(cwd, featureName) {
|
|
83
96
|
const text = readReport(cwd, featureName);
|
|
84
97
|
return text !== null && MEDIA_SECTION_RE.test(text);
|
package/src/lib/risk-detector.js
CHANGED
|
@@ -244,6 +244,14 @@ export function shouldPauseGate(config, signals) {
|
|
|
244
244
|
};
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
// No ship, ausência de stamp não é "verde": uma feature foi aprovada em campo sem verify nenhum.
|
|
248
|
+
if (signals.gate === 'review' && !signals.verificationStatus) {
|
|
249
|
+
return {
|
|
250
|
+
pause: true,
|
|
251
|
+
reason: 'nenhum `morph-spec verify` carimbado nesta feature — rode `morph-spec verify {feature}` antes do ship',
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
247
255
|
// No gate de SHIP: um `verification` PRESENTE cujo escopo não é o da feature
|
|
248
256
|
// é um recorte parcial — de UMA task, nunca da app inteira. O stamp é
|
|
249
257
|
// SINGULAR, sobrescrito por qualquer verify de qualquer escopo, e vale
|
|
@@ -19,7 +19,7 @@ const NO_VALIDATOR_STACKS = new Set(['nodejs-cli', 'node-cli', 'cli']);
|
|
|
19
19
|
const PROFILES = {
|
|
20
20
|
dotnet: {
|
|
21
21
|
isDotnet: true, isNextjs: false,
|
|
22
|
-
defaultValidators: ['
|
|
22
|
+
defaultValidators: ['packages'],
|
|
23
23
|
buildCommand: 'dotnet build --no-restore',
|
|
24
24
|
},
|
|
25
25
|
nextjs: {
|
|
@@ -14,10 +14,10 @@ const NODE_NAMES = ['build', 'tests', 'validators', 'e2e'];
|
|
|
14
14
|
/**
|
|
15
15
|
* @param {Object} result - runVerify() output
|
|
16
16
|
* @param {{ sha: string|null, tree: 'root'|'worktree', persisted: boolean, reason: string|null,
|
|
17
|
-
* fingerprint?: string|null }} context
|
|
17
|
+
* fingerprint?: string|null, source?: 'local'|'ci' }} context
|
|
18
18
|
* @returns {Object}
|
|
19
19
|
*/
|
|
20
|
-
export function buildVerifyEvent(result, { sha, tree, persisted, reason, fingerprint = null }) {
|
|
20
|
+
export function buildVerifyEvent(result, { sha, tree, persisted, reason, fingerprint = null, source = 'local' }) {
|
|
21
21
|
const nodes = {};
|
|
22
22
|
for (const name of NODE_NAMES) {
|
|
23
23
|
const node = result.nodes?.[name];
|
|
@@ -40,6 +40,7 @@ export function buildVerifyEvent(result, { sha, tree, persisted, reason, fingerp
|
|
|
40
40
|
tree,
|
|
41
41
|
persisted,
|
|
42
42
|
reason: persisted ? null : (reason ?? null),
|
|
43
|
+
source,
|
|
43
44
|
nodes,
|
|
44
45
|
};
|
|
45
46
|
}
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Reuse Validator — símbolo novo cujo nome já existe no codebase.
|
|
3
3
|
*
|
|
4
|
-
* A terceira camada do ciclo de reuso
|
|
4
|
+
* A terceira camada do ciclo de reuso:
|
|
5
5
|
*
|
|
6
6
|
* PROSA (morph-plan, personas) ──► ARTEFATO (`reuse` em tasks.json)
|
|
7
7
|
* ──► COBRANÇA (este arquivo)
|
|
8
8
|
*
|
|
9
9
|
* Prosa sozinha não segura — `morph-brainstorming/SKILL.md:39` já manda olhar
|
|
10
10
|
* o que existe, e o ProspectPRO redefiniu `SimpleBuilding` em cinco arquivos da
|
|
11
|
-
* MESMA pasta assim mesmo
|
|
12
|
-
* tokens) tem verificação em código; este é o equivalente para código.
|
|
11
|
+
* MESMA pasta assim mesmo: só a verificação em código segura o ciclo.
|
|
13
12
|
*
|
|
14
13
|
* Regras, na ordem em que reduzem ruído:
|
|
15
14
|
*
|
|
@@ -9,8 +9,7 @@
|
|
|
9
9
|
* papéis produziria um validador cego justamente na janela em que ele precisa
|
|
10
10
|
* morder — ver graph-loader.js `getFreshness`.
|
|
11
11
|
*
|
|
12
|
-
* A varredura é por linha, com regex ancorada no início da declaração
|
|
13
|
-
* limitação (e mesmo grau de tolerância) do design-system-validator: uma
|
|
12
|
+
* A varredura é por linha, com regex ancorada no início da declaração: uma
|
|
14
13
|
* declaração citada dentro de uma string ou de um comentário de bloco pode
|
|
15
14
|
* passar. O custo de errar é um aviso a mais, nunca um bloqueio silencioso.
|
|
16
15
|
*
|
|
@@ -28,8 +27,7 @@ import { isProjectSource } from '../../../../framework/hooks/shared/source-files
|
|
|
28
27
|
*
|
|
29
28
|
* A lista é curta de propósito e só admite nome genérico de UMA palavra: assim
|
|
30
29
|
* que há qualificador de domínio (`BuildingFiltersProps`, `FilterOption`) o
|
|
31
|
-
* nome volta a ser um conceito e a regra vale.
|
|
32
|
-
* cores de sistema no design-system-validator.
|
|
30
|
+
* nome volta a ser um conceito e a regra vale.
|
|
33
31
|
*/
|
|
34
32
|
const IDIOMATIC_LOCAL_NAMES = new Set([
|
|
35
33
|
'props', 'state', 'row', 'item', 'column', 'context', 'params', 'options',
|