@polymorphism-tech/morph-spec 8.46.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 +53 -44
- package/README.md +3 -4
- package/bin/morph-spec.js +38 -55
- package/bin/validate.js +5 -134
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +2 -2
- 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/create-umbraco-lp.md +2 -0
- package/framework/commands/migrate-lp-to-umbraco.md +2 -0
- package/framework/commands/morph-apply.md +5 -28
- package/framework/commands/morph-archive.md +2 -2
- package/framework/commands/morph-hotfix.md +8 -8
- package/framework/commands/morph-infra.md +17 -203
- package/framework/commands/morph-media.md +3 -0
- package/framework/commands/morph-preflight.md +7 -31
- package/framework/commands/morph-proposal.md +2 -50
- package/framework/commands/morph-status.md +3 -3
- package/framework/commands/morph-troubleshoot.md +2 -14
- 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 +17 -19
- 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 +67 -31
- 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 +3 -3
- package/framework/mcp/registry.json +3 -4
- package/framework/phases.json +23 -39
- package/framework/rules/ai-agents-standards.md +18 -16
- package/framework/rules/asaas-standards.md +3 -1
- package/framework/rules/csharp-standards.md +10 -8
- package/framework/rules/data-standards.md +5 -5
- package/framework/rules/dotnet-templates.md +1 -1
- package/framework/rules/frontend-standards.md +4 -2
- package/framework/rules/gsap-standards.md +10 -5
- package/framework/rules/hangfire-standards.md +3 -1
- package/framework/rules/infrastructure-standards.md +4 -2
- package/framework/rules/neon-auth-standards.md +3 -1
- package/framework/rules/neon-standards.md +6 -4
- package/framework/rules/nextjs-standards.md +13 -11
- package/framework/rules/nextjs-templates.md +1 -1
- package/framework/rules/resend-standards.md +3 -1
- package/framework/schemas/config.schema.json +14 -9
- package/framework/schemas/feature.schema.json +15 -52
- package/framework/skills/README.md +4 -5
- package/framework/skills/core/create-umbraco-lp/SKILL.md +3 -3
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +3 -3
- package/framework/skills/core/morph-apply/SKILL.md +150 -223
- package/framework/skills/core/morph-hotfix/SKILL.md +34 -43
- package/framework/skills/core/morph-machine/SKILL.md +4 -4
- package/framework/skills/core/morph-media/SKILL.md +3 -3
- package/framework/skills/core/morph-plan/SKILL.md +138 -300
- package/framework/skills/core/morph-review/SKILL.md +112 -137
- package/framework/skills/core/morph-standards/SKILL.md +29 -41
- package/framework/skills/core/morph-uiux/SKILL.md +78 -153
- package/framework/standards/STANDARDS.json +90 -1219
- package/framework/standards/STANDARDS.meta.json +1305 -0
- package/framework/standards/ai-agents/evals-with-cache.md +2 -3
- package/framework/standards/ai-agents/llm-runtime-defaults.md +1 -1
- package/framework/standards/ai-agents/modalities-speech-to-text.md +36 -26
- package/framework/standards/ai-agents/prompt-sources.md +6 -4
- package/framework/standards/ai-agents/providers/model-registry.md +1 -1
- package/framework/standards/ai-agents/rag-custom-pgvector.md +67 -39
- package/framework/standards/ai-agents/setup.md +2 -2
- package/framework/standards/ai-agents/structured-output.md +1 -1
- package/framework/standards/ai-agents/sweet-spot.md +1 -1
- package/framework/standards/architecture/code-comments.md +8 -8
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +18 -9
- package/framework/standards/backend/api/validation.md +135 -47
- package/framework/standards/backend/database/postgresql/database.md +11 -7
- package/framework/standards/backend/database/vector-search-rag.md +207 -324
- package/framework/standards/backend/dotnet/fluent-validation-vsa.md +14 -8
- package/framework/standards/backend/dotnet/result-pattern.md +40 -6
- package/framework/standards/backend/dotnet/scalar-ui.md +13 -7
- package/framework/standards/backend/dotnet/scrutor.md +1 -1
- package/framework/standards/backend/dotnet/testing.md +1 -1
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +7 -7
- package/framework/standards/backend/integrations/asaas/asaas-api.md +309 -165
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +134 -154
- package/framework/standards/data/nosql/cache/redis.md +120 -44
- package/framework/standards/data/nosql/cosmos-db.md +114 -71
- package/framework/standards/frontend/design-system/animations.md +1 -1
- package/framework/standards/frontend/design-system/naming.md +1 -1
- package/framework/standards/frontend/nextjs/motion-patterns.md +45 -29
- package/framework/standards/frontend/nextjs/naming-conventions.md +1 -1
- package/framework/standards/infrastructure/docker/coolify-deploy.md +8 -6
- package/framework/standards/infrastructure/neon/neon-pgvector.md +120 -75
- package/framework/standards/infrastructure/neon/neon-setup.md +53 -31
- 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/code/dotnet/contracts/contracts-vsa.cs +2 -2
- package/framework/templates/code/dotnet/extensions/database-extensions.cs +1 -1
- package/framework/templates/code/dotnet/extensions/result-extensions.cs +27 -0
- package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +2 -2
- 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/MediaDisclosureTests.cs +1 -1
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
- package/framework/templates/infrastructure/docker/coolify.template.json +3 -3
- package/framework/templates/infrastructure/github/README.md +2 -1
- package/framework/templates/infrastructure/github/workflows/deploy-coolify.yml.hbs +2 -1
- 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/observability/telemetry.js +3 -5
- 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 +6 -1
- 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 +33 -19
- package/src/commands/project/worktree.js +1 -1
- package/src/commands/state/advance.js +62 -46
- package/src/commands/state/approve.js +95 -138
- 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 +86 -95
- 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 +41 -82
- package/src/core/paths/output-schema.js +20 -67
- 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/App.jsx +0 -2
- package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
- package/src/dashboard/app/components/CommandPalette.jsx +7 -8
- package/src/dashboard/app/components/EntityLink.jsx +1 -2
- package/src/dashboard/app/components/Markdown.jsx +1 -1
- package/src/dashboard/app/components/Sidebar.jsx +1 -2
- 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/RulesMap.jsx +3 -3
- package/src/dashboard/app/pages/SkillsLibrary.jsx +3 -3
- package/src/dashboard/app/pages/Standards.jsx +1 -1
- package/src/dashboard/scanner/activity-scanner.js +6 -16
- package/src/dashboard/scanner/agents-scanner.js +1 -1
- package/src/dashboard/scanner/docs-scanner.js +5 -23
- package/src/dashboard/scanner/features-scanner.js +95 -60
- package/src/dashboard/scanner/index.js +0 -2
- package/src/dashboard/scanner/project-api.js +1 -1
- package/src/dashboard/scanner/rules-scanner.js +15 -10
- package/src/dashboard/scanner/skills-scanner.js +2 -3
- 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 +134 -135
- 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-profile.js +1 -1
- 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 +24 -48
- package/src/lib/validators/reuse/symbol-scanner.js +2 -4
- package/src/lib/validators/traceability-check.js +85 -129
- package/src/lib/validators/validation-runner.js +8 -88
- package/src/lib/validators/verify-runner.js +134 -301
- package/src/lib/worktree/docker-audit.js +1 -2
- package/src/lib/worktree/fleet-scanner.js +25 -38
- package/src/scripts/setup-infra.js +42 -22
- package/src/utils/agents-installer.js +92 -13
- package/src/utils/claude-settings-manager.js +0 -35
- package/src/utils/file-copier.js +34 -6
- package/src/utils/hooks-installer.js +16 -9
- package/src/utils/skills-installer.js +1 -2
- package/framework/commands/commit.md +0 -166
- 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/rubrics/backend.json +0 -41
- package/framework/rubrics/default.json +0 -36
- package/framework/rubrics/frontend.json +0 -36
- 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/project/validate.js +0 -97
- 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/dashboard/app/pages/Rubrics.jsx +0 -97
- package/src/dashboard/scanner/rubrics-scanner.js +0 -12
- package/src/lib/agents/output-collector.js +0 -76
- package/src/lib/evaluator-report.js +0 -91
- 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
- package/src/lib/validators/architecture/architecture-validator.js +0 -270
- package/src/lib/validators/architecture/index.js +0 -7
- package/src/lib/validators/css/css-validator.js +0 -340
- package/src/lib/validators/css/index.js +0 -7
- package/src/lib/validators/design-system/design-system-validator.js +0 -237
- package/src/lib/validators/design-system/index.js +0 -7
- package/src/lib/validators/score-aggregator.js +0 -244
- package/src/lib/validators/ui/index.js +0 -7
- package/src/lib/validators/ui/ui-contrast-validator.js +0 -422
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The sibling of build-check.js, one layer out: where `runBuildCheck()` proves
|
|
5
5
|
* the code COMPILES, `runE2eCheck()` proves the application RUNS. That gap is
|
|
6
|
-
* not academic
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* and it is invisible to `dotnet build`. Only a real bring-up catches it.
|
|
6
|
+
* not academic: a service injected into a Minimal API endpoint without a
|
|
7
|
+
* matching `AddScoped` takes the whole app down at startup, and it is
|
|
8
|
+
* invisible to `dotnet build`. Only a real bring-up catches it.
|
|
10
9
|
*
|
|
11
10
|
* Two levels of proof, in increasing strength:
|
|
12
11
|
* 1. SMOKE — the stack comes up and every service reports healthy.
|
|
@@ -23,10 +22,12 @@
|
|
|
23
22
|
|
|
24
23
|
import { execFileSync } from 'child_process';
|
|
25
24
|
import { createHash } from 'crypto';
|
|
26
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'fs';
|
|
25
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'fs';
|
|
27
26
|
import { join } from 'path';
|
|
28
27
|
|
|
28
|
+
import { E2E_SPEC_LOCATIONS } from './frontend-locations.js';
|
|
29
29
|
import { isSecondaryWorktree } from './git-paths.js';
|
|
30
|
+
import { resolveSpawn } from './spawn-command.js';
|
|
30
31
|
|
|
31
32
|
/** Compose filenames Docker itself recognises, in the order it resolves them. */
|
|
32
33
|
const COMPOSE_CANDIDATES = [
|
|
@@ -43,9 +44,6 @@ const PLAYWRIGHT_CONFIGS = [
|
|
|
43
44
|
'playwright.config.mjs',
|
|
44
45
|
];
|
|
45
46
|
|
|
46
|
-
/** Task groups that touch runtime and therefore deserve an e2e pass. */
|
|
47
|
-
const RUNTIME_GROUPS = ['frontend', 'backend', 'infra'];
|
|
48
|
-
|
|
49
47
|
/** How long to wait for every service to report healthy. */
|
|
50
48
|
export const DEFAULT_HEALTH_TIMEOUT = 180_000;
|
|
51
49
|
|
|
@@ -72,9 +70,11 @@ const OUTPUT_TAIL_LINES = 20;
|
|
|
72
70
|
* @returns {{ code: number, output: string }}
|
|
73
71
|
*/
|
|
74
72
|
export function defaultExec(file, args, opts = {}) {
|
|
73
|
+
const spawn = resolveSpawn(file, args, { cwd: opts.cwd });
|
|
75
74
|
try {
|
|
76
|
-
const stdout = execFileSync(file, args, {
|
|
75
|
+
const stdout = execFileSync(spawn.file, spawn.args, {
|
|
77
76
|
cwd: opts.cwd,
|
|
77
|
+
shell: spawn.shell,
|
|
78
78
|
timeout: opts.timeout,
|
|
79
79
|
encoding: 'utf-8',
|
|
80
80
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -126,8 +126,7 @@ export function resolveSpecCommand(cwd, config, frontendPath) {
|
|
|
126
126
|
return { command, args, cwd };
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
const
|
|
130
|
-
for (const dir of searchDirs) {
|
|
129
|
+
for (const dir of specSearchDirs(cwd, frontendPath)) {
|
|
131
130
|
const hasConfig = PLAYWRIGHT_CONFIGS.some((name) => existsSync(join(dir, name)));
|
|
132
131
|
// node_modules guard, same rationale as getBuildSteps(): a config file is
|
|
133
132
|
// versioned but the toolchain is not, and `npx playwright` in a fresh
|
|
@@ -139,6 +138,49 @@ export function resolveSpecCommand(cwd, config, frontendPath) {
|
|
|
139
138
|
return null;
|
|
140
139
|
}
|
|
141
140
|
|
|
141
|
+
function specSearchDirs(cwd, frontendPath) {
|
|
142
|
+
return frontendPath ? [join(cwd, frontendPath), cwd] : E2E_SPEC_LOCATIONS.map((loc) => join(cwd, loc));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function hasPlaywrightConfig(dir) {
|
|
146
|
+
return PLAYWRIGHT_CONFIGS.some((name) => existsSync(join(dir, name)));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Configs `resolveSpecCommand` did not run (searched dir without node_modules,
|
|
150
|
+
// or a first-level dir outside the search): each is a smoke pass hiding specs.
|
|
151
|
+
export function findUnrunPlaywrightConfigs(cwd, frontendPath) {
|
|
152
|
+
const searched = frontendPath ? [frontendPath, '.'] : [...E2E_SPEC_LOCATIONS];
|
|
153
|
+
const norm = (rel) => rel.replace(/\\/g, '/').replace(/^\.\/|\/$/g, '') || '.';
|
|
154
|
+
const searchedSet = new Set(searched.map(norm));
|
|
155
|
+
let firstLevel = [];
|
|
156
|
+
try {
|
|
157
|
+
firstLevel = readdirSync(cwd, { withFileTypes: true })
|
|
158
|
+
.filter((e) => e.isDirectory() && e.name !== 'node_modules' && !e.name.startsWith('.'))
|
|
159
|
+
.map((e) => e.name);
|
|
160
|
+
} catch { /* unreadable root: nothing to report */ }
|
|
161
|
+
|
|
162
|
+
const found = [];
|
|
163
|
+
const seen = new Set();
|
|
164
|
+
for (const rel of [...searched.map(norm), ...firstLevel]) {
|
|
165
|
+
if (seen.has(rel)) continue;
|
|
166
|
+
seen.add(rel);
|
|
167
|
+
const dir = join(cwd, rel);
|
|
168
|
+
if (!hasPlaywrightConfig(dir)) continue;
|
|
169
|
+
if (!searchedSet.has(rel)) found.push({ dir: rel, why: 'not-searched' });
|
|
170
|
+
else if (!existsSync(join(dir, 'node_modules'))) found.push({ dir: rel, why: 'no-node-modules' });
|
|
171
|
+
}
|
|
172
|
+
return found;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function unrunConfigWarning({ dir, why }) {
|
|
176
|
+
const where = dir === '.' ? 'na raiz' : `em ${dir}/`;
|
|
177
|
+
const cause = why === 'no-node-modules'
|
|
178
|
+
? 'sem node_modules ao lado'
|
|
179
|
+
: 'fora de project.frontendPath e dos locais conhecidos';
|
|
180
|
+
const fix = dir === '.' ? 'instale as dependências' : `declare project.frontendPath: "${dir}" e instale as dependências`;
|
|
181
|
+
return `playwright.config.* ${where} não rodou (${cause}) — o nó e2e ficou só no smoke; ${fix}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
142
184
|
/**
|
|
143
185
|
* Compose project name for a feature — the isolation key that lets two
|
|
144
186
|
* worktrees run their stacks side by side without fighting over container
|
|
@@ -487,7 +529,7 @@ export function isDockerAvailable(cwd, exec = defaultExec) {
|
|
|
487
529
|
// ============================================================================
|
|
488
530
|
|
|
489
531
|
/**
|
|
490
|
-
* Decide whether this
|
|
532
|
+
* Decide whether this project deserves an e2e run, and if not, why.
|
|
491
533
|
*
|
|
492
534
|
* The load-bearing rule is the third one: a project that declares Docker in
|
|
493
535
|
* `project.integrations` but has no compose file is NOT dispensed — it is a
|
|
@@ -495,13 +537,10 @@ export function isDockerAvailable(cwd, exec = defaultExec) {
|
|
|
495
537
|
* would end up decorative (every project without a compose file collecting a
|
|
496
538
|
* green Gate 3 forever).
|
|
497
539
|
*
|
|
498
|
-
* @param {{
|
|
499
|
-
* integrations?: string[], skipRequested?: boolean }} params
|
|
540
|
+
* @param {{ composeFile: string|null, integrations?: string[], skipRequested?: boolean }} params
|
|
500
541
|
* @returns {{ run: boolean, status?: 'skip'|'fail', reason?: string }}
|
|
501
542
|
*/
|
|
502
543
|
export function decideApplicability({
|
|
503
|
-
taskGroup,
|
|
504
|
-
scope = 'feature',
|
|
505
544
|
composeFile,
|
|
506
545
|
integrations = [],
|
|
507
546
|
skipRequested = false,
|
|
@@ -510,13 +549,6 @@ export function decideApplicability({
|
|
|
510
549
|
return { run: false, status: 'skip', reason: 'requested' };
|
|
511
550
|
}
|
|
512
551
|
|
|
513
|
-
// Task scope only runs for groups that touch runtime; feature scope (Gate 3)
|
|
514
|
-
// always runs. An absent group is treated as runtime-touching — the safe
|
|
515
|
-
// direction, since a missing field must never buy a free pass.
|
|
516
|
-
if (scope === 'task' && taskGroup && !RUNTIME_GROUPS.includes(String(taskGroup).toLowerCase())) {
|
|
517
|
-
return { run: false, status: 'skip', reason: `task group "${taskGroup}" does not touch runtime` };
|
|
518
|
-
}
|
|
519
|
-
|
|
520
552
|
if (!composeFile) {
|
|
521
553
|
if (integrations.map((i) => String(i).toLowerCase()).includes('docker')) {
|
|
522
554
|
return {
|
|
@@ -837,8 +869,6 @@ export function prepareAuth(
|
|
|
837
869
|
* @param {object} [options]
|
|
838
870
|
* @param {object} [options.config] - .morph/config/config.json contents
|
|
839
871
|
* @param {string} [options.featureName] - drives COMPOSE_PROJECT_NAME
|
|
840
|
-
* @param {string|null} [options.taskGroup] - task's `group`, for applicability
|
|
841
|
-
* @param {string} [options.scope] - 'feature' | 'task'
|
|
842
872
|
* @param {string} [options.gitCommonDir] - where the freshness stamp lives
|
|
843
873
|
* @param {object} [options.env] - extra env (port block) for compose
|
|
844
874
|
* @param {boolean} [options.skipRequested]
|
|
@@ -853,8 +883,6 @@ export function runE2eCheck(cwd, options = {}) {
|
|
|
853
883
|
const {
|
|
854
884
|
config,
|
|
855
885
|
featureName = 'default',
|
|
856
|
-
taskGroup = null,
|
|
857
|
-
scope = 'feature',
|
|
858
886
|
gitCommonDir = null,
|
|
859
887
|
env,
|
|
860
888
|
skipRequested = false,
|
|
@@ -864,6 +892,7 @@ export function runE2eCheck(cwd, options = {}) {
|
|
|
864
892
|
healthTimeout = DEFAULT_HEALTH_TIMEOUT,
|
|
865
893
|
specTimeout = DEFAULT_SPEC_TIMEOUT,
|
|
866
894
|
beforeHeavy = null,
|
|
895
|
+
countSpecs = null,
|
|
867
896
|
_exec: exec = defaultExec,
|
|
868
897
|
} = options;
|
|
869
898
|
|
|
@@ -871,7 +900,7 @@ export function runE2eCheck(cwd, options = {}) {
|
|
|
871
900
|
const composeFile = resolveComposeFile(cwd, config);
|
|
872
901
|
const integrations = Array.isArray(config?.project?.integrations) ? config.project.integrations : [];
|
|
873
902
|
|
|
874
|
-
const applicability = decideApplicability({
|
|
903
|
+
const applicability = decideApplicability({ composeFile, integrations, skipRequested });
|
|
875
904
|
if (!applicability.run) {
|
|
876
905
|
return { status: applicability.status, command: null, output: '', reason: applicability.reason, warnings };
|
|
877
906
|
}
|
|
@@ -937,7 +966,8 @@ export function runE2eCheck(cwd, options = {}) {
|
|
|
937
966
|
if (auth.status === 'fail') {
|
|
938
967
|
return { status: 'fail', command: null, output: '', reason: auth.reason, warnings };
|
|
939
968
|
}
|
|
940
|
-
|
|
969
|
+
const specs = runSpecs(cwd, { config, composeFile, up, auth, specTimeout, composeEnv, countSpecs }, exec);
|
|
970
|
+
return { ...specs, warnings: [...warnings, ...(specs.warnings || [])] };
|
|
941
971
|
} finally {
|
|
942
972
|
if (!keepUp) {
|
|
943
973
|
tearDownStack(cwd, { composeFile, projectName, env: composeEnv }, exec);
|
|
@@ -953,8 +983,9 @@ export function runE2eCheck(cwd, options = {}) {
|
|
|
953
983
|
* hardcoded localhost:3000 — ECONNREFUSED that looks exactly like a broken app.
|
|
954
984
|
* `PLAYWRIGHT_BASE_URL` travels in that env for the config to read.
|
|
955
985
|
*/
|
|
956
|
-
function runSpecs(cwd, { config, composeFile, up, auth, specTimeout, composeEnv }, exec) {
|
|
957
|
-
const
|
|
986
|
+
function runSpecs(cwd, { config, composeFile, up, auth, specTimeout, composeEnv, countSpecs }, exec) {
|
|
987
|
+
const frontendPath = config?.project?.frontendPath;
|
|
988
|
+
const spec = resolveSpecCommand(cwd, config, frontendPath);
|
|
958
989
|
const notes = [up.reused ? up.reason : null, auth?.reason].filter(Boolean);
|
|
959
990
|
|
|
960
991
|
if (!spec) {
|
|
@@ -963,11 +994,14 @@ function runSpecs(cwd, { config, composeFile, up, auth, specTimeout, composeEnv
|
|
|
963
994
|
command: `docker compose -f ${composeFile} up -d --wait`,
|
|
964
995
|
output: '',
|
|
965
996
|
reason: joinNotes('smoke only — stack healthy, no Playwright specs found', notes),
|
|
997
|
+
warnings: findUnrunPlaywrightConfigs(cwd, frontendPath).map(unrunConfigWarning),
|
|
966
998
|
};
|
|
967
999
|
}
|
|
968
1000
|
|
|
969
1001
|
const commandLine = `${spec.command} ${spec.args.join(' ')}`;
|
|
970
1002
|
const run = exec(spec.command, spec.args, { cwd: spec.cwd, timeout: specTimeout, env: composeEnv });
|
|
1003
|
+
// Read from the FULL output, before the tail cut; unparseable is null, never 0.
|
|
1004
|
+
const count = typeof countSpecs === 'function' ? (countSpecs(run.output) ?? null) : null;
|
|
971
1005
|
|
|
972
1006
|
if (run.code !== 0) {
|
|
973
1007
|
return {
|
|
@@ -975,6 +1009,7 @@ function runSpecs(cwd, { config, composeFile, up, auth, specTimeout, composeEnv
|
|
|
975
1009
|
command: commandLine,
|
|
976
1010
|
output: tailLines(run.output, OUTPUT_TAIL_LINES),
|
|
977
1011
|
reason: 'e2e specs failed against the running stack',
|
|
1012
|
+
count,
|
|
978
1013
|
};
|
|
979
1014
|
}
|
|
980
1015
|
|
|
@@ -983,6 +1018,7 @@ function runSpecs(cwd, { config, composeFile, up, auth, specTimeout, composeEnv
|
|
|
983
1018
|
command: commandLine,
|
|
984
1019
|
output: '',
|
|
985
1020
|
reason: notes.length ? notes.join('; ') : null,
|
|
1021
|
+
count,
|
|
986
1022
|
};
|
|
987
1023
|
}
|
|
988
1024
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export const ERRATA_HEADING = '## Errata';
|
|
2
|
+
|
|
3
|
+
/** Strip BOM and CRLF — the one normalization every reader of a frozen markdown applies. */
|
|
4
|
+
export function normalizeMarkdown(text) {
|
|
5
|
+
return String(text ?? '').replace(/^/, '').replace(/\r\n?/g, '\n');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Lines with fenced blocks marked, so no heading is read inside ``` or ~~~.
|
|
10
|
+
*
|
|
11
|
+
* @returns {{ raw: string, n: number, fenced: boolean }[]}
|
|
12
|
+
*/
|
|
13
|
+
export function markdownLines(text) {
|
|
14
|
+
const out = [];
|
|
15
|
+
let fence = null;
|
|
16
|
+
normalizeMarkdown(text).split('\n').forEach((raw, i) => {
|
|
17
|
+
const opener = /^\s*(`{3,}|~{3,})/.exec(raw);
|
|
18
|
+
let fenced = Boolean(fence);
|
|
19
|
+
if (fence) {
|
|
20
|
+
if (opener && opener[1][0] === fence[0] && opener[1].length >= fence.length) fence = null;
|
|
21
|
+
} else if (opener) {
|
|
22
|
+
fence = opener[1];
|
|
23
|
+
fenced = true;
|
|
24
|
+
}
|
|
25
|
+
out.push({ raw, n: i + 1, fenced });
|
|
26
|
+
});
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const isErrataHeading = (line) => !line.fenced
|
|
31
|
+
&& /^##\s+errata\s*$/i.test(line.raw);
|
|
32
|
+
|
|
33
|
+
/** Offset of the `## Errata` heading in the NORMALIZED text, or -1. */
|
|
34
|
+
export function errataOffset(text) {
|
|
35
|
+
let offset = 0;
|
|
36
|
+
for (const line of markdownLines(text)) {
|
|
37
|
+
if (isErrataHeading(line)) return offset;
|
|
38
|
+
offset += line.raw.length + 1;
|
|
39
|
+
}
|
|
40
|
+
return -1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Everything before the errata block, normalized, without trailing blank. */
|
|
44
|
+
export function frozenBody(text) {
|
|
45
|
+
const norm = normalizeMarkdown(text);
|
|
46
|
+
const at = errataOffset(norm);
|
|
47
|
+
return (at === -1 ? norm : norm.slice(0, at)).trimEnd();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Is `proposed` the approved text plus an append-only errata? The body stays
|
|
52
|
+
* byte-identical, the result carries an errata block, and what the errata
|
|
53
|
+
* already said is never rewritten.
|
|
54
|
+
*
|
|
55
|
+
* @returns {{ok: true} | {ok: false, why: string}}
|
|
56
|
+
*/
|
|
57
|
+
export function errataViolation(original, proposed) {
|
|
58
|
+
if (frozenBody(proposed) !== frozenBody(original)) {
|
|
59
|
+
return { ok: false, why: 'o corpo aprovado mudou — errata só ACRESCENTA ao fim, nunca altera o texto aprovado' };
|
|
60
|
+
}
|
|
61
|
+
const next = normalizeMarkdown(proposed);
|
|
62
|
+
const nextAt = errataOffset(next);
|
|
63
|
+
if (nextAt === -1) {
|
|
64
|
+
return { ok: false, why: `o conteúdo novo precisa estar sob um bloco \`${ERRATA_HEADING}\` no fim do arquivo` };
|
|
65
|
+
}
|
|
66
|
+
const prev = normalizeMarkdown(original);
|
|
67
|
+
const prevAt = errataOffset(prev);
|
|
68
|
+
if (prevAt !== -1 && !next.slice(nextAt).startsWith(prev.slice(prevAt).trimEnd())) {
|
|
69
|
+
return { ok: false, why: 'a errata existente foi reescrita — ela é append-only, entradas anteriores não se apagam' };
|
|
70
|
+
}
|
|
71
|
+
return { ok: true };
|
|
72
|
+
}
|
|
@@ -100,7 +100,7 @@ export function featureNamedIn(text, names) {
|
|
|
100
100
|
* know" as opposed to "the text names nothing in particular".
|
|
101
101
|
*
|
|
102
102
|
* This is what the single-feature fallback below was missing: a gate question
|
|
103
|
-
* for a feature not yet registered (no `create`, no `
|
|
103
|
+
* for a feature not yet registered (no `create`, no `plan.md` written
|
|
104
104
|
* yet — a fresh hotfix, say) names NO known feature either, and used to fall
|
|
105
105
|
* through to "the tree's one feature must be it". That is exactly how a Gate 3
|
|
106
106
|
* for `backfill-bool-filter` landed on `export-hub`, the tree's only
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// The single list of frontend package locations: `src/lib/detectors` re-exports it,
|
|
2
|
+
// because a hook cannot import `src/`.
|
|
3
|
+
export const PACKAGE_LOCATIONS = ['.', 'src/frontend', 'src/web', 'frontend', 'apps/web', 'packages/app'];
|
|
4
|
+
|
|
5
|
+
// Where `verify`/`e2e` look for a Playwright setup when `project.frontendPath` is absent.
|
|
6
|
+
export const E2E_SPEC_LOCATIONS = [...PACKAGE_LOCATIONS, 'web'];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gate a gate question asks about, or null. Explicit gate names win over
|
|
3
|
+
* words that may also appear in a feature slug ("plan", "design", "review").
|
|
4
|
+
*
|
|
5
|
+
* @param {string} question
|
|
6
|
+
* @returns {'plan'|'uiux'|'review'|null}
|
|
7
|
+
*/
|
|
8
|
+
export function gateFromQuestion(question) {
|
|
9
|
+
if (typeof question !== 'string') return null;
|
|
10
|
+
if (/gate\s+uiux/i.test(question)) return 'uiux';
|
|
11
|
+
if (/gate\s+3\b/i.test(question)) return 'review';
|
|
12
|
+
if (/gate\s+(?:[12]\b|d[oe]\s+plano|plan\b)/i.test(question)) return 'plan';
|
|
13
|
+
const lower = question.toLowerCase();
|
|
14
|
+
if (lower.includes('plano') || lower.includes('plan')) return 'plan';
|
|
15
|
+
if (lower.includes('design')) return 'uiux';
|
|
16
|
+
if (lower.includes('review') || lower.includes('ship')) return 'review';
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
@@ -8,13 +8,12 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
/** Phase order */
|
|
11
|
-
export const PHASE_ORDER = ["
|
|
11
|
+
export const PHASE_ORDER = ["plan","uiux","implement","review","sync"];
|
|
12
12
|
|
|
13
13
|
/** Map phase → allowed output subdirectory */
|
|
14
14
|
export const PHASE_DIRS = {
|
|
15
|
-
proposal: '0-proposal',
|
|
16
|
-
uiux: '1-design',
|
|
17
15
|
plan: '2-plan',
|
|
16
|
+
uiux: '1-design',
|
|
18
17
|
implement: '3-implement',
|
|
19
18
|
review: '4-review',
|
|
20
19
|
sync: '3-implement',
|
|
@@ -22,57 +21,41 @@ export const PHASE_DIRS = {
|
|
|
22
21
|
|
|
23
22
|
/** Map output type (camelCase) → phase that produces it */
|
|
24
23
|
export const OUTPUT_PHASE_MAP = {
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
plan: 'plan',
|
|
25
|
+
checks: 'plan',
|
|
27
26
|
uiDesignSystem: 'uiux',
|
|
28
27
|
uiMockups: 'uiux',
|
|
29
28
|
uiComponents: 'uiux',
|
|
30
29
|
uiFlows: 'uiux',
|
|
31
|
-
spec: 'plan',
|
|
32
|
-
tasks: 'plan',
|
|
33
|
-
decisions: 'plan',
|
|
34
|
-
usecases: 'plan',
|
|
35
|
-
recap: 'implement',
|
|
36
30
|
evidencias: 'implement',
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
verifierReport: 'review',
|
|
32
|
+
reviewerReport: 'review',
|
|
33
|
+
securityReport: 'review',
|
|
39
34
|
};
|
|
40
35
|
|
|
41
36
|
/** Map filename → output type (camelCase) */
|
|
42
37
|
export const FILENAME_TO_OUTPUT = {
|
|
43
|
-
'
|
|
44
|
-
'
|
|
38
|
+
'plan.md': 'plan',
|
|
39
|
+
'checks.json': 'checks',
|
|
45
40
|
'design-system.md': 'uiDesignSystem',
|
|
46
41
|
'mockups.md': 'uiMockups',
|
|
47
42
|
'components.md': 'uiComponents',
|
|
48
43
|
'flows.md': 'uiFlows',
|
|
49
|
-
'
|
|
50
|
-
'
|
|
51
|
-
'
|
|
52
|
-
'recap.md': 'recap',
|
|
53
|
-
'review-report.md': 'reviewReport',
|
|
54
|
-
'evaluator-report.md': 'evaluatorReport',
|
|
44
|
+
'verifier.md': 'verifierReport',
|
|
45
|
+
'reviewer.md': 'reviewerReport',
|
|
46
|
+
'security.md': 'securityReport',
|
|
55
47
|
};
|
|
56
48
|
|
|
57
49
|
/** Protected spec files and the approval gate that locks them */
|
|
58
50
|
export const PROTECTED_SPEC_FILES = {
|
|
51
|
+
'plan.md': 'plan',
|
|
52
|
+
'checks.json': 'plan',
|
|
59
53
|
'design-system.md': 'uiux',
|
|
60
54
|
'mockups.md': 'uiux',
|
|
61
55
|
'components.md': 'uiux',
|
|
62
56
|
'flows.md': 'uiux',
|
|
63
|
-
'spec.md': 'plan',
|
|
64
|
-
'tasks.json': 'plan',
|
|
65
57
|
};
|
|
66
58
|
|
|
67
|
-
/**
|
|
68
|
-
* For protected files that carry mutable progress fields: filename → the
|
|
69
|
-
* top-level array-item fields that may change after the gate is approved.
|
|
70
|
-
* When a protected file is listed here, the protect-spec-files hook does a
|
|
71
|
-
* semantic diff (allowing only these fields to change) instead of a blanket block.
|
|
72
|
-
*/
|
|
73
|
-
export const MUTABLE_SPEC_FIELDS = {
|
|
74
|
-
'tasks.json': ['status', 'outputs', 'notes'],
|
|
75
|
-
};
|
|
76
59
|
|
|
77
60
|
/**
|
|
78
61
|
* Extract feature name from a .morph/features/{feature}/... path
|
|
@@ -88,7 +71,7 @@ export function extractFeatureName(filePath) {
|
|
|
88
71
|
/**
|
|
89
72
|
* Extract phase subdirectory from a .morph/features/{feature}/{phaseDir}/... path
|
|
90
73
|
* @param {string} filePath - File path to analyze
|
|
91
|
-
* @returns {string|null} Phase directory (e.g., '
|
|
74
|
+
* @returns {string|null} Phase directory (e.g., '2-plan', '1-design') or null
|
|
92
75
|
*/
|
|
93
76
|
export function extractPhaseDir(filePath) {
|
|
94
77
|
const normalized = filePath.replace(/\\/g, '/');
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
/** `PARCIAL` on the report's first non-blank line, tolerating heading/bold/quote typography. */
|
|
5
|
+
export const PARTIAL_MARKER = /^[\s#>*_]*PARCIAL\b/;
|
|
6
|
+
|
|
7
|
+
export const SLICE_STATUSES = ['pendente', 'parcial', 'feito'];
|
|
8
|
+
|
|
9
|
+
export function checksPath(featureDir) {
|
|
10
|
+
return join(featureDir, '2-plan', 'checks.json');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function sliceReportPath(featureDir, sliceId) {
|
|
14
|
+
return join(featureDir, '3-implement', 'reports', `${sliceId}.md`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Status of a slice from its report text: no report = pendente, PARCIAL first line = parcial, else feito. */
|
|
18
|
+
export function sliceStatusFromReport(text) {
|
|
19
|
+
if (text == null) return 'pendente';
|
|
20
|
+
const first = String(text).split(/\r?\n/).find((l) => l.trim() !== '') ?? '';
|
|
21
|
+
return PARTIAL_MARKER.test(first) ? 'parcial' : 'feito';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Progress of a feature's slices.
|
|
26
|
+
*
|
|
27
|
+
* @param {string|null} checksText - the raw extracted checks file of the plan
|
|
28
|
+
* @param {(sliceId: string) => string|null} readReport - report text, null when absent
|
|
29
|
+
* @returns {{ total: number, done: number, partial: number, pending: number,
|
|
30
|
+
* slices: { id: string, title: string, depends: string[], covers: string[], status: string }[] } | null} null when that file is unreadable
|
|
31
|
+
*/
|
|
32
|
+
export function sliceProgressFrom(checksText, readReport) {
|
|
33
|
+
let slices;
|
|
34
|
+
try {
|
|
35
|
+
slices = JSON.parse(checksText ?? '').slices;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
if (!Array.isArray(slices)) return null;
|
|
40
|
+
const out = slices
|
|
41
|
+
.filter((s) => s && typeof s.id === 'string')
|
|
42
|
+
.map((s) => ({
|
|
43
|
+
id: s.id,
|
|
44
|
+
title: s.title ?? '',
|
|
45
|
+
depends: Array.isArray(s.depends) ? s.depends : [],
|
|
46
|
+
covers: Array.isArray(s.covers) ? s.covers : [],
|
|
47
|
+
status: sliceStatusFromReport(readReport(s.id)),
|
|
48
|
+
}));
|
|
49
|
+
const count = (status) => out.filter((s) => s.status === status).length;
|
|
50
|
+
return { total: out.length, done: count('feito'), partial: count('parcial'), pending: count('pendente'), slices: out };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readOrNull(path) {
|
|
54
|
+
try {
|
|
55
|
+
return existsSync(path) ? readFileSync(path, 'utf8') : null;
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Slice progress of the feature directory on disk. */
|
|
62
|
+
export function sliceProgress(featureDir) {
|
|
63
|
+
return sliceProgressFrom(readOrNull(checksPath(featureDir)), (id) => readOrNull(sliceReportPath(featureDir, id)));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Unfinished slices whose dependencies are all done: what the next dispatch can take. */
|
|
67
|
+
export function readySlices(progress) {
|
|
68
|
+
if (!progress) return [];
|
|
69
|
+
const done = new Set(progress.slices.filter((s) => s.status === 'feito').map((s) => s.id));
|
|
70
|
+
return progress.slices.filter((s) => s.status !== 'feito' && s.depends.every((d) => done.has(d)));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* What a slice report must carry, besides prose and its covered scenarios.
|
|
75
|
+
* Loose on purpose: a false "complete" costs a read, a false "incomplete"
|
|
76
|
+
* re-dispatches finished work.
|
|
77
|
+
*/
|
|
78
|
+
export const REPORT_MARKERS = [
|
|
79
|
+
{ id: 'files', re: /^\s*(?:[-*]\s*)?(?:\*\*|#+\s*)?arquivos\s*:?\**\s*:?/mi },
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
const MIN_PROSE_CHARS = 80;
|
|
83
|
+
|
|
84
|
+
const scenarioRe = (id) => new RegExp(`(?<![\\w-])${id}(?![\\w-])`);
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Inspect one report file: does it exist, which mandatory items it lacks,
|
|
88
|
+
* and whether it is partial.
|
|
89
|
+
*
|
|
90
|
+
* @param {string} path
|
|
91
|
+
* @param {{ covers?: string[] }} [slice] - every scenario the slice covers must be named
|
|
92
|
+
*/
|
|
93
|
+
export function inspectReport(path, { covers = [] } = {}) {
|
|
94
|
+
const missingAll = ['what', 'files', ...covers];
|
|
95
|
+
if (!existsSync(path)) return { exists: false, complete: false, missing: missingAll };
|
|
96
|
+
let body = '';
|
|
97
|
+
try {
|
|
98
|
+
body = readFileSync(path, 'utf8');
|
|
99
|
+
} catch {
|
|
100
|
+
return { exists: true, complete: false, missing: missingAll, unreadable: true };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const missing = REPORT_MARKERS.filter((m) => !m.re.test(body)).map((m) => m.id);
|
|
104
|
+
missing.push(...covers.filter((id) => !scenarioRe(id).test(body)));
|
|
105
|
+
const prose = body
|
|
106
|
+
.split('\n')
|
|
107
|
+
.filter((l) => !/^\s*(#|[-*]\s|\*\*)/.test(l))
|
|
108
|
+
.join(' ')
|
|
109
|
+
.replace(/\s+/g, ' ')
|
|
110
|
+
.trim();
|
|
111
|
+
if (prose.length < MIN_PROSE_CHARS) missing.unshift('what');
|
|
112
|
+
|
|
113
|
+
const firstLine = body.split('\n').find((l) => l.trim() !== '') ?? '';
|
|
114
|
+
const partial = PARTIAL_MARKER.test(firstLine);
|
|
115
|
+
if (partial) missing.push('partial');
|
|
116
|
+
|
|
117
|
+
return { exists: true, complete: missing.length === 0, missing, partial };
|
|
118
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
const NEEDS_CMD_SHIM = /^(npm|npx|pnpm|yarn)$/;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* On win32 `npx`/`npm`/… are `.cmd` shims: ENOENT without a shell, EINVAL as `.cmd`
|
|
8
|
+
* with `shell: false`. Playwright runs from its own JS entry, so it needs no shell.
|
|
9
|
+
*/
|
|
10
|
+
export function resolveSpawn(file, args = [], {
|
|
11
|
+
cwd = process.cwd(),
|
|
12
|
+
platform = process.platform,
|
|
13
|
+
exists = existsSync,
|
|
14
|
+
execPath = process.execPath,
|
|
15
|
+
} = {}) {
|
|
16
|
+
if (platform !== 'win32' || !NEEDS_CMD_SHIM.test(file)) return { file, args, shell: false };
|
|
17
|
+
|
|
18
|
+
if (file === 'npx' && args[0] === 'playwright') {
|
|
19
|
+
const cli = join(cwd, 'node_modules', '@playwright', 'test', 'cli.js');
|
|
20
|
+
if (exists(cli)) return { file: execPath, args: [cli, ...args.slice(1)], shell: false };
|
|
21
|
+
}
|
|
22
|
+
return { file: `${file}.cmd`, args, shell: true };
|
|
23
|
+
}
|