@akagilnc/pi-workflow-roles 0.1.2276 → 0.1.2296
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/CLAUDE.md +1 -1
- package/dist/public-cli/main.js +354 -34
- package/package.json +1 -1
- package/src/analyst-cohort.ts +93 -1
- package/src/analyst-gate-cycles-read.ts +309 -0
- package/src/analyst-ledger.ts +35 -0
- package/src/analyst-metric-families/gate-cycles.ts +139 -0
- package/src/analyst-metric-families.ts +2 -0
- package/src/analyst-page.ts +7 -1
package/CLAUDE.md
CHANGED
|
@@ -82,7 +82,7 @@ A probe is temporary evidence. After its evidence purpose is disposed, either de
|
|
|
82
82
|
|
|
83
83
|
**机器只咬契约,不咬呈现**:对自由文本的正则/措辞/表头机械依赖、对图像的像素机械依赖,视同缺陷;机器要消费的信息必须以键、typed 字段或 schema 提供。呈现为人服务,随时可重排。
|
|
84
84
|
|
|
85
|
-
**生产与统计两个 regime(陛下 2026-08-25 释宪)**:上条禁令的对象是生产代码对不可穷举输入的机械依赖。统计/分析对**已冻结卷宗**不适用该禁令——太史报告层可由 LLM 做语义分类与统计,逐条附卷宗引证即为可核;形式语言命令(shell 等)非散文,不因 LLM 书写而成「自由文本」。太史代码机制维持确定性(ADR 0047/0068 不动);闸判据仍只认 typed
|
|
85
|
+
**生产与统计两个 regime(陛下 2026-08-25 释宪)**:上条禁令的对象是生产代码对不可穷举输入的机械依赖。统计/分析对**已冻结卷宗**不适用该禁令——太史报告层可由 LLM 做语义分类与统计,逐条附卷宗引证即为可核;形式语言命令(shell 等)非散文,不因 LLM 书写而成「自由文本」。太史代码机制维持确定性(ADR 0047/0068 不动);闸判据仍只认 typed 键。报告引证同符宝郎证据条款:**指针(runId/toolCallId/路径)即合格引用,开卷核对相符即成立,不誊原文进报告**——卷宗是唯一真源,复印副本违 DRY;举证到问题所需粒度为止,汇总申报方法与判据即可,不逐条立账。
|
|
86
86
|
|
|
87
87
|
**票面先过大理寺再开工;决策问题上呈陛下。**
|
|
88
88
|
|
package/dist/public-cli/main.js
CHANGED
|
@@ -25815,9 +25815,31 @@ function finishRole(role, accum) {
|
|
|
25815
25815
|
successRate: rateMetric(accum.successCount, accum.successEligibleCount)
|
|
25816
25816
|
};
|
|
25817
25817
|
}
|
|
25818
|
+
function emptyGateOfficerNumeratorAccum() {
|
|
25819
|
+
return { rounds: 0, bounceCount: 0, passCount: 0, wallSum: 0 };
|
|
25820
|
+
}
|
|
25821
|
+
function absorbGateOfficerSummary(accum, summary) {
|
|
25822
|
+
accum.rounds += summary.rounds;
|
|
25823
|
+
accum.bounceCount += summary.bounceCount;
|
|
25824
|
+
accum.passCount += summary.passCount;
|
|
25825
|
+
if (summary.meanOfficerWallMs !== void 0) {
|
|
25826
|
+
accum.wallSum += summary.meanOfficerWallMs * summary.rounds;
|
|
25827
|
+
}
|
|
25828
|
+
}
|
|
25829
|
+
function finishGateOfficerNumerators(officer, accum) {
|
|
25830
|
+
return {
|
|
25831
|
+
officer,
|
|
25832
|
+
rounds: accum.rounds,
|
|
25833
|
+
bounceCount: accum.bounceCount,
|
|
25834
|
+
passCount: accum.passCount,
|
|
25835
|
+
bounceRate: rateMetric(accum.bounceCount, accum.rounds),
|
|
25836
|
+
meanOfficerWallMs: accum.rounds === 0 ? ABSENT : presentMetric(accum.wallSum / accum.rounds)
|
|
25837
|
+
};
|
|
25838
|
+
}
|
|
25818
25839
|
async function aggregateGroup(index, input, ensureIssuePage) {
|
|
25819
25840
|
const issueEntries = [];
|
|
25820
25841
|
const roleAccums = /* @__PURE__ */ new Map();
|
|
25842
|
+
const gateOfficerAccums = /* @__PURE__ */ new Map();
|
|
25821
25843
|
let reworkWallMs = 0;
|
|
25822
25844
|
let totalWallMs = 0;
|
|
25823
25845
|
let hasReworkSample = false;
|
|
@@ -25857,14 +25879,26 @@ async function aggregateGroup(index, input, ensureIssuePage) {
|
|
|
25857
25879
|
legWalls.push(leg.wallMs);
|
|
25858
25880
|
}
|
|
25859
25881
|
}
|
|
25882
|
+
const gateCycles = page.gateCycles;
|
|
25883
|
+
if (gateCycles !== void 0) {
|
|
25884
|
+
for (const summary of gateCycles.byOfficer) {
|
|
25885
|
+
const accum = gateOfficerAccums.get(summary.officer) ?? emptyGateOfficerNumeratorAccum();
|
|
25886
|
+
absorbGateOfficerSummary(accum, summary);
|
|
25887
|
+
gateOfficerAccums.set(summary.officer, accum);
|
|
25888
|
+
}
|
|
25889
|
+
}
|
|
25860
25890
|
}
|
|
25861
25891
|
const byRole = [...roleAccums.keys()].sort((a, b) => a.localeCompare(b)).map((role) => finishRole(role, roleAccums.get(role)));
|
|
25892
|
+
const gateCyclesByOfficer = ["inspector", "notary"].filter((officer) => gateOfficerAccums.has(officer)).map(
|
|
25893
|
+
(officer) => finishGateOfficerNumerators(officer, gateOfficerAccums.get(officer))
|
|
25894
|
+
);
|
|
25862
25895
|
return {
|
|
25863
25896
|
groupLabel: input.groupLabel,
|
|
25864
25897
|
issues: issueEntries,
|
|
25865
25898
|
byRole,
|
|
25866
25899
|
reworkRatio: hasReworkSample ? rateMetric(reworkWallMs, totalWallMs) : ABSENT,
|
|
25867
|
-
medianWallMs: optionalMedian(legWalls)
|
|
25900
|
+
medianWallMs: optionalMedian(legWalls),
|
|
25901
|
+
gateCyclesByOfficer
|
|
25868
25902
|
};
|
|
25869
25903
|
}
|
|
25870
25904
|
async function runAnalystCohortMode(ledgerHome, input, ensureIssuePage) {
|
|
@@ -26184,24 +26218,211 @@ var init_run_terminal_artifacts = __esm({
|
|
|
26184
26218
|
}
|
|
26185
26219
|
});
|
|
26186
26220
|
|
|
26187
|
-
// src/analyst-
|
|
26188
|
-
import { readdir as readdir5
|
|
26221
|
+
// src/analyst-gate-cycles-read.ts
|
|
26222
|
+
import { readdir as readdir5 } from "node:fs/promises";
|
|
26189
26223
|
import { join as join23 } from "node:path";
|
|
26224
|
+
function isRecord8(value) {
|
|
26225
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
26226
|
+
}
|
|
26227
|
+
function isMissingDirectoryError(error) {
|
|
26228
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
26229
|
+
}
|
|
26230
|
+
function normalizeOfficerArg(raw) {
|
|
26231
|
+
if (typeof raw !== "string") return void 0;
|
|
26232
|
+
return OFFICER_ARG_ALIASES[raw.trim()];
|
|
26233
|
+
}
|
|
26234
|
+
function isGateTerminatingToolName(toolName) {
|
|
26235
|
+
return DISPATCH_TOOLS.has(toolName) || OFFICER_TOOL_TO_FACE[toolName] !== void 0;
|
|
26236
|
+
}
|
|
26237
|
+
function acceptedGateReceiptIds(rows) {
|
|
26238
|
+
const accepted = /* @__PURE__ */ new Set();
|
|
26239
|
+
for (const row of rows) {
|
|
26240
|
+
const message = isRecord8(row.message) ? row.message : void 0;
|
|
26241
|
+
if (message?.role !== "toolResult") continue;
|
|
26242
|
+
if (typeof message.toolCallId !== "string" || message.toolCallId.length === 0) continue;
|
|
26243
|
+
if (message.isError === false) accepted.add(message.toolCallId);
|
|
26244
|
+
}
|
|
26245
|
+
return accepted;
|
|
26246
|
+
}
|
|
26247
|
+
function extractLastAcceptedGateToolCall(rows) {
|
|
26248
|
+
const acceptedIds = acceptedGateReceiptIds(rows);
|
|
26249
|
+
let last;
|
|
26250
|
+
for (const row of rows) {
|
|
26251
|
+
const message = isRecord8(row.message) ? row.message : void 0;
|
|
26252
|
+
if (message?.role !== "assistant" || !Array.isArray(message.content)) continue;
|
|
26253
|
+
for (const part of message.content) {
|
|
26254
|
+
if (!isRecord8(part) || part.type !== "toolCall") continue;
|
|
26255
|
+
if (typeof part.id !== "string" || part.id.length === 0) continue;
|
|
26256
|
+
if (!acceptedIds.has(part.id)) continue;
|
|
26257
|
+
if (typeof part.name !== "string" || part.name.length === 0) continue;
|
|
26258
|
+
if (!isGateTerminatingToolName(part.name)) continue;
|
|
26259
|
+
last = {
|
|
26260
|
+
toolName: part.name,
|
|
26261
|
+
args: isRecord8(part.arguments) ? part.arguments : void 0
|
|
26262
|
+
};
|
|
26263
|
+
}
|
|
26264
|
+
}
|
|
26265
|
+
return last;
|
|
26266
|
+
}
|
|
26267
|
+
function requireAcceptedGateStatus(args, filePath) {
|
|
26268
|
+
if (args === void 0 || typeof args.status !== "string" || args.status.trim() === "") {
|
|
26269
|
+
throw new Error(
|
|
26270
|
+
`accepted gate receipt missing usable status in ${filePath}`
|
|
26271
|
+
);
|
|
26272
|
+
}
|
|
26273
|
+
return args.status.trim();
|
|
26274
|
+
}
|
|
26275
|
+
function requireAcceptedGateSpan(rows, filePath) {
|
|
26276
|
+
const span = extractSessionTimestampSpan(rows);
|
|
26277
|
+
if (span.startedAt === void 0 || span.endedAt === void 0) {
|
|
26278
|
+
throw new Error(
|
|
26279
|
+
`accepted gate volume missing session timestamp span in ${filePath}`
|
|
26280
|
+
);
|
|
26281
|
+
}
|
|
26282
|
+
const startedMs = Date.parse(span.startedAt);
|
|
26283
|
+
const endedMs = Date.parse(span.endedAt);
|
|
26284
|
+
if (!Number.isFinite(startedMs) || !Number.isFinite(endedMs) || endedMs < startedMs) {
|
|
26285
|
+
throw new Error(
|
|
26286
|
+
`accepted gate volume has unusable timestamp span in ${filePath}`
|
|
26287
|
+
);
|
|
26288
|
+
}
|
|
26289
|
+
return {
|
|
26290
|
+
startedAt: span.startedAt,
|
|
26291
|
+
endedAt: span.endedAt,
|
|
26292
|
+
wallMs: endedMs - startedMs
|
|
26293
|
+
};
|
|
26294
|
+
}
|
|
26295
|
+
async function classifyAuditorVolume(filePath) {
|
|
26296
|
+
const rows = await readLedgerSessionJsonl(filePath);
|
|
26297
|
+
const call = extractLastAcceptedGateToolCall(rows);
|
|
26298
|
+
if (call === void 0) return void 0;
|
|
26299
|
+
const span = requireAcceptedGateSpan(rows, filePath);
|
|
26300
|
+
const status = requireAcceptedGateStatus(call.args, filePath);
|
|
26301
|
+
const findings = call.args?.findings;
|
|
26302
|
+
const findingsCount = Array.isArray(findings) ? findings.length : 0;
|
|
26303
|
+
if (DISPATCH_TOOLS.has(call.toolName)) {
|
|
26304
|
+
if (status !== "dispatch") {
|
|
26305
|
+
throw new Error(
|
|
26306
|
+
`accepted dispatch receipt has non-dispatch status ${JSON.stringify(status)} in ${filePath}`
|
|
26307
|
+
);
|
|
26308
|
+
}
|
|
26309
|
+
const officer2 = normalizeOfficerArg(call.args?.officer);
|
|
26310
|
+
if (officer2 === void 0) {
|
|
26311
|
+
throw new Error(
|
|
26312
|
+
`accepted dispatch receipt missing or unknown officer in ${filePath}`
|
|
26313
|
+
);
|
|
26314
|
+
}
|
|
26315
|
+
return {
|
|
26316
|
+
kind: "dispatch",
|
|
26317
|
+
startedAt: span.startedAt,
|
|
26318
|
+
officer: officer2
|
|
26319
|
+
};
|
|
26320
|
+
}
|
|
26321
|
+
const officer = OFFICER_TOOL_TO_FACE[call.toolName];
|
|
26322
|
+
if (officer === void 0) {
|
|
26323
|
+
throw new Error(
|
|
26324
|
+
`accepted gate receipt has unknown officer tool ${call.toolName} in ${filePath}`
|
|
26325
|
+
);
|
|
26326
|
+
}
|
|
26327
|
+
return {
|
|
26328
|
+
kind: "officer",
|
|
26329
|
+
startedAt: span.startedAt,
|
|
26330
|
+
endedAt: span.endedAt,
|
|
26331
|
+
officer,
|
|
26332
|
+
status,
|
|
26333
|
+
findingsCount,
|
|
26334
|
+
officerWallMs: span.wallMs
|
|
26335
|
+
};
|
|
26336
|
+
}
|
|
26337
|
+
function pairGateRounds(volumes) {
|
|
26338
|
+
const ordered = [...volumes].sort((a, b) => {
|
|
26339
|
+
if (a.startedAt !== b.startedAt) return a.startedAt.localeCompare(b.startedAt);
|
|
26340
|
+
if (a.kind !== b.kind) return a.kind === "dispatch" ? -1 : 1;
|
|
26341
|
+
return 0;
|
|
26342
|
+
});
|
|
26343
|
+
const usedOfficerIdx = /* @__PURE__ */ new Set();
|
|
26344
|
+
const rounds = [];
|
|
26345
|
+
for (let i = 0; i < ordered.length; i += 1) {
|
|
26346
|
+
const vol = ordered[i];
|
|
26347
|
+
if (vol.kind !== "dispatch") continue;
|
|
26348
|
+
let match;
|
|
26349
|
+
for (let j = i + 1; j < ordered.length; j += 1) {
|
|
26350
|
+
if (usedOfficerIdx.has(j)) continue;
|
|
26351
|
+
const candidate = ordered[j];
|
|
26352
|
+
if (candidate.kind !== "officer") continue;
|
|
26353
|
+
if (candidate.officer !== vol.officer) continue;
|
|
26354
|
+
match = { index: j, officer: candidate };
|
|
26355
|
+
break;
|
|
26356
|
+
}
|
|
26357
|
+
if (match === void 0) continue;
|
|
26358
|
+
usedOfficerIdx.add(match.index);
|
|
26359
|
+
rounds.push({
|
|
26360
|
+
roundIndex: rounds.length + 1,
|
|
26361
|
+
officer: match.officer.officer,
|
|
26362
|
+
status: match.officer.status,
|
|
26363
|
+
officerWallMs: match.officer.officerWallMs,
|
|
26364
|
+
officerStartedAt: match.officer.startedAt,
|
|
26365
|
+
officerEndedAt: match.officer.endedAt,
|
|
26366
|
+
findingsCount: match.officer.findingsCount
|
|
26367
|
+
});
|
|
26368
|
+
}
|
|
26369
|
+
return rounds;
|
|
26370
|
+
}
|
|
26371
|
+
async function readAnalystGateCyclesFromAuditorRoles(auditorRolesDirectory) {
|
|
26372
|
+
let names;
|
|
26373
|
+
try {
|
|
26374
|
+
const entries = await readdir5(auditorRolesDirectory, { withFileTypes: true });
|
|
26375
|
+
names = entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) => e.name).sort();
|
|
26376
|
+
} catch (error) {
|
|
26377
|
+
if (isMissingDirectoryError(error)) return [];
|
|
26378
|
+
throw error;
|
|
26379
|
+
}
|
|
26380
|
+
const volumes = [];
|
|
26381
|
+
for (const name of names) {
|
|
26382
|
+
const classified = await classifyAuditorVolume(join23(auditorRolesDirectory, name));
|
|
26383
|
+
if (classified !== void 0) volumes.push(classified);
|
|
26384
|
+
}
|
|
26385
|
+
return pairGateRounds(volumes);
|
|
26386
|
+
}
|
|
26387
|
+
var DISPATCH_TOOLS, OFFICER_TOOL_TO_FACE, OFFICER_ARG_ALIASES;
|
|
26388
|
+
var init_analyst_gate_cycles_read = __esm({
|
|
26389
|
+
"src/analyst-gate-cycles-read.ts"() {
|
|
26390
|
+
"use strict";
|
|
26391
|
+
init_ledger_session_read();
|
|
26392
|
+
DISPATCH_TOOLS = /* @__PURE__ */ new Set(["ak_menxia_output", "ak_gatekeeper_output"]);
|
|
26393
|
+
OFFICER_TOOL_TO_FACE = {
|
|
26394
|
+
ak_jishizhong_output: "inspector",
|
|
26395
|
+
ak_inspector_output: "inspector",
|
|
26396
|
+
ak_fubaolang_output: "notary",
|
|
26397
|
+
ak_notary_output: "notary"
|
|
26398
|
+
};
|
|
26399
|
+
OFFICER_ARG_ALIASES = {
|
|
26400
|
+
jishizhong: "inspector",
|
|
26401
|
+
inspector: "inspector",
|
|
26402
|
+
fubaolang: "notary",
|
|
26403
|
+
notary: "notary"
|
|
26404
|
+
};
|
|
26405
|
+
}
|
|
26406
|
+
});
|
|
26407
|
+
|
|
26408
|
+
// src/analyst-ledger.ts
|
|
26409
|
+
import { readdir as readdir6, readFile as readFile14 } from "node:fs/promises";
|
|
26410
|
+
import { join as join24 } from "node:path";
|
|
26190
26411
|
function isMissingPathError5(error) {
|
|
26191
26412
|
return error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
26192
26413
|
}
|
|
26193
26414
|
function errorText3(error) {
|
|
26194
26415
|
return error instanceof Error ? error.message : String(error);
|
|
26195
26416
|
}
|
|
26196
|
-
function
|
|
26417
|
+
function isRecord9(value) {
|
|
26197
26418
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
26198
26419
|
}
|
|
26199
26420
|
async function readExistingRunLifecycleState(runDirectory) {
|
|
26200
26421
|
try {
|
|
26201
26422
|
const raw = JSON.parse(
|
|
26202
|
-
await readFile14(
|
|
26423
|
+
await readFile14(join24(runDirectory, "run-state.json"), "utf8")
|
|
26203
26424
|
);
|
|
26204
|
-
if (!
|
|
26425
|
+
if (!isRecord9(raw) || typeof raw.state !== "string") return void 0;
|
|
26205
26426
|
return raw.state;
|
|
26206
26427
|
} catch {
|
|
26207
26428
|
return void 0;
|
|
@@ -26221,7 +26442,7 @@ function tryResolveBookKeyFromProjectRoot(projectRoot) {
|
|
|
26221
26442
|
}
|
|
26222
26443
|
async function listLedgerBookNames(booksRoot) {
|
|
26223
26444
|
try {
|
|
26224
|
-
const entries = await
|
|
26445
|
+
const entries = await readdir6(booksRoot, { withFileTypes: true });
|
|
26225
26446
|
return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
26226
26447
|
} catch (error) {
|
|
26227
26448
|
if (isMissingPathError5(error)) return [];
|
|
@@ -26231,13 +26452,13 @@ async function listLedgerBookNames(booksRoot) {
|
|
|
26231
26452
|
async function readInvocationScopeFields(runDirectory) {
|
|
26232
26453
|
let raw;
|
|
26233
26454
|
try {
|
|
26234
|
-
raw = await readFile14(
|
|
26455
|
+
raw = await readFile14(join24(runDirectory, "invocation.json"), "utf8");
|
|
26235
26456
|
} catch (error) {
|
|
26236
26457
|
if (isMissingPathError5(error)) return void 0;
|
|
26237
26458
|
throw error;
|
|
26238
26459
|
}
|
|
26239
26460
|
const parsed = JSON.parse(raw);
|
|
26240
|
-
if (!
|
|
26461
|
+
if (!isRecord9(parsed)) return void 0;
|
|
26241
26462
|
if (typeof parsed.projectRoot !== "string" || parsed.projectRoot.trim() === "") {
|
|
26242
26463
|
return void 0;
|
|
26243
26464
|
}
|
|
@@ -26263,15 +26484,15 @@ function decideIssueScope(input) {
|
|
|
26263
26484
|
}
|
|
26264
26485
|
async function resolveSessionFile(runDirectory) {
|
|
26265
26486
|
try {
|
|
26266
|
-
const raw = await readFile14(
|
|
26487
|
+
const raw = await readFile14(join24(runDirectory, "invocation.json"), "utf8");
|
|
26267
26488
|
const parsed = JSON.parse(raw);
|
|
26268
|
-
if (
|
|
26489
|
+
if (isRecord9(parsed) && typeof parsed.sessionFile === "string" && parsed.sessionFile.trim() !== "") {
|
|
26269
26490
|
return parsed.sessionFile;
|
|
26270
26491
|
}
|
|
26271
26492
|
} catch (error) {
|
|
26272
26493
|
if (!isMissingPathError5(error)) throw error;
|
|
26273
26494
|
}
|
|
26274
|
-
return
|
|
26495
|
+
return join24(runDirectory, "session", "session.jsonl");
|
|
26275
26496
|
}
|
|
26276
26497
|
async function classifyScopedRun(input) {
|
|
26277
26498
|
const missingSources = [];
|
|
@@ -26379,6 +26600,24 @@ async function classifyScopedRun(input) {
|
|
|
26379
26600
|
`classifyScopedRun internal invariant: missing retained facts for ${input.runId}`
|
|
26380
26601
|
);
|
|
26381
26602
|
}
|
|
26603
|
+
let gateCycles;
|
|
26604
|
+
try {
|
|
26605
|
+
gateCycles = await readAnalystGateCyclesFromAuditorRoles(
|
|
26606
|
+
join24(input.runDirectory, "session", "auditor-roles")
|
|
26607
|
+
);
|
|
26608
|
+
} catch (error) {
|
|
26609
|
+
return {
|
|
26610
|
+
kind: "unreadable",
|
|
26611
|
+
entry: {
|
|
26612
|
+
runId: input.runId,
|
|
26613
|
+
book: input.book,
|
|
26614
|
+
missingSources: ["auditor-roles"],
|
|
26615
|
+
reason: errorText3(error),
|
|
26616
|
+
firstFrameAt: { status: "present", at: frameSpan.startedAt },
|
|
26617
|
+
lastFrameAt: { status: "present", at: frameSpan.endedAt }
|
|
26618
|
+
}
|
|
26619
|
+
};
|
|
26620
|
+
}
|
|
26382
26621
|
return {
|
|
26383
26622
|
kind: "readable",
|
|
26384
26623
|
facts: {
|
|
@@ -26388,14 +26627,15 @@ async function classifyScopedRun(input) {
|
|
|
26388
26627
|
frameSpan,
|
|
26389
26628
|
toolIntervals,
|
|
26390
26629
|
terminal,
|
|
26391
|
-
models
|
|
26630
|
+
models,
|
|
26631
|
+
gateCycles
|
|
26392
26632
|
}
|
|
26393
26633
|
};
|
|
26394
26634
|
}
|
|
26395
26635
|
async function scanAnalystIssueRuns(input) {
|
|
26396
26636
|
const ledgerHome = resolveActivationLedgerHome();
|
|
26397
26637
|
const scopeTicketNumber = input.ticketNumber;
|
|
26398
|
-
const booksRoot =
|
|
26638
|
+
const booksRoot = join24(ledgerHome, "books");
|
|
26399
26639
|
let wholeBook = false;
|
|
26400
26640
|
let scopeRootIdentity;
|
|
26401
26641
|
let bookNames;
|
|
@@ -26427,10 +26667,10 @@ async function scanAnalystIssueRuns(input) {
|
|
|
26427
26667
|
const unreadable = [];
|
|
26428
26668
|
const scopeConflicts = [];
|
|
26429
26669
|
for (const book of bookNames) {
|
|
26430
|
-
const runsDir =
|
|
26670
|
+
const runsDir = join24(booksRoot, book, "runs");
|
|
26431
26671
|
let runNames;
|
|
26432
26672
|
try {
|
|
26433
|
-
const entries = await
|
|
26673
|
+
const entries = await readdir6(runsDir, { withFileTypes: true });
|
|
26434
26674
|
runNames = entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
26435
26675
|
} catch (error) {
|
|
26436
26676
|
if (isMissingPathError5(error)) continue;
|
|
@@ -26439,7 +26679,7 @@ async function scanAnalystIssueRuns(input) {
|
|
|
26439
26679
|
for (const runName of runNames) {
|
|
26440
26680
|
const parsed = parseRunDirectoryName(runName);
|
|
26441
26681
|
if (parsed === void 0) continue;
|
|
26442
|
-
const runDirectory =
|
|
26682
|
+
const runDirectory = join24(runsDir, runName);
|
|
26443
26683
|
let scopeFields;
|
|
26444
26684
|
try {
|
|
26445
26685
|
scopeFields = await readInvocationScopeFields(runDirectory);
|
|
@@ -26481,12 +26721,13 @@ var init_analyst_ledger = __esm({
|
|
|
26481
26721
|
init_activation_ledger_topology();
|
|
26482
26722
|
init_ledger_session_read();
|
|
26483
26723
|
init_run_terminal_artifacts();
|
|
26724
|
+
init_analyst_gate_cycles_read();
|
|
26484
26725
|
LIVE_RUN_STATES = /* @__PURE__ */ new Set(["admitted", "running", "resumable"]);
|
|
26485
26726
|
}
|
|
26486
26727
|
});
|
|
26487
26728
|
|
|
26488
26729
|
// src/analyst-metric-families/acceptance-success-rework.ts
|
|
26489
|
-
function
|
|
26730
|
+
function isRecord10(value) {
|
|
26490
26731
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
26491
26732
|
}
|
|
26492
26733
|
function wallMsFromSpan(span) {
|
|
@@ -26495,21 +26736,21 @@ function wallMsFromSpan(span) {
|
|
|
26495
26736
|
function findCollectorGroups(body) {
|
|
26496
26737
|
if (Array.isArray(body.groups)) return body.groups;
|
|
26497
26738
|
const receipt = body.receipt;
|
|
26498
|
-
if (
|
|
26739
|
+
if (isRecord10(receipt) && Array.isArray(receipt.groups)) return receipt.groups;
|
|
26499
26740
|
const outcome = body.outcome;
|
|
26500
|
-
if (
|
|
26741
|
+
if (isRecord10(outcome)) {
|
|
26501
26742
|
const facts = outcome.decisiveFacts;
|
|
26502
|
-
if (
|
|
26743
|
+
if (isRecord10(facts) && Array.isArray(facts.groups)) return facts.groups;
|
|
26503
26744
|
}
|
|
26504
26745
|
return void 0;
|
|
26505
26746
|
}
|
|
26506
26747
|
function extractStatus(body) {
|
|
26507
26748
|
const outcome = body.outcome;
|
|
26508
|
-
if (
|
|
26749
|
+
if (isRecord10(outcome) && typeof outcome.status === "string" && outcome.status.trim() !== "") {
|
|
26509
26750
|
return outcome.status;
|
|
26510
26751
|
}
|
|
26511
26752
|
const receipt = body.receipt;
|
|
26512
|
-
if (
|
|
26753
|
+
if (isRecord10(receipt) && typeof receipt.status === "string" && receipt.status.trim() !== "") {
|
|
26513
26754
|
return receipt.status;
|
|
26514
26755
|
}
|
|
26515
26756
|
if (typeof body.status === "string" && body.status.trim() !== "") {
|
|
@@ -27007,6 +27248,83 @@ var init_b2_frame_buckets_actions = __esm({
|
|
|
27007
27248
|
}
|
|
27008
27249
|
});
|
|
27009
27250
|
|
|
27251
|
+
// src/analyst-metric-families/gate-cycles.ts
|
|
27252
|
+
function projectRound(round) {
|
|
27253
|
+
return {
|
|
27254
|
+
roundIndex: round.roundIndex,
|
|
27255
|
+
officer: round.officer,
|
|
27256
|
+
status: round.status,
|
|
27257
|
+
officerWallMs: round.officerWallMs,
|
|
27258
|
+
findingsCount: round.findingsCount
|
|
27259
|
+
};
|
|
27260
|
+
}
|
|
27261
|
+
function projectLeg(facts) {
|
|
27262
|
+
const rounds = facts.gateCycles.map(projectRound);
|
|
27263
|
+
return {
|
|
27264
|
+
runId: facts.runId,
|
|
27265
|
+
book: facts.book,
|
|
27266
|
+
role: facts.role,
|
|
27267
|
+
roundCount: rounds.length,
|
|
27268
|
+
rounds
|
|
27269
|
+
};
|
|
27270
|
+
}
|
|
27271
|
+
function compareLegs(a, b) {
|
|
27272
|
+
if (a.book !== b.book) return a.book.localeCompare(b.book);
|
|
27273
|
+
if (a.role !== b.role) return a.role.localeCompare(b.role);
|
|
27274
|
+
return a.runId.localeCompare(b.runId);
|
|
27275
|
+
}
|
|
27276
|
+
function emptyAccum() {
|
|
27277
|
+
return { rounds: 0, bounceCount: 0, passCount: 0, wallSum: 0 };
|
|
27278
|
+
}
|
|
27279
|
+
function absorbRound(accum, round) {
|
|
27280
|
+
accum.rounds += 1;
|
|
27281
|
+
accum.wallSum += round.officerWallMs;
|
|
27282
|
+
if (round.status === "bounce") accum.bounceCount += 1;
|
|
27283
|
+
if (round.status === "pass") accum.passCount += 1;
|
|
27284
|
+
}
|
|
27285
|
+
function finishOfficer(officer, accum) {
|
|
27286
|
+
return {
|
|
27287
|
+
officer,
|
|
27288
|
+
rounds: accum.rounds,
|
|
27289
|
+
bounceCount: accum.bounceCount,
|
|
27290
|
+
passCount: accum.passCount,
|
|
27291
|
+
bounceRate: accum.rounds === 0 ? void 0 : accum.bounceCount / accum.rounds,
|
|
27292
|
+
meanOfficerWallMs: accum.rounds === 0 ? void 0 : accum.wallSum / accum.rounds
|
|
27293
|
+
};
|
|
27294
|
+
}
|
|
27295
|
+
function summarizeByOfficer(legs) {
|
|
27296
|
+
const byOfficer = /* @__PURE__ */ new Map();
|
|
27297
|
+
for (const leg of legs) {
|
|
27298
|
+
for (const round of leg.rounds) {
|
|
27299
|
+
const accum = byOfficer.get(round.officer) ?? emptyAccum();
|
|
27300
|
+
absorbRound(accum, round);
|
|
27301
|
+
byOfficer.set(round.officer, accum);
|
|
27302
|
+
}
|
|
27303
|
+
}
|
|
27304
|
+
const officers = ["inspector", "notary"].filter((o) => byOfficer.has(o));
|
|
27305
|
+
return officers.map((officer) => finishOfficer(officer, byOfficer.get(officer)));
|
|
27306
|
+
}
|
|
27307
|
+
var gateCyclesFamily, gate_cycles_default;
|
|
27308
|
+
var init_gate_cycles = __esm({
|
|
27309
|
+
"src/analyst-metric-families/gate-cycles.ts"() {
|
|
27310
|
+
"use strict";
|
|
27311
|
+
gateCyclesFamily = {
|
|
27312
|
+
id: "gate-cycles",
|
|
27313
|
+
contribute(input) {
|
|
27314
|
+
if (input.runs.length === 0) return void 0;
|
|
27315
|
+
const legs = input.runs.map(projectLeg).sort(compareLegs);
|
|
27316
|
+
const section = {
|
|
27317
|
+
kind: "analyst-gate-cycles",
|
|
27318
|
+
legs,
|
|
27319
|
+
byOfficer: summarizeByOfficer(legs)
|
|
27320
|
+
};
|
|
27321
|
+
return { gateCycles: section };
|
|
27322
|
+
}
|
|
27323
|
+
};
|
|
27324
|
+
gate_cycles_default = gateCyclesFamily;
|
|
27325
|
+
}
|
|
27326
|
+
});
|
|
27327
|
+
|
|
27010
27328
|
// src/analyst-metric-families/leg-wall-clock.ts
|
|
27011
27329
|
function frameSpanWallMs(span) {
|
|
27012
27330
|
return Date.parse(span.endedAt) - Date.parse(span.startedAt);
|
|
@@ -27058,21 +27376,21 @@ var init_leg_wall_clock = __esm({
|
|
|
27058
27376
|
});
|
|
27059
27377
|
|
|
27060
27378
|
// src/analyst-metric-families/round-timeline.ts
|
|
27061
|
-
function
|
|
27379
|
+
function isRecord11(value) {
|
|
27062
27380
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
27063
27381
|
}
|
|
27064
27382
|
function wallMsFromSpan2(startedAt, endedAt) {
|
|
27065
27383
|
return Date.parse(endedAt) - Date.parse(startedAt);
|
|
27066
27384
|
}
|
|
27067
27385
|
function readOutcomeStatus(body) {
|
|
27068
|
-
if (!
|
|
27386
|
+
if (!isRecord11(body.outcome)) return void 0;
|
|
27069
27387
|
const status = body.outcome.status;
|
|
27070
27388
|
if (typeof status !== "string" || status.trim() === "") return void 0;
|
|
27071
27389
|
return status;
|
|
27072
27390
|
}
|
|
27073
27391
|
function readClassCount(body) {
|
|
27074
|
-
if (!
|
|
27075
|
-
if (!
|
|
27392
|
+
if (!isRecord11(body.outcome)) return void 0;
|
|
27393
|
+
if (!isRecord11(body.outcome.decisiveFacts)) return void 0;
|
|
27076
27394
|
const classCount = body.outcome.decisiveFacts.classCount;
|
|
27077
27395
|
if (typeof classCount !== "number" || !Number.isFinite(classCount)) {
|
|
27078
27396
|
return void 0;
|
|
@@ -27185,11 +27503,13 @@ var init_analyst_metric_families = __esm({
|
|
|
27185
27503
|
"use strict";
|
|
27186
27504
|
init_acceptance_success_rework();
|
|
27187
27505
|
init_b2_frame_buckets_actions();
|
|
27506
|
+
init_gate_cycles();
|
|
27188
27507
|
init_leg_wall_clock();
|
|
27189
27508
|
init_round_timeline();
|
|
27190
27509
|
ISSUE_METRIC_FAMILIES = [
|
|
27191
27510
|
acceptance_success_rework_default,
|
|
27192
27511
|
b2_frame_buckets_actions_default,
|
|
27512
|
+
gate_cycles_default,
|
|
27193
27513
|
leg_wall_clock_default,
|
|
27194
27514
|
round_timeline_default
|
|
27195
27515
|
].sort((a, b) => a.id.localeCompare(b.id));
|
|
@@ -27214,7 +27534,7 @@ var init_analyst_metric_family = __esm({
|
|
|
27214
27534
|
|
|
27215
27535
|
// src/analyst-page.ts
|
|
27216
27536
|
import { createHash as createHash4 } from "node:crypto";
|
|
27217
|
-
import { dirname as dirname10, join as
|
|
27537
|
+
import { dirname as dirname10, join as join25 } from "node:path";
|
|
27218
27538
|
function analystIssuePageKey(address) {
|
|
27219
27539
|
const parts = ["book", address.bookKey];
|
|
27220
27540
|
if (address.issueNumber !== void 0) {
|
|
@@ -27225,7 +27545,7 @@ function analystIssuePageKey(address) {
|
|
|
27225
27545
|
return createHash4("sha256").update(parts.join("\0")).digest("hex").slice(0, 32);
|
|
27226
27546
|
}
|
|
27227
27547
|
function analystIssuePagePath(ledgerHome, address) {
|
|
27228
|
-
return
|
|
27548
|
+
return join25(ledgerHome, "analyst", "issues", `${analystIssuePageKey(address)}.json`);
|
|
27229
27549
|
}
|
|
27230
27550
|
function analystIssuePageAddressFromPage(page) {
|
|
27231
27551
|
return {
|
|
@@ -27756,7 +28076,7 @@ __export(cli_exports, {
|
|
|
27756
28076
|
});
|
|
27757
28077
|
import { realpath as realpath5 } from "node:fs/promises";
|
|
27758
28078
|
import { homedir as homedir3 } from "node:os";
|
|
27759
|
-
import { join as
|
|
28079
|
+
import { join as join26 } from "node:path";
|
|
27760
28080
|
function takePublicGlobalFlag(argv, index, options) {
|
|
27761
28081
|
const tokens = argv.slice(index);
|
|
27762
28082
|
const taken = options.takeDashed(tokens);
|
|
@@ -27802,7 +28122,7 @@ function resolveHome(env) {
|
|
|
27802
28122
|
return env.home ?? process.env.HOME ?? homedir3();
|
|
27803
28123
|
}
|
|
27804
28124
|
function resolveAgentDir(env, home) {
|
|
27805
|
-
return env.agentDir ?? process.env.PI_CODING_AGENT_DIR ??
|
|
28125
|
+
return env.agentDir ?? process.env.PI_CODING_AGENT_DIR ?? join26(home, ".pi", "agent");
|
|
27806
28126
|
}
|
|
27807
28127
|
function parseThinking(value) {
|
|
27808
28128
|
if (!THINKING_LEVELS2.has(value)) {
|
|
@@ -28672,7 +28992,7 @@ var init_cli = __esm({
|
|
|
28672
28992
|
|
|
28673
28993
|
// src/public-cli/main.ts
|
|
28674
28994
|
import { existsSync as existsSync3 } from "node:fs";
|
|
28675
|
-
import { dirname as dirname11, join as
|
|
28995
|
+
import { dirname as dirname11, join as join27 } from "node:path";
|
|
28676
28996
|
import { fileURLToPath } from "node:url";
|
|
28677
28997
|
|
|
28678
28998
|
// src/public-cli/host-pi-runtime.ts
|
|
@@ -28761,8 +29081,8 @@ function linkPackage(packageRoot2, name, targetDir) {
|
|
|
28761
29081
|
// src/public-cli/main.ts
|
|
28762
29082
|
var here = dirname11(fileURLToPath(import.meta.url));
|
|
28763
29083
|
function resolvePackageRoot(binDir) {
|
|
28764
|
-
const canonical =
|
|
28765
|
-
if (existsSync3(
|
|
29084
|
+
const canonical = join27(binDir, "..", "..");
|
|
29085
|
+
if (existsSync3(join27(canonical, "package.json"))) {
|
|
28766
29086
|
return canonical;
|
|
28767
29087
|
}
|
|
28768
29088
|
return binDir;
|
package/package.json
CHANGED
package/src/analyst-cohort.ts
CHANGED
|
@@ -24,6 +24,7 @@ import type { AnalystIssueMetricsPage } from "./analyst-page.ts";
|
|
|
24
24
|
import type { AnalystRoleAcceptanceStats } from "./analyst-metric-families/acceptance-success-rework.ts";
|
|
25
25
|
import type { AnalystLegWallClockSection } from "./analyst-metric-families/leg-wall-clock.ts";
|
|
26
26
|
import type { AnalystAcceptanceSuccessReworkSection } from "./analyst-metric-families/acceptance-success-rework.ts";
|
|
27
|
+
import type { AnalystGateCyclesSection } from "./analyst-metric-families/gate-cycles.ts";
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* #338 page ensurer — read existing page or compute via sole issue kernel.
|
|
@@ -83,6 +84,19 @@ export type AnalystCohortRoleStats = {
|
|
|
83
84
|
readonly successRate: AnalystCohortOptionalMetric;
|
|
84
85
|
};
|
|
85
86
|
|
|
87
|
+
/** Per-officer gate-cycle contrast stats within one cohort group (#446). */
|
|
88
|
+
export type AnalystCohortGateOfficerStats = {
|
|
89
|
+
readonly officer: "inspector" | "notary";
|
|
90
|
+
/** Merged paired-round count across present issues. */
|
|
91
|
+
readonly rounds: number;
|
|
92
|
+
readonly bounceCount: number;
|
|
93
|
+
readonly passCount: number;
|
|
94
|
+
/** bounceCount / rounds; absent when rounds === 0. */
|
|
95
|
+
readonly bounceRate: AnalystCohortOptionalMetric;
|
|
96
|
+
/** Mean officer subsession wall across merged rounds; absent when rounds === 0. */
|
|
97
|
+
readonly meanOfficerWallMs: AnalystCohortOptionalMetric;
|
|
98
|
+
};
|
|
99
|
+
|
|
86
100
|
export type AnalystCohortGroupResult = {
|
|
87
101
|
readonly groupLabel: string;
|
|
88
102
|
/** One entry per input issue number, in input order (vacancy single-listed). */
|
|
@@ -90,6 +104,11 @@ export type AnalystCohortGroupResult = {
|
|
|
90
104
|
readonly byRole: readonly AnalystCohortRoleStats[];
|
|
91
105
|
readonly reworkRatio: AnalystCohortOptionalMetric;
|
|
92
106
|
readonly medianWallMs: AnalystCohortOptionalMetric;
|
|
107
|
+
/**
|
|
108
|
+
* Gate-cycle by-officer fold from ensured issue pages (#446).
|
|
109
|
+
* Empty when no present page contributed a gateCycles section with rounds.
|
|
110
|
+
*/
|
|
111
|
+
readonly gateCyclesByOfficer: readonly AnalystCohortGateOfficerStats[];
|
|
93
112
|
};
|
|
94
113
|
|
|
95
114
|
export type AnalystCohortModeResult = {
|
|
@@ -97,10 +116,11 @@ export type AnalystCohortModeResult = {
|
|
|
97
116
|
readonly groups: readonly [AnalystCohortGroupResult, AnalystCohortGroupResult];
|
|
98
117
|
};
|
|
99
118
|
|
|
100
|
-
/** Issue page shape cohort actually reads (envelope +
|
|
119
|
+
/** Issue page shape cohort actually reads (envelope + metric-family sections). */
|
|
101
120
|
type AnalystCohortSourcePage = AnalystIssueMetricsPage & {
|
|
102
121
|
readonly acceptanceSuccessRework?: AnalystAcceptanceSuccessReworkSection;
|
|
103
122
|
readonly legWallClock?: AnalystLegWallClockSection;
|
|
123
|
+
readonly gateCycles?: AnalystGateCyclesSection;
|
|
104
124
|
};
|
|
105
125
|
|
|
106
126
|
const ABSENT: AnalystCohortOptionalMetric = { status: "absent" };
|
|
@@ -155,6 +175,55 @@ function finishRole(role: string, accum: RoleAccum): AnalystCohortRoleStats {
|
|
|
155
175
|
};
|
|
156
176
|
}
|
|
157
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Cross-page fold of page-level gateCycles.byOfficer numerators (#446).
|
|
180
|
+
* status→bounce/pass classification stays sole in gate-cycles family;
|
|
181
|
+
* cohort only merges already-projected counts (same ratio-merge nail as rework).
|
|
182
|
+
*/
|
|
183
|
+
type GateOfficerNumeratorAccum = {
|
|
184
|
+
rounds: number;
|
|
185
|
+
bounceCount: number;
|
|
186
|
+
passCount: number;
|
|
187
|
+
/** Σ (meanOfficerWallMs × rounds) recovered from page summaries. */
|
|
188
|
+
wallSum: number;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
function emptyGateOfficerNumeratorAccum(): GateOfficerNumeratorAccum {
|
|
192
|
+
return { rounds: 0, bounceCount: 0, passCount: 0, wallSum: 0 };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function absorbGateOfficerSummary(
|
|
196
|
+
accum: GateOfficerNumeratorAccum,
|
|
197
|
+
summary: {
|
|
198
|
+
readonly rounds: number;
|
|
199
|
+
readonly bounceCount: number;
|
|
200
|
+
readonly passCount: number;
|
|
201
|
+
readonly meanOfficerWallMs: number | undefined;
|
|
202
|
+
},
|
|
203
|
+
): void {
|
|
204
|
+
accum.rounds += summary.rounds;
|
|
205
|
+
accum.bounceCount += summary.bounceCount;
|
|
206
|
+
accum.passCount += summary.passCount;
|
|
207
|
+
if (summary.meanOfficerWallMs !== undefined) {
|
|
208
|
+
accum.wallSum += summary.meanOfficerWallMs * summary.rounds;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function finishGateOfficerNumerators(
|
|
213
|
+
officer: "inspector" | "notary",
|
|
214
|
+
accum: GateOfficerNumeratorAccum,
|
|
215
|
+
): AnalystCohortGateOfficerStats {
|
|
216
|
+
return {
|
|
217
|
+
officer,
|
|
218
|
+
rounds: accum.rounds,
|
|
219
|
+
bounceCount: accum.bounceCount,
|
|
220
|
+
passCount: accum.passCount,
|
|
221
|
+
bounceRate: rateMetric(accum.bounceCount, accum.rounds),
|
|
222
|
+
meanOfficerWallMs:
|
|
223
|
+
accum.rounds === 0 ? ABSENT : presentMetric(accum.wallSum / accum.rounds),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
158
227
|
async function aggregateGroup(
|
|
159
228
|
index: AnalystLibraryIndexPage | undefined,
|
|
160
229
|
input: AnalystCohortGroupInput,
|
|
@@ -162,6 +231,10 @@ async function aggregateGroup(
|
|
|
162
231
|
): Promise<AnalystCohortGroupResult> {
|
|
163
232
|
const issueEntries: AnalystCohortIssueEntry[] = [];
|
|
164
233
|
const roleAccums = new Map<string, RoleAccum>();
|
|
234
|
+
const gateOfficerAccums = new Map<
|
|
235
|
+
"inspector" | "notary",
|
|
236
|
+
GateOfficerNumeratorAccum
|
|
237
|
+
>();
|
|
165
238
|
let reworkWallMs = 0;
|
|
166
239
|
let totalWallMs = 0;
|
|
167
240
|
let hasReworkSample = false;
|
|
@@ -212,18 +285,37 @@ async function aggregateGroup(
|
|
|
212
285
|
legWalls.push(leg.wallMs);
|
|
213
286
|
}
|
|
214
287
|
}
|
|
288
|
+
|
|
289
|
+
// Gate-cycle fold: merge page-projected byOfficer numerators (no rescan,
|
|
290
|
+
// no second status→bounce/pass classifier — sole owner is gate-cycles family).
|
|
291
|
+
const gateCycles = page.gateCycles;
|
|
292
|
+
if (gateCycles !== undefined) {
|
|
293
|
+
for (const summary of gateCycles.byOfficer) {
|
|
294
|
+
const accum =
|
|
295
|
+
gateOfficerAccums.get(summary.officer) ?? emptyGateOfficerNumeratorAccum();
|
|
296
|
+
absorbGateOfficerSummary(accum, summary);
|
|
297
|
+
gateOfficerAccums.set(summary.officer, accum);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
215
300
|
}
|
|
216
301
|
|
|
217
302
|
const byRole = [...roleAccums.keys()]
|
|
218
303
|
.sort((a, b) => a.localeCompare(b))
|
|
219
304
|
.map((role) => finishRole(role, roleAccums.get(role)!));
|
|
220
305
|
|
|
306
|
+
const gateCyclesByOfficer = (["inspector", "notary"] as const)
|
|
307
|
+
.filter((officer) => gateOfficerAccums.has(officer))
|
|
308
|
+
.map((officer) =>
|
|
309
|
+
finishGateOfficerNumerators(officer, gateOfficerAccums.get(officer)!),
|
|
310
|
+
);
|
|
311
|
+
|
|
221
312
|
return {
|
|
222
313
|
groupLabel: input.groupLabel,
|
|
223
314
|
issues: issueEntries,
|
|
224
315
|
byRole,
|
|
225
316
|
reworkRatio: hasReworkSample ? rateMetric(reworkWallMs, totalWallMs) : ABSENT,
|
|
226
317
|
medianWallMs: optionalMedian(legWalls),
|
|
318
|
+
gateCyclesByOfficer,
|
|
227
319
|
};
|
|
228
320
|
}
|
|
229
321
|
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sole nested-volume reader for gate-cycle facts under session/auditor-roles/.
|
|
3
|
+
*
|
|
4
|
+
* Called only from the Analyst sole ledger scan (classifyScopedRun). Metric
|
|
5
|
+
* families must not open a second disk scan — they consume retained facts.
|
|
6
|
+
*
|
|
7
|
+
* Naming: records may carry pre-#440 menxia/jishizhong/fubaolang tool faces or
|
|
8
|
+
* the current gatekeeper/inspector/notary English face. Projection always uses
|
|
9
|
+
* the current English officer identity (inspector | notary).
|
|
10
|
+
*
|
|
11
|
+
* Missing auditor-roles directory (ENOENT only) → empty rounds (lawful zero).
|
|
12
|
+
* Path present but not a directory (ENOTDIR) and discovered nested JSONL that
|
|
13
|
+
* fails canonical read/parse must fail loudly (never silently under-count).
|
|
14
|
+
* An accepted gate terminating receipt (isError:false pair on dispatch/officer
|
|
15
|
+
* tool) whose required typed facts are unusable — status, dispatch officer, or
|
|
16
|
+
* first/last span missing/unknown/unparseable/inverted — also fails loudly via
|
|
17
|
+
* the same throw→ledger `auditor-roles` unreadable seam. True non-gate volumes
|
|
18
|
+
* (soul-audit noise, etc.) stay omitted from pairing.
|
|
19
|
+
*/
|
|
20
|
+
import { readdir } from "node:fs/promises";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
extractSessionTimestampSpan,
|
|
25
|
+
readLedgerSessionJsonl,
|
|
26
|
+
type LedgerSessionRow,
|
|
27
|
+
} from "./ledger-session-read.ts";
|
|
28
|
+
|
|
29
|
+
/** One completed gate round: province dispatch paired with its officer volume. */
|
|
30
|
+
export type AnalystGateCycleRound = {
|
|
31
|
+
/** 1-based chronological order among paired rounds on this leg. */
|
|
32
|
+
readonly roundIndex: number;
|
|
33
|
+
/** Current English officer face after historical alias fold. */
|
|
34
|
+
readonly officer: "inspector" | "notary";
|
|
35
|
+
/** Typed officer terminal status (pass / bounce / incomplete / …). */
|
|
36
|
+
readonly status: string;
|
|
37
|
+
/** Officer subsession first→last usable timestamp delta (ms). */
|
|
38
|
+
readonly officerWallMs: number;
|
|
39
|
+
readonly officerStartedAt: string;
|
|
40
|
+
readonly officerEndedAt: string;
|
|
41
|
+
/** findings[] length only — prose never retained. */
|
|
42
|
+
readonly findingsCount: number;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const DISPATCH_TOOLS = new Set(["ak_menxia_output", "ak_gatekeeper_output"]);
|
|
46
|
+
|
|
47
|
+
/** Officer terminating tool → current English officer identity. */
|
|
48
|
+
const OFFICER_TOOL_TO_FACE: Readonly<Record<string, "inspector" | "notary">> = {
|
|
49
|
+
ak_jishizhong_output: "inspector",
|
|
50
|
+
ak_inspector_output: "inspector",
|
|
51
|
+
ak_fubaolang_output: "notary",
|
|
52
|
+
ak_notary_output: "notary",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** Dispatch `officer` argument aliases → current English face. */
|
|
56
|
+
const OFFICER_ARG_ALIASES: Readonly<Record<string, "inspector" | "notary">> = {
|
|
57
|
+
jishizhong: "inspector",
|
|
58
|
+
inspector: "inspector",
|
|
59
|
+
fubaolang: "notary",
|
|
60
|
+
notary: "notary",
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
64
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Only true absence (ENOENT). ENOTDIR is damaged topology — must stay loud. */
|
|
68
|
+
function isMissingDirectoryError(error: unknown): boolean {
|
|
69
|
+
return (
|
|
70
|
+
error instanceof Error
|
|
71
|
+
&& "code" in error
|
|
72
|
+
&& error.code === "ENOENT"
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function normalizeOfficerArg(raw: unknown): "inspector" | "notary" | undefined {
|
|
77
|
+
if (typeof raw !== "string") return undefined;
|
|
78
|
+
return OFFICER_ARG_ALIASES[raw.trim()];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
type AcceptedGateToolCall = {
|
|
82
|
+
readonly toolName: string;
|
|
83
|
+
readonly args: Record<string, unknown> | undefined;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
function isGateTerminatingToolName(toolName: string): boolean {
|
|
87
|
+
return DISPATCH_TOOLS.has(toolName) || OFFICER_TOOL_TO_FACE[toolName] !== undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* toolCallIds whose paired toolResult is an accepted receipt (`isError === false`).
|
|
92
|
+
* Receipt is the sole lawful role product — rejected / missing results never qualify.
|
|
93
|
+
*/
|
|
94
|
+
function acceptedGateReceiptIds(
|
|
95
|
+
rows: readonly LedgerSessionRow[],
|
|
96
|
+
): ReadonlySet<string> {
|
|
97
|
+
const accepted = new Set<string>();
|
|
98
|
+
for (const row of rows) {
|
|
99
|
+
const message = isRecord(row.message) ? row.message : undefined;
|
|
100
|
+
if (message?.role !== "toolResult") continue;
|
|
101
|
+
if (typeof message.toolCallId !== "string" || message.toolCallId.length === 0) continue;
|
|
102
|
+
if (message.isError === false) accepted.add(message.toolCallId);
|
|
103
|
+
}
|
|
104
|
+
return accepted;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Last accepted gate terminating toolCall on a nested volume (dispatch or officer).
|
|
109
|
+
* Identity only — typed args are validated after recognition so unusable facts
|
|
110
|
+
* fail loud instead of being skipped as "not a gate volume". Soul-audit and
|
|
111
|
+
* other non-gate tools never qualify.
|
|
112
|
+
*/
|
|
113
|
+
function extractLastAcceptedGateToolCall(
|
|
114
|
+
rows: readonly LedgerSessionRow[],
|
|
115
|
+
): AcceptedGateToolCall | undefined {
|
|
116
|
+
const acceptedIds = acceptedGateReceiptIds(rows);
|
|
117
|
+
let last: AcceptedGateToolCall | undefined;
|
|
118
|
+
for (const row of rows) {
|
|
119
|
+
const message = isRecord(row.message) ? row.message : undefined;
|
|
120
|
+
if (message?.role !== "assistant" || !Array.isArray(message.content)) continue;
|
|
121
|
+
for (const part of message.content) {
|
|
122
|
+
if (!isRecord(part) || part.type !== "toolCall") continue;
|
|
123
|
+
// Unpaired / rejected calls have no lawful receipt — skip before reading args.
|
|
124
|
+
if (typeof part.id !== "string" || part.id.length === 0) continue;
|
|
125
|
+
if (!acceptedIds.has(part.id)) continue;
|
|
126
|
+
if (typeof part.name !== "string" || part.name.length === 0) continue;
|
|
127
|
+
if (!isGateTerminatingToolName(part.name)) continue;
|
|
128
|
+
last = {
|
|
129
|
+
toolName: part.name,
|
|
130
|
+
args: isRecord(part.arguments) ? part.arguments : undefined,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return last;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function requireAcceptedGateStatus(
|
|
138
|
+
args: Record<string, unknown> | undefined,
|
|
139
|
+
filePath: string,
|
|
140
|
+
): string {
|
|
141
|
+
if (args === undefined || typeof args.status !== "string" || args.status.trim() === "") {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`accepted gate receipt missing usable status in ${filePath}`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
return args.status.trim();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function requireAcceptedGateSpan(
|
|
150
|
+
rows: readonly LedgerSessionRow[],
|
|
151
|
+
filePath: string,
|
|
152
|
+
): { readonly startedAt: string; readonly endedAt: string; readonly wallMs: number } {
|
|
153
|
+
const span = extractSessionTimestampSpan(rows);
|
|
154
|
+
if (span.startedAt === undefined || span.endedAt === undefined) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`accepted gate volume missing session timestamp span in ${filePath}`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
const startedMs = Date.parse(span.startedAt);
|
|
160
|
+
const endedMs = Date.parse(span.endedAt);
|
|
161
|
+
if (!Number.isFinite(startedMs) || !Number.isFinite(endedMs) || endedMs < startedMs) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`accepted gate volume has unusable timestamp span in ${filePath}`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
startedAt: span.startedAt,
|
|
168
|
+
endedAt: span.endedAt,
|
|
169
|
+
wallMs: endedMs - startedMs,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
type ClassifiedVolume =
|
|
174
|
+
| {
|
|
175
|
+
readonly kind: "dispatch";
|
|
176
|
+
readonly startedAt: string;
|
|
177
|
+
readonly officer: "inspector" | "notary";
|
|
178
|
+
}
|
|
179
|
+
| {
|
|
180
|
+
readonly kind: "officer";
|
|
181
|
+
readonly startedAt: string;
|
|
182
|
+
readonly endedAt: string;
|
|
183
|
+
readonly officer: "inspector" | "notary";
|
|
184
|
+
readonly status: string;
|
|
185
|
+
readonly findingsCount: number;
|
|
186
|
+
readonly officerWallMs: number;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
async function classifyAuditorVolume(
|
|
190
|
+
filePath: string,
|
|
191
|
+
): Promise<ClassifiedVolume | undefined> {
|
|
192
|
+
// Canonical JSONL errors propagate — failure honesty (never wash to fewer rounds).
|
|
193
|
+
const rows = await readLedgerSessionJsonl(filePath);
|
|
194
|
+
// Recognize accepted gate tool first. Non-gate volumes stay omitted; once a
|
|
195
|
+
// gate receipt is present, required typed facts must not silently under-count.
|
|
196
|
+
const call = extractLastAcceptedGateToolCall(rows);
|
|
197
|
+
if (call === undefined) return undefined;
|
|
198
|
+
|
|
199
|
+
const span = requireAcceptedGateSpan(rows, filePath);
|
|
200
|
+
const status = requireAcceptedGateStatus(call.args, filePath);
|
|
201
|
+
const findings = call.args?.findings;
|
|
202
|
+
const findingsCount = Array.isArray(findings) ? findings.length : 0;
|
|
203
|
+
|
|
204
|
+
if (DISPATCH_TOOLS.has(call.toolName)) {
|
|
205
|
+
if (status !== "dispatch") {
|
|
206
|
+
throw new Error(
|
|
207
|
+
`accepted dispatch receipt has non-dispatch status ${JSON.stringify(status)} in ${filePath}`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
const officer = normalizeOfficerArg(call.args?.officer);
|
|
211
|
+
if (officer === undefined) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`accepted dispatch receipt missing or unknown officer in ${filePath}`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
kind: "dispatch",
|
|
218
|
+
startedAt: span.startedAt,
|
|
219
|
+
officer,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const officer = OFFICER_TOOL_TO_FACE[call.toolName];
|
|
224
|
+
if (officer === undefined) {
|
|
225
|
+
// isGateTerminatingToolName already screened; keep loud if tables drift.
|
|
226
|
+
throw new Error(
|
|
227
|
+
`accepted gate receipt has unknown officer tool ${call.toolName} in ${filePath}`,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
kind: "officer",
|
|
232
|
+
startedAt: span.startedAt,
|
|
233
|
+
endedAt: span.endedAt,
|
|
234
|
+
officer,
|
|
235
|
+
status,
|
|
236
|
+
findingsCount,
|
|
237
|
+
officerWallMs: span.wallMs,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function pairGateRounds(
|
|
242
|
+
volumes: readonly ClassifiedVolume[],
|
|
243
|
+
): readonly AnalystGateCycleRound[] {
|
|
244
|
+
const ordered = [...volumes].sort((a, b) => {
|
|
245
|
+
if (a.startedAt !== b.startedAt) return a.startedAt.localeCompare(b.startedAt);
|
|
246
|
+
// Stable tie-break: dispatch before officer at identical start (should not happen).
|
|
247
|
+
if (a.kind !== b.kind) return a.kind === "dispatch" ? -1 : 1;
|
|
248
|
+
return 0;
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const usedOfficerIdx = new Set<number>();
|
|
252
|
+
const rounds: AnalystGateCycleRound[] = [];
|
|
253
|
+
|
|
254
|
+
for (let i = 0; i < ordered.length; i += 1) {
|
|
255
|
+
const vol = ordered[i]!;
|
|
256
|
+
if (vol.kind !== "dispatch") continue;
|
|
257
|
+
let match: { index: number; officer: Extract<ClassifiedVolume, { kind: "officer" }> } | undefined;
|
|
258
|
+
for (let j = i + 1; j < ordered.length; j += 1) {
|
|
259
|
+
if (usedOfficerIdx.has(j)) continue;
|
|
260
|
+
const candidate = ordered[j]!;
|
|
261
|
+
if (candidate.kind !== "officer") continue;
|
|
262
|
+
if (candidate.officer !== vol.officer) continue;
|
|
263
|
+
// First unused later officer with matching identity.
|
|
264
|
+
match = { index: j, officer: candidate };
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
if (match === undefined) continue;
|
|
268
|
+
usedOfficerIdx.add(match.index);
|
|
269
|
+
rounds.push({
|
|
270
|
+
roundIndex: rounds.length + 1,
|
|
271
|
+
officer: match.officer.officer,
|
|
272
|
+
status: match.officer.status,
|
|
273
|
+
officerWallMs: match.officer.officerWallMs,
|
|
274
|
+
officerStartedAt: match.officer.startedAt,
|
|
275
|
+
officerEndedAt: match.officer.endedAt,
|
|
276
|
+
findingsCount: match.officer.findingsCount,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return rounds;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Read and pair gate-cycle rounds from a run's session/auditor-roles directory.
|
|
285
|
+
* ENOENT (directory truly absent) → []. ENOTDIR and other errors propagate
|
|
286
|
+
* (failure honesty — damaged topology must not wash to zero rounds).
|
|
287
|
+
*/
|
|
288
|
+
export async function readAnalystGateCyclesFromAuditorRoles(
|
|
289
|
+
auditorRolesDirectory: string,
|
|
290
|
+
): Promise<readonly AnalystGateCycleRound[]> {
|
|
291
|
+
let names: string[];
|
|
292
|
+
try {
|
|
293
|
+
const entries = await readdir(auditorRolesDirectory, { withFileTypes: true });
|
|
294
|
+
names = entries
|
|
295
|
+
.filter((e) => e.isFile() && e.name.endsWith(".jsonl"))
|
|
296
|
+
.map((e) => e.name)
|
|
297
|
+
.sort();
|
|
298
|
+
} catch (error) {
|
|
299
|
+
if (isMissingDirectoryError(error)) return [];
|
|
300
|
+
throw error;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const volumes: ClassifiedVolume[] = [];
|
|
304
|
+
for (const name of names) {
|
|
305
|
+
const classified = await classifyAuditorVolume(join(auditorRolesDirectory, name));
|
|
306
|
+
if (classified !== undefined) volumes.push(classified);
|
|
307
|
+
}
|
|
308
|
+
return pairGateRounds(volumes);
|
|
309
|
+
}
|
package/src/analyst-ledger.ts
CHANGED
|
@@ -39,6 +39,12 @@ import type {
|
|
|
39
39
|
AnalystScopeConflict,
|
|
40
40
|
AnalystUnreadableRun,
|
|
41
41
|
} from "./analyst-page.ts";
|
|
42
|
+
import {
|
|
43
|
+
readAnalystGateCyclesFromAuditorRoles,
|
|
44
|
+
type AnalystGateCycleRound,
|
|
45
|
+
} from "./analyst-gate-cycles-read.ts";
|
|
46
|
+
|
|
47
|
+
export type { AnalystGateCycleRound } from "./analyst-gate-cycles-read.ts";
|
|
42
48
|
|
|
43
49
|
function isMissingPathError(error: unknown): boolean {
|
|
44
50
|
return (
|
|
@@ -233,6 +239,12 @@ export type AnalystReadableRunFacts = {
|
|
|
233
239
|
* Model-groups mode lists empty as typed session-model vacancy, never as "".
|
|
234
240
|
*/
|
|
235
241
|
readonly models: readonly string[];
|
|
242
|
+
/**
|
|
243
|
+
* Paired gate-cycle rounds from session/auditor-roles/ (#446).
|
|
244
|
+
* Missing directory → empty (lawful zero rounds).
|
|
245
|
+
* Damaged discovered nested JSONL → leg unreadable (`auditor-roles` source).
|
|
246
|
+
*/
|
|
247
|
+
readonly gateCycles: readonly AnalystGateCycleRound[];
|
|
236
248
|
};
|
|
237
249
|
|
|
238
250
|
export type AnalystScopedRunScan = {
|
|
@@ -384,6 +396,28 @@ async function classifyScopedRun(input: {
|
|
|
384
396
|
);
|
|
385
397
|
}
|
|
386
398
|
|
|
399
|
+
// Nested auditor-roles gate pairs stay inside the sole scan (families must
|
|
400
|
+
// not readdir this tree again). Missing directory → []. Damaged discovered
|
|
401
|
+
// nested JSONL is page-local unreadable — never silently under-count rounds.
|
|
402
|
+
let gateCycles: readonly AnalystGateCycleRound[];
|
|
403
|
+
try {
|
|
404
|
+
gateCycles = await readAnalystGateCyclesFromAuditorRoles(
|
|
405
|
+
join(input.runDirectory, "session", "auditor-roles"),
|
|
406
|
+
);
|
|
407
|
+
} catch (error) {
|
|
408
|
+
return {
|
|
409
|
+
kind: "unreadable",
|
|
410
|
+
entry: {
|
|
411
|
+
runId: input.runId,
|
|
412
|
+
book: input.book,
|
|
413
|
+
missingSources: ["auditor-roles"],
|
|
414
|
+
reason: errorText(error),
|
|
415
|
+
firstFrameAt: { status: "present", at: frameSpan.startedAt },
|
|
416
|
+
lastFrameAt: { status: "present", at: frameSpan.endedAt },
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
387
421
|
return {
|
|
388
422
|
kind: "readable",
|
|
389
423
|
facts: {
|
|
@@ -394,6 +428,7 @@ async function classifyScopedRun(input: {
|
|
|
394
428
|
toolIntervals,
|
|
395
429
|
terminal,
|
|
396
430
|
models,
|
|
431
|
+
gateCycles,
|
|
397
432
|
},
|
|
398
433
|
};
|
|
399
434
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate-cycle metric family (#446).
|
|
3
|
+
*
|
|
4
|
+
* Consumes sole-scan retained gateCycles facts only — no second ledger scan.
|
|
5
|
+
* Emits per-leg round counts / officer wall / terminal status, plus by-officer
|
|
6
|
+
* bounce rate and mean officer wall. findings prose is never read (count only).
|
|
7
|
+
*/
|
|
8
|
+
import type { AnalystGateCycleRound } from "../analyst-gate-cycles-read.ts";
|
|
9
|
+
import type { AnalystReadableRunFacts } from "../analyst-ledger.ts";
|
|
10
|
+
import type { AnalystMetricFamilyModule } from "../analyst-metric-family.ts";
|
|
11
|
+
|
|
12
|
+
export type AnalystGateCyclesRoundRow = {
|
|
13
|
+
readonly roundIndex: number;
|
|
14
|
+
readonly officer: "inspector" | "notary";
|
|
15
|
+
readonly status: string;
|
|
16
|
+
readonly officerWallMs: number;
|
|
17
|
+
readonly findingsCount: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type AnalystGateCyclesLeg = {
|
|
21
|
+
readonly runId: string;
|
|
22
|
+
readonly book: string;
|
|
23
|
+
readonly role: string;
|
|
24
|
+
/** Paired dispatch↔officer volume count (0 when no auditor-roles). */
|
|
25
|
+
readonly roundCount: number;
|
|
26
|
+
readonly rounds: readonly AnalystGateCyclesRoundRow[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type AnalystGateCyclesOfficerSummary = {
|
|
30
|
+
readonly officer: "inspector" | "notary";
|
|
31
|
+
readonly rounds: number;
|
|
32
|
+
readonly bounceCount: number;
|
|
33
|
+
readonly passCount: number;
|
|
34
|
+
/**
|
|
35
|
+
* bounceCount / rounds. Absent when rounds === 0 (never encode as 0 rate
|
|
36
|
+
* with empty denominator).
|
|
37
|
+
*/
|
|
38
|
+
readonly bounceRate: number | undefined;
|
|
39
|
+
/** Mean officer subsession wall across this officer's rounds; absent if none. */
|
|
40
|
+
readonly meanOfficerWallMs: number | undefined;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type AnalystGateCyclesSection = {
|
|
44
|
+
readonly kind: "analyst-gate-cycles";
|
|
45
|
+
readonly legs: readonly AnalystGateCyclesLeg[];
|
|
46
|
+
readonly byOfficer: readonly AnalystGateCyclesOfficerSummary[];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function projectRound(round: AnalystGateCycleRound): AnalystGateCyclesRoundRow {
|
|
50
|
+
return {
|
|
51
|
+
roundIndex: round.roundIndex,
|
|
52
|
+
officer: round.officer,
|
|
53
|
+
status: round.status,
|
|
54
|
+
officerWallMs: round.officerWallMs,
|
|
55
|
+
findingsCount: round.findingsCount,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function projectLeg(facts: AnalystReadableRunFacts): AnalystGateCyclesLeg {
|
|
60
|
+
const rounds = facts.gateCycles.map(projectRound);
|
|
61
|
+
return {
|
|
62
|
+
runId: facts.runId,
|
|
63
|
+
book: facts.book,
|
|
64
|
+
role: facts.role,
|
|
65
|
+
roundCount: rounds.length,
|
|
66
|
+
rounds,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function compareLegs(a: AnalystGateCyclesLeg, b: AnalystGateCyclesLeg): number {
|
|
71
|
+
if (a.book !== b.book) return a.book.localeCompare(b.book);
|
|
72
|
+
if (a.role !== b.role) return a.role.localeCompare(b.role);
|
|
73
|
+
return a.runId.localeCompare(b.runId);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type OfficerAccum = {
|
|
77
|
+
rounds: number;
|
|
78
|
+
bounceCount: number;
|
|
79
|
+
passCount: number;
|
|
80
|
+
wallSum: number;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
function emptyAccum(): OfficerAccum {
|
|
84
|
+
return { rounds: 0, bounceCount: 0, passCount: 0, wallSum: 0 };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function absorbRound(accum: OfficerAccum, round: AnalystGateCyclesRoundRow): void {
|
|
88
|
+
accum.rounds += 1;
|
|
89
|
+
accum.wallSum += round.officerWallMs;
|
|
90
|
+
if (round.status === "bounce") accum.bounceCount += 1;
|
|
91
|
+
if (round.status === "pass") accum.passCount += 1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function finishOfficer(
|
|
95
|
+
officer: "inspector" | "notary",
|
|
96
|
+
accum: OfficerAccum,
|
|
97
|
+
): AnalystGateCyclesOfficerSummary {
|
|
98
|
+
return {
|
|
99
|
+
officer,
|
|
100
|
+
rounds: accum.rounds,
|
|
101
|
+
bounceCount: accum.bounceCount,
|
|
102
|
+
passCount: accum.passCount,
|
|
103
|
+
bounceRate: accum.rounds === 0 ? undefined : accum.bounceCount / accum.rounds,
|
|
104
|
+
meanOfficerWallMs: accum.rounds === 0 ? undefined : accum.wallSum / accum.rounds,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function summarizeByOfficer(
|
|
109
|
+
legs: readonly AnalystGateCyclesLeg[],
|
|
110
|
+
): readonly AnalystGateCyclesOfficerSummary[] {
|
|
111
|
+
const byOfficer = new Map<"inspector" | "notary", OfficerAccum>();
|
|
112
|
+
for (const leg of legs) {
|
|
113
|
+
for (const round of leg.rounds) {
|
|
114
|
+
const accum = byOfficer.get(round.officer) ?? emptyAccum();
|
|
115
|
+
absorbRound(accum, round);
|
|
116
|
+
byOfficer.set(round.officer, accum);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Stable officer order; only officers that appeared on the board.
|
|
120
|
+
const officers = (["inspector", "notary"] as const).filter((o) => byOfficer.has(o));
|
|
121
|
+
return officers.map((officer) => finishOfficer(officer, byOfficer.get(officer)!));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Discovered by analyst-metric-families loader (default export). */
|
|
125
|
+
const gateCyclesFamily: AnalystMetricFamilyModule = {
|
|
126
|
+
id: "gate-cycles",
|
|
127
|
+
contribute(input) {
|
|
128
|
+
if (input.runs.length === 0) return undefined;
|
|
129
|
+
const legs = input.runs.map(projectLeg).sort(compareLegs);
|
|
130
|
+
const section: AnalystGateCyclesSection = {
|
|
131
|
+
kind: "analyst-gate-cycles",
|
|
132
|
+
legs,
|
|
133
|
+
byOfficer: summarizeByOfficer(legs),
|
|
134
|
+
};
|
|
135
|
+
return { gateCycles: section };
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export default gateCyclesFamily;
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import type { AnalystMetricFamilyModule } from "./analyst-metric-family.ts";
|
|
12
12
|
import acceptanceSuccessReworkFamily from "./analyst-metric-families/acceptance-success-rework.ts";
|
|
13
13
|
import b2FrameBucketsActionsFamily from "./analyst-metric-families/b2-frame-buckets-actions.ts";
|
|
14
|
+
import gateCyclesFamily from "./analyst-metric-families/gate-cycles.ts";
|
|
14
15
|
import legWallClockFamily from "./analyst-metric-families/leg-wall-clock.ts";
|
|
15
16
|
import roundTimelineFamily from "./analyst-metric-families/round-timeline.ts";
|
|
16
17
|
|
|
@@ -21,6 +22,7 @@ import roundTimelineFamily from "./analyst-metric-families/round-timeline.ts";
|
|
|
21
22
|
const ISSUE_METRIC_FAMILIES: readonly AnalystMetricFamilyModule[] = [
|
|
22
23
|
acceptanceSuccessReworkFamily,
|
|
23
24
|
b2FrameBucketsActionsFamily,
|
|
25
|
+
gateCyclesFamily,
|
|
24
26
|
legWallClockFamily,
|
|
25
27
|
roundTimelineFamily,
|
|
26
28
|
].sort((a, b) => a.id.localeCompare(b.id));
|
package/src/analyst-page.ts
CHANGED
|
@@ -25,7 +25,13 @@ export type AnalystMissingSource =
|
|
|
25
25
|
| "tool-association"
|
|
26
26
|
| "terminal-artifact"
|
|
27
27
|
/** Model-groups mode: leg has no usable session model identity. */
|
|
28
|
-
| "session-model"
|
|
28
|
+
| "session-model"
|
|
29
|
+
/**
|
|
30
|
+
* Nested session/auditor-roles volume was discovered but failed canonical
|
|
31
|
+
* read/parse (#446). Missing auditor-roles directory is lawful zero rounds,
|
|
32
|
+
* not this face.
|
|
33
|
+
*/
|
|
34
|
+
| "auditor-roles";
|
|
29
35
|
|
|
30
36
|
/**
|
|
31
37
|
* First usable session timestamp retained for an unreadable run when the
|