@cosmicdrift/kumiko-framework 0.401.0 → 0.403.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -2
- package/agents/api/framework-engine.md +29 -1
- package/agents/api/framework-ui-types.md +4 -0
- package/agents/api/renderer-web.md +1 -1
- package/agents/api/types.md +34 -0
- package/agents/api.md +1 -1
- package/agents/boot-rules.md +110 -0
- package/agents/examples/dashboard-stats.md +58 -3
- package/dist/db/replay-migration-sql.js +1 -1
- package/dist/engine/boot-validator/api-ext-rules.d.ts +27 -0
- package/dist/engine/boot-validator/api-ext-rules.js +27 -0
- package/dist/engine/boot-validator/api-ext.js +25 -12
- package/dist/engine/boot-validator/boot-check-rules.d.ts +7 -0
- package/dist/engine/boot-validator/boot-check-rules.js +7 -0
- package/dist/engine/boot-validator/boot-check.js +13 -1
- package/dist/engine/boot-validator/boot-phases-app.js +5 -0
- package/dist/engine/boot-validator/boot-report.d.ts +1 -0
- package/dist/engine/boot-validator/boot-report.js +58 -7
- package/dist/engine/boot-validator/boot-rule.d.ts +20 -0
- package/dist/engine/boot-validator/boot-rule.js +33 -2
- package/dist/engine/boot-validator/boot-rules.d.ts +246 -0
- package/dist/engine/boot-validator/boot-rules.js +19 -0
- package/dist/engine/boot-validator/card-meta-overflow-rules.d.ts +7 -0
- package/dist/engine/boot-validator/card-meta-overflow-rules.js +7 -0
- package/dist/engine/boot-validator/card-meta-overflow.js +5 -1
- package/dist/engine/boot-validator/dashboard-stat-navigate.d.ts +2 -0
- package/dist/engine/boot-validator/dashboard-stat-navigate.js +77 -0
- package/dist/engine/boot-validator/entity-list-screens-rules.d.ts +37 -0
- package/dist/engine/boot-validator/entity-list-screens-rules.js +37 -0
- package/dist/engine/boot-validator/entity-list-screens.js +86 -41
- package/dist/engine/boot-validator/i18n-keys-rules.d.ts +27 -0
- package/dist/engine/boot-validator/i18n-keys-rules.js +27 -0
- package/dist/engine/boot-validator/i18n-keys.js +27 -16
- package/dist/engine/boot-validator/named-filter-rules.d.ts +32 -0
- package/dist/engine/boot-validator/named-filter-rules.js +32 -0
- package/dist/engine/boot-validator/pii-retention-rules.d.ts +107 -0
- package/dist/engine/boot-validator/pii-retention-rules.js +107 -0
- package/dist/engine/boot-validator/pii-retention.js +76 -33
- package/dist/engine/boot-validator/projection-list-screens.js +13 -0
- package/dist/engine/boot-validator/screens-actions-rules.d.ts +7 -0
- package/dist/engine/boot-validator/screens-actions-rules.js +7 -0
- package/dist/engine/boot-validator/screens-actions.js +7 -3
- package/dist/engine/entity-handlers.d.ts +1 -0
- package/dist/engine/entity-handlers.js +13 -1
- package/dist/engine/factories.d.ts +2 -0
- package/dist/engine/index.d.ts +3 -0
- package/dist/engine/index.js +3 -0
- package/dist/engine/resolve-named-list-filters.d.ts +3 -0
- package/dist/engine/resolve-named-list-filters.js +30 -0
- package/dist/i18n/required-surface-keys.js +3 -0
- package/dist/schema-cli/validate-command.js +4 -1
- package/package.json +3 -3
- package/src/changes.json +18 -0
package/AGENTS.md
CHANGED
|
@@ -15,6 +15,7 @@ not exist in your version:
|
|
|
15
15
|
| `agents/api/<entry>.md` | Signatures of one entry point; options and screen types as property lists |
|
|
16
16
|
| `agents/api/types.md` | Types the entry points use but do not export, such as nested option shapes |
|
|
17
17
|
| `agents/widgets.md` | Every value export of `@cosmicdrift/kumiko-renderer/web` with its short description |
|
|
18
|
+
| `agents/boot-rules.md` | Every boot rule by id: summary, fix and source file; a boot violation links its entry |
|
|
18
19
|
| `agents/examples.md` | Index of the worked examples, one per concept |
|
|
19
20
|
| `agents/examples/<name>.md` | One tested sample app: README plus every source and test file |
|
|
20
21
|
|
|
@@ -33,7 +34,7 @@ Order of work for a new feature:
|
|
|
33
34
|
`agents/api.md` instead of `dist/*.d.ts`.
|
|
34
35
|
4. Build the feature with the declarative building blocks below.
|
|
35
36
|
5. Write an integration test against the real stack.
|
|
36
|
-
6. Run boot validation and the tests. Boot errors name the fix; apply it.
|
|
37
|
+
6. Run boot validation and the tests. Boot errors name the fix; apply it. Each violation carries a rule id whose entry is in `agents/boot-rules.md`.
|
|
37
38
|
|
|
38
39
|
## Mental model
|
|
39
40
|
|
|
@@ -293,7 +294,8 @@ produces code that compiles and then fails in production.
|
|
|
293
294
|
unknown field in a column, a nav pointing at a missing screen, an
|
|
294
295
|
encrypted field marked searchable. The message names the
|
|
295
296
|
feature, the place and the fix. Apply the fix the message names instead
|
|
296
|
-
of working around the check.
|
|
297
|
+
of working around the check. Each violation shows a rule id and, for
|
|
298
|
+
classified rules, a `Rule:` link to its entry in `agents/boot-rules.md`.
|
|
297
299
|
|
|
298
300
|
## I need X, use Y
|
|
299
301
|
|
|
@@ -11,6 +11,7 @@ function assertNotificationVariables(registry: Pick<Registry, "getAllNotificatio
|
|
|
11
11
|
function assignableAppRolesFromUsages(usages: { entityName: string; options?: Record<string, unknown> | undefined; }[]): AssignableAppRoles;
|
|
12
12
|
function assignableAppRolesOf(registry: Pick<Registry, "getExtensionUsages">): AssignableAppRoles;
|
|
13
13
|
function attachOutsideTransaction<T extends object, O extends object | undefined>(db: T, outsideTransaction: O): T & { outsideTransaction: O; };
|
|
14
|
+
function bootRuleError(rule: BootRule, details: { message: string; }): BootRuleViolation;
|
|
14
15
|
function bridgeStub(opts?: { user?: SessionUser; log?: HandlerObservability["log"]; dispatch?: Partial<HandlerDispatch>; services?: Partial<ContextServices>; }): Pick<HandlerContext, "dispatch" | "events" | "auth" | "obs" | "hasFeature" | "tz" | "locale" | "user" | "services" | "lifecycle" | "tenants">;
|
|
15
16
|
function buildAppSchema(registry: Registry, options?: BuildAppSchemaOptions): AppSchema;
|
|
16
17
|
function buildConfigFeatureSchema(registry: Registry): ConfigFeatureSchema;
|
|
@@ -20,6 +21,7 @@ function buildPipelineSteps<TPayload, TAwaits extends Record<string, AwaitedEven
|
|
|
20
21
|
function buildSessionRoles(globalRoles: string[], membershipRoles: string[]): string[];
|
|
21
22
|
function buildUpdateSchema(entity: EntityDefinition, currencies?: string[], excludedFields?: string[]): z.ZodObject<Record<string, z.ZodTypeAny>>;
|
|
22
23
|
function canActorAssignRole(actorRoles: string[], role: string, assignableAppRoles?: AssignableAppRoles): boolean;
|
|
24
|
+
function collectBootRuleViolations<T>(items: Iterable<T>, run: (item: T) => void): void;
|
|
23
25
|
function collectCardMetaOverflow(features: FeatureDefinition[]): CardMetaOverflow[];
|
|
24
26
|
function collectWriteHandlerQns(features: FeatureDefinition[]): Set<string>;
|
|
25
27
|
function compareVersions(a: string, b: string): number;
|
|
@@ -33,7 +35,7 @@ function createDecimalField<R extends true | false = false>(config: { precision:
|
|
|
33
35
|
function createDerivedField(spec: DerivedFieldDef): DerivedFieldDef; // Read-time computed field for `EntityDefinition.derivedFields` (NOT `fields`), never persisted.
|
|
34
36
|
function createEmbeddedField(schema: EmbeddedFieldDef["schema"], overrides?: Partial<Omit<EmbeddedFieldDef, "type" | "schema" | keyof ResolvedPiiFlags>> & PersonalAnnotationsNoFind): EmbeddedFieldDef;
|
|
35
37
|
function createEmbeddedListField(schema: EmbeddedFieldDef["schema"], overrides?: Partial<Omit<EmbeddedFieldDef, "type" | "schema" | "multiple" | keyof ResolvedPiiFlags>> & PersonalAnnotationsNoFind): EmbeddedFieldDef & { multiple: true; };
|
|
36
|
-
function createEntity<const F, const T extends EntityTenancy = "tenant">(def: { table?: string; fields: F; description?: string; softDelete?: boolean; systemStream?: boolean; eventVersion?: number; eventMigrations?: EntityDefinition["eventMigrations"]; searchWeight?: number; defaultCurrency?: string; transitions?: Readonly<Record<string, Readonly<Record<string, string[]>>>>; lockedWhen?: EntityDefinition["lockedWhen"]; indexes?: EntityIndexDef[]; idType?: "serial" | "uuid"; access?: EntityDefinition["access"]; parentRef?: EntityDefinition["parentRef"]; readGate?: EntityDefinition["readGate"]; retention?: RetentionDef; derivedFields?: EntityDefinition["derivedFields"]; tenancy?: T; transferable?: boolean; attachedFileAccess?: EntityDefinition["attachedFileAccess"]; }): F extends FieldsMap ? EntityDefinition<F, T> : never;
|
|
38
|
+
function createEntity<const F, const T extends EntityTenancy = "tenant">(def: { table?: string; fields: F; description?: string; softDelete?: boolean; systemStream?: boolean; eventVersion?: number; eventMigrations?: EntityDefinition["eventMigrations"]; searchWeight?: number; defaultCurrency?: string; transitions?: Readonly<Record<string, Readonly<Record<string, string[]>>>>; lockedWhen?: EntityDefinition["lockedWhen"]; indexes?: EntityIndexDef[]; idType?: "serial" | "uuid"; access?: EntityDefinition["access"]; parentRef?: EntityDefinition["parentRef"]; readGate?: EntityDefinition["readGate"]; retention?: RetentionDef; derivedFields?: EntityDefinition["derivedFields"]; namedFilters?: EntityDefinition["namedFilters"]; tenancy?: T; transferable?: boolean; attachedFileAccess?: EntityDefinition["attachedFileAccess"]; }): F extends FieldsMap ? EntityDefinition<F, T> : never;
|
|
37
39
|
function createEntityExecutor<E extends EntityDefinition>(entityName: string, entity: E): { table: EntityTable; executor: EventStoreExecutor<EntityRow<E>>; };
|
|
38
40
|
function createFileField(overrides?: Partial<Omit<FileFieldDef, "type" | keyof ResolvedPiiFlags>> & PersonalAnnotationsNoFind): FileFieldDef;
|
|
39
41
|
function createFilesField(overrides?: Partial<Omit<FilesFieldDef, "type" | keyof ResolvedPiiFlags>> & PersonalAnnotationsNoFind): FilesFieldDef;
|
|
@@ -135,6 +137,7 @@ function referenceGroupLabels(ctx: HandlerContext, user: SessionUser, sourceTabl
|
|
|
135
137
|
function requireTierResolverPlugin(usage: RegistrarExtensionRegistration): TierResolverPlugin;
|
|
136
138
|
function resolveAgentExposure(def: { description?: string; agent?: AgentHandlerHints; }, kind: "query" | "write"): AgentExposure; // Fail-closed: a handler without a `description` stays invisible to the agent unless it opts in explicitly.
|
|
137
139
|
function resolveEntityTransferAdjacency(entities: ReadonlyMap<string, EntityDefinition>, rootEntityType?: string): TransferAdjacency;
|
|
140
|
+
function resolveNamedListFilters(ctx: HandlerContext, user: SessionUser, entityName: string, name: string): Promise<NamedListFilterCondition[]>;
|
|
138
141
|
function runPipeline<TPayload, TData, TMap extends object = KumikoEventTypeMap>(pipelineDef: PipelineDef<TPayload, TData>, event: WriteEvent<TPayload>, handlerCtx: HandlerContext<TMap>, workflow?: PipelineCtx["workflow"], resumeFrom?: number): Promise<WriteResult<TData>>;
|
|
139
142
|
function runStepList<TPayload, TMap extends object = KumikoEventTypeMap>(steps: StepInstance[], event: WriteEvent<TPayload>, handlerCtx: HandlerContext<TMap>, stepsAcc: Record<string, unknown>, scopeAcc: Record<string, unknown>, workflow?: PipelineCtx["workflow"], resumeFrom?: number): Promise<StepListOutcome>; // Walk a step-list.
|
|
140
143
|
function sectionFieldSpecs(section: FieldsOrGroupsSection): EditFieldSpec[];
|
|
@@ -225,6 +228,14 @@ const templateVariable: { text; link; richText; money; date; percent; images; li
|
|
|
225
228
|
## Classes
|
|
226
229
|
|
|
227
230
|
```ts
|
|
231
|
+
class BootRuleViolation extends Error {
|
|
232
|
+
constructor(rule: BootRule, message: string);
|
|
233
|
+
name: unknown;
|
|
234
|
+
}
|
|
235
|
+
class BootValidationError extends Error {
|
|
236
|
+
constructor(violations: BootViolation[]);
|
|
237
|
+
name: unknown;
|
|
238
|
+
}
|
|
228
239
|
class ProbeFailure extends Error {
|
|
229
240
|
constructor(text: ProbeText);
|
|
230
241
|
}
|
|
@@ -315,6 +326,19 @@ type BeforeTransactionResult<TPrepared = unknown> = // Outcome of a write handle
|
|
|
315
326
|
| WriteFailure
|
|
316
327
|
;
|
|
317
328
|
type BootOptionName = "mail" | "kmsMode" | "fileStorage" | "billing" | "featureToggles" | "rateLimit"; // runProdApp options a stack can depend on; runProdApp checks the set at boot.
|
|
329
|
+
type BootRule = {
|
|
330
|
+
id: string;
|
|
331
|
+
summary: string;
|
|
332
|
+
fix: string;
|
|
333
|
+
};
|
|
334
|
+
type BootRuleTable = Readonly<Record<string, BootRule>>;
|
|
335
|
+
type BootViolation = {
|
|
336
|
+
ruleId: string;
|
|
337
|
+
check: string;
|
|
338
|
+
feature?: string;
|
|
339
|
+
message: string;
|
|
340
|
+
fix?: string;
|
|
341
|
+
};
|
|
318
342
|
type BundledListsOptions = { // One `table` block for every entityList, projectionList and relatedList of the bundled features.
|
|
319
343
|
table: ListTableOptions;
|
|
320
344
|
pinFirstColumn?: boolean; // Pins the first column left on every bundled list where no column is pinned.
|
|
@@ -427,6 +451,7 @@ type ContentCollectionDefinition = {
|
|
|
427
451
|
variableSchema?: Readonly<Record<string, string>>;
|
|
428
452
|
nav: {
|
|
429
453
|
label: string;
|
|
454
|
+
entryLabel?: string;
|
|
430
455
|
icon?: NavIconKey;
|
|
431
456
|
parent?: string;
|
|
432
457
|
order?: number;
|
|
@@ -540,6 +565,7 @@ type EntityDefinition<F extends FieldsMap = FieldsMap, T extends EntityTenancy =
|
|
|
540
565
|
readGate?: ReadGateDef; // Declarative read gate for rows readable only through other rows (a link to a visible host, a file whose owner is readable).
|
|
541
566
|
retention?: RetentionDef; // Default retention policy for this entity.
|
|
542
567
|
derivedFields?: DerivedFieldsMap; // Read-time computed fields, keyed by name.
|
|
568
|
+
namedFilters?: Readonly<Record<string, NamedListFilterDef>>; // Named work-queue rules keyed by id, selected via `namedFilter`; function-valued, never in the client schema.
|
|
543
569
|
description?: string; // Optional free-text semantics for the AI agent manifest — see TextFieldDef.description.
|
|
544
570
|
transferable?: boolean; // Opts the rows into `tenant-handover`, a grant-legitimated move from an anonymous tenant to the signed-up account's tenant.
|
|
545
571
|
attachedFileAccess?: "record-only"; // Attached files are readable only through the record's detail query: the files feature's privilegedRoles do not bypass it, the uploader check stays.
|
|
@@ -604,6 +630,7 @@ type EntityListScreenDefinition = {
|
|
|
604
630
|
defaultSort?: ListSortSpec;
|
|
605
631
|
defaultFilters?: Readonly<Record<string, string[] | boolean>>; // Initial facet selection, per filterable field: option values (string facets) or a boolean.
|
|
606
632
|
facets?: Readonly<Record<string, EntityListFacetConfig | false>>; // Per-facet display options for the entity's `filterable` fields, keyed by field name.
|
|
633
|
+
namedFilters?: NamedListFilterRef[]; // Named work-queue rules offered on this list: ids declared in `EntityDefinition.namedFilters`, `label` an i18n key.
|
|
607
634
|
emptyState?: ListEmptyStateSpec; // Replaces the default empty-list title/hint; the create button stays when the list can create.
|
|
608
635
|
createLabel?: string; // i18n key for the primary create button label (default `kumiko.actions.create`).
|
|
609
636
|
searchPlaceholder?: string; // i18n key for the search field placeholder (default `kumiko.list.search-placeholder`).
|
|
@@ -1327,6 +1354,7 @@ type ProjectionListScreenDefinition = {
|
|
|
1327
1354
|
paginated?: boolean; // Derived by buildAppSchema from the query handler's Zod schema (`cursor` or `offset` param present).
|
|
1328
1355
|
filter?: ScreenFilter; // Server-side filter, fixed on the screen — see `ScreenFilter` doc above (entityList).
|
|
1329
1356
|
facets?: ListFacetSpec[]; // User-toggleable facet dropdowns — see `ListFacetSpec` doc.
|
|
1357
|
+
namedFilters?: NamedListFilterRef[]; // Named work-queue rules the bound query understands: its Zod schema must have a `namedFilter` key (boot-checked) and is sent the selected id in every list and count query.
|
|
1330
1358
|
slots?: ScreenSlots;
|
|
1331
1359
|
access?: AccessRule;
|
|
1332
1360
|
};
|
|
@@ -252,6 +252,10 @@ type DashboardStatPanel = DashboardPanelQueryOptions & DashboardPanelGate & {
|
|
|
252
252
|
deltaToneField?: string;
|
|
253
253
|
icon?: PlatformComponent; // Static icon next to the label.
|
|
254
254
|
accentColor?: string; // Static CSS color value (e.g. "var(--color-debt)") for the icon chip, passed straight to the tile without a registry lookup.
|
|
255
|
+
navigate?: {
|
|
256
|
+
screen: string;
|
|
257
|
+
namedFilter?: string;
|
|
258
|
+
}; // Makes the tile a link to a list screen (qualified screen id).
|
|
255
259
|
};
|
|
256
260
|
type DashboardText = string | DashboardI18nText; // Plain string is displayed as-is; DashboardI18nText is translated by the renderer.
|
|
257
261
|
type DashboardTextBlock =
|
|
@@ -85,7 +85,7 @@ function SidebarProvider(props: React.ComponentProps<"div"> & { defaultOpen?: bo
|
|
|
85
85
|
function SidebarUser(props: SidebarUserProps): ReactNode;
|
|
86
86
|
function Sparkline(props: { points: number[]; className?: string; }): ReactNode;
|
|
87
87
|
function Stack(props: StackProps): ReactNode;
|
|
88
|
-
function StatCard(props: { icon?: ReactNode; label: string; value: string; sub?: string; tone?: StatTone; accentColor?: string; delta?: StatDelta; trend?: string; spark?: number[]; testId?: string; children?: ReactNode; }): ReactNode; // Icon-bearing metric tile with an optional delta chip, trend line, and sparkline.
|
|
88
|
+
function StatCard(props: { icon?: ReactNode; label: string; value: string; sub?: string; tone?: StatTone; accentColor?: string; delta?: StatDelta; trend?: string; spark?: number[]; testId?: string; link?: StatLink; children?: ReactNode; }): ReactNode; // Icon-bearing metric tile with an optional delta chip, trend line, and sparkline.
|
|
89
89
|
function StatusBadge(props: { tone: StatusBadgeTone; children: ReactNode; className?: string; testId?: string; }): ReactNode; // Pill badge with a status dot.
|
|
90
90
|
function StatusBarChart(props: { entries: StatusBarEntry[]; ariaLabel: string; startLabel?: string; endLabel?: string; highlightLast?: boolean; dense?: boolean; testId?: string; }): ReactNode; // Status bar strip (e.g. 90-day uptime): variable-height bars with gradient fade + tick line on top; the last entry gets a "now" accent stripe.
|
|
91
91
|
function StepBar(props: { steps: string[]; currentIndex: number; compactLabel: string; onStepSelect?: (index: number) => void; doneSteps?: boolean[]; selectableSteps?: "done" | "all"; narrowLayout?: "label" | "steps"; orientation?: "horizontal" | "vertical"; heading?: string; description?: string; subtitles?: (string | undefined)[]; upNext?: { heading: string; title: string; subtitle?: string; }; testId?: string; compactTestId?: string; }): ReactNode; // Wizard step overview — numbered chips with a connector line between them.
|
package/agents/api/types.md
CHANGED
|
@@ -2676,6 +2676,15 @@ interface ResultTableFooterRow {
|
|
|
2676
2676
|
}
|
|
2677
2677
|
```
|
|
2678
2678
|
|
|
2679
|
+
## packages/renderer/src/web/widgets/stat.tsx
|
|
2680
|
+
|
|
2681
|
+
```ts
|
|
2682
|
+
type StatLink = { // Makes a tile a link; `onPress` is the SPA navigation, `href` serves modified clicks and a11y.
|
|
2683
|
+
href: string;
|
|
2684
|
+
onPress: () => void;
|
|
2685
|
+
};
|
|
2686
|
+
```
|
|
2687
|
+
|
|
2679
2688
|
## packages/testing/src/bundled-injection.ts
|
|
2680
2689
|
|
|
2681
2690
|
```ts
|
|
@@ -4377,6 +4386,30 @@ type MultiStreamApplyContext<TMap extends object = KumikoEventTypeMap> = {
|
|
|
4377
4386
|
};
|
|
4378
4387
|
```
|
|
4379
4388
|
|
|
4389
|
+
## packages/types/src/named-list-filters.ts
|
|
4390
|
+
|
|
4391
|
+
```ts
|
|
4392
|
+
type NamedListFilterCondition = {
|
|
4393
|
+
field: string;
|
|
4394
|
+
op: NamedListFilterOp;
|
|
4395
|
+
value: unknown;
|
|
4396
|
+
};
|
|
4397
|
+
type NamedListFilterDef = { // A work-queue rule defined once on the entity: the list and every count query resolve the same conditions.
|
|
4398
|
+
access?: AccessRule;
|
|
4399
|
+
filters: (scope: NamedListFilterScope) => NamedListFilterCondition[] | Promise<NamedListFilterCondition[]>;
|
|
4400
|
+
};
|
|
4401
|
+
type NamedListFilterOp = "eq" | "ne" | "lt" | "gt" | "lte" | "gte" | "in";
|
|
4402
|
+
type NamedListFilterRef = { // Screen-side reference to an entity-defined named filter; `label` is an i18n key.
|
|
4403
|
+
id: string;
|
|
4404
|
+
label: string;
|
|
4405
|
+
};
|
|
4406
|
+
type NamedListFilterScope = {
|
|
4407
|
+
ctx: HandlerContext;
|
|
4408
|
+
user: SessionUser;
|
|
4409
|
+
now: Temporal.Instant;
|
|
4410
|
+
};
|
|
4411
|
+
```
|
|
4412
|
+
|
|
4380
4413
|
## packages/types/src/nav.ts
|
|
4381
4414
|
|
|
4382
4415
|
```ts
|
|
@@ -4783,6 +4816,7 @@ type MetricSpec = {
|
|
|
4783
4816
|
navigate?: MetricNavigate;
|
|
4784
4817
|
tone?: DashboardStatTone; // Tile color as on dashboard stat panels; not applied while the value is 0.
|
|
4785
4818
|
icon?: NavIconKey; // Icon chip of the tile; only drawn with `metricsStyle: "tiles"`.
|
|
4819
|
+
format?: FormatSpec; // Formats the displayed value (e.g. `date`, `enumOption`); the tone still resolves on the raw value.
|
|
4786
4820
|
};
|
|
4787
4821
|
type RecordHeaderSpec = {
|
|
4788
4822
|
title: string;
|
package/agents/api.md
CHANGED
|
@@ -8,7 +8,7 @@ Format: one line per function or constant, object and options types as property
|
|
|
8
8
|
|
|
9
9
|
| Import path | File | Exports |
|
|
10
10
|
| --- | --- | --- |
|
|
11
|
-
| `@cosmicdrift/kumiko-framework/engine` | `api/framework-engine.md` |
|
|
11
|
+
| `@cosmicdrift/kumiko-framework/engine` | `api/framework-engine.md` | 420 |
|
|
12
12
|
| `@cosmicdrift/kumiko-framework/errors` | `api/framework-errors.md` | 25 |
|
|
13
13
|
| `@cosmicdrift/kumiko-framework/ui-types` | `api/framework-ui-types.md` | 154 |
|
|
14
14
|
| `@cosmicdrift/kumiko-framework/db` | `api/framework-db.md` | 150 |
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<!-- Generated by `bun run gen:agent-docs` from boot rule tables (`*-rules.ts`, `boot-rules.ts`). Do not edit. -->
|
|
2
|
+
|
|
3
|
+
# Boot rules
|
|
4
|
+
|
|
5
|
+
`validateBoot` reports every violation as `- [<id>] <message>`, followed by a `Fix:` and a `Rule:` line that links the entry below. Violations with an `unclassified/<check>` id have no entry yet. `kumiko-schema validate --json` returns the same ids.
|
|
6
|
+
|
|
7
|
+
## api
|
|
8
|
+
|
|
9
|
+
| Rule | Summary | Fix | Source |
|
|
10
|
+
| --- | --- | --- | --- |
|
|
11
|
+
| <a id="api-provider-not-mounted"></a>`api/provider-not-mounted` | The feature exposing a used API is part of the app's feature list. | Register the provider feature in the app's feature list. | `packages/framework/src/engine/boot-validator/api-ext-rules.ts` |
|
|
12
|
+
| <a id="api-provider-not-required"></a>`api/provider-not-required` | A feature using an API lists the exposing feature in requires or optionalRequires. | Add the exposing feature via r.requires({ features: [...] }). | `packages/framework/src/engine/boot-validator/api-ext-rules.ts` |
|
|
13
|
+
| <a id="api-unknown-api"></a>`api/unknown-api` | Every r.usesApi name is exposed by some mounted feature. | Mount the feature that exposes the API or fix the API name. | `packages/framework/src/engine/boot-validator/api-ext-rules.ts` |
|
|
14
|
+
|
|
15
|
+
## boot-check
|
|
16
|
+
|
|
17
|
+
| Rule | Summary | Fix | Source |
|
|
18
|
+
| --- | --- | --- | --- |
|
|
19
|
+
| <a id="boot-check-failed"></a>`boot-check/failed` | A feature's r.bootCheck invariant holds for the mounted feature set. | Satisfy the invariant named in the message or unmount the feature that declares it. | `packages/framework/src/engine/boot-validator/boot-check-rules.ts` |
|
|
20
|
+
|
|
21
|
+
## entity-list
|
|
22
|
+
|
|
23
|
+
| Rule | Summary | Fix | Source |
|
|
24
|
+
| --- | --- | --- | --- |
|
|
25
|
+
| <a id="entity-list-default-sort-not-listed"></a>`entity-list/default-sort-not-listed` | defaultSort.field is one of the listed columns. | Pick a field from columns or add it there. | `packages/framework/src/engine/boot-validator/entity-list-screens-rules.ts` |
|
|
26
|
+
| <a id="entity-list-default-sort-not-sortable"></a>`entity-list/default-sort-not-sortable` | The defaultSort column is sortable. | Mark the field sortable or pick another defaultSort field. | `packages/framework/src/engine/boot-validator/entity-list-screens-rules.ts` |
|
|
27
|
+
| <a id="entity-list-default-sort-required"></a>`entity-list/default-sort-required` | A list with active search or filterable fields declares a defaultSort. | Set defaultSort: { field, direction } on the screen. | `packages/framework/src/engine/boot-validator/entity-list-screens-rules.ts` |
|
|
28
|
+
| <a id="entity-list-detail-without-navigate"></a>`entity-list/detail-without-navigate` | A list whose entity has a detail screen offers a navigate row action. | Add a navigate rowAction with id "view" or "edit". | `packages/framework/src/engine/boot-validator/entity-list-screens-rules.ts` |
|
|
29
|
+
| <a id="entity-list-searchable-false"></a>`entity-list/searchable-false` | Operator lists are searchable unless they are on the searchable-false whitelist. | Set searchable: true or add the screen id to the whitelist. | `packages/framework/src/engine/boot-validator/entity-list-screens-rules.ts` |
|
|
30
|
+
| <a id="entity-list-unknown-column"></a>`entity-list/unknown-column` | Every column without a label names an entity field, derived field or row-meta column. | Fix the column field name or give the column a label. | `packages/framework/src/engine/boot-validator/entity-list-screens-rules.ts` |
|
|
31
|
+
| <a id="entity-list-unknown-named-filter"></a>`entity-list/unknown-named-filter` | Every screen namedFilters id is declared in the entity's namedFilters. | Declare the rule in EntityDefinition.namedFilters or remove the id from the screen. | `packages/framework/src/engine/boot-validator/entity-list-screens-rules.ts` |
|
|
32
|
+
|
|
33
|
+
## extension
|
|
34
|
+
|
|
35
|
+
| Rule | Summary | Fix | Source |
|
|
36
|
+
| --- | --- | --- | --- |
|
|
37
|
+
| <a id="extension-provider-not-required"></a>`extension/provider-not-required` | A feature using an extension lists the defining feature in requires or optionalRequires. | Add the defining feature via r.requires({ features: [...] }). | `packages/framework/src/engine/boot-validator/api-ext-rules.ts` |
|
|
38
|
+
| <a id="extension-unknown-extension"></a>`extension/unknown-extension` | Every extension a feature uses is defined by some mounted feature. | Register the feature that defines the extension or fix the extension name. | `packages/framework/src/engine/boot-validator/api-ext-rules.ts` |
|
|
39
|
+
|
|
40
|
+
## i18n
|
|
41
|
+
|
|
42
|
+
| Rule | Summary | Fix | Source |
|
|
43
|
+
| --- | --- | --- | --- |
|
|
44
|
+
| <a id="i18n-locale-inconsistent"></a>`i18n/locale-inconsistent` | Every locale a feature uses is maintained for every one of its translation keys. | Add the missing locale to the key, or move that locale into a locale package. | `packages/framework/src/engine/boot-validator/i18n-keys-rules.ts` |
|
|
45
|
+
| <a id="i18n-missing-feature-key"></a>`i18n/missing-feature-key` | Every translation key a feature surface requires is defined. | Add the key via r.translations(...) in the feature. | `packages/framework/src/engine/boot-validator/i18n-keys-rules.ts` |
|
|
46
|
+
| <a id="i18n-missing-field-value-translation"></a>`i18n/missing-field-value-translation` | With requireFieldValueTranslations, select options and embedded cells are translated. | Add the keys via r.translations(...) in the owning feature. | `packages/framework/src/engine/boot-validator/i18n-keys-rules.ts` |
|
|
47
|
+
| <a id="i18n-missing-required-locale"></a>`i18n/missing-required-locale` | Every translation key exists in every locale the app requires. | Load the matching locale package or add the missing translations. | `packages/framework/src/engine/boot-validator/i18n-keys-rules.ts` |
|
|
48
|
+
| <a id="i18n-missing-settings-hub-key"></a>`i18n/missing-settings-hub-key` | Every translation key the generated settings hub requires is defined. | Add the key via r.translations(...) in the owning feature. | `packages/framework/src/engine/boot-validator/i18n-keys-rules.ts` |
|
|
49
|
+
|
|
50
|
+
## list
|
|
51
|
+
|
|
52
|
+
| Rule | Summary | Fix | Source |
|
|
53
|
+
| --- | --- | --- | --- |
|
|
54
|
+
| <a id="list-card-meta-overflow"></a>`list/card-meta-overflow` | A card list shows at most the maximum number of meta columns. | Reduce each list to the maximum number of meta columns or mark the others hideOnNarrow: true. | `packages/framework/src/engine/boot-validator/card-meta-overflow-rules.ts` |
|
|
55
|
+
|
|
56
|
+
## named-filter
|
|
57
|
+
|
|
58
|
+
| Rule | Summary | Fix | Source |
|
|
59
|
+
| --- | --- | --- | --- |
|
|
60
|
+
| <a id="named-filter-projection-query-accepts-named-filter"></a>`named-filter/projection-query-accepts-named-filter` | A projectionList with namedFilters binds a query whose schema has a namedFilter key. | Add namedFilter: z.string().optional() to the query schema, resolve it with resolveNamedListFilters and pass the conditions into executor.list filters, the only path that applies field-read and PII guards. | `packages/framework/src/engine/boot-validator/named-filter-rules.ts` |
|
|
61
|
+
| <a id="named-filter-stat-navigate-filter-declared"></a>`named-filter/stat-navigate-filter-declared` | A stat navigate.namedFilter is declared in the target screen's namedFilters. | Add { id, label } to namedFilters of the target screen. | `packages/framework/src/engine/boot-validator/named-filter-rules.ts` |
|
|
62
|
+
| <a id="named-filter-stat-navigate-same-named-filter"></a>`named-filter/stat-navigate-same-named-filter` | A stat's params.namedFilter equals navigate.namedFilter. | Set params.namedFilter to the same id as navigate.namedFilter. | `packages/framework/src/engine/boot-validator/named-filter-rules.ts` |
|
|
63
|
+
| <a id="named-filter-stat-navigate-same-query"></a>`named-filter/stat-navigate-same-query` | A stat with navigate counts with the target list's own query. | Set the stat query to the target list's query. | `packages/framework/src/engine/boot-validator/named-filter-rules.ts` |
|
|
64
|
+
| <a id="named-filter-stat-navigate-static-filter"></a>`named-filter/stat-navigate-static-filter` | A stat repeats the target screen's static filter in params.filter. | Set params.filter to the target screen's filter. | `packages/framework/src/engine/boot-validator/named-filter-rules.ts` |
|
|
65
|
+
| <a id="named-filter-stat-navigate-target"></a>`named-filter/stat-navigate-target` | A stat navigate.screen resolves to an entityList or projectionList screen. | Point navigate.screen at a registered list screen. | `packages/framework/src/engine/boot-validator/named-filter-rules.ts` |
|
|
66
|
+
|
|
67
|
+
## pii
|
|
68
|
+
|
|
69
|
+
| Rule | Summary | Fix | Source |
|
|
70
|
+
| --- | --- | --- | --- |
|
|
71
|
+
| <a id="pii-lookupable-non-text"></a>`pii/lookupable-non-text` | Blind-index lookups apply to text fields only. | Use a text field or set find: "none". | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
72
|
+
| <a id="pii-lookupable-without-subject"></a>`pii/lookupable-without-subject` | A blind-index lookup requires a subject annotation on the field. | Add a personal annotation or set find: "none". | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
73
|
+
| <a id="pii-multiple-subject-annotations"></a>`pii/multiple-subject-annotations` | A field carries at most one subject-key annotation. | Keep a single personal annotation on the field. | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
74
|
+
| <a id="pii-personal-jsonb-custom-fields"></a>`pii/personal-jsonb-custom-fields` | The custom-fields jsonb column cannot carry a personal annotation. | Drop the personal annotation. | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
75
|
+
| <a id="pii-personal-jsonb-flag"></a>`pii/personal-jsonb-flag` | A personal jsonb field cannot be sortable, filterable, searchable or lookupable, its value is a ciphertext string. | Remove the flag or the personal annotation. | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
76
|
+
| <a id="pii-personal-jsonb-index"></a>`pii/personal-jsonb-index` | A personal jsonb field is not part of an entity index. | Drop the index or the personal annotation. | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
77
|
+
| <a id="pii-personal-of-invalid-when-absent"></a>`pii/personal-of-invalid-when-absent` | personal.whenAbsent is either "tenant" or "self". | Remove whenAbsent or set it to "tenant" or "self". | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
78
|
+
| <a id="pii-personal-of-missing-owner-name"></a>`pii/personal-of-missing-owner-name` | personal: { of } names the owner field. | Pass the name of the owner field: personal: { of: "<ownerField>" }. | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
79
|
+
| <a id="pii-personal-of-optional-owner"></a>`pii/personal-of-optional-owner` | An optional owner field of personal: { of } declares a whenAbsent fallback. | Set whenAbsent to "tenant" or "self", or make the owner field required. | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
80
|
+
| <a id="pii-personal-of-owner-type"></a>`pii/personal-of-owner-type` | The owner field named by personal: { of } is a reference or text field. | Use a reference or text field as owner. | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
81
|
+
| <a id="pii-personal-of-unknown-owner"></a>`pii/personal-of-unknown-owner` | The owner field named by personal: { of } exists on the entity. | Point personal.of at an existing field of the entity. | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
82
|
+
| <a id="pii-plaintext-user-reference"></a>`pii/plaintext-user-reference` | A reference to the user entity is never plaintext. | Use the default reference field or personal: "ref" instead of personal: false. | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
83
|
+
| <a id="pii-sensitive-searchable"></a>`pii/sensitive-searchable` | A sensitive field is never searchable. | Drop searchable, or add writeOnly to never return the value. | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
84
|
+
| <a id="pii-sensitive-without-ciphertext"></a>`pii/sensitive-without-ciphertext` | A sensitive field is stored as ciphertext at rest. | Add a subject annotation or encrypted: true. | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
85
|
+
| <a id="pii-subject-key-sortable"></a>`pii/subject-key-sortable` | A field with a subject-key annotation is not sortable, its column is ciphertext. | Drop sortable, use find: "exact" for equality lookups, or keep the field plaintext. | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
86
|
+
| <a id="pii-text-without-personal-stance"></a>`pii/text-without-personal-stance` | Every text and longText field declares whether it holds personal data. | Declare a personal stance, or personal: false with a reason stating why the value is not personal data. | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
87
|
+
| <a id="pii-write-only-conflicting-flag"></a>`pii/write-only-conflicting-flag` | A writeOnly field cannot be defaulted, searched, sorted, filtered or looked up. | Remove either writeOnly or the conflicting flag. | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
88
|
+
| <a id="pii-write-only-embedded-sub-field"></a>`pii/write-only-embedded-sub-field` | writeOnly is only supported on top-level text fields, not on embedded sub-fields. | Remove writeOnly from the sub-field or declare it on a top-level text field. | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
89
|
+
| <a id="pii-write-only-non-text"></a>`pii/write-only-non-text` | writeOnly applies to text fields only. | Use a text field or remove writeOnly. | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
90
|
+
| <a id="pii-write-only-without-sensitive"></a>`pii/write-only-without-sensitive` | A writeOnly field is also sensitive. | Declare the field with find: "secret". | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
91
|
+
|
|
92
|
+
## retention
|
|
93
|
+
|
|
94
|
+
| Rule | Summary | Fix | Source |
|
|
95
|
+
| --- | --- | --- | --- |
|
|
96
|
+
| <a id="retention-unknown-reference"></a>`retention/unknown-reference` | retention.reference names an existing field or a framework timestamp. | Point retention.reference at an existing field or a framework-managed timestamp. | `packages/framework/src/engine/boot-validator/pii-retention-rules.ts` |
|
|
97
|
+
|
|
98
|
+
## screen
|
|
99
|
+
|
|
100
|
+
| Rule | Summary | Fix | Source |
|
|
101
|
+
| --- | --- | --- | --- |
|
|
102
|
+
| <a id="screen-action-icon"></a>`screen/action-icon` | Every screen action resolves to a registered icon. | Set icon to a registered NAV_ICONS key or use an action id that resolves one via the default icon map. | `packages/framework/src/engine/boot-validator/screens-actions-rules.ts` |
|
|
103
|
+
|
|
104
|
+
## user-data-rights
|
|
105
|
+
|
|
106
|
+
| Rule | Summary | Fix | Source |
|
|
107
|
+
| --- | --- | --- | --- |
|
|
108
|
+
| <a id="user-data-rights-empty-hook"></a>`user-data-rights/empty-hook` | An EXT_USER_DATA registration declares at least one of an export or a delete hook. | Register at least one hook. | `packages/bundled-features/src/user-data-rights/boot-rules.ts` |
|
|
109
|
+
| <a id="user-data-rights-export-without-delete"></a>`user-data-rights/export-without-delete` | An entity exported via EXT_USER_DATA also registers a delete hook. | Add a delete hook, or a documented no-op delete if erasure is handled elsewhere. | `packages/bundled-features/src/user-data-rights/boot-rules.ts` |
|
|
110
|
+
| <a id="user-data-rights-missing-hook"></a>`user-data-rights/missing-hook` | An entity with user-subject fields registers an EXT_USER_DATA hook. | Register r.useExtension(EXT_USER_DATA, ...) for the entity, or a documented no-op hook if it is covered elsewhere. | `packages/bundled-features/src/user-data-rights/boot-rules.ts` |
|
|
@@ -36,6 +36,11 @@ query and the fields to read, the query does the counting.
|
|
|
36
36
|
- **Fallback by reason**: `fallback: { forbidden, notFound }` renders a screen on
|
|
37
37
|
403/404 of the gate query; other reasons get the default notice or an error
|
|
38
38
|
banner with retry.
|
|
39
|
+
- **Named filter shared by tile and list**: the entity declares `namedFilters`,
|
|
40
|
+
the `ticket-queue` entityList lists the id in `namedFilters`, and the
|
|
41
|
+
`open-work` stat counts through the list's own query with `params.namedFilter`
|
|
42
|
+
and `navigate`. A click opens the queue filtered by the same rule, so tile and
|
|
43
|
+
list cannot drift.
|
|
39
44
|
|
|
40
45
|
## When to reach for it
|
|
41
46
|
|
|
@@ -73,6 +78,7 @@ import {
|
|
|
73
78
|
} from "@cosmicdrift/kumiko-testing/stack";
|
|
74
79
|
import {
|
|
75
80
|
TICKET_CAPABILITIES_QUERY,
|
|
81
|
+
TICKET_LIST_QUERY,
|
|
76
82
|
TICKET_STATS_QUERY,
|
|
77
83
|
ticketEntity,
|
|
78
84
|
ticketFeature,
|
|
@@ -100,11 +106,11 @@ describe("dashboard screen", () => {
|
|
|
100
106
|
const stats = screen.panels.flatMap((panel) =>
|
|
101
107
|
panel.kind === "stat-group" ? panel.stats : panel.kind === "stat" ? [panel] : [],
|
|
102
108
|
);
|
|
103
|
-
expect(stats.map((s) => s.valueField)).toEqual(["open", "closed", "total"]);
|
|
104
|
-
|
|
109
|
+
expect(stats.map((s) => s.valueField)).toEqual(["open", "closed", "total", "total"]);
|
|
110
|
+
const statsQueryStats = stats.filter((s) => s.query === TICKET_STATS_QUERY);
|
|
105
111
|
|
|
106
112
|
const result = await stack.http.queryOk<Record<string, unknown>>(TICKET_STATS_QUERY, {}, admin);
|
|
107
|
-
for (const stat of
|
|
113
|
+
for (const stat of statsQueryStats) expect(result).toHaveProperty(stat.valueField);
|
|
108
114
|
});
|
|
109
115
|
});
|
|
110
116
|
|
|
@@ -165,6 +171,28 @@ describe("record-bound dashboard with a gated detail panel", () => {
|
|
|
165
171
|
});
|
|
166
172
|
});
|
|
167
173
|
});
|
|
174
|
+
|
|
175
|
+
describe("named filter shared by the dashboard tile and the list", () => {
|
|
176
|
+
test("the tile total equals the filtered list total over HTTP", async () => {
|
|
177
|
+
const dashboard = ticketFeature.screens["ticket-dashboard"];
|
|
178
|
+
if (dashboard?.type !== "dashboard") throw new Error("ticket-dashboard missing");
|
|
179
|
+
const tile = dashboard.panels.find(
|
|
180
|
+
(panel) => panel.kind === "stat" && panel.id === "open-work",
|
|
181
|
+
);
|
|
182
|
+
if (tile?.kind !== "stat") throw new Error("open-work tile missing");
|
|
183
|
+
expect(tile.navigate).toEqual({ screen: "ticket-queue", namedFilter: "open-work" });
|
|
184
|
+
|
|
185
|
+
const counted = await stack.http.queryOk<{ total: number }>(tile.query, tile.params, admin);
|
|
186
|
+
const listed = await stack.http.queryOk<{ rows: unknown[]; total: number }>(
|
|
187
|
+
TICKET_LIST_QUERY,
|
|
188
|
+
{ namedFilter: "open-work", totalCount: true, limit: 50 },
|
|
189
|
+
admin,
|
|
190
|
+
);
|
|
191
|
+
expect(counted.total).toBe(listed.total);
|
|
192
|
+
expect(listed.rows).toHaveLength(listed.total);
|
|
193
|
+
expect(listed.total).toBeGreaterThan(0);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
168
196
|
```
|
|
169
197
|
|
|
170
198
|
### src/feature.ts
|
|
@@ -193,10 +221,15 @@ export const ticketEntity = createEntity({
|
|
|
193
221
|
title: createTextField({ personal: false, reason: "is_business_data", required: true }),
|
|
194
222
|
status: createSelectField({ options: TICKET_STATUSES, default: "open", required: true }),
|
|
195
223
|
},
|
|
224
|
+
// Defined once: the queue list and the dashboard tile both read it.
|
|
225
|
+
namedFilters: {
|
|
226
|
+
"open-work": { filters: () => [{ field: "status", op: "eq", value: "open" }] },
|
|
227
|
+
},
|
|
196
228
|
});
|
|
197
229
|
|
|
198
230
|
export const TICKET_STATS_QUERY = "tickets:query:ticket:stats";
|
|
199
231
|
export const TICKET_CAPABILITIES_QUERY = "tickets:query:ticket:capabilities";
|
|
232
|
+
export const TICKET_LIST_QUERY = "tickets:query:ticket:list";
|
|
200
233
|
|
|
201
234
|
const readerRoles = { roles: ["Admin", "User"] } as const;
|
|
202
235
|
|
|
@@ -281,6 +314,9 @@ export const ticketFeature = defineFeature("tickets", (r) => {
|
|
|
281
314
|
"tickets.dashboard.open": { en: "Open" },
|
|
282
315
|
"tickets.dashboard.closed": { en: "Closed" },
|
|
283
316
|
"tickets.dashboard.total": { en: "All tickets" },
|
|
317
|
+
"tickets.dashboard.open-work": { en: "Open work" },
|
|
318
|
+
"tickets.filter.open-work": { en: "Open work" },
|
|
319
|
+
"screen:ticket-queue.title": { en: "Ticket queue" },
|
|
284
320
|
},
|
|
285
321
|
});
|
|
286
322
|
|
|
@@ -316,6 +352,16 @@ export const ticketFeature = defineFeature("tickets", (r) => {
|
|
|
316
352
|
query: TICKET_STATS_QUERY,
|
|
317
353
|
valueField: "total",
|
|
318
354
|
},
|
|
355
|
+
// Counts through the list's own query with the same named filter, then links to that list.
|
|
356
|
+
{
|
|
357
|
+
kind: "stat",
|
|
358
|
+
id: "open-work",
|
|
359
|
+
label: "tickets.dashboard.open-work",
|
|
360
|
+
query: TICKET_LIST_QUERY,
|
|
361
|
+
valueField: "total",
|
|
362
|
+
params: { namedFilter: "open-work", limit: 1, totalCount: true },
|
|
363
|
+
navigate: { screen: "ticket-queue", namedFilter: "open-work" },
|
|
364
|
+
},
|
|
319
365
|
],
|
|
320
366
|
access: readerRoles,
|
|
321
367
|
});
|
|
@@ -324,6 +370,15 @@ export const ticketFeature = defineFeature("tickets", (r) => {
|
|
|
324
370
|
// button, and a fallback chosen by reason.
|
|
325
371
|
const gate = { query: TICKET_CAPABILITIES_QUERY, field: "canClose", eq: true } as const;
|
|
326
372
|
|
|
373
|
+
r.screen({
|
|
374
|
+
id: "ticket-queue",
|
|
375
|
+
type: "entityList",
|
|
376
|
+
entity: "ticket",
|
|
377
|
+
columns: ["title", "status"],
|
|
378
|
+
namedFilters: [{ id: "open-work", label: "tickets.filter.open-work" }],
|
|
379
|
+
access: readerRoles,
|
|
380
|
+
});
|
|
381
|
+
|
|
327
382
|
// The list doubles as the fallback target: it has no visibleWhen of its own.
|
|
328
383
|
r.screen({
|
|
329
384
|
id: "ticket-list",
|
|
@@ -151,7 +151,7 @@ function applyStatement(schema, statement, context) {
|
|
|
151
151
|
// skip: DO block fully handled above
|
|
152
152
|
return;
|
|
153
153
|
}
|
|
154
|
-
const createAs = statement.match(new RegExp(`^CREATE\\s+(?:UNLOGGED\\s+)?TABLE\\s+(IF NOT EXISTS\\s+)?${IDENT}
|
|
154
|
+
const createAs = statement.match(new RegExp(`^CREATE\\s+(?:UNLOGGED\\s+)?TABLE\\s+(IF NOT EXISTS\\s+)?${IDENT}(?:\\s*\\(([^)]*)\\)\\s*|\\s+)AS\\b([\\s\\S]*)$`, "i"));
|
|
155
155
|
if (createAs?.[2] !== undefined && createAs[4] !== undefined) {
|
|
156
156
|
if (!(createAs[1] !== undefined && schema.has(createAs[2]))) {
|
|
157
157
|
const explicitColumns = createAs[3] === undefined ? undefined : parseColumnNames(createAs[3]);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export declare const API_EXT_RULES: {
|
|
2
|
+
readonly unknownApi: {
|
|
3
|
+
readonly id: "api/unknown-api";
|
|
4
|
+
readonly summary: "Every r.usesApi name is exposed by some mounted feature.";
|
|
5
|
+
readonly fix: "Mount the feature that exposes the API or fix the API name.";
|
|
6
|
+
};
|
|
7
|
+
readonly providerNotRequired: {
|
|
8
|
+
readonly id: "api/provider-not-required";
|
|
9
|
+
readonly summary: "A feature using an API lists the exposing feature in requires or optionalRequires.";
|
|
10
|
+
readonly fix: "Add the exposing feature via r.requires({ features: [...] }).";
|
|
11
|
+
};
|
|
12
|
+
readonly providerNotMounted: {
|
|
13
|
+
readonly id: "api/provider-not-mounted";
|
|
14
|
+
readonly summary: "The feature exposing a used API is part of the app's feature list.";
|
|
15
|
+
readonly fix: "Register the provider feature in the app's feature list.";
|
|
16
|
+
};
|
|
17
|
+
readonly unknownExtension: {
|
|
18
|
+
readonly id: "extension/unknown-extension";
|
|
19
|
+
readonly summary: "Every extension a feature uses is defined by some mounted feature.";
|
|
20
|
+
readonly fix: "Register the feature that defines the extension or fix the extension name.";
|
|
21
|
+
};
|
|
22
|
+
readonly extensionProviderNotRequired: {
|
|
23
|
+
readonly id: "extension/provider-not-required";
|
|
24
|
+
readonly summary: "A feature using an extension lists the defining feature in requires or optionalRequires.";
|
|
25
|
+
readonly fix: "Add the defining feature via r.requires({ features: [...] }).";
|
|
26
|
+
};
|
|
27
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const API_EXT_RULES = {
|
|
2
|
+
unknownApi: {
|
|
3
|
+
id: "api/unknown-api",
|
|
4
|
+
summary: "Every r.usesApi name is exposed by some mounted feature.",
|
|
5
|
+
fix: "Mount the feature that exposes the API or fix the API name.",
|
|
6
|
+
},
|
|
7
|
+
providerNotRequired: {
|
|
8
|
+
id: "api/provider-not-required",
|
|
9
|
+
summary: "A feature using an API lists the exposing feature in requires or optionalRequires.",
|
|
10
|
+
fix: "Add the exposing feature via r.requires({ features: [...] }).",
|
|
11
|
+
},
|
|
12
|
+
providerNotMounted: {
|
|
13
|
+
id: "api/provider-not-mounted",
|
|
14
|
+
summary: "The feature exposing a used API is part of the app's feature list.",
|
|
15
|
+
fix: "Register the provider feature in the app's feature list.",
|
|
16
|
+
},
|
|
17
|
+
unknownExtension: {
|
|
18
|
+
id: "extension/unknown-extension",
|
|
19
|
+
summary: "Every extension a feature uses is defined by some mounted feature.",
|
|
20
|
+
fix: "Register the feature that defines the extension or fix the extension name.",
|
|
21
|
+
},
|
|
22
|
+
extensionProviderNotRequired: {
|
|
23
|
+
id: "extension/provider-not-required",
|
|
24
|
+
summary: "A feature using an extension lists the defining feature in requires or optionalRequires.",
|
|
25
|
+
fix: "Add the defining feature via r.requires({ features: [...] }).",
|
|
26
|
+
},
|
|
27
|
+
};
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { createFallbackLogger } from "../../logging/utils.js";
|
|
2
|
+
import { API_EXT_RULES } from "./api-ext-rules.js";
|
|
3
|
+
import { collectBootRuleViolations } from "./boot-report.js";
|
|
4
|
+
import { bootRuleError } from "./boot-rule.js";
|
|
2
5
|
const log = createFallbackLogger("kumiko:boot");
|
|
3
6
|
// --- Cross-feature API exposure / usage matching ---
|
|
4
7
|
//
|
|
@@ -13,42 +16,52 @@ const log = createFallbackLogger("kumiko:boot");
|
|
|
13
16
|
// apiNames must be globally unique across features; validateBoot() checks
|
|
14
17
|
// this before the per-feature walk.
|
|
15
18
|
export function validateApiExposureMatching(feature, allExposedApis, featureMap) {
|
|
16
|
-
|
|
19
|
+
collectBootRuleViolations(feature.usedApis, (apiName) => {
|
|
17
20
|
const providerFeature = allExposedApis.get(apiName);
|
|
18
21
|
if (!providerFeature) {
|
|
19
22
|
const known = [...allExposedApis.keys()].sort().join(", ") || "(none)";
|
|
20
|
-
throw
|
|
23
|
+
throw bootRuleError(API_EXT_RULES.unknownApi, {
|
|
24
|
+
message: `[Feature ${feature.name}] r.usesApi("${apiName}") but no feature exposes that API. Known exposed APIs: ${known}`,
|
|
25
|
+
});
|
|
21
26
|
}
|
|
22
27
|
if (providerFeature === feature.name) {
|
|
23
28
|
log.warn(`[Feature ${feature.name}] r.usesApi("${apiName}") on its own r.exposesApi — typically a refactor leftover. Call the impl directly instead.`);
|
|
24
|
-
|
|
29
|
+
return;
|
|
25
30
|
}
|
|
26
31
|
const allDeps = [...feature.requires, ...feature.optionalRequires];
|
|
27
32
|
if (!allDeps.includes(providerFeature)) {
|
|
28
|
-
throw
|
|
33
|
+
throw bootRuleError(API_EXT_RULES.providerNotRequired, {
|
|
34
|
+
message: `[Feature ${feature.name}] r.usesApi("${apiName}") is exposed by "${providerFeature}" but feature is not in requires/optionalRequires. Add r.requires({ features: ["${providerFeature}"] }).`,
|
|
35
|
+
});
|
|
29
36
|
}
|
|
30
37
|
// Sanity: the provider feature must exist in this app's feature set.
|
|
31
38
|
// This should always hold when allExposedApis was built from `features`,
|
|
32
39
|
// but remains defensive for unclear constructor paths.
|
|
33
40
|
if (!featureMap.has(providerFeature)) {
|
|
34
|
-
throw
|
|
41
|
+
throw bootRuleError(API_EXT_RULES.providerNotMounted, {
|
|
42
|
+
message: `[Feature ${feature.name}] internal: r.usesApi("${apiName}") points to provider "${providerFeature}" which is not in feature map. Register the provider feature in the app's feature list.`,
|
|
43
|
+
});
|
|
35
44
|
}
|
|
36
|
-
}
|
|
45
|
+
});
|
|
37
46
|
}
|
|
38
47
|
// --- Extension usage validation ---
|
|
39
48
|
export function validateExtensionUsages(feature, extensionProviders) {
|
|
40
|
-
|
|
49
|
+
collectBootRuleViolations(feature.extensionUsages, (usage) => {
|
|
41
50
|
const providerFeature = extensionProviders.get(usage.extensionName);
|
|
42
51
|
if (!providerFeature) {
|
|
43
|
-
throw
|
|
52
|
+
throw bootRuleError(API_EXT_RULES.unknownExtension, {
|
|
53
|
+
message: `Feature "${feature.name}" uses extension "${usage.extensionName}" on entity "${usage.entityName}" but no feature defines that extension. Register the feature that defines it or fix the extension name.`,
|
|
54
|
+
});
|
|
44
55
|
}
|
|
45
|
-
// self-extension is legitimate: requires(self) would be circular.
|
|
46
56
|
if (providerFeature === feature.name) {
|
|
47
|
-
|
|
57
|
+
// skip: self-extension is legitimate, requires(self) would be circular
|
|
58
|
+
return;
|
|
48
59
|
}
|
|
49
60
|
const allDeps = [...feature.requires, ...feature.optionalRequires];
|
|
50
61
|
if (!allDeps.includes(providerFeature)) {
|
|
51
|
-
throw
|
|
62
|
+
throw bootRuleError(API_EXT_RULES.extensionProviderNotRequired, {
|
|
63
|
+
message: `Feature "${feature.name}" uses extension "${usage.extensionName}" but missing requires("${providerFeature}"). Add r.requires({ features: ["${providerFeature}"] }).`,
|
|
64
|
+
});
|
|
52
65
|
}
|
|
53
|
-
}
|
|
66
|
+
});
|
|
54
67
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const BOOT_CHECK_RULES: {
|
|
2
|
+
readonly failed: {
|
|
3
|
+
readonly id: "boot-check/failed";
|
|
4
|
+
readonly summary: "A feature's r.bootCheck invariant holds for the mounted feature set.";
|
|
5
|
+
readonly fix: "Satisfy the invariant named in the message or unmount the feature that declares it.";
|
|
6
|
+
};
|
|
7
|
+
};
|