;
},
});
- findApprovedVerticalPageClientMock.mockReturnValue({ load: loadRemotePageMock });
+ findApprovedVerticalPageClientMock.mockReturnValue({
+ load: loadRemotePageMock,
+ });
});
afterEach(() => {
@@ -167,7 +168,10 @@ afterEach(() => {
it.each(['selection_required', 'forbidden', 'not_found', 'unavailable'] as const)(
'does not consult or invoke the private registry for a %s exact-page response',
(state) => {
- useLoaderDataMock.mockReturnValue({ shell, state } satisfies ModuleTargetPageModel);
+ useLoaderDataMock.mockReturnValue({
+ shell,
+ state,
+ } satisfies ModuleTargetPageModel);
render();
expect(findApprovedVerticalPageClientMock).not.toHaveBeenCalled();
expect(loadRemotePageMock).not.toHaveBeenCalled();
@@ -180,9 +184,7 @@ it.live('invokes the exact private page loader only after a resolved authenticat
render();
expect(findApprovedVerticalPageClientMock).toHaveBeenCalledWith(resolvedModel.target);
yield* Effect.promise(() => waitFor(() => expect(loadRemotePageMock).toHaveBeenCalledTimes(1)));
- expect(
- yield* Effect.promise(() => screen.findByText('contacts.core.page-customers:customer-1')),
- ).toBeTruthy();
+ expect(yield* Effect.promise(() => screen.findByText('contacts.core.page-customers:customer-1'))).toBeTruthy();
}),
);
@@ -208,9 +210,7 @@ it.live('maps an unreachable approved remote to its safe local diagnostic', () =
render();
- expect(
- yield* Effect.promise(() => screen.findByText('shell.moduleTarget.unavailable')),
- ).toBeTruthy();
+ expect(yield* Effect.promise(() => screen.findByText('shell.moduleTarget.unavailable'))).toBeTruthy();
}),
);
@@ -221,9 +221,7 @@ it.live('rejects a malformed remote module before React receives it', () =>
render();
- expect(
- yield* Effect.promise(() => screen.findByText('shell.moduleTarget.incompatible')),
- ).toBeTruthy();
+ expect(yield* Effect.promise(() => screen.findByText('shell.moduleTarget.incompatible'))).toBeTruthy();
expect(remotePropsMock).not.toHaveBeenCalled();
}),
);
@@ -233,9 +231,7 @@ it.live('passes an empty route-parameter record to a resolved static page', () =
useLoaderDataMock.mockReturnValue({ ...resolvedModel, routeParams: {} });
render();
yield* Effect.promise(() => waitFor(() => expect(loadRemotePageMock).toHaveBeenCalledTimes(1)));
- expect(
- yield* Effect.promise(() => screen.findByText('contacts.core.page-customers:static')),
- ).toBeTruthy();
+ expect(yield* Effect.promise(() => screen.findByText('contacts.core.page-customers:static'))).toBeTruthy();
}),
);
@@ -253,10 +249,11 @@ it.live.each(exactPageCases)(
render();
expect(findApprovedVerticalPageClientMock).toHaveBeenCalledWith(exactModel.target);
- yield* Effect.promise(() =>
- waitFor(() => expect(loadRemotePageMock).toHaveBeenCalledTimes(1)),
- );
- expect(remotePropsMock).toHaveBeenCalledWith({ routeParams, target: exactModel.target });
+ yield* Effect.promise(() => waitFor(() => expect(loadRemotePageMock).toHaveBeenCalledTimes(1)));
+ expect(remotePropsMock).toHaveBeenCalledWith({
+ routeParams,
+ target: exactModel.target,
+ });
expect(yield* Effect.promise(() => screen.findByText(renderedText))).toBeTruthy();
}),
);
diff --git a/app/apps/shell-super-app/tests/unit/routes/resources/page.test.tsx b/app/apps/shell-super-app/tests/unit/routes/resources/page.test.tsx
index 4161f04a7..7c831cc98 100644
--- a/app/apps/shell-super-app/tests/unit/routes/resources/page.test.tsx
+++ b/app/apps/shell-super-app/tests/unit/routes/resources/page.test.tsx
@@ -1,19 +1,17 @@
-import { browserRuntime } from '../../../../src/runtime/browser-effect-runtime.ts' with {
- rstest: 'importActual',
-};
-import { afterEach, beforeEach, expect, rstest, it } from 'effect-rstest';
import { act, cleanup, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Deferred, Effect, Schema } from 'effect';
+import { afterEach, beforeEach, expect, rstest, it } from 'effect-rstest';
import type { ReactNode } from 'react';
-import {
- ShellResourceResponseSchema,
- ShellTargetForbiddenProblemSchema,
-} from '../../../../shared/api.ts';
+
+import { ShellResourceResponseSchema, ShellTargetForbiddenProblemSchema } from '../../../../shared/api.ts';
import type { MediaAttachmentResponse, ShellResourceResponse } from '../../../../shared/api.ts';
-import { authenticatedShellFixture } from '../authenticated-shell-fixture.ts';
-import ResourcePage from '../../../../src/routes/[lang]/resources/[moduleId]/[resourceType]/[resourceId]/page.tsx';
import type { ResourcePageModel } from '../../../../src/routes/[lang]/resources/[moduleId]/[resourceType]/[resourceId]/page.data.ts';
+import ResourcePage from '../../../../src/routes/[lang]/resources/[moduleId]/[resourceType]/[resourceId]/page.tsx';
+import { browserRuntime } from '../../../../src/runtime/browser-effect-runtime.ts' with {
+ rstest: 'importActual',
+};
+import { authenticatedShellFixture } from '../authenticated-shell-fixture.ts';
type ReadyModel = Extract;
type ClosedState = Exclude;
@@ -23,22 +21,17 @@ interface DashboardPageProps {
readonly title: string;
}
-const {
- attachResourceMediaMock,
- browserRunPromiseMock,
- dashboardRenders,
- shellControlsMock,
- useLoaderDataMock,
-} = rstest.hoisted(() => {
- const renders: DashboardPageProps[] = [];
- return {
- attachResourceMediaMock: rstest.fn(),
- browserRunPromiseMock: rstest.fn(),
- dashboardRenders: renders,
- shellControlsMock: rstest.fn(),
- useLoaderDataMock: rstest.fn(),
- };
-});
+const { attachResourceMediaMock, browserRunPromiseMock, dashboardRenders, shellControlsMock, useLoaderDataMock } =
+ rstest.hoisted(() => {
+ const renders: DashboardPageProps[] = [];
+ return {
+ attachResourceMediaMock: rstest.fn(),
+ browserRunPromiseMock: rstest.fn(),
+ dashboardRenders: renders,
+ shellControlsMock: rstest.fn(),
+ useLoaderDataMock: rstest.fn(),
+ };
+ });
const translations = new Map(
Object.entries({
@@ -88,10 +81,7 @@ rstest.mock('../../../../src/routes/use-shell-controls.ts', () => ({
}));
rstest.mock('../../../../src/routes/shell-frame.tsx', () => ({
- AuthenticatedDashboardLayout: ({
- children,
- ...props
- }: DashboardPageProps & { readonly children: ReactNode }) => {
+ AuthenticatedDashboardLayout: ({ children, ...props }: DashboardPageProps & { readonly children: ReactNode }) => {
dashboardRenders.push(props);
return (
@@ -187,8 +177,14 @@ afterEach(() => {
});
it.each([
- { blockedText: 'The dashboard is unavailable', shellState: 'unavailable' as const },
- { blockedText: 'Select a legal entity first', shellState: 'anonymous' as const },
+ {
+ blockedText: 'The dashboard is unavailable',
+ shellState: 'unavailable' as const,
+ },
+ {
+ blockedText: 'Select a legal entity first',
+ shellState: 'anonymous' as const,
+ },
])(
'refuses the whole page for a $shellState shell without a dashboard or an attach seam',
({ blockedText, shellState }) => {
@@ -204,7 +200,10 @@ it.each([
},
);
-const closedStates: readonly { readonly closedText: string; readonly state: ClosedState }[] = [
+const closedStates: readonly {
+ readonly closedText: string;
+ readonly state: ClosedState;
+}[] = [
{ closedText: 'You cannot open this resource', state: 'forbidden' },
{ closedText: 'This resource does not exist', state: 'not_found' },
{ closedText: 'Select a legal entity first', state: 'selection_required' },
@@ -258,7 +257,10 @@ const disabledMediaCases = [
{ blockedText: 'This module is read only', reason: 'read_only' as const },
{ blockedText: 'This resource has no media', reason: 'absent' as const },
{ blockedText: 'You cannot attach media here', reason: 'forbidden' as const },
- { blockedText: 'Media attachment is unavailable', reason: 'unavailable' as const },
+ {
+ blockedText: 'Media attachment is unavailable',
+ reason: 'unavailable' as const,
+ },
];
it.live.each(disabledMediaCases)(
@@ -317,9 +319,7 @@ it.live('holds the attach seam disabled for the whole in-flight attachment', ()
renderResourcePage(readyModel());
yield* Effect.promise(() => user.click(attachButton()));
- yield* Effect.promise(() =>
- waitFor(() => expect(screen.getByText('Attaching media…')).toBeTruthy()),
- );
+ yield* Effect.promise(() => waitFor(() => expect(screen.getByText('Attaching media…')).toBeTruthy()));
expect(attachButton().hasAttribute('disabled')).toBe(true);
yield* Effect.promise(() => user.click(attachButton()));
@@ -327,9 +327,7 @@ it.live('holds the attach seam disabled for the whole in-flight attachment', ()
expect(browserRunPromiseMock).toHaveBeenCalledTimes(1);
yield* Deferred.succeed(gate, attachedResponse);
- yield* Effect.promise(() =>
- waitFor(() => expect(screen.getByText('Media attached')).toBeTruthy()),
- );
+ yield* Effect.promise(() => waitFor(() => expect(screen.getByText('Media attached')).toBeTruthy()));
expect(attachButton().hasAttribute('disabled')).toBe(false);
}),
);
@@ -341,9 +339,7 @@ it.live('attaches media for the loaded resource reference and reports success on
renderResourcePage(model);
yield* Effect.promise(() => user.click(attachButton()));
- yield* Effect.promise(() =>
- waitFor(() => expect(screen.getByText('Media attached')).toBeTruthy()),
- );
+ yield* Effect.promise(() => waitFor(() => expect(screen.getByText('Media attached')).toBeTruthy()));
expect(attachResourceMediaMock).toHaveBeenCalledTimes(1);
expect(attachResourceMediaMock).toHaveBeenCalledWith(model.resource.ref);
@@ -360,9 +356,7 @@ it.live('settles a typed attachment failure into its own status without a defect
renderResourcePage(readyModel());
yield* Effect.promise(() => user.click(attachButton()));
- yield* Effect.promise(() =>
- waitFor(() => expect(screen.getByText('Attaching the media failed')).toBeTruthy()),
- );
+ yield* Effect.promise(() => waitFor(() => expect(screen.getByText('Attaching the media failed')).toBeTruthy()));
expect(screen.queryByText('Media attached')).toBeNull();
expect(screen.queryByText('Attaching media…')).toBeNull();
@@ -377,14 +371,10 @@ it.live('retries after a failure and replaces the failure status with success',
renderResourcePage(readyModel());
yield* Effect.promise(() => user.click(attachButton()));
- yield* Effect.promise(() =>
- waitFor(() => expect(screen.getByText('Attaching the media failed')).toBeTruthy()),
- );
+ yield* Effect.promise(() => waitFor(() => expect(screen.getByText('Attaching the media failed')).toBeTruthy()));
yield* Effect.promise(() => user.click(attachButton()));
- yield* Effect.promise(() =>
- waitFor(() => expect(screen.getByText('Media attached')).toBeTruthy()),
- );
+ yield* Effect.promise(() => waitFor(() => expect(screen.getByText('Media attached')).toBeTruthy()));
expect(attachResourceMediaMock).toHaveBeenCalledTimes(2);
expect(browserRunPromiseMock).toHaveBeenCalledTimes(2);
diff --git a/app/apps/shell-super-app/tests/unit/routes/search/page.test.tsx b/app/apps/shell-super-app/tests/unit/routes/search/page.test.tsx
new file mode 100644
index 000000000..737e755a7
--- /dev/null
+++ b/app/apps/shell-super-app/tests/unit/routes/search/page.test.tsx
@@ -0,0 +1,257 @@
+import { cleanup, render, screen } from '@testing-library/react';
+import { Effect, Schema } from 'effect';
+import { afterEach, beforeEach, expect, rstest, test } from 'effect-rstest';
+
+import {
+ AppIdSchema,
+ GroupKeySchema,
+ LegalEntityIdSchema,
+ ModuleIdSchema,
+ PrincipalIdSchema,
+ ResourceIdSchema,
+ TenantIdSchema,
+} from '../../../../shared/api.ts';
+import type { HomePageModel } from '../../../../src/routes/[lang]/page.data.ts';
+import type { SearchPageModel } from '../../../../src/routes/[lang]/search/page.data.ts';
+import SearchPage from '../../../../src/routes/[lang]/search/page.tsx';
+import { browserRuntime } from '../../../../src/runtime/browser-effect-runtime.ts' with {
+ rstest: 'importActual',
+};
+import type { LocalizedLinkCall, LocalizedLinkDoubleProps } from '../../../support/localized-link-double.tsx';
+import { renderLocalizedLinkDouble } from '../../../support/localized-link-double.tsx';
+
+const {
+ browserRunPromiseMock,
+ languageState,
+ localizedLinkCalls,
+ navigateMock,
+ signOutMock,
+ switchLegalEntityMock,
+ switchTenantMock,
+ useLoaderDataMock,
+} = rstest.hoisted(() => {
+ const recordedLinkCalls: LocalizedLinkCall[] = [];
+ return {
+ browserRunPromiseMock: rstest.fn(),
+ languageState: { current: 'en' },
+ localizedLinkCalls: recordedLinkCalls,
+ navigateMock: rstest.fn(async () => {}),
+ signOutMock: rstest.fn(),
+ switchLegalEntityMock: rstest.fn(),
+ switchTenantMock: rstest.fn(),
+ useLoaderDataMock: rstest.fn(),
+ };
+});
+
+const translations = new Map(
+ Object.entries({
+ 'shell.auth.identity.title': 'Authenticated identity',
+ 'shell.auth.logout.action': 'Logout',
+ 'shell.auth.logout.failed': 'Logout failed',
+ 'shell.dashboard.account.label': 'Account menu',
+ 'shell.dashboard.brand': 'OntOS',
+ 'shell.dashboard.header.label': 'Dashboard header',
+ 'shell.dashboard.legalEntity.accessibleLabel': 'Current legal entity',
+ 'shell.dashboard.navigation.home': 'Home',
+ 'shell.dashboard.navigation.label': 'Dashboard navigation',
+ 'shell.dashboard.sidebar.label': 'Dashboard sidebar',
+ 'shell.dashboard.tenant.accessibleLabel': 'Current tenant',
+ 'shell.dashboard.unavailable': 'Dashboard unavailable',
+ 'shell.modules.state.readOnly': 'Read only',
+ 'shell.search.empty': 'No results',
+ 'shell.search.label': 'Search this legal entity',
+ 'shell.search.selection_required': 'Select a legal entity first',
+ 'shell.search.submit': 'Search',
+ 'shell.search.title': 'Search',
+ 'shell.search.unavailable': 'Search unavailable',
+ }),
+);
+
+rstest.mock('@modern-js/plugin-i18n/runtime', () => ({
+ Link: (props: LocalizedLinkDoubleProps) =>
+ renderLocalizedLinkDouble(props, {
+ calls: localizedLinkCalls,
+ language: languageState,
+ }),
+ useLocalizedLocation: () => ({
+ alternates: { cs: '/cs/hledat', en: '/en/search' },
+ }),
+ useModernI18n: () => ({
+ language: languageState.current,
+ t: (key: string) => translations.get(key) ?? key,
+ }),
+}));
+
+rstest.mock('@modern-js/plugin-tanstack/runtime', () => ({
+ useLoaderData: useLoaderDataMock,
+ useNavigate: () => navigateMock,
+}));
+
+rstest.mock('../../../../src/api/auth-client.ts', () => ({
+ signOut: signOutMock,
+ switchLegalEntity: switchLegalEntityMock,
+ switchTenant: switchTenantMock,
+}));
+
+rstest.mock('../../../../src/runtime/browser-effect-runtime.ts', () => ({
+ browserRuntime: { runPromise: browserRunPromiseMock },
+}));
+
+const principalId = Schema.decodeUnknownSync(PrincipalIdSchema)('00000000-0000-4000-8000-000000000001');
+const tenantId = Schema.decodeUnknownSync(TenantIdSchema)('00000000-0000-4000-8000-000000000101');
+const legalEntityId = Schema.decodeUnknownSync(LegalEntityIdSchema)('00000000-0000-4000-8000-000000000201');
+const inventoryAppId = Schema.decodeUnknownSync(AppIdSchema)('inventory-app');
+const navigationGroupKey = Schema.decodeUnknownSync(GroupKeySchema)('shell.navigation.modules');
+const inventoryModuleId = Schema.decodeUnknownSync(ModuleIdSchema)('inventory.stock');
+const plainResourceId = Schema.decodeUnknownSync(ResourceIdSchema)('unit-1');
+const awkwardResourceId = Schema.decodeUnknownSync(ResourceIdSchema)('unit #1/2');
+
+const authenticatedShell = (): HomePageModel => ({
+ contextState: 'authenticated',
+ identity: {
+ displayName: 'Ada Lovelace',
+ email: 'ada@example.test',
+ principalId,
+ tenantId,
+ },
+ legalEntities: {
+ items: [{ legalEntityId, legalName: 'Alpha company' }],
+ state: 'available',
+ },
+ navigation: {
+ items: [
+ {
+ appId: inventoryAppId,
+ enabled: true,
+ groupKey: navigationGroupKey,
+ href: '/modules/inventory.stock',
+ label: 'Inventory',
+ moduleId: inventoryModuleId,
+ order: 10,
+ state: 'read_only',
+ unavailable: false,
+ writable: false,
+ },
+ ],
+ state: 'available',
+ unavailableDeployments: [],
+ },
+ selectedLegalEntityId: legalEntityId,
+ state: 'authenticated',
+ tenants: {
+ items: [{ name: 'Alpha tenant', tenantId }],
+ state: 'available',
+ },
+});
+
+const readyModel = (resourceType: string, resourceId: typeof plainResourceId): SearchPageModel => ({
+ query: 'unit',
+ response: {
+ partial: false,
+ results: [
+ {
+ kind: 'resource',
+ ref: { moduleId: inventoryModuleId, resourceId, resourceType },
+ title: 'Unit 1',
+ },
+ ],
+ },
+ shell: authenticatedShell(),
+ state: 'ready',
+});
+
+const resourceLinkCalls = () => localizedLinkCalls.filter((call) => call.to.startsWith('/resources'));
+
+beforeEach(() => {
+ browserRunPromiseMock.mockImplementation(browserRuntime.runPromise);
+ signOutMock.mockReturnValue(Effect.succeed({ signedOut: true }));
+ switchTenantMock.mockReturnValue(Effect.succeed({ selectedTenantId: tenantId }));
+ switchLegalEntityMock.mockReturnValue(Effect.succeed({ selectedLegalEntityId: legalEntityId }));
+ useLoaderDataMock.mockReturnValue(readyModel('stock-item', plainResourceId));
+});
+
+afterEach(() => {
+ cleanup();
+ languageState.current = 'en';
+ localizedLinkCalls.length = 0;
+ rstest.clearAllMocks();
+});
+
+test('a search result hands the canonical resource route to the framework link', () => {
+ render();
+
+ const [resultCall] = resourceLinkCalls();
+ expect(resourceLinkCalls()).toHaveLength(1);
+ expect(resultCall?.to).toBe('/resources/$moduleId/$resourceType/$resourceId');
+ expect(resultCall?.params).toEqual({
+ moduleId: 'inventory.stock',
+ resourceId: 'unit-1',
+ resourceType: 'stock-item',
+ });
+ expect(resultCall?.href).toBeUndefined();
+ expect(screen.getByRole('link', { name: 'Unit 1' }).getAttribute('href')).toBe(
+ '/en/resources/inventory.stock/stock-item/unit-1',
+ );
+});
+
+test('a search result resolves the Czech resource route from the same canonical target', () => {
+ languageState.current = 'cs';
+ render();
+
+ expect(resourceLinkCalls()[0]?.to).toBe('/resources/$moduleId/$resourceType/$resourceId');
+ expect(screen.getByRole('link', { name: 'Unit 1' }).getAttribute('href')).toBe(
+ '/cs/zdroje/inventory.stock/stock-item/unit-1',
+ );
+});
+
+test('resource path segments stay percent-encoded per segment', () => {
+ useLoaderDataMock.mockReturnValue(readyModel('stock item', awkwardResourceId));
+ render();
+
+ expect(resourceLinkCalls()[0]?.params).toEqual({
+ moduleId: 'inventory.stock',
+ resourceId: 'unit #1/2',
+ resourceType: 'stock item',
+ });
+ expect(screen.getByRole('link', { name: 'Unit 1' }).getAttribute('href')).toBe(
+ '/en/resources/inventory.stock/stock%20item/unit%20%231%2F2',
+ );
+});
+
+test('an empty result set exposes no resource affordance', () => {
+ useLoaderDataMock.mockReturnValue({
+ query: 'unit',
+ response: { partial: false, results: [] },
+ shell: authenticatedShell(),
+ state: 'ready',
+ } satisfies SearchPageModel);
+ render();
+
+ expect(screen.getByText('No results')).toBeTruthy();
+ expect(resourceLinkCalls()).toHaveLength(0);
+});
+
+test('an unavailable search exposes no resource affordance', () => {
+ useLoaderDataMock.mockReturnValue({
+ query: 'unit',
+ shell: authenticatedShell(),
+ state: 'unavailable',
+ } satisfies SearchPageModel);
+ render();
+
+ expect(screen.getByText('Search unavailable')).toBeTruthy();
+ expect(resourceLinkCalls()).toHaveLength(0);
+});
+
+test('a closed shell state exposes no navigable affordance at all', () => {
+ useLoaderDataMock.mockReturnValue({
+ query: 'unit',
+ shell: { state: 'unavailable' },
+ state: 'unavailable',
+ } satisfies SearchPageModel);
+ render();
+
+ expect(screen.getByText('Dashboard unavailable')).toBeTruthy();
+ expect(screen.queryAllByRole('link')).toHaveLength(0);
+ expect(localizedLinkCalls).toHaveLength(0);
+});
diff --git a/app/apps/shell-super-app/tests/unit/shell-composition.test.ts b/app/apps/shell-super-app/tests/unit/shell-composition.test.ts
index a95887300..d553e4ecd 100644
--- a/app/apps/shell-super-app/tests/unit/shell-composition.test.ts
+++ b/app/apps/shell-super-app/tests/unit/shell-composition.test.ts
@@ -1,4 +1,3 @@
-import { expect, it } from 'effect-rstest';
import { buildInstalledModuleCatalog, resolveInstalledModuleCatalog } from '@app/core-runtime';
import type {
ContextAccessDecision,
@@ -7,6 +6,8 @@ import type {
TenantModuleState,
} from '@app/core-runtime';
import { Effect, Schema } from 'effect';
+import { expect, it } from 'effect-rstest';
+
import { makeShellComposition } from '../../api/modules/shell-composition.ts';
import { ShellCompositionSchema } from '../../shared/api.ts';
@@ -21,15 +22,7 @@ const deployment = (appId: string, moduleId: string, displayName: string, order:
defaultState: 'inactive',
preservesHistoryWhenInactive: true,
scope: 'tenant',
- supportedStates: [
- 'inactive',
- 'active',
- 'read_only',
- 'suspended',
- 'quarantined',
- 'deprecated',
- 'archived',
- ],
+ supportedStates: ['inactive', 'active', 'read_only', 'suspended', 'quarantined', 'deprecated', 'archived'],
},
module: {
description: `${displayName} capability.`,
@@ -59,7 +52,10 @@ const deployment = (appId: string, moduleId: string, displayName: string, order:
contributionKey: `${moduleId}.navigation.home`,
entrypoint: {
access: 'read',
- authorization: { kind: 'context_permission', permission: 'module_access' },
+ authorization: {
+ kind: 'context_permission',
+ permission: 'module_access',
+ },
entrypointKey: `${moduleId}.page.home`,
moduleKey: moduleId,
role: 'page',
@@ -76,7 +72,10 @@ const deployment = (appId: string, moduleId: string, displayName: string, order:
contributionKey: `${moduleId}.page.home`,
entrypoint: {
access: 'read',
- authorization: { kind: 'context_permission', permission: 'module_access' },
+ authorization: {
+ kind: 'context_permission',
+ permission: 'module_access',
+ },
entrypointKey: `${moduleId}.page.home`,
moduleKey: moduleId,
role: 'page',
@@ -127,12 +126,10 @@ const catalogWithNumberLikeOrder = (): InstalledModuleCatalog => {
...contract.manifest.publicSurface,
shellContributions: {
...contract.manifest.publicSurface.shellContributions,
- navigation: contract.manifest.publicSurface.shellContributions.navigation.map(
- (contribution) => ({
- ...contribution,
- order: numberLikeOrder(contribution.order),
- }),
- ),
+ navigation: contract.manifest.publicSurface.shellContributions.navigation.map((contribution) => ({
+ ...contribution,
+ order: numberLikeOrder(contribution.order),
+ })),
},
},
},
@@ -152,7 +149,10 @@ const catalogWithSecondPropertyPage = (): InstalledModuleCatalog => {
contributionKey: 'property.registry.page.customers',
entrypoint: {
access: 'read',
- authorization: { kind: 'context_permission', permission: 'module_access' },
+ authorization: {
+ kind: 'context_permission',
+ permission: 'module_access',
+ },
entrypointKey: 'property.registry.page.customers',
moduleKey: 'property.registry',
role: 'page',
@@ -200,8 +200,7 @@ it.effect('composes one deterministic state and permission batch with lifecycle
return Effect.succeed(
moduleIds.map((moduleKey) => ({
moduleKey,
- state:
- moduleKey === 'documents.center' ? ('read_only' as const) : ('deprecated' as const),
+ state: moduleKey === 'documents.center' ? ('read_only' as const) : ('deprecated' as const),
})),
);
},
@@ -238,7 +237,10 @@ it.effect('composes one deterministic state and permission batch with lifecycle
state: 'available',
unavailableDeployments: [],
});
- expect({ permissionBatches, stateBatches }).toEqual({ permissionBatches: 1, stateBatches: 1 });
+ expect({ permissionBatches, stateBatches }).toEqual({
+ permissionBatches: 1,
+ stateBatches: 1,
+ });
}),
);
@@ -271,7 +273,11 @@ it.effect('keeps healthy navigation and exposes failed installed deployments sep
}
expect(result.navigation.map(({ moduleId }) => moduleId)).toEqual(['documents.center']);
expect(result.unavailableDeployments).toEqual([
- { appId: 'property-registry', reason: 'timeout', status: 'unavailable' },
+ {
+ appId: 'property-registry',
+ reason: 'timeout',
+ status: 'unavailable',
+ },
]);
expect(() => Schema.decodeUnknownSync(ShellCompositionSchema)(result)).not.toThrow();
}),
@@ -312,7 +318,11 @@ it.effect.each(['inactive', 'suspended', 'quarantined', 'archived'] as const)(
Effect.succeed(moduleIds.map((moduleKey) => ({ moduleKey, state }))),
},
}).compose(context);
- expect(result).toEqual({ navigation: [], state: 'available', unavailableDeployments: [] });
+ expect(result).toEqual({
+ navigation: [],
+ state: 'available',
+ unavailableDeployments: [],
+ });
}),
);
@@ -346,59 +356,57 @@ it.effect('omits definite denial while preserving unavailable authorization as d
}),
);
-it.effect(
- 'resolves direct targets independently with exhaustive safe outcomes and historical reads',
- () =>
- Effect.gen(function* resolvesDirectTargetsIndependentlyWithExhaustive() {
- let state: TenantModuleState = 'active';
- let decision: ContextAccessDecision = 'allowed';
- const mutableAccess = contextAccess({});
- const composition = makeShellComposition({
- catalog: Effect.succeed(catalog()),
- contextAccess: {
- ...mutableAccess,
- modules: ({ moduleIds }) => Effect.succeed(moduleIds.map((key) => ({ decision, key }))),
- },
- moduleStates: {
- getTenantModuleStates: (_tenantId, moduleIds) =>
- Effect.succeed(moduleIds.map((moduleKey) => ({ moduleKey, state }))),
- },
- });
- const resolved = yield* composition.resolveModuleTarget(context, {
- moduleId: 'property.registry',
- });
- expect(resolved.outcome).toBe('resolved');
- decision = 'denied';
- const forbidden = yield* composition.resolveModuleTarget(context, {
- moduleId: 'property.registry',
- });
- expect(forbidden.outcome).toBe('forbidden');
- decision = 'unavailable';
- const unavailable = yield* composition.resolveModuleTarget(context, {
- moduleId: 'property.registry',
- });
- expect(unavailable.outcome).toBe('unavailable');
- decision = 'allowed';
- state = 'archived';
- const archived = yield* composition.resolveModuleTarget(context, {
- moduleId: 'property.registry',
- });
- expect(archived.outcome).toBe('not_found');
- const historical = yield* composition.resolveModuleTarget(context, {
- access: 'historical_read',
- moduleId: 'property.registry',
- });
- expect(historical.outcome).toBe('resolved');
- const selectionRequired = yield* composition.resolveModuleTarget(
- { principalId, tenantId },
- { moduleId: 'property.registry' },
- );
- expect(selectionRequired.outcome).toBe('selection_required');
- const missing = yield* composition.resolveModuleTarget(context, {
- moduleId: 'missing.module',
- });
- expect(missing.outcome).toBe('not_found');
- }),
+it.effect('resolves direct targets independently with exhaustive safe outcomes and historical reads', () =>
+ Effect.gen(function* resolvesDirectTargetsIndependentlyWithExhaustive() {
+ let state: TenantModuleState = 'active';
+ let decision: ContextAccessDecision = 'allowed';
+ const mutableAccess = contextAccess({});
+ const composition = makeShellComposition({
+ catalog: Effect.succeed(catalog()),
+ contextAccess: {
+ ...mutableAccess,
+ modules: ({ moduleIds }) => Effect.succeed(moduleIds.map((key) => ({ decision, key }))),
+ },
+ moduleStates: {
+ getTenantModuleStates: (_tenantId, moduleIds) =>
+ Effect.succeed(moduleIds.map((moduleKey) => ({ moduleKey, state }))),
+ },
+ });
+ const resolved = yield* composition.resolveModuleTarget(context, {
+ moduleId: 'property.registry',
+ });
+ expect(resolved.outcome).toBe('resolved');
+ decision = 'denied';
+ const forbidden = yield* composition.resolveModuleTarget(context, {
+ moduleId: 'property.registry',
+ });
+ expect(forbidden.outcome).toBe('forbidden');
+ decision = 'unavailable';
+ const unavailable = yield* composition.resolveModuleTarget(context, {
+ moduleId: 'property.registry',
+ });
+ expect(unavailable.outcome).toBe('unavailable');
+ decision = 'allowed';
+ state = 'archived';
+ const archived = yield* composition.resolveModuleTarget(context, {
+ moduleId: 'property.registry',
+ });
+ expect(archived.outcome).toBe('not_found');
+ const historical = yield* composition.resolveModuleTarget(context, {
+ access: 'historical_read',
+ moduleId: 'property.registry',
+ });
+ expect(historical.outcome).toBe('resolved');
+ const selectionRequired = yield* composition.resolveModuleTarget(
+ { principalId, tenantId },
+ { moduleId: 'property.registry' },
+ );
+ expect(selectionRequired.outcome).toBe('selection_required');
+ const missing = yield* composition.resolveModuleTarget(context, {
+ moduleId: 'missing.module',
+ });
+ expect(missing.outcome).toBe('not_found');
+ }),
);
it.effect.each(['active', 'read_only', 'deprecated'] as const)(
diff --git a/app/apps/shell-super-app/tests/unit/shell-governed-read-schemas.test.ts b/app/apps/shell-super-app/tests/unit/shell-governed-read-schemas.test.ts
index dd8d5165c..3abd282d8 100644
--- a/app/apps/shell-super-app/tests/unit/shell-governed-read-schemas.test.ts
+++ b/app/apps/shell-super-app/tests/unit/shell-governed-read-schemas.test.ts
@@ -1,5 +1,6 @@
-import { describe, expect, test } from 'effect-rstest';
import { Schema } from 'effect';
+import { describe, expect, test } from 'effect-rstest';
+
import {
GovernedResolvedModuleTargetSchema,
GovernedResolveModuleTargetPayloadSchema,
@@ -24,9 +25,7 @@ describe('Shell governed module-target schemas', () => {
expect(decodedInput).toEqual(input);
expect(decodedResult).toEqual(result);
- expect(Schema.encodeSync(GovernedResolveModuleTargetPayloadSchema)(decodedInput)).toEqual(
- input,
- );
+ expect(Schema.encodeSync(GovernedResolveModuleTargetPayloadSchema)(decodedInput)).toEqual(input);
expect(Schema.encodeSync(GovernedResolvedModuleTargetSchema)(decodedResult)).toEqual(result);
});
});
diff --git a/app/apps/shell-super-app/tests/unit/shell-resources.test.ts b/app/apps/shell-super-app/tests/unit/shell-resources.test.ts
index 5735eca6d..ef4287506 100644
--- a/app/apps/shell-super-app/tests/unit/shell-resources.test.ts
+++ b/app/apps/shell-super-app/tests/unit/shell-resources.test.ts
@@ -1,4 +1,3 @@
-import { expect, it } from 'effect-rstest';
import { buildInstalledModuleCatalog } from '@app/core-runtime';
import type {
ContextAccessDecision,
@@ -7,6 +6,8 @@ import type {
TenantModuleState,
} from '@app/core-runtime';
import { DateTime, Effect, Schema } from 'effect';
+import { expect, it } from 'effect-rstest';
+
import {
attachShellMedia,
makeShellResourceDetail,
@@ -61,15 +62,7 @@ const catalog = (): InstalledModuleCatalog =>
defaultState: 'inactive',
preservesHistoryWhenInactive: true,
scope: 'tenant',
- supportedStates: [
- 'inactive',
- 'active',
- 'read_only',
- 'suspended',
- 'quarantined',
- 'deprecated',
- 'archived',
- ],
+ supportedStates: ['inactive', 'active', 'read_only', 'suspended', 'quarantined', 'deprecated', 'archived'],
},
module: {
description: 'Property capability.',
@@ -100,7 +93,12 @@ const catalog = (): InstalledModuleCatalog =>
schemaVersion: '1',
},
],
- api: [{ key: 'property.registry.resource-api', operationKeys: ['detail'] }],
+ api: [
+ {
+ key: 'property.registry.resource-api',
+ operationKeys: ['detail'],
+ },
+ ],
components: [],
events: [],
reports: [],
@@ -180,8 +178,7 @@ const access = (
resourceWriteDecision: ContextAccessDecision = resourceDecision,
): ContextAccessService => ({
legalEntities: () => Effect.succeed([]),
- modules: ({ moduleIds }) =>
- Effect.succeed(moduleIds.map((key) => ({ decision: moduleDecision, key }))),
+ modules: ({ moduleIds }) => Effect.succeed(moduleIds.map((key) => ({ decision: moduleDecision, key }))),
resources: ({ permission = 'read', resources }) =>
Effect.succeed(
resources.map(({ moduleId: owner, resourceId, resourceType: type }) => ({
@@ -189,8 +186,7 @@ const access = (
key: `${owner}:${type}:${resourceId}`,
})),
),
- tenants: ({ tenantIds }) =>
- Effect.succeed(tenantIds.map((key) => ({ decision: moduleDecision, key }))),
+ tenants: ({ tenantIds }) => Effect.succeed(tenantIds.map((key) => ({ decision: moduleDecision, key }))),
});
const dependencies = (
@@ -232,23 +228,21 @@ it.effect('search treats empty input as empty without touching providers', () =>
}),
);
-it.effect(
- 'search keeps an eligible provider with zero candidates as a successful empty result',
- () =>
- Effect.gen(function* searchKeepsAnEligibleProviderWith() {
- const baseline = dependencies();
- const result = yield* makeShellSearch(
- {
- ...baseline,
- contextAccess: {
- ...baseline.contextAccess,
- resources: () => Effect.die('empty results must not authorize an empty resource batch'),
- },
+it.effect('search keeps an eligible provider with zero candidates as a successful empty result', () =>
+ Effect.gen(function* searchKeepsAnEligibleProviderWith() {
+ const baseline = dependencies();
+ const result = yield* makeShellSearch(
+ {
+ ...baseline,
+ contextAccess: {
+ ...baseline.contextAccess,
+ resources: () => Effect.die('empty results must not authorize an empty resource batch'),
},
- { search: () => Effect.succeed([]) },
- ).search(context, 'unit');
- expect(result).toEqual({ partial: false, results: [] });
- }),
+ },
+ { search: () => Effect.succeed([]) },
+ ).search(context, 'unit');
+ expect(result).toEqual({ partial: false, results: [] });
+ }),
);
it.effect('search filters resource denials and reports partial provider failure', () =>
@@ -392,9 +386,7 @@ it.effect(
},
},
};
- const partyCatalog = buildInstalledModuleCatalog([
- { contract: partyContract, expectedAppId: 'party-registry' },
- ]);
+ const partyCatalog = buildInstalledModuleCatalog([{ contract: partyContract, expectedAppId: 'party-registry' }]);
const calls: unknown[] = [];
const baseline = dependencies();
const result = yield* makeShellSearch(
@@ -429,9 +421,16 @@ it.effect(
]);
},
},
- ).search(tenantContext, { includeArchived: true, query: ' party ', role: 'CUSTOMER' });
+ ).search(tenantContext, {
+ includeArchived: true,
+ query: ' party ',
+ role: 'CUSTOMER',
+ });
- expect(calls[0]).toEqual({ permission: 'read_party_identity', tenantIds: [tenantId] });
+ expect(calls[0]).toEqual({
+ permission: 'read_party_identity',
+ tenantIds: [tenantId],
+ });
expect(calls[1]).toMatchObject({ includeArchived: true, query: 'party' });
expect(calls[1]).not.toHaveProperty('role');
expect(result).toEqual({
@@ -463,136 +462,134 @@ it.effect('search fails only when every eligible provider fails', () =>
}),
);
-it.effect(
- 'Counterparty search preserves both identities, selected scope, roles and collision metadata',
- () =>
- Effect.gen(function* CounterpartySearchPreservesBothIdentitiesSelected() {
- const [contract] = catalog().contracts;
- if (contract === undefined) {
- throw new Error('The test catalog must include one installed contract');
- }
- const filteredCatalog = buildInstalledModuleCatalog([
- {
- contract: {
- ...contract,
- manifest: {
- ...contract.manifest,
- publicSurface: {
- ...contract.manifest.publicSurface,
- search: contract.manifest.publicSurface.search.map((descriptor) => ({
- ...descriptor,
- requestFilters: ['includeArchived', 'role'] as const,
- })),
- },
+it.effect('Counterparty search preserves both identities, selected scope, roles and collision metadata', () =>
+ Effect.gen(function* CounterpartySearchPreservesBothIdentitiesSelected() {
+ const [contract] = catalog().contracts;
+ if (contract === undefined) {
+ throw new Error('The test catalog must include one installed contract');
+ }
+ const filteredCatalog = buildInstalledModuleCatalog([
+ {
+ contract: {
+ ...contract,
+ manifest: {
+ ...contract.manifest,
+ publicSurface: {
+ ...contract.manifest.publicSurface,
+ search: contract.manifest.publicSurface.search.map((descriptor) => ({
+ ...descriptor,
+ requestFilters: ['includeArchived', 'role'] as const,
+ })),
},
},
- expectedAppId: 'property-registry',
},
- ]);
- const counterpartyRef = { ...ref, tenantId };
- const canonicalPartyRef = {
- ...ref,
- resourceId: 'party-1',
- resourceType: 'property.registry.party',
- tenantId,
- };
- const collision = {
- counterpartyRefs: [counterpartyRef, { ...counterpartyRef, resourceId: 'unit-2' }],
- kind: 'CANONICAL_PARTY_COUNTERPARTY_COLLISION',
- };
- const value = {
- collision,
- currentRoles: ['CUSTOMER', 'SUPPLIER'],
- legalEntity: { legalEntityId, tenantId },
- party: {
- archived: true,
- matchedViaAlias: true,
- ref: canonicalPartyRef,
- title: 'Canonical Party',
- },
- ref: counterpartyRef,
- };
- const calls: unknown[] = [];
- const search = makeShellSearch(
- { ...dependencies(), catalog: Effect.succeed(filteredCatalog) },
- {
- search: (input) => {
- calls.push(input);
- return Effect.succeed([value]);
- },
+ expectedAppId: 'property-registry',
+ },
+ ]);
+ const counterpartyRef = { ...ref, tenantId };
+ const canonicalPartyRef = {
+ ...ref,
+ resourceId: 'party-1',
+ resourceType: 'property.registry.party',
+ tenantId,
+ };
+ const collision = {
+ counterpartyRefs: [counterpartyRef, { ...counterpartyRef, resourceId: 'unit-2' }],
+ kind: 'CANONICAL_PARTY_COUNTERPARTY_COLLISION',
+ };
+ const value = {
+ collision,
+ currentRoles: ['CUSTOMER', 'SUPPLIER'],
+ legalEntity: { legalEntityId, tenantId },
+ party: {
+ archived: true,
+ matchedViaAlias: true,
+ ref: canonicalPartyRef,
+ title: 'Canonical Party',
+ },
+ ref: counterpartyRef,
+ };
+ const calls: unknown[] = [];
+ const search = makeShellSearch(
+ { ...dependencies(), catalog: Effect.succeed(filteredCatalog) },
+ {
+ search: (input) => {
+ calls.push(input);
+ return Effect.succeed([value]);
},
- );
- const result = yield* search.search(context, {
- includeArchived: true,
- query: 'canonical',
- role: 'CUSTOMER',
- });
- expect(calls[0]).toMatchObject({ includeArchived: true, role: 'CUSTOMER' });
- expect(result).toEqual({
- partial: false,
- results: [{ ...value, kind: 'counterparty', title: 'Canonical Party' }],
- });
- expect(yield* search.search(tenantContext, 'canonical')).toEqual({
- partial: false,
- results: [],
- });
- expect(calls).toHaveLength(1);
- const baseline = dependencies();
- const redacted = yield* makeShellSearch(
- {
- ...baseline,
- catalog: Effect.succeed(filteredCatalog),
- contextAccess: {
- ...baseline.contextAccess,
- resources: ({ resources }) =>
- Effect.succeed(
- resources.map((resource) => ({
- decision:
- resource.resourceId === 'unit-2' ? ('denied' as const) : ('allowed' as const),
- key: `${resource.moduleId}:${resource.resourceType}:${resource.resourceId}`,
- })),
- ),
- },
+ },
+ );
+ const result = yield* search.search(context, {
+ includeArchived: true,
+ query: 'canonical',
+ role: 'CUSTOMER',
+ });
+ expect(calls[0]).toMatchObject({
+ includeArchived: true,
+ role: 'CUSTOMER',
+ });
+ expect(result).toEqual({
+ partial: false,
+ results: [{ ...value, kind: 'counterparty', title: 'Canonical Party' }],
+ });
+ expect(yield* search.search(tenantContext, 'canonical')).toEqual({
+ partial: false,
+ results: [],
+ });
+ expect(calls).toHaveLength(1);
+ const baseline = dependencies();
+ const redacted = yield* makeShellSearch(
+ {
+ ...baseline,
+ catalog: Effect.succeed(filteredCatalog),
+ contextAccess: {
+ ...baseline.contextAccess,
+ resources: ({ resources }) =>
+ Effect.succeed(
+ resources.map((resource) => ({
+ decision: resource.resourceId === 'unit-2' ? ('denied' as const) : ('allowed' as const),
+ key: `${resource.moduleId}:${resource.resourceType}:${resource.resourceId}`,
+ })),
+ ),
},
- { search: () => Effect.succeed([value]) },
- ).search(context, 'canonical');
- expect(JSON.stringify(redacted)).not.toContain('unit-2');
- expect(redacted.results[0]).not.toHaveProperty('collision');
- }),
+ },
+ { search: () => Effect.succeed([value]) },
+ ).search(context, 'canonical');
+ expect(JSON.stringify(redacted)).not.toContain('unit-2');
+ expect(redacted.results[0]).not.toHaveProperty('collision');
+ }),
);
-it.effect(
- 'treats a missing tenant module-state record as hidden rather than authorization uncertainty',
- () =>
- Effect.gen(function* treatsAMissingTenantModuleState() {
- let calls = 0;
- const hiddenDependencies = {
- ...dependencies(),
- moduleStates: { getTenantModuleStates: () => Effect.succeed([]) },
- };
- expect(
- yield* makeShellSearch(hiddenDependencies, {
- search: () => {
- calls += 1;
- return Effect.succeed([{ ref, title: 'Unit 1' }]);
- },
- }).search(context, 'unit'),
- ).toEqual({ partial: false, results: [] });
- const gateway = {
- detail: () => {
+it.effect('treats a missing tenant module-state record as hidden rather than authorization uncertainty', () =>
+ Effect.gen(function* treatsAMissingTenantModuleState() {
+ let calls = 0;
+ const hiddenDependencies = {
+ ...dependencies(),
+ moduleStates: { getTenantModuleStates: () => Effect.succeed([]) },
+ };
+ expect(
+ yield* makeShellSearch(hiddenDependencies, {
+ search: () => {
calls += 1;
- return Effect.succeed({ fields: [], title: 'Unit 1' });
+ return Effect.succeed([{ ref, title: 'Unit 1' }]);
},
- timeline: () => Effect.succeed({ entries: [], projectionLagging: false }),
- };
- expect(
- yield* makeShellResourceDetail(hiddenDependencies, gateway).resolve(context, ref),
- ).toEqual({ outcome: 'not_found' });
- expect(yield* attachShellMedia(context, ref)).toEqual({
- outcome: 'unavailable',
- });
- expect(calls).toBe(0);
- }),
+ }).search(context, 'unit'),
+ ).toEqual({ partial: false, results: [] });
+ const gateway = {
+ detail: () => {
+ calls += 1;
+ return Effect.succeed({ fields: [], title: 'Unit 1' });
+ },
+ timeline: () => Effect.succeed({ entries: [], projectionLagging: false }),
+ };
+ expect(yield* makeShellResourceDetail(hiddenDependencies, gateway).resolve(context, ref)).toEqual({
+ outcome: 'not_found',
+ });
+ expect(yield* attachShellMedia(context, ref)).toEqual({
+ outcome: 'unavailable',
+ });
+ expect(calls).toBe(0);
+ }),
);
it.effect('search fails closed for module or resource authorization uncertainty', () =>
@@ -618,35 +615,27 @@ it.effect('search fails closed for module or resource authorization uncertainty'
}),
);
-it.effect(
- 'resource detail applies catalog, state, module and resource gates before providers',
- () =>
- Effect.gen(function* resourceDetailAppliesCatalogStateModule() {
- let calls = 0;
- const provider = {
- detail: () => {
- calls += 1;
- return Effect.succeed({ fields: [], title: 'Unit 1' });
- },
- timeline: () => Effect.succeed({ entries: [], projectionLagging: false }),
- };
- expect(
- yield* makeShellResourceDetail(dependencies('inactive'), provider).resolve(context, ref),
- ).toEqual({ outcome: 'not_found' });
- expect(
- yield* makeShellResourceDetail(dependencies('active', 'denied'), provider).resolve(
- context,
- ref,
- ),
- ).toEqual({ outcome: 'forbidden' });
- expect(
- yield* makeShellResourceDetail(
- dependencies('active', 'allowed', 'unavailable'),
- provider,
- ).resolve(context, ref),
- ).toEqual({ outcome: 'unavailable' });
- expect(calls).toBe(0);
- }),
+it.effect('resource detail applies catalog, state, module and resource gates before providers', () =>
+ Effect.gen(function* resourceDetailAppliesCatalogStateModule() {
+ let calls = 0;
+ const provider = {
+ detail: () => {
+ calls += 1;
+ return Effect.succeed({ fields: [], title: 'Unit 1' });
+ },
+ timeline: () => Effect.succeed({ entries: [], projectionLagging: false }),
+ };
+ expect(yield* makeShellResourceDetail(dependencies('inactive'), provider).resolve(context, ref)).toEqual({
+ outcome: 'not_found',
+ });
+ expect(yield* makeShellResourceDetail(dependencies('active', 'denied'), provider).resolve(context, ref)).toEqual({
+ outcome: 'forbidden',
+ });
+ expect(
+ yield* makeShellResourceDetail(dependencies('active', 'allowed', 'unavailable'), provider).resolve(context, ref),
+ ).toEqual({ outcome: 'unavailable' });
+ expect(calls).toBe(0);
+ }),
);
it.effect('resource detail sorts an authorized timeline and exposes projection lag', () =>
@@ -656,8 +645,16 @@ it.effect('resource detail sorts an authorized timeline and exposes projection l
timeline: () =>
Effect.succeed({
entries: [
- { occurredAt: '2026-01-01T00:00:00Z', summary: 'Created', timelineEntryId: '1' },
- { occurredAt: '2026-02-01T00:00:00Z', summary: 'Updated', timelineEntryId: '2' },
+ {
+ occurredAt: '2026-01-01T00:00:00Z',
+ summary: 'Created',
+ timelineEntryId: '1',
+ },
+ {
+ occurredAt: '2026-02-01T00:00:00Z',
+ summary: 'Updated',
+ timelineEntryId: '2',
+ },
],
projectionLagging: true,
}),
@@ -683,11 +680,17 @@ it.effect('resource detail sorts an authorized timeline and exposes projection l
if (result.outcome !== 'resolved') {
throw new TypeError('The authorized resource fixture must resolve');
}
- expect(
- yield* Schema.encodeEffect(Schema.Array(ShellTimelineEntrySchema))(result.timeline),
- ).toEqual([
- { occurredAt: '2026-02-01T00:00:00.000Z', summary: 'Updated', timelineEntryId: '2' },
- { occurredAt: '2026-01-01T00:00:00.000Z', summary: 'Created', timelineEntryId: '1' },
+ expect(yield* Schema.encodeEffect(Schema.Array(ShellTimelineEntrySchema))(result.timeline)).toEqual([
+ {
+ occurredAt: '2026-02-01T00:00:00.000Z',
+ summary: 'Updated',
+ timelineEntryId: '2',
+ },
+ {
+ occurredAt: '2026-01-01T00:00:00.000Z',
+ summary: 'Created',
+ timelineEntryId: '1',
+ },
]);
}),
);
@@ -698,18 +701,18 @@ it.effect('media affordance remains unavailable until a generated Action exists'
detail: () => Effect.succeed({ fields: [], title: 'Unit 1' }),
timeline: () => Effect.succeed({ entries: [], projectionLagging: false }),
};
+ expect(yield* makeShellResourceDetail(dependencies('read_only'), provider).resolve(context, ref)).toMatchObject({
+ media: { enabled: false, reason: 'read_only' },
+ });
expect(
- yield* makeShellResourceDetail(dependencies('read_only'), provider).resolve(context, ref),
- ).toMatchObject({ media: { enabled: false, reason: 'read_only' } });
- expect(
- yield* makeShellResourceDetail(
- dependencies('active', 'allowed', 'allowed', 'denied'),
- provider,
- ).resolve(context, ref),
- ).toMatchObject({ media: { enabled: false, reason: 'unavailable' } });
- expect(
- yield* makeShellResourceDetail(dependencies(), provider).resolve(context, ref),
+ yield* makeShellResourceDetail(dependencies('active', 'allowed', 'allowed', 'denied'), provider).resolve(
+ context,
+ ref,
+ ),
).toMatchObject({ media: { enabled: false, reason: 'unavailable' } });
+ expect(yield* makeShellResourceDetail(dependencies(), provider).resolve(context, ref)).toMatchObject({
+ media: { enabled: false, reason: 'unavailable' },
+ });
}),
);
diff --git a/app/apps/shell-super-app/tests/unit/stage-demo-bootstrap.test.ts b/app/apps/shell-super-app/tests/unit/stage-demo-bootstrap.test.ts
index 6c701852e..41363ad5d 100644
--- a/app/apps/shell-super-app/tests/unit/stage-demo-bootstrap.test.ts
+++ b/app/apps/shell-super-app/tests/unit/stage-demo-bootstrap.test.ts
@@ -1,6 +1,8 @@
-import { expect, it } from 'effect-rstest';
import { readFile } from 'node:fs/promises';
+
import { Effect } from 'effect';
+import { expect, it } from 'effect-rstest';
+
import {
STAGE_DEMO_ACCOUNTS,
classifyExactStageDemoRecord,
@@ -72,7 +74,9 @@ it.effect('refuses to provision outside stage or without an operator-supplied pa
STAGE_DEMO_PASSWORD: undefined,
}),
),
- ).toMatchObject({ reason: expect.stringMatching(/STAGE_DEMO_PASSWORD/u) });
+ ).toMatchObject({
+ reason: expect.stringMatching(/STAGE_DEMO_PASSWORD/u),
+ });
expect(
yield* Effect.flip(
parseStageDemoBootstrapConfig({
@@ -80,18 +84,22 @@ it.effect('refuses to provision outside stage or without an operator-supplied pa
STAGE_SIAMPARK_PASSWORD: undefined,
}),
),
- ).toMatchObject({ reason: expect.stringMatching(/STAGE_SIAMPARK_PASSWORD/u) });
+ ).toMatchObject({
+ reason: expect.stringMatching(/STAGE_SIAMPARK_PASSWORD/u),
+ });
}),
);
it.effect('treats an exact record as idempotent and rejects conflicting state', () =>
Effect.gen(function* treatsAnExactRecordAsIdempotent() {
- const expected = { name: 'Techsio', slug: 'techsio', status: 'active' } as const;
+ const expected = {
+ name: 'Techsio',
+ slug: 'techsio',
+ status: 'active',
+ } as const;
expect(yield* classifyExactStageDemoRecord('tenant', undefined, expected)).toBe('create');
expect(yield* classifyExactStageDemoRecord('tenant', expected, expected)).toBe('existing');
expect(
- yield* Effect.flip(
- classifyExactStageDemoRecord('tenant', { ...expected, name: 'Other tenant' }, expected),
- ),
+ yield* Effect.flip(classifyExactStageDemoRecord('tenant', { ...expected, name: 'Other tenant' }, expected)),
).toMatchObject({ reason: expect.stringMatching(/conflicts/u) });
}),
);
@@ -100,36 +108,24 @@ it.live('keeps the demo bootstrap operator-invoked and excludes its password fro
const rootPackage = yield* Effect.promise(() =>
readFile(new URL('../../../../package.json', import.meta.url), 'utf-8'),
);
- const shellPackage = yield* Effect.promise(() =>
- readFile(new URL('../../package.json', import.meta.url), 'utf-8'),
- );
+ const shellPackage = yield* Effect.promise(() => readFile(new URL('../../package.json', import.meta.url), 'utf-8'));
const bootstrapCommand = yield* Effect.promise(() =>
readFile(new URL('../../scripts/bootstrap-stage-demo.sh', import.meta.url), 'utf-8'),
);
- const zerops = yield* Effect.promise(() =>
- readFile(new URL('../../../../zerops.yaml', import.meta.url), 'utf-8'),
- );
+ const zerops = yield* Effect.promise(() => readFile(new URL('../../../../zerops.yaml', import.meta.url), 'utf-8'));
const coreBootstrap = yield* Effect.promise(() =>
readFile(
- new URL(
- '../../../../packages/core-runtime/src/install/stage-context-bootstrap.ts',
- import.meta.url,
- ),
+ new URL('../../../../packages/core-runtime/src/install/stage-context-bootstrap.ts', import.meta.url),
'utf-8',
),
);
const shellBootstrap = yield* Effect.promise(() =>
- readFile(
- new URL('../../api/auth/stage-demo-bootstrap-runtime-infrastructure.ts', import.meta.url),
- 'utf-8',
- ),
+ readFile(new URL('../../api/auth/stage-demo-bootstrap-runtime-infrastructure.ts', import.meta.url), 'utf-8'),
);
expect(JSON.parse(rootPackage).scripts['stage:bootstrap-demo']).toBe(
'pnpm --filter @app/shell-super-app stage:bootstrap-demo',
);
- expect(JSON.parse(shellPackage).scripts['stage:bootstrap-demo']).toBe(
- 'sh scripts/bootstrap-stage-demo.sh',
- );
+ expect(JSON.parse(shellPackage).scripts['stage:bootstrap-demo']).toBe('sh scripts/bootstrap-stage-demo.sh');
expect(bootstrapCommand).toMatch(/stty -echo/u);
expect(bootstrapCommand).toMatch(/STAGE_DEMO_PASSWORD/u);
expect(bootstrapCommand).toMatch(/STAGE_SIAMPARK_PASSWORD/u);
diff --git a/app/apps/shell-super-app/tsconfig.json b/app/apps/shell-super-app/tsconfig.json
index 97d9677e4..e1f8a9596 100644
--- a/app/apps/shell-super-app/tsconfig.json
+++ b/app/apps/shell-super-app/tsconfig.json
@@ -8,27 +8,30 @@
"incremental": true,
"noEmit": false,
"outDir": "../../node_modules/.cache/tsgo/declarations/apps__shell-super-app",
- "skipLibCheck": true,
"tsBuildInfoFile": "../../node_modules/.cache/tsgo/apps__shell-super-app.tsbuildinfo",
- "types": ["node", "bun-types/sqlite"]
+ "types": [
+ "node",
+ "bun-types/sqlite"
+ ]
},
"include": [
- "api",
"src",
"locales/**/*.json",
"package.json",
- "shared"
+ "shared",
+ "server",
+ "api"
],
"references": [
- {
- "path": "../../packages/core-runtime"
- },
{
"path": "../../packages/shared-contracts"
},
{
"path": "../../packages/shared-design-tokens"
},
+ {
+ "path": "../../packages/core-runtime"
+ },
{
"path": "../../verticals/party-registry"
}
diff --git a/app/apps/shell-super-app/tsconfig.mf-types.json b/app/apps/shell-super-app/tsconfig.mf-types.json
index cb2a4206b..d2e95fd52 100644
--- a/app/apps/shell-super-app/tsconfig.mf-types.json
+++ b/app/apps/shell-super-app/tsconfig.mf-types.json
@@ -2,5 +2,8 @@
"extends": "../../tsconfig.base.json",
"include": [
"src/modern-app-env.d.ts"
- ]
+ ],
+ "compilerOptions": {
+ "skipLibCheck": true
+ }
}
diff --git a/app/docs/architecture/ACTIONS.md b/app/docs/architecture/ACTIONS.md
index c4512d72b..901ed6f7c 100644
--- a/app/docs/architecture/ACTIONS.md
+++ b/app/docs/architecture/ACTIONS.md
@@ -2,10 +2,7 @@
This document defines state-changing Action execution. MicroVertical deployment and communication are defined in [MicroVertical Architecture](./MICROVERTICALS.md); public failure contracts are defined in [Effect Error and HTTP Contracts](./ERRORS.md).
-Operation scope, owner-local scoped services, database settings, and governed read evidence are
-defined in [Governed Data Access and Operation Scope](./DATA_ACCESS.md). Every Action explicitly
-declares legal-entity scope independently of entrypoint tenant/system scope. Business handlers never
-receive or import a database executor.
+Operation scope, owner-local scoped services, database settings, and governed read evidence are defined in [Governed Data Access and Operation Scope](./DATA_ACCESS.md). Every Action explicitly declares legal-entity scope independently of entrypoint tenant/system scope. Business handlers never receive or import a database executor.
## Core Rules
@@ -14,89 +11,21 @@ receive or import a database executor.
- Every Action descriptor declares an explicit readonly array of immutable Policy object references. A global Shell/Core Policy may be referenced by any Action; an executable MicroVertical Policy may be referenced only by an Action with the same owning module key. Raw Policy keys, registries, and cross-owner Policy imports are forbidden.
- A Domain Event is a past-tense business fact produced by a successfully committed Action. Domain Events describe what happened; they do not initiate hidden synchronous business state changes.
- Generate Actions, Permissions, Policies, and Outbox Messages with their respective Codesmith generators.
-- Every Action requires an explicit SpiceDB `executor` relationship. A fully consistent
- `action#execute` result of `NO_PERMISSION`, including an Action with no relationships, is a
- definite denial; there is no unconfigured allow path.
+- Every Action requires an explicit SpiceDB `executor` relationship. A fully consistent `action#execute` result of `NO_PERMISSION`, including an Action with no relationships, is a definite denial; there is no unconfigured allow path.
- Every Action descriptor owns a structured `action`/`write` entrypoint. Business Actions are tenant-scoped; Core recovery capabilities are explicitly system-scoped as defined by [Module Entrypoints and Tenant State](./MODULE_ENTRYPOINTS.md).
-- A MicroVertical Action's `owningModuleKey`, key prefix, event producer, and access-policy identity
- use the manifest's dotted OntOS `moduleId`, never the topology deployment `appId`. The real Action
- value is published in the owner-authored manifest and bound to its private handler only in the
- owner-local runtime registration. See [OntOS Module Manifests](./MODULE_MANIFESTS.md).
-
-The only installation exceptions are the first operator-invoked creation of a deployment's initial
-Tenant context and a statically defined, operator-invoked stage bootstrap context set. Each context
-may include its legal entity, human Principal/Auth binding, module state, and matching authorization
-relationships. An Action cannot perform these transitions because their trusted tenant and Principal
-do not exist yet. The stage set must be fixed in source control; callers cannot supply arbitrary
-Tenant or context data. Every bootstrap must be stage/environment gated, idempotent and conflict
-detecting, must stay inside a Core-owned Effect boundary, and must never run from normal application
-startup or an automatic deployment. Shell may create the matching Better Auth credentials, then pass
-only their provider user IDs in the fixed documented order to that Core installation boundary. Core
-owns the context definitions and their provider-user mapping. Every later or non-fixed state change
-uses an Action.
-
-Better Auth credential and session lifecycle operations—sign-in, sign-out or revocation, refresh,
-active tenant selection, API-key provider mechanics, and mechanical impersonation-session
-creation/restoration—are Shell-owned authentication mechanics, not canonical business-state
-mutations. They use the strict typed Auth BFF and must not update Core business tables or emit
-Domain Events. Core Principal Auth Bindings remain the tenant-access authority, and a selected
-tenant ID stored on the Auth session grants no permission. Any later canonical Core or
-MicroVertical state change still requires an Action; authentication mechanics do not provide a
-bypass.
-
-All Core identity changes use generated restricted `core.identity.*` Actions: non-human principal
-creation/status, self or managed API-key binding/status, and requested/started/stopped support
-checkpoints. Provider key IDs may appear only in private Shell orchestration and the binding Action
-payload; raw keys, hashes, cookies, provider user IDs, and session tokens may not. Identity handlers
-record invariant reads as Data Access Events. Support checkpoint handlers additionally attach the
-safe reason, original/effective principal IDs, checkpoint, and optional safe session reference to
-the sensitive `action.executed` evidence; the audit row supplies tenant, timestamp, and Action
-identity.
-
-Action execution and tenant role authorization are independent grants. The default environment
-rule is an explicit `action:#executor@tenant:#member` relationship, so it
-allows every authenticated active Principal in that trusted Tenant and nobody outside it. Direct
-`principal` executor relationships remain supported for narrower grants and rollout compatibility.
-The self-key Actions require their explicit Action executor. Principal creation/status and managed-key mutations
-require both their Action executor and tenant `manage_identity`; support start requires the support
-checkpoint executor and tenant `impersonate`. Provision Action relations with the lossless object ID
-from `toSpiceDbActionObjectId`, never a hand-maintained alternate encoding, and remove them when the
-role, membership, or workload authorization is revoked. The parameterless operator command
-`mise exec -- pnpm authorization:provision-current-actions` discovers the complete current Action
-catalog and provisions only the fixed development or stage Tenant sets. It is idempotent, accepts no
-caller-supplied Tenant or Action identifiers, and must never run during startup, migration, sandbox
-preparation, or automatic deployment. Bootstrap `allowed-principal` tuples are test-only.
-The generated Action descriptor declares the additional tenant permission, and Core evaluates it
-inside the canonical Action authorization boundary after the executor check. A definite tenant-role
-denial produces the same durable permission-denial outcome; an indeterminate check fails retryably.
-Only a decoded support `stopped` checkpoint omits the continuing `impersonate` requirement so secure
-termination remains possible, while its Action executor check is still mandatory.
-
-Provider cleanup is not an Action retry disguised as a new mutation. Shell compares the governed
-Core binding status with Auth's enabled metadata, reports disagreement as `cleanupPending`, and may
-retry only the provider mechanic when Core already holds the requested terminal state. A rotation
-must never return a retryable failure while leaving a newly active secret undisclosed: it first
-attempts to revoke the replacement, and if that rollback cannot be proven it returns the one-time
-secret with cleanup debt. Newly issued provider keys carry a private mechanical
-`binding_pending_v1` marker from the same provider insert that creates the credential. Shell clears
-the marker after it observes the Core binding; a repeated issuance reconciles any retained marker
-against Core and disables an orphan before creating another key. Each marker is scoped by the
-trusted tenant and issuing Principal and remains leased for five minutes, including retries with the
-same caller idempotency key, so concurrent requests cannot reclaim a credential that is still being
-bound. The marker is neither an OntOS permission nor public metadata.
-Stale-marker lookup is tenant/issuer/staleness-filtered and indexed in Auth, processes at most one
-bounded batch per request, and requires a retry before issuance when more cleanup remains.
-
-Support start creates an Auth-owned non-secret recovery record after the provider session is
-initialized and before the started checkpoint commits; support stop therefore always has durable
-recovery state before Better Auth deletes or expires the impersonated session. The record carries
-only safe correlation, OntOS principal/binding IDs, reason,
-tenant, and safe session reference—not a token or cookie. Stopped evidence is idempotent;
-post-restore evidence or recovery cleanup failure still forwards the restored cookie and a repeated
-stop resumes the checkpoint. The recovery context is accepted only for the exact generated Action
-and a decoded `stopped` payload; it still performs the Action's normal SpiceDB permission check.
-Mechanical session termination therefore remains independent of evidence availability, while a
-denied or unavailable checkpoint remains pending instead of fabricating authorization.
+- A MicroVertical Action's `owningModuleKey`, key prefix, event producer, and access-policy identity use the manifest's dotted OntOS `moduleId`, never the topology deployment `appId`. The real Action value is published in the owner-authored manifest and bound to its private handler only in the owner-local runtime registration. See [OntOS Module Manifests](./MODULE_MANIFESTS.md).
+
+The only installation exceptions are the first operator-invoked creation of a deployment's initial Tenant context and a statically defined, operator-invoked stage bootstrap context set. Each context may include its legal entity, human Principal/Auth binding, module state, and matching authorization relationships. An Action cannot perform these transitions because their trusted tenant and Principal do not exist yet. The stage set must be fixed in source control; callers cannot supply arbitrary Tenant or context data. Every bootstrap must be stage/environment gated, idempotent and conflict detecting, must stay inside a Core-owned Effect boundary, and must never run from normal application startup or an automatic deployment. Shell may create the matching Better Auth credentials, then pass only their provider user IDs in the fixed documented order to that Core installation boundary. Core owns the context definitions and their provider-user mapping. Every later or non-fixed state change uses an Action.
+
+Better Auth credential and session lifecycle operations—sign-in, sign-out or revocation, refresh, active tenant selection, API-key provider mechanics, and mechanical impersonation-session creation/restoration—are Shell-owned authentication mechanics, not canonical business-state mutations. They use the strict typed Auth BFF and must not update Core business tables or emit Domain Events. Core Principal Auth Bindings remain the tenant-access authority, and a selected tenant ID stored on the Auth session grants no permission. Any later canonical Core or MicroVertical state change still requires an Action; authentication mechanics do not provide a bypass.
+
+All Core identity changes use generated restricted `core.identity.*` Actions: non-human principal creation/status, self or managed API-key binding/status, and requested/started/stopped support checkpoints. Provider key IDs may appear only in private Shell orchestration and the binding Action payload; raw keys, hashes, cookies, provider user IDs, and session tokens may not. Identity handlers record invariant reads as Data Access Events. Support checkpoint handlers additionally attach the safe reason, original/effective principal IDs, checkpoint, and optional safe session reference to the sensitive `action.executed` evidence; the audit row supplies tenant, timestamp, and Action identity.
+
+Action execution and tenant role authorization are independent grants. The default environment rule is an explicit `action:#executor@tenant:#member` relationship, so it allows every authenticated active Principal in that trusted Tenant and nobody outside it. Direct `principal` executor relationships remain supported for narrower grants and rollout compatibility. The self-key Actions require their explicit Action executor. Principal creation/status and managed-key mutations require both their Action executor and tenant `manage_identity`; support start requires the support checkpoint executor and tenant `impersonate`. Provision Action relations with the lossless object ID from `toSpiceDbActionObjectId`, never a hand-maintained alternate encoding, and remove them when the role, membership, or workload authorization is revoked. The parameterless operator command `mise exec -- pnpm authorization:provision-current-actions` discovers the complete current Action catalog and provisions only the fixed development or stage Tenant sets. It is idempotent, accepts no caller-supplied Tenant or Action identifiers, and must never run during startup, migration, sandbox preparation, or automatic deployment. Bootstrap `allowed-principal` tuples are test-only. The generated Action descriptor declares the additional tenant permission, and Core evaluates it inside the canonical Action authorization boundary after the executor check. A definite tenant-role denial produces the same durable permission-denial outcome; an indeterminate check fails retryably. Only a decoded support `stopped` checkpoint omits the continuing `impersonate` requirement so secure termination remains possible, while its Action executor check is still mandatory.
+
+Provider cleanup is not an Action retry disguised as a new mutation. Shell compares the governed Core binding status with Auth's enabled metadata, reports disagreement as `cleanupPending`, and may retry only the provider mechanic when Core already holds the requested terminal state. A rotation must never return a retryable failure while leaving a newly active secret undisclosed: it first attempts to revoke the replacement, and if that rollback cannot be proven it returns the one-time secret with cleanup debt. Newly issued provider keys carry a private mechanical `binding_pending_v1` marker from the same provider insert that creates the credential. Shell clears the marker after it observes the Core binding; a repeated issuance reconciles any retained marker against Core and disables an orphan before creating another key. Each marker is scoped by the trusted tenant and issuing Principal and remains leased for five minutes, including retries with the same caller idempotency key, so concurrent requests cannot reclaim a credential that is still being bound. The marker is neither an OntOS permission nor public metadata. Stale-marker lookup is tenant/issuer/staleness-filtered and indexed in Auth, processes at most one bounded batch per request, and requires a retry before issuance when more cleanup remains.
+
+Support start creates an Auth-owned non-secret recovery record after the provider session is initialized and before the started checkpoint commits; support stop therefore always has durable recovery state before Better Auth deletes or expires the impersonated session. The record carries only safe correlation, OntOS principal/binding IDs, reason, tenant, and safe session reference—not a token or cookie. Stopped evidence is idempotent; post-restore evidence or recovery cleanup failure still forwards the restored cookie and a repeated stop resumes the checkpoint. The recovery context is accepted only for the exact generated Action and a decoded `stopped` payload; it still performs the Action's normal SpiceDB permission check. Mechanical session termination therefore remains independent of evidence availability, while a denied or unavailable checkpoint remains pending instead of fabricating authorization.
## Invocation Lifecycle
@@ -113,15 +42,7 @@ Process every Action request in this order:
9. Only after permission and all Policies allow, persist the accepted invocation transition from `received` to `running` independently so a definite business rollback leaves it open.
10. Open the Core-owned transaction, lock and recheck the invocation, install and verify the transaction-local operational database scope, then lock the tenant and authoritatively recheck tenant `write` access. Only then may Core construct owner-local services, create the collector, resolve the private handler, and execute it. Competing requests may repeat read-only gates, but their handlers must never run concurrently.
-The first Shell/Core runtime receives an already trusted principal context. Permission and Policy
-evaluation are both enforced before the invocation becomes `running`, the business transaction
-opens, or the handler and collector are created. Core performs one fully consistent `execute` check:
-`HAS_PERMISSION` allows and `NO_PERMISSION` durably rejects the invocation before Policy, service,
-or handler resolution. Timeout, unavailability, authentication/schema failure, conditional
-decisions, and every other indeterminate result return a sanitized retryable check error while
-leaving the invocation open in `received`. The legacy `restriction` relation and `is_restricted`
-permission remain in the compatible schema only for N/N-1 rollout; the candidate runtime does not
-read them. Remove them only in a later contract release after previous runtimes and tuples are gone.
+The first Shell/Core runtime receives an already trusted principal context. Permission and Policy evaluation are both enforced before the invocation becomes `running`, the business transaction opens, or the handler and collector are created. Core performs one fully consistent `execute` check: `HAS_PERMISSION` allows and `NO_PERMISSION` durably rejects the invocation before Policy, service, or handler resolution. Timeout, unavailability, authentication/schema failure, conditional decisions, and every other indeterminate result return a sanitized retryable check error while leaving the invocation open in `received`. The legacy `restriction` relation and `is_restricted` permission remain in the compatible schema only for N/N-1 rollout; the candidate runtime does not read them. Remove them only in a later contract release after previous runtimes and tuples are gone.
## Outcomes
@@ -177,22 +98,10 @@ Cross-MicroVertical consumers use only the message producer's published schema-o
Authentication, permission, policy, and domain rejections remain typed Effect errors throughout the Action lifecycle. At the Backend for Frontend (BFF) endpoint, map them exhaustively to the declared public error schemas and status codes in [Effect Error and HTTP Contracts](./ERRORS.md). Do not let an Action error escape as an exception, an untyped rejected Promise, or an ad hoc HTTP response.
-Authentication assertion failures occur before the Action lifecycle and must not create an Action
-Invocation Log or reach an Action handler. An endpoint maps missing, malformed, tampered, expired,
-or otherwise unusable assertions to its declared `401` Problem Details response with a
-`WWW-Authenticate: Bearer` challenge. Public-JWKS or verification configuration unavailability maps
-to a declared retryable `503`. These endpoint-specific mappings do not replace the separate Core
-permission and Policy mappings and do not justify a generic Action HTTP endpoint.
+Authentication assertion failures occur before the Action lifecycle and must not create an Action Invocation Log or reach an Action handler. An endpoint maps missing, malformed, tampered, expired, or otherwise unusable assertions to its declared `401` Problem Details response with a `WWW-Authenticate: Bearer` challenge. Public-JWKS or verification configuration unavailability maps to a declared retryable `503`. These endpoint-specific mappings do not replace the separate Core permission and Policy mappings and do not justify a generic Action HTTP endpoint.
## Authorization provisioning and compatibility
-Every Action descriptor declares `authorization.kind = action_execution` and one provisioning
-intent. `tenant_membership_default` permits the fixed development/stage provisioner to create the
-tenant-member executor relation. `explicit` requires its intended relation to exist already and is
-never granted blanket tenant membership.
+Every Action descriptor declares `authorization.kind = action_execution` and one provisioning intent. `tenant_membership_default` permits the fixed development/stage provisioner to create the tenant-member executor relation. `explicit` requires its intended relation to exist already and is never granted blanket tenant membership.
-The default runtime remains fail closed. A bounded `report_only` contract may preserve only an
-explicitly baselined, pre-existing missing-policy allow while computing the candidate denial and
-emitting one sanitized `authorization.would_deny` event. Explicit denials, infrastructure errors,
-cross-tenant scope, invalid or expired credentials, disabled modules, and replay are never
-compatible. New Actions cannot enter the baseline implicitly.
+The default runtime remains fail closed. A bounded `report_only` contract may preserve only an explicitly baselined, pre-existing missing-policy allow while computing the candidate denial and emitting one sanitized `authorization.would_deny` event. Explicit denials, infrastructure errors, cross-tenant scope, invalid or expired credentials, disabled modules, and replay are never compatible. New Actions cannot enter the baseline implicitly.
diff --git a/app/docs/architecture/COMMERCE_APPLICATIONS.md b/app/docs/architecture/COMMERCE_APPLICATIONS.md
index 708d6ecd7..70706d581 100644
--- a/app/docs/architecture/COMMERCE_APPLICATIONS.md
+++ b/app/docs/architecture/COMMERCE_APPLICATIONS.md
@@ -77,20 +77,13 @@ It must preserve owner-local validation, Permission, Business Policy, Action, au
## Customer Configuration and implementations
-This section defines accepted target selection semantics. Current V0 supports one implicit
-`standard` implementation per `moduleId`; it does not yet serialize or select `implementationId`.
+This section defines accepted target selection semantics. Current V0 supports one implicit `standard` implementation per `moduleId`; it does not yet serialize or select `implementationId`.
- `moduleId` is the Module Contract Identity and owns public capability semantics.
- `implementationId` identifies one catalogued executable implementation, for example `standard` or `akros`.
- `appId` remains the independently deployable topology identity and exact gateway audience.
-Once that target contract exists, two implementations may share `moduleId` only while public
-semantics and compatibility remain the same. Different semantics require a different `moduleId`.
-Each implementation records immutable build revision/digest, public-contract hash/version, migration
-set, owner, health, and readiness; the catalog rejects missing, duplicate, ambiguous, incompatible,
-or invisible implementation identities. Implement the target only by extending Codesmith, Effect
-Schemas, serialized contracts, topology/allowlist validation, Customer Configuration resolution,
-and tests together. Do not hand-author fields or customer branches as a substitute.
+Once that target contract exists, two implementations may share `moduleId` only while public semantics and compatibility remain the same. Different semantics require a different `moduleId`. Each implementation records immutable build revision/digest, public-contract hash/version, migration set, owner, health, and readiness; the catalog rejects missing, duplicate, ambiguous, incompatible, or invisible implementation identities. Implement the target only by extending Codesmith, Effect Schemas, serialized contracts, topology/allowlist validation, Customer Configuration resolution, and tests together. Do not hand-author fields or customer branches as a substitute.
Prefer shared behavior plus Business Policy. Add an implementation alternative only when an ordinary reusable capability cannot express the required behavior without distorting its contract. Never patch an implementation per customer under the same identity, and never move the exception into Shell/Core.
@@ -114,8 +107,7 @@ Before production activation, prove:
- approved Purchase Proposal Revisions cannot bypass Approval Revalidation or Order Commitment Gate;
- the Medusa facade matches its declared subset and native clients do not depend on it accidentally;
- dependency failures produce typed partial degradation without unrelated outage;
-- once explicit alternatives exist, Customer Configuration resolves one permitted, healthy
- implementation for every selected contract;
+- once explicit alternatives exist, Customer Configuration resolves one permitted, healthy implementation for every selected contract;
- contract/build skew is rejected and canary/rollback identifies exact artifacts;
- Party/Counterparty linking and lifecycle events contain no credentials or cross-Tenant leakage; and
- every Integration Route demonstrates idempotency, retry, reconciliation, observability, and recovery.
diff --git a/app/docs/architecture/DATABASE.md b/app/docs/architecture/DATABASE.md
index 242719bd5..5df83a11e 100644
--- a/app/docs/architecture/DATABASE.md
+++ b/app/docs/architecture/DATABASE.md
@@ -1,62 +1,29 @@
# Database Architecture
-For operation-scope validation, scoped owner services, RLS, runtime/admin roles, and same-tenant
-constraints, also follow [Governed Data Access and Operation Scope](./DATA_ACCESS.md). The current
-effective-role evidence and trusted-context limitations are recorded in
-[Database Trust-Boundary Audit](./DATABASE_TRUST_BOUNDARIES.md). Business
-handlers and BFF adapters never receive or import a database executor; owner repositories are built
-inside a Core-owned scoped transaction.
-
-This document defines authoritative database access and schema-ownership rules
-for the OntOS application. MicroVertical deployment boundaries remain governed
-by [MicroVertical Architecture](./MICROVERTICALS.md), and state changes remain
-governed by [Action Execution](./ACTIONS.md).
+For operation-scope validation, scoped owner services, RLS, runtime/admin roles, and same-tenant constraints, also follow [Governed Data Access and Operation Scope](./DATA_ACCESS.md). The current effective-role evidence and trusted-context limitations are recorded in [Database Trust-Boundary Audit](./DATABASE_TRUST_BOUNDARIES.md). Business handlers and BFF adapters never receive or import a database executor; owner repositories are built inside a Core-owned scoped transaction.
+
+This document defines authoritative database access and schema-ownership rules for the OntOS application. MicroVertical deployment boundaries remain governed by [MicroVertical Architecture](./MICROVERTICALS.md), and state changes remain governed by [Action Execution](./ACTIONS.md).
## Ownership
- `@app/core-runtime` owns only the PostgreSQL schema named exactly `core`.
- The `core` migration history contains only Core infrastructure tables.
- PostgreSQL schema `public` owns no OntOS application tables.
-- Auth and every MicroVertical own separate schemas and migration histories.
- They are not registered in the Core Drizzle configuration or runtime schema.
-- All migration histories store bookkeeping in the shared PostgreSQL schema
- `drizzle`, using a distinct journal table per owner. Independent migration
- histories must never share one journal table because their timestamps would
- suppress each other's migrations.
-- Each owner history uses the Drizzle v1 layout: one folder per migration
- named `_` that holds `migration.sql` and `snapshot.json`.
- There is no `meta/_journal.json`. Never hand-edit a committed `migration.sql`
- or renumber a folder; the bookkeeping table matches rows by folder name and
- content hash. The upgrade record and re-proof sequence live in
- [Drizzle v1 Upgrade](./DRIZZLE_V1_UPGRADE.md).
-- Code outside an owning package must not import a private schema, repository,
- migration, or database client from that package.
+- Auth and every MicroVertical own separate schemas and migration histories. They are not registered in the Core Drizzle configuration or runtime schema.
+- All migration histories store bookkeeping in the shared PostgreSQL schema `drizzle`, using a distinct journal table per owner. Independent migration histories must never share one journal table because their timestamps would suppress each other's migrations.
+- Each owner history uses the Drizzle v1 layout: one folder per migration named `_` that holds `migration.sql` and `snapshot.json`. There is no `meta/_journal.json`. Never hand-edit a committed `migration.sql` or renumber a folder; the bookkeeping table matches rows by folder name and content hash. The upgrade record and re-proof sequence live in [Drizzle v1 Upgrade](./DRIZZLE_V1_UPGRADE.md).
+- Code outside an owning package must not import a private schema, repository, migration, or database client from that package.
## Drizzle Cohort
-OntOS runs the Drizzle v1 line (`drizzle-orm` and `drizzle-kit` at the same
-version) pinned identically in every owner, together with the matching Better
-Auth line and its `@better-auth/drizzle-adapter/relations-v2` entrypoint. The
-package manifests are the source of truth for the exact versions. Bump the
-pair only as one cohort and only through the proofs in
-[Drizzle v1 Upgrade](./DRIZZLE_V1_UPGRADE.md).
+OntOS runs the Drizzle v1 line (`drizzle-orm` and `drizzle-kit` at the same version) pinned identically in every owner, together with the matching Better Auth line and its `@better-auth/drizzle-adapter/relations-v2` entrypoint. The package manifests are the source of truth for the exact versions. Bump the pair only as one cohort and only through the proofs in [Drizzle v1 Upgrade](./DRIZZLE_V1_UPGRADE.md).
Owner conventions on v1:
-- Declare relations with `defineRelations(, (r) => ...)`. Core, Auth, and Party
- construct native executors with `makeWithDefaults({ relations })` from
- `drizzle-orm/effect-postgres`, supplying `PgClient` and `Reactivity`; their types
- are `EffectPgDatabaseRelations>`. Auth's database service also
- creates Better Auth's supported adapter using a private `node-postgres`
- Drizzle handle on the same scoped pool. It exposes the adapter, never that
- Promise-based handle. Application Auth queries use the native executor. Relational
- Queries v1 (`relations(...)`, callback `where`) are unavailable.
-- Declare governed tables with `.table.withRLS(...)` and attach
- `tenantRlsPolicies` or `tenantLegalEntityRlsPolicies` from `@app/core-runtime`.
+- Declare relations with `defineRelations(, (r) => ...)`. Core, Auth, and Party construct native executors with `makeWithDefaults({ relations })` from `drizzle-orm/effect-postgres`, supplying `PgClient` and `Reactivity`; their types are `EffectPgDatabaseRelations>`. Auth's database service also creates Better Auth's supported adapter using a private `node-postgres` Drizzle handle on the same scoped pool. It exposes the adapter, never that Promise-based handle. Application Auth queries use the native executor. Relational Queries v1 (`relations(...)`, callback `where`) are unavailable.
+- Declare governed tables with `.table.withRLS(...)` and attach `tenantRlsPolicies` or `tenantLegalEntityRlsPolicies` from `@app/core-runtime`.
- Use `getColumns` instead of the deprecated `getTableColumns`.
-- After adding a migration or rebasing a branch that adds one, run
- `pnpm db:check`; it validates each owner's snapshot chain and reports
- non-commutative migrations across branches.
+- After adding a migration or rebasing a branch that adds one, run `pnpm db:check`; it validates each owner's snapshot chain and reports non-commutative migrations across branches.
## Typed Drizzle and Effect
@@ -64,32 +31,14 @@ Every application query and mutation must:
1. run inside an Effect service;
2. use the owning package's typed Drizzle table and column references;
-3. use Drizzle query builders for selects, inserts, updates, deletes, and
- transactions; and
+3. use Drizzle query builders for selects, inserts, updates, deletes, and transactions; and
4. preserve expected failures in a declared Effect error channel.
-Application code must not use direct `pg` queries, interpolated SQL strings,
-string-concatenated SQL, untyped result objects, or exported promise-only
-database APIs when Drizzle and Effect can represent the behavior. The
-node-postgres pool is a private implementation detail acquired and released by
-an Effect scope.
-
-Core, Auth, and Party persistence use `drizzle-orm/effect-postgres` with `@effect/sql-pg`.
-Queries are native Effects: yield the query directly and map its typed error at the
-owning repository or service. Transaction callbacks return an Effect; the native SQL
-client owns connection acquisition, commit, rollback, savepoints, and interruption.
-Do not wrap native queries in `Effect.tryPromise`, or start another runtime inside a
-transaction callback. Caller services, references, tracing, and cancellation remain
-in the same Effect execution.
-
-Drizzle query failures carry an Effect `Cause` containing the SQL driver failure.
-`findPostgresFailure` is the sole decoder for sanitized PostgreSQL code and constraint
-metadata. Owners assign domain meaning only to their exact code/constraint pairs.
-Native SQL settlement failures use the defect channel; transaction owners narrow
-only `SqlError` to their declared failure and preserve all other defects. The Action
-runtime distinguishes an uncertain commit acknowledgement from a failed body, retains
-the failed body for rollback diagnostics, and resolves uncertainty from the durable
-invocation marker instead of rerunning the Action.
+Application code must not use direct `pg` queries, interpolated SQL strings, string-concatenated SQL, untyped result objects, or exported promise-only database APIs when Drizzle and Effect can represent the behavior. The node-postgres pool is a private implementation detail acquired and released by an Effect scope.
+
+Core, Auth, and Party persistence use `drizzle-orm/effect-postgres` with `@effect/sql-pg`. Queries are native Effects: yield the query directly and map its typed error at the owning repository or service. Transaction callbacks return an Effect; the native SQL client owns connection acquisition, commit, rollback, savepoints, and interruption. Do not wrap native queries in `Effect.tryPromise`, or start another runtime inside a transaction callback. Caller services, references, tracing, and cancellation remain in the same Effect execution.
+
+Drizzle query failures carry an Effect `Cause` containing the SQL driver failure. `findPostgresFailure` is the sole decoder for sanitized PostgreSQL code and constraint metadata. Owners assign domain meaning only to their exact code/constraint pairs. Native SQL settlement failures use the defect channel; transaction owners narrow only `SqlError` to their declared failure and preserve all other defects. The Action runtime distinguishes an uncertain commit acknowledgement from a failed body, retains the failed body for rollback diagnostics, and resolves uncertainty from the durable invocation marker instead of rerunning the Action.
## Narrow SQL Exceptions
@@ -97,61 +46,30 @@ Drizzle's parameterized `sql` tagged template is allowed only for:
- typed schema checks, index predicates, and defaults;
- migration or bootstrap work; or
-- a documented operation that cannot be represented by a Drizzle query
- builder.
+- a documented operation that cannot be represented by a Drizzle query builder.
-Every application-level exception needs a nearby explanation and a focused
-test. Parameters must remain values in the tagged template; never construct SQL
-by joining or interpolating strings. Raw native Drizzle reads explicitly pass
-`'objects'` as the second argument to `execute`: the default raw mode exposes
-the underlying driver result instead of the object-row array.
+Every application-level exception needs a nearby explanation and a focused test. Parameters must remain values in the tagged template; never construct SQL by joining or interpolating strings. Raw native Drizzle reads explicitly pass `'objects'` as the second argument to `execute`: the default raw mode exposes the underlying driver result instead of the object-row array.
-Generated migration SQL is an output of the typed schema and is not application
-query code. Handwritten migration SQL must not replace an expressible typed
-Drizzle schema definition.
+Generated migration SQL is an output of the typed schema and is not application query code. Handwritten migration SQL must not replace an expressible typed Drizzle schema definition.
## Environment and Lifecycle
-Local Compose and application tooling share the root `DATABASE_URL` contract
-documented in `.env.example`. Package configuration resolves the root `.env`
-by an explicit path, independent of the invocation directory.
+Local Compose and application tooling share the root `DATABASE_URL` contract documented in `.env.example`. Package configuration resolves the root `.env` by an explicit path, independent of the invocation directory.
-Missing or malformed configuration is an expected typed Effect error. There is
-no silent localhost fallback. The application database layer owns a `pg.Pool`,
-binds it through `PgClient.fromPool` to native Drizzle Effect queries, and closes it
-when its Effect scope ends. Pool acquisition and server-side statement deadlines
-remain configured on the pool; a local timeout does not prove that PostgreSQL stopped
-executing a statement.
+Missing or malformed configuration is an expected typed Effect error. There is no silent localhost fallback. The application database layer owns a `pg.Pool`, binds it through `PgClient.fromPool` to native Drizzle Effect queries, and closes it when its Effect scope ends. Pool acquisition and server-side statement deadlines remain configured on the pool; a local timeout does not prove that PostgreSQL stopped executing a statement.
## Core Migration Boundary
-The Core schema inventory is an exact set. Migration generation, application
-registration, tests, and verification all use
-`packages/core-runtime/src/db/schema.ts` as their only schema source.
+The Core schema inventory is an exact set. Migration generation, application registration, tests, and verification all use `packages/core-runtime/src/db/schema.ts` as their only schema source.
-Each owner verifier must reach every owned table through its typed Drizzle reference and
-exact-match only that owner's schema inventory and migration journal. The root application
-verifier separately exact-matches the complete set of application schemas and owner-specific
-Drizzle journals before invoking every owner verifier. PostgreSQL system catalogs and Drizzle's
-migration bookkeeping remain infrastructure metadata rather than a shared business schema.
+Each owner verifier must reach every owned table through its typed Drizzle reference and exact-match only that owner's schema inventory and migration journal. The root application verifier separately exact-matches the complete set of application schemas and owner-specific Drizzle journals before invoking every owner verifier. PostgreSQL system catalogs and Drizzle's migration bookkeeping remain infrastructure metadata rather than a shared business schema.
## Canonical, Projected, and Artifact Data
-PostgreSQL is canonical for operational state. Neo4j, search documents, reporting aggregates, and
-other read models are projections: they may lag, must be rebuildable, and must not become the only
-source for business writes, audit, billing, or authorization. SpiceDB remains the separate
-authorization store and must not be used as the business relationship graph.
+PostgreSQL is canonical for operational state. Neo4j, search documents, reporting aggregates, and other read models are projections: they may lag, must be rebuildable, and must not become the only source for business writes, audit, billing, or authorization. SpiceDB remains the separate authorization store and must not be used as the business relationship graph.
-Binary content belongs in object storage. PostgreSQL owns its metadata, lifecycle, links, evidence
-references, and authorization context. Storage keys are collision-resistant technical identifiers,
-not user filenames or business hierarchy. Preserve an optional original filename as provenance and
-a sanitized display filename for presentation; neither establishes ownership or uniqueness.
+Binary content belongs in object storage. PostgreSQL owns its metadata, lifecycle, links, evidence references, and authorization context. Storage keys are collision-resistant technical identifiers, not user filenames or business hierarchy. Preserve an optional original filename as provenance and a sanitized display filename for presentation; neither establishes ownership or uniqueness.
-After ingest completes, record exact byte size and a SHA-256 hash of the stored bytes. Treat the
-storage key, provider object-version reference, size, and content hash as immutable content
-identity. Presentation metadata and processing state may change independently.
+After ingest completes, record exact byte size and a SHA-256 hash of the stored bytes. Treat the storage key, provider object-version reference, size, and content hash as immutable content identity. Presentation metadata and processing state may change independently.
-Legal or compliance immutability requires provider-enforced WORM/Object Lock. Record the requested
-and verified provider state in the evidence reference. Database constraints and application
-permissions alone provide application-level protection and must not be described as storage-level
-WORM.
+Legal or compliance immutability requires provider-enforced WORM/Object Lock. Record the requested and verified provider state in the evidence reference. Database constraints and application permissions alone provide application-level protection and must not be described as storage-level WORM.
diff --git a/app/docs/architecture/DATABASE_TRUST_BOUNDARIES.md b/app/docs/architecture/DATABASE_TRUST_BOUNDARIES.md
index 9830bf2d0..f35cc4dfe 100644
--- a/app/docs/architecture/DATABASE_TRUST_BOUNDARIES.md
+++ b/app/docs/architecture/DATABASE_TRUST_BOUNDARIES.md
@@ -1,9 +1,6 @@
# Database Trust-Boundary Audit
-This is the reproducible current-state evidence for
-[TechsioCZ/ontos#370](https://github.com/TechsioCZ/ontos/issues/370). It informs the human decision
-in [TechsioCZ/ontos#174](https://github.com/TechsioCZ/ontos/issues/174); it does not change grants,
-credentials, or application behavior.
+This is the reproducible current-state evidence for [TechsioCZ/ontos#370](https://github.com/TechsioCZ/ontos/issues/370). It informs the human decision in [TechsioCZ/ontos#174](https://github.com/TechsioCZ/ontos/issues/174); it does not change grants, credentials, or application behavior.
## Run it
@@ -13,10 +10,7 @@ After migrations and runtime-role bootstrap:
mise exec -- pnpm database-trust:audit
```
-The command uses distinct admin and runtime connections to the same PostgreSQL database and writes
-`.codex/reports/database/database-trust-boundary.json`. The ignored report contains identities,
-effective authority, RLS state, trusted-context probes, and finding codes. It never contains URLs,
-passwords, secrets, tenant IDs, or legal-entity IDs.
+The command uses distinct admin and runtime connections to the same PostgreSQL database and writes `.codex/reports/database/database-trust-boundary.json`. The ignored report contains identities, effective authority, RLS state, trusted-context probes, and finding codes. It never contains URLs, passwords, secrets, tenant IDs, or legal-entity IDs.
Evidence in this document is:
@@ -24,14 +18,9 @@ Evidence in this document is:
- **INFERRED** when it follows from current composition but was not observed in deployment;
- **UNKNOWN** when it is controlled outside this repository.
-The audit covers current and reachable-role authority over databases, schemas, relations,
-sequences, routines, types, parameters, grant options, defaults, RLS, owner-context views, and
-directly executable `SECURITY DEFINER` routines. The executable report is the detailed capability
-inventory; this document records only the architectural conclusions.
+The audit covers current and reachable-role authority over databases, schemas, relations, sequences, routines, types, parameters, grant options, defaults, RLS, owner-context views, and directly executable `SECURITY DEFINER` routines. The executable report is the detailed capability inventory; this document records only the architectural conclusions.
-This is not a general PostgreSQL reachability analyzer. Indirect execution through triggers,
-rewrite rules, aggregate support functions, event triggers, cascades, or partition routing is
-outside this baseline; introducing such a path requires its own narrow security contract and test.
+This is not a general PostgreSQL reachability analyzer. Indirect execution through triggers, rewrite rules, aggregate support functions, event triggers, cascades, or partition routing is outside this baseline; introducing such a path requires its own narrow security contract and test.
## Reproduced local baseline
@@ -47,17 +36,14 @@ outside this baseline; introducing such a path requires its own narrow security
| RLS | Two tables have enabled and forced RLS |
| Trusted settings | Can set both `ontos.tenant_id` and `ontos.legal_entity_id`; local values disappear after rollback |
-Exact counts describe this local database, not production. Re-run the audit against each target
-environment.
+Exact counts describe this local database, not production. Re-run the audit against each target environment.
The baseline has exactly two high-severity findings:
1. `runtime_role_has_cross_schema_dml`: one credential spans Core, Auth, and Contacts.
-2. `runtime_role_can_forge_trusted_context`: that credential can choose both custom GUC values used
- by RLS.
+2. `runtime_role_can_forge_trusted_context`: that credential can choose both custom GUC values used by RLS.
-The first is a blast-radius problem. The second is a trust-root problem; splitting role names alone
-does not solve it.
+The first is a blast-radius problem. The second is a trust-root problem; splitting role names alone does not solve it.
## Process-to-identity map
@@ -71,8 +57,7 @@ does not solve it.
| SpiceDB | Separate datastore login; applications use gRPC plus a pre-shared key. | Bootstrap **VERIFIED**; deployed distribution **UNKNOWN**. |
| Browser/remotes | Boundary checks reject database imports outside server owners. | Static boundary **VERIFIED**; not protection from a compromised server process. |
-External service configuration may supply production credentials, so their absence from
-`zerops.yaml` proves nothing about deployed identity distribution.
+External service configuration may supply production credentials, so their absence from `zerops.yaml` proves nothing about deployed identity distribution.
## Tenant-context trust path
@@ -86,13 +71,9 @@ validated request context
-> commit or rollback clears them
```
-**VERIFIED:** transaction scoping prevents missing context from matching rows and prevents values
-leaking to a later transaction on the same pooled connection.
+**VERIFIED:** transaction scoping prevents missing context from matching rows and prevents values leaking to a later transaction on the same pooled connection.
-**VERIFIED:** the ordinary runtime role can also call `set_config` directly. Arbitrary SQL inside a
-compromised runtime can therefore choose another valid scope. Forced RLS still runs, but evaluates
-attacker-selected context. Typed services and import rules prevent accidents; they do not make a
-shared credential an unforgeable security boundary.
+**VERIFIED:** the ordinary runtime role can also call `set_config` directly. Arbitrary SQL inside a compromised runtime can therefore choose another valid scope. Forced RLS still runs, but evaluates attacker-selected context. Typed services and import rules prevent accidents; they do not make a shared credential an unforgeable security boundary.
## Negative evidence and remaining gaps
@@ -108,8 +89,7 @@ shared credential an unforgeable security boundary.
### A. Process-scoped roles
-Give Shell, Contacts, and workers distinct logins. Each vertical gets its owner schema plus an
-explicit minimal Core grant set; migrations remain administrative.
+Give Shell, Contacts, and workers distinct logins. Each vertical gets its owner schema plus an explicit minimal Core grant set; migrations remain administrative.
- Benefit: measurable blast-radius reduction.
- Cost: per-process provisioning/rotation and a maintained Core grant manifest.
@@ -117,41 +97,32 @@ explicit minimal Core grant set; migrations remain administrative.
### B. Admin-owned trusted-scope entry point
-Validate an unforgeable scope assertion in a narrow admin-owned entry point and store scope where
-the ordinary role cannot write it. RLS reads that trusted state and direct table access is revoked
-for the pilot.
+Validate an unforgeable scope assertion in a narrow admin-owned entry point and store scope where the ordinary role cannot write it. RLS reads that trusted state and direct table access is revoked for the pilot.
- Benefit: enforceable scope authenticity inside PostgreSQL.
-- Cost: privileged-function hardening, pool lifecycle, replay/audience/expiry validation,
- observability, and rollback design.
+- Cost: privileged-function hardening, pool lifecycle, replay/audience/expiry validation, observability, and rollback design.
- Constraint: a `SECURITY DEFINER` function accepting caller-chosen IDs remains forgeable.
### C. Trusted broker or pooler
-A trusted component validates scope, selects the allowed database identity, and establishes
-authoritative context before forwarding work. Applications cannot connect around it.
+A trusted component validates scope, selects the allowed database identity, and establishes authoritative context before forwarding work. Applications cannot connect around it.
- Benefit: centralized identity, rotation, and audit across independent or multi-cloud deployments.
-- Cost: an availability-sensitive hop, network/provider integration, pooling semantics, local
- parity, and a strict no-bypass credential path.
-- Constraint: generic SQL filtering is insufficient; the broker must establish state the ordinary
- role cannot overwrite.
+- Cost: an availability-sensitive hop, network/provider integration, pooling semantics, local parity, and a strict no-bypass credential path.
+- Constraint: generic SQL filtering is insufficient; the broker must establish state the ordinary role cannot overwrite.
-Per-tenant PostgreSQL logins or databases are outside this pilot because of credential and
-connection cardinality.
+Per-tenant PostgreSQL logins or databases are outside this pilot because of credential and connection cardinality.
## Proposed Contacts pilot
1. Create separate `shell_runtime` and `contacts_runtime` roles; keep `ontos_admin` for migrations.
2. Deny each runtime access to the other vertical and grant only an explicit Core subset.
3. Apply option B or C to one Contacts RLS path.
-4. Prove denial of unrelated DML/DDL, `SET ROLE`, `BYPASSRLS`, trusted-state writes, forged scope,
- cross-tenant access, and retained context.
+4. Prove denial of unrelated DML/DDL, `SET ROLE`, `BYPASSRLS`, trusted-state writes, forged scope, cross-tenant access, and retained context.
5. Define provisioning, rotation, observability, rollback, and local bootstrap before expanding.
Before implementation, Petr and Jiří must decide:
1. Is the trust root an admin-owned database entry point (B) or a trusted broker/pooler (C)?
-2. Is a per-process Core grant manifest acceptable, or should Core access first move behind callable
- APIs?
+2. Is a per-process Core grant manifest acceptable, or should Core access first move behind callable APIs?
3. Is rollback in place sufficient, or must one release support a dual path using the shared role?
diff --git a/app/docs/architecture/DATA_ACCESS.md b/app/docs/architecture/DATA_ACCESS.md
index e278bd2d5..860fccaff 100644
--- a/app/docs/architecture/DATA_ACCESS.md
+++ b/app/docs/architecture/DATA_ACCESS.md
@@ -1,63 +1,29 @@
# Governed Data Access and Operation Scope
-This contract governs every public or business read and write. It complements the entrypoint
-`tenant`/`system` scope with an independent legal-entity scope and makes CoreSDK the only owner of
-trusted operation context, transaction creation, and durable access evidence.
+This contract governs every public or business read and write. It complements the entrypoint `tenant`/`system` scope with an independent legal-entity scope and makes CoreSDK the only owner of trusted operation context, transaction creation, and durable access evidence.
## OperationalScope
-`OperationalScope` is immutable, server-only Core runtime state. Core constructs it from an
-authenticated Shell session or a verified audience-scoped gateway assertion. Browser payloads and
-identity headers never establish tenant, principal, auth-binding, or legal-entity identity. The
-scope contains only revalidated tenant, principal, optional auth binding, optional legal entity,
-authentication metadata, correlation ID, and optional trace ID; it is never persisted as generic
-JSON or exposed through browser-safe contracts.
+`OperationalScope` is immutable, server-only Core runtime state. Core constructs it from an authenticated Shell session or a verified audience-scoped gateway assertion. Browser payloads and identity headers never establish tenant, principal, auth-binding, or legal-entity identity. The scope contains only revalidated tenant, principal, optional auth binding, optional legal entity, authentication metadata, correlation ID, and optional trace ID; it is never persisted as generic JSON or exposed through browser-safe contracts.
Every descriptor declares both dimensions explicitly:
- entrypoint scope `tenant` or `system` controls tenant module-state gating;
-- legal-entity scope `required`, `optional`, or `forbidden` controls whether a selected legal entity
- must be present, is validated when present, or must be absent.
-
-Tenant scope does not imply legal-entity scope. Omitted, malformed, stale, inactive, cross-tenant,
-denied, conditional, or indeterminate context fails closed before module state, permission, Policy,
-owner service factory, or private handler resolution. Definite authentication/context failures are
-typed separately from retryable database or authorization unavailability.
-
-Core rechecks the active tenant and principal, verifies an optional auth binding is active and
-belongs to that tenant/principal, verifies an optional legal entity is active and belongs to the
-tenant, and checks the principal's legal-entity access. System/background operations must use an
-explicit system entrypoint and `forbidden` legal-entity scope unless their approved descriptor and
-runtime contract state otherwise; they are not reachable through business handler capabilities.
-
-Mode-specific trusted context is closed and revalidated: sessions require an active user binding
-and `better-auth-session:` reference; API keys require the single active key binding and
-`better-auth-api-key:` reference; support impersonation uses the target as effective principal and
-binding while retaining the active original administrator plus continuing tenant `impersonate`
-permission; system work requires a branded registration and `job:{job}:run:{run}` reference with no
-binding, impersonator, or legal entity. Raw credentials and provider ownership never enter the
-scope, read evidence, gateway claims, or Core tables.
-
-Identity list endpoints are governed Core reads. Shell may join their authorized binding IDs to
-Auth-owned non-secret metadata, including terminal revoked bindings for administration, but strips
-the stable provider key ID before encoding a response. The one-key-one-binding database invariant
-prevents an API key from selecting another tenant or principal.
-
-Identity operations are tenant-level and use `legalEntityScope = optional`; resolving their trusted
-session context does not require an unrelated legal-entity selection. When a legal entity is present
-it remains subject to normal Core revalidation. API-key list responses derive provider cleanup debt
-from Core binding status versus Auth enabled state rather than hiding a partially completed
-transition.
+- legal-entity scope `required`, `optional`, or `forbidden` controls whether a selected legal entity must be present, is validated when present, or must be absent.
+
+Tenant scope does not imply legal-entity scope. Omitted, malformed, stale, inactive, cross-tenant, denied, conditional, or indeterminate context fails closed before module state, permission, Policy, owner service factory, or private handler resolution. Definite authentication/context failures are typed separately from retryable database or authorization unavailability.
+
+Core rechecks the active tenant and principal, verifies an optional auth binding is active and belongs to that tenant/principal, verifies an optional legal entity is active and belongs to the tenant, and checks the principal's legal-entity access. System/background operations must use an explicit system entrypoint and `forbidden` legal-entity scope unless their approved descriptor and runtime contract state otherwise; they are not reachable through business handler capabilities.
+
+Mode-specific trusted context is closed and revalidated: sessions require an active user binding and `better-auth-session:` reference; API keys require the single active key binding and `better-auth-api-key:` reference; support impersonation uses the target as effective principal and binding while retaining the active original administrator plus continuing tenant `impersonate` permission; system work requires a branded registration and `job:{job}:run:{run}` reference with no binding, impersonator, or legal entity. Raw credentials and provider ownership never enter the scope, read evidence, gateway claims, or Core tables.
+
+Identity list endpoints are governed Core reads. Shell may join their authorized binding IDs to Auth-owned non-secret metadata, including terminal revoked bindings for administration, but strips the stable provider key ID before encoding a response. The one-key-one-binding database invariant prevents an API key from selecting another tenant or principal.
+
+Identity operations are tenant-level and use `legalEntityScope = optional`; resolving their trusted session context does not require an unrelated legal-entity selection. When a legal entity is present it remains subject to normal Core revalidation. API-key list responses derive provider cleanup debt from Core binding status versus Auth enabled state rather than hiding a partially completed transition.
## Scoped Owner Services
-Core owns the top-level transaction. It installs transaction-local `ontos.tenant_id` and, when
-present, `ontos.legal_entity_id`, verifies both settings in the same transaction, and only then
-constructs the owner's private service factory. Action and read handlers receive immutable scope,
-operation identity, collector/evidence methods, and typed owner-local services. They never receive
-or import Drizzle, `pg`, a pool, a database executor, transaction creation, commit/rollback, Core
-evidence repositories, or another owner's schema/repository. A service object built over a global
-pool is invalid.
+Core owns the top-level transaction. It installs transaction-local `ontos.tenant_id` and, when present, `ontos.legal_entity_id`, verifies both settings in the same transaction, and only then constructs the owner's private service factory. Action and read handlers receive immutable scope, operation identity, collector/evidence methods, and typed owner-local services. They never receive or import Drizzle, `pg`, a pool, a database executor, transaction creation, commit/rollback, Core evidence repositories, or another owner's schema/repository. A service object built over a global pool is invalid.
## Governed Read Lifecycle
@@ -73,47 +39,20 @@ Core runs reads in this exact order:
8. Decode the declared result and build bounded metadata/hash evidence.
9. Commit durable allowed evidence before releasing the result.
-Definite authorization or Policy denial runs no handler and writes sanitized denied evidence in a
-separate Core-owned transaction. Indeterminate context, permission, Policy, or evidence persistence
-fails closed and retryably. Evidence contains no raw query, result rows, provider diagnostics,
-authorization internals, or foreign identifiers. Metadata-only is the default; hash-only or an
-already-supported redacted mode requires an explicit descriptor policy.
+Definite authorization or Policy denial runs no handler and writes sanitized denied evidence in a separate Core-owned transaction. Indeterminate context, permission, Policy, or evidence persistence fails closed and retryably. Evidence contains no raw query, result rows, provider diagnostics, authorization internals, or foreign identifiers. Metadata-only is the default; hash-only or an already-supported redacted mode requires an explicit descriptor policy.
-The private permission-target resolver derives module/resource targets only from decoded business
-input and immutable scope; transport metadata never chooses an authorization target. Search
-providers also declare a private result-target resolver. Core bulk-checks every returned resource
-reference and releases no result if any reference is denied or indeterminate. Metadata-only reads
-reject hashes, while hash-only reads accept only bounded SHA-256 values and paired fingerprint
-metadata.
+The private permission-target resolver derives module/resource targets only from decoded business input and immutable scope; transport metadata never chooses an authorization target. Search providers also declare a private result-target resolver. Core bulk-checks every returned resource reference and releases no result if any reference is denied or indeterminate. Metadata-only reads reject hashes, while hash-only reads accept only bounded SHA-256 values and paired fingerprint metadata.
-Every Shell-to-MicroVertical provider attempt acquires a fresh assertion for that provider's app
-audience. The provider transport receives only the resulting Authorization value and business
-payload; receiving BFFs verify the Bearer assertion before invoking `ReadRuntime`.
+Every Shell-to-MicroVertical provider attempt acquires a fresh assertion for that provider's app audience. The provider transport receives only the resulting Authorization value and business payload; receiving BFFs verify the Bearer assertion before invoking `ReadRuntime`.
## PostgreSQL Isolation
-`DATABASE_ADMIN_URL` is used only for role/schema/migration work. `DATABASE_URL` is the application
-pool and must authenticate as a non-superuser role without `BYPASSRLS`. The URLs must not be
-identical. Local/test bootstrap creates or updates `ontos_runtime`, grants only schema/table/sequence
-usage needed by the application, and verifies its capabilities.
+`DATABASE_ADMIN_URL` is used only for role/schema/migration work. `DATABASE_URL` is the application pool and must authenticate as a non-superuser role without `BYPASSRLS`. The URLs must not be identical. Local/test bootstrap creates or updates `ontos_runtime`, grants only schema/table/sequence usage needed by the application, and verifies its capabilities.
-Owner tenant tables use enabled and forced RLS. Tenant-only policies compare `tenant_id` with
-`current_setting('ontos.tenant_id', true)` for `USING` and `WITH CHECK`. Legal-entity-owned policies
-also compare `legal_entity_id` with `current_setting('ontos.legal_entity_id', true)`. Missing or
-malformed settings match no rows and permit no writes. Settings use parameterized
-`set_config(..., true)`, are verified before owner services exist, and disappear at transaction end.
+Owner tenant tables use enabled and forced RLS. Tenant-only policies compare `tenant_id` with `current_setting('ontos.tenant_id', true)` for `USING` and `WITH CHECK`. Legal-entity-owned policies also compare `legal_entity_id` with `current_setting('ontos.legal_entity_id', true)`. Missing or malformed settings match no rows and permit no writes. Settings use parameterized `set_config(..., true)`, are verified before owner services exist, and disappear at transaction end.
-Core global catalogs, schedulers, delivery state, and checkpoints deliberately remain Core-private
-instead of becoming a business-handler RLS surface because controlled global scans are required.
-All Core rows carrying a tenant plus a referenced legal entity, principal, auth binding, Action
-invocation, audit/data-access/domain event, evidence, media, outbox, or checkpoint use composite
-same-tenant uniqueness and foreign keys. This database invariant remains effective if application
-validation is bypassed.
+Core global catalogs, schedulers, delivery state, and checkpoints deliberately remain Core-private instead of becoming a business-handler RLS surface because controlled global scans are required. All Core rows carrying a tenant plus a referenced legal entity, principal, auth binding, Action invocation, audit/data-access/domain event, evidence, media, outbox, or checkpoint use composite same-tenant uniqueness and foreign keys. This database invariant remains effective if application validation is bypassed.
## Public Errors
-Transport adapters map declared typed failures only: missing or unusable authentication to `401`,
-definite permission denial to `403`, semantic Policy denial to its declared `409` or `422`, required
-context/authorization/evidence unavailability to retryable `503`, and caught unexpected defects to
-a sanitized declared `500`. No adapter constructs an ad hoc response or exposes database or SpiceDB
-diagnostics.
+Transport adapters map declared typed failures only: missing or unusable authentication to `401`, definite permission denial to `403`, semantic Policy denial to its declared `409` or `422`, required context/authorization/evidence unavailability to retryable `503`, and caught unexpected defects to a sanitized declared `500`. No adapter constructs an ad hoc response or exposes database or SpiceDB diagnostics.
diff --git a/app/docs/architecture/DEPLOYMENT.md b/app/docs/architecture/DEPLOYMENT.md
index 0220af416..d480eb748 100644
--- a/app/docs/architecture/DEPLOYMENT.md
+++ b/app/docs/architecture/DEPLOYMENT.md
@@ -1,67 +1,38 @@
# Deployment Architecture and Release Playbook
-This playbook is the authoritative release guidance for OntOS application delivery. It covers
-deployment configuration, CI/CD, PostgreSQL and SpiceDB changes, runtime packaging, Shell changes,
-and every new or changed MicroVertical.
-
-> [!IMPORTANT]
-> Explicit `implementationId`, dependency-closure selection, public-contract hashes, migration-set
-> identity, and full artifact metadata are accepted target architecture, not fields in the current
-> manifest/catalog schema. Requirements below that name them become mandatory with that contract.
-> Until then, releases use one implicit `standard` implementation per `moduleId` and the current
-> generated `buildMarker`; do not simulate missing fields with ad hoc configuration.
-
-Application Composition validation is implemented; publication and live Shell loading are not.
-Until #374–#377 wire those paths in, remote URL and generated lazy-registry changes still require
-Shell regeneration and redeployment. The composition promotion sequence below is the target flow.
-
-The rules exist because the first Zerops stage rollout was merged after source-level validation and
-then required 43 linear repair commits. Stage had become the first production-shaped integration
-test. Future releases must prove the target artifact and the distributed user journey before
-promotion.
+This playbook is the authoritative release guidance for OntOS application delivery. It covers deployment configuration, CI/CD, PostgreSQL and SpiceDB changes, runtime packaging, Shell changes, and every new or changed MicroVertical.
+
+> [!IMPORTANT] Explicit `implementationId`, dependency-closure selection, public-contract hashes, migration-set identity, and full artifact metadata are accepted target architecture, not fields in the current manifest/catalog schema. Requirements below that name them become mandatory with that contract. Until then, releases use one implicit `standard` implementation per `moduleId` and the current generated `buildMarker`; do not simulate missing fields with ad hoc configuration.
+
+Application Composition validation is implemented; publication and live Shell loading are not. Until #374–#377 wire those paths in, remote URL and generated lazy-registry changes still require Shell regeneration and redeployment. The composition promotion sequence below is the target flow.
+
+The rules exist because the first Zerops stage rollout was merged after source-level validation and then required 43 linear repair commits. Stage had become the first production-shaped integration test. Future releases must prove the target artifact and the distributed user journey before promotion.
## Release invariants
These are non-negotiable:
-1. **Topology is the delivery inventory.** Every deployable `appId`, service, package path, port,
- readiness route, public URL, and migration owner derives from one generated or mechanically
- validated topology contract. Application Composition separately governs the approved runtime
- module graph and exact contract/remote artifacts; neither tenant state nor a reachable service
- may add an artifact.
-2. **Build once, promote unchanged.** A release deploys immutable artifacts identified by source SHA
- and digest. Do not rebuild the same revision separately for stage and production.
-3. **Prove the real artifact.** A successful source build is not a deploy test. CI must build,
- materialize, install, start, and probe the same runtime artifact shape used by the provider.
-4. **Providers precede consumers.** Migrations and compatible authorization schema precede
- MicroVertical services; referenced MicroVertical remotes precede Shell; activation follows all
- deployed smoke tests.
-5. **Installation is not activation.** Deploy a new MicroVertical dark. Tenant module state is the
- authoritative release flag and defaults inactive until canary verification succeeds.
-6. **Every overlap is backward compatible.** Database, authorization, manifest, BFF, Module
- Federation, and Shell/MicroVertical boundaries must work while old and new versions coexist.
-7. **Rollback is prepared before rollout.** Record a previously validated immutable composition and
- artifact for every affected delivery unit. Rollback is an explicit audited promotion, never an
- automatic persistent fallback, and must not depend on reversing a schema migration.
-8. **One failed gate stops promotion.** Preserve the artifact and evidence, reproduce in the parity
- environment, fix the failure class, and rerun the release sequence from its first gate.
-9. **Continuous product delivery is not customer version pinning.** OntOS controls promotion of
- immutable artifacts. Customer Configuration selects permitted modules/implementations and
- activation state, never a separate whole-product release line.
+1. **Topology is the delivery inventory.** Every deployable `appId`, service, package path, port, readiness route, public URL, and migration owner derives from one generated or mechanically validated topology contract. Application Composition separately governs the approved runtime module graph and exact contract/remote artifacts; neither tenant state nor a reachable service may add an artifact.
+2. **Build once, promote unchanged.** A release deploys immutable artifacts identified by source SHA and digest. Do not rebuild the same revision separately for stage and production.
+3. **Prove the real artifact.** A successful source build is not a deploy test. CI must build, materialize, install, start, and probe the same runtime artifact shape used by the provider.
+4. **Providers precede consumers.** Migrations and compatible authorization schema precede MicroVertical services; referenced MicroVertical remotes precede Shell; activation follows all deployed smoke tests.
+5. **Installation is not activation.** Deploy a new MicroVertical dark. Tenant module state is the authoritative release flag and defaults inactive until canary verification succeeds.
+6. **Every overlap is backward compatible.** Database, authorization, manifest, BFF, Module Federation, and Shell/MicroVertical boundaries must work while old and new versions coexist.
+7. **Rollback is prepared before rollout.** Record a previously validated immutable composition and artifact for every affected delivery unit. Rollback is an explicit audited promotion, never an automatic persistent fallback, and must not depend on reversing a schema migration.
+8. **One failed gate stops promotion.** Preserve the artifact and evidence, reproduce in the parity environment, fix the failure class, and rerun the release sequence from its first gate.
+9. **Continuous product delivery is not customer version pinning.** OntOS controls promotion of immutable artifacts. Customer Configuration selects permitted modules/implementations and activation state, never a separate whole-product release line.
## Delivery-unit contract
A new MicroVertical is not deployable until its delivery contract accounts for all of these fields:
-- topology `appId` and dotted Module Contract Identity `moduleId`, kept distinct; add explicit
- `implementationId` when the accepted target contract is implemented;
+- topology `appId` and dotted Module Contract Identity `moduleId`, kept distinct; add explicit `implementationId` when the accepted target contract is implemented;
- package name and workspace-relative owner path;
- provider service/setup identity and environment service-ID key;
- build and runtime Node/pnpm versions;
- declared `PORT`, service-specific port variable, and readiness route;
- immutable artifact build/materialization command;
-- current immutable `buildMarker`, plus build revision/digest, public-contract hash/version, and
- migration-set identity when the target metadata contract is implemented;
+- current immutable `buildMarker`, plus build revision/digest, public-contract hash/version, and migration-set identity when the target metadata contract is implemented;
- owned PostgreSQL schema, Drizzle journal, migration, grant, and verifier commands;
- compatible SpiceDB schema requirements;
- public URL and module-manifest URL;
@@ -70,35 +41,26 @@ A new MicroVertical is not deployable until its delivery contract accounts for a
- change-impact rules;
- failure-log collection, smoke checks, and rollback target.
-Codesmith or another approved generator must update these surfaces atomically. Until the generator
-exists, do not add another copied Contacts block to the workflow, `zerops.yaml`, migration runner, or
-validator. Extend and test the generator first.
+Codesmith or another approved generator must update these surfaces atomically. Until the generator exists, do not add another copied Contacts block to the workflow, `zerops.yaml`, migration runner, or validator. Extend and test the generator first.
-Change planning must fail closed when a changed path under `apps/*`, `packages/*`, or `verticals/*`
-cannot be mapped to known delivery units. An unknown new vertical must never produce a no-op deploy.
+Change planning must fail closed when a changed path under `apps/*`, `packages/*`, or `verticals/*` cannot be mapped to known delivery units. An unknown new vertical must never produce a no-op deploy.
### Change-impact rules
The generated plan must conservatively include:
-- an owner migration whenever the owner's Drizzle schema, migrations, migration config, or verifier
- changes;
+- an owner migration whenever the owner's Drizzle schema, migrations, migration config, or verifier changes;
- every consumer when a shared runtime package or public contract changes;
- SpiceDB whenever its schema, image, datastore bootstrap, transport, or client contract changes;
-- Shell whenever its code/config or contribution ABI changes, including remote URL and generated
- lazy-registry changes until the live composition loader is integrated. After that integration,
- compatible remote updates move through a new composition revision without redeploying Shell;
-- a MicroVertical whenever its owner-local code, manifest, registration, migrations, configuration,
- or runtime dependencies change;
-- all Node delivery units whenever the common lockfile, workspace dependency policy, runtime
- materializer, Node installer, or deployment manifest changes.
+- Shell whenever its code/config or contribution ABI changes, including remote URL and generated lazy-registry changes until the live composition loader is integrated. After that integration, compatible remote updates move through a new composition revision without redeploying Shell;
+- a MicroVertical whenever its owner-local code, manifest, registration, migrations, configuration, or runtime dependencies change;
+- all Node delivery units whenever the common lockfile, workspace dependency policy, runtime materializer, Node installer, or deployment manifest changes.
The deployment plan, not a hand-written `case` statement, is the reviewable output.
## Production-parity artifact gate
-Before merge or promotion, build from a clean checkout with the frozen lockfile in a target-equivalent
-Linux profile:
+Before merge or promotion, build from a clean checkout with the frozen lockfile in a target-equivalent Linux profile:
1. use the exact pinned Node and pnpm versions;
2. remove stale workspace `node_modules` links and host-global virtual-store state;
@@ -111,9 +73,7 @@ Linux profile:
9. probe readiness and the delivery unit's public contract;
10. publish the source SHA, artifact digest, dependency cohort, and gate result.
-The artifact deployed later must match that digest. If the provider cannot accept a prebuilt
-artifact, the provider build itself must emit and verify the digest and use an identical, pinned
-build profile in every environment.
+The artifact deployed later must match that digest. If the provider cannot accept a prebuilt artifact, the provider build itself must emit and verify the digest and use an identical, pinned build profile in every environment.
Commands run by agents, developers, and ordinary CI from `app/` use:
@@ -121,14 +81,11 @@ Commands run by agents, developers, and ordinary CI from `app/` use:
mise exec -- pnpm
```
-Commands embedded in a minimal provider image may use the deployment-pinned Node/pnpm bootstrap
-when mise is deliberately absent. This is a narrow deployment-runtime exception, not permission to
-run arbitrary local pnpm commands outside mise.
+Commands embedded in a minimal provider image may use the deployment-pinned Node/pnpm bootstrap when mise is deliberately absent. This is a narrow deployment-runtime exception, not permission to run arbitrary local pnpm commands outside mise.
## Typed configuration preflight
-Configuration validation happens before the first service changes. It must verify, without printing
-secrets:
+Configuration validation happens before the first service changes. It must verify, without printing secrets:
- all required project and service IDs;
- administrative and runtime PostgreSQL URLs use distinct identities;
@@ -141,9 +98,7 @@ secrets:
- required dependency/patch versions and provider CLI version;
- readiness paths, timeouts, and retry periods with explicit units.
-Do not infer the canonical authentication origin from a reverse-proxied request. Do not use a
-runtime database identity for role, database, schema, or migration work. Do not silently fall back
-to localhost or another environment.
+Do not infer the canonical authentication origin from a reverse-proxied request. Do not use a runtime database identity for role, database, schema, or migration work. Do not silently fall back to localhost or another environment.
## Migration and authorization sequence
@@ -159,20 +114,13 @@ Every owner retains its own schema and Drizzle journal. Run the release phase in
6. run each owner verifier and the root exact schema/journal verifier;
7. prove the previous and candidate application versions can use the expanded schema.
-Never share a migration journal between owners. Never omit a migration because only an owner-local
-path changed. The first v1 `drizzle-kit migrate` against a database migrated before the
-[Drizzle v1 upgrade](./DRIZZLE_V1_UPGRADE.md) adds `name` and `applied_at` columns to that owner's
-bookkeeping table and backfills `name`; it applies no schema migration and needs no manual step
-beyond the administrative identity. Never execute deployment migrations through an assumed workspace pnpm layout after
-artifact relocation; use the verified owner-local runtime binary or an explicit migration artifact.
+Never share a migration journal between owners. Never omit a migration because only an owner-local path changed. The first v1 `drizzle-kit migrate` against a database migrated before the [Drizzle v1 upgrade](./DRIZZLE_V1_UPGRADE.md) adds `name` and `applied_at` columns to that owner's bookkeeping table and backfills `name`; it applies no schema migration and needs no manual step beyond the administrative identity. Never execute deployment migrations through an assumed workspace pnpm layout after artifact relocation; use the verified owner-local runtime binary or an explicit migration artifact.
-Destructive contraction is a later release after all old readers and writers are gone. Ordinary
-rollback leaves additive schema changes in place.
+Destructive contraction is a later release after all old readers and writers are gone. Ordinary rollback leaves additive schema changes in place.
### SpiceDB
-Distinguish Authzed datastore migrations from the OntOS authorization schema. A datastore migration
-does not publish a changed permission model.
+Distinguish Authzed datastore migrations from the OntOS authorization schema. A datastore migration does not publish a changed permission model.
For every authorization-schema change:
@@ -183,46 +131,24 @@ For every authorization-schema change:
5. verify representative existing and candidate permissions;
6. retain a compatible rollback plan for application versions and relationship writers.
-Bootstrap files are only for an empty installation. They are not the ongoing authorization-schema
-deployment mechanism.
+Bootstrap files are only for an empty installation. They are not the ongoing authorization-schema deployment mechanism.
The fail-closed Action authorization rollout uses an explicit expand/provision/verify/deploy gate:
-1. prepare the candidate application/release artifact for the operator command while the previous
- runtime remains active; this is separate from the PostgreSQL migration artifact;
+1. prepare the candidate application/release artifact for the operator command while the previous runtime remains active; this is separate from the PostgreSQL migration artifact;
2. ensure the fixed stage contexts and their Tenant membership relationships already exist;
-3. run `mise exec -- pnpm authorization:provision-current-actions` in the stage-gated artifact to
- publish the compatible schema and membership-set executor grants for the complete current Action
- catalog across the fixed stage Tenants;
-4. verify every Action for the fixed stage Principals and verify representative non-members are
- denied;
+3. run `mise exec -- pnpm authorization:provision-current-actions` in the stage-gated artifact to publish the compatible schema and membership-set executor grants for the complete current Action catalog across the fixed stage Tenants;
+4. verify every Action for the fixed stage Principals and verify representative non-members are denied;
5. only then deploy the runtime that treats missing `action#execute` permission as denial;
6. smoke one provisioned Action and one deliberately unconfigured Action denial.
-The command is operator-invoked, idempotent, accepts no scope arguments, and must not be attached to
-PostgreSQL migrations, SpiceDB startup, application startup, or automatic deployment. A failure or
-catalog mismatch blocks promotion. Rollback restores the previous application artifact while
-leaving the additive schema and relationships in place.
-
-Provisioning is additive, not stale-grant reconciliation. Before narrowing an Action from
-`tenant_membership_default` to `explicit`, the operator must prepare its intended narrow grants,
-remove the obsolete `action:#executor@tenant:#member` relation for each
-affected fixed Tenant, and verify both the intended allowed Principal and a Tenant member who must
-now be denied. Removed Actions and revoked role/workload assignments likewise require an explicit,
-reviewed removal of their obsolete executor relations. Derive Action object IDs with
-`toSpiceDbActionObjectId`; never delete unrelated tuples or rely on rerunning `TOUCH` to revoke
-access. Record and verify this policy-data transition before promotion. An application rollback
-must not silently restore a revoked grant; any policy restoration needs its own reviewed decision.
-The fixed environment's provisioning input records at least one allowed and one denied Principal
-assertion for every `explicit` Action. Promotion verifies every fixed context plus the representative
-non-member for each `tenant_membership_default` Action; it verifies only those recorded per-Action
-assertions for an `explicit` Action. Missing, duplicate, unknown, allow-only, or deny-only explicit
-assertion sets fail before schema or relationship writes.
+The command is operator-invoked, idempotent, accepts no scope arguments, and must not be attached to PostgreSQL migrations, SpiceDB startup, application startup, or automatic deployment. A failure or catalog mismatch blocks promotion. Rollback restores the previous application artifact while leaving the additive schema and relationships in place.
+
+Provisioning is additive, not stale-grant reconciliation. Before narrowing an Action from `tenant_membership_default` to `explicit`, the operator must prepare its intended narrow grants, remove the obsolete `action:#executor@tenant:#member` relation for each affected fixed Tenant, and verify both the intended allowed Principal and a Tenant member who must now be denied. Removed Actions and revoked role/workload assignments likewise require an explicit, reviewed removal of their obsolete executor relations. Derive Action object IDs with `toSpiceDbActionObjectId`; never delete unrelated tuples or rely on rerunning `TOUCH` to revoke access. Record and verify this policy-data transition before promotion. An application rollback must not silently restore a revoked grant; any policy restoration needs its own reviewed decision. The fixed environment's provisioning input records at least one allowed and one denied Principal assertion for every `explicit` Action. Promotion verifies every fixed context plus the representative non-member for each `tenant_membership_default` Action; it verifies only those recorded per-Action assertions for an `explicit` Action. Missing, duplicate, unknown, allow-only, or deny-only explicit assertion sets fail before schema or relationship writes.
### Stage/demo bootstrap
-Stage bootstrap is an operator action, not a migration, startup hook, or automatic deploy step. It
-must remain:
+Stage bootstrap is an operator action, not a migration, startup hook, or automatic deploy step. It must remain:
- limited to a fixed context set in source control;
- explicitly gated to stage;
@@ -237,66 +163,43 @@ Every later canonical state change uses a typed Action.
### Database and authorization
-Use expand/deploy/contract. During a rolling overlap, both previous and candidate code must tolerate
-the expanded PostgreSQL and SpiceDB models.
+Use expand/deploy/contract. During a rolling overlap, both previous and candidate code must tolerate the expanded PostgreSQL and SpiceDB models.
### Module contracts and BFFs
- Public contracts are versioned, bounded, and JSON-safe.
- Normalize values to serializable primitives before public schema validation.
-- Test candidate Shell against the previous MicroVertical contract and candidate MicroVertical
- against the previous Shell contract.
-- A dependency outage produces a typed unavailable/degraded state; it must not corrupt persisted
- module state or disable unrelated modules.
-- Server-governed schemas stay server-local and use the Core Effect runtime. Do not reuse a client
- package's runtime schema object inside the governed server registration.
-- Once explicit alternatives are supported, a Customer Configuration resolves exactly one permitted
- healthy `implementationId` for each selected `moduleId` and rejects missing, ambiguous, invisible,
- or contract-incompatible alternatives. Until then, one implicit `standard` implementation exists.
-- Compatibility versions and immutable build revisions are rollout evidence, not customer-selectable
- product releases.
+- Test candidate Shell against the previous MicroVertical contract and candidate MicroVertical against the previous Shell contract.
+- A dependency outage produces a typed unavailable/degraded state; it must not corrupt persisted module state or disable unrelated modules.
+- Server-governed schemas stay server-local and use the Core Effect runtime. Do not reuse a client package's runtime schema object inside the governed server registration.
+- Once explicit alternatives are supported, a Customer Configuration resolves exactly one permitted healthy `implementationId` for each selected `moduleId` and rejects missing, ambiguous, invisible, or contract-incompatible alternatives. Until then, one implicit `standard` implementation exists.
+- Compatibility versions and immutable build revisions are rollout evidence, not customer-selectable product releases.
### Commerce applications
-Follow [Commerce Application Boundaries](./COMMERCE_APPLICATIONS.md). Storefront Applications and
-their local BFF/proxies deploy independently from OntOS. Promotion must verify each tenant-bound
-Storefront Client, the separate Portal Account realm, native Commerce Storefront API contracts, and
-any declared Medusa compatibility subset. Commerce Operations deploys as a purpose-built staff
-consumer of public module contracts, not as Shell/Core business behavior.
+Follow [Commerce Application Boundaries](./COMMERCE_APPLICATIONS.md). Storefront Applications and their local BFF/proxies deploy independently from OntOS. Promotion must verify each tenant-bound Storefront Client, the separate Portal Account realm, native Commerce Storefront API contracts, and any declared Medusa compatibility subset. Commerce Operations deploys as a purpose-built staff consumer of public module contracts, not as Shell/Core business behavior.
### Module Federation and CSS
-- React, Modern runtime, and provider-context packages such as i18n must be exact strict singletons
- on both Shell and remotes.
-- Promoted compositions pin immutable `mf-manifest.json` references and permit browser execution
- only. Routine upgrades wait for a new browser document; they never force-replace a loaded remote.
-- Shell/Core SSR renders stable framing and typed placeholders. Any future MicroVertical SSR runs in
- a MicroVertical-owned isolated process, not the Shell/Core Node.js process.
-- Every app owns a CSS prefix/namespace. A Shell or MicroVertical build must not scan, erase, or
- collide with another delivery unit's utility classes.
-- A remote is healthy only when its manifest, remote entry, chunks, shared runtime, localized page,
- and Shell integration all load successfully.
+- React, Modern runtime, and provider-context packages such as i18n must be exact strict singletons on both Shell and remotes.
+- Promoted compositions pin immutable `mf-manifest.json` references and permit browser execution only. Routine upgrades wait for a new browser document; they never force-replace a loaded remote.
+- Shell/Core SSR renders stable framing and typed placeholders. Any future MicroVertical SSR runs in a MicroVertical-owned isolated process, not the Shell/Core Node.js process.
+- Every app owns a CSS prefix/namespace. A Shell or MicroVertical build must not scan, erase, or collide with another delivery unit's utility classes.
+- A remote is healthy only when its manifest, remote entry, chunks, shared runtime, localized page, and Shell integration all load successfully.
## Release sequence
Use this sequence for a new or changed MicroVertical:
-1. **Plan:** generate the impacted delivery-unit graph from topology and capture compatibility,
- migration, flag, smoke, and rollback declarations.
+1. **Plan:** generate the impacted delivery-unit graph from topology and capture compatibility, migration, flag, smoke, and rollback declarations.
2. **Preflight:** validate configuration and record last-known-good artifacts.
3. **Build:** produce and verify immutable target-shaped artifacts.
-4. **Migrate:** expand PostgreSQL, refresh grants, verify schemas, then compatibly update SpiceDB
- and complete any required operator-controlled relationship provisioning before deploying a
- fail-closed consumer.
+4. **Migrate:** expand PostgreSQL, refresh grants, verify schemas, then compatibly update SpiceDB and complete any required operator-controlled relationship provisioning before deploying a fail-closed consumer.
5. **Deploy providers:** deploy affected MicroVerticals in dependency order, initially dark.
-6. **Expose providers:** verify readiness, module manifest, BFF, remote assets, and public endpoint;
- make endpoint provisioning idempotent by checking its final state.
-7. **Promote composition:** validate and explicitly promote one immutable candidate revision. A
- compatible MicroVertical update or installation does not redeploy Shell.
-8. **Smoke:** open a new browser document pinned to that revision and execute the authenticated
- distributed smoke suite.
-9. **Canary:** activate the selected module—and its explicit implementation once supported—plus
- affected Storefront Clients for one approved tenant/cohort.
+6. **Expose providers:** verify readiness, module manifest, BFF, remote assets, and public endpoint; make endpoint provisioning idempotent by checking its final state.
+7. **Promote composition:** validate and explicitly promote one immutable candidate revision. A compatible MicroVertical update or installation does not redeploy Shell.
+8. **Smoke:** open a new browser document pinned to that revision and execute the authenticated distributed smoke suite.
+9. **Canary:** activate the selected module—and its explicit implementation once supported—plus affected Storefront Clients for one approved tenant/cohort.
10. **Observe:** hold expansion until the canary window and required signals are healthy.
11. **Expand:** activate additional tenants gradually.
12. **Close:** record deployed digests, smoke evidence, and the new last-known-good set.
@@ -325,8 +228,7 @@ Provider readiness alone is insufficient. The post-deploy release gate exercises
- basic responsive layout/CSS geometry;
- absence of unexpected browser errors and HTTP 5xx responses.
-Run affected unit, integration, database, contract, and browser tests in CI as well. A root `/`
-health probe cannot substitute for this suite.
+Run affected unit, integration, database, contract, and browser tests in CI as well. A root `/` health probe cannot substitute for this suite.
## Observability
@@ -340,12 +242,9 @@ Every deploy and smoke record includes:
- previous and candidate versions for rollback;
- bounded logs for the failing service and direct dependencies.
-Automatically collect failed-service logs. Alert if the administrative migrator remains running
-after the migration phase.
+Automatically collect failed-service logs. Alert if the administrative migrator remains running after the migration phase.
-Never log credentials, signing material, cookies, raw assertions, complete tenant/composition
-payloads, or unbounded schema diagnostics. Unexpected defects keep full internal Effect causes at
-the owning server boundary with correlation context; public errors remain typed and sanitized.
+Never log credentials, signing material, cookies, raw assertions, complete tenant/composition payloads, or unbounded schema diagnostics. Unexpected defects keep full internal Effect causes at the owning server boundary with correlation context; public errors remain typed and sanitized.
## Rollback
@@ -355,17 +254,12 @@ Rollback must be executable and tested before rollout:
2. stop further promotion;
3. identify the failed unit and the last successful phase from structured evidence;
4. explicitly promote the previously validated composition revision;
-5. restore affected delivery units using the deployment automation's immutable release records.
- Composition pins public contract and MF-manifest digests; its `buildMarker` alone is not an
- executable artifact identity. The publisher in #374 must bind the composition revision to those
- release records before supporting rollback;
+5. restore affected delivery units using the deployment automation's immutable release records. Composition pins public contract and MF-manifest digests; its `buildMarker` alone is not an executable artifact identity. The publisher in #374 must bind the composition revision to those release records before supporting rollback;
6. leave additive PostgreSQL and compatible SpiceDB changes in place;
7. rerun the complete authenticated smoke suite;
8. record the rollback artifacts and outcome.
-If cleanup or endpoint provisioning returns an error, accept only a recognized idempotent state and
-verify the final state. `continue-on-error` without final-state verification is not rollback or
-idempotence.
+If cleanup or endpoint provisioning returns an error, accept only a recognized idempotent state and verify the final state. `continue-on-error` without final-state verification is not rollback or idempotence.
## Pull-request and release hygiene
@@ -382,9 +276,7 @@ Every deploy-affecting PR includes a deployment-impact section containing:
- observability fields/dashboard location;
- generator changes required for future MicroVerticals.
-Separate review concerns when useful—normally deployment generator/infrastructure, compatible
-schema, application behavior, and activation—but assemble and prove one immutable release candidate
-before merge. Do not merge a release and then use stage to discover one failure per follow-up PR.
+Separate review concerns when useful—normally deployment generator/infrastructure, compatible schema, application behavior, and activation—but assemble and prove one immutable release candidate before merge. Do not merge a release and then use stage to discover one failure per follow-up PR.
After any failed rehearsal or rollout:
@@ -394,39 +286,16 @@ After any failed rehearsal or rollout:
4. fix the entire failure class and add a regression test;
5. rebuild once and rerun the ordered gates from the beginning.
-Use the CI provider's rerun or manual dispatch for a genuine retry. Do not create empty commits to
-retrigger a pipeline.
+Use the CI provider's rerun or manual dispatch for a genuine retry. Do not create empty commits to retrigger a pipeline.
## Historical release evidence
-Git history and regression tests own the detailed rollout-failure record. When a failure class
-recurs or deployment behavior changes, add a permanent automated contract test instead of extending
-a prose commit list.
+Git history and regression tests own the detailed rollout-failure record. When a failure class recurs or deployment behavior changes, add a permanent automated contract test instead of extending a prose commit list.
## Fail-closed authorization promotion
-Authorization changes deploy schema and data expansion first: the Contacts assertion-redemption
-migration and SpiceDB policy precede every provider and the Shell. Run the inventory check, collect
-sanitized report-only evidence for one source revision and inventory hash, reduce it with
-`pnpm authorization:impact:report`, and validate fixed-context evidence with
-`pnpm authorization:readiness:check -- stage`. The command accepts only a fixed environment name;
-it loads `topology/authorization-contexts/.json` and the fixed inventory, impact,
-observation, and negative-smoke report names. The resulting artifact binds the environment, source
-revision, inventory and context hashes, schema/data versions, replay migration, impact report,
-smoke evidence, observation bounds, and approval reference.
-
-Pass `--authorization-environment ` to `pnpm deployment-impact:plan --` for a
-promotion plan. `report_only` is valid only before its declared expiry and never in production.
-`enforced` requires matching zero-impact, readiness, and negative-smoke artifacts from the exact
-build. Abort on an expired window, mixed build evidence, unresolved impact, missing
-policy/module/worker/issuer/replay data, or a failed negative smoke.
-
-Production remains blocked while no approved source-controlled production context exists; the
-development/stage provisioner must continue rejecting production and arbitrary tenant or Action
-arguments. Issue #173 owns technical implementation and readiness; issue #369 owns the separate
-production-promotion approval gate. Issue #169 is broader review context, not approval. Their current
-records—not this playbook—determine whether the gates are satisfied. An implementation override
-never records Petr/Jiří approval or permits production enforcement. The checked-in stage context
-remains `pending`; code-only override is not approval.
-Rollback restores the prior application mode only after preserving the exact evidence and must not
-remove the expanded schema or durable redemption rows while old/new consumers overlap.
+Authorization changes deploy schema and data expansion first: the Contacts assertion-redemption migration and SpiceDB policy precede every provider and the Shell. Run the inventory check, collect sanitized report-only evidence for one source revision and inventory hash, reduce it with `pnpm authorization:impact:report`, and validate fixed-context evidence with `pnpm authorization:readiness:check -- stage`. The command accepts only a fixed environment name; it loads `topology/authorization-contexts/.json` and the fixed inventory, impact, observation, and negative-smoke report names. The resulting artifact binds the environment, source revision, inventory and context hashes, schema/data versions, replay migration, impact report, smoke evidence, observation bounds, and approval reference.
+
+Pass `--authorization-environment ` to `pnpm deployment-impact:plan --` for a promotion plan. `report_only` is valid only before its declared expiry and never in production. `enforced` requires matching zero-impact, readiness, and negative-smoke artifacts from the exact build. Abort on an expired window, mixed build evidence, unresolved impact, missing policy/module/worker/issuer/replay data, or a failed negative smoke.
+
+Production remains blocked while no approved source-controlled production context exists; the development/stage provisioner must continue rejecting production and arbitrary tenant or Action arguments. Issue #173 owns technical implementation and readiness; issue #369 owns the separate production-promotion approval gate. Issue #169 is broader review context, not approval. Their current records—not this playbook—determine whether the gates are satisfied. An implementation override never records Petr/Jiří approval or permits production enforcement. The checked-in stage context remains `pending`; code-only override is not approval. Rollback restores the prior application mode only after preserving the exact evidence and must not remove the expanded schema or durable redemption rows while old/new consumers overlap.
diff --git a/app/docs/architecture/DRIZZLE_V1_UPGRADE.md b/app/docs/architecture/DRIZZLE_V1_UPGRADE.md
index 9ae658f38..ffb4c427f 100644
--- a/app/docs/architecture/DRIZZLE_V1_UPGRADE.md
+++ b/app/docs/architecture/DRIZZLE_V1_UPGRADE.md
@@ -2,38 +2,20 @@
Status: **applied**
-Cohort: `drizzle-orm@1.0.0-rc.5-ab785fc`, `drizzle-kit@1.0.0-rc.5-ab785fc`, `better-auth@1.7.2`,
-`@better-auth/api-key@1.7.2`, `@better-auth/drizzle-adapter@1.7.2`, `auth@1.7.2`
+Cohort: `drizzle-orm@1.0.0-rc.5-ab785fc`, `drizzle-kit@1.0.0-rc.5-ab785fc`, `better-auth@1.7.2`, `@better-auth/api-key@1.7.2`, `@better-auth/drizzle-adapter@1.7.2`, `auth@1.7.2`
Native persistence: `@effect/sql-pg@4.0.0-beta.107` with `effect@4.0.0-beta.107`.
-This document records how OntOS moved from the stable `0.45.2`/`0.31.10` pair to the Drizzle v1
-release candidate, which repository surfaces changed, how the three migration histories were
-converted without touching any applied migration, and which proofs gate a future Drizzle bump.
-[Database Architecture](./DATABASE.md) remains the authoritative rule set; this page is the
-upgrade record and operator runbook.
+This document records how OntOS moved from the stable `0.45.2`/`0.31.10` pair to the Drizzle v1 release candidate, which repository surfaces changed, how the three migration histories were converted without touching any applied migration, and which proofs gate a future Drizzle bump. [Database Architecture](./DATABASE.md) remains the authoritative rule set; this page is the upgrade record and operator runbook.
## Decision
-OntOS deliberately tracks the Drizzle `rc` channel instead of waiting for `1.0.0` stable. The
-readiness analysis that preceded this upgrade (pull request `TechsioCZ/ontos#98`) deferred the move
-because `drizzle-kit up` produced a false migration for unchanged owners and because the Auth owner
-still used Relational Queries v1. Both blockers are resolved here:
-
-- the false migration is a documented converter defect
- ([drizzle-team/drizzle-orm#6020](https://github.com/drizzle-team/drizzle-orm/issues/6020)) that
- is corrected once, deterministically, by normalizing SQL fragments in the converted snapshots;
-- Better Auth `1.7.2` ships the `@better-auth/drizzle-adapter/relations-v2` entrypoint, so the Auth
- owner moves to `defineRelations` with the officially supported adapter.
-
-The initial upgrade adopted tagged `rc.4`. The native Effect migration in
-[PR #494](https://github.com/TechsioCZ/ontos/pull/494) adopts the exact published snapshot
-`1.0.0-rc.5-ab785fc` for both Drizzle packages. The `rc.4` Effect driver uses a removed
-Effect Schema API and fails with the workspace's Effect version
-([drizzle-team/drizzle-orm#6162](https://github.com/drizzle-team/drizzle-orm/issues/6162)).
-The selected snapshot contains the upstream API update; it is a pinned branch build,
-not a tagged `rc.5` release. Future bumps still require the
-[Re-proof checklist](#re-proof-checklist).
+OntOS deliberately tracks the Drizzle `rc` channel instead of waiting for `1.0.0` stable. The readiness analysis that preceded this upgrade (pull request `TechsioCZ/ontos#98`) deferred the move because `drizzle-kit up` produced a false migration for unchanged owners and because the Auth owner still used Relational Queries v1. Both blockers are resolved here:
+
+- the false migration is a documented converter defect ([drizzle-team/drizzle-orm#6020](https://github.com/drizzle-team/drizzle-orm/issues/6020)) that is corrected once, deterministically, by normalizing SQL fragments in the converted snapshots;
+- Better Auth `1.7.2` ships the `@better-auth/drizzle-adapter/relations-v2` entrypoint, so the Auth owner moves to `defineRelations` with the officially supported adapter.
+
+The initial upgrade adopted tagged `rc.4`. The native Effect migration in [PR #494](https://github.com/TechsioCZ/ontos/pull/494) adopts the exact published snapshot `1.0.0-rc.5-ab785fc` for both Drizzle packages. The `rc.4` Effect driver uses a removed Effect Schema API and fails with the workspace's Effect version ([drizzle-team/drizzle-orm#6162](https://github.com/drizzle-team/drizzle-orm/issues/6162)). The selected snapshot contains the upstream API update; it is a pinned branch build, not a tagged `rc.5` release. Future bumps still require the [Re-proof checklist](#re-proof-checklist).
## What changed
@@ -48,13 +30,11 @@ not a tagged `rc.5` release. Future bumps still require the
| `@better-auth/drizzle-adapter` | indirect | 1.7.2 direct | root, Shell (`/relations-v2` entrypoint) |
| `auth` (Better Auth CLI) | 1.6.23 | 1.7.2 | Shell |
-This table records the original upgrade, when Party was named Contacts. The current cohort
-above supersedes its Drizzle versions. Every owner pins the identical Drizzle pair.
+This table records the original upgrade, when Party was named Contacts. The current cohort above supersedes its Drizzle versions. Every owner pins the identical Drizzle pair.
### Migration folder layout (v3)
-Each owner history is now one folder per migration instead of numbered SQL files plus
-`meta/_journal.json`:
+Each owner history is now one folder per migration instead of numbered SQL files plus `meta/_journal.json`:
```text
packages/core-runtime/drizzle/
@@ -65,32 +45,19 @@ packages/core-runtime/drizzle/
20260901102632_/
```
-Folder names are `<14-digit UTC timestamp>_`; the timestamp is the old journal `when`
-value. `drizzle-kit up` produced every folder, and each `migration.sql` is byte-identical to the SQL
-file it replaced (verified with `cmp` against `git show HEAD:` for all 20 files across
-Core, Auth, and Contacts). Snapshots are DDL snapshots (`version: 8`) with an `id`/`prevIds` chain
-that `drizzle-kit check` and `generate` use to detect non-commutative migrations across branches.
+Folder names are `<14-digit UTC timestamp>_`; the timestamp is the old journal `when` value. `drizzle-kit up` produced every folder, and each `migration.sql` is byte-identical to the SQL file it replaced (verified with `cmp` against `git show HEAD:` for all 20 files across Core, Auth, and Contacts). Snapshots are DDL snapshots (`version: 8`) with an `id`/`prevIds` chain that `drizzle-kit check` and `generate` use to detect non-commutative migrations across branches.
Repository surfaces that referenced the old layout were updated:
- `verticals/contacts/tests/unit/schema-contract.test.ts` reads `/migration.sql`;
-- `packages/core-runtime/tests/integration/contacts-identity-migration.test.ts` reads the renamed
- Core migration folder;
-- `scripts/validate-ultramodern-workspace.mts` allowlists the historical migration files that still
- carry the pre-rename module identity.
+- `packages/core-runtime/tests/integration/contacts-identity-migration.test.ts` reads the renamed Core migration folder;
+- `scripts/validate-ultramodern-workspace.mts` allowlists the historical migration files that still carry the pre-rename module identity.
### Snapshot normalization after `drizzle-kit up`
-`drizzle-kit up` copies SQL fragments from the v0 snapshots verbatim. The v1 schema reader renders
-partial-index predicates and check-constraint expressions without the `"schema"."table".`
-qualifier, so an unchanged schema diffs as changed. On OntOS this produced 39 false DDL statements
-for Core (4 partial-index rebuilds, 31 check-constraint rewrites) and 12 for Contacts.
+`drizzle-kit up` copies SQL fragments from the v0 snapshots verbatim. The v1 schema reader renders partial-index predicates and check-constraint expressions without the `"schema"."table".` qualifier, so an unchanged schema diffs as changed. On OntOS this produced 39 false DDL statements for Core (4 partial-index rebuilds, 31 check-constraint rewrites) and 12 for Contacts.
-The converted snapshots were normalized once with the script below, after `up` and before the first
-`generate`. It strips only the entity's own `""."
".` prefix from index `where`
-predicates, check `value` expressions, and expression index columns. Row-level-security policy
-predicates are intentionally left alone: the v1 reader keeps them qualified, and stripping them
-reintroduces a false `ALTER POLICY` migration.
+The converted snapshots were normalized once with the script below, after `up` and before the first `generate`. It strips only the entity's own `""."
".` prefix from index `where` predicates, check `value` expressions, and expression index columns. Row-level-security policy predicates are intentionally left alone: the v1 reader keeps them qualified, and stripping them reintroduces a false `ALTER POLICY` migration.
```js
// normalize-v1-snapshots.mjs — run once per owner history after `drizzle-kit up`
@@ -139,75 +106,45 @@ for (const root of roots) {
console.log(`normalized ${fragments} fragments in ${files} snapshots`);
```
-Result on this repository: `normalized 333 fragments in 13 snapshots`. After normalization every
-owner's `db:generate` prints `No schema changes, nothing to migrate`. Snapshots are metadata for
-diffing; the normalization changes no SQL and no database object.
+Result on this repository: `normalized 333 fragments in 13 snapshots`. After normalization every owner's `db:generate` prints `No schema changes, nothing to migrate`. Snapshots are metadata for diffing; the normalization changes no SQL and no database object.
### Initial rc.4 schema and runtime changes
-- **Relational Queries v2.** `apps/shell-super-app/api/auth/db/schema.ts` replaces the four
- `relations(...)` declarations with one `authRelations = defineRelations(authDatabaseSchema, ...)`
- graph (`user.sessions`, `user.accounts`, `user.apiKeys`, and the `one` reverse edges). Core and
- Contacts export `coreRelations` / `contactsRelations` as `defineRelations()` with no
- navigational relations yet, which still exposes typed `db.query.
` access.
-- **Executor types.** `NodePgDatabase`, `NodePgDatabase`,
- and `NodePgDatabase` replace the schema-keyed generics. Every
- `drizzle({ client, schema })` call site now passes `relations` instead.
-- **Better Auth.** All five `drizzleAdapter` imports (`service.ts`, `api-key-service.ts`,
- `impersonation-service.ts`, `stage-demo-bootstrap-runtime-infrastructure.ts`, the e2e fixture)
- and `scripts/initialize-local-development.mts` import from
- `@better-auth/drizzle-adapter/relations-v2`. The adapter still receives `schema: authDatabaseSchema`
- (tables keyed by Better Auth model name) and `transaction: true`.
-- **Row-level security.** The deprecated `table.enableRLS()` wrapper `enableGovernedRls` was removed
- from `@app/core-runtime`; Contacts tables are declared with `contactsSchema.table.withRLS(...)`.
- `tenantRlsPolicies` and `tenantLegalEntityRlsPolicies` are unchanged.
-- **Deprecated helpers.** `getTableColumns` became `getColumns`; the Core schema-contract test asserts
- the sequence column through `getSQLType()` because v1 reports `dataType` as `bigint int64`.
+- **Relational Queries v2.** `apps/shell-super-app/api/auth/db/schema.ts` replaces the four `relations(...)` declarations with one `authRelations = defineRelations(authDatabaseSchema, ...)` graph (`user.sessions`, `user.accounts`, `user.apiKeys`, and the `one` reverse edges). Core and Contacts export `coreRelations` / `contactsRelations` as `defineRelations()` with no navigational relations yet, which still exposes typed `db.query.
` access.
+- **Executor types.** `NodePgDatabase`, `NodePgDatabase`, and `NodePgDatabase` replace the schema-keyed generics. Every `drizzle({ client, schema })` call site now passes `relations` instead.
+- **Better Auth.** All five `drizzleAdapter` imports (`service.ts`, `api-key-service.ts`, `impersonation-service.ts`, `stage-demo-bootstrap-runtime-infrastructure.ts`, the e2e fixture) and `scripts/initialize-local-development.mts` import from `@better-auth/drizzle-adapter/relations-v2`. The adapter still receives `schema: authDatabaseSchema` (tables keyed by Better Auth model name) and `transaction: true`.
+- **Row-level security.** The deprecated `table.enableRLS()` wrapper `enableGovernedRls` was removed from `@app/core-runtime`; Contacts tables are declared with `contactsSchema.table.withRLS(...)`. `tenantRlsPolicies` and `tenantLegalEntityRlsPolicies` are unchanged.
+- **Deprecated helpers.** `getTableColumns` became `getColumns`; the Core schema-contract test asserts the sequence column through `getSQLType()` because v1 reports `dataType` as `bigint int64`.
### Better Auth 1.7 account identity
-Better Auth 1.7 keys every provider identity on `(issuer, accountId)` and requires a non-null
-`account.issuer` column with a unique index over both columns. The Auth owner adds that column in
-`20260905002342_add-account-issuer`. The migration is expand-then-tighten inside one transaction:
+Better Auth 1.7 keys every provider identity on `(issuer, accountId)` and requires a non-null `account.issuer` column with a unique index over both columns. The Auth owner adds that column in `20260905002342_add-account-issuer`. The migration is expand-then-tighten inside one transaction:
1. add `issuer` as nullable;
2. refuse to continue if any `provider_id` needs URI encoding (OntOS only has `credential`);
-3. backfill `local:credential` for credential accounts and `local:oauth:` otherwise,
- which is Better Auth's `provider-id` identity strategy;
+3. backfill `local:credential` for credential accounts and `local:oauth:` otherwise, which is Better Auth's `provider-id` identity strategy;
4. refuse to continue if two rows share an `(issuer, account_id)` identity;
5. set `NOT NULL` and create `auth_account_issuer_account_id_uk`.
-Better Auth 1.6 writers do not supply `issuer`, so the migration also installs a `BEFORE INSERT`
-trigger (`auth.account_issuer_compat`) that derives the value with the same rule when a row arrives
-without one. That keeps the previous Shell release working against the expanded schema, as the
-[Deployment](./DEPLOYMENT.md) sequence requires, so the Auth migration stays expand-only. Drop the
-trigger and its function in a later contraction migration once no Better Auth 1.6 writer remains;
-Better Auth 1.7 always writes `issuer` explicitly, so the trigger is inert for the new release.
+Better Auth 1.6 writers do not supply `issuer`, so the migration also installs a `BEFORE INSERT` trigger (`auth.account_issuer_compat`) that derives the value with the same rule when a row arrives without one. That keeps the previous Shell release working against the expanded schema, as the [Deployment](./DEPLOYMENT.md) sequence requires, so the Auth migration stays expand-only. Drop the trigger and its function in a later contraction migration once no Better Auth 1.6 writer remains; Better Auth 1.7 always writes `issuer` explicitly, so the trigger is inert for the new release.
### New `db:check` script
-`pnpm db:check` runs `drizzle-kit check` for Core, Auth, and Contacts. It validates the snapshot
-chain and reports non-commutative migrations when two branches both add migrations. Run it after
-rebasing a branch that touches any `drizzle/` or `drizzle-auth/` folder.
+`pnpm db:check` runs `drizzle-kit check` for Core, Auth, and Contacts. It validates the snapshot chain and reports non-commutative migrations when two branches both add migrations. Run it after rebasing a branch that touches any `drizzle/` or `drizzle-auth/` folder.
## Migration bookkeeping upgrade
-The first `drizzle-kit migrate` with v1 against an existing database upgrades each owner's
-bookkeeping table in `drizzle` (`__drizzle_migrations_core`, `__drizzle_migrations_auth`,
-`__drizzle_migrations_contacts`):
+The first `drizzle-kit migrate` with v1 against an existing database upgrades each owner's bookkeeping table in `drizzle` (`__drizzle_migrations_core`, `__drizzle_migrations_auth`, `__drizzle_migrations_contacts`):
- adds `name text` and backfills it with the v3 folder name matched by `created_at` millis;
- adds `applied_at timestamptz default now()`; pre-upgrade rows keep `applied_at = NULL`;
- keeps `id`, `hash`, and `created_at` unchanged.
-The v1 migrator applies every migration folder missing from the table, not only folders newer than
-the last applied row. This requires the administrative identity that already runs
-`pnpm db:migrate`; no manual SQL is needed.
+The v1 migrator applies every migration folder missing from the table, not only folders newer than the last applied row. This requires the administrative identity that already runs `pnpm db:migrate`; no manual SQL is needed.
## Initial rc.4 proofs
-Environment: Darwin arm64, Node `26.5.0` and pnpm `11.25.0` through `mise exec --`, PostgreSQL 17
-in the local Compose container on port 5433.
+Environment: Darwin arm64, Node `26.5.0` and pnpm `11.25.0` through `mise exec --`, PostgreSQL 17 in the local Compose container on port 5433.
| Proof | Result |
| -------------------------------------------------------- | ---------------------------------------------------------------------------------- |
@@ -226,53 +163,26 @@ in the local Compose container on port 5433.
Use this sequence for `1.0.0-rc.5`, `1.0.0`, or any later Drizzle bump:
-1. Bump `drizzle-orm` and `drizzle-kit` together in the root, Core, Shell, and Party manifests,
- plus the Better Auth cohort when its Drizzle peer range moves; run
- `mise exec -- pnpm install --no-frozen-lockfile`.
-2. Run `pnpm db:generate` and `pnpm db:check`; both must report no changes for unchanged schemas.
- A generated folder for an unchanged owner is a converter or reader regression, not a schema
- change, and must not be committed.
-3. Create a disposable copy of a migrated database (`create database template ontos`),
- point `DATABASE_ADMIN_URL`/`DATABASE_URL` at it, and run `pnpm db:migrate` twice followed by
- `pnpm db:verify`. Row counts per owner must not change and the second run must be a no-op.
+1. Bump `drizzle-orm` and `drizzle-kit` together in the root, Core, Shell, and Party manifests, plus the Better Auth cohort when its Drizzle peer range moves; run `mise exec -- pnpm install --no-frozen-lockfile`.
+2. Run `pnpm db:generate` and `pnpm db:check`; both must report no changes for unchanged schemas. A generated folder for an unchanged owner is a converter or reader regression, not a schema change, and must not be committed.
+3. Create a disposable copy of a migrated database (`create database template ontos`), point `DATABASE_ADMIN_URL`/`DATABASE_URL` at it, and run `pnpm db:migrate` twice followed by `pnpm db:verify`. Row counts per owner must not change and the second run must be a no-op.
4. Run `pnpm db:migrate` and `pnpm db:verify` against an empty database.
-5. Run `pnpm typecheck`, `pnpm lint`, `pnpm db:test`, `pnpm action:test:unit`, `pnpm outbox:test`,
- and `pnpm check`.
+5. Run `pnpm typecheck`, `pnpm lint`, `pnpm db:test`, `pnpm action:test:unit`, `pnpm outbox:test`, and `pnpm check`.
## Native Effect adoption and rc.5 snapshot re-proof
-Core and Party now use `drizzle-orm/effect-postgres` with `@effect/sql-pg`. Their database
-factories retain scoped `pg.Pool` ownership, provide `PgClient.fromPool` and `Reactivity` to
-`makeWithDefaults`, and expose `EffectPgDatabase` executors. Queries and transaction callbacks
-are native Effects. The persistence-attempt wrappers and Effect–Promise–Effect transaction
-bridge are removed. Better Auth retains its supported `node-postgres` adapter integration.
-The four Drizzle Kit configurations omit the removed `strict` and `verbose` options.
-
-The current contracts, including raw `execute(sql, 'objects')` reads and native SQL
-settlement errors, are defined in [Database Architecture](./DATABASE.md). No migration SQL
-changes in this adoption. The latest Core, Party, and Contacts snapshots normalize 40, 81,
-and 7 check/index SQL fragments respectively by removing only their own table qualifier.
-This applies the same snapshot normalization described above to the current history heads.
-Every normalized entity matches the native Kit output; snapshot IDs, ancestry, RLS policies,
-and other fields remain unchanged. Without that normalization, Kit emits false constraint and
-index rebuilds for unchanged schemas. Those generated migrations must not be applied or committed.
+Core and Party now use `drizzle-orm/effect-postgres` with `@effect/sql-pg`. Their database factories retain scoped `pg.Pool` ownership, provide `PgClient.fromPool` and `Reactivity` to `makeWithDefaults`, and expose `EffectPgDatabase` executors. Queries and transaction callbacks are native Effects. The persistence-attempt wrappers and Effect–Promise–Effect transaction bridge are removed. Better Auth retains its supported `node-postgres` adapter integration. The four Drizzle Kit configurations omit the removed `strict` and `verbose` options.
+
+The current contracts, including raw `execute(sql, 'objects')` reads and native SQL settlement errors, are defined in [Database Architecture](./DATABASE.md). No migration SQL changes in this adoption. The latest Core, Party, and Contacts snapshots normalize 40, 81, and 7 check/index SQL fragments respectively by removing only their own table qualifier. This applies the same snapshot normalization described above to the current history heads. Every normalized entity matches the native Kit output; snapshot IDs, ancestry, RLS policies, and other fields remain unchanged. Without that normalization, Kit emits false constraint and index rebuilds for unchanged schemas. Those generated migrations must not be applied or committed.
Re-proof on 2026-09-07 for commit `538e43a9a7b004e28eefc71df6e4a50eb814d51f`:
- Frozen dependency installation passed with the exact cohort above.
-- All 890 workspace unit/component tests and 61 affected Core, Party, and Shell integration
- tests passed, including generated-owner isolation, RLS, Action atomicity, outbox behavior,
- repeatable-read snapshots, cancellation, rollback failure, and uncertain commit recovery.
+- All 890 workspace unit/component tests and 61 affected Core, Party, and Shell integration tests passed, including generated-owner isolation, RLS, Action atomicity, outbox behavior, repeatable-read snapshots, cancellation, rollback failure, and uncertain commit recovery.
- Database schema verifiers passed for Core, Auth, Party, and Contacts and their journals.
-- All 19 [CI validation jobs](https://github.com/TechsioCZ/ontos/actions/runs/34147241046)
- passed, including database/migration integration, generation and generated-code typechecking,
- workspace contracts, lint, typecheck, and Node plus Cloudflare artifact proofs.
+- All 19 [CI validation jobs](https://github.com/TechsioCZ/ontos/actions/runs/34147241046) passed, including database/migration integration, generation and generated-code typechecking, workspace contracts, lint, typecheck, and Node plus Cloudflare artifact proofs.
- The full local production build passed, including federation types and performance readiness.
-These are the native adoption proofs. The historical populated-copy conversion results above
-belong to the initial rc.4 upgrade and are not a new snapshot conversion for this bump.
+These are the native adoption proofs. The historical populated-copy conversion results above belong to the initial rc.4 upgrade and are not a new snapshot conversion for this bump.
-Review follow-up verified `pnpm db:generate` reports no schema changes and `pnpm db:check`
-passes for all four histories after snapshot normalization. It also reran migrations twice
-against the populated disposable development database and verified the exact schemas and
-journals afterward. Fresh-database migration and verification passed in CI.
+Review follow-up verified `pnpm db:generate` reports no schema changes and `pnpm db:check` passes for all four histories after snapshot normalization. It also reran migrations twice against the populated disposable development database and verified the exact schemas and journals afterward. Fresh-database migration and verification passed in CI.
diff --git a/app/docs/architecture/EFFECT_V4_ANTIPATTERN_AUDIT.md b/app/docs/architecture/EFFECT_V4_ANTIPATTERN_AUDIT.md
index 774898efd..4d9c4b6ee 100644
--- a/app/docs/architecture/EFFECT_V4_ANTIPATTERN_AUDIT.md
+++ b/app/docs/architecture/EFFECT_V4_ANTIPATTERN_AUDIT.md
@@ -1,9 +1,6 @@
# Effect v4 anti-pattern audit
-> Historical findings from the original audit. Implementation has changed since this snapshot.
-> [Database Architecture](DATABASE.md) owns the current native Effect database and transaction
-> model; the Promise bridge proposals below are superseded. Use focused architecture documents
-> and executable policy checks to assess current behavior.
+> Historical findings from the original audit. Implementation has changed since this snapshot. [Database Architecture](DATABASE.md) owns the current native Effect database and transaction model; the Promise bridge proposals below are superseded. Use focused architecture documents and executable policy checks to assess current behavior.
## Verdict
diff --git a/app/docs/architecture/EFFECT_V4_LINT_ENFORCEMENT.md b/app/docs/architecture/EFFECT_V4_LINT_ENFORCEMENT.md
index f8dd185bc..afa504e2f 100644
--- a/app/docs/architecture/EFFECT_V4_LINT_ENFORCEMENT.md
+++ b/app/docs/architecture/EFFECT_V4_LINT_ENFORCEMENT.md
@@ -1,42 +1,24 @@
# Effect v4 lint enforcement
-Diagnostic-only implementation of [the existing audit](EFFECT_V4_ANTIPATTERN_AUDIT.md).
-No application violations are repaired, no new dependencies are installed, and none of the
-71 custom rules supplies autofixes or suggestions. All 71 are enabled as errors.
+Diagnostic-only implementation of [the existing audit](EFFECT_V4_ANTIPATTERN_AUDIT.md). No application violations are repaired, no new dependencies are installed, and none of the 71 custom rules supplies autofixes or suggestions. All 71 are enabled as errors.
## Verified snapshot
-- Source snapshot: main commit `9adca84e`, plus this PR's lint tooling. The report captured at
- `1531cfb6` is unchanged by the portable-launcher follow-up. No application-source changes are
- included in this PR; upstream application changes were retained during rebase.
+- Source snapshot: main commit `9adca84e`, plus this PR's lint tooling. The report captured at `1531cfb6` is unchanged by the portable-launcher follow-up. No application-source changes are included in this PR; upstream application changes were retained during rebase.
- Existing lint toolchain: Oxlint 1.79.0, `@oxlint/plugins` 1.79.0, Node 26.8.1 locally.
- Scope: `apps verticals packages scripts`, including Party Registry; **753 files linted**.
- Effect policy report: **5,286 diagnostics in 517 files**; **70 rules report**, one has zero hits.
- Disjoint groups: **3,054 source**, **1,296 tests**, **936 scripts**. Test paths take precedence.
-- Dedicated strict tooling typecheck and **164 tests pass**, covering **2,248 fixture source
- files**, production defaults, registration, reporting failures, temporary cleanup, nested-script
- scope, isolated file-URL discovery, the portable launcher and lint-command scope parity.
-- The Effect-only scan completes without a plugin crash and exits 1 intentionally because
- application debt is reported, not repaired.
-
-Before rebase, the package-script gates and scoped formatting passed. Full lint on the original
-`e38c97c` source snapshot produced 9,395 errors: 3,862 Effect and 5,533 other-policy diagnostics,
-including five unused-disable directives. Those are **historical**, not the rebased totals above.
-
-**Clean-install CI on `1531cfb6`:** [run 33979642298](https://github.com/TechsioCZ/ontos/actions/runs/33979642298)
-passes all non-lint validation jobs, including strict rule types and 162/162 tests, application
-Typecheck, Format, Workspace Contract, database integration, generation, and Node/Workerd artifact
-proofs. Full lint intentionally reports **12,031 errors on 753 files**; stage deployment is skipped.
-
-**Local environment:** installed application dependencies still lag main's updated lockfile, so
-pnpm package wrappers can report `ERR_PNPM_VERIFY_DEPS_BEFORE_RUN`. No local dependency install was
-performed; unchanged installed lint/compiler binaries were invoked directly. The separate clean
-CI run provides synchronized verification, but neither run is a successful full `pnpm check`
-because application lint debt remains. The Effect-only AST scan does not typecheck application
-dependencies.
-
-Counts are diagnostic occurrences, **not unique audit clusters** or proof of independent bugs. Several rules can report at one source location. Zero hits does not mean a rule is disabled:
-`no-runtime-construction-outside-root` has verified positive fixture coverage.
+- Dedicated strict tooling typecheck and **164 tests pass**, covering **2,248 fixture source files**, production defaults, registration, reporting failures, temporary cleanup, nested-script scope, isolated file-URL discovery, the portable launcher and lint-command scope parity.
+- The Effect-only scan completes without a plugin crash and exits 1 intentionally because application debt is reported, not repaired.
+
+Before rebase, the package-script gates and scoped formatting passed. Full lint on the original `e38c97c` source snapshot produced 9,395 errors: 3,862 Effect and 5,533 other-policy diagnostics, including five unused-disable directives. Those are **historical**, not the rebased totals above.
+
+**Clean-install CI on `1531cfb6`:** [run 33979642298](https://github.com/TechsioCZ/ontos/actions/runs/33979642298) passes all non-lint validation jobs, including strict rule types and 162/162 tests, application Typecheck, Format, Workspace Contract, database integration, generation, and Node/Workerd artifact proofs. Full lint intentionally reports **12,031 errors on 753 files**; stage deployment is skipped.
+
+**Local environment:** installed application dependencies still lag main's updated lockfile, so pnpm package wrappers can report `ERR_PNPM_VERIFY_DEPS_BEFORE_RUN`. No local dependency install was performed; unchanged installed lint/compiler binaries were invoked directly. The separate clean CI run provides synchronized verification, but neither run is a successful full `pnpm check` because application lint debt remains. The Effect-only AST scan does not typecheck application dependencies.
+
+Counts are diagnostic occurrences, **not unique audit clusters** or proof of independent bugs. Several rules can report at one source location. Zero hits does not mean a rule is disabled: `no-runtime-construction-outside-root` has verified positive fixture coverage.
## Reproduce
@@ -50,17 +32,11 @@ pnpm lint:effect --json
pnpm lint
```
-`lint:effect --json` includes every diagnostic, every affected file, and all 71 rule totals.
-The text report explicitly caps only the top-file display at 20. `pnpm check` includes the rule
-gates before application lint; existing reported violations intentionally block it. No `--fix`
-command was run. The implementation [README](../../tools/oxlint/effect-native/README.md) describes
-fixture development, options, and the fail-closed process harness.
+`lint:effect --json` includes every diagnostic, every affected file, and all 71 rule totals. The text report explicitly caps only the top-file display at 20. `pnpm check` includes the rule gates before application lint; existing reported violations intentionally block it. No `--fix` command was run. The implementation [README](../../tools/oxlint/effect-native/README.md) describes fixture development, options, and the fail-closed process harness.
## Audit-to-rule catalog
-The audit column is the **primary** section, not an exclusive mapping. Cross-cutting findings
-(for example B2 time control, A1 reusable clients, A4 ADTs) can also motivate these rules.
-Follow each rule link for its exact detection policy, defaults, exemptions, and limitations.
+The audit column is the **primary** section, not an exclusive mapping. Cross-cutting findings (for example B2 time control, A1 reusable clients, A4 ADTs) can also motivate these rules. Follow each rule link for its exact detection policy, defaults, exemptions, and limitations.
| Rule | Audit | Total | Source | Tests | Scripts |
| -------------------------------------------------------------------------------------------------------------------------------------------- | ----- | ----: | -----: | ----: | ------: |
@@ -138,50 +114,26 @@ Follow each rule link for its exact detection policy, defaults, exemptions, and
## Boundaries that remain review work
-- AST/scope evidence is not a TypeScript semantic checker or cross-file dataflow engine.
- Imported barrels, opaque aliases, arbitrary dynamic keys, external schemas and indirect
- ownership require explicit configuration or review; syntax alone cannot establish them.
-- Sequential-yield and timeout checks are **review candidates**, not proofs of safe concurrency
- or end-to-end deadlines. Never parallelize writes/authentication/reconciliation mechanically.
-- Schema/tag/secret/temporal-name policies cannot establish complete domain semantics. Nullable
- wire encodings may be deliberate; any later codec migration requires round-trip verification.
-- Runtime/layer/observability checks cannot prove resource lifetimes, Layer installation, context
- propagation, exporter connectivity, redaction completeness, or application-wide composition.
-- Effect-shaped port checks cannot create transaction affinity or prove rollback behavior. S1
- still needs transactional integration evidence; a syntactically clean port is insufficient.
-- A7 shared contract authority, vocabulary reuse, and schema equivalence remain cross-file
- architecture work beyond the local structural/document and schema detectors.
-- A8 template checks are lexical: arbitrary generated/dynamically assembled source and real
- scaffold quality still need generator tests and emitted-project gates.
-- B2 uses the upstream `effect-rstest` harness (`it.effect`/`it.live`/`it.layer`),
- enforced by the `no-effect-run-in-tests` and restricted-imports gates.
+- AST/scope evidence is not a TypeScript semantic checker or cross-file dataflow engine. Imported barrels, opaque aliases, arbitrary dynamic keys, external schemas and indirect ownership require explicit configuration or review; syntax alone cannot establish them.
+- Sequential-yield and timeout checks are **review candidates**, not proofs of safe concurrency or end-to-end deadlines. Never parallelize writes/authentication/reconciliation mechanically.
+- Schema/tag/secret/temporal-name policies cannot establish complete domain semantics. Nullable wire encodings may be deliberate; any later codec migration requires round-trip verification.
+- Runtime/layer/observability checks cannot prove resource lifetimes, Layer installation, context propagation, exporter connectivity, redaction completeness, or application-wide composition.
+- Effect-shaped port checks cannot create transaction affinity or prove rollback behavior. S1 still needs transactional integration evidence; a syntactically clean port is insufficient.
+- A7 shared contract authority, vocabulary reuse, and schema equivalence remain cross-file architecture work beyond the local structural/document and schema detectors.
+- A8 template checks are lexical: arbitrary generated/dynamically assembled source and real scaffold quality still need generator tests and emitted-project gates.
+- B2 uses the upstream `effect-rstest` harness (`it.effect`/`it.live`/`it.layer`), enforced by the `no-effect-run-in-tests` and restricted-imports gates.
## Audit exceptions preserved
-Forced React/TanStack/Modern.js/Playwright/Drizzle/Node Promise boundaries, the deliberately
-owned outer runner seam, startup `Layer.orDie` after typed-cause logging, JSONB/HttpApi encoding,
-external test APIs requiring serialized bodies, malformed rejection-test casts, legitimate
-`as const`/`satisfies`, line-preserving `.env` edits, native collections, recursive JSON array
-normalization and correctly scoped fibers are not blanket migration targets. Operational
-success console output remains allowed. The dropped Rspack injected-global finding stays dropped.
+Forced React/TanStack/Modern.js/Playwright/Drizzle/Node Promise boundaries, the deliberately owned outer runner seam, startup `Layer.orDie` after typed-cause logging, JSONB/HttpApi encoding, external test APIs requiring serialized bodies, malformed rejection-test casts, legitimate `as const`/`satisfies`, line-preserving `.env` edits, native collections, recursive JSON array normalization and correctly scoped fibers are not blanket migration targets. Operational success console output remains allowed. The dropped Rspack injected-global finding stays dropped.
-Exemptions are bounded by the local evidence/options each rule documents, not a guarantee that
-every opaque implementation is classified correctly. Correct a confirmed false positive in the
-detector with a regression; do not silence genuine architectural debt with blanket disables.
+Exemptions are bounded by the local evidence/options each rule documents, not a guarantee that every opaque implementation is classified correctly. Correct a confirmed false positive in the detector with a regression; do not silence genuine architectural debt with blanket disables.
## Verification design
-- Real Oxlint processes run all positive and negative inputs; exact counts are asserted where
- declared. Explicit file lists and file-count checks prevent ignored-directory inputs from
- masquerading as verified tests. Declaration syntax is tested in ordinary `.ts` files.
-- Production-default checks stage copies outside `tools/**/tests` ancestry so fixture paths
- do not accidentally select a test-only scope. Option overrides remain separate evidence.
-- Loader errors, malformed output, unexpected diagnostics, inconsistent exits, empty-file
- reports and stderr failures fail the harness rather than being reported as zero violations.
-- Registration imports the actual plugin/config and checks complete rule coverage, error
- severity, preserved typed lint settings, and absence of fixer/suggestion metadata.
-- Temporary workspaces are owned, cleaned on success/failure/normal termination, and covered
- by early/partial-failure and termination regressions. SIGKILL/host loss cannot be cleaned
- synchronously; use an isolated temporary root when running under an external supervisor.
-- Existing application tests and application fixes are outside this change. Full `pnpm check`
- cannot pass while intentionally reported lint debt remains.
+- Real Oxlint processes run all positive and negative inputs; exact counts are asserted where declared. Explicit file lists and file-count checks prevent ignored-directory inputs from masquerading as verified tests. Declaration syntax is tested in ordinary `.ts` files.
+- Production-default checks stage copies outside `tools/**/tests` ancestry so fixture paths do not accidentally select a test-only scope. Option overrides remain separate evidence.
+- Loader errors, malformed output, unexpected diagnostics, inconsistent exits, empty-file reports and stderr failures fail the harness rather than being reported as zero violations.
+- Registration imports the actual plugin/config and checks complete rule coverage, error severity, preserved typed lint settings, and absence of fixer/suggestion metadata.
+- Temporary workspaces are owned, cleaned on success/failure/normal termination, and covered by early/partial-failure and termination regressions. SIGKILL/host loss cannot be cleaned synchronously; use an isolated temporary root when running under an external supervisor.
+- Existing application tests and application fixes are outside this change. Full `pnpm check` cannot pass while intentionally reported lint debt remains.
diff --git a/app/docs/architecture/ERRORS.md b/app/docs/architecture/ERRORS.md
index fbbea05c1..2d157bba4 100644
--- a/app/docs/architecture/ERRORS.md
+++ b/app/docs/architecture/ERRORS.md
@@ -2,10 +2,7 @@
This document defines the error contract from backend Effect programs, through HTTP, into generated Backend for Frontend (BFF) clients and frontend feature code.
-Governed context, read denial/evidence, and isolation failures follow
-[Governed Data Access and Operation Scope](./DATA_ACCESS.md). Context or authorization uncertainty
-is a declared retryable `503`; business handlers and adapters must not receive database executors or
-leak database/SpiceDB diagnostics.
+Governed context, read denial/evidence, and isolation failures follow [Governed Data Access and Operation Scope](./DATA_ACCESS.md). Context or authorization uncertainty is a declared retryable `503`; business handlers and adapters must not receive database executors or leak database/SpiceDB diagnostics.
## Non-Negotiable Rules
@@ -39,23 +36,13 @@ Internal domain and infrastructure errors may be more detailed than the public c
Module entrypoint failures from [Module Entrypoints and Tenant State](./MODULE_ENTRYPOINTS.md) remain typed and sanitized across every boundary. A definite tenant-state denial normally maps to a declared `403`; an unavailable/indeterminate gate check maps to a declared retryable `503`. Frontend integrations must handle both explicitly before any private implementation or remote is loaded.
-Shell composition uses `401` for a missing session, `409` when legal-entity selection is required,
-`403` for definite module/resource denial, `404` for safely undiscoverable targets, retryable `503`
-for catalog/state/context/authorization/provider uncertainty, and a redacted declared `500` only
-after logging an unexpected Effect cause. HTTP status and Problem Details `status` must match.
+Shell composition uses `401` for a missing session, `409` when legal-entity selection is required, `403` for definite module/resource denial, `404` for safely undiscoverable targets, retryable `503` for catalog/state/context/authorization/provider uncertainty, and a redacted declared `500` only after logging an unexpected Effect cause. HTTP status and Problem Details `status` must match.
Unexpected defects are not expected failures. At the outer HTTP seam, log the full Effect cause with correlation context, then convert it to a declared, non-sensitive typed `InternalServerError` with status `500`. No defect may escape as an unstructured backend response.
Generated Action BFF endpoints must also map the complete Core Action error union. `ActionPolicyDenied` carries a stable Policy reason code and safe human-readable reason, but Core deliberately assigns no HTTP status: the endpoint maps the Policy's declared semantics to the correct public Problem Details schema, such as `403` for authorization-like denial, `409` for current-state conflict, or `422` for semantic ineligibility. `ActionPolicyEvaluationError` represents a sanitized evaluator defect or unavailable required capability and must map to the endpoint's declared operational failure, commonly a retryable `503` when appropriate. Neither error may fall through to an exception, generic Action endpoint, or ad hoc response.
-For the Shell-user MicroVertical Action identity boundary, the generated verifier keeps expected
-failures typed as missing, invalid, expired, scope-invalid, configuration, or verification
-unavailable errors. The owning endpoint must map missing, invalid, expired, and scope-invalid
-credentials to its declared `401` Problem Details schema and attach `WWW-Authenticate: Bearer`.
-Configuration or verification capability failures map to a declared retryable `503`. Never expose
-the assertion, a JWK, signature diagnostics, or claim contents in Problem Details or logs. A verified
-assertion supplies authentication context only; SpiceDB denial remains `403` and Policy failures
-retain their endpoint-specific semantics.
+For the Shell-user MicroVertical Action identity boundary, the generated verifier keeps expected failures typed as missing, invalid, expired, scope-invalid, configuration, or verification unavailable errors. The owning endpoint must map missing, invalid, expired, and scope-invalid credentials to its declared `401` Problem Details schema and attach `WWW-Authenticate: Bearer`. Configuration or verification capability failures map to a declared retryable `503`. Never expose the assertion, a JWK, signature diagnostics, or claim contents in Problem Details or logs. A verified assertion supplies authentication context only; SpiceDB denial remains `403` and Policy failures retain their endpoint-specific semantics.
## Status Code Semantics
@@ -74,40 +61,15 @@ Choose the status from the meaning of the failure, not from a generic domain-err
| `503` | A required capability is temporarily unavailable and retry may succeed later. |
| `504` | A required upstream operation did not complete before its deadline. |
-Identity endpoints apply the same meanings exhaustively. Missing or unusable Shell credentials use
-`401` with a Bearer challenge; the API-key exchange uses an API-key challenge. A definite permission
-denial or active credential bound to a forbidden tenant/principal/legal entity is `403`; lifecycle
-state races are `409`; missing runtime records are `404`; ineligible targets are `422`; a missing
-required idempotency key is `428`; provider throttling is `429`. Structurally invalid operation
-payloads are `400`. Database,
-SpiceDB, resolver, evidence, or provider uncertainty is retryable `503`, while only caught defects
-at the outer handler seam become sanitized `500`. Problem Details never include keys, hashes,
-cookies, provider diagnostics, identifiers, or signature details.
-
-`ActionAlreadyCommitted` is a terminal idempotency conflict (`409`) at identity transports, not a
-retryable capability outage. The internal stopped-impersonation recovery path treats that exact
-outcome as successful checkpoint replay, then retries deletion of its Auth-owned recovery record.
-If any work after provider restoration remains pending, Shell forwards the restored cookie first
-and returns the declared retryable `503` without exposing recovery data.
-Requested and started checkpoint failures preserve their typed Action error: definite permission
-denial maps to `403`, invalid identity state maps to `422`, and authorization or persistence
-uncertainty maps to `503`. A stopped checkpoint that fails after mechanical termination is reported
-only as pending recovery and never reactivates the impersonated session.
+Identity endpoints apply the same meanings exhaustively. Missing or unusable Shell credentials use `401` with a Bearer challenge; the API-key exchange uses an API-key challenge. A definite permission denial or active credential bound to a forbidden tenant/principal/legal entity is `403`; lifecycle state races are `409`; missing runtime records are `404`; ineligible targets are `422`; a missing required idempotency key is `428`; provider throttling is `429`. Structurally invalid operation payloads are `400`. Database, SpiceDB, resolver, evidence, or provider uncertainty is retryable `503`, while only caught defects at the outer handler seam become sanitized `500`. Problem Details never include keys, hashes, cookies, provider diagnostics, identifiers, or signature details.
+
+`ActionAlreadyCommitted` is a terminal idempotency conflict (`409`) at identity transports, not a retryable capability outage. The internal stopped-impersonation recovery path treats that exact outcome as successful checkpoint replay, then retries deletion of its Auth-owned recovery record. If any work after provider restoration remains pending, Shell forwards the restored cookie first and returns the declared retryable `503` without exposing recovery data. Requested and started checkpoint failures preserve their typed Action error: definite permission denial maps to `403`, invalid identity state maps to `422`, and authorization or persistence uncertainty maps to `503`. A stopped checkpoint that fails after mechanical termination is reported only as pending recovery and never reactivates the impersonated session.
Use other RFC 9110 statuses when they are a more accurate semantic match. Do not disguise authentication or authorization failures as validation errors, and do not use `500` for declared business rejections.
## Core Action Permission Failures
-Core keeps its Action errors transport-neutral. A future Action BFF endpoint
-must exhaustively map `ActionPermissionDenied` to a declared `403` Problem
-Details schema and `ActionPermissionCheckError` to a declared `503` Problem
-Details schema. The denial exposes only its stable code and safe reason. The
-absence of an executor relationship is a definite `NO_PERMISSION` denial, not
-an unavailable configuration state. The check error covers timeout,
-unavailability, authentication or schema failure, and any conditional or
-otherwise indeterminate SpiceDB decision; it must never be reclassified as a
-permission denial or an unconfigured-Action allow. Do not introduce a generic
-Action HTTP endpoint to perform this mapping.
+Core keeps its Action errors transport-neutral. A future Action BFF endpoint must exhaustively map `ActionPermissionDenied` to a declared `403` Problem Details schema and `ActionPermissionCheckError` to a declared `503` Problem Details schema. The denial exposes only its stable code and safe reason. The absence of an executor relationship is a definite `NO_PERMISSION` denial, not an unavailable configuration state. The check error covers timeout, unavailability, authentication or schema failure, and any conditional or otherwise indeterminate SpiceDB decision; it must never be reclassified as a permission denial or an unconfigured-Action allow. Do not introduce a generic Action HTTP endpoint to perform this mapping.
## Problem Details
@@ -120,18 +82,9 @@ Every error response must contain a Problem Details body whose schema is declare
Add structured extension members only when clients need them to recover, such as safe field issues, a retry hint, or a stable domain reason code. Keep those extensions typed in Effect Schema.
-Contract modules construct these schemas with the browser-safe `makeProblemDetailsSchema` and
-`makeRetryableProblemDetailsSchema` helpers from `@app/shared-contracts/problem-details`. The
-dedicated package entrypoint has no owner-local handler, environment, JOSE, database, or runtime
-dependency. The helper couples the literal body status, HttpApi status annotation, and
-`application/problem+json` representation. The retryable constructor deliberately adds only
-`retryable: true`; other safe recovery data must be supplied as concrete Effect Schema fields.
-Reserved Problem Details fields, arbitrary records, `Schema.Unknown`, and `Schema.Any` are not
-extension points.
+Contract modules construct these schemas with the browser-safe `makeProblemDetailsSchema` and `makeRetryableProblemDetailsSchema` helpers from `@app/shared-contracts/problem-details`. The dedicated package entrypoint has no owner-local handler, environment, JOSE, database, or runtime dependency. The helper couples the literal body status, HttpApi status annotation, and `application/problem+json` representation. The retryable constructor deliberately adds only `retryable: true`; other safe recovery data must be supplied as concrete Effect Schema fields. Reserved Problem Details fields, arbitrary records, `Schema.Unknown`, and `Schema.Any` are not extension points.
-These helpers are transport-contract infrastructure, not a catalog of business errors. Every
-contract still chooses its endpoint-specific tag, status, typed extensions, and visibly ordered
-error collection. Do not derive universal tags or endpoint semantics from status codes.
+These helpers are transport-contract infrastructure, not a catalog of business errors. Every contract still chooses its endpoint-specific tag, status, typed extensions, and visibly ordered error collection. Do not derive universal tags or endpoint semantics from status codes.
## Generated Client Contract
@@ -162,9 +115,4 @@ Before completing backend or BFF client work, verify:
## Single-use gateway assertions
-The receiving owner verifies signature, issuer, audience, expiry, version, `jti`, and trusted
-principal claims before attempting redemption. Atomic duplicate redemption is a typed unusable
-credential and maps to the same sanitized `401` family as another invalid Bearer assertion, with
-`WWW-Authenticate: Bearer`. Redemption storage failure is a typed unavailable result and maps to
-retryable `503`. Neither response exposes the assertion, `jti`, principal, tenant, or storage
-diagnostic.
+The receiving owner verifies signature, issuer, audience, expiry, version, `jti`, and trusted principal claims before attempting redemption. Atomic duplicate redemption is a typed unusable credential and maps to the same sanitized `401` family as another invalid Bearer assertion, with `WWW-Authenticate: Bearer`. Redemption storage failure is a typed unavailable result and maps to retryable `503`. Neither response exposes the assertion, `jti`, principal, tenant, or storage diagnostic.
diff --git a/app/docs/architecture/MICROVERTICALS.md b/app/docs/architecture/MICROVERTICALS.md
index c39c1b4e5..93baa5b6f 100644
--- a/app/docs/architecture/MICROVERTICALS.md
+++ b/app/docs/architecture/MICROVERTICALS.md
@@ -2,19 +2,11 @@
Each MicroVertical is a complete, independently deployable business module. It owns its domain model, database schema and migrations, repositories, Effect services, Backend for Frontend (BFF) contract and implementation, generated BFF client, and feature UI.
-The UltraModern topology `appId` identifies that deployment. Its OntOS `moduleId` identifies the
-business capability and owns Actions, resources, events, Outbox contracts, Policies, and tenant
-module state. Follow [OntOS Module Manifests](./MODULE_MANIFESTS.md); never infer one identity from
-the other.
+The UltraModern topology `appId` identifies that deployment. Its OntOS `moduleId` identifies the business capability and owns Actions, resources, events, Outbox contracts, Policies, and tenant module state. Follow [OntOS Module Manifests](./MODULE_MANIFESTS.md); never infer one identity from the other.
-For Customer Configuration alternatives, `moduleId` is the stable Module Contract Identity and
-`implementationId` identifies one explicit catalogued executable implementation. Different public
-semantics require a different `moduleId`; invisible same-identity forks are forbidden. Follow
-[Commerce Application Boundaries](./COMMERCE_APPLICATIONS.md).
+For Customer Configuration alternatives, `moduleId` is the stable Module Contract Identity and `implementationId` identifies one explicit catalogued executable implementation. Different public semantics require a different `moduleId`; invisible same-identity forks are forbidden. Follow [Commerce Application Boundaries](./COMMERCE_APPLICATIONS.md).
-The current generated manifest/catalog does not yet implement `implementationId`; the only safe
-current state is one implicit `standard` implementation per `moduleId`. Do not encode alternatives
-with ad hoc fields or branches. Extend the generator and validation contract first.
+The current generated manifest/catalog does not yet implement `implementationId`; the only safe current state is one implicit `standard` implementation per `moduleId`. Do not encode alternatives with ad hoc fields or branches. Extend the generator and validation contract first.
## Seam Model
@@ -32,26 +24,19 @@ The vertical seam between MicroVerticals is non-negotiable:
- Every MicroVertical must be deployable to its own server or process independently of every other MicroVertical.
- Moving a MicroVertical from a shared host to a separate host must require deployment configuration or adapter selection only. It must not require changes to consuming business logic.
- A MicroVertical must not import another MicroVertical's implementation, access its database or repositories, call its internal Effect services, or participate in its database transaction.
-- Shell/Core and other MicroVerticals must not import another deployment's `vertical.manifest.ts`
- or `vertical.registration.ts`. The serialized, composition-approved module contract is the
- metadata seam; executable registration remains inside its owning deployment.
+- Shell/Core and other MicroVerticals must not import another deployment's `vertical.manifest.ts` or `vertical.registration.ts`. The serialized, composition-approved module contract is the metadata seam; executable registration remains inside its owning deployment.
- Shared packages may contain stable contracts and genuinely cross-cutting infrastructure. They must not become a back door for sharing MicroVertical business logic or persistence models.
- Executable Policies owned by a MicroVertical are private, owner-local business behavior. Another MicroVertical must not import, register, or execute them. The only cross-module Policy reference exception is the narrow global Policy contract implemented and owned by Shell/Core; an Action may reference a global Policy without gaining access to Core repositories or another module's services.
- Synchronous communication may cross the seam only through the provider's published, contract-derived Effect client.
- Every module entrypoint crosses through the structured Shell/Core gateway and tenant-state rules in [Module Entrypoints and Tenant State](./MODULE_ENTRYPOINTS.md). Raw remote loads, direct private route/handler imports, and eager private implementations are forbidden.
-- Application Composition, not topology or tenant state, is the runtime authority for the approved
- module graph and exact artifact revisions. First-party remote UI executes only in the browser;
- independently deployed MicroVertical code never executes inside the Shell/Core Node.js process.
+- Application Composition, not topology or tenant state, is the runtime authority for the approved module graph and exact artifact revisions. First-party remote UI executes only in the browser; independently deployed MicroVertical code never executes inside the Shell/Core Node.js process.
- Asynchronous communication may cross the seam only through Outbox Messages and their published schemas, using the lifecycle in [Outbox Worker Architecture](./OUTBOX_WORKERS.md).
- Every synchronous request must propagate tenant, principal or service identity, and correlation context. The receiving MicroVertical authenticates and authorizes the request independently; co-location never implies trust.
- Contract adapters must have equivalent observable behavior whether communication is in-process or over the network.
The published client is the calling MicroVertical's interface to the provider. The provider's backend implementation remains private.
-Each provider also follows [Governed Data Access and Operation Scope](./DATA_ACCESS.md): its public
-operation descriptor chooses legal-entity scope explicitly, while its private handler receives only
-owner-local services constructed after Core validates context and installs transaction scope. A
-deployment seam never grants database or executor access.
+Each provider also follows [Governed Data Access and Operation Scope](./DATA_ACCESS.md): its public operation descriptor chooses legal-entity scope explicitly, while its private handler receives only owner-local services constructed after Core validates context and installs transaction scope. A deployment seam never grants database or executor access.
## Horizontal Seam: A Virtual Effect BFF Interface
@@ -107,70 +92,24 @@ Follow [Frontend Architecture Rules](../frontend/FRONTEND.md) for the complete f
## Staff Authentication Boundary
-Staff authentication is a cross-cutting Shell/Core capability, never a MicroVertical. The
-Shell owns staff credentials, Better Auth sessions and cookies, the strict Effect
-authentication BFF, and the private `auth` schema. Core owns only non-secret
-principal auth bindings and active principal/tenant resolution. Do not create an
-Auth vertical, remote, package, delivery unit, or Module Federation boundary.
-
-Commerce Portal Accounts are the deliberate separate-realm exception, not an Auth MicroVertical:
-Commerce owns their distinct BetterAuth configuration/schema, cookies, sessions, account lifecycle,
-and owner-local Principal/Party linkage. They never enter the Shell staff realm. Storefront Clients
-are separately bound service Principals and never identify portal users. Follow
-[Commerce Application Boundaries](./COMMERCE_APPLICATIONS.md).
-
-One Better Auth user may have active bindings to multiple tenant-scoped Principals. Exactly one
-nullable active tenant ID on the current Better Auth session selects which eligible Principal and
-Tenant become trusted context for reads, gateway assertions, and Actions. Core Principal Auth
-Bindings remain the tenant-access authority: the selected session field grants no permission and
-must be revalidated against an active binding, Principal, and Tenant on every session resolution.
-This does not introduce a global Principal, Better Auth Organization/member tables, an Auth
-MicroVertical, or a generic context store.
-
-API-key callers terminate at Shell using `X-API-Key`. Better Auth verifies the credential and
-returns its private stable key ID; Core resolves exactly one active binding; Shell then issues the
-same 300-second assertion for one explicit MicroVertical audience. The key ID remains private join
-data and the raw key never crosses Shell. Separate keys are required for separate tenant/principal
-bindings.
-
-Support impersonation is tenant-local. The assertion and every receiving operation identify the
-target as effective principal and the original administrator as impersonator; authorization and
-Policies use the target. Both identities and support permission are revalidated. Trusted system
-jobs bypass neither boundary: they are constructed inside Core from a branded workload registration
-and active configured `system` or explicitly approved `service` principal, and are not gateway or
-HTTP capabilities.
-
-Stopping impersonation remains available when either identity or support permission changed after
-start. Auth writes a bounded non-secret recovery record before the started checkpoint completes,
-retains it through provider restoration or expiry, always forwards the restored session cookie, and
-retries the stopped Action checkpoint from the original session.
-This recovery table is private Auth mechanics and never becomes a MicroVertical contract or generic
-identity store. Recovery relaxes only the historical active-session validation needed to describe
-the stopped event; the restricted Action still requires its explicit SpiceDB permission.
-
-Authenticated Shell composition also requires exactly one active, tenant-owned, authorized legal
-entity persisted on that session. Tenant changes clear the legal entity; stale, cross-tenant,
-inactive, or newly denied selections fail closed. Browser switch payloads contain only the requested
-ID. The Shell assertion includes the revalidated legal-entity ID, while every receiver authorizes
-module/resource/Action access independently.
+Staff authentication is a cross-cutting Shell/Core capability, never a MicroVertical. The Shell owns staff credentials, Better Auth sessions and cookies, the strict Effect authentication BFF, and the private `auth` schema. Core owns only non-secret principal auth bindings and active principal/tenant resolution. Do not create an Auth vertical, remote, package, delivery unit, or Module Federation boundary.
+
+Commerce Portal Accounts are the deliberate separate-realm exception, not an Auth MicroVertical: Commerce owns their distinct BetterAuth configuration/schema, cookies, sessions, account lifecycle, and owner-local Principal/Party linkage. They never enter the Shell staff realm. Storefront Clients are separately bound service Principals and never identify portal users. Follow [Commerce Application Boundaries](./COMMERCE_APPLICATIONS.md).
+
+One Better Auth user may have active bindings to multiple tenant-scoped Principals. Exactly one nullable active tenant ID on the current Better Auth session selects which eligible Principal and Tenant become trusted context for reads, gateway assertions, and Actions. Core Principal Auth Bindings remain the tenant-access authority: the selected session field grants no permission and must be revalidated against an active binding, Principal, and Tenant on every session resolution. This does not introduce a global Principal, Better Auth Organization/member tables, an Auth MicroVertical, or a generic context store.
+
+API-key callers terminate at Shell using `X-API-Key`. Better Auth verifies the credential and returns its private stable key ID; Core resolves exactly one active binding; Shell then issues the same 300-second assertion for one explicit MicroVertical audience. The key ID remains private join data and the raw key never crosses Shell. Separate keys are required for separate tenant/principal bindings.
+
+Support impersonation is tenant-local. The assertion and every receiving operation identify the target as effective principal and the original administrator as impersonator; authorization and Policies use the target. Both identities and support permission are revalidated. Trusted system jobs bypass neither boundary: they are constructed inside Core from a branded workload registration and active configured `system` or explicitly approved `service` principal, and are not gateway or HTTP capabilities.
+
+Stopping impersonation remains available when either identity or support permission changed after start. Auth writes a bounded non-secret recovery record before the started checkpoint completes, retains it through provider restoration or expiry, always forwards the restored session cookie, and retries the stopped Action checkpoint from the original session. This recovery table is private Auth mechanics and never becomes a MicroVertical contract or generic identity store. Recovery relaxes only the historical active-session validation needed to describe the stopped event; the restricted Action still requires its explicit SpiceDB permission.
+
+Authenticated Shell composition also requires exactly one active, tenant-owned, authorized legal entity persisted on that session. Tenant changes clear the legal entity; stale, cross-tenant, inactive, or newly denied selections fail closed. Browser switch payloads contain only the requested ID. The Shell assertion includes the revalidated legal-entity ID, while every receiver authorizes module/resource/Action access independently.
### Shell-user Action identity
-For a Shell-authenticated user calling an Action owned by an independently deployed
-MicroVertical, the Shell resolves the current Better Auth session and issues one short-lived,
-audience-scoped EdDSA assertion. The Shell alone owns the private Ed25519 signing JWK. The
-receiving BFF receives only a public JWKS and independently verifies the signature, protected
-header, issuer, exact topology app ID audience, times, version, subject consistency, and trusted
-principal schema for every request.
-
-The assertion is authentication context, not authorization. It contains only the safe
-`TrustedPrincipalContext` fields and never contains credentials, cookies, session tokens, display
-data, Action keys, permissions, Policy decisions, or business payload. After verification, Core's
-Action runtime still performs the Action-specific SpiceDB permission check and executable Policy
-evaluation. Co-location with the Shell never bypasses this boundary.
-
-Prepare an existing MicroVertical once with
-`mise exec -- pnpm scaffold:microvertical-action-boundary -- --vertical ` before its BFF accepts
-Shell-user Action calls. The generated server verifier and client acquisition adapter embed the
-vertical's authoritative topology app ID. Actions remain independently generated, and adding an
-Action must never require a new Shell endpoint or a hand-maintained audience registry.
+For a Shell-authenticated user calling an Action owned by an independently deployed MicroVertical, the Shell resolves the current Better Auth session and issues one short-lived, audience-scoped EdDSA assertion. The Shell alone owns the private Ed25519 signing JWK. The receiving BFF receives only a public JWKS and independently verifies the signature, protected header, issuer, exact topology app ID audience, times, version, subject consistency, and trusted principal schema for every request.
+
+The assertion is authentication context, not authorization. It contains only the safe `TrustedPrincipalContext` fields and never contains credentials, cookies, session tokens, display data, Action keys, permissions, Policy decisions, or business payload. After verification, Core's Action runtime still performs the Action-specific SpiceDB permission check and executable Policy evaluation. Co-location with the Shell never bypasses this boundary.
+
+Prepare an existing MicroVertical once with `mise exec -- pnpm scaffold:microvertical-action-boundary -- --vertical ` before its BFF accepts Shell-user Action calls. The generated server verifier and client acquisition adapter embed the vertical's authoritative topology app ID. Actions remain independently generated, and adding an Action must never require a new Shell endpoint or a hand-maintained audience registry.
diff --git a/app/docs/architecture/MODULE_ENTRYPOINTS.md b/app/docs/architecture/MODULE_ENTRYPOINTS.md
index 47b6c6ace..3af42af4e 100644
--- a/app/docs/architecture/MODULE_ENTRYPOINTS.md
+++ b/app/docs/architecture/MODULE_ENTRYPOINTS.md
@@ -1,19 +1,12 @@
# Module Entrypoints and Tenant State
-Entrypoint `tenant`/`system` scope is independent from the descriptor's required/optional/forbidden
-legal-entity scope. Both must be explicit. Missing, malformed, denied, or indeterminate operation
-context fails closed before private implementation resolution as defined by
-[Governed Data Access and Operation Scope](./DATA_ACCESS.md).
+Entrypoint `tenant`/`system` scope is independent from the descriptor's required/optional/forbidden legal-entity scope. Both must be explicit. Missing, malformed, denied, or indeterminate operation context fails closed before private implementation resolution as defined by [Governed Data Access and Operation Scope](./DATA_ACCESS.md).
-This document defines the Core-owned invariant for loading or dispatching OntOS Business Module
-entrypoints. It applies to Actions, pages, public components, module APIs, search providers,
-reports, and Outbox Workers. The gate is separate from authentication, SpiceDB authorization, and
-business Policy; passing module state never grants another kind of access.
+This document defines the Core-owned invariant for loading or dispatching OntOS Business Module entrypoints. It applies to Actions, pages, public components, module APIs, search providers, reports, and Outbox Workers. The gate is separate from authentication, SpiceDB authorization, and business Policy; passing module state never grants another kind of access.
## Structured entrypoints
-Every entrypoint is an immutable Effect Schema-backed value containing a stable entrypoint key,
-owning module key, role, access class, and explicit scope.
+Every entrypoint is an immutable Effect Schema-backed value containing a stable entrypoint key, owning module key, role, access class, and explicit scope.
| Role | Permitted access |
| ------------------------------------ | ------------------------------------------------------------ |
@@ -22,15 +15,9 @@ owning module key, role, access class, and explicit scope.
| `page`, `public_component`, `search` | `read` or an explicit `historical_read` |
| `api`, `report` | an explicitly selected `read`, `historical_read`, or `write` |
-Tenant entrypoints are created only with the tenant constructor. Core capabilities use the system
-constructor explicitly; a `core.*` prefix does not imply a bypass. A system entrypoint bypasses
-tenant module-state acquisition only and still passes every applicable authentication,
-permission, Policy, transaction, and evidence control.
+Tenant entrypoints are created only with the tenant constructor. Core capabilities use the system constructor explicitly; a `core.*` prefix does not imply a bypass. A system entrypoint bypasses tenant module-state acquisition only and still passes every applicable authentication, permission, Policy, transaction, and evidence control.
-Descriptors and private implementations are different surfaces. Public descriptors may be
-imported through approved package exports. Private handlers, routes, Worker registrations,
-search/report implementations, and vertical tables remain owner-local. A gateway accepts a
-deferred Effect or loader thunk; it never receives an eagerly resolved private implementation.
+Descriptors and private implementations are different surfaces. Public descriptors may be imported through approved package exports. Private handlers, routes, Worker registrations, search/report implementations, and vertical tables remain owner-local. A gateway accepts a deferred Effect or loader thunk; it never receives an eagerly resolved private implementation.
## Authoritative matrix
@@ -45,24 +32,13 @@ deferred Effect or loader thunk; it never receives an eagerly resolved private i
| `archived` | deny | allow | deny | deny |
| missing row | deny | deny | deny | deny |
-This is the only matrix. Runtime adapters call the Core decision function instead of maintaining
-local state lists. `historical_read` is explicit and never a fallback from a denied normal read.
-Normal Shell navigation includes installed, authorized `active`, `read_only`, and `deprecated`
-modules. The latter two remain readable and visibly non-writable. Definite permission denial omits
-normal navigation; authorization uncertainty preserves an otherwise eligible item as disabled.
+This is the only matrix. Runtime adapters call the Core decision function instead of maintaining local state lists. `historical_read` is explicit and never a fallback from a denied normal read. Normal Shell navigation includes installed, authorized `active`, `read_only`, and `deprecated` modules. The latter two remain readable and visibly non-writable. Definite permission denial omits normal navigation; authorization uncertainty preserves an otherwise eligible item as disabled.
-Missing state is a definite denial. An unavailable database read, malformed persisted state,
-undeclared snapshot key, absent trusted tenant context, or other indeterminate check is a
-sanitized typed unavailable failure. Core is transport-neutral. Public BFFs normally map definite
-denial to declared `403` Problem Details and check unavailability to a retryable declared `503`.
+Missing state is a definite denial. An unavailable database read, malformed persisted state, undeclared snapshot key, absent trusted tenant context, or other indeterminate check is a sanitized typed unavailable failure. Core is transport-neutral. Public BFFs normally map definite denial to declared `403` Problem Details and check unavailability to a retryable declared `503`.
## Request snapshots and query budget
-At a trusted Shell, SSR, route, or BFF boundary, collect every descriptor the request may use,
-deduplicate and sort its tenant module keys, read them in one indexed query, decode each state once,
-and create an immutable request snapshot covering the exact key set. Every later decision is pure
-in-memory evaluation. Undeclared keys fail closed without an implicit lookup. Empty and system-only
-compositions perform no state query.
+At a trusted Shell, SSR, route, or BFF boundary, collect every descriptor the request may use, deduplicate and sort its tenant module keys, read them in one indexed query, decode each state once, and create an immutable request snapshot covering the exact key set. Every later decision is pure in-memory evaluation. Undeclared keys fail closed without an implicit lookup. Empty and system-only compositions perform no state query.
| Runtime composition | Module-state database work |
| ---------------------------------------------------------------------- | ------------------------------------------------------------ |
@@ -73,86 +49,32 @@ compositions perform no state query.
| One business Action attempt | One early indexed read plus one transaction-aware recheck |
| One Outbox Worker claim cycle | Zero additional queries beyond the existing claim query/join |
-Snapshots are request-scoped, never process-global, browser-authoritative, TTL-based, or
-distributed caches. The next independent request observes state again. A Shell decision does not
-replace the independent BFF or Action check at the next trust boundary. Telemetry may contain batch
-size, acquisition duration, snapshot reuse, scope, access, and outcome, but not payloads,
-credentials, raw persistence causes, or private implementation identifiers.
+Snapshots are request-scoped, never process-global, browser-authoritative, TTL-based, or distributed caches. The next independent request observes state again. A Shell decision does not replace the independent BFF or Action check at the next trust boundary. Telemetry may contain batch size, acquisition duration, snapshot reuse, scope, access, and outcome, but not payloads, credentials, raw persistence causes, or private implementation identifiers.
## Runtime ordering
-Actions validate payload and trusted context, acquire/check state, and only then hash the request,
-create an invocation, call SpiceDB, evaluate Policies, or resolve the handler. A business Action
-rechecks `write` under the Core transaction after locking its invocation and tenant and before
-collector creation or handler resolution. A pre-invocation denial creates no evidence. A locked
-recheck failure rolls back and leaves the invocation open for existing retry semantics. The
-explicit system entrypoint for `core.modules.change-tenant-module-state` remains recoverable.
-
-Outbox claim eligibility evaluates the consumer with `background` semantics inside the existing
-atomic claim query. Producer state never authorizes a consumer. Ineligible or missing state leaves
-delivery pending with no claim or attempt. Private Worker handler resolution follows a successful
-eligible claim.
-
-The active immutable Application Composition revision is the runtime authority for which module and
-Shell contributions may be resolved. One browser document remains pinned to one revision. Tenant
-state may disable or revoke a module immediately, but it never selects another artifact and the
-runtime never hot-swaps an already loaded container.
-
-Page/public-component composition uses the approved Shell lazy adapter: collect the full descriptor
-set, prepare one snapshot, evaluate every load, then call loader thunks. Raw `loadRemote(...)`
-strings, eager remote imports, remote SSR inside Shell/Core, and one state request per component are
-forbidden. The current generated lazy registry is a compatibility bridge until the composition
-loader is integrated. Module APIs need verified trusted tenant context and the server gateway;
-write APIs delegate to registered Actions.
-
-The Shell first establishes exactly one trusted tenant and active legal entity. Composition then
-uses one tenant-state batch and one module-permission batch. Every direct target independently
-rechecks installation/reference, selected context, lifecycle, and permission before a generated
-lazy registry is consulted. Only a `resolved` outcome may execute a remote thunk.
-
-Generated exact page routes may use safe canonical named-parameter templates such as
-`/contacts/customers/:id/edit`; their owner and Shell filesystem routes use `[id]`. Dynamic templates are
-not normal navigation items. The generated connector selects only its declared parameter names and
-bounds each string value before calling the generic page loader. The generic loader keeps that plain
-record separate from the resolved target and never adds it to the module contract, target-resolution
-BFF input, trusted principal context, tenant/legal-entity context, module-state gate, or permission
-decision. Only after authentication, legal-entity selection, exact page resolution, lifecycle and
-permission success, approved lazy-client lookup, and successful remote loading may the Shell pass
-the record to the owner component. The owner treats every value as untrusted business input and
-validates it again before any domain read or Action.
-
-Search filters safe providers through the same context/state/module checks, then bulk-filters
-ResourceRefs by resource permission. Core repeats result-level resource authorization before a
-generated provider response can leave the receiving BFF. Zero providers/results is successful,
-mixed provider success is partial `200`, and total provider failure is retryable. Resource detail
-and timeline providers run only after catalog/type/state/module/resource gates and a fresh
-audience-scoped assertion for each attempt. Media attachment remains unavailable even when declared
-in a manifest until Codesmith generates and registers its Action; no provider mutation callback is
-part of the read gateway.
+Actions validate payload and trusted context, acquire/check state, and only then hash the request, create an invocation, call SpiceDB, evaluate Policies, or resolve the handler. A business Action rechecks `write` under the Core transaction after locking its invocation and tenant and before collector creation or handler resolution. A pre-invocation denial creates no evidence. A locked recheck failure rolls back and leaves the invocation open for existing retry semantics. The explicit system entrypoint for `core.modules.change-tenant-module-state` remains recoverable.
+
+Outbox claim eligibility evaluates the consumer with `background` semantics inside the existing atomic claim query. Producer state never authorizes a consumer. Ineligible or missing state leaves delivery pending with no claim or attempt. Private Worker handler resolution follows a successful eligible claim.
+
+The active immutable Application Composition revision is the runtime authority for which module and Shell contributions may be resolved. One browser document remains pinned to one revision. Tenant state may disable or revoke a module immediately, but it never selects another artifact and the runtime never hot-swaps an already loaded container.
+
+Page/public-component composition uses the approved Shell lazy adapter: collect the full descriptor set, prepare one snapshot, evaluate every load, then call loader thunks. Raw `loadRemote(...)` strings, eager remote imports, remote SSR inside Shell/Core, and one state request per component are forbidden. The current generated lazy registry is a compatibility bridge until the composition loader is integrated. Module APIs need verified trusted tenant context and the server gateway; write APIs delegate to registered Actions.
+
+The Shell first establishes exactly one trusted tenant and active legal entity. Composition then uses one tenant-state batch and one module-permission batch. Every direct target independently rechecks installation/reference, selected context, lifecycle, and permission before a generated lazy registry is consulted. Only a `resolved` outcome may execute a remote thunk.
+
+Generated exact page routes may use safe canonical named-parameter templates such as `/contacts/customers/:id/edit`; their owner and Shell filesystem routes use `[id]`. Dynamic templates are not normal navigation items. The generated connector selects only its declared parameter names and bounds each string value before calling the generic page loader. The generic loader keeps that plain record separate from the resolved target and never adds it to the module contract, target-resolution BFF input, trusted principal context, tenant/legal-entity context, module-state gate, or permission decision. Only after authentication, legal-entity selection, exact page resolution, lifecycle and permission success, approved lazy-client lookup, and successful remote loading may the Shell pass the record to the owner component. The owner treats every value as untrusted business input and validates it again before any domain read or Action.
+
+Search filters safe providers through the same context/state/module checks, then bulk-filters ResourceRefs by resource permission. Core repeats result-level resource authorization before a generated provider response can leave the receiving BFF. Zero providers/results is successful, mixed provider success is partial `200`, and total provider failure is retryable. Resource detail and timeline providers run only after catalog/type/state/module/resource gates and a fresh audience-scoped assertion for each attempt. Media attachment remains unavailable even when declared in a manifest until Codesmith generates and registers its Action; no provider mutation callback is part of the read gateway.
## Generator and registration enforcement
-Codesmith output is the starting point for Actions, MicroVertical pages, and Workers and includes
-the governed descriptor. Worker catalogs and route manifests preserve it. Vertical Runtime
-Registration reserves private lazy bindings for public components, search, and reports beside
-direct typed public descriptors.
+Codesmith output is the starting point for Actions, MicroVertical pages, and Workers and includes the governed descriptor. Worker catalogs and route manifests preserve it. Vertical Runtime Registration reserves private lazy bindings for public components, search, and reports beside direct typed public descriptors.
-API, public-component, search, and report business artifacts may not be introduced until an
-approved generator can patch registration atomically and an approved gateway adapter exists.
-Extend Codesmith first with disposable compile, overwrite, traversal, and no-partial-write tests.
-Repository checks reject missing/mismatched registration, raw remote loads, private cross-vertical
-imports, direct private handler access, and public exports of private implementations.
+API, public-component, search, and report business artifacts may not be introduced until an approved generator can patch registration atomically and an approved gateway adapter exists. Extend Codesmith first with disposable compile, overwrite, traversal, and no-partial-write tests. Repository checks reject missing/mismatched registration, raw remote loads, private cross-vertical imports, direct private handler access, and public exports of private implementations.
## Authorization classification and inventory
-Every descriptor must declare exactly one authorization classification. `public` is an intentional
-authorization result, not the route-discovery `public` or `indexable` flag. Protected descriptors
-use `authenticated_principal`, `context_permission` with a stable permission, `action_execution`
-with a provisioning intent, `owner_local_background`, or API-only `capability_issuance` with its
-credential kind. Role-incompatible and excess fields fail decoding and generation.
-
-`pnpm authorization:inventory:check` is the single repository derivation pass. It reconciles
-generated route metadata with runtime descriptors, current Action registrations, Outbox workers,
-and both Shell gateway issuers, then writes the non-secret deterministic artifact at
-`.codex/reports/authorization/protected-entrypoints.json`. Duplicate, missing, stale, ambiguous,
-or unclassified entries fail the check.
+Every descriptor must declare exactly one authorization classification. `public` is an intentional authorization result, not the route-discovery `public` or `indexable` flag. Protected descriptors use `authenticated_principal`, `context_permission` with a stable permission, `action_execution` with a provisioning intent, `owner_local_background`, or API-only `capability_issuance` with its credential kind. Role-incompatible and excess fields fail decoding and generation.
+
+`pnpm authorization:inventory:check` is the single repository derivation pass. It reconciles generated route metadata with runtime descriptors, current Action registrations, Outbox workers, and both Shell gateway issuers, then writes the non-secret deterministic artifact at `.codex/reports/authorization/protected-entrypoints.json`. Duplicate, missing, stale, ambiguous, or unclassified entries fail the check.
diff --git a/app/docs/architecture/MODULE_MANIFESTS.md b/app/docs/architecture/MODULE_MANIFESTS.md
index 930851aa6..2ca6369ec 100644
--- a/app/docs/architecture/MODULE_MANIFESTS.md
+++ b/app/docs/architecture/MODULE_MANIFESTS.md
@@ -1,146 +1,65 @@
# OntOS Module Manifests
-An OntOS Module Manifest is a validated capability contract. It is data, not an executable plugin
-or an UltraModern deployment inventory. A V0 MicroVertical deployment currently admits exactly one
-`business_module`. The schema vocabulary reserves `foundational_module` and `system_module`, but
-current authored-manifest validation does not deploy them through this MicroVertical path.
+An OntOS Module Manifest is a validated capability contract. It is data, not an executable plugin or an UltraModern deployment inventory. A V0 MicroVertical deployment currently admits exactly one `business_module`. The schema vocabulary reserves `foundational_module` and `system_module`, but current authored-manifest validation does not deploy them through this MicroVertical path.
## Identity
-- `appId` is the hyphenated UltraModern topology identity of a deployment. It remains the Module
- Federation remote identity, deployment lookup key, and exact Shell gateway JWT audience.
-- `moduleId` is the stable dotted OntOS capability identity. It owns Actions, Policies, resources,
- events, Outbox producers and consumers, and `core.tenant_module_states.module_key`.
-- Target `implementationId` is the stable explicit identity of one catalogued executable
- implementation of a `moduleId`, for example `standard` or `akros`. Compatible alternatives may
- share a `moduleId`; different public semantics require a different `moduleId`.
+- `appId` is the hyphenated UltraModern topology identity of a deployment. It remains the Module Federation remote identity, deployment lookup key, and exact Shell gateway JWT audience.
+- `moduleId` is the stable dotted OntOS capability identity. It owns Actions, Policies, resources, events, Outbox producers and consumers, and `core.tenant_module_states.module_key`.
+- Target `implementationId` is the stable explicit identity of one catalogued executable implementation of a `moduleId`, for example `standard` or `akros`. Compatible alternatives may share a `moduleId`; different public semantics require a different `moduleId`.
- These identities may happen to contain equal text, but their roles never become interchangeable.
For example, deployment `property-registry` may publish module `property.registry`.
-The `implementationId` split is an accepted target contract from ADR-0017, not a claim about the
-current schema. The current V0 manifest/catalog still admits one implementation per `moduleId` and
-does not yet serialize or select `implementationId`. Until Codesmith, Effect Schemas, topology,
-catalog validation, Customer Configuration, and tests are extended together, treat the sole
-implementation as implicit `standard`; do not hand-add an unvalidated field or simulate selection
-with customer branches, environment flags, duplicate `moduleId` values, or allowlist aliases.
+The `implementationId` split is an accepted target contract from ADR-0017, not a claim about the current schema. The current V0 manifest/catalog still admits one implementation per `moduleId` and does not yet serialize or select `implementationId`. Until Codesmith, Effect Schemas, topology, catalog validation, Customer Configuration, and tests are extended together, treat the sole implementation as implicit `standard`; do not hand-add an unvalidated field or simulate selection with customer branches, environment flags, duplicate `moduleId` values, or allowlist aliases.
## Five layers
-1. Generated topology and an environment overlay enumerate deployable services and their delivery
- metadata. Topology is delivery inventory, not runtime composition authority.
-2. The owner-authored `vertical.manifest.ts` contains an Effect Schema-validated value referencing
- real typed Actions, Effect API values, Module Federation component values, payload Schemas, and
- plain public descriptors.
-3. The owning build emits a deterministic versioned JSON deployment contract, including only safe
- semantic Shell contribution bindings.
-4. A governed Application Composition revision pins the approved deployment contract and Module
- Federation manifest for each installed module. Candidate validation rejects cross-module
- contradictions before explicit promotion; a reachable service cannot install or promote itself.
-5. `vertical.registration.ts` binds private executable Actions, pages, components, APIs, search,
- reports, and workers for the owning process. Only safe descriptors may be projected into the
- deployment contract.
-
-Tenant activation is separate from installation. Application Composition installs an approved
-capability; tenant module state decides whether that installed module is active for a tenant and
-never selects an artifact version.
+1. Generated topology and an environment overlay enumerate deployable services and their delivery metadata. Topology is delivery inventory, not runtime composition authority.
+2. The owner-authored `vertical.manifest.ts` contains an Effect Schema-validated value referencing real typed Actions, Effect API values, Module Federation component values, payload Schemas, and plain public descriptors.
+3. The owning build emits a deterministic versioned JSON deployment contract, including only safe semantic Shell contribution bindings.
+4. A governed Application Composition revision pins the approved deployment contract and Module Federation manifest for each installed module. Candidate validation rejects cross-module contradictions before explicit promotion; a reachable service cannot install or promote itself.
+5. `vertical.registration.ts` binds private executable Actions, pages, components, APIs, search, reports, and workers for the owning process. Only safe descriptors may be projected into the deployment contract.
+
+Tenant activation is separate from installation. Application Composition installs an approved capability; tenant module state decides whether that installed module is active for a tenant and never selects an artifact version.
## Network and artifact contract
-Every MicroVertical deployment serves the immutable document at
-`/.well-known/ontos-module-manifest.json`. The document uses schema version `2`, media type
-`application/json`, `Cache-Control: no-cache`, a strong build-marker ETag, and is limited to 1 MiB.
-The publisher observes it with a bounded fetch and exact deployment `appId` matching, then supplies
-that evidence to pure candidate validation before promotion. Contract URLs must use HTTPS except
-loopback HTTP during development. Candidate validation requires HTTPS unless the publisher supplies
-`environment: 'development'` in trusted evidence, never in the candidate itself. Omitted environment
-evidence keeps HTTPS required for both deployment contracts and Federation manifests.
-Credentials, fragments, unsafe schemes, and duplicate normalized
-URLs are forbidden. The current Shell loader still uses the generated topology allowlist as a
-compatibility bridge until Application Composition publication and runtime loading are wired in
-follow-up work.
-
-The document may describe identity, activation, public Actions/API/components,
-resources, public events, search, reports, and schema-free Outbox subscriptions. It must never
-contain a function, Effect program, React component, handler, Policy, migration, executable route
-definition, repository, database metadata, source path, import/export specifier, fixture, test,
-secret, or arbitrary private runtime value. The sole routing exception is the normalized
-root-relative `routePath` on a governed Shell page contribution. It identifies that contribution's
-canonical authenticated Shell location; it is not an owner route definition or remote source.
-
-Shell contributions bind stable navigation/page, public-component, API-backed resource detail and
-timeline, search, report, and media targets to descriptors already owned by the same manifest.
-They may contain semantic keys, ordering, grouping metadata, and the page contribution's canonical
-root-relative `routePath`, but never absolute URLs, import specifiers, remote strings, functions,
-schemas, executable routes, or source paths. Candidate promotion validates the complete proposed
-composition and rejects it when one binding is missing, duplicated, cross-owned, or role/access
-incompatible. Runtime discovery then settles each promoted deployment independently: an invalid
-deployment is reported as incompatible without removing unrelated healthy deployments.
+Every MicroVertical deployment serves the immutable document at `/.well-known/ontos-module-manifest.json`. The document uses schema version `2`, media type `application/json`, `Cache-Control: no-cache`, a strong build-marker ETag, and is limited to 1 MiB. The publisher observes it with a bounded fetch and exact deployment `appId` matching, then supplies that evidence to pure candidate validation before promotion. Contract URLs must use HTTPS except loopback HTTP during development. Candidate validation requires HTTPS unless the publisher supplies `environment: 'development'` in trusted evidence, never in the candidate itself. Omitted environment evidence keeps HTTPS required for both deployment contracts and Federation manifests. Credentials, fragments, unsafe schemes, and duplicate normalized URLs are forbidden. The current Shell loader still uses the generated topology allowlist as a compatibility bridge until Application Composition publication and runtime loading are wired in follow-up work.
+
+The document may describe identity, activation, public Actions/API/components, resources, public events, search, reports, and schema-free Outbox subscriptions. It must never contain a function, Effect program, React component, handler, Policy, migration, executable route definition, repository, database metadata, source path, import/export specifier, fixture, test, secret, or arbitrary private runtime value. The sole routing exception is the normalized root-relative `routePath` on a governed Shell page contribution. It identifies that contribution's canonical authenticated Shell location; it is not an owner route definition or remote source.
+
+Shell contributions bind stable navigation/page, public-component, API-backed resource detail and timeline, search, report, and media targets to descriptors already owned by the same manifest. They may contain semantic keys, ordering, grouping metadata, and the page contribution's canonical root-relative `routePath`, but never absolute URLs, import specifiers, remote strings, functions, schemas, executable routes, or source paths. Candidate promotion validates the complete proposed composition and rejects it when one binding is missing, duplicated, cross-owned, or role/access incompatible. Runtime discovery then settles each promoted deployment independently: an invalid deployment is reported as incompatible without removing unrelated healthy deployments.
## Import and execution boundaries
-Shell/Core and ordinary MicroVertical consumers must not statically import another deployment's
-`vertical.manifest.ts`, `vertical.registration.ts`, or private source. Synchronous calls use the
-provider's generated Effect BFF client, public components use generated Module Federation wrappers,
-and asynchronous communication uses published schema-only Outbox contracts. Executable Actions,
-Policies, workers, migrations, routes, repositories, search implementations, and report
-implementations stay owner-local.
-
-The strict candidate catalog rejects unsupported schema versions, deployment/manifest identity
-mismatch, duplicate app or module IDs, mismatched Outbox consumer ownership or entrypoints, and
-duplicate worker keys before promotion. Runtime discovery excludes every claimant involved in a
-global identity or worker-key contradiction while preserving unrelated healthy deployments. Each
-installed deployment remains visible with an authoritative `disabled` or `revoked` state, or a
-typed transient `timeout`, `unavailable`, or `incompatible` diagnostic. Authoritative state takes
-precedence over a stale fetched contract. A degraded runtime result is never cached; discovery is
-retried, and only a fully healthy result is cached for its immutable allowlist/composition revision.
-No persistent last-known-good contract is selected automatically. A subscription may name a
-producer that is not installed; it remains dormant until matching messages can exist.
-
-Required Core capabilities and the Shell contribution ABI are explicit versioned compatibility
-claims in Application Composition. External system readiness and module-owned setup remain private
-implementation concerns and are not generic activation gates.
+Shell/Core and ordinary MicroVertical consumers must not statically import another deployment's `vertical.manifest.ts`, `vertical.registration.ts`, or private source. Synchronous calls use the provider's generated Effect BFF client, public components use generated Module Federation wrappers, and asynchronous communication uses published schema-only Outbox contracts. Executable Actions, Policies, workers, migrations, routes, repositories, search implementations, and report implementations stay owner-local.
+
+The strict candidate catalog rejects unsupported schema versions, deployment/manifest identity mismatch, duplicate app or module IDs, mismatched Outbox consumer ownership or entrypoints, and duplicate worker keys before promotion. Runtime discovery excludes every claimant involved in a global identity or worker-key contradiction while preserving unrelated healthy deployments. Each installed deployment remains visible with an authoritative `disabled` or `revoked` state, or a typed transient `timeout`, `unavailable`, or `incompatible` diagnostic. Authoritative state takes precedence over a stale fetched contract. A degraded runtime result is never cached; discovery is retried, and only a fully healthy result is cached for its immutable allowlist/composition revision. No persistent last-known-good contract is selected automatically. A subscription may name a producer that is not installed; it remains dormant until matching messages can exist.
+
+Required Core capabilities and the Shell contribution ABI are explicit versioned compatibility claims in Application Composition. External system readiness and module-owned setup remain private implementation concerns and are not generic activation gates.
## Application Composition contract
-The provider-neutral Effect Schema and pure candidate validator are defined by
-[ADR-0020](../../../docs/adr/0020-governed-application-composition.md). Publication, promotion, and
-live Shell loading are separate follow-up slices; this contract alone does not change runtime
-loading.
+The provider-neutral Effect Schema and pure candidate validator are defined by [ADR-0020](../../../docs/adr/0020-governed-application-composition.md). Publication, promotion, and live Shell loading are separate follow-up slices; this contract alone does not change runtime loading.
-The validator checks the revision's format; it does not assign or reserve revision identities.
-The publisher in [#374](https://github.com/TechsioCZ/ontos/issues/374) must bind each revision to
-immutable canonical bytes and reject conflicting publication before advancing the active pointer.
+The validator checks the revision's format; it does not assign or reserve revision identities. The publisher in [#374](https://github.com/TechsioCZ/ontos/issues/374) must bind each revision to immutable canonical bytes and reject conflicting publication before advancing the active pointer.
-A continuously delivered Application Composition owns a dependency-closed DAG of Foundational and
-Business Module Contract Identities and their permitted implementations. Core validates that graph
-without learning its business meaning. Installation, activation, and entrypoint execution preserve
-dependency closure: a module activates only when one selected implementation and every required
-dependency are installed, compatible, healthy, and active. Customer Configurations select only
-modules and explicit implementations permitted by the composition.
+A continuously delivered Application Composition owns a dependency-closed DAG of Foundational and Business Module Contract Identities and their permitted implementations. Core validates that graph without learning its business meaning. Installation, activation, and entrypoint execution preserve dependency closure: a module activates only when one selected implementation and every required dependency are installed, compatible, healthy, and active. Customer Configurations select only modules and explicit implementations permitted by the composition.
-Each composition entry carries exact deployment identity, immutable artifact URLs plus digests,
-public-contract identity, allowed Shell contributions, required Core capabilities, Shell ABI, and
-shared-singleton requirements. The composition rejects missing, duplicate, incompatible, or
-unobserved identities before promotion.
+Each composition entry carries exact deployment identity, immutable artifact URLs plus digests, public-contract identity, allowed Shell contributions, required Core capabilities, Shell ABI, and shared-singleton requirements. The composition rejects missing, duplicate, incompatible, or unobserved identities before promotion.
-Dependency enforcement never authorizes private imports, shared repositories, shared business
-transactions, or direct table access. Typed API, public event, and Outbox communication preserves
-the deployment seams. A dependency outage produces an explicit unavailable/degraded result for the
-affected entrypoint without rewriting persisted module states; unrelated modules remain operable.
+Dependency enforcement never authorizes private imports, shared repositories, shared business transactions, or direct table access. Typed API, public event, and Outbox communication preserves the deployment seams. A dependency outage produces an explicit unavailable/degraded result for the affected entrypoint without rewriting persisted module states; unrelated modules remain operable.
## Generator order
-After UltraModern creates a topology-backed vertical, run the module-contract Codesmith generator
-before any business generator:
+After UltraModern creates a topology-backed vertical, run the module-contract Codesmith generator before any business generator:
```bash
mise exec -- pnpm scaffold:module-contract -- --vertical property-registry --module property.registry
```
-All later business generators read the generated module-ID marker and patch only explicit
-generator-owned slots. They fail without a consistent package, topology entry, manifest, and private
-registration.
+All later business generators read the generated module-ID marker and patch only explicit generator-owned slots. They fail without a consistent package, topology entry, manifest, and private registration.
Use the category generator before authoring each supported public artifact:
@@ -152,32 +71,12 @@ mise exec -- pnpm scaffold:search-provider -- --vertical property-registry --nam
mise exec -- pnpm scaffold:report -- --vertical property-registry --name unit-inventory --resource unit --authorization context_permission --permission resource.read
```
-The page name is a stable lower-kebab identity, while `--url` is an optional complete
-root-relative canonical-path override. When `--url` is omitted, Codesmith derives
-`//` from the validated MicroVertical slug. For example,
-`--vertical contacts --page customers` produces canonical `/contacts/customers`; the locale-aware Shell
-router exposes it as `/cs/contacts/customers` or `/en/contacts/customers`. Never include a locale prefix in
-`--url`. A generated page is private and non-indexable, contains only its localized title, and is
-loaded only after the authenticated Shell/Core gateway resolves that exact governed page
-entrypoint. Private metadata alone is not an authentication mechanism.
-
-An explicit page URL may mix lowercase kebab-case static segments with unique named parameter
-segments such as `/contacts/customers/:id/edit`. A parameter name starts with a lowercase letter and
-continues with letters or digits. Optional, repeated, wildcard, catch-all, encoded, query, fragment,
-origin, empty, dot, trailing-slash, and locale-prefixed forms are invalid. The serialized `routePath`
-retains the canonical `:id` spelling as bounded plain data; Codesmith maps it deterministically to
-the `[id]` filesystem segment used by both owner and Shell routers. Templates that differ only by a
-parameter name at one position, and static/dynamic siblings, are routing collisions.
-
-Dynamic page contributions remain in `pages`, component ownership, private registration, Module
-Federation exposure, and the generated Shell lazy-client allowlist. They do not create ordinary
-`navigation` contributions because a route template is not a usable destination. The manifest never
-contains route values, loader functions, imports, private source paths, or executable matching code.
+The page name is a stable lower-kebab identity, while `--url` is an optional complete root-relative canonical-path override. When `--url` is omitted, Codesmith derives `//` from the validated MicroVertical slug. For example, `--vertical contacts --page customers` produces canonical `/contacts/customers`; the locale-aware Shell router exposes it as `/cs/contacts/customers` or `/en/contacts/customers`. Never include a locale prefix in `--url`. A generated page is private and non-indexable, contains only its localized title, and is loaded only after the authenticated Shell/Core gateway resolves that exact governed page entrypoint. Private metadata alone is not an authentication mechanism.
+
+An explicit page URL may mix lowercase kebab-case static segments with unique named parameter segments such as `/contacts/customers/:id/edit`. A parameter name starts with a lowercase letter and continues with letters or digits. Optional, repeated, wildcard, catch-all, encoded, query, fragment, origin, empty, dot, trailing-slash, and locale-prefixed forms are invalid. The serialized `routePath` retains the canonical `:id` spelling as bounded plain data; Codesmith maps it deterministically to the `[id]` filesystem segment used by both owner and Shell routers. Templates that differ only by a parameter name at one position, and static/dynamic siblings, are routing collisions.
+
+Dynamic page contributions remain in `pages`, component ownership, private registration, Module Federation exposure, and the generated Shell lazy-client allowlist. They do not create ordinary `navigation` contributions because a route template is not a usable destination. The manifest never contains route values, loader functions, imports, private source paths, or executable matching code.
## Documentation authority
-Repository-level product semantics and the app-local implementation contract agree that OntOS
-Business Modules preserve independently deployable MicroVertical seams. Proposed historical ADRs
-remain decision history and do not override this app-local implementation rule. If future product
-vocabulary and implementation guidance diverge, update both explicitly rather than silently
-selecting one generation.
+Repository-level product semantics and the app-local implementation contract agree that OntOS Business Modules preserve independently deployable MicroVertical seams. Proposed historical ADRs remain decision history and do not override this app-local implementation rule. If future product vocabulary and implementation guidance diverge, update both explicitly rather than silently selecting one generation.
diff --git a/app/docs/architecture/OUTBOX_WORKERS.md b/app/docs/architecture/OUTBOX_WORKERS.md
index 8a2ecafdf..7592b0a30 100644
--- a/app/docs/architecture/OUTBOX_WORKERS.md
+++ b/app/docs/architecture/OUTBOX_WORKERS.md
@@ -4,56 +4,28 @@ Outbox Workers are module-owned asynchronous entrypoints for committed Outbox Me
## Process and dependency ownership
-Each consuming MicroVertical runs its workers in a dedicated Node process. The process imports only
-that MicroVertical's generated server-side registry, while Core supplies the generic polling and
-delivery runtime. Worker code therefore stays physically inside its owner and may require the
-owner's Effect repositories and services. The owner composes those requirements in its worker
-layer, using the same server-side capabilities available to its Actions; Core never imports or
-publishes the private implementations.
-
-Matching uses the complete schema-free subscription snapshot from the validated installed-module
-deployment catalog. Core's matcher receives that complete snapshot explicitly and creates
-deliveries before independently deployed owner processes claim them. A worker process holds only
-its own private registrations and must prove they match its deployment descriptors. No generator
-scans unrelated vertical source or rewrites a shared source-time subscription registry. This split
-prevents the first polling process from marking a message with only its local handlers and starving
-other independently hosted consumers.
-
-`scaffold:outbox-worker -- --authorization owner_local_background` creates the owner-local process host and adds `dev:worker` and
-`worker:start` scripts to the consumer package when needed. Run one consumer with
-`mise exec -- pnpm --filter @app/ worker:start`; the normal `mise exec -- pnpm dev`
-command starts every generated worker host alongside the applications. Each process performs one
-cycle immediately and then polls every 1,000 ms. These optional scalar environment values may
-override the safe defaults for a deployment:
+Each consuming MicroVertical runs its workers in a dedicated Node process. The process imports only that MicroVertical's generated server-side registry, while Core supplies the generic polling and delivery runtime. Worker code therefore stays physically inside its owner and may require the owner's Effect repositories and services. The owner composes those requirements in its worker layer, using the same server-side capabilities available to its Actions; Core never imports or publishes the private implementations.
+
+Matching uses the complete schema-free subscription snapshot from the validated installed-module deployment catalog. Core's matcher receives that complete snapshot explicitly and creates deliveries before independently deployed owner processes claim them. A worker process holds only its own private registrations and must prove they match its deployment descriptors. No generator scans unrelated vertical source or rewrites a shared source-time subscription registry. This split prevents the first polling process from marking a message with only its local handlers and starving other independently hosted consumers.
+
+`scaffold:outbox-worker -- --authorization owner_local_background` creates the owner-local process host and adds `dev:worker` and `worker:start` scripts to the consumer package when needed. Run one consumer with `mise exec -- pnpm --filter @app/ worker:start`; the normal `mise exec -- pnpm dev` command starts every generated worker host alongside the applications. Each process performs one cycle immediately and then polls every 1,000 ms. These optional scalar environment values may override the safe defaults for a deployment:
- `OUTBOX_WORKER_POLL_INTERVAL_MS` — interval from 10 through 3,600,000 ms; default `1000`.
-- `OUTBOX_WORKER_MAX_DELIVERIES` — maximum deliveries claimed per cycle from 1 through 1,000;
- default `100`.
-- `OUTBOX_WORKER_CLAIM_OWNER` — stable process identity up to 200 characters; the process derives
- one from the MicroVertical, process ID, and a random process nonce by default.
+- `OUTBOX_WORKER_MAX_DELIVERIES` — maximum deliveries claimed per cycle from 1 through 1,000; default `100`.
+- `OUTBOX_WORKER_CLAIM_OWNER` — stable process identity up to 200 characters; the process derives one from the MicroVertical, process ID, and a random process nonce by default.
-Invalid values fail process startup instead of silently selecting an unsafe cadence. `SIGINT` and
-`SIGTERM` interrupt the polling fiber and release the scoped PostgreSQL pool. Multiple instances
-are safe because matching is idempotent and delivery claiming is lease-protected; handler effects
-remain at-least-once and must still be idempotent.
+Invalid values fail process startup instead of silently selecting an unsafe cadence. `SIGINT` and `SIGTERM` interrupt the polling fiber and release the scoped PostgreSQL pool. Multiple instances are safe because matching is idempotent and delivery claiming is lease-protected; handler effects remain at-least-once and must still be idempotent.
## Published Contract Boundary
- A producer publishes one schema-only package subpath per exact topic. It contains the Effect payload schema, producer module key, and topic constant—never an Action, factory, repository, handler, transport, database client, or BFF implementation.
-- Producer, consumer, and worker ownership use dotted OntOS module IDs. Deployment app IDs are not
- Outbox business identities.
+- Producer, consumer, and worker ownership use dotted OntOS module IDs. Deployment app IDs are not Outbox business identities.
- A consumer imports that published subpath and its own Core descriptor API. It never deep-imports another MicroVertical's source or executes another MicroVertical's implementation.
- Generate producer messages with `mise exec -- pnpm scaffold:outbox-message` and consumers with `mise exec -- pnpm scaffold:outbox-worker -- --authorization owner_local_background`. Generated worker registries stay server-side and are not Module Federation or BFF surfaces.
## Immutable Matching
-An Outbox Message is an immutable broadcast source linked to one committed Domain Event. At first
-observation, Core matches the message against the complete installed subscription catalog by exact
-producer module and exact topic. In one transaction it creates at most one delivery per message
-and worker and sets `matched_at`, including when no workers match. Re-observation is idempotent.
-Deploying a new worker does not backfill already matched messages in V0. Each process also verifies
-that every owner-local registration has an identical catalog entry before it can match or claim
-work.
+An Outbox Message is an immutable broadcast source linked to one committed Domain Event. At first observation, Core matches the message against the complete installed subscription catalog by exact producer module and exact topic. In one transaction it creates at most one delivery per message and worker and sets `matched_at`, including when no workers match. Re-observation is idempotent. Deploying a new worker does not backfill already matched messages in V0. Each process also verifies that every owner-local registration has an identical catalog entry before it can match or claim work.
Each Worker declares a structured tenant `worker`/`background` entrypoint governed by [Module Entrypoints and Tenant State](./MODULE_ENTRYPOINTS.md). Claim eligibility uses the central matrix inside the existing atomic claim query. Only `active` consumers are eligible; every other or missing state leaves work unattempted and retryable. The producer's current module state never authorizes the consumer entrypoint, and handler resolution occurs only after an eligible claim.
@@ -81,8 +53,4 @@ Checkpoint advancement must not skip an earlier matching delivery in `pending`,
## Authorization inventory
-Generated workers must declare `owner_local_background`; no other authorization class is valid for
-the `worker` role. The inventory checker reconciles the registered worker, its owner deployment,
-and its generated descriptor. Worker execution remains independently gated by the active tenant
-module state and exact owner-local registration. Report-only rollout never turns a missing owner,
-disabled module, unavailable state check, or foreign deployment into an allow.
+Generated workers must declare `owner_local_background`; no other authorization class is valid for the `worker` role. The inventory checker reconciles the registered worker, its owner deployment, and its generated descriptor. Worker execution remains independently gated by the active tenant module state and exact owner-local registration. Report-only rollout never turns a missing owner, disabled module, unavailable state check, or foreign deployment into an allow.
diff --git a/app/docs/architecture/PARTY_REGISTRY.md b/app/docs/architecture/PARTY_REGISTRY.md
index c74b20a9d..b02799333 100644
--- a/app/docs/architecture/PARTY_REGISTRY.md
+++ b/app/docs/architecture/PARTY_REGISTRY.md
@@ -1,9 +1,6 @@
# Party Registry
-This document defines current implementation rules for the `party.registry` Foundational Module. The
-durable decision is [ADR-0018](../../../docs/adr/0018-party-registry-operational-boundaries.md).
-General Action, governed Read, database, ResourceRef, event, outbox, authorization, and
-MicroVertical rules still apply.
+This document defines current implementation rules for the `party.registry` Foundational Module. The durable decision is [ADR-0018](../../../docs/adr/0018-party-registry-operational-boundaries.md). General Action, governed Read, database, ResourceRef, event, outbox, authorization, and MicroVertical rules still apply.
## Ownership
@@ -49,11 +46,9 @@ party.registry/
└── PartyAlias
```
-Every ResourceRef carries Tenant, module identity, resource type, and resource identity. Public
-contracts never accept a raw identifier when a ResourceRef is required.
+Every ResourceRef carries Tenant, module identity, resource type, and resource identity. Public contracts never accept a raw identifier when a ResourceRef is required.
-`PartyRef` and `LegalEntityRef` are different types. A handler must not construct one from the other.
-A public contract that can refer to either uses a tagged union:
+`PartyRef` and `LegalEntityRef` are different types. A handler must not construct one from the other. A public contract that can refer to either uses a tagged union:
```ts
type OrganizationSubjectRef =
@@ -61,13 +56,11 @@ type OrganizationSubjectRef =
| { readonly kind: 'legal_entity'; readonly legalEntity: LegalEntityRef };
```
-Do not add this union to a contract that only needs one side. Counterparty always uses a PartyRef and
-a LegalEntityRef explicitly.
+Do not add this union to a contract that only needs one side. Counterparty always uses a PartyRef and a LegalEntityRef explicitly.
## Action and Read scope
-All state changes use declared Actions and require idempotency unless the general Action rules
-explicitly justify otherwise.
+All state changes use declared Actions and require idempotency unless the general Action rules explicitly justify otherwise.
| Capability | Legal Entity scope | Permission target | Required authority |
| ------------------------------------- | ------------------ | ---------------------------- | ----------------------------------- |
@@ -81,19 +74,13 @@ explicitly justify otherwise.
| Counterparty create/read/search | required | Legal Entity or Counterparty | read/manage that commercial context |
| Counterparty Role add/end | required | Counterparty | manage that commercial context |
-`legalEntityScope: optional` means trusted session context may contain a selected Legal Entity. It
-does not scope the Party fact or grant authority. The Action payload never supplies or overrides
-trusted Tenant or Legal Entity context.
+`legalEntityScope: optional` means trusted session context may contain a selected Legal Entity. It does not scope the Party fact or grant authority. The Action payload never supplies or overrides trusted Tenant or Legal Entity context.
-A caller authorized only for one Legal Entity does not receive tenant-wide Party Search. It reaches a
-Party through an authorized Counterparty Read and receives the explicitly declared minimum Party
-projection required by that contract. Adding a field to that projection is an authorization and
-privacy change, not a serializer convenience.
+A caller authorized only for one Legal Entity does not receive tenant-wide Party Search. It reaches a Party through an authorized Counterparty Read and receives the explicitly declared minimum Party projection required by that contract. Adding a field to that projection is an authorization and privacy change, not a serializer convenience.
## Canonical persistence
-Use owner-local PostgreSQL tables. No Party invariant depends on Core Search, Neo4j, a cache, or a
-consumer database.
+Use owner-local PostgreSQL tables. No Party invariant depends on Core Search, Neo4j, a cache, or a consumer database.
The initial logical table set is:
@@ -112,12 +99,9 @@ party.party_merges
party.party_aliases
```
-Exact names may follow the repository's generated naming rules. The semantic separation is
-required even when an implementation co-locates supporting records.
+Exact names may follow the repository's generated naming rules. The semantic separation is required even when an implementation co-locates supporting records.
-Every tenant-owned table has an explicit Tenant column, a tenant-qualified unique key for its
-Resource identity, enabled and forced RLS, owner-local foreign keys, and no cross-MicroVertical
-foreign key.
+Every tenant-owned table has an explicit Tenant column, a tenant-qualified unique key for its Resource identity, enabled and forced RLS, owner-local foreign keys, and no cross-MicroVertical foreign key.
Required uniqueness invariants include:
@@ -130,14 +114,11 @@ Strong identifier claim unique (tenant_id, identifier_type_key, namespace, no
Current preferred contact type/purpose-specific partial uniqueness where the type allows one
```
-A `party_identifier_claims` row exists only when the Identifier Type and verification/provenance
-state permit an exclusive identity claim. An unverified or non-exclusive identifier assertion may
-exist without a claim and cannot create an automatic MATCHED outcome.
+A `party_identifier_claims` row exists only when the Identifier Type and verification/provenance state permit an exclusive identity claim. An unverified or non-exclusive identifier assertion may exist without a claim and cannot create an automatic MATCHED outcome.
## Party Candidate
-A Party Candidate is an immutable request snapshot used before an existing or new Party is chosen.
-It may contain:
+A Party Candidate is an immutable request snapshot used before an existing or new Party is chosen. It may contain:
- asserted Party Type or UNRESOLVED;
- names or labels with provenance;
@@ -147,12 +128,9 @@ It may contain:
- Evidence Artifact references;
- caller intent and policy version.
-A Party Candidate is not a Party and has no Party ID. A Source Record Reference identifies a record
-inside one External Business System or migration dataset. It is neither an Official Identifier nor
-evidence that a new real-world subject exists.
+A Party Candidate is not a Party and has no Party ID. A Source Record Reference identifies a record inside one External Business System or migration dataset. It is neither an Official Identifier nor evidence that a new real-world subject exists.
-When matching is ambiguous, the Duplicate Candidate case stores the canonical decoded Candidate
-snapshot and the evaluated evidence. It does not retain raw secrets or unbounded provider payloads.
+When matching is ambiguous, the Duplicate Candidate case stores the canonical decoded Candidate snapshot and the evaluated evidence. It does not retain raw secrets or unbounded provider payloads.
## Atomic Party create
@@ -202,21 +180,11 @@ PartyCreate(candidate)
Domain Events, linked Outbox Messages, and invocation success atomically
```
-CoreSDK opens the one canonical transaction and constructs an owner-local
-`PartyIdentifierClaimService` bound to it. Before reading claims, the service sorts every normalized
-claim key and acquires transaction-scoped database locks in that deterministic order. An equivalent
-conflict-tolerant single-transaction primitive is acceptable only when it provides the same observable
-serialization. The service then reads or attaches claims without exposing a database executor and
-without allowing the handler to begin, commit, roll back, or retry a transaction.
+CoreSDK opens the one canonical transaction and constructs an owner-local `PartyIdentifierClaimService` bound to it. Before reading claims, the service sorts every normalized claim key and acquires transaction-scoped database locks in that deterministic order. An equivalent conflict-tolerant single-transaction primitive is acceptable only when it provides the same observable serialization. The service then reads or attaches claims without exposing a database executor and without allowing the handler to begin, commit, roll back, or retry a transaction.
-A competing Action waits for the same claim-key locks and then observes the committed owner before it
-decides. A uniqueness conflict after those locks indicates a broken invariant, not an instruction for
-the business handler to open a fresh transaction. Database unavailability or an indeterminate commit
-follows the existing Core-owned Action reconciliation lifecycle.
+A competing Action waits for the same claim-key locks and then observes the committed owner before it decides. A uniqueness conflict after those locks indicates a broken invariant, not an instruction for the business handler to open a fresh transaction. Database unavailability or an indeterminate commit follows the existing Core-owned Action reconciliation lifecycle.
-A preflight fuzzy search may improve user experience, but the transaction repeats every invariant
-read against the Party Registry operational store. A result from Core Search is never sufficient to
-create, match, or reject a Party.
+A preflight fuzzy search may improve user experience, but the transaction repeats every invariant read against the Party Registry operational store. A result from Core Search is never sufficient to create, match, or reject a Party.
The create Action result is:
@@ -226,35 +194,17 @@ MATCHED_EXISTING(partyRef, decisionRef)
AMBIGUOUS(caseRef, decisionRef)
```
-Insufficient evidence that the Candidate represents one real-world subject is a typed domain
-rejection and persists no Party Match Decision or Duplicate Candidate case. Identifier conflicts
-that require durable review produce the committed `AMBIGUOUS` result instead of returning an Action
-failure whose transaction would roll back the case.
+Insufficient evidence that the Candidate represents one real-world subject is a typed domain rejection and persists no Party Match Decision or Duplicate Candidate case. Identifier conflicts that require durable review produce the committed `AMBIGUOUS` result instead of returning an Action failure whose transaction would roll back the case.
-`NO_MATCH` is an internal matching result, not proof that an insert will remain safe after the
-transaction begins.
+`NO_MATCH` is an internal matching result, not proof that an insert will remain safe after the transaction begins.
### Commit, publication, and recovery
-`PartyMatchDecision` is the durable result reference for Party Create. It records the Action
-Invocation, Candidate fingerprint, Match Rule version, operation, matching outcome, and exact
-`committedCreateOutcome` for CREATE/REVIEW_CREATE. CREATE records CREATED, MATCHED_EXISTING or
-AMBIGUOUS independently of matching's MATCHED vocabulary. Create has exactly one of `partyRef` or
-`caseRef`; matching-only NO_MATCH has neither. REVIEW_MATCH retains MATCHED. Legacy rows are
-explicitly LEGACY: do not infer whether an old MATCHED row came from Create or matching. The decision commits in the same transaction as the resulting Party or Duplicate
-Candidate case.
-
-No search descriptor, projection update, consumer notification, or external publication occurs
-before commit. The successful Action commits its Party-owned state, Audit and Data Access evidence,
-Domain Events, linked Outbox Messages, Party Match Decision, and invocation success marker
-atomically. Outbox Workers publish projections and integration effects only after that commit.
-
-If the database acknowledgement is indeterminate, the caller uses the standard Action commit
-resolution operation with the Action Invocation identity. A `succeeded` invocation proves commit;
-the caller then performs a governed Party Match Decision Read by Action Invocation or caller
-idempotency identity to recover the same `partyRef`, `caseRef`, and outcome. It never reruns create
-because Party Search did or did not return a result. A repeated request with the same idempotency key
-and request hash must resolve to the same committed decision without executing the handler again.
+`PartyMatchDecision` is the durable result reference for Party Create. It records the Action Invocation, Candidate fingerprint, Match Rule version, operation, matching outcome, and exact `committedCreateOutcome` for CREATE/REVIEW_CREATE. CREATE records CREATED, MATCHED_EXISTING or AMBIGUOUS independently of matching's MATCHED vocabulary. Create has exactly one of `partyRef` or `caseRef`; matching-only NO_MATCH has neither. REVIEW_MATCH retains MATCHED. Legacy rows are explicitly LEGACY: do not infer whether an old MATCHED row came from Create or matching. The decision commits in the same transaction as the resulting Party or Duplicate Candidate case.
+
+No search descriptor, projection update, consumer notification, or external publication occurs before commit. The successful Action commits its Party-owned state, Audit and Data Access evidence, Domain Events, linked Outbox Messages, Party Match Decision, and invocation success marker atomically. Outbox Workers publish projections and integration effects only after that commit.
+
+If the database acknowledgement is indeterminate, the caller uses the standard Action commit resolution operation with the Action Invocation identity. A `succeeded` invocation proves commit; the caller then performs a governed Party Match Decision Read by Action Invocation or caller idempotency identity to recover the same `partyRef`, `caseRef`, and outcome. It never reruns create because Party Search did or did not return a result. A repeated request with the same idempotency key and request hash must resolve to the same committed decision without executing the handler again.
## Party assertion semantics
@@ -279,15 +229,12 @@ Rules:
1. `recordedAt` never substitutes for `validFrom`.
2. Ending a fact does not delete its assertion.
3. Correction retracts or supersedes a wrong assertion; it is not an in-place value overwrite.
-4. A legitimate new real-world value ends the old period and adds a new assertion where the fact
- type is historical.
+4. A legitimate new real-world value ends the old period and adds a new assertion where the fact type is historical.
5. Formal validity, authoritative verification, freshness, and matching strength remain separate.
-6. Raw provider payloads stay with the adapter or Evidence Artifact boundary. Party Registry stores
- bounded normalized evidence and references.
+6. Raw provider payloads stay with the adapter or Evidence Artifact boundary. Party Registry stores bounded normalized evidence and references.
7. A current projection is derived from accepted assertion state and effective time.
-Use the same vocabulary in code, schemas, events, and user-facing audit explanations. Avoid generic
-`updated`, `removed`, or `verified` fields whose exact meaning cannot be determined from the type.
+Use the same vocabulary in code, schemas, events, and user-facing audit explanations. Avoid generic `updated`, `removed`, or `verified` fields whose exact meaning cannot be determined from the type.
## Party Type
@@ -299,28 +246,11 @@ ORGANIZATION
UNRESOLVED
```
-UNRESOLVED means one evidenced real-world subject whose person-versus-organization type is unknown.
-It is not an import staging row, anonymous Principal, missing-name placeholder, or Duplicate
-Candidate case.
-
-Subject eligibility (`party-concrete-subject.v1`) and type support (`party-subject-type.v1`)
-are independent versioned decisions. Every Create and Matching Candidate, type enrichment and type
-Correction must include bounded typed subject evidence. The supported V1 manual boundary is an
-explicit ACTOR_ATTESTATION made through an authorized owner Action: DIRECT_INTERACTION or
-REVIEWED_DOCUMENT, a subject key, evidence reference, statement, and observed subject meaning.
-The accepting Action supplies the authenticated Principal and invocation; caller provenance labels
-never establish a registry authority. A reference only locates supporting material; arbitrary reference
-spelling is allowed. Document/registry records without such an attestation remain unsupported as
-standalone subject proof until their owner provides an authorized resolver. No Evidence Artifact
-service is assumed. ARES prefill itself supplies no manual attestation or authoritative type evidence.
-
-Eligibility requires evidence of exactly one concrete subject. Technical records and managed Legal
-Entities are rejected. PERSON requires an observation of a human, ORGANIZATION of an external
-organization; contradictory observations are rejected. CONCRETE_SUBJECT supports UNRESOLVED only.
-Neither display-name length nor an official identifier establishes existence or type. Evidence
-meaning and both rule versions participate in the Candidate fingerprint and durable evaluation;
-accepted assertions retain the evaluation with the trusted actor. Reviewer selection cannot waive
-these thresholds. Historical cases lacking this evidence require material new evidence.
+UNRESOLVED means one evidenced real-world subject whose person-versus-organization type is unknown. It is not an import staging row, anonymous Principal, missing-name placeholder, or Duplicate Candidate case.
+
+Subject eligibility (`party-concrete-subject.v1`) and type support (`party-subject-type.v1`) are independent versioned decisions. Every Create and Matching Candidate, type enrichment and type Correction must include bounded typed subject evidence. The supported V1 manual boundary is an explicit ACTOR_ATTESTATION made through an authorized owner Action: DIRECT_INTERACTION or REVIEWED_DOCUMENT, a subject key, evidence reference, statement, and observed subject meaning. The accepting Action supplies the authenticated Principal and invocation; caller provenance labels never establish a registry authority. A reference only locates supporting material; arbitrary reference spelling is allowed. Document/registry records without such an attestation remain unsupported as standalone subject proof until their owner provides an authorized resolver. No Evidence Artifact service is assumed. ARES prefill itself supplies no manual attestation or authoritative type evidence.
+
+Eligibility requires evidence of exactly one concrete subject. Technical records and managed Legal Entities are rejected. PERSON requires an observation of a human, ORGANIZATION of an external organization; contradictory observations are rejected. CONCRETE_SUBJECT supports UNRESOLVED only. Neither display-name length nor an official identifier establishes existence or type. Evidence meaning and both rule versions participate in the Candidate fingerprint and durable evaluation; accepted assertions retain the evaluation with the trusted actor. Reviewer selection cannot waive these thresholds. Historical cases lacking this evidence require material new evidence.
Allowed transitions:
@@ -348,18 +278,15 @@ Each Identifier Type declares:
- verification/provenance required for that claim;
- matching rules permitted to consume it.
-Do not persist `OTHER`, generic `VAT_ID`, connector IDs, or Source Record References as Official
-Identifiers.
+Do not persist `OTHER`, generic `VAT_ID`, connector IDs, or Source Record References as Official Identifiers.
-`CZ_DIC` is the Czech tax identifier. Current VAT registration, payer status, reverse-charge
-eligibility, and tax treatment remain outside Party Registry.
+`CZ_DIC` is the Czech tax identifier. Current VAT registration, payer status, reverse-charge eligibility, and tax treatment remain outside Party Registry.
## Contact Points
Initial Contact Point Types are `EMAIL`, `PHONE`, and structured `ADDRESS`.
-Contact Points are contactability facts, not credentials or identity keys. The same normalized email
-or phone may belong to several Parties. Matching may use them only under explicit Match Rules.
+Contact Points are contactability facts, not credentials or identity keys. The same normalized email or phone may belong to several Parties. Matching may use them only under explicit Match Rules.
ADDRESS may carry compatible purposes:
@@ -370,12 +297,9 @@ DELIVERY
CORRESPONDENCE
```
-BILLING and DELIVERY are reusable Party-level defaults only when independent of a Legal Entity,
-Counterparty, contract, or transaction. Context-specific preferences remain with that context. A
-completed document owns the exact address snapshot it used.
+BILLING and DELIVERY are reusable Party-level defaults only when independent of a Legal Entity, Counterparty, contract, or transaction. Context-specific preferences remain with that context. A completed document owns the exact address snapshot it used.
-Any searchable Contact Point requires an explicit privacy classification and Read permission. Do
-not index inactive, retracted, or disputed values as current facts.
+Any searchable Contact Point requires an explicit privacy classification and Read permission. Do not index inactive, retracted, or disputed values as current facts.
## Party Relationships
@@ -387,12 +311,9 @@ Initial production type:
CONTACT_PERSON_OF PERSON -> ORGANIZATION
```
-`EMPLOYEE_OF` remains deferred until a concrete external-organization use case proves it is not a
-second employee/HR lifecycle. `BRANCH_OF` and `OTHER` are not production types.
+`EMPLOYEE_OF` remains deferred until a concrete external-organization use case proves it is not a second employee/HR lifecycle. `BRANCH_OF` and `OTHER` are not production types.
-Relationship endpoints and type are immutable. Changing either ends or corrects the old assertion
-and creates a new relationship. Relationship periods may be open-ended but cannot overlap when the
-type forbids overlap.
+Relationship endpoints and type are immutable. Changing either ends or corrects the old assertion and creates a new relationship. Relationship periods may be open-ended but cannot overlap when the type forbids overlap.
## Counterparty
@@ -402,8 +323,7 @@ A Counterparty is one durable commercial or contractual context:
Counterparty = Party × Legal Entity
```
-The tuple is unique per Tenant. A Counterparty is created only from provenance-backed evidence of a
-commercial or contractual relationship. Knowing or displaying a Party is insufficient.
+The tuple is unique per Tenant. A Counterparty is created only from provenance-backed evidence of a commercial or contractual relationship. Knowing or displaying a Party is insufficient.
Initial role types are:
@@ -412,13 +332,9 @@ CUSTOMER
SUPPLIER
```
-Each role is a separate time-bounded period. Several roles may coexist. Ending one role does not end
-another, the Counterparty, or the Party. A Counterparty may have no current role when the underlying
-commercial context is still evidenced or retained historically.
+Each role is a separate time-bounded period. Several roles may coexist. Ending one role does not end another, the Counterparty, or the Party. A Counterparty may have no current role when the underlying commercial context is still evidenced or retained historically.
-`BUSINESS_PARTNER` is not a role. The Counterparty already represents the generic commercial or
-contractual context. Future distributor, reseller, accounting-office, or other capacities require
-named types with their own preconditions.
+`BUSINESS_PARTNER` is not a role. The Counterparty already represents the generic commercial or contractual context. Future distributor, reseller, accounting-office, or other capacities require named types with their own preconditions.
## Matching
@@ -440,9 +356,7 @@ Rule order:
4. weak signals -> candidate ranking only;
5. no qualifying evidence -> NO_MATCH.
-Weak signals include names, unverified email/phone, address similarity, and provider classification.
-No numeric score may override an authoritative conflict. An ML model may rank review candidates but
-cannot produce canonical identity authority.
+Weak signals include names, unverified email/phone, address similarity, and provider classification. No numeric score may override an authoritative conflict. An ML model may rank review candidates but cannot produce canonical identity authority.
## Duplicate Candidate cases
@@ -466,19 +380,11 @@ DISMISSED_AS_NON_SUBJECT
CONFIRMED_DUPLICATE_PARTIES
```
-`CREATE_NEW` is available only when a transactional recheck proves that every qualifying strong claim
-is still unclaimed, or when the Candidate legitimately has no strong claim and the explicit
-create-without-strong-identifier policy allows creation. It is forbidden while any qualifying strong
-claim is owned by an existing Party. A reviewer cannot drop authoritative evidence merely to make
-creation pass.
+`CREATE_NEW` is available only when a transactional recheck proves that every qualifying strong claim is still unclaimed, or when the Candidate legitimately has no strong claim and the explicit create-without-strong-identifier policy allows creation. It is forbidden while any qualifying strong claim is owned by an existing Party. A reviewer cannot drop authoritative evidence merely to make creation pass.
-A case whose strong claims resolve to one or several existing Parties must instead match an existing
-Party, correct/retract/reassign the wrong claim through an authorized Party Correction and then match,
-confirm duplicate existing Parties for the separate merge flow, request evidence, or dismiss the input
-as not representing a subject.
+A case whose strong claims resolve to one or several existing Parties must instead match an existing Party, correct/retract/reassign the wrong claim through an authorized Party Correction and then match, confirm duplicate existing Parties for the separate merge flow, request evidence, or dismiss the input as not representing a subject.
-`MATCH_EXISTING` consumes an explicit canonical `selectedPartyRef`; it does not rerun ordinary matching
-without the review decision:
+`MATCH_EXISTING` consumes an explicit canonical `selectedPartyRef`; it does not rerun ordinary matching without the review decision:
```text
ResolveDuplicateCandidateMatch(caseRef, selectedPartyRef, expectedRevision)
@@ -501,24 +407,15 @@ ResolveDuplicateCandidateMatch(caseRef, selectedPartyRef, expectedRevision)
Domain Events, Outbox Messages, and invocation success atomically
```
-A weak-evidence case with no strong claims may still be resolved to the selected Party when the
-Identity Reviewer has the required authority and the current Match Rule permits reviewed matching. The
-explicit selection is part of the resolution Action input and evidence; it is never inferred again from
-the unchanged Candidate.
+A weak-evidence case with no strong claims may still be resolved to the selected Party when the Identity Reviewer has the required authority and the current Match Rule permits reviewed matching. The explicit selection is part of the resolution Action input and evidence; it is never inferred again from the unchanged Candidate.
-`CREATE_NEW` uses a separate resolution Action with no selected Party. It acquires the same claim-key
-locks, repeats canonical claim resolution, and may create only when every qualifying claim is still
-unclaimed or the approved no-strong-identifier policy applies. The case decision alone never bypasses
-uniqueness.
+`CREATE_NEW` uses a separate resolution Action with no selected Party. It acquires the same claim-key locks, repeats canonical claim resolution, and may create only when every qualifying claim is still unclaimed or the approved no-strong-identifier policy applies. The case decision alone never bypasses uniqueness.
-Creating or reusing the case and its AMBIGUOUS Party Match Decision is a committed successful Action
-outcome. Resolution is a separate Action. Repeated identical evidence reuses the prior open or
-resolved case unless a new fact, policy version, or Candidate meaning changes the decision input.
+Creating or reusing the case and its AMBIGUOUS Party Match Decision is a committed successful Action outcome. Resolution is a separate Action. Repeated identical evidence reuses the prior open or resolved case unless a new fact, policy version, or Candidate meaning changes the decision input.
## Correction
-Correction applies only when a previously accepted Party-owned assertion was wrong at the time it
-was asserted. It records:
+Correction applies only when a previously accepted Party-owned assertion was wrong at the time it was asserted. It records:
- corrected assertion;
- correction reason;
@@ -528,14 +425,11 @@ was asserted. It records:
- policy version;
- affected current projections and emitted event.
-Enrichment of a previously unknown value and legitimate real-world change are not corrections.
-Correction does not merge two Parties.
+Enrichment of a previously unknown value and legitimate real-world change are not corrections. Correction does not merge two Parties.
## Merge
-Production merge remains disabled for the initial implementation. The schemas and contracts may be
-prepared, but no Action is published as executable until the following behavior is tested end to
-end:
+Production merge remains disabled for the initial implementation. The schemas and contracts may be prepared, but no Action is published as executable until the following behavior is tested end to end:
1. same-Tenant duplicate confirmation;
2. deterministic survivor selection;
@@ -560,17 +454,13 @@ PartyMerged {
}
```
-The event contains identities, not mutable Party payload copies. Consumers resolve current state
-through public Party Registry contracts.
+The event contains identities, not mutable Party payload copies. Consumers resolve current state through public Party Registry contracts.
-A consumer that owns at most one profile per Party must provide real behavior for collision
-detection and reconciliation. A descriptor or marker without tested behavior does not make merge
-safe.
+A consumer that owns at most one profile per Party must provide real behavior for collision detection and reconciliation. A descriptor or marker without tested behavior does not make merge safe.
## Search
-OntOS Core Search owns the physical projection and query runtime. Party Registry publishes safe
-search descriptors and lifecycle events.
+OntOS Core Search owns the physical projection and query runtime. Party Registry publishes safe search descriptors and lifecycle events.
V1 Party Search fields:
@@ -578,45 +468,27 @@ V1 Party Search fields:
- active Official Identifiers;
- active EMAIL and PHONE Contact Points when the caller has the required permission.
-V1 Counterparty Search adds required Legal Entity scope and current CUSTOMER/SUPPLIER filters.
-Archived Parties are excluded by default and may be included explicitly. Party Alias hits resolve to
-the canonical Party and never appear as a second current Party.
+V1 Counterparty Search adds required Legal Entity scope and current CUSTOMER/SUPPLIER filters. Archived Parties are excluded by default and may be included explicitly. Party Alias hits resolve to the canonical Party and never appear as a second current Party.
-Search remains eventually consistent. Reads by ResourceRef, exact identifier claims, create
-uniqueness, correction, and merge resolution use the canonical Party Registry store.
+Search remains eventually consistent. Reads by ResourceRef, exact identifier claims, create uniqueness, correction, and merge resolution use the canonical Party Registry store.
## External evidence
-ARES is an External Evidence Provider reached through an owner-local Direct Provider Adapter or an
-approved Symmy Connector and an explicit Integration Route.
+ARES is an External Evidence Provider reached through an owner-local Direct Provider Adapter or an approved Symmy Connector and an explicit Integration Route.
-The read side returns bounded normalized evidence with source and observed time. It does not mutate
-Party state. Applying evidence invokes Party Registry Actions fact by fact.
+The read side returns bounded normalized evidence with source and observed time. It does not mutate Party state. Applying evidence invokes Party Registry Actions fact by fact.
-September V1 keeps six ARES decisions: PREFILL_ONLY, APPLY_ENRICHMENT, NO_CHANGE,
-NEEDS_CONFIRMATION, CORRECTION_CANDIDATE, IDENTITY_AMBIGUITY. Enrichment requires explicit user
-confirmation. A policy decision is not a committed receipt; each successful standard Action has its
-own receipt and failed multi-fact application stops with the completed subset.
+September V1 keeps six ARES decisions: PREFILL_ONLY, APPLY_ENRICHMENT, NO_CHANGE, NEEDS_CONFIRMATION, CORRECTION_CANDIDATE, IDENTITY_AMBIGUITY. Enrichment requires explicit user confirmation. A policy decision is not a committed receipt; each successful standard Action has its own receipt and failed multi-fact application stops with the completed subset.
-Canonical ARES application excludes Create. Candidate prefill returns proposed data for a separate
-explicit Matching/Create call, without manufacturing subject attestation. For historical-error
-suspicion, a governed current assertion may carry prior ARES provenance for the same ICO and same
-non-null provider revision, observed at or before the assertion's validFrom. A conflicting fresh
-observation on that unchanged revision can nominate the exact assertion for Correction review.
-A newer provider revision or a difference alone cannot. This bounded suspicion never proves error
-or executes Correction: a reviewer must establish the historical error through the existing
-Correction Action. Unsupported historical address correction stays review-only.
+Canonical ARES application excludes Create. Candidate prefill returns proposed data for a separate explicit Matching/Create call, without manufacturing subject attestation. For historical-error suspicion, a governed current assertion may carry prior ARES provenance for the same ICO and same non-null provider revision, observed at or before the assertion's validFrom. A conflicting fresh observation on that unchanged revision can nominate the exact assertion for Correction review. A newer provider revision or a difference alone cannot. This bounded suspicion never proves error or executes Correction: a reviewer must establish the historical error through the existing Correction Action. Unsupported historical address correction stays review-only.
-Initial delivery may support read-only ARES lookup for ICO. Automatic conflict correction, merge, or
-bulk field overwrite is excluded.
+Initial delivery may support read-only ARES lookup for ICO. Automatic conflict correction, merge, or bulk field overwrite is excluded.
-Connector Registry owns provider-issued record correlations. It does not own ICO, CZ_DIC, Party
-identity, or the accepted Party state.
+Connector Registry owns provider-issued record correlations. It does not own ICO, CZ_DIC, Party identity, or the accepted Party state.
## Contacts replacement
-The repository's current Contacts implementation is not a production System of Record. Replace it
-with a breaking change:
+The repository's current Contacts implementation is not a production System of Record. Replace it with a breaking change:
```text
current Contacts customer/contact identity
@@ -633,8 +505,7 @@ Required implementation sequence:
5. remove legacy Contacts customer and subordinate-contact identity ownership;
6. update tests and fixtures to create Party state through public contracts.
-Do not build repository-only backfill, dual-write, compatibility aliases, or long-lived migration
-mapping. Create a migration only when a verified live External Business System or dataset exists.
+Do not build repository-only backfill, dual-write, compatibility aliases, or long-lived migration mapping. Create a migration only when a verified live External Business System or dataset exists.
## Required focused validation
@@ -644,8 +515,7 @@ The initial implementation is not complete until these behaviors pass:
- projection lag cannot produce a duplicate Party;
- a conflicting authoritative identifier commits one ambiguity case and one Party Match Decision;
- an insufficient-subject-evidence rejection commits neither a case nor a decision;
-- an indeterminate Party Create commit recovers the same outcome through invocation resolution and
- Party Match Decision Read;
+- an indeterminate Party Create commit recovers the same outcome through invocation resolution and Party Match Decision Read;
- a repeated idempotent Party Create never executes the handler again and resolves the same result;
- unverified shared email or phone never auto-matches;
- cross-Tenant identifier equality never resolves or conflicts across Tenants;
@@ -660,7 +530,6 @@ The initial implementation is not complete until these behaviors pass:
- all Party tables enforce Tenant isolation with enabled and forced RLS;
- Contacts no longer owns shared person/organization identity after the breaking replacement.
-Run the smallest affected dependency cone for each implementation increment. File presence, a
-manifest declaration, or a generated marker is not evidence that these behaviors work.
+Run the smallest affected dependency cone for each implementation increment. File presence, a manifest declaration, or a generated marker is not evidence that these behaviors work.
ARES dispatch preserves the original confirmed observation and uses its `servedAt` as the logical as-of `decidedAt` in the command provenance envelope. This keeps the command payload and idempotency hash stable across delivery attempts; it is not the execution timestamp. Assertion `recordedAt` and Core invocation/audit time record actual acceptance. Both the original confirmation and refreshed observation must remain fresh; a new refresh cannot revive an expired confirmation. Failed or indeterminate receipts require standard commit resolution before retry.
diff --git a/app/docs/architecture/VALUE_OBJECTS.md b/app/docs/architecture/VALUE_OBJECTS.md
index 54c7ca153..221cb92ea 100644
--- a/app/docs/architecture/VALUE_OBJECTS.md
+++ b/app/docs/architecture/VALUE_OBJECTS.md
@@ -1,8 +1,6 @@
# Value Objects
-Use a value object when a domain concept is defined entirely by its attributes and has no
-independent identity or lifecycle. Two value objects with the same normalized attributes are equal
-even when they were created separately.
+Use a value object when a domain concept is defined entirely by its attributes and has no independent identity or lifecycle. Two value objects with the same normalized attributes are equal even when they were created separately.
## Entity or value object
@@ -21,30 +19,18 @@ Model it as an entity or Resource when any of these are true:
- several owners intentionally share and observe changes to the same instance; or
- another module must refer to it through a ResourceRef and public owner contract.
-Do not introduce identity merely to normalize storage or avoid repeating fields. Conversely, do not
-embed a mutable shared concept as a value object when updates must be coordinated across owners.
+Do not introduce identity merely to normalize storage or avoid repeating fields. Conversely, do not embed a mutable shared concept as a value object when updates must be coordinated across owners.
## Ownership and persistence
-The owning module defines a value object's schema, normalization, validation, and serialization.
-Persist it with its owner, either in the owner's table or an owner-private child table. A child
-table does not automatically make the value an entity.
+The owning module defines a value object's schema, normalization, validation, and serialization. Persist it with its owner, either in the owner's table or an owner-private child table. A child table does not automatically make the value an entity.
-Across a MicroVertical seam, transmit a value snapshot through a published schema when the consumer
-needs the data as observed at that moment. Use a ResourceRef only when the consumer needs the stable
-identity owned by another module.
+Across a MicroVertical seam, transmit a value snapshot through a published schema when the consumer needs the data as observed at that moment. Use a ResourceRef only when the consumer needs the stable identity owned by another module.
-When historical accuracy matters, store the accepted snapshot on the historical record even if an
-independently addressable source entity also exists. An Order, invoice, or evidence record must not
-silently change because a current profile was edited later.
+When historical accuracy matters, store the accepted snapshot on the historical record even if an independently addressable source entity also exists. An Order, invoice, or evidence record must not silently change because a current profile was edited later.
## Address example
-An address is normally a value object owned by the record that uses it: billing address, delivery
-address, registered office snapshot, or Contact Point value. Store normalized structured fields and
-replace the address as one value. Two equal addresses do not imply one shared business object.
+An address is normally a value object owned by the record that uses it: billing address, delivery address, registered office snapshot, or Contact Point value. Store normalized structured fields and replace the address as one value. Two equal addresses do not imply one shared business object.
-Promote a place to an entity, such as a Location, only when that concrete place needs stable
-identity, its own lifecycle or permissions, independent relationships, or deliberate sharing across
-modules. One module then owns the Location and other modules use its ResourceRef and public
-contracts. Historical documents still retain the address snapshot accepted at the time.
+Promote a place to an entity, such as a Location, only when that concrete place needs stable identity, its own lifecycle or permissions, independent relationships, or deliberate sharing across modules. One module then owns the Location and other modules use its ResourceRef and public contracts. Historical documents still retain the address snapshot accepted at the time.
diff --git a/app/docs/frontend/FRONTEND.md b/app/docs/frontend/FRONTEND.md
index e35aaf4b7..31ad80e99 100644
--- a/app/docs/frontend/FRONTEND.md
+++ b/app/docs/frontend/FRONTEND.md
@@ -49,9 +49,7 @@ data hook → generated Effect BFF client → BFF endpoint → Action runtime
`@techsio/ui-kit` is the source of truth for components, tokens, typography, spacing, colors, icons, forms, accessibility, and interaction patterns.
-Treat Figma as a wireframe for information hierarchy, component arrangement, and interaction intent.
-Do not copy its styling or introduce visual values from the design file. The installed
-`@techsio/ui-kit` components and tokens remain the visual and accessibility authority.
+Treat Figma as a wireframe for information hierarchy, component arrangement, and interaction intent. Do not copy its styling or introduce visual values from the design file. The installed `@techsio/ui-kit` components and tokens remain the visual and accessibility authority.
Before creating UI:
@@ -183,12 +181,7 @@ Keep state in the lowest appropriate owner:
- Server data belongs in loaders or query caches.
- Cross-feature interactive state belongs in an application store.
-The authenticated Shell is server-composed. Its layout receives plain navigation and legal-entity
-view models, keeps search persistent, and uses full document reloads after successful tenant or
-legal-entity switches. Direct module, search, and ResourceRef routes map typed loader results to
-explicit selection-required, empty, partial, forbidden, not-found, unavailable/retry, and resolved
-states. Disabled module and media affordances remain semantic, non-interactive content with an
-accessible explanation; inaccessible items are never guessed into links.
+The authenticated Shell is server-composed. Its layout receives plain navigation and legal-entity view models, keeps search persistent, and uses full document reloads after successful tenant or legal-entity switches. Direct module, search, and ResourceRef routes map typed loader results to explicit selection-required, empty, partial, forbidden, not-found, unavailable/retry, and resolved states. Disabled module and media affordances remain semantic, non-interactive content with an accessible explanation; inaccessible items are never guessed into links.
## Hooks and React Effects
diff --git a/app/docs/integrations/ares.md b/app/docs/integrations/ares.md
index 6265d8800..928905456 100644
--- a/app/docs/integrations/ares.md
+++ b/app/docs/integrations/ares.md
@@ -2,15 +2,11 @@
Research verified: 2026-09-01
-> [!IMPORTANT]
-> This document owns the ARES provider protocol, normalized evidence, and adapter resilience. Party
-> identity, matching, correction, and canonical writes follow
-> [Party Registry](../architecture/PARTY_REGISTRY.md). ARES never writes Party state directly.
+> [!IMPORTANT] This document owns the ARES provider protocol, normalized evidence, and adapter resilience. Party identity, matching, correction, and canonical writes follow [Party Registry](../architecture/PARTY_REGISTRY.md). ARES never writes Party state directly.
## Ownership
-ARES is an External Evidence Provider. It can supply observations about a Czech economic subject,
-but it is not the System of Record for an OntOS Party.
+ARES is an External Evidence Provider. It can supply observations about a Czech economic subject, but it is not the System of Record for an OntOS Party.
The `party.registry` MicroVertical owns:
@@ -27,9 +23,7 @@ The owner-local Direct Provider Adapter owns:
- provider error mapping and diagnostics;
- translation into the bounded provider-neutral evidence envelope.
-Connector Registry owns a provider-issued record correlation when OntOS must retain one. ARES
-record identifiers are not Party Official Identifiers merely because they are stable at the
-provider.
+Connector Registry owns a provider-issued record correlation when OntOS must retain one. ARES record identifiers are not Party Official Identifiers merely because they are stable at the provider.
## Supported V1 route
@@ -40,9 +34,7 @@ GET https://ares.gov.cz/ekonomicke-subjekty-v-be/rest/ekonomicke-subjekty/{ico}
Accept: application/json
```
-The public provider contract is documented by the
-[official OpenAPI document](https://ares.gov.cz/ekonomicke-subjekty-v-be/rest/v3/api-docs) and
-[Swagger UI](https://ares.gov.cz/swagger-ui/).
+The public provider contract is documented by the [official OpenAPI document](https://ares.gov.cz/ekonomicke-subjekty-v-be/rest/v3/api-docs) and [Swagger UI](https://ares.gov.cz/swagger-ui/).
Input rules:
@@ -50,8 +42,7 @@ Input rules:
2. require exactly eight decimal digits;
3. preserve leading zeroes;
4. do not guess or pad shorter input;
-5. never accept Tenant, Principal, Legal Entity, Party, or authorization identity from the lookup
- payload.
+5. never accept Tenant, Principal, Legal Entity, Party, or authorization identity from the lookup payload.
The browser calls a generated governed Read. Only the private server-side adapter calls ARES.
@@ -78,9 +69,7 @@ AresSubjectEvidence {
}
```
-The exact Effect Schemas belong to the owning MicroVertical. They must reject unknown unbounded
-payload retention and preserve enough source metadata to explain when and from where each
-observation was obtained.
+The exact Effect Schemas belong to the owning MicroVertical. They must reject unknown unbounded payload retention and preserve enough source metadata to explain when and from where each observation was obtained.
Provider fields commonly used by the consolidated subject route include:
@@ -99,8 +88,7 @@ primarniZdroj
icoId
```
-Their presence in the provider response does not authorize Party Registry to apply them. The Party
-contract owns the allowlist and fact-specific authority policy.
+Their presence in the provider response does not authorize Party Registry to apply them. The Party contract owns the allowlist and fact-specific authority policy.
## Read outcomes
@@ -118,12 +106,9 @@ PROVIDER_TIMEOUT
PROVIDER_RESPONSE_INVALID
```
-`NOT_FOUND` is a valid provider result. Timeout, denial, throttling, transport failure, response
-decode failure, and unavailable provider are failures and must never be interpreted as `NOT_FOUND`
-or `NO_MATCH`.
+`NOT_FOUND` is a valid provider result. Timeout, denial, throttling, transport failure, response decode failure, and unavailable provider are failures and must never be interpreted as `NOT_FOUND` or `NO_MATCH`.
-The BFF maps expected failures exhaustively to the repository's typed Problem Details contract. It
-does not expose raw provider bodies, secrets, stack traces, or internal URLs.
+The BFF maps expected failures exhaustively to the repository's typed Problem Details contract. It does not expose raw provider bodies, secrets, stack traces, or internal URLs.
## Adapter resilience
@@ -136,18 +121,12 @@ The Direct Provider Adapter must:
- cache only successful immutable evidence envelopes for a bounded period;
- expose observation and cache age to the caller;
- never cache validation, denial, not-found, decode, or transport failures as successful evidence;
-- respect the current
- [official operating conditions](https://ares.gov.cz/stranky/podminky-provozu);
-- keep authentication and CORS assumptions private to the adapter so provider changes do not alter
- Party contracts.
+- respect the current [official operating conditions](https://ares.gov.cz/stranky/podminky-provozu);
+- keep authentication and CORS assumptions private to the adapter so provider changes do not alter Party contracts.
-A cache changes transport cost, not fact authority. Cached evidence remains external evidence with
-its original `observedAt` and must pass the same Party policy as a fresh response.
+A cache changes transport cost, not fact authority. Cached evidence remains external evidence with its original `observedAt` and must pass the same Party policy as a fresh response.
-The implemented adapter uses a three-second timeout covering response headers and body decoding,
-at most two bounded exponential retries, a five-minute successful-result cache capped at 256
-entries, same-IČO request coalescing, and four concurrent provider requests. These implementation
-settings do not change the dated external research above.
+The implemented adapter uses a three-second timeout covering response headers and body decoding, at most two bounded exponential retries, a five-minute successful-result cache capped at 256 entries, same-IČO request coalescing, and four concurrent provider requests. These implementation settings do not change the dated external research above.
## Canonical apply boundary
@@ -169,27 +148,16 @@ Examples:
- a valid accepted IČO uses the standard Official Identifier Add Action;
- a previously unknown accepted business name uses Party enrichment;
- an accepted registered address uses the standard Contact Point Action;
-- a conflict with a current authoritative assertion produces confirmation, ambiguity, or Party
- Correction work;
+- a conflict with a current authoritative assertion produces confirmation, ambiguity, or Party Correction work;
- no provider response performs Party Merge.
-V1 may prefill an explicit user-confirmed Party flow. Unattended bulk apply, automatic correction,
-automatic merge, and whole-response overwrite remain excluded until their fact-specific policies and
-behavioral conflict tests exist.
+V1 may prefill an explicit user-confirmed Party flow. Unattended bulk apply, automatic correction, automatic merge, and whole-response overwrite remain excluded until their fact-specific policies and behavioral conflict tests exist.
-The implemented coordinator refreshes governed canonical state before applying selected facts.
-Each accepted fact records bounded observation and decision evidence separately from the trusted
-accepting Principal. Independent stable Action idempotency keys support explicit partial outcomes
-and recovery: a retry skips already-satisfied facts and continues missing facts rather than
-overwriting current assertions. There is no ARES-specific mutation Action or cross-module shared
-transaction.
+The implemented coordinator refreshes governed canonical state before applying selected facts. Each accepted fact records bounded observation and decision evidence separately from the trusted accepting Principal. Independent stable Action idempotency keys support explicit partial outcomes and recovery: a retry skips already-satisfied facts and continues missing facts rather than overwriting current assertions. There is no ARES-specific mutation Action or cross-module shared transaction.
## Optional provider research
-ARES also exposes source-specific public-register and trade-licensing routes and code-list routes.
-They are not part of the V1 Party lookup contract. Add one only when a concrete owning business fact
-requires it, then preserve its source-specific meaning and history rather than flattening it into the
-consolidated subject response.
+ARES also exposes source-specific public-register and trade-licensing routes and code-list routes. They are not part of the V1 Party lookup contract. Add one only when a concrete owning business fact requires it, then preserve its source-specific meaning and history rather than flattening it into the consolidated subject response.
Useful official references:
diff --git a/app/module-federation.shared.ts b/app/module-federation.shared.ts
new file mode 100644
index 000000000..ec7011ea1
--- /dev/null
+++ b/app/module-federation.shared.ts
@@ -0,0 +1,42 @@
+type SharedRuntimeVersions = Readonly<
+ Record<
+ '@modern-js/plugin-i18n/runtime' | '@modern-js/runtime' | '@tanstack/react-router' | 'react' | 'react-dom',
+ string
+ >
+>;
+
+/** Both delivery units must use the same singleton sharing policy. */
+export const createSharedRuntimeConfig = (versions: SharedRuntimeVersions) => ({
+ '@modern-js/plugin-i18n/runtime': {
+ import: '@modern-js/plugin-i18n/runtime/no-react-i18next',
+ requiredVersion: versions['@modern-js/plugin-i18n/runtime'],
+ singleton: true,
+ strictVersion: true,
+ treeShaking: false,
+ },
+ '@modern-js/runtime': {
+ requiredVersion: versions['@modern-js/runtime'],
+ singleton: true,
+ treeShaking: false,
+ },
+ '@tanstack/react-router': {
+ requiredVersion: versions['@tanstack/react-router'],
+ singleton: true,
+ treeShaking: false,
+ },
+ react: {
+ requiredVersion: versions.react,
+ singleton: true,
+ treeShaking: false,
+ },
+ 'react-dom': {
+ requiredVersion: versions['react-dom'],
+ singleton: true,
+ treeShaking: false,
+ },
+ 'react-dom/client': {
+ requiredVersion: versions['react-dom'],
+ singleton: true,
+ treeShaking: false,
+ },
+});
diff --git a/app/oxfmt.config.ts b/app/oxfmt.config.ts
index 62dc36bb3..a31cca208 100644
--- a/app/oxfmt.config.ts
+++ b/app/oxfmt.config.ts
@@ -1,8 +1,8 @@
import { defineConfig } from 'oxfmt';
-import ultracite from 'ultracite/oxfmt';
export default defineConfig({
- extends: [ultracite],
+ printWidth: 120,
+ trailingComma: 'all',
ignorePatterns: [
'.agents',
'.codex/skills',
diff --git a/app/oxlint.config.ts b/app/oxlint.config.ts
index baf614ad2..d1e782a6e 100644
--- a/app/oxlint.config.ts
+++ b/app/oxlint.config.ts
@@ -1,10 +1,19 @@
-import { testRestrictedImports } from './tools/oxlint/effect-native/shared/test-restricted-imports.ts';
import { defineConfig } from 'oxlint';
import core from 'ultracite/oxlint/core';
import { jsPluginSettings, selectJsPlugins } from 'ultracite/oxlint/js-plugins';
import react from 'ultracite/oxlint/react';
-const jsPlugins = selectJsPlugins(['github', 'sonarjs', 'react-doctor']);
+import { testRestrictedImports } from './tools/oxlint/effect-native/shared/test-restricted-imports.ts';
+
+const selectedJsPlugins = selectJsPlugins(['github', 'sonarjs', 'react-doctor']);
+const jsPlugins = {
+ ...selectedJsPlugins,
+ // Load GitHub's published rule-only entrypoint, not its ESLint configuration aggregator.
+ // The aggregator eagerly imports eslint-plugin-import and the ESLint runner; the rules do not.
+ jsPlugins: selectedJsPlugins.jsPlugins.map((plugin) =>
+ plugin.name === 'github' ? { ...plugin, specifier: 'eslint-plugin-github/lib/plugin.js' } : plugin,
+ ),
+};
const antiSlopRules = {
'anti-slop/no-chained-type-assertions': 'error',
@@ -42,7 +51,9 @@ const effectNativeRules: NonNullable[0]['rules']
'effect-native/no-dotenv-loading': 'error',
'effect-native/no-driver-failure-inspection': [
'error',
- { decoderPaths: ['packages/core-runtime/src/database/postgres-failure.ts'] },
+ {
+ decoderPaths: ['packages/core-runtime/src/database/postgres-failure.ts'],
+ },
],
'effect-native/no-duplicate-literal-vocabulary': 'error',
// These factories compose a scoped pool, its native SQL client, and Drizzle once.
@@ -185,7 +196,10 @@ export default defineConfig({
name: 'anti-slop-effect',
specifier: './tools/oxlint/anti-slop/effect/index.ts',
},
- { name: 'effect-native', specifier: './tools/oxlint/effect-native/index.ts' },
+ {
+ name: 'effect-native',
+ specifier: './tools/oxlint/effect-native/index.ts',
+ },
],
options: {
denyWarnings: true,
@@ -224,10 +238,7 @@ export default defineConfig({
{
// This guarded test-only entrypoint composes real services with boundary fakes.
// database-access:check rejects imports of it from production source.
- files: [
- 'packages/core-runtime/src/testing/**/*.ts',
- 'apps/shell-super-app/tests/e2e/auth-fixture.ts',
- ],
+ files: ['packages/core-runtime/src/testing/**/*.ts', 'apps/shell-super-app/tests/e2e/auth-fixture.ts'],
rules: {
'anti-slop-effect/no-service-constructor-imports': 'off',
},
@@ -293,10 +304,7 @@ export default defineConfig({
},
{
// Test registration deliberately returns an ignored promise, and test synchronization may use `.then`.
- files: [
- '**/*.{test,spec,test-d,spec-d}.{ts,tsx,js,jsx}',
- '**/__tests__/**/*.{ts,tsx,js,jsx}',
- ],
+ files: ['**/*.{test,spec,test-d,spec-d}.{ts,tsx,js,jsx}', '**/__tests__/**/*.{ts,tsx,js,jsx}'],
rules: {
'github/no-then': 'off',
// Ultracite's JS-plugin preset applies the same test-data exception; repeat it because
@@ -306,8 +314,16 @@ export default defineConfig({
'error',
{
allowForKnownSafeCalls: [
- { from: 'package', name: ['it', 'test'], package: '@playwright/test' },
- { from: 'package', name: ['it', 'test'], package: '@rstest/core' },
+ {
+ from: 'package',
+ name: ['it', 'test'],
+ package: '@playwright/test',
+ },
+ {
+ from: 'package',
+ name: ['it', 'test'],
+ package: '@rstest/core',
+ },
{ from: 'package', name: ['it', 'test'], package: 'node:test' },
],
},
@@ -477,10 +493,7 @@ export default defineConfig({
{
// These aliases name stable domain boundaries even when their current representation is
// identical to another type; removing the names would couple public/runtime APIs to storage.
- files: [
- 'apps/shell-super-app/src/api/auth-client.ts',
- 'packages/core-runtime/src/actions/runtime.ts',
- ],
+ files: ['apps/shell-super-app/src/api/auth-client.ts', 'packages/core-runtime/src/actions/runtime.ts'],
rules: {
'sonarjs/redundant-type-aliases': 'off',
},
@@ -652,9 +665,7 @@ export default defineConfig({
{
// The edit page keeps one cohesive mutation/detail workflow; splitting it would move
// authorization and retry state across component boundaries during this lint-only migration.
- files: [
- 'verticals/party-registry/src/routes/**/contacts/customers/**/contacts/**/edit/page.tsx',
- ],
+ files: ['verticals/party-registry/src/routes/**/contacts/customers/**/contacts/**/edit/page.tsx'],
rules: {
'react-doctor/no-giant-component': 'off',
},
diff --git a/app/package.json b/app/package.json
index ad4077745..eff5bc68e 100644
--- a/app/package.json
+++ b/app/package.json
@@ -34,8 +34,8 @@
"action:test:unit": "pnpm --filter @app/core-runtime action:test:unit",
"action:test:integration": "pnpm --filter @app/core-runtime action:test:integration",
"outbox:test": "pnpm --filter @app/core-runtime outbox:test:unit && pnpm --filter @app/core-runtime outbox:test:integration",
- "build": "pnpm -r --filter \"./verticals/*\" run build && pnpm --filter \"./apps/shell-super-app\" run build && pnpm mf:types && pnpm performance:readiness",
- "cloudflare:build": "pnpm -r --filter \"./verticals/*\" run cloudflare:build && pnpm --filter \"./apps/shell-super-app\" run cloudflare:build && ULTRAMODERN_MF_TYPES_ARCHIVE=dist-cloudflare/@mf-types.zip pnpm mf:types && pnpm cloudflare-output:verify && pnpm cloudflare:ssr-proof",
+ "build": "node ./scripts/ultramodern-typecheck.mts --build packages/shared-contracts/tsconfig.json && node ./scripts/ultramodern-typecheck.mts --build packages/shared-design-tokens/tsconfig.json && pnpm -r --filter \"./verticals/*\" run build && pnpm --filter \"./apps/shell-super-app\" run build && pnpm mf:types && pnpm performance:readiness",
+ "cloudflare:build": "node ./scripts/ultramodern-typecheck.mts --build packages/shared-contracts/tsconfig.json && node ./scripts/ultramodern-typecheck.mts --build packages/shared-design-tokens/tsconfig.json && pnpm -r --filter \"./verticals/*\" run cloudflare:build && pnpm --filter \"./apps/shell-super-app\" run cloudflare:build && pnpm mf:types --target cloudflare && pnpm cloudflare-output:verify && pnpm cloudflare:ssr-proof",
"cloudflare:deploy": "pnpm -r --filter \"./verticals/*\" run cloudflare:deploy && pnpm --filter \"./apps/shell-super-app\" run cloudflare:deploy",
"cloudflare:proof": "node ./scripts/proof-cloudflare-version.mts --out .codex/reports/cloudflare-version-proof/public-url-proof.json",
"cloudflare-output:verify": "node ./scripts/verify-cloudflare-output.mts",
@@ -67,10 +67,10 @@
"module-entrypoints:check": "node ./scripts/check-module-entrypoint-boundaries.mts",
"check:module-contracts": "node ./scripts/check-ontos-module-contracts.mts",
"typecheck": "node ./scripts/ultramodern-typecheck.mts --build tsconfig.json",
- "check": "pnpm format:check && pnpm typecheck:lint-rules && pnpm test:lint-rules && pnpm lint && pnpm action:test:unit && pnpm typecheck && pnpm skills:check && pnpm i18n:boundaries && pnpm api:check && pnpm database-access:check && pnpm module-entrypoints:check && pnpm check:module-contracts && pnpm contract:check && pnpm performance:readiness && pnpm quality:check",
+ "check": "pnpm typecheck:lint-rules && pnpm test:lint-rules && pnpm action:test:unit && pnpm database-access:check && pnpm module-entrypoints:check && pnpm check:module-contracts && pnpm quality:check && pnpm format:check && pnpm lint && pnpm typecheck && pnpm skills:check && pnpm i18n:boundaries && pnpm api:check && pnpm contract:check && pnpm performance:readiness",
"database-access:check": "node ./scripts/check-database-access-boundaries.mts",
- "format": "oxfmt . '!repos/**'",
- "format:check": "oxfmt --check . '!repos/**'",
+ "format": "oxfmt .",
+ "format:check": "oxfmt --check .",
"lint": "oxlint apps verticals packages scripts tools/oxlint/effect-native/tests/*.test.mts",
"lint:fix": "oxlint apps verticals packages scripts tools/oxlint/effect-native/tests/*.test.mts --fix",
"skills:install": "node ./scripts/bootstrap-agent-skills.mts",
@@ -79,7 +79,7 @@
"agents:refs:check": "node ./scripts/setup-agent-reference-repos.mts --check",
"api:check": "node ./scripts/check-ultramodern-api-boundaries.mts",
"i18n:boundaries": "node ./scripts/check-ultramodern-i18n-boundaries.mts",
- "postinstall": "node ./scripts/bootstrap-agent-skills.mts --postinstall && oxfmt . '!repos/**'",
+ "postinstall": "node ./scripts/bootstrap-agent-skills.mts --postinstall && oxfmt .",
"authorization:provision-current-actions": "node ./scripts/provision-current-action-authorization.mts",
"authorization:inventory:check": "node ./scripts/check-module-entrypoint-boundaries.mts",
"authorization:impact:report": "node ./scripts/report-fail-closed-authorization-impact.mts",
@@ -87,39 +87,39 @@
"quality:audit": "node ./scripts/quality-audit.mts",
"quality:audit:gate": "node ./scripts/quality-audit-gate.mts",
"quality:check": "pnpm quality:audit && pnpm quality:audit:gate",
- "quality:audit:test": "rstest --project scripts scripts/tests/quality-audit.test.mts scripts/tests/quality-audit-model.test.mts scripts/tests/quality-audit-runtime-model.test.mts scripts/tests/quality-audit-gate.test.mts scripts/tests/quality-cli-lifecycle.test.mts scripts/tests/quality-audit-count-domain.test.mts"
+ "quality:audit:test": "rstest --project scripts scripts/tests/quality-audit.test.mts scripts/tests/quality-audit-model.test.mts scripts/tests/quality-audit-runtime-model.test.mts scripts/tests/quality-audit-gate.test.mts scripts/tests/quality-cli-lifecycle.test.mts scripts/tests/quality-audit-count-domain.test.mts",
+ "build:analyze": "cross-env RSDOCTOR=true pnpm build"
},
"dependencies": {
"@authzed/authzed-node": "1.6.1",
"better-auth": "1.7.2",
"drizzle-orm": "1.0.0-rc.5-ab785fc",
"pg": "8.22.0",
- "@effect/sql-pg": "4.0.0-beta.107"
+ "@effect/sql-pg": "4.0.0-rc.112"
},
"devDependencies": {
"effect-rstest": "https://pkg.pr.new/ScriptedAlchemy/effect-rstest@79abbf684c7b150ee5f32694129a7caf969903bc",
"@rstest/core": "0.11.11",
- "@effect/platform-node": "4.0.0-beta.107",
- "@effect/tsgo": "0.19.0",
+ "@effect/platform-node": "4.0.0-rc.112",
+ "@effect/tsgo": "0.41.0",
"@noble/hashes": "2.2.0",
- "@modern-js/code-tools": "npm:@bleedingdev/modern-js-code-tools@3.8.2-ultramodern.12",
+ "@modern-js/code-tools": "npm:@bleedingdev/modern-js-code-tools@3.9.0-ultramodern.4",
"@modern-js/codesmith": "2.6.9",
- "@modern-js/create": "npm:@bleedingdev/modern-js-create@3.8.2-ultramodern.12",
- "@modern-js/plugin-bff": "npm:@bleedingdev/modern-js-plugin-bff@3.8.2-ultramodern.12",
- "@oxlint/plugins": "1.79.0",
- "@types/node": "20.19.43",
+ "@modern-js/plugin-bff": "npm:@bleedingdev/modern-js-plugin-bff@3.9.0-ultramodern.4",
+ "@oxlint/plugins": "1.81.0",
+ "@types/node": "^26.4.1",
"@types/pg": "8.20.0",
"@typescript/native": "npm:typescript@7.0.2",
- "@typescript/native-preview": "npm:typescript@7.0.2",
- "effect": "4.0.0-beta.107",
+ "@typescript/native-preview": "7.0.0-dev.20260707.2",
+ "effect": "4.0.0-rc.112",
"esbuild": "0.28.1",
"jose": "6.2.5",
"lefthook": "^2.1.10",
- "miniflare": "4.20260708.1",
- "oxfmt": "0.64.0",
- "oxlint": "1.79.0",
+ "miniflare": "4.20260730.0",
+ "oxfmt": "0.66.0",
+ "oxlint": "1.81.0",
"oxc-parser": "0.147.0",
- "ultracite": "7.10.7",
+ "ultracite": "7.11.0",
"@nkzw/eslint-plugin": "2.0.0",
"eslint-plugin-github": "6.1.2",
"eslint-plugin-perfectionist": "5.10.1",
@@ -131,7 +131,11 @@
"jsonc-parser": "3.3.1",
"fallow": "3.22.0",
"@vercel/nft": "0.29.2",
- "@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.8.2-ultramodern.12"
+ "@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.9.0-ultramodern.4",
+ "@modern-js/ultramodern-create": "npm:@bleedingdev/modern-js-ultramodern-create@3.9.0-ultramodern.4",
+ "cross-env": "10.1.0",
+ "@effect/opentelemetry": "4.0.0-rc.112",
+ "@modern-js/app-tools-extensions": "npm:@bleedingdev/modern-js-app-tools-extensions@3.9.0-ultramodern.4"
},
"engines": {
"node": ">=26",
diff --git a/app/packages/core-runtime/package.json b/app/packages/core-runtime/package.json
index 21805f093..1bf8492b8 100644
--- a/app/packages/core-runtime/package.json
+++ b/app/packages/core-runtime/package.json
@@ -32,14 +32,14 @@
},
"dependencies": {
"@authzed/authzed-node": "1.6.1",
- "@effect/platform-node": "4.0.0-beta.107",
+ "@effect/platform-node": "4.0.0-rc.112",
"drizzle-orm": "1.0.0-rc.5-ab785fc",
- "effect": "4.0.0-beta.107",
+ "effect": "4.0.0-rc.112",
"pg": "8.22.0",
- "@effect/sql-pg": "4.0.0-beta.107"
+ "@effect/sql-pg": "4.0.0-rc.112"
},
"devDependencies": {
- "@types/node": "^20.19.43",
+ "@types/node": "^26.4.1",
"@types/pg": "8.20.0",
"drizzle-kit": "1.0.0-rc.5-ab785fc",
"effect-rstest": "https://pkg.pr.new/ScriptedAlchemy/effect-rstest@79abbf684c7b150ee5f32694129a7caf969903bc",
diff --git a/app/packages/core-runtime/rstest.config.ts b/app/packages/core-runtime/rstest.config.ts
index 671f8b7a8..0543deff4 100644
--- a/app/packages/core-runtime/rstest.config.ts
+++ b/app/packages/core-runtime/rstest.config.ts
@@ -2,7 +2,11 @@ import { defineConfig } from '@rstest/core';
export default defineConfig({
projects: [
- { include: ['tests/unit/**/*.test.ts'], name: 'unit', testEnvironment: 'node' },
+ {
+ include: ['tests/unit/**/*.test.ts'],
+ name: 'unit',
+ testEnvironment: 'node',
+ },
{
include: ['tests/integration/**/*.test.ts'],
name: 'integration',
diff --git a/app/packages/core-runtime/scripts/verify-db-schema.mts b/app/packages/core-runtime/scripts/verify-db-schema.mts
index 4d57569e2..be160af83 100644
--- a/app/packages/core-runtime/scripts/verify-db-schema.mts
+++ b/app/packages/core-runtime/scripts/verify-db-schema.mts
@@ -1,7 +1,8 @@
-import type { EffectDrizzleQueryError } from 'drizzle-orm/effect-core';
// @effect-diagnostics processEnv:off globalConsole:off strictEffectProvide:off -- Existing compatibility boundary; expires: 2026-12-31.
import { getTableName, sql } from 'drizzle-orm';
+import type { EffectDrizzleQueryError } from 'drizzle-orm/effect-core';
import { Effect, Layer, Schema } from 'effect';
+
import type { CatalogEntry } from '../src/db/catalog.ts';
import { compareApplicationCatalog } from '../src/db/catalog.ts';
import { CoreDatabase, CoreDatabaseLive } from '../src/db/client.ts';
@@ -30,12 +31,9 @@ import {
workerCheckpoints,
} from '../src/db/schema.ts';
-class DatabaseVerificationError extends Schema.TaggedError()(
- 'DatabaseVerificationError',
- {
- reason: Schema.String,
- },
-) {}
+class DatabaseVerificationError extends Schema.TaggedError()('DatabaseVerificationError', {
+ reason: Schema.String,
+}) {}
const CatalogRowSchema = Schema.Struct({
kind: Schema.Literals(['migration', 'table']),
@@ -81,7 +79,9 @@ const verifyRuntimeRole = Effect.gen(function* verifyRuntimeRoleEffect() {
.pipe(
Effect.mapError(
() =>
- new DatabaseVerificationError({ reason: 'Unable to verify the PostgreSQL runtime role' }),
+ new DatabaseVerificationError({
+ reason: 'Unable to verify the PostgreSQL runtime role',
+ }),
),
);
const [role] = runtimeRole;
@@ -131,17 +131,13 @@ const verifySearchIsolation = Effect.gen(function* verifySearchIsolationEffect()
),
);
const [searchIsolationRow] = searchIsolation;
- const expectedSearchPolicies = operations.map(
- (operation) => `core_${tableName}_tenant_${operation}`,
- );
+ const expectedSearchPolicies = operations.map((operation) => `core_${tableName}_tenant_${operation}`);
if (
searchIsolationRow === undefined ||
!searchIsolationRow.relrowsecurity ||
!searchIsolationRow.relforcerowsecurity ||
searchIsolationRow.policy_names.length !== expectedSearchPolicies.length ||
- searchIsolationRow.policy_names.some(
- (policy, index) => policy !== expectedSearchPolicies[index],
- )
+ searchIsolationRow.policy_names.some((policy, index) => policy !== expectedSearchPolicies[index])
) {
return yield* new DatabaseVerificationError({
reason: 'Core Search must enforce forced tenant RLS with complete owner-operation policies',
@@ -227,9 +223,7 @@ const verifyCatalog = Effect.gen(function* verifyCatalogEffect() {
if (
migrationBookkeepingTables.length !== expectedMigrationBookkeepingTables.length ||
- migrationBookkeepingTables.some(
- (tableName, index) => tableName !== expectedMigrationBookkeepingTables[index],
- )
+ migrationBookkeepingTables.some((tableName, index) => tableName !== expectedMigrationBookkeepingTables[index])
) {
return yield* new DatabaseVerificationError({
reason: `Expected Drizzle migration bookkeeping tables [${expectedMigrationBookkeepingTables.join(', ')}], found [${migrationBookkeepingTables.join(', ')}]`,
@@ -298,7 +292,10 @@ const verifyDatabase = Effect.gen(function* verifyDatabaseEffect() {
)
.pipe(
Effect.mapError(
- () => new DatabaseVerificationError({ reason: 'Unable to verify same-tenant constraints' }),
+ () =>
+ new DatabaseVerificationError({
+ reason: 'Unable to verify same-tenant constraints',
+ }),
),
);
const presentCompositeConstraints = constraintRows
@@ -334,9 +331,7 @@ const verifyDatabase = Effect.gen(function* verifyDatabaseEffect() {
searchProjectionGenerations,
searchProjectionRebuilds,
workerCheckpoints,
- ].map((table) =>
- verifyTypedQuery(getTableName(table), () => database.executor.select().from(table).limit(0)),
- );
+ ].map((table) => verifyTypedQuery(getTableName(table), () => database.executor.select().from(table).limit(0)));
for (const query of typedQueries) {
yield* query;
diff --git a/app/packages/core-runtime/src/actions/collector.ts b/app/packages/core-runtime/src/actions/collector.ts
index a8af4848a..b7498965c 100644
--- a/app/packages/core-runtime/src/actions/collector.ts
+++ b/app/packages/core-runtime/src/actions/collector.ts
@@ -1,10 +1,7 @@
import { Effect, Match, Schema, Predicate } from 'effect';
-import {
- DataAccessEventSchema,
- DomainEventSchema,
- OutboxMessageSchema,
- createDomainEventReference,
-} from './events.ts';
+
+import { ActionCollectorError } from './errors.ts';
+import { DataAccessEventSchema, DomainEventSchema, OutboxMessageSchema, createDomainEventReference } from './events.ts';
import type {
ActionAccessEvidencePolicy,
ActionEvidenceSnapshot,
@@ -17,14 +14,8 @@ import type {
DomainEventReference,
OutboxMessage,
} from './events.ts';
-import { ActionCollectorError } from './errors.ts';
-const withOptionalProperty = <
- Base extends object,
- Key extends PropertyKey,
- Value,
- Trailing extends object,
->(
+const withOptionalProperty = (
base: Base,
condition: boolean,
key: Key,
@@ -53,8 +44,12 @@ const cloneAndFreeze = (value: Value): Value => freezeJson(structuredClon
const JsonObjectSchema = Schema.Record(Schema.String, Schema.Json);
const JsonObjectJsonStringSchema = Schema.fromJsonString(JsonObjectSchema);
const UnknownRecordSchema = Schema.Record(Schema.String, Schema.Unknown);
-const RuntimeActionKeyEvidenceSchema = Schema.Struct({ actionKey: Schema.Unknown });
-const RuntimeResultHashEvidenceSchema = Schema.Struct({ resultHash: Schema.Unknown });
+const RuntimeActionKeyEvidenceSchema = Schema.Struct({
+ actionKey: Schema.Unknown,
+});
+const RuntimeResultHashEvidenceSchema = Schema.Struct({
+ resultHash: Schema.Unknown,
+});
const metadataOnlyPolicyFields = (
policy: Extract,
@@ -96,53 +91,37 @@ const hasUnsupportedResultEvidence = (event: DataAccessEvent): boolean =>
(event.evidenceCaptureMode === 'redacted_payload' &&
(event.resultFingerprintHash !== undefined || event.resultFingerprintSchema !== undefined));
-const validateDataAccessInvariant = (
- event: DataAccessEvent,
-): Effect.Effect => {
+const validateDataAccessInvariant = (event: DataAccessEvent): Effect.Effect => {
if (hasIncompleteRedactedEvidence(event)) {
return Effect.fail(
- invalidCollectorInput(
- 'A redacted Data Access Event requires a redaction profile and evidence payload',
- ),
+ invalidCollectorInput('A redacted Data Access Event requires a redaction profile and evidence payload'),
);
}
if (hasUnexpectedRedactionProfile(event)) {
- return Effect.fail(
- invalidCollectorInput('A redaction profile is allowed only for redacted Data Access Events'),
- );
+ return Effect.fail(invalidCollectorInput('A redaction profile is allowed only for redacted Data Access Events'));
}
if (hasMetadataResultEvidence(event)) {
- return Effect.fail(
- invalidCollectorInput('Metadata-only Data Access evidence cannot contain result evidence'),
- );
+ return Effect.fail(invalidCollectorInput('Metadata-only Data Access evidence cannot contain result evidence'));
}
if (hasInvalidHashEvidence(event)) {
return Effect.fail(
- invalidCollectorInput(
- 'Hash-only Data Access evidence requires a paired result fingerprint and schema',
- ),
+ invalidCollectorInput('Hash-only Data Access evidence requires a paired result fingerprint and schema'),
);
}
if (hasUnsupportedResultEvidence(event)) {
- return Effect.fail(
- invalidCollectorInput('The Action runtime does not accept this result evidence shape'),
- );
+ return Effect.fail(invalidCollectorInput('The Action runtime does not accept this result evidence shape'));
}
- const targetParts = [
- event.targetModuleKey,
- event.targetResourceType,
- event.targetResourceId,
- ].filter((part) => part !== undefined);
+ const targetParts = [event.targetModuleKey, event.targetResourceType, event.targetResourceId].filter(
+ (part) => part !== undefined,
+ );
if (targetParts.length !== 0 && targetParts.length !== 3) {
- return Effect.fail(
- invalidCollectorInput('A Data Access Event target must be fully specified or absent'),
- );
+ return Effect.fail(invalidCollectorInput('A Data Access Event target must be fully specified or absent'));
}
return Effect.succeed(event);
@@ -152,9 +131,7 @@ export interface ActionCollector {
readonly addDomainEvent: (
event: DeclaredDomainEvent,
) => Effect.Effect;
- readonly addDomainEventInput: (
- event: Input,
- ) => Effect.Effect;
+ readonly addDomainEventInput: (event: Input) => Effect.Effect;
readonly addOutboxMessage: (
domainEvent: DomainEventReference,
message: OutboxMessage,
@@ -166,15 +143,9 @@ export interface ActionCollector {
readonly recordAuditEvidence: (
evidence: Readonly>>,
) => Effect.Effect;
- readonly recordAuditEvidenceInput: (
- evidence: Input,
- ) => Effect.Effect;
- readonly recordDataAccess: (
- event: DataAccessEventInput,
- ) => Effect.Effect;
- readonly recordDataAccessInput: (
- event: Input,
- ) => Effect.Effect;
+ readonly recordAuditEvidenceInput: (evidence: Input) => Effect.Effect;
+ readonly recordDataAccess: (event: DataAccessEventInput) => Effect.Effect;
+ readonly recordDataAccessInput: (event: Input) => Effect.Effect;
readonly snapshot: () => ActionEvidenceSnapshot;
}
@@ -193,46 +164,34 @@ export const createActionCollector = ();
- const recordAuditEvidenceInput = (
- evidence: Input,
- ): Effect.Effect =>
+ const recordAuditEvidenceInput = (evidence: Input): Effect.Effect =>
Schema.decodeUnknownEffect(UnknownRecordSchema)(evidence).pipe(
Effect.catchTag('SchemaError', () =>
Effect.fail(invalidCollectorInput('Action audit evidence must be a JSON object')),
),
Effect.flatMap((evidenceRecord) => {
if (hasAuditEvidence) {
- return Effect.fail(
- invalidCollectorInput('Action audit evidence may be recorded only once'),
- );
+ return Effect.fail(invalidCollectorInput('Action audit evidence may be recorded only once'));
}
if (
Schema.is(RuntimeActionKeyEvidenceSchema)(evidenceRecord) ||
Schema.is(RuntimeResultHashEvidenceSchema)(evidenceRecord)
) {
- return Effect.fail(
- invalidCollectorInput('Action audit evidence cannot replace runtime-owned fields'),
- );
+ return Effect.fail(invalidCollectorInput('Action audit evidence cannot replace runtime-owned fields'));
}
if (auditEvidenceSchema === undefined) {
- return Effect.fail(
- invalidCollectorInput('This Action does not declare custom audit evidence'),
- );
+ return Effect.fail(invalidCollectorInput('This Action does not declare custom audit evidence'));
}
const inputKeys = Object.keys(evidenceRecord).toSorted();
return Schema.decodeUnknownEffect(auditEvidenceSchema)(evidence).pipe(
Effect.catchTag('SchemaError', () =>
- Effect.fail(
- invalidCollectorInput('The Action audit evidence does not match its declared schema'),
- ),
+ Effect.fail(invalidCollectorInput('The Action audit evidence does not match its declared schema')),
),
Effect.map((declared) => ({ declared, inputKeys })),
);
}),
Effect.flatMap(({ declared, inputKeys }) =>
- Schema.decodeUnknownEffect(Schema.Json)(declared).pipe(
- Effect.map((decoded) => ({ decoded, inputKeys })),
- ),
+ Schema.decodeUnknownEffect(Schema.Json)(declared).pipe(Effect.map((decoded) => ({ decoded, inputKeys }))),
),
Effect.catchTag('SchemaError', () =>
Effect.fail(invalidCollectorInput('The Action audit evidence is not valid JSON')),
@@ -242,13 +201,8 @@ export const createActionCollector = key !== decodedKeys[index])
- ) {
- return Effect.fail(
- invalidCollectorInput('Action audit evidence contains undeclared fields'),
- );
+ if (inputKeys.length !== decodedKeys.length || inputKeys.some((key, index) => key !== decodedKeys[index])) {
+ return Effect.fail(invalidCollectorInput('Action audit evidence contains undeclared fields'));
}
return Schema.encodeEffect(JsonObjectJsonStringSchema)(decoded).pipe(
Effect.catchTag('SchemaError', () =>
@@ -256,9 +210,7 @@ export const createActionCollector = {
if (Buffer.byteLength(encoded, 'utf-8') > 4096) {
- return Effect.fail(
- invalidCollectorInput('Action audit evidence exceeds its size limit'),
- );
+ return Effect.fail(invalidCollectorInput('Action audit evidence exceeds its size limit'));
}
return Effect.sync(() => {
auditEvidence = cloneAndFreeze(decoded);
@@ -268,12 +220,9 @@ export const createActionCollector = ['recordAuditEvidence'] =
- recordAuditEvidenceInput;
+ const recordAuditEvidence: ActionCollector['recordAuditEvidence'] = recordAuditEvidenceInput;
- const recordDataAccessInput = (
- event: Input,
- ): Effect.Effect => {
+ const recordDataAccessInput = (event: Input): Effect.Effect => {
const eventRecord = Schema.is(UnknownRecordSchema)(event) ? event : undefined;
const resultFingerprintHash = eventRecord?.['resultFingerprintHash'];
const policyFields = Match.value(accessEvidencePolicy).pipe(
@@ -293,8 +242,7 @@ export const createActionCollector =
@@ -311,35 +259,25 @@ export const createActionCollector = ['recordDataAccess'] = recordDataAccessInput;
- const addDomainEventInput = (
- event: Input,
- ): Effect.Effect =>
+ const addDomainEventInput = (event: Input): Effect.Effect =>
Schema.decodeUnknownEffect(DomainEventSchema)(event).pipe(
Effect.catchTag('SchemaError', () =>
Effect.fail(invalidCollectorInput('The Domain Event is structurally invalid')),
),
Effect.flatMap((decoded) => {
if (decoded.producerModuleKey !== owningModuleKey) {
- return Effect.fail(
- invalidCollectorInput('A Domain Event producer must match the owning Action module'),
- );
+ return Effect.fail(invalidCollectorInput('A Domain Event producer must match the owning Action module'));
}
if (!Object.hasOwn(domainEventContracts, decoded.eventType)) {
- return Effect.fail(
- invalidCollectorInput('The Domain Event is not declared by this Action'),
- );
+ return Effect.fail(invalidCollectorInput('The Domain Event is not declared by this Action'));
}
const payloadSchema = domainEventContracts[decoded.eventType];
if (payloadSchema === undefined) {
- return Effect.fail(
- invalidCollectorInput('The Domain Event declaration has no payload schema'),
- );
+ return Effect.fail(invalidCollectorInput('The Domain Event declaration has no payload schema'));
}
- return Schema.decodeUnknownEffect(payloadSchema)(decoded.payloadJson).pipe(
+ return Schema.decodeEffect(payloadSchema)(decoded.payloadJson).pipe(
Effect.catchTag('SchemaError', () =>
- Effect.fail(
- invalidCollectorInput('The Domain Event payload violates its declared contract'),
- ),
+ Effect.fail(invalidCollectorInput('The Domain Event payload violates its declared contract')),
),
Effect.flatMap((payload) =>
Schema.decodeUnknownEffect(Schema.Json)(payload).pipe(
@@ -377,9 +315,7 @@ export const createActionCollector = >>,
) => Effect.Effect;
- readonly recordDataAccess: (
- event: DataAccessEventInput,
- ) => Effect.Effect;
+ readonly recordDataAccess: (event: DataAccessEventInput) => Effect.Effect;
}
export interface ActionHandlerContext<
diff --git a/app/packages/core-runtime/src/actions/definition.ts b/app/packages/core-runtime/src/actions/definition.ts
index d8e825537..94a71e38b 100644
--- a/app/packages/core-runtime/src/actions/definition.ts
+++ b/app/packages/core-runtime/src/actions/definition.ts
@@ -1,21 +1,20 @@
import { Effect, Schema, Predicate } from 'effect';
+
+import type { ScopedTransactionExecutor } from '../db/scoped-transaction.ts';
+import type { ModuleEntrypointDescriptor } from '../modules/module-entrypoint.ts';
+import { LEGAL_ENTITY_SCOPES } from '../operations/context.ts';
+import type { OperationalScope, LegalEntityScope } from '../operations/context.ts';
+import type { OperationContextUnavailable } from '../operations/errors.ts';
+import type { ResourceAccessTarget, TenantPermissionKey } from '../permissions/context-access.ts';
import type { ActionHandlerContext } from './context.ts';
import { ActionPayloadValidationError, ActionResultValidationError } from './errors.ts';
import type { ActionCollectorError } from './errors.ts';
import type { ActionAccessEvidencePolicy, DomainEventContractMap } from './events.ts';
import { isActionPolicy } from './policy.ts';
import type { ActionPolicy } from './policy.ts';
-import type { ModuleEntrypointDescriptor } from '../modules/module-entrypoint.ts';
-import type { ScopedTransactionExecutor } from '../db/scoped-transaction.ts';
-import { LEGAL_ENTITY_SCOPES } from '../operations/context.ts';
-import type { OperationalScope, LegalEntityScope } from '../operations/context.ts';
-import type { OperationContextUnavailable } from '../operations/errors.ts';
-import type { ResourceAccessTarget, TenantPermissionKey } from '../permissions/context-access.ts';
const actionRegistration: unique symbol = Symbol('@app/core-runtime/actions/registration');
-const actionResourcePermissionDeclaration: unique symbol = Symbol(
- '@app/core-runtime/actions/resource-permission',
-);
+const actionResourcePermissionDeclaration: unique symbol = Symbol('@app/core-runtime/actions/resource-permission');
class ActionPrivateStorage {
declare readonly [actionRegistration]?: true;
@@ -65,10 +64,9 @@ export type ActionResourcePermissionDeclaration = ActionPrivateStorage<
readonly kind: 'resource';
};
-const ActionDefinitionInvariantError = Schema.TaggedError()(
- 'ActionDefinitionInvariantError',
- { message: Schema.String },
-);
+const ActionDefinitionInvariantError = Schema.TaggedError()('ActionDefinitionInvariantError', {
+ message: Schema.String,
+});
const failActionDefinition = (message: string): never => {
throw new ActionDefinitionInvariantError({ message });
@@ -132,9 +130,7 @@ export interface ActionDescriptor<
* Declares an additional tenant-role permission required for the decoded payload.
* Returning undefined means the Action executor relation is sufficient for that payload.
*/
- readonly tenantPermission?: (
- payload: PayloadSchema['Type'],
- ) => ActionTenantPermission | undefined;
+ readonly tenantPermission?: (payload: PayloadSchema['Type']) => ActionTenantPermission | undefined;
}
export type ActionHandler<
@@ -147,11 +143,7 @@ export type ActionHandler<
> = (
payload: PayloadSchema['Type'],
context: ActionHandlerContext,
-) => Effect.Effect<
- ResultSchema['Type'],
- ActionCollectorError | DomainErrorSchema['Type'],
- Requirements
->;
+) => Effect.Effect;
export type ActionServiceFactory = (
transaction: ScopedTransactionExecutor,
@@ -160,8 +152,7 @@ export type ActionServiceFactory = (
type EmptyActionServices = Readonly>;
const emptyActionServices: EmptyActionServices = Object.freeze({});
-const emptyActionServiceFactory: ActionServiceFactory = () =>
- Effect.succeed(emptyActionServices);
+const emptyActionServiceFactory: ActionServiceFactory = () => Effect.succeed(emptyActionServices);
type ActionRegistrationPrivateValue<
PayloadSchema extends Schema.ConstraintDecoder,
@@ -171,14 +162,7 @@ type ActionRegistrationPrivateValue<
Services = Readonly>,
HandlerRequirements = never,
> = readonly [
- handler: ActionHandler<
- PayloadSchema,
- ResultSchema,
- DomainErrorSchema,
- DomainEvents,
- Services,
- HandlerRequirements
- >,
+ handler: ActionHandler,
serviceFactory: ActionServiceFactory,
];
@@ -203,9 +187,7 @@ export type ActionRegistration<
readonly _handlerRequirements?: HandlerRequirements;
readonly _services?: Services;
readonly [actionRegistration]: true;
- readonly descriptor: Readonly<
- ActionDescriptor
- >;
+ readonly descriptor: Readonly>;
};
/**
@@ -257,15 +239,12 @@ export interface ActionDescriptorValidationInput {
readonly tenantPermission?: unknown;
}
-const validateActionEntrypoint = (
- descriptor: ActionDescriptorValidationInput,
-): void => {
+const validateActionEntrypoint = (descriptor: ActionDescriptorValidationInput): void => {
if (
descriptor.entrypoint.role !== 'action' ||
descriptor.entrypoint.access !== 'write' ||
descriptor.entrypoint.moduleKey !== descriptor.owningModuleKey ||
- descriptor.entrypoint.scope !==
- (descriptor.owningModuleKey.startsWith('core.') ? 'system' : 'tenant') ||
+ descriptor.entrypoint.scope !== (descriptor.owningModuleKey.startsWith('core.') ? 'system' : 'tenant') ||
!Object.isFrozen(descriptor.entrypoint)
) {
return failActionDefinition(
@@ -273,32 +252,24 @@ const validateActionEntrypoint = (
);
}
};
-const validateActionLegalEntityScope = (
- descriptor: ActionDescriptorValidationInput,
-): void => {
+const validateActionLegalEntityScope = (descriptor: ActionDescriptorValidationInput): void => {
if (!LEGAL_ENTITY_SCOPES.some((scope) => scope === descriptor.legalEntityScope)) {
- return failActionDefinition(
- 'Action legal-entity scope must be required, optional, or forbidden',
- );
+ return failActionDefinition('Action legal-entity scope must be required, optional, or forbidden');
}
if (
descriptor.legalEntityPermission !== undefined &&
- (descriptor.legalEntityPermission !== 'manage_counterparty' ||
- descriptor.legalEntityScope !== 'required')
+ (descriptor.legalEntityPermission !== 'manage_counterparty' || descriptor.legalEntityScope !== 'required')
) {
return failActionDefinition(
'Action Legal Entity permission must be supported and require trusted Legal Entity scope',
);
}
};
-const validateActionPermissions = (
- descriptor: ActionDescriptorValidationInput,
-): void => {
+const validateActionPermissions = (descriptor: ActionDescriptorValidationInput): void => {
if (
(descriptor.resourcePermission !== undefined &&
!Schema.is(ActionResourcePermissionDeclarationSchema)(descriptor.resourcePermission)) ||
- (descriptor.tenantPermission !== undefined &&
- !Predicate.isFunction(descriptor.tenantPermission))
+ (descriptor.tenantPermission !== undefined && !Predicate.isFunction(descriptor.tenantPermission))
) {
return failActionDefinition('Action permission declarations and resolvers must be valid');
}
@@ -308,9 +279,7 @@ const validateActionPolicies = (
policies: readonly Policy[] | undefined,
): void => {
if (!Array.isArray(policies)) {
- return failActionDefinition(
- 'Action policies must be an explicit readonly array of Policy references',
- );
+ return failActionDefinition('Action policies must be an explicit readonly array of Policy references');
}
validateActionPermissions(descriptor);
for (const policy of policies) {
@@ -318,15 +287,11 @@ const validateActionPolicies = (
return failActionDefinition('Action policies must contain direct Policy object references');
}
if (policy.scope === 'microvertical' && policy.owningModuleKey !== descriptor.owningModuleKey) {
- return failActionDefinition(
- 'A MicroVertical Policy must be owned by the Action owning module',
- );
+ return failActionDefinition('A MicroVertical Policy must be owned by the Action owning module');
}
}
};
-export const validateActionDescriptorInput = (
- descriptor: ActionDescriptorValidationInput,
-): void => {
+export const validateActionDescriptorInput = (descriptor: ActionDescriptorValidationInput): void => {
validateActionEntrypoint(descriptor);
validateActionLegalEntityScope(descriptor);
validateActionPolicies(descriptor, descriptor.policies);
@@ -369,14 +334,7 @@ export function defineAction<
>(
descriptor: ActionDescriptor,
...definition: readonly [
- handler: ActionHandler<
- PayloadSchema,
- ResultSchema,
- DomainErrorSchema,
- DomainEvents,
- Services,
- HandlerRequirements
- >,
+ handler: ActionHandler,
serviceFactory: ActionServiceFactory,
]
): ActionRegistration<
@@ -446,9 +404,7 @@ export function defineAction<
const AnyActionRegistrationSchema = Schema.instanceOf(ActionPrivateStorage).check(
Schema.makeFilter((registration) =>
- registration[actionRegistration] === true &&
- registration.descriptor !== undefined &&
- Object.isFrozen(registration)
+ registration[actionRegistration] === true && registration.descriptor !== undefined && Object.isFrozen(registration)
? undefined
: 'Expected an immutable Action registration',
),
@@ -477,14 +433,8 @@ export const getActionHandler = <
Services,
HandlerRequirements
>,
-): ActionHandler<
- PayloadSchema,
- ResultSchema,
- DomainErrorSchema,
- DomainEvents,
- Services,
- HandlerRequirements
-> => ActionPrivateStorage.getValue(registration)[0];
+): ActionHandler =>
+ ActionPrivateStorage.getValue(registration)[0];
export const getActionServiceFactory = <
PayloadSchema extends Schema.ConstraintDecoder,
@@ -504,8 +454,7 @@ export const getActionServiceFactory = <
Services,
HandlerRequirements
>,
-): ActionServiceFactory =>
- ActionPrivateStorage.getValue(registration)[1];
+): ActionServiceFactory => ActionPrivateStorage.getValue(registration)[1];
export const getActionResourcePermissionTargetResolver = <
PayloadSchema extends Schema.ConstraintDecoder,
@@ -530,10 +479,7 @@ export const getActionResourcePermissionTargetResolver = <
? undefined
: ActionPrivateStorage.getValue(registration.descriptor.resourcePermission);
-const preserveFailureCause = (
- failure: Failure,
- cause: unknown,
-): Failure => {
+const preserveFailureCause = (failure: Failure, cause: unknown): Failure => {
Object.defineProperty(failure, 'cause', {
configurable: false,
enumerable: false,
@@ -543,10 +489,7 @@ const preserveFailureCause = (
return failure;
};
-export const decodeActionPayload = <
- PayloadSchema extends Schema.ConstraintDecoder,
- Payload,
->(
+export const decodeActionPayload = , Payload>(
schema: PayloadSchema,
payload: Payload,
): Effect.Effect => {
@@ -576,9 +519,7 @@ export const decodeActionResult = =>
- Schema.encodeUnknownEffect(Schema.make>(schema.ast))(
- result,
- ).pipe(
+ Schema.encodeUnknownEffect(Schema.make>(schema.ast))(result).pipe(
Effect.flatMap(Schema.decodeUnknownEffect(schema)),
Effect.mapError((cause) =>
preserveFailureCause(
diff --git a/app/packages/core-runtime/src/actions/error-schema.ts b/app/packages/core-runtime/src/actions/error-schema.ts
index 819e78eb2..91bf06ce0 100644
--- a/app/packages/core-runtime/src/actions/error-schema.ts
+++ b/app/packages/core-runtime/src/actions/error-schema.ts
@@ -1,10 +1,7 @@
import type { Cause } from 'effect';
import { Schema } from 'effect';
-export const actionErrorSchema = <
- const Tag extends string,
- const Fields extends Schema.Struct.Fields,
->(
+export const actionErrorSchema = (
tag: Tag,
fields: Fields,
) => {
diff --git a/app/packages/core-runtime/src/actions/errors.ts b/app/packages/core-runtime/src/actions/errors.ts
index 5f0398bae..bd8e8d422 100644
--- a/app/packages/core-runtime/src/actions/errors.ts
+++ b/app/packages/core-runtime/src/actions/errors.ts
@@ -1,11 +1,9 @@
import { Cause, Schema } from 'effect';
+
+import type { ModuleStateCheckUnavailableError, ModuleStateDeniedError } from '../modules/module-state-gate-errors.ts';
+import type { OperationContextError } from '../operations/errors.ts';
import { actionErrorSchema } from './error-schema.ts';
import type { ActionTransactionError } from './transaction-error.ts';
-import type {
- ModuleStateCheckUnavailableError,
- ModuleStateDeniedError,
-} from '../modules/module-state-gate-errors.ts';
-import type { OperationContextError } from '../operations/errors.ts';
export { ActionTransactionError } from './transaction-error.ts';
@@ -13,10 +11,7 @@ const safeReason = {
reason: Schema.String,
} as const;
-const ActionInvocationIdSchema = Schema.String.pipe(
- Schema.brand('ActionInvocationId'),
- Schema.decodeTo(Schema.String),
-);
+const ActionInvocationIdSchema = Schema.String.pipe(Schema.brand('ActionInvocationId'), Schema.decodeTo(Schema.String));
const ActionPayloadValidationErrorValue = actionErrorSchema('ActionPayloadValidationError', {
code: Schema.Literal('action_payload_invalid'),
@@ -32,16 +27,11 @@ const ActionResultValidationErrorValue = actionErrorSchema('ActionResultValidati
export type ActionResultValidationError = InstanceType;
export { ActionResultValidationErrorValue as ActionResultValidationError };
-const ActionTrustedContextValidationErrorValue = actionErrorSchema(
- 'ActionTrustedContextValidationError',
- {
- code: Schema.Literal('action_trusted_context_invalid'),
- ...safeReason,
- },
-);
-export type ActionTrustedContextValidationError = InstanceType<
- typeof ActionTrustedContextValidationErrorValue
->;
+const ActionTrustedContextValidationErrorValue = actionErrorSchema('ActionTrustedContextValidationError', {
+ code: Schema.Literal('action_trusted_context_invalid'),
+ ...safeReason,
+});
+export type ActionTrustedContextValidationError = InstanceType;
export { ActionTrustedContextValidationErrorValue as ActionTrustedContextValidationError };
const ActionIdempotencyKeyRequiredValue = actionErrorSchema('ActionIdempotencyKeyRequired', {
@@ -80,16 +70,11 @@ const ActionRequestHashConflictValue = actionErrorSchema('ActionRequestHashConfl
export type ActionRequestHashConflict = InstanceType;
export { ActionRequestHashConflictValue as ActionRequestHashConflict };
-const ActionInvocationPersistenceErrorValue = actionErrorSchema(
- 'ActionInvocationPersistenceError',
- {
- code: Schema.Literal('action_invocation_persistence_failed'),
- ...safeReason,
- },
-);
-export type ActionInvocationPersistenceError = InstanceType<
- typeof ActionInvocationPersistenceErrorValue
->;
+const ActionInvocationPersistenceErrorValue = actionErrorSchema('ActionInvocationPersistenceError', {
+ code: Schema.Literal('action_invocation_persistence_failed'),
+ ...safeReason,
+});
+export type ActionInvocationPersistenceError = InstanceType;
const ActionInvocationPersistenceErrorInternals = (() => {
let createWithCause: (
props: ConstructorParameters[0],
@@ -119,8 +104,7 @@ export { ActionInvocationPersistenceErrorClass as ActionInvocationPersistenceErr
// Core-only accessors: deliberately excluded from the package root exports.
export const createActionInvocationPersistenceErrorWithCause =
ActionInvocationPersistenceErrorInternals.createWithCause;
-export const getActionInvocationPersistenceErrorCause =
- ActionInvocationPersistenceErrorInternals.readCause;
+export const getActionInvocationPersistenceErrorCause = ActionInvocationPersistenceErrorInternals.readCause;
const ActionInvocationNotFoundValue = actionErrorSchema('ActionInvocationNotFound', {
code: Schema.Literal('action_invocation_not_found'),
diff --git a/app/packages/core-runtime/src/actions/events.ts b/app/packages/core-runtime/src/actions/events.ts
index a3fa824aa..967b9339d 100644
--- a/app/packages/core-runtime/src/actions/events.ts
+++ b/app/packages/core-runtime/src/actions/events.ts
@@ -1,10 +1,6 @@
import { Schema } from 'effect';
-import {
- decodedStringBrand,
- nonEmptyString,
- TargetModuleKeySchema,
- TargetResourceIdSchema,
-} from './string-schemas.ts';
+
+import { decodedStringBrand, nonEmptyString, TargetModuleKeySchema, TargetResourceIdSchema } from './string-schemas.ts';
const nonNegativeInteger = Schema.Finite.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0));
const EvidencePolicyKeySchema = decodedStringBrand(nonEmptyString, 'EvidencePolicyKey');
@@ -33,12 +29,7 @@ export type ActionAccessEvidencePolicy =
export const DataAccessEventSchema = Schema.Struct({
accessKind: Schema.Literals(['read', 'list', 'search', 'export', 'download']),
- evidenceCaptureMode: Schema.Literals([
- 'metadata_only',
- 'hash_only',
- 'redacted_payload',
- 'stored_artifact',
- ]),
+ evidenceCaptureMode: Schema.Literals(['metadata_only', 'hash_only', 'redacted_payload', 'stored_artifact']),
evidencePayloadJson: Schema.optionalKey(Schema.Json),
evidencePolicyKey: EvidencePolicyKeySchema,
occurredAt: Schema.optionalKey(Schema.Date),
@@ -74,10 +65,7 @@ export const DomainEventSchema = Schema.Struct({
export type DomainEvent = Schema.Schema.Type;
export type DeclaredDomainEvent = {
- readonly [EventType in keyof Contracts & string]: Omit<
- DomainEvent,
- 'eventType' | 'payloadJson'
- > & {
+ readonly [EventType in keyof Contracts & string]: Omit & {
readonly eventType: EventType;
readonly payloadJson: Contracts[EventType]['Type'];
};
@@ -91,9 +79,7 @@ export const OutboxMessageSchema = Schema.Struct({
export type OutboxMessage = Schema.Schema.Type;
-const domainEventReferenceBrand: unique symbol = Symbol(
- '@app/core-runtime/actions/events/DomainEventReference',
-);
+const domainEventReferenceBrand: unique symbol = Symbol('@app/core-runtime/actions/events/DomainEventReference');
/** Opaque reference produced only by one execution's Domain Event collector. */
export interface DomainEventReference {
diff --git a/app/packages/core-runtime/src/actions/policy.ts b/app/packages/core-runtime/src/actions/policy.ts
index 86f7c3e0f..f0660a3e8 100644
--- a/app/packages/core-runtime/src/actions/policy.ts
+++ b/app/packages/core-runtime/src/actions/policy.ts
@@ -1,5 +1,6 @@
import { Schema } from 'effect';
import type { Effect } from 'effect';
+
import type { ActionTransportMetadata, TrustedPrincipalContext } from './context.ts';
const policyReference = '__actionPolicyReference' as const;
@@ -30,10 +31,7 @@ const policyDeniedFields = {
};
const PolicyDeniedContract = Schema.TaggedStruct('PolicyDenied', policyDeniedFields);
type PolicyDeniedSelf = typeof PolicyDeniedContract.Type;
-const PolicyDeniedValue = Schema.TaggedError()(
- 'PolicyDenied',
- policyDeniedFields,
-);
+const PolicyDeniedValue = Schema.TaggedError()('PolicyDenied', policyDeniedFields);
export type PolicyDenied = InstanceType;
export { PolicyDeniedValue as PolicyDenied };
@@ -51,10 +49,7 @@ export interface GlobalActionPolicy extends ActionPolicyBase {
readonly scope: 'global';
}
-export interface MicroverticalActionPolicy<
- Payload,
- Owner extends string,
-> extends ActionPolicyBase {
+export interface MicroverticalActionPolicy extends ActionPolicyBase {
readonly owningModuleKey: Owner;
readonly scope: 'microvertical';
}
@@ -84,7 +79,10 @@ const requireStableIdentifier = (value: string, field: string): void => {
};
const registerPolicy = (policy: Policy): Readonly => {
- Object.defineProperty(policy, policyReference, { enumerable: false, value: true });
+ Object.defineProperty(policy, policyReference, {
+ enumerable: false,
+ value: true,
+ });
const frozen = Object.freeze(policy);
return frozen;
};
@@ -95,9 +93,7 @@ export const denyPolicy = (reasonCode: string, reason: string): PolicyDenied =>
return Object.freeze(new PolicyDeniedValue({ reason, reasonCode }));
};
-export const defineGlobalPolicy =