Friction: aggregate recompute at end of run (.pi-loop/friction/) + console Friction: line #229

Closed
opened 2026-08-16 22:12:22 +00:00 by david · 1 comment
Owner

Context

Cross-run learning (the primary purpose of the friction feature) needs a derived aggregate that is a pure function of the run directories — recomputed from scratch at the end of every run (success or failure), idempotent, and resume-safe by construction (no incremental merge state, no dedup bugs). The aggregate is the primary interface for the external analysis tool (capture now, defer the action — see docs/adr/016-friction-logging.md), so it carries a schemaVersion and stable machine-first JSON.

Depends on the foundation issue (artifact service, validator, renderer) and the implement/remediate wiring issues (they produce the per-run friction.json files the aggregator scans).

Aggregate contract

Derived files (recomputed, never hand-edited):

  • .pi-loop/friction/friction.json:
{
  "schemaVersion": 1,
  "generatedAt": "ISO-8601",
  "runs": 3,                          // run dirs that contained a valid friction.json
  "totalEntries": 7,
  "byCategory": { "build-failure": 3, "missing-docs": 2, "other": 2 },
  "byStage": { "implement": 5, "remediate": 2 },
  "byModel": { "anthropic/claude-opus-4-5": 7 },
  "recentEntries": [
    // last N entries (N = 20), newest first, each with runId + stage + recordedAt + category + description
  ]
}
  • .pi-loop/friction/FRICTION.md — human-readable summary: per-category counts (table or list), per-stage counts, then the recent entries rendered as blocks.

Semantics:

  • Input: scan .pi-loop/runs/*/friction.json (read + validate each; skip invalid files with a warning — never throw).
  • Idempotent: running twice with the same run dirs yields byte-identical output (deterministic ordering: recentEntries sorted by recordedAt desc, ties broken by runId asc; byCategory/byStage/byModel keys sorted).
  • Resume-safe: partial run dirs (a run that crashed before writing friction) are simply absent — no merge state to corrupt.
  • Non-gating: the aggregate is never used for resume stage-skip decisions and an aggregation failure must never fail the run.

Changes

  1. src/friction/services/aggregateFriction.ts (new) — aggregateFriction(runsRoot: string, aggregateDir: string): void:

    • Enumerate .pi-loop/runs/*/friction.json (directories only; stable sorted order).
    • Read + validateFrictionArtifact each; skip invalid with a warning.
    • Merge into the aggregate schema (counts, recent entries with runId + stage, generatedAt).
    • Write .pi-loop/friction/friction.json (pretty JSON) + .pi-loop/friction/FRICTION.md (via a summary renderer).
    • mkdir -p the aggregate dir (use fs.mkdirSync(dir, { recursive: true })).
    • Deterministic, no LLM, unit-testable. Accept runsRoot/aggregateDir as parameters (callers derive them from the repo root) so tests can use temp dirs.
  2. src/friction/helpers/renderFrictionSummaryMd.ts (new) — renderFrictionSummaryMd(aggregate): string: # Friction Summary title, generatedAt + runs + totalEntries line, per-category table (Category | Count), per-stage line, per-model line, then "## Recent entries" blocks (each: runId, stage, recordedAt, category, description, impact, resolution, ref if present), or "No friction recorded yet." when totalEntries === 0.

  3. src/orchestrator/services/runPipeline.ts — in the finalization path that runs on both success and failure (after the final summary is built), call aggregateFriction(runsRoot, frictionAggregateDir) inside a try/catch that logs and never throws (non-gating). Do not add it to any resume stage-skip logic. Ensure the failure path (e.g. agent failure with error.json) also recomputes so partial-run friction is captured.

  4. src/orchestrator/helpers/buildFinalSummary.ts — add a Friction: line to the final summary:

    • Friction: 7 events (3 categories) when totalEntries > 0 (categories = number of keys in byCategory);
    • Friction: no events when totalEntries === 0;
    • Friction: aggregate unavailable when the recompute failed or the aggregate is missing.

Acceptance criteria

  • After every run (success and failure), .pi-loop/friction/friction.json + FRICTION.md exist and validate against the aggregate schema.
  • Recomputing twice with identical run dirs produces identical file contents.
  • A run dir with an invalid/missing friction.json is skipped without throwing; other runs still aggregate.
  • The final summary prints the Friction: line in all three states.
  • Friction never surfaces in the MR description or Jira write-back (no changes to buildMrDescription / write-back code).
  • npm run lint and npm test green.

Unit tests (add alongside the seams)

  • aggregateFriction.test.ts — empty runs dir; single run; multiple runs; invalid friction.json skipped; per-category/per-stage/per-model counts; recentEntries ordering (recordedAt desc, runId asc tiebreak); idempotency (run twice → identical bytes); resume-safety (partial dirs); schemaVersion field; summary renderer (entries + empty).
  • runPipeline.test.ts — recompute hook called on success path and on failure path; aggregation failure does not fail the run (non-gating); Friction: line in the summary (with events / no events / unavailable).

Dependencies

  • Foundation issue (artifact service, validator) merged first.
  • Implement + remediate wiring issues merged (the aggregator scans the per-run artifacts they write; until then the aggregate is empty — the code still works).
  • Uses the existing runPipeline injectable seams for tests.
## Context Cross-run learning (the primary purpose of the friction feature) needs a **derived aggregate** that is a pure function of the run directories — recomputed from scratch at the end of every run (success **or** failure), idempotent, and resume-safe by construction (no incremental merge state, no dedup bugs). The aggregate is the **primary interface for the external analysis tool** (capture now, defer the action — see `docs/adr/016-friction-logging.md`), so it carries a `schemaVersion` and stable machine-first JSON. Depends on the **foundation** issue (artifact service, validator, renderer) and the **implement/remediate wiring** issues (they produce the per-run `friction.json` files the aggregator scans). ## Aggregate contract **Derived files (recomputed, never hand-edited):** - `.pi-loop/friction/friction.json`: ```jsonc { "schemaVersion": 1, "generatedAt": "ISO-8601", "runs": 3, // run dirs that contained a valid friction.json "totalEntries": 7, "byCategory": { "build-failure": 3, "missing-docs": 2, "other": 2 }, "byStage": { "implement": 5, "remediate": 2 }, "byModel": { "anthropic/claude-opus-4-5": 7 }, "recentEntries": [ // last N entries (N = 20), newest first, each with runId + stage + recordedAt + category + description ] } ``` - `.pi-loop/friction/FRICTION.md` — human-readable summary: per-category counts (table or list), per-stage counts, then the recent entries rendered as blocks. **Semantics:** - Input: scan `.pi-loop/runs/*/friction.json` (read + validate each; skip invalid files with a warning — never throw). - Idempotent: running twice with the same run dirs yields byte-identical output (deterministic ordering: `recentEntries` sorted by `recordedAt` desc, ties broken by runId asc; `byCategory`/`byStage`/`byModel` keys sorted). - Resume-safe: partial run dirs (a run that crashed before writing friction) are simply absent — no merge state to corrupt. - Non-gating: the aggregate is never used for resume stage-skip decisions and an aggregation failure must never fail the run. ## Changes 1. **`src/friction/services/aggregateFriction.ts`** (new) — `aggregateFriction(runsRoot: string, aggregateDir: string): void`: - Enumerate `.pi-loop/runs/*/friction.json` (directories only; stable sorted order). - Read + `validateFrictionArtifact` each; skip invalid with a warning. - Merge into the aggregate schema (counts, recent entries with runId + stage, `generatedAt`). - Write `.pi-loop/friction/friction.json` (pretty JSON) + `.pi-loop/friction/FRICTION.md` (via a summary renderer). - `mkdir -p` the aggregate dir (use `fs.mkdirSync(dir, { recursive: true })`). - Deterministic, no LLM, unit-testable. Accept `runsRoot`/`aggregateDir` as parameters (callers derive them from the repo root) so tests can use temp dirs. 2. **`src/friction/helpers/renderFrictionSummaryMd.ts`** (new) — `renderFrictionSummaryMd(aggregate): string`: `# Friction Summary` title, `generatedAt` + `runs` + `totalEntries` line, per-category table (Category | Count), per-stage line, per-model line, then "## Recent entries" blocks (each: runId, stage, recordedAt, category, description, impact, resolution, ref if present), or "_No friction recorded yet._" when `totalEntries === 0`. 3. **`src/orchestrator/services/runPipeline.ts`** — in the finalization path that runs on **both success and failure** (after the final summary is built), call `aggregateFriction(runsRoot, frictionAggregateDir)` inside a try/catch that logs and never throws (non-gating). Do **not** add it to any resume stage-skip logic. Ensure the failure path (e.g. agent failure with `error.json`) also recomputes so partial-run friction is captured. 4. **`src/orchestrator/helpers/buildFinalSummary.ts`** — add a `Friction:` line to the final summary: - `Friction: 7 events (3 categories)` when `totalEntries > 0` (categories = number of keys in `byCategory`); - `Friction: no events` when `totalEntries === 0`; - `Friction: aggregate unavailable` when the recompute failed or the aggregate is missing. ## Acceptance criteria - After every run (success **and** failure), `.pi-loop/friction/friction.json` + `FRICTION.md` exist and validate against the aggregate schema. - Recomputing twice with identical run dirs produces identical file contents. - A run dir with an invalid/missing `friction.json` is skipped without throwing; other runs still aggregate. - The final summary prints the `Friction:` line in all three states. - Friction never surfaces in the MR description or Jira write-back (no changes to `buildMrDescription` / write-back code). - `npm run lint` and `npm test` green. ## Unit tests (add alongside the seams) - `aggregateFriction.test.ts` — empty runs dir; single run; multiple runs; invalid friction.json skipped; per-category/per-stage/per-model counts; `recentEntries` ordering (recordedAt desc, runId asc tiebreak); idempotency (run twice → identical bytes); resume-safety (partial dirs); `schemaVersion` field; summary renderer (entries + empty). - `runPipeline.test.ts` — recompute hook called on success path and on failure path; aggregation failure does not fail the run (non-gating); `Friction:` line in the summary (with events / no events / unavailable). ## Dependencies - Foundation issue (artifact service, validator) merged first. - Implement + remediate wiring issues merged (the aggregator scans the per-run artifacts they write; until then the aggregate is empty — the code still works). - Uses the existing `runPipeline` injectable seams for tests.
david closed this issue 2026-08-17 02:51:17 +00:00
Author
Owner

pi-loop opened and merged a pull request for this issue: #244

pi-loop opened and merged a pull request for this issue: https://git.excelera.net/david/pi-loop/pulls/244
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
david/pi-loop#229
No description provided.