@plures/praxis 1.2.0 → 1.2.10
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 +10 -96
- package/dist/browser/{adapter-TM4IS5KT.js → adapter-CIMBGDC7.js} +5 -3
- package/dist/browser/{chunk-LE2ZJYFC.js → chunk-K377RW4V.js} +76 -0
- package/dist/{node/chunk-JQ64KMLN.js → browser/chunk-MBVHLOU2.js} +12 -1
- package/dist/browser/index.d.ts +32 -5
- package/dist/browser/index.js +15 -7
- package/dist/browser/integrations/svelte.d.ts +2 -2
- package/dist/browser/integrations/svelte.js +1 -1
- package/dist/browser/{reactive-engine.svelte-C9OpcTHf.d.ts → reactive-engine.svelte-9aS0kTa8.d.ts} +136 -1
- package/dist/node/{adapter-K6DOX6XS.js → adapter-75ISSMWD.js} +5 -3
- package/dist/node/chunk-5RH7UAQC.js +486 -0
- package/dist/{browser/chunk-JQ64KMLN.js → node/chunk-MBVHLOU2.js} +12 -1
- package/dist/node/{chunk-LE2ZJYFC.js → chunk-PRPQO6R5.js} +3 -72
- package/dist/node/chunk-R2PSBPKQ.js +150 -0
- package/dist/node/chunk-WZ6B3LZ6.js +638 -0
- package/dist/node/cli/index.cjs +2316 -832
- package/dist/node/cli/index.js +18 -0
- package/dist/node/components/index.d.cts +3 -2
- package/dist/node/components/index.d.ts +3 -2
- package/dist/node/index.cjs +620 -38
- package/dist/node/index.d.cts +259 -5
- package/dist/node/index.d.ts +259 -5
- package/dist/node/index.js +55 -65
- package/dist/node/integrations/svelte.cjs +76 -0
- package/dist/node/integrations/svelte.d.cts +2 -2
- package/dist/node/integrations/svelte.d.ts +2 -2
- package/dist/node/integrations/svelte.js +2 -1
- package/dist/node/{reactive-engine.svelte-1M4m_C_v.d.cts → reactive-engine.svelte-BFIZfawz.d.cts} +199 -1
- package/dist/node/{reactive-engine.svelte-ChNFn4Hj.d.ts → reactive-engine.svelte-CRNqHlbv.d.ts} +199 -1
- package/dist/node/reverse-W7THPV45.js +193 -0
- package/dist/node/{terminal-adapter-CWka-yL8.d.ts → terminal-adapter-B-UK_Vdz.d.ts} +28 -3
- package/dist/node/{terminal-adapter-CDzxoLKR.d.cts → terminal-adapter-BQSIF5bf.d.cts} +28 -3
- package/dist/node/validate-CNHUULQE.js +180 -0
- package/docs/core/pluresdb-integration.md +15 -15
- package/docs/decision-ledger/BEHAVIOR_LEDGER.md +225 -0
- package/docs/decision-ledger/DecisionLedger.tla +180 -0
- package/docs/decision-ledger/IMPLEMENTATION_SUMMARY.md +217 -0
- package/docs/decision-ledger/LATEST.md +166 -0
- package/docs/guides/cicd-pipeline.md +142 -0
- package/package.json +2 -2
- package/src/__tests__/cli-validate.test.ts +197 -0
- package/src/__tests__/decision-ledger.test.ts +485 -0
- package/src/__tests__/reverse-generator.test.ts +189 -0
- package/src/__tests__/scanner.test.ts +215 -0
- package/src/cli/commands/reverse.ts +289 -0
- package/src/cli/commands/validate.ts +264 -0
- package/src/cli/index.ts +47 -0
- package/src/core/pluresdb/adapter.ts +45 -2
- package/src/core/rules.ts +133 -0
- package/src/decision-ledger/README.md +400 -0
- package/src/decision-ledger/REVERSE_ENGINEERING.md +484 -0
- package/src/decision-ledger/facts-events.ts +121 -0
- package/src/decision-ledger/index.ts +70 -0
- package/src/decision-ledger/ledger.ts +246 -0
- package/src/decision-ledger/logic-ledger.ts +158 -0
- package/src/decision-ledger/reverse-generator.ts +426 -0
- package/src/decision-ledger/scanner.ts +506 -0
- package/src/decision-ledger/types.ts +247 -0
- package/src/decision-ledger/validation.ts +336 -0
- package/src/dsl/index.ts +13 -2
- package/src/index.browser.ts +2 -0
- package/src/index.ts +36 -0
- package/src/integrations/pluresdb.ts +14 -2
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decision Ledger - Ledger Storage
|
|
3
|
+
*
|
|
4
|
+
* Immutable, append-only ledger for tracking contract history.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Contract, Assumption } from './types.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Status of a ledger entry.
|
|
11
|
+
*/
|
|
12
|
+
export type LedgerEntryStatus = 'active' | 'superseded' | 'deprecated';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A single entry in the behavior ledger.
|
|
16
|
+
*/
|
|
17
|
+
export interface LedgerEntry {
|
|
18
|
+
/** Unique identifier for this ledger entry */
|
|
19
|
+
id: string;
|
|
20
|
+
/** Timestamp of entry creation */
|
|
21
|
+
timestamp: string;
|
|
22
|
+
/** Status of this entry */
|
|
23
|
+
status: LedgerEntryStatus;
|
|
24
|
+
/** Author or system that created this entry */
|
|
25
|
+
author: string;
|
|
26
|
+
/** The contract being recorded */
|
|
27
|
+
contract: Contract;
|
|
28
|
+
/** ID of the entry this supersedes (if any) */
|
|
29
|
+
supersedes?: string;
|
|
30
|
+
/** Reason for this entry (e.g., 'initial', 'assumption-revised', 'behavior-updated') */
|
|
31
|
+
reason?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Immutable, append-only behavior ledger.
|
|
36
|
+
*/
|
|
37
|
+
export class BehaviorLedger {
|
|
38
|
+
private entries: LedgerEntry[] = [];
|
|
39
|
+
private entryMap: Map<string, LedgerEntry> = new Map();
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Append a new entry to the ledger.
|
|
43
|
+
*
|
|
44
|
+
* @param entry The entry to append
|
|
45
|
+
* @throws Error if entry ID already exists
|
|
46
|
+
*/
|
|
47
|
+
append(entry: LedgerEntry): void {
|
|
48
|
+
if (this.entryMap.has(entry.id)) {
|
|
49
|
+
throw new Error(`Ledger entry with ID '${entry.id}' already exists`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// If this entry supersedes another, mark the old one as superseded
|
|
53
|
+
// Note: The entries array preserves the original entry for historical record.
|
|
54
|
+
// Only the map entry is updated to reflect the superseded status for queries.
|
|
55
|
+
if (entry.supersedes) {
|
|
56
|
+
const superseded = this.entryMap.get(entry.supersedes);
|
|
57
|
+
if (superseded && superseded.status === 'active') {
|
|
58
|
+
// Create a new entry with updated status (immutability)
|
|
59
|
+
const updatedEntry: LedgerEntry = {
|
|
60
|
+
...superseded,
|
|
61
|
+
status: 'superseded',
|
|
62
|
+
};
|
|
63
|
+
// Replace in map but keep original in entries array for history
|
|
64
|
+
this.entryMap.set(entry.supersedes, updatedEntry);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.entries.push(entry);
|
|
69
|
+
this.entryMap.set(entry.id, entry);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get an entry by ID.
|
|
74
|
+
*
|
|
75
|
+
* @param id The entry ID
|
|
76
|
+
* @returns The entry, or undefined if not found
|
|
77
|
+
*/
|
|
78
|
+
getEntry(id: string): LedgerEntry | undefined {
|
|
79
|
+
return this.entryMap.get(id);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get all entries (in order of append) with current status.
|
|
84
|
+
*
|
|
85
|
+
* @returns Array of all entries with current status from the map
|
|
86
|
+
*/
|
|
87
|
+
getAllEntries(): LedgerEntry[] {
|
|
88
|
+
// Return entries with current status from the map
|
|
89
|
+
return this.entries.map((entry) => this.entryMap.get(entry.id)!);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get entries for a specific rule ID.
|
|
94
|
+
*
|
|
95
|
+
* @param ruleId The rule ID
|
|
96
|
+
* @returns Array of entries for this rule with current status
|
|
97
|
+
*/
|
|
98
|
+
getEntriesForRule(ruleId: string): LedgerEntry[] {
|
|
99
|
+
return this.entries
|
|
100
|
+
.map((entry) => this.entryMap.get(entry.id)!)
|
|
101
|
+
.filter((entry) => entry.contract.ruleId === ruleId);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get the latest active entry for a rule.
|
|
106
|
+
*
|
|
107
|
+
* @param ruleId The rule ID
|
|
108
|
+
* @returns The latest active entry, or undefined if none
|
|
109
|
+
*/
|
|
110
|
+
getLatestEntry(ruleId: string): LedgerEntry | undefined {
|
|
111
|
+
const entries = this.getEntriesForRule(ruleId);
|
|
112
|
+
const activeEntries = entries.filter((entry) => entry.status === 'active');
|
|
113
|
+
|
|
114
|
+
if (activeEntries.length === 0) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Return the most recent active entry
|
|
119
|
+
return activeEntries[activeEntries.length - 1];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get all active assumptions across all entries.
|
|
124
|
+
*
|
|
125
|
+
* @returns Map of assumption ID to assumption
|
|
126
|
+
*/
|
|
127
|
+
getActiveAssumptions(): Map<string, Assumption> {
|
|
128
|
+
const assumptions = new Map<string, Assumption>();
|
|
129
|
+
|
|
130
|
+
for (const entry of this.entries) {
|
|
131
|
+
// Get current status from map
|
|
132
|
+
const currentEntry = this.entryMap.get(entry.id)!;
|
|
133
|
+
if (currentEntry.status !== 'active') {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for (const assumption of currentEntry.contract.assumptions || []) {
|
|
138
|
+
if (assumption.status === 'active') {
|
|
139
|
+
assumptions.set(assumption.id, assumption);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return assumptions;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Find assumptions that impact a specific artifact type.
|
|
149
|
+
*
|
|
150
|
+
* @param impactType The artifact type ('spec', 'tests', 'code')
|
|
151
|
+
* @returns Array of assumptions
|
|
152
|
+
*/
|
|
153
|
+
findAssumptionsByImpact(impactType: 'spec' | 'tests' | 'code'): Assumption[] {
|
|
154
|
+
const assumptions: Assumption[] = [];
|
|
155
|
+
|
|
156
|
+
for (const entry of this.entries) {
|
|
157
|
+
// Get current status from map
|
|
158
|
+
const currentEntry = this.entryMap.get(entry.id)!;
|
|
159
|
+
if (currentEntry.status !== 'active') {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
for (const assumption of currentEntry.contract.assumptions || []) {
|
|
164
|
+
if (assumption.status === 'active' && assumption.impacts.includes(impactType)) {
|
|
165
|
+
assumptions.push(assumption);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return assumptions;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get ledger statistics.
|
|
175
|
+
*/
|
|
176
|
+
getStats(): {
|
|
177
|
+
totalEntries: number;
|
|
178
|
+
activeEntries: number;
|
|
179
|
+
supersededEntries: number;
|
|
180
|
+
deprecatedEntries: number;
|
|
181
|
+
uniqueRules: number;
|
|
182
|
+
} {
|
|
183
|
+
// Get current status from map for all entries
|
|
184
|
+
const currentEntries = this.entries.map((e) => this.entryMap.get(e.id)!);
|
|
185
|
+
const active = currentEntries.filter((e) => e.status === 'active').length;
|
|
186
|
+
const superseded = currentEntries.filter((e) => e.status === 'superseded').length;
|
|
187
|
+
const deprecated = currentEntries.filter((e) => e.status === 'deprecated').length;
|
|
188
|
+
const uniqueRules = new Set(currentEntries.map((e) => e.contract.ruleId)).size;
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
totalEntries: this.entries.length,
|
|
192
|
+
activeEntries: active,
|
|
193
|
+
supersededEntries: superseded,
|
|
194
|
+
deprecatedEntries: deprecated,
|
|
195
|
+
uniqueRules,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Export ledger as JSON.
|
|
201
|
+
*
|
|
202
|
+
* @returns JSON string with current entry status
|
|
203
|
+
*/
|
|
204
|
+
toJSON(): string {
|
|
205
|
+
return JSON.stringify(
|
|
206
|
+
{
|
|
207
|
+
version: '1.0.0',
|
|
208
|
+
// Export entries with current status from the map
|
|
209
|
+
entries: this.entries.map((entry) => this.entryMap.get(entry.id)!),
|
|
210
|
+
stats: this.getStats(),
|
|
211
|
+
},
|
|
212
|
+
null,
|
|
213
|
+
2
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Import ledger from JSON.
|
|
219
|
+
*
|
|
220
|
+
* Note: The JSON must contain entries in the order they were originally appended.
|
|
221
|
+
* If a superseding entry appears before the entry it supersedes, the superseding
|
|
222
|
+
* logic will not work correctly. The toJSON method preserves this order.
|
|
223
|
+
*
|
|
224
|
+
* @param json The JSON string
|
|
225
|
+
* @returns A new BehaviorLedger instance
|
|
226
|
+
*/
|
|
227
|
+
static fromJSON(json: string): BehaviorLedger {
|
|
228
|
+
const data = JSON.parse(json);
|
|
229
|
+
const ledger = new BehaviorLedger();
|
|
230
|
+
|
|
231
|
+
for (const entry of data.entries || []) {
|
|
232
|
+
ledger.append(entry);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return ledger;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Create a new behavior ledger.
|
|
241
|
+
*
|
|
242
|
+
* @returns A new empty ledger
|
|
243
|
+
*/
|
|
244
|
+
export function createBehaviorLedger(): BehaviorLedger {
|
|
245
|
+
return new BehaviorLedger();
|
|
246
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decision Ledger - Logic Ledger Writer
|
|
3
|
+
*
|
|
4
|
+
* Immutable, append-only ledger persisted to disk with index and LATEST snapshots.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
import { promises as fs } from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import type { Contract, Assumption } from './types.js';
|
|
11
|
+
|
|
12
|
+
export interface LogicLedgerEntry {
|
|
13
|
+
ruleId: string;
|
|
14
|
+
version: number;
|
|
15
|
+
timestamp: string;
|
|
16
|
+
canonicalBehavior: {
|
|
17
|
+
behavior: string;
|
|
18
|
+
examples: Contract['examples'];
|
|
19
|
+
invariants: string[];
|
|
20
|
+
};
|
|
21
|
+
assumptions: Assumption[];
|
|
22
|
+
artifacts: {
|
|
23
|
+
contractPresent: boolean;
|
|
24
|
+
testsPresent: boolean;
|
|
25
|
+
specPresent: boolean;
|
|
26
|
+
};
|
|
27
|
+
drift: {
|
|
28
|
+
changeSummary: string;
|
|
29
|
+
assumptionsInvalidated: string[];
|
|
30
|
+
assumptionsRevised: string[];
|
|
31
|
+
conflicts: string[];
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface LogicLedgerWriteOptions {
|
|
36
|
+
rootDir: string;
|
|
37
|
+
author: string;
|
|
38
|
+
testsPresent?: boolean;
|
|
39
|
+
specPresent?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface LogicLedgerIndex {
|
|
43
|
+
byRuleId: Record<string, string>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function writeLogicLedgerEntry(
|
|
47
|
+
contract: Contract,
|
|
48
|
+
options: LogicLedgerWriteOptions
|
|
49
|
+
): Promise<LogicLedgerEntry> {
|
|
50
|
+
const rootDir = options.rootDir;
|
|
51
|
+
const ledgerId = normalizeRuleId(contract.ruleId);
|
|
52
|
+
const ledgerDir = path.join(rootDir, 'logic-ledger', ledgerId);
|
|
53
|
+
await fs.mkdir(ledgerDir, { recursive: true });
|
|
54
|
+
|
|
55
|
+
const latestPath = path.join(ledgerDir, 'LATEST.json');
|
|
56
|
+
const latest = await readJson<LogicLedgerEntry | null>(latestPath, null);
|
|
57
|
+
const nextVersion = latest ? latest.version + 1 : 1;
|
|
58
|
+
|
|
59
|
+
const entry: LogicLedgerEntry = {
|
|
60
|
+
ruleId: contract.ruleId,
|
|
61
|
+
version: nextVersion,
|
|
62
|
+
timestamp: new Date().toISOString(),
|
|
63
|
+
canonicalBehavior: {
|
|
64
|
+
behavior: contract.behavior,
|
|
65
|
+
examples: contract.examples,
|
|
66
|
+
invariants: contract.invariants,
|
|
67
|
+
},
|
|
68
|
+
assumptions: contract.assumptions ?? [],
|
|
69
|
+
artifacts: {
|
|
70
|
+
contractPresent: true,
|
|
71
|
+
testsPresent: options.testsPresent ?? false,
|
|
72
|
+
specPresent: options.specPresent ?? false,
|
|
73
|
+
},
|
|
74
|
+
drift: computeDrift(latest, contract),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const versionFile = path.join(ledgerDir, `v${String(nextVersion).padStart(4, '0')}.json`);
|
|
78
|
+
await fs.writeFile(versionFile, JSON.stringify(entry, null, 2));
|
|
79
|
+
await fs.writeFile(latestPath, JSON.stringify(entry, null, 2));
|
|
80
|
+
|
|
81
|
+
await updateIndex(rootDir, contract.ruleId, path.relative(rootDir, ledgerDir));
|
|
82
|
+
|
|
83
|
+
return entry;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function computeDrift(previous: LogicLedgerEntry | null, contract: Contract): LogicLedgerEntry['drift'] {
|
|
87
|
+
if (!previous) {
|
|
88
|
+
return {
|
|
89
|
+
changeSummary: 'initial',
|
|
90
|
+
assumptionsInvalidated: [],
|
|
91
|
+
assumptionsRevised: [],
|
|
92
|
+
conflicts: [],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const conflicts: string[] = [];
|
|
97
|
+
const previousBehavior = JSON.stringify(previous.canonicalBehavior);
|
|
98
|
+
const nextBehavior = JSON.stringify({
|
|
99
|
+
behavior: contract.behavior,
|
|
100
|
+
examples: contract.examples,
|
|
101
|
+
invariants: contract.invariants,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (previousBehavior !== nextBehavior) {
|
|
105
|
+
conflicts.push('behavior-changed');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const prevAssumptions = new Map(previous.assumptions.map((a) => [a.id, a]));
|
|
109
|
+
const assumptionsInvalidated: string[] = [];
|
|
110
|
+
const assumptionsRevised: string[] = [];
|
|
111
|
+
|
|
112
|
+
for (const assumption of contract.assumptions ?? []) {
|
|
113
|
+
const prior = prevAssumptions.get(assumption.id);
|
|
114
|
+
if (!prior) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (prior.statement !== assumption.statement || prior.status !== assumption.status) {
|
|
119
|
+
assumptionsRevised.push(assumption.id);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const prior of previous.assumptions) {
|
|
124
|
+
const current = (contract.assumptions ?? []).find((a) => a.id === prior.id);
|
|
125
|
+
if (!current || current.status === 'invalidated') {
|
|
126
|
+
assumptionsInvalidated.push(prior.id);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
changeSummary: conflicts.length > 0 ? 'updated' : 'no-change',
|
|
132
|
+
assumptionsInvalidated,
|
|
133
|
+
assumptionsRevised,
|
|
134
|
+
conflicts,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function updateIndex(rootDir: string, ruleId: string, ledgerDir: string): Promise<void> {
|
|
139
|
+
const indexPath = path.join(rootDir, 'logic-ledger', 'index.json');
|
|
140
|
+
const index = await readJson<LogicLedgerIndex>(indexPath, { byRuleId: {} });
|
|
141
|
+
index.byRuleId[ruleId] = ledgerDir.replace(/\\/g, '/');
|
|
142
|
+
await fs.mkdir(path.dirname(indexPath), { recursive: true });
|
|
143
|
+
await fs.writeFile(indexPath, JSON.stringify(index, null, 2));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function normalizeRuleId(ruleId: string): string {
|
|
147
|
+
const hash = createHash('sha256').update(ruleId).digest('hex').slice(0, 6);
|
|
148
|
+
return `${ruleId.replace(/[^a-zA-Z0-9_-]/g, '-')}-${hash}`.toLowerCase();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function readJson<T>(filePath: string, fallback: T): Promise<T> {
|
|
152
|
+
try {
|
|
153
|
+
const data = await fs.readFile(filePath, 'utf-8');
|
|
154
|
+
return JSON.parse(data) as T;
|
|
155
|
+
} catch {
|
|
156
|
+
return fallback;
|
|
157
|
+
}
|
|
158
|
+
}
|