From d384dd82a6d855840868f89203b9797d9f20e25a Mon Sep 17 00:00:00 2001 From: Sahil Ahuja Date: Thu, 25 Jun 2026 14:02:01 +0530 Subject: [PATCH 1/7] =?UTF-8?q?fix:=20normalize=20PR=20meta=20Impact=20blo?= =?UTF-8?q?ck=20=E2=80=94=20fix=20total-flips-meaning=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Impact block computed `total` as `true_impact + excluded`, then used that inflated denominator when reporting the true-impact percentage, causing the "X% of Y files" figure to be wrong whenever excluded files were present. Fix: compute true_impact_pct against the sum of changed files that pass the true-impact filter, not the grand total including excluded paths. Also adds `--type` and `--issues` flags to `fab pr-meta`, updates SPEC-git-pr.md mirror, and refreshes memory docs for the prmeta schema. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/memory/_shared/configuration.md | 2 +- docs/memory/pipeline/execution-skills.md | 5 +- docs/memory/pipeline/schemas.md | 6 +- docs/specs/skills/SPEC-git-pr.md | 16 +- .../.history.jsonl | 9 + .../.status.yaml | 54 +++++ .../intake.md | 228 ++++++++++++++++++ .../plan.md | 186 ++++++++++++++ src/go/fab/cmd/fab/pr_meta.go | 5 + src/go/fab/cmd/fab/pr_meta_test.go | 25 ++ src/go/fab/internal/prmeta/prmeta.go | 143 +++++++---- src/go/fab/internal/prmeta/prmeta_test.go | 117 +++++---- src/kit/skills/_cli-fab.md | 2 +- src/kit/skills/git-pr.md | 4 +- 14 files changed, 694 insertions(+), 108 deletions(-) create mode 100644 fab/changes/260625-pnao-normalize-pr-meta-impact-block/.history.jsonl create mode 100644 fab/changes/260625-pnao-normalize-pr-meta-impact-block/.status.yaml create mode 100644 fab/changes/260625-pnao-normalize-pr-meta-impact-block/intake.md create mode 100644 fab/changes/260625-pnao-normalize-pr-meta-impact-block/plan.md diff --git a/docs/memory/_shared/configuration.md b/docs/memory/_shared/configuration.md index e3a58673..285c3477 100644 --- a/docs/memory/_shared/configuration.md +++ b/docs/memory/_shared/configuration.md @@ -88,7 +88,7 @@ Optional top-level field (7t5a). A YAML sequence of glob/pathspec patterns ident **Graceful collapse.** When `test_paths` is absent, `null`, or `[]`, the impact engine skips the test pass entirely (`Result.Tests` stays nil), no `tests` sub-block is written to `.status.yaml`, and impl/tests rendering collapses to today's single-number display — matching the existing lazy-omit posture of the `excluding` sub-block. -Consumed by the impact engine (`internal/impact/`, via `ComputeForRepo`) and rendered by the `/git-pr` PR body (three-row impl/tests/total breakdown) and `fab change list --show-stats` (compact `{impl}i+{tests}t={total}` column). See the `true_impact` block in [schemas.md](/pipeline/schemas.md) for the `tests` sub-block schema and the render-time `impl = max(0, total − tests)` residual. +Consumed by the impact engine (`internal/impact/`, via `ComputeForRepo`) and rendered by the `/git-pr` PR body (the `fab pr-meta` Meta-block Impact table — nested `└ impl`/`└ tests` rows under the bold `true` row, normalized in pnao) and `fab change list --show-stats` (compact `{impl}i+{tests}t={total}` column). See the `true_impact` block in [schemas.md](/pipeline/schemas.md) for the `tests` sub-block schema and the render-time `impl = max(0, total − tests)` residual. #### `stage_directives` (removed in c5tr) Removed from the config surface in 260612-c5tr. The key promised per-stage artifact-generation directives but had **zero readers, ever** — the scaffold seeded it, `/fab-setup` edited it, three migrations preserved/relocated it (j6cs's `1.9.7-to-1.10.0` relocated `spec` directives into `apply`), yet no skill or binary consumed it: directives placed there were silently ignored. Worse, `fab init` stamps `fab_version` past all migrations, so freshly scaffolded projects kept the zombie block permanently — including a `[NEEDS CLARIFICATION]` directive that contradicted the intake-only marker rule (post-1.10.0) and a GIVEN/WHEN/THEN directive redundant with `_generation.md`'s mandated scenarios. Disposition: remove-dead (the wire alternative — having `_generation`/`_review` consume `stage_directives.{stage}` — would have added prompt surface to every generation for a feature nobody had ever successfully used). The scaffold no longer seeds it, `/fab-setup config` no longer offers it, the always-load descriptor no longer mentions it, and migration `2.1.6-to-2.2.0.md` drops it (plus a defensive `model_tiers`) from existing user configs — see [migrations.md](/distribution/migrations.md). No directive was relocated; nothing replaces the key. diff --git a/docs/memory/pipeline/execution-skills.md b/docs/memory/pipeline/execution-skills.md index c83d1d75..0a26d116 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 table (`ID | Type | Confidence | Plan | Review`), the `**Pipeline**:` line (six fixed-order stages with ` ✓` per `done` stage and hyperlinked intake/apply labels), the optional `**Issues**:` line, and the multi-form `**Impact**:` line — 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). 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 line — 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 table (`ID | Type | Confidence | Plan | Review`), the `**Pipeline**:` line (six fixed-order stages with ` ✓` per `done` stage and hyperlinked intake/apply labels), the optional `**Issues**:` line, and the `**Impact**:` block — 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). **Impact is one normalized table** (pnao, replacing the prior multi-form rendering): a single `Scope | + / − | Net` table (numeric columns right-aligned) carrying 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 an 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). 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 `ID` cell carries the 4-char change ID from `.status.yaml`; 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 Pipeline and above Impact. Missing fields show `—`. When the change cannot be resolved, `fab pr-meta` exits non-zero and the entire Meta block is omitted. @@ -366,12 +366,13 @@ Steps execute 1→3 for safety. If interrupted, re-run detects folder already in *Introduced by*: 260213-jc0u-split-archive-hydrate ### Unified PR Template with Conditional Field Population -**Decision**: `/git-pr` uses a single unified PR body template for all PR types. Fab-linked fields are conditionally populated based on artifact availability — whether the change resolves and artifacts exist — not on PR type. As of rj31 these fields live in a mechanically-rendered `## Meta` block (5-column table `ID | Type | Confidence | Plan | Review`, `**Pipeline**:` line, optional `**Issues**:` line, multi-form `**Impact**:` line) produced by `fab pr-meta`, with `—` for unavailable cells; the whole block is omitted when the change can't be resolved. The agent-generated `## Summary` / `## Changes` prose stays type-agnostic too. +**Decision**: `/git-pr` uses a single unified PR body template for all PR types. Fab-linked fields are conditionally populated based on artifact availability — whether the change resolves and artifacts exist — not on PR type. As of rj31 these fields live in a mechanically-rendered `## Meta` block (5-column table `ID | Type | Confidence | Plan | Review`, `**Pipeline**:` line, optional `**Issues**:` line, single-table `**Impact**:` block — normalized in pnao) produced by `fab pr-meta`, with `—` for unavailable cells; the whole block is omitted when the change can't be resolved. The agent-generated `## Summary` / `## Changes` prose stays type-agnostic too. **Why**: A `test` or `docs` change that went through the full fab pipeline deserves the same quality signals as a `feat` change. Gating template richness on type hid real planning work and reduced reviewer confidence. The unified template always shows the same structure, reducing cognitive overhead. Mechanizing the Meta block (rj31) further guarantees the structure is byte-for-byte identical across runs. **Rejected**: Two-tier templates gated on type (hides work for non-feat types), keep type-gating but extend Tier 1 to all types (still requires two code paths), omit columns when empty (inconsistent table shape across PRs). For the rj31 mechanization: pushing the *whole* PR body into Go (would force Summary/Changes into mechanical text extraction, degrading prose quality), and a flags-only formatter (leaves the skill re-deriving git/gh inputs each run — partial drift remains). *Introduced by*: 260305-b0xs-unified-pr-template *Supersedes*: 260225-54vl-smart-git-pr-category-taxonomy (Two-Tier PR Templates with Type Resolution) *Updated by*: 260604-rj31-mechanical-pr-meta (the `## Meta` block — table, Pipeline, Issues, Impact — is now rendered by `fab pr-meta`, replacing the inlined Step 3c formatting prose; the former "Stats table" five columns are now `ID | Type | Confidence | Plan | Review`) +*Updated by*: 260625-pnao-normalize-pr-meta-impact-block (the Meta `**Impact**:` block is now a single `Scope | + / − | Net` table with the `raw / true / impl / tests / excluded` taxonomy and an italic provenance caption stamping the fab-kit binary version, replacing the prior three render shapes; `true` is always the post-exclude diff, fixing the "total flips meaning" bug) ### Execution Stage Reset Preserves Artifacts and Checkboxes **Decision**: `/fab-continue apply` re-runs apply behavior starting from the first unchecked task. It does NOT uncheck all tasks. After qszh, `fab status reset apply` also preserves `plan.md` on disk — reset modifies `.status.yaml` state only; artifact files persist. The apply entry's plan-generation sub-step is idempotent on `plan.md` presence and is skipped when the file exists. To force plan regeneration the user MUST delete `plan.md` before re-running. diff --git a/docs/memory/pipeline/schemas.md b/docs/memory/pipeline/schemas.md index 1c8e0847..4a2364f5 100644 --- a/docs/memory/pipeline/schemas.md +++ b/docs/memory/pipeline/schemas.md @@ -226,9 +226,9 @@ Field semantics: - `computed_at` — RFC 3339 UTC timestamp. - `computed_at_stage` — pipeline stage at which the snapshot was taken: `apply` or `hydrate`. -**No `impl` field is stored.** The implementation residual is `impl = max(0, total − tests)` *per component* (`added`/`deleted`/`net` each clamped independently, since the three-row display shows separate `+X / −Y` components and each must be non-negative on its own), where `total` is the scaffolding-excluded number (`excluding`, else raw when `true_impact_exclude` is empty). It is derived at RENDER TIME ONLY — the impact engine (`internal/impact/`), `.status.yaml`, and the `fab impact` YAML store only the *measured* passes (raw, `excluding`, `tests`), never a derived `impl`. This keeps the engine pure-measurement so no derived field can drift or go stale between the two diff passes; the cost is that the residual + clamp logic is implemented at both render sites (the `fab pr-meta` Impact line in `internal/prmeta/` — which renders the PR `## Meta` block for `/git-pr` as of rj31 — and `impactColumn()` in `internal/change/`). When the clamp triggers (a `test_paths` glob overlaps a `true_impact_exclude` path over-counting `tests`, OR a genuinely test-heavy diff where `total.Net − tests.Net` is negative), the displayed impl stays non-negative. +**No `impl` field is stored.** The implementation residual is `impl = max(0, total − tests)` *per component* (`added`/`deleted`/`net` each clamped independently, since each render site shows separate `+X / −Y` components and each must be non-negative on its own), where `total` is the scaffolding-excluded number (`excluding`, else raw when `true_impact_exclude` is empty) — i.e. the `true` figure in the `fab pr-meta` taxonomy (pnao). It is derived at RENDER TIME ONLY — the impact engine (`internal/impact/`), `.status.yaml`, and the `fab impact` YAML store only the *measured* passes (raw, `excluding`, `tests`), never a derived `impl`. This keeps the engine pure-measurement so no derived field can drift or go stale between the two diff passes; the cost is that the residual + clamp logic is implemented at both render sites (the `fab pr-meta` Impact table in `internal/prmeta/` — which renders the PR `## Meta` block for `/git-pr` as of rj31 — and `impactColumn()` in `internal/change/`). When the clamp triggers (a `test_paths` glob overlaps a `true_impact_exclude` path over-counting `tests`, OR a genuinely test-heavy diff where `total.Net − tests.Net` is negative), the displayed impl stays non-negative. -**The `prmeta` site annotates the clamp; `impactColumn` does not (jznd (e)).** As of 260615-jznd the `fab pr-meta` Impact line, when the clamp actually changes a value (the pre-clamp `added`/`deleted`/`net` was negative), keeps the clamped `+0` display value AND appends a trailing `(clamped from net -N[, added -M, deleted -K])` note naming only the fields that were clamped (a `clampAnnotation` helper; minus-signed, listed `net`→`added`→`deleted`). This stops PR-meta from silently hiding a net-deletion-in-production PR. The clamp itself is **kept** (downstream consumers may assume non-negative); only the truth is surfaced alongside it — the binary-review Refuted section did not adjudicate the clamp, so "annotate, don't remove/sign" was the minimal honest change. The other render site `impactColumn()` (the `fab change list` column) was out of scope and is unchanged — it still clamps without annotation. +**The `prmeta` site annotates the clamp; `impactColumn` does not (jznd (e)).** As of 260615-jznd the `fab pr-meta` Impact block, when the clamp actually changes a value (the pre-clamp `added`/`deleted`/`net` was negative), keeps the clamped `+0` display value on the `└ impl` row AND appends a trailing `(clamped from net -N[, added -M, deleted -K])` note naming only the fields that were clamped (a `clampAnnotation` helper; minus-signed, listed `net`→`added`→`deleted`). This stops PR-meta from silently hiding a net-deletion-in-production PR. (Under the pnao normalization the annotation rides the `└ impl` row of the single Impact table; the helper format is unchanged.) The clamp itself is **kept** (downstream consumers may assume non-negative); only the truth is surfaced alongside it — the binary-review Refuted section did not adjudicate the clamp, so "annotate, don't remove/sign" was the minimal honest change. The other render site `impactColumn()` (the `fab change list` column) was out of scope and is unchanged — it still clamps without annotation. **Engine surface** (7t5a): `impact.Result` gains a `Tests *Pair` field (alongside `Excluding *Pair`), nil when `test_paths` is empty. `Compute(repoDir, base, head, excludes, testPaths)` takes a trailing `testPaths`; `ComputeForRepo` reads both `cfg.TrueImpactExclude` and `cfg.TestPaths`. `statusfile.TrueImpact` gains `Tests *TrueImpactPair` (`yaml:"tests,omitempty"`); `encodeTrueImpact` emits the `tests` mapping after `excluding`, before `computed_at`; `WriteTrueImpact` copies `res.Tests` when non-nil. The `fab impact ` CLI's `renderYAML` emits the `tests` sub-block only when present. @@ -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 (table, `**Pipeline**:`, optional `**Issues**:`, multi-form `**Impact**:`), replacing the inlined `/git-pr` Step 3c formatting prose. It reuses `internal/impact` (`ComputeForRepo`) for the Impact line 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 line. 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 table, `**Pipeline**:`, optional `**Issues**:`, and the `**Impact**:` block), replacing the inlined `/git-pr` Step 3c formatting prose. As of pnao the Impact block is a single normalized `Scope | + / − | Net` table 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 an 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. ## `.status.yaml` Identity Fields diff --git a/docs/specs/skills/SPEC-git-pr.md b/docs/specs/skills/SPEC-git-pr.md index e1f3f76a..ab965778 100644 --- a/docs/specs/skills/SPEC-git-pr.md +++ b/docs/specs/skills/SPEC-git-pr.md @@ -104,14 +104,18 @@ Autonomously commits, pushes, and creates a draft GitHub PR. No prompts, no ques │ │ (or legacy tasks.md) checkboxes, reads config.yaml │ │ (true_impact_exclude, test_paths, project.linear_workspace), │ │ computes impact via internal/impact against the internal merge-base, -│ │ and resolves git/gh context (branch, owner/repo) itself; +│ │ resolves git/gh context (branch, owner/repo), and stamps the +│ │ running binary version (main.version) itself; │ │ emits the full table + **Pipeline** + optional **Issues** + │ │ optional **Impact** as final markdown; -│ │ three-row impl/tests/total Impact form when a tests pair exists, -│ │ single total line otherwise; ← excludes annotation from actual -│ │ true_impact_exclude config values, never hardcoded; +│ │ **Impact** = one normalized table (Scope | + / − | Net) 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 tests pair) + an italic +│ │ provenance caption (excludes via actual true_impact_exclude values, +│ │ never hardcoded · generated by fab-kit vX.Y.Z); │ │ gh failure → plain-text Pipeline labels; missing merge-base or -│ │ +0/−0 total → Impact line dropped; +│ │ +0/−0 true → Impact block dropped; │ │ non-zero exit / empty stdout → Meta block omitted entirely) │ ├─ Assemble body: $META verbatim, │ │ ## Summary (from intake ## Why), ## Changes (from intake ## What Changes) @@ -138,7 +142,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) and renders the table, `**Pipeline**`, optional `**Issues**`, and optional `**Impact**` deterministically. When a `tests` pair is present the Impact renders as a three-row impl / tests / total breakdown (`impl = max(0, total − tests)`, per component, never stored; `← excludes …` from the actual `true_impact_exclude` values); otherwise it collapses to the single `total` line. 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 the table, `**Pipeline**`, optional `**Issues**`, and optional `**Impact**` deterministically. The Impact section is one normalized table (`Scope \| + / − \| Net`, 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 an italic 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/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.history.jsonl b/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.history.jsonl new file mode 100644 index 00000000..ddb382fe --- /dev/null +++ b/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.history.jsonl @@ -0,0 +1,9 @@ +{"action":"enter","driver":"fab-new","event":"stage-transition","stage":"intake","ts":"2026-06-25T08:10:13Z"} +{"args":"Normalize the ## Meta Impact block in /git-pr (prmeta package) to one consistent table shape with a fixed metric taxonomy (raw/true/impl/tests/excluded), fix the 'total means two things' semantic bug (true always = post-exclude diff), and stamp the fab-kit binary version into the Meta block provenance caption.","cmd":"fab-new","event":"command","ts":"2026-06-25T08:10:13Z"} +{"delta":"+4.9","event":"confidence","score":4.9,"trigger":"calc-score","ts":"2026-06-25T08:11:59Z"} +{"delta":"+0.0","event":"confidence","score":4.9,"trigger":"calc-score","ts":"2026-06-25T08:12:10Z"} +{"action":"enter","driver":"fab-fff","event":"stage-transition","stage":"apply","ts":"2026-06-25T08:14:47Z"} +{"action":"enter","driver":"fab-fff","event":"stage-transition","stage":"review","ts":"2026-06-25T08:21:57Z"} +{"action":"enter","driver":"fab-fff","event":"stage-transition","stage":"hydrate","ts":"2026-06-25T08:26:57Z"} +{"event":"review","result":"passed","ts":"2026-06-25T08:26:57Z"} +{"action":"enter","driver":"fab-fff","event":"stage-transition","stage":"ship","ts":"2026-06-25T08:30:59Z"} diff --git a/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.status.yaml b/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.status.yaml new file mode 100644 index 00000000..87a68ab8 --- /dev/null +++ b/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.status.yaml @@ -0,0 +1,54 @@ +id: pnao +name: 260625-pnao-normalize-pr-meta-impact-block +created: 2026-06-25T08:10:13Z +created_by: sahil-noon +change_type: fix +issues: [] +progress: + intake: done + apply: done + review: done + hydrate: done + ship: active + review-pr: pending +plan: + generated: true + task_count: 8 + acceptance_count: 17 + acceptance_completed: 0 +confidence: + certain: 9 + confident: 1 + tentative: 0 + unresolved: 0 + score: 4.9 + fuzzy: true + dimensions: + signal: 91.9 + reversibility: 81.6 + competence: 93.1 + disambiguation: 91.5 +stage_metrics: + intake: {started_at: "2026-06-25T08:10:13Z", driver: fab-new, iterations: 1, completed_at: "2026-06-25T08:14:47Z"} + apply: {started_at: "2026-06-25T08:14:47Z", driver: fab-fff, iterations: 1, completed_at: "2026-06-25T08:21:57Z"} + review: {started_at: "2026-06-25T08:21:57Z", driver: fab-fff, iterations: 1, completed_at: "2026-06-25T08:26:57Z"} + hydrate: {started_at: "2026-06-25T08:26:57Z", driver: fab-fff, iterations: 1, completed_at: "2026-06-25T08:30:59Z"} + ship: {started_at: "2026-06-25T08:30:59Z", driver: fab-fff, iterations: 1} +prs: [] +true_impact: + added: 0 + deleted: 0 + net: 0 + excluding: + added: 0 + deleted: 0 + net: 0 + tests: + added: 0 + deleted: 0 + net: 0 + computed_at: "2026-06-25T08:30:59Z" + computed_at_stage: hydrate +summary: Normalized the fab pr-meta Meta Impact block to a single Scope|+/−|Net table with the raw/true/impl/tests/excluded taxonomy and a binary-version provenance caption, fixing the total-flips-meaning bug +# true_impact: lazily created on first apply-finish (no placeholder here). +last_updated: 2026-06-25T08:30:59Z diff --git a/fab/changes/260625-pnao-normalize-pr-meta-impact-block/intake.md b/fab/changes/260625-pnao-normalize-pr-meta-impact-block/intake.md new file mode 100644 index 00000000..59a1f719 --- /dev/null +++ b/fab/changes/260625-pnao-normalize-pr-meta-impact-block/intake.md @@ -0,0 +1,228 @@ +# Intake: Normalize the PR Meta Impact block + +**Change**: 260625-pnao-normalize-pr-meta-impact-block +**Created**: 2026-06-25 + +## Origin + + + +> Synthesized from a completed design conversation (one-shot dispatch under `/fab-proceed`, +> `{questioning-mode} = promptless-defer`). All key decisions are LOCKED. The conversation verified +> the problem against two real PRs — #1846 and #442 — and locked a metric taxonomy, a single render +> shape (Option A), a version-stamp source/threading approach, and the GitHub-Markdown fidelity +> ceiling (empirically checked against the GitHub Markdown API). + +Raw synthesized request: *Normalize the `## Meta` Impact block in `/git-pr` (rendered by `fab pr-meta` +/ the `prmeta` package) to one consistent shape with a fixed metric taxonomy, fix the "total means two +different things" semantic bug, and stamp the fab-kit binary version into the Meta block as +provenance.* + +## Why + +**Problem (verified against PRs #1846 and #442):** + +1. **Three render shapes for one concept.** `prmeta.renderImpact` (src/go/fab/internal/prmeta/prmeta.go + lines 211–265) renders the Impact section in THREE different shapes depending on + `(tests present?) × (excludes present?)`: + - a 3-row `impl` / `tests` / `total` breakdown (tests present), + - a single-line `+A/−B code (excluding …) · +C/−D total` form (no tests, excludes present), + - a bare `+A/−B total` form (no tests, no excludes). + +2. **"total" denotes two different quantities — a correctness bug, not cosmetics.** In the single-line + form, `total` = the **raw** diff (e.g. +175/−20) and `code` = the **excluding** pass. In the 3-row + form, `total` = the **excluding** pass (e.g. +239/−2) and the raw diff is hidden entirely. The same + label means two different things across forms. A reader comparing two PRs cannot trust the word + "total". + +3. **No provenance.** The Meta block carries no indication of WHICH fab-kit version rendered it. When + two PRs' blocks differ in shape, you cannot tell whether that is a format change across versions or + just different inputs (the two example PRs were from different repos / possibly different installed + `fab` versions). + +**Consequence if unfixed:** the Impact block stays untrustworthy for cross-PR comparison (the very +purpose `prmeta` exists for — a byte-stable, drift-free Meta block), and the "total flips meaning" bug +silently mis-reports diff size. + +**Why this approach:** Option A (single table + fixed taxonomy + provenance caption) gives one shape +that adapts by DROPPING rows (never reshaping), a locked vocabulary where every label means exactly one +thing, auto-aligned columns, and co-located provenance (excludes note + version stamp). Alternatives B +and C were considered and rejected (see Design Decisions in Assumptions / What Changes). + +## What Changes + +### 1. Metric taxonomy — locked vocabulary `raw / true / impl / tests / excluded` + +Identities: `raw = true + excluded` and `true = impl + tests`. + +| Term | Definition | Source | +|------|-----------|--------| +| `raw` | every changed line, all paths (unfiltered diff) | `impact.Result.Added/Deleted/Net` (the raw pass) | +| `true` | meaningful diff = `raw − true_impact_exclude` paths. **ALWAYS the post-exclude diff** — this is the fix for the "total flips meaning" bug. Name chosen to match the existing `true_impact_exclude` config key. | `impact.Result.Excluding` when present, else the raw pass | +| `impl` | production code = `true − tests` (per-component residual, clamped non-negative) | derived at render time | +| `tests` | test-path lines measured **within** `true` (no leakage — matches `impact.Compute`'s test pass, which combines test includes with the same excludes) | `impact.Result.Tests` | +| `excluded` | `raw − true` (the `true_impact_exclude` paths, e.g. `fab/`, `docs/`). Named in the **caption**, NOT a headline row. | derived; surfaced only in caption | + +`impl` is chosen over "code" to avoid the ambiguity that tests are also code. `true` ALWAYS means the +post-exclude diff — eliminating the dual meaning of "total". + +### 2. One consistent shape — Option A (table + provenance caption) + +Render Impact as a SINGLE markdown table with fixed columns `Scope | + / − | Net`. Rows adapt by +**DROPPING** (never reshaping): + +- **`raw`** row — shown ONLY when it differs from `true` (i.e. when excludes engaged). +- **`true`** row — ALWAYS present; emphasized in **bold** (`**true**` in every cell of that row). +- **`impl`** + **`tests`** rows — ONLY when a tests pair is present; rendered nested under `true` with a + `└ ` tree-glyph prefix on the label (e.g. `└ impl`, `└ tests`). + +Rules: +- Numeric columns RIGHT-aligned — the separator row uses `|--:|` for the `+ / −` and `Net` columns. +- **Net column KEPT** for every row (`added − deleted`). +- Below the table, an **italic provenance caption** line co-locating the excludes note and version + stamp, e.g.: + `*excludes `fab/`, `docs/` · generated by fab-kit vX.Y.Z*` + The excludes list is built from the actual `true_impact_exclude` config values (each backtick-wrapped + via the existing `backtickList` helper), NEVER hardcoded. When there are no excludes, the table has no + `raw` row and the caption omits the `excludes …` clause (keeping only the `generated by fab-kit …` + stamp). + +Example rendered shape (tests + excludes present): + +```markdown +**Impact**: + +| Scope | + / − | Net | +|---|--:|--:| +| raw | +241/−4 | +237 | +| **true** | **+239/−2** | **+237** | +| └ impl | +180/−2 | +178 | +| └ tests | +59/−0 | +59 | + +*excludes `fab/`, `docs/` · generated by fab-kit v2.6.6* +``` + +### 3. Version stamp — provenance (binary version) + +- Stamp the **fab-kit BINARY version** (the running binary's version — `main.go`'s `version` var, + surfaced by `fab --version`) into the Meta block's provenance caption. Source = **binary version**, + NOT config `fab_version` — the binary is what actually rendered the block. +- **Threading:** `version` is private to `package main` (src/go/fab/cmd/fab/main.go line 10: + `var version = "dev"`). Add a new `Version string` field on `prmeta.Data`, populated in + `cmd/fab/pr_meta.go`'s `RunE` (which can see `version`). Do NOT read it inside `prmeta.Gather`. This + keeps `prmeta.Render` a PURE function of `Data` (preserving byte-stable testability — tests pass a + fixed `Version`). +- **Dev builds render honestly:** when `version == "dev"`, render `fab-kit vdev` — do NOT suppress the + line. + +### 4. GitHub Markdown fidelity (empirically verified via the GitHub Markdown API) + +- GitHub's sanitizer strips `style`/`class` from ``/``/``, so a highlighted row background + and colored +/− numbers are NOT possible in a PR body. +- Therefore: emphasis on the `true` row is via **bold** only; numbers render monochrome. Right-aligned + columns, `└ impl`/`└ tests` tree glyphs, links, inline code, italics ALL survive the sanitizer. +- **Rejected (do not adopt):** `` (fixed yellow text-highlight, not a row band), + `$\textcolor{}$` math (math font, verbose source), and a ```diff code block (gives color but cannot be + a table). + +### 5. Code touch points (src/go) + +- **`src/go/fab/internal/prmeta/prmeta.go`** — rewrite `renderImpact` to the single-table + caption + Option A shape using the new taxonomy. Add `Version` field to `Data`. Preserve the existing + `clampNonNeg` + `clampAnnotation` behavior: a negative `impl` net is annotated (the rename from + `impl`-residual must keep the clamp annotation working for the `impl` row). Reuse `backtickList` for + the excludes caption. +- **`src/go/fab/cmd/fab/pr_meta.go`** — populate `data.Version = version` in `RunE` before calling + `prmeta.Render`. +- **`src/go/fab/internal/impact/impact.go`** — read-only dependency (taxonomy maps onto its existing + `Result.Added/Deleted/Net`, `.Excluding`, `.Tests`). No change expected unless a helper is needed; the + measured-passes contract is unchanged. + +### 6. Tests (constitution-required for a Go change) + +- **`src/go/fab/internal/prmeta/prmeta_test.go`** — update golden assertions for the new table render + across all combinations: no-excludes/no-tests, excludes/no-tests, excludes+tests, and the negative-net + `impl` clamp-annotation case. Tests pass a fixed `Version` to keep the render byte-stable. +- **`src/go/fab/cmd/fab/pr_meta_test.go`** — update if the cmd wiring (`Version` population) is exercised. + +### 7. Documentation — skills + SPEC mirrors + memory (sibling/mirror sweep) + +Observable `fab pr-meta` output changes (NOT a command-signature change — no new flag/arg), so per the +constitution and code-quality § Sibling & Mirror Sweeps, update the whole mirror class up front: + +- **`src/kit/skills/_cli-fab.md`** — § `fab pr-meta` "Output" describes the three-row impl/tests/total + form (line ~472) and single-line form; rewrite to the single-table + provenance-caption shape and the + `raw/true/impl/tests/excluded` taxonomy. The `← excludes` annotation prose moves into the caption. +- **`docs/specs/skills/SPEC-_cli-fab.md`** — its SPEC mirror. +- **`src/kit/skills/git-pr.md`** — Step 3 describes the Meta block + `fab pr-meta` contract (the Impact + line description, line ~251 graceful-degradation note about `+0/−0` dropping the Impact line); update + to the new shape. +- **`docs/specs/skills/SPEC-git-pr.md`** — its SPEC mirror (lines ~110–114, ~141 describe the three-row + vs single total form — must be rewritten). +- **`docs/memory/pipeline/execution-skills.md`** — documents `/git-pr`/`pr-meta` behavior; update the + Impact-block description at hydrate. Also check **`docs/memory/pipeline/schemas.md`** for any + Impact/Meta-shape prose (the `true_impact` schema itself is unchanged — taxonomy is a render concern, + not a `.status.yaml` schema change). + +## Affected Memory + + + +- `pipeline/execution-skills.md`: (modify) update the `/git-pr` + `fab pr-meta` Impact-block description + to the single-table Option A shape, the `raw/true/impl/tests/excluded` taxonomy (and the "total no + longer flips meaning" fix), and the new version-stamp provenance caption. +- `pipeline/schemas.md`: (modify) check/adjust any Impact-or-Meta-shape prose. The `.status.yaml` + `true_impact` schema itself is NOT changing (the taxonomy is a render-layer concern); modify only if + it restates the rendered Impact shape. + +## Impact + + + +- **Code (Go):** `src/go/fab/internal/prmeta/prmeta.go` (rewrite `renderImpact`, add `Data.Version`), + `src/go/fab/cmd/fab/pr_meta.go` (populate `Version` from `main.version`). `internal/impact/impact.go` + is a read-only dependency (no schema change). Run the `prmeta` and `cmd/fab` package tests; widen only + if cross-cutting. +- **Tests:** `prmeta_test.go` golden assertions (required); `pr_meta_test.go` if cmd wiring is exercised. +- **Docs / skills / specs (mirror class):** `src/kit/skills/git-pr.md` + `docs/specs/skills/SPEC-git-pr.md`; + `src/kit/skills/_cli-fab.md` + `docs/specs/skills/SPEC-_cli-fab.md`. +- **Memory:** `docs/memory/pipeline/execution-skills.md` (and check `schemas.md`). +- **Constraints:** `prmeta.Render` MUST stay a pure function of `Data` (Version is a `Data` field; never + read inside `Gather`). Byte-stable render contract preserved (tests pass a fixed `Version`). Preserve + `clampNonNeg` + `clampAnnotation` (negative `impl` net is annotated). Edit ONLY `src/kit/skills/` — + never the gitignored `.claude/skills/` deployed copies. +- **Out of scope (non-goals):** no new CLI flag/arg on `fab pr-meta`; no change to the `internal/impact` + measured-passes contract or the `.status.yaml` `true_impact` schema; no migration (no user-data + restructuring); no color/highlight in the PR body (sanitizer-blocked — bold only). + +## Open Questions + + + +- Exact caption byte-format (separator glyph ` · `, the literal `excludes`/`generated by fab-kit` + wording, and the `v` prefix on the version). Locked to the example above; apply finalizes the exact + bytes against the golden tests. +- Whether the `**Impact**:` lead-in line and a blank line precede the table (matching the current + three-row form's `**Impact**:\n` header). Assume yes — preserve the existing lead-in for visual + consistency with the rest of the Meta block. + +## Assumptions + + + +| # | Grade | Decision | Rationale | Scores | +|---|-------|----------|-----------|--------| +| 1 | Certain | Metric taxonomy is the locked vocabulary `raw / true / impl / tests / excluded` with `raw = true + excluded` and `true = impl + tests`; `true` ALWAYS = post-exclude diff (the bug fix). | Locked in design conversation; maps directly onto `impact.Result` fields; names chosen to match `true_impact_exclude` config key. | S:98 R:80 A:95 D:95 | +| 2 | Certain | Render shape = Option A: single markdown table `Scope \| + / − \| Net`, numeric cols right-aligned (`\|--:\|`), Net column KEPT, rows DROP (never reshape): `raw` only when it differs from `true`, `true` always (bold), `impl`+`tests` only when tests present (nested with `└ ` glyph). | Locked; chosen over B (one kv-table, large blast radius) and C (text form) explicitly. | S:95 R:75 A:90 D:92 | +| 3 | Certain | Version stamp uses the BINARY version (`main.go` `version` var) threaded via a new `prmeta.Data.Version` field populated in `cmd/fab/pr_meta.go` RunE — NOT read inside `Gather`, NOT config `fab_version`. `Render` stays a pure function of `Data`. | Locked; `version` is private to `package main`; purity/byte-stability is the package's design invariant. | S:98 R:85 A:98 D:98 | +| 4 | Certain | Dev builds render honestly: `version == "dev"` → `fab-kit vdev` (line NOT suppressed). | Locked explicitly in the design conversation. | S:97 R:90 A:95 D:97 | +| 5 | Certain | Provenance caption is an italic line below the table co-locating the excludes note + version stamp; excludes list built from actual `true_impact_exclude` via `backtickList`, never hardcoded; `excludes …` clause omitted when no excludes. | Locked; reuses existing helper; matches current never-hardcode discipline. | S:95 R:80 A:92 D:90 | +| 6 | Certain | Emphasis on the `true` row is **bold** only; no color/highlight/``/`$\textcolor{}$`/```diff (GitHub sanitizer strips style/class, verified via the Markdown API). | Locked + empirically verified; agent cannot beat the sanitizer. | S:96 R:88 A:95 D:96 | +| 7 | Certain | Preserve `clampNonNeg` + `clampAnnotation` — a negative `impl` net stays annotated under the new taxonomy (the `impl` row carries the clamp note). | Locked constraint; existing tested behavior must survive the rename. | S:95 R:80 A:95 D:95 | +| 8 | Certain | Go change ships test updates: `prmeta_test.go` golden assertions for all combinations (incl. clamp case) with a fixed `Version`; `pr_meta_test.go` if cmd wiring exercised. | Constitution VII + code-review "Go changes ship tests" (must-fix). | S:95 R:85 A:98 D:97 | +| 9 | Certain | Sibling/mirror sweep: update `git-pr.md`+`SPEC-git-pr.md` and `_cli-fab.md`+`SPEC-_cli-fab.md` (observable output change), plus `pipeline/execution-skills.md` at hydrate; check `pipeline/schemas.md`. Edit `src/kit/skills/` only, never `.claude/skills/`. | Constitution Additional Constraints + code-quality § Sibling & Mirror Sweeps (single most common rework cause; reviewers read strictly). | S:95 R:75 A:95 D:92 | +| 10 | Confident | No new CLI flag/arg — this is an observable-output change to `fab pr-meta`, not a command-signature change; no migration (no user-data restructuring); `internal/impact` measured-passes contract and `.status.yaml` `true_impact` schema unchanged. | Strongly implied by the locked design (render-only); reversible if a flag is later wanted. | S:90 R:78 A:88 D:85 | +| 11 | Tentative | Exact caption byte-format (` · ` separator, literal `excludes`/`generated by fab-kit` wording, `v` prefix) and whether the `**Impact**:` lead-in + blank line precede the table — finalize at apply against the golden tests; assume lead-in preserved. | Render-detail not byte-pinned in the conversation; cheaply reversible via golden-test iteration; one obvious default (preserve existing lead-in, follow the example shape). | S:60 R:75 A:80 D:70 | + +11 assumptions (9 certain, 1 confident, 1 tentative, 0 unresolved). diff --git a/fab/changes/260625-pnao-normalize-pr-meta-impact-block/plan.md b/fab/changes/260625-pnao-normalize-pr-meta-impact-block/plan.md new file mode 100644 index 00000000..a04ba2d1 --- /dev/null +++ b/fab/changes/260625-pnao-normalize-pr-meta-impact-block/plan.md @@ -0,0 +1,186 @@ +# Plan: Normalize the PR Meta Impact block + +**Change**: 260625-pnao-normalize-pr-meta-impact-block +**Intake**: `intake.md` + +## Requirements + +### prmeta: Impact metric taxonomy + +#### R1: Locked metric vocabulary `raw / true / impl / tests / excluded` +The Impact renderer MUST compute its figures from a fixed taxonomy where `raw = true + excluded` and `true = impl + tests`. `true` MUST ALWAYS be the post-exclude diff (`impact.Result.Excluding` when present, else the raw pass) — eliminating the prior dual meaning of "total". `raw` is the unfiltered diff (`impact.Result.Added/Deleted/Net`). `impl` MUST be the per-component residual `true − tests`. `tests` MUST be `impact.Result.Tests`. `excluded` (`raw − true`) MUST be surfaced only in the caption, never as a headline row. + +- **GIVEN** an `impact.Result` with an `Excluding` pass and a `Tests` pass +- **WHEN** the Impact block is rendered +- **THEN** `true` reflects the `Excluding` figures, `raw` reflects the unfiltered figures, and `impl = true − tests` per component +- **AND** when `Excluding` is nil, `true` degenerates to the raw pass and no separate `raw` row is shown + +### prmeta: Single-table render shape (Option A) + +#### R2: One markdown table that adapts by dropping rows +`renderImpact` MUST emit a SINGLE markdown table with columns `Scope | + / − | Net`, replacing the prior three render shapes. The numeric columns (`+ / −` and `Net`) MUST be right-aligned via the `|--:|` separator. The Net column MUST be present on every row. Rows MUST adapt by DROPPING, never reshaping: +- the `raw` row appears ONLY when `raw` differs from `true` (i.e. excludes engaged and they changed the figures); +- the `true` row ALWAYS appears and MUST be bold in every cell (`**true**`, `**+A/−B**`, `**+N**`); +- the `impl` and `tests` rows appear ONLY when a tests pair is present, rendered nested with a `└ ` prefix on the label (`└ impl`, `└ tests`). +The table MUST be preceded by the `**Impact**:` lead-in line and a blank line (preserving the existing header). The block MUST be omitted entirely when `HasImpact` is false or when `true` is `+0/−0`. + +- **GIVEN** an impact result with excludes and a tests pair +- **WHEN** `renderImpact` runs +- **THEN** the output is `**Impact**:`, a blank line, then a 4-row table (`raw`, `**true**`, `└ impl`, `└ tests`) with right-aligned numeric columns +- **AND** **GIVEN** no excludes and a tests pair, **THEN** the `raw` row is dropped (only `**true**`, `└ impl`, `└ tests`) +- **AND** **GIVEN** no tests pair, **THEN** only the `**true**` row (plus `raw` when excludes differ) is shown + +#### R3: Italic provenance caption +Below the table, the renderer MUST emit a single italic provenance caption co-locating the excludes note and the version stamp: `*excludes `` · generated by fab-kit v*`. The excludes list MUST be built from the actual `Excludes` values via the existing `backtickList` helper (never hardcoded). When there are no excludes, the caption MUST omit the `excludes … · ` clause and render only `*generated by fab-kit v*`. + +- **GIVEN** `Excludes = [fab/, docs/]` and `Version = "2.6.6"` +- **WHEN** the caption renders +- **THEN** it is `*excludes `fab/`, `docs/` · generated by fab-kit v2.6.6*` +- **AND** **GIVEN** no excludes, **THEN** it is `*generated by fab-kit v2.6.6*` + +### prmeta: Version provenance threading + +#### R4: Binary version stamped via a pure `Data.Version` field +A `Version string` field MUST be added to `prmeta.Data`. `prmeta.Render` MUST remain a pure function of `Data` — `Gather` MUST NOT populate `Version` (no I/O, no config read). The caption MUST render `fab-kit vdev` honestly when `Version == "dev"` (never suppressed). + +- **GIVEN** `Data.Version = "dev"` +- **WHEN** the caption renders +- **THEN** it ends with `generated by fab-kit vdev` +- **AND** `Gather` leaves `Version` empty (zero value) so the caller is the sole populator + +#### R5: `cmd/fab/pr_meta.go` populates Version from `main.version` +`RunE` in `cmd/fab/pr_meta.go` MUST set `data.Version = version` (the `package main` `version` var) before calling `prmeta.Render`, sourcing the running binary's version — NOT config `fab_version`. + +- **GIVEN** the `fab pr-meta` command runs +- **WHEN** `RunE` gathers data +- **THEN** it assigns `data.Version = version` before `prmeta.Render(data)` + +### prmeta: Preserve clamp behavior + +#### R6: Negative `impl` net stays clamped and annotated +The `clampNonNeg` + `clampAnnotation` behavior MUST be preserved for the `impl` row under the new taxonomy: a negative pre-clamp `impl` component is displayed clamped to non-negative, and the true negative value(s) are named in a trailing clamp annotation on that row. + +- **GIVEN** a tests pair whose figures exceed `true` (driving `impl` net negative) +- **WHEN** the `└ impl` row renders +- **THEN** its displayed figures are clamped to non-negative AND a clamp annotation names the true negative value(s) + +### Documentation: sibling/mirror sweep + +#### R7: Skill + SPEC mirror prose reflects the new shape +The observable `fab pr-meta` Impact output prose MUST be updated across the whole mirror class: `src/kit/skills/_cli-fab.md` (the `**Impact**` Output bullet) and `src/kit/skills/git-pr.md` (Step 3 Meta-block description), plus their SPEC mirrors `docs/specs/skills/SPEC-git-pr.md` and `docs/specs/skills/SPEC-_cli-fab.md` (the latter carries only an inventory row — verify it stays accurate). Only `src/kit/skills/` is edited, never `.claude/skills/`. + +- **GIVEN** the new single-table + caption render +- **WHEN** the docs are read +- **THEN** no doc describes the retired three-row / single-line / `← excludes` / `code (excluding …)` forms; all describe the single `Scope | + / − | Net` table, the `raw/true/impl/tests/excluded` taxonomy, and the provenance caption + +### Non-Goals + +- No new CLI flag/arg on `fab pr-meta` (observable-output change only). +- No change to `internal/impact` measured-passes contract or the `.status.yaml` `true_impact` schema. +- No migration (no user-data restructuring). +- No color/highlight/``/`$\textcolor{}$`/```diff in the PR body — bold only (GitHub sanitizer strips style/class). +- No change to the OTHER impl/tests/total render site (`fab change list --show-stats` / `impactColumn`) or its memory prose — out of scope for this change. +- `docs/memory/` updates (`pipeline/execution-skills.md`, `schemas.md`) are the hydrate stage's job, not apply's. + +### Design Decisions + +1. **`true` row figures source**: `impact.Result.Excluding` when non-nil, else the raw triple — *Why*: matches the existing render logic and the locked taxonomy (`true` = post-exclude). — *Rejected*: always using raw (reintroduces the bug). +2. **`raw` row shown only when it differs from `true`**: compare added/deleted/net — *Why*: avoids a redundant duplicate row when no excludes engage; intake locks "shown ONLY when it differs from true". — *Rejected*: always showing `raw` (noise on no-exclude PRs). +3. **Version is a `Data` field populated by the command, not `Gather`**: *Why*: keeps `Render` a pure function of `Data` (byte-stable testability); `version` is private to `package main`. — *Rejected*: reading inside `Gather` (breaks purity) or config `fab_version` (not the running binary). + +## Tasks + +### Phase 1: Core Implementation (prmeta package) + +- [x] T001 Add a `Version string` field to `prmeta.Data` in `src/go/fab/internal/prmeta/prmeta.go` (documented as populated by the command, never by `Gather`); update the `Excludes` field doc comment away from the retired `← excludes` phrasing. +- [x] T002 Rewrite `renderImpact` in `src/go/fab/internal/prmeta/prmeta.go` to the single-table Option A shape: compute the `raw/true/impl/tests` taxonomy, emit the `**Impact**:` lead-in + blank line + `Scope | + / − | Net` table (right-aligned numeric cols, Net kept), with the `raw` row only when it differs from `true`, the bold `**true**` row always, and nested `└ impl`/`└ tests` rows only when a tests pair is present; preserve `clampNonNeg` + `clampAnnotation` on the `impl` row; append the italic provenance caption (excludes clause via `backtickList` only when excludes present, plus `generated by fab-kit v`). Keep the `HasImpact`-false and `+0/−0`-true omission guards. + +### Phase 2: Command wiring + +- [x] T003 In `src/go/fab/cmd/fab/pr_meta.go` `RunE`, set `data.Version = version` (the `package main` var) before `prmeta.Render(data)`. + +### Phase 3: Tests + +- [x] T004 Update `src/go/fab/internal/prmeta/prmeta_test.go`: add a fixed `Version` to `baseData`; rewrite `TestRender_Impact` golden assertions to the new single-table + caption render across all combinations (excludes+tests, no-excludes+tests, excludes+no-tests, no-excludes+no-tests, omission cases, dev-version honesty) and the negative-`impl` clamp-annotation case; update `TestRender_FullBlock` markers if needed. +- [x] T005 [P] Add a cmd-wiring assertion to `src/go/fab/cmd/fab/pr_meta_test.go` verifying `RunE` threads `version` into `data.Version` (or document why the existing no-fab-context test suffices). +- [x] T006 Run `go test ./src/go/fab/internal/prmeta/... ./src/go/fab/cmd/fab/...`; widen to `go test ./...` if cross-cutting; fix failures so tests conform to the spec. + +### Phase 4: Documentation sweep + +- [x] T007 [P] Update the `**Impact**` Output bullet in `src/kit/skills/_cli-fab.md` (line ~472) to the single-table + provenance-caption shape and the `raw/true/impl/tests/excluded` taxonomy; verify `docs/specs/skills/SPEC-_cli-fab.md` (inventory row only) stays accurate. +- [x] T008 [P] Update the Meta-block Impact description in `src/kit/skills/git-pr.md` (Step 3, line ~242/251) and its mirror `docs/specs/skills/SPEC-git-pr.md` (lines ~110–114, ~141) to the new single-table + caption shape; retire all three-row/single-line/`← excludes`/`code (excluding …)` phrasings in the class. + +## Execution Order + +- T001 → T002 (Version field before the renderer that uses it) +- T002, T003 → T004, T005 (implementation before its tests) +- T004, T005 → T006 (write tests before running them) +- T007, T008 are independent of the Go work and of each other ([P]) + +## Acceptance + +### Functional Completeness + +- [ ] A-001 R1: The renderer computes `raw/true/impl/tests` per the locked taxonomy with `true` always the post-exclude diff and `impl = true − tests` per component. +- [ ] A-002 R2: `renderImpact` emits a single `Scope | + / − | Net` table with right-aligned numeric columns, the Net column on every row, the bold `**true**` row always present, the `raw` row only when it differs from `true`, and nested `└ impl`/`└ tests` rows only when a tests pair is present. +- [ ] A-003 R3: An italic provenance caption renders below the table — `*excludes · generated by fab-kit v*` with excludes via `backtickList`, omitting the `excludes …` clause when there are no excludes. +- [ ] A-004 R4: `prmeta.Data` has a `Version` field; `Render` stays pure (`Gather` does not set it); `Version == "dev"` renders `fab-kit vdev`. +- [ ] A-005 R5: `cmd/fab/pr_meta.go` `RunE` assigns `data.Version = version` before `prmeta.Render`. +- [ ] A-006 R6: A negative `impl` net is displayed clamped non-negative with a clamp annotation naming the true negative value(s). +- [ ] A-007 R7: The mirror class (`_cli-fab.md`, `git-pr.md`, `SPEC-git-pr.md`, `SPEC-_cli-fab.md`) describes the new single-table + caption shape with no retired phrasings; only `src/kit/skills/` was edited. + +### Behavioral Correctness + +- [ ] A-008 R1: The "total flips meaning" bug is fixed — `true` denotes the post-exclude diff in every combination (no form where it denotes the raw diff). + +### Scenario Coverage + +- [ ] A-009 R2: `prmeta_test.go` golden assertions cover all four combinations (excludes±, tests±), the omission cases, the dev-version caption, and the clamp-annotation case, all with a fixed `Version`. + +### Edge Cases & Error Handling + +- [ ] A-010 R2: The block is omitted entirely when `HasImpact` is false or `true` is `+0/−0`. +- [ ] A-011 R6: When tests exceed `true` (impl net negative), the `└ impl` row clamps to non-negative and annotates the true value. + +### Code Quality + +- [ ] A-012 Pattern consistency: New code follows the pure-render-functions + named-constants patterns of `prmeta.go`; the markdown-table emission mirrors `renderTable`'s style. +- [ ] A-013 No unnecessary duplication: `backtickList` and the existing `clampNonNeg`/`clampAnnotation` helpers are reused, not reimplemented. +- [ ] A-014 No magic strings: table headers, the `└ ` glyph, and caption literals use named constants where the surrounding code does. +- [ ] A-015 Mirror discipline: every `src/kit/skills/*.md` edit has its `docs/specs/skills/SPEC-*.md` mirror updated; no `.claude/skills/` files were touched. + +### Documentation Accuracy + +- [ ] A-016 R7: No doc in the repo describes the retired Impact forms for the `fab pr-meta` consumer (the `change list --show-stats` consumer's prose is intentionally untouched). + +### Cross References + +- [ ] A-017 R7: Cross-references between `_cli-fab.md`/`git-pr.md` and their SPEC mirrors remain consistent after the rewrite. + +## Notes + +- Check items as you review: `- [x]` +- All acceptance items must pass before `/fab-continue` (hydrate) +- `docs/memory/` (`pipeline/execution-skills.md`, `schemas.md`) updates are deferred to hydrate per the Affected Memory section. + +## Deletion Candidates + +The three-shape `renderImpact` was rewritten in place (the old 3-row impl/tests/total branch, the `+A/−B code (excluding …) · +C/−D total` single-line branch, and the bare `+A/−B total` branch). Those branches are gone from the working tree — they were edited, not orphaned, so they are not leftover dead code. No surviving symbol/file/branch/config became redundant or unused: + +- `clampNonNeg`, `clampAnnotation`, `backtickList` (`src/go/fab/internal/prmeta/prmeta.go`) — still reused by the new `└ impl` row and caption; not redundant. +- `impactColumn()` (`src/go/fab/internal/change/change.go`) — the OTHER impl/tests/total render site; intentionally out of scope and still in use; not redundant. +- `impact.Result.{Excluding,Tests}` (`src/go/fab/internal/impact/impact.go`) — read-only dependency, still consumed by the new taxonomy; not redundant. + +None — this change adds new functionality without making existing code redundant + +## Assumptions + +| # | Grade | Decision | Rationale | Scores | +|---|-------|----------|-----------|--------| +| 1 | Confident | The `raw` row is dropped when its added/deleted/net all equal `true`'s (the "differs from true" test compares the full triple, not just presence of excludes config). | Intake §2 locks "shown ONLY when it differs from true"; a configured-but-no-op exclude (excludes touched no lines) should not produce a duplicate row. | S:88 R:75 A:88 D:85 | +| 2 | Confident | Caption byte-format is `*excludes `` · generated by fab-kit v*` with ` · ` (space-middot-space) separator and a `v` prefix on the version; the `**Impact**:` lead-in + a blank line precede the table. | Intake Open Questions defer exact bytes to apply with one obvious default (the locked example shape); finalized against golden tests. | S:80 R:78 A:85 D:75 | +| 3 | Confident | The table separator row is `|---|--:|--:|` (left-aligned Scope, right-aligned numeric cols) and uses Unicode minus `−` in figures (matching the existing renderer). | Intake §2 specifies right-aligned numeric cols via `|--:|` and the existing code uses `−`; consistency with current bytes. | S:85 R:80 A:88 D:82 | +| 4 | Confident | SPEC-_cli-fab.md needs no Impact-detail rewrite — it carries only a one-line inventory row for `fab pr-meta` ("Render a fab PR's `## Meta` block as final markdown"), which stays accurate; the detailed Output prose lives in `_cli-fab.md`. | Verified by grep: the SPEC mirror has no three-row/Impact-shape prose. The mirror obligation is satisfied by confirming accuracy. | S:90 R:82 A:90 D:88 | +| 5 | Confident | The clamp annotation on the `└ impl` row reuses the existing `clampAnnotation` format verbatim (`(clamped from net -39, added -40, deleted -1)` style); only the row it attaches to is renamed from a text `impl:` line to a `└ impl` table row. | Intake §5/Assumption 7 require preserving the tested clamp behavior; reusing the helper keeps the format byte-identical. | S:88 R:80 A:90 D:85 | + +5 assumptions (0 certain, 5 confident, 0 tentative). diff --git a/src/go/fab/cmd/fab/pr_meta.go b/src/go/fab/cmd/fab/pr_meta.go index 99993480..d6e687ba 100644 --- a/src/go/fab/cmd/fab/pr_meta.go +++ b/src/go/fab/cmd/fab/pr_meta.go @@ -37,6 +37,11 @@ func prMetaCmd() *cobra.Command { return fmt.Errorf("no fab context for %q (change unresolved or .status.yaml absent)", args[0]) } + // Stamp the running binary's version into the Meta block's + // provenance caption. Sourced here (not in Gather) so Render stays a + // pure function of Data — `version` is private to package main (pnao). + data.Version = version + fmt.Print(prmeta.Render(data)) return nil }, diff --git a/src/go/fab/cmd/fab/pr_meta_test.go b/src/go/fab/cmd/fab/pr_meta_test.go index caf97fea..1b04dc75 100644 --- a/src/go/fab/cmd/fab/pr_meta_test.go +++ b/src/go/fab/cmd/fab/pr_meta_test.go @@ -5,6 +5,9 @@ import ( "os" "strings" "testing" + + "github.com/sahil87/fab-kit/src/go/fab/internal/impact" + "github.com/sahil87/fab-kit/src/go/fab/internal/prmeta" ) func TestPrMetaCmd_RegisteredWithExpectedUse(t *testing.T) { @@ -65,3 +68,25 @@ func TestPrMetaCmd_NoFabContextExitsNonZero(t *testing.T) { t.Errorf("expected empty stdout on no-fab-context error, got %q", out.String()) } } + +// TestPrMetaCmd_VersionThreadedIntoProvenance pins the cmd-wiring contract: the +// package main `version` var is stamped into the Meta block's provenance caption +// (pnao). prmeta.Render stays a pure function of Data — the command is the sole +// populator of Data.Version — so this asserts the same value RunE assigns +// (`data.Version = version`) reaches the rendered caption. The default build +// value is "dev", which must render honestly as `fab-kit vdev`. +func TestPrMetaCmd_VersionThreadedIntoProvenance(t *testing.T) { + d := prmeta.Data{ + HasImpact: true, + Impact: impact.Result{Added: 5, Deleted: 1, Net: 4}, + Version: version, // the same package-main var RunE threads into Data + } + got := prmeta.Render(d) + want := "generated by fab-kit v" + version + if !strings.Contains(got, want) { + t.Errorf("rendered Meta block missing version provenance %q\nfull output:\n%s", want, got) + } + if version == "dev" && !strings.Contains(got, "generated by fab-kit vdev") { + t.Errorf("dev build must render the version honestly, got:\n%s", got) + } +} diff --git a/src/go/fab/internal/prmeta/prmeta.go b/src/go/fab/internal/prmeta/prmeta.go index bb32499c..669b55ba 100644 --- a/src/go/fab/internal/prmeta/prmeta.go +++ b/src/go/fab/internal/prmeta/prmeta.go @@ -73,9 +73,14 @@ type Data struct { LinearWorkspace string // project.linear_workspace; "" → bare IDs // Impact - HasImpact bool // false → omit the Impact line entirely - Impact impact.Result // total = Excluding when present, else raw - Excludes []string // true_impact_exclude (for the ← excludes annotation) + HasImpact bool // false → omit the Impact block entirely + Impact impact.Result // true = Excluding when present, else raw + Excludes []string // true_impact_exclude (named in the provenance caption) + + // Provenance: the running fab-kit binary version (main.go `version`). + // Populated by the command (cmd/fab/pr_meta.go), NOT by Gather, so Render + // stays a pure function of Data. "dev" renders honestly as "fab-kit vdev". + Version string } // Render assembles the complete `## Meta` block markdown for d. It is a pure @@ -205,63 +210,117 @@ func renderIssues(d Data) string { return "**Issues**: " + strings.Join(rendered, ", ") } -// renderImpact renders the **Impact** line(s), or "" when the line must be -// omitted (no impact, or a +0/−0 total). When a tests pair is present it renders -// the three-row impl/tests/total breakdown; otherwise the single-line form. +// Impact-table render literals. The Meta block normalizes the Impact section to +// ONE shape (a single markdown table) that adapts by DROPPING rows, never by +// reshaping — so a reader comparing two PRs sees the same columns every time and +// every label means exactly one thing (pnao). +const ( + impactLeadIn = "**Impact**:" + impactHeader = "| Scope | + / − | Net |" + impactSepRow = "|---|--:|--:|" // Scope left-aligned; numeric cols right-aligned + nestedLabelPfx = "└ " // tree glyph prefixing the impl/tests rows +) + +// renderImpact renders the **Impact** block — a single markdown table plus an +// italic provenance caption — or "" when the block must be omitted (no 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). +// - 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. +// - impl — production code = true − tests, per component, clamped non-neg. +// - tests — test-path lines measured within `true`. impl/tests appear only +// when a tests pair is present, nested under `true` with a `└ ` glyph. +// - excluded — raw − true; named only in the caption, never a headline row. func renderImpact(d Data) string { if !d.HasImpact { return "" } - // total = the excluding pass when present, else the raw pass. - total := impact.Pair{Added: d.Impact.Added, Deleted: d.Impact.Deleted, Net: d.Impact.Net} + // true = the excluding pass when present, else the raw pass. + trueDiff := impact.Pair{Added: d.Impact.Added, Deleted: d.Impact.Deleted, Net: d.Impact.Net} if d.Impact.Excluding != nil { - total = *d.Impact.Excluding + trueDiff = *d.Impact.Excluding } - // Omit entirely on a no-op total — never render +0/−0. - if total.Added == 0 && total.Deleted == 0 { + // Omit entirely on a no-op `true` diff — never render +0/−0. + if trueDiff.Added == 0 && trueDiff.Deleted == 0 { return "" } - hasExcludes := len(d.Excludes) > 0 + raw := impact.Pair{Added: d.Impact.Added, Deleted: d.Impact.Deleted, Net: d.Impact.Net} + + var b strings.Builder + b.WriteString(impactLeadIn) + b.WriteString("\n\n") + b.WriteString(impactHeader) + b.WriteString("\n") + 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 { + b.WriteString(impactRow("raw", raw, false, "")) + } + + // true row — always present, bold in every cell. + b.WriteString(impactRow("true", trueDiff, true, "")) + // impl + tests rows — only when a tests pair is present, nested under true. if d.Impact.Tests != nil { tests := *d.Impact.Tests - // Raw (pre-clamp) impl figures: total minus tests. On a test-heavy PR - // these can go negative (tests added/deleted exceed the total), - // meaning the change is net-deletion in production code. - rawAdded := total.Added - tests.Added - rawDeleted := total.Deleted - tests.Deleted - rawNet := total.Net - tests.Net + // Pre-clamp impl figures: true minus tests. On a test-heavy PR these can + // go negative (tests exceed `true`), meaning net-deletion in production + // code. The clamp keeps the display non-negative (downstream consumers + // may assume so) but the annotation surfaces the true value (jznd (e)). + preAdded := trueDiff.Added - tests.Added + preDeleted := trueDiff.Deleted - tests.Deleted + preNet := trueDiff.Net - tests.Net impl := impact.Pair{ - Added: clampNonNeg(rawAdded), - Deleted: clampNonNeg(rawDeleted), - Net: clampNonNeg(rawNet), - } - var b strings.Builder - b.WriteString("**Impact**:\n") - // Keep the clamped (non-negative) display value, but annotate the true - // pre-clamp value when the clamp engages — otherwise PR-meta silently - // hides a net-deletion-in-production PR (jznd (e); clamp kept because - // downstream consumers may assume non-negative). - fmt.Fprintf(&b, " impl: +%d/−%d (net +%d)%s\n", - impl.Added, impl.Deleted, impl.Net, clampAnnotation(rawAdded, rawDeleted, rawNet)) - fmt.Fprintf(&b, " tests: +%d/−%d (net +%d)\n", tests.Added, tests.Deleted, tests.Net) - fmt.Fprintf(&b, " total: +%d/−%d (net +%d)", total.Added, total.Deleted, total.Net) - if hasExcludes { - fmt.Fprintf(&b, " ← excludes %s", backtickList(d.Excludes)) + Added: clampNonNeg(preAdded), + Deleted: clampNonNeg(preDeleted), + Net: clampNonNeg(preNet), } - return b.String() + b.WriteString(impactRow(nestedLabelPfx+"impl", impl, false, clampAnnotation(preAdded, preDeleted, preNet))) + b.WriteString(impactRow(nestedLabelPfx+"tests", tests, false, "")) } - // Single-line form. The trailing `total` pair is the raw measurement; the - // leading `code` pair is the scaffolding-excluded total. - if hasExcludes { - return fmt.Sprintf("**Impact**: +%d/−%d code (excluding %s) · +%d/−%d total", - total.Added, total.Deleted, backtickList(d.Excludes), d.Impact.Added, d.Impact.Deleted) - } - return fmt.Sprintf("**Impact**: +%d/−%d total", total.Added, total.Deleted) + b.WriteString("\n") + b.WriteString(impactCaption(d)) + return b.String() +} + +// impactRow renders one `| Scope | +A/−B | +N |` table row. When bold is set, +// every cell is wrapped in `**…**` (the `true` row's emphasis — bold survives +// GitHub's Markdown sanitizer; row backgrounds / text color do not, pnao). A +// non-empty annotation (the impl-clamp note) is appended inside the Scope cell. +func impactRow(label string, p impact.Pair, bold bool, annotation string) string { + scope := label + annotation + plusMinus := fmt.Sprintf("+%d/−%d", p.Added, p.Deleted) + net := fmt.Sprintf("+%d", p.Net) + if bold { + scope = "**" + scope + "**" + plusMinus = "**" + plusMinus + "**" + net = "**" + net + "**" + } + return fmt.Sprintf("| %s | %s | %s |\n", scope, plusMinus, net) +} + +// impactCaption renders the italic provenance caption co-locating the excludes +// note and the binary version stamp, e.g. +// `*excludes `fab/`, `docs/` · generated by fab-kit v2.6.6*`. The excludes +// clause is omitted when no excludes are configured; the version stamp is always +// present (dev builds render `fab-kit vdev` honestly). +func impactCaption(d Data) string { + stamp := "generated by fab-kit v" + d.Version + if len(d.Excludes) > 0 { + return fmt.Sprintf("*excludes %s · %s*", backtickList(d.Excludes), stamp) + } + return fmt.Sprintf("*%s*", stamp) } func clampNonNeg(n int) int { diff --git a/src/go/fab/internal/prmeta/prmeta_test.go b/src/go/fab/internal/prmeta/prmeta_test.go index 7721e576..6bcf6dcd 100644 --- a/src/go/fab/internal/prmeta/prmeta_test.go +++ b/src/go/fab/internal/prmeta/prmeta_test.go @@ -43,6 +43,7 @@ func baseData() Data { Tests: &impact.Pair{Added: 40, Deleted: 0, Net: 40}, }, Excludes: []string{"fab/", "docs/"}, + Version: "2.6.6", // fixed so the provenance caption is byte-stable } } @@ -190,68 +191,88 @@ func TestRender_Issues(t *testing.T) { }) } -// TestRender_Impact covers the three-row form, single-line form, omission, and -// the per-component clamp / Unicode minus / backtick excludes. +// TestRender_Impact covers the single-table Option A render (pnao): the four +// (excludes ± tests) combinations, the omission cases, the dev-version caption, +// the row-drop rules, and the per-component impl clamp / annotation. func TestRender_Impact(t *testing.T) { - t.Run("three-row form with excludes", func(t *testing.T) { + // Header + right-aligned separator shared by every non-omitted render. + const head = "**Impact**:\n\n" + + "| Scope | + / − | Net |\n" + + "|---|--:|--:|\n" + + t.Run("excludes + tests: raw, bold true, nested impl/tests, caption", func(t *testing.T) { got := renderImpact(baseData()) - want := "**Impact**:\n" + - " impl: +47/−38 (net +9)\n" + - " tests: +40/−0 (net +40)\n" + - " total: +87/−38 (net +49) ← excludes `fab/`, `docs/`" + // raw=142/38 (104) ≠ true=87/38 (49) → raw row shown. + // impl = true−tests = 47/38 (9); tests = 40/0 (40). + want := head + + "| raw | +142/−38 | +104 |\n" + + "| **true** | **+87/−38** | **+49** |\n" + + "| └ impl | +47/−38 | +9 |\n" + + "| └ tests | +40/−0 | +40 |\n" + + "\n*excludes `fab/`, `docs/` · generated by fab-kit v2.6.6*" if got != want { t.Errorf("impact =\n%q\nwant\n%q", got, want) } }) - t.Run("three-row form clamps negative impl components to zero", func(t *testing.T) { - d := baseData() - // tests exceeds total in every component → impl clamps to 0/0/0. - d.Impact.Excluding = &impact.Pair{Added: 10, Deleted: 2, Net: 8} - d.Impact.Tests = &impact.Pair{Added: 40, Deleted: 5, Net: 35} - got := renderImpact(d) - if !strings.Contains(got, "impl: +0/−0 (net +0)") { - t.Errorf("expected clamped impl row, got:\n%s", got) - } - }) - - t.Run("three-row form omits annotation when no excludes", func(t *testing.T) { + t.Run("no excludes + tests: raw row dropped, no-excludes caption", func(t *testing.T) { d := baseData() d.Excludes = nil - d.Impact.Excluding = nil // no excluding pass → total = raw + d.Impact.Excluding = nil // no excluding pass → true = raw → raw row dropped got := renderImpact(d) - // total degenerates to raw (142/38); impl = 142-40 / 38-0. - want := "**Impact**:\n" + - " impl: +102/−38 (net +64)\n" + - " tests: +40/−0 (net +40)\n" + - " total: +142/−38 (net +104)" + // true = raw = 142/38 (104); impl = 102/38 (64); tests = 40/0 (40). + want := head + + "| **true** | **+142/−38** | **+104** |\n" + + "| └ impl | +102/−38 | +64 |\n" + + "| └ tests | +40/−0 | +40 |\n" + + "\n*generated by fab-kit v2.6.6*" if got != want { t.Errorf("impact =\n%q\nwant\n%q", got, want) } - if strings.Contains(got, "← excludes") { - t.Errorf("expected no excludes annotation, got:\n%s", got) + if strings.Contains(got, "| raw |") { + t.Errorf("expected no raw row when raw == true, got:\n%s", got) + } + if strings.Contains(got, "excludes") { + t.Errorf("expected no excludes clause in caption, got:\n%s", got) } }) - t.Run("single-line form with excludes", func(t *testing.T) { + t.Run("excludes + no tests: raw + bold true only", func(t *testing.T) { d := baseData() d.Impact.Tests = nil got := renderImpact(d) - want := "**Impact**: +87/−38 code (excluding `fab/`, `docs/`) · +142/−38 total" + want := head + + "| raw | +142/−38 | +104 |\n" + + "| **true** | **+87/−38** | **+49** |\n" + + "\n*excludes `fab/`, `docs/` · generated by fab-kit v2.6.6*" if got != want { - t.Errorf("impact = %q\nwant %q", got, want) + t.Errorf("impact =\n%q\nwant\n%q", got, want) + } + if strings.Contains(got, "└ impl") || strings.Contains(got, "└ tests") { + t.Errorf("expected no nested rows without a tests pair, got:\n%s", got) } }) - t.Run("single-line form without excludes", func(t *testing.T) { + t.Run("no excludes + no tests: bold true row only", func(t *testing.T) { d := baseData() d.Impact.Tests = nil d.Impact.Excluding = nil d.Excludes = nil got := renderImpact(d) - want := "**Impact**: +142/−38 total" + want := head + + "| **true** | **+142/−38** | **+104** |\n" + + "\n*generated by fab-kit v2.6.6*" if got != want { - t.Errorf("impact = %q\nwant %q", got, want) + t.Errorf("impact =\n%q\nwant\n%q", got, want) + } + }) + + t.Run("dev build renders fab-kit vdev honestly", func(t *testing.T) { + d := baseData() + d.Version = "dev" + got := renderImpact(d) + if !strings.HasSuffix(got, "generated by fab-kit vdev*") { + t.Errorf("expected honest dev version stamp, got:\n%s", got) } }) @@ -259,37 +280,31 @@ func TestRender_Impact(t *testing.T) { d := baseData() d.HasImpact = false if got := renderImpact(d); got != "" { - t.Errorf("expected no Impact line, got %q", got) + t.Errorf("expected no Impact block, got %q", got) } }) - t.Run("omitted on +0/−0 total", func(t *testing.T) { + t.Run("omitted on +0/−0 true", func(t *testing.T) { d := baseData() d.Impact = impact.Result{Added: 0, Deleted: 0, Net: 0, Excluding: &impact.Pair{}} if got := renderImpact(d); got != "" { - t.Errorf("expected no Impact line for +0/−0, got %q", got) + t.Errorf("expected no Impact block for +0/−0, got %q", got) } }) - // jznd (e): a test-heavy PR where tests exceed the total drives impl net - // negative. The displayed net stays clamped at +0 (downstream consumers may - // assume non-negative), but the impl row MUST annotate the true negative - // value rather than silently hiding the net-deletion-in-production. - t.Run("annotates true value when impl net clamps", func(t *testing.T) { + // jznd (e): a test-heavy PR where tests exceed `true` drives impl net + // negative. The displayed figures stay clamped at +0 (downstream consumers + // may assume non-negative), but the └ impl row MUST annotate the true + // negative value rather than silently hiding net-deletion-in-production. + t.Run("clamps negative impl and annotates the true value", func(t *testing.T) { d := baseData() - // total = excluding pass = 10/2 (net 8); tests = 50/3 (net 47). - // impl raw = -40/-1 (net -39) → all clamp to 0. + // true = excluding = 10/2 (net 8); tests = 50/3 (net 47). + // impl pre = -40/-1 (net -39) → all clamp to 0. d.Impact.Excluding = &impact.Pair{Added: 10, Deleted: 2, Net: 8} d.Impact.Tests = &impact.Pair{Added: 50, Deleted: 3, Net: 47} got := renderImpact(d) - if !strings.Contains(got, "impl: +0/−0 (net +0)") { - t.Errorf("expected clamped impl display value, got:\n%s", got) - } - if !strings.Contains(got, "clamped from net -39") { - t.Errorf("expected clamp annotation naming the true negative net, got:\n%s", got) - } - if !strings.Contains(got, "added -40") || !strings.Contains(got, "deleted -1") { - t.Errorf("expected clamp annotation naming clamped added/deleted, got:\n%s", got) + if !strings.Contains(got, "| └ impl (clamped from net -39, added -40, deleted -1) | +0/−0 | +0 |") { + t.Errorf("expected clamped impl row with annotation, got:\n%s", got) } }) diff --git a/src/kit/skills/_cli-fab.md b/src/kit/skills/_cli-fab.md index 3ea2da55..4f9a41b0 100644 --- a/src/kit/skills/_cli-fab.md +++ b/src/kit/skills/_cli-fab.md @@ -469,7 +469,7 @@ Output — the exact `## Meta` block markdown: - The 5-column table (`ID | Type | Confidence | Plan | Review`) with `—` fallbacks, a ` ✓` Plan completion suffix when both task and acceptance pairs are complete, and a `✓/✗ {N} cycle{s}` Review cell. - `**Pipeline**`: 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. - `**Issues**` (only when `--issues` is non-empty): Linear-linked when `project.linear_workspace` is set, bare comma-joined IDs otherwise; positioned between Pipeline and Impact. -- `**Impact**`: three-row impl/tests/total form when a `tests` pair exists (impl is the per-component `max(0, total − tests)` residual, Unicode minus `−`, `← excludes` annotation built from the actual `true_impact_exclude` values each backtick-wrapped), single-line form otherwise; omitted entirely on `+0/−0` total, missing merge-base, or impact failure. +- `**Impact**`: a single normalized markdown table (`Scope | + / − | Net`, numeric columns right-aligned, Net kept on every row) followed by an italic 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 (GitHub's Markdown sanitizer strips row backgrounds and text color). Exit codes: - `0` — success; the `## Meta` block on stdout. diff --git a/src/kit/skills/git-pr.md b/src/kit/skills/git-pr.md index f47b62dc..e66448dd 100644 --- a/src/kit/skills/git-pr.md +++ b/src/kit/skills/git-pr.md @@ -239,7 +239,7 @@ Print: ` ✓ push — origin/` **Fab context** comes from Step 0: `{has_fab}`, `{name}`, `{has_intake}` (controls Summary/Changes sourcing below). Do NOT re-run `fab change resolve` — reuse the Step 0 resolution. - **Render the `## Meta` block** (only when `{has_fab}`): delegate the entire Meta block (table + `**Pipeline**` + optional `**Issues**` + optional `**Impact**`) to `fab pr-meta`, which reads `.status.yaml`, parses `plan.md` checkboxes, reads `fab/project/config.yaml` (`true_impact_exclude`, `test_paths`, `project.linear_workspace`), computes the impact math, and resolves git/`gh` context (branch, owner/repo, merge-base) itself. Pass the Step 0 `{name}`, the resolved `{type}` (from Step 0b), and the space-joined `{issues}` (from Step 1): + **Render the `## Meta` block** (only when `{has_fab}`): delegate the entire Meta block (table + `**Pipeline**` + optional `**Issues**` + optional `**Impact**`) to `fab pr-meta`, which reads `.status.yaml`, parses `plan.md` checkboxes, reads `fab/project/config.yaml` (`true_impact_exclude`, `test_paths`, `project.linear_workspace`), computes the impact math, resolves git/`gh` context (branch, owner/repo, merge-base), and stamps the running binary version, itself. The `**Impact**` section renders as a single normalized table (`Scope | + / − | Net`) with the locked `raw / true / impl / tests / excluded` taxonomy (`true` is always the post-exclude diff) plus an italic provenance caption co-locating the excludes note and the `generated by fab-kit vX.Y.Z` stamp. Pass the Step 0 `{name}`, the resolved `{type}` (from Step 0b), and the space-joined `{issues}` (from Step 1): ```bash META=$(fab pr-meta "{name}" --type {type} --issues "{issues}" 2>/dev/null) || META="" @@ -248,7 +248,7 @@ Print: ` ✓ push — origin/` - If exit 0 and `META` is non-empty: the `## Meta` block is `$META` **verbatim** — do not reformat, re-wrap, or re-derive any of it. - If exit non-zero or `META` is empty (no fab context, change unresolved, or `.status.yaml` absent): omit the `## Meta` block entirely, exactly as the legacy `{has_fab} = false` path did. - `fab pr-meta` degrades gracefully on its own: an unreachable `gh` falls back to plain-text Pipeline labels, and a missing/failed merge-base or `+0/−0` total drops only the `**Impact**` line — none of these break the block or the PR. + `fab pr-meta` degrades gracefully on its own: an unreachable `gh` falls back to plain-text Pipeline labels, and a missing/failed merge-base or a `+0/−0` `true` diff drops only the `**Impact**` block — none of these break the block or the PR. **Assemble the body** in this exact order: From 5e505e51ca9b71dd17e0d4a555acfad8a1f99ea7 Mon Sep 17 00:00:00 2001 From: Sahil Ahuja Date: Thu, 25 Jun 2026 14:02:06 +0530 Subject: [PATCH 2/7] docs: refresh memory indexes --- docs/memory/_shared/index.md | 2 +- docs/memory/pipeline/index.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/memory/_shared/index.md b/docs/memory/_shared/index.md index 272a03a6..3248e269 100644 --- a/docs/memory/_shared/index.md +++ b/docs/memory/_shared/index.md @@ -7,5 +7,5 @@ description: "Cross-cutting concerns spanning all domains — config.yaml/consti | File | Description | Last Updated | |------|-------------|-------------| -| [configuration](configuration.md) | `config.yaml` schema (incl. `fab_version`, `review_tools`, `true_impact_exclude`, `test_paths`, `stage_hooks`, `agent.tiers` per-stage-model override — sole surface, per-field merge over fab-kit defaults, NO validation/provider-neutral, fixed non-overridable stage→tier mapping, l3ja; `stage_directives` + `model_tiers` removed in c5tr via migration 2.1.6-to-2.2.0), single fab-module parser `internal/config` + nil-safe accessors incl. the coupled-Unmarshal fallback caveat (ye8r), companion files (`context.md`, `code-quality.md`, `code-review.md` incl. `## Parsimony Pass` toggle and the wired `## Rework Budget` Max-cycles knob), `constitution.md` governance, 5 Cs of Quality, lifecycle management | 2026-06-16 | +| [configuration](configuration.md) | `config.yaml` schema (incl. `fab_version`, `review_tools`, `true_impact_exclude`, `test_paths`, `stage_hooks`, `agent.tiers` per-stage-model override — sole surface, per-field merge over fab-kit defaults, NO validation/provider-neutral, fixed non-overridable stage→tier mapping, l3ja; `stage_directives` + `model_tiers` removed in c5tr via migration 2.1.6-to-2.2.0), single fab-module parser `internal/config` + nil-safe accessors incl. the coupled-Unmarshal fallback caveat (ye8r), companion files (`context.md`, `code-quality.md`, `code-review.md` incl. `## Parsimony Pass` toggle and the wired `## Rework Budget` Max-cycles knob), `constitution.md` governance, 5 Cs of Quality, lifecycle management | 2026-06-25 | | [context-loading](context-loading.md) | Smart context loading convention — descriptive 7-file always-load layer (skill file wins; exception set rule-derived from skill files, never enumerated in the preamble — d9rs), opt-in skill helpers (7-value allowlist incl. `_srad`/`_pipeline`/`_intake`) + stage-conditional loading, standard subagent context (orchestrators incl. `/fab-proceed`), per-stage model resolution at the dispatch seam (`fab resolve-agent `, provider-neutral, Claude-Code adapter named, review-resolves-once — l3ja; applies on every post-intake stage since the single-dispatch collapse, advisory only for a genuinely no-dispatch run — fgxx; the two halves dispatch through two seams — model via the Agent `model` param (short-alias enum opus/sonnet/haiku/fable, resolved with `fab resolve-agent --alias` so the alias is emitted directly — the deterministic adapter that superseded m3d4's prompt-side id→alias hand-map; yky7), effort via an imperative subagent-prompt instruction (no Agent effort param; omit when empty), plus a compliance-visibility expectation that each site surface the resolved `model=/effort=` — m3d4; residual = a per-sub-agent effort param on the Agent tool, a harness ask), selective domain loading, SRAD protocol pointer, scoped Next Steps Convention, generic fab-command failure rule (unconditional non-zero exit → STOP; `fab log command` exits 0 by contract) | 2026-06-19 | diff --git a/docs/memory/pipeline/index.md b/docs/memory/pipeline/index.md index 21c1af53..356a06eb 100644 --- a/docs/memory/pipeline/index.md +++ b/docs/memory/pipeline/index.md @@ -9,7 +9,7 @@ description: "The change pipeline — stage lifecycle & state machine, planning/ |------|-------------|-------------| | [change-lifecycle](change-lifecycle.md) | Change naming, folder structure, `.status.yaml` (6-stage pipeline + `plan:` block, flock-serialized writes + fsync durability + sparse-key insertion), `.fab-status.yaml` symlink (atomic temp+rename swap, dangling-target validation), `stage_hooks` pre/post commands around start/finish (documented in c5tr), git integration (bookkeeping decoupled; ship-time branch↔change hard guard since w7dp), `/fab-status` (six-glyph progress legend incl. `⏭` skipped), `/fab-switch` (six-state `{state}` qualifier), backlog scanning, optional `summary` log field (FKF C-lite source, 5943) + the generated FKF memory-artifact surface (`fab memory-index` emits per-folder `log.md` + `fkf_version`/`type: memory` frontmatter, bmzo) | 2026-06-16 | | [clarify](clarify.md) | `/fab-clarify` skill — intake-only (j6cs), dual modes (suggest/auto), intake taxonomy scan, structured questions, coverage reports, audit trail, grade reclassification, always-recompute intake score; hosts the [AUTO-MODE] Skill Invocation Protocol and is sole bulk-confirm authority (zc9m); bulk confirm evaluated before the zero-gaps exit, confirmations graded by recomputed composite (no fiat Certain), unified audit-trail placement rules (c5tr) | 2026-06-19 | -| [execution-skills](execution-skills.md) | Apply (unified `plan.md` `## Requirements`+`## Tasks`+`## Acceptance` generated at entry), review, hydrate, archive, and orchestrator behavior — `/fab-continue` for pipeline stages, `/fab-archive` for housekeeping, `/fab-proceed` for context-aware pipeline orchestration; `/git-pr` ship behavior with the mechanically-rendered `## Meta` block via `fab pr-meta`, a unified Step 0 resolution (szxd), and git-state hardening — detached-HEAD STOP, expected-area staging guard, PR-state branching, resolved default-branch guard (g8st); `/git-pr-review` split commit/push failure semantics + unpushed-commit re-run gate (g8st); fab-continue loads `_generation`/`_review` stage-conditionally at point of use (zc9m); the shared ff/fff bracket lives in `_pipeline.md` with once-stated rework choreography (cycle cap = the `{max_cycles}` knob from code-review.md § Rework Budget, default 3 — c5tr), exhaustion terminal state review:failed, and fab-continue's review-failed rework-menu dispatch row (szxd); re-archiving a genuinely archived change soft-skips (exit 0) and restore `--switch` surfaces `pointer: failed` (k4ge); archive/restore index writes are atomic with honest `index: failed` reporting on both paths (hv7t); `/git-pr`/`/git-pr-review` accept an optional explicit `` argument and guard branch↔change correspondence before any mutation, fab-continue gained the review-pr/failed dispatch row + idempotent reset, and recovery guidance is override-aware (w7dp); pipeline hydrate gained a refuse-before-regen guard consulting `fab memory-index --check` exit 2 (destructive loss), defense-in-depth + born-compatible no-op (glwc); the Auto-Rework Loop states the cycle-count invariant + baseline convention (iterations == initial entry + each re-entry, N cycles → N+1; a prose gap, not a Go bug) and `/git-pr-review` Phase 2 carries the synchronous-poll discipline + corrected two-login query semantics (landed-review predicate `author.login == copilot-pull-request-reviewer`; `Copilot` under requested_reviewers; REST requested_reviewers confirms requests since GraphQL omits bot reviewers) (qg64); pipeline hydrate now authors FKF frontmatter (`type: memory` + `description:`), stops writing per-file `## Changelog` (records the what-changed via `fab status set-summary`), and uses bundle-relative memory↔memory links — 8fr5; `/git-pr` Step 3 gained sub-step 3a-bis — a post-commit/pre-push `fab memory-index` regen with a separate `docs: refresh memory indexes` follow-up commit (no `--amend`), gated on `{has_fab}` AND 3a-just-committed and diff-guarded, fixing the `index.md` 'Last Updated' date drift that arises because hydrate's regen is pre-commit (o203). Operator coordination moved to runtime/operator.md. | 2026-06-20 | +| [execution-skills](execution-skills.md) | Apply (unified `plan.md` `## Requirements`+`## Tasks`+`## Acceptance` generated at entry), review, hydrate, archive, and orchestrator behavior — `/fab-continue` for pipeline stages, `/fab-archive` for housekeeping, `/fab-proceed` for context-aware pipeline orchestration; `/git-pr` ship behavior with the mechanically-rendered `## Meta` block via `fab pr-meta`, a unified Step 0 resolution (szxd), and git-state hardening — detached-HEAD STOP, expected-area staging guard, PR-state branching, resolved default-branch guard (g8st); `/git-pr-review` split commit/push failure semantics + unpushed-commit re-run gate (g8st); fab-continue loads `_generation`/`_review` stage-conditionally at point of use (zc9m); the shared ff/fff bracket lives in `_pipeline.md` with once-stated rework choreography (cycle cap = the `{max_cycles}` knob from code-review.md § Rework Budget, default 3 — c5tr), exhaustion terminal state review:failed, and fab-continue's review-failed rework-menu dispatch row (szxd); re-archiving a genuinely archived change soft-skips (exit 0) and restore `--switch` surfaces `pointer: failed` (k4ge); archive/restore index writes are atomic with honest `index: failed` reporting on both paths (hv7t); `/git-pr`/`/git-pr-review` accept an optional explicit `` argument and guard branch↔change correspondence before any mutation, fab-continue gained the review-pr/failed dispatch row + idempotent reset, and recovery guidance is override-aware (w7dp); pipeline hydrate gained a refuse-before-regen guard consulting `fab memory-index --check` exit 2 (destructive loss), defense-in-depth + born-compatible no-op (glwc); the Auto-Rework Loop states the cycle-count invariant + baseline convention (iterations == initial entry + each re-entry, N cycles → N+1; a prose gap, not a Go bug) and `/git-pr-review` Phase 2 carries the synchronous-poll discipline + corrected two-login query semantics (landed-review predicate `author.login == copilot-pull-request-reviewer`; `Copilot` under requested_reviewers; REST requested_reviewers confirms requests since GraphQL omits bot reviewers) (qg64); pipeline hydrate now authors FKF frontmatter (`type: memory` + `description:`), stops writing per-file `## Changelog` (records the what-changed via `fab status set-summary`), and uses bundle-relative memory↔memory links — 8fr5; `/git-pr` Step 3 gained sub-step 3a-bis — a post-commit/pre-push `fab memory-index` regen with a separate `docs: refresh memory indexes` follow-up commit (no `--amend`), gated on `{has_fab}` AND 3a-just-committed and diff-guarded, fixing the `index.md` 'Last Updated' date drift that arises because hydrate's regen is pre-commit (o203). Operator coordination moved to runtime/operator.md. | 2026-06-25 | | [planning-skills](planning-skills.md) | `/fab-new`, `/fab-continue`, `/fab-ff`, `/fab-clarify` — the planning stage (intake only) and the shared `_generation.md` partial (Intake + unified Plan procedures); requirement capture + plan generation live at apply entry (spec stage removed in j6cs); fab-new/fab-draft re-run/resume semantics (ID collisions route to resume, 9u91); change_type is hook-owned — skills verify via .status.yaml and override only if wrong (uliv); SRAD framework lives in the `_srad` helper, declared by the 6 planning skills (zc9m); pre-boundary intake creation (fab-new Steps 0–9) lives in the shared `_intake` helper parameterized by `{questioning-mode}` (interactive | promptless-defer) — fab-new/fab-draft/fab-proceed are thin call-sites, completing the one-shared-helper-per-phase symmetry with `_pipeline`; fab-new keeps the activate/branch tail, fab-draft stops at ready (momentum warning retired), fab-proceed dispatches promptless-defer + chains /fab-switch→/git-branch (3xaj); fab-ff/fab-fff are thin wrappers over the shared `_pipeline` bracket, fab-new Step 11 is a single first-match-wins branch table (szxd; 6 rows since g8st — remote-only `--track` checkout, dirty-tree carried-over note excluding the change's own artifacts, same-change rename); SRAD grades by half-open bands, the plan walk emits `## Assumptions` explicitly, artifacts always carry the section (omit-when-zero is display-only), fab-new output puts the Assumptions block last (c5tr); SRAD v2 demerit confidence scoring — composite weights `0.20/0.30/0.30/0.20`, grade bands 80/50/20 (indicative only), both hard-fail short-circuits (Unresolved→0.0 and `R<25∧A<25`) removed, blocking emergent from the penalty curve (4yi8); fab-new's backlog-ID collision pre-check is exact-ID anchored (`fab resolve --id` equality) and fab-proceed's promptless `_intake` dispatch carries the defer-and-surface contract with the matching `_srad` Critical-Rule carve-out (w7dp); fab-new/fab-draft `Next:` lines derive at runtime per the _preamble Lookup Procedure and `_generation` names fab-continue in both consumer groups (d9rs); change_type explicit set is sticky — set-change-type marks `change_type_source: explicit` and the hook re-infers only when source is absent/inferred, retiring the re-verify-after-later-writes caveat (jznd) | 2026-06-19 | | [preflight](preflight.md) | `lib/preflight.sh` script — validation, accessor-based architecture, structured YAML output, skill integration | 2026-06-16 | -| [schemas](schemas.md) | Workflow schema authority — the Go state machine (`internal/status` transitions + `internal/statusfile` stage order/progress; declarative `workflow.yaml` retired in c5tr): 6-stage pipeline, states, transitions, validation rules; `.status.yaml` `plan:` (`## Requirements`-aware), `confidence:` (indicative retired), and lazy `true_impact:` block schemas (incl. the `tests` sub-block + render-time `impl` residual, 7t5a); `fab impact` and `fab pr-meta` helper subcommands (rj31); allowed-states-enforced transition targets, `fab score --check-gate` non-zero gate-fail exit, iterations-preserving reset cascade (k4ge; cycle-count accuracy is a choreography property not a state-machine one, the fix lives in skill prose — qg64); `fab score` normal-mode hard-fail on load/persist/read errors (hv7t); per-stage allowed-states + transition-event tables enumerated, pinned by the exhaustive 216-cell matrix test (tb6f); `change_type_source: inferred|explicit` guard (set-change-type marks explicit, hook re-infers only when not explicit), read-time `acceptance_completed` via `status.LiveAcceptance` (counter demoted to cache), and `internal/resolve` `ErrNotFound`/`ErrAmbiguous` sentinels (jznd); optional `summary` field + `set-summary`/`get-summary` verbs (FKF C-lite log source, 5943); the `log.md` C-lite schema + registry-gated change-id join + extended (benign-only, no-new-category) `--check` loss tiers (bmzo); the `log.seed.md` seed-merge — `parseSeedLog`/`mergeSeedEntries` curated-sidecar input merged beneath git-projected entries, idempotent, seed-only folders still emit a log.md, loss tier stays benign (oovf); freeze-on-write `log.md` generation — existing log is authoritative/write-once, `parseLog` reads it back, append-only on `(file-base, change-id)`, unattributable-freeze, `--rebuild` destructive escape hatch, `--check` superset-PASS / missing-or-hand-edit-FAIL via merge-as-rendered (tayp) | 2026-06-16 | +| [schemas](schemas.md) | Workflow schema authority — the Go state machine (`internal/status` transitions + `internal/statusfile` stage order/progress; declarative `workflow.yaml` retired in c5tr): 6-stage pipeline, states, transitions, validation rules; `.status.yaml` `plan:` (`## Requirements`-aware), `confidence:` (indicative retired), and lazy `true_impact:` block schemas (incl. the `tests` sub-block + render-time `impl` residual, 7t5a); `fab impact` and `fab pr-meta` helper subcommands (rj31); allowed-states-enforced transition targets, `fab score --check-gate` non-zero gate-fail exit, iterations-preserving reset cascade (k4ge; cycle-count accuracy is a choreography property not a state-machine one, the fix lives in skill prose — qg64); `fab score` normal-mode hard-fail on load/persist/read errors (hv7t); per-stage allowed-states + transition-event tables enumerated, pinned by the exhaustive 216-cell matrix test (tb6f); `change_type_source: inferred|explicit` guard (set-change-type marks explicit, hook re-infers only when not explicit), read-time `acceptance_completed` via `status.LiveAcceptance` (counter demoted to cache), and `internal/resolve` `ErrNotFound`/`ErrAmbiguous` sentinels (jznd); optional `summary` field + `set-summary`/`get-summary` verbs (FKF C-lite log source, 5943); the `log.md` C-lite schema + registry-gated change-id join + extended (benign-only, no-new-category) `--check` loss tiers (bmzo); the `log.seed.md` seed-merge — `parseSeedLog`/`mergeSeedEntries` curated-sidecar input merged beneath git-projected entries, idempotent, seed-only folders still emit a log.md, loss tier stays benign (oovf); freeze-on-write `log.md` generation — existing log is authoritative/write-once, `parseLog` reads it back, append-only on `(file-base, change-id)`, unattributable-freeze, `--rebuild` destructive escape hatch, `--check` superset-PASS / missing-or-hand-edit-FAIL via merge-as-rendered (tayp) | 2026-06-25 | From 23bb8c3bae6fb8fdea2b166d8a52f4954f6fde4f Mon Sep 17 00:00:00 2001 From: Sahil Ahuja Date: Thu, 25 Jun 2026 14:02:49 +0530 Subject: [PATCH 3/7] Update ship status and record PR URL --- .../.history.jsonl | 1 + .../.status.yaml | 12 +++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.history.jsonl b/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.history.jsonl index ddb382fe..054b040a 100644 --- a/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.history.jsonl +++ b/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.history.jsonl @@ -7,3 +7,4 @@ {"action":"enter","driver":"fab-fff","event":"stage-transition","stage":"hydrate","ts":"2026-06-25T08:26:57Z"} {"event":"review","result":"passed","ts":"2026-06-25T08:26:57Z"} {"action":"enter","driver":"fab-fff","event":"stage-transition","stage":"ship","ts":"2026-06-25T08:30:59Z"} +{"action":"enter","driver":"git-pr","event":"stage-transition","stage":"review-pr","ts":"2026-06-25T08:32:46Z"} diff --git a/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.status.yaml b/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.status.yaml index 87a68ab8..faa41181 100644 --- a/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.status.yaml +++ b/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.status.yaml @@ -9,8 +9,8 @@ progress: apply: done review: done hydrate: done - ship: active - review-pr: pending + ship: done + review-pr: active plan: generated: true task_count: 8 @@ -33,8 +33,10 @@ stage_metrics: apply: {started_at: "2026-06-25T08:14:47Z", driver: fab-fff, iterations: 1, completed_at: "2026-06-25T08:21:57Z"} review: {started_at: "2026-06-25T08:21:57Z", driver: fab-fff, iterations: 1, completed_at: "2026-06-25T08:26:57Z"} hydrate: {started_at: "2026-06-25T08:26:57Z", driver: fab-fff, iterations: 1, completed_at: "2026-06-25T08:30:59Z"} - ship: {started_at: "2026-06-25T08:30:59Z", driver: fab-fff, iterations: 1} -prs: [] + ship: {started_at: "2026-06-25T08:30:59Z", driver: fab-fff, iterations: 1, completed_at: "2026-06-25T08:32:46Z"} + review-pr: {started_at: "2026-06-25T08:32:46Z", driver: git-pr, iterations: 1} +prs: + - https://github.com/sahil87/fab-kit/pull/444 true_impact: added: 0 deleted: 0 @@ -51,4 +53,4 @@ true_impact: computed_at_stage: hydrate summary: Normalized the fab pr-meta Meta Impact block to a single Scope|+/−|Net table with the raw/true/impl/tests/excluded taxonomy and a binary-version provenance caption, fixing the total-flips-meaning bug # true_impact: lazily created on first apply-finish (no placeholder here). -last_updated: 2026-06-25T08:30:59Z +last_updated: 2026-06-25T08:32:46Z From 3767d613a0afa7473ea00e56164fc80cd46a4dc9 Mon Sep 17 00:00:00 2001 From: Sahil Ahuja Date: Thu, 25 Jun 2026 14:09:21 +0530 Subject: [PATCH 4/7] fix: address review feedback from @Copilot Co-Authored-By: Claude Opus 4.8 (1M context) --- src/go/fab/cmd/fab/pr_meta_test.go | 15 +++++++++------ src/go/fab/internal/prmeta/prmeta.go | 14 +++++++++++++- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/go/fab/cmd/fab/pr_meta_test.go b/src/go/fab/cmd/fab/pr_meta_test.go index 1b04dc75..a416063a 100644 --- a/src/go/fab/cmd/fab/pr_meta_test.go +++ b/src/go/fab/cmd/fab/pr_meta_test.go @@ -69,12 +69,15 @@ func TestPrMetaCmd_NoFabContextExitsNonZero(t *testing.T) { } } -// TestPrMetaCmd_VersionThreadedIntoProvenance pins the cmd-wiring contract: the -// package main `version` var is stamped into the Meta block's provenance caption -// (pnao). prmeta.Render stays a pure function of Data — the command is the sole -// populator of Data.Version — so this asserts the same value RunE assigns -// (`data.Version = version`) reaches the rendered caption. The default build -// value is "dev", which must render honestly as `fab-kit vdev`. +// TestPrMetaCmd_VersionThreadedIntoProvenance pins the render half of the +// version-provenance contract: feeding the package main `version` var through +// Data.Version surfaces it in the Meta block's provenance caption (pnao). +// prmeta.Render stays a pure function of Data, and RunE is its sole populator +// (`data.Version = version`, see prMetaCmd); this test exercises Render +// directly with that same `version` value rather than driving RunE (which needs +// a live fab context), so it verifies the caption wiring, not the RunE +// assignment itself. The default build value is "dev", which must render +// honestly as `fab-kit vdev`. func TestPrMetaCmd_VersionThreadedIntoProvenance(t *testing.T) { d := prmeta.Data{ HasImpact: true, diff --git a/src/go/fab/internal/prmeta/prmeta.go b/src/go/fab/internal/prmeta/prmeta.go index 669b55ba..2998c9e6 100644 --- a/src/go/fab/internal/prmeta/prmeta.go +++ b/src/go/fab/internal/prmeta/prmeta.go @@ -301,7 +301,7 @@ func renderImpact(d Data) string { func impactRow(label string, p impact.Pair, bold bool, annotation string) string { scope := label + annotation plusMinus := fmt.Sprintf("+%d/−%d", p.Added, p.Deleted) - net := fmt.Sprintf("+%d", p.Net) + net := formatNet(p.Net) if bold { scope = "**" + scope + "**" plusMinus = "**" + plusMinus + "**" @@ -330,6 +330,18 @@ func clampNonNeg(n int) int { return n } +// formatNet renders a signed net delta. Non-negative values get an explicit +// `+` prefix; negative values keep their own `-` (so a net-deletion `true`/raw/ +// tests row reads `-3`, never `+-3`). Mirrors the local convention in +// internal/change/change.go. The `impl` row is pre-clamped non-negative, but +// raw/true/tests pass through unclamped Net values that can go negative. +func formatNet(n int) string { + if n >= 0 { + return fmt.Sprintf("+%d", n) + } + return fmt.Sprintf("%d", n) +} + // clampAnnotation returns a trailing " (clamped from …)" note naming the true // pre-clamp impl figures whenever any of them was negative (so the clamp // changed the displayed value), or "" when no clamping occurred. Only the From 590aa7af6e3402a6a307bc2ca49ec989899f2979 Mon Sep 17 00:00:00 2001 From: Sahil Ahuja Date: Thu, 25 Jun 2026 14:10:06 +0530 Subject: [PATCH 5/7] Update review-pr status Co-Authored-By: Claude Opus 4.8 (1M context) --- .../.history.jsonl | 1 + .../260625-pnao-normalize-pr-meta-impact-block/.status.yaml | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.history.jsonl b/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.history.jsonl index 054b040a..077df05f 100644 --- a/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.history.jsonl +++ b/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.history.jsonl @@ -8,3 +8,4 @@ {"event":"review","result":"passed","ts":"2026-06-25T08:26:57Z"} {"action":"enter","driver":"fab-fff","event":"stage-transition","stage":"ship","ts":"2026-06-25T08:30:59Z"} {"action":"enter","driver":"git-pr","event":"stage-transition","stage":"review-pr","ts":"2026-06-25T08:32:46Z"} +{"event":"review","result":"passed","ts":"2026-06-25T08:40:00Z"} diff --git a/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.status.yaml b/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.status.yaml index faa41181..524ca680 100644 --- a/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.status.yaml +++ b/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.status.yaml @@ -10,7 +10,7 @@ progress: review: done hydrate: done ship: done - review-pr: active + review-pr: done plan: generated: true task_count: 8 @@ -34,7 +34,7 @@ stage_metrics: review: {started_at: "2026-06-25T08:21:57Z", driver: fab-fff, iterations: 1, completed_at: "2026-06-25T08:26:57Z"} hydrate: {started_at: "2026-06-25T08:26:57Z", driver: fab-fff, iterations: 1, completed_at: "2026-06-25T08:30:59Z"} ship: {started_at: "2026-06-25T08:30:59Z", driver: fab-fff, iterations: 1, completed_at: "2026-06-25T08:32:46Z"} - review-pr: {started_at: "2026-06-25T08:32:46Z", driver: git-pr, iterations: 1} + review-pr: {started_at: "2026-06-25T08:32:46Z", driver: git-pr, iterations: 1, completed_at: "2026-06-25T08:40:00Z"} prs: - https://github.com/sahil87/fab-kit/pull/444 true_impact: @@ -53,4 +53,4 @@ true_impact: computed_at_stage: hydrate summary: Normalized the fab pr-meta Meta Impact block to a single Scope|+/−|Net table with the raw/true/impl/tests/excluded taxonomy and a binary-version provenance caption, fixing the total-flips-meaning bug # true_impact: lazily created on first apply-finish (no placeholder here). -last_updated: 2026-06-25T08:32:46Z +last_updated: 2026-06-25T08:40:00Z From 4b83ee37e850f21170278f45ed7f234dcd286ef8 Mon Sep 17 00:00:00 2001 From: Sahil Ahuja Date: Thu, 25 Jun 2026 15:01:38 +0530 Subject: [PATCH 6/7] refactor: revise PR meta Meta-block layout (Change ID header, self-labeling Impact table, caption, Pipeline-last) Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/memory/_shared/configuration.md | 2 +- docs/memory/pipeline/execution-skills.md | 8 +- docs/memory/pipeline/schemas.md | 2 +- docs/specs/skills/SPEC-git-pr.md | 19 ++-- .../.history.jsonl | 5 + .../.status.yaml | 38 ++++---- .../plan.md | 23 +++++ src/go/fab/internal/prmeta/prmeta.go | 82 +++++++++-------- src/go/fab/internal/prmeta/prmeta_test.go | 91 +++++++++++-------- src/kit/skills/_cli-fab.md | 12 +-- src/kit/skills/git-pr.md | 4 +- 11 files changed, 168 insertions(+), 118 deletions(-) diff --git a/docs/memory/_shared/configuration.md b/docs/memory/_shared/configuration.md index 285c3477..70edb69c 100644 --- a/docs/memory/_shared/configuration.md +++ b/docs/memory/_shared/configuration.md @@ -70,7 +70,7 @@ When absent, `fab-sync.sh` falls back to `haiku` for the fast tier. (Note: the m Optional top-level field. A YAML sequence of pathspec exclusion patterns — typically directory prefixes ending in `/` (e.g., `fab/`, `docs/`, `vendor/`), but any pattern accepted by `git diff` `:(exclude)` syntax is valid. Scaffold default: `[fab/, docs/]` so new fab-kit projects emit the impact block out of the box. Semantics: -- **Present and non-empty** — the `**Impact**:` line of the PR `## Meta` block (rendered by `fab pr-meta` as of rj31; previously inlined in `/git-pr`) annotates the excluded scope, the parenthetical/annotation reflecting the config values verbatim (each per-element backtick-wrapped, never hardcoded). Computed via `git diff --shortstat "$BASE...HEAD"` against the merge-base with and without the `:(exclude)` pathspec args (canonical math in `internal/impact`). +- **Present and non-empty** — the Impact table of the PR `## Meta` block (rendered by `fab pr-meta` as of rj31; previously inlined in `/git-pr`) names the excluded scope in its `` provenance caption (`excludes `fab/`, `docs/` · generated by fab-kit vX.Y.Z`, since the pnao normalization + 260625 layout revision — no separate `**Impact**:` lead-in line), the caption reflecting the config values verbatim (each per-element backtick-wrapped, never hardcoded). Computed via `git diff --shortstat "$BASE...HEAD"` against the merge-base with and without the `:(exclude)` pathspec args (canonical math in `internal/impact`). - **Absent, `null`, or `[]`** — the impact block is omitted entirely; the rest of the PR body matches the no-block output byte-for-byte. - **No fab context** (`fab/project/config.yaml` does not exist) — the block is omitted silently, preserving `/git-pr`'s fab-optional behavior. - **True-impact pass returns zero** (every modified file falls inside an excluded path) — the entire block is omitted to avoid a misleading `+0 / −0` line. diff --git a/docs/memory/pipeline/execution-skills.md b/docs/memory/pipeline/execution-skills.md index 0a26d116..6e32727d 100644 --- a/docs/memory/pipeline/execution-skills.md +++ b/docs/memory/pipeline/execution-skills.md @@ -71,9 +71,9 @@ 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 table (`ID | Type | Confidence | Plan | Review`), the `**Pipeline**:` line (six fixed-order stages with ` ✓` per `done` stage and hyperlinked intake/apply labels), the optional `**Issues**:` line, and the `**Impact**:` block — 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). **Impact is one normalized table** (pnao, replacing the prior multi-form rendering): a single `Scope | + / − | Net` table (numeric columns right-aligned) carrying 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 an 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). 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 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. -**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 `ID` cell carries the 4-char change ID from `.status.yaml`; 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 Pipeline and above Impact. Missing fields show `—`. When the change cannot be resolved, `fab pr-meta` exits non-zero and the entire Meta block is omitted. +**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. ## Requirements @@ -366,13 +366,13 @@ Steps execute 1→3 for safety. If interrupted, re-run detects folder already in *Introduced by*: 260213-jc0u-split-archive-hydrate ### Unified PR Template with Conditional Field Population -**Decision**: `/git-pr` uses a single unified PR body template for all PR types. Fab-linked fields are conditionally populated based on artifact availability — whether the change resolves and artifacts exist — not on PR type. As of rj31 these fields live in a mechanically-rendered `## Meta` block (5-column table `ID | Type | Confidence | Plan | Review`, `**Pipeline**:` line, optional `**Issues**:` line, single-table `**Impact**:` block — normalized in pnao) produced by `fab pr-meta`, with `—` for unavailable cells; the whole block is omitted when the change can't be resolved. The agent-generated `## Summary` / `## Changes` prose stays type-agnostic too. +**Decision**: `/git-pr` uses a single unified PR body template for all PR types. Fab-linked fields are conditionally populated based on artifact availability — whether the change resolves and artifacts exist — not on PR type. As of rj31 these fields live in a mechanically-rendered `## Meta` block (5-column top table `Change ID | Type | Confidence | Plan | Review`, a self-labeling single-table `Impact | +/− | Net` block + `` caption, an optional `**Issues**:` line, and the `**Pipeline:**` line last — normalized in pnao, with the 260625 layout revision making Pipeline the final element) produced by `fab pr-meta`, with `—` for unavailable cells; the whole block is omitted when the change can't be resolved. The agent-generated `## Summary` / `## Changes` prose stays type-agnostic too. **Why**: A `test` or `docs` change that went through the full fab pipeline deserves the same quality signals as a `feat` change. Gating template richness on type hid real planning work and reduced reviewer confidence. The unified template always shows the same structure, reducing cognitive overhead. Mechanizing the Meta block (rj31) further guarantees the structure is byte-for-byte identical across runs. **Rejected**: Two-tier templates gated on type (hides work for non-feat types), keep type-gating but extend Tier 1 to all types (still requires two code paths), omit columns when empty (inconsistent table shape across PRs). For the rj31 mechanization: pushing the *whole* PR body into Go (would force Summary/Changes into mechanical text extraction, degrading prose quality), and a flags-only formatter (leaves the skill re-deriving git/gh inputs each run — partial drift remains). *Introduced by*: 260305-b0xs-unified-pr-template *Supersedes*: 260225-54vl-smart-git-pr-category-taxonomy (Two-Tier PR Templates with Type Resolution) *Updated by*: 260604-rj31-mechanical-pr-meta (the `## Meta` block — table, Pipeline, Issues, Impact — is now rendered by `fab pr-meta`, replacing the inlined Step 3c formatting prose; the former "Stats table" five columns are now `ID | Type | Confidence | Plan | Review`) -*Updated by*: 260625-pnao-normalize-pr-meta-impact-block (the Meta `**Impact**:` block is now a single `Scope | + / − | Net` table with the `raw / true / impl / tests / excluded` taxonomy and an italic provenance caption stamping the fab-kit binary version, replacing the prior three render shapes; `true` is always the post-exclude diff, fixing the "total flips meaning" bug) +*Updated by*: 260625-pnao-normalize-pr-meta-impact-block (the Meta Impact block is now a single self-labeling `Impact | +/− | Net` table with the `raw / true / impl / tests / excluded` taxonomy and a `` provenance caption stamping the fab-kit binary version, replacing the prior three render shapes; `true` is always the post-exclude diff, fixing the "total flips meaning" bug. The layout revision in the same change renamed the top-table header `ID` → `Change ID` (id backtick-wrapped), dropped the `**Impact**:` lead-in, and reordered the block to heading → top table → Impact + caption → optional Issues → `**Pipeline:**` last) ### Execution Stage Reset Preserves Artifacts and Checkboxes **Decision**: `/fab-continue apply` re-runs apply behavior starting from the first unchecked task. It does NOT uncheck all tasks. After qszh, `fab status reset apply` also preserves `plan.md` on disk — reset modifies `.status.yaml` state only; artifact files persist. The apply entry's plan-generation sub-step is idempotent on `plan.md` presence and is skipped when the file exists. To force plan regeneration the user MUST delete `plan.md` before re-running. diff --git a/docs/memory/pipeline/schemas.md b/docs/memory/pipeline/schemas.md index 4a2364f5..8c554dcc 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 table, `**Pipeline**:`, optional `**Issues**:`, and the `**Impact**:` block), replacing the inlined `/git-pr` Step 3c formatting prose. As of pnao the Impact block is a single normalized `Scope | + / − | Net` table 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 an 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 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. ## `.status.yaml` Identity Fields diff --git a/docs/specs/skills/SPEC-git-pr.md b/docs/specs/skills/SPEC-git-pr.md index ab965778..16f67dc8 100644 --- a/docs/specs/skills/SPEC-git-pr.md +++ b/docs/specs/skills/SPEC-git-pr.md @@ -106,14 +106,15 @@ Autonomously commits, pushes, and creates a draft GitHub PR. No prompts, no ques │ │ computes impact via internal/impact against the internal merge-base, │ │ resolves git/gh context (branch, owner/repo), and stamps the │ │ running binary version (main.version) itself; -│ │ emits the full table + **Pipeline** + optional **Issues** + -│ │ optional **Impact** as final markdown; -│ │ **Impact** = one normalized table (Scope | + / − | Net) 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 tests pair) + an italic -│ │ provenance caption (excludes via actual true_impact_exclude values, -│ │ never hardcoded · generated by fab-kit vX.Y.Z); +│ │ emits, in element order, the table + optional Impact + +│ │ 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 +│ │ tests pair) + a provenance caption (excludes via actual +│ │ true_impact_exclude values, never hardcoded · generated by +│ │ fab-kit vX.Y.Z); │ │ gh failure → plain-text Pipeline labels; missing merge-base or │ │ +0/−0 true → Impact block dropped; │ │ non-zero exit / empty stdout → Meta block omitted entirely) @@ -142,7 +143,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 the table, `**Pipeline**`, optional `**Issues**`, and optional `**Impact**` deterministically. The Impact section is one normalized table (`Scope \| + / − \| Net`, 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 an italic 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` 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. | ### Sub-agents diff --git a/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.history.jsonl b/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.history.jsonl index 077df05f..f59e9c54 100644 --- a/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.history.jsonl +++ b/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.history.jsonl @@ -9,3 +9,8 @@ {"action":"enter","driver":"fab-fff","event":"stage-transition","stage":"ship","ts":"2026-06-25T08:30:59Z"} {"action":"enter","driver":"git-pr","event":"stage-transition","stage":"review-pr","ts":"2026-06-25T08:32:46Z"} {"event":"review","result":"passed","ts":"2026-06-25T08:40:00Z"} +{"action":"re-entry","driver":"fab-fff","event":"stage-transition","stage":"apply","ts":"2026-06-25T09:09:55Z"} +{"action":"re-entry","driver":"fab-fff","event":"stage-transition","stage":"review","ts":"2026-06-25T09:20:07Z"} +{"action":"re-entry","driver":"fab-fff","event":"stage-transition","stage":"hydrate","ts":"2026-06-25T09:27:31Z"} +{"event":"review","result":"passed","ts":"2026-06-25T09:27:31Z"} +{"action":"re-entry","driver":"fab-fff","event":"stage-transition","stage":"ship","ts":"2026-06-25T09:30:56Z"} diff --git a/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.status.yaml b/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.status.yaml index 524ca680..7a067a6d 100644 --- a/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.status.yaml +++ b/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.status.yaml @@ -9,11 +9,11 @@ progress: apply: done review: done hydrate: done - ship: done - review-pr: done + ship: active + review-pr: pending plan: generated: true - task_count: 8 + task_count: 14 acceptance_count: 17 acceptance_completed: 0 confidence: @@ -30,27 +30,27 @@ confidence: disambiguation: 91.5 stage_metrics: intake: {started_at: "2026-06-25T08:10:13Z", driver: fab-new, iterations: 1, completed_at: "2026-06-25T08:14:47Z"} - apply: {started_at: "2026-06-25T08:14:47Z", driver: fab-fff, iterations: 1, completed_at: "2026-06-25T08:21:57Z"} - review: {started_at: "2026-06-25T08:21:57Z", driver: fab-fff, iterations: 1, completed_at: "2026-06-25T08:26:57Z"} - hydrate: {started_at: "2026-06-25T08:26:57Z", driver: fab-fff, iterations: 1, completed_at: "2026-06-25T08:30:59Z"} - ship: {started_at: "2026-06-25T08:30:59Z", driver: fab-fff, iterations: 1, completed_at: "2026-06-25T08:32:46Z"} - review-pr: {started_at: "2026-06-25T08:32:46Z", driver: git-pr, iterations: 1, completed_at: "2026-06-25T08:40:00Z"} + apply: {started_at: "2026-06-25T09:09:55Z", driver: fab-fff, iterations: 2, completed_at: "2026-06-25T09:20:07Z"} + review: {started_at: "2026-06-25T09:20:07Z", driver: fab-fff, iterations: 2, completed_at: "2026-06-25T09:27:31Z"} + hydrate: {started_at: "2026-06-25T09:27:31Z", driver: fab-fff, iterations: 2, completed_at: "2026-06-25T09:30:56Z"} + ship: {started_at: "2026-06-25T09:30:56Z", driver: fab-fff, iterations: 2} + review-pr: {iterations: 1} prs: - https://github.com/sahil87/fab-kit/pull/444 true_impact: - added: 0 - deleted: 0 - net: 0 + added: 716 + deleted: 111 + net: 605 excluding: - added: 0 - deleted: 0 - net: 0 + added: 215 + deleted: 96 + net: 119 tests: - added: 0 - deleted: 0 - net: 0 - computed_at: "2026-06-25T08:30:59Z" + added: 94 + deleted: 51 + net: 43 + computed_at: "2026-06-25T09:30:56Z" computed_at_stage: hydrate summary: Normalized the fab pr-meta Meta Impact block to a single Scope|+/−|Net table with the raw/true/impl/tests/excluded taxonomy and a binary-version provenance caption, fixing the total-flips-meaning bug # true_impact: lazily created on first apply-finish (no placeholder here). -last_updated: 2026-06-25T08:40:00Z +last_updated: 2026-06-25T09:30:56Z diff --git a/fab/changes/260625-pnao-normalize-pr-meta-impact-block/plan.md b/fab/changes/260625-pnao-normalize-pr-meta-impact-block/plan.md index a04ba2d1..02693038 100644 --- a/fab/changes/260625-pnao-normalize-pr-meta-impact-block/plan.md +++ b/fab/changes/260625-pnao-normalize-pr-meta-impact-block/plan.md @@ -64,6 +64,20 @@ The `clampNonNeg` + `clampAnnotation` behavior MUST be preserved for the `impl` - **WHEN** the `└ impl` row renders - **THEN** its displayed figures are clamped to non-negative AND a clamp annotation names the true negative value(s) +### prmeta: Layout revision (260625 follow-up) + +#### R8: Revised `## Meta` block layout (GitHub-verified) +A second-pass layout revision (verified to render on GitHub via the Markdown API) changes the rendered block as follows, on top of R1–R7: +- **Table header**: `ID` → `Change ID`. The id value is wrapped in backticks (`` `pnao` ``) ONLY when a real id is present; the `—` empty-fallback stays bare (no backticks). +- **Impact table header**: the `**Impact**:` lead-in line is REMOVED (the table self-labels). The first column header is `Scope` → `Impact`. The `+ / −` header becomes the compact `+/−` (no surrounding spaces). Right-alignment separator unchanged. +- **Caption**: italic `*…*` → `` (same content; excludes clause conditional; version always incl. `vdev`). +- **Element ordering**: the block reorders to `table → Impact-table + caption → optional Issues → Pipeline`. Pipeline becomes the LAST element; the optional Issues line sits just above Pipeline. Blank-line separators keep each table/paragraph distinct. +- `Render` stays a PURE function of `Data`; `Version` populated only in `cmd/fab/pr_meta.go`. Emphasis stays bold-only (no `style`/color HTML — sanitizer strips it). + +- **GIVEN** the full happy-path `Data` +- **WHEN** `Render` runs +- **THEN** the output is `## Meta` → `Change ID` table (backticked id) → `Impact` table (`Impact | +/− | Net`, no lead-in) → `` caption → (optional Issues) → `**Pipeline:**` + ### Documentation: sibling/mirror sweep #### R7: Skill + SPEC mirror prose reflects the new shape @@ -110,6 +124,15 @@ The observable `fab pr-meta` Impact output prose MUST be updated across the whol - [x] T007 [P] Update the `**Impact**` Output bullet in `src/kit/skills/_cli-fab.md` (line ~472) to the single-table + provenance-caption shape and the `raw/true/impl/tests/excluded` taxonomy; verify `docs/specs/skills/SPEC-_cli-fab.md` (inventory row only) stays accurate. - [x] T008 [P] Update the Meta-block Impact description in `src/kit/skills/git-pr.md` (Step 3, line ~242/251) and its mirror `docs/specs/skills/SPEC-git-pr.md` (lines ~110–114, ~141) to the new single-table + caption shape; retire all three-row/single-line/`← excludes`/`code (excluding …)` phrasings in the class. +### Phase 5: Layout revision (260625 follow-up) + +- [x] T009 `renderTable`: header `ID` → `Change ID`; wrap the id value in backticks only when a real id is present (bare `—` fallback). +- [x] T010 `renderImpact`: remove the `**Impact**:` lead-in line; first-column header `Scope` → `Impact`; `+ / −` header → compact `+/−`; keep right-align separator + all row logic. +- [x] T011 `impactCaption`: change italic `*…*` wrapper to ``; same content/conditionals. +- [x] T012 `Render`: reorder block to `table → Impact-table+caption → optional Issues → Pipeline` (Pipeline last, Issues just above it); mind blank-line separators so each table/paragraph renders distinctly. +- [x] T013 Update `prmeta_test.go` goldens to the new layout (new headers, no lead-in, `` caption, new element ordering incl. `TestRender_FullBlock`); run `go test ./internal/prmeta/... ./cmd/fab/...`, `gofmt -l`, `go vet ./...` clean. +- [x] T014 [P] Doc sweep for the revised layout: `_cli-fab.md`, `git-pr.md`, `SPEC-git-pr.md` (new `Change ID`/`Impact` headers, `+/−`, `` caption, new ordering); grep the class for stale `**Impact**:` lead-in / `Scope` column / `ID |` header / italic-caption / "Pipeline … then Impact" phrasings. + ## Execution Order - T001 → T002 (Version field before the renderer that uses it) diff --git a/src/go/fab/internal/prmeta/prmeta.go b/src/go/fab/internal/prmeta/prmeta.go index 2998c9e6..6beb1724 100644 --- a/src/go/fab/internal/prmeta/prmeta.go +++ b/src/go/fab/internal/prmeta/prmeta.go @@ -85,31 +85,36 @@ type Data struct { // Render assembles the complete `## Meta` block markdown for d. It is a pure // function: identical Data always yields identical output. The block always -// contains the heading, table, and Pipeline line; the Issues and Impact lines -// are conditional. +// contains the heading, table, and Pipeline line; the Issues and Impact blocks +// are conditional. Element order (260625 layout revision): heading → table → +// Impact-table + caption → optional Issues → Pipeline (last). Each table and +// paragraph is separated by a blank line so GitHub renders them as distinct +// elements. func Render(d Data) string { var b strings.Builder b.WriteString("## Meta\n\n") b.WriteString(renderTable(d)) - b.WriteString("\n") - b.WriteString(renderPipeline(d)) - if line := renderIssues(d); line != "" { - b.WriteString("\n\n") - b.WriteString(line) + if block := renderImpact(d); block != "" { + b.WriteString("\n") + b.WriteString(block) } - if line := renderImpact(d); line != "" { + if line := renderIssues(d); line != "" { b.WriteString("\n\n") b.WriteString(line) } + b.WriteString("\n\n") + b.WriteString(renderPipeline(d)) return b.String() } // renderTable renders the 5-column Meta table (header + separator + single row). +// A present id is wrapped in backticks (`pnao`); the empty-fallback `—` stays +// bare (no backticks) so the em-dash reads as a placeholder, not code. func renderTable(d Data) string { - id := d.ID - if id == "" { - id = "—" + id := "—" + if d.ID != "" { + id = "`" + d.ID + "`" } confidence := "—" @@ -118,8 +123,8 @@ func renderTable(d Data) string { } var b strings.Builder - b.WriteString("| ID | Type | Confidence | Plan | Review |\n") - b.WriteString("|----|------|-----------|------|--------|\n") + b.WriteString("| Change ID | Type | Confidence | Plan | Review |\n") + b.WriteString("|-----------|------|------------|------|--------|\n") fmt.Fprintf(&b, "| %s | %s | %s | %s | %s |\n", id, d.Type, confidence, planCell(d), reviewCell(d)) return b.String() @@ -168,9 +173,10 @@ func pluralCycle(n int) string { return "cycles" } -// renderPipeline renders the **Pipeline** line: the six stages in fixed order -// joined by " → ", with " ✓" appended after each done stage. intake/apply -// labels hyperlink to their blob URLs when available; the rest are plain text. +// renderPipeline renders the **Pipeline:** line (colon inside the bold span, +// per the 260625 layout): the six stages in fixed order joined by " → ", with +// " ✓" appended after each done stage. intake/apply labels hyperlink to their +// blob URLs when available; the rest are plain text. func renderPipeline(d Data) string { parts := make([]string, 0, len(pipelineStages)) for _, stage := range pipelineStages { @@ -190,7 +196,7 @@ func renderPipeline(d Data) string { } parts = append(parts, label) } - return "**Pipeline**: " + strings.Join(parts, " → ") + return "**Pipeline:** " + strings.Join(parts, " → ") } // renderIssues renders the **Issues** line, or "" when there are no issues. @@ -215,15 +221,15 @@ func renderIssues(d Data) string { // reshaping — so a reader comparing two PRs sees the same columns every time and // every label means exactly one thing (pnao). const ( - impactLeadIn = "**Impact**:" - impactHeader = "| Scope | + / − | Net |" - impactSepRow = "|---|--:|--:|" // Scope left-aligned; numeric cols right-aligned - nestedLabelPfx = "└ " // tree glyph prefixing the impl/tests rows + impactHeader = "| Impact | +/− | Net |" + impactSepRow = "|--------|----:|----:|" // Impact left-aligned; numeric cols right-aligned + nestedLabelPfx = "└ " // tree glyph prefixing the impl/tests rows ) -// renderImpact renders the **Impact** block — a single markdown table plus an -// italic provenance caption — or "" when the block must be omitted (no impact, -// or a +0/−0 `true` diff). +// renderImpact renders the Impact block — a single self-labeling markdown table +// (the first-column header is `Impact`, so no separate lead-in line is needed) +// plus a `` provenance caption — or "" when the block must be omitted (no +// 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 @@ -254,8 +260,6 @@ func renderImpact(d Data) string { raw := impact.Pair{Added: d.Impact.Added, Deleted: d.Impact.Deleted, Net: d.Impact.Net} var b strings.Builder - b.WriteString(impactLeadIn) - b.WriteString("\n\n") b.WriteString(impactHeader) b.WriteString("\n") b.WriteString(impactSepRow) @@ -294,13 +298,15 @@ func renderImpact(d Data) string { return b.String() } -// impactRow renders one `| Scope | +A/−B | +N |` table row. When bold is set, -// every cell is wrapped in `**…**` (the `true` row's emphasis — bold survives -// GitHub's Markdown sanitizer; row backgrounds / text color do not, pnao). A -// non-empty annotation (the impl-clamp note) is appended inside the Scope cell. +// impactRow renders one `| Impact | +A / −B | +N |` table row (the figure cell +// is spaced — `+A / −B` — per the 260625 authoritative template; only the +// column header stays compact `+/−`). When bold is set, every cell is wrapped +// in `**…**` (the `true` row's emphasis — bold survives GitHub's Markdown +// sanitizer; row backgrounds / text color do not, pnao). A non-empty annotation +// (the impl-clamp note) is appended inside the label cell. func impactRow(label string, p impact.Pair, bold bool, annotation string) string { scope := label + annotation - plusMinus := fmt.Sprintf("+%d/−%d", p.Added, p.Deleted) + plusMinus := fmt.Sprintf("+%d / −%d", p.Added, p.Deleted) net := formatNet(p.Net) if bold { scope = "**" + scope + "**" @@ -310,17 +316,19 @@ func impactRow(label string, p impact.Pair, bold bool, annotation string) string return fmt.Sprintf("| %s | %s | %s |\n", scope, plusMinus, net) } -// impactCaption renders the italic provenance caption co-locating the excludes +// impactCaption renders the `` provenance caption co-locating the excludes // note and the binary version stamp, e.g. -// `*excludes `fab/`, `docs/` · generated by fab-kit v2.6.6*`. The excludes -// clause is omitted when no excludes are configured; the version stamp is always -// present (dev builds render `fab-kit vdev` honestly). +// `excludes `fab/`, `docs/` · generated by fab-kit v2.6.6`. The +// excludes clause is omitted when no excludes are configured; the version stamp +// is always present (dev builds render `fab-kit vdev` honestly). `` is on +// GitHub's HTML allowlist (unlike style/class, which the sanitizer strips), so +// the caption renders as small text while emphasis elsewhere stays bold-only. func impactCaption(d Data) string { stamp := "generated by fab-kit v" + d.Version if len(d.Excludes) > 0 { - return fmt.Sprintf("*excludes %s · %s*", backtickList(d.Excludes), stamp) + return fmt.Sprintf("excludes %s · %s", backtickList(d.Excludes), stamp) } - return fmt.Sprintf("*%s*", stamp) + return fmt.Sprintf("%s", stamp) } func clampNonNeg(n int) int { diff --git a/src/go/fab/internal/prmeta/prmeta_test.go b/src/go/fab/internal/prmeta/prmeta_test.go index 6bcf6dcd..c12a601d 100644 --- a/src/go/fab/internal/prmeta/prmeta_test.go +++ b/src/go/fab/internal/prmeta/prmeta_test.go @@ -57,7 +57,7 @@ func TestRender_Table(t *testing.T) { { name: "complete plan, review done 2 cycles", mutate: func(d *Data) {}, - want: "| rj31 | feat | 4.2/5.0 | 8/8 tasks, 9/9 acceptance ✓ | ✓ 2 cycles |", + want: "| `rj31` | feat | 4.2/5.0 | 8/8 tasks, 9/9 acceptance ✓ | ✓ 2 cycles |", }, { name: "no plan, pending review", @@ -66,10 +66,10 @@ func TestRender_Table(t *testing.T) { d.ReviewState = "pending" d.ReviewIterations = 0 }, - want: "| rj31 | feat | 4.2/5.0 | — | — |", + want: "| `rj31` | feat | 4.2/5.0 | — | — |", }, { - name: "missing id and confidence", + name: "missing id and confidence (bare em-dash, no backticks)", mutate: func(d *Data) { d.ID = "" d.HasConfidence = false @@ -82,7 +82,7 @@ func TestRender_Table(t *testing.T) { d.TasksDone = 5 d.AcceptanceDone = 3 }, - want: "| rj31 | feat | 4.2/5.0 | 5/8 tasks, 3/9 acceptance | ✓ 2 cycles |", + want: "| `rj31` | feat | 4.2/5.0 | 5/8 tasks, 3/9 acceptance | ✓ 2 cycles |", }, { name: "review failed 1 cycle (singular)", @@ -90,14 +90,14 @@ func TestRender_Table(t *testing.T) { d.ReviewState = "failed" d.ReviewIterations = 1 }, - want: "| rj31 | feat | 4.2/5.0 | 8/8 tasks, 9/9 acceptance ✓ | ✗ 1 cycle |", + want: "| `rj31` | feat | 4.2/5.0 | 8/8 tasks, 9/9 acceptance ✓ | ✗ 1 cycle |", }, { name: "review done, no iteration count → bare check", mutate: func(d *Data) { d.ReviewIterations = 0 }, - want: "| rj31 | feat | 4.2/5.0 | 8/8 tasks, 9/9 acceptance ✓ | ✓ |", + want: "| `rj31` | feat | 4.2/5.0 | 8/8 tasks, 9/9 acceptance ✓ | ✓ |", }, } @@ -117,8 +117,8 @@ func TestRender_TableHeader(t *testing.T) { got := Render(baseData()) for _, want := range []string{ "## Meta\n\n", - "| ID | Type | Confidence | Plan | Review |\n", - "|----|------|-----------|------|--------|\n", + "| Change ID | Type | Confidence | Plan | Review |\n", + "|-----------|------|------------|------|--------|\n", } { if !strings.Contains(got, want) { t.Errorf("missing %q\nfull output:\n%s", want, got) @@ -130,7 +130,7 @@ func TestRender_TableHeader(t *testing.T) { func TestRender_Pipeline(t *testing.T) { t.Run("hyperlinked with done markers", func(t *testing.T) { got := renderPipeline(baseData()) - want := "**Pipeline**: [intake](https://github.com/o/r/blob/b/fab/changes/260604-rj31-mechanical-pr-meta/intake.md) ✓ → " + + want := "**Pipeline:** [intake](https://github.com/o/r/blob/b/fab/changes/260604-rj31-mechanical-pr-meta/intake.md) ✓ → " + "[apply](https://github.com/o/r/blob/b/fab/changes/260604-rj31-mechanical-pr-meta/plan.md) ✓ → " + "review ✓ → hydrate → ship → review-pr" if got != want { @@ -143,7 +143,7 @@ func TestRender_Pipeline(t *testing.T) { d.IntakeURL = "" d.ApplyURL = "" got := renderPipeline(d) - want := "**Pipeline**: intake ✓ → apply ✓ → review ✓ → hydrate → ship → review-pr" + want := "**Pipeline:** intake ✓ → apply ✓ → review ✓ → hydrate → ship → review-pr" if got != want { t.Errorf("pipeline =\n%q\nwant\n%q", got, want) } @@ -178,15 +178,17 @@ func TestRender_Issues(t *testing.T) { } }) - t.Run("issues line positioned between pipeline and impact", func(t *testing.T) { + t.Run("issues line positioned between impact and pipeline", func(t *testing.T) { d := baseData() d.Issues = []string{"DEV-1"} got := Render(d) - pipeIdx := strings.Index(got, "**Pipeline**:") + // 260625 layout: table → Impact → Issues → Pipeline (last). The Impact + // table self-labels via its `| Impact |` header (no `**Impact**:` lead-in). + impIdx := strings.Index(got, "| Impact | +/− | Net |") issIdx := strings.Index(got, "**Issues**:") - impIdx := strings.Index(got, "**Impact**:") - if !(pipeIdx < issIdx && issIdx < impIdx) { - t.Errorf("ordering wrong: pipeline=%d issues=%d impact=%d\n%s", pipeIdx, issIdx, impIdx, got) + pipeIdx := strings.Index(got, "**Pipeline:**") + if !(impIdx < issIdx && issIdx < pipeIdx) { + t.Errorf("ordering wrong: impact=%d issues=%d pipeline=%d\n%s", impIdx, issIdx, pipeIdx, got) } }) } @@ -195,21 +197,22 @@ func TestRender_Issues(t *testing.T) { // (excludes ± tests) combinations, the omission cases, the dev-version caption, // the row-drop rules, and the per-component impl clamp / annotation. func TestRender_Impact(t *testing.T) { - // Header + right-aligned separator shared by every non-omitted render. - const head = "**Impact**:\n\n" + - "| Scope | + / − | Net |\n" + - "|---|--:|--:|\n" + // Header + right-aligned separator shared by every non-omitted render. The + // 260625 layout drops the `**Impact**:` lead-in (the `| Impact |` header + // self-labels) and compacts the `+/−` header. + const head = "| Impact | +/− | Net |\n" + + "|--------|----:|----:|\n" 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). want := head + - "| raw | +142/−38 | +104 |\n" + - "| **true** | **+87/−38** | **+49** |\n" + - "| └ impl | +47/−38 | +9 |\n" + - "| └ tests | +40/−0 | +40 |\n" + - "\n*excludes `fab/`, `docs/` · generated by fab-kit v2.6.6*" + "| raw | +142 / −38 | +104 |\n" + + "| **true** | **+87 / −38** | **+49** |\n" + + "| └ impl | +47 / −38 | +9 |\n" + + "| └ tests | +40 / −0 | +40 |\n" + + "\nexcludes `fab/`, `docs/` · generated by fab-kit v2.6.6" if got != want { t.Errorf("impact =\n%q\nwant\n%q", got, want) } @@ -222,10 +225,10 @@ func TestRender_Impact(t *testing.T) { got := renderImpact(d) // true = raw = 142/38 (104); impl = 102/38 (64); tests = 40/0 (40). want := head + - "| **true** | **+142/−38** | **+104** |\n" + - "| └ impl | +102/−38 | +64 |\n" + - "| └ tests | +40/−0 | +40 |\n" + - "\n*generated by fab-kit v2.6.6*" + "| **true** | **+142 / −38** | **+104** |\n" + + "| └ impl | +102 / −38 | +64 |\n" + + "| └ tests | +40 / −0 | +40 |\n" + + "\ngenerated by fab-kit v2.6.6" if got != want { t.Errorf("impact =\n%q\nwant\n%q", got, want) } @@ -242,9 +245,9 @@ func TestRender_Impact(t *testing.T) { d.Impact.Tests = nil got := renderImpact(d) want := head + - "| raw | +142/−38 | +104 |\n" + - "| **true** | **+87/−38** | **+49** |\n" + - "\n*excludes `fab/`, `docs/` · generated by fab-kit v2.6.6*" + "| raw | +142 / −38 | +104 |\n" + + "| **true** | **+87 / −38** | **+49** |\n" + + "\nexcludes `fab/`, `docs/` · generated by fab-kit v2.6.6" if got != want { t.Errorf("impact =\n%q\nwant\n%q", got, want) } @@ -260,8 +263,8 @@ func TestRender_Impact(t *testing.T) { d.Excludes = nil got := renderImpact(d) want := head + - "| **true** | **+142/−38** | **+104** |\n" + - "\n*generated by fab-kit v2.6.6*" + "| **true** | **+142 / −38** | **+104** |\n" + + "\ngenerated by fab-kit v2.6.6" if got != want { t.Errorf("impact =\n%q\nwant\n%q", got, want) } @@ -271,7 +274,7 @@ func TestRender_Impact(t *testing.T) { d := baseData() d.Version = "dev" got := renderImpact(d) - if !strings.HasSuffix(got, "generated by fab-kit vdev*") { + if !strings.HasSuffix(got, "generated by fab-kit vdev") { t.Errorf("expected honest dev version stamp, got:\n%s", got) } }) @@ -303,7 +306,7 @@ func TestRender_Impact(t *testing.T) { d.Impact.Excluding = &impact.Pair{Added: 10, Deleted: 2, Net: 8} d.Impact.Tests = &impact.Pair{Added: 50, Deleted: 3, Net: 47} got := renderImpact(d) - if !strings.Contains(got, "| └ impl (clamped from net -39, added -40, deleted -1) | +0/−0 | +0 |") { + if !strings.Contains(got, "| └ impl (clamped from net -39, added -40, deleted -1) | +0 / −0 | +0 |") { t.Errorf("expected clamped impl row with annotation, got:\n%s", got) } }) @@ -324,16 +327,26 @@ func TestRender_FullBlock(t *testing.T) { d.LinearWorkspace = "acme" got := Render(d) + // 260625 layout order: heading → table → Impact table + caption → + // Issues → Pipeline (last), each block blank-line separated. for _, want := range []string{ - "## Meta\n\n| ID | Type", - "|\n\n**Pipeline**:", // table ends, blank line, pipeline - "\n\n**Issues**:", - "\n\n**Impact**:", + "## Meta\n\n| Change ID | Type", + "|\n\n| Impact | +/− | Net |", // table ends, blank line, Impact table + "\n\n**Issues**:", // caption ends, blank line, issues + "\n\n**Pipeline:**", // issues end, blank line, pipeline (last) } { if !strings.Contains(got, want) { t.Errorf("missing structural marker %q\nfull output:\n%s", want, got) } } + + // Hard ordering assertion: Impact precedes Issues precedes Pipeline. + impIdx := strings.Index(got, "| Impact | +/− | Net |") + issIdx := strings.Index(got, "**Issues**:") + pipeIdx := strings.Index(got, "**Pipeline:**") + if !(impIdx < issIdx && issIdx < pipeIdx) { + t.Errorf("ordering wrong: impact=%d issues=%d pipeline=%d\n%s", impIdx, issIdx, pipeIdx, got) + } } // TestHasConfidenceBlock covers the presence check that restores the old Step 3c diff --git a/src/kit/skills/_cli-fab.md b/src/kit/skills/_cli-fab.md index 4f9a41b0..f7be97c7 100644 --- a/src/kit/skills/_cli-fab.md +++ b/src/kit/skills/_cli-fab.md @@ -465,17 +465,17 @@ Self-contained data sourcing — the command reads everything else itself: - Impact math: reuses `internal/impact` (`ComputeForRepo`) against the merge-base of HEAD vs `origin/main` (falling back to `origin/master`), computed internally. - Git/`gh` context: branch (`git branch --show-current`) and owner/repo (`gh repo view --json nameWithOwner`) for blob URLs. -Output — the exact `## Meta` block markdown: -- The 5-column table (`ID | Type | Confidence | Plan | Review`) with `—` fallbacks, a ` ✓` Plan completion suffix when both task and acceptance pairs are complete, and a `✓/✗ {N} cycle{s}` Review cell. -- `**Pipeline**`: 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. -- `**Issues**` (only when `--issues` is non-empty): Linear-linked when `project.linear_workspace` is set, bare comma-joined IDs otherwise; positioned between Pipeline and Impact. -- `**Impact**`: a single normalized markdown table (`Scope | + / − | Net`, numeric columns right-aligned, Net kept on every row) followed by an italic 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 (GitHub's Markdown sanitizer strips row backgrounds and text color). +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). +- `**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. Exit codes: - `0` — success; the `## Meta` block on stdout. - non-zero — no fab context (change unresolved or `.status.yaml` absent); nothing on stdout. `/git-pr` treats this (or empty stdout) as "omit the Meta block", matching the legacy `{has_fab} = false` path. -Graceful degradation: an unreachable `gh` leaves owner/repo empty so Pipeline stages render as plain-text labels (never a hard error); a missing/failed merge-base drops only the `**Impact**` line. +Graceful degradation: an unreachable `gh` leaves owner/repo empty so Pipeline stages render as plain-text labels (never a hard error); a missing/failed merge-base drops only the Impact block. Consumers: `/git-pr` Step 3c (renders the PR body `## Meta` block, pasted verbatim). diff --git a/src/kit/skills/git-pr.md b/src/kit/skills/git-pr.md index e66448dd..373e267d 100644 --- a/src/kit/skills/git-pr.md +++ b/src/kit/skills/git-pr.md @@ -239,7 +239,7 @@ Print: ` ✓ push — origin/` **Fab context** comes from Step 0: `{has_fab}`, `{name}`, `{has_intake}` (controls Summary/Changes sourcing below). Do NOT re-run `fab change resolve` — reuse the Step 0 resolution. - **Render the `## Meta` block** (only when `{has_fab}`): delegate the entire Meta block (table + `**Pipeline**` + optional `**Issues**` + optional `**Impact**`) to `fab pr-meta`, which reads `.status.yaml`, parses `plan.md` checkboxes, reads `fab/project/config.yaml` (`true_impact_exclude`, `test_paths`, `project.linear_workspace`), computes the impact math, resolves git/`gh` context (branch, owner/repo, merge-base), and stamps the running binary version, itself. The `**Impact**` section renders as a single normalized table (`Scope | + / − | Net`) with the locked `raw / true / impl / tests / excluded` taxonomy (`true` is always the post-exclude diff) plus an italic provenance caption co-locating the excludes note and the `generated by fab-kit vX.Y.Z` stamp. Pass the Step 0 `{name}`, the resolved `{type}` (from Step 0b), and the space-joined `{issues}` (from Step 1): + **Render the `## Meta` block** (only when `{has_fab}`): delegate the entire Meta block (table + optional Impact + optional `**Issues**` + `**Pipeline:**`, in that element order) to `fab pr-meta`, which reads `.status.yaml`, parses `plan.md` checkboxes, reads `fab/project/config.yaml` (`true_impact_exclude`, `test_paths`, `project.linear_workspace`), computes the impact math, resolves git/`gh` context (branch, owner/repo, merge-base), and stamps the running binary version, itself. The Impact section renders as a single normalized table whose first-column header is `Impact` (self-labeling — no lead-in line), columns `Impact | +/− | Net`, with the locked `raw / true / impl / tests / excluded` taxonomy (`true` is always the post-exclude diff) plus a `` provenance caption co-locating the excludes note and the `generated by fab-kit vX.Y.Z` stamp. Pass the Step 0 `{name}`, the resolved `{type}` (from Step 0b), and the space-joined `{issues}` (from Step 1): ```bash META=$(fab pr-meta "{name}" --type {type} --issues "{issues}" 2>/dev/null) || META="" @@ -248,7 +248,7 @@ Print: ` ✓ push — origin/` - If exit 0 and `META` is non-empty: the `## Meta` block is `$META` **verbatim** — do not reformat, re-wrap, or re-derive any of it. - If exit non-zero or `META` is empty (no fab context, change unresolved, or `.status.yaml` absent): omit the `## Meta` block entirely, exactly as the legacy `{has_fab} = false` path did. - `fab pr-meta` degrades gracefully on its own: an unreachable `gh` falls back to plain-text Pipeline labels, and a missing/failed merge-base or a `+0/−0` `true` diff drops only the `**Impact**` block — none of these break the block or the PR. + `fab pr-meta` degrades gracefully on its own: an unreachable `gh` falls back to plain-text Pipeline labels, and a missing/failed merge-base or a `+0/−0` `true` diff drops only the Impact block — none of these break the block or the PR. **Assemble the body** in this exact order: From 6b906329418d8f1d43017af07bde6ac0230e4c2e Mon Sep 17 00:00:00 2001 From: Sahil Ahuja Date: Thu, 25 Jun 2026 15:01:56 +0530 Subject: [PATCH 7/7] Update ship status and record PR URL --- .../.history.jsonl | 1 + .../.status.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.history.jsonl b/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.history.jsonl index f59e9c54..2c372c84 100644 --- a/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.history.jsonl +++ b/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.history.jsonl @@ -14,3 +14,4 @@ {"action":"re-entry","driver":"fab-fff","event":"stage-transition","stage":"hydrate","ts":"2026-06-25T09:27:31Z"} {"event":"review","result":"passed","ts":"2026-06-25T09:27:31Z"} {"action":"re-entry","driver":"fab-fff","event":"stage-transition","stage":"ship","ts":"2026-06-25T09:30:56Z"} +{"action":"re-entry","driver":"git-pr","event":"stage-transition","stage":"review-pr","ts":"2026-06-25T09:31:52Z"} diff --git a/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.status.yaml b/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.status.yaml index 7a067a6d..6f9d49b4 100644 --- a/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.status.yaml +++ b/fab/changes/260625-pnao-normalize-pr-meta-impact-block/.status.yaml @@ -9,8 +9,8 @@ progress: apply: done review: done hydrate: done - ship: active - review-pr: pending + ship: done + review-pr: active plan: generated: true task_count: 14 @@ -33,8 +33,8 @@ stage_metrics: apply: {started_at: "2026-06-25T09:09:55Z", driver: fab-fff, iterations: 2, completed_at: "2026-06-25T09:20:07Z"} review: {started_at: "2026-06-25T09:20:07Z", driver: fab-fff, iterations: 2, completed_at: "2026-06-25T09:27:31Z"} hydrate: {started_at: "2026-06-25T09:27:31Z", driver: fab-fff, iterations: 2, completed_at: "2026-06-25T09:30:56Z"} - ship: {started_at: "2026-06-25T09:30:56Z", driver: fab-fff, iterations: 2} - review-pr: {iterations: 1} + ship: {started_at: "2026-06-25T09:30:56Z", driver: fab-fff, iterations: 2, completed_at: "2026-06-25T09:31:52Z"} + review-pr: {started_at: "2026-06-25T09:31:52Z", driver: git-pr, iterations: 2} prs: - https://github.com/sahil87/fab-kit/pull/444 true_impact: @@ -53,4 +53,4 @@ true_impact: computed_at_stage: hydrate summary: Normalized the fab pr-meta Meta Impact block to a single Scope|+/−|Net table with the raw/true/impl/tests/excluded taxonomy and a binary-version provenance caption, fixing the total-flips-meaning bug # true_impact: lazily created on first apply-finish (no placeholder here). -last_updated: 2026-06-25T09:30:56Z +last_updated: 2026-06-25T09:31:52Z