@mrciphersmith/keryx 0.2.163 → 0.3.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/cli.js +87904 -56917
- package/dist/core.js +28418 -18708
- package/package.json +2 -2
- package/src/gdgraph/affected-report.ts +141 -0
- package/src/gdgraph/build.ts +170 -23
- package/src/gdgraph/service.ts +6 -0
- package/src/gdgraph/staleness.ts +253 -45
- package/src/gdskills/bundled/agents/codebase-navigator.md +55 -0
- package/src/gdskills/bundled/agents/design-advisor.md +64 -0
- package/src/gdskills/bundled/agents/docs-maintainer.md +56 -0
- package/src/gdskills/bundled/agents/end-to-end-tester.md +56 -0
- package/src/gdskills/bundled/agents/error-path-auditor.md +57 -0
- package/src/gdskills/bundled/agents/go-build-fixer.md +52 -0
- package/src/gdskills/bundled/agents/go-code-auditor.md +49 -0
- package/src/gdskills/bundled/agents/performance-auditor.md +63 -0
- package/src/gdskills/bundled/agents/python-build-fixer.md +52 -0
- package/src/gdskills/bundled/agents/python-code-auditor.md +49 -0
- package/src/gdskills/bundled/agents/refactoring-steward.md +61 -0
- package/src/gdskills/bundled/agents/security-auditor.md +62 -0
- package/src/gdskills/bundled/agents/test-first-driver.md +61 -0
- package/src/gdskills/bundled/agents/work-planner.md +62 -0
- package/src/gdskills/bundled/install-manifest.json +530 -0
- package/src/gdskills/bundled/rules/core/skill-lifecycle.mdc +29 -1
- package/src/gdskills/bundled/rules/core/skills-storage-workflow.mdc +2 -2
- package/src/gdskills/bundled/skills/review/code-style-review/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/review/review-orchestrator/SKILL.md +74 -246
- package/src/gdskills/bundled/skills/review/review-orchestrator/output-contract.schema.json +19 -0
- package/src/gdskills/bundled/skills/review/review-orchestrator/reviewer-finding.schema.json +10 -0
- package/src/gdskills/bundled/skills/review/review-orchestrator/reviewer-input.schema.json +5 -0
- package/src/gdskills/bundled/skills/review/review-orchestrator/templates/pr-comment-backend.md +50 -0
- package/src/gdskills/bundled/skills/review/review-orchestrator/templates/pr-comment-frontend.md +52 -0
- package/src/gdskills/bundled/skills/review/review-orchestrator/templates/review-report.md +143 -0
- package/src/gdskills/bundled/stacks/go/agent-refs.json +3 -0
- package/src/gdskills/bundled/stacks/go/governance/eval.json +1745 -0
- package/src/gdskills/bundled/stacks/go/governance/scout.json +31 -0
- package/src/gdskills/bundled/stacks/go/pack.json +41 -0
- package/src/gdskills/bundled/stacks/go/rules/coding-style.mdc +85 -0
- package/src/gdskills/bundled/stacks/go/rules/patterns.mdc +65 -0
- package/src/gdskills/bundled/stacks/go/rules/security.mdc +73 -0
- package/src/gdskills/bundled/stacks/go/rules/testing.mdc +68 -0
- package/src/gdskills/bundled/stacks/go/skills/go-build-fix/SKILL.md +138 -0
- package/src/gdskills/bundled/stacks/go/skills/go-build-fix/evals.json +75 -0
- package/src/gdskills/bundled/stacks/go/skills/go-code-review/SKILL.md +121 -0
- package/src/gdskills/bundled/stacks/go/skills/go-code-review/evals.json +72 -0
- package/src/gdskills/bundled/stacks/go/skills/go-implementation/SKILL.md +122 -0
- package/src/gdskills/bundled/stacks/go/skills/go-implementation/evals.json +76 -0
- package/src/gdskills/bundled/stacks/go/skills/go-testing/SKILL.md +126 -0
- package/src/gdskills/bundled/stacks/go/skills/go-testing/evals.json +73 -0
- package/src/gdskills/bundled/stacks/python/agent-refs.json +3 -0
- package/src/gdskills/bundled/stacks/python/governance/eval.json +1758 -0
- package/src/gdskills/bundled/stacks/python/governance/scout.json +34 -0
- package/src/gdskills/bundled/stacks/python/pack.json +41 -0
- package/src/gdskills/bundled/stacks/python/rules/coding-style.mdc +63 -0
- package/src/gdskills/bundled/stacks/python/rules/patterns.mdc +88 -0
- package/src/gdskills/bundled/stacks/python/rules/security.mdc +84 -0
- package/src/gdskills/bundled/stacks/python/rules/testing.mdc +77 -0
- package/src/gdskills/bundled/stacks/python/skills/python-build-fix/SKILL.md +144 -0
- package/src/gdskills/bundled/stacks/python/skills/python-build-fix/evals.json +74 -0
- package/src/gdskills/bundled/stacks/python/skills/python-code-review/SKILL.md +155 -0
- package/src/gdskills/bundled/stacks/python/skills/python-code-review/evals.json +72 -0
- package/src/gdskills/bundled/stacks/python/skills/python-implementation/SKILL.md +143 -0
- package/src/gdskills/bundled/stacks/python/skills/python-implementation/evals.json +78 -0
- package/src/gdskills/bundled/stacks/python/skills/python-testing/SKILL.md +132 -0
- package/src/gdskills/bundled/stacks/python/skills/python-testing/evals.json +73 -0
- package/src/gdskills/bundled/stacks/react/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/react/governance/eval.json +2188 -0
- package/src/gdskills/bundled/stacks/react/governance/scout.json +40 -0
- package/src/gdskills/bundled/stacks/react/pack.json +42 -0
- package/src/gdskills/bundled/stacks/react/rules/coding-style.mdc +58 -0
- package/src/gdskills/bundled/stacks/react/rules/patterns.mdc +79 -0
- package/src/gdskills/bundled/stacks/react/rules/security.mdc +70 -0
- package/src/gdskills/bundled/stacks/react/rules/testing.mdc +60 -0
- package/src/gdskills/bundled/stacks/react/skills/react-build-fix/SKILL.md +139 -0
- package/src/gdskills/bundled/stacks/react/skills/react-build-fix/evals.json +72 -0
- package/src/gdskills/bundled/stacks/react/skills/react-code-review/SKILL.md +148 -0
- package/src/gdskills/bundled/stacks/react/skills/react-code-review/evals.json +74 -0
- package/src/gdskills/bundled/stacks/react/skills/react-implementation/SKILL.md +140 -0
- package/src/gdskills/bundled/stacks/react/skills/react-implementation/evals.json +74 -0
- package/src/gdskills/bundled/stacks/react/skills/react-testing/SKILL.md +142 -0
- package/src/gdskills/bundled/stacks/react/skills/react-testing/evals.json +83 -0
- package/src/gdskills/bundled/stacks/react/skills/react-upgrade-migration/SKILL.md +155 -0
- package/src/gdskills/bundled/stacks/react/skills/react-upgrade-migration/evals.json +74 -0
- package/src/gdskills/bundled/stacks/ts-js-node/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/ts-js-node/governance/eval.json +2155 -0
- package/src/gdskills/bundled/stacks/ts-js-node/governance/scout.json +40 -0
- package/src/gdskills/bundled/stacks/ts-js-node/pack.json +41 -0
- package/src/gdskills/bundled/stacks/ts-js-node/rules/coding-style.mdc +73 -0
- package/src/gdskills/bundled/stacks/ts-js-node/rules/patterns.mdc +61 -0
- package/src/gdskills/bundled/stacks/ts-js-node/rules/security.mdc +71 -0
- package/src/gdskills/bundled/stacks/ts-js-node/rules/testing.mdc +63 -0
- package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-build-fix/SKILL.md +137 -0
- package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-build-fix/evals.json +73 -0
- package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-code-review/SKILL.md +124 -0
- package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-code-review/evals.json +74 -0
- package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-esm-migration/SKILL.md +152 -0
- package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-esm-migration/evals.json +71 -0
- package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-implementation/SKILL.md +127 -0
- package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-implementation/evals.json +72 -0
- package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-testing/SKILL.md +134 -0
- package/src/gdskills/bundled/stacks/ts-js-node/skills/nodejs-testing/evals.json +70 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrciphersmith/keryx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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
|
+
}
|
package/src/gdgraph/build.ts
CHANGED
|
@@ -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(
|
|
621
|
-
|
|
622
|
-
compilerOptions?: {
|
|
623
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
834
|
+
return normalizePath(path.posix.normalize(path.posix.join(base, replaced)));
|
|
688
835
|
}
|
|
689
836
|
|
|
690
837
|
function stripJsonComments(source: string): string {
|
package/src/gdgraph/service.ts
CHANGED
|
@@ -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>;
|