playwright-intelligence-engine 0.0.0-stage → 2.0.1
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/LICENSE +191 -0
- package/README.md +229 -3
- package/bin/pw-intel.js +37 -0
- package/dist/authoring/author.d.ts +83 -0
- package/dist/authoring/author.d.ts.map +1 -0
- package/dist/authoring/generation-contract.d.ts +90 -0
- package/dist/authoring/generation-contract.d.ts.map +1 -0
- package/dist/authoring/hybrid-author.d.ts +6 -0
- package/dist/authoring/hybrid-author.d.ts.map +1 -0
- package/dist/authoring/index.d.ts +7 -0
- package/dist/authoring/index.d.ts.map +1 -0
- package/dist/authoring/llm-author.d.ts +28 -0
- package/dist/authoring/llm-author.d.ts.map +1 -0
- package/dist/authoring/naming/index.d.ts +4 -0
- package/dist/authoring/naming/index.d.ts.map +1 -0
- package/dist/authoring/naming/name-plan.d.ts +45 -0
- package/dist/authoring/naming/name-plan.d.ts.map +1 -0
- package/dist/authoring/naming/name-planner.d.ts +25 -0
- package/dist/authoring/naming/name-planner.d.ts.map +1 -0
- package/dist/authoring/naming/naming-linter.d.ts +20 -0
- package/dist/authoring/naming/naming-linter.d.ts.map +1 -0
- package/dist/authoring/template-author.d.ts +6 -0
- package/dist/authoring/template-author.d.ts.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/collectors/collector-interface.d.ts +7 -0
- package/dist/collectors/collector-interface.d.ts.map +1 -0
- package/dist/collectors/environment-collector.d.ts +12 -0
- package/dist/collectors/environment-collector.d.ts.map +1 -0
- package/dist/collectors/error-collector.d.ts +18 -0
- package/dist/collectors/error-collector.d.ts.map +1 -0
- package/dist/collectors/git-diff-collector.d.ts +20 -0
- package/dist/collectors/git-diff-collector.d.ts.map +1 -0
- package/dist/collectors/live-browser-collector.d.ts +45 -0
- package/dist/collectors/live-browser-collector.d.ts.map +1 -0
- package/dist/collectors/network-collector.d.ts +28 -0
- package/dist/collectors/network-collector.d.ts.map +1 -0
- package/dist/collectors/static-ast-collector.d.ts +32 -0
- package/dist/collectors/static-ast-collector.d.ts.map +1 -0
- package/dist/collectors/trace-collector.d.ts +12 -0
- package/dist/collectors/trace-collector.d.ts.map +1 -0
- package/dist/config/project-config.d.ts +33 -0
- package/dist/config/project-config.d.ts.map +1 -0
- package/dist/core/errors/engine-errors.d.ts +34 -0
- package/dist/core/errors/engine-errors.d.ts.map +1 -0
- package/dist/core/types/diagnostic-context.d.ts +151 -0
- package/dist/core/types/diagnostic-context.d.ts.map +1 -0
- package/dist/core/types/repair-evidence.d.ts +56 -0
- package/dist/core/types/repair-evidence.d.ts.map +1 -0
- package/dist/core/version.d.ts +13 -0
- package/dist/core/version.d.ts.map +1 -0
- package/dist/diagnostics/diagnostic-contract.d.ts +52 -0
- package/dist/diagnostics/diagnostic-contract.d.ts.map +1 -0
- package/dist/diagnostics/diagnostic-report.d.ts +11 -0
- package/dist/diagnostics/diagnostic-report.d.ts.map +1 -0
- package/dist/diagnostics/evidence-correlator.d.ts +22 -0
- package/dist/diagnostics/evidence-correlator.d.ts.map +1 -0
- package/dist/diagnostics/external-failure-evidence.d.ts +18 -0
- package/dist/diagnostics/external-failure-evidence.d.ts.map +1 -0
- package/dist/diagnostics/failure-category.d.ts +4 -0
- package/dist/diagnostics/failure-category.d.ts.map +1 -0
- package/dist/diagnostics/failure-classifier.d.ts +58 -0
- package/dist/diagnostics/failure-classifier.d.ts.map +1 -0
- package/dist/evaluation/evaluation-scorecard.d.ts +72 -0
- package/dist/evaluation/evaluation-scorecard.d.ts.map +1 -0
- package/dist/evaluation/repository-inspector.d.ts +21 -0
- package/dist/evaluation/repository-inspector.d.ts.map +1 -0
- package/dist/evaluation/repository-runner.d.ts +24 -0
- package/dist/evaluation/repository-runner.d.ts.map +1 -0
- package/dist/evidence/evidence-model.d.ts +88 -0
- package/dist/evidence/evidence-model.d.ts.map +1 -0
- package/dist/evidence/evidence-normalizer.d.ts +19 -0
- package/dist/evidence/evidence-normalizer.d.ts.map +1 -0
- package/dist/evidence/evidence-redactor.d.ts +37 -0
- package/dist/evidence/evidence-redactor.d.ts.map +1 -0
- package/dist/framework/config-adapter.d.ts +28 -0
- package/dist/framework/config-adapter.d.ts.map +1 -0
- package/dist/gate/index.d.ts +21 -0
- package/dist/gate/index.d.ts.map +1 -0
- package/dist/gate/oracle.d.ts +26 -0
- package/dist/gate/oracle.d.ts.map +1 -0
- package/dist/gate/quality.d.ts +27 -0
- package/dist/gate/quality.d.ts.map +1 -0
- package/dist/gate/safety.d.ts +28 -0
- package/dist/gate/safety.d.ts.map +1 -0
- package/dist/generator/architecture-generator.d.ts +55 -0
- package/dist/generator/architecture-generator.d.ts.map +1 -0
- package/dist/generator/assertion-quality-gate.d.ts +18 -0
- package/dist/generator/assertion-quality-gate.d.ts.map +1 -0
- package/dist/generator/candidate-writer.d.ts +34 -0
- package/dist/generator/candidate-writer.d.ts.map +1 -0
- package/dist/generator/test-generator.d.ts +26 -0
- package/dist/generator/test-generator.d.ts.map +1 -0
- package/dist/governance/audit-log.d.ts +23 -0
- package/dist/governance/audit-log.d.ts.map +1 -0
- package/dist/governance/governance-policy.d.ts +24 -0
- package/dist/governance/governance-policy.d.ts.map +1 -0
- package/dist/governance/team-dashboard.d.ts +51 -0
- package/dist/governance/team-dashboard.d.ts.map +1 -0
- package/dist/governance/workflow-audit.d.ts +13 -0
- package/dist/governance/workflow-audit.d.ts.map +1 -0
- package/dist/index.d.ts +90 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20318 -0
- package/dist/integrations/ci-summary.d.ts +14 -0
- package/dist/integrations/ci-summary.d.ts.map +1 -0
- package/dist/integrations/playwright-report-adapter.d.ts +22 -0
- package/dist/integrations/playwright-report-adapter.d.ts.map +1 -0
- package/dist/integrations/quality-gate.d.ts +35 -0
- package/dist/integrations/quality-gate.d.ts.map +1 -0
- package/dist/integrations/sarif-report.d.ts +48 -0
- package/dist/integrations/sarif-report.d.ts.map +1 -0
- package/dist/intelligence/autonomous-repair-workflow.d.ts +31 -0
- package/dist/intelligence/autonomous-repair-workflow.d.ts.map +1 -0
- package/dist/intelligence/business-intent.d.ts +58 -0
- package/dist/intelligence/business-intent.d.ts.map +1 -0
- package/dist/intelligence/evidence-bundle.d.ts +364 -0
- package/dist/intelligence/evidence-bundle.d.ts.map +1 -0
- package/dist/intelligence/failure-clustering.d.ts +38 -0
- package/dist/intelligence/failure-clustering.d.ts.map +1 -0
- package/dist/intelligence/generation-agent.d.ts +120 -0
- package/dist/intelligence/generation-agent.d.ts.map +1 -0
- package/dist/intelligence/generation-verifier.d.ts +151 -0
- package/dist/intelligence/generation-verifier.d.ts.map +1 -0
- package/dist/intelligence/llm-proposal.d.ts +377 -0
- package/dist/intelligence/llm-proposal.d.ts.map +1 -0
- package/dist/intelligence/locator-disambiguation-engine.d.ts +38 -0
- package/dist/intelligence/locator-disambiguation-engine.d.ts.map +1 -0
- package/dist/intelligence/model-contract.d.ts +86 -0
- package/dist/intelligence/model-contract.d.ts.map +1 -0
- package/dist/intelligence/model-provider.d.ts +75 -0
- package/dist/intelligence/model-provider.d.ts.map +1 -0
- package/dist/intelligence/project-intelligence.d.ts +22 -0
- package/dist/intelligence/project-intelligence.d.ts.map +1 -0
- package/dist/intelligence/repair-agent.d.ts +46 -0
- package/dist/intelligence/repair-agent.d.ts.map +1 -0
- package/dist/intelligence/repository-profile.d.ts +131 -0
- package/dist/intelligence/repository-profile.d.ts.map +1 -0
- package/dist/intelligence/suite-intelligence.d.ts +59 -0
- package/dist/intelligence/suite-intelligence.d.ts.map +1 -0
- package/dist/intelligence/test-intelligence.d.ts +44 -0
- package/dist/intelligence/test-intelligence.d.ts.map +1 -0
- package/dist/intelligence/trace-proposal-workflow.d.ts +30 -0
- package/dist/intelligence/trace-proposal-workflow.d.ts.map +1 -0
- package/dist/intelligence/verified-llm-repair-workflow.d.ts +21 -0
- package/dist/intelligence/verified-llm-repair-workflow.d.ts.map +1 -0
- package/dist/intelligence/workflow-planner.d.ts +278 -0
- package/dist/intelligence/workflow-planner.d.ts.map +1 -0
- package/dist/mcp/server.d.ts +32 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/memory/stability-ledger.d.ts +46 -0
- package/dist/memory/stability-ledger.d.ts.map +1 -0
- package/dist/perception/app-model.d.ts +55 -0
- package/dist/perception/app-model.d.ts.map +1 -0
- package/dist/perception/crawler.d.ts +18 -0
- package/dist/perception/crawler.d.ts.map +1 -0
- package/dist/perception/index.d.ts +3 -0
- package/dist/perception/index.d.ts.map +1 -0
- package/dist/repair/approval-service.d.ts +139 -0
- package/dist/repair/approval-service.d.ts.map +1 -0
- package/dist/repair/assertion-hasher.d.ts +62 -0
- package/dist/repair/assertion-hasher.d.ts.map +1 -0
- package/dist/repair/ast-patcher.d.ts +36 -0
- package/dist/repair/ast-patcher.d.ts.map +1 -0
- package/dist/repair/browser-operation-executor.d.ts +27 -0
- package/dist/repair/browser-operation-executor.d.ts.map +1 -0
- package/dist/repair/diff-allowlist.d.ts +74 -0
- package/dist/repair/diff-allowlist.d.ts.map +1 -0
- package/dist/repair/intent-validator.d.ts +157 -0
- package/dist/repair/intent-validator.d.ts.map +1 -0
- package/dist/repair/repair-operations.d.ts +40 -0
- package/dist/repair/repair-operations.d.ts.map +1 -0
- package/dist/repair/repair-planner.d.ts +98 -0
- package/dist/repair/repair-planner.d.ts.map +1 -0
- package/dist/repair/repair-workflow.d.ts +42 -0
- package/dist/repair/repair-workflow.d.ts.map +1 -0
- package/dist/repair/safe-operation-compiler.d.ts +15 -0
- package/dist/repair/safe-operation-compiler.d.ts.map +1 -0
- package/dist/repair/source-mapper.d.ts +19 -0
- package/dist/repair/source-mapper.d.ts.map +1 -0
- package/dist/repair/statement-patcher.d.ts +75 -0
- package/dist/repair/statement-patcher.d.ts.map +1 -0
- package/dist/repair/verification-gates.d.ts +25 -0
- package/dist/repair/verification-gates.d.ts.map +1 -0
- package/dist/repair/workflow-plan.d.ts +35 -0
- package/dist/repair/workflow-plan.d.ts.map +1 -0
- package/dist/repair/workspace-promoter.d.ts +43 -0
- package/dist/repair/workspace-promoter.d.ts.map +1 -0
- package/dist/reproduce/diagnostic-rerunner.d.ts +40 -0
- package/dist/reproduce/diagnostic-rerunner.d.ts.map +1 -0
- package/dist/reproduce/direct-browser-investigator.d.ts +41 -0
- package/dist/reproduce/direct-browser-investigator.d.ts.map +1 -0
- package/dist/reproduce/interactive-workflow-runner.d.ts +43 -0
- package/dist/reproduce/interactive-workflow-runner.d.ts.map +1 -0
- package/dist/sandbox/dependency-strategy.d.ts +45 -0
- package/dist/sandbox/dependency-strategy.d.ts.map +1 -0
- package/dist/sandbox/execution-runner.d.ts +81 -0
- package/dist/sandbox/execution-runner.d.ts.map +1 -0
- package/dist/sandbox/worktree-manager.d.ts +51 -0
- package/dist/sandbox/worktree-manager.d.ts.map +1 -0
- package/dist/selector/abstention-policy.d.ts +34 -0
- package/dist/selector/abstention-policy.d.ts.map +1 -0
- package/dist/selector/candidate-extractor.d.ts +71 -0
- package/dist/selector/candidate-extractor.d.ts.map +1 -0
- package/dist/selector/live-locator-verifier.d.ts +28 -0
- package/dist/selector/live-locator-verifier.d.ts.map +1 -0
- package/dist/selector/repair-candidate-ranker.d.ts +10 -0
- package/dist/selector/repair-candidate-ranker.d.ts.map +1 -0
- package/dist/selector/selector-scorer.d.ts +43 -0
- package/dist/selector/selector-scorer.d.ts.map +1 -0
- package/dist/trace/adapters/adapter-registry.d.ts +18 -0
- package/dist/trace/adapters/adapter-registry.d.ts.map +1 -0
- package/dist/trace/adapters/trace-adapter.d.ts +21 -0
- package/dist/trace/adapters/trace-adapter.d.ts.map +1 -0
- package/dist/trace/adapters/unknown-adapter.d.ts +10 -0
- package/dist/trace/adapters/unknown-adapter.d.ts.map +1 -0
- package/dist/trace/adapters/v10-adapter.d.ts +10 -0
- package/dist/trace/adapters/v10-adapter.d.ts.map +1 -0
- package/dist/trace/adapters/v4-adapter.d.ts +11 -0
- package/dist/trace/adapters/v4-adapter.d.ts.map +1 -0
- package/dist/trace/adapters/v8-adapter.d.ts +10 -0
- package/dist/trace/adapters/v8-adapter.d.ts.map +1 -0
- package/dist/trace/adapters/v9-adapter.d.ts +10 -0
- package/dist/trace/adapters/v9-adapter.d.ts.map +1 -0
- package/dist/trace/archive-reader.d.ts +44 -0
- package/dist/trace/archive-reader.d.ts.map +1 -0
- package/dist/trace/snapshot-resolver.d.ts +55 -0
- package/dist/trace/snapshot-resolver.d.ts.map +1 -0
- package/dist/trace/trace-parser.d.ts +50 -0
- package/dist/trace/trace-parser.d.ts.map +1 -0
- package/dist/trace/version-detector.d.ts +20 -0
- package/dist/trace/version-detector.d.ts.map +1 -0
- package/package.json +76 -6
- package/skills/README.md +20 -0
- package/skills/pie-failure-investigation/SKILL.md +16 -0
- package/skills/pie-quality-engineer/SKILL.md +66 -0
- package/skills/pie-release-readiness/SKILL.md +22 -0
- package/skills/pie-repair-review/SKILL.md +21 -0
- package/skills/pie-trace-to-repair/SKILL.md +35 -0
- package/src/authoring/author.ts +81 -0
- package/src/authoring/generation-contract.ts +283 -0
- package/src/authoring/hybrid-author.ts +115 -0
- package/src/authoring/index.ts +7 -0
- package/src/authoring/llm-author.ts +282 -0
- package/src/authoring/naming/index.ts +4 -0
- package/src/authoring/naming/name-plan.ts +28 -0
- package/src/authoring/naming/name-planner.ts +339 -0
- package/src/authoring/naming/naming-linter.ts +292 -0
- package/src/authoring/template-author.ts +39 -0
- package/src/cli/index.ts +1172 -0
- package/src/collectors/collector-interface.ts +8 -0
- package/src/collectors/environment-collector.ts +103 -0
- package/src/collectors/error-collector.ts +177 -0
- package/src/collectors/git-diff-collector.ts +190 -0
- package/src/collectors/live-browser-collector.ts +412 -0
- package/src/collectors/network-collector.ts +90 -0
- package/src/collectors/static-ast-collector.ts +226 -0
- package/src/collectors/trace-collector.ts +66 -0
- package/src/config/project-config.ts +114 -0
- package/src/core/errors/engine-errors.ts +90 -0
- package/src/core/types/diagnostic-context.ts +94 -0
- package/src/core/types/repair-evidence.ts +45 -0
- package/src/core/version.ts +25 -0
- package/src/diagnostics/diagnostic-contract.ts +79 -0
- package/src/diagnostics/diagnostic-report.ts +204 -0
- package/src/diagnostics/evidence-correlator.ts +117 -0
- package/src/diagnostics/external-failure-evidence.ts +72 -0
- package/src/diagnostics/failure-category.ts +40 -0
- package/src/diagnostics/failure-classifier.ts +701 -0
- package/src/evaluation/evaluation-scorecard.ts +198 -0
- package/src/evaluation/repository-inspector.ts +143 -0
- package/src/evaluation/repository-runner.ts +197 -0
- package/src/evidence/evidence-model.ts +76 -0
- package/src/evidence/evidence-normalizer.ts +322 -0
- package/src/evidence/evidence-redactor.ts +218 -0
- package/src/framework/config-adapter.ts +131 -0
- package/src/gate/index.ts +43 -0
- package/src/gate/oracle.ts +69 -0
- package/src/gate/quality.ts +101 -0
- package/src/gate/safety.ts +94 -0
- package/src/generator/architecture-generator.ts +582 -0
- package/src/generator/assertion-quality-gate.ts +88 -0
- package/src/generator/candidate-writer.ts +288 -0
- package/src/generator/test-generator.ts +52 -0
- package/src/governance/audit-log.ts +115 -0
- package/src/governance/governance-policy.ts +72 -0
- package/src/governance/team-dashboard.ts +189 -0
- package/src/governance/workflow-audit.ts +72 -0
- package/src/index.ts +107 -0
- package/src/integrations/ci-summary.ts +37 -0
- package/src/integrations/playwright-report-adapter.ts +173 -0
- package/src/integrations/quality-gate.ts +95 -0
- package/src/integrations/sarif-report.ts +110 -0
- package/src/intelligence/autonomous-repair-workflow.ts +139 -0
- package/src/intelligence/business-intent.ts +163 -0
- package/src/intelligence/evidence-bundle.ts +300 -0
- package/src/intelligence/failure-clustering.ts +134 -0
- package/src/intelligence/generation-agent.ts +561 -0
- package/src/intelligence/generation-verifier.ts +1115 -0
- package/src/intelligence/llm-proposal.ts +519 -0
- package/src/intelligence/locator-disambiguation-engine.ts +331 -0
- package/src/intelligence/model-contract.ts +205 -0
- package/src/intelligence/model-provider.ts +348 -0
- package/src/intelligence/project-intelligence.ts +140 -0
- package/src/intelligence/repair-agent.ts +297 -0
- package/src/intelligence/repository-profile.ts +796 -0
- package/src/intelligence/suite-intelligence.ts +305 -0
- package/src/intelligence/test-intelligence.ts +136 -0
- package/src/intelligence/trace-proposal-workflow.ts +318 -0
- package/src/intelligence/verified-llm-repair-workflow.ts +48 -0
- package/src/intelligence/workflow-planner.ts +966 -0
- package/src/mcp/server.ts +970 -0
- package/src/memory/stability-ledger.ts +243 -0
- package/src/perception/app-model.ts +32 -0
- package/src/perception/crawler.ts +263 -0
- package/src/perception/index.ts +3 -0
- package/src/repair/approval-service.ts +541 -0
- package/src/repair/assertion-hasher.ts +386 -0
- package/src/repair/ast-patcher.ts +223 -0
- package/src/repair/browser-operation-executor.ts +263 -0
- package/src/repair/diff-allowlist.ts +612 -0
- package/src/repair/intent-validator.ts +761 -0
- package/src/repair/repair-operations.ts +65 -0
- package/src/repair/repair-planner.ts +467 -0
- package/src/repair/repair-workflow.ts +241 -0
- package/src/repair/safe-operation-compiler.ts +68 -0
- package/src/repair/source-mapper.ts +156 -0
- package/src/repair/statement-patcher.ts +544 -0
- package/src/repair/verification-gates.ts +46 -0
- package/src/repair/workflow-plan.ts +59 -0
- package/src/repair/workspace-promoter.ts +313 -0
- package/src/reproduce/diagnostic-rerunner.ts +287 -0
- package/src/reproduce/direct-browser-investigator.ts +208 -0
- package/src/reproduce/interactive-workflow-runner.ts +828 -0
- package/src/sandbox/dependency-strategy.ts +215 -0
- package/src/sandbox/execution-runner.ts +408 -0
- package/src/sandbox/worktree-manager.ts +287 -0
- package/src/selector/abstention-policy.ts +175 -0
- package/src/selector/candidate-extractor.ts +343 -0
- package/src/selector/live-locator-verifier.ts +139 -0
- package/src/selector/repair-candidate-ranker.ts +92 -0
- package/src/selector/selector-scorer.ts +344 -0
- package/src/trace/adapters/adapter-registry.ts +49 -0
- package/src/trace/adapters/trace-adapter.ts +28 -0
- package/src/trace/adapters/unknown-adapter.ts +50 -0
- package/src/trace/adapters/v10-adapter.ts +39 -0
- package/src/trace/adapters/v4-adapter.ts +31 -0
- package/src/trace/adapters/v8-adapter.ts +42 -0
- package/src/trace/adapters/v9-adapter.ts +39 -0
- package/src/trace/archive-reader.ts +246 -0
- package/src/trace/snapshot-resolver.ts +308 -0
- package/src/trace/trace-parser.ts +467 -0
- package/src/trace/version-detector.ts +81 -0
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import type { GeneratedStep } from './test-generator.js';
|
|
4
|
+
import type { RepositoryConventionProfile } from '../intelligence/repository-profile.js';
|
|
5
|
+
import { NamePlanner, NamingLinter, type NamePlan, type NamingLintReport } from '../authoring/naming/index.js';
|
|
6
|
+
import type { AppModel } from '../perception/app-model.js';
|
|
7
|
+
import { AssertionQualityGate } from './assertion-quality-gate.js';
|
|
8
|
+
|
|
9
|
+
export type ArchitectureMode = 'greenfield' | 'brownfield';
|
|
10
|
+
export type ArchitecturePlanStatus = 'READY' | 'PARTIAL' | 'ABSTAIN' | 'NEEDS_INPUT';
|
|
11
|
+
|
|
12
|
+
export interface ArchitectureGenerationInput {
|
|
13
|
+
featureName: string;
|
|
14
|
+
mode: ArchitectureMode;
|
|
15
|
+
baseUrl?: string | undefined;
|
|
16
|
+
requirements?: string | undefined;
|
|
17
|
+
steps?: GeneratedStep[] | undefined;
|
|
18
|
+
profile?: RepositoryConventionProfile | undefined;
|
|
19
|
+
existingFiles?: string[] | undefined;
|
|
20
|
+
includeCi?: boolean | undefined;
|
|
21
|
+
namePlan?: NamePlan | undefined;
|
|
22
|
+
appModel?: AppModel | undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ArchitectureFile {
|
|
26
|
+
relativePath: string;
|
|
27
|
+
content: string;
|
|
28
|
+
purpose: string;
|
|
29
|
+
action: 'CREATE' | 'MODIFY' | 'SKIP_EXISTING';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ArchitecturePlan {
|
|
33
|
+
mode: ArchitectureMode;
|
|
34
|
+
status: ArchitecturePlanStatus;
|
|
35
|
+
featureName: string;
|
|
36
|
+
slug: string;
|
|
37
|
+
conventions: {
|
|
38
|
+
testDirectory: string;
|
|
39
|
+
pageObjectDirectory: string;
|
|
40
|
+
fixtureDirectory: string;
|
|
41
|
+
preferredLocator: string;
|
|
42
|
+
usesCustomFixtures: boolean;
|
|
43
|
+
usesStorageState: boolean;
|
|
44
|
+
};
|
|
45
|
+
assumptions: string[];
|
|
46
|
+
warnings: string[];
|
|
47
|
+
files: ArchitectureFile[];
|
|
48
|
+
namePlan?: NamePlan | undefined;
|
|
49
|
+
namingLintReport?: NamingLintReport | undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ArchitectureWriteResult {
|
|
53
|
+
written: string[];
|
|
54
|
+
skipped: string[];
|
|
55
|
+
modified?: string[] | undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function quote(value: string): string {
|
|
59
|
+
return JSON.stringify(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function toSlug(value: string): string {
|
|
63
|
+
return value
|
|
64
|
+
.trim()
|
|
65
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
66
|
+
.replace(/[^a-zA-Z0-9]+/g, '-')
|
|
67
|
+
.replace(/^-+|-+$/g, '')
|
|
68
|
+
.toLowerCase() || 'feature';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function toClassName(value: string): string {
|
|
72
|
+
const words = value
|
|
73
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
74
|
+
.match(/[A-Za-z0-9]+/g) ?? ['Feature'];
|
|
75
|
+
const className = words
|
|
76
|
+
.map((word) => word[0]!.toUpperCase() + word.slice(1).toLowerCase())
|
|
77
|
+
.join('');
|
|
78
|
+
return /^[A-Za-z_]/.test(className) ? className : `Feature${className}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeRelativeDirectory(value: string | undefined, fallback: string): string {
|
|
82
|
+
const normalized = (value ?? fallback).replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '');
|
|
83
|
+
if (!normalized || normalized === '.') return fallback;
|
|
84
|
+
if (normalized.startsWith('/') || normalized.split('/').includes('..')) return fallback;
|
|
85
|
+
return normalized;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function inferBehavioralSteps(featureName: string, appModel?: AppModel): GeneratedStep[] | undefined {
|
|
89
|
+
if (!appModel || !appModel.routes || appModel.routes.length === 0) return undefined;
|
|
90
|
+
|
|
91
|
+
const targetSlug = toSlug(featureName);
|
|
92
|
+
let route = appModel.routes.find((r) =>
|
|
93
|
+
r.route.toLowerCase().includes(targetSlug) || (r.title && r.title.toLowerCase().includes(featureName.toLowerCase()))
|
|
94
|
+
);
|
|
95
|
+
if (!route) {
|
|
96
|
+
route = appModel.routes.find((r) => r.elements && r.elements.length > 0) ?? appModel.routes[0];
|
|
97
|
+
}
|
|
98
|
+
if (!route || !route.elements || route.elements.length === 0) return undefined;
|
|
99
|
+
|
|
100
|
+
const elements = route.elements;
|
|
101
|
+
const inputEl = elements.find((el) =>
|
|
102
|
+
(el.tag === 'input' && el.type !== 'checkbox' && el.type !== 'radio' && el.type !== 'submit' && el.type !== 'button') ||
|
|
103
|
+
el.tag === 'textarea' ||
|
|
104
|
+
Boolean(el.placeholder) ||
|
|
105
|
+
(el.role === 'textbox' && el.type !== 'checkbox')
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const submitBtn = elements.find((el) =>
|
|
109
|
+
(el.tag === 'button' || el.role === 'button' || el.type === 'submit') &&
|
|
110
|
+
el.accessibleName &&
|
|
111
|
+
!/delete|remove|close|cancel/i.test(el.accessibleName)
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const testValue = `Test ${featureName.replace(/Page$/, '')}`;
|
|
115
|
+
|
|
116
|
+
if (inputEl) {
|
|
117
|
+
const steps: GeneratedStep[] = [
|
|
118
|
+
{ action: 'navigate' },
|
|
119
|
+
{
|
|
120
|
+
action: 'fill',
|
|
121
|
+
role: inputEl.role ?? 'textbox',
|
|
122
|
+
name: inputEl.accessibleName,
|
|
123
|
+
placeholder: inputEl.placeholder,
|
|
124
|
+
selector: inputEl.testId,
|
|
125
|
+
value: testValue,
|
|
126
|
+
frame: inputEl.frameSelector,
|
|
127
|
+
frameSelector: inputEl.frameSelector,
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
if (submitBtn) {
|
|
132
|
+
steps.push({
|
|
133
|
+
action: 'click',
|
|
134
|
+
role: 'button',
|
|
135
|
+
name: submitBtn.accessibleName,
|
|
136
|
+
frame: submitBtn.frameSelector,
|
|
137
|
+
frameSelector: submitBtn.frameSelector,
|
|
138
|
+
});
|
|
139
|
+
} else {
|
|
140
|
+
steps.push({
|
|
141
|
+
action: 'press',
|
|
142
|
+
name: inputEl.accessibleName,
|
|
143
|
+
placeholder: inputEl.placeholder,
|
|
144
|
+
value: 'Enter',
|
|
145
|
+
frame: inputEl.frameSelector,
|
|
146
|
+
frameSelector: inputEl.frameSelector,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
steps.push({
|
|
151
|
+
action: 'assert-visible',
|
|
152
|
+
expectedText: testValue,
|
|
153
|
+
name: testValue,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
return steps;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const actionEl = elements.find((el) => (el.tag === 'button' || el.role === 'button' || el.tag === 'a') && el.accessibleName);
|
|
160
|
+
if (actionEl) {
|
|
161
|
+
return [
|
|
162
|
+
{ action: 'navigate' },
|
|
163
|
+
{
|
|
164
|
+
action: 'click',
|
|
165
|
+
role: actionEl.role ?? 'button',
|
|
166
|
+
name: actionEl.accessibleName,
|
|
167
|
+
frame: actionEl.frameSelector,
|
|
168
|
+
frameSelector: actionEl.frameSelector,
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
action: 'assert-visible',
|
|
172
|
+
role: 'heading',
|
|
173
|
+
name: featureName,
|
|
174
|
+
},
|
|
175
|
+
];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function defaultSteps(input: ArchitectureGenerationInput): GeneratedStep[] {
|
|
182
|
+
if (input.steps && input.steps.length > 0) return input.steps;
|
|
183
|
+
if (input.appModel) {
|
|
184
|
+
const inferred = inferBehavioralSteps(input.featureName, input.appModel);
|
|
185
|
+
if (inferred && inferred.length > 0) return inferred;
|
|
186
|
+
}
|
|
187
|
+
return [
|
|
188
|
+
{ action: 'navigate' },
|
|
189
|
+
{ action: 'assert-visible', role: 'heading', name: input.featureName },
|
|
190
|
+
];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function renderLocator(step: GeneratedStep, preferredLocator: string, frameSelector?: string): string {
|
|
194
|
+
let inner: string;
|
|
195
|
+
if (step.placeholder) {
|
|
196
|
+
inner = `getByPlaceholder('${step.placeholder}')`;
|
|
197
|
+
} else if (step.action === 'click' && step.role && step.name) {
|
|
198
|
+
inner = `getByRole('${step.role}', { name: '${step.name}' })`;
|
|
199
|
+
} else if (step.action === 'fill' && step.name) {
|
|
200
|
+
if (preferredLocator === 'getByLabel') inner = `getByLabel('${step.name}')`;
|
|
201
|
+
else inner = `getByRole('textbox', { name: '${step.name}' })`;
|
|
202
|
+
} else if (step.selector && preferredLocator === 'getByTestId' && /^[A-Za-z0-9_-]+$/.test(step.selector)) {
|
|
203
|
+
inner = `getByTestId('${step.selector}')`;
|
|
204
|
+
} else {
|
|
205
|
+
inner = `locator('${step.selector ?? (step.action === 'fill' ? 'input' : 'button')}')`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const frame = frameSelector ?? step.frame ?? step.frameSelector;
|
|
209
|
+
if (frame) {
|
|
210
|
+
return `this.page.frameLocator('${frame}').${inner}`;
|
|
211
|
+
}
|
|
212
|
+
return `this.page.${inner}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function renderPageObject(className: string, baseUrl: string, steps: GeneratedStep[], preferredLocator: string, namePlan: NamePlan): string {
|
|
216
|
+
const declarations: string[] = [];
|
|
217
|
+
const assignments: string[] = [];
|
|
218
|
+
const methods: string[] = [];
|
|
219
|
+
const seenProperties = new Set<string>();
|
|
220
|
+
|
|
221
|
+
steps.forEach((step, index) => {
|
|
222
|
+
if (step.action !== 'click' && step.action !== 'fill' && step.action !== 'press') return;
|
|
223
|
+
const symbol = namePlan.symbols.find((s) => s.ref === `step-${index + 1}`) ?? namePlan.symbols[index];
|
|
224
|
+
const property = symbol?.property ?? (step.action === 'click' ? `actionButton` : `inputField`);
|
|
225
|
+
const method = symbol?.method ?? (step.action === 'click' ? `clickAction` : step.action === 'press' ? `pressKey` : `fillInput`);
|
|
226
|
+
const frame = symbol?.frame ?? step.frame ?? step.frameSelector;
|
|
227
|
+
|
|
228
|
+
if (!seenProperties.has(property)) {
|
|
229
|
+
seenProperties.add(property);
|
|
230
|
+
declarations.push(` readonly ${property}: Locator;`);
|
|
231
|
+
assignments.push(` this.${property} = ${renderLocator(step, preferredLocator, frame)};`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (step.action === 'click') {
|
|
235
|
+
methods.push(` async ${method}(): Promise<void> {\n await this.${property}.click();\n }`);
|
|
236
|
+
} else if (step.action === 'press') {
|
|
237
|
+
methods.push(` async ${method}(key = ${quote(step.value ?? 'Enter')}): Promise<void> {\n await this.${property}.press(key);\n }`);
|
|
238
|
+
} else {
|
|
239
|
+
methods.push(` async ${method}(value: string): Promise<void> {\n await this.${property}.fill(value);\n }`);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
return `import type { Page, Locator } from '@playwright/test';
|
|
244
|
+
|
|
245
|
+
export class ${className} {
|
|
246
|
+
readonly page: Page;
|
|
247
|
+
${declarations.length > 0 ? `\n${declarations.join('\n')}\n` : ''}
|
|
248
|
+
constructor(page: Page) {
|
|
249
|
+
this.page = page;
|
|
250
|
+
${assignments.length > 0 ? `${assignments.join('\n')}\n` : ''} }
|
|
251
|
+
|
|
252
|
+
async goto(): Promise<void> {
|
|
253
|
+
await this.page.goto(${quote(baseUrl)});
|
|
254
|
+
}
|
|
255
|
+
${methods.length > 0 ? `\n${methods.join('\n\n')}\n` : ''}}
|
|
256
|
+
`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function renderSpec(
|
|
260
|
+
featureName: string,
|
|
261
|
+
className: string,
|
|
262
|
+
pageImport: string,
|
|
263
|
+
fixtureImport: string | undefined,
|
|
264
|
+
steps: GeneratedStep[],
|
|
265
|
+
namePlan: NamePlan,
|
|
266
|
+
requirements?: string,
|
|
267
|
+
useFixtures = false,
|
|
268
|
+
): string {
|
|
269
|
+
const variable = className[0]!.toLowerCase() + className.slice(1);
|
|
270
|
+
const lines: string[] = [];
|
|
271
|
+
|
|
272
|
+
if (!useFixtures) {
|
|
273
|
+
lines.push(` const ${variable} = new ${className}(page);`);
|
|
274
|
+
lines.push(` await ${variable}.goto();`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
steps.forEach((step, index) => {
|
|
278
|
+
const symbol = namePlan.symbols.find((s) => s.ref === `step-${index + 1}`) ?? namePlan.symbols[index];
|
|
279
|
+
const method = symbol?.method ?? (step.action === 'click' ? `clickAction` : step.action === 'press' ? `pressKey` : `fillInput`);
|
|
280
|
+
const action = String(step.action || '').replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
281
|
+
|
|
282
|
+
if (action === 'navigate' && step.value) {
|
|
283
|
+
lines.push(` await page.goto(${quote(step.value)});`);
|
|
284
|
+
} else if (action === 'click') {
|
|
285
|
+
lines.push(` await ${variable}.${method}();`);
|
|
286
|
+
} else if (action === 'press') {
|
|
287
|
+
lines.push(` await ${variable}.${method}(${quote(step.value ?? 'Enter')});`);
|
|
288
|
+
} else if (action === 'fill') {
|
|
289
|
+
lines.push(` await ${variable}.${method}(${quote(step.value ?? 'test-data')});`);
|
|
290
|
+
} else if (action === 'assert-visible') {
|
|
291
|
+
if (step.expectedText) {
|
|
292
|
+
lines.push(` await expect(page.getByText(${quote(step.expectedText)})).toBeVisible();`);
|
|
293
|
+
} else if (step.role && step.name) {
|
|
294
|
+
lines.push(` await expect(page.getByRole('${step.role}', { name: '${step.name}' })).toBeVisible();`);
|
|
295
|
+
} else if (step.name) {
|
|
296
|
+
lines.push(` await expect(page.getByRole('${step.role ?? 'heading'}', { name: '${step.name}' })).toBeVisible();`);
|
|
297
|
+
} else if (step.selector) {
|
|
298
|
+
lines.push(` await expect(page.locator(${quote(step.selector)})).toBeVisible();`);
|
|
299
|
+
} else {
|
|
300
|
+
lines.push(` await expect(page.getByRole('${step.role ?? 'heading'}', { name: '${featureName}' })).toBeVisible();`);
|
|
301
|
+
}
|
|
302
|
+
} else if (action === 'assert-text') {
|
|
303
|
+
lines.push(` await expect(page.locator('${step.selector ?? 'body'}')).toContainText('${step.expectedText ?? ''}');`);
|
|
304
|
+
} else if (action === 'assert-count') {
|
|
305
|
+
lines.push(` await expect(page.locator('${step.selector ?? 'li'}')).toHaveCount(${step.expectedCount ?? 1});`);
|
|
306
|
+
} else if (action === 'assert-value') {
|
|
307
|
+
lines.push(` await expect(page.locator('${step.selector ?? 'input'}')).toHaveValue('${step.expectedValue ?? ''}');`);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const requirementComment = requirements?.trim()
|
|
312
|
+
? `\n // Requirement: ${requirements.trim().replace(/\r?\n/g, ' ').replace(/\*\//g, '* /')}`
|
|
313
|
+
: '';
|
|
314
|
+
|
|
315
|
+
if (useFixtures && fixtureImport) {
|
|
316
|
+
return `import { test, expect } from ${quote(fixtureImport)};
|
|
317
|
+
import { ${className} } from ${quote(pageImport)};
|
|
318
|
+
|
|
319
|
+
test.describe(${quote(`${featureName} workflow`)}, () => {
|
|
320
|
+
test(${quote(`should complete ${featureName}`)}, async ({ ${variable}, page }) => {${requirementComment}
|
|
321
|
+
${lines.join('\n')}
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return `import { test, expect } from '@playwright/test';
|
|
328
|
+
import { ${className} } from ${quote(pageImport)};
|
|
329
|
+
|
|
330
|
+
test.describe(${quote(`${featureName} workflow`)}, () => {
|
|
331
|
+
test(${quote(`should complete ${featureName}`)}, async ({ page }) => {${requirementComment}
|
|
332
|
+
${lines.join('\n')}
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function renderFixtureFile(className?: string, pageImport?: string): string {
|
|
339
|
+
if (!className || !pageImport) {
|
|
340
|
+
return `import { test as base } from '@playwright/test';
|
|
341
|
+
|
|
342
|
+
export const test = base.extend({});
|
|
343
|
+
export { expect } from '@playwright/test';
|
|
344
|
+
`;
|
|
345
|
+
}
|
|
346
|
+
const variable = className[0]!.toLowerCase() + className.slice(1);
|
|
347
|
+
return `import { test as base } from '@playwright/test';
|
|
348
|
+
import { ${className} } from ${quote(pageImport)};
|
|
349
|
+
|
|
350
|
+
type ${className}Fixtures = {
|
|
351
|
+
${variable}: ${className};
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
export const test = base.extend<${className}Fixtures>({
|
|
355
|
+
${variable}: async ({ page }, use) => {
|
|
356
|
+
const ${variable} = new ${className}(page);
|
|
357
|
+
await ${variable}.goto();
|
|
358
|
+
await use(${variable});
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
export { expect } from '@playwright/test';
|
|
363
|
+
`;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function renderPlaywrightConfig(baseUrl: string, testDirectory: string): string {
|
|
367
|
+
return `import { defineConfig, devices } from '@playwright/test';
|
|
368
|
+
|
|
369
|
+
declare const process: { env: Record<string, string | undefined> };
|
|
370
|
+
|
|
371
|
+
export default defineConfig({
|
|
372
|
+
testDir: ${quote(`./${testDirectory}`)},
|
|
373
|
+
fullyParallel: true,
|
|
374
|
+
forbidOnly: !!process.env.CI,
|
|
375
|
+
retries: process.env.CI ? 2 : 0,
|
|
376
|
+
reporter: [['list'], ['html', { open: 'never' }]],
|
|
377
|
+
use: {
|
|
378
|
+
baseURL: ${quote(baseUrl === '/' ? 'http://localhost:3000' : baseUrl)},
|
|
379
|
+
trace: 'on-first-retry',
|
|
380
|
+
screenshot: 'only-on-failure',
|
|
381
|
+
video: 'retain-on-failure',
|
|
382
|
+
},
|
|
383
|
+
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
|
|
384
|
+
});
|
|
385
|
+
`;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function renderCiWorkflow(): string {
|
|
389
|
+
return `name: Playwright tests
|
|
390
|
+
|
|
391
|
+
on:
|
|
392
|
+
pull_request:
|
|
393
|
+
push:
|
|
394
|
+
branches: [main]
|
|
395
|
+
|
|
396
|
+
jobs:
|
|
397
|
+
test:
|
|
398
|
+
runs-on: ubuntu-latest
|
|
399
|
+
steps:
|
|
400
|
+
- uses: actions/checkout@v4
|
|
401
|
+
- uses: actions/setup-node@v4
|
|
402
|
+
with:
|
|
403
|
+
node-version: 20
|
|
404
|
+
cache: npm
|
|
405
|
+
- run: npm ci
|
|
406
|
+
- run: npx playwright install --with-deps chromium
|
|
407
|
+
- run: npx playwright test
|
|
408
|
+
`;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function fileExists(existingFiles: Set<string>, relativePath: string): boolean {
|
|
412
|
+
const normalized = relativePath.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
413
|
+
return [...existingFiles].some((file) => file.replace(/\\/g, '/').replace(/^\.\//, '') === normalized);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function makeFile(relativePath: string, content: string, purpose: string, existingFiles: Set<string>): ArchitectureFile {
|
|
417
|
+
return { relativePath, content, purpose, action: fileExists(existingFiles, relativePath) ? 'SKIP_EXISTING' : 'CREATE' };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export function planArchitecture(input: ArchitectureGenerationInput): ArchitecturePlan {
|
|
421
|
+
const featureName = input.featureName.trim();
|
|
422
|
+
if (!featureName) throw new Error('featureName must be a non-empty string');
|
|
423
|
+
if (input.mode !== 'greenfield' && input.mode !== 'brownfield') throw new Error(`Unsupported architecture mode '${input.mode}'`);
|
|
424
|
+
|
|
425
|
+
const slug = toSlug(featureName);
|
|
426
|
+
const rawBaseName = toClassName(featureName);
|
|
427
|
+
const fallbackClassName = rawBaseName.endsWith('Page') ? rawBaseName : `${rawBaseName}Page`;
|
|
428
|
+
let steps = defaultSteps(input);
|
|
429
|
+
if (input.appModel && input.appModel.routes) {
|
|
430
|
+
const allElements = input.appModel.routes.flatMap((r) => r.elements);
|
|
431
|
+
steps = steps.map((s) => {
|
|
432
|
+
if (s.frame || s.frameSelector) return s;
|
|
433
|
+
const match = allElements.find((el) =>
|
|
434
|
+
el.frameSelector &&
|
|
435
|
+
((s.name && (el.accessibleName === s.name || el.label === s.name)) ||
|
|
436
|
+
(s.role && el.role === s.role && s.name && el.accessibleName === s.name))
|
|
437
|
+
);
|
|
438
|
+
if (match?.frameSelector) {
|
|
439
|
+
return { ...s, frame: match.frameSelector, frameSelector: match.frameSelector };
|
|
440
|
+
}
|
|
441
|
+
return s;
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
const namePlan = input.namePlan ?? NamePlanner.planNames(featureName, steps, input.mode === 'greenfield' ? { existingPageClassName: fallbackClassName } : undefined);
|
|
445
|
+
const className = input.mode === 'brownfield' && namePlan.pageClassName ? namePlan.pageClassName : fallbackClassName;
|
|
446
|
+
const namingLintReport = NamingLinter.lint(namePlan);
|
|
447
|
+
|
|
448
|
+
const profile = input.profile;
|
|
449
|
+
const testConfidence = profile?.pathConfidence?.testDirectory ?? 1;
|
|
450
|
+
const pageConfidence = profile?.pathConfidence?.pageObjectDirectory ?? 1;
|
|
451
|
+
const rawTestDir = (testConfidence >= 0.5 && profile?.paths?.testDirectory) ? profile.paths.testDirectory : 'tests';
|
|
452
|
+
const rawPageDir = (pageConfidence >= 0.5 && profile?.paths?.pageObjectDirectory) ? profile.paths.pageObjectDirectory : `${rawTestDir}/pages`;
|
|
453
|
+
const testDirectory = normalizeRelativeDirectory(rawTestDir, 'tests');
|
|
454
|
+
const pageObjectDirectory = normalizeRelativeDirectory(rawPageDir, `${testDirectory}/pages`);
|
|
455
|
+
const fixtureDirectory = normalizeRelativeDirectory(profile?.paths?.fixtureDirectory, `${testDirectory}/fixtures`);
|
|
456
|
+
const preferredLocator = profile?.locatorConventions.preferred ?? 'getByRole';
|
|
457
|
+
const existingFiles = new Set((input.existingFiles ?? []).map((file) => file.replace(/\\/g, '/')));
|
|
458
|
+
const baseUrl = input.baseUrl ?? profile?.testConventions?.baseUrl ?? '/';
|
|
459
|
+
const testPath = `${testDirectory}/${slug}.spec.ts`;
|
|
460
|
+
const pagePath = `${pageObjectDirectory}/${className}.ts`;
|
|
461
|
+
const pageImport = path.posix.relative(path.posix.dirname(testPath), pagePath).replace(/\.ts$/, '');
|
|
462
|
+
const normalizedPageImport = pageImport.startsWith('.') ? pageImport : `./${pageImport}`;
|
|
463
|
+
|
|
464
|
+
const assumptions: string[] = [];
|
|
465
|
+
const warnings: string[] = [];
|
|
466
|
+
if (namePlan.assumptions.length > 0) {
|
|
467
|
+
assumptions.push(...namePlan.assumptions);
|
|
468
|
+
}
|
|
469
|
+
if (!namingLintReport.passed) {
|
|
470
|
+
for (const v of namingLintReport.violations) {
|
|
471
|
+
warnings.push(`Naming lint violation [${v.kind}]: ${v.message}`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (input.mode === 'greenfield') {
|
|
476
|
+
assumptions.push('The application will be available at the supplied base URL when tests execute.');
|
|
477
|
+
assumptions.push('The generated workflow is a scaffold and requires review of business assertions and test data.');
|
|
478
|
+
} else {
|
|
479
|
+
if (!profile) warnings.push('No repository profile supplied; brownfield paths and conventions are conservative defaults.');
|
|
480
|
+
if (profile?.locatorConventions.preferred === 'unknown') warnings.push('No dominant locator convention was detected; locator choices require review.');
|
|
481
|
+
if (profile?.structure.playwrightTestFiles === 0 && profile.structure.configFiles.length === 0) {
|
|
482
|
+
warnings.push('No Playwright test source or configuration was detected; generated files require manual integration review.');
|
|
483
|
+
}
|
|
484
|
+
if (testConfidence < 0.5) {
|
|
485
|
+
warnings.push(`Test-directory convention has low confidence (${testConfidence}); defaulting to standard 'tests'.`);
|
|
486
|
+
}
|
|
487
|
+
if (pageConfidence < 0.5) {
|
|
488
|
+
warnings.push(`Page-object directory convention has low confidence (${pageConfidence}); defaulting to standard '${testDirectory}/pages'.`);
|
|
489
|
+
}
|
|
490
|
+
assumptions.push('Existing project configuration, fixtures, authentication, and test data remain authoritative.');
|
|
491
|
+
assumptions.push('Generated files are additive and do not replace existing files.');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const files: ArchitectureFile[] = [];
|
|
495
|
+
const fixtureFileRelative = `${fixtureDirectory}/test.ts`;
|
|
496
|
+
const pageImportFromFixture = path.posix.relative(path.posix.dirname(fixtureFileRelative), pagePath).replace(/\.ts$/, '');
|
|
497
|
+
const normalizedPageImportFromFixture = pageImportFromFixture.startsWith('.') ? pageImportFromFixture : `./${pageImportFromFixture}`;
|
|
498
|
+
|
|
499
|
+
const fixtureImport = path.posix.relative(path.posix.dirname(testPath), fixtureFileRelative).replace(/\.ts$/, '');
|
|
500
|
+
const normalizedFixtureImport = fixtureImport.startsWith('.') ? fixtureImport : `./${fixtureImport}`;
|
|
501
|
+
|
|
502
|
+
const usesCustomFixtures = input.mode === 'greenfield' || (profile?.structure.usesCustomFixtures ?? false);
|
|
503
|
+
|
|
504
|
+
files.push(makeFile(pagePath, renderPageObject(className, baseUrl, steps, preferredLocator, namePlan), 'Feature Page Object with explicit locators and actions.', existingFiles));
|
|
505
|
+
files.push(makeFile(testPath, renderSpec(featureName, className, normalizedPageImport, normalizedFixtureImport, steps, namePlan, input.requirements, usesCustomFixtures), 'Workflow spec with web-first assertions.', existingFiles));
|
|
506
|
+
|
|
507
|
+
if (input.mode === 'greenfield') {
|
|
508
|
+
files.push(makeFile('playwright.config.ts', renderPlaywrightConfig(baseUrl, testDirectory), 'Minimal reproducible Playwright configuration.', existingFiles));
|
|
509
|
+
files.push(makeFile(fixtureFileRelative, renderFixtureFile(className, normalizedPageImportFromFixture), 'Composable fixture entry point.', existingFiles));
|
|
510
|
+
if (input.includeCi !== false) files.push(makeFile('.github/workflows/playwright.yml', renderCiWorkflow(), 'Pull-request Playwright execution workflow.', existingFiles));
|
|
511
|
+
} else if (profile?.structure.usesCustomFixtures) {
|
|
512
|
+
files.push(makeFile(fixtureFileRelative, renderFixtureFile(className, normalizedPageImportFromFixture), 'Fixture extension point matching the profiled repository layout.', existingFiles));
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const specFile = files.find((f) => f.relativePath.endsWith('.spec.ts'));
|
|
516
|
+
if (specFile) {
|
|
517
|
+
const qualityReport = AssertionQualityGate.evaluate(specFile.content, featureName);
|
|
518
|
+
if (!qualityReport.passed) {
|
|
519
|
+
for (const v of qualityReport.violations) {
|
|
520
|
+
warnings.push(`Assertion quality violation: ${v}`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const groundedSteps = steps.filter((s) => s.action !== 'navigate' && (s.name || s.selector || s.placeholder));
|
|
526
|
+
const ungrounded = steps.length > 0 && groundedSteps.length === 0;
|
|
527
|
+
if (ungrounded) {
|
|
528
|
+
warnings.push('No grounded steps provided: every step must carry an accessible name or selector. Supply requirements or observed steps.');
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const status: ArchitecturePlanStatus = ungrounded
|
|
532
|
+
? 'NEEDS_INPUT'
|
|
533
|
+
: warnings.some((warning) => warning.includes('No repository profile')) ? 'PARTIAL' : 'READY';
|
|
534
|
+
return {
|
|
535
|
+
mode: input.mode,
|
|
536
|
+
status,
|
|
537
|
+
featureName,
|
|
538
|
+
slug,
|
|
539
|
+
conventions: {
|
|
540
|
+
testDirectory,
|
|
541
|
+
pageObjectDirectory,
|
|
542
|
+
fixtureDirectory,
|
|
543
|
+
preferredLocator,
|
|
544
|
+
usesCustomFixtures: profile?.structure.usesCustomFixtures ?? false,
|
|
545
|
+
usesStorageState: profile?.structure.usesStorageState ?? false,
|
|
546
|
+
},
|
|
547
|
+
assumptions,
|
|
548
|
+
warnings,
|
|
549
|
+
files,
|
|
550
|
+
namePlan,
|
|
551
|
+
namingLintReport,
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
export async function writeArchitecturePlan(
|
|
556
|
+
root: string,
|
|
557
|
+
plan: ArchitecturePlan,
|
|
558
|
+
): Promise<ArchitectureWriteResult> {
|
|
559
|
+
const projectRoot = path.resolve(root);
|
|
560
|
+
const written: string[] = [];
|
|
561
|
+
const skipped: string[] = [];
|
|
562
|
+
for (const file of plan.files) {
|
|
563
|
+
const target = path.resolve(projectRoot, file.relativePath);
|
|
564
|
+
const relative = path.relative(projectRoot, target);
|
|
565
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) throw new Error(`Architecture output path escapes project root: '${file.relativePath}'`);
|
|
566
|
+
if (file.action === 'SKIP_EXISTING') {
|
|
567
|
+
skipped.push(file.relativePath);
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
try {
|
|
571
|
+
await fs.access(target);
|
|
572
|
+
skipped.push(file.relativePath);
|
|
573
|
+
continue;
|
|
574
|
+
} catch {
|
|
575
|
+
// The file is intentionally create-only. Continue to mkdir/write below.
|
|
576
|
+
}
|
|
577
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
578
|
+
await fs.writeFile(target, file.content, 'utf8');
|
|
579
|
+
written.push(file.relativePath);
|
|
580
|
+
}
|
|
581
|
+
return { written, skipped };
|
|
582
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export interface AssertionQualityReport {
|
|
2
|
+
passed: boolean;
|
|
3
|
+
assertionCount: number;
|
|
4
|
+
hasStateAssertion: boolean;
|
|
5
|
+
isVacuousEcho: boolean;
|
|
6
|
+
constantAssertionCount: number;
|
|
7
|
+
arbitrarySleepCount: number;
|
|
8
|
+
emptyOrSkippedCount: number;
|
|
9
|
+
violations: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class AssertionQualityGate {
|
|
13
|
+
/**
|
|
14
|
+
* Evaluates the assertion quality of a generated Playwright spec file.
|
|
15
|
+
* Enforces that generated tests are behavioral, non-vacuous, and contain real assertions.
|
|
16
|
+
*/
|
|
17
|
+
public static evaluate(specContent: string, featureName: string): AssertionQualityReport {
|
|
18
|
+
const violations: string[] = [];
|
|
19
|
+
|
|
20
|
+
// 1. Count assertions
|
|
21
|
+
const expectMatches = [...specContent.matchAll(/\bexpect(?:\.soft|\.poll)?\s*\(/g)];
|
|
22
|
+
const assertionCount = expectMatches.length;
|
|
23
|
+
|
|
24
|
+
if (assertionCount === 0) {
|
|
25
|
+
violations.push('ZERO_ASSERTIONS: Generated spec contains zero expect() assertions.');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 2. Check for constant assertions (e.g. expect(true).toBe(true), expect(1).toBe(1))
|
|
29
|
+
const constantMatches = [
|
|
30
|
+
...specContent.matchAll(
|
|
31
|
+
/\bexpect\s*\(\s*(?:true|false|\d+|['"][^'"]*['"])\s*\)\s*\.\s*(?:toBe|toEqual|toStrictEqual)\s*\(\s*(?:true|false|\d+|['"][^'"]*['"])\s*\)/g,
|
|
32
|
+
),
|
|
33
|
+
];
|
|
34
|
+
const constantAssertionCount = constantMatches.length;
|
|
35
|
+
if (constantAssertionCount > 0) {
|
|
36
|
+
violations.push(`CONSTANT_ASSERTION: Spec contains ${constantAssertionCount} constant assertion(s) that do not inspect application state.`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 3. Check for arbitrary waitForTimeout sleeps
|
|
40
|
+
const sleepMatches = [...specContent.matchAll(/\bwaitForTimeout\s*\(\s*\d+\s*\)/g)];
|
|
41
|
+
const arbitrarySleepCount = sleepMatches.length;
|
|
42
|
+
if (arbitrarySleepCount > 0) {
|
|
43
|
+
violations.push(`ARBITRARY_SLEEP: Spec uses waitForTimeout() (${arbitrarySleepCount}x) instead of web-first assertions or state waits.`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 4. Check for empty or skipped tests
|
|
47
|
+
const skippedMatches = [...specContent.matchAll(/\btest\s*\.\s*(?:skip|fixme)\s*\(/g)];
|
|
48
|
+
const emptyBodyMatches = [...specContent.matchAll(/\btest\s*\([^,]+,\s*async\s*\([^)]*\)\s*=>\s*\{\s*\}\s*\)/g)];
|
|
49
|
+
const emptyOrSkippedCount = skippedMatches.length + emptyBodyMatches.length;
|
|
50
|
+
if (emptyOrSkippedCount > 0) {
|
|
51
|
+
violations.push(`EMPTY_OR_SKIPPED_TEST: Spec contains ${emptyOrSkippedCount} skipped or empty test case(s).`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 5. Check for web-first / response assertions
|
|
55
|
+
const stateAssertionRegex = /\b(toBeVisible|toBeHidden|toHaveText|toContainText|toHaveCount|toBeChecked|toHaveValue|toBeEmpty|toBeEnabled|toBeDisabled|toHaveAttribute|toHaveClass|toHaveURL|toHaveTitle|toBeOK)\b/;
|
|
56
|
+
const hasStateAssertion = stateAssertionRegex.test(specContent);
|
|
57
|
+
|
|
58
|
+
if (assertionCount > 0 && !hasStateAssertion) {
|
|
59
|
+
violations.push('WEAK_ASSERTION: Generated spec contains no auto-retrying web-first state assertions.');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 6. Check for vacuous echo assertion
|
|
63
|
+
// An echo assertion is when the ONLY assertion in the spec is expecting a heading with the exact featureName string.
|
|
64
|
+
let isVacuousEcho = false;
|
|
65
|
+
if (assertionCount === 1) {
|
|
66
|
+
const escapedFeature = featureName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
67
|
+
const headingEchoRegex = new RegExp(
|
|
68
|
+
`expect\\s*\\(\\s*page\\.getByRole\\s*\\(\\s*['"]heading['"]\\s*,\\s*\\{\\s*name:\\s*['"]${escapedFeature}['"]\\s*\\}\\s*\\)\\s*\\)\\.toBeVisible\\(\\)`,
|
|
69
|
+
);
|
|
70
|
+
if (headingEchoRegex.test(specContent)) {
|
|
71
|
+
isVacuousEcho = true;
|
|
72
|
+
violations.push(`VACUOUS_ECHO_ASSERTION: Spec only asserts visibility of input feature name '${featureName}' without verifying dynamic application behavior.`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
passed: violations.length === 0,
|
|
78
|
+
assertionCount,
|
|
79
|
+
hasStateAssertion,
|
|
80
|
+
isVacuousEcho,
|
|
81
|
+
constantAssertionCount,
|
|
82
|
+
arbitrarySleepCount,
|
|
83
|
+
emptyOrSkippedCount,
|
|
84
|
+
violations,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|