Skip to content

Commit 82247fc

Browse files
os-zhuangclaude
andcommitted
feat(objectql): add lean @objectstack/objectql/core entry + D2 boundary ratchet (ADR-0076)
A thin embedder (e.g. the objectbase gateway) can import `@objectstack/objectql/core` to get the engine/registry/hooks/validation surface with NO kernel plugin, kernel factory, or metadata protocol — so `@objectstack/metadata-protocol` (and its 268KB) never enters its bundle/graph. - New src/core.ts lean barrel; objectql now builds two entries (index + core) via a local tsup config; package.json gains the `./core` export. - New core-boundary ratchet test walks core.ts's full import closure and fails if anything reachable imports @objectstack/metadata-protocol / ./plugin / ./kernel-factory. Verified: build emits dist/core.{mjs,js,d.ts} with 0 references to metadata-protocol (vs 5 in index); runtime smoke confirms ObjectQL is exported and ObjectQLPlugin / ObjectStackProtocolImplementation are not; objectql suite 58 files / 719 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 9401878 commit 82247fc

5 files changed

Lines changed: 175 additions & 1 deletion

File tree

.changeset/extract-metadata-protocol.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,5 @@ The protocol no longer depends on the concrete `ObjectQL` class — it is typed
1212
**Non-breaking**: `@objectstack/objectql` re-exports every previously public symbol (`ObjectStackProtocolImplementation`, `SysMetadataRepository`, `SysMetadataEngine`, `SeedLoaderService`, `runBuildProbes`, …), so existing imports keep working.
1313

1414
This is Step 1 of ADR-0076. A later step turns the protocol into a capability plugin so `objectql` itself stops depending on it (making the engine lean by construction).
15+
16+
Also adds a lean **`@objectstack/objectql/core`** entry — the engine/registry/hooks/validation surface only, with no kernel plugin or metadata protocol — so a thin embedder can import just the engine and never pull `@objectstack/metadata-protocol` into its bundle. A boundary ratchet test guards the entry.

packages/objectql/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,15 @@
1010
"types": "./dist/index.d.ts",
1111
"import": "./dist/index.mjs",
1212
"require": "./dist/index.js"
13+
},
14+
"./core": {
15+
"types": "./dist/core.d.ts",
16+
"import": "./dist/core.mjs",
17+
"require": "./dist/core.js"
1318
}
1419
},
1520
"scripts": {
16-
"build": "tsup --config ../../tsup.config.ts",
21+
"build": "tsup",
1722
"test": "vitest run"
1823
},
1924
"dependencies": {
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// ADR-0076 D2 boundary ratchet. The lean engine entry `@objectstack/objectql/core`
4+
// (src/core.ts) and its entire local import closure must NOT depend on the kernel
5+
// plugin, the kernel factory, or the metadata-management protocol — so a thin
6+
// embedder importing `@objectstack/objectql/core` never pulls
7+
// `@objectstack/metadata-protocol` (or its 268KB) into its graph.
8+
//
9+
// If this test fails, you added a forbidden import somewhere reachable from
10+
// core.ts. Keep metadata/plugin/kernel concerns out of the core closure.
11+
12+
import { describe, it, expect } from 'vitest';
13+
import { readFileSync } from 'node:fs';
14+
import { dirname, resolve } from 'node:path';
15+
import { fileURLToPath } from 'node:url';
16+
17+
const SRC = dirname(fileURLToPath(import.meta.url));
18+
19+
const FORBIDDEN_PACKAGES = ['@objectstack/metadata-protocol'];
20+
const FORBIDDEN_LOCAL = ['plugin', 'kernel-factory'];
21+
22+
function localImports(source: string): string[] {
23+
const out: string[] = [];
24+
const re = /(?:from|import)\s*\(?\s*['"](\.\.?\/[^'"]+)['"]/g;
25+
let m: RegExpExecArray | null;
26+
while ((m = re.exec(source))) out.push(m[1]);
27+
return out;
28+
}
29+
30+
function toTsPath(fromFile: string, spec: string): string {
31+
const base = resolve(dirname(fromFile), spec.replace(/\.js$/, ''));
32+
return base.endsWith('.ts') ? base : `${base}.ts`;
33+
}
34+
35+
describe('ADR-0076 D2 — @objectstack/objectql/core boundary', () => {
36+
it('core.ts closure pulls neither metadata-protocol nor plugin/kernel-factory', () => {
37+
const entry = resolve(SRC, 'core.ts');
38+
const visited = new Set<string>();
39+
const violations: string[] = [];
40+
const stack = [entry];
41+
42+
while (stack.length) {
43+
const file = stack.pop()!;
44+
if (visited.has(file)) continue;
45+
visited.add(file);
46+
47+
let src: string;
48+
try {
49+
src = readFileSync(file, 'utf8');
50+
} catch {
51+
continue; // generated / non-existent; ignore
52+
}
53+
54+
for (const pkg of FORBIDDEN_PACKAGES) {
55+
if (new RegExp(`['"]${pkg.replace('/', '\\/')}['"]`).test(src)) {
56+
violations.push(`${file} imports forbidden package ${pkg}`);
57+
}
58+
}
59+
60+
for (const spec of localImports(src)) {
61+
const base = spec.replace(/\.js$/, '').split('/').pop();
62+
if (FORBIDDEN_LOCAL.includes(base ?? '')) {
63+
violations.push(`${file} imports forbidden local module ./${base}`);
64+
}
65+
stack.push(toTsPath(file, spec));
66+
}
67+
}
68+
69+
expect(violations, violations.join('\n')).toEqual([]);
70+
// sanity: the engine itself IS in the closure
71+
expect([...visited].some((f) => f.endsWith('/engine.ts'))).toBe(true);
72+
});
73+
});

packages/objectql/src/core.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// Lean engine entry (ADR-0076). Exposes the data engine surface — engine,
4+
// registry, hooks, validation, in-memory aggregation, utilities — WITHOUT the
5+
// kernel plugin (`ObjectQLPlugin`), the kernel factory, or any metadata
6+
// management (`@objectstack/metadata-protocol`). Embedders that want only the
7+
// engine (e.g. a thin gateway) import from `@objectstack/objectql/core` so the
8+
// 268KB metadata protocol is never pulled into their dependency graph.
9+
//
10+
// A boundary ratchet (ADR-0076 D2) keeps this entry free of protocol/plugin
11+
// imports; do not add `./plugin`, `./kernel-factory`, or `@objectstack/metadata-protocol`
12+
// re-exports here.
13+
14+
// Registry
15+
export {
16+
SchemaRegistry,
17+
applySystemFields,
18+
computeFQN,
19+
parseFQN,
20+
RESERVED_NAMESPACES,
21+
DEFAULT_OWNER_PRIORITY,
22+
DEFAULT_EXTENDER_PRIORITY,
23+
} from './registry.js';
24+
export type { ObjectContributor, SchemaRegistryOptions } from './registry.js';
25+
26+
// Engine
27+
export { ObjectQL, ObjectRepository, ScopedContext } from './engine.js';
28+
export type { ObjectQLHostContext, HookHandler, HookEntry, OperationContext, EngineMiddleware } from './engine.js';
29+
30+
// In-memory aggregation fallback
31+
export { applyInMemoryAggregation, bucketDateValue } from './in-memory-aggregation.js';
32+
33+
// Hook binder & wrappers (declarative-metadata → engine glue)
34+
export { bindHooksToEngine } from './hook-binder.js';
35+
export type { BindHooksOptions, BindHooksResult } from './hook-binder.js';
36+
export { wrapDeclarativeHook } from './hook-wrappers.js';
37+
export type { WrapDeclarativeOptions } from './hook-wrappers.js';
38+
39+
// Validation
40+
export { ValidationError, validateRecord } from './validation/record-validator.js';
41+
export type { FieldValidationError } from './validation/record-validator.js';
42+
export { evaluateValidationRules, needsPriorRecord, legalNextStates } from './validation/rule-validator.js';
43+
export type { EvaluateRulesOptions } from './validation/rule-validator.js';
44+
export {
45+
InMemoryHookMetricsRecorder,
46+
noopHookMetricsRecorder,
47+
} from './hook-metrics.js';
48+
export type {
49+
HookMetricsRecorder,
50+
HookMetricLabel,
51+
HookMetricOutcome,
52+
HookSkipReason,
53+
} from './hook-metrics.js';
54+
55+
// MetadataFacade
56+
export { MetadataFacade } from './metadata-facade.js';
57+
58+
// Secret-field channel helpers
59+
export {
60+
SECRET_REF_PREFIX,
61+
SECRET_MASK,
62+
makeSecretRef,
63+
isSecretRef,
64+
parseSecretRef,
65+
collectSecretFields,
66+
} from './secret-fields.js';
67+
68+
// Utilities
69+
export {
70+
toTitleCase,
71+
convertIntrospectedSchemaToObjects,
72+
} from './util.js';
73+
export type {
74+
IntrospectedColumn,
75+
IntrospectedForeignKey,
76+
IntrospectedTable,
77+
IntrospectedSchema,
78+
} from './util.js';

packages/objectql/tsup.config.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { defineConfig } from 'tsup';
4+
5+
export default defineConfig({
6+
// `core` is the lean engine entry (ADR-0076) — engine/registry/hooks/validation
7+
// only, no kernel plugin or @objectstack/metadata-protocol. `index` is the
8+
// batteries-included barrel.
9+
entry: ['src/index.ts', 'src/core.ts'],
10+
splitting: false,
11+
sourcemap: true,
12+
clean: true,
13+
dts: !process.env.OS_SKIP_DTS,
14+
format: ['esm', 'cjs'],
15+
target: 'es2020',
16+
});

0 commit comments

Comments
 (0)