@fayz-ai/core 0.8.1 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access/denial.d.ts +21 -0
- package/dist/access/denial.d.ts.map +1 -0
- package/dist/access/engine.d.ts +63 -0
- package/dist/access/engine.d.ts.map +1 -0
- package/dist/access/index.cjs +60 -0
- package/dist/access/index.cjs.map +1 -0
- package/dist/access/index.d.ts +6 -0
- package/dist/access/index.d.ts.map +1 -0
- package/dist/access/index.js +51 -0
- package/dist/access/index.js.map +1 -0
- package/dist/access/limits.d.ts +10 -0
- package/dist/access/limits.d.ts.map +1 -0
- package/dist/address/index.d.ts +52 -0
- package/dist/address/index.d.ts.map +1 -0
- package/dist/{chunk-QR7U2URY.cjs → chunk-CATASHPK.cjs} +345 -4
- package/dist/chunk-CATASHPK.cjs.map +1 -0
- package/dist/{chunk-WKZVSKEU.js → chunk-CU64QI4A.js} +2 -2
- package/dist/chunk-CU64QI4A.js.map +1 -0
- package/dist/{chunk-KNIVT6XZ.js → chunk-DZALZ6Q6.js} +345 -4
- package/dist/chunk-DZALZ6Q6.js.map +1 -0
- package/dist/{chunk-54SRL7AJ.cjs → chunk-KSI2L3E4.cjs} +145 -86
- package/dist/chunk-KSI2L3E4.cjs.map +1 -0
- package/dist/{chunk-ZBIWYDAR.js → chunk-TYPVSWLG.js} +144 -87
- package/dist/chunk-TYPVSWLG.js.map +1 -0
- package/dist/{chunk-2NTHNWEH.cjs → chunk-UKU4RIWM.cjs} +2 -2
- package/dist/chunk-UKU4RIWM.cjs.map +1 -0
- package/dist/{chunk-CHGX6UYR.js → chunk-YH25Y4FW.js} +6 -3
- package/dist/chunk-YH25Y4FW.js.map +1 -0
- package/dist/{chunk-5X2VX3RQ.cjs → chunk-ZOPJB5FN.cjs} +6 -3
- package/dist/chunk-ZOPJB5FN.cjs.map +1 -0
- package/dist/data/archetype.d.ts.map +1 -1
- package/dist/data/count.d.ts +34 -0
- package/dist/data/count.d.ts.map +1 -0
- package/dist/data/index.cjs +18 -10
- package/dist/data/index.d.ts +2 -0
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/index.js +1 -1
- package/dist/data/refresh.d.ts +36 -0
- package/dist/data/refresh.d.ts.map +1 -0
- package/dist/data/resolve.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/types.d.ts +6 -0
- package/dist/data/types.d.ts.map +1 -1
- package/dist/entity/index.cjs +6 -6
- package/dist/entity/index.js +1 -1
- package/dist/entity/registry.d.ts +9 -0
- package/dist/entity/registry.d.ts.map +1 -1
- package/dist/i18n/index.cjs +10 -10
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/shell-translations.d.ts.map +1 -1
- package/dist/index.cjs +1517 -180
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +15 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1412 -110
- package/dist/index.js.map +1 -1
- package/dist/manifest/index.d.ts +109 -3
- package/dist/manifest/index.d.ts.map +1 -1
- package/dist/manifest/serialize.d.ts +14 -0
- package/dist/manifest/serialize.d.ts.map +1 -0
- package/dist/plugin/index.cjs +7 -7
- package/dist/plugin/index.js +1 -1
- package/dist/plugin/runtime.d.ts.map +1 -1
- package/dist/search/engine.d.ts +12 -0
- package/dist/search/engine.d.ts.map +1 -0
- package/dist/search/index.d.ts +5 -0
- package/dist/search/index.d.ts.map +1 -0
- package/dist/search/text.d.ts +37 -0
- package/dist/search/text.d.ts.map +1 -0
- package/dist/search/types.d.ts +72 -0
- package/dist/search/types.d.ts.map +1 -0
- package/dist/types/billing.d.ts +20 -0
- package/dist/types/billing.d.ts.map +1 -1
- package/dist/types/crud.d.ts +47 -1
- package/dist/types/crud.d.ts.map +1 -1
- package/dist/types/entitlements.d.ts +45 -0
- package/dist/types/entitlements.d.ts.map +1 -0
- package/dist/types/index.d.ts +4 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/org.d.ts +28 -0
- package/dist/types/org.d.ts.map +1 -1
- package/dist/types/plugins.d.ts +88 -0
- package/dist/types/plugins.d.ts.map +1 -1
- package/package.json +14 -4
- package/src/access/denial.ts +33 -0
- package/src/access/engine.test.ts +48 -0
- package/src/access/engine.ts +121 -0
- package/src/access/index.ts +13 -0
- package/src/access/limits.ts +18 -0
- package/src/address/index.ts +188 -0
- package/src/data/archetype.ts +4 -2
- package/src/data/count.test.ts +152 -0
- package/src/data/count.ts +131 -0
- package/src/data/index.ts +2 -0
- package/src/data/refresh.ts +105 -0
- package/src/data/resolve.ts +7 -1
- package/src/data/supabase.ts +3 -1
- package/src/data/types.ts +6 -0
- package/src/entity/registry.ts +7 -0
- package/src/i18n/shell-translations.ts +345 -2
- package/src/index.ts +37 -4
- package/src/manifest/app-manifest.schema.json +618 -45
- package/src/manifest/index.ts +127 -3
- package/src/manifest/serialize.ts +52 -0
- package/src/plugin/runtime.ts +5 -1
- package/src/search/engine.ts +551 -0
- package/src/search/index.ts +25 -0
- package/src/search/text.ts +206 -0
- package/src/search/types.ts +75 -0
- package/src/types/billing.ts +22 -0
- package/src/types/crud.ts +48 -1
- package/src/types/entitlements.ts +45 -0
- package/src/types/index.ts +4 -3
- package/src/types/org.ts +29 -0
- package/src/types/plugins.ts +83 -0
- package/dist/chunk-2NTHNWEH.cjs.map +0 -1
- package/dist/chunk-54SRL7AJ.cjs.map +0 -1
- package/dist/chunk-5X2VX3RQ.cjs.map +0 -1
- package/dist/chunk-CHGX6UYR.js.map +0 -1
- package/dist/chunk-KNIVT6XZ.js.map +0 -1
- package/dist/chunk-QR7U2URY.cjs.map +0 -1
- package/dist/chunk-WKZVSKEU.js.map +0 -1
- package/dist/chunk-ZBIWYDAR.js.map +0 -1
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// Text folding + ranking for global search. The same folding must exist in SQL
|
|
2
|
+
// as public.fayz_norm() (@fayz-ai/db 018_global_search.sql), or server and
|
|
3
|
+
// client disagree on what matched.
|
|
4
|
+
|
|
5
|
+
const COMBINING_MARKS = /[\u0300-\u036f]/g
|
|
6
|
+
|
|
7
|
+
/** Strip diacritics, lowercase, collapse non-alphanumerics to single spaces. */
|
|
8
|
+
export function foldText(value: unknown): string {
|
|
9
|
+
if (typeof value !== 'string' || value === '') return ''
|
|
10
|
+
return value
|
|
11
|
+
.normalize('NFD')
|
|
12
|
+
.replace(COMBINING_MARKS, '')
|
|
13
|
+
.toLowerCase()
|
|
14
|
+
.replace(/[^a-z0-9]+/g, ' ')
|
|
15
|
+
.trim()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Fold, keeping each folded char's index in the source — used for highlighting. */
|
|
19
|
+
export function foldWithMap(value: string): { folded: string; map: number[] } {
|
|
20
|
+
const out: string[] = []
|
|
21
|
+
const map: number[] = []
|
|
22
|
+
let pendingSpace = false
|
|
23
|
+
for (let i = 0; i < value.length; i++) {
|
|
24
|
+
const ch = value[i]!
|
|
25
|
+
const folded = ch.normalize('NFD').replace(COMBINING_MARKS, '').toLowerCase()
|
|
26
|
+
for (const f of folded) {
|
|
27
|
+
const alnum = (f >= 'a' && f <= 'z') || (f >= '0' && f <= '9')
|
|
28
|
+
if (!alnum) { pendingSpace = out.length > 0; continue }
|
|
29
|
+
if (pendingSpace) { out.push(' '); map.push(i); pendingSpace = false }
|
|
30
|
+
out.push(f)
|
|
31
|
+
map.push(i)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { folded: out.join(''), map }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Digits only — phone numbers, CPF/CNPJ, SKUs typed with separators. */
|
|
38
|
+
export function digitsOf(value: unknown): string {
|
|
39
|
+
return typeof value === 'string' ? value.replace(/\D+/g, '') : ''
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface NormalizedQuery {
|
|
43
|
+
/** What the user typed, untouched. */
|
|
44
|
+
raw: string
|
|
45
|
+
/** Folded form of the whole query — the "phrase". */
|
|
46
|
+
folded: string
|
|
47
|
+
/** Folded query split on whitespace. */
|
|
48
|
+
tokens: string[]
|
|
49
|
+
/** Digits the user typed, when they typed enough of them to mean a number. */
|
|
50
|
+
digits: string
|
|
51
|
+
/** The longest token — the most selective thing to send to a `%LIKE%` backend. */
|
|
52
|
+
anchor: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Below this a phone/document lookup is indistinguishable from noise. */
|
|
56
|
+
const MIN_DIGIT_QUERY = 4
|
|
57
|
+
|
|
58
|
+
export function normalizeQuery(raw: string): NormalizedQuery {
|
|
59
|
+
const folded = foldText(raw)
|
|
60
|
+
const tokens = folded ? folded.split(' ').filter(Boolean) : []
|
|
61
|
+
const digits = raw.replace(/\D+/g, '')
|
|
62
|
+
let anchor = ''
|
|
63
|
+
for (const token of tokens) if (token.length > anchor.length) anchor = token
|
|
64
|
+
return {
|
|
65
|
+
raw,
|
|
66
|
+
folded,
|
|
67
|
+
tokens,
|
|
68
|
+
digits: digits.length >= MIN_DIGIT_QUERY ? digits : '',
|
|
69
|
+
anchor,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Fuzzy similarity — Sørensen–Dice over bigrams. Same family as pg_trgm's
|
|
74
|
+
// similarity(), so both sides rank near-misses the same way.
|
|
75
|
+
|
|
76
|
+
function bigrams(value: string): string[] {
|
|
77
|
+
if (value.length < 2) return value ? [value] : []
|
|
78
|
+
const out: string[] = []
|
|
79
|
+
for (let i = 0; i < value.length - 1; i++) out.push(value.slice(i, i + 2))
|
|
80
|
+
return out
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 0 (nothing in common) … 1 (identical). Both inputs must already be folded. */
|
|
84
|
+
export function similarity(a: string, b: string): number {
|
|
85
|
+
if (!a || !b) return 0
|
|
86
|
+
if (a === b) return 1
|
|
87
|
+
const left = bigrams(a)
|
|
88
|
+
const right = bigrams(b)
|
|
89
|
+
if (left.length === 0 || right.length === 0) return 0
|
|
90
|
+
const pool = new Map<string, number>()
|
|
91
|
+
for (const g of left) pool.set(g, (pool.get(g) ?? 0) + 1)
|
|
92
|
+
let shared = 0
|
|
93
|
+
for (const g of right) {
|
|
94
|
+
const count = pool.get(g)
|
|
95
|
+
if (count) { shared++; pool.set(g, count - 1) }
|
|
96
|
+
}
|
|
97
|
+
return (2 * shared) / (left.length + right.length)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Best similarity between the query token and any single word of the text. */
|
|
101
|
+
function wordSimilarity(text: string, token: string): number {
|
|
102
|
+
let best = similarity(text, token)
|
|
103
|
+
for (const word of text.split(' ')) {
|
|
104
|
+
if (!word) continue
|
|
105
|
+
const s = similarity(word, token)
|
|
106
|
+
if (s > best) best = s
|
|
107
|
+
}
|
|
108
|
+
return best
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Ranking
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
export interface RankCandidate {
|
|
116
|
+
/** Folded display name — what the user is most likely aiming at. */
|
|
117
|
+
title: string
|
|
118
|
+
/** Folded concatenation of every searchable value on the record. */
|
|
119
|
+
haystack: string
|
|
120
|
+
/** Digits of every phone/document-ish value on the record. */
|
|
121
|
+
digits?: string
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** A fuzzy word has to be at least this close before it counts as a match. */
|
|
125
|
+
const FUZZY_FLOOR = 0.62
|
|
126
|
+
|
|
127
|
+
/** Score a candidate 0…1. Zero means "not a match" — the score is also the filter. */
|
|
128
|
+
export function scoreCandidate(query: NormalizedQuery, candidate: RankCandidate): number {
|
|
129
|
+
const { folded, tokens } = query
|
|
130
|
+
if (!folded || tokens.length === 0) return 0
|
|
131
|
+
const title = candidate.title
|
|
132
|
+
const haystack = candidate.haystack || title
|
|
133
|
+
|
|
134
|
+
// Phrase: the query as one string.
|
|
135
|
+
let phrase = 0
|
|
136
|
+
if (title === folded) phrase = 1
|
|
137
|
+
else if (title.startsWith(folded)) phrase = 0.94
|
|
138
|
+
else if (title.includes(' ' + folded)) phrase = 0.88
|
|
139
|
+
else if (title.includes(folded)) phrase = 0.76
|
|
140
|
+
else if (haystack.startsWith(folded) || haystack.includes(' ' + folded)) phrase = 0.7
|
|
141
|
+
else if (haystack.includes(folded)) phrase = 0.62
|
|
142
|
+
|
|
143
|
+
// Tokens: every word must land somewhere ("maria silva" → "Maria da Silva").
|
|
144
|
+
let tokenTotal = 0
|
|
145
|
+
let fuzzyUsed = false
|
|
146
|
+
for (const token of tokens) {
|
|
147
|
+
let best = 0
|
|
148
|
+
if (title.startsWith(token)) best = 0.95
|
|
149
|
+
else if (title.includes(' ' + token)) best = 0.85
|
|
150
|
+
else if (title.includes(token)) best = 0.7
|
|
151
|
+
else if (haystack.startsWith(token) || haystack.includes(' ' + token)) best = 0.6
|
|
152
|
+
else if (haystack.includes(token)) best = 0.5
|
|
153
|
+
else {
|
|
154
|
+
const sim = wordSimilarity(title, token)
|
|
155
|
+
if (sim >= FUZZY_FLOOR) { best = 0.45 * sim; fuzzyUsed = true }
|
|
156
|
+
}
|
|
157
|
+
if (best === 0) return phrase > 0 ? phrase : 0
|
|
158
|
+
tokenTotal += best
|
|
159
|
+
}
|
|
160
|
+
let tokenScore = tokenTotal / tokens.length
|
|
161
|
+
// A typo-only match must never outrank a literal one.
|
|
162
|
+
if (fuzzyUsed) tokenScore *= 0.8
|
|
163
|
+
|
|
164
|
+
let score = Math.max(phrase, tokenScore)
|
|
165
|
+
|
|
166
|
+
// Digits: a phone number has no words to match.
|
|
167
|
+
if (query.digits && candidate.digits && candidate.digits.includes(query.digits)) {
|
|
168
|
+
score = Math.max(score, 0.72 + Math.min(0.2, query.digits.length / 50))
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (score === 0) return 0
|
|
172
|
+
|
|
173
|
+
// Tie-break: shorter title wins ("Ana" over "Ana Beatriz" for "ana").
|
|
174
|
+
const brevity = 1 / (1 + Math.max(0, title.length - folded.length) / 40)
|
|
175
|
+
return score * 0.97 + brevity * 0.03
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Ranges in the ORIGINAL text covered by the query, merged and sorted. */
|
|
179
|
+
export function highlightRanges(text: string, query: NormalizedQuery): Array<[number, number]> {
|
|
180
|
+
if (!text || query.tokens.length === 0) return []
|
|
181
|
+
const { folded, map } = foldWithMap(text)
|
|
182
|
+
if (!folded) return []
|
|
183
|
+
const spans: Array<[number, number]> = []
|
|
184
|
+
const needles = query.tokens.length > 1 ? [query.folded, ...query.tokens] : [query.folded]
|
|
185
|
+
for (const needle of needles) {
|
|
186
|
+
if (!needle) continue
|
|
187
|
+
let from = 0
|
|
188
|
+
for (;;) {
|
|
189
|
+
const at = folded.indexOf(needle, from)
|
|
190
|
+
if (at === -1) break
|
|
191
|
+
const start = map[at]
|
|
192
|
+
const endChar = map[at + needle.length - 1]
|
|
193
|
+
if (start !== undefined && endChar !== undefined) spans.push([start, endChar + 1])
|
|
194
|
+
from = at + needle.length
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (spans.length === 0) return []
|
|
198
|
+
spans.sort((a, b) => a[0] - b[0])
|
|
199
|
+
const merged: Array<[number, number]> = [spans[0]!]
|
|
200
|
+
for (const span of spans.slice(1)) {
|
|
201
|
+
const last = merged[merged.length - 1]!
|
|
202
|
+
if (span[0] <= last[1]) last[1] = Math.max(last[1], span[1])
|
|
203
|
+
else merged.push(span)
|
|
204
|
+
}
|
|
205
|
+
return merged
|
|
206
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { EntityDef } from '../types/crud'
|
|
2
|
+
|
|
3
|
+
/** One searchable thing — declaring an entity is all it takes to be findable. */
|
|
4
|
+
export interface SearchTarget {
|
|
5
|
+
/** Stable entity key (`deriveEntityKey`): 'person:client', 'product', … */
|
|
6
|
+
key: string
|
|
7
|
+
/** Plural human label; becomes the result group heading. */
|
|
8
|
+
label: string
|
|
9
|
+
icon?: string
|
|
10
|
+
entity: EntityDef
|
|
11
|
+
mockData?: Array<{ id: string }>
|
|
12
|
+
/** Multiplies the final score. Default 1. */
|
|
13
|
+
boost?: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SearchHit {
|
|
17
|
+
/** `${target.key}:${record id}` — stable across both search paths. */
|
|
18
|
+
uid: string
|
|
19
|
+
id: string
|
|
20
|
+
/** Target key this record came from. */
|
|
21
|
+
key: string
|
|
22
|
+
/** Group heading (the target's plural label). */
|
|
23
|
+
group: string
|
|
24
|
+
icon?: string
|
|
25
|
+
title: string
|
|
26
|
+
subtitle?: string
|
|
27
|
+
/** 0 … 1 (times the target boost). Higher is better. */
|
|
28
|
+
score: number
|
|
29
|
+
archetype?: string
|
|
30
|
+
archetypeKind?: string
|
|
31
|
+
table?: string
|
|
32
|
+
/** The row, when the path that found it had one. The index path returns
|
|
33
|
+
* titles only. */
|
|
34
|
+
record?: Record<string, unknown>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SearchGroup {
|
|
38
|
+
key: string
|
|
39
|
+
label: string
|
|
40
|
+
icon?: string
|
|
41
|
+
hits: SearchHit[]
|
|
42
|
+
/** True when this group hit its per-target cap — more matches exist. */
|
|
43
|
+
hasMore: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type SearchPath = 'index' | 'scan' | 'cache'
|
|
47
|
+
|
|
48
|
+
export interface GlobalSearchResult {
|
|
49
|
+
query: string
|
|
50
|
+
hits: SearchHit[]
|
|
51
|
+
groups: SearchGroup[]
|
|
52
|
+
/** Which path produced this result — surfaced for diagnostics, not for UI. */
|
|
53
|
+
via: SearchPath
|
|
54
|
+
/** Targets whose query failed. Never fatal: a broken table cannot blind the box. */
|
|
55
|
+
failed: string[]
|
|
56
|
+
/** Wall-clock milliseconds. */
|
|
57
|
+
elapsedMs: number
|
|
58
|
+
/** True while a better (network) answer is still on its way. */
|
|
59
|
+
partial: boolean
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface SearchOptions {
|
|
63
|
+
/** Permission filtering is the caller's job — the engine searches what it's handed. */
|
|
64
|
+
targets: SearchTarget[]
|
|
65
|
+
/** Max hits returned overall. Default 30. */
|
|
66
|
+
limit?: number
|
|
67
|
+
/** Max hits per target. Default 5. */
|
|
68
|
+
perTarget?: number
|
|
69
|
+
/** Fired with early/partial answers before the authoritative one resolves. */
|
|
70
|
+
onPartial?: (result: GlobalSearchResult) => void
|
|
71
|
+
/** Discard the result if this aborts. */
|
|
72
|
+
signal?: AbortSignal
|
|
73
|
+
/** Skip the server index and force the per-entity scan (tests, diagnostics). */
|
|
74
|
+
forceScan?: boolean
|
|
75
|
+
}
|
package/src/types/billing.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { PlanEntitlements } from './entitlements'
|
|
2
|
+
|
|
1
3
|
export interface Plan {
|
|
2
4
|
id: string
|
|
3
5
|
name: string
|
|
@@ -5,13 +7,33 @@ export interface Plan {
|
|
|
5
7
|
price: number
|
|
6
8
|
currency: string
|
|
7
9
|
interval: 'month' | 'year'
|
|
10
|
+
/** Marketing bullets shown on the pricing page (display only). */
|
|
8
11
|
features: string[]
|
|
9
12
|
highlighted?: boolean
|
|
10
13
|
stripePriceId?: string
|
|
14
|
+
/**
|
|
15
|
+
* Structured access grants for this plan (feature gates + quantity caps). This
|
|
16
|
+
* is the enforcement source of truth — distinct from `features` (display
|
|
17
|
+
* bullets). See {@link PlanEntitlements}.
|
|
18
|
+
*/
|
|
19
|
+
entitlements?: PlanEntitlements
|
|
11
20
|
}
|
|
12
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Signature of the checkout seam. When `BillingConfig.onCheckout` is defined the
|
|
24
|
+
* shell's Subscription page calls it (passing the target plan id) INSTEAD of
|
|
25
|
+
* writing `plan` straight onto the org via `adapter.updateOrg`. This is where a
|
|
26
|
+
* real payment gateway (Stripe Checkout, Pix, Mercado Pago, …) plugs in: return
|
|
27
|
+
* once the user is redirected / the intent is created, and let the webhook be the
|
|
28
|
+
* source of truth for the persisted plan. Left undefined, the shell mutates the
|
|
29
|
+
* plan optimistically (dev / self-serve / free-tier flows).
|
|
30
|
+
*/
|
|
31
|
+
export type BillingCheckoutFn = (planId: string) => Promise<void> | void
|
|
32
|
+
|
|
13
33
|
export interface BillingConfig {
|
|
14
34
|
plans: Plan[]
|
|
15
35
|
stripePublishableKey?: string
|
|
16
36
|
portalUrl?: string
|
|
37
|
+
/** Payment-gateway seam — see {@link BillingCheckoutFn}. */
|
|
38
|
+
onCheckout?: BillingCheckoutFn
|
|
17
39
|
}
|
package/src/types/crud.ts
CHANGED
|
@@ -79,7 +79,8 @@ export interface FieldGroup {
|
|
|
79
79
|
label: string
|
|
80
80
|
description?: string
|
|
81
81
|
columns?: 1 | 2 | 3
|
|
82
|
-
/** Renders
|
|
82
|
+
/** Renders an image slot to the left of the group's fields. Functional when
|
|
83
|
+
* the EntityDef supplies `image`; a plain placeholder otherwise. */
|
|
83
84
|
imageSlot?: boolean
|
|
84
85
|
}
|
|
85
86
|
|
|
@@ -104,6 +105,28 @@ export interface DetailTab {
|
|
|
104
105
|
|
|
105
106
|
export type FormLayout = 'person' | 'product' | 'service' | 'location' | 'order' | 'subject' | 'generic'
|
|
106
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Makes a field group's `imageSlot` functional. Without this the slot renders as
|
|
110
|
+
* a decorative placeholder — it always did, which is why product forms showed an
|
|
111
|
+
* empty thumbnail even for products that have images.
|
|
112
|
+
*
|
|
113
|
+
* Upload needs the record to exist (the file is attached to its id), so the slot
|
|
114
|
+
* stays read-only in create mode and becomes editable after the first save.
|
|
115
|
+
*/
|
|
116
|
+
export interface EntityImageConfig {
|
|
117
|
+
// Rows are loosely typed here on purpose: EntityDef<T> is consumed as
|
|
118
|
+
// EntityDef<Record<string, unknown>> by the generic CRUD machinery, and a
|
|
119
|
+
// T-parameterised callback makes the whole EntityDef invariant in T.
|
|
120
|
+
/** Current image URL for this record, if any. */
|
|
121
|
+
get: (row: Record<string, any>) => string | undefined
|
|
122
|
+
/** Persist a new image and return its URL. Omit to keep the slot read-only. */
|
|
123
|
+
upload?: (file: File, row: Record<string, any>) => Promise<string>
|
|
124
|
+
/** Remove the current image. Omit to hide the remove affordance. */
|
|
125
|
+
remove?: (row: Record<string, any>) => Promise<void>
|
|
126
|
+
/** Accepted mime types. Defaults to image/*. */
|
|
127
|
+
accept?: string
|
|
128
|
+
}
|
|
129
|
+
|
|
107
130
|
export interface EntityDef<T = Record<string, unknown>> {
|
|
108
131
|
name: string
|
|
109
132
|
namePlural?: string
|
|
@@ -111,6 +134,8 @@ export interface EntityDef<T = Record<string, unknown>> {
|
|
|
111
134
|
layout?: FormLayout
|
|
112
135
|
fields: FieldDef[]
|
|
113
136
|
fieldGroups?: FieldGroup[]
|
|
137
|
+
/** Wiring for the `imageSlot` of whichever field group declares one. */
|
|
138
|
+
image?: EntityImageConfig
|
|
114
139
|
detailTabs?: DetailTab[]
|
|
115
140
|
data?: {
|
|
116
141
|
table: string
|
|
@@ -134,4 +159,26 @@ export interface EntityDef<T = Record<string, unknown>> {
|
|
|
134
159
|
/** Faceted filters shown as pills below the list search box. Each references a
|
|
135
160
|
* field whose `options` supply the pills; an "All" pill clears the filter. */
|
|
136
161
|
facets?: { field: string; allLabel?: string }[]
|
|
162
|
+
/**
|
|
163
|
+
* Plan quantity-limit key gating creation of this entity (matches a
|
|
164
|
+
* `PlanEntitlements.limits` key + a `LimitDeclaration`). When set, the CRUD
|
|
165
|
+
* engine gates the "+ New" button (LimitGate), guards the create-form submit
|
|
166
|
+
* and the CSV import (useLimitGuard) against the plan cap, and invalidates the
|
|
167
|
+
* live count after a successful create. Absent ⇒ no plan gating (RBAC only).
|
|
168
|
+
*/
|
|
169
|
+
limitKey?: string
|
|
170
|
+
/**
|
|
171
|
+
* Permission required to READ this entity through the agent's generic data
|
|
172
|
+
* primitives (searchRecords/queryData) — checked per TARGET at execution
|
|
173
|
+
* time, so one generic tool still honors per-user access. Absent ⇒ readable
|
|
174
|
+
* by any signed-in member (same as the CRUD list today).
|
|
175
|
+
*/
|
|
176
|
+
permission?: { feature: string; action: string }
|
|
177
|
+
/**
|
|
178
|
+
* Exclude this entity from the agent's data primitives. For entities whose
|
|
179
|
+
* table is an extension/join shape the generic provider can't read usefully
|
|
180
|
+
* (e.g. staff_members without a name column) — expose a queryEntities
|
|
181
|
+
* read-model instead and hide the raw one here.
|
|
182
|
+
*/
|
|
183
|
+
agentHidden?: boolean
|
|
137
184
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entitlements — the plan side of the access decision (the role side lives in
|
|
3
|
+
* permissions.ts). A plan grants FEATURES (gate a page/module) and caps LIMITS
|
|
4
|
+
* (quantity ceilings). Feature ids are the SAME id space as RBAC/nav
|
|
5
|
+
* (`permission.feature`), so a single id threads role → nav → plan.
|
|
6
|
+
*/
|
|
7
|
+
export interface PlanEntitlements {
|
|
8
|
+
/**
|
|
9
|
+
* feature id (same id as RBAC/nav) → whether the plan grants it. Absent = the
|
|
10
|
+
* plan does not gate the feature (allowed). Only an explicit `false` denies —
|
|
11
|
+
* this keeps plans additive: features unknown to a plan stay open.
|
|
12
|
+
*/
|
|
13
|
+
features?: Record<string, boolean>
|
|
14
|
+
/**
|
|
15
|
+
* limitKey → cap. `-1` = unlimited. Keys are app/plugin-defined (e.g.
|
|
16
|
+
* `clients`, `users`, `locations`, `bookings_month`). A key absent here means
|
|
17
|
+
* "no cap on this plan" (unlimited). See {@link LimitDeclaration} for how a
|
|
18
|
+
* key is bound to a countable table.
|
|
19
|
+
*/
|
|
20
|
+
limits?: Record<string, number>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Declares HOW a limit key is counted. Contributed by plugins via
|
|
25
|
+
* `PluginManifest.declaredLimits` (aggregated by the plugin runtime) and/or
|
|
26
|
+
* overridden per-app via `billing.limitDeclarations`. The access engine reads a
|
|
27
|
+
* declaration to resolve a `limitKey` → a live `count(*)` against `table` for
|
|
28
|
+
* the active tenant, with no per-entity code.
|
|
29
|
+
*/
|
|
30
|
+
export interface LimitDeclaration {
|
|
31
|
+
/** Limit key — matches a key in {@link PlanEntitlements.limits}. */
|
|
32
|
+
key: string
|
|
33
|
+
/** Human label for paywalls / usage banners (e.g. "Patients"). */
|
|
34
|
+
label: string
|
|
35
|
+
/** Table the count runs against (tenant-scoped via `tenant_id`). */
|
|
36
|
+
table: string
|
|
37
|
+
/** Optional `kind` column filter for tables that hold several entity kinds. */
|
|
38
|
+
kindFilter?: string
|
|
39
|
+
/**
|
|
40
|
+
* Counting window. `'month'` counts rows created since the start of the
|
|
41
|
+
* current month (recurring quotas like bookings/month); `'total'` (default)
|
|
42
|
+
* counts every row (stock quantities like clients/users).
|
|
43
|
+
*/
|
|
44
|
+
period?: 'month' | 'total'
|
|
45
|
+
}
|
package/src/types/index.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// Re-export all core types
|
|
2
2
|
export type { EntityArchetype, BaseEntity, PersonEntity, ProductEntity, ServiceEntity, OrderEntity, TransactionEntity, LocationEntity, ScheduleEntity, CategoryEntity } from './entities'
|
|
3
3
|
export type { EntityDef, FieldDef, FieldType, FieldRelation, FieldGroup, DetailTab, FormLayout, ComputedFieldValue } from './crud'
|
|
4
|
-
export type { PluginManifest, PluginScope, PluginStatus, ResolvedPluginManifest, PluginRuntime, PluginRuntimeContext, PluginWidgetZone, PluginWidgetDefinition, PluginNavigationEntry, PluginSettingsTab, PluginRouteDefinition, PluginAITool, PluginRegistryDef, PluginMigration, PluginDiagnostic, VerticalId, ScaffoldType } from './plugins'
|
|
4
|
+
export type { PluginManifest, PluginScope, PluginStatus, ResolvedPluginManifest, PluginRuntime, PluginRuntimeContext, PluginWidgetZone, PluginWidgetDefinition, PluginNavigationEntry, PluginSettingsTab, PluginRouteDefinition, PluginAITool, AIToolMode, AIToolExecution, AgentRpcDeclaration, AIToolParameterProperty, AIToolParameters, AIToolSuggestion, PluginRegistryDef, PluginMigration, PluginDiagnostic, VerticalId, ScaffoldType } from './plugins'
|
|
5
5
|
export type { AuthAdapter, AuthUser, AuthSession } from './auth'
|
|
6
6
|
export type { OrgAdapter, Organization, OrgMember } from './org'
|
|
7
|
-
export type { PermissionsConfig, FeatureDeclaration, PermissionAction } from './permissions'
|
|
8
|
-
export type { Plan, BillingConfig } from './billing'
|
|
7
|
+
export type { PermissionsConfig, FeatureDeclaration, PermissionAction, PermissionProfile, SystemPermission } from './permissions'
|
|
8
|
+
export type { Plan, BillingConfig, BillingCheckoutFn } from './billing'
|
|
9
|
+
export type { PlanEntitlements, LimitDeclaration } from './entitlements'
|
|
9
10
|
export type { ThemeBrand, ThemeRadius, ThemeMode } from './theme'
|
|
10
11
|
export type { LocaleConfig } from './i18n'
|
package/src/types/org.ts
CHANGED
|
@@ -36,6 +36,28 @@ export interface OrgMembership {
|
|
|
36
36
|
profileName: string
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* A team member in the Person-first model: a person (of a configured team kind)
|
|
41
|
+
* with an OPTIONAL access overlay. `membership` is present only when the person
|
|
42
|
+
* has login + RBAC role (a `tenant_members` row linked by `person_id`).
|
|
43
|
+
*/
|
|
44
|
+
export interface TeamPerson {
|
|
45
|
+
personId: string
|
|
46
|
+
name: string
|
|
47
|
+
kind: string
|
|
48
|
+
email?: string
|
|
49
|
+
avatarUrl?: string
|
|
50
|
+
isActive: boolean
|
|
51
|
+
membership?: {
|
|
52
|
+
memberId: string
|
|
53
|
+
userId: string
|
|
54
|
+
/** RBAC role key (tenant_members.role). */
|
|
55
|
+
profileId: string
|
|
56
|
+
profileName?: string
|
|
57
|
+
joinedAt?: string
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
39
61
|
export interface Location {
|
|
40
62
|
id: string
|
|
41
63
|
tenantId: string
|
|
@@ -78,6 +100,13 @@ export interface OrgAdapter {
|
|
|
78
100
|
createOrg(name: string, userId: string, options?: CreateOrgOptions): Promise<Organization>
|
|
79
101
|
updateOrg(orgId: string, data: Partial<Organization>): Promise<Organization>
|
|
80
102
|
listMembers(orgId: string): Promise<OrgMember[]>
|
|
103
|
+
/**
|
|
104
|
+
* Person-first team list: people whose `kind` is in `personKinds`, each with an
|
|
105
|
+
* optional membership overlay (login + role) linked by `tenant_members.person_id`.
|
|
106
|
+
* Optional so adapters can adopt it incrementally; the Team screen falls back to
|
|
107
|
+
* `listMembers` when absent or when no `personKinds` are configured.
|
|
108
|
+
*/
|
|
109
|
+
listTeam?(orgId: string, personKinds: string[]): Promise<TeamPerson[]>
|
|
81
110
|
updateMemberProfile(orgId: string, memberId: string, profileId: string): Promise<void>
|
|
82
111
|
removeMember(orgId: string, memberId: string): Promise<void>
|
|
83
112
|
listProfiles(orgId: string): Promise<PermissionProfile[]>
|
package/src/types/plugins.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type React from 'react'
|
|
2
2
|
import type { EntityDef } from './crud'
|
|
3
3
|
import type { FeatureDeclaration, PermissionAction } from './permissions'
|
|
4
|
+
import type { LimitDeclaration } from './entitlements'
|
|
4
5
|
import type { ConnectorDefinition } from '../integrations'
|
|
5
6
|
|
|
6
7
|
export type VerticalId = 'beauty' | 'food' | 'health' | 'services' | 'retail' | 'education' | (string & {})
|
|
@@ -28,6 +29,14 @@ export interface PluginNavigationEntry {
|
|
|
28
29
|
icon?: string
|
|
29
30
|
badge?: string | number
|
|
30
31
|
permission?: PluginPermissionRequirement
|
|
32
|
+
/**
|
|
33
|
+
* When this entry gains sub-path children, the shell also synthesizes a child
|
|
34
|
+
* pointing back at the parent's own route (so /courses stays reachable under
|
|
35
|
+
* the "Courses" group). Set false when the parent route is a duplicate of its
|
|
36
|
+
* first child — e.g. /shop and /shop/products render the same catalog, and
|
|
37
|
+
* listing both reads as a bug. Defaults to true.
|
|
38
|
+
*/
|
|
39
|
+
indexChild?: boolean
|
|
31
40
|
}
|
|
32
41
|
|
|
33
42
|
export interface PluginSettingsTab {
|
|
@@ -54,6 +63,10 @@ export interface PluginRouteDefinition {
|
|
|
54
63
|
permission?: PluginPermissionRequirement
|
|
55
64
|
/** Render edge-to-edge with no page padding/animation wrapper (chat, kanban, canvas). */
|
|
56
65
|
fullBleed?: boolean
|
|
66
|
+
/** Archetypes this page displays (`archetype` or `archetype:kind`, e.g.
|
|
67
|
+
* `'schedule:appointment'`). A CRUD page claims its own; a hand-built page
|
|
68
|
+
* must declare it or records it owns resolve to no route. */
|
|
69
|
+
entityRoutes?: string[]
|
|
57
70
|
}
|
|
58
71
|
|
|
59
72
|
export type PluginWidgetZone =
|
|
@@ -225,6 +238,27 @@ export interface AIToolSuggestion {
|
|
|
225
238
|
verticalId?: VerticalId
|
|
226
239
|
}
|
|
227
240
|
|
|
241
|
+
export type AIToolExecution =
|
|
242
|
+
| { plane: 'server'; kind: 'rpc'; rpc: string }
|
|
243
|
+
| { plane: 'server'; kind: 'entity_read'; entity: string }
|
|
244
|
+
| { plane: 'client' }
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* A tenant-scoped pool RPC (`agent_<domain>_<verb>`) this plugin ships for the
|
|
248
|
+
* server-plane agent executor. Surfaces in the manifest as
|
|
249
|
+
* `agent.rpcs` (RpcContract) — the broker only ever calls declared functions.
|
|
250
|
+
*/
|
|
251
|
+
export interface AgentRpcDeclaration {
|
|
252
|
+
/** Function name in the pool, e.g. 'agent_agenda_create_appointment'. */
|
|
253
|
+
name: string
|
|
254
|
+
kind: 'read' | 'write'
|
|
255
|
+
description: string
|
|
256
|
+
/** JSON Schema of the p_payload argument. */
|
|
257
|
+
payloadSchema?: AIToolParameters
|
|
258
|
+
/** Whether the function writes audit_logs itself. */
|
|
259
|
+
audits?: boolean
|
|
260
|
+
}
|
|
261
|
+
|
|
228
262
|
export interface PluginAITool {
|
|
229
263
|
id: string
|
|
230
264
|
name: string
|
|
@@ -233,6 +267,31 @@ export interface PluginAITool {
|
|
|
233
267
|
mode: AIToolMode
|
|
234
268
|
parameters?: AIToolParameters
|
|
235
269
|
permission?: PluginPermissionRequirement
|
|
270
|
+
/**
|
|
271
|
+
* Quantity-limit key this tool's writes consume (e.g. 'bookings_month' for a
|
|
272
|
+
* booking-creating tool). Persist-mode tools that create countable rows MUST
|
|
273
|
+
* declare it so the agent executor can run the SAME plan-limit guard the UI
|
|
274
|
+
* runs (guard before execute, invalidate after) — an AI agent never clicks a
|
|
275
|
+
* gated button, so the declarative contract is its only gate.
|
|
276
|
+
*/
|
|
277
|
+
limitKey?: string
|
|
278
|
+
/**
|
|
279
|
+
* Where the tool runs. Server-plane is what makes browserless channels
|
|
280
|
+
* (WhatsApp, MCP) able to run the tool at all:
|
|
281
|
+
* - `{ plane:'server', kind:'rpc', rpc }` — the Fayz broker calls the named
|
|
282
|
+
* `agent_*` RPC in the tenant pool (writes / guarded reads).
|
|
283
|
+
* - `{ plane:'server', kind:'entity_read', entity }` — the broker runs the
|
|
284
|
+
* generic tenant-scoped read against the entity's table (derived tools).
|
|
285
|
+
* - `{ plane:'client' }` (or absent) — a locally registered executor runs it
|
|
286
|
+
* in the surface (navigation, modals).
|
|
287
|
+
*/
|
|
288
|
+
execution?: AIToolExecution
|
|
289
|
+
/**
|
|
290
|
+
* Require an explicit human confirmation before executing. Defaults to true
|
|
291
|
+
* for `mode: 'persist'` tools — the channel renders it natively (FAB modal,
|
|
292
|
+
* WhatsApp yes/no reply).
|
|
293
|
+
*/
|
|
294
|
+
requiresConfirmation?: boolean
|
|
236
295
|
suggestions?: AIToolSuggestion[]
|
|
237
296
|
category?: string
|
|
238
297
|
tags?: string[]
|
|
@@ -314,6 +373,28 @@ export interface PluginManifest {
|
|
|
314
373
|
entities?: string[]
|
|
315
374
|
permissions?: string[]
|
|
316
375
|
declaredFeatures?: FeatureDeclaration[]
|
|
376
|
+
/**
|
|
377
|
+
* Quantity limits this plugin knows how to count. Aggregated by the plugin
|
|
378
|
+
* runtime into `PluginRuntime.pluginLimits` and consumed by the access engine
|
|
379
|
+
* (useLimit / useLimitGuard) so a plugin "is born compatible" with freemium
|
|
380
|
+
* caps without any per-app wiring. See {@link LimitDeclaration}.
|
|
381
|
+
*/
|
|
382
|
+
declaredLimits?: LimitDeclaration[]
|
|
383
|
+
/** Pool RPCs this plugin ships for the server-plane agent executor. */
|
|
384
|
+
declaredRpcs?: AgentRpcDeclaration[]
|
|
385
|
+
/**
|
|
386
|
+
* Read-models this plugin exposes to the agent's generic data primitives
|
|
387
|
+
* (searchRecords/queryData) BEYOND its CRUD registries — typically its
|
|
388
|
+
* views/ledgers (v_appointments, plg_financial_movements). `key` is a stable
|
|
389
|
+
* ASCII id (never derived from a translated label); the entity's own
|
|
390
|
+
* `permission` gates each call.
|
|
391
|
+
*/
|
|
392
|
+
queryEntities?: Array<{
|
|
393
|
+
key: string
|
|
394
|
+
entity: EntityDef
|
|
395
|
+
/** Base tables may opt into createRecord; views/ledgers stay read-only. */
|
|
396
|
+
writable?: boolean
|
|
397
|
+
}>
|
|
317
398
|
registries?: PluginRegistryDef[]
|
|
318
399
|
/**
|
|
319
400
|
* Connectors this plugin contributes. An ADDON plugin declares its connector(s)
|
|
@@ -386,4 +467,6 @@ export interface PluginRuntime {
|
|
|
386
467
|
/** Connectors contributed by active plugins, grouped by the host plugin they extend. */
|
|
387
468
|
connectorsByHost: Map<string, ConnectorDefinition[]>
|
|
388
469
|
pluginFeatures: FeatureDeclaration[]
|
|
470
|
+
/** Limit declarations aggregated from every active plugin's `declaredLimits`. */
|
|
471
|
+
pluginLimits: LimitDeclaration[]
|
|
389
472
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/entity/registry.ts"],"names":[],"mappings":";;;AAgBA,IAAM,cAAA,uBAAqB,GAAA,EAA8B;AAElD,SAAS,eAAe,KAAA,EAA+B;AAC5D,EAAA,cAAA,CAAe,GAAA,CAAI,KAAA,CAAM,SAAA,EAAW,KAAK,CAAA;AAC3C;AAEO,SAAS,eAAe,GAAA,EAA2C;AACxE,EAAA,OAAO,cAAA,CAAe,IAAI,GAAG,CAAA;AAC/B;AAEO,SAAS,cAAA,GAAqC;AACnD,EAAA,OAAO,KAAA,CAAM,IAAA,CAAK,cAAA,CAAe,MAAA,EAAQ,CAAA;AAC3C;AAEO,SAAS,mBAAA,GAA4B;AAC1C,EAAA,cAAA,CAAe,KAAA,EAAM;AACvB;AAEO,SAAS,gBAAgB,SAAA,EAA8B;AAC5D,EAAA,MAAM,OAAO,SAAA,CAAU,IAAA,CAAK,aAAY,CAAE,OAAA,CAAQ,QAAQ,GAAG,CAAA;AAC7D,EAAA,IAAI,SAAA,CAAU,IAAA,EAAM,SAAA,IAAa,SAAA,CAAU,MAAM,aAAA,EAAe;AAC9D,IAAA,OAAO,GAAG,SAAA,CAAU,IAAA,CAAK,SAAS,CAAA,CAAA,EAAI,SAAA,CAAU,KAAK,aAAa,CAAA,CAAA;AAAA,EACpE;AACA,EAAA,IAAI,SAAA,CAAU,IAAA,EAAM,SAAA,EAAW,OAAO,UAAU,IAAA,CAAK,SAAA;AACrD,EAAA,OAAO,IAAA;AACT","file":"chunk-2NTHNWEH.cjs","sourcesContent":["import type { EntityDef } from '../types/crud'\nimport type { EntityArchetype } from '../types/entities'\n\nexport interface RegisteredEntity {\n entityKey: string\n label: string\n labelPlural: string\n icon?: string\n fields: EntityDef['fields']\n source: 'app' | 'plugin'\n archetype?: EntityArchetype\n pluginId?: string\n pluginName?: string\n pluginIcon?: string\n}\n\nconst entityRegistry = new Map<string, RegisteredEntity>()\n\nexport function registerEntity(entry: RegisteredEntity): void {\n entityRegistry.set(entry.entityKey, entry)\n}\n\nexport function getEntityByKey(key: string): RegisteredEntity | undefined {\n return entityRegistry.get(key)\n}\n\nexport function getAllEntities(): RegisteredEntity[] {\n return Array.from(entityRegistry.values())\n}\n\nexport function clearEntityRegistry(): void {\n entityRegistry.clear()\n}\n\nexport function deriveEntityKey(entityDef: EntityDef): string {\n const name = entityDef.name.toLowerCase().replace(/\\s+/g, '-')\n if (entityDef.data?.archetype && entityDef.data?.archetypeKind) {\n return `${entityDef.data.archetype}:${entityDef.data.archetypeKind}`\n }\n if (entityDef.data?.archetype) return entityDef.data.archetype\n return name\n}\n"]}
|