@cosmicdrift/kumiko-framework 0.133.0 → 0.135.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/package.json +2 -2
- package/src/engine/__tests__/boot-validator-dashboard.test.ts +136 -2
- package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +3 -19
- package/src/engine/__tests__/factories-time.test.ts +2 -66
- package/src/engine/__tests__/{visual-tree-patterns.test.ts → tree-actions-patterns.test.ts} +4 -84
- package/src/engine/boot-validator/entity-handler.ts +5 -4
- package/src/engine/boot-validator/screens-nav.ts +112 -16
- package/src/engine/define-feature.ts +3 -17
- package/src/engine/factories.ts +2 -49
- package/src/engine/feature-ast/extractors/index.ts +1 -4
- package/src/engine/feature-ast/extractors/round6.ts +2 -36
- package/src/engine/feature-ast/parse.ts +1 -4
- package/src/engine/feature-ast/patch.ts +2 -5
- package/src/engine/feature-ast/patterns.ts +0 -14
- package/src/engine/feature-ast/render.ts +1 -9
- package/src/engine/index.ts +0 -1
- package/src/engine/pattern-library/__tests__/library.test.ts +0 -3
- package/src/engine/pattern-library/library.ts +0 -19
- package/src/engine/registry.ts +6 -16
- package/src/engine/types/feature.ts +3 -33
- package/src/engine/types/fields.ts +5 -4
- package/src/engine/types/index.ts +5 -0
- package/src/engine/types/screen.ts +64 -1
- package/src/engine/types/workspace.ts +0 -7
- package/src/errors/classes.ts +1 -1
- package/src/errors/field-issue.ts +0 -3
- package/src/errors/index.ts +0 -1
- package/src/i18n/required-surface-keys.ts +29 -10
- package/src/observability/__tests__/recording-tracer.test.ts +6 -4
- package/src/observability/noop-provider.ts +0 -9
- package/src/observability/recording-tracer.ts +0 -12
- package/src/observability/types/span.ts +0 -6
- package/src/time/tz-context.ts +1 -1
- package/src/ui-types/index.ts +5 -0
- package/src/bun-db/__tests__/bun-test-stack.ts +0 -6
- package/src/db/row-helpers.ts +0 -4
- package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +0 -184
package/src/engine/factories.ts
CHANGED
|
@@ -230,8 +230,8 @@ export function createDateField<R extends true | false = false>(
|
|
|
230
230
|
* (createdAt, loginAt, actualPickupAt). Temporal.Instant intern.
|
|
231
231
|
*
|
|
232
232
|
* Mit `locatedBy: "<name>Tz"` wird das Feld zum Wall-Clock-Pair eines
|
|
233
|
-
* Termins an einem Ort — bevorzuge dafür
|
|
234
|
-
*
|
|
233
|
+
* Termins an einem Ort — bevorzuge dafür `createLocatedTimestampField()`,
|
|
234
|
+
* das EIN atomares Feld statt eines lose verdrahteten Pairs erzeugt.
|
|
235
235
|
*/
|
|
236
236
|
export function createTimestampField<R extends true | false = false>(
|
|
237
237
|
overrides?: Partial<Omit<TimestampFieldDef, "type" | "required">> & { required?: R },
|
|
@@ -300,53 +300,6 @@ export function createLocatedTimestampField<R extends true | false = false>(
|
|
|
300
300
|
} as LocatedTimestampFieldDef & { required: R }; // @cast-boundary engine-payload
|
|
301
301
|
}
|
|
302
302
|
|
|
303
|
-
/**
|
|
304
|
-
* @deprecated Verwende stattdessen `createLocatedTimestampField()` —
|
|
305
|
-
* EIN Field-Type statt zwei lose Pair-Felder. Migration: ersetze
|
|
306
|
-
* `...locatedTimestamp("pickup")` durch `pickup: createLocatedTimestampField()`.
|
|
307
|
-
*
|
|
308
|
-
* Wall-Clock-Termin an einem Ort — Helper der ZWEI verbundene Felder
|
|
309
|
-
* erzeugt. Bevorzugte Form für jeden Date-Wert mit Location-Bezug
|
|
310
|
-
* (Pickup, Delivery, Meeting, Schedule).
|
|
311
|
-
*
|
|
312
|
-
* ```ts
|
|
313
|
-
* r.entity("order", {
|
|
314
|
-
* fields: {
|
|
315
|
-
* ...locatedTimestamp("pickup"), // → pickupAt + pickupTz
|
|
316
|
-
* ...locatedTimestamp("delivery"), // → deliveryAt + deliveryTz
|
|
317
|
-
* },
|
|
318
|
-
* });
|
|
319
|
-
* ```
|
|
320
|
-
*
|
|
321
|
-
* Zur Laufzeit wird:
|
|
322
|
-
* - DB: `<name>_at TIMESTAMPTZ` (UTC) + `<name>_tz TEXT` (IANA-Name)
|
|
323
|
-
* - JSON: `{ <name>At: "2026-04-03T10:00:00", <name>Tz: "Europe/Lisbon" }`
|
|
324
|
-
* (Wall-Clock OHNE Offset, plus IANA-Name — zwei getrennte Felder)
|
|
325
|
-
* - Reducer/Apply: kann mit Temporal.ZonedDateTime arbeiten
|
|
326
|
-
*
|
|
327
|
-
* Optionen pro Feld (z.B. `required` auf den At-Teil) per zweitem Argument.
|
|
328
|
-
*/
|
|
329
|
-
export function locatedTimestamp(
|
|
330
|
-
name: string,
|
|
331
|
-
overrides?: { readonly required?: boolean; readonly access?: TimestampFieldDef["access"] },
|
|
332
|
-
): Readonly<Record<string, TimestampFieldDef | TzFieldDef>> {
|
|
333
|
-
const atName = `${name}At`;
|
|
334
|
-
const tzName = `${name}Tz`;
|
|
335
|
-
return {
|
|
336
|
-
[atName]: {
|
|
337
|
-
type: "timestamp",
|
|
338
|
-
locatedBy: tzName,
|
|
339
|
-
required: overrides?.required,
|
|
340
|
-
access: overrides?.access,
|
|
341
|
-
},
|
|
342
|
-
[tzName]: {
|
|
343
|
-
type: "tz",
|
|
344
|
-
required: overrides?.required,
|
|
345
|
-
access: overrides?.access,
|
|
346
|
-
},
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
|
|
350
303
|
export function createFileField(overrides?: Partial<Omit<FileFieldDef, "type">>): FileFieldDef {
|
|
351
304
|
return { type: "file", ...overrides };
|
|
352
305
|
}
|
|
@@ -58,10 +58,7 @@ export {
|
|
|
58
58
|
extractUnmanagedTable,
|
|
59
59
|
extractUsesApi,
|
|
60
60
|
} from "./round5";
|
|
61
|
-
export {
|
|
62
|
-
extractTree,
|
|
63
|
-
extractTreeActions,
|
|
64
|
-
} from "./round6";
|
|
61
|
+
export { extractTreeActions } from "./round6";
|
|
65
62
|
export type { ExtractOutput } from "./shared";
|
|
66
63
|
export {
|
|
67
64
|
fail,
|
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import type { CallExpression, SourceFile } from "ts-morph";
|
|
2
2
|
import type { TreeActionDef } from "../../types/tree-node";
|
|
3
|
-
import type { TreeActionsPattern
|
|
3
|
+
import type { TreeActionsPattern } from "../patterns";
|
|
4
4
|
import { sourceLocationFromNode } from "../source-location";
|
|
5
|
-
import {
|
|
6
|
-
type ExtractOutput,
|
|
7
|
-
fail,
|
|
8
|
-
findFunctionLiteral,
|
|
9
|
-
isPlainObject,
|
|
10
|
-
ok,
|
|
11
|
-
readDataLiteralNode,
|
|
12
|
-
} from "./shared";
|
|
5
|
+
import { type ExtractOutput, fail, isPlainObject, ok, readDataLiteralNode } from "./shared";
|
|
13
6
|
|
|
14
7
|
export function extractTreeActions(
|
|
15
8
|
call: CallExpression,
|
|
@@ -37,30 +30,3 @@ export function extractTreeActions(
|
|
|
37
30
|
definitions: definitions as Readonly<Record<string, TreeActionDef>>,
|
|
38
31
|
});
|
|
39
32
|
}
|
|
40
|
-
|
|
41
|
-
export function extractTree(
|
|
42
|
-
call: CallExpression,
|
|
43
|
-
sourceFile: SourceFile,
|
|
44
|
-
): ExtractOutput<TreePattern> {
|
|
45
|
-
const arg = call.getArguments()[0];
|
|
46
|
-
if (!arg) {
|
|
47
|
-
return fail(
|
|
48
|
-
"tree",
|
|
49
|
-
sourceLocationFromNode(call, sourceFile),
|
|
50
|
-
"expected a tree-provider function as first argument",
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
const fn = findFunctionLiteral(arg);
|
|
54
|
-
if (!fn) {
|
|
55
|
-
return fail(
|
|
56
|
-
"tree",
|
|
57
|
-
sourceLocationFromNode(call, sourceFile),
|
|
58
|
-
"first argument must be an inline arrow function or function expression",
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
return ok({
|
|
62
|
-
kind: "tree",
|
|
63
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
64
|
-
providerBody: sourceLocationFromNode(fn, sourceFile),
|
|
65
|
-
});
|
|
66
|
-
}
|
|
@@ -56,7 +56,6 @@ import {
|
|
|
56
56
|
extractSystemScope,
|
|
57
57
|
extractToggleable,
|
|
58
58
|
extractTranslations,
|
|
59
|
-
extractTree,
|
|
60
59
|
extractTreeActions,
|
|
61
60
|
extractUiHints,
|
|
62
61
|
extractUnmanagedTable,
|
|
@@ -364,11 +363,9 @@ function dispatchExtractor(
|
|
|
364
363
|
return extractExposesApi(call, sourceFile);
|
|
365
364
|
case "unmanagedTable":
|
|
366
365
|
return extractUnmanagedTable(call, sourceFile);
|
|
367
|
-
// Round 6 —
|
|
366
|
+
// Round 6 — Tree-Actions pattern
|
|
368
367
|
case "treeActions":
|
|
369
368
|
return extractTreeActions(call, sourceFile);
|
|
370
|
-
case "tree":
|
|
371
|
-
return extractTree(call, sourceFile);
|
|
372
369
|
// Round 7 — env-schema contract (opaque, Zod-expression argument)
|
|
373
370
|
case "envSchema":
|
|
374
371
|
return extractEnvSchema(call, sourceFile);
|
|
@@ -84,8 +84,7 @@ export type PatternId =
|
|
|
84
84
|
| { readonly kind: "config" }
|
|
85
85
|
| { readonly kind: "translations" }
|
|
86
86
|
| { readonly kind: "authClaims" }
|
|
87
|
-
| { readonly kind: "treeActions" }
|
|
88
|
-
| { readonly kind: "tree" };
|
|
87
|
+
| { readonly kind: "treeActions" };
|
|
89
88
|
|
|
90
89
|
// =============================================================================
|
|
91
90
|
// Change ops — generic apply API
|
|
@@ -277,10 +276,9 @@ export const SINGLETON_KINDS: ReadonlySet<PatternId["kind"]> = new Set([
|
|
|
277
276
|
"config",
|
|
278
277
|
"translations",
|
|
279
278
|
"authClaims",
|
|
280
|
-
//
|
|
279
|
+
// Tree-Actions slot — at-most-one per feature, mirrors the registrar's
|
|
281
280
|
// only-once-guard in define-feature.ts.
|
|
282
281
|
"treeActions",
|
|
283
|
-
"tree",
|
|
284
282
|
]);
|
|
285
283
|
|
|
286
284
|
/**
|
|
@@ -336,7 +334,6 @@ function callMatchesId(call: CallExpression, id: PatternId): boolean {
|
|
|
336
334
|
case "translations":
|
|
337
335
|
case "authClaims":
|
|
338
336
|
case "treeActions":
|
|
339
|
-
case "tree":
|
|
340
337
|
return true;
|
|
341
338
|
|
|
342
339
|
case "entity":
|
|
@@ -445,18 +445,6 @@ export type AuthClaimsPattern = {
|
|
|
445
445
|
readonly fnBody: SourceLocation;
|
|
446
446
|
};
|
|
447
447
|
|
|
448
|
-
// `r.tree(provider)` — the feature's top-level tree provider: a subscribe
|
|
449
|
-
// function (emit-fn in, unsubscribe-fn out) that feeds workspaces with
|
|
450
|
-
// `navigation: "tree"`. Features without `r.tree()` are invisible there —
|
|
451
|
-
// provider presence IS the filter, there is no workspace mapping.
|
|
452
|
-
// Closure-only, no header form: the Designer renders a read-only code
|
|
453
|
-
// block, the AI patcher overwrites the span verbatim.
|
|
454
|
-
export type TreePattern = {
|
|
455
|
-
readonly kind: "tree";
|
|
456
|
-
readonly source: SourceLocation;
|
|
457
|
-
readonly providerBody: SourceLocation;
|
|
458
|
-
};
|
|
459
|
-
|
|
460
448
|
// `r.httpRoute(definition)` — mounts an HTTP route owned by the feature,
|
|
461
449
|
// outside the dispatcher pipeline (not under `/api/write|query|batch`) —
|
|
462
450
|
// for RSS/Atom feeds, OG images, OpenAPI specs and similar. Duplicate
|
|
@@ -625,7 +613,6 @@ export type FeaturePattern =
|
|
|
625
613
|
| DefineEventPattern
|
|
626
614
|
| EventMigrationPattern
|
|
627
615
|
| ExtendsRegistrarPattern
|
|
628
|
-
| TreePattern
|
|
629
616
|
| EnvSchemaPattern
|
|
630
617
|
// Catch-all
|
|
631
618
|
| UnknownPattern;
|
|
@@ -684,7 +671,6 @@ export function getEditability(pattern: FeaturePattern): Editability {
|
|
|
684
671
|
return "mixed";
|
|
685
672
|
case "authClaims":
|
|
686
673
|
case "extendsRegistrar":
|
|
687
|
-
case "tree":
|
|
688
674
|
case "envSchema":
|
|
689
675
|
case "uiHints":
|
|
690
676
|
case "unknown":
|
|
@@ -49,7 +49,6 @@ import type {
|
|
|
49
49
|
ToggleablePattern,
|
|
50
50
|
TranslationsPattern,
|
|
51
51
|
TreeActionsPattern,
|
|
52
|
-
TreePattern,
|
|
53
52
|
UiHintsPattern,
|
|
54
53
|
UnknownPattern,
|
|
55
54
|
UseExtensionPattern,
|
|
@@ -139,8 +138,6 @@ export function renderPattern(pattern: FeaturePattern): string {
|
|
|
139
138
|
return renderExposesApi(pattern);
|
|
140
139
|
case "treeActions":
|
|
141
140
|
return renderTreeActions(pattern);
|
|
142
|
-
case "tree":
|
|
143
|
-
return renderTree(pattern);
|
|
144
141
|
case "envSchema":
|
|
145
142
|
return renderEnvSchema(pattern);
|
|
146
143
|
case "unknown":
|
|
@@ -433,16 +430,11 @@ function renderAuthClaims(p: AuthClaimsPattern): string {
|
|
|
433
430
|
return `r.authClaims(${p.fnBody.raw});`;
|
|
434
431
|
}
|
|
435
432
|
|
|
436
|
-
//
|
|
437
|
-
// renderWorkspace), tree is opaque-only (mirrors renderAuthClaims).
|
|
433
|
+
// treeActions is a static object-literal (mirrors renderWorkspace).
|
|
438
434
|
function renderTreeActions(p: TreeActionsPattern): string {
|
|
439
435
|
return `r.treeActions(${renderValue(p.definitions)});`;
|
|
440
436
|
}
|
|
441
437
|
|
|
442
|
-
function renderTree(p: TreePattern): string {
|
|
443
|
-
return `r.tree(${p.providerBody.raw});`;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
438
|
function renderHttpRoute(p: HttpRoutePattern): string {
|
|
447
439
|
const lines: string[] = ["r.httpRoute({"];
|
|
448
440
|
lines.push(` method: ${JSON.stringify(p.method)},`);
|
package/src/engine/index.ts
CHANGED
|
@@ -123,7 +123,6 @@ export {
|
|
|
123
123
|
createTextField,
|
|
124
124
|
createTimestampField,
|
|
125
125
|
createTzField,
|
|
126
|
-
locatedTimestamp,
|
|
127
126
|
} from "./factories";
|
|
128
127
|
// AST inspection + patching pipeline — used by the CLI scaffolder, the
|
|
129
128
|
// Designer (C5/C6), and the AI-Builder (L2). See feature-ast/index.ts
|
|
@@ -60,7 +60,6 @@ const ALL_KINDS: FeaturePatternKind[] = [
|
|
|
60
60
|
"usesApi",
|
|
61
61
|
"exposesApi",
|
|
62
62
|
"treeActions",
|
|
63
|
-
"tree",
|
|
64
63
|
"envSchema",
|
|
65
64
|
"unknown",
|
|
66
65
|
];
|
|
@@ -351,8 +350,6 @@ function makePlaceholderPattern(kind: FeaturePatternKind): FeaturePattern {
|
|
|
351
350
|
};
|
|
352
351
|
case "treeActions":
|
|
353
352
|
return { kind, source: PLACEHOLDER_LOC, definitions: {} };
|
|
354
|
-
case "tree":
|
|
355
|
-
return { kind, source: PLACEHOLDER_LOC, providerBody: PLACEHOLDER_BODY_LOC };
|
|
356
353
|
case "envSchema":
|
|
357
354
|
return { kind, source: PLACEHOLDER_LOC, schemaBody: PLACEHOLDER_BODY_LOC };
|
|
358
355
|
case "unknown":
|
|
@@ -1102,24 +1102,6 @@ const treeActionsSchema: PatternFormSchema = {
|
|
|
1102
1102
|
],
|
|
1103
1103
|
};
|
|
1104
1104
|
|
|
1105
|
-
const treeSchema: PatternFormSchema = {
|
|
1106
|
-
kind: "tree",
|
|
1107
|
-
label: { en: "Tree provider", de: "Tree-Provider" },
|
|
1108
|
-
summary: { en: "Subscribe-Function emitting top-level Visual-Tree nodes." },
|
|
1109
|
-
category: "ui",
|
|
1110
|
-
editability: "opaque",
|
|
1111
|
-
singleton: true,
|
|
1112
|
-
fields: [
|
|
1113
|
-
{
|
|
1114
|
-
path: "providerBody",
|
|
1115
|
-
label: { en: "Provider body (source)", de: "Provider-Body (Source)" },
|
|
1116
|
-
input: "code-block",
|
|
1117
|
-
language: "typescript",
|
|
1118
|
-
readOnly: true,
|
|
1119
|
-
},
|
|
1120
|
-
],
|
|
1121
|
-
};
|
|
1122
|
-
|
|
1123
1105
|
const envSchemaSchema: PatternFormSchema = {
|
|
1124
1106
|
kind: "envSchema",
|
|
1125
1107
|
label: { en: "Env schema", de: "Env-Schema" },
|
|
@@ -1200,7 +1182,6 @@ export const PATTERN_LIBRARY: Readonly<Record<FeaturePatternKind, PatternFormSch
|
|
|
1200
1182
|
usesApi: usesApiSchema,
|
|
1201
1183
|
exposesApi: exposesApiSchema,
|
|
1202
1184
|
treeActions: treeActionsSchema,
|
|
1203
|
-
tree: treeSchema,
|
|
1204
1185
|
envSchema: envSchemaSchema,
|
|
1205
1186
|
unknown: unknownSchema,
|
|
1206
1187
|
} satisfies Readonly<Record<FeaturePatternKind, PatternFormSchema>>;
|
package/src/engine/registry.ts
CHANGED
|
@@ -57,7 +57,6 @@ import type {
|
|
|
57
57
|
SecretKeyDefinition,
|
|
58
58
|
TranslationKeys,
|
|
59
59
|
TreeActionDef,
|
|
60
|
-
TreeChildrenSubscribe,
|
|
61
60
|
UnmanagedTableDef,
|
|
62
61
|
WorkspaceDefinition,
|
|
63
62
|
WriteHandlerDef,
|
|
@@ -291,12 +290,10 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
291
290
|
const navsByWorkspace = new Map<string, string[]>();
|
|
292
291
|
let defaultWorkspace: WorkspaceDefinition | undefined;
|
|
293
292
|
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
// setup-export-handle, siehe FeatureRegistrar.treeActions docs).
|
|
299
|
-
const treeProvidersMap = new Map<string, TreeChildrenSubscribe>();
|
|
293
|
+
// Tree-Actions — keyed by declaring feature name (NOT qualified; ein
|
|
294
|
+
// Feature liefert genau eine erased Action-Map (compile-time-typed
|
|
295
|
+
// Variante geht über setup-export-handle, siehe
|
|
296
|
+
// FeatureRegistrar.treeActions docs).
|
|
300
297
|
const treeActionsMap = new Map<string, Readonly<Record<string, TreeActionDef>>>();
|
|
301
298
|
|
|
302
299
|
// Local alias for readability — `qualifyEntityName` is the shared helper
|
|
@@ -786,12 +783,9 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
786
783
|
}
|
|
787
784
|
}
|
|
788
785
|
|
|
789
|
-
//
|
|
790
|
-
// registrar). Erased
|
|
786
|
+
// Tree-Actions slot — at-most-one per feature (only-once-guard im
|
|
787
|
+
// registrar). Erased Map für Runtime-Lookup; compile-time-typed
|
|
791
788
|
// Surface läuft über FeatureDefinition.exports (TreeActionsHandle).
|
|
792
|
-
if (feature.treeProvider !== undefined) {
|
|
793
|
-
treeProvidersMap.set(feature.name, feature.treeProvider);
|
|
794
|
-
}
|
|
795
789
|
if (feature.treeActions !== undefined) {
|
|
796
790
|
treeActionsMap.set(feature.name, feature.treeActions);
|
|
797
791
|
}
|
|
@@ -1693,10 +1687,6 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
1693
1687
|
return defaultWorkspace;
|
|
1694
1688
|
},
|
|
1695
1689
|
|
|
1696
|
-
getTreeProviders(): ReadonlyMap<string, TreeChildrenSubscribe> {
|
|
1697
|
-
return treeProvidersMap;
|
|
1698
|
-
},
|
|
1699
|
-
|
|
1700
1690
|
getTreeActions(featureName: string): Readonly<Record<string, TreeActionDef>> | undefined {
|
|
1701
1691
|
return treeActionsMap.get(featureName);
|
|
1702
1692
|
},
|
|
@@ -70,7 +70,7 @@ import type {
|
|
|
70
70
|
} from "./projection";
|
|
71
71
|
import type { EntityRelations, RelationDefinition } from "./relations";
|
|
72
72
|
import type { ScreenDefinition } from "./screen";
|
|
73
|
-
import type { TreeActionDef, TreeActionsHandle
|
|
73
|
+
import type { TreeActionDef, TreeActionsHandle } from "./tree-node";
|
|
74
74
|
import type { WorkspaceDefinition } from "./workspace";
|
|
75
75
|
|
|
76
76
|
// --- Metrics (declared by features via r.metric()) ---
|
|
@@ -352,13 +352,6 @@ export type FeatureDefinition = {
|
|
|
352
352
|
// feature exports via setup-return — buildTarget consumes the handle,
|
|
353
353
|
// not this slot. See visual-tree.md A5 + A7.
|
|
354
354
|
readonly treeActions?: Readonly<Record<string, TreeActionDef>>;
|
|
355
|
-
// Tree-Provider declared via r.tree(). At-most-one per feature.
|
|
356
|
-
// Provider liefert die Top-Level-Knoten dieses Features im Visual-
|
|
357
|
-
// Workspace (navigation: "tree"). Subscribe-Form mit lazy-Eval: erst
|
|
358
|
-
// beim Mount des Workspaces aufgerufen, kann Updates emittieren.
|
|
359
|
-
// Feature ohne treeProvider ist im Visual-Workspace unsichtbar
|
|
360
|
-
// (Zero-Whitelist-Filter aus visual-tree.md A2).
|
|
361
|
-
readonly treeProvider?: TreeChildrenSubscribe;
|
|
362
355
|
// HTTP-Routes declared via r.httpRoute(). Index is "METHOD path"
|
|
363
356
|
// (z.B. "GET /feed.xml") — eindeutig pro Feature. Die App-Server-
|
|
364
357
|
// Boot-Stage iteriert getAllHttpRoutes() und mountet jede Route auf
|
|
@@ -771,20 +764,6 @@ export type FeatureRegistrar<TFeature extends string = string> = {
|
|
|
771
764
|
// k8s-secret hints) goes into `.meta({ kumiko: { pulumi: {...} } })`
|
|
772
765
|
// — see framework/env/index.ts for the meta-shape.
|
|
773
766
|
envSchema(schema: z.ZodObject<z.ZodRawShape>): void;
|
|
774
|
-
|
|
775
|
-
// Register the tree-provider for this feature — the Subscribe-Function
|
|
776
|
-
// that emits the top-level Tree-Knoten when the Visual-Workspace
|
|
777
|
-
// (navigation: "tree") mounts. At-most-one call per feature.
|
|
778
|
-
//
|
|
779
|
-
// Provider returns a Subscribe-Function (emit-fn → unsubscribe-fn).
|
|
780
|
-
// Initial-emit synchron oder async, weitere Emits beliebig oft (e.g.
|
|
781
|
-
// on entity-update SSE). Provider sind session-bound; tenantId fließt
|
|
782
|
-
// über die Backend-Session bei fetch/dispatch, nicht über ein ctx-Arg.
|
|
783
|
-
//
|
|
784
|
-
// A feature without r.tree() is invisible in `navigation: "tree"`-
|
|
785
|
-
// workspaces — that's the Zero-Whitelist-Filter from visual-tree.md A2:
|
|
786
|
-
// provider-Vorhandensein ist der Filter, kein Workspace-Mapping.
|
|
787
|
-
tree(provider: TreeChildrenSubscribe): void;
|
|
788
767
|
};
|
|
789
768
|
|
|
790
769
|
// --- Registry (created from features) ---
|
|
@@ -977,18 +956,9 @@ export type Registry = {
|
|
|
977
956
|
// the first workspace the user has access to.
|
|
978
957
|
getDefaultWorkspace(): WorkspaceDefinition | undefined;
|
|
979
958
|
|
|
980
|
-
// Tree-Providers declared via r.tree() across all features. Keyed by
|
|
981
|
-
// declaring feature name (NOT qualified — Provider sind feature-bound,
|
|
982
|
-
// ein Feature liefert genau eine Provider-Function). The Visual-Tree
|
|
983
|
-
// component (renderer-web) iteriert getTreeProviders() beim Mount des
|
|
984
|
-
// navigation: "tree"-Workspaces, ruft jeden Provider mit ctx auf,
|
|
985
|
-
// sammelt die emitted TreeNode[] und merged sie zur Top-Level-Liste.
|
|
986
|
-
// See visual-tree.md A2 (Zero-Whitelist) + A4 (Subscribe-Form).
|
|
987
|
-
getTreeProviders(): ReadonlyMap<string, TreeChildrenSubscribe>;
|
|
988
|
-
|
|
989
959
|
// Tree-Actions-Map des Features. Returns the erased Record (compile-
|
|
990
|
-
// time-typed handle wandert über setup-export, nicht hier).
|
|
991
|
-
// Tree-
|
|
960
|
+
// time-typed handle wandert über setup-export, nicht hier). Die
|
|
961
|
+
// Content-Tree-Nav nutzt das für Runtime-Action-Lookup beim Klick auf
|
|
992
962
|
// einen TreeNode.target — der Resolver findet das Feature via
|
|
993
963
|
// TargetRef.featureId und holt sich die zugehörige Action-Definition.
|
|
994
964
|
getTreeActions(featureName: string): Readonly<Record<string, TreeActionDef>> | undefined;
|
|
@@ -354,7 +354,7 @@ export type JsonbFieldDef = {
|
|
|
354
354
|
// Legacy "date" — JS-Date-Object, semantisch unklar (Wall-Clock vs Instant).
|
|
355
355
|
// Für neue Felder bevorzuge:
|
|
356
356
|
// - `timestamp` für UTC-Instant ("wann ist das passiert")
|
|
357
|
-
// - `
|
|
357
|
+
// - `createLocatedTimestampField()` für Termine die an einem Ort
|
|
358
358
|
// stattfinden ("Pickup um 10:00 in Lissabon")
|
|
359
359
|
// - (kommt) `plainDate` für Kalender-Daten ohne Uhrzeit (z.B. Geburtstag)
|
|
360
360
|
// Siehe docs/plans/architecture/timezones.md
|
|
@@ -384,8 +384,8 @@ export type DateFieldDef = {
|
|
|
384
384
|
// speichert Wall-Clock+tz und konvertiert transparent (siehe DB-Wrapper,
|
|
385
385
|
// kommt in einer späteren Iteration).
|
|
386
386
|
//
|
|
387
|
-
//
|
|
388
|
-
//
|
|
387
|
+
// Für neue Felder bevorzuge `createLocatedTimestampField()` — EIN atomares
|
|
388
|
+
// Feld statt eines lose verdrahteten Pairs (siehe LocatedTimestampFieldDef).
|
|
389
389
|
export type TimestampFieldDef = {
|
|
390
390
|
readonly type: "timestamp";
|
|
391
391
|
readonly required?: boolean;
|
|
@@ -397,8 +397,9 @@ export type TimestampFieldDef = {
|
|
|
397
397
|
* Marker: dieses Timestamp-Feld ist Wall-Clock-Zeit an einem Ort.
|
|
398
398
|
* Wert ist der Name des begleitenden tz-Felds (IANA-Zone).
|
|
399
399
|
*
|
|
400
|
-
* Beispiel:
|
|
400
|
+
* Beispiel: manuelles Pair
|
|
401
401
|
* { pickupAt: { type: "timestamp", locatedBy: "pickupTz" }, pickupTz: { type: "tz" } }
|
|
402
|
+
* — bevorzuge stattdessen `createLocatedTimestampField()`.
|
|
402
403
|
*/
|
|
403
404
|
readonly locatedBy?: string;
|
|
404
405
|
/** Erlaubte Grenzen als ISO-Datetime. Begrenzt den Picker auf
|
|
@@ -220,9 +220,14 @@ export type {
|
|
|
220
220
|
CustomScreenDefinition,
|
|
221
221
|
CustomScreenRoute,
|
|
222
222
|
DashboardChartPanel,
|
|
223
|
+
DashboardCustomPanel,
|
|
224
|
+
DashboardFeedPanel,
|
|
225
|
+
DashboardFilterDefinition,
|
|
223
226
|
DashboardListPanel,
|
|
224
227
|
DashboardPanelDefinition,
|
|
228
|
+
DashboardProgressListPanel,
|
|
225
229
|
DashboardScreenDefinition,
|
|
230
|
+
DashboardStatGroupPanel,
|
|
226
231
|
DashboardStatPanel,
|
|
227
232
|
EditExtensionSection,
|
|
228
233
|
EditFieldSpec,
|
|
@@ -369,15 +369,78 @@ export type DashboardListPanel = {
|
|
|
369
369
|
readonly columns: readonly ListColumnSpec[];
|
|
370
370
|
};
|
|
371
371
|
|
|
372
|
+
// Betitelte Sektion aus mehreren Stat-Panels (z.B. "Net Worth": Assets/Debts/
|
|
373
|
+
// Net). Ein Nesting-Level, kein Group-of-Groups — jedes Kind bleibt ein
|
|
374
|
+
// vollwertiges DashboardStatPanel mit eigener Query/id/label, der Renderer
|
|
375
|
+
// zieht sie nur gemeinsam unter einen Sektions-Titel.
|
|
376
|
+
export type DashboardStatGroupPanel = {
|
|
377
|
+
readonly kind: "stat-group";
|
|
378
|
+
readonly id: string;
|
|
379
|
+
readonly label: string;
|
|
380
|
+
readonly stats: readonly DashboardStatPanel[];
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
// Nicht-tabellarische Kurzliste (z.B. "nächste Termine"). Query-Result-
|
|
384
|
+
// Contract: `{ rows: { primary: string; trailing?: string }[] }`.
|
|
385
|
+
export type DashboardFeedPanel = {
|
|
386
|
+
readonly kind: "feed";
|
|
387
|
+
readonly id: string;
|
|
388
|
+
readonly label: string;
|
|
389
|
+
readonly query: string;
|
|
390
|
+
readonly emptyLabel?: string;
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
// Liste aus Label/Wert/Fortschrittsbalken (z.B. Tilgungsfortschritt pro
|
|
394
|
+
// Kredit). Query-Result-Contract: `{ rows: { label: string; value: string;
|
|
395
|
+
// fraction: number }[] }` — fraction wird auf 0..1 geclampt.
|
|
396
|
+
export type DashboardProgressListPanel = {
|
|
397
|
+
readonly kind: "progress-list";
|
|
398
|
+
readonly id: string;
|
|
399
|
+
readonly label: string;
|
|
400
|
+
readonly query: string;
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
// Eingehängte App-Komponente, die ihre Daten/Titel selbst verwaltet (wie ein
|
|
404
|
+
// custom Screen, nur als Panel — bleibt an ihrer Array-Position statt in
|
|
405
|
+
// einen separaten Slot zu wandern). Kein `query`, keine `label`: der Renderer
|
|
406
|
+
// löst `component` über dieselbe extensionSectionComponents-Registry auf wie
|
|
407
|
+
// entityEdit-Extension-Sections und List-Header-Slots.
|
|
408
|
+
export type DashboardCustomPanel = {
|
|
409
|
+
readonly kind: "custom";
|
|
410
|
+
readonly id: string;
|
|
411
|
+
readonly component: PlatformComponent;
|
|
412
|
+
};
|
|
413
|
+
|
|
372
414
|
export type DashboardPanelDefinition =
|
|
373
415
|
| DashboardStatPanel
|
|
416
|
+
| DashboardStatGroupPanel
|
|
374
417
|
| DashboardChartPanel
|
|
375
|
-
| DashboardListPanel
|
|
418
|
+
| DashboardListPanel
|
|
419
|
+
| DashboardFeedPanel
|
|
420
|
+
| DashboardProgressListPanel
|
|
421
|
+
| DashboardCustomPanel;
|
|
422
|
+
|
|
423
|
+
// Screen-weiter Picker (Combobox), dessen gewählter Wert unter `id` in JEDE
|
|
424
|
+
// Panel-Query dieses Screens gemerged wird (Query-Handler validieren den Wert
|
|
425
|
+
// selbst gegen die Tenant-Sicht — dies ist UX-Scoping, keine Access-Boundary).
|
|
426
|
+
// Genau eins von `options`/`optionsQuery` ist gesetzt (Boot-Validator prüft).
|
|
427
|
+
export type DashboardFilterDefinition = {
|
|
428
|
+
readonly id: string;
|
|
429
|
+
readonly label: string;
|
|
430
|
+
readonly kind: "select";
|
|
431
|
+
readonly placeholder?: string;
|
|
432
|
+
/** i18n-Key für den "(alle)"-Eintrag. */
|
|
433
|
+
readonly allLabel?: string;
|
|
434
|
+
readonly options?: readonly { readonly value: string; readonly label: string }[];
|
|
435
|
+
/** Query-Result-Contract: `{ rows: { value: string; label: string }[] }`. */
|
|
436
|
+
readonly optionsQuery?: string;
|
|
437
|
+
};
|
|
376
438
|
|
|
377
439
|
export type DashboardScreenDefinition = {
|
|
378
440
|
readonly id: string;
|
|
379
441
|
readonly type: "dashboard";
|
|
380
442
|
readonly panels: readonly DashboardPanelDefinition[];
|
|
443
|
+
readonly filter?: DashboardFilterDefinition;
|
|
381
444
|
readonly slots?: ScreenSlots;
|
|
382
445
|
readonly access?: AccessRule;
|
|
383
446
|
};
|
|
@@ -39,11 +39,4 @@ export type WorkspaceDefinition = {
|
|
|
39
39
|
// Default workspace at login when the user has access to multiple. Boot
|
|
40
40
|
// validator rejects more than one default per app.
|
|
41
41
|
readonly default?: boolean;
|
|
42
|
-
// Render mode of the workspace's sidebar. "nav" (default, missing-ok)
|
|
43
|
-
// mounts the existing NavTree component as before. "tree" mounts the
|
|
44
|
-
// Visual-Tree component which collects r.tree() providers across all
|
|
45
|
-
// active features. Default-on-undefined preserves backwards-compat —
|
|
46
|
-
// every existing workspace stays nav-mode without code touch.
|
|
47
|
-
// See visual-tree.md A1.
|
|
48
|
-
readonly navigation?: "nav" | "tree";
|
|
49
42
|
};
|
package/src/errors/classes.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { toSnakeCase } from "../utils/case";
|
|
|
2
2
|
import type { FieldIssue } from "./field-issue";
|
|
3
3
|
import { type ErrorOpts, KumikoError } from "./kumiko-error";
|
|
4
4
|
|
|
5
|
-
export type { FieldIssue
|
|
5
|
+
export type { FieldIssue } from "./field-issue";
|
|
6
6
|
|
|
7
7
|
export type ValidationDetails = {
|
|
8
8
|
readonly fields: readonly FieldIssue[];
|
package/src/errors/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ActionFormScreenDefinition,
|
|
3
3
|
ConfigEditScreenDefinition,
|
|
4
|
+
DashboardFilterDefinition,
|
|
5
|
+
DashboardPanelDefinition,
|
|
4
6
|
DashboardScreenDefinition,
|
|
5
7
|
EntityEditScreenDefinition,
|
|
6
8
|
EntityListScreenDefinition,
|
|
@@ -66,6 +68,32 @@ function pushRowActionKeys(out: Set<string>, action: RowAction): void {
|
|
|
66
68
|
}
|
|
67
69
|
}
|
|
68
70
|
|
|
71
|
+
function pushDashboardScreenKeys(out: Set<string>, dashboard: DashboardScreenDefinition): void {
|
|
72
|
+
for (const panel of dashboard.panels) pushDashboardPanelKeys(out, panel);
|
|
73
|
+
if (dashboard.filter !== undefined) pushDashboardFilterKeys(out, dashboard.filter);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function pushDashboardPanelKeys(out: Set<string>, panel: DashboardPanelDefinition): void {
|
|
77
|
+
// skip: custom-Panel übersetzt sich selbst, kein Key hier
|
|
78
|
+
if (panel.kind === "custom") return;
|
|
79
|
+
pushKey(out, panel.label);
|
|
80
|
+
if (panel.kind === "stat-group") {
|
|
81
|
+
for (const stat of panel.stats) pushKey(out, stat.label);
|
|
82
|
+
}
|
|
83
|
+
if (panel.kind === "list") {
|
|
84
|
+
for (const col of panel.columns) {
|
|
85
|
+
const normalized = normalizeListColumn(col);
|
|
86
|
+
if (normalized.label !== undefined) pushKey(out, normalized.label);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function pushDashboardFilterKeys(out: Set<string>, filter: DashboardFilterDefinition): void {
|
|
92
|
+
pushKey(out, filter.label);
|
|
93
|
+
if (filter.allLabel !== undefined) pushKey(out, filter.allLabel);
|
|
94
|
+
for (const opt of filter.options ?? []) pushKey(out, opt.label);
|
|
95
|
+
}
|
|
96
|
+
|
|
69
97
|
function pushToolbarActionKeys(out: Set<string>, action: ToolbarAction): void {
|
|
70
98
|
pushKey(out, action.label);
|
|
71
99
|
if (action.kind === "writeHandler") {
|
|
@@ -108,16 +136,7 @@ export function requiredKeysFromScreen(
|
|
|
108
136
|
break;
|
|
109
137
|
}
|
|
110
138
|
case "dashboard": {
|
|
111
|
-
|
|
112
|
-
for (const panel of dashboard.panels) {
|
|
113
|
-
pushKey(out, panel.label);
|
|
114
|
-
if (panel.kind === "list") {
|
|
115
|
-
for (const col of panel.columns) {
|
|
116
|
-
const normalized = normalizeListColumn(col);
|
|
117
|
-
if (normalized.label !== undefined) pushKey(out, normalized.label);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
139
|
+
pushDashboardScreenKeys(out, screen as DashboardScreenDefinition);
|
|
121
140
|
break;
|
|
122
141
|
}
|
|
123
142
|
case "entityEdit": {
|