@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.
- package/CLAUDE.md +48 -39
- package/README.md +3 -4
- package/bin/morph-spec.js +37 -51
- package/bin/validate.js +3 -111
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +41 -101
- package/framework/MORPH.md +95 -252
- package/framework/agents.json +75 -18
- package/framework/commands/morph-apply.md +5 -28
- package/framework/commands/morph-archive.md +1 -1
- package/framework/commands/morph-hotfix.md +8 -8
- package/framework/commands/morph-infra.md +1 -1
- package/framework/commands/morph-preflight.md +2 -3
- package/framework/commands/morph-proposal.md +2 -50
- package/framework/commands/morph-status.md +3 -3
- package/framework/docs/CLI.md +585 -286
- package/framework/evals/review-checklist.md +82 -41
- package/framework/hooks/README.md +10 -11
- package/framework/hooks/claude-code/core/comment-guard.js +1 -1
- package/framework/hooks/claude-code/core/denylist.json +33 -3
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +16 -20
- package/framework/hooks/claude-code/core/gate-guard.js +2 -11
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +38 -0
- package/framework/hooks/claude-code/core/protect-spec-files.js +30 -242
- package/framework/hooks/claude-code/core/state-sync.js +7 -21
- package/framework/hooks/claude-code/core/telemetry-log.js +3 -3
- package/framework/hooks/claude-code/core/validate-completion.js +33 -97
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +31 -0
- package/framework/hooks/claude-code/core/worktree-context.js +5 -4
- package/framework/hooks/claude-code/statusline.py +55 -41
- package/framework/hooks/package.json +3 -0
- package/framework/hooks/shared/activity-logger.js +1 -1
- package/framework/hooks/shared/build-check.js +10 -6
- package/framework/hooks/shared/command-tokens.js +169 -0
- package/framework/hooks/shared/comment-rules.js +1 -1
- package/framework/hooks/shared/comment-scanner.js +10 -23
- package/framework/hooks/shared/comment-stripper.js +1 -1
- package/framework/hooks/shared/e2e-check.js +64 -27
- package/framework/hooks/shared/errata.js +72 -0
- package/framework/hooks/shared/feature-locator.js +1 -1
- package/framework/hooks/shared/frontend-locations.js +6 -0
- package/framework/hooks/shared/gate-question.js +18 -0
- package/framework/hooks/shared/phase-utils.js +15 -32
- package/framework/hooks/shared/slice-progress.js +118 -0
- package/framework/hooks/shared/spawn-command.js +23 -0
- package/framework/hooks/shared/state-reader.js +61 -87
- package/framework/hooks/shared/state-writer.js +2 -3
- package/framework/mcp/registry.json +3 -4
- package/framework/phases.json +23 -39
- package/framework/schemas/config.schema.json +14 -9
- package/framework/schemas/feature.schema.json +15 -50
- package/framework/skills/README.md +3 -4
- 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 +149 -222
- package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
- package/framework/skills/core/morph-machine/SKILL.md +2 -2
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +137 -299
- package/framework/skills/core/morph-review/SKILL.md +111 -136
- package/framework/skills/core/morph-standards/SKILL.md +27 -39
- package/framework/skills/core/morph-uiux/SKILL.md +77 -152
- package/framework/standards/STANDARDS.json +86 -1218
- package/framework/standards/STANDARDS.meta.json +1305 -0
- package/framework/standards/ai-agents/evals-with-cache.md +1 -2
- package/framework/standards/ai-agents/setup.md +1 -1
- package/framework/standards/architecture/code-comments.md +6 -6
- package/framework/standards/integration/mcp/mcp-tools.md +7 -7
- package/framework/templates/README.md +2 -3
- package/framework/templates/REGISTRY.json +6 -170
- package/framework/templates/docs/plan.md +72 -0
- package/framework/templates/dotnet/ai-kit/README.md +1 -9
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
- package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
- package/package.json +1 -1
- package/src/commands/e2e/e2e.js +2 -2
- package/src/commands/observability/cost.js +18 -60
- package/src/commands/project/doctor-procs.js +1 -1
- package/src/commands/project/doctor.js +9 -9
- package/src/commands/project/fleet.js +4 -4
- package/src/commands/project/heavy.js +3 -3
- package/src/commands/project/init.js +4 -0
- package/src/commands/project/setup-infra-cmd.js +2 -1
- package/src/commands/project/stack-scan.js +58 -0
- package/src/commands/project/standards.js +79 -25
- package/src/commands/project/status.js +29 -69
- package/src/commands/project/update.js +17 -1
- package/src/commands/project/worktree.js +1 -1
- package/src/commands/state/advance.js +62 -46
- package/src/commands/state/approve.js +81 -141
- package/src/commands/state/archive.js +20 -84
- package/src/commands/state/auto-approval.js +68 -0
- package/src/commands/state/create.js +28 -1
- package/src/commands/state/delete.js +1 -1
- package/src/commands/state/finish.js +11 -23
- package/src/commands/state/gate-check.js +85 -84
- package/src/commands/state/gate-decision.js +6 -4
- package/src/commands/state/handoff.js +1 -1
- package/src/commands/state/list.js +9 -9
- package/src/commands/state/report-check.js +157 -311
- package/src/commands/state/retype.js +9 -3
- package/src/commands/validation/mutate.js +77 -14
- package/src/commands/validation/plan-check.js +81 -0
- package/src/commands/validation/validate-feature.js +60 -135
- package/src/commands/validation/verify.js +40 -72
- package/src/core/paths/output-schema.js +19 -66
- package/src/core/state/gate-order.js +5 -6
- package/src/core/state/lifecycle-audit.js +2 -2
- package/src/core/state/state-manager.js +17 -98
- package/src/core/templates/template-registry.js +2 -2
- package/src/core/templates/template-seed.js +22 -0
- package/src/core/work-types.js +10 -10
- package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
- package/src/dashboard/app/components/CommandPalette.jsx +5 -5
- package/src/dashboard/app/components/EntityLink.jsx +1 -1
- package/src/dashboard/app/hooks/useMorphQuery.js +1 -1
- package/src/dashboard/app/lib/phases.js +2 -4
- package/src/dashboard/app/pages/Activity.jsx +1 -1
- package/src/dashboard/app/pages/FeatureDetail.jsx +139 -129
- package/src/dashboard/app/pages/Features.jsx +9 -9
- package/src/dashboard/app/pages/Guide.jsx +2 -2
- package/src/dashboard/app/pages/PhasePipeline.jsx +3 -4
- package/src/dashboard/app/pages/Standards.jsx +1 -1
- package/src/dashboard/scanner/activity-scanner.js +5 -15
- package/src/dashboard/scanner/agents-scanner.js +1 -1
- package/src/dashboard/scanner/docs-scanner.js +2 -2
- package/src/dashboard/scanner/features-scanner.js +95 -60
- package/src/dashboard/scanner/project-api.js +1 -1
- package/src/dashboard/scanner/standards-scanner.js +2 -1
- package/src/lib/ci/ci-verify.js +17 -1
- package/src/lib/ci/github-ci.js +1 -1
- package/src/lib/detectors/index.js +3 -8
- package/src/lib/generators/handoff-generator.js +34 -65
- package/src/lib/generators/settings-generator.js +11 -89
- package/src/lib/git/evidence-staleness.js +1 -1
- package/src/lib/git/tree-fingerprint.js +19 -7
- package/src/lib/git/tree-state.js +2 -2
- package/src/lib/heavy/cleanup.js +1 -5
- package/src/lib/installers/memory-installer.js +6 -8
- package/src/lib/mutation/commands.js +3 -2
- package/src/lib/{tasks/dotnet-test-introspection.js → mutation/dotnet-introspection.js} +17 -282
- package/src/lib/mutation/plan.js +14 -6
- package/src/lib/mutation/runner.js +1 -1
- package/src/lib/mutation/subset.js +46 -9
- package/src/lib/plan/artifact-vocabulary.js +60 -0
- package/src/lib/plan/checks.js +234 -0
- package/src/lib/plan/plan-parser.js +209 -0
- package/src/lib/plan/plan-template.js +18 -0
- package/src/lib/review-reports.js +222 -0
- package/src/lib/risk-detector.js +131 -140
- package/src/lib/specs/adr-index.js +50 -69
- package/src/lib/specs/design-seed.js +18 -0
- package/src/lib/specs/frontmatter.js +44 -0
- package/src/lib/specs/gfm-table.js +17 -0
- package/src/lib/specs/old-format.js +68 -0
- package/src/lib/specs/revocations.js +37 -95
- package/src/lib/specs/screen-checklist.js +18 -17
- package/src/lib/stack/dotnet-build-ignores.js +47 -0
- package/src/lib/stack/stack-scan.js +267 -0
- package/src/lib/standards/registry.js +43 -0
- package/src/lib/standards/sections.js +58 -0
- package/src/lib/tasks/eval-node.js +4 -12
- package/src/lib/tasks/test-nodes.js +1 -74
- package/src/lib/tasks/test-runner.js +75 -117
- package/src/lib/telemetry/metrics.js +8 -69
- package/src/lib/telemetry/telemetry-reader.js +5 -118
- package/src/lib/telemetry/verify-event.js +5 -7
- package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
- package/src/lib/validators/contracts/contract-compliance-validator.js +2 -2
- package/src/lib/validators/reuse/reuse-validator.js +22 -45
- package/src/lib/validators/traceability-check.js +85 -129
- package/src/lib/validators/verify-runner.js +118 -235
- package/src/lib/worktree/docker-audit.js +1 -2
- package/src/lib/worktree/fleet-scanner.js +25 -38
- package/src/scripts/setup-infra.js +9 -0
- package/src/utils/agents-installer.js +76 -9
- package/src/utils/claude-settings-manager.js +0 -35
- package/src/utils/file-copier.js +27 -1
- package/src/utils/hooks-installer.js +16 -9
- package/src/utils/skills-installer.js +1 -2
- package/framework/evals/trace-schema.md +0 -67
- package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
- package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
- package/framework/hooks/git/pre-commit/specs.sh +0 -50
- package/framework/hooks/shared/tasks-validator.js +0 -111
- package/framework/rules/morph-task-tracking.md +0 -54
- package/framework/rules/morph-workflow.md +0 -244
- package/framework/schemas/tasks.schema.json +0 -79
- package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
- package/framework/skills/core/morph-implement/SKILL.md +0 -194
- package/framework/skills/core/morph-proposal/SKILL.md +0 -152
- package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
- package/framework/templates/docs/proposal.md +0 -192
- package/framework/templates/docs/spec.md +0 -57
- package/framework/templates/docs/ui-components.md +0 -52
- package/framework/templates/docs/ui-design-system.md +0 -76
- package/framework/templates/docs/ui-flows.md +0 -167
- package/framework/templates/examples/spec-examples.md +0 -91
- package/framework/templates/feature/decisions.md +0 -124
- package/framework/templates/feature/recap.md +0 -145
- package/framework/templates/tasks.template.json +0 -17
- package/src/commands/state/usecase.js +0 -143
- package/src/commands/tasks/dag.js +0 -181
- package/src/lib/agents/output-collector.js +0 -76
- package/src/lib/evaluator-report.js +0 -104
- package/src/lib/specs/acervo-format.js +0 -70
- package/src/lib/specs/acervo-lifecycle.js +0 -264
- package/src/lib/specs/done-criteria-check.js +0 -200
- package/src/lib/specs/usecase-check.js +0 -301
- package/src/lib/specs/usecase-promoter.js +0 -301
- package/src/lib/specs/usecase-registry.js +0 -237
- package/src/lib/tasks/dispatch-planner.js +0 -319
- package/src/lib/tasks/mermaid-renderer.js +0 -132
- package/src/lib/tasks/task-classifier.js +0 -30
- package/src/lib/tasks/task-parser.js +0 -209
- package/src/lib/tasks/tasks-json-parser.js +0 -296
- 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
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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,
|
|
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
|
|
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 <
|
|
53
|
-
problem(null, 'malformed-row', `linha com ${row.length} coluna(s), esperado
|
|
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
|
|
58
|
-
const
|
|
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
|
-
|
|
62
|
-
|
|
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 } : {
|
|
46
|
+
return { entry: problems.length ? { id } : { id, change, scope }, problems };
|
|
86
47
|
}
|
|
87
48
|
|
|
88
49
|
/**
|
|
89
|
-
* @param {string}
|
|
50
|
+
* @param {string|null} conflicts - the text of `### Conflitos com o acervo` (`parsePlan(...).conflicts.text`)
|
|
90
51
|
* @returns {{
|
|
91
|
-
*
|
|
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(
|
|
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 = [];
|
|
58
|
+
const rows = [];
|
|
117
59
|
let fenced = false;
|
|
118
|
-
let
|
|
119
|
-
let tableLines = 0;
|
|
60
|
+
let table = null;
|
|
120
61
|
|
|
121
|
-
for (const line of
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
|
7
|
-
* não é
|
|
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
|
|
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 {
|
|
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
|
|
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`
|
|
184
|
-
* somar os standards da tela
|
|
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
|
|
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 =
|
|
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
|
|
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 `
|
|
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
|
+
}
|