@polymorphism-tech/morph-spec 8.43.0 → 8.44.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 -4
- 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/docs/CLI.md +13 -8
- package/framework/hooks/claude-code/core/worktree-env-inject.js +1 -4
- package/framework/rules/ai-agents-standards.md +1 -0
- 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-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +1 -1
- package/framework/skills/core/morph-machine/SKILL.md +1 -1
- 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-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-stack-scan/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 +45 -1
- package/framework/standards/ai-agents/tool-adapters.md +226 -0
- package/framework/standards/infrastructure/docker/local-compose-isolation.md +11 -1
- package/package.json +1 -1
- package/src/commands/project/worktree.js +34 -16
- package/src/commands/state/approve.js +2 -1
- package/src/commands/state/finish.js +4 -0
- package/src/commands/state/gate-check.js +2 -1
- package/src/commands/validation/verify.js +13 -21
- package/src/lib/e2e/compose-env.js +5 -2
- package/src/lib/git/evidence-staleness.js +39 -0
- package/src/lib/risk-detector.js +3 -3
- package/src/lib/tasks/eval-node.js +4 -34
- package/src/lib/validators/contracts/contract-compliance-validator.js +4 -42
- package/src/lib/worktree/env-allocator.js +35 -5
package/src/lib/risk-detector.js
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
* - e2eStatus + e2eReason + gate — nó e2e do último verify. Um `skip`
|
|
16
16
|
* cuja razão NÃO seja dispensa legítima pausa SÓ no ship (gate 'review').
|
|
17
17
|
* Sem escopo (stamp sem o campo) cai do mesmo lado seguro que `unknown`
|
|
18
|
-
* - verificationStale + gate: bool/str — stamp
|
|
19
|
-
* ship (gate 'review')
|
|
18
|
+
* - verificationStale + gate: bool/str — stamp de um commit cujo código já mudou
|
|
19
|
+
* (fora de .morph/) pausa SÓ no ship (gate 'review')
|
|
20
20
|
* - evalScoreSource + gate: str — no ship, score que NÃO veio do avaliador
|
|
21
21
|
* independente pausa (auto-avaliação não decide o Gate 3)
|
|
22
22
|
* - mutationProof + gate: object — { section, min, productionCode } lido do
|
|
@@ -274,7 +274,7 @@ export function shouldPauseGate(config, signals) {
|
|
|
274
274
|
if (signals.gate === 'review' && signals.verificationStale === true) {
|
|
275
275
|
return {
|
|
276
276
|
pause: true,
|
|
277
|
-
reason: 'verification stamp desatualizado (
|
|
277
|
+
reason: 'verification stamp desatualizado (o código mudou depois do commit do stamp) — rode `morph-spec verify` de novo antes do ship',
|
|
278
278
|
};
|
|
279
279
|
}
|
|
280
280
|
|
|
@@ -36,7 +36,7 @@ import { join, resolve, sep } from 'path';
|
|
|
36
36
|
import { tmpdir } from 'os';
|
|
37
37
|
|
|
38
38
|
import { isInside } from './test-runner.js';
|
|
39
|
-
import {
|
|
39
|
+
import { resolveAiPin } from '../ai/pin-drift.js';
|
|
40
40
|
|
|
41
41
|
/** The runner kind. One spelling, imported by every reader. */
|
|
42
42
|
export const EVAL_RUNNER = 'evals';
|
|
@@ -72,34 +72,6 @@ export function cacheRelevantPackages(packages) {
|
|
|
72
72
|
return out;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
/**
|
|
76
|
-
* Read the pin from a project, wherever the framework lives.
|
|
77
|
-
*
|
|
78
|
-
* Two roots, ONE reader: this repository carries `framework/ai-pin.json` at the
|
|
79
|
-
* top; an installed project carries it under `.morph/framework/`. `readAiPin`
|
|
80
|
-
* appends `framework/ai-pin.json` to whatever root it is given, so both cases
|
|
81
|
-
* are the same function with a different argument — never a second parser.
|
|
82
|
-
*
|
|
83
|
-
* Returns null when neither exists: a project with no pin simply has no stamp
|
|
84
|
-
* ruler, and refusing to run there would make the eval node unusable outside
|
|
85
|
-
* this framework's own stack.
|
|
86
|
-
*
|
|
87
|
-
* @param {string} projectPath
|
|
88
|
-
* @returns {{pinnedAt: string, packages: object}|null}
|
|
89
|
-
*/
|
|
90
|
-
export function readProjectPin(projectPath) {
|
|
91
|
-
for (const root of [projectPath, join(projectPath, '.morph')]) {
|
|
92
|
-
try {
|
|
93
|
-
return readAiPin(root);
|
|
94
|
-
} catch {
|
|
95
|
-
// Next candidate. The throw is readAiPin's contract for "no pin here",
|
|
96
|
-
// and it is the reason this loop exists instead of an existsSync probe
|
|
97
|
-
// that would duplicate the path knowledge.
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
75
|
/**
|
|
104
76
|
* Resolve every path an eval node names, against the project root.
|
|
105
77
|
*
|
|
@@ -256,11 +228,9 @@ export function evalPreflight(projectPath, node, options = {}) {
|
|
|
256
228
|
return { status: 'fail', reason: `eval-cache-empty: ${cacheRoot} não contém nenhuma entrada de cache` };
|
|
257
229
|
}
|
|
258
230
|
|
|
259
|
-
const pin =
|
|
231
|
+
const pin = resolveAiPin(projectPath);
|
|
260
232
|
if (!pin) {
|
|
261
|
-
// No pin
|
|
262
|
-
// failure: the stamp check simply does not apply. Saying so is the point;
|
|
263
|
-
// silently treating it as "stamp ok" would be the same thing said badly.
|
|
233
|
+
// No pin in the project or in the CLI package: nothing to stamp against.
|
|
264
234
|
return { status: null, reason: null };
|
|
265
235
|
}
|
|
266
236
|
|
|
@@ -315,7 +285,7 @@ export function evalNodeEnv(node, projectPath, options = {}) {
|
|
|
315
285
|
MORPH_EVAL_ENVFILE: envFile,
|
|
316
286
|
};
|
|
317
287
|
|
|
318
|
-
const pin =
|
|
288
|
+
const pin = resolveAiPin(projectPath);
|
|
319
289
|
if (pin) {
|
|
320
290
|
// Compact, and only the packages that decide the cache format. The fixture
|
|
321
291
|
// stamps from THIS, so it never has to read (or parse) the pin itself.
|
|
@@ -1,56 +1,21 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Contract Compliance Validator
|
|
3
|
-
*
|
|
4
|
-
* Parses contracts.cs from feature outputs and verifies that the project code
|
|
5
|
-
* actually implements all defined interfaces, methods, DTOs, and enums.
|
|
6
|
-
*
|
|
7
|
-
* MORPH-SPEC 3.0 - Phase A: Active Validation Pipeline
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
1
|
import { readFileSync, existsSync } from 'fs';
|
|
11
2
|
import { join } from 'path';
|
|
12
3
|
import { glob } from 'glob';
|
|
13
4
|
import chalk from 'chalk';
|
|
14
5
|
import { getAbsoluteOutputPath, resolveContractsOutputType } from '../../../core/paths/output-schema.js';
|
|
15
6
|
|
|
16
|
-
/**
|
|
17
|
-
* Validate that contracts.cs interfaces are implemented in project code
|
|
18
|
-
*
|
|
19
|
-
* @param {string} projectPath - Project root path
|
|
20
|
-
* @param {string} featureName - Feature name
|
|
21
|
-
* @param {Object} options - Options
|
|
22
|
-
* @returns {Object} { status, errors, warnings, issues, coverage }
|
|
23
|
-
*/
|
|
24
7
|
export async function validateContracts(projectPath, featureName, options = {}) {
|
|
25
|
-
// `'contracts'` is not an output type — `getAbsoluteOutputPath` threw
|
|
26
|
-
// "Unknown output type" on every call. The runner already resolves this the
|
|
27
|
-
// right way before deciding to schedule this validator
|
|
28
|
-
// (`resolveContractsOutputType()`); using the same resolver here is what
|
|
29
|
-
// keeps the two from disagreeing again.
|
|
30
8
|
const contractsPath = getAbsoluteOutputPath(projectPath, featureName, resolveContractsOutputType());
|
|
31
9
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
errors: 0,
|
|
36
|
-
warnings: 0,
|
|
37
|
-
issues: [],
|
|
38
|
-
coverage: null,
|
|
39
|
-
message: 'No contracts.cs found — skipping'
|
|
40
|
-
};
|
|
41
|
-
}
|
|
10
|
+
const skipped = (message) => ({ status: 'ok', errors: 0, warnings: 0, issues: [], coverage: null, message });
|
|
11
|
+
|
|
12
|
+
if (!existsSync(contractsPath)) return skipped('No spec.md found — skipping');
|
|
42
13
|
|
|
43
14
|
const contractsContent = readFileSync(contractsPath, 'utf-8');
|
|
44
15
|
const contracts = parseContracts(contractsContent);
|
|
45
16
|
|
|
46
17
|
if (contracts.interfaces.length === 0 && contracts.records.length === 0 && contracts.enums.length === 0) {
|
|
47
|
-
return
|
|
48
|
-
status: 'ok',
|
|
49
|
-
errors: 0,
|
|
50
|
-
warnings: 1,
|
|
51
|
-
issues: [{ level: 'warning', message: 'contracts.cs exists but no interfaces/records/enums found' }],
|
|
52
|
-
coverage: null
|
|
53
|
-
};
|
|
18
|
+
return skipped('spec.md declares no interfaces/records/enums — skipping');
|
|
54
19
|
}
|
|
55
20
|
|
|
56
21
|
// Find all .cs files in the project (excluding .morph outputs, bin, obj)
|
|
@@ -168,9 +133,6 @@ export async function validateContracts(projectPath, featureName, options = {})
|
|
|
168
133
|
};
|
|
169
134
|
}
|
|
170
135
|
|
|
171
|
-
/**
|
|
172
|
-
* Parse contracts.cs content and extract interfaces, records, and enums
|
|
173
|
-
*/
|
|
174
136
|
function parseContracts(content) {
|
|
175
137
|
const interfaces = [];
|
|
176
138
|
const records = [];
|
|
@@ -168,10 +168,10 @@ export const PORT_OFFSETS = Object.freeze({ web: 0, api: 1, postgres: 2, redis:
|
|
|
168
168
|
* not in the root's. Every value is a string (env-safe); key order is stable.
|
|
169
169
|
*
|
|
170
170
|
* @param {{ base: number, range: [number, number] }} block
|
|
171
|
-
* @param {{ stack?: string, feature?: string }} [opts]
|
|
171
|
+
* @param {{ stack?: string, feature?: string, aliases?: Record<string, string> }} [opts] - aliases: resolvePortAliases()
|
|
172
172
|
* @returns {Record<string,string>}
|
|
173
173
|
*/
|
|
174
|
-
export function portLayout(block, { stack = '', feature = '' } = {}) {
|
|
174
|
+
export function portLayout(block, { stack = '', feature = '', aliases = {} } = {}) {
|
|
175
175
|
const env = {};
|
|
176
176
|
if (feature) env.COMPOSE_PROJECT_NAME = composeProjectName(feature);
|
|
177
177
|
env.MORPH_PORT_BASE = String(block.base);
|
|
@@ -183,9 +183,39 @@ export function portLayout(block, { stack = '', feature = '' } = {}) {
|
|
|
183
183
|
if (/\bdotnet\b/i.test(String(stack))) {
|
|
184
184
|
env.ASPNETCORE_URLS = `http://localhost:${block.base + PORT_OFFSETS.api}`;
|
|
185
185
|
}
|
|
186
|
+
for (const [alias, port] of Object.entries(aliases)) env[alias] = String(block.base + PORT_OFFSETS[port]);
|
|
186
187
|
return env;
|
|
187
188
|
}
|
|
188
189
|
|
|
190
|
+
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
191
|
+
const RESERVED_ENV_NAMES = new Set([
|
|
192
|
+
...Object.keys(portLayout({ base: 0, range: [0, 0] }, { stack: 'dotnet', feature: 'x' })),
|
|
193
|
+
'PLAYWRIGHT_BASE_URL',
|
|
194
|
+
'MORPH_FEATURE',
|
|
195
|
+
]);
|
|
196
|
+
|
|
197
|
+
// e2e.portEnvNames: extra env names for the same four ports, so a compose file reading
|
|
198
|
+
// ${PG_PORT} is fed by the block, not by its own default. An entry that cannot be
|
|
199
|
+
// honoured is dropped and named in `problems`.
|
|
200
|
+
export function resolvePortAliases(config) {
|
|
201
|
+
const declared = config?.e2e?.portEnvNames;
|
|
202
|
+
const aliases = {};
|
|
203
|
+
const problems = [];
|
|
204
|
+
if (declared === undefined || declared === null) return { aliases, problems };
|
|
205
|
+
if (typeof declared !== 'object' || Array.isArray(declared)) {
|
|
206
|
+
return { aliases, problems: [`e2e.portEnvNames deve ser um objeto { ${Object.keys(PORT_OFFSETS).join('|')}: "NOME" }`] };
|
|
207
|
+
}
|
|
208
|
+
for (const [port, name] of Object.entries(declared)) {
|
|
209
|
+
const at = `e2e.portEnvNames.${port}`;
|
|
210
|
+
if (!Object.hasOwn(PORT_OFFSETS, port)) problems.push(`${at}: porta desconhecida (use ${Object.keys(PORT_OFFSETS).join('|')})`);
|
|
211
|
+
else if (typeof name !== 'string' || !ENV_NAME.test(name)) problems.push(`${at}: ${JSON.stringify(name)} não é um nome de variável de ambiente válido`);
|
|
212
|
+
else if (RESERVED_ENV_NAMES.has(name)) problems.push(`${at}: "${name}" já é uma variável do layout`);
|
|
213
|
+
else if (Object.hasOwn(aliases, name)) problems.push(`${at}: "${name}" já é o alias de ${aliases[name]}`);
|
|
214
|
+
else aliases[name] = port;
|
|
215
|
+
}
|
|
216
|
+
return { aliases, problems };
|
|
217
|
+
}
|
|
218
|
+
|
|
189
219
|
/**
|
|
190
220
|
* Render the `.morph/worktree.env` contents for a block: a header, the
|
|
191
221
|
* feature, and exactly the `portLayout()` pairs — nothing else, so the file
|
|
@@ -199,15 +229,15 @@ export function portLayout(block, { stack = '', feature = '' } = {}) {
|
|
|
199
229
|
* @param {string} feature - the env-registry key: `<feature>` or `task/<name>`
|
|
200
230
|
* @param {{ base: number, range: [number, number] }} block
|
|
201
231
|
* @param {string} [stack] - config.json `project.stack` (e.g. "nextjs+dotnet")
|
|
202
|
-
* @param {{ morphFeature?: string|null }} [opts]
|
|
232
|
+
* @param {{ morphFeature?: string|null, aliases?: Record<string, string> }} [opts]
|
|
203
233
|
* @returns {string}
|
|
204
234
|
*/
|
|
205
|
-
export function buildEnvFileContent(feature, block, stack = '', { morphFeature = null } = {}) {
|
|
235
|
+
export function buildEnvFileContent(feature, block, stack = '', { morphFeature = null, aliases = {} } = {}) {
|
|
206
236
|
const lines = [
|
|
207
237
|
`# Managed by morph-spec — ambiente dedicado desta worktree (feature: ${feature}).`,
|
|
208
238
|
'# Derivado, git-invisível, regenerado por `worktree setup|link`; nunca editar nem commitar.',
|
|
209
239
|
`MORPH_FEATURE=${morphFeature || feature}`,
|
|
210
|
-
...Object.entries(portLayout(block, { stack, feature })).map(([k, v]) => `${k}=${v}`),
|
|
240
|
+
...Object.entries(portLayout(block, { stack, feature, aliases })).map(([k, v]) => `${k}=${v}`),
|
|
211
241
|
];
|
|
212
242
|
return `${lines.join('\n')}\n`;
|
|
213
243
|
}
|