@konitif/workbench 0.285.4 → 0.285.5

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
@@ -39,6 +39,12 @@ domain that specializes it. A product can reference an authored workspace
39
39
  preset unchanged or declare an override with ordered transformation records;
40
40
  admission always verifies the referenced canonical content.
41
41
 
42
+ Workbench also owns the generic Experience lifecycle: an immutable revision
43
+ opens into a draft bound to an explicit base, saving creates a new revision only
44
+ when that base is still the current head, and archive, restore and tombstone
45
+ transitions are explicit. The repository contract is storage-neutral; browser,
46
+ remote, tenant and product-admission policies remain host adapters.
47
+
42
48
  ## Quick start
43
49
 
44
50
  ```ts
@@ -0,0 +1,62 @@
1
+ import { type ExperienceIdentity, type ExperienceLifecycleStatus, type ExperienceRevisionIdentity, type WorkbenchExperience, type WorkbenchExperienceRevision, type WorkspaceExperiencePresetBinding } from "./experienceArtifacts.js";
2
+ export interface ExperienceDraftIdentity {
3
+ kind: 'experience-draft';
4
+ value: string;
5
+ }
6
+ export interface WorkbenchExperienceDraft<TSpecialization = unknown> {
7
+ id: ExperienceDraftIdentity;
8
+ experienceId: ExperienceIdentity;
9
+ baseRevisionId: ExperienceRevisionIdentity;
10
+ preset: WorkspaceExperiencePresetBinding;
11
+ specialization: TSpecialization;
12
+ }
13
+ export interface WorkbenchExperienceLifecycleValue<TMetadata = unknown, TSpecialization = unknown> {
14
+ experience: WorkbenchExperience<TMetadata>;
15
+ revision: WorkbenchExperienceRevision<TSpecialization>;
16
+ }
17
+ export type ExperienceLifecycleDiagnosticCode = 'experience.lifecycle.experience-mismatch' | 'experience.lifecycle.head-conflict' | 'experience.lifecycle.inactive' | 'experience.lifecycle.status-conflict';
18
+ export interface ExperienceLifecycleDiagnostic {
19
+ code: ExperienceLifecycleDiagnosticCode;
20
+ path: string;
21
+ message: string;
22
+ expected?: string;
23
+ actual?: string;
24
+ }
25
+ export type ExperienceLifecycleResult<T> = {
26
+ ok: true;
27
+ value: T;
28
+ diagnostics: [];
29
+ } | {
30
+ ok: false;
31
+ diagnostics: ExperienceLifecycleDiagnostic[];
32
+ };
33
+ export declare function defineExperienceDraftIdentity(value: string): ExperienceDraftIdentity;
34
+ export declare function createWorkbenchExperience<TMetadata, TSpecialization>(input: {
35
+ experienceId: ExperienceIdentity;
36
+ revisionId: ExperienceRevisionIdentity;
37
+ metadata: TMetadata;
38
+ preset: WorkspaceExperiencePresetBinding;
39
+ specialization: TSpecialization;
40
+ derivedFrom?: ExperienceRevisionIdentity | null;
41
+ }): WorkbenchExperienceLifecycleValue<TMetadata, TSpecialization>;
42
+ export declare function createWorkbenchExperienceDraft<TSpecialization>(input: {
43
+ draftId: ExperienceDraftIdentity;
44
+ revision: WorkbenchExperienceRevision<TSpecialization>;
45
+ }): WorkbenchExperienceDraft<TSpecialization>;
46
+ export declare function updateWorkbenchExperienceDraft<TSpecialization>(input: {
47
+ draft: WorkbenchExperienceDraft<TSpecialization>;
48
+ preset: WorkspaceExperiencePresetBinding;
49
+ specialization: TSpecialization;
50
+ }): WorkbenchExperienceDraft<TSpecialization>;
51
+ export declare function saveWorkbenchExperienceRevision<TMetadata, TSpecialization>(input: {
52
+ experience: WorkbenchExperience<TMetadata>;
53
+ draft: WorkbenchExperienceDraft<TSpecialization>;
54
+ expectedHeadRevisionId: ExperienceRevisionIdentity;
55
+ revisionId: ExperienceRevisionIdentity;
56
+ metadata?: TMetadata;
57
+ }): ExperienceLifecycleResult<WorkbenchExperienceLifecycleValue<TMetadata, TSpecialization>>;
58
+ export declare function transitionWorkbenchExperienceLifecycle<TMetadata>(input: {
59
+ experience: WorkbenchExperience<TMetadata>;
60
+ expected: Exclude<ExperienceLifecycleStatus, 'tombstoned'>;
61
+ next: ExperienceLifecycleStatus;
62
+ }): ExperienceLifecycleResult<WorkbenchExperience<TMetadata>>;
@@ -0,0 +1,115 @@
1
+ import { defineExperienceIdentity, defineExperienceRevisionIdentity, } from "./experienceArtifacts.js";
2
+ export function defineExperienceDraftIdentity(value) {
3
+ return { kind: 'experience-draft', value: requireIdentity(value, 'Experience draft') };
4
+ }
5
+ export function createWorkbenchExperience(input) {
6
+ return freezeDetached({
7
+ experience: {
8
+ id: defineExperienceIdentity(input.experienceId.value),
9
+ headRevisionId: defineExperienceRevisionIdentity(input.revisionId.value),
10
+ lifecycle: 'active',
11
+ metadata: input.metadata,
12
+ },
13
+ revision: {
14
+ schema: 'konitif.workbench-experience',
15
+ schemaVersion: 1,
16
+ id: defineExperienceRevisionIdentity(input.revisionId.value),
17
+ experienceId: defineExperienceIdentity(input.experienceId.value),
18
+ parentRevisionId: null,
19
+ derivedFrom: input.derivedFrom ?? null,
20
+ preset: input.preset,
21
+ specialization: input.specialization,
22
+ },
23
+ });
24
+ }
25
+ export function createWorkbenchExperienceDraft(input) {
26
+ return freezeDetached({
27
+ id: defineExperienceDraftIdentity(input.draftId.value),
28
+ experienceId: input.revision.experienceId,
29
+ baseRevisionId: input.revision.id,
30
+ preset: input.revision.preset,
31
+ specialization: input.revision.specialization,
32
+ });
33
+ }
34
+ export function updateWorkbenchExperienceDraft(input) {
35
+ return freezeDetached({
36
+ ...input.draft,
37
+ preset: input.preset,
38
+ specialization: input.specialization,
39
+ });
40
+ }
41
+ export function saveWorkbenchExperienceRevision(input) {
42
+ if (input.experience.lifecycle !== 'active') {
43
+ return refused('experience.lifecycle.inactive', '$.experience.lifecycle', 'Only an active Experience can receive a new revision.', 'active', input.experience.lifecycle);
44
+ }
45
+ if (input.draft.experienceId.value !== input.experience.id.value) {
46
+ return refused('experience.lifecycle.experience-mismatch', '$.draft.experienceId', 'The draft and Experience identities do not match.', input.experience.id.value, input.draft.experienceId.value);
47
+ }
48
+ const actualHead = input.experience.headRevisionId.value;
49
+ if (input.expectedHeadRevisionId.value !== actualHead
50
+ || input.draft.baseRevisionId.value !== actualHead) {
51
+ return refused('experience.lifecycle.head-conflict', '$.expectedHeadRevisionId', 'The draft base is no longer the expected Experience head.', input.expectedHeadRevisionId.value, actualHead);
52
+ }
53
+ const revisionId = defineExperienceRevisionIdentity(input.revisionId.value);
54
+ return accepted({
55
+ experience: {
56
+ ...input.experience,
57
+ headRevisionId: revisionId,
58
+ metadata: input.metadata ?? input.experience.metadata,
59
+ },
60
+ revision: {
61
+ schema: 'konitif.workbench-experience',
62
+ schemaVersion: 1,
63
+ id: revisionId,
64
+ experienceId: input.experience.id,
65
+ parentRevisionId: input.draft.baseRevisionId,
66
+ derivedFrom: null,
67
+ preset: input.draft.preset,
68
+ specialization: input.draft.specialization,
69
+ },
70
+ });
71
+ }
72
+ export function transitionWorkbenchExperienceLifecycle(input) {
73
+ if (input.experience.lifecycle === 'tombstoned') {
74
+ return refused('experience.lifecycle.inactive', '$.experience.lifecycle', 'A tombstoned Experience cannot transition again.', input.expected, input.experience.lifecycle);
75
+ }
76
+ if (input.experience.lifecycle === input.next)
77
+ return accepted(input.experience);
78
+ if (input.experience.lifecycle !== input.expected) {
79
+ return refused('experience.lifecycle.status-conflict', '$.expected', 'The Experience lifecycle no longer matches the expected status.', input.expected, input.experience.lifecycle);
80
+ }
81
+ if (!isAllowedTransition(input.experience.lifecycle, input.next)) {
82
+ return refused('experience.lifecycle.status-conflict', '$.next', `Experience lifecycle cannot transition from ${input.experience.lifecycle} to ${input.next}.`, undefined, input.next);
83
+ }
84
+ return accepted({ ...input.experience, lifecycle: input.next });
85
+ }
86
+ function isAllowedTransition(current, next) {
87
+ return next === 'tombstoned'
88
+ || (current === 'active' && next === 'archived')
89
+ || (current === 'archived' && next === 'active');
90
+ }
91
+ function requireIdentity(value, label) {
92
+ const normalized = value.trim();
93
+ if (!normalized)
94
+ throw new TypeError(`${label} identity is required.`);
95
+ return normalized;
96
+ }
97
+ function accepted(value) {
98
+ return { ok: true, value: freezeDetached(value), diagnostics: [] };
99
+ }
100
+ function refused(code, path, message, expected, actual) {
101
+ return {
102
+ ok: false,
103
+ diagnostics: [{ code, path, message, ...(expected ? { expected } : {}), ...(actual ? { actual } : {}) }],
104
+ };
105
+ }
106
+ function freezeDetached(value) {
107
+ return deepFreeze(structuredClone(value));
108
+ }
109
+ function deepFreeze(value) {
110
+ if (!value || typeof value !== 'object' || Object.isFrozen(value))
111
+ return value;
112
+ for (const nested of Object.values(value))
113
+ deepFreeze(nested);
114
+ return Object.freeze(value);
115
+ }
@@ -0,0 +1,41 @@
1
+ import type { ExperienceIdentity, ExperienceLifecycleStatus, ExperienceRevisionIdentity, WorkbenchExperience, WorkbenchExperienceRevision } from "./experienceArtifacts.js";
2
+ import type { ExperienceLifecycleDiagnostic, WorkbenchExperienceDraft, WorkbenchExperienceLifecycleValue } from "./experienceLifecycle.js";
3
+ export interface ExperienceRevisionReference {
4
+ experienceId: ExperienceIdentity;
5
+ revisionId: ExperienceRevisionIdentity;
6
+ }
7
+ export interface WorkbenchExperienceRepositorySnapshot<TMetadata = unknown, TSpecialization = unknown> {
8
+ experiences: readonly WorkbenchExperience<TMetadata>[];
9
+ revisions: readonly WorkbenchExperienceRevision<TSpecialization>[];
10
+ }
11
+ export type WorkbenchExperienceRepositoryErrorCode = 'experience.repository.not-found' | 'experience.repository.already-exists' | 'experience.repository.head-conflict' | 'experience.repository.lifecycle-conflict' | 'experience.repository.invalid-artifact' | 'experience.repository.unavailable';
12
+ export interface WorkbenchExperienceRepositoryError {
13
+ code: WorkbenchExperienceRepositoryErrorCode;
14
+ message: string;
15
+ lifecycleDiagnostic?: ExperienceLifecycleDiagnostic;
16
+ }
17
+ export type WorkbenchExperienceRepositoryResult<T> = {
18
+ ok: true;
19
+ value: T;
20
+ } | {
21
+ ok: false;
22
+ error: WorkbenchExperienceRepositoryError;
23
+ };
24
+ export interface WorkbenchExperienceRepository<TMetadata = unknown, TSpecialization = unknown> {
25
+ hydrate(): Promise<WorkbenchExperienceRepositoryResult<WorkbenchExperienceRepositorySnapshot<TMetadata, TSpecialization>>>;
26
+ list(): Promise<WorkbenchExperienceRepositoryResult<readonly WorkbenchExperience<TMetadata>[]>>;
27
+ resolveRevision(reference: ExperienceRevisionReference): Promise<WorkbenchExperienceRepositoryResult<WorkbenchExperienceRevision<TSpecialization>>>;
28
+ create(value: WorkbenchExperienceLifecycleValue<TMetadata, TSpecialization>): Promise<WorkbenchExperienceRepositoryResult<WorkbenchExperienceLifecycleValue<TMetadata, TSpecialization>>>;
29
+ saveRevision(input: {
30
+ experience: WorkbenchExperience<TMetadata>;
31
+ draft: WorkbenchExperienceDraft<TSpecialization>;
32
+ expectedHeadRevisionId: ExperienceRevisionIdentity;
33
+ revisionId: ExperienceRevisionIdentity;
34
+ metadata?: TMetadata;
35
+ }): Promise<WorkbenchExperienceRepositoryResult<WorkbenchExperienceLifecycleValue<TMetadata, TSpecialization>>>;
36
+ transitionLifecycle(input: {
37
+ experienceId: ExperienceIdentity;
38
+ expected: Exclude<ExperienceLifecycleStatus, 'tombstoned'>;
39
+ next: ExperienceLifecycleStatus;
40
+ }): Promise<WorkbenchExperienceRepositoryResult<WorkbenchExperience<TMetadata>>>;
41
+ }
package/dist/index.d.ts CHANGED
@@ -56,6 +56,8 @@ export * from "./application/runtime/RuntimeProjectionViewerSessionSync.js";
56
56
  export * from "./application/experiment/ControlledExperimentRecorder.js";
57
57
  export * from "./application/experiment/ControlledExperimentCampaign.js";
58
58
  export * from "./application/experience/experienceArtifacts.js";
59
+ export * from "./application/experience/experienceLifecycle.js";
60
+ export * from "./application/experience/experienceRepository.js";
59
61
  export * from "./application/repository/RepositoryPreflightEngine.js";
60
62
  export * from "./domain/layout/model.js";
61
63
  export * from "./domain/layout/interaction.js";
package/dist/index.js CHANGED
@@ -56,6 +56,8 @@ export * from "./application/runtime/RuntimeProjectionViewerSessionSync.js";
56
56
  export * from "./application/experiment/ControlledExperimentRecorder.js";
57
57
  export * from "./application/experiment/ControlledExperimentCampaign.js";
58
58
  export * from "./application/experience/experienceArtifacts.js";
59
+ export * from "./application/experience/experienceLifecycle.js";
60
+ export * from "./application/experience/experienceRepository.js";
59
61
  export * from "./application/repository/RepositoryPreflightEngine.js";
60
62
  export * from "./domain/layout/model.js";
61
63
  export * from "./domain/layout/interaction.js";
@@ -40,6 +40,8 @@ export * from "./application/workspace/usageSnapshots.js";
40
40
  export * from "./application/launch/workbenchLaunchPolicy.js";
41
41
  export * from "./application/tool/experienceToolSelection.js";
42
42
  export * from "./application/experience/experienceArtifacts.js";
43
+ export * from "./application/experience/experienceLifecycle.js";
44
+ export * from "./application/experience/experienceRepository.js";
43
45
  export * from "./application/workspace/unassignTool.js";
44
46
  export * from "./application/workspace/parkedTool.js";
45
47
  export * from "./application/workspace/workspacePresetCatalogAdmission.js";
@@ -32,6 +32,8 @@ export * from "./application/workspace/usageSnapshots.js";
32
32
  export * from "./application/launch/workbenchLaunchPolicy.js";
33
33
  export * from "./application/tool/experienceToolSelection.js";
34
34
  export * from "./application/experience/experienceArtifacts.js";
35
+ export * from "./application/experience/experienceLifecycle.js";
36
+ export * from "./application/experience/experienceRepository.js";
35
37
  export * from "./application/workspace/unassignTool.js";
36
38
  export * from "./application/workspace/parkedTool.js";
37
39
  export * from "./application/workspace/workspacePresetCatalogAdmission.js";
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.4",
4
+ "version": "0.285.5",
5
5
  "description": "Workspace composition, docking, registries and hosting contracts for KONITIF.",
6
6
  "repository": {
7
7
  "type": "git",
@@ -218,6 +218,32 @@
218
218
  "application/shell/"
219
219
  ]
220
220
  },
221
+ {
222
+ "id": "experience-application",
223
+ "title": "Experience lifecycle",
224
+ "summary": "Content-bound Experience revisions, drafts, lifecycle transitions, conflicts, and storage-neutral repository ports.",
225
+ "authority": "Reusable Experience identity, revision lineage, lifecycle and persistence boundary.",
226
+ "owns": [
227
+ "Experience drafts",
228
+ "head conflict detection",
229
+ "archive restore and tombstone transitions",
230
+ "repository ports"
231
+ ],
232
+ "doesNotOwn": [
233
+ "product specialization",
234
+ "browser or remote storage",
235
+ "tenant authorization"
236
+ ],
237
+ "workflow": [
238
+ "Draft",
239
+ "Save",
240
+ "Resolve",
241
+ "Transition"
242
+ ],
243
+ "sourcePrefixes": [
244
+ "application/experience/"
245
+ ]
246
+ },
221
247
  {
222
248
  "id": "runtime-application",
223
249
  "title": "Runtime application",