@polymorphism-tech/morph-spec 8.43.0 → 8.45.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 +16 -13
- package/README.md +10 -10
- package/bin/morph-spec.js +10 -13
- package/bin/validate.js +379 -379
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +2 -4
- package/docs/QUICKSTART.md +6 -6
- package/framework/CLAUDE.md +6 -8
- package/framework/MORPH.md +39 -45
- package/framework/agents.json +8 -9
- package/framework/commands/morph-apply.md +1 -3
- package/framework/commands/morph-hotfix.md +1 -1
- package/framework/commands/morph-proposal.md +2 -2
- package/framework/docs/CLI.md +22 -17
- package/framework/evals/review-checklist.md +90 -0
- package/framework/evals/trace-schema.md +2 -12
- package/framework/hooks/README.md +2 -3
- package/framework/hooks/claude-code/core/heavy-gate.js +1 -1
- package/framework/hooks/claude-code/core/protect-spec-files.js +2 -2
- package/framework/hooks/claude-code/core/telemetry-log.js +2 -2
- package/framework/hooks/claude-code/core/trace-autogen.js +1 -22
- package/framework/hooks/claude-code/core/validate-completion.js +3 -56
- package/framework/hooks/claude-code/core/worktree-env-inject.js +1 -4
- package/framework/hooks/claude-code/statusline.sh +11 -11
- package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
- package/framework/hooks/git/pre-commit/agents.sh +25 -25
- package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
- package/framework/hooks/git/pre-commit/specs.sh +50 -50
- package/framework/hooks/git/pre-push/run-tests.sh +44 -44
- package/framework/hooks/shared/build-check.js +1 -1
- package/framework/hooks/shared/comment-scanner.js +1 -1
- package/framework/hooks/shared/phase-utils.js +0 -5
- package/framework/hooks/shared/stdin-reader.js +26 -26
- package/framework/phases.json +4 -6
- package/framework/rubrics/backend.json +1 -1
- package/framework/rubrics/default.json +1 -1
- package/framework/rubrics/frontend.json +1 -1
- package/framework/rules/ai-agents-standards.md +1 -0
- package/framework/rules/morph-workflow.md +6 -6
- package/framework/schemas/feature.schema.json +2 -28
- package/framework/schemas/tasks.schema.json +6 -2
- package/framework/skills/README.md +1 -2
- 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 +145 -246
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +5 -3
- package/framework/skills/core/morph-implement/SKILL.md +101 -201
- package/framework/skills/core/morph-machine/SKILL.md +3 -3
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +171 -287
- package/framework/skills/core/morph-proposal/SKILL.md +4 -5
- package/framework/skills/core/morph-review/SKILL.md +109 -430
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +2 -2
- package/framework/skills/core/morph-uiux/SKILL.md +2 -3
- package/framework/standards/STANDARDS.json +54 -10
- package/framework/standards/ai-agents/agent-archetypes.md +1 -1
- package/framework/standards/ai-agents/agent-session.md +1 -1
- package/framework/standards/ai-agents/agent-spec.md +1 -1
- package/framework/standards/ai-agents/batching.md +1 -1
- package/framework/standards/ai-agents/context-providers.md +1 -1
- package/framework/standards/ai-agents/conversational-agent-with-phases.md +1 -1
- package/framework/standards/ai-agents/cost-and-budget.md +1 -1
- package/framework/standards/ai-agents/durable-workflows-hitl.md +1 -1
- package/framework/standards/ai-agents/guardrails.md +1 -1
- package/framework/standards/ai-agents/llm-runtime-defaults.md +1 -1
- package/framework/standards/ai-agents/mcp-server.md +1 -1
- package/framework/standards/ai-agents/mcp-tools.md +1 -1
- package/framework/standards/ai-agents/media-pipeline.md +1 -1
- package/framework/standards/ai-agents/media-video.md +1 -1
- package/framework/standards/ai-agents/middleware-patterns.md +1 -1
- package/framework/standards/ai-agents/modalities-image-gen.md +1 -1
- package/framework/standards/ai-agents/modalities-speech-to-text.md +1 -1
- package/framework/standards/ai-agents/modalities-text-to-speech.md +1 -1
- package/framework/standards/ai-agents/modalities-vision.md +1 -1
- package/framework/standards/ai-agents/multi-agent-patterns.md +1 -1
- package/framework/standards/ai-agents/observability-patterns.md +3 -3
- package/framework/standards/ai-agents/production.md +2 -2
- package/framework/standards/ai-agents/prompt-sources.md +1 -1
- package/framework/standards/ai-agents/providers/model-registry.md +1 -1
- package/framework/standards/ai-agents/rag-custom-pgvector.md +1 -1
- package/framework/standards/ai-agents/service-tier-flex.md +1 -1
- 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/ai-agents/testing-ai.md +1 -1
- package/framework/standards/ai-agents/tool-adapters.md +226 -0
- package/framework/standards/ai-agents/vector-data-extensions.md +1 -1
- package/framework/standards/ai-agents/workflows.md +1 -1
- package/framework/standards/infrastructure/docker/local-compose-isolation.md +11 -1
- package/framework/templates/CLAUDE.md.core.template +1 -1
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/README.md +1 -1
- package/framework/templates/REGISTRY.json +0 -81
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
- package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/docs/spec.md +19 -205
- package/framework/templates/docs/ui-components.md +52 -52
- package/framework/templates/docs/ui-design-system.md +76 -76
- package/framework/templates/docs/ui-flows.md +167 -167
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/github/README.md +56 -56
- package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
- package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
- package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
- package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
- package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
- package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
- package/framework/templates/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- package/package.json +2 -2
- package/src/commands/e2e/e2e.js +1 -1
- package/src/commands/project/heavy.js +1 -1
- package/src/commands/project/status.js +3 -3
- package/src/commands/project/worktree.js +34 -16
- package/src/commands/state/approve.js +9 -50
- package/src/commands/state/finish.js +4 -0
- package/src/commands/state/gate-check.js +18 -107
- package/src/commands/state/gate-decision.js +1 -1
- package/src/commands/state/report-check.js +42 -17
- package/src/commands/validation/mutate.js +1 -1
- package/src/commands/validation/scout-check.js +28 -0
- package/src/commands/validation/validate-feature.js +5 -32
- package/src/commands/validation/verify.js +16 -25
- package/src/core/paths/output-schema.js +0 -2
- package/src/core/state/state-manager.js +1 -2
- package/src/core/templates/template-renderer.js +477 -477
- package/src/dashboard/app/lib/format.js +5 -0
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/FeatureDetail.jsx +16 -22
- package/src/dashboard/app/pages/Features.jsx +7 -7
- package/src/dashboard/app/pages/PhasePipeline.jsx +1 -1
- package/src/dashboard/app/pages/Rubrics.jsx +7 -11
- package/src/dashboard/scanner/docs-scanner.js +1 -2
- package/src/dashboard/scanner/features-scanner.js +3 -4
- package/src/dashboard/scanner/rubrics-scanner.js +12 -12
- package/src/dashboard/vite.config.js +24 -24
- package/src/lib/agents/output-collector.js +0 -1
- package/src/lib/detectors/design-system-detector.js +188 -188
- package/src/lib/e2e/compose-env.js +5 -2
- package/src/lib/evaluator-report.js +52 -334
- package/src/lib/generators/design-system-generator.js +508 -508
- package/src/lib/generators/handoff-generator.js +4 -5
- package/src/lib/git/evidence-staleness.js +39 -0
- package/src/lib/installers/memory-installer.js +2 -2
- package/src/lib/mutation/manifest.js +8 -2
- package/src/lib/mutation/report.js +5 -3
- package/src/lib/mutation/runner.js +9 -7
- package/src/lib/risk-detector.js +63 -125
- package/src/lib/scout/scout-check.js +293 -0
- package/src/lib/tasks/eval-node.js +4 -34
- package/src/lib/tasks/task-classifier.js +30 -30
- package/src/lib/tasks/task-parser.js +209 -209
- package/src/lib/tasks/tasks-json-parser.js +7 -5
- package/src/lib/tasks/test-nodes.js +1 -1
- package/src/lib/tasks/test-runner.js +1 -1
- package/src/lib/validators/contracts/contract-compliance-validator.js +4 -42
- package/src/lib/validators/css/css-validator.js +340 -340
- package/src/lib/validators/reuse/reuse-validator.js +6 -8
- package/src/lib/validators/verify-runner.js +1 -1
- package/src/lib/worktree/env-allocator.js +35 -5
- package/src/scripts/setup-infra.js +1 -1
- package/src/utils/agent-id-mapper.js +11 -11
- package/src/utils/agents-installer.js +5 -9
- package/src/utils/claude-settings-manager.js +300 -300
- package/src/utils/hooks-installer.js +3 -10
- package/src/utils/skills-installer.js +1 -1
- package/framework/evals/proof-rubrics/README.md +0 -76
- package/framework/evals/proof-rubrics/eval-de-modelo.md +0 -80
- package/framework/evals/proof-rubrics/fiacao.md +0 -54
- package/framework/evals/proof-rubrics/guard-cego.md +0 -52
- package/framework/evals/proof-rubrics/media-claims.md +0 -109
- package/framework/evals/proof-rubrics/mutation-proof.md +0 -107
- package/framework/evals/proof-rubrics/observabilidade-genai.md +0 -119
- package/framework/evals/proof-rubrics/verde-nao-e-coberto.md +0 -48
- package/framework/evals/rubrics/README.md +0 -45
- package/framework/evals/rubrics/architecture.md +0 -103
- package/framework/evals/rubrics/code-quality.md +0 -98
- package/framework/evals/rubrics/condensed.md +0 -57
- package/framework/evals/rubrics/contracts.md +0 -67
- package/framework/evals/rubrics/test-coverage.md +0 -99
- package/framework/evals/scenarios/dotnet-crud.yaml +0 -42
- package/framework/evals/scenarios/nextjs-auth.yaml +0 -38
- package/framework/evals/suites/standard.yaml +0 -5
- package/framework/hooks/claude-code/core/post-edit-typecheck.js +0 -127
- package/framework/skills/core/morph-eval/SKILL.md +0 -64
- package/framework/templates/.idea/morph-templates.xml +0 -92
- package/framework/templates/.vscode/morph-templates.code-snippets +0 -186
- package/framework/templates/docs/clarifications.md +0 -253
- package/framework/templates/docs/onboarding.md +0 -122
- package/framework/templates/docs/schema-analysis.md +0 -119
- package/framework/templates/docs/user-stories.md +0 -34
- package/src/commands/state/score.js +0 -147
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
|
|
3
|
+
import { join, relative, resolve } from 'path';
|
|
4
|
+
import { workTreeTop } from '../../../framework/hooks/shared/git-paths.js';
|
|
5
|
+
|
|
6
|
+
export const LINE_TOLERANCE = 2;
|
|
7
|
+
|
|
8
|
+
const SPAN = /`([^`\n]+)`/g;
|
|
9
|
+
const SPAN_CITATION = /^(.+\.[A-Za-z0-9]+):(\d+)(?:-(\d+))?$/;
|
|
10
|
+
const BARE_CITATION = /(?<![\w.:\\/-])(?:[A-Za-z]:)?[\w.\\/-]*\w\.[A-Za-z0-9]+:(\d+)(?:-(\d+))?\b/g;
|
|
11
|
+
const GREP_LINE = /^((?:[A-Za-z]:)?[^\s:]+\.[A-Za-z0-9]+)(?::(\d+):|-(\d+)-)(.*)$/;
|
|
12
|
+
const FENCE = /^\s*(```|~~~)/;
|
|
13
|
+
const HEADING = /^\s{0,3}#{1,6}\s+(.*)$/;
|
|
14
|
+
const INFERENCE_HEADING = /inferid|hip[óo]tese|inferred|hypothes/i;
|
|
15
|
+
const INFERENCE_MARKER = /\b(INFERIDO|hip[óo]tese)\b/i;
|
|
16
|
+
const COMMAND_SPAN = /^(rg|grep|git\s+grep|morph-spec\s+graph|findstr|Select-String|sls)\b/;
|
|
17
|
+
const COUNT = /(?:→|->|=>)\s*\d+|\b\d+\s+(?:ocorr[êe]ncias?|arquivos?|linhas?|hits?|resultados?|matches?|files?|lines?)\b/i;
|
|
18
|
+
const PATH_SPAN = /^(?:[\w.-]*[\\/])+[\w.-]+$|^[\w.-]+\.(?:cs|csproj|fs|vb|razor|js|mjs|cjs|ts|tsx|jsx|json|md|py|go|java|sql|ya?ml|xml|props|targets|sln)$/i;
|
|
19
|
+
|
|
20
|
+
const VERDICTS = [
|
|
21
|
+
/\b(?:VIVO|VIVA|MORTO|MORTA|ALIVE|DEAD)\b/,
|
|
22
|
+
/\b(?:c[óo]digo morto|dead code|unused)\b/i,
|
|
23
|
+
/\bsem (?:nenhum )?(?:consumidor|chamador|leitor|escritor|setter|uso|refer[êe]ncia|teste)s?\b/i,
|
|
24
|
+
/(?<!\b(?:execut|rod)\w*\s)\bnenhum[a]? (?:consumidor|chamador|leitor|escritor|setter|uso|refer[êe]ncia|teste|caller|call site)s?\b/i,
|
|
25
|
+
/\bningu[ée]m (?:chama|usa|l[êe]|escreve|grava|consome|referencia)\b/i,
|
|
26
|
+
/\bn[ãa]o (?:é|est[áa]) (?:usad|chamad|lid|referenciad|escrit)/i,
|
|
27
|
+
/\bnunca (?:é )?(?:usad|chamad|lid|escrit)/i,
|
|
28
|
+
/\bn[ãa]o h[áa] (?:consumidor|chamador|leitor|escritor|uso|teste|refer[êe]ncia)/i,
|
|
29
|
+
/\bno (?:callers?|consumers?|readers?|writers?|tests?|usages?)\b/i,
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const WALK_SKIP = new Set(['node_modules', '.git', 'bin', 'obj', 'worktrees', '.worktrees']);
|
|
33
|
+
|
|
34
|
+
function splitLines(text) {
|
|
35
|
+
const lines = text.replace(/^/, '').split(/\r?\n/);
|
|
36
|
+
if (lines.length > 1 && lines[lines.length - 1] === '') lines.pop();
|
|
37
|
+
return lines;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const squash = (s) => s.replace(/\s+/g, ' ').trim();
|
|
41
|
+
|
|
42
|
+
function lineTokens(line) {
|
|
43
|
+
const tokens = [];
|
|
44
|
+
const masked = line.replace(SPAN, (whole, inner, index) => {
|
|
45
|
+
const cite = inner.trim().match(SPAN_CITATION);
|
|
46
|
+
if (cite) tokens.push({ index, cite: { raw: inner.trim(), path: cite[1], start: +cite[2], end: +(cite[3] ?? cite[2]) } });
|
|
47
|
+
else tokens.push({ index, quote: inner });
|
|
48
|
+
return ' '.repeat(whole.length);
|
|
49
|
+
});
|
|
50
|
+
for (const m of masked.matchAll(BARE_CITATION)) {
|
|
51
|
+
const cite = m[0].match(SPAN_CITATION);
|
|
52
|
+
if (cite) tokens.push({ index: m.index, cite: { raw: m[0], path: cite[1], start: +cite[2], end: +(cite[3] ?? cite[2]) } });
|
|
53
|
+
}
|
|
54
|
+
return tokens.sort((a, b) => a.index - b.index);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function citationsOf(line) {
|
|
58
|
+
const tokens = lineTokens(line);
|
|
59
|
+
const out = [];
|
|
60
|
+
tokens.forEach((t, i) => {
|
|
61
|
+
if (!t.cite) return;
|
|
62
|
+
let quote = null;
|
|
63
|
+
for (const next of tokens.slice(i + 1)) {
|
|
64
|
+
if (next.cite) break;
|
|
65
|
+
const q = next.quote.trim();
|
|
66
|
+
if (COMMAND_SPAN.test(q) || PATH_SPAN.test(q)) continue;
|
|
67
|
+
quote = next.quote;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
out.push({ ...t.cite, quote });
|
|
71
|
+
});
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function makeFileLister(root) {
|
|
76
|
+
let files = null;
|
|
77
|
+
return () => {
|
|
78
|
+
if (files) return files;
|
|
79
|
+
try {
|
|
80
|
+
files = execFileSync('git', ['ls-files', '-z', '--cached', '--others', '--exclude-standard'], {
|
|
81
|
+
cwd: root, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true, maxBuffer: 64 * 1024 * 1024,
|
|
82
|
+
}).toString().split('\0').filter(Boolean);
|
|
83
|
+
} catch {
|
|
84
|
+
files = [];
|
|
85
|
+
const walk = (dir) => {
|
|
86
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
87
|
+
if (WALK_SKIP.has(entry.name)) continue;
|
|
88
|
+
const full = join(dir, entry.name);
|
|
89
|
+
if (entry.isDirectory()) walk(full);
|
|
90
|
+
else files.push(relative(root, full).replace(/\\/g, '/'));
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
walk(root);
|
|
94
|
+
}
|
|
95
|
+
return files;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const isFile = (p) => existsSync(p) && statSync(p).isFile();
|
|
100
|
+
|
|
101
|
+
function resolveCitedPath(rawPath, root, listFiles) {
|
|
102
|
+
const p = rawPath.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
103
|
+
if (/^[A-Za-z]:\//.test(p) || p.startsWith('/')) {
|
|
104
|
+
return isFile(p) ? { path: p } : { problem: 'missing-file' };
|
|
105
|
+
}
|
|
106
|
+
const direct = join(root, p);
|
|
107
|
+
if (isFile(direct)) return { path: direct };
|
|
108
|
+
const fold = process.platform === 'win32' ? (s) => s.toLowerCase() : (s) => s;
|
|
109
|
+
const suffix = fold(`/${p}`);
|
|
110
|
+
const candidates = listFiles().filter((f) => fold(`/${f}`).endsWith(suffix));
|
|
111
|
+
if (candidates.length === 1) return { path: join(root, candidates[0]) };
|
|
112
|
+
if (candidates.length > 1) return { problem: 'ambiguous-path', candidates };
|
|
113
|
+
return { problem: 'missing-file' };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function quoteAppears(quote, lines, start, end) {
|
|
117
|
+
const from = Math.max(0, start - 1 - LINE_TOLERANCE);
|
|
118
|
+
const to = Math.min(lines.length, end + LINE_TOLERANCE);
|
|
119
|
+
const window = squash(lines.slice(from, to).join(' '));
|
|
120
|
+
let at = 0;
|
|
121
|
+
for (const fragment of quote.split(/\.\.\.|…/).map(squash).filter(Boolean)) {
|
|
122
|
+
const found = window.indexOf(fragment, at);
|
|
123
|
+
if (found === -1) return false;
|
|
124
|
+
at = found + fragment.length;
|
|
125
|
+
}
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function auditCitation(cite, root, listFiles, readCache) {
|
|
130
|
+
const where = resolveCitedPath(cite.path, root, listFiles);
|
|
131
|
+
if (where.problem === 'ambiguous-path') {
|
|
132
|
+
return { kind: 'ambiguous-path', detail: `${where.candidates.length} arquivos casam: ${where.candidates.slice(0, 5).join(', ')}` };
|
|
133
|
+
}
|
|
134
|
+
if (where.problem) return { kind: 'missing-file', detail: `${cite.path} não existe sob ${root}` };
|
|
135
|
+
if (!readCache.has(where.path)) readCache.set(where.path, splitLines(readFileSync(where.path, 'utf8')));
|
|
136
|
+
const lines = readCache.get(where.path);
|
|
137
|
+
cite.resolved = where.path;
|
|
138
|
+
if (cite.start < 1 || cite.end < cite.start || cite.end > lines.length) {
|
|
139
|
+
return { kind: 'line-out-of-range', detail: `o arquivo tem ${lines.length} linhas` };
|
|
140
|
+
}
|
|
141
|
+
const quotes = cite.quote != null ? [cite.quote] : cite.fence;
|
|
142
|
+
if (!quotes) {
|
|
143
|
+
return cite.listing ? null : { kind: 'missing-quote', detail: 'a citação não traz o trecho literal em crase depois dela' };
|
|
144
|
+
}
|
|
145
|
+
const missing = quotes.find((q) => !quoteAppears(q, lines, cite.start, cite.end));
|
|
146
|
+
if (missing != null) {
|
|
147
|
+
return {
|
|
148
|
+
kind: 'quote-mismatch',
|
|
149
|
+
detail: `trecho ausente nas linhas ${Math.max(1, cite.start - LINE_TOLERANCE)}-${Math.min(lines.length, cite.end + LINE_TOLERANCE)}; linha ${cite.start}: ${squash(lines[cite.start - 1])}`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
cite.verified = true;
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const asMention = (line) => line.replace(SPAN, ' ').replace(/"[^"\n]*"|“[^”\n]*”/g, ' ');
|
|
157
|
+
|
|
158
|
+
const isVerdict = (line) =>
|
|
159
|
+
VERDICTS[0].test(line.replace(SPAN, ' ')) || VERDICTS.slice(1).some((re) => re.test(asMention(line)));
|
|
160
|
+
|
|
161
|
+
const hasSearchCommand = (line) => [...line.matchAll(SPAN)].some((m) => COMMAND_SPAN.test(m[1].trim()));
|
|
162
|
+
|
|
163
|
+
function underSearchCommand(lines, i) {
|
|
164
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
165
|
+
if (hasSearchCommand(lines[j])) return true;
|
|
166
|
+
if (HEADING.test(lines[j])) return false;
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function auditVerdict(line, evidence, inInference) {
|
|
172
|
+
if (!inInference && !INFERENCE_MARKER.test(line)) {
|
|
173
|
+
return { kind: 'verdict-outside-inference', detail: 'veredito de ausência/vida fora da seção de hipóteses (Inferido)' };
|
|
174
|
+
}
|
|
175
|
+
const spans = [...evidence.matchAll(SPAN)].map((m) => m[1].trim());
|
|
176
|
+
if (!spans.some((s) => COMMAND_SPAN.test(s))) {
|
|
177
|
+
return { kind: 'verdict-without-command', detail: 'hipótese de ausência/vida sem o comando de busca usado, em crase' };
|
|
178
|
+
}
|
|
179
|
+
if (!COUNT.test(evidence)) return { kind: 'verdict-without-count', detail: 'hipótese de ausência/vida sem a contagem bruta do comando' };
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function fenceAfter(lines, i) {
|
|
184
|
+
let j = i + 1;
|
|
185
|
+
while (j < lines.length && lines[j].trim() === '') j++;
|
|
186
|
+
if (j >= lines.length || !FENCE.test(lines[j])) return null;
|
|
187
|
+
const body = [];
|
|
188
|
+
for (let k = j + 1; k < lines.length && !FENCE.test(lines[k]); k++) {
|
|
189
|
+
const text = lines[k].replace(/^\s*:?\d+[:\s]\s*/, '').trim();
|
|
190
|
+
if (text) body.push(text);
|
|
191
|
+
}
|
|
192
|
+
return body.length ? body : null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const indentOf = (line) => line.match(/^\s*/)[0].length;
|
|
196
|
+
const BULLET = /^\s*([-*+]|\d+[.)])\s/;
|
|
197
|
+
|
|
198
|
+
function itemStart(lines, i) {
|
|
199
|
+
let start = i;
|
|
200
|
+
for (let j = i - 1; j >= 0 && lines[j].trim() !== '' && !HEADING.test(lines[j]); j--) {
|
|
201
|
+
if (BULLET.test(lines[j]) && indentOf(lines[j]) < indentOf(lines[start])) start = j;
|
|
202
|
+
}
|
|
203
|
+
return start;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function continuationEnd(lines, i) {
|
|
207
|
+
const indent = indentOf(lines[i]);
|
|
208
|
+
let j = i + 1;
|
|
209
|
+
while (j < lines.length && lines[j].trim() !== '' && !HEADING.test(lines[j]) && !FENCE.test(lines[j])) {
|
|
210
|
+
if (indentOf(lines[j]) <= indent && BULLET.test(lines[j])) break;
|
|
211
|
+
j++;
|
|
212
|
+
}
|
|
213
|
+
return j;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function resolveScoutRoot(root, cwd = process.cwd()) {
|
|
217
|
+
return root ? resolve(cwd, root) : (workTreeTop(cwd) ?? cwd);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function checkScoutMap(text, { root }) {
|
|
221
|
+
const lines = splitLines(text);
|
|
222
|
+
const listFiles = makeFileLister(root);
|
|
223
|
+
const readCache = new Map();
|
|
224
|
+
const failures = [];
|
|
225
|
+
const citations = [];
|
|
226
|
+
let verdicts = 0;
|
|
227
|
+
let inFence = false;
|
|
228
|
+
let inferenceLevel = null;
|
|
229
|
+
let subsectionStart = null;
|
|
230
|
+
|
|
231
|
+
const audit = (cite, lineNo) => {
|
|
232
|
+
citations.push(cite);
|
|
233
|
+
const problem = auditCitation(cite, root, listFiles, readCache);
|
|
234
|
+
if (problem) failures.push({ line: lineNo, citation: cite.raw, cite, ...problem });
|
|
235
|
+
};
|
|
236
|
+
const subsectionText = (from) => {
|
|
237
|
+
let to = from + 1;
|
|
238
|
+
while (to < lines.length && !HEADING.test(lines[to])) to++;
|
|
239
|
+
return lines.slice(from, to).join('\n');
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
lines.forEach((line, i) => {
|
|
243
|
+
const lineNo = i + 1;
|
|
244
|
+
if (FENCE.test(line)) { inFence = !inFence; return; }
|
|
245
|
+
if (inFence) {
|
|
246
|
+
const grep = line.trim().match(GREP_LINE);
|
|
247
|
+
if (grep) {
|
|
248
|
+
const n = +(grep[2] ?? grep[3]);
|
|
249
|
+
audit({ raw: `${grep[1]}:${n}`, path: grep[1], start: n, end: n, quote: grep[4].trim() || null }, lineNo);
|
|
250
|
+
}
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const heading = line.match(HEADING);
|
|
254
|
+
if (heading) {
|
|
255
|
+
const level = line.trim().match(/^#+/)[0].length;
|
|
256
|
+
if (INFERENCE_HEADING.test(heading[1])) { inferenceLevel = level; subsectionStart = null; }
|
|
257
|
+
else if (inferenceLevel != null && level > inferenceLevel) subsectionStart = i;
|
|
258
|
+
else { inferenceLevel = null; subsectionStart = null; }
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const cites = citationsOf(line);
|
|
262
|
+
const residue = cites.reduce((rest, c) => rest.split(c.raw).join(' '), line)
|
|
263
|
+
.replace(/[`*_|>()[\]\-+:;,.—–\s]|\d+[.)]/g, '');
|
|
264
|
+
for (const cite of cites) {
|
|
265
|
+
if (cite.quote == null) {
|
|
266
|
+
cite.fence = fenceAfter(lines, i);
|
|
267
|
+
cite.listing = residue === '' && underSearchCommand(lines, i);
|
|
268
|
+
}
|
|
269
|
+
audit(cite, lineNo);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (!isVerdict(line)) return;
|
|
273
|
+
verdicts++;
|
|
274
|
+
const evidence = subsectionStart != null
|
|
275
|
+
? subsectionText(subsectionStart)
|
|
276
|
+
: lines.slice(itemStart(lines, i), continuationEnd(lines, itemStart(lines, i))).join('\n');
|
|
277
|
+
const verdict = auditVerdict(line, evidence, inferenceLevel != null);
|
|
278
|
+
if (verdict) failures.push({ line: lineNo, text: squash(line), ...verdict });
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const covered = (c) => citations.some((v) => v.verified && v.resolved === c.resolved && v.start <= c.start && c.end <= v.end);
|
|
282
|
+
const kept = failures.filter((f) => !(f.kind === 'missing-quote' && covered(f.cite)));
|
|
283
|
+
if (citations.length === 0) {
|
|
284
|
+
kept.push({ line: null, kind: 'no-citations', detail: 'o mapa não cita nenhum arquivo:linha — nada nele é verificável' });
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
ok: kept.length === 0,
|
|
288
|
+
root,
|
|
289
|
+
citations: citations.map(({ raw, resolved, quote }) => ({ raw, resolved: resolved ?? null, quote })),
|
|
290
|
+
verdicts,
|
|
291
|
+
failures: kept.map(({ cite, ...f }) => f),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
@@ -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,30 +1,30 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Task classifier — identifies test/validation and infrastructure tasks by title.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
const TEST_TASK_PATTERN = /\b(tests?|testing|specs?|coverage|e2e|unit|integration|integra[cç][aã]o|valida[cç][aã]o|cobertura|testes?)\b/i;
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Returns true if the task title indicates a testing or validation task.
|
|
9
|
-
*
|
|
10
|
-
* @param {string} taskTitle
|
|
11
|
-
* @returns {boolean}
|
|
12
|
-
*/
|
|
13
|
-
export function isTestTask(taskTitle) {
|
|
14
|
-
if (!taskTitle) return false;
|
|
15
|
-
return TEST_TASK_PATTERN.test(taskTitle);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const INFRA_TASK_PATTERN = /\b(infrastructure|infra|cleanup|migration|migra[cç][aã]o|config|setup|ci\/cd|devops|deploy|docker|packages?|dependencies|tags)\b/i;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Returns true if the task title indicates an infrastructure/cleanup/migration task.
|
|
22
|
-
* Used to skip contract validation and suppress breaking change warnings.
|
|
23
|
-
*
|
|
24
|
-
* @param {string} taskTitle
|
|
25
|
-
* @returns {boolean}
|
|
26
|
-
*/
|
|
27
|
-
export function isInfrastructureTask(taskTitle) {
|
|
28
|
-
if (!taskTitle) return false;
|
|
29
|
-
return INFRA_TASK_PATTERN.test(taskTitle);
|
|
30
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Task classifier — identifies test/validation and infrastructure tasks by title.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const TEST_TASK_PATTERN = /\b(tests?|testing|specs?|coverage|e2e|unit|integration|integra[cç][aã]o|valida[cç][aã]o|cobertura|testes?)\b/i;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Returns true if the task title indicates a testing or validation task.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} taskTitle
|
|
11
|
+
* @returns {boolean}
|
|
12
|
+
*/
|
|
13
|
+
export function isTestTask(taskTitle) {
|
|
14
|
+
if (!taskTitle) return false;
|
|
15
|
+
return TEST_TASK_PATTERN.test(taskTitle);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const INFRA_TASK_PATTERN = /\b(infrastructure|infra|cleanup|migration|migra[cç][aã]o|config|setup|ci\/cd|devops|deploy|docker|packages?|dependencies|tags)\b/i;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Returns true if the task title indicates an infrastructure/cleanup/migration task.
|
|
22
|
+
* Used to skip contract validation and suppress breaking change warnings.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} taskTitle
|
|
25
|
+
* @returns {boolean}
|
|
26
|
+
*/
|
|
27
|
+
export function isInfrastructureTask(taskTitle) {
|
|
28
|
+
if (!taskTitle) return false;
|
|
29
|
+
return INFRA_TASK_PATTERN.test(taskTitle);
|
|
30
|
+
}
|