@konitif/workbench 0.285.3 → 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 +14 -0
- package/dist/application/experience/experienceArtifacts.d.ts +79 -0
- package/dist/application/experience/experienceArtifacts.js +109 -0
- package/dist/application/experience/experienceLifecycle.d.ts +62 -0
- package/dist/application/experience/experienceLifecycle.js +115 -0
- package/dist/application/experience/experienceRepository.d.ts +41 -0
- package/dist/application/experience/experienceRepository.js +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/workspace-contracts.d.ts +3 -0
- package/dist/workspace-contracts.js +3 -0
- package/package.json +1 -1
- package/reference/catalog.json +26 -0
package/README.md
CHANGED
|
@@ -17,6 +17,8 @@ npm install @konitif/workbench
|
|
|
17
17
|
- Project preflight and repository-quality contracts.
|
|
18
18
|
- Explicit workspace persistence, synchronization and surface ports.
|
|
19
19
|
- Content-addressed workspace usage bundles with strict host-context admission.
|
|
20
|
+
- Abstract Experience revisions that bind presets by immutable content reference
|
|
21
|
+
or by an explicit, traceable override.
|
|
20
22
|
|
|
21
23
|
## Authority boundary
|
|
22
24
|
|
|
@@ -31,6 +33,18 @@ content-addressed projection of a preset and its workspace, shell and focus
|
|
|
31
33
|
fragments. Admission rejects altered content, incomplete fragments, incompatible
|
|
32
34
|
host contexts and identity/revision conflicts.
|
|
33
35
|
|
|
36
|
+
An Experience is a Workbench authority: it records the stable identity and
|
|
37
|
+
revision lineage of an experimentation arrangement without absorbing the
|
|
38
|
+
domain that specializes it. A product can reference an authored workspace
|
|
39
|
+
preset unchanged or declare an override with ordered transformation records;
|
|
40
|
+
admission always verifies the referenced canonical content.
|
|
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
|
+
|
|
34
48
|
## Quick start
|
|
35
49
|
|
|
36
50
|
```ts
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { WorkspacePresetArtifact } from "../workspace/presetArtifacts.js";
|
|
2
|
+
export declare const WORKBENCH_EXPERIENCE_SCHEMA: "konitif.workbench-experience";
|
|
3
|
+
export declare const WORKBENCH_EXPERIENCE_SCHEMA_VERSION: 1;
|
|
4
|
+
export interface ExperienceIdentity {
|
|
5
|
+
kind: 'experience';
|
|
6
|
+
value: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ExperienceRevisionIdentity {
|
|
9
|
+
kind: 'experience-revision';
|
|
10
|
+
value: string;
|
|
11
|
+
}
|
|
12
|
+
export type ExperienceLifecycleStatus = 'active' | 'archived' | 'tombstoned';
|
|
13
|
+
export interface ExperienceContentAddressPort {
|
|
14
|
+
sha256(canonicalJson: string): Promise<string>;
|
|
15
|
+
}
|
|
16
|
+
export interface WorkspacePresetContentReference {
|
|
17
|
+
kind: 'workspace-preset';
|
|
18
|
+
id: string;
|
|
19
|
+
revision: string;
|
|
20
|
+
contentId: string;
|
|
21
|
+
}
|
|
22
|
+
export interface ExperienceTransformation {
|
|
23
|
+
order: number;
|
|
24
|
+
kind: string;
|
|
25
|
+
summary: string;
|
|
26
|
+
}
|
|
27
|
+
export type WorkspaceExperiencePresetBinding = {
|
|
28
|
+
mode: 'reference';
|
|
29
|
+
source: WorkspacePresetContentReference;
|
|
30
|
+
} | {
|
|
31
|
+
mode: 'override';
|
|
32
|
+
source: WorkspacePresetContentReference;
|
|
33
|
+
result: WorkspacePresetContentReference;
|
|
34
|
+
transformations: readonly ExperienceTransformation[];
|
|
35
|
+
};
|
|
36
|
+
export interface WorkbenchExperienceRevision<TSpecialization = unknown> {
|
|
37
|
+
schema: typeof WORKBENCH_EXPERIENCE_SCHEMA;
|
|
38
|
+
schemaVersion: typeof WORKBENCH_EXPERIENCE_SCHEMA_VERSION;
|
|
39
|
+
id: ExperienceRevisionIdentity;
|
|
40
|
+
experienceId: ExperienceIdentity;
|
|
41
|
+
parentRevisionId: ExperienceRevisionIdentity | null;
|
|
42
|
+
derivedFrom: ExperienceRevisionIdentity | null;
|
|
43
|
+
preset: WorkspaceExperiencePresetBinding;
|
|
44
|
+
specialization: TSpecialization;
|
|
45
|
+
}
|
|
46
|
+
export interface WorkbenchExperience<TMetadata = unknown> {
|
|
47
|
+
id: ExperienceIdentity;
|
|
48
|
+
headRevisionId: ExperienceRevisionIdentity;
|
|
49
|
+
lifecycle: ExperienceLifecycleStatus;
|
|
50
|
+
metadata: TMetadata;
|
|
51
|
+
}
|
|
52
|
+
export type ExperienceArtifactDiagnosticCode = 'experience.invalid-identity' | 'experience.invalid-content-id' | 'experience.preset-identity-conflict' | 'experience.preset-content-conflict' | 'experience.invalid-override';
|
|
53
|
+
export interface ExperienceArtifactDiagnostic {
|
|
54
|
+
code: ExperienceArtifactDiagnosticCode;
|
|
55
|
+
path: string;
|
|
56
|
+
message: string;
|
|
57
|
+
}
|
|
58
|
+
export type ExperienceArtifactResult<T> = {
|
|
59
|
+
ok: true;
|
|
60
|
+
value: T;
|
|
61
|
+
diagnostics: [];
|
|
62
|
+
} | {
|
|
63
|
+
ok: false;
|
|
64
|
+
diagnostics: ExperienceArtifactDiagnostic[];
|
|
65
|
+
};
|
|
66
|
+
export declare function defineExperienceIdentity(value: string): ExperienceIdentity;
|
|
67
|
+
export declare function defineExperienceRevisionIdentity(value: string): ExperienceRevisionIdentity;
|
|
68
|
+
export declare function createWorkspacePresetContentReference(artifact: WorkspacePresetArtifact, content: ExperienceContentAddressPort): Promise<WorkspacePresetContentReference>;
|
|
69
|
+
export declare function admitWorkspacePresetContentReference(reference: WorkspacePresetContentReference, artifact: WorkspacePresetArtifact, content: ExperienceContentAddressPort): Promise<ExperienceArtifactResult<WorkspacePresetContentReference>>;
|
|
70
|
+
export declare function createWorkspaceExperiencePresetBinding(input: {
|
|
71
|
+
source: WorkspacePresetArtifact;
|
|
72
|
+
result?: WorkspacePresetArtifact;
|
|
73
|
+
transformations?: readonly ExperienceTransformation[];
|
|
74
|
+
}, content: ExperienceContentAddressPort): Promise<ExperienceArtifactResult<WorkspaceExperiencePresetBinding>>;
|
|
75
|
+
export declare function admitWorkspaceExperiencePresetBinding(input: {
|
|
76
|
+
binding: WorkspaceExperiencePresetBinding;
|
|
77
|
+
source: WorkspacePresetArtifact;
|
|
78
|
+
result?: WorkspacePresetArtifact;
|
|
79
|
+
}, content: ExperienceContentAddressPort): Promise<ExperienceArtifactResult<WorkspaceExperiencePresetBinding>>;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { serializeWorkspacePresetArtifact } from "../workspace/presetArtifacts.js";
|
|
2
|
+
export const WORKBENCH_EXPERIENCE_SCHEMA = 'konitif.workbench-experience';
|
|
3
|
+
export const WORKBENCH_EXPERIENCE_SCHEMA_VERSION = 1;
|
|
4
|
+
export function defineExperienceIdentity(value) {
|
|
5
|
+
return { kind: 'experience', value: requireIdentity(value, 'Experience') };
|
|
6
|
+
}
|
|
7
|
+
export function defineExperienceRevisionIdentity(value) {
|
|
8
|
+
return { kind: 'experience-revision', value: requireIdentity(value, 'Experience revision') };
|
|
9
|
+
}
|
|
10
|
+
export async function createWorkspacePresetContentReference(artifact, content) {
|
|
11
|
+
const contentId = await content.sha256(serializeWorkspacePresetArtifact(artifact));
|
|
12
|
+
if (!isContentId(contentId)) {
|
|
13
|
+
throw new TypeError('Workspace preset content addressing must return a lowercase SHA-256 digest.');
|
|
14
|
+
}
|
|
15
|
+
return freezeDetached({
|
|
16
|
+
kind: 'workspace-preset',
|
|
17
|
+
id: artifact.id,
|
|
18
|
+
revision: artifact.version,
|
|
19
|
+
contentId
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export async function admitWorkspacePresetContentReference(reference, artifact, content) {
|
|
23
|
+
if (!isContentId(reference.contentId)) {
|
|
24
|
+
return refused('experience.invalid-content-id', '$.contentId', 'Workspace preset content identity must be a lowercase SHA-256 digest.');
|
|
25
|
+
}
|
|
26
|
+
if (reference.kind !== 'workspace-preset' || reference.id !== artifact.id || reference.revision !== artifact.version) {
|
|
27
|
+
return refused('experience.preset-identity-conflict', '$', 'Workspace preset identity and revision must match their immutable reference.');
|
|
28
|
+
}
|
|
29
|
+
const resolved = await createWorkspacePresetContentReference(artifact, content);
|
|
30
|
+
if (resolved.contentId !== reference.contentId) {
|
|
31
|
+
return refused('experience.preset-content-conflict', '$.contentId', 'Workspace preset reference is bound to different canonical content.');
|
|
32
|
+
}
|
|
33
|
+
return accepted(reference);
|
|
34
|
+
}
|
|
35
|
+
export async function createWorkspaceExperiencePresetBinding(input, content) {
|
|
36
|
+
const source = await createWorkspacePresetContentReference(input.source, content);
|
|
37
|
+
if (!input.result) {
|
|
38
|
+
return accepted(freezeDetached({ mode: 'reference', source }));
|
|
39
|
+
}
|
|
40
|
+
const transformations = normalizeTransformations(input.transformations ?? []);
|
|
41
|
+
if (!transformations) {
|
|
42
|
+
return refused('experience.invalid-override', '$.transformations', 'A workspace preset override requires ordered, non-empty transformation records.');
|
|
43
|
+
}
|
|
44
|
+
const result = await createWorkspacePresetContentReference(input.result, content);
|
|
45
|
+
if (source.contentId === result.contentId) {
|
|
46
|
+
return refused('experience.invalid-override', '$.result', 'A workspace preset override must produce content distinct from its source.');
|
|
47
|
+
}
|
|
48
|
+
return accepted(freezeDetached({ mode: 'override', source, result, transformations }));
|
|
49
|
+
}
|
|
50
|
+
export async function admitWorkspaceExperiencePresetBinding(input, content) {
|
|
51
|
+
const source = await admitWorkspacePresetContentReference(input.binding.source, input.source, content);
|
|
52
|
+
if (!source.ok)
|
|
53
|
+
return source;
|
|
54
|
+
if (input.binding.mode === 'reference') {
|
|
55
|
+
if (input.result) {
|
|
56
|
+
return refused('experience.invalid-override', '$.result', 'A reference binding cannot admit override content.');
|
|
57
|
+
}
|
|
58
|
+
return accepted(freezeDetached(input.binding));
|
|
59
|
+
}
|
|
60
|
+
const transformations = normalizeTransformations(input.binding.transformations);
|
|
61
|
+
if (!transformations || !input.result) {
|
|
62
|
+
return refused('experience.invalid-override', '$', 'An override binding requires its resolved result and ordered transformation records.');
|
|
63
|
+
}
|
|
64
|
+
const result = await admitWorkspacePresetContentReference(input.binding.result, input.result, content);
|
|
65
|
+
if (!result.ok)
|
|
66
|
+
return result;
|
|
67
|
+
if (input.binding.source.contentId === input.binding.result.contentId) {
|
|
68
|
+
return refused('experience.invalid-override', '$.result', 'A workspace preset override must produce content distinct from its source.');
|
|
69
|
+
}
|
|
70
|
+
return accepted(freezeDetached({ ...input.binding, transformations }));
|
|
71
|
+
}
|
|
72
|
+
function normalizeTransformations(values) {
|
|
73
|
+
if (values.length === 0)
|
|
74
|
+
return null;
|
|
75
|
+
const normalized = values.map((value, index) => ({
|
|
76
|
+
order: value.order,
|
|
77
|
+
kind: value.kind.trim(),
|
|
78
|
+
summary: value.summary.trim()
|
|
79
|
+
}));
|
|
80
|
+
if (normalized.some((value, index) => value.order !== index || !value.kind || !value.summary)) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
return freezeDetached(normalized);
|
|
84
|
+
}
|
|
85
|
+
function requireIdentity(value, label) {
|
|
86
|
+
const normalized = value.trim();
|
|
87
|
+
if (!normalized)
|
|
88
|
+
throw new TypeError(`${label} identity is required.`);
|
|
89
|
+
return normalized;
|
|
90
|
+
}
|
|
91
|
+
function isContentId(value) {
|
|
92
|
+
return /^[a-f0-9]{64}$/.test(value);
|
|
93
|
+
}
|
|
94
|
+
function accepted(value) {
|
|
95
|
+
return { ok: true, value: freezeDetached(value), diagnostics: [] };
|
|
96
|
+
}
|
|
97
|
+
function refused(code, path, message) {
|
|
98
|
+
return { ok: false, diagnostics: [{ code, path, message }] };
|
|
99
|
+
}
|
|
100
|
+
function freezeDetached(value) {
|
|
101
|
+
return deepFreeze(structuredClone(value));
|
|
102
|
+
}
|
|
103
|
+
function deepFreeze(value) {
|
|
104
|
+
if (!value || typeof value !== 'object' || Object.isFrozen(value))
|
|
105
|
+
return value;
|
|
106
|
+
for (const nested of Object.values(value))
|
|
107
|
+
deepFreeze(nested);
|
|
108
|
+
return Object.freeze(value);
|
|
109
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -55,6 +55,9 @@ export * from "./application/runtime/RuntimeProjectionSession.js";
|
|
|
55
55
|
export * from "./application/runtime/RuntimeProjectionViewerSessionSync.js";
|
|
56
56
|
export * from "./application/experiment/ControlledExperimentRecorder.js";
|
|
57
57
|
export * from "./application/experiment/ControlledExperimentCampaign.js";
|
|
58
|
+
export * from "./application/experience/experienceArtifacts.js";
|
|
59
|
+
export * from "./application/experience/experienceLifecycle.js";
|
|
60
|
+
export * from "./application/experience/experienceRepository.js";
|
|
58
61
|
export * from "./application/repository/RepositoryPreflightEngine.js";
|
|
59
62
|
export * from "./domain/layout/model.js";
|
|
60
63
|
export * from "./domain/layout/interaction.js";
|
package/dist/index.js
CHANGED
|
@@ -55,6 +55,9 @@ export * from "./application/runtime/RuntimeProjectionSession.js";
|
|
|
55
55
|
export * from "./application/runtime/RuntimeProjectionViewerSessionSync.js";
|
|
56
56
|
export * from "./application/experiment/ControlledExperimentRecorder.js";
|
|
57
57
|
export * from "./application/experiment/ControlledExperimentCampaign.js";
|
|
58
|
+
export * from "./application/experience/experienceArtifacts.js";
|
|
59
|
+
export * from "./application/experience/experienceLifecycle.js";
|
|
60
|
+
export * from "./application/experience/experienceRepository.js";
|
|
58
61
|
export * from "./application/repository/RepositoryPreflightEngine.js";
|
|
59
62
|
export * from "./domain/layout/model.js";
|
|
60
63
|
export * from "./domain/layout/interaction.js";
|
|
@@ -39,6 +39,9 @@ export * from "./application/workspace/presetSelection.js";
|
|
|
39
39
|
export * from "./application/workspace/usageSnapshots.js";
|
|
40
40
|
export * from "./application/launch/workbenchLaunchPolicy.js";
|
|
41
41
|
export * from "./application/tool/experienceToolSelection.js";
|
|
42
|
+
export * from "./application/experience/experienceArtifacts.js";
|
|
43
|
+
export * from "./application/experience/experienceLifecycle.js";
|
|
44
|
+
export * from "./application/experience/experienceRepository.js";
|
|
42
45
|
export * from "./application/workspace/unassignTool.js";
|
|
43
46
|
export * from "./application/workspace/parkedTool.js";
|
|
44
47
|
export * from "./application/workspace/workspacePresetCatalogAdmission.js";
|
|
@@ -31,6 +31,9 @@ export * from "./application/workspace/presetSelection.js";
|
|
|
31
31
|
export * from "./application/workspace/usageSnapshots.js";
|
|
32
32
|
export * from "./application/launch/workbenchLaunchPolicy.js";
|
|
33
33
|
export * from "./application/tool/experienceToolSelection.js";
|
|
34
|
+
export * from "./application/experience/experienceArtifacts.js";
|
|
35
|
+
export * from "./application/experience/experienceLifecycle.js";
|
|
36
|
+
export * from "./application/experience/experienceRepository.js";
|
|
34
37
|
export * from "./application/workspace/unassignTool.js";
|
|
35
38
|
export * from "./application/workspace/parkedTool.js";
|
|
36
39
|
export * from "./application/workspace/workspacePresetCatalogAdmission.js";
|
package/package.json
CHANGED
package/reference/catalog.json
CHANGED
|
@@ -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",
|