@owlmeans/planning 0.1.18-rc.8 → 0.1.18
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 +122 -29
- package/agent-meta/manifest.json +131 -4
- package/agent-meta/skills/planning/SKILL.md +78 -171
- package/agent-meta/skills/planning/parts/collaboration.md +67 -0
- package/agent-meta/skills/planning/parts/errors.md +51 -0
- package/agent-meta/skills/planning/parts/fold.md +69 -0
- package/agent-meta/skills/planning/parts/mounting.md +64 -0
- package/agent-meta/skills/planning/parts/types.md +141 -0
- package/build/aliases/types.d.ts +72 -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 +28 -0
- package/build/aliases.js.map +1 -0
- package/build/consts.d.ts +43 -34
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +49 -25
- package/build/consts.js.map +1 -1
- package/build/entrypoints/types.d.ts +105 -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 +24 -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/app/consts.local.d.ts +5 -0
- package/build/helpers/app/consts.local.d.ts.map +1 -0
- package/build/helpers/app/consts.local.js +5 -0
- package/build/helpers/app/consts.local.js.map +1 -0
- package/build/helpers/app/types.d.ts +53 -0
- package/build/helpers/app/types.d.ts.map +1 -0
- package/build/helpers/app/types.js +2 -0
- package/build/helpers/app/types.js.map +1 -0
- package/build/helpers/app.d.ts +4 -0
- package/build/helpers/app.d.ts.map +1 -0
- package/build/helpers/app.js +139 -0
- package/build/helpers/app.js.map +1 -0
- 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 +127 -144
- 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 +237 -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 +16 -0
- package/build/helpers/index.d.ts.map +1 -1
- package/build/helpers/index.js +4 -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/reply/consts.d.ts +2 -0
- package/build/helpers/reply/consts.d.ts.map +1 -0
- package/build/helpers/reply/consts.js +2 -0
- package/build/helpers/reply/consts.js.map +1 -0
- package/build/helpers/reply/types.d.ts +10 -0
- package/build/helpers/reply/types.d.ts.map +1 -0
- package/build/helpers/reply/types.js +2 -0
- package/build/helpers/reply/types.js.map +1 -0
- package/build/helpers/reply.d.ts +5 -0
- package/build/helpers/reply.d.ts.map +1 -0
- package/build/helpers/reply.js +29 -0
- package/build/helpers/reply.js.map +1 -0
- package/build/helpers/scoped/types.d.ts +73 -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 +25 -0
- package/build/helpers/scoped.d.ts.map +1 -0
- package/build/helpers/scoped.js +223 -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 +31 -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 +79 -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 +216 -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 +5 -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 +3 -3
- package/build/i18n.js.map +1 -1
- package/build/index.d.ts +7 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3 -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/registry.d.ts +3 -2
- package/build/registry.d.ts.map +1 -1
- package/build/registry.js +51 -3
- package/build/registry.js.map +1 -1
- package/build/resources/mentions.d.ts +4 -0
- package/build/resources/mentions.d.ts.map +1 -0
- package/build/resources/mentions.js +15 -0
- package/build/resources/mentions.js.map +1 -0
- package/build/resources/protocol/types.d.ts +30 -0
- package/build/resources/protocol/types.d.ts.map +1 -0
- package/build/resources/protocol/types.js +2 -0
- package/build/resources/protocol/types.js.map +1 -0
- package/build/resources/protocol.d.ts +6 -0
- package/build/resources/protocol.d.ts.map +1 -0
- package/build/resources/protocol.js +31 -0
- package/build/resources/protocol.js.map +1 -0
- package/build/resources/types.d.ts +146 -0
- package/build/resources/types.d.ts.map +1 -0
- package/build/resources/types.js +2 -0
- package/build/resources/types.js.map +1 -0
- package/build/resources/types.local.d.ts +6 -0
- package/build/resources/types.local.d.ts.map +1 -0
- package/build/resources/types.local.js +2 -0
- package/build/resources/types.local.js.map +1 -0
- package/build/schemas.d.ts +21 -2
- package/build/schemas.d.ts.map +1 -1
- package/build/schemas.js +164 -2
- package/build/schemas.js.map +1 -1
- package/build/types.d.ts +330 -151
- 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 +13 -13
- package/src/aliases/types.ts +41 -0
- package/src/aliases.ts +34 -0
- package/src/consts.ts +50 -25
- package/src/entrypoints/types.ts +77 -0
- package/src/entrypoints.ts +53 -24
- package/src/errors.ts +71 -0
- package/src/helpers/app/consts.local.ts +5 -0
- package/src/helpers/app/types.ts +54 -0
- package/src/helpers/app.ts +158 -0
- package/src/helpers/apply/consts.local.ts +10 -0
- package/src/helpers/apply/types.ts +32 -0
- package/src/helpers/apply.ts +141 -153
- 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 +270 -280
- package/src/helpers/code/types.ts +31 -0
- package/src/helpers/code.ts +52 -48
- package/src/helpers/index.ts +17 -0
- package/src/helpers/query/types.ts +26 -0
- package/src/helpers/query.ts +144 -314
- package/src/helpers/reply/consts.ts +1 -0
- package/src/helpers/reply/types.ts +13 -0
- package/src/helpers/reply.ts +27 -0
- package/src/helpers/scoped/types.ts +78 -0
- package/src/helpers/scoped.ts +281 -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 +31 -0
- package/src/helpers/validate.ts +89 -77
- package/src/helpers/wire/types.ts +31 -0
- package/src/helpers/wire.ts +252 -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 +5 -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 +4 -3
- package/src/index.ts +8 -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/registry.ts +55 -4
- package/src/resources/mentions.ts +11 -0
- package/src/resources/protocol/types.ts +21 -0
- package/src/resources/protocol.ts +36 -0
- package/src/resources/types.local.ts +5 -0
- package/src/resources/types.ts +148 -0
- package/src/schemas.ts +179 -4
- package/src/types.ts +334 -130
- package/src/utils.ts +9 -0
- package/tests/app.spec.ts +188 -0
- package/tests/apply.spec.ts +95 -31
- package/tests/code.spec.ts +9 -9
- package/tests/csp.spec.ts +122 -0
- package/tests/entrypoints.spec.ts +18 -2
- package/tests/error-status.spec.ts +6 -1
- package/tests/models.spec.ts +24 -16
- package/tests/query.spec.ts +13 -15
- package/tests/schemas.spec.ts +8 -8
- package/tests/scoped.spec.ts +161 -0
- package/tests/status.spec.ts +31 -22
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import type { AssigneeTypeSchema } from '../resources/types.js'
|
|
2
|
+
import { AssigneeTypeSchemaSchema } from '../schemas.js'
|
|
3
|
+
import { memoHelper } from '@owlmeans/context'
|
|
4
|
+
import { PlanningSchemaKind, SchemaOrigin, WorkcardKind } from '../consts.js'
|
|
5
|
+
import { PlanningUnsupported, SchemaInvalid, SchemaSealed } from '../errors.js'
|
|
6
|
+
import { makeSchemaRegistry } from '../registry.js'
|
|
7
|
+
import { StatusFlowSchemaSchema, WorkcardTypeSchemaSchema } from '../schemas.js'
|
|
8
|
+
import type {
|
|
9
|
+
AnyTypeSchema, PlanningAppDeclarations, PlanningSchemaBundle, PlanningSchemaRegistry, SchemaScope, ScopedSchemaBundle,
|
|
10
|
+
ScopedSchemaRecord, ScopedSchemaRegistry, StatusFlowSchema, WorkcardTypeSchema,
|
|
11
|
+
} from '../types.js'
|
|
12
|
+
import { appSchemaHelper } from './app.js'
|
|
13
|
+
import type { ScopedSchemaHelper } from './scoped/types.js'
|
|
14
|
+
import type { Candidate, Resolved } from './types.local.js'
|
|
15
|
+
import { makeAjv, validateHelper } from './validate.js'
|
|
16
|
+
|
|
17
|
+
const messageOf = (error: unknown): string => error instanceof Error ? error.message : `${error}`
|
|
18
|
+
|
|
19
|
+
export const createScopedSchemaHelper = (): ScopedSchemaHelper => {
|
|
20
|
+
const checkers = memoHelper.once(() => {
|
|
21
|
+
const ajv = makeAjv()
|
|
22
|
+
return { flow: ajv.compile(StatusFlowSchemaSchema), type: ajv.compile(WorkcardTypeSchemaSchema), assignee: ajv.compile(AssigneeTypeSchemaSchema) }
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const schemaRecordKey = (record: Pick<ScopedSchemaRecord, 'entityId' | 'project' | 'kind' | 'key'>): string =>
|
|
26
|
+
`${record.entityId}\u0000${record.project ?? ''}\u0000${record.kind}\u0000${record.key}`
|
|
27
|
+
|
|
28
|
+
const schemaKeyOf = (kind: PlanningSchemaKind, definition: WorkcardTypeSchema | StatusFlowSchema | AssigneeTypeSchema): string =>
|
|
29
|
+
kind !== PlanningSchemaKind.Flow ? (definition as WorkcardTypeSchema).type : (definition as StatusFlowSchema).id
|
|
30
|
+
|
|
31
|
+
const openType = (type: AnyTypeSchema): boolean => type.kind === WorkcardKind.Card && type.overridable === true
|
|
32
|
+
|
|
33
|
+
const assertOverridable = (
|
|
34
|
+
code: Pick<PlanningSchemaRegistry, 'has' | 'type' | 'flows' | 'assigneeTypes'>, kind: PlanningSchemaKind, key: string
|
|
35
|
+
): void => {
|
|
36
|
+
if (kind === PlanningSchemaKind.AssigneeType) {
|
|
37
|
+
const schema = code.assigneeTypes().find(schema => schema.type === key)
|
|
38
|
+
if (schema != null && schema.overridable !== true) throw new SchemaSealed(`assignee:${key}`)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
if (kind === PlanningSchemaKind.Type) {
|
|
42
|
+
if (code.has(key) && !openType(code.type(key))) {
|
|
43
|
+
throw new SchemaSealed(`type:${key}`)
|
|
44
|
+
}
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
const flow = code.flows().find(entry => entry.id === key)
|
|
48
|
+
if (flow != null && flow.overridable !== true) {
|
|
49
|
+
throw new SchemaSealed(`flow:${key}`)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const assertAssigneeTypeSchema = (schema: AssigneeTypeSchema): void => {
|
|
54
|
+
const validate = checkers().assignee
|
|
55
|
+
if (!validate(schema)) throw new SchemaInvalid(`assignee:${(schema as Partial<AssigneeTypeSchema>)?.type}:${validateHelper.ajvErrorText(validate.errors)}`)
|
|
56
|
+
try { makeAjv().compile(schema.fields) } catch (error) { throw new SchemaInvalid(`assignee:${schema.type}:fields:${messageOf(error)}`) }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const assertFlowSchema = (flow: StatusFlowSchema, app?: PlanningAppDeclarations): void => {
|
|
60
|
+
const validate = checkers().flow
|
|
61
|
+
if (!validate(flow)) {
|
|
62
|
+
throw new SchemaInvalid(`flow:${(flow as Partial<StatusFlowSchema>)?.id ?? ''}:${validateHelper.ajvErrorText(validate.errors)}`)
|
|
63
|
+
}
|
|
64
|
+
const keys = new Set<string>()
|
|
65
|
+
for (const status of flow.statuses) {
|
|
66
|
+
if (keys.has(status.key)) {
|
|
67
|
+
throw new SchemaInvalid(`flow:${flow.id}:status-repeated:${status.key}`)
|
|
68
|
+
}
|
|
69
|
+
keys.add(status.key)
|
|
70
|
+
}
|
|
71
|
+
for (const rule of flow.transitions) {
|
|
72
|
+
if (!keys.has(rule.to)) {
|
|
73
|
+
throw new SchemaInvalid(`flow:${flow.id}:${rule.name}:to:${rule.to}`)
|
|
74
|
+
}
|
|
75
|
+
if (typeof rule.from === 'string') {
|
|
76
|
+
if (rule.from !== '*') {
|
|
77
|
+
throw new SchemaInvalid(`flow:${flow.id}:${rule.name}:from:${rule.from}`)
|
|
78
|
+
}
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
const unknown = rule.from.find(status => !keys.has(status))
|
|
82
|
+
if (unknown != null) {
|
|
83
|
+
throw new SchemaInvalid(`flow:${flow.id}:${rule.name}:from:${unknown}`)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
appSchemaHelper.assertBag(PlanningSchemaKind.Flow, flow.id, flow.app, app)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const assertTypeSchema = (
|
|
90
|
+
type: WorkcardTypeSchema, flows: Pick<PlanningSchemaRegistry, 'flow'>, app?: PlanningAppDeclarations
|
|
91
|
+
): void => {
|
|
92
|
+
const validate = checkers().type
|
|
93
|
+
if (!validate(type)) {
|
|
94
|
+
throw new SchemaInvalid(`type:${(type as Partial<WorkcardTypeSchema>)?.type ?? ''}:${validateHelper.ajvErrorText(validate.errors)}`)
|
|
95
|
+
}
|
|
96
|
+
if (type.kind !== WorkcardKind.Card) {
|
|
97
|
+
throw new SchemaInvalid(`type:${type.type}:kind:${type.kind}`)
|
|
98
|
+
}
|
|
99
|
+
appSchemaHelper.assertBag(PlanningSchemaKind.Type, type.type, type.app, app)
|
|
100
|
+
try {
|
|
101
|
+
makeAjv().compile(appSchemaHelper.fieldsSchemaOf(type, app))
|
|
102
|
+
} catch (error) {
|
|
103
|
+
throw new SchemaInvalid(`type:${type.type}:fields:${messageOf(error)}`)
|
|
104
|
+
}
|
|
105
|
+
const seen = new Set<string>()
|
|
106
|
+
for (const id of type.flows) {
|
|
107
|
+
if (seen.has(id)) {
|
|
108
|
+
throw new SchemaInvalid(`type:${type.type}:flow-repeated:${id}`)
|
|
109
|
+
}
|
|
110
|
+
seen.add(id)
|
|
111
|
+
try {
|
|
112
|
+
flows.flow(id)
|
|
113
|
+
} catch {
|
|
114
|
+
throw new SchemaInvalid(`type:${type.type}:flow:${id}`)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const resolveKind = <T extends WorkcardTypeSchema | StatusFlowSchema | AssigneeTypeSchema | AnyTypeSchema>(
|
|
120
|
+
kind: PlanningSchemaKind,
|
|
121
|
+
code: readonly T[],
|
|
122
|
+
keyOf: (definition: T) => string,
|
|
123
|
+
open: (definition: T) => boolean,
|
|
124
|
+
valid: (definition: T) => boolean,
|
|
125
|
+
layers: { entity: readonly ScopedSchemaRecord[], project: readonly ScopedSchemaRecord[] },
|
|
126
|
+
): Resolved<T> => {
|
|
127
|
+
const coded = new Map(code.map(definition => [keyOf(definition), definition]))
|
|
128
|
+
const byKey = (records: readonly ScopedSchemaRecord[]) => new Map(records
|
|
129
|
+
.filter(record => record.kind === kind)
|
|
130
|
+
.map(record => [record.key, record] as const)
|
|
131
|
+
.filter(([key, record]) => {
|
|
132
|
+
const definition = record.definition as T
|
|
133
|
+
return valid(definition) && keyOf(definition) === key
|
|
134
|
+
}))
|
|
135
|
+
const entity = byKey(layers.entity)
|
|
136
|
+
const project = byKey(layers.project)
|
|
137
|
+
const added = [...new Set([...entity.keys(), ...project.keys()])].filter(key => !coded.has(key)).sort()
|
|
138
|
+
|
|
139
|
+
const stamped = (record: ScopedSchemaRecord): T => ({ ...(record.definition as T), version: record.version })
|
|
140
|
+
const result: Resolved<T> = { definitions: [], origins: {}, retired: [] }
|
|
141
|
+
for (const key of [...coded.keys(), ...added]) {
|
|
142
|
+
const declared = coded.get(key)
|
|
143
|
+
const candidates: Candidate<T>[] = []
|
|
144
|
+
if (declared == null || open(declared)) {
|
|
145
|
+
const own = project.get(key)
|
|
146
|
+
if (own != null) {
|
|
147
|
+
candidates.push({ origin: SchemaOrigin.Project, definition: stamped(own), retired: own.retired === true })
|
|
148
|
+
}
|
|
149
|
+
const shared = entity.get(key)
|
|
150
|
+
if (shared != null) {
|
|
151
|
+
candidates.push({ origin: SchemaOrigin.Entity, definition: stamped(shared), retired: shared.retired === true })
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (declared != null) {
|
|
155
|
+
candidates.push({ origin: SchemaOrigin.Code, definition: declared, retired: false })
|
|
156
|
+
}
|
|
157
|
+
// A retired record gives way to a live declaration below it; only when every layer holding the
|
|
158
|
+
// key retired it does the key resolve retired — still there for the cards that use it.
|
|
159
|
+
const chosen = candidates.find(candidate => !candidate.retired) ?? candidates[0]
|
|
160
|
+
if (chosen == null) {
|
|
161
|
+
continue
|
|
162
|
+
}
|
|
163
|
+
result.definitions.push(chosen.definition)
|
|
164
|
+
result.origins[key] = chosen.origin
|
|
165
|
+
if (chosen.retired) {
|
|
166
|
+
result.retired.push(key)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return result
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const resolveScopedBundle = (
|
|
174
|
+
code: PlanningSchemaBundle, records: readonly ScopedSchemaRecord[], scope: SchemaScope & { revision?: number }
|
|
175
|
+
): ScopedSchemaBundle => {
|
|
176
|
+
const mine = records.filter(record => record.entityId === scope.entityId)
|
|
177
|
+
const layers = {
|
|
178
|
+
entity: mine.filter(record => record.project == null),
|
|
179
|
+
project: scope.project == null ? [] : mine.filter(record => record.project === scope.project),
|
|
180
|
+
}
|
|
181
|
+
const types = resolveKind<AnyTypeSchema>(
|
|
182
|
+
PlanningSchemaKind.Type, code.types, type => type.type, openType,
|
|
183
|
+
type => type != null && typeof type.type === 'string' && type.kind === WorkcardKind.Card, layers,
|
|
184
|
+
)
|
|
185
|
+
const flows = resolveKind<StatusFlowSchema>(
|
|
186
|
+
PlanningSchemaKind.Flow, code.flows, flow => flow.id, flow => flow.overridable === true,
|
|
187
|
+
flow => flow != null && typeof flow.id === 'string', layers,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
const assignees = resolveKind<AssigneeTypeSchema>(
|
|
191
|
+
PlanningSchemaKind.AssigneeType, code.assigneeTypes ?? [], schema => schema.type, schema => schema.overridable === true,
|
|
192
|
+
schema => schema != null && typeof schema.type === 'string', { entity: layers.entity, project: [] },
|
|
193
|
+
)
|
|
194
|
+
return structuredClone({
|
|
195
|
+
assigneeTypes: assignees.definitions,
|
|
196
|
+
version: code.version,
|
|
197
|
+
types: types.definitions,
|
|
198
|
+
flows: flows.definitions,
|
|
199
|
+
...(code.app != null ? { app: code.app } : {}),
|
|
200
|
+
scope: scope.project == null ? { entityId: scope.entityId } : { entityId: scope.entityId, project: scope.project },
|
|
201
|
+
revision: scope.revision ?? 0,
|
|
202
|
+
origins: { types: types.origins, flows: flows.origins, assigneeTypes: assignees.origins },
|
|
203
|
+
retired: { types: types.retired, flows: flows.retired, ...(assignees.retired.length > 0 ? { assigneeTypes: assignees.retired } : {}) },
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const scopedRegistryOf = (bundle: ScopedSchemaBundle): ScopedSchemaRegistry => {
|
|
208
|
+
const base = makeSchemaRegistry(bundle)
|
|
209
|
+
const kept = structuredClone(bundle)
|
|
210
|
+
const refuse = (): never => {
|
|
211
|
+
throw new PlanningUnsupported('schemas:read-only')
|
|
212
|
+
}
|
|
213
|
+
const originsOf = (kind: PlanningSchemaKind) =>
|
|
214
|
+
kind === PlanningSchemaKind.AssigneeType ? kept.origins?.assigneeTypes : kind === PlanningSchemaKind.Type ? kept.origins?.types : kept.origins?.flows
|
|
215
|
+
const retiredOf = (kind: PlanningSchemaKind) =>
|
|
216
|
+
kind === PlanningSchemaKind.AssigneeType ? kept.retired?.assigneeTypes : kind === PlanningSchemaKind.Type ? kept.retired?.types : kept.retired?.flows
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
...base,
|
|
220
|
+
registerAssigneeType: refuse,
|
|
221
|
+
registerType: refuse,
|
|
222
|
+
registerFlow: refuse,
|
|
223
|
+
registerApp: refuse,
|
|
224
|
+
load: refuse,
|
|
225
|
+
scope: kept.scope ?? { entityId: '' },
|
|
226
|
+
revision: kept.revision ?? 0,
|
|
227
|
+
originOf: (kind, key) => originsOf(kind)?.[key]
|
|
228
|
+
?? ((kind === PlanningSchemaKind.AssigneeType ? base.assigneeTypes().some(schema => schema.type === key) : kind === PlanningSchemaKind.Type ? base.has(key) : base.flows().some(flow => flow.id === key))
|
|
229
|
+
? SchemaOrigin.Code
|
|
230
|
+
: undefined),
|
|
231
|
+
isRetired: (kind, key) => retiredOf(kind)?.includes(key) === true,
|
|
232
|
+
bundle: () => structuredClone(kept),
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const flowInUse = (bundle: ScopedSchemaBundle, flow: string): string | undefined => {
|
|
237
|
+
if (bundle.retired?.flows.includes(flow) !== true) {
|
|
238
|
+
return undefined
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return bundle.types.find(type => bundle.retired?.types.includes(type.type) !== true && type.flows.includes(flow))?.type
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
schemaRecordKey, schemaKeyOf, assertOverridable, assertAssigneeTypeSchema, assertFlowSchema, assertTypeSchema, resolveScopedBundle,
|
|
246
|
+
scopedRegistryOf, flowInUse,
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export const scopedSchemaHelper = createScopedSchemaHelper()
|
|
251
|
+
|
|
252
|
+
/** @deprecated compat:factory-refactor — use `scopedSchemaHelper.schemaRecordKey(…)` */
|
|
253
|
+
export const schemaRecordKey = (record: Pick<ScopedSchemaRecord, 'entityId' | 'project' | 'kind' | 'key'>): string =>
|
|
254
|
+
scopedSchemaHelper.schemaRecordKey(record)
|
|
255
|
+
|
|
256
|
+
/** @deprecated compat:factory-refactor — use `scopedSchemaHelper.schemaKeyOf(…)` */
|
|
257
|
+
export const schemaKeyOf = (kind: PlanningSchemaKind, definition: WorkcardTypeSchema | StatusFlowSchema | AssigneeTypeSchema): string =>
|
|
258
|
+
scopedSchemaHelper.schemaKeyOf(kind, definition)
|
|
259
|
+
|
|
260
|
+
/** @deprecated compat:factory-refactor — use `scopedSchemaHelper.assertOverridable(…)` */
|
|
261
|
+
export const assertOverridable = (
|
|
262
|
+
code: Pick<PlanningSchemaRegistry, 'has' | 'type' | 'flows' | 'assigneeTypes'>, kind: PlanningSchemaKind, key: string
|
|
263
|
+
): void => scopedSchemaHelper.assertOverridable(code, kind, key)
|
|
264
|
+
|
|
265
|
+
/** @deprecated compat:factory-refactor — use `scopedSchemaHelper.assertFlowSchema(…)` */
|
|
266
|
+
export const assertFlowSchema = (flow: StatusFlowSchema, app?: PlanningAppDeclarations): void => scopedSchemaHelper.assertFlowSchema(flow, app)
|
|
267
|
+
|
|
268
|
+
/** @deprecated compat:factory-refactor — use `scopedSchemaHelper.assertTypeSchema(…)` */
|
|
269
|
+
export const assertTypeSchema = (type: WorkcardTypeSchema, flows: Pick<PlanningSchemaRegistry, 'flow'>, app?: PlanningAppDeclarations): void =>
|
|
270
|
+
scopedSchemaHelper.assertTypeSchema(type, flows, app)
|
|
271
|
+
|
|
272
|
+
/** @deprecated compat:factory-refactor — use `scopedSchemaHelper.resolveScopedBundle(…)` */
|
|
273
|
+
export const resolveScopedBundle = (
|
|
274
|
+
code: PlanningSchemaBundle, records: readonly ScopedSchemaRecord[], scope: SchemaScope & { revision?: number }
|
|
275
|
+
): ScopedSchemaBundle => scopedSchemaHelper.resolveScopedBundle(code, records, scope)
|
|
276
|
+
|
|
277
|
+
/** @deprecated compat:factory-refactor — use `scopedSchemaHelper.scopedRegistryOf(…)` */
|
|
278
|
+
export const scopedRegistryOf = (bundle: ScopedSchemaBundle): ScopedSchemaRegistry => scopedSchemaHelper.scopedRegistryOf(bundle)
|
|
279
|
+
|
|
280
|
+
/** @deprecated compat:factory-refactor — use `scopedSchemaHelper.flowInUse(…)` */
|
|
281
|
+
export const flowInUse = (bundle: ScopedSchemaBundle, flow: string): string | undefined => scopedSchemaHelper.flowInUse(bundle, flow)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AnyTypeSchema, PlanningSchemaRegistry, Specification, SpecificationSlot, Workcard } from '../../types.js'
|
|
2
|
+
|
|
3
|
+
/** The slot and revision rules of the documents a parent card carries. */
|
|
4
|
+
export interface SpecificationHelper {
|
|
5
|
+
/** The slot a parent type declares for a category. */
|
|
6
|
+
slotOf: (type: AnyTypeSchema, category: string) => SpecificationSlot | undefined
|
|
7
|
+
isRevisioned: (slot?: SpecificationSlot | null) => boolean
|
|
8
|
+
bodyCharsOf: (body?: string | null) => number | undefined
|
|
9
|
+
/** The revision a write lands as: 1 for a new document, current + 1 after, none when unrevisioned. */
|
|
10
|
+
nextRevision: (current: Pick<Specification, 'revision'> | null | undefined, slot?: SpecificationSlot | null) => number | undefined
|
|
11
|
+
/**
|
|
12
|
+
* The current document of a category among a parent's specifications: the highest revision, then
|
|
13
|
+
* the most recently updated. `null` when the category has none.
|
|
14
|
+
*/
|
|
15
|
+
currentSpecification: (specs: Workcard[], category: string) => Specification | null
|
|
16
|
+
/**
|
|
17
|
+
* The specification type a new document of a slot is created with: the slot's own `type`, else
|
|
18
|
+
* the only registered specification type, else the one sharing the parent type's prefix
|
|
19
|
+
* (`viable:project` → `viable:spec`).
|
|
20
|
+
*
|
|
21
|
+
* @throws {UnknownWorkcardType} when none or several candidates remain
|
|
22
|
+
*/
|
|
23
|
+
specificationTypeOf: (
|
|
24
|
+
registry: Pick<PlanningSchemaRegistry, 'types'>, parentType: string, slot?: SpecificationSlot | null
|
|
25
|
+
) => string
|
|
26
|
+
}
|
|
@@ -3,63 +3,81 @@ import { UnknownWorkcardType } from '../errors.js'
|
|
|
3
3
|
import type {
|
|
4
4
|
AnyTypeSchema, PlanningSchemaRegistry, Specification, SpecificationSlot, Workcard,
|
|
5
5
|
} from '../types.js'
|
|
6
|
+
import type { SpecificationHelper } from './specification/types.js'
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
export const createSpecificationHelper = (): SpecificationHelper => {
|
|
9
|
+
const slotOf = (type: AnyTypeSchema, category: string): SpecificationSlot | undefined =>
|
|
10
|
+
type.specifications.find(slot => slot.category === category)
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
const isRevisioned = (slot?: SpecificationSlot | null): boolean => slot?.revisioned === true
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
const bodyCharsOf = (body?: string | null): number | undefined => body == null ? undefined : body.length
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
16
|
+
const nextRevision = (
|
|
17
|
+
current: Pick<Specification, 'revision'> | null | undefined, slot?: SpecificationSlot | null
|
|
18
|
+
): number | undefined => isRevisioned(slot) ? (current?.revision ?? 0) + 1 : undefined
|
|
19
|
+
|
|
20
|
+
const currentSpecification = (
|
|
21
|
+
specs: Workcard[], category: string
|
|
22
|
+
): Specification | null => specs
|
|
23
|
+
.filter((spec): spec is Specification => spec.kind === WorkcardKind.Specification
|
|
24
|
+
&& (spec as Specification).category === category)
|
|
25
|
+
.reduce<Specification | null>((best, spec) => {
|
|
26
|
+
if (best == null) {
|
|
27
|
+
return spec
|
|
28
|
+
}
|
|
29
|
+
const byRevision = (spec.revision ?? 0) - (best.revision ?? 0)
|
|
30
|
+
if (byRevision !== 0) {
|
|
31
|
+
return byRevision > 0 ? spec : best
|
|
32
|
+
}
|
|
33
|
+
return (spec.updatedAt ?? spec.createdAt) > (best.updatedAt ?? best.createdAt) ? spec : best
|
|
34
|
+
}, null)
|
|
35
|
+
|
|
36
|
+
const prefixOf = (type: string): string => type.includes(':') ? type.slice(0, type.lastIndexOf(':')) : ''
|
|
37
|
+
|
|
38
|
+
const specificationTypeOf = (
|
|
39
|
+
registry: Pick<PlanningSchemaRegistry, 'types'>, parentType: string, slot?: SpecificationSlot | null
|
|
40
|
+
): string => {
|
|
41
|
+
if (slot?.type != null) {
|
|
42
|
+
return slot.type
|
|
32
43
|
}
|
|
33
|
-
const
|
|
34
|
-
if (
|
|
35
|
-
return
|
|
44
|
+
const candidates = registry.types().filter(type => type.kind === WorkcardKind.Specification)
|
|
45
|
+
if (candidates.length === 1) {
|
|
46
|
+
return candidates[0].type
|
|
36
47
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
* The specification type a new document of a slot is created with: the slot's own `type`, else
|
|
44
|
-
* the only registered specification type, else the one sharing the parent type's prefix
|
|
45
|
-
* (`viable:project` → `viable:spec`).
|
|
46
|
-
*
|
|
47
|
-
* @throws {UnknownWorkcardType} when none or several candidates remain
|
|
48
|
-
*/
|
|
49
|
-
export const specificationTypeOf = (
|
|
50
|
-
registry: Pick<PlanningSchemaRegistry, 'types'>, parentType: string, slot?: SpecificationSlot | null
|
|
51
|
-
): string => {
|
|
52
|
-
if (slot?.type != null) {
|
|
53
|
-
return slot.type
|
|
54
|
-
}
|
|
55
|
-
const candidates = registry.types().filter(type => type.kind === WorkcardKind.Specification)
|
|
56
|
-
if (candidates.length === 1) {
|
|
57
|
-
return candidates[0].type
|
|
58
|
-
}
|
|
59
|
-
const prefixed = candidates.filter(type => prefixOf(type.type) === prefixOf(parentType))
|
|
60
|
-
if (prefixed.length === 1) {
|
|
61
|
-
return prefixed[0].type
|
|
48
|
+
const prefixed = candidates.filter(type => prefixOf(type.type) === prefixOf(parentType))
|
|
49
|
+
if (prefixed.length === 1) {
|
|
50
|
+
return prefixed[0].type
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
throw new UnknownWorkcardType(`specification:${parentType}:${slot?.category ?? ''}`)
|
|
62
54
|
}
|
|
63
55
|
|
|
64
|
-
|
|
56
|
+
return { slotOf, isRevisioned, bodyCharsOf, nextRevision, currentSpecification, specificationTypeOf }
|
|
65
57
|
}
|
|
58
|
+
|
|
59
|
+
export const specificationHelper = createSpecificationHelper()
|
|
60
|
+
|
|
61
|
+
/** @deprecated compat:factory-refactor — use `specificationHelper.slotOf(…)` */
|
|
62
|
+
export const slotOf = (type: AnyTypeSchema, category: string): SpecificationSlot | undefined =>
|
|
63
|
+
specificationHelper.slotOf(type, category)
|
|
64
|
+
|
|
65
|
+
/** @deprecated compat:factory-refactor — use `specificationHelper.isRevisioned(…)` */
|
|
66
|
+
export const isRevisioned = (slot?: SpecificationSlot | null): boolean => specificationHelper.isRevisioned(slot)
|
|
67
|
+
|
|
68
|
+
/** @deprecated compat:factory-refactor — use `specificationHelper.bodyCharsOf(…)` */
|
|
69
|
+
export const bodyCharsOf = (body?: string | null): number | undefined => specificationHelper.bodyCharsOf(body)
|
|
70
|
+
|
|
71
|
+
/** @deprecated compat:factory-refactor — use `specificationHelper.nextRevision(…)` */
|
|
72
|
+
export const nextRevision = (
|
|
73
|
+
current: Pick<Specification, 'revision'> | null | undefined, slot?: SpecificationSlot | null
|
|
74
|
+
): number | undefined => specificationHelper.nextRevision(current, slot)
|
|
75
|
+
|
|
76
|
+
/** @deprecated compat:factory-refactor — use `specificationHelper.currentSpecification(…)` */
|
|
77
|
+
export const currentSpecification = (specs: Workcard[], category: string): Specification | null =>
|
|
78
|
+
specificationHelper.currentSpecification(specs, category)
|
|
79
|
+
|
|
80
|
+
/** @deprecated compat:factory-refactor — use `specificationHelper.specificationTypeOf(…)` */
|
|
81
|
+
export const specificationTypeOf = (
|
|
82
|
+
registry: Pick<PlanningSchemaRegistry, 'types'>, parentType: string, slot?: SpecificationSlot | null
|
|
83
|
+
): string => specificationHelper.specificationTypeOf(registry, parentType, slot)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { IntrinsicStatus } from '../../consts.js'
|
|
2
|
+
import type { AnyTypeSchema, StatusDefinition, StatusFlowSchema, StatusTransitionRule } from '../../types.js'
|
|
3
|
+
import type { FlowLookup } from '../types.local.js'
|
|
4
|
+
|
|
5
|
+
/** The status-flow rules: statuses, the transitions a flow offers, and the intrinsic state. */
|
|
6
|
+
export interface StatusHelper {
|
|
7
|
+
/** The primary flow id of a type — `flows[0]`. @throws {UnknownStatusFlow} when it declares none */
|
|
8
|
+
primaryFlowOf: (type: AnyTypeSchema) => string
|
|
9
|
+
flowIdsOf: (type: AnyTypeSchema) => string[]
|
|
10
|
+
statusDefinitionOf: (flow: StatusFlowSchema, status: string) => StatusDefinition | undefined
|
|
11
|
+
/** The intrinsic state a status maps onto, or `undefined` for a status the flow does not declare. */
|
|
12
|
+
intrinsicOf: (flow: StatusFlowSchema, status: string) => IntrinsicStatus | undefined
|
|
13
|
+
/** The status marked `initial`, else the first one. @throws {PlanningError} for a flow with none */
|
|
14
|
+
initialStatusOf: (flow: StatusFlowSchema) => string
|
|
15
|
+
isTerminal: (flow: StatusFlowSchema, status: string) => boolean
|
|
16
|
+
/**
|
|
17
|
+
* The rule a named transition follows from a status.
|
|
18
|
+
*
|
|
19
|
+
* A name may be declared several times with different `from` sets; the rule naming the status
|
|
20
|
+
* explicitly wins, and a `'*'` rule of that name answers only when none does. A status the flow
|
|
21
|
+
* does NOT declare — a card whose flow changed under it — matches every rule of the name, the first
|
|
22
|
+
* declared answering: such a card can always move back onto the flow it now runs.
|
|
23
|
+
*/
|
|
24
|
+
ruleOf: (flow: StatusFlowSchema, transition: string, from: string) => StatusTransitionRule | undefined
|
|
25
|
+
canTransit: (flow: StatusFlowSchema, transition: string, from: string) => boolean
|
|
26
|
+
/**
|
|
27
|
+
* Every transition available from a status, one rule per name (the one `ruleOf` would pick), in
|
|
28
|
+
* declaration order. `explicit: true` keeps only the rules offered to a person.
|
|
29
|
+
*/
|
|
30
|
+
transitionsFrom: (flow: StatusFlowSchema, status: string, opts?: { explicit?: boolean }) => StatusTransitionRule[]
|
|
31
|
+
/**
|
|
32
|
+
* A new card's status per flow: every flow's initial status, with the primary one overridden by
|
|
33
|
+
* `status` when the draft names one.
|
|
34
|
+
*
|
|
35
|
+
* @throws {UnknownStatusFlow}
|
|
36
|
+
*/
|
|
37
|
+
initialFlowsOf: (type: AnyTypeSchema, registry: FlowLookup, status?: string) => Record<string, string>
|
|
38
|
+
/**
|
|
39
|
+
* The card's intrinsic state over its flows. {@link IntrinsicPolicy.Primary} reads the primary
|
|
40
|
+
* flow; {@link IntrinsicPolicy.All} takes the least advanced flow. A status the flow does not
|
|
41
|
+
* declare reads as planned.
|
|
42
|
+
*
|
|
43
|
+
* @throws {UnknownStatusFlow}
|
|
44
|
+
*/
|
|
45
|
+
resolveIntrinsic: (type: AnyTypeSchema, flows: Record<string, string>, registry: FlowLookup) => IntrinsicStatus
|
|
46
|
+
}
|