@polymorphism-tech/morph-spec 8.47.0 → 9.0.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 (222) hide show
  1. package/CLAUDE.md +48 -39
  2. package/README.md +3 -4
  3. package/bin/morph-spec.js +37 -51
  4. package/bin/validate.js +3 -111
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +1 -1
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +41 -101
  9. package/framework/MORPH.md +95 -252
  10. package/framework/agents.json +75 -18
  11. package/framework/commands/morph-apply.md +5 -28
  12. package/framework/commands/morph-archive.md +1 -1
  13. package/framework/commands/morph-hotfix.md +8 -8
  14. package/framework/commands/morph-infra.md +1 -1
  15. package/framework/commands/morph-preflight.md +2 -3
  16. package/framework/commands/morph-proposal.md +2 -50
  17. package/framework/commands/morph-status.md +3 -3
  18. package/framework/docs/CLI.md +585 -286
  19. package/framework/evals/review-checklist.md +82 -41
  20. package/framework/hooks/README.md +10 -11
  21. package/framework/hooks/claude-code/core/comment-guard.js +1 -1
  22. package/framework/hooks/claude-code/core/denylist.json +33 -3
  23. package/framework/hooks/claude-code/core/enforce-phase-writes.js +16 -20
  24. package/framework/hooks/claude-code/core/gate-guard.js +2 -11
  25. package/framework/hooks/claude-code/core/pre-bash-denylist.js +38 -0
  26. package/framework/hooks/claude-code/core/protect-spec-files.js +30 -242
  27. package/framework/hooks/claude-code/core/state-sync.js +7 -21
  28. package/framework/hooks/claude-code/core/telemetry-log.js +3 -3
  29. package/framework/hooks/claude-code/core/validate-completion.js +33 -97
  30. package/framework/hooks/claude-code/core/warn-bash-source-write.js +31 -0
  31. package/framework/hooks/claude-code/core/worktree-context.js +5 -4
  32. package/framework/hooks/claude-code/statusline.py +55 -41
  33. package/framework/hooks/package.json +3 -0
  34. package/framework/hooks/shared/activity-logger.js +1 -1
  35. package/framework/hooks/shared/build-check.js +10 -6
  36. package/framework/hooks/shared/command-tokens.js +169 -0
  37. package/framework/hooks/shared/comment-rules.js +1 -1
  38. package/framework/hooks/shared/comment-scanner.js +10 -23
  39. package/framework/hooks/shared/comment-stripper.js +1 -1
  40. package/framework/hooks/shared/e2e-check.js +64 -27
  41. package/framework/hooks/shared/errata.js +72 -0
  42. package/framework/hooks/shared/feature-locator.js +1 -1
  43. package/framework/hooks/shared/frontend-locations.js +6 -0
  44. package/framework/hooks/shared/gate-question.js +18 -0
  45. package/framework/hooks/shared/phase-utils.js +15 -32
  46. package/framework/hooks/shared/slice-progress.js +118 -0
  47. package/framework/hooks/shared/spawn-command.js +23 -0
  48. package/framework/hooks/shared/state-reader.js +61 -87
  49. package/framework/hooks/shared/state-writer.js +2 -3
  50. package/framework/mcp/registry.json +3 -4
  51. package/framework/phases.json +23 -39
  52. package/framework/schemas/config.schema.json +14 -9
  53. package/framework/schemas/feature.schema.json +15 -50
  54. package/framework/skills/README.md +3 -4
  55. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  56. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  57. package/framework/skills/core/morph-apply/SKILL.md +149 -222
  58. package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
  59. package/framework/skills/core/morph-machine/SKILL.md +2 -2
  60. package/framework/skills/core/morph-media/SKILL.md +1 -1
  61. package/framework/skills/core/morph-plan/SKILL.md +137 -299
  62. package/framework/skills/core/morph-review/SKILL.md +111 -136
  63. package/framework/skills/core/morph-standards/SKILL.md +27 -39
  64. package/framework/skills/core/morph-uiux/SKILL.md +77 -152
  65. package/framework/standards/STANDARDS.json +86 -1218
  66. package/framework/standards/STANDARDS.meta.json +1305 -0
  67. package/framework/standards/ai-agents/evals-with-cache.md +1 -2
  68. package/framework/standards/ai-agents/setup.md +1 -1
  69. package/framework/standards/architecture/code-comments.md +6 -6
  70. package/framework/standards/integration/mcp/mcp-tools.md +7 -7
  71. package/framework/templates/README.md +2 -3
  72. package/framework/templates/REGISTRY.json +6 -170
  73. package/framework/templates/docs/plan.md +72 -0
  74. package/framework/templates/dotnet/ai-kit/README.md +1 -9
  75. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
  76. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
  77. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
  78. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
  79. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
  80. package/package.json +1 -1
  81. package/src/commands/e2e/e2e.js +2 -2
  82. package/src/commands/observability/cost.js +18 -60
  83. package/src/commands/project/doctor-procs.js +1 -1
  84. package/src/commands/project/doctor.js +9 -9
  85. package/src/commands/project/fleet.js +4 -4
  86. package/src/commands/project/heavy.js +3 -3
  87. package/src/commands/project/init.js +4 -0
  88. package/src/commands/project/setup-infra-cmd.js +2 -1
  89. package/src/commands/project/stack-scan.js +58 -0
  90. package/src/commands/project/standards.js +79 -25
  91. package/src/commands/project/status.js +29 -69
  92. package/src/commands/project/update.js +17 -1
  93. package/src/commands/project/worktree.js +1 -1
  94. package/src/commands/state/advance.js +62 -46
  95. package/src/commands/state/approve.js +81 -141
  96. package/src/commands/state/archive.js +20 -84
  97. package/src/commands/state/auto-approval.js +68 -0
  98. package/src/commands/state/create.js +28 -1
  99. package/src/commands/state/delete.js +1 -1
  100. package/src/commands/state/finish.js +11 -23
  101. package/src/commands/state/gate-check.js +85 -84
  102. package/src/commands/state/gate-decision.js +6 -4
  103. package/src/commands/state/handoff.js +1 -1
  104. package/src/commands/state/list.js +9 -9
  105. package/src/commands/state/report-check.js +157 -311
  106. package/src/commands/state/retype.js +9 -3
  107. package/src/commands/validation/mutate.js +77 -14
  108. package/src/commands/validation/plan-check.js +81 -0
  109. package/src/commands/validation/validate-feature.js +60 -135
  110. package/src/commands/validation/verify.js +40 -72
  111. package/src/core/paths/output-schema.js +19 -66
  112. package/src/core/state/gate-order.js +5 -6
  113. package/src/core/state/lifecycle-audit.js +2 -2
  114. package/src/core/state/state-manager.js +17 -98
  115. package/src/core/templates/template-registry.js +2 -2
  116. package/src/core/templates/template-seed.js +22 -0
  117. package/src/core/work-types.js +10 -10
  118. package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
  119. package/src/dashboard/app/components/CommandPalette.jsx +5 -5
  120. package/src/dashboard/app/components/EntityLink.jsx +1 -1
  121. package/src/dashboard/app/hooks/useMorphQuery.js +1 -1
  122. package/src/dashboard/app/lib/phases.js +2 -4
  123. package/src/dashboard/app/pages/Activity.jsx +1 -1
  124. package/src/dashboard/app/pages/FeatureDetail.jsx +139 -129
  125. package/src/dashboard/app/pages/Features.jsx +9 -9
  126. package/src/dashboard/app/pages/Guide.jsx +2 -2
  127. package/src/dashboard/app/pages/PhasePipeline.jsx +3 -4
  128. package/src/dashboard/app/pages/Standards.jsx +1 -1
  129. package/src/dashboard/scanner/activity-scanner.js +5 -15
  130. package/src/dashboard/scanner/agents-scanner.js +1 -1
  131. package/src/dashboard/scanner/docs-scanner.js +2 -2
  132. package/src/dashboard/scanner/features-scanner.js +95 -60
  133. package/src/dashboard/scanner/project-api.js +1 -1
  134. package/src/dashboard/scanner/standards-scanner.js +2 -1
  135. package/src/lib/ci/ci-verify.js +17 -1
  136. package/src/lib/ci/github-ci.js +1 -1
  137. package/src/lib/detectors/index.js +3 -8
  138. package/src/lib/generators/handoff-generator.js +34 -65
  139. package/src/lib/generators/settings-generator.js +11 -89
  140. package/src/lib/git/evidence-staleness.js +1 -1
  141. package/src/lib/git/tree-fingerprint.js +19 -7
  142. package/src/lib/git/tree-state.js +2 -2
  143. package/src/lib/heavy/cleanup.js +1 -5
  144. package/src/lib/installers/memory-installer.js +6 -8
  145. package/src/lib/mutation/commands.js +3 -2
  146. package/src/lib/{tasks/dotnet-test-introspection.js → mutation/dotnet-introspection.js} +17 -282
  147. package/src/lib/mutation/plan.js +14 -6
  148. package/src/lib/mutation/runner.js +1 -1
  149. package/src/lib/mutation/subset.js +46 -9
  150. package/src/lib/plan/artifact-vocabulary.js +60 -0
  151. package/src/lib/plan/checks.js +234 -0
  152. package/src/lib/plan/plan-parser.js +209 -0
  153. package/src/lib/plan/plan-template.js +18 -0
  154. package/src/lib/review-reports.js +222 -0
  155. package/src/lib/risk-detector.js +131 -140
  156. package/src/lib/specs/adr-index.js +50 -69
  157. package/src/lib/specs/design-seed.js +18 -0
  158. package/src/lib/specs/frontmatter.js +44 -0
  159. package/src/lib/specs/gfm-table.js +17 -0
  160. package/src/lib/specs/old-format.js +68 -0
  161. package/src/lib/specs/revocations.js +37 -95
  162. package/src/lib/specs/screen-checklist.js +18 -17
  163. package/src/lib/stack/dotnet-build-ignores.js +47 -0
  164. package/src/lib/stack/stack-scan.js +267 -0
  165. package/src/lib/standards/registry.js +43 -0
  166. package/src/lib/standards/sections.js +58 -0
  167. package/src/lib/tasks/eval-node.js +4 -12
  168. package/src/lib/tasks/test-nodes.js +1 -74
  169. package/src/lib/tasks/test-runner.js +75 -117
  170. package/src/lib/telemetry/metrics.js +8 -69
  171. package/src/lib/telemetry/telemetry-reader.js +5 -118
  172. package/src/lib/telemetry/verify-event.js +5 -7
  173. package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
  174. package/src/lib/validators/contracts/contract-compliance-validator.js +2 -2
  175. package/src/lib/validators/reuse/reuse-validator.js +22 -45
  176. package/src/lib/validators/traceability-check.js +85 -129
  177. package/src/lib/validators/verify-runner.js +118 -235
  178. package/src/lib/worktree/docker-audit.js +1 -2
  179. package/src/lib/worktree/fleet-scanner.js +25 -38
  180. package/src/scripts/setup-infra.js +9 -0
  181. package/src/utils/agents-installer.js +76 -9
  182. package/src/utils/claude-settings-manager.js +0 -35
  183. package/src/utils/file-copier.js +27 -1
  184. package/src/utils/hooks-installer.js +16 -9
  185. package/src/utils/skills-installer.js +1 -2
  186. package/framework/evals/trace-schema.md +0 -67
  187. package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
  188. package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
  189. package/framework/hooks/git/pre-commit/specs.sh +0 -50
  190. package/framework/hooks/shared/tasks-validator.js +0 -111
  191. package/framework/rules/morph-task-tracking.md +0 -54
  192. package/framework/rules/morph-workflow.md +0 -244
  193. package/framework/schemas/tasks.schema.json +0 -79
  194. package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
  195. package/framework/skills/core/morph-implement/SKILL.md +0 -194
  196. package/framework/skills/core/morph-proposal/SKILL.md +0 -152
  197. package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
  198. package/framework/templates/docs/proposal.md +0 -192
  199. package/framework/templates/docs/spec.md +0 -57
  200. package/framework/templates/docs/ui-components.md +0 -52
  201. package/framework/templates/docs/ui-design-system.md +0 -76
  202. package/framework/templates/docs/ui-flows.md +0 -167
  203. package/framework/templates/examples/spec-examples.md +0 -91
  204. package/framework/templates/feature/decisions.md +0 -124
  205. package/framework/templates/feature/recap.md +0 -145
  206. package/framework/templates/tasks.template.json +0 -17
  207. package/src/commands/state/usecase.js +0 -143
  208. package/src/commands/tasks/dag.js +0 -181
  209. package/src/lib/agents/output-collector.js +0 -76
  210. package/src/lib/evaluator-report.js +0 -104
  211. package/src/lib/specs/acervo-format.js +0 -70
  212. package/src/lib/specs/acervo-lifecycle.js +0 -264
  213. package/src/lib/specs/done-criteria-check.js +0 -200
  214. package/src/lib/specs/usecase-check.js +0 -301
  215. package/src/lib/specs/usecase-promoter.js +0 -301
  216. package/src/lib/specs/usecase-registry.js +0 -237
  217. package/src/lib/tasks/dispatch-planner.js +0 -319
  218. package/src/lib/tasks/mermaid-renderer.js +0 -132
  219. package/src/lib/tasks/task-classifier.js +0 -30
  220. package/src/lib/tasks/task-parser.js +0 -209
  221. package/src/lib/tasks/tasks-json-parser.js +0 -296
  222. package/src/lib/tasks/test-scope.js +0 -597
@@ -0,0 +1,18 @@
1
+ import { readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { templateSourcePath, writeIfAbsent } from '../../core/templates/template-seed.js';
4
+ import { renderTemplateString } from '../../core/templates/template-renderer.js';
5
+ import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
6
+
7
+ export function seedDesignArtifacts(featureName, cwd = process.cwd()) {
8
+ const mockupsTemplate = readFileSync(templateSourcePath('ui-mockups', cwd), 'utf8');
9
+ const mockups = writeIfAbsent(
10
+ getAbsoluteOutputPath(cwd, featureName, 'uiMockups'),
11
+ renderTemplateString(mockupsTemplate, { FEATURE_NAME: featureName, DATE: new Date().toISOString().slice(0, 10) }),
12
+ );
13
+ const designSystem = writeIfAbsent(
14
+ join(cwd, '.morph', 'context', 'design-system.md'),
15
+ readFileSync(templateSourcePath('context-design-system', cwd), 'utf8'),
16
+ );
17
+ return { mockups, designSystem };
18
+ }
@@ -0,0 +1,44 @@
1
+ const FRONTMATTER_RE = /^(?---\r?\n)([\s\S]*?)(\r?\n---(?:\r?\n|$))/;
2
+
3
+ /**
4
+ * Set (or replace) a single top-level frontmatter field, preserving every
5
+ * other line, the original EOL style, and anything after the closing `---`.
6
+ * A document with no frontmatter block is returned unchanged.
7
+ *
8
+ * @param {string} content
9
+ * @param {string} key
10
+ * @param {string} value - written as-is; quote it first when it is free text
11
+ * @returns {string}
12
+ */
13
+ export function setFrontmatterField(content, key, value) {
14
+ const m = FRONTMATTER_RE.exec(content);
15
+ if (!m || m.index !== 0) return content;
16
+
17
+ const [full, open, body, close] = m;
18
+ const eol = full.includes('\r\n') ? '\r\n' : '\n';
19
+ const fieldLine = new RegExp(`^${key}:[ \\t]*.*$`, 'm');
20
+ // Function replacer: a value carrying `$&` or `$1` is text, not a pattern.
21
+ const next = fieldLine.test(body)
22
+ ? body.replace(fieldLine, () => `${key}: ${value}`)
23
+ : `${body}${eol}${key}: ${value}`;
24
+
25
+ return open + next + close + content.slice(full.length);
26
+ }
27
+
28
+ /**
29
+ * Read a single top-level frontmatter field; `null` when absent or there is no
30
+ * frontmatter block. A double-quoted value is read as the JSON string it is
31
+ * written as; a single-quoted one as YAML reads it (`''` is a quote).
32
+ */
33
+ export function readFrontmatterField(content, key) {
34
+ const m = FRONTMATTER_RE.exec(content);
35
+ if (!m || m.index !== 0) return null;
36
+ const line = new RegExp(`^${key}:[ \\t]*(.+?)[ \\t]*$`, 'm').exec(m[2]);
37
+ if (!line) return null;
38
+ const raw = line[1].trim();
39
+ if (raw.length >= 2 && raw.startsWith('"') && raw.endsWith('"')) {
40
+ try { return JSON.parse(raw); } catch { return raw; }
41
+ }
42
+ if (raw.length >= 2 && raw.startsWith("'") && raw.endsWith("'")) return raw.slice(1, -1).replace(/''/g, "'");
43
+ return raw;
44
+ }
@@ -0,0 +1,17 @@
1
+ /** A markdown table separator row: `|---|:--:|`, with or without the outer pipes. */
2
+ export const SEPARATOR_ROW_RE = /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?\s*$/;
3
+
4
+ /** A line that belongs to a table: it carries an unescaped pipe (GFM does not require the leading one). */
5
+ export const isTableLine = (line) => /(?<!\\)\|/.test(line);
6
+
7
+ /**
8
+ * Split a table row on unescaped pipes, outer pipes optional; `\|` inside a
9
+ * cell is a literal pipe.
10
+ *
11
+ * @param {string} row
12
+ * @returns {string[]} trimmed cells
13
+ */
14
+ export function tableCells(row) {
15
+ const inner = row.trim().replace(/^\|/, '').replace(/(?<!\\)\|$/, '');
16
+ return inner.split(/(?<!\\)\|/).map((c) => c.replace(/\\\|/g, '|').trim());
17
+ }
@@ -0,0 +1,68 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'fs';
2
+ import { join, resolve } from 'path';
3
+ import { OUTPUT_SCHEMA } from '../../core/paths/output-schema.js';
4
+ import { parseWorktreeList } from '../../commands/project/worktree.js';
5
+
6
+ const FINISHED = new Set(['completed', 'abandoned']);
7
+ const PREVIOUS_LINE = '8.47';
8
+
9
+ export function isOldFormatFeature(featureDir) {
10
+ const plan = join(featureDir, OUTPUT_SCHEMA.plan.phaseDir, OUTPUT_SCHEMA.plan.filename);
11
+ if (existsSync(plan)) return false;
12
+ return existsSync(join(featureDir, '2-plan', 'tasks.json')) || existsSync(join(featureDir, '0-proposal'));
13
+ }
14
+
15
+ function featureStatus(featureDir) {
16
+ try {
17
+ return JSON.parse(readFileSync(join(featureDir, 'feature.json'), 'utf8'))?.status ?? null;
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ function featureDirs(tree) {
24
+ const base = join(tree, '.morph', 'features');
25
+ try {
26
+ return readdirSync(base, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => [e.name, join(base, e.name)]);
27
+ } catch {
28
+ return [];
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Features still in flight, in the root and in every git worktree, written in
34
+ * the layout this CLI no longer drives. Archived ones live outside `.morph/features/`.
35
+ *
36
+ * @param {string} root
37
+ * @param {{ listTrees?: (cwd: string) => Array<{ path: string }> }} [deps]
38
+ * @returns {Array<{ feature: string, trees: string[] }>}
39
+ */
40
+ export function findOldFormatFeatures(root, { listTrees = parseWorktreeList } = {}) {
41
+ const trees = [resolve(root)];
42
+ for (const wt of listTrees(root)) {
43
+ const p = resolve(wt.path);
44
+ if (!trees.some((t) => t.toLowerCase() === p.toLowerCase())) trees.push(p);
45
+ }
46
+ const found = new Map();
47
+ for (const tree of trees) {
48
+ for (const [name, dir] of featureDirs(tree)) {
49
+ if (FINISHED.has(featureStatus(dir)) || !isOldFormatFeature(dir)) continue;
50
+ if (!found.has(name)) found.set(name, []);
51
+ found.get(name).push(tree);
52
+ }
53
+ }
54
+ return [...found.entries()].map(([feature, where]) => ({ feature, trees: where }));
55
+ }
56
+
57
+ export function oldFormatGuardLines(found, root) {
58
+ const lines = [
59
+ `Recusado: ${found.length} feature(s) em voo no formato anterior ao plan.md (proposal/spec/tasks.json), que esta versão não conduz:`,
60
+ ];
61
+ for (const { feature, trees } of found) {
62
+ const where = trees.map((t) => (resolve(t) === resolve(root) ? 'raiz' : t)).join(', ');
63
+ lines.push(` - ${feature} (${where})`);
64
+ }
65
+ lines.push(` Termine cada uma na ${PREVIOUS_LINE}: \`npx @polymorphism-tech/morph-spec@${PREVIOUS_LINE} <comando>\` (por exemplo \`finish <feature> --pr\`),`);
66
+ lines.push(' ou arquive sem integrar: `morph-spec archive <feature> --force` (o Gate 3 dela não foi aprovado). Depois rode o `update` de novo.');
67
+ return lines;
68
+ }
@@ -1,156 +1,98 @@
1
1
  /**
2
- * The plan's declaration of what a feature supersedes in the project's acervo:
3
- * the "UCs e ADRs que esta feature altera ou revoga" section of `spec.md`.
4
- *
5
- * ## UCs e ADRs que esta feature altera ou revoga
6
- *
7
- * | Id | Mudança | Substituto | Escopo em que o antigo ainda vale |
8
- * |---|---|---|---|
9
- * | UC-{nn} | parcial | UC-{nn} | {onde o antigo ainda vale} |
10
- * | {feature}/ADR-{nnn} | total | — | — |
11
- *
12
- * Columns are positional, so the header wording is free. Parsing is pure: the
13
- * finish applies the entries (`acervo-lifecycle.js`), and `dag`/`usecase
14
- * check` report the problems while `spec.md` can still be edited — it freezes
15
- * at Gate 2, so a bad row found at close time can no longer be fixed there.
16
- *
17
- * Nothing ambiguous is applied: a row with any problem, and every row of an id
18
- * declared twice, stays out of `entries`. A table inside a code fence is an
19
- * example, never a declaration.
2
+ * The ADRs of the project index that a plan revokes: the table under
3
+ * `### Conflitos com o acervo` whose first header cell is `ADR`, with columns
4
+ * `{feature}/ADR-{nnn}`, `total|parcial` and the scope where the old one still holds.
5
+ * plan-check reports the problems while the plan can still change (it freezes
6
+ * at the plan gate); nothing ambiguous reaches the finish.
20
7
  */
21
8
 
22
- import { SEPARATOR_ROW_RE, UC_ID_RE, isTableLine, tableCells } from './acervo-format.js';
23
-
24
- export const REVOCATIONS_TITLE = 'UCs e ADRs que esta feature altera ou revoga';
9
+ import { SEPARATOR_ROW_RE, isTableLine, tableCells } from './gfm-table.js';
25
10
 
26
11
  /** ADR numbers restart in every feature, so only `{feature}/ADR-{nnn}` is unambiguous. */
27
12
  export const ADR_REF_RE = /^[a-z0-9][a-z0-9-]*\/ADR-\d+$/;
28
13
 
29
- const HEADING_RE = /^(#{1,6})\s+(.+?)\s*#*\s*$/;
30
14
  const FENCE_RE = /^\s*(```|~~~)/;
31
- const ID_TOKEN_RE = /\b(?:UC|ADR)-\d+\b/;
32
- /** A list item: the shape a revocation takes when the author skips the table. */
15
+ const REF_TOKEN_RE = /\b[a-z0-9][a-z0-9-]*\/ADR-\d+\b/;
33
16
  const LIST_ITEM_RE = /^\s*(?:[-*+]|\d+[.)])\s/;
34
-
35
17
  const CHANGES = new Map([['total', 'total'], ['parcial', 'parcial'], ['partial', 'parcial']]);
36
18
 
37
- /** `—`, `–`, `-` and an empty cell all mean "none". */
38
19
  const isNone = (cell) => /^(?:—|–|-)?$/.test(cell);
39
-
40
- /** Id-like cells tolerate the typography an author reaches for: `code` and **bold**. */
41
20
  const bare = (cell) => cell.replace(/^[`*]+|[`*]+$/g, '').trim();
42
21
 
43
- /**
44
- * The problems of one data row; `[]` = the row is applicable.
45
- * @returns {{ entry: object|null, problems: Array<{ id: string|null, reason: string, message: string }> }}
46
- */
47
22
  function readRow(line) {
48
23
  const problems = [];
49
24
  const problem = (id, reason, message) => problems.push({ id, reason, message });
50
25
 
51
26
  const row = tableCells(line);
52
- if (row.length < 4) {
53
- problem(null, 'malformed-row', `linha com ${row.length} coluna(s), esperado 4 (Id | Mudança | Substituto | Escopo): ${line.trim()}`);
27
+ if (row.length < 3) {
28
+ problem(null, 'malformed-row', `linha com ${row.length} coluna(s), esperado 3 (ADR | Mudança | Escopo): ${line.trim()}`);
54
29
  return { entry: null, problems };
55
30
  }
56
31
  const id = bare(row[0]);
57
- const changeCell = bare(row[1]).toLowerCase();
58
- const substitute = isNone(bare(row[2])) ? null : bare(row[2]);
59
- const scope = isNone(row[3]) ? null : row[3];
32
+ const change = CHANGES.get(bare(row[1]).toLowerCase());
33
+ const scope = isNone(row[2]) ? null : row[2];
60
34
 
61
- const kind = UC_ID_RE.test(id) ? 'uc' : ADR_REF_RE.test(id) ? 'adr' : null;
62
- if (!kind) {
63
- problem(id || null, 'malformed-id', `"${id}" não é um UC-{nn} nem um {feature}/ADR-{nnn}`);
35
+ if (!ADR_REF_RE.test(id)) {
36
+ problem(id || null, 'malformed-id', `"${id}" não é um {feature}/ADR-{nnn}`);
64
37
  return { entry: null, problems };
65
38
  }
66
-
67
- const change = CHANGES.get(changeCell);
68
39
  if (!change) problem(id, 'malformed-change', `${id}: mudança "${bare(row[1])}" — use total ou parcial`);
69
-
70
- if (kind === 'uc' && !substitute) {
71
- problem(id, 'missing-substitute', `${id}: UC revogado precisa do substituto (UC-{nn}) — é o superseded_by do acervo`);
72
- } else if (substitute && !(kind === 'uc' ? UC_ID_RE : ADR_REF_RE).test(substitute)) {
73
- problem(id, 'malformed-substitute',
74
- `${id}: substituto "${substitute}" — esperado ${kind === 'uc' ? 'UC-{nn}' : '{feature}/ADR-{nnn}'}`);
75
- } else if (substitute === id) {
76
- problem(id, 'self-substitute', `${id}: o substituto é o próprio ${id}`);
77
- }
78
-
79
40
  if (change === 'parcial' && !scope) {
80
41
  problem(id, 'missing-scope', `${id}: revogação parcial precisa dizer onde o antigo ainda vale (coluna Escopo)`);
81
42
  } else if (change === 'total' && scope) {
82
43
  problem(id, 'scope-on-total', `${id}: revogação total com escopo "${scope}" — se o antigo ainda vale em algum lugar, a mudança é parcial`);
83
44
  }
84
45
 
85
- return { entry: problems.length ? { id } : { kind, id, change, substitute, scope }, problems };
46
+ return { entry: problems.length ? { id } : { id, change, scope }, problems };
86
47
  }
87
48
 
88
49
  /**
89
- * @param {string} content - the whole `spec.md`
50
+ * @param {string|null} conflicts - the text of `### Conflitos com o acervo` (`parsePlan(...).conflicts.text`)
90
51
  * @returns {{
91
- * declared: boolean,
92
- * entries: Array<{ kind: 'uc'|'adr', id: string, change: 'total'|'parcial', substitute: string|null, scope: string|null }>,
52
+ * entries: Array<{ id: string, change: 'total'|'parcial', scope: string|null }>,
93
53
  * problems: Array<{ id: string|null, reason: string, message: string }>
94
54
  * }}
95
- * `declared: false` — no section at all (a plan written before it existed).
96
55
  */
97
- export function parseRevocations(content) {
98
- const lines = String(content ?? '').split(/\r?\n/);
99
- const title = REVOCATIONS_TITLE.toLowerCase();
100
-
101
- let start = -1;
102
- let level = 0;
103
- for (let i = 0; i < lines.length; i++) {
104
- const h = HEADING_RE.exec(lines[i]);
105
- if (h && h[2].toLowerCase().startsWith(title)) { start = i + 1; level = h[1].length; break; }
106
- }
107
- if (start === -1) return { declared: false, entries: [], problems: [] };
108
-
109
- let end = lines.length;
110
- for (let i = start; i < lines.length; i++) {
111
- const h = HEADING_RE.exec(lines[i]);
112
- if (h && h[1].length <= level) { end = i; break; }
113
- }
114
-
56
+ export function parseRevocations(conflicts) {
115
57
  const problems = [];
116
- const rows = []; // { id, entry } in order, applicable or not
58
+ const rows = [];
117
59
  let fenced = false;
118
- let separatorSeen = false;
119
- let tableLines = 0;
60
+ let table = null;
120
61
 
121
- for (const line of lines.slice(start, end)) {
122
- if (FENCE_RE.test(line)) { fenced = !fenced; continue; }
62
+ for (const line of String(conflicts ?? '').split(/\r?\n/)) {
63
+ if (FENCE_RE.test(line)) { fenced = !fenced; table = null; continue; }
123
64
  if (fenced) continue;
124
65
 
125
66
  if (!isTableLine(line)) {
126
- const stray = LIST_ITEM_RE.test(line) ? ID_TOKEN_RE.exec(line) : null;
67
+ table = null;
68
+ const stray = LIST_ITEM_RE.test(line) ? REF_TOKEN_RE.exec(line) : null;
127
69
  if (stray) {
128
70
  problems.push({ id: stray[0], reason: 'outside-table',
129
- message: `${stray[0]} aparece fora da tabela — só as linhas da tabela são aplicadas no finish` });
71
+ message: `${stray[0]} aparece fora da tabela ADR | Mudança | Escopo — só as linhas da tabela são aplicadas no finish` });
130
72
  }
131
73
  continue;
132
74
  }
133
- tableLines++;
134
- if (SEPARATOR_ROW_RE.test(line)) { separatorSeen = true; continue; }
135
- if (!separatorSeen) continue; // the header row: its wording is free
136
-
75
+ if (!table) {
76
+ table = { revocations: bare(tableCells(line)[0] ?? '').toLowerCase() === 'adr', separatorSeen: false };
77
+ continue;
78
+ }
79
+ if (!table.revocations) continue;
80
+ if (!table.separatorSeen) {
81
+ if (SEPARATOR_ROW_RE.test(line)) { table.separatorSeen = true; continue; }
82
+ problems.push({ id: null, reason: 'unreadable-table',
83
+ message: 'tabela de revogação ilegível — falta a linha separadora (|---|---|---|) depois do cabeçalho; nenhuma linha foi lida' });
84
+ table.revocations = false;
85
+ continue;
86
+ }
137
87
  const { entry, problems: rowProblems } = readRow(line);
138
88
  problems.push(...rowProblems);
139
89
  if (entry) rows.push(entry);
140
90
  }
141
91
 
142
- if (tableLines > 0 && !separatorSeen) {
143
- problems.push({ id: null, reason: 'unreadable-table',
144
- message: 'tabela ilegível — falta a linha separadora (|---|---|---|---) depois do cabeçalho; nenhuma linha foi lida' });
145
- }
146
-
147
- // An id declared twice is ambiguous as a whole: none of its rows applies.
148
92
  const count = new Map();
149
93
  for (const r of rows) count.set(r.id, (count.get(r.id) ?? 0) + 1);
150
94
  for (const [id, n] of count) {
151
95
  if (n > 1) problems.push({ id, reason: 'duplicate-id', message: `${id} aparece em ${n} linhas — nenhuma foi aplicada; deixe uma só` });
152
96
  }
153
- const entries = rows.filter((r) => r.kind && count.get(r.id) === 1);
154
-
155
- return { declared: true, entries, problems };
97
+ return { entries: rows.filter((r) => r.change && count.get(r.id) === 1), problems };
156
98
  }
@@ -3,14 +3,14 @@
3
3
  * dados precisa (volume, paginação, ordenação, filtro, os três estados,
4
4
  * mobile, abas) escritas por tela, na fase uiux.
5
5
  *
6
- * Existe porque o que o design não pergunta não vira `doneCriteria`, e o que
7
- * não é critério ninguém entrega: telas de admin saíam listando tudo, sem
6
+ * Existe porque o que o design não pergunta não vira cenário, e o que
7
+ * não é cenário ninguém entrega: telas de admin saíam listando tudo, sem
8
8
  * paginar nem ordenar, com o design aprovado. Os leitores do MESMO parser:
9
9
  *
10
10
  * - `gate-check uiux` pausa com os problemas nomeados;
11
11
  * - `approve`/`advance` com assinatura `auto` recusam a mesma pausa sem uma
12
12
  * decisão gravada (`uiuxAutoApprovalBlock`) — a regra é a do risk-detector;
13
- * - `gate-check review` exige que o avaliador registre a conferência de cada
13
+ * - `gate-check review` exige que o verificador registre a conferência de cada
14
14
  * item decidido contra a tela entregue (`checklistCoverage`).
15
15
  *
16
16
  * Mockup no formato ANTERIOR ao checklist (telas `## S001: …`, sem nenhuma
@@ -27,7 +27,7 @@ import { existsSync, readFileSync } from 'fs';
27
27
  import { join } from 'path';
28
28
  import { fileURLToPath } from 'url';
29
29
  import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
30
- import { evaluatorScreenChecklist } from '../evaluator-report.js';
30
+ import { verifierScreenChecklist } from '../review-reports.js';
31
31
  import { LEGACY_MOCKUP_EXEMPTION, uiuxChecklistPause } from '../risk-detector.js';
32
32
 
33
33
  const PACKAGE_AGENTS = fileURLToPath(new URL('../../../framework/agents.json', import.meta.url));
@@ -52,7 +52,7 @@ export const SCREEN_CHECKLIST_ITEMS = [
52
52
  ];
53
53
 
54
54
  /**
55
- * Itens que o avaliador não confere contra a tela: a superfície decide QUAIS
55
+ * Itens que o verificador não confere contra a tela: a superfície decide QUAIS
56
56
  * standards a tela lê, não um comportamento que a tela entregue exiba.
57
57
  */
58
58
  const NOT_VERIFIED_AT_REVIEW = new Set(['superficie']);
@@ -180,8 +180,8 @@ const labelOf = (id) => SCREEN_CHECKLIST_ITEMS.find((item) => item.id === id)?.l
180
180
 
181
181
  /**
182
182
  * As superfícies que a persona `ui-designer` declara no `agents.json` — o
183
- * vocabulário válido do item `Superfície`, que o `morph-plan` §5a usa para
184
- * somar os standards da tela em `tasks[].standards`. Lê a cópia instalada do
183
+ * vocabulário válido do item `Superfície`, que o `morph-plan` usa para
184
+ * somar os standards da tela no `standards:` da fatia. Lê a cópia instalada do
185
185
  * projeto; sem ela (este repositório), a do pacote. Persona sem `surfaces`
186
186
  * (instalação anterior ao campo) devolve null: o valor não é validado.
187
187
  *
@@ -189,14 +189,15 @@ const labelOf = (id) => SCREEN_CHECKLIST_ITEMS.find((item) => item.id === id)?.l
189
189
  * @returns {string[]|null}
190
190
  */
191
191
  export function knownSurfaces(cwd) {
192
+ const surfaces = readAgentsJson(cwd)?.agents?.['ui-designer']?.surfaces;
193
+ return surfaces && typeof surfaces === 'object' && !Array.isArray(surfaces) ? Object.keys(surfaces) : null;
194
+ }
195
+
196
+ /** The project's installed `agents.json`, else the package's; null when unreadable. */
197
+ export function readAgentsJson(cwd) {
192
198
  for (const path of [join(cwd, '.morph', 'framework', 'agents.json'), PACKAGE_AGENTS]) {
193
199
  if (!existsSync(path)) continue;
194
- try {
195
- const surfaces = JSON.parse(readFileSync(path, 'utf8'))?.agents?.['ui-designer']?.surfaces;
196
- return surfaces && typeof surfaces === 'object' && !Array.isArray(surfaces) ? Object.keys(surfaces) : null;
197
- } catch {
198
- return null;
199
- }
200
+ try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; }
200
201
  }
201
202
  return null;
202
203
  }
@@ -257,7 +258,7 @@ export function auditScreenChecklist(markdown, { surfaces = null } = {}) {
257
258
  *
258
259
  * `mockups: false` é "a feature não tem mockup" — nada a cobrar. No review, o
259
260
  * que se cobra é a conferência (`uncovered`: itens decididos sem linha na
260
- * seção do avaliador), não o design, que já passou pelo Gate uiux.
261
+ * seção do verificador), não o design, que já passou pelo Gate uiux.
261
262
  *
262
263
  * @param {string} cwd
263
264
  * @param {string} featureName
@@ -272,7 +273,7 @@ export function collectScreenChecklist(cwd, featureName, gateName) {
272
273
  const signal = { mockups: true, legacy: audit.legacy, screens: audit.screens.map((s) => s.name), problems: audit.problems };
273
274
  if (audit.legacy) signal.exempt = LEGACY_MOCKUP_EXEMPTION;
274
275
  if (gateName === 'review') {
275
- const report = evaluatorScreenChecklist(cwd, featureName);
276
+ const report = verifierScreenChecklist(cwd, featureName);
276
277
  signal.section = report.section;
277
278
  signal.uncovered = checklistCoverage(mockups, report.section ? report.body : '');
278
279
  }
@@ -300,13 +301,13 @@ export function uiuxAutoApprovalBlock(cwd, featureName, feature) {
300
301
  }
301
302
 
302
303
  /**
303
- * O que o avaliador deixou de conferir: por tela do mockup, os itens com
304
+ * O que o verificador deixou de conferir: por tela do mockup, os itens com
304
305
  * decisão (nem "não se aplica", nem a superfície) sem linha na seção
305
306
  * `## Checklist de tela` do relatório. Uma linha que o avaliador marca como
306
307
  * não verificável conta como conferida — o que pausa é a AUSÊNCIA da linha.
307
308
  *
308
309
  * @param {string} designMarkdown - o `1-design/mockups.md`
309
- * @param {string} reportSection - o corpo da seção do `evaluator-report.md`
310
+ * @param {string} reportSection - o corpo da seção do `verifier.md`
310
311
  * @returns {string[]} `"<tela>: <item>, <item>"` por tela com lacuna
311
312
  */
312
313
  export function checklistCoverage(designMarkdown, reportSection) {
@@ -0,0 +1,47 @@
1
+ import { readdirSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { ensureDotnetBuildIgnores } from '../../utils/file-copier.js';
4
+ import { getProjectTags } from '../stack-filter.js';
5
+ import { readMorphConfig } from '../tasks/test-runner.js';
6
+ import { readPlanChecks } from '../plan/checks.js';
7
+ import { WORKTREE_DIR_NAMES } from '../worktree/dirs.js';
8
+
9
+ export const DOTNET_IMPLEMENTERS = ['dotnet-senior', 'ef-modeler', 'maf-expert'];
10
+
11
+ const SKIPPED_DIRS = new Set([...WORKTREE_DIR_NAMES, 'node_modules', 'bin', 'obj']);
12
+ const DOTNET_PROJECT_FILE = /\.(?:csproj|slnx?)$/i;
13
+ const MAX_DEPTH = 6;
14
+
15
+ export function hasDotnetProjectFile(root, depth = 0) {
16
+ let entries;
17
+ try { entries = readdirSync(root, { withFileTypes: true }); } catch { return false; }
18
+ if (entries.some((e) => e.isFile() && DOTNET_PROJECT_FILE.test(e.name))) return true;
19
+ if (depth >= MAX_DEPTH) return false;
20
+ return entries.some((e) => e.isDirectory() && !e.name.startsWith('.') && !SKIPPED_DIRS.has(e.name)
21
+ && hasDotnetProjectFile(join(root, e.name), depth + 1));
22
+ }
23
+
24
+ function planHasDotnetSlice(tree, featureName) {
25
+ if (!featureName) return false;
26
+ const { checks } = readPlanChecks(tree, featureName);
27
+ return (checks?.slices || []).some((slice) => DOTNET_IMPLEMENTERS.includes(String(slice.persona || '').replace(/^morph-/, '')));
28
+ }
29
+
30
+ /**
31
+ * "Is this a .NET project?" from what is known before the first code commit:
32
+ * the declared `project.stack`, a .NET implementer in the plan's slices, or a
33
+ * .csproj/.sln/.slnx already in the tree. Greenfield has only the second.
34
+ */
35
+ export function isDotnetProject(tree, { featureName, configRoot = tree } = {}) {
36
+ if (getProjectTags(readMorphConfig(configRoot)).includes('dotnet')) return true;
37
+ return planHasDotnetSlice(tree, featureName) || hasDotnetProjectFile(tree);
38
+ }
39
+
40
+ /** @returns {string[]} the `.gitignore` entries added to `tree`; `[]` when not .NET or on any error */
41
+ export function seedDotnetBuildIgnores(tree, options = {}) {
42
+ try {
43
+ return isDotnetProject(tree, options) ? ensureDotnetBuildIgnores(tree) : [];
44
+ } catch {
45
+ return [];
46
+ }
47
+ }