@xano/cli 1.3.4 → 1.3.5-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -3
- package/dist/base-command.d.ts +12 -1
- package/dist/base-command.js +36 -22
- package/dist/commands/branch/set_live/index.d.ts +14 -0
- package/dist/commands/branch/set_live/index.js +38 -0
- package/dist/commands/ephemeral/push/index.js +6 -1
- package/dist/commands/function/create/index.d.ts +15 -0
- package/dist/commands/function/create/index.js +39 -0
- package/dist/commands/function/edit/index.d.ts +15 -0
- package/dist/commands/function/edit/index.js +39 -0
- package/dist/commands/function/run/index.js +5 -11
- package/dist/commands/policy/catalogue/index.d.ts +16 -0
- package/dist/commands/policy/catalogue/index.js +60 -0
- package/dist/commands/{tenant/snapshot/create → policy/coverage}/index.d.ts +4 -4
- package/dist/commands/policy/coverage/index.js +33 -0
- package/dist/commands/policy/create/index.d.ts +18 -0
- package/dist/commands/policy/create/index.js +44 -0
- package/dist/commands/{tenant/snapshot → policy}/delete/index.d.ts +5 -5
- package/dist/commands/policy/delete/index.js +61 -0
- package/dist/commands/policy/evaluate/index.d.ts +32 -0
- package/dist/commands/policy/evaluate/index.js +71 -0
- package/dist/commands/{tenant/snapshot → policy}/list/index.d.ts +3 -5
- package/dist/commands/policy/list/index.js +21 -0
- package/dist/commands/policy/parse/index.d.ts +19 -0
- package/dist/commands/policy/parse/index.js +18 -0
- package/dist/commands/policy/publish/index.d.ts +20 -0
- package/dist/commands/policy/publish/index.js +38 -0
- package/dist/commands/policy/runs/index.d.ts +54 -0
- package/dist/commands/policy/runs/index.js +273 -0
- package/dist/commands/policy/status/index.d.ts +21 -0
- package/dist/commands/policy/status/index.js +54 -0
- package/dist/commands/release/create/index.js +4 -0
- package/dist/commands/release/deploy/index.d.ts +19 -0
- package/dist/commands/release/deploy/index.js +68 -6
- package/dist/commands/release/import/index.js +5 -1
- package/dist/commands/release/list/index.d.ts +7 -0
- package/dist/commands/release/list/index.js +46 -9
- package/dist/commands/release/push/index.js +19 -1
- package/dist/commands/sandbox/push/index.js +6 -1
- package/dist/commands/skills/pull/index.d.ts +30 -0
- package/dist/commands/skills/pull/index.js +118 -0
- package/dist/commands/tenant/create/index.d.ts +3 -0
- package/dist/commands/tenant/create/index.js +21 -0
- package/dist/commands/tenant/deploy_release/index.d.ts +40 -0
- package/dist/commands/tenant/deploy_release/index.js +182 -4
- package/dist/commands/tenant/edit/index.d.ts +2 -0
- package/dist/commands/tenant/edit/index.js +39 -4
- package/dist/commands/tenant_deploy_request/bypass/index.d.ts +31 -0
- package/dist/commands/tenant_deploy_request/bypass/index.js +109 -0
- package/dist/commands/tenant_deploy_request/create/index.d.ts +21 -0
- package/dist/commands/tenant_deploy_request/create/index.js +108 -0
- package/dist/commands/tenant_deploy_request/edit/index.d.ts +22 -0
- package/dist/commands/tenant_deploy_request/edit/index.js +103 -0
- package/dist/commands/tenant_deploy_request/get/index.d.ts +25 -0
- package/dist/commands/tenant_deploy_request/get/index.js +113 -0
- package/dist/commands/tenant_deploy_request/list/index.d.ts +20 -0
- package/dist/commands/tenant_deploy_request/list/index.js +122 -0
- package/dist/commands/{tenant/snapshot/swap → tenant_deploy_request/revision}/index.d.ts +8 -6
- package/dist/commands/tenant_deploy_request/revision/index.js +83 -0
- package/dist/commands/tenant_deploy_request/set_status/index.d.ts +38 -0
- package/dist/commands/tenant_deploy_request/set_status/index.js +159 -0
- package/dist/commands/workspace/pull/index.d.ts +10 -0
- package/dist/commands/workspace/pull/index.js +70 -7
- package/dist/commands/workspace/push/index.d.ts +19 -0
- package/dist/commands/workspace/push/index.js +135 -6
- package/dist/policy-command.d.ts +91 -0
- package/dist/policy-command.js +95 -0
- package/dist/utils/document-parser.d.ts +10 -0
- package/dist/utils/document-parser.js +21 -0
- package/dist/utils/multidoc-push.d.ts +57 -2
- package/dist/utils/multidoc-push.js +102 -33
- package/dist/utils/policy/catalogue.d.ts +85 -0
- package/dist/utils/policy/catalogue.js +196 -0
- package/dist/utils/policy/coverage.d.ts +19 -0
- package/dist/utils/policy/coverage.js +93 -0
- package/dist/utils/policy/errors.d.ts +20 -0
- package/dist/utils/policy/errors.js +93 -0
- package/dist/utils/policy/feedback.d.ts +90 -0
- package/dist/utils/policy/feedback.js +210 -0
- package/dist/utils/policy/findings.d.ts +29 -0
- package/dist/utils/policy/findings.js +82 -0
- package/dist/utils/policy/gate.d.ts +138 -0
- package/dist/utils/policy/gate.js +193 -0
- package/dist/utils/policy/permission.d.ts +7 -0
- package/dist/utils/policy/permission.js +55 -0
- package/dist/utils/policy/push-gate.d.ts +5 -0
- package/dist/utils/policy/push-gate.js +43 -0
- package/dist/utils/policy/release.d.ts +85 -0
- package/dist/utils/policy/release.js +117 -0
- package/dist/utils/policy/request.d.ts +55 -0
- package/dist/utils/policy/request.js +119 -0
- package/dist/utils/policy/runs.d.ts +30 -0
- package/dist/utils/policy/runs.js +101 -0
- package/dist/utils/policy/status.d.ts +55 -0
- package/dist/utils/policy/status.js +127 -0
- package/dist/utils/policy/template.d.ts +66 -0
- package/dist/utils/policy/template.js +152 -0
- package/dist/utils/policy/types.d.ts +295 -0
- package/dist/utils/policy/types.js +1 -0
- package/oclif.manifest.json +6829 -4006
- package/package.json +9 -1
- package/dist/commands/tenant/snapshot/create/index.js +0 -78
- package/dist/commands/tenant/snapshot/delete/index.js +0 -102
- package/dist/commands/tenant/snapshot/list/index.js +0 -100
- package/dist/commands/tenant/snapshot/swap/index.js +0 -103
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { coverageFindingLines } from './findings.js';
|
|
2
|
+
/** An object as findings name it, `type:id` (`query:42`), or `null` when the text is not one. */
|
|
3
|
+
export function parseObjectRef(text) {
|
|
4
|
+
const match = /^([a-z_]+):(\d+)$/i.exec(text.trim());
|
|
5
|
+
if (!match)
|
|
6
|
+
return null;
|
|
7
|
+
const id = Number(match[2]);
|
|
8
|
+
return Number.isSafeInteger(id) && id > 0 ? { id, type: match[1].toLowerCase() } : null;
|
|
9
|
+
}
|
|
10
|
+
const count = (value) => value ?? 0;
|
|
11
|
+
/** The object coverage chip, worded as Studio's object panel words it. */
|
|
12
|
+
export function coverageChip(summary) {
|
|
13
|
+
switch (summary?.state) {
|
|
14
|
+
case 'check_failed': {
|
|
15
|
+
return 'Policies · Last check failed';
|
|
16
|
+
}
|
|
17
|
+
case 'findings': {
|
|
18
|
+
return `Policies ${count(summary.blocking)} blocking · ${count(summary.advisory)} advisory`;
|
|
19
|
+
}
|
|
20
|
+
case 'none_apply': {
|
|
21
|
+
return 'Policies · None apply';
|
|
22
|
+
}
|
|
23
|
+
case 'not_checked': {
|
|
24
|
+
return `Policies · ${count(summary.applies)} apply · not checked yet`;
|
|
25
|
+
}
|
|
26
|
+
case 'pass': {
|
|
27
|
+
return `Policies ✓ ${count(summary.passing)} pass`;
|
|
28
|
+
}
|
|
29
|
+
default: {
|
|
30
|
+
return `Policies · ${summary?.state ?? 'unknown'}`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const STATUS = {
|
|
35
|
+
could_not_check: "couldn't check",
|
|
36
|
+
failing: 'failing',
|
|
37
|
+
not_checked: 'not checked yet',
|
|
38
|
+
passing: 'passing',
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* The policies that apply with no finding here, counted from the summary as Studio's panel words its
|
|
42
|
+
* line under the findings: "6 passing · 1 couldn't check · 1 not checked yet". Empty when it counts none.
|
|
43
|
+
*/
|
|
44
|
+
export function coverageCounts(summary) {
|
|
45
|
+
const counted = [
|
|
46
|
+
[summary?.passing, 'passing'],
|
|
47
|
+
[summary?.could_not_check, "couldn't check"],
|
|
48
|
+
[summary?.not_checked, 'not checked yet'],
|
|
49
|
+
];
|
|
50
|
+
return counted
|
|
51
|
+
.filter(([value]) => count(value) > 0)
|
|
52
|
+
.map(([value, label]) => `${value} ${label}`)
|
|
53
|
+
.join(' · ');
|
|
54
|
+
}
|
|
55
|
+
function policyLines(policy) {
|
|
56
|
+
const enforcement = policy.blocking ? 'Blocking' : 'Advisory';
|
|
57
|
+
const status = STATUS[policy.status ?? ''] ?? policy.status ?? '';
|
|
58
|
+
return [
|
|
59
|
+
` ${[policy.key, enforcement, status, policy.title].filter(Boolean).join(' ')}`,
|
|
60
|
+
...(policy.reasons ?? []).map(reason => ` Applies: ${reason}`),
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
function objectName(object) {
|
|
64
|
+
if (!object)
|
|
65
|
+
return 'object';
|
|
66
|
+
const name = object.name ? ` ${object.name}` : '';
|
|
67
|
+
return `${object.type ?? 'object'}${name}${object.id ? ` (id ${object.id})` : ''}`;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* An object's coverage on stdout: the chip, the run it reads, the findings Blocking then Advisory,
|
|
71
|
+
* each with its fix, the policies that apply (counted, then each with its status and why), and the
|
|
72
|
+
* platform's note.
|
|
73
|
+
*/
|
|
74
|
+
export function coverageLines(answer) {
|
|
75
|
+
const lines = [`${objectName(answer.object)}: ${coverageChip(answer.summary)}`];
|
|
76
|
+
lines.push(answer.run
|
|
77
|
+
? `Latest run: ${answer.run.started_at ?? 'unknown time'} (${answer.run.status ?? 'unknown'}); changes made since are not included.`
|
|
78
|
+
: 'No run yet: the branch has never been checked.');
|
|
79
|
+
const findings = answer.findings ?? [];
|
|
80
|
+
const blocking = findings.filter(finding => finding.blocking === true);
|
|
81
|
+
const advisory = findings.filter(finding => finding.blocking !== true);
|
|
82
|
+
if (blocking.length > 0)
|
|
83
|
+
lines.push(`Blocking findings (${blocking.length}):`, ...blocking.flatMap(finding => coverageFindingLines(finding)));
|
|
84
|
+
if (advisory.length > 0)
|
|
85
|
+
lines.push(`Advisory findings (${advisory.length}):`, ...advisory.flatMap(finding => coverageFindingLines(finding)));
|
|
86
|
+
const policies = answer.policies ?? [];
|
|
87
|
+
const counts = coverageCounts(answer.summary);
|
|
88
|
+
if (policies.length > 0)
|
|
89
|
+
lines.push(`Policies that apply (${policies.length}):${counts ? ` ${counts}` : ''}`, ...policies.flatMap(policy => policyLines(policy)));
|
|
90
|
+
if (answer.note?.trim())
|
|
91
|
+
lines.push(answer.note.trim());
|
|
92
|
+
return lines;
|
|
93
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Guidance appended to a refusal whose `payload.code` the CLI has advice for, or `''`. */
|
|
2
|
+
export declare function policyCodeGuidance(payload: unknown): string;
|
|
3
|
+
/**
|
|
4
|
+
* The CLI's own sentence for a refusal that names policies, keyed on the platform's `payload.code`,
|
|
5
|
+
* or `null` to print the platform's coded message (also when the payload lacks the names):
|
|
6
|
+
*
|
|
7
|
+
* policy_duplicate a new policy's rules exactly match a policy the branch has
|
|
8
|
+
* policy_key_taken another live policy of the branch holds the key
|
|
9
|
+
*/
|
|
10
|
+
export declare function policyRefusal(payload: unknown): null | string;
|
|
11
|
+
/** A failed policy-route response: one message to print, and the refusal's payload. */
|
|
12
|
+
export interface PolicyError {
|
|
13
|
+
message: string;
|
|
14
|
+
payload?: unknown;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A failed policy-route response as one message: the platform's code and message, and where a parse
|
|
18
|
+
* error is. Traces are dropped, and an empty 404 names the branch the request selected.
|
|
19
|
+
*/
|
|
20
|
+
export declare function describePolicyError(text: string, status: number, requestUrl: string): PolicyError;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/** Keep backend implementation details out of ordinary command errors. */
|
|
2
|
+
function withoutStack(text) {
|
|
3
|
+
return text.split(/\n\s*(?:Stack trace:|#\d+\s|at\s|file:\s*\/)/i, 1)[0];
|
|
4
|
+
}
|
|
5
|
+
/** Where a parse error is: the payload's 1-based `line` and `col`, printed as served, and the offending text. */
|
|
6
|
+
function positionLine(payload) {
|
|
7
|
+
const text = [payload.error_line, payload.error_snippet, payload.snippet]
|
|
8
|
+
.find(value => typeof value === 'string' && value.trim() !== '');
|
|
9
|
+
const where = typeof payload.line === 'number' && Number.isFinite(payload.line)
|
|
10
|
+
? `line ${payload.line}${typeof payload.col === 'number' && Number.isFinite(payload.col) ? `, col ${payload.col}` : ''}`
|
|
11
|
+
: '';
|
|
12
|
+
if (!where && text === undefined)
|
|
13
|
+
return '';
|
|
14
|
+
return `\n ${where ? `at ${where}` : 'at'}${text === undefined ? '' : `: ${text}`}`;
|
|
15
|
+
}
|
|
16
|
+
/** An empty 404 from a branch-scoped request means the branch it named does not exist. */
|
|
17
|
+
function missingBranch(requestUrl) {
|
|
18
|
+
const url = new URL(requestUrl);
|
|
19
|
+
const workspace = url.pathname.match(/\/workspace\/([^/]+)/)?.[1];
|
|
20
|
+
const branch = url.searchParams.get('branch');
|
|
21
|
+
return workspace && branch ? `Branch "${branch}" was not found in workspace ${decodeURIComponent(workspace)}.` : '';
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* What to do about a refusal of the request itself, keyed on the platform's `payload.code`:
|
|
25
|
+
*
|
|
26
|
+
* policy_unknown_check a rule names a check id this instance does not have
|
|
27
|
+
* policy_stale the policy changed after the command read it
|
|
28
|
+
*
|
|
29
|
+
* Permission refusals are explained by `policyPermissionGuidance`.
|
|
30
|
+
*/
|
|
31
|
+
const CODE_GUIDANCE = {
|
|
32
|
+
policy_stale: '\nThe policy changed after this command read it, so nothing was changed. Run the command again to act on its current version.',
|
|
33
|
+
policy_unknown_check: '\nRun `xano policy catalogue` to list every check id this instance has.',
|
|
34
|
+
};
|
|
35
|
+
/** Guidance appended to a refusal whose `payload.code` the CLI has advice for, or `''`. */
|
|
36
|
+
export function policyCodeGuidance(payload) {
|
|
37
|
+
const code = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload.code : undefined;
|
|
38
|
+
return typeof code === 'string' && Object.hasOwn(CODE_GUIDANCE, code) ? CODE_GUIDANCE[code] : '';
|
|
39
|
+
}
|
|
40
|
+
/** A payload string worth naming, trimmed, or `''`. */
|
|
41
|
+
function named(value) {
|
|
42
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The CLI's own sentence for a refusal that names policies, keyed on the platform's `payload.code`,
|
|
46
|
+
* or `null` to print the platform's coded message (also when the payload lacks the names):
|
|
47
|
+
*
|
|
48
|
+
* policy_duplicate a new policy's rules exactly match a policy the branch has
|
|
49
|
+
* policy_key_taken another live policy of the branch holds the key
|
|
50
|
+
*/
|
|
51
|
+
export function policyRefusal(payload) {
|
|
52
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
53
|
+
return null;
|
|
54
|
+
const refusal = payload;
|
|
55
|
+
const existing = named(refusal.existing_key);
|
|
56
|
+
if (!existing)
|
|
57
|
+
return null;
|
|
58
|
+
if (refusal.code === 'policy_duplicate') {
|
|
59
|
+
const copies = Array.isArray(refusal.policies) ? refusal.policies : [];
|
|
60
|
+
const tried = named(copies[0]?.key);
|
|
61
|
+
return `${tried ? `Policy ${tried}` : 'The policy'} was not created: its rules exactly match ${existing}, which this branch already has. `
|
|
62
|
+
+ 'Change a parameter or scope to add a different policy (for policy create, --param N.path=JSON); its key does not count.';
|
|
63
|
+
}
|
|
64
|
+
if (refusal.code === 'policy_key_taken') {
|
|
65
|
+
const tried = named(refusal.key) || existing;
|
|
66
|
+
const id = typeof refusal.existing_id === 'number' ? ` (policy id ${refusal.existing_id})` : '';
|
|
67
|
+
return `Policy ${tried} was not saved: this branch already has ${existing}${id}. `
|
|
68
|
+
+ `Choose a key no policy has (for policy create, --key), or update ${existing} with policy publish.`;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* A failed policy-route response as one message: the platform's code and message, and where a parse
|
|
74
|
+
* error is. Traces are dropped, and an empty 404 names the branch the request selected.
|
|
75
|
+
*/
|
|
76
|
+
export function describePolicyError(text, status, requestUrl) {
|
|
77
|
+
let data;
|
|
78
|
+
try {
|
|
79
|
+
data = JSON.parse(text.trim() || '{}');
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return { message: withoutStack(text) };
|
|
83
|
+
}
|
|
84
|
+
if (!data || typeof data !== 'object' || Array.isArray(data))
|
|
85
|
+
return { message: withoutStack(text) };
|
|
86
|
+
const { code, message: raw, payload } = data;
|
|
87
|
+
let message = typeof raw === 'string' ? withoutStack(raw) : '';
|
|
88
|
+
if (status === 404 && !message.trim())
|
|
89
|
+
message = missingBranch(requestUrl);
|
|
90
|
+
const headline = [typeof code === 'string' ? code : '', message].filter(Boolean).join(': ');
|
|
91
|
+
const position = payload && typeof payload === 'object' ? positionLine(payload) : '';
|
|
92
|
+
return { message: `${headline}${position}` || 'The server returned no message.', payload };
|
|
93
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { PolicyEvaluation, PolicyFinding, PolicyRuleResult, PolicySnapshotPolicy, PolicyVerdict, PushPolicyCheck } from './types.js';
|
|
2
|
+
/** 2 whenever the platform says a finding blocks, whatever the status; every other outcome is 0. */
|
|
3
|
+
export declare function policyExitCode(check?: PolicyVerdict): number;
|
|
4
|
+
/**
|
|
5
|
+
* The one warning line for feedback without a headline (`disabled`, `unavailable`, `error`, an
|
|
6
|
+
* unknown status such as an older platform's `forbidden`, or a pass or fail that does not say whether
|
|
7
|
+
* it blocks): the server's own status and message, or that none came back. `null` for headlined
|
|
8
|
+
* feedback.
|
|
9
|
+
*/
|
|
10
|
+
export declare function policyCheckWarning(check?: PolicyVerdict): null | string;
|
|
11
|
+
/**
|
|
12
|
+
* What happened to the policy documents a push carried. The preview says, per policy, whether it was
|
|
13
|
+
* created, updated or left unchanged. Without a preview only the number sent is known: the import
|
|
14
|
+
* response reports an unchanged policy exactly as it reports a saved one.
|
|
15
|
+
*/
|
|
16
|
+
export declare function policyDocumentSummary(preview: null | {
|
|
17
|
+
operations: Array<{
|
|
18
|
+
action: string;
|
|
19
|
+
name: string;
|
|
20
|
+
type: string;
|
|
21
|
+
}>;
|
|
22
|
+
}, sentPolicies: number): string[];
|
|
23
|
+
/** What a verdict is about: its findings, which of them block, and the rule results. */
|
|
24
|
+
export interface PolicyEvidence {
|
|
25
|
+
/** The ids of the findings that block. */
|
|
26
|
+
blocking: Set<string>;
|
|
27
|
+
/**
|
|
28
|
+
* When the answer lists only the first findings: how many there are in all, and how many block.
|
|
29
|
+
* The stored run (`runId`, 0 when nothing was stored) has them all.
|
|
30
|
+
*/
|
|
31
|
+
cut?: {
|
|
32
|
+
blocking: number;
|
|
33
|
+
findings: number;
|
|
34
|
+
runId: number;
|
|
35
|
+
};
|
|
36
|
+
/** In the platform's order: blocking first, then by severity. */
|
|
37
|
+
findings: PolicyFinding[];
|
|
38
|
+
results: PolicyRuleResult[];
|
|
39
|
+
/** The run's own `policies[]`, which names unnamed rules; a push answers none. */
|
|
40
|
+
snapshot: PolicySnapshotPolicy[];
|
|
41
|
+
/**
|
|
42
|
+
* The answer to a caller without `workspace:policy` read (`access: "coverage"`): its own findings
|
|
43
|
+
* on the pushed objects, trimmed, with no run to read (`run_id` 0) and no rule results, the
|
|
44
|
+
* platform's `note` on what it leaves out, how many rules could not run on the branch, and how
|
|
45
|
+
* many findings there are on the objects it does not list (`elsewhere`, never named): those the
|
|
46
|
+
* push introduced when `basis` is `introduced`, otherwise every one (no baseline, or an older platform).
|
|
47
|
+
*/
|
|
48
|
+
trimmed?: {
|
|
49
|
+
elsewhere: {
|
|
50
|
+
blocking: number;
|
|
51
|
+
introduced: boolean;
|
|
52
|
+
total: number;
|
|
53
|
+
};
|
|
54
|
+
errors: number;
|
|
55
|
+
note: string;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A push answers no run, so its `policy_check` carries the findings and results itself: the first
|
|
60
|
+
* 100 of each list, with `counts` and `total` saying what they are out of.
|
|
61
|
+
*/
|
|
62
|
+
export declare function pushEvidence(check?: PushPolicyCheck): PolicyEvidence;
|
|
63
|
+
/**
|
|
64
|
+
* A push's `policy_check` for a caller without `workspace:policy` read: only that caller's findings
|
|
65
|
+
* on the objects the push carried, and no run it may read.
|
|
66
|
+
*/
|
|
67
|
+
export declare function isCoverageCheck(check?: PushPolicyCheck): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* An evaluation answers the run, and its verdict names the blocking findings by id. Its summary
|
|
70
|
+
* answer (`--summary`) lists the first findings, and its counts say what they are out of.
|
|
71
|
+
*/
|
|
72
|
+
export declare function evaluationEvidence(evaluation: PolicyEvaluation): PolicyEvidence;
|
|
73
|
+
/**
|
|
74
|
+
* The feedback on stdout: a headline with the server's message, then any findings, errors and
|
|
75
|
+
* warnings. Feedback without a headline is reported by `policyCheckWarning` instead.
|
|
76
|
+
*/
|
|
77
|
+
export declare function policySummary(check: PolicyVerdict | undefined, evidence: PolicyEvidence): string[];
|
|
78
|
+
/**
|
|
79
|
+
* The platform's one notice that a push or its dry run left policy files out
|
|
80
|
+
* (`policies_skipped.message`): a push to a remote tenant or to a tenant without a policy table, a
|
|
81
|
+
* `release push` by a caller without `workspace:policy` create and update (its release carries the
|
|
82
|
+
* live branch's policies instead), and any push while the Policies feature is off. `null` when it
|
|
83
|
+
* left none out.
|
|
84
|
+
*/
|
|
85
|
+
export declare function policiesSkippedNotice(answer: unknown): null | string;
|
|
86
|
+
/**
|
|
87
|
+
* The line for an evaluation that ran but was not recorded (the credential may read, not write), or
|
|
88
|
+
* `null`. A branch with nothing to evaluate stores nothing either, and its headline says so.
|
|
89
|
+
*/
|
|
90
|
+
export declare function notStoredLine(evaluation: PolicyEvaluation): null | string;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { coverageFindingLines, findingLine, hasNoObjects, policyResultSummary, snapshotRules } from './findings.js';
|
|
2
|
+
/**
|
|
3
|
+
* Feedback printed as a headline: a pass or fail that says whether its findings block, or a branch
|
|
4
|
+
* with no active policy to evaluate. Everything else is reported by `policyCheckWarning`.
|
|
5
|
+
*/
|
|
6
|
+
function isHeadlined(check) {
|
|
7
|
+
if (check.status === 'not_applicable')
|
|
8
|
+
return true;
|
|
9
|
+
return ['fail', 'pass'].includes(check.status ?? '') && typeof check.blocking === 'boolean';
|
|
10
|
+
}
|
|
11
|
+
/** 2 whenever the platform says a finding blocks, whatever the status; every other outcome is 0. */
|
|
12
|
+
export function policyExitCode(check) {
|
|
13
|
+
return check?.blocking === true ? 2 : 0;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The one warning line for feedback without a headline (`disabled`, `unavailable`, `error`, an
|
|
17
|
+
* unknown status such as an older platform's `forbidden`, or a pass or fail that does not say whether
|
|
18
|
+
* it blocks): the server's own status and message, or that none came back. `null` for headlined
|
|
19
|
+
* feedback.
|
|
20
|
+
*/
|
|
21
|
+
export function policyCheckWarning(check) {
|
|
22
|
+
if (!check)
|
|
23
|
+
return 'Policy check: no policy feedback returned.';
|
|
24
|
+
if (isHeadlined(check))
|
|
25
|
+
return null;
|
|
26
|
+
const status = typeof check.status === 'string' && check.status.trim() !== '' ? check.status.trim() : 'unknown';
|
|
27
|
+
const message = typeof check.message === 'string' && check.message.trim() !== ''
|
|
28
|
+
? check.message.trim()
|
|
29
|
+
: (['fail', 'pass'].includes(status) ? 'the server did not say whether its findings block.' : 'no message returned.');
|
|
30
|
+
return `Policy check ${status}: ${message}`;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* What happened to the policy documents a push carried. The preview says, per policy, whether it was
|
|
34
|
+
* created, updated or left unchanged. Without a preview only the number sent is known: the import
|
|
35
|
+
* response reports an unchanged policy exactly as it reports a saved one.
|
|
36
|
+
*/
|
|
37
|
+
export function policyDocumentSummary(preview, sentPolicies) {
|
|
38
|
+
const operations = (preview?.operations ?? []).filter((op) => op.type === 'policy');
|
|
39
|
+
if (operations.length === 0) {
|
|
40
|
+
return sentPolicies > 0
|
|
41
|
+
? [`Policy documents: ${sentPolicies} sent without a preview, so which of them changed is not known`]
|
|
42
|
+
: [];
|
|
43
|
+
}
|
|
44
|
+
const named = (action) => operations.filter((op) => op.action === action).map((op) => op.name).sort();
|
|
45
|
+
const created = named('create');
|
|
46
|
+
const updated = named('update');
|
|
47
|
+
const unchanged = named('unchanged');
|
|
48
|
+
const parts = [
|
|
49
|
+
created.length > 0 && `${created.length} created (${created.join(', ')})`,
|
|
50
|
+
updated.length > 0 && `${updated.length} updated (${updated.join(', ')})`,
|
|
51
|
+
unchanged.length > 0 && `${unchanged.length} unchanged`,
|
|
52
|
+
].filter(Boolean);
|
|
53
|
+
return parts.length > 0 ? [`Policy documents: ${parts.join(', ')}`] : [];
|
|
54
|
+
}
|
|
55
|
+
/** A count the platform served, or 0. */
|
|
56
|
+
function count(value) {
|
|
57
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : 0;
|
|
58
|
+
}
|
|
59
|
+
/** What a trimmed answer (`access: "coverage"`) says beyond its findings. */
|
|
60
|
+
function trimmedEvidence(check) {
|
|
61
|
+
return {
|
|
62
|
+
elsewhere: {
|
|
63
|
+
blocking: count(check.elsewhere?.blocking),
|
|
64
|
+
introduced: check.elsewhere?.basis === 'introduced',
|
|
65
|
+
total: count(check.elsewhere?.total),
|
|
66
|
+
},
|
|
67
|
+
errors: check.counts?.errors ?? 0,
|
|
68
|
+
note: typeof check.note === 'string' ? check.note.trim() : '',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** A trimmed answer's closing lines: what it counted elsewhere, the branch's rule errors, its note. */
|
|
72
|
+
function trimmedLines(trimmed) {
|
|
73
|
+
const { elsewhere } = trimmed;
|
|
74
|
+
return [
|
|
75
|
+
// Counted, never named: the objects are not the caller's to be told about.
|
|
76
|
+
...(elsewhere.total > 0
|
|
77
|
+
// "new" only when the platform compared with the branch before the push (`basis: introduced`).
|
|
78
|
+
? [`On objects this push did not list: ${elsewhere.total} ${elsewhere.introduced ? 'new ' : ''}finding${elsewhere.total === 1 ? '' : 's'} (${elsewhere.blocking} blocking), counted, not named.`]
|
|
79
|
+
: []),
|
|
80
|
+
...(trimmed.errors > 0 ? [`Rules that could not run on the branch: ${trimmed.errors}.`] : []),
|
|
81
|
+
...(trimmed.note ? [trimmed.note] : []),
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* A push answers no run, so its `policy_check` carries the findings and results itself: the first
|
|
86
|
+
* 100 of each list, with `counts` and `total` saying what they are out of.
|
|
87
|
+
*/
|
|
88
|
+
export function pushEvidence(check) {
|
|
89
|
+
const trimmed = check && isCoverageCheck(check) ? { trimmed: trimmedEvidence(check) } : {};
|
|
90
|
+
return {
|
|
91
|
+
blocking: new Set((check?.blocking_findings ?? []).map(finding => finding.id ?? '').filter(Boolean)),
|
|
92
|
+
...(check?.truncated && check.counts
|
|
93
|
+
? { cut: { blocking: check.counts.blocking, findings: check.total ?? check.counts.findings, runId: check.run_id ?? 0 } }
|
|
94
|
+
: {}),
|
|
95
|
+
findings: check?.findings ?? [],
|
|
96
|
+
results: check?.results ?? [],
|
|
97
|
+
snapshot: [],
|
|
98
|
+
...trimmed,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* A push's `policy_check` for a caller without `workspace:policy` read: only that caller's findings
|
|
103
|
+
* on the objects the push carried, and no run it may read.
|
|
104
|
+
*/
|
|
105
|
+
export function isCoverageCheck(check) {
|
|
106
|
+
return check?.access === 'coverage';
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* An evaluation answers the run, and its verdict names the blocking findings by id. Its summary
|
|
110
|
+
* answer (`--summary`) lists the first findings, and its counts say what they are out of.
|
|
111
|
+
*/
|
|
112
|
+
export function evaluationEvidence(evaluation) {
|
|
113
|
+
const { counts } = evaluation;
|
|
114
|
+
return {
|
|
115
|
+
blocking: new Set(evaluation.policy_check?.blocking_finding_ids ?? []),
|
|
116
|
+
...(evaluation.truncated && counts
|
|
117
|
+
? { cut: { blocking: counts.blocking, findings: evaluation.total ?? counts.findings, runId: evaluation.stored === false ? 0 : (evaluation.id ?? 0) } }
|
|
118
|
+
: {}),
|
|
119
|
+
findings: evaluation.findings ?? [],
|
|
120
|
+
results: evaluation.results ?? [],
|
|
121
|
+
snapshot: evaluation.policies ?? [],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/** A list's count, or `first N of M` when the answer lists only the first of them. */
|
|
125
|
+
function listed(count, all) {
|
|
126
|
+
return all !== undefined && all > count ? `first ${count} of ${all}` : String(count);
|
|
127
|
+
}
|
|
128
|
+
/** The headline's outcome. A pass whose rules inspected no object is never headlined as a plain pass. */
|
|
129
|
+
function outcome(check, results) {
|
|
130
|
+
if (check.status === 'fail')
|
|
131
|
+
return check.blocking ? 'fail (blocking findings)' : 'advisory findings (not blocking)';
|
|
132
|
+
if (check.status !== 'pass')
|
|
133
|
+
return check.status ?? '';
|
|
134
|
+
const unchecked = results.filter((result) => hasNoObjects(result)).length;
|
|
135
|
+
if (unchecked === 0)
|
|
136
|
+
return 'pass';
|
|
137
|
+
return unchecked === results.length
|
|
138
|
+
? 'pass, but no objects were checked'
|
|
139
|
+
: `pass, but ${unchecked} rule${unchecked === 1 ? '' : 's'} checked no objects`;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The feedback on stdout: a headline with the server's message, then any findings, errors and
|
|
143
|
+
* warnings. Feedback without a headline is reported by `policyCheckWarning` instead.
|
|
144
|
+
*/
|
|
145
|
+
export function policySummary(check, evidence) {
|
|
146
|
+
if (!check)
|
|
147
|
+
return [];
|
|
148
|
+
const lines = [];
|
|
149
|
+
if (isHeadlined(check)) {
|
|
150
|
+
lines.push(`Policy check: ${outcome(check, evidence.results)}`);
|
|
151
|
+
if (check.message)
|
|
152
|
+
lines.push(check.message);
|
|
153
|
+
}
|
|
154
|
+
const rules = snapshotRules(evidence.snapshot);
|
|
155
|
+
const { blocking, cut, findings, trimmed } = evidence;
|
|
156
|
+
const render = (finding) => trimmed ? coverageFindingLines(finding) : [findingLine(finding, rules)];
|
|
157
|
+
// A cut list is out of the counts; the ones the answer lists come first, blocking first.
|
|
158
|
+
const allBlocking = cut?.blocking;
|
|
159
|
+
const allAdvisory = cut ? cut.findings - cut.blocking : undefined;
|
|
160
|
+
if (cut)
|
|
161
|
+
lines.push(`Findings: ${cut.findings} (${cut.blocking} blocking, ${allAdvisory} advisory)`);
|
|
162
|
+
// The list splits only when some findings block and others do not.
|
|
163
|
+
const separated = blocking.size > 0 && findings.some(finding => !blocking.has(finding.id ?? ''));
|
|
164
|
+
if (separated) {
|
|
165
|
+
const blocked = findings.filter(finding => blocking.has(finding.id ?? ''));
|
|
166
|
+
const advisory = findings.filter(finding => !blocking.has(finding.id ?? ''));
|
|
167
|
+
lines.push(`Blocking findings (${listed(blocked.length, allBlocking)}) — active blocking policies; a gate refuses a change that introduces one or changes its object:`, ...blocked.flatMap(finding => render(finding)), `Advisory findings (${listed(advisory.length, allAdvisory)}) — reported, not blocking:`, ...advisory.flatMap(finding => render(finding)));
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
if (blocking.size > 0 && findings.length > 0)
|
|
171
|
+
lines.push(`Blocking findings (${listed(findings.length, allBlocking)}) — active blocking policies; a gate refuses a change that introduces one or changes its object:`);
|
|
172
|
+
// Without a headline (an `error` status, say) nothing else says what these findings are.
|
|
173
|
+
else if (findings.length > 0 && !isHeadlined(check))
|
|
174
|
+
lines.push(`Advisory findings (${listed(findings.length, allAdvisory)}) — reported, not blocking:`);
|
|
175
|
+
lines.push(...findings.flatMap(finding => render(finding)));
|
|
176
|
+
}
|
|
177
|
+
if (cut) {
|
|
178
|
+
// A trimmed answer names no run: the caller may not read one.
|
|
179
|
+
lines.push(trimmed
|
|
180
|
+
? `Listed: the first ${findings.length} of ${cut.findings}.`
|
|
181
|
+
: `Listed: the first ${findings.length} of ${cut.findings}; ${cut.runId > 0
|
|
182
|
+
? `\`xano policy runs ${cut.runId}\` has them all.`
|
|
183
|
+
: 'this evaluation was not stored, so the rest cannot be listed.'}`);
|
|
184
|
+
}
|
|
185
|
+
lines.push(...policyResultSummary(evidence.results));
|
|
186
|
+
if (trimmed)
|
|
187
|
+
lines.push(...trimmedLines(trimmed));
|
|
188
|
+
return lines;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* The platform's one notice that a push or its dry run left policy files out
|
|
192
|
+
* (`policies_skipped.message`): a push to a remote tenant or to a tenant without a policy table, a
|
|
193
|
+
* `release push` by a caller without `workspace:policy` create and update (its release carries the
|
|
194
|
+
* live branch's policies instead), and any push while the Policies feature is off. `null` when it
|
|
195
|
+
* left none out.
|
|
196
|
+
*/
|
|
197
|
+
export function policiesSkippedNotice(answer) {
|
|
198
|
+
const skipped = answer && typeof answer === 'object' ? answer.policies_skipped : undefined;
|
|
199
|
+
const message = skipped && typeof skipped === 'object' ? skipped.message : undefined;
|
|
200
|
+
return typeof message === 'string' && message.trim() !== '' ? message.trim() : null;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* The line for an evaluation that ran but was not recorded (the credential may read, not write), or
|
|
204
|
+
* `null`. A branch with nothing to evaluate stores nothing either, and its headline says so.
|
|
205
|
+
*/
|
|
206
|
+
export function notStoredLine(evaluation) {
|
|
207
|
+
if (evaluation.stored !== false || !['error', 'fail', 'pass'].includes(evaluation.policy_check?.status ?? ''))
|
|
208
|
+
return null;
|
|
209
|
+
return 'Not stored: this credential can run checks but not record runs, so `xano policy status` and `xano policy runs` will not show this one.';
|
|
210
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { CoverageFinding, PolicyFinding, PolicyRuleResult, PolicySnapshotPolicy, PolicySnapshotRule } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The platform's naming rule, shared with Studio: the author's title, then the check's human
|
|
4
|
+
* label, then the rule id. Pass `label` from the run snapshot, or from the catalogue entry for
|
|
5
|
+
* `check` when the caller already holds the catalogue; never fetch one just to name a rule.
|
|
6
|
+
*/
|
|
7
|
+
export declare function policyRuleName(rule: PolicySnapshotRule): string;
|
|
8
|
+
/** Collision-free regardless of what a key or rule id contains. */
|
|
9
|
+
export declare const ruleKey: (policyKey?: string, ruleId?: string) => string;
|
|
10
|
+
export declare function snapshotRules(policies: PolicySnapshotPolicy[]): Map<string, PolicySnapshotRule>;
|
|
11
|
+
/**
|
|
12
|
+
* One finding line. The rule id leads it, because the check's label is shared by every
|
|
13
|
+
* rule that uses that check — with two rules of one policy failing, the labels alone do
|
|
14
|
+
* not tell them apart, and the id is what `--run-detail` and the JSON cite.
|
|
15
|
+
*/
|
|
16
|
+
export declare function findingLine(finding: PolicyFinding, rules: Map<string, PolicySnapshotRule>): string;
|
|
17
|
+
/**
|
|
18
|
+
* A finding as a caller without `workspace:policy` read is told it (`access: "coverage"`): its rule's
|
|
19
|
+
* name, its policy's key and title, its object and message, then the check's fix hint on a line of
|
|
20
|
+
* its own. Such a finding has no rule id or severity, and a deleted policy's finding has no fix.
|
|
21
|
+
*/
|
|
22
|
+
export declare function coverageFindingLines(finding: CoverageFinding): string[];
|
|
23
|
+
/** A completed rule with no objects checked, including older stored pass + checked 0 results. */
|
|
24
|
+
export declare function hasNoObjects(result: PolicyRuleResult): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Rule errors ("this rule could not run") and scope warnings ("part of your scope selected
|
|
27
|
+
* nothing") under their own headings, then the rules that inspected nothing.
|
|
28
|
+
*/
|
|
29
|
+
export declare function policyResultSummary(results?: PolicyRuleResult[]): string[];
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The platform's naming rule, shared with Studio: the author's title, then the check's human
|
|
3
|
+
* label, then the rule id. Pass `label` from the run snapshot, or from the catalogue entry for
|
|
4
|
+
* `check` when the caller already holds the catalogue; never fetch one just to name a rule.
|
|
5
|
+
*/
|
|
6
|
+
export function policyRuleName(rule) {
|
|
7
|
+
return rule.title?.trim() || rule.label?.trim() || rule.id?.trim() || '';
|
|
8
|
+
}
|
|
9
|
+
/** Collision-free regardless of what a key or rule id contains. */
|
|
10
|
+
export const ruleKey = (policyKey = '', ruleId = '') => JSON.stringify([policyKey, ruleId]);
|
|
11
|
+
export function snapshotRules(policies) {
|
|
12
|
+
return new Map(policies.flatMap(policy => (policy.rules ?? []).map(rule => [ruleKey(policy.key, rule.id), rule])));
|
|
13
|
+
}
|
|
14
|
+
/** A finding names its rule the way the platform does, falling back to what the run recorded. */
|
|
15
|
+
function findingName(finding, rules) {
|
|
16
|
+
const recorded = rules.get(ruleKey(finding.policy_key, finding.rule_id)) ?? {};
|
|
17
|
+
return policyRuleName({
|
|
18
|
+
...recorded,
|
|
19
|
+
id: finding.rule_id ?? recorded.id,
|
|
20
|
+
title: finding.rule_title || recorded.title,
|
|
21
|
+
}) || 'finding';
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* One finding line. The rule id leads it, because the check's label is shared by every
|
|
25
|
+
* rule that uses that check — with two rules of one policy failing, the labels alone do
|
|
26
|
+
* not tell them apart, and the id is what `--run-detail` and the JSON cite.
|
|
27
|
+
*/
|
|
28
|
+
export function findingLine(finding, rules) {
|
|
29
|
+
// A rule the author left unnamed is called by its check's label, and only then by its id.
|
|
30
|
+
const rule = findingName(finding, rules);
|
|
31
|
+
const id = finding.rule_id?.trim();
|
|
32
|
+
const policy = finding.policy_title || finding.policy_key || 'policy';
|
|
33
|
+
const severity = finding.severity?.trim() ? ` [${finding.severity.trim()}]` : '';
|
|
34
|
+
return ` ${[id, id === rule ? '' : rule].filter(Boolean).join(' ')}${severity} (${policy}) ${finding.object?.type ?? ''} ${finding.object?.name ?? ''}: ${finding.message ?? ''}`;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A finding as a caller without `workspace:policy` read is told it (`access: "coverage"`): its rule's
|
|
38
|
+
* name, its policy's key and title, its object and message, then the check's fix hint on a line of
|
|
39
|
+
* its own. Such a finding has no rule id or severity, and a deleted policy's finding has no fix.
|
|
40
|
+
*/
|
|
41
|
+
export function coverageFindingLines(finding) {
|
|
42
|
+
const key = finding.policy_key?.trim() || 'policy';
|
|
43
|
+
const title = finding.policy_title?.trim();
|
|
44
|
+
const rule = finding.rule_title?.trim() || finding.check?.label?.trim() || 'finding';
|
|
45
|
+
const object = [finding.object?.type, finding.object?.name].filter(Boolean).join(' ');
|
|
46
|
+
const deleted = finding.deleted ? ' [policy deleted]' : '';
|
|
47
|
+
const lines = [` ${rule} (${title ? `${key} ${title}` : key})${deleted} ${object}: ${finding.message ?? ''}`];
|
|
48
|
+
const fix = finding.deleted ? '' : finding.check?.fix_hint?.trim();
|
|
49
|
+
if (fix)
|
|
50
|
+
lines.push(` Fix: ${fix}`);
|
|
51
|
+
return lines;
|
|
52
|
+
}
|
|
53
|
+
/** A completed rule with no objects checked, including older stored pass + checked 0 results. */
|
|
54
|
+
export function hasNoObjects(result) {
|
|
55
|
+
return ['no_objects', 'pass'].includes(result.status ?? '') && (result.checked ?? 0) === 0;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Rule errors ("this rule could not run") and scope warnings ("part of your scope selected
|
|
59
|
+
* nothing") under their own headings, then the rules that inspected nothing.
|
|
60
|
+
*/
|
|
61
|
+
export function policyResultSummary(results = []) {
|
|
62
|
+
const where = (result) => `${result.policy_key ?? 'policy'} ${result.check_id ?? 'rule'}`;
|
|
63
|
+
const errors = results.filter(result => result.status === 'error')
|
|
64
|
+
.map(result => ` ${where(result)}: ${result.message ?? 'No diagnostic returned.'}`);
|
|
65
|
+
const warnings = results.flatMap(result => (result.warnings ?? []).map(warning => ` ${where(result)}: ${warning}`));
|
|
66
|
+
const lines = [];
|
|
67
|
+
if (errors.length > 0)
|
|
68
|
+
lines.push('Errors:', ...errors);
|
|
69
|
+
if (warnings.length > 0)
|
|
70
|
+
lines.push('Warnings:', ...warnings);
|
|
71
|
+
const checked = results.reduce((sum, result) => sum + (result.checked ?? 0), 0);
|
|
72
|
+
// When no rule checked anything, one sentence about the run says it all.
|
|
73
|
+
if (results.length > 0 && checked === 0) {
|
|
74
|
+
lines.push('No objects checked; this run does not demonstrate coverage.');
|
|
75
|
+
return lines;
|
|
76
|
+
}
|
|
77
|
+
const empty = results.filter(result => hasNoObjects(result));
|
|
78
|
+
if (empty.length > 0) {
|
|
79
|
+
lines.push('No objects checked (proves nothing about coverage):', ...empty.map(result => ` ${where(result)}: no objects checked`));
|
|
80
|
+
}
|
|
81
|
+
return lines;
|
|
82
|
+
}
|