playwright-intelligence-engine 0.0.0-stage → 2.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/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,313 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { ApprovalService, ApprovalTokenPayload } from './approval-service.js';
|
|
5
|
+
import { WorktreeManager } from '../sandbox/worktree-manager.js';
|
|
6
|
+
|
|
7
|
+
export interface PromotionResult {
|
|
8
|
+
success: boolean;
|
|
9
|
+
targetFilePath: string;
|
|
10
|
+
bytesWritten: number;
|
|
11
|
+
finalHash: string;
|
|
12
|
+
error?: string | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface LockFilePayload {
|
|
16
|
+
pid: number;
|
|
17
|
+
timestamp: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* WorkspacePromoter handles safe, atomic, compare-before-write promotion of
|
|
22
|
+
* verified worktree repairs into the developer's active workspace.
|
|
23
|
+
*
|
|
24
|
+
* Enforces:
|
|
25
|
+
* 1. Canonical realpath containment inside the approved workspace.
|
|
26
|
+
* 2. Cross-process and in-process cooperative file locks.
|
|
27
|
+
* 3. Compare-And-Swap (CAS) hash verification of original source before mutation.
|
|
28
|
+
* 4. Patch diff and content hash validation against cryptographic approval token.
|
|
29
|
+
* 5. Safe temporary file write in the same directory.
|
|
30
|
+
* 6. Recoverable backup creation for fail-closed rollback.
|
|
31
|
+
* 7. Verification of post-promotion content hash.
|
|
32
|
+
* 8. Elimination of unvalidated direct-write fallbacks.
|
|
33
|
+
*/
|
|
34
|
+
export class WorkspacePromoter {
|
|
35
|
+
private static activeLocks = new Set<string>();
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Promotes an approved patch to the active workspace with CAS hash verification,
|
|
39
|
+
* filesystem locks, and atomic rename.
|
|
40
|
+
*/
|
|
41
|
+
public static async promote(params: {
|
|
42
|
+
targetFilePath: string;
|
|
43
|
+
workspaceRoot: string;
|
|
44
|
+
patchedContent: string;
|
|
45
|
+
approvalTokenPayload: ApprovalTokenPayload;
|
|
46
|
+
}): Promise<PromotionResult> {
|
|
47
|
+
const { targetFilePath, workspaceRoot, patchedContent, approvalTokenPayload } = params;
|
|
48
|
+
|
|
49
|
+
// 1. Resolve canonical target path
|
|
50
|
+
if (!fs.existsSync(targetFilePath)) {
|
|
51
|
+
return {
|
|
52
|
+
success: false,
|
|
53
|
+
targetFilePath,
|
|
54
|
+
bytesWritten: 0,
|
|
55
|
+
finalHash: '',
|
|
56
|
+
error: `Target file does not exist: '${targetFilePath}'`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const canonicalTarget = fs.realpathSync(targetFilePath);
|
|
61
|
+
const canonicalWorkspace = fs.realpathSync(workspaceRoot);
|
|
62
|
+
|
|
63
|
+
// 2. Verify workspace containment
|
|
64
|
+
const relative = path.relative(canonicalWorkspace, canonicalTarget);
|
|
65
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
66
|
+
return {
|
|
67
|
+
success: false,
|
|
68
|
+
targetFilePath: canonicalTarget,
|
|
69
|
+
bytesWritten: 0,
|
|
70
|
+
finalHash: '',
|
|
71
|
+
error: `Security Violation: Target path '${canonicalTarget}' escapes workspace root '${canonicalWorkspace}'.`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Verify target matches token binding
|
|
76
|
+
if (canonicalTarget !== approvalTokenPayload.targetFilePath) {
|
|
77
|
+
return {
|
|
78
|
+
success: false,
|
|
79
|
+
targetFilePath: canonicalTarget,
|
|
80
|
+
bytesWritten: 0,
|
|
81
|
+
finalHash: '',
|
|
82
|
+
error: `Approval Token Target Mismatch: Token bound to '${approvalTokenPayload.targetFilePath}', received '${canonicalTarget}'.`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 3. Acquire per-file in-process and cross-process lock
|
|
87
|
+
const lockKey = canonicalTarget.toLowerCase();
|
|
88
|
+
if (WorkspacePromoter.activeLocks.has(lockKey)) {
|
|
89
|
+
return {
|
|
90
|
+
success: false,
|
|
91
|
+
targetFilePath: canonicalTarget,
|
|
92
|
+
bytesWritten: 0,
|
|
93
|
+
finalHash: '',
|
|
94
|
+
error: `Conflict: Another promotion is currently active for '${canonicalTarget}'.`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const lockFilePath = `${canonicalTarget}.pie-promote.lock`;
|
|
99
|
+
const lockAcquired = WorkspacePromoter.acquireCrossProcessLock(lockFilePath);
|
|
100
|
+
if (!lockAcquired.success) {
|
|
101
|
+
return {
|
|
102
|
+
success: false,
|
|
103
|
+
targetFilePath: canonicalTarget,
|
|
104
|
+
bytesWritten: 0,
|
|
105
|
+
finalHash: '',
|
|
106
|
+
error: lockAcquired.error,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
WorkspacePromoter.activeLocks.add(lockKey);
|
|
111
|
+
|
|
112
|
+
const targetDir = path.dirname(canonicalTarget);
|
|
113
|
+
const tempFile = path.join(targetDir, `.pie-promo-${process.pid}-${crypto.randomBytes(8).toString('hex')}.tmp`);
|
|
114
|
+
const backupFile = path.join(targetDir, `.pie-backup-${process.pid}-${crypto.randomBytes(8).toString('hex')}.bak`);
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
// 4 & 5. Read current file and calculate hash
|
|
118
|
+
const currentContent = await fs.promises.readFile(canonicalTarget, 'utf-8');
|
|
119
|
+
const currentHash = ApprovalService.hashContent(currentContent);
|
|
120
|
+
|
|
121
|
+
// 6 & 7. Compare hash (CAS check)
|
|
122
|
+
if (currentHash !== approvalTokenPayload.originalSourceHash) {
|
|
123
|
+
return {
|
|
124
|
+
success: false,
|
|
125
|
+
targetFilePath: canonicalTarget,
|
|
126
|
+
bytesWritten: 0,
|
|
127
|
+
finalHash: currentHash,
|
|
128
|
+
error: `Concurrent Modification Detected: File '${canonicalTarget}' has been modified since approval (current: ${currentHash.slice(0, 8)}, expected: ${approvalTokenPayload.originalSourceHash.slice(0, 8)}). Promotion aborted.`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Verify patchedContent matches token patchedSourceHash
|
|
133
|
+
const contentHash = ApprovalService.hashContent(patchedContent);
|
|
134
|
+
if (approvalTokenPayload.patchedSourceHash && contentHash !== approvalTokenPayload.patchedSourceHash) {
|
|
135
|
+
return {
|
|
136
|
+
success: false,
|
|
137
|
+
targetFilePath: canonicalTarget,
|
|
138
|
+
bytesWritten: 0,
|
|
139
|
+
finalHash: contentHash,
|
|
140
|
+
error: `Patch Content Corrupted: Content hash does not match approved patchedSourceHash.`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// P0 TOCTOU Fix: Verify and atomically consume approval token strictly INSIDE the locked critical section
|
|
145
|
+
const tokenConsumption = ApprovalService.getInstance().verifyAndConsumeToken({
|
|
146
|
+
tokenId: approvalTokenPayload.tokenId,
|
|
147
|
+
targetFilePath: canonicalTarget,
|
|
148
|
+
currentSourceContent: currentContent,
|
|
149
|
+
targetClassName: approvalTokenPayload.targetClassName,
|
|
150
|
+
targetMemberName: approvalTokenPayload.targetMemberName,
|
|
151
|
+
patchHash: approvalTokenPayload.replacementAstHash,
|
|
152
|
+
fullDiffHash: approvalTokenPayload.fullDiffHash,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (!tokenConsumption.isValid) {
|
|
156
|
+
return {
|
|
157
|
+
success: false,
|
|
158
|
+
targetFilePath: canonicalTarget,
|
|
159
|
+
bytesWritten: 0,
|
|
160
|
+
finalHash: currentHash,
|
|
161
|
+
error: `Security Violation: Token authorization denied under lock: ${tokenConsumption.reason}`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 8. Create safety rollback backup
|
|
166
|
+
await fs.promises.copyFile(canonicalTarget, backupFile);
|
|
167
|
+
|
|
168
|
+
// 9. Write to secure temporary file in the same directory
|
|
169
|
+
await fs.promises.writeFile(tempFile, patchedContent, 'utf-8');
|
|
170
|
+
const tempContent = await fs.promises.readFile(tempFile, 'utf-8');
|
|
171
|
+
const writtenHash = ApprovalService.hashContent(tempContent);
|
|
172
|
+
if (writtenHash !== contentHash) {
|
|
173
|
+
throw new Error(`Integrity check failed: written temporary content hash mismatch.`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 10. Atomic replace with retry for Windows transient scanner locks
|
|
177
|
+
let replaced = false;
|
|
178
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
179
|
+
try {
|
|
180
|
+
fs.renameSync(tempFile, canonicalTarget);
|
|
181
|
+
replaced = true;
|
|
182
|
+
break;
|
|
183
|
+
} catch {
|
|
184
|
+
// Brief backoff for Windows anti-virus or indexer lock
|
|
185
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!replaced) {
|
|
190
|
+
// Safe rollback from backup: never do unvalidated overwrite
|
|
191
|
+
await fs.promises.copyFile(backupFile, canonicalTarget).catch(() => {});
|
|
192
|
+
throw new Error(`Failed to atomically replace '${canonicalTarget}' after multiple attempts.`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 11. Verify resulting file hash
|
|
196
|
+
const verifiedContent = await fs.promises.readFile(canonicalTarget, 'utf-8');
|
|
197
|
+
const finalHash = ApprovalService.hashContent(verifiedContent);
|
|
198
|
+
|
|
199
|
+
if (finalHash !== contentHash) {
|
|
200
|
+
// Rollback immediately if final hash mismatches expected
|
|
201
|
+
await fs.promises.copyFile(backupFile, canonicalTarget).catch(() => {});
|
|
202
|
+
return {
|
|
203
|
+
success: false,
|
|
204
|
+
targetFilePath: canonicalTarget,
|
|
205
|
+
bytesWritten: 0,
|
|
206
|
+
finalHash,
|
|
207
|
+
error: `Post-promotion verification failed: final file hash did not match expected hash. Rolled back from backup.`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 12. Cleanup safety backup on success
|
|
212
|
+
if (fs.existsSync(backupFile)) {
|
|
213
|
+
await fs.promises.unlink(backupFile).catch(() => {});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
success: true,
|
|
218
|
+
targetFilePath: canonicalTarget,
|
|
219
|
+
bytesWritten: Buffer.byteLength(verifiedContent, 'utf-8'),
|
|
220
|
+
finalHash,
|
|
221
|
+
};
|
|
222
|
+
} catch (err: any) {
|
|
223
|
+
// Clean up temporary file and restore from backup if necessary
|
|
224
|
+
if (fs.existsSync(tempFile)) {
|
|
225
|
+
try { fs.unlinkSync(tempFile); } catch {}
|
|
226
|
+
}
|
|
227
|
+
if (fs.existsSync(backupFile)) {
|
|
228
|
+
try {
|
|
229
|
+
await fs.promises.copyFile(backupFile, canonicalTarget);
|
|
230
|
+
await fs.promises.unlink(backupFile);
|
|
231
|
+
} catch {}
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
success: false,
|
|
235
|
+
targetFilePath: canonicalTarget,
|
|
236
|
+
bytesWritten: 0,
|
|
237
|
+
finalHash: '',
|
|
238
|
+
error: `Promotion I/O Error: ${err?.message ?? String(err)}`,
|
|
239
|
+
};
|
|
240
|
+
} finally {
|
|
241
|
+
// 13. Release in-process and cross-process lock
|
|
242
|
+
WorkspacePromoter.activeLocks.delete(lockKey);
|
|
243
|
+
WorkspacePromoter.releaseCrossProcessLock(lockFilePath);
|
|
244
|
+
if (fs.existsSync(tempFile)) {
|
|
245
|
+
try { fs.unlinkSync(tempFile); } catch {}
|
|
246
|
+
}
|
|
247
|
+
if (fs.existsSync(backupFile)) {
|
|
248
|
+
try { fs.unlinkSync(backupFile); } catch {}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Acquires a cross-process lock using exclusive file creation ('wx').
|
|
255
|
+
* If a lock exists, verifies whether the owning process is still alive.
|
|
256
|
+
* If the owner is dead or the lock is older than 30s, the stale lock is safely reclaimed.
|
|
257
|
+
*/
|
|
258
|
+
private static acquireCrossProcessLock(lockFilePath: string): { success: boolean; error?: string } {
|
|
259
|
+
const maxTries = 3;
|
|
260
|
+
for (let i = 0; i < maxTries; i++) {
|
|
261
|
+
try {
|
|
262
|
+
const payload: LockFilePayload = {
|
|
263
|
+
pid: process.pid,
|
|
264
|
+
timestamp: Date.now(),
|
|
265
|
+
};
|
|
266
|
+
fs.writeFileSync(lockFilePath, JSON.stringify(payload), { flag: 'wx' });
|
|
267
|
+
return { success: true };
|
|
268
|
+
} catch (err: any) {
|
|
269
|
+
if (err.code === 'EEXIST') {
|
|
270
|
+
// Check if lock is stale
|
|
271
|
+
try {
|
|
272
|
+
const raw = fs.readFileSync(lockFilePath, 'utf-8');
|
|
273
|
+
const existing: LockFilePayload = JSON.parse(raw);
|
|
274
|
+
const isAlive = WorktreeManager.isProcessAlive(existing.pid);
|
|
275
|
+
|
|
276
|
+
// P1: Never steal lock from a living process regardless of elapsed time
|
|
277
|
+
if (!isAlive) {
|
|
278
|
+
// Owning process has terminated: safely reclaim dead lock
|
|
279
|
+
try { fs.unlinkSync(lockFilePath); } catch {}
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
success: false,
|
|
285
|
+
error: `Conflict: Another active PIE process (PID ${existing.pid}) holds the promotion lock for this file.`,
|
|
286
|
+
};
|
|
287
|
+
} catch {
|
|
288
|
+
// Malformed lock file: reclaim
|
|
289
|
+
try { fs.unlinkSync(lockFilePath); } catch {}
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
success: false,
|
|
295
|
+
error: `Cross-process lock failure: ${err.message}`,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
success: false,
|
|
302
|
+
error: `Could not acquire promotion lock for '${lockFilePath}' after ${maxTries} attempts.`,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private static releaseCrossProcessLock(lockFilePath: string): void {
|
|
307
|
+
try {
|
|
308
|
+
if (fs.existsSync(lockFilePath)) {
|
|
309
|
+
fs.unlinkSync(lockFilePath);
|
|
310
|
+
}
|
|
311
|
+
} catch {}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import { WorktreeManager } from '../sandbox/worktree-manager.js';
|
|
4
|
+
import { ExecutionRunner } from '../sandbox/execution-runner.js';
|
|
5
|
+
import { ErrorCollector } from '../collectors/error-collector.js';
|
|
6
|
+
import { StaticAstCollector } from '../collectors/static-ast-collector.js';
|
|
7
|
+
import { GitDiffCollector } from '../collectors/git-diff-collector.js';
|
|
8
|
+
import { EnvironmentCollector } from '../collectors/environment-collector.js';
|
|
9
|
+
import { TraceCollector } from '../collectors/trace-collector.js';
|
|
10
|
+
import { EvidenceNormalizer } from '../evidence/evidence-normalizer.js';
|
|
11
|
+
import { EvidenceScorecard, UnifiedEvidenceRecord } from '../evidence/evidence-model.js';
|
|
12
|
+
import { EvidenceRedactor } from '../evidence/evidence-redactor.js';
|
|
13
|
+
import { DiagnosticContext } from '../core/types/diagnostic-context.js';
|
|
14
|
+
import { EvidenceCorrelator } from '../diagnostics/evidence-correlator.js';
|
|
15
|
+
import { DiagnosticReportGenerator } from '../diagnostics/diagnostic-report.js';
|
|
16
|
+
import { DirectBrowserInvestigator } from './direct-browser-investigator.js';
|
|
17
|
+
import { LocatorPlan } from '../selector/candidate-extractor.js';
|
|
18
|
+
import { BrowserOperationExecutionResult } from '../repair/browser-operation-executor.js';
|
|
19
|
+
|
|
20
|
+
export interface DiagnosticRerunOptions {
|
|
21
|
+
specFilePath: string;
|
|
22
|
+
workspaceRoot: string;
|
|
23
|
+
testNamePattern?: string;
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
useWorktree?: boolean;
|
|
26
|
+
enableLiveEscalation?: boolean;
|
|
27
|
+
operation?: unknown;
|
|
28
|
+
operationTargetPlan?: LocatorPlan | undefined;
|
|
29
|
+
operationReplacementPlan?: LocatorPlan | undefined;
|
|
30
|
+
operationOverlayPlan?: LocatorPlan | undefined;
|
|
31
|
+
allowInteraction?: boolean | undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface DiagnosticRerunResult {
|
|
35
|
+
passed: boolean;
|
|
36
|
+
exitCode: number;
|
|
37
|
+
stdout: string;
|
|
38
|
+
stderr: string;
|
|
39
|
+
scorecard: EvidenceScorecard;
|
|
40
|
+
unifiedEvidence: UnifiedEvidenceRecord[];
|
|
41
|
+
context: DiagnosticContext;
|
|
42
|
+
report: string;
|
|
43
|
+
executedInWorktree: boolean;
|
|
44
|
+
targetSpecPath: string;
|
|
45
|
+
browserOperationResult?: BrowserOperationExecutionResult | undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class DiagnosticRerunner {
|
|
49
|
+
/**
|
|
50
|
+
* Mode A: Executes the failing Playwright test inside an isolated Git worktree (AUDIT-11 / B2).
|
|
51
|
+
* Canonicalizes paths, guarantees worktree containment, captures multi-source evidence,
|
|
52
|
+
* enforces central redaction, and escalates to live browser investigation if trace is absent.
|
|
53
|
+
*/
|
|
54
|
+
public static async rerunAndDiagnose(options: DiagnosticRerunOptions): Promise<DiagnosticRerunResult> {
|
|
55
|
+
const executionId = `rerun-${Date.now()}`;
|
|
56
|
+
const worktreeManager = new WorktreeManager(options.workspaceRoot);
|
|
57
|
+
let targetDir = options.workspaceRoot;
|
|
58
|
+
let worktreeId: string | undefined;
|
|
59
|
+
|
|
60
|
+
// 1. Resolve canonical workspace root (AUDIT-05)
|
|
61
|
+
if (!fs.existsSync(options.workspaceRoot)) {
|
|
62
|
+
throw new Error(`Workspace root does not exist: '${options.workspaceRoot}'`);
|
|
63
|
+
}
|
|
64
|
+
const canonicalWorkspace = fs.realpathSync(options.workspaceRoot);
|
|
65
|
+
|
|
66
|
+
// 2. Fix absolute spec path bug (AUDIT-11 / B2): Calculate worktree-relative spec path
|
|
67
|
+
let relativeSpec = options.specFilePath;
|
|
68
|
+
if (path.isAbsolute(options.specFilePath)) {
|
|
69
|
+
if (!fs.existsSync(options.specFilePath)) {
|
|
70
|
+
throw new Error(`Spec file not found: '${options.specFilePath}'`);
|
|
71
|
+
}
|
|
72
|
+
const canonicalSpec = fs.realpathSync(options.specFilePath);
|
|
73
|
+
const rel = path.relative(canonicalWorkspace, canonicalSpec);
|
|
74
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Security Violation: Spec file '${options.specFilePath}' escapes workspace root '${canonicalWorkspace}'.`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
relativeSpec = rel;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (options.useWorktree !== false) {
|
|
83
|
+
worktreeId = `reproduce-${Date.now().toString(36)}`;
|
|
84
|
+
const info = await worktreeManager.createWorktree(worktreeId);
|
|
85
|
+
targetDir = info.worktreePath;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const allRecords: UnifiedEvidenceRecord[] = [];
|
|
90
|
+
const addRedacted = (records: UnifiedEvidenceRecord[]) => {
|
|
91
|
+
for (const rec of records) {
|
|
92
|
+
allRecords.push(EvidenceRedactor.redactRecord(rec));
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// 1. Environment Evidence
|
|
97
|
+
const envCollector = new EnvironmentCollector();
|
|
98
|
+
const envRecords = await envCollector.collect({
|
|
99
|
+
workspaceRoot: targetDir,
|
|
100
|
+
executionId,
|
|
101
|
+
});
|
|
102
|
+
addRedacted(envRecords);
|
|
103
|
+
|
|
104
|
+
// 2. Git Diff Evidence
|
|
105
|
+
const gitCollector = new GitDiffCollector();
|
|
106
|
+
const gitRecords = await gitCollector.collect({
|
|
107
|
+
workspaceRoot: targetDir,
|
|
108
|
+
executionId,
|
|
109
|
+
});
|
|
110
|
+
addRedacted(gitRecords);
|
|
111
|
+
|
|
112
|
+
// 3. Static AST Evidence of the failing spec strictly inside the worktree
|
|
113
|
+
const worktreeTargetSpec = path.resolve(targetDir, relativeSpec);
|
|
114
|
+
const astCollector = new StaticAstCollector();
|
|
115
|
+
const astRecords = await astCollector.collect({
|
|
116
|
+
targetFilePath: fs.existsSync(worktreeTargetSpec) ? worktreeTargetSpec : options.specFilePath,
|
|
117
|
+
executionId,
|
|
118
|
+
});
|
|
119
|
+
addRedacted(astRecords);
|
|
120
|
+
|
|
121
|
+
// 4. Execute Playwright test strictly targeting the worktree copy with --trace on (forward slashes for Windows regex matcher)
|
|
122
|
+
const playwrightArgs = ['playwright', 'test', relativeSpec.replace(/\\/g, '/'), '--trace', 'on'];
|
|
123
|
+
if (options.testNamePattern) {
|
|
124
|
+
playwrightArgs.push('-g', options.testNamePattern);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const runStartTime = Date.now();
|
|
128
|
+
const runResult = await ExecutionRunner.run('npx', {
|
|
129
|
+
cwd: targetDir,
|
|
130
|
+
args: playwrightArgs,
|
|
131
|
+
timeoutMs: options.timeoutMs ?? 60000,
|
|
132
|
+
env: {
|
|
133
|
+
PW_INTELLIGENCE_REPRODUCE: '1',
|
|
134
|
+
CI: '1',
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
const passed = runResult.exitCode === 0;
|
|
138
|
+
|
|
139
|
+
// 5. Ingest trace artifacts from test-results with freshness filtering (eliminates stale trace risk)
|
|
140
|
+
let traceCaptured = false;
|
|
141
|
+
const testResultsDir = path.join(targetDir, 'test-results');
|
|
142
|
+
if (fs.existsSync(testResultsDir)) {
|
|
143
|
+
const traceFiles = this.findTraceFiles(testResultsDir, runStartTime - 5000);
|
|
144
|
+
if (traceFiles.length > 0 && traceFiles[0]) {
|
|
145
|
+
const traceCollector = new TraceCollector();
|
|
146
|
+
const traceRecords = await traceCollector.collect({
|
|
147
|
+
archivePath: traceFiles[0],
|
|
148
|
+
executionId,
|
|
149
|
+
});
|
|
150
|
+
addRedacted(traceRecords);
|
|
151
|
+
traceCaptured = traceRecords.length > 0;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 6. Collect error evidence on failure
|
|
156
|
+
if (!passed) {
|
|
157
|
+
const errorCollector = new ErrorCollector();
|
|
158
|
+
const errorRecords = await errorCollector.collect({
|
|
159
|
+
errorMessage: runResult.stderr || runResult.stdout || 'Playwright test execution failed',
|
|
160
|
+
errorStack: runResult.stderr,
|
|
161
|
+
testFile: relativeSpec,
|
|
162
|
+
executionId,
|
|
163
|
+
});
|
|
164
|
+
addRedacted(errorRecords);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Check if trace records contain failure-point DOM snapshot (prevents Rule R-015 INCOMPLETE_EVIDENCE)
|
|
168
|
+
const hasDomSnapshot = allRecords.some((r) => r.evidenceType === 'dom-snapshot');
|
|
169
|
+
|
|
170
|
+
// Extract failing action selector if any from test output
|
|
171
|
+
let failingSelector: string | undefined;
|
|
172
|
+
let browserOperationResult: BrowserOperationExecutionResult | undefined;
|
|
173
|
+
const selectorMatch = (runResult.stdout + '\n' + runResult.stderr).match(/locator\(['"]([^'"]+)['"]\)/);
|
|
174
|
+
if (selectorMatch && selectorMatch[1]) {
|
|
175
|
+
failingSelector = selectorMatch[1];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 7. Tiered Escalation: If execution failed and (trace was absent OR trace lacked DOM snapshot), trigger Mode B DirectBrowserInvestigator
|
|
179
|
+
if (!passed && (!traceCaptured || !hasDomSnapshot) && options.enableLiveEscalation !== false) {
|
|
180
|
+
try {
|
|
181
|
+
// Attempt to extract target URL from AST records or test stdout/stderr
|
|
182
|
+
let discoveredUrl: string | undefined;
|
|
183
|
+
const urlMatch = (runResult.stdout + '\n' + runResult.stderr).match(/https?:\/\/[^\s"'`)]+/);
|
|
184
|
+
if (urlMatch && urlMatch[0]) {
|
|
185
|
+
discoveredUrl = urlMatch[0];
|
|
186
|
+
} else {
|
|
187
|
+
// Check AST records for page.goto calls
|
|
188
|
+
for (const rec of astRecords) {
|
|
189
|
+
const strPayload = JSON.stringify(rec.payload ?? {});
|
|
190
|
+
const astUrlMatch = strPayload.match(/https?:\/\/[^\s"'`)]+/);
|
|
191
|
+
if (astUrlMatch && astUrlMatch[0]) {
|
|
192
|
+
discoveredUrl = astUrlMatch[0];
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (discoveredUrl) {
|
|
199
|
+
const directRes = await DirectBrowserInvestigator.investigateUrl({
|
|
200
|
+
url: discoveredUrl,
|
|
201
|
+
actionSelector: failingSelector,
|
|
202
|
+
intendedAction: 'click',
|
|
203
|
+
executionId,
|
|
204
|
+
timeoutMs: 10000,
|
|
205
|
+
operation: options.operation,
|
|
206
|
+
operationTargetPlan: options.operationTargetPlan,
|
|
207
|
+
operationReplacementPlan: options.operationReplacementPlan,
|
|
208
|
+
operationOverlayPlan: options.operationOverlayPlan,
|
|
209
|
+
allowInteraction: options.allowInteraction,
|
|
210
|
+
});
|
|
211
|
+
browserOperationResult = directRes.browserOperationResult;
|
|
212
|
+
|
|
213
|
+
// AUDIT FIX: Only accept live browser evidence if action genuinely failed (REPRODUCED) or captured failure DOM
|
|
214
|
+
if (directRes.success && (directRes.status === 'REPRODUCED' || directRes.domSnapshotRecord)) {
|
|
215
|
+
if (directRes.domSnapshotRecord) {
|
|
216
|
+
addRedacted([directRes.domSnapshotRecord]);
|
|
217
|
+
}
|
|
218
|
+
if (directRes.ariaSnapshotRecord) {
|
|
219
|
+
addRedacted([directRes.ariaSnapshotRecord]);
|
|
220
|
+
}
|
|
221
|
+
if (directRes.status === 'REPRODUCED' && directRes.records.length > 0) {
|
|
222
|
+
addRedacted(directRes.records);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
} catch {
|
|
227
|
+
// Graceful fallback if headless chromium cannot launch
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 8. Normalize multi-source evidence to DiagnosticContext
|
|
232
|
+
const context = EvidenceNormalizer.toDiagnosticContext(allRecords);
|
|
233
|
+
const scorecard = EvidenceNormalizer.calculateScorecard(allRecords);
|
|
234
|
+
|
|
235
|
+
// 9. Correlate and generate report
|
|
236
|
+
const correlated = EvidenceCorrelator.correlate(context);
|
|
237
|
+
const report = DiagnosticReportGenerator.generateTerminalReport(correlated, scorecard);
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
passed,
|
|
241
|
+
exitCode: runResult.exitCode,
|
|
242
|
+
stdout: runResult.stdout,
|
|
243
|
+
stderr: runResult.stderr,
|
|
244
|
+
scorecard,
|
|
245
|
+
unifiedEvidence: allRecords,
|
|
246
|
+
context,
|
|
247
|
+
report,
|
|
248
|
+
executedInWorktree: options.useWorktree !== false,
|
|
249
|
+
targetSpecPath: worktreeTargetSpec,
|
|
250
|
+
browserOperationResult,
|
|
251
|
+
};
|
|
252
|
+
} finally {
|
|
253
|
+
if (worktreeId) {
|
|
254
|
+
await worktreeManager.destroyWorktree(worktreeId).catch(() => {});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private static findTraceFiles(dir: string, minMtimeMs?: number): string[] {
|
|
260
|
+
const results: Array<{ filePath: string; mtimeMs: number }> = [];
|
|
261
|
+
const scan = (currentDir: string) => {
|
|
262
|
+
try {
|
|
263
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
264
|
+
for (const entry of entries) {
|
|
265
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
266
|
+
if (entry.isDirectory()) {
|
|
267
|
+
scan(fullPath);
|
|
268
|
+
} else if (entry.isFile() && entry.name.endsWith('trace.zip')) {
|
|
269
|
+
try {
|
|
270
|
+
const stat = fs.statSync(fullPath);
|
|
271
|
+
if (!minMtimeMs || stat.mtimeMs >= minMtimeMs) {
|
|
272
|
+
results.push({ filePath: fullPath, mtimeMs: stat.mtimeMs });
|
|
273
|
+
}
|
|
274
|
+
} catch {
|
|
275
|
+
results.push({ filePath: fullPath, mtimeMs: 0 });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
} catch {
|
|
280
|
+
// Ignore directory read errors
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
scan(dir);
|
|
284
|
+
// Sort descending by modification time (freshest trace first)
|
|
285
|
+
return results.sort((a, b) => b.mtimeMs - a.mtimeMs).map((r) => r.filePath);
|
|
286
|
+
}
|
|
287
|
+
}
|