@owlmeans/server-planning 0.1.18-rc.2 → 0.1.18-rc.21
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 +40 -12
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/server-planning/SKILL.md +193 -18
- package/build/actions/cards.d.ts +3 -5
- package/build/actions/cards.d.ts.map +1 -1
- package/build/actions/cards.js +1 -1
- package/build/actions/cards.js.map +1 -1
- package/build/actions/execute.d.ts +24 -3
- package/build/actions/execute.d.ts.map +1 -1
- package/build/actions/execute.js +82 -8
- package/build/actions/execute.js.map +1 -1
- package/build/actions/schemas.d.ts +27 -5
- package/build/actions/schemas.d.ts.map +1 -1
- package/build/actions/schemas.js +67 -7
- package/build/actions/schemas.js.map +1 -1
- package/build/actions/specs.d.ts +2 -4
- package/build/actions/specs.d.ts.map +1 -1
- package/build/actions/specs.js.map +1 -1
- package/build/actions/types.d.ts +4 -0
- package/build/actions/types.d.ts.map +1 -0
- package/build/actions/types.js +2 -0
- package/build/actions/types.js.map +1 -0
- package/build/actions/watch.d.ts.map +1 -1
- package/build/actions/watch.js +4 -2
- package/build/actions/watch.js.map +1 -1
- package/build/conformance/assert.d.ts +20 -0
- package/build/conformance/assert.d.ts.map +1 -0
- package/build/conformance/assert.js +54 -0
- package/build/conformance/assert.js.map +1 -0
- package/build/conformance/cases.d.ts +7 -0
- package/build/conformance/cases.d.ts.map +1 -0
- package/build/conformance/cases.js +447 -0
- package/build/conformance/cases.js.map +1 -0
- package/build/conformance/consts.d.ts +19 -0
- package/build/conformance/consts.d.ts.map +1 -0
- package/build/conformance/consts.js +42 -0
- package/build/conformance/consts.js.map +1 -0
- package/build/conformance/fixtures.d.ts +14 -0
- package/build/conformance/fixtures.d.ts.map +1 -0
- package/build/conformance/fixtures.js +109 -0
- package/build/conformance/fixtures.js.map +1 -0
- package/build/conformance/index.d.ts +12 -0
- package/build/conformance/index.d.ts.map +1 -0
- package/build/conformance/index.js +11 -0
- package/build/conformance/index.js.map +1 -0
- package/build/conformance/types.d.ts +18 -0
- package/build/conformance/types.d.ts.map +1 -0
- package/build/conformance/types.js +2 -0
- package/build/conformance/types.js.map +1 -0
- package/build/consts.d.ts +5 -0
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +5 -0
- package/build/consts.js.map +1 -1
- package/build/definitions.d.ts +17 -0
- package/build/definitions.d.ts.map +1 -0
- package/build/definitions.js +211 -0
- package/build/definitions.js.map +1 -0
- package/build/executor/changes.d.ts +2 -2
- package/build/executor/changes.d.ts.map +1 -1
- package/build/executor/changes.js.map +1 -1
- package/build/executor/code.d.ts +3 -3
- package/build/executor/code.d.ts.map +1 -1
- package/build/executor/code.js +1 -1
- package/build/executor/code.js.map +1 -1
- package/build/executor/consts.local.d.ts +5 -0
- package/build/executor/consts.local.d.ts.map +1 -0
- package/build/executor/consts.local.js +6 -0
- package/build/executor/consts.local.js.map +1 -0
- package/build/executor/creator.d.ts +30 -0
- package/build/executor/creator.d.ts.map +1 -0
- package/build/executor/creator.js +46 -0
- package/build/executor/creator.js.map +1 -0
- package/build/executor/resolve.d.ts +31 -23
- package/build/executor/resolve.d.ts.map +1 -1
- package/build/executor/resolve.js +74 -15
- package/build/executor/resolve.js.map +1 -1
- package/build/executor/types.d.ts +24 -0
- package/build/executor/types.d.ts.map +1 -0
- package/build/executor/types.js +2 -0
- package/build/executor/types.js.map +1 -0
- package/build/executor/validate.d.ts +6 -5
- package/build/executor/validate.d.ts.map +1 -1
- package/build/executor/validate.js +18 -15
- package/build/executor/validate.js.map +1 -1
- package/build/executor.d.ts +10 -7
- package/build/executor.d.ts.map +1 -1
- package/build/executor.js +17 -14
- package/build/executor.js.map +1 -1
- package/build/facade.d.ts +20 -2
- package/build/facade.d.ts.map +1 -1
- package/build/facade.js +128 -18
- package/build/facade.js.map +1 -1
- package/build/helper.d.ts +3 -2
- package/build/helper.d.ts.map +1 -1
- package/build/helper.js +7 -3
- package/build/helper.js.map +1 -1
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/projection.d.ts +2 -10
- package/build/projection.d.ts.map +1 -1
- package/build/projection.js +1 -1
- package/build/projection.js.map +1 -1
- package/build/queue/consts.d.ts +5 -0
- package/build/queue/consts.d.ts.map +1 -0
- package/build/queue/consts.js +5 -0
- package/build/queue/consts.js.map +1 -0
- package/build/queue/index.d.ts +4 -16
- package/build/queue/index.d.ts.map +1 -1
- package/build/queue/index.js +2 -4
- package/build/queue/index.js.map +1 -1
- package/build/queue/types.d.ts +13 -0
- package/build/queue/types.d.ts.map +1 -0
- package/build/queue/types.js +2 -0
- package/build/queue/types.js.map +1 -0
- package/build/registry.d.ts +2 -21
- package/build/registry.d.ts.map +1 -1
- package/build/registry.js +3 -1
- package/build/registry.js.map +1 -1
- package/build/service.d.ts +3 -16
- package/build/service.d.ts.map +1 -1
- package/build/service.js +9 -0
- package/build/service.js.map +1 -1
- package/build/store/commits.d.ts.map +1 -1
- package/build/store/commits.js +3 -1
- package/build/store/commits.js.map +1 -1
- package/build/store/composite.d.ts +1 -0
- package/build/store/composite.d.ts.map +1 -1
- package/build/store/composite.js +3 -0
- package/build/store/composite.js.map +1 -1
- package/build/store/consts.local.d.ts +2 -0
- package/build/store/consts.local.d.ts.map +1 -0
- package/build/store/consts.local.js +2 -0
- package/build/store/consts.local.js.map +1 -0
- package/build/store/fold.d.ts +4 -3
- package/build/store/fold.d.ts.map +1 -1
- package/build/store/fold.js +14 -6
- package/build/store/fold.js.map +1 -1
- package/build/store/memory.d.ts +12 -0
- package/build/store/memory.d.ts.map +1 -1
- package/build/store/memory.js +0 -0
- package/build/store/memory.js.map +1 -1
- package/build/store/types.d.ts +15 -1
- package/build/store/types.d.ts.map +1 -1
- package/build/store/types.local.d.ts +3 -0
- package/build/store/types.local.d.ts.map +1 -0
- package/build/store/types.local.js +2 -0
- package/build/store/types.local.js.map +1 -0
- package/build/types.d.ts +111 -1
- package/build/types.d.ts.map +1 -1
- package/build/types.local.d.ts +7 -0
- package/build/types.local.d.ts.map +1 -0
- package/build/types.local.js +2 -0
- package/build/types.local.js.map +1 -0
- package/build/utils/index.d.ts +1 -0
- package/build/utils/index.d.ts.map +1 -1
- package/build/utils/scope.d.ts +39 -2
- package/build/utils/scope.d.ts.map +1 -1
- package/build/utils/scope.js +79 -15
- package/build/utils/scope.js.map +1 -1
- package/build/utils/types.d.ts +8 -0
- package/build/utils/types.d.ts.map +1 -0
- package/build/utils/types.js +2 -0
- package/build/utils/types.js.map +1 -0
- package/package.json +28 -20
- package/src/actions/cards.ts +2 -6
- package/src/actions/execute.ts +95 -12
- package/src/actions/schemas.ts +81 -9
- package/src/actions/specs.ts +1 -2
- package/src/actions/types.ts +4 -0
- package/src/actions/watch.ts +5 -2
- package/src/conformance/assert.ts +59 -0
- package/src/conformance/cases.ts +522 -0
- package/src/conformance/consts.ts +44 -0
- package/src/conformance/fixtures.ts +121 -0
- package/src/conformance/index.ts +11 -0
- package/src/conformance/types.ts +20 -0
- package/src/consts.ts +6 -0
- package/src/definitions.ts +250 -0
- package/src/executor/changes.ts +2 -6
- package/src/executor/code.ts +3 -9
- package/src/executor/consts.local.ts +8 -0
- package/src/executor/creator.ts +51 -0
- package/src/executor/resolve.ts +84 -40
- package/src/executor/types.ts +25 -0
- package/src/executor/validate.ts +22 -26
- package/src/executor.ts +21 -21
- package/src/facade.ts +143 -25
- package/src/helper.ts +7 -3
- package/src/index.ts +1 -0
- package/src/projection.ts +4 -14
- package/src/queue/consts.ts +5 -0
- package/src/queue/index.ts +6 -21
- package/src/queue/types.ts +14 -0
- package/src/registry.ts +5 -29
- package/src/service.ts +13 -20
- package/src/store/commits.ts +5 -4
- package/src/store/composite.ts +3 -0
- package/src/store/consts.local.ts +1 -0
- package/src/store/fold.ts +15 -12
- package/src/store/memory.ts +0 -0
- package/src/store/types.local.ts +3 -0
- package/src/store/types.ts +16 -1
- package/src/types.local.ts +7 -0
- package/src/types.ts +126 -4
- package/src/utils/index.ts +1 -0
- package/src/utils/scope.ts +97 -17
- package/src/utils/types.ts +8 -0
- package/tests/access.spec.ts +160 -0
- package/tests/conformance.spec.ts +47 -0
- package/tests/create-example.spec.ts +202 -0
- package/tests/creator.spec.ts +257 -0
- package/tests/entrypoints.spec.ts +5 -0
- package/tests/narrowing.spec.ts +94 -0
- package/tests/plugins.spec.ts +4 -3
- package/tests/writes.spec.ts +140 -0
package/src/executor/resolve.ts
CHANGED
|
@@ -1,33 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
} from '
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
type: AnyTypeSchema
|
|
17
|
-
flowId: string
|
|
18
|
-
flow: StatusFlowSchema
|
|
19
|
-
/** The primary parent, when it resolves. */
|
|
20
|
-
parent?: Workcard
|
|
21
|
-
/** The slot a specification belongs to. */
|
|
22
|
-
slot?: SpecificationSlot
|
|
23
|
-
/** The store the type is written to. */
|
|
24
|
-
store: PlanningStore
|
|
1
|
+
import { CardTypeNotAllowed, isProject, normalizeParents, ParentNotFound, PlanningError, PlanningSchemaKind, PlanningScopeMismatch, primaryFlowOf, projectOf, SchemaOrigin, slotOf, TransitionAction, UnknownStatusFlow, UnknownWorkcardType, WorkcardKind, WorkcardNotFound, type AnyTypeSchema, type PlanningFacade, type PlanningSchemaRegistry, type ProjectTypeSchema, type ScopedSchemaRegistry, type Specification, type TransitionExecution, type Workcard, type WorkcardDraft } from '@owlmeans/planning'
|
|
2
|
+
import type { PlanningRuntime } from '../types.js'
|
|
3
|
+
import { TRIMMED } from './consts.local.js'
|
|
4
|
+
import type { Resolved } from './types.js'
|
|
5
|
+
|
|
6
|
+
const isScoped = (schemas: PlanningSchemaRegistry): schemas is ScopedSchemaRegistry =>
|
|
7
|
+
typeof (schemas as Partial<ScopedSchemaRegistry>).originOf === 'function'
|
|
8
|
+
|
|
9
|
+
/** A type the layer resolves from a data-defined record rather than from code. */
|
|
10
|
+
const dataDefined = (schemas: PlanningSchemaRegistry | undefined, type: string): boolean => {
|
|
11
|
+
if (schemas == null || !isScoped(schemas) || !schemas.has(type)) {
|
|
12
|
+
return false
|
|
13
|
+
}
|
|
14
|
+
const origin = schemas.originOf(PlanningSchemaKind.Type, type)
|
|
15
|
+
return origin === SchemaOrigin.Entity || origin === SchemaOrigin.Project
|
|
25
16
|
}
|
|
26
17
|
|
|
27
18
|
const trimmed = (value: unknown): unknown => typeof value === 'string' ? value.trim() : value
|
|
28
19
|
|
|
29
|
-
const TRIMMED = ['title', 'description', 'code'] as const
|
|
30
|
-
|
|
31
20
|
const trim = <T extends object>(record: T | undefined): T | undefined => {
|
|
32
21
|
if (record == null) {
|
|
33
22
|
return record
|
|
@@ -45,7 +34,7 @@ const trim = <T extends object>(record: T | undefined): T | undefined => {
|
|
|
45
34
|
* Step 1: a deep copy with trimmed text, `parents ∋ parent` on a draft, and no duplicate `unset`
|
|
46
35
|
* paths. The caller's object is never touched.
|
|
47
36
|
*/
|
|
48
|
-
|
|
37
|
+
const normalizeExecution = (input: TransitionExecution): TransitionExecution => {
|
|
49
38
|
const exec = structuredClone(input) as TransitionExecution
|
|
50
39
|
if (exec.card != null && typeof exec.card === 'object') {
|
|
51
40
|
const draft = trim(exec.card) as WorkcardDraft
|
|
@@ -61,9 +50,15 @@ export const normalizeExecution = (input: TransitionExecution): TransitionExecut
|
|
|
61
50
|
return exec
|
|
62
51
|
}
|
|
63
52
|
|
|
64
|
-
/**
|
|
65
|
-
|
|
66
|
-
|
|
53
|
+
/**
|
|
54
|
+
* The parent type's rule for a child of `kind`/`type`: a project lists the `cardTypes` (and
|
|
55
|
+
* `projectTypes`) it admits, and one declaring `scopedCardTypes` also admits a card type its layer
|
|
56
|
+
* (`view`, the parent project's resolved schemas) defines as data. Project types are code's alone.
|
|
57
|
+
*
|
|
58
|
+
* @throws {CardTypeNotAllowed}
|
|
59
|
+
*/
|
|
60
|
+
const assertChildAllowed = (
|
|
61
|
+
runtime: PlanningRuntime, parent: Workcard, kind: WorkcardKind, type: string, view?: PlanningSchemaRegistry
|
|
67
62
|
): void => {
|
|
68
63
|
if (kind === WorkcardKind.Specification) {
|
|
69
64
|
return
|
|
@@ -73,24 +68,37 @@ export const assertChildAllowed = (
|
|
|
73
68
|
}
|
|
74
69
|
const parentType = runtime.service().schemas.type(parent.type) as ProjectTypeSchema
|
|
75
70
|
const allowed = kind === WorkcardKind.Project ? parentType.projectTypes ?? [] : parentType.cardTypes ?? []
|
|
76
|
-
if (
|
|
77
|
-
|
|
71
|
+
if (allowed.includes(type)) {
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
if (kind === WorkcardKind.Card && parentType.scopedCardTypes === true && dataDefined(view, type)) {
|
|
75
|
+
return
|
|
78
76
|
}
|
|
77
|
+
throw new CardTypeNotAllowed(`${parent.type}:${type}`)
|
|
79
78
|
}
|
|
80
79
|
|
|
80
|
+
/** The layer a parent project admits data-defined children through — `undefined` without a schema port. */
|
|
81
|
+
const childViewOf = async (
|
|
82
|
+
runtime: PlanningRuntime, entityId: string, parent: Workcard
|
|
83
|
+
): Promise<PlanningSchemaRegistry | undefined> =>
|
|
84
|
+
runtime.schemaStore() == null || !isProject(parent) ? undefined : await runtime.schemasFor(entityId, parent.id)
|
|
85
|
+
|
|
81
86
|
/**
|
|
82
87
|
* Step 4: the card, its type and flow, its parent and — for a specification — its slot.
|
|
83
88
|
*
|
|
84
89
|
* @throws {WorkcardNotFound | PlanningScopeMismatch | UnknownWorkcardType | UnknownStatusFlow}
|
|
85
90
|
* @throws {ParentNotFound | CardTypeNotAllowed | PlanningError}
|
|
86
91
|
*/
|
|
87
|
-
|
|
92
|
+
const resolveExecution = async (
|
|
88
93
|
runtime: PlanningRuntime, facade: PlanningFacade, exec: TransitionExecution
|
|
89
94
|
): Promise<Resolved> => {
|
|
90
95
|
const service = runtime.service()
|
|
91
|
-
const schemas = service.schemas
|
|
92
96
|
const entityId = facade.scope.entityId
|
|
93
97
|
const create = exec.action === TransitionAction.Create
|
|
98
|
+
// Without a schema port every step reads the code registry itself, in today's order; with one,
|
|
99
|
+
// the card's project is found first and the step reads that project's resolved layer.
|
|
100
|
+
const layered = runtime.schemaStore() != null
|
|
101
|
+
let schemas: PlanningSchemaRegistry = service.schemas
|
|
94
102
|
|
|
95
103
|
let card: Workcard | undefined
|
|
96
104
|
let type: AnyTypeSchema
|
|
@@ -101,14 +109,34 @@ export const resolveExecution = async (
|
|
|
101
109
|
throw new PlanningError('malformed:create-without-draft')
|
|
102
110
|
}
|
|
103
111
|
const draft = exec.card
|
|
112
|
+
const parents = normalizeParents(draft.parent, draft.parents)
|
|
113
|
+
const loaded = new Map<string, Workcard>()
|
|
114
|
+
if (layered) {
|
|
115
|
+
// Loaded through the facade, so a parent this scope cannot see is absent here too.
|
|
116
|
+
const primary = parents[0] != null ? await facade.cards.load(parents[0]) : null
|
|
117
|
+
if (parents[0] != null && primary == null) {
|
|
118
|
+
throw new ParentNotFound(parents[0])
|
|
119
|
+
}
|
|
120
|
+
if (primary != null) {
|
|
121
|
+
loaded.set(parents[0], primary)
|
|
122
|
+
}
|
|
123
|
+
const project = draft.kind === WorkcardKind.Project || primary == null
|
|
124
|
+
? undefined
|
|
125
|
+
: projectOf({ kind: draft.kind, parent: parents[0], parents } as unknown as Workcard, primary)
|
|
126
|
+
schemas = await runtime.schemasFor(entityId, project)
|
|
127
|
+
}
|
|
104
128
|
type = schemas.type(draft.type)
|
|
105
129
|
if (type.kind !== draft.kind) {
|
|
106
130
|
throw new PlanningError(`malformed:kind:${draft.type}:${draft.kind}`)
|
|
107
131
|
}
|
|
132
|
+
if (layered && isScoped(schemas) && schemas.isRetired(PlanningSchemaKind.Type, draft.type)) {
|
|
133
|
+
// Existing cards keep resolving a retired type; nothing new is made of it.
|
|
134
|
+
throw new UnknownWorkcardType(`retired:${draft.type}`)
|
|
135
|
+
}
|
|
108
136
|
// Normalized: the primary parent is first. It must accept the child; so must every project
|
|
109
137
|
// among the secondary parents.
|
|
110
|
-
for (const [index, id] of
|
|
111
|
-
const found = await facade.cards.load(id)
|
|
138
|
+
for (const [index, id] of parents.entries()) {
|
|
139
|
+
const found = loaded.get(id) ?? await facade.cards.load(id)
|
|
112
140
|
if (found == null) {
|
|
113
141
|
throw new ParentNotFound(id)
|
|
114
142
|
}
|
|
@@ -116,7 +144,8 @@ export const resolveExecution = async (
|
|
|
116
144
|
parent = found
|
|
117
145
|
}
|
|
118
146
|
if (index === 0 || isProject(found)) {
|
|
119
|
-
|
|
147
|
+
const view = !layered ? undefined : index === 0 && isProject(found) ? schemas : await childViewOf(runtime, entityId, found)
|
|
148
|
+
assertChildAllowed(runtime, found, draft.kind, draft.type, view)
|
|
120
149
|
}
|
|
121
150
|
}
|
|
122
151
|
if (draft.kind === WorkcardKind.Specification && parent == null) {
|
|
@@ -133,9 +162,19 @@ export const resolveExecution = async (
|
|
|
133
162
|
if (found.entityId !== entityId) {
|
|
134
163
|
throw new PlanningScopeMismatch(exec.card)
|
|
135
164
|
}
|
|
165
|
+
if (facade.scope.projects != null && await facade.cards.load(exec.card) == null) {
|
|
166
|
+
// Outside the scope's projects: absent, exactly like another entity's card.
|
|
167
|
+
throw new WorkcardNotFound(exec.card)
|
|
168
|
+
}
|
|
136
169
|
card = found
|
|
137
|
-
|
|
138
|
-
|
|
170
|
+
if (layered) {
|
|
171
|
+
parent = card.parent != null ? await facade.cards.load(card.parent) ?? undefined : undefined
|
|
172
|
+
schemas = await runtime.schemasFor(entityId, projectOf(card, parent))
|
|
173
|
+
type = schemas.type(card.type)
|
|
174
|
+
} else {
|
|
175
|
+
type = schemas.type(card.type)
|
|
176
|
+
parent = card.parent != null ? await facade.cards.load(card.parent) ?? undefined : undefined
|
|
177
|
+
}
|
|
139
178
|
}
|
|
140
179
|
|
|
141
180
|
const flowId = exec.flow ?? primaryFlowOf(type)
|
|
@@ -158,11 +197,12 @@ export const resolveExecution = async (
|
|
|
158
197
|
...(parent != null ? { parent } : {}),
|
|
159
198
|
...(slot != null ? { slot } : {}),
|
|
160
199
|
store: service.store(type.type),
|
|
200
|
+
schemas,
|
|
161
201
|
}
|
|
162
202
|
}
|
|
163
203
|
|
|
164
204
|
/** The project a transition is filed under: a project's own id, else its parent's project. */
|
|
165
|
-
|
|
205
|
+
const projectFor = (resolved: Resolved, exec: TransitionExecution, cardId: string): string | undefined => {
|
|
166
206
|
if (resolved.card != null) {
|
|
167
207
|
return isProject(resolved.card) ? resolved.card.id : projectOf(resolved.card, resolved.parent)
|
|
168
208
|
}
|
|
@@ -172,3 +212,7 @@ export const projectFor = (resolved: Resolved, exec: TransitionExecution, cardId
|
|
|
172
212
|
}
|
|
173
213
|
return projectOf({ kind: draft.kind, parent: draft.parent, parents: draft.parents ?? [] } as unknown as Workcard, resolved.parent)
|
|
174
214
|
}
|
|
215
|
+
|
|
216
|
+
export const createResolveHelper = () => ({ normalizeExecution, assertChildAllowed, childViewOf, resolveExecution, projectFor })
|
|
217
|
+
|
|
218
|
+
export const resolveHelper = createResolveHelper()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { AnyTypeSchema, PlanningSchemaRegistry, PlanningStore, SpecificationSlot, StatusFlowSchema, Workcard } from '@owlmeans/planning'
|
|
2
|
+
import type { createResolveHelper } from './resolve.js'
|
|
3
|
+
|
|
4
|
+
/** Everything the steps after resolution read about an execution. */
|
|
5
|
+
export interface Resolved {
|
|
6
|
+
create: boolean
|
|
7
|
+
/** The existing card (not a create). */
|
|
8
|
+
card?: Workcard
|
|
9
|
+
type: AnyTypeSchema
|
|
10
|
+
flowId: string
|
|
11
|
+
flow: StatusFlowSchema
|
|
12
|
+
/** The primary parent, when it resolves. */
|
|
13
|
+
parent?: Workcard
|
|
14
|
+
/** The slot a specification belongs to. */
|
|
15
|
+
slot?: SpecificationSlot
|
|
16
|
+
/** The store the type is written to. */
|
|
17
|
+
store: PlanningStore
|
|
18
|
+
/**
|
|
19
|
+
* The registry every later step reads: the service's code registry itself, or — where the store
|
|
20
|
+
* holds data-defined schemas — the resolved layer of the card's project.
|
|
21
|
+
*/
|
|
22
|
+
schemas: PlanningSchemaRegistry
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type ResolveHelper = ReturnType<typeof createResolveHelper>
|
package/src/executor/validate.ts
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
} from '
|
|
7
|
-
import type {
|
|
8
|
-
PlanningFacade, RelationshipDraft, Specification, TransitionExecution, WorkcardDraft,
|
|
9
|
-
} from '@owlmeans/planning'
|
|
10
|
-
import type { PlanningRuntime } from '../service.js'
|
|
11
|
-
import { assertChildAllowed } from './resolve.js'
|
|
12
|
-
import type { Resolved } from './resolve.js'
|
|
13
|
-
|
|
14
|
-
const ACTIONS = new Set<string>(Object.values(TransitionAction))
|
|
1
|
+
import { applyUnset, assertMutable, BODY_MAX, CODE_MAX, DESCRIPTION_MAX, IllegalTransition, LabelNotAllowed, MAX_LABELS, MAX_PARENTS, mergeFields, normalizeParents, ParentNotFound, PlanningError, RelationshipRefused, ruleOf, SpecificationRevisionConflict, SpecificationSlotUnknown, slotOf, TITLE_MAX, TransitionAction, validateFields, validateSpecificationBody, WorkcardKind, type PlanningFacade, type RelationshipDraft, type Specification, type TransitionExecution, type WorkcardDraft } from '@owlmeans/planning'
|
|
2
|
+
import type { PlanningRuntime } from '../types.js'
|
|
3
|
+
import { assertCreatorFixed } from './creator.js'
|
|
4
|
+
import type { Resolved } from './types.js'
|
|
5
|
+
import { ACTIONS } from './consts.local.js'
|
|
6
|
+
import { resolveHelper } from './resolve.js'
|
|
15
7
|
|
|
16
8
|
const malformed = (what: string): PlanningError => new PlanningError(`malformed:${what}`)
|
|
17
9
|
|
|
@@ -58,10 +50,10 @@ const assertShape = (exec: TransitionExecution, resolved: Resolved): void => {
|
|
|
58
50
|
assertText('code', changes.code ?? undefined, CODE_MAX, false)
|
|
59
51
|
}
|
|
60
52
|
|
|
61
|
-
const assertFlow = (exec: TransitionExecution, resolved: Resolved
|
|
53
|
+
const assertFlow = (exec: TransitionExecution, resolved: Resolved): void => {
|
|
62
54
|
if (resolved.create) {
|
|
63
55
|
const status = (exec.card as WorkcardDraft).status
|
|
64
|
-
const primary =
|
|
56
|
+
const primary = resolved.schemas.primaryFlow(resolved.type.type)
|
|
65
57
|
if (status != null && !primary.statuses.some(definition => definition.key === status)) {
|
|
66
58
|
throw new IllegalTransition(`${primary.id}:create:${status}`)
|
|
67
59
|
}
|
|
@@ -77,8 +69,8 @@ const assertFlow = (exec: TransitionExecution, resolved: Resolved, runtime: Plan
|
|
|
77
69
|
}
|
|
78
70
|
}
|
|
79
71
|
|
|
80
|
-
const assertFields = (exec: TransitionExecution, resolved: Resolved
|
|
81
|
-
const schemas =
|
|
72
|
+
const assertFields = (exec: TransitionExecution, resolved: Resolved): void => {
|
|
73
|
+
const schemas = resolved.schemas
|
|
82
74
|
if (resolved.create) {
|
|
83
75
|
const draft = exec.card as WorkcardDraft
|
|
84
76
|
validateFields(schemas, resolved.type.type, mergeFields(draft.fields, exec.changes?.fields))
|
|
@@ -127,18 +119,20 @@ const assertParentMove = async (
|
|
|
127
119
|
if (found == null) {
|
|
128
120
|
throw new ParentNotFound(id)
|
|
129
121
|
}
|
|
130
|
-
assertChildAllowed(
|
|
122
|
+
resolveHelper.assertChildAllowed(
|
|
123
|
+
runtime, found, resolved.card!.kind, resolved.card!.type, await resolveHelper.childViewOf(runtime, facade.scope.entityId, found)
|
|
124
|
+
)
|
|
131
125
|
}
|
|
132
126
|
}
|
|
133
127
|
|
|
134
128
|
const assertSpecification = async (
|
|
135
|
-
exec: TransitionExecution, resolved: Resolved,
|
|
129
|
+
exec: TransitionExecution, resolved: Resolved, facade: PlanningFacade
|
|
136
130
|
): Promise<void> => {
|
|
137
131
|
const kind = resolved.create ? (exec.card as WorkcardDraft).kind : resolved.card!.kind
|
|
138
132
|
if (kind !== WorkcardKind.Specification) {
|
|
139
133
|
return
|
|
140
134
|
}
|
|
141
|
-
const schemas =
|
|
135
|
+
const schemas = resolved.schemas
|
|
142
136
|
|
|
143
137
|
if (resolved.create) {
|
|
144
138
|
const draft = exec.card as WorkcardDraft
|
|
@@ -232,8 +226,9 @@ const assertRelationships = async (
|
|
|
232
226
|
}
|
|
233
227
|
|
|
234
228
|
/**
|
|
235
|
-
* Step 6: shape, flow rule, immutables
|
|
236
|
-
*
|
|
229
|
+
* Step 6: shape, flow rule, immutables (`createdBy` among them, {@link assertCreatorFixed}),
|
|
230
|
+
* `fields`, labels, parent moves, the specification slot and relationships — in that order.
|
|
231
|
+
* Nothing is written by a refusal here.
|
|
237
232
|
*
|
|
238
233
|
* @throws {PlanningError} `malformed:*` / `immutable:*`
|
|
239
234
|
* @throws {IllegalTransition | FieldsInvalid | LabelNotAllowed | ParentNotFound | CardTypeNotAllowed}
|
|
@@ -243,11 +238,12 @@ export const validateExecution = async (
|
|
|
243
238
|
runtime: PlanningRuntime, facade: PlanningFacade, exec: TransitionExecution, resolved: Resolved
|
|
244
239
|
): Promise<void> => {
|
|
245
240
|
assertShape(exec, resolved)
|
|
246
|
-
assertFlow(exec, resolved
|
|
241
|
+
assertFlow(exec, resolved)
|
|
247
242
|
assertMutable(exec, resolved.type)
|
|
248
|
-
|
|
243
|
+
assertCreatorFixed(exec)
|
|
244
|
+
assertFields(exec, resolved)
|
|
249
245
|
assertLabels(exec, resolved)
|
|
250
246
|
await assertParentMove(exec, resolved, runtime, facade)
|
|
251
|
-
await assertSpecification(exec, resolved,
|
|
247
|
+
await assertSpecification(exec, resolved, facade)
|
|
252
248
|
await assertRelationships(exec, resolved, facade)
|
|
253
249
|
}
|
package/src/executor.ts
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { uuid } from '@owlmeans/basic-ids'
|
|
2
2
|
import type { BasicConfig, BasicContext } from '@owlmeans/context'
|
|
3
|
-
import { PlanningError, PlanningUnsupported, WorkcardConflict } from '@owlmeans/planning'
|
|
4
|
-
import type {
|
|
5
|
-
ExecuteOptions, PlanningExecContext, PlanningFacade, PlanningPlugin, PlanningStore, Transition,
|
|
6
|
-
TransitionExecution, TransitionReceipt,
|
|
7
|
-
} from '@owlmeans/planning'
|
|
3
|
+
import { PlanningError, PlanningUnsupported, WorkcardConflict, type ExecuteOptions, type PlanningExecContext, type PlanningFacade, type PlanningPlugin, type PlanningStore, type Transition, type TransitionExecution, type TransitionReceipt } from '@owlmeans/planning'
|
|
8
4
|
import { changeSetOf, transitionOf } from './executor/changes.js'
|
|
9
5
|
import { assignCode } from './executor/code.js'
|
|
6
|
+
import { withCreator } from './executor/creator.js'
|
|
10
7
|
import { makeReceipt } from './executor/receipt.js'
|
|
11
|
-
import {
|
|
12
|
-
import type { Resolved } from './executor/resolve.js'
|
|
8
|
+
import type { Resolved } from './executor/types.js'
|
|
13
9
|
import { validateExecution } from './executor/validate.js'
|
|
14
|
-
import type { PlanningRuntime } from './
|
|
10
|
+
import type { PlanningRuntime } from './types.js'
|
|
11
|
+
import { resolveHelper } from './executor/resolve.js'
|
|
12
|
+
|
|
13
|
+
export { assertCreatorFixed, creatorOf, withCreator } from './executor/creator.js'
|
|
15
14
|
|
|
16
15
|
const isoNow = (): string => new Date().toISOString()
|
|
17
16
|
|
|
@@ -25,7 +24,7 @@ const execContextOf = (
|
|
|
25
24
|
) => (plugin: PlanningPlugin): PlanningExecContext => ({
|
|
26
25
|
context: runtime.context() as BasicContext<BasicConfig>,
|
|
27
26
|
scope: facade.scope,
|
|
28
|
-
schemas:
|
|
27
|
+
schemas: resolved.schemas,
|
|
29
28
|
store: resolved.store,
|
|
30
29
|
facade,
|
|
31
30
|
plugin,
|
|
@@ -64,11 +63,13 @@ const currentReceipt = async (
|
|
|
64
63
|
/**
|
|
65
64
|
* THE write path. Nothing is appended before step 11, so every refusal leaves the log untouched.
|
|
66
65
|
*
|
|
67
|
-
* 1 normalize
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
66
|
+
* 1 normalize (a create's `createdBy` defaults to the scope's subject — {@link withCreator}) →
|
|
67
|
+
* 2 scope → 3 idempotency (a known `key` answers its first receipt, before any validation) →
|
|
68
|
+
* 4 resolve → 5 the plugins' `before` chain (re-resolved once when it moved the type or the parent)
|
|
69
|
+
* → 6 validate (a `createdBy` in `changes`/`unset` is refused — {@link assertCreatorFixed}) →
|
|
70
|
+
* 7 allocate the card id → 8 code → 9 changes (an update changing nothing answers
|
|
71
|
+
* the current receipt and appends nothing) → 10 seq, a CAS against the head → 11 append →
|
|
72
|
+
* 12 request the projection → 13 receipt → 14 `wait`.
|
|
72
73
|
*
|
|
73
74
|
* `after` hooks are NOT run here — the process that folds runs them, once per commit.
|
|
74
75
|
*/
|
|
@@ -76,10 +77,9 @@ export const executeTransition = async (
|
|
|
76
77
|
runtime: PlanningRuntime, facade: PlanningFacade, input: TransitionExecution, opts?: ExecuteOptions
|
|
77
78
|
): Promise<TransitionReceipt> => {
|
|
78
79
|
const scope = facade.scope
|
|
79
|
-
const service = runtime.service()
|
|
80
80
|
const at = (runtime.options.now ?? isoNow)()
|
|
81
81
|
|
|
82
|
-
let exec = normalizeExecution(input)
|
|
82
|
+
let exec = withCreator(resolveHelper.normalizeExecution(input), scope)
|
|
83
83
|
|
|
84
84
|
if (scope.entityId == null || scope.entityId === '') {
|
|
85
85
|
throw new PlanningError('malformed:scope-without-entity')
|
|
@@ -92,12 +92,12 @@ export const executeTransition = async (
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
let resolved = await resolveExecution(runtime, facade, exec)
|
|
95
|
+
let resolved = await resolveHelper.resolveExecution(runtime, facade, exec)
|
|
96
96
|
|
|
97
97
|
const before = identityOf(exec)
|
|
98
|
-
exec = normalizeExecution(await runtime.registry.before(exec, execContextOf(runtime, facade, resolved)))
|
|
98
|
+
exec = resolveHelper.normalizeExecution(await runtime.registry.before(exec, execContextOf(runtime, facade, resolved)))
|
|
99
99
|
if (identityOf(exec) !== before) {
|
|
100
|
-
resolved = await resolveExecution(runtime, facade, exec)
|
|
100
|
+
resolved = await resolveHelper.resolveExecution(runtime, facade, exec)
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
await validateExecution(runtime, facade, exec, resolved)
|
|
@@ -114,7 +114,7 @@ export const executeTransition = async (
|
|
|
114
114
|
|
|
115
115
|
exec = await assignCode(exec, resolved, scope, runtime.registry, execContextOf(runtime, facade, resolved))
|
|
116
116
|
|
|
117
|
-
const { set, empty } = changeSetOf(exec, resolved,
|
|
117
|
+
const { set, empty } = changeSetOf(exec, resolved, resolved.schemas, at)
|
|
118
118
|
if (empty) {
|
|
119
119
|
return await currentReceipt(store, resolved, scope.entityId, opts)
|
|
120
120
|
}
|
|
@@ -126,7 +126,7 @@ export const executeTransition = async (
|
|
|
126
126
|
|
|
127
127
|
const transition = transitionOf({
|
|
128
128
|
exec, resolved, scope, set, cardId, seq, at,
|
|
129
|
-
project: projectFor(resolved, exec, cardId),
|
|
129
|
+
project: resolveHelper.projectFor(resolved, exec, cardId),
|
|
130
130
|
})
|
|
131
131
|
|
|
132
132
|
let appended: Transition
|