@lovrozagar/oat 0.9.1 → 0.10.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 (40) hide show
  1. package/README.md +103 -32
  2. package/dist/cli.js +56 -14
  3. package/dist/config/define-config.d.ts +7 -1
  4. package/dist/config/define-config.d.ts.map +1 -1
  5. package/dist/config/define-config.js.map +1 -1
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +1 -0
  8. package/dist/report/console.d.ts +2 -1
  9. package/dist/report/console.d.ts.map +1 -1
  10. package/dist/report/console.js +33 -1
  11. package/dist/report/console.js.map +1 -1
  12. package/dist/report/matrix.d.ts +8 -13
  13. package/dist/report/matrix.d.ts.map +1 -1
  14. package/dist/report/matrix.js +21 -10
  15. package/dist/report/matrix.js.map +1 -1
  16. package/dist/report/render.d.ts +8 -0
  17. package/dist/report/render.d.ts.map +1 -1
  18. package/dist/report/render.js +94 -1
  19. package/dist/report/render.js.map +1 -1
  20. package/dist/runtime/checks.d.ts +42 -1
  21. package/dist/runtime/checks.d.ts.map +1 -1
  22. package/dist/runtime/checks.js +190 -24
  23. package/dist/runtime/checks.js.map +1 -1
  24. package/dist/runtime/finding.d.ts +14 -1
  25. package/dist/runtime/finding.d.ts.map +1 -1
  26. package/dist/runtime/finding.js +25 -6
  27. package/dist/runtime/finding.js.map +1 -1
  28. package/dist/runtime/run.d.ts +17 -0
  29. package/dist/runtime/run.d.ts.map +1 -1
  30. package/dist/runtime/run.js +126 -66
  31. package/dist/runtime/run.js.map +1 -1
  32. package/dist/runtime/scope.d.ts +163 -0
  33. package/dist/runtime/scope.d.ts.map +1 -0
  34. package/dist/runtime/scope.js +423 -0
  35. package/dist/runtime/scope.js.map +1 -0
  36. package/dist/runtime/world.d.ts +7 -2
  37. package/dist/runtime/world.d.ts.map +1 -1
  38. package/dist/runtime/world.js +12 -6
  39. package/dist/runtime/world.js.map +1 -1
  40. package/package.json +1 -1
@@ -17,6 +17,8 @@ export interface Finding {
17
17
  origin?: string;
18
18
  /** Fixture filename when `uploads.each` drove the invocation. */
19
19
  fixture?: string;
20
+ /** Operations this finding judges. Absent on run-level findings (teardown, network, pacing). */
21
+ operations?: string[];
20
22
  }
21
23
  /**
22
24
  * A check that ran, could not reach a verdict, and stopped.
@@ -32,10 +34,21 @@ export interface Inconclusive {
32
34
  entity: string;
33
35
  /** Why no verdict was reachable, in the reader's terms. */
34
36
  reason: string;
37
+ operations?: string[];
35
38
  }
36
39
  export declare class FindingCollector {
40
+ #private;
37
41
  readonly findings: Finding[];
38
42
  readonly inconclusive: Inconclusive[];
43
+ constructor(findings?: Finding[], inconclusive?: Inconclusive[], operations?: readonly string[]);
44
+ /**
45
+ * A view over the same findings that attributes everything it records to `operations`.
46
+ *
47
+ * Each check runs against its own view, so a finding lands on the operations the check judges
48
+ * without threading an id through every call site. Read-only checks run concurrently, so a
49
+ * shared "current check" would attribute one check's finding to another.
50
+ */
51
+ attributed(operations: readonly string[]): FindingCollector;
39
52
  /**
40
53
  * Records that a check ran without reaching a verdict. Returns `undefined` so a check can
41
54
  * `return ctx.findings.unresolved(...)` at the point it gives up, which keeps the reason
@@ -47,7 +60,7 @@ export declare class FindingCollector {
47
60
  security(check: string, entity: string, summary: string, detail: string, evidence: Exchange[], fixture?: string): void;
48
61
  spec(check: string, entity: string, summary: string, detail: string, evidence: Exchange[], fixture?: string): void;
49
62
  gap(check: string, entity: string, summary: string, detail: string, fixture?: string): void;
50
- blocked(check: string, entity: string, summary: string, cause: string): void;
63
+ blocked(check: string, entity: string, summary: string, cause: string, evidence?: Exchange[]): void;
51
64
  checks(): string[];
52
65
  byVerdict(verdict: Verdict): Finding[];
53
66
  }
@@ -1 +1 @@
1
- {"version":3,"file":"finding.d.ts","sourceRoot":"","sources":["../../src/runtime/finding.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAE3C,MAAM,MAAM,OAAO,GAAG,aAAa,GAAG,UAAU,GAAG,UAAU,GAAG,WAAW,GAAG,cAAc,GAAG,SAAS,CAAA;AAExG,MAAM,WAAW,OAAO;IACvB,gGAAgG;IAChG,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,QAAQ,EAAE,CAAA;IACpB,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,iEAAiE;IACjE,OAAO,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,2DAA2D;IAC3D,MAAM,EAAE,MAAM,CAAA;CACd;AAOD,qBAAa,gBAAgB;IAC5B,QAAQ,CAAC,QAAQ,EAAE,OAAO,EAAE,CAAK;IACjC,QAAQ,CAAC,YAAY,EAAE,YAAY,EAAE,CAAK;IAE1C;;;;OAIG;IACH,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS;IAKpE,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAI9B,OAAO,CACN,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,QAAQ,EAAE,EACpB,OAAO,CAAC,EAAE,MAAM,GACd,IAAI;IAIP,QAAQ,CACP,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,QAAQ,EAAE,EACpB,OAAO,CAAC,EAAE,MAAM,GACd,IAAI;IAIP,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI;IAIlH,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI;IAI3F,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAW5E,MAAM,IAAI,MAAM,EAAE;IAIlB,SAAS,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,EAAE;CAGtC"}
1
+ {"version":3,"file":"finding.d.ts","sourceRoot":"","sources":["../../src/runtime/finding.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAE3C,MAAM,MAAM,OAAO,GAAG,aAAa,GAAG,UAAU,GAAG,UAAU,GAAG,WAAW,GAAG,cAAc,GAAG,SAAS,CAAA;AAExG,MAAM,WAAW,OAAO;IACvB,gGAAgG;IAChG,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,QAAQ,EAAE,CAAA;IACpB,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,iEAAiE;IACjE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,gGAAgG;IAChG,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;CACrB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,2DAA2D;IAC3D,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;CACrB;AAOD,qBAAa,gBAAgB;;IAC5B,QAAQ,CAAC,QAAQ,EAAE,OAAO,EAAE,CAAA;IAC5B,QAAQ,CAAC,YAAY,EAAE,YAAY,EAAE,CAAA;gBAGzB,QAAQ,GAAE,OAAO,EAAO,EAAE,YAAY,GAAE,YAAY,EAAO,EAAE,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE;IAMvG;;;;;;OAMG;IACH,UAAU,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,GAAG,gBAAgB;IAQ3D;;;;OAIG;IACH,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS;IAKpE,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAI9B,OAAO,CACN,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,QAAQ,EAAE,EACpB,OAAO,CAAC,EAAE,MAAM,GACd,IAAI;IAIP,QAAQ,CACP,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,QAAQ,EAAE,EACpB,OAAO,CAAC,EAAE,MAAM,GACd,IAAI;IAIP,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI;IAIlH,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI;IAI3F,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,GAAE,QAAQ,EAAO,GAAG,IAAI;IAWvG,MAAM,IAAI,MAAM,EAAE;IAIlB,SAAS,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,EAAE;CAGtC"}
@@ -9,19 +9,38 @@ function withFixture(finding, fixture) {
9
9
  return { ...finding, fixture };
10
10
  }
11
11
  export class FindingCollector {
12
- findings = [];
13
- inconclusive = [];
12
+ findings;
13
+ inconclusive;
14
+ #operations;
15
+ constructor(findings = [], inconclusive = [], operations) {
16
+ this.findings = findings;
17
+ this.inconclusive = inconclusive;
18
+ this.#operations = operations;
19
+ }
20
+ /**
21
+ * A view over the same findings that attributes everything it records to `operations`.
22
+ *
23
+ * Each check runs against its own view, so a finding lands on the operations the check judges
24
+ * without threading an id through every call site. Read-only checks run concurrently, so a
25
+ * shared "current check" would attribute one check's finding to another.
26
+ */
27
+ attributed(operations) {
28
+ return new FindingCollector(this.findings, this.inconclusive, operations);
29
+ }
30
+ #stamp() {
31
+ return this.#operations === undefined ? {} : { operations: [...this.#operations] };
32
+ }
14
33
  /**
15
34
  * Records that a check ran without reaching a verdict. Returns `undefined` so a check can
16
35
  * `return ctx.findings.unresolved(...)` at the point it gives up, which keeps the reason
17
36
  * beside the condition that caused it rather than in a comment.
18
37
  */
19
38
  unresolved(check, entity, reason) {
20
- this.inconclusive.push({ check, entity, reason });
39
+ this.inconclusive.push({ check, entity, reason, ...this.#stamp() });
21
40
  return undefined;
22
41
  }
23
42
  report(finding) {
24
- this.findings.push(finding);
43
+ this.findings.push(finding.operations === undefined ? { ...finding, ...this.#stamp() } : finding);
25
44
  }
26
45
  backend(check, entity, summary, detail, evidence, fixture) {
27
46
  this.report(withFixture({ check, detail, entity, evidence, summary, verdict: "BACKEND_BUG" }, fixture));
@@ -35,12 +54,12 @@ export class FindingCollector {
35
54
  gap(check, entity, summary, detail, fixture) {
36
55
  this.report(withFixture({ check, detail, entity, evidence: [], summary, verdict: "COVERAGE_GAP" }, fixture));
37
56
  }
38
- blocked(check, entity, summary, cause) {
57
+ blocked(check, entity, summary, cause, evidence = []) {
39
58
  this.report({
40
59
  check,
41
60
  detail: `blocked by ${cause}`,
42
61
  entity,
43
- evidence: [],
62
+ evidence,
44
63
  summary,
45
64
  verdict: "BLOCKED",
46
65
  });
@@ -1 +1 @@
1
- {"version":3,"file":"finding.js","sourceRoot":"","sources":["../../src/runtime/finding.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAoCH,SAAS,WAAW,CAAC,OAAgB,EAAE,OAAgB;IACtD,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,KAAK,EAAE;QAAE,OAAO,OAAO,CAAA;IAC3D,OAAO,EAAE,GAAG,OAAO,EAAE,OAAO,EAAE,CAAA;AAC/B,CAAC;AAED,MAAM,OAAO,gBAAgB;IACnB,QAAQ,GAAc,EAAE,CAAA;IACxB,YAAY,GAAmB,EAAE,CAAA;IAE1C;;;;OAIG;IACH,UAAU,CAAC,KAAa,EAAE,MAAc,EAAE,MAAc;QACvD,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;QACjD,OAAO,SAAS,CAAA;IACjB,CAAC;IAED,MAAM,CAAC,OAAgB;QACtB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IAC5B,CAAC;IAED,OAAO,CACN,KAAa,EACb,MAAc,EACd,OAAe,EACf,MAAc,EACd,QAAoB,EACpB,OAAgB;QAEhB,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,EAAE,OAAO,CAAC,CAAC,CAAA;IACxG,CAAC;IAED,QAAQ,CACP,KAAa,EACb,MAAc,EACd,OAAe,EACf,MAAc,EACd,QAAoB,EACpB,OAAgB;QAEhB,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,OAAO,CAAC,CAAC,CAAA;IACrG,CAAC;IAED,IAAI,CAAC,KAAa,EAAE,MAAc,EAAE,OAAe,EAAE,MAAc,EAAE,QAAoB,EAAE,OAAgB;QAC1G,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,OAAO,CAAC,CAAC,CAAA;IACrG,CAAC;IAED,GAAG,CAAC,KAAa,EAAE,MAAc,EAAE,OAAe,EAAE,MAAc,EAAE,OAAgB;QACnF,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,EAAE,OAAO,CAAC,CAAC,CAAA;IAC7G,CAAC;IAED,OAAO,CAAC,KAAa,EAAE,MAAc,EAAE,OAAe,EAAE,KAAa;QACpE,IAAI,CAAC,MAAM,CAAC;YACX,KAAK;YACL,MAAM,EAAE,cAAc,KAAK,EAAE;YAC7B,MAAM;YACN,QAAQ,EAAE,EAAE;YACZ,OAAO;YACP,OAAO,EAAE,SAAS;SAClB,CAAC,CAAA;IACH,CAAC;IAED,MAAM;QACL,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;IAC9D,CAAC;IAED,SAAS,CAAC,OAAgB;QACzB,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,OAAO,CAAC,CAAA;IAC1D,CAAC;CACD"}
1
+ {"version":3,"file":"finding.js","sourceRoot":"","sources":["../../src/runtime/finding.ts"],"names":[],"mappings":"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"}
@@ -12,6 +12,7 @@ import { type PersistedPrincipal } from "./principals.ts";
12
12
  import { Client } from "./client.ts";
13
13
  import type { ProgressHandler } from "./progress.ts";
14
14
  import { type Finding, type Inconclusive } from "./finding.ts";
15
+ import { type ScopeReport } from "./scope.ts";
15
16
  import { type TeardownReport } from "./teardown.ts";
16
17
  export type PrincipalSpec = Principal;
17
18
  export interface RunOptions {
@@ -26,7 +27,13 @@ export interface RunOptions {
26
27
  seed?: number;
27
28
  cohortSize?: number;
28
29
  globalHeaders?: Record<string, string>;
30
+ /** Entity names; each expands into the target set. */
29
31
  only?: string[];
32
+ /**
33
+ * Target operationIds (`*` globs, `<originId>:` prefixes). With `only`, the union is graded and
34
+ * every other operation is support — called, never graded. Omit both for a full run.
35
+ */
36
+ ops?: string[];
30
37
  /** Named profiles this run can select between. `"full"` and `"cheap"` exist without an entry. */
31
38
  profiles?: Record<string, ProfileSpec>;
32
39
  /** Active profile by name. Defaults to `"full"` — every operation runs, today's behaviour. */
@@ -86,6 +93,14 @@ export interface RunResult {
86
93
  entity: string;
87
94
  because: string;
88
95
  }>;
96
+ /**
97
+ * Checks a targeted run did not run on an entity because none of their subjects is a target.
98
+ * Neither run nor skipped: the check could apply, this run was not asked to grade it.
99
+ */
100
+ checksOutOfScope: Array<{
101
+ check: string;
102
+ entity: string;
103
+ }>;
89
104
  /** Checks that ran but could not reach a verdict — see `Inconclusive`. */
90
105
  inconclusive: Inconclusive[];
91
106
  /** Name of the profile that ran — `"full"` unless `--profile` / `config.profile` said otherwise. */
@@ -96,6 +111,8 @@ export interface RunResult {
96
111
  operationId: string;
97
112
  reason: string;
98
113
  }>;
114
+ /** What this run graded, per operation — targets only when targeted, every operation when full. */
115
+ scope: ScopeReport;
99
116
  created: number;
100
117
  teardown: TeardownReport | null;
101
118
  /** Credentials as they stood after acquire — written to `<outDir>/<datetime>/principals.json` by the CLI. */
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/runtime/run.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/runtime/run.ts"],"names":[],"mappings":"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"}
@@ -7,7 +7,6 @@
7
7
  */
8
8
  import { buildModel } from "../spec/graph.js";
9
9
  import { dereference, loadSpec } from "../spec/load.js";
10
- import { isAuthFlow } from "../config/define-config.js";
11
10
  import { createPrincipal } from "./auth.js";
12
11
  import { createExchangeJournal } from "./exchanges.js";
13
12
  import { DEFAULT_NETWORK_RETRIES, DEFAULT_NETWORK_WAIT_MS, NetworkError, createNetworkGate, describeNetworkFailure, isNetworkError, probeOrigin, resolveRequestTimeoutMs, } from "./network.js";
@@ -19,12 +18,13 @@ import { FindingCollector } from "./finding.js";
19
18
  import { reportFeatureGateSchemaDrift } from "./feature-gate.js";
20
19
  import { formatUniqueSets } from "../spec/extensions.js";
21
20
  import { excludedByProfile, resolveProfile } from "./profile.js";
21
+ import { authStepOperationIds, buildScopeReport, createIsAuthProvisioned, GradeLedger, resolveTargetScope, staticSubjects, } from "./scope.js";
22
22
  import { buildRateLimitRules, RateLimiter } from "./rate-limit.js";
23
23
  import { resolveEntityCapabilities } from "./query-capabilities.js";
24
24
  import { Ledger } from "./teardown.js";
25
25
  import { SchemaValidator } from "./validate.js";
26
26
  import { isOverflowError, overflowFrom } from "./fixture.js";
27
- import { SeedError, fillPath, listExisting, probeCreateFixtures, resolveScope, seedCohort, } from "./world.js";
27
+ import { SeedError, fillPath, listExisting, probeCreateFixtures, resolvePathScope, seedCohort, } from "./world.js";
28
28
  function readPointer(body, pointer) {
29
29
  const path = pointer
30
30
  .replace(/^\$\.?/, "")
@@ -178,39 +178,6 @@ async function readExisting(listOp, client, headers, roots) {
178
178
  const records = await listExisting(listOp, client, headers, scope);
179
179
  return { records, scope };
180
180
  }
181
- /** operationIds named in any principal `auth.steps` — those creates already ran during login. */
182
- function authStepOperationIds(principals) {
183
- const ids = new Set();
184
- for (const principal of principals) {
185
- const auth = principal.auth;
186
- if (auth === undefined || !isAuthFlow(auth))
187
- continue;
188
- for (const step of auth.steps) {
189
- if ("operationId" in step)
190
- ids.add(step.operationId);
191
- }
192
- }
193
- return ids;
194
- }
195
- function createIsAuthProvisioned(createOp, authCreates) {
196
- return createOp.freshPrincipal || authCreates.has(createOp.operationId);
197
- }
198
- function testableEntities(model, only, authCreates) {
199
- return [...model.entities.values()]
200
- .filter((entity) => {
201
- if (only !== undefined && only.length > 0 && !only.includes(entity.name))
202
- return false;
203
- const createOp = entity.create === undefined ? undefined : model.byOperationId.get(entity.create);
204
- const authProvisioned = createOp !== undefined && createIsAuthProvisioned(createOp, authCreates);
205
- /* Register-as-create with no item route is the auth flow, not a fixture. */
206
- if (authProvisioned && entity.read === undefined)
207
- return false;
208
- if (entity.invite !== null && entity.list !== undefined)
209
- return true;
210
- return entity.list !== undefined && entity.create !== undefined && entity.trackable;
211
- })
212
- .sort((a, b) => a.name.localeCompare(b.name));
213
- }
214
181
  export async function run(options) {
215
182
  const startedAt = Date.now();
216
183
  const findings = new FindingCollector();
@@ -394,6 +361,32 @@ export async function run(options) {
394
361
  if (options.principals[0] === undefined)
395
362
  throw new Error("oat: at least one principal is required");
396
363
  const originClients = await loadOriginClients(options.origins ?? [], hooks, options.maxInFlight ?? 4, options.globalHeaders ?? {}, onExchange, httpHooks, networkClient);
364
+ /* Resolved before any principal signs up: a typo in --ops must cost nothing. */
365
+ const runScope = resolveTargetScope(model, {
366
+ ...(options.ops === undefined ? {} : { ops: options.ops }),
367
+ ...(options.only === undefined ? {} : { only: options.only }),
368
+ authCreates,
369
+ exclusion: { profile: profile.name, reason: (op) => excludedByProfile(op, profile.spec) },
370
+ origins: new Map([...originClients].map(([id, origin]) => [id, origin.model])),
371
+ });
372
+ const grades = new GradeLedger();
373
+ tick({
374
+ message: runScope.mode === "full"
375
+ ? "scope: full"
376
+ : `scope: targeted · ${runScope.targets.size} operation(s) · ${runScope.entities.length} entit${runScope.entities.length === 1 ? "y" : "ies"}`,
377
+ phase: "load",
378
+ requests: client.transcript.length,
379
+ });
380
+ const scopeReport = () => buildScopeReport({
381
+ authCreates,
382
+ baseUrl: options.baseUrl,
383
+ findings: findings.findings,
384
+ inconclusive: findings.inconclusive,
385
+ ledger: grades,
386
+ model,
387
+ scope: runScope,
388
+ transcript: client.transcript,
389
+ });
397
390
  const uploads = {
398
391
  seed,
399
392
  ...(options.uploads === undefined ? {} : { uploads: options.uploads }),
@@ -413,6 +406,7 @@ export async function run(options) {
413
406
  throw error;
414
407
  noteNetwork(error, "auth");
415
408
  return {
409
+ checksOutOfScope: [],
416
410
  checksRun: [],
417
411
  checksSkipped: [],
418
412
  checksSuppressed: [],
@@ -425,6 +419,7 @@ export async function run(options) {
425
419
  principals: [],
426
420
  profile: profile.name,
427
421
  profileExclusions,
422
+ scope: scopeReport(),
428
423
  teardown: null,
429
424
  ...(journal === null ? {} : { exchanges: { count: journal.count } }),
430
425
  ...(networkOutcome === undefined ? {} : { network: networkOutcome }),
@@ -443,6 +438,11 @@ export async function run(options) {
443
438
  const checksRun = new Set();
444
439
  const checksSkipped = [];
445
440
  const checksSuppressed = [];
441
+ const checksOutOfScope = [];
442
+ const profileExcludes = (operationId) => {
443
+ const op = model.byOperationId.get(operationId);
444
+ return op !== undefined && excludedByProfile(op, profile.spec) !== null;
445
+ };
446
446
  const excludedIds = new Set();
447
447
  const excludeOp = (entity, op) => {
448
448
  if (op === undefined)
@@ -454,7 +454,9 @@ export async function run(options) {
454
454
  return true;
455
455
  excludedIds.add(op.operationId);
456
456
  profileExclusions.push({ entity: entity.name, operationId: op.operationId, reason });
457
- findings.gap("profile.skip", entity.name, `${op.operationId} excluded by profile "${profile.name}"`, `${reason}, under --profile ${profile.name}. Checks that depend on this operation stand ` +
457
+ findings
458
+ .attributed([op.operationId])
459
+ .gap("profile.skip", entity.name, `${op.operationId} excluded by profile "${profile.name}"`, `${reason}, under --profile ${profile.name}. Checks that depend on this operation stand ` +
458
460
  "down rather than run against data oat did not create through it.");
459
461
  return true;
460
462
  };
@@ -465,6 +467,14 @@ export async function run(options) {
465
467
  return;
466
468
  if (networkGate.exhausted)
467
469
  return;
470
+ /* Targets this entity's checks can grade. Seeding findings land on them: under --ops a seed
471
+ * that fails leaves exactly these ungraded, and the report has to say which. */
472
+ const entityTargets = runScope.mode === "full"
473
+ ? createOp === undefined
474
+ ? []
475
+ : [createOp.operationId]
476
+ : [...staticSubjects(entity, model)].filter((id) => runScope.inScope(id));
477
+ const entityFindings = entityTargets.length === 0 ? findings : findings.attributed(entityTargets);
468
478
  const inviteOnly = entity.invite !== null && createOp === undefined;
469
479
  const authProvisioned = createOp !== undefined && createIsAuthProvisioned(createOp, authCreates);
470
480
  if (createOp === undefined && !inviteOnly)
@@ -501,7 +511,7 @@ export async function run(options) {
501
511
  ...alpha.roots,
502
512
  });
503
513
  if (existing.records.length === 0) {
504
- findings.blocked("profile.skip", entity.name, `could not test "${entity.name}"`, `create is excluded by profile "${profile.name}" and the list route returned no ` +
514
+ entityFindings.blocked("profile.skip", entity.name, `could not test "${entity.name}"`, `create is excluded by profile "${profile.name}" and the list route returned no ` +
505
515
  "existing records to fall back on.");
506
516
  return;
507
517
  }
@@ -522,7 +532,7 @@ export async function run(options) {
522
532
  }
523
533
  else {
524
534
  try {
525
- scope = await resolveScope(createOp, model, client, {
535
+ scope = await resolvePathScope(createOp, model, client, {
526
536
  authHeaders: alpha.headers,
527
537
  roots: rootValues,
528
538
  seed,
@@ -541,13 +551,13 @@ export async function run(options) {
541
551
  uploads,
542
552
  }, scope);
543
553
  if (cohort.uniqueGap !== undefined) {
544
- findings.gap("world.seed", entity.name, cohort.uniqueGap, cohort.uniqueGap);
554
+ entityFindings.gap("world.seed", entity.name, cohort.uniqueGap, cohort.uniqueGap);
545
555
  }
546
556
  if (cohort.adopted === true) {
547
557
  /* Plan limit after an effect already created the row: keep the id so children
548
558
  * (row after extract→table) can seed, but do not assert write-path oracles
549
559
  * against a body oat never submitted. */
550
- findings.gap("world.seed", entity.name, `seeding "${entity.name}" hit a plan limit; using an existing same-tenant record`, `${createOp.operationId} returned a plan-limit refusal and the list already ` +
560
+ entityFindings.gap("world.seed", entity.name, `seeding "${entity.name}" hit a plan limit; using an existing same-tenant record`, `${createOp.operationId} returned a plan-limit refusal and the list already ` +
551
561
  "had a record — likely an earlier x-effects create. Write-path checks stand " +
552
562
  "down rather than treat payment_required as a backend defect.");
553
563
  records = cohort.records;
@@ -567,7 +577,7 @@ export async function run(options) {
567
577
  * but unique-conflict checks still run against that row. Write-path oracles
568
578
  * that need a body oat submitted stay skipped. The seed 409 is not the
569
579
  * unique check passing — that check is the explicit second POST. */
570
- findings.gap("world.seed", entity.name, `seeding "${entity.name}" could not insert because of x-unique`, `${createOp.operationId} returned 409 and the list already had a record. ` +
580
+ entityFindings.gap("world.seed", entity.name, `seeding "${entity.name}" could not insert because of x-unique`, `${createOp.operationId} returned 409 and the list already had a record. ` +
571
581
  `x-unique: ${formatUniqueSets(createOp.unique)}. The create could not insert; ` +
572
582
  "unique-conflict checks still run against that row. Write-path oracles that " +
573
583
  "need a body oat submitted stand down.");
@@ -588,15 +598,15 @@ export async function run(options) {
588
598
  /* Same degradation a profile-excluded create takes: the tag said this
589
599
  * principal cannot create the row, so a correct 403 is coverage, not a
590
600
  * seed defect. The 403 body still has to match the documented schema. */
591
- reportFeatureGateSchemaDrift(findings, validator, createOp, model.rawOperations.get(createOp.operationId), cohort.featureGate.exchange, entity.name);
592
- findings.gap("world.seed", entity.name, `seeding "${entity.name}" is gated by ${cohort.featureGate.detail}`, `${cohort.featureGate.detail}. Checks that need a row oat created stand down ` +
601
+ reportFeatureGateSchemaDrift(entityFindings, validator, createOp, model.rawOperations.get(createOp.operationId), cohort.featureGate.exchange, entity.name);
602
+ entityFindings.gap("world.seed", entity.name, `seeding "${entity.name}" is gated by ${cohort.featureGate.detail}`, `${cohort.featureGate.detail}. Checks that need a row oat created stand down ` +
593
603
  "rather than treat the documented 403 as a defect.");
594
604
  const existing = await readExisting(listOp, client, alpha.headers(), {
595
605
  ...options.roots,
596
606
  ...alpha.roots,
597
607
  });
598
608
  if (existing.records.length === 0) {
599
- findings.blocked("world.seed", entity.name, `could not test "${entity.name}"`, `${cohort.featureGate.detail} and the list route returned no existing ` + "records to fall back on.");
609
+ entityFindings.blocked("world.seed", entity.name, `could not test "${entity.name}"`, `${cohort.featureGate.detail} and the list route returned no existing ` + "records to fall back on.");
600
610
  return;
601
611
  }
602
612
  scope = { created: [], values: existing.scope };
@@ -622,13 +632,13 @@ export async function run(options) {
622
632
  }
623
633
  if (isOverflowError(error)) {
624
634
  const overflow = overflowFrom(error, createOp.operationId);
625
- findings.gap("world.seed", entity.name, overflow.message, overflow.message);
635
+ entityFindings.gap("world.seed", entity.name, overflow.message, overflow.message);
626
636
  const existing = await readExisting(listOp, client, alpha.headers(), {
627
637
  ...options.roots,
628
638
  ...alpha.roots,
629
639
  });
630
640
  if (existing.records.length === 0) {
631
- findings.blocked("world.seed", entity.name, `could not seed "${entity.name}"`, overflow.message);
641
+ entityFindings.blocked("world.seed", entity.name, `could not seed "${entity.name}"`, overflow.message);
632
642
  return;
633
643
  }
634
644
  scope = { created: [], values: existing.scope };
@@ -639,13 +649,23 @@ export async function run(options) {
639
649
  const cause = error instanceof SeedError ? error.cause_ : "unknown";
640
650
  const status = error instanceof SeedError ? error.status : undefined;
641
651
  const message = error instanceof Error ? error.message : String(error);
652
+ const failedOp = (error instanceof SeedError ? error.operationId : undefined) ?? createOp.operationId;
653
+ const evidence = error instanceof SeedError && error.exchange !== undefined
654
+ ? [error.exchange]
655
+ : client.transcript.filter((e) => e.status >= 500).slice(-1);
656
+ /* Under --ops a create nobody targeted is support. Its failure leaves the targets
657
+ * ungraded — blocked, with the evidence — but it is not what this run was asked to
658
+ * judge, so it is not reported as a defect of its own. */
659
+ const supportFailed = runScope.mode === "targeted" && !runScope.inScope(failedOp);
642
660
  /* A create that fails with 5xx is not a fixture problem — it is the defect.
643
661
  * Reporting it as merely "blocked" buries the most serious thing oat found. */
644
- if (status !== undefined && status >= 500) {
645
- findings.backend("create.does-not-error", entity.name, `creating a "${entity.name}" fails with a server error`, `${message}. The request body was generated from the documented schema, so ` +
662
+ if (status !== undefined && status >= 500 && !supportFailed) {
663
+ findings
664
+ .attributed([failedOp])
665
+ .backend("create.does-not-error", entity.name, `creating a "${entity.name}" fails with a server error`, `${message}. The request body was generated from the documented schema, so ` +
646
666
  "either the handler rejects input the document permits, or it is failing " +
647
667
  "outright. Everything downstream of this entity is untestable until it is " +
648
- "fixed.", client.transcript.filter((e) => e.status >= 500).slice(-1));
668
+ "fixed.", evidence);
649
669
  }
650
670
  /* Fall back to whatever already exists. A backend whose create is broken can still
651
671
  * have a working list, and read-only coverage beats no coverage — this is exactly
@@ -655,10 +675,10 @@ export async function run(options) {
655
675
  ...alpha.roots,
656
676
  });
657
677
  if (existing.records.length === 0) {
658
- findings.blocked("world.seed", entity.name, `could not seed "${entity.name}"`, `${cause}: ${message}`);
678
+ entityFindings.blocked("world.seed", entity.name, `could not seed "${entity.name}"`, supportFailed ? `support operation ${failedOp} failing: ${message}` : `${cause}: ${message}`, evidence);
659
679
  return;
660
680
  }
661
- findings.gap("world.seed", entity.name, `seeding "${entity.name}" failed; running read-only checks against existing records`, `${cause}: ${message}. Write-path and lifecycle checks are skipped for this entity.`);
681
+ entityFindings.gap("world.seed", entity.name, `seeding "${entity.name}" failed; running read-only checks against existing records`, `${cause}: ${message}. Write-path and lifecycle checks are skipped for this entity.`);
662
682
  scope = { created: [], values: existing.scope };
663
683
  records = existing.records;
664
684
  degraded = true;
@@ -668,7 +688,7 @@ export async function run(options) {
668
688
  const actorOf = async (principal, seedOffset) => {
669
689
  const roots = { ...options.roots, ...principal.roots };
670
690
  try {
671
- const next = await resolveScope(listOp, model, client, {
691
+ const next = await resolvePathScope(listOp, model, client, {
672
692
  authHeaders: principal.headers,
673
693
  roots,
674
694
  seed: seed + seedOffset,
@@ -786,9 +806,12 @@ export async function run(options) {
786
806
  actors,
787
807
  altAuth,
788
808
  altScope,
789
- asyncOps: model.operations.filter((op) => op.entity === entity.name && op.async !== null && invocable(op)),
790
- effectOps: model.operations.filter((op) => op.entity === entity.name && op.effects.length > 0 && invocable(op)),
791
- waitOps: model.operations.filter((op) => op.entity === entity.name && op.wait !== null && invocable(op)),
809
+ /* Narrowed to targets: these are the subjects their checks invoke, and invoking an
810
+ * untargeted one is exactly the cost --ops exists to skip. */
811
+ asyncOps: model.operations.filter((op) => op.entity === entity.name && op.async !== null && invocable(op) && runScope.inScope(op.operationId)),
812
+ effectOps: model.operations.filter((op) => op.entity === entity.name && op.effects.length > 0 && invocable(op) && runScope.inScope(op.operationId)),
813
+ waitOps: model.operations.filter((op) => op.entity === entity.name && op.wait !== null && invocable(op) && runScope.inScope(op.operationId)),
814
+ inScope: (op) => runScope.inScope(op.operationId),
792
815
  hooks,
793
816
  outOfBand,
794
817
  auth: alpha.headers,
@@ -833,7 +856,20 @@ export async function run(options) {
833
856
  : undefined,
834
857
  };
835
858
  entitiesTested.push(entity.name);
859
+ /* Subjects each check grades on this entity: in scope, and not excluded by the profile. */
860
+ const gradedBy = new Map();
836
861
  const runOne = async (check) => {
862
+ const graded = gradedBy.get(check.id) ?? [];
863
+ let judged;
864
+ const view = {
865
+ ...ctx,
866
+ ...(graded.length === 0 ? {} : { findings: findings.attributed(graded) }),
867
+ judged: (operationIds) => {
868
+ judged ??= new Set();
869
+ for (const id of operationIds)
870
+ judged.add(id);
871
+ },
872
+ };
837
873
  checksRun.add(check.id);
838
874
  currentCheck = check.id;
839
875
  currentPhase = "test";
@@ -846,15 +882,16 @@ export async function run(options) {
846
882
  requests: client.transcript.length,
847
883
  });
848
884
  try {
849
- await check.run(ctx);
885
+ await check.run(view);
850
886
  }
851
887
  catch (error) {
852
888
  if (isNetworkError(error)) {
853
889
  noteNetwork(error, entity.name);
854
890
  return;
855
891
  }
856
- findings.gap(check.id, entity.name, `check "${check.id}" could not complete`, error instanceof Error ? error.message : String(error));
892
+ view.findings.gap(check.id, entity.name, `check "${check.id}" could not complete`, error instanceof Error ? error.message : String(error));
857
893
  }
894
+ grades.graded(judged === undefined ? graded : graded.filter((id) => judged?.has(id) === true), check.id);
858
895
  };
859
896
  /* Cascade suppression: a check whose premise is already known broken would report a
860
897
  * consequence, not a defect. One root cause, one finding. */
@@ -881,6 +918,7 @@ export async function run(options) {
881
918
  const because = failed ? dependency : inherited;
882
919
  suppressedBy.set(check.id, because);
883
920
  checksSuppressed.push({ because, check: check.id, entity: entity.name });
921
+ grades.suppress(gradedBy.get(check.id) ?? [], check.id);
884
922
  return true;
885
923
  }
886
924
  return false;
@@ -902,14 +940,19 @@ export async function run(options) {
902
940
  await Promise.all(pending.map(runOne));
903
941
  };
904
942
  for (const check of CHECKS) {
943
+ const graded = check.subjects(entity, model).filter((id) => runScope.inScope(id) && !profileExcludes(id));
944
+ /* Out of scope is not a skip: the check could have run, this run was not asked to. */
945
+ if (runScope.mode === "targeted" && graded.length === 0) {
946
+ checksOutOfScope.push({ check: check.id, entity: entity.name });
947
+ continue;
948
+ }
949
+ gradedBy.set(check.id, graded);
905
950
  if (!check.applicable(ctx)) {
906
951
  /* Recorded, not dropped: on an API shaped unlike the fixture this is most of the
907
952
  * suite, and a silent skip reads exactly like a clean result. */
908
- checksSkipped.push({
909
- check: check.id,
910
- entity: entity.name,
911
- needs: check.needs ?? "an unstated precondition",
912
- });
953
+ const needs = check.needs ?? "an unstated precondition";
954
+ checksSkipped.push({ check: check.id, entity: entity.name, needs });
955
+ grades.skipped(graded, needs);
913
956
  continue;
914
957
  }
915
958
  if (check.mutates === true) {
@@ -935,7 +978,7 @@ export async function run(options) {
935
978
  * entities: a child create in flight while a parent page-walk runs invents pagination
936
979
  * findings, so there are no entity lanes.
937
980
  */
938
- const queue = testableEntities(model, options.only, authCreates);
981
+ const queue = runScope.entities;
939
982
  entityTotal = queue.length;
940
983
  for (const [index, entity] of queue.entries()) {
941
984
  if (networkGate.exhausted) {
@@ -982,8 +1025,9 @@ export async function run(options) {
982
1025
  rank: principal.rank,
983
1026
  ...(principal.inviteAs === undefined ? {} : { inviteAs: principal.inviteAs }),
984
1027
  }));
1028
+ const coverage = scopeReport();
985
1029
  if ((options.origins ?? []).length > 0 && options.skipPrincipalTeardown !== true) {
986
- await runSecondaryOrigins(options, persisted, findings, checksRun, checksSkipped, checksSuppressed, entitiesTested);
1030
+ await runSecondaryOrigins(options, persisted, findings, checksRun, checksSkipped, checksSuppressed, checksOutOfScope, entitiesTested, runScope, coverage);
987
1031
  }
988
1032
  if (options.skipPrincipalTeardown !== true) {
989
1033
  await teardownPrincipals(resolved, hooks, findings);
@@ -994,6 +1038,7 @@ export async function run(options) {
994
1038
  requests: client.transcript.length,
995
1039
  });
996
1040
  return {
1041
+ checksOutOfScope,
997
1042
  checksRun: [...checksRun].sort(),
998
1043
  checksSkipped,
999
1044
  checksSuppressed,
@@ -1006,6 +1051,7 @@ export async function run(options) {
1006
1051
  principals: persisted,
1007
1052
  profile: profile.name,
1008
1053
  profileExclusions,
1054
+ scope: coverage,
1009
1055
  teardown,
1010
1056
  ...(journal === null ? {} : { exchanges: { count: journal.count } }),
1011
1057
  ...(networkOutcome === undefined ? {} : { network: networkOutcome }),
@@ -1028,8 +1074,15 @@ async function loadOriginClients(origins, hooks, maxInFlight, globalHeaders, onE
1028
1074
  }
1029
1075
  return map;
1030
1076
  }
1031
- async function runSecondaryOrigins(options, persisted, findings, checksRun, checksSkipped, checksSuppressed, entitiesTested) {
1077
+ async function runSecondaryOrigins(options, persisted, findings, checksRun, checksSkipped, checksSuppressed, checksOutOfScope, entitiesTested, runScope, coverage) {
1032
1078
  for (const origin of options.origins ?? []) {
1079
+ /* Targets were resolved against every origin up front; an origin none of them name has
1080
+ * nothing to grade, and running it anyway is the cost --ops exists to skip. */
1081
+ const originOps = runScope.origins.get(origin.id);
1082
+ if (runScope.mode === "targeted" && originOps === undefined) {
1083
+ coverage.originsSkipped.push(origin.id);
1084
+ continue;
1085
+ }
1033
1086
  const originHooks = options.hooks === undefined
1034
1087
  ? undefined
1035
1088
  : (() => {
@@ -1048,7 +1101,7 @@ async function runSecondaryOrigins(options, persisted, findings, checksRun, chec
1048
1101
  ...(options.roots === undefined ? {} : { roots: options.roots }),
1049
1102
  ...(options.seed === undefined ? {} : { seed: options.seed }),
1050
1103
  ...(options.cohortSize === undefined ? {} : { cohortSize: options.cohortSize }),
1051
- ...(options.only === undefined ? {} : { only: options.only }),
1104
+ ...(originOps === undefined ? {} : { ops: originOps }),
1052
1105
  ...(options.profiles === undefined ? {} : { profiles: options.profiles }),
1053
1106
  ...(options.profile === undefined ? {} : { profile: options.profile }),
1054
1107
  ...(options.rateLimits === undefined ? {} : { rateLimits: options.rateLimits }),
@@ -1073,6 +1126,13 @@ async function runSecondaryOrigins(options, persisted, findings, checksRun, chec
1073
1126
  }
1074
1127
  for (const entity of result.entitiesTested)
1075
1128
  entitiesTested.push(`${origin.id}:${entity}`);
1129
+ for (const item of result.checksOutOfScope) {
1130
+ checksOutOfScope.push({ ...item, entity: `${origin.id}:${item.entity}` });
1131
+ }
1132
+ for (const op of result.scope.operations)
1133
+ coverage.operations.push({ ...op, origin: origin.id });
1134
+ for (const use of result.scope.support)
1135
+ coverage.support.push({ ...use, origin: origin.id });
1076
1136
  for (const item of result.inconclusive) {
1077
1137
  findings.unresolved(item.check, `${origin.id}:${item.entity}`, item.reason);
1078
1138
  }