@jawache/flow 0.1.2 → 0.2.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.
@@ -33,9 +33,20 @@ export declare const textBan: (options: {
33
33
  * (which is right for a file, where a hit must be able to name its line) every one of those
34
34
  * patterns matches nothing, silently: the rule loads, counts, and cannot catch the thing it
35
35
  * names.
36
+ *
37
+ * WITHOUT ITS HEREDOC BODIES (`elideHeredocs`), by default. A heredoc is text fed to a program,
38
+ * and a python script or a note that names a banned command is not a use of it: matched whole,
39
+ * `python3 - <<'PY'` scripts were refused for naming a recipe in a string. Only the bodies go —
40
+ * the newlines the patterns above span are kept — and a body a shell reads stays, because it is
41
+ * commands.
42
+ *
43
+ * `matchHeredocs: true` matches the whole command instead. A rule about what a command is FED —
44
+ * the text of a commit message given on stdin, `git commit -F - <<'EOF'` — has to see the body,
45
+ * and would be blind to that message without it.
36
46
  */
37
47
  export declare const banCommands: (options: {
38
48
  ban: readonly string[];
49
+ matchHeredocs?: boolean;
39
50
  }) => Check;
40
51
  /**
41
52
  * A `banCommands` pattern for: a command that writes permanent prose, with a BACKTICK inside a
@@ -86,6 +97,24 @@ export declare const SUBSTITUTION_MESSAGE: string;
86
97
  export declare const protectedPath: (options: {
87
98
  existingOnly?: boolean;
88
99
  }) => Check;
100
+ /**
101
+ * Only the listed categories may change a file: a write or a delete by any other actor is refused.
102
+ *
103
+ * It reads `ctx.actor` — the categories the acting session wears, recognised from what the host
104
+ * wrote about it, never from a claim it made — and nothing about the file. A session wearing none
105
+ * of `writers` is refused whatever it wrote, and one wearing none at all is refused too: "no
106
+ * category" is not "every category".
107
+ *
108
+ * `.for(…)` on an entry says which actors a RULE applies to; this says which actors may WRITE. Both
109
+ * read the same categories, and they answer different questions — a rule for a checker says what a
110
+ * checker may not do, this says that everybody but a builder may not write.
111
+ *
112
+ * An empty `writers` is refused at load: it would refuse every change by everyone, which is a
113
+ * frozen worktree, not a rule about who writes it.
114
+ */
115
+ export declare const oneWriter: (options: {
116
+ writers: readonly string[];
117
+ }) => Check;
89
118
  /**
90
119
  * A file must have a required companion on disk — every `src/domain/**` needs its `.test.ts`.
91
120
  *
@@ -241,6 +270,44 @@ export declare const changeTogether: (options: {
241
270
  * aimed at a directory called HEAD.
242
271
  */
243
272
  export declare const GIT_VALUE_OPTS: ReadonlySet<string>;
273
+ /** A heredoc's opener: its delimiter, whether `<<-` strips leading tabs, and where the opener ends. */
274
+ export interface HeredocOpen {
275
+ readonly delim: string;
276
+ readonly tabs: boolean;
277
+ readonly end: number;
278
+ }
279
+ /**
280
+ * The first heredoc opener anywhere in a line, quoted context or not — the one definition of "this
281
+ * line carries a heredoc". A here-string (`<<<`) is stepped over whole, so its tail is not read as
282
+ * one.
283
+ */
284
+ export declare function firstHeredoc(s: string): HeredocOpen | null;
285
+ /**
286
+ * Where a `$(` closes, honouring quotes, nesting and any heredoc inside it — a body is text, so an
287
+ * apostrophe or a `)` in it closes nothing. -1 when it never closes.
288
+ */
289
+ export declare function closeParen(s: string, from: number): number;
290
+ /** Where a double-quoted run closes — a `"` inside a `$(…)` in it does not close it. -1 when it never does. */
291
+ export declare function dquoteEnd(s: string, from: number): number;
292
+ /**
293
+ * THE COMMAND LINE WITHOUT ITS HEREDOC BODIES — what a command ban matches unless the rule sets
294
+ * `matchHeredocs`.
295
+ *
296
+ * A heredoc is text fed to a program on its standard input: a python script, a file being written,
297
+ * a note. The words of a banned command inside it are not a use of that command, and matching
298
+ * them refused `python3 - <<'PY'` scripts for naming a recipe in a string. Every body is removed
299
+ * (its terminator line stays, so the opener still ends), whether the delimiter is quoted or not,
300
+ * and wherever the heredoc sits —
301
+ * inside a `$(…)` or a quoted argument too. EVERYTHING ELSE IS LEFT AS WRITTEN — newlines
302
+ * included, because the patterns that police a commit message span them.
303
+ *
304
+ * One exception keeps a body: a heredoc fed to a SHELL (`bash`, `sh`, `zsh`, `eval`, or a pipeline
305
+ * into one), whose body is commands, and a banned one in it will run.
306
+ *
307
+ * Best-effort, and it errs toward matching: from a word whose quote never closes, the line is
308
+ * returned as written, so a ban still sees everything it saw before.
309
+ */
310
+ export declare function elideHeredocs(command: string): string;
244
311
  /**
245
312
  * A shell-ish tokeniser: quotes respected (so a commit message mentioning `git commit` is not a
246
313
  * second command), operators emitted as their own tokens.
@@ -266,19 +333,6 @@ export interface GitInvocation {
266
333
  * operator — so a `git` inside a quoted message or as an argument to something else is not one.
267
334
  */
268
335
  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
336
  /**
283
337
  * The message a `git commit` in this command line carries, or null when there is no judgeable
284
338
  * commit in it at all. Null is the "not our business" answer and every caller passes on it.
@@ -296,7 +350,10 @@ export declare function heredocBody(command: string): string | null;
296
350
  *
297
351
  * git commit -m MSG · -mMSG · --message MSG · --message=MSG judged
298
352
  * 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
353
+ * git commit -F - <<'EOF' … EOF (the commit reads stdin) judged, via the heredoc body
354
+ * python3 - <<'PY' … PY; git commit -F msg.txt NOT judged — the heredoc is
355
+ * another command's script, and
356
+ * the message is on disk
300
357
  * git commit -F path/to/file NOT judged — the message is on
301
358
  * disk, and reading it is a read
302
359
  * of the world this layer does not
@@ -266,8 +266,16 @@ export declare const FLOW_DIR = ".flow";
266
266
  export declare const FLOW_GITIGNORE = "# flow's own state \u2014 telemetry and session marks, never committed.\n*\n";
267
267
  /** The log schema this build writes, stamped into every session file's first row. */
268
268
  export declare const LOG_VERSION = 1;
269
- /** Every kind a log row may be. Closed — a reader that meets another word has met corruption. */
270
- export declare const ROW_KINDS: readonly ["meta", "tool", "breadcrumb", "guardrail", "compaction", "run"];
269
+ /**
270
+ * Every kind a log row may be. Closed — a reader that meets another word has met corruption.
271
+ *
272
+ * `read` and `write` are what went ROUND the tool rows: a file a shell command read by name, and a
273
+ * file a call changed that the call did not name — a heredoc, a `sed -i`, a script. A `tool` row
274
+ * says a call happened; these say what it did to the tree, so the coverage and lead numbers count
275
+ * work done through the shell as well as through Edit. `mismatch` is the host's own list of the
276
+ * files a command changed disagreeing with flow's diff: a fact to read, never an input to a rule.
277
+ */
278
+ export declare const ROW_KINDS: readonly ["meta", "tool", "breadcrumb", "guardrail", "compaction", "run", "read", "write", "mismatch"];
271
279
  export type RowKind = (typeof ROW_KINDS)[number];
272
280
  export declare function flowDir(root: string): string;
273
281
  /** A session id becomes a filename, so it is held to a filename's charset. */
@@ -2,7 +2,7 @@ export { guardrail, breadcrumb, type Sentence, type EntrySpec, type GuardrailSpe
2
2
  export { definePack, pack, override, defineConfig, type Pack, type PackDefinition, type EntryRef, type RefTarget, type FlowConfig, type Binding, type PackBinding, type OverrideBinding, type ConfigSentence, } from "./language/domain.ts";
3
3
  export { defineCheck, withFaults, faultsOf, type Check, type Ctx, type World, type Verdict, type ExecResult, type TurnAction, type Case, type CaseWorld, type Cases, } from "./language/domain.ts";
4
4
  export { defineCategory, type Category, type Classifier, type SessionFacts } from "./language/domain.ts";
5
- export { astGrep, banCommands, canonicalFiles, changeTogether, commitReason, depcruise, execPasses, jsonInvariant, protectedPath, ranSinceEdit, siblingExists, symbolsInSibling, textBan, type CanonOptions, type ChangeGroup, type Dialect, type Layer, type DiffAdds, type ForbidEdge, type JsonAssert, type RequireEdge, } from "./checks/domain.ts";
5
+ export { astGrep, banCommands, canonicalFiles, changeTogether, commitReason, depcruise, execPasses, jsonInvariant, oneWriter, protectedPath, ranSinceEdit, siblingExists, symbolsInSibling, textBan, type CanonOptions, type ChangeGroup, type Dialect, type Layer, type DiffAdds, type ForbidEdge, type JsonAssert, type RequireEdge, } from "./checks/domain.ts";
6
6
  export { substitutionInProse, SUBSTITUTION_MESSAGE } from "./checks/domain.ts";
7
7
  export { commitMessage, gitDirPrefix, gitInvocations, givesReason, quoteArg, tokenizeCommand, type GitInvocation, } from "./checks/domain.ts";
8
8
  export { spawnedAs, type SpawnRecipe } from "./engine/domain.ts";
@@ -141,6 +141,12 @@ export interface Ctx {
141
141
  readonly staged?: readonly string[] | undefined;
142
142
  /** turn-end: what the actor did this turn, in order. */
143
143
  readonly turn?: readonly TurnAction[] | undefined;
144
+ /**
145
+ * Every moment: the category names that match the session making this change — the categories
146
+ * some bound entry names, whose recognizer said yes to the host-written evidence about it. Empty
147
+ * when it matches none, which is not "every category".
148
+ */
149
+ readonly actor: readonly string[];
144
150
  exec(cmd: string): Promise<ExecResult>;
145
151
  readonly fs: {
146
152
  read(path: string): Promise<string>;
@@ -263,16 +269,20 @@ export type Case =
263
269
  string | {
264
270
  readonly command: string;
265
271
  readonly world?: CaseWorld;
272
+ readonly actor?: readonly string[];
266
273
  } | {
267
274
  readonly path: string;
268
275
  readonly content: string;
269
276
  readonly world?: CaseWorld;
277
+ readonly actor?: readonly string[];
270
278
  } | {
271
279
  readonly staged: readonly string[];
272
280
  readonly world?: CaseWorld;
281
+ readonly actor?: readonly string[];
273
282
  } | {
274
283
  readonly actions: readonly TurnAction[];
275
284
  readonly world?: CaseWorld;
285
+ readonly actor?: readonly string[];
276
286
  };
277
287
  /** The cases an entry carries: what must pass it, and what must be blocked by it. */
278
288
  export interface Cases {
@@ -538,6 +548,19 @@ export interface Settings {
538
548
  * it two ways.
539
549
  */
540
550
  readonly grammars?: readonly Grammar[];
551
+ /**
552
+ * Globs for the IGNORED paths the snapshot records as well. The snapshot is git's view of the
553
+ * tree — tracked files, and untracked ones git does not ignore — so a shell write to an ignored
554
+ * file is never seen unless a glob here brings it back. `.env` is the usual one.
555
+ */
556
+ readonly snapshotInclude?: readonly string[];
557
+ /**
558
+ * What is put back when a rule refuses a write that has already landed: `"refused"` (the default)
559
+ * writes back only the files a rule refused, `"all"` every file that call changed. While another
560
+ * agent's call overlaps it, only the refused files are put back whatever this says — that call's
561
+ * writes can be in the same diff.
562
+ */
563
+ readonly revert?: "refused" | "all";
541
564
  }
542
565
  /**
543
566
  * One hand-built tree-sitter grammar, as `registerDynamicLanguage` wants it.
@@ -5,9 +5,9 @@
5
5
  * least possible cognitive load, and the agent wants almost nothing in context. So `docs/` has
6
6
  * exactly two doors and everything inside follows its door's rules.
7
7
  *
8
- * @setup A `docs/` folder holding exactly `user/` and `agent/`, each with its own index —
9
- * `docs/user/index.html` for people and `docs/agent/README.md` as a list of pointers. The two
10
- * guardrails below refuse anything else at the top of the folder, so a repo with a different shape
8
+ * @setup A `docs/` folder holding exactly `user/` and `agent/`, each with its own index — an index
9
+ * file under `docs/user/` for people and `docs/agent/README.md` as a list of pointers. The
10
+ * guardrail below refuses anything else at the top of the folder, so a repo with a different shape
11
11
  * either moves its pages or does not bind this pack.
12
12
  */
13
13
  export interface Doors {
@@ -45,5 +45,4 @@ export declare const docs: import("../index.ts").Pack<{
45
45
  * drawer, and neither audience's rules can be applied to what is in it.
46
46
  */
47
47
  docsShape: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "on" | "description", "write" | "commit">;
48
- noMarkdownInUserDocs: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "on" | "description", "write" | "commit">;
49
48
  }, Doors | undefined>;
@@ -17,6 +17,20 @@ export declare const builder: import("../index.ts").Category;
17
17
  * a frontmatter list somebody can widen.
18
18
  */
19
19
  export declare const checker: import("../index.ts").Category;
20
+ /** The rungs that may be named as writers — a checker is the rung that never writes. */
21
+ export type Writer = "builder" | "parent";
22
+ /** What a repo may say about how it runs the lifecycle. Every field is optional. */
23
+ export interface Rungs {
24
+ /**
25
+ * The rungs allowed to change files in a worktree, turning on ONE WRITER: a write or a delete by
26
+ * any other actor is refused — an Edit before it lands, anything else put back after.
27
+ *
28
+ * OPTIONAL, and with no list there is no entry at all. One writer is for a worktree several
29
+ * agents share with one builder in it; in a repo where a helper agent of any other kind is
30
+ * expected to write, it would refuse that helper's every change, so it is off until a repo asks.
31
+ */
32
+ readonly writers?: readonly Writer[];
33
+ }
20
34
  /**
21
35
  * The WORK LIFECYCLE's rungs: who is acting, and what each rung may do.
22
36
  *
@@ -38,12 +52,14 @@ export declare const checker: import("../index.ts").Category;
38
52
  * so in a repo that does not run the lifecycle they are five rules that can never fire.
39
53
  * @adopt Bind it only in a repo whose tasks are actually run through the lifecycle. The three rung
40
54
  * rules are scoped to categories this pack defines, so they judge nobody until a session is
41
- * classified as a builder or a checker — a human at a keyboard is neither.
55
+ * classified as a builder or a checker — a human at a keyboard is neither. Name `writers` to let
56
+ * only those rungs change files in the worktree.
42
57
  */
43
58
  export declare const work: import("../index.ts").Pack<{
59
+ builderWrites?: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "description", "write" | "delete">;
44
60
  planIsTheParents: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "for" | "description", "command">;
45
61
  checkersDoNotWrite: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "for" | "description", "write">;
46
62
  ticksAreTheChilds: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "for" | "description", "command">;
47
63
  noAgentInboxItems: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "description", "command">;
48
64
  noShellSubstitutionInProse: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "description", "command">;
49
- }, undefined>;
65
+ }, Rungs | undefined>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jawache/flow",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "description": "Guardrails and breadcrumbs as code — packs you import, a config that typechecks, one adapter per harness.",
5
5
  "license": "MIT",
6
6
  "repository": {