@fougere/testing 0.3.0-alpha.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 (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +43 -0
  3. package/dist/all.d.ts +33 -0
  4. package/dist/all.d.ts.map +1 -0
  5. package/dist/all.js +47 -0
  6. package/dist/all.js.map +1 -0
  7. package/dist/app.d.ts +68 -0
  8. package/dist/app.d.ts.map +1 -0
  9. package/dist/app.js +65 -0
  10. package/dist/app.js.map +1 -0
  11. package/dist/comparison.d.ts +43 -0
  12. package/dist/comparison.d.ts.map +1 -0
  13. package/dist/comparison.js +182 -0
  14. package/dist/comparison.js.map +1 -0
  15. package/dist/derive.d.ts +12 -0
  16. package/dist/derive.d.ts.map +1 -0
  17. package/dist/derive.js +13 -0
  18. package/dist/derive.js.map +1 -0
  19. package/dist/doors.d.ts +37 -0
  20. package/dist/doors.d.ts.map +1 -0
  21. package/dist/doors.js +76 -0
  22. package/dist/doors.js.map +1 -0
  23. package/dist/gql.d.ts +67 -0
  24. package/dist/gql.d.ts.map +1 -0
  25. package/dist/gql.js +113 -0
  26. package/dist/gql.js.map +1 -0
  27. package/dist/index.d.ts +14 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +15 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/load.d.ts +34 -0
  32. package/dist/load.d.ts.map +1 -0
  33. package/dist/load.js +92 -0
  34. package/dist/load.js.map +1 -0
  35. package/dist/remotes.d.ts +49 -0
  36. package/dist/remotes.d.ts.map +1 -0
  37. package/dist/remotes.js +121 -0
  38. package/dist/remotes.js.map +1 -0
  39. package/dist/sample.d.ts +22 -0
  40. package/dist/sample.d.ts.map +1 -0
  41. package/dist/sample.js +61 -0
  42. package/dist/sample.js.map +1 -0
  43. package/dist/scope.d.ts +49 -0
  44. package/dist/scope.d.ts.map +1 -0
  45. package/dist/scope.js +62 -0
  46. package/dist/scope.js.map +1 -0
  47. package/dist/stub.d.ts +44 -0
  48. package/dist/stub.d.ts.map +1 -0
  49. package/dist/stub.js +71 -0
  50. package/dist/stub.js.map +1 -0
  51. package/dist/sync.d.ts +46 -0
  52. package/dist/sync.d.ts.map +1 -0
  53. package/dist/sync.js +79 -0
  54. package/dist/sync.js.map +1 -0
  55. package/dist/vitest.d.ts +22 -0
  56. package/dist/vitest.d.ts.map +1 -0
  57. package/dist/vitest.js +49 -0
  58. package/dist/vitest.js.map +1 -0
  59. package/package.json +68 -0
package/dist/doors.js ADDED
@@ -0,0 +1,76 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { createLocalRunner, validationErrorsOf } from '@fougere/core';
3
+ import { EMPTY_INVOCATION } from '@fougere/core/contract';
4
+ import { outputFields, registrationKeyOf } from '@fougere/schema';
5
+ import { holds } from '@fougere/schema';
6
+ import { derivedCases } from './derive.js';
7
+ import { sampleInput, replaySeed } from './sample.js';
8
+ /**
9
+ * What a door answers, in the shape a verdict is compared in.
10
+ *
11
+ * A refusal reaches a caller as a thrown `FougereError` carrying `details`, not as a
12
+ * returned value — so the translation happens once, here, and every reader below compares
13
+ * the same thing.
14
+ */
15
+ export async function verdictOf(call) {
16
+ try {
17
+ await call();
18
+ return { success: true };
19
+ }
20
+ catch (error) {
21
+ const refusals = validationErrorsOf(error);
22
+ if (!refusals)
23
+ throw error;
24
+ return { success: false, errors: refusals };
25
+ }
26
+ }
27
+ function opsFor(entity) {
28
+ const name = registrationKeyOf(entity.name ?? '');
29
+ return { name, create: 'create', update: 'update' };
30
+ }
31
+ /**
32
+ * The declared contract, posed to the façade that will receive it.
33
+ *
34
+ * Declares one `it` per case rather than looping inside a single one: a failure names the
35
+ * case, and a suite that lists what it checked is the point — the list comes from the
36
+ * entity, not from what the author remembered.
37
+ */
38
+ export function checkContract(app, entity, options = {}) {
39
+ const { name, create, update } = opsFor(entity);
40
+ const run = createLocalRunner(app);
41
+ const table = derivedCases(entity, options.given ?? {}, options);
42
+ describe(`${entity.name} — the contract it declares`, () => {
43
+ for (const one of table) {
44
+ it(one.why, async () => {
45
+ const verdict = await verdictOf(() => run({ entity: name, op: one.patch ? update : create }, { ...EMPTY_INVOCATION, params: one.patch ? { id: '__absent__' } : {}, body: one.body }));
46
+ expect(holds(one.expect, verdict), `${JSON.stringify(verdict)} — replay: ${replaySeed()}`).toBe(true);
47
+ });
48
+ }
49
+ });
50
+ }
51
+ /**
52
+ * What may leave, checked against what the entity says may leave.
53
+ *
54
+ * Costs nothing to state: `outputFields` already answers it, and a field the boundary
55
+ * closes has no business in any response. A `password: text({ boundary: 'writeOnly' })`
56
+ * that reaches a caller is the one leak no reviewer catches by reading a handler.
57
+ */
58
+ export function checkOutput(app, entity, options = {}) {
59
+ const { name, create } = opsFor(entity);
60
+ const run = createLocalRunner(app);
61
+ const allowed = new Set(Object.keys(outputFields(entity.getFields())));
62
+ const closed = Object.keys(entity.getFields()).filter((field) => !allowed.has(field));
63
+ describe(`${entity.name} — what leaves it`, () => {
64
+ it(closed.length > 0 ? `keeps ${closed.join(', ')} in` : 'closes no field, and says so', async () => {
65
+ const row = await run({ entity: name, op: create }, { ...EMPTY_INVOCATION, body: sampleInput(entity, options.given ?? {}, options) });
66
+ expect(Object.keys(row).filter((field) => closed.includes(field))).toEqual([]);
67
+ });
68
+ it('answers with fields the entity declares, and no others', async () => {
69
+ const row = await run({ entity: name, op: create }, { ...EMPTY_INVOCATION, body: sampleInput(entity, options.given ?? {}, options) });
70
+ // A computed field from a presenter is declared by the presenter, not the entity,
71
+ // so this reads the entity's own names as a floor rather than a ceiling.
72
+ expect(Object.keys(row).every((field) => field in entity.getFields())).toBe(true);
73
+ });
74
+ });
75
+ }
76
+ //# sourceMappingURL=doors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doors.js","sourceRoot":"","sources":["../src/doors.ts"],"names":[],"mappings":"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"}
package/dist/gql.d.ts ADDED
@@ -0,0 +1,67 @@
1
+ import { type SchemaView } from '@fougere/schema';
2
+ /** One field of a root type, in the shape this file reads it. */
3
+ interface RootField {
4
+ name: string;
5
+ type: {
6
+ toString(): string;
7
+ };
8
+ args: {
9
+ name: string;
10
+ }[];
11
+ }
12
+ /** The minimum of a GraphQL schema this file reads — no `graphql` import, no second copy. */
13
+ interface Introspectable {
14
+ getQueryType(): {
15
+ getFields(): Record<string, RootField>;
16
+ } | null | undefined;
17
+ getMutationType?(): {
18
+ getFields(): Record<string, RootField>;
19
+ } | null | undefined;
20
+ }
21
+ /**
22
+ * The scalar fields of an entity, as a GraphQL selection.
23
+ *
24
+ * Relations are left out: they resolve to an object, so naming one without a sub-selection
25
+ * is a syntax error, and following it would compare a neighbour's rows rather than these.
26
+ */
27
+ export declare function selectionOf(entity: SchemaView): string;
28
+ /**
29
+ * The Query field that answers an operation, asked of the schema rather than recomputed.
30
+ *
31
+ * `pluralize` is written privately in `adapter/rest/src/routes.ts` AND in
32
+ * `adapter/graphql/src/pothos.ts`; a third copy here would be the one that drifts. The
33
+ * schema already states the answer, so it is read: a list is the field whose type is the
34
+ * entity's list type, and a find is the field of the entity's own type that takes an id.
35
+ */
36
+ export declare function queryFieldFor(schema: Introspectable, entity: SchemaView, op: 'list' | 'findById'): string | undefined;
37
+ /** A `list` query, and the path at which its rows sit in the answer. */
38
+ export declare function listQuery(schema: Introspectable, entity: SchemaView): {
39
+ query: string;
40
+ at: string[];
41
+ } | undefined;
42
+ /** A `findById` query for one row. */
43
+ export declare function findQuery(schema: Introspectable, entity: SchemaView, id: string): {
44
+ query: string;
45
+ at: string[];
46
+ } | undefined;
47
+ /** Follow a path into a GraphQL answer, tolerating an absence rather than throwing. */
48
+ export declare function at(data: unknown, path: string[]): unknown;
49
+ /**
50
+ * The Mutation field that answers an operation.
51
+ *
52
+ * By NAME here, unlike the Query side: `createProduct` and `quote` both take a single
53
+ * `input` argument and both return `Product!`, so their shapes do not separate them. Two
54
+ * candidates are tried against the real fields — `<op><Entity>` for a CRUD write, and the
55
+ * bare op for a custom one — which needs no pluralization and invents nothing.
56
+ */
57
+ export declare function mutationFieldFor(schema: Introspectable, entity: SchemaView, op: string): string | undefined;
58
+ /** A mutation, with the arguments its operation takes and where its answer sits. */
59
+ export declare function mutationFor(schema: Introspectable, entity: SchemaView, op: string, input: {
60
+ id?: string;
61
+ body?: Record<string, unknown>;
62
+ }): {
63
+ query: string;
64
+ at: string[];
65
+ } | undefined;
66
+ export {};
67
+ //# sourceMappingURL=gql.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gql.d.ts","sourceRoot":"","sources":["../src/gql.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2C,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE3F,iEAAiE;AACjE,UAAU,SAAS;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE;QAAE,QAAQ,IAAI,MAAM,CAAA;KAAE,CAAC;IAC7B,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC1B;AAED,6FAA6F;AAC7F,UAAU,cAAc;IACtB,YAAY,IAAI;QAAE,SAAS,IAAI,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;KAAE,GAAG,IAAI,GAAG,SAAS,CAAC;IAC9E,eAAe,CAAC,IAAI;QAAE,SAAS,IAAI,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;KAAE,GAAG,IAAI,GAAG,SAAS,CAAC;CACnF;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAKtD;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE,UAAU,EAClB,EAAE,EAAE,MAAM,GAAG,UAAU,GACtB,MAAM,GAAG,SAAS,CAapB;AAED,wEAAwE;AACxE,wBAAgB,SAAS,CAAC,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,UAAU,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,SAAS,CAMjH;AAED,sCAAsC;AACtC,wBAAgB,SAAS,CAAC,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,SAAS,CAI7H;AAED,uFAAuF;AACvF,wBAAgB,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAEzD;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAI3G;AAED,oFAAoF;AACpF,wBAAgB,WAAW,CACzB,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE,UAAU,EAClB,EAAE,EAAE,MAAM,EACV,KAAK,EAAE;IAAE,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GACrD;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,SAAS,CAa7C"}
package/dist/gql.js ADDED
@@ -0,0 +1,113 @@
1
+ import { Anatomy, Role, outputFields } from '@fougere/schema';
2
+ /**
3
+ * The scalar fields of an entity, as a GraphQL selection.
4
+ *
5
+ * Relations are left out: they resolve to an object, so naming one without a sub-selection
6
+ * is a syntax error, and following it would compare a neighbour's rows rather than these.
7
+ */
8
+ export function selectionOf(entity) {
9
+ return Object.entries(outputFields(entity.getFields()))
10
+ .filter(([, field]) => !Role.of(field).relation)
11
+ .map(([name]) => name)
12
+ .join(' ');
13
+ }
14
+ /**
15
+ * The Query field that answers an operation, asked of the schema rather than recomputed.
16
+ *
17
+ * `pluralize` is written privately in `adapter/rest/src/routes.ts` AND in
18
+ * `adapter/graphql/src/pothos.ts`; a third copy here would be the one that drifts. The
19
+ * schema already states the answer, so it is read: a list is the field whose type is the
20
+ * entity's list type, and a find is the field of the entity's own type that takes an id.
21
+ */
22
+ export function queryFieldFor(schema, entity, op) {
23
+ const fields = schema.getQueryType()?.getFields() ?? {};
24
+ const wanted = op === 'list' ? `${entity.name}List` : entity.name;
25
+ for (const field of Object.values(fields)) {
26
+ // `String(type)` gives the name with its wrappers (`Product!`, `[Product!]!`), which
27
+ // is why this compares on inclusion rather than equality.
28
+ const named = String(field.type).replace(/[![\]]/g, '');
29
+ if (named !== wanted)
30
+ continue;
31
+ const takesId = field.args.some((arg) => arg.name === 'id');
32
+ if (op === 'findById' ? takesId : !takesId)
33
+ return field.name;
34
+ }
35
+ return undefined;
36
+ }
37
+ /** A `list` query, and the path at which its rows sit in the answer. */
38
+ export function listQuery(schema, entity) {
39
+ const field = queryFieldFor(schema, entity, 'list');
40
+ if (!field)
41
+ return undefined;
42
+ // The list type wraps its rows — `ProductList { items }` — so the reader below has to
43
+ // be told where to look rather than assume the answer IS the rows.
44
+ return { query: `{ ${field} { items { ${selectionOf(entity)} } } }`, at: [field, 'items'] };
45
+ }
46
+ /** A `findById` query for one row. */
47
+ export function findQuery(schema, entity, id) {
48
+ const field = queryFieldFor(schema, entity, 'findById');
49
+ if (!field)
50
+ return undefined;
51
+ return { query: `{ ${field}(id: ${JSON.stringify(id)}) { ${selectionOf(entity)} } }`, at: [field] };
52
+ }
53
+ /** Follow a path into a GraphQL answer, tolerating an absence rather than throwing. */
54
+ export function at(data, path) {
55
+ return path.reduce((value, key) => value?.[key], data);
56
+ }
57
+ /**
58
+ * The Mutation field that answers an operation.
59
+ *
60
+ * By NAME here, unlike the Query side: `createProduct` and `quote` both take a single
61
+ * `input` argument and both return `Product!`, so their shapes do not separate them. Two
62
+ * candidates are tried against the real fields — `<op><Entity>` for a CRUD write, and the
63
+ * bare op for a custom one — which needs no pluralization and invents nothing.
64
+ */
65
+ export function mutationFieldFor(schema, entity, op) {
66
+ const fields = schema.getMutationType?.()?.getFields() ?? {};
67
+ const candidates = [`${op}${entity.name}`, op];
68
+ return candidates.find((name) => name in fields);
69
+ }
70
+ /** A mutation, with the arguments its operation takes and where its answer sits. */
71
+ export function mutationFor(schema, entity, op, input) {
72
+ const field = mutationFieldFor(schema, entity, op);
73
+ if (!field)
74
+ return undefined;
75
+ const args = [];
76
+ if (input.id !== undefined)
77
+ args.push(`id: ${JSON.stringify(input.id)}`);
78
+ if (input.body !== undefined)
79
+ args.push(`input: ${literalOf(input.body, enumsOf(entity))}`);
80
+ const call = args.length ? `${field}(${args.join(', ')})` : field;
81
+ // `delete` answers a Boolean, which takes no sub-selection — asking for one is a syntax
82
+ // error, and the schema is what says which case this is.
83
+ const scalar = String(schema.getMutationType?.()?.getFields()[field]?.type ?? '').replace(/[!]/g, '') === 'Boolean';
84
+ return { query: `mutation { ${call}${scalar ? '' : ` { ${selectionOf(entity)} }`} }`, at: [field] };
85
+ }
86
+ /** The fields the entity declares as a bounded set — GraphQL turns each into an enum. */
87
+ function enumsOf(entity) {
88
+ return new Set(Object.entries(entity.getFields())
89
+ .filter(([, field]) => { const base = Anatomy.of(field.shape).base; return base?.type === 'string' && Array.isArray(base.enum); })
90
+ .map(([name]) => name));
91
+ }
92
+ /**
93
+ * A JS value as a GraphQL literal.
94
+ *
95
+ * `JSON.stringify` is not it, twice over: an input object's keys are NAMES, so
96
+ * `{"sku": "x"}` is a syntax error where `{sku: "x"}` is the value — and an ENUM value is
97
+ * a name too, so `status: "draft"` is refused where `status: draft` is taken. Which
98
+ * fields are enums is read from the entity (`shape.enum`), not guessed from the string.
99
+ */
100
+ function literalOf(value, enums = new Set(), key) {
101
+ if (value === null)
102
+ return 'null';
103
+ if (key !== undefined && enums.has(key) && typeof value === 'string')
104
+ return value;
105
+ if (Array.isArray(value))
106
+ return `[${value.map((one) => literalOf(one, enums, key)).join(', ')}]`;
107
+ if (typeof value === 'object') {
108
+ return `{${Object.entries(value)
109
+ .map(([name, one]) => `${name}: ${literalOf(one, enums, name)}`).join(', ')}}`;
110
+ }
111
+ return JSON.stringify(value);
112
+ }
113
+ //# sourceMappingURL=gql.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gql.js","sourceRoot":"","sources":["../src/gql.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ export { sampleInput, replaySeed, type SampleOptions } from './sample.js';
2
+ export { derivedCases } from './derive.js';
3
+ export { casesFor, holds, refusalBranches, type Case } from '@fougere/schema';
4
+ export { testApp, type TestAppOptions, type TestApp } from './app.js';
5
+ export { checkContract, checkOutput, verdictOf, type Verdict, type CheckOptions } from './doors.js';
6
+ export { stubOf, methodsOf, installStubs, type Port, type Stub } from './stub.js';
7
+ export { scopeOf, scopeOfRun, frondOf, rootOf, type Scope } from './scope.js';
8
+ export { loadScript, reachableOps, type LoadOptions } from './load.js';
9
+ export { checkDoorContract, checkDoors, type DoorContractCase, type DoorInput, type DoorOptions, } from './comparison.js';
10
+ export { selectionOf, queryFieldFor, mutationFieldFor, listQuery, findQuery, mutationFor, at } from './gql.js';
11
+ export { driftOf, agrees, explain, type CardDrift } from './remotes.js';
12
+ export { checkAll, servedEntities, type CheckAllOptions } from './all.js';
13
+ export { syncedRemotes, heldShapes, syncDriftOf, inSync, type SyncedRemote, type SyncDrift } from './sync.js';
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAC1E,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,eAAe,EAAE,KAAK,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAC9E,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,OAAO,EAAE,MAAM,UAAU,CAAC;AACtE,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,SAAS,EAAE,KAAK,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AACpG,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,KAAK,IAAI,EAAE,KAAK,IAAI,EAAE,MAAM,WAAW,CAAC;AAClF,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,KAAK,EAAE,MAAM,YAAY,CAAC;AAC9E,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AACvE,OAAO,EACL,iBAAiB,EACjB,UAAU,EACV,KAAK,gBAAgB,EACrB,KAAK,SAAS,EACd,KAAK,WAAW,GACjB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,gBAAgB,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,UAAU,CAAC;AAC/G,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AACxE,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,UAAU,CAAC;AAC1E,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,KAAK,YAAY,EAAE,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ export { sampleInput, replaySeed } from './sample.js';
2
+ export { derivedCases } from './derive.js';
3
+ // The derivation itself lives with the axes it reads.
4
+ export { casesFor, holds, refusalBranches } from '@fougere/schema';
5
+ export { testApp } from './app.js';
6
+ export { checkContract, checkOutput, verdictOf } from './doors.js';
7
+ export { stubOf, methodsOf, installStubs } from './stub.js';
8
+ export { scopeOf, scopeOfRun, frondOf, rootOf } from './scope.js';
9
+ export { loadScript, reachableOps } from './load.js';
10
+ export { checkDoorContract, checkDoors, } from './comparison.js';
11
+ export { selectionOf, queryFieldFor, mutationFieldFor, listQuery, findQuery, mutationFor, at } from './gql.js';
12
+ export { driftOf, agrees, explain } from './remotes.js';
13
+ export { checkAll, servedEntities } from './all.js';
14
+ export { syncedRemotes, heldShapes, syncDriftOf, inSync } from './sync.js';
15
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAsB,MAAM,aAAa,CAAC;AAC1E,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,sDAAsD;AACtD,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,eAAe,EAAa,MAAM,iBAAiB,CAAC;AAC9E,OAAO,EAAE,OAAO,EAAqC,MAAM,UAAU,CAAC;AACtE,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,SAAS,EAAmC,MAAM,YAAY,CAAC;AACpG,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,YAAY,EAAwB,MAAM,WAAW,CAAC;AAClF,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,EAAc,MAAM,YAAY,CAAC;AAC9E,OAAO,EAAE,UAAU,EAAE,YAAY,EAAoB,MAAM,WAAW,CAAC;AACvE,OAAO,EACL,iBAAiB,EACjB,UAAU,GAIX,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,gBAAgB,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,UAAU,CAAC;AAC/G,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAkB,MAAM,cAAc,CAAC;AACxE,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAwB,MAAM,UAAU,CAAC;AAC1E,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,EAAqC,MAAM,WAAW,CAAC"}
package/dist/load.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ import type { App } from '@fougere/core';
2
+ export interface LoadOptions {
3
+ /** Where the calls go. The RPC door of a running app. */
4
+ door?: string;
5
+ /** Values the generator cannot invent, by entity name — the id a `ref()` points at. */
6
+ given?: Record<string, Record<string, unknown>>;
7
+ }
8
+ interface Reachable {
9
+ method: string;
10
+ body: unknown;
11
+ }
12
+ /**
13
+ * Every operation the app answers, with a body for those that take one.
14
+ *
15
+ * Enumerated rather than chosen, and that is the whole point: a scenario written by hand
16
+ * holds the operations its author thought of — `demos/observability/load.js` exercises
17
+ * three — while this one holds all of them, and an operation given `weight: 0` becomes a
18
+ * decision visible in the file instead of an omission nobody can see.
19
+ */
20
+ export declare function reachableOps(app: App, given?: LoadOptions['given']): Reachable[];
21
+ /**
22
+ * A k6 scenario, written from what the app answers.
23
+ *
24
+ * k6 runs on its own runtime and cannot import from here, so the file is GENERATED rather
25
+ * than made to import `frameCall` — but the envelope in it comes from `frameCall` itself,
26
+ * called once at generation time. `demos/observability/load.js` spells that envelope by
27
+ * hand, so it will go on claiming to be JSON-RPC the day the format moves.
28
+ *
29
+ * What stays the author's: the weights, the stages and the thresholds. The scan knows
30
+ * which operations exist; it knows nothing about the traffic they receive.
31
+ */
32
+ export declare function loadScript(app: App, options?: LoadOptions): string;
33
+ export {};
34
+ //# sourceMappingURL=load.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../src/load.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AAIzC,MAAM,WAAW,WAAW;IAC1B,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uFAAuF;IACvF,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACjD;AAED,UAAU,SAAS;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;CACf;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,GAAE,WAAW,CAAC,OAAO,CAAM,GAAG,SAAS,EAAE,CAiBpF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,GAAE,WAAgB,GAAG,MAAM,CAmDtE"}
package/dist/load.js ADDED
@@ -0,0 +1,92 @@
1
+ import { frameCall } from '@fougere/transport-http';
2
+ import { sampleInput } from './sample.js';
3
+ /**
4
+ * Every operation the app answers, with a body for those that take one.
5
+ *
6
+ * Enumerated rather than chosen, and that is the whole point: a scenario written by hand
7
+ * holds the operations its author thought of — `demos/observability/load.js` exercises
8
+ * three — while this one holds all of them, and an operation given `weight: 0` becomes a
9
+ * decision visible in the file instead of an omission nobody can see.
10
+ */
11
+ export function reachableOps(app, given = {}) {
12
+ const found = [];
13
+ for (const frond of app.fronds) {
14
+ for (const handler of frond.handlers) {
15
+ // A named surface is a restricted door; the load of an app is what its default
16
+ // door answers, so a surface would count the same operation twice.
17
+ if (handler.surface)
18
+ continue;
19
+ for (const [op, contract] of handler.operations ?? []) {
20
+ const schema = contract.input;
21
+ found.push({
22
+ method: `${handler.address}.${op}`,
23
+ body: schema ? sampleInput(schema, given[handler.address] ?? {}) : undefined,
24
+ });
25
+ }
26
+ }
27
+ }
28
+ return found;
29
+ }
30
+ /**
31
+ * A k6 scenario, written from what the app answers.
32
+ *
33
+ * k6 runs on its own runtime and cannot import from here, so the file is GENERATED rather
34
+ * than made to import `frameCall` — but the envelope in it comes from `frameCall` itself,
35
+ * called once at generation time. `demos/observability/load.js` spells that envelope by
36
+ * hand, so it will go on claiming to be JSON-RPC the day the format moves.
37
+ *
38
+ * What stays the author's: the weights, the stages and the thresholds. The scan knows
39
+ * which operations exist; it knows nothing about the traffic they receive.
40
+ */
41
+ export function loadScript(app, options = {}) {
42
+ const door = options.door ?? 'http://127.0.0.1:3000/_fougere/call';
43
+ const ops = reachableOps(app, options.given);
44
+ // The shape, from the one function that states it. `body` is replaced per iteration.
45
+ const envelope = frameCall({ entity: 'ENTITY', op: 'OP' }, { params: {}, query: {}, body: undefined, state: {} }, 0);
46
+ // What the envelope carries that an iteration does not fill in itself. Keeping
47
+ // `params` here too put it in the object AND in the spread that overwrites it.
48
+ const perCall = new Set(['method', 'id', 'params']);
49
+ const keys = Object.keys(envelope).filter((key) => !perCall.has(key));
50
+ return `// Generated by \`fougere load\` — edit the weights, the stages and the thresholds.
51
+ // Everything else is read from what the app answers: regenerate rather than patch.
52
+ import http from 'k6/http';
53
+ import { check } from 'k6';
54
+
55
+ const DOOR = ${JSON.stringify(door)};
56
+
57
+ // Every operation the app serves. A weight of 0 takes one out, visibly.
58
+ const OPS = ${JSON.stringify(ops.map((op) => ({ ...op, weight: 1 })), null, 2)};
59
+
60
+ export const options = {
61
+ // Yours: a flat rate draws flat lines and there is nothing to read in them.
62
+ stages: [
63
+ { duration: '30s', target: 5 },
64
+ { duration: '45s', target: 5 },
65
+ { duration: '30s', target: 0 },
66
+ ],
67
+ // Yours: what counts as too slow is a fact about your users.
68
+ thresholds: { http_req_failed: ['rate<0.01'], http_req_duration: ['p(95)<500'] },
69
+ };
70
+
71
+ const TOTAL = OPS.reduce((sum, op) => sum + op.weight, 0);
72
+ let id = 0;
73
+
74
+ function pick() {
75
+ let roll = Math.random() * TOTAL;
76
+ for (const op of OPS) if ((roll -= op.weight) < 0) return op;
77
+ return OPS[OPS.length - 1];
78
+ }
79
+
80
+ export default function () {
81
+ const op = pick();
82
+ const payload = ${JSON.stringify(Object.fromEntries(keys.map((key) => [key, envelope[key]])))};
83
+ const response = http.post(
84
+ DOOR,
85
+ JSON.stringify({ ...payload, id: ++id, method: op.method, params: { params: {}, query: {}, body: op.body, state: {} } }),
86
+ { headers: { 'content-type': 'application/json' }, tags: { op: op.method } },
87
+ );
88
+ check(response, { 'answered': (r) => r.status === 200 });
89
+ }
90
+ `;
91
+ }
92
+ //# sourceMappingURL=load.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load.js","sourceRoot":"","sources":["../src/load.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAGpD,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAc1C;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,GAAQ,EAAE,KAAK,GAAyB,EAAE;IACrE,MAAM,KAAK,GAAgB,EAAE,CAAC;IAC9B,KAAK,MAAM,KAAK,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC;QAC/B,KAAK,MAAM,OAAO,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;YACrC,+EAA+E;YAC/E,mEAAmE;YACnE,IAAI,OAAO,CAAC,OAAO;gBAAE,SAAS;YAC9B,KAAK,MAAM,CAAC,EAAE,EAAE,QAAQ,CAAC,IAAI,OAAO,CAAC,UAAU,IAAI,EAAE,EAAE,CAAC;gBACtD,MAAM,MAAM,GAAG,QAAQ,CAAC,KAA+B,CAAC;gBACxD,KAAK,CAAC,IAAI,CAAC;oBACT,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,IAAI,EAAE,EAAE;oBAClC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS;iBAC7E,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,UAAU,CAAC,GAAQ,EAAE,OAAO,GAAgB,EAAE;IAC5D,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,qCAAqC,CAAC;IACnE,MAAM,GAAG,GAAG,YAAY,CAAC,GAAG,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IAC7C,qFAAqF;IACrF,MAAM,QAAQ,GAAG,SAAS,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,EAAW,EAAE,CAAC,CAAC,CAAC;IAC9H,+EAA+E;IAC/E,+EAA+E;IAC/E,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC;IACpD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;IAEtE,OAAO;;;;;eAKM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;;;cAGrB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;oBAwB1D,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,QAAQ,CAAC,GAA4B,CAAC,CAAC,CAAC,CAAC,CAAC;;;;;;;;CAQvH,CAAC;AACF,CAAC"}
@@ -0,0 +1,49 @@
1
+ import { type Change } from '@fougere/schema';
2
+ import type { IdentityCard } from '@fougere/core';
3
+ /** What separates the copy a consumer holds from what the producer actually serves. */
4
+ export interface CardDrift {
5
+ frond: string;
6
+ /** A door the consumer calls that the producer no longer serves. */
7
+ missingDoors: string[];
8
+ /** An operation the consumer calls that the door no longer has. */
9
+ missingOps: {
10
+ door: string;
11
+ ops: string[];
12
+ }[];
13
+ /** A shape that moved under a door the consumer still calls. */
14
+ shapes: {
15
+ door: string;
16
+ changes: Change[];
17
+ }[];
18
+ /** A fact the consumer subscribes to whose shape moved, or that is gone. */
19
+ facts: {
20
+ fact: string;
21
+ changes: Change[] | 'gone';
22
+ }[];
23
+ }
24
+ /**
25
+ * What a consumer's synced copy no longer matches in what the producer serves.
26
+ *
27
+ * The gap TypeScript cannot see, and the only place the gradient genuinely lies: the code
28
+ * is identical in-process and split, but one side may have aged. `fougere sync` wrote the
29
+ * consumer's copy three weeks ago, the producer moved on, and it still compiles —
30
+ * production is where that is found today. This is what Pact sells; the material was
31
+ * already here, in `rpc.discover` and in `diff`.
32
+ *
33
+ * Read in ONE direction on purpose: what the consumer holds, checked against what is
34
+ * served. A producer serving MORE than the consumer knows is not drift — it is a producer
35
+ * that moved forward without breaking anyone, which is the whole point of the order the
36
+ * repo already states (re-sync the readers, then deploy the sender).
37
+ */
38
+ export declare function driftOf(mine: IdentityCard, theirs: IdentityCard, frond: string): CardDrift;
39
+ /** Whether anything at all separates the two cards. */
40
+ export declare function agrees(drift: CardDrift): boolean;
41
+ /**
42
+ * The drift, in the words a deploy needs.
43
+ *
44
+ * A fact says the order out loud, because the repo already states it as a rule and
45
+ * nothing enforced it: a fact is judged strictly, so a reader that has not been re-synced
46
+ * refuses what the sender now announces.
47
+ */
48
+ export declare function explain(drift: CardDrift): string[];
49
+ //# sourceMappingURL=remotes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remotes.d.ts","sourceRoot":"","sources":["../src/remotes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,MAAM,EAAyB,MAAM,iBAAiB,CAAC;AAC3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,uFAAuF;AACvF,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,mEAAmE;IACnE,UAAU,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,EAAE,CAAA;KAAE,EAAE,CAAC;IAC9C,gEAAgE;IAChE,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,EAAE,CAAC;IAC9C,4EAA4E;IAC5E,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,CAAA;KAAE,EAAE,CAAC;CACvD;AAuBD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,CAgC1F;AAED,uDAAuD;AACvD,wBAAgB,MAAM,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAKhD;AAED;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,EAAE,CAYlD"}
@@ -0,0 +1,121 @@
1
+ import { diff } from '@fougere/schema';
2
+ /** Every door of a card, by name. */
3
+ function doorsOf(card, frond) {
4
+ const found = new Map();
5
+ for (const one of card.fronds) {
6
+ if (one.name !== frond)
7
+ continue;
8
+ for (const door of one.doors) {
9
+ found.set(door.name, { ops: new Set(door.ops.map((op) => op.name)), schema: door.schema });
10
+ }
11
+ }
12
+ return found;
13
+ }
14
+ function factsOf(card, frond) {
15
+ const found = new Map();
16
+ for (const one of card.fronds) {
17
+ if (one.name !== frond)
18
+ continue;
19
+ for (const fact of one.facts ?? [])
20
+ found.set(fact.name, fact.schema);
21
+ }
22
+ return found;
23
+ }
24
+ /**
25
+ * What a consumer's synced copy no longer matches in what the producer serves.
26
+ *
27
+ * The gap TypeScript cannot see, and the only place the gradient genuinely lies: the code
28
+ * is identical in-process and split, but one side may have aged. `fougere sync` wrote the
29
+ * consumer's copy three weeks ago, the producer moved on, and it still compiles —
30
+ * production is where that is found today. This is what Pact sells; the material was
31
+ * already here, in `rpc.discover` and in `diff`.
32
+ *
33
+ * Read in ONE direction on purpose: what the consumer holds, checked against what is
34
+ * served. A producer serving MORE than the consumer knows is not drift — it is a producer
35
+ * that moved forward without breaking anyone, which is the whole point of the order the
36
+ * repo already states (re-sync the readers, then deploy the sender).
37
+ */
38
+ export function driftOf(mine, theirs, frond) {
39
+ const held = doorsOf(mine, frond);
40
+ const served = doorsOf(theirs, frond);
41
+ const drift = { frond, missingDoors: [], missingOps: [], shapes: [], facts: [] };
42
+ for (const [name, door] of held) {
43
+ const there = served.get(name);
44
+ if (!there) {
45
+ drift.missingDoors.push(name);
46
+ continue;
47
+ }
48
+ const missing = [...door.ops].filter((op) => !there.ops.has(op));
49
+ if (missing.length > 0)
50
+ drift.missingOps.push({ door: name, ops: missing.sort() });
51
+ if (door.schema && there.schema) {
52
+ // `diff` never guesses a rename — a field gone plus a field appeared lands in
53
+ // `ambiguous`, and only a declaration settles it. Here nobody can declare one, so
54
+ // the pair is reported as it is and a human reads it.
55
+ const moved = diff(door.schema, there.schema);
56
+ if (moved.changes.length > 0)
57
+ drift.shapes.push({ door: name, changes: moved.changes });
58
+ }
59
+ }
60
+ const heldFacts = factsOf(mine, frond);
61
+ const servedFacts = factsOf(theirs, frond);
62
+ for (const [name, shape] of heldFacts) {
63
+ if (!servedFacts.has(name)) {
64
+ drift.facts.push({ fact: name, changes: 'gone' });
65
+ continue;
66
+ }
67
+ const there = servedFacts.get(name);
68
+ if (!shape || !there)
69
+ continue;
70
+ const moved = diff(shape, there);
71
+ if (moved.changes.length > 0)
72
+ drift.facts.push({ fact: name, changes: moved.changes });
73
+ }
74
+ return drift;
75
+ }
76
+ /** Whether anything at all separates the two cards. */
77
+ export function agrees(drift) {
78
+ return drift.missingDoors.length === 0
79
+ && drift.missingOps.length === 0
80
+ && drift.shapes.length === 0
81
+ && drift.facts.length === 0;
82
+ }
83
+ /**
84
+ * The drift, in the words a deploy needs.
85
+ *
86
+ * A fact says the order out loud, because the repo already states it as a rule and
87
+ * nothing enforced it: a fact is judged strictly, so a reader that has not been re-synced
88
+ * refuses what the sender now announces.
89
+ */
90
+ export function explain(drift) {
91
+ const lines = [];
92
+ for (const door of drift.missingDoors)
93
+ lines.push(`${drift.frond}.${door} — you call it, it is not served`);
94
+ for (const { door, ops } of drift.missingOps)
95
+ lines.push(`${drift.frond}.${door} — gone: ${ops.join(', ')}`);
96
+ for (const { door, changes } of drift.shapes) {
97
+ for (const change of changes)
98
+ lines.push(`${drift.frond}.${door} — ${describe(change)}`);
99
+ }
100
+ for (const { fact, changes } of drift.facts) {
101
+ if (changes === 'gone') {
102
+ lines.push(`${fact} — you subscribe to it, it is no longer announced`);
103
+ continue;
104
+ }
105
+ for (const change of changes)
106
+ lines.push(`${fact} — ${describe(change)} → re-sync and deploy the readers, THEN the sender`);
107
+ }
108
+ return lines;
109
+ }
110
+ function describe(change) {
111
+ switch (change.kind) {
112
+ case 'added': return `+ ${change.field}${change.required ? ' (required)' : ''}`;
113
+ case 'removed': return `- ${change.field}`;
114
+ case 'renamed': return `${change.from} → ${change.to}`;
115
+ case 'retyped': return `${change.field}: ${[...change.from].join('|')} → ${[...change.to].join('|')}`;
116
+ case 'reshaped': return `${change.field}: its bounds moved`;
117
+ case 'required': return `${change.field}: ${change.from ? 'no longer' : 'now'} required`;
118
+ case 'restated': return `${change.field}: its ${change.axis} moved`;
119
+ }
120
+ }
121
+ //# sourceMappingURL=remotes.js.map