@cosmicdrift/kumiko-framework 0.132.0 → 0.134.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/db/__tests__/assert-no-unreachable-live-rows.integration.test.ts +139 -0
- package/src/db/queries/shadow-swap.ts +62 -0
- 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__/deep-link.test.ts +35 -0
- 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/deep-link.ts +23 -0
- 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 +2 -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/jobs/__tests__/job-queue-depth.integration.test.ts +82 -0
- package/src/jobs/job-runner.ts +41 -1
- package/src/observability/__tests__/recording-tracer.test.ts +6 -4
- package/src/observability/index.ts +1 -0
- package/src/observability/noop-provider.ts +0 -9
- package/src/observability/recording-tracer.ts +0 -12
- package/src/observability/standard-metrics.ts +28 -0
- package/src/observability/types/span.ts +0 -6
- package/src/pipeline/projection-rebuild.ts +7 -0
- 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
|
@@ -178,8 +178,9 @@ export function validateMultiStreamProjections(feature: FeatureDefinition): void
|
|
|
178
178
|
//
|
|
179
179
|
// Die häufigste Quelle von Konflikten ist Hand-Konstruktion:
|
|
180
180
|
// { foo: { type: "timestamp", locatedBy: "fooTz" } }
|
|
181
|
-
// ohne das `fooTz`-Feld zu deklarieren.
|
|
182
|
-
//
|
|
181
|
+
// ohne das `fooTz`-Feld zu deklarieren. `createLocatedTimestampField()`
|
|
182
|
+
// erzeugt stattdessen EIN Feld vom Typ "locatedTimestamp" — wer den nutzt,
|
|
183
|
+
// fliegt nicht durch diesen Validator.
|
|
183
184
|
export function validateLocatedTimestamps(feature: FeatureDefinition): void {
|
|
184
185
|
for (const [entityName, entity] of Object.entries(feature.entities ?? {})) {
|
|
185
186
|
const fields = entity.fields;
|
|
@@ -190,8 +191,8 @@ export function validateLocatedTimestamps(feature: FeatureDefinition): void {
|
|
|
190
191
|
throw new Error(
|
|
191
192
|
`Feature "${feature.name}", entity "${entityName}": field "${fieldName}" has ` +
|
|
192
193
|
`locatedBy: "${field.locatedBy}" but no field with that name exists in the entity. ` +
|
|
193
|
-
`Either declare the tz-field, or use
|
|
194
|
-
`to create
|
|
194
|
+
`Either declare the tz-field, or use createLocatedTimestampField() ` +
|
|
195
|
+
`to create a single located-timestamp field instead.`,
|
|
195
196
|
);
|
|
196
197
|
}
|
|
197
198
|
if (referenced.type !== "tz") {
|
|
@@ -4,7 +4,11 @@ import { qualifyEntityName } from "../qualified-name";
|
|
|
4
4
|
import { getAllowedFilterOps, isFieldFilterable } from "../screen-filter-ops";
|
|
5
5
|
import type { FeatureDefinition, NavDefinition, WorkspaceDefinition } from "../types";
|
|
6
6
|
import type {
|
|
7
|
+
DashboardCustomPanel,
|
|
8
|
+
DashboardFilterDefinition,
|
|
9
|
+
DashboardPanelDefinition,
|
|
7
10
|
DashboardScreenDefinition,
|
|
11
|
+
DashboardStatGroupPanel,
|
|
8
12
|
FieldCondition,
|
|
9
13
|
RowAction,
|
|
10
14
|
RowFieldExtractor,
|
|
@@ -655,36 +659,128 @@ function validateDashboardScreen(
|
|
|
655
659
|
);
|
|
656
660
|
}
|
|
657
661
|
const panelIds = new Set<string>();
|
|
662
|
+
const addPanelId = (id: string, context: string): void => {
|
|
663
|
+
if (panelIds.has(id)) {
|
|
664
|
+
throw new Error(
|
|
665
|
+
`[Feature ${featureName}] Screen "${screenId}" (dashboard) has duplicate panel id "${id}" (${context}).`,
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
panelIds.add(id);
|
|
669
|
+
};
|
|
670
|
+
|
|
658
671
|
for (const panel of screen.panels) {
|
|
659
|
-
|
|
672
|
+
addPanelId(panel.id, panel.kind);
|
|
673
|
+
if (panel.kind === "stat-group") {
|
|
674
|
+
validateDashboardStatGroupPanel(featureName, screenId, panel, addPanelId);
|
|
675
|
+
} else if (panel.kind === "custom") {
|
|
676
|
+
validateDashboardCustomPanel(featureName, screenId, panel);
|
|
677
|
+
} else {
|
|
678
|
+
validateDashboardQueryPanel(featureName, screenId, panel);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (screen.filter !== undefined) {
|
|
683
|
+
validateDashboardFilterDefinition(featureName, screenId, screen.filter);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function validateDashboardStatGroupPanel(
|
|
688
|
+
featureName: string,
|
|
689
|
+
screenId: string,
|
|
690
|
+
panel: DashboardStatGroupPanel,
|
|
691
|
+
addPanelId: (id: string, context: string) => void,
|
|
692
|
+
): void {
|
|
693
|
+
if (panel.stats.length === 0) {
|
|
694
|
+
throw new Error(
|
|
695
|
+
`[Feature ${featureName}] Screen "${screenId}" (dashboard) stat-group "${panel.id}" has an empty stats list.`,
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
for (const stat of panel.stats) {
|
|
699
|
+
addPanelId(stat.id, "stat-group child");
|
|
700
|
+
if (!stat.query || typeof stat.query !== "string") {
|
|
660
701
|
throw new Error(
|
|
661
|
-
`[Feature ${featureName}] Screen "${screenId}" (dashboard)
|
|
702
|
+
`[Feature ${featureName}] Screen "${screenId}" (dashboard) stat-group "${panel.id}" child "${stat.id}" has empty or non-string query.`,
|
|
662
703
|
);
|
|
663
704
|
}
|
|
664
|
-
|
|
665
|
-
if (!panel.query || typeof panel.query !== "string") {
|
|
705
|
+
if (stat.valueField.length === 0) {
|
|
666
706
|
throw new Error(
|
|
667
|
-
`[Feature ${featureName}] Screen "${screenId}" (dashboard)
|
|
707
|
+
`[Feature ${featureName}] Screen "${screenId}" (dashboard) stat-group "${panel.id}" child "${stat.id}" has empty valueField.`,
|
|
668
708
|
);
|
|
669
709
|
}
|
|
670
|
-
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function validateDashboardCustomPanel(
|
|
714
|
+
featureName: string,
|
|
715
|
+
screenId: string,
|
|
716
|
+
panel: DashboardCustomPanel,
|
|
717
|
+
): void {
|
|
718
|
+
if (panel.component.react === undefined && panel.component.native === undefined) {
|
|
719
|
+
throw new Error(
|
|
720
|
+
`[Feature ${featureName}] Screen "${screenId}" (dashboard) custom-panel "${panel.id}" has no component — ` +
|
|
721
|
+
`declare a react/native component marker.`,
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function validateDashboardQueryPanel(
|
|
727
|
+
featureName: string,
|
|
728
|
+
screenId: string,
|
|
729
|
+
panel: Exclude<DashboardPanelDefinition, DashboardStatGroupPanel | DashboardCustomPanel>,
|
|
730
|
+
): void {
|
|
731
|
+
if (!panel.query || typeof panel.query !== "string") {
|
|
732
|
+
throw new Error(
|
|
733
|
+
`[Feature ${featureName}] Screen "${screenId}" (dashboard) panel "${panel.id}" has empty or non-string query.`,
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
if (panel.kind === "stat" && panel.valueField.length === 0) {
|
|
737
|
+
throw new Error(
|
|
738
|
+
`[Feature ${featureName}] Screen "${screenId}" (dashboard) stat-panel "${panel.id}" has empty valueField.`,
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
if (panel.kind === "list") {
|
|
742
|
+
if (panel.columns.length === 0) {
|
|
671
743
|
throw new Error(
|
|
672
|
-
`[Feature ${featureName}] Screen "${screenId}" (dashboard)
|
|
744
|
+
`[Feature ${featureName}] Screen "${screenId}" (dashboard) list-panel "${panel.id}" has an empty columns list.`,
|
|
673
745
|
);
|
|
674
746
|
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
throw new Error(
|
|
678
|
-
`[Feature ${featureName}] Screen "${screenId}" (dashboard) list-panel "${panel.id}" has an empty columns list.`,
|
|
679
|
-
);
|
|
680
|
-
}
|
|
681
|
-
for (const col of panel.columns) {
|
|
682
|
-
validateColumnRendererForm(featureName, screenId, normalizeListColumn(col));
|
|
683
|
-
}
|
|
747
|
+
for (const col of panel.columns) {
|
|
748
|
+
validateColumnRendererForm(featureName, screenId, normalizeListColumn(col));
|
|
684
749
|
}
|
|
685
750
|
}
|
|
686
751
|
}
|
|
687
752
|
|
|
753
|
+
function validateDashboardFilterDefinition(
|
|
754
|
+
featureName: string,
|
|
755
|
+
screenId: string,
|
|
756
|
+
filter: DashboardFilterDefinition,
|
|
757
|
+
): void {
|
|
758
|
+
if (filter.id.length === 0) {
|
|
759
|
+
throw new Error(
|
|
760
|
+
`[Feature ${featureName}] Screen "${screenId}" (dashboard) filter has an empty id.`,
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
if (filter.label.length === 0) {
|
|
764
|
+
throw new Error(
|
|
765
|
+
`[Feature ${featureName}] Screen "${screenId}" (dashboard) filter has an empty label.`,
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
const hasOptions = filter.options !== undefined;
|
|
769
|
+
const hasOptionsQuery = filter.optionsQuery !== undefined;
|
|
770
|
+
if (hasOptions === hasOptionsQuery) {
|
|
771
|
+
throw new Error(
|
|
772
|
+
`[Feature ${featureName}] Screen "${screenId}" (dashboard) filter must set exactly one of ` +
|
|
773
|
+
`options/optionsQuery.`,
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
if (hasOptions && (filter.options?.length ?? 0) === 0) {
|
|
777
|
+
throw new Error(
|
|
778
|
+
`[Feature ${featureName}] Screen "${screenId}" (dashboard) filter.options is empty — ` +
|
|
779
|
+
`declare at least one option or use optionsQuery instead.`,
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
688
784
|
// Form-check für ListColumn-Renderer in der PlatformComponent-Form
|
|
689
785
|
// (`{ react: { __component: "Name" } }`). Der Server kennt die client-
|
|
690
786
|
// seitige columnRenderers-Map nicht — also nur prüfen ob die Struktur
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Deep-Link-URL-Builder für Notification-Templates (#449, Lazy-Scope: nur
|
|
2
|
+
// Notification→Screen, kein Permalink-Sharing-Layer). Server-seitig nutzbar
|
|
3
|
+
// (kein React) — der Renderer hat mit `formatPath`
|
|
4
|
+
// (packages/renderer/src/app/nav.tsx) dasselbe Pfad-Format fürs Client-Routing,
|
|
5
|
+
// hier bewusst dupliziert statt importiert: `renderer` zieht React als
|
|
6
|
+
// Dependency, Notification-Data-Fns laufen server-seitig im Write-Handler.
|
|
7
|
+
//
|
|
8
|
+
// baseUrl kommt vom App-Autor (analog `AuthMailOptions.baseUrl` /
|
|
9
|
+
// magic-link-mail.ts appendToken) — kein Auto-Detect, kein Env-Var-Read hier.
|
|
10
|
+
|
|
11
|
+
export type DeepLinkTarget = {
|
|
12
|
+
readonly screenId: string;
|
|
13
|
+
readonly entityId?: string;
|
|
14
|
+
readonly workspaceId?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function buildDeepLinkUrl(baseUrl: string, target: DeepLinkTarget): string {
|
|
18
|
+
const segments: string[] = [];
|
|
19
|
+
if (target.workspaceId !== undefined) segments.push(target.workspaceId);
|
|
20
|
+
segments.push(target.screenId);
|
|
21
|
+
if (target.entityId !== undefined) segments.push(target.entityId);
|
|
22
|
+
return `${baseUrl.replace(/\/+$/, "")}/${segments.join("/")}`;
|
|
23
|
+
}
|
|
@@ -64,7 +64,6 @@ import type {
|
|
|
64
64
|
TranslationsDef,
|
|
65
65
|
TreeActionDef,
|
|
66
66
|
TreeActionsHandle,
|
|
67
|
-
TreeChildrenSubscribe,
|
|
68
67
|
UiHints,
|
|
69
68
|
UnmanagedTableEntry,
|
|
70
69
|
UnmanagedTableOptions,
|
|
@@ -163,14 +162,13 @@ export function defineFeature<const TName extends string, TExports = undefined>(
|
|
|
163
162
|
let toggleableDefault: boolean | undefined;
|
|
164
163
|
let description: string | undefined;
|
|
165
164
|
let uiHints: UiHints | undefined;
|
|
166
|
-
//
|
|
167
|
-
// registrar (siehe r.treeActions
|
|
168
|
-
//
|
|
165
|
+
// Tree-Action-Slot — at-most-one per feature, only-once-guard im
|
|
166
|
+
// registrar (siehe r.treeActions). Undefined wenn das Feature keine
|
|
167
|
+
// Tree-Actions liefert (Zero-Whitelist-Filter).
|
|
169
168
|
// Name-Collision-Sicherheit: object-literal-method-Names im registrar
|
|
170
169
|
// sind keine bindings im closure-scope, daher kollidiert die `treeActions`
|
|
171
170
|
// closure-let-var nicht mit der `treeActions(...)` registrar-Methode.
|
|
172
171
|
let treeActions: Readonly<Record<string, TreeActionDef>> | undefined;
|
|
173
|
-
let treeProvider: TreeChildrenSubscribe | undefined;
|
|
174
172
|
// Optional Zod-schema for env-vars this feature reads at runtime,
|
|
175
173
|
// declared via r.envSchema(). At-most-one per feature.
|
|
176
174
|
let envSchema: z.ZodObject<z.ZodRawShape> | undefined;
|
|
@@ -969,17 +967,6 @@ export function defineFeature<const TName extends string, TExports = undefined>(
|
|
|
969
967
|
treeActions: actions,
|
|
970
968
|
});
|
|
971
969
|
},
|
|
972
|
-
|
|
973
|
-
tree(provider: TreeChildrenSubscribe): void {
|
|
974
|
-
// Only-once-guard analog zu r.treeActions.
|
|
975
|
-
if (treeProvider !== undefined) {
|
|
976
|
-
throw new Error(
|
|
977
|
-
`[Feature ${name}] r.tree() already called. ` +
|
|
978
|
-
`Each feature may declare a single tree-provider.`,
|
|
979
|
-
);
|
|
980
|
-
}
|
|
981
|
-
treeProvider = provider;
|
|
982
|
-
},
|
|
983
970
|
};
|
|
984
971
|
|
|
985
972
|
const exports = setup(registrar) as TExports; // @cast-boundary engine-bridge
|
|
@@ -1049,7 +1036,6 @@ export function defineFeature<const TName extends string, TExports = undefined>(
|
|
|
1049
1036
|
rawTables,
|
|
1050
1037
|
unmanagedTables,
|
|
1051
1038
|
...(treeActions !== undefined && { treeActions }),
|
|
1052
|
-
...(treeProvider !== undefined && { treeProvider }),
|
|
1053
1039
|
...(envSchema !== undefined && { envSchema }),
|
|
1054
1040
|
};
|
|
1055
1041
|
}
|
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
|
@@ -38,6 +38,8 @@ export {
|
|
|
38
38
|
export type { App, AppConfig } from "./create-app";
|
|
39
39
|
export { createApp } from "./create-app";
|
|
40
40
|
export { crossTenantOverrideDenied } from "./cross-tenant";
|
|
41
|
+
export type { DeepLinkTarget } from "./deep-link";
|
|
42
|
+
export { buildDeepLinkUrl } from "./deep-link";
|
|
41
43
|
export { defineFeature } from "./define-feature";
|
|
42
44
|
export type {
|
|
43
45
|
QueryHandlerDefinition,
|
|
@@ -121,7 +123,6 @@ export {
|
|
|
121
123
|
createTextField,
|
|
122
124
|
createTimestampField,
|
|
123
125
|
createTzField,
|
|
124
|
-
locatedTimestamp,
|
|
125
126
|
} from "./factories";
|
|
126
127
|
// AST inspection + patching pipeline — used by the CLI scaffolder, the
|
|
127
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
|
},
|