baesian 0.0.0-stage → 1.0.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.
Files changed (69) hide show
  1. package/LICENSE.md +105 -0
  2. package/README.md +217 -2
  3. package/dist/agents.js +249 -0
  4. package/dist/api.js +13 -0
  5. package/dist/bin.js +14 -0
  6. package/dist/ci.js +82 -0
  7. package/dist/cite.js +66 -0
  8. package/dist/cli.js +831 -0
  9. package/dist/client/assets/alegreya-cyrillic-ext-wght-italic-DzWRXZb0.woff2 +0 -0
  10. package/dist/client/assets/alegreya-cyrillic-ext-wght-normal-CN0-BfWD.woff2 +0 -0
  11. package/dist/client/assets/alegreya-cyrillic-wght-italic-Dhrv2xfk.woff2 +0 -0
  12. package/dist/client/assets/alegreya-cyrillic-wght-normal-CU5lb38f.woff2 +0 -0
  13. package/dist/client/assets/alegreya-greek-ext-wght-italic-CoUI9EEu.woff2 +0 -0
  14. package/dist/client/assets/alegreya-greek-ext-wght-normal-C8PPRmwJ.woff2 +0 -0
  15. package/dist/client/assets/alegreya-greek-wght-italic-CVt9-Jr4.woff2 +0 -0
  16. package/dist/client/assets/alegreya-greek-wght-normal-BPx8yWcZ.woff2 +0 -0
  17. package/dist/client/assets/alegreya-latin-ext-wght-italic-BIuo_2sR.woff2 +0 -0
  18. package/dist/client/assets/alegreya-latin-ext-wght-normal-o8axeAGu.woff2 +0 -0
  19. package/dist/client/assets/alegreya-latin-wght-italic-Bxkcgmge.woff2 +0 -0
  20. package/dist/client/assets/alegreya-latin-wght-normal-BG9rTJo4.woff2 +0 -0
  21. package/dist/client/assets/alegreya-vietnamese-wght-italic-BAPuWgnQ.woff2 +0 -0
  22. package/dist/client/assets/alegreya-vietnamese-wght-normal-DOV9JYup.woff2 +0 -0
  23. package/dist/client/assets/archivo-latin-ext-wdth-normal-7khWdh9v.woff2 +0 -0
  24. package/dist/client/assets/archivo-latin-wdth-normal-DY7AcnAa.woff2 +0 -0
  25. package/dist/client/assets/archivo-vietnamese-wdth-normal-rJmnGBSt.woff2 +0 -0
  26. package/dist/client/assets/index-9yf70Vqv.css +1 -0
  27. package/dist/client/assets/index-Dwg9e0YR.js +15 -0
  28. package/dist/client/index.html +15 -0
  29. package/dist/facts.js +278 -0
  30. package/dist/folder-storage.js +71 -0
  31. package/dist/frontmatter.js +38 -0
  32. package/dist/gate-paths.js +77 -0
  33. package/dist/gate.js +237 -0
  34. package/dist/git.js +100 -0
  35. package/dist/globs.js +120 -0
  36. package/dist/hookblock.js +146 -0
  37. package/dist/hooks.js +911 -0
  38. package/dist/ids.js +23 -0
  39. package/dist/login.js +301 -0
  40. package/dist/mcp.js +303 -0
  41. package/dist/merges.js +849 -0
  42. package/dist/modes.js +245 -0
  43. package/dist/notify.js +91 -0
  44. package/dist/ops.js +381 -0
  45. package/dist/passkey.js +86 -0
  46. package/dist/personal.js +163 -0
  47. package/dist/practice-answers.js +42 -0
  48. package/dist/proposals.js +197 -0
  49. package/dist/quote.js +28 -0
  50. package/dist/report.js +261 -0
  51. package/dist/routing.js +411 -0
  52. package/dist/schema.js +1178 -0
  53. package/dist/server.js +514 -0
  54. package/dist/similarity.js +45 -0
  55. package/dist/sshsig.js +159 -0
  56. package/dist/state.js +135 -0
  57. package/dist/storage.js +61 -0
  58. package/dist/store.js +2259 -0
  59. package/dist/style.js +408 -0
  60. package/dist/sync.js +797 -0
  61. package/dist/trailers.js +121 -0
  62. package/dist/trust.js +222 -0
  63. package/dist/usage-fields.js +161 -0
  64. package/dist/vault.js +849 -0
  65. package/dist/verify.js +443 -0
  66. package/dist/version.js +70 -0
  67. package/dist/wait.js +449 -0
  68. package/dist/wikilinks.js +25 -0
  69. package/package.json +71 -4
@@ -0,0 +1,15 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <!-- viewport-fit=cover: a phone with a notch gives the page its whole screen, and style.css keeps clear of the
6
+ notch with its safe areas. resizes-content: the on-screen keyboard shrinks the page, rather than covering it. -->
7
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content" />
8
+ <title>Baesian</title>
9
+ <script type="module" crossorigin src="/assets/index-Dwg9e0YR.js"></script>
10
+ <link rel="stylesheet" crossorigin href="/assets/index-9yf70Vqv.css">
11
+ </head>
12
+ <body>
13
+ <div id="root"></div>
14
+ </body>
15
+ </html>
package/dist/facts.js ADDED
@@ -0,0 +1,278 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { basename } from "node:path";
3
+ import { parse as parseToml } from "smol-toml";
4
+ import { DEFAULT_IGNORE, pathsOf } from "./gate-paths.js";
5
+ import { matcher } from "./globs.js";
6
+ import { onlyBaesianBlock } from "./hookblock.js";
7
+ /**
8
+ * Ask livedocs for exported-signature changes (D56). Baesian doesn't parse code itself: livedocs
9
+ * already has tested Python and TypeScript parsers. Without livedocs, the detector warns and is off.
10
+ */
11
+ export function livedocsSignatures(cwd, args) {
12
+ try {
13
+ const out = execFileSync("livedocs", ["signatures", ...args, "--json", "--repo", cwd], {
14
+ cwd,
15
+ encoding: "utf8",
16
+ stdio: ["ignore", "pipe", "pipe"],
17
+ maxBuffer: 64 << 20,
18
+ });
19
+ return { changes: JSON.parse(out) };
20
+ }
21
+ catch (e) {
22
+ const err = e;
23
+ if (err.code === "ENOENT") {
24
+ return { changes: [], warning: "your preferences name public modules (gate_api), but livedocs isn't installed, so API changes weren't checked (uv tool install livedocs)" };
25
+ }
26
+ const why = (err.stderr ?? err.message ?? "").toString().trim().split("\n").at(-1);
27
+ return { changes: [], warning: `livedocs signatures failed, so API changes weren't checked: ${why}` };
28
+ }
29
+ }
30
+ /** git's empty tree: the "before" side of a repository's first commit. */
31
+ export const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
32
+ function gitRaw(cwd, args) {
33
+ return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 64 << 20 });
34
+ }
35
+ function show(cwd, spec) {
36
+ try {
37
+ return gitRaw(cwd, ["show", spec]);
38
+ }
39
+ catch {
40
+ return undefined;
41
+ }
42
+ }
43
+ function hasRev(cwd, rev) {
44
+ try {
45
+ gitRaw(cwd, ["rev-parse", "--verify", "-q", `${rev}^{commit}`]);
46
+ return true;
47
+ }
48
+ catch {
49
+ return false;
50
+ }
51
+ }
52
+ function parseNameStatus(z) {
53
+ const parts = z.split("\0").filter((p) => p !== "");
54
+ const out = [];
55
+ for (let i = 0; i < parts.length;) {
56
+ const status = parts[i++];
57
+ if (status.startsWith("R") || status.startsWith("C")) {
58
+ const oldPath = parts[i++];
59
+ out.push({ status: status[0], path: parts[i++], oldPath });
60
+ }
61
+ else {
62
+ out.push({ status: status[0], path: parts[i++] });
63
+ }
64
+ }
65
+ return out;
66
+ }
67
+ /** What `git commit` is about to record: the index against HEAD (or the empty tree). */
68
+ export function stagedSnapshot(cwd) {
69
+ const head = hasRev(cwd, "HEAD");
70
+ const base = head ? "HEAD" : EMPTY_TREE;
71
+ return {
72
+ changes: () => parseNameStatus(gitRaw(cwd, ["diff", "--cached", "--name-status", "-M", "-z", base])),
73
+ before: (path) => (head ? show(cwd, `HEAD:${path}`) : undefined),
74
+ after: (path) => show(cwd, `:${path}`),
75
+ apiChanges: () => livedocsSignatures(cwd, ["--cached"]),
76
+ };
77
+ }
78
+ /** One commit against its first parent, for CI (M3b). */
79
+ export function commitSnapshot(cwd, commit) {
80
+ const parent = hasRev(cwd, `${commit}^`) ? `${commit}^` : undefined;
81
+ return {
82
+ changes: () => parseNameStatus(gitRaw(cwd, ["diff", "--name-status", "-M", "-z", parent ?? EMPTY_TREE, commit])),
83
+ before: (path) => (parent ? show(cwd, `${parent}:${path}`) : undefined),
84
+ after: (path) => show(cwd, `${commit}:${path}`),
85
+ apiChanges: () => livedocsSignatures(cwd, ["--rev", commit]),
86
+ };
87
+ }
88
+ export { DEFAULT_IGNORE, DEFAULT_INFRA, DEFAULT_SCHEMA } from "./gate-paths.js";
89
+ /** Breaking changes only (D58): a new export is additive, so it's a two-way door. */
90
+ const BREAKING = new Set(["signature", "removed"]);
91
+ function shorten(text, max = 90) {
92
+ const one = text.replace(/\s+/g, " ").trim();
93
+ return one.length > max ? `${one.slice(0, max - 1)}…` : one;
94
+ }
95
+ // ---------------------------------------------------------------- dependencies (D55)
96
+ /** PEP 503 normalisation, so `Pydantic_Core` and `pydantic-core` are the same package. */
97
+ export function normalisePython(name) {
98
+ return name.toLowerCase().replace(/[-_.]+/g, "-");
99
+ }
100
+ /** The name at the start of a PEP 508 requirement: `httpx[http2]>=0.27; python_version>"3.8"` → `httpx`. */
101
+ export function requirementName(req) {
102
+ const m = /^\s*([A-Za-z0-9][A-Za-z0-9._-]*)/.exec(req);
103
+ return m ? normalisePython(m[1]) : undefined;
104
+ }
105
+ const NODE_RUNTIME = ["dependencies", "peerDependencies", "optionalDependencies"];
106
+ function nodeDeps(text) {
107
+ const pkg = JSON.parse(text);
108
+ const names = new Set();
109
+ for (const section of NODE_RUNTIME) {
110
+ const deps = pkg[section];
111
+ if (deps && typeof deps === "object")
112
+ for (const n of Object.keys(deps))
113
+ names.add(n);
114
+ }
115
+ return names;
116
+ }
117
+ function pythonProjectDeps(text) {
118
+ const doc = parseToml(text);
119
+ const names = new Set();
120
+ const add = (req) => {
121
+ const n = typeof req === "string" ? requirementName(req) : undefined;
122
+ if (n)
123
+ names.add(n);
124
+ };
125
+ const project = doc.project ?? {};
126
+ for (const req of project.dependencies ?? [])
127
+ add(req);
128
+ for (const extra of Object.values(project["optional-dependencies"] ?? {})) {
129
+ for (const req of extra ?? [])
130
+ add(req);
131
+ }
132
+ // Poetry's main group is runtime; its other groups (and PEP 735 dependency-groups) are dev (D55).
133
+ for (const name of Object.keys(doc.tool?.poetry?.dependencies ?? {})) {
134
+ if (name.toLowerCase() !== "python")
135
+ names.add(normalisePython(name));
136
+ }
137
+ return names;
138
+ }
139
+ function requirementsDeps(text) {
140
+ const names = new Set();
141
+ for (const raw of text.split(/\r?\n/)) {
142
+ const line = raw.replace(/(^|\s)#.*$/, "").trim();
143
+ if (!line || line.startsWith("-"))
144
+ continue; // options, -r includes, -e editables
145
+ const n = requirementName(line);
146
+ if (n)
147
+ names.add(n);
148
+ }
149
+ return names;
150
+ }
151
+ function manifestParser(path) {
152
+ const name = basename(path);
153
+ if (name === "package.json")
154
+ return nodeDeps;
155
+ if (name === "pyproject.toml")
156
+ return pythonProjectDeps;
157
+ if (/^requirements.*\.txt$/.test(name) && !/(dev|test|lint|doc)/i.test(name))
158
+ return requirementsDeps;
159
+ return undefined;
160
+ }
161
+ function dependencyFacts(snap, change, warnings) {
162
+ const parse = manifestParser(change.path);
163
+ if (!parse)
164
+ return [];
165
+ const read = (text, side) => {
166
+ if (text === undefined)
167
+ return new Set();
168
+ try {
169
+ return parse(text);
170
+ }
171
+ catch {
172
+ warnings.push(`couldn't parse the ${side} version of ${change.path}; its dependencies weren't checked`);
173
+ return undefined;
174
+ }
175
+ };
176
+ const before = read(change.status === "A" ? undefined : snap.before(change.oldPath ?? change.path), "old");
177
+ const after = read(change.status === "D" ? undefined : snap.after(change.path), "new");
178
+ if (!before || !after)
179
+ return [];
180
+ const facts = [];
181
+ for (const n of after) {
182
+ if (!before.has(n))
183
+ facts.push({ id: `dep:${n}`, kind: "dep", path: change.path, detail: `added to ${change.path}` });
184
+ }
185
+ for (const n of before) {
186
+ if (!after.has(n))
187
+ facts.push({ id: `dep:${n}`, kind: "dep", path: change.path, detail: `removed from ${change.path}` });
188
+ }
189
+ return facts;
190
+ }
191
+ // ---------------------------------------------------------------- the repo's link to the service (B-w4g6c)
192
+ /** The committed config, where a repo names the service and project it syncs with (P16 section 8). */
193
+ export const CONFIG_PATH = ".baesian/config.json";
194
+ /** What ties a repo to a project on the service: where its cards go (B-w4g6c), and which history is the repo's (B-5gnea). */
195
+ const LINK_FIELDS = ["service", "project", "first_commit"];
196
+ /**
197
+ * Changing where a repo's cards are sent is a gate fact, like a CI file (B-w4g6c): `infra:.baesian/config.json` when a
198
+ * commit changes the config's service, project or first commit, linking the repo first included. The rest of
199
+ * `.baesian/` stays out of the gate's sight (`DEFAULT_IGNORE`), and so does the rest of the config: what the gate looks
200
+ * at is no longer there, but in the preference files, which a commit can't make count (B-qkek1).
201
+ */
202
+ function linkFact(snap, change, warnings) {
203
+ if (change.path !== CONFIG_PATH && change.oldPath !== CONFIG_PATH)
204
+ return undefined;
205
+ const read = (text, side) => {
206
+ if (text === undefined)
207
+ return {};
208
+ try {
209
+ const v = JSON.parse(text);
210
+ return typeof v === "object" && v !== null && !Array.isArray(v) ? v : {};
211
+ }
212
+ catch {
213
+ warnings.push(`couldn't parse the ${side} version of ${CONFIG_PATH}; whether it changes the repo's service or project wasn't checked`);
214
+ return undefined;
215
+ }
216
+ };
217
+ // Renamed onto the config's path, it had no config before; renamed away, it has none after.
218
+ const wasThere = change.status !== "A" && (change.oldPath === undefined || change.oldPath === CONFIG_PATH);
219
+ const before = read(wasThere ? snap.before(CONFIG_PATH) : undefined, "old");
220
+ const after = read(change.path === CONFIG_PATH && change.status !== "D" ? snap.after(CONFIG_PATH) : undefined, "new");
221
+ if (!before || !after)
222
+ return undefined;
223
+ const moved = LINK_FIELDS.filter((k) => before[k] !== after[k]);
224
+ if (!moved.length)
225
+ return undefined;
226
+ const detail = after.project === undefined ? `unlinked the repo from ${String(before.service)} (project ${String(before.project)})` : before.project === undefined ? `linked the repo to ${String(after.service)} as project ${String(after.project)}` : `changed the repo's ${moved.join(", ")} in ${CONFIG_PATH}`;
227
+ return { id: `infra:${CONFIG_PATH}`, kind: "infra", path: CONFIG_PATH, detail };
228
+ }
229
+ // ---------------------------------------------------------------- detection
230
+ const VERB = { A: "added", M: "changed", D: "deleted", R: "renamed", C: "copied", T: "changed" };
231
+ export function detectFacts(snap, opts = {}) {
232
+ const ignored = matcher([...DEFAULT_IGNORE, ...pathsOf(opts, "ignore")]);
233
+ const isSchema = matcher(pathsOf(opts, "schema"));
234
+ const isInfra = matcher(pathsOf(opts, "infra"));
235
+ const warnings = [];
236
+ const facts = new Map();
237
+ const add = (f) => {
238
+ if (!facts.has(f.id))
239
+ facts.set(f.id, f);
240
+ };
241
+ for (const change of snap.changes()) {
242
+ // Checked before what's ignored: `.baesian/` is, and a repo's own ignores mustn't hide where its cards go (B-w4g6c).
243
+ const link = linkFact(snap, change, warnings);
244
+ if (link)
245
+ add(link);
246
+ if (ignored(change.path))
247
+ continue;
248
+ const verb = VERB[change.status] ?? "changed";
249
+ const what = change.oldPath ? `${verb} ${change.oldPath} → ${change.path}` : `${verb} ${change.path}`;
250
+ // A rename counts if either end is in scope: moving a migration out of migrations/ is a decision too.
251
+ const either = (test) => test(change.path) || (change.oldPath !== undefined && test(change.oldPath));
252
+ for (const f of dependencyFacts(snap, change, warnings))
253
+ add(f);
254
+ if (either(isSchema))
255
+ add({ id: `schema:${change.path}`, kind: "schema", path: change.path, detail: what });
256
+ // Baesian's own block, as `baesian init` writes it, isn't a decision anyone made (B-g6ywj); any other hook change is.
257
+ const ownBlock = (change.status === "A" || change.status === "M") && onlyBaesianBlock(change.path, snap.before(change.path), snap.after(change.path));
258
+ if (either(isInfra) && !ownBlock)
259
+ add({ id: `infra:${change.path}`, kind: "infra", path: change.path, detail: what });
260
+ }
261
+ const api = pathsOf(opts, "api");
262
+ if (api.length && snap.apiChanges) {
263
+ const isApi = matcher(api);
264
+ const result = snap.apiChanges();
265
+ if (result.warning)
266
+ warnings.push(result.warning);
267
+ for (const c of result.changes) {
268
+ if (!BREAKING.has(c.kind) || !isApi(c.path) || ignored(c.path))
269
+ continue;
270
+ const detail = c.kind === "removed"
271
+ ? `removed from ${c.path}`
272
+ : `signature changed in ${c.path}${c.now ? `: ${shorten(c.now)}` : ""}`;
273
+ add({ id: `api:${c.qualname}`, kind: "api", path: c.path, detail });
274
+ }
275
+ }
276
+ return { facts: [...facts.values()].sort((a, b) => a.id.localeCompare(b.id)), warnings };
277
+ }
278
+ //# sourceMappingURL=facts.js.map
@@ -0,0 +1,71 @@
1
+ import { existsSync, linkSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { randomSuffix } from "./ids.js";
4
+ import { checkPath } from "./storage.js";
5
+ /** A write in progress: a temporary file beside the one it becomes. Never listed. */
6
+ const isTemp = (name) => name.endsWith(".tmp");
7
+ /**
8
+ * The live layer as a folder on your machine (B-0ag7h): `.git/baesian/`, shared by every worktree of a clone (D34), or a
9
+ * commit's record read as a layer in CI. A write goes to a temporary file first, so no other process reads half a file.
10
+ */
11
+ export class FolderStorage {
12
+ dir;
13
+ constructor(dir) {
14
+ this.dir = dir;
15
+ }
16
+ /** The file or folder at a layer path. */
17
+ at(path) {
18
+ return checkPath(path, true) === "" ? this.dir : join(this.dir, ...path.split("/"));
19
+ }
20
+ read(path) {
21
+ try {
22
+ return readFileSync(this.at(path));
23
+ }
24
+ catch (e) {
25
+ const code = e.code;
26
+ if (code === "ENOENT" || code === "ENOTDIR")
27
+ return undefined;
28
+ throw e;
29
+ }
30
+ }
31
+ list(dir) {
32
+ const at = this.at(dir);
33
+ return existsSync(at) ? readdirSync(at).filter((f) => !isTemp(f)) : [];
34
+ }
35
+ /**
36
+ * Written whole to a temporary file first, then linked into place, which fails if the name is taken: so another process
37
+ * never reads it half-written. An exclusive create alone made the file visible empty for a moment, which the server's
38
+ * four-writer test caught once the gate made filing read more of the layer. Where hard links aren't supported, it falls
39
+ * back to the exclusive create.
40
+ */
41
+ create(path, bytes) {
42
+ const at = this.at(path);
43
+ mkdirSync(dirname(at), { recursive: true });
44
+ const tmp = `${at}.${randomSuffix()}.tmp`;
45
+ writeFileSync(tmp, bytes, { flag: "wx" });
46
+ try {
47
+ linkSync(tmp, at);
48
+ }
49
+ catch (e) {
50
+ const code = e.code;
51
+ if (code !== "EPERM" && code !== "ENOTSUP" && code !== "EXDEV" && code !== "ENOSYS")
52
+ throw e;
53
+ writeFileSync(at, bytes, { flag: "wx" });
54
+ }
55
+ finally {
56
+ rmSync(tmp, { force: true });
57
+ }
58
+ }
59
+ /** Written to a temporary file and renamed over the old one, which is atomic on one file system. */
60
+ replace(path, bytes) {
61
+ const at = this.at(path);
62
+ mkdirSync(dirname(at), { recursive: true });
63
+ const tmp = `${at}.${randomSuffix()}.tmp`;
64
+ writeFileSync(tmp, bytes);
65
+ renameSync(tmp, at);
66
+ }
67
+ remove(path) {
68
+ rmSync(this.at(path), { force: true });
69
+ }
70
+ }
71
+ //# sourceMappingURL=folder-storage.js.map
@@ -0,0 +1,38 @@
1
+ import { parse, stringify } from "yaml";
2
+ const FENCED = /^---\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)([\s\S]*)$/;
3
+ /** Split a Markdown file into its YAML frontmatter and body. Frontmatter must be flat (vault convention). */
4
+ export function parseDoc(text) {
5
+ const m = FENCED.exec(text);
6
+ if (!m)
7
+ throw new Error("missing frontmatter: the file must start with a --- block");
8
+ const meta = parse(m[1] ?? "") ?? {};
9
+ if (typeof meta !== "object" || meta === null || Array.isArray(meta)) {
10
+ throw new Error("frontmatter must be a mapping of keys to values");
11
+ }
12
+ for (const [key, value] of Object.entries(meta)) {
13
+ const nested = Array.isArray(value)
14
+ ? value.some((item) => typeof item === "object" && item !== null)
15
+ : typeof value === "object" && value !== null;
16
+ if (nested)
17
+ throw new Error(`frontmatter must be flat: "${key}" holds a nested object`);
18
+ }
19
+ return { meta: meta, body: m[2] ?? "" };
20
+ }
21
+ /** Render frontmatter and body back into a Markdown file. Undefined values and empty lists are left out. */
22
+ export function renderDoc(meta, body) {
23
+ const clean = Object.fromEntries(Object.entries(meta).filter(([, v]) => v !== undefined && !(Array.isArray(v) && v.length === 0)));
24
+ const yaml = stringify(clean, { lineWidth: 0 }).trimEnd();
25
+ const text = body.replace(/^\n+/, "");
26
+ return `---\n${yaml}\n---\n\n${text}${text.endsWith("\n") ? "" : "\n"}`;
27
+ }
28
+ /** The text under a `## Heading` in a Markdown body, up to the next heading of the same or higher level. */
29
+ export function section(body, heading) {
30
+ const lines = body.split(/\r?\n/);
31
+ const start = lines.findIndex((l) => l.replace(/\s+$/, "") === `## ${heading}`);
32
+ if (start < 0)
33
+ return undefined;
34
+ const rest = lines.slice(start + 1);
35
+ const end = rest.findIndex((l) => /^#{1,2}\s/.test(l));
36
+ return (end < 0 ? rest : rest.slice(0, end)).join("\n").trim();
37
+ }
38
+ //# sourceMappingURL=frontmatter.js.map
@@ -0,0 +1,77 @@
1
+ import { uncovered } from "./globs.js";
2
+ // What the commit gate looks at (D25, D59), and who changes it (B-qkek1): the globs its schema and infra detectors match,
3
+ // the public modules that turn on its api: detector, and the paths it never looks at. The built-in lists are here; a
4
+ // repo changes them only in its preference files, which only the client writes, never in the committed config. A change
5
+ // that makes the gate see less is loosening (D163), told by comparing the globs (`pathsLoosening()`). Pure, with no
6
+ // Node, so the CLI, CI, the relay and the client all read it alike.
7
+ export const DEFAULT_SCHEMA = [
8
+ "**/migrations/**",
9
+ "**/*.sql",
10
+ "**/schema.prisma",
11
+ "**/*.schema.json",
12
+ "**/schema.graphql",
13
+ // A TypeScript schema module: Drizzle's and Convex's are named so, as are Zod schemas for files on disk, like Baesian's own.
14
+ "**/schema.ts",
15
+ ];
16
+ export const DEFAULT_INFRA = [
17
+ ".github/workflows/**",
18
+ // Git hooks that are committed run on everyone's machine: changing one is an infra decision (D135).
19
+ ".githooks/**",
20
+ ".husky/**",
21
+ ".gitlab-ci.yml",
22
+ "**/Dockerfile",
23
+ "**/Dockerfile.*",
24
+ "**/docker-compose*.yml",
25
+ "**/docker-compose*.yaml",
26
+ "**/compose.yml",
27
+ "**/compose.yaml",
28
+ "**/*.tf",
29
+ "**/wrangler.toml",
30
+ "**/wrangler.json",
31
+ "**/wrangler.jsonc",
32
+ "**/fly.toml",
33
+ "**/vercel.json",
34
+ "**/netlify.toml",
35
+ ];
36
+ /** Never looked at, whatever a repo sets: a repo's own ignores are added to these. */
37
+ export const DEFAULT_IGNORE = ["**/node_modules/**", ".baesian/**", "**/vendor/**", "**/__fixtures__/**"];
38
+ export const PATH_KINDS = ["schema", "infra", "api", "ignore"];
39
+ /** Each kind's key in a preference file. */
40
+ export const PATH_KEYS = { schema: "gate_schema", infra: "gate_infra", api: "gate_api", ignore: "gate_ignore" };
41
+ /** The built-in list each kind starts from. A repo's ignores are added to `DEFAULT_IGNORE`, which is always applied. */
42
+ const BUILT_IN = { schema: DEFAULT_SCHEMA, infra: DEFAULT_INFRA, api: [], ignore: [] };
43
+ /** The list the gate applies for a kind: the setting's, or the built-in one. For `ignore`, the repo's own, beside `DEFAULT_IGNORE`. */
44
+ export function pathsOf(paths, kind) {
45
+ return paths[kind] ?? BUILT_IN[kind];
46
+ }
47
+ /**
48
+ * A list as a preference file keeps it (B-qkek1): trimmed, without blanks or repeats; and left out when it's the built-in
49
+ * one, in any order, so a file that keeps the defaults means them, whatever later defaults are. An empty schema or infra
50
+ * list is kept: the gate looks for none.
51
+ */
52
+ export function pathSetting(kind, list) {
53
+ const globs = [...new Set(list.map((g) => g.trim()).filter(Boolean))];
54
+ const built = BUILT_IN[kind];
55
+ if (globs.length === built.length && built.every((g) => globs.includes(g)))
56
+ return undefined;
57
+ return globs;
58
+ }
59
+ /**
60
+ * What a change to the gate's paths loosens (D163, B-qkek1): a schema, infra or API list that no longer covers every
61
+ * path the old one did, and paths ignored that weren't before. Told by comparing the globs (`uncovered()`), exactly; a
62
+ * glob it can't tell about counts as loosening. Each makes the gate see less, so in a hosted repo it takes the passkey.
63
+ */
64
+ export function pathsLoosening(before, after) {
65
+ const out = [];
66
+ const list = (globs) => globs.map((g) => `"${g}"`).join(", ");
67
+ for (const kind of ["schema", "infra", "api"]) {
68
+ const lost = uncovered(pathsOf(after, kind), pathsOf(before, kind));
69
+ if (lost.length)
70
+ out.push(`it narrows ${PATH_KEYS[kind]}, which no longer covers ${list(lost)}`);
71
+ }
72
+ const more = uncovered([...DEFAULT_IGNORE, ...pathsOf(before, "ignore")], pathsOf(after, "ignore"));
73
+ if (more.length)
74
+ out.push(`it widens ${PATH_KEYS.ignore} to ${list(more)}`);
75
+ return out;
76
+ }
77
+ //# sourceMappingURL=gate-paths.js.map