Skip to content

Commit a683b6d

Browse files
committed
refactor(gjc): enforce that the engine does not depend on the app
A closed core is only possible while the part that would close depends on nothing it cannot take with it. The engine - the Rust core, the desktop shell, and the worker with its SDK adapter and protocol - shares no file path with the historical upstream, so it is the part that can move. That property held by construction rather than by rule, and one convenient import would have ended it without anyone noticing. `gjc-command-surface.generated.ts` is generated from the installed runtime but lived in server/modules/providers/, which forced the adapter to import the app to read it. It now sits beside the engine that generates it and the app imports it from there, which is the direction that survives a split. eslint now classifies the engine and fails any import from it into a backend module. The rule is last in the list because this plugin lets a later rule re-allow what an earlier one denied: written first, the barrel allowance above it silently reopened the door, and a mutation test importing through modules/database/index.js passed. Its first form also used `to` as a sibling of `disallow`, which the plugin ignores - that version rejected the engine's own internal imports, seventeen of them, while allowing exactly what it was written to stop. gjc-cli.js and gjc-worker-node-runtime.ts stay outside the classified engine rather than being exempted inside it. gjc-cli.js still reaches into the app for providerAuthService and the notification orchestrator, and only to supply defaults its caller can already override, so injecting that pair at the wiring point is the whole remaining job. Listing them now would describe a boundary that does not exist.
1 parent 476224b commit a683b6d

9 files changed

Lines changed: 97 additions & 7 deletions

‎docs/LICENSING.md‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,43 @@ The fix for both is a generated `THIRD-PARTY-NOTICES` produced at build time fro
9999
the shipped tree, so new dependencies are covered without anyone maintaining a
100100
list by hand.
101101

102+
## The engine boundary
103+
104+
A closed core is possible only if the part that would close depends on nothing
105+
it cannot take with it. That part exists and is measured:
106+
107+
| Layer | Files sharing a path with the historical upstream | Original to this project |
108+
| --- | --- | --- |
109+
| `native/` (Rust core) | 0 | 5,368 lines |
110+
| `src-tauri/src` (desktop shell) | 0 | 784 lines |
111+
| `server/gjc-*` (worker, adapter, protocol) | 0 | 13,397 lines |
112+
113+
The app may call into the engine. **The engine may not reach back**, and that is
114+
enforced rather than hoped for: `eslint.config.js` classifies the engine files as
115+
`gjc-engine` and fails any import from them into `server/modules/*`, barrel
116+
imports included. The rule sits last in the list because this plugin lets a later
117+
rule re-allow what an earlier one denied, and the barrel allowance above it would
118+
otherwise reopen the door.
119+
120+
One file moved to make that true: `gjc-command-surface.generated.ts` is generated
121+
from the installed runtime and was living in `server/modules/providers/`, so the
122+
engine had to import the app to read it. It now sits beside the engine that
123+
generates it, and the app imports it from there - the allowed direction.
124+
125+
### What is not separable yet
126+
127+
`server/gjc-cli.js` and `server/gjc-worker-node-runtime.ts` are the pre-worker
128+
spawn path, still loaded as the Node-only production runtime. `gjc-cli.js`
129+
imports `providerAuthService` and the notification orchestrator from the app,
130+
**and only to supply default values its caller can already override**
131+
(`gjc-cli.js:232-234`). Injecting that pair at the wiring point instead is the
132+
whole remaining job; the two files then join `gjc-engine` and the boundary is
133+
complete.
134+
135+
They are deliberately left out of the `gjc-engine` list rather than exempted
136+
inside it. A rule that skipped them quietly would describe a boundary that does
137+
not exist.
138+
102139
## Adding a dependency
103140

104141
1. Check the license before adding it. MIT, BSD, ISC, Apache-2.0 and MPL-2.0 are

‎eslint.config.js‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,44 @@ export default tseslint.config(
149149
}),
150150
],
151151
"boundaries/elements": [
152+
{
153+
// The GJC engine: the worker process, its SDK adapter, its tool policy
154+
// and the protocol between them. Every file here is original to this
155+
// project and imports nothing from the app around it, which is what
156+
// makes it separable from the AGPL shell. The rule below keeps it that
157+
// way, because the property is easy to lose and expensive to rebuild.
158+
//
159+
// Three files are deliberately absent. `gjc-worker-client.ts` is the
160+
// app's end of the protocol and runs in the app's process. `gjc-cli.js`
161+
// and `gjc-worker-node-runtime.ts` are the pre-worker spawn path, which
162+
// still reaches into the app for `providerAuthService` and the
163+
// notification orchestrator - and only to supply default values its
164+
// caller can already override (`gjc-cli.js:232-234`). Injecting that
165+
// pair at the wiring point instead is what moves them in here and
166+
// finishes the boundary; see docs/LICENSING.md. Listing them now would
167+
// make this rule a claim rather than a fact.
168+
type: "gjc-engine",
169+
pattern: [
170+
"server/gjc-agent-tools.ts",
171+
"server/gjc-automation-tools.ts",
172+
"server/gjc-bun-ask-controller.ts",
173+
"server/gjc-bun-oauth-controller.ts",
174+
"server/gjc-bun-sdk-adapter.ts",
175+
"server/gjc-bun-sdk-events.ts",
176+
"server/gjc-bun-worker.ts",
177+
"server/gjc-command-surface.generated.ts",
178+
"server/gjc-export-path.ts",
179+
"server/gjc-runtime-manifest.ts",
180+
"server/gjc-sdk-bridge.ts",
181+
"server/gjc-sdk-client.ts",
182+
"server/gjc-session-state.ts",
183+
"server/gjc-windows-job.ts",
184+
"server/gjc-worker.ts",
185+
"server/gjc-worker-protocol.ts",
186+
],
187+
mode: "file",
188+
},
189+
152190
{
153191
type: "backend-shared-type-contract", // shared backend type/interface contracts that modules may consume without creating runtime coupling
154192
pattern: [
@@ -253,6 +291,21 @@ export default tseslint.config(
253291
},
254292
}, // re-allow only public module entry points (barrel files)
255293
},
294+
{
295+
// Last, because boundaries lets a later rule re-allow what an
296+
// earlier one denied: the barrel allowance directly above would
297+
// otherwise let the engine import any module through its index.
298+
//
299+
// The engine is separable from the AGPL shell only while it depends
300+
// on nothing in it. That is true today by construction, not by
301+
// accident of what nobody has needed yet, and one convenient import
302+
// would end it silently. The app may call into the engine; the
303+
// engine may not reach back - through a barrel or otherwise.
304+
from: { type: "gjc-engine" },
305+
disallow: { to: { type: ["backend-module", "backend-legacy-runtime"] } },
306+
message:
307+
"The GJC engine may not import the app around it, barrel included. Move the shared piece next to the engine, or pass it through server/gjc-worker-protocol.ts.",
308+
},
256309
],
257310
},
258311
],

‎scripts/generate-command-surface.mjs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ const execFile = promisify(execFileCallback);
3333
const __dirname = path.dirname(fileURLToPath(import.meta.url));
3434
const rootDir = path.resolve(__dirname, '..');
3535
const bunPath = path.join(rootDir, 'dist-native', process.platform === 'win32' ? 'bun.exe' : 'bun');
36-
const outputPath = path.join(rootDir, 'server', 'modules', 'providers', 'gjc-command-surface.generated.ts');
36+
const outputPath = path.join(rootDir, 'server', 'gjc-command-surface.generated.ts');
3737
const update = process.argv.slice(2).includes('--update');
3838

3939
if (process.argv.slice(2).some((argument) => argument !== '--update')) {

‎server/gjc-bun-sdk-adapter.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { initTheme, theme } from '@gajae-code/coding-agent/modes/theme/theme';
1111
import { getSupportedEfforts } from '@gajae-code/ai/model-thinking';
1212

1313
import { GjcBunOAuthController, type GjcBunOAuthControllerOptions } from './gjc-bun-oauth-controller.js';
14-
import { GJC_APP_BUILTIN_COMMAND_NAMES } from './modules/providers/gjc-command-surface.generated.js';
14+
import { GJC_APP_BUILTIN_COMMAND_NAMES } from './gjc-command-surface.generated.js';
1515
import type { GjcWorkerOAuthRuntime, GjcWorkerRuntime, GjcWorkerWriter } from './gjc-worker.js';
1616
import { GjcBunAskController } from './gjc-bun-ask-controller.js';
1717
import { forwardPromptTerminal, forwardSdkEvent, normalizeBuiltinCommandStdout, type SdkRunState } from './gjc-bun-sdk-events.js';
File renamed without changes.

‎server/gjc-sdk-contract.bun.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
GJC_APP_BUILTIN_COMMANDS,
1414
GJC_APP_BUILTIN_COMMAND_ALIASES,
1515
GJC_APP_BUILTIN_COMMAND_NAMES,
16-
} from './modules/providers/gjc-command-surface.generated.js';
16+
} from './gjc-command-surface.generated.js';
1717
import {
1818
GjcBunSdkAdapter,
1919
createGjcBunSdkAdapter,
@@ -77,7 +77,7 @@ async function waitFor<T>(read: () => T | undefined): Promise<T> {
7777
* command it does not know is to forward the raw text to the model as a prompt.
7878
*/
7979
const generatedSurface = await readFile(
80-
join(process.cwd(), 'server/modules/providers/gjc-command-surface.generated.ts'),
80+
join(process.cwd(), 'server/gjc-command-surface.generated.ts'),
8181
'utf8',
8282
);
8383

‎server/modules/providers/list/gjc/gjc-skills.provider.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import matter from 'gray-matter';
66

77
import type { IProviderSkills } from '@/shared/interfaces.js';
88
import type { ProviderSkill, ProviderSkillListOptions } from '@/shared/types.js';
9-
import { GJC_BUNDLED_SKILLS } from '@/modules/providers/gjc-command-surface.generated.js';
9+
import { GJC_BUNDLED_SKILLS } from '@/gjc-command-surface.generated.js';
1010

1111
// Names and descriptions come from each SKILL.md in the installed runtime,
1212
// generated rather than transcribed: `BUNDLED_SKILLS` is not exported upstream,

‎server/modules/providers/services/provider-commands.service.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import path from 'node:path';
44

55
import matter from 'gray-matter';
66

7-
import { GJC_APP_BUILTIN_COMMANDS } from '@/modules/providers/gjc-command-surface.generated.js';
7+
import { GJC_APP_BUILTIN_COMMANDS } from '@/gjc-command-surface.generated.js';
88
import { projectsDb } from '@/modules/database/index.js';
99
import { AppError } from '@/shared/utils.js';
1010

‎server/modules/providers/tests/provider-commands.service.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import os from 'node:os';
44
import path from 'node:path';
55
import test from 'node:test';
66

7-
import { GJC_APP_BUILTIN_COMMANDS } from '@/modules/providers/gjc-command-surface.generated.js';
7+
import { GJC_APP_BUILTIN_COMMANDS } from '@/gjc-command-surface.generated.js';
88
import { createProviderCommandsService } from '@/modules/providers/services/provider-commands.service.js';
99

1010
async function writeCommand(root: string, relativePath: string, description: string): Promise<void> {

0 commit comments

Comments
 (0)