@plures/praxis 1.1.3 → 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/FRAMEWORK.md +106 -15
- package/README.md +194 -119
- package/dist/browser/adapter-CIMBGDC7.js +14 -0
- package/dist/browser/chunk-K377RW4V.js +230 -0
- package/dist/browser/chunk-MBVHLOU2.js +152 -0
- package/dist/browser/{chunk-R45WXWKH.js → chunk-VOMLVI6V.js} +1 -149
- package/dist/browser/engine-YJZV4SLD.js +8 -0
- package/dist/browser/index.d.ts +161 -5
- package/dist/browser/index.js +156 -141
- package/dist/browser/integrations/svelte.d.ts +2 -2
- package/dist/browser/integrations/svelte.js +2 -1
- package/dist/browser/{reactive-engine.svelte-C9OpcTHf.d.ts → reactive-engine.svelte-9aS0kTa8.d.ts} +136 -1
- package/dist/node/adapter-75ISSMWD.js +15 -0
- package/dist/node/chunk-5RH7UAQC.js +486 -0
- package/dist/node/chunk-MBVHLOU2.js +152 -0
- package/dist/node/chunk-PRPQO6R5.js +85 -0
- package/dist/node/chunk-R2PSBPKQ.js +150 -0
- package/dist/node/chunk-S54337I5.js +446 -0
- package/dist/node/{chunk-R45WXWKH.js → chunk-VOMLVI6V.js} +1 -149
- package/dist/node/chunk-WZ6B3LZ6.js +638 -0
- package/dist/node/cli/index.cjs +2936 -897
- package/dist/node/cli/index.js +27 -0
- package/dist/node/components/index.d.cts +3 -2
- package/dist/node/components/index.d.ts +3 -2
- package/dist/node/docs-JFNYTOJA.js +102 -0
- package/dist/node/engine-2DQBKBJC.js +9 -0
- package/dist/node/index.cjs +1114 -354
- package/dist/node/index.d.cts +388 -5
- package/dist/node/index.d.ts +388 -5
- package/dist/node/index.js +201 -640
- 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 +3 -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/docs.ts +147 -0
- package/src/cli/commands/reverse.ts +289 -0
- package/src/cli/commands/validate.ts +264 -0
- package/src/cli/index.ts +68 -0
- package/src/core/pluresdb/adapter.ts +46 -3
- package/src/core/reactive-engine.svelte.ts +6 -1
- package/src/core/reactive-engine.ts +1 -1
- 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 +6 -0
- package/src/index.ts +40 -0
- package/src/integrations/pluresdb.ts +14 -2
- package/src/integrations/unified.ts +350 -0
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
// src/decision-ledger/types.ts
|
|
2
|
+
function isContract(obj) {
|
|
3
|
+
if (typeof obj !== "object" || obj === null) {
|
|
4
|
+
return false;
|
|
5
|
+
}
|
|
6
|
+
const contract = obj;
|
|
7
|
+
return typeof contract.ruleId === "string" && typeof contract.behavior === "string" && Array.isArray(contract.examples) && contract.examples.length > 0 && contract.examples.every(
|
|
8
|
+
(ex) => typeof ex === "object" && ex !== null && typeof ex.given === "string" && typeof ex.when === "string" && typeof ex.then === "string"
|
|
9
|
+
) && Array.isArray(contract.invariants) && contract.invariants.every((inv) => typeof inv === "string");
|
|
10
|
+
}
|
|
11
|
+
function defineContract(options) {
|
|
12
|
+
if (options.examples.length === 0) {
|
|
13
|
+
throw new Error("Contract must have at least one example");
|
|
14
|
+
}
|
|
15
|
+
if (options.assumptions) {
|
|
16
|
+
for (const assumption of options.assumptions) {
|
|
17
|
+
if (assumption.confidence < 0 || assumption.confidence > 1) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
`Assumption '${assumption.id}' has invalid confidence value ${assumption.confidence}. Must be between 0.0 and 1.0`
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
ruleId: options.ruleId,
|
|
26
|
+
behavior: options.behavior,
|
|
27
|
+
examples: options.examples,
|
|
28
|
+
invariants: options.invariants,
|
|
29
|
+
assumptions: options.assumptions,
|
|
30
|
+
references: options.references,
|
|
31
|
+
version: options.version || "1.0.0",
|
|
32
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function getContract(meta) {
|
|
36
|
+
if (!meta || !meta.contract) {
|
|
37
|
+
return void 0;
|
|
38
|
+
}
|
|
39
|
+
if (isContract(meta.contract)) {
|
|
40
|
+
return meta.contract;
|
|
41
|
+
}
|
|
42
|
+
return void 0;
|
|
43
|
+
}
|
|
44
|
+
function getContractFromDescriptor(descriptor) {
|
|
45
|
+
if (!descriptor) {
|
|
46
|
+
return void 0;
|
|
47
|
+
}
|
|
48
|
+
if (descriptor.contract && isContract(descriptor.contract)) {
|
|
49
|
+
return descriptor.contract;
|
|
50
|
+
}
|
|
51
|
+
return getContract(descriptor.meta);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/decision-ledger/logic-ledger.ts
|
|
55
|
+
import { createHash } from "crypto";
|
|
56
|
+
import { promises as fs } from "fs";
|
|
57
|
+
import path from "path";
|
|
58
|
+
async function writeLogicLedgerEntry(contract, options) {
|
|
59
|
+
const rootDir = options.rootDir;
|
|
60
|
+
const ledgerId = normalizeRuleId(contract.ruleId);
|
|
61
|
+
const ledgerDir = path.join(rootDir, "logic-ledger", ledgerId);
|
|
62
|
+
await fs.mkdir(ledgerDir, { recursive: true });
|
|
63
|
+
const latestPath = path.join(ledgerDir, "LATEST.json");
|
|
64
|
+
const latest = await readJson(latestPath, null);
|
|
65
|
+
const nextVersion = latest ? latest.version + 1 : 1;
|
|
66
|
+
const entry = {
|
|
67
|
+
ruleId: contract.ruleId,
|
|
68
|
+
version: nextVersion,
|
|
69
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
70
|
+
canonicalBehavior: {
|
|
71
|
+
behavior: contract.behavior,
|
|
72
|
+
examples: contract.examples,
|
|
73
|
+
invariants: contract.invariants
|
|
74
|
+
},
|
|
75
|
+
assumptions: contract.assumptions ?? [],
|
|
76
|
+
artifacts: {
|
|
77
|
+
contractPresent: true,
|
|
78
|
+
testsPresent: options.testsPresent ?? false,
|
|
79
|
+
specPresent: options.specPresent ?? false
|
|
80
|
+
},
|
|
81
|
+
drift: computeDrift(latest, contract)
|
|
82
|
+
};
|
|
83
|
+
const versionFile = path.join(ledgerDir, `v${String(nextVersion).padStart(4, "0")}.json`);
|
|
84
|
+
await fs.writeFile(versionFile, JSON.stringify(entry, null, 2));
|
|
85
|
+
await fs.writeFile(latestPath, JSON.stringify(entry, null, 2));
|
|
86
|
+
await updateIndex(rootDir, contract.ruleId, path.relative(rootDir, ledgerDir));
|
|
87
|
+
return entry;
|
|
88
|
+
}
|
|
89
|
+
function computeDrift(previous, contract) {
|
|
90
|
+
if (!previous) {
|
|
91
|
+
return {
|
|
92
|
+
changeSummary: "initial",
|
|
93
|
+
assumptionsInvalidated: [],
|
|
94
|
+
assumptionsRevised: [],
|
|
95
|
+
conflicts: []
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const conflicts = [];
|
|
99
|
+
const previousBehavior = JSON.stringify(previous.canonicalBehavior);
|
|
100
|
+
const nextBehavior = JSON.stringify({
|
|
101
|
+
behavior: contract.behavior,
|
|
102
|
+
examples: contract.examples,
|
|
103
|
+
invariants: contract.invariants
|
|
104
|
+
});
|
|
105
|
+
if (previousBehavior !== nextBehavior) {
|
|
106
|
+
conflicts.push("behavior-changed");
|
|
107
|
+
}
|
|
108
|
+
const prevAssumptions = new Map(previous.assumptions.map((a) => [a.id, a]));
|
|
109
|
+
const assumptionsInvalidated = [];
|
|
110
|
+
const assumptionsRevised = [];
|
|
111
|
+
for (const assumption of contract.assumptions ?? []) {
|
|
112
|
+
const prior = prevAssumptions.get(assumption.id);
|
|
113
|
+
if (!prior) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (prior.statement !== assumption.statement || prior.status !== assumption.status) {
|
|
117
|
+
assumptionsRevised.push(assumption.id);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (const prior of previous.assumptions) {
|
|
121
|
+
const current = (contract.assumptions ?? []).find((a) => a.id === prior.id);
|
|
122
|
+
if (!current || current.status === "invalidated") {
|
|
123
|
+
assumptionsInvalidated.push(prior.id);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
changeSummary: conflicts.length > 0 ? "updated" : "no-change",
|
|
128
|
+
assumptionsInvalidated,
|
|
129
|
+
assumptionsRevised,
|
|
130
|
+
conflicts
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
async function updateIndex(rootDir, ruleId, ledgerDir) {
|
|
134
|
+
const indexPath = path.join(rootDir, "logic-ledger", "index.json");
|
|
135
|
+
const index = await readJson(indexPath, { byRuleId: {} });
|
|
136
|
+
index.byRuleId[ruleId] = ledgerDir.replace(/\\/g, "/");
|
|
137
|
+
await fs.mkdir(path.dirname(indexPath), { recursive: true });
|
|
138
|
+
await fs.writeFile(indexPath, JSON.stringify(index, null, 2));
|
|
139
|
+
}
|
|
140
|
+
function normalizeRuleId(ruleId) {
|
|
141
|
+
const hash = createHash("sha256").update(ruleId).digest("hex").slice(0, 6);
|
|
142
|
+
return `${ruleId.replace(/[^a-zA-Z0-9_-]/g, "-")}-${hash}`.toLowerCase();
|
|
143
|
+
}
|
|
144
|
+
async function readJson(filePath, fallback) {
|
|
145
|
+
try {
|
|
146
|
+
const data = await fs.readFile(filePath, "utf-8");
|
|
147
|
+
return JSON.parse(data);
|
|
148
|
+
} catch {
|
|
149
|
+
return fallback;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/decision-ledger/scanner.ts
|
|
154
|
+
import { promises as fs2 } from "fs";
|
|
155
|
+
import path2 from "path";
|
|
156
|
+
async function scanRepository(options) {
|
|
157
|
+
const startTime = Date.now();
|
|
158
|
+
const { rootDir, scanTests = true, scanSpecs = true, maxDepth = 10 } = options;
|
|
159
|
+
const normalizedRoot = path2.resolve(rootDir);
|
|
160
|
+
try {
|
|
161
|
+
const stats = await fs2.stat(normalizedRoot);
|
|
162
|
+
if (!stats.isDirectory()) {
|
|
163
|
+
throw new Error(`Path is not a directory: ${normalizedRoot}`);
|
|
164
|
+
}
|
|
165
|
+
} catch (error) {
|
|
166
|
+
throw new Error(`Invalid root directory: ${normalizedRoot} - ${error instanceof Error ? error.message : String(error)}`);
|
|
167
|
+
}
|
|
168
|
+
const rules = [];
|
|
169
|
+
const constraints = [];
|
|
170
|
+
const testFiles = /* @__PURE__ */ new Map();
|
|
171
|
+
const specFiles = /* @__PURE__ */ new Map();
|
|
172
|
+
const scanWarnings = [];
|
|
173
|
+
let filesScanned = 0;
|
|
174
|
+
const implFiles = await findFiles(
|
|
175
|
+
normalizedRoot,
|
|
176
|
+
{
|
|
177
|
+
include: options.include || ["**/*.ts", "**/*.js"],
|
|
178
|
+
exclude: options.exclude || [
|
|
179
|
+
"**/node_modules/**",
|
|
180
|
+
"**/dist/**",
|
|
181
|
+
"**/build/**",
|
|
182
|
+
"**/*.test.ts",
|
|
183
|
+
"**/*.test.js",
|
|
184
|
+
"**/*.spec.ts",
|
|
185
|
+
"**/*.spec.js"
|
|
186
|
+
],
|
|
187
|
+
maxDepth
|
|
188
|
+
},
|
|
189
|
+
scanWarnings
|
|
190
|
+
);
|
|
191
|
+
for (const file of implFiles) {
|
|
192
|
+
filesScanned++;
|
|
193
|
+
const stats = await fs2.stat(file);
|
|
194
|
+
const maxFileSize = 10 * 1024 * 1024;
|
|
195
|
+
if (stats.size > maxFileSize) {
|
|
196
|
+
scanWarnings.push(`Skipping large file (${(stats.size / 1024 / 1024).toFixed(2)} MB): ${file}`);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const content = await fs2.readFile(file, "utf-8");
|
|
200
|
+
const discoveredRules = await extractRulesFromFile(file, content);
|
|
201
|
+
const discoveredConstraints = await extractConstraintsFromFile(file, content);
|
|
202
|
+
rules.push(...discoveredRules);
|
|
203
|
+
constraints.push(...discoveredConstraints);
|
|
204
|
+
}
|
|
205
|
+
if (scanTests) {
|
|
206
|
+
const testFileList = await findFiles(
|
|
207
|
+
normalizedRoot,
|
|
208
|
+
{
|
|
209
|
+
include: ["**/*.test.ts", "**/*.test.js", "**/*.spec.ts", "**/*.spec.js"],
|
|
210
|
+
exclude: ["**/node_modules/**", "**/dist/**", "**/build/**"],
|
|
211
|
+
maxDepth
|
|
212
|
+
},
|
|
213
|
+
scanWarnings
|
|
214
|
+
);
|
|
215
|
+
for (const testFile of testFileList) {
|
|
216
|
+
filesScanned++;
|
|
217
|
+
const stats = await fs2.stat(testFile);
|
|
218
|
+
const maxFileSize = 10 * 1024 * 1024;
|
|
219
|
+
if (stats.size > maxFileSize) {
|
|
220
|
+
scanWarnings.push(`Skipping large test file (${(stats.size / 1024 / 1024).toFixed(2)} MB): ${testFile}`);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const content = await fs2.readFile(testFile, "utf-8");
|
|
224
|
+
const mappings = await mapTestsToRules(testFile, content, rules);
|
|
225
|
+
for (const [ruleId, filePath] of mappings) {
|
|
226
|
+
if (!testFiles.has(ruleId)) {
|
|
227
|
+
testFiles.set(ruleId, []);
|
|
228
|
+
}
|
|
229
|
+
testFiles.get(ruleId).push(filePath);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (scanSpecs) {
|
|
234
|
+
const specFileList = await findFiles(
|
|
235
|
+
normalizedRoot,
|
|
236
|
+
{
|
|
237
|
+
include: ["**/*.tla", "**/*.md", "**/spec/**/*.ts"],
|
|
238
|
+
exclude: ["**/node_modules/**", "**/dist/**", "**/build/**"],
|
|
239
|
+
maxDepth
|
|
240
|
+
},
|
|
241
|
+
scanWarnings
|
|
242
|
+
);
|
|
243
|
+
for (const specFile of specFileList) {
|
|
244
|
+
filesScanned++;
|
|
245
|
+
const stats = await fs2.stat(specFile);
|
|
246
|
+
const maxFileSize = 10 * 1024 * 1024;
|
|
247
|
+
if (stats.size > maxFileSize) {
|
|
248
|
+
scanWarnings.push(`Skipping large spec file (${(stats.size / 1024 / 1024).toFixed(2)} MB): ${specFile}`);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
const content = await fs2.readFile(specFile, "utf-8");
|
|
252
|
+
const mappings = await mapSpecsToRules(specFile, content, rules);
|
|
253
|
+
for (const [ruleId, filePath] of mappings) {
|
|
254
|
+
if (!specFiles.has(ruleId)) {
|
|
255
|
+
specFiles.set(ruleId, []);
|
|
256
|
+
}
|
|
257
|
+
specFiles.get(ruleId).push(filePath);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const duration = Date.now() - startTime;
|
|
262
|
+
return {
|
|
263
|
+
rules,
|
|
264
|
+
constraints,
|
|
265
|
+
testFiles,
|
|
266
|
+
specFiles,
|
|
267
|
+
warnings: scanWarnings,
|
|
268
|
+
filesScanned,
|
|
269
|
+
duration
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
async function extractRulesFromFile(filePath, content) {
|
|
273
|
+
const rules = [];
|
|
274
|
+
const defineRulePattern = /defineRule\s*\(\s*\{([^}]+)\}\s*\)/g;
|
|
275
|
+
let match;
|
|
276
|
+
while ((match = defineRulePattern.exec(content)) !== null) {
|
|
277
|
+
const ruleConfig = match[1];
|
|
278
|
+
const idMatch = /id:\s*['"]([^'"]+)['"]/.exec(ruleConfig);
|
|
279
|
+
const descMatch = /description:\s*['"]([^'"]+)['"]/.exec(ruleConfig);
|
|
280
|
+
if (idMatch) {
|
|
281
|
+
const id = idMatch[1];
|
|
282
|
+
const description = descMatch ? descMatch[1] : "";
|
|
283
|
+
rules.push({
|
|
284
|
+
id,
|
|
285
|
+
description,
|
|
286
|
+
impl: () => [],
|
|
287
|
+
// Placeholder
|
|
288
|
+
meta: {
|
|
289
|
+
sourceFile: filePath,
|
|
290
|
+
discovered: true
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return rules;
|
|
296
|
+
}
|
|
297
|
+
async function extractConstraintsFromFile(filePath, content) {
|
|
298
|
+
const constraints = [];
|
|
299
|
+
const defineConstraintPattern = /defineConstraint\s*\(\s*\{([^}]+)\}\s*\)/g;
|
|
300
|
+
let match;
|
|
301
|
+
while ((match = defineConstraintPattern.exec(content)) !== null) {
|
|
302
|
+
const constraintConfig = match[1];
|
|
303
|
+
const idMatch = /id:\s*['"]([^'"]+)['"]/.exec(constraintConfig);
|
|
304
|
+
const descMatch = /description:\s*['"]([^'"]+)['"]/.exec(constraintConfig);
|
|
305
|
+
if (idMatch) {
|
|
306
|
+
const id = idMatch[1];
|
|
307
|
+
const description = descMatch ? descMatch[1] : "";
|
|
308
|
+
constraints.push({
|
|
309
|
+
id,
|
|
310
|
+
description,
|
|
311
|
+
impl: () => true,
|
|
312
|
+
// Placeholder
|
|
313
|
+
meta: {
|
|
314
|
+
sourceFile: filePath,
|
|
315
|
+
discovered: true
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return constraints;
|
|
321
|
+
}
|
|
322
|
+
async function mapTestsToRules(testFile, content, rules) {
|
|
323
|
+
const mappings = /* @__PURE__ */ new Map();
|
|
324
|
+
for (const rule of rules) {
|
|
325
|
+
if (content.includes(rule.id)) {
|
|
326
|
+
mappings.set(rule.id, testFile);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return mappings;
|
|
330
|
+
}
|
|
331
|
+
async function mapSpecsToRules(specFile, content, rules) {
|
|
332
|
+
const mappings = /* @__PURE__ */ new Map();
|
|
333
|
+
for (const rule of rules) {
|
|
334
|
+
if (content.includes(rule.id)) {
|
|
335
|
+
mappings.set(rule.id, specFile);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return mappings;
|
|
339
|
+
}
|
|
340
|
+
async function findFiles(rootDir, options, warnings) {
|
|
341
|
+
const files = [];
|
|
342
|
+
async function walk(dir, depth) {
|
|
343
|
+
if (depth > options.maxDepth) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
const entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
348
|
+
for (const entry of entries) {
|
|
349
|
+
const fullPath = path2.join(dir, entry.name);
|
|
350
|
+
const relativePath = path2.relative(rootDir, fullPath);
|
|
351
|
+
if (shouldExclude(relativePath, options.exclude)) {
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (entry.isDirectory()) {
|
|
355
|
+
await walk(fullPath, depth + 1);
|
|
356
|
+
} else if (entry.isFile()) {
|
|
357
|
+
if (shouldInclude(relativePath, options.include)) {
|
|
358
|
+
files.push(fullPath);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
} catch (error) {
|
|
363
|
+
if (error instanceof Error && "code" in error) {
|
|
364
|
+
const nodeError = error;
|
|
365
|
+
if (nodeError.code === "EACCES" || nodeError.code === "EPERM") {
|
|
366
|
+
warnings.push(`Permission denied: ${dir}`);
|
|
367
|
+
} else {
|
|
368
|
+
warnings.push(`Error scanning ${dir}: ${error.message}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
await walk(rootDir, 0);
|
|
374
|
+
return files;
|
|
375
|
+
}
|
|
376
|
+
function shouldExclude(relativePath, patterns) {
|
|
377
|
+
return patterns.some((pattern) => {
|
|
378
|
+
const regex = globToRegex(pattern);
|
|
379
|
+
return regex.test(relativePath);
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
function shouldInclude(relativePath, patterns) {
|
|
383
|
+
return patterns.some((pattern) => {
|
|
384
|
+
const regex = globToRegex(pattern);
|
|
385
|
+
return regex.test(relativePath);
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
function globToRegex(pattern) {
|
|
389
|
+
let regexPattern = pattern.replace(/\\/g, "\\\\").replace(/\./g, "\\.").replace(/\*\*/g, "___DOUBLESTAR___").replace(/\*/g, "[^/]*").replace(/___DOUBLESTAR___/g, ".*").replace(/\?/g, ".");
|
|
390
|
+
return new RegExp(`^${regexPattern}$`);
|
|
391
|
+
}
|
|
392
|
+
async function inferContractFromFile(filePath, ruleId) {
|
|
393
|
+
const content = await fs2.readFile(filePath, "utf-8");
|
|
394
|
+
const behavior = inferBehavior(content, ruleId);
|
|
395
|
+
const invariants = inferInvariants(content);
|
|
396
|
+
return {
|
|
397
|
+
ruleId,
|
|
398
|
+
behavior,
|
|
399
|
+
invariants,
|
|
400
|
+
examples: []
|
|
401
|
+
// Populated separately from tests
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
function inferBehavior(content, ruleId) {
|
|
405
|
+
const jsdocMatch = /\/\*\*\s*\n\s*\*\s*([^\n]+)/.exec(content);
|
|
406
|
+
if (jsdocMatch) {
|
|
407
|
+
return jsdocMatch[1].trim();
|
|
408
|
+
}
|
|
409
|
+
const descMatch = /description:\s*['"]([^'"]+)['"]/.exec(content);
|
|
410
|
+
if (descMatch) {
|
|
411
|
+
return descMatch[1];
|
|
412
|
+
}
|
|
413
|
+
return `Process ${ruleId} events`;
|
|
414
|
+
}
|
|
415
|
+
function inferInvariants(content) {
|
|
416
|
+
const invariants = [];
|
|
417
|
+
const assertPattern = /assert\s*\([^)]+,\s*['"]([^'"]+)['"]\)/g;
|
|
418
|
+
let match;
|
|
419
|
+
while ((match = assertPattern.exec(content)) !== null) {
|
|
420
|
+
invariants.push(match[1]);
|
|
421
|
+
}
|
|
422
|
+
return invariants;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// src/decision-ledger/reverse-generator.ts
|
|
426
|
+
async function generateContractFromRule(descriptor, options = {}) {
|
|
427
|
+
const {
|
|
428
|
+
aiProvider = "none",
|
|
429
|
+
confidenceThreshold = 0.7,
|
|
430
|
+
includeAssumptions = true,
|
|
431
|
+
generateExamples = true,
|
|
432
|
+
sourceFile,
|
|
433
|
+
testFiles = [],
|
|
434
|
+
specFiles = []
|
|
435
|
+
} = options;
|
|
436
|
+
const warnings = [];
|
|
437
|
+
if (aiProvider !== "none") {
|
|
438
|
+
try {
|
|
439
|
+
const aiResult = await generateWithAI(descriptor, options);
|
|
440
|
+
if (aiResult.confidence >= confidenceThreshold) {
|
|
441
|
+
return aiResult;
|
|
442
|
+
}
|
|
443
|
+
warnings.push(
|
|
444
|
+
`AI confidence ${aiResult.confidence} below threshold ${confidenceThreshold}, falling back to heuristic`
|
|
445
|
+
);
|
|
446
|
+
} catch (error) {
|
|
447
|
+
warnings.push(`AI generation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
const heuristicResult = await generateWithHeuristics(
|
|
451
|
+
descriptor,
|
|
452
|
+
{ sourceFile, testFiles, specFiles, includeAssumptions, generateExamples }
|
|
453
|
+
);
|
|
454
|
+
return {
|
|
455
|
+
...heuristicResult,
|
|
456
|
+
warnings: [...warnings, ...heuristicResult.warnings]
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
async function generateWithAI(descriptor, options) {
|
|
460
|
+
const { aiProvider, openaiApiKey, githubToken } = options;
|
|
461
|
+
if (aiProvider === "openai") {
|
|
462
|
+
if (!openaiApiKey) {
|
|
463
|
+
throw new Error("OpenAI API key is required for OpenAI provider");
|
|
464
|
+
}
|
|
465
|
+
return await generateWithOpenAI(descriptor, openaiApiKey, options);
|
|
466
|
+
}
|
|
467
|
+
if (aiProvider === "github-copilot") {
|
|
468
|
+
if (!githubToken) {
|
|
469
|
+
throw new Error("GitHub token is required for GitHub Copilot provider");
|
|
470
|
+
}
|
|
471
|
+
return await generateWithGitHubCopilot(descriptor, githubToken, options);
|
|
472
|
+
}
|
|
473
|
+
if (aiProvider === "auto") {
|
|
474
|
+
if (githubToken) {
|
|
475
|
+
return await generateWithGitHubCopilot(descriptor, githubToken, options);
|
|
476
|
+
}
|
|
477
|
+
if (openaiApiKey) {
|
|
478
|
+
return await generateWithOpenAI(descriptor, openaiApiKey, options);
|
|
479
|
+
}
|
|
480
|
+
throw new Error("Auto AI provider requires either GitHub token or OpenAI API key");
|
|
481
|
+
}
|
|
482
|
+
throw new Error(`Unsupported AI provider: ${aiProvider}`);
|
|
483
|
+
}
|
|
484
|
+
async function generateWithOpenAI(descriptor, _apiKey, options) {
|
|
485
|
+
return await generateWithHeuristics(descriptor, {
|
|
486
|
+
sourceFile: options.sourceFile,
|
|
487
|
+
testFiles: options.testFiles,
|
|
488
|
+
specFiles: options.specFiles,
|
|
489
|
+
includeAssumptions: options.includeAssumptions,
|
|
490
|
+
generateExamples: options.generateExamples
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
async function generateWithGitHubCopilot(descriptor, _token, options) {
|
|
494
|
+
return await generateWithHeuristics(descriptor, {
|
|
495
|
+
sourceFile: options.sourceFile,
|
|
496
|
+
testFiles: options.testFiles,
|
|
497
|
+
specFiles: options.specFiles,
|
|
498
|
+
includeAssumptions: options.includeAssumptions,
|
|
499
|
+
generateExamples: options.generateExamples
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
async function generateWithHeuristics(descriptor, options) {
|
|
503
|
+
const warnings = [];
|
|
504
|
+
const { sourceFile, testFiles = [], specFiles = [] } = options;
|
|
505
|
+
let behavior = descriptor.description || `Process ${descriptor.id}`;
|
|
506
|
+
let examples = [];
|
|
507
|
+
let invariants = [];
|
|
508
|
+
let assumptions = [];
|
|
509
|
+
let references = [];
|
|
510
|
+
if (sourceFile) {
|
|
511
|
+
try {
|
|
512
|
+
const inferred = await inferContractFromFile(sourceFile, descriptor.id);
|
|
513
|
+
if (inferred.behavior) {
|
|
514
|
+
behavior = inferred.behavior;
|
|
515
|
+
}
|
|
516
|
+
if (inferred.invariants && inferred.invariants.length > 0) {
|
|
517
|
+
invariants = inferred.invariants;
|
|
518
|
+
}
|
|
519
|
+
} catch (error) {
|
|
520
|
+
warnings.push(`Failed to analyze source file: ${error instanceof Error ? error.message : String(error)}`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
if (options.generateExamples && testFiles.length > 0) {
|
|
524
|
+
for (const testFile of testFiles) {
|
|
525
|
+
try {
|
|
526
|
+
const testExamples = await extractExamplesFromTests(testFile, descriptor.id);
|
|
527
|
+
examples.push(...testExamples);
|
|
528
|
+
} catch (error) {
|
|
529
|
+
warnings.push(`Failed to extract examples from ${testFile}: ${error instanceof Error ? error.message : String(error)}`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (examples.length === 0) {
|
|
534
|
+
examples.push({
|
|
535
|
+
given: `System is in a valid state`,
|
|
536
|
+
when: `${descriptor.id} is triggered`,
|
|
537
|
+
then: `Expected outcome is produced`
|
|
538
|
+
});
|
|
539
|
+
if (!options.generateExamples) {
|
|
540
|
+
warnings.push("Example generation disabled - using default example");
|
|
541
|
+
} else if (!testFiles || testFiles.length === 0) {
|
|
542
|
+
warnings.push("No test files provided - using default example");
|
|
543
|
+
} else {
|
|
544
|
+
warnings.push("No examples could be extracted from provided test files - using default example");
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (specFiles.length > 0) {
|
|
548
|
+
references = specFiles.map((file) => ({
|
|
549
|
+
type: "spec",
|
|
550
|
+
url: file,
|
|
551
|
+
description: `Specification for ${descriptor.id}`
|
|
552
|
+
}));
|
|
553
|
+
}
|
|
554
|
+
if (options.includeAssumptions) {
|
|
555
|
+
assumptions = generateDefaultAssumptions(descriptor);
|
|
556
|
+
}
|
|
557
|
+
const contract = defineContract({
|
|
558
|
+
ruleId: descriptor.id,
|
|
559
|
+
behavior,
|
|
560
|
+
examples,
|
|
561
|
+
invariants: invariants.length > 0 ? invariants : [`${descriptor.id} maintains system invariants`],
|
|
562
|
+
assumptions,
|
|
563
|
+
references
|
|
564
|
+
});
|
|
565
|
+
let confidence = 0.5;
|
|
566
|
+
const bonuses = [];
|
|
567
|
+
if (sourceFile) bonuses.push(0.1);
|
|
568
|
+
if (testFiles.length > 0) bonuses.push(0.2);
|
|
569
|
+
if (specFiles.length > 0) bonuses.push(0.1);
|
|
570
|
+
if (examples.length > 1) bonuses.push(0.1);
|
|
571
|
+
const totalBonus = Math.min(bonuses.reduce((sum, b) => sum + b, 0), 0.4);
|
|
572
|
+
confidence = confidence + totalBonus;
|
|
573
|
+
return {
|
|
574
|
+
contract,
|
|
575
|
+
confidence,
|
|
576
|
+
method: "heuristic",
|
|
577
|
+
warnings
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
async function extractExamplesFromTests(testFile, _ruleId) {
|
|
581
|
+
const fs3 = await import("fs/promises");
|
|
582
|
+
const content = await fs3.readFile(testFile, "utf-8");
|
|
583
|
+
const examples = [];
|
|
584
|
+
const testPattern = /(?:it|test)\s*\(\s*(?:'((?:\\'|[^'])*)'|"((?:\\"|[^"])*)"|`((?:\\`|[^`])*?)`)/g;
|
|
585
|
+
let match;
|
|
586
|
+
while ((match = testPattern.exec(content)) !== null) {
|
|
587
|
+
const description = match[1] ?? match[2] ?? match[3] ?? "";
|
|
588
|
+
if (description.includes("when") || description.includes("should")) {
|
|
589
|
+
examples.push(parseTestDescription(description));
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return examples;
|
|
593
|
+
}
|
|
594
|
+
function parseTestDescription(description) {
|
|
595
|
+
const parts = description.split(/\b(?:when|should)\b/i);
|
|
596
|
+
if (parts.length >= 2) {
|
|
597
|
+
return {
|
|
598
|
+
given: parts[0].trim() || "Initial state",
|
|
599
|
+
when: parts.length > 2 ? parts[1].trim() : "Action is triggered",
|
|
600
|
+
then: parts[parts.length - 1].trim() || "Expected outcome occurs"
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
return {
|
|
604
|
+
given: description,
|
|
605
|
+
when: "Action is triggered",
|
|
606
|
+
then: "Expected outcome occurs"
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
function generateDefaultAssumptions(descriptor) {
|
|
610
|
+
return [
|
|
611
|
+
{
|
|
612
|
+
id: `${descriptor.id}-assumption-1`,
|
|
613
|
+
statement: "Input data is valid and well-formed",
|
|
614
|
+
confidence: 0.8,
|
|
615
|
+
justification: "Standard assumption for rule processing",
|
|
616
|
+
impacts: ["tests", "code"],
|
|
617
|
+
status: "active"
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
id: `${descriptor.id}-assumption-2`,
|
|
621
|
+
statement: "System state is consistent before rule execution",
|
|
622
|
+
confidence: 0.7,
|
|
623
|
+
justification: "Required for deterministic rule behavior",
|
|
624
|
+
impacts: ["spec", "tests"],
|
|
625
|
+
status: "active"
|
|
626
|
+
}
|
|
627
|
+
];
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
export {
|
|
631
|
+
isContract,
|
|
632
|
+
defineContract,
|
|
633
|
+
getContract,
|
|
634
|
+
getContractFromDescriptor,
|
|
635
|
+
writeLogicLedgerEntry,
|
|
636
|
+
scanRepository,
|
|
637
|
+
generateContractFromRule
|
|
638
|
+
};
|