simt-dev 0.2.2 → 0.3.1
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/CHANGELOG.md +100 -0
- package/README.md +129 -75
- package/bin/simt-dev.mjs +21 -2
- package/docs/decisions/ADR-073-a-kernel-is-a-smaller-projection-the-repository-asks-for.md +1 -1
- package/docs/decisions/ADR-074-a-rule-and-a-decision-are-readable-by-uri.md +99 -0
- package/docs/decisions/ADR-075-nothing-leaves-the-machine.md +56 -0
- package/docs/decisions/ADR-076-a-change-to-what-agents-read-is-measured.md +70 -0
- package/docs/decisions/ADR-077-the-command-card-leads-the-region-and-reads-committed-evidence.md +91 -0
- package/docs/decisions/ADR-078-a-line-the-repository-already-proves-does-not-ship.md +117 -0
- package/docs/decisions/ADR-079-the-rules-are-enforced-at-the-tool-call-from-one-policy-table.md +162 -0
- package/docs/decisions/ADR-080-the-tools-own-names-are-not-repositories-and-neither-reader-decides-by-shape.md +99 -0
- package/docs/decisions/ADR-081-a-tracker-is-read-through-the-repositorys-template.md +36 -0
- package/docs/decisions/ADR-082-the-tracker-override-is-a-manifest-key.md +35 -0
- package/docs/decisions/ADR-083-a-budget-is-met-by-cutting-the-kernel-and-hard-is-the-floor.md +153 -0
- package/docs/decisions/ADR-084-a-workspace-carries-its-repeated-rules-once.md +100 -0
- package/docs/decisions/ADR-085-the-region-ends-with-the-names-it-kept-out.md +72 -0
- package/docs/decisions/ADR-086-the-stable-prefix-comes-first-and-the-lock-records-both-digests.md +69 -0
- package/docs/decisions/ADR-087-a-run-is-evidence-for-one-tree.md +67 -0
- package/docs/decisions/ADR-088-a-marked-line-is-enforced-by-a-checker.md +59 -0
- package/docs/decisions/ADR-089-the-local-index-is-a-cache-of-git-blobs.md +52 -0
- package/docs/decisions/ADR-090-an-optional-adapter-is-off-until-named.md +55 -0
- package/docs/decisions/ADR-091-the-map-ranks-files-a-task-mentions.md +60 -0
- package/docs/decisions/ADR-092-tests-are-imports-conventions-and-coverage.md +60 -0
- package/docs/decisions/ADR-093-why-names-what-governs-a-file.md +58 -0
- package/docs/decisions/ADR-094-a-change-runs-the-tests-that-import-it.md +44 -0
- package/docs/decisions/ADR-095-gate-output-keeps-the-failures.md +31 -0
- package/docs/decisions/ADR-096-impact-names-who-a-change-reaches.md +38 -0
- package/package.json +2 -1
- package/rules/core.md +1 -1
- package/src/agenthooks/decide.mjs +123 -0
- package/src/agenthooks/policy.mjs +78 -0
- package/src/agenthooks/render.mjs +240 -0
- package/src/argv.mjs +54 -1
- package/src/bench/ablate.mjs +630 -0
- package/src/bench/adapters.json +149 -0
- package/src/bench/grade.mjs +229 -0
- package/src/bench/mine.mjs +811 -0
- package/src/bench/probes.mjs +1350 -0
- package/src/bench/report.mjs +899 -0
- package/src/bench/run.mjs +1002 -0
- package/src/ci/workflow.mjs +11 -1
- package/src/commands/agenthook.mjs +282 -0
- package/src/commands/bench.mjs +371 -0
- package/src/commands/evidence.mjs +96 -0
- package/src/commands/gate.mjs +165 -0
- package/src/commands/harness.mjs +156 -5
- package/src/commands/help.mjs +89 -1
- package/src/commands/hooks.mjs +45 -5
- package/src/commands/impact.mjs +205 -0
- package/src/commands/index.mjs +60 -1
- package/src/commands/map.mjs +91 -0
- package/src/commands/plan.mjs +99 -0
- package/src/commands/repoindex.mjs +52 -0
- package/src/commands/shared.mjs +398 -0
- package/src/commands/sync.mjs +72 -3
- package/src/commands/tests.mjs +192 -0
- package/src/commands/tokens.mjs +61 -1
- package/src/commands/why.mjs +177 -0
- package/src/decisions/index.mjs +23 -5
- package/src/graph/spec.mjs +1 -1
- package/src/hooks/dispatcher.mjs +15 -0
- package/src/hooks/shim.mjs +31 -0
- package/src/index/compact.mjs +228 -0
- package/src/index/extract.mjs +331 -0
- package/src/index/gate.mjs +101 -0
- package/src/index/govern.mjs +222 -0
- package/src/index/impact.mjs +186 -0
- package/src/index/map.mjs +393 -0
- package/src/index/persist.mjs +172 -0
- package/src/index/store.mjs +301 -0
- package/src/index/tests.mjs +294 -0
- package/src/kernel/budget.mjs +100 -0
- package/src/kernel/card.mjs +254 -0
- package/src/kernel/checks.mjs +378 -0
- package/src/kernel/inferability.mjs +188 -0
- package/src/ledger/index.mjs +112 -0
- package/src/mcp/resources.mjs +157 -0
- package/src/mcp/server.mjs +37 -6
- package/src/mcp/tools.mjs +84 -0
- package/src/measure/context.mjs +184 -11
- package/src/measure/loaderView.mjs +435 -0
- package/src/measure/session.mjs +267 -0
- package/src/plan/index.mjs +650 -0
- package/src/plan/template.mjs +331 -0
- package/src/projection/disclosure.mjs +92 -0
- package/src/projection/loaders.mjs +689 -0
- package/src/projection/lock.mjs +93 -0
- package/src/projection/manifest.mjs +78 -3
- package/src/projection/markers.mjs +56 -17
- package/src/projection/project.mjs +24 -17
- package/src/projection/rules.mjs +452 -43
- package/src/projection/shared.mjs +428 -0
- package/src/projection/surfaces.mjs +22 -8
- package/src/projection/targets.mjs +2 -1
- package/src/toolwords.mjs +71 -0
- package/templates/plan/tracker.json +36 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// Local session transcript attribution for `simt tokens --session <path>` (R06, ADR-075).
|
|
2
|
+
//
|
|
3
|
+
// Splits a recorded Claude Code or Codex JSONL/JSON session transcript into `rules`,
|
|
4
|
+
// `fileReads` and `toolOutput` using only the local file on disk. Remote URLs are
|
|
5
|
+
// refused before any socket or filesystem call so session contents never leave the
|
|
6
|
+
// machine.
|
|
7
|
+
|
|
8
|
+
import { readFile } from 'node:fs/promises';
|
|
9
|
+
import { resolve } from 'node:path';
|
|
10
|
+
|
|
11
|
+
import { tokens } from './context.mjs';
|
|
12
|
+
|
|
13
|
+
const RULE_PATH_RE =
|
|
14
|
+
/(?:^|\/)(?:AGENTS(?:\.override)?\.md|CLAUDE(?:\.local)?\.md|GEMINI\.md|SKILL\.md|\.cursor\/rules\/|\.claude\/rules\/|\.agents\/rules\/|\.github\/(?:copilot-instructions\.md|instructions\/)|\.simt-dev\/profiles\/)/;
|
|
15
|
+
|
|
16
|
+
const FILE_READ_TOOLS = new Set(['Read', 'read_file', 'NotebookRead', 'view_file', 'read']);
|
|
17
|
+
|
|
18
|
+
function bytes(text) {
|
|
19
|
+
return Buffer.byteLength(text);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isRulePath(path) {
|
|
23
|
+
return typeof path === 'string' && RULE_PATH_RE.test(path.replace(/\\/g, '/'));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function textOfContent(content) {
|
|
27
|
+
if (typeof content === 'string') return content;
|
|
28
|
+
if (Array.isArray(content)) {
|
|
29
|
+
return content
|
|
30
|
+
.map((item) => {
|
|
31
|
+
if (typeof item === 'string') return item;
|
|
32
|
+
if (typeof item?.text === 'string') return item.text;
|
|
33
|
+
if (typeof item?.content === 'string') return item.content;
|
|
34
|
+
return '';
|
|
35
|
+
})
|
|
36
|
+
.filter(Boolean)
|
|
37
|
+
.join('\n');
|
|
38
|
+
}
|
|
39
|
+
if (content && typeof content === 'object') {
|
|
40
|
+
if (typeof content.text === 'string') return content.text;
|
|
41
|
+
if (typeof content.output === 'string') return content.output;
|
|
42
|
+
if (typeof content.content === 'string') return textOfContent(content.content);
|
|
43
|
+
}
|
|
44
|
+
return '';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Split a local Claude Code or Codex session transcript into `rules`, `fileReads` and
|
|
49
|
+
* `toolOutput` (R06, ADR-075).
|
|
50
|
+
*/
|
|
51
|
+
export async function measureSession(transcriptPath) {
|
|
52
|
+
if (typeof transcriptPath !== 'string' || transcriptPath.trim() === '') {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
error: '--session requires a local transcript path',
|
|
56
|
+
remedy: 'pass a local JSONL or JSON transcript path: simt tokens --session <path>',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(transcriptPath.trim())) {
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
error: `${transcriptPath}: remote URLs are refused; session transcripts are read locally only (ADR-075)`,
|
|
63
|
+
remedy: 'pass a local file path on this machine: simt tokens --session ./session.jsonl',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const resolvedPath = resolve(transcriptPath);
|
|
68
|
+
let raw;
|
|
69
|
+
try {
|
|
70
|
+
raw = await readFile(resolvedPath, 'utf8');
|
|
71
|
+
} catch (err) {
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
error: `${resolvedPath}: cannot read session transcript (${err.code ?? err.message})`,
|
|
75
|
+
remedy: 'pass a readable local JSONL or JSON transcript file to --session <path>',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let records = [];
|
|
80
|
+
const trimmed = raw.trim();
|
|
81
|
+
if (trimmed.length > 0) {
|
|
82
|
+
try {
|
|
83
|
+
if (trimmed.startsWith('[')) {
|
|
84
|
+
records = JSON.parse(trimmed);
|
|
85
|
+
} else {
|
|
86
|
+
records = trimmed
|
|
87
|
+
.split('\n')
|
|
88
|
+
.map((line) => line.trim())
|
|
89
|
+
.filter(Boolean)
|
|
90
|
+
.map((line) => JSON.parse(line));
|
|
91
|
+
}
|
|
92
|
+
} catch (err) {
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
error: `${resolvedPath}: invalid JSON/JSONL session transcript (${err.message})`,
|
|
96
|
+
remedy: 'pass a valid Claude Code or Codex JSONL transcript file to --session <path>',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const callsById = new Map();
|
|
102
|
+
for (const rec of records) {
|
|
103
|
+
const msg = rec?.message ?? rec?.payload ?? rec;
|
|
104
|
+
const content = msg?.content;
|
|
105
|
+
if (Array.isArray(content)) {
|
|
106
|
+
for (const block of content) {
|
|
107
|
+
if (block?.type === 'tool_use' && block.id) {
|
|
108
|
+
const input = block.input ?? {};
|
|
109
|
+
const path = input.file_path ?? input.path ?? input.notebook_path ?? null;
|
|
110
|
+
callsById.set(block.id, { name: block.name, path });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if ((rec?.type === 'function_call' || msg?.type === 'function_call') && (rec.call_id ?? msg.call_id)) {
|
|
115
|
+
const id = rec.call_id ?? msg.call_id;
|
|
116
|
+
const name = rec.name ?? msg.name ?? '';
|
|
117
|
+
let args = {};
|
|
118
|
+
try {
|
|
119
|
+
args =
|
|
120
|
+
typeof (rec.arguments ?? msg.arguments) === 'string'
|
|
121
|
+
? JSON.parse(rec.arguments ?? msg.arguments)
|
|
122
|
+
: (rec.arguments ?? msg.arguments ?? {});
|
|
123
|
+
} catch {
|
|
124
|
+
args = {};
|
|
125
|
+
}
|
|
126
|
+
const path = args.file_path ?? args.path ?? args.file ?? null;
|
|
127
|
+
callsById.set(id, { name, path });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const buckets = {
|
|
132
|
+
rules: { bytes: 0, items: 0 },
|
|
133
|
+
fileReads: { bytes: 0, items: 0 },
|
|
134
|
+
toolOutput: { bytes: 0, items: 0 },
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const credit = (bucket, text) => {
|
|
138
|
+
const n = bytes(text);
|
|
139
|
+
if (n <= 0) return;
|
|
140
|
+
buckets[bucket].bytes += n;
|
|
141
|
+
buckets[bucket].items += 1;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
for (const rec of records) {
|
|
145
|
+
if (!rec || typeof rec !== 'object') continue;
|
|
146
|
+
|
|
147
|
+
const explicitKind = rec.category ?? rec.kind;
|
|
148
|
+
if (explicitKind === 'rules' || explicitKind === 'rule') {
|
|
149
|
+
credit('rules', textOfContent(rec.text ?? rec.content ?? rec.output ?? ''));
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (explicitKind === 'file_read' || explicitKind === 'fileRead') {
|
|
153
|
+
const target = rec.path ?? rec.file ?? null;
|
|
154
|
+
credit(isRulePath(target) ? 'rules' : 'fileReads', textOfContent(rec.text ?? rec.content ?? rec.output ?? ''));
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (explicitKind === 'tool_output' || explicitKind === 'toolOutput') {
|
|
158
|
+
credit('toolOutput', textOfContent(rec.text ?? rec.content ?? rec.output ?? ''));
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const msg = rec.message ?? rec.payload ?? rec;
|
|
163
|
+
const role = msg.role ?? rec.role;
|
|
164
|
+
const type = msg.type ?? rec.type;
|
|
165
|
+
|
|
166
|
+
if (
|
|
167
|
+
role === 'system' ||
|
|
168
|
+
role === 'developer' ||
|
|
169
|
+
type === 'system' ||
|
|
170
|
+
type === 'user_instructions' ||
|
|
171
|
+
type === 'instructions'
|
|
172
|
+
) {
|
|
173
|
+
credit('rules', textOfContent(msg.content ?? msg.text ?? msg.instructions ?? ''));
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (type === 'session_meta' && (msg.instructions || msg.base_instructions)) {
|
|
178
|
+
credit('rules', textOfContent(msg.instructions ?? msg.base_instructions));
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (type === 'function_call_output' || type === 'custom_tool_call_output' || type === 'local_shell_call_output') {
|
|
183
|
+
const call = callsById.get(msg.call_id ?? rec.call_id);
|
|
184
|
+
const outText = textOfContent(msg.output ?? rec.output ?? '');
|
|
185
|
+
if (call && isRulePath(call.path)) credit('rules', outText);
|
|
186
|
+
else if (call && FILE_READ_TOOLS.has(call.name)) credit('fileReads', outText);
|
|
187
|
+
else credit('toolOutput', outText);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (role === 'tool' || type === 'tool_result') {
|
|
192
|
+
const call = callsById.get(msg.tool_use_id ?? rec.tool_use_id ?? msg.call_id);
|
|
193
|
+
const target = msg.path ?? rec.path ?? call?.path ?? null;
|
|
194
|
+
const outText = textOfContent(msg.content ?? msg.output ?? '');
|
|
195
|
+
if (isRulePath(target)) credit('rules', outText);
|
|
196
|
+
else if ((call && FILE_READ_TOOLS.has(call.name)) || FILE_READ_TOOLS.has(msg.name ?? rec.name)) {
|
|
197
|
+
credit('fileReads', outText);
|
|
198
|
+
} else {
|
|
199
|
+
credit('toolOutput', outText);
|
|
200
|
+
}
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const content = msg.content;
|
|
205
|
+
if (typeof content === 'string') {
|
|
206
|
+
for (const m of content.matchAll(
|
|
207
|
+
/<(?:user_instructions|instructions|system-reminder)>([\s\S]*?)<\/(?:user_instructions|instructions|system-reminder)>/g,
|
|
208
|
+
)) {
|
|
209
|
+
credit('rules', m[1]);
|
|
210
|
+
}
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (Array.isArray(content)) {
|
|
215
|
+
for (const block of content) {
|
|
216
|
+
if (!block || typeof block !== 'object') continue;
|
|
217
|
+
if (block.type === 'tool_result') {
|
|
218
|
+
const call = callsById.get(block.tool_use_id);
|
|
219
|
+
const target = block.path ?? call?.path ?? null;
|
|
220
|
+
const outText = textOfContent(block.content ?? block.output ?? '');
|
|
221
|
+
if (isRulePath(target)) credit('rules', outText);
|
|
222
|
+
else if ((call && FILE_READ_TOOLS.has(call.name)) || FILE_READ_TOOLS.has(block.name)) {
|
|
223
|
+
credit('fileReads', outText);
|
|
224
|
+
} else {
|
|
225
|
+
credit('toolOutput', outText);
|
|
226
|
+
}
|
|
227
|
+
} else if (typeof block.text === 'string') {
|
|
228
|
+
for (const m of block.text.matchAll(
|
|
229
|
+
/<(?:user_instructions|instructions|system-reminder)>([\s\S]*?)<\/(?:user_instructions|instructions|system-reminder)>/g,
|
|
230
|
+
)) {
|
|
231
|
+
credit('rules', m[1]);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const totalBytes = buckets.rules.bytes + buckets.fileReads.bytes + buckets.toolOutput.bytes;
|
|
239
|
+
const share = (n) => (totalBytes === 0 ? 0 : Math.round((n / totalBytes) * 100));
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
ok: true,
|
|
243
|
+
path: resolvedPath,
|
|
244
|
+
rules: {
|
|
245
|
+
bytes: buckets.rules.bytes,
|
|
246
|
+
tokens: tokens(buckets.rules.bytes),
|
|
247
|
+
share: share(buckets.rules.bytes),
|
|
248
|
+
items: buckets.rules.items,
|
|
249
|
+
},
|
|
250
|
+
fileReads: {
|
|
251
|
+
bytes: buckets.fileReads.bytes,
|
|
252
|
+
tokens: tokens(buckets.fileReads.bytes),
|
|
253
|
+
share: share(buckets.fileReads.bytes),
|
|
254
|
+
items: buckets.fileReads.items,
|
|
255
|
+
},
|
|
256
|
+
toolOutput: {
|
|
257
|
+
bytes: buckets.toolOutput.bytes,
|
|
258
|
+
tokens: tokens(buckets.toolOutput.bytes),
|
|
259
|
+
share: share(buckets.toolOutput.bytes),
|
|
260
|
+
items: buckets.toolOutput.items,
|
|
261
|
+
},
|
|
262
|
+
total: {
|
|
263
|
+
bytes: totalBytes,
|
|
264
|
+
tokens: tokens(totalBytes),
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
}
|