@lumenflow/core 1.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 +190 -0
- package/README.md +119 -0
- package/dist/active-wu-detector.d.ts +33 -0
- package/dist/active-wu-detector.js +106 -0
- package/dist/adapters/filesystem-metrics.adapter.d.ts +108 -0
- package/dist/adapters/filesystem-metrics.adapter.js +519 -0
- package/dist/adapters/terminal-renderer.adapter.d.ts +106 -0
- package/dist/adapters/terminal-renderer.adapter.js +337 -0
- package/dist/arg-parser.d.ts +63 -0
- package/dist/arg-parser.js +560 -0
- package/dist/backlog-editor.d.ts +98 -0
- package/dist/backlog-editor.js +179 -0
- package/dist/backlog-generator.d.ts +111 -0
- package/dist/backlog-generator.js +381 -0
- package/dist/backlog-parser.d.ts +45 -0
- package/dist/backlog-parser.js +102 -0
- package/dist/backlog-sync-validator.d.ts +78 -0
- package/dist/backlog-sync-validator.js +294 -0
- package/dist/branch-drift.d.ts +34 -0
- package/dist/branch-drift.js +51 -0
- package/dist/cleanup-install-config.d.ts +33 -0
- package/dist/cleanup-install-config.js +37 -0
- package/dist/cleanup-lock.d.ts +139 -0
- package/dist/cleanup-lock.js +313 -0
- package/dist/code-path-validator.d.ts +146 -0
- package/dist/code-path-validator.js +537 -0
- package/dist/code-paths-overlap.d.ts +55 -0
- package/dist/code-paths-overlap.js +245 -0
- package/dist/commands-logger.d.ts +77 -0
- package/dist/commands-logger.js +254 -0
- package/dist/commit-message-utils.d.ts +25 -0
- package/dist/commit-message-utils.js +41 -0
- package/dist/compliance-parser.d.ts +150 -0
- package/dist/compliance-parser.js +507 -0
- package/dist/constants/backlog-patterns.d.ts +20 -0
- package/dist/constants/backlog-patterns.js +23 -0
- package/dist/constants/dora-constants.d.ts +49 -0
- package/dist/constants/dora-constants.js +53 -0
- package/dist/constants/gate-constants.d.ts +15 -0
- package/dist/constants/gate-constants.js +15 -0
- package/dist/constants/linter-constants.d.ts +16 -0
- package/dist/constants/linter-constants.js +16 -0
- package/dist/constants/tokenizer-constants.d.ts +15 -0
- package/dist/constants/tokenizer-constants.js +15 -0
- package/dist/core/scope-checker.d.ts +97 -0
- package/dist/core/scope-checker.js +163 -0
- package/dist/core/tool-runner.d.ts +161 -0
- package/dist/core/tool-runner.js +393 -0
- package/dist/core/tool.constants.d.ts +105 -0
- package/dist/core/tool.constants.js +101 -0
- package/dist/core/tool.schemas.d.ts +226 -0
- package/dist/core/tool.schemas.js +226 -0
- package/dist/core/worktree-guard.d.ts +130 -0
- package/dist/core/worktree-guard.js +242 -0
- package/dist/coverage-gate.d.ts +108 -0
- package/dist/coverage-gate.js +196 -0
- package/dist/date-utils.d.ts +75 -0
- package/dist/date-utils.js +140 -0
- package/dist/dependency-graph.d.ts +142 -0
- package/dist/dependency-graph.js +550 -0
- package/dist/dependency-guard.d.ts +54 -0
- package/dist/dependency-guard.js +142 -0
- package/dist/dependency-validator.d.ts +105 -0
- package/dist/dependency-validator.js +154 -0
- package/dist/docs-path-validator.d.ts +36 -0
- package/dist/docs-path-validator.js +95 -0
- package/dist/domain/orchestration.constants.d.ts +99 -0
- package/dist/domain/orchestration.constants.js +97 -0
- package/dist/domain/orchestration.schemas.d.ts +280 -0
- package/dist/domain/orchestration.schemas.js +211 -0
- package/dist/domain/orchestration.types.d.ts +133 -0
- package/dist/domain/orchestration.types.js +12 -0
- package/dist/error-handler.d.ts +116 -0
- package/dist/error-handler.js +136 -0
- package/dist/file-classifiers.d.ts +62 -0
- package/dist/file-classifiers.js +108 -0
- package/dist/gates-agent-mode.d.ts +81 -0
- package/dist/gates-agent-mode.js +94 -0
- package/dist/generate-traceability.d.ts +107 -0
- package/dist/generate-traceability.js +411 -0
- package/dist/git-adapter.d.ts +395 -0
- package/dist/git-adapter.js +649 -0
- package/dist/git-staged-validator.d.ts +32 -0
- package/dist/git-staged-validator.js +48 -0
- package/dist/hardcoded-strings.d.ts +61 -0
- package/dist/hardcoded-strings.js +270 -0
- package/dist/incremental-lint.d.ts +78 -0
- package/dist/incremental-lint.js +129 -0
- package/dist/incremental-test.d.ts +39 -0
- package/dist/incremental-test.js +61 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +61 -0
- package/dist/invariants/check-automated-tests.d.ts +50 -0
- package/dist/invariants/check-automated-tests.js +166 -0
- package/dist/invariants-runner.d.ts +103 -0
- package/dist/invariants-runner.js +527 -0
- package/dist/lane-checker.d.ts +50 -0
- package/dist/lane-checker.js +319 -0
- package/dist/lane-inference.d.ts +39 -0
- package/dist/lane-inference.js +195 -0
- package/dist/lane-lock.d.ts +211 -0
- package/dist/lane-lock.js +474 -0
- package/dist/lane-validator.d.ts +48 -0
- package/dist/lane-validator.js +114 -0
- package/dist/logs-lib.d.ts +104 -0
- package/dist/logs-lib.js +207 -0
- package/dist/lumenflow-config-schema.d.ts +272 -0
- package/dist/lumenflow-config-schema.js +207 -0
- package/dist/lumenflow-config.d.ts +95 -0
- package/dist/lumenflow-config.js +236 -0
- package/dist/manual-test-validator.d.ts +80 -0
- package/dist/manual-test-validator.js +200 -0
- package/dist/merge-lock.d.ts +115 -0
- package/dist/merge-lock.js +251 -0
- package/dist/micro-worktree.d.ts +159 -0
- package/dist/micro-worktree.js +427 -0
- package/dist/migration-deployer.d.ts +69 -0
- package/dist/migration-deployer.js +151 -0
- package/dist/orchestration-advisory-loader.d.ts +28 -0
- package/dist/orchestration-advisory-loader.js +87 -0
- package/dist/orchestration-advisory.d.ts +58 -0
- package/dist/orchestration-advisory.js +94 -0
- package/dist/orchestration-di.d.ts +48 -0
- package/dist/orchestration-di.js +57 -0
- package/dist/orchestration-rules.d.ts +57 -0
- package/dist/orchestration-rules.js +201 -0
- package/dist/orphan-detector.d.ts +131 -0
- package/dist/orphan-detector.js +226 -0
- package/dist/path-classifiers.d.ts +57 -0
- package/dist/path-classifiers.js +93 -0
- package/dist/piped-command-detector.d.ts +34 -0
- package/dist/piped-command-detector.js +64 -0
- package/dist/ports/dashboard-renderer.port.d.ts +112 -0
- package/dist/ports/dashboard-renderer.port.js +25 -0
- package/dist/ports/metrics-collector.port.d.ts +132 -0
- package/dist/ports/metrics-collector.port.js +26 -0
- package/dist/process-detector.d.ts +84 -0
- package/dist/process-detector.js +172 -0
- package/dist/prompt-linter.d.ts +72 -0
- package/dist/prompt-linter.js +312 -0
- package/dist/prompt-monitor.d.ts +15 -0
- package/dist/prompt-monitor.js +205 -0
- package/dist/rebase-artifact-cleanup.d.ts +145 -0
- package/dist/rebase-artifact-cleanup.js +433 -0
- package/dist/retry-strategy.d.ts +189 -0
- package/dist/retry-strategy.js +283 -0
- package/dist/risk-detector.d.ts +108 -0
- package/dist/risk-detector.js +252 -0
- package/dist/rollback-utils.d.ts +76 -0
- package/dist/rollback-utils.js +104 -0
- package/dist/section-headings.d.ts +43 -0
- package/dist/section-headings.js +49 -0
- package/dist/spawn-escalation.d.ts +90 -0
- package/dist/spawn-escalation.js +253 -0
- package/dist/spawn-monitor.d.ts +229 -0
- package/dist/spawn-monitor.js +672 -0
- package/dist/spawn-recovery.d.ts +82 -0
- package/dist/spawn-recovery.js +298 -0
- package/dist/spawn-registry-schema.d.ts +98 -0
- package/dist/spawn-registry-schema.js +108 -0
- package/dist/spawn-registry-store.d.ts +146 -0
- package/dist/spawn-registry-store.js +273 -0
- package/dist/spawn-tree.d.ts +121 -0
- package/dist/spawn-tree.js +285 -0
- package/dist/stamp-status-validator.d.ts +84 -0
- package/dist/stamp-status-validator.js +134 -0
- package/dist/stamp-utils.d.ts +100 -0
- package/dist/stamp-utils.js +229 -0
- package/dist/state-machine.d.ts +26 -0
- package/dist/state-machine.js +83 -0
- package/dist/system-map-validator.d.ts +80 -0
- package/dist/system-map-validator.js +272 -0
- package/dist/telemetry.d.ts +80 -0
- package/dist/telemetry.js +213 -0
- package/dist/token-counter.d.ts +51 -0
- package/dist/token-counter.js +145 -0
- package/dist/usecases/get-dashboard-data.usecase.d.ts +52 -0
- package/dist/usecases/get-dashboard-data.usecase.js +61 -0
- package/dist/usecases/get-suggestions.usecase.d.ts +100 -0
- package/dist/usecases/get-suggestions.usecase.js +153 -0
- package/dist/user-normalizer.d.ts +41 -0
- package/dist/user-normalizer.js +141 -0
- package/dist/validators/phi-constants.d.ts +97 -0
- package/dist/validators/phi-constants.js +152 -0
- package/dist/validators/phi-scanner.d.ts +58 -0
- package/dist/validators/phi-scanner.js +215 -0
- package/dist/worktree-ownership.d.ts +50 -0
- package/dist/worktree-ownership.js +74 -0
- package/dist/worktree-scanner.d.ts +103 -0
- package/dist/worktree-scanner.js +168 -0
- package/dist/worktree-symlink.d.ts +99 -0
- package/dist/worktree-symlink.js +359 -0
- package/dist/wu-backlog-updater.d.ts +17 -0
- package/dist/wu-backlog-updater.js +37 -0
- package/dist/wu-checkpoint.d.ts +124 -0
- package/dist/wu-checkpoint.js +233 -0
- package/dist/wu-claim-helpers.d.ts +26 -0
- package/dist/wu-claim-helpers.js +63 -0
- package/dist/wu-claim-resume.d.ts +106 -0
- package/dist/wu-claim-resume.js +276 -0
- package/dist/wu-consistency-checker.d.ts +95 -0
- package/dist/wu-consistency-checker.js +567 -0
- package/dist/wu-constants.d.ts +1275 -0
- package/dist/wu-constants.js +1382 -0
- package/dist/wu-create-validators.d.ts +42 -0
- package/dist/wu-create-validators.js +93 -0
- package/dist/wu-done-branch-only.d.ts +63 -0
- package/dist/wu-done-branch-only.js +191 -0
- package/dist/wu-done-messages.d.ts +119 -0
- package/dist/wu-done-messages.js +185 -0
- package/dist/wu-done-pr.d.ts +72 -0
- package/dist/wu-done-pr.js +174 -0
- package/dist/wu-done-retry-helpers.d.ts +85 -0
- package/dist/wu-done-retry-helpers.js +172 -0
- package/dist/wu-done-ui.d.ts +37 -0
- package/dist/wu-done-ui.js +69 -0
- package/dist/wu-done-validators.d.ts +411 -0
- package/dist/wu-done-validators.js +1229 -0
- package/dist/wu-done-worktree.d.ts +182 -0
- package/dist/wu-done-worktree.js +1097 -0
- package/dist/wu-helpers.d.ts +128 -0
- package/dist/wu-helpers.js +248 -0
- package/dist/wu-lint.d.ts +70 -0
- package/dist/wu-lint.js +234 -0
- package/dist/wu-paths.d.ts +171 -0
- package/dist/wu-paths.js +178 -0
- package/dist/wu-preflight-validators.d.ts +86 -0
- package/dist/wu-preflight-validators.js +251 -0
- package/dist/wu-recovery.d.ts +138 -0
- package/dist/wu-recovery.js +341 -0
- package/dist/wu-repair-core.d.ts +131 -0
- package/dist/wu-repair-core.js +669 -0
- package/dist/wu-schema-normalization.d.ts +17 -0
- package/dist/wu-schema-normalization.js +82 -0
- package/dist/wu-schema.d.ts +793 -0
- package/dist/wu-schema.js +881 -0
- package/dist/wu-spawn-helpers.d.ts +121 -0
- package/dist/wu-spawn-helpers.js +271 -0
- package/dist/wu-spawn.d.ts +158 -0
- package/dist/wu-spawn.js +1306 -0
- package/dist/wu-state-schema.d.ts +213 -0
- package/dist/wu-state-schema.js +156 -0
- package/dist/wu-state-store.d.ts +264 -0
- package/dist/wu-state-store.js +691 -0
- package/dist/wu-status-transition.d.ts +63 -0
- package/dist/wu-status-transition.js +382 -0
- package/dist/wu-status-updater.d.ts +25 -0
- package/dist/wu-status-updater.js +116 -0
- package/dist/wu-transaction-collectors.d.ts +116 -0
- package/dist/wu-transaction-collectors.js +272 -0
- package/dist/wu-transaction.d.ts +170 -0
- package/dist/wu-transaction.js +273 -0
- package/dist/wu-validation-constants.d.ts +60 -0
- package/dist/wu-validation-constants.js +66 -0
- package/dist/wu-validation.d.ts +118 -0
- package/dist/wu-validation.js +243 -0
- package/dist/wu-validator.d.ts +62 -0
- package/dist/wu-validator.js +325 -0
- package/dist/wu-yaml-fixer.d.ts +97 -0
- package/dist/wu-yaml-fixer.js +264 -0
- package/dist/wu-yaml.d.ts +86 -0
- package/dist/wu-yaml.js +222 -0
- package/package.json +114 -0
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WU State Store (WU-1570, WU-2240)
|
|
3
|
+
*
|
|
4
|
+
* Event-sourced state store for WU lifecycle following INIT-007 pattern.
|
|
5
|
+
* Stores events in .beacon/state/wu-events.jsonl (append-only, git-friendly).
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Event sourcing with replay for current state
|
|
9
|
+
* - Atomic append operations (WU-2240: temp file + fsync + rename)
|
|
10
|
+
* - O(1) queries by status and lane via in-memory indexes
|
|
11
|
+
* - State machine validation for legal transitions
|
|
12
|
+
* - File locking with stale detection (WU-2240)
|
|
13
|
+
* - Corruption recovery via repairStateFile (WU-2240)
|
|
14
|
+
*
|
|
15
|
+
* @see {@link tools/__tests__/state-store-concurrent.test.mjs} - Concurrent access tests
|
|
16
|
+
* @see {@link tools/lib/wu-state-schema.mjs} - Schema definitions
|
|
17
|
+
*/
|
|
18
|
+
import fs from 'node:fs/promises';
|
|
19
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync, unlinkSync, openSync, closeSync, fsyncSync, } from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import os from 'node:os';
|
|
22
|
+
import { validateWUEvent } from './wu-state-schema.js';
|
|
23
|
+
/**
|
|
24
|
+
* Lock timeout in milliseconds (5 minutes)
|
|
25
|
+
*/
|
|
26
|
+
const LOCK_TIMEOUT_MS = 5 * 60 * 1000;
|
|
27
|
+
/**
|
|
28
|
+
* Lock retry configuration
|
|
29
|
+
*/
|
|
30
|
+
const LOCK_RETRY_DELAY_MS = 50;
|
|
31
|
+
const LOCK_MAX_RETRIES = 100; // 5 seconds total
|
|
32
|
+
/**
|
|
33
|
+
* WU events file name constant
|
|
34
|
+
*/
|
|
35
|
+
export const WU_EVENTS_FILE_NAME = 'wu-events.jsonl';
|
|
36
|
+
/**
|
|
37
|
+
* WU State Store class
|
|
38
|
+
*
|
|
39
|
+
* Manages WU lifecycle state via event sourcing pattern.
|
|
40
|
+
* Events are appended to JSONL file, state is rebuilt by replaying events.
|
|
41
|
+
*/
|
|
42
|
+
export class WUStateStore {
|
|
43
|
+
baseDir;
|
|
44
|
+
eventsFilePath;
|
|
45
|
+
wuState;
|
|
46
|
+
byStatus;
|
|
47
|
+
byLane;
|
|
48
|
+
byParent;
|
|
49
|
+
constructor(baseDir) {
|
|
50
|
+
this.baseDir = baseDir;
|
|
51
|
+
this.eventsFilePath = path.join(baseDir, WU_EVENTS_FILE_NAME);
|
|
52
|
+
// In-memory state (rebuilt from events)
|
|
53
|
+
this.wuState = new Map();
|
|
54
|
+
this.byStatus = new Map();
|
|
55
|
+
this.byLane = new Map();
|
|
56
|
+
this.byParent = new Map();
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Loads and replays events from JSONL file into current state.
|
|
60
|
+
*
|
|
61
|
+
* Handles:
|
|
62
|
+
* - Missing file: returns empty state
|
|
63
|
+
* - Empty file: returns empty state
|
|
64
|
+
* - Empty lines: skipped gracefully
|
|
65
|
+
* - Malformed JSON: throws error with line info
|
|
66
|
+
* - Invalid events: throws validation error
|
|
67
|
+
*
|
|
68
|
+
* @throws Error If file contains malformed JSON or invalid events
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* const store = new WUStateStore('/path/to/project');
|
|
72
|
+
* await store.load();
|
|
73
|
+
* const inProgress = store.getByStatus('in_progress');
|
|
74
|
+
*/
|
|
75
|
+
async load() {
|
|
76
|
+
// Reset state
|
|
77
|
+
this.wuState.clear();
|
|
78
|
+
this.byStatus.clear();
|
|
79
|
+
this.byLane.clear();
|
|
80
|
+
this.byParent.clear();
|
|
81
|
+
// Check if file exists
|
|
82
|
+
let content;
|
|
83
|
+
try {
|
|
84
|
+
content = await fs.readFile(this.eventsFilePath, 'utf-8');
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (error.code === 'ENOENT') {
|
|
88
|
+
// File doesn't exist - return empty state
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
// Parse JSONL content
|
|
94
|
+
const lines = content.split('\n');
|
|
95
|
+
for (let i = 0; i < lines.length; i++) {
|
|
96
|
+
const line = lines[i].trim();
|
|
97
|
+
// Skip empty lines
|
|
98
|
+
if (!line) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
// Parse JSON line
|
|
102
|
+
let parsed;
|
|
103
|
+
try {
|
|
104
|
+
parsed = JSON.parse(line);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
throw new Error(`Malformed JSON on line ${i + 1}: ${error.message}`);
|
|
108
|
+
}
|
|
109
|
+
// Validate against schema
|
|
110
|
+
const validation = validateWUEvent(parsed);
|
|
111
|
+
if (!validation.success) {
|
|
112
|
+
const issues = validation.error.issues
|
|
113
|
+
.map((issue) => `${issue.path.join('.')}: ${issue.message}`)
|
|
114
|
+
.join(', ');
|
|
115
|
+
throw new Error(`Validation error on line ${i + 1}: ${issues}`);
|
|
116
|
+
}
|
|
117
|
+
const event = validation.data;
|
|
118
|
+
// Apply event to state
|
|
119
|
+
this._applyEvent(event);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Transition WU to a new status if it exists.
|
|
124
|
+
*/
|
|
125
|
+
_transitionToStatus(wuId, newStatus) {
|
|
126
|
+
const current = this.wuState.get(wuId);
|
|
127
|
+
if (current) {
|
|
128
|
+
this._setState(wuId, newStatus, current.lane, current.title);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Applies an event to the in-memory state.
|
|
133
|
+
*/
|
|
134
|
+
_applyEvent(event) {
|
|
135
|
+
const { wuId, type } = event;
|
|
136
|
+
if (type === 'create' || type === 'claim') {
|
|
137
|
+
const claimEvent = event;
|
|
138
|
+
this._setState(wuId, 'in_progress', claimEvent.lane, claimEvent.title);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (type === 'block') {
|
|
142
|
+
this._transitionToStatus(wuId, 'blocked');
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (type === 'unblock') {
|
|
146
|
+
this._transitionToStatus(wuId, 'in_progress');
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (type === 'complete') {
|
|
150
|
+
this._transitionToStatus(wuId, 'done');
|
|
151
|
+
// WU-2244: Store completion timestamp for accurate date reporting
|
|
152
|
+
const current = this.wuState.get(wuId);
|
|
153
|
+
if (current) {
|
|
154
|
+
current.completedAt = event.timestamp;
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (type === 'checkpoint') {
|
|
159
|
+
const checkpointEvent = event;
|
|
160
|
+
const currentCheckpoint = this.wuState.get(wuId);
|
|
161
|
+
if (currentCheckpoint) {
|
|
162
|
+
currentCheckpoint.lastCheckpoint = event.timestamp;
|
|
163
|
+
currentCheckpoint.lastCheckpointNote = checkpointEvent.note;
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (type === 'spawn') {
|
|
168
|
+
const spawnEvent = event;
|
|
169
|
+
const { parentWuId } = spawnEvent;
|
|
170
|
+
if (!this.byParent.has(parentWuId)) {
|
|
171
|
+
this.byParent.set(parentWuId, new Set());
|
|
172
|
+
}
|
|
173
|
+
this.byParent.get(parentWuId).add(wuId);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Sets WU state and updates indexes.
|
|
178
|
+
*/
|
|
179
|
+
_setState(wuId, status, lane, title) {
|
|
180
|
+
// Remove from old status index
|
|
181
|
+
const oldState = this.wuState.get(wuId);
|
|
182
|
+
if (oldState) {
|
|
183
|
+
const oldStatusSet = this.byStatus.get(oldState.status);
|
|
184
|
+
if (oldStatusSet) {
|
|
185
|
+
oldStatusSet.delete(wuId);
|
|
186
|
+
}
|
|
187
|
+
// Remove from old lane index
|
|
188
|
+
const oldLaneSet = this.byLane.get(oldState.lane);
|
|
189
|
+
if (oldLaneSet) {
|
|
190
|
+
oldLaneSet.delete(wuId);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Update state
|
|
194
|
+
this.wuState.set(wuId, { status, lane, title });
|
|
195
|
+
// Add to new status index
|
|
196
|
+
if (!this.byStatus.has(status)) {
|
|
197
|
+
this.byStatus.set(status, new Set());
|
|
198
|
+
}
|
|
199
|
+
this.byStatus.get(status).add(wuId);
|
|
200
|
+
// Add to new lane index
|
|
201
|
+
if (!this.byLane.has(lane)) {
|
|
202
|
+
this.byLane.set(lane, new Set());
|
|
203
|
+
}
|
|
204
|
+
this.byLane.get(lane).add(wuId);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Appends an event to the events file.
|
|
208
|
+
*
|
|
209
|
+
* Uses append mode to avoid full file rewrite.
|
|
210
|
+
* Creates file and parent directories if they don't exist.
|
|
211
|
+
* Validates event before appending.
|
|
212
|
+
*
|
|
213
|
+
* @throws Error If event fails validation
|
|
214
|
+
*/
|
|
215
|
+
async _appendEvent(event) {
|
|
216
|
+
// Validate event before appending
|
|
217
|
+
const validation = validateWUEvent(event);
|
|
218
|
+
if (!validation.success) {
|
|
219
|
+
const issues = validation.error.issues
|
|
220
|
+
.map((issue) => `${issue.path.join('.')}: ${issue.message}`)
|
|
221
|
+
.join(', ');
|
|
222
|
+
throw new Error(`Validation error: ${issues}`);
|
|
223
|
+
}
|
|
224
|
+
const line = `${JSON.stringify(event)}\n`;
|
|
225
|
+
// WU-1740: Ensure parent directory exists before appending
|
|
226
|
+
// fs.appendFile creates the file but not parent directories
|
|
227
|
+
await fs.mkdir(this.baseDir, { recursive: true });
|
|
228
|
+
// Use append flag to avoid rewriting the file
|
|
229
|
+
await fs.appendFile(this.eventsFilePath, line, 'utf-8');
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Claims a WU (transitions to in_progress).
|
|
233
|
+
*
|
|
234
|
+
* @throws Error If WU is already in_progress
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* await store.claim('WU-1570', 'Operations: Tooling', 'Test WU');
|
|
238
|
+
*/
|
|
239
|
+
async claim(wuId, lane, title) {
|
|
240
|
+
// Check state machine: can't claim if already in_progress
|
|
241
|
+
const currentState = this.wuState.get(wuId);
|
|
242
|
+
if (currentState && currentState.status === 'in_progress') {
|
|
243
|
+
throw new Error(`WU ${wuId} is already in_progress`);
|
|
244
|
+
}
|
|
245
|
+
const event = {
|
|
246
|
+
type: 'claim',
|
|
247
|
+
wuId,
|
|
248
|
+
lane,
|
|
249
|
+
title,
|
|
250
|
+
timestamp: new Date().toISOString(),
|
|
251
|
+
};
|
|
252
|
+
await this._appendEvent(event);
|
|
253
|
+
this._applyEvent(event);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Completes a WU (transitions to done).
|
|
257
|
+
*
|
|
258
|
+
* @throws Error If WU is not in_progress
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* await store.complete('WU-1570');
|
|
262
|
+
*/
|
|
263
|
+
async complete(wuId) {
|
|
264
|
+
// Check state machine: can only complete if in_progress
|
|
265
|
+
const currentState = this.wuState.get(wuId);
|
|
266
|
+
if (!currentState || currentState.status !== 'in_progress') {
|
|
267
|
+
throw new Error(`WU ${wuId} is not in_progress`);
|
|
268
|
+
}
|
|
269
|
+
const event = {
|
|
270
|
+
type: 'complete',
|
|
271
|
+
wuId,
|
|
272
|
+
timestamp: new Date().toISOString(),
|
|
273
|
+
};
|
|
274
|
+
await this._appendEvent(event);
|
|
275
|
+
this._applyEvent(event);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Get current in-memory state for a WU.
|
|
279
|
+
*/
|
|
280
|
+
getWUState(wuId) {
|
|
281
|
+
return this.wuState.get(wuId);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Create a complete event without writing to disk.
|
|
285
|
+
*
|
|
286
|
+
* Used by transactional flows where event log writes are staged and committed atomically.
|
|
287
|
+
*
|
|
288
|
+
* @throws Error If WU is not in_progress or event fails validation
|
|
289
|
+
*/
|
|
290
|
+
createCompleteEvent(wuId, timestamp = new Date().toISOString()) {
|
|
291
|
+
const currentState = this.wuState.get(wuId);
|
|
292
|
+
if (!currentState || currentState.status !== 'in_progress') {
|
|
293
|
+
throw new Error(`WU ${wuId} is not in_progress`);
|
|
294
|
+
}
|
|
295
|
+
const event = { type: 'complete', wuId, timestamp };
|
|
296
|
+
const validation = validateWUEvent(event);
|
|
297
|
+
if (!validation.success) {
|
|
298
|
+
const issues = validation.error.issues
|
|
299
|
+
.map((issue) => `${issue.path.join('.')}: ${issue.message}`)
|
|
300
|
+
.join(', ');
|
|
301
|
+
throw new Error(`Validation error: ${issues}`);
|
|
302
|
+
}
|
|
303
|
+
return validation.data;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Apply a validated event to in-memory state without writing to disk.
|
|
307
|
+
*
|
|
308
|
+
* @throws Error If event fails validation
|
|
309
|
+
*/
|
|
310
|
+
applyEvent(event) {
|
|
311
|
+
const validation = validateWUEvent(event);
|
|
312
|
+
if (!validation.success) {
|
|
313
|
+
const issues = validation.error.issues
|
|
314
|
+
.map((issue) => `${issue.path.join('.')}: ${issue.message}`)
|
|
315
|
+
.join(', ');
|
|
316
|
+
throw new Error(`Validation error: ${issues}`);
|
|
317
|
+
}
|
|
318
|
+
this._applyEvent(validation.data);
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Blocks a WU (transitions to blocked).
|
|
322
|
+
*
|
|
323
|
+
* @throws Error If WU is not in_progress
|
|
324
|
+
*
|
|
325
|
+
* @example
|
|
326
|
+
* await store.block('WU-1570', 'Blocked by dependency');
|
|
327
|
+
*/
|
|
328
|
+
async block(wuId, reason) {
|
|
329
|
+
// Check state machine: can only block if in_progress
|
|
330
|
+
const currentState = this.wuState.get(wuId);
|
|
331
|
+
if (!currentState || currentState.status !== 'in_progress') {
|
|
332
|
+
throw new Error(`WU ${wuId} is not in_progress`);
|
|
333
|
+
}
|
|
334
|
+
const event = {
|
|
335
|
+
type: 'block',
|
|
336
|
+
wuId,
|
|
337
|
+
reason,
|
|
338
|
+
timestamp: new Date().toISOString(),
|
|
339
|
+
};
|
|
340
|
+
await this._appendEvent(event);
|
|
341
|
+
this._applyEvent(event);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Unblocks a WU (transitions back to in_progress).
|
|
345
|
+
*
|
|
346
|
+
* @throws Error If WU is not blocked
|
|
347
|
+
*
|
|
348
|
+
* @example
|
|
349
|
+
* await store.unblock('WU-1570');
|
|
350
|
+
*/
|
|
351
|
+
async unblock(wuId) {
|
|
352
|
+
// Check state machine: can only unblock if blocked
|
|
353
|
+
const currentState = this.wuState.get(wuId);
|
|
354
|
+
if (!currentState || currentState.status !== 'blocked') {
|
|
355
|
+
throw new Error(`WU ${wuId} is not blocked`);
|
|
356
|
+
}
|
|
357
|
+
const event = {
|
|
358
|
+
type: 'unblock',
|
|
359
|
+
wuId,
|
|
360
|
+
timestamp: new Date().toISOString(),
|
|
361
|
+
};
|
|
362
|
+
await this._appendEvent(event);
|
|
363
|
+
this._applyEvent(event);
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Records a checkpoint for a WU (WU-1748: cross-agent visibility).
|
|
367
|
+
*
|
|
368
|
+
* Checkpoints are recorded for visibility but don't change WU state.
|
|
369
|
+
* Used to track progress and detect abandoned WUs.
|
|
370
|
+
*
|
|
371
|
+
* @example
|
|
372
|
+
* await store.checkpoint('WU-1748', 'Completed worktree scanner', {
|
|
373
|
+
* progress: 'Scanner implemented and tests passing',
|
|
374
|
+
* nextSteps: 'Integrate into orchestrate:status'
|
|
375
|
+
* });
|
|
376
|
+
*/
|
|
377
|
+
async checkpoint(wuId, note, options = {}) {
|
|
378
|
+
const { sessionId, progress, nextSteps } = options;
|
|
379
|
+
const event = {
|
|
380
|
+
type: 'checkpoint',
|
|
381
|
+
wuId,
|
|
382
|
+
note,
|
|
383
|
+
timestamp: new Date().toISOString(),
|
|
384
|
+
};
|
|
385
|
+
if (sessionId)
|
|
386
|
+
event.sessionId = sessionId;
|
|
387
|
+
if (progress)
|
|
388
|
+
event.progress = progress;
|
|
389
|
+
if (nextSteps)
|
|
390
|
+
event.nextSteps = nextSteps;
|
|
391
|
+
await this._appendEvent(event);
|
|
392
|
+
this._applyEvent(event);
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Gets WU IDs by status (O(1) lookup).
|
|
396
|
+
*
|
|
397
|
+
* @example
|
|
398
|
+
* const inProgress = store.getByStatus('in_progress');
|
|
399
|
+
* for (const wuId of inProgress) {
|
|
400
|
+
* console.log(wuId);
|
|
401
|
+
* }
|
|
402
|
+
*/
|
|
403
|
+
getByStatus(status) {
|
|
404
|
+
return this.byStatus.get(status) ?? new Set();
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Gets WU IDs by lane (O(1) lookup).
|
|
408
|
+
*
|
|
409
|
+
* @example
|
|
410
|
+
* const tooling = store.getByLane('Operations: Tooling');
|
|
411
|
+
* for (const wuId of tooling) {
|
|
412
|
+
* console.log(wuId);
|
|
413
|
+
* }
|
|
414
|
+
*/
|
|
415
|
+
getByLane(lane) {
|
|
416
|
+
return this.byLane.get(lane) ?? new Set();
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Gets child WU IDs spawned from a parent WU (O(1) lookup).
|
|
420
|
+
* WU-1947: Parent-child relationship tracking.
|
|
421
|
+
*
|
|
422
|
+
* @example
|
|
423
|
+
* const children = store.getChildWUs('WU-100');
|
|
424
|
+
* for (const childId of children) {
|
|
425
|
+
* console.log(`Child WU: ${childId}`);
|
|
426
|
+
* }
|
|
427
|
+
*/
|
|
428
|
+
getChildWUs(parentWuId) {
|
|
429
|
+
return this.byParent.get(parentWuId) ?? new Set();
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Records a spawn relationship between parent and child WUs.
|
|
433
|
+
* WU-1947: Parent-child relationship tracking.
|
|
434
|
+
*
|
|
435
|
+
* @example
|
|
436
|
+
* await store.spawn('WU-200', 'WU-100', 'spawn-abc123');
|
|
437
|
+
*/
|
|
438
|
+
async spawn(childWuId, parentWuId, spawnId) {
|
|
439
|
+
const event = {
|
|
440
|
+
type: 'spawn',
|
|
441
|
+
wuId: childWuId,
|
|
442
|
+
parentWuId,
|
|
443
|
+
spawnId,
|
|
444
|
+
timestamp: new Date().toISOString(),
|
|
445
|
+
};
|
|
446
|
+
await this._appendEvent(event);
|
|
447
|
+
this._applyEvent(event);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Check if a process with given PID is running
|
|
452
|
+
*/
|
|
453
|
+
function isProcessRunning(pid) {
|
|
454
|
+
try {
|
|
455
|
+
// Sending signal 0 checks if process exists without affecting it
|
|
456
|
+
process.kill(pid, 0);
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
catch {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Check if a lock is stale (expired or dead process)
|
|
465
|
+
*
|
|
466
|
+
* WU-2240: Prepared for proper-lockfile integration
|
|
467
|
+
*/
|
|
468
|
+
export function isLockStale(lockData) {
|
|
469
|
+
const now = Date.now();
|
|
470
|
+
const lockAge = now - lockData.timestamp;
|
|
471
|
+
// Check timeout first (5 minutes)
|
|
472
|
+
if (lockAge > LOCK_TIMEOUT_MS) {
|
|
473
|
+
return true;
|
|
474
|
+
}
|
|
475
|
+
// Check if on same host - if different host, can't check PID
|
|
476
|
+
if (lockData.hostname !== os.hostname()) {
|
|
477
|
+
// Different host, only rely on timeout
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
// Same host - check if process is still alive
|
|
481
|
+
return !isProcessRunning(lockData.pid);
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Safely remove a lock file, ignoring errors
|
|
485
|
+
*/
|
|
486
|
+
function safeUnlink(lockPath) {
|
|
487
|
+
try {
|
|
488
|
+
unlinkSync(lockPath);
|
|
489
|
+
}
|
|
490
|
+
catch {
|
|
491
|
+
// Ignore removal errors
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Read and parse existing lock file
|
|
496
|
+
*/
|
|
497
|
+
function readLockFile(lockPath) {
|
|
498
|
+
try {
|
|
499
|
+
const content = readFileSync(lockPath, 'utf-8');
|
|
500
|
+
return JSON.parse(content);
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Handle existing lock file - returns true if should retry
|
|
508
|
+
*/
|
|
509
|
+
async function handleExistingLock(lockPath) {
|
|
510
|
+
const existingLock = readLockFile(lockPath);
|
|
511
|
+
if (!existingLock) {
|
|
512
|
+
// Corrupted lock file - remove and retry
|
|
513
|
+
safeUnlink(lockPath);
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
516
|
+
if (isLockStale(existingLock)) {
|
|
517
|
+
safeUnlink(lockPath);
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
// Lock is held by active process - wait and retry
|
|
521
|
+
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_DELAY_MS));
|
|
522
|
+
return true;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Try to create a lock file atomically
|
|
526
|
+
*/
|
|
527
|
+
async function tryCreateLock(lockPath, lockData) {
|
|
528
|
+
try {
|
|
529
|
+
mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
530
|
+
const fd = openSync(lockPath, 'wx');
|
|
531
|
+
const content = JSON.stringify(lockData);
|
|
532
|
+
writeFileSync(fd, content, 'utf-8');
|
|
533
|
+
fsyncSync(fd);
|
|
534
|
+
closeSync(fd);
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
catch (error) {
|
|
538
|
+
if (error.code === 'EEXIST') {
|
|
539
|
+
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_DELAY_MS));
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
throw error;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Acquire a file lock for the events file
|
|
547
|
+
*
|
|
548
|
+
* Uses a JSON lock file containing PID, timestamp, and hostname.
|
|
549
|
+
* Implements stale lock detection via:
|
|
550
|
+
* - PID check (on same host)
|
|
551
|
+
* - 5-minute timeout (across hosts)
|
|
552
|
+
*
|
|
553
|
+
* WU-2240: Prepared for proper-lockfile integration
|
|
554
|
+
*
|
|
555
|
+
* @throws Error If lock cannot be acquired after retries
|
|
556
|
+
*/
|
|
557
|
+
export async function acquireLock(lockPath) {
|
|
558
|
+
const lockData = {
|
|
559
|
+
pid: process.pid,
|
|
560
|
+
timestamp: Date.now(),
|
|
561
|
+
hostname: os.hostname(),
|
|
562
|
+
};
|
|
563
|
+
for (let attempt = 0; attempt < LOCK_MAX_RETRIES; attempt++) {
|
|
564
|
+
if (existsSync(lockPath)) {
|
|
565
|
+
const shouldRetry = await handleExistingLock(lockPath);
|
|
566
|
+
if (shouldRetry)
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
const acquired = await tryCreateLock(lockPath, lockData);
|
|
570
|
+
if (acquired)
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
throw new Error(`Failed to acquire lock after ${LOCK_MAX_RETRIES} attempts`);
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Release a file lock
|
|
577
|
+
*
|
|
578
|
+
* WU-2240: Prepared for proper-lockfile integration
|
|
579
|
+
*/
|
|
580
|
+
export function releaseLock(lockPath) {
|
|
581
|
+
safeUnlink(lockPath);
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Repair a corrupted state file by removing invalid lines.
|
|
585
|
+
*
|
|
586
|
+
* WU-2240: Corruption recovery for wu-events.jsonl
|
|
587
|
+
*
|
|
588
|
+
* Features:
|
|
589
|
+
* - Creates backup before repair
|
|
590
|
+
* - Removes malformed JSON lines
|
|
591
|
+
* - Removes lines that fail schema validation
|
|
592
|
+
* - Returns detailed repair statistics
|
|
593
|
+
*
|
|
594
|
+
* @example
|
|
595
|
+
* const stateFilePath = path.join(process.cwd(), '.beacon', 'state', 'wu-events.jsonl');
|
|
596
|
+
* const result = await repairStateFile(stateFilePath);
|
|
597
|
+
* if (result.success) {
|
|
598
|
+
* console.log(`Repaired: kept ${result.linesKept}, removed ${result.linesRemoved}`);
|
|
599
|
+
* }
|
|
600
|
+
*/
|
|
601
|
+
export async function repairStateFile(filePath) {
|
|
602
|
+
const warnings = [];
|
|
603
|
+
let linesKept = 0;
|
|
604
|
+
let linesRemoved = 0;
|
|
605
|
+
// Check if file exists
|
|
606
|
+
if (!existsSync(filePath)) {
|
|
607
|
+
return {
|
|
608
|
+
success: true,
|
|
609
|
+
linesKept: 0,
|
|
610
|
+
linesRemoved: 0,
|
|
611
|
+
backupPath: null,
|
|
612
|
+
warnings: ['File does not exist, nothing to repair'],
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
// Read the original content
|
|
616
|
+
const originalContent = readFileSync(filePath, 'utf-8');
|
|
617
|
+
const lines = originalContent.split('\n');
|
|
618
|
+
// Create backup with timestamp
|
|
619
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
620
|
+
const backupPath = `${filePath}.backup.${timestamp}`;
|
|
621
|
+
writeFileSync(backupPath, originalContent, 'utf-8');
|
|
622
|
+
// Process each line
|
|
623
|
+
const validLines = [];
|
|
624
|
+
for (let i = 0; i < lines.length; i++) {
|
|
625
|
+
const line = lines[i].trim();
|
|
626
|
+
// Skip empty lines
|
|
627
|
+
if (!line) {
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
// Try to parse JSON
|
|
631
|
+
let parsed;
|
|
632
|
+
try {
|
|
633
|
+
parsed = JSON.parse(line);
|
|
634
|
+
}
|
|
635
|
+
catch {
|
|
636
|
+
linesRemoved++;
|
|
637
|
+
warnings.push(`Line ${i + 1}: Malformed JSON removed`);
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
// Validate against schema
|
|
641
|
+
const validation = validateWUEvent(parsed);
|
|
642
|
+
if (!validation.success) {
|
|
643
|
+
linesRemoved++;
|
|
644
|
+
const issues = validation.error.issues
|
|
645
|
+
.map((issue) => `${issue.path.join('.')}: ${issue.message}`)
|
|
646
|
+
.join(', ');
|
|
647
|
+
warnings.push(`Line ${i + 1}: Invalid event removed (${issues})`);
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
// Line is valid
|
|
651
|
+
validLines.push(line);
|
|
652
|
+
linesKept++;
|
|
653
|
+
}
|
|
654
|
+
// Write repaired file atomically
|
|
655
|
+
const tempPath = `${filePath}.tmp.${process.pid}`;
|
|
656
|
+
const repairedContent = validLines.length > 0 ? `${validLines.join('\n')}\n` : '';
|
|
657
|
+
try {
|
|
658
|
+
const fd = openSync(tempPath, 'w');
|
|
659
|
+
writeFileSync(fd, repairedContent, 'utf-8');
|
|
660
|
+
fsyncSync(fd);
|
|
661
|
+
closeSync(fd);
|
|
662
|
+
// Atomic rename
|
|
663
|
+
renameSync(tempPath, filePath);
|
|
664
|
+
// Fsync directory
|
|
665
|
+
const dirPath = path.dirname(filePath);
|
|
666
|
+
const dirFd = openSync(dirPath, 'r');
|
|
667
|
+
fsyncSync(dirFd);
|
|
668
|
+
closeSync(dirFd);
|
|
669
|
+
}
|
|
670
|
+
catch (error) {
|
|
671
|
+
// Cleanup temp file on failure
|
|
672
|
+
try {
|
|
673
|
+
unlinkSync(tempPath);
|
|
674
|
+
}
|
|
675
|
+
catch {
|
|
676
|
+
// Ignore cleanup errors
|
|
677
|
+
}
|
|
678
|
+
throw error;
|
|
679
|
+
}
|
|
680
|
+
// Add warning if file is now empty
|
|
681
|
+
if (linesKept === 0 && linesRemoved > 0) {
|
|
682
|
+
warnings.push('All lines were invalid - file is now empty');
|
|
683
|
+
}
|
|
684
|
+
return {
|
|
685
|
+
success: true,
|
|
686
|
+
linesKept,
|
|
687
|
+
linesRemoved,
|
|
688
|
+
backupPath,
|
|
689
|
+
warnings,
|
|
690
|
+
};
|
|
691
|
+
}
|