@rasensio/aidlc 1.26.1 → 1.27.0
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/dist/commands/cost.d.ts +7 -0
- package/dist/commands/cost.d.ts.map +1 -1
- package/dist/commands/cost.js +137 -0
- package/dist/commands/cost.js.map +1 -1
- package/dist/commands/discover.d.ts.map +1 -1
- package/dist/commands/discover.js +11 -3
- package/dist/commands/discover.js.map +1 -1
- package/dist/commands/review.d.ts +15 -2
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/review.js +163 -9
- package/dist/commands/review.js.map +1 -1
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +27 -66
- package/dist/commands/start.js.map +1 -1
- package/dist/commands/transition.d.ts.map +1 -1
- package/dist/commands/transition.js +45 -8
- package/dist/commands/transition.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +8 -2
- package/dist/commands/update.js.map +1 -1
- package/dist/core/lifecycle.d.ts.map +1 -1
- package/dist/core/lifecycle.js +30 -1
- package/dist/core/lifecycle.js.map +1 -1
- package/dist/cost/context.d.ts +72 -0
- package/dist/cost/context.d.ts.map +1 -0
- package/dist/cost/context.js +155 -0
- package/dist/cost/context.js.map +1 -0
- package/dist/cost/dedupe-history.d.ts +98 -0
- package/dist/cost/dedupe-history.d.ts.map +1 -0
- package/dist/cost/dedupe-history.js +275 -0
- package/dist/cost/dedupe-history.js.map +1 -0
- package/dist/cost/ledger.d.ts.map +1 -1
- package/dist/cost/ledger.js +11 -5
- package/dist/cost/ledger.js.map +1 -1
- package/dist/cost/providers/claude-code.d.ts +13 -1
- package/dist/cost/providers/claude-code.d.ts.map +1 -1
- package/dist/cost/providers/claude-code.js +112 -10
- package/dist/cost/providers/claude-code.js.map +1 -1
- package/dist/cost/rates.d.ts +16 -1
- package/dist/cost/rates.d.ts.map +1 -1
- package/dist/cost/rates.js +21 -0
- package/dist/cost/rates.js.map +1 -1
- package/dist/cost/recorder.d.ts.map +1 -1
- package/dist/cost/recorder.js +3 -0
- package/dist/cost/recorder.js.map +1 -1
- package/dist/cost/report.d.ts +30 -0
- package/dist/cost/report.d.ts.map +1 -1
- package/dist/cost/report.js +96 -5
- package/dist/cost/report.js.map +1 -1
- package/dist/cost/sync.d.ts +18 -0
- package/dist/cost/sync.d.ts.map +1 -1
- package/dist/cost/sync.js +1 -1
- package/dist/cost/sync.js.map +1 -1
- package/dist/cost/types.d.ts +40 -0
- package/dist/cost/types.d.ts.map +1 -1
- package/dist/cost/types.js +15 -0
- package/dist/cost/types.js.map +1 -1
- package/dist/discover/scanner.d.ts +20 -2
- package/dist/discover/scanner.d.ts.map +1 -1
- package/dist/discover/scanner.js +80 -14
- package/dist/discover/scanner.js.map +1 -1
- package/dist/doctor/migrations/findings-filename.d.ts +77 -0
- package/dist/doctor/migrations/findings-filename.d.ts.map +1 -0
- package/dist/doctor/migrations/findings-filename.js +189 -0
- package/dist/doctor/migrations/findings-filename.js.map +1 -0
- package/dist/doctor/migrations/index.d.ts.map +1 -1
- package/dist/doctor/migrations/index.js +8 -0
- package/dist/doctor/migrations/index.js.map +1 -1
- package/dist/review/findings-naming.d.ts +112 -0
- package/dist/review/findings-naming.d.ts.map +1 -0
- package/dist/review/findings-naming.js +132 -0
- package/dist/review/findings-naming.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How large is the context this session is carrying, and is it over budget?
|
|
3
|
+
*
|
|
4
|
+
* 91% of what an AIDLC project spends on inference is cache traffic — re-reading
|
|
5
|
+
* the same conversation prefix, turn after turn. Measured over 20,920 attributed
|
|
6
|
+
* ledger records, the average carried prefix grows monotonically through the
|
|
7
|
+
* lifecycle: 80k tokens in ideation, 164k in requirements, 259k in implementation,
|
|
8
|
+
* 395k in deployment, 791k in maintenance. The `complete` phase does comparable
|
|
9
|
+
* work at 76k for one reason — it runs in its own session.
|
|
10
|
+
*
|
|
11
|
+
* So a phase boundary is a free context reset, and this is how a phase skill checks
|
|
12
|
+
* whether it should have taken one.
|
|
13
|
+
*
|
|
14
|
+
* **Reads the transcript, not the ledger.** The question is asked at a phase
|
|
15
|
+
* boundary, which is exactly when no capture has run recently and the ledger is
|
|
16
|
+
* stale or empty — and it must answer on a machine where no cost hook was ever
|
|
17
|
+
* installed, because this project already has a lesson about a capability whose
|
|
18
|
+
* arming was manual and was therefore inert on arrival.
|
|
19
|
+
*
|
|
20
|
+
* Only token counts are read. No transcript content — no prompt text, no file
|
|
21
|
+
* body, no tool output — is copied anywhere, so nothing here can leak a secret into
|
|
22
|
+
* a report (NFR-4).
|
|
23
|
+
*
|
|
24
|
+
* Requirements: context-budget/AC-16, AC-17, AC-18, AC-19; NFR-4
|
|
25
|
+
* Design: design.md Part 4 (D4.3, D4.4)
|
|
26
|
+
*
|
|
27
|
+
* @module
|
|
28
|
+
*/
|
|
29
|
+
import type { CostConfig } from './types.js';
|
|
30
|
+
import type { ProviderEnv } from './providers/provider.js';
|
|
31
|
+
export interface ContextReading {
|
|
32
|
+
/**
|
|
33
|
+
* Resident prefix at the most recent API call, in tokens — everything the model
|
|
34
|
+
* had to be *given*. Null when no usage-bearing transcript line was found for
|
|
35
|
+
* this project, which is an ordinary state (a fresh session that has not yet made
|
|
36
|
+
* a call), not a failure.
|
|
37
|
+
*/
|
|
38
|
+
residentTokens: number | null;
|
|
39
|
+
/** Model of that call, for context. Null when unknown. */
|
|
40
|
+
model: string | null;
|
|
41
|
+
/** ISO timestamp of that call. Null when unknown. */
|
|
42
|
+
at: string | null;
|
|
43
|
+
/** The budget in force, from config or the measured default. */
|
|
44
|
+
budgetTokens: number;
|
|
45
|
+
/** True when `residentTokens` exceeds the budget. Always false when unmeasured. */
|
|
46
|
+
overBudget: boolean;
|
|
47
|
+
/** Transcripts belonging to this project that were considered. */
|
|
48
|
+
transcriptsScanned: number;
|
|
49
|
+
/**
|
|
50
|
+
* Transcripts that could not be read through to the end.
|
|
51
|
+
*
|
|
52
|
+
* Counted rather than swallowed. A silent fail-soft here would report `unmeasured`
|
|
53
|
+
* with no reason on a machine where every transcript is unreadable, which looks
|
|
54
|
+
* identical to a fresh session — and "an unmeasured reading with no explanation" is
|
|
55
|
+
* the shape of failure this project has already paid for once. SOC 2 CC7.2 asks the
|
|
56
|
+
* same thing of any failure path: record it with enough context to notice.
|
|
57
|
+
*/
|
|
58
|
+
transcriptsUnreadable: number;
|
|
59
|
+
}
|
|
60
|
+
/** The budget in force: config when set to a positive number, else the measured default. */
|
|
61
|
+
export declare function contextBudget(config: CostConfig | null): number;
|
|
62
|
+
/**
|
|
63
|
+
* Read resident context for this project's most recent API call.
|
|
64
|
+
*
|
|
65
|
+
* Scopes to this repository via `projectClaudeCodeRecords`, which resolves each
|
|
66
|
+
* transcript's `cwd` to a git common dir rather than comparing path prefixes — the
|
|
67
|
+
* transcript root holds every project this machine has ever worked on, and a
|
|
68
|
+
* prefix match once put 711 entries of another repository's work into this one's
|
|
69
|
+
* ledgers.
|
|
70
|
+
*/
|
|
71
|
+
export declare function readContext(projectRoot: string, config: CostConfig | null, env?: ProviderEnv): Promise<ContextReading>;
|
|
72
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/cost/context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG7C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE3D,MAAM,WAAW,cAAc;IAC7B;;;;;OAKG;IACH,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,0DAA0D;IAC1D,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,qDAAqD;IACrD,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,gEAAgE;IAChE,YAAY,EAAE,MAAM,CAAC;IACrB,mFAAmF;IACnF,UAAU,EAAE,OAAO,CAAC;IACpB,kEAAkE;IAClE,kBAAkB,EAAE,MAAM,CAAC;IAC3B;;;;;;;;OAQG;IACH,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAED,4FAA4F;AAC5F,wBAAgB,aAAa,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,GAAG,MAAM,CAG/D;AAyFD;;;;;;;;GAQG;AACH,wBAAsB,WAAW,CAC/B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,UAAU,GAAG,IAAI,EACzB,GAAG,GAAE,WAA6B,GACjC,OAAO,CAAC,cAAc,CAAC,CAwBzB"}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How large is the context this session is carrying, and is it over budget?
|
|
3
|
+
*
|
|
4
|
+
* 91% of what an AIDLC project spends on inference is cache traffic — re-reading
|
|
5
|
+
* the same conversation prefix, turn after turn. Measured over 20,920 attributed
|
|
6
|
+
* ledger records, the average carried prefix grows monotonically through the
|
|
7
|
+
* lifecycle: 80k tokens in ideation, 164k in requirements, 259k in implementation,
|
|
8
|
+
* 395k in deployment, 791k in maintenance. The `complete` phase does comparable
|
|
9
|
+
* work at 76k for one reason — it runs in its own session.
|
|
10
|
+
*
|
|
11
|
+
* So a phase boundary is a free context reset, and this is how a phase skill checks
|
|
12
|
+
* whether it should have taken one.
|
|
13
|
+
*
|
|
14
|
+
* **Reads the transcript, not the ledger.** The question is asked at a phase
|
|
15
|
+
* boundary, which is exactly when no capture has run recently and the ledger is
|
|
16
|
+
* stale or empty — and it must answer on a machine where no cost hook was ever
|
|
17
|
+
* installed, because this project already has a lesson about a capability whose
|
|
18
|
+
* arming was manual and was therefore inert on arrival.
|
|
19
|
+
*
|
|
20
|
+
* Only token counts are read. No transcript content — no prompt text, no file
|
|
21
|
+
* body, no tool output — is copied anywhere, so nothing here can leak a secret into
|
|
22
|
+
* a report (NFR-4).
|
|
23
|
+
*
|
|
24
|
+
* Requirements: context-budget/AC-16, AC-17, AC-18, AC-19; NFR-4
|
|
25
|
+
* Design: design.md Part 4 (D4.3, D4.4)
|
|
26
|
+
*
|
|
27
|
+
* @module
|
|
28
|
+
*/
|
|
29
|
+
import { createReadStream } from 'node:fs';
|
|
30
|
+
import { createInterface } from 'node:readline';
|
|
31
|
+
import { DEFAULT_CONTEXT_BUDGET_TOKENS, MODEL_ID_RE } from './types.js';
|
|
32
|
+
import { projectClaudeCodeRecords } from './sync.js';
|
|
33
|
+
/** The budget in force: config when set to a positive number, else the measured default. */
|
|
34
|
+
export function contextBudget(config) {
|
|
35
|
+
const v = config?.context_budget_tokens;
|
|
36
|
+
return typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : DEFAULT_CONTEXT_BUDGET_TOKENS;
|
|
37
|
+
}
|
|
38
|
+
function isCount(v) {
|
|
39
|
+
return typeof v === 'number' && Number.isFinite(v) && v >= 0;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The newest usage-bearing line in one transcript.
|
|
43
|
+
*
|
|
44
|
+
* Streams rather than reading the file: transcripts here run to megabytes, and this
|
|
45
|
+
* is called at a phase boundary where the answer must be cheap enough that nobody
|
|
46
|
+
* skips asking for it.
|
|
47
|
+
*/
|
|
48
|
+
async function latestOf(path) {
|
|
49
|
+
let best = null;
|
|
50
|
+
let rl;
|
|
51
|
+
try {
|
|
52
|
+
rl = createInterface({
|
|
53
|
+
input: createReadStream(path, { encoding: 'utf8' }),
|
|
54
|
+
crlfDelay: Infinity,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return { latest: null, readable: false };
|
|
59
|
+
}
|
|
60
|
+
let readable = true;
|
|
61
|
+
/*
|
|
62
|
+
* Fail-soft on the *stream*, not just on each line.
|
|
63
|
+
*
|
|
64
|
+
* `createInterface` does not throw for a path that is gone — the error surfaces
|
|
65
|
+
* when the stream is iterated, so a transcript deleted or rotated between the
|
|
66
|
+
* directory walk and this read would otherwise reject out of here and take
|
|
67
|
+
* `aidlc cost context` down with it. Transcripts are churning files owned by
|
|
68
|
+
* another process; a read that loses one must report what it has, exactly as the
|
|
69
|
+
* provider skips a malformed line.
|
|
70
|
+
*/
|
|
71
|
+
try {
|
|
72
|
+
for await (const line of rl) {
|
|
73
|
+
const t = line.trim();
|
|
74
|
+
if (t === '')
|
|
75
|
+
continue;
|
|
76
|
+
let entry;
|
|
77
|
+
try {
|
|
78
|
+
entry = JSON.parse(t);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Fail-soft, exactly as the provider is: a malformed line is skipped, never
|
|
82
|
+
// fatal. A truncated last line is normal in a file being appended to.
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const message = entry.message;
|
|
86
|
+
const usage = message?.usage;
|
|
87
|
+
const model = typeof message?.model === 'string' ? message.model : null;
|
|
88
|
+
const at = typeof entry.timestamp === 'string' && !Number.isNaN(Date.parse(entry.timestamp))
|
|
89
|
+
? entry.timestamp
|
|
90
|
+
: null;
|
|
91
|
+
if (!usage || model === null || !MODEL_ID_RE.test(model) || at === null)
|
|
92
|
+
continue;
|
|
93
|
+
/*
|
|
94
|
+
* Resident context = everything given to the model, output excluded.
|
|
95
|
+
*
|
|
96
|
+
* The three input-side counters are disjoint in Anthropic's shape — fresh
|
|
97
|
+
* input, cache reads, cache writes — so adding them is a sum, not a double
|
|
98
|
+
* count. Output is excluded because it is what the turn produced, not what it
|
|
99
|
+
* had to carry.
|
|
100
|
+
*/
|
|
101
|
+
const tokens = (isCount(usage.input_tokens) ? usage.input_tokens : 0) +
|
|
102
|
+
(isCount(usage.cache_read_input_tokens) ? usage.cache_read_input_tokens : 0) +
|
|
103
|
+
(isCount(usage.cache_creation_input_tokens) ? usage.cache_creation_input_tokens : 0);
|
|
104
|
+
if (best === null || Date.parse(at) >= Date.parse(best.at)) {
|
|
105
|
+
best = { at, tokens, model };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// Stream died mid-read. Return what was seen rather than nothing — a partial
|
|
111
|
+
// reading is still a reading — but say that it happened, so an unmeasured result
|
|
112
|
+
// is never indistinguishable from a quiet failure.
|
|
113
|
+
readable = false;
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
rl.close();
|
|
117
|
+
}
|
|
118
|
+
return { latest: best, readable };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Read resident context for this project's most recent API call.
|
|
122
|
+
*
|
|
123
|
+
* Scopes to this repository via `projectClaudeCodeRecords`, which resolves each
|
|
124
|
+
* transcript's `cwd` to a git common dir rather than comparing path prefixes — the
|
|
125
|
+
* transcript root holds every project this machine has ever worked on, and a
|
|
126
|
+
* prefix match once put 711 entries of another repository's work into this one's
|
|
127
|
+
* ledgers.
|
|
128
|
+
*/
|
|
129
|
+
export async function readContext(projectRoot, config, env = { projectRoot }) {
|
|
130
|
+
const budgetTokens = contextBudget(config);
|
|
131
|
+
const records = projectClaudeCodeRecords(projectRoot, env);
|
|
132
|
+
let best = null;
|
|
133
|
+
let unreadable = 0;
|
|
134
|
+
for (const { path } of records) {
|
|
135
|
+
const { latest, readable } = await latestOf(path);
|
|
136
|
+
if (!readable)
|
|
137
|
+
unreadable++;
|
|
138
|
+
if (latest === null)
|
|
139
|
+
continue;
|
|
140
|
+
if (best === null || Date.parse(latest.at) >= Date.parse(best.at))
|
|
141
|
+
best = latest;
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
residentTokens: best?.tokens ?? null,
|
|
145
|
+
model: best?.model ?? null,
|
|
146
|
+
at: best?.at ?? null,
|
|
147
|
+
budgetTokens,
|
|
148
|
+
// Unmeasured is never "over": an absent reading must not raise an alarm it has
|
|
149
|
+
// no evidence for.
|
|
150
|
+
overBudget: best !== null && best.tokens > budgetTokens,
|
|
151
|
+
transcriptsScanned: records.length,
|
|
152
|
+
transcriptsUnreadable: unreadable,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/cost/context.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Correct ledger entries written while one API response was counted many times.
|
|
3
|
+
*
|
|
4
|
+
* The claude-code provider used to emit one usage record per transcript *line*.
|
|
5
|
+
* Claude Code writes one line per content block and repeats the response's `usage`
|
|
6
|
+
* object on every one, so a response spanning three blocks was charged three times.
|
|
7
|
+
* Measured on this repo's 53 transcripts: 17,292 usage-bearing lines against 9,357
|
|
8
|
+
* unique `message.id` values — an over-count factor of **1.848**, with 7,935 repeat
|
|
9
|
+
* lines and zero whose usage object differed.
|
|
10
|
+
*
|
|
11
|
+
* `providers/claude-code.ts` now groups by `message.id`, which fixes capture going
|
|
12
|
+
* forward. This corrects what is already on disk, by re-reading each entry's source
|
|
13
|
+
* transcript through the fixed provider.
|
|
14
|
+
*
|
|
15
|
+
* Three properties are deliberate, and each is the same choice `rederive.ts` made
|
|
16
|
+
* for the same reason:
|
|
17
|
+
*
|
|
18
|
+
* 1. **Bounded by the entry's own coverage window.** One entry's cursor sits in a
|
|
19
|
+
* 633-record transcript while the entry covers only 80 of them, so re-deriving
|
|
20
|
+
* over the whole file inflated it 23x there. The bound is what makes this
|
|
21
|
+
* arithmetic rather than estimation.
|
|
22
|
+
* 2. **A missing transcript is reported, never estimated.** An entry that cannot be
|
|
23
|
+
* re-read is left byte-identical and counted. Scaling it by the measured 1.848x
|
|
24
|
+
* is explicitly rejected: an estimate that looks like a measurement is the
|
|
25
|
+
* defect this whole change exists to remove. `provenance.ts` keeps `unknown`
|
|
26
|
+
* distinct from `foreign` for the same reason — under-recovering is the safe
|
|
27
|
+
* direction.
|
|
28
|
+
* 3. **Verification is on precedence-weighted dollars, not raw token sums.** Raw
|
|
29
|
+
* sums can balance while weighted totals move, which is the exact shape of a
|
|
30
|
+
* defect that once reported a plausible total while double-counting 950 entries.
|
|
31
|
+
*
|
|
32
|
+
* Read-only unless `apply` is set. The plan names the dollar delta before anything
|
|
33
|
+
* is written, because applying it moves every cost figure this project has already
|
|
34
|
+
* published in a retrospective or a blog post.
|
|
35
|
+
*
|
|
36
|
+
* Requirements: context-budget/AC-11, AC-12, AC-13; NFR-5
|
|
37
|
+
* Design: design.md Part 3
|
|
38
|
+
*
|
|
39
|
+
* @module
|
|
40
|
+
*/
|
|
41
|
+
import type { CostConfig } from './types.js';
|
|
42
|
+
import type { ProviderEnv } from './providers/provider.js';
|
|
43
|
+
import type { ProvenanceIndex } from './provenance.js';
|
|
44
|
+
export interface DedupeReport {
|
|
45
|
+
/** Priced entries examined. */
|
|
46
|
+
scanned: number;
|
|
47
|
+
/** Entries whose units were re-derived to a smaller figure. */
|
|
48
|
+
rederived: number;
|
|
49
|
+
/** Entries re-read and found already correct. */
|
|
50
|
+
unchanged: number;
|
|
51
|
+
/**
|
|
52
|
+
* Entries that are provably a second charge for a response already counted.
|
|
53
|
+
*
|
|
54
|
+
* These are the bulk of the historical defect, and they are *extra entries*
|
|
55
|
+
* rather than inflated ones: `rederive.ts` split most buckets into one zero-width
|
|
56
|
+
* entry per transcript line, so each duplicate line became its own ledger entry.
|
|
57
|
+
* Re-deriving such an entry is meaningless — the correct figure for it is that it
|
|
58
|
+
* should not exist.
|
|
59
|
+
*
|
|
60
|
+
* The proof is what separates this from `transcriptGone`: the entry's transcript
|
|
61
|
+
* **resolved**, and the deduped read of it emits no record at this entry's own
|
|
62
|
+
* instant. A timestamp disappears from the deduped output for exactly one reason —
|
|
63
|
+
* its line was a non-leading content block of a response whose usage is now
|
|
64
|
+
* carried by the group's first line. So the charge exists elsewhere.
|
|
65
|
+
*
|
|
66
|
+
* Dropped under `--apply`; counted and left alone in a plan.
|
|
67
|
+
*/
|
|
68
|
+
duplicates: number;
|
|
69
|
+
/** Micro-USD carried by those duplicate entries, unweighted. */
|
|
70
|
+
duplicateMicroUsd: number;
|
|
71
|
+
/**
|
|
72
|
+
* Entries whose transcript could not be found, with the reason, left byte-identical.
|
|
73
|
+
*
|
|
74
|
+
* Kept strictly separate from `duplicates` because the two justify opposite
|
|
75
|
+
* actions and only one of them is proven. A cursor that resolves nowhere is an
|
|
76
|
+
* honest unknown — the transcript rotated or was deleted — and an unknown is never
|
|
77
|
+
* resolved by deleting data or by scaling it to a plausible figure.
|
|
78
|
+
*/
|
|
79
|
+
transcriptGone: string[];
|
|
80
|
+
/** Entries a human placed with `cost record --manual`; never overruled. */
|
|
81
|
+
manualSkipped: number;
|
|
82
|
+
/** Precedence-weighted dollars before, in micro-USD. */
|
|
83
|
+
microUsdBefore: number;
|
|
84
|
+
/** Precedence-weighted dollars after, in micro-USD. */
|
|
85
|
+
microUsdAfter: number;
|
|
86
|
+
/** False for a plan; true when ledgers were rewritten. */
|
|
87
|
+
applied: boolean;
|
|
88
|
+
/** Ledger files that would change (or did). */
|
|
89
|
+
files: string[];
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Plan (or apply) the dedupe correction across every ledger.
|
|
93
|
+
*
|
|
94
|
+
* @param apply - false plans and writes nothing; true rewrites each changed ledger
|
|
95
|
+
* atomically via `rewriteLedger`, all-or-nothing per file (NFR-5).
|
|
96
|
+
*/
|
|
97
|
+
export declare function dedupeHistory(projectRoot: string, config: CostConfig | null, apply: boolean, env?: ProviderEnv, provenanceFiles?: ProvenanceIndex): Promise<DedupeReport>;
|
|
98
|
+
//# sourceMappingURL=dedupe-history.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dedupe-history.d.ts","sourceRoot":"","sources":["../../src/cost/dedupe-history.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAIH,OAAO,KAAK,EAAa,UAAU,EAA4B,MAAM,YAAY,CAAC;AAUlF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD,MAAM,WAAW,YAAY;IAC3B,+BAA+B;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,+DAA+D;IAC/D,SAAS,EAAE,MAAM,CAAC;IAClB,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;;;;;;;;;;;OAgBG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,gEAAgE;IAChE,iBAAiB,EAAE,MAAM,CAAC;IAC1B;;;;;;;OAOG;IACH,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,2EAA2E;IAC3E,aAAa,EAAE,MAAM,CAAC;IACtB,wDAAwD;IACxD,cAAc,EAAE,MAAM,CAAC;IACvB,uDAAuD;IACvD,aAAa,EAAE,MAAM,CAAC;IACtB,0DAA0D;IAC1D,OAAO,EAAE,OAAO,CAAC;IACjB,+CAA+C;IAC/C,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AA6HD;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,UAAU,GAAG,IAAI,EACzB,KAAK,EAAE,OAAO,EACd,GAAG,GAAE,WAA6B,EAClC,eAAe,CAAC,EAAE,eAAe,GAChC,OAAO,CAAC,YAAY,CAAC,CA0GvB"}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Correct ledger entries written while one API response was counted many times.
|
|
3
|
+
*
|
|
4
|
+
* The claude-code provider used to emit one usage record per transcript *line*.
|
|
5
|
+
* Claude Code writes one line per content block and repeats the response's `usage`
|
|
6
|
+
* object on every one, so a response spanning three blocks was charged three times.
|
|
7
|
+
* Measured on this repo's 53 transcripts: 17,292 usage-bearing lines against 9,357
|
|
8
|
+
* unique `message.id` values — an over-count factor of **1.848**, with 7,935 repeat
|
|
9
|
+
* lines and zero whose usage object differed.
|
|
10
|
+
*
|
|
11
|
+
* `providers/claude-code.ts` now groups by `message.id`, which fixes capture going
|
|
12
|
+
* forward. This corrects what is already on disk, by re-reading each entry's source
|
|
13
|
+
* transcript through the fixed provider.
|
|
14
|
+
*
|
|
15
|
+
* Three properties are deliberate, and each is the same choice `rederive.ts` made
|
|
16
|
+
* for the same reason:
|
|
17
|
+
*
|
|
18
|
+
* 1. **Bounded by the entry's own coverage window.** One entry's cursor sits in a
|
|
19
|
+
* 633-record transcript while the entry covers only 80 of them, so re-deriving
|
|
20
|
+
* over the whole file inflated it 23x there. The bound is what makes this
|
|
21
|
+
* arithmetic rather than estimation.
|
|
22
|
+
* 2. **A missing transcript is reported, never estimated.** An entry that cannot be
|
|
23
|
+
* re-read is left byte-identical and counted. Scaling it by the measured 1.848x
|
|
24
|
+
* is explicitly rejected: an estimate that looks like a measurement is the
|
|
25
|
+
* defect this whole change exists to remove. `provenance.ts` keeps `unknown`
|
|
26
|
+
* distinct from `foreign` for the same reason — under-recovering is the safe
|
|
27
|
+
* direction.
|
|
28
|
+
* 3. **Verification is on precedence-weighted dollars, not raw token sums.** Raw
|
|
29
|
+
* sums can balance while weighted totals move, which is the exact shape of a
|
|
30
|
+
* defect that once reported a plausible total while double-counting 950 entries.
|
|
31
|
+
*
|
|
32
|
+
* Read-only unless `apply` is set. The plan names the dollar delta before anything
|
|
33
|
+
* is written, because applying it moves every cost figure this project has already
|
|
34
|
+
* published in a retrospective or a blog post.
|
|
35
|
+
*
|
|
36
|
+
* Requirements: context-budget/AC-11, AC-12, AC-13; NFR-5
|
|
37
|
+
* Design: design.md Part 3
|
|
38
|
+
*
|
|
39
|
+
* @module
|
|
40
|
+
*/
|
|
41
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
42
|
+
import { join } from 'node:path';
|
|
43
|
+
import { readEntries, rewriteLedger, applyPrecedence, ledgerPath, unattributedPath, } from './ledger.js';
|
|
44
|
+
import { effectiveRates, priceUnits } from './rates.js';
|
|
45
|
+
import { claudeCodeProvider } from './providers/claude-code.js';
|
|
46
|
+
import { buildCursorFileIndex } from './rederive.js';
|
|
47
|
+
function emptyReport(applied) {
|
|
48
|
+
return {
|
|
49
|
+
scanned: 0,
|
|
50
|
+
rederived: 0,
|
|
51
|
+
unchanged: 0,
|
|
52
|
+
duplicates: 0,
|
|
53
|
+
duplicateMicroUsd: 0,
|
|
54
|
+
transcriptGone: [],
|
|
55
|
+
manualSkipped: 0,
|
|
56
|
+
microUsdBefore: 0,
|
|
57
|
+
microUsdAfter: 0,
|
|
58
|
+
applied,
|
|
59
|
+
files: [],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Locate the single transcript holding `cursor`, or null when it is gone or ambiguous. */
|
|
63
|
+
function fileForCursor(cursor, index) {
|
|
64
|
+
if (cursor === null)
|
|
65
|
+
return null;
|
|
66
|
+
const files = index.get(cursor);
|
|
67
|
+
// A uuid in two files is ambiguous, never resolved by picking one: `/branch`
|
|
68
|
+
// copies transcripts, so the same uuid legitimately appears twice.
|
|
69
|
+
if (files === undefined || files.size !== 1)
|
|
70
|
+
return null;
|
|
71
|
+
return [...files][0];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Precedence-weighted micro-USD over a set of entries.
|
|
75
|
+
*
|
|
76
|
+
* Weighted, not raw, because `applyPrecedence` reduces overlap across fidelity
|
|
77
|
+
* ranks — a total that ignores it can balance while the real figure moves.
|
|
78
|
+
*/
|
|
79
|
+
function weightedMicroUsd(entries, rates) {
|
|
80
|
+
let micro = 0;
|
|
81
|
+
for (const { entry, weight } of applyPrecedence(entries)) {
|
|
82
|
+
if (weight === 0 || entry.units === null)
|
|
83
|
+
continue;
|
|
84
|
+
const price = priceUnits(entry.units, rates);
|
|
85
|
+
if (price !== null)
|
|
86
|
+
micro += price * weight;
|
|
87
|
+
}
|
|
88
|
+
return Math.round(micro);
|
|
89
|
+
}
|
|
90
|
+
function sameTokens(a, b) {
|
|
91
|
+
return (a.model === b.model &&
|
|
92
|
+
a.input_tokens === b.input_tokens &&
|
|
93
|
+
a.output_tokens === b.output_tokens &&
|
|
94
|
+
a.cache_read_tokens === b.cache_read_tokens &&
|
|
95
|
+
a.cache_write_tokens === b.cache_write_tokens);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Re-derive one entry's units from its transcript.
|
|
99
|
+
*
|
|
100
|
+
* Three outcomes, deliberately distinct because they justify three different
|
|
101
|
+
* actions: corrected units (rewrite), `'duplicate'` (drop — the charge is already
|
|
102
|
+
* counted elsewhere, provably), or `'unreadable'` (leave alone — an honest unknown).
|
|
103
|
+
*
|
|
104
|
+
* Returns the number of unique responses behind the corrected units, so the entry
|
|
105
|
+
* can carry a response count that pre-fix entries lack.
|
|
106
|
+
*/
|
|
107
|
+
async function rederiveUnits(entry, source, env, cache) {
|
|
108
|
+
/*
|
|
109
|
+
* One parse per transcript, not one per entry.
|
|
110
|
+
*
|
|
111
|
+
* Many entries share a source — this repo has 30,824 candidate entries across 53
|
|
112
|
+
* transcripts — so re-extracting per entry re-reads each megabyte file hundreds of
|
|
113
|
+
* times. Cached by path, which is safe because a single pass is a point-in-time
|
|
114
|
+
* correction: re-reading the same file twice mid-run and getting different answers
|
|
115
|
+
* would make the plan disagree with the apply.
|
|
116
|
+
*
|
|
117
|
+
* `null` caches a *failure* too, so an unreadable file is attempted once rather
|
|
118
|
+
* than once per entry that points at it.
|
|
119
|
+
*/
|
|
120
|
+
let segments = cache.get(source);
|
|
121
|
+
if (segments === undefined) {
|
|
122
|
+
try {
|
|
123
|
+
segments = await claudeCodeProvider.extract({ provider: 'claude-code', path: source }, null, env);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
segments = null;
|
|
127
|
+
}
|
|
128
|
+
cache.set(source, segments);
|
|
129
|
+
}
|
|
130
|
+
if (segments === null)
|
|
131
|
+
return 'unreadable';
|
|
132
|
+
if (entry.units === null || entry.units.kind !== 'tokens')
|
|
133
|
+
return 'unreadable';
|
|
134
|
+
const model = entry.units.model;
|
|
135
|
+
const lo = Date.parse(entry.coverage.from);
|
|
136
|
+
const hi = Date.parse(entry.coverage.to);
|
|
137
|
+
const sum = {
|
|
138
|
+
kind: 'tokens',
|
|
139
|
+
model,
|
|
140
|
+
input_tokens: 0,
|
|
141
|
+
output_tokens: 0,
|
|
142
|
+
cache_read_tokens: 0,
|
|
143
|
+
cache_write_tokens: 0,
|
|
144
|
+
};
|
|
145
|
+
let responses = 0;
|
|
146
|
+
for (const s of segments) {
|
|
147
|
+
if (s.units.kind !== 'tokens' || s.units.model !== model)
|
|
148
|
+
continue;
|
|
149
|
+
const at = Date.parse(s.at);
|
|
150
|
+
if (at < lo || at > hi)
|
|
151
|
+
continue;
|
|
152
|
+
sum.input_tokens += s.units.input_tokens;
|
|
153
|
+
sum.output_tokens += s.units.output_tokens;
|
|
154
|
+
sum.cache_read_tokens += s.units.cache_read_tokens;
|
|
155
|
+
sum.cache_write_tokens += s.units.cache_write_tokens;
|
|
156
|
+
responses += 1;
|
|
157
|
+
}
|
|
158
|
+
// No deduped record inside the window, in a transcript that *was* found. The
|
|
159
|
+
// entry's line was a non-leading content block of a response now carried by its
|
|
160
|
+
// group's first line, so this entry is a second charge for something already
|
|
161
|
+
// counted. Signalled distinctly from a read failure, because it justifies dropping
|
|
162
|
+
// the entry while a read failure justifies leaving it alone.
|
|
163
|
+
if (responses === 0)
|
|
164
|
+
return 'duplicate';
|
|
165
|
+
return { units: sum, responses };
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Plan (or apply) the dedupe correction across every ledger.
|
|
169
|
+
*
|
|
170
|
+
* @param apply - false plans and writes nothing; true rewrites each changed ledger
|
|
171
|
+
* atomically via `rewriteLedger`, all-or-nothing per file (NFR-5).
|
|
172
|
+
*/
|
|
173
|
+
export async function dedupeHistory(projectRoot, config, apply, env = { projectRoot }, provenanceFiles) {
|
|
174
|
+
const report = emptyReport(apply);
|
|
175
|
+
const rates = effectiveRates(projectRoot, config);
|
|
176
|
+
const targets = [];
|
|
177
|
+
const stateDir = join(projectRoot, '.aidlc', 'state');
|
|
178
|
+
if (existsSync(stateDir)) {
|
|
179
|
+
for (const d of readdirSync(stateDir, { withFileTypes: true })) {
|
|
180
|
+
if (!d.isDirectory())
|
|
181
|
+
continue;
|
|
182
|
+
const p = ledgerPath(projectRoot, d.name);
|
|
183
|
+
if (existsSync(p))
|
|
184
|
+
targets.push(p);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const un = unattributedPath(projectRoot);
|
|
188
|
+
if (existsSync(un))
|
|
189
|
+
targets.push(un);
|
|
190
|
+
if (targets.length === 0)
|
|
191
|
+
return report;
|
|
192
|
+
const cursorFiles = provenanceFiles ?? buildCursorFileIndex(env);
|
|
193
|
+
/** Parsed segments per transcript path, shared across every entry in this run. */
|
|
194
|
+
const segmentCache = new Map();
|
|
195
|
+
for (const file of targets) {
|
|
196
|
+
const entries = readEntries(file);
|
|
197
|
+
report.microUsdBefore += weightedMicroUsd(entries, rates);
|
|
198
|
+
const kept = [];
|
|
199
|
+
let changed = false;
|
|
200
|
+
for (const e of entries) {
|
|
201
|
+
// A human placed these deliberately; a re-read must not overrule them.
|
|
202
|
+
if (e.provider === 'manual') {
|
|
203
|
+
report.manualSkipped++;
|
|
204
|
+
kept.push(e);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
// `unknown`-fidelity interval markers carry no units and no money.
|
|
208
|
+
if (e.units === null || e.units.kind !== 'tokens') {
|
|
209
|
+
kept.push(e);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
// Already counted per response — nothing to correct.
|
|
213
|
+
if (typeof e.responses === 'number') {
|
|
214
|
+
kept.push(e);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
report.scanned++;
|
|
218
|
+
const source = fileForCursor(e.cursor, cursorFiles);
|
|
219
|
+
if (source === null) {
|
|
220
|
+
report.transcriptGone.push(`${file}: cursor ${e.cursor ?? 'null'} has no unique transcript`);
|
|
221
|
+
kept.push(e);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const fixed = await rederiveUnits(e, source, env, segmentCache);
|
|
225
|
+
if (fixed === 'unreadable') {
|
|
226
|
+
report.transcriptGone.push(`${file}: could not read ${source} for ${e.cursor ?? 'null'}`);
|
|
227
|
+
kept.push(e);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (fixed === 'duplicate') {
|
|
231
|
+
/*
|
|
232
|
+
* A second charge for a response counted elsewhere — see
|
|
233
|
+
* DedupeReport.duplicates for why this is proven rather than inferred.
|
|
234
|
+
*
|
|
235
|
+
* Dropped from `kept` in **both** modes, because `kept` is what
|
|
236
|
+
* `microUsdAfter` is measured from and a plan whose projected total ignores
|
|
237
|
+
* its own largest correction is not a plan. Only `rewriteLedger` is gated on
|
|
238
|
+
* `apply`.
|
|
239
|
+
*/
|
|
240
|
+
report.duplicates++;
|
|
241
|
+
const price = priceUnits(e.units, rates);
|
|
242
|
+
if (price !== null)
|
|
243
|
+
report.duplicateMicroUsd += price;
|
|
244
|
+
changed = true;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (sameTokens(e.units, fixed.units)) {
|
|
248
|
+
// Re-read and identical: the entry was never a duplicate. It still gains a
|
|
249
|
+
// response count, which is new information and costs nothing.
|
|
250
|
+
report.unchanged++;
|
|
251
|
+
kept.push({ ...e, responses: fixed.responses });
|
|
252
|
+
changed = true;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
/*
|
|
256
|
+
* `reconciled` is how this schema already says "corrected after the fact",
|
|
257
|
+
* and `applyPrecedence` already ranks it above `metered`. So a reader of an
|
|
258
|
+
* older retrospective can tell a moved figure from a historical one without a
|
|
259
|
+
* new field, and no new fidelity value is minted (AC-11).
|
|
260
|
+
*/
|
|
261
|
+
kept.push({ ...e, units: fixed.units, responses: fixed.responses, fidelity: 'reconciled' });
|
|
262
|
+
report.rederived++;
|
|
263
|
+
changed = true;
|
|
264
|
+
}
|
|
265
|
+
report.microUsdAfter += weightedMicroUsd(kept, rates);
|
|
266
|
+
if (changed) {
|
|
267
|
+
report.files.push(file);
|
|
268
|
+
// Atomic per file: an interrupted run leaves the original intact (NFR-5).
|
|
269
|
+
if (apply)
|
|
270
|
+
rewriteLedger(file, kept);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return report;
|
|
274
|
+
}
|
|
275
|
+
//# sourceMappingURL=dedupe-history.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dedupe-history.js","sourceRoot":"","sources":["../../src/cost/dedupe-history.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../../src/cost/ledger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAaH,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAa,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../../src/cost/ledger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAaH,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAa,MAAM,YAAY,CAAC;AAuDrE;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CA8C3D;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,IAAI,CAUtE;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,EAAE,CAkC3D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,IAAI,CAU5E;AAED,mCAAmC;AACnC,wBAAgB,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAExE;AAED,uCAAuC;AACvC,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CAerF;AAiDD,uEAAuE;AACvE,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,SAAS,CAAC;IACjB,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,aAAa,EAAE,CA8CrE;AAED,+DAA+D;AAC/D,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC9F,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;CAC/C;AAED,wBAAgB,UAAU,CAAC,QAAQ,EAAE,aAAa,EAAE,GAAG,WAAW,CAsBjE"}
|