fapony 0.1.1 → 0.1.3

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.
Files changed (44) hide show
  1. package/README.md +22 -36
  2. package/images/logo.png +0 -0
  3. package/images/logo.webp +0 -0
  4. package/images/logo@400.webp +0 -0
  5. package/images/sample.webp +0 -0
  6. package/images/summary.webp +0 -0
  7. package/package.json +10 -8
  8. package/skill/plan-with-pony/SKILL.md +1 -1
  9. package/src/analyze.ts +9 -8
  10. package/src/conventions-seed.ts +10 -10
  11. package/src/db/index.ts +1 -1
  12. package/src/db/store.ts +4 -0
  13. package/src/debt.ts +26 -22
  14. package/src/digest/collect.ts +7 -7
  15. package/src/digest/html.ts +2 -2
  16. package/src/digest/text.ts +1 -1
  17. package/src/gate.ts +3 -3
  18. package/src/hook.ts +119 -21
  19. package/src/init-mem.ts +3 -3
  20. package/src/install/opencode.ts +95 -8
  21. package/src/install/types.ts +1 -1
  22. package/src/install.ts +5 -1
  23. package/src/lint-baseline.ts +9 -9
  24. package/src/mcp/tools/mem.ts +1 -1
  25. package/src/mcp/tools/stats.ts +2 -2
  26. package/src/mcp/tools/usage.ts +3 -3
  27. package/src/mcp/tools/verdict.ts +29 -16
  28. package/src/mcp/transport.ts +1 -1
  29. package/src/plan-seed.ts +23 -21
  30. package/src/price/fetch.ts +19 -19
  31. package/src/price/resolve.ts +24 -24
  32. package/src/review-seed.ts +1 -1
  33. package/src/stats/data.ts +8 -7
  34. package/src/stats/format.ts +6 -5
  35. package/src/usage/render.ts +6 -5
  36. package/templates/PLAN.md +1 -1
  37. package/templates/mem/commands/plan.ts +44 -44
  38. package/templates/mem/commands/read.ts +5 -5
  39. package/templates/mem/commands/rotate.ts +6 -6
  40. package/templates/mem/commands/selftest.ts +14 -14
  41. package/templates/mem/commands/write.ts +13 -13
  42. package/templates/mem/mem.ts +5 -5
  43. package/templates/mem/selectors.ts +18 -18
  44. package/templates/mem/store.ts +50 -50
package/src/plan-seed.ts CHANGED
@@ -88,7 +88,7 @@ function scopeSourceFiles(root: string): string[] {
88
88
  // is about to plan. Headers only (title + shipped date) — pulling the bodies
89
89
  // in would recreate the reading task the plan exists to avoid.
90
90
  function renderPriorArt(cwd: string, config: Config, roots: string[]): string {
91
- const placeholder = "- _(agent เติม)_";
91
+ const placeholder = "- _(agent fills in)_";
92
92
  const keys = roots
93
93
  .map((r) => relative(cwd, r))
94
94
  .filter((r) => r !== "" && r !== ".");
@@ -124,7 +124,7 @@ function renderPriorArt(cwd: string, config: Config, roots: string[]): string {
124
124
  const shipped = content.match(/shipped\s+(\d{4}-\d{2}-\d{2})/)?.[1] ?? "";
125
125
  hits.push({
126
126
  shipped,
127
- line: `- ✅ ตัดสินไปแล้ว: [${n}](../${label}/${n}) — ${title}${shipped ? ` (shipped ${shipped})` : ""} \`(fapony plan-seed)\``,
127
+ line: `- ✅ Already decided: [${n}](../${label}/${n}) — ${title}${shipped ? ` (shipped ${shipped})` : ""} \`(fapony plan-seed)\``,
128
128
  });
129
129
  }
130
130
  }
@@ -137,7 +137,9 @@ function renderPriorArt(cwd: string, config: Config, roots: string[]): string {
137
137
  );
138
138
  const shown = hits.slice(0, MAX_PRIOR_ART).map((h) => h.line);
139
139
  if (hits.length > MAX_PRIOR_ART) {
140
- shown.push(`- … +${hits.length - MAX_PRIOR_ART} more ที่แตะ scope เดียวกัน`);
140
+ shown.push(
141
+ `- … +${hits.length - MAX_PRIOR_ART} more touching the same scope`,
142
+ );
141
143
  }
142
144
  shown.push(placeholder);
143
145
  return shown.join("\n");
@@ -185,49 +187,49 @@ kind: unit
185
187
  status: active
186
188
  ---
187
189
 
188
- # PLAN-${name} — (agent เติมชื่อเรื่อง)
190
+ # PLAN-${name} — (agent fills in a title)
189
191
 
190
- > **Status:** 🚧 in-progress · **Created:** (agent เติมวันที่)
192
+ > **Status:** 🚧 in-progress · **Created:** (agent fills in the date)
191
193
 
192
194
  ## TL;DR
193
- - **What:** (agent เติม) · **Why:** (agent เติม) · **Done when:** (agent เติม)
194
- - **Order:** (agent เติม)
195
+ - **What:** (agent fills in) · **Why:** (agent fills in) · **Done when:** (agent fills in)
196
+ - **Order:** (agent fills in)
195
197
  - **Progress:**
196
- - [ ] chunk 1 — (agent เติม)
198
+ - [ ] chunk 1 — (agent fills in)
197
199
 
198
200
  ## Context (fapony)
199
201
  ${contextFapony}
200
202
 
201
203
  ## 1. Goal (why)
202
- _(agent เติม)_
204
+ _(agent fills in)_
203
205
 
204
206
  ## 2. Scope (do / don't do)
205
- _(agent เติม)_
207
+ _(agent fills in)_
206
208
 
207
209
  ## 3. Done criteria (how we know it's finished)
208
- _(agent เติม — ต้อง verify ได้)_
210
+ _(agent fills in — must be verifiable)_
209
211
 
210
212
  ## 4. Constraints / Hard rules (must not violate)
211
- _(agent เติม)_
213
+ _(agent fills in)_
212
214
 
213
215
  ## 5. Risks & Escape hatches (if it fails)
214
- _(agent เติม)_
216
+ _(agent fills in)_
215
217
 
216
218
  ## 6. Steps (what in which order)
217
- 1. _(agent เติม — แต่ละขั้น verify ได้)_
219
+ 1. _(agent fills in — each step must be verifiable)_
218
220
 
219
221
  ## 7. Examples
220
222
  ${
221
223
  specLink
222
- ? `→ ${specLink} (signature อยู่ spec ไม่ใช่ plan)`
223
- : "_(agent เติม — หรือเพิ่ม SPEC ด้วย plan-seed --spec)_"
224
+ ? `→ ${specLink} (signatures live in the spec, not the plan)`
225
+ : "_(agent fills in — or add a SPEC with plan-seed --spec)_"
224
226
  }
225
227
 
226
228
  ## 8. References
227
229
  ${priorArt}
228
230
 
229
231
  ## Context (agent)
230
- _(slot ว่าง — agent dump graph/code-summary ของตัวเอง)_
232
+ _(empty slot — the agent dumps its own graph/code-summary)_
231
233
  `;
232
234
  }
233
235
 
@@ -437,7 +439,7 @@ function specTemplate(
437
439
  // undershot the cap by that many lines (the 18k-SPEC failure mode). Cap the
438
440
  // index separately so the body's capLines has a bounded head to work with.
439
441
  const fixedHead = [
440
- `# SPEC-${name} — (agent เติมชื่อเรื่อง)`,
442
+ `# SPEC-${name} — (agent fills in a title)`,
441
443
  "",
442
444
  `> **Used by:** PLAN-${name} — signatures below come from a live source scan — re-seed after structural changes.`,
443
445
  ...(scopeEcho ? [`> **Scope:** ${scopeEcho}`] : []),
@@ -456,7 +458,7 @@ function specTemplate(
456
458
  );
457
459
  const head = [...fixedHead, ...cappedIndex, ""];
458
460
  const tail = [
459
- "## (agent เติม — wireframes / edge cases / API shapes ที่ plan อ้างถึง)",
461
+ "## (agent fills in — wireframes / edge cases / API shapes the plan references)",
460
462
  ];
461
463
  const bodyLines = chunks.flatMap((c) => [
462
464
  `## <a id="${c.slug}"></a>${c.title}`,
@@ -557,7 +559,7 @@ export function cmdPlanSeed(args: string[]): void {
557
559
  const planPath = join(planDirAbs, `PLAN-${name}.md`);
558
560
  if (existsSync(planPath)) {
559
561
  console.error(
560
- `${planPath} already exists — not overwriting. ใช้ชื่อใหม่ เช่น PLAN-${name}-v2`,
562
+ `${planPath} already exists — not overwriting. Use a new name, e.g. PLAN-${name}-v2`,
561
563
  );
562
564
  process.exit(1);
563
565
  }
@@ -571,7 +573,7 @@ export function cmdPlanSeed(args: string[]): void {
571
573
  const specPath = join(specDirAbs, `SPEC-${name}.md`);
572
574
  if (existsSync(specPath)) {
573
575
  console.error(
574
- `${specPath} already exists — not overwriting. ใช้ชื่อใหม่ เช่น SPEC-${name}-v2`,
576
+ `${specPath} already exists — not overwriting. Use a new name, e.g. SPEC-${name}-v2`,
575
577
  );
576
578
  process.exit(1);
577
579
  }
@@ -1,9 +1,9 @@
1
- // src/price/fetch.ts — ดึงตารางราคา OpenRouter + cache เป็น prices.json
1
+ // src/price/fetch.ts — fetch OpenRouter's price table + cache it as prices.json
2
2
  //
3
- // ราคาเป็น cache ไม่ใช่ state: เก็บที่ ~/.config/fapony/prices.json (เคารพ
4
- // FAPONY_STATE_DIR) ห้ามเพิ่มตาราง SQLite (กฎ DB Schema: 2 ตารางเท่านั้น)
5
- // refresh เกิดตอนคนสั่ง `fapony price-scan` เท่านั้น — query ไม่ fetch เอง
6
- // (วินัยเดียวกับ usage-scan) offline แล้วอ่าน cache เดิม
3
+ // prices are cache, not state: stored at ~/.config/fapony/prices.json (honors
4
+ // FAPONY_STATE_DIR) no new SQLite tables (DB Schema rule: 2 tables only)
5
+ // refresh happens only when someone runs `fapony price-scan` — a query never fetches on its own
6
+ // (same discipline as usage-scan) offline, read the existing cache
7
7
 
8
8
  import {
9
9
  existsSync,
@@ -19,12 +19,12 @@ import type { Config } from "../db/types.js";
19
19
  const PRICES_FILENAME = "prices.json";
20
20
  const MODELS_URL = "https://openrouter.ai/api/v1/models";
21
21
 
22
- /** เรตราย token (ดอลลาร์) — ทุกเรตมาจาก OpenRouter ตรง ๆ ไม่เดา */
22
+ /** per-token rates (dollars) — every rate comes straight from OpenRouter, no guessing */
23
23
  export interface ModelRates {
24
24
  input: number;
25
25
  output: number;
26
26
  cacheRead: number;
27
- /** null = ตารางไม่ให้มา → ใช้เรต input แทน (ดู calcCost) */
27
+ /** null = the table does not provide it → use the input rate instead (see calcCost) */
28
28
  cacheWrite: number | null;
29
29
  }
30
30
 
@@ -37,7 +37,7 @@ export function pricesPath(config?: Config): string {
37
37
  return join(faponyDir(config), PRICES_FILENAME);
38
38
  }
39
39
 
40
- /** อ่าน cache — คืน null เมื่อไม่มีไฟล์หรือพัง (caller แสดง — + hint) */
40
+ /** Read the cache — null when the file is missing or broken (caller shows "—" + hint) */
41
41
  export function loadPrices(config?: Config): PriceTable | null {
42
42
  const p = pricesPath(config);
43
43
  if (!existsSync(p)) return null;
@@ -53,7 +53,7 @@ export function loadPrices(config?: Config): PriceTable | null {
53
53
  }
54
54
  }
55
55
 
56
- /** เขียนแบบ atomic (tmp + rename) สร้าง state dir เมื่อยังไม่มี */
56
+ /** Write atomically (tmp + rename), create the state dir when absent */
57
57
  export function writePrices(table: PriceTable, config?: Config): void {
58
58
  const p = pricesPath(config);
59
59
  mkdirSync(dirname(p), { recursive: true });
@@ -72,14 +72,14 @@ function toRate(v: unknown): number | null {
72
72
  }
73
73
 
74
74
  /**
75
- * แปลง response ดิบของ OpenRouter เป็นตารางเรต (pure — เทสต์ได้โดยไม่ยิงเน็ต)
75
+ * Turn OpenRouter's raw response into a rate table (pure — testable without hitting the network)
76
76
  *
77
- * ฟิลด์จริง (ยืนยัน 2026-09-14, 445 models): pricing.prompt / .completion /
77
+ * Real fields (confirmed 2026-09-14, 445 models): pricing.prompt / .completion /
78
78
  * .input_cache_read / .input_cache_write (optional) / .input_cache_write_1h
79
- * (เรต 1h TTL ของบางรุ่น — ใช้เรต 5m มาตรฐานพอ เพราะ log ไม่บอก TTL)
80
- * เมิน pricing.web_search / pricing.overrides (tier ตาม min_prompt_tokens /
81
- * utc_days — ใช้ base rate แล้วประกาศข้อจำกัด) · id ขึ้นต้น ~ (alias) ตัดทิ้ง
82
- * id ลงท้าย :free / :batch เก็บตามนั้น (เป็นแถวราคาของมันเอง)
79
+ * (the 1h TTL rate of some models — the standard 5m rate suffices because logs do not report TTL)
80
+ * ignore pricing.web_search / pricing.overrides (tiers based on min_prompt_tokens /
81
+ * utc_days — use the base rate and state the limitation) · ids starting with ~ (alias) are dropped
82
+ * ids ending :free / :batch are kept as-is (they are their own price rows)
83
83
  */
84
84
  export function parsePricesResponse(json: unknown): Record<string, ModelRates> {
85
85
  const out: Record<string, ModelRates> = {};
@@ -102,8 +102,8 @@ export function parsePricesResponse(json: unknown): Record<string, ModelRates> {
102
102
  }
103
103
 
104
104
  /**
105
- * รวมตารางใหม่เข้ากับ cache เดิม — merge ไม่ replace: id ที่หายไปจาก
106
- * response รอบนี้ (รุ่นเก่าหลุดตาราง) ต้องยังคิดราคาได้ด้วยเรตเดิม
105
+ * Merge the new table into the old cache — merge, not replace: an id missing from
106
+ * this response (an old model dropped from the table) must still be priced with its old rate
107
107
  */
108
108
  export function mergePriceTables(
109
109
  old: PriceTable | null,
@@ -115,7 +115,7 @@ export function mergePriceTables(
115
115
  };
116
116
  }
117
117
 
118
- /** ดึงตารางจาก OpenRouter (public, ไม่ต้อง auth) — fetcher แทรกได้ไว้เทสต์ */
118
+ /** Fetch the table from OpenRouter (public, no auth) — fetcher is injectable for tests */
119
119
  export async function fetchPriceTable(
120
120
  fetcher: typeof fetch = fetch,
121
121
  ): Promise<Record<string, ModelRates>> {
@@ -134,7 +134,7 @@ export async function cmdPriceScan(rawArgs: string[]): Promise<void> {
134
134
  fresh = await fetchPriceTable();
135
135
  } catch (err) {
136
136
  console.error(
137
- `fapony price-scan: fetch failed (${String(err)}) — cache เดิมยังอยู่ ใช้ราคาที่มีได้`,
137
+ `fapony price-scan: fetch failed (${String(err)}) — the existing cache is still here, use the prices you have`,
138
138
  );
139
139
  process.exit(1);
140
140
  }
@@ -1,7 +1,7 @@
1
- // src/price/resolve.ts — normalize model id + คิด list-price equivalent
1
+ // src/price/resolve.ts — normalize model id + compute list-price equivalent
2
2
  //
3
- // กฎเหล็ก: map ไม่ได้ต้องเป็น unpriced ห้ามตีเป็น 0 เงียบ ๆ (failure mode หลัก
4
- // ของฟีเจอร์นี้) · free มีแต่ของที่ราคา 0 จริง (local / แถว :free ในตาราง)
3
+ // Hard rule: an unmappable model must be unpriced, never silently counted as 0 (the main failure mode
4
+ // of this feature) · free only applies to things that truly cost 0 (local / :free rows in the table)
5
5
 
6
6
  import type { ModelBreakdown, PassiveUsageResult } from "../session/types.js";
7
7
  import type { ModelRates, PriceTable } from "./fetch.js";
@@ -13,10 +13,10 @@ export interface PriceResolution {
13
13
  rates: ModelRates | null;
14
14
  }
15
15
 
16
- /** prefix ของ client ที่แปะหน้าทุก model id — ตัดทิ้งแล้ว lookup ใหม่ */
16
+ /** client prefix prepended to every model id — strip it, then look up again */
17
17
  const CLIENT_PREFIXES = ["openrouter/", "opencode-go/", "opencode/"];
18
18
 
19
- /** tier ต่อท้ายของ OpenRouter — :free คือ endpoint ฟรีจริง :batch คือส่วนลด */
19
+ /** OpenRouter's trailing tier — :free is a genuinely free endpoint, :batch is a discount */
20
20
  function stripTier(id: string): string {
21
21
  return id.endsWith(":free") || id.endsWith(":batch")
22
22
  ? id.slice(0, id.lastIndexOf(":"))
@@ -24,8 +24,8 @@ function stripTier(id: string): string {
24
24
  }
25
25
 
26
26
  /**
27
- * รายชื่อ candidate id ตามลำดับความเฉพาะ: ตรงตัวก่อน กว้างทีหลัง
28
- * (ตรงตัวชนก่อนเสมอ — bare slug แมตช์กว้างสุดอยู่ท้าย)
27
+ * Candidate ids ordered by specificity: exact first, broad later
28
+ * (an exact match always wins — the bare slug is the broadest match and goes last)
29
29
  */
30
30
  export function candidateIds(provider: string, model: string): string[] {
31
31
  const full = provider ? `${provider}/${model}` : model;
@@ -38,14 +38,14 @@ export function candidateIds(provider: string, model: string): string[] {
38
38
  break;
39
39
  }
40
40
  }
41
- // opencode ต่อ -free ท้าย slug ของรุ่นฟรี (deepseek-v4-flash-free)
41
+ // opencode appends -free to the slug of a free model (deepseek-v4-flash-free)
42
42
  if (rest.endsWith("-free")) out.push(rest.slice(0, -"-free".length));
43
43
  return out;
44
44
  }
45
45
 
46
46
  /**
47
- * หาเรตให้ model หนึ่งตัว — คืน null เฉพาะของ local เท่านั้นที่ข้ามตาราง
48
- * (local ไม่เคยมีราคาตั้งแต่แรก ไม่ใช่ "หาไม่เจอ")
47
+ * Find rates for one model — null only for local, which skips the table
48
+ * (local never had a price to begin with, it is not "not found")
49
49
  */
50
50
  function isLocalProvider(provider: string): boolean {
51
51
  return provider === "lmstudio_local";
@@ -59,14 +59,14 @@ export function resolvePrice(
59
59
  if (!model || model === "(no model id)" || model === "(unknown)")
60
60
  return { status: "unpriced", rates: null };
61
61
  if (isLocalProvider(provider)) return { status: "free", rates: null };
62
- // ลงท้าย :free หรือ -free = เรียกผ่าน free endpoint / รุ่นฟรีของ client มา
63
- // ราคาจริงคือ 0 (ไม่ใช่ list price) ไม่ว่าจะ map OpenRouter ได้หรือไม่
62
+ // ends with :free or -free = used a free endpoint / the client's free model
63
+ // the real price is 0 (not list price), whether or not OpenRouter maps it
64
64
  if (model.endsWith(":free") || model.endsWith("-free"))
65
65
  return { status: "free", rates: null };
66
66
  for (const id of candidateIds(provider, model)) {
67
67
  const rates = table.models[id] ?? table.models[stripTier(id)];
68
68
  if (rates) {
69
- // แถวราคา 0 ทั้งแถว (:free / รุ่นฟรี) = free จริง ไม่ใช่ unpriced
69
+ // an all-zero rate row (:free / free model) = genuinely free, not unpriced
70
70
  if (
71
71
  rates.input === 0 &&
72
72
  rates.output === 0 &&
@@ -77,10 +77,10 @@ export function resolvePrice(
77
77
  return { status: "priced", rates };
78
78
  }
79
79
  }
80
- // zcode เก็บแค่ slug ไม่มี vendor (GLM-5.3-Flash) — เทียบส่วนหลัง / ตรงตัว
81
- // แบบ case-insensitive (exact ไม่ใช่ fuzzy: ยาวเท่ากันทั้งสตริง)
82
- // ฝั่งตารางตัด tier (:free/:batch) ก่อนเทียบ — slug เปลือยจะได้ชนแถว :free
83
- // ที่ราคา 0 จริง (เช่น ling-3.0-flash-fin) กลายเป็น free ไม่ใช่ unpriced
80
+ // zcode stores only the slug with no vendor (GLM-5.3-Flash) — compare the suffix / exact
81
+ // case-insensitive match (exact, not fuzzy: the whole string must be equal in length)
82
+ // the table side strips the tier (:free/:batch) before comparing — so a bare slug hits the :free
83
+ // row that is truly 0 (e.g. ling-3.0-flash-fin) and becomes free, not unpriced
84
84
  const slug = stripTier(
85
85
  candidateIds(provider, model).at(-1) ?? "",
86
86
  ).toLowerCase();
@@ -114,11 +114,11 @@ export interface TokenCounts {
114
114
  }
115
115
 
116
116
  /**
117
- * คิดเงิน pure: แยกเรต input / cache-read / cache-write — ห้ามใช้เรตเดียวรวบ
117
+ * Pure costing: separate rates for input / cache-read / cache-write — never one rate for all
118
118
  *
119
- * reasoning ไม่คูณแยก: ของ Anthropic-family thinking รวมอยู่ใน output อยู่แล้ว
120
- * (reader แยกเก็บไว้ดูเฉย ๆ) คูณแยก = double count · cache_write ไม่มีในตาราง
121
- * → fallback เรต input (เขียน cache แพงกว่า/เท่าอ่านสด ไม่มีทางถูกกว่า)
119
+ * reasoning is not multiplied separately: Anthropic-family thinking is already included in output
120
+ * (the reader keeps it separately just for visibility); multiplying separately = double counting · cache_write absent from the table
121
+ * → fall back to the input rate (writing cache costs more than or equal to a fresh read, never less)
122
122
  */
123
123
  export function calcCost(t: TokenCounts, rates: ModelRates): number {
124
124
  return (
@@ -138,7 +138,7 @@ export interface ImputedModel {
138
138
  tokens_cache_read: number;
139
139
  tokens_cache_write: number;
140
140
  status: PriceStatus;
141
- /** ดอลลาร์ list-price — 0 เมื่อ free/unpriced (ดู status อย่าอ่านเลขอย่างเดียว) */
141
+ /** dollars at list-price — 0 when free/unpriced (check status, do not read the number alone) */
142
142
  imputed_cost: number;
143
143
  }
144
144
 
@@ -152,8 +152,8 @@ export interface ImputeSummary {
152
152
  }
153
153
 
154
154
  /**
155
- * ตีราคาทั้ง PassiveUsageResult — ใช้กับผลสด (stats/usage) หรือแถว cache
156
- * (usage-web) ก็ได้เพราะรับแค่ token ต่อ model
155
+ * Price an entire PassiveUsageResult — works on live results (stats/usage) or cache rows
156
+ * (usage-web) alike, because it only takes tokens per model
157
157
  */
158
158
  export function imputeResult(
159
159
  result: PassiveUsageResult,
@@ -544,7 +544,7 @@ function hasDynamicDispatch(absFile: string): boolean {
544
544
 
545
545
  // --- --body: declaration slice (indent-out, no parser) ---
546
546
  // extractBody lives in map.ts beside extractExports — the conventions seeder
547
- // reuses the same slice for wrapper detection (one implementation, กฎ 1).
547
+ // reuses the same slice for wrapper detection (one implementation, rule 1).
548
548
 
549
549
  // --- --callers: symbol→symbol over importer files (identifier scan) ---
550
550
 
package/src/stats/data.ts CHANGED
@@ -622,15 +622,16 @@ function addSessionTokens(
622
622
  }
623
623
 
624
624
  /**
625
- * tokens/pass — retry tax ที่มองเห็นได้ (SPEC-cost-per-pass).
625
+ * tokens/pass — the visible retry tax (SPEC-cost-per-pass).
626
626
  *
627
- * ตัวหารคือ pass-family gate ไม่ใช่ run: bucket เป็น gate อยู่แล้ว และงานที่
628
- * "จบ" คือ gate ที่ผ่าน · token เป็นยอดต่อ session (dedupe แล้ว) ไม่ใช่ต่อ gate —
629
- * session เดียวออกหลาย gate ได้ ถ้า sum ต่อ gate จะคูณเกินไม่เท่ากันทุกโมเดล
627
+ * The divisor is a pass-family gate, not a run: bucket is already a gate, and
628
+ * work "finished" is a gate that passed · token is a per-session total (deduped),
629
+ * not per gate — one session can emit several gates, so summing per gate would
630
+ * multiply unevenly across models.
630
631
  *
631
- * `passes=0` หรือ token รวม 0 → null (ไม่ใช่ Infinity/NaN/0): "วัดไม่ได้" ต้อง
632
- * แยกจาก "ฟรี" และห้ามหารศูนย์ · cost/pass ยังไม่มีใน v1 — ไม่มี cache split
633
- * (tokensInput คือ fresh+cache_read+cache_write รวมกัน) จึงคิดราคาไม่ได้โดยไม่เดา
632
+ * `passes=0` or total token 0 → null (not Infinity/NaN/0): "unmeasurable" is not
633
+ * "free" and never divide by zero · no cost/pass in v1 — no cache split
634
+ * (tokensInput is fresh+cache_read+cache_write), so price needs a guess.
634
635
  */
635
636
  function tokensPerPass(
636
637
  passes: number,
@@ -51,9 +51,10 @@ function modelLine(
51
51
  }
52
52
 
53
53
  /**
54
- * บรรทัด list-price equivalent ต่อท้ายแต่ละ usage section — หน่วยเดียวที่
55
- * เทียบข้าม client ได้ (client ที่ไม่บันทึก cost มีราคาติดตรงนี้)
56
- * ราคา list ไม่ใช่เงินที่จ่ายจริง · unpriced แยกออกมาให้เห็น ไม่รวมใน 0
54
+ * list-price equivalent line appended to each usage section — the one unit
55
+ * that compares across clients (clients that do not record cost get a price
56
+ * attached here). List price is not money paid · unpriced is broken out
57
+ * separately, never folded into 0.
57
58
  */
58
59
  function imputedLines(
59
60
  result: PassiveUsageResult,
@@ -103,8 +104,8 @@ export function formatStatsText(data: StatsData): string {
103
104
  if (data.runs.total === 0) return "no runs yet";
104
105
 
105
106
  const lines: string[] = [];
106
- // ราคา list จาก cache อย่างเดียว — query ไม่ fetch เอง (offline ได้, ไม่มี
107
- // ไฟล์ = แสดง — + hint ไม่ throw)
107
+ // list price from cache only — the query never fetches itself (works
108
+ // offline, missing file = shows — + hint, never throws)
108
109
  const prices = loadPrices();
109
110
 
110
111
  if (data.scope) {
@@ -304,8 +304,9 @@ function shareSection(
304
304
  }
305
305
 
306
306
  /**
307
- * เติมราคาตั้งให้แถวที่ client ไม่บันทึก cost (0) — ของที่มีราคาจริงอยู่แล้ว
308
- * ไม่แตะ · คืน view ไว้ render + note ไว้ใต้ตาราง (ป้าย list-price ทุกจุด)
307
+ * Imputes list prices for rows where the client records no cost (0) — rows
308
+ * that already have a real price are left alone · returns a view to render
309
+ * plus a note for below the table (list-price label everywhere)
309
310
  */
310
311
  function withImputed(
311
312
  data: PassiveUsageResult | null,
@@ -332,7 +333,7 @@ function withImputed(
332
333
  tokens_reasoning: real?.tokens_reasoning ?? 0,
333
334
  tokens_cache_read: m.tokens_cache_read,
334
335
  tokens_cache_write: m.tokens_cache_write,
335
- // รวมกับ cost จริงรายรุ่น (opencode บันทึกเอง) — ข้างไหนมีค่ากว่ากันเอาข้างนั้น
336
+ // combine with the real per-model cost (OpenCode records its own) — whichever has a value wins
336
337
  cost: real && real.cost > 0 ? real.cost : m.imputed_cost,
337
338
  };
338
339
  });
@@ -363,7 +364,7 @@ export function renderUsageHtml(
363
364
  .filter((k) => k !== "__global__")
364
365
  .sort();
365
366
 
366
- // ราคา list จาก cache อย่างเดียว — serve ไม่ fetch เอง (offline ได้)
367
+ // list price from cache only — serving never fetches itself (works offline)
367
368
  const table = prices === undefined ? loadPrices() : prices;
368
369
 
369
370
  const owner = ownerName?.trim() ? esc(ownerName.trim()) : "";
@@ -395,7 +396,7 @@ export function renderUsageHtml(
395
396
 
396
397
  const heading = isGlobal ? "All projects" : shortWt(label);
397
398
 
398
- // การ์ดที่ไม่มี session เลยไม่มีอะไรให้ดู — ซ่อนแทนที่จะโชว์ "no sessions"
399
+ // a card with no sessions has nothing to show — hide it instead of showing "no sessions"
399
400
  const cards = [
400
401
  ["OpenCode", "var(--green)", pOc],
401
402
  ["ZCode", "var(--accent)", pZc],
package/templates/PLAN.md CHANGED
@@ -24,7 +24,7 @@ spec: SPEC-<feature>.md # if any
24
24
  - **Done when:** one line, testable
25
25
  - **Order:** what this waits on / what it unblocks (mirrors the frontmatter)
26
26
  - **Progress:**
27
- - [x] chunk 1 — <what landed> `<short sha>` <YYYY-MM-DD>
27
+ - [x] chunk 1 — <what landed> `<short sha>` <YYYY-MM-DD> · verdict: <grade>
28
28
  - [ ] chunk 2 — <what is next>
29
29
 
30
30
  ---