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,243 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export interface SelectorHistoryRecord {
|
|
5
|
+
selector: string;
|
|
6
|
+
successes: number;
|
|
7
|
+
failures: number;
|
|
8
|
+
semanticVerifications: number;
|
|
9
|
+
repairsAccepted: number;
|
|
10
|
+
falseRepairIncidents: number;
|
|
11
|
+
lastSuccess?: number | undefined;
|
|
12
|
+
lastFailure?: number | undefined;
|
|
13
|
+
failureCategories: string[];
|
|
14
|
+
reliabilityScore: number;
|
|
15
|
+
historicalExecutionReliability: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface StabilityLedgerData {
|
|
19
|
+
version: number;
|
|
20
|
+
updatedAt: number;
|
|
21
|
+
scope: {
|
|
22
|
+
projectRoot: string;
|
|
23
|
+
repositoryName: string;
|
|
24
|
+
};
|
|
25
|
+
selectors: Record<string, SelectorHistoryRecord>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type RepairFeedbackOutcome = 'accepted' | 'rejected';
|
|
29
|
+
|
|
30
|
+
/** Applies bounded time decay toward the neutral prior without mutating history. */
|
|
31
|
+
export function effectiveReliabilityScore(
|
|
32
|
+
record: SelectorHistoryRecord,
|
|
33
|
+
now = Date.now(),
|
|
34
|
+
halfLifeMs = 30 * 24 * 60 * 60 * 1000,
|
|
35
|
+
): number {
|
|
36
|
+
if (record.falseRepairIncidents > 0) return 0.1;
|
|
37
|
+
const latestEvidence = Math.max(record.lastSuccess ?? 0, record.lastFailure ?? 0);
|
|
38
|
+
if (latestEvidence <= 0 || halfLifeMs <= 0) return record.reliabilityScore;
|
|
39
|
+
const ageMs = Math.max(0, now - latestEvidence);
|
|
40
|
+
const decay = Math.exp(-ageMs / halfLifeMs);
|
|
41
|
+
const score = 0.8 + (record.reliabilityScore - 0.8) * decay;
|
|
42
|
+
return Number(Math.max(0, Math.min(1, score)).toFixed(3));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class StabilityLedger {
|
|
46
|
+
private readonly ledgerPath: string;
|
|
47
|
+
private data: StabilityLedgerData | null = null;
|
|
48
|
+
|
|
49
|
+
constructor(baseDir = '.') {
|
|
50
|
+
this.ledgerPath = path.resolve(baseDir, '.pw-intelligence', 'stability-ledger.json');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public async init(): Promise<void> {
|
|
54
|
+
await fs.mkdir(path.dirname(this.ledgerPath), { recursive: true });
|
|
55
|
+
try {
|
|
56
|
+
const raw = await fs.readFile(this.ledgerPath, 'utf8');
|
|
57
|
+
const parsed = JSON.parse(raw) as Partial<StabilityLedgerData>;
|
|
58
|
+
this.data = {
|
|
59
|
+
version: parsed.version ?? 3,
|
|
60
|
+
updatedAt: parsed.updatedAt ?? Date.now(),
|
|
61
|
+
scope: parsed.scope ?? {
|
|
62
|
+
projectRoot: path.resolve(path.dirname(this.ledgerPath), '..'),
|
|
63
|
+
repositoryName: path.basename(path.resolve(path.dirname(this.ledgerPath), '..')),
|
|
64
|
+
},
|
|
65
|
+
selectors: parsed.selectors ?? {},
|
|
66
|
+
};
|
|
67
|
+
if (!parsed.scope || parsed.version !== 3) await this.save();
|
|
68
|
+
} catch {
|
|
69
|
+
this.data = {
|
|
70
|
+
version: 3,
|
|
71
|
+
updatedAt: Date.now(),
|
|
72
|
+
scope: {
|
|
73
|
+
projectRoot: path.resolve(path.dirname(this.ledgerPath), '..'),
|
|
74
|
+
repositoryName: path.basename(path.resolve(path.dirname(this.ledgerPath), '..')),
|
|
75
|
+
},
|
|
76
|
+
selectors: {},
|
|
77
|
+
};
|
|
78
|
+
await this.save();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public getScore(selector: string): number {
|
|
83
|
+
if (!this.data) return 0.8; // Default baseline score
|
|
84
|
+
const record = this.data.selectors[selector];
|
|
85
|
+
if (!record) return 0.8;
|
|
86
|
+
return record.reliabilityScore;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public getEffectiveScore(selector: string, now = Date.now(), halfLifeMs?: number): number {
|
|
90
|
+
if (!this.data) return 0.8;
|
|
91
|
+
const record = this.data.selectors[selector];
|
|
92
|
+
return record ? effectiveReliabilityScore(record, now, halfLifeMs) : 0.8;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public async recordSuccess(selector: string): Promise<void> {
|
|
96
|
+
await this.ensureLoaded();
|
|
97
|
+
const existing = this.data!.selectors[selector] ?? {
|
|
98
|
+
selector,
|
|
99
|
+
successes: 0,
|
|
100
|
+
failures: 0,
|
|
101
|
+
semanticVerifications: 1,
|
|
102
|
+
repairsAccepted: 0,
|
|
103
|
+
falseRepairIncidents: 0,
|
|
104
|
+
failureCategories: [],
|
|
105
|
+
reliabilityScore: 0.8,
|
|
106
|
+
historicalExecutionReliability: 0.8,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
existing.successes += 1;
|
|
110
|
+
existing.lastSuccess = Date.now();
|
|
111
|
+
existing.reliabilityScore = this.computeScore(existing);
|
|
112
|
+
existing.historicalExecutionReliability = existing.reliabilityScore;
|
|
113
|
+
this.data!.selectors[selector] = existing;
|
|
114
|
+
await this.save();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public async recordFailure(selector: string, category: string): Promise<void> {
|
|
118
|
+
await this.ensureLoaded();
|
|
119
|
+
const existing = this.data!.selectors[selector] ?? {
|
|
120
|
+
selector,
|
|
121
|
+
successes: 0,
|
|
122
|
+
failures: 0,
|
|
123
|
+
semanticVerifications: 0,
|
|
124
|
+
repairsAccepted: 0,
|
|
125
|
+
falseRepairIncidents: 0,
|
|
126
|
+
failureCategories: [],
|
|
127
|
+
reliabilityScore: 0.8,
|
|
128
|
+
historicalExecutionReliability: 0.8,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
existing.failures += 1;
|
|
132
|
+
existing.lastFailure = Date.now();
|
|
133
|
+
if (!existing.failureCategories.includes(category)) {
|
|
134
|
+
existing.failureCategories.push(category);
|
|
135
|
+
}
|
|
136
|
+
existing.reliabilityScore = this.computeScore(existing);
|
|
137
|
+
existing.historicalExecutionReliability = existing.reliabilityScore;
|
|
138
|
+
this.data!.selectors[selector] = existing;
|
|
139
|
+
await this.save();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public async recordSemanticVerification(selector: string): Promise<void> {
|
|
143
|
+
await this.ensureLoaded();
|
|
144
|
+
const existing = this.data!.selectors[selector] ?? this.createRecord(selector);
|
|
145
|
+
existing.semanticVerifications += 1;
|
|
146
|
+
// A semantic verification is produced only after the isolated execution
|
|
147
|
+
// and intent gates pass. Count it as one successful observation so future
|
|
148
|
+
// candidate ranking can learn from verified repairs, while keeping the
|
|
149
|
+
// separate repairsAccepted field as operator-approval metadata.
|
|
150
|
+
existing.successes += 1;
|
|
151
|
+
existing.lastSuccess = Date.now();
|
|
152
|
+
existing.reliabilityScore = this.computeScore(existing);
|
|
153
|
+
existing.historicalExecutionReliability = existing.reliabilityScore;
|
|
154
|
+
this.data!.selectors[selector] = existing;
|
|
155
|
+
await this.save();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
public async recordRepairAccepted(selector: string): Promise<void> {
|
|
159
|
+
await this.ensureLoaded();
|
|
160
|
+
const existing = this.data!.selectors[selector] ?? this.createRecord(selector);
|
|
161
|
+
existing.repairsAccepted += 1;
|
|
162
|
+
this.data!.selectors[selector] = existing;
|
|
163
|
+
await this.save();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
public getSnapshot(): StabilityLedgerData {
|
|
167
|
+
if (!this.data) {
|
|
168
|
+
return {
|
|
169
|
+
version: 3,
|
|
170
|
+
updatedAt: 0,
|
|
171
|
+
scope: {
|
|
172
|
+
projectRoot: path.resolve(path.dirname(this.ledgerPath), '..'),
|
|
173
|
+
repositoryName: path.basename(path.resolve(path.dirname(this.ledgerPath), '..')),
|
|
174
|
+
},
|
|
175
|
+
selectors: {},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
return JSON.parse(JSON.stringify(this.data)) as StabilityLedgerData;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
public async recordFalseRepairIncident(selector: string): Promise<void> {
|
|
182
|
+
await this.ensureLoaded();
|
|
183
|
+
const existing = this.data!.selectors[selector] ?? {
|
|
184
|
+
selector,
|
|
185
|
+
successes: 0,
|
|
186
|
+
failures: 1,
|
|
187
|
+
semanticVerifications: 0,
|
|
188
|
+
repairsAccepted: 0,
|
|
189
|
+
falseRepairIncidents: 0,
|
|
190
|
+
failureCategories: ['FALSE_REPAIR'],
|
|
191
|
+
reliabilityScore: 0.2,
|
|
192
|
+
historicalExecutionReliability: 0.2,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
existing.falseRepairIncidents += 1;
|
|
196
|
+
existing.reliabilityScore = this.computeScore(existing);
|
|
197
|
+
existing.historicalExecutionReliability = existing.reliabilityScore;
|
|
198
|
+
this.data!.selectors[selector] = existing;
|
|
199
|
+
await this.save();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Records operator review feedback without erasing the underlying history. */
|
|
203
|
+
public async recordRepairFeedback(selector: string, outcome: RepairFeedbackOutcome): Promise<void> {
|
|
204
|
+
if (outcome === 'accepted') await this.recordRepairAccepted(selector);
|
|
205
|
+
else await this.recordFalseRepairIncident(selector);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private computeScore(record: { successes: number; failures: number; falseRepairIncidents: number }): number {
|
|
209
|
+
// If any false repair incident occurred, severely penalize (fail-safe against confirmation bias)
|
|
210
|
+
if (record.falseRepairIncidents > 0) {
|
|
211
|
+
return 0.1;
|
|
212
|
+
}
|
|
213
|
+
const total = record.successes + record.failures * 2; // Penalize failures with double weight
|
|
214
|
+
if (total === 0) return 0.8;
|
|
215
|
+
const score = record.successes / total;
|
|
216
|
+
return parseFloat(score.toFixed(3));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private createRecord(selector: string): SelectorHistoryRecord {
|
|
220
|
+
return {
|
|
221
|
+
selector,
|
|
222
|
+
successes: 0,
|
|
223
|
+
failures: 0,
|
|
224
|
+
semanticVerifications: 0,
|
|
225
|
+
repairsAccepted: 0,
|
|
226
|
+
falseRepairIncidents: 0,
|
|
227
|
+
failureCategories: [],
|
|
228
|
+
reliabilityScore: 0.8,
|
|
229
|
+
historicalExecutionReliability: 0.8,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private async ensureLoaded(): Promise<void> {
|
|
234
|
+
if (!this.data) await this.init();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private async save(): Promise<void> {
|
|
238
|
+
if (!this.data) return;
|
|
239
|
+
this.data.updatedAt = Date.now();
|
|
240
|
+
await fs.writeFile(this.ledgerPath, JSON.stringify(this.data, null, 2), 'utf8');
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const AppElementSchema = z.object({
|
|
4
|
+
tag: z.string().min(1),
|
|
5
|
+
role: z.string().optional(),
|
|
6
|
+
accessibleName: z.string().optional(),
|
|
7
|
+
label: z.string().optional(),
|
|
8
|
+
testId: z.string().optional(),
|
|
9
|
+
placeholder: z.string().optional(),
|
|
10
|
+
type: z.string().optional(),
|
|
11
|
+
frameSelector: z.string().optional(),
|
|
12
|
+
count: z.number().int().min(1).default(1),
|
|
13
|
+
visible: z.boolean().default(true),
|
|
14
|
+
});
|
|
15
|
+
export type AppElement = z.infer<typeof AppElementSchema>;
|
|
16
|
+
|
|
17
|
+
export const AppRouteSchema = z.object({
|
|
18
|
+
route: z.string().min(1),
|
|
19
|
+
title: z.string().optional(),
|
|
20
|
+
elements: z.array(AppElementSchema).default([]),
|
|
21
|
+
});
|
|
22
|
+
export type AppRoute = z.infer<typeof AppRouteSchema>;
|
|
23
|
+
|
|
24
|
+
export const AppModelSchema = z.object({
|
|
25
|
+
version: z.literal('1').default('1'),
|
|
26
|
+
baseUrl: z.string().min(1),
|
|
27
|
+
fingerprint: z.string().min(1),
|
|
28
|
+
capturedAt: z.string(),
|
|
29
|
+
routes: z.array(AppRouteSchema).default([]),
|
|
30
|
+
});
|
|
31
|
+
export type AppModel = z.infer<typeof AppModelSchema>;
|
|
32
|
+
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { AppModel, AppModelSchema, AppRoute, AppElement } from './app-model.js';
|
|
5
|
+
|
|
6
|
+
export interface CrawlerOptions {
|
|
7
|
+
baseUrl: string;
|
|
8
|
+
fingerprint?: string;
|
|
9
|
+
maxRoutes?: number;
|
|
10
|
+
maxDepth?: number;
|
|
11
|
+
storageStatePath?: string;
|
|
12
|
+
cacheDir?: string;
|
|
13
|
+
browser?: any;
|
|
14
|
+
page?: any;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function computeCacheKey(baseUrl: string, fingerprint?: string): string {
|
|
18
|
+
const normalized = baseUrl.replace(/\/+$/, '').toLowerCase();
|
|
19
|
+
const raw = `${normalized}::${fingerprint ?? 'default'}`;
|
|
20
|
+
return crypto.createHash('sha256').update(raw).digest('hex').slice(0, 16);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function getCachedAppModel(cacheDir: string, baseUrl: string, fingerprint?: string): Promise<AppModel | null> {
|
|
24
|
+
try {
|
|
25
|
+
const key = computeCacheKey(baseUrl, fingerprint);
|
|
26
|
+
const filePath = path.join(cacheDir, `app-model-${key}.json`);
|
|
27
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
28
|
+
const parsed = JSON.parse(content);
|
|
29
|
+
const validated = AppModelSchema.safeParse(parsed);
|
|
30
|
+
if (validated.success) {
|
|
31
|
+
return validated.data;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function saveCachedAppModel(cacheDir: string, model: AppModel): Promise<void> {
|
|
40
|
+
try {
|
|
41
|
+
await fs.mkdir(cacheDir, { recursive: true });
|
|
42
|
+
const key = computeCacheKey(model.baseUrl, model.fingerprint);
|
|
43
|
+
const filePath = path.join(cacheDir, `app-model-${key}.json`);
|
|
44
|
+
await fs.writeFile(filePath, JSON.stringify(model, null, 2), 'utf8');
|
|
45
|
+
} catch {
|
|
46
|
+
// Non-fatal if cache cannot be written
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function extractFrameSelector(frame: any): Promise<string> {
|
|
51
|
+
try {
|
|
52
|
+
if (typeof frame.name === 'function' && frame.name()) {
|
|
53
|
+
return `iframe[name="${frame.name()}"]`;
|
|
54
|
+
}
|
|
55
|
+
if (typeof frame.frameElement === 'function') {
|
|
56
|
+
const handle = await frame.frameElement();
|
|
57
|
+
if (handle) {
|
|
58
|
+
const selector = await handle.evaluate((el: any) => {
|
|
59
|
+
const title = el.getAttribute('title');
|
|
60
|
+
if (title) return `iframe[title="${title}"]`;
|
|
61
|
+
const name = el.getAttribute('name');
|
|
62
|
+
if (name) return `iframe[name="${name}"]`;
|
|
63
|
+
const id = el.getAttribute('id');
|
|
64
|
+
if (id) return `iframe#${id}`;
|
|
65
|
+
const src = el.getAttribute('src');
|
|
66
|
+
if (src) return `iframe[src="${src}"]`;
|
|
67
|
+
return 'iframe';
|
|
68
|
+
});
|
|
69
|
+
if (selector) return selector;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
// Fallback if frame element lookup fails
|
|
74
|
+
}
|
|
75
|
+
return 'iframe';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function collectFrameElements(frameHandle: any, frameSelector?: string): Promise<AppElement[]> {
|
|
79
|
+
try {
|
|
80
|
+
const raw = await frameHandle.$$eval('button, a, input, select, textarea, [role="button"], [role="textbox"], [role="status"], [role="list"], [role="listitem"], ul, ol, heading, h1, h2, h3', (nodes: any[]) => {
|
|
81
|
+
return nodes.map((el) => {
|
|
82
|
+
const tag = el.tagName.toLowerCase();
|
|
83
|
+
const type = el.getAttribute('type') || undefined;
|
|
84
|
+
let role = el.getAttribute('role');
|
|
85
|
+
if (!role) {
|
|
86
|
+
if (tag === 'button') role = 'button';
|
|
87
|
+
else if (tag === 'input') {
|
|
88
|
+
if (type === 'checkbox') role = 'checkbox';
|
|
89
|
+
else if (type === 'radio') role = 'radio';
|
|
90
|
+
else if (type === 'submit' || type === 'button') role = 'button';
|
|
91
|
+
else role = 'textbox';
|
|
92
|
+
} else if (tag === 'textarea') role = 'textbox';
|
|
93
|
+
else if (tag === 'select') role = 'combobox';
|
|
94
|
+
else if (/^h[1-6]$/.test(tag)) role = 'heading';
|
|
95
|
+
else if (tag === 'ul' || tag === 'ol') role = 'list';
|
|
96
|
+
}
|
|
97
|
+
const placeholder = el.getAttribute('placeholder') || undefined;
|
|
98
|
+
const accessibleName = el.getAttribute('aria-label') || el.innerText?.trim() || placeholder || undefined;
|
|
99
|
+
const label = el.getAttribute('aria-label') || el.getAttribute('name') || undefined;
|
|
100
|
+
const testId = el.getAttribute('data-testid') || el.getAttribute('data-test') || undefined;
|
|
101
|
+
const visible = Boolean(el.offsetWidth || el.offsetHeight || el.getClientRects().length);
|
|
102
|
+
return { tag, role, accessibleName, label, testId, placeholder, type, visible };
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const elements: AppElement[] = raw.map((item: any) => ({
|
|
107
|
+
tag: item.tag,
|
|
108
|
+
role: item.role,
|
|
109
|
+
accessibleName: item.accessibleName,
|
|
110
|
+
label: item.label,
|
|
111
|
+
testId: item.testId,
|
|
112
|
+
placeholder: item.placeholder,
|
|
113
|
+
type: item.type,
|
|
114
|
+
frameSelector,
|
|
115
|
+
count: 1,
|
|
116
|
+
visible: item.visible,
|
|
117
|
+
}));
|
|
118
|
+
return elements;
|
|
119
|
+
} catch {
|
|
120
|
+
if (frameSelector) {
|
|
121
|
+
return [{
|
|
122
|
+
tag: 'iframe',
|
|
123
|
+
role: 'cross-origin-unavailable',
|
|
124
|
+
frameSelector,
|
|
125
|
+
count: 1,
|
|
126
|
+
visible: true,
|
|
127
|
+
}];
|
|
128
|
+
}
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export class AppCrawler {
|
|
134
|
+
public static async crawl(options: CrawlerOptions): Promise<AppModel> {
|
|
135
|
+
const cacheDir = options.cacheDir ?? path.join(process.cwd(), '.pie', 'cache');
|
|
136
|
+
const fingerprint = options.fingerprint ?? 'default';
|
|
137
|
+
|
|
138
|
+
// 1. Check cache
|
|
139
|
+
const cached = await getCachedAppModel(cacheDir, options.baseUrl, fingerprint);
|
|
140
|
+
if (cached) {
|
|
141
|
+
return cached;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const maxRoutes = options.maxRoutes ?? 20;
|
|
145
|
+
const maxDepth = options.maxDepth ?? 2;
|
|
146
|
+
const baseHost = new URL(options.baseUrl).host;
|
|
147
|
+
|
|
148
|
+
const routes: AppRoute[] = [];
|
|
149
|
+
let browserToClose: any = null;
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
let page = options.page;
|
|
153
|
+
if (!page) {
|
|
154
|
+
if (options.browser) {
|
|
155
|
+
const context = await options.browser.newContext({
|
|
156
|
+
storageState: options.storageStatePath,
|
|
157
|
+
});
|
|
158
|
+
page = await context.newPage();
|
|
159
|
+
} else {
|
|
160
|
+
// Attempt dynamic import of playwright
|
|
161
|
+
try {
|
|
162
|
+
const { chromium } = await import('@playwright/test');
|
|
163
|
+
browserToClose = await chromium.launch({ headless: true });
|
|
164
|
+
const context = await browserToClose.newContext({
|
|
165
|
+
storageState: options.storageStatePath,
|
|
166
|
+
});
|
|
167
|
+
page = await context.newPage();
|
|
168
|
+
} catch {
|
|
169
|
+
// If browser cannot be launched, return minimal bounded model
|
|
170
|
+
const fallbackModel: AppModel = {
|
|
171
|
+
version: '1',
|
|
172
|
+
baseUrl: options.baseUrl,
|
|
173
|
+
fingerprint,
|
|
174
|
+
capturedAt: new Date().toISOString(),
|
|
175
|
+
routes: [{ route: '/', elements: [] }],
|
|
176
|
+
};
|
|
177
|
+
return fallbackModel;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const queue: Array<{ url: string; depth: number }> = [{ url: options.baseUrl, depth: 0 }];
|
|
183
|
+
const visited = new Set<string>();
|
|
184
|
+
|
|
185
|
+
while (queue.length > 0 && routes.length < maxRoutes) {
|
|
186
|
+
const item = queue.shift()!;
|
|
187
|
+
if (visited.has(item.url)) continue;
|
|
188
|
+
visited.add(item.url);
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
await page.goto(item.url, { waitUntil: 'domcontentloaded', timeout: 15_000 });
|
|
192
|
+
} catch {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const elements: AppElement[] = [];
|
|
197
|
+
|
|
198
|
+
// Main frame elements
|
|
199
|
+
const mainElements = await collectFrameElements(page, undefined);
|
|
200
|
+
elements.push(...mainElements);
|
|
201
|
+
|
|
202
|
+
// Child frame elements
|
|
203
|
+
if (typeof page.frames === 'function') {
|
|
204
|
+
const childFrames = page.frames().slice(1);
|
|
205
|
+
for (const childFrame of childFrames) {
|
|
206
|
+
const frameSelector = await extractFrameSelector(childFrame);
|
|
207
|
+
const childElements = await collectFrameElements(childFrame, frameSelector);
|
|
208
|
+
elements.push(...childElements);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const urlObj = new URL(item.url);
|
|
213
|
+
routes.push({
|
|
214
|
+
route: urlObj.pathname || '/',
|
|
215
|
+
title: typeof page.title === 'function' ? await page.title().catch(() => undefined) : undefined,
|
|
216
|
+
elements,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Bounded exploration: collect links without clicking destructive elements
|
|
220
|
+
if (item.depth < maxDepth && routes.length < maxRoutes) {
|
|
221
|
+
try {
|
|
222
|
+
const hrefs: string[] = await page.$$eval('a[href]', (anchors: any[]) =>
|
|
223
|
+
anchors.map((a) => a.getAttribute('href')).filter(Boolean),
|
|
224
|
+
);
|
|
225
|
+
for (const href of hrefs) {
|
|
226
|
+
try {
|
|
227
|
+
const resolved = new URL(href, item.url);
|
|
228
|
+
// Allowlisted host only, no logout/delete/submit paths
|
|
229
|
+
if (resolved.host === baseHost && !/logout|signout|delete|destroy/i.test(resolved.pathname)) {
|
|
230
|
+
const cleaned = resolved.origin + resolved.pathname;
|
|
231
|
+
if (!visited.has(cleaned) && !queue.some((q) => q.url === cleaned)) {
|
|
232
|
+
queue.push({ url: cleaned, depth: item.depth + 1 });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
} catch {
|
|
236
|
+
// Ignore invalid URLs
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
} catch {
|
|
240
|
+
// Ignore link evaluation failure
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} catch {
|
|
245
|
+
// Bounded fallback
|
|
246
|
+
} finally {
|
|
247
|
+
if (browserToClose) {
|
|
248
|
+
await browserToClose.close().catch(() => {});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const model: AppModel = {
|
|
253
|
+
version: '1',
|
|
254
|
+
baseUrl: options.baseUrl,
|
|
255
|
+
fingerprint,
|
|
256
|
+
capturedAt: new Date().toISOString(),
|
|
257
|
+
routes: routes.length > 0 ? routes : [{ route: '/', elements: [] }],
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
await saveCachedAppModel(cacheDir, model);
|
|
261
|
+
return model;
|
|
262
|
+
}
|
|
263
|
+
}
|