@owlmeans/viable-common 0.0.25 → 0.0.27
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 +3 -3
- 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 +111 -8
- package/build/blueprint/case.d.ts.map +1 -1
- package/build/blueprint/case.js +58 -16
- package/build/blueprint/case.js.map +1 -1
- package/build/blueprint/resolve.d.ts.map +1 -1
- package/build/blueprint/resolve.js +3 -0
- package/build/blueprint/resolve.js.map +1 -1
- package/build/blueprint/types.d.ts +44 -2
- package/build/blueprint/types.d.ts.map +1 -1
- package/build/blueprint/types.js +35 -1
- package/build/blueprint/types.js.map +1 -1
- package/build/branding.d.ts +5 -1
- package/build/branding.d.ts.map +1 -1
- package/build/branding.js +6 -0
- package/build/branding.js.map +1 -1
- package/build/connect/consts.d.ts +21 -0
- package/build/connect/consts.d.ts.map +1 -1
- package/build/connect/consts.js +21 -0
- package/build/connect/consts.js.map +1 -1
- package/build/connect/entrypoints.d.ts +15 -1
- package/build/connect/entrypoints.d.ts.map +1 -1
- package/build/connect/entrypoints.js +16 -1
- package/build/connect/entrypoints.js.map +1 -1
- package/build/connect/references.d.ts +14 -1
- package/build/connect/references.d.ts.map +1 -1
- package/build/connect/references.js +4 -0
- package/build/connect/references.js.map +1 -1
- package/build/connect/schemas.d.ts +12 -1
- package/build/connect/schemas.d.ts.map +1 -1
- package/build/connect/schemas.js +27 -1
- package/build/connect/schemas.js.map +1 -1
- package/build/connect/types.d.ts +27 -0
- package/build/connect/types.d.ts.map +1 -1
- package/build/consts.d.ts +1 -1
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +3 -0
- package/build/consts.js.map +1 -1
- package/build/metadata/types.d.ts +8 -0
- package/build/metadata/types.d.ts.map +1 -1
- package/build/metadata/types.js.map +1 -1
- package/build/planning/helpers.d.ts +2 -1
- package/build/planning/helpers.d.ts.map +1 -1
- package/build/planning/helpers.js +5 -2
- package/build/planning/helpers.js.map +1 -1
- package/build/planning/index.d.ts +1 -0
- package/build/planning/index.d.ts.map +1 -1
- package/build/planning/index.js +1 -0
- package/build/planning/index.js.map +1 -1
- package/build/planning/landing.d.ts +25 -0
- package/build/planning/landing.d.ts.map +1 -0
- package/build/planning/landing.js +38 -0
- package/build/planning/landing.js.map +1 -0
- package/build/planning/schemas.d.ts.map +1 -1
- package/build/planning/schemas.js +13 -0
- package/build/planning/schemas.js.map +1 -1
- package/build/planning/types.d.ts +28 -3
- package/build/planning/types.d.ts.map +1 -1
- package/build/scaffold/consts.d.ts +8 -0
- package/build/scaffold/consts.d.ts.map +1 -1
- package/build/scaffold/consts.js +7 -0
- package/build/scaffold/consts.js.map +1 -1
- package/build/scaffold/schemas.d.ts.map +1 -1
- package/build/scaffold/schemas.js +257 -12
- package/build/scaffold/schemas.js.map +1 -1
- package/build/scaffold/types.d.ts +119 -4
- package/build/scaffold/types.d.ts.map +1 -1
- package/build/skills/catalogue.d.ts.map +1 -1
- package/build/skills/catalogue.js +128 -24
- package/build/skills/catalogue.js.map +1 -1
- package/build/skills/consts.d.ts +5 -0
- package/build/skills/consts.d.ts.map +1 -1
- package/build/skills/consts.js +8 -0
- package/build/skills/consts.js.map +1 -1
- package/build/skills/roles.d.ts.map +1 -1
- package/build/skills/roles.js +11 -5
- package/build/skills/roles.js.map +1 -1
- package/build/types.d.ts +30 -0
- package/build/types.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/blueprint/case.ts +64 -16
- package/src/blueprint/resolve.ts +3 -0
- package/src/blueprint/types.ts +57 -1
- package/src/branding.ts +6 -0
- package/src/connect/consts.ts +22 -0
- package/src/connect/entrypoints.ts +26 -4
- package/src/connect/references.ts +16 -3
- package/src/connect/schemas.ts +33 -4
- package/src/connect/types.ts +28 -0
- package/src/consts.ts +3 -0
- package/src/metadata/types.ts +8 -0
- package/src/planning/helpers.ts +5 -2
- package/src/planning/index.ts +1 -0
- package/src/planning/landing.ts +43 -0
- package/src/planning/schemas.ts +13 -0
- package/src/planning/types.ts +29 -3
- package/src/scaffold/consts.ts +10 -0
- package/src/scaffold/schemas.ts +262 -13
- package/src/scaffold/types.ts +105 -5
- package/src/skills/catalogue.ts +129 -24
- package/src/skills/consts.ts +8 -0
- package/src/skills/roles.ts +11 -5
- package/src/types.ts +30 -0
- package/tests/blueprint.spec.ts +98 -0
- package/tests/branding.spec.ts +21 -1
- package/tests/connect-entrypoints.spec.ts +44 -5
- package/tests/planning.spec.ts +56 -1
- package/tests/scaffold.spec.ts +131 -0
- package/tests/skills.spec.ts +65 -2
package/src/scaffold/schemas.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { JSONSchemaType } from 'ajv'
|
|
2
2
|
|
|
3
3
|
import { ProjectArea } from '../areas/consts.js'
|
|
4
|
-
import { WidgetKind } from './consts.js'
|
|
4
|
+
import { BENTO_FRAGMENT_KINDS, WidgetKind } from './consts.js'
|
|
5
5
|
import type {
|
|
6
|
-
|
|
6
|
+
BentoFragment, GuestHomePlan, LandingGatePlan, ProductIdentity,
|
|
7
|
+
ScaffoldAreaPlan, ScaffoldPlan, ScaffoldStoryPlan, SketchContent,
|
|
7
8
|
} from './types.js'
|
|
8
9
|
|
|
9
10
|
const SketchContentSchema: JSONSchemaType<SketchContent> = {
|
|
@@ -160,6 +161,166 @@ const ScaffoldAreaPlanSchema: JSONSchemaType<ScaffoldAreaPlan> = {
|
|
|
160
161
|
additionalProperties: false,
|
|
161
162
|
}
|
|
162
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Every property added to the guest home after the first stored plans is OPTIONAL and `nullable`.
|
|
166
|
+
*
|
|
167
|
+
* This schema is not only what a model answers with: it is the validator of the project card's
|
|
168
|
+
* `scaffold` document (`VIABLE_PROJECT_SLOTS`), so a plan written before a field existed must
|
|
169
|
+
* still pass it whenever that plan is carried into a new revision. A field made required here is a
|
|
170
|
+
* project whose plan can no longer be revised.
|
|
171
|
+
*
|
|
172
|
+
* Counts ("four tiles", "5–8 chips") are stated in the descriptions and CLAMPED in code — never
|
|
173
|
+
* `minItems`/`maxItems`, which a provider's structured output refuses or ignores, and never a
|
|
174
|
+
* tuple.
|
|
175
|
+
*/
|
|
176
|
+
const nullableLink = (description: string) => ({
|
|
177
|
+
type: 'object' as const,
|
|
178
|
+
nullable: true as const,
|
|
179
|
+
description,
|
|
180
|
+
properties: {
|
|
181
|
+
label: { type: 'string' as const, description: 'The link text, a verb first, 2-4 words' },
|
|
182
|
+
href: {
|
|
183
|
+
type: 'string' as const, nullable: true as const,
|
|
184
|
+
description: 'OPTIONAL, normally omitted: absent means the default action (sign in, or a'
|
|
185
|
+
+ ' section of this page). Never invent an address'
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
required: ['label'] as 'label'[],
|
|
189
|
+
additionalProperties: false,
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
const BentoFragmentSchema: JSONSchemaType<BentoFragment> = {
|
|
193
|
+
type: 'object',
|
|
194
|
+
description: 'A small piece of the product\'s own interface drawn at the bottom of the tile, built'
|
|
195
|
+
+ ' from its own records - never a paragraph',
|
|
196
|
+
properties: {
|
|
197
|
+
kind: {
|
|
198
|
+
type: 'string',
|
|
199
|
+
enum: BENTO_FRAGMENT_KINDS,
|
|
200
|
+
description: 'list (label + value rows, e.g. ingredients and amounts), note (one short remark'
|
|
201
|
+
+ ' in the label), people (name in the label, role in the value), steps (numbered labels)'
|
|
202
|
+
},
|
|
203
|
+
title: {
|
|
204
|
+
type: 'string', nullable: true,
|
|
205
|
+
description: 'OPTIONAL small caption over the fragment, e.g. "INGREDIENTS · 1 LOAF"'
|
|
206
|
+
},
|
|
207
|
+
rows: {
|
|
208
|
+
type: 'array',
|
|
209
|
+
items: {
|
|
210
|
+
type: 'object',
|
|
211
|
+
properties: {
|
|
212
|
+
label: { type: 'string', description: 'The row\'s main text, a few words' },
|
|
213
|
+
value: {
|
|
214
|
+
type: 'string', nullable: true,
|
|
215
|
+
description: 'The row\'s second column - an amount, a role; omit it for note and steps'
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
required: ['label'],
|
|
219
|
+
additionalProperties: false,
|
|
220
|
+
},
|
|
221
|
+
description: '2-4 rows of realistic records of this product'
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
required: ['kind', 'rows'],
|
|
225
|
+
additionalProperties: false,
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const LandingGatePlanSchema: JSONSchemaType<LandingGatePlan> = {
|
|
229
|
+
type: 'object',
|
|
230
|
+
title: 'LandingGatePlan',
|
|
231
|
+
description: 'The working entry into the key end-user workflow, drawn in the hero instead of the'
|
|
232
|
+
+ ' call-to-action buttons: a guest makes a few picks, sees sample results ranked against them,'
|
|
233
|
+
+ ' and signs in to continue with the picks carried over. Every text is in the end user\'s words',
|
|
234
|
+
properties: {
|
|
235
|
+
story: {
|
|
236
|
+
type: 'string',
|
|
237
|
+
description: 'The code of the landing story, copied EXACTLY as given to you. Never shown'
|
|
238
|
+
},
|
|
239
|
+
target: {
|
|
240
|
+
type: 'string', nullable: true,
|
|
241
|
+
description: 'Leave it out - the platform fills it'
|
|
242
|
+
},
|
|
243
|
+
question: {
|
|
244
|
+
type: 'string',
|
|
245
|
+
description: 'The question the gate asks, 3-6 words, in the end user\'s words - e.g. "What\'s'
|
|
246
|
+
+ ' in your pantry?"'
|
|
247
|
+
},
|
|
248
|
+
hint: { type: 'string', description: 'A short reassurance beside it, e.g. "No account needed"' },
|
|
249
|
+
label: {
|
|
250
|
+
type: 'string',
|
|
251
|
+
description: 'The accessible name of the group of choices, 2-3 words - e.g. "Your pantry"'
|
|
252
|
+
},
|
|
253
|
+
inputs: {
|
|
254
|
+
type: 'array',
|
|
255
|
+
items: { type: 'string' },
|
|
256
|
+
description: '5 to 8 things a guest can pick, 1-3 words each, from the story\'s own domain.'
|
|
257
|
+
+ ' Nothing personal, nothing sensitive, nothing that needs an account'
|
|
258
|
+
},
|
|
259
|
+
selected: {
|
|
260
|
+
type: 'array',
|
|
261
|
+
items: { type: 'string' },
|
|
262
|
+
description: '4 or 5 of the inputs above, copied exactly, pre-selected so the first view'
|
|
263
|
+
+ ' already shows results'
|
|
264
|
+
},
|
|
265
|
+
results: {
|
|
266
|
+
type: 'array',
|
|
267
|
+
items: {
|
|
268
|
+
type: 'object',
|
|
269
|
+
properties: {
|
|
270
|
+
title: { type: 'string', description: 'A realistic record title, 2-4 words' },
|
|
271
|
+
meta: {
|
|
272
|
+
type: 'string',
|
|
273
|
+
description: 'One short line: how well it fits and one fact, e.g. "Uses all 3 · 3 h 20 min"'
|
|
274
|
+
},
|
|
275
|
+
needs: {
|
|
276
|
+
type: 'array',
|
|
277
|
+
items: { type: 'string' },
|
|
278
|
+
description: '2-4 of the inputs above, copied exactly, that this record uses'
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
required: ['title', 'meta', 'needs'],
|
|
282
|
+
additionalProperties: false,
|
|
283
|
+
},
|
|
284
|
+
description: '3 or 4 sample records the picks are ranked against - illustrative, never real data'
|
|
285
|
+
},
|
|
286
|
+
count: {
|
|
287
|
+
type: 'object',
|
|
288
|
+
properties: {
|
|
289
|
+
one: { type: 'string', description: 'The count for one match, with {n}: "{n} bake matches"' },
|
|
290
|
+
many: { type: 'string', description: 'The count for several, with {n}: "{n} bakes match"' },
|
|
291
|
+
none: { type: 'string', description: 'The count for none, e.g. "No matches yet"' },
|
|
292
|
+
},
|
|
293
|
+
required: ['one', 'many', 'none'],
|
|
294
|
+
additionalProperties: false,
|
|
295
|
+
description: 'The live count label; write the literal token {n} where the number goes'
|
|
296
|
+
},
|
|
297
|
+
empty: {
|
|
298
|
+
type: 'string',
|
|
299
|
+
description: 'One line shown when nothing matches, saying what to pick - e.g. "Pick two or more'
|
|
300
|
+
+ ' ingredients to see what you can bake."'
|
|
301
|
+
},
|
|
302
|
+
note: {
|
|
303
|
+
type: 'string',
|
|
304
|
+
description: 'One short line beside the button: signing in is by email and the picks come'
|
|
305
|
+
+ ' along - e.g. "Sign in with your email. Your picks come with you."'
|
|
306
|
+
},
|
|
307
|
+
cta: {
|
|
308
|
+
type: 'string',
|
|
309
|
+
description: 'The button label, a verb first, 2-3 words and an arrow - e.g. "Open recipes →"'
|
|
310
|
+
},
|
|
311
|
+
lock: {
|
|
312
|
+
type: 'string',
|
|
313
|
+
description: 'What unlocks after sign-in, as the accessible name of a lock icon - e.g. "Full'
|
|
314
|
+
+ ' method after sign-in"'
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
required: [
|
|
318
|
+
'story', 'question', 'hint', 'label', 'inputs', 'selected', 'results', 'count', 'empty',
|
|
319
|
+
'note', 'cta', 'lock',
|
|
320
|
+
],
|
|
321
|
+
additionalProperties: false,
|
|
322
|
+
}
|
|
323
|
+
|
|
163
324
|
const GuestHomePlanSchema: JSONSchemaType<GuestHomePlan> = {
|
|
164
325
|
type: 'object',
|
|
165
326
|
title: 'GuestHomePlan',
|
|
@@ -170,16 +331,27 @@ const GuestHomePlanSchema: JSONSchemaType<GuestHomePlan> = {
|
|
|
170
331
|
properties: {
|
|
171
332
|
headline: {
|
|
172
333
|
type: 'string',
|
|
173
|
-
description: 'The product\'s promise in one line,
|
|
334
|
+
description: 'The product\'s promise in one short concrete line, at most 6 words. Never the'
|
|
335
|
+
+ ' product name alone'
|
|
336
|
+
},
|
|
337
|
+
sub: {
|
|
338
|
+
type: 'string',
|
|
339
|
+
description: 'One or two sentences: what it does and who it is for. No invented numbers'
|
|
340
|
+
},
|
|
341
|
+
cta: {
|
|
342
|
+
type: 'string',
|
|
343
|
+
description: 'The primary button label, a verb first, 2-3 words. Shown only when the page'
|
|
344
|
+
+ ' has no gate'
|
|
174
345
|
},
|
|
175
|
-
sub: { type: 'string', description: 'One or two sentences: what it does and who it is for' },
|
|
176
|
-
cta: { type: 'string', description: 'The call-to-action label, 2-3 words' },
|
|
177
346
|
eyebrow: {
|
|
178
347
|
type: 'string', nullable: true,
|
|
179
|
-
description: '
|
|
180
|
-
+ ' the
|
|
181
|
-
+ ' default: the headline is what positions the product'
|
|
348
|
+
description: 'Omit it. The page shows no badge above the headline; the headline is what'
|
|
349
|
+
+ ' positions the product'
|
|
182
350
|
},
|
|
351
|
+
secondary: nullableLink('OPTIONAL second button beside the primary one, when there is no'
|
|
352
|
+
+ ' gate - e.g. "Browse bakes"'),
|
|
353
|
+
browse: nullableLink('OPTIONAL muted link under the gate or the buttons for someone not'
|
|
354
|
+
+ ' ready to start - the whole line, e.g. "Just browsing? Explore bakes"'),
|
|
183
355
|
},
|
|
184
356
|
required: ['headline', 'sub', 'cta'],
|
|
185
357
|
additionalProperties: false,
|
|
@@ -213,11 +385,13 @@ const GuestHomePlanSchema: JSONSchemaType<GuestHomePlan> = {
|
|
|
213
385
|
properties: {
|
|
214
386
|
title: { type: 'string', description: 'The capability, 2-4 words' },
|
|
215
387
|
text: { type: 'string', description: 'One sentence on what it lets someone do' },
|
|
388
|
+
fragment: { ...BentoFragmentSchema, nullable: true },
|
|
216
389
|
},
|
|
217
390
|
required: ['title', 'text'],
|
|
218
391
|
additionalProperties: false,
|
|
219
392
|
},
|
|
220
|
-
description: '
|
|
393
|
+
description: '4 capabilities - the bento tiles "What you get" - taken from what the'
|
|
394
|
+
+ ' specification actually describes. Give most of them a fragment'
|
|
221
395
|
},
|
|
222
396
|
testimonials: {
|
|
223
397
|
type: 'array',
|
|
@@ -231,7 +405,80 @@ const GuestHomePlanSchema: JSONSchemaType<GuestHomePlan> = {
|
|
|
231
405
|
required: ['quote', 'name', 'role'],
|
|
232
406
|
additionalProperties: false,
|
|
233
407
|
},
|
|
234
|
-
description: '2
|
|
408
|
+
description: '2 illustrative testimonials. No ratings, no numbers, no company names'
|
|
409
|
+
},
|
|
410
|
+
steps: {
|
|
411
|
+
type: 'array', nullable: true,
|
|
412
|
+
items: {
|
|
413
|
+
type: 'object',
|
|
414
|
+
properties: {
|
|
415
|
+
title: { type: 'string', description: 'The step as an action, 2-5 words' },
|
|
416
|
+
text: { type: 'string', description: 'One short sentence on what happens in it' },
|
|
417
|
+
},
|
|
418
|
+
required: ['title', 'text'],
|
|
419
|
+
additionalProperties: false,
|
|
420
|
+
},
|
|
421
|
+
description: '"How it works": 3 steps taken from the main flow, in order, the last one'
|
|
422
|
+
+ ' delivering the value'
|
|
423
|
+
},
|
|
424
|
+
labels: {
|
|
425
|
+
type: 'object', nullable: true,
|
|
426
|
+
description: 'The small label over each section and that section\'s heading, in the product\'s'
|
|
427
|
+
+ ' language. Labels are 1-3 plain words; headings are one short concrete sentence',
|
|
428
|
+
properties: {
|
|
429
|
+
steps: { type: 'string', nullable: true, description: 'e.g. "How it works"' },
|
|
430
|
+
stepsTitle: {
|
|
431
|
+
type: 'string', nullable: true,
|
|
432
|
+
description: 'e.g. "From pantry to proofing basket in three steps."'
|
|
433
|
+
},
|
|
434
|
+
features: { type: 'string', nullable: true, description: 'e.g. "What you get"' },
|
|
435
|
+
featuresTitle: {
|
|
436
|
+
type: 'string', nullable: true, description: 'e.g. "Everything behind the bake."'
|
|
437
|
+
},
|
|
438
|
+
testimonials: { type: 'string', nullable: true, description: 'e.g. "Community"' },
|
|
439
|
+
testimonialsTitle: {
|
|
440
|
+
type: 'string', nullable: true, description: 'e.g. "Bakers who made it at home."'
|
|
441
|
+
},
|
|
442
|
+
problem: { type: 'string', nullable: true, description: 'e.g. "The problem"' },
|
|
443
|
+
solution: { type: 'string', nullable: true, description: 'e.g. "The solution"' },
|
|
444
|
+
},
|
|
445
|
+
required: [],
|
|
446
|
+
additionalProperties: false,
|
|
447
|
+
},
|
|
448
|
+
closing: {
|
|
449
|
+
type: 'object', nullable: true,
|
|
450
|
+
description: 'The closing band at the foot of the page',
|
|
451
|
+
properties: {
|
|
452
|
+
headline: { type: 'string', description: 'A short invitation, at most 6 words' },
|
|
453
|
+
lead: { type: 'string', description: 'One or two sentences under it' },
|
|
454
|
+
primary: { type: 'string', description: 'The primary button label, a verb first, 2-4 words' },
|
|
455
|
+
secondary: nullableLink('OPTIONAL second button, e.g. "Browse bakes"'),
|
|
456
|
+
links: {
|
|
457
|
+
type: 'array', nullable: true,
|
|
458
|
+
items: {
|
|
459
|
+
type: 'object',
|
|
460
|
+
properties: {
|
|
461
|
+
title: { type: 'string', description: 'What the card leads to, 2-3 words' },
|
|
462
|
+
text: { type: 'string', description: 'One short line about it' },
|
|
463
|
+
href: {
|
|
464
|
+
type: 'string', nullable: true,
|
|
465
|
+
description: 'OPTIONAL, normally omitted. Never invent an address'
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
required: ['title', 'text'],
|
|
469
|
+
additionalProperties: false,
|
|
470
|
+
},
|
|
471
|
+
description: '2 link cards beside the closing headline'
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
required: ['headline', 'lead', 'primary'],
|
|
475
|
+
additionalProperties: false,
|
|
476
|
+
},
|
|
477
|
+
gate: {
|
|
478
|
+
...LandingGatePlanSchema,
|
|
479
|
+
nullable: true,
|
|
480
|
+
description: 'ONLY when you were given a landing story; omit it otherwise. '
|
|
481
|
+
+ LandingGatePlanSchema.description,
|
|
235
482
|
},
|
|
236
483
|
},
|
|
237
484
|
required: ['hero', 'problem', 'solution', 'features', 'testimonials'],
|
|
@@ -278,9 +525,11 @@ export const ScaffoldPlanSchema: JSONSchemaType<ScaffoldPlan> = {
|
|
|
278
525
|
},
|
|
279
526
|
motifs: {
|
|
280
527
|
type: 'string',
|
|
281
|
-
description: 'Two or three sentences
|
|
282
|
-
+ '
|
|
283
|
-
+ '
|
|
528
|
+
description: 'Two or three sentences describing this product\'s hero illustration as a SCENE:'
|
|
529
|
+
+ ' one main object of its domain resting on a surface, one or two smaller objects beside'
|
|
530
|
+
+ ' it, and one simplified piece of the product\'s interface, each in its natural muted'
|
|
531
|
+
+ ' colours. Concrete recognisable objects - never abstract geometry, never floating'
|
|
532
|
+
+ ' circles, rings or dots, never people, text or a described photograph'
|
|
284
533
|
},
|
|
285
534
|
},
|
|
286
535
|
required: ['identity', 'guestHome', 'areas', 'stories', 'motifs'],
|
package/src/scaffold/types.ts
CHANGED
|
@@ -89,22 +89,122 @@ export interface ProductIdentity {
|
|
|
89
89
|
description: string
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* A labelled link on the landing page — a secondary pill, the "just browsing" link, a closing card.
|
|
94
|
+
*
|
|
95
|
+
* `href` is OPTIONAL and normally absent: absent means the default action the stamper decides
|
|
96
|
+
* (start sign-in, or jump to a section of the page itself). A model never knows the application's
|
|
97
|
+
* addresses, and an invented one is a link that 404s on the public face of the product.
|
|
98
|
+
*/
|
|
99
|
+
export interface PlanLink {
|
|
100
|
+
label: string
|
|
101
|
+
href?: string
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The four shapes a bento tile's product fragment can take — each one a stamped primitive. */
|
|
105
|
+
export type BentoFragmentKind = 'list' | 'note' | 'people' | 'steps'
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* A small piece of the product's own interface, drawn at the bottom of a bento tile.
|
|
109
|
+
*
|
|
110
|
+
* Structured for the same reason {@link SketchContent} is: the fragment is STAMPED, so it must be
|
|
111
|
+
* rows a component can lay out, never a paragraph describing them. Built from the product's own
|
|
112
|
+
* records, and decorative — the stamper renders it `aria-hidden`.
|
|
113
|
+
*/
|
|
114
|
+
export interface BentoFragment {
|
|
115
|
+
kind: BentoFragmentKind
|
|
116
|
+
/** Small caption over the fragment ("INGREDIENTS · 1 LOAF", "Baker's note"). */
|
|
117
|
+
title?: string
|
|
118
|
+
/** list: {label, value}; note: {label: text}; people: {label: name, value: role}; steps: {label}. */
|
|
119
|
+
rows: { label: string, value?: string }[]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The landing gate — the working entry into the product's key END-USER workflow, drawn in the
|
|
124
|
+
* hero where the call-to-action pills would otherwise be.
|
|
125
|
+
*
|
|
126
|
+
* A guest makes a few non-sensitive choices on the landing page, sees sample results ranked
|
|
127
|
+
* against them, and signs in to continue on the story's full-scale screen with those choices
|
|
128
|
+
* carried over. Every field is copy or sample data in the end user's own words; nothing here is
|
|
129
|
+
* an address the model invented — `target` is filled by code.
|
|
130
|
+
*/
|
|
131
|
+
export interface LandingGatePlan {
|
|
132
|
+
/** The key end-user story's code — never rendered. */
|
|
133
|
+
story: string
|
|
134
|
+
/** Entrypoint ALIAS of the story's full-scale screen in the user area (filled by code, not the model). */
|
|
135
|
+
target?: string
|
|
136
|
+
/** The question the gate asks, in the end user's words: "What's in your pantry?". */
|
|
137
|
+
question: string
|
|
138
|
+
/** The reassurance beside it: "No account needed". */
|
|
139
|
+
hint: string
|
|
140
|
+
/** The accessible name of the chip group: "Your pantry". */
|
|
141
|
+
label: string
|
|
142
|
+
/** 5–8 chip labels the guest picks from. */
|
|
143
|
+
inputs: string[]
|
|
144
|
+
/** 4–5 of `inputs`, pre-selected so the first view already shows results. */
|
|
145
|
+
selected: string[]
|
|
146
|
+
/** 3–4 sample records; `needs` is the subset of `inputs` each one uses. */
|
|
147
|
+
results: { title: string, meta: string, needs: string[] }[]
|
|
148
|
+
/** The live count label; `{n}` is replaced by the number ("{n} bakes match"). */
|
|
149
|
+
count: { one: string, many: string, none: string }
|
|
150
|
+
/** The line shown when nothing matches the picks. */
|
|
151
|
+
empty: string
|
|
152
|
+
/** The line beside the CTA: "Sign in with your email. Your picks come with you.". */
|
|
153
|
+
note: string
|
|
154
|
+
/** The CTA label: "Open recipes →". */
|
|
155
|
+
cta: string
|
|
156
|
+
/** The accessible name of a result row's lock icon: "Full method after sign-in". */
|
|
157
|
+
lock: string
|
|
158
|
+
}
|
|
159
|
+
|
|
92
160
|
/** The guest area's landing page — the product's public face. */
|
|
93
161
|
export interface GuestHomePlan {
|
|
94
162
|
hero: {
|
|
95
163
|
headline: string
|
|
96
164
|
sub: string
|
|
165
|
+
/** The primary pill's label — shown only when the page has no {@link GuestHomePlan.gate}. */
|
|
97
166
|
cta: string
|
|
98
167
|
/**
|
|
99
|
-
* A badge above the headline — OPTIONAL and
|
|
100
|
-
*
|
|
168
|
+
* A badge above the headline — OPTIONAL and kept only so an older stored plan still reads.
|
|
169
|
+
* The page renders no badge: the headline is what positions the product.
|
|
101
170
|
*/
|
|
102
171
|
eyebrow?: string
|
|
172
|
+
/** The second pill beside the primary one, when there is no gate. */
|
|
173
|
+
secondary?: PlanLink
|
|
174
|
+
/** The muted text link under the gate or the pills: "Just browsing? Explore bakes ›". */
|
|
175
|
+
browse?: PlanLink
|
|
103
176
|
}
|
|
104
177
|
problem: { title: string, text: string }
|
|
105
178
|
solution: { title: string, text: string }
|
|
106
|
-
|
|
179
|
+
/** The bento tiles — four, each optionally anchored by a fragment of the product's interface. */
|
|
180
|
+
features: { title: string, text: string, fragment?: BentoFragment }[]
|
|
107
181
|
testimonials: { quote: string, name: string, role: string }[]
|
|
182
|
+
/** "How it works" — three steps taken from the main flow. */
|
|
183
|
+
steps?: { title: string, text: string }[]
|
|
184
|
+
/** The small section labels and section headings; absent ones fall back to the stamper's own. */
|
|
185
|
+
labels?: {
|
|
186
|
+
steps?: string
|
|
187
|
+
stepsTitle?: string
|
|
188
|
+
features?: string
|
|
189
|
+
featuresTitle?: string
|
|
190
|
+
testimonials?: string
|
|
191
|
+
testimonialsTitle?: string
|
|
192
|
+
problem?: string
|
|
193
|
+
solution?: string
|
|
194
|
+
}
|
|
195
|
+
/** The closing band: a headline, a lead, the two pills and two link cards. */
|
|
196
|
+
closing?: {
|
|
197
|
+
headline: string
|
|
198
|
+
lead: string
|
|
199
|
+
primary: string
|
|
200
|
+
secondary?: PlanLink
|
|
201
|
+
links?: { title: string, text: string, href?: string }[]
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* The landing gate — present only when a gate story was decided for the project. When it is,
|
|
205
|
+
* it REPLACES the hero's pills.
|
|
206
|
+
*/
|
|
207
|
+
gate?: LandingGatePlan
|
|
108
208
|
}
|
|
109
209
|
|
|
110
210
|
/**
|
|
@@ -120,8 +220,8 @@ export interface ScaffoldPlan {
|
|
|
120
220
|
areas: ScaffoldAreaPlan[]
|
|
121
221
|
stories: ScaffoldStoryPlan[]
|
|
122
222
|
/**
|
|
123
|
-
* How this product's illustrations should look —
|
|
124
|
-
*
|
|
223
|
+
* How this product's illustrations should look — the scene, its domain objects and their
|
|
224
|
+
* palette. The one field here that is prose, because it is read by a model.
|
|
125
225
|
*/
|
|
126
226
|
motifs: string
|
|
127
227
|
}
|
package/src/skills/catalogue.ts
CHANGED
|
@@ -596,39 +596,66 @@ carries. Introducing a second spelling orphans every grant made against the firs
|
|
|
596
596
|
files, not a copy of their own. They are already there: import them, never delete, rename or
|
|
597
597
|
move one, and never re-add a primitive the directory already carries.
|
|
598
598
|
- Styling is Tailwind utility classes. There are no CSS modules and no styled-components.
|
|
599
|
+
- The GROUND is white in light mode and black in dark mode, and it stays that way: every page,
|
|
600
|
+
section, band and header sits on \`bg-background\`. Colour comes from ONE product accent —
|
|
601
|
+
\`bg-primary\` / \`text-primary\` — and only on the primary button, a section's small label, the rule
|
|
602
|
+
over the first step and a check mark. It never comes from a background, a tint or a band.
|
|
603
|
+
|
|
604
|
+
<section className="bg-primary/10 py-20">...</section> // WRONG
|
|
605
|
+
<section className="bg-gradient-to-b from-amber-50 to-white py-20">...</section> // WRONG
|
|
606
|
+
<section className="border-t py-20">...</section> // right
|
|
607
|
+
|
|
608
|
+
- Depth comes from exactly three things: a 1px HAIRLINE (\`border\`, the \`--border\` token), the
|
|
609
|
+
NEUTRAL TILE (\`bg-muted\` — the only tinted surface: feature tiles, testimonial tiles, result
|
|
610
|
+
rows), and ONE shadow, \`shadow-floating\`, which belongs to the one card that floats (the landing
|
|
611
|
+
gate). An ordinary card is \`rounded-xl border bg-card\` and carries no shadow. There are no
|
|
612
|
+
gradients, no glass, no backdrop blur, no glow and no coloured shadows anywhere in the interface.
|
|
613
|
+
|
|
614
|
+
<div className="rounded-xl bg-white/60 shadow-2xl backdrop-blur-md p-6" /> // WRONG
|
|
615
|
+
<div className="rounded-xl border bg-card p-6" /> // right
|
|
616
|
+
<li className="rounded-[32px] bg-muted p-10" /> // right — a tile
|
|
617
|
+
|
|
618
|
+
- TYPE carries the brand. Headings are the theme's heavy grotesk — \`font-extrabold\` with tight
|
|
619
|
+
negative tracking (\`tracking-tight\`, or \`tracking-[-0.045em]\` on a section heading) and
|
|
620
|
+
\`text-balance\`; paragraphs are \`text-muted-foreground\` with \`text-pretty\` and a readable
|
|
621
|
+
measure (\`max-w-[52ch]\`); whitespace is generous. Never set a different font family.
|
|
622
|
+
- A section opens with a small LABEL — a plain \`<p className="text-[15px] font-bold text-primary">\`
|
|
623
|
+
— then its \`<h2>\`. A label is text: never a chip, a pill or a badge.
|
|
624
|
+
- Buttons are pills. The primary one is \`cta-btn\` (on an \`<a>\` or a bare \`<button>\`, never on a
|
|
625
|
+
shadcn \`Button\`, whose own \`bg-\` utility would win); the secondary one is
|
|
626
|
+
\`rounded-full border-[1.5px] border-foreground text-foreground hover:bg-muted\`. Labels are verbs.
|
|
627
|
+
- Motion is small and belongs to the picture: \`reveal\` fades a block in once as it enters, and
|
|
628
|
+
\`float-slow\` / \`drift-slow\` lift PARTS OF AN ILLUSTRATION by a few pixels. Nothing spins,
|
|
629
|
+
pulses, orbits or scales, and nothing moves for someone who asked for reduced motion.
|
|
630
|
+
- The stylesheet still defines the older classes \`gradient-heading\`, \`glass-card\`, \`eyebrow\`,
|
|
631
|
+
\`glow-orb\`, \`shadow-soft\`, \`shadow-raised\`, \`shadow-glow\`, \`pulse-soft\` and \`spin-slow\`,
|
|
632
|
+
because screens written earlier use them. They are REPAINTED flat — \`gradient-heading\` is solid
|
|
633
|
+
ink, \`glass-card\` a neutral tile, \`eyebrow\` a plain accent label, \`glow-orb\` hidden, the three
|
|
634
|
+
shadows and the two animations nothing at all. Never reach for one of them as decoration: it
|
|
635
|
+
paints nothing, or it paints the flat thing you could have written directly. Do not invent other
|
|
636
|
+
classes in the same family.
|
|
637
|
+
- Overrides: change any of the look rules above — the ground, the accent, depth, type, motion —
|
|
638
|
+
ONLY when the customer's specification explicitly asks: it names a background colour, a
|
|
639
|
+
dark-only or coloured theme, a brand palette, or gradients. The product's subject matter is not
|
|
640
|
+
a request — a bakery is not a warm background and a bank is not a blue one.
|
|
599
641
|
- Colour comes in PAIRS. Every surface token in this theme has a foreground partner —
|
|
600
642
|
\`--background\`/\`--foreground\`, \`--card\`/\`--card-foreground\`, \`--primary\`/\`--primary-foreground\`,
|
|
601
643
|
and the same for \`secondary\`, \`muted\`, \`accent\`, \`popover\`, \`destructive\` and \`sidebar\`. Painting a
|
|
602
644
|
surface is therefore never a single decision: give the element its partner in the SAME
|
|
603
645
|
\`className\`, or the text keeps the colour meant for the surface underneath and vanishes wherever
|
|
604
|
-
|
|
646
|
+
that surface is dark.
|
|
605
647
|
|
|
606
|
-
<
|
|
607
|
-
<
|
|
648
|
+
<a className="rounded-full bg-primary px-7 py-3">Get started</a> // WRONG
|
|
649
|
+
<a className="rounded-full bg-primary text-primary-foreground px-7 py-3">Get started</a> // right
|
|
608
650
|
|
|
609
|
-
Nothing catches the wrong form — it compiles and it renders. It is unreadable only
|
|
610
|
-
|
|
651
|
+
Nothing catches the wrong form — it compiles and it renders. It is unreadable only where the
|
|
652
|
+
accent happens to be dark, which is why it survives to the finished screen.
|
|
611
653
|
- Never paint a surface with a raw or palette colour — \`bg-slate-900\`, \`bg-[#101820]\`,
|
|
612
654
|
\`style={{ background: '#101820' }}\`. Those have no foreground partner, so nothing keeps the text
|
|
613
|
-
on them legible, and they ignore the theme. Every surface is one of the tokens
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
panel with depth), \`eyebrow\` (the small chip above a headline), \`cta-btn\` (the primary call to
|
|
618
|
-
action, on an \`<a>\` or a bare \`<button>\`, never on a shadcn \`Button\` whose own \`bg-\`
|
|
619
|
-
utility would win), \`glow-orb\` (a soft colour field behind a section, always
|
|
620
|
-
\`pointer-events-none\` and behind the content); the elevation utilities \`shadow-soft\`,
|
|
621
|
-
\`shadow-raised\`, \`shadow-floating\`, \`shadow-glow\`; the idle-motion classes \`reveal\`,
|
|
622
|
-
\`float-slow\`, \`drift-slow\`, \`pulse-soft\`, \`spin-slow\`; and gradients whose stops are THEME
|
|
623
|
-
tokens with opacity. None of these is a raw colour.
|
|
624
|
-
|
|
625
|
-
<div className="bg-[radial-gradient(#818cf8,transparent)] blur-[120px]" /> // WRONG
|
|
626
|
-
<span className="glow-orb h-80 w-96 -z-10" aria-hidden /> // right
|
|
627
|
-
<div className="bg-gradient-to-br from-primary/20 to-transparent p-6" /> // right
|
|
628
|
-
|
|
629
|
-
Keep a gradient under a text element light — \`/20\` or less — unless the element also carries a
|
|
630
|
-
\`-foreground\` partner class. These class names are defined in the project's stylesheet; do not
|
|
631
|
-
invent others in the same family.
|
|
655
|
+
on them legible, and they ignore the theme and its dark mode. Every surface is one of the tokens
|
|
656
|
+
above.
|
|
657
|
+
- Every interactive element keeps its visible \`focus-visible\` ring, is at least 44px tall to touch,
|
|
658
|
+
and never signals anything by hover alone.
|
|
632
659
|
- Text that is NOT on a coloured surface takes NO colour class: it already inherits the readable
|
|
633
660
|
one. Never add \`text-white\`, \`text-black\` or a \`-foreground\` class "to be safe" — on an ordinary
|
|
634
661
|
panel that is the same fault inverted. \`text-muted-foreground\` for secondary text is the
|
|
@@ -793,6 +820,84 @@ symbols from the component name (PascalCase of the component definition — for
|
|
|
793
820
|
Both sides must use these exact names. This is a DEFAULT: when a "View-model contract"
|
|
794
821
|
block is present in the task, it lists the symbols that actually exist and overrides this
|
|
795
822
|
convention entirely.
|
|
823
|
+
`),
|
|
824
|
+
|
|
825
|
+
skill(ViableSkill.LandingGate, 'The landing gate — from the landing page into the app', `
|
|
826
|
+
A product can let a guest START its key end-user workflow on the landing page, before any account
|
|
827
|
+
exists, and finish it in the app after signing in with the choices they made carried over. The
|
|
828
|
+
card that does this is the LANDING GATE. It sits in the hero where the call-to-action buttons
|
|
829
|
+
would be, and it REPLACES them.
|
|
830
|
+
|
|
831
|
+
**Which story is gate-worthy.** At most ONE per project, and only a story that is all of:
|
|
832
|
+
- in the \`user\` area — the signed-in END user's own workflow, never an operator's, an owner's or
|
|
833
|
+
one a guest already completes without signing in;
|
|
834
|
+
- a FIRST-VALUE step — something useful comes out of it the moment the guest acts;
|
|
835
|
+
- begun with NON-SENSITIVE input — a few picks, a filter, a short text. Never a payment, an
|
|
836
|
+
address, an upload, contact details or anything else about the person.
|
|
837
|
+
A pantry-to-recipe finder is gate-worthy: pick ingredients, see what you can bake. Checking out a
|
|
838
|
+
basket, setting up a profile or reviewing a queue of requests is not. When no story qualifies
|
|
839
|
+
there is no gate and the hero keeps its buttons — a correct answer, not a failure.
|
|
840
|
+
|
|
841
|
+
**The card.** The sketch is \`@/components/home/gate\`; the implemented one is
|
|
842
|
+
\`@/components/home/landing-gate\` and exports the same \`LandingGate\` with the same props.
|
|
843
|
+
- Its root is \`<div id="gate" data-home-gate>\` — never a \`<section>\`, \`<aside>\`, \`<header>\`
|
|
844
|
+
or \`<footer>\`; the page already has exactly one of each landmark it needs.
|
|
845
|
+
- A question in the end user's words as its heading, and a "No account needed" hint beside it.
|
|
846
|
+
- 5 to 8 choice chips, each a \`<button type="button" aria-pressed={selected}>\` inside a
|
|
847
|
+
\`role="group"\` with an \`aria-label\`, 4 or 5 pre-selected so the first view already shows
|
|
848
|
+
results. When choices do not fit, 1 to 3 labelled fields instead.
|
|
849
|
+
- A count with \`aria-live="polite"\`, at most 2 result rows re-ranked on every change, and one
|
|
850
|
+
line for when nothing matches. A row's lock icon is named "Full details after sign-in".
|
|
851
|
+
- A one-line note ("Sign in with your email. Your picks come with you.") and ONE primary button,
|
|
852
|
+
straight after the choices in tab order. No second button beside it.
|
|
853
|
+
- The sketch works signed out and offline from its sample records. The implemented card may call
|
|
854
|
+
a PUBLIC endpoint; never one that needs a signed-in user.
|
|
855
|
+
|
|
856
|
+
**The handoff.** The button carries the guest's choices across sign-in in \`sessionStorage\`,
|
|
857
|
+
through \`@/lib/handoff\` — never in the URL, never in a cookie, never through the server:
|
|
858
|
+
|
|
859
|
+
import { useLandingStart } from '@/lib/handoff'
|
|
860
|
+
|
|
861
|
+
const start = useLandingStart(target)
|
|
862
|
+
<button type="button" className="cta-btn" onClick={() => start({ picks })}>{cta}</button>
|
|
863
|
+
|
|
864
|
+
- \`target\` is the ALIAS of the story's own full-scale screen in the user area — the prop the guest
|
|
865
|
+
home passes — never a URL.
|
|
866
|
+
- \`useLandingStart(target)\` is the whole button: it writes the handoff, then navigates to
|
|
867
|
+
\`target\` when the visitor is already signed in, or starts the ordinary sign-in otherwise. After
|
|
868
|
+
sign-in the guest home's \`Hero\` (\`useLandingContinuation\`) sends the visitor on to \`target\`.
|
|
869
|
+
- NEVER \`useLogin(target)\` for the gate: its continuation navigates straight to the guarded screen,
|
|
870
|
+
which answers a signed-out visitor with "Sign in required", and that card's own sign-in lands on the
|
|
871
|
+
home page — the full-scale screen is never reached.
|
|
872
|
+
- The handler \`useLandingStart\` returns is SYNCHRONOUS, and the click calls it in the same gesture.
|
|
873
|
+
A sign-in that has to open a window is eaten by the popup blocker once anything has been awaited.
|
|
874
|
+
|
|
875
|
+
onClick={async () => { await saveDraft(picks); start({ picks }) }} // WRONG — the gesture is gone
|
|
876
|
+
href={'/frontoffice/recipes?picks=' + picks.join(',')} // WRONG — a URL, the picks in it, no sign-in
|
|
877
|
+
|
|
878
|
+
**The full-scale screen** — the story's own screen in the user area — reads the handoff once in its
|
|
879
|
+
view model, pre-fills from it, says so in one line, and clears it:
|
|
880
|
+
|
|
881
|
+
import { clearLandingHandoff, readLandingHandoff } from '@/lib/handoff'
|
|
882
|
+
|
|
883
|
+
useEffect(() => {
|
|
884
|
+
const carried = readLandingHandoff<{ picks: string[] }>(alias)
|
|
885
|
+
if (carried == null) return
|
|
886
|
+
setPicks(carried.picks)
|
|
887
|
+
setCarriedOver(true) // "We kept your 4 picks from the home page."
|
|
888
|
+
clearLandingHandoff()
|
|
889
|
+
}, [alias])
|
|
890
|
+
|
|
891
|
+
\`alias\` is the screen's own alias — the same string the gate was given as \`target\`. A missing,
|
|
892
|
+
expired or foreign handoff reads as \`null\`, and the screen then starts empty exactly as it does for
|
|
893
|
+
someone who arrived from the menu; never treat its absence as an error.
|
|
894
|
+
|
|
895
|
+
**Never:**
|
|
896
|
+
- ask for an account, a payment, an email address or any personal detail before sign-in — the gate
|
|
897
|
+
collects choices and nothing else, and the handoff stores only those choices;
|
|
898
|
+
- render a story code, a "Preview" badge or a dashed frame inside the gate — it is the product's
|
|
899
|
+
public face in production;
|
|
900
|
+
- keep the hero's buttons beside a gate, or give the gate a second call to action.
|
|
796
901
|
`),
|
|
797
902
|
|
|
798
903
|
skill(ViableSkill.ResourceLayer, 'OwlMeans resources own the schema', `
|
package/src/skills/consts.ts
CHANGED
|
@@ -31,6 +31,11 @@ export enum ViableSkill {
|
|
|
31
31
|
FormFeedback = 'form-feedback',
|
|
32
32
|
StoreAccess = 'store-access',
|
|
33
33
|
ViewModelNaming = 'viewmodel-naming',
|
|
34
|
+
/**
|
|
35
|
+
* The landing gate: a guest starts the key end-user story on the landing page and continues on
|
|
36
|
+
* its full-scale screen after signing in, with the choices carried over.
|
|
37
|
+
*/
|
|
38
|
+
LandingGate = 'landing-gate',
|
|
34
39
|
|
|
35
40
|
ResourceLayer = 'resource-layer',
|
|
36
41
|
ResourceResults = 'resource-results',
|
|
@@ -99,6 +104,9 @@ export const SKILL_ORDER: Record<ViableSkill, number> = {
|
|
|
99
104
|
[ViableSkill.FormFeedback]: 51,
|
|
100
105
|
[ViableSkill.StoreAccess]: 52,
|
|
101
106
|
[ViableSkill.ViewModelNaming]: 55,
|
|
107
|
+
// After the frontend rules it builds on (entrypoints, nav, shadcn, form feedback, the store), and
|
|
108
|
+
// before the backend block — it is only ever carried by analysis and frontend personas.
|
|
109
|
+
[ViableSkill.LandingGate]: 56,
|
|
102
110
|
[ViableSkill.ResourceLayer]: 60,
|
|
103
111
|
[ViableSkill.ResourceResults]: 62,
|
|
104
112
|
[ViableSkill.ResourceMigrations]: 64,
|