@peers-app/peers-sdk 0.26.6 → 0.27.1
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/README.md +3 -1
- package/bin/peers-package.js +13 -0
- package/define/package.json +4 -0
- package/dist/contracts/__tests__/action-dispatch.test.js +56 -76
- package/dist/contracts/__tests__/also-implements.test.d.ts +1 -0
- package/dist/contracts/__tests__/also-implements.test.js +110 -0
- package/dist/contracts/__tests__/builder.test.js +45 -0
- package/dist/contracts/__tests__/contract-resolvers.test.js +134 -4
- package/dist/contracts/__tests__/frozen-contracts.test.d.ts +1 -0
- package/dist/contracts/__tests__/frozen-contracts.test.js +191 -0
- package/dist/contracts/__tests__/generic-tool-dispatch.test.js +13 -56
- package/dist/contracts/__tests__/host-contract-client.test.d.ts +1 -0
- package/dist/contracts/__tests__/host-contract-client.test.js +793 -0
- package/dist/contracts/__tests__/host-contract-transport.test.js +15 -0
- package/dist/contracts/__tests__/registry.test.js +68 -0
- package/dist/contracts/__tests__/target-dispatch.d.ts +11 -0
- package/dist/contracts/__tests__/target-dispatch.js +24 -0
- package/dist/contracts/__tests__/tool-catalog-service.test.js +37 -1
- package/dist/contracts/__tests__/tool-catalog.test.js +12 -3
- package/dist/contracts/__tests__/validate.test.js +177 -7
- package/dist/contracts/__tests__/workflow-context.test.js +0 -11
- package/dist/contracts/builder.d.ts +16 -2
- package/dist/contracts/builder.js +44 -3
- package/dist/contracts/contract-definition-schema.d.ts +251 -0
- package/dist/contracts/contract-definition-schema.js +48 -0
- package/dist/contracts/contract-provider-router.d.ts +2 -0
- package/dist/contracts/contract-provider-router.js +5 -2
- package/dist/contracts/contract-proxy.d.ts +52 -18
- package/dist/contracts/contract-proxy.js +144 -203
- package/dist/contracts/contract-resolvers.d.ts +6 -4
- package/dist/contracts/contract-resolvers.js +32 -9
- package/dist/contracts/frozen-contracts.d.ts +59 -0
- package/dist/contracts/frozen-contracts.js +86 -0
- package/dist/contracts/host-contract-client.d.ts +134 -0
- package/dist/contracts/host-contract-client.js +396 -0
- package/dist/contracts/host-contract-transport.d.ts +2 -0
- package/dist/contracts/host-contract-transport.js +8 -0
- package/dist/contracts/index.d.ts +6 -4
- package/dist/contracts/index.js +11 -5
- package/dist/contracts/registry.d.ts +31 -1
- package/dist/contracts/registry.js +100 -6
- package/dist/contracts/system/contract-catalog-contract.d.ts +61 -6
- package/dist/contracts/system/contract-catalog-contract.js +17 -5
- package/dist/contracts/tool-catalog-service.js +26 -6
- package/dist/contracts/tool-catalog.d.ts +1 -1
- package/dist/contracts/tool-catalog.js +4 -1
- package/dist/contracts/types.d.ts +32 -2
- package/dist/contracts/validate.d.ts +16 -1
- package/dist/contracts/validate.js +139 -25
- package/dist/data/change-tracking.d.ts +17 -5
- package/dist/data/change-tracking.js +22 -6
- package/dist/data/change-tracking.test.d.ts +1 -0
- package/dist/data/change-tracking.test.js +62 -0
- package/dist/data/create-group.js +1 -1
- package/dist/data/group-members.d.ts +14 -0
- package/dist/data/group-members.js +36 -0
- package/dist/data/group-members.test.js +49 -0
- package/dist/data/orm/sql.data-source.d.ts +9 -0
- package/dist/data/orm/sql.data-source.js +47 -2
- package/dist/data/orm/sql.data-source.test.js +142 -0
- package/dist/data/orm/table-container.js +9 -9
- package/dist/data/orm/table-container.test.js +44 -0
- package/dist/data/orm/types.d.ts +41 -1
- package/dist/data/orm/types.js +159 -28
- package/dist/data/orm/types.test.js +129 -0
- package/dist/data/peer-types.d.ts +8 -62
- package/dist/data/persistent-vars.d.ts +4 -1
- package/dist/data/persistent-vars.js +7 -2
- package/dist/data/tools.d.ts +29 -221
- package/dist/define/also-implements.test.d.ts +1 -0
- package/dist/define/also-implements.test.js +123 -0
- package/dist/define/context.d.ts +77 -0
- package/dist/define/context.js +2 -0
- package/dist/define/contract-ref.d.ts +177 -0
- package/dist/define/contract-ref.js +67 -0
- package/dist/define/declare-contract.d.ts +167 -0
- package/dist/define/declare-contract.js +220 -0
- package/dist/define/declare-contract.test.d.ts +1 -0
- package/dist/define/declare-contract.test.js +573 -0
- package/dist/define/define-package.d.ts +203 -0
- package/dist/define/define-package.js +289 -0
- package/dist/define/define.test.d.ts +1 -0
- package/dist/define/define.test.js +1010 -0
- package/dist/define/empty-fields.d.ts +15 -0
- package/dist/define/empty-fields.js +134 -0
- package/dist/define/empty-fields.test.d.ts +1 -0
- package/dist/define/empty-fields.test.js +55 -0
- package/dist/define/events-observables.test.d.ts +1 -0
- package/dist/define/events-observables.test.js +1013 -0
- package/dist/define/events.d.ts +81 -0
- package/dist/define/events.js +81 -0
- package/dist/define/generated-field.d.ts +20 -0
- package/dist/define/generated-field.js +25 -0
- package/dist/define/index.d.ts +28 -0
- package/dist/define/index.js +62 -0
- package/dist/define/lazy-members.d.ts +8 -0
- package/dist/define/lazy-members.js +38 -0
- package/dist/define/manifest.d.ts +63 -0
- package/dist/define/manifest.js +653 -0
- package/dist/define/observables.d.ts +146 -0
- package/dist/define/observables.js +187 -0
- package/dist/define/serve-members.d.ts +21 -0
- package/dist/define/serve-members.js +152 -0
- package/dist/define/serve-members.test.d.ts +1 -0
- package/dist/define/serve-members.test.js +630 -0
- package/dist/define/serve.d.ts +22 -0
- package/dist/define/serve.js +58 -0
- package/dist/define/system-contracts.d.ts +41 -0
- package/dist/define/system-contracts.js +21 -0
- package/dist/define/table-handle.d.ts +13 -0
- package/dist/define/table-handle.js +36 -0
- package/dist/define/table.d.ts +128 -0
- package/dist/define/table.js +122 -0
- package/dist/define/tool.d.ts +129 -0
- package/dist/define/tool.js +91 -0
- package/dist/define/vars.d.ts +82 -0
- package/dist/define/vars.js +256 -0
- package/dist/define/vars.test.d.ts +1 -0
- package/dist/define/vars.test.js +39 -0
- package/dist/define/wire-value.d.ts +12 -0
- package/dist/define/wire-value.js +76 -0
- package/dist/define/worker-state.d.ts +15 -0
- package/dist/define/worker-state.js +45 -0
- package/dist/device/tx-encoding.test.js +10 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +5 -1
- package/dist/invites/group-approval.js +4 -2
- package/dist/invites/group-approval.test.js +2 -0
- package/dist/observable.d.ts +42 -2
- package/dist/observable.js +25 -7
- package/dist/package-build/build-package.d.ts +60 -0
- package/dist/package-build/build-package.js +312 -0
- package/dist/package-build/build-package.test.d.ts +1 -0
- package/dist/package-build/build-package.test.js +204 -0
- package/dist/package-build/cli.d.ts +7 -0
- package/dist/package-build/cli.js +218 -0
- package/dist/package-build/contract-add.d.ts +35 -0
- package/dist/package-build/contract-add.js +97 -0
- package/dist/package-build/contract-add.test.d.ts +1 -0
- package/dist/package-build/contract-add.test.js +467 -0
- package/dist/package-build/contract-source.d.ts +59 -0
- package/dist/package-build/contract-source.js +471 -0
- package/dist/package-build/freeze-contracts.d.ts +56 -0
- package/dist/package-build/freeze-contracts.js +198 -0
- package/dist/package-build/freeze-contracts.test.d.ts +1 -0
- package/dist/package-build/freeze-contracts.test.js +355 -0
- package/dist/package-installer/package-creator.d.ts +4 -0
- package/dist/package-installer/package-creator.js +57 -16
- package/dist/package-installer/package-creator.test.js +86 -0
- package/dist/package-loader/contract-members.d.ts +53 -0
- package/dist/package-loader/contract-members.js +31 -0
- package/dist/package-loader/contract-package-loader.js +5 -1
- package/dist/package-loader/contract-package-loader.test.js +11 -0
- package/dist/package-loader/host-authority-loading.test.js +9 -5
- package/dist/package-loader/index.d.ts +1 -0
- package/dist/package-loader/index.js +1 -0
- package/dist/package-loader/isolated-artifact.d.ts +80 -17
- package/dist/package-loader/isolated-artifact.js +257 -51
- package/dist/package-loader/isolated-artifact.test.js +294 -21
- package/dist/package-loader/isolated-runtime.d.ts +22 -7
- package/dist/package-loader/isolated-table-gateway.js +30 -11
- package/dist/package-loader/isolated-table-gateway.test.js +243 -0
- package/dist/package-loader/package-loader.d.ts +8 -1
- package/dist/package-loader/package-loader.js +39 -20
- package/dist/package-loader/package-loader.test.js +47 -0
- package/dist/peer-id.d.ts +25 -0
- package/dist/peer-id.js +99 -0
- package/dist/serial-json.compat.test.d.ts +1 -0
- package/dist/serial-json.compat.test.js +369 -0
- package/dist/serial-json.d.ts +41 -5
- package/dist/serial-json.js +216 -157
- package/dist/serial-json.oracle.d.ts +21 -0
- package/dist/serial-json.oracle.js +222 -0
- package/dist/serial-json.test.js +315 -0
- package/dist/testing/index.d.ts +4 -0
- package/dist/testing/index.js +8 -0
- package/dist/testing/mock-host-contract.d.ts +123 -0
- package/dist/testing/mock-host-contract.js +434 -0
- package/dist/testing/mock-host-contract.test.d.ts +1 -0
- package/dist/testing/mock-host-contract.test.js +782 -0
- package/dist/types/field-type.d.ts +35 -18
- package/dist/types/field-type.js +4 -1
- package/dist/types/zod-types.d.ts +9 -0
- package/dist/types/zod-types.js +8 -2
- package/dist/utils.d.ts +22 -25
- package/dist/utils.js +45 -92
- package/dist/utils.test.js +36 -0
- package/package.json +9 -2
- package/testing/package.json +4 -0
|
@@ -36,6 +36,21 @@ describe("host contract transport", () => {
|
|
|
36
36
|
disposers.push(dispose);
|
|
37
37
|
await expect(pending).resolves.toBe(transport);
|
|
38
38
|
});
|
|
39
|
+
it("cancels waits and clears their deadline", async () => {
|
|
40
|
+
jest.useFakeTimers();
|
|
41
|
+
try {
|
|
42
|
+
const controller = new AbortController();
|
|
43
|
+
const pending = (0, index_1.waitForHostContractTransport)({ signal: controller.signal, timeoutMs: 25 });
|
|
44
|
+
const rejected = expect(pending).rejects.toThrow("cancelled");
|
|
45
|
+
controller.abort(new Error("cancelled"));
|
|
46
|
+
await rejected;
|
|
47
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
48
|
+
await expect((0, index_1.waitForHostContractTransport)({ signal: controller.signal })).rejects.toThrow("cancelled");
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
jest.useRealTimers();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
39
54
|
it("rejects when no transport arrives before timeoutMs", async () => {
|
|
40
55
|
jest.useFakeTimers();
|
|
41
56
|
try {
|
|
@@ -168,3 +168,71 @@ describe("ContractRegistry.replaceBatch", () => {
|
|
|
168
168
|
expect(registry.getRegisteredProviderIds(rejectedContractId, 1)).toEqual([]);
|
|
169
169
|
});
|
|
170
170
|
});
|
|
171
|
+
describe("ContractRegistry exposes", () => {
|
|
172
|
+
const hubId = (0, utils_1.newid)();
|
|
173
|
+
const newsId = (0, utils_1.newid)();
|
|
174
|
+
const provider = (0, utils_1.newid)();
|
|
175
|
+
const other = (0, utils_1.newid)();
|
|
176
|
+
it("keeps each provider's exposures, outside the contract's definition", () => {
|
|
177
|
+
const registry = new registry_1.ContractRegistry();
|
|
178
|
+
const result = registry.registerBatch(provider, [
|
|
179
|
+
{ definition: makeDefinition(newsId, 1, "News") },
|
|
180
|
+
{
|
|
181
|
+
definition: makeDefinition(hubId, 2, "Hub"),
|
|
182
|
+
exposes: [{ contractId: newsId, version: 1 }],
|
|
183
|
+
},
|
|
184
|
+
]);
|
|
185
|
+
expect(result.valid).toBe(true);
|
|
186
|
+
registry.registerBatch(other, [{ definition: makeDefinition(hubId, 2, "Hub") }]);
|
|
187
|
+
expect(registry.getExposedContracts(hubId, 2, provider)).toEqual([
|
|
188
|
+
{ contractId: newsId, version: 1 },
|
|
189
|
+
]);
|
|
190
|
+
expect(registry.getExposedContracts(hubId, 2, other)).toEqual([]);
|
|
191
|
+
expect(registry.getExposedContracts(newsId, 1, provider)).toEqual([]);
|
|
192
|
+
expect(registry.getExposedContracts(hubId, 2, (0, utils_1.newid)())).toEqual([]);
|
|
193
|
+
expect(registry.getDefinition(hubId, 2)).not.toHaveProperty("exposes");
|
|
194
|
+
expect(registry.getProviderDefinition(newsId, 1, provider)?.name).toBe("News");
|
|
195
|
+
expect(registry.getProviderDefinition(newsId, 1, other)).toBeUndefined();
|
|
196
|
+
// A caller can't change what the registry holds through the returned list.
|
|
197
|
+
registry.getExposedContracts(hubId, 2, provider).pop();
|
|
198
|
+
expect(registry.getExposedContracts(hubId, 2, provider)).toHaveLength(1);
|
|
199
|
+
});
|
|
200
|
+
it("carries exposures to an alsoImplements alias, and drops them on replacement", () => {
|
|
201
|
+
const registry = new registry_1.ContractRegistry();
|
|
202
|
+
registry.register(other, makeDefinition(hubId, 1, "Hub v1"));
|
|
203
|
+
registry.registerBatch(provider, [
|
|
204
|
+
{ definition: makeDefinition(newsId, 1, "News") },
|
|
205
|
+
{
|
|
206
|
+
definition: makeDefinition(hubId, 2, "Hub"),
|
|
207
|
+
alsoImplements: [{ contractId: hubId, version: 1 }],
|
|
208
|
+
exposes: [{ contractId: newsId, version: 1 }],
|
|
209
|
+
},
|
|
210
|
+
]);
|
|
211
|
+
expect(registry.getExposedContracts(hubId, 1, provider)).toEqual([
|
|
212
|
+
{ contractId: newsId, version: 1 },
|
|
213
|
+
]);
|
|
214
|
+
registry.replaceBatch(provider, [
|
|
215
|
+
{ definition: makeDefinition(newsId, 1, "News") },
|
|
216
|
+
{ definition: makeDefinition(hubId, 2, "Hub") },
|
|
217
|
+
]);
|
|
218
|
+
expect(registry.getExposedContracts(hubId, 2, provider)).toEqual([]);
|
|
219
|
+
});
|
|
220
|
+
it("refuses an exposure the batch doesn't provide, of itself, or twice", () => {
|
|
221
|
+
const registry = new registry_1.ContractRegistry();
|
|
222
|
+
const hub = (exposes) => [
|
|
223
|
+
{ definition: makeDefinition(newsId, 1, "News") },
|
|
224
|
+
{ definition: makeDefinition(hubId, 2, "Hub"), exposes },
|
|
225
|
+
];
|
|
226
|
+
expect(registry.validateBatch(provider, hub([{ contractId: (0, utils_1.newid)(), version: 1 }]))).toEqual(expect.objectContaining({
|
|
227
|
+
valid: false,
|
|
228
|
+
errors: [expect.objectContaining({ message: expect.stringMatching(/doesn't provide/) })],
|
|
229
|
+
}));
|
|
230
|
+
expect(registry.validateBatch(provider, hub([{ contractId: hubId, version: 2 }])).errors[0].message).toMatch(/can't expose itself/);
|
|
231
|
+
expect(registry.validateBatch(provider, hub([
|
|
232
|
+
{ contractId: newsId, version: 1 },
|
|
233
|
+
{ contractId: newsId, version: 1 },
|
|
234
|
+
])).errors[0].message).toMatch(/twice/);
|
|
235
|
+
expect(registry.registerBatch(provider, hub([{ contractId: newsId, version: 2 }])).valid).toBe(false);
|
|
236
|
+
expect(registry.getRegisteredProviderIds(newsId, 1)).toEqual([]);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { IToolInstance } from "../../data/tools";
|
|
2
|
+
import { type ContractToolDispatch } from "../contract-proxy";
|
|
3
|
+
/**
|
|
4
|
+
* The host's `dispatchTool`, for tests: runs `tools` as the provider of `contractId` v1 with the
|
|
5
|
+
* frame the calling tool's provider bound, so each target is admitted as the host would admit
|
|
6
|
+
* it. Targets can dispatch further through the same bridge.
|
|
7
|
+
*
|
|
8
|
+
* @param contractId - The contract the targets are provided under.
|
|
9
|
+
* @param tools - The target tools, by name.
|
|
10
|
+
*/
|
|
11
|
+
export declare function targetDispatch(contractId: string, tools: Record<string, IToolInstance>): ContractToolDispatch;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.targetDispatch = targetDispatch;
|
|
4
|
+
const contract_proxy_1 = require("../contract-proxy");
|
|
5
|
+
/**
|
|
6
|
+
* The host's `dispatchTool`, for tests: runs `tools` as the provider of `contractId` v1 with the
|
|
7
|
+
* frame the calling tool's provider bound, so each target is admitted as the host would admit
|
|
8
|
+
* it. Targets can dispatch further through the same bridge.
|
|
9
|
+
*
|
|
10
|
+
* @param contractId - The contract the targets are provided under.
|
|
11
|
+
* @param tools - The target tools, by name.
|
|
12
|
+
*/
|
|
13
|
+
function targetDispatch(contractId, tools) {
|
|
14
|
+
const dispatch = (target, args, context) => provider({
|
|
15
|
+
contractId: target.contractId,
|
|
16
|
+
version: target.version,
|
|
17
|
+
member: "tool",
|
|
18
|
+
name: target.toolName,
|
|
19
|
+
operation: "run",
|
|
20
|
+
args: [args],
|
|
21
|
+
}, context);
|
|
22
|
+
const provider = (0, contract_proxy_1.createContractProvider)({ contractId, version: 1, tools, forwardCallContext: true }, { dispatchTool: dispatch });
|
|
23
|
+
return dispatch;
|
|
24
|
+
}
|
|
@@ -203,9 +203,11 @@ test("validates arguments, converts dates, and forwards original nested objects
|
|
|
203
203
|
expect(forwarded).toEqual({ ...args, when: new Date(args.when) });
|
|
204
204
|
expect(forwarded.object).toBe(object);
|
|
205
205
|
expect(forwarded).not.toHaveProperty("defaulted");
|
|
206
|
+
await expect(invoke({ ...args, defaulted: null })).resolves.toBe("ok");
|
|
207
|
+
expect(f.endpoint.handleCall.mock.calls[1][0].args[0]).not.toHaveProperty("defaulted");
|
|
206
208
|
expect(args.when).toBe("2026-10-16");
|
|
207
209
|
await expect(invoke({ ...args, when: "not-a-date" })).rejects.toThrow("argument validation");
|
|
208
|
-
expect(f.endpoint.dispose).toHaveBeenCalledTimes(
|
|
210
|
+
expect(f.endpoint.dispose).toHaveBeenCalledTimes(2);
|
|
209
211
|
});
|
|
210
212
|
test("selects strict, preferred and fallback providers and observes active changes under the same ID", async () => {
|
|
211
213
|
const f = fixture();
|
|
@@ -252,6 +254,40 @@ test("host alsoImplements validates the requested v1 schema and resolves v2 code
|
|
|
252
254
|
f.host(f.provider, 2);
|
|
253
255
|
expect((0, tool_catalog_service_1.describeToolCatalogRow)(f.dataContext, f.row).provider?.implementation?.version).toBe(2);
|
|
254
256
|
await expect((0, tool_catalog_service_1.invokeToolCatalogTool)(f.dataContext, { toolId: f.row.toolId, args: { value: "v1", new: "not a v2 number" } }, f.context)).resolves.toBe("ok");
|
|
257
|
+
// By name, the v1 row and the v2 row run the same tool: the v2 row is chosen, not "ambiguous".
|
|
258
|
+
const v2Target = { ...f.target, version: 2 };
|
|
259
|
+
const v2Row = {
|
|
260
|
+
...f.row,
|
|
261
|
+
toolId: (0, tool_catalog_identity_1.deriveToolCatalogId)(v2Target),
|
|
262
|
+
contractVersion: 2,
|
|
263
|
+
};
|
|
264
|
+
f.rows.set(v2Row.toolId, v2Row);
|
|
265
|
+
await expect((0, tool_catalog_service_1.findToolCatalogEntry)(f.dataContext, { toolIdOrName: "run", runnableOnly: true })).resolves.toMatchObject({ toolId: v2Row.toolId });
|
|
266
|
+
});
|
|
267
|
+
test("name lookup keeps aliases ambiguous when the implementing version selects another provider", async () => {
|
|
268
|
+
const f = fixture();
|
|
269
|
+
const newer = { ...f.definition, version: 2 };
|
|
270
|
+
expect(f.registry.register(f.provider, newer, [
|
|
271
|
+
{ contractId: f.target.contractId, version: 1, definition: f.definition },
|
|
272
|
+
]).valid).toBe(true);
|
|
273
|
+
f.host(f.provider, 2);
|
|
274
|
+
const other = (0, utils_1.newid)();
|
|
275
|
+
expect(f.registry.register(other, newer).valid).toBe(true);
|
|
276
|
+
f.host(other, 2);
|
|
277
|
+
expect(f.registry.setActiveProvider(f.target.contractId, 2, other)).toBe(true);
|
|
278
|
+
const v2Row = {
|
|
279
|
+
...f.row,
|
|
280
|
+
toolId: (0, tool_catalog_identity_1.deriveToolCatalogId)({ ...f.target, version: 2 }),
|
|
281
|
+
contractVersion: 2,
|
|
282
|
+
};
|
|
283
|
+
f.rows.set(v2Row.toolId, v2Row);
|
|
284
|
+
await expect((0, tool_catalog_service_1.findToolCatalogEntry)(f.dataContext, { toolIdOrName: "run", runnableOnly: true })).rejects.toThrow('Ambiguous tool "run": matched 2 tools');
|
|
285
|
+
for (const [row, provider] of [
|
|
286
|
+
[f.row, f.provider],
|
|
287
|
+
[v2Row, other],
|
|
288
|
+
]) {
|
|
289
|
+
await expect((0, tool_catalog_service_1.findToolCatalogEntry)(f.dataContext, { toolIdOrName: row.toolId, runnableOnly: true })).resolves.toMatchObject({ provider: { activePackageId: provider } });
|
|
290
|
+
}
|
|
255
291
|
});
|
|
256
292
|
test("rejects self targets, unresolved and quarantined rows, and never uses legacy invocation", async () => {
|
|
257
293
|
const f = fixture();
|
|
@@ -465,13 +465,22 @@ describe("per-context catalog lifecycle", () => {
|
|
|
465
465
|
await expect(f.publish()).rejects.toThrow(/identity cannot change/);
|
|
466
466
|
await f.catalog.dispose();
|
|
467
467
|
});
|
|
468
|
-
test(
|
|
468
|
+
test.each([
|
|
469
|
+
false,
|
|
470
|
+
true,
|
|
471
|
+
])("alsoImplements indexes only older public members, and removal retains history (carried=%s)", async (carried) => {
|
|
469
472
|
const f = fixture();
|
|
470
473
|
const provider = (0, utils_1.newid)();
|
|
471
474
|
const older = definition(["old"]);
|
|
472
475
|
const newer = definition(["old", "new"], 2);
|
|
473
|
-
|
|
474
|
-
|
|
476
|
+
if (!carried)
|
|
477
|
+
f.registry.register((0, utils_1.newid)(), older);
|
|
478
|
+
const regs = [
|
|
479
|
+
{
|
|
480
|
+
definition: newer,
|
|
481
|
+
alsoImplements: [{ contractId, version: 1, ...(carried ? { definition: older } : {}) }],
|
|
482
|
+
},
|
|
483
|
+
];
|
|
475
484
|
await f.catalog.publish(provider, regs, async () => { }, () => f.registry.replaceBatch(provider, regs));
|
|
476
485
|
expect([...f.rows.values()]
|
|
477
486
|
.filter((r) => r.contractId === contractId)
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const zod_1 = require("zod");
|
|
4
|
+
const types_1 = require("../../data/orm/types");
|
|
3
5
|
const field_type_1 = require("../../types/field-type");
|
|
4
6
|
const utils_1 = require("../../utils");
|
|
5
|
-
const
|
|
7
|
+
const types_2 = require("../types");
|
|
6
8
|
const validate_1 = require("../validate");
|
|
7
9
|
const DEFAULT_CONTRACT_ID = (0, utils_1.newid)();
|
|
8
10
|
/** Helper to build a minimal contract definition for tests. */
|
|
@@ -666,13 +668,181 @@ describe("validateImmutability", () => {
|
|
|
666
668
|
expect(result.errors[0].message).toContain("frozen");
|
|
667
669
|
});
|
|
668
670
|
});
|
|
671
|
+
describe("validateImmutability compares shapes, not text", () => {
|
|
672
|
+
const tool = (inputFields) => ({
|
|
673
|
+
name: "setMode",
|
|
674
|
+
usageDescription: "Set the mode.",
|
|
675
|
+
inputFields,
|
|
676
|
+
outputFields: [{ name: "ok", type: field_type_1.FieldType.boolean }],
|
|
677
|
+
});
|
|
678
|
+
const mode = { name: "mode", type: field_type_1.FieldType.string, enumValues: ["on", "off"] };
|
|
679
|
+
const note = { name: "note", type: field_type_1.FieldType.string, optional: true, description: "A note." };
|
|
680
|
+
const frozen = makeContract({
|
|
681
|
+
tools: [tool([mode, note])],
|
|
682
|
+
observables: [
|
|
683
|
+
{
|
|
684
|
+
name: "state",
|
|
685
|
+
description: "The state.",
|
|
686
|
+
valueType: field_type_1.FieldType.object,
|
|
687
|
+
value: { type: field_type_1.FieldType.object, fields: [{ name: "on", type: field_type_1.FieldType.boolean }] },
|
|
688
|
+
writable: false,
|
|
689
|
+
},
|
|
690
|
+
],
|
|
691
|
+
});
|
|
692
|
+
it("allows new descriptions and any member, field, or key order", () => {
|
|
693
|
+
const incoming = makeContract({
|
|
694
|
+
observables: [
|
|
695
|
+
{
|
|
696
|
+
writable: false,
|
|
697
|
+
value: { fields: [{ type: field_type_1.FieldType.boolean, name: "on" }], type: field_type_1.FieldType.object },
|
|
698
|
+
valueType: field_type_1.FieldType.object,
|
|
699
|
+
description: "Deprecated: use v2.",
|
|
700
|
+
name: "state",
|
|
701
|
+
},
|
|
702
|
+
],
|
|
703
|
+
tools: [
|
|
704
|
+
{
|
|
705
|
+
...tool([
|
|
706
|
+
{ ...note, description: "Don't use this." },
|
|
707
|
+
{ ...mode, enumValues: ["off", "on"] },
|
|
708
|
+
]),
|
|
709
|
+
usageDescription: "Deprecated: use v2.",
|
|
710
|
+
},
|
|
711
|
+
],
|
|
712
|
+
});
|
|
713
|
+
expect((0, validate_1.validateImmutability)(frozen, incoming).valid).toBe(true);
|
|
714
|
+
});
|
|
715
|
+
it("matches a shape built before fields, enumValues, and value existed", () => {
|
|
716
|
+
const older = makeContract({
|
|
717
|
+
tools: [
|
|
718
|
+
tool([
|
|
719
|
+
{ name: "mode", type: field_type_1.FieldType.string },
|
|
720
|
+
{ name: "note", type: field_type_1.FieldType.string, optional: true },
|
|
721
|
+
]),
|
|
722
|
+
],
|
|
723
|
+
observables: [
|
|
724
|
+
{ name: "state", description: "", valueType: field_type_1.FieldType.object, writable: false },
|
|
725
|
+
],
|
|
726
|
+
});
|
|
727
|
+
expect((0, validate_1.validateImmutability)(frozen, older).valid).toBe(true);
|
|
728
|
+
expect((0, validate_1.validateImmutability)(older, frozen).valid).toBe(true);
|
|
729
|
+
});
|
|
730
|
+
it("ignores duplicate enum entries when comparing sets", () => {
|
|
731
|
+
const incoming = makeContract({
|
|
732
|
+
tools: [tool([{ ...mode, enumValues: ["off", "on", "on"] }, note])],
|
|
733
|
+
observables: frozen.observables,
|
|
734
|
+
});
|
|
735
|
+
expect((0, validate_1.validateImmutability)(frozen, incoming).valid).toBe(true);
|
|
736
|
+
expect((0, validate_1.validateImmutability)(incoming, frozen).valid).toBe(true);
|
|
737
|
+
});
|
|
738
|
+
it("matches unchanged legacy default factories across schema rebuilds and JSON storage", () => {
|
|
739
|
+
const factory = jest.fn(() => "generated");
|
|
740
|
+
const legacy = () => makeContract({
|
|
741
|
+
tables: [
|
|
742
|
+
{
|
|
743
|
+
name: "Records",
|
|
744
|
+
description: "",
|
|
745
|
+
primaryKeyName: "id",
|
|
746
|
+
fields: (0, types_1.schemaToFields)(zod_1.z.object({
|
|
747
|
+
id: zod_1.z.string().default(() => factory()),
|
|
748
|
+
label: zod_1.z.string().default("untitled"),
|
|
749
|
+
})),
|
|
750
|
+
},
|
|
751
|
+
],
|
|
752
|
+
});
|
|
753
|
+
const first = legacy();
|
|
754
|
+
const rebuilt = legacy();
|
|
755
|
+
const stored = JSON.parse(JSON.stringify(first));
|
|
756
|
+
expect((0, validate_1.validateImmutability)(first, rebuilt).valid).toBe(true);
|
|
757
|
+
expect((0, validate_1.validateImmutability)(stored, rebuilt).valid).toBe(true);
|
|
758
|
+
expect((0, validate_1.validateImmutability)(rebuilt, stored).valid).toBe(true);
|
|
759
|
+
expect(factory).not.toHaveBeenCalled();
|
|
760
|
+
});
|
|
761
|
+
it("compares default objects as data, including descriptions and array order", () => {
|
|
762
|
+
const contract = (defaultValue) => makeContract({
|
|
763
|
+
tools: [tool([{ ...mode, defaultValue }])],
|
|
764
|
+
});
|
|
765
|
+
const first = contract({ description: "data", values: ["a", "b"] });
|
|
766
|
+
expect((0, validate_1.validateImmutability)(first, contract({ values: ["a", "b"], description: "data" })).valid).toBe(true);
|
|
767
|
+
expect((0, validate_1.validateImmutability)(first, contract({ description: "changed", values: ["a", "b"] })).valid).toBe(false);
|
|
768
|
+
expect((0, validate_1.validateImmutability)(first, contract({ description: "data", values: ["b", "a"] })).valid).toBe(false);
|
|
769
|
+
});
|
|
770
|
+
it.each([
|
|
771
|
+
["an enum value", tool([{ ...mode, enumValues: ["on"] }, note])],
|
|
772
|
+
["optional", tool([mode, { ...note, optional: false }])],
|
|
773
|
+
["a default value", tool([{ ...mode, defaultValue: "on" }, note])],
|
|
774
|
+
["a type", tool([mode, { ...note, type: field_type_1.FieldType.number }])],
|
|
775
|
+
])("rejects a changed %s", (_change, changed) => {
|
|
776
|
+
const result = (0, validate_1.validateImmutability)(frozen, makeContract({ tools: [changed], observables: frozen.observables }));
|
|
777
|
+
expect(result.valid).toBe(false);
|
|
778
|
+
});
|
|
779
|
+
it("rejects a changed observable value shape when both have one", () => {
|
|
780
|
+
const [state] = frozen.observables;
|
|
781
|
+
const incoming = makeContract({
|
|
782
|
+
tools: frozen.tools,
|
|
783
|
+
observables: [
|
|
784
|
+
{
|
|
785
|
+
...state,
|
|
786
|
+
value: { type: field_type_1.FieldType.object, fields: [{ name: "on", type: field_type_1.FieldType.string }] },
|
|
787
|
+
},
|
|
788
|
+
],
|
|
789
|
+
});
|
|
790
|
+
expect((0, validate_1.validateImmutability)(frozen, incoming).valid).toBe(false);
|
|
791
|
+
});
|
|
792
|
+
it("ignores a tool's policy, which isn't part of its shape", () => {
|
|
793
|
+
const tool = {
|
|
794
|
+
name: "run",
|
|
795
|
+
usageDescription: "Run.",
|
|
796
|
+
inputFields: [{ name: "id", type: field_type_1.FieldType.id }],
|
|
797
|
+
outputFields: [],
|
|
798
|
+
};
|
|
799
|
+
const frozen = makeContract({ tools: [{ ...tool, remoteAccessLevel: 40 }] });
|
|
800
|
+
const incoming = makeContract({
|
|
801
|
+
tools: [{ ...tool, remoteAccessLevel: 60, requiresWorkflowRun: true }],
|
|
802
|
+
});
|
|
803
|
+
expect((0, validate_1.validateImmutability)(frozen, incoming)).toEqual({ valid: true, errors: [] });
|
|
804
|
+
});
|
|
805
|
+
it("names what changed", () => {
|
|
806
|
+
const field = { name: "id", type: field_type_1.FieldType.id };
|
|
807
|
+
const tool = (name, inputFields = [field]) => ({
|
|
808
|
+
name,
|
|
809
|
+
usageDescription: name,
|
|
810
|
+
inputFields,
|
|
811
|
+
outputFields: [],
|
|
812
|
+
});
|
|
813
|
+
const frozen = makeContract({ tools: [tool("keep"), tool("change"), tool("drop")] });
|
|
814
|
+
const incoming = makeContract({
|
|
815
|
+
tools: [tool("keep"), tool("change", [{ ...field, type: field_type_1.FieldType.string }]), tool("new")],
|
|
816
|
+
});
|
|
817
|
+
expect((0, validate_1.contractShapeChanges)(frozen, incoming)).toEqual([
|
|
818
|
+
"tool change changed",
|
|
819
|
+
"tool drop was removed",
|
|
820
|
+
"tool new was added",
|
|
821
|
+
]);
|
|
822
|
+
expect((0, validate_1.validateImmutability)(frozen, incoming).errors[0].message).toContain("(tool change changed; tool drop was removed; tool new was added)");
|
|
823
|
+
});
|
|
824
|
+
it.each([
|
|
825
|
+
"tables",
|
|
826
|
+
"tools",
|
|
827
|
+
"observables",
|
|
828
|
+
"events",
|
|
829
|
+
])("does not hide added or changed duplicate %s behind the last member", (kind) => {
|
|
830
|
+
const member = { name: "duplicate", description: "Original" };
|
|
831
|
+
const before = makeContract({ [kind]: [member] });
|
|
832
|
+
const after = makeContract({ [kind]: [{ ...member, extra: true }, member] });
|
|
833
|
+
expect((0, validate_1.validateImmutability)(before, after).valid).toBe(false);
|
|
834
|
+
expect((0, validate_1.validateImmutability)(after, before).valid).toBe(false);
|
|
835
|
+
const changedDuplicate = makeContract({ [kind]: [{ ...member, extra: false }, member] });
|
|
836
|
+
expect((0, validate_1.validateImmutability)(after, changedDuplicate).valid).toBe(false);
|
|
837
|
+
});
|
|
838
|
+
});
|
|
669
839
|
describe("validateAlsoImplements", () => {
|
|
670
840
|
const contractStore = new Map();
|
|
671
841
|
const TASKS_CONTRACT_ID = (0, utils_1.newid)();
|
|
672
842
|
const FANCY_TASKS_CONTRACT_ID = (0, utils_1.newid)();
|
|
673
843
|
const NONEXISTENT_ID = (0, utils_1.newid)();
|
|
674
844
|
function getContract(id, ver) {
|
|
675
|
-
return contractStore.get((0,
|
|
845
|
+
return contractStore.get((0, types_2.contractKey)(id, ver));
|
|
676
846
|
}
|
|
677
847
|
beforeEach(() => {
|
|
678
848
|
contractStore.clear();
|
|
@@ -693,7 +863,7 @@ describe("validateAlsoImplements", () => {
|
|
|
693
863
|
},
|
|
694
864
|
],
|
|
695
865
|
});
|
|
696
|
-
contractStore.set((0,
|
|
866
|
+
contractStore.set((0, types_2.contractKey)(TASKS_CONTRACT_ID, 1), targetV1);
|
|
697
867
|
const impl = makeContract({
|
|
698
868
|
contractId: FANCY_TASKS_CONTRACT_ID,
|
|
699
869
|
version: 1,
|
|
@@ -745,8 +915,8 @@ describe("validateAlsoImplements", () => {
|
|
|
745
915
|
},
|
|
746
916
|
],
|
|
747
917
|
});
|
|
748
|
-
contractStore.set((0,
|
|
749
|
-
contractStore.set((0,
|
|
918
|
+
contractStore.set((0, types_2.contractKey)(TASKS_CONTRACT_ID, 1), v1);
|
|
919
|
+
contractStore.set((0, types_2.contractKey)(TASKS_CONTRACT_ID, 2), v2);
|
|
750
920
|
// Implementation satisfies both: has all fields from both versions.
|
|
751
921
|
// 'status' is optional so v1 consumers can still write valid rows.
|
|
752
922
|
const impl = makeContract({
|
|
@@ -794,8 +964,8 @@ describe("validateAlsoImplements", () => {
|
|
|
794
964
|
},
|
|
795
965
|
],
|
|
796
966
|
});
|
|
797
|
-
contractStore.set((0,
|
|
798
|
-
contractStore.set((0,
|
|
967
|
+
contractStore.set((0, types_2.contractKey)(TASKS_CONTRACT_ID, 1), v1);
|
|
968
|
+
contractStore.set((0, types_2.contractKey)(TASKS_CONTRACT_ID, 2), v2);
|
|
799
969
|
// Implementation only satisfies v1 (missing 'assignee')
|
|
800
970
|
const impl = makeContract({
|
|
801
971
|
tables: [
|
|
@@ -5,19 +5,10 @@ const contract_proxy_1 = require("../contract-proxy");
|
|
|
5
5
|
test("only the directly invoked tool receives workflow capability", async () => {
|
|
6
6
|
const workflow = { dataContextId: "group" };
|
|
7
7
|
const nested = jest.fn();
|
|
8
|
-
const action = jest.fn((_args, context) => {
|
|
9
|
-
expect(context.workflow).toBeUndefined();
|
|
10
|
-
expect(context.callContext.workflow).toBeUndefined();
|
|
11
|
-
expect(context.incomingCallContext.workflow).toBeUndefined();
|
|
12
|
-
});
|
|
13
8
|
const resolution = {
|
|
14
9
|
contractId: "contract",
|
|
15
10
|
version: 1,
|
|
16
11
|
forwardCallContext: true,
|
|
17
|
-
actionAliases: { do: "action" },
|
|
18
|
-
actionTools: {
|
|
19
|
-
action: { tool: { name: "action", usageDescription: "", accessLevel: 0 }, toolFn: action },
|
|
20
|
-
},
|
|
21
12
|
tools: {
|
|
22
13
|
run: {
|
|
23
14
|
tool: { name: "run", usageDescription: "", accessLevel: 0 },
|
|
@@ -26,7 +17,6 @@ test("only the directly invoked tool receives workflow capability", async () =>
|
|
|
26
17
|
expect(context.callContext.workflow).toBeUndefined();
|
|
27
18
|
expect(context.incomingCallContext.workflow).toBeUndefined();
|
|
28
19
|
await context.dispatchTool({ contractId: "other", version: 1, toolName: "target" }, {});
|
|
29
|
-
await context.dispatchAction("do", {});
|
|
30
20
|
},
|
|
31
21
|
},
|
|
32
22
|
},
|
|
@@ -51,7 +41,6 @@ test("only the directly invoked tool receives workflow capability", async () =>
|
|
|
51
41
|
args: [{}],
|
|
52
42
|
}, { dataContext, callerUserId: "u", workflow });
|
|
53
43
|
expect(nested.mock.calls[0][2].workflow).toBeUndefined();
|
|
54
|
-
expect(action).toHaveBeenCalled();
|
|
55
44
|
expect(() => (0, contract_proxy_1.requireWorkflowRunContext)({ dataContextId: "other", workflow }, "run")).toThrow("same data context");
|
|
56
45
|
});
|
|
57
46
|
test("workflow policy is retained by shape extraction", () => {
|
|
@@ -2,7 +2,7 @@ import type { ITableDefinition } from "../data/orm/table-definitions.type";
|
|
|
2
2
|
import type { IContractConsumer, IContractConsumerBase } from "./contract-proxy";
|
|
3
3
|
import type { IExtractableTableDef, IExtractableToolInstance } from "./extract";
|
|
4
4
|
import type { IToolCatalogMetadata } from "./tool-catalog";
|
|
5
|
-
import type { IAlsoImplementsDeclaration, IConsumedContractHandle, IConsumedContractOptions, IContractDefinition, IContractEvent, IContractObservable, IPackageDefinitionResult, IRemoteContractHandle } from "./types";
|
|
5
|
+
import type { IAlsoImplementsDeclaration, IConsumedContractHandle, IConsumedContractOptions, IContractDefinition, IContractEvent, IContractObservable, IExposedContract, IPackageDefinitionResult, IRemoteContractHandle } from "./types";
|
|
6
6
|
/**
|
|
7
7
|
* Scoped builder for a single contract within a package. Returned by
|
|
8
8
|
* `PackageBuilder.contract()`.
|
|
@@ -17,6 +17,7 @@ export declare class ContractBuilder {
|
|
|
17
17
|
private _observables;
|
|
18
18
|
private _events;
|
|
19
19
|
private _alsoImplements;
|
|
20
|
+
private _exposes;
|
|
20
21
|
private warnedAboutToolInstances;
|
|
21
22
|
/** Full table definitions for this contract (used by the install flow). */
|
|
22
23
|
private _tableDefinitions;
|
|
@@ -50,10 +51,20 @@ export declare class ContractBuilder {
|
|
|
50
51
|
from: number;
|
|
51
52
|
to: number;
|
|
52
53
|
}): void;
|
|
54
|
+
/**
|
|
55
|
+
* Expose another contract this package provides through this one: this contract's consumers
|
|
56
|
+
* may call that contract's tools on this provider without consuming it. Use it when this
|
|
57
|
+
* contract is a hub's, such as Voice Skill.
|
|
58
|
+
*
|
|
59
|
+
* @param contractId - A contract this package also provides.
|
|
60
|
+
* @param version - Its version.
|
|
61
|
+
*/
|
|
62
|
+
exposes(contractId: string, version: number): void;
|
|
53
63
|
/** @internal Build the contract definition by extracting shapes. */
|
|
54
64
|
_build(): {
|
|
55
65
|
definition: IContractDefinition;
|
|
56
66
|
alsoImplements: IAlsoImplementsDeclaration[];
|
|
67
|
+
exposes: IExposedContract[];
|
|
57
68
|
toolMetadata: IToolCatalogMetadata[];
|
|
58
69
|
tableDefinitions: ITableDefinition[];
|
|
59
70
|
};
|
|
@@ -141,8 +152,11 @@ export declare class PackageBuilder {
|
|
|
141
152
|
* @param contractId - Must be a valid peer ID (25-char alphanumeric from `newid()`).
|
|
142
153
|
* @param version - Positive integer version number.
|
|
143
154
|
* @param name - Human-readable contract name. This is the meaningful identifier that creators can change over time.
|
|
155
|
+
* @param description - What the contract is for, for people and models reading it. Hubs such
|
|
156
|
+
* as Voice Hub show it for a contract a provider exposes. A description may change at any
|
|
157
|
+
* time, also on a frozen contract.
|
|
144
158
|
*/
|
|
145
|
-
contract(contractId: string, version: number, name: string): ContractBuilder;
|
|
159
|
+
contract(contractId: string, version: number, name: string, description?: string): ContractBuilder;
|
|
146
160
|
/**
|
|
147
161
|
* Declare a dependency on another package's contract.
|
|
148
162
|
*
|
|
@@ -25,6 +25,7 @@ class ContractBuilder {
|
|
|
25
25
|
_observables = [];
|
|
26
26
|
_events = [];
|
|
27
27
|
_alsoImplements = [];
|
|
28
|
+
_exposes = [];
|
|
28
29
|
warnedAboutToolInstances = false;
|
|
29
30
|
/** Full table definitions for this contract (used by the install flow). */
|
|
30
31
|
_tableDefinitions = [];
|
|
@@ -90,6 +91,23 @@ class ContractBuilder {
|
|
|
90
91
|
}
|
|
91
92
|
this._alsoImplements.push({ contractId: targetContractId, version: versionOrRange });
|
|
92
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Expose another contract this package provides through this one: this contract's consumers
|
|
96
|
+
* may call that contract's tools on this provider without consuming it. Use it when this
|
|
97
|
+
* contract is a hub's, such as Voice Skill.
|
|
98
|
+
*
|
|
99
|
+
* @param contractId - A contract this package also provides.
|
|
100
|
+
* @param version - Its version.
|
|
101
|
+
*/
|
|
102
|
+
exposes(contractId, version) {
|
|
103
|
+
if (!(0, utils_1.isid)(contractId)) {
|
|
104
|
+
throw new Error(`exposes contractId must be a valid peer ID (25-char alphanumeric), got "${contractId}"`);
|
|
105
|
+
}
|
|
106
|
+
if (!Number.isInteger(version) || version < 1) {
|
|
107
|
+
throw new Error(`exposes version must be a positive integer, got ${version}`);
|
|
108
|
+
}
|
|
109
|
+
this._exposes.push({ contractId, version });
|
|
110
|
+
}
|
|
93
111
|
/** @internal Build the contract definition by extracting shapes. */
|
|
94
112
|
_build() {
|
|
95
113
|
return {
|
|
@@ -105,6 +123,7 @@ class ContractBuilder {
|
|
|
105
123
|
events: [...this._events],
|
|
106
124
|
},
|
|
107
125
|
alsoImplements: [...this._alsoImplements],
|
|
126
|
+
exposes: [...this._exposes],
|
|
108
127
|
toolMetadata: this._tools.flatMap(({ tool }) => {
|
|
109
128
|
const { isAssistantRunner, configSchema, detailedDescription } = tool;
|
|
110
129
|
return isAssistantRunner !== undefined ||
|
|
@@ -249,8 +268,11 @@ class PackageBuilder {
|
|
|
249
268
|
* @param contractId - Must be a valid peer ID (25-char alphanumeric from `newid()`).
|
|
250
269
|
* @param version - Positive integer version number.
|
|
251
270
|
* @param name - Human-readable contract name. This is the meaningful identifier that creators can change over time.
|
|
271
|
+
* @param description - What the contract is for, for people and models reading it. Hubs such
|
|
272
|
+
* as Voice Hub show it for a contract a provider exposes. A description may change at any
|
|
273
|
+
* time, also on a frozen contract.
|
|
252
274
|
*/
|
|
253
|
-
contract(contractId, version, name) {
|
|
275
|
+
contract(contractId, version, name, description) {
|
|
254
276
|
if (!(0, utils_1.isid)(contractId)) {
|
|
255
277
|
throw new Error(`contractId must be a valid peer ID (25-char alphanumeric), got "${contractId}"`);
|
|
256
278
|
}
|
|
@@ -261,7 +283,7 @@ class PackageBuilder {
|
|
|
261
283
|
if (existing) {
|
|
262
284
|
throw new Error(`Duplicate contract: ${contractId} v${version} is already defined in this package`);
|
|
263
285
|
}
|
|
264
|
-
const builder = new ContractBuilder(contractId, version, name);
|
|
286
|
+
const builder = new ContractBuilder(contractId, version, name, description);
|
|
265
287
|
this._contracts.push(builder);
|
|
266
288
|
return builder;
|
|
267
289
|
}
|
|
@@ -330,17 +352,35 @@ class PackageBuilder {
|
|
|
330
352
|
}
|
|
331
353
|
const contracts = [];
|
|
332
354
|
const alsoImplementsMap = new Map();
|
|
355
|
+
const exposesMap = new Map();
|
|
333
356
|
const allToolMetadata = [];
|
|
334
357
|
const allTableDefinitions = [];
|
|
335
358
|
for (const cb of this._contracts) {
|
|
336
|
-
const { definition, alsoImplements, toolMetadata, tableDefinitions } = cb._build();
|
|
359
|
+
const { definition, alsoImplements, exposes, toolMetadata, tableDefinitions } = cb._build();
|
|
337
360
|
contracts.push(definition);
|
|
338
361
|
if (alsoImplements.length > 0) {
|
|
339
362
|
alsoImplementsMap.set((0, types_1.contractKey)(cb.contractId, cb.version), alsoImplements);
|
|
340
363
|
}
|
|
364
|
+
if (exposes.length > 0) {
|
|
365
|
+
exposesMap.set((0, types_1.contractKey)(cb.contractId, cb.version), exposes);
|
|
366
|
+
}
|
|
341
367
|
allToolMetadata.push(...toolMetadata);
|
|
342
368
|
allTableDefinitions.push(...tableDefinitions);
|
|
343
369
|
}
|
|
370
|
+
const providedKeys = new Set(contracts.map((contract) => (0, types_1.contractKey)(contract.contractId, contract.version)));
|
|
371
|
+
for (const [hub, exposes] of exposesMap) {
|
|
372
|
+
const seen = new Set();
|
|
373
|
+
for (const target of exposes) {
|
|
374
|
+
const key = (0, types_1.contractKey)(target.contractId, target.version);
|
|
375
|
+
if (key === hub)
|
|
376
|
+
throw new Error(`Contract ${hub} can't expose itself`);
|
|
377
|
+
if (seen.has(key))
|
|
378
|
+
throw new Error(`Contract ${hub} exposes ${key} twice`);
|
|
379
|
+
if (!providedKeys.has(key))
|
|
380
|
+
throw new Error(`Contract ${hub} exposes ${key}, which the package doesn't provide`);
|
|
381
|
+
seen.add(key);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
344
384
|
if (contracts.length === 0 && allTableDefinitions.length > 0) {
|
|
345
385
|
throw new Error("Tables must be declared on a contract (use pkg.contract(...), then assign .tables or .tableDefinitions on that builder). UI-only packages omit contracts and do not register tables.");
|
|
346
386
|
}
|
|
@@ -351,6 +391,7 @@ class PackageBuilder {
|
|
|
351
391
|
consumes: [...this._consumes],
|
|
352
392
|
remotes: [...this._remotes],
|
|
353
393
|
alsoImplements: alsoImplementsMap,
|
|
394
|
+
exposes: exposesMap,
|
|
354
395
|
assistants: [...this.assistants],
|
|
355
396
|
appNavs: [...this.appNavs],
|
|
356
397
|
...(allToolMetadata.length ? { toolMetadata: allToolMetadata } : {}),
|