@mrciphersmith/keryx 0.2.164 → 0.3.1

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 (182) hide show
  1. package/README.md +4 -1
  2. package/dist/cli.js +82540 -50300
  3. package/dist/core.js +28967 -18937
  4. package/package.json +2 -2
  5. package/src/gdgraph/affected-report.ts +141 -0
  6. package/src/gdgraph/build.ts +170 -23
  7. package/src/gdgraph/service.ts +6 -0
  8. package/src/gdgraph/staleness.ts +253 -45
  9. package/src/gdskills/bundled/agents/codebase-navigator.md +55 -0
  10. package/src/gdskills/bundled/agents/design-advisor.md +64 -0
  11. package/src/gdskills/bundled/agents/docs-maintainer.md +56 -0
  12. package/src/gdskills/bundled/agents/end-to-end-tester.md +56 -0
  13. package/src/gdskills/bundled/agents/error-path-auditor.md +57 -0
  14. package/src/gdskills/bundled/agents/go-build-fixer.md +52 -0
  15. package/src/gdskills/bundled/agents/go-code-auditor.md +49 -0
  16. package/src/gdskills/bundled/agents/performance-auditor.md +63 -0
  17. package/src/gdskills/bundled/agents/python-build-fixer.md +52 -0
  18. package/src/gdskills/bundled/agents/python-code-auditor.md +49 -0
  19. package/src/gdskills/bundled/agents/refactoring-steward.md +61 -0
  20. package/src/gdskills/bundled/agents/security-auditor.md +62 -0
  21. package/src/gdskills/bundled/agents/test-first-driver.md +61 -0
  22. package/src/gdskills/bundled/agents/work-planner.md +62 -0
  23. package/src/gdskills/bundled/install-manifest.json +797 -0
  24. package/src/gdskills/bundled/rules/core/model-selection.mdc +51 -0
  25. package/src/gdskills/bundled/rules/core/skill-lifecycle.mdc +29 -1
  26. package/src/gdskills/bundled/rules/core/skills-storage-workflow.mdc +2 -2
  27. package/src/gdskills/bundled/skills/review/code-style-review/SKILL.md +1 -1
  28. package/src/gdskills/bundled/skills/review/review-jev-rules/SKILL.md +267 -0
  29. package/src/gdskills/bundled/skills/review/review-orchestrator/SKILL.detail.md +26 -0
  30. package/src/gdskills/bundled/skills/review/review-orchestrator/SKILL.md +75 -247
  31. package/src/gdskills/bundled/skills/review/review-orchestrator/output-contract.schema.json +19 -0
  32. package/src/gdskills/bundled/skills/review/review-orchestrator/reviewer-finding.schema.json +10 -0
  33. package/src/gdskills/bundled/skills/review/review-orchestrator/reviewer-input.schema.json +5 -0
  34. package/src/gdskills/bundled/skills/review/review-orchestrator/templates/pr-comment-backend.md +50 -0
  35. package/src/gdskills/bundled/skills/review/review-orchestrator/templates/pr-comment-frontend.md +52 -0
  36. package/src/gdskills/bundled/skills/review/review-orchestrator/templates/review-report.md +143 -0
  37. package/src/gdskills/bundled/stacks/angular/agent-refs.json +4 -0
  38. package/src/gdskills/bundled/stacks/angular/governance/eval.json +1751 -0
  39. package/src/gdskills/bundled/stacks/angular/governance/scout.json +32 -0
  40. package/src/gdskills/bundled/stacks/angular/pack.json +55 -0
  41. package/src/gdskills/bundled/stacks/angular/rules/coding-style.mdc +82 -0
  42. package/src/gdskills/bundled/stacks/angular/rules/patterns.mdc +84 -0
  43. package/src/gdskills/bundled/stacks/angular/rules/security.mdc +70 -0
  44. package/src/gdskills/bundled/stacks/angular/rules/testing.mdc +73 -0
  45. package/src/gdskills/bundled/stacks/angular/skills/angular-build-fix/SKILL.md +127 -0
  46. package/src/gdskills/bundled/stacks/angular/skills/angular-build-fix/evals.json +72 -0
  47. package/src/gdskills/bundled/stacks/angular/skills/angular-code-review/SKILL.md +98 -0
  48. package/src/gdskills/bundled/stacks/angular/skills/angular-code-review/evals.json +73 -0
  49. package/src/gdskills/bundled/stacks/angular/skills/angular-implementation/SKILL.md +112 -0
  50. package/src/gdskills/bundled/stacks/angular/skills/angular-implementation/evals.json +74 -0
  51. package/src/gdskills/bundled/stacks/angular/skills/angular-testing/SKILL.md +102 -0
  52. package/src/gdskills/bundled/stacks/angular/skills/angular-testing/evals.json +71 -0
  53. package/src/gdskills/bundled/stacks/go/agent-refs.json +3 -0
  54. package/src/gdskills/bundled/stacks/go/governance/eval.json +1745 -0
  55. package/src/gdskills/bundled/stacks/go/governance/scout.json +31 -0
  56. package/src/gdskills/bundled/stacks/go/pack.json +41 -0
  57. package/src/gdskills/bundled/stacks/go/rules/coding-style.mdc +85 -0
  58. package/src/gdskills/bundled/stacks/go/rules/patterns.mdc +65 -0
  59. package/src/gdskills/bundled/stacks/go/rules/security.mdc +73 -0
  60. package/src/gdskills/bundled/stacks/go/rules/testing.mdc +68 -0
  61. package/src/gdskills/bundled/stacks/go/skills/go-build-fix/SKILL.md +138 -0
  62. package/src/gdskills/bundled/stacks/go/skills/go-build-fix/evals.json +75 -0
  63. package/src/gdskills/bundled/stacks/go/skills/go-code-review/SKILL.md +121 -0
  64. package/src/gdskills/bundled/stacks/go/skills/go-code-review/evals.json +72 -0
  65. package/src/gdskills/bundled/stacks/go/skills/go-implementation/SKILL.md +122 -0
  66. package/src/gdskills/bundled/stacks/go/skills/go-implementation/evals.json +76 -0
  67. package/src/gdskills/bundled/stacks/go/skills/go-testing/SKILL.md +126 -0
  68. package/src/gdskills/bundled/stacks/go/skills/go-testing/evals.json +73 -0
  69. package/src/gdskills/bundled/stacks/mobx/agent-refs.json +4 -0
  70. package/src/gdskills/bundled/stacks/mobx/governance/eval.json +904 -0
  71. package/src/gdskills/bundled/stacks/mobx/governance/scout.json +18 -0
  72. package/src/gdskills/bundled/stacks/mobx/pack.json +28 -0
  73. package/src/gdskills/bundled/stacks/mobx/rules/coding-style.mdc +91 -0
  74. package/src/gdskills/bundled/stacks/mobx/rules/patterns.mdc +122 -0
  75. package/src/gdskills/bundled/stacks/mobx/rules/security.mdc +56 -0
  76. package/src/gdskills/bundled/stacks/mobx/rules/testing.mdc +63 -0
  77. package/src/gdskills/bundled/stacks/mobx/skills/mobx-observable-testing/SKILL.md +124 -0
  78. package/src/gdskills/bundled/stacks/mobx/skills/mobx-observable-testing/evals.json +73 -0
  79. package/src/gdskills/bundled/stacks/mobx/skills/mobx-store-implementation/SKILL.md +149 -0
  80. package/src/gdskills/bundled/stacks/mobx/skills/mobx-store-implementation/evals.json +74 -0
  81. package/src/gdskills/bundled/stacks/nestjs/agent-refs.json +4 -0
  82. package/src/gdskills/bundled/stacks/nestjs/governance/eval.json +1308 -0
  83. package/src/gdskills/bundled/stacks/nestjs/governance/scout.json +34 -0
  84. package/src/gdskills/bundled/stacks/nestjs/pack.json +53 -0
  85. package/src/gdskills/bundled/stacks/nestjs/rules/coding-style.mdc +70 -0
  86. package/src/gdskills/bundled/stacks/nestjs/rules/patterns.mdc +83 -0
  87. package/src/gdskills/bundled/stacks/nestjs/rules/security.mdc +73 -0
  88. package/src/gdskills/bundled/stacks/nestjs/rules/testing.mdc +69 -0
  89. package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-build-fix/SKILL.md +157 -0
  90. package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-build-fix/evals.json +70 -0
  91. package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-implementation/SKILL.md +129 -0
  92. package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-implementation/evals.json +71 -0
  93. package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-testing/SKILL.md +143 -0
  94. package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-testing/evals.json +69 -0
  95. package/src/gdskills/bundled/stacks/nextjs-nuxt/agent-refs.json +4 -0
  96. package/src/gdskills/bundled/stacks/nextjs-nuxt/governance/eval.json +2413 -0
  97. package/src/gdskills/bundled/stacks/nextjs-nuxt/governance/scout.json +42 -0
  98. package/src/gdskills/bundled/stacks/nextjs-nuxt/pack.json +42 -0
  99. package/src/gdskills/bundled/stacks/nextjs-nuxt/rules/coding-style.mdc +69 -0
  100. package/src/gdskills/bundled/stacks/nextjs-nuxt/rules/patterns.mdc +88 -0
  101. package/src/gdskills/bundled/stacks/nextjs-nuxt/rules/security.mdc +72 -0
  102. package/src/gdskills/bundled/stacks/nextjs-nuxt/rules/testing.mdc +64 -0
  103. package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-build-fix/SKILL.md +147 -0
  104. package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-build-fix/evals.json +75 -0
  105. package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-code-review/SKILL.md +118 -0
  106. package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-code-review/evals.json +76 -0
  107. package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-implementation/SKILL.md +135 -0
  108. package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-implementation/evals.json +78 -0
  109. package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-testing/SKILL.md +116 -0
  110. package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-testing/evals.json +75 -0
  111. package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-upgrade-migration/SKILL.md +134 -0
  112. package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-upgrade-migration/evals.json +76 -0
  113. package/src/gdskills/bundled/stacks/python/agent-refs.json +3 -0
  114. package/src/gdskills/bundled/stacks/python/governance/eval.json +1758 -0
  115. package/src/gdskills/bundled/stacks/python/governance/scout.json +34 -0
  116. package/src/gdskills/bundled/stacks/python/pack.json +41 -0
  117. package/src/gdskills/bundled/stacks/python/rules/coding-style.mdc +63 -0
  118. package/src/gdskills/bundled/stacks/python/rules/patterns.mdc +88 -0
  119. package/src/gdskills/bundled/stacks/python/rules/security.mdc +84 -0
  120. package/src/gdskills/bundled/stacks/python/rules/testing.mdc +77 -0
  121. package/src/gdskills/bundled/stacks/python/skills/python-build-fix/SKILL.md +144 -0
  122. package/src/gdskills/bundled/stacks/python/skills/python-build-fix/evals.json +74 -0
  123. package/src/gdskills/bundled/stacks/python/skills/python-code-review/SKILL.md +155 -0
  124. package/src/gdskills/bundled/stacks/python/skills/python-code-review/evals.json +72 -0
  125. package/src/gdskills/bundled/stacks/python/skills/python-implementation/SKILL.md +143 -0
  126. package/src/gdskills/bundled/stacks/python/skills/python-implementation/evals.json +78 -0
  127. package/src/gdskills/bundled/stacks/python/skills/python-testing/SKILL.md +132 -0
  128. package/src/gdskills/bundled/stacks/python/skills/python-testing/evals.json +73 -0
  129. package/src/gdskills/bundled/stacks/react/agent-refs.json +4 -0
  130. package/src/gdskills/bundled/stacks/react/governance/eval.json +2188 -0
  131. package/src/gdskills/bundled/stacks/react/governance/scout.json +40 -0
  132. package/src/gdskills/bundled/stacks/react/pack.json +42 -0
  133. package/src/gdskills/bundled/stacks/react/rules/coding-style.mdc +58 -0
  134. package/src/gdskills/bundled/stacks/react/rules/patterns.mdc +79 -0
  135. package/src/gdskills/bundled/stacks/react/rules/security.mdc +70 -0
  136. package/src/gdskills/bundled/stacks/react/rules/testing.mdc +60 -0
  137. package/src/gdskills/bundled/stacks/react/skills/react-build-fix/SKILL.md +139 -0
  138. package/src/gdskills/bundled/stacks/react/skills/react-build-fix/evals.json +72 -0
  139. package/src/gdskills/bundled/stacks/react/skills/react-code-review/SKILL.md +148 -0
  140. package/src/gdskills/bundled/stacks/react/skills/react-code-review/evals.json +74 -0
  141. package/src/gdskills/bundled/stacks/react/skills/react-implementation/SKILL.md +140 -0
  142. package/src/gdskills/bundled/stacks/react/skills/react-implementation/evals.json +74 -0
  143. package/src/gdskills/bundled/stacks/react/skills/react-testing/SKILL.md +142 -0
  144. package/src/gdskills/bundled/stacks/react/skills/react-testing/evals.json +83 -0
  145. package/src/gdskills/bundled/stacks/react/skills/react-upgrade-migration/SKILL.md +155 -0
  146. package/src/gdskills/bundled/stacks/react/skills/react-upgrade-migration/evals.json +74 -0
  147. package/src/gdskills/bundled/stacks/ts-js-node/agent-refs.json +4 -0
  148. package/src/gdskills/bundled/stacks/ts-js-node/governance/eval.json +2155 -0
  149. package/src/gdskills/bundled/stacks/ts-js-node/governance/scout.json +40 -0
  150. package/src/gdskills/bundled/stacks/ts-js-node/pack.json +41 -0
  151. package/src/gdskills/bundled/stacks/ts-js-node/rules/coding-style.mdc +73 -0
  152. package/src/gdskills/bundled/stacks/ts-js-node/rules/patterns.mdc +61 -0
  153. package/src/gdskills/bundled/stacks/ts-js-node/rules/security.mdc +71 -0
  154. package/src/gdskills/bundled/stacks/ts-js-node/rules/testing.mdc +63 -0
  155. package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-build-fix/SKILL.md +137 -0
  156. package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-build-fix/evals.json +73 -0
  157. package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-code-review/SKILL.md +124 -0
  158. package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-code-review/evals.json +74 -0
  159. package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-esm-migration/SKILL.md +152 -0
  160. package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-esm-migration/evals.json +71 -0
  161. package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-implementation/SKILL.md +127 -0
  162. package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-implementation/evals.json +72 -0
  163. package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-testing/SKILL.md +134 -0
  164. package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-testing/evals.json +70 -0
  165. package/src/gdskills/bundled/stacks/vue/agent-refs.json +4 -0
  166. package/src/gdskills/bundled/stacks/vue/governance/eval.json +2215 -0
  167. package/src/gdskills/bundled/stacks/vue/governance/scout.json +42 -0
  168. package/src/gdskills/bundled/stacks/vue/pack.json +42 -0
  169. package/src/gdskills/bundled/stacks/vue/rules/coding-style.mdc +73 -0
  170. package/src/gdskills/bundled/stacks/vue/rules/patterns.mdc +84 -0
  171. package/src/gdskills/bundled/stacks/vue/rules/security.mdc +60 -0
  172. package/src/gdskills/bundled/stacks/vue/rules/testing.mdc +69 -0
  173. package/src/gdskills/bundled/stacks/vue/skills/vue-build-fix/SKILL.md +137 -0
  174. package/src/gdskills/bundled/stacks/vue/skills/vue-build-fix/evals.json +72 -0
  175. package/src/gdskills/bundled/stacks/vue/skills/vue-code-review/SKILL.md +120 -0
  176. package/src/gdskills/bundled/stacks/vue/skills/vue-code-review/evals.json +71 -0
  177. package/src/gdskills/bundled/stacks/vue/skills/vue-implementation/SKILL.md +122 -0
  178. package/src/gdskills/bundled/stacks/vue/skills/vue-implementation/evals.json +72 -0
  179. package/src/gdskills/bundled/stacks/vue/skills/vue-testing/SKILL.md +115 -0
  180. package/src/gdskills/bundled/stacks/vue/skills/vue-testing/evals.json +72 -0
  181. package/src/gdskills/bundled/stacks/vue/skills/vue2-to-vue3-migration/SKILL.md +135 -0
  182. package/src/gdskills/bundled/stacks/vue/skills/vue2-to-vue3-migration/evals.json +71 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrciphersmith/keryx",
3
- "version": "0.2.164",
3
+ "version": "0.3.1",
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/capability/ src/commands/ src/contracts/ src/ctx/ src/eval/ src/flow/ src/forgetting/ src/gdgraph/ src/gdskills/ src/governance/ src/health/ src/job/ src/lib/ src/mcp/ src/memory/ src/metrics/ src/retention/ src/review/ src/sac/ src/security/ src/standard/ src/sync/ src/testing/ src/trigger/ src/wiki/",
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/",
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",
@@ -0,0 +1,141 @@
1
+ // Flow 308 (W8, Lane B, T6): the one builder behind BOTH `keryx gdgraph
2
+ // affected <target> --json` (src/commands/gdgraph.ts#runAffected) and the
3
+ // impact-evidence "importers" section (src/security/impact-evidence). AC9
4
+ // requires those two to be byte-for-byte the same JSON at the same graph
5
+ // state — this module is that shared source, so there is exactly one place
6
+ // that decides what the affected-report JSON looks like.
7
+ //
8
+ // This is a straight extraction of `runAffected`'s `--json` branch (and the
9
+ // checks that feed it) out of `src/commands/gdgraph.ts`, with every
10
+ // `process.cwd()` replaced by an explicit `root` parameter. `runAffected`
11
+ // itself now delegates to `buildAffectedReport` for its `--json` path and is
12
+ // otherwise untouched — its non-JSON rendering stays exactly as it was.
13
+ //
14
+ // Core zone (`src/lib/import-zones.ts`): this module must never import from
15
+ // `src/commands` or any other adapter — only from other core modules.
16
+
17
+ import { loadGraph } from "./query";
18
+ import { computeAffected, type AffectedResult } from "./affected";
19
+ import { resolveSymbols } from "./symbol";
20
+ import { loadGdgraphConfig } from "./config";
21
+ import { checkGraphStaleness, type StalenessCheck } from "./staleness";
22
+ import { RETRIEVAL_NEXT_ACTIONS, retrievalOutcome } from "../lib/retrieval-codes";
23
+ import { explainAbsentGraphTarget, loadDeletionTrail } from "../forgetting/service";
24
+
25
+ export interface AffectedReportOptions {
26
+ depth?: number;
27
+ ranked?: boolean;
28
+ }
29
+
30
+ export interface AffectedReport {
31
+ // The exit code `runAffected --json` would set for this exact answer.
32
+ exitCode: 0 | 1;
33
+ // The exact object `runAffected --json` prints via
34
+ // `JSON.stringify(json, null, 2)` — one of the three shapes described at
35
+ // the top: index-incomplete, target-not-indexed (with `removal`), or the
36
+ // success `{...affected, freshness}` shape.
37
+ json: Record<string, unknown>;
38
+ // The symbol->file resolution note `runAffected` prints above its text
39
+ // output (non-JSON only) — carried here so a caller that wants it (none do
40
+ // today) does not have to re-derive it.
41
+ resolutionNote: string;
42
+ }
43
+
44
+ /**
45
+ * Build the affected report for `target` under `root`, reproducing
46
+ * `runAffected --json` exactly. A caller error (an ambiguous suffix — see
47
+ * `./target.ts#resolveGraphTarget`) is NOT caught here: it is thrown, the
48
+ * same way `computeAffected` throws it, so a caller maps it to whatever
49
+ * "bad argument" handling it already has (`runAffected`'s `--json` branch
50
+ * catches it and reproduces its existing stderr+exit-1 behavior).
51
+ */
52
+ export async function buildAffectedReport(
53
+ root: string,
54
+ targetInput: string,
55
+ opts: AffectedReportOptions = {},
56
+ ): Promise<AffectedReport> {
57
+ const config = await loadGdgraphConfig(root);
58
+ const depth = opts.depth !== undefined && Number.isFinite(opts.depth) ? opts.depth : config.affected.defaultDepth;
59
+ const ranked = opts.ranked ?? true;
60
+
61
+ const graph = await loadGraph(root);
62
+
63
+ // Symbol-aware: if the target isn't a known file but names a symbol,
64
+ // resolve it to its owning file — mirrors `runAffected`'s own resolution.
65
+ let target = targetInput;
66
+ const isFile = graph.nodes.some((n) => n.kind === "file" && n.path === target);
67
+ let resolutionNote = "";
68
+ if (!isFile && graph.symbols && graph.symbols.length > 0) {
69
+ const hits = resolveSymbols(graph.symbols, target, 5);
70
+ const files = [...new Set(hits.map((s) => s.path))];
71
+ if (files.length > 0) {
72
+ resolutionNote = `resolved symbol "${target}" → ${files[0]}${files.length > 1 ? ` (+${files.length - 1} more file)` : ""}`;
73
+ target = files[0]!;
74
+ }
75
+ }
76
+
77
+ // Caller error (ambiguous suffix): let it throw. See doc comment above.
78
+ const affected: AffectedResult = computeAffected(graph, target, { depth, ranked });
79
+
80
+ if (graph.nodes.length === 0) {
81
+ const outcome = retrievalOutcome(
82
+ "index-incomplete",
83
+ "the graph index holds no file nodes — it was never built here, or its storage is unreadable. " +
84
+ "No claim is being made about whether this target exists.",
85
+ );
86
+ const freshness = await checkGraphStaleness(root);
87
+ return {
88
+ exitCode: 1,
89
+ resolutionNote,
90
+ json: {
91
+ schemaVersion: 1,
92
+ code: outcome.code,
93
+ error: outcome.code,
94
+ reason: outcome.reason,
95
+ nextActions: outcome.nextActions,
96
+ target,
97
+ dependencies: [],
98
+ dependents: [],
99
+ freshness,
100
+ },
101
+ };
102
+ }
103
+
104
+ const isKnownNode = graph.nodes.some((node) => node.path === affected.target);
105
+ if (!isKnownNode) {
106
+ const absence = explainAbsentGraphTarget(graph, affected.target, await loadDeletionTrail(root));
107
+ const message =
108
+ `gdgraph: "${target}" is not a node in the built graph (never indexed, or the ` +
109
+ `path/symbol does not exist) — this is not the same as an indexed target with ` +
110
+ `zero edges. Run \`keryx gdgraph build\` if the file is new, or double-check the path.`;
111
+ const freshness = await checkGraphStaleness(root);
112
+ return {
113
+ exitCode: 1,
114
+ resolutionNote,
115
+ json: {
116
+ schemaVersion: 1,
117
+ code: "target-not-indexed",
118
+ error: "target-not-indexed",
119
+ reason: message,
120
+ nextActions: RETRIEVAL_NEXT_ACTIONS["target-not-indexed"],
121
+ target,
122
+ dependencies: [],
123
+ dependents: [],
124
+ removal: {
125
+ verdict: absence.verdict,
126
+ reason: absence.reason,
127
+ referencedBy: absence.referencedBy,
128
+ trailPath: absence.removal.path,
129
+ },
130
+ freshness,
131
+ },
132
+ };
133
+ }
134
+
135
+ const freshness: StalenessCheck = await checkGraphStaleness(root);
136
+ return {
137
+ exitCode: 0,
138
+ resolutionNote,
139
+ json: { ...affected, freshness },
140
+ };
141
+ }
@@ -67,6 +67,20 @@ type SourceCollection = {
67
67
  type PathMapping = {
68
68
  pattern: string;
69
69
  targets: string[];
70
+ // GDGRAPH-3 fix round 2 (R2-2): the project-root-relative directory these
71
+ // targets resolve against — the EFFECTIVE `baseUrl` of the whole `extends`
72
+ // chain when ANY config in that chain sets one (child's own `baseUrl` wins
73
+ // over an inherited one, per field-level override), otherwise the directory
74
+ // of whichever config in the chain declares the (wholesale, nearest-wins)
75
+ // `paths` map that ended up effective (TS 4.1 `pathsBasePath`). This can
76
+ // only be known once the ENTIRE chain has been walked, so it is filled in
77
+ // after `resolveTsconfigOptions` returns — never at the point a config's own
78
+ // `paths`/`baseUrl` is first read. "" means the project root itself.
79
+ // `paths` is taken wholesale (never merged) from the nearest config in the
80
+ // chain that declares it, so every mapping in a given resolved set shares
81
+ // this same `base` — it is carried per-mapping only so `createTsconfigResolver`
82
+ // does not need a second parameter to learn it.
83
+ base: string;
70
84
  };
71
85
 
72
86
  // A per-language import resolver, selected by the importing file's language.
@@ -609,6 +623,37 @@ export function parseGradleSourceRoots(buildGradle: string): string[] {
609
623
  return [...roots];
610
624
  }
611
625
 
626
+ // GDGRAPH-3 (W7): the root `tsconfig.json` may itself declare no
627
+ // `paths`/`baseUrl` and instead `extends` a shared base config (common in a
628
+ // monorepo with one `tsconfig.base.json` carrying the alias map for every
629
+ // package). Before this fix, only the root config's own `compilerOptions`
630
+ // were read — an inherited alias resolved to nothing, silently, with no
631
+ // error. `resolveTsconfigOptions` below walks the `extends` chain, merging
632
+ // `baseUrl`/`paths` so an inherited alias resolves the same way `tsc` itself
633
+ // would for it.
634
+ const TSCONFIG_EXTENDS_MAX_DEPTH = 10;
635
+
636
+ // GDGRAPH-3 fix round 2 (R2-2): `baseUrl` and `paths` are tracked as two
637
+ // SEPARATE properties of the whole `extends` chain, matching `tsc`'s actual
638
+ // resolution (verified against `tsc --traceResolution`):
639
+ // - `baseUrl`: the EFFECTIVE baseUrl of the chain — the nearest (most
640
+ // child-ward) config that sets one, full stop. It does not matter which
641
+ // config declared the `paths` that ended up effective; ANY config in the
642
+ // chain setting `baseUrl` makes every mapping resolve against it.
643
+ // - `pathsBasePath`: the directory of whichever config declares the
644
+ // (wholesale, nearest-wins) `paths` map that ended up effective. This is
645
+ // the fallback used ONLY when NO config anywhere in the chain sets
646
+ // `baseUrl`.
647
+ // Each mapping's final `base` (`effectiveBaseUrl ?? pathsBasePath`) can only
648
+ // be computed once the whole chain is known, so `resolveTsconfigOptions`
649
+ // leaves it unset and `loadTsconfigResolver` fills it in on the fully merged
650
+ // result below.
651
+ type TsconfigOptions = {
652
+ baseUrl: string | null;
653
+ paths: Array<{ pattern: string; targets: string[] }>;
654
+ pathsBasePath: string | null;
655
+ };
656
+
612
657
  async function loadTsconfigResolver(projectRoot: string): Promise<ImportResolver> {
613
658
  const empty = createTsconfigResolver(null, []);
614
659
  const tsconfigPath = path.join(projectRoot, "tsconfig.json");
@@ -616,30 +661,132 @@ async function loadTsconfigResolver(projectRoot: string): Promise<ImportResolver
616
661
  return empty;
617
662
  }
618
663
 
664
+ const resolved = await resolveTsconfigOptions(tsconfigPath, new Set<string>(), 0, projectRoot);
665
+ if (!resolved) {
666
+ return empty;
667
+ }
668
+ const base = resolved.baseUrl ?? resolved.pathsBasePath ?? "";
669
+ const paths: PathMapping[] = resolved.paths.map((mapping) => ({ ...mapping, base }));
670
+ return createTsconfigResolver(resolved.baseUrl, paths);
671
+ }
672
+
673
+ // GDGRAPH-3 fix round 1 (F6): `baseUrl` and `paths` are each resolved relative
674
+ // to the config file that DECLARES them, not relative to the project root —
675
+ // the same rule `tsc` itself follows. A config in a subdirectory
676
+ // (`config/tsconfig.base.json`) declaring `baseUrl: ".."` means "one level up
677
+ // from `config/`" (the project root), never a literal `".."` measured from the
678
+ // project root (which would escape it entirely). `toProjectRelativeDir` below
679
+ // converts an absolute directory into this resolver's project-root-relative,
680
+ // posix, no-leading-"./" convention ("" means the project root itself) so
681
+ // every `baseUrl`/mapping `base` this module carries is expressed in the same
682
+ // coordinate space regardless of which file in the `extends` chain declared
683
+ // it.
684
+ function toProjectRelativeDir(projectRoot: string, absoluteDir: string): string {
685
+ const relative = normalizePath(path.relative(projectRoot, absoluteDir)).replace(/^\.\//, "");
686
+ return relative === "." ? "" : relative;
687
+ }
688
+
689
+ async function readTsconfigJson(
690
+ configPath: string,
691
+ ): Promise<{ compilerOptions?: { baseUrl?: unknown; paths?: unknown }; extends?: unknown } | null> {
619
692
  try {
620
- const raw = await readFile(tsconfigPath, "utf8");
621
- const parsed = JSON.parse(stripJsonComments(raw)) as {
622
- compilerOptions?: {
623
- baseUrl?: unknown;
624
- paths?: unknown;
625
- };
693
+ const raw = await readFile(configPath, "utf8");
694
+ return JSON.parse(stripJsonComments(raw)) as {
695
+ compilerOptions?: { baseUrl?: unknown; paths?: unknown };
696
+ extends?: unknown;
626
697
  };
627
- const options = parsed.compilerOptions ?? {};
628
- const baseUrl = typeof options.baseUrl === "string"
629
- ? normalizePath(path.posix.normalize(options.baseUrl)).replace(/^\.\//, "")
630
- : null;
631
- const paths = isRecord(options.paths)
632
- ? Object.entries(options.paths)
633
- .filter((entry): entry is [string, string[]] => Array.isArray(entry[1]))
634
- .map(([pattern, targets]) => ({
635
- pattern,
636
- targets: targets.filter((target): target is string => typeof target === "string"),
637
- }))
638
- : [];
639
- return createTsconfigResolver(baseUrl, paths);
640
698
  } catch {
641
- return empty;
699
+ return null;
700
+ }
701
+ }
702
+
703
+ // A relative/local `extends` ("./tsconfig.base.json", "../base.json") is
704
+ // resolved against the EXTENDING config's own directory — the same rule
705
+ // `tsc` uses — and gets a `.json` suffix when the specifier omits one. A
706
+ // bare package specifier (no leading "." or "/", e.g. "@tsconfig/node20")
707
+ // is intentionally left unresolved here: following it would mean
708
+ // replicating node_modules package resolution inside this cheap, dependency-
709
+ // light graph-build resolver. It is skipped, not treated as an error — the
710
+ // chain simply stops contributing inherited `paths`/`baseUrl` from that link.
711
+ function resolveTsconfigExtendsPath(configDir: string, extendsSpecifier: string): string | null {
712
+ if (!extendsSpecifier.startsWith(".") && !extendsSpecifier.startsWith("/")) {
713
+ return null;
642
714
  }
715
+ const resolved = path.resolve(configDir, extendsSpecifier);
716
+ return resolved.endsWith(".json") ? resolved : `${resolved}.json`;
717
+ }
718
+
719
+ // Depth-capped (10 hops — no real project chains that deep) and cycle-guarded
720
+ // (a config that `extends` back to one already on the current chain stops
721
+ // walking rather than looping forever). `baseUrl`/`paths` are each replaced
722
+ // wholesale by a child that declares them (TS's own `extends` semantics —
723
+ // not a deep merge), inherited unchanged from the base otherwise. Any parse
724
+ // failure at any link degrades that link to "nothing inherited", never a
725
+ // thrown error that would take the whole graph build down.
726
+ async function resolveTsconfigOptions(
727
+ configPath: string,
728
+ visited: Set<string>,
729
+ depth: number,
730
+ projectRoot: string,
731
+ ): Promise<TsconfigOptions | null> {
732
+ const resolvedPath = path.resolve(configPath);
733
+ if (depth > TSCONFIG_EXTENDS_MAX_DEPTH || visited.has(resolvedPath)) {
734
+ return null;
735
+ }
736
+ visited.add(resolvedPath);
737
+
738
+ const parsed = await readTsconfigJson(resolvedPath);
739
+ if (!parsed) {
740
+ return null;
741
+ }
742
+
743
+ let inherited: TsconfigOptions | null = null;
744
+ if (typeof parsed.extends === "string") {
745
+ const basePath = resolveTsconfigExtendsPath(path.dirname(resolvedPath), parsed.extends);
746
+ if (basePath) {
747
+ inherited = await resolveTsconfigOptions(basePath, visited, depth + 1, projectRoot);
748
+ }
749
+ }
750
+
751
+ // R2-2: `baseUrl` resolves relative to THIS config's own directory when
752
+ // THIS config sets one — never the project root and never some other
753
+ // config's directory. `declaringDir` is this config's own directory,
754
+ // already expressed in the resolver's project-root-relative convention;
755
+ // it is the `pathsBasePath` candidate when THIS config declares `paths`.
756
+ const configDir = path.dirname(resolvedPath);
757
+ const declaringDir = toProjectRelativeDir(projectRoot, configDir);
758
+
759
+ const options = parsed.compilerOptions ?? {};
760
+ const ownBaseUrl = typeof options.baseUrl === "string"
761
+ ? toProjectRelativeDir(projectRoot, path.resolve(configDir, options.baseUrl))
762
+ : null;
763
+ // Child overrides parent per field: this config's own `baseUrl` wins when
764
+ // present, otherwise the (already-resolved) inherited one, otherwise none.
765
+ // This is the EFFECTIVE baseUrl of the chain as seen from this level —
766
+ // since a config's own fields are computed only after its `extends`
767
+ // ancestor has already been fully resolved, the outermost caller's return
768
+ // value carries the true effective baseUrl for the whole chain.
769
+ const baseUrl = ownBaseUrl ?? inherited?.baseUrl ?? null;
770
+
771
+ const ownPaths = isRecord(options.paths)
772
+ ? Object.entries(options.paths)
773
+ .filter((entry): entry is [string, string[]] => Array.isArray(entry[1]))
774
+ .map(([pattern, targets]) => ({
775
+ pattern,
776
+ targets: targets.filter((target): target is string => typeof target === "string"),
777
+ }))
778
+ : null;
779
+ // `paths` is replaced wholesale by a child that declares its own (never
780
+ // merged with the parent's). Crucially, the `base` those mappings resolve
781
+ // against is NOT decided here: whether it ends up being THIS config's
782
+ // directory depends on whether ANY config anywhere in the chain (including
783
+ // ones not yet visited further up) sets `baseUrl`, which isn't known until
784
+ // the whole chain unwinds. So only `pathsBasePath` (this declaring config's
785
+ // own directory) is tracked here, and the real `base` is filled in once by
786
+ // `loadTsconfigResolver` from the final `baseUrl ?? pathsBasePath`.
787
+ const paths = ownPaths ?? inherited?.paths ?? [];
788
+ const pathsBasePath = ownPaths ? declaringDir : (inherited?.pathsBasePath ?? null);
789
+ return { baseUrl, paths, pathsBasePath };
643
790
  }
644
791
 
645
792
  function createTsconfigResolver(baseUrl: string | null, mappings: PathMapping[]): ImportResolver {
@@ -657,7 +804,7 @@ function createTsconfigResolver(baseUrl: string | null, mappings: PathMapping[])
657
804
  continue;
658
805
  }
659
806
  for (const target of mapping.targets) {
660
- candidates.push(applyPathTarget(baseUrl, target, match));
807
+ candidates.push(applyPathTarget(mapping.base, target, match));
661
808
  }
662
809
  }
663
810
  if (baseUrl !== null) {
@@ -682,9 +829,9 @@ function matchPathPattern(pattern: string, specifier: string): string | null {
682
829
  return specifier.slice(prefix.length, specifier.length - suffix.length);
683
830
  }
684
831
 
685
- function applyPathTarget(baseUrl: string | null, target: string, wildcard: string): string {
832
+ function applyPathTarget(base: string, target: string, wildcard: string): string {
686
833
  const replaced = target.includes("*") ? target.replace("*", wildcard) : target;
687
- return normalizePath(path.posix.normalize(path.posix.join(baseUrl ?? "", replaced)));
834
+ return normalizePath(path.posix.normalize(path.posix.join(base, replaced)));
688
835
  }
689
836
 
690
837
  function stripJsonComments(source: string): string {
@@ -12,6 +12,12 @@ import { getCycles, getOrphans, loadGraph } from "./query";
12
12
  import { writeRepomap, type RepomapOptions, type RepomapResult } from "./repomap";
13
13
  import type { GraphData } from "./types";
14
14
 
15
+ // Re-exported so a client (`commands/gdgraph.ts`) can build the shared
16
+ // `--json` report through this facade rather than reaching past it into
17
+ // `gdgraph/affected-report.ts` directly (`src/lib/import-policy.ts`'s
18
+ // facade rule).
19
+ export { buildAffectedReport, type AffectedReport, type AffectedReportOptions } from "./affected-report";
20
+
15
21
  export interface GdgraphService {
16
22
  build(cwd: string): Promise<{ nodes: number; edges: number; summaryPath: string }>;
17
23
  loadGraph(cwd: string): Promise<GraphData>;