@de-otio/repo-aegis-core 0.2.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.
Files changed (191) hide show
  1. package/dist/age.d.ts +32 -0
  2. package/dist/age.d.ts.map +1 -0
  3. package/dist/age.js +98 -0
  4. package/dist/age.js.map +1 -0
  5. package/dist/audit-log.d.ts +50 -0
  6. package/dist/audit-log.d.ts.map +1 -0
  7. package/dist/audit-log.js +183 -0
  8. package/dist/audit-log.js.map +1 -0
  9. package/dist/audit-log.test.d.ts +2 -0
  10. package/dist/audit-log.test.d.ts.map +1 -0
  11. package/dist/audit-log.test.js +181 -0
  12. package/dist/audit-log.test.js.map +1 -0
  13. package/dist/deny-set.d.ts +43 -0
  14. package/dist/deny-set.d.ts.map +1 -0
  15. package/dist/deny-set.js +165 -0
  16. package/dist/deny-set.js.map +1 -0
  17. package/dist/deny-set.test.d.ts +2 -0
  18. package/dist/deny-set.test.d.ts.map +1 -0
  19. package/dist/deny-set.test.js +155 -0
  20. package/dist/deny-set.test.js.map +1 -0
  21. package/dist/exceptions.d.ts +96 -0
  22. package/dist/exceptions.d.ts.map +1 -0
  23. package/dist/exceptions.js +143 -0
  24. package/dist/exceptions.js.map +1 -0
  25. package/dist/exit-codes.d.ts +4 -0
  26. package/dist/exit-codes.d.ts.map +1 -0
  27. package/dist/exit-codes.js +6 -0
  28. package/dist/exit-codes.js.map +1 -0
  29. package/dist/first-touch.d.ts +57 -0
  30. package/dist/first-touch.d.ts.map +1 -0
  31. package/dist/first-touch.js +112 -0
  32. package/dist/first-touch.js.map +1 -0
  33. package/dist/import-graph.test.d.ts +2 -0
  34. package/dist/import-graph.test.d.ts.map +1 -0
  35. package/dist/import-graph.test.js +210 -0
  36. package/dist/import-graph.test.js.map +1 -0
  37. package/dist/index.d.ts +37 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +68 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/lock.d.ts +22 -0
  42. package/dist/lock.d.ts.map +1 -0
  43. package/dist/lock.js +86 -0
  44. package/dist/lock.js.map +1 -0
  45. package/dist/lock.test.d.ts +2 -0
  46. package/dist/lock.test.d.ts.map +1 -0
  47. package/dist/lock.test.js +125 -0
  48. package/dist/lock.test.js.map +1 -0
  49. package/dist/paths.d.ts +22 -0
  50. package/dist/paths.d.ts.map +1 -0
  51. package/dist/paths.js +46 -0
  52. package/dist/paths.js.map +1 -0
  53. package/dist/paths.test.d.ts +2 -0
  54. package/dist/paths.test.d.ts.map +1 -0
  55. package/dist/paths.test.js +78 -0
  56. package/dist/paths.test.js.map +1 -0
  57. package/dist/redaction.d.ts +29 -0
  58. package/dist/redaction.d.ts.map +1 -0
  59. package/dist/redaction.js +48 -0
  60. package/dist/redaction.js.map +1 -0
  61. package/dist/redaction.test.d.ts +2 -0
  62. package/dist/redaction.test.d.ts.map +1 -0
  63. package/dist/redaction.test.js +67 -0
  64. package/dist/redaction.test.js.map +1 -0
  65. package/dist/regex-safety.d.ts +87 -0
  66. package/dist/regex-safety.d.ts.map +1 -0
  67. package/dist/regex-safety.js +322 -0
  68. package/dist/regex-safety.js.map +1 -0
  69. package/dist/regex-safety.test.d.ts +2 -0
  70. package/dist/regex-safety.test.d.ts.map +1 -0
  71. package/dist/regex-safety.test.js +149 -0
  72. package/dist/regex-safety.test.js.map +1 -0
  73. package/dist/registry-mutate.d.ts +35 -0
  74. package/dist/registry-mutate.d.ts.map +1 -0
  75. package/dist/registry-mutate.js +149 -0
  76. package/dist/registry-mutate.js.map +1 -0
  77. package/dist/registry-mutate.test.d.ts +2 -0
  78. package/dist/registry-mutate.test.d.ts.map +1 -0
  79. package/dist/registry-mutate.test.js +96 -0
  80. package/dist/registry-mutate.test.js.map +1 -0
  81. package/dist/registry.d.ts +64 -0
  82. package/dist/registry.d.ts.map +1 -0
  83. package/dist/registry.js +120 -0
  84. package/dist/registry.js.map +1 -0
  85. package/dist/registry.test.d.ts +2 -0
  86. package/dist/registry.test.d.ts.map +1 -0
  87. package/dist/registry.test.js +316 -0
  88. package/dist/registry.test.js.map +1 -0
  89. package/dist/remote-url.d.ts +18 -0
  90. package/dist/remote-url.d.ts.map +1 -0
  91. package/dist/remote-url.js +66 -0
  92. package/dist/remote-url.js.map +1 -0
  93. package/dist/remote-url.test.d.ts +2 -0
  94. package/dist/remote-url.test.d.ts.map +1 -0
  95. package/dist/remote-url.test.js +116 -0
  96. package/dist/remote-url.test.js.map +1 -0
  97. package/dist/render.d.ts +54 -0
  98. package/dist/render.d.ts.map +1 -0
  99. package/dist/render.js +182 -0
  100. package/dist/render.js.map +1 -0
  101. package/dist/render.test.d.ts +2 -0
  102. package/dist/render.test.d.ts.map +1 -0
  103. package/dist/render.test.js +152 -0
  104. package/dist/render.test.js.map +1 -0
  105. package/dist/repo.d.ts +40 -0
  106. package/dist/repo.d.ts.map +1 -0
  107. package/dist/repo.js +214 -0
  108. package/dist/repo.js.map +1 -0
  109. package/dist/repo.test.d.ts +2 -0
  110. package/dist/repo.test.d.ts.map +1 -0
  111. package/dist/repo.test.js +234 -0
  112. package/dist/repo.test.js.map +1 -0
  113. package/dist/scan.d.ts +103 -0
  114. package/dist/scan.d.ts.map +1 -0
  115. package/dist/scan.js +436 -0
  116. package/dist/scan.js.map +1 -0
  117. package/dist/scan.test.d.ts +2 -0
  118. package/dist/scan.test.d.ts.map +1 -0
  119. package/dist/scan.test.js +437 -0
  120. package/dist/scan.test.js.map +1 -0
  121. package/dist/schemas.d.ts +50 -0
  122. package/dist/schemas.d.ts.map +1 -0
  123. package/dist/schemas.js +190 -0
  124. package/dist/schemas.js.map +1 -0
  125. package/dist/secret-markers.d.ts +34 -0
  126. package/dist/secret-markers.d.ts.map +1 -0
  127. package/dist/secret-markers.js +118 -0
  128. package/dist/secret-markers.js.map +1 -0
  129. package/dist/secret-markers.test.d.ts +2 -0
  130. package/dist/secret-markers.test.d.ts.map +1 -0
  131. package/dist/secret-markers.test.js +154 -0
  132. package/dist/secret-markers.test.js.map +1 -0
  133. package/dist/trust-boundary.d.ts +33 -0
  134. package/dist/trust-boundary.d.ts.map +1 -0
  135. package/dist/trust-boundary.js +77 -0
  136. package/dist/trust-boundary.js.map +1 -0
  137. package/dist/trust-boundary.test.d.ts +2 -0
  138. package/dist/trust-boundary.test.d.ts.map +1 -0
  139. package/dist/trust-boundary.test.js +170 -0
  140. package/dist/trust-boundary.test.js.map +1 -0
  141. package/dist/types.d.ts +47 -0
  142. package/dist/types.d.ts.map +1 -0
  143. package/dist/types.js +8 -0
  144. package/dist/types.js.map +1 -0
  145. package/dist/working-tree.d.ts +38 -0
  146. package/dist/working-tree.d.ts.map +1 -0
  147. package/dist/working-tree.js +133 -0
  148. package/dist/working-tree.js.map +1 -0
  149. package/dist/working-tree.test.d.ts +2 -0
  150. package/dist/working-tree.test.d.ts.map +1 -0
  151. package/dist/working-tree.test.js +162 -0
  152. package/dist/working-tree.test.js.map +1 -0
  153. package/package.json +40 -0
  154. package/src/age.ts +113 -0
  155. package/src/audit-log.test.ts +222 -0
  156. package/src/audit-log.ts +215 -0
  157. package/src/deny-set.test.ts +208 -0
  158. package/src/deny-set.ts +231 -0
  159. package/src/exceptions.ts +134 -0
  160. package/src/exit-codes.ts +5 -0
  161. package/src/first-touch.ts +172 -0
  162. package/src/import-graph.test.ts +239 -0
  163. package/src/index.ts +191 -0
  164. package/src/lock.test.ts +151 -0
  165. package/src/lock.ts +88 -0
  166. package/src/paths.test.ts +94 -0
  167. package/src/paths.ts +55 -0
  168. package/src/redaction.test.ts +81 -0
  169. package/src/redaction.ts +49 -0
  170. package/src/regex-safety.test.ts +194 -0
  171. package/src/regex-safety.ts +349 -0
  172. package/src/registry-mutate.test.ts +134 -0
  173. package/src/registry-mutate.ts +185 -0
  174. package/src/registry.test.ts +460 -0
  175. package/src/registry.ts +178 -0
  176. package/src/remote-url.test.ts +121 -0
  177. package/src/remote-url.ts +78 -0
  178. package/src/render.test.ts +206 -0
  179. package/src/render.ts +215 -0
  180. package/src/repo.test.ts +275 -0
  181. package/src/repo.ts +245 -0
  182. package/src/scan.test.ts +580 -0
  183. package/src/scan.ts +531 -0
  184. package/src/schemas.ts +207 -0
  185. package/src/secret-markers.test.ts +183 -0
  186. package/src/secret-markers.ts +145 -0
  187. package/src/trust-boundary.test.ts +198 -0
  188. package/src/trust-boundary.ts +98 -0
  189. package/src/types.ts +55 -0
  190. package/src/working-tree.test.ts +193 -0
  191. package/src/working-tree.ts +130 -0
@@ -0,0 +1,149 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+ // Copyright (C) 2026 Richard Myers and contributors.
3
+ //
4
+ // `addMarkerPattern` / `addMarkerPatterns` (P2-B-1) — core helper for
5
+ // programmatically appending validated regex patterns to an
6
+ // engagement's markers.
7
+ //
8
+ // [SEC M-3] Lock scope covers the entire load-modify-write-render
9
+ // cycle so concurrent callers (parallel `suggest-markers` runs against
10
+ // different engagements) cannot lose updates or leave the rendered
11
+ // markers stale relative to the registry.
12
+ import { existsSync, readFileSync, writeFileSync, chmodSync } from "node:fs";
13
+ import { parseDocument, YAMLSeq, Scalar, isMap } from "yaml";
14
+ import { registryPath as defaultRegistryPath } from "./paths.js";
15
+ import { loadRegistry } from "./registry.js";
16
+ import { renderMarkers } from "./render.js";
17
+ import { withLockSync } from "./lock.js";
18
+ import { validatePattern } from "./regex-safety.js";
19
+ import { appendAuditRecord } from "./audit-log.js";
20
+ import { EngagementNotFoundError, PatternValidationError, } from "./exceptions.js";
21
+ function findEngagementNode(doc, id) {
22
+ const seq = doc.get("engagements");
23
+ if (!seq || !seq.items)
24
+ return null;
25
+ for (const item of seq.items) {
26
+ if (!isMap(item))
27
+ continue;
28
+ const idNode = item.get("id");
29
+ if (typeof idNode === "string" && idNode === id)
30
+ return item;
31
+ if (idNode instanceof Scalar && idNode.value === id)
32
+ return item;
33
+ }
34
+ return null;
35
+ }
36
+ function readMarkers(node) {
37
+ const seq = node.get("markers");
38
+ if (!seq || !seq.items)
39
+ return [];
40
+ const out = [];
41
+ for (const item of seq.items) {
42
+ if (typeof item === "string")
43
+ out.push(item);
44
+ else if (item instanceof Scalar && typeof item.value === "string")
45
+ out.push(item.value);
46
+ }
47
+ return out;
48
+ }
49
+ /**
50
+ * Append one or more validated regex patterns to an engagement's
51
+ * markers. Held under a single registry lock for the entire
52
+ * load-modify-write-render cycle (see [SEC M-3]). Idempotent: patterns
53
+ * already present are reported in `skipped`, not re-added.
54
+ *
55
+ * Throws:
56
+ * - `EngagementNotFoundError` if no engagement with that id exists.
57
+ * - `PatternValidationError` if any pattern fails `validatePattern`.
58
+ */
59
+ export function addMarkerPatterns(engagementId, patterns, opts = {}) {
60
+ const path = opts.registryPath ?? defaultRegistryPath();
61
+ const source = opts.source ?? "manual";
62
+ // Validate every pattern up front. Fail closed before touching the
63
+ // registry on the first bad pattern.
64
+ const invalid = [];
65
+ for (const p of patterns) {
66
+ const v = validatePattern(p);
67
+ if (!v.ok) {
68
+ invalid.push({ pattern: p, reason: v.reason ?? "unknown" });
69
+ }
70
+ }
71
+ if (invalid.length > 0) {
72
+ throw new PatternValidationError(invalid.map(i => ({ ...i, engagementId })));
73
+ }
74
+ // [SEC M-3] Lock spans the entire read-modify-write-render cycle.
75
+ const result = withLockSync(() => {
76
+ if (!existsSync(path)) {
77
+ throw new Error(`registry not found at ${path}`);
78
+ }
79
+ const raw = readFileSync(path, "utf8");
80
+ const doc = parseDocument(raw);
81
+ const node = findEngagementNode(doc, engagementId);
82
+ if (node === null) {
83
+ throw new EngagementNotFoundError(engagementId);
84
+ }
85
+ let seq = node.get("markers");
86
+ if (!seq || !(seq instanceof YAMLSeq)) {
87
+ seq = new YAMLSeq();
88
+ node.set("markers", seq);
89
+ }
90
+ const existing = new Set(readMarkers(node));
91
+ const added = [];
92
+ const skipped = [];
93
+ for (const p of patterns) {
94
+ if (existing.has(p)) {
95
+ skipped.push(p);
96
+ continue;
97
+ }
98
+ seq.add(p);
99
+ existing.add(p);
100
+ added.push(p);
101
+ }
102
+ if (added.length > 0) {
103
+ writeFileSync(path, doc.toString(), { mode: 0o600 });
104
+ try {
105
+ chmodSync(path, 0o600);
106
+ }
107
+ catch {
108
+ /* platform-restricted */
109
+ }
110
+ }
111
+ // Re-render markers regardless of whether new patterns landed:
112
+ // skipped duplicates still represent a successful no-op for the
113
+ // caller, and re-rendering keeps the markers/ output in sync if
114
+ // anything else mutated the registry between previous render and
115
+ // now (covered by the same lock).
116
+ const reg = loadRegistry(path);
117
+ const render = renderMarkers(reg);
118
+ return {
119
+ added,
120
+ skipped,
121
+ rendered: { written: render.written.length, removed: render.removed.length },
122
+ };
123
+ });
124
+ // Audit (best-effort). Records id + counts only — never the literal
125
+ // patterns themselves.
126
+ try {
127
+ appendAuditRecord({
128
+ action: "engagements-add-marker",
129
+ engagement: engagementId,
130
+ details: {
131
+ added: result.added.length,
132
+ skipped: result.skipped.length,
133
+ source,
134
+ },
135
+ });
136
+ }
137
+ catch {
138
+ /* audit log must not break user-facing ops */
139
+ }
140
+ return result;
141
+ }
142
+ /**
143
+ * Convenience wrapper: append a single pattern. Same semantics as
144
+ * `addMarkerPatterns([pattern])`.
145
+ */
146
+ export function addMarkerPattern(engagementId, pattern, opts = {}) {
147
+ return addMarkerPatterns(engagementId, [pattern], opts);
148
+ }
149
+ //# sourceMappingURL=registry-mutate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry-mutate.js","sourceRoot":"","sources":["../src/registry-mutate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=registry-mutate.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry-mutate.test.d.ts","sourceRoot":"","sources":["../src/registry-mutate.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,96 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+ // Copyright (C) 2026 Richard Myers and contributors.
3
+ import { describe, it, before, after, beforeEach } from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync, } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import { join } from "node:path";
8
+ import { addMarkerPattern, addMarkerPatterns } from "./registry-mutate.js";
9
+ import { EngagementNotFoundError, PatternValidationError, } from "./exceptions.js";
10
+ let tmp;
11
+ let home;
12
+ let registryPath;
13
+ const STUB = `\
14
+ schemaVersion: 2
15
+ always_block: []
16
+ engagements:
17
+ - id: foo-corp
18
+ name: Foo Corp
19
+ started: 2026-01-01
20
+ markers: [\\bfoo\\b]
21
+ - id: bar-co
22
+ name: Bar Co
23
+ started: 2026-01-15
24
+ markers: []
25
+ `;
26
+ before(() => {
27
+ tmp = mkdtempSync(join(tmpdir(), "repo-aegis-registry-mutate-"));
28
+ home = join(tmp, "home");
29
+ registryPath = join(home, "engagements.yaml");
30
+ });
31
+ after(() => {
32
+ rmSync(tmp, { recursive: true, force: true });
33
+ });
34
+ beforeEach(() => {
35
+ rmSync(home, { recursive: true, force: true });
36
+ mkdirSync(join(home, "markers"), { recursive: true });
37
+ mkdirSync(join(home, "state"), { recursive: true });
38
+ writeFileSync(registryPath, STUB);
39
+ process.env["REPO_AEGIS_HOME"] = home;
40
+ });
41
+ describe("addMarkerPattern — happy path", () => {
42
+ it("appends a single pattern and renders markers", () => {
43
+ const result = addMarkerPattern("bar-co", "\\bbar-co\\b", { registryPath });
44
+ assert.deepEqual(result.added, ["\\bbar-co\\b"]);
45
+ assert.deepEqual(result.skipped, []);
46
+ const reg = readFileSync(registryPath, "utf8");
47
+ assert.match(reg, /\\bbar-co\\b/);
48
+ });
49
+ it("idempotent — re-adding the same pattern is a no-op", () => {
50
+ addMarkerPattern("bar-co", "\\bbar-co\\b", { registryPath });
51
+ const r2 = addMarkerPattern("bar-co", "\\bbar-co\\b", { registryPath });
52
+ assert.deepEqual(r2.added, []);
53
+ assert.deepEqual(r2.skipped, ["\\bbar-co\\b"]);
54
+ });
55
+ });
56
+ describe("addMarkerPatterns — bulk add", () => {
57
+ it("appends multiple patterns with mixed new/duplicate handling", () => {
58
+ addMarkerPattern("bar-co", "\\bbar-co\\b", { registryPath });
59
+ const result = addMarkerPatterns("bar-co", ["\\bbar-co\\b", "\\bbar\\.example\\b", "\\bBC-[0-9]+\\b"], { registryPath });
60
+ assert.equal(result.added.length, 2);
61
+ assert.equal(result.skipped.length, 1);
62
+ assert.ok(result.added.includes("\\bbar\\.example\\b"));
63
+ assert.ok(result.added.includes("\\bBC-[0-9]+\\b"));
64
+ });
65
+ });
66
+ describe("addMarkerPatterns — error paths", () => {
67
+ it("throws EngagementNotFoundError for unknown id", () => {
68
+ assert.throws(() => addMarkerPattern("nonexistent", "\\bfoo\\b", { registryPath }), EngagementNotFoundError);
69
+ });
70
+ it("throws PatternValidationError on invalid regex", () => {
71
+ assert.throws(() => addMarkerPattern("foo-corp", "(?<unclosed", { registryPath }), PatternValidationError);
72
+ });
73
+ it("validates ALL patterns before mutating any", () => {
74
+ // First pattern good, second bad — registry must be unchanged.
75
+ const before = readFileSync(registryPath, "utf8");
76
+ assert.throws(() => addMarkerPatterns("bar-co", ["\\bgood\\b", "(?<bad"], { registryPath }), PatternValidationError);
77
+ const after = readFileSync(registryPath, "utf8");
78
+ assert.equal(before, after);
79
+ });
80
+ });
81
+ describe("addMarkerPatterns — [SEC M-3] lock scope", () => {
82
+ it("two parallel calls on different engagements both succeed without lost updates", async () => {
83
+ // Run two adds concurrently — using Promise.all as a parallel-ish
84
+ // proxy. The lock is sync (withLockSync), so they will serialise via
85
+ // proper-lockfile. Asserting both sets land is the correctness goal.
86
+ const a = Promise.resolve().then(() => addMarkerPatterns("foo-corp", ["\\bnew-foo\\b"], { registryPath }));
87
+ const b = Promise.resolve().then(() => addMarkerPatterns("bar-co", ["\\bnew-bar\\b"], { registryPath }));
88
+ const [ra, rb] = await Promise.all([a, b]);
89
+ assert.deepEqual(ra.added, ["\\bnew-foo\\b"]);
90
+ assert.deepEqual(rb.added, ["\\bnew-bar\\b"]);
91
+ const reg = readFileSync(registryPath, "utf8");
92
+ assert.match(reg, /\\bnew-foo\\b/);
93
+ assert.match(reg, /\\bnew-bar\\b/);
94
+ });
95
+ });
96
+ //# sourceMappingURL=registry-mutate.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry-mutate.test.js","sourceRoot":"","sources":["../src/registry-mutate.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ export interface Engagement {
2
+ id: string;
3
+ name: string;
4
+ started?: string | null;
5
+ ended?: string | null;
6
+ reposActive?: string[];
7
+ markers: string[];
8
+ notes?: string;
9
+ /**
10
+ * GitHub orgs that map to this engagement (Phase 1, schemaVersion 2+).
11
+ * A repo whose origin's org appears here is auto-classified
12
+ * `customer-coupled` with this engagement. Lowercased, GitHub
13
+ * org-name shape; cross-validated for uniqueness across all
14
+ * engagements and disjointness with {@link Registry.personalOrgs}.
15
+ */
16
+ githubOrgs?: string[];
17
+ }
18
+ export interface Registry {
19
+ engagements: Engagement[];
20
+ alwaysBlock: string[];
21
+ /**
22
+ * GitHub orgs the user owns / treats as public (schemaVersion 2+).
23
+ * A repo whose origin's org is in this list is auto-classified
24
+ * `public-eligible`. Disjoint from any engagement's `githubOrgs`.
25
+ * Optional in the type so callers constructing Registry literals
26
+ * (tests, fixtures) don't have to specify; `loadRegistry` always
27
+ * populates it (defaults to `[]` for v1 registries that don't
28
+ * declare the field).
29
+ */
30
+ personalOrgs?: string[];
31
+ /**
32
+ * Schema version of the on-disk registry. Defaults to 1 when the YAML has
33
+ * no `schemaVersion:` field (legacy). Readers refuse versions
34
+ * greater than {@link MAX_SUPPORTED_REGISTRY_SCHEMA_VERSION}. Optional in
35
+ * the type so callers constructing Registry literals (tests, fixtures)
36
+ * don't have to specify; loadRegistry always populates it.
37
+ */
38
+ schemaVersion?: number;
39
+ }
40
+ export declare const ALWAYS_BLOCK_RESERVED_ID = "_always";
41
+ /**
42
+ * Highest registry `schemaVersion` this build of repo-aegis can read.
43
+ *
44
+ * - v1 (legacy): no `schemaVersion`, no `personalOrgs`, no
45
+ * `engagements[*].githubOrgs`. Loaded with `personalOrgs: []` and every
46
+ * engagement's `githubOrgs` undefined.
47
+ * - v2 (Phase 1 onboarding): adds `personalOrgs` and
48
+ * `engagements[*].githubOrgs` for org-keyed JIT classification.
49
+ *
50
+ * Reader policy:
51
+ * - missing field => treat as version 1 (legacy);
52
+ * - version <= MAX => accept (unknown sibling fields are ignored);
53
+ * - version > MAX => refuse with "upgrade required".
54
+ * Writers must never lower the version.
55
+ */
56
+ export declare const MAX_SUPPORTED_REGISTRY_SCHEMA_VERSION = 2;
57
+ export declare function loadRegistry(path?: string): Registry;
58
+ export declare function isActive(e: Engagement, retentionMonths?: number): boolean;
59
+ export interface ResolveResult {
60
+ match: Engagement | null;
61
+ candidates: Engagement[];
62
+ }
63
+ export declare function resolveEngagement(reg: Registry, query: string): ResolveResult;
64
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAaA,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,QAAQ;IACvB,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,eAAO,MAAM,wBAAwB,YAAY,CAAC;AAElD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,qCAAqC,IAAI,CAAC;AAEvD,wBAAgB,YAAY,CAAC,IAAI,GAAE,MAAuB,GAAG,QAAQ,CA6EpE;AAED,wBAAgB,QAAQ,CAAC,CAAC,EAAE,UAAU,EAAE,eAAe,SAAK,GAAG,OAAO,CAOrE;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,UAAU,EAAE,CAAC;CAC1B;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,aAAa,CAW7E"}
@@ -0,0 +1,120 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+ // Copyright (C) 2026 Richard Myers and contributors.
3
+ import { readFileSync, existsSync } from "node:fs";
4
+ import { parse } from "yaml";
5
+ import { z } from "zod";
6
+ import { registryPath } from "./paths.js";
7
+ import { RegistryNotFoundError, RegistryParseError, RegistryEncryptedError, } from "./exceptions.js";
8
+ import { registryFileSchema, formatZodError } from "./schemas.js";
9
+ export const ALWAYS_BLOCK_RESERVED_ID = "_always";
10
+ /**
11
+ * Highest registry `schemaVersion` this build of repo-aegis can read.
12
+ *
13
+ * - v1 (legacy): no `schemaVersion`, no `personalOrgs`, no
14
+ * `engagements[*].githubOrgs`. Loaded with `personalOrgs: []` and every
15
+ * engagement's `githubOrgs` undefined.
16
+ * - v2 (Phase 1 onboarding): adds `personalOrgs` and
17
+ * `engagements[*].githubOrgs` for org-keyed JIT classification.
18
+ *
19
+ * Reader policy:
20
+ * - missing field => treat as version 1 (legacy);
21
+ * - version <= MAX => accept (unknown sibling fields are ignored);
22
+ * - version > MAX => refuse with "upgrade required".
23
+ * Writers must never lower the version.
24
+ */
25
+ export const MAX_SUPPORTED_REGISTRY_SCHEMA_VERSION = 2;
26
+ export function loadRegistry(path = registryPath()) {
27
+ // If the plaintext registry is absent but a sibling `<path>.age`
28
+ // ciphertext exists, the registry is in its encrypted-at-rest state.
29
+ // We deliberately do NOT auto-decrypt: the whole point of the
30
+ // encryption is that the registry only goes plaintext when the user
31
+ // explicitly opts in with `repo-aegis registry decrypt --identity
32
+ // <path>`. Auto-decrypt would defeat the purpose.
33
+ if (!existsSync(path)) {
34
+ const ciphertextPath = `${path}.age`;
35
+ if (existsSync(ciphertextPath)) {
36
+ throw new RegistryEncryptedError(path, ciphertextPath);
37
+ }
38
+ throw new RegistryNotFoundError(path);
39
+ }
40
+ let parsed;
41
+ try {
42
+ parsed = parse(readFileSync(path, "utf8"));
43
+ }
44
+ catch (err) {
45
+ throw new RegistryParseError(path, err);
46
+ }
47
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
48
+ throw new RegistryParseError(path, new Error("registry must be a YAML mapping"));
49
+ }
50
+ // The "missing 'engagements:'" failure used to be a separate check
51
+ // ahead of structural validation. Surfacing it explicitly makes the
52
+ // common-case error message punchier than zod's generic
53
+ // "Required" — and several tests pin on this wording.
54
+ if (!("engagements" in parsed)) {
55
+ throw new RegistryParseError(path, new Error("missing 'engagements:' top-level key"));
56
+ }
57
+ let validated;
58
+ try {
59
+ validated = registryFileSchema.parse(parsed);
60
+ }
61
+ catch (err) {
62
+ if (err instanceof z.ZodError) {
63
+ throw new RegistryParseError(path, new Error(formatZodError(err, "registry")));
64
+ }
65
+ throw err;
66
+ }
67
+ // Schema-version gate (per design B14). Zod has accepted any number;
68
+ // the policy decision (reject newer-than-supported with an upgrade
69
+ // hint) is encoded here rather than as a refine() so the error
70
+ // wording is exactly what the user-facing tests assert on.
71
+ const schemaVersion = validated.schemaVersion ?? 1;
72
+ if (schemaVersion > MAX_SUPPORTED_REGISTRY_SCHEMA_VERSION) {
73
+ throw new RegistryParseError(path, new Error(`registry schemaVersion ${schemaVersion} is newer than this build supports ` +
74
+ `(max ${MAX_SUPPORTED_REGISTRY_SCHEMA_VERSION}); ` +
75
+ `registry written by a newer repo-aegis — please upgrade`));
76
+ }
77
+ // Map zod's struct shape back to the domain types. The `passthrough()`
78
+ // on the schema preserves unknown sibling fields, but those are not
79
+ // part of the public Registry interface — drop them at the boundary.
80
+ const engagements = validated.engagements.map(e => ({
81
+ id: e.id,
82
+ name: e.name,
83
+ ...(e.started !== undefined && { started: e.started }),
84
+ ...(e.ended !== undefined && { ended: e.ended }),
85
+ ...(e.reposActive !== undefined && { reposActive: e.reposActive }),
86
+ markers: e.markers,
87
+ ...(e.notes !== undefined && { notes: e.notes }),
88
+ ...(e.githubOrgs !== undefined && { githubOrgs: e.githubOrgs }),
89
+ }));
90
+ return {
91
+ engagements,
92
+ alwaysBlock: validated.always_block ?? [],
93
+ personalOrgs: validated.personalOrgs ?? [],
94
+ schemaVersion,
95
+ };
96
+ }
97
+ export function isActive(e, retentionMonths = 12) {
98
+ if (!e.ended)
99
+ return true;
100
+ const endedDate = new Date(e.ended);
101
+ if (Number.isNaN(endedDate.getTime()))
102
+ return true;
103
+ const cutoff = new Date();
104
+ cutoff.setMonth(cutoff.getMonth() - retentionMonths);
105
+ return endedDate > cutoff;
106
+ }
107
+ export function resolveEngagement(reg, query) {
108
+ const q = query.toLowerCase();
109
+ const exactId = reg.engagements.find(e => e.id.toLowerCase() === q);
110
+ if (exactId)
111
+ return { match: exactId, candidates: [exactId] };
112
+ const exactName = reg.engagements.find(e => e.name.toLowerCase() === q);
113
+ if (exactName)
114
+ return { match: exactName, candidates: [exactName] };
115
+ const fuzzy = reg.engagements.filter(e => e.id.toLowerCase().includes(q) || e.name.toLowerCase().includes(q));
116
+ if (fuzzy.length === 1)
117
+ return { match: fuzzy[0], candidates: fuzzy };
118
+ return { match: null, candidates: fuzzy };
119
+ }
120
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=registry.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.test.d.ts","sourceRoot":"","sources":["../src/registry.test.ts"],"names":[],"mappings":""}