@mrciphersmith/keryx 0.3.22 → 0.3.25

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrciphersmith/keryx",
3
- "version": "0.3.22",
3
+ "version": "0.3.25",
4
4
  "description": "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -45,7 +45,7 @@
45
45
  "typecheck": "tsc --noEmit",
46
46
  "typecheck:scripts": "tsc --project tsconfig.scripts.json --noEmit",
47
47
  "test": "bun test",
48
- "test:core": "bun test src/cli src/core src/shell-source-audits.test.ts src/acp/ src/assets/ src/bundle/ src/capability/ src/commands/ src/contracts/ src/ctx/ src/eval/ src/flow/ src/forgetting/ src/gdgraph/ src/gdskills/ src/governance/ src/health/ src/integrations/ src/job/ src/learning/ src/lib/ src/mcp/ src/memory/ src/metrics/ src/retention/ src/review/ src/rules/ src/sac/ src/security/ src/stack/ src/standard/ src/sync/ src/testing/ src/trigger/ src/wiki/",
48
+ "test:core": "bun test src/cli src/core src/impact-evidence/ src/shell-source-audits.test.ts src/acp/ src/assets/ src/bundle/ src/capability/ src/commands/ src/contracts/ src/ctx/ src/eval/ src/flow/ src/forgetting/ src/gdgraph/ src/gdskills/ src/governance/ src/health/ src/integrations/ src/job/ src/learning/ src/lib/ src/mcp/ src/memory/ src/metrics/ src/retention/ src/review/ src/rules/ src/sac/ src/security/ src/stack/ src/standard/ src/sync/ src/testing/ src/trigger/ src/wiki/ scripts/",
49
49
  "test:client:terminal": "bun test src/tui/ src/commands/shell",
50
50
  "test:client:streaming": "bun test src/harness/provider/",
51
51
  "test:client:cancel-resume": "bun test src/harness/run/ src/harness/resume/ src/harness/session/ src/session/ src/bus/ src/commands/sessions",
@@ -72,7 +72,20 @@ export function getFilesDescribedBy(graph: GraphData, pageId: string): string[]
72
72
  return [...files].sort();
73
73
  }
74
74
 
75
- export function getOrphans(graph: GraphData): string[] {
75
+ /**
76
+ * `roots` (A-8, flow 356): files reached OUT OF BAND — never by any `import`
77
+ * statement this graph's builder can see, so they carry no inbound OR
78
+ * outbound edge and would otherwise report as orphans. `src/lib/
79
+ * test-preload.ts` is the motivating case: `bunfig.toml`'s `[test].preload`
80
+ * loads it before any test module runs, which is a real caller this scan
81
+ * simply cannot see (it is not written as an `import`). Reporting it dead is
82
+ * a false positive, not a finding — `bunfigPreloadRoots` below is the one
83
+ * caller (`commands/gdgraph.ts`, `gdgraph/service.ts`) resolves and passes
84
+ * in; a caller that does not know about a project's `bunfig.toml` (e.g. this
85
+ * module's own unit tests, `forgetting/propagation.ts`) omits it and gets
86
+ * the previous, unfiltered behaviour.
87
+ */
88
+ export function getOrphans(graph: GraphData, roots: ReadonlySet<string> = new Set()): string[] {
76
89
  const inbound = new Set(
77
90
  graph.edges.filter((edge) => edge.kind !== "unresolved").map((edge) => edge.to),
78
91
  );
@@ -81,10 +94,97 @@ export function getOrphans(graph: GraphData): string[] {
81
94
  );
82
95
  return graph.nodes
83
96
  .map((node) => node.path)
84
- .filter((file) => !inbound.has(file) && !outbound.has(file))
97
+ .filter((file) => !inbound.has(file) && !outbound.has(file) && !roots.has(file))
85
98
  .sort();
86
99
  }
87
100
 
101
+ /**
102
+ * `bunfig.toml`'s declared `preload` entries, resolved to graph-relative
103
+ * paths (forward-slash, relative to `projectRoot` — the same shape
104
+ * `GraphNode.path` uses) — the roots {@link getOrphans} excludes (A-8).
105
+ *
106
+ * Handles the two shapes Bun actually reads a `preload` array from: a
107
+ * top-level `preload = […]` (`bun run`/`bunx`) and a table-scoped one
108
+ * (`[test]\npreload = […]`, this repository's own `bunfig.toml`). Not a
109
+ * general TOML parser — narrow on purpose, one array key, matching this
110
+ * repository's own `mcp-servers/compat.ts` precedent of a small reader over
111
+ * a dependency for one config shape. An absent or unreadable `bunfig.toml`
112
+ * yields no roots rather than an error: most projects this graph runs
113
+ * against have none at all.
114
+ */
115
+ /**
116
+ * Strip `#`-to-end-of-line comments from `text`, leaving string literals
117
+ * (single- or double-quoted, double-quoted honoring a `\"` escape) alone —
118
+ * a `#` inside a string is content, not a comment start. Review round 1,
119
+ * L3: without this, a commented-out `# preload = […]` line still matched
120
+ * {@link bunfigPreloadRoots}'s array pattern, since that pattern searched
121
+ * the raw text with no comment awareness.
122
+ */
123
+ function stripTomlComments(text: string): string {
124
+ let result = "";
125
+ let quote: '"' | "'" | undefined;
126
+ for (let i = 0; i < text.length; i++) {
127
+ const ch = text[i];
128
+ if (quote !== undefined) {
129
+ result += ch;
130
+ if (ch === "\\" && quote === '"' && i + 1 < text.length) {
131
+ result += text[i + 1];
132
+ i++;
133
+ continue;
134
+ }
135
+ if (ch === quote) {
136
+ quote = undefined;
137
+ }
138
+ continue;
139
+ }
140
+ if (ch === '"' || ch === "'") {
141
+ quote = ch;
142
+ result += ch;
143
+ continue;
144
+ }
145
+ if (ch === "#") {
146
+ const newlineIndex = text.indexOf("\n", i);
147
+ if (newlineIndex === -1) {
148
+ break;
149
+ }
150
+ i = newlineIndex - 1;
151
+ continue;
152
+ }
153
+ result += ch;
154
+ }
155
+ return result;
156
+ }
157
+
158
+ export async function bunfigPreloadRoots(projectRoot: string): Promise<Set<string>> {
159
+ const roots = new Set<string>();
160
+ let text: string;
161
+ try {
162
+ text = await readFile(path.join(projectRoot, "bunfig.toml"), "utf8");
163
+ } catch {
164
+ return roots;
165
+ }
166
+ // Review round 1, L3: strip `#` comments (outside string literals) before
167
+ // matching, and anchor the key to a line start — a commented-out
168
+ // `# preload = […]` line must never contribute a root, and a key that
169
+ // merely ENDS in "preload" mid-line must never match either.
170
+ const stripped = stripTomlComments(text);
171
+ const arrayPattern = /^[ \t]*preload\s*=\s*\[([^\]]*)\]/gm;
172
+ const stringPattern = /"((?:[^"\\]|\\.)*)"|'([^']*)'/g;
173
+ for (const arrayMatch of stripped.matchAll(arrayPattern)) {
174
+ const body = arrayMatch[1] ?? "";
175
+ for (const stringMatch of body.matchAll(stringPattern)) {
176
+ const raw = stringMatch[1] ?? stringMatch[2] ?? "";
177
+ if (raw.length === 0) {
178
+ continue;
179
+ }
180
+ const absolute = path.resolve(projectRoot, raw);
181
+ const relative = path.relative(projectRoot, absolute).split(path.sep).join("/");
182
+ roots.add(relative);
183
+ }
184
+ }
185
+ return roots;
186
+ }
187
+
88
188
  export function getAffected(
89
189
  graph: GraphData,
90
190
  target: string,
@@ -8,7 +8,7 @@
8
8
  import { buildGraph } from "./build";
9
9
  import { computeAffected, type AffectedOptions, type AffectedResult } from "./affected";
10
10
  import { loadGdgraphConfig } from "./config";
11
- import { getCycles, getOrphans, loadGraph } from "./query";
11
+ import { bunfigPreloadRoots, getCycles, getOrphans, loadGraph } from "./query";
12
12
  import { writeRepomap, type RepomapOptions, type RepomapResult } from "./repomap";
13
13
  import type { GraphData } from "./types";
14
14
 
@@ -90,7 +90,13 @@ export function createGdgraphService(): GdgraphService {
90
90
 
91
91
  async query(cwd, q) {
92
92
  const graph = await loadGraph(cwd);
93
- return q === "cycles" ? getCycles(graph) : getOrphans(graph);
93
+ if (q === "cycles") {
94
+ return getCycles(graph);
95
+ }
96
+ // A-8 (flow 356): same roots as `commands/gdgraph.ts` — a `bunfig.toml`
97
+ // `preload` entry is a real caller this graph's scan cannot see.
98
+ const roots = await bunfigPreloadRoots(cwd);
99
+ return getOrphans(graph, roots);
94
100
  },
95
101
  };
96
102
  }