cursedbelt-core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/dist/client/wsWire.d.ts +49 -0
- package/dist/client/wsWire.js +57 -0
- package/dist/config/appConfig.d.ts +361 -0
- package/dist/config/appConfig.js +308 -0
- package/dist/core/activity/index.d.ts +8 -0
- package/dist/core/activity/index.js +7 -0
- package/dist/core/activity/model.d.ts +115 -0
- package/dist/core/activity/model.js +63 -0
- package/dist/core/analytics/index.d.ts +18 -0
- package/dist/core/analytics/index.js +18 -0
- package/dist/core/analytics/range.d.ts +186 -0
- package/dist/core/analytics/range.js +271 -0
- package/dist/core/analytics/schedule.d.ts +301 -0
- package/dist/core/analytics/schedule.js +372 -0
- package/dist/core/category/index.d.ts +3 -0
- package/dist/core/category/index.js +6 -0
- package/dist/core/category/memoryStore.d.ts +21 -0
- package/dist/core/category/memoryStore.js +97 -0
- package/dist/core/category/tree.d.ts +26 -0
- package/dist/core/category/tree.js +113 -0
- package/dist/core/category/types.d.ts +48 -0
- package/dist/core/category/types.js +8 -0
- package/dist/core/color.d.ts +14 -0
- package/dist/core/color.js +32 -0
- package/dist/core/colorScheme/colorSchemeStore.d.ts +70 -0
- package/dist/core/colorScheme/colorSchemeStore.js +123 -0
- package/dist/core/colorScheme/index.d.ts +7 -0
- package/dist/core/colorScheme/index.js +7 -0
- package/dist/core/domainOwners.d.ts +19 -0
- package/dist/core/domainOwners.js +15 -0
- package/dist/core/download.d.ts +14 -0
- package/dist/core/download.js +37 -0
- package/dist/core/events/ccEventBus.d.ts +44 -0
- package/dist/core/events/ccEventBus.js +70 -0
- package/dist/core/file-tree/fileTreeModel.d.ts +276 -0
- package/dist/core/file-tree/fileTreeModel.js +312 -0
- package/dist/core/folder-tree/actions.d.ts +84 -0
- package/dist/core/folder-tree/actions.js +119 -0
- package/dist/core/folder-tree/index.d.ts +3 -0
- package/dist/core/folder-tree/index.js +20 -0
- package/dist/core/folder-tree/path.d.ts +76 -0
- package/dist/core/folder-tree/path.js +146 -0
- package/dist/core/folder-tree/tree.d.ts +86 -0
- package/dist/core/folder-tree/tree.js +184 -0
- package/dist/core/layout/index.d.ts +1 -0
- package/dist/core/layout/index.js +7 -0
- package/dist/core/master-lock/index.d.ts +14 -0
- package/dist/core/master-lock/index.js +14 -0
- package/dist/core/master-lock/kdf.d.ts +63 -0
- package/dist/core/master-lock/kdf.js +117 -0
- package/dist/core/master-lock/policy.d.ts +32 -0
- package/dist/core/master-lock/policy.js +52 -0
- package/dist/core/master-lock/presence.d.ts +52 -0
- package/dist/core/master-lock/presence.js +61 -0
- package/dist/core/master-lock/wire.d.ts +139 -0
- package/dist/core/master-lock/wire.js +81 -0
- package/dist/core/media/bgRemoval.d.ts +27 -0
- package/dist/core/media/bgRemoval.js +101 -0
- package/dist/core/media/ffmpeg.d.ts +57 -0
- package/dist/core/media/ffmpeg.js +126 -0
- package/dist/core/media/format.d.ts +20 -0
- package/dist/core/media/format.js +54 -0
- package/dist/core/media/index.d.ts +12 -0
- package/dist/core/media/index.js +18 -0
- package/dist/core/media/mediaItem.d.ts +15 -0
- package/dist/core/media/mediaItem.js +34 -0
- package/dist/core/media/renderPlan.d.ts +63 -0
- package/dist/core/media/renderPlan.js +232 -0
- package/dist/core/media/segment.d.ts +79 -0
- package/dist/core/media/segment.js +247 -0
- package/dist/core/media/size.d.ts +1 -0
- package/dist/core/media/size.js +3 -0
- package/dist/core/media/trim.d.ts +21 -0
- package/dist/core/media/trim.js +40 -0
- package/dist/core/media/types.d.ts +148 -0
- package/dist/core/media/types.js +11 -0
- package/dist/core/media/uploadLimits.d.ts +7 -0
- package/dist/core/media/uploadLimits.js +16 -0
- package/dist/core/media/validate.d.ts +9 -0
- package/dist/core/media/validate.js +121 -0
- package/dist/core/media/videoCuts.d.ts +152 -0
- package/dist/core/media/videoCuts.js +284 -0
- package/dist/core/navigation/breadcrumbCompat.d.ts +24 -0
- package/dist/core/navigation/breadcrumbCompat.js +21 -0
- package/dist/core/navigation/coverageTest.d.ts +24 -0
- package/dist/core/navigation/coverageTest.js +48 -0
- package/dist/core/navigation/index.d.ts +6 -0
- package/dist/core/navigation/index.js +10 -0
- package/dist/core/navigation/migrateNavPreferences.d.ts +55 -0
- package/dist/core/navigation/migrateNavPreferences.js +63 -0
- package/dist/core/navigation/navigationModel.d.ts +165 -0
- package/dist/core/navigation/navigationModel.js +14 -0
- package/dist/core/navigation/navigationSelectors.d.ts +63 -0
- package/dist/core/navigation/navigationSelectors.js +167 -0
- package/dist/core/navigation/searchCatalogue.d.ts +23 -0
- package/dist/core/navigation/searchCatalogue.js +76 -0
- package/dist/core/net/reconnectSchedule.d.ts +46 -0
- package/dist/core/net/reconnectSchedule.js +38 -0
- package/dist/core/palette/ShortcutRegistry.d.ts +33 -0
- package/dist/core/palette/ShortcutRegistry.js +52 -0
- package/dist/core/palette/fuzzyRank.d.ts +18 -0
- package/dist/core/palette/fuzzyRank.js +73 -0
- package/dist/core/palette/hotkeys.d.ts +28 -0
- package/dist/core/palette/hotkeys.js +118 -0
- package/dist/core/palette/index.d.ts +3 -0
- package/dist/core/palette/index.js +8 -0
- package/dist/core/schema/projection.d.ts +84 -0
- package/dist/core/schema/projection.js +184 -0
- package/dist/core/session/claims.d.ts +145 -0
- package/dist/core/session/claims.js +157 -0
- package/dist/core/sharing/index.d.ts +5 -0
- package/dist/core/sharing/index.js +4 -0
- package/dist/core/sharing/model.d.ts +195 -0
- package/dist/core/sharing/model.js +150 -0
- package/dist/core/site/siteCopy.d.ts +92 -0
- package/dist/core/site/siteCopy.js +91 -0
- package/dist/core/slots.d.ts +22 -0
- package/dist/core/slots.js +31 -0
- package/dist/core/styles/constructableSheet.d.ts +51 -0
- package/dist/core/styles/constructableSheet.js +80 -0
- package/dist/core/tokens/registry.d.ts +119 -0
- package/dist/core/tokens/registry.js +447 -0
- package/dist/core/uiScale/index.d.ts +8 -0
- package/dist/core/uiScale/index.js +8 -0
- package/dist/core/uiScale/scale.d.ts +36 -0
- package/dist/core/uiScale/scale.js +47 -0
- package/dist/core/uiScale/uiScaleStore.d.ts +47 -0
- package/dist/core/uiScale/uiScaleStore.js +63 -0
- package/dist/core/wire/__fixtures__/rowsMarshalWorker.d.ts +1 -0
- package/dist/core/wire/__fixtures__/rowsMarshalWorker.js +23 -0
- package/dist/core/wire/index.d.ts +17 -0
- package/dist/core/wire/index.js +17 -0
- package/dist/core/wire/ingress.d.ts +44 -0
- package/dist/core/wire/ingress.js +134 -0
- package/dist/core/wire/marshalWorker.d.ts +70 -0
- package/dist/core/wire/marshalWorker.js +273 -0
- package/dist/core/wire/marshalling.d.ts +122 -0
- package/dist/core/wire/marshalling.js +126 -0
- package/dist/core/wire/wireHub.d.ts +80 -0
- package/dist/core/wire/wireHub.js +116 -0
- package/dist/ctgr/base64url.d.ts +8 -0
- package/dist/ctgr/base64url.js +39 -0
- package/dist/ctgr/chunkAccumulator.d.ts +214 -0
- package/dist/ctgr/chunkAccumulator.js +307 -0
- package/dist/ctgr/chunkStore.d.ts +24 -0
- package/dist/ctgr/chunkStore.js +125 -0
- package/dist/ctgr/codec.d.ts +44 -0
- package/dist/ctgr/codec.js +153 -0
- package/dist/ctgr/compress.d.ts +2 -0
- package/dist/ctgr/compress.js +20 -0
- package/dist/ctgr/crc32.d.ts +2 -0
- package/dist/ctgr/crc32.js +23 -0
- package/dist/ctgr/digest.d.ts +1 -0
- package/dist/ctgr/digest.js +15 -0
- package/dist/ctgr/index.d.ts +9 -0
- package/dist/ctgr/index.js +17 -0
- package/dist/ctgr/types.d.ts +64 -0
- package/dist/ctgr/types.js +21 -0
- package/dist/ctgr/v0compat.d.ts +27 -0
- package/dist/ctgr/v0compat.js +128 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +17 -0
- package/dist/shared/errors.d.ts +101 -0
- package/dist/shared/errors.js +99 -0
- package/dist/testing/bytes.d.ts +32 -0
- package/dist/testing/bytes.js +33 -0
- package/dist/testing/domInspect.d.ts +59 -0
- package/dist/testing/domInspect.js +115 -0
- package/dist/testing/fakeFetch.d.ts +23 -0
- package/dist/testing/fakeFetch.js +21 -0
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.js +11 -0
- package/package.json +373 -0
- package/src/client/wsWire.ts +83 -0
- package/src/config/appConfig.spec.ts +158 -0
- package/src/config/appConfig.ts +372 -0
- package/src/core/activity/index.ts +21 -0
- package/src/core/activity/model.ts +160 -0
- package/src/core/analytics/index.ts +18 -0
- package/src/core/analytics/range.spec.ts +280 -0
- package/src/core/analytics/range.ts +383 -0
- package/src/core/analytics/schedule.spec.ts +311 -0
- package/src/core/analytics/schedule.ts +624 -0
- package/src/core/category/category.spec.ts +144 -0
- package/src/core/category/index.ts +19 -0
- package/src/core/category/memoryStore.ts +104 -0
- package/src/core/category/tree.ts +118 -0
- package/src/core/category/types.ts +59 -0
- package/src/core/color.spec.ts +35 -0
- package/src/core/color.ts +35 -0
- package/src/core/colorScheme/colorScheme.spec.ts +127 -0
- package/src/core/colorScheme/colorSchemeStore.ts +184 -0
- package/src/core/colorScheme/index.ts +23 -0
- package/src/core/domainOwners.ts +32 -0
- package/src/core/download.spec.ts +38 -0
- package/src/core/download.ts +35 -0
- package/src/core/events/ccEventBus.spec.ts +90 -0
- package/src/core/events/ccEventBus.ts +120 -0
- package/src/core/file-tree/fileTreeModel.spec.ts +462 -0
- package/src/core/file-tree/fileTreeModel.ts +511 -0
- package/src/core/folder-tree/actions.spec.ts +103 -0
- package/src/core/folder-tree/actions.ts +153 -0
- package/src/core/folder-tree/index.ts +46 -0
- package/src/core/folder-tree/path.spec.ts +113 -0
- package/src/core/folder-tree/path.ts +148 -0
- package/src/core/folder-tree/tree.spec.ts +157 -0
- package/src/core/folder-tree/tree.ts +244 -0
- package/src/core/layout/index.ts +7 -0
- package/src/core/master-lock/index.ts +50 -0
- package/src/core/master-lock/kdf.spec.ts +95 -0
- package/src/core/master-lock/kdf.ts +145 -0
- package/src/core/master-lock/policy.ts +55 -0
- package/src/core/master-lock/presence.spec.ts +73 -0
- package/src/core/master-lock/presence.ts +71 -0
- package/src/core/master-lock/wire.ts +155 -0
- package/src/core/media/bgRemoval.spec.ts +120 -0
- package/src/core/media/bgRemoval.ts +123 -0
- package/src/core/media/ffmpeg.spec.ts +135 -0
- package/src/core/media/ffmpeg.ts +162 -0
- package/src/core/media/format.spec.ts +41 -0
- package/src/core/media/format.ts +58 -0
- package/src/core/media/index.ts +105 -0
- package/src/core/media/mediaItem.spec.ts +46 -0
- package/src/core/media/mediaItem.ts +40 -0
- package/src/core/media/renderPlan.spec.ts +140 -0
- package/src/core/media/renderPlan.ts +318 -0
- package/src/core/media/segment.spec.ts +159 -0
- package/src/core/media/segment.ts +352 -0
- package/src/core/media/size.spec.ts +19 -0
- package/src/core/media/size.ts +3 -0
- package/src/core/media/trim.spec.ts +75 -0
- package/src/core/media/trim.ts +60 -0
- package/src/core/media/types.ts +147 -0
- package/src/core/media/uploadLimits.ts +18 -0
- package/src/core/media/validate.spec.ts +106 -0
- package/src/core/media/validate.ts +131 -0
- package/src/core/media/videoCuts.spec.ts +327 -0
- package/src/core/media/videoCuts.ts +355 -0
- package/src/core/navigation/breadcrumbCompat.ts +44 -0
- package/src/core/navigation/coverageTest.ts +54 -0
- package/src/core/navigation/index.ts +50 -0
- package/src/core/navigation/migrateNavPreferences.ts +119 -0
- package/src/core/navigation/navigation.spec.ts +388 -0
- package/src/core/navigation/navigationModel.ts +185 -0
- package/src/core/navigation/navigationSelectors.ts +206 -0
- package/src/core/navigation/searchCatalogue.ts +89 -0
- package/src/core/net/reconnectSchedule.spec.ts +49 -0
- package/src/core/net/reconnectSchedule.ts +78 -0
- package/src/core/palette/ShortcutRegistry.ts +69 -0
- package/src/core/palette/fuzzyRank.ts +78 -0
- package/src/core/palette/hotkeys.ts +132 -0
- package/src/core/palette/index.ts +22 -0
- package/src/core/palette/palette.spec.ts +160 -0
- package/src/core/schema/projection.spec.ts +122 -0
- package/src/core/schema/projection.ts +225 -0
- package/src/core/session/claims.ts +181 -0
- package/src/core/sharing/index.ts +33 -0
- package/src/core/sharing/model.spec.ts +104 -0
- package/src/core/sharing/model.ts +296 -0
- package/src/core/site/siteCopy.ts +115 -0
- package/src/core/slots.ts +38 -0
- package/src/core/styles/constructableSheet.ts +86 -0
- package/src/core/tokens/registry.spec.ts +160 -0
- package/src/core/tokens/registry.ts +536 -0
- package/src/core/uiScale/index.ts +32 -0
- package/src/core/uiScale/scale.ts +63 -0
- package/src/core/uiScale/uiScale.spec.ts +77 -0
- package/src/core/uiScale/uiScaleStore.ts +100 -0
- package/src/core/wire/__fixtures__/rowsMarshalWorker.ts +24 -0
- package/src/core/wire/index.ts +56 -0
- package/src/core/wire/ingress.spec.ts +276 -0
- package/src/core/wire/ingress.ts +182 -0
- package/src/core/wire/marshalWorker.spec.ts +196 -0
- package/src/core/wire/marshalWorker.ts +361 -0
- package/src/core/wire/marshalling.spec.ts +139 -0
- package/src/core/wire/marshalling.ts +238 -0
- package/src/core/wire/wireHub.spec.ts +137 -0
- package/src/core/wire/wireHub.ts +213 -0
- package/src/ctgr/base64url.ts +44 -0
- package/src/ctgr/chunkAccumulator.ts +403 -0
- package/src/ctgr/chunkStore.spec.ts +101 -0
- package/src/ctgr/chunkStore.ts +168 -0
- package/src/ctgr/codec.spec.ts +208 -0
- package/src/ctgr/codec.ts +202 -0
- package/src/ctgr/compress.ts +21 -0
- package/src/ctgr/crc32.ts +23 -0
- package/src/ctgr/digest.ts +14 -0
- package/src/ctgr/index.ts +39 -0
- package/src/ctgr/types.ts +80 -0
- package/src/ctgr/v0compat.spec.ts +73 -0
- package/src/ctgr/v0compat.ts +156 -0
- package/src/declaredDepsAreImported.spec.ts +87 -0
- package/src/index.ts +25 -0
- package/src/leafSubpathsImportNothing.spec.ts +287 -0
- package/src/lockfileIsTracked.spec.ts +50 -0
- package/src/namedSubpathsResolve.spec.ts +122 -0
- package/src/noPathDeps.spec.ts +24 -0
- package/src/publishShape.spec.ts +89 -0
- package/src/shared/errors.ts +153 -0
- package/src/skippedTestsAreAnswered.spec.ts +221 -0
- package/src/testTmpRoot.spec.ts +102 -0
- package/src/testing/bytes.ts +38 -0
- package/src/testing/domInspect.spec.ts +79 -0
- package/src/testing/domInspect.ts +127 -0
- package/src/testing/fakeFetch.ts +26 -0
- package/src/testing/index.ts +11 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `app-config.json` — the single schema the compiler AND the runtime import
|
|
3
|
+
* (02-MASTER-ENGINES §2). Nothing else may re-declare this shape: a second copy
|
|
4
|
+
* is how `privateMedia` without `guard` parsed as valid for a year (J2-1).
|
|
5
|
+
*
|
|
6
|
+
* Two laws are encoded here rather than in prose:
|
|
7
|
+
*
|
|
8
|
+
* 1. `.strict()` end to end. An unknown key — including inside a per-engine
|
|
9
|
+
* `features` block — is a parse ERROR (`unknown-config-key`), never silently
|
|
10
|
+
* stripped. A typo'd flag that gets dropped reads as "the feature is off" at
|
|
11
|
+
* runtime, which is indistinguishable from a bug in the feature.
|
|
12
|
+
* 2. `.superRefine()` cross-engine dependencies. `privateMedia` needs `guard`,
|
|
13
|
+
* `oralHistory` needs `asset`, `auth.enabled` needs `guard`, and the saas
|
|
14
|
+
* profile needs `guard` + `rbac:true`. These are structural: the config
|
|
15
|
+
* references something that will not exist at runtime, so it dies at V0 —
|
|
16
|
+
* fail-closed on structure (04 §0).
|
|
17
|
+
*
|
|
18
|
+
* Zod v4 notes: `z.record` requires an explicit key schema, and `addIssue` takes
|
|
19
|
+
* the literal `'custom'` code (the `ZodIssueCode` enum is gone).
|
|
20
|
+
*/
|
|
21
|
+
import { z } from 'zod';
|
|
22
|
+
export const ENGINE_IDS = ['asset', 'knowledge', 'dev', 'guard', 'system'];
|
|
23
|
+
export const EngineIdSchema = z.enum(ENGINE_IDS);
|
|
24
|
+
/* ---------- per-engine feature schemas (STRICT: unknown key = V0 error) ---------- */
|
|
25
|
+
export const AssetFeatures = z
|
|
26
|
+
.object({
|
|
27
|
+
gallery: z.boolean().default(true),
|
|
28
|
+
galleryLayouts: z.array(z.enum(['masonry', 'rows', 'columns', 'dense'])).min(1).default(['rows']),
|
|
29
|
+
video: z.boolean().default(true), // false ⇒ video ports REMOVED (fail-closed, V2)
|
|
30
|
+
hls: z.boolean().default(true), // false ⇒ ports stay, progressive resolver forced (V2 degrade)
|
|
31
|
+
editor: z.boolean().default(false), // lazy chunk; joins bundleKey
|
|
32
|
+
fileBrowser: z.boolean().default(false),
|
|
33
|
+
taggedFiles: z.boolean().default(false),
|
|
34
|
+
privateMedia: z.boolean().default(false), // requires guard engine — enforced in superRefine
|
|
35
|
+
vaultSource: z.boolean().default(false),
|
|
36
|
+
ocr: z.boolean().default(false),
|
|
37
|
+
imageStudio: z.boolean().default(false),
|
|
38
|
+
})
|
|
39
|
+
.strict();
|
|
40
|
+
export const KnowledgeFeatures = z
|
|
41
|
+
.object({
|
|
42
|
+
notes: z.boolean().default(true),
|
|
43
|
+
todos: z.boolean().default(true),
|
|
44
|
+
airtableStyleLists: z.boolean().default(false),
|
|
45
|
+
kanban: z.boolean().default(false),
|
|
46
|
+
contacts: z.boolean().default(false),
|
|
47
|
+
oralHistory: z.boolean().default(false), // requires asset engine — enforced in superRefine
|
|
48
|
+
textCompare: z.boolean().default(false),
|
|
49
|
+
ragChat: z.boolean().default(false),
|
|
50
|
+
ragDiskIndexing: z.boolean().default(false),
|
|
51
|
+
aiOrganizer: z.boolean().default(false),
|
|
52
|
+
bookmarks: z.boolean().default(false),
|
|
53
|
+
worklog: z.boolean().default(false),
|
|
54
|
+
family: z.boolean().default(false),
|
|
55
|
+
habits: z.boolean().default(false),
|
|
56
|
+
articles: z.boolean().default(false),
|
|
57
|
+
})
|
|
58
|
+
.strict();
|
|
59
|
+
export const DevFeatures = z
|
|
60
|
+
.object({
|
|
61
|
+
appRegistry: z.boolean().default(true),
|
|
62
|
+
configEditor: z.boolean().default(false), // PRIVILEGED: requires 'infra:write' claim (02 §4.4)
|
|
63
|
+
deploy: z.boolean().default(false),
|
|
64
|
+
fleet: z.boolean().default(false),
|
|
65
|
+
pipelines: z.boolean().default(false),
|
|
66
|
+
docs: z.boolean().default(true),
|
|
67
|
+
screenshots: z.boolean().default(false),
|
|
68
|
+
diskUsage: z.boolean().default(false),
|
|
69
|
+
dbAdmin: z.boolean().default(false), // PRIVILEGED: requires 'infra:write' claim (02 §4.4)
|
|
70
|
+
orchestration: z.boolean().default(false),
|
|
71
|
+
})
|
|
72
|
+
.strict();
|
|
73
|
+
/**
|
|
74
|
+
* Who may create an account (D33 — the commercial-launch gate). Ordered from
|
|
75
|
+
* most open to most closed; `effectiveSignupMode` picks the STRICTEST configured
|
|
76
|
+
* value, so two guard instances can never disagree in the open direction.
|
|
77
|
+
*
|
|
78
|
+
* - `open` — anyone signs up (saas: minting their own tenant as OWNER).
|
|
79
|
+
* - `invite` — a valid invitation token is required; token-less signup is a
|
|
80
|
+
* 403. This is the "coming soon" launch posture: the operator invites
|
|
81
|
+
* trusted testers while the public sees a closed door.
|
|
82
|
+
* - `closed` — no signups at all, invitation or not.
|
|
83
|
+
*/
|
|
84
|
+
export const SIGNUP_MODES = ['open', 'invite', 'closed'];
|
|
85
|
+
export const GuardFeatures = z
|
|
86
|
+
.object({
|
|
87
|
+
privateMode: z.boolean().default(true),
|
|
88
|
+
secretsPanel: z.boolean().default(false), // station-style credential management UI
|
|
89
|
+
accounts: z.boolean().default(true), // username/password change (ex-Admin)
|
|
90
|
+
rbac: z.boolean().default(false), // SaaS profile forces true — superRefine (e)
|
|
91
|
+
sessionTtlMinutes: z.number().int().min(1).max(720).default(30),
|
|
92
|
+
signupMode: z.enum(SIGNUP_MODES).default('open'), // D33 launch gate — see SIGNUP_MODES
|
|
93
|
+
// D36: db-stored marketing-copy overrides + the owner-only editor panel.
|
|
94
|
+
// OFF by default: an app that is not THE commercial deployment has no
|
|
95
|
+
// marketing page to feed, and a public copy endpoint on it would be noise.
|
|
96
|
+
siteCopy: z.boolean().default(false),
|
|
97
|
+
})
|
|
98
|
+
.strict();
|
|
99
|
+
export const SystemFeatures = z
|
|
100
|
+
.object({
|
|
101
|
+
dashboard: z.boolean().default(false),
|
|
102
|
+
calendar: z.boolean().default(false),
|
|
103
|
+
notifications: z.boolean().default(true),
|
|
104
|
+
stopwatch: z.boolean().default(false),
|
|
105
|
+
timeOff: z.boolean().default(false),
|
|
106
|
+
formatLab: z.boolean().default(false),
|
|
107
|
+
lineFilter: z.boolean().default(false),
|
|
108
|
+
httpClient: z.boolean().default(false),
|
|
109
|
+
budgets: z.boolean().default(false),
|
|
110
|
+
subscriptions: z.boolean().default(false),
|
|
111
|
+
})
|
|
112
|
+
.strict();
|
|
113
|
+
export const FeaturesByEngine = {
|
|
114
|
+
asset: AssetFeatures,
|
|
115
|
+
knowledge: KnowledgeFeatures,
|
|
116
|
+
dev: DevFeatures,
|
|
117
|
+
guard: GuardFeatures,
|
|
118
|
+
system: SystemFeatures,
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* The signup gate a whole app enforces (D33). The STRICTEST configured value
|
|
122
|
+
* wins across guard instances — with two instances, `open` beside `invite`
|
|
123
|
+
* must mean invite-only, or adding a second guard instance would silently
|
|
124
|
+
* reopen a closed door. Apps with no guard instance have no signup surface at
|
|
125
|
+
* all, so the returned default is never enforced anywhere.
|
|
126
|
+
*/
|
|
127
|
+
export const effectiveSignupMode = (instances) => {
|
|
128
|
+
let strictest = 'open';
|
|
129
|
+
for (const instance of instances) {
|
|
130
|
+
if (instance.engine !== 'guard')
|
|
131
|
+
continue;
|
|
132
|
+
const raw = instance.effective.signupMode;
|
|
133
|
+
const mode = raw === 'invite' || raw === 'closed' ? raw : 'open';
|
|
134
|
+
if (SIGNUP_MODES.indexOf(mode) > SIGNUP_MODES.indexOf(strictest))
|
|
135
|
+
strictest = mode;
|
|
136
|
+
}
|
|
137
|
+
return strictest;
|
|
138
|
+
};
|
|
139
|
+
/* ---------- instances, wires, routes, fills ---------- */
|
|
140
|
+
/**
|
|
141
|
+
* Client SPA path (03 §3): kebab-case literal segments, camelCase `:params`.
|
|
142
|
+
* `/chat/:conversationId` is the canonical example, so param names must admit
|
|
143
|
+
* capitals while literal segments stay lowercase — a `/Chat` vs `/chat` pair is
|
|
144
|
+
* two routes on a case-sensitive router and one route on a case-insensitive one.
|
|
145
|
+
*/
|
|
146
|
+
export const ROUTE_PATH_RE = /^\/(?:[a-z0-9-]+|:[a-zA-Z][a-zA-Z0-9]*)?(?:\/(?:[a-z0-9-]+|:[a-zA-Z][a-zA-Z0-9]*))*$/;
|
|
147
|
+
export const RouteDeclSchema = z
|
|
148
|
+
.object({
|
|
149
|
+
path: z.string().regex(ROUTE_PATH_RE),
|
|
150
|
+
view: z.string().min(1), // engine view id; checked against the export index at V1
|
|
151
|
+
aliases: z.array(z.string()).default([]),
|
|
152
|
+
deepLinkEntity: z.string().optional(), // token id whose payloads resolve here (03 §4)
|
|
153
|
+
})
|
|
154
|
+
.strict();
|
|
155
|
+
export const FillDeclSchema = z
|
|
156
|
+
.object({
|
|
157
|
+
slot: z.string().min(1), // must exist in the SlotRegistry — V1 `slot-unknown`
|
|
158
|
+
component: z.string().min(1), // exported fill id of this instance's engine
|
|
159
|
+
order: z.number().int().default(0),
|
|
160
|
+
requiredFeatures: z.array(z.string()).default([]), // J2-3: pure boolean lookup, no predicates
|
|
161
|
+
})
|
|
162
|
+
.strict();
|
|
163
|
+
export const InstanceSchema = z
|
|
164
|
+
.object({
|
|
165
|
+
engine: EngineIdSchema,
|
|
166
|
+
id: z.string().regex(/^[a-z][a-z0-9-]*$/), // unique per app — V1 `duplicate-instance-id`
|
|
167
|
+
title: z.string().optional(),
|
|
168
|
+
features: z.record(z.string(), z.unknown()).default({}), // validated per-engine in superRefine
|
|
169
|
+
options: z.record(z.string(), z.unknown()).default({}),
|
|
170
|
+
routes: z.array(RouteDeclSchema).default([]),
|
|
171
|
+
fills: z.array(FillDeclSchema).default([]),
|
|
172
|
+
cssIsolation: z.enum(['none', 'scope', 'shadow']).default('none'), // 02 §5.2
|
|
173
|
+
})
|
|
174
|
+
.strict();
|
|
175
|
+
/** '<instanceId>:<portId>' or 'data:<sourceId>'. */
|
|
176
|
+
export const ENDPOINT_RE = /^[a-z][a-z0-9-]*:[a-z0-9\-:]+$/;
|
|
177
|
+
export const ConnectionSchema = z
|
|
178
|
+
.object({
|
|
179
|
+
from: z.string().regex(ENDPOINT_RE),
|
|
180
|
+
to: z.string().regex(ENDPOINT_RE),
|
|
181
|
+
cyclic: z.boolean().default(false),
|
|
182
|
+
})
|
|
183
|
+
.strict();
|
|
184
|
+
/* ---------- the app config ---------- */
|
|
185
|
+
export const AppConfigSchema = z
|
|
186
|
+
.object({
|
|
187
|
+
configVersion: z.literal(1), // unknown version ⇒ V0 `config-version-unknown`
|
|
188
|
+
app: z
|
|
189
|
+
.object({
|
|
190
|
+
id: z.string().regex(/^[a-z0-9-]+$/),
|
|
191
|
+
title: z.string().min(1),
|
|
192
|
+
port: z.number().int().min(1024).max(65535).optional(), // registry precedence (01 §3.2)
|
|
193
|
+
kind: z.enum(['app', 'test', 'demo']).default('app'),
|
|
194
|
+
})
|
|
195
|
+
.strict(),
|
|
196
|
+
profile: z.enum(['local', 'saas']).default('local'),
|
|
197
|
+
deployTarget: z
|
|
198
|
+
.object({
|
|
199
|
+
// context-aware capability branching input (04 V2 / J4-B)
|
|
200
|
+
capabilities: z.record(z.string(), z.boolean()).default({}), // e.g. { ffmpeg: true, mps: true }
|
|
201
|
+
})
|
|
202
|
+
.strict()
|
|
203
|
+
.default({ capabilities: {} }),
|
|
204
|
+
engines: z.array(EngineIdSchema).min(1), // ACTIVE MASTER ENGINE ARRAY — drives server mixins
|
|
205
|
+
instances: z.array(InstanceSchema).min(1),
|
|
206
|
+
connections: z.array(ConnectionSchema).default([]),
|
|
207
|
+
auth: z
|
|
208
|
+
.object({
|
|
209
|
+
enabled: z.boolean().default(false),
|
|
210
|
+
authUI: z
|
|
211
|
+
.object({
|
|
212
|
+
showLogout: z.boolean().default(true),
|
|
213
|
+
placement: z.enum(['header-right', 'header-left', 'sidebar-footer']).default('header-right'),
|
|
214
|
+
icon: z.string().default('LogOut'),
|
|
215
|
+
variant: z.enum(['ghost', 'outline', 'default']).default('ghost'),
|
|
216
|
+
})
|
|
217
|
+
.strict()
|
|
218
|
+
.default({ showLogout: true, placement: 'header-right', icon: 'LogOut', variant: 'ghost' }),
|
|
219
|
+
})
|
|
220
|
+
.strict()
|
|
221
|
+
.default({
|
|
222
|
+
enabled: false,
|
|
223
|
+
authUI: { showLogout: true, placement: 'header-right', icon: 'LogOut', variant: 'ghost' },
|
|
224
|
+
}),
|
|
225
|
+
theme: z
|
|
226
|
+
.object({
|
|
227
|
+
preset: z.string().default('cc-default'),
|
|
228
|
+
/**
|
|
229
|
+
* CSS custom-property map (02 §5.1), emitted into the generated app's
|
|
230
|
+
* `client/styles.css` as a `:root` block by `emitStylesCss`.
|
|
231
|
+
*
|
|
232
|
+
* **Both halves are validated because both are written verbatim into a
|
|
233
|
+
* stylesheet.** A property name is restricted to `--` plus
|
|
234
|
+
* word/dash characters, and a value may not contain the characters that
|
|
235
|
+
* would end the declaration or the rule (`;` `{` `}`) or open a comment.
|
|
236
|
+
* Without that, an override value of `red; } body { background: url(…)`
|
|
237
|
+
* escapes its own declaration and appends arbitrary rules to the app's
|
|
238
|
+
* first-party stylesheet — CSS is not inert (it fetches URLs and can
|
|
239
|
+
* hide or overlay UI). This is `.strict()`-doctrine fail-closed on
|
|
240
|
+
* STRUCTURE: an unusable custom property is a malformed config, not a
|
|
241
|
+
* capability to degrade.
|
|
242
|
+
*/
|
|
243
|
+
overrides: z
|
|
244
|
+
.record(z
|
|
245
|
+
.string()
|
|
246
|
+
.regex(/^--[A-Za-z0-9_-]+$/, 'a theme override key must be a CSS custom property, e.g. "--accent"'), z
|
|
247
|
+
.string()
|
|
248
|
+
.min(1)
|
|
249
|
+
.refine((value) => !/[;{}]|\/\*|\*\//.test(value), {
|
|
250
|
+
message: 'a theme override value may not contain ";", "{", "}" or a CSS comment',
|
|
251
|
+
}))
|
|
252
|
+
.default({}),
|
|
253
|
+
})
|
|
254
|
+
.strict()
|
|
255
|
+
.default({ preset: 'cc-default', overrides: {} }),
|
|
256
|
+
ignoreExistingData: z.boolean().default(false), // the ONLY registry bypass; always logs
|
|
257
|
+
})
|
|
258
|
+
.strict()
|
|
259
|
+
.superRefine((config, ctx) => {
|
|
260
|
+
const engineSet = new Set(config.engines);
|
|
261
|
+
// `ccCode` rides along in params so V0 can map each cross-engine issue onto
|
|
262
|
+
// its diagnostic code without pattern-matching on English message text.
|
|
263
|
+
const issue = (path, message, ccCode = 'engine-dependency-missing') => ctx.addIssue({ code: 'custom', path, message, params: { ccCode } });
|
|
264
|
+
config.instances.forEach((inst, i) => {
|
|
265
|
+
// (a) instance engine must be activated
|
|
266
|
+
if (!engineSet.has(inst.engine))
|
|
267
|
+
issue(['instances', i, 'engine'], `Instance '${inst.id}' uses engine '${inst.engine}' which is absent from engines[].`);
|
|
268
|
+
// (b) features parse against the FULL per-engine schema — typos die here, not at boot
|
|
269
|
+
const parsed = FeaturesByEngine[inst.engine].safeParse(inst.features);
|
|
270
|
+
if (!parsed.success) {
|
|
271
|
+
for (const e of parsed.error.issues) {
|
|
272
|
+
const keys = e.code === 'unrecognized_keys' ? e.keys : [];
|
|
273
|
+
const suffix = keys.length > 0 ? keys.map(String) : e.path.map(String);
|
|
274
|
+
issue(['instances', i, 'features', ...suffix], e.message, e.code === 'unrecognized_keys' ? 'unknown-config-key' : 'config-invalid-value');
|
|
275
|
+
}
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const f = parsed.data;
|
|
279
|
+
// (c) cross-engine dependencies — hard blocks, not comments (J2-1)
|
|
280
|
+
if (inst.engine === 'asset' && f.privateMedia === true && !engineSet.has('guard'))
|
|
281
|
+
issue(['instances', i, 'features', 'privateMedia'], `'privateMedia' requires the 'guard' engine in engines[] (engine-dependency-missing).`);
|
|
282
|
+
if (inst.engine === 'knowledge' && f.oralHistory === true && !engineSet.has('asset'))
|
|
283
|
+
issue(['instances', i, 'features', 'oralHistory'], `'oralHistory' requires the 'asset' engine for its media player tier (engine-dependency-missing).`);
|
|
284
|
+
});
|
|
285
|
+
// (d) auth requires guard
|
|
286
|
+
if (config.auth.enabled && !engineSet.has('guard'))
|
|
287
|
+
issue(['auth', 'enabled'], `auth.enabled requires the 'guard' engine (engine-dependency-missing).`);
|
|
288
|
+
// (e) SaaS guardrails: guard engine present, rbac on
|
|
289
|
+
if (config.profile === 'saas') {
|
|
290
|
+
if (!engineSet.has('guard'))
|
|
291
|
+
issue(['engines'], `profile 'saas' structurally requires the 'guard' engine (engine-dependency-missing).`);
|
|
292
|
+
const guardInst = config.instances.find((x) => x.engine === 'guard');
|
|
293
|
+
if (guardInst && guardInst.features.rbac !== true)
|
|
294
|
+
issue(['instances', config.instances.indexOf(guardInst), 'features', 'rbac'], `profile 'saas' structurally requires 'features.rbac: true' on the guard instance.`);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
/**
|
|
298
|
+
* Resolve one instance's declared features through the FULL (non-partial) engine
|
|
299
|
+
* schema — the "full-resolution preview" of V0.3. Defaults land here, so a
|
|
300
|
+
* missing required option surfaces at design time instead of at first boot.
|
|
301
|
+
*/
|
|
302
|
+
export function resolveInstanceFeatures(engine, declared) {
|
|
303
|
+
return FeaturesByEngine[engine].parse(declared);
|
|
304
|
+
}
|
|
305
|
+
/** The declared feature key set of an engine — used by V1 `unknown-required-feature`. */
|
|
306
|
+
export function featureKeysOf(engine) {
|
|
307
|
+
return Object.keys(FeaturesByEngine[engine].shape);
|
|
308
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cursedbelt-core/activity` — the fleet's activity/audit vocabulary, pure.
|
|
3
|
+
*
|
|
4
|
+
* The storage + query half is `cursedbelt-server/activity`; this entry is what a
|
|
5
|
+
* browser, a spec or a wire type can import without pulling `bun:sqlite` in.
|
|
6
|
+
*/
|
|
7
|
+
export { ACTIVITY_OUTCOMES, activityFromShareEvent, activitySubject, describeActivityEvent, describeActivityResource, isActivityAction, isActivityOutcome, } from "./model";
|
|
8
|
+
export type { ActivityEvent, ActivityEventInput, ActivityOutcome, ActivityResource, } from "./model";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cursedbelt-core/activity` — the fleet's activity/audit vocabulary, pure.
|
|
3
|
+
*
|
|
4
|
+
* The storage + query half is `cursedbelt-server/activity`; this entry is what a
|
|
5
|
+
* browser, a spec or a wire type can import without pulling `bun:sqlite` in.
|
|
6
|
+
*/
|
|
7
|
+
export { ACTIVITY_OUTCOMES, activityFromShareEvent, activitySubject, describeActivityEvent, describeActivityResource, isActivityAction, isActivityOutcome, } from "./model";
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fleet's ACTIVITY vocabulary — who did what, to which record, when, and how
|
|
3
|
+
* it went. One shape, so twelve apps' event streams can be read as one.
|
|
4
|
+
*
|
|
5
|
+
* ── Why this is not a second audit shape ────────────────────────────────────
|
|
6
|
+
* `cursedbelt-core/sharing` already had to answer this question for grants, and its
|
|
7
|
+
* `ShareEvent` is the shape that survived review: an ISO-8601 `at`, an `actor`
|
|
8
|
+
* that is always an account and never a session, a `(app, kind, id)` resource
|
|
9
|
+
* triple, a dotted `action`, and a free `note` for whoever reads it six months
|
|
10
|
+
* later. This vocabulary is deliberately the SAME five fields plus an outcome —
|
|
11
|
+
* `activityFromShareEvent` below is the proof, and it is a total function.
|
|
12
|
+
*
|
|
13
|
+
* So a consumer never has to choose between two audit trails:
|
|
14
|
+
* - a sharing grant is `share.grant` in the stream, projected out of
|
|
15
|
+
* `share_events` by `cursedbelt-server/activity`'s `shareEventsSource` — the
|
|
16
|
+
* same rows, never a copy, so the two can never disagree;
|
|
17
|
+
* - everything else an app wants remembered is `record()`d into
|
|
18
|
+
* `activity_events`.
|
|
19
|
+
*
|
|
20
|
+
* ── The rules the shape enforces ────────────────────────────────────────────
|
|
21
|
+
* - **`at` is an ISO-8601 string**, matching `share_events` and the fleet's
|
|
22
|
+
* audit vocabulary. An audit trail is read by humans far more often than it
|
|
23
|
+
* is sorted by a machine, and a sortable string is both.
|
|
24
|
+
* - **`actor` is required and is never optional.** An event that cannot say who
|
|
25
|
+
* is a log line, not an audit trail. Apps with no session pass a label that
|
|
26
|
+
* says so (`"curt@… (dev-open)"`), which is honest rather than empty.
|
|
27
|
+
* - **`action` is `subject.verb`**, lowercase and dotted — `note.create`,
|
|
28
|
+
* `backup.delete`, `share.grant`. The prefix is what makes "everything that
|
|
29
|
+
* happened to backups" a filter rather than a regex.
|
|
30
|
+
* - **`id` is source-qualified** (`log:41`, `share:12`), because the stream is a
|
|
31
|
+
* UNION over more than one table and two tables' autoincrements collide at 1.
|
|
32
|
+
*/
|
|
33
|
+
import type { ShareEvent } from "../sharing/model";
|
|
34
|
+
/** How it went. `denied` is its own outcome rather than an `error`: a refused
|
|
35
|
+
* action is the single most interesting row in an audit trail, and one that
|
|
36
|
+
* reads as a crash gets triaged as one. */
|
|
37
|
+
export type ActivityOutcome = "ok" | "denied" | "error";
|
|
38
|
+
export declare const ACTIVITY_OUTCOMES: readonly ActivityOutcome[];
|
|
39
|
+
export declare const isActivityOutcome: (value: unknown) => value is ActivityOutcome;
|
|
40
|
+
/**
|
|
41
|
+
* What was acted on: `(app, kind, id)` — deliberately identical to
|
|
42
|
+
* {@link import("../sharing/model").ShareResource}, so a resource can be handed
|
|
43
|
+
* from one primitive to the other without a translation layer.
|
|
44
|
+
*/
|
|
45
|
+
export interface ActivityResource {
|
|
46
|
+
app: string;
|
|
47
|
+
kind: string;
|
|
48
|
+
id: string;
|
|
49
|
+
}
|
|
50
|
+
/** One line of the stream, as read. */
|
|
51
|
+
export interface ActivityEvent {
|
|
52
|
+
/** `"<source>:<row id>"`. Stable, and unique across the union. */
|
|
53
|
+
id: string;
|
|
54
|
+
/** Which table it came from — `log`, `share`, or a source an app registered. */
|
|
55
|
+
source: string;
|
|
56
|
+
/** ISO-8601, UTC. */
|
|
57
|
+
at: string;
|
|
58
|
+
/** The satellite. Present on every row so a fleet-wide merge stays labeled. */
|
|
59
|
+
app: string;
|
|
60
|
+
actor: string;
|
|
61
|
+
/** `subject.verb`, lowercase. */
|
|
62
|
+
action: string;
|
|
63
|
+
/** Null when the action is about no single record (a sign-in, a sweep). */
|
|
64
|
+
resource: ActivityResource | null;
|
|
65
|
+
outcome: ActivityOutcome;
|
|
66
|
+
/** Why, in the words of whoever did it. */
|
|
67
|
+
note: string;
|
|
68
|
+
/** Anything else worth keeping, as JSON. Never indexed, never queried on. */
|
|
69
|
+
data: Record<string, unknown> | null;
|
|
70
|
+
}
|
|
71
|
+
/** One line of the stream, as written. The store supplies `app`, `id` and `source`. */
|
|
72
|
+
export interface ActivityEventInput {
|
|
73
|
+
actor: string;
|
|
74
|
+
action: string;
|
|
75
|
+
/** The `app` half is supplied by the store, so a caller cannot write an event
|
|
76
|
+
* into another app's name. */
|
|
77
|
+
resource?: {
|
|
78
|
+
kind: string;
|
|
79
|
+
id: string;
|
|
80
|
+
} | null;
|
|
81
|
+
outcome?: ActivityOutcome;
|
|
82
|
+
note?: string;
|
|
83
|
+
data?: Record<string, unknown> | null;
|
|
84
|
+
/** ISO-8601. Defaults to now — passed explicitly only by tests and importers. */
|
|
85
|
+
at?: string;
|
|
86
|
+
}
|
|
87
|
+
/** `note.create` → `note`. The half a filter groups on. */
|
|
88
|
+
export declare const activitySubject: (action: string) => string;
|
|
89
|
+
export declare const isActivityAction: (value: unknown) => value is string;
|
|
90
|
+
/** `family · person 3f2` — the resource, for a row that has one. */
|
|
91
|
+
export declare const describeActivityResource: (resource: ActivityResource | null) => string;
|
|
92
|
+
/**
|
|
93
|
+
* One sentence for a person. Deliberately generic — an app that wants its own
|
|
94
|
+
* wording for its own actions renders it itself; this is what the fleet-wide
|
|
95
|
+
* board shows for an action it has never seen before, and it must never be
|
|
96
|
+
* empty.
|
|
97
|
+
*/
|
|
98
|
+
export declare function describeActivityEvent(event: ActivityEvent): string;
|
|
99
|
+
/**
|
|
100
|
+
* 🔴 THE coordination point with `cursedbelt-core/sharing`.
|
|
101
|
+
*
|
|
102
|
+
* A sharing grant is already audited, in `share_events`, by a store that
|
|
103
|
+
* guarantees no grant can be made without one. This maps that row into the
|
|
104
|
+
* activity vocabulary so the fleet's event stream can INCLUDE it rather than
|
|
105
|
+
* ask every consumer to write a second copy — the "coordinate the shape rather
|
|
106
|
+
* than invent a second one" rule, as code.
|
|
107
|
+
*
|
|
108
|
+
* `cursedbelt-server/activity`'s `shareEventsSource` is the SQL form of exactly
|
|
109
|
+
* this mapping, and `store.spec.ts` asserts the two agree field for field. This
|
|
110
|
+
* function is the one to use from a `ShareStore`'s `onEvent` hook when an app
|
|
111
|
+
* genuinely wants a copy (a different database, say); an app whose share events
|
|
112
|
+
* live beside its activity table should register the SQL source instead and
|
|
113
|
+
* keep one row.
|
|
114
|
+
*/
|
|
115
|
+
export declare function activityFromShareEvent(event: ShareEvent): ActivityEventInput;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export const ACTIVITY_OUTCOMES = ["ok", "denied", "error"];
|
|
2
|
+
export const isActivityOutcome = (value) => value === "ok" || value === "denied" || value === "error";
|
|
3
|
+
/** `note.create` → `note`. The half a filter groups on. */
|
|
4
|
+
export const activitySubject = (action) => action.split(".")[0] ?? action;
|
|
5
|
+
/**
|
|
6
|
+
* A well-formed action: `subject.verb`, optionally deeper (`share.group.create`),
|
|
7
|
+
* lowercase, dots and dashes only.
|
|
8
|
+
*
|
|
9
|
+
* Enforced at the WRITE (see the store), not merely documented — an action
|
|
10
|
+
* vocabulary that drifts to `Note Created` on one screen and `note.create` on
|
|
11
|
+
* another cannot be filtered, and a filter is the whole point of the column.
|
|
12
|
+
*/
|
|
13
|
+
const ACTION_RE = /^[a-z0-9-]+(\.[a-z0-9-]+)+$/;
|
|
14
|
+
export const isActivityAction = (value) => typeof value === "string" && value.length <= 64 && ACTION_RE.test(value);
|
|
15
|
+
/** `family · person 3f2` — the resource, for a row that has one. */
|
|
16
|
+
export const describeActivityResource = (resource) => resource ? `${resource.kind} ${resource.id}` : "—";
|
|
17
|
+
/**
|
|
18
|
+
* One sentence for a person. Deliberately generic — an app that wants its own
|
|
19
|
+
* wording for its own actions renders it itself; this is what the fleet-wide
|
|
20
|
+
* board shows for an action it has never seen before, and it must never be
|
|
21
|
+
* empty.
|
|
22
|
+
*/
|
|
23
|
+
export function describeActivityEvent(event) {
|
|
24
|
+
const what = event.resource
|
|
25
|
+
? `${event.resource.kind} ${event.resource.id}`
|
|
26
|
+
: `${event.app}`;
|
|
27
|
+
const verb = event.action.replace(/\./g, " ");
|
|
28
|
+
const how = event.outcome === "ok" ? "" : event.outcome === "denied" ? " — refused" : " — failed";
|
|
29
|
+
const why = event.note ? ` (${event.note})` : "";
|
|
30
|
+
return `${event.actor} ${verb} ${what}${how}${why}`;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 🔴 THE coordination point with `cursedbelt-core/sharing`.
|
|
34
|
+
*
|
|
35
|
+
* A sharing grant is already audited, in `share_events`, by a store that
|
|
36
|
+
* guarantees no grant can be made without one. This maps that row into the
|
|
37
|
+
* activity vocabulary so the fleet's event stream can INCLUDE it rather than
|
|
38
|
+
* ask every consumer to write a second copy — the "coordinate the shape rather
|
|
39
|
+
* than invent a second one" rule, as code.
|
|
40
|
+
*
|
|
41
|
+
* `cursedbelt-server/activity`'s `shareEventsSource` is the SQL form of exactly
|
|
42
|
+
* this mapping, and `store.spec.ts` asserts the two agree field for field. This
|
|
43
|
+
* function is the one to use from a `ShareStore`'s `onEvent` hook when an app
|
|
44
|
+
* genuinely wants a copy (a different database, say); an app whose share events
|
|
45
|
+
* live beside its activity table should register the SQL source instead and
|
|
46
|
+
* keep one row.
|
|
47
|
+
*/
|
|
48
|
+
export function activityFromShareEvent(event) {
|
|
49
|
+
return {
|
|
50
|
+
at: event.at,
|
|
51
|
+
actor: event.actor,
|
|
52
|
+
action: `share.${event.action}`,
|
|
53
|
+
resource: event.resource ? { kind: event.resource.kind, id: event.resource.id } : null,
|
|
54
|
+
outcome: "ok",
|
|
55
|
+
note: event.note,
|
|
56
|
+
data: {
|
|
57
|
+
subjectType: event.subject.type,
|
|
58
|
+
subjectId: event.subject.id,
|
|
59
|
+
level: event.level,
|
|
60
|
+
previousLevel: event.previousLevel,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cursedbelt-core/analytics` — the framework-free half of an analytics surface.
|
|
3
|
+
*
|
|
4
|
+
* Framework-free because BOTH sides need it: a server buckets a series before it
|
|
5
|
+
* ships it, and the browser renders the same buckets. They must agree exactly or
|
|
6
|
+
* a chart's x-axis disagrees with its own tooltip — so this half must not pull
|
|
7
|
+
* React in.
|
|
8
|
+
*
|
|
9
|
+
* - `range` — the time vocabulary (today/week/…/custom), local-day bucketing,
|
|
10
|
+
* and the baseline window every comparison is measured against.
|
|
11
|
+
* - `schedule` — list scheduling with resource exclusion and a contention term:
|
|
12
|
+
* how long a backlog takes to drain, whether another worker helps, and when a
|
|
13
|
+
* budget runs out at each fleet size.
|
|
14
|
+
*
|
|
15
|
+
* The React furniture that renders it lives in `cursedbelt/react/analytics`.
|
|
16
|
+
*/
|
|
17
|
+
export * from './range';
|
|
18
|
+
export * from './schedule';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cursedbelt-core/analytics` — the framework-free half of an analytics surface.
|
|
3
|
+
*
|
|
4
|
+
* Framework-free because BOTH sides need it: a server buckets a series before it
|
|
5
|
+
* ships it, and the browser renders the same buckets. They must agree exactly or
|
|
6
|
+
* a chart's x-axis disagrees with its own tooltip — so this half must not pull
|
|
7
|
+
* React in.
|
|
8
|
+
*
|
|
9
|
+
* - `range` — the time vocabulary (today/week/…/custom), local-day bucketing,
|
|
10
|
+
* and the baseline window every comparison is measured against.
|
|
11
|
+
* - `schedule` — list scheduling with resource exclusion and a contention term:
|
|
12
|
+
* how long a backlog takes to drain, whether another worker helps, and when a
|
|
13
|
+
* budget runs out at each fleet size.
|
|
14
|
+
*
|
|
15
|
+
* The React furniture that renders it lives in `cursedbelt/react/analytics`.
|
|
16
|
+
*/
|
|
17
|
+
export * from './range';
|
|
18
|
+
export * from './schedule';
|