@digimuza/system-core 0.2.0-rc.2 → 0.3.0-rc.1

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.
Files changed (151) hide show
  1. package/INTEGRATION.md +209 -3
  2. package/README.md +23 -0
  3. package/dist/ai/api.d.ts +120 -0
  4. package/dist/ai/api.d.ts.map +1 -0
  5. package/dist/ai/api.js +446 -0
  6. package/dist/ai/api.js.map +1 -0
  7. package/dist/ai/card.d.ts +27 -0
  8. package/dist/ai/card.d.ts.map +1 -0
  9. package/dist/ai/card.js +84 -0
  10. package/dist/ai/card.js.map +1 -0
  11. package/dist/ai/contract.d.ts +364 -0
  12. package/dist/ai/contract.d.ts.map +1 -0
  13. package/dist/ai/contract.js +101 -0
  14. package/dist/ai/contract.js.map +1 -0
  15. package/dist/ai/display.d.ts +20 -0
  16. package/dist/ai/display.d.ts.map +1 -0
  17. package/dist/ai/display.js +35 -0
  18. package/dist/ai/display.js.map +1 -0
  19. package/dist/ai/index.d.ts +7 -0
  20. package/dist/ai/index.d.ts.map +1 -0
  21. package/dist/ai/index.js +10 -0
  22. package/dist/ai/index.js.map +1 -0
  23. package/dist/ai/manifest.d.ts +25 -0
  24. package/dist/ai/manifest.d.ts.map +1 -0
  25. package/dist/ai/manifest.js +15 -0
  26. package/dist/ai/manifest.js.map +1 -0
  27. package/dist/ai/runner.d.ts +71 -0
  28. package/dist/ai/runner.d.ts.map +1 -0
  29. package/dist/ai/runner.js +246 -0
  30. package/dist/ai/runner.js.map +1 -0
  31. package/dist/ai/tools.d.ts +42 -0
  32. package/dist/ai/tools.d.ts.map +1 -0
  33. package/dist/ai/tools.js +117 -0
  34. package/dist/ai/tools.js.map +1 -0
  35. package/dist/audit/csv.d.ts +31 -0
  36. package/dist/audit/csv.d.ts.map +1 -0
  37. package/dist/audit/csv.js +39 -0
  38. package/dist/audit/csv.js.map +1 -0
  39. package/dist/audit/index.d.ts +5 -0
  40. package/dist/audit/index.d.ts.map +1 -0
  41. package/dist/audit/index.js +6 -0
  42. package/dist/audit/index.js.map +1 -0
  43. package/dist/convex/functions.d.ts +39 -0
  44. package/dist/convex/functions.d.ts.map +1 -1
  45. package/dist/convex/functions.js +38 -6
  46. package/dist/convex/functions.js.map +1 -1
  47. package/dist/convex/index.d.ts +1 -1
  48. package/dist/convex/index.d.ts.map +1 -1
  49. package/dist/convex/index.js.map +1 -1
  50. package/dist/core/access.d.ts +14 -0
  51. package/dist/core/access.d.ts.map +1 -1
  52. package/dist/core/access.js +62 -0
  53. package/dist/core/access.js.map +1 -1
  54. package/dist/core/audit-module.d.ts +5 -0
  55. package/dist/core/audit-module.d.ts.map +1 -0
  56. package/dist/core/audit-module.js +25 -0
  57. package/dist/core/audit-module.js.map +1 -0
  58. package/dist/core/audit.d.ts +31 -0
  59. package/dist/core/audit.d.ts.map +1 -1
  60. package/dist/core/audit.js +46 -2
  61. package/dist/core/audit.js.map +1 -1
  62. package/dist/core/catalog.d.ts +41 -0
  63. package/dist/core/catalog.d.ts.map +1 -1
  64. package/dist/core/catalog.js +45 -0
  65. package/dist/core/catalog.js.map +1 -1
  66. package/dist/core/context.d.ts +8 -2
  67. package/dist/core/context.d.ts.map +1 -1
  68. package/dist/core/context.js +4 -1
  69. package/dist/core/context.js.map +1 -1
  70. package/dist/core/index.d.ts +2 -0
  71. package/dist/core/index.d.ts.map +1 -1
  72. package/dist/core/index.js +2 -0
  73. package/dist/core/index.js.map +1 -1
  74. package/dist/core/notify.d.ts +37 -0
  75. package/dist/core/notify.d.ts.map +1 -0
  76. package/dist/core/notify.js +16 -0
  77. package/dist/core/notify.js.map +1 -0
  78. package/dist/core/roles.d.ts.map +1 -1
  79. package/dist/core/roles.js +5 -1
  80. package/dist/core/roles.js.map +1 -1
  81. package/dist/core/system-modules.d.ts +4 -0
  82. package/dist/core/system-modules.d.ts.map +1 -1
  83. package/dist/core/system-modules.js +11 -0
  84. package/dist/core/system-modules.js.map +1 -1
  85. package/dist/finance/ai-tools.d.ts +55 -0
  86. package/dist/finance/ai-tools.d.ts.map +1 -0
  87. package/dist/finance/ai-tools.js +65 -0
  88. package/dist/finance/ai-tools.js.map +1 -0
  89. package/dist/finance/index.d.ts +87 -1
  90. package/dist/finance/index.d.ts.map +1 -1
  91. package/dist/finance/index.js +92 -38
  92. package/dist/finance/index.js.map +1 -1
  93. package/dist/finance/search.d.ts +15 -0
  94. package/dist/finance/search.d.ts.map +1 -0
  95. package/dist/finance/search.js +51 -0
  96. package/dist/finance/search.js.map +1 -0
  97. package/dist/index.d.ts +2 -0
  98. package/dist/index.d.ts.map +1 -1
  99. package/dist/index.js +2 -0
  100. package/dist/index.js.map +1 -1
  101. package/dist/modules/index.d.ts +1 -1
  102. package/dist/modules/index.d.ts.map +1 -1
  103. package/dist/modules/index.js +1 -1
  104. package/dist/modules/index.js.map +1 -1
  105. package/dist/notifications/api.d.ts +130 -0
  106. package/dist/notifications/api.d.ts.map +1 -0
  107. package/dist/notifications/api.js +384 -0
  108. package/dist/notifications/api.js.map +1 -0
  109. package/dist/notifications/contract.d.ts +132 -0
  110. package/dist/notifications/contract.d.ts.map +1 -0
  111. package/dist/notifications/contract.js +59 -0
  112. package/dist/notifications/contract.js.map +1 -0
  113. package/dist/notifications/index.d.ts +6 -0
  114. package/dist/notifications/index.d.ts.map +1 -0
  115. package/dist/notifications/index.js +9 -0
  116. package/dist/notifications/index.js.map +1 -0
  117. package/dist/search/index.d.ts +95 -0
  118. package/dist/search/index.d.ts.map +1 -0
  119. package/dist/search/index.js +151 -0
  120. package/dist/search/index.js.map +1 -0
  121. package/package.json +41 -2
  122. package/src/ai/api.ts +535 -0
  123. package/src/ai/card.ts +91 -0
  124. package/src/ai/contract.ts +245 -0
  125. package/src/ai/display.ts +38 -0
  126. package/src/ai/index.ts +9 -0
  127. package/src/ai/manifest.ts +16 -0
  128. package/src/ai/runner.ts +275 -0
  129. package/src/ai/tools.ts +128 -0
  130. package/src/audit/csv.ts +60 -0
  131. package/src/audit/index.ts +7 -0
  132. package/src/convex/functions.ts +54 -7
  133. package/src/convex/index.ts +1 -1
  134. package/src/core/access.ts +55 -1
  135. package/src/core/audit-module.ts +26 -0
  136. package/src/core/audit.ts +68 -2
  137. package/src/core/catalog.ts +63 -0
  138. package/src/core/context.ts +13 -2
  139. package/src/core/index.ts +2 -0
  140. package/src/core/notify.ts +52 -0
  141. package/src/core/roles.ts +5 -1
  142. package/src/core/system-modules.ts +12 -0
  143. package/src/finance/ai-tools.ts +68 -0
  144. package/src/finance/index.ts +119 -42
  145. package/src/finance/search.ts +58 -0
  146. package/src/index.ts +2 -0
  147. package/src/modules/index.ts +3 -0
  148. package/src/notifications/api.ts +477 -0
  149. package/src/notifications/contract.ts +142 -0
  150. package/src/notifications/index.ts +8 -0
  151. package/src/search/index.ts +198 -0
@@ -0,0 +1,198 @@
1
+ // `@digimuza/system-core/search` (DIG-72): one global search over every
2
+ // installed module. Each module contributes a provider guarded by one of its
3
+ // own permissions; the query only runs the providers the caller may use now.
4
+ //
5
+ // // convex/search.ts
6
+ // import { defineSearch } from "@digimuza/system-core/search"
7
+ // import { financeSearch } from "@digimuza/system-core/finance"
8
+ // export const { search } = defineSearch(system, { providers: [financeSearch] }).functions
9
+ import { queryGeneric } from "convex/server"
10
+ import { v } from "convex/values"
11
+ import { decide, isModuleEnabled, isSuperActor, requireHuman, type Actor } from "../core/access.js"
12
+ import { MODULE_ID, PERMISSION_ID } from "../core/catalog.js"
13
+ import type { SystemContext } from "../core/context.js"
14
+ import { validation } from "../core/errors.js"
15
+ import { toConvexError, type ConvexSystem } from "../convex/functions.js"
16
+
17
+ export const SEARCH_QUERY_MIN = 2
18
+ export const SEARCH_QUERY_MAX = 100
19
+ /** Hits per provider when the caller passes no `limit`. */
20
+ export const SEARCH_DEFAULT_LIMIT = 5
21
+ /** Largest `limit` (hits per provider). */
22
+ export const SEARCH_PROVIDER_MAX = 20
23
+ /** Hits across all providers. */
24
+ export const SEARCH_TOTAL_MAX = 50
25
+
26
+ /** `module.name`, e.g. `finance.invoices`. */
27
+ export const SEARCH_PROVIDER_ID = /^[a-z][a-z0-9-]*\.[a-z][a-z0-9_-]*$/
28
+
29
+ export interface SearchHit {
30
+ /** Set by the search to the provider's id. */
31
+ providerId: string
32
+ /** Stable id of the hit within the provider (e.g. the document id). */
33
+ id: string
34
+ title: string
35
+ subtitle?: string
36
+ /** App-relative link, `/app/...`. Hits with any other href are dropped. */
37
+ href: string
38
+ /** Optional kind for an icon (e.g. `invoice`). */
39
+ kind?: string
40
+ }
41
+
42
+ /** What a provider's `search` gets. Runs inside the search query (read-only). */
43
+ export interface SearchContext {
44
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
45
+ db: any
46
+ sys: SystemContext
47
+ actor: Actor
48
+ }
49
+
50
+ export interface SearchProvider {
51
+ /** Stable `module.name`, starts with `${moduleId}.`. */
52
+ id: string
53
+ /** Owning module: the provider is skipped while it is disabled (except for an effective Super). */
54
+ moduleId: string
55
+ /** Fallback group label; the app translates by `id`. */
56
+ label: string
57
+ /** The owning module's permission the caller must hold now (checked with `decide`). */
58
+ permission: string
59
+ /** Trimmed, validated query; return at most `limit` hits, best first. */
60
+ search(ctx: SearchContext, input: { query: string; limit: number }): Promise<Omit<SearchHit, "providerId">[] | SearchHit[]>
61
+ }
62
+
63
+ export interface SearchGroup {
64
+ providerId: string
65
+ moduleId: string
66
+ label: string
67
+ hits: SearchHit[]
68
+ }
69
+
70
+ export interface SearchResult {
71
+ /** The trimmed query. */
72
+ query: string
73
+ /** Providers with at least one hit, in the order they were installed. */
74
+ groups: SearchGroup[]
75
+ /** Hits across all groups (≤ SEARCH_TOTAL_MAX). */
76
+ total: number
77
+ }
78
+
79
+ /** Declares a provider. Identity at runtime; validates ids and the permission format. */
80
+ export function defineSearchProvider<const P extends SearchProvider>(provider: P): P {
81
+ const id = typeof provider?.id === "string" ? provider.id : ""
82
+ if (!SEARCH_PROVIDER_ID.test(id)) throw new Error(`defineSearchProvider: invalid id "${id}" (expected module.name, e.g. finance.invoices)`)
83
+ if (typeof provider.moduleId !== "string" || !MODULE_ID.test(provider.moduleId)) throw new Error(`defineSearchProvider(${id}): invalid moduleId`)
84
+ if (!id.startsWith(`${provider.moduleId}.`)) throw new Error(`defineSearchProvider(${id}): id must start with "${provider.moduleId}."`)
85
+ if (typeof provider.permission !== "string" || !PERMISSION_ID.test(provider.permission)) throw new Error(`defineSearchProvider(${id}): invalid permission`)
86
+ if (typeof provider.label !== "string" || !provider.label.trim()) throw new Error(`defineSearchProvider(${id}): label is required`)
87
+ if (typeof provider.search !== "function") throw new Error(`defineSearchProvider(${id}): search must be a function`)
88
+ return provider
89
+ }
90
+
91
+ const SAFE_HREF = /^\/(?!\/)[^\s\\]*$/
92
+ const clip = (s: string, n: number) => (s.length > n ? `${s.slice(0, n - 1)}…` : s)
93
+
94
+ /** Keeps well-formed hits with a safe, app-relative href. */
95
+ function cleanHit(providerId: string, raw: unknown): SearchHit | null {
96
+ if (!raw || typeof raw !== "object") return null
97
+ const h = raw as Record<string, unknown>
98
+ if (typeof h.id !== "string" || !h.id || typeof h.title !== "string" || !h.title.trim()) return null
99
+ if (typeof h.href !== "string" || h.href.length > 500 || !SAFE_HREF.test(h.href)) return null
100
+ const hit: SearchHit = { providerId, id: h.id, title: clip(h.title.trim(), 200), href: h.href }
101
+ if (typeof h.subtitle === "string" && h.subtitle.trim()) hit.subtitle = clip(h.subtitle.trim(), 300)
102
+ if (typeof h.kind === "string" && h.kind) hit.kind = clip(h.kind, 40)
103
+ return hit
104
+ }
105
+
106
+ /** Trims and checks the query; throws VALIDATION (`fields: ["query"]`). */
107
+ export function normalizeSearchQuery(query: string): string {
108
+ const q = query.replace(/\s+/g, " ").trim()
109
+ if (q.length < SEARCH_QUERY_MIN) throw validation("query_too_short", ["query"])
110
+ if (q.length > SEARCH_QUERY_MAX) throw validation("query_too_long", ["query"])
111
+ return q
112
+ }
113
+
114
+ /** Runs the providers the actor may use. No audit: it is a read. */
115
+ export async function runSearch(
116
+ ctx: { db: unknown },
117
+ sys: SystemContext,
118
+ actor: Actor,
119
+ providers: readonly SearchProvider[],
120
+ input: { query: string; limit?: number | undefined },
121
+ ): Promise<SearchResult> {
122
+ const query = normalizeSearchQuery(input.query)
123
+ const perProvider = Math.min(Math.max(1, Math.floor(input.limit ?? SEARCH_DEFAULT_LIMIT)), SEARCH_PROVIDER_MAX)
124
+ const isSuper = await isSuperActor(sys, actor)
125
+ const groups: SearchGroup[] = []
126
+ let total = 0
127
+ for (const provider of providers) {
128
+ if (total >= SEARCH_TOTAL_MAX) break
129
+ if (!isSuper && !isModuleEnabled(sys, await sys.store.get("modules", provider.moduleId))) continue
130
+ if (!(await decide(sys, actor, provider.permission)).allowed) continue
131
+ const limit = Math.min(perProvider, SEARCH_TOTAL_MAX - total)
132
+ let raw: unknown[]
133
+ try {
134
+ raw = (await provider.search({ db: ctx.db, sys, actor }, { query, limit })) ?? []
135
+ } catch (error) {
136
+ // One failing provider never fails the whole search. The query is not logged.
137
+ sys.logger.error("search_provider_failed", { providerId: provider.id, error })
138
+ continue
139
+ }
140
+ const hits: SearchHit[] = []
141
+ const seen = new Set<string>()
142
+ for (const r of Array.isArray(raw) ? raw : []) {
143
+ const hit = cleanHit(provider.id, r)
144
+ if (!hit || seen.has(hit.id)) continue
145
+ seen.add(hit.id)
146
+ hits.push(hit)
147
+ if (hits.length >= limit) break
148
+ }
149
+ if (!hits.length) continue
150
+ groups.push({ providerId: provider.id, moduleId: provider.moduleId, label: provider.label, hits })
151
+ total += hits.length
152
+ }
153
+ return { query, groups, total }
154
+ }
155
+
156
+ export interface SearchOptions {
157
+ providers: readonly SearchProvider[]
158
+ }
159
+
160
+ /**
161
+ * The search's Convex functions, bound to the app's system:
162
+ * export const { search } = defineSearch(system, { providers: [financeSearch] }).functions
163
+ */
164
+ export function defineSearch(system: ConvexSystem, options: SearchOptions) {
165
+ const catalog = system.definition.catalog
166
+ const ids = new Set<string>()
167
+ for (const p of options.providers) {
168
+ if (ids.has(p.id)) throw new Error(`defineSearch: duplicate provider id "${p.id}"`)
169
+ ids.add(p.id)
170
+ if (!catalog.modules.has(p.moduleId)) throw new Error(`defineSearch(${p.id}): module "${p.moduleId}" is not installed in this system`)
171
+ if (!catalog.permissions.has(p.permission)) throw new Error(`defineSearch(${p.id}): unknown permission "${p.permission}"`)
172
+ }
173
+ const providers = [...options.providers]
174
+
175
+ return {
176
+ functions: {
177
+ /**
178
+ * `({ query, limit? })` → SearchResult. Human sessions only (API keys: FORBIDDEN).
179
+ * `limit` is hits per provider (default 5, max 20); at most 50 in total.
180
+ * VALIDATION (`fields: ["query"]`) unless 2–100 characters after trimming.
181
+ */
182
+ search: queryGeneric({
183
+ args: { query: v.string(), limit: v.optional(v.number()), apiKey: v.optional(v.string()), operationId: v.optional(v.string()) },
184
+ handler: async (ctx, a): Promise<SearchResult> => {
185
+ const { sys, actor } = await system.context(ctx, { apiKey: a.apiKey, operationId: a.operationId })
186
+ try {
187
+ const human = requireHuman(sys, actor)
188
+ return await runSearch(ctx, sys, human, providers, { query: a.query, limit: a.limit })
189
+ } catch (e) {
190
+ throw toConvexError(e, sys.operationId)
191
+ }
192
+ },
193
+ }),
194
+ },
195
+ }
196
+ }
197
+
198
+ export type SearchFunctions = ReturnType<typeof defineSearch>["functions"]