@owlmeans/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 +69 -26
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/planning/SKILL.md +157 -45
- package/build/aliases/types.d.ts +49 -0
- package/build/aliases/types.d.ts.map +1 -0
- package/build/aliases/types.js +2 -0
- package/build/aliases/types.js.map +1 -0
- package/build/aliases.d.ts +4 -0
- package/build/aliases.d.ts.map +1 -0
- package/build/aliases.js +27 -0
- package/build/aliases.js.map +1 -0
- package/build/consts.d.ts +23 -34
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +26 -25
- package/build/consts.js.map +1 -1
- package/build/entrypoints/types.d.ts +96 -0
- package/build/entrypoints/types.d.ts.map +1 -0
- package/build/entrypoints/types.js +2 -0
- package/build/entrypoints/types.js.map +1 -0
- package/build/entrypoints.d.ts +5 -1
- package/build/entrypoints.d.ts.map +1 -1
- package/build/entrypoints.js +17 -6
- package/build/entrypoints.js.map +1 -1
- package/build/errors.d.ts +41 -0
- package/build/errors.d.ts.map +1 -1
- package/build/errors.js +61 -0
- package/build/errors.js.map +1 -1
- package/build/helpers/apply/consts.local.d.ts +7 -0
- package/build/helpers/apply/consts.local.d.ts.map +1 -0
- package/build/helpers/apply/consts.local.js +9 -0
- package/build/helpers/apply/consts.local.js.map +1 -0
- package/build/helpers/apply/types.d.ts +32 -0
- package/build/helpers/apply/types.d.ts.map +1 -0
- package/build/helpers/apply/types.js +2 -0
- package/build/helpers/apply/types.js.map +1 -0
- package/build/helpers/apply.d.ts +5 -23
- package/build/helpers/apply.d.ts.map +1 -1
- package/build/helpers/apply.js +121 -143
- package/build/helpers/apply.js.map +1 -1
- package/build/helpers/card/types.d.ts +20 -0
- package/build/helpers/card/types.d.ts.map +1 -0
- package/build/helpers/card/types.js +2 -0
- package/build/helpers/card/types.js.map +1 -0
- package/build/helpers/card.d.ts +11 -7
- package/build/helpers/card.d.ts.map +1 -1
- package/build/helpers/card.js +41 -28
- package/build/helpers/card.js.map +1 -1
- package/build/helpers/changes/consts.local.d.ts +13 -0
- package/build/helpers/changes/consts.local.d.ts.map +1 -0
- package/build/helpers/changes/consts.local.js +13 -0
- package/build/helpers/changes/consts.local.js.map +1 -0
- package/build/helpers/changes/types.d.ts +58 -0
- package/build/helpers/changes/types.d.ts.map +1 -0
- package/build/helpers/changes/types.js +2 -0
- package/build/helpers/changes/types.js.map +1 -0
- package/build/helpers/changes.d.ts +11 -42
- package/build/helpers/changes.d.ts.map +1 -1
- package/build/helpers/changes.js +234 -251
- package/build/helpers/changes.js.map +1 -1
- package/build/helpers/code/types.d.ts +27 -0
- package/build/helpers/code/types.d.ts.map +1 -0
- package/build/helpers/code/types.js +2 -0
- package/build/helpers/code/types.js.map +1 -0
- package/build/helpers/code.d.ts +6 -17
- package/build/helpers/code.d.ts.map +1 -1
- package/build/helpers/code.js +39 -40
- package/build/helpers/code.js.map +1 -1
- package/build/helpers/index.d.ts +12 -0
- package/build/helpers/index.d.ts.map +1 -1
- package/build/helpers/index.js +2 -0
- package/build/helpers/index.js.map +1 -1
- package/build/helpers/query/types.d.ts +23 -0
- package/build/helpers/query/types.d.ts.map +1 -0
- package/build/helpers/query/types.js +2 -0
- package/build/helpers/query/types.js.map +1 -0
- package/build/helpers/query.d.ts +25 -17
- package/build/helpers/query.d.ts.map +1 -1
- package/build/helpers/query.js +108 -280
- package/build/helpers/query.js.map +1 -1
- package/build/helpers/scoped/types.d.ts +59 -0
- package/build/helpers/scoped/types.d.ts.map +1 -0
- package/build/helpers/scoped/types.js +2 -0
- package/build/helpers/scoped/types.js.map +1 -0
- package/build/helpers/scoped.d.ts +24 -0
- package/build/helpers/scoped.d.ts.map +1 -0
- package/build/helpers/scoped.js +197 -0
- package/build/helpers/scoped.js.map +1 -0
- package/build/helpers/specification/types.d.ts +24 -0
- package/build/helpers/specification/types.d.ts.map +1 -0
- package/build/helpers/specification/types.js +2 -0
- package/build/helpers/specification/types.js.map +1 -0
- package/build/helpers/specification.d.ts +9 -13
- package/build/helpers/specification.d.ts.map +1 -1
- package/build/helpers/specification.js +47 -44
- package/build/helpers/specification.js.map +1 -1
- package/build/helpers/status/types.d.ts +48 -0
- package/build/helpers/status/types.d.ts.map +1 -0
- package/build/helpers/status/types.js +2 -0
- package/build/helpers/status/types.js.map +1 -0
- package/build/helpers/status.d.ts +16 -29
- package/build/helpers/status.d.ts.map +1 -1
- package/build/helpers/status.js +77 -73
- package/build/helpers/status.js.map +1 -1
- package/build/helpers/types.local.d.ts +17 -0
- package/build/helpers/types.local.d.ts.map +1 -0
- package/build/helpers/types.local.js +2 -0
- package/build/helpers/types.local.js.map +1 -0
- package/build/helpers/validate/types.d.ts +30 -0
- package/build/helpers/validate/types.d.ts.map +1 -0
- package/build/helpers/validate/types.js +2 -0
- package/build/helpers/validate/types.js.map +1 -0
- package/build/helpers/validate.d.ts +8 -19
- package/build/helpers/validate.d.ts.map +1 -1
- package/build/helpers/validate.js +74 -79
- package/build/helpers/validate.js.map +1 -1
- package/build/helpers/wire/types.d.ts +28 -0
- package/build/helpers/wire/types.d.ts.map +1 -0
- package/build/helpers/wire/types.js +2 -0
- package/build/helpers/wire/types.js.map +1 -0
- package/build/helpers/wire.d.ts +4 -0
- package/build/helpers/wire.d.ts.map +1 -0
- package/build/helpers/wire.js +215 -0
- package/build/helpers/wire.js.map +1 -0
- package/build/i18n/be.json +5 -0
- package/build/i18n/de.json +5 -0
- package/build/i18n/en.json +5 -0
- package/build/i18n/es.json +5 -0
- package/build/i18n/fr.json +56 -0
- package/build/i18n/pl.json +5 -0
- package/build/i18n/ru.json +5 -0
- package/build/i18n/uk.json +5 -0
- package/build/i18n.js +5 -4
- package/build/i18n.js.map +1 -1
- package/build/index.d.ts +4 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -0
- package/build/index.js.map +1 -1
- package/build/models/index.d.ts +2 -3
- package/build/models/index.d.ts.map +1 -1
- package/build/models/index.js +1 -15
- package/build/models/index.js.map +1 -1
- package/build/models/model.d.ts +5 -0
- package/build/models/model.d.ts.map +1 -0
- package/build/models/model.js +16 -0
- package/build/models/model.js.map +1 -0
- package/build/models/project.d.ts +2 -1
- package/build/models/project.d.ts.map +1 -1
- package/build/models/project.js.map +1 -1
- package/build/models/specification.d.ts +2 -1
- package/build/models/specification.d.ts.map +1 -1
- package/build/models/specification.js.map +1 -1
- package/build/models/types.d.ts +69 -0
- package/build/models/types.d.ts.map +1 -0
- package/build/models/types.js +2 -0
- package/build/models/types.js.map +1 -0
- package/build/models/workcard.d.ts +2 -1
- package/build/models/workcard.d.ts.map +1 -1
- package/build/models/workcard.js +12 -12
- package/build/models/workcard.js.map +1 -1
- package/build/schemas.d.ts +10 -2
- package/build/schemas.d.ts.map +1 -1
- package/build/schemas.js +87 -1
- package/build/schemas.js.map +1 -1
- package/build/types.d.ts +199 -148
- package/build/types.d.ts.map +1 -1
- package/build/utils.d.ts +6 -0
- package/build/utils.d.ts.map +1 -0
- package/build/utils.js +6 -0
- package/build/utils.js.map +1 -0
- package/package.json +11 -11
- package/src/aliases/types.ts +38 -0
- package/src/aliases.ts +31 -0
- package/src/consts.ts +26 -25
- package/src/entrypoints/types.ts +68 -0
- package/src/entrypoints.ts +34 -14
- package/src/errors.ts +71 -0
- package/src/helpers/apply/consts.local.ts +10 -0
- package/src/helpers/apply/types.ts +32 -0
- package/src/helpers/apply.ts +135 -152
- package/src/helpers/card/types.ts +20 -0
- package/src/helpers/card.ts +59 -33
- package/src/helpers/changes/consts.local.ts +17 -0
- package/src/helpers/changes/types.ts +66 -0
- package/src/helpers/changes.ts +267 -280
- package/src/helpers/code/types.ts +31 -0
- package/src/helpers/code.ts +52 -48
- package/src/helpers/index.ts +13 -0
- package/src/helpers/query/types.ts +26 -0
- package/src/helpers/query.ts +144 -314
- package/src/helpers/scoped/types.ts +64 -0
- package/src/helpers/scoped.ts +257 -0
- package/src/helpers/specification/types.ts +26 -0
- package/src/helpers/specification.ts +69 -51
- package/src/helpers/status/types.ts +46 -0
- package/src/helpers/status.ts +117 -93
- package/src/helpers/types.local.ts +18 -0
- package/src/helpers/validate/types.ts +30 -0
- package/src/helpers/validate.ts +84 -77
- package/src/helpers/wire/types.ts +31 -0
- package/src/helpers/wire.ts +251 -0
- package/src/i18n/be.json +5 -0
- package/src/i18n/de.json +5 -0
- package/src/i18n/en.json +5 -0
- package/src/i18n/es.json +5 -0
- package/src/i18n/fr.json +56 -0
- package/src/i18n/pl.json +5 -0
- package/src/i18n/ru.json +5 -0
- package/src/i18n/uk.json +5 -0
- package/src/i18n.ts +6 -4
- package/src/index.ts +4 -0
- package/src/models/index.ts +2 -17
- package/src/models/model.ts +18 -0
- package/src/models/project.ts +2 -1
- package/src/models/specification.ts +2 -1
- package/src/models/types.ts +70 -0
- package/src/models/workcard.ts +14 -16
- package/src/schemas.ts +98 -2
- package/src/types.ts +207 -126
- package/src/utils.ts +9 -0
- package/tests/apply.spec.ts +95 -31
- package/tests/code.spec.ts +9 -9
- package/tests/entrypoints.spec.ts +18 -2
- package/tests/error-status.spec.ts +6 -1
- package/tests/models.spec.ts +16 -15
- package/tests/query.spec.ts +13 -15
- package/tests/schemas.spec.ts +8 -8
- package/tests/scoped.spec.ts +154 -0
- package/tests/status.spec.ts +31 -22
package/src/helpers/apply.ts
CHANGED
|
@@ -1,183 +1,166 @@
|
|
|
1
1
|
import { IntrinsicStatus, TransitionAction } from '../consts.js'
|
|
2
2
|
import { PlanningError } from '../errors.js'
|
|
3
3
|
import type { Relationship, RelationshipDraft, Transition, Workcard } from '../types.js'
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const IDENTITY_KEYS = new Set(['id', 'kind', 'type', 'entityId', 'seq', 'head', 'createdAt'])
|
|
7
|
-
|
|
8
|
-
/** Cannot be cleared: a record without them is not a record. */
|
|
9
|
-
const REQUIRED_KEYS = new Set([
|
|
10
|
-
...IDENTITY_KEYS, 'title', 'parents', 'status', 'intrinsic', 'flows', 'labels', 'fields',
|
|
11
|
-
])
|
|
12
|
-
|
|
13
|
-
/** Merged shallowly into the card's own map rather than replacing it. */
|
|
14
|
-
const MERGED_KEYS = new Set(['fields', 'flows'])
|
|
4
|
+
import type { ApplyHelper } from './apply/types.js'
|
|
5
|
+
import { IDENTITY_KEYS, MERGED_KEYS, REQUIRED_KEYS } from './apply/consts.local.js'
|
|
15
6
|
|
|
16
7
|
const clean = <T extends object>(record: T): T =>
|
|
17
8
|
Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined)) as T
|
|
18
9
|
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
10
|
+
export const createApplyHelper = (): ApplyHelper => {
|
|
11
|
+
const outOfOrder = (card: Workcard | null | undefined, transition: Transition): PlanningError =>
|
|
12
|
+
new PlanningError(`fold:out-of-order:${transition.card}:${card?.seq ?? 0}:${transition.seq}`)
|
|
13
|
+
|
|
14
|
+
const unsetOn = (record: Record<string, unknown>, paths: string[] | undefined): void => {
|
|
15
|
+
for (const path of paths ?? []) {
|
|
16
|
+
const dot = path.indexOf('.')
|
|
17
|
+
if (dot > 0) {
|
|
18
|
+
const head = path.slice(0, dot)
|
|
19
|
+
const map = record[head]
|
|
20
|
+
if (MERGED_KEYS.has(head) && map != null && typeof map === 'object') {
|
|
21
|
+
delete (map as Record<string, unknown>)[path.slice(dot + 1)]
|
|
22
|
+
}
|
|
23
|
+
continue
|
|
24
|
+
}
|
|
25
|
+
if (!REQUIRED_KEYS.has(path)) {
|
|
26
|
+
delete record[path]
|
|
30
27
|
}
|
|
31
|
-
continue
|
|
32
|
-
}
|
|
33
|
-
if (!REQUIRED_KEYS.has(path)) {
|
|
34
|
-
delete record[path]
|
|
35
28
|
}
|
|
36
29
|
}
|
|
37
|
-
}
|
|
38
30
|
|
|
39
|
-
const writeChanges = (record: Record<string, unknown>, changes: Record<string, unknown>): void => {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
31
|
+
const writeChanges = (record: Record<string, unknown>, changes: Record<string, unknown>): void => {
|
|
32
|
+
for (const [key, value] of Object.entries(changes)) {
|
|
33
|
+
if (value === undefined || IDENTITY_KEYS.has(key)) {
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
if (MERGED_KEYS.has(key)) {
|
|
37
|
+
const merged = { ...(record[key] as Record<string, unknown> | undefined ?? {}) }
|
|
38
|
+
for (const [entry, entryValue] of Object.entries(value as Record<string, unknown> ?? {})) {
|
|
39
|
+
if (entryValue === null) {
|
|
40
|
+
delete merged[entry]
|
|
41
|
+
} else if (entryValue !== undefined) {
|
|
42
|
+
merged[entry] = entryValue
|
|
43
|
+
}
|
|
51
44
|
}
|
|
45
|
+
record[key] = merged
|
|
46
|
+
continue
|
|
52
47
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
delete record[key]
|
|
48
|
+
if (value === null) {
|
|
49
|
+
if (!REQUIRED_KEYS.has(key)) {
|
|
50
|
+
delete record[key]
|
|
51
|
+
}
|
|
52
|
+
continue
|
|
59
53
|
}
|
|
60
|
-
|
|
54
|
+
record[key] = value
|
|
61
55
|
}
|
|
62
|
-
record[key] = value
|
|
63
56
|
}
|
|
64
|
-
}
|
|
65
57
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
* safe.
|
|
72
|
-
* - Anything but the next seq (`card.seq + 1`, or 1 for a create with no card) is refused with
|
|
73
|
-
* `PlanningError('fold:out-of-order')`.
|
|
74
|
-
* - `create` builds the whole record from `changes` plus the transition's identity; `delete`
|
|
75
|
-
* answers `null`; `link`/`unlink` only move `seq`/`head`/`updatedAt` (the edges are
|
|
76
|
-
* {@link applyRelationship}'s); `update`/`transit` write `changes` — `fields` and `flows` merge,
|
|
77
|
-
* everything else replaces — and clear `unset`.
|
|
78
|
-
* - Every result has `seq = transition.seq`, `head = max(head, seq)`, `updatedAt = transition.at`.
|
|
79
|
-
*
|
|
80
|
-
* @throws {PlanningError} `planning:fold:out-of-order:…`
|
|
81
|
-
*/
|
|
82
|
-
export const applyTransition = <T extends Workcard = Workcard>(
|
|
83
|
-
card: T | null | undefined, transition: Transition
|
|
84
|
-
): T | null => {
|
|
85
|
-
if (card != null && transition.seq <= card.seq) {
|
|
86
|
-
return card
|
|
87
|
-
}
|
|
88
|
-
if (card == null) {
|
|
89
|
-
if (transition.action === TransitionAction.Delete) {
|
|
90
|
-
return null
|
|
58
|
+
const applyTransition = <T extends Workcard = Workcard>(
|
|
59
|
+
card: T | null | undefined, transition: Transition
|
|
60
|
+
): T | null => {
|
|
61
|
+
if (card != null && transition.seq <= card.seq) {
|
|
62
|
+
return card
|
|
91
63
|
}
|
|
92
|
-
if (
|
|
64
|
+
if (card == null) {
|
|
65
|
+
if (transition.action === TransitionAction.Delete) {
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
if (transition.action !== TransitionAction.Create || transition.seq !== 1) {
|
|
69
|
+
throw outOfOrder(card, transition)
|
|
70
|
+
}
|
|
71
|
+
} else if (transition.seq !== card.seq + 1) {
|
|
93
72
|
throw outOfOrder(card, transition)
|
|
94
73
|
}
|
|
95
|
-
} else if (transition.seq !== card.seq + 1) {
|
|
96
|
-
throw outOfOrder(card, transition)
|
|
97
|
-
}
|
|
98
74
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
75
|
+
if (transition.action === TransitionAction.Delete) {
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const head = Math.max(card?.head ?? card?.seq ?? 0, transition.seq)
|
|
80
|
+
const changes = structuredClone(transition.changes ?? {}) as Record<string, unknown>
|
|
102
81
|
|
|
103
|
-
|
|
104
|
-
|
|
82
|
+
if (transition.action === TransitionAction.Create) {
|
|
83
|
+
const record: Record<string, unknown> = {
|
|
84
|
+
parents: [], labels: [], fields: {}, flows: {}, status: '', intrinsic: IntrinsicStatus.Planned,
|
|
85
|
+
}
|
|
86
|
+
writeChanges(record, changes)
|
|
87
|
+
unsetOn(record, transition.unset)
|
|
88
|
+
|
|
89
|
+
return clean({
|
|
90
|
+
...record,
|
|
91
|
+
id: transition.card,
|
|
92
|
+
kind: transition.kind,
|
|
93
|
+
type: transition.type,
|
|
94
|
+
entityId: transition.entityId,
|
|
95
|
+
seq: transition.seq,
|
|
96
|
+
head,
|
|
97
|
+
createdAt: transition.at,
|
|
98
|
+
updatedAt: transition.at,
|
|
99
|
+
}) as unknown as T
|
|
100
|
+
}
|
|
105
101
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
102
|
+
const record = structuredClone(card) as unknown as Record<string, unknown>
|
|
103
|
+
if (transition.action === TransitionAction.Update || transition.action === TransitionAction.Transit) {
|
|
104
|
+
writeChanges(record, changes)
|
|
105
|
+
unsetOn(record, transition.unset)
|
|
109
106
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return clean({
|
|
114
|
-
...record,
|
|
115
|
-
id: transition.card,
|
|
116
|
-
kind: transition.kind,
|
|
117
|
-
type: transition.type,
|
|
118
|
-
entityId: transition.entityId,
|
|
119
|
-
seq: transition.seq,
|
|
120
|
-
head,
|
|
121
|
-
createdAt: transition.at,
|
|
122
|
-
updatedAt: transition.at,
|
|
123
|
-
}) as unknown as T
|
|
124
|
-
}
|
|
107
|
+
record.seq = transition.seq
|
|
108
|
+
record.head = head
|
|
109
|
+
record.updatedAt = transition.at
|
|
125
110
|
|
|
126
|
-
|
|
127
|
-
if (transition.action === TransitionAction.Update || transition.action === TransitionAction.Transit) {
|
|
128
|
-
writeChanges(record, changes)
|
|
129
|
-
unsetOn(record, transition.unset)
|
|
111
|
+
return clean(record) as unknown as T
|
|
130
112
|
}
|
|
131
|
-
record.seq = transition.seq
|
|
132
|
-
record.head = head
|
|
133
|
-
record.updatedAt = transition.at
|
|
134
|
-
|
|
135
|
-
return clean(record) as unknown as T
|
|
136
|
-
}
|
|
137
113
|
|
|
138
|
-
const edgeOf = (transition: Transition, draft: RelationshipDraft): Relationship => clean({
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
const sameEdge = (left: Pick<Relationship, 'entityId' | 'from' | 'to' | 'type'>, right: Pick<Relationship, 'entityId' | 'from' | 'to' | 'type'>): boolean =>
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
return add(transition.link == null ? [] : [transition.link])
|
|
170
|
-
case TransitionAction.Unlink: {
|
|
171
|
-
if (transition.link == null) {
|
|
172
|
-
return [...links]
|
|
114
|
+
const edgeOf = (transition: Transition, draft: RelationshipDraft): Relationship => clean({
|
|
115
|
+
entityId: transition.entityId,
|
|
116
|
+
type: draft.type,
|
|
117
|
+
from: draft.from ?? transition.card,
|
|
118
|
+
to: draft.to,
|
|
119
|
+
project: transition.project,
|
|
120
|
+
fields: draft.fields == null ? undefined : structuredClone(draft.fields),
|
|
121
|
+
createdAt: transition.at,
|
|
122
|
+
transition: transition.id,
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const sameEdge = (left: Pick<Relationship, 'entityId' | 'from' | 'to' | 'type'>, right: Pick<Relationship, 'entityId' | 'from' | 'to' | 'type'>): boolean =>
|
|
126
|
+
left.entityId === right.entityId && left.from === right.from && left.to === right.to && left.type === right.type
|
|
127
|
+
|
|
128
|
+
const applyRelationship = (links: Relationship[], transition: Transition): Relationship[] => {
|
|
129
|
+
const add = (drafts: RelationshipDraft[]): Relationship[] => drafts.reduce((result, draft) => {
|
|
130
|
+
const edge = edgeOf(transition, draft)
|
|
131
|
+
return result.some(existing => sameEdge(existing, edge)) ? result : [...result, edge]
|
|
132
|
+
}, [...links])
|
|
133
|
+
|
|
134
|
+
switch (transition.action) {
|
|
135
|
+
case TransitionAction.Create:
|
|
136
|
+
return add(transition.links ?? [])
|
|
137
|
+
case TransitionAction.Link:
|
|
138
|
+
return add(transition.link == null ? [] : [transition.link])
|
|
139
|
+
case TransitionAction.Unlink: {
|
|
140
|
+
if (transition.link == null) {
|
|
141
|
+
return [...links]
|
|
142
|
+
}
|
|
143
|
+
const edge = edgeOf(transition, transition.link)
|
|
144
|
+
return links.filter(existing => !sameEdge(existing, edge))
|
|
173
145
|
}
|
|
174
|
-
|
|
175
|
-
|
|
146
|
+
case TransitionAction.Delete:
|
|
147
|
+
return links.filter(existing => existing.entityId !== transition.entityId
|
|
148
|
+
|| (existing.from !== transition.card && existing.to !== transition.card))
|
|
149
|
+
default:
|
|
150
|
+
return [...links]
|
|
176
151
|
}
|
|
177
|
-
case TransitionAction.Delete:
|
|
178
|
-
return links.filter(existing => existing.entityId !== transition.entityId
|
|
179
|
-
|| (existing.from !== transition.card && existing.to !== transition.card))
|
|
180
|
-
default:
|
|
181
|
-
return [...links]
|
|
182
152
|
}
|
|
153
|
+
|
|
154
|
+
return { applyTransition, applyRelationship }
|
|
183
155
|
}
|
|
156
|
+
|
|
157
|
+
export const applyHelper = createApplyHelper()
|
|
158
|
+
|
|
159
|
+
/** @deprecated compat:factory-refactor — use `applyHelper.applyTransition(…)` */
|
|
160
|
+
export const applyTransition = <T extends Workcard = Workcard>(
|
|
161
|
+
card: T | null | undefined, transition: Transition
|
|
162
|
+
): T | null => applyHelper.applyTransition<T>(card, transition)
|
|
163
|
+
|
|
164
|
+
/** @deprecated compat:factory-refactor — use `applyHelper.applyRelationship(…)` */
|
|
165
|
+
export const applyRelationship = (links: Relationship[], transition: Transition): Relationship[] =>
|
|
166
|
+
applyHelper.applyRelationship(links, transition)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Card, Project, Specification, Workcard, WorkcardDraft } from '../../types.js'
|
|
2
|
+
|
|
3
|
+
/** The kind guards and the parent rules every record keeps. */
|
|
4
|
+
export interface CardHelper {
|
|
5
|
+
isProject: (card?: Workcard | null) => card is Project
|
|
6
|
+
isCard: (card?: Workcard | null) => card is Card
|
|
7
|
+
isSpecification: (card?: Workcard | null) => card is Specification
|
|
8
|
+
/** `parents` with `parent` first and no repeats — the invariant every record keeps. */
|
|
9
|
+
normalizeParents: (parent?: string | null, parents?: string[] | null) => string[]
|
|
10
|
+
/** A draft with the collections every record carries filled in. */
|
|
11
|
+
cardDefaults: (draft: WorkcardDraft) => WorkcardDraft & Required<Pick<WorkcardDraft, 'parents' | 'labels' | 'fields'>>
|
|
12
|
+
/** A transition was allocated for the card and has not been folded yet. */
|
|
13
|
+
isPending: (card: Pick<Workcard, 'seq' | 'head'>) => boolean
|
|
14
|
+
parentOf: (card: Workcard) => string | undefined
|
|
15
|
+
/**
|
|
16
|
+
* The project a card belongs to: a project is its own; a card under a project is that project's;
|
|
17
|
+
* anything deeper is its parent's project, which needs the parent to answer.
|
|
18
|
+
*/
|
|
19
|
+
projectOf: (card: Workcard, parent?: Workcard | null) => string | undefined
|
|
20
|
+
}
|
package/src/helpers/card.ts
CHANGED
|
@@ -1,42 +1,68 @@
|
|
|
1
1
|
import { WorkcardKind } from '../consts.js'
|
|
2
2
|
import type { Card, Project, Specification, Workcard, WorkcardDraft } from '../types.js'
|
|
3
|
+
import type { CardHelper } from './card/types.js'
|
|
3
4
|
|
|
4
|
-
export const
|
|
5
|
+
export const createCardHelper = (): CardHelper => {
|
|
6
|
+
const isProject = (card?: Workcard | null): card is Project => card?.kind === WorkcardKind.Project
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
const isCard = (card?: Workcard | null): card is Card => card?.kind === WorkcardKind.Card
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
const isSpecification = (card?: Workcard | null): card is Specification =>
|
|
11
|
+
card?.kind === WorkcardKind.Specification
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return card.id
|
|
36
|
-
}
|
|
37
|
-
if (parent != null) {
|
|
38
|
-
return isProject(parent) ? parent.id : parent.parent
|
|
13
|
+
const normalizeParents = (parent?: string | null, parents?: string[] | null): string[] =>
|
|
14
|
+
[...new Set([...(parent != null && parent !== '' ? [parent] : []), ...(parents ?? [])])]
|
|
15
|
+
.filter(id => id !== '')
|
|
16
|
+
|
|
17
|
+
const cardDefaults = (draft: WorkcardDraft): WorkcardDraft & Required<Pick<WorkcardDraft, 'parents' | 'labels' | 'fields'>> => ({
|
|
18
|
+
...draft,
|
|
19
|
+
parents: normalizeParents(draft.parent, draft.parents),
|
|
20
|
+
labels: [...new Set(draft.labels ?? [])],
|
|
21
|
+
fields: draft.fields ?? {},
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const isPending = (card: Pick<Workcard, 'seq' | 'head'>): boolean => (card.head ?? card.seq) > card.seq
|
|
25
|
+
|
|
26
|
+
const parentOf = (card: Workcard): string | undefined => card.parent ?? card.parents[0]
|
|
27
|
+
|
|
28
|
+
const projectOf = (card: Workcard, parent?: Workcard | null): string | undefined => {
|
|
29
|
+
if (isProject(card)) {
|
|
30
|
+
return card.id
|
|
31
|
+
}
|
|
32
|
+
if (parent != null) {
|
|
33
|
+
return isProject(parent) ? parent.id : parent.parent
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return card.parent
|
|
39
37
|
}
|
|
40
38
|
|
|
41
|
-
return
|
|
39
|
+
return { isProject, isCard, isSpecification, normalizeParents, cardDefaults, isPending, parentOf, projectOf }
|
|
42
40
|
}
|
|
41
|
+
|
|
42
|
+
export const cardHelper = createCardHelper()
|
|
43
|
+
|
|
44
|
+
/** @deprecated compat:factory-refactor — use `cardHelper.isProject(…)` */
|
|
45
|
+
export const isProject = (card?: Workcard | null): card is Project => cardHelper.isProject(card)
|
|
46
|
+
|
|
47
|
+
/** @deprecated compat:factory-refactor — use `cardHelper.isCard(…)` */
|
|
48
|
+
export const isCard = (card?: Workcard | null): card is Card => cardHelper.isCard(card)
|
|
49
|
+
|
|
50
|
+
/** @deprecated compat:factory-refactor — use `cardHelper.isSpecification(…)` */
|
|
51
|
+
export const isSpecification = (card?: Workcard | null): card is Specification => cardHelper.isSpecification(card)
|
|
52
|
+
|
|
53
|
+
/** @deprecated compat:factory-refactor — use `cardHelper.normalizeParents(…)` */
|
|
54
|
+
export const normalizeParents = (parent?: string | null, parents?: string[] | null): string[] =>
|
|
55
|
+
cardHelper.normalizeParents(parent, parents)
|
|
56
|
+
|
|
57
|
+
/** @deprecated compat:factory-refactor — use `cardHelper.cardDefaults(…)` */
|
|
58
|
+
export const cardDefaults = (draft: WorkcardDraft): WorkcardDraft & Required<Pick<WorkcardDraft, 'parents' | 'labels' | 'fields'>> =>
|
|
59
|
+
cardHelper.cardDefaults(draft)
|
|
60
|
+
|
|
61
|
+
/** @deprecated compat:factory-refactor — use `cardHelper.isPending(…)` */
|
|
62
|
+
export const isPending = (card: Pick<Workcard, 'seq' | 'head'>): boolean => cardHelper.isPending(card)
|
|
63
|
+
|
|
64
|
+
/** @deprecated compat:factory-refactor — use `cardHelper.parentOf(…)` */
|
|
65
|
+
export const parentOf = (card: Workcard): string | undefined => cardHelper.parentOf(card)
|
|
66
|
+
|
|
67
|
+
/** @deprecated compat:factory-refactor — use `cardHelper.projectOf(…)` */
|
|
68
|
+
export const projectOf = (card: Workcard, parent?: Workcard | null): string | undefined => cardHelper.projectOf(card, parent)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Never in a caller's changes: the transition itself carries them. */
|
|
2
|
+
export const IDENTITY_KEYS = ['id', 'kind', 'type', 'entityId', 'seq', 'head', 'createdAt']
|
|
3
|
+
|
|
4
|
+
/** Written once, from a create's draft; never by `changes` or `unset`, whatever the action. */
|
|
5
|
+
export const PROVENANCE_KEYS = ['createdBy']
|
|
6
|
+
|
|
7
|
+
/** Move only through `transit` (or a create's draft). */
|
|
8
|
+
export const FLOW_KEYS = ['status', 'intrinsic', 'flows', 'closedAt']
|
|
9
|
+
|
|
10
|
+
/** Computed by the executor, never supplied. */
|
|
11
|
+
export const DERIVED_KEYS = ['revision', 'bodyChars']
|
|
12
|
+
|
|
13
|
+
/** Cannot be cleared. */
|
|
14
|
+
export const REQUIRED_KEYS = ['title', 'parents', 'labels', 'fields', 'category', 'format']
|
|
15
|
+
|
|
16
|
+
/** Changing one of these produces a new revision of a revisioned document. */
|
|
17
|
+
export const CONTENT_KEYS = ['body', 'ref', 'format', 'version']
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AnyTypeSchema, SpecificationSlot, TransitionExecution, Workcard, WorkcardChanges,
|
|
3
|
+
} from '../../types.js'
|
|
4
|
+
import type { FlowLookup } from '../types.local.js'
|
|
5
|
+
|
|
6
|
+
export interface ChangeSet {
|
|
7
|
+
changes: WorkcardChanges
|
|
8
|
+
unset: string[]
|
|
9
|
+
/** Transit only: the flow that moved, and the move. */
|
|
10
|
+
flow?: string
|
|
11
|
+
from?: string
|
|
12
|
+
to?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** What a transition records — computed from an execution, and the guards it must pass first. */
|
|
16
|
+
export interface ChangesHelper {
|
|
17
|
+
/** Structural equality over JSON values. */
|
|
18
|
+
sameValue: (left: unknown, right: unknown) => boolean
|
|
19
|
+
/** A shallow merge that drops keys patched to `null`. */
|
|
20
|
+
mergeFields: (base?: Record<string, unknown> | null, patch?: Record<string, unknown> | null) => Record<string, unknown>
|
|
21
|
+
/** A copy of the record with the paths (`key`, `fields.key`, `flows.id`) removed. */
|
|
22
|
+
applyUnset: <T extends object>(record: T, unset?: string[]) => T
|
|
23
|
+
/**
|
|
24
|
+
* Refuse an execution that writes what only the transition, the flow or the executor may write.
|
|
25
|
+
*
|
|
26
|
+
* `status`/`intrinsic`/`flows`/`closedAt` move only through `transit` (a create's draft may name
|
|
27
|
+
* the initial status); `code` is fixed once minted unless the type's policy says `mutable`;
|
|
28
|
+
* `category` is fixed; `revision`/`bodyChars` are computed; `createdBy` is the create DRAFT's
|
|
29
|
+
* alone — named in `changes` or `unset` it is refused on every action, so an owner check
|
|
30
|
+
* (`card.createdBy === profileId`) can trust it.
|
|
31
|
+
*
|
|
32
|
+
* @throws {PlanningError} `planning:immutable:<field>`
|
|
33
|
+
*/
|
|
34
|
+
assertMutable: (exec: TransitionExecution, type: AnyTypeSchema) => void
|
|
35
|
+
/**
|
|
36
|
+
* The `changes`/`unset` a transition records (plan step 9).
|
|
37
|
+
*
|
|
38
|
+
* - `create`: the whole initial record from the draft (overlaid by `exec.changes`) — `parents`
|
|
39
|
+
* normalized, every flow at its initial status (the draft's `status` for the primary one),
|
|
40
|
+
* `status`/`intrinsic` mirrored, `closedAt` when it starts closed; a specification gets its
|
|
41
|
+
* format, `bodyChars` and revision 1 when the slot is revisioned.
|
|
42
|
+
* - `update`: only what differs; `null` or `exec.unset` clears a field that has a value; a moved
|
|
43
|
+
* `parent` replaces the old one in `parents`; a revisioned document whose content changed gets
|
|
44
|
+
* `revision + 1`. An update that changes nothing is empty ({@link ChangesHelper.isEmptyChange}).
|
|
45
|
+
* - `transit`: `flows[flow] = rule.to`, `status` when the flow is primary, `intrinsic` when it
|
|
46
|
+
* moves, `closedAt` set on entering closed and cleared on leaving it — plus the caller's own
|
|
47
|
+
* changes, diffed as in an update.
|
|
48
|
+
* - `link`, `unlink`, `delete`: nothing.
|
|
49
|
+
*
|
|
50
|
+
* `createdBy` comes from a create's draft only: `changes` and `unset` never carry it, on any action
|
|
51
|
+
* ({@link ChangesHelper.assertMutable} refuses an execution that tries).
|
|
52
|
+
*
|
|
53
|
+
* @throws {IllegalTransition} a transit the flow does not offer from the current status
|
|
54
|
+
* @throws {PlanningError} `malformed:*` for an execution missing what its action needs
|
|
55
|
+
*/
|
|
56
|
+
computeChanges: (
|
|
57
|
+
card: Workcard | null | undefined,
|
|
58
|
+
exec: TransitionExecution,
|
|
59
|
+
type: AnyTypeSchema,
|
|
60
|
+
registry: FlowLookup,
|
|
61
|
+
at: string,
|
|
62
|
+
opts?: { slot?: SpecificationSlot | null }
|
|
63
|
+
) => ChangeSet
|
|
64
|
+
/** Nothing to write: no changed value and nothing to clear. */
|
|
65
|
+
isEmptyChange: (set: Pick<ChangeSet, 'changes' | 'unset'>) => boolean
|
|
66
|
+
}
|