@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 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. **Architecture-regression test** — `runtime-v2/__tests__/architecture-regression.test.ts`
11
- scans every `.ts` file under `src/` for `fs`/`path` imports and compares against
12
- an explicit whitelist (`ALLOWED_IO_FILES`). Any new I/O file must be added there.
13
-
14
- 2. **ESLint `no-restricted-imports`** `eslint.config.js` bans `fs`/`path` imports
15
- in `packages/principles-core/src/`. Whitelisted files and test files are exempt.
16
-
17
- 3. **PR template & AGENTS.md** the PR checklist asks whether new `fs`/`path` imports
18
- were added; AGENTS.md lists "writing I/O code in core" as an anti-pattern trigger.
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 registrythere 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 `ALLOWED_IO_FILES` in `architecture-regression.test.ts`
25
- 2. Add the file path to the exemption list in `eslint.config.js`
26
- 3. Explain in the PR why the I/O cannot live in `openclaw-plugin`
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
- // ── PRI-450: Core I/O whitelist guard ──────────────────────────────────────────
3196
- //
3197
- // Scans all production (.ts, non-test) files under packages/principles-core/src/
3198
- // for fs/path imports and verifies they match an explicit whitelist. Any new file
3199
- // that needs I/O must be added to ALLOWED_IO_FILES here — otherwise CI fails.
3200
- // This forces developers to explain "why does this file need I/O?" in their PR.
3201
- describe('PRI-450: core I/O whitelist guard', () => {
3202
- // Whitelist of production files allowed to import fs/path.
3203
- // Paths are relative to packages/principles-core/src/.
3204
- const ALLOWED_IO_FILES = new Set([
3205
- 'principle-tree-ledger.ts',
3206
- 'evolution-store.ts',
3207
- 'trajectory-store.ts',
3208
- 'workflow-funnel-loader.ts',
3209
- 'runtime-v2/store/sqlite-connection.ts',
3210
- 'runtime-v2/store/runtime-state-manager.ts',
3211
- 'runtime-v2/adapter/openclaw-cli-runtime-adapter.ts',
3212
- 'runtime-v2/candidate-audit.ts',
3213
- 'runtime-v2/pain-signal-observability.ts',
3214
- 'runtime-v2/internalization-chain-integrity-read-model.ts',
3215
- 'runtime-v2/internalization-integrity-remediation.ts',
3216
- 'runtime-v2/operator-health-read-model.ts',
3217
- 'runtime-v2/pain-chain-read-model.ts',
3218
- 'runtime-v2/pruning-read-model.ts',
3219
- 'runtime-v2/pruning-review-log.ts',
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
- it('core/src/ production files: only whitelisted files import fs/path', () => {
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
- expect(violations).toEqual([]);
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('ALLOWED_IO_FILES whitelist entries all exist on disk', () => {
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);