@volter/supercode-interchange 0.1.21 → 0.1.23
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/LICENSE-diffy-MIT +23 -0
- package/README.md +422 -2
- package/channel-records.mjs +71 -0
- package/channel-session-counts.mjs +81 -0
- package/channels-read.mjs +90 -0
- package/corpus-audit-records.mjs +192 -0
- package/corpus-audit-render.mjs +47 -0
- package/corpus-audit-schema.mjs +89 -0
- package/corpus-audit.d.ts +16 -0
- package/corpus-audit.mjs +132 -0
- package/discovery.mjs +18 -6
- package/file-io.mjs +21 -2
- package/harness-settings.mjs +19 -0
- package/hermes-fire-links.mjs +135 -0
- package/hermes-fires.mjs +106 -0
- package/hermes-read-home.d.ts +3 -0
- package/hermes-read-home.mjs +235 -0
- package/hermes-read-store.mjs +49 -0
- package/hermes-read-validation.mjs +170 -0
- package/hermes-read-workflow.mjs +164 -0
- package/hermes-read-yaml.mjs +68 -0
- package/hermes-store.mjs +1 -1
- package/homes.mjs +2 -2
- package/index.d.ts +16 -1
- package/index.mjs +1 -1
- package/inspection-builtin-tools.json +261 -0
- package/inspection-config-fields.json +765 -0
- package/inspection-schemas.d.ts +4 -0
- package/inspection-schemas.mjs +117 -0
- package/job-notepad-read.mjs +97 -0
- package/jobs-control.mjs +250 -0
- package/jobs-read.mjs +198 -0
- package/jsonl.mjs +4 -3
- package/memory-show.mjs +187 -0
- package/metadata.mjs +45 -3
- package/native-json.mjs +46 -3
- package/ontology-contracts.d.ts +5 -0
- package/ontology-contracts.mjs +29 -0
- package/ontology-operations.json +198 -0
- package/ontology-schemas.json +7946 -0
- package/openclaw-jobs-read.mjs +117 -0
- package/openclaw-profile-home.mjs +142 -0
- package/orchestrator-read-records.mjs +112 -0
- package/orchestrator-store.mjs +21 -9
- package/package.json +59 -4
- package/parity-ledger.json +6984 -0
- package/parity-orchestration.json +5538 -0
- package/parity-report.d.ts +7 -0
- package/parity-report.mjs +46 -0
- package/preset-profiles.mjs +27 -0
- package/previews.mjs +25 -3
- package/profile-command.mjs +32 -0
- package/profile-orchestrator-door.mjs +104 -0
- package/profile-paths.mjs +25 -0
- package/profiles-control.mjs +98 -0
- package/profiles-read.mjs +119 -0
- package/reduction-attribution.mjs +54 -0
- package/reduction-diff.mjs +165 -0
- package/reduction-log.mjs +177 -0
- package/reduction-view.mjs +145 -0
- package/retained-projection.mjs +9 -0
- package/routes-read.mjs +103 -0
- package/runs-read.mjs +224 -0
- package/session-activity.d.ts +23 -0
- package/session-activity.mjs +124 -0
- package/session-append-scan.mjs +27 -17
- package/session-branch-handoff.mjs +115 -0
- package/session-claude-graph.mjs +29 -7
- package/session-claude-index.mjs +732 -114
- package/session-claude-runtime.d.ts +28 -0
- package/session-claude-runtime.mjs +171 -6
- package/session-claude-scan-store.mjs +164 -0
- package/session-claude.mjs +9 -0
- package/session-codex-activity.d.ts +6 -0
- package/session-codex-activity.mjs +120 -0
- package/session-codex-window.mjs +24 -4
- package/session-config-scan.mjs +141 -0
- package/session-convert.d.ts +33 -0
- package/session-convert.mjs +179 -0
- package/session-emission-version.json +1 -0
- package/session-file.mjs +11 -1
- package/session-follower.mjs +17 -5
- package/session-fresh-id.mjs +15 -0
- package/session-hermes-import.mjs +61 -0
- package/session-hermes.mjs +1 -1
- package/session-import.mjs +37 -0
- package/session-inspect.d.ts +18 -0
- package/session-inspect.mjs +122 -0
- package/session-machine-tree.d.ts +33 -0
- package/session-machine-tree.mjs +274 -0
- package/session-materialize-files.mjs +78 -0
- package/session-materialize-opencode.mjs +137 -0
- package/session-materialize.mjs +186 -0
- package/session-message.mjs +85 -0
- package/session-model.d.ts +9 -0
- package/session-model.mjs +1 -0
- package/session-opencode-store.mjs +39 -3
- package/session-path.mjs +6 -3
- package/session-pi-content.mjs +7 -2
- package/session-read.d.ts +134 -2
- package/session-read.mjs +86 -9
- package/session-recorded-config.mjs +3 -2
- package/session-records.mjs +12 -6
- package/session-residue-store.mjs +90 -0
- package/session-residue.mjs +150 -0
- package/session-resume-instructions.mjs +1 -1
- package/session-saved-read.mjs +105 -0
- package/session-transfer.mjs +141 -0
- package/session-views.mjs +1 -0
- package/session-write-claude.mjs +135 -0
- package/session-write-codex.mjs +146 -0
- package/session-write-gemini.mjs +77 -0
- package/session-write-goose.mjs +99 -0
- package/session-write-grok.mjs +70 -0
- package/session-write-native.mjs +41 -0
- package/session-write-opencode.mjs +197 -0
- package/session-write-pi.mjs +130 -0
- package/session-write-residue.mjs +19 -0
- package/session-write-utils.mjs +77 -0
- package/session-write.d.ts +10 -0
- package/session-write.mjs +37 -0
- package/skill-toml.mjs +80 -0
- package/skills-control.mjs +193 -0
- package/skills-list.mjs +237 -0
- package/source-files.mjs +12 -2
- package/sqlite-reader.mjs +134 -18
- package/support-registry.json +402 -0
- package/support-report.mjs +69 -0
- package/trigger-records.mjs +77 -0
- package/triggers-read.mjs +67 -0
- package/workflow-read.mjs +165 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Recorded channel inventory/status. Configured means credential presence;
|
|
2
|
+
// connection health is always unknown. This reader creates no endpoint.
|
|
3
|
+
import { isAbsolute } from 'node:path';
|
|
4
|
+
import { resolveHarnessHomes } from './homes.mjs';
|
|
5
|
+
import { profilePathJoin as join, profilePathParent as parent } from './profile-paths.mjs';
|
|
6
|
+
import { readChannelFolderHome } from './hermes-read-home.mjs';
|
|
7
|
+
import { readOpenclawChannelHome } from './openclaw-profile-home.mjs';
|
|
8
|
+
import { channelSessionCounts } from './channel-session-counts.mjs';
|
|
9
|
+
import { nativeDuplicateObjectKeys, parseNativeJson, stringifyNativeJsonMember, setNativeInteger } from './native-json.mjs';
|
|
10
|
+
|
|
11
|
+
export const CHANNEL_HARNESSES = Object.freeze(['hermes', 'openclaw', 'orchestrator']);
|
|
12
|
+
const object = value => value != null && typeof value === 'object' && !Array.isArray(value);
|
|
13
|
+
const compare = (a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b));
|
|
14
|
+
const refuse = (message, rpcCode = -32602) => { throw Object.assign(new Error(message), { rpcCode }); };
|
|
15
|
+
const filesystem = (path, cwd) => !path.length || isAbsolute(path) || process.platform === 'win32' && /^[A-Za-z]:/u.test(path) ? path : join(cwd, path);
|
|
16
|
+
function struct(value, fields, name) {
|
|
17
|
+
if (Array.isArray(value)) {
|
|
18
|
+
if (value.length > fields.length) refuse(`${name} has too many fields`);
|
|
19
|
+
return parseNativeJson(`{${value.map((_, at) => `${JSON.stringify(fields[at])}:${stringifyNativeJsonMember(value, String(at))}`).join(',')}}`);
|
|
20
|
+
}
|
|
21
|
+
if (!object(value)) refuse(`${name} must be a struct`);
|
|
22
|
+
if (nativeDuplicateObjectKeys(value).some(key => fields.includes(key))) refuse(`${name} has a duplicate field`);
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
function query(params, env) {
|
|
26
|
+
const selected = struct(params, ['harness', 'name', 'homes'], 'channels params');
|
|
27
|
+
for (const key of ['harness', 'name']) if (selected[key] != null && typeof selected[key] !== 'string') refuse(`${key} must be a string`);
|
|
28
|
+
const homes = resolveHarnessHomes(env);
|
|
29
|
+
if (Object.hasOwn(selected, 'homes')) {
|
|
30
|
+
const supplied = struct(selected.homes, Object.keys(homes), 'homes');
|
|
31
|
+
for (const key of Object.keys(homes)) if (Object.hasOwn(supplied, key)) {
|
|
32
|
+
if (typeof supplied[key] !== 'string') refuse(`homes.${key} must be a string`);
|
|
33
|
+
homes[key] = supplied[key];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { harness: selected.harness ?? null, name: selected.name ?? null, homes };
|
|
37
|
+
}
|
|
38
|
+
const envCredentials = [
|
|
39
|
+
['telegram', ['TELEGRAM_BOT_TOKEN']], ['discord', ['DISCORD_BOT_TOKEN']], ['slack', ['SLACK_BOT_TOKEN']],
|
|
40
|
+
['whatsapp_cloud', ['WHATSAPP_CLOUD_PHONE_NUMBER_ID', 'WHATSAPP_CLOUD_ACCESS_TOKEN'], true], ['signal', ['SIGNAL_HTTP_URL']], ['mattermost', ['MATTERMOST_TOKEN']],
|
|
41
|
+
['matrix', ['MATRIX_ACCESS_TOKEN', 'MATRIX_PASSWORD']], ['homeassistant', ['HASS_TOKEN']], ['email', ['EMAIL_ADDRESS', 'EMAIL_PASSWORD', 'EMAIL_IMAP_HOST', 'EMAIL_SMTP_HOST'], true],
|
|
42
|
+
['sms', ['TWILIO_ACCOUNT_SID']], ['dingtalk', ['DINGTALK_CLIENT_ID', 'DINGTALK_CLIENT_SECRET'], true], ['feishu', ['FEISHU_APP_ID', 'FEISHU_APP_SECRET'], true],
|
|
43
|
+
['wecom', ['WECOM_BOT_ID', 'WECOM_SECRET'], true], ['wecom_callback', ['WECOM_CALLBACK_CORP_ID', 'WECOM_CALLBACK_CORP_SECRET'], true],
|
|
44
|
+
['weixin', ['WEIXIN_TOKEN', 'WEIXIN_ACCOUNT_ID']], ['bluebubbles', ['BLUEBUBBLES_SERVER_URL', 'BLUEBUBBLES_PASSWORD'], true],
|
|
45
|
+
['qqbot', ['QQ_APP_ID', 'QQ_CLIENT_SECRET']], ['yuanbao', ['YUANBAO_APP_ID', 'YUANBAO_APP_SECRET'], true], ['relay', ['GATEWAY_RELAY_URL']], ['api_server', ['API_SERVER_KEY']],
|
|
46
|
+
];
|
|
47
|
+
const presence = (platform, env) => {
|
|
48
|
+
const entry = envCredentials.find(([name]) => name === platform);
|
|
49
|
+
return entry == null ? null : entry[2] ? entry[1].every(key => Object.hasOwn(env, key)) : entry[1].some(key => Object.hasOwn(env, key));
|
|
50
|
+
};
|
|
51
|
+
async function rows(selected, context) {
|
|
52
|
+
if (selected.harness != null && !CHANNEL_HARNESSES.includes(selected.harness)) refuse(`harness \`${selected.harness}\` has no channel concept (channels exist for: ${CHANNEL_HARNESSES.join(', ')})`, -32020);
|
|
53
|
+
const homes = Object.fromEntries(Object.entries(selected.homes).map(([key, path]) => [key, filesystem(path, context.cwd)])), result = [];
|
|
54
|
+
for (const harness of CHANNEL_HARNESSES) {
|
|
55
|
+
if (selected.harness != null && selected.harness !== harness) continue;
|
|
56
|
+
const counts = await channelSessionCounts(harness, homes);
|
|
57
|
+
let channels;
|
|
58
|
+
try {
|
|
59
|
+
if (harness === 'openclaw') channels = readOpenclawChannelHome(homes.openclaw);
|
|
60
|
+
else {
|
|
61
|
+
const profiles = readChannelFolderHome(harness === 'hermes' ? parent(homes.hermes) ?? '.' : homes.orchestrator, harness);
|
|
62
|
+
channels = [];
|
|
63
|
+
for (const name of [...profiles.keys()].filter(name => harness !== 'hermes' || name === 'default').sort((a, b) => Number(a !== 'default') - Number(b !== 'default') || compare(a, b))) {
|
|
64
|
+
const recorded = new Map(profiles.get(name).map(row => [row.name, row]));
|
|
65
|
+
if (harness === 'hermes') for (const [platform] of envCredentials) if (!recorded.has(platform) && presence(platform, context.env) === true) recorded.set(platform, { name: platform, account: null, enabled: true, configured: false });
|
|
66
|
+
for (const key of [...recorded.keys()].sort(compare)) {
|
|
67
|
+
const row = recorded.get(key), present = harness === 'hermes' ? presence(key, context.env) : null;
|
|
68
|
+
channels.push({ ...row, kind: key, configured: present === true || row.configured || present == null });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch { continue; }
|
|
73
|
+
for (const row of channels) {
|
|
74
|
+
const value = { name: row.name, harness, kind: row.kind, account: row.account, enabled: row.enabled, configured: row.configured, status: 'unknown', sessions: null };
|
|
75
|
+
setNativeInteger(value, 'sessions', counts.get(row.kind) ?? 0n); result.push(value);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
export async function listChannels(params, { env = process.env, cwd = process.cwd() } = {}) {
|
|
81
|
+
return { schema: 'supercode.channels.v1', channels: await rows(query(params, env), { env, cwd }) };
|
|
82
|
+
}
|
|
83
|
+
export async function channelStatus(params, { env = process.env, cwd = process.cwd() } = {}) {
|
|
84
|
+
const selected = query(params, env);
|
|
85
|
+
if (selected.harness == null) refuse('`harness` is required');
|
|
86
|
+
if (selected.name == null) refuse('`name` is required');
|
|
87
|
+
const channel = (await rows(selected, { env, cwd })).find(row => row.name === selected.name);
|
|
88
|
+
if (channel == null) refuse(`\`${selected.harness}\` has no channel \`${selected.name}\``);
|
|
89
|
+
return { schema: 'supercode.channels.v1', channel };
|
|
90
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import {parseNativeJson, setNativeInteger, stringifyNativeJson} from './native-json.mjs';
|
|
2
|
+
import {field, stringField} from './session-codec-utils.mjs';
|
|
3
|
+
import {nativeTrim} from './text.mjs';
|
|
4
|
+
import {opencodeImagePart} from './session-opencode-content.mjs';
|
|
5
|
+
import {piImageShape} from './session-pi-content.mjs';
|
|
6
|
+
import {auditClaudeRecord, auditCodexRecord} from './corpus-audit-schema.mjs';
|
|
7
|
+
|
|
8
|
+
export const AUDIT_COVERAGE = Object.freeze(['normalized', 'retained', 'dropped', 'unmodeled']);
|
|
9
|
+
export const auditOrder = (a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b));
|
|
10
|
+
const sorted = map => [...map].sort(([a], [b]) => auditOrder(a, b));
|
|
11
|
+
const next = count => ((count ?? 0n) + 1n) & ((1n << 64n) - 1n);
|
|
12
|
+
const countValue = (count, coverage, unmodeled_fields) => {
|
|
13
|
+
const out = {coverage}; setNativeInteger(out, 'count', count);
|
|
14
|
+
if (unmodeled_fields !== undefined) out.unmodeled_fields = unmodeled_fields;
|
|
15
|
+
return out;
|
|
16
|
+
};
|
|
17
|
+
const ordered = entries => parseNativeJson(`{${entries.map(([key, value]) => `${JSON.stringify(key)}:${stringifyNativeJson(value)}`).join(',')}}`);
|
|
18
|
+
|
|
19
|
+
/** Counter state is exact u64; the public value keeps ordinary JS numbers
|
|
20
|
+
* with the shared native-JSON facts used by the lossless serializer. */
|
|
21
|
+
export class CorpusAuditReport {
|
|
22
|
+
constructor(corpus) {
|
|
23
|
+
this.corpus = corpus; this.files = 0n; this.lines = 0n; this.parse_errors = 0n;
|
|
24
|
+
this.records = new Map(); this.blocks = new Map(); this.tools = new Map(); this.notes = new Map();
|
|
25
|
+
}
|
|
26
|
+
bump(key, coverage, extra = []) {
|
|
27
|
+
const tally = this.records.get(key) ?? {coverage, count:0n, fields:new Map()};
|
|
28
|
+
tally.coverage = coverage; tally.count = next(tally.count);
|
|
29
|
+
for (const key of extra) tally.fields.set(key, next(tally.fields.get(key)));
|
|
30
|
+
this.records.set(key, tally);
|
|
31
|
+
}
|
|
32
|
+
block(tag, coverage) {
|
|
33
|
+
const buckets = this.blocks.get(tag) ?? new Map();
|
|
34
|
+
buckets.set(coverage, next(buckets.get(coverage))); this.blocks.set(tag, buckets);
|
|
35
|
+
}
|
|
36
|
+
tool(name) { this.tools.set(name, next(this.tools.get(name))); }
|
|
37
|
+
note(name) { this.notes.set(name, next(this.notes.get(name))); }
|
|
38
|
+
toJSON() {
|
|
39
|
+
const records = ordered(sorted(this.records).map(([key, tally]) => [key, countValue(tally.count, tally.coverage, [...tally.fields.keys()].sort(auditOrder))]));
|
|
40
|
+
const blocks = ordered(sorted(this.blocks).map(([key, buckets]) => [key, AUDIT_COVERAGE.filter(c => buckets.has(c)).map(c => countValue(buckets.get(c), c))]));
|
|
41
|
+
const tools = ordered(sorted(this.tools)), notes = ordered(sorted(this.notes));
|
|
42
|
+
const out = {corpus:this.corpus};
|
|
43
|
+
for (const key of ['files', 'lines', 'parse_errors']) setNativeInteger(out, key, this[key]);
|
|
44
|
+
Object.assign(out, {records, blocks, tools, notes});
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// The CLI enables serde_json preserve_order: raw Grok scalar/object text
|
|
50
|
+
// and constructed report objects retain their native insertion order.
|
|
51
|
+
export const auditJsonText = stringifyNativeJson;
|
|
52
|
+
function imageCoverage(source) {
|
|
53
|
+
const type = stringField(source, 'type');
|
|
54
|
+
if (type === 'base64') return stringField(source, 'media_type') && stringField(source, 'data') ? 'normalized' : 'dropped';
|
|
55
|
+
return type === 'url' && stringField(source, 'url') ? 'normalized' : 'dropped';
|
|
56
|
+
}
|
|
57
|
+
function auditBlocks(blocks, rawPayload, report) {
|
|
58
|
+
const raw = field(rawPayload, 'content');
|
|
59
|
+
for (const [at, block] of blocks.entries()) {
|
|
60
|
+
const tag = block.tag ?? stringField(Array.isArray(raw) ? raw[at] : null, 'type') ?? '<no-type>';
|
|
61
|
+
const coverage = block.tag == null ? 'unmodeled' : block.tag === 'image' ? imageCoverage(field(block.value, 'source'))
|
|
62
|
+
: ['thinking', 'redacted_thinking'].includes(block.tag) ? 'dropped' : 'normalized';
|
|
63
|
+
report.block(tag, coverage);
|
|
64
|
+
const nested = field(block.value, 'content');
|
|
65
|
+
if (block.tag === 'tool_result' && Array.isArray(nested)) for (const item of nested) {
|
|
66
|
+
if (stringField(item, 'type') === 'image') report.block('tool_result/image', imageCoverage(field(item, 'source')));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function claude(raw, report) {
|
|
71
|
+
const record = auditClaudeRecord(raw), {tag, extra} = record;
|
|
72
|
+
if (tag == null) { report.bump(`<line>/${stringField(raw, 'type') ?? '<no-type>'}`, 'unmodeled'); return; }
|
|
73
|
+
if (tag === 'user' || tag === 'assistant') {
|
|
74
|
+
report.bump(tag, 'normalized', extra);
|
|
75
|
+
if (record.sidechain) report.note('sidechain (subagent) lines — flattened, not separated');
|
|
76
|
+
if (typeof record.content === 'string') report.block('text', 'normalized');
|
|
77
|
+
else {
|
|
78
|
+
// This lane deliberately receives no raw block array, as the replaced
|
|
79
|
+
// reader did: unknown Claude block tags are tallied as <no-type>.
|
|
80
|
+
auditBlocks(record.content, null, report);
|
|
81
|
+
for (const block of record.content) if (block.tag === 'tool_use') report.tool(block.value.name);
|
|
82
|
+
}
|
|
83
|
+
} else if (tag === 'system') report.bump(`system/${record.subtype}`, ['scheduled_task_fire', 'local_command', 'away_summary'].includes(record.subtype) ? 'normalized' : 'dropped', extra);
|
|
84
|
+
else report.bump(tag, ['attachment', 'fork-context-ref'].includes(tag) ? 'normalized'
|
|
85
|
+
: ['permission-mode', 'mode', 'last-prompt', 'queue-operation', 'ai-title', 'pr-link', 'frame-link', 'agent-name', 'worktree-state'].includes(tag) ? 'dropped' : 'unmodeled', extra);
|
|
86
|
+
}
|
|
87
|
+
function codex(raw, report) {
|
|
88
|
+
const record = auditCodexRecord(raw), {tag, extra} = record;
|
|
89
|
+
if (tag == null) { report.bump(`<line>/${stringField(raw, 'type') ?? '<no-type>'}`, 'unmodeled'); return; }
|
|
90
|
+
if (tag === 'response_item') {
|
|
91
|
+
const item = record.item, name = item.tag ?? stringField(raw.payload, 'type') ?? '<no-type>';
|
|
92
|
+
const coverage = item.tag == null ? 'unmodeled' : name === 'reasoning' ? 'retained' : 'normalized';
|
|
93
|
+
report.bump(`response_item/${name}${name === 'message' ? `[${item.value.role}]` : ''}`, coverage, item.extra);
|
|
94
|
+
if (name === 'message') auditBlocks(item.content, raw.payload, report);
|
|
95
|
+
if (name === 'function_call' || name === 'custom_tool_call' && item.value.name != null) report.tool(item.value.name);
|
|
96
|
+
} else if (tag === 'event_msg') {
|
|
97
|
+
const sub = record.payload.type ?? '?';
|
|
98
|
+
report.bump(`event_msg/${sub}`, ['agent_message', 'thread_rolled_back', 'thread_goal_updated', 'entered_review_mode', 'exited_review_mode'].includes(sub) ? 'retained' : 'dropped', extra);
|
|
99
|
+
} else report.bump(tag, tag === 'compacted' ? 'normalized' : 'retained', extra);
|
|
100
|
+
}
|
|
101
|
+
function pi(raw, report) {
|
|
102
|
+
const type = stringField(raw, 'type');
|
|
103
|
+
if (type === 'message') {
|
|
104
|
+
const message = field(raw, 'message'), role = stringField(message, 'role'), content = field(message, 'content');
|
|
105
|
+
const unknownImage = ['user', 'toolResult', 'custom'].includes(role) && Array.isArray(content)
|
|
106
|
+
&& content.some(item => stringField(item, 'type') === 'image' && piImageShape(item) == null);
|
|
107
|
+
report.bump(unknownImage ? 'message/UnknownImageShape' : ['user', 'assistant', 'toolResult', 'bashExecution', 'custom'].includes(role) ? `message/${role}` : `message/UnknownRole:${role ?? '<none>'}`,
|
|
108
|
+
unknownImage || !['user', 'assistant', 'toolResult', 'bashExecution', 'custom'].includes(role) ? 'unmodeled' : 'normalized');
|
|
109
|
+
} else if (['session', 'custom_message', 'compaction', 'branch_summary', 'model_change', 'session_info'].includes(type)) report.bump(type, 'normalized');
|
|
110
|
+
else if (['thinking_level_change', 'custom', 'label'].includes(type)) report.bump(type, 'dropped');
|
|
111
|
+
else report.bump(`<line>/${type ?? '<no-type>'}`, 'unmodeled');
|
|
112
|
+
}
|
|
113
|
+
function gemini(raw, report) {
|
|
114
|
+
const type = stringField(raw, 'type');
|
|
115
|
+
if (type == null) {
|
|
116
|
+
const key = stringField(raw, 'sessionId') != null ? 'session_header' : field(raw, '$set') !== undefined ? 'metadata_update' : '<line>/<no-type>';
|
|
117
|
+
report.bump(key, key.startsWith('<line>') ? 'unmodeled' : 'retained'); return;
|
|
118
|
+
}
|
|
119
|
+
if (type === 'user' || type === 'gemini') {
|
|
120
|
+
report.bump(type, 'normalized');
|
|
121
|
+
const content = field(raw, 'content');
|
|
122
|
+
if (Array.isArray(content)) for (const part of content) {
|
|
123
|
+
if (field(part, 'text') !== undefined) report.bump('content/text', 'normalized');
|
|
124
|
+
else if (field(part, 'inlineData') !== undefined) report.bump('content/inlineData', 'normalized');
|
|
125
|
+
else if (field(part, 'functionCall') !== undefined || field(part, 'functionResponse') !== undefined) {
|
|
126
|
+
const key = field(part, 'functionCall') !== undefined ? 'functionCall' : 'functionResponse';
|
|
127
|
+
report.bump(`content/${key}`, 'normalized'); const name = stringField(field(part, key), 'name'); if (name != null) report.tool(name);
|
|
128
|
+
} else report.bump('content/unknown', 'unmodeled');
|
|
129
|
+
} else if (content !== null && typeof content !== 'string') report.bump('content/nonstandard', 'unmodeled');
|
|
130
|
+
if (field(raw, 'thoughts') !== undefined) report.bump('thoughts', 'retained');
|
|
131
|
+
} else if (type === 'info' || type === 'error') report.bump(type, 'retained');
|
|
132
|
+
else report.bump(`<line>/${type}`, 'unmodeled');
|
|
133
|
+
}
|
|
134
|
+
function grokContent(content, prefix, coverage, report) {
|
|
135
|
+
if (Array.isArray(content)) for (const item of content) {
|
|
136
|
+
const modeled = typeof item === 'string' || stringField(item, 'text') != null;
|
|
137
|
+
report.bump(`${prefix}/content/${stringField(item, 'type') ?? '<no-type>'}`, coverage === 'dropped' ? 'dropped' : modeled ? 'normalized' : 'unmodeled');
|
|
138
|
+
} else report.bump(`${prefix}/content${content === undefined ? ':<missing>' : ''}`, content === undefined && coverage !== 'dropped' ? 'unmodeled' : coverage);
|
|
139
|
+
}
|
|
140
|
+
function grok(raw, report) {
|
|
141
|
+
const type = stringField(raw, 'type'), content = field(raw, 'content');
|
|
142
|
+
if (type === 'system') report.bump('system', typeof content === 'string' ? 'retained' : 'unmodeled');
|
|
143
|
+
else if (type === 'user') {
|
|
144
|
+
const text = nativeTrim(typeof content === 'string' ? content : Array.isArray(content)
|
|
145
|
+
? content.map(item => stringField(item, 'text') ?? (typeof item === 'string' ? item : null)).filter(item => item != null).join('\n')
|
|
146
|
+
: content === undefined ? '' : auditJsonText(content));
|
|
147
|
+
let key = 'user', coverage = 'normalized';
|
|
148
|
+
if (stringField(raw, 'synthetic_reason') === 'supercode_system_event') key = 'user/system_event';
|
|
149
|
+
else if (text.startsWith('<user_info>')) { key = 'user/injected_context:user_info'; coverage = 'dropped'; }
|
|
150
|
+
else if (text.startsWith('<system-reminder>')) { key = 'user/injected_context:system-reminder'; coverage = 'dropped'; }
|
|
151
|
+
else if (text === '') { key = 'user/empty'; coverage = 'dropped'; }
|
|
152
|
+
else if (text.startsWith('<user_query>') && text.endsWith('</user_query>') && nativeTrim(text.slice(12, -13)) === '') { key = 'user/empty_query'; coverage = 'dropped'; }
|
|
153
|
+
report.bump(key, coverage); grokContent(content, key, coverage, report);
|
|
154
|
+
} else if (type === 'assistant') {
|
|
155
|
+
report.bump('assistant', content == null || typeof content === 'string' ? 'normalized' : 'unmodeled');
|
|
156
|
+
const calls = field(raw, 'tool_calls');
|
|
157
|
+
if (Array.isArray(calls)) for (const call of calls) {
|
|
158
|
+
const name = stringField(call, 'name'), modeled = name != null && stringField(call, 'id') != null;
|
|
159
|
+
report.bump(`assistant/tool_call${modeled ? '' : ':invalid'}`, modeled ? 'normalized' : 'unmodeled'); if (name != null) report.tool(name);
|
|
160
|
+
} else if (calls !== undefined) report.bump('assistant/tool_calls:non-array', 'unmodeled');
|
|
161
|
+
} else if (type === 'tool_result') {
|
|
162
|
+
report.bump(type, stringField(raw, 'tool_call_id') != null ? 'normalized' : 'unmodeled'); grokContent(content, type, 'normalized', report);
|
|
163
|
+
} else if (type === 'reasoning' || type === 'backend_tool_call') report.bump(type, 'dropped');
|
|
164
|
+
else report.bump(`<line>/${type ?? '<no-type>'}`, 'unmodeled');
|
|
165
|
+
}
|
|
166
|
+
const partTypes = new Set(['text', 'reasoning', 'tool', 'file', 'step-start', 'step-finish', 'snapshot', 'patch', 'agent', 'subtask', 'retry', 'compaction']);
|
|
167
|
+
function opencode(raw, report) {
|
|
168
|
+
const key = field(raw, 'key'), value = field(raw, 'value');
|
|
169
|
+
if (!Array.isArray(key)) { report.bump('<line>/<no-key>', 'unmodeled'); return; }
|
|
170
|
+
const kind = typeof key[0] === 'string' ? key[0] : '<no-kind>';
|
|
171
|
+
if (kind === 'message') {
|
|
172
|
+
const role = stringField(value, 'role'); report.bump(['user', 'assistant'].includes(role) ? `message/${role}` : `message/UnknownRole:${role ?? '<none>'}`, ['user', 'assistant'].includes(role) ? 'normalized' : 'unmodeled');
|
|
173
|
+
} else if (kind === 'part') {
|
|
174
|
+
const type = stringField(value, 'type');
|
|
175
|
+
if (!partTypes.has(type)) report.bump(`part/UnknownType:${type ?? '<none>'}`, 'unmodeled');
|
|
176
|
+
else if (type === 'tool') {
|
|
177
|
+
const name = stringField(value, 'tool'); if (name != null) report.tool(name);
|
|
178
|
+
const status = stringField(field(value, 'state'), 'status'), known = ['pending', 'running', 'completed', 'error'].includes(status);
|
|
179
|
+
report.bump(known ? `part/tool/${status}` : `part/tool/UnknownStatus:${status ?? '<none>'}`, known ? 'normalized' : 'unmodeled');
|
|
180
|
+
} else if (type === 'text') report.bump(`part/text${field(value, 'ignored') === true ? ':ignored' : ''}`, field(value, 'ignored') === true ? 'dropped' : 'normalized');
|
|
181
|
+
else if (type === 'file') { const modeled = opencodeImagePart(value) != null; report.bump(`part/file${modeled ? '' : ':residue'}`, modeled ? 'normalized' : 'dropped'); }
|
|
182
|
+
else report.bump(`part/${type}`, type === 'compaction' ? 'normalized' : 'dropped');
|
|
183
|
+
} else if (['session', 'session_diff', 'todo'].includes(kind)) report.bump(kind, 'normalized');
|
|
184
|
+
else report.bump(`<line>/${kind}`, 'unmodeled');
|
|
185
|
+
}
|
|
186
|
+
const classifiers = {'claude-code':claude, codex, pi, opencode, grok, gemini};
|
|
187
|
+
export function auditCorpusLine(line, report) {
|
|
188
|
+
let raw;
|
|
189
|
+
try { raw = parseNativeJson(line); classifiers[report.corpus](raw, report); }
|
|
190
|
+
catch (error) { if (!(error instanceof SyntaxError || error instanceof TypeError)) throw error; report.parse_errors = next(report.parse_errors); }
|
|
191
|
+
return raw;
|
|
192
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {nativeInteger, nativeObjectKeys} from './native-json.mjs';
|
|
2
|
+
|
|
3
|
+
const scalars = text => [...text];
|
|
4
|
+
const pad = (text, size, left = false) => { const blank = ' '.repeat(Math.max(0, size - scalars(text).length)); return left ? blank + text : text + blank; };
|
|
5
|
+
const truncate = (text, size) => scalars(text).length <= size ? text : `${scalars(text).slice(0, size - 1).join('')}…`;
|
|
6
|
+
const thousands = value => String(value).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
7
|
+
const count = (value, key = 'count') => nativeInteger(value, key) ?? 0n;
|
|
8
|
+
const descending = (a, b) => a > b ? -1 : a < b ? 1 : 0;
|
|
9
|
+
/** Compact terminal rendering retains the native caps, stable count order,
|
|
10
|
+
* per-instance block buckets and gap summary. Notes remain in the JSON view. */
|
|
11
|
+
export function formatCorpusAudit(report, {paint = (_role, text) => text, bold = text => text} = {}) {
|
|
12
|
+
const section = title => `${paint('ACCENT', '▌')} ${bold(title)}`;
|
|
13
|
+
const rule = paint('DIM', '─'.repeat(60));
|
|
14
|
+
const kv = (key, value) => ` ${paint('MUTED', pad(key, 12))} ${value}`;
|
|
15
|
+
const glyph = coverage => paint(coverage === 'normalized' ? 'OK' : coverage === 'retained' ? 'TEAL' : coverage === 'dropped' ? 'DIM' : 'ERR', coverage === 'normalized' ? '✓' : coverage === 'retained' ? '◆' : coverage === 'dropped' ? '◦' : '✗');
|
|
16
|
+
const row = (label, value, extra = '') => {
|
|
17
|
+
const name = truncate(label, 38), number = thousands(count(value));
|
|
18
|
+
return ` ${glyph(value.coverage)} ${paint('MUTED', name)} ${paint('DIM', '.'.repeat(Math.max(0, 40 - scalars(name).length)))} ${' '.repeat(Math.max(0, 9 - scalars(number).length))}${bold(number)}${extra}\n`;
|
|
19
|
+
};
|
|
20
|
+
const entries = object => nativeObjectKeys(object).map(key => [key, object[key]]);
|
|
21
|
+
const sort = rows => rows.sort(([, a], [, b]) => descending(count(a), count(b)));
|
|
22
|
+
let text = `\n${section(`coverage audit · ${report.corpus}`)}\n${rule}\n\n`;
|
|
23
|
+
text += `${kv('files', bold(String(count(report, 'files'))))}\n${kv('lines', bold(thousands(count(report, 'lines'))))}\n${kv('parse errors', paint(count(report, 'parse_errors') === 0n ? 'OK' : 'ERR', String(count(report, 'parse_errors'))))}\n${rule}\n\n`;
|
|
24
|
+
const records = entries(report.records), blocks = entries(report.blocks).flatMap(([tag, rows]) => rows.map(value => [tag, value]));
|
|
25
|
+
text += `${section(`Records (${records.length} types)`)}\n`;
|
|
26
|
+
for (const [key, value] of sort(records).slice(0, 12)) text += row(key, value, value.unmodeled_fields.length ? paint('DIM', ` +${value.unmodeled_fields.length}`) : '');
|
|
27
|
+
if (records.length > 12) text += ` ${paint('DIM', ' ')} ${paint('DIM', `… ${records.length - 12} more — use --json for the full report`)}\n`;
|
|
28
|
+
text += '\n';
|
|
29
|
+
if (blocks.length) { text += `${section(`Content blocks (${nativeObjectKeys(report.blocks).length} types)`)}\n`; for (const [key, value] of sort(blocks)) text += row(key, value); text += '\n'; }
|
|
30
|
+
const tools = entries(report.tools).sort(([a], [b]) => descending(count(report.tools, a), count(report.tools, b)));
|
|
31
|
+
if (tools.length) {
|
|
32
|
+
text += `${section('Tools observed')}\n`;
|
|
33
|
+
const max = count(report.tools, tools[0][0]) || 1n;
|
|
34
|
+
for (const [key] of tools.slice(0, 8)) {
|
|
35
|
+
const n = count(report.tools, key), bar = '█'.repeat(Math.max(1, Math.round(Number(n) / Number(max) * 28)));
|
|
36
|
+
text += ` ${paint('MUTED', pad(truncate(key, 16), 16))} ${paint('DIM', pad(thousands(n), 8, true))} ${paint('ACCENT', bar)}\n`;
|
|
37
|
+
}
|
|
38
|
+
text += '\n';
|
|
39
|
+
}
|
|
40
|
+
const gaps = [...entries(report.records).filter(([, value]) => value.coverage === 'unmodeled'), ...entries(report.blocks).flatMap(([tag, rows]) => rows.filter(value => value.coverage === 'unmodeled').map(value => [`content block \`${tag}\``, value]))];
|
|
41
|
+
if (!gaps.length) text += ` ${paint('OK', '✓')} ${paint('MUTED', 'every record type is modeled — no gaps')}\n`;
|
|
42
|
+
else {
|
|
43
|
+
text += `${section('Gaps (not yet modeled)')}\n`;
|
|
44
|
+
for (const [key, value] of sort(gaps)) text += ` ${paint('ERR', '✗')} ${paint('MUTED', pad(key, 40))} ${paint('DIM', `${thousands(count(value))} occurrence(s)`)}\n`;
|
|
45
|
+
}
|
|
46
|
+
return `${text}\n`;
|
|
47
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// The audit's typed envelope boundary. Unknown variants accept their opaque
|
|
2
|
+
// fields; modeled fields keep their required/default/nullable native meanings.
|
|
3
|
+
import {nativeDuplicateObjectKeys, nativeObjectKeys} from './native-json.mjs';
|
|
4
|
+
import {isObject} from './session-codec-utils.mjs';
|
|
5
|
+
|
|
6
|
+
const invalid = () => { throw new TypeError('record does not match the corpus schema'); };
|
|
7
|
+
const string = value => typeof value === 'string';
|
|
8
|
+
const nullableString = value => value === null || string(value);
|
|
9
|
+
const any = () => true;
|
|
10
|
+
const required = check => ({check, required:true});
|
|
11
|
+
const fields = (value, model) => {
|
|
12
|
+
if (!isObject(value)) invalid();
|
|
13
|
+
if (nativeDuplicateObjectKeys(value).some(key => Object.hasOwn(model, key))) invalid();
|
|
14
|
+
for (const [key, rule] of Object.entries(model)) {
|
|
15
|
+
if (!Object.hasOwn(value, key)) { if (rule.required) invalid(); }
|
|
16
|
+
else if (!(rule.check ?? rule)(value[key])) invalid();
|
|
17
|
+
}
|
|
18
|
+
return nativeObjectKeys(value).filter(key => !Object.hasOwn(model, key));
|
|
19
|
+
};
|
|
20
|
+
const tagged = value => { fields(value, {type:required(string)}); return value.type; };
|
|
21
|
+
const blockModels = {
|
|
22
|
+
text:{text:string}, thinking:{thinking:string}, redacted_thinking:{data:nullableString},
|
|
23
|
+
tool_use:{id:required(string), name:required(string), input:any},
|
|
24
|
+
tool_result:{tool_use_id:nullableString, content:any}, image:{source:any},
|
|
25
|
+
input_text:{text:string}, output_text:{text:string}, input_image:{}, fallback:{},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function auditBlock(value) {
|
|
29
|
+
const tag = tagged(value);
|
|
30
|
+
if (!Object.hasOwn(blockModels, tag)) return {tag:null, value};
|
|
31
|
+
fields(value, {type:required(string), ...blockModels[tag]});
|
|
32
|
+
return {tag, value};
|
|
33
|
+
}
|
|
34
|
+
function blocks(value) {
|
|
35
|
+
if (!Array.isArray(value)) invalid();
|
|
36
|
+
return value.map(auditBlock);
|
|
37
|
+
}
|
|
38
|
+
const claudeMarkers = new Set(['attachment', 'file-history-snapshot', 'file-history-delta', 'ai-title',
|
|
39
|
+
'permission-mode', 'mode', 'last-prompt', 'queue-operation', 'pr-link', 'frame-link', 'agent-name',
|
|
40
|
+
'started', 'result', 'worktree-state', 'fork-context-ref']);
|
|
41
|
+
const meta = {uuid:nullableString, parent_uuid:nullableString, is_sidechain:value => typeof value === 'boolean',
|
|
42
|
+
session_id:nullableString, cwd:nullableString, git_branch:nullableString};
|
|
43
|
+
|
|
44
|
+
export function auditClaudeRecord(value) {
|
|
45
|
+
const tag = tagged(value);
|
|
46
|
+
if (tag === 'user' || tag === 'assistant') {
|
|
47
|
+
const extra = fields(value, {type:required(string), message:required(isObject), ...meta});
|
|
48
|
+
fields(value.message, {role:nullableString, model:nullableString, content:any});
|
|
49
|
+
const content = Object.hasOwn(value.message, 'content') ? value.message.content : [];
|
|
50
|
+
return {tag, extra, content:typeof content === 'string' ? content : blocks(content), sidechain:value.is_sidechain === true};
|
|
51
|
+
}
|
|
52
|
+
if (tag === 'system') return {tag, extra:fields(value, {type:required(string), subtype:nullableString}), subtype:value.subtype ?? '?'};
|
|
53
|
+
if (claudeMarkers.has(tag)) return {tag, extra:fields(value, {type:required(string)})};
|
|
54
|
+
return {tag:null, extra:[]};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const itemModels = {
|
|
58
|
+
message:{role:required(string), content:any},
|
|
59
|
+
function_call:{name:required(string), arguments:string, call_id:required(string)},
|
|
60
|
+
function_call_output:{call_id:required(string), output:any},
|
|
61
|
+
custom_tool_call:{name:nullableString, input:any, call_id:nullableString},
|
|
62
|
+
custom_tool_call_output:{call_id:nullableString, output:any},
|
|
63
|
+
reasoning:{}, web_search_call:{}, tool_search_call:{}, tool_search_output:{}, image_generation_call:{},
|
|
64
|
+
};
|
|
65
|
+
function auditCodexItem(value) {
|
|
66
|
+
const tag = tagged(value);
|
|
67
|
+
if (!Object.hasOwn(itemModels, tag)) return {tag:null, extra:[], value};
|
|
68
|
+
const extra = fields(value, {type:required(string), ...itemModels[tag]});
|
|
69
|
+
return {tag, extra, value, ...(tag === 'message' ? {content:blocks(Object.hasOwn(value, 'content') ? value.content : [])} : {})};
|
|
70
|
+
}
|
|
71
|
+
const payloadModels = {
|
|
72
|
+
session_meta:{id:nullableString, cwd:nullableString, base_instructions:any, model_provider:nullableString},
|
|
73
|
+
turn_context:{model:nullableString, cwd:nullableString}, event_msg:{type:nullableString},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export function auditCodexRecord(value) {
|
|
77
|
+
fields(value, {timestamp:nullableString});
|
|
78
|
+
const tag = tagged(value);
|
|
79
|
+
if (tag === 'response_item') {
|
|
80
|
+
fields(value, {timestamp:nullableString, type:required(string), payload:required(isObject)});
|
|
81
|
+
return {tag, item:auditCodexItem(value.payload)};
|
|
82
|
+
}
|
|
83
|
+
if (Object.hasOwn(payloadModels, tag)) {
|
|
84
|
+
fields(value, {timestamp:nullableString, type:required(string), payload:required(isObject)});
|
|
85
|
+
return {tag, extra:fields(value.payload, payloadModels[tag]), payload:value.payload};
|
|
86
|
+
}
|
|
87
|
+
if (tag === 'compacted') { fields(value, {timestamp:nullableString, type:required(string), payload:any}); return {tag, extra:[]}; }
|
|
88
|
+
return {tag:null, extra:[]};
|
|
89
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type AuditFormat = 'claude-code' | 'codex' | 'opencode' | 'pi' | 'grok' | 'gemini' | 'goose';
|
|
2
|
+
export type AuditCoverage = 'normalized' | 'retained' | 'dropped' | 'unmodeled';
|
|
3
|
+
export interface AuditTally {coverage:AuditCoverage; count:number; unmodeled_fields:string[]}
|
|
4
|
+
export interface CorpusAuditReport {
|
|
5
|
+
corpus:AuditFormat; files:number; lines:number; parse_errors:number;
|
|
6
|
+
records:Record<string, AuditTally>;
|
|
7
|
+
blocks:Record<string, {coverage:AuditCoverage; count:number}[]>;
|
|
8
|
+
tools:Record<string, number>; notes:Record<string, number>;
|
|
9
|
+
}
|
|
10
|
+
export const AUDIT_FORMATS: readonly AuditFormat[];
|
|
11
|
+
export const AUDIT_COVERAGE: readonly AuditCoverage[];
|
|
12
|
+
/** Counts expose JS numbers; corpusAuditJson retains the exact native u64 wire values. */
|
|
13
|
+
export function auditCorpus(root:string, options:{format:AuditFormat; limit?:number | bigint | null}):Promise<CorpusAuditReport>;
|
|
14
|
+
export function corpusAuditJson(report:CorpusAuditReport):string;
|
|
15
|
+
export function formatCorpusAuditInventory(report:CorpusAuditReport):string;
|
|
16
|
+
export function formatCorpusAudit(report:CorpusAuditReport, style?:{paint?:(role:string, text:string) => string; bold?:(text:string) => string}):string;
|
package/corpus-audit.mjs
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import {readFile, readdir, stat} from 'node:fs/promises';
|
|
2
|
+
import {basename, extname, join} from 'node:path';
|
|
3
|
+
import {decodeSessionUtf8} from './session-file.mjs';
|
|
4
|
+
import {nativeTrim} from './text.mjs';
|
|
5
|
+
import {field} from './session-codec-utils.mjs';
|
|
6
|
+
import {nativeInteger, stringifyNativeJsonPretty} from './native-json.mjs';
|
|
7
|
+
import {readForeignStore} from './foreign-store.mjs';
|
|
8
|
+
import {readGooseSqlite} from './session-goose.mjs';
|
|
9
|
+
import {readOpenCodeCorpus} from './session-opencode-store.mjs';
|
|
10
|
+
import {CorpusAuditReport, auditCorpusLine, auditOrder} from './corpus-audit-records.mjs';
|
|
11
|
+
|
|
12
|
+
export {formatCorpusAudit} from './corpus-audit-render.mjs';
|
|
13
|
+
export {AUDIT_COVERAGE} from './corpus-audit-records.mjs';
|
|
14
|
+
export const AUDIT_FORMATS = Object.freeze(['claude-code', 'codex', 'opencode', 'pi', 'grok', 'gemini', 'goose']);
|
|
15
|
+
const info = async path => { try { return await stat(path); } catch { return null; } };
|
|
16
|
+
const entries = async path => { try { return await readdir(path, {withFileTypes:true}); } catch { return []; } };
|
|
17
|
+
const cap = (rows, limit) => limit == null || limit >= BigInt(rows.length) ? rows : rows.slice(0, Number(limit));
|
|
18
|
+
function nativeLimit(value) {
|
|
19
|
+
if (value == null) return null;
|
|
20
|
+
if (typeof value !== 'bigint' && !Number.isSafeInteger(value)) throw new TypeError('audit limit must be an exact nonnegative integer');
|
|
21
|
+
const whole = BigInt(value);
|
|
22
|
+
if (whole < 0n || whole > (1n << 64n) - 1n) throw new RangeError('audit limit is outside native usize');
|
|
23
|
+
return whole;
|
|
24
|
+
}
|
|
25
|
+
async function jsonlFiles(root) {
|
|
26
|
+
const out = [], pending = [root];
|
|
27
|
+
// Native walks ordinary directories without following child symlinks;
|
|
28
|
+
// root symlinks are resolved by the root's own filesystem lookup.
|
|
29
|
+
if (extname(root) === '.jsonl' && await info(root)) out.push(root);
|
|
30
|
+
if (!(await info(root))?.isDirectory()) return out;
|
|
31
|
+
while (pending.length) {
|
|
32
|
+
for (const entry of await entries(pending.pop())) {
|
|
33
|
+
const path = join(entry.parentPath ?? entry.path, entry.name);
|
|
34
|
+
if (extname(path) === '.jsonl') out.push(path);
|
|
35
|
+
if (entry.isDirectory()) pending.push(path);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return out.sort(auditOrder);
|
|
39
|
+
}
|
|
40
|
+
async function openCodeDatabase(root) {
|
|
41
|
+
const override = process.env.OPENCODE_DB;
|
|
42
|
+
if (override !== undefined && (await info(override))?.isFile()) return override;
|
|
43
|
+
const candidates = (await entries(root)).filter(entry => entry.name.startsWith('opencode') && entry.name.endsWith('.db'))
|
|
44
|
+
.map(entry => join(root, entry.name)).sort(auditOrder);
|
|
45
|
+
return candidates.find(path => basename(path) === 'opencode.db') ?? candidates[0] ?? null;
|
|
46
|
+
}
|
|
47
|
+
async function goose(root, limit, report) {
|
|
48
|
+
// Native discovery quietly excludes a store it cannot read. A selected
|
|
49
|
+
// session that fails full normalization is instead named in report.notes.
|
|
50
|
+
const direct = join(root, 'sessions.db');
|
|
51
|
+
const path = (await info(root))?.isFile() ? root : (await info(direct))?.isFile() ? direct : join(root, 'sessions', 'sessions.db');
|
|
52
|
+
let rows;
|
|
53
|
+
try { rows = (await readForeignStore('goose', path)).result.rows; }
|
|
54
|
+
catch { return; }
|
|
55
|
+
rows.sort((a, b) => {
|
|
56
|
+
const left = a.updated_at_ms, right = b.updated_at_ms;
|
|
57
|
+
return left === right ? auditOrder(a.locator.session_id, b.locator.session_id) : left == null ? 1 : right == null ? -1 : right - left;
|
|
58
|
+
});
|
|
59
|
+
for (const row of cap(rows, limit)) {
|
|
60
|
+
const id = row.locator.session_id;
|
|
61
|
+
let session;
|
|
62
|
+
try { session = (await readGooseSqlite(path, id)).result; }
|
|
63
|
+
catch (error) { report.note(`Goose session ${id} failed to load: ${error.message}`); continue; }
|
|
64
|
+
report.files++;
|
|
65
|
+
for (const message of session.messages) {
|
|
66
|
+
report.lines++; report.bump(`message/${message.role.toLowerCase()}`, 'normalized');
|
|
67
|
+
for (const call of message.tool_calls ?? []) report.tool(call.function.name);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Read-only corpus audit. JSONL counts selected paths before reading them;
|
|
73
|
+
* store formats count the native selected sessions, not database files. */
|
|
74
|
+
export async function auditCorpus(root, {format, limit = null} = {}) {
|
|
75
|
+
if (typeof root !== 'string') throw new TypeError('audit root must be a path string');
|
|
76
|
+
if (!AUDIT_FORMATS.includes(format)) throw new TypeError(`unknown corpus format ${format}`);
|
|
77
|
+
limit = nativeLimit(limit);
|
|
78
|
+
const report = new CorpusAuditReport(format);
|
|
79
|
+
if (format === 'goose') await goose(root, limit, report);
|
|
80
|
+
else {
|
|
81
|
+
const database = format === 'opencode' ? await openCodeDatabase(root) : null;
|
|
82
|
+
if (database != null) {
|
|
83
|
+
try {
|
|
84
|
+
const lines = await readOpenCodeCorpus(database, limit);
|
|
85
|
+
for (const source of lines) {
|
|
86
|
+
const line = nativeTrim(source); if (line === '') continue;
|
|
87
|
+
report.lines++; const raw = auditCorpusLine(line, report);
|
|
88
|
+
if (field(raw, 'key')?.[0] === 'session') report.files++;
|
|
89
|
+
}
|
|
90
|
+
} catch (error) { report.note(`opencode_sqlite_error: ${error.message}`); }
|
|
91
|
+
} else {
|
|
92
|
+
let files = await jsonlFiles(root);
|
|
93
|
+
if (format === 'grok') files = files.filter(path => basename(path) === 'chat_history.jsonl');
|
|
94
|
+
for (const path of cap(files, limit)) {
|
|
95
|
+
report.files++;
|
|
96
|
+
let source;
|
|
97
|
+
try { source = decodeSessionUtf8(await readFile(path)); } catch { continue; }
|
|
98
|
+
for (const line of source.split('\n').map(nativeTrim)) {
|
|
99
|
+
if (line === '') continue;
|
|
100
|
+
report.lines++; auditCorpusLine(line, report);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return report.toJSON();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Native-compatible report bytes, including exact integer counters and LF. */
|
|
109
|
+
export function corpusAuditJson(report) { return `${stringifyNativeJsonPretty(report)}\n`; }
|
|
110
|
+
|
|
111
|
+
/** The library inventory view, separate from the CLI's compact terminal view. */
|
|
112
|
+
export function formatCorpusAuditInventory(report) {
|
|
113
|
+
const symbols = {normalized:'✅ normalized', retained:'◆ retained ', dropped:'➖ dropped ', unmodeled:'❌ UNMODELED '};
|
|
114
|
+
const count = (row, key = 'count') => nativeInteger(row, key) ?? 0n;
|
|
115
|
+
const sorted = object => Object.entries(object).sort(([a], [b]) => auditOrder(a, b));
|
|
116
|
+
const pad = (text, width) => text + ' '.repeat(Math.max(0, width - [...text].length));
|
|
117
|
+
let text = `# Coverage audit: ${report.corpus ?? '?'}\nfiles=${count(report, 'files')} lines=${count(report, 'lines')} parse_errors=${count(report, 'parse_errors')}\n\n`;
|
|
118
|
+
text += '## Records (discriminant → coverage, count, unmodeled fields)\n';
|
|
119
|
+
for (const [key, row] of sorted(report.records)) text += ` ${symbols[row.coverage]} ${pad(key, 40)} ${String(count(row)).padStart(9)}${row.unmodeled_fields.length ? ` unmodeled fields: ${row.unmodeled_fields.join(', ')}` : ''}\n`;
|
|
120
|
+
const blocks = sorted(report.blocks).flatMap(([key, rows]) => rows.map(row => [key, row]));
|
|
121
|
+
if (blocks.length) { text += '\n## Content blocks\n'; for (const [key, row] of blocks) text += ` ${symbols[row.coverage]} ${pad(key, 28)} ${String(count(row)).padStart(9)}\n`; }
|
|
122
|
+
if (Object.keys(report.notes).length) { text += '\n## Structural notes\n'; for (const [key] of sorted(report.notes)) text += ` ${key}: ${count(report.notes, key)}\n`; }
|
|
123
|
+
if (Object.keys(report.tools).length) {
|
|
124
|
+
text += '\n## Tools observed (top 30 by frequency)\n';
|
|
125
|
+
const rows = sorted(report.tools).sort(([a], [b]) => count(report.tools, a) > count(report.tools, b) ? -1 : count(report.tools, a) < count(report.tools, b) ? 1 : 0);
|
|
126
|
+
for (const [key] of rows.slice(0, 30)) text += ` ${String(count(report.tools, key)).padStart(9)} ${key}\n`;
|
|
127
|
+
}
|
|
128
|
+
text += '\n## Summary of gaps (UNMODELED or dropped, non-UI)\n';
|
|
129
|
+
for (const [key, row] of sorted(report.records)) if (row.coverage === 'unmodeled') text += ` ❌ ${key} (${count(row)} occurrences) — not modeled\n`;
|
|
130
|
+
for (const [key, row] of blocks) if (row.coverage === 'unmodeled') text += ` ❌ content block \`${key}\` (${count(row)}) — not modeled\n`;
|
|
131
|
+
return text;
|
|
132
|
+
}
|
package/discovery.mjs
CHANGED
|
@@ -10,7 +10,7 @@ export async function enrichDescriptor(query, descriptor, { codexTopics, metrics
|
|
|
10
10
|
const row = { ...descriptor };
|
|
11
11
|
const take = async (read) => {
|
|
12
12
|
try { return await read(); }
|
|
13
|
-
catch (error) { previewFailure?.(row.locator, error); return []; }
|
|
13
|
+
catch (error) { if (metrics) metrics.preview_failures = (metrics.preview_failures ?? 0) + 1; previewFailure?.(row.locator, error); return []; }
|
|
14
14
|
};
|
|
15
15
|
if (query.include_topic_candidates) {
|
|
16
16
|
const history = row.locator.harness === 'codex' ? codexTopics?.get(row.locator.session_id) : null;
|
|
@@ -30,8 +30,19 @@ export async function enrichDescriptor(query, descriptor, { codexTopics, metrics
|
|
|
30
30
|
export const readsPreviews = (query) => query.include_latest_candidates !== false || Boolean(query.include_topic_candidates);
|
|
31
31
|
|
|
32
32
|
export async function enrichIndexPage(query, rows, options) {
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
// A page used to await every open/read/stat/close before starting the next row. Keep a small number of
|
|
34
|
+
// independent reads in flight, preserve row order and retain every fingerprint/failure check. Never fan out
|
|
35
|
+
// with Promise.all(rows): a whole-index request can contain thousands of transcripts and expanded tails.
|
|
36
|
+
const result = new Array(rows.length);
|
|
37
|
+
let next = 0;
|
|
38
|
+
const read = async () => {
|
|
39
|
+
while (next < rows.length) {
|
|
40
|
+
const at = next++;
|
|
41
|
+
result[at] = await enrichDescriptor(query, rows[at], options);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const finished = await Promise.allSettled(Array.from({ length: Math.min(4, rows.length) }, read));
|
|
45
|
+
for (const worker of finished) if (worker.status === 'rejected') throw worker.reason;
|
|
35
46
|
return result;
|
|
36
47
|
}
|
|
37
48
|
|
|
@@ -46,9 +57,10 @@ export function namesWholeSessionId(query) {
|
|
|
46
57
|
* title supplier chooses one id by source mtime, before applying the other
|
|
47
58
|
* query filters, as native claude_session_named does. Neither is caller proof. */
|
|
48
59
|
export async function discoverFromIndex(query, descriptors, options = {}) {
|
|
60
|
+
const step = options.step ?? ((_name, work) => work());
|
|
49
61
|
const raw = [...descriptors];
|
|
50
|
-
if (query.search_previews) return previewPage(query, raw, options);
|
|
51
|
-
const page = projectMetadataPage(query, raw, options);
|
|
62
|
+
if (query.search_previews) return step('preview search', () => previewPage(query, raw, options));
|
|
63
|
+
const page = await step('metadata projection', () => projectMetadataPage(query, raw, options));
|
|
52
64
|
const name = queryName(query);
|
|
53
65
|
if (query.cursor == null && name) {
|
|
54
66
|
const selected = query.harnesses;
|
|
@@ -78,7 +90,7 @@ export async function discoverFromIndex(query, descriptors, options = {}) {
|
|
|
78
90
|
}
|
|
79
91
|
// Keep the native receipt from projection: its counts precede the exact-name
|
|
80
92
|
// post-filter and late-name fallback. Do not silently rewrite that contract.
|
|
81
|
-
page.sessions = await enrichIndexPage(query, page.sessions, options);
|
|
93
|
+
page.sessions = await step('preview reads', () => enrichIndexPage(query, page.sessions, options));
|
|
82
94
|
return page;
|
|
83
95
|
}
|
|
84
96
|
|