@konitif/workbench 0.285.6 → 0.285.8

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/README.md CHANGED
@@ -45,6 +45,10 @@ when that base is still the current head, and archive, restore and tombstone
45
45
  transitions are explicit. The repository contract is storage-neutral; browser,
46
46
  remote, tenant and product-admission policies remain host adapters.
47
47
 
48
+ Workbench ships the generic browser adapter for that repository contract. A
49
+ host selects its storage key and keeps product migration, metadata admission
50
+ and domain specialization outside the adapter.
51
+
48
52
  ## Quick start
49
53
 
50
54
  ```ts
@@ -60,4 +60,5 @@ export declare function transitionWorkbenchExperienceLifecycle<TMetadata>(input:
60
60
  experience: WorkbenchExperience<TMetadata>;
61
61
  expected: Exclude<ExperienceLifecycleStatus, 'tombstoned'>;
62
62
  next: ExperienceLifecycleStatus;
63
+ metadata?: TMetadata;
63
64
  }): ExperienceLifecycleResult<WorkbenchExperience<TMetadata>>;
@@ -83,7 +83,11 @@ export function transitionWorkbenchExperienceLifecycle(input) {
83
83
  if (!isAllowedTransition(input.experience.lifecycle, input.next)) {
84
84
  return refused('experience.lifecycle.status-conflict', '$.next', `Experience lifecycle cannot transition from ${input.experience.lifecycle} to ${input.next}.`, undefined, input.next);
85
85
  }
86
- return accepted({ ...input.experience, lifecycle: input.next });
86
+ return accepted({
87
+ ...input.experience,
88
+ lifecycle: input.next,
89
+ metadata: input.metadata ?? input.experience.metadata,
90
+ });
87
91
  }
88
92
  function isAllowedTransition(current, next) {
89
93
  return next === 'tombstoned'
@@ -31,11 +31,13 @@ export interface WorkbenchExperienceRepository<TMetadata = unknown, TSpecializat
31
31
  draft: WorkbenchExperienceDraft<TSpecialization>;
32
32
  expectedHeadRevisionId: ExperienceRevisionIdentity;
33
33
  revisionId: ExperienceRevisionIdentity;
34
+ derivedFrom?: ExperienceRevisionIdentity | null;
34
35
  metadata?: TMetadata;
35
36
  }): Promise<WorkbenchExperienceRepositoryResult<WorkbenchExperienceLifecycleValue<TMetadata, TSpecialization>>>;
36
37
  transitionLifecycle(input: {
37
38
  experienceId: ExperienceIdentity;
38
39
  expected: Exclude<ExperienceLifecycleStatus, 'tombstoned'>;
39
40
  next: ExperienceLifecycleStatus;
41
+ metadata?: TMetadata;
40
42
  }): Promise<WorkbenchExperienceRepositoryResult<WorkbenchExperience<TMetadata>>>;
41
43
  }
package/dist/index.d.ts CHANGED
@@ -114,6 +114,7 @@ export * from "./domain/workspace/validation.js";
114
114
  export * from "./infrastructure/audio/WorkbenchAudioManager.js";
115
115
  export * from "./infrastructure/backend/HttpWorkbenchBackendClient.js";
116
116
  export * from "./infrastructure/persistence/LocalWorkspacePersistence.js";
117
+ export * from "./infrastructure/persistence/BrowserExperienceRepository.js";
117
118
  export * from "./infrastructure/surfaces/InMemorySurfaceViewerRegistry.js";
118
119
  export * from "./infrastructure/temporal/InMemoryTemporalDialectRegistry.js";
119
120
  export * from "./infrastructure/tools/InMemoryToolRegistry.js";
package/dist/index.js CHANGED
@@ -114,6 +114,7 @@ export * from "./domain/workspace/validation.js";
114
114
  export * from "./infrastructure/audio/WorkbenchAudioManager.js";
115
115
  export * from "./infrastructure/backend/HttpWorkbenchBackendClient.js";
116
116
  export * from "./infrastructure/persistence/LocalWorkspacePersistence.js";
117
+ export * from "./infrastructure/persistence/BrowserExperienceRepository.js";
117
118
  export * from "./infrastructure/surfaces/InMemorySurfaceViewerRegistry.js";
118
119
  export * from "./infrastructure/temporal/InMemoryTemporalDialectRegistry.js";
119
120
  export * from "./infrastructure/tools/InMemoryToolRegistry.js";
@@ -0,0 +1,17 @@
1
+ import type { WorkbenchExperienceRepository, WorkbenchExperienceRepositoryResult, WorkbenchExperienceRepositorySnapshot } from "../../application/experience/experienceRepository.js";
2
+ export interface BrowserExperienceRepositoryStorage {
3
+ getItem(key: string): string | null;
4
+ setItem(key: string, value: string): void;
5
+ }
6
+ export interface BrowserWorkbenchExperienceRepository<TMetadata = unknown, TSpecialization = unknown> extends WorkbenchExperienceRepository<TMetadata, TSpecialization> {
7
+ initialize(snapshot: WorkbenchExperienceRepositorySnapshot<TMetadata, TSpecialization>): Promise<WorkbenchExperienceRepositoryResult<WorkbenchExperienceRepositorySnapshot<TMetadata, TSpecialization>>>;
8
+ }
9
+ /**
10
+ * Browser persistence adapter for the generic Workbench Experience authority.
11
+ * Products choose the storage key and keep migration and specialization policy
12
+ * outside this adapter.
13
+ */
14
+ export declare function createBrowserWorkbenchExperienceRepository<TMetadata = unknown, TSpecialization = unknown>(input: {
15
+ storageKey: string;
16
+ storage?: BrowserExperienceRepositoryStorage | null;
17
+ }): BrowserWorkbenchExperienceRepository<TMetadata, TSpecialization>;
@@ -0,0 +1,263 @@
1
+ import { WORKBENCH_EXPERIENCE_SCHEMA, WORKBENCH_EXPERIENCE_SCHEMA_VERSION, } from "../../application/experience/experienceArtifacts.js";
2
+ import { saveWorkbenchExperienceRevision, transitionWorkbenchExperienceLifecycle, } from "../../application/experience/experienceLifecycle.js";
3
+ /**
4
+ * Browser persistence adapter for the generic Workbench Experience authority.
5
+ * Products choose the storage key and keep migration and specialization policy
6
+ * outside this adapter.
7
+ */
8
+ export function createBrowserWorkbenchExperienceRepository(input) {
9
+ const storage = input.storage === undefined ? resolveBrowserStorage() : input.storage;
10
+ const storageKey = requireStorageKey(input.storageKey);
11
+ return {
12
+ hydrate: () => hydrate(storage, storageKey),
13
+ async initialize(snapshot) {
14
+ const current = await hydrate(storage, storageKey);
15
+ if (!current.ok)
16
+ return current;
17
+ if (current.value.experiences.length > 0 || current.value.revisions.length > 0) {
18
+ return refused('experience.repository.already-exists', 'Browser Experience repository is already initialized.');
19
+ }
20
+ if (!isRepositorySnapshot(snapshot)) {
21
+ return refused('experience.repository.invalid-artifact', 'Initial Experience snapshot does not match the Workbench contract.');
22
+ }
23
+ return persist(storage, storageKey, snapshot);
24
+ },
25
+ async list() {
26
+ const snapshot = await hydrate(storage, storageKey);
27
+ return snapshot.ok ? accepted(snapshot.value.experiences) : snapshot;
28
+ },
29
+ async resolveRevision(reference) {
30
+ const snapshot = await hydrate(storage, storageKey);
31
+ if (!snapshot.ok)
32
+ return snapshot;
33
+ const revision = findRevision(snapshot.value, reference);
34
+ return revision
35
+ ? accepted(revision)
36
+ : refused('experience.repository.not-found', `Experience revision ${reference.revisionId.value} was not found.`);
37
+ },
38
+ async create(value) {
39
+ const snapshot = await hydrate(storage, storageKey);
40
+ if (!snapshot.ok)
41
+ return snapshot;
42
+ if (snapshot.value.experiences.some(candidate => candidate.id.value === value.experience.id.value)) {
43
+ return refused('experience.repository.already-exists', `Experience ${value.experience.id.value} already exists.`);
44
+ }
45
+ if (snapshot.value.revisions.some(candidate => candidate.id.value === value.revision.id.value)) {
46
+ return refused('experience.repository.already-exists', `Experience revision ${value.revision.id.value} already exists.`);
47
+ }
48
+ const persisted = persist(storage, storageKey, {
49
+ experiences: [...snapshot.value.experiences, value.experience],
50
+ revisions: [...snapshot.value.revisions, value.revision],
51
+ });
52
+ return persisted.ok ? accepted(value) : persisted;
53
+ },
54
+ async saveRevision(value) {
55
+ const snapshot = await hydrate(storage, storageKey);
56
+ if (!snapshot.ok)
57
+ return snapshot;
58
+ const experience = snapshot.value.experiences.find(candidate => candidate.id.value === value.draft.experienceId.value);
59
+ if (!experience) {
60
+ return refused('experience.repository.not-found', `Experience ${value.draft.experienceId.value} was not found.`);
61
+ }
62
+ if (snapshot.value.revisions.some(candidate => candidate.id.value === value.revisionId.value)) {
63
+ return refused('experience.repository.already-exists', `Experience revision ${value.revisionId.value} already exists.`);
64
+ }
65
+ const saved = saveWorkbenchExperienceRevision({
66
+ experience,
67
+ draft: value.draft,
68
+ expectedHeadRevisionId: value.expectedHeadRevisionId,
69
+ revisionId: value.revisionId,
70
+ derivedFrom: value.derivedFrom,
71
+ metadata: value.metadata,
72
+ });
73
+ if (!saved.ok)
74
+ return lifecycleRefused(saved.diagnostics[0]);
75
+ const persisted = persist(storage, storageKey, {
76
+ experiences: snapshot.value.experiences.map(candidate => candidate.id.value === saved.value.experience.id.value ? saved.value.experience : candidate),
77
+ revisions: [...snapshot.value.revisions, saved.value.revision],
78
+ });
79
+ return persisted.ok ? accepted(saved.value) : persisted;
80
+ },
81
+ async transitionLifecycle(value) {
82
+ const snapshot = await hydrate(storage, storageKey);
83
+ if (!snapshot.ok)
84
+ return snapshot;
85
+ const experience = snapshot.value.experiences.find(candidate => candidate.id.value === value.experienceId.value);
86
+ if (!experience) {
87
+ return refused('experience.repository.not-found', `Experience ${value.experienceId.value} was not found.`);
88
+ }
89
+ const transitioned = transitionWorkbenchExperienceLifecycle({
90
+ experience,
91
+ expected: value.expected,
92
+ next: value.next,
93
+ metadata: value.metadata,
94
+ });
95
+ if (!transitioned.ok)
96
+ return lifecycleRefused(transitioned.diagnostics[0]);
97
+ const persisted = persist(storage, storageKey, {
98
+ experiences: snapshot.value.experiences.map(candidate => candidate.id.value === transitioned.value.id.value ? transitioned.value : candidate),
99
+ revisions: snapshot.value.revisions,
100
+ });
101
+ return persisted.ok ? accepted(transitioned.value) : persisted;
102
+ },
103
+ };
104
+ }
105
+ async function hydrate(storage, storageKey) {
106
+ if (!storage)
107
+ return refused('experience.repository.unavailable', 'Browser Experience storage is unavailable.');
108
+ let raw;
109
+ try {
110
+ raw = storage.getItem(storageKey);
111
+ }
112
+ catch {
113
+ return refused('experience.repository.unavailable', 'Browser Experience storage could not be read.');
114
+ }
115
+ if (!raw)
116
+ return accepted({ experiences: [], revisions: [] });
117
+ try {
118
+ const parsed = JSON.parse(raw);
119
+ if (!isStoredRepository(parsed)) {
120
+ return refused('experience.repository.invalid-artifact', 'Stored Experience repository does not match the Workbench contract.');
121
+ }
122
+ return accepted({ experiences: parsed.experiences, revisions: parsed.revisions });
123
+ }
124
+ catch {
125
+ return refused('experience.repository.invalid-artifact', 'Stored Experience repository is not valid JSON.');
126
+ }
127
+ }
128
+ function persist(storage, storageKey, snapshot) {
129
+ if (!storage)
130
+ return refused('experience.repository.unavailable', 'Browser Experience storage is unavailable.');
131
+ const stored = {
132
+ schema: 'konitif.workbench-experience-repository',
133
+ schemaVersion: 1,
134
+ experiences: [...snapshot.experiences],
135
+ revisions: [...snapshot.revisions],
136
+ };
137
+ try {
138
+ storage.setItem(storageKey, JSON.stringify(stored));
139
+ return accepted(snapshot);
140
+ }
141
+ catch {
142
+ return refused('experience.repository.unavailable', 'Browser Experience storage could not be updated.');
143
+ }
144
+ }
145
+ function findRevision(snapshot, reference) {
146
+ return snapshot.revisions.find(candidate => candidate.experienceId.value === reference.experienceId.value
147
+ && candidate.id.value === reference.revisionId.value);
148
+ }
149
+ function isStoredRepository(value) {
150
+ if (!isRecord(value))
151
+ return false;
152
+ if (value.schema !== 'konitif.workbench-experience-repository'
153
+ || value.schemaVersion !== 1
154
+ || !Array.isArray(value.experiences)
155
+ || !Array.isArray(value.revisions))
156
+ return false;
157
+ return isRepositorySnapshot(value);
158
+ }
159
+ function isRepositorySnapshot(value) {
160
+ if (!isRecord(value) || !Array.isArray(value.experiences) || !Array.isArray(value.revisions))
161
+ return false;
162
+ const experiences = value.experiences;
163
+ const revisions = value.revisions;
164
+ if (!experiences.every(isWorkbenchExperience) || !revisions.every(isWorkbenchExperienceRevision))
165
+ return false;
166
+ return experiences.every(experience => revisions.some(revision => isRecord(revision)
167
+ && isIdentity(revision.experienceId, 'experience')
168
+ && isIdentity(revision.id, 'experience-revision')
169
+ && revision.experienceId.value === experience.id.value
170
+ && revision.id.value === experience.headRevisionId.value)) && revisions.every(revision => experiences.some(experience => isRecord(revision)
171
+ && isIdentity(revision.experienceId, 'experience')
172
+ && revision.experienceId.value === experience.id.value));
173
+ }
174
+ function isWorkbenchExperience(value) {
175
+ return isRecord(value)
176
+ && isIdentity(value.id, 'experience')
177
+ && isIdentity(value.headRevisionId, 'experience-revision')
178
+ && isLifecycle(value.lifecycle)
179
+ && Object.hasOwn(value, 'metadata');
180
+ }
181
+ function isWorkbenchExperienceRevision(value) {
182
+ return isRecord(value)
183
+ && value.schema === WORKBENCH_EXPERIENCE_SCHEMA
184
+ && value.schemaVersion === WORKBENCH_EXPERIENCE_SCHEMA_VERSION
185
+ && isIdentity(value.id, 'experience-revision')
186
+ && isIdentity(value.experienceId, 'experience')
187
+ && isNullableIdentity(value.parentRevisionId, 'experience-revision')
188
+ && isNullableIdentity(value.derivedFrom, 'experience-revision')
189
+ && isPresetBinding(value.preset)
190
+ && Object.hasOwn(value, 'specialization');
191
+ }
192
+ function isPresetBinding(value) {
193
+ if (!isRecord(value) || !isContentReference(value.source))
194
+ return false;
195
+ if (value.mode === 'reference')
196
+ return true;
197
+ return value.mode === 'override'
198
+ && isContentReference(value.result)
199
+ && Array.isArray(value.transformations)
200
+ && value.transformations.length > 0
201
+ && value.transformations.every((item, index) => isRecord(item)
202
+ && item.order === index
203
+ && typeof item.kind === 'string'
204
+ && item.kind.trim().length > 0
205
+ && typeof item.summary === 'string'
206
+ && item.summary.trim().length > 0);
207
+ }
208
+ function isContentReference(value) {
209
+ return isRecord(value)
210
+ && value.kind === 'workspace-preset'
211
+ && typeof value.id === 'string'
212
+ && value.id.trim().length > 0
213
+ && typeof value.revision === 'string'
214
+ && value.revision.trim().length > 0
215
+ && typeof value.contentId === 'string'
216
+ && /^[a-f0-9]{64}$/.test(value.contentId);
217
+ }
218
+ function isIdentity(value, kind) {
219
+ return isRecord(value)
220
+ && value.kind === kind
221
+ && typeof value.value === 'string'
222
+ && value.value.trim().length > 0;
223
+ }
224
+ function isNullableIdentity(value, kind) {
225
+ return value === null || isIdentity(value, kind);
226
+ }
227
+ function isLifecycle(value) {
228
+ return value === 'active' || value === 'archived' || value === 'tombstoned';
229
+ }
230
+ function isRecord(value) {
231
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
232
+ }
233
+ function lifecycleRefused(diagnostic) {
234
+ const code = diagnostic?.code === 'experience.lifecycle.head-conflict'
235
+ ? 'experience.repository.head-conflict'
236
+ : 'experience.repository.lifecycle-conflict';
237
+ return refused(code, diagnostic?.message ?? 'Experience lifecycle operation was refused.');
238
+ }
239
+ function accepted(value) {
240
+ return { ok: true, value: freezeDetached(value) };
241
+ }
242
+ function refused(code, message) {
243
+ return { ok: false, error: { code, message } };
244
+ }
245
+ function requireStorageKey(value) {
246
+ const normalized = value.trim();
247
+ if (!normalized)
248
+ throw new TypeError('Browser Experience repository storage key is required.');
249
+ return normalized;
250
+ }
251
+ function resolveBrowserStorage() {
252
+ return typeof localStorage === 'undefined' ? null : localStorage;
253
+ }
254
+ function freezeDetached(value) {
255
+ return deepFreeze(structuredClone(value));
256
+ }
257
+ function deepFreeze(value) {
258
+ if (!value || typeof value !== 'object' || Object.isFrozen(value))
259
+ return value;
260
+ for (const nested of Object.values(value))
261
+ deepFreeze(nested);
262
+ return Object.freeze(value);
263
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@konitif/workbench",
3
3
  "license": "PolyForm-Noncommercial-1.0.0",
4
- "version": "0.285.6",
4
+ "version": "0.285.8",
5
5
  "description": "Workspace composition, docking, registries and hosting contracts for KONITIF.",
6
6
  "repository": {
7
7
  "type": "git",