fapony 0.1.3 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -46
- package/fapony.ts +35 -2
- package/package.json +3 -2
- package/skill/move-to-done/SKILL.md +18 -28
- package/skill/plan-with-pony/SKILL.md +8 -3
- package/src/analyze.ts +175 -5
- package/src/conventions-seed.ts +3 -3
- package/src/db/defaults.ts +13 -5
- package/src/db/getters.ts +11 -9
- package/src/db/load.ts +2 -2
- package/src/db/store.ts +0 -75
- package/src/db/types.ts +2 -4
- package/src/debt.ts +176 -36
- package/src/digest/collect.ts +19 -2
- package/src/digest/text.ts +25 -0
- package/src/hook.ts +665 -24
- package/src/init-mem.ts +123 -37
- package/src/init.ts +57 -39
- package/src/install/claude.ts +44 -8
- package/src/install/codex.ts +105 -13
- package/src/install/opencode.ts +169 -5
- package/src/install.ts +2 -1
- package/src/lint-baseline.ts +3 -8
- package/src/mcp/evidence.ts +15 -3
- package/src/mcp/tools/index.ts +97 -185
- package/src/mcp/tools/mem.ts +153 -11
- package/src/mcp/transport.ts +5 -13
- package/src/mem/commands/read.ts +448 -0
- package/src/mem/commands/where.ts +56 -0
- package/{templates → src}/mem/commands/write.ts +12 -5
- package/src/mem/index.ts +144 -0
- package/src/mem/store.ts +350 -0
- package/src/memory.ts +238 -40
- package/src/plan-seed.ts +26 -6
- package/src/review-seed.ts +19 -0
- package/src/setup.ts +7 -8
- package/src/stats/data.ts +40 -104
- package/src/stats/format.ts +8 -9
- package/src/stats/index.ts +0 -1
- package/templates/PLAN.md +1 -0
- package/src/mcp/tools/context.ts +0 -66
- package/src/mcp/tools/plans.ts +0 -255
- package/src/mcp/tools/stats.ts +0 -96
- package/templates/mem/commands/read.ts +0 -194
- package/templates/mem/commands/selftest.ts +0 -450
- package/templates/mem/mem.ts +0 -68
- package/templates/mem/store.ts +0 -285
- /package/{templates → src}/mem/commands/plan.ts +0 -0
- /package/{templates → src}/mem/commands/rotate.ts +0 -0
- /package/{templates → src}/mem/render.ts +0 -0
- /package/{templates → src}/mem/selectors.ts +0 -0
package/src/debt.ts
CHANGED
|
@@ -18,18 +18,21 @@
|
|
|
18
18
|
//
|
|
19
19
|
// Read-only stdout: no file writes, no state.db, no cache (rule 5b).
|
|
20
20
|
|
|
21
|
-
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
22
|
-
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
21
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
22
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
23
23
|
import { collectSourceFiles } from "./analyze.js";
|
|
24
|
+
import {
|
|
25
|
+
CONVENTIONS_FILE,
|
|
26
|
+
CONVENTIONS_FILENAME,
|
|
27
|
+
FAPONY_DIR,
|
|
28
|
+
} from "./db/defaults.js";
|
|
24
29
|
import { openDb } from "./db/index.js";
|
|
25
|
-
import {
|
|
30
|
+
import { readMemLog, resolveMemDir } from "./memory.js";
|
|
26
31
|
|
|
27
32
|
// A stale regex matching more than this many files is not a convention — it is
|
|
28
33
|
// a broken/wide regex (stale="e" would flag the repo). SPEC §6: drop the entry
|
|
29
34
|
// and say so, never report 600 files.
|
|
30
35
|
const DEBT_FILE_CAP = 250;
|
|
31
|
-
// Same cap for the file list printed per convention — a wall is not an answer.
|
|
32
|
-
const LIST_SHOWN = 40;
|
|
33
36
|
|
|
34
37
|
export interface Convention {
|
|
35
38
|
id: string;
|
|
@@ -51,13 +54,16 @@ export interface Convention {
|
|
|
51
54
|
// --- conventions.json resolution (same guess as the mem log, SPEC §2.1) ---
|
|
52
55
|
|
|
53
56
|
export function resolveConventionsPath(worktree: string): string | null {
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
// Conventions live in the same .fapony/ dir as the mem log — derive from
|
|
58
|
+
// the resolved mem dir so both resolvers cannot drift apart.
|
|
59
|
+
const memDir = resolveMemDir(worktree);
|
|
60
|
+
const base = memDir ? join(memDir, "..") : join(worktree, FAPONY_DIR);
|
|
61
|
+
const app = join(base, CONVENTIONS_FILENAME);
|
|
56
62
|
if (existsSync(app)) return app;
|
|
57
63
|
// Monorepo where the app has not scaffolded .fapony/ yet, and single repos
|
|
58
64
|
// that ran `fapony init` at the root — the root file still scopes fine
|
|
59
65
|
// because every `where` is repo-relative.
|
|
60
|
-
const root = join(worktree,
|
|
66
|
+
const root = join(worktree, CONVENTIONS_FILE);
|
|
61
67
|
return existsSync(root) ? root : null;
|
|
62
68
|
}
|
|
63
69
|
|
|
@@ -295,7 +301,7 @@ export function debtScan(
|
|
|
295
301
|
// Stop counting a runaway regex early — the entry will be dropped.
|
|
296
302
|
if (cur.length > DEBT_FILE_CAP) tooBroad.set(c.conv.id, cur.length);
|
|
297
303
|
}
|
|
298
|
-
if (c.okRe
|
|
304
|
+
if (c.okRe?.test(content)) {
|
|
299
305
|
moved.set(c.conv.id, (moved.get(c.conv.id) ?? 0) + 1);
|
|
300
306
|
}
|
|
301
307
|
}
|
|
@@ -509,41 +515,99 @@ export function formatPromotions(promotions: Promotion[]): string[] {
|
|
|
509
515
|
|
|
510
516
|
// --- Formatting ---
|
|
511
517
|
|
|
512
|
-
|
|
518
|
+
// Zone grouping: a zone is a file's *directory*, capped at this many leading
|
|
519
|
+
// segments — never a fixed-depth prefix of the path (which would cut into the
|
|
520
|
+
// filename) and never the filename itself. SPEC §4 shows zones at depth 5
|
|
521
|
+
// (`apps/mdl/src/server/services`) and depth 3 (`packages/cache/src`) in the
|
|
522
|
+
// same report, so the cap must follow the directory, not a constant.
|
|
523
|
+
const ZONE_DEPTH = 5;
|
|
524
|
+
// Default cap on zones shown per convention — more than this is a wall, not an answer.
|
|
525
|
+
const ZONE_CAP = 6;
|
|
526
|
+
|
|
527
|
+
/** The zone of a file: its directory path, capped at `depth` segments. */
|
|
528
|
+
function zoneOf(file: string, depth: number): string {
|
|
529
|
+
const parts = dirname(file)
|
|
530
|
+
.split("/")
|
|
531
|
+
.filter((p) => p && p !== ".");
|
|
532
|
+
return parts.slice(0, depth).join("/") || ".";
|
|
533
|
+
}
|
|
513
534
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
535
|
+
/** Group files by their directory zone (see `zoneOf`). */
|
|
536
|
+
function groupFilesByZone(
|
|
537
|
+
files: string[],
|
|
538
|
+
depth: number,
|
|
539
|
+
): Map<string, string[]> {
|
|
540
|
+
const zones = new Map<string, string[]>();
|
|
517
541
|
for (const f of files) {
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
} else {
|
|
523
|
-
cur = piece;
|
|
524
|
-
}
|
|
542
|
+
const zone = zoneOf(f, depth);
|
|
543
|
+
const cur = zones.get(zone) ?? [];
|
|
544
|
+
cur.push(f);
|
|
545
|
+
zones.set(zone, cur);
|
|
525
546
|
}
|
|
526
|
-
|
|
527
|
-
return
|
|
547
|
+
// Sort zones by file count descending, then alphabetically
|
|
548
|
+
return new Map(
|
|
549
|
+
[...zones.entries()].sort((a, b) => {
|
|
550
|
+
const d = b[1].length - a[1].length;
|
|
551
|
+
return d !== 0 ? d : a[0].localeCompare(b[0]);
|
|
552
|
+
}),
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** Escape a regex source for use in a shell grep command. */
|
|
557
|
+
function shellEscapeRe(src: string): string {
|
|
558
|
+
return src.replace(/'/g, "'\\''");
|
|
528
559
|
}
|
|
529
560
|
|
|
530
|
-
export function formatDebt(report: DebtReport): string {
|
|
561
|
+
export function formatDebt(report: DebtReport, showAll = false): string {
|
|
531
562
|
const lines: string[] = [];
|
|
532
563
|
lines.push(
|
|
533
564
|
`fapony debt — ${report.entries.length + report.declared.length + report.checkedCount} convention(s), ` +
|
|
534
565
|
`${report.scannedFiles} files scanned, ${report.ms}ms — derived fresh, not stored`,
|
|
535
566
|
);
|
|
536
567
|
for (const e of report.entries) {
|
|
537
|
-
const moved =
|
|
538
|
-
|
|
568
|
+
const moved =
|
|
569
|
+
e.movedCount !== null && e.files.length > 0
|
|
570
|
+
? ` · moved ${e.movedCount} (${Math.round((e.movedCount / (e.files.length + e.movedCount)) * 100)}%)`
|
|
571
|
+
: e.movedCount !== null
|
|
572
|
+
? ` · moved ${e.movedCount}`
|
|
573
|
+
: "";
|
|
574
|
+
lines.push(`\n${e.conv.id} — ${e.conv.rule}`);
|
|
575
|
+
// Show the patterns actually used
|
|
576
|
+
const patterns: string[] = [];
|
|
577
|
+
if (e.conv.stale) patterns.push(`stale: ${e.conv.stale}`);
|
|
578
|
+
if (e.conv.ok) patterns.push(`ok: ${e.conv.ok}`);
|
|
579
|
+
if (e.conv.guard) patterns.push(`guard: ${e.conv.guard}`);
|
|
580
|
+
patterns.push(`where ${e.conv.where}`);
|
|
581
|
+
lines.push(` ${patterns.join(" · ")}`);
|
|
539
582
|
if (e.files.length === 0) {
|
|
540
583
|
lines.push(` debt 0${moved} — clean`);
|
|
541
584
|
continue;
|
|
542
585
|
}
|
|
543
|
-
lines.push(` debt ${e.files.length}${moved}
|
|
544
|
-
|
|
545
|
-
if (e.
|
|
546
|
-
lines.push(`
|
|
586
|
+
lines.push(` debt ${e.files.length}${moved}`);
|
|
587
|
+
// Verify command derived from stale
|
|
588
|
+
if (e.conv.stale) {
|
|
589
|
+
lines.push(` verify: grep -rn '${shellEscapeRe(e.conv.stale)}' <zone>`);
|
|
590
|
+
}
|
|
591
|
+
// Zone grouping
|
|
592
|
+
const zones = groupFilesByZone(e.files, ZONE_DEPTH);
|
|
593
|
+
const zoneEntries = [...zones.entries()];
|
|
594
|
+
const cap = showAll
|
|
595
|
+
? zoneEntries.length
|
|
596
|
+
: Math.min(zoneEntries.length, ZONE_CAP);
|
|
597
|
+
let totalCapped = 0;
|
|
598
|
+
for (let i = 0; i < cap; i++) {
|
|
599
|
+
const [zone, zoneFiles] = zoneEntries[i];
|
|
600
|
+
const pad = " ".repeat(Math.max(0, 42 - zone.length));
|
|
601
|
+
lines.push(`\n ${zone}${pad}${zoneFiles.length} ไฟล์`);
|
|
602
|
+
lines.push(` ${zoneFiles.map((f) => f.split("/").pop()).join(" · ")}`);
|
|
603
|
+
totalCapped += zoneFiles.length;
|
|
604
|
+
}
|
|
605
|
+
if (zoneEntries.length > cap) {
|
|
606
|
+
const remaining = e.files.length - totalCapped;
|
|
607
|
+
const remainingZones = zoneEntries.length - cap;
|
|
608
|
+
lines.push(
|
|
609
|
+
`\n … อีก ${remainingZones} โซน (${remaining} ไฟล์) — fapony debt --id ${e.conv.id} --all`,
|
|
610
|
+
);
|
|
547
611
|
}
|
|
548
612
|
}
|
|
549
613
|
for (const c of report.declared) {
|
|
@@ -565,27 +629,66 @@ export function formatDebt(report: DebtReport): string {
|
|
|
565
629
|
|
|
566
630
|
// --- CLI ---
|
|
567
631
|
|
|
568
|
-
const USAGE =
|
|
569
|
-
|
|
570
|
-
|
|
632
|
+
const USAGE = `usage: fapony debt [path] [options]
|
|
633
|
+
--files f1,f2 check specific files instead of scanning
|
|
634
|
+
--id <conv> show only this convention
|
|
635
|
+
--where <path> narrow scope to files under this path
|
|
636
|
+
--all show all zones (default: cap at ${ZONE_CAP})
|
|
637
|
+
--json output raw JSON
|
|
638
|
+
-h, --help this help`;
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* The dir `debt` measures: the nearest ancestor of `arg` (or cwd) that holds
|
|
642
|
+
* `.fapony/conventions.json`, bounded by the git root.
|
|
643
|
+
*
|
|
644
|
+
* Jumping straight to the git root was the bug: in a monorepo the root has no
|
|
645
|
+
* conventions.json and two apps have one each, so the mem resolver went
|
|
646
|
+
* ambiguous and `debt` said "nothing tracked yet" while
|
|
647
|
+
* apps/<x>/.fapony/conventions.json sat right there — and the positional path
|
|
648
|
+
* argument was silently ignored. Falling back to the git root keeps single
|
|
649
|
+
* repos run from a subdir scanning the whole repo.
|
|
650
|
+
*/
|
|
651
|
+
export function worktreeOf(arg: string | undefined): string {
|
|
571
652
|
const base = resolve(arg ?? ".");
|
|
653
|
+
let gitRoot: string | null = null;
|
|
572
654
|
try {
|
|
573
655
|
const p = Bun.spawnSync(["git", "rev-parse", "--show-toplevel"], {
|
|
574
656
|
cwd: base,
|
|
575
657
|
stdout: "pipe",
|
|
576
658
|
stderr: "pipe",
|
|
577
659
|
});
|
|
578
|
-
if (p.exitCode === 0)
|
|
660
|
+
if (p.exitCode === 0) gitRoot = p.stdout.toString().trim() || null;
|
|
579
661
|
} catch {
|
|
580
|
-
//
|
|
662
|
+
// not a repo — base is all we have
|
|
663
|
+
}
|
|
664
|
+
// `git rev-parse` returns a physical path (/var → /private/var on macOS),
|
|
665
|
+
// so the boundary check compares realpaths, same as the mem resolver.
|
|
666
|
+
const real = (d: string): string => {
|
|
667
|
+
try {
|
|
668
|
+
return realpathSync(d);
|
|
669
|
+
} catch {
|
|
670
|
+
return d;
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
const boundary = gitRoot ? real(gitRoot) : null;
|
|
674
|
+
let dir = base;
|
|
675
|
+
while (true) {
|
|
676
|
+
if (existsSync(join(dir, CONVENTIONS_FILE))) return dir;
|
|
677
|
+
if (boundary && real(dir) === boundary) break;
|
|
678
|
+
const parent = dirname(dir);
|
|
679
|
+
if (parent === dir) break;
|
|
680
|
+
dir = parent;
|
|
581
681
|
}
|
|
582
|
-
return base;
|
|
682
|
+
return gitRoot ?? base;
|
|
583
683
|
}
|
|
584
684
|
|
|
585
685
|
export function cmdDebt(args: string[]): void {
|
|
586
686
|
let path: string | undefined;
|
|
587
687
|
let filesMode: string[] | null = null;
|
|
588
688
|
let json = false;
|
|
689
|
+
let filterId: string | undefined;
|
|
690
|
+
let wherePath: string | undefined;
|
|
691
|
+
let showAll = false;
|
|
589
692
|
for (let i = 0; i < args.length; i++) {
|
|
590
693
|
const a = args[i];
|
|
591
694
|
if (a === "--files") {
|
|
@@ -603,6 +706,24 @@ export function cmdDebt(args: string[]): void {
|
|
|
603
706
|
console.error(`fapony debt: --files needs at least one path\n${USAGE}`);
|
|
604
707
|
process.exit(1);
|
|
605
708
|
}
|
|
709
|
+
} else if (a === "--id") {
|
|
710
|
+
const v = args[i + 1];
|
|
711
|
+
if (!v || v.startsWith("--")) {
|
|
712
|
+
console.error(`fapony debt: --id needs a convention id\n${USAGE}`);
|
|
713
|
+
process.exit(1);
|
|
714
|
+
}
|
|
715
|
+
i++;
|
|
716
|
+
filterId = v;
|
|
717
|
+
} else if (a === "--where") {
|
|
718
|
+
const v = args[i + 1];
|
|
719
|
+
if (!v || v.startsWith("--")) {
|
|
720
|
+
console.error(`fapony debt: --where needs a path\n${USAGE}`);
|
|
721
|
+
process.exit(1);
|
|
722
|
+
}
|
|
723
|
+
i++;
|
|
724
|
+
wherePath = v;
|
|
725
|
+
} else if (a === "--all") {
|
|
726
|
+
showAll = true;
|
|
606
727
|
} else if (a === "--json") {
|
|
607
728
|
json = true;
|
|
608
729
|
} else if (a === "-h" || a === "--help") {
|
|
@@ -653,6 +774,25 @@ export function cmdDebt(args: string[]): void {
|
|
|
653
774
|
return;
|
|
654
775
|
}
|
|
655
776
|
const report = debtScan(worktree, loaded);
|
|
777
|
+
|
|
778
|
+
// --id filter: keep only the named convention
|
|
779
|
+
if (filterId) {
|
|
780
|
+
report.entries = report.entries.filter((e) => e.conv.id === filterId);
|
|
781
|
+
report.declared = report.declared.filter((c) => c.id === filterId);
|
|
782
|
+
report.checkedCount = 0; // not relevant when filtering
|
|
783
|
+
report.dropped = report.dropped.filter((d) => d.id === filterId);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// --where filter: narrow file lists to paths under the given prefix
|
|
787
|
+
if (wherePath) {
|
|
788
|
+
const prefix = wherePath.replace(/\/+$/, "");
|
|
789
|
+
for (const e of report.entries) {
|
|
790
|
+
e.files = e.files.filter(
|
|
791
|
+
(f) => f === prefix || f.startsWith(`${prefix}/`),
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
656
796
|
if (json) {
|
|
657
797
|
console.log(
|
|
658
798
|
JSON.stringify(
|
|
@@ -663,7 +803,7 @@ export function cmdDebt(args: string[]): void {
|
|
|
663
803
|
);
|
|
664
804
|
return;
|
|
665
805
|
}
|
|
666
|
-
console.log(formatDebt(report));
|
|
806
|
+
console.log(formatDebt(report, showAll));
|
|
667
807
|
for (const w of loaded.warnings) console.log(`⚠ ${w}`);
|
|
668
808
|
for (const l of formatPromotions(findPromotions(worktree, report))) {
|
|
669
809
|
console.log(l);
|
package/src/digest/collect.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { execSync } from "node:child_process";
|
|
|
6
6
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import {
|
|
9
|
+
CONFIG_FILENAME,
|
|
9
10
|
doneDir,
|
|
10
11
|
type Event,
|
|
11
12
|
loadConfig,
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
planDir,
|
|
14
15
|
type Run,
|
|
15
16
|
} from "../db/index.js";
|
|
17
|
+
import { computeHintImpact, type HintImpact, hintLogPath } from "../hook.js";
|
|
16
18
|
import { type MemRow, readMemLog } from "../memory.js";
|
|
17
19
|
import { isPassFamily, VERDICT_GRADES } from "../parse.js";
|
|
18
20
|
import { imputeResult, loadPrices } from "../price/index.js";
|
|
@@ -83,6 +85,8 @@ export interface DigestData {
|
|
|
83
85
|
/** % of those whose earliest in-period gate passed at round ≤ 1. */
|
|
84
86
|
round1_pct: number;
|
|
85
87
|
};
|
|
88
|
+
/** Hint-fire log counts for this worktree — null when no log exists. */
|
|
89
|
+
impact: HintImpact | null;
|
|
86
90
|
skipped_malformed: number;
|
|
87
91
|
}
|
|
88
92
|
|
|
@@ -162,8 +166,8 @@ function readPlans(worktree: string): {
|
|
|
162
166
|
ok: boolean;
|
|
163
167
|
detail: string;
|
|
164
168
|
} {
|
|
165
|
-
const config = loadConfig(join(worktree,
|
|
166
|
-
const pDir = join(worktree, planDir(
|
|
169
|
+
const config = loadConfig(join(worktree, CONFIG_FILENAME));
|
|
170
|
+
const pDir = join(worktree, planDir());
|
|
167
171
|
const dDir = join(worktree, doneDir(config));
|
|
168
172
|
|
|
169
173
|
if (!existsSync(pDir)) {
|
|
@@ -586,6 +590,18 @@ export async function collectDigest(
|
|
|
586
590
|
detail: verdictResult.detail,
|
|
587
591
|
});
|
|
588
592
|
|
|
593
|
+
// 6. hint-fire log — annotate surface, this worktree only. A log that
|
|
594
|
+
// exists but fired 0 in the window is "0", not "no data" (done criteria 6).
|
|
595
|
+
const hasHintLog = existsSync(hintLogPath(worktree));
|
|
596
|
+
const impact = hasHintLog ? computeHintImpact(sinceIso, worktree) : null;
|
|
597
|
+
sources.push({
|
|
598
|
+
name: "hint-log",
|
|
599
|
+
ok: hasHintLog,
|
|
600
|
+
detail: impact
|
|
601
|
+
? `${impact.fired} fired this window (${impact.debt.shown} debt shown)`
|
|
602
|
+
: "no hints recorded",
|
|
603
|
+
});
|
|
604
|
+
|
|
589
605
|
// classify mem rows
|
|
590
606
|
const decisions = mem.rows.filter((r) => r.kind === "decision");
|
|
591
607
|
const notes = mem.rows.filter((r) => r.kind === "note");
|
|
@@ -620,6 +636,7 @@ export async function collectDigest(
|
|
|
620
636
|
units_graded: verdictResult.units_graded,
|
|
621
637
|
round1_pct: verdictResult.round1_pct,
|
|
622
638
|
},
|
|
639
|
+
impact,
|
|
623
640
|
skipped_malformed: skippedMalformed,
|
|
624
641
|
};
|
|
625
642
|
}
|
package/src/digest/text.ts
CHANGED
|
@@ -34,6 +34,23 @@ function fmtTokens(n: number): string {
|
|
|
34
34
|
return String(Math.round(n));
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
function impactLines(i: NonNullable<DigestData["impact"]>): string[] {
|
|
38
|
+
const b = i.by_surface;
|
|
39
|
+
const out = [
|
|
40
|
+
`hints fired: ${i.fired} (read ${b.read} · debt ${b.debt} · mem ${b.mem} · commit ${b.commit})`,
|
|
41
|
+
];
|
|
42
|
+
if (i.debt.shown > 0) {
|
|
43
|
+
const pct = Math.round((i.debt.resolved / i.debt.shown) * 100);
|
|
44
|
+
out.push(
|
|
45
|
+
`debt lines shown: ${i.debt.shown} · no longer present at HEAD: ${i.debt.resolved} (${pct}%) · unverifiable: ${i.debt.unknown}`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
out.push(
|
|
49
|
+
"note: counts what fapony showed and what the repo looks like now, not proof the agent acted because of the hint",
|
|
50
|
+
);
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
37
54
|
function costLine(c: CostRow): string {
|
|
38
55
|
const name = c.provider ? `${c.provider}/${c.model}` : c.model;
|
|
39
56
|
const costStr =
|
|
@@ -70,6 +87,14 @@ export function renderDigestText(d: DigestData): string {
|
|
|
70
87
|
);
|
|
71
88
|
lines.push("");
|
|
72
89
|
|
|
90
|
+
// 2b. fapony impact — what the annotate surfaces showed, not proof of use
|
|
91
|
+
lines.push(
|
|
92
|
+
section("FAPONY IMPACT", [
|
|
93
|
+
...(d.impact ? impactLines(d.impact) : ["(no hints recorded)"]),
|
|
94
|
+
]),
|
|
95
|
+
);
|
|
96
|
+
lines.push("");
|
|
97
|
+
|
|
73
98
|
// 3. Decisions
|
|
74
99
|
lines.push(
|
|
75
100
|
section(`DECISIONS (${d.decisions.length})`, [
|