@codyswann/lisa 3.47.10 → 3.48.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/all/copy-overwrite/scripts/lisa-work-item.mjs +19 -6
- package/dist/cli/doctor-render.d.ts +22 -0
- package/dist/cli/doctor-render.d.ts.map +1 -0
- package/dist/cli/doctor-render.js +27 -0
- package/dist/cli/doctor-render.js.map +1 -0
- package/dist/cli/doctor.d.ts +15 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +2 -9
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/gate-report-cells.d.ts +101 -0
- package/dist/cli/gate-report-cells.d.ts.map +1 -0
- package/dist/cli/gate-report-cells.js +269 -0
- package/dist/cli/gate-report-cells.js.map +1 -0
- package/dist/cli/gate-report-config.d.ts +28 -0
- package/dist/cli/gate-report-config.d.ts.map +1 -0
- package/dist/cli/gate-report-config.js +33 -0
- package/dist/cli/gate-report-config.js.map +1 -0
- package/dist/cli/gate-report-executors.d.ts +62 -0
- package/dist/cli/gate-report-executors.d.ts.map +1 -0
- package/dist/cli/gate-report-executors.js +218 -0
- package/dist/cli/gate-report-executors.js.map +1 -0
- package/dist/cli/gate-report-preview.d.ts +27 -0
- package/dist/cli/gate-report-preview.d.ts.map +1 -0
- package/dist/cli/gate-report-preview.js +286 -0
- package/dist/cli/gate-report-preview.js.map +1 -0
- package/dist/cli/gate-report-registry.d.ts +68 -0
- package/dist/cli/gate-report-registry.d.ts.map +1 -0
- package/dist/cli/gate-report-registry.js +52 -0
- package/dist/cli/gate-report-registry.js.map +1 -0
- package/dist/cli/gate-report-ruleset.d.ts +60 -0
- package/dist/cli/gate-report-ruleset.d.ts.map +1 -0
- package/dist/cli/gate-report-ruleset.js +132 -0
- package/dist/cli/gate-report-ruleset.js.map +1 -0
- package/dist/cli/gate-report-skip-jobs.d.ts +34 -0
- package/dist/cli/gate-report-skip-jobs.d.ts.map +1 -0
- package/dist/cli/gate-report-skip-jobs.js +42 -0
- package/dist/cli/gate-report-skip-jobs.js.map +1 -0
- package/dist/cli/gate-report-summary.d.ts +18 -0
- package/dist/cli/gate-report-summary.d.ts.map +1 -0
- package/dist/cli/gate-report-summary.js +80 -0
- package/dist/cli/gate-report-summary.js.map +1 -0
- package/dist/cli/gate-report-types.d.ts +206 -0
- package/dist/cli/gate-report-types.d.ts.map +1 -0
- package/dist/cli/gate-report-types.js +19 -0
- package/dist/cli/gate-report-types.js.map +1 -0
- package/dist/cli/gate-report.d.ts +35 -0
- package/dist/cli/gate-report.d.ts.map +1 -0
- package/dist/cli/gate-report.js +317 -0
- package/dist/cli/gate-report.js.map +1 -0
- package/dist/cli/ui-github-repo-gh.d.ts +14 -0
- package/dist/cli/ui-github-repo-gh.d.ts.map +1 -1
- package/dist/cli/ui-github-repo-gh.js +6 -1
- package/dist/cli/ui-github-repo-gh.js.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +1 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +19 -1
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read `.lisa.config.json`, degrading a refusal into a stated problem.
|
|
3
|
+
* @param registry - The shipped registry
|
|
4
|
+
* @param projectRoot - Project root
|
|
5
|
+
* @returns The parsed config
|
|
6
|
+
*/
|
|
7
|
+
export function readConfig(registry, projectRoot) {
|
|
8
|
+
try {
|
|
9
|
+
const { runner, gates } = registry.readGates(projectRoot);
|
|
10
|
+
return {
|
|
11
|
+
gates,
|
|
12
|
+
runner,
|
|
13
|
+
runnerFinding: { state: "verified", value: runner },
|
|
14
|
+
runnerSource: runner === registry.DEFAULT_RUNNER ? "default" : "declared",
|
|
15
|
+
problems: registry.validateGates(gates),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
20
|
+
return {
|
|
21
|
+
gates: {},
|
|
22
|
+
runner: registry.DEFAULT_RUNNER,
|
|
23
|
+
runnerFinding: {
|
|
24
|
+
state: "unknown",
|
|
25
|
+
reason: "config-unreadable",
|
|
26
|
+
message,
|
|
27
|
+
},
|
|
28
|
+
runnerSource: "unknown",
|
|
29
|
+
problems: [message],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=gate-report-config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gate-report-config.js","sourceRoot":"","sources":["../../src/cli/gate-report-config.ts"],"names":[],"mappings":"AAsBA;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CACxB,QAA4B,EAC5B,WAAmB;IAEnB,IAAI,CAAC;QACH,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,QAAQ,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;QAC1D,OAAO;YACL,KAAK;YACL,MAAM;YACN,aAAa,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE;YACnD,YAAY,EAAE,MAAM,KAAK,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU;YACzE,QAAQ,EAAE,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC;SACxC,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACvE,OAAO;YACL,KAAK,EAAE,EAAE;YACT,MAAM,EAAE,QAAQ,CAAC,cAAc;YAC/B,aAAa,EAAE;gBACb,KAAK,EAAE,SAAS;gBAChB,MAAM,EAAE,mBAAmB;gBAC3B,OAAO;aACR;YACD,YAAY,EAAE,SAAS;YACvB,QAAQ,EAAE,CAAC,OAAO,CAAC;SACpB,CAAC;IACJ,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ExecutorEvidence } from "./gate-report-types.js";
|
|
2
|
+
/** One project hook file, read and classified. */
|
|
3
|
+
export interface HookFile {
|
|
4
|
+
/** Project-relative path, e.g. `.husky/pre-push`. */
|
|
5
|
+
readonly file: string;
|
|
6
|
+
/** Its contents, with comment lines blanked. */
|
|
7
|
+
readonly body: string;
|
|
8
|
+
/** The moments this hook fires at, sorted. */
|
|
9
|
+
readonly moments: readonly string[];
|
|
10
|
+
/** Moments it hands to the gate runner, sorted. Empty when it runs none. */
|
|
11
|
+
readonly gateRunnerMoments: readonly string[];
|
|
12
|
+
/** Gates it carries a `lisa_gate_covers`-guarded built-in step for. */
|
|
13
|
+
readonly builtinGates: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
/** Everything the project's hooks were found to do. */
|
|
16
|
+
export interface HookEvidence {
|
|
17
|
+
/** Hook files, sorted by name, so the report is stable across machines. */
|
|
18
|
+
readonly files: readonly HookFile[];
|
|
19
|
+
}
|
|
20
|
+
/** Inputs for one (gate, moment) executor lookup. */
|
|
21
|
+
export interface ExecutorQuery {
|
|
22
|
+
/** The moment being reported. */
|
|
23
|
+
readonly moment: string;
|
|
24
|
+
/** The gate being reported. */
|
|
25
|
+
readonly gateId: string;
|
|
26
|
+
/** The task this gate resolves to, or null. */
|
|
27
|
+
readonly task: string | null;
|
|
28
|
+
/** Whether the settings file puts this pair into service. */
|
|
29
|
+
readonly declared: boolean;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Classify one hook file.
|
|
33
|
+
* @param file - Project-relative path
|
|
34
|
+
* @param raw - Raw file contents
|
|
35
|
+
* @returns The classified hook
|
|
36
|
+
*/
|
|
37
|
+
export declare function classifyHook(file: string, raw: string): HookFile;
|
|
38
|
+
/**
|
|
39
|
+
* Read and classify every hook in `.husky`.
|
|
40
|
+
* @param projectRoot - Project root
|
|
41
|
+
* @returns Hook evidence, empty when the directory is absent
|
|
42
|
+
*/
|
|
43
|
+
export declare function collectHookEvidence(projectRoot: string): Promise<HookEvidence>;
|
|
44
|
+
/**
|
|
45
|
+
* Whether a hook body invokes one task literally, outside the gate runner.
|
|
46
|
+
*
|
|
47
|
+
* Bounded on both sides on purpose. `test:cov` must not match `test:cov:unit`:
|
|
48
|
+
* they are different scripts, and reporting the wrong one as proof would
|
|
49
|
+
* manufacture exactly the green this report exists to withhold.
|
|
50
|
+
* @param body - Comment-stripped hook body
|
|
51
|
+
* @param task - The task to look for
|
|
52
|
+
* @returns True when the hook runs that exact task
|
|
53
|
+
*/
|
|
54
|
+
export declare function hookInvokesTask(body: string, task: string): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Every executor proved for one gate at one moment.
|
|
57
|
+
* @param evidence - Hook evidence for the project
|
|
58
|
+
* @param query - The pair being reported
|
|
59
|
+
* @returns Executors, sorted by file then kind
|
|
60
|
+
*/
|
|
61
|
+
export declare function executorsFor(evidence: HookEvidence, query: ExecutorQuery): ExecutorEvidence[];
|
|
62
|
+
//# sourceMappingURL=gate-report-executors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gate-report-executors.d.ts","sourceRoot":"","sources":["../../src/cli/gate-report-executors.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AA4C/D,kDAAkD;AAClD,MAAM,WAAW,QAAQ;IACvB,qDAAqD;IACrD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,gDAAgD;IAChD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8CAA8C;IAC9C,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,4EAA4E;IAC5E,QAAQ,CAAC,iBAAiB,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9C,uEAAuE;IACvE,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAED,uDAAuD;AACvD,MAAM,WAAW,YAAY;IAC3B,2EAA2E;IAC3E,QAAQ,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;CACrC;AAED,qDAAqD;AACrD,MAAM,WAAW,aAAa;IAC5B,iCAAiC;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,+BAA+B;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,+CAA+C;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,6DAA6D;IAC7D,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B;AA2CD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,QAAQ,CAgBhE;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,YAAY,CAAC,CAoBvB;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAOnE;AAqED;;;;;GAKG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,YAAY,EACtB,KAAK,EAAE,aAAa,GACnB,gBAAgB,EAAE,CAMpB"}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What, in this project, would actually run a gate's task.
|
|
3
|
+
*
|
|
4
|
+
* Three kinds, and the difference between them is the report's central
|
|
5
|
+
* question. A `gate-runner` site reads the declaration — the hook shells
|
|
6
|
+
* `lisa-run-gates.mjs --moment=<moment>`, so changing the settings file changes
|
|
7
|
+
* what runs. A `hook-builtin` site does not: the shipped hooks carry a
|
|
8
|
+
* fallback step for a named gate, guarded by `lisa_gate_covers <gate>`, which
|
|
9
|
+
* stands down only when the declaration covered it and runs unconditionally
|
|
10
|
+
* otherwise. A `hook-literal` site is the same story without the marker — the
|
|
11
|
+
* command is simply written into the script.
|
|
12
|
+
*
|
|
13
|
+
* The last two are bucket B, the one an operator most needs and can least
|
|
14
|
+
* discover: the check IS running, so the repository looks healthy, and the
|
|
15
|
+
* configuration surface is a decoration.
|
|
16
|
+
*
|
|
17
|
+
* Everything here is read from files the project holds, so both answers are
|
|
18
|
+
* verified rather than inferred — including the negative. "No hook runs this"
|
|
19
|
+
* is a fact about `.husky/`, not a guess about CI.
|
|
20
|
+
* @module cli/gate-report-executors
|
|
21
|
+
*/
|
|
22
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
23
|
+
import * as path from "node:path";
|
|
24
|
+
/** A hook that mentions this is shelling Lisa's gate runner. */
|
|
25
|
+
const GATE_RUNNER_REFERENCE = "lisa-run-gates.mjs";
|
|
26
|
+
/**
|
|
27
|
+
* The moments a hook hands to the gate runner.
|
|
28
|
+
*
|
|
29
|
+
* Scanned file-wide rather than line-wide, because the shipped hooks assign the
|
|
30
|
+
* runner to a shell variable several lines above the call site, then invoke
|
|
31
|
+
* `node "$GATE_RUNNER" --moment=push`. A same-line pattern matches neither
|
|
32
|
+
* half, and a report that found no gate runner in a repository whose hooks run
|
|
33
|
+
* one on every commit would be the exact "not present, therefore does not run"
|
|
34
|
+
* error this report exists to remove.
|
|
35
|
+
*/
|
|
36
|
+
const MOMENT_FLAG = /--moment=([A-Za-z0-9:_-]{1,64})/g;
|
|
37
|
+
/**
|
|
38
|
+
* The handover marker guarding every built-in step in a shipped hook.
|
|
39
|
+
*
|
|
40
|
+
* `lisa_gate_covers <gate>` is the hook asking whether the gate runner already
|
|
41
|
+
* proved this property. Its presence is therefore proof of the opposite thing
|
|
42
|
+
* from what it reads like: a built-in step exists here, and it runs whenever
|
|
43
|
+
* the declaration does not cover it.
|
|
44
|
+
*/
|
|
45
|
+
const BUILTIN_MARKER = /lisa_gate_covers[ \t]{1,8}([a-z][a-z0-9-]{0,63})/g;
|
|
46
|
+
/** Package-manager prefixes a hook may invoke a task through. */
|
|
47
|
+
const RUNNER_PREFIX = String.raw `(?:\$RUNNER|\$\{RUNNER\}|npm run|pnpm run|yarn run|bun run|npm|yarn|bun|pnpm)`;
|
|
48
|
+
/**
|
|
49
|
+
* The moment a git hook fires at, for hooks that name no moment themselves.
|
|
50
|
+
*
|
|
51
|
+
* `commit-msg` runs commitlint with no gate marker and no `--moment` flag, so
|
|
52
|
+
* without this it would contribute evidence to no moment at all. The mapping is
|
|
53
|
+
* git's own semantics, not a Lisa convention.
|
|
54
|
+
*/
|
|
55
|
+
const HOOK_NAME_MOMENTS = {
|
|
56
|
+
"pre-commit": "commit",
|
|
57
|
+
"commit-msg": "commit",
|
|
58
|
+
"prepare-commit-msg": "commit",
|
|
59
|
+
"pre-push": "push",
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Order strings without mutating the caller's collection.
|
|
63
|
+
* @param values - Values to order
|
|
64
|
+
* @returns A new, sorted array
|
|
65
|
+
*/
|
|
66
|
+
function sorted(values) {
|
|
67
|
+
return [...values].sort((left, right) => left.localeCompare(right));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Drop shell comments so a commented-out step is never read as an executor.
|
|
71
|
+
*
|
|
72
|
+
* `.husky/pre-push` carries a worked example of a Lighthouse step inside a
|
|
73
|
+
* comment block. Counting that as proof the gate runs is exactly the class of
|
|
74
|
+
* error this report exists to refuse.
|
|
75
|
+
* @param body - Raw file contents
|
|
76
|
+
* @returns The same text with comment lines blanked
|
|
77
|
+
*/
|
|
78
|
+
function stripComments(body) {
|
|
79
|
+
return body
|
|
80
|
+
.split("\n")
|
|
81
|
+
.map(line => (/^\s{0,64}#/.test(line) ? "" : line))
|
|
82
|
+
.join("\n");
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Every distinct capture of one global pattern, sorted.
|
|
86
|
+
* @param body - Text to scan
|
|
87
|
+
* @param pattern - A global regular expression with one capture group
|
|
88
|
+
* @returns Sorted, de-duplicated captures
|
|
89
|
+
*/
|
|
90
|
+
function captures(body, pattern) {
|
|
91
|
+
return sorted(new Set([...body.matchAll(pattern)]
|
|
92
|
+
.map(match => match[1])
|
|
93
|
+
.filter((value) => value !== undefined)));
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Classify one hook file.
|
|
97
|
+
* @param file - Project-relative path
|
|
98
|
+
* @param raw - Raw file contents
|
|
99
|
+
* @returns The classified hook
|
|
100
|
+
*/
|
|
101
|
+
export function classifyHook(file, raw) {
|
|
102
|
+
const body = stripComments(raw);
|
|
103
|
+
const name = file.slice(file.lastIndexOf("/") + 1);
|
|
104
|
+
const gateRunnerMoments = body.includes(GATE_RUNNER_REFERENCE)
|
|
105
|
+
? captures(body, MOMENT_FLAG)
|
|
106
|
+
: [];
|
|
107
|
+
const byName = HOOK_NAME_MOMENTS[name];
|
|
108
|
+
const allMoments = byName === undefined ? gateRunnerMoments : [...gateRunnerMoments, byName];
|
|
109
|
+
return {
|
|
110
|
+
file,
|
|
111
|
+
body,
|
|
112
|
+
moments: sorted(new Set(allMoments)),
|
|
113
|
+
gateRunnerMoments,
|
|
114
|
+
builtinGates: captures(body, BUILTIN_MARKER),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Read and classify every hook in `.husky`.
|
|
119
|
+
* @param projectRoot - Project root
|
|
120
|
+
* @returns Hook evidence, empty when the directory is absent
|
|
121
|
+
*/
|
|
122
|
+
export async function collectHookEvidence(projectRoot) {
|
|
123
|
+
const dir = path.join(projectRoot, ".husky");
|
|
124
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => undefined);
|
|
125
|
+
if (entries === undefined)
|
|
126
|
+
return { files: [] };
|
|
127
|
+
const names = sorted(entries
|
|
128
|
+
.filter(entry => entry.isFile() && !entry.name.startsWith("."))
|
|
129
|
+
.map(entry => entry.name));
|
|
130
|
+
const read = await Promise.all(names.map(async (name) => {
|
|
131
|
+
const raw = await readFile(path.join(dir, name), "utf8").catch(() => undefined);
|
|
132
|
+
return raw === undefined ? null : classifyHook(`.husky/${name}`, raw);
|
|
133
|
+
}));
|
|
134
|
+
return { files: read.filter((hook) => hook !== null) };
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Whether a hook body invokes one task literally, outside the gate runner.
|
|
138
|
+
*
|
|
139
|
+
* Bounded on both sides on purpose. `test:cov` must not match `test:cov:unit`:
|
|
140
|
+
* they are different scripts, and reporting the wrong one as proof would
|
|
141
|
+
* manufacture exactly the green this report exists to withhold.
|
|
142
|
+
* @param body - Comment-stripped hook body
|
|
143
|
+
* @param task - The task to look for
|
|
144
|
+
* @returns True when the hook runs that exact task
|
|
145
|
+
*/
|
|
146
|
+
export function hookInvokesTask(body, task) {
|
|
147
|
+
const escaped = task.replace(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`);
|
|
148
|
+
const pattern = new RegExp(String.raw `(?:^|[\s;&|(])${RUNNER_PREFIX}\s+["']?${escaped}["']?(?=$|[\s"';|&)])`, "m");
|
|
149
|
+
return pattern.test(body);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The evidence a hook contributes for a gate no marker of its names.
|
|
153
|
+
* @param hook - The hook file
|
|
154
|
+
* @param task - The task this gate resolves to, or null
|
|
155
|
+
* @returns Zero or one piece of evidence
|
|
156
|
+
*/
|
|
157
|
+
function unmarkedEvidence(hook, task) {
|
|
158
|
+
if (task === null || !hookInvokesTask(hook.body, task))
|
|
159
|
+
return [];
|
|
160
|
+
return [
|
|
161
|
+
{
|
|
162
|
+
kind: "hook-literal",
|
|
163
|
+
file: hook.file,
|
|
164
|
+
detail: `${hook.file} runs \`${task}\` as a written-in step, whatever the settings file says`,
|
|
165
|
+
},
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The gate-runner evidence a hook contributes, when the pair is declared.
|
|
170
|
+
* @param hook - The hook file
|
|
171
|
+
* @param query - The pair being reported
|
|
172
|
+
* @returns Zero or one piece of evidence
|
|
173
|
+
*/
|
|
174
|
+
function runnerEvidence(hook, query) {
|
|
175
|
+
if (!query.declared || !hook.gateRunnerMoments.includes(query.moment)) {
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
return [
|
|
179
|
+
{
|
|
180
|
+
kind: "gate-runner",
|
|
181
|
+
file: hook.file,
|
|
182
|
+
detail: `${hook.file} runs the gate runner at ${query.moment}, which reads this declaration`,
|
|
183
|
+
},
|
|
184
|
+
];
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* One hook's contribution to one (gate, moment) pair.
|
|
188
|
+
* @param hook - The hook file
|
|
189
|
+
* @param query - The pair being reported
|
|
190
|
+
* @returns Evidence from this hook alone
|
|
191
|
+
*/
|
|
192
|
+
function evidenceFrom(hook, query) {
|
|
193
|
+
const runner = runnerEvidence(hook, query);
|
|
194
|
+
if (!hook.moments.includes(query.moment))
|
|
195
|
+
return runner;
|
|
196
|
+
if (!hook.builtinGates.includes(query.gateId)) {
|
|
197
|
+
return [...runner, ...unmarkedEvidence(hook, query.task)];
|
|
198
|
+
}
|
|
199
|
+
return [
|
|
200
|
+
...runner,
|
|
201
|
+
{
|
|
202
|
+
kind: "hook-builtin",
|
|
203
|
+
file: hook.file,
|
|
204
|
+
detail: `${hook.file} carries a built-in ${query.gateId} step that runs unless the declaration covers it`,
|
|
205
|
+
},
|
|
206
|
+
];
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Every executor proved for one gate at one moment.
|
|
210
|
+
* @param evidence - Hook evidence for the project
|
|
211
|
+
* @param query - The pair being reported
|
|
212
|
+
* @returns Executors, sorted by file then kind
|
|
213
|
+
*/
|
|
214
|
+
export function executorsFor(evidence, query) {
|
|
215
|
+
const found = evidence.files.flatMap(hook => evidenceFrom(hook, query));
|
|
216
|
+
return [...found].sort((left, right) => left.file.localeCompare(right.file) || left.kind.localeCompare(right.kind));
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=gate-report-executors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gate-report-executors.js","sourceRoot":"","sources":["../../src/cli/gate-report-executors.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render the gate report as a standalone HTML page.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately NOT part of `ui/index.html`. That file is one 13,000-line
|
|
5
|
+
* document with no build step and a known hazard where concurrent edits merge
|
|
6
|
+
* badly, and where the report should finally live is an open question nobody
|
|
7
|
+
* has ruled on. A separate page sidesteps both: it is a viewing surface for a
|
|
8
|
+
* payload that already exists, not a decision about the console's structure.
|
|
9
|
+
*
|
|
10
|
+
* Pure and total — report in, string out, no I/O — so the same emitter output
|
|
11
|
+
* always produces the same bytes.
|
|
12
|
+
*
|
|
13
|
+
* The rendering rule that matters: `unknown` must never look like a pass. A
|
|
14
|
+
* cell this run could not determine is drawn in its own hatched, muted style
|
|
15
|
+
* with its own label, and the header counts it in a band of its own rather
|
|
16
|
+
* than folding it into a green total.
|
|
17
|
+
* @module cli/gate-report-preview
|
|
18
|
+
*/
|
|
19
|
+
import type { GateReport } from "./gate-report-types.js";
|
|
20
|
+
/**
|
|
21
|
+
* Render the whole page.
|
|
22
|
+
* @param report - The emitter's output
|
|
23
|
+
* @param projectLabel - How to name the project being reported on
|
|
24
|
+
* @returns A complete, self-contained HTML document
|
|
25
|
+
*/
|
|
26
|
+
export declare function renderGateReportPreview(report: GateReport, projectLabel: string): string;
|
|
27
|
+
//# sourceMappingURL=gate-report-preview.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gate-report-preview.d.ts","sourceRoot":"","sources":["../../src/cli/gate-report-preview.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,EAGV,UAAU,EAEX,MAAM,wBAAwB,CAAC;AA+LhC;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,UAAU,EAClB,YAAY,EAAE,MAAM,GACnB,MAAM,CAgDR"}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/** How each declaration state is written and coloured. */
|
|
2
|
+
const DECLARATION_LABELS = {
|
|
3
|
+
required: "must",
|
|
4
|
+
optional: "tells",
|
|
5
|
+
off: "off",
|
|
6
|
+
"not-declared": "not declared",
|
|
7
|
+
};
|
|
8
|
+
/** Escape text for HTML text nodes and attribute values. */
|
|
9
|
+
const ESCAPES = {
|
|
10
|
+
"&": "&",
|
|
11
|
+
"<": "<",
|
|
12
|
+
">": ">",
|
|
13
|
+
'"': """,
|
|
14
|
+
"'": "'",
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Escape a string for interpolation into HTML.
|
|
18
|
+
* @param value - Raw text
|
|
19
|
+
* @returns Escaped text
|
|
20
|
+
*/
|
|
21
|
+
function escapeHtml(value) {
|
|
22
|
+
return value.replace(/[&<>"']/g, character => ESCAPES[character] ?? character);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The bucket chip for one cell, or the honest refusal to classify it.
|
|
26
|
+
* @param bucket - The cell's bucket finding
|
|
27
|
+
* @returns One chip of HTML
|
|
28
|
+
*/
|
|
29
|
+
function bucketChip(bucket) {
|
|
30
|
+
if (bucket.state === "not-applicable")
|
|
31
|
+
return "";
|
|
32
|
+
if (bucket.state === "unknown") {
|
|
33
|
+
return `<span class="chip unknown" title="${escapeHtml(bucket.message)}">not checked here</span>`;
|
|
34
|
+
}
|
|
35
|
+
const titles = {
|
|
36
|
+
A: "declared, and an executor resolves the declaration",
|
|
37
|
+
B: "an executor exists, but no declaration reaches it",
|
|
38
|
+
C: "a declaration exists that nothing can execute",
|
|
39
|
+
D: "neither declared nor executed",
|
|
40
|
+
};
|
|
41
|
+
return `<span class="chip b-${bucket.value}" title="${escapeHtml(titles[bucket.value] ?? "")}">${bucket.value}</span>`;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* One matrix cell.
|
|
45
|
+
* @param cell - The (gate, moment) pair
|
|
46
|
+
* @returns A table cell
|
|
47
|
+
*/
|
|
48
|
+
function momentCell(cell) {
|
|
49
|
+
if (!cell.legal) {
|
|
50
|
+
return '<td class="m illegal" title="the registry does not permit declaring this gate here"></td>';
|
|
51
|
+
}
|
|
52
|
+
const label = DECLARATION_LABELS[cell.declaration] ?? cell.declaration;
|
|
53
|
+
const state = cell.declaration === "not-declared" ? "undeclared" : cell.declaration;
|
|
54
|
+
return `<td class="m"><span class="state ${state}">${escapeHtml(label)}</span>${bucketChip(cell.bucket)}</td>`;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Whether `package.json` has the command, said in three states.
|
|
58
|
+
* @param cell - The cell carrying the command
|
|
59
|
+
* @returns One chip of HTML
|
|
60
|
+
*/
|
|
61
|
+
function commandChip(cell) {
|
|
62
|
+
if (cell.commandExists.state === "unknown") {
|
|
63
|
+
return '<span class="chip unknown">command not checked</span>';
|
|
64
|
+
}
|
|
65
|
+
if (cell.commandExists.state === "not-applicable")
|
|
66
|
+
return "";
|
|
67
|
+
return cell.commandExists.value
|
|
68
|
+
? '<span class="chip ok">script exists</span>'
|
|
69
|
+
: '<span class="chip bad">no such script</span>';
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The first cell of a row that carries a resolved command, for the task column.
|
|
73
|
+
* @param row - One gate's row
|
|
74
|
+
* @returns The most informative cell, or undefined
|
|
75
|
+
*/
|
|
76
|
+
function representativeCell(row) {
|
|
77
|
+
return (row.moments.find(cell => cell.legal && cell.declaration !== "not-declared") ?? row.moments.find(cell => cell.legal));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Legal moments the project's own axis has no column for.
|
|
81
|
+
*
|
|
82
|
+
* There is no bare `deploy` moment in Lisa, and the deploy families only get a
|
|
83
|
+
* column when a project declares an environment. Naming them here keeps the
|
|
84
|
+
* information without inventing a column the project does not have.
|
|
85
|
+
* @param row - One gate's row
|
|
86
|
+
* @param axis - The report's moment axis
|
|
87
|
+
* @returns Human text, or an empty string
|
|
88
|
+
*/
|
|
89
|
+
function offAxisMoments(row, axis) {
|
|
90
|
+
const families = row.legalMoments.filter(moment => !axis.includes(moment) && !axis.some(one => one.startsWith(`${moment}:`)));
|
|
91
|
+
return families.length === 0
|
|
92
|
+
? ""
|
|
93
|
+
: `<div class="offaxis">also legal at ${escapeHtml(families.join(", "))}</div>`;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The cloud job a gate maps to, from the static table.
|
|
97
|
+
* @param job - The job id, or null when the table has no pair for this gate
|
|
98
|
+
* @returns One cell fragment
|
|
99
|
+
*/
|
|
100
|
+
function jobCell(job) {
|
|
101
|
+
return job === null
|
|
102
|
+
? '<span class="chip none">no job</span>'
|
|
103
|
+
: `<code>${escapeHtml(job)}</code>`;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* One gate row.
|
|
107
|
+
* @param row - The gate
|
|
108
|
+
* @param axis - The report's moment axis
|
|
109
|
+
* @returns A table row
|
|
110
|
+
*/
|
|
111
|
+
function gateRow(row, axis) {
|
|
112
|
+
const cell = representativeCell(row);
|
|
113
|
+
const task = cell?.task ?? row.defaultTask ?? "—";
|
|
114
|
+
return [
|
|
115
|
+
"<tr>",
|
|
116
|
+
`<td><span class="gid">${escapeHtml(row.id)}</span><span class="glabel">${escapeHtml(row.label)}</span>`,
|
|
117
|
+
offAxisMoments(row, axis),
|
|
118
|
+
"</td>",
|
|
119
|
+
`<td class="proves">${escapeHtml(row.summary)}</td>`,
|
|
120
|
+
`<td><code>${escapeHtml(task)}</code>${cell === undefined ? "" : commandChip(cell)}</td>`,
|
|
121
|
+
row.moments.map(momentCell).join(""),
|
|
122
|
+
`<td>${jobCell(row.qualityJob)}<div class="tier3">reads the declaration: not checkable here</div></td>`,
|
|
123
|
+
"</tr>",
|
|
124
|
+
].join("");
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The ruleset section — including the case where it was not read.
|
|
128
|
+
* @param report - The whole report
|
|
129
|
+
* @returns A section of HTML
|
|
130
|
+
*/
|
|
131
|
+
function rulesetSection(report) {
|
|
132
|
+
if (report.ruleset.state !== "verified") {
|
|
133
|
+
return `<p class="unknown-note">Branch protection was not read this run — <strong>${escapeHtml(report.ruleset.reason)}</strong>. ${escapeHtml(report.ruleset.message)} Nothing below claims a gate does or does not block a merge.</p>`;
|
|
134
|
+
}
|
|
135
|
+
const { matched, declaredNotRequired, requiredNotDeclared } = report.ruleset.value;
|
|
136
|
+
const list = (values) => values.length === 0
|
|
137
|
+
? "<li><em>none</em></li>"
|
|
138
|
+
: values
|
|
139
|
+
.map(value => `<li><code>${escapeHtml(value)}</code></li>`)
|
|
140
|
+
.join("");
|
|
141
|
+
return [
|
|
142
|
+
'<div class="cols">',
|
|
143
|
+
`<div><h3>Matched <span class="count">${matched.length}</span></h3><p>Declared <code>required</code>, and the ruleset requires it.</p><ul>${list(matched)}</ul></div>`,
|
|
144
|
+
`<div><h3>Declared, not required <span class="count">${declaredNotRequired.length}</span></h3><p>The settings file asks for it; the ruleset does not enforce it, so it blocks nothing.</p><ul>${list(declaredNotRequired)}</ul></div>`,
|
|
145
|
+
`<div class="sharp"><h3>Required, not declared <span class="count">${requiredNotDeclared.length}</span></h3><p>A merge is blocked on these, and <strong>no declaration governs them</strong>. Changing the settings file cannot turn them on, off, or into something else.</p><ul>${list(requiredNotDeclared)}</ul></div>`,
|
|
146
|
+
"</div>",
|
|
147
|
+
].join("");
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* The header counts, each with its denominator and its unknown band.
|
|
151
|
+
* @param report - The whole report
|
|
152
|
+
* @returns A section of HTML
|
|
153
|
+
*/
|
|
154
|
+
function summarySection(report) {
|
|
155
|
+
const s = report.summary;
|
|
156
|
+
const verified = s.buckets.A + s.buckets.B + s.buckets.C + s.buckets.D;
|
|
157
|
+
return [
|
|
158
|
+
'<div class="stats">',
|
|
159
|
+
`<div class="stat"><b>${s.governedBySettings}</b><span>of ${s.gateCount} properties governed by the settings file</span></div>`,
|
|
160
|
+
`<div class="stat"><b>${s.provedAnyway}</b><span>proved anyway by a command written into a script</span></div>`,
|
|
161
|
+
`<div class="stat"><b>${s.declaredOffOnly}</b><span>mentioned only to be switched off</span></div>`,
|
|
162
|
+
`<div class="stat"><b>${s.notDeclared}</b><span>never mentioned at all</span></div>`,
|
|
163
|
+
"</div>",
|
|
164
|
+
`<p class="denominator"><strong>${verified} of ${s.legalCells}</strong> gate-and-moment pairs were classified in this run — A ${s.buckets.A} · B ${s.buckets.B} · C ${s.buckets.C} · D ${s.buckets.D}. The other <strong>${s.bucketUnknown}</strong> are <span class="chip unknown">not checked here</span>: this run could not determine them, and they are counted separately rather than added to anything green.</p>`,
|
|
165
|
+
].join("");
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Render the whole page.
|
|
169
|
+
* @param report - The emitter's output
|
|
170
|
+
* @param projectLabel - How to name the project being reported on
|
|
171
|
+
* @returns A complete, self-contained HTML document
|
|
172
|
+
*/
|
|
173
|
+
export function renderGateReportPreview(report, projectLabel) {
|
|
174
|
+
const axis = report.momentAxis;
|
|
175
|
+
const head = axis
|
|
176
|
+
.map(moment => `<th class="m">${escapeHtml(moment)}</th>`)
|
|
177
|
+
.join("");
|
|
178
|
+
return `<!doctype html>
|
|
179
|
+
<html lang="en"><head><meta charset="utf-8">
|
|
180
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
181
|
+
<title>Gate report — ${escapeHtml(projectLabel)}</title>
|
|
182
|
+
<style>${STYLE}</style></head><body>
|
|
183
|
+
<div class="wrap">
|
|
184
|
+
<header>
|
|
185
|
+
<h1>What this project's checks actually prove</h1>
|
|
186
|
+
<p class="sub">Derived, not written. Every number below comes from ${escapeHtml(projectLabel)}'s own settings file, package scripts and git hooks, joined against Lisa's check registry. Nothing here is a language model's summary of those files.</p>
|
|
187
|
+
${summarySection(report)}
|
|
188
|
+
</header>
|
|
189
|
+
|
|
190
|
+
<section>
|
|
191
|
+
<h2>Every check, and what governs it</h2>
|
|
192
|
+
<p class="lede">One row per check. One column per moment this project can declare a check at — there is no bare "deploy" column, because Lisa has no such moment: the deploy families take an environment suffix and only appear once a project names one.</p>
|
|
193
|
+
<div class="legend">
|
|
194
|
+
<span><span class="state required">must</span> declared required</span>
|
|
195
|
+
<span><span class="state optional">tells</span> declared optional</span>
|
|
196
|
+
<span><span class="state off">off</span> declared off — different from never declared</span>
|
|
197
|
+
<span><span class="state undeclared">not declared</span> the settings file never mentions it here</span>
|
|
198
|
+
<span><span class="chip b-A">A</span> declared, and something reads the declaration</span>
|
|
199
|
+
<span><span class="chip b-B">B</span> something runs it anyway; the settings file has no say</span>
|
|
200
|
+
<span><span class="chip b-C">C</span> declared, but nothing can run it</span>
|
|
201
|
+
<span><span class="chip unknown">not checked here</span> this run could not determine it — NOT a pass</span>
|
|
202
|
+
</div>
|
|
203
|
+
<div class="scroll"><table>
|
|
204
|
+
<thead><tr><th>Check</th><th>What a pass proves</th><th>Command</th>${head}<th>Cloud job</th></tr></thead>
|
|
205
|
+
<tbody>${report.gates.map(row => gateRow(row, axis)).join("")}</tbody>
|
|
206
|
+
</table></div>
|
|
207
|
+
</section>
|
|
208
|
+
|
|
209
|
+
<section>
|
|
210
|
+
<h2>What a merge is actually blocked on</h2>
|
|
211
|
+
<p class="lede">The contexts the settings file implies, beside the ones the repository's branch-protection rules really require. Nothing compares these two today, so they are only ever equal by hand.</p>
|
|
212
|
+
${rulesetSection(report)}
|
|
213
|
+
</section>
|
|
214
|
+
|
|
215
|
+
<footer>
|
|
216
|
+
<p>Report format v${String(report.version)} · registry read from the running Lisa package · runner <code>${escapeHtml(report.runner.state === "verified" ? report.runner.value : "unknown")}</code>${report.declarationProblems.length === 0 ? "" : ` · <strong>${String(report.declarationProblems.length)} declaration problem(s)</strong>: ${escapeHtml(report.declarationProblems.join("; "))}`}</p>
|
|
217
|
+
<p>Not rendered yet, and deliberately: the agent on-edit scripts, the table of cloud jobs that ignore the settings file, and any filtering.</p>
|
|
218
|
+
</footer>
|
|
219
|
+
</div>
|
|
220
|
+
</body></html>`;
|
|
221
|
+
}
|
|
222
|
+
/** All styling, inlined so the file opens from disk with no server. */
|
|
223
|
+
const STYLE = `
|
|
224
|
+
:root{--bg:#0f1115;--panel:#161923;--line:#252a38;--ink:#e6e9f2;--dim:#98a0b5;
|
|
225
|
+
--ok:#3fb950;--warn:#d29922;--bad:#f85149;--unk:#6b7280;--acc:#58a6ff}
|
|
226
|
+
*{box-sizing:border-box}
|
|
227
|
+
body{margin:0;background:var(--bg);color:var(--ink);
|
|
228
|
+
font:15px/1.55 ui-sans-serif,-apple-system,"Segoe UI",Roboto,sans-serif}
|
|
229
|
+
.wrap{max-width:1500px;margin:0 auto;padding:32px 24px 72px}
|
|
230
|
+
h1{font-size:26px;margin:0 0 8px}
|
|
231
|
+
h2{font-size:19px;margin:40px 0 6px;padding-top:24px;border-top:1px solid var(--line)}
|
|
232
|
+
h3{font-size:14px;margin:0 0 4px;text-transform:uppercase;letter-spacing:.06em;color:var(--dim)}
|
|
233
|
+
.sub,.lede{color:var(--dim);margin:0 0 16px;max-width:80ch}
|
|
234
|
+
.stats{display:flex;flex-wrap:wrap;gap:12px;margin:20px 0 12px}
|
|
235
|
+
.stat{background:var(--panel);border:1px solid var(--line);border-radius:10px;
|
|
236
|
+
padding:12px 16px;min-width:170px}
|
|
237
|
+
.stat b{display:block;font-size:28px;line-height:1.1}
|
|
238
|
+
.stat span{color:var(--dim);font-size:12.5px}
|
|
239
|
+
.denominator{background:var(--panel);border:1px solid var(--line);border-left:3px solid var(--acc);
|
|
240
|
+
border-radius:8px;padding:12px 16px;margin:0;max-width:100ch}
|
|
241
|
+
.legend{display:flex;flex-wrap:wrap;gap:8px 18px;margin:14px 0;color:var(--dim);font-size:12.5px}
|
|
242
|
+
.legend>span{display:flex;align-items:center;gap:6px}
|
|
243
|
+
.scroll{overflow-x:auto;border:1px solid var(--line);border-radius:10px}
|
|
244
|
+
table{border-collapse:collapse;width:100%;font-size:13px;background:var(--panel)}
|
|
245
|
+
th,td{text-align:left;padding:9px 11px;border-bottom:1px solid var(--line);vertical-align:top}
|
|
246
|
+
th{position:sticky;top:0;background:#1b2030;font-size:11.5px;text-transform:uppercase;
|
|
247
|
+
letter-spacing:.05em;color:var(--dim);z-index:1}
|
|
248
|
+
th.m,td.m{text-align:center;white-space:nowrap}
|
|
249
|
+
td.illegal{background:repeating-linear-gradient(45deg,transparent,transparent 5px,#1b2030 5px,#1b2030 10px)}
|
|
250
|
+
tr:hover td{background:#1a1f2e}
|
|
251
|
+
.gid{display:block;font-family:ui-monospace,monospace;font-size:12px;color:var(--acc)}
|
|
252
|
+
.glabel{display:block;color:var(--dim);font-size:12px}
|
|
253
|
+
.offaxis{margin-top:4px;color:var(--unk);font-size:11px;font-style:italic}
|
|
254
|
+
.proves{color:var(--dim);max-width:34ch}
|
|
255
|
+
code{font-family:ui-monospace,monospace;font-size:12px;background:#10131c;
|
|
256
|
+
border:1px solid var(--line);border-radius:4px;padding:1px 5px}
|
|
257
|
+
.state{display:inline-block;font-size:11px;border-radius:5px;padding:2px 7px;font-weight:600}
|
|
258
|
+
.required{background:#1d3a24;color:#7ee787;border:1px solid #2ea04326}
|
|
259
|
+
.optional{background:#3a3117;color:#e3b341;border:1px solid #d2992226}
|
|
260
|
+
.off{background:#3a1d1d;color:#ff9492;border:1px solid #f8514926}
|
|
261
|
+
.undeclared{background:#1b2030;color:var(--unk);border:1px dashed #3a4157}
|
|
262
|
+
.chip{display:inline-block;margin-left:5px;font-size:10.5px;border-radius:4px;
|
|
263
|
+
padding:1px 5px;font-weight:600;vertical-align:1px}
|
|
264
|
+
.b-A{background:#123021;color:#7ee787}
|
|
265
|
+
.b-B{background:#3a2c12;color:#e3b341}
|
|
266
|
+
.b-C{background:#3a1616;color:#ff9492}
|
|
267
|
+
.b-D{background:#2a2a2a;color:#bbb}
|
|
268
|
+
.ok{background:#123021;color:#7ee787}
|
|
269
|
+
.bad{background:#3a1616;color:#ff9492}
|
|
270
|
+
.none{background:#1b2030;color:var(--unk)}
|
|
271
|
+
.unknown{background:repeating-linear-gradient(45deg,#22262f,#22262f 3px,#191d25 3px,#191d25 6px);
|
|
272
|
+
color:#9aa3b8;border:1px dashed #3f465c}
|
|
273
|
+
.tier3{margin-top:5px;font-size:11px;color:var(--unk);font-style:italic}
|
|
274
|
+
.unknown-note{background:repeating-linear-gradient(45deg,#22262f,#22262f 4px,#191d25 4px,#191d25 8px);
|
|
275
|
+
border:1px dashed #3f465c;border-radius:8px;padding:14px 16px;color:#c2c9db}
|
|
276
|
+
.cols{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:14px}
|
|
277
|
+
.cols>div{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:14px 16px}
|
|
278
|
+
.cols .sharp{border-color:#7d2b2b;background:#1e1618}
|
|
279
|
+
.cols p{color:var(--dim);font-size:12.5px;margin:0 0 8px}
|
|
280
|
+
.cols ul{margin:0;padding-left:18px}
|
|
281
|
+
.cols li{margin:3px 0;font-size:12.5px}
|
|
282
|
+
.count{float:right;color:var(--ink);font-size:13px}
|
|
283
|
+
footer{margin-top:44px;padding-top:18px;border-top:1px solid var(--line);
|
|
284
|
+
color:var(--unk);font-size:12.5px}
|
|
285
|
+
`;
|
|
286
|
+
//# sourceMappingURL=gate-report-preview.js.map
|