@owlmeans/viable-common 0.0.40 → 0.0.42

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 (67) 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 +323 -401
  5. package/build/blueprint/consts.d.ts +35 -1
  6. package/build/blueprint/consts.d.ts.map +1 -1
  7. package/build/blueprint/consts.js +35 -0
  8. package/build/blueprint/consts.js.map +1 -1
  9. package/build/blueprint/types.d.ts +7 -0
  10. package/build/blueprint/types.d.ts.map +1 -1
  11. package/build/blueprint/types.js.map +1 -1
  12. package/build/branding.d.ts +3 -0
  13. package/build/branding.d.ts.map +1 -1
  14. package/build/branding.js +11 -1
  15. package/build/branding.js.map +1 -1
  16. package/build/connect/consts.d.ts +8 -0
  17. package/build/connect/consts.d.ts.map +1 -1
  18. package/build/connect/consts.js +8 -0
  19. package/build/connect/consts.js.map +1 -1
  20. package/build/connect/entrypoints.d.ts +15 -1
  21. package/build/connect/entrypoints.d.ts.map +1 -1
  22. package/build/connect/entrypoints.js +13 -1
  23. package/build/connect/entrypoints.js.map +1 -1
  24. package/build/connect/references.d.ts +14 -1
  25. package/build/connect/references.d.ts.map +1 -1
  26. package/build/connect/references.js +4 -0
  27. package/build/connect/references.js.map +1 -1
  28. package/build/connect/schemas.d.ts +7 -1
  29. package/build/connect/schemas.d.ts.map +1 -1
  30. package/build/connect/schemas.js +80 -0
  31. package/build/connect/schemas.js.map +1 -1
  32. package/build/connect/types.d.ts +51 -0
  33. package/build/connect/types.d.ts.map +1 -1
  34. package/build/planning/schemas.d.ts.map +1 -1
  35. package/build/planning/schemas.js +3 -0
  36. package/build/planning/schemas.js.map +1 -1
  37. package/build/planning/types.d.ts +5 -0
  38. package/build/planning/types.d.ts.map +1 -1
  39. package/build/slot/consts.d.ts +15 -1
  40. package/build/slot/consts.d.ts.map +1 -1
  41. package/build/slot/consts.js +19 -0
  42. package/build/slot/consts.js.map +1 -1
  43. package/build/slot/schemas.d.ts.map +1 -1
  44. package/build/slot/schemas.js +2 -1
  45. package/build/slot/schemas.js.map +1 -1
  46. package/build/slot/types.d.ts +44 -2
  47. package/build/slot/types.d.ts.map +1 -1
  48. package/build/types.d.ts +5 -0
  49. package/build/types.d.ts.map +1 -1
  50. package/package.json +6 -6
  51. package/src/blueprint/consts.ts +36 -0
  52. package/src/blueprint/types.ts +7 -0
  53. package/src/branding.ts +11 -1
  54. package/src/connect/consts.ts +8 -0
  55. package/src/connect/entrypoints.ts +21 -2
  56. package/src/connect/references.ts +12 -1
  57. package/src/connect/schemas.ts +87 -1
  58. package/src/connect/types.ts +55 -0
  59. package/src/planning/schemas.ts +3 -0
  60. package/src/planning/types.ts +5 -0
  61. package/src/slot/consts.ts +20 -0
  62. package/src/slot/schemas.ts +2 -1
  63. package/src/slot/types.ts +31 -2
  64. package/src/types.ts +5 -0
  65. package/tests/branding.spec.ts +13 -2
  66. package/tests/connect-entrypoints.spec.ts +52 -5
  67. package/tests/planning.spec.ts +8 -0
@@ -3,7 +3,7 @@ import { route, RouteMethod } from '@owlmeans/route'
3
3
  import { connect } from './consts.js'
4
4
  import {
5
5
  ConnectAttachBodySchema, ConnectConfirmBodySchema, ConnectConvertCreateBodySchema,
6
- ConnectConvertProceedBodySchema, ConnectCreateBodySchema, ConnectInquiryParamsSchema,
6
+ ConnectConvertProceedBodySchema, ConnectCreateBodySchema, ConnectInquiryParamsSchema, ConnectKitApplyBodySchema,
7
7
  ConnectModifyBodySchema, ConnectOpParamsSchema, ConnectOpResultSchema,
8
8
  ConnectPipelineParamsSchema, ConnectPipelineResumeBodySchema, ConnectProjectBrandingSaveSchema,
9
9
  ConnectProjectIdSchema, ConnectSessionOpenSchema, ConnectSessionParamsSchema, ConnectPullQuerySchema,
@@ -11,7 +11,8 @@ import {
11
11
  } from './schemas.js'
12
12
  import type {
13
13
  ConnectAttachBody, ConnectConfirmBody, ConnectConvertCreateBody, ConnectConvertProceedBody,
14
- ConnectCreateBody, ConnectInquiryAnswerBody, ConnectModifyBody,
14
+ ConnectCreateBody, ConnectInquiryAnswerBody, ConnectKitApplyBody, ConnectKitApplyResult, ConnectKitDescribe,
15
+ ConnectModifyBody,
15
16
  ConnectPipelineParams, ConnectPipelineResumeBody, ConnectProjectBranding,
16
17
  ConnectProjectBrandingSave, ConnectSessionOpen,
17
18
  ConnectPipelineState, ConnectProjectStatus, ConnectPullQuery, ConnectSessionParams,
@@ -143,6 +144,24 @@ export const connectProtocols = (opts: ConnectEntrypointOptions) => {
143
144
  }),
144
145
  contract.request({ params: typed<{ id: string }>(ConnectProjectIdSchema), body: typed<ConnectModifyBody>(ConnectModifyBodySchema) }, typed<ConnectProjectStatus>())
145
146
  ),
147
+ // Planning kits: one path, read and applied under the owned base like every project route.
148
+ kit: {
149
+ describe: protocol(
150
+ route(connect.project.kit.describe, '/project/:id/kits', {
151
+ parent: base, method: RouteMethod.GET,
152
+ }),
153
+ contract.request({ params: typed<{ id: string }>(ConnectProjectIdSchema) }, typed<ConnectKitDescribe>()),
154
+ ),
155
+ apply: protocol(
156
+ route(connect.project.kit.apply, '/project/:id/kits', {
157
+ parent: base, method: RouteMethod.POST,
158
+ }),
159
+ contract.request({
160
+ params: typed<{ id: string }>(ConnectProjectIdSchema),
161
+ body: typed<ConnectKitApplyBody>(ConnectKitApplyBodySchema),
162
+ }, typed<ConnectKitApplyResult>()),
163
+ ),
164
+ },
146
165
  // Under `base` like every sibling — the guard and the ownership gate — and never under the
147
166
  // paid gate: saving branding is free, and the paid credit switch is not reachable from here.
148
167
  branding: {
@@ -5,7 +5,8 @@ import { connect } from './consts.js'
5
5
  import type { ConnectOp, ConnectOpResult } from './ops.js'
6
6
  import type {
7
7
  ConnectAttachBody, ConnectConfirmBody, ConnectCreateBody, ConnectConvertCreateBody,
8
- ConnectConvertProceedBody, ConnectInquiryAnswerBody, ConnectModifyBody, ConnectOpSubmission,
8
+ ConnectConvertProceedBody, ConnectInquiryAnswerBody, ConnectKitApplyBody, ConnectKitApplyResult,
9
+ ConnectKitDescribe, ConnectModifyBody, ConnectOpSubmission,
9
10
  ConnectPipelineParams, ConnectPipelineResumeBody, ConnectPipelineState, ConnectProjectBranding,
10
11
  ConnectProjectBrandingSave, ConnectProjectStatus, ConnectProjectSummary, ConnectPullQuery,
11
12
  ConnectSessionOpen, ConnectSessionParams, ConnectSessionView, ConnectStoryStatus,
@@ -35,6 +36,10 @@ export interface ConnectReferences {
35
36
  attach: ConnectReference<{ body: ConnectAttachBody }, ConnectProjectStatus>
36
37
  reinit: ConnectReference<{ params: { id: string } }, ConnectProjectStatus>
37
38
  modify: ConnectReference<{ params: { id: string }, body: ConnectModifyBody }, ConnectProjectStatus>
39
+ kit: {
40
+ describe: ConnectReference<{ params: { id: string } }, ConnectKitDescribe>
41
+ apply: ConnectReference<{ params: { id: string }, body: ConnectKitApplyBody }, ConnectKitApplyResult>
42
+ }
38
43
  branding: {
39
44
  get: ConnectReference<{ params: { id: string } }, ConnectProjectBranding>
40
45
  save: ConnectReference<{
@@ -96,6 +101,12 @@ export const connectRef: ConnectReferences = {
96
101
  attach: entrypointRef<{ body: ConnectAttachBody }, ConnectProjectStatus>(connect.project.attach),
97
102
  reinit: entrypointRef<{ params: { id: string } }, ConnectProjectStatus>(connect.project.reinit),
98
103
  modify: entrypointRef<{ params: { id: string }, body: ConnectModifyBody }, ConnectProjectStatus>(connect.project.modify),
104
+ kit: {
105
+ describe: entrypointRef<{ params: { id: string } }, ConnectKitDescribe>(connect.project.kit.describe),
106
+ apply: entrypointRef<{
107
+ params: { id: string }, body: ConnectKitApplyBody
108
+ }, ConnectKitApplyResult>(connect.project.kit.apply),
109
+ },
99
110
  branding: {
100
111
  get: entrypointRef<{ params: { id: string } }, ConnectProjectBranding>(connect.project.branding.get),
101
112
  save: entrypointRef<{
@@ -11,7 +11,8 @@ import type {
11
11
  ConnectAttachBody, ConnectConfirmBody, ConnectConvertCreateBody, ConnectConvertProceedBody,
12
12
  ConnectCreateBody, ConnectLlmBody, ConnectModifyBody, ConnectPipelineParams,
13
13
  ConnectPipelineResumeBody, ConnectProjectBrandingSave, ConnectProjectLlmBody, ConnectSession,
14
- ConnectSessionOpen, ConnectPullQuery, ConnectSessionParams
14
+ ConnectSessionOpen, ConnectPullQuery, ConnectSessionParams, ConnectKitApplyBody, ConnectKitApplyResult,
15
+ ConnectKitDescribe, PlanningKitView
15
16
  } from './types.js'
16
17
 
17
18
  /**
@@ -202,6 +203,91 @@ export const ConnectModifyBodySchema = {
202
203
  additionalProperties: false,
203
204
  } as JSONSchemaType<ConnectModifyBody>
204
205
 
206
+ const kitKey = { type: 'string', minLength: 1, maxLength: 128 } as const
207
+ const kitText = { type: 'string', maxLength: 4096 } as const
208
+ const kitStrings = { type: 'array', items: kitKey, maxItems: 256 } as const
209
+
210
+ /** A planning kit as `project.kit.describe` lists it. Closed, like every view this package pins. */
211
+ export const PlanningKitViewSchema = {
212
+ type: 'object',
213
+ properties: {
214
+ id: kitKey,
215
+ kind: kitKey,
216
+ title: kitText,
217
+ purpose: kitText,
218
+ container: {
219
+ type: 'object',
220
+ properties: { key: kitKey, label: kitText },
221
+ required: ['key', 'label'],
222
+ additionalProperties: false,
223
+ },
224
+ types: {
225
+ type: 'array',
226
+ maxItems: 256,
227
+ items: {
228
+ type: 'object',
229
+ properties: { key: kitKey, label: kitText, flow: kitKey },
230
+ required: ['key', 'label', 'flow'],
231
+ additionalProperties: false,
232
+ },
233
+ },
234
+ flows: {
235
+ type: 'array',
236
+ maxItems: 256,
237
+ items: {
238
+ type: 'object',
239
+ properties: {
240
+ key: kitKey,
241
+ label: kitText,
242
+ statuses: {
243
+ type: 'array',
244
+ maxItems: 256,
245
+ items: {
246
+ type: 'object',
247
+ properties: { key: kitKey, label: kitText, intrinsic: kitKey },
248
+ required: ['key', 'label', 'intrinsic'],
249
+ additionalProperties: false,
250
+ },
251
+ },
252
+ },
253
+ required: ['key', 'label', 'statuses'],
254
+ additionalProperties: false,
255
+ },
256
+ },
257
+ },
258
+ required: ['id', 'kind', 'title', 'purpose', 'container', 'types', 'flows'],
259
+ additionalProperties: false,
260
+ } as unknown as JSONSchemaType<PlanningKitView>
261
+
262
+ export const ConnectKitDescribeSchema = {
263
+ type: 'object',
264
+ properties: { kits: { type: 'array', items: PlanningKitViewSchema, maxItems: 64 } },
265
+ required: ['kits'],
266
+ additionalProperties: false,
267
+ } as unknown as JSONSchemaType<ConnectKitDescribe>
268
+
269
+ /** `types` keeps only these card-type keys of the kit; omitted (or `null`) keeps every type. */
270
+ export const ConnectKitApplyBodySchema = {
271
+ type: 'object',
272
+ properties: {
273
+ kit: kitKey,
274
+ types: { ...kitStrings, nullable: true },
275
+ },
276
+ required: ['kit'],
277
+ additionalProperties: false,
278
+ } as unknown as JSONSchemaType<ConnectKitApplyBody>
279
+
280
+ export const ConnectKitApplyResultSchema = {
281
+ type: 'object',
282
+ properties: {
283
+ applied: kitStrings,
284
+ skipped: kitStrings,
285
+ warnings: { type: 'array', items: kitText, maxItems: 256 },
286
+ },
287
+ required: ['applied', 'skipped', 'warnings'],
288
+ additionalProperties: false,
289
+ } as unknown as JSONSchemaType<ConnectKitApplyResult>
290
+
205
291
  export const ConnectAttachBodySchema = {
206
292
  type: 'object',
207
293
  properties: {
@@ -409,3 +409,58 @@ export interface ConnectPipelineState {
409
409
  heartbeatAt: string
410
410
  updatedAt: string
411
411
  }
412
+
413
+ /** One status of a planning kit's flow. `intrinsic` is the planning intrinsic it maps to. */
414
+ export interface PlanningKitStatusView {
415
+ key: string
416
+ label: string
417
+ intrinsic: string
418
+ }
419
+
420
+ /** One status flow a planning kit declares. */
421
+ export interface PlanningKitFlowView {
422
+ key: string
423
+ label: string
424
+ statuses: PlanningKitStatusView[]
425
+ }
426
+
427
+ /** One card type a planning kit declares; `flow` is the key of its main flow. */
428
+ export interface PlanningKitTypeView {
429
+ key: string
430
+ label: string
431
+ flow: string
432
+ }
433
+
434
+ /**
435
+ * A planning kit as a connector sees it: a ready set of card types and status flows for one kind
436
+ * of work-management product, written into the target's common package on `apply`.
437
+ */
438
+ export interface PlanningKitView {
439
+ id: string
440
+ /** The work kind the kit serves — a bare string, so a newer kind never fails an older reader. */
441
+ kind: string
442
+ title: string
443
+ purpose: string
444
+ /** The project-level container type the kit's cards live in. */
445
+ container: { key: string, label: string }
446
+ types: PlanningKitTypeView[]
447
+ flows: PlanningKitFlowView[]
448
+ }
449
+
450
+ /** What `project.kit.describe` answers. */
451
+ export interface ConnectKitDescribe {
452
+ kits: PlanningKitView[]
453
+ }
454
+
455
+ /** Apply one kit; `types` keeps only these card-type keys of it (every type when omitted). */
456
+ export interface ConnectKitApplyBody {
457
+ kit: string
458
+ types?: string[]
459
+ }
460
+
461
+ /** What `project.kit.apply` answers: the type keys written, those left out, and warnings. */
462
+ export interface ConnectKitApplyResult {
463
+ applied: string[]
464
+ skipped: string[]
465
+ warnings: string[]
466
+ }
@@ -2,6 +2,7 @@ import type { JSONSchemaType } from 'ajv'
2
2
  import { CODE_MAX } from '@owlmeans/planning'
3
3
  import { ProjectArea } from '../areas/consts.js'
4
4
  import { TENANCY_QUOTE_MAX } from '../areas/tenancy.js'
5
+ import { CASE_QUOTE_MAX, WorkKind } from '../blueprint/consts.js'
5
6
  import { StoryKind } from '../ba/consts.js'
6
7
  import { ConnectLlm, ConnectTarget } from '../connect/consts.js'
7
8
  import { ProjectOriginSchema } from '../convert/schemas.js'
@@ -25,6 +26,8 @@ export const ViableProjectFieldsSchema = {
25
26
  blueprint: { type: 'string', minLength: 1, maxLength: 64, nullable: true },
26
27
  blueprintCase: { type: 'string', minLength: 1, maxLength: 64, nullable: true },
27
28
  gameKind: { type: 'string', minLength: 1, maxLength: 64, nullable: true },
29
+ workKind: { type: 'string', enum: [...Object.values(WorkKind), null], nullable: true },
30
+ caseQuote: { type: 'string', minLength: 1, maxLength: CASE_QUOTE_MAX, nullable: true },
28
31
  target: { type: 'string', enum: [...Object.values(ConnectTarget), null], nullable: true },
29
32
  origin: { ...ProjectOriginSchema, nullable: true },
30
33
  connectLlmMode: { type: 'string', enum: [...Object.values(ConnectLlm), null], nullable: true },
@@ -1,5 +1,6 @@
1
1
  import type { Card, Project, Specification } from '@owlmeans/planning'
2
2
  import type { ProjectArea } from '../areas/consts.js'
3
+ import type { WorkKind } from '../blueprint/consts.js'
3
4
  import type { ViableTenancyDecision } from '../areas/tenancy.js'
4
5
  import type { StoryKind } from '../ba/consts.js'
5
6
  import type { ConnectLlm, ConnectTarget } from '../connect/consts.js'
@@ -25,6 +26,10 @@ export type ViableProjectFields = {
25
26
  blueprintCase?: string
26
27
  /** How a game is played; absent for every other case. */
27
28
  gameKind?: string
29
+ /** Which kind of work a work-management product keeps; absent for every other case. */
30
+ workKind?: WorkKind
31
+ /** The requester's sentence the work-management case was verified against. */
32
+ caseQuote?: string
28
33
  /** Where the tree lives; absent is the platform's own slot. */
29
34
  target?: ConnectTarget
30
35
  /** Where converted code came from; present only on an import. */
@@ -37,8 +37,24 @@ export enum SlotCommandType {
37
37
  Files = 'files',
38
38
  Shell = 'shell',
39
39
  Git = 'git',
40
+ Database = 'database',
40
41
  }
41
42
 
43
+ /** Inspection of the live dev database; mutations belong in target source migrations. */
44
+ export enum SlotDatabaseCommand {
45
+ Info = 'databaseInfo',
46
+ Query = 'databaseQuery',
47
+ }
48
+
49
+ export const DATABASE_READ_LIMITS = {
50
+ defaultRows: 100,
51
+ maxRows: 1000,
52
+ maxBytes: 131_072,
53
+ maxQueryChars: 16_000,
54
+ maxParams: 100,
55
+ timeoutMs: 10_000,
56
+ } as const
57
+
42
58
  export enum SlotGitCommand {
43
59
  Ensure = 'ensure',
44
60
  /**
@@ -306,6 +322,7 @@ export const COMMAND_TIMEOUTS: Partial<Record<SlotCommandType, number>> = {
306
322
  [SlotCommandType.Files]: 60_000,
307
323
  [SlotCommandType.Git]: 120_000,
308
324
  [SlotCommandType.Shell]: 1_200_000,
325
+ [SlotCommandType.Database]: 45_000,
309
326
  }
310
327
 
311
328
  export const DEFAULT_COMMAND_TIMEOUT = 60_000
@@ -356,6 +373,7 @@ const perCommandTimeout = (type: SlotCommandType, command: string): number | und
356
373
  switch (type) {
357
374
  case SlotCommandType.Shell: return SHELL_COMMAND_TIMEOUTS[command]
358
375
  case SlotCommandType.Git: return GIT_COMMAND_TIMEOUTS[command as SlotGitCommand]
376
+ case SlotCommandType.Database: return COMMAND_TIMEOUTS[SlotCommandType.Database]
359
377
  default: return FILE_COMMAND_TIMEOUTS[command as SlotFileCommand]
360
378
  }
361
379
  }
@@ -387,6 +405,8 @@ export const commandDeadline = (type: SlotCommandType, command: string): number
387
405
  return COMMAND_DEADLINES[command] ?? DEFAULT_COMMAND_DEADLINE
388
406
  case SlotCommandType.Git:
389
407
  return GIT_COMMAND_DEADLINES[command as SlotGitCommand] ?? DEFAULT_GIT_COMMAND_DEADLINE
408
+ case SlotCommandType.Database:
409
+ return 30_000
390
410
  default:
391
411
  return FILE_COMMAND_DEADLINES[command as SlotFileCommand] ?? DEFAULT_FILE_COMMAND_DEADLINE
392
412
  }
@@ -1,5 +1,5 @@
1
1
  import type { JSONSchemaType } from 'ajv'
2
- import { SlotCommandType, SlotFileCommand, SlotGitCommand, SlotShellCommand } from './consts.js'
2
+ import { SlotCommandType, SlotDatabaseCommand, SlotFileCommand, SlotGitCommand, SlotShellCommand } from './consts.js'
3
3
  import type { SlotCommandPayload, SlotGitCloneArgs } from './types.js'
4
4
 
5
5
  export const SlotCommandPayloadSchema = {
@@ -14,6 +14,7 @@ export const SlotCommandPayloadSchema = {
14
14
  { type: 'string', enum: Object.values(SlotFileCommand) },
15
15
  { type: 'string', enum: Object.values(SlotShellCommand) },
16
16
  { type: 'string', enum: Object.values(SlotGitCommand) },
17
+ { type: 'string', enum: Object.values(SlotDatabaseCommand) },
17
18
  ]
18
19
  },
19
20
  args: {
package/src/slot/types.ts CHANGED
@@ -1,6 +1,35 @@
1
- import type { SlotCommandType, SlotFileCommand, SlotGitCommand, SlotShellCommand, SubProject } from './consts.js'
1
+ import type { SlotCommandType, SlotDatabaseCommand, SlotFileCommand, SlotGitCommand, SlotShellCommand, SubProject } from './consts.js'
2
2
 
3
- export type SlotCommand = SlotFileCommand | SlotShellCommand | SlotGitCommand
3
+ export type SlotCommand = SlotFileCommand | SlotShellCommand | SlotGitCommand | SlotDatabaseCommand
4
+
5
+ /** Driver identity is open-ended; inspect it before choosing a query language. No credentials. */
6
+ export interface SlotDatabaseInfo {
7
+ kind: string
8
+ queryLanguage: string
9
+ database: string
10
+ schema: string
11
+ role: string
12
+ version: string
13
+ readOnly: boolean
14
+ limits: { defaultRows: number; maxRows: number; maxBytes: number; maxQueryChars: number; maxParams: number; timeoutMs: number }
15
+ }
16
+
17
+ export interface SlotDatabaseQueryArgs {
18
+ query: string
19
+ params?: Array<string | number | boolean | null>
20
+ maxRows?: number
21
+ timeoutMs?: number
22
+ }
23
+
24
+ export type SlotDatabaseQueryResult = {
25
+ ok: true
26
+ columns: Array<{ name: string; dataType: string }>
27
+ rows: Record<string, unknown>[]
28
+ truncated: boolean
29
+ } | {
30
+ ok: false
31
+ error: { message: string; code?: string; detail?: string; hint?: string; position?: string }
32
+ }
4
33
 
5
34
  export interface SlotCommandPayload {
6
35
  type: SlotCommandType
package/src/types.ts CHANGED
@@ -7,6 +7,11 @@ export interface SlotMetadata extends
7
7
  }
8
8
 
9
9
  export interface SlotConstMetadata {
10
+ /**
11
+ * The project's ALIAS — a lowercase slug, an identifier. The OIDC client id base, the key
12
+ * namespace prefix and the address are composed from it, so it never reaches a person as the
13
+ * product's name; that is the project card's `title`.
14
+ */
10
15
  projectName: string
11
16
  oidcClientId: string
12
17
  oidcRealm: string
@@ -25,18 +25,29 @@ describe('brandingEnv', () => {
25
25
 
26
26
  it('composes the rest of the branding env from whatever is present, blank otherwise', () => {
27
27
  expect(brandingEnv({
28
- brandingCopyright: '© 2026 Acme', projectName: 'Acme CRM',
28
+ brandingCopyright: '© 2026 Acme', projectName: 'acme-crm',
29
29
  })).toEqual({
30
30
  BRANDING_COPYRIGHT: '© 2026 Acme',
31
31
  BRANDING_ORGANIZATION: '',
32
32
  BRANDING_TERMS_URL: '',
33
33
  BRANDING_PRIVACY_URL: '',
34
34
  BRANDING_CREDIT: '1',
35
- BRANDING_PRODUCT: 'Acme CRM',
35
+ BRANDING_PRODUCT: '',
36
36
  BRANDING_GOOGLE_TAG: '',
37
37
  })
38
38
  })
39
39
 
40
+ it('never delivers the project alias as the product name', () => {
41
+ // `projectName` is the lowercase slug the OIDC client and the address are composed from. Taken
42
+ // for the product's name it titled the document `slopepact` over a header saying `SlopePact`.
43
+ // The name a person reads is the target's own `APP_TITLE`, which the build falls back to when
44
+ // this is empty — and the key is still emitted, so the bundle has no bare `process.env` read.
45
+ for (const projectName of ['slopepact', 'Acme CRM', '']) {
46
+ expect(brandingEnv({ projectName }).BRANDING_PRODUCT).toBe('')
47
+ }
48
+ expect(Object.keys(brandingEnv({ projectName: 'slopepact' }))).toContain('BRANDING_PRODUCT')
49
+ })
50
+
40
51
  it('carries the Google tag, and emits it empty when none is set', () => {
41
52
  // The METADATA key is omitted from a push while unset (an older publisher refuses it); the
42
53
  // ENVIRONMENT key is always there, and '' is what tells the build to load no tag.
@@ -4,7 +4,10 @@ import { aliasOf, protocols } from '@owlmeans/entrypoint'
4
4
  import { DEFAULT_GUARD } from '@owlmeans/auth-common'
5
5
  import { RouteMethod, RouteProtocols } from '@owlmeans/route'
6
6
  import { connect, CONNECT_BRANDING_GOOGLE_TAG_MAX } from '../src/connect/consts.js'
7
- import { ConnectProjectBrandingSaveSchema } from '../src/connect/schemas.js'
7
+ import {
8
+ ConnectKitApplyBodySchema, ConnectKitApplyResultSchema, ConnectKitDescribeSchema, ConnectProjectBrandingSaveSchema,
9
+ PlanningKitViewSchema,
10
+ } from '../src/connect/schemas.js'
8
11
  import { connectProtocols } from '../src/connect/entrypoints.js'
9
12
  import { connectRef } from '../src/connect/references.js'
10
13
 
@@ -18,8 +21,8 @@ describe('@owlmeans/viable-common — connector protocol tree', () => {
18
21
  expect(tree.convert.proceed.alias).toBe(connect.convert.proceed)
19
22
  expect(tree.inquiry.answer.alias).toBe(connect.inquiry.answer)
20
23
  expect(tree.files.list.alias).toBe(connect.files.list)
21
- expect(protocols(tree)).toHaveLength(26)
22
- expect(new Set(protocols(tree).map(protocol => protocol.alias)).size).toBe(26)
24
+ expect(protocols(tree)).toHaveLength(28)
25
+ expect(new Set(protocols(tree).map(protocol => protocol.alias)).size).toBe(28)
23
26
  })
24
27
 
25
28
  test('declares exactly the routes a connector calls — every alias, reference and path, and no socket', () => {
@@ -29,11 +32,11 @@ describe('@owlmeans/viable-common — connector protocol tree', () => {
29
32
 
30
33
  expect(addresses).toEqual([
31
34
  'GET /convert/:id', 'GET /convert/:id/check', 'GET /pipeline/:id/:runId', 'GET /project',
32
- 'GET /project/:id/branding', 'GET /project/:id/files', 'GET /project/:id/status',
35
+ 'GET /project/:id/branding', 'GET /project/:id/files', 'GET /project/:id/kits', 'GET /project/:id/status',
33
36
  'GET /project/:id/story/:storyId/status', 'GET /session/:sessionId/ops',
34
37
  'POST /convert', 'POST /convert/:id/proceed', 'POST /convert/:id/purge', 'POST /convert/:id/start',
35
38
  'POST /pipeline/:id/:runId/resume', 'POST /project', 'POST /project/:id/branding',
36
- 'POST /project/:id/confirm', 'POST /project/:id/inquiry/:inquiryId', 'POST /project/:id/modify',
39
+ 'POST /project/:id/confirm', 'POST /project/:id/inquiry/:inquiryId', 'POST /project/:id/kits', 'POST /project/:id/modify',
37
40
  'POST /project/:id/reinit', 'POST /project/attach', 'POST /session', 'POST /session/:sessionId/close',
38
41
  'POST /session/:sessionId/ops/:opId', 'POST /session/delegated',
39
42
  ])
@@ -94,4 +97,48 @@ describe('@owlmeans/viable-common — connector protocol tree', () => {
94
97
  expect((connect.project as Record<string, unknown>).job).toBeUndefined()
95
98
  expect((connectRef.project as Record<string, unknown>).job).toBeUndefined()
96
99
  })
100
+
101
+ test('describes and applies planning kits at one project path, under the owned base and no paid gate', () => {
102
+ const paid = connectProtocols({
103
+ guard: DEFAULT_GUARD,
104
+ gate: { alias: 'test-gate', params: ['owner'] },
105
+ localLlm: { alias: 'test-paid-llm', params: ['cap'] },
106
+ })
107
+ const { describe: list, apply } = paid.project.kit
108
+
109
+ expect([list.alias, apply.alias]).toEqual([connect.project.kit.describe, connect.project.kit.apply])
110
+ expect([list.route.route.path, apply.route.route.path]).toEqual(['/project/:id/kits', '/project/:id/kits'])
111
+ expect([list.route.route.method, apply.route.route.method]).toEqual([RouteMethod.GET, RouteMethod.POST])
112
+ expect([aliasOf(list.route.route.parent), aliasOf(apply.route.route.parent)]).toEqual([connect.base, connect.base])
113
+ expect([list.gate, apply.gate]).toEqual([undefined, undefined])
114
+ expect([connectRef.project.kit.describe.alias, connectRef.project.kit.apply.alias])
115
+ .toEqual([connect.project.kit.describe, connect.project.kit.apply])
116
+ })
117
+
118
+ test('the kit view, the describe reply, the apply body and its result are closed shapes', () => {
119
+ const ajv = new Ajv({ strict: false })
120
+ const view = ajv.compile(PlanningKitViewSchema)
121
+ const reply = ajv.compile(ConnectKitDescribeSchema)
122
+ const body = ajv.compile(ConnectKitApplyBodySchema)
123
+ const result = ajv.compile(ConnectKitApplyResultSchema)
124
+ const kit = {
125
+ id: 'project', kind: 'project', title: 'Project tracking', purpose: 'Tasks moving to done',
126
+ container: { key: 'workspace', label: 'Project' },
127
+ types: [{ key: 'task', label: 'Task', flow: 'task' }],
128
+ flows: [{ key: 'task', label: 'Task', statuses: [{ key: 'open', label: 'Open', intrinsic: 'planned' }] }],
129
+ }
130
+
131
+ expect(view(kit)).toBe(true)
132
+ expect(view({ ...kit, extra: true })).toBe(false)
133
+ expect(view({ ...kit, types: [{ key: 'task', label: 'Task' }] })).toBe(false)
134
+ expect(reply({ kits: [kit] })).toBe(true)
135
+ expect(body({ kit: 'project' })).toBe(true)
136
+ expect(body({ kit: 'project', types: ['task', 'bug'] })).toBe(true)
137
+ expect(body({ kit: 'project', types: null })).toBe(true)
138
+ expect(body({ kit: '' })).toBe(false)
139
+ expect(body({ types: ['task'] })).toBe(false)
140
+ expect(body({ kit: 'project', force: true })).toBe(false)
141
+ expect(result({ applied: ['task'], skipped: ['bug'], warnings: [] })).toBe(true)
142
+ expect(result({ applied: ['task'], skipped: [] })).toBe(false)
143
+ })
97
144
  })
@@ -263,6 +263,14 @@ describe('viable-common - the viable field schemas', () => {
263
263
  expect(project({ alias: 'slot-booker' })).toBe(false)
264
264
  })
265
265
 
266
+ test('a project records a work kind from the closed set and a bounded case quote', () => {
267
+ expect(project({ blueprintCase: 'work-management', workKind: 'crm', caseQuote: 'A CRM for ski resorts.' })).toBe(true)
268
+ expect(project({ workKind: null, caseQuote: null })).toBe(true)
269
+ expect(project({ workKind: 'kanban' })).toBe(false)
270
+ expect(project({ caseQuote: '' })).toBe(false)
271
+ expect(project({ caseQuote: 'x'.repeat(1025) })).toBe(false)
272
+ })
273
+
266
274
  test('a story carries the landing flag, spelled or nulled', () => {
267
275
  expect(story({ area: ProjectArea.User, primary: false, landing: true })).toBe(true)
268
276
  expect(story({ area: ProjectArea.User, primary: false, landing: null })).toBe(true)