@owlmeans/viable-common 0.0.41 → 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 +2 -2
  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 +9 -6
  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 +2 -2
  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
@@ -34,6 +34,16 @@ export enum BlueprintCase {
34
34
  AiAgent = 'ai-agent',
35
35
  /** A browser game: a three.js scene with an ordinary React interface over it. */
36
36
  Game = 'game',
37
+ /**
38
+ * Work management: the product's core is records of work people create and move through
39
+ * statuses or a pipeline, built on the planning packages. Single organization.
40
+ */
41
+ WorkManagement = 'work-management',
42
+ /**
43
+ * Work management whose audiences work inside many organizations. Never classified directly:
44
+ * code picks it from {@link WorkManagement} when the project's tenancy decision has a flag on.
45
+ */
46
+ WorkManagementTenanted = 'work-management-tenanted',
37
47
  }
38
48
 
39
49
  /**
@@ -49,3 +59,29 @@ export enum GameKind {
49
59
  OnlineTurn = 'online-turn',
50
60
  OnlineLive = 'online-live',
51
61
  }
62
+
63
+ /**
64
+ * Which kind of work a work-management product keeps — read by the planning kit and the analysis.
65
+ *
66
+ * Recorded beside the case on the project card (`fields.workKind`), like {@link GameKind}: it
67
+ * changes no dependency, only which ready-made card types and flows the product starts from.
68
+ */
69
+ export enum WorkKind {
70
+ /** Projects broken into tasks or issues moved across stages to done. */
71
+ Project = 'project',
72
+ /** Leads, contacts, accounts and opportunities through a sales pipeline. */
73
+ Crm = 'crm',
74
+ /** Requests become tickets assigned to agents and worked to resolution. */
75
+ ServiceDesk = 'service-desk',
76
+ /** Items and stock across locations; orders and movements through fulfilment stages. */
77
+ Inventory = 'inventory',
78
+ /** Candidates and applications through a hiring workflow per opening. */
79
+ Recruiting = 'recruiting',
80
+ /** Jobs dispatched to field workers and tracked to completion. */
81
+ FieldService = 'field-service',
82
+ /** Requests, approvals or cases through a defined multi-step process. */
83
+ Process = 'process',
84
+ }
85
+
86
+ /** The longest case quote a project card keeps (`fields.caseQuote`) — one sentence, generously. */
87
+ export const CASE_QUOTE_MAX = 1024
@@ -166,6 +166,13 @@ export interface TemplateLayer {
166
166
  patches: BlueprintPatchSpec[]
167
167
  /** Overlay subtrees that belong to one role only — the per-package half of a template. */
168
168
  perRole?: Partial<Record<SubProject, string>>
169
+ /**
170
+ * Case seed directories under the overlay's `.cases/`, copied over the staged tree in order.
171
+ *
172
+ * Names, never paths: the host resolves them against the overlay it resolved. A later seed wins
173
+ * over an earlier one, so a variant lists its base seed first and itself after it.
174
+ */
175
+ seeds?: string[]
169
176
  }
170
177
 
171
178
  export interface BlueprintPatchSpec {
package/src/branding.ts CHANGED
@@ -25,6 +25,9 @@ export const BRANDING_ENV_KEYS = [
25
25
  * safe direction — the opposite failure hands out a paid feature and nobody reports getting more
26
26
  * than they paid for.
27
27
  *
28
+ * `BRANDING_PRODUCT` is always EMITTED empty: the product's name is the target's own `APP_TITLE`,
29
+ * stated from the project card, and the slot metadata carries no human-readable name to deliver.
30
+ *
28
31
  * `BRANDING_GOOGLE_TAG` is always EMITTED, empty when no tag is set, although its metadata key is
29
32
  * omitted from a push while unset: the environment is read by the application's own build, which
30
33
  * treats `''` as "load no tag", while the metadata key is read by a publisher that may predate it.
@@ -35,6 +38,13 @@ export const brandingEnv = (meta: Partial<SlotMetadata>): Record<string, string>
35
38
  BRANDING_TERMS_URL: meta.brandingTermsUrl ?? '',
36
39
  BRANDING_PRIVACY_URL: meta.brandingPrivacyUrl ?? '',
37
40
  BRANDING_CREDIT: meta.brandingCredit === '' ? '' : '1',
38
- BRANDING_PRODUCT: meta.projectName ?? '',
41
+ // Never `projectName`. That is the project's ALIAS — a lowercase slug the OIDC client id, the
42
+ // key namespace and the address are composed from — and a build that took it for the product's
43
+ // name put `slopepact` in the tab, the manifest and the legal pages beside a header saying
44
+ // `SlopePact`. The human-readable name is the project card's title, which the platform states in
45
+ // the target's own `APP_TITLE`; every page reads that, and the build falls back to it whenever
46
+ // this is empty. The key stays, always emitted, so a bundle never carries a bare `process.env`
47
+ // read — and so an explicit override still has somewhere to arrive.
48
+ BRANDING_PRODUCT: '',
39
49
  BRANDING_GOOGLE_TAG: meta.brandingGoogleTag ?? '',
40
50
  })
@@ -394,6 +394,14 @@ export const connect = Object.freeze({
394
394
  attach: 'viable:manager-api:connect:project:attach',
395
395
  reinit: 'viable:manager-api:connect:project:reinit',
396
396
  modify: 'viable:manager-api:connect:project:modify',
397
+ /**
398
+ * Planning kits — ready sets of card types and status flows the platform writes into a
399
+ * target's common package (`describe` lists them, `apply` writes one and rebuilds the slot).
400
+ */
401
+ kit: Object.freeze({
402
+ describe: 'viable:manager-api:connect:project:kit:describe',
403
+ apply: 'viable:manager-api:connect:project:kit:apply',
404
+ }),
397
405
  /**
398
406
  * The project's own branding — copyright, organization, the two legal links, the Google tag.
399
407
  * The platform credit is deliberately NOT here: hiding it is a paid capability with its own
@@ -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)