@christang/keel 5.4.0 → 5.6.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,105 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
+ // The closed vocabulary of actions a repository may standing-authorize. It is
7
+ // closed so an entry outside it can be reported by name: a free-form grant
8
+ // cannot tell a typo from a decision, and silently dropping one leaves the
9
+ // author believing they authorized something they did not.
10
+ const STANDING_AUTHORIZATION_ACTIONS = ["commit", "push", "release", "archive"];
11
+
12
+ const CONFIG_RELATIVE_PATH = path.join("keel", "config.yaml");
13
+
14
+ // The declaration shares keel/config.yaml with fast_check, so the reader stays
15
+ // line-oriented rather than pulling in a YAML dependency for a format Keel
16
+ // controls and keeps flat on purpose.
17
+ function readStandingAuthorization(repo) {
18
+ const configPath = path.join(repo, "keel", "config.yaml");
19
+ const declared = [];
20
+ const unknown = [];
21
+ if (!fs.existsSync(configPath)) return { declared, unknown };
22
+ let inBlock = false;
23
+ for (const line of fs.readFileSync(configPath, "utf8").split(/\r?\n/)) {
24
+ if (/^\s*#/.test(line)) continue;
25
+ if (/^authorize\s*:\s*$/.test(line)) {
26
+ inBlock = true;
27
+ continue;
28
+ }
29
+ if (!inBlock) continue;
30
+ if (line.trim() === "") continue;
31
+ const entry = line.match(/^\s+-\s*(\S+)\s*$/);
32
+ // Anything that is not a list item closes the block; the next top-level
33
+ // key belongs to the rest of the file.
34
+ if (!entry) break;
35
+ if (STANDING_AUTHORIZATION_ACTIONS.includes(entry[1])) declared.push(entry[1]);
36
+ else unknown.push(entry[1]);
37
+ }
38
+ // Fail closed. A declaration Keel cannot fully read authorizes nothing,
39
+ // because the alternative is granting the entries beside a typo while the
40
+ // author believes they granted the typo too.
41
+ if (unknown.length > 0) return { declared: [], unknown };
42
+ return { declared, unknown };
43
+ }
44
+
45
+ function configScalar(repo, key) {
46
+ const configPath = path.join(repo, "keel", "config.yaml");
47
+ if (!fs.existsSync(configPath)) return null;
48
+ const pattern = new RegExp(`^${key}\\s*:\\s*(.+?)\\s*$`);
49
+ for (const line of fs.readFileSync(configPath, "utf8").split(/\r?\n/)) {
50
+ const stripped = line.trim();
51
+ if (stripped.startsWith("#")) continue;
52
+ const match = stripped.match(pattern);
53
+ if (match) return match[1];
54
+ }
55
+ return null;
56
+ }
57
+
58
+ // Keel reads a local directory and nothing else. How that directory came to
59
+ // exist — a clone, an installed plugin, hand-authored files — is outside Keel,
60
+ // because a surface that reaches the network trades the local, offline,
61
+ // deterministic properties that make it trustworthy.
62
+ function readPrecedentStore(repo) {
63
+ const declared = configScalar(repo, "precedents");
64
+ if (!declared) return { declared: null, path: null, precedents: [] };
65
+ const resolved = path.isAbsolute(declared)
66
+ ? declared
67
+ : path.resolve(repo, declared);
68
+ // A declared path that is not there degrades to the no-store behavior rather
69
+ // than to an error: a private store is exactly what a fresh clone and CI will
70
+ // not have, and a repository declaring one must still be usable by them.
71
+ if (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory()) {
72
+ return { declared, path: resolved, precedents: [] };
73
+ }
74
+ const precedents = fs
75
+ .readdirSync(resolved)
76
+ .filter((name) => name.endsWith(".md") && name !== "README.md")
77
+ .sort()
78
+ .map((name) => {
79
+ const content = fs.readFileSync(path.join(resolved, name), "utf8");
80
+ const status = (content.match(/^-\s*Status:\s*(\S+)/mi) || [])[1] || "";
81
+ // Presence, never judgement. Keel cannot tell a good reason from a bad
82
+ // one and must not imply it can; it can tell a reason from no reason,
83
+ // and a conclusion with no reason cannot be carried to a situation that
84
+ // is not literally the recorded one.
85
+ const rationale = content
86
+ .split(/^##\s+/m)
87
+ .find((section) => /^Rationale\s*$/i.test(section.split(/\r?\n/)[0]));
88
+ return {
89
+ name: name.replace(/\.md$/, ""),
90
+ status: status.toLowerCase(),
91
+ complete: Boolean(
92
+ rationale
93
+ && rationale.split(/\r?\n/).slice(1).join("\n").trim()
94
+ ),
95
+ };
96
+ });
97
+ return { declared, path: resolved, precedents };
98
+ }
99
+
100
+ module.exports = {
101
+ CONFIG_RELATIVE_PATH,
102
+ STANDING_AUTHORIZATION_ACTIONS,
103
+ readPrecedentStore,
104
+ readStandingAuthorization,
105
+ };
@@ -4,6 +4,11 @@ const crypto = require("crypto");
4
4
  const fs = require("fs");
5
5
  const path = require("path");
6
6
 
7
+ const {
8
+ CONFIG_RELATIVE_PATH,
9
+ readStandingAuthorization,
10
+ } = require("./config");
11
+
7
12
  const SUPPORTED_MODES = new Set([
8
13
  "implementation",
9
14
  "diagnose-only",
@@ -844,6 +849,20 @@ function compileTaskContract(repo, change, task) {
844
849
  if (!autonomy.some((item) => /^Default:/i.test(item))) {
845
850
  autonomy.unshift("Default: hard-stop");
846
851
  }
852
+ // A repository declaration supplies the default a task did not author; it
853
+ // never edits one the task did, because a repository-wide default that could
854
+ // override a task's stated boundary would make the capsule unreadable on its
855
+ // own. The entry names its source so an inherited authorization is never
856
+ // mistaken for one this task decided.
857
+ if (explicitAutonomy.length === 0) {
858
+ const { declared } = readStandingAuthorization(repo);
859
+ if (declared.length > 0) {
860
+ autonomy.push(
861
+ `Standing authorization (${CONFIG_RELATIVE_PATH.split(path.sep).join("/")}): `
862
+ + declared.join(", ")
863
+ );
864
+ }
865
+ }
847
866
  if (!autonomy.some((item) => /^Pre-authorized fallback:/i.test(item))) {
848
867
  autonomy.push("Pre-authorized fallback: none");
849
868
  }