@mmerterden/multi-agent-pipeline 16.12.0 → 16.13.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/CHANGELOG.md +20 -0
- package/README.md +4 -4
- package/README.tr.md +4 -4
- package/docs/adr/0010-own-code-graph.md +129 -0
- package/docs/adr/README.md +1 -0
- package/docs/architecture.md +2 -2
- package/docs/ecosystem.md +5 -5
- package/docs/features.md +8 -0
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/graph/SKILL.md +105 -0
- package/pipeline/commands/multi-agent/help/SKILL.md +8 -8
- package/pipeline/commands/multi-agent/sync/SKILL.md +12 -12
- package/pipeline/commands/multi-agent/uninstall/SKILL.md +9 -7
- package/pipeline/multi-agent-refs/cross-cli-contract.md +10 -10
- package/pipeline/multi-agent-refs/features/code-graph.md +62 -0
- package/pipeline/multi-agent-refs/features/model-fallback.md +44 -2
- package/pipeline/multi-agent-refs/knowledge.md +6 -0
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-1-analysis.md +5 -0
- package/pipeline/multi-agent-refs/phases/phase-4-review.md +3 -3
- package/pipeline/multi-agent-refs/phases/phase-7-report.md +2 -0
- package/pipeline/preferences-template.json +2 -0
- package/pipeline/schemas/code-graph.schema.json +91 -0
- package/pipeline/schemas/prefs.schema.json +45 -0
- package/pipeline/schemas/token-budget.json +2 -2
- package/pipeline/scripts/_code-graph.mjs +518 -0
- package/pipeline/scripts/_path-match.mjs +87 -0
- package/pipeline/scripts/code-graph-rules/android.json +130 -0
- package/pipeline/scripts/code-graph-rules/ios.json +95 -0
- package/pipeline/scripts/code-graph-rules/node.json +151 -0
- package/pipeline/scripts/code-graph-rules/python.json +91 -0
- package/pipeline/scripts/graph-affected.mjs +161 -0
- package/pipeline/scripts/graph-build.mjs +157 -0
- package/pipeline/scripts/graph-query.mjs +191 -0
- package/pipeline/scripts/graph-report.mjs +237 -0
- package/pipeline/scripts/smoke-cross-cli-behavior.sh +7 -4
- package/pipeline/scripts/test-gap-rules/ios.json +38 -10
- package/pipeline/scripts/test-gap-scan.mjs +2 -21
- package/pipeline/scripts/uninstall.mjs +11 -2
- package/pipeline/scripts/validate-code-graph.mjs +174 -0
- package/pipeline/skills/.skills-index.json +14 -3
- package/pipeline/skills/shared/README.md +6 -5
- package/pipeline/skills/shared/core/multi-agent-graph/SKILL.md +106 -0
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +11 -11
- package/pipeline/skills/shared/core/multi-agent-uninstall/SKILL.md +4 -4
- package/pipeline/skills/skills-index.md +4 -3
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"stack": "android",
|
|
4
|
+
"description": "Kotlin + Java code-graph rules. Adds what the graph needs on top of test-gap-rules/android.json, which already owns sourceExtensions, excludePathGlobs and the test-path predicates. Definition patterns here are deliberately broader than the test-gap publicApiPatterns: the graph wants internal types too, because Phase 1 narrows scope on the whole module, not just its public surface.",
|
|
5
|
+
"comments": {
|
|
6
|
+
"line": [
|
|
7
|
+
"//"
|
|
8
|
+
],
|
|
9
|
+
"block": [
|
|
10
|
+
[
|
|
11
|
+
"/*",
|
|
12
|
+
"*/"
|
|
13
|
+
]
|
|
14
|
+
],
|
|
15
|
+
"string": [
|
|
16
|
+
"\"\"\"",
|
|
17
|
+
"\""
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"definitionPatterns": [
|
|
21
|
+
{
|
|
22
|
+
"id": "enum",
|
|
23
|
+
"kind": "enum",
|
|
24
|
+
"regex": "\\benum\\s+(?:class\\s+)?([A-Z][A-Za-z0-9_]*)"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "record",
|
|
28
|
+
"kind": "record",
|
|
29
|
+
"regex": "\\brecord\\s+([A-Z][A-Za-z0-9_]*)"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": "typealias",
|
|
33
|
+
"kind": "typealias",
|
|
34
|
+
"regex": "\\btypealias\\s+([A-Z][A-Za-z0-9_]*)"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": "object",
|
|
38
|
+
"kind": "object",
|
|
39
|
+
"regex": "\\bobject\\s+([A-Z][A-Za-z0-9_]*)"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"id": "interface",
|
|
43
|
+
"kind": "interface",
|
|
44
|
+
"regex": "\\binterface\\s+([A-Z][A-Za-z0-9_]*)"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"id": "class",
|
|
48
|
+
"kind": "class",
|
|
49
|
+
"regex": "\\bclass\\s+([A-Z][A-Za-z0-9_]*)"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "fun",
|
|
53
|
+
"kind": "fun",
|
|
54
|
+
"regex": "\\bfun\\s+(?:<[^>]*>\\s*)?([a-zA-Z_][A-Za-z0-9_]*)"
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
"importPatterns": [
|
|
58
|
+
{
|
|
59
|
+
"id": "import",
|
|
60
|
+
"regex": "^\\s*import\\s+(?:static\\s+)?(?:[A-Za-z_][A-Za-z0-9_]*\\.)+([A-Z][A-Za-z0-9_]*)"
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
"referenceKinds": [
|
|
64
|
+
"class",
|
|
65
|
+
"interface",
|
|
66
|
+
"object",
|
|
67
|
+
"enum",
|
|
68
|
+
"record",
|
|
69
|
+
"typealias"
|
|
70
|
+
],
|
|
71
|
+
"referenceKindsNote": "fun is deliberately absent, for the reason Swift's rules record for func: a bare lowercase name matched across files is almost never a call to that exact declaration, and on a large app the generic ones (invoke, build, create, get) take the top god-node slots purely because each happened to be declared once. Composables are the tempting exception - they are capitalised and behave like types - but they are declared with fun, so including them means including every other fun with them.",
|
|
72
|
+
"ignoredIdentifiers": [
|
|
73
|
+
"this",
|
|
74
|
+
"super",
|
|
75
|
+
"it",
|
|
76
|
+
"init",
|
|
77
|
+
"constructor",
|
|
78
|
+
"companion",
|
|
79
|
+
"class",
|
|
80
|
+
"interface",
|
|
81
|
+
"object",
|
|
82
|
+
"enum",
|
|
83
|
+
"record",
|
|
84
|
+
"fun",
|
|
85
|
+
"val",
|
|
86
|
+
"var",
|
|
87
|
+
"return",
|
|
88
|
+
"when",
|
|
89
|
+
"where",
|
|
90
|
+
"is",
|
|
91
|
+
"as",
|
|
92
|
+
"in",
|
|
93
|
+
"out",
|
|
94
|
+
"by",
|
|
95
|
+
"import",
|
|
96
|
+
"package",
|
|
97
|
+
"public",
|
|
98
|
+
"private",
|
|
99
|
+
"protected",
|
|
100
|
+
"internal",
|
|
101
|
+
"static",
|
|
102
|
+
"final",
|
|
103
|
+
"abstract",
|
|
104
|
+
"open",
|
|
105
|
+
"override",
|
|
106
|
+
"sealed",
|
|
107
|
+
"data",
|
|
108
|
+
"inline",
|
|
109
|
+
"suspend",
|
|
110
|
+
"lateinit",
|
|
111
|
+
"throws",
|
|
112
|
+
"try",
|
|
113
|
+
"catch",
|
|
114
|
+
"finally",
|
|
115
|
+
"null",
|
|
116
|
+
"true",
|
|
117
|
+
"false",
|
|
118
|
+
"void",
|
|
119
|
+
"int",
|
|
120
|
+
"long",
|
|
121
|
+
"boolean",
|
|
122
|
+
"String",
|
|
123
|
+
"Int",
|
|
124
|
+
"Long",
|
|
125
|
+
"Boolean",
|
|
126
|
+
"Unit",
|
|
127
|
+
"Any"
|
|
128
|
+
],
|
|
129
|
+
"note": "Kotlin declaration modifiers are not separate patterns: `data class Foo`, `sealed class Foo` and `annotation class Foo` all still contain `class Foo`, so one pattern covers them. `enum` allows an optional `class` between keyword and name, which is the only shape that differs between Java (`enum Foo`) and Kotlin (`enum class Foo`). `object` matches the named form only - `companion object {` and the anonymous `object : Iface {` carry no name and correctly produce nothing. Imports are fully qualified on the JVM, so the pattern captures the final segment: `import com.example.ui.Button` resolves against the file named Button.kt, which is how the engine links a file-level import edge. A star import (`import com.example.ui.*`) captures nothing, deliberately - it names a package, and pointing every star import at an arbitrary file in that package would invent an edge nobody wrote. Pattern ORDER is load-bearing: `enum class Cabin` matches both the enum pattern and the class pattern, and the first match to register the name decides its kind, so the more specific patterns are listed first. With class first, every Kotlin enum was filed as a class and the report's symbol-kind census was wrong."
|
|
130
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"stack": "ios",
|
|
4
|
+
"description": "Swift code-graph rules. Adds what the graph needs on top of test-gap-rules/ios.json, which already owns sourceExtensions, excludePathGlobs and the test-path predicates. Definition patterns here are deliberately broader than the test-gap publicApiPatterns: the graph wants internal types too, because Phase 1 narrows scope on the whole module, not just its public surface.",
|
|
5
|
+
"comments": {
|
|
6
|
+
"line": ["//"],
|
|
7
|
+
"block": [["/*", "*/"]],
|
|
8
|
+
"string": ["\"\"\"", "\""]
|
|
9
|
+
},
|
|
10
|
+
"definitionPatterns": [
|
|
11
|
+
{
|
|
12
|
+
"id": "class",
|
|
13
|
+
"kind": "class",
|
|
14
|
+
"regex": "\\b(?:final\\s+)?class\\s+([A-Z][A-Za-z0-9_]*)"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": "struct",
|
|
18
|
+
"kind": "struct",
|
|
19
|
+
"regex": "\\bstruct\\s+([A-Z][A-Za-z0-9_]*)"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"id": "enum",
|
|
23
|
+
"kind": "enum",
|
|
24
|
+
"regex": "\\benum\\s+([A-Z][A-Za-z0-9_]*)"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "protocol",
|
|
28
|
+
"kind": "protocol",
|
|
29
|
+
"regex": "\\bprotocol\\s+([A-Z][A-Za-z0-9_]*)"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": "actor",
|
|
33
|
+
"kind": "actor",
|
|
34
|
+
"regex": "\\bactor\\s+([A-Z][A-Za-z0-9_]*)"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": "typealias",
|
|
38
|
+
"kind": "typealias",
|
|
39
|
+
"regex": "\\btypealias\\s+([A-Z][A-Za-z0-9_]*)"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"id": "func",
|
|
43
|
+
"kind": "func",
|
|
44
|
+
"regex": "\\bfunc\\s+([a-zA-Z_][A-Za-z0-9_]*)"
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"importPatterns": [
|
|
48
|
+
{
|
|
49
|
+
"id": "import",
|
|
50
|
+
"regex": "^\\s*import\\s+(?:class|struct|enum|protocol|func|var|let|typealias)\\s+([A-Za-z_][A-Za-z0-9_]*)|^\\s*import\\s+([A-Za-z_][A-Za-z0-9_]*)"
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"referenceKinds": ["class", "struct", "enum", "protocol", "actor", "typealias"],
|
|
54
|
+
"referenceKindsNote": "func is deliberately absent. A bare lowercase method name matched across files is almost never a call to that exact declaration: on a large Swift app `with`, `size`, `name`, `image` and `model` took the top god-node slots purely because each happened to be declared in exactly one file. Functions still reach the graph through their `defines` edge, so they stay findable by name.",
|
|
55
|
+
"ignoredIdentifiers": [
|
|
56
|
+
"self",
|
|
57
|
+
"Self",
|
|
58
|
+
"super",
|
|
59
|
+
"init",
|
|
60
|
+
"deinit",
|
|
61
|
+
"body",
|
|
62
|
+
"some",
|
|
63
|
+
"any",
|
|
64
|
+
"func",
|
|
65
|
+
"class",
|
|
66
|
+
"struct",
|
|
67
|
+
"enum",
|
|
68
|
+
"protocol",
|
|
69
|
+
"extension",
|
|
70
|
+
"var",
|
|
71
|
+
"let",
|
|
72
|
+
"return",
|
|
73
|
+
"guard",
|
|
74
|
+
"where",
|
|
75
|
+
"case",
|
|
76
|
+
"default",
|
|
77
|
+
"import",
|
|
78
|
+
"public",
|
|
79
|
+
"private",
|
|
80
|
+
"internal",
|
|
81
|
+
"fileprivate",
|
|
82
|
+
"open",
|
|
83
|
+
"static",
|
|
84
|
+
"final",
|
|
85
|
+
"override",
|
|
86
|
+
"throws",
|
|
87
|
+
"async",
|
|
88
|
+
"await",
|
|
89
|
+
"try",
|
|
90
|
+
"nil",
|
|
91
|
+
"true",
|
|
92
|
+
"false"
|
|
93
|
+
],
|
|
94
|
+
"note": "extension is intentionally NOT a definition pattern. `extension Foo` would register a second symbol named Foo alongside its declaring file, the name would become ambiguous, and buildGraph drops ambiguous references - so every reference to an extended type would vanish. Extensions still reach the graph as reference edges through the identifier pass."
|
|
95
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"stack": "node",
|
|
4
|
+
"description": "TypeScript + JavaScript code-graph rules. Adds what the graph needs on top of test-gap-rules/node.json, which already owns sourceExtensions, excludePathGlobs and the test-path predicates. Definition patterns here are deliberately broader than the test-gap publicApiPatterns: the graph wants unexported declarations too, because Phase 1 narrows scope on the whole module, not just its public surface.",
|
|
5
|
+
"comments": {
|
|
6
|
+
"line": [
|
|
7
|
+
"//"
|
|
8
|
+
],
|
|
9
|
+
"block": [
|
|
10
|
+
[
|
|
11
|
+
"/*",
|
|
12
|
+
"*/"
|
|
13
|
+
]
|
|
14
|
+
],
|
|
15
|
+
"string": [
|
|
16
|
+
"`",
|
|
17
|
+
"\"",
|
|
18
|
+
"'"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"definitionPatterns": [
|
|
22
|
+
{
|
|
23
|
+
"id": "class",
|
|
24
|
+
"kind": "class",
|
|
25
|
+
"regex": "\\bclass\\s+([A-Za-z_$][A-Za-z0-9_$]*)"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "interface",
|
|
29
|
+
"kind": "interface",
|
|
30
|
+
"regex": "\\binterface\\s+([A-Za-z_$][A-Za-z0-9_$]*)"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "type",
|
|
34
|
+
"kind": "type",
|
|
35
|
+
"regex": "\\btype\\s+([A-Za-z_$][A-Za-z0-9_$]*)\\s*(?:<[^=>]*>\\s*)?="
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "enum",
|
|
39
|
+
"kind": "enum",
|
|
40
|
+
"regex": "\\benum\\s+([A-Za-z_$][A-Za-z0-9_$]*)"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"id": "const",
|
|
44
|
+
"kind": "const",
|
|
45
|
+
"regex": "^(?:export\\s+)?(?:declare\\s+)?const\\s+([A-Z][A-Za-z0-9_$]*)\\s*[:=]"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": "function",
|
|
49
|
+
"kind": "function",
|
|
50
|
+
"regex": "\\bfunction\\s*\\*?\\s+([A-Za-z_$][A-Za-z0-9_$]*)"
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"importPatterns": [
|
|
54
|
+
{
|
|
55
|
+
"id": "specifier",
|
|
56
|
+
"regex": "(?:\\bfrom|\\brequire\\s*\\(|\\bimport)\\s*\\(?\\s*[\"'](?:[^\"']*[\\/:])?([A-Za-z_$][A-Za-z0-9_$-]*)(?:\\.[A-Za-z0-9]+)*[\"']"
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
"referenceKinds": [
|
|
60
|
+
"class",
|
|
61
|
+
"interface",
|
|
62
|
+
"type",
|
|
63
|
+
"enum",
|
|
64
|
+
"const"
|
|
65
|
+
],
|
|
66
|
+
"referenceKindsNote": "function is deliberately absent, for the reason Swift's rules record for func: a bare name matched across files is almost never a call to that exact declaration, and the generic ones (run, main, get, parse, handler) would take the top god-node slots purely because each happened to be declared once. Functions still reach the graph through their defines edge, so they stay findable by name. const is present but narrowed to an uppercase-initial declaration at column 0, which is what a component, a singleton or an exported table looks like; a lowercase const is a local and is not a declaration anyone references across files. Measured on this repo (126 sources): including function put `ok` at degree 52, `f` at 38, `tokens` at 15 and `arg` at 12 - local helper names that happen to be declared exactly once, so the ambiguity rule does not catch them either. Excluding it leaves a thin symbol layer, and that is the honest shape of this stack: a JavaScript module's exported unit is usually a function, so the useful graph here is the import graph between files. `graph-affected \"<file>.mjs\"` answers who imports a module, which is the question this stack actually gets asked.",
|
|
67
|
+
"ignoredIdentifiers": [
|
|
68
|
+
"this",
|
|
69
|
+
"super",
|
|
70
|
+
"constructor",
|
|
71
|
+
"prototype",
|
|
72
|
+
"arguments",
|
|
73
|
+
"class",
|
|
74
|
+
"interface",
|
|
75
|
+
"type",
|
|
76
|
+
"enum",
|
|
77
|
+
"function",
|
|
78
|
+
"const",
|
|
79
|
+
"let",
|
|
80
|
+
"var",
|
|
81
|
+
"return",
|
|
82
|
+
"import",
|
|
83
|
+
"export",
|
|
84
|
+
"default",
|
|
85
|
+
"from",
|
|
86
|
+
"as",
|
|
87
|
+
"in",
|
|
88
|
+
"of",
|
|
89
|
+
"new",
|
|
90
|
+
"delete",
|
|
91
|
+
"typeof",
|
|
92
|
+
"instanceof",
|
|
93
|
+
"void",
|
|
94
|
+
"await",
|
|
95
|
+
"async",
|
|
96
|
+
"yield",
|
|
97
|
+
"if",
|
|
98
|
+
"else",
|
|
99
|
+
"switch",
|
|
100
|
+
"case",
|
|
101
|
+
"for",
|
|
102
|
+
"while",
|
|
103
|
+
"do",
|
|
104
|
+
"break",
|
|
105
|
+
"continue",
|
|
106
|
+
"try",
|
|
107
|
+
"catch",
|
|
108
|
+
"finally",
|
|
109
|
+
"throw",
|
|
110
|
+
"extends",
|
|
111
|
+
"implements",
|
|
112
|
+
"static",
|
|
113
|
+
"public",
|
|
114
|
+
"private",
|
|
115
|
+
"protected",
|
|
116
|
+
"readonly",
|
|
117
|
+
"declare",
|
|
118
|
+
"namespace",
|
|
119
|
+
"module",
|
|
120
|
+
"require",
|
|
121
|
+
"null",
|
|
122
|
+
"undefined",
|
|
123
|
+
"true",
|
|
124
|
+
"false",
|
|
125
|
+
"string",
|
|
126
|
+
"number",
|
|
127
|
+
"boolean",
|
|
128
|
+
"object",
|
|
129
|
+
"any",
|
|
130
|
+
"unknown",
|
|
131
|
+
"never",
|
|
132
|
+
"console",
|
|
133
|
+
"process",
|
|
134
|
+
"window",
|
|
135
|
+
"document",
|
|
136
|
+
"Promise",
|
|
137
|
+
"Array",
|
|
138
|
+
"Object",
|
|
139
|
+
"String",
|
|
140
|
+
"Number",
|
|
141
|
+
"Boolean",
|
|
142
|
+
"Error",
|
|
143
|
+
"JSON",
|
|
144
|
+
"Math",
|
|
145
|
+
"Date",
|
|
146
|
+
"Map",
|
|
147
|
+
"Set"
|
|
148
|
+
],
|
|
149
|
+
"note": "A JavaScript module specifier IS a string literal, so `importSpecifiersAreStrings` tells the engine to run the import patterns over a text with comments stripped but strings intact. Without it every import edge in this stack would vanish silently, because the specifier is blanked before the pattern ever sees it. Only the import pass reads that text; definitions and the identifier set still come from the fully stripped body, so a name that appears only inside a string still produces no reference edge. Template literals are stripped whole, interpolations included: it is better to lose a real mention than to invent one from a string. The single specifier pattern captures the last path segment with any extension chain removed, which is what the engine resolves against a file basename: ./_retrieval.mjs and ../a/b/util and node:fs and @scope/pkg all reduce to one name.",
|
|
150
|
+
"importSpecifiersAreStrings": true
|
|
151
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"stack": "python",
|
|
4
|
+
"description": "Python code-graph rules. Adds what the graph needs on top of test-gap-rules/python.json, which already owns sourceExtensions, excludePathGlobs and the test-path predicates. Definition patterns here are deliberately broader than the test-gap publicApiPatterns: the graph wants private declarations too, because Phase 1 narrows scope on the whole package, not just its public surface.",
|
|
5
|
+
"comments": {
|
|
6
|
+
"line": ["#"],
|
|
7
|
+
"block": [],
|
|
8
|
+
"string": ["\"\"\"", "'''", "\"", "'"]
|
|
9
|
+
},
|
|
10
|
+
"definitionPatterns": [
|
|
11
|
+
{
|
|
12
|
+
"id": "class",
|
|
13
|
+
"kind": "class",
|
|
14
|
+
"regex": "\\bclass\\s+([A-Za-z_][A-Za-z0-9_]*)"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": "def",
|
|
18
|
+
"kind": "def",
|
|
19
|
+
"regex": "\\bdef\\s+([A-Za-z_][A-Za-z0-9_]*)"
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"importPatterns": [
|
|
23
|
+
{
|
|
24
|
+
"id": "from-import",
|
|
25
|
+
"regex": "^\\s*from\\s+[.\\w]*?([A-Za-z_][A-Za-z0-9_]*)\\s+import\\b"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "import",
|
|
29
|
+
"regex": "^\\s*import\\s+(?:[A-Za-z_][A-Za-z0-9_]*\\.)*([A-Za-z_][A-Za-z0-9_]*)"
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"referenceKinds": ["class"],
|
|
33
|
+
"referenceKindsNote": "def is deliberately absent, for the reason Swift's rules record for func and the node rules record with measurements: a bare lowercase name matched across files is almost never a call to that exact declaration, and Python's convention of short module-level helpers (main, run, load, parse, setup) makes it worse than most. Functions still reach the graph through their defines edge, so they stay findable by name.",
|
|
34
|
+
"ignoredIdentifiers": [
|
|
35
|
+
"self",
|
|
36
|
+
"cls",
|
|
37
|
+
"class",
|
|
38
|
+
"def",
|
|
39
|
+
"return",
|
|
40
|
+
"import",
|
|
41
|
+
"from",
|
|
42
|
+
"as",
|
|
43
|
+
"in",
|
|
44
|
+
"is",
|
|
45
|
+
"not",
|
|
46
|
+
"and",
|
|
47
|
+
"or",
|
|
48
|
+
"if",
|
|
49
|
+
"elif",
|
|
50
|
+
"else",
|
|
51
|
+
"for",
|
|
52
|
+
"while",
|
|
53
|
+
"break",
|
|
54
|
+
"continue",
|
|
55
|
+
"pass",
|
|
56
|
+
"try",
|
|
57
|
+
"except",
|
|
58
|
+
"finally",
|
|
59
|
+
"raise",
|
|
60
|
+
"with",
|
|
61
|
+
"yield",
|
|
62
|
+
"lambda",
|
|
63
|
+
"global",
|
|
64
|
+
"nonlocal",
|
|
65
|
+
"assert",
|
|
66
|
+
"del",
|
|
67
|
+
"async",
|
|
68
|
+
"await",
|
|
69
|
+
"None",
|
|
70
|
+
"True",
|
|
71
|
+
"False",
|
|
72
|
+
"print",
|
|
73
|
+
"len",
|
|
74
|
+
"str",
|
|
75
|
+
"int",
|
|
76
|
+
"float",
|
|
77
|
+
"bool",
|
|
78
|
+
"list",
|
|
79
|
+
"dict",
|
|
80
|
+
"set",
|
|
81
|
+
"tuple",
|
|
82
|
+
"type",
|
|
83
|
+
"range",
|
|
84
|
+
"open",
|
|
85
|
+
"super",
|
|
86
|
+
"property",
|
|
87
|
+
"staticmethod",
|
|
88
|
+
"classmethod"
|
|
89
|
+
],
|
|
90
|
+
"note": "There is no block-comment token: a Python triple-quoted docstring is a string literal, and it is listed under `string` so the stripper removes it the same way it removes any other string. Longest tokens come first there, so \"\"\" and ''' are consumed whole before the single-quote forms get a chance to match their first character. Both import patterns capture the LAST dotted segment, which is what the engine resolves against a file basename: `from package.module import Thing` points at module.py, and `import package.module` does the same. `from . import x` and `from .. import x` capture nothing, deliberately - a bare relative package names a directory, and pointing it at an arbitrary file inside would invent an edge nobody wrote."
|
|
91
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file graph-affected.mjs - reverse traversal: what breaks if this changes.
|
|
5
|
+
*
|
|
6
|
+
* Phase 1's impact surface, feeding `analysis.touchedAreas[]`. Where
|
|
7
|
+
* graph-query.mjs answers "where does this live", this answers "who depends on
|
|
8
|
+
* it": it walks edges backwards from a named symbol, so the result is the blast
|
|
9
|
+
* radius of a change rather than its neighbourhood.
|
|
10
|
+
*
|
|
11
|
+
* Inputs:
|
|
12
|
+
* "<symbol>" Symbol name, exact or case-insensitive. Required.
|
|
13
|
+
* --graph <path> Graph file. Default: ~/.claude/knowledge/<cwd name>/code-graph.json
|
|
14
|
+
* --depth N Reverse traversal depth. Default: 2
|
|
15
|
+
* --kind K Restrict to one edge kind (repeatable via comma)
|
|
16
|
+
* --json Emit JSON instead of text
|
|
17
|
+
*
|
|
18
|
+
* Exit codes:
|
|
19
|
+
* 0 - answered (possibly empty)
|
|
20
|
+
* 1 - graph missing or unreadable
|
|
21
|
+
* 64 - usage error
|
|
22
|
+
*
|
|
23
|
+
* @module pipeline/scripts/graph-affected
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { resolve } from "node:path";
|
|
27
|
+
import { parseFlags } from "./graph-build.mjs";
|
|
28
|
+
import { loadGraph, defaultGraphPath } from "./graph-query.mjs";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Nodes whose name matches, preferring an exact hit.
|
|
32
|
+
*
|
|
33
|
+
* A case-insensitive fallback keeps the command usable from a task description,
|
|
34
|
+
* where the symbol is often typed in prose casing.
|
|
35
|
+
*
|
|
36
|
+
* @param {object} graph
|
|
37
|
+
* @param {string} name
|
|
38
|
+
* @returns {object[]}
|
|
39
|
+
*/
|
|
40
|
+
export function findByName(graph, name) {
|
|
41
|
+
const exact = graph.nodes.filter((n) => n.name === name);
|
|
42
|
+
if (exact.length) return exact;
|
|
43
|
+
const lower = name.toLowerCase();
|
|
44
|
+
return graph.nodes.filter((n) => (n.name || "").toLowerCase() === lower);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Incoming adjacency only.
|
|
49
|
+
*
|
|
50
|
+
* @param {object} graph
|
|
51
|
+
* @param {Set<string>|null} kinds
|
|
52
|
+
* @returns {Map<string, Array<{id: string, kind: string}>>}
|
|
53
|
+
*/
|
|
54
|
+
export function incoming(graph, kinds = null) {
|
|
55
|
+
const adj = new Map();
|
|
56
|
+
for (const e of graph.edges) {
|
|
57
|
+
if (kinds && !kinds.has(e.kind)) continue;
|
|
58
|
+
if (!adj.has(e.to)) adj.set(e.to, []);
|
|
59
|
+
adj.get(e.to).push({ id: e.from, kind: e.kind });
|
|
60
|
+
}
|
|
61
|
+
return adj;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Walk backwards from each seed.
|
|
66
|
+
*
|
|
67
|
+
* @param {object} params
|
|
68
|
+
* @returns {Array<{id: string, dist: number, kind: string, node: object}>}
|
|
69
|
+
*/
|
|
70
|
+
export function affected({ graph, seeds, depth, kinds }) {
|
|
71
|
+
const byId = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
72
|
+
const adj = incoming(graph, kinds);
|
|
73
|
+
const seen = new Set(seeds.map((n) => n.id));
|
|
74
|
+
const out = [];
|
|
75
|
+
const queue = seeds.map((n) => ({ id: n.id, dist: 0, kind: "seed" }));
|
|
76
|
+
|
|
77
|
+
while (queue.length) {
|
|
78
|
+
const cur = queue.shift();
|
|
79
|
+
if (cur.dist > 0) out.push({ ...cur, node: byId.get(cur.id) });
|
|
80
|
+
if (cur.dist >= depth) continue;
|
|
81
|
+
for (const dep of adj.get(cur.id) || []) {
|
|
82
|
+
if (seen.has(dep.id)) continue;
|
|
83
|
+
seen.add(dep.id);
|
|
84
|
+
queue.push({ id: dep.id, dist: cur.dist + 1, kind: dep.kind });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function main() {
|
|
91
|
+
const { flags, positional } = parseFlags(process.argv.slice(2));
|
|
92
|
+
const name = positional.join(" ").trim();
|
|
93
|
+
if (!name) {
|
|
94
|
+
process.stderr.write(
|
|
95
|
+
'graph-affected: a symbol name is required, e.g. graph-affected "Flight"\n',
|
|
96
|
+
);
|
|
97
|
+
process.exit(64);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const path = flags.graph && flags.graph !== true ? resolve(flags.graph) : defaultGraphPath();
|
|
101
|
+
let graph;
|
|
102
|
+
try {
|
|
103
|
+
graph = loadGraph(path);
|
|
104
|
+
} catch (e) {
|
|
105
|
+
process.stderr.write(`graph-affected: ${e.message}\n`);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const depth = Number(flags.depth) > 0 ? Number(flags.depth) : 2;
|
|
110
|
+
const kinds = flags.kind && flags.kind !== true ? new Set(String(flags.kind).split(",")) : null;
|
|
111
|
+
|
|
112
|
+
const seeds = findByName(graph, name);
|
|
113
|
+
if (seeds.length === 0) {
|
|
114
|
+
if (flags.json === true) {
|
|
115
|
+
process.stdout.write(
|
|
116
|
+
JSON.stringify({ symbol: name, seeds: [], affected: [] }, null, 2) + "\n",
|
|
117
|
+
);
|
|
118
|
+
} else {
|
|
119
|
+
process.stdout.write(`no node named '${name}'\n`);
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const hits = affected({ graph, seeds, depth, kinds });
|
|
125
|
+
|
|
126
|
+
if (flags.json === true) {
|
|
127
|
+
process.stdout.write(
|
|
128
|
+
JSON.stringify(
|
|
129
|
+
{
|
|
130
|
+
symbol: name,
|
|
131
|
+
seeds: seeds.map((s) => s.id),
|
|
132
|
+
affected: hits.map((h) => ({
|
|
133
|
+
id: h.id,
|
|
134
|
+
dist: h.dist,
|
|
135
|
+
edgeKind: h.kind,
|
|
136
|
+
path: h.node?.path || null,
|
|
137
|
+
})),
|
|
138
|
+
},
|
|
139
|
+
null,
|
|
140
|
+
2,
|
|
141
|
+
) + "\n",
|
|
142
|
+
);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
process.stdout.write(`# affected by ${name}\n\n`);
|
|
147
|
+
for (const seed of seeds) {
|
|
148
|
+
process.stdout.write(`* ${seed.name} (${seed.symbolKind || seed.kind}) ${seed.path || "-"}\n`);
|
|
149
|
+
}
|
|
150
|
+
process.stdout.write("\n");
|
|
151
|
+
const files = new Set();
|
|
152
|
+
for (const hit of hits) {
|
|
153
|
+
if (hit.node?.path) files.add(hit.node.path);
|
|
154
|
+
process.stdout.write(
|
|
155
|
+
`${" ".repeat(hit.dist)}- ${hit.node?.name || hit.id} [${hit.kind}] ${hit.node?.path || "-"}\n`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
process.stdout.write(`\n${hits.length} dependents across ${files.size} files\n`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (process.argv[1] && process.argv[1].endsWith("graph-affected.mjs")) main();
|