@starci/skills 1.1.0

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 (321) hide show
  1. package/INDEX.md +74 -0
  2. package/INDEX.vi.md +75 -0
  3. package/README.md +44 -0
  4. package/README.vi.md +43 -0
  5. package/SKILL.md +135 -0
  6. package/SKILL.vi.md +128 -0
  7. package/alias/INDEX.md +104 -0
  8. package/alias/INDEX.vi.md +104 -0
  9. package/alias/alias.json +297 -0
  10. package/bin/starci-skills.mjs +219 -0
  11. package/knowledge/INDEX.md +22 -0
  12. package/knowledge/INDEX.vi.md +22 -0
  13. package/knowledge/grammars/starci/DNA.md +146 -0
  14. package/knowledge/grammars/starci/DNA.vi.md +146 -0
  15. package/knowledge/grammars/starci/INDEX.md +25 -0
  16. package/knowledge/grammars/starci/INDEX.vi.md +25 -0
  17. package/knowledge/grammars/starci/family.md +50 -0
  18. package/knowledge/grammars/starci/family.vi.md +50 -0
  19. package/knowledge/grammars/starci/idioms.md +120 -0
  20. package/knowledge/grammars/starci/idioms.vi.md +121 -0
  21. package/knowledge/grammars/starci/playbook.md +36 -0
  22. package/knowledge/grammars/starci/playbook.vi.md +35 -0
  23. package/knowledge/patterns/be/INDEX.md +29 -0
  24. package/knowledge/patterns/be/INDEX.vi.md +29 -0
  25. package/knowledge/patterns/be/comment.md +80 -0
  26. package/knowledge/patterns/be/comment.vi.md +80 -0
  27. package/knowledge/patterns/be/error.md +86 -0
  28. package/knowledge/patterns/be/error.vi.md +86 -0
  29. package/knowledge/patterns/be/folder.md +87 -0
  30. package/knowledge/patterns/be/folder.vi.md +86 -0
  31. package/knowledge/patterns/be/function.md +80 -0
  32. package/knowledge/patterns/be/function.vi.md +80 -0
  33. package/knowledge/patterns/be/imports.md +79 -0
  34. package/knowledge/patterns/be/imports.vi.md +79 -0
  35. package/knowledge/patterns/be/naming.md +87 -0
  36. package/knowledge/patterns/be/naming.vi.md +87 -0
  37. package/knowledge/patterns/be/test.md +79 -0
  38. package/knowledge/patterns/be/test.vi.md +79 -0
  39. package/knowledge/patterns/be/typing.md +73 -0
  40. package/knowledge/patterns/be/typing.vi.md +73 -0
  41. package/knowledge/patterns/fe/INDEX.md +29 -0
  42. package/knowledge/patterns/fe/INDEX.vi.md +29 -0
  43. package/knowledge/patterns/fe/comment.md +70 -0
  44. package/knowledge/patterns/fe/comment.vi.md +70 -0
  45. package/knowledge/patterns/fe/error.md +61 -0
  46. package/knowledge/patterns/fe/error.vi.md +61 -0
  47. package/knowledge/patterns/fe/folder.md +98 -0
  48. package/knowledge/patterns/fe/folder.vi.md +95 -0
  49. package/knowledge/patterns/fe/function.md +70 -0
  50. package/knowledge/patterns/fe/function.vi.md +70 -0
  51. package/knowledge/patterns/fe/imports.md +87 -0
  52. package/knowledge/patterns/fe/imports.vi.md +87 -0
  53. package/knowledge/patterns/fe/naming.md +77 -0
  54. package/knowledge/patterns/fe/naming.vi.md +77 -0
  55. package/knowledge/patterns/fe/test.md +73 -0
  56. package/knowledge/patterns/fe/test.vi.md +73 -0
  57. package/knowledge/patterns/fe/typing.md +67 -0
  58. package/knowledge/patterns/fe/typing.vi.md +67 -0
  59. package/knowledge/ui/INDEX.md +101 -0
  60. package/knowledge/ui/INDEX.vi.md +101 -0
  61. package/knowledge/ui/composition/INDEX.md +60 -0
  62. package/knowledge/ui/composition/INDEX.vi.md +63 -0
  63. package/knowledge/ui/composition/accent.md +73 -0
  64. package/knowledge/ui/composition/accent.vi.md +74 -0
  65. package/knowledge/ui/composition/action.md +55 -0
  66. package/knowledge/ui/composition/action.vi.md +56 -0
  67. package/knowledge/ui/composition/coverage.md +37 -0
  68. package/knowledge/ui/composition/coverage.vi.md +37 -0
  69. package/knowledge/ui/composition/cta.md +79 -0
  70. package/knowledge/ui/composition/cta.vi.md +79 -0
  71. package/knowledge/ui/composition/feedback.md +55 -0
  72. package/knowledge/ui/composition/feedback.vi.md +56 -0
  73. package/knowledge/ui/composition/hierarchy.md +83 -0
  74. package/knowledge/ui/composition/hierarchy.vi.md +82 -0
  75. package/knowledge/ui/composition/layout.md +91 -0
  76. package/knowledge/ui/composition/layout.vi.md +93 -0
  77. package/knowledge/ui/composition/responsive.md +67 -0
  78. package/knowledge/ui/composition/responsive.vi.md +67 -0
  79. package/knowledge/ui/composition/state.md +105 -0
  80. package/knowledge/ui/composition/state.vi.md +106 -0
  81. package/knowledge/ui/presentation/INDEX.md +94 -0
  82. package/knowledge/ui/presentation/INDEX.vi.md +69 -0
  83. package/knowledge/ui/presentation/boundary.md +170 -0
  84. package/knowledge/ui/presentation/boundary.vi.md +169 -0
  85. package/knowledge/ui/presentation/font.md +155 -0
  86. package/knowledge/ui/presentation/font.vi.md +156 -0
  87. package/knowledge/ui/presentation/gap.md +181 -0
  88. package/knowledge/ui/presentation/gap.vi.md +181 -0
  89. package/knowledge/ui/presentation/margin.md +168 -0
  90. package/knowledge/ui/presentation/margin.vi.md +171 -0
  91. package/knowledge/ui/presentation/measure.md +174 -0
  92. package/knowledge/ui/presentation/measure.vi.md +178 -0
  93. package/knowledge/ui/presentation/overflow.md +142 -0
  94. package/knowledge/ui/presentation/overflow.vi.md +142 -0
  95. package/knowledge/ui/presentation/padding.md +293 -0
  96. package/knowledge/ui/presentation/padding.vi.md +292 -0
  97. package/knowledge/ui/presentation/surface.md +166 -0
  98. package/knowledge/ui/presentation/surface.vi.md +166 -0
  99. package/knowledge/ui/presentation/text-flow.md +123 -0
  100. package/knowledge/ui/presentation/text-flow.vi.md +123 -0
  101. package/knowledge/ui/presentation/tone.md +114 -0
  102. package/knowledge/ui/presentation/tone.vi.md +113 -0
  103. package/knowledge/ui/proof/INDEX.md +55 -0
  104. package/knowledge/ui/proof/INDEX.vi.md +55 -0
  105. package/knowledge/ui/proof/accessibility.md +67 -0
  106. package/knowledge/ui/proof/accessibility.vi.md +68 -0
  107. package/knowledge/ui/proof/contrast.md +58 -0
  108. package/knowledge/ui/proof/contrast.vi.md +57 -0
  109. package/knowledge/ui/proof/focus.md +76 -0
  110. package/knowledge/ui/proof/focus.vi.md +76 -0
  111. package/knowledge/ui/proof/motion.md +63 -0
  112. package/knowledge/ui/proof/motion.vi.md +64 -0
  113. package/knowledge/ui/proof/render-truth.md +63 -0
  114. package/knowledge/ui/proof/render-truth.vi.md +63 -0
  115. package/operators/INDEX.md +199 -0
  116. package/operators/INDEX.vi.md +199 -0
  117. package/operators/architecture-decide/errors.json +115 -0
  118. package/operators/architecture-decide/operator.json +20 -0
  119. package/operators/architecture-decide/operator.md +133 -0
  120. package/operators/architecture-decide/operator.vi.md +130 -0
  121. package/operators/architecture-decide/self-test.mjs +221 -0
  122. package/operators/architecture-decide/validate.mjs +112 -0
  123. package/operators/backend-source-apply/errors.json +66 -0
  124. package/operators/backend-source-apply/operator.json +20 -0
  125. package/operators/backend-source-apply/operator.md +189 -0
  126. package/operators/backend-source-apply/operator.vi.md +185 -0
  127. package/operators/backend-source-apply/self-test.mjs +257 -0
  128. package/operators/backend-source-apply/validate.mjs +240 -0
  129. package/operators/business-decide/errors.json +78 -0
  130. package/operators/business-decide/operator.json +20 -0
  131. package/operators/business-decide/operator.md +176 -0
  132. package/operators/business-decide/operator.vi.md +174 -0
  133. package/operators/business-decide/self-test.mjs +225 -0
  134. package/operators/business-decide/validate.mjs +277 -0
  135. package/operators/content-generate/errors.json +106 -0
  136. package/operators/content-generate/operator.json +21 -0
  137. package/operators/content-generate/operator.md +155 -0
  138. package/operators/content-generate/operator.vi.md +155 -0
  139. package/operators/content-generate/self-test.mjs +288 -0
  140. package/operators/content-generate/validate.mjs +196 -0
  141. package/operators/errors.json +178 -0
  142. package/operators/frontend-direction-decide/errors.json +135 -0
  143. package/operators/frontend-direction-decide/operator.json +21 -0
  144. package/operators/frontend-direction-decide/operator.md +167 -0
  145. package/operators/frontend-direction-decide/operator.vi.md +167 -0
  146. package/operators/frontend-direction-decide/self-test.mjs +226 -0
  147. package/operators/frontend-direction-decide/validate.mjs +146 -0
  148. package/operators/frontend-presentation-resolve/errors.json +42 -0
  149. package/operators/frontend-presentation-resolve/operator.json +19 -0
  150. package/operators/frontend-presentation-resolve/operator.md +142 -0
  151. package/operators/frontend-presentation-resolve/operator.vi.md +140 -0
  152. package/operators/frontend-presentation-resolve/self-test.mjs +163 -0
  153. package/operators/frontend-presentation-resolve/validate.mjs +143 -0
  154. package/operators/frontend-source-apply/errors.json +30 -0
  155. package/operators/frontend-source-apply/operator.json +20 -0
  156. package/operators/frontend-source-apply/operator.md +127 -0
  157. package/operators/frontend-source-apply/operator.vi.md +126 -0
  158. package/operators/frontend-source-apply/self-test.mjs +214 -0
  159. package/operators/frontend-source-apply/validate.mjs +133 -0
  160. package/operators/frontend-surface-audit/errors.json +5 -0
  161. package/operators/frontend-surface-audit/operator.json +22 -0
  162. package/operators/frontend-surface-audit/operator.md +121 -0
  163. package/operators/frontend-surface-audit/operator.vi.md +121 -0
  164. package/operators/frontend-surface-audit/self-test.mjs +185 -0
  165. package/operators/frontend-surface-audit/validate.mjs +127 -0
  166. package/operators/git-publish/errors.json +54 -0
  167. package/operators/git-publish/operator.json +20 -0
  168. package/operators/git-publish/operator.md +180 -0
  169. package/operators/git-publish/operator.vi.md +178 -0
  170. package/operators/git-publish/self-test.mjs +177 -0
  171. package/operators/git-publish/validate.mjs +106 -0
  172. package/operators/platform-operate/errors.json +90 -0
  173. package/operators/platform-operate/operator.json +22 -0
  174. package/operators/platform-operate/operator.md +162 -0
  175. package/operators/platform-operate/operator.vi.md +160 -0
  176. package/operators/platform-operate/self-test.mjs +202 -0
  177. package/operators/platform-operate/validate.mjs +193 -0
  178. package/operators/quality-verify/errors.json +54 -0
  179. package/operators/quality-verify/operator.json +20 -0
  180. package/operators/quality-verify/operator.md +185 -0
  181. package/operators/quality-verify/operator.vi.md +177 -0
  182. package/operators/quality-verify/self-test.mjs +210 -0
  183. package/operators/quality-verify/validate.mjs +197 -0
  184. package/operators/release-deploy/errors.json +158 -0
  185. package/operators/release-deploy/operator.json +23 -0
  186. package/operators/release-deploy/operator.md +183 -0
  187. package/operators/release-deploy/operator.vi.md +181 -0
  188. package/operators/release-deploy/self-test.mjs +235 -0
  189. package/operators/release-deploy/validate.mjs +129 -0
  190. package/operators/uat-verify/errors.json +78 -0
  191. package/operators/uat-verify/operator.json +25 -0
  192. package/operators/uat-verify/operator.md +162 -0
  193. package/operators/uat-verify/operator.vi.md +161 -0
  194. package/operators/uat-verify/self-test.mjs +270 -0
  195. package/operators/uat-verify/validate.mjs +202 -0
  196. package/operators/workspace-bind/errors.json +90 -0
  197. package/operators/workspace-bind/operator.json +21 -0
  198. package/operators/workspace-bind/operator.md +148 -0
  199. package/operators/workspace-bind/operator.vi.md +148 -0
  200. package/operators/workspace-bind/self-test.mjs +205 -0
  201. package/operators/workspace-bind/validate.mjs +152 -0
  202. package/package.json +50 -0
  203. package/readiness/initialization/workspaces/commit-policy.json +91 -0
  204. package/readiness/initialization/workspaces/config.schema.json +35 -0
  205. package/readiness/initialization/workspaces/device-state.schema.json +54 -0
  206. package/readiness/initialization/workspaces/local-route.schema.json +206 -0
  207. package/readiness/initialization/workspaces/portable-route.schema.json +200 -0
  208. package/resources/INDEX.md +96 -0
  209. package/resources/INDEX.vi.md +99 -0
  210. package/resources/agents/profiles/claude.json +131 -0
  211. package/resources/agents/profiles/openai.json +131 -0
  212. package/resources/orchestrator.json +71 -0
  213. package/resources/tools.json +85 -0
  214. package/routing.json +272 -0
  215. package/scripts/alias-registry.mjs +31 -0
  216. package/scripts/device-state.mjs +497 -0
  217. package/scripts/device-state.spec.mjs +18 -0
  218. package/scripts/errors-registry.mjs +68 -0
  219. package/scripts/generate-alias-doc.mjs +66 -0
  220. package/scripts/generate-grammar-dna.mjs +387 -0
  221. package/scripts/generate-operators-index.mjs +95 -0
  222. package/scripts/generate-presentation-owned.mjs +681 -0
  223. package/scripts/install-cli.spec.mjs +74 -0
  224. package/scripts/json-schema.mjs +94 -0
  225. package/scripts/operator-md.mjs +96 -0
  226. package/scripts/run-operator-self-tests.mjs +36 -0
  227. package/scripts/validate-alias.mjs +165 -0
  228. package/scripts/validate-defaults.mjs +72 -0
  229. package/scripts/validate-knowledge-citations.mjs +90 -0
  230. package/scripts/validate-operator.mjs +125 -0
  231. package/scripts/validate-request.mjs +80 -0
  232. package/scripts/validate-resources.mjs +117 -0
  233. package/scripts/validate-response.mjs +141 -0
  234. package/scripts/validate-routing.mjs +91 -0
  235. package/scripts/validate-step.mjs +50 -0
  236. package/scripts/validate-templates.mjs +226 -0
  237. package/scripts/validate-templates.spec.mjs +144 -0
  238. package/scripts/validate-workflows.mjs +106 -0
  239. package/scripts/workspace-portable.mjs +389 -0
  240. package/scripts/workspace-portable.spec.mjs +246 -0
  241. package/templates/README.md +47 -0
  242. package/templates/README.vi.md +45 -0
  243. package/templates/changes.example.md +27 -0
  244. package/templates/grammars.template.md +34 -0
  245. package/templates/kinds/architecture-decision.contract.json +14 -0
  246. package/templates/kinds/architecture-decision.skeleton.md +52 -0
  247. package/templates/kinds/backend-source-application.contract.json +10 -0
  248. package/templates/kinds/backend-source-application.skeleton.md +34 -0
  249. package/templates/kinds/business-promise-authority.contract.json +13 -0
  250. package/templates/kinds/business-promise-authority.skeleton.md +67 -0
  251. package/templates/kinds/capture.schema.json +33 -0
  252. package/templates/kinds/changes.contract.json +10 -0
  253. package/templates/kinds/changes.skeleton.md +26 -0
  254. package/templates/kinds/checks.schema.json +44 -0
  255. package/templates/kinds/claims.schema.json +165 -0
  256. package/templates/kinds/conformance.schema.json +15 -0
  257. package/templates/kinds/content-brief.contract.json +12 -0
  258. package/templates/kinds/content-brief.skeleton.md +45 -0
  259. package/templates/kinds/content-generation-receipt.contract.json +11 -0
  260. package/templates/kinds/content-generation-receipt.skeleton.md +40 -0
  261. package/templates/kinds/content-review.contract.json +11 -0
  262. package/templates/kinds/content-review.skeleton.md +45 -0
  263. package/templates/kinds/contract.schema.json +28 -0
  264. package/templates/kinds/coverage-matrix.schema.json +86 -0
  265. package/templates/kinds/coverage.schema.json +62 -0
  266. package/templates/kinds/current-state.schema.json +40 -0
  267. package/templates/kinds/delta.schema.json +95 -0
  268. package/templates/kinds/e2e.schema.json +63 -0
  269. package/templates/kinds/frontend-direction-decision.contract.json +93 -0
  270. package/templates/kinds/frontend-direction-decision.skeleton.md +68 -0
  271. package/templates/kinds/frontend-presentation-resolution.contract.json +11 -0
  272. package/templates/kinds/frontend-presentation-resolution.skeleton.md +32 -0
  273. package/templates/kinds/frontend-source-application.contract.json +10 -0
  274. package/templates/kinds/frontend-source-application.skeleton.md +30 -0
  275. package/templates/kinds/frontend-surface-audit.contract.json +52 -0
  276. package/templates/kinds/frontend-surface-audit.skeleton.md +32 -0
  277. package/templates/kinds/gate-result.schema.json +64 -0
  278. package/templates/kinds/git-publication.contract.json +82 -0
  279. package/templates/kinds/git-publication.skeleton.md +61 -0
  280. package/templates/kinds/independent-critique.contract.json +9 -0
  281. package/templates/kinds/independent-critique.skeleton.md +28 -0
  282. package/templates/kinds/inventory.schema.json +35 -0
  283. package/templates/kinds/model.schema.json +99 -0
  284. package/templates/kinds/mutations.schema.json +65 -0
  285. package/templates/kinds/platform-operation-receipt.contract.json +76 -0
  286. package/templates/kinds/platform-operation-receipt.skeleton.md +54 -0
  287. package/templates/kinds/probes.schema.json +130 -0
  288. package/templates/kinds/proof.schema.json +17 -0
  289. package/templates/kinds/quality-verification.contract.json +87 -0
  290. package/templates/kinds/quality-verification.skeleton.md +58 -0
  291. package/templates/kinds/release-deployment.contract.json +85 -0
  292. package/templates/kinds/release-deployment.skeleton.md +67 -0
  293. package/templates/kinds/route.schema.json +293 -0
  294. package/templates/kinds/stack-model.schema.json +90 -0
  295. package/templates/kinds/uat-capture.schema.json +35 -0
  296. package/templates/kinds/uat-flow-verification.contract.json +11 -0
  297. package/templates/kinds/uat-flow-verification.skeleton.md +47 -0
  298. package/templates/kinds/uat-snapshot.schema.json +96 -0
  299. package/templates/kinds/uat-verdicts.schema.json +41 -0
  300. package/templates/kinds/ui-coverage.schema.json +77 -0
  301. package/templates/kinds/verdicts.schema.json +39 -0
  302. package/templates/kinds/workspace-route-binding.contract.json +12 -0
  303. package/templates/kinds/workspace-route-binding.skeleton.md +60 -0
  304. package/templates/kinds/writes.schema.json +37 -0
  305. package/templates/operator.template.md +73 -0
  306. package/templates/patterns.template.md +31 -0
  307. package/templates/step/request.schema.json +42 -0
  308. package/templates/step/response.schema.json +144 -0
  309. package/templates/ui-composition.template.md +36 -0
  310. package/templates/ui-presentation.template.md +57 -0
  311. package/templates/ui-proof.template.md +34 -0
  312. package/workflows/README.md +37 -0
  313. package/workflows/README.vi.md +37 -0
  314. package/workflows/backend-feature.json +59 -0
  315. package/workflows/content-unit.json +19 -0
  316. package/workflows/frontend-new-surface.json +81 -0
  317. package/workflows/frontend-reconstruct.json +67 -0
  318. package/workflows/frontend-refine.json +67 -0
  319. package/workflows/frontend-with-uat.json +78 -0
  320. package/workflows/full-feature.json +104 -0
  321. package/workflows/release.json +29 -0
@@ -0,0 +1,148 @@
1
+ # workspace.bind
2
+
3
+ ## Job
4
+
5
+ Resolve one project and role into a verified checkout identity, its exact source head, and the closed
6
+ runtime binding it may consume, and return that as one typed route receipt.
7
+
8
+ ## Declaration is the only authority
9
+
10
+ A route exists because a portable declaration in `@workspaces/projects/<project>/<role>` says so and a
11
+ hydrated local route projects it onto this machine's disk. Four things routinely resemble a route and
12
+ are none of it: a directory whose name matches the project, a sibling checkout that sits next to the
13
+ Source, the current working directory, and the origin open in a browser. Those are hints, they carry
14
+ no authority at all, and this operator has nowhere to put them: Requirements declares no hint field,
15
+ so a request that carries one fails the gate with `INVALID_INPUT` at step 1 rather than being
16
+ weighed. Refusing them at the gate is the point, because a hint that survives into the body of a run
17
+ is a hint that gets followed the moment the declared route looks inconvenient. The two route halves
18
+ must agree on project, role, Git repository, and branch; a `source` repository kind carries no
19
+ directory and resolves to the Source root itself, a `sibling` kind carries a safe relative directory
20
+ and resolves beside it, and a hydrated route that names another Source belongs to another machine and
21
+ is refused.
22
+
23
+ ## Endpoint binding is a closed projection
24
+
25
+ An endpoint is never a URL somebody chose. It is the `workspace-route-port-projection`: the verified
26
+ frontend and backend routes, the project offset and slot step from `@workspaces/ports/<project>`, the
27
+ application slot, and the routed backend's declared port services, folded into one fingerprint. The
28
+ binding carries that fingerprint, and a stale one is refused rather than recomputed into agreement.
29
+ Only origin-only `http://localhost:<canonical-port>` values are endpoints; a free URL, `127.0.0.1`, a
30
+ remote host, an alternate application, an undeclared service, or a port that merely happens to be
31
+ listening establishes nothing. Step 5 runs only when `runtimeNeed` is not `none`: a caller that binds
32
+ no runtime binds no endpoints either, and a route that carries endpoints nobody asked for has
33
+ consumed a shared resource on its own initiative.
34
+
35
+ ## The caller is a consumer, never an owner
36
+
37
+ The shared local frontend, api, and identity processes belong to exactly one delegated owner task.
38
+ This operator binds the caller to that owner's endpoints as a consumer. It does not start, stop,
39
+ restart, replace, or kill a process, and it claims no port, no PID, and no runtime lifecycle. A
40
+ registry that is missing, stale, or not ready yields a typed stop so the caller can raise one
41
+ coordination request; it never authorises a replacement. An address already in use, an unexpected
42
+ authenticated session, and a failing probe are all evidence to report, and none of them is permission.
43
+
44
+ ## Nothing is repaired here
45
+
46
+ No credential is read, copied, or recorded; only the sealed roster reference is bound, and
47
+ `IDENTITY_ROSTER_SEALED` states that. A declaration that does not exist is repaired by the workspace
48
+ owner, never by this operator, so `ROUTE_UNDECLARED` and `ROUTE_UNHYDRATED` are expected outcomes of
49
+ an unprepared workspace rather than defects in the request. `CHECKOUT_DIRTY` never falls back to
50
+ anything: this operator does not stash, clean, or reset a working tree to make a binding possible.
51
+ The businesses authority root is derived as `<git root>/.worktrees/businesses` when that worktree
52
+ exists on a source checkout and is absent otherwise; it is never accepted from the person, because a
53
+ typed authority root is how a second business tree is born. Provenance and freshness are not a step
54
+ of their own: they are written inside the emit, next to the binding they describe.
55
+
56
+ ## Boundary
57
+
58
+ Context is read-only apart from the machine-local hydrated route state, which Git ignores. The
59
+ operator writes only `response/` of its own branch: `response.md`, `response/data/route.json` and
60
+ `response.json`. It never accepts a similar name, a sibling directory, the working directory, or a
61
+ browser URL as route authority, never accepts a chosen URL, a loopback alias, or a merely listening
62
+ port as an endpoint, never starts, stops, restarts, replaces, or kills a shared runtime process, never
63
+ claims a port, a PID, or a runtime lifecycle for a feature task, never creates or switches to a task,
64
+ feature, or worktree branch under a forbidden worktree policy, never writes a credential, token,
65
+ cookie, or password into the receipt or any evidence, and never repairs a missing route, initializes a
66
+ workspace, or provisions an account. It makes no product decision and carries no verdict.
67
+
68
+ ## Context
69
+
70
+ | Alias | Bind | Required |
71
+ | --- | --- | --- |
72
+ | `@workspaces/projects/<project>/<role>` | the portable route declaration, read by fingerprint; the only route authority | yes |
73
+ | `@workspaces/local/routes/<project>/<role>` | the hydrated route this machine projects the declaration onto, and the checkout it resolves to | yes |
74
+ | `@workspaces/device-state` | machine identity and the sealed credential roster, bound by name and never read | yes |
75
+ | `@workspaces/ports/<project>` | the port projection, read only when the caller consumes the runtime | no |
76
+ | `@worktrees/sessions/central-runtime` | the runtime owner registry, its generation and health evidence, bound only when the caller consumes | no |
77
+
78
+ ## Inputs
79
+
80
+ | Kind | From | Required |
81
+ | --- | --- | --- |
82
+ | — | this operator opens the chain, so it consumes no earlier branch | no |
83
+
84
+ ## Requirements
85
+
86
+ | Field | Type | Default | Ask |
87
+ | --- | --- | --- | --- |
88
+ | `project` | id | — | The project to bind |
89
+ | `role` | choice | — | `fe` or `be`: the role of that project to bind |
90
+ | `gitPolicy` | list of `{worktreeBranches, mutationBranch}` | the policy the route declaration carries | The branch law this binding is verified against; `forbidden` keeps every write on the mutation branch |
91
+ | `declaredWriteRoots` | list | empty | The only paths later work may write; anything dirty outside them is `CHECKOUT_DIRTY` |
92
+ | `runtimeNeed` | choice | none | `none` binds no runtime and skips step 5; `consume` binds the owner's endpoints as a consumer |
93
+ | `resume` | token | null | The blocked branch's token when re-entering after a stop |
94
+
95
+ ## Steps
96
+
97
+ | # | Step | Params | Reads | Writes | Stops with |
98
+ | --- | --- | --- | --- | --- | --- |
99
+ | 1 | Validate the gate and resume, and refuse every hint it carries | `resume` | `request/request.json`, its requirements and its frozen head | — | `INVALID_INPUT`, `SOURCE_DRIFT`, `NO_PROGRESS` |
100
+ | 2 | Bind bootstrap and identity | — | @workspaces/device-state, the machine identity and the sealed credential roster, @tools/secrets | — | `IDENTITY_UNVERIFIED` |
101
+ | 3 | Resolve the route | `project`, `role` | @workspaces/projects/<project>/<role> for exactly this project and role, @workspaces/local/routes/<project>/<role>, @tools/git | — | `ROUTE_UNDECLARED`, `ROUTE_UNHYDRATED`, `ROUTE_MISMATCH` |
102
+ | 4 | Verify the checkout: branch policy, a clean tree, and the write roots | `gitPolicy`, `declaredWriteRoots` | @workspaces/local/routes/<project>/<role>, the resolved checkout, its branch, its head and its working tree, @tools/git, @tools/shell | — | `BRANCH_POLICY_VIOLATION`, `CHECKOUT_DIRTY` |
103
+ | 5 | Bind the runtime the caller consumes, only when `runtimeNeed` is not none | `runtimeNeed` | @worktrees/sessions/central-runtime, the owner registry, its generation and health evidence, @workspaces/ports/<project>, @tools/http | — | `ENDPOINT_AUTHORITY_STALE`, `RUNTIME_NOT_READY` |
104
+ | 6 | Bind provenance and freshness, then emit | — | everything above, @workspaces/device-state | `response/response.md`, `response/data/route.json`, `response/response.json` | — |
105
+
106
+ Step 5 recomputes nothing: the endpoint fingerprint either matches the closed projection or the
107
+ branch stops. Under `worktreeBranches` set to forbidden, a route binds only on the mutation branch and
108
+ records `WORKTREE_BRANCH_FORBIDDEN`; under `session-only` it binds on the mutation branch or on a
109
+ `session/<sessionId>` worktree branch, the only shape a source-writing operator may commit to; a redacted conversation head records `PROVENANCE_HEAD_BOUND`, and
110
+ a cached receipt matching the same identity tuple and fingerprints records `CACHED_ROUTE_REUSED`. A
111
+ resume begins again at step 1, reuses only unchanged fingerprinted observations, and consumes the
112
+ exact delta; a republished route arrives as a new route fingerprint, because the same fingerprint
113
+ cannot yield a different binding.
114
+
115
+ ## Outputs
116
+
117
+ | Kind | File | Type | Required |
118
+ | --- | --- | --- | --- |
119
+ | `workspace-route-binding` | `response/response.md` | md | yes |
120
+ | `route` | `response/data/route.json` | data | yes |
121
+
122
+ ## Stops
123
+
124
+ | Code | Disposition |
125
+ | --- | --- |
126
+ | `INVALID_INPUT` | terminate |
127
+ | `SOURCE_DRIFT` | terminate |
128
+ | `NO_PROGRESS` | terminate |
129
+ | `IDENTITY_UNVERIFIED` | terminate |
130
+ | `ROUTE_UNDECLARED` | terminate |
131
+ | `ROUTE_UNHYDRATED` | terminate |
132
+ | `ROUTE_MISMATCH` | terminate |
133
+ | `BRANCH_POLICY_VIOLATION` | terminate |
134
+ | `CHECKOUT_DIRTY` | terminate |
135
+ | `ENDPOINT_AUTHORITY_STALE` | terminate |
136
+ | `RUNTIME_NOT_READY` | terminate |
137
+
138
+ ## Next
139
+
140
+ | When | Operator |
141
+ | --- | --- |
142
+ | the route is bound and the checkout carries a publication to push | `git.publish` |
143
+ | the route is bound and a promise must be decided against its source | `business.decide` |
144
+ | the route is bound and a backend contract must be filled inside it | `backend.source.apply` |
145
+ | the route is bound and a frontend surface must be written inside it | `frontend.source.apply` |
146
+ | the runtime owner is missing or not ready and one coordination request must be raised | `platform.operate` |
147
+ | the route is bound and a frontend surface must be decided inside it | `frontend.direction.decide` |
148
+ | the route is bound and a published head must be verified before it ships | `quality.verify` |
@@ -0,0 +1,148 @@
1
+ # workspace.bind
2
+
3
+ ## Việc
4
+
5
+ Phân giải một project và một role thành định danh checkout đã kiểm, head source chính xác của nó, và
6
+ ràng buộc runtime đóng mà nó được tiêu thụ, rồi trả về thành một biên nhận route có kiểu.
7
+
8
+ ## Khai báo là thẩm quyền duy nhất
9
+
10
+ Một route tồn tại vì một khai báo portable trong `@workspaces/projects/<project>/<role>` nói vậy, và
11
+ một route local đã hydrate chiếu nó xuống ổ đĩa của máy này. Bốn thứ thường trông giống route mà
12
+ không phải: một thư mục trùng tên project, một checkout anh em nằm cạnh Source, thư mục làm việc hiện
13
+ tại, và origin đang mở trong trình duyệt. Chúng là hint, không mang chút thẩm quyền nào, và operator
14
+ này không có chỗ nào để chứa: bảng Yêu cầu không khai field hint nào, nên một request mang hint sẽ
15
+ trượt gate với `INVALID_INPUT` ở bước 1 thay vì được cân nhắc. Từ chối ngay ở gate mới là điểm mấu
16
+ chốt, vì một hint sống sót vào thân của lượt chạy là một hint sẽ được nghe theo ngay khi route đã
17
+ khai trông bất tiện. Hai nửa của route phải khớp nhau về project, role, kho Git và nhánh; kiểu kho
18
+ `source` không mang thư mục và phân giải về chính gốc Source, kiểu `sibling` mang một thư mục tương
19
+ đối an toàn và phân giải bên cạnh nó, còn một route đã hydrate nêu tên một Source khác là của máy
20
+ khác và bị từ chối.
21
+
22
+ ## Ràng endpoint là một phép chiếu đóng
23
+
24
+ Một endpoint không bao giờ là URL ai đó chọn. Nó là `workspace-route-port-projection`: route frontend
25
+ và backend đã kiểm, offset project và bước slot lấy từ `@workspaces/ports/<project>`, slot ứng dụng,
26
+ và các port service đã khai của backend được route, gộp thành một fingerprint. Binding mang đúng
27
+ fingerprint ấy, và một fingerprint cũ bị từ chối chứ không được tính lại cho khớp. Chỉ giá trị
28
+ `http://localhost:<port chuẩn>` dạng chỉ-origin mới là endpoint; một URL tự do, `127.0.0.1`, một host
29
+ từ xa, một ứng dụng khác, một service không khai, hay một port tình cờ đang lắng nghe đều không thiết
30
+ lập gì cả. Bước 5 chỉ chạy khi `runtimeNeed` khác `none`: người gọi không ràng runtime thì cũng không
31
+ ràng endpoint, và một route mang endpoint không ai xin là một route đã tiêu thụ tài nguyên chung theo
32
+ ý riêng.
33
+
34
+ ## Người gọi là người tiêu thụ, không bao giờ là chủ
35
+
36
+ Các tiến trình frontend, api và identity local dùng chung thuộc về đúng một task chủ được uỷ quyền.
37
+ Operator này ràng người gọi vào endpoint của chủ ấy với vai người tiêu thụ. Nó không khởi động, dừng,
38
+ khởi động lại, thay thế hay giết một tiến trình, và không đòi port, PID hay vòng đời runtime nào. Một
39
+ registry thiếu, cũ hay chưa sẵn sàng sinh ra một mã dừng có kiểu để người gọi nêu một yêu cầu phối
40
+ hợp; nó không bao giờ cho phép thay thế. Một địa chỉ đang bị chiếm, một phiên đã đăng nhập bất ngờ,
41
+ và một probe hỏng đều là bằng chứng để báo cáo, và không cái nào là sự cho phép.
42
+
43
+ ## Không sửa chữa gì ở đây
44
+
45
+ Không credential nào được đọc, chép hay ghi lại; chỉ tham chiếu roster đã niêm phong được ràng, và
46
+ `IDENTITY_ROSTER_SEALED` nói đúng điều đó. Một khai báo không tồn tại thì do chủ workspace sửa, không
47
+ bao giờ do operator này, nên `ROUTE_UNDECLARED` và `ROUTE_UNHYDRATED` là kết cục dự kiến của một
48
+ workspace chưa chuẩn bị chứ không phải lỗi của request. `CHECKOUT_DIRTY` không bao giờ có fallback:
49
+ operator này không stash, không clean, không reset cây làm việc để một binding trở nên khả thi. Gốc
50
+ thẩm quyền businesses được suy ra là `<gốc git>/.worktrees/businesses` khi worktree ấy tồn tại trên
51
+ một checkout source và vắng mặt nếu không; nó không bao giờ được nhận từ người, vì một gốc thẩm quyền
52
+ do người gõ vào chính là cách cây business thứ hai ra đời. Provenance và độ tươi không phải một bước
53
+ riêng: chúng được ghi ngay trong bước phát, cạnh binding mà chúng mô tả.
54
+
55
+ ## Ranh giới
56
+
57
+ Context chỉ đọc, trừ trạng thái route đã hydrate cục bộ của máy, thứ mà Git bỏ qua. Operator chỉ ghi
58
+ `response/` của nhánh mình: `response.md`, `response/data/route.json` và `response.json`. Nó không bao
59
+ giờ nhận một cái tên na ná, một thư mục anh em, thư mục làm việc hay một URL trình duyệt làm thẩm
60
+ quyền route, không bao giờ nhận một URL tự chọn, một bí danh loopback hay một port chỉ đang lắng nghe
61
+ làm endpoint, không bao giờ khởi động, dừng, khởi động lại, thay thế hay giết một tiến trình runtime
62
+ dùng chung, không bao giờ đòi port, PID hay vòng đời runtime cho một task feature, không bao giờ tạo
63
+ hay chuyển sang nhánh task, feature hay worktree dưới một chính sách worktree cấm, không bao giờ ghi
64
+ credential, token, cookie hay mật khẩu vào biên nhận hay bất kỳ bằng chứng nào, và không bao giờ sửa
65
+ một route thiếu, khởi tạo một workspace hay cấp phát một tài khoản. Nó không ra quyết định sản phẩm
66
+ và không mang phán quyết nào.
67
+
68
+ ## Context
69
+
70
+ | Alias | Bind | Bắt buộc |
71
+ | --- | --- | --- |
72
+ | `@workspaces/projects/<project>/<role>` | khai báo route portable, đọc theo fingerprint; thẩm quyền route duy nhất | có |
73
+ | `@workspaces/local/routes/<project>/<role>` | route đã hydrate mà máy này chiếu khai báo xuống, và checkout nó phân giải ra | có |
74
+ | `@workspaces/device-state` | định danh máy và roster credential đã niêm phong, ràng theo tên và không bao giờ đọc | có |
75
+ | `@workspaces/ports/<project>` | phép chiếu port, chỉ đọc khi người gọi tiêu thụ runtime | không |
76
+ | `@worktrees/sessions/central-runtime` | registry chủ runtime, generation và bằng chứng sức khoẻ của nó, chỉ ràng khi người gọi tiêu thụ | không |
77
+
78
+ ## Đầu vào
79
+
80
+ | Kind | Từ đâu | Bắt buộc |
81
+ | --- | --- | --- |
82
+ | — | operator này mở chuỗi, nên nó không tiêu thụ nhánh nào trước đó | không |
83
+
84
+ ## Yêu cầu
85
+
86
+ | Field | Kiểu | Mặc định | Hỏi |
87
+ | --- | --- | --- | --- |
88
+ | `project` | id | — | Project cần ràng |
89
+ | `role` | choice | — | `fe` hay `be`: vai của project ấy cần ràng |
90
+ | `gitPolicy` | list `{worktreeBranches, mutationBranch}` | the policy the route declaration carries | Luật nhánh mà binding này được kiểm theo; `forbidden` giữ mọi lần ghi trên nhánh mutation |
91
+ | `declaredWriteRoots` | list | empty | Những đường dẫn duy nhất mà việc sau được ghi; bẩn ngoài chúng là `CHECKOUT_DIRTY` |
92
+ | `runtimeNeed` | choice | none | `none` không ràng runtime và bỏ qua bước 5; `consume` ràng endpoint của chủ với vai tiêu thụ |
93
+ | `resume` | token | null | Token của nhánh bị chặn khi vào lại sau một mã dừng |
94
+
95
+ ## Các bước
96
+
97
+ | # | Bước | Tham số | Đọc | Ghi | Dừng với |
98
+ | --- | --- | --- | --- | --- | --- |
99
+ | 1 | Kiểm gate và chạy lại, và từ chối mọi hint nó mang | `resume` | `request/request.json`, phần requirements và head đóng băng của nó | — | `INVALID_INPUT`, `SOURCE_DRIFT`, `NO_PROGRESS` |
100
+ | 2 | Ràng bootstrap và identity | — | @workspaces/device-state, định danh máy và roster credential đã niêm phong, @tools/secrets | — | `IDENTITY_UNVERIFIED` |
101
+ | 3 | Phân giải route | `project`, `role` | @workspaces/projects/<project>/<role> đúng project và role này, @workspaces/local/routes/<project>/<role>, @tools/git | — | `ROUTE_UNDECLARED`, `ROUTE_UNHYDRATED`, `ROUTE_MISMATCH` |
102
+ | 4 | Kiểm checkout: chính sách nhánh, cây sạch và các write root | `gitPolicy`, `declaredWriteRoots` | @workspaces/local/routes/<project>/<role>, checkout đã phân giải, nhánh, head và cây làm việc của nó, @tools/git, @tools/shell | — | `BRANCH_POLICY_VIOLATION`, `CHECKOUT_DIRTY` |
103
+ | 5 | Ràng runtime mà người gọi tiêu thụ, chỉ khi `runtimeNeed` khác none | `runtimeNeed` | @worktrees/sessions/central-runtime, registry chủ, generation và bằng chứng sức khoẻ của nó, @workspaces/ports/<project>, @tools/http | — | `ENDPOINT_AUTHORITY_STALE`, `RUNTIME_NOT_READY` |
104
+ | 6 | Ràng provenance và độ tươi, rồi phát | — | mọi thứ ở trên, @workspaces/device-state | `response/response.md`, `response/data/route.json`, `response/response.json` | — |
105
+
106
+ Bước 5 không tính lại gì cả: fingerprint endpoint hoặc khớp phép chiếu đóng, hoặc nhánh dừng. Dưới
107
+ `worktreeBranches` đặt là forbidden, một route chỉ ràng trên nhánh mutation và ghi
108
+ `WORKTREE_BRANCH_FORBIDDEN`; một head hội thoại đã che ghi `PROVENANCE_HEAD_BOUND`, và một biên nhận Đặt là `session-only` thì route ràng trên nhánh mutation hoặc trên nhánh worktree
109
+ `session/<sessionId>`, hình dạng duy nhất mà operator ghi source được commit lên;
110
+ cache khớp cùng bộ định danh và fingerprint ghi `CACHED_ROUTE_REUSED`. Chạy lại thì bắt đầu lại từ
111
+ bước 1, chỉ dùng lại quan sát có fingerprint không đổi, và tiêu thụ đúng phần delta; một route
112
+ publish lại tới dưới dạng một fingerprint route mới, vì cùng một fingerprint không thể cho một ràng
113
+ buộc khác.
114
+
115
+ ## Đầu ra
116
+
117
+ | Kind | File | Kiểu | Bắt buộc |
118
+ | --- | --- | --- | --- |
119
+ | `workspace-route-binding` | `response/response.md` | md | có |
120
+ | `route` | `response/data/route.json` | data | có |
121
+
122
+ ## Dừng
123
+
124
+ | Code | Xử lý |
125
+ | --- | --- |
126
+ | `INVALID_INPUT` | terminate |
127
+ | `SOURCE_DRIFT` | terminate |
128
+ | `NO_PROGRESS` | terminate |
129
+ | `IDENTITY_UNVERIFIED` | terminate |
130
+ | `ROUTE_UNDECLARED` | terminate |
131
+ | `ROUTE_UNHYDRATED` | terminate |
132
+ | `ROUTE_MISMATCH` | terminate |
133
+ | `BRANCH_POLICY_VIOLATION` | terminate |
134
+ | `CHECKOUT_DIRTY` | terminate |
135
+ | `ENDPOINT_AUTHORITY_STALE` | terminate |
136
+ | `RUNTIME_NOT_READY` | terminate |
137
+
138
+ ## Kế tiếp
139
+
140
+ | Khi | Operator |
141
+ | --- | --- |
142
+ | route đã ràng và checkout mang một bản publish cần đẩy | `git.publish` |
143
+ | route đã ràng và một lời hứa phải được quyết dựa trên source của nó | `business.decide` |
144
+ | route đã ràng và một contract backend phải được điền bên trong nó | `backend.source.apply` |
145
+ | route đã ràng và một bề mặt frontend phải được viết bên trong nó | `frontend.source.apply` |
146
+ | chủ runtime thiếu hoặc chưa sẵn sàng và cần nêu một yêu cầu phối hợp | `platform.operate` |
147
+ | route đã ràng và một bề mặt frontend phải được quyết bên trong nó | `frontend.direction.decide` |
148
+ | route đã ràng và một head đã publish phải được kiểm trước khi lên | `quality.verify` |
@@ -0,0 +1,205 @@
1
+ // Proves validate.mjs on a synthetic session branch: one conforming bind of a source checkout with no
2
+ // runtime, one bind that consumes the shared runtime, one branch blocked on a terminate code, and one
3
+ // mutation per law, each of which must fail with a line that names the defect.
4
+ import assert from 'node:assert/strict';
5
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
6
+ import { tmpdir } from 'node:os';
7
+ import path from 'node:path';
8
+ import { validateWorkspaceStep } from './validate.mjs';
9
+
10
+ const head = 'd'.repeat(40);
11
+ const fp = (c) => `sha256:${c.repeat(64)}`;
12
+ const SOURCE = 'D:/Repositories/starci-academy-backend';
13
+ const PORTABLE = '.workspaces/projects/starci-academy/be.json';
14
+ const HYDRATED = '.workspaces/local/routes/starci-academy/be/config.json';
15
+ const OWNER = 'task-runtime-owner-1';
16
+
17
+ const runtimeConsumption = (overrides = {}) => ({
18
+ ownerTaskId: OWNER,
19
+ generation: 3,
20
+ status: 'ready',
21
+ consumerRole: 'consumer',
22
+ endpointBinding: {
23
+ authority: 'workspace-route-port-projection',
24
+ project: 'starci-academy',
25
+ application: 'academy',
26
+ services: { frontend: 'web', api: 'api', identity: 'keycloak' },
27
+ authorityFingerprint: fp('b'),
28
+ },
29
+ endpoints: { frontend: 'http://localhost:3000', api: 'http://localhost:3001', identity: 'http://localhost:8089' },
30
+ ...overrides,
31
+ });
32
+ function routeBinding(overrides = {}) {
33
+ return {
34
+ project: 'starci-academy', role: 'be',
35
+ portableRouteRef: PORTABLE, hydratedRouteRef: HYDRATED,
36
+ routeFingerprint: fp('a'), identityFingerprint: fp('c'), sourceHead: head,
37
+ checkout: { diskPath: SOURCE, gitRoot: SOURCE, gitRepository: 'git@github.com:starci/academy-backend.git', branch: 'mtp', repositoryKind: 'source', directory: null, sourceHead: head },
38
+ gitPolicy: { worktreeBranches: 'forbidden', mutationBranch: 'mtp' },
39
+ mutationReadiness: 'ready',
40
+ writeRoots: ['src', 'test'],
41
+ authorityRoots: { businesses: `${SOURCE}/.worktrees/businesses` },
42
+ runtime: null,
43
+ provenanceHeadRef: null,
44
+ ...overrides,
45
+ };
46
+ }
47
+
48
+ function responseMd({ binding = routeBinding(), findings = null, runtimeRows = null } = {}) {
49
+ const defaultFindings = [
50
+ ['ROUTE_HYDRATED_FROM_PORTABLE', binding.hydratedRouteRef, 'the portable declaration resolved to this local route'],
51
+ ['IDENTITY_ROSTER_SEALED', 'the credential roster reference', 'the roster was bound by name and never read'],
52
+ ...(binding.gitPolicy.worktreeBranches === 'forbidden' ? [['WORKTREE_BRANCH_FORBIDDEN', binding.gitPolicy.mutationBranch, 'the routed policy forbids task and worktree branches']] : []),
53
+ ...(binding.provenanceHeadRef ? [['PROVENANCE_HEAD_BOUND', binding.provenanceHeadRef, 'a redacted conversation head was attached']] : []),
54
+ ...(binding.runtime ? [['RUNTIME_CONSUMED_NOT_OWNED', binding.runtime.ownerTaskId, 'the caller consumes the owner endpoints and owns no lifecycle']] : []),
55
+ ];
56
+ const rows = (findings ?? defaultFindings).map(([code, subject, statement]) => `| \`${code}\` | ${subject} | ${statement} |`).join('\n');
57
+ const runtime = runtimeRows ?? (binding.runtime
58
+ ? [['Owner task', binding.runtime.ownerTaskId], ['Status', binding.runtime.status], ['Consumer role', binding.runtime.consumerRole], ['Frontend', binding.runtime.endpoints.frontend], ['Api', binding.runtime.endpoints.api], ['Identity', binding.runtime.endpoints.identity]]
59
+ : []);
60
+ const runtimeTable = runtime.map(([k, v]) => `| ${k} | ${v} |`).join('\n');
61
+ const writeRootRows = binding.writeRoots.map((p) => `| ${p} | the only paths later work may write |`).join('\n');
62
+ return `# workspace-route-binding — ${binding.project}/${binding.role}
63
+
64
+ The routed backend checkout of this project, bound at the frozen head with its declared write roots.
65
+
66
+ ## Binding
67
+
68
+ | Field | Value |
69
+ | --- | --- |
70
+ | Project | ${binding.project} |
71
+ | Role | ${binding.role} |
72
+ | Portable route | ${binding.portableRouteRef} |
73
+ | Hydrated route | ${binding.hydratedRouteRef} |
74
+ | Source head | ${binding.sourceHead} |
75
+
76
+ ## Checkout
77
+
78
+ | Field | Value |
79
+ | --- | --- |
80
+ | Disk path | ${binding.checkout.diskPath} |
81
+ | Git root | ${binding.checkout.gitRoot} |
82
+ | Git repository | ${binding.checkout.gitRepository} |
83
+ | Branch | ${binding.checkout.branch} |
84
+ | Repository kind | ${binding.checkout.repositoryKind} |
85
+ | Directory | ${binding.checkout.directory ?? '—'} |
86
+ | Source head | ${binding.checkout.sourceHead} |
87
+ | Mutation readiness | ${binding.mutationReadiness} |
88
+ | Businesses root | ${binding.authorityRoots.businesses ?? '—'} |
89
+
90
+ ## Policy
91
+
92
+ | Field | Value |
93
+ | --- | --- |
94
+ | Worktree branches | ${binding.gitPolicy.worktreeBranches} |
95
+ | Mutation branch | ${binding.gitPolicy.mutationBranch} |
96
+
97
+ ## Write roots
98
+
99
+ | Path | Why |
100
+ | --- | --- |
101
+ ${writeRootRows}
102
+
103
+ ## Runtime
104
+
105
+ | Field | Value |
106
+ | --- | --- |
107
+ ${runtimeTable}${runtimeTable ? '\n' : ''}
108
+ ## Findings
109
+
110
+ | Code | Subject | Statement |
111
+ | --- | --- | --- |
112
+ ${rows}
113
+ `;
114
+ }
115
+
116
+ const requestJson = ({ runtimeNeed = 'none', extra = {} } = {}) => ({
117
+ schemaVersion: 9, operatorId: 'workspace.bind', step: 1, parallel: 1, sessionId: 's-test',
118
+ contexts: [{ alias: '@workspaces/projects/starci-academy/be', head: null }, { alias: '@workspaces/local/routes/starci-academy/be', head }, { alias: '@workspaces/device-state', head: null }],
119
+ requirements: { project: 'starci-academy', role: 'be', gitPolicy: { worktreeBranches: 'forbidden', mutationBranch: 'mtp' }, declaredWriteRoots: ['src', 'test'], runtimeNeed, resume: null, ...extra },
120
+ inputs: {}, resume: null,
121
+ });
122
+ function responseJson({ status = 'done', stop, fallbacks = [], fields = null, next = [] } = {}) {
123
+ return {
124
+ schemaVersion: 9, operatorId: 'workspace.bind', step: 1, parallel: 1, status, ...(stop ? { stop } : {}), fallbacks,
125
+ fields: fields ?? { 'workspace-route-binding': 'response/response.md', route: 'response/data/route.json' },
126
+ commits: [], next,
127
+ };
128
+ }
129
+
130
+ function writeBranch(files) {
131
+ const session = mkdtempSync(path.join(tmpdir(), 'workspace-session-'));
132
+ const branch = path.join(session, 'step-1', 'parallel-1');
133
+ for (const d of ['request', 'response/data', 'response/artifacts']) mkdirSync(path.join(branch, d), { recursive: true });
134
+ writeFileSync(path.join(session, 'state.json'), JSON.stringify({ id: 's-test', chain: [['1/1']], steps: { '1/1': 'workspace.bind' }, current: '1/1', status: 'running' }));
135
+ for (const [name, content] of Object.entries(files)) {
136
+ if (content === null) continue;
137
+ writeFileSync(path.join(branch, name), typeof content === 'string' ? content : JSON.stringify(content, null, 2));
138
+ }
139
+ return { branch, session };
140
+ }
141
+ const baseline = () => ({
142
+ 'request/request.json': requestJson(),
143
+ 'response/response.json': responseJson(),
144
+ 'response/response.md': responseMd(),
145
+ 'response/data/route.json': routeBinding(),
146
+ });
147
+ const withBinding = (binding, requestOverrides = {}) => ({
148
+ ...baseline(),
149
+ 'request/request.json': requestJson(requestOverrides),
150
+ 'response/data/route.json': binding,
151
+ 'response/response.md': responseMd({ binding }),
152
+ });
153
+
154
+ async function expectValid(files, label) {
155
+ const { branch, session } = writeBranch(files);
156
+ const { errors } = await validateWorkspaceStep(branch);
157
+ rmSync(session, { recursive: true, force: true });
158
+ assert.deepEqual(errors, [], `${label} should be valid`);
159
+ }
160
+ async function expectError(files, needle, label) {
161
+ const { branch, session } = writeBranch(files);
162
+ const { errors } = await validateWorkspaceStep(branch);
163
+ rmSync(session, { recursive: true, force: true });
164
+ assert.ok(errors.some((e) => e.includes(needle)), `${label}: expected an error containing "${needle}", got:\n${errors.join('\n') || '(none)'}`);
165
+ }
166
+
167
+ const consuming = () => withBinding(routeBinding({ runtime: runtimeConsumption() }), { runtimeNeed: 'consume' });
168
+
169
+ await expectValid(baseline(), 'a source checkout bound with no runtime');
170
+ await expectValid(consuming(), 'a bind that consumes the shared runtime');
171
+ await expectValid({ ...baseline(), 'response/response.json': responseJson({ status: 'blocked', stop: 'ROUTE_UNDECLARED', fields: {} }), 'response/response.md': null, 'response/data/route.json': null }, 'blocked on an undeclared route');
172
+
173
+ await expectError({ ...baseline(), 'response/response.json': { ...responseJson(), stop: 'ROUTE_UNDECLARED' } }, 'only a blocked response carries a stop', 'done with a stop');
174
+ await expectError({ ...baseline(), 'response/response.json': responseJson({ status: 'blocked', stop: 'MADE_UP_CODE', fields: {} }), 'response/response.md': null, 'response/data/route.json': null }, 'not a registered code', 'unknown stop code');
175
+ await expectError({ ...baseline(), 'response/response.json': responseJson({ fallbacks: ['CHECKOUT_DIRTY'] }) }, 'has disposition terminate under these requirements; it cannot be taken as a fallback', 'fallback on a terminate code');
176
+ await expectError({ ...baseline(), 'response/response.json': responseJson({ status: 'blocked', stop: 'ROUTE_MISMATCH' }) }, 'a blocked branch cannot carry a route', 'blocked while binding a route');
177
+ await expectError({ ...baseline(), 'request/request.json': requestJson({ extra: { mystery: 1 } }) }, 'requirements.mystery is not a field', 'undeclared requirement');
178
+ await expectError({ ...baseline(), 'request/request.json': requestJson({ extra: { project: '' } }) }, 'required field project has no value', 'missing required project');
179
+ await expectError(withBinding(routeBinding({ checkout: { ...routeBinding().checkout, directory: 'academy' } })), 'a source checkout must report a null directory', 'a source checkout carrying a directory');
180
+ await expectError(withBinding(routeBinding({ checkout: { ...routeBinding().checkout, repositoryKind: 'sibling' } })), 'a sibling checkout carries no business authority root', 'a sibling checkout claiming business authority');
181
+ await expectError(withBinding(routeBinding({ authorityRoots: { businesses: '.worktrees/businesses' } })), 'must be derived from the checkout as', 'a typed businesses root');
182
+ await expectError(withBinding(routeBinding({ checkout: { ...routeBinding().checkout, branch: 'feature/x' } })), 'a forbidden worktree policy cannot bind a route on another branch', 'a forbidden policy on a task branch');
183
+ await expectError(withBinding(routeBinding({ gitPolicy: { worktreeBranches: 'allowed', mutationBranch: 'mtp' }, checkout: { ...routeBinding().checkout, branch: 'feature/x' } }), { extra: { gitPolicy: { worktreeBranches: 'allowed', mutationBranch: 'mtp' } } }), 'mutation is ready only on mtp, not on feature/x', 'mutation ready off the mutation branch');
184
+ await expectError(withBinding(routeBinding({ checkout: { ...routeBinding().checkout, gitRoot: `${SOURCE}/api` } })), 'the checkout disk path and Git root must be the same checkout', 'the checkout and its Git root disagree');
185
+ await expectError(withBinding(routeBinding({ sourceHead: 'e'.repeat(40) })), 'must name the same source head', 'the binding and the checkout disagree on the head');
186
+ await expectError({ ...baseline(), 'request/request.json': requestJson({ runtimeNeed: 'consume' }) }, 'must bind the runtime owner', 'consuming with no runtime bound');
187
+ await expectError(withBinding(routeBinding({ writeRoots: ['src'] })), 'which the binding does not carry', 'a declared write root the binding drops');
188
+ await expectError(withBinding(routeBinding({ runtime: runtimeConsumption() })), 'runtimeNeed is none, so step 5 never ran and no runtime may be bound', 'a runtime bound without need');
189
+ await expectError(withBinding(routeBinding({ runtime: runtimeConsumption({ status: 'starting' }) }), { runtimeNeed: 'consume' }), 'cannot bind a starting runtime owner for consumption', 'a runtime that is not ready');
190
+ await expectError(withBinding(routeBinding({ runtime: runtimeConsumption({ endpoints: { frontend: 'http://127.0.0.1:3000', api: 'http://localhost:3001', identity: 'http://localhost:8089' } }) }), { runtimeNeed: 'consume' }), 'is not an origin-only localhost projection', 'a loopback alias as an endpoint');
191
+ await expectError(withBinding(routeBinding({ runtime: runtimeConsumption({ endpoints: { frontend: 'http://localhost:3000', api: 'http://localhost:3000', identity: 'http://localhost:8089' } }) }), { runtimeNeed: 'consume' }), 'must resolve to distinct ports', 'two services on one port');
192
+ await expectError(withBinding(routeBinding({ runtime: runtimeConsumption({ endpointBinding: { ...runtimeConsumption().endpointBinding, services: { frontend: 'web', api: 'web', identity: 'keycloak' } } }) }), { runtimeNeed: 'consume' }), 'service keys must be distinct', 'two services under one key');
193
+ await expectError(withBinding(routeBinding({ runtime: runtimeConsumption({ endpointBinding: { ...runtimeConsumption().endpointBinding, project: 'other-project' } }) }), { runtimeNeed: 'consume' }), 'belongs to another project than the bound route', 'an endpoint binding from another project');
194
+ await expectError({ ...consuming(), 'response/response.md': responseMd({ binding: routeBinding({ runtime: runtimeConsumption() }), findings: [['ROUTE_HYDRATED_FROM_PORTABLE', HYDRATED, 'resolved'], ['IDENTITY_ROSTER_SEALED', 'roster', 'sealed'], ['WORKTREE_BRANCH_FORBIDDEN', 'mtp', 'forbidden']] }) }, 'must record that the caller does not own it', 'a consumed runtime with no ownership finding');
195
+ await expectError({ ...baseline(), 'response/response.md': responseMd({ findings: [['IDENTITY_ROSTER_SEALED', 'roster', 'sealed'], ['WORKTREE_BRANCH_FORBIDDEN', 'mtp', 'forbidden']] }) }, 'must record the hydrated route it resolved from', 'a bound route with no hydration finding');
196
+ await expectError({ ...baseline(), 'response/response.md': responseMd({ findings: [['ROUTE_HYDRATED_FROM_PORTABLE', HYDRATED, 'resolved'], ['IDENTITY_ROSTER_SEALED', 'roster', 'sealed'], ['WORKTREE_BRANCH_FORBIDDEN', 'mtp', 'forbidden'], ['HINT_REJECTED', 'D:/Repositories/starci-academy', 'a similar directory name']] }) }, 'a hint is INVALID_INPUT at the gate', 'a receipt that weighs a hint');
197
+ await expectError({ ...baseline(), 'response/response.md': responseMd({ findings: [['ROUTE_HYDRATED_FROM_PORTABLE', HYDRATED, 'resolved'], ['WORKTREE_BRANCH_FORBIDDEN', 'mtp', 'forbidden']] }) }, 'the credential roster was sealed and never read', 'no sealed roster finding');
198
+ await expectError({ ...baseline(), 'response/response.md': responseMd({ findings: [['ROUTE_HYDRATED_FROM_PORTABLE', HYDRATED, 'resolved'], ['IDENTITY_ROSTER_SEALED', 'roster', 'sealed']] }) }, 'a forbidden worktree policy must be recorded', 'a forbidden policy that was never recorded');
199
+ await expectError({ ...baseline(), 'response/response.md': responseMd({ findings: [['ROUTE_HYDRATED_FROM_PORTABLE', HYDRATED, 'resolved'], ['ROUTE_HYDRATED_FROM_PORTABLE', HYDRATED, 'resolved again'], ['IDENTITY_ROSTER_SEALED', 'roster', 'sealed'], ['WORKTREE_BRANCH_FORBIDDEN', 'mtp', 'forbidden']] }) }, 'repeats subject', 'a repeated finding subject');
200
+ await expectError({ ...baseline(), 'response/response.md': responseMd().replace('## Policy', '## Git policy') }, 'missing section ^## Policy$', 'response section renamed');
201
+ await expectError({ ...baseline(), 'response/response.md': responseMd().replace('| Branch | mtp |', '| Branch | feature/x |') }, 'differs from the route binding', 'the receipt and the binding disagree on the branch');
202
+ await expectError({ ...baseline(), 'response/data/route.json': routeBinding({ sourceHead: 'not-a-head' }) }, 'sourceHead', 'route schema');
203
+ await expectError({ ...baseline(), 'response/response.json': (() => { const o = responseJson(); delete o.fields.route; return o; })() }, 'required output route is not in fields', 'missing required output');
204
+
205
+ process.stdout.write('workspace.bind self-test: 3 valid branches, 29 rejected mutations\n');