@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.
- package/dist/age.d.ts +32 -0
- package/dist/age.d.ts.map +1 -0
- package/dist/age.js +98 -0
- package/dist/age.js.map +1 -0
- package/dist/audit-log.d.ts +50 -0
- package/dist/audit-log.d.ts.map +1 -0
- package/dist/audit-log.js +183 -0
- package/dist/audit-log.js.map +1 -0
- package/dist/audit-log.test.d.ts +2 -0
- package/dist/audit-log.test.d.ts.map +1 -0
- package/dist/audit-log.test.js +181 -0
- package/dist/audit-log.test.js.map +1 -0
- package/dist/deny-set.d.ts +43 -0
- package/dist/deny-set.d.ts.map +1 -0
- package/dist/deny-set.js +165 -0
- package/dist/deny-set.js.map +1 -0
- package/dist/deny-set.test.d.ts +2 -0
- package/dist/deny-set.test.d.ts.map +1 -0
- package/dist/deny-set.test.js +155 -0
- package/dist/deny-set.test.js.map +1 -0
- package/dist/exceptions.d.ts +96 -0
- package/dist/exceptions.d.ts.map +1 -0
- package/dist/exceptions.js +143 -0
- package/dist/exceptions.js.map +1 -0
- package/dist/exit-codes.d.ts +4 -0
- package/dist/exit-codes.d.ts.map +1 -0
- package/dist/exit-codes.js +6 -0
- package/dist/exit-codes.js.map +1 -0
- package/dist/first-touch.d.ts +57 -0
- package/dist/first-touch.d.ts.map +1 -0
- package/dist/first-touch.js +112 -0
- package/dist/first-touch.js.map +1 -0
- package/dist/import-graph.test.d.ts +2 -0
- package/dist/import-graph.test.d.ts.map +1 -0
- package/dist/import-graph.test.js +210 -0
- package/dist/import-graph.test.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +68 -0
- package/dist/index.js.map +1 -0
- package/dist/lock.d.ts +22 -0
- package/dist/lock.d.ts.map +1 -0
- package/dist/lock.js +86 -0
- package/dist/lock.js.map +1 -0
- package/dist/lock.test.d.ts +2 -0
- package/dist/lock.test.d.ts.map +1 -0
- package/dist/lock.test.js +125 -0
- package/dist/lock.test.js.map +1 -0
- package/dist/paths.d.ts +22 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +46 -0
- package/dist/paths.js.map +1 -0
- package/dist/paths.test.d.ts +2 -0
- package/dist/paths.test.d.ts.map +1 -0
- package/dist/paths.test.js +78 -0
- package/dist/paths.test.js.map +1 -0
- package/dist/redaction.d.ts +29 -0
- package/dist/redaction.d.ts.map +1 -0
- package/dist/redaction.js +48 -0
- package/dist/redaction.js.map +1 -0
- package/dist/redaction.test.d.ts +2 -0
- package/dist/redaction.test.d.ts.map +1 -0
- package/dist/redaction.test.js +67 -0
- package/dist/redaction.test.js.map +1 -0
- package/dist/regex-safety.d.ts +87 -0
- package/dist/regex-safety.d.ts.map +1 -0
- package/dist/regex-safety.js +322 -0
- package/dist/regex-safety.js.map +1 -0
- package/dist/regex-safety.test.d.ts +2 -0
- package/dist/regex-safety.test.d.ts.map +1 -0
- package/dist/regex-safety.test.js +149 -0
- package/dist/regex-safety.test.js.map +1 -0
- package/dist/registry-mutate.d.ts +35 -0
- package/dist/registry-mutate.d.ts.map +1 -0
- package/dist/registry-mutate.js +149 -0
- package/dist/registry-mutate.js.map +1 -0
- package/dist/registry-mutate.test.d.ts +2 -0
- package/dist/registry-mutate.test.d.ts.map +1 -0
- package/dist/registry-mutate.test.js +96 -0
- package/dist/registry-mutate.test.js.map +1 -0
- package/dist/registry.d.ts +64 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +120 -0
- package/dist/registry.js.map +1 -0
- package/dist/registry.test.d.ts +2 -0
- package/dist/registry.test.d.ts.map +1 -0
- package/dist/registry.test.js +316 -0
- package/dist/registry.test.js.map +1 -0
- package/dist/remote-url.d.ts +18 -0
- package/dist/remote-url.d.ts.map +1 -0
- package/dist/remote-url.js +66 -0
- package/dist/remote-url.js.map +1 -0
- package/dist/remote-url.test.d.ts +2 -0
- package/dist/remote-url.test.d.ts.map +1 -0
- package/dist/remote-url.test.js +116 -0
- package/dist/remote-url.test.js.map +1 -0
- package/dist/render.d.ts +54 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +182 -0
- package/dist/render.js.map +1 -0
- package/dist/render.test.d.ts +2 -0
- package/dist/render.test.d.ts.map +1 -0
- package/dist/render.test.js +152 -0
- package/dist/render.test.js.map +1 -0
- package/dist/repo.d.ts +40 -0
- package/dist/repo.d.ts.map +1 -0
- package/dist/repo.js +214 -0
- package/dist/repo.js.map +1 -0
- package/dist/repo.test.d.ts +2 -0
- package/dist/repo.test.d.ts.map +1 -0
- package/dist/repo.test.js +234 -0
- package/dist/repo.test.js.map +1 -0
- package/dist/scan.d.ts +103 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +436 -0
- package/dist/scan.js.map +1 -0
- package/dist/scan.test.d.ts +2 -0
- package/dist/scan.test.d.ts.map +1 -0
- package/dist/scan.test.js +437 -0
- package/dist/scan.test.js.map +1 -0
- package/dist/schemas.d.ts +50 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +190 -0
- package/dist/schemas.js.map +1 -0
- package/dist/secret-markers.d.ts +34 -0
- package/dist/secret-markers.d.ts.map +1 -0
- package/dist/secret-markers.js +118 -0
- package/dist/secret-markers.js.map +1 -0
- package/dist/secret-markers.test.d.ts +2 -0
- package/dist/secret-markers.test.d.ts.map +1 -0
- package/dist/secret-markers.test.js +154 -0
- package/dist/secret-markers.test.js.map +1 -0
- package/dist/trust-boundary.d.ts +33 -0
- package/dist/trust-boundary.d.ts.map +1 -0
- package/dist/trust-boundary.js +77 -0
- package/dist/trust-boundary.js.map +1 -0
- package/dist/trust-boundary.test.d.ts +2 -0
- package/dist/trust-boundary.test.d.ts.map +1 -0
- package/dist/trust-boundary.test.js +170 -0
- package/dist/trust-boundary.test.js.map +1 -0
- package/dist/types.d.ts +47 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/working-tree.d.ts +38 -0
- package/dist/working-tree.d.ts.map +1 -0
- package/dist/working-tree.js +133 -0
- package/dist/working-tree.js.map +1 -0
- package/dist/working-tree.test.d.ts +2 -0
- package/dist/working-tree.test.d.ts.map +1 -0
- package/dist/working-tree.test.js +162 -0
- package/dist/working-tree.test.js.map +1 -0
- package/package.json +40 -0
- package/src/age.ts +113 -0
- package/src/audit-log.test.ts +222 -0
- package/src/audit-log.ts +215 -0
- package/src/deny-set.test.ts +208 -0
- package/src/deny-set.ts +231 -0
- package/src/exceptions.ts +134 -0
- package/src/exit-codes.ts +5 -0
- package/src/first-touch.ts +172 -0
- package/src/import-graph.test.ts +239 -0
- package/src/index.ts +191 -0
- package/src/lock.test.ts +151 -0
- package/src/lock.ts +88 -0
- package/src/paths.test.ts +94 -0
- package/src/paths.ts +55 -0
- package/src/redaction.test.ts +81 -0
- package/src/redaction.ts +49 -0
- package/src/regex-safety.test.ts +194 -0
- package/src/regex-safety.ts +349 -0
- package/src/registry-mutate.test.ts +134 -0
- package/src/registry-mutate.ts +185 -0
- package/src/registry.test.ts +460 -0
- package/src/registry.ts +178 -0
- package/src/remote-url.test.ts +121 -0
- package/src/remote-url.ts +78 -0
- package/src/render.test.ts +206 -0
- package/src/render.ts +215 -0
- package/src/repo.test.ts +275 -0
- package/src/repo.ts +245 -0
- package/src/scan.test.ts +580 -0
- package/src/scan.ts +531 -0
- package/src/schemas.ts +207 -0
- package/src/secret-markers.test.ts +183 -0
- package/src/secret-markers.ts +145 -0
- package/src/trust-boundary.test.ts +198 -0
- package/src/trust-boundary.ts +98 -0
- package/src/types.ts +55 -0
- package/src/working-tree.test.ts +193 -0
- 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":"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"}
|
|
@@ -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 @@
|
|
|
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":"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"}
|
package/dist/render.d.ts
ADDED
|
@@ -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"}
|