@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,49 @@
1
+ /**
2
+ * Two documentation audiences, one folder, nothing replicated.
3
+ *
4
+ * Every repo documents for exactly two readers who want opposite things: the end user wants the
5
+ * least possible cognitive load, and the agent wants almost nothing in context. So `docs/` has
6
+ * exactly two doors and everything inside follows its door's rules.
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
11
+ * either moves its pages or does not bind this pack.
12
+ */
13
+ export interface Doors {
14
+ /**
15
+ * The documentation folder, as a path from the repo root. Defaults to `docs`.
16
+ *
17
+ * Both guardrails and both breadcrumbs are scoped to it, so a repo whose pages live in
18
+ * `documentation/` names that here and keeps every rule.
19
+ */
20
+ readonly root?: string;
21
+ /**
22
+ * Filenames allowed to sit LOOSE at the top of that folder, beside the two doors. Defaults to
23
+ * none — every file belongs behind `user/` or `agent/`.
24
+ *
25
+ * A repo that keeps a `README.md` at the top of its docs folder names it here.
26
+ */
27
+ readonly allow?: readonly string[];
28
+ }
29
+ export declare const docs: import("../index.ts").Pack<{
30
+ /**
31
+ * Without it, an agent writing under `docs/` has no way to know there are two audiences. User
32
+ * pages fill with rationale nobody asked for, and agent pages fill with copies of what the code
33
+ * already says, which go stale silently while still reading as true.
34
+ */
35
+ docs: import("../index.ts").BreadcrumbSentence<"text" | "at" | "on" | "description", "touch">;
36
+ /**
37
+ * Without it, every user page is written in whatever style that session had, and the docs read
38
+ * as ten authors. Every source it cites is a public standard — Diátaxis, the Google developer
39
+ * style guide — so nothing in it is one repo's taste.
40
+ */
41
+ userDocsStyle: import("../index.ts").BreadcrumbSentence<"text" | "at" | "on" | "description", "touch">;
42
+ /**
43
+ * Without it, `docs/` grows a third folder nobody chose — notes, scratch, drafts — and the two
44
+ * doors stop being a rule. A folder with no declared audience is how a docs folder becomes a
45
+ * drawer, and neither audience's rules can be applied to what is in it.
46
+ */
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
+ }, Doors | undefined>;
@@ -0,0 +1,71 @@
1
+ /** Where a repo's pure code lives, and what proves it. Stated by the repo; nothing is defaulted. */
2
+ export interface Convention {
3
+ /** Every pure FILE — what the rails judge, one glob per shape the repo actually uses. */
4
+ readonly files: readonly string[];
5
+ /** Every pure HOME — where the teaching breadcrumb fires, and what the coverage gate watches. */
6
+ readonly homes: readonly string[];
7
+ /** The recipe that runs the coverage gate. */
8
+ readonly coverage: string;
9
+ /**
10
+ * ONE PURE FILE, spelled out — a path both `files` and `homes` match.
11
+ *
12
+ * `pureCovered` and `newPureFileNeedsReason` narrow inside their CHECK rather than through
13
+ * `.on(…)`, so their cases have to name a concrete path, and a path the pack invented would fall
14
+ * outside the repo's own convention and prove nothing. Asked for rather than computed: turning a
15
+ * caller's glob into a concrete path is arithmetic that does not belong in a pack. A repo knows
16
+ * one of its own pure files; nothing else does.
17
+ */
18
+ readonly example: string;
19
+ /**
20
+ * Where a pure file's test sits, as a template over the file's own path. Defaults to
21
+ * `{dir}/{name}.test.ts`.
22
+ *
23
+ * `{dir}` is the file's folder and `{name}` its basename without the extension.
24
+ * `pureHasTest` demands it exists and `pureExportsTested` reads it, so a repo whose tests live
25
+ * in `__tests__/` or end in `.spec.ts` states that here rather than turning both rules off.
26
+ */
27
+ readonly sibling?: string;
28
+ /**
29
+ * What a new pure file may be without owing a reason: a test. Defaults to a glob matching any
30
+ * file whose name carries `.test.`, at any depth.
31
+ *
32
+ * It is `newPureFileNeedsReason`'s exemption, and the `example` path with `.test` before its
33
+ * extension is what proves it, so the two are read together.
34
+ */
35
+ readonly tests?: string;
36
+ }
37
+ /**
38
+ * Functional Core, Imperative Shell (Bernhardt, "Boundaries", SCNA 2012).
39
+ *
40
+ * The one-stop shop for running FCIS in a repo: the model, the rules that hold both sides honest,
41
+ * the tests that prove the pure side, and the gate on growing it.
42
+ *
43
+ * EVERY RULE KEYS OFF ONE IDENTITY CONVENTION — the single way a repo names its pure code — and
44
+ * the pack DEMANDS it rather than guessing: `Convention` is a mandatory typed parameter, so a repo
45
+ * that binds this pack without saying where its pure code lives does not compile.
46
+ *
47
+ * The import FENCE is not here and cannot be: its layers ARE a repo's architecture. What the pack
48
+ * ships is the half that is true everywhere (no shell IMPORTS in pure); the fence lives in the
49
+ * guarded repo's own house pack, where its layers are.
50
+ *
51
+ * @setup A pure home the repo can name in globs, one real pure file to point at, and a coverage
52
+ * recipe — all four parameters. The sibling-test trio reads them, so a repo that cannot say where
53
+ * its pure code lives cannot bind this pack, by design.
54
+ * @adopt Expect the coverage gate and the sibling-test rules to fire on the first commit that
55
+ * touches pure code; both are scoped to the home you named, so the way to narrow them is to narrow
56
+ * that, never to disable the rule.
57
+ */
58
+ export declare const fcis: import("../index.ts").Pack<{
59
+ /**
60
+ * Without it, the split is a rule with no model behind it: an agent moves a side effect out of a
61
+ * pure file because a guardrail refused it, learns nothing, and puts the next one back.
62
+ */
63
+ fcis: import("../index.ts").BreadcrumbSentence<"text" | "at" | "on" | "description", "touch">;
64
+ noSideEffectsInPure: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "on" | "ignore" | "description", "write" | "commit">;
65
+ noShellInPure: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "on" | "ignore" | "description", "write" | "commit">;
66
+ noThrowInPure: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "on" | "ignore" | "description", "write" | "commit">;
67
+ pureHasTest: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "on" | "ignore" | "description", "write" | "commit">;
68
+ pureExportsTested: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "on" | "ignore" | "description", "write" | "commit">;
69
+ pureCovered: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "description", "commit">;
70
+ newPureFileNeedsReason: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "description", "command">;
71
+ }, Convention>;
@@ -0,0 +1,50 @@
1
+ /** The one fact this pack cannot know: where this repo keeps the packs its config imports. */
2
+ export interface Home {
3
+ /**
4
+ * Every glob covering a pack file `flow.config.ts` imports — one entry per folder a repo keeps
5
+ * its own packs in, and `[]` in a repo whose rules are all written inline in the config.
6
+ *
7
+ * MANDATORY, and empty is a real answer rather than the absence of one. An unstated list is a
8
+ * list nobody reviews, and here it decides what two rules watch: the nudge when you edit the
9
+ * guard, and the refusal to delete it. Both name it and neither defaults.
10
+ *
11
+ * The engine derives the REPAIR surface from the config's imports directly (`configSurface`),
12
+ * because it is asked in the broken state and can only read text. This is the same fact asked
13
+ * at a moment where the config loads, so it is stated once, here, and typed.
14
+ */
15
+ readonly packs: readonly string[];
16
+ /**
17
+ * The harness's own permission surface — the files an agent's tools and permissions are declared
18
+ * in. Defaults to Claude Code's: `.claude/settings.json`, `.claude/settings.local.json`,
19
+ * `.claude/agents/**` and `.claude/skills/**`.
20
+ *
21
+ * The edit nudge fires on these as well as on the guard itself, because a tool list and a hook
22
+ * registration decide what any rule here can ever see. A repo on another harness names its own
23
+ * files; a repo with none passes `[]`.
24
+ */
25
+ readonly hostSurface?: readonly string[];
26
+ }
27
+ /**
28
+ * flow's own orientation and self-protection, as a pack any repo binds.
29
+ *
30
+ * Every guarded repo wants these three: a note saying what is steering you, a nudge when you edit
31
+ * the steering itself, and a refusal when a command would delete it.
32
+ *
33
+ * @setup A `flow.config.ts` at the repo root and a folder for the packs the repo writes itself —
34
+ * whatever it is called, named here as the `packs` parameter. `flow init` writes both.
35
+ * @adopt Bind it first, before any other pack: it is the one every guarded repo wants whether or
36
+ * not it agrees with a single other opinion in this package.
37
+ */
38
+ export declare const flow: import("../index.ts").Pack<{
39
+ /**
40
+ * Without it, an agent works in a guarded repo without knowing it is guarded: a refusal arrives
41
+ * with no model of where it came from, and the config that produced it is just another file.
42
+ */
43
+ orientation: import("../index.ts").BreadcrumbSentence<"text" | "at" | "description", "session">;
44
+ /**
45
+ * Without it, the steering layer gets edited like any other file — a rule loosened in passing,
46
+ * a hook rewritten mid-task — and the guard changes without anybody deciding that it should.
47
+ */
48
+ editingTheGuardrails: import("../index.ts").BreadcrumbSentence<"text" | "at" | "on" | "description", "touch">;
49
+ noDeleteGuardrails: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "on" | "description", "delete">;
50
+ }, Home>;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * The working-tree paths a command line would discard OUTRIGHT, or [] when it is safe to run.
3
+ *
4
+ * Deliberately narrow — false negatives beat blocking real work. Every path here is one the
5
+ * command line itself declares: the `--` pathspec form of checkout, the restore forms that touch
6
+ * the tree, and `reset --hard` / `clean -f`, whose target is the tree itself.
7
+ *
8
+ * The BARE form of checkout — `git checkout <thing>` with no `--` — is not here and cannot be:
9
+ * whether `<thing>` is a path or a branch is a fact about the repo, not about the string, and this
10
+ * function is pure. `checkoutBarePaths` below names the candidates and the check asks git.
11
+ */
12
+ export declare function discardPaths(command: string): string[];
13
+ /**
14
+ * The words a bare `git checkout` might be aiming at — candidates, not answers.
15
+ *
16
+ * `git checkout <path>` discards that path exactly as `git checkout -- <path>` does, and until F3
17
+ * only the second was refused. The comment above this rule claimed the narrowing was deliberate
18
+ * ("branch switching passes"), and it was — but it bought that pass by missing the form a person
19
+ * actually types, which the F3 builder proved on itself by running `git checkout adapter/claude.ts`
20
+ * over an hour of uncommitted work and watching the guard allow it.
21
+ *
22
+ * An invocation carrying `--` is skipped: `discardPaths` already has it, and everything before the
23
+ * `--` there is a tree-ish by definition.
24
+ */
25
+ export declare function checkoutBarePaths(command: string): string[];
26
+ /** Of some porcelain lines, the paths with real uncommitted work. */
27
+ export declare function dirtyIn(porcelain: string, includeUntracked: boolean): string[];
28
+ /** Does this header line speak Conventional Commits for the given type set? */
29
+ export declare function isConventional(header: string, types: readonly string[]): boolean;
30
+ /**
31
+ * A release commit's header — the ONE shape allowed to write a version by hand.
32
+ *
33
+ * `release: …` · `release(scope): …` · `release(scope)!: …` · `chore(release): …`, and a `chore`
34
+ * ONLY with a release-ish scope.
35
+ *
36
+ * `chore` REQUIRES its release scope: a bare `chore:` licensing a hand-edited version is the one
37
+ * thing this rule exists to refuse. A `release(scope)` scope is free text — the word "release" is
38
+ * not demanded inside the parentheses, so `release(cli): 1.0.0` passes.
39
+ */
40
+ export declare function isReleaseCommit(message: string): boolean;
41
+ /**
42
+ * Does this diff change the version VALUE?
43
+ *
44
+ * Values on both sides, compared — never "an added line mentions version". Adding a key after the
45
+ * version rewrites its line (it gains a comma), and a rule that fired on every reformat is a rule
46
+ * whose message people stop reading.
47
+ */
48
+ export declare function changesVersion(diff: string): boolean;
49
+ /** What this pack cannot know: which recipe cuts a release, and which file carries the version. */
50
+ export interface Release {
51
+ /**
52
+ * The file the release writes the version into. Defaults to `package.json`.
53
+ *
54
+ * `noHandEditedVersion` reads whatever this names out of the commit's own diff, so any manifest
55
+ * works there. `versionIsSemver` reads it as JSON, so a repo whose manifest is TOML or YAML names
56
+ * the file here and disables that one entry.
57
+ */
58
+ readonly versionFile?: string;
59
+ /**
60
+ * The type words a commit header may use. Defaults to the eleven of Conventional Commits plus
61
+ * `release`: feat · fix · docs · style · refactor · perf · test · build · ci · chore · revert ·
62
+ * release.
63
+ *
64
+ * A repo with its own vocabulary states the WHOLE list, never an addition — the refusal prints
65
+ * what it was given, so a half-list would refuse a type it then failed to mention.
66
+ */
67
+ readonly types?: readonly string[];
68
+ /**
69
+ * The recipe that computes the version from the history, writes the changelog, stamps the
70
+ * version file and tags — for example `just release` or `npm run release`.
71
+ *
72
+ * MANDATORY rather than defaulted, by the same rule tdd's suite recipe follows: a default is a
73
+ * rule that names the wrong command in every repo that spells it differently, and it names it in
74
+ * the one sentence a person reads at the moment they are blocked.
75
+ */
76
+ readonly release: string;
77
+ }
78
+ /**
79
+ * Conventional commit headers, computed versions, and nothing destroys uncommitted work.
80
+ *
81
+ * The `node/` sub-group is release discipline spelled in package.json, and package.json only.
82
+ *
83
+ * @setup A release path the repo really has, named as the `release` parameter: the recipe that
84
+ * computes the next version from the commit headers, writes the changelog and tags. The version
85
+ * rules refuse a hand-edited version in favour of it, so a repo without one has nowhere to send a
86
+ * blocked reader.
87
+ * @adopt Bind it in a repo that already writes conventional commits, or expect the header rule to
88
+ * refuse the first few commits while everyone re-learns the format — that is the rule working, and
89
+ * `flow status` will show it firing.
90
+ */
91
+ export declare const git: import("../index.ts").Pack<{
92
+ /**
93
+ * Without it, the commit surface is learned by trial and refusal: an agent writes a header the
94
+ * changelog cannot read, reaches for a force push, or discards uncommitted work with a command
95
+ * that has no undo.
96
+ */
97
+ orientation: import("../index.ts").BreadcrumbSentence<"text" | "at" | "description", "session">;
98
+ noAiAttributionInCommits: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "description", "command">;
99
+ noGitDiscard: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "description", "command">;
100
+ noForcePush: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "description", "command">;
101
+ conventionalCommitFormat: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "description", "command">;
102
+ noShellSubstitutionInProse: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "description", "command">;
103
+ node: {
104
+ versionIsSemver: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "on" | "description", "write" | "commit">;
105
+ noHandEditedVersion: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "description", "command">;
106
+ };
107
+ }, Release>;
@@ -0,0 +1,10 @@
1
+ export { docs } from "./docs.ts";
2
+ export { fcis, type Convention } from "./fcis.ts";
3
+ export { flow, type Home } from "./flow.ts";
4
+ export { git, type Release } from "./git.ts";
5
+ export { justfile, type Catalogue } from "./justfile.ts";
6
+ export { node } from "./node.ts";
7
+ export { secrets, type Seam } from "./secrets.ts";
8
+ export { tdd, type Suite } from "./tdd.ts";
9
+ export { typescript, type Gates } from "./typescript.ts";
10
+ export { work, builder, checker, parent } from "./work.ts";
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Recipes with no `[doc(…)]` attribute, as `{ name, line }` (1-based).
3
+ *
4
+ * `[private]` recipes are SKIPPED, and that is a fix rather than a nicety: `just --list` hides
5
+ * them, so a private recipe is not in the catalogue and cannot owe the catalogue a description.
6
+ * The message has claimed this exemption from the day it was written and the walk never honoured
7
+ * it — a rule that refuses what its own sentence promises to allow is the worst kind, because the
8
+ * reader does the thing they were told to do and is refused anyway.
9
+ */
10
+ export declare function undocumentedRecipes(text: string, exempt?: readonly string[]): {
11
+ name: string;
12
+ line: number;
13
+ }[];
14
+ export interface Catalogue {
15
+ /**
16
+ * Recipe names that owe no `[doc("…")]`. Usually empty, and MANDATORY so that it is empty on
17
+ * the record rather than by default — an exemption list nobody stated is one nobody reviews.
18
+ *
19
+ * A parameter rather than an override, because `exempt` is the CHECK's option and an override
20
+ * speaks only the sentence keys (at · for · on · ignore · message · disabled).
21
+ *
22
+ * `[private]` recipes need no entry — `just --list` hides them, so the check skips them.
23
+ */
24
+ readonly exempt: readonly string[];
25
+ /** The folder holding the code behind the recipes. Defaults to `tools`. */
26
+ readonly tools?: string;
27
+ /**
28
+ * Commands this repo has a recipe for, as a map from the command's pattern to the recipe that
29
+ * replaces it — `{ "npm run build": "just build" }`.
30
+ *
31
+ * OPTIONAL, and with no map there is no entry at all: which raw command a repo has wrapped is a
32
+ * fact only that repo has, and a rule with an empty list would be one more armed entry matching
33
+ * nothing. The patterns are regular expressions over the command line, so `npx vitest\\b` is a
34
+ * pattern and `npx vitest` is one too.
35
+ *
36
+ * The catalogue's whole promise is that the recipe is the thing to reach for; a breadcrumb says
37
+ * so once a session, and this refuses the raw command by name for the ones that matter.
38
+ */
39
+ readonly recipes?: Readonly<Record<string, string>>;
40
+ }
41
+ /**
42
+ * The justfile is the repo's TOOL CATALOGUE.
43
+ *
44
+ * The frame: the justfile plays the role MCP tools play elsewhere — the discovery surface an agent
45
+ * can reach for with confidence. `just --list` is the manifest and each `[doc]` string is a tool
46
+ * description. Every entry here is the same arrow pointing at it: the orientation says look here
47
+ * first, `node/noPackageScripts` closes the decoy surface agents habitually read, and `justfileDocs`
48
+ * keeps the manifest readable.
49
+ *
50
+ * @install `just` itself — `brew install just`, or see https://just.systems. Every rule here reads
51
+ * a `justfile` at the repo root and none of them shells out, so nothing else is needed.
52
+ * @setup A `justfile` at the root whose every non-private recipe carries a `[doc("…")]`, and a
53
+ * `package.json` whose `scripts` block holds nothing but `//`-prefixed comment keys. A repo with
54
+ * live npm scripts moves them into recipes first, or does not bind this pack.
55
+ */
56
+ export declare const justfile: import("../index.ts").Pack<{
57
+ useTheRecipe?: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "description", "command">;
58
+ /**
59
+ * Without it, an agent discovers commands from package.json, from memory, or from a README that
60
+ * has drifted — and reaches for a tool this repo does not have, or hand-runs the chain a recipe
61
+ * already spells correctly.
62
+ */
63
+ orientation: import("../index.ts").BreadcrumbSentence<"text" | "at" | "description", "session">;
64
+ /**
65
+ * Without it, everything in the tools folder is treated the same way: one-shots get promoted
66
+ * into the catalogue until `just --list` is noise, or a genuinely repeatable tool is left with
67
+ * no recipe pointing at it and nobody finds it again.
68
+ */
69
+ toolsHome: import("../index.ts").BreadcrumbSentence<"text" | "at" | "on" | "description", "touch">;
70
+ node: {
71
+ /**
72
+ * Without it, package.json grows a second command surface — the one an agent reads FIRST, out
73
+ * of habit — and the two drift until the catalogue is no longer the truth about this repo.
74
+ */
75
+ noPackageScripts: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "on" | "description", "write" | "commit">;
76
+ };
77
+ /**
78
+ * Without it, `just --list` falls back to the last comment line above a recipe, which for a
79
+ * multi-line comment block is a mid-sentence fragment — a help screen assembled by accident, and
80
+ * a tool nobody can choose from its description.
81
+ */
82
+ justfileDocs: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "on" | "description", "write" | "commit">;
83
+ }, Catalogue>;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The dependency names whose entry differs between two package.json texts.
3
+ *
4
+ * Unparseable JSON on either side names every dependency both sides mention: a file being reshaped
5
+ * into something else is a change we cannot rule out. An empty string means the file did not exist
6
+ * on that side.
7
+ */
8
+ export declare function movedDependencies(beforeText: string, afterText: string): string[];
9
+ /**
10
+ * package.json discipline: reach for a maintained package, say why, pin it, move the lockfile with
11
+ * it.
12
+ *
13
+ * The dependencies breadcrumb's POLARITY is deliberate. It does not read as "adding a dependency is
14
+ * a risk"; it reads as "check whether it is already solved" — because the code we hand-roll to
15
+ * avoid a package has no users, no reviewers and no fixes coming, which is the larger risk most of
16
+ * the time.
17
+ *
18
+ * @setup A lockfile the repo commits — `package-lock.json`, `pnpm-lock.yaml`, `bun.lock`,
19
+ * `bun.lockb` or `yarn.lock`; `lockfileInStep` recognises all five and asks for whichever one the
20
+ * repo already has.
21
+ */
22
+ export declare const node: import("../index.ts").Pack<{
23
+ /**
24
+ * Without it, the reflex is to hand-roll rather than to look, and a repo accumulates private
25
+ * versions of solved problems — code with no users, no reviewers and no fixes coming.
26
+ */
27
+ dependencies: import("../index.ts").BreadcrumbSentence<"text" | "at" | "on" | "description", "touch">;
28
+ /**
29
+ * Without it, a dependency arrives with no record of what else was considered, and the next
30
+ * person to ask "why do we depend on this" has only the package name to go on.
31
+ */
32
+ newDependencyNeedsReason: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "description", "command">;
33
+ /**
34
+ * Without it, a dependency moves in one commit and the lockfile in another (or never), so a
35
+ * fresh clone installs something nobody tested and the two files disagree about what this repo
36
+ * depends on.
37
+ */
38
+ lockfileInStep: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "description", "commit">;
39
+ }, undefined>;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * What this pack cannot know: how this repo spells the env files it commits, and the two commands
3
+ * that seal them and run against them.
4
+ *
5
+ * The two command names default to dotenvx's own, which is the honest floor — a repo that has
6
+ * wrapped them in recipes (`just dx`, `just env-encrypt`) hands those over instead, and every
7
+ * sentence a stuck reader meets then names a command that repo really has.
8
+ */
9
+ export interface Seam {
10
+ /**
11
+ * The value prefixes that stay plaintext in a committed env file. Defaults to `["PUBLIC_"]`.
12
+ *
13
+ * A public value is one a bundler inlines at build time, so sealing it would break the build for
14
+ * nothing. `DOTENV_PUBLIC_KEY` is always allowed beside these — it is dotenvx's own header, not a
15
+ * repo's choice.
16
+ */
17
+ readonly publicPrefixes?: readonly string[];
18
+ /**
19
+ * The files that must never be committed. Defaults to `[".env.keys", ".env.*.local",
20
+ * ".dev.vars"]`.
21
+ *
22
+ * These are also what `envEncrypted` ignores: a file nobody commits cannot carry a committed
23
+ * plaintext value, and the two lists being one is what stops them drifting apart.
24
+ */
25
+ readonly localFiles?: readonly string[];
26
+ /** The committed env files, as a glob. Defaults to `.env*`. */
27
+ readonly envFiles?: string;
28
+ /**
29
+ * Extra credential shapes to scan staged content for, added to the six this pack ships (Stripe
30
+ * live keys, AWS access keys, private key headers, GitHub and Slack tokens). Defaults to none.
31
+ *
32
+ * One regular expression per shape. A vendor whose token this pack has never heard of is a fact
33
+ * only the repo has.
34
+ */
35
+ readonly extraSecretShapes?: readonly string[];
36
+ /**
37
+ * The repo's wrapper around dotenvx's runner, called as `<dx> <env> <cmd>` — it decrypts and
38
+ * injects, and every env-dependent command goes through it. Defaults to dotenvx itself, spelled
39
+ * `dotenvx run -f .env.<env> --`, which takes the env file rather than an environment name.
40
+ *
41
+ * A wrapper is worth having and this pack cannot guess its name: `just dx` is one fleet's
42
+ * spelling, and written in here it would send every reader of a refusal to a command their repo
43
+ * does not have, in the one sentence they read at the moment they are stuck.
44
+ */
45
+ readonly dx?: string;
46
+ /**
47
+ * Seals every env file in place, leaving the public prefixes plaintext. Defaults to dotenvx's
48
+ * own `dotenvx encrypt`, excluding the prefixes above by name.
49
+ */
50
+ readonly encrypt?: string;
51
+ }
52
+ /**
53
+ * Env files committed encrypted, decrypted through one seam, and the local-only files kept out of
54
+ * git.
55
+ *
56
+ * The orientation is at SESSION, and that is the whole design. Models arrive with a trained
57
+ * aversion to env files — usually correct, and reinforced by permission denials — which is exactly
58
+ * wrong for the dotenvx model, where the committed files are encrypted and reading them is the
59
+ * expected way to work. A touch breadcrumb cannot fix that: the sessions where an agent silently
60
+ * avoids env work are the sessions it never fires in. So it is said up front, every time.
61
+ *
62
+ * Three tiers, and every value lives in exactly one: committed-and-encrypted (`.env*`), local
63
+ * ephemera that must never be committed (`.env.keys`, `.dev.vars`, `.env.*.local`), and runtime
64
+ * secrets set per environment on the platform.
65
+ *
66
+ * @install dotenvx — https://dotenvx.com. It is what encrypts the committed files and what runs a
67
+ * command with them decrypted, and with nothing else in place every sentence below names it
68
+ * directly. The two command parameters are for a repo that has wrapped it in recipes of its own.
69
+ * @setup `.env.keys`, `.dev.vars` and `.env.*.local` in `.gitignore` — the rules below refuse them
70
+ * in a commit, and an ignore entry is what stops anyone reaching that refusal.
71
+ *
72
+ * Nothing else is required: unbound, the sentences name dotenvx itself. THIS PACK SHIPS NO FILE
73
+ * and nothing is copied into a repo. What a repo gains by wrapping the tool is one place to say
74
+ * which env file each environment means — REFERENCE below, not a file to match: it is a seam that
75
+ * works as written, to copy and then spell your own way. The environments are the repo's own fact,
76
+ * and so is the file list the sealing loop walks.
77
+ *
78
+ * ```just
79
+ * [doc("Run any command with this environment's secrets injected — the ONE env seam. Usage: just dx dev npm run build")]
80
+ * dx env +cmd:
81
+ * dotenvx run {{ if env == "prod" { "-f .env.production -f .env" } else { "-f .env.development.local -f .env" } }} -- {{cmd}}
82
+ *
83
+ * [doc("Seal every env file in place — everything encrypted except PUBLIC_* (bundlers inline those at build).")]
84
+ * env-encrypt:
85
+ * #!/usr/bin/env bash
86
+ * set -euo pipefail
87
+ * for f in .env .env.development.local .env.production; do
88
+ * if [ -f "$f" ]; then dotenvx encrypt -f "$f" -ek 'PUBLIC_*'; fi
89
+ * done
90
+ *
91
+ * [doc("List the variable NAMES in an env file — no values, nothing decrypted. Usage: just env-names .env")]
92
+ * env-names file=".env":
93
+ * grep -oE '^[A-Za-z_][A-Za-z0-9_]*=' {{file}} | tr -d '=' | sort
94
+ * ```
95
+ *
96
+ * Bound, that repo hands over `dx: "just dx"` and `encrypt: "just env-encrypt"`. The third job is
97
+ * nobody's parameter — no sentence in this pack names it — and it is here because an env seam
98
+ * wants it.
99
+ * @adopt A repo with no `.env*` at all binds this for `noSecretsInCommits` alone, which is worth
100
+ * having in a repo that has never held a secret, and turns `envEncrypted` off BY NAME in its
101
+ * config with the reason on the record — an entry sitting dead is not the same as one opted out of.
102
+ */
103
+ export declare const secrets: import("../index.ts").Pack<{
104
+ /**
105
+ * Without it, an agent avoids the env files on instinct — the trained reflex is right nearly
106
+ * everywhere else — and either works around the seam or asks a human for values that are
107
+ * committed, encrypted, three feet away.
108
+ */
109
+ orientation: import("../index.ts").BreadcrumbSentence<"text" | "at" | "description", "session">;
110
+ /**
111
+ * Without it, one plaintext value lands in a committed `.env` and the whole model is gone: the
112
+ * files are committed on the promise that everything in them is sealed.
113
+ */
114
+ envEncrypted: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "on" | "ignore" | "description", "write" | "commit">;
115
+ noKeysFileInCommits: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "on" | "description", "commit">;
116
+ noSecretsInCommits: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "on" | "ignore" | "description", "commit">;
117
+ }, Seam | undefined>;
@@ -0,0 +1,52 @@
1
+ /** What this pack cannot know: the suite recipe, and how this repo spells a test file. */
2
+ export interface Suite {
3
+ /**
4
+ * The recipe that runs the deterministic suite — for example `just test-commit` or `npm test`.
5
+ *
6
+ * MANDATORY rather than defaulted: which command is the gate is a repo's fact, and a default is
7
+ * a rule that runs the wrong suite in every repo whose gate has another name.
8
+ */
9
+ readonly run: string;
10
+ /**
11
+ * Every spelling of a test file. Defaults to the seven this fleet uses — `.test.` and `.spec.`
12
+ * with `.ts`, `.tsx`, `.js` and `.mjs`.
13
+ *
14
+ * The strategy breadcrumb fires on them and the four `vitestIdioms` rules judge them, so a repo
15
+ * that keeps its tests in `__tests__/` names that here rather than re-scoping five entries.
16
+ */
17
+ readonly tests?: readonly string[];
18
+ }
19
+ /**
20
+ * Test-first artefacts exist; tests test behaviour, not implementation; the suite gates the commit.
21
+ *
22
+ * `commitRunsTests` takes the suite recipe as a mandatory PARAMETER rather than defaulting to
23
+ * `just test`: which command is the deterministic gate is a repo's fact, and a default is a rule
24
+ * that runs the wrong suite in every repo whose gate has another name.
25
+ *
26
+ * The `vitest/` sub-group holds FRAMEWORK spellings rather than testing principles: a repo on
27
+ * another runner disables the four of them without losing the artefacts or the gate.
28
+ *
29
+ * @install vitest, unless the four `vitestIdioms` entries are disabled — every one of them reads a
30
+ * vitest spelling.
31
+ * @setup One recipe that runs the deterministic suite, named as the `run` parameter. It is what
32
+ * the commit gate runs, so it has to be the whole suite and it has to be green.
33
+ * @adopt A repo on another runner binds the pack and disables `vitest` and the `vitestIdioms`
34
+ * group by name, keeping the strategy breadcrumb and the commit gate — that split is why the
35
+ * framework rules are a sub-group rather than four loose entries.
36
+ */
37
+ export declare const tdd: import("../index.ts").Pack<{
38
+ /**
39
+ * Without it, testing effort goes where it is easiest to write rather than where confidence is
40
+ * cheapest — a thick unit layer pinned to implementation details, which then refuses every
41
+ * behaviour-preserving refactor.
42
+ */
43
+ testingStrategy: import("../index.ts").BreadcrumbSentence<"text" | "at" | "on" | "description", "touch">;
44
+ vitest: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "on" | "description", "write" | "commit">;
45
+ vitestIdioms: {
46
+ noMockInternal: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "on" | "description", "write" | "commit">;
47
+ noOnlyInTests: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "on" | "description", "write" | "commit">;
48
+ noNetworkStubs: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "on" | "description", "write" | "commit">;
49
+ noEmptyTest: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "on" | "description", "write" | "commit">;
50
+ };
51
+ commitRunsTests: import("../index.ts").GuardrailSentence<"at" | "check" | "message" | "test" | "description", "commit">;
52
+ }, Suite>;