@praxisui/core 9.0.4-rc.32 → 9.0.4-rc.34

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.
@@ -22766,696 +22766,1084 @@ function getEditorialSolutionPresetById(presetId) {
22766
22766
  return match ? structuredClone(match) : undefined;
22767
22767
  }
22768
22768
 
22769
- const WIDGET_PAGE_COMPOSITION_SCHEMA_VERSION = '1.0.0';
22770
- const STRUCTURAL_NESTED_PATH_KINDS$1 = new Set([
22771
- 'nav',
22772
- ]);
22773
- /**
22774
- * Normalizes a CompositionLink for deterministic persistence.
22775
- * Ensures explicit ordering and strips compatibility-only legacy residues.
22776
- */
22777
- function normalizeCompositionLink(link) {
22778
- const normalized = {
22779
- id: link.id,
22780
- from: normalizeEndpoint(link.from),
22781
- to: normalizeEndpoint(link.to),
22782
- intent: link.intent,
22783
- };
22784
- if (link.transform) {
22785
- normalized.transform = clone$1(link.transform);
22786
- }
22787
- const condition = normalizeCondition(link.condition);
22788
- if (condition !== undefined) {
22789
- normalized.condition = condition;
22769
+ class NestedWidgetConfigAccessor {
22770
+ listNestedWidgets(owner) {
22771
+ return this.listNestedWidgetsInDefinition(owner.key, owner.definition, []);
22790
22772
  }
22791
- const policy = normalizePolicy(link.policy);
22792
- if (policy) {
22793
- normalized.policy = policy;
22773
+ resolveNestedWidget(owner, nestedPath) {
22774
+ if (!nestedPath?.length) {
22775
+ return undefined;
22776
+ }
22777
+ return this.resolveNestedWidgetInDefinition(owner.definition, nestedPath);
22794
22778
  }
22795
- const metadata = normalizeMetadata(link.metadata);
22796
- if (metadata) {
22797
- normalized.metadata = metadata;
22779
+ setNestedWidgetInput(owner, nestedPath, inputName, value) {
22780
+ const normalizedInputName = String(inputName || '').trim();
22781
+ if (!nestedPath?.length || !normalizedInputName) {
22782
+ return { widget: this.clone(owner), changed: false };
22783
+ }
22784
+ const next = this.clone(owner);
22785
+ const changed = this.setNestedWidgetInputInDefinition(next.definition, nestedPath, normalizedInputName, this.clone(value));
22786
+ return { widget: next, changed };
22798
22787
  }
22799
- return normalized;
22800
- }
22801
- /**
22802
- * Normalizes an array of CompositionLinks for deterministic persistence.
22803
- */
22804
- function normalizeCompositionLinks(links) {
22805
- return links
22806
- .map(normalizeCompositionLink)
22807
- .sort(compareNormalizedLinks);
22808
- }
22809
- /**
22810
- * Canonical persisted write surface for CompositionLink[].
22811
- * New saved shapes must serialize links under `composition.links`.
22812
- * Semantic guards persist only through `link.condition` as Json Logic.
22813
- * Operational delivery behavior remains under `link.policy` and must not be
22814
- * folded into the condition payload.
22815
- */
22816
- function serializeCompositionLinks(links) {
22817
- return {
22818
- version: WIDGET_PAGE_COMPOSITION_SCHEMA_VERSION,
22819
- links: normalizeCompositionLinks(links),
22820
- };
22821
- }
22822
- /**
22823
- * Writes the canonical persisted composition surface into a page shape.
22824
- * The canonical writer omits legacy `connections` to avoid dual-write ambiguity.
22825
- */
22826
- function writeCompositionLinksToPersistedPage(page, links) {
22827
- const { composition: _legacyComposition, ...rest } = clone$1(page);
22828
- return {
22829
- ...rest,
22830
- composition: serializeCompositionLinks(links),
22831
- };
22832
- }
22833
- function normalizeEndpoint(endpoint) {
22834
- if (endpoint.kind === 'component-port') {
22835
- const nestedPath = normalizeNestedPath(endpoint.ref.nestedPath);
22836
- return {
22837
- kind: 'component-port',
22838
- ref: {
22839
- widget: endpoint.ref.widget,
22840
- port: endpoint.ref.port,
22841
- direction: endpoint.ref.direction,
22842
- ...(endpoint.ref.componentType ? { componentType: endpoint.ref.componentType } : {}),
22843
- ...(endpoint.ref.bindingPath ? { bindingPath: endpoint.ref.bindingPath } : {}),
22844
- ...(nestedPath.length ? { nestedPath } : {}),
22845
- },
22846
- };
22788
+ removeNestedWidgetInput(owner, nestedPath, inputName) {
22789
+ const normalizedInputName = String(inputName || '').trim();
22790
+ if (!nestedPath?.length || !normalizedInputName) {
22791
+ return { widget: this.clone(owner), changed: false };
22792
+ }
22793
+ const next = this.clone(owner);
22794
+ const changed = this.removeNestedWidgetInputInDefinition(next.definition, nestedPath, normalizedInputName);
22795
+ return { widget: next, changed };
22847
22796
  }
22848
- if (endpoint.kind === 'global-action') {
22849
- return {
22850
- kind: 'global-action',
22851
- ref: normalizeGlobalActionRef(endpoint.ref),
22852
- };
22797
+ listNestedWidgetsInDefinition(ownerWidgetKey, definition, basePath) {
22798
+ const results = [];
22799
+ for (const location of this.listChildWidgetLocations(definition)) {
22800
+ for (const [index, widget] of location.widgets.entries()) {
22801
+ if (!this.isWidgetDefinition(widget)) {
22802
+ continue;
22803
+ }
22804
+ const childWidgetKey = this.resolveChildWidgetKey(widget);
22805
+ const widgetSegment = {
22806
+ kind: 'widget',
22807
+ ...(childWidgetKey ? { key: childWidgetKey } : { index }),
22808
+ componentType: widget.id,
22809
+ };
22810
+ const nestedPath = [
22811
+ ...basePath,
22812
+ ...location.path,
22813
+ widgetSegment,
22814
+ ];
22815
+ results.push({
22816
+ ownerWidgetKey,
22817
+ nestedPath,
22818
+ widget: this.clone(widget),
22819
+ componentId: widget.id,
22820
+ childWidgetKey,
22821
+ });
22822
+ results.push(...this.listNestedWidgetsInDefinition(ownerWidgetKey, widget, nestedPath));
22823
+ }
22824
+ }
22825
+ return results;
22853
22826
  }
22854
- return {
22855
- kind: 'state',
22856
- ref: {
22857
- path: endpoint.ref.path,
22858
- ...(endpoint.ref.layer ? { layer: endpoint.ref.layer } : {}),
22859
- },
22860
- };
22861
- }
22862
- function normalizeGlobalActionRef(ref) {
22863
- return {
22864
- actionId: String(ref.actionId || '').trim(),
22865
- ...(ref.payload !== undefined ? { payload: clone$1(ref.payload) } : {}),
22866
- ...(ref.payloadExpr ? { payloadExpr: String(ref.payloadExpr).trim() } : {}),
22867
- ...(ref.meta && typeof ref.meta === 'object' ? { meta: { ...ref.meta } } : {}),
22868
- };
22869
- }
22870
- function normalizeNestedPath(nestedPath) {
22871
- if (!nestedPath?.length) {
22872
- return [];
22827
+ resolveNestedWidgetInDefinition(definition, nestedPath) {
22828
+ const location = this.resolveWidgetArrayLocation(definition, nestedPath, 0);
22829
+ if (!location) {
22830
+ return undefined;
22831
+ }
22832
+ const widgetSegment = nestedPath[location.segmentCount];
22833
+ if (widgetSegment?.kind !== 'widget' || !widgetSegment.key) {
22834
+ return undefined;
22835
+ }
22836
+ const child = location.widgets.find((widget) => widget.childWidgetKey === widgetSegment.key);
22837
+ if (!this.isWidgetDefinition(child)) {
22838
+ return undefined;
22839
+ }
22840
+ const remaining = nestedPath.slice(location.segmentCount + 1);
22841
+ if (!remaining.length) {
22842
+ return this.clone(child);
22843
+ }
22844
+ return this.resolveNestedWidgetInDefinition(child, remaining);
22873
22845
  }
22874
- return nestedPath
22875
- .map((segment) => {
22876
- const normalized = {
22877
- kind: segment.kind,
22878
- };
22879
- if (segment.id) {
22880
- normalized.id = segment.id;
22846
+ setNestedWidgetInputInDefinition(definition, nestedPath, inputName, value) {
22847
+ const location = this.resolveWidgetArrayLocation(definition, nestedPath, 0);
22848
+ if (!location) {
22849
+ return false;
22881
22850
  }
22882
- if (segment.key) {
22883
- normalized.key = segment.key;
22851
+ const widgetSegment = nestedPath[location.segmentCount];
22852
+ if (widgetSegment?.kind !== 'widget' || !widgetSegment.key) {
22853
+ return false;
22884
22854
  }
22885
- if (typeof segment.index === 'number' && Number.isFinite(segment.index)) {
22886
- normalized.index = Math.trunc(segment.index);
22855
+ const widgetIndex = location.widgets.findIndex((widget) => widget.childWidgetKey === widgetSegment.key);
22856
+ if (widgetIndex < 0) {
22857
+ return false;
22887
22858
  }
22888
- if (segment.componentType) {
22889
- normalized.componentType = segment.componentType;
22859
+ const remaining = nestedPath.slice(location.segmentCount + 1);
22860
+ const child = location.widgets[widgetIndex];
22861
+ if (!this.isWidgetDefinition(child)) {
22862
+ return false;
22890
22863
  }
22891
- return normalized;
22892
- })
22893
- .filter((segment) => !!segment.kind
22894
- && (!!segment.id
22895
- || !!segment.key
22896
- || typeof segment.index === 'number'
22897
- || !!segment.componentType
22898
- || segment.kind === 'widget'
22899
- || STRUCTURAL_NESTED_PATH_KINDS$1.has(segment.kind)));
22900
- }
22901
- function normalizeCondition(condition) {
22902
- if (condition === undefined) {
22903
- return undefined;
22864
+ if (remaining.length) {
22865
+ return this.setNestedWidgetInputInDefinition(child, remaining, inputName, value);
22866
+ }
22867
+ const currentInputs = child.inputs || {};
22868
+ const nextInputs = {
22869
+ ...currentInputs,
22870
+ [inputName]: value,
22871
+ };
22872
+ if (this.isEqual(currentInputs, nextInputs)) {
22873
+ return false;
22874
+ }
22875
+ location.widgets[widgetIndex] = {
22876
+ ...child,
22877
+ inputs: nextInputs,
22878
+ };
22879
+ return true;
22904
22880
  }
22905
- if (condition === null) {
22906
- return null;
22881
+ removeNestedWidgetInputInDefinition(definition, nestedPath, inputName) {
22882
+ const location = this.resolveWidgetArrayLocation(definition, nestedPath, 0);
22883
+ if (!location) {
22884
+ return false;
22885
+ }
22886
+ const widgetSegment = nestedPath[location.segmentCount];
22887
+ if (widgetSegment?.kind !== 'widget' || !widgetSegment.key) {
22888
+ return false;
22889
+ }
22890
+ const widgetIndex = location.widgets.findIndex((widget) => widget.childWidgetKey === widgetSegment.key);
22891
+ if (widgetIndex < 0) {
22892
+ return false;
22893
+ }
22894
+ const remaining = nestedPath.slice(location.segmentCount + 1);
22895
+ const child = location.widgets[widgetIndex];
22896
+ if (!this.isWidgetDefinition(child)) {
22897
+ return false;
22898
+ }
22899
+ if (remaining.length) {
22900
+ return this.removeNestedWidgetInputInDefinition(child, remaining, inputName);
22901
+ }
22902
+ const currentInputs = child.inputs || {};
22903
+ if (!Object.prototype.hasOwnProperty.call(currentInputs, inputName)) {
22904
+ return false;
22905
+ }
22906
+ const { [inputName]: _runtimeValue, ...nextInputs } = currentInputs;
22907
+ location.widgets[widgetIndex] = {
22908
+ ...child,
22909
+ inputs: nextInputs,
22910
+ };
22911
+ return true;
22907
22912
  }
22908
- return clone$1(condition);
22909
- }
22910
- function normalizePolicy(policy) {
22911
- if (!policy) {
22913
+ listChildWidgetLocations(definition) {
22914
+ const config = definition.inputs?.['config'];
22915
+ if (!config || typeof config !== 'object') {
22916
+ return [];
22917
+ }
22918
+ if (definition.id === 'praxis-tabs') {
22919
+ return this.listTabsWidgetLocations(config);
22920
+ }
22921
+ if (definition.id === 'praxis-expansion') {
22922
+ return this.listExpansionWidgetLocations(config);
22923
+ }
22924
+ return [];
22925
+ }
22926
+ listTabsWidgetLocations(config) {
22927
+ const locations = [];
22928
+ const tabs = Array.isArray(config['tabs']) ? config['tabs'] : [];
22929
+ for (const [index, tab] of tabs.entries()) {
22930
+ if (!tab || typeof tab !== 'object') {
22931
+ continue;
22932
+ }
22933
+ const widgets = this.asWidgetDefinitions(tab['widgets']);
22934
+ if (!widgets.some((widget) => this.isWidgetDefinition(widget))) {
22935
+ continue;
22936
+ }
22937
+ locations.push({
22938
+ path: [{
22939
+ kind: 'tab',
22940
+ ...this.segmentIdentity(tab, index),
22941
+ }],
22942
+ widgets,
22943
+ });
22944
+ }
22945
+ const nav = config['nav'];
22946
+ const links = nav && typeof nav === 'object' && Array.isArray(nav['links'])
22947
+ ? nav['links']
22948
+ : [];
22949
+ for (const [index, link] of links.entries()) {
22950
+ if (!link || typeof link !== 'object') {
22951
+ continue;
22952
+ }
22953
+ const widgets = this.asWidgetDefinitions(link['widgets']);
22954
+ if (!widgets.some((widget) => this.isWidgetDefinition(widget))) {
22955
+ continue;
22956
+ }
22957
+ locations.push({
22958
+ path: [
22959
+ { kind: 'nav' },
22960
+ {
22961
+ kind: 'link',
22962
+ ...this.segmentIdentity(link, index),
22963
+ },
22964
+ ],
22965
+ widgets,
22966
+ });
22967
+ }
22968
+ return locations;
22969
+ }
22970
+ listExpansionWidgetLocations(config) {
22971
+ const panels = Array.isArray(config['panels']) ? config['panels'] : [];
22972
+ const locations = [];
22973
+ for (const [index, panel] of panels.entries()) {
22974
+ if (!panel || typeof panel !== 'object') {
22975
+ continue;
22976
+ }
22977
+ const widgets = this.asWidgetDefinitions(panel['widgets']);
22978
+ if (!widgets.some((widget) => this.isWidgetDefinition(widget))) {
22979
+ continue;
22980
+ }
22981
+ locations.push({
22982
+ path: [{
22983
+ kind: 'panel',
22984
+ ...this.segmentIdentity(panel, index),
22985
+ }],
22986
+ widgets,
22987
+ });
22988
+ }
22989
+ return locations;
22990
+ }
22991
+ resolveWidgetArrayLocation(definition, nestedPath, offset) {
22992
+ if (definition.id === 'praxis-tabs') {
22993
+ return this.resolveTabsWidgetArray(definition, nestedPath, offset);
22994
+ }
22995
+ if (definition.id === 'praxis-expansion') {
22996
+ return this.resolveExpansionWidgetArray(definition, nestedPath, offset);
22997
+ }
22912
22998
  return undefined;
22913
22999
  }
22914
- const normalized = {
22915
- ...(policy.debounceMs !== undefined ? { debounceMs: policy.debounceMs } : {}),
22916
- ...(policy.distinct !== undefined ? { distinct: policy.distinct } : {}),
22917
- ...(policy.distinctBy ? { distinctBy: policy.distinctBy } : {}),
22918
- ...(policy.delivery ? { delivery: policy.delivery } : {}),
22919
- ...(policy.missingValuePolicy ? { missingValuePolicy: policy.missingValuePolicy } : {}),
22920
- ...(policy.errorPolicy ? { errorPolicy: policy.errorPolicy } : {}),
22921
- };
22922
- return Object.keys(normalized).length ? normalized : undefined;
22923
- }
22924
- function normalizeMetadata(metadata) {
22925
- if (!metadata) {
23000
+ resolveTabsWidgetArray(definition, nestedPath, offset) {
23001
+ const config = definition.inputs?.['config'];
23002
+ if (!config || typeof config !== 'object') {
23003
+ return undefined;
23004
+ }
23005
+ const first = nestedPath[offset];
23006
+ if (first?.kind === 'tab') {
23007
+ const tabs = Array.isArray(config['tabs'])
23008
+ ? config['tabs']
23009
+ : [];
23010
+ const tab = this.findBySegment(tabs, first);
23011
+ const widgets = tab && typeof tab === 'object'
23012
+ ? this.asWidgetDefinitions(tab['widgets'])
23013
+ : [];
23014
+ return widgets.length ? { widgets, segmentCount: offset + 1 } : undefined;
23015
+ }
23016
+ const linkSegment = first?.kind === 'nav'
23017
+ ? nestedPath[offset + 1]
23018
+ : first;
23019
+ if (linkSegment?.kind !== 'link') {
23020
+ return undefined;
23021
+ }
23022
+ const nav = config['nav'];
23023
+ const links = nav && typeof nav === 'object' && Array.isArray(nav['links'])
23024
+ ? nav['links']
23025
+ : [];
23026
+ const link = this.findBySegment(links, linkSegment);
23027
+ const widgets = link && typeof link === 'object'
23028
+ ? this.asWidgetDefinitions(link['widgets'])
23029
+ : [];
23030
+ return widgets.length
23031
+ ? { widgets, segmentCount: first?.kind === 'nav' ? offset + 2 : offset + 1 }
23032
+ : undefined;
23033
+ }
23034
+ resolveExpansionWidgetArray(definition, nestedPath, offset) {
23035
+ const config = definition.inputs?.['config'];
23036
+ const panelSegment = nestedPath[offset];
23037
+ if (!config || typeof config !== 'object' || panelSegment?.kind !== 'panel') {
23038
+ return undefined;
23039
+ }
23040
+ const panels = Array.isArray(config['panels'])
23041
+ ? config['panels']
23042
+ : [];
23043
+ const panel = this.findBySegment(panels, panelSegment);
23044
+ const widgets = panel && typeof panel === 'object'
23045
+ ? this.asWidgetDefinitions(panel['widgets'])
23046
+ : [];
23047
+ return widgets.length ? { widgets, segmentCount: offset + 1 } : undefined;
23048
+ }
23049
+ findBySegment(items, segment) {
23050
+ if (segment.id) {
23051
+ const byId = items.find((item) => !!item
23052
+ && typeof item === 'object'
23053
+ && item['id'] === segment.id);
23054
+ if (byId) {
23055
+ return byId;
23056
+ }
23057
+ }
23058
+ if (typeof segment.index === 'number') {
23059
+ return items[segment.index];
23060
+ }
22926
23061
  return undefined;
22927
23062
  }
22928
- const tags = metadata.tags?.length
22929
- ? [...metadata.tags].sort((a, b) => a.localeCompare(b))
22930
- : undefined;
22931
- const source = metadata.source === 'legacy-widget-connection'
22932
- ? undefined
22933
- : metadata.source;
22934
- const normalized = {
22935
- ...(metadata.label ? { label: metadata.label } : {}),
22936
- ...(metadata.description ? { description: metadata.description } : {}),
22937
- ...(tags?.length ? { tags } : {}),
22938
- ...(metadata.deprecated !== undefined ? { deprecated: metadata.deprecated } : {}),
22939
- ...(source ? { source } : {}),
22940
- };
22941
- return Object.keys(normalized).length ? normalized : undefined;
22942
- }
22943
- function compareNormalizedLinks(a, b) {
22944
- return compareValues([
22945
- a.id,
22946
- a.intent,
22947
- endpointSortKey(a.from),
22948
- endpointSortKey(a.to),
22949
- JSON.stringify(a.condition ?? null),
22950
- ], [
22951
- b.id,
22952
- b.intent,
22953
- endpointSortKey(b.from),
22954
- endpointSortKey(b.to),
22955
- JSON.stringify(b.condition ?? null),
22956
- ]);
22957
- }
22958
- function endpointSortKey(endpoint) {
22959
- if (endpoint.kind === 'component-port') {
22960
- return [
22961
- endpoint.kind,
22962
- endpoint.ref.widget,
22963
- endpoint.ref.port,
22964
- endpoint.ref.direction,
22965
- endpoint.ref.bindingPath || '',
22966
- endpoint.ref.componentType || '',
22967
- JSON.stringify(normalizeNestedPath(endpoint.ref.nestedPath)),
22968
- ].join('|');
23063
+ segmentIdentity(source, index) {
23064
+ const id = typeof source['id'] === 'string' ? source['id'].trim() : '';
23065
+ return {
23066
+ ...(id ? { id } : {}),
23067
+ index,
23068
+ };
22969
23069
  }
22970
- if (endpoint.kind === 'global-action') {
22971
- return [
22972
- endpoint.kind,
22973
- endpoint.ref.actionId,
22974
- endpoint.ref.payloadExpr || '',
22975
- JSON.stringify(endpoint.ref.payload ?? null),
22976
- ].join('|');
23070
+ asWidgetDefinitions(value) {
23071
+ return Array.isArray(value) ? value : [];
22977
23072
  }
22978
- return [
22979
- endpoint.kind,
22980
- endpoint.ref.path,
22981
- endpoint.ref.layer || '',
22982
- ].join('|');
22983
- }
22984
- function compareValues(left, right) {
22985
- for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
22986
- const comparison = (left[index] || '').localeCompare(right[index] || '');
22987
- if (comparison !== 0) {
22988
- return comparison;
23073
+ isWidgetDefinition(value) {
23074
+ return !!value
23075
+ && typeof value === 'object'
23076
+ && typeof value.id === 'string';
23077
+ }
23078
+ resolveChildWidgetKey(widget) {
23079
+ return widget.childWidgetKey || '';
23080
+ }
23081
+ clone(value) {
23082
+ if (value == null || typeof value !== 'object') {
23083
+ return value;
22989
23084
  }
23085
+ return JSON.parse(JSON.stringify(value));
22990
23086
  }
22991
- return 0;
22992
- }
22993
- function clone$1(value) {
22994
- if (value == null || typeof value !== 'object') {
22995
- return value;
23087
+ isEqual(left, right) {
23088
+ return JSON.stringify(left) === JSON.stringify(right);
22996
23089
  }
22997
- return JSON.parse(JSON.stringify(value));
22998
23090
  }
22999
23091
 
23000
- /** Utility: generate a random id using crypto.randomUUID or fallback. */
23001
- function generateId() {
23002
- const g = globalThis.crypto?.randomUUID?.();
23003
- return g || `id_${Math.random().toString(36).slice(2, 10)}_${Date.now().toString(36)}`;
23004
- }
23005
- /** Utility: build a natural key from identity fields. */
23006
- function buildPageKey(identity) {
23007
- const app = sanitize(identity.appId || 'app');
23008
- const tenant = sanitize(identity.tenantId || '*');
23009
- const user = sanitize(identity.userId || '*');
23010
- const route = sanitize(identity.routePath || '/');
23011
- const locale = identity.locale ? `:${sanitize(identity.locale)}` : '';
23012
- return `${app}:${tenant}:${user}:${route}${locale}`;
23013
- }
23014
- function sanitize(s) {
23015
- return String(s).replace(/\s+/g, '_').replace(/[:]/g, '-');
23016
- }
23092
+ const WIDGET_PAGE_COMPOSITION_SCHEMA_VERSION = '1.0.0';
23093
+ const STRUCTURAL_NESTED_PATH_KINDS$1 = new Set([
23094
+ 'nav',
23095
+ ]);
23017
23096
  /**
23018
- * Ensure that each widget has a stable id. If missing, assigns a new one.
23019
- * Returns the new page definition (immutable) and an index by key/id.
23097
+ * Normalizes a CompositionLink for deterministic persistence.
23098
+ * Ensures explicit ordering and strips compatibility-only legacy residues.
23020
23099
  */
23021
- function ensurePageIds(page) {
23022
- const widgets = (page.widgets || []).map((w) => ({
23023
- id: w.id || generateId(),
23024
- ...w,
23025
- }));
23026
- const withIds = { ...page, widgets };
23027
- const byKey = new Map();
23028
- const byId = new Map();
23029
- for (const w of widgets) {
23030
- byKey.set(w.key, w);
23031
- byId.set(w.id, w);
23100
+ function normalizeCompositionLink(link) {
23101
+ const normalized = {
23102
+ id: link.id,
23103
+ from: normalizeEndpoint(link.from),
23104
+ to: normalizeEndpoint(link.to),
23105
+ intent: link.intent,
23106
+ };
23107
+ if (link.transform) {
23108
+ normalized.transform = clone$1(link.transform);
23032
23109
  }
23033
- return { page: withIds, byKey, byId };
23110
+ const condition = normalizeCondition(link.condition);
23111
+ if (condition !== undefined) {
23112
+ normalized.condition = condition;
23113
+ }
23114
+ const policy = normalizePolicy(link.policy);
23115
+ if (policy) {
23116
+ normalized.policy = policy;
23117
+ }
23118
+ const metadata = normalizeMetadata(link.metadata);
23119
+ if (metadata) {
23120
+ normalized.metadata = metadata;
23121
+ }
23122
+ return normalized;
23034
23123
  }
23035
23124
  /**
23036
- * Create a persisted page record from an in-memory page definition and identity.
23125
+ * Normalizes an array of CompositionLinks for deterministic persistence.
23037
23126
  */
23038
- function createPersistedPage(identity, page, opts) {
23039
- const persistedPage = writeCompositionLinksToPersistedPage(page, page.composition?.links || []);
23040
- const { page: pageWithIds } = ensurePageIds(persistedPage);
23041
- const now = new Date().toISOString();
23042
- return {
23043
- id: opts?.id || generateId(),
23044
- key: buildPageKey(identity),
23045
- version: opts?.version || '1.0.0',
23046
- title: opts?.title,
23047
- description: opts?.description,
23048
- tags: opts?.tags || [],
23049
- identity,
23050
- status: opts?.status || 'draft',
23051
- page: pageWithIds,
23052
- owner: opts?.owner,
23053
- createdAt: now,
23054
- updatedAt: now,
23055
- parentId: opts?.parentId,
23056
- };
23127
+ function normalizeCompositionLinks(links) {
23128
+ return links
23129
+ .map(normalizeCompositionLink)
23130
+ .sort(compareNormalizedLinks);
23057
23131
  }
23058
-
23059
- // Global configuration model for cross-library defaults
23060
- // Designed to be minimal and avoid coupling to feature-library types
23061
-
23062
- // Global Dialog domain types for GlobalConfig (kept independent of @praxisui/dialog types)
23063
-
23064
- const DOMAIN_CATALOG_CONTEXT_HINT_SCHEMA_VERSION = 'praxis.ai.context-hints.domain-catalog/v0.2';
23065
-
23066
- const DEFAULT_TITLE$1 = 'Governed knowledge timeline';
23067
- const DEFAULT_EMPTY_TEXT$1 = 'No safe timeline events published for this change-set.';
23068
- function domainKnowledgeTimelineToRichContentDocument(timeline, options = {}) {
23132
+ /**
23133
+ * Canonical persisted write surface for CompositionLink[].
23134
+ * New saved shapes must serialize links under `composition.links`.
23135
+ * Semantic guards persist only through `link.condition` as Json Logic.
23136
+ * Operational delivery behavior remains under `link.policy` and must not be
23137
+ * folded into the condition payload.
23138
+ */
23139
+ function serializeCompositionLinks(links) {
23069
23140
  return {
23070
- kind: 'praxis.rich-content',
23071
- version: '1.0.0',
23072
- nodes: [
23073
- {
23074
- type: 'timeline',
23075
- id: `domain-knowledge-timeline:${timeline.changeSetId}`,
23076
- title: options.title ?? DEFAULT_TITLE$1,
23077
- emptyText: options.emptyText ?? DEFAULT_EMPTY_TEXT$1,
23078
- orientation: 'vertical',
23079
- position: 'right',
23080
- connectorVariant: 'solid',
23081
- connectorColor: 'neutral',
23082
- markerVariant: 'icon',
23083
- markerColor: 'info',
23084
- markerStyle: 'filled',
23085
- items: timeline.events
23086
- .filter(isSafeTimelineEvent$1)
23087
- .map((event, index) => mapTimelineEvent$1(event, index)),
23088
- },
23089
- ],
23141
+ version: WIDGET_PAGE_COMPOSITION_SCHEMA_VERSION,
23142
+ links: normalizeCompositionLinks(links),
23090
23143
  };
23091
23144
  }
23092
- function isSafeTimelineEvent$1(event) {
23093
- return event.visibility === 'safe';
23094
- }
23095
- function mapTimelineEvent$1(event, index) {
23096
- const target = formatTarget$1(event);
23097
- const operations = formatOperations(event);
23098
- const meta = [target, operations].filter((value) => Boolean(value)).join(' | ');
23145
+ /**
23146
+ * Writes the canonical persisted composition surface into a page shape.
23147
+ * The canonical writer omits legacy `connections` to avoid dual-write ambiguity.
23148
+ */
23149
+ function writeCompositionLinksToPersistedPage(page, links) {
23150
+ const { composition: _legacyComposition, ...rest } = stripRuntimeOnlyState(clone$1(page), links);
23099
23151
  return {
23100
- id: `domain-knowledge-timeline-event:${index}:${event.eventType}`,
23101
- title: event.summary || event.eventType,
23102
- subtitle: event.actor ? `${event.actorType ?? 'actor'}: ${event.actor}` : event.actorType ?? undefined,
23103
- opposite: event.occurredAt ?? undefined,
23104
- meta: meta || undefined,
23105
- badge: event.validationStatus ?? event.status ?? event.eventType,
23106
- icon: iconForEvent$1(event.eventType),
23107
- markerColor: markerColorForEvent$1(event.eventType),
23108
- connectorColor: markerColorForEvent$1(event.eventType),
23152
+ ...rest,
23153
+ composition: serializeCompositionLinks(links),
23109
23154
  };
23110
23155
  }
23111
- function formatTarget$1(event) {
23112
- const keys = event.targetConceptKeys?.filter(Boolean) ?? [];
23113
- if (!keys.length) {
23114
- return null;
23115
- }
23116
- return `${keys.length} governed concept${keys.length === 1 ? '' : 's'}`;
23117
- }
23118
- function formatOperations(event) {
23119
- const operationTypes = event.operationTypes?.filter(Boolean) ?? [];
23120
- if (!operationTypes.length && event.operationCount == null) {
23121
- return null;
23122
- }
23123
- const count = event.operationCount == null ? null : `${event.operationCount} operation${event.operationCount === 1 ? '' : 's'}`;
23124
- const types = operationTypes.length ? operationTypes.join(', ') : null;
23125
- return [count, types].filter((value) => Boolean(value)).join(': ') || null;
23126
- }
23127
- function iconForEvent$1(eventType) {
23128
- if (eventType.includes('applied')) {
23129
- return 'published_with_changes';
23130
- }
23131
- if (eventType.includes('approved')) {
23132
- return 'verified';
23133
- }
23134
- if (eventType.includes('rejected')) {
23135
- return 'block';
23156
+ function stripRuntimeOnlyState(page, links) {
23157
+ const stateSchema = page.state?.schema || {};
23158
+ const nonPersistentPaths = Object.keys(stateSchema)
23159
+ .filter((path) => stateSchema[path]?.persist === false)
23160
+ .map((path) => path.trim())
23161
+ .filter(Boolean);
23162
+ if (!nonPersistentPaths.length) {
23163
+ return page;
23136
23164
  }
23137
- if (eventType.includes('validation')) {
23138
- return 'rule';
23165
+ const next = clone$1(page);
23166
+ if (next.state?.values) {
23167
+ for (const path of nonPersistentPaths) {
23168
+ removeValueAtPath(next.state.values, path);
23169
+ }
23139
23170
  }
23140
- if (eventType.includes('created')) {
23141
- return 'add_circle';
23171
+ const accessor = new NestedWidgetConfigAccessor();
23172
+ for (const link of links) {
23173
+ const source = link.from;
23174
+ const target = link.to;
23175
+ if (source.kind !== 'state' || target.kind !== 'component-port') {
23176
+ continue;
23177
+ }
23178
+ if (!nonPersistentPaths.some((path) => isPathWithin(source.ref.path, path))) {
23179
+ continue;
23180
+ }
23181
+ const widgetIndex = next.widgets?.findIndex((widget) => widget.key === target.ref.widget) ?? -1;
23182
+ if (widgetIndex < 0 || !next.widgets) {
23183
+ continue;
23184
+ }
23185
+ if (target.ref.nestedPath?.length) {
23186
+ const result = accessor.removeNestedWidgetInput(next.widgets[widgetIndex], target.ref.nestedPath, target.ref.port);
23187
+ if (result.changed) {
23188
+ next.widgets[widgetIndex] = result.widget;
23189
+ }
23190
+ continue;
23191
+ }
23192
+ const inputPath = target.ref.bindingPath || target.ref.port;
23193
+ const inputs = next.widgets[widgetIndex].definition.inputs;
23194
+ if (inputs) {
23195
+ removeValueAtPath(inputs, inputPath);
23196
+ }
23142
23197
  }
23143
- return 'timeline';
23198
+ return next;
23144
23199
  }
23145
- function markerColorForEvent$1(eventType) {
23146
- if (eventType.includes('applied')) {
23147
- return 'success';
23148
- }
23149
- if (eventType.includes('approved')) {
23150
- return 'primary';
23200
+ function isPathWithin(candidate, parent) {
23201
+ return candidate === parent || candidate.startsWith(`${parent}.`);
23202
+ }
23203
+ function removeValueAtPath(target, path) {
23204
+ const segments = String(path || '')
23205
+ .split('.')
23206
+ .map((segment) => segment.trim())
23207
+ .filter(Boolean);
23208
+ if (!segments.length) {
23209
+ return;
23151
23210
  }
23152
- if (eventType.includes('rejected')) {
23153
- return 'error';
23211
+ let current = target;
23212
+ for (const segment of segments.slice(0, -1)) {
23213
+ const next = current[segment];
23214
+ if (!next || typeof next !== 'object' || Array.isArray(next)) {
23215
+ return;
23216
+ }
23217
+ current = next;
23154
23218
  }
23155
- if (eventType.includes('validation')) {
23156
- return 'warning';
23219
+ delete current[segments[segments.length - 1]];
23220
+ }
23221
+ function normalizeEndpoint(endpoint) {
23222
+ if (endpoint.kind === 'component-port') {
23223
+ const nestedPath = normalizeNestedPath(endpoint.ref.nestedPath);
23224
+ return {
23225
+ kind: 'component-port',
23226
+ ref: {
23227
+ widget: endpoint.ref.widget,
23228
+ port: endpoint.ref.port,
23229
+ direction: endpoint.ref.direction,
23230
+ ...(endpoint.ref.componentType ? { componentType: endpoint.ref.componentType } : {}),
23231
+ ...(endpoint.ref.bindingPath ? { bindingPath: endpoint.ref.bindingPath } : {}),
23232
+ ...(nestedPath.length ? { nestedPath } : {}),
23233
+ },
23234
+ };
23157
23235
  }
23158
- if (eventType.includes('created')) {
23159
- return 'info';
23236
+ if (endpoint.kind === 'global-action') {
23237
+ return {
23238
+ kind: 'global-action',
23239
+ ref: normalizeGlobalActionRef(endpoint.ref),
23240
+ };
23160
23241
  }
23161
- return 'neutral';
23242
+ return {
23243
+ kind: 'state',
23244
+ ref: {
23245
+ path: endpoint.ref.path,
23246
+ ...(endpoint.ref.layer ? { layer: endpoint.ref.layer } : {}),
23247
+ },
23248
+ };
23162
23249
  }
23163
-
23164
- const DEFAULT_TITLE = 'Governed decision timeline';
23165
- const DEFAULT_EMPTY_TEXT = 'No safe timeline events published for this decision.';
23166
- function domainRuleTimelineToRichContentDocument(timeline, options = {}) {
23250
+ function normalizeGlobalActionRef(ref) {
23167
23251
  return {
23168
- kind: 'praxis.rich-content',
23169
- version: '1.0.0',
23170
- nodes: [
23171
- {
23172
- type: 'timeline',
23173
- id: `domain-rule-timeline:${timeline.ruleDefinitionId}`,
23174
- title: options.title ?? DEFAULT_TITLE,
23175
- emptyText: options.emptyText ?? DEFAULT_EMPTY_TEXT,
23176
- orientation: 'vertical',
23177
- position: 'right',
23178
- connectorVariant: 'solid',
23179
- connectorColor: 'neutral',
23180
- markerVariant: 'icon',
23181
- markerColor: 'info',
23182
- markerStyle: 'filled',
23183
- items: timeline.events
23184
- .filter(isSafeTimelineEvent)
23185
- .map((event, index) => mapTimelineEvent(event, index)),
23186
- },
23187
- ],
23252
+ actionId: String(ref.actionId || '').trim(),
23253
+ ...(ref.payload !== undefined ? { payload: clone$1(ref.payload) } : {}),
23254
+ ...(ref.payloadExpr ? { payloadExpr: String(ref.payloadExpr).trim() } : {}),
23255
+ ...(ref.meta && typeof ref.meta === 'object' ? { meta: { ...ref.meta } } : {}),
23188
23256
  };
23189
23257
  }
23190
- function isSafeTimelineEvent(event) {
23191
- return event.visibility === 'safe';
23258
+ function normalizeNestedPath(nestedPath) {
23259
+ if (!nestedPath?.length) {
23260
+ return [];
23261
+ }
23262
+ return nestedPath
23263
+ .map((segment) => {
23264
+ const normalized = {
23265
+ kind: segment.kind,
23266
+ };
23267
+ if (segment.id) {
23268
+ normalized.id = segment.id;
23269
+ }
23270
+ if (segment.key) {
23271
+ normalized.key = segment.key;
23272
+ }
23273
+ if (typeof segment.index === 'number' && Number.isFinite(segment.index)) {
23274
+ normalized.index = Math.trunc(segment.index);
23275
+ }
23276
+ if (segment.componentType) {
23277
+ normalized.componentType = segment.componentType;
23278
+ }
23279
+ return normalized;
23280
+ })
23281
+ .filter((segment) => !!segment.kind
23282
+ && (!!segment.id
23283
+ || !!segment.key
23284
+ || typeof segment.index === 'number'
23285
+ || !!segment.componentType
23286
+ || segment.kind === 'widget'
23287
+ || STRUCTURAL_NESTED_PATH_KINDS$1.has(segment.kind)));
23192
23288
  }
23193
- function mapTimelineEvent(event, index) {
23194
- const target = formatTarget(event);
23195
- const sourceHash = event.sourceHash ? `sourceHash: ${event.sourceHash}` : null;
23196
- const meta = [target, sourceHash].filter((value) => Boolean(value)).join(' | ');
23197
- return {
23198
- id: `domain-rule-timeline-event:${index}:${event.eventType}`,
23199
- title: event.summary || event.eventType,
23200
- subtitle: event.actor ? `${event.actorType ?? 'actor'}: ${event.actor}` : event.actorType ?? undefined,
23201
- opposite: event.occurredAt ?? undefined,
23202
- meta: meta || undefined,
23203
- badge: event.targetLayer ?? event.eventType,
23204
- icon: iconForEvent(event.eventType),
23205
- markerColor: markerColorForEvent(event.eventType),
23206
- connectorColor: markerColorForEvent(event.eventType),
23207
- };
23289
+ function normalizeCondition(condition) {
23290
+ if (condition === undefined) {
23291
+ return undefined;
23292
+ }
23293
+ if (condition === null) {
23294
+ return null;
23295
+ }
23296
+ return clone$1(condition);
23208
23297
  }
23209
- function formatTarget(event) {
23210
- const parts = [
23211
- event.targetLayer,
23212
- event.targetArtifactType,
23213
- event.targetArtifactKey,
23214
- ].filter((value) => Boolean(value));
23215
- return parts.length ? parts.join(' / ') : null;
23298
+ function normalizePolicy(policy) {
23299
+ if (!policy) {
23300
+ return undefined;
23301
+ }
23302
+ const normalized = {
23303
+ ...(policy.debounceMs !== undefined ? { debounceMs: policy.debounceMs } : {}),
23304
+ ...(policy.distinct !== undefined ? { distinct: policy.distinct } : {}),
23305
+ ...(policy.distinctBy ? { distinctBy: policy.distinctBy } : {}),
23306
+ ...(policy.delivery ? { delivery: policy.delivery } : {}),
23307
+ ...(policy.missingValuePolicy ? { missingValuePolicy: policy.missingValuePolicy } : {}),
23308
+ ...(policy.errorPolicy ? { errorPolicy: policy.errorPolicy } : {}),
23309
+ };
23310
+ return Object.keys(normalized).length ? normalized : undefined;
23216
23311
  }
23217
- function iconForEvent(eventType) {
23218
- if (eventType.includes('materialization')) {
23219
- return 'deployed_code';
23312
+ function normalizeMetadata(metadata) {
23313
+ if (!metadata) {
23314
+ return undefined;
23220
23315
  }
23221
- if (eventType.includes('approved') || eventType.includes('activated')) {
23222
- return 'verified';
23316
+ const tags = metadata.tags?.length
23317
+ ? [...metadata.tags].sort((a, b) => a.localeCompare(b))
23318
+ : undefined;
23319
+ const source = metadata.source === 'legacy-widget-connection'
23320
+ ? undefined
23321
+ : metadata.source;
23322
+ const normalized = {
23323
+ ...(metadata.label ? { label: metadata.label } : {}),
23324
+ ...(metadata.description ? { description: metadata.description } : {}),
23325
+ ...(tags?.length ? { tags } : {}),
23326
+ ...(metadata.deprecated !== undefined ? { deprecated: metadata.deprecated } : {}),
23327
+ ...(source ? { source } : {}),
23328
+ };
23329
+ return Object.keys(normalized).length ? normalized : undefined;
23330
+ }
23331
+ function compareNormalizedLinks(a, b) {
23332
+ return compareValues([
23333
+ a.id,
23334
+ a.intent,
23335
+ endpointSortKey(a.from),
23336
+ endpointSortKey(a.to),
23337
+ JSON.stringify(a.condition ?? null),
23338
+ ], [
23339
+ b.id,
23340
+ b.intent,
23341
+ endpointSortKey(b.from),
23342
+ endpointSortKey(b.to),
23343
+ JSON.stringify(b.condition ?? null),
23344
+ ]);
23345
+ }
23346
+ function endpointSortKey(endpoint) {
23347
+ if (endpoint.kind === 'component-port') {
23348
+ return [
23349
+ endpoint.kind,
23350
+ endpoint.ref.widget,
23351
+ endpoint.ref.port,
23352
+ endpoint.ref.direction,
23353
+ endpoint.ref.bindingPath || '',
23354
+ endpoint.ref.componentType || '',
23355
+ JSON.stringify(normalizeNestedPath(endpoint.ref.nestedPath)),
23356
+ ].join('|');
23223
23357
  }
23224
- if (eventType.includes('created')) {
23225
- return 'add_circle';
23358
+ if (endpoint.kind === 'global-action') {
23359
+ return [
23360
+ endpoint.kind,
23361
+ endpoint.ref.actionId,
23362
+ endpoint.ref.payloadExpr || '',
23363
+ JSON.stringify(endpoint.ref.payload ?? null),
23364
+ ].join('|');
23226
23365
  }
23227
- return 'timeline';
23366
+ return [
23367
+ endpoint.kind,
23368
+ endpoint.ref.path,
23369
+ endpoint.ref.layer || '',
23370
+ ].join('|');
23228
23371
  }
23229
- function markerColorForEvent(eventType) {
23230
- if (eventType.includes('materialization') || eventType.includes('activated')) {
23231
- return 'success';
23232
- }
23233
- if (eventType.includes('approved')) {
23234
- return 'primary';
23372
+ function compareValues(left, right) {
23373
+ for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
23374
+ const comparison = (left[index] || '').localeCompare(right[index] || '');
23375
+ if (comparison !== 0) {
23376
+ return comparison;
23377
+ }
23235
23378
  }
23236
- if (eventType.includes('created')) {
23237
- return 'info';
23379
+ return 0;
23380
+ }
23381
+ function clone$1(value) {
23382
+ if (value == null || typeof value !== 'object') {
23383
+ return value;
23238
23384
  }
23239
- return 'neutral';
23385
+ return JSON.parse(JSON.stringify(value));
23240
23386
  }
23241
23387
 
23242
- const PRAXIS_QUERY_FILTER_EXPRESSION_SCHEMA_VERSION = 'praxis.query-filter-expression.v1';
23243
- const PRAXIS_QUERY_FILTER_GROUP_OPERATORS = new Set(['all', 'any']);
23244
- const PRAXIS_QUERY_FILTER_PREDICATE_OPERATORS = new Set([
23245
- 'equals',
23246
- 'notEquals',
23247
- 'in',
23248
- 'notIn',
23249
- 'contains',
23250
- 'startsWith',
23251
- 'between',
23252
- 'gte',
23253
- 'lte',
23254
- 'isNull',
23255
- 'isNotNull',
23256
- ]);
23257
- const PRAXIS_QUERY_FILTER_PREDICATE_SOURCE_KINDS = new Set([
23258
- 'selected-records',
23259
- 'manual',
23260
- 'workflow',
23261
- 'current-context',
23262
- ]);
23263
- const PRAXIS_QUERY_FILTER_GOVERNANCE_SOURCES = new Set([
23264
- 'selected-records',
23265
- 'manual',
23266
- 'workflow',
23267
- 'ai-authored',
23268
- ]);
23269
- function normalizeRecord(record) {
23270
- if (!record || typeof record !== 'object')
23271
- return null;
23272
- const next = Object.entries(record).reduce((acc, [key, value]) => {
23273
- if (value === null || value === undefined)
23274
- return acc;
23275
- if (Array.isArray(value) && value.length === 0)
23276
- return acc;
23277
- if (typeof value === 'string' && value.trim() === '')
23278
- return acc;
23279
- acc[key] = value;
23280
- return acc;
23281
- }, {});
23282
- return Object.keys(next).length ? next : null;
23388
+ /** Utility: generate a random id using crypto.randomUUID or fallback. */
23389
+ function generateId() {
23390
+ const g = globalThis.crypto?.randomUUID?.();
23391
+ return g || `id_${Math.random().toString(36).slice(2, 10)}_${Date.now().toString(36)}`;
23283
23392
  }
23284
- function isMeaningfulQueryFilterValue(value) {
23285
- if (value === null || value === undefined)
23286
- return false;
23287
- if (Array.isArray(value))
23288
- return value.some((item) => isMeaningfulQueryFilterValue(item));
23289
- if (typeof value === 'string')
23290
- return value.trim() !== '';
23291
- return true;
23393
+ /** Utility: build a natural key from identity fields. */
23394
+ function buildPageKey(identity) {
23395
+ const app = sanitize(identity.appId || 'app');
23396
+ const tenant = sanitize(identity.tenantId || '*');
23397
+ const user = sanitize(identity.userId || '*');
23398
+ const route = sanitize(identity.routePath || '/');
23399
+ const locale = identity.locale ? `:${sanitize(identity.locale)}` : '';
23400
+ return `${app}:${tenant}:${user}:${route}${locale}`;
23292
23401
  }
23293
- function normalizeQueryFilterSource(source) {
23294
- if (!source || typeof source !== 'object')
23295
- return null;
23296
- const kind = typeof source['kind'] === 'string' ? source['kind'].trim() : '';
23297
- if (!PRAXIS_QUERY_FILTER_PREDICATE_SOURCE_KINDS.has(kind))
23298
- return null;
23299
- const field = typeof source['field'] === 'string' && source['field'].trim()
23300
- ? source['field'].trim()
23301
- : null;
23302
- const selectedIds = Array.isArray(source['selectedIds'])
23303
- ? source['selectedIds'].filter((id) => (typeof id === 'string' && id.trim() !== '') ||
23304
- (typeof id === 'number' && Number.isFinite(id)))
23305
- : [];
23306
- return {
23307
- kind: kind,
23308
- ...(field ? { field } : {}),
23309
- ...(selectedIds.length ? { selectedIds } : {}),
23310
- };
23402
+ function sanitize(s) {
23403
+ return String(s).replace(/\s+/g, '_').replace(/[:]/g, '-');
23311
23404
  }
23312
- function normalizeQueryFilterGovernance(governance) {
23313
- if (!governance || typeof governance !== 'object')
23314
- return null;
23315
- const source = typeof governance['source'] === 'string' ? governance['source'].trim() : '';
23316
- const decisionId = typeof governance['decisionId'] === 'string' && governance['decisionId'].trim()
23317
- ? governance['decisionId'].trim()
23318
- : null;
23319
- const explanation = typeof governance['explanation'] === 'string' && governance['explanation'].trim()
23320
- ? governance['explanation'].trim()
23321
- : null;
23322
- const next = {};
23323
- if (PRAXIS_QUERY_FILTER_GOVERNANCE_SOURCES.has(source)) {
23324
- next.source = source;
23325
- }
23326
- if (decisionId)
23327
- next.decisionId = decisionId;
23328
- if (explanation)
23329
- next.explanation = explanation;
23330
- return Object.keys(next).length ? next : null;
23331
- }
23332
- function normalizePraxisQueryFilterNode(node) {
23333
- if (!node || typeof node !== 'object')
23334
- return null;
23335
- const kind = typeof node['kind'] === 'string' ? node['kind'].trim() : '';
23336
- if (kind === 'group') {
23337
- const operator = typeof node['operator'] === 'string' ? node['operator'].trim() : '';
23338
- if (!PRAXIS_QUERY_FILTER_GROUP_OPERATORS.has(operator))
23339
- return null;
23340
- const clauses = Array.isArray(node['clauses'])
23341
- ? node['clauses']
23342
- .map((clause) => normalizePraxisQueryFilterNode(clause))
23343
- .filter((clause) => !!clause)
23344
- : [];
23345
- if (!clauses.length)
23346
- return null;
23347
- return {
23348
- kind: 'group',
23349
- operator: operator,
23350
- clauses,
23351
- };
23352
- }
23353
- if (kind === 'predicate') {
23354
- const field = typeof node['field'] === 'string' ? node['field'].trim() : '';
23355
- const operator = typeof node['operator'] === 'string' ? node['operator'].trim() : '';
23356
- if (!field || !PRAXIS_QUERY_FILTER_PREDICATE_OPERATORS.has(operator))
23357
- return null;
23358
- const values = Array.isArray(node['values'])
23359
- ? node['values'].filter((value) => isMeaningfulQueryFilterValue(value))
23360
- : [];
23361
- const hasValue = isMeaningfulQueryFilterValue(node['value']);
23362
- const label = typeof node['label'] === 'string' && node['label'].trim()
23363
- ? node['label'].trim()
23364
- : null;
23365
- const source = normalizeQueryFilterSource(node['source']);
23366
- return {
23367
- kind: 'predicate',
23368
- field,
23369
- operator: operator,
23370
- ...(hasValue ? { value: node['value'] } : {}),
23371
- ...(values.length ? { values } : {}),
23372
- ...(label ? { label } : {}),
23373
- ...(source ? { source } : {}),
23374
- };
23405
+ /**
23406
+ * Ensure that each widget has a stable id. If missing, assigns a new one.
23407
+ * Returns the new page definition (immutable) and an index by key/id.
23408
+ */
23409
+ function ensurePageIds(page) {
23410
+ const widgets = (page.widgets || []).map((w) => ({
23411
+ id: w.id || generateId(),
23412
+ ...w,
23413
+ }));
23414
+ const withIds = { ...page, widgets };
23415
+ const byKey = new Map();
23416
+ const byId = new Map();
23417
+ for (const w of widgets) {
23418
+ byKey.set(w.key, w);
23419
+ byId.set(w.id, w);
23375
23420
  }
23376
- return null;
23421
+ return { page: withIds, byKey, byId };
23377
23422
  }
23378
- function normalizePraxisQueryFilterExpression(expression) {
23379
- if (!expression || typeof expression !== 'object')
23380
- return null;
23381
- if (expression.schemaVersion !== PRAXIS_QUERY_FILTER_EXPRESSION_SCHEMA_VERSION)
23382
- return null;
23383
- const root = normalizePraxisQueryFilterNode(expression.root);
23384
- if (!root)
23385
- return null;
23386
- const projection = expression.projection && typeof expression.projection === 'object'
23387
- ? {
23388
- filters: normalizeRecord(expression.projection.filters),
23389
- lossless: expression.projection.lossless === true,
23390
- reason: typeof expression.projection.reason === 'string' && expression.projection.reason.trim()
23391
- ? expression.projection.reason.trim()
23392
- : undefined,
23393
- }
23394
- : null;
23395
- const governance = normalizeQueryFilterGovernance(expression.governance);
23423
+ /**
23424
+ * Create a persisted page record from an in-memory page definition and identity.
23425
+ */
23426
+ function createPersistedPage(identity, page, opts) {
23427
+ const persistedPage = writeCompositionLinksToPersistedPage(page, page.composition?.links || []);
23428
+ const { page: pageWithIds } = ensurePageIds(persistedPage);
23429
+ const now = new Date().toISOString();
23396
23430
  return {
23397
- schemaVersion: PRAXIS_QUERY_FILTER_EXPRESSION_SCHEMA_VERSION,
23398
- root,
23399
- ...(projection ? {
23400
- projection: {
23401
- ...(projection.filters ? { filters: projection.filters } : {}),
23402
- lossless: projection.lossless,
23403
- ...(projection.reason ? { reason: projection.reason } : {}),
23431
+ id: opts?.id || generateId(),
23432
+ key: buildPageKey(identity),
23433
+ version: opts?.version || '1.0.0',
23434
+ title: opts?.title,
23435
+ description: opts?.description,
23436
+ tags: opts?.tags || [],
23437
+ identity,
23438
+ status: opts?.status || 'draft',
23439
+ page: pageWithIds,
23440
+ owner: opts?.owner,
23441
+ createdAt: now,
23442
+ updatedAt: now,
23443
+ parentId: opts?.parentId,
23444
+ };
23445
+ }
23446
+
23447
+ // Global configuration model for cross-library defaults
23448
+ // Designed to be minimal and avoid coupling to feature-library types
23449
+
23450
+ // Global Dialog domain types for GlobalConfig (kept independent of @praxisui/dialog types)
23451
+
23452
+ const DOMAIN_CATALOG_CONTEXT_HINT_SCHEMA_VERSION = 'praxis.ai.context-hints.domain-catalog/v0.2';
23453
+
23454
+ const DEFAULT_TITLE$1 = 'Governed knowledge timeline';
23455
+ const DEFAULT_EMPTY_TEXT$1 = 'No safe timeline events published for this change-set.';
23456
+ function domainKnowledgeTimelineToRichContentDocument(timeline, options = {}) {
23457
+ return {
23458
+ kind: 'praxis.rich-content',
23459
+ version: '1.0.0',
23460
+ nodes: [
23461
+ {
23462
+ type: 'timeline',
23463
+ id: `domain-knowledge-timeline:${timeline.changeSetId}`,
23464
+ title: options.title ?? DEFAULT_TITLE$1,
23465
+ emptyText: options.emptyText ?? DEFAULT_EMPTY_TEXT$1,
23466
+ orientation: 'vertical',
23467
+ position: 'right',
23468
+ connectorVariant: 'solid',
23469
+ connectorColor: 'neutral',
23470
+ markerVariant: 'icon',
23471
+ markerColor: 'info',
23472
+ markerStyle: 'filled',
23473
+ items: timeline.events
23474
+ .filter(isSafeTimelineEvent$1)
23475
+ .map((event, index) => mapTimelineEvent$1(event, index)),
23404
23476
  },
23405
- } : {}),
23406
- ...(governance ? { governance } : {}),
23477
+ ],
23407
23478
  };
23408
23479
  }
23409
- function normalizePraxisDataQueryContext(context) {
23410
- if (!context || typeof context !== 'object')
23411
- return null;
23412
- const filters = normalizeRecord(context.filters);
23413
- const filterExpression = normalizePraxisQueryFilterExpression(context.filterExpression);
23414
- const meta = normalizeRecord(context.meta);
23415
- const sort = Array.isArray(context.sort)
23416
- ? context.sort
23417
- .map((item) => (typeof item === 'string' ? item.trim() : ''))
23418
- .filter((item) => item.length > 0)
23419
- : [];
23420
- const limit = typeof context.limit === 'number' && Number.isFinite(context.limit)
23421
- ? Math.max(0, Math.trunc(context.limit))
23422
- : null;
23423
- const page = context.page && typeof context.page === 'object'
23424
- ? {
23425
- index: typeof context.page.index === 'number' && Number.isFinite(context.page.index)
23426
- ? Math.max(0, Math.trunc(context.page.index))
23427
- : null,
23428
- size: typeof context.page.size === 'number' && Number.isFinite(context.page.size)
23429
- ? Math.max(0, Math.trunc(context.page.size))
23430
- : null,
23431
- }
23432
- : null;
23433
- const next = {};
23434
- if (filters)
23435
- next.filters = filters;
23436
- if (filterExpression)
23437
- next.filterExpression = filterExpression;
23438
- if (sort.length)
23439
- next.sort = sort;
23440
- if (limit !== null)
23441
- next.limit = limit;
23442
- if (page && (page.index !== null || page.size !== null))
23443
- next.page = page;
23444
- if (meta)
23445
- next.meta = meta;
23446
- return Object.keys(next).length ? next : null;
23480
+ function isSafeTimelineEvent$1(event) {
23481
+ return event.visibility === 'safe';
23447
23482
  }
23448
- function resolvePraxisFilterCriteria(filterCriteria, queryContext) {
23449
- const normalizedFilterCriteria = normalizeRecord(filterCriteria);
23450
- const normalizedQueryContext = normalizePraxisDataQueryContext(queryContext);
23483
+ function mapTimelineEvent$1(event, index) {
23484
+ const target = formatTarget$1(event);
23485
+ const operations = formatOperations(event);
23486
+ const meta = [target, operations].filter((value) => Boolean(value)).join(' | ');
23451
23487
  return {
23452
- ...(normalizedFilterCriteria || {}),
23453
- ...(normalizedQueryContext?.filters || {}),
23488
+ id: `domain-knowledge-timeline-event:${index}:${event.eventType}`,
23489
+ title: event.summary || event.eventType,
23490
+ subtitle: event.actor ? `${event.actorType ?? 'actor'}: ${event.actor}` : event.actorType ?? undefined,
23491
+ opposite: event.occurredAt ?? undefined,
23492
+ meta: meta || undefined,
23493
+ badge: event.validationStatus ?? event.status ?? event.eventType,
23494
+ icon: iconForEvent$1(event.eventType),
23495
+ markerColor: markerColorForEvent$1(event.eventType),
23496
+ connectorColor: markerColorForEvent$1(event.eventType),
23454
23497
  };
23455
23498
  }
23499
+ function formatTarget$1(event) {
23500
+ const keys = event.targetConceptKeys?.filter(Boolean) ?? [];
23501
+ if (!keys.length) {
23502
+ return null;
23503
+ }
23504
+ return `${keys.length} governed concept${keys.length === 1 ? '' : 's'}`;
23505
+ }
23506
+ function formatOperations(event) {
23507
+ const operationTypes = event.operationTypes?.filter(Boolean) ?? [];
23508
+ if (!operationTypes.length && event.operationCount == null) {
23509
+ return null;
23510
+ }
23511
+ const count = event.operationCount == null ? null : `${event.operationCount} operation${event.operationCount === 1 ? '' : 's'}`;
23512
+ const types = operationTypes.length ? operationTypes.join(', ') : null;
23513
+ return [count, types].filter((value) => Boolean(value)).join(': ') || null;
23514
+ }
23515
+ function iconForEvent$1(eventType) {
23516
+ if (eventType.includes('applied')) {
23517
+ return 'published_with_changes';
23518
+ }
23519
+ if (eventType.includes('approved')) {
23520
+ return 'verified';
23521
+ }
23522
+ if (eventType.includes('rejected')) {
23523
+ return 'block';
23524
+ }
23525
+ if (eventType.includes('validation')) {
23526
+ return 'rule';
23527
+ }
23528
+ if (eventType.includes('created')) {
23529
+ return 'add_circle';
23530
+ }
23531
+ return 'timeline';
23532
+ }
23533
+ function markerColorForEvent$1(eventType) {
23534
+ if (eventType.includes('applied')) {
23535
+ return 'success';
23536
+ }
23537
+ if (eventType.includes('approved')) {
23538
+ return 'primary';
23539
+ }
23540
+ if (eventType.includes('rejected')) {
23541
+ return 'error';
23542
+ }
23543
+ if (eventType.includes('validation')) {
23544
+ return 'warning';
23545
+ }
23546
+ if (eventType.includes('created')) {
23547
+ return 'info';
23548
+ }
23549
+ return 'neutral';
23550
+ }
23456
23551
 
23457
- const DEFAULT_FIELD_SELECTOR_CONTROL_TYPE_MAP = {
23458
- 'pdx-text-input': FieldControlType.INPUT,
23552
+ const DEFAULT_TITLE = 'Governed decision timeline';
23553
+ const DEFAULT_EMPTY_TEXT = 'No safe timeline events published for this decision.';
23554
+ function domainRuleTimelineToRichContentDocument(timeline, options = {}) {
23555
+ return {
23556
+ kind: 'praxis.rich-content',
23557
+ version: '1.0.0',
23558
+ nodes: [
23559
+ {
23560
+ type: 'timeline',
23561
+ id: `domain-rule-timeline:${timeline.ruleDefinitionId}`,
23562
+ title: options.title ?? DEFAULT_TITLE,
23563
+ emptyText: options.emptyText ?? DEFAULT_EMPTY_TEXT,
23564
+ orientation: 'vertical',
23565
+ position: 'right',
23566
+ connectorVariant: 'solid',
23567
+ connectorColor: 'neutral',
23568
+ markerVariant: 'icon',
23569
+ markerColor: 'info',
23570
+ markerStyle: 'filled',
23571
+ items: timeline.events
23572
+ .filter(isSafeTimelineEvent)
23573
+ .map((event, index) => mapTimelineEvent(event, index)),
23574
+ },
23575
+ ],
23576
+ };
23577
+ }
23578
+ function isSafeTimelineEvent(event) {
23579
+ return event.visibility === 'safe';
23580
+ }
23581
+ function mapTimelineEvent(event, index) {
23582
+ const target = formatTarget(event);
23583
+ const sourceHash = event.sourceHash ? `sourceHash: ${event.sourceHash}` : null;
23584
+ const meta = [target, sourceHash].filter((value) => Boolean(value)).join(' | ');
23585
+ return {
23586
+ id: `domain-rule-timeline-event:${index}:${event.eventType}`,
23587
+ title: event.summary || event.eventType,
23588
+ subtitle: event.actor ? `${event.actorType ?? 'actor'}: ${event.actor}` : event.actorType ?? undefined,
23589
+ opposite: event.occurredAt ?? undefined,
23590
+ meta: meta || undefined,
23591
+ badge: event.targetLayer ?? event.eventType,
23592
+ icon: iconForEvent(event.eventType),
23593
+ markerColor: markerColorForEvent(event.eventType),
23594
+ connectorColor: markerColorForEvent(event.eventType),
23595
+ };
23596
+ }
23597
+ function formatTarget(event) {
23598
+ const parts = [
23599
+ event.targetLayer,
23600
+ event.targetArtifactType,
23601
+ event.targetArtifactKey,
23602
+ ].filter((value) => Boolean(value));
23603
+ return parts.length ? parts.join(' / ') : null;
23604
+ }
23605
+ function iconForEvent(eventType) {
23606
+ if (eventType.includes('materialization')) {
23607
+ return 'deployed_code';
23608
+ }
23609
+ if (eventType.includes('approved') || eventType.includes('activated')) {
23610
+ return 'verified';
23611
+ }
23612
+ if (eventType.includes('created')) {
23613
+ return 'add_circle';
23614
+ }
23615
+ return 'timeline';
23616
+ }
23617
+ function markerColorForEvent(eventType) {
23618
+ if (eventType.includes('materialization') || eventType.includes('activated')) {
23619
+ return 'success';
23620
+ }
23621
+ if (eventType.includes('approved')) {
23622
+ return 'primary';
23623
+ }
23624
+ if (eventType.includes('created')) {
23625
+ return 'info';
23626
+ }
23627
+ return 'neutral';
23628
+ }
23629
+
23630
+ const PRAXIS_QUERY_FILTER_EXPRESSION_SCHEMA_VERSION = 'praxis.query-filter-expression.v1';
23631
+ const PRAXIS_QUERY_FILTER_GROUP_OPERATORS = new Set(['all', 'any']);
23632
+ const PRAXIS_QUERY_FILTER_PREDICATE_OPERATORS = new Set([
23633
+ 'equals',
23634
+ 'notEquals',
23635
+ 'in',
23636
+ 'notIn',
23637
+ 'contains',
23638
+ 'startsWith',
23639
+ 'between',
23640
+ 'gte',
23641
+ 'lte',
23642
+ 'isNull',
23643
+ 'isNotNull',
23644
+ ]);
23645
+ const PRAXIS_QUERY_FILTER_PREDICATE_SOURCE_KINDS = new Set([
23646
+ 'selected-records',
23647
+ 'manual',
23648
+ 'workflow',
23649
+ 'current-context',
23650
+ ]);
23651
+ const PRAXIS_QUERY_FILTER_GOVERNANCE_SOURCES = new Set([
23652
+ 'selected-records',
23653
+ 'manual',
23654
+ 'workflow',
23655
+ 'ai-authored',
23656
+ ]);
23657
+ function normalizeRecord(record) {
23658
+ if (!record || typeof record !== 'object')
23659
+ return null;
23660
+ const next = Object.entries(record).reduce((acc, [key, value]) => {
23661
+ if (value === null || value === undefined)
23662
+ return acc;
23663
+ if (Array.isArray(value) && value.length === 0)
23664
+ return acc;
23665
+ if (typeof value === 'string' && value.trim() === '')
23666
+ return acc;
23667
+ acc[key] = value;
23668
+ return acc;
23669
+ }, {});
23670
+ return Object.keys(next).length ? next : null;
23671
+ }
23672
+ function isMeaningfulQueryFilterValue(value) {
23673
+ if (value === null || value === undefined)
23674
+ return false;
23675
+ if (Array.isArray(value))
23676
+ return value.some((item) => isMeaningfulQueryFilterValue(item));
23677
+ if (typeof value === 'string')
23678
+ return value.trim() !== '';
23679
+ return true;
23680
+ }
23681
+ function normalizeQueryFilterSource(source) {
23682
+ if (!source || typeof source !== 'object')
23683
+ return null;
23684
+ const kind = typeof source['kind'] === 'string' ? source['kind'].trim() : '';
23685
+ if (!PRAXIS_QUERY_FILTER_PREDICATE_SOURCE_KINDS.has(kind))
23686
+ return null;
23687
+ const field = typeof source['field'] === 'string' && source['field'].trim()
23688
+ ? source['field'].trim()
23689
+ : null;
23690
+ const selectedIds = Array.isArray(source['selectedIds'])
23691
+ ? source['selectedIds'].filter((id) => (typeof id === 'string' && id.trim() !== '') ||
23692
+ (typeof id === 'number' && Number.isFinite(id)))
23693
+ : [];
23694
+ return {
23695
+ kind: kind,
23696
+ ...(field ? { field } : {}),
23697
+ ...(selectedIds.length ? { selectedIds } : {}),
23698
+ };
23699
+ }
23700
+ function normalizeQueryFilterGovernance(governance) {
23701
+ if (!governance || typeof governance !== 'object')
23702
+ return null;
23703
+ const source = typeof governance['source'] === 'string' ? governance['source'].trim() : '';
23704
+ const decisionId = typeof governance['decisionId'] === 'string' && governance['decisionId'].trim()
23705
+ ? governance['decisionId'].trim()
23706
+ : null;
23707
+ const explanation = typeof governance['explanation'] === 'string' && governance['explanation'].trim()
23708
+ ? governance['explanation'].trim()
23709
+ : null;
23710
+ const next = {};
23711
+ if (PRAXIS_QUERY_FILTER_GOVERNANCE_SOURCES.has(source)) {
23712
+ next.source = source;
23713
+ }
23714
+ if (decisionId)
23715
+ next.decisionId = decisionId;
23716
+ if (explanation)
23717
+ next.explanation = explanation;
23718
+ return Object.keys(next).length ? next : null;
23719
+ }
23720
+ function normalizePraxisQueryFilterNode(node) {
23721
+ if (!node || typeof node !== 'object')
23722
+ return null;
23723
+ const kind = typeof node['kind'] === 'string' ? node['kind'].trim() : '';
23724
+ if (kind === 'group') {
23725
+ const operator = typeof node['operator'] === 'string' ? node['operator'].trim() : '';
23726
+ if (!PRAXIS_QUERY_FILTER_GROUP_OPERATORS.has(operator))
23727
+ return null;
23728
+ const clauses = Array.isArray(node['clauses'])
23729
+ ? node['clauses']
23730
+ .map((clause) => normalizePraxisQueryFilterNode(clause))
23731
+ .filter((clause) => !!clause)
23732
+ : [];
23733
+ if (!clauses.length)
23734
+ return null;
23735
+ return {
23736
+ kind: 'group',
23737
+ operator: operator,
23738
+ clauses,
23739
+ };
23740
+ }
23741
+ if (kind === 'predicate') {
23742
+ const field = typeof node['field'] === 'string' ? node['field'].trim() : '';
23743
+ const operator = typeof node['operator'] === 'string' ? node['operator'].trim() : '';
23744
+ if (!field || !PRAXIS_QUERY_FILTER_PREDICATE_OPERATORS.has(operator))
23745
+ return null;
23746
+ const values = Array.isArray(node['values'])
23747
+ ? node['values'].filter((value) => isMeaningfulQueryFilterValue(value))
23748
+ : [];
23749
+ const hasValue = isMeaningfulQueryFilterValue(node['value']);
23750
+ const label = typeof node['label'] === 'string' && node['label'].trim()
23751
+ ? node['label'].trim()
23752
+ : null;
23753
+ const source = normalizeQueryFilterSource(node['source']);
23754
+ return {
23755
+ kind: 'predicate',
23756
+ field,
23757
+ operator: operator,
23758
+ ...(hasValue ? { value: node['value'] } : {}),
23759
+ ...(values.length ? { values } : {}),
23760
+ ...(label ? { label } : {}),
23761
+ ...(source ? { source } : {}),
23762
+ };
23763
+ }
23764
+ return null;
23765
+ }
23766
+ function normalizePraxisQueryFilterExpression(expression) {
23767
+ if (!expression || typeof expression !== 'object')
23768
+ return null;
23769
+ if (expression.schemaVersion !== PRAXIS_QUERY_FILTER_EXPRESSION_SCHEMA_VERSION)
23770
+ return null;
23771
+ const root = normalizePraxisQueryFilterNode(expression.root);
23772
+ if (!root)
23773
+ return null;
23774
+ const projection = expression.projection && typeof expression.projection === 'object'
23775
+ ? {
23776
+ filters: normalizeRecord(expression.projection.filters),
23777
+ lossless: expression.projection.lossless === true,
23778
+ reason: typeof expression.projection.reason === 'string' && expression.projection.reason.trim()
23779
+ ? expression.projection.reason.trim()
23780
+ : undefined,
23781
+ }
23782
+ : null;
23783
+ const governance = normalizeQueryFilterGovernance(expression.governance);
23784
+ return {
23785
+ schemaVersion: PRAXIS_QUERY_FILTER_EXPRESSION_SCHEMA_VERSION,
23786
+ root,
23787
+ ...(projection ? {
23788
+ projection: {
23789
+ ...(projection.filters ? { filters: projection.filters } : {}),
23790
+ lossless: projection.lossless,
23791
+ ...(projection.reason ? { reason: projection.reason } : {}),
23792
+ },
23793
+ } : {}),
23794
+ ...(governance ? { governance } : {}),
23795
+ };
23796
+ }
23797
+ function normalizePraxisDataQueryContext(context) {
23798
+ if (!context || typeof context !== 'object')
23799
+ return null;
23800
+ const filters = normalizeRecord(context.filters);
23801
+ const filterExpression = normalizePraxisQueryFilterExpression(context.filterExpression);
23802
+ const meta = normalizeRecord(context.meta);
23803
+ const sort = Array.isArray(context.sort)
23804
+ ? context.sort
23805
+ .map((item) => (typeof item === 'string' ? item.trim() : ''))
23806
+ .filter((item) => item.length > 0)
23807
+ : [];
23808
+ const limit = typeof context.limit === 'number' && Number.isFinite(context.limit)
23809
+ ? Math.max(0, Math.trunc(context.limit))
23810
+ : null;
23811
+ const page = context.page && typeof context.page === 'object'
23812
+ ? {
23813
+ index: typeof context.page.index === 'number' && Number.isFinite(context.page.index)
23814
+ ? Math.max(0, Math.trunc(context.page.index))
23815
+ : null,
23816
+ size: typeof context.page.size === 'number' && Number.isFinite(context.page.size)
23817
+ ? Math.max(0, Math.trunc(context.page.size))
23818
+ : null,
23819
+ }
23820
+ : null;
23821
+ const next = {};
23822
+ if (filters)
23823
+ next.filters = filters;
23824
+ if (filterExpression)
23825
+ next.filterExpression = filterExpression;
23826
+ if (sort.length)
23827
+ next.sort = sort;
23828
+ if (limit !== null)
23829
+ next.limit = limit;
23830
+ if (page && (page.index !== null || page.size !== null))
23831
+ next.page = page;
23832
+ if (meta)
23833
+ next.meta = meta;
23834
+ return Object.keys(next).length ? next : null;
23835
+ }
23836
+ function resolvePraxisFilterCriteria(filterCriteria, queryContext) {
23837
+ const normalizedFilterCriteria = normalizeRecord(filterCriteria);
23838
+ const normalizedQueryContext = normalizePraxisDataQueryContext(queryContext);
23839
+ return {
23840
+ ...(normalizedFilterCriteria || {}),
23841
+ ...(normalizedQueryContext?.filters || {}),
23842
+ };
23843
+ }
23844
+
23845
+ const DEFAULT_FIELD_SELECTOR_CONTROL_TYPE_MAP = {
23846
+ 'pdx-text-input': FieldControlType.INPUT,
23459
23847
  'pdx-email-input': FieldControlType.EMAIL_INPUT,
23460
23848
  'pdx-password-input': FieldControlType.PASSWORD,
23461
23849
  'pdx-color-input': FieldControlType.COLOR_INPUT,
@@ -28973,457 +29361,175 @@ const DOMAIN_CATALOG_COMPONENT_CONTEXT_PACK = {
28973
29361
  'domainCatalog.items[].payload.aiUsage.visibility': {
28974
29362
  mode: 'suggested',
28975
29363
  options: [
28976
- { value: 'public', label: 'Visible to public AI tasks' },
28977
- { value: 'internal', label: 'Visible to internal AI tasks' },
28978
- { value: 'restricted', label: 'Restricted AI visibility' },
28979
- { value: 'forbidden', label: 'Forbidden for AI use' },
28980
- ],
28981
- },
28982
- },
28983
- actionCatalog: [
28984
- {
28985
- id: 'domainCatalog.release.list',
28986
- operation: 'read',
28987
- scope: 'GLOBAL',
28988
- intentExamples: ['domain catalog releases', 'listar releases de dominio', 'catalogo semantico'],
28989
- patchTemplate: {},
28990
- safetyNotes: 'Read-only operation. Use DomainCatalogService.listReleases; do not write FormConfig or component config with returned governance rules.',
28991
- },
28992
- {
28993
- id: 'domainCatalog.governanceContext.read',
28994
- operation: 'read',
28995
- scope: 'GLOBAL',
28996
- valueType: 'OBJECT',
28997
- intentExamples: ['governanca', 'LGPD', 'GDPR', 'dados sensiveis', 'ai visibility'],
28998
- params: [
28999
- { name: 'resourceKey', type: 'STRING', description: 'Stable semantic resource key, for example human-resources.funcionarios.' },
29000
- { name: 'query', type: 'STRING', description: 'Field or concept probe, for example cpf, salario or status.' },
29001
- ],
29002
- patchTemplate: {},
29003
- safetyNotes: 'Read-only operation. Resolve governance through DomainCatalogService.getGovernanceContext(resourceKey, query) before proposing UI, rule or AI-use changes.',
29004
- },
29005
- {
29006
- id: 'contextHints.domainCatalog.forward',
29007
- operation: 'read',
29008
- scope: 'GLOBAL',
29009
- valueType: 'OBJECT',
29010
- intentExamples: ['contextHints.domainCatalog', 'authoring hints', 'semantic retrieval hint', 'llm domain context'],
29011
- params: [
29012
- { name: 'serviceKey', type: 'STRING', description: 'Semantic owner service, for example praxis-service.' },
29013
- { name: 'resourceKey', type: 'STRING', description: 'Stable semantic resource key, for example human-resources.funcionarios.' },
29014
- { name: 'type', type: 'STRING', description: 'Canonical item focus: context, node, edge, binding, evidence, governance, vocabulary or relationship.' },
29015
- { name: 'intent', type: 'STRING', description: 'Retrieval purpose: authoring, explain, validate or ai-access-control.' },
29016
- { name: 'query', type: 'STRING', description: 'Business query used to retrieve relevant domain context.' },
29017
- ],
29018
- patchTemplate: {
29019
- contextHints: {
29020
- domainCatalog: {
29021
- schemaVersion: 'praxis.ai.context-hints.domain-catalog/v0.2',
29022
- recommendedAuthoringFlow: 'shared_rule_authoring',
29023
- recommendedRuleType: 'privacy',
29024
- serviceKey: '<serviceKey>',
29025
- resourceKey: '<resourceKey>',
29026
- type: 'governance',
29027
- itemTypes: ['governance'],
29028
- intent: 'authoring',
29029
- query: '<business query>',
29030
- limit: 12,
29031
- relationships: {
29032
- enabled: true,
29033
- federated: true,
29034
- query: '<business query>',
29035
- limit: 8,
29036
- },
29037
- },
29038
- },
29039
- },
29040
- safetyNotes: 'Forward these hints to AI authoring calls as runtime retrieval context. Do not convert them into executable rules or persist them as FormConfig unless an approved backend materialization flow says so.',
29041
- },
29042
- ],
29043
- fieldResolvers: {
29044
- resourceKey: [
29045
- 'Prefer backend-provided resourceKey from surfaces, actions, capabilities or api_metadata.',
29046
- 'Do not derive resourceKey from resource.path when discovery already returned a stable semantic key.',
29047
- ],
29048
- governanceProbe: [
29049
- 'Use the business field/concept name as query: cpf, salario, status, email, documento.',
29050
- 'Keep probes narrow; request more context instead of loading every governance item into a prompt.',
29051
- ],
29364
+ { value: 'public', label: 'Visible to public AI tasks' },
29365
+ { value: 'internal', label: 'Visible to internal AI tasks' },
29366
+ { value: 'restricted', label: 'Restricted AI visibility' },
29367
+ { value: 'forbidden', label: 'Forbidden for AI use' },
29368
+ ],
29369
+ },
29052
29370
  },
29053
- hints: [
29054
- 'Domain Catalog is a runtime semantic/governance context source, not a FormConfig rule store.',
29055
- 'Use DomainCatalogService for read-only lookup of domain vocabulary, relationships and governance items.',
29056
- 'LLM tasks must treat aiUsage.visibility and complianceTags as prompt/data-access constraints before generating patches.',
29057
- 'Use contextHints.domainCatalog with schemaVersion praxis.ai.context-hints.domain-catalog/v0.2 to share runtime semantic retrieval intent between frontend and backend.',
29058
- 'Shared domain rules may be referenced by resourceKey and catalog release, but should not be copied into component JSON unless intentionally materialized by a backend policy.',
29059
- ],
29060
- };
29061
-
29062
- const OWNER_EVENT_SEGMENT_KINDS = new Set(['tabs', 'expansion', 'stepper']);
29063
- const STRUCTURAL_NESTED_PATH_KINDS = new Set([
29064
- 'nav',
29065
- ]);
29066
- function normalizeWidgetEventPath(event, options = {}) {
29067
- const normalized = [];
29068
- const sourceChildWidgetKey = normalizeString(event.sourceChildWidgetKey);
29069
- const sourceComponentId = normalizeString(event.sourceComponentId);
29070
- for (const [index, segment] of (event.path || []).entries()) {
29071
- if (isOwnerEventSegment(segment, index, options)) {
29072
- continue;
29073
- }
29074
- const normalizedSegment = normalizeEventSegment(segment);
29075
- if (normalizedSegment) {
29076
- normalized.push(normalizedSegment);
29077
- }
29078
- }
29079
- const terminal = normalized[normalized.length - 1];
29080
- if (terminal?.kind === 'widget') {
29081
- if (!terminal.key && sourceChildWidgetKey) {
29082
- terminal.key = sourceChildWidgetKey;
29083
- }
29084
- if (!terminal.componentType && sourceComponentId) {
29085
- terminal.componentType = sourceComponentId;
29086
- }
29087
- return stripEmptySegments(normalized);
29088
- }
29089
- if (sourceChildWidgetKey) {
29090
- normalized.push({
29091
- kind: 'widget',
29092
- key: sourceChildWidgetKey,
29093
- ...(sourceComponentId ? { componentType: sourceComponentId } : {}),
29094
- });
29095
- }
29096
- return stripEmptySegments(normalized);
29097
- }
29098
- function normalizeEventSegment(segment) {
29099
- if (segment.kind === 'tabs') {
29100
- return null;
29101
- }
29102
- const kind = segment.kind === 'widget'
29103
- ? 'widget'
29104
- : segment.kind;
29105
- const key = normalizeString(segment.key);
29106
- const id = normalizeString(segment.id);
29107
- const componentType = normalizeString(segment.componentType);
29108
- const normalized = { kind };
29109
- if (id) {
29110
- normalized.id = id;
29111
- }
29112
- if (key) {
29113
- normalized.key = key;
29114
- }
29115
- if (typeof segment.index === 'number' && Number.isFinite(segment.index)) {
29116
- normalized.index = Math.trunc(segment.index);
29117
- }
29118
- if (componentType) {
29119
- normalized.componentType = componentType;
29120
- }
29121
- return normalized;
29122
- }
29123
- function isOwnerEventSegment(segment, index, options) {
29124
- if (index !== 0 || !OWNER_EVENT_SEGMENT_KINDS.has(segment.kind)) {
29125
- return false;
29126
- }
29127
- const ownerComponentId = normalizeString(options.ownerComponentId);
29128
- if (!ownerComponentId) {
29129
- return true;
29130
- }
29131
- return segment.kind === ownerComponentId
29132
- || segment.id === ownerComponentId;
29133
- }
29134
- function stripEmptySegments(segments) {
29135
- return segments.filter((segment) => !!segment.kind
29136
- && (segment.kind === 'widget'
29137
- || STRUCTURAL_NESTED_PATH_KINDS.has(segment.kind)
29138
- || !!segment.id
29139
- || !!segment.key
29140
- || typeof segment.index === 'number'
29141
- || !!segment.componentType));
29142
- }
29143
- function normalizeString(value) {
29144
- return typeof value === 'string' ? value.trim() : '';
29145
- }
29146
-
29147
- class NestedWidgetConfigAccessor {
29148
- listNestedWidgets(owner) {
29149
- return this.listNestedWidgetsInDefinition(owner.key, owner.definition, []);
29150
- }
29151
- resolveNestedWidget(owner, nestedPath) {
29152
- if (!nestedPath?.length) {
29153
- return undefined;
29154
- }
29155
- return this.resolveNestedWidgetInDefinition(owner.definition, nestedPath);
29156
- }
29157
- setNestedWidgetInput(owner, nestedPath, inputName, value) {
29158
- const normalizedInputName = String(inputName || '').trim();
29159
- if (!nestedPath?.length || !normalizedInputName) {
29160
- return { widget: this.clone(owner), changed: false };
29161
- }
29162
- const next = this.clone(owner);
29163
- const changed = this.setNestedWidgetInputInDefinition(next.definition, nestedPath, normalizedInputName, this.clone(value));
29164
- return { widget: next, changed };
29165
- }
29166
- listNestedWidgetsInDefinition(ownerWidgetKey, definition, basePath) {
29167
- const results = [];
29168
- for (const location of this.listChildWidgetLocations(definition)) {
29169
- for (const [index, widget] of location.widgets.entries()) {
29170
- if (!this.isWidgetDefinition(widget)) {
29171
- continue;
29172
- }
29173
- const childWidgetKey = this.resolveChildWidgetKey(widget);
29174
- const widgetSegment = {
29175
- kind: 'widget',
29176
- ...(childWidgetKey ? { key: childWidgetKey } : { index }),
29177
- componentType: widget.id,
29178
- };
29179
- const nestedPath = [
29180
- ...basePath,
29181
- ...location.path,
29182
- widgetSegment,
29183
- ];
29184
- results.push({
29185
- ownerWidgetKey,
29186
- nestedPath,
29187
- widget: this.clone(widget),
29188
- componentId: widget.id,
29189
- childWidgetKey,
29190
- });
29191
- results.push(...this.listNestedWidgetsInDefinition(ownerWidgetKey, widget, nestedPath));
29192
- }
29193
- }
29194
- return results;
29195
- }
29196
- resolveNestedWidgetInDefinition(definition, nestedPath) {
29197
- const location = this.resolveWidgetArrayLocation(definition, nestedPath, 0);
29198
- if (!location) {
29199
- return undefined;
29200
- }
29201
- const widgetSegment = nestedPath[location.segmentCount];
29202
- if (widgetSegment?.kind !== 'widget' || !widgetSegment.key) {
29203
- return undefined;
29204
- }
29205
- const child = location.widgets.find((widget) => widget.childWidgetKey === widgetSegment.key);
29206
- if (!this.isWidgetDefinition(child)) {
29207
- return undefined;
29208
- }
29209
- const remaining = nestedPath.slice(location.segmentCount + 1);
29210
- if (!remaining.length) {
29211
- return this.clone(child);
29212
- }
29213
- return this.resolveNestedWidgetInDefinition(child, remaining);
29214
- }
29215
- setNestedWidgetInputInDefinition(definition, nestedPath, inputName, value) {
29216
- const location = this.resolveWidgetArrayLocation(definition, nestedPath, 0);
29217
- if (!location) {
29218
- return false;
29219
- }
29220
- const widgetSegment = nestedPath[location.segmentCount];
29221
- if (widgetSegment?.kind !== 'widget' || !widgetSegment.key) {
29222
- return false;
29223
- }
29224
- const widgetIndex = location.widgets.findIndex((widget) => widget.childWidgetKey === widgetSegment.key);
29225
- if (widgetIndex < 0) {
29226
- return false;
29227
- }
29228
- const remaining = nestedPath.slice(location.segmentCount + 1);
29229
- const child = location.widgets[widgetIndex];
29230
- if (!this.isWidgetDefinition(child)) {
29231
- return false;
29232
- }
29233
- if (remaining.length) {
29234
- return this.setNestedWidgetInputInDefinition(child, remaining, inputName, value);
29235
- }
29236
- const currentInputs = child.inputs || {};
29237
- const nextInputs = {
29238
- ...currentInputs,
29239
- [inputName]: value,
29240
- };
29241
- if (this.isEqual(currentInputs, nextInputs)) {
29242
- return false;
29243
- }
29244
- location.widgets[widgetIndex] = {
29245
- ...child,
29246
- inputs: nextInputs,
29247
- };
29248
- return true;
29249
- }
29250
- listChildWidgetLocations(definition) {
29251
- const config = definition.inputs?.['config'];
29252
- if (!config || typeof config !== 'object') {
29253
- return [];
29254
- }
29255
- if (definition.id === 'praxis-tabs') {
29256
- return this.listTabsWidgetLocations(config);
29257
- }
29258
- if (definition.id === 'praxis-expansion') {
29259
- return this.listExpansionWidgetLocations(config);
29260
- }
29261
- return [];
29262
- }
29263
- listTabsWidgetLocations(config) {
29264
- const locations = [];
29265
- const tabs = Array.isArray(config['tabs']) ? config['tabs'] : [];
29266
- for (const [index, tab] of tabs.entries()) {
29267
- if (!tab || typeof tab !== 'object') {
29268
- continue;
29269
- }
29270
- const widgets = this.asWidgetDefinitions(tab['widgets']);
29271
- if (!widgets.some((widget) => this.isWidgetDefinition(widget))) {
29272
- continue;
29273
- }
29274
- locations.push({
29275
- path: [{
29276
- kind: 'tab',
29277
- ...this.segmentIdentity(tab, index),
29278
- }],
29279
- widgets,
29280
- });
29281
- }
29282
- const nav = config['nav'];
29283
- const links = nav && typeof nav === 'object' && Array.isArray(nav['links'])
29284
- ? nav['links']
29285
- : [];
29286
- for (const [index, link] of links.entries()) {
29287
- if (!link || typeof link !== 'object') {
29288
- continue;
29289
- }
29290
- const widgets = this.asWidgetDefinitions(link['widgets']);
29291
- if (!widgets.some((widget) => this.isWidgetDefinition(widget))) {
29292
- continue;
29293
- }
29294
- locations.push({
29295
- path: [
29296
- { kind: 'nav' },
29297
- {
29298
- kind: 'link',
29299
- ...this.segmentIdentity(link, index),
29371
+ actionCatalog: [
29372
+ {
29373
+ id: 'domainCatalog.release.list',
29374
+ operation: 'read',
29375
+ scope: 'GLOBAL',
29376
+ intentExamples: ['domain catalog releases', 'listar releases de dominio', 'catalogo semantico'],
29377
+ patchTemplate: {},
29378
+ safetyNotes: 'Read-only operation. Use DomainCatalogService.listReleases; do not write FormConfig or component config with returned governance rules.',
29379
+ },
29380
+ {
29381
+ id: 'domainCatalog.governanceContext.read',
29382
+ operation: 'read',
29383
+ scope: 'GLOBAL',
29384
+ valueType: 'OBJECT',
29385
+ intentExamples: ['governanca', 'LGPD', 'GDPR', 'dados sensiveis', 'ai visibility'],
29386
+ params: [
29387
+ { name: 'resourceKey', type: 'STRING', description: 'Stable semantic resource key, for example human-resources.funcionarios.' },
29388
+ { name: 'query', type: 'STRING', description: 'Field or concept probe, for example cpf, salario or status.' },
29389
+ ],
29390
+ patchTemplate: {},
29391
+ safetyNotes: 'Read-only operation. Resolve governance through DomainCatalogService.getGovernanceContext(resourceKey, query) before proposing UI, rule or AI-use changes.',
29392
+ },
29393
+ {
29394
+ id: 'contextHints.domainCatalog.forward',
29395
+ operation: 'read',
29396
+ scope: 'GLOBAL',
29397
+ valueType: 'OBJECT',
29398
+ intentExamples: ['contextHints.domainCatalog', 'authoring hints', 'semantic retrieval hint', 'llm domain context'],
29399
+ params: [
29400
+ { name: 'serviceKey', type: 'STRING', description: 'Semantic owner service, for example praxis-service.' },
29401
+ { name: 'resourceKey', type: 'STRING', description: 'Stable semantic resource key, for example human-resources.funcionarios.' },
29402
+ { name: 'type', type: 'STRING', description: 'Canonical item focus: context, node, edge, binding, evidence, governance, vocabulary or relationship.' },
29403
+ { name: 'intent', type: 'STRING', description: 'Retrieval purpose: authoring, explain, validate or ai-access-control.' },
29404
+ { name: 'query', type: 'STRING', description: 'Business query used to retrieve relevant domain context.' },
29405
+ ],
29406
+ patchTemplate: {
29407
+ contextHints: {
29408
+ domainCatalog: {
29409
+ schemaVersion: 'praxis.ai.context-hints.domain-catalog/v0.2',
29410
+ recommendedAuthoringFlow: 'shared_rule_authoring',
29411
+ recommendedRuleType: 'privacy',
29412
+ serviceKey: '<serviceKey>',
29413
+ resourceKey: '<resourceKey>',
29414
+ type: 'governance',
29415
+ itemTypes: ['governance'],
29416
+ intent: 'authoring',
29417
+ query: '<business query>',
29418
+ limit: 12,
29419
+ relationships: {
29420
+ enabled: true,
29421
+ federated: true,
29422
+ query: '<business query>',
29423
+ limit: 8,
29424
+ },
29300
29425
  },
29301
- ],
29302
- widgets,
29303
- });
29304
- }
29305
- return locations;
29306
- }
29307
- listExpansionWidgetLocations(config) {
29308
- const panels = Array.isArray(config['panels']) ? config['panels'] : [];
29309
- const locations = [];
29310
- for (const [index, panel] of panels.entries()) {
29311
- if (!panel || typeof panel !== 'object') {
29312
- continue;
29313
- }
29314
- const widgets = this.asWidgetDefinitions(panel['widgets']);
29315
- if (!widgets.some((widget) => this.isWidgetDefinition(widget))) {
29316
- continue;
29317
- }
29318
- locations.push({
29319
- path: [{
29320
- kind: 'panel',
29321
- ...this.segmentIdentity(panel, index),
29322
- }],
29323
- widgets,
29324
- });
29325
- }
29326
- return locations;
29327
- }
29328
- resolveWidgetArrayLocation(definition, nestedPath, offset) {
29329
- if (definition.id === 'praxis-tabs') {
29330
- return this.resolveTabsWidgetArray(definition, nestedPath, offset);
29426
+ },
29427
+ },
29428
+ safetyNotes: 'Forward these hints to AI authoring calls as runtime retrieval context. Do not convert them into executable rules or persist them as FormConfig unless an approved backend materialization flow says so.',
29429
+ },
29430
+ ],
29431
+ fieldResolvers: {
29432
+ resourceKey: [
29433
+ 'Prefer backend-provided resourceKey from surfaces, actions, capabilities or api_metadata.',
29434
+ 'Do not derive resourceKey from resource.path when discovery already returned a stable semantic key.',
29435
+ ],
29436
+ governanceProbe: [
29437
+ 'Use the business field/concept name as query: cpf, salario, status, email, documento.',
29438
+ 'Keep probes narrow; request more context instead of loading every governance item into a prompt.',
29439
+ ],
29440
+ },
29441
+ hints: [
29442
+ 'Domain Catalog is a runtime semantic/governance context source, not a FormConfig rule store.',
29443
+ 'Use DomainCatalogService for read-only lookup of domain vocabulary, relationships and governance items.',
29444
+ 'LLM tasks must treat aiUsage.visibility and complianceTags as prompt/data-access constraints before generating patches.',
29445
+ 'Use contextHints.domainCatalog with schemaVersion praxis.ai.context-hints.domain-catalog/v0.2 to share runtime semantic retrieval intent between frontend and backend.',
29446
+ 'Shared domain rules may be referenced by resourceKey and catalog release, but should not be copied into component JSON unless intentionally materialized by a backend policy.',
29447
+ ],
29448
+ };
29449
+
29450
+ const OWNER_EVENT_SEGMENT_KINDS = new Set(['tabs', 'expansion', 'stepper']);
29451
+ const STRUCTURAL_NESTED_PATH_KINDS = new Set([
29452
+ 'nav',
29453
+ ]);
29454
+ function normalizeWidgetEventPath(event, options = {}) {
29455
+ const normalized = [];
29456
+ const sourceChildWidgetKey = normalizeString(event.sourceChildWidgetKey);
29457
+ const sourceComponentId = normalizeString(event.sourceComponentId);
29458
+ for (const [index, segment] of (event.path || []).entries()) {
29459
+ if (isOwnerEventSegment(segment, index, options)) {
29460
+ continue;
29331
29461
  }
29332
- if (definition.id === 'praxis-expansion') {
29333
- return this.resolveExpansionWidgetArray(definition, nestedPath, offset);
29462
+ const normalizedSegment = normalizeEventSegment(segment);
29463
+ if (normalizedSegment) {
29464
+ normalized.push(normalizedSegment);
29334
29465
  }
29335
- return undefined;
29336
29466
  }
29337
- resolveTabsWidgetArray(definition, nestedPath, offset) {
29338
- const config = definition.inputs?.['config'];
29339
- if (!config || typeof config !== 'object') {
29340
- return undefined;
29341
- }
29342
- const first = nestedPath[offset];
29343
- if (first?.kind === 'tab') {
29344
- const tabs = Array.isArray(config['tabs'])
29345
- ? config['tabs']
29346
- : [];
29347
- const tab = this.findBySegment(tabs, first);
29348
- const widgets = tab && typeof tab === 'object'
29349
- ? this.asWidgetDefinitions(tab['widgets'])
29350
- : [];
29351
- return widgets.length ? { widgets, segmentCount: offset + 1 } : undefined;
29467
+ const terminal = normalized[normalized.length - 1];
29468
+ if (terminal?.kind === 'widget') {
29469
+ if (!terminal.key && sourceChildWidgetKey) {
29470
+ terminal.key = sourceChildWidgetKey;
29352
29471
  }
29353
- const linkSegment = first?.kind === 'nav'
29354
- ? nestedPath[offset + 1]
29355
- : first;
29356
- if (linkSegment?.kind !== 'link') {
29357
- return undefined;
29472
+ if (!terminal.componentType && sourceComponentId) {
29473
+ terminal.componentType = sourceComponentId;
29358
29474
  }
29359
- const nav = config['nav'];
29360
- const links = nav && typeof nav === 'object' && Array.isArray(nav['links'])
29361
- ? nav['links']
29362
- : [];
29363
- const link = this.findBySegment(links, linkSegment);
29364
- const widgets = link && typeof link === 'object'
29365
- ? this.asWidgetDefinitions(link['widgets'])
29366
- : [];
29367
- return widgets.length
29368
- ? { widgets, segmentCount: first?.kind === 'nav' ? offset + 2 : offset + 1 }
29369
- : undefined;
29475
+ return stripEmptySegments(normalized);
29370
29476
  }
29371
- resolveExpansionWidgetArray(definition, nestedPath, offset) {
29372
- const config = definition.inputs?.['config'];
29373
- const panelSegment = nestedPath[offset];
29374
- if (!config || typeof config !== 'object' || panelSegment?.kind !== 'panel') {
29375
- return undefined;
29376
- }
29377
- const panels = Array.isArray(config['panels'])
29378
- ? config['panels']
29379
- : [];
29380
- const panel = this.findBySegment(panels, panelSegment);
29381
- const widgets = panel && typeof panel === 'object'
29382
- ? this.asWidgetDefinitions(panel['widgets'])
29383
- : [];
29384
- return widgets.length ? { widgets, segmentCount: offset + 1 } : undefined;
29477
+ if (sourceChildWidgetKey) {
29478
+ normalized.push({
29479
+ kind: 'widget',
29480
+ key: sourceChildWidgetKey,
29481
+ ...(sourceComponentId ? { componentType: sourceComponentId } : {}),
29482
+ });
29385
29483
  }
29386
- findBySegment(items, segment) {
29387
- if (segment.id) {
29388
- const byId = items.find((item) => !!item
29389
- && typeof item === 'object'
29390
- && item['id'] === segment.id);
29391
- if (byId) {
29392
- return byId;
29393
- }
29394
- }
29395
- if (typeof segment.index === 'number') {
29396
- return items[segment.index];
29397
- }
29398
- return undefined;
29484
+ return stripEmptySegments(normalized);
29485
+ }
29486
+ function normalizeEventSegment(segment) {
29487
+ if (segment.kind === 'tabs') {
29488
+ return null;
29399
29489
  }
29400
- segmentIdentity(source, index) {
29401
- const id = typeof source['id'] === 'string' ? source['id'].trim() : '';
29402
- return {
29403
- ...(id ? { id } : {}),
29404
- index,
29405
- };
29490
+ const kind = segment.kind === 'widget'
29491
+ ? 'widget'
29492
+ : segment.kind;
29493
+ const key = normalizeString(segment.key);
29494
+ const id = normalizeString(segment.id);
29495
+ const componentType = normalizeString(segment.componentType);
29496
+ const normalized = { kind };
29497
+ if (id) {
29498
+ normalized.id = id;
29406
29499
  }
29407
- asWidgetDefinitions(value) {
29408
- return Array.isArray(value) ? value : [];
29500
+ if (key) {
29501
+ normalized.key = key;
29409
29502
  }
29410
- isWidgetDefinition(value) {
29411
- return !!value
29412
- && typeof value === 'object'
29413
- && typeof value.id === 'string';
29503
+ if (typeof segment.index === 'number' && Number.isFinite(segment.index)) {
29504
+ normalized.index = Math.trunc(segment.index);
29414
29505
  }
29415
- resolveChildWidgetKey(widget) {
29416
- return widget.childWidgetKey || '';
29506
+ if (componentType) {
29507
+ normalized.componentType = componentType;
29417
29508
  }
29418
- clone(value) {
29419
- if (value == null || typeof value !== 'object') {
29420
- return value;
29421
- }
29422
- return JSON.parse(JSON.stringify(value));
29509
+ return normalized;
29510
+ }
29511
+ function isOwnerEventSegment(segment, index, options) {
29512
+ if (index !== 0 || !OWNER_EVENT_SEGMENT_KINDS.has(segment.kind)) {
29513
+ return false;
29423
29514
  }
29424
- isEqual(left, right) {
29425
- return JSON.stringify(left) === JSON.stringify(right);
29515
+ const ownerComponentId = normalizeString(options.ownerComponentId);
29516
+ if (!ownerComponentId) {
29517
+ return true;
29426
29518
  }
29519
+ return segment.kind === ownerComponentId
29520
+ || segment.id === ownerComponentId;
29521
+ }
29522
+ function stripEmptySegments(segments) {
29523
+ return segments.filter((segment) => !!segment.kind
29524
+ && (segment.kind === 'widget'
29525
+ || STRUCTURAL_NESTED_PATH_KINDS.has(segment.kind)
29526
+ || !!segment.id
29527
+ || !!segment.key
29528
+ || typeof segment.index === 'number'
29529
+ || !!segment.componentType));
29530
+ }
29531
+ function normalizeString(value) {
29532
+ return typeof value === 'string' ? value.trim() : '';
29427
29533
  }
29428
29534
 
29429
29535
  const EDITORIAL_ALLOWED_CONTENT_FORMATS = ['plain', 'markdown'];
@@ -38505,10 +38611,11 @@ class DynamicWidgetPageComponent {
38505
38611
  this.pageState = runtime.state;
38506
38612
  this.pageRuntime = runtime;
38507
38613
  this.applyResponsivePresentation(hydrated, stateBoundWidgets, runtime);
38508
- this.pageChange.emit(hydrated);
38614
+ const canonicalPage = this.toPersistedCanonicalPage(hydrated);
38615
+ this.pageChange.emit(canonicalPage);
38509
38616
  this.reportStateDiagnostics(runtime.diagnostics);
38510
38617
  if (persist && this.autoPersist && !this.isHydrating) {
38511
- this.savePage(hydrated);
38618
+ this.savePage(canonicalPage);
38512
38619
  }
38513
38620
  }
38514
38621
  applyLayout(layout, grouping) {