@owlmeans/viable-common 0.0.39 → 0.0.41

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 (62) hide show
  1. package/README.md +4 -4
  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 +331 -391
  5. package/build/areas/index.d.ts +1 -0
  6. package/build/areas/index.d.ts.map +1 -1
  7. package/build/areas/index.js +1 -0
  8. package/build/areas/index.js.map +1 -1
  9. package/build/areas/tenancy.d.ts +55 -0
  10. package/build/areas/tenancy.d.ts.map +1 -0
  11. package/build/areas/tenancy.js +33 -0
  12. package/build/areas/tenancy.js.map +1 -0
  13. package/build/blueprint/types.d.ts +19 -1
  14. package/build/blueprint/types.d.ts.map +1 -1
  15. package/build/blueprint/types.js.map +1 -1
  16. package/build/connect/consts.d.ts +3 -15
  17. package/build/connect/consts.d.ts.map +1 -1
  18. package/build/connect/consts.js +3 -15
  19. package/build/connect/consts.js.map +1 -1
  20. package/build/connect/entrypoints.d.ts +33 -69
  21. package/build/connect/entrypoints.d.ts.map +1 -1
  22. package/build/connect/entrypoints.js +5 -25
  23. package/build/connect/entrypoints.js.map +1 -1
  24. package/build/connect/references.d.ts +1 -31
  25. package/build/connect/references.d.ts.map +1 -1
  26. package/build/connect/references.js +0 -7
  27. package/build/connect/references.js.map +1 -1
  28. package/build/connect/types.d.ts +1 -25
  29. package/build/connect/types.d.ts.map +1 -1
  30. package/build/dev/consts.d.ts +15 -0
  31. package/build/dev/consts.d.ts.map +1 -1
  32. package/build/dev/consts.js +16 -0
  33. package/build/dev/consts.js.map +1 -1
  34. package/build/dev/schemas.d.ts +7 -0
  35. package/build/dev/schemas.d.ts.map +1 -1
  36. package/build/dev/schemas.js +9 -1
  37. package/build/dev/schemas.js.map +1 -1
  38. package/build/dev/types.d.ts +15 -1
  39. package/build/dev/types.d.ts.map +1 -1
  40. package/build/planning/schemas.d.ts.map +1 -1
  41. package/build/planning/schemas.js +25 -0
  42. package/build/planning/schemas.js.map +1 -1
  43. package/build/planning/types.d.ts +9 -0
  44. package/build/planning/types.d.ts.map +1 -1
  45. package/package.json +6 -6
  46. package/src/areas/index.ts +1 -0
  47. package/src/areas/tenancy.ts +69 -0
  48. package/src/blueprint/types.ts +19 -1
  49. package/src/connect/consts.ts +3 -16
  50. package/src/connect/entrypoints.ts +10 -60
  51. package/src/connect/references.ts +4 -26
  52. package/src/connect/types.ts +1 -22
  53. package/src/dev/consts.ts +16 -0
  54. package/src/dev/schemas.ts +13 -1
  55. package/src/dev/types.ts +15 -1
  56. package/src/planning/schemas.ts +25 -0
  57. package/src/planning/types.ts +9 -0
  58. package/tests/access-schema.spec.ts +37 -0
  59. package/tests/connect-convert.spec.ts +5 -11
  60. package/tests/connect-entrypoints.spec.ts +33 -14
  61. package/tests/planning.spec.ts +26 -0
  62. package/tests/tenancy.spec.ts +48 -0
@@ -1,8 +1,8 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
2
  import Ajv from 'ajv'
3
3
  import addFormats from 'ajv-formats'
4
- import { RouteMethod, route } from '@owlmeans/route'
5
- import { aliasOf, openProtocol, protocols } from '@owlmeans/entrypoint'
4
+ import { RouteMethod } from '@owlmeans/route'
5
+ import { aliasOf, protocols } from '@owlmeans/entrypoint'
6
6
  import { connect, ConnectInquiryKind, ConnectOpKind } from '../src/connect/consts.js'
7
7
  import { connectProtocols } from '../src/connect/entrypoints.js'
8
8
  import { connectRef } from '../src/connect/references.js'
@@ -20,7 +20,6 @@ const entrypoints = () => connectProtocols({
20
20
  guard: 'test-guard',
21
21
  gate: { alias: 'test-gate', params: ['id'] },
22
22
  localLlm: { alias: 'test-paid-llm', params: ['id'] },
23
- updateBase: openProtocol(route('test:update:base', '/update')),
24
23
  })
25
24
 
26
25
  const entrypointOf = (alias: string) => {
@@ -102,11 +101,9 @@ describe('viable-common - the conversion additions to the connector contract', (
102
101
  [connect.convert.check, '/convert/:id/check', RouteMethod.GET],
103
102
  [connect.convert.start, '/convert/:id/start', RouteMethod.POST],
104
103
  [connect.convert.proceed, '/convert/:id/proceed', RouteMethod.POST],
105
- [connect.convert.cancel, '/convert/:id/cancel', RouteMethod.POST],
106
104
  [connect.convert.status, '/convert/:id', RouteMethod.GET],
107
105
  [connect.convert.purge, '/convert/:id/purge', RouteMethod.POST],
108
106
  [connect.inquiry.answer, '/project/:id/inquiry/:inquiryId', RouteMethod.POST],
109
- [connect.project.converterLlm, '/project/:id/converter-llm', RouteMethod.POST],
110
107
  [connect.files.list, '/project/:id/files', RouteMethod.GET],
111
108
  ]
112
109
 
@@ -122,21 +119,18 @@ describe('viable-common - the conversion additions to the connector contract', (
122
119
  expect(connectRef.convert.check.alias).toBe(connect.convert.check)
123
120
  expect(connectRef.convert.start.alias).toBe(connect.convert.start)
124
121
  expect(connectRef.convert.proceed.alias).toBe(connect.convert.proceed)
125
- expect(connectRef.convert.cancel.alias).toBe(connect.convert.cancel)
126
122
  expect(connectRef.convert.status.alias).toBe(connect.convert.status)
127
123
  expect(connectRef.convert.purge.alias).toBe(connect.convert.purge)
128
124
  expect(connectRef.inquiry.answer.alias).toBe(connect.inquiry.answer)
129
- expect(connectRef.project.converterLlm.alias).toBe(connect.project.converterLlm)
130
125
  expect(connectRef.files.list.alias).toBe(connect.files.list)
131
126
  })
132
127
 
133
128
  test('the conversion routes hang under the connector base and carry no paid gate', () => {
134
129
  // Delegated inference is the DEFAULT for a conversion, not an experimental capability — so
135
- // unlike `project.llm`, nothing here sits behind the local-LLM gate.
136
- expect(entrypointOf(connect.project.llm).gate?.alias).toBe('test-paid-llm')
130
+ // unlike the delegated session, nothing here sits behind the local-LLM gate.
131
+ expect(entrypointOf(connect.session.openDelegated).gate?.alias).toBe('test-paid-llm')
137
132
 
138
- const gated = [...Object.values(connect.convert), connect.inquiry.answer,
139
- connect.project.converterLlm]
133
+ const gated = [...Object.values(connect.convert), connect.inquiry.answer]
140
134
  for (const alias of gated) {
141
135
  expect(entrypointOf(alias).gate, alias).toBeUndefined()
142
136
  expect(aliasOf(routeOf(alias).parent!), alias).toBe(connect.base)
@@ -1,30 +1,48 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
2
  import Ajv from 'ajv'
3
- import { aliasOf, openProtocol, protocols } from '@owlmeans/entrypoint'
3
+ import { aliasOf, protocols } from '@owlmeans/entrypoint'
4
4
  import { DEFAULT_GUARD } from '@owlmeans/auth-common'
5
- import { route, RouteMethod } from '@owlmeans/route'
5
+ import { RouteMethod, RouteProtocols } from '@owlmeans/route'
6
6
  import { connect, CONNECT_BRANDING_GOOGLE_TAG_MAX } from '../src/connect/consts.js'
7
7
  import { ConnectProjectBrandingSaveSchema } from '../src/connect/schemas.js'
8
8
  import { connectProtocols } from '../src/connect/entrypoints.js'
9
9
  import { connectRef } from '../src/connect/references.js'
10
10
 
11
11
  describe('@owlmeans/viable-common — connector protocol tree', () => {
12
- const tree = connectProtocols({
13
- guard: DEFAULT_GUARD,
14
- updateBase: openProtocol(route('manager:update', '/update')),
15
- })
12
+ const tree = connectProtocols({ guard: DEFAULT_GUARD })
16
13
 
17
14
  test('exports named immutable declarations and a complete flat materialization view', () => {
18
15
  expect(tree.base.alias).toBe(connect.base)
19
16
  expect(tree.session.open.alias).toBe(connect.session.open)
20
- expect(tree.project.llm.alias).toBe(connect.project.llm)
21
- expect(tree.project.converterLlm.alias).toBe(connect.project.converterLlm)
22
17
  expect(tree.story.status.alias).toBe(connect.story.status)
23
18
  expect(tree.convert.proceed.alias).toBe(connect.convert.proceed)
24
19
  expect(tree.inquiry.answer.alias).toBe(connect.inquiry.answer)
25
20
  expect(tree.files.list.alias).toBe(connect.files.list)
26
- expect(protocols(tree)).toHaveLength(34)
27
- expect(new Set(protocols(tree).map(protocol => protocol.alias)).size).toBe(34)
21
+ expect(protocols(tree)).toHaveLength(26)
22
+ expect(new Set(protocols(tree).map(protocol => protocol.alias)).size).toBe(26)
23
+ })
24
+
25
+ test('declares exactly the routes a connector calls — every alias, reference and path, and no socket', () => {
26
+ const declared = protocols(tree)
27
+ const addresses = declared.filter(protocol => protocol.route.route.method != null)
28
+ .map(protocol => `${protocol.route.route.method!.toUpperCase()} ${protocol.route.route.path}`).sort()
29
+
30
+ expect(addresses).toEqual([
31
+ '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',
33
+ 'GET /project/:id/story/:storyId/status', 'GET /session/:sessionId/ops',
34
+ 'POST /convert', 'POST /convert/:id/proceed', 'POST /convert/:id/purge', 'POST /convert/:id/start',
35
+ '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',
37
+ 'POST /project/:id/reinit', 'POST /project/attach', 'POST /session', 'POST /session/:sessionId/close',
38
+ 'POST /session/:sessionId/ops/:opId', 'POST /session/delegated',
39
+ ])
40
+ expect(declared.every(protocol => protocol.route.route.protocol !== RouteProtocols.SOCKET)).toBe(true)
41
+ // The alias table, the tree and the typed references name the same routes.
42
+ const leaves = (value: object): string[] => Object.values(value).flatMap(entry =>
43
+ typeof entry === 'string' ? [entry] : 'alias' in entry ? [entry.alias as string] : leaves(entry))
44
+ expect(leaves(connect).sort()).toEqual(declared.map(protocol => protocol.alias).sort())
45
+ expect(leaves(connectRef).sort()).toEqual(declared.map(protocol => protocol.alias).filter(alias => alias !== connect.base).sort())
28
46
  })
29
47
 
30
48
  test('reads and saves a project\'s branding at one path, under the owned base and no paid gate', () => {
@@ -32,7 +50,6 @@ describe('@owlmeans/viable-common — connector protocol tree', () => {
32
50
  guard: DEFAULT_GUARD,
33
51
  gate: { alias: 'test-gate', params: ['owner'] },
34
52
  localLlm: { alias: 'test-paid-llm', params: ['cap'] },
35
- updateBase: openProtocol(route('manager:update', '/update')),
36
53
  })
37
54
  const get = paid.project.branding.get
38
55
  const save = paid.project.branding.save
@@ -45,9 +62,11 @@ describe('@owlmeans/viable-common — connector protocol tree', () => {
45
62
  expect(save.route.route.method).toBe(RouteMethod.POST)
46
63
  expect(aliasOf(get.route.route.parent)).toBe(connect.base)
47
64
  expect(aliasOf(save.route.route.parent)).toBe(connect.base)
48
- // The paid local-LLM gate sits on its own two routes; branding carries no gate of its own and
49
- // inherits only the base's guard and ownership gate.
50
- expect(paid.project.llm.gate?.alias).toBe('test-paid-llm')
65
+ // The paid local-LLM gate sits on the delegated session alone; branding carries no gate of its
66
+ // own and inherits only the base's guard and ownership gate.
67
+ expect(paid.session.openDelegated.gate?.alias).toBe('test-paid-llm')
68
+ expect(protocols(paid).filter(protocol => protocol.gate?.alias === 'test-paid-llm'))
69
+ .toEqual([paid.session.openDelegated])
51
70
  expect(get.gate).toBeUndefined()
52
71
  expect(save.gate).toBeUndefined()
53
72
  expect(connectRef.project.branding.save.alias).toBe(connect.project.branding.save)
@@ -276,6 +276,32 @@ describe('viable-common - the viable field schemas', () => {
276
276
  expect(project({ landing: { story: 'US-ABC12' } })).toBe(false)
277
277
  expect(project({ landing: { story: 'US-ABC12', at, reason: 'best fit' } })).toBe(false)
278
278
  })
279
+
280
+ test('a project records a tenancy decision with both flags, who decided it and when', () => {
281
+ expect(project({ tenancy: { operators: false, users: true, by: 'model', at } })).toBe(true)
282
+ expect(project({
283
+ tenancy: {
284
+ operators: true, users: true, by: 'owner', at,
285
+ quotes: { operators: 'Each agency manages its own staff.', users: null },
286
+ },
287
+ })).toBe(true)
288
+ expect(project({ tenancy: null })).toBe(true)
289
+ })
290
+
291
+ test('a project refuses a partial tenancy decision, an unknown decider and a stray key', () => {
292
+ expect(project({ tenancy: { users: true, by: 'model', at } })).toBe(false)
293
+ expect(project({ tenancy: { operators: true, users: false, at } })).toBe(false)
294
+ expect(project({ tenancy: { operators: true, users: false, by: 'model' } })).toBe(false)
295
+ expect(project({ tenancy: { operators: 'yes', users: false, by: 'model', at } })).toBe(false)
296
+ expect(project({ tenancy: { operators: true, users: false, by: 'agent', at } })).toBe(false)
297
+ expect(project({ tenancy: { operators: true, users: false, by: 'model', at, reason: 'b2b' } })).toBe(false)
298
+ expect(project({
299
+ tenancy: { operators: true, users: false, by: 'model', at, quotes: { staff: 'x' } },
300
+ })).toBe(false)
301
+ expect(project({
302
+ tenancy: { operators: true, users: false, by: 'model', at, quotes: { operators: 'x'.repeat(1025) } },
303
+ })).toBe(false)
304
+ })
279
305
  })
280
306
 
281
307
  describe('viable-common - the viable card helpers', () => {
@@ -0,0 +1,48 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { NO_TENANCY, ProjectArea, tenancyOf, tenantedArea } from '../src/index.js'
3
+ import { LandingGatePreference } from '../src/blueprint/index.js'
4
+
5
+ const experience = (tenancy: unknown) =>
6
+ ({ experience: { landingGate: LandingGatePreference.Allow, tenancy: tenancy as never } })
7
+
8
+ describe('viable-common - the project tenancy', () => {
9
+ test('NO_TENANCY is both flags off and cannot be changed', () => {
10
+ expect(NO_TENANCY).toEqual({ operators: false, users: false })
11
+ expect(Object.isFrozen(NO_TENANCY)).toBe(true)
12
+ })
13
+
14
+ test('tenancyOf answers NO_TENANCY for no blueprint, no layer, no key and an unreadable value', () => {
15
+ expect(tenancyOf()).toBe(NO_TENANCY)
16
+ expect(tenancyOf(null)).toBe(NO_TENANCY)
17
+ expect(tenancyOf({})).toBe(NO_TENANCY)
18
+ expect(tenancyOf({ experience: { landingGate: LandingGatePreference.Allow } })).toBe(NO_TENANCY)
19
+ expect(tenancyOf(experience(null))).toBe(NO_TENANCY)
20
+ expect(tenancyOf(experience('both'))).toBe(NO_TENANCY)
21
+ expect(tenancyOf(experience({ operators: 'yes', users: 1 }))).toBe(NO_TENANCY)
22
+ expect(tenancyOf(experience({ operators: false, users: false }))).toBe(NO_TENANCY)
23
+ expect(tenancyOf({ experience: 'flat' as never })).toBe(NO_TENANCY)
24
+ })
25
+
26
+ test('tenancyOf reads a literal true per flag, and an absent flag as off', () => {
27
+ expect(tenancyOf(experience({ operators: true, users: false }))).toEqual({ operators: true, users: false })
28
+ expect(tenancyOf(experience({ users: true }))).toEqual({ operators: false, users: true })
29
+ const both = tenancyOf(experience({ operators: true, users: true }))
30
+ expect(both).toEqual({ operators: true, users: true })
31
+ expect(Object.isFrozen(both)).toBe(true)
32
+ })
33
+
34
+ test('tenantedArea: user follows users, operator follows operators, guest and admin never', () => {
35
+ const all = { operators: true, users: true }
36
+ expect(tenantedArea(ProjectArea.User, all)).toBe(true)
37
+ expect(tenantedArea(ProjectArea.Operator, all)).toBe(true)
38
+ expect(tenantedArea(ProjectArea.Guest, all)).toBe(false)
39
+ expect(tenantedArea(ProjectArea.Admin, all)).toBe(false)
40
+
41
+ expect(tenantedArea(ProjectArea.User, { operators: true, users: false })).toBe(false)
42
+ expect(tenantedArea(ProjectArea.Operator, { operators: false, users: true })).toBe(false)
43
+ for (const area of Object.values(ProjectArea)) {
44
+ expect(tenantedArea(area, NO_TENANCY)).toBe(false)
45
+ }
46
+ expect(tenantedArea('partner' as ProjectArea, all)).toBe(false)
47
+ })
48
+ })