@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.
- package/LICENCE +21 -0
- package/README.md +309 -0
- package/dist/flow.mjs +167 -0
- package/dist/index.mjs +18 -0
- package/dist/packs.mjs +116 -0
- package/dist/types/adapter/archive.d.ts +45 -0
- package/dist/types/adapter/claude.d.ts +157 -0
- package/dist/types/adapter/domain.d.ts +1112 -0
- package/dist/types/adapter/product.d.ts +10 -0
- package/dist/types/checks/domain.d.ts +792 -0
- package/dist/types/engine/domain.d.ts +747 -0
- package/dist/types/engine/state.d.ts +104 -0
- package/dist/types/errors.d.ts +14 -0
- package/dist/types/flow.d.ts +1 -0
- package/dist/types/glob.d.ts +53 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/language/domain.d.ts +641 -0
- package/dist/types/packs/docs.d.ts +49 -0
- package/dist/types/packs/fcis.d.ts +71 -0
- package/dist/types/packs/flow.d.ts +50 -0
- package/dist/types/packs/git.d.ts +107 -0
- package/dist/types/packs/index.d.ts +10 -0
- package/dist/types/packs/justfile.d.ts +83 -0
- package/dist/types/packs/node.d.ts +39 -0
- package/dist/types/packs/secrets.d.ts +117 -0
- package/dist/types/packs/tdd.d.ts +52 -0
- package/dist/types/packs/typescript.d.ts +101 -0
- package/dist/types/packs/work.d.ts +49 -0
- package/dist/types/version.d.ts +2 -0
- package/package.json +61 -0
|
@@ -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>;
|