@owlmeans/viable-common 0.0.6 → 0.0.8

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 (86) hide show
  1. package/README.md +1 -68
  2. package/build/areas/consts.d.ts +1 -1
  3. package/build/areas/consts.js +1 -1
  4. package/build/connect/consts.d.ts +6 -94
  5. package/build/connect/consts.d.ts.map +1 -1
  6. package/build/connect/consts.js +2 -91
  7. package/build/connect/consts.js.map +1 -1
  8. package/build/connect/entrypoints.d.ts +3 -3
  9. package/build/connect/entrypoints.d.ts.map +1 -1
  10. package/build/connect/entrypoints.js +54 -95
  11. package/build/connect/entrypoints.js.map +1 -1
  12. package/build/connect/ops.d.ts +2 -46
  13. package/build/connect/ops.d.ts.map +1 -1
  14. package/build/connect/references.d.ts +63 -117
  15. package/build/connect/references.d.ts.map +1 -1
  16. package/build/connect/references.js +0 -16
  17. package/build/connect/references.js.map +1 -1
  18. package/build/connect/schemas.d.ts +2 -17
  19. package/build/connect/schemas.d.ts.map +1 -1
  20. package/build/connect/schemas.js +7 -91
  21. package/build/connect/schemas.js.map +1 -1
  22. package/build/connect/types.d.ts +1 -125
  23. package/build/connect/types.d.ts.map +1 -1
  24. package/build/dev/types.d.ts +3 -2
  25. package/build/dev/types.d.ts.map +1 -1
  26. package/build/index.d.ts +0 -1
  27. package/build/index.d.ts.map +1 -1
  28. package/build/index.js +0 -1
  29. package/build/index.js.map +1 -1
  30. package/build/skills/catalogue.d.ts.map +1 -1
  31. package/build/skills/catalogue.js +109 -103
  32. package/build/skills/catalogue.js.map +1 -1
  33. package/build/slot/consts.d.ts +2 -73
  34. package/build/slot/consts.d.ts.map +1 -1
  35. package/build/slot/consts.js +5 -105
  36. package/build/slot/consts.js.map +1 -1
  37. package/build/slot/schemas.d.ts +1 -9
  38. package/build/slot/schemas.d.ts.map +1 -1
  39. package/build/slot/schemas.js +0 -18
  40. package/build/slot/schemas.js.map +1 -1
  41. package/build/slot/types.d.ts +0 -71
  42. package/build/slot/types.d.ts.map +1 -1
  43. package/package.json +7 -15
  44. package/src/areas/consts.ts +1 -1
  45. package/src/connect/consts.ts +2 -94
  46. package/src/connect/entrypoints.ts +69 -139
  47. package/src/connect/ops.ts +2 -50
  48. package/src/connect/references.ts +56 -32
  49. package/src/connect/schemas.ts +13 -102
  50. package/src/connect/types.ts +2 -130
  51. package/src/dev/types.ts +3 -2
  52. package/src/index.ts +0 -1
  53. package/src/skills/catalogue.ts +109 -103
  54. package/src/slot/consts.ts +6 -112
  55. package/src/slot/schemas.ts +1 -20
  56. package/src/slot/types.ts +0 -80
  57. package/agent-meta/manifest.json +0 -16
  58. package/agent-meta/skills/viable-common/SKILL.md +0 -179
  59. package/build/convert/consts.d.ts +0 -539
  60. package/build/convert/consts.d.ts.map +0 -1
  61. package/build/convert/consts.js +0 -630
  62. package/build/convert/consts.js.map +0 -1
  63. package/build/convert/helpers.d.ts +0 -91
  64. package/build/convert/helpers.d.ts.map +0 -1
  65. package/build/convert/helpers.js +0 -301
  66. package/build/convert/helpers.js.map +0 -1
  67. package/build/convert/index.d.ts +0 -11
  68. package/build/convert/index.d.ts.map +0 -1
  69. package/build/convert/index.js +0 -4
  70. package/build/convert/index.js.map +0 -1
  71. package/build/convert/schemas.d.ts +0 -62
  72. package/build/convert/schemas.d.ts.map +0 -1
  73. package/build/convert/schemas.js +0 -626
  74. package/build/convert/schemas.js.map +0 -1
  75. package/build/convert/types.d.ts +0 -683
  76. package/build/convert/types.d.ts.map +0 -1
  77. package/build/convert/types.js +0 -2
  78. package/build/convert/types.js.map +0 -1
  79. package/src/convert/consts.ts +0 -672
  80. package/src/convert/helpers.ts +0 -340
  81. package/src/convert/index.ts +0 -10
  82. package/src/convert/schemas.ts +0 -663
  83. package/src/convert/types.ts +0 -741
  84. package/tests/connect-convert.spec.ts +0 -161
  85. package/tests/convert.spec.ts +0 -318
  86. /package/tests/{design.spec.ts → design.test.ts} +0 -0
@@ -107,10 +107,10 @@ These files are the CONTRACT between the packages. Every screen, endpoint and jo
107
107
  them, and each has a sentinel comment marking where a new line goes:
108
108
 
109
109
  - \`sources/common/src/consts.ts\` — the \`app\` alias tree.
110
- - \`sources/common/src/entrypoints.ts\` — the shared \`entrypoints\` declaration list.
111
- - \`sources/api/src/entrypoints.ts\` — server elevations (\`appEntrypoints\`).
112
- - \`sources/web/src/entrypoints.ts\` — client elevations (\`appEntrypoints\`).
113
- - \`sources/worker/src/entrypoints.ts\` — job elevations.
110
+ - \`sources/common/src/entrypoints.ts\` — the shared immutable protocol tree (\`appEntrypoints\`).
111
+ - \`sources/api/src/entrypoints.ts\` — server bindings (\`bind\` + \`handlers<Context>()\`).
112
+ - \`sources/web/src/entrypoints.ts\` — client bindings (\`bindAll\` + \`bindScreen\`).
113
+ - \`sources/worker/src/entrypoints.ts\` — job bindings (\`bind\`).
114
114
  - \`sources/web/src/nav.ts\` — the navigation registry: one line per screen, which
115
115
  is the only thing that puts it in the menus.
116
116
 
@@ -272,13 +272,13 @@ in its own language, with no TypeScript syntax and no import statements it does
272
272
  skill(ViableSkill.OwlMeansEntrypoints, 'Entrypoints — screens and endpoints', `
273
273
  Everything this app addresses — a screen the browser renders, an endpoint the server answers
274
274
  — is an OwlMeans **entrypoint**: declared with \`@owlmeans/entrypoint\` and \`@owlmeans/route\`
275
- in the shared package, elevated with \`@owlmeans/web-client\` in the browser and
276
- \`@owlmeans/server-app\` on the backend. \`@owlmeans/web-client\` builds the route table from the
275
+ in the shared package, bound with \`@owlmeans/client-entrypoint\` in the browser and
276
+ \`@owlmeans/server-entrypoint\` on the backend. \`@owlmeans/web-client\` builds the route table from the
277
277
  entrypoints and renders it over the History API, so there is no router file, no \`<Routes>\` and
278
278
  no \`app.get(...)\`. No third-party routing or HTTP-server package is installed — importing one
279
279
  fails to resolve.
280
280
 
281
- An entrypoint is DECLARED once in the shared package, then ELEVATED on each side that uses it.
281
+ An entrypoint protocol is DECLARED once in the shared package, then BOUND on each side that uses it.
282
282
 
283
283
  **1. The alias.** Aliases live in ONE place, the \`app\` tree in
284
284
  \`sources/common/src/consts.ts\`. NEVER write an alias string inline — always reference
@@ -286,27 +286,30 @@ An entrypoint is DECLARED once in the shared package, then ELEVATED on each side
286
286
 
287
287
  ${ALIAS_CONVENTION}
288
288
 
289
- **2. The declaration** — \`sources/common/src/entrypoints.ts\`, one line in the \`entrypoints\`
290
- array:
289
+ **2. The declaration** — \`sources/common/src/entrypoints.ts\`, one property in the immutable
290
+ \`appEntrypoints\` protocol tree:
291
291
 
292
- import { entrypoint, guard, gate, filter, body } from '@owlmeans/entrypoint'
292
+ import { openProtocol, protocol, contract, typed } from '@owlmeans/entrypoint'
293
293
  import { route, frontend, RouteMethod } from '@owlmeans/route'
294
294
 
295
- entrypoint(route(app.api.task.list, '/list', { parent: app.api.task }), guard(DEFAULT_GUARD))
295
+ task: {
296
+ list: protocol(route(app.api.task.list, '/list', { parent: app.api.task }),
297
+ contract(typed<Task[]>()), { guards: DEFAULT_GUARD }),
298
+ }
296
299
 
297
300
  - \`route(alias, path)\` is a BACKEND route by default. \`route(alias, path, frontend())\` makes
298
301
  it a screen; \`frontend({ default: true, parent: app.web.base })\` makes it the index screen.
299
302
  - Nest with \`{ parent: <group alias> }\` — the child path is APPENDED to the parent's, so the
300
303
  child path is the tail only (\`'/list'\`, not \`'/tasks/list'\`).
301
304
  - \`{ method: RouteMethod.POST }\` (from \`@owlmeans/route\`) for anything that is not a GET.
302
- - \`filter(body<T>(TSchema))\` whenever the body's type has a schema beside it in the shared
305
+ - \`contract.request({ body: typed<T>(TSchema) }, typed<Response>())\` whenever the body's type has a schema beside it in the shared
303
306
  package — every generated type exports one, named after the type with a \`Schema\` suffix
304
307
  (\`Task\` → \`TaskSchema\`). Import both from the shared package and use them; that is what makes
305
308
  the framework reject a malformed request before your handler runs, so the handler never has to
306
309
  check whether a field arrived. Still never INVENT a schema inline: if the type has none, leave
307
310
  the endpoint unfiltered rather than writing a literal here that nothing else agrees with.
308
- - Access is declarative: no \`guard()\` = public, \`guard(DEFAULT_GUARD)\` = any signed-in user,
309
- \`guard(DEFAULT_GUARD, gate(OIDC_GATE, ['<permission>']))\` = a permission is required.
311
+ - Access is declarative: no \`guards\` option = public, \`{ guards: DEFAULT_GUARD }\` = any signed-in user,
312
+ \`{ guards: DEFAULT_GUARD, gate: { alias: OIDC_GATE, params: ['<permission>'] } }\` = a permission is required.
310
313
  \`<permission>\` is a SHAPE, never a value: build the real name from the domain being
311
314
  implemented — the resource, TWO hyphens, the action, lowercase kebab-case, singular resource.
312
315
  Do not write a bracketed word into an application, and do not copy a name out of this
@@ -316,30 +319,29 @@ array:
316
319
  nothing and the gate then refuses every request. Guards and gates are INHERITED by children and
317
320
  enforced by the framework — a handler or a screen never re-checks them.
318
321
 
319
- **3. The elevation.** The declaration alone renders and answers nothing.
322
+ **3. The binding.** The protocol declaration alone renders and answers nothing.
320
323
 
321
324
  - Server, \`sources/api/src/entrypoints.ts\`:
322
- \`elevate(appEntrypoints, app.api.task.list, handleRequest(...))\`
325
+ \`bind(protocols.api.task.list, api.request(protocols.api.task.list, handleTaskList))\`
323
326
  - Client screen, \`sources/web/src/entrypoints.ts\`:
324
- \`elevate(appEntrypoints, app.web.taskList, handler(TaskListScreen))\`
325
- - Client CALL, same file: a BARE elevation, no component
326
- \`elevate(appEntrypoints, app.api.task.list)\`. This is what makes the alias callable from the
327
- browser. Passing a component to a backend alias is a hard error, not a warning.
327
+ \`bindScreen(protocols.web.taskList, handler(TaskListScreen))\`
328
+ - Client CALL, same file: \`bindAll(protocols.api.task)\`. This is what makes the protocol callable
329
+ from the browser. Passing a component to a backend protocol is a hard error, not a warning.
328
330
 
329
- **A missing client elevation is invisible until the app runs.** The whole shared list is
330
- registered on the browser context, so \`ctx.entrypoint(alias)\` finds the un-elevated declaration
331
- and returns it; the call site casts it to \`ClientEntrypoint\`, so TypeScript sees a type that has
332
- \`call\` and the build is clean. The only symptom is in the browser:
331
+ **A missing client binding is invisible until the app runs.** The whole shared tree is
332
+ registered on the browser context, so \`ctx.entrypoint(protocol)\` finds the unbound declaration
333
+ and returns it; the call site assumes the protocol is bound, so the build can still be clean. The
334
+ only symptom is in the browser:
333
335
 
334
336
  TypeError: entrypoint.call is not a function
335
337
 
336
- That message means EXACTLY one thing — the alias in that \`ctx.entrypoint(...)\` has no
337
- \`elevate(list, <alias>)\` line in \`sources/web/src/entrypoints.ts\`. Add the bare
338
- elevation above the \`// owlmeans: add new backend elevations above this line\` sentinel. It is
338
+ That message means EXACTLY one thing — the protocol in that \`ctx.entrypoint(...)\` has no
339
+ \`bindAll(protocols.api)\` or specific \`bind(protocols.api.<name>)\` line in
340
+ \`sources/web/src/entrypoints.ts\`. Add the binding above the API binding sentinel. It is
339
341
  never a broken component, never a bad import, and never a reason to rewrite the view model or to
340
342
  replace the call with \`fetch\`. If the alias is not declared in
341
- \`sources/common/src/entrypoints.ts\` either, declare it there FIRST: elevating an alias the
342
- shared list does not carry throws \`Entrypoint with alias X not present\` while the module is
343
+ \`sources/common/src/entrypoints.ts\` either, declare it there FIRST: binding a protocol the
344
+ shared tree does not carry throws while the module is
343
345
  still loading, which blanks the whole app instead of failing one call.
344
346
 
345
347
  **4. An AREA is the PARENT entrypoint.** The chrome of the app — header, navigation, footer —
@@ -352,16 +354,16 @@ project, and an area contributes chrome AND a URL prefix:
352
354
  - \`app.web.area.admin\` at \`/admin\` — the owner of the application.
353
355
  - \`app.web.area.operator\` at \`/backoffice\` — staff running the business process; the back office.
354
356
 
355
- Those four declarations and their elevations are FIXED. NEVER add an area, NEVER change one's
356
- path, NEVER touch its \`guard()\`/\`gate()\`, NEVER re-declare one — put the screen in the area
357
+ Those four declarations and their bindings are FIXED. NEVER add an area, NEVER change one's
358
+ path, NEVER touch its \`guards\`/\`gate\` options, NEVER re-declare one — put the screen in the area
357
359
  whose access and audience it needs instead. A product's own roles are not areas: they are users
358
360
  or operators holding different permissions.
359
361
 
360
362
  // sources/common/src/entrypoints.ts — a screen: TAIL path, area as parent, no access
361
- entrypoint(route(app.web.taskList, '/tasks', frontend({ parent: app.web.area.user }))),
363
+ taskList: openProtocol(route(app.web.taskList, '/tasks', frontend({ parent: app.web.area.user }))),
362
364
 
363
365
  // sources/web/src/entrypoints.ts
364
- elevate(list, app.web.taskList, handler(TaskListScreen))
366
+ bindScreen(protocols.web.taskList, handler(TaskListScreen))
365
367
 
366
368
  That screen answers at \`/frontoffice/tasks\`. The path you write is the TAIL ONLY — repeating the
367
369
  area prefix (\`'/frontoffice/tasks'\`) publishes it at \`/frontoffice/frontoffice/tasks\`, which
@@ -372,7 +374,7 @@ The framework passes the matched child to the area as \`children\`, so an area l
372
374
  renders ONLY its own content: importing a layout inside a screen renders the header, the menu
373
375
  and the footer a second time, nested inside the first.
374
376
 
375
- - \`app.web.base\` stays BARE — \`elevate(list, app.web.base)\`, no \`handler()\`. It is the
377
+ - \`app.web.base\` stays BOUND with \`stab\` — \`bindScreen(protocols.web.base, stab)\`, no \`handler()\`. It is the
376
378
  pass-through shell that hosts the four areas and contributes no chrome.
377
379
  - Every area needs a child declared \`frontend({ default: true, parent: app.web.area.<area> })\`.
378
380
  An entrypoint with children but no default child matches nothing and renders a BLANK PAGE.
@@ -381,20 +383,20 @@ and the footer a second time, nested inside the first.
381
383
  **Access is INHERITED from the area.** Guards and gates cascade to children, so choosing the area
382
384
  IS the access decision and a screen declares none of its own:
383
385
 
384
- - guest area — no guard at all; its screens add nothing.
385
- - user area — \`guard(DEFAULT_GUARD)\`; its screens add nothing.
386
- - admin area — \`guard(DEFAULT_GUARD, gate(OIDC_GATE, ['project--admin']))\`; its screens add
386
+ - guest area — no guards option; its screens add nothing.
387
+ - user area — \`{ guards: DEFAULT_GUARD }\`; its screens add nothing.
388
+ - admin area — \`{ guards: DEFAULT_GUARD, gate: { alias: OIDC_GATE, params: ['project--admin'] } }\`; its screens add
387
389
  nothing, the marker is the whole rule.
388
- - operator area — \`guard(DEFAULT_GUARD)\`, and EVERY screen under it additionally declares
389
- \`gate(OIDC_GATE, ['<permission>'])\` naming the permission THAT screen needs, built from the
390
+ - operator area — \`{ guards: DEFAULT_GUARD, gate: { alias: OIDC_GATE, params: ['project--operator'] } }\`, and EVERY screen under it additionally declares
391
+ a \`gate\` option naming the permission THAT screen needs, built from the
390
392
  screen's own resource and action (add \`@<routeParam>\` when the screen shows one specific
391
393
  record). This is the ONLY screen that declares access.
392
394
 
393
- An ENDPOINT has no area to inherit from, so it states its level itself: no \`guard()\` for guest,
394
- \`guard(DEFAULT_GUARD)\` for any signed-in user,
395
- \`guard(DEFAULT_GUARD, gate(OIDC_GATE, ['<permission>']))\` when a permission is required — built
395
+ An ENDPOINT has no area to inherit from, so it states its level itself: no \`guards\` option for guest,
396
+ \`{ guards: DEFAULT_GUARD }\` for any signed-in user,
397
+ \`{ guards: DEFAULT_GUARD, gate: { alias: OIDC_GATE, params: ['<permission>'] } }\` when a permission is required — built
396
398
  from ITS OWN resource and action — and
397
- \`guard(DEFAULT_GUARD, gate(OIDC_GATE, ['project--admin']))\` for owner-only.
399
+ \`{ guards: DEFAULT_GUARD, gate: { alias: OIDC_GATE, params: ['project--admin'] } }\` for owner-only.
398
400
 
399
401
  \`project--admin\` is the OWNER's marker — holding it passes EVERY gate, so the owner is not a
400
402
  role inside the app. It is never one of the permissions this app declares for itself, and it is
@@ -403,17 +405,18 @@ existing guard to make something reachable, and never model a "limited admin" wi
403
405
  that is an ordinary user holding some of the app's own permissions.
404
406
 
405
407
  Insert every new line ABOVE the matching \`// owlmeans: add new ... above this line\` sentinel,
406
- one declaration or elevation per line. Adding an endpoint touches five files (alias,
407
- declaration, handler, server elevation, client elevation); a screen touches five too (alias,
408
- declaration, screen component, client elevation, navigation entry). Skipping one leaves it
408
+ one declaration or binding per line. Adding an endpoint touches five files (alias,
409
+ protocol, handler, server binding, client binding); a screen touches five too (alias,
410
+ protocol, screen component, client binding, navigation entry). Skipping one leaves it
409
411
  unreachable.
410
412
 
411
- **Calling an endpoint** from the frontend — \`call()\` resolves to the VALUE, and a non-2xx THROWS:
413
+ **Calling an endpoint** from the frontend — \`call()\` resolves to the VALUE, and a non-2xx THROWS.
414
+ Use the shared protocol reference; request and response types come from its contract:
412
415
 
413
- const tasks = await owlCtx.entrypoint<ClientEntrypoint<Task[]>>(app.api.task.list).call()
414
- const task = await owlCtx.entrypoint<ClientEntrypoint<Task>>(app.api.task.create)
416
+ const tasks = await owlCtx.entrypoint(protocols.api.task.list).call()
417
+ const task = await owlCtx.entrypoint(protocols.api.task.create)
415
418
  .call({ body: input })
416
- const task = await owlCtx.entrypoint<ClientEntrypoint<Task>>(app.api.task.get)
419
+ const task = await owlCtx.entrypoint(protocols.api.task.get)
417
420
  .call({ params: { taskId } })
418
421
 
419
422
  An entrypoint carries three verbs and each answers a different question:
@@ -426,15 +429,9 @@ An entrypoint carries three verbs and each answers a different question:
426
429
  for a fully qualified one. A SCREEN entrypoint answers only this verb — calling \`call()\` or
427
430
  \`invoke()\` on one THROWS.
428
431
 
429
- \`ClientEntrypoint\` comes from \`@owlmeans/web-client\` and from nowhere else:
430
-
431
- import type { ClientEntrypoint } from '@owlmeans/web-client'
432
-
433
- \`@owlmeans/client\` does NOT export it — importing it from there fails the build with
434
- \`error TS2305: Module '"@owlmeans/client"' has no exported member 'ClientEntrypoint'\`.
435
- And the type argument goes on \`ClientEntrypoint\`, never on \`entrypoint\` itself:
436
- \`owlCtx.entrypoint<Task>(alias)\` fails with \`error TS2739: Type 'Task' is missing the
437
- following properties from type 'BasicEntrypoint'\`.
432
+ Do not import or cast a legacy client-entrypoint type, and do not put a value type argument on
433
+ \`entrypoint\`. The protocol reference is the only source of request and response types; a call
434
+ site that needs a different shape must fix the shared contract instead of overriding it locally.
438
435
 
439
436
  Inside a component or a hook — which is where nearly every call belongs — read the context with
440
437
  \`useContext()\` from \`@owlmeans/web-client\`. \`owlCtx\` is for module-level code that has no
@@ -491,7 +488,7 @@ resolve.
491
488
  a layout. The chrome comes from \`NavLayout\` (\`@owlmeans/web-panel\`), which the four area
492
489
  layouts already render off this registry.
493
490
 
494
- A screen with no line here compiles, elevates and renders — it is simply reachable by direct URL
491
+ A screen with no line here compiles, binds and renders — it is simply reachable by direct URL
495
492
  only, and nothing reports it.
496
493
  `),
497
494
 
@@ -502,7 +499,7 @@ copied out of this document is a gate nobody can ever pass.
502
499
 
503
500
  A permission is ONE string that has to line up in three places:
504
501
 
505
- 1. the GATE on the entrypoint — \`gate(OIDC_GATE, ['enquiry--view@enquiryId'])\`
502
+ 1. the GATE on the protocol — \`gate: { alias: OIDC_GATE, params: ['enquiry--view@enquiryId'] }\`
506
503
  2. the permission DEFINITION registered for the project — name \`enquiry--view\`,
507
504
  resource \`enquiry\`, action \`view\`, resource-scoped \`true\`
508
505
  3. the GRANT an administrator makes, which addresses that definition by name.
@@ -510,9 +507,10 @@ A permission is ONE string that has to line up in three places:
510
507
  **The \`@\` suffix belongs to the GATE ONLY. It is never part of a permission's name.**
511
508
 
512
509
  // right — the gate scopes the check; the definition and the grant use the bare name
513
- entrypoint(
510
+ protocol(
514
511
  route(app.api.enquiry.get, '/:enquiryId', { parent: app.api.enquiry }),
515
- guard(DEFAULT_GUARD, gate(OIDC_GATE, ['enquiry--view@enquiryId']))
512
+ contract(typed<Enquiry>()),
513
+ { guards: DEFAULT_GUARD, gate: { alias: OIDC_GATE, params: ['enquiry--view@enquiryId'] } },
516
514
  )
517
515
  // definition: enquiry--view grant: enquiry--view
518
516
 
@@ -556,16 +554,16 @@ examples; yours is not, so read the right-hand column as a shape and build the n
556
554
  domain.
557
555
 
558
556
  // WRONG — a bracketed word from an instruction, written as if it were a name
559
- gate(OIDC_GATE, ['<permission>'])
560
- gate(OIDC_GATE, ['<resource>--<action>'])
557
+ gate: { alias: OIDC_GATE, params: ['<permission>'] }
558
+ gate: { alias: OIDC_GATE, params: ['<resource>--<action>'] }
561
559
  // RIGHT — this application's own resource and action
562
- gate(OIDC_GATE, ['appointment--modify'])
560
+ gate: { alias: OIDC_GATE, params: ['appointment--modify'] }
563
561
  // Costs: nothing registers a bracketed name, so the gate refuses every request forever.
564
562
 
565
563
  // WRONG — a name copied out of a skill, a comment or another app's example
566
- gate(OIDC_GATE, ['article--modify']) // in an application that has no articles
564
+ gate: { alias: OIDC_GATE, params: ['article--modify'] } // in an application that has no articles
567
565
  // RIGHT — a name that exists in THIS domain
568
- gate(OIDC_GATE, ['appointment--modify'])
566
+ gate: { alias: OIDC_GATE, params: ['appointment--modify'] }
569
567
  // Costs: the permission is asserted but never declared, so no administrator can grant it.
570
568
 
571
569
  // WRONG — the selector carried into the stored name
@@ -576,15 +574,15 @@ domain.
576
574
 
577
575
  // WRONG — a selector naming a param this route does not declare
578
576
  route(app.api.appointment.get, '/:appointmentId')
579
- gate(OIDC_GATE, ['appointment--view@id'])
577
+ gate: { alias: OIDC_GATE, params: ['appointment--view@id'] }
580
578
  // RIGHT — the name after @ is a ":" segment of this very route
581
- gate(OIDC_GATE, ['appointment--view@appointmentId'])
579
+ gate: { alias: OIDC_GATE, params: ['appointment--view@appointmentId'] }
582
580
  // Costs: the id resolves to nothing, so the endpoint refuses every request with nothing logged.
583
581
 
584
582
  // WRONG — one hyphen
585
- gate(OIDC_GATE, ['appointment-modify'])
583
+ gate: { alias: OIDC_GATE, params: ['appointment-modify'] }
586
584
  // RIGHT — two
587
- gate(OIDC_GATE, ['appointment--modify'])
585
+ gate: { alias: OIDC_GATE, params: ['appointment--modify'] }
588
586
  // Costs: it registers as a resource with no action, and never lines up with the real one.
589
587
 
590
588
  **Never spell one permission two ways.** Reuse the exact string an existing declaration already
@@ -1014,7 +1012,7 @@ So a null check around it is dead code that never runs:
1014
1012
  const svc = ctx.service(ALIAS)
1015
1013
  if (!svc) { throw { status: 503 } }
1016
1014
 
1017
- \`ctx.resource(alias)\` and \`ctx.entrypoint(alias)\` throw the same way
1015
+ \`ctx.resource(alias)\` and \`ctx.entrypoint(protocol)\` throw the same way
1018
1016
  (\`Resource X not found\`, \`Entrypoint X not found\`).
1019
1017
 
1020
1018
  **Only when a real lifecycle-owning singleton is needed** — a client holding a connection, a
@@ -1103,28 +1101,37 @@ There is NO express here. \`express\`, \`cors\` and \`@types/express\` are not i
1103
1101
  no \`app.get(...)\`, no \`req\`/\`res\` of a web server, no \`next\`, and no middleware. Anything
1104
1102
  written for express fails to resolve.
1105
1103
 
1106
- A handler is a plain async function that RETURNS its result, wrapped in one of three helpers
1107
- from \`@owlmeans/server-app\`. Pick by what the handler reads:
1104
+ A handler is a plain async function that RETURNS its result, created from the protocol with a
1105
+ typed helper from \`@owlmeans/server-api\`. Pick by what the handler reads:
1108
1106
 
1109
- import { handleBody, handleParams, handleRequest } from '@owlmeans/server-app'
1107
+ import { handlers } from '@owlmeans/server-api'
1108
+ import { bind } from '@owlmeans/server-entrypoint'
1109
+ import { protocols } from 'project-common/entrypoints'
1110
+ const api = handlers<Context>()
1110
1111
 
1111
- // the request BODY — the payload type is the type argument
1112
- export const createTask = handleBody<TaskInput>(async (payload, ctx) => {
1112
+ // the request BODY — the payload type comes from the protocol contract
1113
+ const createTask = api.body(protocols.api.task.create, async (payload, ctx) => {
1113
1114
  const tasks = getTaskResource(ctx)
1114
1115
  return await tasks.create(payload)
1115
1116
  })
1116
1117
 
1117
1118
  // the route PARAMS — names match the ':' segments of the declared path
1118
- export const getTask = handleParams<{ taskId: string }>(async ({ taskId }, ctx) => {
1119
+ const getTask = api.params(protocols.api.task.get, async ({ taskId }, ctx) => {
1119
1120
  return await tasks(ctx).load(taskId)
1120
1121
  })
1121
1122
 
1122
1123
  // anything else (query, headers, nothing at all) — the whole request
1123
- export const listTasks = handleRequest(async (req, ctx) => {
1124
+ const listTasks = api.request(protocols.api.task.list, async (req, ctx) => {
1124
1125
  const query = req.query as { search?: string }
1125
1126
  return await tasks(ctx).list(query.search != null ? { search: query.search } : {})
1126
1127
  })
1127
1128
 
1129
+ export const entrypoints = [
1130
+ bind(protocols.api.task.create, createTask),
1131
+ bind(protocols.api.task.get, getTask),
1132
+ bind(protocols.api.task.list, listTasks),
1133
+ ]
1134
+
1128
1135
  - The SECOND argument is the OwlMeans context. Reach every resource through it — never
1129
1136
  import a database connection and never write raw SQL in a handler.
1130
1137
  - RETURN the value. \`res.json(...)\`, \`res.status(...)\`, \`res.send(...)\` do not exist; a
@@ -1133,9 +1140,9 @@ from \`@owlmeans/server-app\`. Pick by what the handler reads:
1133
1140
  \`ResilientError\` subclass is mapped to its status by the framework; catching it produces a
1134
1141
  200 carrying an error object instead.
1135
1142
  - Never read a token, never check a role, never look at an \`Authorization\` header. Access is
1136
- declared on the entrypoint (\`guard()\` / \`gate()\`) and enforced before the handler runs.
1137
- - A handler is inert until an \`elevate(appEntrypoints, alias, handler)\` line in
1138
- \`sources/api/src/entrypoints.ts\` binds it to its alias. Without that line the endpoint
1143
+ declared on the protocol (\`guards\` / \`gate\` options) and enforced before the handler runs.
1144
+ - A handler is inert until a \`bind(protocols.api.<name>, handler)\` line in
1145
+ \`sources/api/src/entrypoints.ts\` binds it to its protocol. Without that line the endpoint
1139
1146
  answers 404 and nothing reports an error.
1140
1147
  - Handlers are ENTITY-SCOPED: \`sources/api/src/app/<entity>/<action>.ts\`, named exports only.
1141
1148
  The directory is what keeps two entities' \`list\` apart — the file name carries no marker.
@@ -1196,15 +1203,15 @@ says before rewriting anything.
1196
1203
  \`@reduxjs/toolkit\`, \`react-redux\`, \`@/state/store\` or \`@/lib/fetch\` means the file was
1197
1204
  written against the REMOVED stack. None of those packages are installed and none will be.
1198
1205
  Rewrite the file against entrypoints: \`useNavigate\` from \`@owlmeans/client\` for navigation,
1199
- \`ctx.entrypoint(alias).call(...)\` for a backend call, a
1200
- \`handleRequest\`/\`handleBody\`/\`handleParams\` function for an endpoint, and the state hooks
1206
+ \`ctx.entrypoint(protocol).call(...)\` for a backend call, a typed
1207
+ \`handlers<Context>()\` callback bound to its protocol for an endpoint, and the state hooks
1201
1208
  (\`useStoreModel\`/\`useStoreList\` over a state resource) for client state. Do NOT install the
1202
1209
  package and do NOT create the missing module.
1203
1210
  - A missing export from a \`*.ts\` module — a selector, an action creator, a thunk, a
1204
1211
  reducer — is the same removed stack in a different disguise. That module exports an ALIAS
1205
1212
  constant and read hooks only. Replace a selector with the matching hook, a dispatched action
1206
1213
  with \`resource.save(record)\` / \`model.update({ ... })\`, and a thunk with an
1207
- \`entrypoint(alias).call(...)\` in the VIEW MODEL followed by a \`save\`. Do not add the missing
1214
+ \`entrypoint(protocol).call(...)\` in the VIEW MODEL followed by a \`save\`. Do not add the missing
1208
1215
  export to the state module.
1209
1216
  - A runtime \`Resource <alias> not found\` for a state alias means the state module is written but
1210
1217
  nothing registered it. Add the import and ONE
@@ -1222,21 +1229,20 @@ says before rewriting anything.
1222
1229
  that changes which records match, so the \`useValue(() => resource.list())\` that fed it a list
1223
1230
  of ids goes away entirely rather than being repaired.
1224
1231
  - A runtime \`TypeError: entrypoint.call is not a function\` (or \`<name>.call is not a function\`
1225
- on the result of \`ctx.entrypoint(...)\`) is a MISSING CLIENT ELEVATION and nothing else. The
1226
- alias resolves — the browser context carries every shared declaration — but only an elevated
1227
- one has \`call\`. Add the bare \`elevate(list, <the alias in that call>)\` above the
1228
- \`// owlmeans: add new backend elevations above this line\` sentinel in
1232
+ on the result of \`ctx.entrypoint(...)\`) is a MISSING CLIENT BINDING and nothing else. The
1233
+ protocol resolves — the browser context carries every shared declaration — but only a bound
1234
+ one has \`call\`. Add \`bindAll(protocols.api)\` (or a specific \`bind(protocols.api.<name>)\`) above the
1235
+ API binding sentinel in
1229
1236
  \`sources/web/src/entrypoints.ts\`. Do NOT rewrite the view model, do NOT replace the
1230
- call with \`fetch\`, and do NOT add a component to a backend alias. The build was clean
1231
- because the call site casts to \`ClientEntrypoint\`, so \`tsc\` will not confirm the fix
1232
- the elevation line is the fix.
1237
+ call with \`fetch\`, and do NOT add a component to a backend alias. The binding line is the fix;
1238
+ the declaration itself cannot provide \`call\` until a client binding materializes it.
1233
1239
  - A screen that renders blank is the same class of fault on the other side: the screen alias has
1234
- no \`elevate(list, alias, handler(Screen))\` line. Adding a declaration without its elevation
1240
+ no \`bindScreen(protocols.web.<name>, handler(Screen))\` line. Adding a protocol without its binding
1235
1241
  is the usual cause of both.
1236
1242
  - A header, menu or footer rendered TWICE is a screen importing its own layout. The AREA is the
1237
1243
  screen's parent entrypoint and the framework already wraps it — delete the import and the
1238
1244
  wrapper element from the screen, never the area from the entrypoint tree.
1239
- - An error naming an AREA — \`app.web.area.<area>\`, its \`route(...)\`, its \`guard()\`/\`gate()\`,
1245
+ - An error naming an AREA — \`app.web.area.<area>\`, its \`route(...)\`, its \`guards\`/\`gate\` options,
1240
1246
  or one of the four layouts in \`sources/web/src/layout/area.tsx\` — means generated code
1241
1247
  EDITED something that ships with the project. RESTORE the shipped form: four areas under
1242
1248
  \`app.web.base\` at \`/\`, \`/frontoffice\`, \`/admin\` and \`/backoffice\`, each with its own guard
@@ -1245,14 +1251,14 @@ says before rewriting anything.
1245
1251
  access belongs in a different area.
1246
1252
  - A user who WAS granted a permission and still gets 403 is almost always an \`@\` in the stored
1247
1253
  permission NAME. The gate splits its parameter at the first \`@\`: \`'enquiry--view@enquiryId'\`
1248
- in \`gate(OIDC_GATE, [...])\` is CORRECT and means "look up \`enquiry--view\`, read the resource
1254
+ in a protocol's \`gate: { alias: OIDC_GATE, params: [...] }\` is CORRECT and means "look up \`enquiry--view\`, read the resource
1249
1255
  id from the \`:enquiryId\` route param". The same string registered or granted as a permission
1250
1256
  NAME is a key nothing ever looks up, so every grant against it is a silent no-op. Fix the
1251
1257
  registration and the grant, never the gate line — and never delete the \`@\` from the gate to
1252
1258
  "make the names match". A gate whose \`@name\` is not a \`:\` segment of that entrypoint's own
1253
1259
  declared path is the other half of the same fault: it refuses every request.
1254
1260
  - A PERMISSION that no definition backs is a gate nobody can pass, and there are two ways generated
1255
- code gets one. A bracketed word — \`gate(OIDC_GATE, ['<permission>'])\` — is an instruction's
1261
+ code gets one. A bracketed word — \`gate: { alias: OIDC_GATE, params: ['<permission>'] }\` — is an instruction's
1256
1262
  placeholder written out as if it were a name. A foreign name — \`article--modify\` in an
1257
1263
  application that has no articles — is an example copied from a comment or a skill. Both look
1258
1264
  perfectly valid to the compiler and to the boot check, so nothing reports them; the app simply
@@ -1380,12 +1386,12 @@ export const queues: QueueDeclaration[] = [
1380
1386
 
1381
1387
  ## 2. The alias and the entrypoint — \`sources/common/src\`
1382
1388
  The job's name IS its entrypoint alias. Declare \`app.job.<name>\` in \`consts.ts\` above the
1383
- sentinel, and the entrypoint in \`entrypoints.ts\` with \`job()\` from \`@owlmeans/route\`.
1389
+ sentinel, and the protocol in \`entrypoints.ts\` with \`job()\` from \`@owlmeans/route\`.
1384
1390
 
1385
1391
  ## 3. The processor — \`sources/worker/src/jobs/<name>.ts\`
1386
- A plain async function wrapped in \`handleRequest\` / \`handleBody<T>\` / \`handleParams<T>\`,
1387
- exactly like an endpoint handler. It RETURNS its result; throwing a \`ResilientError\` subclass
1388
- is how a refusal is reported, and the class survives the broker.
1392
+ A plain async function created with \`handlers<Context>()\` and bound to the job protocol, exactly
1393
+ like an endpoint handler. It RETURNS its result; throwing a \`ResilientError\` subclass is how a
1394
+ refusal is reported, and the class survives the broker.
1389
1395
 
1390
1396
  Two rules with no equivalent on the HTTP side:
1391
1397
  - **Call \`job.touch()\` inside every long loop.** The broker judges liveness by the lock, and
@@ -1394,8 +1400,8 @@ Two rules with no equivalent on the HTTP side:
1394
1400
  - **A processor must be safe to run twice.** Skip what a previous attempt recorded, or delete
1395
1401
  what it created, and say in a comment which of the two this one does.
1396
1402
 
1397
- Elevate it in \`sources/worker/src/entrypoints.ts\` above the sentinel. Enqueue from an endpoint
1398
- with \`context.jobs().create({ name: app.job.<name>, data })\`.
1403
+ Bind it in \`sources/worker/src/entrypoints.ts\` above the sentinel. Enqueue from an endpoint
1404
+ with the same typed call used for HTTP: \`context.entrypoint(appEntrypoints.job.<name>).call({ body: data })\`.
1399
1405
  `),
1400
1406
 
1401
1407
  skill(ViableSkill.TargetAgents, 'LLM agents inside the application', `
@@ -41,14 +41,6 @@ export enum SlotCommandType {
41
41
 
42
42
  export enum SlotGitCommand {
43
43
  Ensure = 'ensure',
44
- /**
45
- * Bring a remote repository's tree onto a volume that already exists.
46
- *
47
- * NOT `git clone`: a slot's directory is never empty — provisioning leaves its own metadata
48
- * there — and `git clone` refuses a non-empty destination. The executor therefore initializes,
49
- * adds the remote, fetches at a bounded depth and checks the branch out over the tree.
50
- */
51
- Clone = 'clone',
52
44
  Status = 'status',
53
45
  Commit = 'commit',
54
46
  Log = 'log',
@@ -74,34 +66,6 @@ export enum SlotFileCommand {
74
66
  DeleteFile = 'deleteFile',
75
67
  FindFilesWithEnvVars = 'findFilesWithEnvVars',
76
68
  GetRootPath = 'getRootPath',
77
- /**
78
- * List a tree with a size and a binary flag per entry, bounded by a caller-given limit.
79
- *
80
- * The reason it exists rather than being composed out of {@link SlotFileCommand.GetSourceList}
81
- * plus a read per file: a foreign repository can hold a hundred thousand files, and one round
82
- * trip each — over a signed HTTP call to a pod, or over a connector on somebody's laptop — is
83
- * not a slower version of the same thing, it is a walk that never finishes.
84
- */
85
- StatTree = 'statTree',
86
- /**
87
- * Read the first N bytes of one file.
88
- *
89
- * A census classifies a file from its head; reading whole files to do it would hold a 1 MB
90
- * export in memory to look at its first line. The answer is text, so a binary head comes back
91
- * as whatever decoding produced — which is exactly the signal the entropy classification wants.
92
- */
93
- ReadHead = 'readHead',
94
- /**
95
- * Move everything in the project root into a subdirectory, keeping a named few in place.
96
- *
97
- * One command rather than a listing plus a move per file, because it must be ATOMIC from the
98
- * caller's point of view: a relocation interrupted half way leaves a tree that is neither the
99
- * origin nor a target, and nothing downstream can tell which files already moved. Binary-safe —
100
- * it moves paths and never reads contents.
101
- */
102
- Relocate = 'relocate',
103
- /** Delete a directory and everything under it. The purge of a relocated origin. */
104
- RemoveTree = 'removeTree',
105
69
  }
106
70
 
107
71
  export enum SlotShellCommand {
@@ -264,37 +228,6 @@ export const COMMAND_DEADLINES: Record<string, number> = {
264
228
 
265
229
  export const DEFAULT_COMMAND_DEADLINE = 60_000
266
230
 
267
- /**
268
- * EXECUTOR-side ceilings for the git commands whose real duration is not a git command's.
269
- *
270
- * Everything git does inside a slot is local and takes seconds — except a clone, which is a
271
- * network fetch of somebody else's repository and is measured in minutes. Bounding it by the
272
- * generic git deadline made a conversion's very first step fail on any repository large enough
273
- * to be worth converting.
274
- */
275
- export const GIT_COMMAND_DEADLINES: Partial<Record<SlotGitCommand, number>> = {
276
- [SlotGitCommand.Clone]: 600_000,
277
- }
278
-
279
- /** What every other git command gets. The value the type-level branch used to hard-code. */
280
- export const DEFAULT_GIT_COMMAND_DEADLINE = 60_000
281
-
282
- /**
283
- * EXECUTOR-side ceilings for the file commands that walk or move a whole tree.
284
- *
285
- * The other file commands are one path each and finish in milliseconds; these three are bounded
286
- * by how big the tree is, which for an origin project is set by whoever wrote it.
287
- */
288
- export const FILE_COMMAND_DEADLINES: Partial<Record<SlotFileCommand, number>> = {
289
- [SlotFileCommand.StatTree]: 120_000,
290
- [SlotFileCommand.Relocate]: 300_000,
291
- [SlotFileCommand.RemoveTree]: 120_000,
292
- [SlotFileCommand.ReadHead]: 30_000,
293
- }
294
-
295
- /** What every other file command gets. The value the type-level branch used to hard-code. */
296
- export const DEFAULT_FILE_COMMAND_DEADLINE = 30_000
297
-
298
231
  /**
299
232
  * CALLER-side ceilings per command type: how long the asker waits for an answer.
300
233
  *
@@ -333,33 +266,6 @@ export const SHELL_COMMAND_TIMEOUTS: Record<string, number> = {
333
266
  [SlotShellCommand.Integrity]: 25_000, // executor deadline (15s) + margin
334
267
  }
335
268
 
336
- /**
337
- * Per-COMMAND caller ceilings for git and for files.
338
- *
339
- * Each is its executor deadline plus the same 40 s margin: the caller's bound must outlast the
340
- * executor's, or a command that failed cleanly inside its own deadline reaches the caller as a
341
- * timeout and the real reason — the one the executor took the trouble to produce — is lost.
342
- */
343
- export const GIT_COMMAND_TIMEOUTS: Partial<Record<SlotGitCommand, number>> = {
344
- [SlotGitCommand.Clone]: 640_000,
345
- }
346
-
347
- export const FILE_COMMAND_TIMEOUTS: Partial<Record<SlotFileCommand, number>> = {
348
- [SlotFileCommand.StatTree]: 160_000,
349
- [SlotFileCommand.Relocate]: 340_000,
350
- [SlotFileCommand.RemoveTree]: 160_000,
351
- [SlotFileCommand.ReadHead]: 70_000,
352
- }
353
-
354
- /** The per-command caller bound for one command, or nothing where its type's default is right. */
355
- const perCommandTimeout = (type: SlotCommandType, command: string): number | undefined => {
356
- switch (type) {
357
- case SlotCommandType.Shell: return SHELL_COMMAND_TIMEOUTS[command]
358
- case SlotCommandType.Git: return GIT_COMMAND_TIMEOUTS[command as SlotGitCommand]
359
- default: return FILE_COMMAND_TIMEOUTS[command as SlotFileCommand]
360
- }
361
- }
362
-
363
269
  /**
364
270
  * Resolve the caller-side bound for one command.
365
271
  *
@@ -370,24 +276,12 @@ const perCommandTimeout = (type: SlotCommandType, command: string): number | und
370
276
  export const commandTimeout = (
371
277
  type: SlotCommandType, command: string, override?: number
372
278
  ): number => override
373
- ?? perCommandTimeout(type, command)
279
+ ?? (type === SlotCommandType.Shell ? SHELL_COMMAND_TIMEOUTS[command] : undefined)
374
280
  ?? COMMAND_TIMEOUTS[type]
375
281
  ?? DEFAULT_COMMAND_TIMEOUT
376
282
 
377
- /**
378
- * Resolve the executor-side deadline for one command.
379
- *
380
- * Per-command first, per-type second. It was per-type only, which is how a clone inherited the
381
- * bound of a `git status` — the same shape as the shell table above, and added for the same
382
- * reason.
383
- */
384
- export const commandDeadline = (type: SlotCommandType, command: string): number => {
385
- switch (type) {
386
- case SlotCommandType.Shell:
387
- return COMMAND_DEADLINES[command] ?? DEFAULT_COMMAND_DEADLINE
388
- case SlotCommandType.Git:
389
- return GIT_COMMAND_DEADLINES[command as SlotGitCommand] ?? DEFAULT_GIT_COMMAND_DEADLINE
390
- default:
391
- return FILE_COMMAND_DEADLINES[command as SlotFileCommand] ?? DEFAULT_FILE_COMMAND_DEADLINE
392
- }
393
- }
283
+ /** Resolve the executor-side deadline for one command. */
284
+ export const commandDeadline = (type: SlotCommandType, command: string): number =>
285
+ type === SlotCommandType.Shell
286
+ ? COMMAND_DEADLINES[command] ?? DEFAULT_COMMAND_DEADLINE
287
+ : type === SlotCommandType.Git ? 60_000 : 30_000
@@ -1,6 +1,6 @@
1
1
  import type { JSONSchemaType } from 'ajv'
2
2
  import { SlotCommandType, SlotFileCommand, SlotGitCommand, SlotShellCommand } from './consts.js'
3
- import type { SlotCommandPayload, SlotGitCloneArgs } from './types.js'
3
+ import type { SlotCommandPayload } from './types.js'
4
4
 
5
5
  export const SlotCommandPayloadSchema = {
6
6
  type: 'object',
@@ -25,22 +25,3 @@ export const SlotCommandPayloadSchema = {
25
25
  required: ['type', 'command'],
26
26
  additionalProperties: false
27
27
  } as JSONSchemaType<SlotCommandPayload>
28
-
29
- /**
30
- * The clone command's arguments, as a signed body carries them.
31
- *
32
- * `token` is declared because it is SENT — a body field the schema does not declare is stripped
33
- * by validation and then verified against a signature the sender made over the whole thing, which
34
- * the guard reports as a 401 rather than as the validation gap it is.
35
- */
36
- export const SlotGitCloneArgsSchema = {
37
- type: 'object',
38
- properties: {
39
- remoteUrl: { type: 'string', minLength: 1, maxLength: 2048 },
40
- branch: { type: 'string', maxLength: 256, nullable: true },
41
- depth: { type: 'number', minimum: 0, nullable: true },
42
- token: { type: 'string', maxLength: 4096, nullable: true },
43
- },
44
- required: ['remoteUrl'],
45
- additionalProperties: false,
46
- } as unknown as JSONSchemaType<SlotGitCloneArgs>