@principles/core 1.169.0 → 1.171.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/README.md +38 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +1 -1
- package/dist/pain-signal.d.ts +11 -71
- package/dist/pain-signal.d.ts.map +1 -1
- package/dist/pain-signal.js +10 -120
- package/dist/pain-signal.js.map +1 -1
- package/dist/principle-tree-ledger.d.ts +4 -87
- package/dist/principle-tree-ledger.d.ts.map +1 -1
- package/dist/principle-tree-ledger.js +51 -148
- package/dist/principle-tree-ledger.js.map +1 -1
- package/dist/runtime-v2/__tests__/architecture-regression.test.js +178 -4
- package/dist/runtime-v2/__tests__/architecture-regression.test.js.map +1 -1
- package/dist/runtime-v2/__tests__/barrel-io-cleanup.test.d.ts +12 -0
- package/dist/runtime-v2/__tests__/barrel-io-cleanup.test.d.ts.map +1 -0
- package/dist/runtime-v2/__tests__/barrel-io-cleanup.test.js +46 -0
- package/dist/runtime-v2/__tests__/barrel-io-cleanup.test.js.map +1 -0
- package/dist/runtime-v2/__tests__/io-deletion.test.d.ts +12 -0
- package/dist/runtime-v2/__tests__/io-deletion.test.d.ts.map +1 -0
- package/dist/runtime-v2/__tests__/io-deletion.test.js +92 -0
- package/dist/runtime-v2/__tests__/io-deletion.test.js.map +1 -0
- package/dist/runtime-v2/__tests__/ledger-codec.test.d.ts +2 -0
- package/dist/runtime-v2/__tests__/ledger-codec.test.d.ts.map +1 -0
- package/dist/runtime-v2/__tests__/ledger-codec.test.js +133 -0
- package/dist/runtime-v2/__tests__/ledger-codec.test.js.map +1 -0
- package/dist/runtime-v2/__tests__/ledger-store.test.d.ts +2 -0
- package/dist/runtime-v2/__tests__/ledger-store.test.d.ts.map +1 -0
- package/dist/runtime-v2/__tests__/ledger-store.test.js +65 -0
- package/dist/runtime-v2/__tests__/ledger-store.test.js.map +1 -0
- package/dist/runtime-v2/__tests__/pain-signal-unification.test.d.ts +2 -0
- package/dist/runtime-v2/__tests__/pain-signal-unification.test.d.ts.map +1 -0
- package/dist/runtime-v2/__tests__/pain-signal-unification.test.js +150 -0
- package/dist/runtime-v2/__tests__/pain-signal-unification.test.js.map +1 -0
- package/dist/runtime-v2/index.d.ts +1 -2
- package/dist/runtime-v2/index.d.ts.map +1 -1
- package/dist/runtime-v2/index.js +4 -2
- package/dist/runtime-v2/index.js.map +1 -1
- package/dist/runtime-v2/principle-tree/ledger-codec.d.ts +30 -0
- package/dist/runtime-v2/principle-tree/ledger-codec.d.ts.map +1 -0
- package/dist/runtime-v2/principle-tree/ledger-codec.js +154 -0
- package/dist/runtime-v2/principle-tree/ledger-codec.js.map +1 -0
- package/dist/runtime-v2/types/index.d.ts +2 -0
- package/dist/runtime-v2/types/index.d.ts.map +1 -1
- package/dist/runtime-v2/types/index.js +1 -0
- package/dist/runtime-v2/types/index.js.map +1 -1
- package/dist/runtime-v2/types/ledger-store.d.ts +101 -0
- package/dist/runtime-v2/types/ledger-store.d.ts.map +1 -0
- package/dist/runtime-v2/types/ledger-store.js +15 -0
- package/dist/runtime-v2/types/ledger-store.js.map +1 -0
- package/dist/runtime-v2/types/pain-signal.d.ts +5 -3
- package/dist/runtime-v2/types/pain-signal.d.ts.map +1 -1
- package/dist/runtime-v2/types/pain-signal.js +23 -3
- package/dist/runtime-v2/types/pain-signal.js.map +1 -1
- package/package.json +4 -4
- package/dist/io.d.ts +0 -35
- package/dist/io.d.ts.map +0 -1
- package/dist/io.js +0 -95
- package/dist/io.js.map +0 -1
|
@@ -12,148 +12,64 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import * as fs from 'fs';
|
|
14
14
|
import * as path from 'path';
|
|
15
|
-
import {
|
|
15
|
+
import { TREE_NAMESPACE } from './runtime-v2/types/ledger-store.js';
|
|
16
|
+
// PRI-443: Pure parse/serialize functions extracted to codec module
|
|
17
|
+
import { uniqueStrings, createEmptyTree, parseHybridLedger, serializeLedger, } from './runtime-v2/principle-tree/ledger-codec.js';
|
|
18
|
+
export { TREE_NAMESPACE };
|
|
16
19
|
const PRINCIPLE_TRAINING_FILE = 'principle_training_state.json';
|
|
17
20
|
// ---------------------------------------------------------------------------
|
|
18
|
-
//
|
|
21
|
+
// Atomic file write (inlined PRI-443 Phase 4)
|
|
19
22
|
// ---------------------------------------------------------------------------
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
function
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
continue;
|
|
60
|
-
result[principleId] = {
|
|
61
|
-
principleId,
|
|
62
|
-
evaluability: VALID_EVALUABILITIES.includes(candidate.evaluability)
|
|
63
|
-
? candidate.evaluability
|
|
64
|
-
: 'manual_only',
|
|
65
|
-
applicableOpportunityCount: clampInt(candidate.applicableOpportunityCount, { min: 0, max: Infinity, fallback: 0 }),
|
|
66
|
-
observedViolationCount: clampInt(candidate.observedViolationCount, { min: 0, max: Infinity, fallback: 0 }),
|
|
67
|
-
complianceRate: clampFloat(candidate.complianceRate, { min: 0, max: 1, fallback: 0 }),
|
|
68
|
-
violationTrend: clampFloat(candidate.violationTrend, { min: -1, max: 1, fallback: 0 }),
|
|
69
|
-
generatedSampleCount: clampInt(candidate.generatedSampleCount, { min: 0, max: Infinity, fallback: 0 }),
|
|
70
|
-
approvedSampleCount: clampInt(candidate.approvedSampleCount, { min: 0, max: Infinity, fallback: 0 }),
|
|
71
|
-
includedTrainRunIds: stringArray(candidate.includedTrainRunIds),
|
|
72
|
-
deployedCheckpointIds: stringArray(candidate.deployedCheckpointIds),
|
|
73
|
-
lastEvalScore: typeof candidate.lastEvalScore === 'number' && Number.isFinite(candidate.lastEvalScore)
|
|
74
|
-
? clampFloat(candidate.lastEvalScore, { min: 0, max: 1, fallback: 0 }) : undefined,
|
|
75
|
-
internalizationStatus: VALID_INTERNALIZATION_STATUSES.includes(candidate.internalizationStatus)
|
|
76
|
-
? candidate.internalizationStatus
|
|
77
|
-
: 'prompt_only',
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
return result;
|
|
81
|
-
}
|
|
82
|
-
function parsePrinciples(raw) {
|
|
83
|
-
if (!isRecord(raw))
|
|
84
|
-
return {};
|
|
85
|
-
const principles = {};
|
|
86
|
-
for (const [id, value] of Object.entries(raw)) {
|
|
87
|
-
if (!isRecord(value))
|
|
88
|
-
continue;
|
|
89
|
-
principles[id] = {
|
|
90
|
-
...value,
|
|
91
|
-
id,
|
|
92
|
-
ruleIds: stringArray(value.ruleIds),
|
|
93
|
-
conflictsWithPrincipleIds: stringArray(value.conflictsWithPrincipleIds),
|
|
94
|
-
derivedFromPainIds: stringArray(value.derivedFromPainIds),
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
return principles;
|
|
98
|
-
}
|
|
99
|
-
function parseRules(raw) {
|
|
100
|
-
if (!isRecord(raw))
|
|
101
|
-
return {};
|
|
102
|
-
const rules = {};
|
|
103
|
-
for (const [id, value] of Object.entries(raw)) {
|
|
104
|
-
if (!isRecord(value))
|
|
105
|
-
continue;
|
|
106
|
-
rules[id] = {
|
|
107
|
-
...value,
|
|
108
|
-
id,
|
|
109
|
-
principleId: typeof value.principleId === 'string' ? value.principleId : '',
|
|
110
|
-
implementationIds: stringArray(value.implementationIds),
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
return rules;
|
|
114
|
-
}
|
|
115
|
-
function parseImplementations(raw) {
|
|
116
|
-
if (!isRecord(raw))
|
|
117
|
-
return {};
|
|
118
|
-
const implementations = {};
|
|
119
|
-
for (const [id, value] of Object.entries(raw)) {
|
|
120
|
-
if (!isRecord(value) || typeof value.ruleId !== 'string')
|
|
121
|
-
continue;
|
|
122
|
-
implementations[id] = { ...value, id, ruleId: value.ruleId };
|
|
23
|
+
//
|
|
24
|
+
// Previously exported from ./io.ts. Inlined here as a private helper because
|
|
25
|
+
// this is the ONLY consumer in principles-core. The openclaw-plugin has its
|
|
26
|
+
// own copy at src/utils/io.ts.
|
|
27
|
+
//
|
|
28
|
+
// Crash-safe: writes to a .tmp file then renames. On Windows, retries with
|
|
29
|
+
// exponential backoff on EPERM/EBUSY/EACCES to handle transient file locks.
|
|
30
|
+
const RENAME_MAX_RETRIES = 3;
|
|
31
|
+
const RENAME_BASE_DELAY_MS = 50;
|
|
32
|
+
function atomicWriteFileSync(filePath, data) {
|
|
33
|
+
const tmpPath = filePath + '.tmp';
|
|
34
|
+
fs.writeFileSync(tmpPath, data, 'utf8');
|
|
35
|
+
let lastError;
|
|
36
|
+
for (let attempt = 0; attempt < RENAME_MAX_RETRIES; attempt++) {
|
|
37
|
+
try {
|
|
38
|
+
fs.renameSync(tmpPath, filePath);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
lastError = err;
|
|
43
|
+
const { code } = err;
|
|
44
|
+
// Only retry on Windows transient lock errors
|
|
45
|
+
if (code === 'EPERM' || code === 'EBUSY' || code === 'EACCES') {
|
|
46
|
+
if (attempt < RENAME_MAX_RETRIES - 1) {
|
|
47
|
+
const delay = RENAME_BASE_DELAY_MS * Math.pow(2, attempt);
|
|
48
|
+
// Synchronous sleep using a tight spin with accessSync yield
|
|
49
|
+
const waitUntil = Date.now() + delay;
|
|
50
|
+
while (Date.now() < waitUntil) {
|
|
51
|
+
try {
|
|
52
|
+
fs.accessSync(tmpPath);
|
|
53
|
+
}
|
|
54
|
+
catch { /* ignore */ }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
// Non-retryable error — throw immediately
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
123
62
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (!isRecord(raw))
|
|
128
|
-
return {};
|
|
129
|
-
const metrics = {};
|
|
130
|
-
for (const [id, value] of Object.entries(raw)) {
|
|
131
|
-
if (!isRecord(value))
|
|
132
|
-
continue;
|
|
133
|
-
metrics[id] = { ...value, principleId: typeof value.principleId === 'string' ? value.principleId : id };
|
|
63
|
+
// Clean up temp file on failure
|
|
64
|
+
try {
|
|
65
|
+
fs.unlinkSync(tmpPath);
|
|
134
66
|
}
|
|
135
|
-
|
|
67
|
+
catch { /* best effort */ }
|
|
68
|
+
throw lastError ?? new Error('atomicWriteFileSync: rename failed');
|
|
136
69
|
}
|
|
137
70
|
// ---------------------------------------------------------------------------
|
|
138
|
-
//
|
|
71
|
+
// Ledger file I/O (the only non-pure part of this module)
|
|
139
72
|
// ---------------------------------------------------------------------------
|
|
140
|
-
function createEmptyTree() {
|
|
141
|
-
return { principles: {}, rules: {}, implementations: {}, metrics: {}, lastUpdated: new Date(0).toISOString() };
|
|
142
|
-
}
|
|
143
|
-
// ---------------------------------------------------------------------------
|
|
144
|
-
// Parsers
|
|
145
|
-
// ---------------------------------------------------------------------------
|
|
146
|
-
function parseTree(raw) {
|
|
147
|
-
if (!isRecord(raw))
|
|
148
|
-
return createEmptyTree();
|
|
149
|
-
return {
|
|
150
|
-
principles: parsePrinciples(raw.principles),
|
|
151
|
-
rules: parseRules(raw.rules),
|
|
152
|
-
implementations: parseImplementations(raw.implementations),
|
|
153
|
-
metrics: parseMetrics(raw.metrics),
|
|
154
|
-
lastUpdated: typeof raw.lastUpdated === 'string' ? raw.lastUpdated : new Date(0).toISOString(),
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
73
|
function getLedgerFilePath(stateDir) {
|
|
158
74
|
return path.join(stateDir, PRINCIPLE_TRAINING_FILE);
|
|
159
75
|
}
|
|
@@ -167,25 +83,12 @@ function readLedgerFromFile(filePath) {
|
|
|
167
83
|
return { trainingStore: {}, tree: createEmptyTree() };
|
|
168
84
|
}
|
|
169
85
|
const parsed = JSON.parse(content);
|
|
170
|
-
|
|
171
|
-
return { trainingStore: {}, tree: createEmptyTree() };
|
|
172
|
-
const trainingStoreRaw = parsed.trainingStore ?? parsed;
|
|
173
|
-
const treeRaw = parsed[TREE_NAMESPACE] ?? parsed.tree;
|
|
174
|
-
return {
|
|
175
|
-
trainingStore: parseLegacyTrainingStore(trainingStoreRaw),
|
|
176
|
-
tree: parseTree(treeRaw),
|
|
177
|
-
};
|
|
86
|
+
return parseHybridLedger(parsed);
|
|
178
87
|
}
|
|
179
88
|
catch {
|
|
180
89
|
return { trainingStore: {}, tree: createEmptyTree() };
|
|
181
90
|
}
|
|
182
91
|
}
|
|
183
|
-
function serializeLedger(store) {
|
|
184
|
-
return JSON.stringify({
|
|
185
|
-
...store.trainingStore,
|
|
186
|
-
[TREE_NAMESPACE]: { ...store.tree, lastUpdated: new Date().toISOString() },
|
|
187
|
-
}, null, 2);
|
|
188
|
-
}
|
|
189
92
|
// ---------------------------------------------------------------------------
|
|
190
93
|
// Ledger mutations (synchronous — safe for single-process use)
|
|
191
94
|
// ---------------------------------------------------------------------------
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"principle-tree-ledger.js","sourceRoot":"","sources":["../src/principle-tree-ledger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"principle-tree-ledger.js","sourceRoot":"","sources":["../src/principle-tree-ledger.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* Add entries here whenever a new service/read-model boundary is established.
|
|
6
6
|
*/
|
|
7
7
|
import { describe, it, expect, beforeAll } from 'vitest';
|
|
8
|
+
import * as fsSync from 'fs';
|
|
9
|
+
import * as pathSync from 'path';
|
|
8
10
|
// ── Source-file existence ──────────────────────────────────────────────────
|
|
9
11
|
const REQUIRED_SOURCE_FILES = [
|
|
10
12
|
'pain-to-principle-service.ts',
|
|
@@ -2427,7 +2429,8 @@ describe('PRI-139 L1 Hard Cap & LRU Eviction', () => {
|
|
|
2427
2429
|
it('LedgerPrinciple includes lastTriggeredAt field', async () => {
|
|
2428
2430
|
const { readFileSync } = await import('node:fs');
|
|
2429
2431
|
const { resolve } = await import('node:path');
|
|
2430
|
-
|
|
2432
|
+
// PRI-443: type moved to pure module
|
|
2433
|
+
const src = readFileSync(resolve(__dirname, '..', 'types', 'ledger-store.ts'), 'utf-8');
|
|
2431
2434
|
expect(src).toContain('lastTriggeredAt');
|
|
2432
2435
|
});
|
|
2433
2436
|
it('PruningHealthSummary includes activeL1Count and l1Cap fields', async () => {
|
|
@@ -3022,11 +3025,18 @@ describe('PRI-416: type single-ownership guards', () => {
|
|
|
3022
3025
|
});
|
|
3023
3026
|
it('HybridLedgerStore is defined exactly once in core/src/', () => {
|
|
3024
3027
|
const hits = findDefinitions(/export\s+interface\s+HybridLedgerStore\s*\{/);
|
|
3025
|
-
expect(hits).toEqual(['
|
|
3028
|
+
expect(hits).toEqual(['runtime-v2/types/ledger-store.ts']);
|
|
3026
3029
|
});
|
|
3027
|
-
it('atomicWriteFileSync is
|
|
3030
|
+
it('atomicWriteFileSync is NOT exported from any core/src/ file (PRI-443 Phase 4)', () => {
|
|
3031
|
+
// After PRI-443 Phase 4, atomicWriteFileSync is inlined as a PRIVATE helper
|
|
3032
|
+
// inside principle-tree-ledger.ts. It must NOT be exported from core.
|
|
3028
3033
|
const hits = findDefinitions(/export\s+function\s+atomicWriteFileSync\s*\(/);
|
|
3029
|
-
expect(hits).toEqual([
|
|
3034
|
+
expect(hits).toEqual([]);
|
|
3035
|
+
});
|
|
3036
|
+
it('atomicWriteFileSync is defined exactly once as a private function in principle-tree-ledger.ts (PRI-443 Phase 4)', () => {
|
|
3037
|
+
// The function is now a private (non-exported) helper inlined in principle-tree-ledger.ts
|
|
3038
|
+
const hits = findDefinitions(/function\s+atomicWriteFileSync\s*\(/);
|
|
3039
|
+
expect(hits).toEqual(['principle-tree-ledger.ts']);
|
|
3030
3040
|
});
|
|
3031
3041
|
});
|
|
3032
3042
|
// ── PRI-416: Barrel Cap Guards ────────────────────────────────────────────────
|
|
@@ -3054,4 +3064,168 @@ describe('PRI-416: barrel cap guards', () => {
|
|
|
3054
3064
|
expect(exportLines.length).toBeLessThanOrEqual(BARREL_MAX_EXPORT_LINES);
|
|
3055
3065
|
});
|
|
3056
3066
|
});
|
|
3067
|
+
// ── PRI-443: Pure module boundary guards ──────────────────────────────────────
|
|
3068
|
+
//
|
|
3069
|
+
// Phases 1-3 extracted pure types/codecs/schema out of I/O modules. These guards
|
|
3070
|
+
// ensure those pure modules never regress to importing fs/path, and that the
|
|
3071
|
+
// runtime-v2 barrel never re-exports I/O functions from principle-tree-ledger.
|
|
3072
|
+
describe('PRI-443: pure module boundary guards', () => {
|
|
3073
|
+
const FORBIDDEN_PURE_IMPORTS = [
|
|
3074
|
+
"from 'fs'",
|
|
3075
|
+
'from "fs"',
|
|
3076
|
+
"from 'node:fs'",
|
|
3077
|
+
'from "node:fs"',
|
|
3078
|
+
"from 'path'",
|
|
3079
|
+
'from "path"',
|
|
3080
|
+
"from 'node:path'",
|
|
3081
|
+
'from "node:path"',
|
|
3082
|
+
];
|
|
3083
|
+
async function readSrc(relPath) {
|
|
3084
|
+
const fsMod = await import('fs'); // eslint-disable-line @typescript-eslint/consistent-type-imports
|
|
3085
|
+
const pathMod = await import('path'); // eslint-disable-line @typescript-eslint/consistent-type-imports
|
|
3086
|
+
const full = pathMod.resolve(__dirname, '..', relPath);
|
|
3087
|
+
return fsMod.readFileSync(full, 'utf-8');
|
|
3088
|
+
}
|
|
3089
|
+
it('runtime-v2/types/ledger-store.ts has zero fs/path imports (pure types module)', async () => {
|
|
3090
|
+
const src = await readSrc('types/ledger-store.ts');
|
|
3091
|
+
for (const pat of FORBIDDEN_PURE_IMPORTS) {
|
|
3092
|
+
expect(src).not.toContain(pat);
|
|
3093
|
+
}
|
|
3094
|
+
});
|
|
3095
|
+
it('runtime-v2/principle-tree/ledger-codec.ts has zero fs/path imports (pure codec module)', async () => {
|
|
3096
|
+
const src = await readSrc('principle-tree/ledger-codec.ts');
|
|
3097
|
+
for (const pat of FORBIDDEN_PURE_IMPORTS) {
|
|
3098
|
+
expect(src).not.toContain(pat);
|
|
3099
|
+
}
|
|
3100
|
+
});
|
|
3101
|
+
it('runtime-v2/types/pain-signal.ts has zero fs/path imports (pure schema module)', async () => {
|
|
3102
|
+
const src = await readSrc('types/pain-signal.ts');
|
|
3103
|
+
for (const pat of FORBIDDEN_PURE_IMPORTS) {
|
|
3104
|
+
expect(src).not.toContain(pat);
|
|
3105
|
+
}
|
|
3106
|
+
});
|
|
3107
|
+
it('runtime-v2/index.ts barrel does NOT export I/O functions from principle-tree-ledger', async () => {
|
|
3108
|
+
const src = await readSrc('index.ts');
|
|
3109
|
+
// I/O functions must not be re-exported through the pure-types barrel.
|
|
3110
|
+
expect(src).not.toMatch(/export\s+\{[^}]*\bloadLedger\b[^}]*\}\s*from\s*['"]\.\.\/principle-tree-ledger/);
|
|
3111
|
+
expect(src).not.toMatch(/export\s+\{[^}]*\bsaveLedger\b[^}]*\}\s*from\s*['"]\.\.\/principle-tree-ledger/);
|
|
3112
|
+
expect(src).not.toMatch(/export\s+\{[^}]*\bgetLedgerFilePathPublic\b[^}]*\}\s*from\s*['"]\.\.\/principle-tree-ledger/);
|
|
3113
|
+
expect(src).not.toMatch(/export\s+\{[^}]*\bupdatePrinciple\b[^}]*\}\s*from\s*['"]\.\.\/principle-tree-ledger/);
|
|
3114
|
+
expect(src).not.toMatch(/export\s+\{[^}]*\batomicWriteFileSync\b[^}]*\}\s*from\s*['"]\.\.\/principle-tree-ledger/);
|
|
3115
|
+
});
|
|
3116
|
+
it('runtime-v2/index.ts barrel re-exports LedgerTreeStore type from pure types module, not from principle-tree-ledger', async () => {
|
|
3117
|
+
const src = await readSrc('index.ts');
|
|
3118
|
+
// The type must come from the pure types module (./types/ledger-store.js),
|
|
3119
|
+
// NOT from the I/O module (../principle-tree-ledger.js).
|
|
3120
|
+
expect(src).toMatch(/export\s+type\s+\{\s*LedgerTreeStore\s*\}\s*from\s*['"]\.\/types\/ledger-store\.js['"]/);
|
|
3121
|
+
expect(src).not.toMatch(/export\s+type\s+\{\s*LedgerTreeStore\s*\}\s*from\s*['"]\.\.\/principle-tree-ledger\.js['"]/);
|
|
3122
|
+
});
|
|
3123
|
+
});
|
|
3124
|
+
// ── PRI-450: Core I/O whitelist guard ──────────────────────────────────────────
|
|
3125
|
+
//
|
|
3126
|
+
// Scans all production (.ts, non-test) files under packages/principles-core/src/
|
|
3127
|
+
// for fs/path imports and verifies they match an explicit whitelist. Any new file
|
|
3128
|
+
// that needs I/O must be added to ALLOWED_IO_FILES here — otherwise CI fails.
|
|
3129
|
+
// This forces developers to explain "why does this file need I/O?" in their PR.
|
|
3130
|
+
describe('PRI-450: core I/O whitelist guard', () => {
|
|
3131
|
+
// Whitelist of production files allowed to import fs/path.
|
|
3132
|
+
// Paths are relative to packages/principles-core/src/.
|
|
3133
|
+
const ALLOWED_IO_FILES = new Set([
|
|
3134
|
+
'principle-tree-ledger.ts',
|
|
3135
|
+
'evolution-store.ts',
|
|
3136
|
+
'trajectory-store.ts',
|
|
3137
|
+
'workflow-funnel-loader.ts',
|
|
3138
|
+
'runtime-v2/store/sqlite-connection.ts',
|
|
3139
|
+
'runtime-v2/store/runtime-state-manager.ts',
|
|
3140
|
+
'runtime-v2/adapter/openclaw-cli-runtime-adapter.ts',
|
|
3141
|
+
'runtime-v2/candidate-audit.ts',
|
|
3142
|
+
'runtime-v2/pain-signal-observability.ts',
|
|
3143
|
+
'runtime-v2/internalization-chain-integrity-read-model.ts',
|
|
3144
|
+
'runtime-v2/internalization-integrity-remediation.ts',
|
|
3145
|
+
'runtime-v2/operator-health-read-model.ts',
|
|
3146
|
+
'runtime-v2/pain-chain-read-model.ts',
|
|
3147
|
+
'runtime-v2/pruning-read-model.ts',
|
|
3148
|
+
'runtime-v2/pruning-review-log.ts',
|
|
3149
|
+
'runtime-v2/schema-conformance-read-model.ts',
|
|
3150
|
+
]);
|
|
3151
|
+
// Extract all import module paths from source code.
|
|
3152
|
+
// Handles: import ... from 'mod', import 'mod' (side-effect), and multiline imports.
|
|
3153
|
+
// Reuses the same pattern proven in the PRI-215 extractImportModulePaths helper.
|
|
3154
|
+
function extractImportPaths(src) {
|
|
3155
|
+
const paths = [];
|
|
3156
|
+
for (const m of src.matchAll(/import\s+[\s\S]*?from\s+['"]([^'"]+)['"]/g)) {
|
|
3157
|
+
if (m[1] != null)
|
|
3158
|
+
paths.push(m[1]);
|
|
3159
|
+
}
|
|
3160
|
+
for (const m of src.matchAll(/import\s+['"]([^'"]+)['"]/g)) {
|
|
3161
|
+
if (m[1] != null && !paths.includes(m[1]))
|
|
3162
|
+
paths.push(m[1]);
|
|
3163
|
+
}
|
|
3164
|
+
return paths;
|
|
3165
|
+
}
|
|
3166
|
+
// Check if any import path references fs or path (including sub-paths like fs/promises).
|
|
3167
|
+
function importsFsOrPath(src) {
|
|
3168
|
+
const paths = extractImportPaths(src);
|
|
3169
|
+
return paths.some((p) => p === 'fs' || p.startsWith('fs/') ||
|
|
3170
|
+
p === 'node:fs' || p.startsWith('node:fs/') ||
|
|
3171
|
+
p === 'path' || p.startsWith('path/') ||
|
|
3172
|
+
p === 'node:path' || p.startsWith('node:path/'));
|
|
3173
|
+
}
|
|
3174
|
+
function collectTsFiles(dir, acc) {
|
|
3175
|
+
const entries = fsSync.readdirSync(dir, { withFileTypes: true });
|
|
3176
|
+
for (const entry of entries) {
|
|
3177
|
+
const fullPath = pathSync.join(dir, entry.name);
|
|
3178
|
+
if (entry.isDirectory()) {
|
|
3179
|
+
collectTsFiles(fullPath, acc);
|
|
3180
|
+
}
|
|
3181
|
+
else if (entry.isFile() &&
|
|
3182
|
+
entry.name.endsWith('.ts') &&
|
|
3183
|
+
!entry.name.endsWith('.test.ts') &&
|
|
3184
|
+
!entry.name.endsWith('.spec.ts')) {
|
|
3185
|
+
acc.push(fullPath);
|
|
3186
|
+
}
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
it('core/src/ production files: only whitelisted files import fs/path', () => {
|
|
3190
|
+
const coreSrcDir = pathSync.resolve(__dirname, '..', '..');
|
|
3191
|
+
const allFiles = [];
|
|
3192
|
+
collectTsFiles(coreSrcDir, allFiles);
|
|
3193
|
+
const violations = [];
|
|
3194
|
+
for (const filePath of allFiles) {
|
|
3195
|
+
const content = fsSync.readFileSync(filePath, 'utf-8');
|
|
3196
|
+
if (importsFsOrPath(content)) {
|
|
3197
|
+
const relPath = pathSync.relative(coreSrcDir, filePath).replace(/\\/g, '/');
|
|
3198
|
+
if (!ALLOWED_IO_FILES.has(relPath)) {
|
|
3199
|
+
violations.push(relPath);
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
expect(violations).toEqual([]);
|
|
3204
|
+
});
|
|
3205
|
+
it('ALLOWED_IO_FILES whitelist entries all exist on disk', () => {
|
|
3206
|
+
const coreSrcDir = pathSync.resolve(__dirname, '..', '..');
|
|
3207
|
+
const missing = [];
|
|
3208
|
+
for (const relPath of ALLOWED_IO_FILES) {
|
|
3209
|
+
const fullPath = pathSync.join(coreSrcDir, ...relPath.split('/'));
|
|
3210
|
+
if (!fsSync.existsSync(fullPath)) {
|
|
3211
|
+
missing.push(relPath);
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
expect(missing).toEqual([]);
|
|
3215
|
+
});
|
|
3216
|
+
it('importsFsOrPath detects sub-path imports (fs/promises) and side-effect imports', () => {
|
|
3217
|
+
// Sub-path import
|
|
3218
|
+
expect(importsFsOrPath(`import { mkdir } from 'node:fs/promises';`)).toBe(true);
|
|
3219
|
+
expect(importsFsOrPath(`import { posix } from 'path/posix';`)).toBe(true);
|
|
3220
|
+
// Side-effect import
|
|
3221
|
+
expect(importsFsOrPath(`import 'fs';`)).toBe(true);
|
|
3222
|
+
expect(importsFsOrPath(`import "node:path";`)).toBe(true);
|
|
3223
|
+
// Standard import
|
|
3224
|
+
expect(importsFsOrPath(`import * as fs from 'fs';`)).toBe(true);
|
|
3225
|
+
expect(importsFsOrPath(`import { resolve } from 'node:path';`)).toBe(true);
|
|
3226
|
+
// Non-fs/path imports
|
|
3227
|
+
expect(importsFsOrPath(`import { foo } from 'bar';`)).toBe(false);
|
|
3228
|
+
expect(importsFsOrPath(``)).toBe(false);
|
|
3229
|
+
});
|
|
3230
|
+
});
|
|
3057
3231
|
//# sourceMappingURL=architecture-regression.test.js.map
|