@principles/core 1.179.0 → 1.181.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 +31 -13
- package/dist/runtime-v2/__tests__/architecture-regression.test.js +90 -30
- package/dist/runtime-v2/__tests__/architecture-regression.test.js.map +1 -1
- package/dist/runtime-v2/__tests__/pain-signal-bridge-result-shaping.test.js +105 -0
- package/dist/runtime-v2/__tests__/pain-signal-bridge-result-shaping.test.js.map +1 -1
- package/dist/runtime-v2/feature-flags/__tests__/feature-flag-contract.test.js +23 -0
- package/dist/runtime-v2/feature-flags/__tests__/feature-flag-contract.test.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,27 +3,45 @@
|
|
|
3
3
|
> **Pure logic only — no I/O.** This package must not import `fs`, `path`, or any
|
|
4
4
|
> I/O module. All filesystem/DB/network operations belong in `openclaw-plugin`.
|
|
5
5
|
|
|
6
|
-
## Boundary enforcement (PRI-450)
|
|
6
|
+
## Boundary enforcement (PRI-450 / PRI-462)
|
|
7
7
|
|
|
8
8
|
Three automated layers prevent I/O from leaking into core:
|
|
9
9
|
|
|
10
|
-
1. **
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
1. **I/O seam registry** — `io-seam-registry.json` is the single source of truth
|
|
11
|
+
for which production files may import `fs`/`path`. Each file belongs to a
|
|
12
|
+
named seam with a description explaining WHY the I/O is allowed. Both the
|
|
13
|
+
architecture-regression test and ESLint derive their exemption lists from
|
|
14
|
+
this registry — there is no second hand-maintained list.
|
|
15
|
+
|
|
16
|
+
2. **Architecture-regression test** — `runtime-v2/__tests__/architecture-regression.test.ts`
|
|
17
|
+
loads the registry, scans every `.ts` file under `src/` for `fs`/`path` imports,
|
|
18
|
+
and verifies they match the registry. Any new I/O file must be added to the
|
|
19
|
+
registry — otherwise CI fails.
|
|
20
|
+
|
|
21
|
+
3. **ESLint `no-restricted-imports`** — `eslint.config.js` bans `fs`/`path` imports
|
|
22
|
+
in `packages/principles-core/src/`. The exemption list is generated from the
|
|
23
|
+
registry at lint time.
|
|
24
|
+
|
|
25
|
+
### Named I/O seams
|
|
26
|
+
|
|
27
|
+
| Seam | Files | Why allowed |
|
|
28
|
+
|------|-------|-------------|
|
|
29
|
+
| `ledger-tree` | `principle-tree-ledger.ts` | File-based principle ledger SSOT; pd-cli reads/writes without importing openclaw-plugin |
|
|
30
|
+
| `sdk-store` | `evolution-store.ts`, `trajectory-store.ts`, `workflow-funnel-loader.ts` | Standalone SDK store primitives exposed as package subpaths |
|
|
31
|
+
| `sqlite-state-store` | `runtime-v2/store/sqlite-connection.ts`, `runtime-v2/store/runtime-state-manager.ts` | runtime-v2 state.db connection + manager |
|
|
32
|
+
| `cli-runtime-adapter` | `runtime-v2/adapter/openclaw-cli-runtime-adapter.ts` | Spawns openclaw CLI process |
|
|
33
|
+
| `read-model` | 5 `*read-model*.ts` files | Read-only composite models querying state.db + ledger |
|
|
34
|
+
| `audit-observability` | `candidate-audit.ts`, `pain-signal-observability.ts` | Audit + pain signal observability writers |
|
|
35
|
+
| `remediation-review` | `internalization-integrity-remediation.ts`, `pruning-review-log.ts` | Integrity remediation + pruning review log |
|
|
19
36
|
|
|
20
37
|
### Adding a new I/O file (rare — prefer plugin)
|
|
21
38
|
|
|
22
39
|
If a core file genuinely needs I/O:
|
|
23
40
|
|
|
24
|
-
1. Add the file path to
|
|
25
|
-
|
|
26
|
-
|
|
41
|
+
1. Add the file path to the appropriate seam in `io-seam-registry.json`
|
|
42
|
+
(or create a new seam with a name and description)
|
|
43
|
+
2. Explain in the PR why the I/O cannot live in `openclaw-plugin`
|
|
44
|
+
3. Both the architecture-regression test and ESLint exemption update automatically
|
|
27
45
|
|
|
28
46
|
## Usage
|
|
29
47
|
|
|
@@ -3192,33 +3192,31 @@ describe('PRI-443: pure module boundary guards', () => {
|
|
|
3192
3192
|
expect(src).not.toMatch(/export\s+type\s+\{\s*LedgerTreeStore\s*\}\s*from\s*['"]\.\.\/principle-tree-ledger\.js['"]/);
|
|
3193
3193
|
});
|
|
3194
3194
|
});
|
|
3195
|
-
|
|
3196
|
-
//
|
|
3197
|
-
//
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
//
|
|
3203
|
-
//
|
|
3204
|
-
|
|
3205
|
-
'
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
'runtime-v2/schema-conformance-read-model.ts',
|
|
3221
|
-
]);
|
|
3195
|
+
describe('PRI-450 / PRI-462: core I/O seam registry guard', () => {
|
|
3196
|
+
// Registry lives at packages/principles-core/io-seam-registry.json
|
|
3197
|
+
// (3 levels up from __tests__: __tests__ → runtime-v2 → src → principles-core)
|
|
3198
|
+
const registryPath = pathSync.resolve(__dirname, '..', '..', '..', 'io-seam-registry.json');
|
|
3199
|
+
const registryRaw = fsSync.readFileSync(registryPath, 'utf-8');
|
|
3200
|
+
const registry = JSON.parse(registryRaw);
|
|
3201
|
+
// Runtime-validate the registry shape (Runtime Contract Rule 1+2: treat
|
|
3202
|
+
// parsed JSON as unknown, validate with typeof/Array.isArray before access).
|
|
3203
|
+
// `as` is used only for type narrowing AFTER runtime checks, not to bypass them.
|
|
3204
|
+
function isValidRegistry(v) {
|
|
3205
|
+
if (typeof v !== 'object' || v === null || !Object.hasOwn(v, 'seams'))
|
|
3206
|
+
return false;
|
|
3207
|
+
const { seams } = v;
|
|
3208
|
+
if (!Array.isArray(seams))
|
|
3209
|
+
return false;
|
|
3210
|
+
return seams.every((s) => typeof s === 'object' && s !== null &&
|
|
3211
|
+
typeof s.name === 'string' &&
|
|
3212
|
+
typeof s.description === 'string' &&
|
|
3213
|
+
Array.isArray(s.files) &&
|
|
3214
|
+
s.files.every((f) => typeof f === 'string'));
|
|
3215
|
+
}
|
|
3216
|
+
// Flatten all registry files into a Set (relative to packages/principles-core/src/).
|
|
3217
|
+
const ALLOWED_IO_FILES = new Set(isValidRegistry(registry)
|
|
3218
|
+
? registry.seams.flatMap((s) => s.files)
|
|
3219
|
+
: []);
|
|
3222
3220
|
// Extract all import module paths from source code.
|
|
3223
3221
|
// Handles: import ... from 'mod', import 'mod' (side-effect), and multiline imports.
|
|
3224
3222
|
// Reuses the same pattern proven in the PRI-215 extractImportModulePaths helper.
|
|
@@ -3257,7 +3255,52 @@ describe('PRI-450: core I/O whitelist guard', () => {
|
|
|
3257
3255
|
}
|
|
3258
3256
|
}
|
|
3259
3257
|
}
|
|
3260
|
-
|
|
3258
|
+
// ── Registry validity (PRI-462) ───────────────────────────────────────────
|
|
3259
|
+
it('io-seam-registry.json is valid: seams array with name/description/files', () => {
|
|
3260
|
+
expect(isValidRegistry(registry)).toBe(true);
|
|
3261
|
+
if (!isValidRegistry(registry))
|
|
3262
|
+
return; // type narrowing
|
|
3263
|
+
expect(registry.seams.length).toBeGreaterThan(0);
|
|
3264
|
+
for (const seam of registry.seams) {
|
|
3265
|
+
expect(seam.name.length).toBeGreaterThan(0);
|
|
3266
|
+
expect(seam.description.length).toBeGreaterThan(0);
|
|
3267
|
+
expect(seam.files.length).toBeGreaterThan(0);
|
|
3268
|
+
}
|
|
3269
|
+
});
|
|
3270
|
+
it('registry has no duplicate files across seams', () => {
|
|
3271
|
+
if (!isValidRegistry(registry)) {
|
|
3272
|
+
// isValidRegistry test above will fail with details; skip here.
|
|
3273
|
+
expect(isValidRegistry(registry)).toBe(true);
|
|
3274
|
+
return;
|
|
3275
|
+
}
|
|
3276
|
+
const seen = new Set();
|
|
3277
|
+
const dupes = [];
|
|
3278
|
+
for (const seam of registry.seams) {
|
|
3279
|
+
for (const f of seam.files) {
|
|
3280
|
+
if (seen.has(f))
|
|
3281
|
+
dupes.push(f);
|
|
3282
|
+
seen.add(f);
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
expect(dupes).toEqual([]);
|
|
3286
|
+
});
|
|
3287
|
+
it('every seam description explains why the I/O is allowed (non-trivial)', () => {
|
|
3288
|
+
if (!isValidRegistry(registry)) {
|
|
3289
|
+
expect(isValidRegistry(registry)).toBe(true);
|
|
3290
|
+
return;
|
|
3291
|
+
}
|
|
3292
|
+
const weak = [];
|
|
3293
|
+
for (const seam of registry.seams) {
|
|
3294
|
+
// Description must be at least 20 chars — a single word like "legacy" is
|
|
3295
|
+
// not an explanation. This forces maintainers to write a real rationale.
|
|
3296
|
+
if (seam.description.trim().length < 20) {
|
|
3297
|
+
weak.push(`${seam.name}: "${seam.description}"`);
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
expect(weak).toEqual([]);
|
|
3301
|
+
});
|
|
3302
|
+
// ── Registry ↔ production files (PRI-450, EP-02) ──────────────────────────
|
|
3303
|
+
it('core/src/ production files: only registry-listed files import fs/path', () => {
|
|
3261
3304
|
const coreSrcDir = pathSync.resolve(__dirname, '..', '..');
|
|
3262
3305
|
const allFiles = [];
|
|
3263
3306
|
collectTsFiles(coreSrcDir, allFiles);
|
|
@@ -3271,9 +3314,13 @@ describe('PRI-450: core I/O whitelist guard', () => {
|
|
|
3271
3314
|
}
|
|
3272
3315
|
}
|
|
3273
3316
|
}
|
|
3274
|
-
|
|
3317
|
+
if (violations.length > 0) {
|
|
3318
|
+
throw new Error(`Found ${violations.length} file(s) importing fs/path that are NOT in io-seam-registry.json.\n` +
|
|
3319
|
+
`Add them to the appropriate seam in packages/principles-core/io-seam-registry.json.\n` +
|
|
3320
|
+
`Violations:\n ` + violations.join('\n '));
|
|
3321
|
+
}
|
|
3275
3322
|
});
|
|
3276
|
-
it('
|
|
3323
|
+
it('registry-listed files all exist on disk', () => {
|
|
3277
3324
|
const coreSrcDir = pathSync.resolve(__dirname, '..', '..');
|
|
3278
3325
|
const missing = [];
|
|
3279
3326
|
for (const relPath of ALLOWED_IO_FILES) {
|
|
@@ -3284,6 +3331,19 @@ describe('PRI-450: core I/O whitelist guard', () => {
|
|
|
3284
3331
|
}
|
|
3285
3332
|
expect(missing).toEqual([]);
|
|
3286
3333
|
});
|
|
3334
|
+
// ── Registry ↔ ESLint exemption (PRI-462, EP-06 single source of truth) ───
|
|
3335
|
+
//
|
|
3336
|
+
// ESLint's no-restricted-imports exemption list must be DERIVED from the
|
|
3337
|
+
// registry, not hand-maintained. This test verifies eslint.config.js
|
|
3338
|
+
// references the registry file, proving the two stay in sync.
|
|
3339
|
+
it('eslint.config.js derives fs/path exemption from the registry (no shadow list)', () => {
|
|
3340
|
+
const eslintPath = pathSync.resolve(__dirname, '..', '..', '..', '..', '..', 'eslint.config.js');
|
|
3341
|
+
const eslintSrc = fsSync.readFileSync(eslintPath, 'utf-8');
|
|
3342
|
+
// The eslint config must read the registry JSON — if this reference is
|
|
3343
|
+
// removed, someone has reintroduced a hand-maintained shadow list.
|
|
3344
|
+
expect(eslintSrc).toContain('io-seam-registry.json');
|
|
3345
|
+
});
|
|
3346
|
+
// ── Helper unit tests ─────────────────────────────────────────────────────
|
|
3287
3347
|
it('importsFsOrPath detects sub-path imports (fs/promises) and side-effect imports', () => {
|
|
3288
3348
|
// Sub-path import
|
|
3289
3349
|
expect(importsFsOrPath(`import { mkdir } from 'node:fs/promises';`)).toBe(true);
|