pi-aia-asf 0.17.8 → 0.17.9
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 +38 -0
- package/lib/code-health/index.mjs +27 -19
- package/lib/code-health/measure.d.mts +25 -0
- package/lib/code-health/measure.mjs +76 -0
- package/lib/extension/commands/inspect.ts +6 -1
- package/lib/extension/commands/verify.ts +5 -1
- package/lib/extension/health-record.ts +46 -0
- package/lib/extension/tools/health.ts +12 -12
- package/package.json +1 -1
- package/skills/aia-asf/references/06e-code-health.md +14 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.17.9] - 2026-11-10
|
|
11
|
+
|
|
12
|
+
### Fixed — the health verdict now reports what it measured, not what it planned
|
|
13
|
+
|
|
14
|
+
`asf_health` recorded `scanned` as `rows.length`, and `rows` contains every metric
|
|
15
|
+
the gate **planned** — including the ones that never ran because a tool was not
|
|
16
|
+
installed. Three things followed, all reported from the same observation:
|
|
17
|
+
|
|
18
|
+
- the ledger's `scanned 0` rule ("examined nothing, not a pass") was unreachable
|
|
19
|
+
while any metric was planned, so a run that measured nothing was recorded as a
|
|
20
|
+
nine-metric audit;
|
|
21
|
+
- the tool's own summary line contradicted the report printed directly above it —
|
|
22
|
+
`NOT healthy — 8 metric(s) skipped` followed by `9 metric(s) measured`;
|
|
23
|
+
- the honest number already existed and was thrown away: `verdict()` returns
|
|
24
|
+
`ran`, counting pass+fail and deliberately **excluding** error rows, but
|
|
25
|
+
`runHealth()` never returned it.
|
|
26
|
+
|
|
27
|
+
`runHealth()` now returns `ran` and `skipped`, and the recorded count comes from
|
|
28
|
+
them. Counting `status !== "skipped"` was the tempting fix and is wrong: it counts
|
|
29
|
+
errored rows as measured, which puts the report, the ledger and the summary line
|
|
30
|
+
back out of agreement — the exact disagreement being fixed. A metric that does not
|
|
31
|
+
apply (no persistence in the project) is neither ran nor skipped.
|
|
32
|
+
|
|
33
|
+
### Changed — every path that produces a health verdict records it
|
|
34
|
+
|
|
35
|
+
`/asf health` and the Gate 8 section of `/asf verify` printed the measurement and
|
|
36
|
+
left no trace, so a human could run the command, read HEALTHY, and leave the
|
|
37
|
+
completion gate's health gap open. Recording was the `asf_health` tool's private
|
|
38
|
+
privilege; it is now shared, with `by:` saying who produced the verdict
|
|
39
|
+
(`tool` / `command` / `verify`). The counting rules and the summary wording moved
|
|
40
|
+
to `lib/code-health/measure.mjs` — pure, dependency-free, and tested without a
|
|
41
|
+
harness — so the three callers cannot drift into contradicting each other again.
|
|
42
|
+
|
|
43
|
+
A verdict over zero metrics says so out loud: *"Nothing was actually measured — a
|
|
44
|
+
verdict over zero metrics is not a pass, and installing the missing tools is what
|
|
45
|
+
closes this properly."*
|
|
46
|
+
|
|
47
|
+
|
|
10
48
|
## [0.17.8] - 2026-11-09
|
|
11
49
|
|
|
12
50
|
### Added — the tracked-work vocabulary is the project's to widen
|
|
@@ -35,7 +35,7 @@ export async function runHealth(projectRoot, args = [], deps = {}) {
|
|
|
35
35
|
const config = await loadConfig(projectRoot, deps.readFile);
|
|
36
36
|
if (!config.enabled) {
|
|
37
37
|
const text = "Code Health Gate: disabled (enabled: false).";
|
|
38
|
-
return { text, json: { enabled: false }, exitCode: 0, healthy: true, rows: [], config };
|
|
38
|
+
return { text, json: { enabled: false }, exitCode: 0, healthy: true, rows: [], ran: 0, skipped: 0, config };
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
const files = listSourceFiles(projectRoot, config.ignore);
|
|
@@ -50,29 +50,37 @@ export async function runHealth(projectRoot, args = [], deps = {}) {
|
|
|
50
50
|
const gateBlocks = config.gate.mode === "block" && config.gate.scope !== "off";
|
|
51
51
|
const exitCode = gateBlocks && !v.healthy ? 1 : 0;
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
const text = applyBaselineFlags({ text: formatReport(rows, config), rows, root: projectRoot, flags });
|
|
54
54
|
|
|
55
|
+
const json = flags.json ? jsonReport(rows, config) : null;
|
|
56
|
+
// `ran` and `skipped` are verdict()'s own counts, and they leave here because
|
|
57
|
+
// they are the ONLY honest answer to "how much was actually measured" — see
|
|
58
|
+
// lib/code-health/measure.mjs. `rows.length` counts metrics PLANNED.
|
|
59
|
+
return { text, json, exitCode, healthy: v.healthy, rows, ran: v.ran, skipped: v.skipped, config };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* `--update-baseline` and `--diff`, factored out of `runHealth` so the gate's
|
|
64
|
+
* main path stays readable and the metric accounting stays in one place.
|
|
65
|
+
*/
|
|
66
|
+
function applyBaselineFlags({ text, rows, root, flags }) {
|
|
55
67
|
if (flags.updateBaseline) {
|
|
56
68
|
const payload = baselinePayload(rows);
|
|
57
|
-
writeFileSync(join(
|
|
58
|
-
text
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
writeFileSync(join(root, BASELINE_FILE), JSON.stringify(payload, null, 2) + "\n", "utf-8");
|
|
70
|
+
return `${text}\n\nBaseline updated: ${BASELINE_FILE}`;
|
|
71
|
+
}
|
|
72
|
+
if (!flags.diff) return text;
|
|
73
|
+
const baselinePath = join(root, BASELINE_FILE);
|
|
74
|
+
let baselineRows = [];
|
|
75
|
+
if (existsSync(baselinePath)) {
|
|
76
|
+
try {
|
|
77
|
+
const base = JSON.parse(readFileSync(baselinePath, "utf-8"));
|
|
78
|
+
baselineRows = Object.entries(base.metrics || {}).map(([key, m]) => ({ key, value: m.value, status: m.status }));
|
|
79
|
+
} catch {
|
|
80
|
+
/* unreadable baseline → no diff */
|
|
69
81
|
}
|
|
70
|
-
const changes = diffRows(rows, baselineRows);
|
|
71
|
-
text += "\n\n" + formatDiff(changes);
|
|
72
82
|
}
|
|
73
|
-
|
|
74
|
-
const json = flags.json ? jsonReport(rows, config) : null;
|
|
75
|
-
return { text, json, exitCode, healthy: v.healthy, rows, config };
|
|
83
|
+
return `${text}\n\n${formatDiff(diffRows(rows, baselineRows))}`;
|
|
76
84
|
}
|
|
77
85
|
|
|
78
86
|
export { BASELINE_FILE };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface HealthVerdictSource {
|
|
2
|
+
healthy?: boolean;
|
|
3
|
+
exitCode?: number;
|
|
4
|
+
ran?: number;
|
|
5
|
+
skipped?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface HealthRecord {
|
|
9
|
+
verdict: string;
|
|
10
|
+
scanned: number;
|
|
11
|
+
skipped: number;
|
|
12
|
+
level: string;
|
|
13
|
+
recorded: boolean;
|
|
14
|
+
error: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function measuredMetrics(res: HealthVerdictSource | null | undefined): number;
|
|
18
|
+
export function skippedMetrics(res: HealthVerdictSource | null | undefined): number;
|
|
19
|
+
export function verdictOf(res: HealthVerdictSource | null | undefined): string;
|
|
20
|
+
export function measureSummary(res: HealthVerdictSource | null | undefined, level: string): string;
|
|
21
|
+
export function healthRunEntry(
|
|
22
|
+
res: HealthVerdictSource | null | undefined,
|
|
23
|
+
level: string,
|
|
24
|
+
): { kind: "health-run"; level: string; verdict: string; scanned: number };
|
|
25
|
+
export function recordLine(rec: HealthRecord): string;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a health verdict actually measured — the pure half.
|
|
3
|
+
*
|
|
4
|
+
* `scanned` is the number three readers must agree on: the ledger's
|
|
5
|
+
* "examined nothing, not a pass" rule, the report's own `HEALTHY — N metric(s)
|
|
6
|
+
* checked, M skipped` line, and the summary line the agent reads after the
|
|
7
|
+
* report. It used to be `rows.length`, and `rows` counts every metric PLANNED —
|
|
8
|
+
* including the ones that never ran because a tool was missing. So a project
|
|
9
|
+
* where eight of nine metrics could not run was recorded as a nine-metric
|
|
10
|
+
* audit, the ledger's zero-rule became unreachable, and the tool's summary
|
|
11
|
+
* contradicted the report printed directly above it.
|
|
12
|
+
*
|
|
13
|
+
* The honest number already existed: `verdict()` returns `ran`, which counts
|
|
14
|
+
* pass+fail and deliberately EXCLUDES error rows. Counting `status !==
|
|
15
|
+
* "skipped"` instead would count errors as measured and put the three readers
|
|
16
|
+
* back out of agreement — which is the bug this file exists to prevent.
|
|
17
|
+
*
|
|
18
|
+
* Pure and dependency-free on purpose: the recording glue lives beside the
|
|
19
|
+
* ledger, and this is what the tests can pin without a harness.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Metrics actually measured. Skipped, disabled and error rows are NOT counted.
|
|
24
|
+
*
|
|
25
|
+
* Both spellings are accepted because the two objects that travel through this
|
|
26
|
+
* file use different names for the same fact: the gate's verdict says `ran`,
|
|
27
|
+
* the recorded ledger entry says `scanned`. A summary that read only one of them
|
|
28
|
+
* would print "0 metric(s) measured" over an eight-metric audit — which is the
|
|
29
|
+
* exact contradiction this module exists to prevent, so it is read from
|
|
30
|
+
* whichever field the caller has.
|
|
31
|
+
*/
|
|
32
|
+
export function measuredMetrics(res) {
|
|
33
|
+
if (Number.isFinite(res?.ran)) return Number(res.ran);
|
|
34
|
+
if (Number.isFinite(res?.scanned)) return Number(res.scanned);
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Metrics that were planned but never ran. */
|
|
39
|
+
export function skippedMetrics(res) {
|
|
40
|
+
return Number.isFinite(res?.skipped) ? Number(res.skipped) : 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** pass/fail/warn — the same reading the exit code and the report use. */
|
|
44
|
+
export function verdictOf(res) {
|
|
45
|
+
if (res?.exitCode === 1) return "fail";
|
|
46
|
+
return res?.healthy ? "pass" : "warn";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The one wording, so no caller can invent a contradictory one. */
|
|
50
|
+
export function measureSummary(res, level) {
|
|
51
|
+
const skipped = skippedMetrics(res);
|
|
52
|
+
return `${measuredMetrics(res)} metric(s) measured${skipped ? `, ${skipped} skipped` : ""}, level ${level}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The ledger entry: `scanned` is what ran, never what was planned. */
|
|
56
|
+
export function healthRunEntry(res, level) {
|
|
57
|
+
return { kind: "health-run", level, verdict: verdictOf(res), scanned: measuredMetrics(res) };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* What the reader is told after a recording.
|
|
62
|
+
*
|
|
63
|
+
* A verdict over zero metrics is spelled out as not-a-pass: the failure mode
|
|
64
|
+
* this guards against is an agent (or a human) reading a green-looking line that
|
|
65
|
+
* summarised a measurement of nothing at all.
|
|
66
|
+
*/
|
|
67
|
+
export function recordLine(rec) {
|
|
68
|
+
const recorded = rec.recorded
|
|
69
|
+
? "recorded as evidence; the completion gate's health gap is now closed."
|
|
70
|
+
: `NOT recorded (${rec.error})`;
|
|
71
|
+
const nothing = rec.scanned === 0
|
|
72
|
+
? " Nothing was actually measured — a verdict over zero metrics is not a pass, " +
|
|
73
|
+
"and installing the missing tools is what closes this properly."
|
|
74
|
+
: "";
|
|
75
|
+
return `verdict: ${rec.verdict} (${measureSummary(rec, rec.level)}) — ${recorded}${nothing}`;
|
|
76
|
+
}
|
|
@@ -40,14 +40,19 @@ export async function cmdStatus(): Promise<string> {
|
|
|
40
40
|
/** Code Health Gate: objective measurement at function / module / architecture level. */
|
|
41
41
|
export async function cmdHealth(args: string[]): Promise<string> {
|
|
42
42
|
const { runHealth } = await import("../../code-health/index.mjs");
|
|
43
|
+
const { recordHealthVerdict, recordLine } = await import("../health-record.ts");
|
|
43
44
|
const res = await runHealth(runRoot(), args.slice(1));
|
|
45
|
+
// The command records what it measured. Before this, only the asf_health TOOL
|
|
46
|
+
// recorded: a human could run /asf health, read HEALTHY, and leave the
|
|
47
|
+
// completion gate's health gap open with no trace that anything was measured.
|
|
48
|
+
const rec = recordHealthVerdict(res, { by: "command" });
|
|
44
49
|
const tail =
|
|
45
50
|
res.exitCode === 1
|
|
46
51
|
? "\n\n✗ GATE FAILED — resolve the violations before delivery."
|
|
47
52
|
: res.config?.gate?.mode === "warn" && !res.healthy
|
|
48
53
|
? "\n\n(warn mode: violations reported, gate not blocking)"
|
|
49
54
|
: "";
|
|
50
|
-
return res.text
|
|
55
|
+
return `${res.text}${tail}\n\n${recordLine(rec)}`;
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
/** "What enforces anything around browser testing?" — answered from the registry. */
|
|
@@ -55,8 +55,12 @@ async function traceabilitySection(): Promise<string[]> {
|
|
|
55
55
|
/** Gate 8 — objective convolution measurement, on the real working tree. */
|
|
56
56
|
async function healthSection(): Promise<string[]> {
|
|
57
57
|
const { runHealth } = await import("../../code-health/index.mjs");
|
|
58
|
+
const { recordHealthVerdict, recordLine } = await import("../health-record.ts");
|
|
58
59
|
const health = await runHealth(runRoot(), []);
|
|
59
|
-
|
|
60
|
+
// Same rule as the tool and the slash command: a measurement that is produced
|
|
61
|
+
// is a measurement that is recorded.
|
|
62
|
+
const rec = recordHealthVerdict(health, { by: "verify" });
|
|
63
|
+
const lines = ["\nCODE HEALTH (Gate 8 — large work):", health.text, recordLine(rec)];
|
|
60
64
|
if (health.exitCode === 1) {
|
|
61
65
|
lines.push("\n ✗ GATE NOT PASSED — resolve code-health violations before delivery.");
|
|
62
66
|
} else if (!health.healthy && health.config?.gate?.mode === "warn") {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recording a health verdict — one definition, three callers.
|
|
3
|
+
*
|
|
4
|
+
* The completion gate asks for a recorded health verdict. It used to be that
|
|
5
|
+
* only the `asf_health` tool recorded one, while `/asf health` and `/asf verify`
|
|
6
|
+
* printed the same measurement and left no trace: a human could run the command,
|
|
7
|
+
* see HEALTHY, and the gate would stay open with nothing to show for it. Either
|
|
8
|
+
* a measurement is evidence or it is not — so every path that produces the
|
|
9
|
+
* verdict records it, and `by:` says who produced it.
|
|
10
|
+
*
|
|
11
|
+
* The counting rules and the wording are in `lib/code-health/measure.mjs`
|
|
12
|
+
* (pure, tested without a harness); this file only supplies the ledger, the
|
|
13
|
+
* evidence trail and the run's scale.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { ledger } from "./gate-state.ts";
|
|
17
|
+
import { appendEvidence } from "./evidence.ts";
|
|
18
|
+
import { currentScale } from "./run.ts";
|
|
19
|
+
import { healthRunEntry, measuredMetrics, skippedMetrics } from "../code-health/measure.mjs";
|
|
20
|
+
import type { HealthVerdictSource, HealthRecord } from "../code-health/measure.d.mts";
|
|
21
|
+
|
|
22
|
+
export type { HealthVerdictSource, HealthRecord } from "../code-health/measure.d.mts";
|
|
23
|
+
export { measuredMetrics, measureSummary, recordLine } from "../code-health/measure.mjs";
|
|
24
|
+
|
|
25
|
+
const LEVELS = ["small", "standard", "major"];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Append the verdict to the ledger and the evidence trail.
|
|
29
|
+
*
|
|
30
|
+
* `level` falls back to the run's scale, so a bare `/asf health` still records
|
|
31
|
+
* at a level the gate can judge.
|
|
32
|
+
*/
|
|
33
|
+
export function recordHealthVerdict(res: HealthVerdictSource | null | undefined, opts: { by: string; level?: string }): HealthRecord {
|
|
34
|
+
const level = LEVELS.includes(String(opts?.level)) ? String(opts.level) : currentScale();
|
|
35
|
+
const entry = healthRunEntry(res, level);
|
|
36
|
+
const appended = ledger.append(entry);
|
|
37
|
+
appendEvidence({ ...entry, by: opts.by });
|
|
38
|
+
return {
|
|
39
|
+
verdict: entry.verdict,
|
|
40
|
+
scanned: measuredMetrics(res as { ran?: number }),
|
|
41
|
+
skipped: skippedMetrics(res),
|
|
42
|
+
level,
|
|
43
|
+
recorded: !!appended.ok,
|
|
44
|
+
error: appended.ok ? "" : String(appended.error),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -12,15 +12,17 @@
|
|
|
12
12
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
13
13
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
14
14
|
import { Type } from "typebox";
|
|
15
|
-
import { ledger } from "../gate-state.ts";
|
|
16
|
-
import { appendEvidence } from "../evidence.ts";
|
|
17
15
|
import { currentScale } from "../run.ts";
|
|
16
|
+
import { recordHealthVerdict, recordLine } from "../health-record.ts";
|
|
18
17
|
|
|
19
18
|
interface HealthResult {
|
|
20
19
|
text: string;
|
|
21
20
|
healthy: boolean;
|
|
22
21
|
exitCode: number;
|
|
23
22
|
rows?: unknown[];
|
|
23
|
+
/** Metrics actually measured (pass+fail). Skipped and error rows are NOT in it. */
|
|
24
|
+
ran?: number;
|
|
25
|
+
skipped?: number;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
async function runHealthGate(scope: string): Promise<HealthResult> {
|
|
@@ -33,20 +35,18 @@ async function runHealthGate(scope: string): Promise<HealthResult> {
|
|
|
33
35
|
return (await runHealth(runRoot(), args)) as HealthResult;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* A recorded verdict is the point: measuring without recording clears nothing.
|
|
40
|
+
* The counting rules and the wording live in health-record.ts so the tool, the
|
|
41
|
+
* slash command and `/asf verify` cannot drift into disagreeing with each other.
|
|
42
|
+
*/
|
|
37
43
|
async function measureAndRecord(levelIn: string, scopeIn: string) {
|
|
38
44
|
const res = await runHealthGate(scopeIn);
|
|
39
|
-
const verdict = res.exitCode === 1 ? "fail" : res.healthy ? "pass" : "warn";
|
|
40
|
-
const scanned = Array.isArray(res.rows) ? res.rows.length : 0;
|
|
41
45
|
const level: string = ["small", "standard", "major"].includes(levelIn) ? levelIn : currentScale();
|
|
42
|
-
const
|
|
43
|
-
appendEvidence({ kind: "health-run", level, verdict, scanned, by: "tool" });
|
|
44
|
-
const recorded = appended.ok
|
|
45
|
-
? "recorded as evidence; the completion gate's health gap is now closed."
|
|
46
|
-
: `NOT recorded (${appended.error})`;
|
|
46
|
+
const rec = recordHealthVerdict(res, { by: "tool", level });
|
|
47
47
|
return {
|
|
48
|
-
text: `${res.text}\n\
|
|
49
|
-
details: { verdict, scanned, level, recorded:
|
|
48
|
+
text: `${res.text}\n\n${recordLine(rec)}`,
|
|
49
|
+
details: { verdict: rec.verdict, scanned: rec.scanned, level: rec.level, recorded: rec.recorded, by: "tool", error: rec.error },
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
52
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-aia-asf",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.9",
|
|
4
4
|
"description": "Ai Applied Agentic Software Factory — codifies the full software development flow: intake, research, spec capture, adversarial analysis, planning with approval gates, test-first implementation, and release. Requires pi-vigilant, pi-smart-web-search, pi-smart-fetch, and pi-aia-browser.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -67,6 +67,20 @@ whose tool is not installed are skipped (or fail the gate when
|
|
|
67
67
|
`/asf verify` (large work) runs the gate as **Gate 8** after the M1
|
|
68
68
|
traceability matrix. Small work shows the report but never blocks.
|
|
69
69
|
|
|
70
|
+
All three paths — the `/asf health` command, `/asf verify`, and the
|
|
71
|
+
**`asf_health` tool** — record the verdict as evidence, so any of them closes the
|
|
72
|
+
completion gate's health gap. An autonomous run uses the tool: a slash command is
|
|
73
|
+
something the agent cannot type, and a gap it cannot close is a bug, not a gate.
|
|
74
|
+
|
|
75
|
+
**What `scanned` counts.** It is the number of metrics that actually ran
|
|
76
|
+
(pass/fail), never the number of metrics planned. Skipped metrics (tool not
|
|
77
|
+
installed, level off) and disabled ones (no persistence in the project) are not
|
|
78
|
+
in it, and neither are rows that errored. So `scanned: 0` means the gate examined
|
|
79
|
+
nothing — which the ledger prints as *"examined nothing, not a pass"* rather than
|
|
80
|
+
letting a green-looking line summarise a measurement of nothing at all. If every
|
|
81
|
+
metric is skipped, installing the toolchain is what closes the gap properly;
|
|
82
|
+
re-running the gate does not.
|
|
83
|
+
|
|
70
84
|
## 3. Configuration
|
|
71
85
|
|
|
72
86
|
Optional `.asf-code-health.json` at the project root. Merged over the defaults
|