@jawache/flow 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,747 @@
1
+ import { inScope, type Scope, type BreadcrumbMoment, type CaseWorld, type Category, type EntrySpec, type GuardrailMoment, type LoadResult, type LoadedEntry, type Moment, type SessionFacts, type Settings, type TurnAction, type Unanswered, type World } from "../language/domain.ts";
2
+ /**
3
+ * How a category recognises its members from host-written evidence.
4
+ *
5
+ * The three rungs above, as fields — and their ORDER is enforced by `spawnedAs` rather than by the
6
+ * author remembering it. That is the point of the recipe existing at all: a hand-written classifier
7
+ * that greps the brief first is one line of plausible code, and it silently mislabels every
8
+ * specifically-typed spawn whose brief happens to quote another rung's work.
9
+ */
10
+ export interface SpawnRecipe {
11
+ /**
12
+ * Sidecar agent types that ARE this category, decisively. Brief text is never consulted for a
13
+ * session whose type is named here — nor for one whose type is named anywhere but `generic`.
14
+ */
15
+ readonly types?: readonly string[];
16
+ /**
17
+ * The harness's generic spawn buckets — the ONLY types whose brief may be read. Which names
18
+ * these are is harness knowledge, so it is supplied rather than assumed.
19
+ */
20
+ readonly generic?: readonly string[];
21
+ /** Substrings that recover this category from a generic spawn's brief. */
22
+ readonly brief?: readonly string[];
23
+ /**
24
+ * True for the top-level session only — no sidecar, no agent type. Structural, and exclusive: a
25
+ * recipe is either about spawns or about the parent, never both.
26
+ */
27
+ readonly parent?: boolean;
28
+ }
29
+ /**
30
+ * A classifier built from the settled order — what `defineCategory` is normally handed.
31
+ *
32
+ * A bespoke function remains the escape hatch and always will: a repo that classifies on something
33
+ * nobody anticipated writes `defineCategory("x", facts => …)` and reads whatever it likes. What
34
+ * this buys is that the ORDINARY case cannot be got wrong.
35
+ */
36
+ export declare function spawnedAs(recipe: SpawnRecipe): (facts: SessionFacts) => boolean;
37
+ /** A classifier that threw. Fail-loud: the engine refuses rather than guessing who you are. */
38
+ export interface ClassifierFault {
39
+ readonly category: string;
40
+ readonly error: string;
41
+ }
42
+ /** Who a session is, and every classifier that could not say. */
43
+ export interface Identity {
44
+ /** Every category whose classifier said yes. A session wears all of them, not the first. */
45
+ readonly wearing: readonly string[];
46
+ readonly faults: readonly ClassifierFault[];
47
+ /** True when this verdict was read now, and the caller therefore owes it to the state file. */
48
+ readonly fresh: boolean;
49
+ }
50
+ /**
51
+ * The categories any bound entry names — REFERENCED IS REGISTERED, and this is that sentence.
52
+ *
53
+ * There is no list to register in and none to fall out of step with the code: a category reaches
54
+ * the classifier because some entry scoped itself to it, so an unused category costs nothing and a
55
+ * used one cannot be forgotten. Deduped by NAME rather than by identity, because two copies of one
56
+ * package in a node_modules tree are two objects meaning one category.
57
+ */
58
+ export declare function categoriesIn(entries: readonly LoadedEntry[]): Category[];
59
+ /**
60
+ * Who this session × agent is — the stored verdict if there is one, otherwise a fresh reading.
61
+ *
62
+ * STICKY, and that is a decision rather than a cache. A session's identity is settled by evidence
63
+ * written when it was spawned, so re-deriving it every hook could only ever change the answer by
64
+ * accident — a transcript since compacted, a brief scrolled away — and a rule that fired for the
65
+ * first half of a session and not the second is worse than either outcome alone. The unit is
66
+ * session × agent because that is what the evidence is about: each subagent has its own transcript
67
+ * and its own sidecar.
68
+ */
69
+ export declare function identify(stored: readonly string[] | undefined, facts: SessionFacts, categories: readonly Category[]): Identity;
70
+ /** One event the guardrail rails carry. Facts only — who it happened to is `wearing`. */
71
+ export interface GuardEvent {
72
+ readonly moment: GuardrailMoment;
73
+ /** write/delete: the would-be file, in memory, before disk. */
74
+ readonly file?: {
75
+ readonly path: string;
76
+ readonly content: string;
77
+ } | undefined;
78
+ /** command: the line about to run. */
79
+ readonly command?: string | undefined;
80
+ /** commit: the staged paths. */
81
+ readonly staged?: readonly string[] | undefined;
82
+ /** turn-end: what the actor did this turn, in order. */
83
+ readonly turn?: readonly TurnAction[] | undefined;
84
+ /** Every category this session × agent wears. Empty means it wears none, not "all of them". */
85
+ readonly wearing: readonly string[];
86
+ }
87
+ /** One event the breadcrumb rails carry. A note has no rail to block, so it has no world either. */
88
+ export interface BriefEvent {
89
+ readonly moment: BreadcrumbMoment;
90
+ /** touch: the path the tool call is about. A call carrying no path narrows nothing. */
91
+ readonly path?: string | undefined;
92
+ readonly wearing: readonly string[];
93
+ /** How far this session has drifted, in context tokens. The adapter measures it. */
94
+ readonly tokens: number;
95
+ }
96
+ /** Does this entry fire at this moment at all? A disabled entry is inert and never does. */
97
+ export declare function fires(spec: EntrySpec, moment: Moment): boolean;
98
+ /**
99
+ * Does this session wear what the entry binds to? An entry with no `.for(…)` binds to everyone.
100
+ *
101
+ * Absence means EVERY category, exactly as an absent `on` means every path — scoping is opt-in.
102
+ * `.for(a, b)` ORs, so a rule for two rungs fires for a session wearing either of them.
103
+ */
104
+ export declare function bindsTo(entry: LoadedEntry, wearing: readonly string[]): boolean;
105
+ export { inScope, type Scope };
106
+ /**
107
+ * Does this entry's scope REACH this path — the coverage question, which is not the matching one.
108
+ *
109
+ * `inScope` answers "may this entry run here", and an entry with no `on` at all answers yes to
110
+ * every path in the repo. That is right for matching and catastrophic for coverage: a commit gate
111
+ * or a command ban would claim the whole tree, and every gap in it would disappear. Coverage is
112
+ * only ever claimed by an entry that NAMES paths.
113
+ */
114
+ export declare function covers(scope: Scope, path: string): boolean;
115
+ /**
116
+ * The entries that can claim coverage of anything — live, and naming paths.
117
+ *
118
+ * ONE spelling, because three surfaces ask it and they must agree: the gap list, the terrain's
119
+ * per-node answer, and the transcript-side reading of areas nobody watches. They were spelled
120
+ * three ways for one commit, and a set that differs by a disabled entry is a folder that reads as
121
+ * guarded on one page and abandoned on the next.
122
+ */
123
+ export declare function watching(entries: readonly Bound[]): Bound[];
124
+ /** A rail refusing. `entry` is null when it is the guard itself that is broken, not a rule. */
125
+ export interface Block {
126
+ readonly do: "block";
127
+ readonly entry: string | null;
128
+ /** The sentence the entry's author wrote, or the fault's own text when there is no entry. */
129
+ readonly message: string;
130
+ /** What it is about: a path, a command, or null. */
131
+ readonly subject: string | null;
132
+ /** What the check said was wrong. Empty when it gave no detail. */
133
+ readonly detail: string;
134
+ }
135
+ /** Why a breadcrumb is showing. Closed, and every recorded row carries one of these words. */
136
+ export declare const CAUSES: readonly ["session", "first-touch", "drift", "fault"];
137
+ export type Cause = (typeof CAUSES)[number];
138
+ /** A note being shown. Never a rail — a breadcrumb informs and cannot refuse. */
139
+ export interface Notice {
140
+ readonly do: "notice";
141
+ readonly entry: string | null;
142
+ /** The prose, when the entry carried it inline. */
143
+ readonly text: string | undefined;
144
+ /** The file the prose lives in, when it did not. The shell resolves it; a pure home cannot. */
145
+ readonly file: string | undefined;
146
+ readonly cause: Cause;
147
+ }
148
+ export type Effect = Block | Notice;
149
+ /**
150
+ * What one entry did during one run — the tally the log carries and the coach reads.
151
+ *
152
+ * `matched` did not travel from the old engine. It counted subjects that fell inside an entry's
153
+ * scope, and once `on` became mandatory for a file rule it equalled `evaluated` in every row ever
154
+ * written: two names for one number, which is a question two readers can answer differently.
155
+ * `silenced` replaces it with something genuinely new — an entry can now be in force and still not
156
+ * run, because this session is not the actor it binds to, and a rule that looks dead in a parent's
157
+ * log may be doing its job perfectly in every builder's.
158
+ */
159
+ export interface Tally {
160
+ readonly id: string;
161
+ /** Subjects it was run against. */
162
+ readonly evaluated: number;
163
+ /** Subjects it blocked. */
164
+ readonly hits: number;
165
+ /** Times its `.for(…)` scope kept it out of this session entirely. */
166
+ readonly silenced: number;
167
+ }
168
+ /** Everything one rail produced: what happens now, and what the log records. */
169
+ export interface Outcome {
170
+ readonly effects: readonly Effect[];
171
+ readonly tallies: readonly Tally[];
172
+ }
173
+ /**
174
+ * One block from the guard itself rather than from any rule. `entry: null` is how a reader tells
175
+ * this from a rule refusing. The adapter raises the same shape for a broken config, so this is the
176
+ * one home the canonical block wears — a field added here reaches both callers.
177
+ */
178
+ export declare function fault(message: string): Block;
179
+ export interface GuardArgs {
180
+ /** The load's answer. A refusal here refuses the rail — that is J3.1. */
181
+ readonly load: LoadResult;
182
+ readonly event: GuardEvent;
183
+ readonly world: World;
184
+ /** Classifiers that could not answer. Any of them and the rail refuses: identity is a gate. */
185
+ readonly faults?: readonly ClassifierFault[] | undefined;
186
+ /** The kill switch. Set, and nothing runs at all — see `offPath`. */
187
+ readonly off?: boolean | undefined;
188
+ }
189
+ /**
190
+ * Run one gated moment. THE engine entry point for anything that can refuse.
191
+ *
192
+ * The order of the three short-circuits is the order of the questions: is the guard on, does its
193
+ * config load, and do we know who this is. The last two refuse rather than proceeding, because a
194
+ * guard that runs a subset of its rules while looking healthy is the single failure this whole
195
+ * package exists to delete.
196
+ */
197
+ export declare function guard({ load, event, world, faults, off }: GuardArgs): Promise<Outcome>;
198
+ /** Entry id → the context size, in tokens, at which it was last shown. */
199
+ export type Marks = Readonly<Record<string, number>>;
200
+ /** How far a session drifts before a note is worth showing again. The old engine's number, kept. */
201
+ export declare const DEFAULT_DRIFT_TOKENS = 200000;
202
+ /** The threshold in force: the config's dial, or the default. */
203
+ export declare function driftTokens(settings: Settings): number;
204
+ export interface BriefArgs {
205
+ readonly load: LoadResult;
206
+ readonly event: BriefEvent;
207
+ readonly marks: Marks;
208
+ /**
209
+ * The config's own dials, and REQUIRED — `driftTokens` reaches the arithmetic through here and
210
+ * nowhere else.
211
+ *
212
+ * It was an optional `threshold` number, and that is a dead dial waiting to happen: `brief()`
213
+ * defaulted it independently, so a caller that never called `driftTokens(config.settings)` would
214
+ * run on 200k while the repo's `driftTokens:` sat in the config doing nothing, green and silent.
215
+ * That is the class of failure this whole package exists to delete, so the dial has one route and
216
+ * the compiler makes the adapter take it. `defineConfig` always produces a settings object, so
217
+ * passing `config.settings` costs the caller nothing.
218
+ */
219
+ readonly settings: Settings;
220
+ readonly off?: boolean | undefined;
221
+ }
222
+ /** The notes to show now, and the marks to store once they have been shown. */
223
+ export interface Briefing {
224
+ readonly notices: readonly Notice[];
225
+ readonly marks: Marks;
226
+ }
227
+ /**
228
+ * Which notes show at this moment, and what that leaves the marks looking like.
229
+ *
230
+ * EVERY matching breadcrumb shows — the general and the specific stack, so a file gets its area
231
+ * note and the finer notes layered on top. The old engine scored matches by glob length and then
232
+ * used only the sign of the score, a narrowing rule already abandoned in fact; it is not carried.
233
+ *
234
+ * A `session` breadcrumb shows once at the session moment and is MARKED there, which is why the
235
+ * first touch afterwards does not repeat what the greeting has just said.
236
+ */
237
+ export declare function brief({ load, event, marks, settings, off }: BriefArgs): Briefing;
238
+ /**
239
+ * The marks a compaction leaves behind: the session's, and nothing else.
240
+ *
241
+ * A compaction is the boundary where the agent stops knowing what it was told, so every AREA note
242
+ * has to be earned again — clearing their marks re-briefs them at the next touch. The session notes
243
+ * are kept because the host re-delivers orientation at the same moment, and clearing those too
244
+ * would print the greeting twice in a row.
245
+ *
246
+ * Their cause on re-showing is `first-touch`, deliberately and not for want of a fifth word: after
247
+ * a compaction it IS the agent's first touch of that area, and a separate cause would name the
248
+ * mechanism that cleared the mark rather than what the reader is looking at.
249
+ */
250
+ export declare function afterCompaction(marks: Marks, entries: readonly LoadedEntry[]): Marks;
251
+ /**
252
+ * One block, as the hook prints it.
253
+ *
254
+ * The entry's id is the WHOLE address — there is no second naming scheme to print beside it, which
255
+ * is the visible half of the old string routes dying. A reader who sees `core.noTodo` can open the
256
+ * config, find that property, and be looking at the sentence that refused them.
257
+ */
258
+ export declare function formatBlock(block: Block): string;
259
+ /** Every block in one outcome, as one message. Empty when nothing refused. */
260
+ export declare function formatBlocks(effects: readonly Effect[]): string;
261
+ /** flow's state directory, relative to a repo root. The one spelling. */
262
+ export declare const FLOW_DIR = ".flow";
263
+ /** What makes the directory invisible to git — `*` covers the ignore file itself. */
264
+ export declare const FLOW_GITIGNORE = "# flow's own state \u2014 telemetry and session marks, never committed.\n*\n";
265
+ /** The log schema this build writes, stamped into every session file's first row. */
266
+ export declare const LOG_VERSION = 1;
267
+ /** Every kind a log row may be. Closed — a reader that meets another word has met corruption. */
268
+ export declare const ROW_KINDS: readonly ["meta", "tool", "breadcrumb", "guardrail", "compaction", "run"];
269
+ export type RowKind = (typeof ROW_KINDS)[number];
270
+ export declare function flowDir(root: string): string;
271
+ /** A session id becomes a filename, so it is held to a filename's charset. */
272
+ export declare function sanitise(session: string): string;
273
+ /** One session's event stream. */
274
+ export declare function logFile(root: string, session: string): string;
275
+ /** One session × agent's own state: the categories it wears, and its breadcrumb marks. */
276
+ export declare function statePath(root: string, session: string, agent: string): string;
277
+ /**
278
+ * What one session × agent's state file holds.
279
+ *
280
+ * ONE FILE for the identity unit, not two. The categories it wears and the notes it has been shown
281
+ * are both facts about the same session × agent, they are written by the same hooks, and splitting
282
+ * them would double the reads on every tool call to save nothing.
283
+ */
284
+ export interface SessionState {
285
+ /** The sticky classification verdict. Absent means it has not been read yet. */
286
+ readonly categories?: readonly string[] | undefined;
287
+ readonly marks: Marks;
288
+ }
289
+ /**
290
+ * A state file's text → the state. Corrupt, empty or absent all mean the same thing: nothing known.
291
+ *
292
+ * FAILING SAFE HERE IS CORRECT, and it is the one place in flow where that is true. State is a
293
+ * de-duplication convenience: losing it re-shows a breadcrumb and re-reads a classification, which
294
+ * is the harmless direction. A guard that refused to run because a marks file had a stray byte
295
+ * would be failing loud about the one thing that does not matter.
296
+ */
297
+ export declare function readState(text: string | null | undefined): SessionState;
298
+ /**
299
+ * The marker that tells the commit gate which live session this worktree's commit belongs to.
300
+ *
301
+ * The BRANCH is in the name, and it has to be: the gate runs inside git's pre-commit hook, outside
302
+ * any session, and two worktrees committing at once must not clobber each other's answer and
303
+ * mis-attribute a commit. A worktree IS a branch — git refuses to check one out twice — so the
304
+ * branch is a safe key. It also keeps the scheme working the day a repo points several checkouts at
305
+ * one shared state directory, which is why it survived the move out of the journal.
306
+ */
307
+ export declare function markerPath(root: string, branch: string | null): string;
308
+ /**
309
+ * The kill switch: an existence-file, unchanged from the old engine.
310
+ *
311
+ * Its whole value is that turning the guard off needs no settings surgery, so an A/B run is one
312
+ * `touch` and one `rm`. It sits beside the state rather than in the repo, which makes it
313
+ * project-wide rather than per-worktree — "is the guard on here" is a question about the project.
314
+ */
315
+ export declare function offPath(root: string): string;
316
+ /**
317
+ * The RECORDING switch, and it is an existence-file for the same reason the kill switch is.
318
+ *
319
+ * Turning capture on is one `touch` and turning it off is one `rm`, with no settings surgery and
320
+ * no flag to remember on a hook the harness invokes rather than you. That matters more here than
321
+ * for `off`: recording is what you reach for the moment something misbehaves, and a mechanism that
322
+ * needs a config edit is one nobody arms in time to catch the bug they were looking at.
323
+ */
324
+ export declare function recordPath(root: string): string;
325
+ /** One session's recording — the canonical event stream, beside the log it should agree with. */
326
+ export declare function recordingFile(root: string, session: string): string;
327
+ /**
328
+ * Where a repo remembers which conversations the archival side has already read.
329
+ *
330
+ * One empty file per conversation, so EXISTENCE is the whole fact and there is nothing to keep in
331
+ * step. It lives here with every other `.flow/` path for the reason this section exists at all:
332
+ * the old engine spelled one state path two ways and the disagreement was invisible in exactly the
333
+ * direction that hurts — a reader looking in the wrong place reports "nothing read yet" rather
334
+ * than an error, and silently re-offers a year of history.
335
+ */
336
+ export declare function readDir(root: string): string;
337
+ /** The marker for one conversation. Sanitised, because a stray id must never escape the folder. */
338
+ export declare function readMarkPath(root: string, session: string): string;
339
+ /** What a session marker holds. Loose, because it is read back off disk. */
340
+ export interface SessionMarker {
341
+ readonly session?: unknown;
342
+ /**
343
+ * WHICH AGENT of that session was live — the field that lets a commit wear categories.
344
+ *
345
+ * Identity is stored per session × agent, so the session id alone cannot find it: a parent and
346
+ * three subagents share one session and are four different actors. Without this the commit gate
347
+ * had no honest `wearing` to hand the engine, so a `.for(builder).at(commit)` rule was loaded,
348
+ * counted, and silently silenced — the exact class of quiet failure this package exists to
349
+ * delete. Absent on a marker written before this field existed, which reads as "unknown actor".
350
+ */
351
+ readonly agent?: unknown;
352
+ readonly ts?: unknown;
353
+ }
354
+ /** A commit long after the last edit is not this session's. */
355
+ export declare const MARKER_MAX_AGE_MS: number;
356
+ /** Who a commit-gate run belongs to: the live session, and which of its agents was working. */
357
+ export interface Attribution {
358
+ readonly session: string;
359
+ /** Null when the marker named no agent — an old marker, or a run with nobody to attribute to. */
360
+ readonly agent: string | null;
361
+ }
362
+ /**
363
+ * Which session × agent a commit-gate run belongs to, given the marker it found.
364
+ *
365
+ * A fresh, well-formed marker wins. Stale, missing, malformed or absurdly-future → the fallback,
366
+ * with NO agent, so a human's or CI's commit stays unattributed rather than mis-pinned to a session
367
+ * that ended hours ago. `now` is an argument because a clock inside a decision is a decision nobody
368
+ * can test.
369
+ *
370
+ * The agent rides with the session rather than being read separately, because they are one fact:
371
+ * an agent id paired with the wrong session names a state file that does not exist, and half of a
372
+ * stale marker is not more useful than none of it.
373
+ */
374
+ export declare function attribution(marker: SessionMarker | null | undefined, fallback: string, nowMs: number, maxAgeMs?: number): Attribution;
375
+ /**
376
+ * The next event index, read from the TAIL of the session's own file.
377
+ *
378
+ * `seq` is what makes "before / after" computable without timestamp arithmetic — two rows written
379
+ * in the same millisecond still have an order. It comes from the log rather than from a counter
380
+ * file because a sidecar counter is a second thing that can be lost or disagree, and every writer
381
+ * here is a hook process too short-lived to hold one. A tail with no readable seq starts at 1,
382
+ * which is also where a truncated final line lands: the log is appended by several processes, so
383
+ * the last line can be caught mid-write.
384
+ */
385
+ export declare function nextSeq(tail: string): number;
386
+ /** One line of the log. Loose by necessity: it is read off disk, from a file a human can open. */
387
+ export interface Row {
388
+ readonly kind: RowKind;
389
+ readonly [key: string]: unknown;
390
+ }
391
+ /** The first row of every session file: who this stream is, and which build wrote it. */
392
+ export declare function metaRow(session: string, branch: string | null, worktree: string, startedAt: string): Row;
393
+ /**
394
+ * One summary row per engine invocation, and one row per rail that spoke.
395
+ *
396
+ * A block is a `guardrail` row with `out: "deny"` and never a row of its own. Two rows for one
397
+ * event meant every reader had to know which of them to count, and every block was in the log
398
+ * twice — the old engine's mistake, not repeated here.
399
+ */
400
+ export declare function runRows(moment: Moment, outcome: Outcome, subjects: number): Row[];
401
+ /** One bound entry, in the words the recorded rows can be matched against. */
402
+ export interface Bound extends Scope {
403
+ readonly kind: EntrySpec["kind"];
404
+ readonly id: string;
405
+ /** The pack it came from — the group a reader finds it in. */
406
+ readonly pack: string;
407
+ readonly at: readonly Moment[];
408
+ /** The categories it binds to. Empty means every actor. */
409
+ readonly for: readonly string[];
410
+ readonly description: string | null;
411
+ /** The sentence a guardrail refuses with, or a breadcrumb's own prose. */
412
+ readonly says: string | null;
413
+ /** The reason it is turned off, when it is. An absent value means it is live. */
414
+ readonly disabled: string | null;
415
+ }
416
+ /**
417
+ * Every bound entry as the record reads it — one reader, so "what does this entry watch" cannot
418
+ * be answered two ways by two surfaces.
419
+ *
420
+ * A DISABLED entry is included, carrying its reason. It fires at nothing, so it earns no verdict
421
+ * and claims no coverage; but leaving it out would make it invisible on every surface that lists
422
+ * the regime, and "turned off, here is why" is the one thing a reader most needs to see.
423
+ */
424
+ export declare function universe(entries: readonly LoadedEntry[]): Bound[];
425
+ /** The lead distribution's buckets, coarse on purpose — the shape matters, the exact number does not. */
426
+ export declare const LEAD_BUCKETS: readonly ["0", "1-5", "6-19", "20+"];
427
+ export type LeadBucket = (typeof LEAD_BUCKETS)[number];
428
+ export interface Lead {
429
+ /** The middle of the measured leads, or null when nothing could be measured. */
430
+ readonly median: number | null;
431
+ readonly buckets: Readonly<Record<LeadBucket, number>>;
432
+ /** Shows with no in-scope edit after them in the same chat. Counted, never scored as zero. */
433
+ readonly noEdit: number;
434
+ }
435
+ /** What one breadcrumb has done, and whether its scope has ever spoken. */
436
+ export interface BreadcrumbRecord {
437
+ readonly id: string;
438
+ readonly pack: string;
439
+ readonly shown: number;
440
+ readonly byCause: Readonly<Record<Cause, number>>;
441
+ readonly lead: Lead;
442
+ readonly last: string | null;
443
+ readonly dead: boolean;
444
+ }
445
+ /**
446
+ * What one guardrail has done — and the four numbers are the tally's own, not a second vocabulary.
447
+ *
448
+ * `evaluated` is subjects it was actually run against, which is already scope-filtered by the
449
+ * engine, so it IS the old `matched` under a truer name. `silenced` is the count the old engine
450
+ * could not have had: an entry in force that never ran because this session was not the actor it
451
+ * binds to, which is a rule doing its job rather than a rule doing nothing.
452
+ */
453
+ export interface GuardrailRecord {
454
+ readonly id: string;
455
+ readonly pack: string;
456
+ /** Rails it stood on — engine invocations where it was in force at the moment that fired. */
457
+ readonly runs: number;
458
+ readonly evaluated: number;
459
+ readonly hits: number;
460
+ readonly silenced: number;
461
+ readonly lastBlock: {
462
+ readonly ts: string | null;
463
+ readonly subject: string | null;
464
+ } | null;
465
+ /** Same entry, same subject, same chat, no compaction between: the message did not teach. */
466
+ readonly repeats: number;
467
+ readonly dead: boolean;
468
+ }
469
+ /** An area nothing watches, and how much work happened there. */
470
+ export interface Gap {
471
+ readonly area: string;
472
+ readonly edits: number;
473
+ }
474
+ /** An entry that has caught nothing for long enough that somebody owes it a decision. */
475
+ export interface Quiet {
476
+ readonly id: string;
477
+ readonly lastHit: string | null;
478
+ readonly daysSince: number;
479
+ }
480
+ /** How much history this reading stands on, and whether it is enough to call anything dead. */
481
+ export interface Span {
482
+ readonly sessions: number;
483
+ readonly days: number;
484
+ readonly first: string | null;
485
+ readonly last: string | null;
486
+ /** Enough opportunity that "never matched" is a verdict rather than a wait. */
487
+ readonly ample: boolean;
488
+ readonly tools: number;
489
+ }
490
+ export interface Metrics {
491
+ readonly headline: {
492
+ readonly blocks: number;
493
+ readonly lead: Lead;
494
+ readonly gaps: readonly Gap[];
495
+ readonly dead: readonly string[];
496
+ /** Bound, never once evaluated, over an ample history — nothing has ever reached it. */
497
+ readonly retire: readonly string[];
498
+ /** The same silence, over a history too thin to read anything into. */
499
+ readonly unproven: readonly string[];
500
+ /** It used to catch things and has not for a long time. */
501
+ readonly quiet: readonly Quiet[];
502
+ };
503
+ readonly breadcrumbs: readonly BreadcrumbRecord[];
504
+ readonly guardrails: readonly GuardrailRecord[];
505
+ readonly gaps: readonly Gap[];
506
+ readonly span: Span;
507
+ /**
508
+ * The per-path tally the terrain is drawn from — and it comes out of THIS walk rather than a
509
+ * second one over the same rows.
510
+ *
511
+ * The two readings had drifted before they were even finished: `gaps` decided "no entry reaches
512
+ * this edit" against one scoped set and the terrain decided it against another, spelled
513
+ * separately. There is one decision now, made here, and the tree reads its answer.
514
+ */
515
+ readonly heat: Heat;
516
+ }
517
+ /** One session's rows, as every reader of the record takes them. */
518
+ export interface SessionRows {
519
+ readonly session: string;
520
+ readonly rows: readonly Row[];
521
+ }
522
+ export interface MetricsArgs {
523
+ readonly sessions: readonly SessionRows[];
524
+ readonly entries: readonly Bound[];
525
+ readonly nowMs: number;
526
+ /** How much opportunity a scope needs before "never matched" is a verdict rather than a wait. */
527
+ readonly minSessions?: number | undefined;
528
+ readonly minDays?: number | undefined;
529
+ }
530
+ /**
531
+ * Is this a path inside the repo — the ONE predicate, and it is exported because two layers need
532
+ * the same answer.
533
+ *
534
+ * Every path flow reasons about is repo-relative, because that is how an `on` glob is written. An
535
+ * absolute path or one that climbs out is a real thing an agent touched and is simply not a node
536
+ * of this tree, not a file any glob could reach, and not an area anybody could have been watching.
537
+ * There were two spellings of that sentence and an inline third; a predicate that disagrees with
538
+ * itself decides that an edit was uncovered in one reading and out of scope in another.
539
+ */
540
+ export declare function insideRepo(path: string): boolean;
541
+ /**
542
+ * What a row was about, in the order the row kinds spell it.
543
+ *
544
+ * A mechanism row (`runRows`) states its `subject` — the path or the command line the entry was
545
+ * run against — and a tool row states the call's own `path` or `command`. One reader over both,
546
+ * because "what was this row about" is one question and the metrics ask it of every kind.
547
+ */
548
+ export declare function subjectOf(row: Row): string | null;
549
+ /**
550
+ * Did this row record a file being CHANGED?
551
+ *
552
+ * It reads a flag the ROW carries rather than a set of tool names, and that is the seam holding:
553
+ * which of a harness's tools change a file is harness knowledge, so the adapter decides it once
554
+ * when it writes the row and the engine — which may never know what a `MultiEdit` is — reads the
555
+ * answer. The old engine kept a tool-name set in its metrics layer, and a second, already-drifted
556
+ * copy in a rule; a fact about somebody else's tool names belongs on the far side of the seam.
557
+ */
558
+ export declare function isEdit(row: Row): boolean;
559
+ /**
560
+ * The whole reading, from the rows and the bindings they are judged against.
561
+ *
562
+ * `entries` is the universe as it stands today; `sessions` is every stream being read. The two
563
+ * together are the only inputs, and there is no clock inside — `nowMs` is an argument, because a
564
+ * judgement about how long something has been quiet is a decision and a decision must be testable.
565
+ */
566
+ export declare function metrics({ sessions, entries, nowMs, minSessions, minDays }: MetricsArgs): Metrics;
567
+ export interface TerrainNode {
568
+ /** Repo-relative path. A folder carries no trailing slash. */
569
+ readonly path: string;
570
+ /** The last segment — what a tree draws. */
571
+ readonly name: string;
572
+ readonly dir: boolean;
573
+ readonly depth: number;
574
+ /** Tool calls recorded against this path (a folder: everything under it). */
575
+ touches: number;
576
+ /** Of those, the ones that CHANGED a file. */
577
+ edits: number;
578
+ /** Ids of the entries whose globs reach here (a folder: anything inside it). */
579
+ entries: string[];
580
+ /** Edits here that no entry's globs reach. A folder's gap is the sum of its children's. */
581
+ uncovered: number;
582
+ }
583
+ /**
584
+ * What the recorded rows say about each path: how often it was touched at all, how often it was
585
+ * CHANGED, and how many of those changes nothing was watching.
586
+ *
587
+ * Three numbers rather than one because they answer different questions — reading an area is how
588
+ * an agent learns it, editing it is how the area changes (and a breadcrumb that arrives on the
589
+ * read is doing its job while one that arrives on the edit is not), and the third is the gap.
590
+ *
591
+ * It is produced by `metrics` and by nothing else. Coverage is a judgement about an entry's scope,
592
+ * so a second producer would be a second answer to "was anybody watching here" — which the gap
593
+ * list and the terrain briefly had.
594
+ */
595
+ export interface Heat {
596
+ readonly touches: Readonly<Record<string, number>>;
597
+ readonly edits: Readonly<Record<string, number>>;
598
+ readonly uncovered: Readonly<Record<string, number>>;
599
+ }
600
+ export interface TerrainArgs {
601
+ /** The tracked tree — what the repo HAS. */
602
+ readonly paths: readonly string[];
603
+ /** `metrics(…).heat` — the rows, already read. This function walks no rows of its own. */
604
+ readonly heat: Heat;
605
+ readonly entries: readonly Bound[];
606
+ }
607
+ /** The tree, flattened into display order: folders before files, alphabetical within each. */
608
+ export declare function terrain({ paths, heat: recorded, entries }: TerrainArgs): TerrainNode[];
609
+ /** The moments, in the order a session meets them. */
610
+ export declare const MOMENT_ORDER: readonly Moment[];
611
+ /** One entry as a moments view draws it: the binding, plus what it has done. */
612
+ export interface MomentEntry extends Bound {
613
+ readonly record: BreadcrumbRecord | GuardrailRecord | null;
614
+ }
615
+ export interface MomentsView {
616
+ readonly moments: readonly {
617
+ readonly moment: Moment;
618
+ readonly entries: readonly MomentEntry[];
619
+ }[];
620
+ readonly totals: {
621
+ readonly breadcrumbs: number;
622
+ readonly guardrails: number;
623
+ readonly disabled: number;
624
+ };
625
+ }
626
+ /** The universe arranged by moment, each entry carrying whatever the record says about it. */
627
+ export declare function momentsView(entries: readonly Bound[], recorded?: Metrics | null): MomentsView;
628
+ /** The recording format this build writes and reads. */
629
+ export declare const RECORDING_VERSION = 1;
630
+ /**
631
+ * One engine invocation, as it happened.
632
+ *
633
+ * `world` is what the checks reached for during that step, and its absence is not an empty world —
634
+ * it is a step whose checks reached for nothing. An unanswered reach at replay time is REPORTED
635
+ * rather than guessed at, which is `cannedWorld`'s own rule and the reason it is the one builder.
636
+ */
637
+ export type RecordedStep = {
638
+ readonly rail: "guard";
639
+ readonly moment: GuardrailMoment;
640
+ readonly file?: {
641
+ readonly path: string;
642
+ readonly content: string;
643
+ } | undefined;
644
+ readonly command?: string | undefined;
645
+ readonly staged?: readonly string[] | undefined;
646
+ readonly turn?: readonly TurnAction[] | undefined;
647
+ readonly wearing: readonly string[];
648
+ readonly world?: CaseWorld | undefined;
649
+ } | {
650
+ readonly rail: "brief";
651
+ readonly moment: BreadcrumbMoment;
652
+ readonly path?: string | undefined;
653
+ readonly wearing: readonly string[];
654
+ readonly tokens: number;
655
+ }
656
+ /** The host compacted: every area mark is cleared, the session marks hold. */
657
+ | {
658
+ readonly rail: "compaction";
659
+ };
660
+ export interface Recording {
661
+ readonly v: number;
662
+ readonly session: string;
663
+ readonly steps: readonly RecordedStep[];
664
+ }
665
+ /**
666
+ * A recording file's text → the steps in it. JSONL, one step per line, appended live.
667
+ *
668
+ * It is JSONL rather than one JSON document for the reason the log is: the writer is a hook
669
+ * process that lives for one tool call, so it can only ever append, and a document would have to
670
+ * be read, parsed and rewritten on every keystroke. An unreadable line is dropped rather than
671
+ * failing the read — the file is appended to by processes that can be killed mid-write, and one
672
+ * lost step is a smaller lie than no recording at all.
673
+ *
674
+ * The first line is a header carrying the session and the format version. A file with none is
675
+ * still read: what it holds is steps, and refusing a recording for want of a header would refuse
676
+ * exactly the hand-written fixture this format exists to make writable.
677
+ */
678
+ export declare function readRecording(text: string, fallback?: string): Recording;
679
+ /** The header line a recording opens with — who this stream is, and which build wrote it. */
680
+ export declare function recordingHeader(session: string): Record<string, unknown>;
681
+ /** One replayed step: what the engine said, and the rows it would have written. */
682
+ export interface ReplayedStep {
683
+ readonly step: RecordedStep;
684
+ readonly effects: readonly Effect[];
685
+ readonly rows: readonly Row[];
686
+ }
687
+ export interface ReplayResult {
688
+ readonly steps: readonly ReplayedStep[];
689
+ /** Every row the replay produced, in order — what the live log is diffed against. */
690
+ readonly rows: readonly Row[];
691
+ /** Reaches the recording could not answer. A replay with any of these is not evidence. */
692
+ readonly unanswered: readonly Unanswered[];
693
+ readonly marks: Marks;
694
+ }
695
+ export interface ReplayArgs {
696
+ readonly load: LoadResult;
697
+ readonly recording: Recording;
698
+ readonly settings: Settings;
699
+ /** Where the marks stood when the recording began. Empty is a session opening cold. */
700
+ readonly marks?: Marks | undefined;
701
+ }
702
+ /**
703
+ * Run a recording through the engine.
704
+ *
705
+ * It calls the SAME `guard`, `brief`, `afterCompaction` and `runRows` the live path calls — there
706
+ * is no replay engine, only a replay caller, and that is the property being proved. A second
707
+ * implementation here would make a green replay evidence about the replay rather than about the
708
+ * guard.
709
+ */
710
+ export declare function replay({ load, recording, settings, marks }: ReplayArgs): Promise<ReplayResult>;
711
+ /**
712
+ * One difference between what a replay said and what the live log recorded.
713
+ *
714
+ * `side` is which stream the row is missing FROM, so a reader can tell "the replay invented a
715
+ * block" from "the replay lost one" without holding both files open.
716
+ */
717
+ export interface RowDiff {
718
+ readonly side: "replay" | "live";
719
+ readonly at: number;
720
+ readonly row: string;
721
+ }
722
+ /**
723
+ * The rows a replay and a live log disagree about — EMPTY is the proof.
724
+ *
725
+ * Only the two MECHANISM rows are compared, and that is not a softening: a replay has no tool
726
+ * calls to record and no session to stamp a meta row for, so `tool`, `run` and `meta` are facts
727
+ * about the live process rather than about the guard's judgement. What the recording claims is
728
+ * that the same rules refuse the same subjects and the same notes show for the same causes, and
729
+ * those are exactly the rows named here.
730
+ */
731
+ export declare function diffRows(replayed: readonly Row[], live: readonly Row[]): RowDiff[];
732
+ /**
733
+ * A world that answers exactly as the one it wraps, and writes down every answer — the RECORDING
734
+ * SEAM, and the reason a bug becomes a fixture before it becomes a fix.
735
+ *
736
+ * It is the live world's own answers that are kept, not a re-derivation: the file as it was at
737
+ * that instant, the command's real exit code, the staged set git really had. Re-running the check
738
+ * tomorrow against today's repo would answer differently, and a fixture that cannot reproduce
739
+ * yesterday's block is not a fixture.
740
+ *
741
+ * `exec` answers are keyed by the EXACT command, because `cannedWorld` matches exactly before it
742
+ * matches by containment: a recording is machine-written and has no reason to be approximate.
743
+ */
744
+ export declare function recorder(world: World): {
745
+ readonly world: World;
746
+ readonly taken: () => CaseWorld;
747
+ };