@sdods/core 0.7.2 → 0.8.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 (53) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/analyze/index.d.ts +2 -0
  3. package/dist/analyze/index.js +2 -0
  4. package/dist/analyze/traceability-render.d.ts +5 -0
  5. package/dist/analyze/traceability-render.js +254 -0
  6. package/dist/analyze/traceability.d.ts +87 -0
  7. package/dist/analyze/traceability.js +559 -0
  8. package/dist/api/json-path.d.ts +11 -1
  9. package/dist/api/json-path.js +23 -1
  10. package/dist/config/emulation.d.ts +74 -0
  11. package/dist/config/emulation.js +154 -0
  12. package/dist/config/index.d.ts +1 -0
  13. package/dist/config/index.js +1 -0
  14. package/dist/config/runner.js +4 -1
  15. package/dist/config/tags.d.ts +1 -1
  16. package/dist/config/tags.js +7 -0
  17. package/dist/errors.d.ts +1 -1
  18. package/dist/fixtures/api-context.d.ts +19 -1
  19. package/dist/fixtures/api-context.js +28 -1
  20. package/dist/fixtures/test.js +36 -0
  21. package/dist/fixtures/types.d.ts +3 -0
  22. package/dist/index.d.ts +3 -0
  23. package/dist/index.js +3 -0
  24. package/dist/lint/index.js +86 -0
  25. package/dist/load/index.d.ts +4 -0
  26. package/dist/load/index.js +4 -0
  27. package/dist/load/profile.d.ts +22 -0
  28. package/dist/load/profile.js +109 -0
  29. package/dist/load/run.d.ts +85 -0
  30. package/dist/load/run.js +255 -0
  31. package/dist/load/script.d.ts +27 -0
  32. package/dist/load/script.js +188 -0
  33. package/dist/mail/extract.d.ts +38 -0
  34. package/dist/mail/extract.js +106 -0
  35. package/dist/mail/index.d.ts +8 -0
  36. package/dist/mail/index.js +11 -0
  37. package/dist/mail/mailpit.d.ts +26 -0
  38. package/dist/mail/mailpit.js +124 -0
  39. package/dist/mail/types.d.ts +38 -0
  40. package/dist/mail/types.js +2 -0
  41. package/dist/matrix/index.d.ts +104 -0
  42. package/dist/matrix/index.js +486 -0
  43. package/dist/steps/api.steps.js +5 -7
  44. package/dist/steps/browser.steps.d.ts +12 -0
  45. package/dist/steps/browser.steps.js +154 -3
  46. package/dist/steps/canvas.steps.d.ts +9 -0
  47. package/dist/steps/canvas.steps.js +331 -0
  48. package/dist/steps/email.steps.d.ts +2 -0
  49. package/dist/steps/email.steps.js +182 -0
  50. package/dist/steps/hybrid.steps.js +2 -2
  51. package/dist/steps/index.d.ts +2 -0
  52. package/dist/steps/index.js +2 -0
  53. package/package.json +4 -3
@@ -0,0 +1,38 @@
1
+ import type { MailMessage } from './types.js';
2
+ /** Decode the HTML entities that show up in hrefs and body text: named basics and numeric. */
3
+ export declare function decodeEntities(s: string): string;
4
+ /** The HTML part as plain text: scripts and styles dropped, tags removed, whitespace collapsed. */
5
+ export declare function htmlToText(html: string): string;
6
+ /** Everything a reader could see: the text part, then the HTML part rendered to text. */
7
+ export declare function readableText(m: Pick<MailMessage, 'text' | 'html'>): string;
8
+ export interface MailLink {
9
+ href: string;
10
+ /** Visible text of the anchor; empty for a URL found in the text part. */
11
+ text: string;
12
+ }
13
+ /**
14
+ * Links in the message. Anchors in the HTML part come first, with `&amp;` and friends decoded
15
+ * (an href is HTML-escaped, and a verify link with `?token=…&amp;email=…` is the common case).
16
+ * URLs in the text part follow, so a text-only message still has links; duplicates are dropped.
17
+ */
18
+ export declare function extractLinks(m: Pick<MailMessage, 'text' | 'html'>): MailLink[];
19
+ /** Compile a scenario's pattern; one that is not a valid regular expression is matched literally. */
20
+ export declare function toPattern(pattern: string): RegExp;
21
+ /**
22
+ * The first link whose href contains `pattern` literally or matches it as a regular expression.
23
+ * Both, because a URL is the natural thing to write and its `?` and `.` are regex syntax:
24
+ * `https://app.test/verify?token=` is a valid regex that does NOT match its own text.
25
+ */
26
+ export declare function findLinkMatching(m: Pick<MailMessage, 'text' | 'html'>, pattern: string): MailLink | undefined;
27
+ /**
28
+ * The link a person would click by name: an anchor whose visible text equals `name`
29
+ * (case-insensitive, whitespace-normalised), else one whose text contains it, else one whose href
30
+ * contains it.
31
+ */
32
+ export declare function findLinkNamed(m: Pick<MailMessage, 'text' | 'html'>, name: string): MailLink | undefined;
33
+ /**
34
+ * The first match of `pattern` in the readable text: capture group 1 when the pattern has one,
35
+ * the whole match otherwise. `code: (\d{6})` saves the six digits, `\d{6}` saves the same.
36
+ */
37
+ export declare function findCode(m: Pick<MailMessage, 'text' | 'html'>, pattern: string): string | undefined;
38
+ //# sourceMappingURL=extract.d.ts.map
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Pure helpers over a fetched message: its readable text, its links and a code in it. Kept apart
3
+ * from the steps so the parsing, which fails quietly when wrong, is testable on its own.
4
+ */
5
+ const ENTITIES = {
6
+ amp: '&',
7
+ lt: '<',
8
+ gt: '>',
9
+ quot: '"',
10
+ apos: "'",
11
+ nbsp: ' ',
12
+ };
13
+ /** Decode the HTML entities that show up in hrefs and body text: named basics and numeric. */
14
+ export function decodeEntities(s) {
15
+ return s.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (m, ref) => {
16
+ if (ref[0] === '#') {
17
+ const code = ref[1]?.toLowerCase() === 'x' ? parseInt(ref.slice(2), 16) : parseInt(ref.slice(1), 10);
18
+ return Number.isFinite(code) ? String.fromCodePoint(code) : m;
19
+ }
20
+ return ENTITIES[ref.toLowerCase()] ?? m;
21
+ });
22
+ }
23
+ /** The HTML part as plain text: scripts and styles dropped, tags removed, whitespace collapsed. */
24
+ export function htmlToText(html) {
25
+ return decodeEntities(html
26
+ .replace(/<(script|style|head)\b[\s\S]*?<\/\1>/gi, ' ')
27
+ .replace(/<br\s*\/?>|<\/(p|div|tr|li|h[1-6])>/gi, '\n')
28
+ .replace(/<[^>]+>/g, ' '))
29
+ .replace(/[ \t\f\v\r]+/g, ' ')
30
+ .replace(/ *\n */g, '\n')
31
+ .trim();
32
+ }
33
+ /** Everything a reader could see: the text part, then the HTML part rendered to text. */
34
+ export function readableText(m) {
35
+ return [m.text, m.html ? htmlToText(m.html) : ''].filter(Boolean).join('\n');
36
+ }
37
+ /** Trailing punctuation that ends a sentence rather than a URL. */
38
+ const TRAILING = /[.,;:!?)\]}>'"]+$/;
39
+ /**
40
+ * Links in the message. Anchors in the HTML part come first, with `&amp;` and friends decoded
41
+ * (an href is HTML-escaped, and a verify link with `?token=…&amp;email=…` is the common case).
42
+ * URLs in the text part follow, so a text-only message still has links; duplicates are dropped.
43
+ */
44
+ export function extractLinks(m) {
45
+ const out = [];
46
+ const seen = new Set();
47
+ const add = (href, text) => {
48
+ if (!href || seen.has(href))
49
+ return;
50
+ seen.add(href);
51
+ out.push({ href, text });
52
+ };
53
+ const anchor = /<a\b[^>]*?\bhref\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))[^>]*>([\s\S]*?)<\/a>/gi;
54
+ for (const match of m.html.matchAll(anchor)) {
55
+ const href = decodeEntities((match[1] ?? match[2] ?? match[3] ?? '').trim());
56
+ if (/^(mailto:|tel:|#|javascript:)/i.test(href))
57
+ continue;
58
+ add(href, htmlToText(match[4] ?? ''));
59
+ }
60
+ for (const match of m.text.matchAll(/https?:\/\/[^\s<>"]+/gi)) {
61
+ add(match[0].replace(TRAILING, ''), '');
62
+ }
63
+ return out;
64
+ }
65
+ /** Compile a scenario's pattern; one that is not a valid regular expression is matched literally. */
66
+ export function toPattern(pattern) {
67
+ try {
68
+ return new RegExp(pattern);
69
+ }
70
+ catch {
71
+ return new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
72
+ }
73
+ }
74
+ /**
75
+ * The first link whose href contains `pattern` literally or matches it as a regular expression.
76
+ * Both, because a URL is the natural thing to write and its `?` and `.` are regex syntax:
77
+ * `https://app.test/verify?token=` is a valid regex that does NOT match its own text.
78
+ */
79
+ export function findLinkMatching(m, pattern) {
80
+ const re = toPattern(pattern);
81
+ return extractLinks(m).find((l) => l.href.includes(pattern) || re.test(l.href));
82
+ }
83
+ /**
84
+ * The link a person would click by name: an anchor whose visible text equals `name`
85
+ * (case-insensitive, whitespace-normalised), else one whose text contains it, else one whose href
86
+ * contains it.
87
+ */
88
+ export function findLinkNamed(m, name) {
89
+ const norm = (s) => s.replace(/\s+/g, ' ').trim().toLowerCase();
90
+ const wanted = norm(name);
91
+ const links = extractLinks(m);
92
+ return (links.find((l) => norm(l.text) === wanted) ??
93
+ links.find((l) => wanted !== '' && norm(l.text).includes(wanted)) ??
94
+ links.find((l) => l.href.includes(name)));
95
+ }
96
+ /**
97
+ * The first match of `pattern` in the readable text: capture group 1 when the pattern has one,
98
+ * the whole match otherwise. `code: (\d{6})` saves the six digits, `\d{6}` saves the same.
99
+ */
100
+ export function findCode(m, pattern) {
101
+ const match = toPattern(pattern).exec(readableText(m));
102
+ if (!match)
103
+ return undefined;
104
+ return match[1] ?? match[0];
105
+ }
106
+ //# sourceMappingURL=extract.js.map
@@ -0,0 +1,8 @@
1
+ import type { MailConfig } from '@sdods/contracts';
2
+ import type { MailInbox } from './types.js';
3
+ export type { MailAddress, MailInbox, MailMessage, MailSearch, MailSummary } from './types.js';
4
+ export { MailpitInbox, type MailpitOptions } from './mailpit.js';
5
+ export { decodeEntities, extractLinks, findCode, findLinkMatching, findLinkNamed, htmlToText, readableText, type MailLink, } from './extract.js';
6
+ /** The inbox adapter for an environment's `mail` block. */
7
+ export declare function createMailInbox(config: Pick<MailConfig, 'provider' | 'url' | 'auth'>): MailInbox;
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,11 @@
1
+ import { MailpitInbox } from './mailpit.js';
2
+ export { MailpitInbox } from './mailpit.js';
3
+ export { decodeEntities, extractLinks, findCode, findLinkMatching, findLinkNamed, htmlToText, readableText, } from './extract.js';
4
+ /** The inbox adapter for an environment's `mail` block. */
5
+ export function createMailInbox(config) {
6
+ switch (config.provider ?? 'mailpit') {
7
+ case 'mailpit':
8
+ return new MailpitInbox({ url: config.url, auth: config.auth });
9
+ }
10
+ }
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,26 @@
1
+ import type { MailInbox, MailMessage, MailSearch, MailSummary } from './types.js';
2
+ export interface MailpitOptions {
3
+ url: string;
4
+ auth?: {
5
+ type: 'basic';
6
+ username: string;
7
+ password: string;
8
+ };
9
+ }
10
+ /**
11
+ * Mailpit over its REST API (`/api/v1`).
12
+ *
13
+ * The server-side query is only `to:"<address>"`. Mailpit's `to:` is a substring match and its
14
+ * `subject:` is word-tokenised rather than a substring, so both are narrowed here, where the
15
+ * semantics are the ones the step text promises.
16
+ */
17
+ export declare class MailpitInbox implements MailInbox {
18
+ private readonly base;
19
+ private readonly headers;
20
+ constructor(opts: MailpitOptions);
21
+ clear(address: string): Promise<void>;
22
+ search(query: MailSearch): Promise<MailSummary[]>;
23
+ get(id: string): Promise<MailMessage>;
24
+ private call;
25
+ }
26
+ //# sourceMappingURL=mailpit.d.ts.map
@@ -0,0 +1,124 @@
1
+ import { SdodsError } from '../errors.js';
2
+ /** Page size for search. Mailpit's own default is 50; a per-address inbox rarely comes close. */
3
+ const PAGE = 100;
4
+ /** A hard stop on pagination, so a runaway inbox fails loudly instead of paging forever. */
5
+ const MAX_PAGES = 50;
6
+ /**
7
+ * Mailpit over its REST API (`/api/v1`).
8
+ *
9
+ * The server-side query is only `to:"<address>"`. Mailpit's `to:` is a substring match and its
10
+ * `subject:` is word-tokenised rather than a substring, so both are narrowed here, where the
11
+ * semantics are the ones the step text promises.
12
+ */
13
+ export class MailpitInbox {
14
+ base;
15
+ headers;
16
+ constructor(opts) {
17
+ this.base = opts.url.replace(/\/+$/, '');
18
+ this.headers = { accept: 'application/json' };
19
+ if (opts.auth) {
20
+ const token = Buffer.from(`${opts.auth.username}:${opts.auth.password}`).toString('base64');
21
+ this.headers.authorization = `Basic ${token}`;
22
+ }
23
+ }
24
+ async clear(address) {
25
+ const ids = (await this.search({ to: address })).map((m) => m.id);
26
+ if (ids.length === 0)
27
+ return;
28
+ // By ID rather than `DELETE /api/v1/search?query=to:"…"`: that query is a substring match
29
+ // and would also delete another scenario's mail to `b${address}`.
30
+ await this.call('DELETE', '/api/v1/messages', { IDs: ids });
31
+ }
32
+ async search(query) {
33
+ const wanted = query.to.trim().toLowerCase();
34
+ const q = `to:"${wanted.replace(/"/g, '')}"`;
35
+ const out = [];
36
+ for (let page = 0; page < MAX_PAGES; page++) {
37
+ const params = new URLSearchParams({
38
+ query: q,
39
+ start: String(page * PAGE),
40
+ limit: String(PAGE),
41
+ });
42
+ const body = (await this.call('GET', `/api/v1/search?${params}`));
43
+ const messages = body.messages ?? [];
44
+ for (const m of messages) {
45
+ const summary = toSummary(m);
46
+ if (!summary.to.some((t) => t.address.toLowerCase() === wanted))
47
+ continue;
48
+ if (query.since && summary.receivedAt.getTime() < query.since.getTime())
49
+ continue;
50
+ if (query.subjectContains !== undefined &&
51
+ !summary.subject.includes(query.subjectContains)) {
52
+ continue;
53
+ }
54
+ out.push(summary);
55
+ }
56
+ // Newest first: once a page ends before `since`, every later page is older still.
57
+ const oldest = messages.at(-1);
58
+ if (messages.length < PAGE)
59
+ break;
60
+ if (query.since && oldest && toDate(oldest.Created).getTime() < query.since.getTime())
61
+ break;
62
+ }
63
+ return out.sort((a, b) => b.receivedAt.getTime() - a.receivedAt.getTime());
64
+ }
65
+ async get(id) {
66
+ const m = (await this.call('GET', `/api/v1/message/${encodeURIComponent(id)}`));
67
+ return { ...toSummary(m), text: m.Text ?? '', html: m.HTML ?? '' };
68
+ }
69
+ async call(method, path, json) {
70
+ const url = `${this.base}${path}`;
71
+ let res;
72
+ try {
73
+ res = await fetch(url, {
74
+ method,
75
+ headers: json === undefined
76
+ ? this.headers
77
+ : { ...this.headers, 'content-type': 'application/json' },
78
+ body: json === undefined ? undefined : JSON.stringify(json),
79
+ });
80
+ }
81
+ catch (e) {
82
+ throw new SdodsError('MAIL_REQUIRED', `Could not reach Mailpit at ${this.base}.`, {
83
+ hint: 'Start Mailpit (for example `docker run -p 8025:8025 -p 1025:1025 axllent/mailpit`) and check `mail.url` in the environment yaml. The application under test must send its SMTP mail to the same Mailpit.',
84
+ cause: e,
85
+ details: { url, method },
86
+ });
87
+ }
88
+ if (!res.ok) {
89
+ const text = await res.text().catch(() => '');
90
+ throw new SdodsError('MAIL_REQUIRED', `Mailpit answered ${res.status} to ${method} ${path}.`, {
91
+ hint: res.status === 401 || res.status === 403
92
+ ? 'Mailpit is protected: set `mail.auth: { type: basic, username, password: ${VAR} }` in the environment yaml.'
93
+ : 'Check that `mail.url` points at the Mailpit web/API port (8025 by default), not the SMTP port.',
94
+ details: { url, method, status: res.status, body: text.slice(0, 500) },
95
+ });
96
+ }
97
+ const text = await res.text();
98
+ if (!text)
99
+ return {};
100
+ try {
101
+ return JSON.parse(text);
102
+ }
103
+ catch {
104
+ return {};
105
+ }
106
+ }
107
+ }
108
+ function toAddress(a) {
109
+ return { name: a?.Name ?? '', address: a?.Address ?? '' };
110
+ }
111
+ function toDate(s) {
112
+ const d = s ? new Date(s) : new Date(0);
113
+ return Number.isNaN(d.getTime()) ? new Date(0) : d;
114
+ }
115
+ function toSummary(m) {
116
+ return {
117
+ id: m.ID,
118
+ from: toAddress(m.From),
119
+ to: (m.To ?? []).map(toAddress),
120
+ subject: m.Subject ?? '',
121
+ receivedAt: toDate(m.Created ?? m.Date),
122
+ };
123
+ }
124
+ //# sourceMappingURL=mailpit.js.map
@@ -0,0 +1,38 @@
1
+ export interface MailAddress {
2
+ name: string;
3
+ address: string;
4
+ }
5
+ /** One message as a search returns it: enough to pick which message to open, no body. */
6
+ export interface MailSummary {
7
+ id: string;
8
+ from: MailAddress;
9
+ to: MailAddress[];
10
+ subject: string;
11
+ /** When the mail catcher received it, on the catcher's clock. */
12
+ receivedAt: Date;
13
+ }
14
+ /** A whole message. `text` and `html` are empty strings when that part is absent. */
15
+ export interface MailMessage extends MailSummary {
16
+ text: string;
17
+ html: string;
18
+ }
19
+ export interface MailSearch {
20
+ /** Exact recipient address, compared case-insensitively. */
21
+ to: string;
22
+ /** Case-sensitive substring of the subject. */
23
+ subjectContains?: string;
24
+ /** Only messages received at or after this instant. */
25
+ since?: Date;
26
+ }
27
+ /**
28
+ * A mail catcher the email steps read. Implementations return search results newest first and
29
+ * match `to` EXACTLY: a catcher whose own `to:` filter is a substring search (Mailpit's is) must
30
+ * narrow it client-side, or `a@x.test` would also match `ba@x.test`.
31
+ */
32
+ export interface MailInbox {
33
+ /** Delete every message addressed to `address`. */
34
+ clear(address: string): Promise<void>;
35
+ search(query: MailSearch): Promise<MailSummary[]>;
36
+ get(id: string): Promise<MailMessage>;
37
+ }
38
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,104 @@
1
+ import { ROLES_MATRIX_FILE, type ProjectConfig } from '@sdods/contracts';
2
+ /**
3
+ * Role matrix (#118): `roles.matrix.yaml` declares, per surface, the outcome each role should see;
4
+ * a `@matrix:<name>` Scenario Outline is the template; `sdods matrix expand` writes one `Examples:`
5
+ * block per role, tagged `@user:<role>`, between marker comments.
6
+ *
7
+ * playwright-bdd has no hook for external Examples, so the rows are generated INTO the feature file
8
+ * rather than injected at bddgen time. That keeps the generated tests visible in review, in
9
+ * `sdods features list`, and to every tool that reads Gherkin, at the cost of a generator that has
10
+ * to be idempotent and must never touch rows a person wrote.
11
+ */
12
+ export { ROLES_MATRIX_FILE };
13
+ export declare const MATRIX_BEGIN = "sdods:matrix:begin";
14
+ export declare const MATRIX_END = "sdods:matrix:end";
15
+ export interface RolesMatrix {
16
+ name: string;
17
+ description?: string;
18
+ roles: string[];
19
+ /** Row columns in declaration order, excluding `role` and `expect`. */
20
+ columns: string[];
21
+ /** `# title-format:` written above every generated block. */
22
+ title: string;
23
+ rows: Array<{
24
+ values: Record<string, string>;
25
+ expect: Record<string, string>;
26
+ }>;
27
+ /** 1-based line of `<name>:` in the yaml, for findings. */
28
+ line?: number;
29
+ }
30
+ export interface MatrixProblem {
31
+ rule: string;
32
+ severity: 'error' | 'warning';
33
+ message: string;
34
+ /** Relative to the project root. */
35
+ file: string;
36
+ line?: number;
37
+ }
38
+ export interface LoadedMatrices {
39
+ file: string;
40
+ /** Every name under `matrices:`, valid or not, so an invalid matrix is not also reported as unknown. */
41
+ declared: string[];
42
+ matrices: Map<string, RolesMatrix>;
43
+ problems: MatrixProblem[];
44
+ }
45
+ /** Read and validate `roles.matrix.yaml`. Undefined when the project has none. */
46
+ export declare function loadRolesMatrices(projectRoot: string): LoadedMatrices | undefined;
47
+ /** Validate matrix yaml text. Problems never throw: lint reports them as findings. */
48
+ export declare function parseRolesMatrices(text: string, file?: string): LoadedMatrices;
49
+ export interface ExpandResult {
50
+ text: string;
51
+ changed: boolean;
52
+ /** Generated example rows in the file after expansion. */
53
+ examples: number;
54
+ problems: MatrixProblem[];
55
+ }
56
+ /**
57
+ * Rewrite the generated Examples of every `@matrix:<name>` outline in one feature file.
58
+ * Pure: text in, text out. Hand-written Examples and everything outside the markers are kept.
59
+ */
60
+ export declare function expandFeatureText(text: string, matrices: ReadonlyMap<string, RolesMatrix>, opts?: {
61
+ file?: string;
62
+ }): ExpandResult;
63
+ /** The generated region for one outline: a title-format, a role tag and a table per role. */
64
+ export declare function renderBlock(matrix: RolesMatrix, indent: string): string[];
65
+ export interface MatrixFilePlan {
66
+ /** Absolute path. */
67
+ file: string;
68
+ /** Relative to the project root. */
69
+ path: string;
70
+ changed: boolean;
71
+ examples: number;
72
+ before: string;
73
+ after: string;
74
+ problems: MatrixProblem[];
75
+ }
76
+ export interface MatrixPlan {
77
+ loaded?: LoadedMatrices;
78
+ files: MatrixFilePlan[];
79
+ /** Config, role and pool findings (not tied to one feature). */
80
+ problems: MatrixProblem[];
81
+ }
82
+ /**
83
+ * Everything `sdods matrix expand`, `--check`, lint and the MCP tool need, computed once:
84
+ * the matrices, their project-level findings, and the before/after text of every feature that
85
+ * uses (or used to use) a matrix.
86
+ */
87
+ export declare function planMatrixExpansion(project: ProjectConfig & {
88
+ root: string;
89
+ }, featureFiles: string[]): MatrixPlan;
90
+ /**
91
+ * Roles the matrix names must be roles the project declares, and each needs an account to lease.
92
+ *
93
+ * The matrix does not create identities. A role with no pool row turns every generated scenario for
94
+ * that role red with a lease error that says nothing about the product, so this is surfaced before
95
+ * anything runs — per environment, because a role seeded on staging may not exist locally.
96
+ */
97
+ export declare function checkMatrixRoles(project: ProjectConfig & {
98
+ root: string;
99
+ }, matrices: ReadonlyMap<string, RolesMatrix>): MatrixProblem[];
100
+ /** Roles present in the user-pool dataset, per available environment. Undefined if not file-backed. */
101
+ export declare function poolRolesByEnv(project: ProjectConfig & {
102
+ root: string;
103
+ }): Map<string, Set<string>> | undefined;
104
+ //# sourceMappingURL=index.d.ts.map