@pathmx/core 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/LICENSE.md +172 -0
  2. package/README.md +111 -0
  3. package/actions/actions.ts +307 -0
  4. package/actions/draft.ts +105 -0
  5. package/actions/transaction.ts +164 -0
  6. package/assets.ts +149 -0
  7. package/authority/access.ts +820 -0
  8. package/authority/actors.ts +271 -0
  9. package/authority/agents.ts +95 -0
  10. package/authority/credentials.ts +144 -0
  11. package/authority/model.ts +71 -0
  12. package/authority/sharing.ts +89 -0
  13. package/browser-bundle.ts +61 -0
  14. package/canonical.ts +41 -0
  15. package/cli/dev.ts +134 -0
  16. package/cli/export.ts +74 -0
  17. package/cli/index.ts +52 -0
  18. package/cli/lint.ts +164 -0
  19. package/cli/mv.ts +30 -0
  20. package/cli/perms.ts +182 -0
  21. package/cli/plugin-packages.ts +159 -0
  22. package/cli/plugins.ts +95 -0
  23. package/cli/query.ts +225 -0
  24. package/cli/readme.md +95 -0
  25. package/cli/release.ts +75 -0
  26. package/cli/rm.ts +22 -0
  27. package/cli/serve.ts +100 -0
  28. package/cli/tree.ts +131 -0
  29. package/cli/update.ts +78 -0
  30. package/cli/utils.ts +53 -0
  31. package/core.ts +96 -0
  32. package/database.ts +189 -0
  33. package/dependencies.ts +60 -0
  34. package/dist/pathmx.js +1224 -0
  35. package/document.ts +194 -0
  36. package/environment.ts +110 -0
  37. package/environments/bun-plugins.ts +68 -0
  38. package/environments/bun-publish.ts +194 -0
  39. package/environments/bun.ts +223 -0
  40. package/globals.d.ts +14 -0
  41. package/graph.ts +153 -0
  42. package/host/compiler.ts +314 -0
  43. package/host/create.ts +66 -0
  44. package/host/engine.ts +630 -0
  45. package/host/plugin-host.ts +487 -0
  46. package/host/render.ts +102 -0
  47. package/host/response.ts +20 -0
  48. package/host/shell.html +15 -0
  49. package/host/source-sync.ts +254 -0
  50. package/html-attributes.ts +15 -0
  51. package/html.ts +48 -0
  52. package/icons/LICENSE +43 -0
  53. package/icons/lucide.ts +54 -0
  54. package/index.ts +75 -0
  55. package/invalidation.ts +85 -0
  56. package/log.ts +84 -0
  57. package/ops/href.ts +87 -0
  58. package/ops/index.ts +9 -0
  59. package/ops/move-source.ts +114 -0
  60. package/ops/plan.ts +47 -0
  61. package/ops/remove-source.ts +70 -0
  62. package/package.json +46 -0
  63. package/path.ts +75 -0
  64. package/pattern.ts +314 -0
  65. package/perf.ts +88 -0
  66. package/plugins/actor-path.ts +43 -0
  67. package/plugins/actor-profile.ts +55 -0
  68. package/plugins/agent-browsing.ts +183 -0
  69. package/plugins/app-component.ts +23 -0
  70. package/plugins/block-layout/index.ts +78 -0
  71. package/plugins/block-layout/theme.css +90 -0
  72. package/plugins/callout/index.ts +236 -0
  73. package/plugins/callout/theme.css +138 -0
  74. package/plugins/canonical-links.ts +144 -0
  75. package/plugins/code-highlight/client.ts +89 -0
  76. package/plugins/code-highlight/index.ts +147 -0
  77. package/plugins/code-highlight/theme.css +89 -0
  78. package/plugins/context.ts +131 -0
  79. package/plugins/datatype/Datatype[wdth,wght].woff2 +0 -0
  80. package/plugins/datatype/OFL.txt +96 -0
  81. package/plugins/datatype/index.ts +287 -0
  82. package/plugins/definition-list.ts +236 -0
  83. package/plugins/directive-definitions.ts +13 -0
  84. package/plugins/embedding/client.ts +88 -0
  85. package/plugins/embedding/index.ts +43 -0
  86. package/plugins/image/index.ts +68 -0
  87. package/plugins/image/theme.css +43 -0
  88. package/plugins/include.ts +119 -0
  89. package/plugins/index.ts +91 -0
  90. package/plugins/input/authoring.ts +236 -0
  91. package/plugins/input/config.ts +85 -0
  92. package/plugins/input/index.ts +359 -0
  93. package/plugins/input/model.ts +173 -0
  94. package/plugins/input/state.ts +272 -0
  95. package/plugins/layout.ts +313 -0
  96. package/plugins/literate/client.ts +208 -0
  97. package/plugins/literate/index.ts +742 -0
  98. package/plugins/math/index.ts +140 -0
  99. package/plugins/meta.ts +42 -0
  100. package/plugins/navigation/index.ts +158 -0
  101. package/plugins/navigation/model.ts +364 -0
  102. package/plugins/preset.ts +65 -0
  103. package/plugins/query/index.ts +322 -0
  104. package/plugins/rss/index.ts +46 -0
  105. package/plugins/runtime/actions.ts +62 -0
  106. package/plugins/runtime/client.ts +8 -0
  107. package/plugins/runtime/document.ts +506 -0
  108. package/plugins/runtime/error.ts +154 -0
  109. package/plugins/runtime/heading-links.ts +31 -0
  110. package/plugins/runtime/index.ts +104 -0
  111. package/plugins/runtime/live.ts +51 -0
  112. package/plugins/runtime/navigation.ts +113 -0
  113. package/plugins/runtime/pending.ts +53 -0
  114. package/plugins/runtime/publication.ts +19 -0
  115. package/plugins/runtime/start.ts +177 -0
  116. package/plugins/runtime/static-client.ts +6 -0
  117. package/plugins/runtime/static-live.ts +188 -0
  118. package/plugins/runtime/view-transition.ts +72 -0
  119. package/plugins/runtime/viewport.ts +110 -0
  120. package/plugins/schema.ts +24 -0
  121. package/plugins/scripts.ts +140 -0
  122. package/plugins/source-preview/client.ts +256 -0
  123. package/plugins/source-preview/index.ts +132 -0
  124. package/plugins/source-preview/model.ts +11 -0
  125. package/plugins/source-preview/preview.css +107 -0
  126. package/plugins/styles.ts +145 -0
  127. package/plugins/theme/components.ts +10 -0
  128. package/plugins/theme/foundation.css +358 -0
  129. package/plugins/theme/index.ts +333 -0
  130. package/plugins/theme/prose.css +284 -0
  131. package/plugins/theme/scheme.js +63 -0
  132. package/plugins/theme/toggle.css +44 -0
  133. package/plugins/theme/view-transition.css +68 -0
  134. package/plugins/toc/client.ts +146 -0
  135. package/plugins/toc/index.ts +232 -0
  136. package/plugins/toc/theme.css +172 -0
  137. package/plugins/types.ts +538 -0
  138. package/problem.ts +41 -0
  139. package/project.ts +6 -0
  140. package/publish/build.ts +402 -0
  141. package/publish/concurrent.ts +32 -0
  142. package/publish/identity.ts +63 -0
  143. package/publish/index.ts +11 -0
  144. package/publish/location.ts +167 -0
  145. package/publish/model.ts +58 -0
  146. package/publish/plan.ts +300 -0
  147. package/publish/validate.ts +150 -0
  148. package/query/candidate-index.ts +101 -0
  149. package/query/dependency-index.ts +99 -0
  150. package/query/fields.ts +216 -0
  151. package/query/invalidation.ts +48 -0
  152. package/query/kernel.ts +776 -0
  153. package/query/model.ts +79 -0
  154. package/repository.ts +374 -0
  155. package/runtime-profile.ts +34 -0
  156. package/schema/model.ts +92 -0
  157. package/schema/registry.ts +642 -0
  158. package/schema/standard.ts +226 -0
  159. package/server/context.ts +94 -0
  160. package/server/css-imports.ts +66 -0
  161. package/server/http.ts +333 -0
  162. package/server/page.ts +21 -0
  163. package/server/protocols.ts +12 -0
  164. package/server/representations.ts +66 -0
  165. package/server/router.ts +131 -0
  166. package/server/routes.ts +202 -0
  167. package/server/serve.ts +5 -0
  168. package/server/watch.ts +120 -0
  169. package/source/blocks.ts +71 -0
  170. package/source/format.ts +164 -0
  171. package/source/index.ts +67 -0
  172. package/source/interpolate.ts +63 -0
  173. package/source/markdown/blocks.ts +46 -0
  174. package/source/markdown/comments.ts +32 -0
  175. package/source/markdown/fences.ts +67 -0
  176. package/source/markdown/headings.ts +88 -0
  177. package/source/markdown/index.ts +34 -0
  178. package/source/markdown/inline-code.ts +6 -0
  179. package/source/markdown/links.ts +326 -0
  180. package/source/markdown/lists.ts +223 -0
  181. package/source/markdown/prose.ts +74 -0
  182. package/source/markdown/scan.ts +79 -0
  183. package/source/markdown/text.ts +82 -0
  184. package/source/meta.ts +87 -0
  185. package/source/source.ts +121 -0
  186. package/source/topmatter.ts +57 -0
  187. package/source/utils.ts +21 -0
  188. package/source-diagnostic.ts +63 -0
  189. package/text-file.ts +26 -0
  190. package/util.ts +35 -0
  191. package/view/components.ts +499 -0
  192. package/view/model.ts +91 -0
  193. package/view/projection.ts +162 -0
  194. package/view/session.ts +146 -0
  195. package/view/syntax.ts +372 -0
package/query/model.ts ADDED
@@ -0,0 +1,79 @@
1
+ import type { SqlDatabase } from "../database.ts"
2
+ import type { DependencyContext } from "../dependencies.ts"
3
+ import type { PathMXDataSchema, StandardSchemaV1 } from "../schema/standard.ts"
4
+ import type { BlockSummary, SourceSummary, View } from "../view/model.ts"
5
+
6
+ export type QueryScalar = string | number | boolean | null
7
+
8
+ export type QuerySort = Readonly<{
9
+ by: string
10
+ direction?: "asc" | "desc"
11
+ }>
12
+
13
+ export type QueryRequest<Input = undefined> = Readonly<{
14
+ from?: string
15
+ input?: Input
16
+ where?: Readonly<Record<string, QueryScalar>>
17
+ sort?: readonly QuerySort[]
18
+ limit?: number
19
+ }>
20
+
21
+ export type SourceQueryRequest = QueryRequest &
22
+ Readonly<{
23
+ type?: string
24
+ has?: string | readonly string[]
25
+ recursive?: boolean
26
+ }>
27
+
28
+ export type BlockQueryRequest = SourceQueryRequest
29
+
30
+ type Awaitable<T> = T | Promise<T>
31
+ type DataSchema = PathMXDataSchema<unknown, unknown>
32
+ type OutputSchema = PathMXDataSchema<unknown, readonly unknown[]>
33
+
34
+ export type QueryDefinition<
35
+ InputSchema extends DataSchema | undefined = DataSchema | undefined,
36
+ ResultSchema extends OutputSchema = OutputSchema,
37
+ > = Readonly<{
38
+ input?: InputSchema
39
+ output: ResultSchema
40
+ resolve(
41
+ input: InputSchema extends DataSchema
42
+ ? StandardSchemaV1.InferOutput<InputSchema>
43
+ : undefined,
44
+ context: QueryResolverContext,
45
+ ): Awaitable<StandardSchemaV1.InferOutput<ResultSchema>>
46
+ }>
47
+
48
+ export type QueryContribution = QueryDefinition<any, any>
49
+
50
+ export type QueryDefinitionInput<Definition extends QueryDefinition> =
51
+ Definition["input"] extends DataSchema
52
+ ? StandardSchemaV1.InferInput<Definition["input"]>
53
+ : undefined
54
+
55
+ export type QueryDefinitionOutput<Definition extends QueryDefinition> =
56
+ StandardSchemaV1.InferOutput<Definition["output"]>
57
+
58
+ export type QueryRunner = {
59
+ <Definition extends QueryDefinition>(
60
+ definition: Definition,
61
+ request?: QueryRequest<QueryDefinitionInput<Definition>>,
62
+ ): Promise<QueryDefinitionOutput<Definition>>
63
+ (id: string, request?: QueryRequest<unknown>): Promise<readonly unknown[]>
64
+ }
65
+
66
+ export type ViewQuery = Readonly<{
67
+ sources(request?: SourceQueryRequest): Promise<readonly SourceSummary[]>
68
+ blocks(request?: BlockQueryRequest): Promise<readonly BlockSummary[]>
69
+ run: QueryRunner
70
+ }>
71
+
72
+ export type QueryResolverContext = DependencyContext &
73
+ Readonly<{
74
+ view: View
75
+ from: string
76
+ query: ViewQuery
77
+ /** Database isolated to the query's owning Plugin. */
78
+ database?: SqlDatabase
79
+ }>
package/repository.ts ADDED
@@ -0,0 +1,374 @@
1
+ import type { PathMXEnvironment } from "./environment.ts"
2
+ import { type Source, createSource, resolveSourceId } from "./source/index.ts"
3
+ import { canonicalSourceId } from "./canonical.ts"
4
+ import {
5
+ readTextFileState,
6
+ sameTextFileState,
7
+ type TextFileState,
8
+ } from "./text-file.ts"
9
+
10
+ const LOAD_CONCURRENCY = 16
11
+
12
+ export type SourceChange =
13
+ | { type: "create"; path: string; content: string }
14
+ | { type: "replace"; path: string; content: string }
15
+ | { type: "delete"; path: string }
16
+
17
+ export type SourceChangeSet = {
18
+ changes: SourceChange[]
19
+ }
20
+
21
+ export type CommitResult = {
22
+ applied: SourceChange[]
23
+ error?: Error
24
+ }
25
+
26
+ export type UpsertResult =
27
+ | { type: "updated"; source: Source }
28
+ | { type: "deleted" }
29
+ | { type: "error"; error: SourceLoadError; source?: Source }
30
+
31
+ export class DuplicateSourceIdError extends Error {
32
+ readonly id: string
33
+ readonly existingPath: string
34
+ readonly incomingPath: string
35
+
36
+ constructor(id: string, existingPath: string, incomingPath: string) {
37
+ super(`Duplicate source id "${id}": ${existingPath} and ${incomingPath}`)
38
+ this.name = "DuplicateSourceIdError"
39
+ this.id = id
40
+ this.existingPath = existingPath
41
+ this.incomingPath = incomingPath
42
+ }
43
+ }
44
+
45
+ export class SourceLoadError extends Error {
46
+ readonly path: string
47
+
48
+ constructor(path: string, cause: unknown) {
49
+ const detail = cause instanceof Error ? cause.message : String(cause)
50
+ super(`Failed to load source ${path}: ${detail}`)
51
+ this.name = "SourceLoadError"
52
+ this.path = path
53
+ this.cause = cause
54
+ }
55
+ }
56
+
57
+ export class SourceCommitError extends Error {
58
+ constructor(message: string) {
59
+ super(message)
60
+ this.name = "SourceCommitError"
61
+ }
62
+ }
63
+
64
+ export class SourceConflictError extends SourceCommitError {
65
+ readonly path: string
66
+
67
+ constructor(path: string) {
68
+ super(`Source changed before commit: ${path}`)
69
+ this.name = "SourceConflictError"
70
+ this.path = path
71
+ }
72
+ }
73
+
74
+ function isNotFound(error: unknown) {
75
+ return (
76
+ error instanceof Error &&
77
+ "code" in error &&
78
+ (error as { code?: string }).code === "ENOENT"
79
+ )
80
+ }
81
+
82
+ export class Repository {
83
+ rootPath: string
84
+ environment: PathMXEnvironment
85
+ private byId = new Map<string, Source>()
86
+ private byPath = new Map<string, Source>()
87
+ private revisionValue = 0
88
+
89
+ constructor(rootPath: string, environment: PathMXEnvironment) {
90
+ this.rootPath = rootPath
91
+ this.environment = environment
92
+ }
93
+
94
+ graphPath(relPath: string) {
95
+ return this.environment.path.join("/", relPath.replaceAll("\\", "/"))
96
+ }
97
+
98
+ pathToId(path: string) {
99
+ return resolveSourceId(this.graphPath(path), {})
100
+ }
101
+
102
+ get(id: string) {
103
+ return this.byId.get(id)
104
+ }
105
+
106
+ atPath(path: string) {
107
+ const routePath = this.graphPath(path).replace(/\/$/, "") || "/"
108
+ return this.byPath.get(routePath)
109
+ }
110
+
111
+ /** @deprecated Use atPath. */
112
+ sourceAtPath(path: string) {
113
+ return this.atPath(path)
114
+ }
115
+
116
+ all() {
117
+ return [...this.byPath.values()]
118
+ }
119
+
120
+ /** Changes whenever repository membership or source identity changes. */
121
+ get revision() {
122
+ return this.revisionValue
123
+ }
124
+
125
+ async load() {
126
+ this.byId.clear()
127
+ this.byPath.clear()
128
+ this.revisionValue++
129
+ const files = [
130
+ ...(await this.environment.storage.list(this.rootPath)),
131
+ ].filter((file) => file.endsWith(".md"))
132
+
133
+ for (let start = 0; start < files.length; start += LOAD_CONCURRENCY) {
134
+ const loaded = await Promise.all(
135
+ files
136
+ .slice(start, start + LOAD_CONCURRENCY)
137
+ .map((file) => this.readListedSource(file)),
138
+ )
139
+ for (const item of loaded) {
140
+ if (!item) continue
141
+ if ("error" in item) throw item.error
142
+ try {
143
+ this.put(createSource(item.path, item.content, this.environment.yaml))
144
+ } catch (error) {
145
+ if (error instanceof DuplicateSourceIdError) throw error
146
+ throw new SourceLoadError(item.path, error)
147
+ }
148
+ }
149
+ }
150
+ }
151
+
152
+ private async readListedSource(relPath: string) {
153
+ const path = this.graphPath(relPath)
154
+ const storagePath = this.storagePath(relPath)
155
+ if (!storagePath) return
156
+ try {
157
+ return {
158
+ path,
159
+ content: await this.environment.storage.readText(storagePath),
160
+ }
161
+ } catch (error) {
162
+ if (isNotFound(error)) return
163
+ try {
164
+ if (!(await this.environment.storage.exists(storagePath))) return
165
+ } catch {
166
+ // Preserve the original read error when existence cannot be checked.
167
+ }
168
+ return { error: new SourceLoadError(path, error) }
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Index a constructed Source after enforcing the one-to-one id/path invariant.
174
+ */
175
+ put(source: Source) {
176
+ canonicalSourceId(source.id)
177
+ const existing = this.byId.get(source.id)
178
+ if (existing && existing.path !== source.path) {
179
+ throw new DuplicateSourceIdError(source.id, existing.path, source.path)
180
+ }
181
+ const previous = this.byPath.get(source.path)
182
+ if (previous && previous.id !== source.id) this.byId.delete(previous.id)
183
+ this.byId.set(source.id, source)
184
+ this.byPath.set(source.path, source)
185
+ this.revisionValue++
186
+ return source
187
+ }
188
+
189
+ async upsert(
190
+ relPath: string,
191
+ options: { onError?: "throw" | "preserve" } = {},
192
+ ): Promise<UpsertResult> {
193
+ const onError = options.onError ?? "preserve"
194
+ const path = this.graphPath(relPath)
195
+ const storagePath = this.storagePath(relPath)
196
+ const previous = this.atPath(path)
197
+ if (!storagePath) {
198
+ this.drop(previous)
199
+ return { type: "deleted" }
200
+ }
201
+ try {
202
+ if (!(await this.environment.storage.exists(storagePath))) {
203
+ this.drop(previous)
204
+ return { type: "deleted" }
205
+ }
206
+ const content = await this.environment.storage.readText(storagePath)
207
+ // Same text: keep the indexed Source. createSource() starts with
208
+ // empty blocks; replacing here made the watch echo after commit
209
+ // drop them (syncSource returns early when content is unchanged).
210
+ if (previous && previous.content === content) {
211
+ return { type: "updated", source: previous }
212
+ }
213
+ const source = createSource(path, content, this.environment.yaml)
214
+ this.put(source)
215
+ return { type: "updated", source }
216
+ } catch (error) {
217
+ if (isNotFound(error)) {
218
+ this.drop(previous)
219
+ return { type: "deleted" }
220
+ }
221
+ if (error instanceof DuplicateSourceIdError) {
222
+ if (onError === "throw") throw error
223
+ return {
224
+ type: "error",
225
+ error: new SourceLoadError(path, error),
226
+ source: previous,
227
+ }
228
+ }
229
+ const loadError = new SourceLoadError(path, error)
230
+ if (onError === "throw") throw loadError
231
+ return { type: "error", error: loadError, source: previous }
232
+ }
233
+ }
234
+
235
+ storagePath(targetPath: string) {
236
+ const rel = targetPath.replaceAll("\\", "/").replace(/^\/+/, "")
237
+ if (!rel || rel.split("/").includes("..")) return
238
+ const normalizedRoot = this.rootPath.replaceAll("\\", "/")
239
+ const root =
240
+ normalizedRoot === "/" ? "/" : normalizedRoot.replace(/\/+$/, "")
241
+ if (root === "/") return `/${rel}`
242
+ return root ? `${root}/${rel}` : rel
243
+ }
244
+
245
+ async isFile(targetPath: string) {
246
+ const storagePath = this.storagePath(targetPath)
247
+ if (!storagePath) return false
248
+ try {
249
+ return await this.environment.storage.exists(storagePath)
250
+ } catch {
251
+ return false
252
+ }
253
+ }
254
+
255
+ remove(relPath: string) {
256
+ this.drop(this.atPath(relPath))
257
+ }
258
+
259
+ async commit(changeSet: SourceChangeSet): Promise<CommitResult> {
260
+ const changes = changeSet.changes.map((change) => {
261
+ if (change.path.split(/[\\/]/).includes("..")) {
262
+ throw new SourceCommitError(`Path escapes repository: ${change.path}`)
263
+ }
264
+ return { ...change, path: this.graphPath(change.path) }
265
+ })
266
+ this.preflight(changes)
267
+ await this.verifyStorage(changes)
268
+
269
+ const applied: SourceChange[] = []
270
+ try {
271
+ for (const change of changes) {
272
+ const file = this.storagePath(change.path)
273
+ if (!file)
274
+ throw new SourceCommitError(`Path escapes repository: ${change.path}`)
275
+ if (change.type === "delete") {
276
+ if (await this.environment.storage.exists(file)) {
277
+ await this.environment.storage.delete(file)
278
+ }
279
+ } else {
280
+ await this.environment.storage.writeText(file, change.content)
281
+ }
282
+ applied.push(change)
283
+ }
284
+ } catch (error) {
285
+ await this.resync(applied)
286
+ return {
287
+ applied,
288
+ error: error instanceof Error ? error : new Error(String(error)),
289
+ }
290
+ }
291
+
292
+ await this.resync(applied)
293
+ return { applied }
294
+ }
295
+
296
+ private preflight(changes: SourceChange[]) {
297
+ const nextByPath = new Map(this.byPath)
298
+ const nextById = new Map(this.byId)
299
+
300
+ for (const change of changes) {
301
+ const file = this.storagePath(change.path)
302
+ if (!file) {
303
+ throw new SourceCommitError(`Path escapes repository: ${change.path}`)
304
+ }
305
+ const previous = nextByPath.get(change.path)
306
+ if (change.type === "delete") {
307
+ if (!previous) {
308
+ throw new SourceCommitError(`Path does not exist: ${change.path}`)
309
+ }
310
+ nextByPath.delete(change.path)
311
+ nextById.delete(previous.id)
312
+ continue
313
+ }
314
+
315
+ const source = createSource(
316
+ change.path,
317
+ change.content,
318
+ this.environment.yaml,
319
+ )
320
+ if (change.type === "create" && previous) {
321
+ throw new SourceCommitError(`Path already exists: ${change.path}`)
322
+ }
323
+ if (change.type === "replace" && !previous) {
324
+ throw new SourceCommitError(`Path does not exist: ${change.path}`)
325
+ }
326
+ if (previous && previous.id !== source.id) nextById.delete(previous.id)
327
+ const owner = nextById.get(source.id)
328
+ if (owner && owner.path !== change.path) {
329
+ throw new DuplicateSourceIdError(source.id, owner.path, change.path)
330
+ }
331
+ nextByPath.set(change.path, source)
332
+ nextById.set(source.id, source)
333
+ }
334
+ }
335
+
336
+ private async verifyStorage(changes: SourceChange[]) {
337
+ for (const change of changes) {
338
+ const file = this.storagePath(change.path)
339
+ if (!file)
340
+ throw new SourceCommitError(`Path escapes repository: ${change.path}`)
341
+ const before = this.atPath(change.path)
342
+ if (change.type !== "create" && !before) {
343
+ throw new SourceConflictError(change.path)
344
+ }
345
+ const expected: TextFileState = before
346
+ ? { type: "present", content: before.content }
347
+ : { type: "missing" }
348
+ let current: TextFileState
349
+ try {
350
+ current = await readTextFileState(this.environment.storage, file)
351
+ } catch {
352
+ throw new SourceConflictError(change.path)
353
+ }
354
+ if (!sameTextFileState(current, expected)) {
355
+ throw new SourceConflictError(change.path)
356
+ }
357
+ }
358
+ }
359
+
360
+ private async resync(changes: SourceChange[]) {
361
+ for (const change of changes) {
362
+ const rel = change.path.replace(/^\/+/, "")
363
+ if (change.type === "delete") this.remove(change.path)
364
+ else await this.upsert(rel, { onError: "throw" })
365
+ }
366
+ }
367
+
368
+ private drop(source?: Source) {
369
+ if (!source) return
370
+ this.byId.delete(source.id)
371
+ this.byPath.delete(source.path)
372
+ this.revisionValue++
373
+ }
374
+ }
@@ -0,0 +1,34 @@
1
+ export type RuntimeProfileOptions = Readonly<{
2
+ mode?: "development" | "production"
3
+ target?: "server" | "static"
4
+ capabilities?: readonly Readonly<{ plugin: string; key: string }>[]
5
+ }>
6
+
7
+ export type RuntimeProfile = Readonly<{
8
+ mode: NonNullable<RuntimeProfileOptions["mode"]>
9
+ target: NonNullable<RuntimeProfileOptions["target"]>
10
+ has(plugin: string, key: string): boolean
11
+ }>
12
+
13
+ /** Normalize portable startup configuration into an immutable runtime view. */
14
+ export function createRuntimeProfile(
15
+ options: RuntimeProfileOptions = {},
16
+ ): RuntimeProfile {
17
+ const capabilities = new Map<string, Set<string>>()
18
+ for (const capability of options.capabilities ?? []) {
19
+ let keys = capabilities.get(capability.plugin)
20
+ if (!keys) {
21
+ keys = new Set()
22
+ capabilities.set(capability.plugin, keys)
23
+ }
24
+ keys.add(capability.key)
25
+ }
26
+
27
+ return Object.freeze({
28
+ mode: options.mode ?? "production",
29
+ target: options.target ?? "server",
30
+ has(plugin: string, key: string) {
31
+ return capabilities.get(plugin)?.has(key) ?? false
32
+ },
33
+ })
34
+ }
@@ -0,0 +1,92 @@
1
+ import type { ActionContribution } from "../actions/actions.ts"
2
+ import type { QueryContribution } from "../query/model.ts"
3
+ import type { PathMXDataSchema } from "./standard.ts"
4
+ import type { SourceNode } from "../source/index.ts"
5
+
6
+ export type NodeTypeDefinition = Readonly<{
7
+ /** Singular human label. Falls back to the title-cased node type. */
8
+ label?: string
9
+ description?: string
10
+ /** Renderer-owned icon name, such as a Lucide icon id. */
11
+ icon?: string
12
+ /** Renderer-owned color token or CSS color. */
13
+ color?: string
14
+ /** Validate the complete authored data map for this node type. */
15
+ schema?: PathMXDataSchema
16
+ }>
17
+
18
+ export type SourceTypeDefinition = NodeTypeDefinition
19
+ export type BlockTypeDefinition = NodeTypeDefinition
20
+
21
+ export type SchemaContribution = Readonly<{
22
+ /** Ordered Source-type contributions. Later Plugins may refine presentation. */
23
+ sources?: Readonly<Record<string, SourceTypeDefinition>>
24
+ /** Ordered Block-type contributions. Later Plugins may refine presentation. */
25
+ blocks?: Readonly<Record<string, BlockTypeDefinition>>
26
+ /** Exclusively owned Source/Block data props and their value schemas. */
27
+ props?: Readonly<Record<string, PathMXDataSchema>>
28
+ /** Plugin-local query names installed externally as `<plugin>:<name>`. */
29
+ query?: Readonly<Record<string, QueryContribution>>
30
+ /** Plugin-local Action names installed externally as `<plugin>:<name>`. */
31
+ actions?: Readonly<Record<string, ActionContribution>>
32
+ }>
33
+
34
+ /** Preserve the complete inferred shape of one Plugin-owned Schema fragment. */
35
+ export function defineSchema<const Definition extends SchemaContribution>(
36
+ definition: Definition,
37
+ ) {
38
+ return definition
39
+ }
40
+
41
+ /** Serializable node-type meaning safe to project into Views and UIs. */
42
+ export type NodeType = Readonly<{
43
+ type: string
44
+ label: string
45
+ description?: string
46
+ icon: string
47
+ color: string
48
+ origin: "defined" | "observed"
49
+ plugins: readonly string[]
50
+ }>
51
+
52
+ export type SourceType = NodeType
53
+ export type BlockType = NodeType
54
+
55
+ export type DataProp = Readonly<{
56
+ name: string
57
+ plugin: string
58
+ schema: PathMXDataSchema
59
+ inputJsonSchema: Readonly<Record<string, unknown>>
60
+ outputJsonSchema: Readonly<Record<string, unknown>>
61
+ }>
62
+
63
+ export type NodeSchema = Readonly<{
64
+ plugin: string
65
+ schema: PathMXDataSchema
66
+ inputJsonSchema: Readonly<Record<string, unknown>>
67
+ outputJsonSchema: Readonly<Record<string, unknown>>
68
+ }>
69
+
70
+ export type QuerySchema = Readonly<{
71
+ id: string
72
+ plugin: string
73
+ input?: PathMXDataSchema
74
+ output: PathMXDataSchema<unknown, readonly unknown[]>
75
+ inputJsonSchema?: Readonly<Record<string, unknown>>
76
+ outputJsonSchema: Readonly<Record<string, unknown>>
77
+ }>
78
+
79
+ export type ApplicationSchema = Readonly<{
80
+ sourceType(sourceOrType: SourceNode | string): SourceType
81
+ sourceTypes(): readonly SourceType[]
82
+ blockType(blockOrType: SourceNode | string): BlockType
83
+ blockTypes(): readonly BlockType[]
84
+ sourceSchemas(sourceOrType: SourceNode | string): readonly NodeSchema[]
85
+ blockSchemas(blockOrType: SourceNode | string): readonly NodeSchema[]
86
+ query(id: string): QuerySchema | undefined
87
+ queries(): readonly QuerySchema[]
88
+ prop(name: string): DataProp | undefined
89
+ props(node?: SourceNode): readonly DataProp[]
90
+ has(node: SourceNode, prop: string): boolean
91
+ value(node: SourceNode, prop: string): unknown
92
+ }>