@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/changes.ts
CHANGED
|
@@ -1,333 +1,320 @@
|
|
|
1
1
|
import { IntrinsicStatus, SpecificationFormat, TransitionAction, WorkcardKind } from '../consts.js'
|
|
2
2
|
import { IllegalTransition, PlanningError } from '../errors.js'
|
|
3
|
-
import type {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
} from '
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
type FlowLookup = Pick<PlanningSchemaRegistry, 'flow'>
|
|
12
|
-
|
|
13
|
-
export interface ChangeSet {
|
|
14
|
-
changes: WorkcardChanges
|
|
15
|
-
unset: string[]
|
|
16
|
-
/** Transit only: the flow that moved, and the move. */
|
|
17
|
-
flow?: string
|
|
18
|
-
from?: string
|
|
19
|
-
to?: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Never in a caller's changes: the transition itself carries them. */
|
|
23
|
-
const IDENTITY_KEYS = ['id', 'kind', 'type', 'entityId', 'seq', 'head', 'createdAt']
|
|
24
|
-
/** Move only through `transit` (or a create's draft). */
|
|
25
|
-
const FLOW_KEYS = ['status', 'intrinsic', 'flows', 'closedAt']
|
|
26
|
-
/** Computed by the executor, never supplied. */
|
|
27
|
-
const DERIVED_KEYS = ['revision', 'bodyChars']
|
|
28
|
-
/** Cannot be cleared. */
|
|
29
|
-
const REQUIRED_KEYS = ['title', 'parents', 'labels', 'fields', 'category', 'format']
|
|
30
|
-
/** Changing one of these produces a new revision of a revisioned document. */
|
|
31
|
-
const CONTENT_KEYS = ['body', 'ref', 'format', 'version']
|
|
3
|
+
import type { AnyTypeSchema, Specification, SpecificationSlot, TransitionExecution, Workcard, WorkcardChanges, WorkcardDraft } from '../types.js'
|
|
4
|
+
import { cardHelper } from './card.js'
|
|
5
|
+
import type { ChangeSet, ChangesHelper } from './changes/types.js'
|
|
6
|
+
import { CONTENT_KEYS, DERIVED_KEYS, FLOW_KEYS, IDENTITY_KEYS, PROVENANCE_KEYS, REQUIRED_KEYS } from './changes/consts.local.js'
|
|
7
|
+
import { specificationHelper } from './specification.js'
|
|
8
|
+
import { statusHelper } from './status.js'
|
|
9
|
+
import type { FlowLookup } from './types.local.js'
|
|
32
10
|
|
|
33
11
|
const isPlainObject = (value: unknown): value is Record<string, unknown> =>
|
|
34
12
|
value != null && typeof value === 'object' && !Array.isArray(value)
|
|
35
13
|
|
|
36
|
-
/** Structural equality over JSON values. */
|
|
37
|
-
export const sameValue = (left: unknown, right: unknown): boolean => {
|
|
38
|
-
if (left === right) {
|
|
39
|
-
return true
|
|
40
|
-
}
|
|
41
|
-
if (Array.isArray(left) && Array.isArray(right)) {
|
|
42
|
-
return left.length === right.length && left.every((entry, index) => sameValue(entry, right[index]))
|
|
43
|
-
}
|
|
44
|
-
if (isPlainObject(left) && isPlainObject(right)) {
|
|
45
|
-
const keys = Object.keys(left).filter(key => left[key] !== undefined)
|
|
46
|
-
const other = Object.keys(right).filter(key => right[key] !== undefined)
|
|
47
|
-
return keys.length === other.length && keys.every(key => sameValue(left[key], right[key]))
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return false
|
|
51
|
-
}
|
|
52
|
-
|
|
53
14
|
const clean = <T extends object>(record: T): T =>
|
|
54
15
|
Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined)) as T
|
|
55
16
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const map = copy[head]
|
|
69
|
-
if (isPlainObject(map)) {
|
|
70
|
-
delete map[rest.join('.')]
|
|
71
|
-
}
|
|
72
|
-
} else {
|
|
73
|
-
delete copy[head]
|
|
17
|
+
export const createChangesHelper = (): ChangesHelper => {
|
|
18
|
+
const sameValue = (left: unknown, right: unknown): boolean => {
|
|
19
|
+
if (left === right) {
|
|
20
|
+
return true
|
|
21
|
+
}
|
|
22
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
23
|
+
return left.length === right.length && left.every((entry, index) => sameValue(entry, right[index]))
|
|
24
|
+
}
|
|
25
|
+
if (isPlainObject(left) && isPlainObject(right)) {
|
|
26
|
+
const keys = Object.keys(left).filter(key => left[key] !== undefined)
|
|
27
|
+
const other = Object.keys(right).filter(key => right[key] !== undefined)
|
|
28
|
+
return keys.length === other.length && keys.every(key => sameValue(left[key], right[key]))
|
|
74
29
|
}
|
|
30
|
+
|
|
31
|
+
return false
|
|
75
32
|
}
|
|
76
33
|
|
|
77
|
-
|
|
78
|
-
|
|
34
|
+
const mergeFields = (
|
|
35
|
+
base?: Record<string, unknown> | null, patch?: Record<string, unknown> | null
|
|
36
|
+
): Record<string, unknown> => Object.fromEntries(Object.entries({ ...(base ?? {}), ...(patch ?? {}) })
|
|
37
|
+
.filter(([, value]) => value !== null && value !== undefined))
|
|
38
|
+
|
|
39
|
+
const applyUnset = <T extends object>(record: T, unset?: string[]): T => {
|
|
40
|
+
const copy = structuredClone(record) as Record<string, unknown>
|
|
41
|
+
for (const path of unset ?? []) {
|
|
42
|
+
const [head, ...rest] = path.split('.')
|
|
43
|
+
if (rest.length > 0) {
|
|
44
|
+
const map = copy[head]
|
|
45
|
+
if (isPlainObject(map)) {
|
|
46
|
+
delete map[rest.join('.')]
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
delete copy[head]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
79
52
|
|
|
80
|
-
|
|
81
|
-
* Refuse an execution that writes what only the transition, the flow or the executor may write.
|
|
82
|
-
*
|
|
83
|
-
* `status`/`intrinsic`/`flows`/`closedAt` move only through `transit` (a create's draft may name
|
|
84
|
-
* the initial status); `code` is fixed once minted unless the type's policy says `mutable`;
|
|
85
|
-
* `category` is fixed; `revision`/`bodyChars` are computed.
|
|
86
|
-
*
|
|
87
|
-
* @throws {PlanningError} `planning:immutable:<field>`
|
|
88
|
-
*/
|
|
89
|
-
export const assertMutable = (exec: TransitionExecution, type: AnyTypeSchema): void => {
|
|
90
|
-
const create = exec.action === TransitionAction.Create
|
|
91
|
-
const codeFixed = type.code != null && type.code.mutable !== true
|
|
92
|
-
const refuse = (key: string): never => {
|
|
93
|
-
throw new PlanningError(`immutable:${FLOW_KEYS.includes(key) ? 'status' : key}`)
|
|
53
|
+
return copy as T
|
|
94
54
|
}
|
|
95
55
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
refuse(key)
|
|
56
|
+
const assertMutable = (exec: TransitionExecution, type: AnyTypeSchema): void => {
|
|
57
|
+
const create = exec.action === TransitionAction.Create
|
|
58
|
+
const codeFixed = type.code != null && type.code.mutable !== true
|
|
59
|
+
const refuse = (key: string): never => {
|
|
60
|
+
throw new PlanningError(`immutable:${FLOW_KEYS.includes(key) ? 'status' : key}`)
|
|
102
61
|
}
|
|
103
|
-
|
|
104
|
-
|
|
62
|
+
|
|
63
|
+
for (const [key, value] of Object.entries(exec.changes ?? {})) {
|
|
64
|
+
if (value === undefined) {
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
if (IDENTITY_KEYS.includes(key) || PROVENANCE_KEYS.includes(key) || DERIVED_KEYS.includes(key)) {
|
|
68
|
+
refuse(key)
|
|
69
|
+
}
|
|
70
|
+
if (!create && (FLOW_KEYS.includes(key) || key === 'category' || (key === 'code' && codeFixed))) {
|
|
71
|
+
refuse(key)
|
|
72
|
+
}
|
|
105
73
|
}
|
|
106
|
-
}
|
|
107
74
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
75
|
+
for (const path of exec.unset ?? []) {
|
|
76
|
+
const head = path.split('.')[0]
|
|
77
|
+
if (PROVENANCE_KEYS.includes(head)) {
|
|
78
|
+
refuse(head)
|
|
79
|
+
}
|
|
80
|
+
if (IDENTITY_KEYS.includes(path) || DERIVED_KEYS.includes(path) || REQUIRED_KEYS.includes(path)
|
|
81
|
+
|| FLOW_KEYS.includes(head) || (path === 'code' && codeFixed)) {
|
|
82
|
+
refuse(path)
|
|
83
|
+
}
|
|
113
84
|
}
|
|
114
85
|
}
|
|
115
|
-
}
|
|
116
86
|
|
|
117
|
-
const reach = (record: Record<string, unknown>, path: string): unknown => path.split('.')
|
|
118
|
-
|
|
87
|
+
const reach = (record: Record<string, unknown>, path: string): unknown => path.split('.')
|
|
88
|
+
.reduce<unknown>((value, step) => isPlainObject(value) ? value[step] : undefined, record)
|
|
119
89
|
|
|
120
|
-
/** Only what differs from the card; `null` becomes an unset of a field that has a value. */
|
|
121
|
-
const diffInto = (card: Workcard, changes: WorkcardChanges | undefined, skip: string[], set: ChangeSet): void => {
|
|
122
|
-
|
|
123
|
-
|
|
90
|
+
/** Only what differs from the card; `null` becomes an unset of a field that has a value. */
|
|
91
|
+
const diffInto = (card: Workcard, changes: WorkcardChanges | undefined, skip: string[], set: ChangeSet): void => {
|
|
92
|
+
const out = set.changes as Record<string, unknown>
|
|
93
|
+
const current = card as unknown as Record<string, unknown>
|
|
124
94
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
95
|
+
for (const [key, value] of Object.entries(changes ?? {})) {
|
|
96
|
+
if (value === undefined || IDENTITY_KEYS.includes(key) || PROVENANCE_KEYS.includes(key) || skip.includes(key)) {
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
if (key === 'fields' || key === 'flows') {
|
|
100
|
+
const own = (current[key] ?? {}) as Record<string, unknown>
|
|
101
|
+
const patch: Record<string, unknown> = {}
|
|
102
|
+
for (const [entry, entryValue] of Object.entries(value as Record<string, unknown> ?? {})) {
|
|
103
|
+
if (entryValue === undefined) {
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
if (entryValue === null) {
|
|
107
|
+
if (own[entry] !== undefined) {
|
|
108
|
+
set.unset.push(`${key}.${entry}`)
|
|
109
|
+
}
|
|
110
|
+
} else if (!sameValue(own[entry], entryValue)) {
|
|
111
|
+
patch[entry] = entryValue
|
|
139
112
|
}
|
|
140
|
-
} else if (!sameValue(own[entry], entryValue)) {
|
|
141
|
-
patch[entry] = entryValue
|
|
142
113
|
}
|
|
114
|
+
if (Object.keys(patch).length > 0) {
|
|
115
|
+
out[key] = patch
|
|
116
|
+
}
|
|
117
|
+
continue
|
|
143
118
|
}
|
|
144
|
-
if (
|
|
145
|
-
|
|
119
|
+
if (value === null) {
|
|
120
|
+
if (current[key] != null) {
|
|
121
|
+
set.unset.push(key)
|
|
122
|
+
}
|
|
123
|
+
continue
|
|
146
124
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (value === null) {
|
|
150
|
-
if (current[key] != null) {
|
|
151
|
-
set.unset.push(key)
|
|
125
|
+
if (!sameValue(current[key], value)) {
|
|
126
|
+
out[key] = value
|
|
152
127
|
}
|
|
153
|
-
continue
|
|
154
|
-
}
|
|
155
|
-
if (!sameValue(current[key], value)) {
|
|
156
|
-
out[key] = value
|
|
157
128
|
}
|
|
158
129
|
}
|
|
159
|
-
}
|
|
160
130
|
|
|
161
|
-
const unsetInto = (card: Workcard, unset: string[] | undefined, set: ChangeSet): void => {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
131
|
+
const unsetInto = (card: Workcard, unset: string[] | undefined, set: ChangeSet): void => {
|
|
132
|
+
for (const path of unset ?? []) {
|
|
133
|
+
if (PROVENANCE_KEYS.includes(path.split('.')[0])) {
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
if (reach(card as unknown as Record<string, unknown>, path) !== undefined && !set.unset.includes(path)) {
|
|
137
|
+
set.unset.push(path)
|
|
138
|
+
}
|
|
165
139
|
}
|
|
166
140
|
}
|
|
167
|
-
}
|
|
168
141
|
|
|
169
|
-
/** A moved primary parent replaces the old one in `parents`. */
|
|
170
|
-
const reparent = (card: Workcard, set: ChangeSet): void => {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
142
|
+
/** A moved primary parent replaces the old one in `parents`. */
|
|
143
|
+
const reparent = (card: Workcard, set: ChangeSet): void => {
|
|
144
|
+
const out = set.changes
|
|
145
|
+
const moved = out.parent !== undefined || set.unset.includes('parent')
|
|
146
|
+
if (!moved || out.parents !== undefined) {
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
const parents = cardHelper.normalizeParents(out.parent, card.parents.filter(parent => parent !== card.parent))
|
|
150
|
+
if (!sameValue(parents, card.parents)) {
|
|
151
|
+
out.parents = parents
|
|
152
|
+
}
|
|
179
153
|
}
|
|
180
|
-
}
|
|
181
154
|
|
|
182
|
-
const reviseInto = (card: Workcard, slot: SpecificationSlot | null | undefined, set: ChangeSet): void => {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
155
|
+
const reviseInto = (card: Workcard, slot: SpecificationSlot | null | undefined, set: ChangeSet): void => {
|
|
156
|
+
if (card.kind !== WorkcardKind.Specification) {
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
const out = set.changes
|
|
160
|
+
if (out.body !== undefined) {
|
|
161
|
+
out.bodyChars = specificationHelper.bodyCharsOf(out.body)
|
|
162
|
+
} else if (set.unset.includes('body') && !set.unset.includes('bodyChars')) {
|
|
163
|
+
set.unset.push('bodyChars')
|
|
164
|
+
}
|
|
165
|
+
const revised = CONTENT_KEYS.some(key => (out as Record<string, unknown>)[key] !== undefined || set.unset.includes(key))
|
|
166
|
+
if (revised) {
|
|
167
|
+
const revision = specificationHelper.nextRevision(card as Specification, slot)
|
|
168
|
+
if (revision != null) {
|
|
169
|
+
out.revision = revision
|
|
170
|
+
}
|
|
197
171
|
}
|
|
198
172
|
}
|
|
199
|
-
}
|
|
200
173
|
|
|
201
|
-
const createOf = (
|
|
202
|
-
|
|
203
|
-
): ChangeSet => {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
174
|
+
const createOf = (
|
|
175
|
+
exec: TransitionExecution, type: AnyTypeSchema, registry: FlowLookup, at: string, slot?: SpecificationSlot | null
|
|
176
|
+
): ChangeSet => {
|
|
177
|
+
if (typeof exec.card === 'string') {
|
|
178
|
+
throw new PlanningError('malformed:create-without-draft')
|
|
179
|
+
}
|
|
180
|
+
const draft: WorkcardDraft = exec.card
|
|
181
|
+
const overlay = Object.fromEntries(Object.entries(exec.changes ?? {})
|
|
182
|
+
.filter(([key]) => !FLOW_KEYS.includes(key) && !IDENTITY_KEYS.includes(key) && !PROVENANCE_KEYS.includes(key)
|
|
183
|
+
&& !DERIVED_KEYS.includes(key))
|
|
184
|
+
) as WorkcardChanges
|
|
185
|
+
|
|
186
|
+
const parent = overlay.parent ?? draft.parent
|
|
187
|
+
const flows = statusHelper.initialFlowsOf(type, registry, draft.status ?? exec.changes?.status)
|
|
188
|
+
const intrinsic = statusHelper.resolveIntrinsic(type, flows, registry)
|
|
189
|
+
|
|
190
|
+
const record: Record<string, unknown> = {
|
|
191
|
+
title: draft.title,
|
|
192
|
+
description: draft.description,
|
|
193
|
+
code: draft.code,
|
|
194
|
+
order: draft.order,
|
|
195
|
+
createdBy: draft.createdBy,
|
|
196
|
+
...overlay,
|
|
197
|
+
labels: [...new Set(overlay.labels ?? draft.labels ?? [])],
|
|
198
|
+
parent,
|
|
199
|
+
parents: cardHelper.normalizeParents(parent, overlay.parents ?? draft.parents),
|
|
200
|
+
fields: mergeFields(draft.fields, overlay.fields),
|
|
201
|
+
flows,
|
|
202
|
+
status: flows[statusHelper.primaryFlowOf(type)],
|
|
203
|
+
intrinsic,
|
|
204
|
+
closedAt: intrinsic === IntrinsicStatus.Closed ? at : undefined,
|
|
205
|
+
}
|
|
232
206
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
207
|
+
if (draft.kind === WorkcardKind.Specification) {
|
|
208
|
+
const body = overlay.body ?? draft.body
|
|
209
|
+
Object.assign(record, {
|
|
210
|
+
category: overlay.category ?? draft.category ?? slot?.category,
|
|
211
|
+
format: overlay.format ?? draft.format ?? slot?.format ?? SpecificationFormat.Markdown,
|
|
212
|
+
body,
|
|
213
|
+
ref: overlay.ref ?? draft.ref,
|
|
214
|
+
version: overlay.version ?? draft.version ?? slot?.version,
|
|
215
|
+
bodyChars: specificationHelper.bodyCharsOf(body),
|
|
216
|
+
revision: specificationHelper.nextRevision(null, slot),
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return { changes: clean(record) as WorkcardChanges, unset: [] }
|
|
244
221
|
}
|
|
245
222
|
|
|
246
|
-
|
|
247
|
-
|
|
223
|
+
const computeChanges = (
|
|
224
|
+
card: Workcard | null | undefined,
|
|
225
|
+
exec: TransitionExecution,
|
|
226
|
+
type: AnyTypeSchema,
|
|
227
|
+
registry: FlowLookup,
|
|
228
|
+
at: string,
|
|
229
|
+
opts?: { slot?: SpecificationSlot | null }
|
|
230
|
+
): ChangeSet => {
|
|
231
|
+
if (exec.action === TransitionAction.Create) {
|
|
232
|
+
return createOf(exec, type, registry, at, opts?.slot)
|
|
233
|
+
}
|
|
234
|
+
const set: ChangeSet = { changes: {}, unset: [] }
|
|
235
|
+
if (exec.action !== TransitionAction.Update && exec.action !== TransitionAction.Transit) {
|
|
236
|
+
return set
|
|
237
|
+
}
|
|
238
|
+
if (card == null) {
|
|
239
|
+
throw new PlanningError(`malformed:${exec.action}-without-card`)
|
|
240
|
+
}
|
|
248
241
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
exec: TransitionExecution,
|
|
270
|
-
type: AnyTypeSchema,
|
|
271
|
-
registry: FlowLookup,
|
|
272
|
-
at: string,
|
|
273
|
-
opts?: { slot?: SpecificationSlot | null }
|
|
274
|
-
): ChangeSet => {
|
|
275
|
-
if (exec.action === TransitionAction.Create) {
|
|
276
|
-
return createOf(exec, type, registry, at, opts?.slot)
|
|
277
|
-
}
|
|
278
|
-
const set: ChangeSet = { changes: {}, unset: [] }
|
|
279
|
-
if (exec.action !== TransitionAction.Update && exec.action !== TransitionAction.Transit) {
|
|
280
|
-
return set
|
|
281
|
-
}
|
|
282
|
-
if (card == null) {
|
|
283
|
-
throw new PlanningError(`malformed:${exec.action}-without-card`)
|
|
284
|
-
}
|
|
242
|
+
if (exec.action === TransitionAction.Update) {
|
|
243
|
+
diffInto(card, exec.changes, [], set)
|
|
244
|
+
unsetInto(card, exec.unset, set)
|
|
245
|
+
reparent(card, set)
|
|
246
|
+
reviseInto(card, opts?.slot, set)
|
|
247
|
+
|
|
248
|
+
return set
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (exec.transition == null) {
|
|
252
|
+
throw new PlanningError('malformed:transit-without-transition')
|
|
253
|
+
}
|
|
254
|
+
const primary = statusHelper.primaryFlowOf(type)
|
|
255
|
+
const flowId = exec.flow ?? primary
|
|
256
|
+
const flow = registry.flow(flowId)
|
|
257
|
+
const from = card.flows[flowId] ?? (flowId === primary ? card.status : statusHelper.initialStatusOf(flow))
|
|
258
|
+
const rule = statusHelper.ruleOf(flow, exec.transition, from)
|
|
259
|
+
if (rule == null) {
|
|
260
|
+
throw new IllegalTransition(`${flowId}:${exec.transition}:${from}`)
|
|
261
|
+
}
|
|
285
262
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
unsetInto(card, exec.unset, set)
|
|
263
|
+
diffInto(card, exec.changes, FLOW_KEYS, set)
|
|
264
|
+
unsetInto(card, exec.unset?.filter(path => !FLOW_KEYS.includes(path.split('.')[0])), set)
|
|
289
265
|
reparent(card, set)
|
|
290
266
|
reviseInto(card, opts?.slot, set)
|
|
291
267
|
|
|
292
|
-
|
|
293
|
-
|
|
268
|
+
const flows = { ...card.flows, [flowId]: rule.to }
|
|
269
|
+
set.changes.flows = { [flowId]: rule.to }
|
|
270
|
+
if (flowId === primary) {
|
|
271
|
+
set.changes.status = rule.to
|
|
272
|
+
}
|
|
273
|
+
const intrinsic = statusHelper.resolveIntrinsic(type, flows, registry)
|
|
274
|
+
if (intrinsic !== card.intrinsic) {
|
|
275
|
+
set.changes.intrinsic = intrinsic
|
|
276
|
+
}
|
|
277
|
+
if (intrinsic === IntrinsicStatus.Closed && card.intrinsic !== IntrinsicStatus.Closed) {
|
|
278
|
+
set.changes.closedAt = at
|
|
279
|
+
} else if (intrinsic !== IntrinsicStatus.Closed && card.closedAt != null) {
|
|
280
|
+
set.unset.push('closedAt')
|
|
281
|
+
}
|
|
294
282
|
|
|
295
|
-
|
|
296
|
-
throw new PlanningError('malformed:transit-without-transition')
|
|
297
|
-
}
|
|
298
|
-
const primary = primaryFlowOf(type)
|
|
299
|
-
const flowId = exec.flow ?? primary
|
|
300
|
-
const flow = registry.flow(flowId)
|
|
301
|
-
const from = card.flows[flowId] ?? (flowId === primary ? card.status : initialStatusOf(flow))
|
|
302
|
-
const rule = ruleOf(flow, exec.transition, from)
|
|
303
|
-
if (rule == null) {
|
|
304
|
-
throw new IllegalTransition(`${flowId}:${exec.transition}:${from}`)
|
|
283
|
+
return { ...set, flow: flowId, from, to: rule.to }
|
|
305
284
|
}
|
|
306
285
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
reviseInto(card, opts?.slot, set)
|
|
311
|
-
|
|
312
|
-
const flows = { ...card.flows, [flowId]: rule.to }
|
|
313
|
-
set.changes.flows = { [flowId]: rule.to }
|
|
314
|
-
if (flowId === primary) {
|
|
315
|
-
set.changes.status = rule.to
|
|
316
|
-
}
|
|
317
|
-
const intrinsic = resolveIntrinsic(type, flows, registry)
|
|
318
|
-
if (intrinsic !== card.intrinsic) {
|
|
319
|
-
set.changes.intrinsic = intrinsic
|
|
320
|
-
}
|
|
321
|
-
if (intrinsic === IntrinsicStatus.Closed && card.intrinsic !== IntrinsicStatus.Closed) {
|
|
322
|
-
set.changes.closedAt = at
|
|
323
|
-
} else if (intrinsic !== IntrinsicStatus.Closed && card.closedAt != null) {
|
|
324
|
-
set.unset.push('closedAt')
|
|
325
|
-
}
|
|
286
|
+
const isEmptyChange = (set: Pick<ChangeSet, 'changes' | 'unset'>): boolean =>
|
|
287
|
+
set.unset.length === 0 && Object.entries(set.changes).every(([, value]) =>
|
|
288
|
+
value === undefined || (isPlainObject(value) && Object.keys(value).length === 0))
|
|
326
289
|
|
|
327
|
-
return {
|
|
290
|
+
return { sameValue, mergeFields, applyUnset, assertMutable, computeChanges, isEmptyChange }
|
|
328
291
|
}
|
|
329
292
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
293
|
+
export const changesHelper = createChangesHelper()
|
|
294
|
+
|
|
295
|
+
/** @deprecated compat:factory-refactor — use `changesHelper.sameValue(…)` */
|
|
296
|
+
export const sameValue = (left: unknown, right: unknown): boolean => changesHelper.sameValue(left, right)
|
|
297
|
+
|
|
298
|
+
/** @deprecated compat:factory-refactor — use `changesHelper.mergeFields(…)` */
|
|
299
|
+
export const mergeFields = (
|
|
300
|
+
base?: Record<string, unknown> | null, patch?: Record<string, unknown> | null
|
|
301
|
+
): Record<string, unknown> => changesHelper.mergeFields(base, patch)
|
|
302
|
+
|
|
303
|
+
/** @deprecated compat:factory-refactor — use `changesHelper.applyUnset(…)` */
|
|
304
|
+
export const applyUnset = <T extends object>(record: T, unset?: string[]): T => changesHelper.applyUnset<T>(record, unset)
|
|
305
|
+
|
|
306
|
+
/** @deprecated compat:factory-refactor — use `changesHelper.assertMutable(…)` */
|
|
307
|
+
export const assertMutable = (exec: TransitionExecution, type: AnyTypeSchema): void => changesHelper.assertMutable(exec, type)
|
|
308
|
+
|
|
309
|
+
/** @deprecated compat:factory-refactor — use `changesHelper.computeChanges(…)` */
|
|
310
|
+
export const computeChanges = (
|
|
311
|
+
card: Workcard | null | undefined,
|
|
312
|
+
exec: TransitionExecution,
|
|
313
|
+
type: AnyTypeSchema,
|
|
314
|
+
registry: FlowLookup,
|
|
315
|
+
at: string,
|
|
316
|
+
opts?: { slot?: SpecificationSlot | null }
|
|
317
|
+
): ChangeSet => changesHelper.computeChanges(card, exec, type, registry, at, opts)
|
|
318
|
+
|
|
319
|
+
/** @deprecated compat:factory-refactor — use `changesHelper.isEmptyChange(…)` */
|
|
320
|
+
export const isEmptyChange = (set: Pick<ChangeSet, 'changes' | 'unset'>): boolean => changesHelper.isEmptyChange(set)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { CodePolicy, WorkcardDraft } from '../../types.js'
|
|
2
|
+
|
|
3
|
+
export interface MintCodeOptions {
|
|
4
|
+
/** Slug style: the text the slug is derived from (a title). A word slug when omitted. */
|
|
5
|
+
seed?: string
|
|
6
|
+
/** Sequential style: how many codes the scope already holds. */
|
|
7
|
+
count?: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** A card's human code: minted under the type's policy, unique within its scope. */
|
|
11
|
+
export interface CodeHelper {
|
|
12
|
+
/** `Some Title!` → `some-title`. */
|
|
13
|
+
slugOf: (text: string, max?: number) => string
|
|
14
|
+
/**
|
|
15
|
+
* Mint a code under a policy, asking `taken` for each candidate.
|
|
16
|
+
*
|
|
17
|
+
* `random` draws `prefix + Base58(length)` (upper-cased when asked) afresh per attempt; `slug`
|
|
18
|
+
* walks `base`, `base-2`, `base-3` from the seed (a word slug without one); `sequential` counts on
|
|
19
|
+
* from `opts.count`, zero-padded to `length`. After `attempts` taken candidates it refuses.
|
|
20
|
+
*
|
|
21
|
+
* @throws {CodeTaken}
|
|
22
|
+
*/
|
|
23
|
+
mintCode: (
|
|
24
|
+
policy: CodePolicy,
|
|
25
|
+
taken: (code: string) => boolean | Promise<boolean>,
|
|
26
|
+
attempts?: number,
|
|
27
|
+
opts?: MintCodeOptions
|
|
28
|
+
) => Promise<string>
|
|
29
|
+
/** Where a draft's code must be unique: its parent, or the whole entity. */
|
|
30
|
+
codeScopeOf: (policy: CodePolicy, draft: Pick<WorkcardDraft, 'parent'>) => { parent?: string }
|
|
31
|
+
}
|