@owlmeans/viable-common 0.0.12 → 0.0.14

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 (102) hide show
  1. package/README.md +68 -1
  2. package/agent-meta/manifest.json +23 -0
  3. package/agent-meta/skills/agent-presentation/SKILL.md +62 -0
  4. package/agent-meta/skills/viable-common/SKILL.md +180 -0
  5. package/build/agent/index.d.ts +1 -0
  6. package/build/agent/index.d.ts.map +1 -1
  7. package/build/agent/index.js +1 -0
  8. package/build/agent/index.js.map +1 -1
  9. package/build/agent/presentation.d.ts +102 -0
  10. package/build/agent/presentation.d.ts.map +1 -0
  11. package/build/agent/presentation.js +303 -0
  12. package/build/agent/presentation.js.map +1 -0
  13. package/build/blueprint/case.js +3 -3
  14. package/build/connect/consts.d.ts +93 -5
  15. package/build/connect/consts.d.ts.map +1 -1
  16. package/build/connect/consts.js +90 -1
  17. package/build/connect/consts.js.map +1 -1
  18. package/build/connect/entrypoints.d.ts +208 -4
  19. package/build/connect/entrypoints.d.ts.map +1 -1
  20. package/build/connect/entrypoints.js +61 -33
  21. package/build/connect/entrypoints.js.map +1 -1
  22. package/build/connect/errors.d.ts +20 -0
  23. package/build/connect/errors.d.ts.map +1 -1
  24. package/build/connect/errors.js +39 -0
  25. package/build/connect/errors.js.map +1 -1
  26. package/build/connect/ops.d.ts +46 -2
  27. package/build/connect/ops.d.ts.map +1 -1
  28. package/build/connect/references.d.ts +60 -1
  29. package/build/connect/references.d.ts.map +1 -1
  30. package/build/connect/references.js +16 -0
  31. package/build/connect/references.js.map +1 -1
  32. package/build/connect/schemas.d.ts +17 -2
  33. package/build/connect/schemas.d.ts.map +1 -1
  34. package/build/connect/schemas.js +91 -7
  35. package/build/connect/schemas.js.map +1 -1
  36. package/build/connect/types.d.ts +125 -1
  37. package/build/connect/types.d.ts.map +1 -1
  38. package/build/convert/consts.d.ts +539 -0
  39. package/build/convert/consts.d.ts.map +1 -0
  40. package/build/convert/consts.js +630 -0
  41. package/build/convert/consts.js.map +1 -0
  42. package/build/convert/helpers.d.ts +91 -0
  43. package/build/convert/helpers.d.ts.map +1 -0
  44. package/build/convert/helpers.js +301 -0
  45. package/build/convert/helpers.js.map +1 -0
  46. package/build/convert/index.d.ts +11 -0
  47. package/build/convert/index.d.ts.map +1 -0
  48. package/build/convert/index.js +4 -0
  49. package/build/convert/index.js.map +1 -0
  50. package/build/convert/schemas.d.ts +62 -0
  51. package/build/convert/schemas.d.ts.map +1 -0
  52. package/build/convert/schemas.js +626 -0
  53. package/build/convert/schemas.js.map +1 -0
  54. package/build/convert/types.d.ts +683 -0
  55. package/build/convert/types.d.ts.map +1 -0
  56. package/build/convert/types.js +2 -0
  57. package/build/convert/types.js.map +1 -0
  58. package/build/index.d.ts +1 -0
  59. package/build/index.d.ts.map +1 -1
  60. package/build/index.js +1 -0
  61. package/build/index.js.map +1 -1
  62. package/build/skills/catalogue.d.ts.map +1 -1
  63. package/build/skills/catalogue.js +76 -34
  64. package/build/skills/catalogue.js.map +1 -1
  65. package/build/slot/consts.d.ts +73 -2
  66. package/build/slot/consts.d.ts.map +1 -1
  67. package/build/slot/consts.js +105 -5
  68. package/build/slot/consts.js.map +1 -1
  69. package/build/slot/schemas.d.ts +9 -1
  70. package/build/slot/schemas.d.ts.map +1 -1
  71. package/build/slot/schemas.js +18 -0
  72. package/build/slot/schemas.js.map +1 -1
  73. package/build/slot/types.d.ts +71 -0
  74. package/build/slot/types.d.ts.map +1 -1
  75. package/package.json +15 -7
  76. package/src/agent/index.ts +1 -0
  77. package/src/agent/presentation.ts +335 -0
  78. package/src/blueprint/case.ts +3 -3
  79. package/src/connect/consts.ts +93 -1
  80. package/src/connect/entrypoints.ts +113 -40
  81. package/src/connect/errors.ts +44 -0
  82. package/src/connect/ops.ts +50 -2
  83. package/src/connect/references.ts +53 -5
  84. package/src/connect/schemas.ts +102 -13
  85. package/src/connect/types.ts +130 -2
  86. package/src/convert/consts.ts +672 -0
  87. package/src/convert/helpers.ts +340 -0
  88. package/src/convert/index.ts +10 -0
  89. package/src/convert/schemas.ts +663 -0
  90. package/src/convert/types.ts +741 -0
  91. package/src/index.ts +1 -0
  92. package/src/skills/catalogue.ts +76 -34
  93. package/src/slot/consts.ts +112 -6
  94. package/src/slot/schemas.ts +20 -1
  95. package/src/slot/types.ts +80 -0
  96. package/tests/connect-convert.spec.ts +162 -0
  97. package/tests/connect-entrypoints.spec.ts +12 -4
  98. package/tests/connect-errors.spec.ts +22 -0
  99. package/tests/convert.spec.ts +318 -0
  100. package/tests/presentation.spec.ts +85 -0
  101. package/tests/skills.spec.ts +47 -0
  102. /package/tests/{design.test.ts → design.spec.ts} +0 -0
package/src/index.ts CHANGED
@@ -16,6 +16,7 @@ export * from './topology/index.js'
16
16
  export * from './blueprint/index.js'
17
17
  export * from './slot/index.js'
18
18
  export * from './connect/index.js'
19
+ export * from './convert/index.js'
19
20
  export * from './scaffold/index.js'
20
21
  export * from './design/index.js'
21
22
  export * from './branding.js'
@@ -108,7 +108,8 @@ 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
110
  - \`sources/common/src/entrypoints.ts\` — the shared immutable protocol tree (\`appEntrypoints\`).
111
- - \`sources/api/src/entrypoints.ts\` — server bindings (\`bind\` + \`handlers<Context>()\`).
111
+ - \`sources/api/src/entrypoints.ts\` — server bindings: \`bind\` + one \`handlers<Context>()\` wrap
112
+ per handler.
112
113
  - \`sources/web/src/entrypoints.ts\` — client bindings (\`bindAll\` + \`bindScreen\`).
113
114
  - \`sources/worker/src/entrypoints.ts\` — job bindings (\`bind\`).
114
115
  - \`sources/web/src/nav.ts\` — the navigation registry: one line per screen, which
@@ -702,7 +703,7 @@ It declares the alias and the hooks that READ it, and nothing else:
702
703
  import { useStoreList, useStoreModel } from '@owlmeans/client'
703
704
  import { useContext } from '@owlmeans/web-client'
704
705
  import type { Criteria } from '@owlmeans/resource'
705
- import type { Task } from 'project-common/models/task/task.type.js'
706
+ import type { Task } from 'project-common/models/task/task'
706
707
 
707
708
  export const TASK_STATE = 'task-state'
708
709
 
@@ -982,7 +983,7 @@ Write this instead, and let the route handler import the function directly:
982
983
  export const getSummary = async (ctx: BasicContext<BasicConfig>, guestId: string) => { ... }
983
984
 
984
985
  // RIGHT — sources/api/src/app/dashboard/activity.ts
985
- import { getSummary } from 'project-backend/models/dashboard/activity.js'
986
+ import { getSummary } from 'project-backend/models/dashboard/activity'
986
987
  const summary = await getSummary(ctx, guestId)
987
988
 
988
989
  Do not create a service just to hold functions.
@@ -1101,37 +1102,43 @@ There is NO express here. \`express\`, \`cors\` and \`@types/express\` are not i
1101
1102
  no \`app.get(...)\`, no \`req\`/\`res\` of a web server, no \`next\`, and no middleware. Anything
1102
1103
  written for express fails to resolve.
1103
1104
 
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:
1105
+ A handler is a PLAIN exported async function that RETURNS its result — named exports only, one
1106
+ file per entity: \`sources/api/src/app/<entity>/<action>.ts\`. It NEVER imports \`handlers\` and
1107
+ NEVER calls \`handlers<Context>()\`; that belongs only in the file that BINDS it:
1108
+
1109
+ // sources/api/src/app/task/create.ts — the handler module
1110
+ export const handleTaskCreate = async (payload: TaskCreatePayload, ctx: Context) => {
1111
+ const tasks = getTaskResource(ctx)
1112
+ return await tasks.create(payload)
1113
+ }
1114
+
1115
+ sources/api/src/entrypoints.ts wraps that export EXACTLY ONCE, choosing the helper by what the
1116
+ protocol carries — the request BODY → \`api.body\`, route PARAMS (the ':' segments of the declared
1117
+ path) → \`api.params\`, anything else (query, headers, nothing at all) → \`api.request\`:
1106
1118
 
1107
1119
  import { handlers } from '@owlmeans/server-api'
1108
1120
  import { bind } from '@owlmeans/server-entrypoint'
1109
1121
  import { protocols } from 'project-common/entrypoints'
1122
+ import { handleTaskCreate } from '@/app/task/create.js'
1110
1123
  const api = handlers<Context>()
1111
1124
 
1112
- // the request BODY — the payload type comes from the protocol contract
1113
- const createTask = api.body(protocols.api.task.create, async (payload, ctx) => {
1114
- const tasks = getTaskResource(ctx)
1115
- return await tasks.create(payload)
1116
- })
1125
+ export const entrypoints = [
1126
+ bind(protocols.api.task.create, api.body(protocols.api.task.create, handleTaskCreate)),
1127
+ ]
1117
1128
 
1118
- // the route PARAMS — names match the ':' segments of the declared path
1119
- const getTask = api.params(protocols.api.task.get, async ({ taskId }, ctx) => {
1120
- return await tasks(ctx).load(taskId)
1121
- })
1129
+ WRONG — the handler module ALSO calls \`handlers()\` and wraps itself, so the export bound above is
1130
+ wrapped a SECOND time. \`tsc\` catches it: \`TS2345 "Argument of type 'BoundEntrypointHandler<…>' is
1131
+ not assignable"\`. Left unfixed, the build stays clean anyway if nothing runs \`tsc\` on this
1132
+ package, and every request to the route then fails with \`TypeError: handler is not a function\`
1133
+ (\`execute is not a function\` for \`.request\`) — an endpoint that "completed" and answers nothing:
1122
1134
 
1123
- // anything else (query, headers, nothing at all) — the whole request
1124
- const listTasks = api.request(protocols.api.task.list, async (req, ctx) => {
1125
- const query = req.query as { search?: string }
1126
- return await tasks(ctx).list(query.search != null ? { search: query.search } : {})
1135
+ // WRONG — sources/api/src/app/task/create.ts
1136
+ const api = handlers<Context>()
1137
+ export const handleTaskCreate = api.body(protocols.api.task.create, async (payload, ctx) => {
1138
+ const tasks = getTaskResource(ctx)
1139
+ return await tasks.create(payload)
1127
1140
  })
1128
1141
 
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
-
1135
1142
  - The SECOND argument is the OwlMeans context. Reach every resource through it — never
1136
1143
  import a database connection and never write raw SQL in a handler.
1137
1144
  - RETURN the value. \`res.json(...)\`, \`res.status(...)\`, \`res.send(...)\` do not exist; a
@@ -1141,9 +1148,9 @@ typed helper from \`@owlmeans/server-api\`. Pick by what the handler reads:
1141
1148
  200 carrying an error object instead.
1142
1149
  - Never read a token, never check a role, never look at an \`Authorization\` header. Access is
1143
1150
  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
1146
- answers 404 and nothing reports an error.
1151
+ - A handler is inert until the \`bind(protocols.api.<name>, api.body/params/request(protocols.api.<name>, handler))\`
1152
+ line in \`sources/api/src/entrypoints.ts\` binds it to its protocol. Without that line the
1153
+ endpoint answers 404 and nothing reports an error.
1147
1154
  - Handlers are ENTITY-SCOPED: \`sources/api/src/app/<entity>/<action>.ts\`, named exports only.
1148
1155
  The directory is what keeps two entities' \`list\` apart — the file name carries no marker.
1149
1156
  `),
@@ -1169,6 +1176,21 @@ says before rewriting anything.
1169
1176
  its \`.js\` suffix — \`'../../resources/task/task.js'\`. Adding, removing or changing the
1170
1177
  extension on the alias fixes nothing and the same error comes back under the other spelling;
1171
1178
  two attempts that differ only in the suffix mean you are in this case.
1179
+ - **"Cannot find module 'project-common/…' or 'project-backend/…'" is almost always the
1180
+ extension, not the path.** These two are PACKAGES, resolved through their published
1181
+ \`exports\` map, and a deep import into either carries NO suffix — no \`.js\`, no \`.ts\`, no
1182
+ \`.type\`. \`project-backend/models/enquiry/enquiry.js\` resolves to a file
1183
+ (\`build/models/enquiry/enquiry.js.js\`) that can never exist; drop the extension entirely.
1184
+ This is unrelated to the \`sources/backend/\` bullet above it: that one is the SAME package
1185
+ importing itself and keeps \`.js\`; this one is a DIFFERENT package importing it by name.
1186
+ - **\`TS2345\` naming \`BoundEntrypointHandler\` at a \`bind(...)\` line in
1187
+ \`sources/api/src/entrypoints.ts\` or \`sources/worker/src/entrypoints.ts\` is a handler wrapped
1188
+ TWICE** — the handler module already exported a bound handler, and this line wraps it again
1189
+ with \`.body\`/\`.params\`/\`.request\`. Bind the already-bound export DIRECTLY
1190
+ (\`bind(protocol, handler)\`, no wrapper) — never cast around the type error, and never touch the
1191
+ handler module. Left as a type error, this is also the runtime fingerprint of the same defect:
1192
+ a story reported \`completed\` whose every request to that route fails with
1193
+ \`TypeError: handler is not a function\` (\`execute is not a function\` for \`.request\`).
1172
1194
  - "Cannot find module" for \`postgres\`, \`@/lib/db.js\` or \`@/db/schema/...\` inside a
1173
1195
  \`resources/**\` file means the file was written against the REMOVED Drizzle-DDL layout.
1174
1196
  Rewrite it as an OwlMeans resource: an AJV schema on \`resource.schema\`, built with
@@ -1203,8 +1225,8 @@ says before rewriting anything.
1203
1225
  \`@reduxjs/toolkit\`, \`react-redux\`, \`@/state/store\` or \`@/lib/fetch\` means the file was
1204
1226
  written against the REMOVED stack. None of those packages are installed and none will be.
1205
1227
  Rewrite the file against entrypoints: \`useNavigate\` from \`@owlmeans/client\` for navigation,
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
1228
+ \`ctx.entrypoint(protocol).call(...)\` for a backend call, a plain exported handler wrapped ONCE
1229
+ where it is bound for an endpoint (see "Writing an endpoint handler"), and the state hooks
1208
1230
  (\`useStoreModel\`/\`useStoreList\` over a state resource) for client state. Do NOT install the
1209
1231
  package and do NOT create the missing module.
1210
1232
  - A missing export from a \`*.ts\` module — a selector, an action creator, a thunk, a
@@ -1389,9 +1411,29 @@ The job's name IS its entrypoint alias. Declare \`app.job.<name>\` in \`consts.t
1389
1411
  sentinel, and the protocol in \`entrypoints.ts\` with \`job()\` from \`@owlmeans/route\`.
1390
1412
 
1391
1413
  ## 3. The processor — \`sources/worker/src/jobs/<name>.ts\`
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.
1414
+ A PLAIN exported async function — named exports only, exactly like an endpoint handler. It never
1415
+ imports \`handlers\` and never calls \`handlers<Context>()\`; that belongs only in
1416
+ \`sources/worker/src/entrypoints.ts\`, which wraps it EXACTLY ONCE when binding it:
1417
+
1418
+ // sources/worker/src/jobs/report-build.ts — the processor module
1419
+ export const handleReportBuildJob = async (req: ReportBuildRequest, ctx: Context) => { ... }
1420
+
1421
+ // sources/worker/src/entrypoints.ts — the ONLY wrap
1422
+ import * as jobs from '@/jobs/index.js'
1423
+ const worker = handlers<Context>()
1424
+ bind(protocols.job.reportBuild, worker.request(protocols.job.reportBuild, jobs.handleReportBuildJob))
1425
+
1426
+ WRONG — the processor module ALSO calls \`handlers()\` and wraps itself, so the export bound above
1427
+ is wrapped a SECOND time. \`tsc\` catches it (\`TS2345 "BoundEntrypointHandler<…> is not
1428
+ assignable"\`); left unfixed, every enqueued job then fails with \`TypeError: handler is not a
1429
+ function\`:
1430
+
1431
+ // WRONG — sources/worker/src/jobs/report-build.ts
1432
+ const worker = handlers<Context>()
1433
+ export const handleReportBuildJob = worker.request(protocols.job.reportBuild, async (req, ctx) => { ... })
1434
+
1435
+ It RETURNS its result; throwing a \`ResilientError\` subclass is how a refusal is reported, and the
1436
+ class survives the broker.
1395
1437
 
1396
1438
  Two rules with no equivalent on the HTTP side:
1397
1439
  - **Call \`job.touch()\` inside every long loop.** The broker judges liveness by the lock, and
@@ -1400,8 +1442,8 @@ Two rules with no equivalent on the HTTP side:
1400
1442
  - **A processor must be safe to run twice.** Skip what a previous attempt recorded, or delete
1401
1443
  what it created, and say in a comment which of the two this one does.
1402
1444
 
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 })\`.
1445
+ Enqueue from an endpoint with the same typed call used for HTTP:
1446
+ \`context.entrypoint(appEntrypoints.job.<name>).call({ body: data })\`.
1405
1447
  `),
1406
1448
 
1407
1449
  skill(ViableSkill.TargetAgents, 'LLM agents inside the application', `
@@ -41,6 +41,14 @@ 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',
44
52
  Status = 'status',
45
53
  Commit = 'commit',
46
54
  Log = 'log',
@@ -66,6 +74,34 @@ export enum SlotFileCommand {
66
74
  DeleteFile = 'deleteFile',
67
75
  FindFilesWithEnvVars = 'findFilesWithEnvVars',
68
76
  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',
69
105
  }
70
106
 
71
107
  export enum SlotShellCommand {
@@ -228,6 +264,37 @@ export const COMMAND_DEADLINES: Record<string, number> = {
228
264
 
229
265
  export const DEFAULT_COMMAND_DEADLINE = 60_000
230
266
 
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
+
231
298
  /**
232
299
  * CALLER-side ceilings per command type: how long the asker waits for an answer.
233
300
  *
@@ -266,6 +333,33 @@ export const SHELL_COMMAND_TIMEOUTS: Record<string, number> = {
266
333
  [SlotShellCommand.Integrity]: 25_000, // executor deadline (15s) + margin
267
334
  }
268
335
 
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
+
269
363
  /**
270
364
  * Resolve the caller-side bound for one command.
271
365
  *
@@ -276,12 +370,24 @@ export const SHELL_COMMAND_TIMEOUTS: Record<string, number> = {
276
370
  export const commandTimeout = (
277
371
  type: SlotCommandType, command: string, override?: number
278
372
  ): number => override
279
- ?? (type === SlotCommandType.Shell ? SHELL_COMMAND_TIMEOUTS[command] : undefined)
373
+ ?? perCommandTimeout(type, command)
280
374
  ?? COMMAND_TIMEOUTS[type]
281
375
  ?? DEFAULT_COMMAND_TIMEOUT
282
376
 
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
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
+ }
@@ -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 } from './types.js'
3
+ import type { SlotCommandPayload, SlotGitCloneArgs } from './types.js'
4
4
 
5
5
  export const SlotCommandPayloadSchema = {
6
6
  type: 'object',
@@ -25,3 +25,22 @@ 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>
package/src/slot/types.ts CHANGED
@@ -75,6 +75,86 @@ export interface SlotGitLogResult {
75
75
  commits: SlotGitCommitInfo[]
76
76
  }
77
77
 
78
+ /**
79
+ * Bring a remote repository onto the volume.
80
+ *
81
+ * `token` is a per-invocation credential and never reaches the tree: the executor passes it
82
+ * through the process environment for the one fetch, exactly as push and pull already do, so it
83
+ * appears in no `.git/config`, no remote URL and no argv anything can observe.
84
+ */
85
+ export interface SlotGitCloneArgs {
86
+ remoteUrl: string
87
+ /** The branch to check out. Absent means whatever the remote calls its default. */
88
+ branch?: string
89
+ /** Fetch depth; 0 means the whole history. Defaults to a shallow fetch. */
90
+ depth?: number
91
+ token?: string
92
+ }
93
+
94
+ export interface SlotGitCloneResult {
95
+ cloned: boolean
96
+ /** The branch actually checked out — the resolved default, when none was asked for. */
97
+ branch: string
98
+ head: SlotGitCommitInfo | null
99
+ /** Executor diagnostics, when it has any worth reporting. */
100
+ result?: string
101
+ }
102
+
103
+ /**
104
+ * One entry of a {@link SlotFileCommand.StatTree} listing.
105
+ *
106
+ * The shape both a publisher and a connector answer with, so a census walks a pod's volume and a
107
+ * developer's own directory through one contract. `binary` is the executor's own verdict — the
108
+ * caller has not read the file and cannot form one.
109
+ */
110
+ export interface FileStat {
111
+ path: string
112
+ bytes: number
113
+ binary: boolean
114
+ modifiedAt?: string
115
+ }
116
+
117
+ export interface StatTreeArgs {
118
+ /** Project-relative directory to walk. Absent means the whole project. */
119
+ dir?: string
120
+ /** Entries to return before the answer reports itself truncated. */
121
+ limit?: number
122
+ }
123
+
124
+ /**
125
+ * `total` is what the walk SAW, `entries` what it returned.
126
+ *
127
+ * The two differ on a truncated walk, and the difference is what tells a caller its picture of the
128
+ * tree is partial — a bounded listing that reported only what it returned would be indistinguish-
129
+ * able from a small repository.
130
+ */
131
+ export interface StatTreeResult {
132
+ entries: FileStat[]
133
+ truncated: boolean
134
+ total: number
135
+ }
136
+
137
+ export interface ReadHeadArgs {
138
+ path: string
139
+ bytes: number
140
+ }
141
+
142
+ /** Move everything in the project root under `dir`, leaving the named entries where they are. */
143
+ export interface RelocateArgs {
144
+ dir: string
145
+ /** Root-relative names that stay put — the slot's own metadata and markers. */
146
+ keep?: string[]
147
+ }
148
+
149
+ export interface RelocateResult {
150
+ moved: number
151
+ kept: string[]
152
+ }
153
+
154
+ export interface RemoveTreeArgs {
155
+ dir: string
156
+ }
157
+
78
158
  /** Arguments a shell command may carry. */
79
159
  export interface SlotShellArgs {
80
160
  subproject?: SubProject
@@ -0,0 +1,162 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import Ajv from 'ajv'
3
+ import addFormats from 'ajv-formats'
4
+ import { RouteMethod, route } from '@owlmeans/route'
5
+ import { aliasOf, openProtocol, protocols } from '@owlmeans/entrypoint'
6
+ import { connect, ConnectInquiryKind, ConnectJobKind, ConnectOpKind } from '../src/connect/consts.js'
7
+ import { connectProtocols } from '../src/connect/entrypoints.js'
8
+ import { connectRef } from '../src/connect/references.js'
9
+ import {
10
+ ConnectCapabilitiesSchema, ConnectConvertCreateBodySchema, ConnectConvertProceedBodySchema,
11
+ ConnectInquiryParamsSchema, ConnectPipelineResumeBodySchema, InquiryAnswerSchema
12
+ } from '../src/connect/schemas.js'
13
+ import { ConnectExecutor } from '../src/connect/consts.js'
14
+ import { InquiryKind } from '@owlmeans/llm-common'
15
+ import { ConversionDecision, OriginKind } from '../src/convert/consts.js'
16
+
17
+ const compiler = () => addFormats(new Ajv({ strict: false }))
18
+
19
+ const entrypoints = () => connectProtocols({
20
+ guard: 'test-guard',
21
+ gate: { alias: 'test-gate', params: ['id'] },
22
+ localLlm: { alias: 'test-paid-llm', params: ['id'] },
23
+ updateBase: openProtocol(route('test:update:base', '/update')),
24
+ })
25
+
26
+ const entrypointOf = (alias: string) => {
27
+ const found = protocols(entrypoints()).find(item => item.alias === alias)
28
+ expect(found, alias).toBeDefined()
29
+
30
+ return found!
31
+ }
32
+
33
+ /** The immutable declaration a route model wraps — path, method and parent as written. */
34
+ const routeOf = (alias: string) => entrypointOf(alias).route.route
35
+
36
+ describe('viable-common - the conversion additions to the connector contract', () => {
37
+ test('every new schema compiles', () => {
38
+ const ajv = compiler()
39
+ const schemas: [string, object][] = [
40
+ ['InquiryAnswerSchema', InquiryAnswerSchema],
41
+ ['ConnectInquiryParamsSchema', ConnectInquiryParamsSchema],
42
+ ['ConnectConvertCreateBodySchema', ConnectConvertCreateBodySchema],
43
+ ['ConnectConvertProceedBodySchema', ConnectConvertProceedBodySchema],
44
+ ['ConnectPipelineResumeBodySchema', ConnectPipelineResumeBodySchema],
45
+ ['ConnectCapabilitiesSchema', ConnectCapabilitiesSchema],
46
+ ]
47
+
48
+ for (const [name, schema] of schemas) {
49
+ expect(() => ajv.compile(schema as never), name).not.toThrow()
50
+ }
51
+ })
52
+
53
+ test('an answer carries one value or several, and declining is a value too', () => {
54
+ const validate = compiler().compile(InquiryAnswerSchema as never)
55
+
56
+ expect(validate({ inquiryId: 'q1', value: 'yes' })).toBe(true)
57
+ expect(validate({ inquiryId: 'q1', value: ['a', 'b'] })).toBe(true)
58
+ expect(validate({ inquiryId: 'q1', declined: true })).toBe(true)
59
+ expect(validate({ inquiryId: 'q1', text: 'because the origin has two admin roles' })).toBe(true)
60
+ expect(validate({ value: 'orphaned' })).toBe(false)
61
+ })
62
+
63
+ test('a resume carries the answers the parked run asked for', () => {
64
+ const validate = compiler().compile(ConnectPipelineResumeBodySchema as never)
65
+
66
+ expect(validate({ answers: { q1: { inquiryId: 'q1', value: 'yes' } } })).toBe(true)
67
+ expect(validate({})).toBe(true)
68
+ })
69
+
70
+ test('the convert bodies accept an origin and a decision', () => {
71
+ const ajv = compiler()
72
+ const create = ajv.compile(ConnectConvertCreateBodySchema as never)
73
+ const proceed = ajv.compile(ConnectConvertProceedBodySchema as never)
74
+
75
+ expect(create({
76
+ name: 'Deskflow',
77
+ origin: { kind: OriginKind.Github, repoUrl: 'https://github.com/acme/deskflow' },
78
+ })).toBe(true)
79
+ // A local target the connector is already attached to fetches nothing.
80
+ expect(create({})).toBe(true)
81
+ expect(proceed({ decision: ConversionDecision.Extract })).toBe(true)
82
+ expect(proceed({ decision: 'teleport' })).toBe(false)
83
+ })
84
+
85
+ test('capabilities accept an executor kind this platform has never heard of', () => {
86
+ // The SDK and the platform ship separately: a connector built against a newer package must not
87
+ // be refused a session because it advertises one more capability than this platform knows.
88
+ const validate = compiler().compile(ConnectCapabilitiesSchema as never)
89
+
90
+ expect(validate({
91
+ harness: 'claude-code',
92
+ tiers: {},
93
+ subagents: true,
94
+ effortControl: true,
95
+ executors: [ConnectExecutor.Files, ConnectExecutor.Human, 'telepathy'],
96
+ })).toBe(true)
97
+ })
98
+
99
+ test('the conversion and generated-file routes are declared, at their paths and methods', () => {
100
+ const expected: [string, string, RouteMethod][] = [
101
+ [connect.convert.create, '/convert', RouteMethod.POST],
102
+ [connect.convert.check, '/convert/:id/check', RouteMethod.GET],
103
+ [connect.convert.start, '/convert/:id/start', RouteMethod.POST],
104
+ [connect.convert.proceed, '/convert/:id/proceed', RouteMethod.POST],
105
+ [connect.convert.cancel, '/convert/:id/cancel', RouteMethod.POST],
106
+ [connect.convert.status, '/convert/:id', RouteMethod.GET],
107
+ [connect.convert.purge, '/convert/:id/purge', RouteMethod.POST],
108
+ [connect.inquiry.answer, '/project/:id/inquiry/:inquiryId', RouteMethod.POST],
109
+ [connect.project.converterLlm, '/project/:id/converter-llm', RouteMethod.POST],
110
+ [connect.files.list, '/project/:id/files', RouteMethod.GET],
111
+ ]
112
+
113
+ for (const [alias, path, method] of expected) {
114
+ const route = routeOf(alias)
115
+ expect(route.path, alias).toBe(path)
116
+ expect(route.method, alias).toBe(method)
117
+ }
118
+ })
119
+
120
+ test('every conversion route has the typed reference used by connector adapters', () => {
121
+ expect(connectRef.convert.create.alias).toBe(connect.convert.create)
122
+ expect(connectRef.convert.check.alias).toBe(connect.convert.check)
123
+ expect(connectRef.convert.start.alias).toBe(connect.convert.start)
124
+ expect(connectRef.convert.proceed.alias).toBe(connect.convert.proceed)
125
+ expect(connectRef.convert.cancel.alias).toBe(connect.convert.cancel)
126
+ expect(connectRef.convert.status.alias).toBe(connect.convert.status)
127
+ expect(connectRef.convert.purge.alias).toBe(connect.convert.purge)
128
+ expect(connectRef.inquiry.answer.alias).toBe(connect.inquiry.answer)
129
+ expect(connectRef.project.converterLlm.alias).toBe(connect.project.converterLlm)
130
+ expect(connectRef.files.list.alias).toBe(connect.files.list)
131
+ })
132
+
133
+ test('the conversion routes hang under the connector base and carry no paid gate', () => {
134
+ // 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')
137
+
138
+ const gated = [...Object.values(connect.convert), connect.inquiry.answer,
139
+ connect.project.converterLlm]
140
+ for (const alias of gated) {
141
+ expect(entrypointOf(alias).gate, alias).toBeUndefined()
142
+ expect(aliasOf(routeOf(alias).parent!), alias).toBe(connect.base)
143
+ }
144
+ })
145
+
146
+ test('the vocabulary a conversion adds to the connector is declared', () => {
147
+ expect(ConnectOpKind.Inquiry).toBe('inquiry')
148
+ expect(ConnectExecutor.Human).toBe('human')
149
+ expect(Object.values(ConnectInquiryKind)).toEqual(['choice', 'text', 'confirm'])
150
+ // The platform's mapper is a widening rather than a translation table, which is only true
151
+ // while the two enums stay byte-identical — `manager-api` does not depend on the model
152
+ // runtime, so nothing else in the stack would notice one of them gaining a fourth shape.
153
+ expect(Object.values(ConnectInquiryKind)).toEqual(Object.values(InquiryKind))
154
+ // One job kind per stage: each stage is its own pipeline run, looked up by the alias its kind
155
+ // names, so a single `convert` kind would match whichever run row happened to be newest.
156
+ expect(ConnectJobKind.ConvertIntake).toBe('convert-intake')
157
+ expect(ConnectJobKind.ConvertAnalysis).toBe('convert-analysis')
158
+ expect(ConnectJobKind.ConvertExtraction).toBe('convert-extraction')
159
+ expect(ConnectJobKind.ConvertImplementation).toBe('convert-implementation')
160
+ expect(ConnectJobKind.ConvertPurge).toBe('convert-purge')
161
+ })
162
+ })
@@ -1,17 +1,25 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
- import { protocols } from '@owlmeans/entrypoint'
2
+ import { openProtocol, protocols } from '@owlmeans/entrypoint'
3
3
  import { DEFAULT_GUARD } from '@owlmeans/auth-common'
4
+ import { route } from '@owlmeans/route'
4
5
  import { connect } from '../src/connect/consts.js'
5
6
  import { connectProtocols } from '../src/connect/entrypoints.js'
6
7
 
7
8
  describe('@owlmeans/viable-common — connector protocol tree', () => {
8
- const tree = connectProtocols({ guard: DEFAULT_GUARD, updateBase: 'manager:update' })
9
+ const tree = connectProtocols({
10
+ guard: DEFAULT_GUARD,
11
+ updateBase: openProtocol(route('manager:update', '/update')),
12
+ })
9
13
 
10
14
  test('exports named immutable declarations and a complete flat materialization view', () => {
11
15
  expect(tree.base.alias).toBe(connect.base)
12
16
  expect(tree.session.open.alias).toBe(connect.session.open)
13
17
  expect(tree.project.llm.alias).toBe(connect.project.llm)
14
- expect(protocols(tree)).toHaveLength(28)
15
- expect(new Set(protocols(tree).map(protocol => protocol.alias)).size).toBe(28)
18
+ expect(tree.project.converterLlm.alias).toBe(connect.project.converterLlm)
19
+ expect(tree.convert.proceed.alias).toBe(connect.convert.proceed)
20
+ expect(tree.inquiry.answer.alias).toBe(connect.inquiry.answer)
21
+ expect(tree.files.list.alias).toBe(connect.files.list)
22
+ expect(protocols(tree)).toHaveLength(38)
23
+ expect(new Set(protocols(tree).map(protocol => protocol.alias)).size).toBe(38)
16
24
  })
17
25
  })