arkgate 2.8.1 → 2.8.2

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/CHANGELOG.md CHANGED
@@ -2,6 +2,34 @@
2
2
 
3
3
  All notable changes to ArkGate (`arkgate`; formerly `ark-runtime-kernel`) are documented here.
4
4
 
5
+ ## Unreleased
6
+
7
+ ## 2.8.2 — 2026-07-09
8
+
9
+ Field-honesty patch (Next/UI host probe): no Nest false positives, no false ENFORCE on
10
+ ui-surface bags, honest `ark start` mode, Next proxy/middleware classification.
11
+ **No intentional CLI flag or JSON shape breaks.**
12
+
13
+ ### Fixed
14
+
15
+ - **Nest detection false positive:** bare `*.service.ts` / similar names no longer set
16
+ `nestFramework` without `@nestjs/*` or controller/module/gateway/resolver files (Next/Node
17
+ apps no longer get a spurious `nestjs+next` overlay).
18
+ - **False ENFORCE on UI bags:** doctor/report mode stays **ADAPT** when Domain+Persistence are
19
+ empty while Presentation dominates, or when core layers with files remain `optional: true`.
20
+ - **ui-surface / Next defaults:** drop whole-`src` and bare `**/lib/**` presentation bags;
21
+ classify conventional data clients (`lib/supabase`, `lib/airtable`, `lib/prisma`, …) as
22
+ **Persistence**; add Application patterns for actions/services.
23
+ - **Generated CI:** when `package.json` has `lint` / `typecheck` scripts, the installed
24
+ GitHub Actions workflow runs them before ark-check (closes deploy-path gaps for Next hosts).
25
+ - **`ark start` wrap-up mode:** prefers `ark-check --doctor` `operatingMode` over plan-only
26
+ `resolveOperatingMode` (default **adapt**, double-lock against false **ENFORCE**).
27
+ - **Next middleware / proxy:** ui-surface + Next overlay classify `src/proxy.ts`, root
28
+ `proxy.ts`, and classic `middleware.ts` as **Presentation** (Next 16 rename no longer
29
+ leaves the edge entry ungoverned).
30
+ - **Idempotent Next overlay:** re-applying framework overlays no longer yields
31
+ `frameworkOverlay: "next+next"`.
32
+
5
33
  ## 2.8.1 — 2026-07-09
6
34
 
7
35
  Runtime honesty release (roadmap **R8–R9**): EventBus publish pipeline decomposition and
@@ -217,6 +217,15 @@ export function applyFrameworkLayoutOverlays(config, root) {
217
217
  'src/**/error.tsx',
218
218
  'src/**/route.ts',
219
219
  'src/**/route.tsx',
220
+ // Next middleware edge entry (classic + Next 16 proxy rename)
221
+ 'src/middleware.ts',
222
+ 'src/middleware.js',
223
+ 'middleware.ts',
224
+ 'middleware.js',
225
+ 'src/proxy.ts',
226
+ 'src/proxy.js',
227
+ 'proxy.ts',
228
+ 'proxy.js',
220
229
  ]);
221
230
  mergeLayerPatterns(next, 'ApplicationOrchestration', [
222
231
  'src/features/**',
@@ -247,6 +256,21 @@ export function applyFrameworkLayoutOverlays(config, root) {
247
256
  'src/lib/db/**',
248
257
  'src/lib/prisma/**',
249
258
  'src/server/db/**',
259
+ // Conventional client data bags under lib/ (higher specificity than bare src/lib/**).
260
+ 'src/lib/supabase/**',
261
+ 'src/lib/airtable/**',
262
+ 'src/lib/firebase/**',
263
+ 'src/lib/firestore/**',
264
+ 'src/lib/mongodb/**',
265
+ 'src/lib/mongoose/**',
266
+ 'src/lib/drizzle/**',
267
+ 'src/lib/kysely/**',
268
+ 'src/lib/planetscale/**',
269
+ 'src/lib/neon/**',
270
+ '**/lib/supabase/**',
271
+ '**/lib/airtable/**',
272
+ '**/lib/prisma/**',
273
+ '**/lib/db/**',
250
274
  ]);
251
275
  // Demo assets, generated public output, and tool configs are not architecture surface.
252
276
  const nextExcludes = [
@@ -262,9 +286,12 @@ export function applyFrameworkLayoutOverlays(config, root) {
262
286
  '**/scripts/**',
263
287
  ];
264
288
  next.exclude = [...new Set([...(next.exclude ?? []), ...nextExcludes])];
265
- next.frameworkOverlay = next.frameworkOverlay
266
- ? `${next.frameworkOverlay}+next`
267
- : 'next';
289
+ // Idempotent: re-applying overlays (e.g. re-init / re-start) must not yield "next+next".
290
+ if (!String(next.frameworkOverlay || '').split('+').includes('next')) {
291
+ next.frameworkOverlay = next.frameworkOverlay
292
+ ? `${next.frameworkOverlay}+next`
293
+ : 'next';
294
+ }
268
295
  }
269
296
 
270
297
  if (signals.expressLike && !signals.nestFramework) {
@@ -323,15 +350,47 @@ export function applyFrameworkLayoutOverlays(config, root) {
323
350
  /**
324
351
  * Operating mode for the co-pilot surfaces (not "who the user is"):
325
352
  * suggest | adapt | enforce
353
+ *
354
+ * ENFORCE means gates can honestly protect the tree. High governed% alone is not
355
+ * enough when core layers are empty (presentation bag false-green) or core layers
356
+ * with real files remain optional: true.
357
+ *
358
+ * @param {object} opts
359
+ * @param {number|null} [opts.governedPercent]
360
+ * @param {boolean|null} [opts.planMet]
361
+ * @param {boolean} [opts.mature]
362
+ * @param {number|null} [opts.totalFiles]
363
+ * @param {string[]} [opts.emptyLayers] layer names with zero matched files
364
+ * @param {number} [opts.coreOptionalWithFiles] count of core layers that have files but optional:true
365
+ * @param {number|null} [opts.presentationShare] PresentationAdapters file share 0..1 when known
326
366
  */
327
367
  export function resolveOperatingMode({
328
368
  governedPercent = null,
329
369
  planMet = null,
330
370
  mature = false,
331
371
  totalFiles = null,
372
+ emptyLayers = null,
373
+ coreOptionalWithFiles = 0,
374
+ presentationShare = null,
332
375
  } = {}) {
333
376
  // Zero files in scope is never ENFORCE — the contract is not looking at any code.
334
377
  if (totalFiles === 0) return 'adapt';
378
+ // Core layers still optional while holding real files → contract weaker than the tree.
379
+ if (coreOptionalWithFiles > 0) return 'adapt';
380
+ // Presentation-bag false green: almost everything is Presentation, Domain+Persistence empty.
381
+ const empty = Array.isArray(emptyLayers) ? emptyLayers : [];
382
+ const domainEmpty = empty.includes('DomainModel');
383
+ const persistenceEmpty = empty.includes('PersistenceAdapters');
384
+ if (
385
+ planMet === true &&
386
+ domainEmpty &&
387
+ persistenceEmpty &&
388
+ presentationShare != null &&
389
+ presentationShare >= 0.5 &&
390
+ (governedPercent ?? 0) >= 50
391
+ ) {
392
+ return 'adapt';
393
+ }
335
394
  if (planMet === true && (governedPercent == null || governedPercent >= 50)) return 'enforce';
336
395
  if (governedPercent != null && governedPercent < 50) return 'adapt';
337
396
  if (mature) return 'adapt';
@@ -1027,10 +1086,13 @@ export function collectRepoShapeSignals(root) {
1027
1086
 
1028
1087
  const topNames = new Set(srcDirs.flatMap((d) => listTopLevelDirNames(root, d)));
1029
1088
  // Framework / filename signals — strong enough that a tiny Nest starter is not a "prototype".
1089
+ // Nest detection: require real Nest surface (@nestjs/* or controller/module/gateway/resolver).
1090
+ // Do NOT treat bare `*.service.ts` / `*.guard.ts` as Nest — many Next/Node apps use those
1091
+ // names without Nest (false nestjs overlay + wrong doctor toolHints).
1030
1092
  const nestFramework =
1031
1093
  Object.keys(deps).some((name) => name.startsWith('@nestjs/')) ||
1032
1094
  sourceFiles.some((file) =>
1033
- /\.(controller|module|service|guard|interceptor|pipe)\.ts$/i.test(file)
1095
+ /\.(controller|module|gateway|resolver)\.ts$/i.test(file)
1034
1096
  );
1035
1097
  const nextFramework =
1036
1098
  Boolean(deps.next) ||
package/bin/ark.mjs CHANGED
@@ -393,27 +393,64 @@ async function start(args) {
393
393
  console.log('Your architecture plan:');
394
394
  runArkCheck(['--root', root, '--config', 'ark.config.json', '--plan'], { cwd: root });
395
395
 
396
- // Capture plan JSON for an honest wrap-up (governed% + goal.met) without re-printing.
396
+ // Capture plan + doctor JSON for an honest wrap-up. Mode MUST match --doctor
397
+ // (emptyLayers, core-optional, presentation bag) — never claim ENFORCE from plan alone.
397
398
  const planCapture = spawnSync(
398
399
  process.execPath,
399
400
  [arkCheck, '--root', root, '--config', 'ark.config.json', '--plan', '--json'],
400
401
  { cwd: root, encoding: 'utf8' }
401
402
  );
403
+ const doctorCapture = spawnSync(
404
+ process.execPath,
405
+ [arkCheck, '--root', root, '--config', 'ark.config.json', '--doctor', '--json'],
406
+ { cwd: root, encoding: 'utf8' }
407
+ );
402
408
  let planOk = true;
403
409
  let governedPercent = null;
404
- let mode = 'enforce'; // suggest | adapt | enforce
410
+ let mode = 'adapt'; // conservative default never default to enforce
405
411
  try {
406
412
  const parsed = JSON.parse(planCapture.stdout || '{}');
407
413
  planOk = parsed.ok === true && parsed.plan?.goal?.met === true;
408
414
  governedPercent = parsed.plan?.goal?.governedPercent ?? null;
409
415
  const totalFiles = parsed.plan?.goal?.totalFiles ?? null;
410
- mode = resolveOperatingMode({
411
- governedPercent:
412
- totalFiles === 0 ? 0 : governedPercent,
413
- planMet: parsed.plan?.goal?.met === true,
414
- mature: Boolean(rec?.mature),
415
- totalFiles,
416
- });
416
+
417
+ let doctorMode = null;
418
+ try {
419
+ const doc = JSON.parse(doctorCapture.stdout || '{}');
420
+ doctorMode = doc.doctor?.operatingMode ?? null;
421
+ // Prefer doctor's mode (includes emptyLayers + coreOptional + presentation-bag honesty).
422
+ if (doctorMode === 'suggest' || doctorMode === 'adapt' || doctorMode === 'enforce') {
423
+ mode = doctorMode;
424
+ } else {
425
+ // Fallback: recompute with honesty inputs from doctor payload when present.
426
+ const emptyLayers = doc.doctor?.emptyLayers ?? [];
427
+ const coreOptionalWithFiles = Array.isArray(doc.doctor?.adoption?.coreOptional)
428
+ ? doc.doctor.adoption.coreOptional.length
429
+ : 0;
430
+ const total = totalFiles || doc.doctor?.governed?.totalFiles || 0;
431
+ const presentationShare =
432
+ total > 0 && typeof doc.doctor?.governed?.percent === 'number'
433
+ ? null // presentationShare only from coverage layers when available
434
+ : null;
435
+ mode = resolveOperatingMode({
436
+ governedPercent: totalFiles === 0 ? 0 : governedPercent,
437
+ planMet: parsed.plan?.goal?.met === true,
438
+ mature: Boolean(rec?.mature),
439
+ totalFiles: totalFiles ?? total,
440
+ emptyLayers,
441
+ coreOptionalWithFiles,
442
+ presentationShare,
443
+ });
444
+ }
445
+ } catch {
446
+ mode = resolveOperatingMode({
447
+ governedPercent: totalFiles === 0 ? 0 : governedPercent,
448
+ planMet: parsed.plan?.goal?.met === true,
449
+ mature: Boolean(rec?.mature),
450
+ totalFiles,
451
+ });
452
+ }
453
+
417
454
  // Fresh greenfield with good coverage but no real tree yet → suggest, not enforce theatre.
418
455
  if (mode === 'enforce' && rec && !rec.mature && (governedPercent ?? 0) < 80) {
419
456
  mode = 'suggest';
@@ -423,6 +460,10 @@ async function start(args) {
423
460
  mode = 'adapt';
424
461
  planOk = false;
425
462
  }
463
+ // Never claim ENFORCE wrap-up if doctor would not (double-lock).
464
+ if (mode === 'enforce' && doctorMode && doctorMode !== 'enforce') {
465
+ mode = doctorMode;
466
+ }
426
467
  } catch {
427
468
  // If capture fails, stay conservative: don't claim full enforcement.
428
469
  mode = 'adapt';
@@ -483,7 +483,12 @@ export function detectCiNode(root) {
483
483
  return { kind: 'default', value: DEFAULT_CI_NODE_VERSION };
484
484
  }
485
485
 
486
- export function githubWorkflow(pm, ciNode) {
486
+ /**
487
+ * @param {{ name: string, install: string, run: string, cache: string, setup: string[] }} pm
488
+ * @param {{ kind: string, value: string }} ciNode
489
+ * @param {{ hasLintScript?: boolean, hasTypecheckScript?: boolean }} [quality]
490
+ */
491
+ export function githubWorkflow(pm, ciNode, quality = {}) {
487
492
  // pnpm/yarn setup (corepack enable) MUST run before actions/setup-node so the package
488
493
  // manager is on PATH when setup-node's `cache: pnpm|yarn` tries to resolve the store —
489
494
  // otherwise the cache step fails on a fresh runner ("Unable to locate executable file: pnpm").
@@ -500,6 +505,24 @@ export function githubWorkflow(pm, ciNode) {
500
505
  # of sync, your local package manager is newer than this Node's — add a
501
506
  # .nvmrc with your Node version so CI matches the dev environment.
502
507
  node-version: '${ciNode.value}'`;
508
+ // When package.json already has lint/typecheck, emit CI steps so deploy-path
509
+ // honesty matches local scripts (Next/CRA often run these in production build).
510
+ const install = pm.install || '';
511
+ const runPrefix = install.startsWith('pnpm')
512
+ ? 'pnpm run'
513
+ : install.startsWith('yarn')
514
+ ? 'yarn'
515
+ : install.startsWith('bun')
516
+ ? 'bun run'
517
+ : 'npm run';
518
+ const qualityBlock = [
519
+ quality.hasTypecheckScript
520
+ ? ` - name: Typecheck\n run: ${runPrefix} typecheck`
521
+ : '',
522
+ quality.hasLintScript ? ` - name: Lint\n run: ${runPrefix} lint` : '',
523
+ ]
524
+ .filter(Boolean)
525
+ .join('\n');
503
526
  return `name: Ark architecture gate
504
527
 
505
528
  on:
@@ -520,7 +543,7 @@ ${nodeSetup}
520
543
  cache: ${pm.cache}
521
544
  - name: Install dependencies
522
545
  run: ${pm.install}
523
- - name: Ark architecture check
546
+ ${qualityBlock ? `${qualityBlock}\n` : ''} - name: Ark architecture check
524
547
  run: ${pm.run}
525
548
  `;
526
549
  }
@@ -1608,7 +1631,13 @@ export function runInstallAgentGates(args) {
1608
1631
  templates.push(['.mcp.json', mcpJson(root)]);
1609
1632
  templates.push([
1610
1633
  '.github/workflows/ark-check.yml',
1611
- githubWorkflow(pm, detectCiNode(root)),
1634
+ (() => {
1635
+ const deploy = detectDeployPathQuality(root);
1636
+ return githubWorkflow(pm, detectCiNode(root), {
1637
+ hasLintScript: deploy.hasLintScript,
1638
+ hasTypecheckScript: deploy.hasTypecheckScript,
1639
+ });
1640
+ })(),
1612
1641
  ]);
1613
1642
  if (tools.has('cursor')) {
1614
1643
  templates.push(['.cursor/mcp.json', mcpJson(root)]);
@@ -290,6 +290,15 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
290
290
  governedPercent: cov.governed.percent,
291
291
  planMet: activeCount === 0 && cov.governed.percent >= 50,
292
292
  mature: cov.governed.totalFiles >= 150,
293
+ totalFiles: cov.governed.totalFiles,
294
+ emptyLayers: cov.emptyLayers,
295
+ coreOptionalWithFiles: adoption.coreOptional?.length ?? 0,
296
+ presentationShare: (() => {
297
+ const total = cov.governed.totalFiles || 0;
298
+ if (total <= 0) return null;
299
+ const p = cov.layers.find((r) => r.name === 'PresentationAdapters');
300
+ return p ? p.files / total : null;
301
+ })(),
293
302
  }),
294
303
  governed: cov.governed,
295
304
  emptyLayers: cov.emptyLayers,
@@ -345,11 +354,18 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
345
354
  console.log(color.bold(`Ark doctor — ${path.basename(path.resolve(root)) || '.'}`));
346
355
 
347
356
  const emptyScope = cov.governed.totalFiles === 0;
357
+ const totalFiles = cov.governed.totalFiles || 0;
358
+ const presentationRow = cov.layers.find((r) => r.name === 'PresentationAdapters');
348
359
  const mode = resolveOperatingMode({
349
360
  governedPercent: emptyScope ? 0 : cov.governed.percent,
350
361
  planMet:
351
362
  activeCount === 0 && !emptyScope && cov.governed.percent >= 50,
352
363
  mature: cov.governed.totalFiles >= 150,
364
+ totalFiles: cov.governed.totalFiles,
365
+ emptyLayers: cov.emptyLayers,
366
+ coreOptionalWithFiles: adoption.coreOptional?.length ?? 0,
367
+ presentationShare:
368
+ totalFiles > 0 && presentationRow ? presentationRow.files / totalFiles : null,
353
369
  });
354
370
  console.log('');
355
371
  console.log(color.bold('Operating mode'));
@@ -153,6 +153,17 @@ export function computeReportFitness({ coverage, violations, ok, enforcement, co
153
153
  const governedPercent = coverage?.governed?.percent ?? null;
154
154
  const totalFiles = coverage?.governed?.totalFiles ?? 0;
155
155
  const classifiedFiles = coverage?.governed?.classifiedFiles ?? 0;
156
+ const emptyLayers = (coverage?.layers ?? [])
157
+ .filter((r) => (r.files ?? 0) === 0)
158
+ .map((r) => r.name);
159
+ const presentationRow = (coverage?.layers ?? []).find(
160
+ (r) => r.name === 'PresentationAdapters'
161
+ );
162
+ const coreOptionalWithFiles = (config?.layers ?? []).filter((layer) => {
163
+ if (layer.optional !== true) return false;
164
+ const row = (coverage?.layers ?? []).find((r) => r.name === layer.name);
165
+ return (row?.files ?? 0) > 0;
166
+ }).length;
156
167
  const mode = resolveOperatingMode({
157
168
  governedPercent: totalFiles === 0 ? 0 : governedPercent,
158
169
  planMet:
@@ -162,6 +173,10 @@ export function computeReportFitness({ coverage, violations, ok, enforcement, co
162
173
  (governedPercent == null || governedPercent >= 50),
163
174
  mature: totalFiles >= 150,
164
175
  totalFiles,
176
+ emptyLayers,
177
+ coreOptionalWithFiles,
178
+ presentationShare:
179
+ totalFiles > 0 && presentationRow ? presentationRow.files / totalFiles : null,
165
180
  });
166
181
  const modeLabel = { suggest: 'SUGGEST', adapt: 'ADAPT', enforce: 'ENFORCE' }[mode] || String(mode).toUpperCase();
167
182
  const modeBlurb = {
@@ -267,22 +267,29 @@ export const ARCHITECTURE_PRESETS = {
267
267
  name: 'DomainModel',
268
268
  description: 'Shared types and pure view-models (optional on UI-first trees).',
269
269
  // Avoid bare **/types.ts — see monorepo DomainModel note (false Domain on core/**/types.ts).
270
- patterns: ['**/domain/**', '**/cinematic/types.ts'],
270
+ patterns: [
271
+ '**/domain/**',
272
+ '**/cinematic/types.ts',
273
+ // Common pure types bag (not **/types.ts — that traps core/**/types.ts).
274
+ 'src/lib/types.ts',
275
+ ],
271
276
  exclude: FRAMEWORK_INTERNAL_EXCLUDE,
272
277
  forbiddenGlobals: DEFAULT_DOMAIN_FORBIDDEN_GLOBALS,
273
278
  optional: true,
274
279
  },
275
280
  {
276
- name: 'PresentationAdapters',
277
- description: 'UI, routes, hooks, components, compositions.',
281
+ name: 'ApplicationOrchestration',
282
+ description: 'Server actions, features, and non-UI lib orchestration (when present).',
278
283
  patterns: [
279
- '**/src/**',
280
- '**/components/**',
281
- '**/hooks/**',
282
- '**/lib/**',
283
- '**/routes/**',
284
- '**/app/**',
285
- '**/pages/**',
284
+ 'src/features/**',
285
+ 'src/server/**',
286
+ 'src/services/**',
287
+ 'src/use-cases/**',
288
+ 'src/actions/**',
289
+ 'src/lib/actions/**',
290
+ 'src/lib/services/**',
291
+ '**/lib/actions/**',
292
+ '**/lib/services/**',
286
293
  ],
287
294
  exclude: FRAMEWORK_INTERNAL_EXCLUDE,
288
295
  optional: true,
@@ -290,7 +297,61 @@ export const ARCHITECTURE_PRESETS = {
290
297
  {
291
298
  name: 'PersistenceAdapters',
292
299
  description: 'Client data access and external API adapters (when present).',
293
- patterns: ['**/infrastructure/**', '**/adapters/**', '**/repositories/**'],
300
+ // Prefer specific data-client bags over a presentation catch-all on **/lib/**
301
+ patterns: [
302
+ '**/infrastructure/**',
303
+ '**/adapters/**',
304
+ '**/repositories/**',
305
+ '**/persistence/**',
306
+ 'src/db/**',
307
+ 'src/data/**',
308
+ 'src/lib/db/**',
309
+ 'src/lib/prisma/**',
310
+ 'src/lib/supabase/**',
311
+ 'src/lib/airtable/**',
312
+ 'src/lib/firebase/**',
313
+ 'src/lib/firestore/**',
314
+ 'src/lib/mongodb/**',
315
+ 'src/lib/mongoose/**',
316
+ 'src/lib/drizzle/**',
317
+ 'src/lib/kysely/**',
318
+ '**/lib/supabase/**',
319
+ '**/lib/airtable/**',
320
+ '**/lib/prisma/**',
321
+ '**/lib/db/**',
322
+ ],
323
+ exclude: FRAMEWORK_INTERNAL_EXCLUDE,
324
+ optional: true,
325
+ },
326
+ {
327
+ name: 'PresentationAdapters',
328
+ description: 'UI, routes, hooks, components (not a whole-src bag).',
329
+ // No **/src/** or bare **/lib/** — those swallowed data clients and forced false ENFORCE.
330
+ patterns: [
331
+ '**/components/**',
332
+ '**/hooks/**',
333
+ '**/routes/**',
334
+ '**/app/**',
335
+ '**/pages/**',
336
+ 'src/app/**',
337
+ 'src/pages/**',
338
+ 'src/components/**',
339
+ 'src/hooks/**',
340
+ 'src/ui/**',
341
+ 'src/layouts/**',
342
+ 'app/**',
343
+ 'pages/**',
344
+ 'components/**',
345
+ // Next middleware edge entry (classic + Next 16 proxy rename)
346
+ 'src/middleware.ts',
347
+ 'src/middleware.js',
348
+ 'middleware.ts',
349
+ 'middleware.js',
350
+ 'src/proxy.ts',
351
+ 'src/proxy.js',
352
+ 'proxy.ts',
353
+ 'proxy.js',
354
+ ],
294
355
  exclude: FRAMEWORK_INTERNAL_EXCLUDE,
295
356
  optional: true,
296
357
  },
@@ -298,7 +359,20 @@ export const ARCHITECTURE_PRESETS = {
298
359
  rules: [
299
360
  { from: 'DomainModel', to: 'PresentationAdapters', allowed: false },
300
361
  { from: 'DomainModel', to: 'PersistenceAdapters', allowed: false },
301
- { from: 'PresentationAdapters', to: 'PersistenceAdapters', allowed: false },
362
+ { from: 'DomainModel', to: 'ApplicationOrchestration', allowed: false },
363
+ { from: 'ApplicationOrchestration', to: 'PresentationAdapters', allowed: false },
364
+ // Next RSC often imports data clients from routes; deny is ideal but day-one
365
+ // ui-surface keeps this as a soft guidance edge (allowed) until ports exist —
366
+ // Persistence → Presentation stays denied when that edge appears.
367
+ {
368
+ from: 'PresentationAdapters',
369
+ to: 'PersistenceAdapters',
370
+ allowed: true,
371
+ message:
372
+ 'UI/routes may reach data clients on day one (RSC); prefer application ports as the product grows.',
373
+ },
374
+ { from: 'PersistenceAdapters', to: 'PresentationAdapters', allowed: false },
375
+ { from: 'PersistenceAdapters', to: 'ApplicationOrchestration', allowed: false },
302
376
  ],
303
377
  },
304
378
  root
package/dist/index.cjs CHANGED
@@ -80,7 +80,7 @@ __export(index_exports, {
80
80
  module.exports = __toCommonJS(index_exports);
81
81
 
82
82
  // src/version.ts
83
- var version = "2.8.1";
83
+ var version = "2.8.2";
84
84
 
85
85
  // src/kernel/intent/IntentRegistry.ts
86
86
  var IntentRegistry = class {