sentinelayer-cli 0.30.7 → 0.30.8
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/package.json
CHANGED
|
@@ -49,7 +49,12 @@ const INVESTOR_DD_PERSONAS = Object.freeze([...FULL_DEPTH_PERSONAS]);
|
|
|
49
49
|
* Walk the target repo and return a list of relative POSIX file paths.
|
|
50
50
|
* Skips common noise directories.
|
|
51
51
|
*/
|
|
52
|
-
|
|
52
|
+
function isPathInsideOrSame(candidate, root) {
|
|
53
|
+
const rel = path.relative(root, candidate);
|
|
54
|
+
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function walkRepoFiles(rootPath, { excludeRoots = [] } = {}) {
|
|
53
58
|
const SKIP_DIRS = new Set([
|
|
54
59
|
"node_modules",
|
|
55
60
|
".git",
|
|
@@ -84,7 +89,11 @@ async function walkRepoFiles(rootPath) {
|
|
|
84
89
|
]);
|
|
85
90
|
|
|
86
91
|
const results = [];
|
|
92
|
+
const excludedRoots = excludeRoots.map((item) => path.resolve(item));
|
|
87
93
|
async function walk(absDir, relDir) {
|
|
94
|
+
if (excludedRoots.some((excludedRoot) => isPathInsideOrSame(absDir, excludedRoot))) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
88
97
|
let entries;
|
|
89
98
|
try {
|
|
90
99
|
entries = await fsp.readdir(absDir, { withFileTypes: true });
|
|
@@ -99,6 +108,7 @@ async function walkRepoFiles(rootPath) {
|
|
|
99
108
|
const absPath = path.join(absDir, entry.name);
|
|
100
109
|
if (entry.isDirectory()) {
|
|
101
110
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
111
|
+
if (excludedRoots.some((excludedRoot) => isPathInsideOrSame(absPath, excludedRoot))) continue;
|
|
102
112
|
await walk(absPath, relPath);
|
|
103
113
|
} else if (entry.isFile()) {
|
|
104
114
|
const ext = path.extname(entry.name).toLowerCase();
|
|
@@ -111,6 +121,45 @@ async function walkRepoFiles(rootPath) {
|
|
|
111
121
|
return results;
|
|
112
122
|
}
|
|
113
123
|
|
|
124
|
+
function countSourceLoc(text) {
|
|
125
|
+
return String(text || "")
|
|
126
|
+
.split(/\r\n|\n|\r/)
|
|
127
|
+
.filter((line) => line.trim().length > 0).length;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function collectFileMetrics(rootPath, files) {
|
|
131
|
+
const metrics = {};
|
|
132
|
+
for (const file of files) {
|
|
133
|
+
const rel = String(file || "").trim();
|
|
134
|
+
if (!rel) continue;
|
|
135
|
+
const abs = path.resolve(rootPath, rel);
|
|
136
|
+
try {
|
|
137
|
+
const stat = await fsp.stat(abs);
|
|
138
|
+
if (!stat.isFile()) continue;
|
|
139
|
+
let loc = 0;
|
|
140
|
+
let truncated = false;
|
|
141
|
+
if (stat.size <= 2_000_000) {
|
|
142
|
+
const text = await fsp.readFile(abs, "utf-8");
|
|
143
|
+
loc = countSourceLoc(text);
|
|
144
|
+
} else {
|
|
145
|
+
truncated = true;
|
|
146
|
+
}
|
|
147
|
+
metrics[rel] = {
|
|
148
|
+
bytes: stat.size,
|
|
149
|
+
loc,
|
|
150
|
+
truncated,
|
|
151
|
+
};
|
|
152
|
+
} catch {
|
|
153
|
+
metrics[rel] = {
|
|
154
|
+
bytes: 0,
|
|
155
|
+
loc: 0,
|
|
156
|
+
missing: true,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return metrics;
|
|
161
|
+
}
|
|
162
|
+
|
|
114
163
|
async function writeJson(filePath, obj) {
|
|
115
164
|
await fsp.mkdir(path.dirname(filePath), { recursive: true });
|
|
116
165
|
await fsp.writeFile(filePath, JSON.stringify(obj, null, 2), "utf-8");
|
|
@@ -317,11 +366,18 @@ export async function runInvestorDd({
|
|
|
317
366
|
const startTime = Date.now();
|
|
318
367
|
emit({ type: "investor_dd_start", rootPath, personas });
|
|
319
368
|
|
|
320
|
-
const files = await walkRepoFiles(rootPath);
|
|
369
|
+
const files = await walkRepoFiles(rootPath, { excludeRoots: [runRoot] });
|
|
321
370
|
emit({ type: "investor_dd_files_discovered", totalFiles: files.length });
|
|
322
371
|
|
|
323
372
|
const routing = routeFilesToPersonas({ files, personas });
|
|
324
373
|
const routingSummary = summarizeRouting(routing);
|
|
374
|
+
const fileMetrics = await collectFileMetrics(rootPath, files);
|
|
375
|
+
await writeJson(path.join(artifactBase, "file-metrics.json"), {
|
|
376
|
+
runId,
|
|
377
|
+
rootPath,
|
|
378
|
+
totalFiles: files.length,
|
|
379
|
+
metrics: fileMetrics,
|
|
380
|
+
});
|
|
325
381
|
await writeJson(path.join(artifactBase, "plan.json"), {
|
|
326
382
|
runId,
|
|
327
383
|
rootPath,
|
|
@@ -520,6 +576,7 @@ export async function runInvestorDd({
|
|
|
520
576
|
notification,
|
|
521
577
|
artifactFiles: artifactFilesBeforeManifest,
|
|
522
578
|
budgetState,
|
|
579
|
+
fileMetrics,
|
|
523
580
|
});
|
|
524
581
|
summary.ddProgress = {
|
|
525
582
|
version: ddProgress.version,
|
|
@@ -531,6 +588,7 @@ export async function runInvestorDd({
|
|
|
531
588
|
blockingGapCount: ddProgress.summary.blockingGapCount,
|
|
532
589
|
artifact: "progress.json",
|
|
533
590
|
};
|
|
591
|
+
summary.usageTelemetry = ddProgress.usageTelemetry;
|
|
534
592
|
runResult.progress = ddProgress;
|
|
535
593
|
await writeJson(path.join(artifactBase, "progress.json"), ddProgress);
|
|
536
594
|
await writeJson(path.join(artifactBase, "summary.json"), summary);
|
|
@@ -180,6 +180,7 @@ export async function runPersonaAcrossFiles({
|
|
|
180
180
|
onEvent = () => {},
|
|
181
181
|
} = {}) {
|
|
182
182
|
if (!Array.isArray(files)) throw new TypeError("runPersonaAcrossFiles requires files array");
|
|
183
|
+
const startedAtMs = Date.now();
|
|
183
184
|
const safeBudget = budget || createBudgetState();
|
|
184
185
|
const perFile = [];
|
|
185
186
|
const allFindings = [];
|
|
@@ -228,6 +229,7 @@ export async function runPersonaAcrossFiles({
|
|
|
228
229
|
visited,
|
|
229
230
|
skipped,
|
|
230
231
|
terminationReason,
|
|
232
|
+
durationMs: Math.max(0, Date.now() - startedAtMs),
|
|
231
233
|
};
|
|
232
234
|
}
|
|
233
235
|
|
|
@@ -268,6 +270,7 @@ export async function runPersonaAcrossFiles({
|
|
|
268
270
|
visited,
|
|
269
271
|
skipped,
|
|
270
272
|
terminationReason,
|
|
273
|
+
durationMs: Math.max(0, Date.now() - startedAtMs),
|
|
271
274
|
};
|
|
272
275
|
}
|
|
273
276
|
|
|
@@ -29,13 +29,26 @@ function compactBudgetState(budgetState) {
|
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function nonNegativeNumber(value, fallback = 0) {
|
|
33
|
+
const parsed = Number(value);
|
|
34
|
+
if (Number.isFinite(parsed) && parsed >= 0) return parsed;
|
|
35
|
+
return fallback;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function roundedMoney(value) {
|
|
39
|
+
const parsed = Number(value);
|
|
40
|
+
if (!Number.isFinite(parsed)) return null;
|
|
41
|
+
return Math.round(parsed * 1_000_000) / 1_000_000;
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
function usageLedgerKey(entry) {
|
|
33
45
|
return [
|
|
34
46
|
entry?.ledgerEntry?.ledgerEntryId,
|
|
35
|
-
entry?.action,
|
|
47
|
+
entry?.action || entry?.ledgerEntry?.action,
|
|
48
|
+
entry?.agentId || entry?.ledgerEntry?.agentId,
|
|
36
49
|
entry?.ledgerEntry?.idempotencyKey,
|
|
37
|
-
entry?.inputTokens,
|
|
38
|
-
entry?.outputTokens,
|
|
50
|
+
entry?.inputTokens ?? entry?.ledgerEntry?.inputTokens,
|
|
51
|
+
entry?.outputTokens ?? entry?.ledgerEntry?.outputTokens,
|
|
39
52
|
].map((value) => String(value || "")).join(":");
|
|
40
53
|
}
|
|
41
54
|
|
|
@@ -56,6 +69,205 @@ function collectSessionUsageLedgerEntries({ budgetState = null, devTestBotPhase
|
|
|
56
69
|
return entries;
|
|
57
70
|
}
|
|
58
71
|
|
|
72
|
+
function ledgerValue(entry, key, fallback = 0) {
|
|
73
|
+
if (entry && Object.prototype.hasOwnProperty.call(entry, key)) {
|
|
74
|
+
return nonNegativeNumber(entry[key], fallback);
|
|
75
|
+
}
|
|
76
|
+
if (entry?.ledgerEntry && Object.prototype.hasOwnProperty.call(entry.ledgerEntry, key)) {
|
|
77
|
+
return nonNegativeNumber(entry.ledgerEntry[key], fallback);
|
|
78
|
+
}
|
|
79
|
+
return fallback;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function ledgerString(entry, key) {
|
|
83
|
+
return String(entry?.[key] || entry?.ledgerEntry?.[key] || "").trim();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function personaFromAgentId(agentId) {
|
|
87
|
+
const normalized = String(agentId || "").trim();
|
|
88
|
+
if (!normalized.startsWith("investor-dd-")) return "";
|
|
89
|
+
return normalized.slice("investor-dd-".length);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function sumFileMetrics(files, fileMetrics = {}) {
|
|
93
|
+
const seen = new Set();
|
|
94
|
+
let filesWithMetrics = 0;
|
|
95
|
+
let locScanned = 0;
|
|
96
|
+
let bytesScanned = 0;
|
|
97
|
+
let truncatedFiles = 0;
|
|
98
|
+
let missingFiles = 0;
|
|
99
|
+
for (const file of files || []) {
|
|
100
|
+
const rel = String(file || "").trim();
|
|
101
|
+
if (!rel || seen.has(rel)) continue;
|
|
102
|
+
seen.add(rel);
|
|
103
|
+
const metrics = fileMetrics[rel];
|
|
104
|
+
if (!metrics) continue;
|
|
105
|
+
filesWithMetrics += 1;
|
|
106
|
+
locScanned += nonNegativeNumber(metrics.loc, 0);
|
|
107
|
+
bytesScanned += nonNegativeNumber(metrics.bytes, 0);
|
|
108
|
+
if (metrics.truncated) truncatedFiles += 1;
|
|
109
|
+
if (metrics.missing) missingFiles += 1;
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
filesWithMetrics,
|
|
113
|
+
locScanned,
|
|
114
|
+
bytesScanned,
|
|
115
|
+
truncatedFiles,
|
|
116
|
+
missingFiles,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function countToolInvocations(record = {}) {
|
|
121
|
+
return (Array.isArray(record.perFile) ? record.perFile : []).reduce(
|
|
122
|
+
(count, item) => count + (Array.isArray(item.toolInvocations) ? item.toolInvocations.length : 0),
|
|
123
|
+
0,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function createUsageRecord({ personaId = "", agentId = "" } = {}) {
|
|
128
|
+
const id = String(personaId || "").trim();
|
|
129
|
+
const agent = String(agentId || (id ? `investor-dd-${id}` : "")).trim();
|
|
130
|
+
return {
|
|
131
|
+
personaId: id || null,
|
|
132
|
+
agentId: agent,
|
|
133
|
+
routedFiles: 0,
|
|
134
|
+
visitedFiles: 0,
|
|
135
|
+
skippedFiles: 0,
|
|
136
|
+
filesWithMetrics: 0,
|
|
137
|
+
locScanned: 0,
|
|
138
|
+
bytesScanned: 0,
|
|
139
|
+
truncatedFiles: 0,
|
|
140
|
+
missingFiles: 0,
|
|
141
|
+
durationMs: 0,
|
|
142
|
+
toolCalls: 0,
|
|
143
|
+
findingCount: 0,
|
|
144
|
+
inputTokens: 0,
|
|
145
|
+
outputTokens: 0,
|
|
146
|
+
totalTokens: 0,
|
|
147
|
+
providerCostUsd: 0,
|
|
148
|
+
customerCostUsd: null,
|
|
149
|
+
marginUsd: null,
|
|
150
|
+
ledgerEntries: 0,
|
|
151
|
+
actions: [],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function buildUsageTelemetry({
|
|
156
|
+
activePersonas = [],
|
|
157
|
+
routing = {},
|
|
158
|
+
byPersona = {},
|
|
159
|
+
fileMetrics = {},
|
|
160
|
+
sessionUsageLedgerEntries = [],
|
|
161
|
+
} = {}) {
|
|
162
|
+
const records = new Map();
|
|
163
|
+
const ensure = ({ personaId = "", agentId = "" } = {}) => {
|
|
164
|
+
const key = personaId || agentId;
|
|
165
|
+
if (!records.has(key)) {
|
|
166
|
+
records.set(key, createUsageRecord({ personaId, agentId }));
|
|
167
|
+
}
|
|
168
|
+
return records.get(key);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
for (const personaId of activePersonas) {
|
|
172
|
+
const record = ensure({ personaId });
|
|
173
|
+
const routed = Array.isArray(routing?.[personaId]) ? routing[personaId] : [];
|
|
174
|
+
const personaRecord = byPersona?.[personaId] || {};
|
|
175
|
+
const visited = Array.isArray(personaRecord.visited) ? personaRecord.visited : [];
|
|
176
|
+
const skipped = Array.isArray(personaRecord.skipped) ? personaRecord.skipped : [];
|
|
177
|
+
const filesForMetrics = visited.length > 0 ? visited : routed;
|
|
178
|
+
const metrics = sumFileMetrics(filesForMetrics, fileMetrics);
|
|
179
|
+
record.routedFiles = routed.length;
|
|
180
|
+
record.visitedFiles = visited.length;
|
|
181
|
+
record.skippedFiles = skipped.length;
|
|
182
|
+
record.filesWithMetrics = metrics.filesWithMetrics;
|
|
183
|
+
record.locScanned = metrics.locScanned;
|
|
184
|
+
record.bytesScanned = metrics.bytesScanned;
|
|
185
|
+
record.truncatedFiles = metrics.truncatedFiles;
|
|
186
|
+
record.missingFiles = metrics.missingFiles;
|
|
187
|
+
record.durationMs = Math.max(0, Math.floor(nonNegativeNumber(personaRecord.durationMs, 0)));
|
|
188
|
+
record.toolCalls = countToolInvocations(personaRecord);
|
|
189
|
+
record.findingCount = Array.isArray(personaRecord.findings) ? personaRecord.findings.length : 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
for (const entry of sessionUsageLedgerEntries) {
|
|
193
|
+
const agentId = ledgerString(entry, "agentId");
|
|
194
|
+
const personaId = personaFromAgentId(agentId);
|
|
195
|
+
const record = ensure({ personaId, agentId });
|
|
196
|
+
const inputTokens = ledgerValue(entry, "inputTokens", 0);
|
|
197
|
+
const outputTokens = ledgerValue(entry, "outputTokens", 0);
|
|
198
|
+
const totalTokens = ledgerValue(entry, "totalTokens", inputTokens + outputTokens);
|
|
199
|
+
const providerCost = roundedMoney(
|
|
200
|
+
entry?.ledgerEntry?.providerCostUsd ?? entry?.ledgerEntry?.costUsd ?? entry?.providerCostUsd ?? entry?.costUsd,
|
|
201
|
+
);
|
|
202
|
+
const customerCost = roundedMoney(entry?.ledgerEntry?.customerCostUsd ?? entry?.customerCostUsd);
|
|
203
|
+
record.inputTokens += inputTokens;
|
|
204
|
+
record.outputTokens += outputTokens;
|
|
205
|
+
record.totalTokens += totalTokens;
|
|
206
|
+
if (providerCost != null) {
|
|
207
|
+
record.providerCostUsd = roundedMoney(record.providerCostUsd + providerCost);
|
|
208
|
+
}
|
|
209
|
+
if (customerCost != null) {
|
|
210
|
+
record.customerCostUsd = roundedMoney((record.customerCostUsd || 0) + customerCost);
|
|
211
|
+
}
|
|
212
|
+
if (record.customerCostUsd != null && record.providerCostUsd != null) {
|
|
213
|
+
record.marginUsd = roundedMoney(record.customerCostUsd - record.providerCostUsd);
|
|
214
|
+
}
|
|
215
|
+
record.ledgerEntries += 1;
|
|
216
|
+
const action = ledgerString(entry, "action");
|
|
217
|
+
if (action && !record.actions.includes(action)) record.actions.push(action);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const perAgent = Array.from(records.values()).map((record) => ({
|
|
221
|
+
...record,
|
|
222
|
+
providerCostUsd: roundedMoney(record.providerCostUsd) || 0,
|
|
223
|
+
}));
|
|
224
|
+
const hasCustomerCost = perAgent.some((record) => record.customerCostUsd != null);
|
|
225
|
+
const totals = perAgent.reduce(
|
|
226
|
+
(acc, record) => {
|
|
227
|
+
acc.routedFiles += record.routedFiles;
|
|
228
|
+
acc.visitedFiles += record.visitedFiles;
|
|
229
|
+
acc.filesWithMetrics += record.filesWithMetrics;
|
|
230
|
+
acc.locScanned += record.locScanned;
|
|
231
|
+
acc.bytesScanned += record.bytesScanned;
|
|
232
|
+
acc.durationMs += record.durationMs;
|
|
233
|
+
acc.toolCalls += record.toolCalls;
|
|
234
|
+
acc.findingCount += record.findingCount;
|
|
235
|
+
acc.inputTokens += record.inputTokens;
|
|
236
|
+
acc.outputTokens += record.outputTokens;
|
|
237
|
+
acc.totalTokens += record.totalTokens;
|
|
238
|
+
acc.providerCostUsd += record.providerCostUsd || 0;
|
|
239
|
+
acc.customerCostUsd += record.customerCostUsd || 0;
|
|
240
|
+
acc.ledgerEntries += record.ledgerEntries;
|
|
241
|
+
return acc;
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
routedFiles: 0,
|
|
245
|
+
visitedFiles: 0,
|
|
246
|
+
filesWithMetrics: 0,
|
|
247
|
+
locScanned: 0,
|
|
248
|
+
bytesScanned: 0,
|
|
249
|
+
durationMs: 0,
|
|
250
|
+
toolCalls: 0,
|
|
251
|
+
findingCount: 0,
|
|
252
|
+
inputTokens: 0,
|
|
253
|
+
outputTokens: 0,
|
|
254
|
+
totalTokens: 0,
|
|
255
|
+
providerCostUsd: 0,
|
|
256
|
+
customerCostUsd: 0,
|
|
257
|
+
ledgerEntries: 0,
|
|
258
|
+
},
|
|
259
|
+
);
|
|
260
|
+
totals.providerCostUsd = roundedMoney(totals.providerCostUsd) || 0;
|
|
261
|
+
totals.customerCostUsd = hasCustomerCost ? roundedMoney(totals.customerCostUsd) : null;
|
|
262
|
+
totals.marginUsd = totals.customerCostUsd != null ? roundedMoney(totals.customerCostUsd - totals.providerCostUsd) : null;
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
schema: "investor_dd_usage_telemetry_v1",
|
|
266
|
+
totals,
|
|
267
|
+
perAgent,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
59
271
|
function byId(capabilities, id) {
|
|
60
272
|
return capabilities.find((capability) => capability.id === id);
|
|
61
273
|
}
|
|
@@ -110,6 +322,7 @@ export function buildInvestorDdProgress({
|
|
|
110
322
|
artifactFiles = [],
|
|
111
323
|
budgetState = null,
|
|
112
324
|
usageLedgerEntries = [],
|
|
325
|
+
fileMetrics = {},
|
|
113
326
|
} = {}) {
|
|
114
327
|
const activePersonas = uniqueStrings(personas);
|
|
115
328
|
const missingPersonas = INVESTOR_DD_EXPECTED_PERSONAS.filter(
|
|
@@ -169,13 +382,28 @@ export function buildInvestorDdProgress({
|
|
|
169
382
|
devTestBotPhase,
|
|
170
383
|
usageLedgerEntries,
|
|
171
384
|
});
|
|
385
|
+
const usageTelemetry = buildUsageTelemetry({
|
|
386
|
+
activePersonas,
|
|
387
|
+
routing,
|
|
388
|
+
byPersona,
|
|
389
|
+
fileMetrics,
|
|
390
|
+
sessionUsageLedgerEntries,
|
|
391
|
+
});
|
|
172
392
|
const hasSessionUsageLedger = sessionUsageLedgerEntries.length > 0;
|
|
393
|
+
const hasPerAgentTelemetry = usageTelemetry.perAgent.length > 0;
|
|
394
|
+
const hasLocTelemetry = usageTelemetry.totals.locScanned > 0 || usageTelemetry.totals.filesWithMetrics > 0;
|
|
395
|
+
const hasRuntimeTelemetry = usageTelemetry.totals.durationMs > 0;
|
|
396
|
+
const hasCustomerPricing = usageTelemetry.totals.customerCostUsd != null;
|
|
173
397
|
addCapability(capabilities, {
|
|
174
398
|
id: "usage_margin_telemetry",
|
|
175
399
|
label: "Billing-grade per-agent usage, token, time, LOC, and margin telemetry",
|
|
176
|
-
status: compactBudget || hasSessionUsageLedger ? "partial" : dryRun ? "deferred" : "not_configured",
|
|
177
|
-
evidence: compactBudget || hasSessionUsageLedger
|
|
400
|
+
status: compactBudget || hasSessionUsageLedger || hasPerAgentTelemetry ? "partial" : dryRun ? "deferred" : "not_configured",
|
|
401
|
+
evidence: compactBudget || hasSessionUsageLedger || hasPerAgentTelemetry
|
|
178
402
|
? [
|
|
403
|
+
`agentUsageTelemetry=${hasPerAgentTelemetry}`,
|
|
404
|
+
`perAgentUsageRecords=${usageTelemetry.perAgent.length}`,
|
|
405
|
+
`locScanned=${usageTelemetry.totals.locScanned}`,
|
|
406
|
+
`durationMs=${usageTelemetry.totals.durationMs}`,
|
|
179
407
|
...(compactBudget
|
|
180
408
|
? [
|
|
181
409
|
`localBudgetSpentUsd=${compactBudget.spentUsd}`,
|
|
@@ -192,11 +420,12 @@ export function buildInvestorDdProgress({
|
|
|
192
420
|
? ["dryRun=true"]
|
|
193
421
|
: [],
|
|
194
422
|
gaps: [
|
|
195
|
-
hasSessionUsageLedger
|
|
196
|
-
? "only optional DD planner calls are wired to billing-grade session_usage"
|
|
197
|
-
: "
|
|
198
|
-
|
|
199
|
-
|
|
423
|
+
...(hasSessionUsageLedger
|
|
424
|
+
? ["only optional DD planner calls are wired to billing-grade session_usage"]
|
|
425
|
+
: ["billing-grade token/customer-cost telemetry is only available when session_usage ledger entries exist"]),
|
|
426
|
+
...(hasRuntimeTelemetry || dryRun ? [] : ["per-agent runtime is unavailable because no persona execution records were produced"]),
|
|
427
|
+
...(hasLocTelemetry ? [] : ["per-agent LOC is unavailable because file metrics were not collected"]),
|
|
428
|
+
...(hasCustomerPricing ? [] : ["customerCostUsd and marginUsd are unavailable until customer pricing is supplied by the session_usage ledger"]),
|
|
200
429
|
],
|
|
201
430
|
});
|
|
202
431
|
|
|
@@ -277,8 +506,8 @@ export function buildInvestorDdProgress({
|
|
|
277
506
|
});
|
|
278
507
|
|
|
279
508
|
const artifactRequired = dryRun
|
|
280
|
-
? ["plan.json", "stream.ndjson", "summary.json", "report.md", "report.html"]
|
|
281
|
-
: ["plan.json", "stream.ndjson", "summary.json", "report.md", "report.html", "findings.json"];
|
|
509
|
+
? ["file-metrics.json", "plan.json", "stream.ndjson", "summary.json", "report.md", "report.html"]
|
|
510
|
+
: ["file-metrics.json", "plan.json", "stream.ndjson", "summary.json", "report.md", "report.html", "findings.json"];
|
|
282
511
|
const missingArtifacts = artifactRequired.filter((file) => !artifactFiles.includes(file));
|
|
283
512
|
addCapability(capabilities, {
|
|
284
513
|
id: "artifact_bundle",
|
|
@@ -315,6 +544,7 @@ export function buildInvestorDdProgress({
|
|
|
315
544
|
plannedPersonaCount: INVESTOR_DD_EXPECTED_PERSONAS.length,
|
|
316
545
|
missingPersonas,
|
|
317
546
|
capabilities,
|
|
547
|
+
usageTelemetry,
|
|
318
548
|
routingSummary: {
|
|
319
549
|
routedPersonas: Object.keys(routing || {}).length,
|
|
320
550
|
routedFiles: Object.values(routing || {}).reduce(
|
|
@@ -330,7 +560,7 @@ export function buildInvestorDdProgress({
|
|
|
330
560
|
? ["add the missing frontend/Jules persona to the default Investor-DD roster"]
|
|
331
561
|
: ["run the full 13-persona roster before claiming sellable DD coverage"]),
|
|
332
562
|
"wire Senti session id and live usage counters into Investor-DD runs",
|
|
333
|
-
"
|
|
563
|
+
"wire billing-grade token/customer-price/margin telemetry for every Investor-DD persona",
|
|
334
564
|
"require live reconciliation and report-email proof for sellable DD closeout",
|
|
335
565
|
],
|
|
336
566
|
};
|