principles-disciple 1.122.0 → 1.124.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/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/core/code-implementation-storage.ts +0 -31
- package/src/core/principle-tree-ledger.ts +0 -13
- package/src/core/rule-host.ts +77 -153
- package/tests/core/code-implementation-storage.test.ts +16 -114
- package/tests/core/rule-host-sqlite-source.test.ts +720 -0
- package/tests/evolution-worker-quarantine.test.ts +6 -4
- package/tests/hooks/gate-auto-correct-shadow.test.ts +0 -1
- package/tests/hooks/gate-auto-correct.test.ts +0 -1
- package/tests/hooks/gate-no-path-write-tool.test.ts +0 -1
- package/tests/hooks/gate-rule-host-pipeline.test.ts +0 -1
- package/tests/integration/pain-id-chain-e2e.test.ts +59 -5
- package/tests/integration/principle-compiler-e2e.test.ts +62 -13
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "principles-disciple",
|
|
3
3
|
"name": "Principles Disciple",
|
|
4
4
|
"description": "Evolutionary programming agent framework with strategic guardrails and reflection loops.",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.124.0",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onCapabilities": [
|
|
8
8
|
"hook"
|
package/package.json
CHANGED
|
@@ -104,20 +104,6 @@ export function getImplementationAssetRoot(stateDir: string, implId: string): st
|
|
|
104
104
|
return path.join(stateDir, 'principles', 'implementations', implId);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
/**
|
|
108
|
-
* Load manifest from disk. Returns null if not found (does not throw).
|
|
109
|
-
*/
|
|
110
|
-
export function loadManifest(stateDir: string, implId: string): CodeImplementationManifest | null {
|
|
111
|
-
validateImplId(implId);
|
|
112
|
-
const manifestPath = path.join(getImplementationAssetRoot(stateDir, implId), MANIFEST_FILENAME);
|
|
113
|
-
if (!fs.existsSync(manifestPath)) return null;
|
|
114
|
-
try {
|
|
115
|
-
return JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) as CodeImplementationManifest;
|
|
116
|
-
} catch {
|
|
117
|
-
return null;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
107
|
/**
|
|
122
108
|
* Write manifest atomically using withLock.
|
|
123
109
|
*/
|
|
@@ -163,23 +149,6 @@ export function deleteImplementationAssetDir(stateDir: string, implId: string):
|
|
|
163
149
|
});
|
|
164
150
|
}
|
|
165
151
|
|
|
166
|
-
/**
|
|
167
|
-
* Load the entry source code from disk.
|
|
168
|
-
* Returns null if manifest doesn't exist or entry file is missing.
|
|
169
|
-
*/
|
|
170
|
-
export function loadEntrySource(stateDir: string, implId: string): string | null {
|
|
171
|
-
validateImplId(implId);
|
|
172
|
-
const manifest = loadManifest(stateDir, implId);
|
|
173
|
-
if (!manifest) return null;
|
|
174
|
-
const entryPath = path.join(getImplementationAssetRoot(stateDir, implId), manifest.entryFile);
|
|
175
|
-
if (!fs.existsSync(entryPath)) return null;
|
|
176
|
-
try {
|
|
177
|
-
return fs.readFileSync(entryPath, 'utf-8');
|
|
178
|
-
} catch {
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
152
|
/**
|
|
184
153
|
* Create the full asset directory structure for a new implementation.
|
|
185
154
|
*
|
|
@@ -701,19 +701,6 @@ export function transitionImplementationState(
|
|
|
701
701
|
});
|
|
702
702
|
}
|
|
703
703
|
|
|
704
|
-
/**
|
|
705
|
-
* Get all implementations for a specific lifecycle state across all rules.
|
|
706
|
-
*/
|
|
707
|
-
export function listImplementationsByLifecycleState(
|
|
708
|
-
stateDir: string,
|
|
709
|
-
state: ImplementationLifecycleState
|
|
710
|
-
): Implementation[] {
|
|
711
|
-
const ledger = loadLedger(stateDir);
|
|
712
|
-
return Object.values(ledger.tree.implementations).filter(
|
|
713
|
-
(impl) => impl.lifecycleState === state
|
|
714
|
-
);
|
|
715
|
-
}
|
|
716
|
-
|
|
717
704
|
/**
|
|
718
705
|
* Get implementations in a specific lifecycle state for a given rule.
|
|
719
706
|
*/
|
package/src/core/rule-host.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Rule Host — Constrained execution layer for active code implementations
|
|
3
3
|
*
|
|
4
|
-
* PURPOSE: Load active code implementations from the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* PURPOSE: Load active code implementations from the SQLite activations table
|
|
5
|
+
* (code_tool_hook channel), execute them in a constrained node:vm context,
|
|
6
|
+
* and merge their decisions.
|
|
7
7
|
*
|
|
8
|
-
* ARCHITECTURE:
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
8
|
+
* ARCHITECTURE (PRI-436):
|
|
9
|
+
* - SQLite is the SOLE production source of active RuleCode
|
|
10
|
+
* - No filesystem ledger or implementation asset reads occur during evaluation
|
|
11
|
+
* - Constructor takes stateDir for API compatibility (no longer used for impl loading)
|
|
12
|
+
* - workspaceDir enables reading code_tool_hook activations from SQLite
|
|
11
13
|
* - evaluate(input) loads active code implementations and runs them
|
|
12
14
|
* - Each implementation executes in an isolated vm context with minimal helpers
|
|
13
15
|
* - Decision merge: block short-circuits, requireApproval collects, allow is implicit
|
|
@@ -22,11 +24,6 @@
|
|
|
22
24
|
* - Never throw, never bypass downstream gates (Progressive Gate, Edit Verification)
|
|
23
25
|
*/
|
|
24
26
|
|
|
25
|
-
import * as fs from 'fs';
|
|
26
|
-
import {
|
|
27
|
-
listImplementationsByLifecycleState,
|
|
28
|
-
} from './principle-tree-ledger.js';
|
|
29
|
-
import { loadEntrySource } from './code-implementation-storage.js';
|
|
30
27
|
import { createRuleHostHelpers } from '@principles/core/runtime-v2';
|
|
31
28
|
import { mergeDecisions } from '@principles/core/runtime-v2';
|
|
32
29
|
import { SqliteConnection } from '@principles/core/runtime-v2';
|
|
@@ -37,13 +34,12 @@ import type {
|
|
|
37
34
|
RuleHostMeta,
|
|
38
35
|
LoadedImplementation,
|
|
39
36
|
} from '@principles/core/runtime-v2';
|
|
40
|
-
import type { Implementation } from '../types/principle-tree-schema.js';
|
|
41
37
|
|
|
42
38
|
import type { RuleHostLogger } from '@principles/core/runtime-v2';
|
|
43
39
|
export type { RuleHostLogger } from '@principles/core/runtime-v2';
|
|
44
40
|
|
|
45
41
|
export interface RuleHostOptions {
|
|
46
|
-
/** Workspace directory for SQLite access.
|
|
42
|
+
/** Workspace directory for SQLite access. Required for RuleHost to load active code_tool_hook activations. */
|
|
47
43
|
workspaceDir?: string;
|
|
48
44
|
}
|
|
49
45
|
|
|
@@ -84,7 +80,6 @@ export class RuleHost {
|
|
|
84
80
|
* - { decision: 'block', ... } when any implementation returns block (short-circuits)
|
|
85
81
|
* - { decision: 'requireApproval', ... } when any implementation returns requireApproval
|
|
86
82
|
*/
|
|
87
|
-
|
|
88
83
|
evaluate(input: RuleHostInput): RuleHostResult | undefined {
|
|
89
84
|
try {
|
|
90
85
|
const activeImpls = this._loadActiveCodeImplementations();
|
|
@@ -99,70 +94,28 @@ export class RuleHost {
|
|
|
99
94
|
}
|
|
100
95
|
|
|
101
96
|
/**
|
|
102
|
-
* Load active code implementations from the
|
|
97
|
+
* Load active code implementations from the SQLite activations table.
|
|
103
98
|
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
99
|
+
* PRI-436: SQLite is the SOLE production source. The filesystem ledger
|
|
100
|
+
* (principle-tree-ledger) and implementation asset paths have been deleted.
|
|
101
|
+
* No fallback, no dual-source, no deprecated adapter.
|
|
107
102
|
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* See BUG-001 / ERR-011 / ERR-035.
|
|
103
|
+
* Source: activations table (code_tool_hook channel, deactivated_at IS NULL)
|
|
104
|
+
* → JOIN pi_artifacts for content_json → extract implementationCode → compile
|
|
111
105
|
*/
|
|
112
106
|
private _loadActiveCodeImplementations(): LoadedImplementation[] {
|
|
113
|
-
|
|
114
|
-
|
|
107
|
+
if (!this.workspaceDir) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
115
110
|
|
|
116
|
-
// Source 1: principle-tree-ledger.json (existing path)
|
|
117
111
|
try {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
'active'
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
// Filter to code-type implementations only
|
|
124
|
-
const codeImpls = activeAllTypes.filter((impl) => impl.type === 'code');
|
|
125
|
-
|
|
126
|
-
for (const impl of codeImpls) {
|
|
127
|
-
try {
|
|
128
|
-
const loadedImpl = this._loadSingleImplementation(impl);
|
|
129
|
-
if (loadedImpl && !seenImplIds.has(loadedImpl.implId)) {
|
|
130
|
-
loaded.push(loadedImpl);
|
|
131
|
-
seenImplIds.add(loadedImpl.implId);
|
|
132
|
-
}
|
|
133
|
-
} catch (loadError: unknown) {
|
|
134
|
-
// Individual load failure: log and skip
|
|
135
|
-
this.logger.warn?.(
|
|
136
|
-
`[RuleHost] Failed to load implementation ${impl.id}: ${String(loadError)}`
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
} catch (ledgerError: unknown) {
|
|
141
|
-
// Ledger access failure: log and continue to activations table
|
|
112
|
+
return this._loadFromActivationsTable(this.workspaceDir);
|
|
113
|
+
} catch (activationError: unknown) {
|
|
142
114
|
this.logger.warn?.(
|
|
143
|
-
`[RuleHost] Failed to
|
|
115
|
+
`[RuleHost] Failed to load code_tool_hook activations: ${String(activationError)}`
|
|
144
116
|
);
|
|
117
|
+
return [];
|
|
145
118
|
}
|
|
146
|
-
|
|
147
|
-
// Source 2: activations table (code_tool_hook channel) — bridges BUG-001
|
|
148
|
-
if (this.workspaceDir) {
|
|
149
|
-
try {
|
|
150
|
-
const activationImpls = this._loadFromActivationsTable(this.workspaceDir);
|
|
151
|
-
for (const impl of activationImpls) {
|
|
152
|
-
if (!seenImplIds.has(impl.implId)) {
|
|
153
|
-
loaded.push(impl);
|
|
154
|
-
seenImplIds.add(impl.implId);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
} catch (activationError: unknown) {
|
|
158
|
-
// Activations table access failure: log and continue with ledger-only results
|
|
159
|
-
this.logger.warn?.(
|
|
160
|
-
`[RuleHost] Failed to load code_tool_hook activations: ${String(activationError)}`
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return loaded;
|
|
166
119
|
}
|
|
167
120
|
|
|
168
121
|
/**
|
|
@@ -170,10 +123,15 @@ export class RuleHost {
|
|
|
170
123
|
*
|
|
171
124
|
* For each activation record:
|
|
172
125
|
* 1. Query the pi_artifacts table for the artifact content
|
|
173
|
-
* 2. Parse content_json to extract implementationCode
|
|
174
|
-
* 3. Compile via loadRuleImplementationModule (
|
|
126
|
+
* 2. Parse content_json to extract implementationCode (treated as unknown, EP-01)
|
|
127
|
+
* 3. Compile via loadRuleImplementationModule (isolated vm context)
|
|
175
128
|
*
|
|
176
|
-
*
|
|
129
|
+
* PRI-436 invariant: at most one active activation per rule (target_ref).
|
|
130
|
+
* Duplicate active activations for the same target_ref are ALL skipped
|
|
131
|
+
* (zero executions) and emit structured unhealthy evidence via logger.warn
|
|
132
|
+
* (Runtime Contract Rule 9: graceful degradation includes a reason).
|
|
133
|
+
*
|
|
134
|
+
* All data from SQLite is treated as unknown and validated before use.
|
|
177
135
|
*/
|
|
178
136
|
private _loadFromActivationsTable(workspaceDir: string): LoadedImplementation[] {
|
|
179
137
|
const sqliteConn = new SqliteConnection(workspaceDir);
|
|
@@ -195,13 +153,58 @@ export class RuleHost {
|
|
|
195
153
|
return [];
|
|
196
154
|
}
|
|
197
155
|
|
|
198
|
-
|
|
199
|
-
|
|
156
|
+
// PRI-436: Group active rows by target_ref to detect duplicates.
|
|
157
|
+
// At most one active activation per rule (target_ref) is allowed.
|
|
158
|
+
// Duplicate groups are skipped entirely (zero executions) and emit
|
|
159
|
+
// structured unhealthy evidence. Non-duplicate rows proceed to compilation.
|
|
160
|
+
const rowsByTargetRef = new Map<string, Record<string, unknown>[]>();
|
|
200
161
|
for (const row of rows) {
|
|
201
162
|
if (!row || typeof row !== 'object') {
|
|
202
163
|
continue;
|
|
203
164
|
}
|
|
204
165
|
const r = row as Record<string, unknown>;
|
|
166
|
+
const targetRef = typeof r['target_ref'] === 'string' ? r['target_ref'] : '';
|
|
167
|
+
const activationId = typeof r['activation_id'] === 'string' ? r['activation_id'] : '';
|
|
168
|
+
if (!targetRef) {
|
|
169
|
+
this.logger.warn?.(
|
|
170
|
+
`[RuleHost] Activation ${activationId}: missing target_ref, skipping`
|
|
171
|
+
);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const group = rowsByTargetRef.get(targetRef);
|
|
175
|
+
if (group) {
|
|
176
|
+
group.push(r);
|
|
177
|
+
} else {
|
|
178
|
+
rowsByTargetRef.set(targetRef, [r]);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Emit structured unhealthy evidence for duplicate groups; collect valid (non-duplicate) rows.
|
|
183
|
+
const validRows: Record<string, unknown>[] = [];
|
|
184
|
+
for (const [targetRef, group] of rowsByTargetRef) {
|
|
185
|
+
if (group.length > 1) {
|
|
186
|
+
const activationIds: string[] = [];
|
|
187
|
+
const artifactIds: string[] = [];
|
|
188
|
+
for (const r of group) {
|
|
189
|
+
if (typeof r['activation_id'] === 'string') activationIds.push(r['activation_id']);
|
|
190
|
+
if (typeof r['artifact_id'] === 'string') artifactIds.push(r['artifact_id']);
|
|
191
|
+
}
|
|
192
|
+
this.logger.warn?.(
|
|
193
|
+
`[RuleHost] Duplicate active activations detected — skipping all executions for this rule. ` +
|
|
194
|
+
`targetRef=${targetRef} count=${group.length} ` +
|
|
195
|
+
`activationIds=[${activationIds.join(', ')}] ` +
|
|
196
|
+
`artifactIds=[${artifactIds.join(', ')}] ` +
|
|
197
|
+
`reason=at most one active activation per rule is allowed ` +
|
|
198
|
+
`nextAction=deactivate all but one activation for this target_ref`
|
|
199
|
+
);
|
|
200
|
+
} else {
|
|
201
|
+
validRows.push(group[0]);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const loaded: LoadedImplementation[] = [];
|
|
206
|
+
|
|
207
|
+
for (const r of validRows) {
|
|
205
208
|
const activationId = typeof r['activation_id'] === 'string' ? r['activation_id'] : '';
|
|
206
209
|
const artifactId = typeof r['artifact_id'] === 'string' ? r['artifact_id'] : '';
|
|
207
210
|
const contentJson = typeof r['content_json'] === 'string' ? r['content_json'] : '';
|
|
@@ -290,83 +293,4 @@ export class RuleHost {
|
|
|
290
293
|
}
|
|
291
294
|
}
|
|
292
295
|
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Load and compile a single implementation from its code asset path.
|
|
296
|
-
*
|
|
297
|
-
* The implementation file is expected to export:
|
|
298
|
-
* - meta: { name, version, ruleId, coversCondition }
|
|
299
|
-
* - evaluate(input: RuleHostInput): RuleHostResult
|
|
300
|
-
*
|
|
301
|
-
* Uses the shared isolated runtime loader so candidate code does not execute
|
|
302
|
-
* in the host global realm.
|
|
303
|
-
*/
|
|
304
|
-
|
|
305
|
-
private _loadSingleImplementation(
|
|
306
|
-
impl: Implementation
|
|
307
|
-
): LoadedImplementation | null {
|
|
308
|
-
let sourceCode = loadEntrySource(this.stateDir, impl.id);
|
|
309
|
-
if (!sourceCode) {
|
|
310
|
-
const assetPath = impl.path;
|
|
311
|
-
if (!assetPath || !fs.existsSync(assetPath)) {
|
|
312
|
-
return null;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
try {
|
|
316
|
-
sourceCode = fs.readFileSync(assetPath, 'utf-8');
|
|
317
|
-
} catch {
|
|
318
|
-
return null;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
try {
|
|
323
|
-
const moduleExports = loadRuleImplementationModule(sourceCode, impl.id);
|
|
324
|
-
|
|
325
|
-
if (!moduleExports || typeof moduleExports.evaluate !== 'function') {
|
|
326
|
-
return null;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const fallbackMeta: RuleHostMeta = {
|
|
330
|
-
name: impl.id,
|
|
331
|
-
version: impl.version,
|
|
332
|
-
ruleId: impl.ruleId,
|
|
333
|
-
coversCondition: impl.coversCondition,
|
|
334
|
-
};
|
|
335
|
-
const meta: RuleHostMeta =
|
|
336
|
-
moduleExports.meta && typeof moduleExports.meta === 'object'
|
|
337
|
-
? (moduleExports.meta as RuleHostMeta)
|
|
338
|
-
: fallbackMeta;
|
|
339
|
-
|
|
340
|
-
// Return a loaded implementation that wraps the compiled evaluate
|
|
341
|
-
// with the actual helpers from the input at evaluation time
|
|
342
|
-
|
|
343
|
-
const rawEvaluate = moduleExports.evaluate as (
|
|
344
|
-
_input: RuleHostInput,
|
|
345
|
-
_helpers: ReturnType<typeof createRuleHostHelpers>
|
|
346
|
-
) => RuleHostResult;
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
return {
|
|
350
|
-
implId: impl.id,
|
|
351
|
-
ruleId: impl.ruleId,
|
|
352
|
-
meta,
|
|
353
|
-
evaluate: (input: RuleHostInput): RuleHostResult => {
|
|
354
|
-
const frozenHelpers = createRuleHostHelpers(input);
|
|
355
|
-
const result = rawEvaluate(input, frozenHelpers);
|
|
356
|
-
// C: Enrich result with rule/principle IDs for observability
|
|
357
|
-
if (result.matched && (result.decision === 'block' || result.decision === 'requireApproval')) {
|
|
358
|
-
result.ruleId = impl.ruleId;
|
|
359
|
-
result.principleId = meta.ruleId ?? impl.ruleId;
|
|
360
|
-
}
|
|
361
|
-
return result;
|
|
362
|
-
},
|
|
363
|
-
};
|
|
364
|
-
} catch (compileError: unknown) {
|
|
365
|
-
// Compilation failure: log and skip
|
|
366
|
-
this.logger.warn?.(
|
|
367
|
-
`[RuleHost] Failed to compile implementation ${impl.id}: ${String(compileError)}`
|
|
368
|
-
);
|
|
369
|
-
return null;
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
296
|
}
|
|
@@ -5,9 +5,7 @@ import * as path from 'path';
|
|
|
5
5
|
import {
|
|
6
6
|
deleteImplementationAssetDir,
|
|
7
7
|
getImplementationAssetRoot,
|
|
8
|
-
loadManifest,
|
|
9
8
|
writeManifest,
|
|
10
|
-
loadEntrySource,
|
|
11
9
|
createImplementationAssetDir,
|
|
12
10
|
writeEntrySource,
|
|
13
11
|
type CodeImplementationManifest,
|
|
@@ -63,47 +61,6 @@ describe('CodeImplementationStorage', () => {
|
|
|
63
61
|
});
|
|
64
62
|
});
|
|
65
63
|
|
|
66
|
-
// -------------------------------------------------------------------------
|
|
67
|
-
// loadManifest
|
|
68
|
-
// -------------------------------------------------------------------------
|
|
69
|
-
|
|
70
|
-
describe('loadManifest', () => {
|
|
71
|
-
it('returns null when no manifest file exists', () => {
|
|
72
|
-
const result = loadManifest(stateDir, 'nonexistent');
|
|
73
|
-
expect(result).toBeNull();
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('returns parsed manifest when manifest file exists', () => {
|
|
77
|
-
const implId = 'IMPL_002';
|
|
78
|
-
const manifest: CodeImplementationManifest = {
|
|
79
|
-
version: '1.0.0',
|
|
80
|
-
entryFile: 'entry.js',
|
|
81
|
-
createdAt: '2026-01-01T00:00:00.000Z',
|
|
82
|
-
updatedAt: '2026-01-01T00:00:00.000Z',
|
|
83
|
-
replaySampleRefs: [],
|
|
84
|
-
lastEvalReportRef: null,
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
// Write directly (bypass writeManifest to test load independently)
|
|
88
|
-
const assetRoot = getImplementationAssetRoot(stateDir, implId);
|
|
89
|
-
fs.mkdirSync(assetRoot, { recursive: true });
|
|
90
|
-
fs.writeFileSync(path.join(assetRoot, 'manifest.json'), JSON.stringify(manifest), 'utf-8');
|
|
91
|
-
|
|
92
|
-
const result = loadManifest(stateDir, implId);
|
|
93
|
-
expect(result).toEqual(manifest);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('returns null for corrupted manifest JSON', () => {
|
|
97
|
-
const implId = 'IMPL_003';
|
|
98
|
-
const assetRoot = getImplementationAssetRoot(stateDir, implId);
|
|
99
|
-
fs.mkdirSync(assetRoot, { recursive: true });
|
|
100
|
-
fs.writeFileSync(path.join(assetRoot, 'manifest.json'), 'not-valid-json{{{', 'utf-8');
|
|
101
|
-
|
|
102
|
-
const result = loadManifest(stateDir, implId);
|
|
103
|
-
expect(result).toBeNull();
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
64
|
// -------------------------------------------------------------------------
|
|
108
65
|
// writeManifest
|
|
109
66
|
// -------------------------------------------------------------------------
|
|
@@ -177,67 +134,6 @@ describe('CodeImplementationStorage', () => {
|
|
|
177
134
|
});
|
|
178
135
|
});
|
|
179
136
|
|
|
180
|
-
// -------------------------------------------------------------------------
|
|
181
|
-
// loadEntrySource
|
|
182
|
-
// -------------------------------------------------------------------------
|
|
183
|
-
|
|
184
|
-
describe('loadEntrySource', () => {
|
|
185
|
-
it('returns null when no manifest exists', () => {
|
|
186
|
-
const result = loadEntrySource(stateDir, 'nonexistent');
|
|
187
|
-
expect(result).toBeNull();
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it('returns null when manifest exists but entry file does not', () => {
|
|
191
|
-
const implId = 'IMPL_007';
|
|
192
|
-
const assetRoot = getImplementationAssetRoot(stateDir, implId);
|
|
193
|
-
fs.mkdirSync(assetRoot, { recursive: true });
|
|
194
|
-
|
|
195
|
-
// Write manifest pointing to a non-existent entry file
|
|
196
|
-
const manifest: CodeImplementationManifest = {
|
|
197
|
-
version: '1.0.0',
|
|
198
|
-
entryFile: 'nonexistent.js',
|
|
199
|
-
createdAt: '2026-01-01T00:00:00.000Z',
|
|
200
|
-
updatedAt: '2026-01-01T00:00:00.000Z',
|
|
201
|
-
replaySampleRefs: [],
|
|
202
|
-
lastEvalReportRef: null,
|
|
203
|
-
};
|
|
204
|
-
fs.writeFileSync(
|
|
205
|
-
path.join(assetRoot, 'manifest.json'),
|
|
206
|
-
JSON.stringify(manifest),
|
|
207
|
-
'utf-8',
|
|
208
|
-
);
|
|
209
|
-
|
|
210
|
-
const result = loadEntrySource(stateDir, implId);
|
|
211
|
-
expect(result).toBeNull();
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it('returns file content when entry file exists', () => {
|
|
215
|
-
const implId = 'IMPL_008';
|
|
216
|
-
const assetRoot = getImplementationAssetRoot(stateDir, implId);
|
|
217
|
-
fs.mkdirSync(assetRoot, { recursive: true });
|
|
218
|
-
|
|
219
|
-
const entryContent = 'export function evaluate() { return true; }';
|
|
220
|
-
fs.writeFileSync(path.join(assetRoot, 'entry.js'), entryContent, 'utf-8');
|
|
221
|
-
|
|
222
|
-
const manifest: CodeImplementationManifest = {
|
|
223
|
-
version: '1.0.0',
|
|
224
|
-
entryFile: 'entry.js',
|
|
225
|
-
createdAt: '2026-01-01T00:00:00.000Z',
|
|
226
|
-
updatedAt: '2026-01-01T00:00:00.000Z',
|
|
227
|
-
replaySampleRefs: [],
|
|
228
|
-
lastEvalReportRef: null,
|
|
229
|
-
};
|
|
230
|
-
fs.writeFileSync(
|
|
231
|
-
path.join(assetRoot, 'manifest.json'),
|
|
232
|
-
JSON.stringify(manifest),
|
|
233
|
-
'utf-8',
|
|
234
|
-
);
|
|
235
|
-
|
|
236
|
-
const result = loadEntrySource(stateDir, implId);
|
|
237
|
-
expect(result).toBe(entryContent);
|
|
238
|
-
});
|
|
239
|
-
});
|
|
240
|
-
|
|
241
137
|
// -------------------------------------------------------------------------
|
|
242
138
|
// createImplementationAssetDir
|
|
243
139
|
// -------------------------------------------------------------------------
|
|
@@ -300,7 +196,9 @@ describe('CodeImplementationStorage', () => {
|
|
|
300
196
|
artificerArtifactId: 'artifact-1',
|
|
301
197
|
});
|
|
302
198
|
|
|
303
|
-
|
|
199
|
+
// Verify persisted manifest contains lineage
|
|
200
|
+
const assetRoot = getImplementationAssetRoot(stateDir, implId);
|
|
201
|
+
const loaded = JSON.parse(fs.readFileSync(path.join(assetRoot, 'manifest.json'), 'utf-8'));
|
|
304
202
|
expect(loaded?.lineage).toMatchObject({
|
|
305
203
|
sourcePainIds: ['pain:gate:1'],
|
|
306
204
|
sourceGateBlockIds: ['gate:write:1'],
|
|
@@ -344,22 +242,23 @@ describe('CodeImplementationStorage', () => {
|
|
|
344
242
|
expect(fs.existsSync(reportPath)).toBe(true);
|
|
345
243
|
});
|
|
346
244
|
|
|
347
|
-
it('manifest is
|
|
245
|
+
it('manifest is persisted with correct version after creation', () => {
|
|
348
246
|
const implId = 'IMPL_014';
|
|
349
247
|
createImplementationAssetDir(stateDir, implId, '4.0.0');
|
|
350
248
|
|
|
351
|
-
const
|
|
249
|
+
const assetRoot = getImplementationAssetRoot(stateDir, implId);
|
|
250
|
+
const loaded = JSON.parse(fs.readFileSync(path.join(assetRoot, 'manifest.json'), 'utf-8'));
|
|
352
251
|
expect(loaded).not.toBeNull();
|
|
353
|
-
expect(loaded
|
|
354
|
-
expect(loaded
|
|
252
|
+
expect(loaded.version).toBe('4.0.0');
|
|
253
|
+
expect(loaded.entryFile).toBe('entry.js');
|
|
355
254
|
});
|
|
356
255
|
|
|
357
|
-
it('entry source is
|
|
256
|
+
it('entry source is persisted after creation', () => {
|
|
358
257
|
const implId = 'IMPL_015';
|
|
359
258
|
createImplementationAssetDir(stateDir, implId, '1.0.0');
|
|
360
259
|
|
|
361
|
-
const
|
|
362
|
-
|
|
260
|
+
const assetRoot = getImplementationAssetRoot(stateDir, implId);
|
|
261
|
+
const source = fs.readFileSync(path.join(assetRoot, 'entry.js'), 'utf-8');
|
|
363
262
|
expect(source).toContain('export const meta');
|
|
364
263
|
expect(source).toContain('export function evaluate');
|
|
365
264
|
});
|
|
@@ -370,7 +269,8 @@ describe('CodeImplementationStorage', () => {
|
|
|
370
269
|
entrySource: 'export const meta = { name: "x", version: "1", ruleId: "R", coversCondition: "c" }; export function evaluate() { return { decision: "allow", matched: false, reason: "ok" }; }',
|
|
371
270
|
});
|
|
372
271
|
|
|
373
|
-
const
|
|
272
|
+
const assetRoot = getImplementationAssetRoot(stateDir, implId);
|
|
273
|
+
const source = fs.readFileSync(path.join(assetRoot, 'entry.js'), 'utf-8');
|
|
374
274
|
expect(source).toContain('export const meta');
|
|
375
275
|
expect(source).not.toContain('placeholder');
|
|
376
276
|
});
|
|
@@ -383,7 +283,9 @@ describe('CodeImplementationStorage', () => {
|
|
|
383
283
|
|
|
384
284
|
writeEntrySource(stateDir, implId, 'export const meta = { name: "updated", version: "1", ruleId: "R", coversCondition: "c" }; export function evaluate() { return { decision: "allow", matched: false, reason: "updated" }; }');
|
|
385
285
|
|
|
386
|
-
|
|
286
|
+
const assetRoot = getImplementationAssetRoot(stateDir, implId);
|
|
287
|
+
const source = fs.readFileSync(path.join(assetRoot, 'entry.js'), 'utf-8');
|
|
288
|
+
expect(source).toContain('updated');
|
|
387
289
|
});
|
|
388
290
|
|
|
389
291
|
it('removes the asset directory through deleteImplementationAssetDir', () => {
|