@lovrozagar/oat 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -736,6 +736,14 @@ Default cohort is **7** records, one of each variant, sliced by `cohortSize`:
736
736
 
737
737
  Numbers use the ladder `1, 2, 5, 10, 20, 50, 100` so **lexical order ≠ numeric order** (otherwise a TEXT compare looks correct). Enums walk `index % enum.length`. `readOnly` / `x-generated` fields are omitted. Required fields that cannot be generated get a type fallback (`0`, `false`, `[]`, `{}`, `"value"`). Arrays honour `minItems` (never send `[]` when `minItems ≥ 1`). Nested objects stop at depth 4.
738
738
 
739
+ Empty schemas (`{}`, `true`, `additionalProperties: {}`) and cyclic `$ref`s after inlining stop the walk — they become a scalar or `{}`, never another object descent. A `RangeError` during generation is a `COVERAGE_GAP` on that entity naming the operationId and JSON pointer (`fixture generation overflow on table.create (/)`), not `blocked by unknown`.
740
+
741
+ String fields honour `format` and `pattern`: `email` → `oat-{variant}-{index}@example.test`, `uri` / `url` → `https://example.test/...`, `uuid` → a fixed-shape UUID, `pattern` → a string that matches (or the field is omitted / the entity is a gap). `"Quarterly Report N"` is only used when the document does not constrain the string.
742
+
743
+ An operation with `x-invite` is not `entity.create`. oat does not POST a generated invitee. The invite check sends `granteeField` = the peer's `inviteAs`. Missing `inviteAs` is a coverage gap naming the tag.
744
+
745
+ A create whose `operationId` appears in any principal `auth.steps`, or that declares `x-fresh-principal`, is not seeded. Those rows were provisioned by the auth flow.
746
+
739
747
  Parent path parameters are created first (depth-first through the owning entity's create). Config / principal `roots` fill parameters oat cannot create.
740
748
 
741
749
  A create that returns `>= 300` on the first variant fails the entity (downstream checks `BLOCKED`). Later variants that fail just shorten the cohort — a partial cohort is still used.
@@ -892,6 +900,8 @@ x-invite:
892
900
 
893
901
  Put this on the invite operation. Config must give the invitee `inviteAs`. Defaults if omitted: `granteeField: key`, `tokenPointer: $.token`, `grantPointer: $.grant_id`. All three of `invite` / `accept` / `revoke` (operationIds) are required or the tag is ignored.
894
902
 
903
+ An invite operation is **not** the entity's fixture create, even when it is `POST` on the collection. oat will not seed it with a generated email. The invite check (and only that check) creates the grant, using `inviteAs` as `granteeField`. The check still runs when there is no non-invite create, as long as an item or list route exists.
904
+
895
905
  Timeline asserted: cannot read → invite → still cannot → accept → can → revoke → cannot.
896
906
 
897
907
  **Fallback:** the check does not run.
@@ -1071,7 +1081,11 @@ export default defineConfig({
1071
1081
  })
1072
1082
  ```
1073
1083
 
1074
- An excluded operation never silently narrows what gets reported. Excluding an entity's `create` degrades that entity the same way a real seeding failure does — a `COVERAGE_GAP` naming the reason, then read-only checks run against whatever the list route already returns; `BLOCKED` if nothing exists to fall back on. Excluding `read`/`update`/`delete` individually stands down just the checks that need that one operation. The run summary states what a profile skipped: `skipped 12 operation(s) under --profile cheap (12 high-cost)` — the same principle as `did not apply` for a coverage gap: a report that only shows what ran invites the reader to assume the rest was verified.
1084
+ An excluded operation never silently narrows what gets reported. Excluding an entity's `create` degrades that entity the same way a real seeding failure does — a `COVERAGE_GAP` naming the reason, then read-only checks run against whatever the list route already returns; `BLOCKED` if nothing exists to fall back on. Excluding `read`/`update`/`delete` individually stands down just the checks that need that one operation.
1085
+
1086
+ Exclusion applies to every invocation, not only create/read/update/delete. `effects.declared-effect-occurs` and `async.reaches-terminal-state` drop ops the profile forbids and record `profile.skip` — they do not POST `extract.once` under `--profile cheap`. A check whose only targets were excluded did not apply / names the profile; it never looks like the backend returned 500.
1087
+
1088
+ The run summary states what a profile skipped: `skipped 12 operation(s) under --profile cheap (12 high-cost)` — the same principle as `did not apply` for a coverage gap: a report that only shows what ran invites the reader to assume the rest was verified.
1075
1089
 
1076
1090
  ### `x-rate-limit` — pacing oat's own traffic
1077
1091
 
package/dist/cli.js CHANGED
@@ -200,7 +200,7 @@ async function main() {
200
200
  return 0;
201
201
  }
202
202
  if (command === "conformance") {
203
- const { postgresAvailable, renderParserSuite, renderSuite, runExampleSpecSuite, runTagUnlockSuite, runParserSuite, runCoverageReportSuite, runTenantScopeSuite, runSuite, sqliteAvailable, d1Available, } = await import("./conformance/suite.js");
203
+ const { postgresAvailable, renderParserSuite, renderSuite, runExampleSpecSuite, runTagUnlockSuite, runParserSuite, runCoverageReportSuite, runSeedContractSuite, runTenantScopeSuite, runSuite, sqliteAvailable, d1Available, } = await import("./conformance/suite.js");
204
204
  const { runFeatureGateSuite } = await import("./conformance/feature-gate.js");
205
205
  const parser = renderParserSuite(runParserSuite());
206
206
  process.stdout.write(parser.text);
@@ -222,6 +222,9 @@ async function main() {
222
222
  const featureGate = renderParserSuite(await runFeatureGateSuite());
223
223
  process.stdout.write(featureGate.text);
224
224
  parser.failures += featureGate.failures;
225
+ const seedContract = renderParserSuite(await runSeedContractSuite());
226
+ process.stdout.write(seedContract.text);
227
+ parser.failures += seedContract.failures;
225
228
  if (flags.parser === true)
226
229
  return parser.failures > 0 ? 1 : 0;
227
230
  if (flags.precision !== undefined) {
@@ -372,6 +375,15 @@ async function main() {
372
375
  const raw = await loadSpec(specSource, str(flags, "base-url"));
373
376
  const { doc, externalRefs } = dereference(raw);
374
377
  const model = buildModel(doc);
378
+ try {
379
+ const { probeCreateFixtures } = await import("./runtime/world.js");
380
+ probeCreateFixtures(model);
381
+ }
382
+ catch (error) {
383
+ const { isOverflowError } = await import("./runtime/fixture.js");
384
+ if (!isOverflowError(error))
385
+ throw error;
386
+ }
375
387
  switch (command) {
376
388
  case "plan":
377
389
  process.stdout.write(report.plan(model, flags.json === true));
@@ -1 +1 @@
1
- {"version":3,"file":"checks.d.ts","sourceRoot":"","sources":["../../src/runtime/checks.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"checks.d.ts","sourceRoot":"","sources":["../../src/runtime/checks.ts"],"names":[],"mappings":"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"}
@@ -2543,25 +2543,20 @@ function pointerValue(body, pointer) {
2543
2543
  * at the same check — the timeline is the property.
2544
2544
  */
2545
2545
  const inviteGrantsThenRevokes = {
2546
- applicable: (ctx) => ctx.invite !== null &&
2547
- ctx.readOp !== undefined &&
2548
- ctx.records.length > 0 &&
2549
- ctx.actors.some((actor) => actor.inviteAs !== undefined &&
2550
- ctx.actors[0] !== undefined &&
2551
- !sameTenantScope(actor.roots, ctx.actors[0].roots)),
2546
+ applicable: (ctx) => ctx.invite !== null && (ctx.readOp !== undefined || ctx.listOp !== undefined),
2552
2547
  dependsOn: ["list.read-after-write", "tenant.item-not-readable-cross-tenant"],
2553
2548
  id: "auth.invite-grants-then-revokes",
2554
- needs: "x-invite naming invite/accept/revoke, a peer principal with inviteAs, and an item route",
2549
+ needs: "x-invite naming invite/accept/revoke, a peer principal with inviteAs, and an item or list route",
2555
2550
  async run(ctx) {
2556
2551
  const spec = ctx.invite;
2557
2552
  const owner = ctx.actors[0];
2558
- const target = ctx.records[0];
2559
- if (spec === null || owner === undefined || target === undefined || ctx.readOp === undefined) {
2553
+ if (spec === null || owner === undefined) {
2560
2554
  return;
2561
2555
  }
2562
- const delegate = ctx.actors.find((actor) => actor.inviteAs !== undefined && !sameTenantScope(actor.roots, owner.roots));
2556
+ const delegate = ctx.actors.find((actor) => actor !== owner && actor.inviteAs !== undefined && !sameTenantScope(actor.roots, owner.roots)) ?? ctx.actors.find((actor) => actor !== owner && actor.inviteAs !== undefined);
2563
2557
  if (delegate === undefined || delegate.inviteAs === undefined) {
2564
- return ctx.findings.unresolved(this.id, ctx.entityName, "no peer principal declares inviteAs, so there is no one to invite");
2558
+ return ctx.findings.gap(this.id, ctx.entityName, "x-invite requires a peer principal with inviteAs", "x-invite names the grant flow, but no peer principal declares inviteAs, so oat " +
2559
+ "cannot fill the grantee field without inventing an email");
2565
2560
  }
2566
2561
  const inviteOp = ctx.model.byOperationId.get(spec.invite);
2567
2562
  const acceptOp = ctx.model.byOperationId.get(spec.accept);
@@ -2569,16 +2564,23 @@ const inviteGrantsThenRevokes = {
2569
2564
  if (inviteOp === undefined || acceptOp === undefined || revokeOp === undefined) {
2570
2565
  return ctx.findings.unresolved(this.id, ctx.entityName, "x-invite names an operation that is not in the document");
2571
2566
  }
2572
- const id = String(target[ctx.identity]);
2573
- const resource = { ...ctx.scope, ...itemParamFor(ctx, id) };
2574
- const itemPath = ctx.readOp.path;
2567
+ const target = ctx.records[0];
2568
+ const id = target === undefined ? undefined : String(target[ctx.identity]);
2569
+ const resource = {
2570
+ ...ctx.scope,
2571
+ ...(id === undefined ? {} : itemParamFor(ctx, id)),
2572
+ };
2573
+ const itemPath = ctx.readOp?.path;
2575
2574
  const canRead = async () => {
2575
+ if (itemPath === undefined || id === undefined)
2576
+ return null;
2576
2577
  const exchange = await ctx.client.get(fillPath(itemPath, { ...delegate.roots, ...resource }), {
2577
2578
  headers: delegate.headers(),
2578
2579
  });
2579
2580
  return exchange.status < 300;
2580
2581
  };
2581
- if (await canRead()) {
2582
+ const alreadyReadable = await canRead();
2583
+ if (alreadyReadable === true) {
2582
2584
  return ctx.findings.unresolved(this.id, ctx.entityName, "the delegate could already read the record, so invite cannot be distinguished from a leak");
2583
2585
  }
2584
2586
  const invited = await ctx.client.request(inviteOp.method, fillPath(inviteOp.path, resource), {
@@ -2590,7 +2592,7 @@ const inviteGrantsThenRevokes = {
2590
2592
  if (invited.status >= 300) {
2591
2593
  return ctx.findings.unresolved(this.id, ctx.entityName, `invite returned ${invited.status}, so accept/revoke were never exercised`);
2592
2594
  }
2593
- if (await canRead()) {
2595
+ if ((await canRead()) === true) {
2594
2596
  ctx.findings.backend(this.id, ctx.entityName, "an invite grants access before it is accepted", `inviting ${delegate.id} made ${ctx.entityName} ${id} readable immediately. The accept ` +
2595
2597
  "step is then theatre — anyone who can be named in an invite body is already in.", [invited]);
2596
2598
  return;
@@ -2608,7 +2610,7 @@ const inviteGrantsThenRevokes = {
2608
2610
  if (accepted.status >= 300) {
2609
2611
  return ctx.findings.unresolved(this.id, ctx.entityName, `accept returned ${accepted.status}`);
2610
2612
  }
2611
- if (!(await canRead())) {
2613
+ if ((await canRead()) === false) {
2612
2614
  ctx.findings.backend(this.id, ctx.entityName, "accepting an invite does not grant access", `${delegate.id} accepted an invite to ${ctx.entityName} ${id} and still cannot read it. ` +
2613
2615
  "The invite flow completed; the grant did not.", [invited, accepted]);
2614
2616
  return;
@@ -2622,7 +2624,7 @@ const inviteGrantsThenRevokes = {
2622
2624
  if (revoked.status >= 300) {
2623
2625
  return ctx.findings.unresolved(this.id, ctx.entityName, `revoke returned ${revoked.status}`);
2624
2626
  }
2625
- if (await canRead()) {
2627
+ if ((await canRead()) === true) {
2626
2628
  ctx.findings.backend(this.id, ctx.entityName, "revoking an invite does not remove access", `${delegate.id} can still read ${ctx.entityName} ${id} after the grant was revoked. ` +
2627
2629
  "A share that cannot be taken back is a standing leak.", [invited, accepted, revoked]);
2628
2630
  }
@@ -2983,9 +2985,24 @@ const successSchemaHonoured = {
2983
2985
  };
2984
2986
  /** Builds a body that should be accepted, for use as the base of a negative probe. */
2985
2987
  function validBody(ctx, schema) {
2986
- const [member] = buildCohort(schema, ctx.seed, ["baseline"]);
2988
+ const [member] = buildCohort(schema, ctx.seed, ["baseline"], ctx.createOp?.operationId ?? ctx.entityName);
2987
2989
  return member?.body ?? {};
2988
2990
  }
2991
+ /** Invite bodies must carry the peer's `inviteAs`, never a generated email. */
2992
+ function bodyForOp(ctx, op) {
2993
+ const body = validBody(ctx, requestSchemaOf(ctx, op) ?? {});
2994
+ const spec = op.invite ?? ctx.invite;
2995
+ if (spec === null || spec === undefined)
2996
+ return body;
2997
+ const owner = ctx.actors[0];
2998
+ const delegate = ctx.actors.find((actor) => actor !== owner &&
2999
+ actor.inviteAs !== undefined &&
3000
+ owner !== undefined &&
3001
+ !sameTenantScope(actor.roots, owner.roots)) ?? ctx.actors.find((actor) => actor !== owner && actor.inviteAs !== undefined);
3002
+ if (delegate?.inviteAs !== undefined)
3003
+ body[spec.granteeField] = delegate.inviteAs;
3004
+ return body;
3005
+ }
2989
3006
  function requestSchemaOf(ctx, op) {
2990
3007
  const raw = ctx.model.rawOperations.get(op.operationId);
2991
3008
  const content = raw?.requestBody?.content;
@@ -3245,7 +3262,7 @@ const declaredEffectsOccur = {
3245
3262
  const before = await observe(ctx, listOp);
3246
3263
  if (before === null)
3247
3264
  continue;
3248
- const body = op.hasRequestBody ? validBody(ctx, requestSchemaOf(ctx, op) ?? {}) : undefined;
3265
+ const body = op.hasRequestBody ? bodyForOp(ctx, op) : undefined;
3249
3266
  const invoked = await ctx.client.request(op.method, fillPath(op.path, ctx.scope), {
3250
3267
  headers: ctx.auth(),
3251
3268
  ...(body === undefined ? {} : { body }),
@@ -3317,7 +3334,7 @@ const asyncReachesTerminalState = {
3317
3334
  const spec = op.async;
3318
3335
  if (spec === null)
3319
3336
  continue;
3320
- const body = op.hasRequestBody ? validBody(ctx, requestSchemaOf(ctx, op) ?? {}) : undefined;
3337
+ const body = op.hasRequestBody ? bodyForOp(ctx, op) : undefined;
3321
3338
  const start = await ctx.client.request(op.method, fillPath(op.path, ctx.scope), {
3322
3339
  headers: ctx.auth(),
3323
3340
  ...(body === undefined ? {} : { body }),