sela-core 0.1.0-alpha.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/README.md +103 -0
- package/bin/sela.js +3 -0
- package/dist/cli/ErrorHandler.d.ts +10 -0
- package/dist/cli/ErrorHandler.d.ts.map +1 -0
- package/dist/cli/ErrorHandler.js +70 -0
- package/dist/cli/commands/bulk.d.ts +3 -0
- package/dist/cli/commands/bulk.d.ts.map +1 -0
- package/dist/cli/commands/bulk.js +158 -0
- package/dist/cli/commands/find.d.ts +3 -0
- package/dist/cli/commands/find.d.ts.map +1 -0
- package/dist/cli/commands/find.js +51 -0
- package/dist/cli/commands/init.d.ts +3 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +166 -0
- package/dist/cli/commands/list.d.ts +3 -0
- package/dist/cli/commands/list.d.ts.map +1 -0
- package/dist/cli/commands/list.js +56 -0
- package/dist/cli/commands/merge.d.ts +13 -0
- package/dist/cli/commands/merge.d.ts.map +1 -0
- package/dist/cli/commands/merge.js +119 -0
- package/dist/cli/commands/refactor.d.ts +3 -0
- package/dist/cli/commands/refactor.d.ts.map +1 -0
- package/dist/cli/commands/refactor.js +30 -0
- package/dist/cli/commands/showReport.d.ts +3 -0
- package/dist/cli/commands/showReport.d.ts.map +1 -0
- package/dist/cli/commands/showReport.js +82 -0
- package/dist/cli/commands/status.d.ts +3 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +51 -0
- package/dist/cli/commands/sync.d.ts +3 -0
- package/dist/cli/commands/sync.d.ts.map +1 -0
- package/dist/cli/commands/sync.js +131 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +94 -0
- package/dist/cli/ui/DnaTable.d.ts +3 -0
- package/dist/cli/ui/DnaTable.d.ts.map +1 -0
- package/dist/cli/ui/DnaTable.js +77 -0
- package/dist/cli/ui/LocatorPicker.d.ts +8 -0
- package/dist/cli/ui/LocatorPicker.d.ts.map +1 -0
- package/dist/cli/ui/LocatorPicker.js +33 -0
- package/dist/cli/ui/ProgressReporter.d.ts +11 -0
- package/dist/cli/ui/ProgressReporter.d.ts.map +1 -0
- package/dist/cli/ui/ProgressReporter.js +33 -0
- package/dist/cli/ui/RefactorWizard.d.ts +26 -0
- package/dist/cli/ui/RefactorWizard.d.ts.map +1 -0
- package/dist/cli/ui/RefactorWizard.js +295 -0
- package/dist/config/ConfigLoader.d.ts +16 -0
- package/dist/config/ConfigLoader.d.ts.map +1 -0
- package/dist/config/ConfigLoader.js +185 -0
- package/dist/config/DryRunGuard.d.ts +14 -0
- package/dist/config/DryRunGuard.d.ts.map +1 -0
- package/dist/config/DryRunGuard.js +80 -0
- package/dist/config/PathScopeGuard.d.ts +42 -0
- package/dist/config/PathScopeGuard.d.ts.map +1 -0
- package/dist/config/PathScopeGuard.js +192 -0
- package/dist/config/SelaConfig.d.ts +178 -0
- package/dist/config/SelaConfig.d.ts.map +1 -0
- package/dist/config/SelaConfig.js +141 -0
- package/dist/config/defineConfig.d.ts +3 -0
- package/dist/config/defineConfig.d.ts.map +1 -0
- package/dist/config/defineConfig.js +9 -0
- package/dist/engine/FixwrightEngine.d.ts +24 -0
- package/dist/engine/FixwrightEngine.d.ts.map +1 -0
- package/dist/engine/FixwrightEngine.js +403 -0
- package/dist/engine/HealingRegistry.d.ts +40 -0
- package/dist/engine/HealingRegistry.d.ts.map +1 -0
- package/dist/engine/HealingRegistry.js +99 -0
- package/dist/engine/SelaEngine.d.ts +57 -0
- package/dist/engine/SelaEngine.d.ts.map +1 -0
- package/dist/engine/SelaEngine.js +1146 -0
- package/dist/engine/singleton.d.ts +3 -0
- package/dist/engine/singleton.d.ts.map +1 -0
- package/dist/engine/singleton.js +5 -0
- package/dist/errors/SelaError.d.ts +146 -0
- package/dist/errors/SelaError.d.ts.map +1 -0
- package/dist/errors/SelaError.js +168 -0
- package/dist/fixtures/expectProxy.d.ts +12 -0
- package/dist/fixtures/expectProxy.d.ts.map +1 -0
- package/dist/fixtures/expectProxy.js +246 -0
- package/dist/fixtures/index.d.ts +30 -0
- package/dist/fixtures/index.d.ts.map +1 -0
- package/dist/fixtures/index.js +765 -0
- package/dist/fixtures/moduleExpect.d.ts +2 -0
- package/dist/fixtures/moduleExpect.d.ts.map +1 -0
- package/dist/fixtures/moduleExpect.js +54 -0
- package/dist/fixtures/proxyTag.d.ts +12 -0
- package/dist/fixtures/proxyTag.d.ts.map +1 -0
- package/dist/fixtures/proxyTag.js +45 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/reporter/SelaReporter.d.ts +165 -0
- package/dist/reporter/SelaReporter.d.ts.map +1 -0
- package/dist/reporter/SelaReporter.js +517 -0
- package/dist/services/ASTSourceUpdater.d.ts +143 -0
- package/dist/services/ASTSourceUpdater.d.ts.map +1 -0
- package/dist/services/ASTSourceUpdater.js +3514 -0
- package/dist/services/AnchorResolver.d.ts +157 -0
- package/dist/services/AnchorResolver.d.ts.map +1 -0
- package/dist/services/AnchorResolver.js +289 -0
- package/dist/services/ArgumentTypeAnalyzer.d.ts +26 -0
- package/dist/services/ArgumentTypeAnalyzer.d.ts.map +1 -0
- package/dist/services/ArgumentTypeAnalyzer.js +92 -0
- package/dist/services/BlastRadiusAnalyzer.d.ts +15 -0
- package/dist/services/BlastRadiusAnalyzer.d.ts.map +1 -0
- package/dist/services/BlastRadiusAnalyzer.js +107 -0
- package/dist/services/ChainValidator.d.ts +76 -0
- package/dist/services/ChainValidator.d.ts.map +1 -0
- package/dist/services/ChainValidator.js +570 -0
- package/dist/services/CrossFileHealer.d.ts +6 -0
- package/dist/services/CrossFileHealer.d.ts.map +1 -0
- package/dist/services/CrossFileHealer.js +135 -0
- package/dist/services/DecisionEngine.d.ts +51 -0
- package/dist/services/DecisionEngine.d.ts.map +1 -0
- package/dist/services/DecisionEngine.js +260 -0
- package/dist/services/DefinitionTracer.d.ts +41 -0
- package/dist/services/DefinitionTracer.d.ts.map +1 -0
- package/dist/services/DefinitionTracer.js +366 -0
- package/dist/services/DnaEditorService.d.ts +31 -0
- package/dist/services/DnaEditorService.d.ts.map +1 -0
- package/dist/services/DnaEditorService.js +217 -0
- package/dist/services/DnaIndexService.d.ts +24 -0
- package/dist/services/DnaIndexService.d.ts.map +1 -0
- package/dist/services/DnaIndexService.js +131 -0
- package/dist/services/HealBudget.d.ts +32 -0
- package/dist/services/HealBudget.d.ts.map +1 -0
- package/dist/services/HealBudget.js +85 -0
- package/dist/services/HealReportService.d.ts +161 -0
- package/dist/services/HealReportService.d.ts.map +1 -0
- package/dist/services/HealReportService.js +1039 -0
- package/dist/services/HealingAdvisory.d.ts +30 -0
- package/dist/services/HealingAdvisory.d.ts.map +1 -0
- package/dist/services/HealingAdvisory.js +51 -0
- package/dist/services/HealingCacheService.d.ts +135 -0
- package/dist/services/HealingCacheService.d.ts.map +1 -0
- package/dist/services/HealingCacheService.js +404 -0
- package/dist/services/HealthReportService.d.ts +10 -0
- package/dist/services/HealthReportService.d.ts.map +1 -0
- package/dist/services/HealthReportService.js +84 -0
- package/dist/services/InitializerUpdater.d.ts +16 -0
- package/dist/services/InitializerUpdater.d.ts.map +1 -0
- package/dist/services/InitializerUpdater.js +73 -0
- package/dist/services/IntentAuditor.d.ts +56 -0
- package/dist/services/IntentAuditor.d.ts.map +1 -0
- package/dist/services/IntentAuditor.js +326 -0
- package/dist/services/InteractiveReview.d.ts +15 -0
- package/dist/services/InteractiveReview.d.ts.map +1 -0
- package/dist/services/InteractiveReview.js +128 -0
- package/dist/services/LLMService.d.ts +121 -0
- package/dist/services/LLMService.d.ts.map +1 -0
- package/dist/services/LLMService.js +475 -0
- package/dist/services/MutationApplier.d.ts +69 -0
- package/dist/services/MutationApplier.d.ts.map +1 -0
- package/dist/services/MutationApplier.js +360 -0
- package/dist/services/PRAutomationService.d.ts +50 -0
- package/dist/services/PRAutomationService.d.ts.map +1 -0
- package/dist/services/PRAutomationService.js +760 -0
- package/dist/services/PendingPromptLedger.d.ts +51 -0
- package/dist/services/PendingPromptLedger.d.ts.map +1 -0
- package/dist/services/PendingPromptLedger.js +205 -0
- package/dist/services/ReportGenerator.d.ts +156 -0
- package/dist/services/ReportGenerator.d.ts.map +1 -0
- package/dist/services/ReportGenerator.js +278 -0
- package/dist/services/ReportMergeService.d.ts +95 -0
- package/dist/services/ReportMergeService.d.ts.map +1 -0
- package/dist/services/ReportMergeService.js +0 -0
- package/dist/services/SafetyGuard.d.ts +112 -0
- package/dist/services/SafetyGuard.d.ts.map +1 -0
- package/dist/services/SafetyGuard.js +87 -0
- package/dist/services/Sanitizer.d.ts +28 -0
- package/dist/services/Sanitizer.d.ts.map +1 -0
- package/dist/services/Sanitizer.js +142 -0
- package/dist/services/SanitizingAnthropic.d.ts +19 -0
- package/dist/services/SanitizingAnthropic.d.ts.map +1 -0
- package/dist/services/SanitizingAnthropic.js +84 -0
- package/dist/services/SelectorSanitizer.d.ts +52 -0
- package/dist/services/SelectorSanitizer.d.ts.map +1 -0
- package/dist/services/SelectorSanitizer.js +318 -0
- package/dist/services/SnapshotService.d.ts +11 -0
- package/dist/services/SnapshotService.d.ts.map +1 -0
- package/dist/services/SnapshotService.js +352 -0
- package/dist/services/SourceLinkService.d.ts +26 -0
- package/dist/services/SourceLinkService.d.ts.map +1 -0
- package/dist/services/SourceLinkService.js +165 -0
- package/dist/services/SourceUpdater.d.ts +45 -0
- package/dist/services/SourceUpdater.d.ts.map +1 -0
- package/dist/services/SourceUpdater.js +1091 -0
- package/dist/services/TemplateDiffService.d.ts +25 -0
- package/dist/services/TemplateDiffService.d.ts.map +1 -0
- package/dist/services/TemplateDiffService.js +334 -0
- package/dist/services/TraceBackEngine.d.ts +67 -0
- package/dist/services/TraceBackEngine.d.ts.map +1 -0
- package/dist/services/TraceBackEngine.js +672 -0
- package/dist/services/WorkspaceSnapshotService.d.ts +71 -0
- package/dist/services/WorkspaceSnapshotService.d.ts.map +1 -0
- package/dist/services/WorkspaceSnapshotService.js +200 -0
- package/dist/services/safety/PipelineContext.d.ts +63 -0
- package/dist/services/safety/PipelineContext.d.ts.map +1 -0
- package/dist/services/safety/PipelineContext.js +34 -0
- package/dist/services/safety/roleClassifier.d.ts +4 -0
- package/dist/services/safety/roleClassifier.d.ts.map +1 -0
- package/dist/services/safety/roleClassifier.js +233 -0
- package/dist/services/safety/rules/ConfidenceTierRule.d.ts +3 -0
- package/dist/services/safety/rules/ConfidenceTierRule.d.ts.map +1 -0
- package/dist/services/safety/rules/ConfidenceTierRule.js +32 -0
- package/dist/services/safety/rules/DangerousSinkRule.d.ts +10 -0
- package/dist/services/safety/rules/DangerousSinkRule.d.ts.map +1 -0
- package/dist/services/safety/rules/DangerousSinkRule.js +53 -0
- package/dist/services/safety/rules/FunctionalRoleRule.d.ts +3 -0
- package/dist/services/safety/rules/FunctionalRoleRule.d.ts.map +1 -0
- package/dist/services/safety/rules/FunctionalRoleRule.js +64 -0
- package/dist/services/safety/rules/HardConfidenceRule.d.ts +3 -0
- package/dist/services/safety/rules/HardConfidenceRule.d.ts.map +1 -0
- package/dist/services/safety/rules/HardConfidenceRule.js +22 -0
- package/dist/services/safety/rules/IntentAuditorRule.d.ts +3 -0
- package/dist/services/safety/rules/IntentAuditorRule.d.ts.map +1 -0
- package/dist/services/safety/rules/IntentAuditorRule.js +97 -0
- package/dist/services/safety/rules/SemanticOppositeRule.d.ts +3 -0
- package/dist/services/safety/rules/SemanticOppositeRule.d.ts.map +1 -0
- package/dist/services/safety/rules/SemanticOppositeRule.js +30 -0
- package/dist/services/safety/rules/StructuralOnlyRule.d.ts +3 -0
- package/dist/services/safety/rules/StructuralOnlyRule.d.ts.map +1 -0
- package/dist/services/safety/rules/StructuralOnlyRule.js +38 -0
- package/dist/services/safety/rules/VisibilityRule.d.ts +3 -0
- package/dist/services/safety/rules/VisibilityRule.d.ts.map +1 -0
- package/dist/services/safety/rules/VisibilityRule.js +53 -0
- package/dist/services/safety/rules/ZeroTrustRule.d.ts +3 -0
- package/dist/services/safety/rules/ZeroTrustRule.d.ts.map +1 -0
- package/dist/services/safety/rules/ZeroTrustRule.js +37 -0
- package/dist/services/safety/rules/index.d.ts +3 -0
- package/dist/services/safety/rules/index.d.ts.map +1 -0
- package/dist/services/safety/rules/index.js +34 -0
- package/dist/services/types.d.ts +59 -0
- package/dist/services/types.d.ts.map +1 -0
- package/dist/services/types.js +2 -0
- package/dist/storage/SnapshotManager.d.ts +5 -0
- package/dist/storage/SnapshotManager.d.ts.map +1 -0
- package/dist/storage/SnapshotManager.js +32 -0
- package/dist/types/index.d.ts +95 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +7 -0
- package/dist/utils/DOMUtils.d.ts +49 -0
- package/dist/utils/DOMUtils.d.ts.map +1 -0
- package/dist/utils/DOMUtils.js +466 -0
- package/dist/utils/FileLock.d.ts +44 -0
- package/dist/utils/FileLock.d.ts.map +1 -0
- package/dist/utils/FileLock.js +189 -0
- package/dist/utils/IsolatedDiff.d.ts +45 -0
- package/dist/utils/IsolatedDiff.d.ts.map +1 -0
- package/dist/utils/IsolatedDiff.js +382 -0
- package/dist/utils/StackUtils.d.ts +11 -0
- package/dist/utils/StackUtils.d.ts.map +1 -0
- package/dist/utils/StackUtils.js +120 -0
- package/dist/utils/SummaryTable.d.ts +17 -0
- package/dist/utils/SummaryTable.d.ts.map +1 -0
- package/dist/utils/SummaryTable.js +37 -0
- package/dist/utils/atomicWrite.d.ts +9 -0
- package/dist/utils/atomicWrite.d.ts.map +1 -0
- package/dist/utils/atomicWrite.js +83 -0
- package/dist/utils/logger.d.ts +59 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +123 -0
- package/dist/vendor/enquirer.d.ts +33 -0
- package/dist/vendor/enquirer.d.ts.map +1 -0
- package/dist/vendor/enquirer.js +11 -0
- package/package.json +71 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FunctionalReturnAnalyzer = exports.DefinitionTracer = void 0;
|
|
4
|
+
const ts_morph_1 = require("ts-morph");
|
|
5
|
+
const logger_1 = require("../utils/logger");
|
|
6
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
7
|
+
// DefinitionTracer
|
|
8
|
+
//
|
|
9
|
+
// T-5: traceIdentifier - follow bare variable name to its initializer.
|
|
10
|
+
// T-6: tracePropertyAccess - follow OBJECT.prop to the property value.
|
|
11
|
+
//
|
|
12
|
+
// In-file first; signals crossFile when symbol comes from an import.
|
|
13
|
+
// Never does I/O beyond what ts-morph provides on the given SourceFile.
|
|
14
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
15
|
+
const MAX_TRACE_DEPTH = 5;
|
|
16
|
+
class DefinitionTracer {
|
|
17
|
+
// ─────────────────────────────────────────────────────────────────
|
|
18
|
+
// T-5: Identifier trace
|
|
19
|
+
// ─────────────────────────────────────────────────────────────────
|
|
20
|
+
static traceIdentifier(identifierNode, sourceFile, callerLine, depth = 0) {
|
|
21
|
+
if (depth >= MAX_TRACE_DEPTH) {
|
|
22
|
+
return {
|
|
23
|
+
found: false,
|
|
24
|
+
crossFile: false,
|
|
25
|
+
reason: "max trace depth reached",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const symbolName = identifierNode.getText().trim();
|
|
29
|
+
// 1. In-file: find closest VariableDeclaration before callerLine.
|
|
30
|
+
const decl = findVarDeclInFile(sourceFile, symbolName, callerLine);
|
|
31
|
+
if (decl) {
|
|
32
|
+
const init = decl.getInitializer();
|
|
33
|
+
if (!init) {
|
|
34
|
+
return {
|
|
35
|
+
found: false,
|
|
36
|
+
crossFile: false,
|
|
37
|
+
reason: `"${symbolName}" has no initializer`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// Recursive follow: if init is another identifier, trace again.
|
|
41
|
+
if (ts_morph_1.Node.isIdentifier(init)) {
|
|
42
|
+
return DefinitionTracer.traceIdentifier(init, sourceFile, decl.getStartLineNumber(), depth + 1);
|
|
43
|
+
}
|
|
44
|
+
logger_1.logger.debug(`traceIdentifier: "${symbolName}" → ` +
|
|
45
|
+
`${init.getKindName()} @ ${sourceFile.getBaseName()}:${init.getStartLineNumber()}`);
|
|
46
|
+
return {
|
|
47
|
+
found: true,
|
|
48
|
+
targetNode: init,
|
|
49
|
+
targetFile: sourceFile,
|
|
50
|
+
declarationNode: decl,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
// 2. Symbol not in file - check import declarations.
|
|
54
|
+
const imported = findImportedSymbol(sourceFile, symbolName);
|
|
55
|
+
if (imported) {
|
|
56
|
+
logger_1.logger.debug(`traceIdentifier: "${symbolName}" comes from "${imported.modulePath}" (cross-file)`);
|
|
57
|
+
return { found: false, crossFile: true, ...imported };
|
|
58
|
+
}
|
|
59
|
+
// 3. Last resort: ts-morph language-service definition lookup.
|
|
60
|
+
try {
|
|
61
|
+
const defNodes = identifierNode.getDefinitionNodes?.();
|
|
62
|
+
if (defNodes && defNodes.length > 0) {
|
|
63
|
+
for (const def of defNodes) {
|
|
64
|
+
if (ts_morph_1.Node.isVariableDeclaration(def)) {
|
|
65
|
+
const init = def.getInitializer();
|
|
66
|
+
if (init) {
|
|
67
|
+
logger_1.logger.debug(`traceIdentifier (via LangSvc): "${symbolName}" → ` +
|
|
68
|
+
`${init.getKindName()} @ ${def.getSourceFile().getBaseName()}:${init.getStartLineNumber()}`);
|
|
69
|
+
return {
|
|
70
|
+
found: true,
|
|
71
|
+
targetNode: init,
|
|
72
|
+
targetFile: def.getSourceFile(),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Language service unavailable - not an error.
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
found: false,
|
|
84
|
+
crossFile: false,
|
|
85
|
+
reason: `"${symbolName}" not resolved in file or imports`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// ─────────────────────────────────────────────────────────────────
|
|
89
|
+
// T-6: PropertyAccessExpression trace
|
|
90
|
+
// ─────────────────────────────────────────────────────────────────
|
|
91
|
+
static tracePropertyAccess(propAccessNode, sourceFile, callerLine) {
|
|
92
|
+
if (!ts_morph_1.Node.isPropertyAccessExpression(propAccessNode)) {
|
|
93
|
+
return {
|
|
94
|
+
found: false,
|
|
95
|
+
crossFile: false,
|
|
96
|
+
reason: "node is not a PropertyAccessExpression",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const expr = propAccessNode;
|
|
100
|
+
const objectName = expr.getExpression().getText().trim();
|
|
101
|
+
const propName = expr.getName();
|
|
102
|
+
// Find the object's VariableDeclaration.
|
|
103
|
+
const objDecl = findVarDeclInFile(sourceFile, objectName, callerLine);
|
|
104
|
+
if (!objDecl) {
|
|
105
|
+
const imported = findImportedSymbol(sourceFile, objectName);
|
|
106
|
+
if (imported) {
|
|
107
|
+
logger_1.logger.debug(`tracePropertyAccess: "${objectName}" comes from "${imported.modulePath}" (cross-file)`);
|
|
108
|
+
return { found: false, crossFile: true, ...imported };
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
found: false,
|
|
112
|
+
crossFile: false,
|
|
113
|
+
reason: `object "${objectName}" not found in file or imports`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const init = objDecl.getInitializer();
|
|
117
|
+
if (!init || !ts_morph_1.Node.isObjectLiteralExpression(init)) {
|
|
118
|
+
return {
|
|
119
|
+
found: false,
|
|
120
|
+
crossFile: false,
|
|
121
|
+
reason: `"${objectName}" initializer is not an ObjectLiteralExpression (got ${init?.getKindName() ?? "none"})`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
// ts-morph getProperty(name) - returns the first matching property.
|
|
125
|
+
const prop = init.getProperty(propName);
|
|
126
|
+
if (!prop || !ts_morph_1.Node.isPropertyAssignment(prop)) {
|
|
127
|
+
return {
|
|
128
|
+
found: false,
|
|
129
|
+
crossFile: false,
|
|
130
|
+
reason: `property "${propName}" not found on "${objectName}"`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const valueNode = prop.getInitializer();
|
|
134
|
+
if (!valueNode) {
|
|
135
|
+
return {
|
|
136
|
+
found: false,
|
|
137
|
+
crossFile: false,
|
|
138
|
+
reason: `property "${propName}" on "${objectName}" has no initializer`,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
logger_1.logger.debug(`tracePropertyAccess: "${objectName}.${propName}" → ` +
|
|
142
|
+
`${valueNode.getKindName()} "${valueNode.getText()}" ` +
|
|
143
|
+
`@ ${sourceFile.getBaseName()}:${valueNode.getStartLineNumber()}`);
|
|
144
|
+
return {
|
|
145
|
+
found: true,
|
|
146
|
+
targetNode: valueNode,
|
|
147
|
+
targetFile: sourceFile,
|
|
148
|
+
declarationNode: prop,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
// ─────────────────────────────────────────────────────────────────
|
|
152
|
+
// traceByName - used by CrossFileHealer
|
|
153
|
+
//
|
|
154
|
+
// Like traceIdentifier but takes a plain symbol name string instead
|
|
155
|
+
// of an Identifier node. Searches the entire file (no callerLine
|
|
156
|
+
// constraint) so it's suitable for resolving exported symbols.
|
|
157
|
+
// ─────────────────────────────────────────────────────────────────
|
|
158
|
+
static traceByName(symbolName, sourceFile) {
|
|
159
|
+
const decl = findVarDeclInFile(sourceFile, symbolName, Number.MAX_SAFE_INTEGER);
|
|
160
|
+
if (!decl) {
|
|
161
|
+
const imported = findImportedSymbol(sourceFile, symbolName);
|
|
162
|
+
if (imported)
|
|
163
|
+
return { found: false, crossFile: true, ...imported };
|
|
164
|
+
return {
|
|
165
|
+
found: false,
|
|
166
|
+
crossFile: false,
|
|
167
|
+
reason: `"${symbolName}" not found in ${sourceFile.getBaseName()}`,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const init = decl.getInitializer();
|
|
171
|
+
if (!init) {
|
|
172
|
+
return {
|
|
173
|
+
found: false,
|
|
174
|
+
crossFile: false,
|
|
175
|
+
reason: `"${symbolName}" has no initializer`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
logger_1.logger.debug(`traceByName: "${symbolName}" → ` +
|
|
179
|
+
`${init.getKindName()} @ ${sourceFile.getBaseName()}:${init.getStartLineNumber()}`);
|
|
180
|
+
return {
|
|
181
|
+
found: true,
|
|
182
|
+
targetNode: init,
|
|
183
|
+
targetFile: sourceFile,
|
|
184
|
+
declarationNode: decl,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
exports.DefinitionTracer = DefinitionTracer;
|
|
189
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
190
|
+
// FunctionalReturnAnalyzer
|
|
191
|
+
//
|
|
192
|
+
// T-7: Given a CallExpression whose callee is a local function,
|
|
193
|
+
// enter the function body and return its return expression + arg bindings.
|
|
194
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
195
|
+
class FunctionalReturnAnalyzer {
|
|
196
|
+
static analyze(callNode, sourceFile) {
|
|
197
|
+
if (!ts_morph_1.Node.isCallExpression(callNode)) {
|
|
198
|
+
return { kind: "complex", reason: "node is not a CallExpression" };
|
|
199
|
+
}
|
|
200
|
+
const call = callNode;
|
|
201
|
+
const calleeExpr = call.getExpression();
|
|
202
|
+
// Only handle simple identifier callees: getScopedInput(role).
|
|
203
|
+
// PropertyAccess callees (obj.method()) are out of scope for Phase 2.
|
|
204
|
+
if (!ts_morph_1.Node.isIdentifier(calleeExpr)) {
|
|
205
|
+
return {
|
|
206
|
+
kind: "complex",
|
|
207
|
+
reason: `callee is ${calleeExpr.getKindName()}, not a simple identifier`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const calleeName = calleeExpr.getText();
|
|
211
|
+
const fnResult = findFunctionBody(sourceFile, calleeName);
|
|
212
|
+
if (!fnResult) {
|
|
213
|
+
return {
|
|
214
|
+
kind: "complex",
|
|
215
|
+
reason: `function "${calleeName}" not found in file`,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const { returnExpr, params, line } = fnResult;
|
|
219
|
+
// Build argument bindings: paramName → runtime value text.
|
|
220
|
+
const callArgs = call.getArguments();
|
|
221
|
+
const bindings = new Map();
|
|
222
|
+
for (let i = 0; i < params.length; i++) {
|
|
223
|
+
const argNode = callArgs[i];
|
|
224
|
+
if (!argNode)
|
|
225
|
+
continue;
|
|
226
|
+
const raw = argNode.getText();
|
|
227
|
+
// Strip surrounding quotes for string literals so bindings hold the value, not the syntax.
|
|
228
|
+
const value = ts_morph_1.Node.isStringLiteral(argNode)
|
|
229
|
+
? argNode.getLiteralValue()
|
|
230
|
+
: raw.replace(/^["'`]|["'`]$/g, "");
|
|
231
|
+
bindings.set(params[i], value);
|
|
232
|
+
}
|
|
233
|
+
if (ts_morph_1.Node.isStringLiteral(returnExpr) ||
|
|
234
|
+
ts_morph_1.Node.isNoSubstitutionTemplateLiteral(returnExpr)) {
|
|
235
|
+
logger_1.logger.debug(`"${calleeName}" returns StringLiteral ` +
|
|
236
|
+
`"${returnExpr.getLiteralValue()}" @ line ${line}`);
|
|
237
|
+
return {
|
|
238
|
+
kind: "literal",
|
|
239
|
+
literalNode: returnExpr,
|
|
240
|
+
functionFile: sourceFile,
|
|
241
|
+
functionLine: line,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
if (ts_morph_1.Node.isTemplateExpression(returnExpr)) {
|
|
245
|
+
const head = returnExpr
|
|
246
|
+
.getHead()
|
|
247
|
+
.getLiteralText();
|
|
248
|
+
logger_1.logger.debug(`"${calleeName}" returns TemplateExpression ` +
|
|
249
|
+
`(head: "${head}") @ line ${line}. Bindings: ${JSON.stringify(Object.fromEntries(bindings))}`);
|
|
250
|
+
return {
|
|
251
|
+
kind: "template",
|
|
252
|
+
templateNode: returnExpr,
|
|
253
|
+
bindings,
|
|
254
|
+
functionFile: sourceFile,
|
|
255
|
+
functionLine: line,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
kind: "complex",
|
|
260
|
+
reason: `"${calleeName}" return expression is ${returnExpr.getKindName()} - not directly healable`,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
exports.FunctionalReturnAnalyzer = FunctionalReturnAnalyzer;
|
|
265
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
266
|
+
// Private helpers
|
|
267
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
268
|
+
function findVarDeclInFile(sourceFile, varName, beforeLine) {
|
|
269
|
+
const text = sourceFile.getFullText();
|
|
270
|
+
const lines = text.split("\n");
|
|
271
|
+
let beforeOffset = 0;
|
|
272
|
+
for (let i = 0; i < Math.min(beforeLine - 1, lines.length); i++) {
|
|
273
|
+
beforeOffset += lines[i].length + 1;
|
|
274
|
+
}
|
|
275
|
+
let best = null;
|
|
276
|
+
let bestOffset = -1;
|
|
277
|
+
for (const decl of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.VariableDeclaration)) {
|
|
278
|
+
if (decl.getName() !== varName)
|
|
279
|
+
continue;
|
|
280
|
+
const offset = decl.getStart();
|
|
281
|
+
if (offset < beforeOffset && offset > bestOffset) {
|
|
282
|
+
best = decl;
|
|
283
|
+
bestOffset = offset;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return best;
|
|
287
|
+
}
|
|
288
|
+
function findImportedSymbol(sourceFile, symbolName) {
|
|
289
|
+
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
290
|
+
const modulePath = importDecl.getModuleSpecifierValue();
|
|
291
|
+
// Default import: import Foo from '...'
|
|
292
|
+
const defaultImport = importDecl.getDefaultImport();
|
|
293
|
+
if (defaultImport?.getText() === symbolName) {
|
|
294
|
+
return { modulePath, symbolName };
|
|
295
|
+
}
|
|
296
|
+
// Named imports: import { Foo, Bar as B } from '...'
|
|
297
|
+
for (const named of importDecl.getNamedImports()) {
|
|
298
|
+
const localName = named.getAliasNode()?.getText() ?? named.getName();
|
|
299
|
+
if (localName === symbolName) {
|
|
300
|
+
return { modulePath, symbolName: named.getName() };
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// Namespace import: import * as NS from '...'
|
|
304
|
+
const nsImport = importDecl.getNamespaceImport();
|
|
305
|
+
if (nsImport?.getText() === symbolName) {
|
|
306
|
+
return { modulePath, symbolName };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
function findFunctionBody(sourceFile, fnName) {
|
|
312
|
+
// 1. FunctionDeclaration: function getScopedInput(...) { ... }
|
|
313
|
+
const fnDecl = sourceFile.getFunction(fnName);
|
|
314
|
+
if (fnDecl) {
|
|
315
|
+
const params = fnDecl.getParameters().map((p) => p.getName());
|
|
316
|
+
const body = fnDecl.getBody();
|
|
317
|
+
if (!body)
|
|
318
|
+
return null;
|
|
319
|
+
const returnExpr = extractReturnExpression(body);
|
|
320
|
+
if (!returnExpr)
|
|
321
|
+
return null;
|
|
322
|
+
return { returnExpr, params, line: fnDecl.getStartLineNumber() };
|
|
323
|
+
}
|
|
324
|
+
// 2. const fnName = (...) => expr or const fnName = function(...) { ... }
|
|
325
|
+
for (const decl of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.VariableDeclaration)) {
|
|
326
|
+
if (decl.getName() !== fnName)
|
|
327
|
+
continue;
|
|
328
|
+
const init = decl.getInitializer();
|
|
329
|
+
if (!init)
|
|
330
|
+
continue;
|
|
331
|
+
if (ts_morph_1.Node.isArrowFunction(init)) {
|
|
332
|
+
const arrow = init;
|
|
333
|
+
const params = arrow.getParameters().map((p) => p.getName());
|
|
334
|
+
const body = arrow.getBody();
|
|
335
|
+
// Concise body: (role) => `...` - body IS the return expression.
|
|
336
|
+
if (!ts_morph_1.Node.isBlock(body)) {
|
|
337
|
+
return { returnExpr: body, params, line: decl.getStartLineNumber() };
|
|
338
|
+
}
|
|
339
|
+
const returnExpr = extractReturnExpression(body);
|
|
340
|
+
if (!returnExpr)
|
|
341
|
+
return null;
|
|
342
|
+
return { returnExpr, params, line: decl.getStartLineNumber() };
|
|
343
|
+
}
|
|
344
|
+
if (ts_morph_1.Node.isFunctionExpression(init)) {
|
|
345
|
+
const fnExpr = init;
|
|
346
|
+
const params = fnExpr.getParameters().map((p) => p.getName());
|
|
347
|
+
const body = fnExpr.getBody();
|
|
348
|
+
const returnExpr = extractReturnExpression(body);
|
|
349
|
+
if (!returnExpr)
|
|
350
|
+
return null;
|
|
351
|
+
return { returnExpr, params, line: decl.getStartLineNumber() };
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
function extractReturnExpression(body) {
|
|
357
|
+
// Concise arrow body - the node itself is the return value.
|
|
358
|
+
if (!ts_morph_1.Node.isBlock(body))
|
|
359
|
+
return body;
|
|
360
|
+
// Block body: search for ReturnStatement.
|
|
361
|
+
const returnStmts = body.getDescendantsOfKind(ts_morph_1.SyntaxKind.ReturnStatement);
|
|
362
|
+
if (returnStmts.length === 0)
|
|
363
|
+
return null;
|
|
364
|
+
// Use the first one - for conditional returns we signal COMPLEX upstream.
|
|
365
|
+
return returnStmts[0].getExpression() ?? null;
|
|
366
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ElementSnapshotV2 } from "../types/index.js";
|
|
2
|
+
export interface ValidationResult {
|
|
3
|
+
valid: boolean;
|
|
4
|
+
errors: string[];
|
|
5
|
+
}
|
|
6
|
+
export interface DnaSnapshotExtended extends ElementSnapshotV2 {
|
|
7
|
+
selector: string;
|
|
8
|
+
testTitle?: string;
|
|
9
|
+
sourceFile?: string;
|
|
10
|
+
sourceLine?: number;
|
|
11
|
+
lastHealed?: string | null;
|
|
12
|
+
status?: "healed" | "broken";
|
|
13
|
+
originalSelector?: string;
|
|
14
|
+
healedSelector?: string;
|
|
15
|
+
}
|
|
16
|
+
export type DnaPatch = Partial<DnaSnapshotExtended>;
|
|
17
|
+
export interface IDnaEditorService {
|
|
18
|
+
read(key: string): Promise<DnaSnapshotExtended>;
|
|
19
|
+
write(key: string, patch: DnaPatch): Promise<string>;
|
|
20
|
+
rollback(key: string): Promise<void>;
|
|
21
|
+
validate(candidate: unknown): ValidationResult;
|
|
22
|
+
}
|
|
23
|
+
export declare class DnaEditorService implements IDnaEditorService {
|
|
24
|
+
private readonly snapshotDir;
|
|
25
|
+
constructor(snapshotDir: string);
|
|
26
|
+
read(key: string): Promise<DnaSnapshotExtended>;
|
|
27
|
+
write(key: string, patch: DnaPatch): Promise<string>;
|
|
28
|
+
rollback(key: string): Promise<void>;
|
|
29
|
+
validate(candidate: unknown): ValidationResult;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=DnaEditorService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DnaEditorService.d.ts","sourceRoot":"","sources":["../../src/services/DnaEditorService.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAE3D,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAID,MAAM,WAAW,mBAAoB,SAAQ,iBAAiB;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC7B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;AAEpD,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAChD,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,QAAQ,CAAC,SAAS,EAAE,OAAO,GAAG,gBAAgB,CAAC;CAChD;AA6BD,qBAAa,gBAAiB,YAAW,iBAAiB;IAC5C,OAAO,CAAC,QAAQ,CAAC,WAAW;gBAAX,WAAW,EAAE,MAAM;IAE1C,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAgB/C,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC;IA2CpD,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAa1C,QAAQ,CAAC,SAAS,EAAE,OAAO,GAAG,gBAAgB;CA4H/C"}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DnaEditorService = void 0;
|
|
7
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
function jsonPath(dir, key) {
|
|
10
|
+
return path_1.default.join(dir, `${key}.json`);
|
|
11
|
+
}
|
|
12
|
+
function tmpPath(dir, key) {
|
|
13
|
+
return path_1.default.join(dir, `${key}.json.tmp`);
|
|
14
|
+
}
|
|
15
|
+
function bakPath(dir, key) {
|
|
16
|
+
return path_1.default.join(dir, `${key}.json.bak`);
|
|
17
|
+
}
|
|
18
|
+
// Derive a best-effort selector from engine snapshot fields when none is stored.
|
|
19
|
+
function deriveSelector(snap) {
|
|
20
|
+
const id = snap["id"];
|
|
21
|
+
const classes = snap["classes"];
|
|
22
|
+
const tagName = snap["tagName"];
|
|
23
|
+
if (typeof id === "string" && id)
|
|
24
|
+
return `#${id}`;
|
|
25
|
+
if (Array.isArray(classes) && classes.length > 0)
|
|
26
|
+
return `.${classes.join(".")}`;
|
|
27
|
+
return typeof tagName === "string" ? tagName : "unknown";
|
|
28
|
+
}
|
|
29
|
+
function dnaError(message, code) {
|
|
30
|
+
const err = new Error(message);
|
|
31
|
+
err.code = code;
|
|
32
|
+
return err;
|
|
33
|
+
}
|
|
34
|
+
class DnaEditorService {
|
|
35
|
+
snapshotDir;
|
|
36
|
+
constructor(snapshotDir) {
|
|
37
|
+
this.snapshotDir = snapshotDir;
|
|
38
|
+
}
|
|
39
|
+
async read(key) {
|
|
40
|
+
const target = jsonPath(this.snapshotDir, key);
|
|
41
|
+
let raw;
|
|
42
|
+
try {
|
|
43
|
+
raw = await promises_1.default.readFile(target, "utf-8");
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
throw dnaError(`DNA not found: ${key}`, "DNA_NOT_FOUND");
|
|
47
|
+
}
|
|
48
|
+
const parsed = JSON.parse(raw);
|
|
49
|
+
// Promote to extended format if selector not stored
|
|
50
|
+
if (!parsed["selector"]) {
|
|
51
|
+
parsed["selector"] = deriveSelector(parsed);
|
|
52
|
+
}
|
|
53
|
+
return parsed;
|
|
54
|
+
}
|
|
55
|
+
async write(key, patch) {
|
|
56
|
+
const target = jsonPath(this.snapshotDir, key);
|
|
57
|
+
const bak = bakPath(this.snapshotDir, key);
|
|
58
|
+
const tmp = tmpPath(this.snapshotDir, key);
|
|
59
|
+
// 1. Read existing snapshot
|
|
60
|
+
const original = await this.read(key);
|
|
61
|
+
// 2. Shallow-merge patch (arrays and objects replaced, not deep-merged)
|
|
62
|
+
const merged = {
|
|
63
|
+
...original,
|
|
64
|
+
...patch,
|
|
65
|
+
};
|
|
66
|
+
// Any CLI write = human has reviewed the DNA → clear auto-heal status flag.
|
|
67
|
+
delete merged["status"];
|
|
68
|
+
// 3. Derive selector only when absent - never override an explicit (possibly invalid) value
|
|
69
|
+
if (merged["selector"] == null) {
|
|
70
|
+
merged["selector"] = deriveSelector(merged);
|
|
71
|
+
}
|
|
72
|
+
// 4. Validate before touching disk
|
|
73
|
+
const validation = this.validate(merged);
|
|
74
|
+
if (!validation.valid) {
|
|
75
|
+
throw dnaError(`Validation failed for ${key}: ${validation.errors.join("; ")}`, "VALIDATION_FAILED");
|
|
76
|
+
}
|
|
77
|
+
// 5. Backup current file (overwrite previous bak)
|
|
78
|
+
await promises_1.default.writeFile(bak, JSON.stringify(original, null, 2), "utf-8");
|
|
79
|
+
// 6. Write to tmp
|
|
80
|
+
await promises_1.default.writeFile(tmp, JSON.stringify(merged, null, 2), "utf-8");
|
|
81
|
+
// 7. Atomic rename tmp → json
|
|
82
|
+
await promises_1.default.rename(tmp, target);
|
|
83
|
+
return bak;
|
|
84
|
+
}
|
|
85
|
+
async rollback(key) {
|
|
86
|
+
const target = jsonPath(this.snapshotDir, key);
|
|
87
|
+
const bak = bakPath(this.snapshotDir, key);
|
|
88
|
+
try {
|
|
89
|
+
await promises_1.default.access(bak);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
throw dnaError(`No backup found for: ${key}`, "DNA_NOT_FOUND");
|
|
93
|
+
}
|
|
94
|
+
await promises_1.default.rename(bak, target);
|
|
95
|
+
}
|
|
96
|
+
validate(candidate) {
|
|
97
|
+
const errors = [];
|
|
98
|
+
if (typeof candidate !== "object" ||
|
|
99
|
+
candidate === null ||
|
|
100
|
+
Array.isArray(candidate)) {
|
|
101
|
+
return { valid: false, errors: ["candidate must be a plain object"] };
|
|
102
|
+
}
|
|
103
|
+
const snap = candidate;
|
|
104
|
+
// ── Required top-level fields ─────────────────────────────────────────────
|
|
105
|
+
for (const field of [
|
|
106
|
+
"tagName",
|
|
107
|
+
"selector",
|
|
108
|
+
"schemaVersion",
|
|
109
|
+
"timestamp",
|
|
110
|
+
]) {
|
|
111
|
+
if (!(field in snap) || snap[field] === undefined) {
|
|
112
|
+
errors.push(`Missing required field: ${field}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (snap["schemaVersion"] !== 2) {
|
|
116
|
+
errors.push(`schemaVersion must be 2, got ${String(snap["schemaVersion"])}`);
|
|
117
|
+
}
|
|
118
|
+
// ── selector ─────────────────────────────────────────────────────────────
|
|
119
|
+
const sel = snap["selector"];
|
|
120
|
+
if (typeof sel === "string") {
|
|
121
|
+
if (sel.trim() === "")
|
|
122
|
+
errors.push("selector must not be empty");
|
|
123
|
+
if (sel === "undefined")
|
|
124
|
+
errors.push('selector must not be the string "undefined"');
|
|
125
|
+
if (/[\r\n]/.test(sel))
|
|
126
|
+
errors.push("selector must not contain raw newlines");
|
|
127
|
+
}
|
|
128
|
+
else if ("selector" in snap) {
|
|
129
|
+
errors.push("selector must be a string");
|
|
130
|
+
}
|
|
131
|
+
// ── tagName ───────────────────────────────────────────────────────────────
|
|
132
|
+
const tag = snap["tagName"];
|
|
133
|
+
if (typeof tag === "string") {
|
|
134
|
+
if (!/^[a-z][a-z0-9-]*$/.test(tag)) {
|
|
135
|
+
errors.push("tagName must match /^[a-z][a-z0-9-]*$/");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else if ("tagName" in snap) {
|
|
139
|
+
errors.push("tagName must be a string");
|
|
140
|
+
}
|
|
141
|
+
// ── text ─────────────────────────────────────────────────────────────────
|
|
142
|
+
const text = snap["text"];
|
|
143
|
+
if (text !== null && text !== undefined) {
|
|
144
|
+
if (typeof text !== "string") {
|
|
145
|
+
errors.push("text must be a string or null");
|
|
146
|
+
}
|
|
147
|
+
else if (text.length >= 2000) {
|
|
148
|
+
errors.push("text must be fewer than 2000 characters");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// ── attributes ───────────────────────────────────────────────────────────
|
|
152
|
+
const attrs = snap["attributes"];
|
|
153
|
+
if (attrs !== undefined) {
|
|
154
|
+
if (typeof attrs !== "object" || attrs === null || Array.isArray(attrs)) {
|
|
155
|
+
errors.push("attributes must be a plain object");
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
159
|
+
if (!k)
|
|
160
|
+
errors.push("attributes keys must not be empty");
|
|
161
|
+
if (typeof v !== "string")
|
|
162
|
+
errors.push(`attributes["${k}"] must be a string`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// ── visibility (all 4 keys required) ─────────────────────────────────────
|
|
167
|
+
const vis = snap["visibility"];
|
|
168
|
+
if (typeof vis === "object" && vis !== null && !Array.isArray(vis)) {
|
|
169
|
+
const visObj = vis;
|
|
170
|
+
for (const key of [
|
|
171
|
+
"isGhost",
|
|
172
|
+
"ghostReason",
|
|
173
|
+
"isOccluded",
|
|
174
|
+
"opacity",
|
|
175
|
+
]) {
|
|
176
|
+
if (!(key in visObj))
|
|
177
|
+
errors.push(`visibility.${key} is required`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
errors.push("visibility block is required and must be an object");
|
|
182
|
+
}
|
|
183
|
+
// ── anchors (all 3 keys required) ────────────────────────────────────────
|
|
184
|
+
const anch = snap["anchors"];
|
|
185
|
+
if (typeof anch === "object" && anch !== null && !Array.isArray(anch)) {
|
|
186
|
+
const anchObj = anch;
|
|
187
|
+
for (const key of [
|
|
188
|
+
"closestLabel",
|
|
189
|
+
"rowAnchor",
|
|
190
|
+
"neighborTexts",
|
|
191
|
+
]) {
|
|
192
|
+
if (!(key in anchObj))
|
|
193
|
+
errors.push(`anchors.${key} is required`);
|
|
194
|
+
}
|
|
195
|
+
const cl = anchObj["closestLabel"];
|
|
196
|
+
if (cl !== null && typeof cl !== "string") {
|
|
197
|
+
errors.push("anchors.closestLabel must be a string or null");
|
|
198
|
+
}
|
|
199
|
+
const nt = anchObj["neighborTexts"];
|
|
200
|
+
if (!Array.isArray(nt)) {
|
|
201
|
+
errors.push("anchors.neighborTexts must be an array");
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
if (nt.length > 10)
|
|
205
|
+
errors.push("anchors.neighborTexts must have at most 10 elements");
|
|
206
|
+
if (!nt.every((x) => typeof x === "string")) {
|
|
207
|
+
errors.push("anchors.neighborTexts elements must all be strings");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
errors.push("anchors block is required and must be an object");
|
|
213
|
+
}
|
|
214
|
+
return { valid: errors.length === 0, errors };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
exports.DnaEditorService = DnaEditorService;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { DnaRecord, DnaIndex } from "./types.js";
|
|
2
|
+
export interface IDnaIndexService {
|
|
3
|
+
buildIndex(snapshotDir: string): Promise<DnaIndex>;
|
|
4
|
+
findBySelector(pattern: string): DnaRecord[];
|
|
5
|
+
findByText(pattern: string): DnaRecord[];
|
|
6
|
+
findBySourceFile(filePath: string): DnaRecord[];
|
|
7
|
+
findByTestTitle(pattern: string): DnaRecord[];
|
|
8
|
+
getByKey(key: string): DnaRecord | null;
|
|
9
|
+
getIndex(): DnaIndex;
|
|
10
|
+
refresh(): void;
|
|
11
|
+
}
|
|
12
|
+
export declare class DnaIndexService implements IDnaIndexService {
|
|
13
|
+
private cachedIndex;
|
|
14
|
+
buildIndex(snapshotDir: string): Promise<DnaIndex>;
|
|
15
|
+
findBySelector(pattern: string): DnaRecord[];
|
|
16
|
+
findByText(pattern: string): DnaRecord[];
|
|
17
|
+
findBySourceFile(filePath: string): DnaRecord[];
|
|
18
|
+
findByTestTitle(pattern: string): DnaRecord[];
|
|
19
|
+
getByKey(key: string): DnaRecord | null;
|
|
20
|
+
getIndex(): DnaIndex;
|
|
21
|
+
refresh(): void;
|
|
22
|
+
private requireIndex;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=DnaIndexService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DnaIndexService.d.ts","sourceRoot":"","sources":["../../src/services/DnaIndexService.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAatD,MAAM,WAAW,gBAAgB;IAC/B,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACnD,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,CAAC;IAC7C,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,CAAC;IACzC,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,CAAC;IAChD,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,CAAC;IAC9C,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAC;IACxC,QAAQ,IAAI,QAAQ,CAAC;IACrB,OAAO,IAAI,IAAI,CAAC;CACjB;AAkED,qBAAa,eAAgB,YAAW,gBAAgB;IACtD,OAAO,CAAC,WAAW,CAAyB;IAEtC,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAoCxD,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE;IAK5C,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE;IAOxC,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE;IAO/C,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE;IAK7C,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAIvC,QAAQ,IAAI,QAAQ;IAIpB,OAAO,IAAI,IAAI;IAIf,OAAO,CAAC,YAAY;CAQrB"}
|