@crvouga/atlas 1.1.0 → 1.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/CHANGELOG.md +12 -0
- package/dist/reports/src/artifact-query.d.ts +123 -0
- package/dist/reports/src/artifact-query.js +234 -0
- package/dist/reports/src/evidence-detail.d.ts +14 -3
- package/dist/reports/src/evidence-detail.js +34 -19
- package/dist/reports/src/evidence-scope.d.ts +83 -0
- package/dist/reports/src/evidence-scope.js +129 -0
- package/dist/reports/src/evidence-types.d.ts +5 -1
- package/dist/reports/src/explorer-types.d.ts +1 -1
- package/dist/reports/src/layer-merge.js +3 -2
- package/dist/reports/src/layer-types.d.ts +1 -1
- package/dist/reports/src/publish-checks.js +1 -0
- package/dist/reports/src/reporter.d.ts +1 -1
- package/dist/reports/src/reports-explorer-route.d.ts +276 -0
- package/dist/reports/src/reports-route.d.ts +276 -0
- package/dist/reports/src/test-catalog.d.ts +108 -6
- package/dist/reports/src/test-catalog.js +155 -8
- package/dist/schema/src/api.d.ts +745 -0
- package/dist/schema/src/api.js +48 -6
- package/dist/schema/src/layer.d.ts +981 -0
- package/dist/schema/src/layer.js +43 -4
- package/dist/server/src/app.js +16 -5
- package/dist/server/src/openapi.js +42 -3
- package/docs/extending.md +1 -0
- package/docs/reference/http-api.md +86 -2
- package/docs/reference/reports.md +550 -14
- package/docs/reference/schema.md +55 -0
- package/docs/rich-evidence.md +78 -0
- package/docs/server.md +4 -1
- package/docs/ui.md +1 -1
- package/llms-full.txt +170 -4
- package/package.json +1 -1
- package/packages/schema/schemas/check-detail.schema.json +673 -1
- package/packages/schema/schemas/effective-delta.schema.json +206 -0
- package/packages/schema/schemas/effective-evidence.schema.json +206 -0
- package/packages/schema/schemas/evidence-page.schema.json +103 -0
- package/packages/schema/schemas/layer-rows.schema.json +103 -0
- package/packages/schema/schemas/layer.schema.json +1598 -62
- package/packages/schema/schemas/publish-checks.schema.json +768 -0
- package/packages/schema/schemas/report-run.schema.json +131 -0
- package/packages/schema/schemas/report-runs.schema.json +221 -0
- package/packages/schema/schemas/test-catalog.schema.json +135 -0
- package/packages/schema/schemas/test-history.schema.json +216 -0
- package/packages/server/openapi.json +4731 -735
- package/packages/visualizer/src/evidence/artifacts.tsx +24 -18
- package/packages/visualizer/src/evidence/evidence-scope.tsx +106 -0
- package/packages/visualizer/src/evidence/format.ts +11 -0
- package/packages/visualizer/src/explorer/ArtifactsView.tsx +162 -0
- package/packages/visualizer/src/explorer/Explorer.tsx +47 -3
- package/packages/visualizer/src/explorer/RunsView.tsx +43 -19
- package/packages/visualizer/src/explorer/catalog.tsx +97 -12
- package/packages/visualizer/src/explorer/filters.tsx +279 -0
- package/packages/visualizer/src/explorer/search.ts +85 -6
- package/packages/visualizer/src/explorer/views.ts +75 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog: @crvouga/atlas
|
|
2
2
|
|
|
3
|
+
## 1.3.0 (2026-10-28)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- expose readable evidence scope for synthetic, scripted, simulator and physical runs ([fae5d02](https://github.com/crvouga/atlas/commit/fae5d02c7bdaecdfdf4ec113b23f6b11f9e3e012))
|
|
8
|
+
|
|
9
|
+
## 1.2.0 (2026-10-28)
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
- filter runs and artifacts by tags, labels and media type ([ae89175](https://github.com/crvouga/atlas/commit/ae8917551ab82e469e3b35244bf9d570986be76d))
|
|
14
|
+
|
|
3
15
|
## 1.1.0 (2026-10-28)
|
|
4
16
|
|
|
5
17
|
### Features
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/** A value of a dimension and how many records have it. */
|
|
2
|
+
export type QueryFacetValue = {
|
|
3
|
+
readonly value: string;
|
|
4
|
+
readonly count: number;
|
|
5
|
+
};
|
|
6
|
+
/** The dimensions a query narrows by, with their address parameters. */
|
|
7
|
+
export declare const QUERY_DIMENSIONS: readonly ["artifactTag", "type", "label", "environment", "producer", "branch", "sha", "outcome", "domain", "spec"];
|
|
8
|
+
/** One of {@link QUERY_DIMENSIONS}. */
|
|
9
|
+
export type QueryDimension = (typeof QUERY_DIMENSIONS)[number];
|
|
10
|
+
/** What a query asks for. Every field is optional; nothing asked is everything. See the module comment for the semantics. */
|
|
11
|
+
export type ArtifactFilter = {
|
|
12
|
+
/** Words (all of them) found in the title, test id, spec, domain, branch, labels, tags, environment or producer; or, for a run, its title. */
|
|
13
|
+
readonly q?: string;
|
|
14
|
+
/** Artifact tags (`demo` and `#demo` are the same). */
|
|
15
|
+
readonly artifactTag?: readonly string[];
|
|
16
|
+
/** Also match the tags an artifact inherits from its check and run. Default false: the exact artifact query. */
|
|
17
|
+
readonly inherited?: boolean;
|
|
18
|
+
/** Media types as roles: `video`, `image`, `audio`, `snapshot`, `text`, `file`, or a producer's own role. `video` means a playable recording. */
|
|
19
|
+
readonly type?: readonly string[];
|
|
20
|
+
/** `key=value`, or `key` for any value. */
|
|
21
|
+
readonly label?: readonly string[];
|
|
22
|
+
/** Environments or devices (`ios`, `desktop-web`). */
|
|
23
|
+
readonly environment?: readonly string[];
|
|
24
|
+
readonly producer?: readonly string[];
|
|
25
|
+
readonly branch?: readonly string[];
|
|
26
|
+
/** Revisions: a full commit id, or a prefix of at least seven characters. */
|
|
27
|
+
readonly sha?: readonly string[];
|
|
28
|
+
/** Statuses (`passed`, `failed`, `running`, `not-run`). A run's outcome is `failed` when any result failed, else `running`, else `passed`, else `not-run`. */
|
|
29
|
+
readonly outcome?: readonly string[];
|
|
30
|
+
/** The feature domain of a result's spec. Not applicable to runs. */
|
|
31
|
+
readonly domain?: readonly string[];
|
|
32
|
+
/** Spec paths. Not applicable to runs. */
|
|
33
|
+
readonly spec?: readonly string[];
|
|
34
|
+
/** Only records at or after this time (epoch ms): a result's recording time, a run's end. */
|
|
35
|
+
readonly since?: number;
|
|
36
|
+
/** Only records at or before this time (epoch ms). */
|
|
37
|
+
readonly until?: number;
|
|
38
|
+
/** Only records with a playable video recording (the same as `type: ['video']`). */
|
|
39
|
+
readonly video?: boolean;
|
|
40
|
+
};
|
|
41
|
+
/** The filters a query used, as they are named in the address and in `filters` of a response. */
|
|
42
|
+
export type FilterName = QueryDimension | 'q' | 'inherited' | 'since' | 'until' | 'video';
|
|
43
|
+
/** The values of a record a query looks at: made by {@link fieldsOfRun} or by the artifact search. */
|
|
44
|
+
export type FilterFields = {
|
|
45
|
+
/** Lower-cased text a search matches. */
|
|
46
|
+
readonly text: string;
|
|
47
|
+
/** The normalized tags of its artifacts. */
|
|
48
|
+
readonly artifactTags: readonly string[];
|
|
49
|
+
/** The tags its artifacts inherit from checks and runs, and its own. */
|
|
50
|
+
readonly tags: readonly string[];
|
|
51
|
+
/** Distinct media types; `video` only when a recording can be played. */
|
|
52
|
+
readonly types: readonly string[];
|
|
53
|
+
/** Artifacts by type and their own tags. Where the report does not say which artifact carries which tag, every artifact of the record is given the record's artifact tags. */
|
|
54
|
+
readonly media: readonly {
|
|
55
|
+
readonly type: string;
|
|
56
|
+
readonly tags: readonly string[];
|
|
57
|
+
}[];
|
|
58
|
+
/** `key=value`, sorted and distinct. */
|
|
59
|
+
readonly labels: readonly string[];
|
|
60
|
+
readonly environments: readonly string[];
|
|
61
|
+
readonly producers: readonly string[];
|
|
62
|
+
readonly branches: readonly string[];
|
|
63
|
+
readonly shas: readonly string[];
|
|
64
|
+
readonly outcomes: readonly string[];
|
|
65
|
+
/** Absent when the dimension does not apply (runs have no domain). */
|
|
66
|
+
readonly domains?: readonly string[];
|
|
67
|
+
readonly specs?: readonly string[];
|
|
68
|
+
readonly at: number;
|
|
69
|
+
};
|
|
70
|
+
/** The facets of a query: for each dimension, its values with disjunctive counts (see the module comment). */
|
|
71
|
+
export type QueryFacets = {
|
|
72
|
+
readonly artifactTags: readonly QueryFacetValue[];
|
|
73
|
+
readonly types: readonly QueryFacetValue[];
|
|
74
|
+
/** Values are `key=value`. */
|
|
75
|
+
readonly labels: readonly QueryFacetValue[];
|
|
76
|
+
readonly environments: readonly QueryFacetValue[];
|
|
77
|
+
readonly producers: readonly QueryFacetValue[];
|
|
78
|
+
readonly branches: readonly QueryFacetValue[];
|
|
79
|
+
readonly outcomes: readonly QueryFacetValue[];
|
|
80
|
+
readonly domains?: readonly QueryFacetValue[];
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Splits a comma-separated list from an address: `a,b` is two values; `\,` is a comma and `\\` a backslash inside a
|
|
84
|
+
* value. Values are trimmed and empty ones dropped.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```ts
|
|
88
|
+
* splitList('demo,release\\,2'); // ['demo', 'release,2']
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export declare function splitList(raw: string): string[];
|
|
92
|
+
/** Joins values into one address parameter that {@link splitList} reads back; `undefined` when there are none. */
|
|
93
|
+
export declare function joinList(list: readonly string[]): string | undefined;
|
|
94
|
+
/** A time from an address: epoch milliseconds, or a date or date-time Date can read (a date alone is UTC). `undefined` when unreadable. */
|
|
95
|
+
export declare function parseTime(raw: string | null | undefined): number | undefined;
|
|
96
|
+
/**
|
|
97
|
+
* Reads a query from URL search parameters: `q`, `video`, `inherited`, `since`, `until` and the repeatable or
|
|
98
|
+
* comma-separated {@link QUERY_DIMENSIONS}. Unreadable values are left out, never an error.
|
|
99
|
+
*/
|
|
100
|
+
export declare function parseArtifactFilter(params: URLSearchParams): ArtifactFilter;
|
|
101
|
+
/** A query as URL search parameters, the form {@link parseArtifactFilter} reads back. */
|
|
102
|
+
export declare function filterParams(filter: ArtifactFilter): URLSearchParams;
|
|
103
|
+
/** Which filters a query uses, by the names an address gives them. */
|
|
104
|
+
export declare function usedFilters(filter: ArtifactFilter): FilterName[];
|
|
105
|
+
/** Whether a query narrows anything. */
|
|
106
|
+
export declare function isFiltered(filter: ArtifactFilter): boolean;
|
|
107
|
+
/** Every filter in `filter` that may be applied to a record with these fields, as the runs query applies them (no domain or spec). */
|
|
108
|
+
export declare function forRuns(filter: ArtifactFilter): ArtifactFilter;
|
|
109
|
+
/**
|
|
110
|
+
* Whether a record satisfies a query. `skip` leaves one dimension out: what {@link facetsOf} uses to count the values of
|
|
111
|
+
* that dimension, and nothing else should need.
|
|
112
|
+
*/
|
|
113
|
+
export declare function matchesFilter(fields: FilterFields, filter: ArtifactFilter, skip?: QueryDimension): boolean;
|
|
114
|
+
/**
|
|
115
|
+
* The facets of a query over every record: for each dimension, the values the records have and how many records
|
|
116
|
+
* would match if that value were chosen in its dimension (see the module comment). A value chosen in the query is
|
|
117
|
+
* listed even when nothing has it, with a count of 0, so it can be removed.
|
|
118
|
+
*/
|
|
119
|
+
export declare function facetsOf(records: readonly FilterFields[], filter: ArtifactFilter): QueryFacets;
|
|
120
|
+
/** The outcome of a run from its results by status: `failed`, else `running`, else `passed`, else `not-run`. */
|
|
121
|
+
export declare function outcomeOfCounts(counts: Readonly<Record<string, number>>): string;
|
|
122
|
+
/** The `key=value` labels of a record, sorted and distinct. */
|
|
123
|
+
export declare function labelList(labels: Readonly<Record<string, string>> | undefined): string[];
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Run and artifact queries
|
|
3
|
+
*
|
|
4
|
+
* One shareable query for the runs of a project and for the recorded results that carry evidence (recordings,
|
|
5
|
+
* screenshots, traces): by artifact tag, label, media type, feature (domain, spec), environment or device, producer,
|
|
6
|
+
* branch, revision, outcome and time. The same rules answer `GET /api/reports/runs`, `GET /api/reports/artifacts`
|
|
7
|
+
* and the viewer's Runs and Artifacts views, so a link means the same thing everywhere.
|
|
8
|
+
*
|
|
9
|
+
* The semantics are fixed, not inferred:
|
|
10
|
+
*
|
|
11
|
+
* - Across dimensions the filters combine with AND: `artifactTag=demo&type=video&environment=ios` is a demo
|
|
12
|
+
* recording on ios.
|
|
13
|
+
* - Within a dimension the values combine with OR: `environment=ios,android` is either.
|
|
14
|
+
* - When both `artifactTag` and `type` are asked for, one artifact must be both (a `demo`-tagged video, not a demo-tagged
|
|
15
|
+
* screenshot beside an untagged video) wherever the report says which videos carry which tags; otherwise a result with
|
|
16
|
+
* an artifact of that type and an artifact with that tag matches.
|
|
17
|
+
* - Facet counts are disjunctive: the count beside a value is how many records the list would hold if that value were
|
|
18
|
+
* the only choice in its dimension, with every other dimension's filter applied. With a value already chosen, the
|
|
19
|
+
* other values show what choosing them in addition would add, and the chosen one's count is the list's.
|
|
20
|
+
* - A filter never changes a status. Evidence that is not recorded is unknown, never a match.
|
|
21
|
+
*
|
|
22
|
+
* Lists in the address are comma-separated; a comma or backslash inside a value is written `\,` or `\\`
|
|
23
|
+
* ({@link splitList}, {@link joinList}), so a label such as `note=a,b` survives.
|
|
24
|
+
*
|
|
25
|
+
* @see docs/server.md#reading-the-report-as-an-agent
|
|
26
|
+
*/
|
|
27
|
+
import { normalizeTag } from './artifact-tags.js';
|
|
28
|
+
/** The dimensions a query narrows by, with their address parameters. */
|
|
29
|
+
export const QUERY_DIMENSIONS = ['artifactTag', 'type', 'label', 'environment', 'producer', 'branch', 'sha', 'outcome', 'domain', 'spec'];
|
|
30
|
+
/**
|
|
31
|
+
* Splits a comma-separated list from an address: `a,b` is two values; `\,` is a comma and `\\` a backslash inside a
|
|
32
|
+
* value. Values are trimmed and empty ones dropped.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* splitList('demo,release\\,2'); // ['demo', 'release,2']
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function splitList(raw) {
|
|
40
|
+
const out = [];
|
|
41
|
+
let current = '';
|
|
42
|
+
for (let at = 0; at < raw.length; at += 1) {
|
|
43
|
+
const char = raw[at];
|
|
44
|
+
const next = raw[at + 1];
|
|
45
|
+
if (char === '\\' && (next === ',' || next === '\\')) {
|
|
46
|
+
current += next;
|
|
47
|
+
at += 1;
|
|
48
|
+
}
|
|
49
|
+
else if (char === ',') {
|
|
50
|
+
out.push(current);
|
|
51
|
+
current = '';
|
|
52
|
+
}
|
|
53
|
+
else
|
|
54
|
+
current += char;
|
|
55
|
+
}
|
|
56
|
+
out.push(current);
|
|
57
|
+
return out.map(value => value.trim()).filter(Boolean);
|
|
58
|
+
}
|
|
59
|
+
/** Joins values into one address parameter that {@link splitList} reads back; `undefined` when there are none. */
|
|
60
|
+
export function joinList(list) {
|
|
61
|
+
return list.length ? list.map(value => value.replace(/\\/g, '\\\\').replace(/,/g, '\\,')).join(',') : undefined;
|
|
62
|
+
}
|
|
63
|
+
/** A time from an address: epoch milliseconds, or a date or date-time Date can read (a date alone is UTC). `undefined` when unreadable. */
|
|
64
|
+
export function parseTime(raw) {
|
|
65
|
+
if (!raw)
|
|
66
|
+
return undefined;
|
|
67
|
+
const value = /^\d+$/.test(raw.trim()) ? Number(raw) : Date.parse(raw);
|
|
68
|
+
return Number.isFinite(value) ? value : undefined;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Reads a query from URL search parameters: `q`, `video`, `inherited`, `since`, `until` and the repeatable or
|
|
72
|
+
* comma-separated {@link QUERY_DIMENSIONS}. Unreadable values are left out, never an error.
|
|
73
|
+
*/
|
|
74
|
+
export function parseArtifactFilter(params) {
|
|
75
|
+
const many = (name) => params.getAll(name).flatMap(splitList);
|
|
76
|
+
const filter = {};
|
|
77
|
+
const q = params.get('q')?.trim();
|
|
78
|
+
if (q)
|
|
79
|
+
filter.q = q;
|
|
80
|
+
for (const name of QUERY_DIMENSIONS) {
|
|
81
|
+
const values = many(name);
|
|
82
|
+
if (values.length)
|
|
83
|
+
filter[name] = values;
|
|
84
|
+
}
|
|
85
|
+
const truthy = (name) => ['1', 'true'].includes(params.get(name) ?? '');
|
|
86
|
+
if (truthy('inherited'))
|
|
87
|
+
filter.inherited = true;
|
|
88
|
+
if (truthy('video'))
|
|
89
|
+
filter.video = true;
|
|
90
|
+
const since = parseTime(params.get('since'));
|
|
91
|
+
if (since !== undefined)
|
|
92
|
+
filter.since = since;
|
|
93
|
+
const until = parseTime(params.get('until'));
|
|
94
|
+
if (until !== undefined)
|
|
95
|
+
filter.until = until;
|
|
96
|
+
return filter;
|
|
97
|
+
}
|
|
98
|
+
/** A query as URL search parameters, the form {@link parseArtifactFilter} reads back. */
|
|
99
|
+
export function filterParams(filter) {
|
|
100
|
+
const params = new URLSearchParams();
|
|
101
|
+
if (filter.q)
|
|
102
|
+
params.set('q', filter.q);
|
|
103
|
+
for (const name of QUERY_DIMENSIONS) {
|
|
104
|
+
const joined = joinList(filter[name] ?? []);
|
|
105
|
+
if (joined)
|
|
106
|
+
params.set(name, joined);
|
|
107
|
+
}
|
|
108
|
+
if (filter.inherited)
|
|
109
|
+
params.set('inherited', '1');
|
|
110
|
+
if (filter.since !== undefined)
|
|
111
|
+
params.set('since', String(filter.since));
|
|
112
|
+
if (filter.until !== undefined)
|
|
113
|
+
params.set('until', String(filter.until));
|
|
114
|
+
if (filter.video)
|
|
115
|
+
params.set('video', 'true');
|
|
116
|
+
return params;
|
|
117
|
+
}
|
|
118
|
+
/** Which filters a query uses, by the names an address gives them. */
|
|
119
|
+
export function usedFilters(filter) {
|
|
120
|
+
const used = [];
|
|
121
|
+
if (filter.q)
|
|
122
|
+
used.push('q');
|
|
123
|
+
for (const name of QUERY_DIMENSIONS)
|
|
124
|
+
if (filter[name]?.length)
|
|
125
|
+
used.push(name);
|
|
126
|
+
if (filter.inherited)
|
|
127
|
+
used.push('inherited');
|
|
128
|
+
if (filter.since !== undefined)
|
|
129
|
+
used.push('since');
|
|
130
|
+
if (filter.until !== undefined)
|
|
131
|
+
used.push('until');
|
|
132
|
+
if (filter.video)
|
|
133
|
+
used.push('video');
|
|
134
|
+
return used;
|
|
135
|
+
}
|
|
136
|
+
/** Whether a query narrows anything. */
|
|
137
|
+
export function isFiltered(filter) {
|
|
138
|
+
return usedFilters(filter).some(name => name !== 'inherited');
|
|
139
|
+
}
|
|
140
|
+
/** Every filter in `filter` that may be applied to a record with these fields, as the runs query applies them (no domain or spec). */
|
|
141
|
+
export function forRuns(filter) {
|
|
142
|
+
const { domain: _domain, spec: _spec, ...rest } = filter;
|
|
143
|
+
return rest;
|
|
144
|
+
}
|
|
145
|
+
function hasTag(tags, wanted) {
|
|
146
|
+
return wanted.some(tag => { const normal = normalizeTag(tag); return normal !== null && tags.includes(normal); });
|
|
147
|
+
}
|
|
148
|
+
function labelMatches(labels, wanted) {
|
|
149
|
+
return wanted.includes('=') ? labels.includes(wanted) : labels.some(label => label.startsWith(`${wanted}=`));
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Whether a record satisfies a query. `skip` leaves one dimension out: what {@link facetsOf} uses to count the values of
|
|
153
|
+
* that dimension, and nothing else should need.
|
|
154
|
+
*/
|
|
155
|
+
export function matchesFilter(fields, filter, skip) {
|
|
156
|
+
if (filter.q) {
|
|
157
|
+
for (const term of filter.q.toLowerCase().split(/\s+/).filter(Boolean))
|
|
158
|
+
if (!fields.text.includes(term))
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
const tags = skip === 'artifactTag' ? [] : filter.artifactTag ?? [];
|
|
162
|
+
const types = skip === 'type' ? [] : [...(filter.type ?? []), ...(filter.video ? ['video'] : [])];
|
|
163
|
+
if (tags.length && types.length) {
|
|
164
|
+
if (!fields.media.some(item => types.includes(item.type) && (hasTag(item.tags, tags) || (filter.inherited === true && hasTag(fields.tags, tags)))))
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
if (tags.length && !hasTag(fields.artifactTags, tags) && !(filter.inherited === true && hasTag(fields.tags, tags)))
|
|
169
|
+
return false;
|
|
170
|
+
if (types.length && !types.some(type => fields.types.includes(type)))
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
if (skip !== 'label' && filter.label?.length && !filter.label.some(wanted => labelMatches(fields.labels, wanted)))
|
|
174
|
+
return false;
|
|
175
|
+
if (skip !== 'environment' && filter.environment?.length && !filter.environment.some(value => fields.environments.includes(value)))
|
|
176
|
+
return false;
|
|
177
|
+
if (skip !== 'producer' && filter.producer?.length && !filter.producer.some(value => fields.producers.includes(value)))
|
|
178
|
+
return false;
|
|
179
|
+
if (skip !== 'branch' && filter.branch?.length && !filter.branch.some(value => fields.branches.includes(value)))
|
|
180
|
+
return false;
|
|
181
|
+
if (skip !== 'sha' && filter.sha?.length && !filter.sha.some(value => value.length >= 7 && fields.shas.some(sha => sha.startsWith(value.toLowerCase()))))
|
|
182
|
+
return false;
|
|
183
|
+
if (skip !== 'outcome' && filter.outcome?.length && !filter.outcome.some(value => fields.outcomes.includes(value)))
|
|
184
|
+
return false;
|
|
185
|
+
if (skip !== 'domain' && filter.domain?.length && fields.domains && !filter.domain.some(value => fields.domains.includes(value)))
|
|
186
|
+
return false;
|
|
187
|
+
if (skip !== 'spec' && filter.spec?.length && fields.specs && !filter.spec.some(value => fields.specs.includes(value)))
|
|
188
|
+
return false;
|
|
189
|
+
if (filter.since !== undefined && fields.at < filter.since)
|
|
190
|
+
return false;
|
|
191
|
+
if (filter.until !== undefined && fields.at > filter.until)
|
|
192
|
+
return false;
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
/** Distinct values counted, most first and then alphabetically. */
|
|
196
|
+
function tally(values) {
|
|
197
|
+
const counts = new Map();
|
|
198
|
+
for (const list of values)
|
|
199
|
+
for (const value of new Set(list))
|
|
200
|
+
counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
201
|
+
return [...counts].map(([value, count]) => ({ value, count })).sort((a, b) => b.count - a.count || (a.value < b.value ? -1 : a.value > b.value ? 1 : 0));
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* The facets of a query over every record: for each dimension, the values the records have and how many records
|
|
205
|
+
* would match if that value were chosen in its dimension (see the module comment). A value chosen in the query is
|
|
206
|
+
* listed even when nothing has it, with a count of 0, so it can be removed.
|
|
207
|
+
*/
|
|
208
|
+
export function facetsOf(records, filter) {
|
|
209
|
+
const scope = (skip) => records.filter(record => matchesFilter(record, filter, skip));
|
|
210
|
+
const withChosen = (found, chosen) => [
|
|
211
|
+
...found, ...(chosen ?? []).filter(value => !found.some(entry => entry.value === value)).map(value => ({ value, count: 0 }))
|
|
212
|
+
];
|
|
213
|
+
const dimension = (skip, pick, chosen) => withChosen(tally(scope(skip).map(pick)), chosen);
|
|
214
|
+
const normalTags = (filter.artifactTag ?? []).map(tag => normalizeTag(tag)).filter((tag) => tag !== null);
|
|
215
|
+
const domains = records.some(record => record.domains);
|
|
216
|
+
return {
|
|
217
|
+
artifactTags: dimension('artifactTag', fields => fields.artifactTags, normalTags),
|
|
218
|
+
types: dimension('type', fields => fields.types, filter.type),
|
|
219
|
+
labels: dimension('label', fields => fields.labels, (filter.label ?? []).filter(value => value.includes('='))),
|
|
220
|
+
environments: dimension('environment', fields => fields.environments, filter.environment),
|
|
221
|
+
producers: dimension('producer', fields => fields.producers, filter.producer),
|
|
222
|
+
branches: dimension('branch', fields => fields.branches, filter.branch),
|
|
223
|
+
outcomes: dimension('outcome', fields => fields.outcomes, filter.outcome),
|
|
224
|
+
...(domains ? { domains: dimension('domain', fields => fields.domains ?? [], filter.domain) } : {})
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/** The outcome of a run from its results by status: `failed`, else `running`, else `passed`, else `not-run`. */
|
|
228
|
+
export function outcomeOfCounts(counts) {
|
|
229
|
+
return (counts.failed ?? 0) > 0 ? 'failed' : (counts.running ?? 0) > 0 ? 'running' : (counts.passed ?? 0) > 0 ? 'passed' : 'not-run';
|
|
230
|
+
}
|
|
231
|
+
/** The `key=value` labels of a record, sorted and distinct. */
|
|
232
|
+
export function labelList(labels) {
|
|
233
|
+
return Object.entries(labels ?? {}).map(([key, value]) => `${key}=${value}`).sort();
|
|
234
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* @see docs/extending.md
|
|
10
10
|
* @see docs/report-contract.md (evidence)
|
|
11
11
|
*/
|
|
12
|
-
import type { Annotation, Attachment, Attempt, Check, Step } from '../../schema/src/layer.js';
|
|
12
|
+
import type { Annotation, Attachment, Attempt, Check, EvidenceScope, Step } from '../../schema/src/layer.js';
|
|
13
13
|
import type { EvidenceSummary, VideoCounts, VideoRef } from './evidence-types.js';
|
|
14
14
|
/** Anything that may carry evidence: a layer, a part or a check. */
|
|
15
15
|
export type EvidenceHolder = {
|
|
@@ -25,6 +25,8 @@ export type EvidenceHolder = {
|
|
|
25
25
|
readonly steps?: readonly Step[];
|
|
26
26
|
readonly attempts?: readonly Attempt[];
|
|
27
27
|
readonly extensions?: Readonly<Record<string, unknown>>;
|
|
28
|
+
/** What its evidence shows, as its producer declared it (see `EvidenceScope`). */
|
|
29
|
+
readonly scope?: EvidenceScope;
|
|
28
30
|
};
|
|
29
31
|
/**
|
|
30
32
|
* What a result carries beyond its outcome, as counts and short facts. It rides in every row of the
|
|
@@ -35,9 +37,15 @@ export type EvidenceHolder = {
|
|
|
35
37
|
* attempts, `steps` counts only each holder's own top-level steps, tags are de-duplicated, and for a repeated
|
|
36
38
|
* label the later holder wins. `artifactTags` are the attachments' own normalized tags and are never inherited. `roles` are the attachments' roles (or, without one, what their media type or file
|
|
37
39
|
* name implies), sorted and distinct. `videos` lists every attachment whose effective media type is video, with the
|
|
38
|
-
* check, retry attempt or step that holds it (see {@link isVideoAttachment}).
|
|
40
|
+
* check, retry attempt or step that holds it (see {@link isVideoAttachment}), each with the scope that applies to it.
|
|
39
41
|
*/
|
|
40
42
|
export declare function summarizeEvidence(...holders: readonly EvidenceHolder[]): EvidenceSummary | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Like {@link summarizeEvidence} for the checks of a part: a video that declares no scope, and sits in a holder that
|
|
45
|
+
* declares none, takes the scope `inherited` from its part or layer (the nearest declaration of the two, see
|
|
46
|
+
* `effectiveScope`). Without `inherited` it is the same as `summarizeEvidence`.
|
|
47
|
+
*/
|
|
48
|
+
export declare function summarizeEvidenceIn(inherited: EvidenceScope | undefined, ...holders: readonly EvidenceHolder[]): EvidenceSummary | undefined;
|
|
41
49
|
/**
|
|
42
50
|
* Like {@link summarizeEvidence} for a layer and its parts: the same summary, but a video attached to the layer or a
|
|
43
51
|
* part is owned by the `layer`, not by a check.
|
|
@@ -66,6 +74,8 @@ export type CheckDetail = {
|
|
|
66
74
|
readonly check: Check;
|
|
67
75
|
/** What the run as a whole carries: its logs, its links, its labels. */
|
|
68
76
|
readonly run: EvidenceHolder;
|
|
77
|
+
/** The scope the check inherits: its part's declaration, else its layer's. Absent when neither declared one, which is unknown, not live or physical. */
|
|
78
|
+
readonly inheritedScope?: EvidenceScope;
|
|
69
79
|
};
|
|
70
80
|
/** One result's evidence, from a layer: the first check with id `checkId` (in a part of kind `kind`, when given) plus the layer's own evidence. Null when the layer holds no such check. */
|
|
71
81
|
export declare function checkDetailOf(layer: {
|
|
@@ -73,9 +83,10 @@ export declare function checkDetailOf(layer: {
|
|
|
73
83
|
readonly parts: readonly {
|
|
74
84
|
readonly kind: string;
|
|
75
85
|
readonly checks: readonly Check[];
|
|
86
|
+
readonly scope?: EvidenceScope;
|
|
76
87
|
}[];
|
|
77
88
|
} & EvidenceHolder, checkId: string, kind?: string): CheckDetail | null;
|
|
78
|
-
/** What a layer carries as a whole (its tags, labels, links, attachments, annotations and extensions; not its parts' checks). Absent fields stay absent. */
|
|
89
|
+
/** What a layer carries as a whole (its scope, tags, labels, links, attachments, annotations and extensions; not its parts' checks). Absent fields stay absent. */
|
|
79
90
|
export declare function runEvidence(layer: EvidenceHolder): EvidenceHolder;
|
|
80
91
|
/** Every distinct bundle key (`bundleKey`) that the attachments of a holder, its steps and its attempts point to, so a layer's files can be copied. Attachments held by `uri` or inline `text` have none. */
|
|
81
92
|
export declare function bundleKeysOf(holder: EvidenceHolder): string[];
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalizeTags } from './artifact-tags.js';
|
|
2
|
+
import { effectiveScope } from './evidence-scope.js';
|
|
2
3
|
import { contentTypeOf } from './media-types.js';
|
|
3
4
|
function count(holder) {
|
|
4
5
|
return (holder.attachments?.length ?? 0) + (holder.steps?.length ?? 0) + (holder.annotations?.length ?? 0) + (holder.attempts?.length ?? 0) + (holder.tags?.length ?? 0) + (holder.links?.length ?? 0) + Object.keys(holder.labels ?? {}).length + Object.keys(holder.extensions ?? {}).length;
|
|
@@ -12,19 +13,28 @@ function count(holder) {
|
|
|
12
13
|
* attempts, `steps` counts only each holder's own top-level steps, tags are de-duplicated, and for a repeated
|
|
13
14
|
* label the later holder wins. `artifactTags` are the attachments' own normalized tags and are never inherited. `roles` are the attachments' roles (or, without one, what their media type or file
|
|
14
15
|
* name implies), sorted and distinct. `videos` lists every attachment whose effective media type is video, with the
|
|
15
|
-
* check, retry attempt or step that holds it (see {@link isVideoAttachment}).
|
|
16
|
+
* check, retry attempt or step that holds it (see {@link isVideoAttachment}), each with the scope that applies to it.
|
|
16
17
|
*/
|
|
17
18
|
export function summarizeEvidence(...holders) {
|
|
18
19
|
return summarize(holders, 'check');
|
|
19
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Like {@link summarizeEvidence} for the checks of a part: a video that declares no scope, and sits in a holder that
|
|
23
|
+
* declares none, takes the scope `inherited` from its part or layer (the nearest declaration of the two, see
|
|
24
|
+
* `effectiveScope`). Without `inherited` it is the same as `summarizeEvidence`.
|
|
25
|
+
*/
|
|
26
|
+
export function summarizeEvidenceIn(inherited, ...holders) {
|
|
27
|
+
return summarize(holders, 'check', inherited);
|
|
28
|
+
}
|
|
20
29
|
/**
|
|
21
30
|
* Like {@link summarizeEvidence} for a layer and its parts: the same summary, but a video attached to the layer or a
|
|
22
31
|
* part is owned by the `layer`, not by a check.
|
|
23
32
|
*/
|
|
24
33
|
export function summarizeLayerEvidence(...holders) {
|
|
25
|
-
|
|
34
|
+
// The first holder is the layer: its scope reaches the parts after it, which may declare their own.
|
|
35
|
+
return summarize(holders, 'layer', undefined, true);
|
|
26
36
|
}
|
|
27
|
-
function summarize(holders, owner) {
|
|
37
|
+
function summarize(holders, owner, inherited, firstIsParent = false) {
|
|
28
38
|
if (!holders.some(holder => count(holder) > 0))
|
|
29
39
|
return undefined;
|
|
30
40
|
const roles = new Set();
|
|
@@ -33,26 +43,28 @@ function summarize(holders, owner) {
|
|
|
33
43
|
const labels = {};
|
|
34
44
|
const videos = [];
|
|
35
45
|
let attachments = 0, steps = 0, annotations = 0, attempts = 0, links = 0;
|
|
36
|
-
const attachmentsOf = (list, by, where = {}) => {
|
|
46
|
+
const attachmentsOf = (list, by, scopes, where = {}) => {
|
|
37
47
|
for (const item of list ?? []) {
|
|
38
48
|
attachments += 1;
|
|
39
49
|
for (const tag of normalizeTags(item.tags))
|
|
40
50
|
artifactTags.add(tag);
|
|
41
51
|
roles.add(item.role ?? roleOfType(item.contentType ?? contentTypeOf(item.name)));
|
|
42
|
-
const video = videoOf(item, by, where);
|
|
52
|
+
const video = videoOf(item, by, where, effectiveScope(item.scope, ...scopes));
|
|
43
53
|
if (video)
|
|
44
54
|
videos.push(video);
|
|
45
55
|
}
|
|
46
56
|
};
|
|
47
|
-
const stepsOf = (list, attempt) => { for (const step of list ?? [])
|
|
48
|
-
attachmentsOf(step.attachments, 'step', { ...(attempt === undefined ? {} : { attempt }), step: step.name }); };
|
|
49
|
-
for (const holder of holders) {
|
|
50
|
-
|
|
57
|
+
const stepsOf = (list, scopes, attempt) => { for (const step of list ?? [])
|
|
58
|
+
attachmentsOf(step.attachments, 'step', scopes, { ...(attempt === undefined ? {} : { attempt }), step: step.name }); };
|
|
59
|
+
for (const [index, holder] of holders.entries()) {
|
|
60
|
+
const around = [holder.scope, inherited, ...(firstIsParent && index > 0 ? [holders[0]?.scope] : [])];
|
|
61
|
+
attachmentsOf(holder.attachments, owner, around);
|
|
51
62
|
// Attachments on steps and attempts are part of the check too.
|
|
52
|
-
stepsOf(holder.steps);
|
|
63
|
+
stepsOf(holder.steps, around);
|
|
53
64
|
for (const attempt of holder.attempts ?? []) {
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
const tried = [attempt.scope, ...around];
|
|
66
|
+
attachmentsOf(attempt.attachments, 'attempt', tried, { attempt: attempt.index });
|
|
67
|
+
stepsOf(attempt.steps, tried, attempt.index);
|
|
56
68
|
}
|
|
57
69
|
steps += holder.steps?.length ?? 0;
|
|
58
70
|
annotations += holder.annotations?.length ?? 0;
|
|
@@ -73,11 +85,12 @@ export function isVideoAttachment(attachment) {
|
|
|
73
85
|
const type = (attachment.contentType ?? contentTypeOf(attachment.name)).toLowerCase();
|
|
74
86
|
return type.startsWith('video/') || (attachment.role === 'video' && type.startsWith('application/octet-stream'));
|
|
75
87
|
}
|
|
76
|
-
function videoOf(item, owner, where) {
|
|
88
|
+
function videoOf(item, owner, where, scope) {
|
|
77
89
|
if (!isVideoAttachment(item))
|
|
78
90
|
return undefined;
|
|
79
91
|
const recording = item.sha256 ? `sha256:${item.sha256}` : item.bundleKey ?? item.uri ?? (item.text === undefined ? undefined : `inline:${item.name}`);
|
|
80
|
-
|
|
92
|
+
const tags = normalizeTags(item.tags);
|
|
93
|
+
return { name: item.name, owner, ...where, ...(tags.length ? { tags } : {}), ...(recording === undefined ? {} : { recording }), available: recording !== undefined, ...(scope ? { scope } : {}) };
|
|
81
94
|
}
|
|
82
95
|
/**
|
|
83
96
|
* Counts references, distinct playable recordings and unplayable references. Undefined when there is no reference, so
|
|
@@ -117,15 +130,17 @@ export function checkDetailOf(layer, checkId, kind) {
|
|
|
117
130
|
if (kind !== undefined && part.kind !== kind)
|
|
118
131
|
continue;
|
|
119
132
|
const check = part.checks.find(item => item.id === checkId);
|
|
120
|
-
if (check)
|
|
121
|
-
|
|
133
|
+
if (check) {
|
|
134
|
+
const inheritedScope = effectiveScope(part.scope, layer.scope);
|
|
135
|
+
return { layer: layer.id, kind: part.kind, check, run: runEvidence(layer), ...(inheritedScope ? { inheritedScope } : {}) };
|
|
136
|
+
}
|
|
122
137
|
}
|
|
123
138
|
return null;
|
|
124
139
|
}
|
|
125
|
-
/** What a layer carries as a whole (its tags, labels, links, attachments, annotations and extensions; not its parts' checks). Absent fields stay absent. */
|
|
140
|
+
/** What a layer carries as a whole (its scope, tags, labels, links, attachments, annotations and extensions; not its parts' checks). Absent fields stay absent. */
|
|
126
141
|
export function runEvidence(layer) {
|
|
127
|
-
const { tags, labels, links, attachments, annotations, extensions } = layer;
|
|
128
|
-
return { ...(tags ? { tags } : {}), ...(labels ? { labels } : {}), ...(links ? { links } : {}), ...(attachments ? { attachments } : {}), ...(annotations ? { annotations } : {}), ...(extensions ? { extensions } : {}) };
|
|
142
|
+
const { tags, labels, links, attachments, annotations, extensions, scope } = layer;
|
|
143
|
+
return { ...(scope ? { scope } : {}), ...(tags ? { tags } : {}), ...(labels ? { labels } : {}), ...(links ? { links } : {}), ...(attachments ? { attachments } : {}), ...(annotations ? { annotations } : {}), ...(extensions ? { extensions } : {}) };
|
|
129
144
|
}
|
|
130
145
|
/** Every distinct bundle key (`bundleKey`) that the attachments of a holder, its steps and its attempts point to, so a layer's files can be copied. Attachments held by `uri` or inline `text` have none. */
|
|
131
146
|
export function bundleKeysOf(holder) {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Evidence scope
|
|
3
|
+
*
|
|
4
|
+
* Reads what a producer declared a piece of evidence shows (`EvidenceScope`): which declaration applies to an
|
|
5
|
+
* attachment, the short badges a reviewer sees before playing a recording, the facts split into declared and
|
|
6
|
+
* independently verified, and the filter tokens (`device:physical`, `audio:recorded`) a query narrows by. Pure
|
|
7
|
+
* functions, safe in a browser.
|
|
8
|
+
*
|
|
9
|
+
* The one rule: what is not declared is unknown. A missing scope, or a missing field, never reads as live, physical,
|
|
10
|
+
* audible or end to end, and a filter for a value never matches evidence that did not say it. A scope describes
|
|
11
|
+
* evidence; it never decides a status, a slot or a winner.
|
|
12
|
+
*
|
|
13
|
+
* @see docs/rich-evidence.md#evidence-scope
|
|
14
|
+
*/
|
|
15
|
+
import type { EvidenceScope } from '../../schema/src/layer.js';
|
|
16
|
+
/** The fields of a scope that say something about the evidence and that `verified` may name, in the order they are shown. */
|
|
17
|
+
export declare const SCOPE_DIMENSIONS: readonly ["surface", "deviceKind", "syntheticData", "dependencies", "audioCaptured", "pacing"];
|
|
18
|
+
/** One of {@link SCOPE_DIMENSIONS}. */
|
|
19
|
+
export type ScopeDimension = (typeof SCOPE_DIMENSIONS)[number];
|
|
20
|
+
/** The filter token of a recording that declares no scope at all. */
|
|
21
|
+
export declare const UNKNOWN_SCOPE = "unknown";
|
|
22
|
+
/**
|
|
23
|
+
* The declaration that applies to something: the nearest one wins as a whole, never field by field, so a recording
|
|
24
|
+
* never shows a limitation or a device that was declared for another source. Pass the candidates nearest first
|
|
25
|
+
* (attachment, attempt, check, part, layer); absent ones are skipped. Undefined when none declares a scope.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* effectiveScope(attachment.scope, attempt.scope, check.scope, layer.scope);
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare function effectiveScope(...candidates: readonly (EvidenceScope | undefined)[]): EvidenceScope | undefined;
|
|
33
|
+
/** A stable string that is equal for equal declarations (key order never matters), to tell one scope from another. */
|
|
34
|
+
export declare function scopeKey(scope: EvidenceScope | undefined): string;
|
|
35
|
+
/**
|
|
36
|
+
* The filter tokens one declaration states, normalized (letter case and spaces never matter): `surface:<value>`,
|
|
37
|
+
* `device:<value>`, `data:synthetic` or `data:real`, `dependencies:<value>`, `audio:recorded` or `audio:silent`,
|
|
38
|
+
* `pacing:<value>`. A field that is not declared adds no token. No scope at all is the single token
|
|
39
|
+
* {@link UNKNOWN_SCOPE}, so a filter can ask for the unknown on purpose; a scope that declares nothing readable is
|
|
40
|
+
* also unknown.
|
|
41
|
+
*/
|
|
42
|
+
export declare function scopeTokens(scope: EvidenceScope | undefined): string[];
|
|
43
|
+
/**
|
|
44
|
+
* Whether one recording's tokens satisfy a filter: every wanted token must be among them. Tokens compare normalized.
|
|
45
|
+
* An empty filter matches everything. The token {@link UNKNOWN_SCOPE} matches only a recording with no declared
|
|
46
|
+
* fields, so asking for `device:physical` never includes a recording that did not say what it ran on.
|
|
47
|
+
*/
|
|
48
|
+
export declare function matchesScope(tokens: readonly string[], wanted: readonly string[]): boolean;
|
|
49
|
+
/** How a fact is known: `verified` (independently checked), `declared` (the producer's word) or `undeclared` (unknown). */
|
|
50
|
+
export type ScopeBasis = 'verified' | 'declared' | 'undeclared';
|
|
51
|
+
/** One line of what a scope says: the dimension, a sentence for a person, and how well it is known. */
|
|
52
|
+
export type ScopeFact = {
|
|
53
|
+
readonly dimension: ScopeDimension;
|
|
54
|
+
/** A name for the dimension: `Device`, `Audio`. */
|
|
55
|
+
readonly label: string;
|
|
56
|
+
/** The value as words: `Physical device`, `Silent`, `Not declared`. */
|
|
57
|
+
readonly value: string;
|
|
58
|
+
readonly basis: ScopeBasis;
|
|
59
|
+
};
|
|
60
|
+
/** A short qualification to show beside a recording: `Device: Simulator`, `Scripted dependencies`, `Silent`. */
|
|
61
|
+
export type ScopeBadge = {
|
|
62
|
+
readonly dimension: ScopeDimension | 'unknown';
|
|
63
|
+
readonly text: string;
|
|
64
|
+
readonly basis: ScopeBasis;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Every dimension of a scope with its value and how it is known, in a fixed order, including the ones nobody declared
|
|
68
|
+
* (`Not declared`, basis `undeclared`). A dimension is `verified` only when the scope's `verified` names it and it has
|
|
69
|
+
* a value; everything else a producer wrote is `declared`. With no scope every dimension is undeclared.
|
|
70
|
+
*/
|
|
71
|
+
export declare function scopeFacts(scope: EvidenceScope | undefined): ScopeFact[];
|
|
72
|
+
/**
|
|
73
|
+
* The concise badges for a scope: one per declared dimension, in a fixed order (`Surface: Browser`, `Device: Simulator`, `Synthetic
|
|
74
|
+
* data`, `Scripted dependencies`, `Silent`, `Paced`). With no declared dimension the one badge `Scope unknown`, so
|
|
75
|
+
* a reviewer is told nothing was claimed rather than shown nothing.
|
|
76
|
+
*/
|
|
77
|
+
export declare function scopeBadges(scope: EvidenceScope | undefined): ScopeBadge[];
|
|
78
|
+
/**
|
|
79
|
+
* What a reviewer must not read into evidence: a sentence per thing that is not shown. It names the assertions the
|
|
80
|
+
* result stands on when there are some (so activity visible beyond them is not verified), and every kind of reality
|
|
81
|
+
* that was not declared. Empty only for a scope that declares and verifies everything and lists no limitation.
|
|
82
|
+
*/
|
|
83
|
+
export declare function scopeCaveats(scope: EvidenceScope | undefined): string[];
|