@principles/core 1.170.0 → 1.172.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 ADDED
@@ -0,0 +1,38 @@
1
+ # @principles/core
2
+
3
+ > **Pure logic only — no I/O.** This package must not import `fs`, `path`, or any
4
+ > I/O module. All filesystem/DB/network operations belong in `openclaw-plugin`.
5
+
6
+ ## Boundary enforcement (PRI-450)
7
+
8
+ Three automated layers prevent I/O from leaking into core:
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.
19
+
20
+ ### Adding a new I/O file (rare — prefer plugin)
21
+
22
+ If a core file genuinely needs I/O:
23
+
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`
27
+
28
+ ## Usage
29
+
30
+ ```bash
31
+ npm install @principles/core
32
+ ```
33
+
34
+ ```typescript
35
+ import { /* ... */ } from '@principles/core';
36
+ ```
37
+
38
+ See `package.json` `exports` for available subpaths.
@@ -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',
@@ -3119,4 +3121,111 @@ describe('PRI-443: pure module boundary guards', () => {
3119
3121
  expect(src).not.toMatch(/export\s+type\s+\{\s*LedgerTreeStore\s*\}\s*from\s*['"]\.\.\/principle-tree-ledger\.js['"]/);
3120
3122
  });
3121
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
+ });
3122
3231
  //# sourceMappingURL=architecture-regression.test.js.map