@pellux/goodvibes-contracts 1.6.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/artifacts/operator-contract.json +7590 -1633
  2. package/artifacts/operator-openapi.json +92169 -0
  3. package/artifacts/python/homeassistant_operator_client.py +630 -0
  4. package/dist/core-verbs.d.ts.map +1 -1
  5. package/dist/core-verbs.js +51 -2
  6. package/dist/generated/foundation-client-types.d.ts +943 -2
  7. package/dist/generated/foundation-client-types.d.ts.map +1 -1
  8. package/dist/generated/foundation-metadata.d.ts +3 -3
  9. package/dist/generated/foundation-metadata.js +3 -3
  10. package/dist/generated/mock-daemon-fixtures.d.ts +11 -0
  11. package/dist/generated/mock-daemon-fixtures.d.ts.map +1 -0
  12. package/dist/generated/mock-daemon-fixtures.js +16279 -0
  13. package/dist/generated/operator-contract.d.ts.map +1 -1
  14. package/dist/generated/operator-contract.js +7590 -1633
  15. package/dist/generated/operator-method-ids.d.ts +1 -1
  16. package/dist/generated/operator-method-ids.d.ts.map +1 -1
  17. package/dist/generated/operator-method-ids.js +51 -0
  18. package/dist/generated/runtime-event-domains.d.ts +1 -1
  19. package/dist/generated/runtime-event-domains.d.ts.map +1 -1
  20. package/dist/generated/runtime-event-domains.js +1 -0
  21. package/dist/generated/webui-facade.d.ts +51 -0
  22. package/dist/generated/webui-facade.d.ts.map +1 -0
  23. package/dist/generated/webui-facade.js +18609 -0
  24. package/dist/testing/conformance.d.ts +56 -0
  25. package/dist/testing/conformance.d.ts.map +1 -0
  26. package/dist/testing/conformance.js +50 -0
  27. package/dist/testing/index.d.ts +16 -0
  28. package/dist/testing/index.d.ts.map +1 -0
  29. package/dist/testing/index.js +15 -0
  30. package/dist/testing/mock-daemon.d.ts +58 -0
  31. package/dist/testing/mock-daemon.d.ts.map +1 -0
  32. package/dist/testing/mock-daemon.js +96 -0
  33. package/package.json +14 -1
@@ -0,0 +1,56 @@
1
+ /**
2
+ * testing/conformance.ts — the descriptor/handler drift gate, shipped from the
3
+ * contracts package so every consuming front-end (terminal-shell, the tui and
4
+ * agent forks, the webui) runs the SAME gate against its own composition rather
5
+ * than keeping a divergent local copy.
6
+ *
7
+ * A gateway method catalog registers DESCRIPTORS (the contract surface) and,
8
+ * separately, HANDLERS (what actually answers an invoke). A descriptor with no
9
+ * attached handler answers 501 "Gateway method is not invokable" over both
10
+ * websocket and HTTP invoke — a whole verb family can look present in the
11
+ * contract yet be dead. That is the exact regression this gate catches: run
12
+ * `assertEveryDescriptorHasHandler` against a FULLY-composed catalog in the
13
+ * consumer's test suite and it fails loudly the moment any registered
14
+ * descriptor is left handler-less.
15
+ *
16
+ * The catalog is accepted through a narrow structural view, so a consumer can
17
+ * pass its concrete GatewayMethodCatalog (or any equivalent) without the
18
+ * contracts package depending on the catalog's full type.
19
+ */
20
+ /** The minimal catalog surface this gate reads. GatewayMethodCatalog satisfies it structurally. */
21
+ export interface GatewayCatalogConformanceView {
22
+ /** Every registered method descriptor. Only the `id` field is read here. */
23
+ list(): ReadonlyArray<{
24
+ readonly id: string;
25
+ }>;
26
+ /** True when the descriptor with this id has a handler attached. */
27
+ hasHandler(id: string): boolean;
28
+ }
29
+ export interface ConformanceOptions {
30
+ /**
31
+ * Restrict the check to these descriptor ids. Use when a catalog carries
32
+ * builtin descriptors whose handlers are attached by a different layer (host
33
+ * daemon surfaces) and are legitimately absent in the composition under test.
34
+ */
35
+ readonly onlyIds?: readonly string[];
36
+ /**
37
+ * Descriptor ids allowed to have no handler — descriptors a given
38
+ * composition intentionally does not answer. Prefer `onlyIds` for the common
39
+ * case; use `ignoreIds` to carve out a small known set from a full sweep.
40
+ */
41
+ readonly ignoreIds?: readonly string[];
42
+ }
43
+ /**
44
+ * Return the sorted ids of every registered descriptor that has no attached
45
+ * handler (the 501 set), honoring `onlyIds` / `ignoreIds`. Empty means every
46
+ * checked descriptor is invokable.
47
+ */
48
+ export declare function findMethodsMissingHandlers(catalog: GatewayCatalogConformanceView, options?: ConformanceOptions): string[];
49
+ /**
50
+ * Assert every registered descriptor (honoring `onlyIds` / `ignoreIds`) has a
51
+ * handler attached. Throws with the full list of offending ids when any is
52
+ * handler-less. This is the consumer-facing gate: call it in your CI against
53
+ * the same catalog your daemon serves.
54
+ */
55
+ export declare function assertEveryDescriptorHasHandler(catalog: GatewayCatalogConformanceView, options?: ConformanceOptions): void;
56
+ //# sourceMappingURL=conformance.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conformance.d.ts","sourceRoot":"","sources":["../../src/testing/conformance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,mGAAmG;AACnG,MAAM,WAAW,6BAA6B;IAC5C,4EAA4E;IAC5E,IAAI,IAAI,aAAa,CAAC;QAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/C,oEAAoE;IACpE,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC;AAED,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,6BAA6B,EACtC,OAAO,GAAE,kBAAuB,GAC/B,MAAM,EAAE,CAUV;AAED;;;;;GAKG;AACH,wBAAgB,+BAA+B,CAC7C,OAAO,EAAE,6BAA6B,EACtC,OAAO,GAAE,kBAAuB,GAC/B,IAAI,CASN"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * testing/conformance.ts — the descriptor/handler drift gate, shipped from the
3
+ * contracts package so every consuming front-end (terminal-shell, the tui and
4
+ * agent forks, the webui) runs the SAME gate against its own composition rather
5
+ * than keeping a divergent local copy.
6
+ *
7
+ * A gateway method catalog registers DESCRIPTORS (the contract surface) and,
8
+ * separately, HANDLERS (what actually answers an invoke). A descriptor with no
9
+ * attached handler answers 501 "Gateway method is not invokable" over both
10
+ * websocket and HTTP invoke — a whole verb family can look present in the
11
+ * contract yet be dead. That is the exact regression this gate catches: run
12
+ * `assertEveryDescriptorHasHandler` against a FULLY-composed catalog in the
13
+ * consumer's test suite and it fails loudly the moment any registered
14
+ * descriptor is left handler-less.
15
+ *
16
+ * The catalog is accepted through a narrow structural view, so a consumer can
17
+ * pass its concrete GatewayMethodCatalog (or any equivalent) without the
18
+ * contracts package depending on the catalog's full type.
19
+ */
20
+ /**
21
+ * Return the sorted ids of every registered descriptor that has no attached
22
+ * handler (the 501 set), honoring `onlyIds` / `ignoreIds`. Empty means every
23
+ * checked descriptor is invokable.
24
+ */
25
+ export function findMethodsMissingHandlers(catalog, options = {}) {
26
+ const only = options.onlyIds ? new Set(options.onlyIds) : null;
27
+ const ignore = options.ignoreIds ? new Set(options.ignoreIds) : null;
28
+ return catalog
29
+ .list()
30
+ .map((descriptor) => descriptor.id)
31
+ .filter((id) => (only ? only.has(id) : true))
32
+ .filter((id) => (ignore ? !ignore.has(id) : true))
33
+ .filter((id) => !catalog.hasHandler(id))
34
+ .sort();
35
+ }
36
+ /**
37
+ * Assert every registered descriptor (honoring `onlyIds` / `ignoreIds`) has a
38
+ * handler attached. Throws with the full list of offending ids when any is
39
+ * handler-less. This is the consumer-facing gate: call it in your CI against
40
+ * the same catalog your daemon serves.
41
+ */
42
+ export function assertEveryDescriptorHasHandler(catalog, options = {}) {
43
+ const missing = findMethodsMissingHandlers(catalog, options);
44
+ if (missing.length === 0)
45
+ return;
46
+ throw new Error(`Gateway catalog has ${missing.length} descriptor(s) with no attached handler — `
47
+ + `each answers 501 "Gateway method is not invokable" over websocket and HTTP invoke. `
48
+ + `Attach handlers together with the descriptors at composition time. Offending ids:\n `
49
+ + missing.join('\n '));
50
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @pellux/goodvibes-contracts/testing — the shipped conformance kit + mock-daemon
3
+ * fixture generator, the single source consuming front-ends (terminal-shell, the
4
+ * tui and agent forks, the webui, the Home Assistant fixtures) run instead of a
5
+ * divergent local copy.
6
+ *
7
+ * - conformance: assertEveryDescriptorHasHandler / findMethodsMissingHandlers —
8
+ * the descriptor/handler drift gate, run against a fully-composed catalog.
9
+ * - mock-daemon: buildMockDaemonResponses / buildMockDaemonFixtureMap /
10
+ * createMockDaemon / sampleFromSchema — schema-valid sample responses for
11
+ * every cataloged method, generated from the contract's own JSON Schemas so
12
+ * Playwright/HA mocks are generated rather than hand-written.
13
+ */
14
+ export { assertEveryDescriptorHasHandler, findMethodsMissingHandlers, type GatewayCatalogConformanceView, type ConformanceOptions, } from './conformance.js';
15
+ export { sampleFromSchema, buildMockDaemonResponses, buildMockDaemonFixtureMap, createMockDaemon, type MockDaemonResponse, type MockDaemonFixtureMap, } from './mock-daemon.js';
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EACL,+BAA+B,EAC/B,0BAA0B,EAC1B,KAAK,6BAA6B,EAClC,KAAK,kBAAkB,GACxB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,gBAAgB,EAChB,wBAAwB,EACxB,yBAAyB,EACzB,gBAAgB,EAChB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,GAC1B,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @pellux/goodvibes-contracts/testing — the shipped conformance kit + mock-daemon
3
+ * fixture generator, the single source consuming front-ends (terminal-shell, the
4
+ * tui and agent forks, the webui, the Home Assistant fixtures) run instead of a
5
+ * divergent local copy.
6
+ *
7
+ * - conformance: assertEveryDescriptorHasHandler / findMethodsMissingHandlers —
8
+ * the descriptor/handler drift gate, run against a fully-composed catalog.
9
+ * - mock-daemon: buildMockDaemonResponses / buildMockDaemonFixtureMap /
10
+ * createMockDaemon / sampleFromSchema — schema-valid sample responses for
11
+ * every cataloged method, generated from the contract's own JSON Schemas so
12
+ * Playwright/HA mocks are generated rather than hand-written.
13
+ */
14
+ export { assertEveryDescriptorHasHandler, findMethodsMissingHandlers, } from './conformance.js';
15
+ export { sampleFromSchema, buildMockDaemonResponses, buildMockDaemonFixtureMap, createMockDaemon, } from './mock-daemon.js';
@@ -0,0 +1,58 @@
1
+ /**
2
+ * testing/mock-daemon.ts — generate schema-valid sample responses for every
3
+ * cataloged operator method, straight from the contract's own JSON Schemas.
4
+ *
5
+ * Consumers (the webui Playwright suite, the Home Assistant test fixtures) have
6
+ * historically hand-written a mock response per method their UI calls, which
7
+ * drifts silently the moment a method's output schema changes. This generator
8
+ * removes the hand-authoring: given the operator contract manifest, it walks
9
+ * each method's `outputSchema` and produces a minimal, deterministic,
10
+ * schema-valid body — the single source those mocks can be generated from.
11
+ *
12
+ * The walk is deterministic (no randomness, no clock) so a regenerated fixture
13
+ * set is byte-identical unless the contract itself changed — the property that
14
+ * lets a checked-in fixture artifact carry a drift check.
15
+ *
16
+ * Scope: this understands the JSON Schema subset the contract generator emits —
17
+ * object (properties/required), array (items), string (enum), number/integer,
18
+ * boolean, null, and anyOf unions. It is a fixture generator, not a full JSON
19
+ * Schema materializer; an unrecognized shape yields null rather than throwing,
20
+ * so a new schema keyword degrades to a still-valid (if minimal) sample.
21
+ */
22
+ import type { JsonSchema, OperatorContractManifest } from '../types.js';
23
+ /**
24
+ * Produce a minimal, deterministic value that satisfies `schema`. Fills every
25
+ * declared object property (required and optional) so the sample exercises the
26
+ * full shape a consumer's mock renders.
27
+ */
28
+ export declare function sampleFromSchema(schema: JsonSchema | undefined): unknown;
29
+ /** One generated mock response: what a daemon would answer for a method. */
30
+ export interface MockDaemonResponse {
31
+ readonly methodId: string;
32
+ readonly http: {
33
+ readonly method: string;
34
+ readonly path: string;
35
+ } | null;
36
+ readonly status: number;
37
+ readonly body: unknown;
38
+ }
39
+ /**
40
+ * Build a schema-valid sample response for every method in the contract, in
41
+ * catalog order. A method with no `outputSchema` yields a `null` body (honest:
42
+ * the contract declares no shape to sample).
43
+ */
44
+ export declare function buildMockDaemonResponses(contract: OperatorContractManifest): MockDaemonResponse[];
45
+ /** A methodId -> sample response map, the fixture shape most consumers want. */
46
+ export type MockDaemonFixtureMap = Readonly<Record<string, MockDaemonResponse>>;
47
+ /** Reduce the response list to a methodId-keyed map. */
48
+ export declare function buildMockDaemonFixtureMap(contract: OperatorContractManifest): MockDaemonFixtureMap;
49
+ /**
50
+ * A tiny in-memory mock daemon over the generated fixtures: answer by methodId
51
+ * or by HTTP method+path. Returns null for an unknown method/route so a caller
52
+ * can fall through to its own 404, exactly like a real dispatcher.
53
+ */
54
+ export declare function createMockDaemon(contract: OperatorContractManifest): {
55
+ answer(methodId: string): MockDaemonResponse | null;
56
+ answerHttp(method: string, path: string): MockDaemonResponse | null;
57
+ };
58
+ //# sourceMappingURL=mock-daemon.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mock-daemon.d.ts","sourceRoot":"","sources":["../../src/testing/mock-daemon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAQxE;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS,GAAG,OAAO,CAoDxE;AAED,4EAA4E;AAC5E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACzE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,wBAAwB,GAAG,kBAAkB,EAAE,CAOjG;AAED,gFAAgF;AAChF,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC,CAAC;AAEhF,wDAAwD;AACxD,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,wBAAwB,GAAG,oBAAoB,CAMlG;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,wBAAwB,GAAG;IACpE,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CAAC;IACpD,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CAAC;CACrE,CAYA"}
@@ -0,0 +1,96 @@
1
+ function asRecord(value) {
2
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
3
+ ? value
4
+ : null;
5
+ }
6
+ /**
7
+ * Produce a minimal, deterministic value that satisfies `schema`. Fills every
8
+ * declared object property (required and optional) so the sample exercises the
9
+ * full shape a consumer's mock renders.
10
+ */
11
+ export function sampleFromSchema(schema) {
12
+ if (!schema)
13
+ return null;
14
+ const record = asRecord(schema);
15
+ if (!record)
16
+ return null;
17
+ // anyOf union: take the first branch that is not a bare `null` type, so the
18
+ // sample carries a representative value rather than degenerating to null.
19
+ const anyOf = record['anyOf'];
20
+ if (Array.isArray(anyOf) && anyOf.length > 0) {
21
+ const branches = anyOf;
22
+ const preferred = branches.find((branch) => asRecord(branch)?.['type'] !== 'null') ?? branches[0];
23
+ return sampleFromSchema(preferred);
24
+ }
25
+ const enumValues = record['enum'];
26
+ if (Array.isArray(enumValues) && enumValues.length > 0) {
27
+ return enumValues[0];
28
+ }
29
+ const type = record['type'];
30
+ if (type === 'object' || (type === undefined && asRecord(record['properties']))) {
31
+ const properties = asRecord(record['properties']) ?? {};
32
+ const out = {};
33
+ for (const [key, propSchema] of Object.entries(properties)) {
34
+ out[key] = sampleFromSchema(propSchema);
35
+ }
36
+ return out;
37
+ }
38
+ if (type === 'array') {
39
+ const items = record['items'];
40
+ if (items && !Array.isArray(items)) {
41
+ return [sampleFromSchema(items)];
42
+ }
43
+ return [];
44
+ }
45
+ switch (type) {
46
+ case 'string':
47
+ return 'sample';
48
+ case 'number':
49
+ case 'integer':
50
+ return 0;
51
+ case 'boolean':
52
+ return false;
53
+ case 'null':
54
+ return null;
55
+ default:
56
+ // Unknown / absent type with no properties: JSON Schema's "accept anything".
57
+ return null;
58
+ }
59
+ }
60
+ /**
61
+ * Build a schema-valid sample response for every method in the contract, in
62
+ * catalog order. A method with no `outputSchema` yields a `null` body (honest:
63
+ * the contract declares no shape to sample).
64
+ */
65
+ export function buildMockDaemonResponses(contract) {
66
+ return contract.operator.methods.map((method) => ({
67
+ methodId: method.id,
68
+ http: method.http ? { method: method.http.method, path: method.http.path } : null,
69
+ status: 200,
70
+ body: sampleFromSchema(method.outputSchema),
71
+ }));
72
+ }
73
+ /** Reduce the response list to a methodId-keyed map. */
74
+ export function buildMockDaemonFixtureMap(contract) {
75
+ const map = {};
76
+ for (const response of buildMockDaemonResponses(contract)) {
77
+ map[response.methodId] = response;
78
+ }
79
+ return map;
80
+ }
81
+ /**
82
+ * A tiny in-memory mock daemon over the generated fixtures: answer by methodId
83
+ * or by HTTP method+path. Returns null for an unknown method/route so a caller
84
+ * can fall through to its own 404, exactly like a real dispatcher.
85
+ */
86
+ export function createMockDaemon(contract) {
87
+ const responses = buildMockDaemonResponses(contract);
88
+ const byId = new Map(responses.map((response) => [response.methodId, response]));
89
+ const byHttp = new Map(responses
90
+ .filter((response) => response.http)
91
+ .map((response) => [`${response.http.method} ${response.http.path}`, response]));
92
+ return {
93
+ answer: (methodId) => byId.get(methodId) ?? null,
94
+ answerHttp: (method, path) => byHttp.get(`${method.toUpperCase()} ${path}`) ?? null,
95
+ };
96
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-contracts",
3
- "version": "1.6.1",
3
+ "version": "1.7.0",
4
4
  "engines": {
5
5
  "bun": "1.3.10",
6
6
  "node": ">=22.0.0"
@@ -14,10 +14,18 @@
14
14
  "types": "./dist/index.d.ts",
15
15
  "import": "./dist/index.js"
16
16
  },
17
+ "./testing": {
18
+ "types": "./dist/testing/index.d.ts",
19
+ "import": "./dist/testing/index.js"
20
+ },
17
21
  "./generated/foundation-client-types": {
18
22
  "types": "./dist/generated/foundation-client-types.d.ts",
19
23
  "import": "./dist/generated/foundation-client-types.js"
20
24
  },
25
+ "./generated/mock-daemon-fixtures": {
26
+ "types": "./dist/generated/mock-daemon-fixtures.d.ts",
27
+ "import": "./dist/generated/mock-daemon-fixtures.js"
28
+ },
21
29
  "./generated/foundation-metadata": {
22
30
  "types": "./dist/generated/foundation-metadata.d.ts",
23
31
  "import": "./dist/generated/foundation-metadata.js"
@@ -42,6 +50,10 @@
42
50
  "types": "./dist/generated/runtime-event-domains.d.ts",
43
51
  "import": "./dist/generated/runtime-event-domains.js"
44
52
  },
53
+ "./generated/webui-facade": {
54
+ "types": "./dist/generated/webui-facade.d.ts",
55
+ "import": "./dist/generated/webui-facade.js"
56
+ },
45
57
  "./node": {
46
58
  "types": "./dist/node.d.ts",
47
59
  "import": "./dist/node.js"
@@ -71,6 +83,7 @@
71
83
  "import": "./dist/zod-schemas/session.js"
72
84
  },
73
85
  "./operator-contract.json": "./artifacts/operator-contract.json",
86
+ "./operator-openapi.json": "./artifacts/operator-openapi.json",
74
87
  "./peer-contract.json": "./artifacts/peer-contract.json",
75
88
  "./package.json": "./package.json"
76
89
  },