borgmcp-shared 0.6.2 → 0.6.3

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.
@@ -18,11 +18,11 @@ incident is bound to tag object `045268aa8873da330819860012ecaddb4bc2883c`, prot
18
18
  `90a1cf686a0ce32a7aef836b0b82a930191b9030` peels to protected-main commit
19
19
  `fd69b08586481a60c88099dede8e4e066f73f2f2`; attempt-1 workflow run
20
20
  `30054936226` failed in tests before build, packaging, authentication, or registry
21
- mutation and must never be rerun or moved. `borgmcp-shared@0.6.1` is published
22
- and immutable. This source now identifies the unpublished `0.6.2` Coordinator
23
- activation-cadence release. The version bump grants no tag or publication
24
- authority: creating `v0.6.2` and publishing the reviewed artifact remain separate,
25
- independently gated steps.
21
+ mutation and must never be rerun or moved. `borgmcp-shared@0.6.1` and
22
+ `borgmcp-shared@0.6.2` are published and immutable. This source now identifies
23
+ the reviewed, unpublished `0.6.3` protocol-v4 cube-creation contract release.
24
+ The version bump grants no tag or publication authority: creating `v0.6.3` and
25
+ publishing the reviewed artifact remain separate, independently gated steps.
26
26
  There is no compatibility path that returns a bearer from the server.
27
27
 
28
28
  A client verifies the credential-free `GET /api/protocol` tag preflight before
@@ -128,43 +128,67 @@ enrollment and attach routes never create or widen grants.
128
128
 
129
129
  ## Cube Creation
130
130
 
131
- `POST /api/cubes` requires an active parent-client credential with the persisted
132
- `create_cube` server capability. Ordinary clients, revoked clients, and drone
133
- sessions cannot use it. Its strict payload is:
131
+ Under protocol v4, `POST /api/cubes` requires an active parent-client credential
132
+ with the persisted `create_cube` server capability. Ordinary clients, revoked
133
+ clients, and drone sessions cannot use it. Its strict payload is:
134
134
 
135
135
  ```json
136
136
  {
137
137
  "retry_key": "00000000-0000-4000-8000-000000000201",
138
- "name": "repository presentation name",
139
- "template": "default"
138
+ "name": "My Repository Cube",
139
+ "working_repo_name": "repository-display-name",
140
+ "repository": {
141
+ "kind": "origin",
142
+ "value": "https://github.com/owner/repository"
143
+ },
144
+ "template": "software-dev"
140
145
  }
141
146
  ```
142
147
 
143
148
  The client persists the pending retry tuple before network I/O and reuses it
144
- after ambiguous transport failure. `name` is bounded presentation data and
145
- `template` selects allowlisted, server-owned inert data. The request cannot
146
- supply cube or role IDs, an owner, access, grant target, capability, arbitrary
147
- template data, paths, URLs, modules, commands, or repository credentials.
148
-
149
- The server binds `(authenticated client, retry_key)` to the exact canonical name
150
- and template. A successful transaction atomically creates one cube, exactly one
151
- human-seat role, exactly one default worker role, and exactly one creator
152
- `manage` grant. It returns:
149
+ after ambiguous transport failure. `name` is the bounded user-selected cube
150
+ name. `working_repo_name` is derived repository display metadata and does not
151
+ identify or authorize the repository. `repository` is either an exact canonical
152
+ public origin or a client-generated opaque local UUID. `template` accepts
153
+ `software-dev`, `starter`, or the compatible legacy `default` seed. The request
154
+ cannot supply cube or role IDs, an owner, access, grant target, capability,
155
+ arbitrary template data, local paths, raw origins, modules, commands, or
156
+ repository credentials.
157
+
158
+ The server binds `(authenticated client, retry_key)` to the exact cube name,
159
+ repository identity, and template. A successful transaction atomically creates
160
+ one cube, the selected template directive, roles and flags, message taxonomy,
161
+ and exactly one creator `manage` grant. The legacy `default` input retains its
162
+ empty directive, platform Coordinator, default Builder, and no taxonomy. The
163
+ authoritative response is:
153
164
 
154
165
  ```json
155
166
  {
167
+ "result": "created",
156
168
  "cube_id": "<canonical UUID>",
169
+ "name": "My Repository Cube",
170
+ "working_repo_name": "repository-display-name",
171
+ "repository": {
172
+ "kind": "origin",
173
+ "value": "https://github.com/owner/repository"
174
+ },
175
+ "template": "software-dev",
157
176
  "human_seat_role_id": "<canonical UUID>",
158
177
  "default_worker_role_id": "<canonical UUID>",
159
178
  "access": "manage"
160
179
  }
161
180
  ```
162
181
 
163
- An exact retry returns that stable response without mutation. Reusing the same
164
- retry key with a different name or template returns non-enumerating HTTP `409`
165
- `INVALID_INPUT` and creates nothing. A fresh retry key may create another cube,
166
- subject to implementation quotas. `owner_id` and role labels remain metadata;
167
- cube access derives only from the explicit cube-scoped grant.
182
+ An exact retry returns `result: "resolved"` with the stored authoritative fields
183
+ and no mutation. A fresh retry key for the same creator-scoped repository
184
+ association also resolves that stored cube, name, repository display, and
185
+ template without mutation. Changed `working_repo_name` input does not overwrite
186
+ stored display metadata. Reusing a bound retry key with a different cube name,
187
+ repository identity, or template returns non-enumerating HTTP `409`
188
+ `INVALID_INPUT` and creates nothing. A fresh retry for a different, unassociated
189
+ repository may create another cube, subject to implementation quotas. `owner_id`
190
+ and role labels remain metadata; cube access derives only from the explicit
191
+ cube-scoped grant.
168
192
 
169
193
  ## Conformance
170
194
 
@@ -178,3 +202,9 @@ every retry vector, observes client/capability/cube/role/grant counts, verifies
178
202
  secret-free errors, and proves authorized idempotent cube creation. Hostile
179
203
  reference adapters demonstrate that each retry, authority, and idempotency
180
204
  violation fails conformance.
205
+
206
+ `CREATE_CUBE_RETRY_CONFORMANCE` independently pins exact resolution, stored
207
+ display readback, and mismatch rejection for cube name, repository kind/value,
208
+ and template. `CREATE_CUBE_ASSOCIATION_CONFORMANCE` distinguishes no-mutation
209
+ resolution of an existing repository association from creation for a different,
210
+ unassociated repository.
package/docs/releasing.md CHANGED
@@ -20,16 +20,16 @@ The release lane has one build, test, package, and publication authority:
20
20
  4. rejects an existing immutable version or a package not owned solely by the
21
21
  configured npm owner before any registry mutation;
22
22
  5. publishes that exact local tarball through npm Trusted Publishing with
23
- provenance; and
24
- 6. uses bounded registry-propagation retries to compare the published
25
- `dist.integrity` with the tarball, then installs the exact registry version
26
- with scripts disabled and runs `npm audit signatures`.
23
+ provenance. Successful completion of `npm publish` is the workflow's terminal
24
+ release boundary; no post-publication registry readback can fail the immutable
25
+ release after npm has accepted it.
27
26
 
28
- npm and GitHub verify the registry signature and publish attestation. The
29
- repository does not reconstruct DSSE or SLSA statements, transfer approval
30
- tuples between runs, rebuild in a second job, or place checksum, SBOM, and report
31
- bundles on the critical publication path. SBOM portability remains covered by
32
- CI and the repository's dedicated deterministic SBOM tests.
27
+ npm and GitHub produce the registry signature and publish attestation as part of
28
+ Trusted Publishing. The repository does not reconstruct or immediately read back
29
+ those records, reconstruct DSSE or SLSA statements, transfer approval tuples
30
+ between runs, rebuild in a second job, or place checksum, SBOM, and report bundles
31
+ on the critical publication path. SBOM portability remains covered by CI and the
32
+ repository's dedicated deterministic SBOM tests.
33
33
 
34
34
  ## Permanent Configuration
35
35
 
@@ -77,8 +77,9 @@ repository, workflow output, artifact, issue, or shell history.
77
77
  tag gets one first attempt.
78
78
  5. Obtain the separately required environment approval, then approve that exact
79
79
  pending job. Approval does not permit a local rebuild or alternate artifact.
80
- 6. Require every job step to pass, including exact registry integrity and
81
- `npm audit signatures`, before announcing the version or updating consumers.
80
+ 6. Require the protected publish job to complete successfully before announcing
81
+ the version or updating consumers. Registry propagation and later consumer
82
+ availability are operational observations, not release-workflow gates.
82
83
 
83
84
  The workflow publishes only `./release/<tarball>`. It never publishes from the
84
85
  repository directory, a package name, a URL, a prior workflow artifact, or a
@@ -96,11 +97,9 @@ and run as immutable evidence. Fix the source and begin a separately reviewed an
96
97
  authorized version/tag plan. Never move, reuse, rerun, or force-update the failed
97
98
  tag.
98
99
 
99
- If npm accepts the version but a bounded integrity or signature check fails, do
100
- not rerun or republish. Preserve the run, inspect the live registry integrity and
101
- `npm audit signatures` result, and require an explicit recovery decision before
102
- consumer adoption. npm versions are immutable; never overwrite, unpublish, or
103
- silently substitute a replacement.
100
+ Once npm accepts the version, the immutable release has occurred. Never rerun,
101
+ republish, overwrite, unpublish, or silently substitute a replacement because a
102
+ later registry read is delayed or unavailable.
104
103
 
105
104
  ## Immutable Historical Evidence
106
105
 
@@ -133,10 +132,12 @@ that tag. `borgmcp-shared@0.6.0` was never published. Its annotated tag object
133
132
  `90a1cf686a0ce32a7aef836b0b82a930191b9030` peels to protected-main commit
134
133
  `fd69b08586481a60c88099dede8e4e066f73f2f2`; attempt-1 workflow run
135
134
  `30054936226` failed in tests before build, packaging, authentication, or registry
136
- mutation. Never rerun or move that tag. `borgmcp-shared@0.6.1` is published and
137
- immutable. This source identifies the unpublished `0.6.2` Coordinator
138
- activation-cadence release. Its release requires reviewed source and explicit publication authorization
139
- through one protected publish run, followed by postpublish registry integrity,
140
- signature, and attestation verification. Consumers
135
+ mutation. Never rerun or move that tag. `borgmcp-shared@0.6.1` and
136
+ `borgmcp-shared@0.6.2` are published and immutable. This source identifies the
137
+ reviewed, unpublished `0.6.3` protocol-v4 cube-creation contract release. Its
138
+ release requires reviewed source and explicit publication authorization through
139
+ one protected publish run whose terminal boundary is successful `npm publish`.
140
+ The workflow performs no post-publication registry readback. Consumers
141
141
  must update the reviewed shared artifact before adopting the matching server and
142
- client releases; protocol v2 and v3 peers fail closed at credential-free preflight.
142
+ client releases; incompatible protocol v2, v3, and v4 peers fail closed at
143
+ credential-free preflight.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "borgmcp-shared",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "description": "Shared protocol, domain, templates, and conformance contracts for Borg MCP implementations.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -22,11 +22,17 @@ import {
22
22
  PROTOCOL_VERSION,
23
23
  utf8ByteLength,
24
24
  type CreateCubeResponse,
25
+ type CreateCubeRepository,
26
+ type CubeTemplate,
25
27
  type LogCursor,
26
28
  type StreamEvent,
27
29
  type DroneRuntimeMetadata,
28
30
  } from '../protocol/index.js';
29
- import { ENROLLMENT_RETRY_CONFORMANCE } from './index.js';
31
+ import {
32
+ CREATE_CUBE_ASSOCIATION_CONFORMANCE,
33
+ CREATE_CUBE_RETRY_CONFORMANCE,
34
+ ENROLLMENT_RETRY_CONFORMANCE,
35
+ } from './index.js';
30
36
 
31
37
  export interface ConformanceHttpResponse {
32
38
  status: number;
@@ -102,13 +108,21 @@ export interface ConformanceAuthorityState {
102
108
  grants: number;
103
109
  server_capabilities: number;
104
110
  cube_create_bindings: number;
111
+ repository_associations: number;
105
112
  }
106
113
 
107
114
  export interface ConformanceCreatedCubeState {
108
115
  cube_exists: boolean;
109
116
  creator_has_grant: boolean;
117
+ creator_access: 'manage' | null;
110
118
  grant_count: number;
111
119
  role_count: number;
120
+ name: string;
121
+ working_repo_name: string;
122
+ repository: CreateCubeRepository;
123
+ template: CubeTemplate;
124
+ human_seat_role_id: string;
125
+ default_worker_role_id: string;
112
126
  human_seat_role_matches: boolean;
113
127
  default_worker_role_matches: boolean;
114
128
  }
@@ -686,7 +700,12 @@ export async function runAdapterConformance(
686
700
 
687
701
  const cubeRequest = {
688
702
  retry_key: '00000000-0000-4000-8000-000000000213',
689
- name: 'repository-one',
703
+ name: 'Repository One',
704
+ working_repo_name: 'repository-one',
705
+ repository: {
706
+ kind: 'origin' as const,
707
+ value: 'https://github.com/Byte-Ventures/repository-one',
708
+ },
690
709
  template: 'default',
691
710
  };
692
711
  const droneCredential = await environment.admin.issueDroneSession(ownerPrincipal);
@@ -724,13 +743,25 @@ export async function runAdapterConformance(
724
743
  const createdResponse = await environment.operations.createCube(ownerCredential, createProtocolEnvelope('cube-create', cubeRequest));
725
744
  expectStatus(createdResponse, 201, 'Owner cube create');
726
745
  const created = decodeCreateCubeResponseEnvelope(createdResponse.body).payload;
727
- assertStateDelta(beforeCreate, await environment.admin.observeAuthorityState(), { cubes: 1, roles: 2, grants: 1, cube_create_bindings: 1 }, 'Owner cube create');
746
+ assertStateDelta(
747
+ beforeCreate,
748
+ await environment.admin.observeAuthorityState(),
749
+ { cubes: 1, roles: 2, grants: 1, cube_create_bindings: 1, repository_associations: 1 },
750
+ 'Owner cube create',
751
+ );
728
752
  invariant(
729
753
  same(await environment.admin.inspectCreatedCube(ownerPrincipal, created), {
730
754
  cube_exists: true,
731
755
  creator_has_grant: true,
756
+ creator_access: 'manage',
732
757
  grant_count: 1,
733
758
  role_count: 2,
759
+ name: cubeRequest.name,
760
+ working_repo_name: cubeRequest.working_repo_name,
761
+ repository: cubeRequest.repository,
762
+ template: cubeRequest.template,
763
+ human_seat_role_id: created.human_seat_role_id,
764
+ default_worker_role_id: created.default_worker_role_id,
734
765
  human_seat_role_matches: true,
735
766
  default_worker_role_matches: true,
736
767
  }),
@@ -739,19 +770,92 @@ export async function runAdapterConformance(
739
770
  const beforeCreateRetry = await environment.admin.observeAuthorityState();
740
771
  const retriedCreateResponse = await environment.operations.createCube(ownerCredential, createProtocolEnvelope('cube-retry', cubeRequest));
741
772
  expectStatus(retriedCreateResponse, 201, 'Exact cube-create retry');
742
- invariant(same(decodeCreateCubeResponseEnvelope(retriedCreateResponse.body).payload, created), 'Exact cube-create retry returned different identities.');
773
+ const retriedCreate = decodeCreateCubeResponseEnvelope(retriedCreateResponse.body).payload;
774
+ invariant(retriedCreate.result === 'resolved', 'Exact cube-create retry did not report resolved readback.');
775
+ invariant(
776
+ same({ ...retriedCreate, result: 'created' }, created),
777
+ 'Exact cube-create retry returned different authoritative fields.',
778
+ );
743
779
  assertStateDelta(beforeCreateRetry, await environment.admin.observeAuthorityState(), {}, 'Exact cube-create retry');
744
- const beforeCreateMismatch = await environment.admin.observeAuthorityState();
745
- expectSecretFreeError(
746
- await environment.operations.createCube(ownerCredential, createProtocolEnvelope('cube-mismatch', { ...cubeRequest, name: 'repository-two' })),
747
- 409,
748
- ErrorCode.INVALID_INPUT,
749
- 'Cube-create retry mismatch',
750
- [cubeRequest.retry_key],
780
+ const displayRetryVector = CREATE_CUBE_RETRY_CONFORMANCE.find(
781
+ (vector) => vector.expected.outcome === 'resolved_response' &&
782
+ vector.retry.working_repo_name !== vector.initial.working_repo_name,
783
+ );
784
+ invariant(displayRetryVector !== undefined, 'Missing repository display readback vector.');
785
+ const beforeDisplayRetry = await environment.admin.observeAuthorityState();
786
+ const displayRetryResponse = await environment.operations.createCube(
787
+ ownerCredential,
788
+ createProtocolEnvelope('cube-display-retry', {
789
+ ...displayRetryVector.retry,
790
+ retry_key: cubeRequest.retry_key,
791
+ }),
792
+ );
793
+ expectStatus(displayRetryResponse, 201, 'Changed repository display retry');
794
+ invariant(
795
+ same(decodeCreateCubeResponseEnvelope(displayRetryResponse.body).payload, retriedCreate),
796
+ 'Changed repository display retry did not return stored authoritative display.',
797
+ );
798
+ assertStateDelta(
799
+ beforeDisplayRetry,
800
+ await environment.admin.observeAuthorityState(),
801
+ {},
802
+ 'Changed repository display retry',
803
+ );
804
+ for (const [index, vector] of CREATE_CUBE_RETRY_CONFORMANCE.entries()) {
805
+ if (vector.expected.outcome !== 'retry_tuple_mismatch') continue;
806
+ const mismatch = { ...vector.retry, retry_key: cubeRequest.retry_key };
807
+ const beforeCreateMismatch = await environment.admin.observeAuthorityState();
808
+ expectSecretFreeError(
809
+ await environment.operations.createCube(
810
+ ownerCredential,
811
+ createProtocolEnvelope(`cube-mismatch-${index}`, mismatch),
812
+ ),
813
+ 409,
814
+ ErrorCode.INVALID_INPUT,
815
+ `Cube-create ${vector.name}`,
816
+ [cubeRequest.retry_key],
817
+ );
818
+ assertStateDelta(
819
+ beforeCreateMismatch,
820
+ await environment.admin.observeAuthorityState(),
821
+ {},
822
+ `Cube-create ${vector.name}`,
823
+ );
824
+ }
825
+ const sameAssociationVector = CREATE_CUBE_ASSOCIATION_CONFORMANCE.find(
826
+ (vector) => vector.expected.outcome === 'resolved',
827
+ );
828
+ invariant(sameAssociationVector !== undefined, 'Missing same-repository association vector.');
829
+ const beforeAssociationResolve = await environment.admin.observeAuthorityState();
830
+ const associationResolveResponse = await environment.operations.createCube(
831
+ ownerCredential,
832
+ createProtocolEnvelope('cube-association-resolve', {
833
+ ...sameAssociationVector.request,
834
+ repository: cubeRequest.repository,
835
+ }),
836
+ );
837
+ expectStatus(associationResolveResponse, 201, 'Fresh retry for associated repository');
838
+ const associationResolved = decodeCreateCubeResponseEnvelope(associationResolveResponse.body).payload;
839
+ invariant(associationResolved.result === 'resolved', 'Associated repository did not resolve.');
840
+ invariant(
841
+ same({ ...associationResolved, result: 'created' }, created),
842
+ 'Associated repository did not return stored authoritative fields.',
843
+ );
844
+ assertStateDelta(
845
+ beforeAssociationResolve,
846
+ await environment.admin.observeAuthorityState(),
847
+ sameAssociationVector.expected.authority_state_delta,
848
+ 'Fresh retry for associated repository',
751
849
  );
752
- assertStateDelta(beforeCreateMismatch, await environment.admin.observeAuthorityState(), {}, 'Cube-create retry mismatch');
753
850
  await environment.admin.grantCreateCubeCapability(ordinaryPrincipal);
754
- const crossClientRequest = { ...cubeRequest, name: 'ordinary-repository' };
851
+ const crossClientRequest = {
852
+ ...cubeRequest,
853
+ working_repo_name: 'ordinary-repository',
854
+ repository: {
855
+ kind: 'local' as const,
856
+ value: '00000000-0000-4000-8000-000000000216',
857
+ },
858
+ };
755
859
  const beforeCrossClientCreate = await environment.admin.observeAuthorityState();
756
860
  const crossClientResponse = await environment.operations.createCube(
757
861
  ordinaryCredential,
@@ -763,7 +867,7 @@ export async function runAdapterConformance(
763
867
  assertStateDelta(
764
868
  beforeCrossClientCreate,
765
869
  await environment.admin.observeAuthorityState(),
766
- { cubes: 1, roles: 2, grants: 1, cube_create_bindings: 1 },
870
+ { cubes: 1, roles: 2, grants: 1, cube_create_bindings: 1, repository_associations: 1 },
767
871
  'Cross-client cube create',
768
872
  );
769
873
  invariant(
@@ -776,24 +880,35 @@ export async function runAdapterConformance(
776
880
  createProtocolEnvelope('cube-cross-client-retry', crossClientRequest),
777
881
  );
778
882
  expectStatus(crossClientRetry, 201, 'Exact cross-client cube-create retry');
883
+ const crossClientResolved = decodeCreateCubeResponseEnvelope(crossClientRetry.body).payload;
779
884
  invariant(
780
- same(decodeCreateCubeResponseEnvelope(crossClientRetry.body).payload, crossClientCreated),
781
- 'Exact cross-client cube-create retry returned different identities.',
885
+ crossClientResolved.result === 'resolved',
886
+ 'Exact cross-client cube-create retry did not report resolved readback.',
887
+ );
888
+ invariant(
889
+ same(
890
+ { ...crossClientResolved, result: 'created' },
891
+ crossClientCreated,
892
+ ),
893
+ 'Exact cross-client cube-create retry returned different authoritative fields.',
782
894
  );
783
895
  assertStateDelta(beforeCrossClientRetry, await environment.admin.observeAuthorityState(), {}, 'Exact cross-client cube-create retry');
896
+ const differentAssociationVector = CREATE_CUBE_ASSOCIATION_CONFORMANCE.find(
897
+ (vector) => vector.expected.outcome === 'created',
898
+ );
899
+ invariant(differentAssociationVector !== undefined, 'Missing different-repository association vector.');
784
900
  const beforeSecondCreate = await environment.admin.observeAuthorityState();
785
- const secondCreatedResponse = await environment.operations.createCube(ownerCredential, createProtocolEnvelope('cube-create-second', {
786
- ...cubeRequest,
787
- retry_key: '00000000-0000-4000-8000-000000000214',
788
- name: 'repository-two',
789
- }));
901
+ const secondCreatedResponse = await environment.operations.createCube(
902
+ ownerCredential,
903
+ createProtocolEnvelope('cube-create-second', differentAssociationVector.request),
904
+ );
790
905
  expectStatus(secondCreatedResponse, 201, 'Second cube create');
791
906
  const secondCreated = decodeCreateCubeResponseEnvelope(secondCreatedResponse.body).payload;
792
907
  invariant(secondCreated.cube_id !== created.cube_id, 'Fresh cube-create retry key reused an existing cube.');
793
908
  assertStateDelta(
794
909
  beforeSecondCreate,
795
910
  await environment.admin.observeAuthorityState(),
796
- { cubes: 1, roles: 2, grants: 1, cube_create_bindings: 1 },
911
+ differentAssociationVector.expected.authority_state_delta,
797
912
  'Second cube create',
798
913
  );
799
914
  await environment.admin.revokePrincipal(ownerPrincipal);
@@ -1,5 +1,9 @@
1
1
  import type { BroadcastHwm } from '../log-stream-hwm.js';
2
- import type { AttachResponse, EnrollmentExchangeRequest } from '../protocol/contract.js';
2
+ import type {
3
+ AttachResponse,
4
+ CreateCubeRequest,
5
+ EnrollmentExchangeRequest,
6
+ } from '../protocol/contract.js';
3
7
 
4
8
  export * from './adapter.js';
5
9
 
@@ -167,6 +171,126 @@ export const ENROLLMENT_RETRY_CONFORMANCE: readonly EnrollmentRetryConformanceVe
167
171
  },
168
172
  ];
169
173
 
174
+ export interface CreateCubeRetryConformanceVector {
175
+ name: string;
176
+ initial: CreateCubeRequest;
177
+ retry: CreateCubeRequest;
178
+ expected:
179
+ | { outcome: 'resolved_response'; status: 201 }
180
+ | { outcome: 'retry_tuple_mismatch'; status: 409; error: 'INVALID_INPUT' };
181
+ }
182
+
183
+ const CREATE_CUBE_RETRY_KEY = '00000000-0000-4000-8000-000000000121';
184
+ const CREATE_CUBE_INITIAL: CreateCubeRequest = {
185
+ retry_key: CREATE_CUBE_RETRY_KEY,
186
+ name: 'Repository One',
187
+ working_repo_name: 'repository-one',
188
+ repository: { kind: 'origin', value: 'https://github.com/Byte-Ventures/repository-one' },
189
+ template: 'default',
190
+ };
191
+
192
+ /** Stateful vectors: name, repository identity, and template bind the retry key. */
193
+ export const CREATE_CUBE_RETRY_CONFORMANCE: readonly CreateCubeRetryConformanceVector[] = [
194
+ {
195
+ name: 'exact retry resolves the authoritative response',
196
+ initial: CREATE_CUBE_INITIAL,
197
+ retry: CREATE_CUBE_INITIAL,
198
+ expected: { outcome: 'resolved_response', status: 201 },
199
+ },
200
+ {
201
+ name: 'changed repository display metadata resolves stored authoritative display',
202
+ initial: CREATE_CUBE_INITIAL,
203
+ retry: { ...CREATE_CUBE_INITIAL, working_repo_name: 'repository-one-renamed' },
204
+ expected: { outcome: 'resolved_response', status: 201 },
205
+ },
206
+ {
207
+ name: 'cube-name mismatch is rejected',
208
+ initial: CREATE_CUBE_INITIAL,
209
+ retry: { ...CREATE_CUBE_INITIAL, name: 'Repository One Renamed' },
210
+ expected: { outcome: 'retry_tuple_mismatch', status: 409, error: 'INVALID_INPUT' },
211
+ },
212
+ {
213
+ name: 'template mismatch is rejected',
214
+ initial: CREATE_CUBE_INITIAL,
215
+ retry: { ...CREATE_CUBE_INITIAL, template: 'software-dev' },
216
+ expected: { outcome: 'retry_tuple_mismatch', status: 409, error: 'INVALID_INPUT' },
217
+ },
218
+ {
219
+ name: 'repository kind mismatch is rejected',
220
+ initial: CREATE_CUBE_INITIAL,
221
+ retry: {
222
+ ...CREATE_CUBE_INITIAL,
223
+ repository: { kind: 'local', value: '00000000-0000-4000-8000-000000000122' },
224
+ },
225
+ expected: { outcome: 'retry_tuple_mismatch', status: 409, error: 'INVALID_INPUT' },
226
+ },
227
+ {
228
+ name: 'repository value mismatch is rejected',
229
+ initial: CREATE_CUBE_INITIAL,
230
+ retry: {
231
+ ...CREATE_CUBE_INITIAL,
232
+ repository: { kind: 'origin', value: 'https://github.com/Byte-Ventures/repository-two' },
233
+ },
234
+ expected: { outcome: 'retry_tuple_mismatch', status: 409, error: 'INVALID_INPUT' },
235
+ },
236
+ ];
237
+
238
+ export interface CreateCubeAssociationConformanceVector {
239
+ name: string;
240
+ created: CreateCubeRequest;
241
+ request: CreateCubeRequest;
242
+ expected:
243
+ | { outcome: 'resolved'; authority_state_delta: Record<string, never> }
244
+ | {
245
+ outcome: 'created';
246
+ authority_state_delta: {
247
+ cubes: 1;
248
+ roles: 2;
249
+ grants: 1;
250
+ cube_create_bindings: 1;
251
+ repository_associations: 1;
252
+ };
253
+ };
254
+ }
255
+
256
+ /** A creator-scoped repository association resolves independently of operation retry keys. */
257
+ export const CREATE_CUBE_ASSOCIATION_CONFORMANCE:
258
+ readonly CreateCubeAssociationConformanceVector[] = [
259
+ {
260
+ name: 'fresh retry for the same repository resolves stored authoritative fields',
261
+ created: CREATE_CUBE_INITIAL,
262
+ request: {
263
+ ...CREATE_CUBE_INITIAL,
264
+ retry_key: '00000000-0000-4000-8000-000000000123',
265
+ name: 'Ignored New Cube Name',
266
+ working_repo_name: 'ignored-new-display',
267
+ template: 'starter',
268
+ },
269
+ expected: { outcome: 'resolved', authority_state_delta: {} },
270
+ },
271
+ {
272
+ name: 'fresh retry for a different repository may create',
273
+ created: CREATE_CUBE_INITIAL,
274
+ request: {
275
+ ...CREATE_CUBE_INITIAL,
276
+ retry_key: '00000000-0000-4000-8000-000000000124',
277
+ name: 'Repository Two',
278
+ working_repo_name: 'repository-two',
279
+ repository: { kind: 'origin', value: 'https://github.com/Byte-Ventures/repository-two' },
280
+ },
281
+ expected: {
282
+ outcome: 'created',
283
+ authority_state_delta: {
284
+ cubes: 1,
285
+ roles: 2,
286
+ grants: 1,
287
+ cube_create_bindings: 1,
288
+ repository_associations: 1,
289
+ },
290
+ },
291
+ },
292
+ ];
293
+
170
294
  export const ENROLLMENT_AUTHORITY_CONFORMANCE = [
171
295
  {
172
296
  name: 'ordinary enrollment creates no authority or cube state',