@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.
- package/INDEX.md +74 -0
- package/INDEX.vi.md +75 -0
- package/README.md +44 -0
- package/README.vi.md +43 -0
- package/SKILL.md +135 -0
- package/SKILL.vi.md +128 -0
- package/alias/INDEX.md +104 -0
- package/alias/INDEX.vi.md +104 -0
- package/alias/alias.json +297 -0
- package/bin/starci-skills.mjs +219 -0
- package/knowledge/INDEX.md +22 -0
- package/knowledge/INDEX.vi.md +22 -0
- package/knowledge/grammars/starci/DNA.md +146 -0
- package/knowledge/grammars/starci/DNA.vi.md +146 -0
- package/knowledge/grammars/starci/INDEX.md +25 -0
- package/knowledge/grammars/starci/INDEX.vi.md +25 -0
- package/knowledge/grammars/starci/family.md +50 -0
- package/knowledge/grammars/starci/family.vi.md +50 -0
- package/knowledge/grammars/starci/idioms.md +120 -0
- package/knowledge/grammars/starci/idioms.vi.md +121 -0
- package/knowledge/grammars/starci/playbook.md +36 -0
- package/knowledge/grammars/starci/playbook.vi.md +35 -0
- package/knowledge/patterns/be/INDEX.md +29 -0
- package/knowledge/patterns/be/INDEX.vi.md +29 -0
- package/knowledge/patterns/be/comment.md +80 -0
- package/knowledge/patterns/be/comment.vi.md +80 -0
- package/knowledge/patterns/be/error.md +86 -0
- package/knowledge/patterns/be/error.vi.md +86 -0
- package/knowledge/patterns/be/folder.md +87 -0
- package/knowledge/patterns/be/folder.vi.md +86 -0
- package/knowledge/patterns/be/function.md +80 -0
- package/knowledge/patterns/be/function.vi.md +80 -0
- package/knowledge/patterns/be/imports.md +79 -0
- package/knowledge/patterns/be/imports.vi.md +79 -0
- package/knowledge/patterns/be/naming.md +87 -0
- package/knowledge/patterns/be/naming.vi.md +87 -0
- package/knowledge/patterns/be/test.md +79 -0
- package/knowledge/patterns/be/test.vi.md +79 -0
- package/knowledge/patterns/be/typing.md +73 -0
- package/knowledge/patterns/be/typing.vi.md +73 -0
- package/knowledge/patterns/fe/INDEX.md +29 -0
- package/knowledge/patterns/fe/INDEX.vi.md +29 -0
- package/knowledge/patterns/fe/comment.md +70 -0
- package/knowledge/patterns/fe/comment.vi.md +70 -0
- package/knowledge/patterns/fe/error.md +61 -0
- package/knowledge/patterns/fe/error.vi.md +61 -0
- package/knowledge/patterns/fe/folder.md +98 -0
- package/knowledge/patterns/fe/folder.vi.md +95 -0
- package/knowledge/patterns/fe/function.md +70 -0
- package/knowledge/patterns/fe/function.vi.md +70 -0
- package/knowledge/patterns/fe/imports.md +87 -0
- package/knowledge/patterns/fe/imports.vi.md +87 -0
- package/knowledge/patterns/fe/naming.md +77 -0
- package/knowledge/patterns/fe/naming.vi.md +77 -0
- package/knowledge/patterns/fe/test.md +73 -0
- package/knowledge/patterns/fe/test.vi.md +73 -0
- package/knowledge/patterns/fe/typing.md +67 -0
- package/knowledge/patterns/fe/typing.vi.md +67 -0
- package/knowledge/ui/INDEX.md +101 -0
- package/knowledge/ui/INDEX.vi.md +101 -0
- package/knowledge/ui/composition/INDEX.md +60 -0
- package/knowledge/ui/composition/INDEX.vi.md +63 -0
- package/knowledge/ui/composition/accent.md +73 -0
- package/knowledge/ui/composition/accent.vi.md +74 -0
- package/knowledge/ui/composition/action.md +55 -0
- package/knowledge/ui/composition/action.vi.md +56 -0
- package/knowledge/ui/composition/coverage.md +37 -0
- package/knowledge/ui/composition/coverage.vi.md +37 -0
- package/knowledge/ui/composition/cta.md +79 -0
- package/knowledge/ui/composition/cta.vi.md +79 -0
- package/knowledge/ui/composition/feedback.md +55 -0
- package/knowledge/ui/composition/feedback.vi.md +56 -0
- package/knowledge/ui/composition/hierarchy.md +83 -0
- package/knowledge/ui/composition/hierarchy.vi.md +82 -0
- package/knowledge/ui/composition/layout.md +91 -0
- package/knowledge/ui/composition/layout.vi.md +93 -0
- package/knowledge/ui/composition/responsive.md +67 -0
- package/knowledge/ui/composition/responsive.vi.md +67 -0
- package/knowledge/ui/composition/state.md +105 -0
- package/knowledge/ui/composition/state.vi.md +106 -0
- package/knowledge/ui/presentation/INDEX.md +94 -0
- package/knowledge/ui/presentation/INDEX.vi.md +69 -0
- package/knowledge/ui/presentation/boundary.md +170 -0
- package/knowledge/ui/presentation/boundary.vi.md +169 -0
- package/knowledge/ui/presentation/font.md +155 -0
- package/knowledge/ui/presentation/font.vi.md +156 -0
- package/knowledge/ui/presentation/gap.md +181 -0
- package/knowledge/ui/presentation/gap.vi.md +181 -0
- package/knowledge/ui/presentation/margin.md +168 -0
- package/knowledge/ui/presentation/margin.vi.md +171 -0
- package/knowledge/ui/presentation/measure.md +174 -0
- package/knowledge/ui/presentation/measure.vi.md +178 -0
- package/knowledge/ui/presentation/overflow.md +142 -0
- package/knowledge/ui/presentation/overflow.vi.md +142 -0
- package/knowledge/ui/presentation/padding.md +293 -0
- package/knowledge/ui/presentation/padding.vi.md +292 -0
- package/knowledge/ui/presentation/surface.md +166 -0
- package/knowledge/ui/presentation/surface.vi.md +166 -0
- package/knowledge/ui/presentation/text-flow.md +123 -0
- package/knowledge/ui/presentation/text-flow.vi.md +123 -0
- package/knowledge/ui/presentation/tone.md +114 -0
- package/knowledge/ui/presentation/tone.vi.md +113 -0
- package/knowledge/ui/proof/INDEX.md +55 -0
- package/knowledge/ui/proof/INDEX.vi.md +55 -0
- package/knowledge/ui/proof/accessibility.md +67 -0
- package/knowledge/ui/proof/accessibility.vi.md +68 -0
- package/knowledge/ui/proof/contrast.md +58 -0
- package/knowledge/ui/proof/contrast.vi.md +57 -0
- package/knowledge/ui/proof/focus.md +76 -0
- package/knowledge/ui/proof/focus.vi.md +76 -0
- package/knowledge/ui/proof/motion.md +63 -0
- package/knowledge/ui/proof/motion.vi.md +64 -0
- package/knowledge/ui/proof/render-truth.md +63 -0
- package/knowledge/ui/proof/render-truth.vi.md +63 -0
- package/operators/INDEX.md +199 -0
- package/operators/INDEX.vi.md +199 -0
- package/operators/architecture-decide/errors.json +115 -0
- package/operators/architecture-decide/operator.json +20 -0
- package/operators/architecture-decide/operator.md +133 -0
- package/operators/architecture-decide/operator.vi.md +130 -0
- package/operators/architecture-decide/self-test.mjs +221 -0
- package/operators/architecture-decide/validate.mjs +112 -0
- package/operators/backend-source-apply/errors.json +66 -0
- package/operators/backend-source-apply/operator.json +20 -0
- package/operators/backend-source-apply/operator.md +189 -0
- package/operators/backend-source-apply/operator.vi.md +185 -0
- package/operators/backend-source-apply/self-test.mjs +257 -0
- package/operators/backend-source-apply/validate.mjs +240 -0
- package/operators/business-decide/errors.json +78 -0
- package/operators/business-decide/operator.json +20 -0
- package/operators/business-decide/operator.md +176 -0
- package/operators/business-decide/operator.vi.md +174 -0
- package/operators/business-decide/self-test.mjs +225 -0
- package/operators/business-decide/validate.mjs +277 -0
- package/operators/content-generate/errors.json +106 -0
- package/operators/content-generate/operator.json +21 -0
- package/operators/content-generate/operator.md +155 -0
- package/operators/content-generate/operator.vi.md +155 -0
- package/operators/content-generate/self-test.mjs +288 -0
- package/operators/content-generate/validate.mjs +196 -0
- package/operators/errors.json +178 -0
- package/operators/frontend-direction-decide/errors.json +135 -0
- package/operators/frontend-direction-decide/operator.json +21 -0
- package/operators/frontend-direction-decide/operator.md +167 -0
- package/operators/frontend-direction-decide/operator.vi.md +167 -0
- package/operators/frontend-direction-decide/self-test.mjs +226 -0
- package/operators/frontend-direction-decide/validate.mjs +146 -0
- package/operators/frontend-presentation-resolve/errors.json +42 -0
- package/operators/frontend-presentation-resolve/operator.json +19 -0
- package/operators/frontend-presentation-resolve/operator.md +142 -0
- package/operators/frontend-presentation-resolve/operator.vi.md +140 -0
- package/operators/frontend-presentation-resolve/self-test.mjs +163 -0
- package/operators/frontend-presentation-resolve/validate.mjs +143 -0
- package/operators/frontend-source-apply/errors.json +30 -0
- package/operators/frontend-source-apply/operator.json +20 -0
- package/operators/frontend-source-apply/operator.md +127 -0
- package/operators/frontend-source-apply/operator.vi.md +126 -0
- package/operators/frontend-source-apply/self-test.mjs +214 -0
- package/operators/frontend-source-apply/validate.mjs +133 -0
- package/operators/frontend-surface-audit/errors.json +5 -0
- package/operators/frontend-surface-audit/operator.json +22 -0
- package/operators/frontend-surface-audit/operator.md +121 -0
- package/operators/frontend-surface-audit/operator.vi.md +121 -0
- package/operators/frontend-surface-audit/self-test.mjs +185 -0
- package/operators/frontend-surface-audit/validate.mjs +127 -0
- package/operators/git-publish/errors.json +54 -0
- package/operators/git-publish/operator.json +20 -0
- package/operators/git-publish/operator.md +180 -0
- package/operators/git-publish/operator.vi.md +178 -0
- package/operators/git-publish/self-test.mjs +177 -0
- package/operators/git-publish/validate.mjs +106 -0
- package/operators/platform-operate/errors.json +90 -0
- package/operators/platform-operate/operator.json +22 -0
- package/operators/platform-operate/operator.md +162 -0
- package/operators/platform-operate/operator.vi.md +160 -0
- package/operators/platform-operate/self-test.mjs +202 -0
- package/operators/platform-operate/validate.mjs +193 -0
- package/operators/quality-verify/errors.json +54 -0
- package/operators/quality-verify/operator.json +20 -0
- package/operators/quality-verify/operator.md +185 -0
- package/operators/quality-verify/operator.vi.md +177 -0
- package/operators/quality-verify/self-test.mjs +210 -0
- package/operators/quality-verify/validate.mjs +197 -0
- package/operators/release-deploy/errors.json +158 -0
- package/operators/release-deploy/operator.json +23 -0
- package/operators/release-deploy/operator.md +183 -0
- package/operators/release-deploy/operator.vi.md +181 -0
- package/operators/release-deploy/self-test.mjs +235 -0
- package/operators/release-deploy/validate.mjs +129 -0
- package/operators/uat-verify/errors.json +78 -0
- package/operators/uat-verify/operator.json +25 -0
- package/operators/uat-verify/operator.md +162 -0
- package/operators/uat-verify/operator.vi.md +161 -0
- package/operators/uat-verify/self-test.mjs +270 -0
- package/operators/uat-verify/validate.mjs +202 -0
- package/operators/workspace-bind/errors.json +90 -0
- package/operators/workspace-bind/operator.json +21 -0
- package/operators/workspace-bind/operator.md +148 -0
- package/operators/workspace-bind/operator.vi.md +148 -0
- package/operators/workspace-bind/self-test.mjs +205 -0
- package/operators/workspace-bind/validate.mjs +152 -0
- package/package.json +50 -0
- package/readiness/initialization/workspaces/commit-policy.json +91 -0
- package/readiness/initialization/workspaces/config.schema.json +35 -0
- package/readiness/initialization/workspaces/device-state.schema.json +54 -0
- package/readiness/initialization/workspaces/local-route.schema.json +206 -0
- package/readiness/initialization/workspaces/portable-route.schema.json +200 -0
- package/resources/INDEX.md +96 -0
- package/resources/INDEX.vi.md +99 -0
- package/resources/agents/profiles/claude.json +131 -0
- package/resources/agents/profiles/openai.json +131 -0
- package/resources/orchestrator.json +71 -0
- package/resources/tools.json +85 -0
- package/routing.json +272 -0
- package/scripts/alias-registry.mjs +31 -0
- package/scripts/device-state.mjs +497 -0
- package/scripts/device-state.spec.mjs +18 -0
- package/scripts/errors-registry.mjs +68 -0
- package/scripts/generate-alias-doc.mjs +66 -0
- package/scripts/generate-grammar-dna.mjs +387 -0
- package/scripts/generate-operators-index.mjs +95 -0
- package/scripts/generate-presentation-owned.mjs +681 -0
- package/scripts/install-cli.spec.mjs +74 -0
- package/scripts/json-schema.mjs +94 -0
- package/scripts/operator-md.mjs +96 -0
- package/scripts/run-operator-self-tests.mjs +36 -0
- package/scripts/validate-alias.mjs +165 -0
- package/scripts/validate-defaults.mjs +72 -0
- package/scripts/validate-knowledge-citations.mjs +90 -0
- package/scripts/validate-operator.mjs +125 -0
- package/scripts/validate-request.mjs +80 -0
- package/scripts/validate-resources.mjs +117 -0
- package/scripts/validate-response.mjs +141 -0
- package/scripts/validate-routing.mjs +91 -0
- package/scripts/validate-step.mjs +50 -0
- package/scripts/validate-templates.mjs +226 -0
- package/scripts/validate-templates.spec.mjs +144 -0
- package/scripts/validate-workflows.mjs +106 -0
- package/scripts/workspace-portable.mjs +389 -0
- package/scripts/workspace-portable.spec.mjs +246 -0
- package/templates/README.md +47 -0
- package/templates/README.vi.md +45 -0
- package/templates/changes.example.md +27 -0
- package/templates/grammars.template.md +34 -0
- package/templates/kinds/architecture-decision.contract.json +14 -0
- package/templates/kinds/architecture-decision.skeleton.md +52 -0
- package/templates/kinds/backend-source-application.contract.json +10 -0
- package/templates/kinds/backend-source-application.skeleton.md +34 -0
- package/templates/kinds/business-promise-authority.contract.json +13 -0
- package/templates/kinds/business-promise-authority.skeleton.md +67 -0
- package/templates/kinds/capture.schema.json +33 -0
- package/templates/kinds/changes.contract.json +10 -0
- package/templates/kinds/changes.skeleton.md +26 -0
- package/templates/kinds/checks.schema.json +44 -0
- package/templates/kinds/claims.schema.json +165 -0
- package/templates/kinds/conformance.schema.json +15 -0
- package/templates/kinds/content-brief.contract.json +12 -0
- package/templates/kinds/content-brief.skeleton.md +45 -0
- package/templates/kinds/content-generation-receipt.contract.json +11 -0
- package/templates/kinds/content-generation-receipt.skeleton.md +40 -0
- package/templates/kinds/content-review.contract.json +11 -0
- package/templates/kinds/content-review.skeleton.md +45 -0
- package/templates/kinds/contract.schema.json +28 -0
- package/templates/kinds/coverage-matrix.schema.json +86 -0
- package/templates/kinds/coverage.schema.json +62 -0
- package/templates/kinds/current-state.schema.json +40 -0
- package/templates/kinds/delta.schema.json +95 -0
- package/templates/kinds/e2e.schema.json +63 -0
- package/templates/kinds/frontend-direction-decision.contract.json +93 -0
- package/templates/kinds/frontend-direction-decision.skeleton.md +68 -0
- package/templates/kinds/frontend-presentation-resolution.contract.json +11 -0
- package/templates/kinds/frontend-presentation-resolution.skeleton.md +32 -0
- package/templates/kinds/frontend-source-application.contract.json +10 -0
- package/templates/kinds/frontend-source-application.skeleton.md +30 -0
- package/templates/kinds/frontend-surface-audit.contract.json +52 -0
- package/templates/kinds/frontend-surface-audit.skeleton.md +32 -0
- package/templates/kinds/gate-result.schema.json +64 -0
- package/templates/kinds/git-publication.contract.json +82 -0
- package/templates/kinds/git-publication.skeleton.md +61 -0
- package/templates/kinds/independent-critique.contract.json +9 -0
- package/templates/kinds/independent-critique.skeleton.md +28 -0
- package/templates/kinds/inventory.schema.json +35 -0
- package/templates/kinds/model.schema.json +99 -0
- package/templates/kinds/mutations.schema.json +65 -0
- package/templates/kinds/platform-operation-receipt.contract.json +76 -0
- package/templates/kinds/platform-operation-receipt.skeleton.md +54 -0
- package/templates/kinds/probes.schema.json +130 -0
- package/templates/kinds/proof.schema.json +17 -0
- package/templates/kinds/quality-verification.contract.json +87 -0
- package/templates/kinds/quality-verification.skeleton.md +58 -0
- package/templates/kinds/release-deployment.contract.json +85 -0
- package/templates/kinds/release-deployment.skeleton.md +67 -0
- package/templates/kinds/route.schema.json +293 -0
- package/templates/kinds/stack-model.schema.json +90 -0
- package/templates/kinds/uat-capture.schema.json +35 -0
- package/templates/kinds/uat-flow-verification.contract.json +11 -0
- package/templates/kinds/uat-flow-verification.skeleton.md +47 -0
- package/templates/kinds/uat-snapshot.schema.json +96 -0
- package/templates/kinds/uat-verdicts.schema.json +41 -0
- package/templates/kinds/ui-coverage.schema.json +77 -0
- package/templates/kinds/verdicts.schema.json +39 -0
- package/templates/kinds/workspace-route-binding.contract.json +12 -0
- package/templates/kinds/workspace-route-binding.skeleton.md +60 -0
- package/templates/kinds/writes.schema.json +37 -0
- package/templates/operator.template.md +73 -0
- package/templates/patterns.template.md +31 -0
- package/templates/step/request.schema.json +42 -0
- package/templates/step/response.schema.json +144 -0
- package/templates/ui-composition.template.md +36 -0
- package/templates/ui-presentation.template.md +57 -0
- package/templates/ui-proof.template.md +34 -0
- package/workflows/README.md +37 -0
- package/workflows/README.vi.md +37 -0
- package/workflows/backend-feature.json +59 -0
- package/workflows/content-unit.json +19 -0
- package/workflows/frontend-new-surface.json +81 -0
- package/workflows/frontend-reconstruct.json +67 -0
- package/workflows/frontend-refine.json +67 -0
- package/workflows/frontend-with-uat.json +78 -0
- package/workflows/full-feature.json +104 -0
- 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');
|