@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,172 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Richard Myers and contributors.
|
|
3
|
+
//
|
|
4
|
+
// Phase 1 onboarding: just-in-time classification of a repo from its
|
|
5
|
+
// git remote against the engagement registry. Used by:
|
|
6
|
+
// - the MCP `aegis_classify_first_touch` tool (agent-facing entry).
|
|
7
|
+
// - the CLI `repo-aegis hook first-touch` subcommand (Claude Code
|
|
8
|
+
// SessionStart hook entry).
|
|
9
|
+
//
|
|
10
|
+
// Pure-ish: reads git config / remote URL via execFileSync, reads the
|
|
11
|
+
// registry, mutates only the *per-repo* `git config` (setClass /
|
|
12
|
+
// addEngagement) on the `applied` path. Never mutates the registry —
|
|
13
|
+
// that's a follow-up requiring user confirmation, per the agent guide.
|
|
14
|
+
|
|
15
|
+
import { execFileSync } from "node:child_process";
|
|
16
|
+
import { loadRegistry, type Engagement, type Registry } from "./registry.js";
|
|
17
|
+
import { RegistryNotFoundError } from "./exceptions.js";
|
|
18
|
+
import { parseRemoteUrl } from "./remote-url.js";
|
|
19
|
+
import { readRepoConfig, setClass, addEngagement } from "./repo.js";
|
|
20
|
+
|
|
21
|
+
export type FirstTouchSkipReason =
|
|
22
|
+
| "non-git"
|
|
23
|
+
| "no-remote"
|
|
24
|
+
| "non-github-host"
|
|
25
|
+
| "registry-not-found";
|
|
26
|
+
|
|
27
|
+
export interface FirstTouchAlreadyClassified {
|
|
28
|
+
status: "already-classified";
|
|
29
|
+
class: string;
|
|
30
|
+
engagements: string[];
|
|
31
|
+
}
|
|
32
|
+
export interface FirstTouchApplied {
|
|
33
|
+
status: "applied";
|
|
34
|
+
class: string;
|
|
35
|
+
engagement: string | null;
|
|
36
|
+
/**
|
|
37
|
+
* [SEC H-5] follow-up: when the applied path attaches an engagement
|
|
38
|
+
* with zero markers, surface a warning the agent can show the user.
|
|
39
|
+
* Closes the window where a freshly registered org has no marker yet.
|
|
40
|
+
*/
|
|
41
|
+
markerWarning: { engagementId: string; count: 0 } | null;
|
|
42
|
+
}
|
|
43
|
+
export interface FirstTouchNeedsConfirmation {
|
|
44
|
+
status: "needs-confirmation";
|
|
45
|
+
remote: string;
|
|
46
|
+
org: string;
|
|
47
|
+
/**
|
|
48
|
+
* [SEC H-5] redacted form the agent should use in any context-bearing
|
|
49
|
+
* summary. Equal to `org` when the org is too short to redact (< 4 chars).
|
|
50
|
+
*/
|
|
51
|
+
redactedOrg: string;
|
|
52
|
+
suggestion:
|
|
53
|
+
| { newEngagement: { idHint: string } }
|
|
54
|
+
| { addToExisting: { engagementId: string } }
|
|
55
|
+
| { addAsPersonal: true };
|
|
56
|
+
}
|
|
57
|
+
export interface FirstTouchSkipped {
|
|
58
|
+
status: "skipped";
|
|
59
|
+
reason: FirstTouchSkipReason;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type FirstTouchResult =
|
|
63
|
+
| FirstTouchAlreadyClassified
|
|
64
|
+
| FirstTouchApplied
|
|
65
|
+
| FirstTouchNeedsConfirmation
|
|
66
|
+
| FirstTouchSkipped;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* [SEC H-5] Redact an org name to `xx***y` form (first 2 + ellipsis +
|
|
70
|
+
* last 1). For orgs shorter than 4 characters, returns the org as-is —
|
|
71
|
+
* redaction would be pointless and conspicuous.
|
|
72
|
+
*/
|
|
73
|
+
export function redactOrg(org: string): string {
|
|
74
|
+
if (org.length < 4) return org;
|
|
75
|
+
return `${org.slice(0, 2)}***${org.slice(-1)}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function readRemote(cwd: string): string | null {
|
|
79
|
+
try {
|
|
80
|
+
const out = execFileSync("git", ["remote", "get-url", "origin"], {
|
|
81
|
+
cwd,
|
|
82
|
+
encoding: "utf8",
|
|
83
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
84
|
+
}).trim();
|
|
85
|
+
return out.length > 0 ? out : null;
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function tryRegistryMatch(
|
|
92
|
+
parsedOrg: string,
|
|
93
|
+
):
|
|
94
|
+
| { class: "public-eligible"; engagement: null }
|
|
95
|
+
| { class: "customer-coupled"; engagement: Engagement }
|
|
96
|
+
| null
|
|
97
|
+
| "registry-not-found" {
|
|
98
|
+
let reg: Registry;
|
|
99
|
+
try {
|
|
100
|
+
reg = loadRegistry();
|
|
101
|
+
} catch (err) {
|
|
102
|
+
if (err instanceof RegistryNotFoundError) return "registry-not-found";
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const personalOrgs = reg.personalOrgs ?? [];
|
|
106
|
+
if (personalOrgs.includes(parsedOrg)) {
|
|
107
|
+
return { class: "public-eligible", engagement: null };
|
|
108
|
+
}
|
|
109
|
+
for (const eng of reg.engagements) {
|
|
110
|
+
if ((eng.githubOrgs ?? []).includes(parsedOrg)) {
|
|
111
|
+
return { class: "customer-coupled", engagement: eng };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface FirstTouchOptions {
|
|
118
|
+
cwd?: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function firstTouchClassify(opts: FirstTouchOptions = {}): FirstTouchResult {
|
|
122
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
123
|
+
const repo = readRepoConfig(cwd);
|
|
124
|
+
|
|
125
|
+
if (!repo.isGitRepo) {
|
|
126
|
+
return { status: "skipped", reason: "non-git" };
|
|
127
|
+
}
|
|
128
|
+
if (repo.classExplicit) {
|
|
129
|
+
return {
|
|
130
|
+
status: "already-classified",
|
|
131
|
+
class: repo.class,
|
|
132
|
+
engagements: repo.engagements,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const remote = readRemote(cwd);
|
|
136
|
+
if (remote === null) {
|
|
137
|
+
return { status: "skipped", reason: "no-remote" };
|
|
138
|
+
}
|
|
139
|
+
const parsed = parseRemoteUrl(remote);
|
|
140
|
+
if (parsed === null) {
|
|
141
|
+
return { status: "skipped", reason: "non-github-host" };
|
|
142
|
+
}
|
|
143
|
+
const match = tryRegistryMatch(parsed.org);
|
|
144
|
+
if (match === "registry-not-found") {
|
|
145
|
+
return { status: "skipped", reason: "registry-not-found" };
|
|
146
|
+
}
|
|
147
|
+
if (match === null) {
|
|
148
|
+
return {
|
|
149
|
+
status: "needs-confirmation",
|
|
150
|
+
remote,
|
|
151
|
+
org: parsed.org,
|
|
152
|
+
redactedOrg: redactOrg(parsed.org),
|
|
153
|
+
suggestion: { newEngagement: { idHint: parsed.org } },
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
setClass(match.class, cwd);
|
|
158
|
+
if (match.class === "customer-coupled") {
|
|
159
|
+
addEngagement(match.engagement.id, cwd);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let markerWarning: FirstTouchApplied["markerWarning"] = null;
|
|
163
|
+
if (match.class === "customer-coupled" && match.engagement.markers.length === 0) {
|
|
164
|
+
markerWarning = { engagementId: match.engagement.id, count: 0 };
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
status: "applied",
|
|
168
|
+
class: match.class,
|
|
169
|
+
engagement: match.class === "customer-coupled" ? match.engagement.id : null,
|
|
170
|
+
markerWarning,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Richard Myers and contributors.
|
|
3
|
+
//
|
|
4
|
+
// Hot-path determinism guard.
|
|
5
|
+
//
|
|
6
|
+
// The deterministic gate (PostToolUse hook, pre-commit, pre-push, render)
|
|
7
|
+
// must remain offline and free of LLM dependencies. This test walks the
|
|
8
|
+
// static import graph from the gate-path entry points and fails if any
|
|
9
|
+
// resolved file lives under `packages/llm/`. It also greps the same set
|
|
10
|
+
// of files for string literals matching `@de-otio/repo-aegis-llm` or
|
|
11
|
+
// `packages/llm`, catching dynamic `import()` / `require(varName)` /
|
|
12
|
+
// templated specifiers that the static walker would otherwise miss.
|
|
13
|
+
//
|
|
14
|
+
// Tests run from the repo root (process.cwd() at `npm test` time). The
|
|
15
|
+
// walk operates on the *compiled* .js sources in each package's `dist/`,
|
|
16
|
+
// not the .ts sources, because that's what actually loads at runtime —
|
|
17
|
+
// any tsc-time conditional or stripped code disappears here naturally.
|
|
18
|
+
|
|
19
|
+
import { describe, it, before } from "node:test";
|
|
20
|
+
import assert from "node:assert/strict";
|
|
21
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
22
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
23
|
+
|
|
24
|
+
// ---- configuration --------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Entry points whose import closure must not touch
|
|
28
|
+
* `@de-otio/repo-aegis-llm` (or its on-disk path `packages/llm/`).
|
|
29
|
+
*
|
|
30
|
+
* Express as relative paths from the repo root; the walker resolves them
|
|
31
|
+
* to compiled `.js` and then walks `import` / `require` references.
|
|
32
|
+
*/
|
|
33
|
+
const GATE_PATH_ENTRY_POINTS: string[] = [
|
|
34
|
+
"packages/core/dist/scan.js",
|
|
35
|
+
"packages/core/dist/render.js",
|
|
36
|
+
"packages/core/dist/deny-set.js",
|
|
37
|
+
"packages/cli/dist/commands/check.js",
|
|
38
|
+
"packages/cli/dist/commands/hook-scan-after-write.js",
|
|
39
|
+
"packages/cli/dist/commands/render.js",
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const FORBIDDEN_PACKAGE_PATTERNS: RegExp[] = [
|
|
43
|
+
/(?:^|[\\/])packages[\\/]llm[\\/]/,
|
|
44
|
+
/(?:^|[\\/])@de-otio[\\/]repo-aegis-llm[\\/]/,
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
// String literals that must not appear in any source file reachable
|
|
48
|
+
// from the gate path (catches dynamic imports the static walker misses).
|
|
49
|
+
const FORBIDDEN_STRING_LITERALS: string[] = [
|
|
50
|
+
"@de-otio/repo-aegis-llm",
|
|
51
|
+
"packages/llm",
|
|
52
|
+
"repo-aegis-llm",
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
// ---- repo-root resolution -------------------------------------------------
|
|
56
|
+
|
|
57
|
+
/** Locate the monorepo root by walking up from cwd until package.json's
|
|
58
|
+
* `repo-aegis-monorepo` name marker is found. */
|
|
59
|
+
function findRepoRoot(): string {
|
|
60
|
+
let cur = process.cwd();
|
|
61
|
+
for (let i = 0; i < 16; i++) {
|
|
62
|
+
const pkg = join(cur, "package.json");
|
|
63
|
+
if (existsSync(pkg)) {
|
|
64
|
+
try {
|
|
65
|
+
const json = JSON.parse(readFileSync(pkg, "utf8")) as { name?: string };
|
|
66
|
+
if (json.name === "repo-aegis-monorepo") return cur;
|
|
67
|
+
} catch {
|
|
68
|
+
// ignore parse errors at intermediate package.json files
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const parent = dirname(cur);
|
|
72
|
+
if (parent === cur) break;
|
|
73
|
+
cur = parent;
|
|
74
|
+
}
|
|
75
|
+
throw new Error(`could not locate repo-aegis monorepo root from cwd=${process.cwd()}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---- import extraction ----------------------------------------------------
|
|
79
|
+
|
|
80
|
+
// Match `import ... from "X"`, `import "X"`, `import("X")`, `require("X")`.
|
|
81
|
+
// Strict on the quote style: only matches single or double quotes (no
|
|
82
|
+
// template literals — those are caught by the string-literal grep step
|
|
83
|
+
// instead, intentionally, since they suggest dynamic resolution).
|
|
84
|
+
const IMPORT_RE =
|
|
85
|
+
/(?:^|[^\w$])(?:import\s+(?:[\w*${},\s]+\s+from\s+)?|import\s*\(\s*|require\s*\(\s*)["']([^"']+)["']/g;
|
|
86
|
+
|
|
87
|
+
function extractStaticImportSpecifiers(source: string): string[] {
|
|
88
|
+
const results: string[] = [];
|
|
89
|
+
IMPORT_RE.lastIndex = 0;
|
|
90
|
+
let m: RegExpExecArray | null;
|
|
91
|
+
while ((m = IMPORT_RE.exec(source)) !== null) {
|
|
92
|
+
const spec = m[1];
|
|
93
|
+
if (spec !== undefined) results.push(spec);
|
|
94
|
+
}
|
|
95
|
+
return results;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---- specifier resolution -------------------------------------------------
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Resolve an ES module specifier to an absolute file path on disk, or
|
|
102
|
+
* `null` if the specifier is external (a node_modules dependency we
|
|
103
|
+
* don't follow). Workspace links under `node_modules/@de-otio/...` are
|
|
104
|
+
* followed via `realpathSync`.
|
|
105
|
+
*/
|
|
106
|
+
function resolveSpecifier(
|
|
107
|
+
specifier: string,
|
|
108
|
+
fromFile: string,
|
|
109
|
+
repoRoot: string,
|
|
110
|
+
): string | null {
|
|
111
|
+
// Node built-ins (`node:fs`, etc) — skip.
|
|
112
|
+
if (specifier.startsWith("node:")) return null;
|
|
113
|
+
|
|
114
|
+
// Relative — resolve against the importing file.
|
|
115
|
+
if (specifier.startsWith("./") || specifier.startsWith("../")) {
|
|
116
|
+
const candidate = resolve(dirname(fromFile), specifier);
|
|
117
|
+
return resolveFileWithExtensions(candidate);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Workspace package via node_modules symlink.
|
|
121
|
+
if (specifier.startsWith("@de-otio/")) {
|
|
122
|
+
const linkPath = join(repoRoot, "node_modules", specifier);
|
|
123
|
+
if (!existsSync(linkPath)) return null;
|
|
124
|
+
const real = realpathSync(linkPath);
|
|
125
|
+
// Resolve to the package's main (typically dist/index.js).
|
|
126
|
+
const pkgJson = join(real, "package.json");
|
|
127
|
+
if (!existsSync(pkgJson)) return null;
|
|
128
|
+
const json = JSON.parse(readFileSync(pkgJson, "utf8")) as { main?: string };
|
|
129
|
+
const main = json.main ?? "dist/index.js";
|
|
130
|
+
const mainPath = join(real, main);
|
|
131
|
+
return resolveFileWithExtensions(mainPath);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// External package — skip.
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function resolveFileWithExtensions(candidate: string): string | null {
|
|
139
|
+
if (existsSync(candidate) && statSync(candidate).isFile()) return candidate;
|
|
140
|
+
for (const ext of [".js", ".cjs", ".mjs"]) {
|
|
141
|
+
const withExt = candidate + ext;
|
|
142
|
+
if (existsSync(withExt) && statSync(withExt).isFile()) return withExt;
|
|
143
|
+
}
|
|
144
|
+
// index.js inside a directory
|
|
145
|
+
if (existsSync(candidate) && statSync(candidate).isDirectory()) {
|
|
146
|
+
const indexJs = join(candidate, "index.js");
|
|
147
|
+
if (existsSync(indexJs)) return indexJs;
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ---- graph walk -----------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
function walkImportGraph(entryPoints: string[], repoRoot: string): Set<string> {
|
|
155
|
+
const visited = new Set<string>();
|
|
156
|
+
const stack: string[] = entryPoints.map(p => resolve(repoRoot, p));
|
|
157
|
+
|
|
158
|
+
while (stack.length > 0) {
|
|
159
|
+
const file = stack.pop();
|
|
160
|
+
if (file === undefined || visited.has(file)) continue;
|
|
161
|
+
if (!existsSync(file)) {
|
|
162
|
+
// Entry point not built — surface in test failure.
|
|
163
|
+
throw new Error(`gate-path entry point not built: ${file}`);
|
|
164
|
+
}
|
|
165
|
+
visited.add(file);
|
|
166
|
+
|
|
167
|
+
let source: string;
|
|
168
|
+
try {
|
|
169
|
+
source = readFileSync(file, "utf8");
|
|
170
|
+
} catch {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const specs = extractStaticImportSpecifiers(source);
|
|
175
|
+
for (const spec of specs) {
|
|
176
|
+
const resolved = resolveSpecifier(spec, file, repoRoot);
|
|
177
|
+
if (resolved !== null && !visited.has(resolved)) {
|
|
178
|
+
stack.push(resolved);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return visited;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ---- the test ------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
describe("hot-path determinism guard", () => {
|
|
189
|
+
let repoRoot: string;
|
|
190
|
+
let reachable: Set<string>;
|
|
191
|
+
|
|
192
|
+
before(() => {
|
|
193
|
+
repoRoot = findRepoRoot();
|
|
194
|
+
reachable = walkImportGraph(GATE_PATH_ENTRY_POINTS, repoRoot);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("walks at least one entry point successfully", () => {
|
|
198
|
+
// Sanity — if this fails, the walker is broken (or no .js was built).
|
|
199
|
+
assert.ok(reachable.size > 0, "expected at least one reachable file");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("no gate-path file resolves under packages/llm/", () => {
|
|
203
|
+
const offenders = [...reachable].filter(p =>
|
|
204
|
+
FORBIDDEN_PACKAGE_PATTERNS.some(re => re.test(relative(repoRoot, p))),
|
|
205
|
+
);
|
|
206
|
+
assert.equal(
|
|
207
|
+
offenders.length,
|
|
208
|
+
0,
|
|
209
|
+
`gate path imports forbidden package(s):\n` +
|
|
210
|
+
offenders.map(o => ` ${relative(repoRoot, o)}`).join("\n"),
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// [SEC M-1] Catch dynamic imports / `require(variable)` / templated
|
|
215
|
+
// specifiers that the static walker would miss. Greps every reachable
|
|
216
|
+
// file's source for the forbidden literals.
|
|
217
|
+
it("[SEC M-1] no reachable file contains a literal LLM-package reference", () => {
|
|
218
|
+
const offenders: Array<{ file: string; literal: string }> = [];
|
|
219
|
+
for (const file of reachable) {
|
|
220
|
+
let source: string;
|
|
221
|
+
try {
|
|
222
|
+
source = readFileSync(file, "utf8");
|
|
223
|
+
} catch {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
for (const literal of FORBIDDEN_STRING_LITERALS) {
|
|
227
|
+
if (source.includes(literal)) {
|
|
228
|
+
offenders.push({ file: relative(repoRoot, file), literal });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
assert.equal(
|
|
233
|
+
offenders.length,
|
|
234
|
+
0,
|
|
235
|
+
`gate-path file(s) contain forbidden string literal(s):\n` +
|
|
236
|
+
offenders.map(o => ` ${o.file} (literal: "${o.literal}")`).join("\n"),
|
|
237
|
+
);
|
|
238
|
+
});
|
|
239
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Richard Myers and contributors.
|
|
3
|
+
// Curated public API surface for `@de-otio/repo-aegis-core`.
|
|
4
|
+
//
|
|
5
|
+
// Every name re-exported below is part of the package's compatibility
|
|
6
|
+
// contract: renaming or removing one is a semver-major change. Items not
|
|
7
|
+
// listed here are intentionally internal — they may be re-shaped at any
|
|
8
|
+
// time without a major bump.
|
|
9
|
+
//
|
|
10
|
+
// To keep an item internal while still allowing intra-monorepo imports
|
|
11
|
+
// (tests, sibling packages reaching into specific modules), tag it with
|
|
12
|
+
// a `/** @internal */` JSDoc comment in its source module rather than
|
|
13
|
+
// re-exporting it from this file. api-extractor / consumers honour the
|
|
14
|
+
// hint; existing deep-path imports continue to resolve.
|
|
15
|
+
|
|
16
|
+
// ---- paths ---------------------------------------------------------------
|
|
17
|
+
export {
|
|
18
|
+
repoAegisHome,
|
|
19
|
+
registryPath,
|
|
20
|
+
markersDir,
|
|
21
|
+
statePath,
|
|
22
|
+
leakContextFlagPath,
|
|
23
|
+
lockFilePath,
|
|
24
|
+
denySetCachePath,
|
|
25
|
+
isHomeOverridden,
|
|
26
|
+
flatMarkersPath,
|
|
27
|
+
auditLogPath,
|
|
28
|
+
} from "./paths.js";
|
|
29
|
+
|
|
30
|
+
// ---- audit log -----------------------------------------------------------
|
|
31
|
+
export {
|
|
32
|
+
appendAuditRecord,
|
|
33
|
+
isAuditLogEnabled,
|
|
34
|
+
setAuditLogEnabled,
|
|
35
|
+
activeAuditLogPath,
|
|
36
|
+
} from "./audit-log.js";
|
|
37
|
+
export type { AuditRecord } from "./audit-log.js";
|
|
38
|
+
|
|
39
|
+
// ---- registry ------------------------------------------------------------
|
|
40
|
+
export {
|
|
41
|
+
loadRegistry,
|
|
42
|
+
isActive,
|
|
43
|
+
resolveEngagement,
|
|
44
|
+
ALWAYS_BLOCK_RESERVED_ID,
|
|
45
|
+
MAX_SUPPORTED_REGISTRY_SCHEMA_VERSION,
|
|
46
|
+
} from "./registry.js";
|
|
47
|
+
export type { Engagement, Registry, ResolveResult } from "./registry.js";
|
|
48
|
+
|
|
49
|
+
// ---- remote URL parser ---------------------------------------------------
|
|
50
|
+
export { parseRemoteUrl } from "./remote-url.js";
|
|
51
|
+
export type { ParsedRemote } from "./remote-url.js";
|
|
52
|
+
|
|
53
|
+
// ---- working-tree resolution (path-aware hook) --------------------------
|
|
54
|
+
export {
|
|
55
|
+
findEnclosingWorkingTree,
|
|
56
|
+
resolveGitDir,
|
|
57
|
+
getRemoteOrg,
|
|
58
|
+
} from "./working-tree.js";
|
|
59
|
+
|
|
60
|
+
// ---- trust-boundary (path-aware hook) -----------------------------------
|
|
61
|
+
export {
|
|
62
|
+
computeTrustBoundary,
|
|
63
|
+
trustBoundariesOverlap,
|
|
64
|
+
} from "./trust-boundary.js";
|
|
65
|
+
export type { TrustBoundary } from "./trust-boundary.js";
|
|
66
|
+
|
|
67
|
+
// ---- first-touch classification (Phase 1 onboarding) --------------------
|
|
68
|
+
export { firstTouchClassify, redactOrg } from "./first-touch.js";
|
|
69
|
+
export type {
|
|
70
|
+
FirstTouchResult,
|
|
71
|
+
FirstTouchOptions,
|
|
72
|
+
FirstTouchSkipReason,
|
|
73
|
+
FirstTouchAlreadyClassified,
|
|
74
|
+
FirstTouchApplied,
|
|
75
|
+
FirstTouchNeedsConfirmation,
|
|
76
|
+
FirstTouchSkipped,
|
|
77
|
+
} from "./first-touch.js";
|
|
78
|
+
|
|
79
|
+
// ---- registry mutation (Phase 2 onboarding) -----------------------------
|
|
80
|
+
export {
|
|
81
|
+
addMarkerPattern,
|
|
82
|
+
addMarkerPatterns,
|
|
83
|
+
} from "./registry-mutate.js";
|
|
84
|
+
export type {
|
|
85
|
+
AddMarkerPatternOptions,
|
|
86
|
+
AddMarkerPatternResult,
|
|
87
|
+
} from "./registry-mutate.js";
|
|
88
|
+
|
|
89
|
+
// ---- repo (per-repo config + engagement membership mutators) -------------
|
|
90
|
+
export {
|
|
91
|
+
readRepoConfig,
|
|
92
|
+
addEngagement,
|
|
93
|
+
addEngagements,
|
|
94
|
+
removeEngagement,
|
|
95
|
+
setClass,
|
|
96
|
+
unsetClass,
|
|
97
|
+
REPO_CLASSES,
|
|
98
|
+
RepoOverrideError,
|
|
99
|
+
OVERRIDE_FILENAME,
|
|
100
|
+
} from "./repo.js";
|
|
101
|
+
export type { RepoClass, RepoConfig, RepoOverride } from "./repo.js";
|
|
102
|
+
|
|
103
|
+
// ---- deny set ------------------------------------------------------------
|
|
104
|
+
export { computeDenySet, ALWAYS_FILE_STEM } from "./deny-set.js";
|
|
105
|
+
export type { DenySet, DenySetFile, DenySetOptions } from "./deny-set.js";
|
|
106
|
+
|
|
107
|
+
// ---- scan primitives -----------------------------------------------------
|
|
108
|
+
export {
|
|
109
|
+
scanText,
|
|
110
|
+
scanFile,
|
|
111
|
+
scanStagedDiff,
|
|
112
|
+
scanRange,
|
|
113
|
+
scanHistory,
|
|
114
|
+
ALLOW_COMMENT,
|
|
115
|
+
} from "./scan.js";
|
|
116
|
+
export type { ScanHit, SkippedFile, HistoryHit, ScanOptions } from "./scan.js";
|
|
117
|
+
|
|
118
|
+
// ---- secret-shaped markers (universal, not engagement-scoped) -----------
|
|
119
|
+
export { scanForSecrets, summariseHits } from "./secret-markers.js";
|
|
120
|
+
export type { SecretMarkerKind, SecretMarkerHit } from "./secret-markers.js";
|
|
121
|
+
|
|
122
|
+
// ---- render --------------------------------------------------------------
|
|
123
|
+
export { renderMarkers, MARKER_FORMAT_VERSION } from "./render.js";
|
|
124
|
+
export type { RenderOptions, RenderedFile, RenderResult } from "./render.js";
|
|
125
|
+
|
|
126
|
+
// ---- redaction -----------------------------------------------------------
|
|
127
|
+
export { redactMatch, revealMatch } from "./redaction.js";
|
|
128
|
+
export type { RedactionMode } from "./redaction.js";
|
|
129
|
+
|
|
130
|
+
// ---- regex safety --------------------------------------------------------
|
|
131
|
+
// `validatePattern` (the single-pattern, in-process variant) is tagged
|
|
132
|
+
// `@internal` in its source. Callers should prefer `validatePatterns`
|
|
133
|
+
// (which can run strict, subprocess-backed validation). It remains
|
|
134
|
+
// re-exported here so existing intra-repo imports keep working without
|
|
135
|
+
// a coordinated breaking change.
|
|
136
|
+
export {
|
|
137
|
+
validatePattern,
|
|
138
|
+
validatePatterns,
|
|
139
|
+
validateCombinedSize,
|
|
140
|
+
getRegexBackend,
|
|
141
|
+
} from "./regex-safety.js";
|
|
142
|
+
export type {
|
|
143
|
+
PatternValidationResult,
|
|
144
|
+
ValidatePatternsOptions,
|
|
145
|
+
RegexBackend,
|
|
146
|
+
} from "./regex-safety.js";
|
|
147
|
+
|
|
148
|
+
// ---- exceptions ----------------------------------------------------------
|
|
149
|
+
export {
|
|
150
|
+
RegistryNotFoundError,
|
|
151
|
+
RegistryParseError,
|
|
152
|
+
RegistryEncryptedError,
|
|
153
|
+
NotAGitRepoError,
|
|
154
|
+
AmbiguousQueryError,
|
|
155
|
+
EngagementNotFoundError,
|
|
156
|
+
PatternValidationError,
|
|
157
|
+
OutsideWorkingTreeError,
|
|
158
|
+
LockTimeoutError,
|
|
159
|
+
CustomerCoupledNoEngagementError,
|
|
160
|
+
} from "./exceptions.js";
|
|
161
|
+
|
|
162
|
+
// ---- exit codes ----------------------------------------------------------
|
|
163
|
+
export { EXIT_OK, EXIT_HIT, EXIT_USAGE } from "./exit-codes.js";
|
|
164
|
+
|
|
165
|
+
// ---- age (file-encryption helpers) --------------------------------------
|
|
166
|
+
// Wrapper around the `age` CLI. Used by the scan package's
|
|
167
|
+
// `encrypt-query` / `decrypt-query` commands and by the cli's
|
|
168
|
+
// `registry encrypt` / `registry decrypt` commands. The `age` binary
|
|
169
|
+
// is a runtime requirement; absence surfaces as `AgeNotFoundError`.
|
|
170
|
+
export {
|
|
171
|
+
encryptFile,
|
|
172
|
+
decryptFile,
|
|
173
|
+
writeBufferTo,
|
|
174
|
+
AgeNotFoundError,
|
|
175
|
+
AgeError,
|
|
176
|
+
} from "./age.js";
|
|
177
|
+
export type { EncryptOptions, DecryptOptions } from "./age.js";
|
|
178
|
+
|
|
179
|
+
// ---- locking -------------------------------------------------------------
|
|
180
|
+
export { withLock, withLockSync } from "./lock.js";
|
|
181
|
+
export type { LockOptions } from "./lock.js";
|
|
182
|
+
|
|
183
|
+
// ---- canonical JSON shapes ----------------------------------------------
|
|
184
|
+
export type { RepoJson, EngagementJson } from "./types.js";
|
|
185
|
+
|
|
186
|
+
// ---- schema helpers ------------------------------------------------------
|
|
187
|
+
// `formatZodError` is the canonical "render a ZodError as a one-line
|
|
188
|
+
// human-readable message" helper. Sibling packages (cli, scan) own their
|
|
189
|
+
// own schemas (classify rules, queries) but import this so error wording
|
|
190
|
+
// is consistent across the surface.
|
|
191
|
+
export { formatZodError, ORG_NAME_REGEX } from "./schemas.js";
|