@owlmeans/viable-common 0.0.19 → 0.0.20
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 +2 -2
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/agent-presentation/SKILL.md +1 -1
- package/agent-meta/skills/viable-common/SKILL.md +195 -10
- package/build/ba/consts.d.ts +2 -3
- package/build/ba/consts.d.ts.map +1 -1
- package/build/ba/consts.js +2 -3
- package/build/ba/consts.js.map +1 -1
- package/build/ba/helpers.d.ts +2 -1
- package/build/ba/helpers.d.ts.map +1 -1
- package/build/ba/helpers.js +3 -2
- package/build/ba/helpers.js.map +1 -1
- package/build/ba/types.d.ts +17 -2
- package/build/ba/types.d.ts.map +1 -1
- package/build/connect/consts.d.ts +5 -8
- package/build/connect/consts.d.ts.map +1 -1
- package/build/connect/consts.js +5 -8
- package/build/connect/consts.js.map +1 -1
- package/build/connect/entrypoints.d.ts +1 -43
- package/build/connect/entrypoints.d.ts.map +1 -1
- package/build/connect/entrypoints.js +1 -18
- package/build/connect/entrypoints.js.map +1 -1
- package/build/connect/index.d.ts +1 -0
- package/build/connect/index.d.ts.map +1 -1
- package/build/connect/index.js +1 -0
- package/build/connect/index.js.map +1 -1
- package/build/connect/jobs.d.ts +25 -0
- package/build/connect/jobs.d.ts.map +1 -0
- package/build/connect/jobs.js +29 -0
- package/build/connect/jobs.js.map +1 -0
- package/build/connect/references.d.ts +1 -40
- package/build/connect/references.d.ts.map +1 -1
- package/build/connect/references.js +0 -8
- package/build/connect/references.js.map +1 -1
- package/build/connect/schemas.d.ts +1 -7
- package/build/connect/schemas.d.ts.map +1 -1
- package/build/connect/schemas.js +1 -25
- package/build/connect/schemas.js.map +1 -1
- package/build/connect/types.d.ts +30 -45
- package/build/connect/types.d.ts.map +1 -1
- package/build/design/consts.d.ts +0 -2
- package/build/design/consts.d.ts.map +1 -1
- package/build/design/consts.js +0 -2
- package/build/design/consts.js.map +1 -1
- package/build/design/helpers.d.ts +6 -2
- package/build/design/helpers.d.ts.map +1 -1
- package/build/design/helpers.js +6 -2
- package/build/design/helpers.js.map +1 -1
- package/build/design/types.d.ts +12 -11
- package/build/design/types.d.ts.map +1 -1
- package/build/execution/types.d.ts +11 -0
- package/build/execution/types.d.ts.map +1 -1
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/planning/consts.d.ts +141 -0
- package/build/planning/consts.d.ts.map +1 -0
- package/build/planning/consts.js +170 -0
- package/build/planning/consts.js.map +1 -0
- package/build/planning/errors.d.ts +64 -0
- package/build/planning/errors.d.ts.map +1 -0
- package/build/planning/errors.js +99 -0
- package/build/planning/errors.js.map +1 -0
- package/build/planning/flows.d.ts +34 -0
- package/build/planning/flows.d.ts.map +1 -0
- package/build/planning/flows.js +174 -0
- package/build/planning/flows.js.map +1 -0
- package/build/planning/helpers.d.ts +54 -0
- package/build/planning/helpers.d.ts.map +1 -0
- package/build/planning/helpers.js +101 -0
- package/build/planning/helpers.js.map +1 -0
- package/build/planning/index.d.ts +7 -0
- package/build/planning/index.d.ts.map +1 -0
- package/build/planning/index.js +6 -0
- package/build/planning/index.js.map +1 -0
- package/build/planning/schemas.d.ts +23 -0
- package/build/planning/schemas.d.ts.map +1 -0
- package/build/planning/schemas.js +65 -0
- package/build/planning/schemas.js.map +1 -0
- package/build/planning/types.d.ts +70 -0
- package/build/planning/types.d.ts.map +1 -0
- package/build/planning/types.js +2 -0
- package/build/planning/types.js.map +1 -0
- package/build/scaffold/schemas.d.ts.map +1 -1
- package/build/scaffold/schemas.js +7 -2
- package/build/scaffold/schemas.js.map +1 -1
- package/build/scaffold/types.d.ts +5 -1
- package/build/scaffold/types.d.ts.map +1 -1
- package/package.json +3 -1
- package/src/ba/consts.ts +2 -3
- package/src/ba/helpers.ts +3 -2
- package/src/ba/types.ts +17 -2
- package/src/connect/consts.ts +5 -8
- package/src/connect/entrypoints.ts +2 -41
- package/src/connect/index.ts +1 -0
- package/src/connect/jobs.ts +40 -0
- package/src/connect/references.ts +1 -22
- package/src/connect/schemas.ts +2 -29
- package/src/connect/types.ts +30 -51
- package/src/design/consts.ts +0 -3
- package/src/design/helpers.ts +6 -2
- package/src/design/types.ts +12 -11
- package/src/execution/types.ts +11 -0
- package/src/index.ts +1 -0
- package/src/planning/consts.ts +182 -0
- package/src/planning/errors.ts +117 -0
- package/src/planning/flows.ts +204 -0
- package/src/planning/helpers.ts +133 -0
- package/src/planning/index.ts +6 -0
- package/src/planning/schemas.ts +70 -0
- package/src/planning/types.ts +75 -0
- package/src/scaffold/schemas.ts +7 -2
- package/src/scaffold/types.ts +10 -1
- package/tests/branding.spec.ts +37 -0
- package/tests/connect-entrypoints.spec.ts +35 -3
- package/tests/design.spec.ts +3 -3
- package/tests/planning.spec.ts +356 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { IntrinsicPolicy, IntrinsicStatus, SpecificationFormat, WorkcardKind } from '@owlmeans/planning'
|
|
2
|
+
import type {
|
|
3
|
+
AnyTypeSchema, ProjectTypeSchema, RelationshipType, SpecificationSlot, StatusFlowSchema,
|
|
4
|
+
WorkcardTypeSchema,
|
|
5
|
+
} from '@owlmeans/planning'
|
|
6
|
+
import { STORY_DESIGN_VERSION } from '../design/consts.js'
|
|
7
|
+
import { StoryDesignSchema } from '../design/schemas.js'
|
|
8
|
+
import { ScaffoldPlanSchema } from '../scaffold/schemas.js'
|
|
9
|
+
import {
|
|
10
|
+
VIABLE_BUG_CODE, VIABLE_BUG_TYPE, VIABLE_DESIGN_REVISIONS_KEPT, VIABLE_IMPROVEMENT_CODE,
|
|
11
|
+
VIABLE_IMPROVEMENT_TYPE, VIABLE_PROJECT_CODE, VIABLE_PROJECT_FLOW, VIABLE_PROJECT_TYPE,
|
|
12
|
+
VIABLE_REQUIREMENT_CODE, VIABLE_REQUIREMENT_TYPE, VIABLE_SPEC_FLOW, VIABLE_SPEC_TYPE,
|
|
13
|
+
VIABLE_STORY_CODE, VIABLE_STORY_FLOW, VIABLE_STORY_TYPE, ViableProjectStatus,
|
|
14
|
+
ViableProjectTransition, ViableRelationship, ViableSpecCategory, ViableSpecStatus,
|
|
15
|
+
ViableStoryStatus, ViableStoryTransition, ViableTone,
|
|
16
|
+
} from './consts.js'
|
|
17
|
+
import {
|
|
18
|
+
ViableOpenFieldsSchema, ViableProjectFieldsSchema, ViableSpecFieldsSchema, ViableStoryFieldsSchema,
|
|
19
|
+
} from './schemas.js'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The story flow.
|
|
23
|
+
*
|
|
24
|
+
* `failed` maps to PLANNED: a failed story is work still to do, so "done" is the closed count and
|
|
25
|
+
* a retry is simply `start` again. `reset` from anywhere is the manual recovery — the one move a
|
|
26
|
+
* completed story still accepts.
|
|
27
|
+
*/
|
|
28
|
+
export const VIABLE_STORY_FLOW_SCHEMA: StatusFlowSchema = {
|
|
29
|
+
id: VIABLE_STORY_FLOW,
|
|
30
|
+
version: 1,
|
|
31
|
+
statuses: [
|
|
32
|
+
{ key: ViableStoryStatus.Planned, intrinsic: IntrinsicStatus.Planned, initial: true, tone: ViableTone.Blue },
|
|
33
|
+
{ key: ViableStoryStatus.InProgress, intrinsic: IntrinsicStatus.InProgress, tone: ViableTone.Yellow },
|
|
34
|
+
{ key: ViableStoryStatus.Completed, intrinsic: IntrinsicStatus.Closed, tone: ViableTone.Green },
|
|
35
|
+
{ key: ViableStoryStatus.Failed, intrinsic: IntrinsicStatus.Planned, tone: ViableTone.Red },
|
|
36
|
+
],
|
|
37
|
+
transitions: [
|
|
38
|
+
{
|
|
39
|
+
name: ViableStoryTransition.Start,
|
|
40
|
+
from: [ViableStoryStatus.Planned, ViableStoryStatus.Failed],
|
|
41
|
+
to: ViableStoryStatus.InProgress,
|
|
42
|
+
explicit: true,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: ViableStoryTransition.Complete,
|
|
46
|
+
from: [ViableStoryStatus.InProgress],
|
|
47
|
+
to: ViableStoryStatus.Completed,
|
|
48
|
+
explicit: true,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: ViableStoryTransition.Fail,
|
|
52
|
+
from: [ViableStoryStatus.InProgress],
|
|
53
|
+
to: ViableStoryStatus.Failed,
|
|
54
|
+
},
|
|
55
|
+
{ name: ViableStoryTransition.Reset, from: '*', to: ViableStoryStatus.Planned, explicit: true },
|
|
56
|
+
],
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The project flow.
|
|
61
|
+
*
|
|
62
|
+
* A create lands at `draft`; confirming the brief is `confirm`; the end of an initialization is
|
|
63
|
+
* `activate`. A reinitialization and a failed initialization are SLOT facts, not transitions, and
|
|
64
|
+
* destroying a project is a `delete` plus the platform's cascade — never `archive`.
|
|
65
|
+
*/
|
|
66
|
+
export const VIABLE_PROJECT_FLOW_SCHEMA: StatusFlowSchema = {
|
|
67
|
+
id: VIABLE_PROJECT_FLOW,
|
|
68
|
+
version: 1,
|
|
69
|
+
statuses: [
|
|
70
|
+
{ key: ViableProjectStatus.Draft, intrinsic: IntrinsicStatus.Planned, initial: true, tone: ViableTone.Blue },
|
|
71
|
+
{ key: ViableProjectStatus.Confirmed, intrinsic: IntrinsicStatus.InProgress, tone: ViableTone.Yellow },
|
|
72
|
+
{ key: ViableProjectStatus.Active, intrinsic: IntrinsicStatus.InProgress, tone: ViableTone.Green },
|
|
73
|
+
{ key: ViableProjectStatus.Archived, intrinsic: IntrinsicStatus.Closed, tone: ViableTone.Neutral },
|
|
74
|
+
],
|
|
75
|
+
transitions: [
|
|
76
|
+
{
|
|
77
|
+
name: ViableProjectTransition.Confirm,
|
|
78
|
+
from: [ViableProjectStatus.Draft],
|
|
79
|
+
to: ViableProjectStatus.Confirmed,
|
|
80
|
+
explicit: true,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: ViableProjectTransition.Activate,
|
|
84
|
+
from: [ViableProjectStatus.Confirmed],
|
|
85
|
+
to: ViableProjectStatus.Active,
|
|
86
|
+
},
|
|
87
|
+
{ name: ViableProjectTransition.Archive, from: '*', to: ViableProjectStatus.Archived, explicit: true },
|
|
88
|
+
{
|
|
89
|
+
name: ViableProjectTransition.Reopen,
|
|
90
|
+
from: [ViableProjectStatus.Archived],
|
|
91
|
+
to: ViableProjectStatus.Active,
|
|
92
|
+
explicit: true,
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** A document is written or revised; it has no lifecycle to move through. */
|
|
98
|
+
export const VIABLE_SPEC_FLOW_SCHEMA: StatusFlowSchema = {
|
|
99
|
+
id: VIABLE_SPEC_FLOW,
|
|
100
|
+
version: 1,
|
|
101
|
+
statuses: [
|
|
102
|
+
{ key: ViableSpecStatus.Current, intrinsic: IntrinsicStatus.Closed, initial: true, tone: ViableTone.Neutral },
|
|
103
|
+
],
|
|
104
|
+
transitions: [],
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const slot = (
|
|
108
|
+
category: ViableSpecCategory, format: SpecificationFormat, extra: Partial<SpecificationSlot> = {},
|
|
109
|
+
): SpecificationSlot => ({ category, format, type: VIABLE_SPEC_TYPE, ...extra })
|
|
110
|
+
|
|
111
|
+
/** The brief a person confirms, and the plan the scaffold draws from it. */
|
|
112
|
+
export const VIABLE_PROJECT_SLOTS: SpecificationSlot[] = [
|
|
113
|
+
slot(ViableSpecCategory.Specification, SpecificationFormat.Markdown, { required: true }),
|
|
114
|
+
slot(ViableSpecCategory.Vision, SpecificationFormat.Markdown),
|
|
115
|
+
slot(ViableSpecCategory.DesignSystem, SpecificationFormat.Markdown),
|
|
116
|
+
slot(ViableSpecCategory.Scaffold, SpecificationFormat.Json, {
|
|
117
|
+
revisioned: true, keepRevisions: VIABLE_DESIGN_REVISIONS_KEPT, schema: ScaffoldPlanSchema,
|
|
118
|
+
}),
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
/** A story's design — the payload the implementation stage consumes. */
|
|
122
|
+
export const VIABLE_STORY_SLOTS: SpecificationSlot[] = [
|
|
123
|
+
slot(ViableSpecCategory.Design, SpecificationFormat.Json, {
|
|
124
|
+
revisioned: true,
|
|
125
|
+
keepRevisions: VIABLE_DESIGN_REVISIONS_KEPT,
|
|
126
|
+
schema: StoryDesignSchema,
|
|
127
|
+
version: STORY_DESIGN_VERSION,
|
|
128
|
+
}),
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
const storyLink = (name: ViableRelationship): RelationshipType =>
|
|
132
|
+
({ name, from: [VIABLE_STORY_TYPE], to: [VIABLE_STORY_TYPE], single: true })
|
|
133
|
+
|
|
134
|
+
export const VIABLE_STORY_RELATIONSHIPS: RelationshipType[] = [
|
|
135
|
+
storyLink(ViableRelationship.Follows),
|
|
136
|
+
storyLink(ViableRelationship.SharesWidget),
|
|
137
|
+
storyLink(ViableRelationship.SharesScreen),
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
export const VIABLE_PROJECT_TYPE_SCHEMA: ProjectTypeSchema = {
|
|
141
|
+
type: VIABLE_PROJECT_TYPE,
|
|
142
|
+
kind: WorkcardKind.Project,
|
|
143
|
+
version: 1,
|
|
144
|
+
fields: ViableProjectFieldsSchema,
|
|
145
|
+
flows: [VIABLE_PROJECT_FLOW],
|
|
146
|
+
intrinsic: IntrinsicPolicy.Primary,
|
|
147
|
+
specifications: VIABLE_PROJECT_SLOTS,
|
|
148
|
+
code: VIABLE_PROJECT_CODE,
|
|
149
|
+
cardTypes: [VIABLE_STORY_TYPE],
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export const VIABLE_STORY_TYPE_SCHEMA: WorkcardTypeSchema = {
|
|
153
|
+
type: VIABLE_STORY_TYPE,
|
|
154
|
+
kind: WorkcardKind.Card,
|
|
155
|
+
version: 1,
|
|
156
|
+
fields: ViableStoryFieldsSchema,
|
|
157
|
+
flows: [VIABLE_STORY_FLOW],
|
|
158
|
+
intrinsic: IntrinsicPolicy.Primary,
|
|
159
|
+
specifications: VIABLE_STORY_SLOTS,
|
|
160
|
+
relationships: VIABLE_STORY_RELATIONSHIPS,
|
|
161
|
+
code: VIABLE_STORY_CODE,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export const VIABLE_SPEC_TYPE_SCHEMA: WorkcardTypeSchema = {
|
|
165
|
+
type: VIABLE_SPEC_TYPE,
|
|
166
|
+
kind: WorkcardKind.Specification,
|
|
167
|
+
version: 1,
|
|
168
|
+
fields: ViableSpecFieldsSchema,
|
|
169
|
+
flows: [VIABLE_SPEC_FLOW],
|
|
170
|
+
specifications: [],
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const reserved = (type: string, code: WorkcardTypeSchema['code']): WorkcardTypeSchema => ({
|
|
174
|
+
type,
|
|
175
|
+
kind: WorkcardKind.Card,
|
|
176
|
+
version: 1,
|
|
177
|
+
fields: ViableOpenFieldsSchema,
|
|
178
|
+
flows: [VIABLE_STORY_FLOW],
|
|
179
|
+
intrinsic: IntrinsicPolicy.Primary,
|
|
180
|
+
specifications: [],
|
|
181
|
+
code,
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
/** Registered so their prefixes and flow are decided once; not creatable under a project yet. */
|
|
185
|
+
export const VIABLE_RESERVED_TYPE_SCHEMAS: WorkcardTypeSchema[] = [
|
|
186
|
+
reserved(VIABLE_BUG_TYPE, VIABLE_BUG_CODE),
|
|
187
|
+
reserved(VIABLE_IMPROVEMENT_TYPE, VIABLE_IMPROVEMENT_CODE),
|
|
188
|
+
reserved(VIABLE_REQUIREMENT_TYPE, VIABLE_REQUIREMENT_CODE),
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
/** Every Viable type — what a planning plugin registers. */
|
|
192
|
+
export const VIABLE_TYPE_SCHEMAS: AnyTypeSchema[] = [
|
|
193
|
+
VIABLE_PROJECT_TYPE_SCHEMA,
|
|
194
|
+
VIABLE_STORY_TYPE_SCHEMA,
|
|
195
|
+
VIABLE_SPEC_TYPE_SCHEMA,
|
|
196
|
+
...VIABLE_RESERVED_TYPE_SCHEMAS,
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
/** Every Viable flow — registered beside the types. */
|
|
200
|
+
export const VIABLE_FLOW_SCHEMAS: StatusFlowSchema[] = [
|
|
201
|
+
VIABLE_PROJECT_FLOW_SCHEMA,
|
|
202
|
+
VIABLE_STORY_FLOW_SCHEMA,
|
|
203
|
+
VIABLE_SPEC_FLOW_SCHEMA,
|
|
204
|
+
]
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { WorkcardKind } from '@owlmeans/planning'
|
|
2
|
+
import type { Specification, Workcard } from '@owlmeans/planning'
|
|
3
|
+
import { ProjectArea } from '../areas/consts.js'
|
|
4
|
+
import type { StoryDraft } from '../areas/types.js'
|
|
5
|
+
import type { AgentProject, UserStory } from '../ba/types.js'
|
|
6
|
+
import { userStoryOfDesign } from '../design/helpers.js'
|
|
7
|
+
import type { StoryDesign } from '../design/types.js'
|
|
8
|
+
import type { StoryWriteInput } from '../metadata/types.js'
|
|
9
|
+
import {
|
|
10
|
+
VIABLE_PROJECT_TYPE, VIABLE_STORY_TYPE, VIABLE_USER_CHANNELS, ViableSpecCategory,
|
|
11
|
+
} from './consts.js'
|
|
12
|
+
import { ProjectStoryMissconfigured } from './errors.js'
|
|
13
|
+
import type {
|
|
14
|
+
StoryWriteContent, ViableProjectCard, ViableProjectFields, ViableStoryCard, ViableStoryFields,
|
|
15
|
+
} from './types.js'
|
|
16
|
+
|
|
17
|
+
/** A `viable:project` card — kind AND type, since a foreign provider may share either alone. */
|
|
18
|
+
export const isViableProject = (card?: Workcard | null): card is ViableProjectCard =>
|
|
19
|
+
card != null && card.kind === WorkcardKind.Project && card.type === VIABLE_PROJECT_TYPE
|
|
20
|
+
|
|
21
|
+
/** A `viable:user-story` card. */
|
|
22
|
+
export const isViableStory = (card?: Workcard | null): card is ViableStoryCard =>
|
|
23
|
+
card != null && card.kind === WorkcardKind.Card && card.type === VIABLE_STORY_TYPE
|
|
24
|
+
|
|
25
|
+
/** Whether a write came from a person (`web`, `connect`) rather than the platform's own runs. */
|
|
26
|
+
export const isUserChannel = (channel?: string | null): boolean =>
|
|
27
|
+
channel != null && VIABLE_USER_CHANNELS.includes(channel)
|
|
28
|
+
|
|
29
|
+
export const projectFieldsOf = (card: Workcard): ViableProjectFields =>
|
|
30
|
+
(card.fields ?? {}) as ViableProjectFields
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A story card's fields, with the two required ones always answered.
|
|
34
|
+
*
|
|
35
|
+
* An absent area reads as `guest` — the area that guards nothing — and an absent primary flag as
|
|
36
|
+
* `false`, so a reader never branches on a card written by a store that dropped them.
|
|
37
|
+
*/
|
|
38
|
+
export const storyFieldsOf = (card: Workcard): ViableStoryFields => {
|
|
39
|
+
const fields = (card.fields ?? {}) as Partial<ViableStoryFields>
|
|
40
|
+
|
|
41
|
+
return { ...fields, area: fields.area ?? ProjectArea.Guest, primary: fields.primary === true }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The draft shape the analysis prompts and moderation take, from a card. */
|
|
45
|
+
export const storyDraftOf = (card: Workcard): StoryDraft =>
|
|
46
|
+
({ story: card.title, area: storyFieldsOf(card).area })
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The aggregate the coders take, for a story CARD.
|
|
50
|
+
*
|
|
51
|
+
* The card wins on the narrative, the code, the area and the actor — a person may have changed any
|
|
52
|
+
* of them since the design was written. The design supplies what only it knows: the entities and
|
|
53
|
+
* the screens. Without a design the aggregate has no screens yet.
|
|
54
|
+
*/
|
|
55
|
+
export const userStoryOf = (card: Workcard, design?: StoryDesign | null): UserStory => {
|
|
56
|
+
const fields = storyFieldsOf(card)
|
|
57
|
+
const base: UserStory = design != null
|
|
58
|
+
? userStoryOfDesign(design)
|
|
59
|
+
: { story: card.title, area: fields.area, entity: null as unknown as string, entities: [], screens: [] }
|
|
60
|
+
const code = card.code ?? base.code
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
...base,
|
|
64
|
+
story: card.title,
|
|
65
|
+
...(code != null ? { code } : {}),
|
|
66
|
+
area: fields.area,
|
|
67
|
+
...(fields.actor != null ? { actor: fields.actor } : {}),
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The current document of one category among a card's specifications.
|
|
73
|
+
*
|
|
74
|
+
* The highest revision wins, then the most recently updated; a category nobody wrote answers
|
|
75
|
+
* `null`.
|
|
76
|
+
*/
|
|
77
|
+
export const currentSpecOf = (
|
|
78
|
+
specs: readonly Specification[], category: string,
|
|
79
|
+
): Specification | null =>
|
|
80
|
+
specs
|
|
81
|
+
.filter(spec => spec.category === category)
|
|
82
|
+
.reduce<Specification | null>((best, spec) => {
|
|
83
|
+
if (best == null) return spec
|
|
84
|
+
const byRevision = (spec.revision ?? 0) - (best.revision ?? 0)
|
|
85
|
+
if (byRevision !== 0) return byRevision > 0 ? spec : best
|
|
86
|
+
|
|
87
|
+
return (spec.updatedAt ?? spec.createdAt) > (best.updatedAt ?? best.createdAt) ? spec : best
|
|
88
|
+
}, null)
|
|
89
|
+
|
|
90
|
+
/** The body of a category's current document, `''` when there is none. */
|
|
91
|
+
export const currentSpecBody = (specs: readonly Specification[], category: string): string =>
|
|
92
|
+
currentSpecOf(specs, category)?.body ?? ''
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The brief a generator reads, assembled from the project card and its specifications.
|
|
96
|
+
*
|
|
97
|
+
* `name` is the card's title and `alias` its code; a brief part nobody wrote is `''`, which is
|
|
98
|
+
* what every prompt already treats as "not stated".
|
|
99
|
+
*/
|
|
100
|
+
export const projectBriefOf = (project: Workcard, specs: readonly Specification[]): AgentProject => {
|
|
101
|
+
const { language } = projectFieldsOf(project)
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
name: project.title,
|
|
105
|
+
...(project.code != null ? { alias: project.code } : {}),
|
|
106
|
+
description: project.description ?? '',
|
|
107
|
+
specification: currentSpecBody(specs, ViableSpecCategory.Specification),
|
|
108
|
+
designSystem: currentSpecBody(specs, ViableSpecCategory.DesignSystem),
|
|
109
|
+
vision: currentSpecBody(specs, ViableSpecCategory.Vision),
|
|
110
|
+
...(language != null ? { language } : {}),
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* What the metadata store writes to `docs/stories/<code>.md`, from the card and a run's content.
|
|
116
|
+
*
|
|
117
|
+
* The CODE is the target's only story identifier — a card id never reaches a target file — so a
|
|
118
|
+
* story card without one is refused rather than filed under its id.
|
|
119
|
+
*/
|
|
120
|
+
export const storyWriteInputOf = (card: Workcard, content: StoryWriteContent): StoryWriteInput => {
|
|
121
|
+
if (card.code == null || card.code === '') {
|
|
122
|
+
throw new ProjectStoryMissconfigured('no-code')
|
|
123
|
+
}
|
|
124
|
+
const { status, ...rest } = content
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
...rest,
|
|
128
|
+
code: card.code,
|
|
129
|
+
narrative: card.title,
|
|
130
|
+
status: status ?? card.status,
|
|
131
|
+
primary: storyFieldsOf(card).primary,
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { JSONSchemaType } from 'ajv'
|
|
2
|
+
import { CODE_MAX } from '@owlmeans/planning'
|
|
3
|
+
import { ProjectArea } from '../areas/consts.js'
|
|
4
|
+
import { StoryKind } from '../ba/consts.js'
|
|
5
|
+
import { ConnectLlm, ConnectTarget } from '../connect/consts.js'
|
|
6
|
+
import { ProjectOriginSchema } from '../convert/schemas.js'
|
|
7
|
+
import { StoryActor } from '../design/runtime.js'
|
|
8
|
+
import type { ViableProjectFields, ViableStoryFields } from './types.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The `fields` schema of a `viable:project` card.
|
|
12
|
+
*
|
|
13
|
+
* A record-element schema: hand-written, cast, every optional key nullable with its type, every
|
|
14
|
+
* nullable enum listing `null`. Plain strings where the vocabulary grows in a library this record
|
|
15
|
+
* does not redeploy with (blueprint, case, game kind).
|
|
16
|
+
*/
|
|
17
|
+
export const ViableProjectFieldsSchema = {
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {
|
|
20
|
+
formerAliases: {
|
|
21
|
+
type: 'array', items: { type: 'string', minLength: 1, maxLength: CODE_MAX }, nullable: true,
|
|
22
|
+
},
|
|
23
|
+
language: { type: 'string', minLength: 2, maxLength: 16, nullable: true },
|
|
24
|
+
blueprint: { type: 'string', minLength: 1, maxLength: 64, nullable: true },
|
|
25
|
+
blueprintCase: { type: 'string', minLength: 1, maxLength: 64, nullable: true },
|
|
26
|
+
gameKind: { type: 'string', minLength: 1, maxLength: 64, nullable: true },
|
|
27
|
+
target: { type: 'string', enum: [...Object.values(ConnectTarget), null], nullable: true },
|
|
28
|
+
origin: { ...ProjectOriginSchema, nullable: true },
|
|
29
|
+
connectLlmMode: { type: 'string', enum: [...Object.values(ConnectLlm), null], nullable: true },
|
|
30
|
+
converterLlmMode: { type: 'string', enum: [...Object.values(ConnectLlm), null], nullable: true },
|
|
31
|
+
},
|
|
32
|
+
required: [],
|
|
33
|
+
additionalProperties: false,
|
|
34
|
+
} as unknown as JSONSchemaType<ViableProjectFields>
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The `fields` schema of a `viable:user-story` card.
|
|
38
|
+
*
|
|
39
|
+
* `area` is required, and a create that carries none is still valid by the time it is checked:
|
|
40
|
+
* validation runs after the planning middlewares, and the viable plugin's re-format hook fills the
|
|
41
|
+
* area of a narrative a person wrote.
|
|
42
|
+
*/
|
|
43
|
+
export const ViableStoryFieldsSchema = {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
area: { type: 'string', enum: Object.values(ProjectArea) },
|
|
47
|
+
primary: { type: 'boolean' },
|
|
48
|
+
actor: { type: 'string', enum: [...Object.values(StoryActor), null], nullable: true },
|
|
49
|
+
warning: { type: 'string', maxLength: 1024, nullable: true },
|
|
50
|
+
kind: { type: 'string', enum: [...Object.values(StoryKind), null], nullable: true },
|
|
51
|
+
},
|
|
52
|
+
required: ['area', 'primary'],
|
|
53
|
+
additionalProperties: false,
|
|
54
|
+
} as unknown as JSONSchemaType<ViableStoryFields>
|
|
55
|
+
|
|
56
|
+
/** The open `fields` of a reserved card type — nothing is decided about them yet. */
|
|
57
|
+
export const ViableOpenFieldsSchema = {
|
|
58
|
+
type: 'object',
|
|
59
|
+
properties: {},
|
|
60
|
+
required: [],
|
|
61
|
+
additionalProperties: true,
|
|
62
|
+
} as unknown as JSONSchemaType<Record<string, unknown>>
|
|
63
|
+
|
|
64
|
+
/** A document carries nothing under `fields`: its payload is the specification body. */
|
|
65
|
+
export const ViableSpecFieldsSchema = {
|
|
66
|
+
type: 'object',
|
|
67
|
+
properties: {},
|
|
68
|
+
required: [],
|
|
69
|
+
additionalProperties: false,
|
|
70
|
+
} as unknown as JSONSchemaType<Record<string, never>>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { Card, Project, Specification } from '@owlmeans/planning'
|
|
2
|
+
import type { ProjectArea } from '../areas/consts.js'
|
|
3
|
+
import type { StoryKind } from '../ba/consts.js'
|
|
4
|
+
import type { ConnectLlm, ConnectTarget } from '../connect/consts.js'
|
|
5
|
+
import type { ProjectOrigin } from '../convert/types.js'
|
|
6
|
+
import type { StoryActor } from '../design/runtime.js'
|
|
7
|
+
import type { StoryWriteInput } from '../metadata/types.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* What a `viable:project` card keeps under `fields`.
|
|
11
|
+
*
|
|
12
|
+
* The alias is the card's `code`, the name its `title`; the brief parts are specifications. Every
|
|
13
|
+
* key is optional: a project created before a field existed carries none, and an absent value
|
|
14
|
+
* means the platform default (inherit a mode, the default blueprint, a cloud slot).
|
|
15
|
+
*/
|
|
16
|
+
export type ViableProjectFields = {
|
|
17
|
+
/** Aliases this project gave up. Names are retired, never freed to a sibling project. */
|
|
18
|
+
formerAliases?: string[]
|
|
19
|
+
/** The IETF tag generated UI text is written in. */
|
|
20
|
+
language?: string
|
|
21
|
+
/** The blueprint id the project was drawn with. */
|
|
22
|
+
blueprint?: string
|
|
23
|
+
/** The blueprint case classified from the prompt. Absent applies no patch. */
|
|
24
|
+
blueprintCase?: string
|
|
25
|
+
/** How a game is played; absent for every other case. */
|
|
26
|
+
gameKind?: string
|
|
27
|
+
/** Where the tree lives; absent is the platform's own slot. */
|
|
28
|
+
target?: ConnectTarget
|
|
29
|
+
/** Where converted code came from; present only on an import. */
|
|
30
|
+
origin?: ProjectOrigin
|
|
31
|
+
/** Who performs a run's model calls on this project; absent inherits the owner's preference. */
|
|
32
|
+
connectLlmMode?: ConnectLlm
|
|
33
|
+
/** Who performs a conversion's model calls on this project; absent inherits. */
|
|
34
|
+
converterLlmMode?: ConnectLlm
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* What a `viable:user-story` card keeps under `fields`.
|
|
39
|
+
*
|
|
40
|
+
* The narrative is the card's `title`, the flow ordinal its `order` (a double — a connective story
|
|
41
|
+
* sits between two flow steps), the status its primary flow status.
|
|
42
|
+
*/
|
|
43
|
+
export type ViableStoryFields = {
|
|
44
|
+
/** The audience acting in the story; its screens hang under this area. */
|
|
45
|
+
area: ProjectArea
|
|
46
|
+
/** The story whose screen is the project's front door. Exactly one per project. */
|
|
47
|
+
primary: boolean
|
|
48
|
+
/** Who acts; absent is a person. */
|
|
49
|
+
actor?: StoryActor
|
|
50
|
+
/** Why the last development did not end cleanly. Written as `''` to clear. */
|
|
51
|
+
warning?: string
|
|
52
|
+
/** Which half of the analysis the story came from. */
|
|
53
|
+
kind?: StoryKind
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ViableProjectCard extends Project {
|
|
57
|
+
fields: ViableProjectFields
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ViableStoryCard extends Card {
|
|
61
|
+
fields: ViableStoryFields
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A document under a Viable card — the brief parts, the scaffold plan, a story design. */
|
|
65
|
+
export type ViableSpecification = Specification
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* What a develop run hands the metadata store beside the card.
|
|
69
|
+
*
|
|
70
|
+
* The code, the narrative and the primary flag are the CARD's and are never passed separately — a
|
|
71
|
+
* second copy is a second answer. `status` defaults to the card's own.
|
|
72
|
+
*/
|
|
73
|
+
export type StoryWriteContent =
|
|
74
|
+
& Omit<StoryWriteInput, 'code' | 'narrative' | 'primary' | 'status'>
|
|
75
|
+
& { status?: string }
|
package/src/scaffold/schemas.ts
CHANGED
|
@@ -168,15 +168,20 @@ const GuestHomePlanSchema: JSONSchemaType<GuestHomePlan> = {
|
|
|
168
168
|
hero: {
|
|
169
169
|
type: 'object',
|
|
170
170
|
properties: {
|
|
171
|
-
eyebrow: { type: 'string', description: 'A 2-4 word positioning phrase above the headline' },
|
|
172
171
|
headline: {
|
|
173
172
|
type: 'string',
|
|
174
173
|
description: 'The product\'s promise in one line, under 9 words. Never the product name alone'
|
|
175
174
|
},
|
|
176
175
|
sub: { type: 'string', description: 'One or two sentences: what it does and who it is for' },
|
|
177
176
|
cta: { type: 'string', description: 'The call-to-action label, 2-3 words' },
|
|
177
|
+
eyebrow: {
|
|
178
|
+
type: 'string', nullable: true,
|
|
179
|
+
description: 'OPTIONAL, normally omitted. A small badge above the headline - set it only when'
|
|
180
|
+
+ ' the specification explicitly asks for a label there. A landing page carries no badge by'
|
|
181
|
+
+ ' default: the headline is what positions the product'
|
|
182
|
+
},
|
|
178
183
|
},
|
|
179
|
-
required: ['
|
|
184
|
+
required: ['headline', 'sub', 'cta'],
|
|
180
185
|
additionalProperties: false,
|
|
181
186
|
},
|
|
182
187
|
problem: {
|
package/src/scaffold/types.ts
CHANGED
|
@@ -91,7 +91,16 @@ export interface ProductIdentity {
|
|
|
91
91
|
|
|
92
92
|
/** The guest area's landing page — the product's public face. */
|
|
93
93
|
export interface GuestHomePlan {
|
|
94
|
-
hero: {
|
|
94
|
+
hero: {
|
|
95
|
+
headline: string
|
|
96
|
+
sub: string
|
|
97
|
+
cta: string
|
|
98
|
+
/**
|
|
99
|
+
* A badge above the headline — OPTIONAL and normally absent. A landing page carries no badge
|
|
100
|
+
* unless its specification asks for one; the headline is what positions the product.
|
|
101
|
+
*/
|
|
102
|
+
eyebrow?: string
|
|
103
|
+
}
|
|
95
104
|
problem: { title: string, text: string }
|
|
96
105
|
solution: { title: string, text: string }
|
|
97
106
|
features: { title: string, text: string }[]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { brandingEnv } from '../src/branding.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `BRANDING_CREDIT` is the one key whose ABSENCE means something different from an explicit
|
|
7
|
+
* empty string — the platform side of the bug this pins: `brandingCredit: ''` stored to hide the
|
|
8
|
+
* platform credit must survive as `BRANDING_CREDIT: ''`, not fall back to the "shown" default.
|
|
9
|
+
*/
|
|
10
|
+
describe('brandingEnv', () => {
|
|
11
|
+
it('emits an empty BRANDING_CREDIT for an explicitly hidden credit', () => {
|
|
12
|
+
expect(brandingEnv({ brandingCredit: '' }).BRANDING_CREDIT).toBe('')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('emits BRANDING_CREDIT=1 when the credit is shown', () => {
|
|
16
|
+
expect(brandingEnv({ brandingCredit: '1' }).BRANDING_CREDIT).toBe('1')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('defaults to shown (BRANDING_CREDIT=1) when nothing was ever delivered', () => {
|
|
20
|
+
// The safe direction: a delivery that lost the value keeps the credit rather than
|
|
21
|
+
// silently giving away a paid feature.
|
|
22
|
+
expect(brandingEnv({}).BRANDING_CREDIT).toBe('1')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('composes the rest of the branding env from whatever is present, blank otherwise', () => {
|
|
26
|
+
expect(brandingEnv({
|
|
27
|
+
brandingCopyright: '© 2026 Acme', projectName: 'Acme CRM',
|
|
28
|
+
})).toEqual({
|
|
29
|
+
BRANDING_COPYRIGHT: '© 2026 Acme',
|
|
30
|
+
BRANDING_ORGANIZATION: '',
|
|
31
|
+
BRANDING_TERMS_URL: '',
|
|
32
|
+
BRANDING_PRIVACY_URL: '',
|
|
33
|
+
BRANDING_CREDIT: '1',
|
|
34
|
+
BRANDING_PRODUCT: 'Acme CRM',
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
})
|
|
@@ -2,8 +2,10 @@ import { describe, expect, test } from 'bun:test'
|
|
|
2
2
|
import { openProtocol, protocols } from '@owlmeans/entrypoint'
|
|
3
3
|
import { DEFAULT_GUARD } from '@owlmeans/auth-common'
|
|
4
4
|
import { route } from '@owlmeans/route'
|
|
5
|
-
import { connect } from '../src/connect/consts.js'
|
|
5
|
+
import { connect, ConnectJobKind } from '../src/connect/consts.js'
|
|
6
6
|
import { connectProtocols } from '../src/connect/entrypoints.js'
|
|
7
|
+
import { JOB_SEPARATOR, jobIdOf, parseJobId } from '../src/connect/jobs.js'
|
|
8
|
+
import { connectRef } from '../src/connect/references.js'
|
|
7
9
|
|
|
8
10
|
describe('@owlmeans/viable-common — connector protocol tree', () => {
|
|
9
11
|
const tree = connectProtocols({
|
|
@@ -19,7 +21,37 @@ describe('@owlmeans/viable-common — connector protocol tree', () => {
|
|
|
19
21
|
expect(tree.convert.proceed.alias).toBe(connect.convert.proceed)
|
|
20
22
|
expect(tree.inquiry.answer.alias).toBe(connect.inquiry.answer)
|
|
21
23
|
expect(tree.files.list.alias).toBe(connect.files.list)
|
|
22
|
-
expect(protocols(tree)).toHaveLength(
|
|
23
|
-
expect(new Set(protocols(tree).map(protocol => protocol.alias)).size).toBe(
|
|
24
|
+
expect(protocols(tree)).toHaveLength(32)
|
|
25
|
+
expect(new Set(protocols(tree).map(protocol => protocol.alias)).size).toBe(32)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('carries no story routes: stories are planning cards served by the planning tree', () => {
|
|
29
|
+
expect((tree as Record<string, unknown>).story).toBeUndefined()
|
|
30
|
+
expect((connect as Record<string, unknown>).story).toBeUndefined()
|
|
31
|
+
expect((connectRef as unknown as Record<string, unknown>).story).toBeUndefined()
|
|
32
|
+
expect(protocols(tree).some(protocol => protocol.alias.includes(':story:'))).toBe(false)
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('@owlmeans/viable-common — connector job ids', () => {
|
|
37
|
+
test('a story job id names the card and splits back into its parts', () => {
|
|
38
|
+
const id = jobIdOf(ConnectJobKind.StoryDevelop, 'project-card', 'story-card')
|
|
39
|
+
|
|
40
|
+
expect(id).toBe(['story-develop', 'project-card', 'story-card'].join(JOB_SEPARATOR))
|
|
41
|
+
expect(parseJobId(id)).toEqual({
|
|
42
|
+
kind: ConnectJobKind.StoryDevelop, projectId: 'project-card', suffix: 'story-card',
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('a job with no subject carries no suffix', () => {
|
|
47
|
+
expect(parseJobId(jobIdOf(ConnectJobKind.ProjectInit, 'p1'))).toEqual({
|
|
48
|
+
kind: ConnectJobKind.ProjectInit, projectId: 'p1',
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('a suffix that contains the separator is rejoined, never truncated', () => {
|
|
53
|
+
const suffix = `run${JOB_SEPARATOR}tail`
|
|
54
|
+
|
|
55
|
+
expect(parseJobId(jobIdOf(ConnectJobKind.PipelineResume, 'p1', suffix)).suffix).toBe(suffix)
|
|
24
56
|
})
|
|
25
57
|
})
|
package/tests/design.spec.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { ProjectArea } from '../src/areas/consts.js'
|
|
|
4
4
|
import { SpecCategory } from '../src/ba/consts.js'
|
|
5
5
|
import {
|
|
6
6
|
DesignStaleness, STORY_DESIGN_VERSION, StoryDesignSchema, designHash, designStaleness,
|
|
7
|
-
emptyStoryDesign, screenMapOf,
|
|
7
|
+
emptyStoryDesign, screenMapOf, userStoryOfDesign,
|
|
8
8
|
} from '../src/design/index.js'
|
|
9
9
|
import type { StoryDesign } from '../src/design/index.js'
|
|
10
10
|
|
|
@@ -45,7 +45,7 @@ describe('viable-common - the design aggregate', () => {
|
|
|
45
45
|
test('derives the UserStory the coders take, carrying the story code', () => {
|
|
46
46
|
// The scaffold stamps every placeholder it draws with the code, so a run keyed by anything
|
|
47
47
|
// else cannot recognise its own reservation.
|
|
48
|
-
const story =
|
|
48
|
+
const story = userStoryOfDesign(withScreens())
|
|
49
49
|
|
|
50
50
|
expect(story.code).toBe('US-ABC12')
|
|
51
51
|
expect(story.area).toBe(ProjectArea.User)
|
|
@@ -60,7 +60,7 @@ describe('viable-common - the design aggregate', () => {
|
|
|
60
60
|
const broken = withScreens()
|
|
61
61
|
broken.screens[0].components = ['BookingList', 'Ghost']
|
|
62
62
|
|
|
63
|
-
expect(
|
|
63
|
+
expect(userStoryOfDesign(broken).screens[0].components.map(entry => entry.name)).toEqual(['BookingList'])
|
|
64
64
|
})
|
|
65
65
|
|
|
66
66
|
test('screenMapOf is the shape every UX and UI prompt takes', () => {
|