@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.
Files changed (111) hide show
  1. package/README.md +3 -3
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/agent-presentation/SKILL.md +1 -1
  4. package/agent-meta/skills/viable-common/SKILL.md +111 -8
  5. package/build/blueprint/case.d.ts.map +1 -1
  6. package/build/blueprint/case.js +58 -16
  7. package/build/blueprint/case.js.map +1 -1
  8. package/build/blueprint/resolve.d.ts.map +1 -1
  9. package/build/blueprint/resolve.js +3 -0
  10. package/build/blueprint/resolve.js.map +1 -1
  11. package/build/blueprint/types.d.ts +44 -2
  12. package/build/blueprint/types.d.ts.map +1 -1
  13. package/build/blueprint/types.js +35 -1
  14. package/build/blueprint/types.js.map +1 -1
  15. package/build/branding.d.ts +5 -1
  16. package/build/branding.d.ts.map +1 -1
  17. package/build/branding.js +6 -0
  18. package/build/branding.js.map +1 -1
  19. package/build/connect/consts.d.ts +21 -0
  20. package/build/connect/consts.d.ts.map +1 -1
  21. package/build/connect/consts.js +21 -0
  22. package/build/connect/consts.js.map +1 -1
  23. package/build/connect/entrypoints.d.ts +15 -1
  24. package/build/connect/entrypoints.d.ts.map +1 -1
  25. package/build/connect/entrypoints.js +16 -1
  26. package/build/connect/entrypoints.js.map +1 -1
  27. package/build/connect/references.d.ts +14 -1
  28. package/build/connect/references.d.ts.map +1 -1
  29. package/build/connect/references.js +4 -0
  30. package/build/connect/references.js.map +1 -1
  31. package/build/connect/schemas.d.ts +12 -1
  32. package/build/connect/schemas.d.ts.map +1 -1
  33. package/build/connect/schemas.js +27 -1
  34. package/build/connect/schemas.js.map +1 -1
  35. package/build/connect/types.d.ts +27 -0
  36. package/build/connect/types.d.ts.map +1 -1
  37. package/build/consts.d.ts +1 -1
  38. package/build/consts.d.ts.map +1 -1
  39. package/build/consts.js +3 -0
  40. package/build/consts.js.map +1 -1
  41. package/build/metadata/types.d.ts +8 -0
  42. package/build/metadata/types.d.ts.map +1 -1
  43. package/build/metadata/types.js.map +1 -1
  44. package/build/planning/helpers.d.ts +2 -1
  45. package/build/planning/helpers.d.ts.map +1 -1
  46. package/build/planning/helpers.js +5 -2
  47. package/build/planning/helpers.js.map +1 -1
  48. package/build/planning/index.d.ts +1 -0
  49. package/build/planning/index.d.ts.map +1 -1
  50. package/build/planning/index.js +1 -0
  51. package/build/planning/index.js.map +1 -1
  52. package/build/planning/landing.d.ts +25 -0
  53. package/build/planning/landing.d.ts.map +1 -0
  54. package/build/planning/landing.js +38 -0
  55. package/build/planning/landing.js.map +1 -0
  56. package/build/planning/schemas.d.ts.map +1 -1
  57. package/build/planning/schemas.js +13 -0
  58. package/build/planning/schemas.js.map +1 -1
  59. package/build/planning/types.d.ts +28 -3
  60. package/build/planning/types.d.ts.map +1 -1
  61. package/build/scaffold/consts.d.ts +8 -0
  62. package/build/scaffold/consts.d.ts.map +1 -1
  63. package/build/scaffold/consts.js +7 -0
  64. package/build/scaffold/consts.js.map +1 -1
  65. package/build/scaffold/schemas.d.ts.map +1 -1
  66. package/build/scaffold/schemas.js +257 -12
  67. package/build/scaffold/schemas.js.map +1 -1
  68. package/build/scaffold/types.d.ts +119 -4
  69. package/build/scaffold/types.d.ts.map +1 -1
  70. package/build/skills/catalogue.d.ts.map +1 -1
  71. package/build/skills/catalogue.js +128 -24
  72. package/build/skills/catalogue.js.map +1 -1
  73. package/build/skills/consts.d.ts +5 -0
  74. package/build/skills/consts.d.ts.map +1 -1
  75. package/build/skills/consts.js +8 -0
  76. package/build/skills/consts.js.map +1 -1
  77. package/build/skills/roles.d.ts.map +1 -1
  78. package/build/skills/roles.js +11 -5
  79. package/build/skills/roles.js.map +1 -1
  80. package/build/types.d.ts +30 -0
  81. package/build/types.d.ts.map +1 -1
  82. package/package.json +8 -8
  83. package/src/blueprint/case.ts +64 -16
  84. package/src/blueprint/resolve.ts +3 -0
  85. package/src/blueprint/types.ts +57 -1
  86. package/src/branding.ts +6 -0
  87. package/src/connect/consts.ts +22 -0
  88. package/src/connect/entrypoints.ts +26 -4
  89. package/src/connect/references.ts +16 -3
  90. package/src/connect/schemas.ts +33 -4
  91. package/src/connect/types.ts +28 -0
  92. package/src/consts.ts +3 -0
  93. package/src/metadata/types.ts +8 -0
  94. package/src/planning/helpers.ts +5 -2
  95. package/src/planning/index.ts +1 -0
  96. package/src/planning/landing.ts +43 -0
  97. package/src/planning/schemas.ts +13 -0
  98. package/src/planning/types.ts +29 -3
  99. package/src/scaffold/consts.ts +10 -0
  100. package/src/scaffold/schemas.ts +262 -13
  101. package/src/scaffold/types.ts +105 -5
  102. package/src/skills/catalogue.ts +129 -24
  103. package/src/skills/consts.ts +8 -0
  104. package/src/skills/roles.ts +11 -5
  105. package/src/types.ts +30 -0
  106. package/tests/blueprint.spec.ts +98 -0
  107. package/tests/branding.spec.ts +21 -1
  108. package/tests/connect-entrypoints.spec.ts +44 -5
  109. package/tests/planning.spec.ts +56 -1
  110. package/tests/scaffold.spec.ts +131 -0
  111. package/tests/skills.spec.ts +65 -2
@@ -16,6 +16,12 @@ import type { TopologyDescriptor } from '../topology/types.js'
16
16
  * Deliberately NOT called a preset: `presets/` in the agent library already means the LLM
17
17
  * model/provider configurations, and the two are asked at different moments about different
18
18
  * things.
19
+ *
20
+ * Five layers are REQUIRED and say what the project is built WITH; the sixth, `experience`, is
21
+ * OPTIONAL and says what its product should do for the people using it. Optional because a
22
+ * blueprint registered before the layer existed — or a patch serialized into a run row — must keep
23
+ * resolving: every reader asks through a helper that answers the default for an absent layer
24
+ * ({@link landingGatePreferenceOf}).
19
25
  */
20
26
  export interface Blueprint {
21
27
  id: string
@@ -25,6 +31,7 @@ export interface Blueprint {
25
31
  template: TemplateLayer
26
32
  createApp: CreateAppLayer
27
33
  packages: PackagesLayer
34
+ experience?: ExperienceLayer
28
35
  }
29
36
 
30
37
  /**
@@ -32,7 +39,8 @@ export interface Blueprint {
32
39
  *
33
40
  * The ordering is the point: `technology` overrides everything, because a change of language
34
41
  * invalidates every template, prompt and package below it. Nothing today has more than one
35
- * technology — the ladder exists so that adding one is a data change.
42
+ * technology — the ladder exists so that adding one is a data change. `experience` is last and
43
+ * optional: it shares no key with the five build layers, so its place decides nothing today.
36
44
  */
37
45
  export enum BlueprintLayer {
38
46
  Technology = 'technology',
@@ -40,6 +48,7 @@ export enum BlueprintLayer {
40
48
  Template = 'template',
41
49
  CreateApp = 'create-app',
42
50
  Packages = 'packages',
51
+ Experience = 'experience',
43
52
  }
44
53
 
45
54
  export const BLUEPRINT_LAYER_ORDER: BlueprintLayer[] = [
@@ -48,8 +57,55 @@ export const BLUEPRINT_LAYER_ORDER: BlueprintLayer[] = [
48
57
  BlueprintLayer.Template,
49
58
  BlueprintLayer.CreateApp,
50
59
  BlueprintLayer.Packages,
60
+ BlueprintLayer.Experience,
51
61
  ]
52
62
 
63
+ /**
64
+ * How strongly a product of this kind wants a LANDING GATE — the working entry into the key
65
+ * end-user workflow drawn on the guest home, where a guest starts before signing in.
66
+ *
67
+ * A PRIOR handed to the model that decides, never the decision itself: `Encourage` still lets it
68
+ * answer "no gate" for a product with no end-user step a guest could begin, and `Discourage` still
69
+ * lets it choose one when the specification plainly describes such a step.
70
+ */
71
+ export enum LandingGatePreference {
72
+ /** Most products of this kind have a first-value step a guest can begin — look for it. */
73
+ Encourage = 'encourage',
74
+ /** Neutral: decide from the specification alone. */
75
+ Allow = 'allow',
76
+ /** Products of this kind rarely have one — choose a gate only when the specification asks. */
77
+ Discourage = 'discourage',
78
+ }
79
+
80
+ /**
81
+ * What the product should do for the people using it, independent of the stack it is built on.
82
+ *
83
+ * A layer of its own rather than a field of `packages`, because it changes no dependency, no
84
+ * capability and no prompt a coder reads — it is asked by the analysis stage about the PRODUCT.
85
+ */
86
+ export interface ExperienceLayer {
87
+ landingGate: LandingGatePreference
88
+ }
89
+
90
+ const LANDING_GATE_PREFERENCES = new Set<string>(Object.values(LandingGatePreference))
91
+
92
+ /**
93
+ * The landing-gate preference of a resolved blueprint.
94
+ *
95
+ * Total: an absent layer, an absent key and a value this deploy does not know (a patch serialized
96
+ * by a newer one) all answer `Allow` — the neutral prior, which leaves the decision to the
97
+ * specification rather than tilting it either way on a value nobody set.
98
+ */
99
+ export const landingGatePreferenceOf = (
100
+ blueprint?: Pick<Blueprint, 'experience'> | null
101
+ ): LandingGatePreference => {
102
+ const value = blueprint?.experience?.landingGate
103
+
104
+ return value != null && LANDING_GATE_PREFERENCES.has(value)
105
+ ? value
106
+ : LandingGatePreference.Allow
107
+ }
108
+
53
109
  /** Language and toolchain. Overrides everything below it. */
54
110
  export interface TechnologyLayer {
55
111
  id: string
package/src/branding.ts CHANGED
@@ -14,6 +14,7 @@ export const BRANDING_ENV_KEYS = [
14
14
  'BRANDING_PRIVACY_URL',
15
15
  'BRANDING_CREDIT',
16
16
  'BRANDING_PRODUCT',
17
+ 'BRANDING_GOOGLE_TAG',
17
18
  ] as const
18
19
 
19
20
  /**
@@ -23,6 +24,10 @@ export const BRANDING_ENV_KEYS = [
23
24
  * hides the platform credit, so a metadata set that never carried the key keeps it. That is the
24
25
  * safe direction — the opposite failure hands out a paid feature and nobody reports getting more
25
26
  * than they paid for.
27
+ *
28
+ * `BRANDING_GOOGLE_TAG` is always EMITTED, empty when no tag is set, although its metadata key is
29
+ * omitted from a push while unset: the environment is read by the application's own build, which
30
+ * treats `''` as "load no tag", while the metadata key is read by a publisher that may predate it.
26
31
  */
27
32
  export const brandingEnv = (meta: Partial<SlotMetadata>): Record<string, string> => ({
28
33
  BRANDING_COPYRIGHT: meta.brandingCopyright ?? '',
@@ -31,4 +36,5 @@ export const brandingEnv = (meta: Partial<SlotMetadata>): Record<string, string>
31
36
  BRANDING_PRIVACY_URL: meta.brandingPrivacyUrl ?? '',
32
37
  BRANDING_CREDIT: meta.brandingCredit === '' ? '' : '1',
33
38
  BRANDING_PRODUCT: meta.projectName ?? '',
39
+ BRANDING_GOOGLE_TAG: meta.brandingGoogleTag ?? '',
34
40
  })
@@ -301,6 +301,19 @@ export const CONNECT_CONFIGURE_TIMEOUT_MS = 60_000
301
301
  */
302
302
  export const CONNECT_CAP_LOCAL_LLM_KEY = 'connect--local-llm'
303
303
 
304
+ /**
305
+ * The ceilings of a project's branding strings on the connector wire.
306
+ *
307
+ * Each equals its twin in the platform's own branding contract (`BRANDING_COPYRIGHT_MAX`,
308
+ * `BRANDING_ORGANIZATION_MAX`, `BRANDING_URL_MAX`) and must stay equal: a connector refused below the
309
+ * web form's limit, or accepted above it and refused further in, is one value with two answers.
310
+ * The Google tag's is its own — the longest id any Google product issues is well under it.
311
+ */
312
+ export const CONNECT_BRANDING_COPYRIGHT_MAX = 200
313
+ export const CONNECT_BRANDING_ORGANIZATION_MAX = 120
314
+ export const CONNECT_BRANDING_URL_MAX = 2048
315
+ export const CONNECT_BRANDING_GOOGLE_TAG_MAX = 32
316
+
304
317
  /** The access-token prefix the platform issues. Every connector token starts with it. */
305
318
  export const CONNECT_TOKEN_PREFIX = 'vib_'
306
319
 
@@ -393,6 +406,15 @@ export const connect = Object.freeze({
393
406
  llm: 'viable:manager-api:connect:project:llm',
394
407
  /** The per-project converter inference mode. Separate from `llm`: it is not a paid capability. */
395
408
  converterLlm: 'viable:manager-api:connect:project:converter-llm',
409
+ /**
410
+ * The project's own branding — copyright, organization, the two legal links, the Google tag.
411
+ * The platform credit is deliberately NOT here: hiding it is a paid capability with its own
412
+ * gated route, and a connector setting the rest must never be able to touch it.
413
+ */
414
+ branding: Object.freeze({
415
+ get: 'viable:manager-api:connect:project:branding:get',
416
+ save: 'viable:manager-api:connect:project:branding:save',
417
+ }),
396
418
  }),
397
419
  story: Object.freeze({
398
420
  status: 'viable:manager-api:connect:story:status',
@@ -7,14 +7,15 @@ import {
7
7
  ConnectAttachBodySchema, ConnectConfirmBodySchema, ConnectConvertCreateBodySchema,
8
8
  ConnectConvertProceedBodySchema, ConnectCreateBodySchema, ConnectInquiryParamsSchema,
9
9
  ConnectModifyBodySchema, ConnectOpParamsSchema, ConnectOpResultSchema,
10
- ConnectPipelineParamsSchema, ConnectPipelineResumeBodySchema, ConnectProjectIdSchema,
11
- ConnectProjectLlmBodySchema, ConnectSessionOpenSchema, ConnectSessionParamsSchema,
12
- ConnectPullQuerySchema, ConnectStoryParamsSchema, InquiryAnswerSchema,
10
+ ConnectPipelineParamsSchema, ConnectPipelineResumeBodySchema, ConnectProjectBrandingSaveSchema,
11
+ ConnectProjectIdSchema, ConnectProjectLlmBodySchema, ConnectSessionOpenSchema,
12
+ ConnectSessionParamsSchema, ConnectPullQuerySchema, ConnectStoryParamsSchema, InquiryAnswerSchema,
13
13
  } from './schemas.js'
14
14
  import type {
15
15
  ConnectAttachBody, ConnectConfirmBody, ConnectConvertCreateBody, ConnectConvertProceedBody,
16
16
  ConnectCreateBody, ConnectInquiryAnswerBody, ConnectModifyBody,
17
- ConnectPipelineParams, ConnectPipelineResumeBody, ConnectProjectLlmBody, ConnectSessionOpen,
17
+ ConnectPipelineParams, ConnectPipelineResumeBody, ConnectProjectBranding,
18
+ ConnectProjectBrandingSave, ConnectProjectLlmBody, ConnectSessionOpen,
18
19
  ConnectPipelineState, ConnectProjectStatus, ConnectPullQuery, ConnectSessionParams,
19
20
  ConnectStoryStatus, ConversionStatusView, ConvertCheck,
20
21
  } from './types.js'
@@ -188,6 +189,27 @@ export const connectProtocols = (opts: ConnectEntrypointOptions) => {
188
189
  body: typed<ConverterProjectLlmBody>(ConverterProjectLlmBodySchema),
189
190
  }, typed()),
190
191
  ),
192
+ // Under `base` like every sibling — the guard and the ownership gate — and never under the
193
+ // paid gate: saving branding is free, and the paid credit switch is not reachable from here.
194
+ branding: {
195
+ get: protocol(
196
+ route(connect.project.branding.get, '/project/:id/branding', {
197
+ parent: base, method: RouteMethod.GET,
198
+ }),
199
+ contract.request({
200
+ params: typed<{ id: string }>(ConnectProjectIdSchema),
201
+ }, typed<ConnectProjectBranding>()),
202
+ ),
203
+ save: protocol(
204
+ route(connect.project.branding.save, '/project/:id/branding', {
205
+ parent: base, method: RouteMethod.POST,
206
+ }),
207
+ contract.request({
208
+ params: typed<{ id: string }>(ConnectProjectIdSchema),
209
+ body: typed<ConnectProjectBrandingSave>(ConnectProjectBrandingSaveSchema),
210
+ }, typed<ConnectProjectBranding>()),
211
+ ),
212
+ },
191
213
  },
192
214
 
193
215
  story: {
@@ -8,9 +8,10 @@ import type {
8
8
  ConnectAttachBody, ConnectCapabilitiesView, ConnectConfirmBody, ConnectCreateBody,
9
9
  ConnectConvertCreateBody, ConnectConvertProceedBody, ConnectInquiryAnswerBody,
10
10
  ConnectModifyBody, ConnectOpSubmission, ConnectPipelineParams, ConnectPipelineResumeBody,
11
- ConnectPipelineState, ConnectProjectLlmBody, ConnectProjectSettings, ConnectProjectStatus,
12
- ConnectProjectSummary, ConnectPullQuery, ConnectSessionOpen, ConnectSessionParams,
13
- ConnectSessionView, ConnectStoryStatus, ConversionStatusView, ConvertCheck,
11
+ ConnectPipelineState, ConnectProjectBranding, ConnectProjectBrandingSave, ConnectProjectLlmBody,
12
+ ConnectProjectSettings, ConnectProjectStatus, ConnectProjectSummary, ConnectPullQuery,
13
+ ConnectSessionOpen, ConnectSessionParams, ConnectSessionView, ConnectStoryStatus,
14
+ ConversionStatusView, ConvertCheck,
14
15
  } from './types.js'
15
16
 
16
17
  type ConnectReference<Request extends RequestShape, Response> =
@@ -44,6 +45,12 @@ export interface ConnectReferences {
44
45
  converterLlm: ConnectReference<{
45
46
  params: { id: string }, body: ConverterProjectLlmBody
46
47
  }, ConnectProjectSettings>
48
+ branding: {
49
+ get: ConnectReference<{ params: { id: string } }, ConnectProjectBranding>
50
+ save: ConnectReference<{
51
+ params: { id: string }, body: ConnectProjectBrandingSave
52
+ }, ConnectProjectBranding>
53
+ }
47
54
  }
48
55
  story: {
49
56
  status: ConnectReference<{ params: { id: string, storyId: string } }, ConnectStoryStatus>
@@ -110,6 +117,12 @@ export const connectRef: ConnectReferences = {
110
117
  converterLlm: entrypointRef<{
111
118
  params: { id: string }, body: ConverterProjectLlmBody
112
119
  }, ConnectProjectSettings>(connect.project.converterLlm),
120
+ branding: {
121
+ get: entrypointRef<{ params: { id: string } }, ConnectProjectBranding>(connect.project.branding.get),
122
+ save: entrypointRef<{
123
+ params: { id: string }, body: ConnectProjectBrandingSave
124
+ }, ConnectProjectBranding>(connect.project.branding.save),
125
+ },
113
126
  },
114
127
  story: {
115
128
  status: entrypointRef<{
@@ -1,15 +1,17 @@
1
1
  import type { JSONSchemaType } from 'ajv'
2
2
  import { ConversionDecision, OriginKind } from '../convert/consts.js'
3
3
  import {
4
- CONNECT_INQUIRY_MAX_TEXT, ConnectHarness, ConnectLlm, ConnectOpErrorKind, ConnectSessionStatus,
5
- ConnectTarget, ConnectTransport, ModelTaskResultKind, ModelTier
4
+ CONNECT_BRANDING_COPYRIGHT_MAX, CONNECT_BRANDING_GOOGLE_TAG_MAX,
5
+ CONNECT_BRANDING_ORGANIZATION_MAX, CONNECT_BRANDING_URL_MAX, CONNECT_INQUIRY_MAX_TEXT,
6
+ ConnectHarness, ConnectLlm, ConnectOpErrorKind, ConnectSessionStatus, ConnectTarget,
7
+ ConnectTransport, ModelTaskResultKind, ModelTier
6
8
  } from './consts.js'
7
9
  import type { ConnectOpResult, InquiryAnswerPayload } from './ops.js'
8
10
  import type {
9
11
  ConnectAttachBody, ConnectConfirmBody, ConnectConvertCreateBody, ConnectConvertProceedBody,
10
12
  ConnectCreateBody, ConnectLlmBody, ConnectModifyBody, ConnectPipelineParams,
11
- ConnectPipelineResumeBody, ConnectProjectLlmBody, ConnectSession, ConnectSessionOpen,
12
- ConnectPullQuery, ConnectSessionParams
13
+ ConnectPipelineResumeBody, ConnectProjectBrandingSave, ConnectProjectLlmBody, ConnectSession,
14
+ ConnectSessionOpen, ConnectPullQuery, ConnectSessionParams
13
15
  } from './types.js'
14
16
 
15
17
  /**
@@ -313,6 +315,33 @@ export const ConnectProjectLlmBodySchema = {
313
315
  additionalProperties: false,
314
316
  } as unknown as JSONSchemaType<ConnectProjectLlmBody>
315
317
 
318
+ /**
319
+ * The branding patch a connector saves.
320
+ *
321
+ * STRUCTURAL only — lengths and types. What makes a value acceptable (a non-empty copyright and
322
+ * organization, a legal link that is `https://` or a same-origin path, a real Google tag id) is the
323
+ * platform's rule, applied by the handler to the MERGED record with the same checks the web form
324
+ * uses; a pattern repeated here would be a second copy of those rules free to drift from the first.
325
+ * No `pattern`, and no `enum` on the tag: a connector and the platform deploy apart, and a tag kind
326
+ * Google adds later must be refused by the rule that knows about it, not by a stale wire schema.
327
+ */
328
+ export const ConnectProjectBrandingSaveSchema = {
329
+ type: 'object',
330
+ properties: {
331
+ copyright: {
332
+ type: 'string', minLength: 1, maxLength: CONNECT_BRANDING_COPYRIGHT_MAX, nullable: true,
333
+ },
334
+ organizationName: {
335
+ type: 'string', minLength: 1, maxLength: CONNECT_BRANDING_ORGANIZATION_MAX, nullable: true,
336
+ },
337
+ termsUrl: { type: 'string', maxLength: CONNECT_BRANDING_URL_MAX, nullable: true },
338
+ privacyUrl: { type: 'string', maxLength: CONNECT_BRANDING_URL_MAX, nullable: true },
339
+ googleTag: { type: 'string', maxLength: CONNECT_BRANDING_GOOGLE_TAG_MAX, nullable: true },
340
+ },
341
+ required: [],
342
+ additionalProperties: false,
343
+ } as unknown as JSONSchemaType<ConnectProjectBrandingSave>
344
+
316
345
  export const ModelTaskResultSchema = {
317
346
  type: 'object',
318
347
  properties: {
@@ -223,6 +223,34 @@ export interface ConnectProjectLlmBody {
223
223
  llmMode: ConnectLlm | null
224
224
  }
225
225
 
226
+ /**
227
+ * A project's branding as a connector reads it — what the generated application says about who
228
+ * made it, and the Google tag it loads.
229
+ *
230
+ * Every field is a string, `''` when unset, in the platform's own branding vocabulary. The
231
+ * platform credit is absent on purpose: hiding it is a paid capability behind its own gated route,
232
+ * and neither reading nor saving this record may reach it.
233
+ */
234
+ export interface ConnectProjectBranding {
235
+ /** The copyright line in the application's footer. Never empty once saved. */
236
+ copyright: string
237
+ /** The organization's display name. Never empty once saved. */
238
+ organizationName: string
239
+ /** `''`, an `https://` URL, or a same-origin path such as `/terms` (the generated page). */
240
+ termsUrl: string
241
+ /** `''`, an `https://` URL, or a same-origin path such as `/privacy` (the generated page). */
242
+ privacyUrl: string
243
+ /** `''`, or a Google tag id: `GTM-…`, `G-…`, `GT-…`, `AW-…`, `DC-…`. */
244
+ googleTag: string
245
+ }
246
+
247
+ /**
248
+ * A PATCH of a project's branding: every field optional, and an absent field keeps its current
249
+ * value. The platform merges it over what is stored, validates the result as a whole with the
250
+ * rules the web form uses, and answers the merged record.
251
+ */
252
+ export interface ConnectProjectBrandingSave extends Partial<ConnectProjectBranding> {}
253
+
226
254
  /** What the platform tells a connector about itself. */
227
255
  export interface ConnectCapabilitiesView {
228
256
  /** Tier → the platform role names that will be asked for at that tier. */
package/src/consts.ts CHANGED
@@ -17,6 +17,9 @@ export const metadataConfigs = [
17
17
  'brandingPrivacyUrl',
18
18
  'brandingCredit',
19
19
  'brandingHideCreditIntent',
20
+ // Optional in `SlotConstMetadata`: omitted from a push while unset (an older publisher refuses
21
+ // an undeclared key), so an empty stored row must never be delivered as `''`.
22
+ 'brandingGoogleTag',
20
23
  ] satisfies (keyof NonSecretScalarSlotMetadata)[]
21
24
 
22
25
  export const metadataLists = [
@@ -116,6 +116,12 @@ export interface StoryMeta extends Record<string, unknown> {
116
116
  * that moves section when it is implemented leaves its own dashboard behind.
117
117
  */
118
118
  section?: string
119
+ /**
120
+ * This is the landing gate's story — a guest starts it on the landing page and continues here
121
+ * after signing in. Copied from the card's `fields.landing`; written only when `true`, so every
122
+ * other story's file keeps the shape it always had.
123
+ */
124
+ landing?: boolean
119
125
  entity: string
120
126
  entities: string[]
121
127
  screens: StoryScreenRef[]
@@ -153,6 +159,8 @@ export interface StoryWriteInput {
153
159
  narrative: string
154
160
  status: string
155
161
  primary: boolean
162
+ /** The card's landing flag — see {@link StoryMeta.landing}. Absent reads as `false`. */
163
+ landing?: boolean
156
164
  /** Carried through a develop run so the rewrite does not drop the scaffold's assignment. */
157
165
  section?: string
158
166
  /** Likewise the reserved screen name. */
@@ -115,19 +115,22 @@ export const projectBriefOf = (project: Workcard, specs: readonly Specification[
115
115
  * What the metadata store writes to `docs/stories/<code>.md`, from the card and a run's content.
116
116
  *
117
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.
118
+ * story card without one is refused rather than filed under its id. The landing flag is carried
119
+ * only when it is set, so every other story's frontmatter keeps the shape it always had.
119
120
  */
120
121
  export const storyWriteInputOf = (card: Workcard, content: StoryWriteContent): StoryWriteInput => {
121
122
  if (card.code == null || card.code === '') {
122
123
  throw new ProjectStoryMissconfigured('no-code')
123
124
  }
124
125
  const { status, ...rest } = content
126
+ const fields = storyFieldsOf(card)
125
127
 
126
128
  return {
127
129
  ...rest,
128
130
  code: card.code,
129
131
  narrative: card.title,
130
132
  status: status ?? card.status,
131
- primary: storyFieldsOf(card).primary,
133
+ primary: fields.primary,
134
+ ...(fields.landing === true ? { landing: true } : {}),
132
135
  }
133
136
  }
@@ -3,4 +3,5 @@ export * from './errors.js'
3
3
  export * from './schemas.js'
4
4
  export * from './flows.js'
5
5
  export * from './helpers.js'
6
+ export * from './landing.js'
6
7
  export type * from './types.js'
@@ -0,0 +1,43 @@
1
+ import { TITLE_MAX } from '@owlmeans/planning'
2
+
3
+ /**
4
+ * The sentence a landing story's narrative carries — what a person reading the board sees.
5
+ *
6
+ * The AUTHORITY is the card's `fields.landing` flag, never this text: the design and development
7
+ * stages key on the flag, and a person may reword the narrative at any time. The sentence exists so
8
+ * the board says in plain words why this story is different, and so the analyst and the designer,
9
+ * who read the narrative, write a screen that continues what the guest started.
10
+ *
11
+ * A constant, byte for byte, because {@link hasLandingSentence} is how a resumed decision step
12
+ * knows it already appended it.
13
+ */
14
+ export const LANDING_STORY_SENTENCE = 'A visitor can start this on the landing page without an account;'
15
+ + ' after signing in they continue here with their choices carried over.'
16
+
17
+ const normalized = (text: string): string => text.replace(/\s+/g, ' ')
18
+
19
+ /** Whether a narrative already carries the landing sentence, whatever whitespace surrounds it. */
20
+ export const hasLandingSentence = (title: string): boolean =>
21
+ normalized(title).includes(LANDING_STORY_SENTENCE)
22
+
23
+ /**
24
+ * The narrative with the landing sentence appended once.
25
+ *
26
+ * Idempotent — a narrative that already carries the sentence comes back unchanged, so a decision
27
+ * step that is resumed after it wrote the card never appends a second copy. It never produces a
28
+ * title longer than a card accepts (`TITLE_MAX`): a narrative with no room left is returned as it
29
+ * is rather than cut, because a person's words are worth more than the note, and the flag on the
30
+ * card still carries the fact.
31
+ */
32
+ export const withLandingSentence = (title: string): string => {
33
+ if (hasLandingSentence(title)) {
34
+ return title
35
+ }
36
+ const base = title.trimEnd()
37
+ if (base === '') {
38
+ return LANDING_STORY_SENTENCE
39
+ }
40
+ const joined = `${base} ${LANDING_STORY_SENTENCE}`
41
+
42
+ return joined.length > TITLE_MAX ? title : joined
43
+ }
@@ -28,6 +28,18 @@ export const ViableProjectFieldsSchema = {
28
28
  origin: { ...ProjectOriginSchema, nullable: true },
29
29
  connectLlmMode: { type: 'string', enum: [...Object.values(ConnectLlm), null], nullable: true },
30
30
  converterLlmMode: { type: 'string', enum: [...Object.values(ConnectLlm), null], nullable: true },
31
+ // `story: null` is a DECIDED "no gate" and must stay spellable; an absent `landing` is "never
32
+ // decided". `at` carries no `format`: the planning registry compiles without ajv-formats.
33
+ landing: {
34
+ type: 'object',
35
+ nullable: true,
36
+ properties: {
37
+ story: { type: 'string', minLength: 1, maxLength: CODE_MAX, nullable: true },
38
+ at: { type: 'string', minLength: 1, maxLength: 32 },
39
+ },
40
+ required: ['at'],
41
+ additionalProperties: false,
42
+ },
31
43
  },
32
44
  required: [],
33
45
  additionalProperties: false,
@@ -48,6 +60,7 @@ export const ViableStoryFieldsSchema = {
48
60
  actor: { type: 'string', enum: [...Object.values(StoryActor), null], nullable: true },
49
61
  warning: { type: 'string', maxLength: 1024, nullable: true },
50
62
  kind: { type: 'string', enum: [...Object.values(StoryKind), null], nullable: true },
63
+ landing: { type: 'boolean', nullable: true },
51
64
  },
52
65
  required: ['area', 'primary'],
53
66
  additionalProperties: false,
@@ -32,6 +32,23 @@ export type ViableProjectFields = {
32
32
  connectLlmMode?: ConnectLlm
33
33
  /** Who performs a conversion's model calls on this project; absent inherits. */
34
34
  converterLlmMode?: ConnectLlm
35
+ /**
36
+ * The landing-gate decision: which story a guest can start on the landing page.
37
+ *
38
+ * Three states, and the third is why this is an object rather than a code: ABSENT means the
39
+ * decision was never made (an initialization that has not reached it, or a project older than
40
+ * the gate), `story: null` means it WAS made and the answer is "no gate", and a code names the
41
+ * story. A failed decision is never recorded — writing `null` for it would read as a decided
42
+ * "none" and no later run would ask again. `at` is when it was decided, an ISO timestamp.
43
+ */
44
+ landing?: ViableLandingDecision
45
+ }
46
+
47
+ /** The recorded landing-gate decision — see {@link ViableProjectFields.landing}. */
48
+ export interface ViableLandingDecision {
49
+ /** The story card's CODE, or `null` for a decided "no gate". */
50
+ story: string | null
51
+ at: string
35
52
  }
36
53
 
37
54
  /**
@@ -51,6 +68,15 @@ export type ViableStoryFields = {
51
68
  warning?: string
52
69
  /** Which half of the analysis the story came from. */
53
70
  kind?: StoryKind
71
+ /**
72
+ * The story a guest starts on the landing page — the landing gate's story. At most one per
73
+ * project, and the project card's `fields.landing.story` names the same code.
74
+ *
75
+ * Its development gains a step that replaces the gate's sketch on the guest home with the real
76
+ * component, and its full-scale screen reads the choices the guest carried over. Absent and
77
+ * `false` read the same.
78
+ */
79
+ landing?: boolean
54
80
  }
55
81
 
56
82
  export interface ViableProjectCard extends Project {
@@ -67,9 +93,9 @@ export type ViableSpecification = Specification
67
93
  /**
68
94
  * What a develop run hands the metadata store beside the card.
69
95
  *
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.
96
+ * The code, the narrative, the primary flag and the landing flag are the CARD's and are never
97
+ * passed separately — a second copy is a second answer. `status` defaults to the card's own.
72
98
  */
73
99
  export type StoryWriteContent =
74
- & Omit<StoryWriteInput, 'code' | 'narrative' | 'primary' | 'status'>
100
+ & Omit<StoryWriteInput, 'code' | 'narrative' | 'primary' | 'landing' | 'status'>
75
101
  & { status?: string }
@@ -1,3 +1,5 @@
1
+ import type { BentoFragmentKind } from './types.js'
2
+
1
3
  /**
2
4
  * The shapes a drawn widget can take.
3
5
  *
@@ -24,3 +26,11 @@ export const AREA_HOME_WIDGET_CAP = 3
24
26
 
25
27
  /** Sections per area beyond which a top menu stops being navigable. */
26
28
  export const AREA_SECTION_CAP = 6
29
+
30
+ /**
31
+ * The shapes a landing page's bento fragment can take.
32
+ *
33
+ * A closed set for the same reason as {@link WidgetKind}: each value is one stamped primitive, so a
34
+ * value nothing can draw is a tile with an empty bottom.
35
+ */
36
+ export const BENTO_FRAGMENT_KINDS: BentoFragmentKind[] = ['list', 'note', 'people', 'steps']