@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,792 @@
1
+ import type { Case, CaseWorld, Check, Ctx, ExecResult, LoadedEntry, Moment, Unanswered } from "../language/domain.ts";
2
+ /** One pattern that matched, and the 1-based line it matched on. */
3
+ export interface PatternHit {
4
+ readonly pattern: string;
5
+ readonly line: number;
6
+ }
7
+ /**
8
+ * Every pattern that matches, per line, in order.
9
+ *
10
+ * A pattern is applied per LINE rather than to the whole text, because a hit has to be able to
11
+ * name one: "matches /TODO/" sends a reader to a file, "line 42: matches /TODO/" sends them to the
12
+ * place. A single-line subject — a command — simply has one line.
13
+ */
14
+ export declare function patternHits(text: string, patterns: readonly string[]): PatternHit[];
15
+ /**
16
+ * Ban a word or shape in a file's text.
17
+ *
18
+ * For code shapes prefer `astGrep` — it parses, so it will not match the word inside a string
19
+ * literal or inside a comment that is talking ABOUT the ban.
20
+ */
21
+ export declare const textBan: (options: {
22
+ ban: readonly string[];
23
+ }) => Check;
24
+ /**
25
+ * Ban a dangerous command before it runs.
26
+ *
27
+ * A DETERRENT: the patterns ARE its scope, so matching nothing in a repo is the rule working, not
28
+ * a rule watching the wrong place.
29
+ *
30
+ * MATCHED WHOLE, and that is the one difference from `textBan` above. A command is ONE subject
31
+ * however many lines it occupies — `git commit -m "subject\n\nCo-Authored-By: …"` is a single
32
+ * invocation, and the patterns that police it span the newline on purpose. Swept line by line
33
+ * (which is right for a file, where a hit must be able to name its line) every one of those
34
+ * patterns matches nothing, silently: the rule loads, counts, and cannot catch the thing it
35
+ * names.
36
+ */
37
+ export declare const banCommands: (options: {
38
+ ban: readonly string[];
39
+ }) => Check;
40
+ /**
41
+ * A `banCommands` pattern for: a command that writes permanent prose, with a BACKTICK inside a
42
+ * double-quoted argument.
43
+ *
44
+ * Reads as: <one of `heads`> … <an OPEN double quote> … <a backtick>, where the quote-pair walk
45
+ * skips arguments that are already closed, and both scans refuse to cross a QUOTED heredoc opener
46
+ * — inside one the shell substitutes nothing, so it is the sanctioned way to carry backticks and
47
+ * must not be blocked. An unquoted `<<EOF` is deliberately still caught: there, substitution
48
+ * really does happen. This is the one pattern that has to span a newline, and the reason
49
+ * `banCommands` matches a command whole rather than line by line.
50
+ *
51
+ * A BUILDER over a `heads` list rather than one frozen expression, because the commands that
52
+ * record prose belong to different packs: `git commit` and `gh pr|issue` are `git`'s, which every
53
+ * repo binds, and the `work …` verbs are `work`'s, which only a repo running the work lifecycle
54
+ * binds. One tail, two heads, and neither pack carries a copy of the other's — a second copy of
55
+ * this expression is how the two would drift while both still read as armed.
56
+ *
57
+ * WHY IT IS CORE and not a helper beside the packs: TWO PACKS
58
+ * SHARE IT, and that is the whole rule the fold settled — anything two packs share is a core
59
+ * check. A shared file sitting beside the packs is a library with two users and no owner, and this
60
+ * one proved it by growing four unrelated sections around this pair. Here it sits next to the
61
+ * check it builds a pattern for, and the door that exports one exports both.
62
+ */
63
+ export declare function substitutionInProse(heads: readonly string[]): string;
64
+ /**
65
+ * What a blocked prose-writing command is told, and it is the same sentence whichever pack caught
66
+ * it: the mechanism, the absence of an undo, the fix, and the one false positive the rule owns.
67
+ *
68
+ * ONE constant rather than a message per pack. The sentence never named the verb that was blocked
69
+ * — it names the shape — so two copies would be two places for the same advice to drift, and the
70
+ * copy that drifted would still read as correct.
71
+ */
72
+ export declare const SUBSTITUTION_MESSAGE: string;
73
+ /**
74
+ * Refuse a hand-edit of a protected path.
75
+ *
76
+ * The entry's `.on()` IS the protected set and the engine has already filtered on it, so any file
77
+ * that reaches this check is protected by definition: matching it is the violation. There is no
78
+ * second list of paths inside the check: a rule that could be scoped one way and protect another
79
+ * is a rule whose two halves can disagree in silence.
80
+ *
81
+ * `existingOnly` is what an APPEND-ONLY tree needs: a migrations folder, where yesterday's file is
82
+ * history and may never move while today's is the whole point of the folder. Absent means no
83
+ * narrowing — every write to a protected path is refused — which is the empty value rather than a
84
+ * default a reader has to know about.
85
+ */
86
+ export declare const protectedPath: (options: {
87
+ existingOnly?: boolean;
88
+ }) => Check;
89
+ /**
90
+ * A file must have a required companion on disk — every `src/domain/**` needs its `.test.ts`.
91
+ *
92
+ * Path shape only: it reads no content, just asks whether the sibling is there. The template fills
93
+ * `{dir}` `{name}` `{base}` `{path}` from the touched path.
94
+ */
95
+ export declare const siblingExists: (options: {
96
+ sibling: string;
97
+ }) => Check;
98
+ /**
99
+ * The canon a feature folder is judged against.
100
+ *
101
+ * `root` and `allow` are REQUIRED: the canon IS the content of the rule, so an entry that does not
102
+ * state it is an entry nobody can read, and a default would be one repo's canon applied silently
103
+ * in every other.
104
+ * `thinking` and `folders` stay optional because absent means "no exemption" and "the folder tier
105
+ * is not policed" — the empty value in both cases.
106
+ */
107
+ export interface CanonOptions {
108
+ /** The tier boundary — everything below it is judged. */
109
+ readonly root: string;
110
+ /** The filenames a feature folder may hold. Empty means the filename tier is not policed. */
111
+ readonly allow: readonly string[];
112
+ /** The one file that may carry a `.test.<ext>` sibling. Absent means none may. */
113
+ readonly thinking?: string;
114
+ /**
115
+ * The names allowed DIRECTLY under `root` — the folder tier.
116
+ *
117
+ * A second allowlist rather than a mode flag, because the two questions are genuinely different
118
+ * and a repo can want either alone. `docs/` wants only this one (exactly `user/` and `agent/` at
119
+ * the top, anything below them); a feature tree wants only the filename one. Absent means the
120
+ * folder tier is not policed, which is what every caller before docs asked for.
121
+ */
122
+ readonly folders?: readonly string[];
123
+ }
124
+ /**
125
+ * What the canon says about one path.
126
+ *
127
+ * A DISCRIMINATED reason rather than a boolean, because there are two different violations here
128
+ * and they earn different sentences. The decision and the wording used to be two functions, each
129
+ * re-deriving the tier from the path — which meant the message could describe a different fault
130
+ * from the one that fired, and only a reader comparing the two would ever notice.
131
+ */
132
+ export type CanonVerdict = {
133
+ readonly ok: true;
134
+ }
135
+ /** A name directly under `root` that the folder tier does not allow. `found` is that name. */
136
+ | {
137
+ readonly ok: false;
138
+ readonly tier: "folder";
139
+ readonly found: string;
140
+ }
141
+ /** A filename inside a feature folder that the canon does not allow. */
142
+ | {
143
+ readonly ok: false;
144
+ readonly tier: "filename";
145
+ };
146
+ /**
147
+ * Judge one path against the canon. The ONE place the tier decision is made.
148
+ *
149
+ * The rule. A file directly under `root` is the shared-util tier and may be named anything — the
150
+ * test and coverage rules already cover it. A file INSIDE a feature folder (`<root>/<feature>/…`)
151
+ * must be one of `allow`. An invented name (payload · helpers · utils · views · mappers) is the
152
+ * classic escape hatch: somewhere to stash logic that dodges the domain's test and its coverage
153
+ * gate. So it is refused, and the fix is to fold that logic into the thinking file or to rename
154
+ * the talking file after the thing it talks to.
155
+ *
156
+ * The extensions are derived from the path rather than configured — a repo that writes .ts and one
157
+ * that writes .mjs want the same rule, and a second list to keep in step is a second list to get
158
+ * wrong. It reads no content: this is a claim about NAMES, and reading the file would only invite
159
+ * the rule to start having opinions about what is in it.
160
+ */
161
+ export declare function judgeCanonical(path: string, o: CanonOptions): CanonVerdict;
162
+ /** The sentence a verdict earns. It renders what was decided — it never decides anything itself. */
163
+ export declare function canonicalFault(verdict: CanonVerdict, path: string, o: CanonOptions): string;
164
+ /** A feature folder may hold only canonical filenames. */
165
+ export declare const canonicalFiles: (options: CanonOptions) => Check;
166
+ /**
167
+ * One assertion about a JSON file: a dotted path, and one thing that must hold at it.
168
+ *
169
+ * `required` is what tells an absent OPTIONAL path from an absent mandatory one. Without it every
170
+ * assertion would either have to exist, or would silently pass the day the key was renamed.
171
+ */
172
+ export interface JsonAssert {
173
+ /** The dotted path to dig to. Absent (or empty) means the document root. */
174
+ readonly path?: string;
175
+ /** The value is an object and EVERY key starts with this string. */
176
+ readonly keysPrefixedWith?: string;
177
+ /** The value deep-equals this (compared as JSON). */
178
+ readonly equals?: unknown;
179
+ /** The value, stringified, matches this regular expression. */
180
+ readonly matches?: string;
181
+ /** The path must exist at all. An absent optional path is skipped silently. */
182
+ readonly required?: boolean;
183
+ }
184
+ /**
185
+ * Every assertion that fails over one JSON document.
186
+ *
187
+ * Text that will not parse yields NO violations, and that is deliberate rather than lax: this runs
188
+ * on a would-be file at write time, which is frequently mid-keystroke, and a rule that shouted
189
+ * "invalid JSON" at every second character would be turned off within a day. A file that really is
190
+ * broken JSON is a problem the tool that reads it will name, loudly, on its own.
191
+ */
192
+ export declare function jsonViolations(jsonText: string, assert: readonly JsonAssert[]): string[];
193
+ /** A structured (JSON) file must hold an invariant. */
194
+ export declare const jsonInvariant: (options: {
195
+ assert: readonly JsonAssert[];
196
+ }) => Check;
197
+ /** Of some exported names, which are never mentioned in the sibling test's text? */
198
+ export declare function unreferenced(names: readonly string[], siblingText: string): string[];
199
+ /**
200
+ * The names a TypeScript source exports — functions, classes and consts.
201
+ *
202
+ * A real parser rather than a regex, because `export const x = …` inside a string or a comment is
203
+ * exactly the false positive that teaches people to ignore a rule. Parsing is not reaching the
204
+ * world — a string in, a tree out — which is why the grammar library sits in this layer's import
205
+ * allowlist beside the HTML parser, and why the import is dynamic: the native binding costs real
206
+ * milliseconds to load and no other check needs it.
207
+ */
208
+ export declare function exportedNames(content: string): Promise<string[]>;
209
+ /** Every exported symbol is referenced in its sibling test. */
210
+ export declare const symbolsInSibling: (options: {
211
+ sibling: string;
212
+ }) => Check;
213
+ /**
214
+ * The changed set this event is about.
215
+ *
216
+ * ONE read, in one place, because three separate checks used to derive it three ways: the commit
217
+ * gate had it as an event fact, the tool-gate re-shelled for it, and change-together shelled for
218
+ * it again with its own error handling. It is an event FACT first — the adapter puts the staged
219
+ * paths on ctx at the commit moment — and a capability call only when the moment carried none,
220
+ * which is what lets a `.test()` case supply it as plain data.
221
+ */
222
+ export declare function changedSet(ctx: Ctx): Promise<readonly string[]>;
223
+ /** One "these move together" group: if any `if` glob changed, some `thenAny` glob must have too. */
224
+ export interface ChangeGroup {
225
+ readonly if: readonly string[];
226
+ readonly thenAny: readonly string[];
227
+ }
228
+ /** Which groups were triggered without their companion. */
229
+ export declare function lonelyChanges(changed: readonly string[], groups: readonly ChangeGroup[]): string[];
230
+ /** Files that must move in the SAME commit — a schema and its migration, a dep and its lockfile. */
231
+ export declare const changeTogether: (options: {
232
+ groups: readonly ChangeGroup[];
233
+ }) => Check;
234
+ /**
235
+ * git's own options that TAKE A VALUE, and therefore swallow the token after them.
236
+ *
237
+ * They have to be known by name to find where the subcommand starts: `git -C ~/repo commit -m x`
238
+ * puts two tokens between `git` and `commit`, and a scan that took the first argument as the
239
+ * subcommand saw `-C` and walked away. Knowing them by name is also what keeps `git commit -C
240
+ * HEAD` — the reuse-this-message flag, nothing to do with directories — from reading as a command
241
+ * aimed at a directory called HEAD.
242
+ */
243
+ export declare const GIT_VALUE_OPTS: ReadonlySet<string>;
244
+ /**
245
+ * A shell-ish tokeniser: quotes respected (so a commit message mentioning `git commit` is not a
246
+ * second command), operators emitted as their own tokens.
247
+ *
248
+ * Null on unparseable input — an unclosed quote — and null means "cannot tell", which every caller
249
+ * must read as "do not judge". A guard that guesses at a line it cannot parse is worse than one
250
+ * that stands aside.
251
+ */
252
+ export declare function tokenizeCommand(command: string): string[] | null;
253
+ /** One `git …` in a command line, split where git stops and its subcommand starts. */
254
+ export interface GitInvocation {
255
+ /** git's OWN options, before the subcommand — where `-C` counts and only there. */
256
+ readonly globals: readonly string[];
257
+ /** The subcommand word (`commit`, `status`), or null when the invocation carries none. */
258
+ readonly subcommand: string | null;
259
+ /** Everything after the subcommand — its own flags and arguments. */
260
+ readonly args: readonly string[];
261
+ }
262
+ /**
263
+ * Every git invocation in a command line, in order. Null when the line cannot be parsed at all.
264
+ *
265
+ * A token only starts an invocation at a COMMAND position — the start of the line or just after an
266
+ * operator — so a `git` inside a quoted message or as an argument to something else is not one.
267
+ */
268
+ export declare function gitInvocations(command: string): GitInvocation[] | null;
269
+ /**
270
+ * The body of the first heredoc in the raw command, or null.
271
+ *
272
+ * `git commit -F -` reads its message on stdin, and a QUOTED heredoc is the safe way to write one
273
+ * containing backticks — a form this codebase's own rules push commits toward. The argument scan
274
+ * cannot see it: there is no `-m` token, so a message written that way goes unjudged, and every
275
+ * rule that reads a commit message is inert on it.
276
+ *
277
+ * Deliberately last-resort and deliberately dumb — it runs only when no inline message was found,
278
+ * and reads the FIRST heredoc, because a commit command carrying two is not a shape anyone writes
279
+ * and guessing between them would be worse than the gap.
280
+ */
281
+ export declare function heredocBody(command: string): string | null;
282
+ /**
283
+ * The message a `git commit` in this command line carries, or null when there is no judgeable
284
+ * commit in it at all. Null is the "not our business" answer and every caller passes on it.
285
+ *
286
+ * EVERY `-m` IS READ, AND THEY JOIN AS PARAGRAPHS. This returned the FIRST one and stopped, which
287
+ * made `git commit -m "<subject>" -m "<body>"` — git's own two-paragraph form, and the one an agent
288
+ * reaches for when the reason will not fit in a subject — a commit whose body was never read. The
289
+ * consequence was not a missed block but the worse direction: a commit that HAD recorded its reason
290
+ * in the second `-m` was refused, on the one rail whose whole job is to be obeyed.
291
+ *
292
+ * The separator is a BLANK line because that is what git writes. Verified rather than assumed:
293
+ * `git commit -m a -m b -m c` then `git log --format=%B | od -c` shows `a\n\nb\n\nc\n\n`.
294
+ *
295
+ * The forms it sees, settled from the two scripts this absorbed rather than invented:
296
+ *
297
+ * git commit -m MSG · -mMSG · --message MSG · --message=MSG judged
298
+ * git commit -m SUBJECT -m BODY (repeated, any mix of forms) judged, joined as git joins them
299
+ * git commit -F - <<'EOF' … EOF (any heredoc on the line) judged, via the heredoc body
300
+ * git commit -F path/to/file NOT judged — the message is on
301
+ * disk, and reading it is a read
302
+ * of the world this layer does not
303
+ * do
304
+ * git commit (an editor commit, no message anywhere) NOT judged — there is nothing to
305
+ * read before the editor opens
306
+ *
307
+ * The boundary is on the record: this rail polices AGENT-typed commits, which are its audience. A
308
+ * human committing from an editor is not its subject and never was.
309
+ */
310
+ export declare function commitMessage(command: string): string | null;
311
+ /** One shell argument, single-quoted so nothing in it can be expanded or re-parsed. */
312
+ export declare function quoteArg(value: string): string;
313
+ /**
314
+ * WHICH WORKING TREE THIS COMMAND IS ABOUT, as a `-C` prefix to copy onto every read — or "".
315
+ *
316
+ * It is COPIED rather than resolved. Resolving it to an absolute root needs a root and a cwd to
317
+ * point at one, and a check has neither: inventing a `root` on ctx would put a filesystem fact into
318
+ * a contract whose whole promise is that there is no filesystem in it. Copying the tokens through
319
+ * means the shell resolves them exactly as git would have, relative to wherever `ctx.exec` runs —
320
+ * which is the repo.
321
+ *
322
+ * TWO WAYS A COMMAND LEAVES THIS REPO, and until F3 only one of them was read.
323
+ *
324
+ * · `git -C <path> …` — git's own option.
325
+ * · `cd <path> && git …` — the shell's, and the one a person actually types.
326
+ *
327
+ * The second was found the way the first was: live, by a guard allowing what it existed to refuse.
328
+ * The F3 builder ran `git checkout <path>` from a session rooted in another worktree, and every
329
+ * git read the check made answered about the session's tree, where the path did not exist — so the
330
+ * discard was judged against a repo it was never aimed at, found nothing to lose, and went through.
331
+ * A rule that reads the wrong tree does not fail loudly; it passes quietly, which is the whole
332
+ * failure this package is about.
333
+ *
334
+ * BOTH ARE EMITTED, in the order the shell would apply them, because git chains multiple `-C`
335
+ * options exactly as a shell chains directory changes: `cd /a && git -C b status` runs in `/a/b`,
336
+ * and `-C '/a' -C 'b'` says precisely that. Nothing has to decide which of the two wins.
337
+ *
338
+ * ONLY A LEADING `cd`, and only when an operator follows its path. A `cd` in the middle of a chain
339
+ * can be preceded by another that already moved, and a command line that walks two trees is beyond
340
+ * what one answer can honestly describe — the same reason the FIRST git invocation decides below.
341
+ *
342
+ * A path the SHELL would have expanded — `~`, a variable — is quoted here and so reaches git
343
+ * literally, which makes the read fail rather than lie. Every caller treats a failed read as "not
344
+ * a repo, git owns that error" and passes: a false negative, which is this pack's stated trade.
345
+ */
346
+ export declare function gitDirPrefix(command: string): string;
347
+ /**
348
+ * Does the message carry the token, on a LINE OF ITS OWN?
349
+ *
350
+ * A line, not a mention. The line is what makes the reason findable later — a reviewer reads these
351
+ * at every phase boundary — and a token buried mid-sentence ("reverted the new-dep: thing") is both
352
+ * unreadable to them and a free pass to anyone who types the word by accident. The two scripts this
353
+ * absorbed matched the token anywhere; this is the one place the rebuild is deliberately stricter,
354
+ * and the hit says so in as many words.
355
+ *
356
+ * The token is regex-ESCAPED. Both absorbed scripts interpolated it raw, so a token carrying `.` or
357
+ * `+` quietly matched more than it said.
358
+ */
359
+ export declare function givesReason(message: string, token: string): boolean;
360
+ /** What the working tree is about to carry, split by whether each path is new to the repo. */
361
+ export interface WorkingState {
362
+ /** Everything that moved — tracked edits, staged adds, and untracked files. */
363
+ readonly changed: readonly string[];
364
+ /** The subset that is NEW here: a staged add, or untracked. */
365
+ readonly added: readonly string[];
366
+ }
367
+ /**
368
+ * One file whose DIFF is read for names that owe a reason, and the shape of the third condition.
369
+ *
370
+ * The two conditions above it ask about PATHS — a file changed, a file is new — and there is a
371
+ * third question a guard keeps wanting to ask that neither can reach: not "did this file change"
372
+ * but "what did this commit ADD INSIDE it". A dispatch surface is the case that forced it: every
373
+ * commit to `cli/work.ts` changes the file, so a path condition there asks for a sentence about
374
+ * every edit and is turned off within a week; what actually owes one is a NEW VERB, and a verb is
375
+ * a name on an added line.
376
+ *
377
+ * WHY IT REPLACED A CHECK OF ITS OWN. This arrived as `newCommandNeedsCaller` in the shared file
378
+ * beside the packs, and it was `commitReason` with one condition missing and everything else
379
+ * rewritten: its own commit detection, its own message parse, its own token read, its own
380
+ * one-sentence refusal. No pack bound it — two house packs in two repos did — so it was a shipped
381
+ * check with no shipped user, carrying a second copy of a mechanism that already existed. As a
382
+ * condition it inherits all of it, and the `known` list stays where it always had to be: on the
383
+ * binding, because which names are already there is a project's own fact.
384
+ */
385
+ export interface DiffAdds {
386
+ /** The one file whose `git diff HEAD -- <file>` is read. Nothing else in the commit is looked at. */
387
+ readonly file: string;
388
+ /**
389
+ * Regex sources, each with exactly ONE capture group — the name a hit is reported by.
390
+ *
391
+ * Several, because one surface registers a name several ways: a `case "x":` in a dispatch
392
+ * switch, a `command === "x"`, a `sub === "x"` in a subcommand router. Two of those three went
393
+ * unwatched for a year under a check that hard-coded them, which is the argument for the list
394
+ * being a parameter a reader of the binding can see rather than a constant inside a function.
395
+ */
396
+ readonly patterns: readonly string[];
397
+ /**
398
+ * Names already on the surface: matched, and ignored.
399
+ *
400
+ * This is what makes it a RATCHET rather than a rule about editing the file at all. Renaming,
401
+ * moving or reformatting an existing name is silent by design — a rule that fired on every
402
+ * dispatch tidy is a rule someone turns off — and a stale list is the same silent failure the
403
+ * ratchet exists to catch, which is why it is stated at the binding and never inside the check.
404
+ */
405
+ readonly known?: readonly string[] | undefined;
406
+ }
407
+ /**
408
+ * The names one file's diff ADDED and did not already know.
409
+ *
410
+ * Added lines only: removals and context are ignored, so a rename shows the new name and nothing
411
+ * else, and a reformat shows nothing. `+++` is skipped — it is the header naming the file, not a
412
+ * line of the file.
413
+ *
414
+ * A pattern with no capture group contributes nothing rather than throwing: the binding is
415
+ * TypeScript and a missing group is a mistake, but the moment this runs is a commit gate, and a
416
+ * gate that crashes on a bad pattern refuses every commit in the repo until someone reads a stack
417
+ * trace.
418
+ */
419
+ export declare function addedNames(diff: string, patterns: readonly string[], known?: readonly string[]): string[];
420
+ export interface ReasonInput {
421
+ /** The commit message, as the command line carries it. */
422
+ readonly message: string;
423
+ /**
424
+ * What the working tree has to say — index and worktree, tracked and untracked.
425
+ *
426
+ * Deliberately the generous reading: a commit is usually typed straight after the staging,
427
+ * `git commit -a` sweeps tracked worktree edits in, and the cost of asking for a sentence that
428
+ * was not strictly owed is a sentence, while the cost of missing one is the record this rule
429
+ * exists to keep.
430
+ */
431
+ readonly state: WorkingState;
432
+ /** Paths that owe a reason when they change at all. */
433
+ readonly whenChanged?: readonly string[] | undefined;
434
+ /** Paths that owe a reason only when they are NEW. */
435
+ readonly whenAdded?: readonly string[] | undefined;
436
+ /** Paths that never owe one, whichever condition matched — a test sibling, typically. */
437
+ readonly except?: readonly string[] | undefined;
438
+ /**
439
+ * What the third condition found: names added inside a watched file, each with the file it came
440
+ * from. Already filtered against that file's `known` list by `addedNames` — reading the diff is
441
+ * IO and belongs to the check, and what is left here is the same pure judgement as the other two.
442
+ */
443
+ readonly namesAdded?: readonly {
444
+ readonly name: string;
445
+ readonly file: string;
446
+ }[] | undefined;
447
+ /** The key the message must carry on a line of its own. */
448
+ readonly token: string;
449
+ }
450
+ /**
451
+ * The findings for one commit. Empty means either nothing this rule watches moved, or the reason is
452
+ * there.
453
+ *
454
+ * ONE hit, naming everything that asked and why it asked — `(new)`, `(changed)`, or `(added in
455
+ * <file>)` for a name the third condition found inside a diff. A message that names four things
456
+ * when one is the reason is a message nobody reads twice, so all three conditions report together
457
+ * in one sentence rather than one hit each.
458
+ *
459
+ * WHAT IT BLOCKS is the UNEXPLAINED act, never the act. Adding a dependency is fine; adding one
460
+ * with nothing written down is not, because the one thing a diff can never show is which
461
+ * alternatives were weighed.
462
+ */
463
+ export declare function reasonHits(o: ReasonInput): string[];
464
+ /**
465
+ * This commit needs a reason recorded, and here is whether it gave one.
466
+ *
467
+ * WHAT THIS REPLACES. THREE hand-rolled checks asked the same question about different nouns — a
468
+ * commit adding a dependency, a commit adding a file to a pure core, and a commit adding a verb to
469
+ * a dispatch surface — each carrying its own git-commit detection, its own read of what moved, its
470
+ * own message parse and its own refusal sentence. One check, three conditions and a token: that is
471
+ * the whole mechanism now, and it is stated on the entry where a reader meets it.
472
+ *
473
+ * THE THIRD CONDITION is `diffAdds`, and it is the one that is not about paths: see `DiffAdds`. It
474
+ * came in from `newCommandNeedsCaller`, a shipped check no pack ever bound — two house packs in two
475
+ * repos did — which on inspection was this check with one condition missing and everything else
476
+ * written a second time.
477
+ *
478
+ * AT WHICH RAIL, and the boundary that comes with it: the COMMAND moment — the Bash rail, before
479
+ * the command runs, which is where an agent's commit can still be stopped and rewritten. git's own
480
+ * commit-msg hook (which would also catch a human committing from an editor) was considered and
481
+ * shelved on the record: the agent is the audience.
482
+ *
483
+ * THREE COMMANDS, AND NOT `git status`. The old script read `git status --porcelain=v1` and carried
484
+ * a fixed-width parser for the answer. That parser reads the world's own format and went with the
485
+ * adapter; what is left here asks three questions whose answers need no format knowledge at all —
486
+ * each one is a list of paths, one per line. It is also strictly better: the porcelain parser had a
487
+ * war story about eating the first character of a path, and there is now no column to slice.
488
+ *
489
+ * …AND IT ASKS THEM ONLY WHEN A PATH CONDITION IS BOUND. A binding that names `diffAdds` alone —
490
+ * the verb ratchets in both house packs do — reads one diff and nothing else. This is not a
491
+ * micro-optimisation: every one of these commands is a question a `.test()` case has to answer, so
492
+ * asking three that cannot change the verdict makes every case for a diff-adds entry carry three
493
+ * lines of git output that mean nothing, and a case full of noise is a case nobody re-reads.
494
+ *
495
+ * Fails safe on every read: not a git repo, or git unavailable, and the check stands aside rather
496
+ * than trapping a commit it cannot judge.
497
+ */
498
+ export declare const commitReason: (options: {
499
+ whenChanged?: readonly string[];
500
+ whenAdded?: readonly string[];
501
+ except?: readonly string[];
502
+ diffAdds?: readonly DiffAdds[];
503
+ token: string;
504
+ }) => Check;
505
+ /**
506
+ * "You changed X this turn but never ran Y afterwards" — the turn-end rule.
507
+ *
508
+ * The actions arrive as harness-neutral facts on ctx, which is what makes this rule drivable by a
509
+ * case and reachable by a replay: a rule that opened the transcript and parsed it would be reading
510
+ * the world three times inside a check. Which tool names count as an edit is the adapter's
511
+ * knowledge and stays there.
512
+ *
513
+ * Stands aside when the moment carried no turn facts at all: there is nothing to judge, and a rule
514
+ * that blocked on the absence of evidence would block every turn on a harness that supplies none.
515
+ */
516
+ export declare const ranSinceEdit: (options: {
517
+ edited: readonly string[];
518
+ mustRun: string;
519
+ }) => Check;
520
+ /**
521
+ * Should a scoped gate skip — is its area untouched?
522
+ *
523
+ * True only when `changed` globs are given AND none of the changed files match, so a file-scoped
524
+ * gate (a type-checker for `site/**`) stays quiet on commits that do not touch its area. No globs,
525
+ * or nothing changed to judge → run it. Fail-safe: never silently skip a gate we cannot scope.
526
+ */
527
+ export declare function skipForChanged(files: readonly string[], changed: readonly string[] | undefined): boolean;
528
+ /**
529
+ * The lines worth showing FIRST when a tool fails — the errors, not the success chatter.
530
+ *
531
+ * Prefer lines that look like a failure; fall back to the tail. Capped, so a blocked commit stays
532
+ * terse: quiet on pass, and on failure the error is up top rather than buried under a progress log.
533
+ */
534
+ export declare function failureExcerpt(output: string, cap?: number): string;
535
+ /**
536
+ * Run a whole-project tool and block on its failure — the one tool-delegated check.
537
+ *
538
+ * Some checks need the whole tree in a consistent state (a transitive import boundary, coverage,
539
+ * the type graph) and cannot run per keystroke, so this belongs at commit or turn-end.
540
+ *
541
+ * IT IS HANDED THE CHANGED SET. A gate spawned with empty argv makes a repo's own script re-scan
542
+ * the whole tree and leaves it unable to name the file that broke it. Here the changed set is an
543
+ * event fact: it scopes the gate (`changed`), and
544
+ * `{files}` in the run string expands to the files themselves, shell-quoted. A run string naming
545
+ * `{files}` with nothing changed does not run at all, because a tool handed an empty file list
546
+ * usually reads that as "do everything".
547
+ */
548
+ export declare const execPasses: (options: {
549
+ run: string;
550
+ changed?: readonly string[];
551
+ }) => Check;
552
+ export type Matcher = Record<string, unknown>;
553
+ export interface NativeRule {
554
+ name: string;
555
+ severity: string;
556
+ comment?: string;
557
+ from?: Matcher;
558
+ to?: Matcher;
559
+ module?: Matcher;
560
+ }
561
+ export interface NativeConfig {
562
+ forbidden: NativeRule[];
563
+ required: NativeRule[];
564
+ options: Record<string, unknown>;
565
+ }
566
+ export interface ForbidEdge {
567
+ readonly from?: string;
568
+ readonly to?: string;
569
+ readonly transitive?: boolean;
570
+ readonly why?: string;
571
+ /** A feature importing a DIFFERENT feature, sideways — named by the layer it applies within. */
572
+ readonly cyclesBetween?: string;
573
+ }
574
+ export interface RequireEdge {
575
+ readonly in: string;
576
+ readonly import: string;
577
+ readonly why?: string;
578
+ }
579
+ /**
580
+ * The dialect an entry writes its fences in.
581
+ *
582
+ * `layers` names sets of path globs, npm packages or node builtins; everything else declares a
583
+ * relationship between them. It exists because dependency-cruiser's native schema is a regex
584
+ * language, and a fence written in regexes is a fence nobody re-reads — the compiled literals of
585
+ * one such config were dead for four days while the guard's own status reported it healthy.
586
+ */
587
+ export interface Dialect {
588
+ /** What to cruise. A GLOB, because a bare directory makes depcruise cruise zero modules. */
589
+ readonly scan: string;
590
+ readonly layers: Readonly<Record<string, readonly string[]>>;
591
+ /** Fail-closed allowlist: a layer may reach ONLY these layers. */
592
+ readonly only?: Readonly<Record<string, readonly string[]>>;
593
+ /** Forbidden edges, each carrying the `why` its violation prints. */
594
+ readonly forbid?: readonly (ForbidEdge | "circular" | "orphans")[];
595
+ /** An inverted fence: modules in a path MUST import a target. */
596
+ readonly require?: readonly RequireEdge[];
597
+ /** Carve-outs — `type-only` above all, since a type import is erased at compile time. */
598
+ readonly except?: readonly string[];
599
+ /** Informational rules that never block. `sdp` = the Stable Dependencies Principle. */
600
+ readonly warn?: readonly string[];
601
+ /** dependency-cruiser's own options, passed through. */
602
+ readonly options?: Readonly<Record<string, unknown>>;
603
+ }
604
+ /**
605
+ * A path glob → a module-path regex for dependency-cruiser (posix module paths).
606
+ *
607
+ * It emits from ../glob.ts's tokens, and that is the whole point: this used to be a SECOND glob
608
+ * translator, and it did not speak the whole dialect. It escaped `{` and `}` into literals, so
609
+ * `src/**` + `/*.{ts,tsx}` — an ordinary layer glob — compiled to a fence that matched nothing,
610
+ * silently, behind a green tick. One parser now, and a construct either has a token every emitter
611
+ * maps or it is not in the dialect at all.
612
+ *
613
+ * The ONE thing this emitter does differently, and why it is an anchor rule rather than a dialect:
614
+ * a TRAILING `**` means "everything under here", so the prefix is emitted and the end is left
615
+ * unanchored. dependency-cruiser matches module paths unanchored at the tail, and every layer in
616
+ * every repo already written against that behaviour must keep meaning what it meant.
617
+ */
618
+ export declare function globToRe(glob: string): string;
619
+ /**
620
+ * A layer entry → a module-path regex or a marker. `node:*` → the builtin marker; a bare name with
621
+ * no slash, no star and no DOT → an npm package under node_modules; anything else → a path glob.
622
+ *
623
+ * THE DOT IS WHAT TELLS A ROOT FILE FROM A PACKAGE, and it is a fix rather than a nicety. Read as a
624
+ * package name, an entry for a file at the repo root — `glob.ts`, `index.ts` — compiled to a matcher
625
+ * for `node_modules/glob.ts`, so the layer covered nothing at all while `flow status` reported the
626
+ * fence armed. It bit twice in this package alone, and only the commit gate caught either.
627
+ *
628
+ * The mirror case is the cost, and it has a spelling already: an npm package whose NAME carries a
629
+ * dot (`socket.io`) now reads as a file, and is named the way a SCOPED package has always had to be
630
+ * named here — as a path under node_modules, behind a leading globstar. One escape, already in use,
631
+ * rather than a second convention.
632
+ */
633
+ export declare function entryToMatcher(entry: string): {
634
+ core?: true;
635
+ path?: string;
636
+ };
637
+ /** Layer names → a depcruise matcher fragment. `path` is an OR-list; builtins become a type. */
638
+ export declare function layersMatcher(names: readonly string[], layers: Readonly<Record<string, readonly string[]>>): Matcher;
639
+ /** The whole dialect → a native `{ forbidden, required, options }` config. */
640
+ export declare function compileDialect(rule: Dialect): NativeConfig;
641
+ /**
642
+ * The one command the depcruise check runs.
643
+ *
644
+ * A separate function because it is the whole of what this check DOES, and a case that stubs the
645
+ * tool has to be able to see it. Three facts about its shape, each load-bearing:
646
+ *
647
+ * The config travels in a QUOTED heredoc. dependency-cruiser takes its rules from a file and
648
+ * nothing else, and a check may not write one — so the command writes it, into a temp dir it
649
+ * removes afterwards. A quoted heredoc is literal: no expansion, no quoting hazard, and the one
650
+ * way it could break (a body line equal to the delimiter) cannot happen, because `JSON.stringify`
651
+ * with no indent emits exactly one line.
652
+ *
653
+ * `npx --no-install`. dependency-cruiser is one of flow's own dependencies, so installing flow
654
+ * installs it; `--no-install` means the command resolves the local copy or fails loudly, and
655
+ * never silently downloads a different version in the middle of a commit.
656
+ *
657
+ * The exit code is the TOOL's. `rm -rf` on the temp dir must not swallow a non-zero exit, which
658
+ * is what the explicit `rc` capture is for.
659
+ */
660
+ export declare function depcruiseCommand(dialect: Dialect): string;
661
+ /** The tool's JSON report → one hit per violation, each naming the fence and its `why`. */
662
+ export declare function depcruiseHits(result: ExecResult): string[];
663
+ /**
664
+ * The generic import-fence runner.
665
+ *
666
+ * A whole-graph check: bind it at commit or turn-end, never per keystroke. It orchestrates and
667
+ * never reimplements — the graph walk is dependency-cruiser's job, and the dialect above exists
668
+ * only so the fences are readable by the person who has to change them.
669
+ */
670
+ export declare const depcruise: (options: Dialect) => Check;
671
+ /** The grammars that need no install. */
672
+ export declare const NATIVE_LANGUAGES: readonly string[];
673
+ /** A resolved grammar, or the sentence saying why there is none. */
674
+ type LangResult = {
675
+ readonly ok: true;
676
+ readonly lang: unknown;
677
+ } | {
678
+ readonly ok: false;
679
+ readonly detail: string;
680
+ };
681
+ /**
682
+ * Resolve a grammar name to something `parse()` accepts.
683
+ *
684
+ * TWO ROUTES. A tier-0 native is free. A published
685
+ * `@ast-grep/lang-<name>` package (sql, python, go, rust…) is registered on first use — its
686
+ * compiled binary lives in node_modules, and `registerDynamicLanguage` is safe to call per grammar.
687
+ *
688
+ * A THIRD ROUTE is deliberately absent: a hand-built grammar declared in an `sgconfig.yml` under a
689
+ * rule library. flow has no library, and resolving one would mean reading a YAML file from a path
690
+ * this layer may not touch. It can arrive the day someone needs it, as a parameter naming a built
691
+ * `.so`, which would be a decision rather than a leftover.
692
+ *
693
+ * An unknown name is a REFUSAL VALUE, and it never falls through to Tsx. The check turns it into a
694
+ * block, which is the fail-loud doctrine at its narrowest and most useful: a silent fall-through is
695
+ * how a rule ends up parsing the wrong grammar, matching nothing, and reporting green forever.
696
+ */
697
+ export declare function resolveLang(name: string): Promise<LangResult>;
698
+ /** Every match of an ast-grep rule in some source, as `line: text` — or why there are none. */
699
+ export declare function astGrepHits(content: string, rule: unknown, language: string): Promise<{
700
+ readonly ok: true;
701
+ readonly hits: string[];
702
+ } | {
703
+ readonly ok: false;
704
+ readonly detail: string;
705
+ }>;
706
+ /**
707
+ * Ban or require a code shape.
708
+ *
709
+ * ast-grep is a Rust tool with a native Node binding, so the would-be file is handed over AS A
710
+ * STRING and the matches come back — no subprocess, no temp file — which is what lets a structural
711
+ * rule run pre-emptively on every edit as well as at commit.
712
+ *
713
+ * `language` is mandatory. A default here is a private fallback the entry cannot show — and the
714
+ * wrong parser mis-parses a file and then matches nothing, silently, which is the failure this
715
+ * package exists to delete.
716
+ */
717
+ export declare const astGrep: (options: {
718
+ rule: unknown;
719
+ language: string;
720
+ }) => Check;
721
+ /** The four dialects a case can be written in — one per kind of event fact. */
722
+ export type CaseDialect = "command" | "write" | "commit" | "turn-end";
723
+ /**
724
+ * Read a case once: which dialect it speaks, the event facts it carries, and its recorded world.
725
+ *
726
+ * ONE ladder over the union, and it is the only one. There were three — this, a `caseMoment` that
727
+ * asked the same questions to answer half of it, and a `momentSpeaks` that hard-coded the same
728
+ * pairing a fourth time — so adding a fifth case shape meant finding three places, and missing one
729
+ * of them would have produced a case that ran at the wrong moment rather than an error.
730
+ */
731
+ export declare function readCase(c: Case): {
732
+ readonly dialect: CaseDialect;
733
+ readonly facts: Partial<Ctx>;
734
+ readonly world: CaseWorld;
735
+ };
736
+ /** Which moment's dialect a case speaks. */
737
+ export declare function caseMoment(c: Case): Moment;
738
+ /**
739
+ * Build the ctx one case becomes, at one moment.
740
+ *
741
+ * BOTH halves are the language layer's, and deliberately the same two calls the live engine makes:
742
+ * `cannedWorld` turns what the case wrote down into the three capabilities, and `makeCtx` assembles
743
+ * the ctx from those and the event's facts. A case is a recorded WORLD handed to one assembler, not
744
+ * a second kind of ctx — the moment either of those is written twice, a green case stops being
745
+ * evidence about the live rail, which is the whole value of cases existing.
746
+ */
747
+ export declare function cannedCtx(moment: Moment, c: Case, unanswered: Unanswered[]): Ctx;
748
+ /**
749
+ * Every ctx one case becomes for one entry — one, or one per staged file it is scoped to.
750
+ *
751
+ * The plural is the whole of the fan-out fix, and the world is built ONCE and shared across the
752
+ * subjects: a case records one world, and a per-file rule reading it four times must see the same
753
+ * four answers the live gate would.
754
+ */
755
+ export declare function caseCtxs(entry: LoadedEntry, moment: Moment, c: Case, unanswered: Unanswered[]): Promise<Ctx[]>;
756
+ /** How one case came out. */
757
+ export interface CaseResult {
758
+ readonly entry: string;
759
+ readonly expect: "pass" | "block";
760
+ /** The case's index within its own list — what `flow test` prints to point at one. */
761
+ readonly index: number;
762
+ readonly ok: boolean;
763
+ /** Empty when it went as declared; otherwise the one sentence saying what happened instead. */
764
+ readonly detail: string;
765
+ }
766
+ /**
767
+ * One case, driven.
768
+ *
769
+ * Three ways it fails besides the obvious one, and each was a real hole:
770
+ *
771
+ * The case speaks a dialect the entry has no rail for. A write rule tested with a command string
772
+ * proves nothing about the rule and looks exactly like proof.
773
+ *
774
+ * The check throws. A crashing check is a failing check everywhere in flow; here it is the case's
775
+ * failure, named, which is the earliest anyone could have found out.
776
+ *
777
+ * The check reached the world and the case did not answer. Left alone, that case would pass on a
778
+ * fiction — the canned world's silence read as a clean exit or an empty file.
779
+ */
780
+ export declare function runCase(entry: LoadedEntry, check: Check, c: Case, expect: "pass" | "block", index: number): Promise<CaseResult>;
781
+ /** The sentence an unanswered reach earns — it names what to add to the case. */
782
+ export declare function unansweredText(missing: readonly Unanswered[]): string;
783
+ /**
784
+ * Every bound entry's cases, driven — what `flow test` is.
785
+ *
786
+ * Disabled entries are skipped: they are listed with their reason and never run, so asking them to
787
+ * prove anything is how an opt-out becomes cheaper to delete than to explain. Breadcrumbs carry no
788
+ * check and have nothing to prove. Everything else must have cases at all, which the load already
789
+ * refused it without.
790
+ */
791
+ export declare function runCases(entries: readonly LoadedEntry[]): Promise<CaseResult[]>;
792
+ export {};