@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.
Files changed (203) hide show
  1. package/CLAUDE.md +16 -13
  2. package/README.md +10 -10
  3. package/bin/morph-spec.js +10 -13
  4. package/bin/validate.js +379 -379
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +2 -4
  7. package/docs/QUICKSTART.md +6 -6
  8. package/framework/CLAUDE.md +6 -8
  9. package/framework/MORPH.md +39 -45
  10. package/framework/agents.json +8 -9
  11. package/framework/commands/morph-apply.md +1 -3
  12. package/framework/commands/morph-hotfix.md +1 -1
  13. package/framework/commands/morph-proposal.md +2 -2
  14. package/framework/docs/CLI.md +22 -17
  15. package/framework/evals/review-checklist.md +90 -0
  16. package/framework/evals/trace-schema.md +2 -12
  17. package/framework/hooks/README.md +2 -3
  18. package/framework/hooks/claude-code/core/heavy-gate.js +1 -1
  19. package/framework/hooks/claude-code/core/protect-spec-files.js +2 -2
  20. package/framework/hooks/claude-code/core/telemetry-log.js +2 -2
  21. package/framework/hooks/claude-code/core/trace-autogen.js +1 -22
  22. package/framework/hooks/claude-code/core/validate-completion.js +3 -56
  23. package/framework/hooks/claude-code/core/worktree-env-inject.js +1 -4
  24. package/framework/hooks/claude-code/statusline.sh +11 -11
  25. package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
  26. package/framework/hooks/git/pre-commit/agents.sh +25 -25
  27. package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
  28. package/framework/hooks/git/pre-commit/specs.sh +50 -50
  29. package/framework/hooks/git/pre-push/run-tests.sh +44 -44
  30. package/framework/hooks/shared/build-check.js +1 -1
  31. package/framework/hooks/shared/comment-scanner.js +1 -1
  32. package/framework/hooks/shared/phase-utils.js +0 -5
  33. package/framework/hooks/shared/stdin-reader.js +26 -26
  34. package/framework/phases.json +4 -6
  35. package/framework/rubrics/backend.json +1 -1
  36. package/framework/rubrics/default.json +1 -1
  37. package/framework/rubrics/frontend.json +1 -1
  38. package/framework/rules/ai-agents-standards.md +1 -0
  39. package/framework/rules/morph-workflow.md +6 -6
  40. package/framework/schemas/feature.schema.json +2 -28
  41. package/framework/schemas/tasks.schema.json +6 -2
  42. package/framework/skills/README.md +1 -2
  43. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  44. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  45. package/framework/skills/core/morph-apply/SKILL.md +145 -246
  46. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  47. package/framework/skills/core/morph-hotfix/SKILL.md +5 -3
  48. package/framework/skills/core/morph-implement/SKILL.md +101 -201
  49. package/framework/skills/core/morph-machine/SKILL.md +3 -3
  50. package/framework/skills/core/morph-media/SKILL.md +1 -1
  51. package/framework/skills/core/morph-plan/SKILL.md +171 -287
  52. package/framework/skills/core/morph-proposal/SKILL.md +4 -5
  53. package/framework/skills/core/morph-review/SKILL.md +109 -430
  54. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  55. package/framework/skills/core/morph-standards/SKILL.md +2 -2
  56. package/framework/skills/core/morph-uiux/SKILL.md +2 -3
  57. package/framework/standards/STANDARDS.json +54 -10
  58. package/framework/standards/ai-agents/agent-archetypes.md +1 -1
  59. package/framework/standards/ai-agents/agent-session.md +1 -1
  60. package/framework/standards/ai-agents/agent-spec.md +1 -1
  61. package/framework/standards/ai-agents/batching.md +1 -1
  62. package/framework/standards/ai-agents/context-providers.md +1 -1
  63. package/framework/standards/ai-agents/conversational-agent-with-phases.md +1 -1
  64. package/framework/standards/ai-agents/cost-and-budget.md +1 -1
  65. package/framework/standards/ai-agents/durable-workflows-hitl.md +1 -1
  66. package/framework/standards/ai-agents/guardrails.md +1 -1
  67. package/framework/standards/ai-agents/llm-runtime-defaults.md +1 -1
  68. package/framework/standards/ai-agents/mcp-server.md +1 -1
  69. package/framework/standards/ai-agents/mcp-tools.md +1 -1
  70. package/framework/standards/ai-agents/media-pipeline.md +1 -1
  71. package/framework/standards/ai-agents/media-video.md +1 -1
  72. package/framework/standards/ai-agents/middleware-patterns.md +1 -1
  73. package/framework/standards/ai-agents/modalities-image-gen.md +1 -1
  74. package/framework/standards/ai-agents/modalities-speech-to-text.md +1 -1
  75. package/framework/standards/ai-agents/modalities-text-to-speech.md +1 -1
  76. package/framework/standards/ai-agents/modalities-vision.md +1 -1
  77. package/framework/standards/ai-agents/multi-agent-patterns.md +1 -1
  78. package/framework/standards/ai-agents/observability-patterns.md +3 -3
  79. package/framework/standards/ai-agents/production.md +2 -2
  80. package/framework/standards/ai-agents/prompt-sources.md +1 -1
  81. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  82. package/framework/standards/ai-agents/rag-custom-pgvector.md +1 -1
  83. package/framework/standards/ai-agents/service-tier-flex.md +1 -1
  84. package/framework/standards/ai-agents/setup.md +1 -1
  85. package/framework/standards/ai-agents/structured-output.md +1 -1
  86. package/framework/standards/ai-agents/sweet-spot.md +1 -1
  87. package/framework/standards/ai-agents/testing-ai.md +1 -1
  88. package/framework/standards/ai-agents/tool-adapters.md +226 -0
  89. package/framework/standards/ai-agents/vector-data-extensions.md +1 -1
  90. package/framework/standards/ai-agents/workflows.md +1 -1
  91. package/framework/standards/infrastructure/docker/local-compose-isolation.md +11 -1
  92. package/framework/templates/CLAUDE.md.core.template +1 -1
  93. package/framework/templates/IDE-SNIPPETS.md +262 -262
  94. package/framework/templates/README.md +1 -1
  95. package/framework/templates/REGISTRY.json +0 -81
  96. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  97. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
  98. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  99. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  100. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  101. package/framework/templates/docs/spec.md +19 -205
  102. package/framework/templates/docs/ui-components.md +52 -52
  103. package/framework/templates/docs/ui-design-system.md +76 -76
  104. package/framework/templates/docs/ui-flows.md +167 -167
  105. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  106. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  107. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  108. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  109. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  110. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  111. package/framework/templates/infrastructure/github/README.md +56 -56
  112. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  113. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  114. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  115. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  116. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  117. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  118. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
  119. package/framework/templates/state.template.json +17 -17
  120. package/framework/templates/ui/design-system.css +226 -226
  121. package/package.json +2 -2
  122. package/src/commands/e2e/e2e.js +1 -1
  123. package/src/commands/project/heavy.js +1 -1
  124. package/src/commands/project/status.js +3 -3
  125. package/src/commands/project/worktree.js +34 -16
  126. package/src/commands/state/approve.js +9 -50
  127. package/src/commands/state/finish.js +4 -0
  128. package/src/commands/state/gate-check.js +18 -107
  129. package/src/commands/state/gate-decision.js +1 -1
  130. package/src/commands/state/report-check.js +42 -17
  131. package/src/commands/validation/mutate.js +1 -1
  132. package/src/commands/validation/scout-check.js +28 -0
  133. package/src/commands/validation/validate-feature.js +5 -32
  134. package/src/commands/validation/verify.js +16 -25
  135. package/src/core/paths/output-schema.js +0 -2
  136. package/src/core/state/state-manager.js +1 -2
  137. package/src/core/templates/template-renderer.js +477 -477
  138. package/src/dashboard/app/lib/format.js +5 -0
  139. package/src/dashboard/app/main.jsx +13 -13
  140. package/src/dashboard/app/pages/FeatureDetail.jsx +16 -22
  141. package/src/dashboard/app/pages/Features.jsx +7 -7
  142. package/src/dashboard/app/pages/PhasePipeline.jsx +1 -1
  143. package/src/dashboard/app/pages/Rubrics.jsx +7 -11
  144. package/src/dashboard/scanner/docs-scanner.js +1 -2
  145. package/src/dashboard/scanner/features-scanner.js +3 -4
  146. package/src/dashboard/scanner/rubrics-scanner.js +12 -12
  147. package/src/dashboard/vite.config.js +24 -24
  148. package/src/lib/agents/output-collector.js +0 -1
  149. package/src/lib/detectors/design-system-detector.js +188 -188
  150. package/src/lib/e2e/compose-env.js +5 -2
  151. package/src/lib/evaluator-report.js +52 -334
  152. package/src/lib/generators/design-system-generator.js +508 -508
  153. package/src/lib/generators/handoff-generator.js +4 -5
  154. package/src/lib/git/evidence-staleness.js +39 -0
  155. package/src/lib/installers/memory-installer.js +2 -2
  156. package/src/lib/mutation/manifest.js +8 -2
  157. package/src/lib/mutation/report.js +5 -3
  158. package/src/lib/mutation/runner.js +9 -7
  159. package/src/lib/risk-detector.js +63 -125
  160. package/src/lib/scout/scout-check.js +293 -0
  161. package/src/lib/tasks/eval-node.js +4 -34
  162. package/src/lib/tasks/task-classifier.js +30 -30
  163. package/src/lib/tasks/task-parser.js +209 -209
  164. package/src/lib/tasks/tasks-json-parser.js +7 -5
  165. package/src/lib/tasks/test-nodes.js +1 -1
  166. package/src/lib/tasks/test-runner.js +1 -1
  167. package/src/lib/validators/contracts/contract-compliance-validator.js +4 -42
  168. package/src/lib/validators/css/css-validator.js +340 -340
  169. package/src/lib/validators/reuse/reuse-validator.js +6 -8
  170. package/src/lib/validators/verify-runner.js +1 -1
  171. package/src/lib/worktree/env-allocator.js +35 -5
  172. package/src/scripts/setup-infra.js +1 -1
  173. package/src/utils/agent-id-mapper.js +11 -11
  174. package/src/utils/agents-installer.js +5 -9
  175. package/src/utils/claude-settings-manager.js +300 -300
  176. package/src/utils/hooks-installer.js +3 -10
  177. package/src/utils/skills-installer.js +1 -1
  178. package/framework/evals/proof-rubrics/README.md +0 -76
  179. package/framework/evals/proof-rubrics/eval-de-modelo.md +0 -80
  180. package/framework/evals/proof-rubrics/fiacao.md +0 -54
  181. package/framework/evals/proof-rubrics/guard-cego.md +0 -52
  182. package/framework/evals/proof-rubrics/media-claims.md +0 -109
  183. package/framework/evals/proof-rubrics/mutation-proof.md +0 -107
  184. package/framework/evals/proof-rubrics/observabilidade-genai.md +0 -119
  185. package/framework/evals/proof-rubrics/verde-nao-e-coberto.md +0 -48
  186. package/framework/evals/rubrics/README.md +0 -45
  187. package/framework/evals/rubrics/architecture.md +0 -103
  188. package/framework/evals/rubrics/code-quality.md +0 -98
  189. package/framework/evals/rubrics/condensed.md +0 -57
  190. package/framework/evals/rubrics/contracts.md +0 -67
  191. package/framework/evals/rubrics/test-coverage.md +0 -99
  192. package/framework/evals/scenarios/dotnet-crud.yaml +0 -42
  193. package/framework/evals/scenarios/nextjs-auth.yaml +0 -38
  194. package/framework/evals/suites/standard.yaml +0 -5
  195. package/framework/hooks/claude-code/core/post-edit-typecheck.js +0 -127
  196. package/framework/skills/core/morph-eval/SKILL.md +0 -64
  197. package/framework/templates/.idea/morph-templates.xml +0 -92
  198. package/framework/templates/.vscode/morph-templates.code-snippets +0 -186
  199. package/framework/templates/docs/clarifications.md +0 -253
  200. package/framework/templates/docs/onboarding.md +0 -122
  201. package/framework/templates/docs/schema-analysis.md +0 -119
  202. package/framework/templates/docs/user-stories.md +0 -34
  203. 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 { readAiPin } from '../standards/verified-against.js';
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 = readProjectPin(projectPath);
231
+ const pin = resolveAiPin(projectPath);
260
232
  if (!pin) {
261
- // No pin, no ruler — and a project without one is a legitimate case, not a
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 = readProjectPin(projectPath);
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
+ }