@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,316 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+ // Copyright (C) 2026 Richard Myers and contributors.
3
+ import { describe, it, before, after } from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import { join } from "node:path";
8
+ import { loadRegistry, isActive, resolveEngagement, MAX_SUPPORTED_REGISTRY_SCHEMA_VERSION, } from "./registry.js";
9
+ import { RegistryNotFoundError, RegistryParseError, RegistryEncryptedError, } from "./exceptions.js";
10
+ let tmp;
11
+ before(() => {
12
+ tmp = mkdtempSync(join(tmpdir(), "repo-aegis-registry-"));
13
+ });
14
+ after(() => {
15
+ rmSync(tmp, { recursive: true, force: true });
16
+ });
17
+ function writeYaml(name, body) {
18
+ const p = join(tmp, name);
19
+ writeFileSync(p, body);
20
+ return p;
21
+ }
22
+ describe("loadRegistry", () => {
23
+ it("loads a minimal valid registry with alwaysBlock", () => {
24
+ const path = writeYaml("minimal.yaml", `
25
+ always_block:
26
+ - PROJECT-CODENAME-ALPHA
27
+
28
+ engagements:
29
+ - id: customer-a
30
+ name: Customer A
31
+ markers:
32
+ - acme-corp
33
+ `);
34
+ const reg = loadRegistry(path);
35
+ assert.equal(reg.engagements.length, 1);
36
+ assert.equal(reg.engagements[0].id, "customer-a");
37
+ assert.equal(reg.alwaysBlock.length, 1);
38
+ assert.equal(reg.alwaysBlock[0], "PROJECT-CODENAME-ALPHA");
39
+ });
40
+ it("treats missing always_block as empty", () => {
41
+ const path = writeYaml("no-always.yaml", `engagements:
42
+ - id: customer-a
43
+ name: Customer A
44
+ markers: [acme]`);
45
+ const reg = loadRegistry(path);
46
+ assert.equal(reg.alwaysBlock.length, 0);
47
+ });
48
+ it("throws RegistryNotFoundError when file missing", () => {
49
+ assert.throws(() => loadRegistry(join(tmp, "nonexistent.yaml")), RegistryNotFoundError);
50
+ });
51
+ it("throws RegistryEncryptedError when only <path>.age exists", () => {
52
+ const plain = join(tmp, "encrypted-only.yaml");
53
+ const cipher = `${plain}.age`;
54
+ writeFileSync(cipher, "age-encrypted-payload");
55
+ assert.throws(() => loadRegistry(plain), (err) => err instanceof RegistryEncryptedError &&
56
+ err.code === "REGISTRY_ENCRYPTED" &&
57
+ err.path === plain &&
58
+ err.ciphertextPath === cipher &&
59
+ /registry decrypt/.test(err.message));
60
+ rmSync(cipher);
61
+ });
62
+ it("prefers RegistryEncryptedError over RegistryNotFoundError when both states would apply", () => {
63
+ // i.e. plaintext absent + ciphertext present => encrypted error,
64
+ // not "not found". The agent gets a recoverable signal.
65
+ const plain = join(tmp, "encrypted-vs-missing.yaml");
66
+ writeFileSync(`${plain}.age`, "ciphertext");
67
+ assert.throws(() => loadRegistry(plain), RegistryEncryptedError);
68
+ rmSync(`${plain}.age`);
69
+ });
70
+ it("does not throw RegistryEncryptedError when plaintext exists alongside .age", () => {
71
+ // Defensive: if for any reason both files coexist, the plaintext
72
+ // wins (loadRegistry's whole job is to read the plaintext). The
73
+ // CLI's encrypt/decrypt flow ensures this doesn't happen, but the
74
+ // load path must not surprise callers.
75
+ const plain = writeYaml("both-exist.yaml", `engagements:
76
+ - id: customer-a
77
+ name: Customer A
78
+ markers: [foo]`);
79
+ writeFileSync(`${plain}.age`, "ciphertext");
80
+ const reg = loadRegistry(plain);
81
+ assert.equal(reg.engagements.length, 1);
82
+ rmSync(`${plain}.age`);
83
+ });
84
+ it("throws RegistryParseError for invalid YAML", () => {
85
+ const path = writeYaml("invalid.yaml", "not: valid: yaml: at: all:");
86
+ assert.throws(() => loadRegistry(path), RegistryParseError);
87
+ });
88
+ it("throws when missing top-level engagements key", () => {
89
+ const path = writeYaml("missing-engagements.yaml", "always_block:\n - foo");
90
+ assert.throws(() => loadRegistry(path), RegistryParseError);
91
+ });
92
+ it("rejects engagement with id _always (reserved)", () => {
93
+ const path = writeYaml("reserved-id.yaml", `engagements:
94
+ - id: _always
95
+ name: Always
96
+ markers: [foo]`);
97
+ assert.throws(() => loadRegistry(path), /reserved/);
98
+ });
99
+ it("requires markers to be a list", () => {
100
+ const path = writeYaml("no-markers.yaml", `engagements:
101
+ - id: a
102
+ name: A`);
103
+ assert.throws(() => loadRegistry(path), /markers/);
104
+ });
105
+ it("requires always_block to be a list of strings", () => {
106
+ const path = writeYaml("bad-always.yaml", `always_block: not_a_list
107
+ engagements: []`);
108
+ assert.throws(() => loadRegistry(path), RegistryParseError);
109
+ });
110
+ it("treats missing schemaVersion as version 1 (legacy)", () => {
111
+ const path = writeYaml("no-schema-version.yaml", `engagements:
112
+ - id: customer-a
113
+ name: Customer A
114
+ markers: [foo]`);
115
+ const reg = loadRegistry(path);
116
+ assert.equal(reg.schemaVersion, 1);
117
+ });
118
+ it("accepts schemaVersion: 1", () => {
119
+ const path = writeYaml("schema-v1.yaml", `schemaVersion: 1
120
+ engagements:
121
+ - id: customer-a
122
+ name: Customer A
123
+ markers: [foo]`);
124
+ const reg = loadRegistry(path);
125
+ assert.equal(reg.schemaVersion, 1);
126
+ });
127
+ it("rejects schemaVersion greater than max supported with an upgrade message", () => {
128
+ const path = writeYaml("schema-v99.yaml", `schemaVersion: 99
129
+ engagements:
130
+ - id: customer-a
131
+ name: Customer A
132
+ markers: [foo]`);
133
+ assert.throws(() => loadRegistry(path), (err) => err instanceof RegistryParseError &&
134
+ /please upgrade/i.test(err.message) &&
135
+ /99/.test(err.message));
136
+ });
137
+ it("rejects non-numeric schemaVersion", () => {
138
+ const path = writeYaml("schema-bad.yaml", `schemaVersion: "not-a-number"
139
+ engagements:
140
+ - id: customer-a
141
+ name: Customer A
142
+ markers: [foo]`);
143
+ assert.throws(() => loadRegistry(path), RegistryParseError);
144
+ });
145
+ it("MAX_SUPPORTED_REGISTRY_SCHEMA_VERSION is the current pinned version", () => {
146
+ // Sanity guard: bumping this constant is intentional and should be
147
+ // accompanied by a migration plan and updated tests.
148
+ assert.equal(MAX_SUPPORTED_REGISTRY_SCHEMA_VERSION, 2);
149
+ });
150
+ // -- schemaVersion 2: personalOrgs + engagements[*].githubOrgs --
151
+ it("v1 file (no schemaVersion, no personalOrgs/githubOrgs) loads with empty defaults", () => {
152
+ const path = writeYaml("v1-defaults.yaml", `engagements:
153
+ - id: customer-a
154
+ name: Customer A
155
+ markers: [foo]`);
156
+ const reg = loadRegistry(path);
157
+ assert.equal(reg.schemaVersion, 1);
158
+ assert.deepEqual(reg.personalOrgs, []);
159
+ assert.equal(reg.engagements[0].githubOrgs, undefined);
160
+ });
161
+ it("loads schemaVersion 2 with personalOrgs and engagements[*].githubOrgs", () => {
162
+ const path = writeYaml("v2-full.yaml", `schemaVersion: 2
163
+ personalOrgs:
164
+ - my-handle
165
+ - my-oss-org
166
+ engagements:
167
+ - id: foo-corp
168
+ name: Foo Corp
169
+ githubOrgs: [foo-corp, foo-corp-archived]
170
+ markers: [foo]
171
+ - id: bar-co
172
+ name: Bar Co
173
+ githubOrgs: [bar-co]
174
+ markers: [bar]`);
175
+ const reg = loadRegistry(path);
176
+ assert.equal(reg.schemaVersion, 2);
177
+ assert.deepEqual(reg.personalOrgs, ["my-handle", "my-oss-org"]);
178
+ assert.deepEqual(reg.engagements[0].githubOrgs, ["foo-corp", "foo-corp-archived"]);
179
+ assert.deepEqual(reg.engagements[1].githubOrgs, ["bar-co"]);
180
+ });
181
+ it("rejects org name with uppercase characters", () => {
182
+ const path = writeYaml("v2-uppercase.yaml", `schemaVersion: 2
183
+ personalOrgs: [Bad-Org]
184
+ engagements: []`);
185
+ assert.throws(() => loadRegistry(path), (err) => err instanceof RegistryParseError && /lowercase/i.test(err.message));
186
+ });
187
+ it("rejects org name with leading hyphen", () => {
188
+ const path = writeYaml("v2-leading-hyphen.yaml", `schemaVersion: 2
189
+ engagements:
190
+ - id: foo
191
+ name: Foo
192
+ githubOrgs: ["-bad"]
193
+ markers: []`);
194
+ assert.throws(() => loadRegistry(path), RegistryParseError);
195
+ });
196
+ it("rejects org name with whitespace", () => {
197
+ const path = writeYaml("v2-whitespace.yaml", `schemaVersion: 2
198
+ engagements:
199
+ - id: foo
200
+ name: Foo
201
+ githubOrgs: ["bad org"]
202
+ markers: []`);
203
+ assert.throws(() => loadRegistry(path), RegistryParseError);
204
+ });
205
+ it("rejects empty string in githubOrgs", () => {
206
+ const path = writeYaml("v2-empty.yaml", `schemaVersion: 2
207
+ engagements:
208
+ - id: foo
209
+ name: Foo
210
+ githubOrgs: [""]
211
+ markers: []`);
212
+ assert.throws(() => loadRegistry(path), RegistryParseError);
213
+ });
214
+ it("rejects same org appearing in personalOrgs and engagements[*].githubOrgs (disjointness)", () => {
215
+ const path = writeYaml("v2-overlap.yaml", `schemaVersion: 2
216
+ personalOrgs: [my-handle]
217
+ engagements:
218
+ - id: foo
219
+ name: Foo
220
+ githubOrgs: [my-handle]
221
+ markers: []`);
222
+ assert.throws(() => loadRegistry(path), (err) => err instanceof RegistryParseError &&
223
+ /personalOrgs/.test(err.message) &&
224
+ /my-handle/.test(err.message) &&
225
+ /mutually exclusive/.test(err.message));
226
+ });
227
+ it("rejects same org listed in two different engagements' githubOrgs (uniqueness)", () => {
228
+ const path = writeYaml("v2-dup.yaml", `schemaVersion: 2
229
+ engagements:
230
+ - id: foo
231
+ name: Foo
232
+ githubOrgs: [shared-org]
233
+ markers: []
234
+ - id: bar
235
+ name: Bar
236
+ githubOrgs: [shared-org]
237
+ markers: []`);
238
+ assert.throws(() => loadRegistry(path), (err) => err instanceof RegistryParseError &&
239
+ /shared-org/.test(err.message) &&
240
+ /at most one engagement/.test(err.message) &&
241
+ /foo/.test(err.message));
242
+ });
243
+ it("rejects duplicate entries inside personalOrgs", () => {
244
+ const path = writeYaml("v2-personal-dup.yaml", `schemaVersion: 2
245
+ personalOrgs: [me, me]
246
+ engagements: []`);
247
+ assert.throws(() => loadRegistry(path), (err) => err instanceof RegistryParseError && /duplicate/i.test(err.message));
248
+ });
249
+ it("rejects schemaVersion 3 with the existing 'newer than supported' message", () => {
250
+ const path = writeYaml("v3.yaml", `schemaVersion: 3
251
+ engagements:
252
+ - id: customer-a
253
+ name: Customer A
254
+ markers: [foo]`);
255
+ assert.throws(() => loadRegistry(path), (err) => err instanceof RegistryParseError &&
256
+ /please upgrade/i.test(err.message) &&
257
+ /3/.test(err.message));
258
+ });
259
+ });
260
+ describe("isActive", () => {
261
+ it("returns true when ended is null/undefined", () => {
262
+ assert.equal(isActive({ id: "a", name: "A", markers: [] }), true);
263
+ assert.equal(isActive({ id: "a", name: "A", ended: null, markers: [] }), true);
264
+ });
265
+ it("returns true within retention window", () => {
266
+ const recent = new Date();
267
+ recent.setMonth(recent.getMonth() - 6);
268
+ assert.equal(isActive({ id: "a", name: "A", ended: recent.toISOString().slice(0, 10), markers: [] }, 12), true);
269
+ });
270
+ it("returns false past retention window", () => {
271
+ const old = new Date();
272
+ old.setFullYear(old.getFullYear() - 2);
273
+ assert.equal(isActive({ id: "a", name: "A", ended: old.toISOString().slice(0, 10), markers: [] }, 12), false);
274
+ });
275
+ it("treats malformed dates as active (conservative)", () => {
276
+ assert.equal(isActive({ id: "a", name: "A", ended: "not-a-date", markers: [] }), true);
277
+ });
278
+ });
279
+ describe("resolveEngagement", () => {
280
+ const reg = {
281
+ engagements: [
282
+ { id: "customer-a-2025", name: "Customer A", markers: [] },
283
+ { id: "customer-b-2024", name: "Customer B", markers: [] },
284
+ { id: "customer-c", name: "Acme Corp", markers: [] },
285
+ ],
286
+ alwaysBlock: [],
287
+ schemaVersion: 1,
288
+ };
289
+ it("matches by exact id", () => {
290
+ const r = resolveEngagement(reg, "customer-a-2025");
291
+ assert.equal(r.match?.id, "customer-a-2025");
292
+ });
293
+ it("matches by exact name (case-insensitive)", () => {
294
+ const r = resolveEngagement(reg, "customer a");
295
+ assert.equal(r.match?.id, "customer-a-2025");
296
+ });
297
+ it("matches by fuzzy substring on id", () => {
298
+ const r = resolveEngagement(reg, "2024");
299
+ assert.equal(r.match?.id, "customer-b-2024");
300
+ });
301
+ it("returns no match with multiple candidates", () => {
302
+ const r = resolveEngagement(reg, "customer");
303
+ assert.equal(r.match, null);
304
+ assert.ok(r.candidates.length >= 2);
305
+ });
306
+ it("returns no match and empty candidates when nothing matches", () => {
307
+ const r = resolveEngagement(reg, "nonexistent");
308
+ assert.equal(r.match, null);
309
+ assert.equal(r.candidates.length, 0);
310
+ });
311
+ it("matches by name when name is distinct from id", () => {
312
+ const r = resolveEngagement(reg, "acme corp");
313
+ assert.equal(r.match?.id, "customer-c");
314
+ });
315
+ });
316
+ //# sourceMappingURL=registry.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.test.js","sourceRoot":"","sources":["../src/registry.test.ts"],"names":[],"mappings":"AAAA,4CAA4C;AAC5C,qDAAqD;AACrD,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AACxD,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAC7D,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EACL,YAAY,EACZ,QAAQ,EACR,iBAAiB,EACjB,qCAAqC,GACtC,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,qBAAqB,EACrB,kBAAkB,EAClB,sBAAsB,GACvB,MAAM,iBAAiB,CAAC;AAEzB,IAAI,GAAW,CAAC;AAEhB,MAAM,CAAC,GAAG,EAAE;IACV,GAAG,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,sBAAsB,CAAC,CAAC,CAAC;AAC5D,CAAC,CAAC,CAAC;AAEH,KAAK,CAAC,GAAG,EAAE;IACT,MAAM,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AAChD,CAAC,CAAC,CAAC;AAEH,SAAS,SAAS,CAAC,IAAY,EAAE,IAAY;IAC3C,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAC1B,aAAa,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;IACvB,OAAO,CAAC,CAAC;AACX,CAAC;AAED,QAAQ,CAAC,cAAc,EAAE,GAAG,EAAE;IAC5B,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;QACzD,MAAM,IAAI,GAAG,SAAS,CACpB,cAAc,EACd;;;;;;;;;CASL,CACI,CAAC;QACF,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;QAC/B,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QACxC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAE,CAAC,EAAE,EAAE,YAAY,CAAC,CAAC;QACnD,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QACxC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,wBAAwB,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,IAAI,GAAG,SAAS,CACpB,gBAAgB,EAChB;;;oBAGc,CACf,CAAC;QACF,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;QAC/B,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gDAAgD,EAAE,GAAG,EAAE;QACxD,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAC,EAAE,qBAAqB,CAAC,CAAC;IAC1F,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2DAA2D,EAAE,GAAG,EAAE;QACnE,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,qBAAqB,CAAC,CAAC;QAC/C,MAAM,MAAM,GAAG,GAAG,KAAK,MAAM,CAAC;QAC9B,aAAa,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAC;QAC/C,MAAM,CAAC,MAAM,CACX,GAAG,EAAE,CAAC,YAAY,CAAC,KAAK,CAAC,EACzB,CAAC,GAAY,EAAE,EAAE,CACf,GAAG,YAAY,sBAAsB;YACrC,GAAG,CAAC,IAAI,KAAK,oBAAoB;YACjC,GAAG,CAAC,IAAI,KAAK,KAAK;YAClB,GAAG,CAAC,cAAc,KAAK,MAAM;YAC7B,kBAAkB,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CACvC,CAAC;QACF,MAAM,CAAC,MAAM,CAAC,CAAC;IACjB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wFAAwF,EAAE,GAAG,EAAE;QAChG,iEAAiE;QACjE,wDAAwD;QACxD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,2BAA2B,CAAC,CAAC;QACrD,aAAa,CAAC,GAAG,KAAK,MAAM,EAAE,YAAY,CAAC,CAAC;QAC5C,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,KAAK,CAAC,EAAE,sBAAsB,CAAC,CAAC;QACjE,MAAM,CAAC,GAAG,KAAK,MAAM,CAAC,CAAC;IACzB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4EAA4E,EAAE,GAAG,EAAE;QACpF,iEAAiE;QACjE,gEAAgE;QAChE,kEAAkE;QAClE,uCAAuC;QACvC,MAAM,KAAK,GAAG,SAAS,CACrB,iBAAiB,EACjB;;;mBAGa,CACd,CAAC;QACF,aAAa,CAAC,GAAG,KAAK,MAAM,EAAE,YAAY,CAAC,CAAC;QAC5C,MAAM,GAAG,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;QAChC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QACxC,MAAM,CAAC,GAAG,KAAK,MAAM,CAAC,CAAC;IACzB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;QACpD,MAAM,IAAI,GAAG,SAAS,CAAC,cAAc,EAAE,4BAA4B,CAAC,CAAC;QACrE,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,kBAAkB,CAAC,CAAC;IAC9D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;QACvD,MAAM,IAAI,GAAG,SAAS,CAAC,0BAA0B,EAAE,wBAAwB,CAAC,CAAC;QAC7E,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,kBAAkB,CAAC,CAAC;IAC9D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;QACvD,MAAM,IAAI,GAAG,SAAS,CACpB,kBAAkB,EAClB;;;mBAGa,CACd,CAAC;QACF,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC,CAAC;IACtD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+BAA+B,EAAE,GAAG,EAAE;QACvC,MAAM,IAAI,GAAG,SAAS,CACpB,iBAAiB,EACjB;;YAEM,CACP,CAAC;QACF,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,SAAS,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;QACvD,MAAM,IAAI,GAAG,SAAS,CACpB,iBAAiB,EACjB;gBACU,CACX,CAAC;QACF,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,kBAAkB,CAAC,CAAC;IAC9D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oDAAoD,EAAE,GAAG,EAAE;QAC5D,MAAM,IAAI,GAAG,SAAS,CACpB,wBAAwB,EACxB;;;mBAGa,CACd,CAAC;QACF,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;QAC/B,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0BAA0B,EAAE,GAAG,EAAE;QAClC,MAAM,IAAI,GAAG,SAAS,CACpB,gBAAgB,EAChB;;;;mBAIa,CACd,CAAC;QACF,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;QAC/B,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0EAA0E,EAAE,GAAG,EAAE;QAClF,MAAM,IAAI,GAAG,SAAS,CACpB,iBAAiB,EACjB;;;;mBAIa,CACd,CAAC;QACF,MAAM,CAAC,MAAM,CACX,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,EACxB,CAAC,GAAY,EAAE,EAAE,CACf,GAAG,YAAY,kBAAkB;YACjC,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC;YACnC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CACzB,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;QAC3C,MAAM,IAAI,GAAG,SAAS,CACpB,iBAAiB,EACjB;;;;mBAIa,CACd,CAAC;QACF,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,kBAAkB,CAAC,CAAC;IAC9D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qEAAqE,EAAE,GAAG,EAAE;QAC7E,mEAAmE;QACnE,qDAAqD;QACrD,MAAM,CAAC,KAAK,CAAC,qCAAqC,EAAE,CAAC,CAAC,CAAC;IACzD,CAAC,CAAC,CAAC;IAEH,kEAAkE;IAElE,EAAE,CAAC,kFAAkF,EAAE,GAAG,EAAE;QAC1F,MAAM,IAAI,GAAG,SAAS,CACpB,kBAAkB,EAClB;;;mBAGa,CACd,CAAC;QACF,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;QAC/B,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC;QACnC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;QACvC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAE,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;IAC1D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uEAAuE,EAAE,GAAG,EAAE;QAC/E,MAAM,IAAI,GAAG,SAAS,CACpB,cAAc,EACd;;;;;;;;;;;;mBAYa,CACd,CAAC;QACF,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;QAC/B,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC;QACnC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC,CAAC;QAChE,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAE,CAAC,UAAU,EAAE,CAAC,UAAU,EAAE,mBAAmB,CAAC,CAAC,CAAC;QACpF,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAE,CAAC,UAAU,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;QACpD,MAAM,IAAI,GAAG,SAAS,CACpB,mBAAmB,EACnB;;gBAEU,CACX,CAAC;QACF,MAAM,CAAC,MAAM,CACX,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,EACxB,CAAC,GAAY,EAAE,EAAE,CACf,GAAG,YAAY,kBAAkB,IAAI,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CACtE,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,IAAI,GAAG,SAAS,CACpB,wBAAwB,EACxB;;;;;gBAKU,CACX,CAAC;QACF,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,kBAAkB,CAAC,CAAC;IAC9D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC1C,MAAM,IAAI,GAAG,SAAS,CACpB,oBAAoB,EACpB;;;;;gBAKU,CACX,CAAC;QACF,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,kBAAkB,CAAC,CAAC;IAC9D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,IAAI,GAAG,SAAS,CACpB,eAAe,EACf;;;;;gBAKU,CACX,CAAC;QACF,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,kBAAkB,CAAC,CAAC;IAC9D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yFAAyF,EAAE,GAAG,EAAE;QACjG,MAAM,IAAI,GAAG,SAAS,CACpB,iBAAiB,EACjB;;;;;;gBAMU,CACX,CAAC;QACF,MAAM,CAAC,MAAM,CACX,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,EACxB,CAAC,GAAY,EAAE,EAAE,CACf,GAAG,YAAY,kBAAkB;YACjC,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC;YAChC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC;YAC7B,oBAAoB,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CACzC,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+EAA+E,EAAE,GAAG,EAAE;QACvF,MAAM,IAAI,GAAG,SAAS,CACpB,aAAa,EACb;;;;;;;;;gBASU,CACX,CAAC;QACF,MAAM,CAAC,MAAM,CACX,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,EACxB,CAAC,GAAY,EAAE,EAAE,CACf,GAAG,YAAY,kBAAkB;YACjC,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC;YAC9B,wBAAwB,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC;YAC1C,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAC1B,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;QACvD,MAAM,IAAI,GAAG,SAAS,CACpB,sBAAsB,EACtB;;gBAEU,CACX,CAAC;QACF,MAAM,CAAC,MAAM,CACX,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,EACxB,CAAC,GAAY,EAAE,EAAE,CACf,GAAG,YAAY,kBAAkB,IAAI,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CACtE,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0EAA0E,EAAE,GAAG,EAAE;QAClF,MAAM,IAAI,GAAG,SAAS,CACpB,SAAS,EACT;;;;mBAIa,CACd,CAAC;QACF,MAAM,CAAC,MAAM,CACX,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,EACxB,CAAC,GAAY,EAAE,EAAE,CACf,GAAG,YAAY,kBAAkB;YACjC,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC;YACnC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CACxB,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,UAAU,EAAE,GAAG,EAAE;IACxB,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACnD,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC;QAClE,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC;IACjF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QAC1B,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC;QACvC,MAAM,CAAC,KAAK,CACV,QAAQ,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EAC3F,IAAI,CACL,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,CAAC;QACvC,MAAM,CAAC,KAAK,CACV,QAAQ,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EACxF,KAAK,CACN,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;QACzD,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC;IACzF,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IACjC,MAAM,GAAG,GAAG;QACV,WAAW,EAAE;YACX,EAAE,EAAE,EAAE,iBAAiB,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,EAAE,EAAE;YAC1D,EAAE,EAAE,EAAE,iBAAiB,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,EAAE,EAAE;YAC1D,EAAE,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,EAAE,EAAE;SACrD;QACD,WAAW,EAAE,EAAE;QACf,aAAa,EAAE,CAAC;KACjB,CAAC;IAEF,EAAE,CAAC,qBAAqB,EAAE,GAAG,EAAE;QAC7B,MAAM,CAAC,GAAG,iBAAiB,CAAC,GAAG,EAAE,iBAAiB,CAAC,CAAC;QACpD,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,EAAE,iBAAiB,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QAClD,MAAM,CAAC,GAAG,iBAAiB,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;QAC/C,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,EAAE,iBAAiB,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC1C,MAAM,CAAC,GAAG,iBAAiB,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QACzC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,EAAE,iBAAiB,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACnD,MAAM,CAAC,GAAG,iBAAiB,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;QAC7C,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QAC5B,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4DAA4D,EAAE,GAAG,EAAE;QACpE,MAAM,CAAC,GAAG,iBAAiB,CAAC,GAAG,EAAE,aAAa,CAAC,CAAC;QAChD,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QAC5B,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;QACvD,MAAM,CAAC,GAAG,iBAAiB,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;QAC9C,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,EAAE,YAAY,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,18 @@
1
+ export interface ParsedRemote {
2
+ /** Always `"github.com"` in v1; ssh-alias suffixes are stripped. */
3
+ host: string;
4
+ /** Lowercased GitHub org name. */
5
+ org: string;
6
+ /** Lowercased GitHub repo name (no `.git` suffix). */
7
+ repo: string;
8
+ }
9
+ /**
10
+ * Parse a git remote URL into `{ host, org, repo }`. Returns `null`
11
+ * for malformed input or non-github hosts. Never throws.
12
+ *
13
+ * Org and repo are lowercased in the output. The original casing is
14
+ * not preserved — callers that need the casing-as-typed must read
15
+ * the raw remote themselves.
16
+ */
17
+ export declare function parseRemoteUrl(raw: unknown): ParsedRemote | null;
18
+ //# sourceMappingURL=remote-url.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-url.d.ts","sourceRoot":"","sources":["../src/remote-url.ts"],"names":[],"mappings":"AAgBA,MAAM,WAAW,YAAY;IAC3B,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;CACd;AAgBD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,YAAY,GAAG,IAAI,CA8BhE"}
@@ -0,0 +1,66 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+ // Copyright (C) 2026 Richard Myers and contributors.
3
+ //
4
+ // Pure parser for git remote URLs. Used by `classify` to derive the
5
+ // engagement (or personal-org) attribution from `git remote get-url
6
+ // origin`.
7
+ //
8
+ // The parser is total: malformed input returns `null` rather than
9
+ // throwing. This is load-bearing — `parseRemoteUrl` runs in the JIT
10
+ // classify path, and a thrown exception there would surface to the
11
+ // agent as a tool-failure rather than the intended `skipped` status.
12
+ //
13
+ // Phase 1 scope: github.com only (including multi-account ssh aliases
14
+ // like `git@github.com-personal:`). Non-github hosts return `null`;
15
+ // extending to gitlab/bitbucket is a deliberate follow-up.
16
+ // SSH form: `git@github.com[-<alias>]:<org>/<repo>[.git][/]`
17
+ // The optional `-<alias>` segment is the multi-account ssh pattern
18
+ // recommended by GitHub for users with multiple accounts on one
19
+ // machine — e.g. `git@github.com-personal:foo/bar.git`. The alias
20
+ // is stripped and the host normalised back to `github.com`.
21
+ const SSH_RE = /^git@github\.com(?:-[a-zA-Z0-9_-]+)?:([a-zA-Z0-9][a-zA-Z0-9-]*)\/([a-zA-Z0-9._-]+?)(?:\.git)?\/?$/;
22
+ // URL forms: `(http|https|ssh)://[user[:pw]@]github.com/<org>/<repo>[.git][/]`
23
+ // Credential prefix (`user@` or `user:pw@`) is stripped. We do not
24
+ // validate password content; the gate path never sees this URL.
25
+ const URL_RE = /^(?:https?|ssh):\/\/(?:[^@/]+@)?github\.com\/([a-zA-Z0-9][a-zA-Z0-9-]*)\/([a-zA-Z0-9._-]+?)(?:\.git)?\/?$/;
26
+ /**
27
+ * Parse a git remote URL into `{ host, org, repo }`. Returns `null`
28
+ * for malformed input or non-github hosts. Never throws.
29
+ *
30
+ * Org and repo are lowercased in the output. The original casing is
31
+ * not preserved — callers that need the casing-as-typed must read
32
+ * the raw remote themselves.
33
+ */
34
+ export function parseRemoteUrl(raw) {
35
+ if (typeof raw !== "string")
36
+ return null;
37
+ const url = raw.trim();
38
+ if (url.length === 0)
39
+ return null;
40
+ const sshMatch = SSH_RE.exec(url);
41
+ if (sshMatch) {
42
+ const org = sshMatch[1];
43
+ const repo = sshMatch[2];
44
+ if (org === undefined || repo === undefined)
45
+ return null;
46
+ return {
47
+ host: "github.com",
48
+ org: org.toLowerCase(),
49
+ repo: repo.toLowerCase(),
50
+ };
51
+ }
52
+ const urlMatch = URL_RE.exec(url);
53
+ if (urlMatch) {
54
+ const org = urlMatch[1];
55
+ const repo = urlMatch[2];
56
+ if (org === undefined || repo === undefined)
57
+ return null;
58
+ return {
59
+ host: "github.com",
60
+ org: org.toLowerCase(),
61
+ repo: repo.toLowerCase(),
62
+ };
63
+ }
64
+ return null;
65
+ }
66
+ //# sourceMappingURL=remote-url.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-url.js","sourceRoot":"","sources":["../src/remote-url.ts"],"names":[],"mappings":"AAAA,4CAA4C;AAC5C,qDAAqD;AACrD,EAAE;AACF,oEAAoE;AACpE,oEAAoE;AACpE,WAAW;AACX,EAAE;AACF,kEAAkE;AAClE,oEAAoE;AACpE,mEAAmE;AACnE,qEAAqE;AACrE,EAAE;AACF,sEAAsE;AACtE,oEAAoE;AACpE,2DAA2D;AAW3D,6DAA6D;AAC7D,mEAAmE;AACnE,gEAAgE;AAChE,kEAAkE;AAClE,4DAA4D;AAC5D,MAAM,MAAM,GACV,mGAAmG,CAAC;AAEtG,+EAA+E;AAC/E,mEAAmE;AACnE,gEAAgE;AAChE,MAAM,MAAM,GACV,2GAA2G,CAAC;AAE9G;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,GAAY;IACzC,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACzC,MAAM,GAAG,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IACvB,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAElC,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAClC,IAAI,QAAQ,EAAE,CAAC;QACb,MAAM,GAAG,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;QACxB,MAAM,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;QACzB,IAAI,GAAG,KAAK,SAAS,IAAI,IAAI,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC;QACzD,OAAO;YACL,IAAI,EAAE,YAAY;YAClB,GAAG,EAAE,GAAG,CAAC,WAAW,EAAE;YACtB,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE;SACzB,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAClC,IAAI,QAAQ,EAAE,CAAC;QACb,MAAM,GAAG,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;QACxB,MAAM,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;QACzB,IAAI,GAAG,KAAK,SAAS,IAAI,IAAI,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC;QACzD,OAAO;YACL,IAAI,EAAE,YAAY;YAClB,GAAG,EAAE,GAAG,CAAC,WAAW,EAAE;YACtB,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE;SACzB,CAAC;IACJ,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=remote-url.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-url.test.d.ts","sourceRoot":"","sources":["../src/remote-url.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,116 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+ // Copyright (C) 2026 Richard Myers and contributors.
3
+ import { describe, it } from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import { parseRemoteUrl } from "./remote-url.js";
6
+ describe("parseRemoteUrl — accepted forms", () => {
7
+ const cases = [
8
+ // [input, org, repo]
9
+ ["https://github.com/foo/bar", "foo", "bar"],
10
+ ["https://github.com/foo/bar.git", "foo", "bar"],
11
+ ["https://github.com/foo/bar/", "foo", "bar"],
12
+ ["http://github.com/foo/bar", "foo", "bar"],
13
+ ["http://github.com/foo/bar.git", "foo", "bar"],
14
+ ["git@github.com:foo/bar", "foo", "bar"],
15
+ ["git@github.com:foo/bar.git", "foo", "bar"],
16
+ ["git@github.com-personal:foo/bar.git", "foo", "bar"],
17
+ ["git@github.com-work:foo/bar.git", "foo", "bar"],
18
+ ["git@github.com-MIXED_alias-1:foo/bar", "foo", "bar"],
19
+ ["ssh://git@github.com/foo/bar.git", "foo", "bar"],
20
+ ["ssh://git@github.com/foo/bar", "foo", "bar"],
21
+ ["https://user@github.com/foo/bar.git", "foo", "bar"],
22
+ ["https://user:token@github.com/foo/bar.git", "foo", "bar"],
23
+ // Lowercasing
24
+ ["https://github.com/Foo-Corp/Bar-Repo.git", "foo-corp", "bar-repo"],
25
+ ["git@github.com:DELL/UMP.git", "dell", "ump"],
26
+ // Whitespace trimmed
27
+ [" https://github.com/foo/bar.git \n", "foo", "bar"],
28
+ // Hyphens, digits, dots in repo names
29
+ ["https://github.com/foo/bar.baz.git", "foo", "bar.baz"],
30
+ ["https://github.com/foo/bar_baz", "foo", "bar_baz"],
31
+ ["https://github.com/foo/bar-123", "foo", "bar-123"],
32
+ ];
33
+ for (const [input, org, repo] of cases) {
34
+ it(`parses ${JSON.stringify(input)}`, () => {
35
+ const parsed = parseRemoteUrl(input);
36
+ assert.deepEqual(parsed, { host: "github.com", org, repo });
37
+ });
38
+ }
39
+ });
40
+ describe("parseRemoteUrl — rejected forms (return null)", () => {
41
+ const cases = [
42
+ // Non-string
43
+ "",
44
+ " ",
45
+ // Non-github hosts
46
+ "https://gitlab.com/foo/bar.git",
47
+ "git@gitlab.com:foo/bar.git",
48
+ "https://bitbucket.org/foo/bar.git",
49
+ "https://example.com/foo/bar",
50
+ "git@example.com:foo/bar.git",
51
+ // Self-hosted github-like host
52
+ "https://github.example.com/foo/bar.git",
53
+ "https://my-github.com/foo/bar.git",
54
+ // Missing repo
55
+ "https://github.com/foo",
56
+ "https://github.com/foo/",
57
+ "git@github.com:foo",
58
+ // Missing org
59
+ "https://github.com//bar",
60
+ "git@github.com:/bar.git",
61
+ // Org starting with hyphen
62
+ "https://github.com/-bad/bar",
63
+ "git@github.com:-bad/bar.git",
64
+ // Extra path segments
65
+ "https://github.com/foo/bar/tree/main",
66
+ "https://github.com/foo/bar/issues/1",
67
+ // Garbage
68
+ "not a url",
69
+ "://github.com/foo/bar",
70
+ "github.com/foo/bar",
71
+ // Empty parts
72
+ "https://github.com/",
73
+ "git@github.com:",
74
+ ];
75
+ for (const input of cases) {
76
+ it(`rejects ${JSON.stringify(input)}`, () => {
77
+ assert.equal(parseRemoteUrl(input), null);
78
+ });
79
+ }
80
+ it("returns null for non-string input (typed)", () => {
81
+ assert.equal(parseRemoteUrl(undefined), null);
82
+ assert.equal(parseRemoteUrl(null), null);
83
+ assert.equal(parseRemoteUrl(42), null);
84
+ assert.equal(parseRemoteUrl({}), null);
85
+ assert.equal(parseRemoteUrl([]), null);
86
+ });
87
+ });
88
+ describe("parseRemoteUrl — fuzz", () => {
89
+ it("never throws on random ASCII input", () => {
90
+ const seed = 0xc0ffee;
91
+ let s = seed;
92
+ function rand() {
93
+ // xorshift32 — deterministic
94
+ s ^= s << 13;
95
+ s ^= s >>> 17;
96
+ s ^= s << 5;
97
+ return Math.abs(s);
98
+ }
99
+ for (let i = 0; i < 1000; i++) {
100
+ const len = rand() % 64;
101
+ let str = "";
102
+ for (let j = 0; j < len; j++) {
103
+ str += String.fromCharCode(0x20 + (rand() % 95));
104
+ }
105
+ // Should never throw, regardless of input.
106
+ const result = parseRemoteUrl(str);
107
+ // Sanity: result is null or has the expected shape.
108
+ if (result !== null) {
109
+ assert.equal(typeof result.host, "string");
110
+ assert.equal(typeof result.org, "string");
111
+ assert.equal(typeof result.repo, "string");
112
+ }
113
+ }
114
+ });
115
+ });
116
+ //# sourceMappingURL=remote-url.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-url.test.js","sourceRoot":"","sources":["../src/remote-url.test.ts"],"names":[],"mappings":"AAAA,4CAA4C;AAC5C,qDAAqD;AACrD,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEjD,QAAQ,CAAC,iCAAiC,EAAE,GAAG,EAAE;IAC/C,MAAM,KAAK,GAAoC;QAC7C,qBAAqB;QACrB,CAAC,4BAA4B,EAAE,KAAK,EAAE,KAAK,CAAC;QAC5C,CAAC,gCAAgC,EAAE,KAAK,EAAE,KAAK,CAAC;QAChD,CAAC,6BAA6B,EAAE,KAAK,EAAE,KAAK,CAAC;QAC7C,CAAC,2BAA2B,EAAE,KAAK,EAAE,KAAK,CAAC;QAC3C,CAAC,+BAA+B,EAAE,KAAK,EAAE,KAAK,CAAC;QAC/C,CAAC,wBAAwB,EAAE,KAAK,EAAE,KAAK,CAAC;QACxC,CAAC,4BAA4B,EAAE,KAAK,EAAE,KAAK,CAAC;QAC5C,CAAC,qCAAqC,EAAE,KAAK,EAAE,KAAK,CAAC;QACrD,CAAC,iCAAiC,EAAE,KAAK,EAAE,KAAK,CAAC;QACjD,CAAC,sCAAsC,EAAE,KAAK,EAAE,KAAK,CAAC;QACtD,CAAC,kCAAkC,EAAE,KAAK,EAAE,KAAK,CAAC;QAClD,CAAC,8BAA8B,EAAE,KAAK,EAAE,KAAK,CAAC;QAC9C,CAAC,qCAAqC,EAAE,KAAK,EAAE,KAAK,CAAC;QACrD,CAAC,2CAA2C,EAAE,KAAK,EAAE,KAAK,CAAC;QAC3D,cAAc;QACd,CAAC,0CAA0C,EAAE,UAAU,EAAE,UAAU,CAAC;QACpE,CAAC,6BAA6B,EAAE,MAAM,EAAE,KAAK,CAAC;QAC9C,qBAAqB;QACrB,CAAC,sCAAsC,EAAE,KAAK,EAAE,KAAK,CAAC;QACtD,sCAAsC;QACtC,CAAC,oCAAoC,EAAE,KAAK,EAAE,SAAS,CAAC;QACxD,CAAC,gCAAgC,EAAE,KAAK,EAAE,SAAS,CAAC;QACpD,CAAC,gCAAgC,EAAE,KAAK,EAAE,SAAS,CAAC;KACrD,CAAC;IAEF,KAAK,MAAM,CAAC,KAAK,EAAE,GAAG,EAAE,IAAI,CAAC,IAAI,KAAK,EAAE,CAAC;QACvC,EAAE,CAAC,UAAU,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,EAAE,GAAG,EAAE;YACzC,MAAM,MAAM,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;YACrC,MAAM,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC;QAC9D,CAAC,CAAC,CAAC;IACL,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,+CAA+C,EAAE,GAAG,EAAE;IAC7D,MAAM,KAAK,GAAa;QACtB,aAAa;QACb,EAAE;QACF,KAAK;QACL,mBAAmB;QACnB,gCAAgC;QAChC,4BAA4B;QAC5B,mCAAmC;QACnC,6BAA6B;QAC7B,6BAA6B;QAC7B,+BAA+B;QAC/B,wCAAwC;QACxC,mCAAmC;QACnC,eAAe;QACf,wBAAwB;QACxB,yBAAyB;QACzB,oBAAoB;QACpB,cAAc;QACd,yBAAyB;QACzB,yBAAyB;QACzB,2BAA2B;QAC3B,6BAA6B;QAC7B,6BAA6B;QAC7B,sBAAsB;QACtB,sCAAsC;QACtC,qCAAqC;QACrC,UAAU;QACV,WAAW;QACX,uBAAuB;QACvB,oBAAoB;QACpB,cAAc;QACd,qBAAqB;QACrB,iBAAiB;KAClB,CAAC;IAEF,KAAK,MAAM,KAAK,IAAI,KAAK,EAAE,CAAC;QAC1B,EAAE,CAAC,WAAW,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,EAAE,GAAG,EAAE;YAC1C,MAAM,CAAC,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,CAAC;QAC5C,CAAC,CAAC,CAAC;IACL,CAAC;IAED,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACnD,MAAM,CAAC,KAAK,CAAC,cAAc,CAAC,SAAS,CAAC,EAAE,IAAI,CAAC,CAAC;QAC9C,MAAM,CAAC,KAAK,CAAC,cAAc,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC;QACzC,MAAM,CAAC,KAAK,CAAC,cAAc,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC;QACvC,MAAM,CAAC,KAAK,CAAC,cAAc,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC;QACvC,MAAM,CAAC,KAAK,CAAC,cAAc,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,uBAAuB,EAAE,GAAG,EAAE;IACrC,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,IAAI,GAAG,QAAQ,CAAC;QACtB,IAAI,CAAC,GAAG,IAAI,CAAC;QACb,SAAS,IAAI;YACX,6BAA6B;YAC7B,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;YACb,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YACd,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACZ,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACrB,CAAC;QACD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;YAC9B,MAAM,GAAG,GAAG,IAAI,EAAE,GAAG,EAAE,CAAC;YACxB,IAAI,GAAG,GAAG,EAAE,CAAC;YACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;gBAC7B,GAAG,IAAI,MAAM,CAAC,YAAY,CAAC,IAAI,GAAG,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;YACnD,CAAC;YACD,2CAA2C;YAC3C,MAAM,MAAM,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC;YACnC,oDAAoD;YACpD,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;gBACpB,MAAM,CAAC,KAAK,CAAC,OAAO,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;gBAC3C,MAAM,CAAC,KAAK,CAAC,OAAO,MAAM,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;gBAC1C,MAAM,CAAC,KAAK,CAAC,OAAO,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;YAC7C,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,54 @@
1
+ import { type Registry } from "./registry.js";
2
+ /**
3
+ * Format version of marker files written by {@link renderMarkers}. Emitted
4
+ * as the second header line (`; repo-aegis-marker-format: <N>`) of every
5
+ * generated marker file. The deny-set parser ignores `;`-comment lines, so
6
+ * older readers tolerate the field; future readers can branch on it. Per
7
+ * design B14: writers must never lower this version.
8
+ */
9
+ export declare const MARKER_FORMAT_VERSION = 1;
10
+ export interface RenderOptions {
11
+ dryRun?: boolean;
12
+ markersDir?: string;
13
+ flatPath?: string;
14
+ retentionMonths?: number;
15
+ /**
16
+ * If true (default), all marker patterns across the registry are validated
17
+ * before any file is written. Patterns failing validation cause render to
18
+ * throw `PatternValidationError` without writing.
19
+ */
20
+ validatePatterns?: boolean;
21
+ }
22
+ export interface RenderedFile {
23
+ path: string;
24
+ engagementId: string;
25
+ patternCount: number;
26
+ }
27
+ export interface RenderResult {
28
+ written: RenderedFile[];
29
+ removed: string[];
30
+ flat: string | null;
31
+ invalidPatterns: {
32
+ engagementId: string;
33
+ pattern: string;
34
+ reason: string;
35
+ }[];
36
+ }
37
+ /**
38
+ * Generate per-engagement marker files from the registry.
39
+ *
40
+ * Behaviour:
41
+ * 1. Validate every pattern across all engagements + alwaysBlock. If any
42
+ * fail validation, throw `PatternValidationError` and write nothing.
43
+ * 2. Write `markers/_always.txt` from `reg.alwaysBlock`.
44
+ * 3. For each engagement where `isActive(e, retentionMonths)` is true,
45
+ * write `markers/<id>.txt`.
46
+ * 4. Compare existing marker files against the new set; delete files
47
+ * whose stem is not in the new set.
48
+ * 5. Write the flat union `markers.txt` for back-compat.
49
+ *
50
+ * All files are written with mode 0600. The markers directory is created
51
+ * with mode 0700.
52
+ */
53
+ export declare function renderMarkers(reg: Registry, opts?: RenderOptions): RenderResult;
54
+ //# sourceMappingURL=render.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AASA,OAAO,EAAY,KAAK,QAAQ,EAAmB,MAAM,eAAe,CAAC;AAKzE;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAEvC,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,eAAe,EAAE;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC9E;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,GAAE,aAAkB,GAAG,YAAY,CAkHnF"}