diff --git a/docs/memory/pipeline/execution-skills.md b/docs/memory/pipeline/execution-skills.md index 6e32727d..d8e7b2e4 100644 --- a/docs/memory/pipeline/execution-skills.md +++ b/docs/memory/pipeline/execution-skills.md @@ -71,7 +71,7 @@ Each prefix step (the `_intake` Create-Intake Procedure, `/fab-switch`, `/git-br **PR type system**: `/git-pr` supports 7 PR types (feat, fix, refactor, docs, test, ci, chore) derived from Conventional Commits. Types are resolved via a four-step chain: explicit argument → read from `.status.yaml` → infer from fab change intake → infer from diff. The type controls the PR title prefix (`{type}: {title}`). The PR body has two layers: an agent-generated `## Summary` + optional `## Changes` (prose synthesized from `intake.md`), and a mechanically-rendered `## Meta` block. Title derivation uses intake heading when available, commit subject otherwise, regardless of type. -**Mechanical `## Meta` block via `fab pr-meta`** (rj31): As of rj31, `/git-pr` Step 3c no longer assembles the `## Meta` block from natural-language formatting prose. The entire block — the 5-column top table (`Change ID | Type | Confidence | Plan | Review`), the `**Impact**:` table + caption, the optional `**Issues**:` line, and the `**Pipeline:**` line (six fixed-order stages with ` ✓` per `done` stage and hyperlinked intake/apply labels) — is rendered deterministically by the `fab pr-meta --type [--issues ""]` subcommand, whose stdout the skill pastes verbatim (omitting the block on non-zero exit / empty stdout, matching the legacy `{has_fab} = false` path). **Element order (260625-pnao layout revision):** heading → top table → Impact table + caption → optional Issues → **Pipeline (LAST)** — Pipeline moved from before Impact to the final element, with the optional Issues line sitting just above it. Each table/paragraph is blank-line separated so GitHub renders them distinctly. The top table's first header is **`Change ID`** (was `ID`); a present id is backtick-wrapped (`` `pnao` ``), the empty-fallback `—` stays bare. `**Pipeline:**` carries the colon **inside** the bold span. **Impact is one normalized, self-labeling table** (pnao, replacing the prior multi-form rendering): a single `Impact | +/− | Net` table — the compact `+/−` is the *column header* only, while the data rows carry spaced `+A / −B` figures; the first column header is `Impact` (was `Scope`), so the table needs **no separate `**Impact**:` lead-in line**. Numeric columns are right-aligned. It carries the fixed taxonomy `raw / true / impl / tests / excluded` where `raw = true + excluded` and `true = impl + tests`. `true` is ALWAYS the post-exclude diff (the bold row, always present) — fixing the prior "total flips meaning" bug where the same label denoted the raw diff in one form and the post-exclude diff in another. The table adapts by DROPPING rows, never reshaping: the `raw` row appears only when it differs from `true` (excludes engaged), and nested `└ impl` / `└ tests` rows appear only when a tests pair is present. Below the table a `` (small-text, NOT italic) provenance caption co-locates the excludes note and the fab-kit **binary** version stamp — `excludes `fab/`, `docs/` · generated by fab-kit vX.Y.Z` (excludes clause omitted when none configured; `vdev` rendered honestly on dev builds). Emphasis stays bold-only and the caption uses `` because GitHub's Markdown sanitizer strips `style`/`class` (so row backgrounds / colored numbers are impossible) but keeps `` on its HTML allowlist. Type resolution (Step 0b), issue gathering (Step 1), and the agent-generated Summary/Changes stay skill-side; the progress token remains `✓ body — meta + summary + changes`. `fab pr-meta` is self-contained — it reads `.status.yaml`, parses `plan.md` task checkboxes, reads config (`true_impact_exclude`, `test_paths`, `project.linear_workspace`), computes impact via `internal/impact`, and resolves git/`gh` context (branch, owner/repo, merge-base) itself; the skill passes only ``, `--type`, and optional `--issues`. This moves the Meta block's determinism into Go (tested via golden-output tests), eliminating per-run drift (e.g., the dropped exclude-list backticks the prose previously produced). `gh` failure degrades to plain-text Pipeline labels; a missing merge-base drops only the Impact block — never a hard `/git-pr` failure. `/git-pr` no longer calls `fab impact` directly (the subcommand remains public for other consumers). Blob URLs use `https://github.com/{owner}/{repo}/blob/{branch}/...` to resolve against the feature branch instead of main. See [schemas.md](/pipeline/schemas.md) for the `true_impact` render-time `impl` residual and [the `fab pr-meta` CLI reference](../../../src/kit/skills/_cli-fab.md) for the full signature, output contract, and exit codes. +**Mechanical `## Meta` block via `fab pr-meta`** (rj31): As of rj31, `/git-pr` Step 3c no longer assembles the `## Meta` block from natural-language formatting prose. The entire block — the 5-column top table (`Change ID | Type | Confidence | Plan | Review`), the Impact table + caption, the optional `**Issues**:` line, and the `**Pipeline:**` line (six fixed-order stages with ` ✓` per `done` stage and hyperlinked intake/apply labels) — is rendered deterministically by the `fab pr-meta --type [--issues ""]` subcommand, whose stdout the skill pastes verbatim (omitting the block on non-zero exit / empty stdout, matching the legacy `{has_fab} = false` path). **Element order (260625-pnao layout revision):** heading → top table → Impact table + caption → optional Issues → **Pipeline (LAST)** — Pipeline moved from before Impact to the final element, with the optional Issues line sitting just above it. Each table/paragraph is blank-line separated so GitHub renders them distinctly. The top table's first header is **`Change ID`** (was `ID`); a present id is backtick-wrapped (`` `pnao` ``), the empty-fallback `—` stays bare. `**Pipeline:**` carries the colon **inside** the bold span. **Impact is one normalized, self-labeling table** (pnao, replacing the prior multi-form rendering): a single `Impact | +/− | Net` table — the compact `+/−` is the *column header* only, while the data rows carry spaced `+A / −B` figures; the first column header is `Impact` (was `Scope`), so the table needs **no separate `**Impact**:` lead-in line**. Numeric columns are right-aligned. It carries the fixed taxonomy `raw / true / impl / tests / excluded` where `raw = true + excluded` and `true = impl + tests`. `true` is ALWAYS the post-exclude diff (the bold row, always present) — fixing the prior "total flips meaning" bug where the same label denoted the raw diff in one form and the post-exclude diff in another. The table adapts by DROPPING rows, never reshaping: the `raw` row is shown whenever `true_impact_exclude` is configured (`Excluding != nil`, even when raw equals true — a configured-but-no-op exclude still earns its own row), and is omitted only when no excludes are configured (`Excluding == nil`), where `true` is definitionally identical to `raw` so no redundant duplicate row appears *(260625-pnao follow-up — supersedes the prior "shown only when it differs from true" rule per the user's "always show raw when excludes are configured" decision)*; nested `└ impl` / `└ tests` rows appear only when a tests pair is present. Below the table a `` (small-text, NOT italic) provenance caption co-locates the excludes note and the fab-kit **binary** version stamp — `excludes `fab/`, `docs/` · generated by fab-kit vX.Y.Z` (excludes clause omitted when none configured; `vdev` rendered honestly on dev builds). Emphasis stays bold-only and the caption uses `` because GitHub's Markdown sanitizer strips `style`/`class` (so row backgrounds / colored numbers are impossible) but keeps `` on its HTML allowlist. Type resolution (Step 0b), issue gathering (Step 1), and the agent-generated Summary/Changes stay skill-side; the progress token remains `✓ body — meta + summary + changes`. `fab pr-meta` is self-contained — it reads `.status.yaml`, parses `plan.md` task checkboxes, reads config (`true_impact_exclude`, `test_paths`, `project.linear_workspace`), computes impact via `internal/impact`, and resolves git/`gh` context (branch, owner/repo, merge-base) itself; the skill passes only ``, `--type`, and optional `--issues`. This moves the Meta block's determinism into Go (tested via golden-output tests), eliminating per-run drift (e.g., the dropped exclude-list backticks the prose previously produced). `gh` failure degrades to plain-text Pipeline labels; a missing merge-base drops only the Impact block — never a hard `/git-pr` failure. `/git-pr` no longer calls `fab impact` directly (the subcommand remains public for other consumers). Blob URLs use `https://github.com/{owner}/{repo}/blob/{branch}/...` to resolve against the feature branch instead of main. See [schemas.md](/pipeline/schemas.md) for the `true_impact` render-time `impl` residual and [the `fab pr-meta` CLI reference](../../../src/kit/skills/_cli-fab.md) for the full signature, output contract, and exit codes. **PR change metadata**: Change identity and linked issues are part of the mechanically-rendered `## Meta` block (see "Mechanical `## Meta` block via `fab pr-meta`" above). The Meta table's `Change ID` cell carries the 4-char change ID from `.status.yaml` (backtick-wrapped when present; bare `—` when absent); the optional `**Issues**:` line (rendered only when the skill passes a non-empty `--issues`, gathered via `fab status get-issues`) renders each ID as a Linear hyperlink `https://linear.app/{linear_workspace}/issue/{ISSUE_ID}` when `project.linear_workspace` is configured, bare comma-joined IDs otherwise, positioned below the Impact table + caption and above Pipeline (the pnao layout revision made Pipeline the last element). Missing fields show `—`. When the change cannot be resolved, `fab pr-meta` exits non-zero and the entire Meta block is omitted. diff --git a/docs/memory/pipeline/schemas.md b/docs/memory/pipeline/schemas.md index 8c554dcc..2e7e6b53 100644 --- a/docs/memory/pipeline/schemas.md +++ b/docs/memory/pipeline/schemas.md @@ -236,7 +236,7 @@ Field semantics: **Helper subcommand**: `fab impact ` is the canonical CLI for computing the block (consumed by `WriteTrueImpact`). It emits the same YAML schema (minus `computed_at_stage` — that is the caller's responsibility) on stdout, exits non-zero with an actionable stderr message on merge-base or `git diff` failure, and reads `true_impact_exclude` from `fab/project/config.yaml` to apply the same `excluding` rule. See `_cli-fab.md` for the full CLI reference. -**`fab pr-meta` subcommand** (rj31): `fab pr-meta --type [--issues ""]` renders the complete `## Meta` block of a fab-generated PR as final markdown (the 5-column top table, the `**Impact**:` table + caption, optional `**Issues**:`, and the `**Pipeline:**` line), replacing the inlined `/git-pr` Step 3c formatting prose. As of pnao (with the 260625 layout revision) the block orders as heading → top table (header `Change ID`, id backtick-wrapped, bare `—` fallback) → Impact table + caption → optional Issues → `**Pipeline:**` (LAST; colon inside the bold). The Impact block is a single normalized, self-labeling `Impact | +/− | Net` table (compact `+/−` column header, spaced `+A / −B` data figures, no separate `**Impact**:` lead-in) carrying the `raw / true / impl / tests / excluded` taxonomy (`raw = true + excluded`, `true = impl + tests`; `true` always the post-exclude diff, bold and always present) plus a `` (small-text, not italic) provenance caption stamping the **binary** version (`excludes … · generated by fab-kit vX.Y.Z`); the `raw` row drops when it equals `true`, the nested `└ impl`/`└ tests` rows appear only with a tests pair. The binary version is threaded via a pure `prmeta.Data.Version` field populated in `cmd/fab/pr_meta.go` `RunE` (not read in `Gather`, never config `fab_version`), keeping `Render` a pure function of `Data` for byte-stable golden tests. It reuses `internal/impact` (`ComputeForRepo`) for the Impact figures against an internally-resolved merge-base (HEAD vs `origin/main`, falling back to `origin/master`) rather than shelling to `fab impact`, and derives the `impl` residual at render time per the rule above. It is self-contained otherwise — reading `.status.yaml`, `plan.md` task checkboxes, and config (`true_impact_exclude`, `test_paths`, `project.linear_workspace`) directly. Non-zero exit (no fab context) or empty stdout signals `/git-pr` to omit the Meta block; `gh` failure degrades to plain-text Pipeline labels and a missing merge-base drops only the Impact block. Render logic lives in `internal/prmeta/`; see `_cli-fab.md` for the full CLI reference. +**`fab pr-meta` subcommand** (rj31): `fab pr-meta --type [--issues ""]` renders the complete `## Meta` block of a fab-generated PR as final markdown (the 5-column top table, the Impact table + caption, optional `**Issues**:`, and the `**Pipeline:**` line), replacing the inlined `/git-pr` Step 3c formatting prose. As of pnao (with the 260625 layout revision) the block orders as heading → top table (header `Change ID`, id backtick-wrapped, bare `—` fallback) → Impact table + caption → optional Issues → `**Pipeline:**` (LAST; colon inside the bold). The Impact block is a single normalized, self-labeling `Impact | +/− | Net` table (compact `+/−` column header, spaced `+A / −B` data figures, no separate `**Impact**:` lead-in) carrying the `raw / true / impl / tests / excluded` taxonomy (`raw = true + excluded`, `true = impl + tests`; `true` always the post-exclude diff, bold and always present) plus a `` (small-text, not italic) provenance caption stamping the **binary** version (`excludes … · generated by fab-kit vX.Y.Z`); the `raw` row is shown whenever `true_impact_exclude` is configured (`Excluding != nil`, even when raw equals true), and is omitted only when no excludes are configured (`Excluding == nil`) since `true` is then definitionally identical to `raw` *(260625-pnao follow-up — supersedes the prior "drops when it equals true" rule)*; the nested `└ impl`/`└ tests` rows appear only with a tests pair. The binary version is threaded via a pure `prmeta.Data.Version` field populated in `cmd/fab/pr_meta.go` `RunE` (not read in `Gather`, never config `fab_version`), keeping `Render` a pure function of `Data` for byte-stable golden tests. It reuses `internal/impact` (`ComputeForRepo`) for the Impact figures against an internally-resolved merge-base (HEAD vs `origin/main`, falling back to `origin/master`) rather than shelling to `fab impact`, and derives the `impl` residual at render time per the rule above. It is self-contained otherwise — reading `.status.yaml`, `plan.md` task checkboxes, and config (`true_impact_exclude`, `test_paths`, `project.linear_workspace`) directly. Non-zero exit (no fab context) or empty stdout signals `/git-pr` to omit the Meta block; `gh` failure degrades to plain-text Pipeline labels and a missing merge-base drops only the Impact block. Render logic lives in `internal/prmeta/`; see `_cli-fab.md` for the full CLI reference. ## `.status.yaml` Identity Fields diff --git a/docs/specs/skills/SPEC-git-pr.md b/docs/specs/skills/SPEC-git-pr.md index 16f67dc8..614661ec 100644 --- a/docs/specs/skills/SPEC-git-pr.md +++ b/docs/specs/skills/SPEC-git-pr.md @@ -110,8 +110,9 @@ Autonomously commits, pushes, and creates a draft GitHub PR. No prompts, no ques │ │ optional **Issues** + **Pipeline:** (last) as final markdown; │ │ Impact = one self-labeling normalized table (Impact | +/− | Net, │ │ no lead-in line) with the raw/true/impl/tests/excluded taxonomy -│ │ (true always = post-exclude diff), rows DROP not reshape (raw only -│ │ when ≠ true; bold true always; nested └ impl/└ tests only with a +│ │ (true always = post-exclude diff), rows DROP not reshape (raw shown +│ │ whenever excludes are configured (Excluding != nil), else collapsed +│ │ into true; bold true always; nested └ impl/└ tests only with a │ │ tests pair) + a provenance caption (excludes via actual │ │ true_impact_exclude values, never hardcoded · generated by │ │ fab-kit vX.Y.Z); @@ -143,7 +144,7 @@ Autonomously commits, pushes, and creates a draft GitHub PR. No prompts, no ques | Tool | Purpose | |------|---------| | Read | Intake (for PR title + the agent-generated `## Summary` and `## Changes` sections) | -| Bash | All git operations, gh CLI, fab status commands. Step 3c renders the body's entire `## Meta` block by calling `fab pr-meta --type --issues ""` and pasting its stdout verbatim — the subcommand is self-contained (reads `.status.yaml`, `plan.md`/`tasks.md`, `config.yaml`, computes impact via `internal/impact`, resolves git/`gh` context, stamps the running binary version) and renders, in element order, the table, optional Impact, optional `**Issues**`, and `**Pipeline:**` (last) deterministically. The Impact section is one self-labeling normalized table (`Impact \| +/− \| Net`, first-column header is `Impact` so there is no lead-in line, numeric columns right-aligned, Net kept) with the locked `raw / true / impl / tests / excluded` taxonomy (`true` always = the post-exclude diff — fixing the prior "total flips meaning" bug), rows dropping rather than reshaping (`raw` only when it differs from `true`; the bold `**true**` row always; nested `└ impl` / `└ tests` only when a `tests` pair exists, `impl = max(0, true − tests)` per component, clamp-annotated when net-negative), followed by a `` provenance caption (`excludes …` from the actual `true_impact_exclude` values · `generated by fab-kit vX.Y.Z`; dev builds render `fab-kit vdev`). A non-zero exit / empty stdout means the Meta block is omitted. `/git-pr` no longer calls `fab impact` directly. | +| Bash | All git operations, gh CLI, fab status commands. Step 3c renders the body's entire `## Meta` block by calling `fab pr-meta --type --issues ""` and pasting its stdout verbatim — the subcommand is self-contained (reads `.status.yaml`, `plan.md`/`tasks.md`, `config.yaml`, computes impact via `internal/impact`, resolves git/`gh` context, stamps the running binary version) and renders, in element order, the table, optional Impact, optional `**Issues**`, and `**Pipeline:**` (last) deterministically. The Impact section is one self-labeling normalized table (`Impact \| +/− \| Net`, first-column header is `Impact` so there is no lead-in line, numeric columns right-aligned, Net kept) with the locked `raw / true / impl / tests / excluded` taxonomy (`true` always = the post-exclude diff — fixing the prior "total flips meaning" bug), rows dropping rather than reshaping (`raw` shown whenever excludes are configured — the `Excluding` pass is present — even when its figures equal `true`; with no excludes it collapses into `true` and no duplicate row appears; the bold `**true**` row always; nested `└ impl` / `└ tests` only when a `tests` pair exists, `impl = max(0, true − tests)` per component, clamp-annotated when net-negative), followed by a `` provenance caption (`excludes …` from the actual `true_impact_exclude` values · `generated by fab-kit vX.Y.Z`; dev builds render `fab-kit vdev`). A non-zero exit / empty stdout means the Meta block is omitted. `/git-pr` no longer calls `fab impact` directly. | ### Sub-agents diff --git a/src/go/fab/internal/prmeta/prmeta.go b/src/go/fab/internal/prmeta/prmeta.go index 6beb1724..32560068 100644 --- a/src/go/fab/internal/prmeta/prmeta.go +++ b/src/go/fab/internal/prmeta/prmeta.go @@ -232,8 +232,12 @@ const ( // impact, or a +0/−0 `true` diff). // // Taxonomy (pnao): raw = true + excluded, true = impl + tests. -// - raw — every changed line, all paths (the unfiltered diff). Shown only -// when it differs from `true` (i.e. excludes engaged and changed the count). +// - raw — every changed line, all paths (the unfiltered diff). Shown +// whenever excludes are configured (Excluding != nil) — even when its +// numbers equal `true` (a configured-but-no-op exclude still earns its own +// row, so a reader always sees raw alongside true when excludes apply). +// With NO excludes (Excluding == nil), `true` is definitionally identical +// to raw, so the separate raw row is dropped — no redundant duplicate. // - true — the post-exclude diff (Excluding when present, else raw). ALWAYS // shown, bold in every cell. This is the fix for the prior "total flips // meaning" bug: `true` is unambiguously the meaningful diff. @@ -265,9 +269,12 @@ func renderImpact(d Data) string { b.WriteString(impactSepRow) b.WriteString("\n") - // raw row — only when it differs from `true` (excludes engaged and changed - // the figures). A configured-but-no-op exclude does not earn a duplicate row. - if raw != trueDiff { + // raw row — shown whenever excludes are configured (the Excluding pass is + // present), EVEN IF its numbers equal `true`: with excludes applied, a reader + // always sees raw alongside true. With NO excludes, `true` is definitionally + // identical to raw (trueDiff == raw above), so a separate raw row would be a + // redundant duplicate and is dropped. + if d.Impact.Excluding != nil { b.WriteString(impactRow("raw", raw, false, "")) } diff --git a/src/go/fab/internal/prmeta/prmeta_test.go b/src/go/fab/internal/prmeta/prmeta_test.go index c12a601d..047eba54 100644 --- a/src/go/fab/internal/prmeta/prmeta_test.go +++ b/src/go/fab/internal/prmeta/prmeta_test.go @@ -205,8 +205,8 @@ func TestRender_Impact(t *testing.T) { t.Run("excludes + tests: raw, bold true, nested impl/tests, caption", func(t *testing.T) { got := renderImpact(baseData()) - // raw=142/38 (104) ≠ true=87/38 (49) → raw row shown. - // impl = true−tests = 47/38 (9); tests = 40/0 (40). + // excludes configured (Excluding != nil) → raw row shown. + // raw=142/38 (104); true=87/38 (49); impl = true−tests = 47/38 (9); tests = 40/0 (40). want := head + "| raw | +142 / −38 | +104 |\n" + "| **true** | **+87 / −38** | **+49** |\n" + @@ -218,10 +218,10 @@ func TestRender_Impact(t *testing.T) { } }) - t.Run("no excludes + tests: raw row dropped, no-excludes caption", func(t *testing.T) { + t.Run("no excludes + tests: no duplicate raw row, no-excludes caption", func(t *testing.T) { d := baseData() d.Excludes = nil - d.Impact.Excluding = nil // no excluding pass → true = raw → raw row dropped + d.Impact.Excluding = nil // no excluding pass → true == raw → no separate raw row got := renderImpact(d) // true = raw = 142/38 (104); impl = 102/38 (64); tests = 40/0 (40). want := head + @@ -233,13 +233,34 @@ func TestRender_Impact(t *testing.T) { t.Errorf("impact =\n%q\nwant\n%q", got, want) } if strings.Contains(got, "| raw |") { - t.Errorf("expected no raw row when raw == true, got:\n%s", got) + t.Errorf("expected no duplicate raw row when Excluding == nil, got:\n%s", got) } if strings.Contains(got, "excludes") { t.Errorf("expected no excludes clause in caption, got:\n%s", got) } }) + t.Run("excludes configured but no-op (raw == true): raw row still shown", func(t *testing.T) { + // New rule (pnao follow-up): the raw row is shown whenever excludes are + // configured (Excluding != nil), EVEN IF the excluding pass touched no + // lines so raw == true. A configured-but-no-op exclude still earns its + // own raw row — the reader always sees raw alongside true when excludes + // apply. (This is distinct from the no-excludes case above, where + // Excluding == nil collapses raw into true and the row is dropped.) + d := baseData() + d.Impact.Tests = nil + // Excluding equals the raw triple: excludes configured but matched nothing. + d.Impact.Excluding = &impact.Pair{Added: 142, Deleted: 38, Net: 104} + got := renderImpact(d) + want := head + + "| raw | +142 / −38 | +104 |\n" + + "| **true** | **+142 / −38** | **+104** |\n" + + "\nexcludes `fab/`, `docs/` · generated by fab-kit v2.6.6" + if got != want { + t.Errorf("impact =\n%q\nwant\n%q", got, want) + } + }) + t.Run("excludes + no tests: raw + bold true only", func(t *testing.T) { d := baseData() d.Impact.Tests = nil diff --git a/src/kit/skills/_cli-fab.md b/src/kit/skills/_cli-fab.md index baae59bd..57d49980 100644 --- a/src/kit/skills/_cli-fab.md +++ b/src/kit/skills/_cli-fab.md @@ -467,7 +467,7 @@ Self-contained data sourcing — the command reads everything else itself: Output — the exact `## Meta` block markdown, in element order **table → Impact → optional Issues → Pipeline** (each block blank-line separated so GitHub renders them as distinct elements): - The 5-column table (`Change ID | Type | Confidence | Plan | Review`) with `—` fallbacks, the `Change ID` value backtick-wrapped when present (the bare `—` fallback is not), a ` ✓` Plan completion suffix when both task and acceptance pairs are complete, and a `✓/✗ {N} cycle{s}` Review cell. -- Impact: a single normalized markdown table whose first-column header is `Impact` (so the table self-labels — there is no `**Impact**:` lead-in line), columns `Impact | +/− | Net` (numeric columns right-aligned, Net kept on every row), followed by a `` provenance caption. The locked taxonomy is `raw / true / impl / tests / excluded` with `raw = true + excluded` and `true = impl + tests`; `true` is ALWAYS the post-exclude diff (the fix for the prior "total flips meaning" bug). The table adapts by DROPPING rows, never reshaping: the `raw` row is shown only when it differs from `true` (excludes engaged), the `**true**` row is always present and bold, and the nested `└ impl` / `└ tests` rows appear only when a `tests` pair exists (`impl` is the per-component `max(0, true − tests)` residual, Unicode minus `−`, clamp-annotated when net-negative). The caption co-locates the excludes note and the version stamp — `excludes `…` · generated by fab-kit vX.Y.Z` — with the excludes list built from the actual `true_impact_exclude` values each backtick-wrapped (the `excludes …` clause omitted when none are configured) and the binary version stamped from the running `fab` (`main.version`, threaded via `prmeta.Data.Version`; a dev build renders `fab-kit vdev` honestly). The whole block is omitted entirely on `+0/−0` `true`, missing merge-base, or impact failure. Only **bold** is used for emphasis (`` is on GitHub's HTML allowlist; the sanitizer strips row backgrounds and text color). +- Impact: a single normalized markdown table whose first-column header is `Impact` (so the table self-labels — there is no `**Impact**:` lead-in line), columns `Impact | +/− | Net` (numeric columns right-aligned, Net kept on every row), followed by a `` provenance caption. The locked taxonomy is `raw / true / impl / tests / excluded` with `raw = true + excluded` and `true = impl + tests`; `true` is ALWAYS the post-exclude diff (the fix for the prior "total flips meaning" bug). The table adapts by DROPPING rows, never reshaping: the `raw` row is shown whenever excludes are configured (the `Excluding` pass is present), even when its figures equal `true` — with NO excludes configured, `true` is definitionally identical to `raw`, so no redundant duplicate row is rendered; the `**true**` row is always present and bold, and the nested `└ impl` / `└ tests` rows appear only when a `tests` pair exists (`impl` is the per-component `max(0, true − tests)` residual, Unicode minus `−`, clamp-annotated when net-negative). The caption co-locates the excludes note and the version stamp — `excludes `…` · generated by fab-kit vX.Y.Z` — with the excludes list built from the actual `true_impact_exclude` values each backtick-wrapped (the `excludes …` clause omitted when none are configured) and the binary version stamped from the running `fab` (`main.version`, threaded via `prmeta.Data.Version`; a dev build renders `fab-kit vdev` honestly). The whole block is omitted entirely on `+0/−0` `true`, missing merge-base, or impact failure. Only **bold** is used for emphasis (`` is on GitHub's HTML allowlist; the sanitizer strips row backgrounds and text color). - `**Issues**` (only when `--issues` is non-empty): Linear-linked when `project.linear_workspace` is set, bare comma-joined IDs otherwise; positioned between Impact and Pipeline. - `**Pipeline:**` (colon inside the bold span): the six stages in fixed order with ` ✓` per `done` stage; `intake`/`apply` labels hyperlink to blob URLs when the artifact exists and owner/repo resolved. Rendered LAST in the block.