gitnexus 1.6.3-rc.8 → 1.6.3
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/README.md +21 -5
- package/dist/_shared/graph/types.d.ts +16 -0
- package/dist/_shared/graph/types.d.ts.map +1 -1
- package/dist/_shared/index.d.ts +20 -2
- package/dist/_shared/index.d.ts.map +1 -1
- package/dist/_shared/index.js +11 -0
- package/dist/_shared/index.js.map +1 -1
- package/dist/_shared/scope-resolution/def-index.js +2 -2
- package/dist/_shared/scope-resolution/def-index.js.map +1 -1
- package/dist/_shared/scope-resolution/method-dispatch-index.d.ts +8 -0
- package/dist/_shared/scope-resolution/method-dispatch-index.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/method-dispatch-index.js +2 -2
- package/dist/_shared/scope-resolution/method-dispatch-index.js.map +1 -1
- package/dist/_shared/scope-resolution/module-scope-index.d.ts +8 -0
- package/dist/_shared/scope-resolution/module-scope-index.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/module-scope-index.js +10 -2
- package/dist/_shared/scope-resolution/module-scope-index.js.map +1 -1
- package/dist/_shared/scope-resolution/parsed-file.d.ts +76 -0
- package/dist/_shared/scope-resolution/parsed-file.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/parsed-file.js +54 -0
- package/dist/_shared/scope-resolution/parsed-file.js.map +1 -0
- package/dist/_shared/scope-resolution/position-index.d.ts +12 -0
- package/dist/_shared/scope-resolution/position-index.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/position-index.js +2 -2
- package/dist/_shared/scope-resolution/position-index.js.map +1 -1
- package/dist/_shared/scope-resolution/qualified-name-index.js +2 -2
- package/dist/_shared/scope-resolution/qualified-name-index.js.map +1 -1
- package/dist/_shared/scope-resolution/reference-site.d.ts +75 -0
- package/dist/_shared/scope-resolution/reference-site.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/reference-site.js +24 -0
- package/dist/_shared/scope-resolution/reference-site.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/class-registry.d.ts +27 -0
- package/dist/_shared/scope-resolution/registries/class-registry.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/class-registry.js +30 -0
- package/dist/_shared/scope-resolution/registries/class-registry.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/context.d.ts +69 -0
- package/dist/_shared/scope-resolution/registries/context.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/context.js +44 -0
- package/dist/_shared/scope-resolution/registries/context.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/evidence.d.ts +56 -0
- package/dist/_shared/scope-resolution/registries/evidence.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/evidence.js +150 -0
- package/dist/_shared/scope-resolution/registries/evidence.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/field-registry.d.ts +26 -0
- package/dist/_shared/scope-resolution/registries/field-registry.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/field-registry.js +31 -0
- package/dist/_shared/scope-resolution/registries/field-registry.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/lookup-core.d.ts +81 -0
- package/dist/_shared/scope-resolution/registries/lookup-core.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/lookup-core.js +332 -0
- package/dist/_shared/scope-resolution/registries/lookup-core.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/lookup-qualified.d.ts +33 -0
- package/dist/_shared/scope-resolution/registries/lookup-qualified.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/lookup-qualified.js +56 -0
- package/dist/_shared/scope-resolution/registries/lookup-qualified.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/method-registry.d.ts +36 -0
- package/dist/_shared/scope-resolution/registries/method-registry.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/method-registry.js +32 -0
- package/dist/_shared/scope-resolution/registries/method-registry.js.map +1 -0
- package/dist/_shared/scope-resolution/registries/tie-breaks.d.ts +43 -0
- package/dist/_shared/scope-resolution/registries/tie-breaks.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/registries/tie-breaks.js +60 -0
- package/dist/_shared/scope-resolution/registries/tie-breaks.js.map +1 -0
- package/dist/_shared/scope-resolution/resolve-type-ref.d.ts +1 -10
- package/dist/_shared/scope-resolution/resolve-type-ref.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/resolve-type-ref.js +6 -0
- package/dist/_shared/scope-resolution/resolve-type-ref.js.map +1 -1
- package/dist/_shared/scope-resolution/scope-tree.d.ts +4 -4
- package/dist/_shared/scope-resolution/scope-tree.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/scope-tree.js +3 -2
- package/dist/_shared/scope-resolution/scope-tree.js.map +1 -1
- package/dist/_shared/scope-resolution/shadow/aggregate.d.ts +6 -2
- package/dist/_shared/scope-resolution/shadow/aggregate.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/shadow/aggregate.js +5 -0
- package/dist/_shared/scope-resolution/shadow/aggregate.js.map +1 -1
- package/dist/_shared/scope-resolution/types.d.ts +11 -0
- package/dist/_shared/scope-resolution/types.d.ts.map +1 -1
- package/dist/cli/ai-context.js +35 -4
- package/dist/cli/analyze.d.ts +27 -0
- package/dist/cli/analyze.js +31 -1
- package/dist/cli/clean.js +19 -1
- package/dist/cli/group.js +73 -0
- package/dist/cli/index-repo.js +8 -1
- package/dist/cli/index.js +26 -1
- package/dist/cli/list.js +11 -1
- package/dist/cli/remove.d.ts +30 -0
- package/dist/cli/remove.js +99 -0
- package/dist/cli/setup.js +185 -57
- package/dist/cli/tool.d.ts +5 -0
- package/dist/cli/tool.js +42 -0
- package/dist/config/ignore-service.d.ts +9 -0
- package/dist/config/ignore-service.js +80 -13
- package/dist/core/embedding-mode.d.ts +30 -0
- package/dist/core/embedding-mode.js +30 -0
- package/dist/core/embeddings/ast-utils.js +22 -22
- package/dist/core/embeddings/chunker.js +30 -25
- package/dist/core/embeddings/embedding-pipeline.d.ts +6 -0
- package/dist/core/embeddings/embedding-pipeline.js +15 -6
- package/dist/core/embeddings/text-generator.d.ts +1 -1
- package/dist/core/embeddings/text-generator.js +33 -24
- package/dist/core/embeddings/types.d.ts +43 -1
- package/dist/core/embeddings/types.js +101 -29
- package/dist/core/git-staleness.d.ts +18 -0
- package/dist/core/git-staleness.js +108 -0
- package/dist/core/graph/graph.js +115 -20
- package/dist/core/graph/types.d.ts +12 -1
- package/dist/core/group/config-parser.d.ts +4 -0
- package/dist/core/group/config-parser.js +18 -1
- package/dist/core/group/cross-impact.d.ts +41 -0
- package/dist/core/group/cross-impact.js +441 -0
- package/dist/core/group/extractors/http-patterns/php.js +126 -18
- package/dist/core/group/group-path-utils.d.ts +17 -0
- package/dist/core/group/group-path-utils.js +40 -0
- package/dist/core/group/resolve-at-member.d.ts +10 -0
- package/dist/core/group/resolve-at-member.js +31 -0
- package/dist/core/group/service.d.ts +9 -0
- package/dist/core/group/service.js +259 -25
- package/dist/core/group/types.d.ts +30 -0
- package/dist/core/ingestion/ast-cache.d.ts +16 -1
- package/dist/core/ingestion/ast-cache.js +14 -2
- package/dist/core/ingestion/call-processor.js +9 -0
- package/dist/core/ingestion/emit-references.d.ts +88 -0
- package/dist/core/ingestion/emit-references.js +229 -0
- package/dist/core/ingestion/filesystem-walker.js +6 -4
- package/dist/core/ingestion/finalize-orchestrator.d.ts +63 -0
- package/dist/core/ingestion/finalize-orchestrator.js +139 -0
- package/dist/core/ingestion/framework-detection.js +6 -2
- package/dist/core/ingestion/import-processor.js +4 -0
- package/dist/core/ingestion/import-resolvers/python.js +9 -6
- package/dist/core/ingestion/import-target-adapter.d.ts +73 -0
- package/dist/core/ingestion/import-target-adapter.js +95 -0
- package/dist/core/ingestion/language-provider.d.ts +36 -33
- package/dist/core/ingestion/languages/csharp/accessor-unwrap.d.ts +21 -0
- package/dist/core/ingestion/languages/csharp/accessor-unwrap.js +56 -0
- package/dist/core/ingestion/languages/csharp/arity-metadata.d.ts +26 -0
- package/dist/core/ingestion/languages/csharp/arity-metadata.js +46 -0
- package/dist/core/ingestion/languages/csharp/arity.d.ts +23 -0
- package/dist/core/ingestion/languages/csharp/arity.js +37 -0
- package/dist/core/ingestion/languages/csharp/cache-stats.d.ts +15 -0
- package/dist/core/ingestion/languages/csharp/cache-stats.js +26 -0
- package/dist/core/ingestion/languages/csharp/captures.d.ts +19 -0
- package/dist/core/ingestion/languages/csharp/captures.js +249 -0
- package/dist/core/ingestion/languages/csharp/import-decomposer.d.ts +19 -0
- package/dist/core/ingestion/languages/csharp/import-decomposer.js +93 -0
- package/dist/core/ingestion/languages/csharp/import-target.d.ts +25 -0
- package/dist/core/ingestion/languages/csharp/import-target.js +123 -0
- package/dist/core/ingestion/languages/csharp/index.d.ts +82 -0
- package/dist/core/ingestion/languages/csharp/index.js +82 -0
- package/dist/core/ingestion/languages/csharp/interpret.d.ts +15 -0
- package/dist/core/ingestion/languages/csharp/interpret.js +132 -0
- package/dist/core/ingestion/languages/csharp/merge-bindings.d.ts +27 -0
- package/dist/core/ingestion/languages/csharp/merge-bindings.js +55 -0
- package/dist/core/ingestion/languages/csharp/namespace-siblings.d.ts +50 -0
- package/dist/core/ingestion/languages/csharp/namespace-siblings.js +374 -0
- package/dist/core/ingestion/languages/csharp/query.d.ts +35 -0
- package/dist/core/ingestion/languages/csharp/query.js +515 -0
- package/dist/core/ingestion/languages/csharp/receiver-binding.d.ts +31 -0
- package/dist/core/ingestion/languages/csharp/receiver-binding.js +135 -0
- package/dist/core/ingestion/languages/csharp/scope-resolver.d.ts +10 -0
- package/dist/core/ingestion/languages/csharp/scope-resolver.js +63 -0
- package/dist/core/ingestion/languages/csharp/simple-hooks.d.ts +53 -0
- package/dist/core/ingestion/languages/csharp/simple-hooks.js +76 -0
- package/dist/core/ingestion/languages/csharp.js +14 -0
- package/dist/core/ingestion/languages/python/arity-metadata.d.ts +24 -0
- package/dist/core/ingestion/languages/python/arity-metadata.js +45 -0
- package/dist/core/ingestion/languages/python/arity.d.ts +22 -0
- package/dist/core/ingestion/languages/python/arity.js +38 -0
- package/dist/core/ingestion/languages/python/cache-stats.d.ts +17 -0
- package/dist/core/ingestion/languages/python/cache-stats.js +28 -0
- package/dist/core/ingestion/languages/python/captures.d.ts +19 -0
- package/dist/core/ingestion/languages/python/captures.js +106 -0
- package/dist/core/ingestion/languages/python/import-decomposer.d.ts +15 -0
- package/dist/core/ingestion/languages/python/import-decomposer.js +112 -0
- package/dist/core/ingestion/languages/python/import-target.d.ts +21 -0
- package/dist/core/ingestion/languages/python/import-target.js +99 -0
- package/dist/core/ingestion/languages/python/index.d.ts +80 -0
- package/dist/core/ingestion/languages/python/index.js +80 -0
- package/dist/core/ingestion/languages/python/interpret.d.ts +15 -0
- package/dist/core/ingestion/languages/python/interpret.js +191 -0
- package/dist/core/ingestion/languages/python/merge-bindings.d.ts +16 -0
- package/dist/core/ingestion/languages/python/merge-bindings.js +44 -0
- package/dist/core/ingestion/languages/python/query.d.ts +9 -0
- package/dist/core/ingestion/languages/python/query.js +267 -0
- package/dist/core/ingestion/languages/python/receiver-binding.d.ts +21 -0
- package/dist/core/ingestion/languages/python/receiver-binding.js +116 -0
- package/dist/core/ingestion/languages/python/scope-resolver.d.ts +16 -0
- package/dist/core/ingestion/languages/python/scope-resolver.js +53 -0
- package/dist/core/ingestion/languages/python/simple-hooks.d.ts +23 -0
- package/dist/core/ingestion/languages/python/simple-hooks.js +35 -0
- package/dist/core/ingestion/languages/python.js +14 -0
- package/dist/core/ingestion/model/method-registry.d.ts +9 -0
- package/dist/core/ingestion/model/method-registry.js +4 -0
- package/dist/core/ingestion/model/scope-resolution-indexes.d.ts +59 -0
- package/dist/core/ingestion/model/scope-resolution-indexes.js +42 -0
- package/dist/core/ingestion/model/semantic-model.d.ts +64 -0
- package/dist/core/ingestion/model/semantic-model.js +55 -0
- package/dist/core/ingestion/mro-processor.js +38 -22
- package/dist/core/ingestion/parsing-processor.d.ts +18 -1
- package/dist/core/ingestion/parsing-processor.js +45 -11
- package/dist/core/ingestion/pipeline-phases/index.d.ts +1 -0
- package/dist/core/ingestion/pipeline-phases/index.js +1 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +10 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +17 -2
- package/dist/core/ingestion/pipeline-phases/parse.d.ts +18 -0
- package/dist/core/ingestion/pipeline.js +2 -1
- package/dist/core/ingestion/registry-primary-flag.d.ts +86 -0
- package/dist/core/ingestion/registry-primary-flag.js +111 -0
- package/dist/core/ingestion/resolve-references.d.ts +63 -0
- package/dist/core/ingestion/resolve-references.js +175 -0
- package/dist/core/ingestion/scope-extractor-bridge.d.ts +32 -0
- package/dist/core/ingestion/scope-extractor-bridge.js +44 -0
- package/dist/core/ingestion/scope-extractor.d.ts +86 -0
- package/dist/core/ingestion/scope-extractor.js +758 -0
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +372 -0
- package/dist/core/ingestion/scope-resolution/contract/scope-resolver.js +212 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/edges.d.ts +43 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/edges.js +79 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/ids.d.ts +57 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/ids.js +112 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/imports-to-edges.d.ts +17 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/imports-to-edges.js +46 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/method-dispatch.d.ts +19 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/method-dispatch.js +30 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.d.ts +37 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.js +113 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.d.ts +38 -0
- package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.js +73 -0
- package/dist/core/ingestion/scope-resolution/passes/compound-receiver.d.ts +42 -0
- package/dist/core/ingestion/scope-resolution/passes/compound-receiver.js +198 -0
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +27 -0
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +131 -0
- package/dist/core/ingestion/scope-resolution/passes/imported-return-types.d.ts +48 -0
- package/dist/core/ingestion/scope-resolution/passes/imported-return-types.js +130 -0
- package/dist/core/ingestion/scope-resolution/passes/mro.d.ts +42 -0
- package/dist/core/ingestion/scope-resolution/passes/mro.js +99 -0
- package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.d.ts +26 -0
- package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.js +61 -0
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.d.ts +46 -0
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +327 -0
- package/dist/core/ingestion/scope-resolution/pipeline/phase.d.ts +47 -0
- package/dist/core/ingestion/scope-resolution/pipeline/phase.js +130 -0
- package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.d.ts +68 -0
- package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.js +125 -0
- package/dist/core/ingestion/scope-resolution/pipeline/registry.d.ts +17 -0
- package/dist/core/ingestion/scope-resolution/pipeline/registry.js +21 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +66 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +157 -0
- package/dist/core/ingestion/scope-resolution/scope/namespace-targets.d.ts +36 -0
- package/dist/core/ingestion/scope-resolution/scope/namespace-targets.js +52 -0
- package/dist/core/ingestion/scope-resolution/scope/walkers.d.ts +127 -0
- package/dist/core/ingestion/scope-resolution/scope/walkers.js +349 -0
- package/dist/core/ingestion/scope-resolution/workspace-index.d.ts +52 -0
- package/dist/core/ingestion/scope-resolution/workspace-index.js +61 -0
- package/dist/core/ingestion/shadow-harness.d.ts +113 -0
- package/dist/core/ingestion/shadow-harness.js +148 -0
- package/dist/core/ingestion/utils/ast-helpers.d.ts +19 -1
- package/dist/core/ingestion/utils/ast-helpers.js +70 -0
- package/dist/core/ingestion/utils/max-file-size.d.ts +20 -0
- package/dist/core/ingestion/utils/max-file-size.js +52 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +9 -0
- package/dist/core/ingestion/workers/parse-worker.js +57 -21
- package/dist/core/lbug/lbug-adapter.d.ts +22 -2
- package/dist/core/lbug/lbug-adapter.js +58 -14
- package/dist/core/lbug/pool-adapter.d.ts +17 -0
- package/dist/core/lbug/pool-adapter.js +24 -14
- package/dist/core/run-analyze.d.ts +32 -0
- package/dist/core/run-analyze.js +74 -19
- package/dist/core/search/bm25-index.d.ts +18 -0
- package/dist/core/search/bm25-index.js +125 -12
- package/dist/core/tree-sitter/parser-loader.js +6 -1
- package/dist/mcp/local/local-backend.d.ts +67 -3
- package/dist/mcp/local/local-backend.js +296 -34
- package/dist/mcp/resources.d.ts +31 -0
- package/dist/mcp/resources.js +100 -17
- package/dist/mcp/tools.d.ts +4 -1
- package/dist/mcp/tools.js +75 -54
- package/dist/server/api.js +6 -2
- package/dist/storage/git.d.ts +49 -0
- package/dist/storage/git.js +111 -0
- package/dist/storage/repo-manager.d.ts +246 -1
- package/dist/storage/repo-manager.js +391 -9
- package/package.json +7 -6
- package/scripts/bench-scope-resolution.ts +134 -0
- package/scripts/ci-list-migrated-languages.ts +24 -0
- package/skills/gitnexus-cli.md +1 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-repo impact (Phase 1 local walk + Phase 2 bridge fan-out).
|
|
3
|
+
* All bridge Cypher for this feature lives in this module.
|
|
4
|
+
*/
|
|
5
|
+
import fsp from 'node:fs/promises';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { GroupNotFoundError, loadGroupConfig } from './config-parser.js';
|
|
8
|
+
import { fileMatchesServicePrefix, normalizeServicePrefix, repoInSubgroup, } from './group-path-utils.js';
|
|
9
|
+
import { getGroupDir } from './storage.js';
|
|
10
|
+
import { closeBridgeDb, openBridgeDbReadOnly, queryBridge, readBridgeMeta } from './bridge-db.js';
|
|
11
|
+
import { BRIDGE_SCHEMA_VERSION } from './bridge-schema.js';
|
|
12
|
+
/** Cross-boundary hops beyond this value are clamped (multi-hop reserved for future work). */
|
|
13
|
+
export const MAX_SUPPORTED_CROSS_DEPTH = 1;
|
|
14
|
+
/** Default wall-clock budget for the Phase 1 `impact` leg when callers omit `timeoutMs`. */
|
|
15
|
+
export const DEFAULT_LOCAL_IMPACT_TIMEOUT_MS = 30_000;
|
|
16
|
+
const CY_NEIGHBORS_UPSTREAM = `
|
|
17
|
+
MATCH (consumer:Contract)-[l:ContractLink]->(provider:Contract)
|
|
18
|
+
WHERE provider.repo = $localRepo
|
|
19
|
+
AND provider.symbolUid IN $uids
|
|
20
|
+
AND provider.role = 'provider'
|
|
21
|
+
RETURN consumer.repo AS neighborRepo,
|
|
22
|
+
consumer.symbolUid AS neighborUid,
|
|
23
|
+
consumer.filePath AS neighborFilePath,
|
|
24
|
+
l.matchType AS matchType,
|
|
25
|
+
l.confidence AS confidence,
|
|
26
|
+
l.contractId AS contractId,
|
|
27
|
+
consumer.type AS contractType
|
|
28
|
+
`;
|
|
29
|
+
const CY_NEIGHBORS_DOWNSTREAM = `
|
|
30
|
+
MATCH (consumer:Contract)-[l:ContractLink]->(provider:Contract)
|
|
31
|
+
WHERE consumer.repo = $localRepo
|
|
32
|
+
AND consumer.symbolUid IN $uids
|
|
33
|
+
AND consumer.role = 'consumer'
|
|
34
|
+
RETURN provider.repo AS neighborRepo,
|
|
35
|
+
provider.symbolUid AS neighborUid,
|
|
36
|
+
provider.filePath AS neighborFilePath,
|
|
37
|
+
l.matchType AS matchType,
|
|
38
|
+
l.confidence AS confidence,
|
|
39
|
+
l.contractId AS contractId,
|
|
40
|
+
provider.type AS contractType
|
|
41
|
+
`;
|
|
42
|
+
function parseDirection(raw) {
|
|
43
|
+
if (raw === 'upstream' || raw === 'downstream')
|
|
44
|
+
return raw;
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
function clampCrossDepth(raw) {
|
|
48
|
+
const n = typeof raw === 'number' && Number.isFinite(raw) ? Math.floor(raw) : 1;
|
|
49
|
+
const d = n < 1 ? 1 : n;
|
|
50
|
+
if (d > MAX_SUPPORTED_CROSS_DEPTH) {
|
|
51
|
+
return {
|
|
52
|
+
depth: MAX_SUPPORTED_CROSS_DEPTH,
|
|
53
|
+
warning: `crossDepth was ${d}; multi-hop cross-boundary traversal beyond ${MAX_SUPPORTED_CROSS_DEPTH} is not implemented yet. Using crossDepth ${MAX_SUPPORTED_CROSS_DEPTH}.`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return { depth: d };
|
|
57
|
+
}
|
|
58
|
+
export function validateGroupImpactParams(params) {
|
|
59
|
+
const name = String(params.name ?? '').trim();
|
|
60
|
+
const repoPath = String(params.repo ?? '').trim();
|
|
61
|
+
const target = String(params.target ?? '').trim();
|
|
62
|
+
if (!name)
|
|
63
|
+
return { ok: false, error: 'name is required' };
|
|
64
|
+
if (!repoPath)
|
|
65
|
+
return { ok: false, error: 'repo is required (group repo path, e.g. app/backend)' };
|
|
66
|
+
if (!target)
|
|
67
|
+
return { ok: false, error: 'target is required' };
|
|
68
|
+
if (params.service !== undefined &&
|
|
69
|
+
params.service !== null &&
|
|
70
|
+
String(params.service).trim() === '') {
|
|
71
|
+
return { ok: false, error: 'service must not be an empty string' };
|
|
72
|
+
}
|
|
73
|
+
const direction = parseDirection(params.direction);
|
|
74
|
+
if (!direction)
|
|
75
|
+
return { ok: false, error: 'direction must be upstream or downstream' };
|
|
76
|
+
let maxDepth = typeof params.maxDepth === 'number' && params.maxDepth > 0 ? params.maxDepth : 3;
|
|
77
|
+
if (maxDepth > 32)
|
|
78
|
+
maxDepth = 32;
|
|
79
|
+
const { depth: crossDepth, warning: crossDepthWarning } = clampCrossDepth(params.crossDepth);
|
|
80
|
+
const relationTypes = Array.isArray(params.relationTypes)
|
|
81
|
+
? params.relationTypes.filter((t) => typeof t === 'string')
|
|
82
|
+
: undefined;
|
|
83
|
+
const includeTests = Boolean(params.includeTests);
|
|
84
|
+
let minConfidence = typeof params.minConfidence === 'number' ? params.minConfidence : 0;
|
|
85
|
+
if (minConfidence < 0)
|
|
86
|
+
minConfidence = 0;
|
|
87
|
+
if (minConfidence > 1)
|
|
88
|
+
minConfidence = 1;
|
|
89
|
+
const service = normalizeServicePrefix(params.service);
|
|
90
|
+
const subgroup = typeof params.subgroup === 'string' ? params.subgroup : undefined;
|
|
91
|
+
let timeoutMs = typeof params.timeoutMs === 'number' && params.timeoutMs > 0
|
|
92
|
+
? params.timeoutMs
|
|
93
|
+
: typeof params.timeout === 'number' && params.timeout > 0
|
|
94
|
+
? params.timeout
|
|
95
|
+
: DEFAULT_LOCAL_IMPACT_TIMEOUT_MS;
|
|
96
|
+
if (timeoutMs > 3_600_000)
|
|
97
|
+
timeoutMs = 3_600_000;
|
|
98
|
+
return {
|
|
99
|
+
ok: true,
|
|
100
|
+
name,
|
|
101
|
+
repoPath,
|
|
102
|
+
target,
|
|
103
|
+
direction,
|
|
104
|
+
maxDepth,
|
|
105
|
+
crossDepth,
|
|
106
|
+
crossDepthWarning,
|
|
107
|
+
relationTypes,
|
|
108
|
+
includeTests,
|
|
109
|
+
minConfidence,
|
|
110
|
+
service,
|
|
111
|
+
subgroup,
|
|
112
|
+
timeoutMs,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
async function resolveGroupRepo(port, config, repoPath) {
|
|
116
|
+
const registryName = config.repos[repoPath];
|
|
117
|
+
if (!registryName) {
|
|
118
|
+
return { error: `Unknown repo path "${repoPath}" in this group.` };
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
return await port.resolveRepo(registryName);
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async function safeLocalImpact(port, repo, impactParams, timeoutMs) {
|
|
128
|
+
let timer;
|
|
129
|
+
const impactP = port.impact(repo, impactParams).catch((err) => ({
|
|
130
|
+
error: err instanceof Error ? err.message : String(err),
|
|
131
|
+
}));
|
|
132
|
+
const timeoutP = new Promise((resolve) => {
|
|
133
|
+
timer = setTimeout(() => resolve('timeout'), timeoutMs);
|
|
134
|
+
});
|
|
135
|
+
const won = await Promise.race([
|
|
136
|
+
impactP.then((v) => ({ tag: 'impact', v })),
|
|
137
|
+
timeoutP.then(() => ({ tag: 'timeout' })),
|
|
138
|
+
]);
|
|
139
|
+
if (timer !== undefined)
|
|
140
|
+
clearTimeout(timer);
|
|
141
|
+
if (won.tag === 'timeout') {
|
|
142
|
+
return {
|
|
143
|
+
value: { error: 'Local impact timed out', partial: true },
|
|
144
|
+
timedOut: true,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return { value: won.v, timedOut: false };
|
|
148
|
+
}
|
|
149
|
+
export function collectImpactSymbolUids(local, servicePrefix) {
|
|
150
|
+
const uids = new Set();
|
|
151
|
+
let targetFilePath;
|
|
152
|
+
const obj = local;
|
|
153
|
+
if (!obj || typeof obj !== 'object')
|
|
154
|
+
return { uids: [], targetFilePath };
|
|
155
|
+
const target = obj.target;
|
|
156
|
+
if (target?.id) {
|
|
157
|
+
targetFilePath = typeof target.filePath === 'string' ? target.filePath : undefined;
|
|
158
|
+
if (fileMatchesServicePrefix(targetFilePath, servicePrefix)) {
|
|
159
|
+
uids.add(String(target.id));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const byDepth = obj.byDepth;
|
|
163
|
+
if (byDepth && typeof byDepth === 'object') {
|
|
164
|
+
for (const items of Object.values(byDepth)) {
|
|
165
|
+
if (!Array.isArray(items))
|
|
166
|
+
continue;
|
|
167
|
+
for (const it of items) {
|
|
168
|
+
const row = it;
|
|
169
|
+
if (row?.id && fileMatchesServicePrefix(row.filePath, servicePrefix)) {
|
|
170
|
+
uids.add(String(row.id));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return { uids: [...uids], targetFilePath };
|
|
176
|
+
}
|
|
177
|
+
function extractProcessNames(impact) {
|
|
178
|
+
const o = impact;
|
|
179
|
+
if (!o?.affected_processes)
|
|
180
|
+
return [];
|
|
181
|
+
return o.affected_processes.map((p) => String(p.name ?? '')).filter(Boolean);
|
|
182
|
+
}
|
|
183
|
+
function mergeRisk(localRisk, cross) {
|
|
184
|
+
const highConf = cross.some((c) => c.contract.confidence >= 0.85);
|
|
185
|
+
if (localRisk === 'CRITICAL')
|
|
186
|
+
return 'CRITICAL';
|
|
187
|
+
if (cross.length >= 3)
|
|
188
|
+
return 'CRITICAL';
|
|
189
|
+
if (highConf)
|
|
190
|
+
return 'HIGH';
|
|
191
|
+
if (cross.length > 0 && (localRisk === 'LOW' || localRisk === 'UNKNOWN'))
|
|
192
|
+
return 'MEDIUM';
|
|
193
|
+
return localRisk;
|
|
194
|
+
}
|
|
195
|
+
async function ensureBridgeReady(groupDir) {
|
|
196
|
+
const meta = await readBridgeMeta(groupDir);
|
|
197
|
+
if (meta.version > 0 && meta.version !== BRIDGE_SCHEMA_VERSION) {
|
|
198
|
+
return {
|
|
199
|
+
error: `Bridge schema version mismatch (meta.json has ${meta.version}, expected ${BRIDGE_SCHEMA_VERSION}). Run gitnexus group sync for this group.`,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const dbPath = path.join(groupDir, 'bridge.lbug');
|
|
203
|
+
try {
|
|
204
|
+
await fsp.access(dbPath);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return {
|
|
208
|
+
error: `No bridge.lbug in this group directory. Run gitnexus group sync (schema ${BRIDGE_SCHEMA_VERSION}).`,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
const handle = await openBridgeDbReadOnly(groupDir);
|
|
212
|
+
if (!handle) {
|
|
213
|
+
return {
|
|
214
|
+
error: `Could not open bridge.lbug read-only (schema ${BRIDGE_SCHEMA_VERSION}). Run gitnexus group sync.`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
return { handle };
|
|
218
|
+
}
|
|
219
|
+
function rowToNeighbor(r) {
|
|
220
|
+
const neighborRepo = String(r.neighborRepo ?? r[0] ?? '');
|
|
221
|
+
const neighborUid = String(r.neighborUid ?? r[1] ?? '');
|
|
222
|
+
if (!neighborRepo || !neighborUid)
|
|
223
|
+
return null;
|
|
224
|
+
return {
|
|
225
|
+
neighborRepo,
|
|
226
|
+
neighborUid,
|
|
227
|
+
neighborFilePath: r.neighborFilePath !== undefined ? String(r.neighborFilePath) : String(r[2] ?? ''),
|
|
228
|
+
matchType: String(r.matchType ?? r[3] ?? 'exact'),
|
|
229
|
+
confidence: Number(r.confidence ?? r[4] ?? 0),
|
|
230
|
+
contractId: String(r.contractId ?? r[5] ?? ''),
|
|
231
|
+
contractType: String(r.contractType ?? r[6] ?? 'custom'),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
export async function runGroupImpact(deps, params) {
|
|
235
|
+
const parsed = validateGroupImpactParams(params);
|
|
236
|
+
if (parsed.ok === false)
|
|
237
|
+
return { error: parsed.error };
|
|
238
|
+
const { name, repoPath, target, direction, maxDepth, crossDepth: _crossDepth, crossDepthWarning, relationTypes, includeTests, minConfidence, service: servicePrefix, subgroup, timeoutMs, } = parsed;
|
|
239
|
+
const groupDir = getGroupDir(deps.gitnexusDir, name);
|
|
240
|
+
let config;
|
|
241
|
+
try {
|
|
242
|
+
config = await loadGroupConfig(groupDir);
|
|
243
|
+
}
|
|
244
|
+
catch (e) {
|
|
245
|
+
if (e instanceof GroupNotFoundError)
|
|
246
|
+
return { error: `Group "${name}" not found. Run group_list to see configured groups.` };
|
|
247
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
248
|
+
}
|
|
249
|
+
const resolved = await resolveGroupRepo(deps.port, config, repoPath);
|
|
250
|
+
if ('error' in resolved)
|
|
251
|
+
return { error: resolved.error };
|
|
252
|
+
const impactParams = {
|
|
253
|
+
target,
|
|
254
|
+
direction,
|
|
255
|
+
maxDepth,
|
|
256
|
+
relationTypes: relationTypes && relationTypes.length > 0 ? relationTypes : undefined,
|
|
257
|
+
includeTests,
|
|
258
|
+
minConfidence,
|
|
259
|
+
};
|
|
260
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
261
|
+
const { value: local, timedOut: localTimedOut } = await safeLocalImpact(deps.port, resolved, impactParams, timeoutMs);
|
|
262
|
+
if (localTimedOut) {
|
|
263
|
+
const _base = local;
|
|
264
|
+
return {
|
|
265
|
+
local,
|
|
266
|
+
group: name,
|
|
267
|
+
cross: [],
|
|
268
|
+
outOfScope: [],
|
|
269
|
+
truncated: true,
|
|
270
|
+
truncatedRepos: [],
|
|
271
|
+
summary: {
|
|
272
|
+
direct: 0,
|
|
273
|
+
processes_affected: 0,
|
|
274
|
+
modules_affected: 0,
|
|
275
|
+
cross_repo_hits: 0,
|
|
276
|
+
},
|
|
277
|
+
risk: 'UNKNOWN',
|
|
278
|
+
timeoutMs,
|
|
279
|
+
truncationReason: 'timeout',
|
|
280
|
+
crossDepthWarning,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
const localObj = local;
|
|
284
|
+
if (localObj?.error && typeof localObj.error === 'string') {
|
|
285
|
+
// Fail closed: the local-impact phase errored (missing symbol, graph-load
|
|
286
|
+
// failure, thrown exception wrapped by safeLocalImpact, or port-returned
|
|
287
|
+
// `{ error }`). Do NOT wrap it into a zero-hit success payload — callers
|
|
288
|
+
// branch on top-level `error`, and a blast-radius tool reporting "no
|
|
289
|
+
// impact" on the failure path is a false negative on a safety-critical
|
|
290
|
+
// signal. Bubble the error so consumers treat it as a failure.
|
|
291
|
+
return { error: `Local impact failed for ${repoPath}: ${localObj.error}` };
|
|
292
|
+
}
|
|
293
|
+
if (servicePrefix) {
|
|
294
|
+
const tf = localObj?.target?.filePath;
|
|
295
|
+
if (!fileMatchesServicePrefix(tf, servicePrefix)) {
|
|
296
|
+
return {
|
|
297
|
+
local: {},
|
|
298
|
+
group: name,
|
|
299
|
+
cross: [],
|
|
300
|
+
outOfScope: [],
|
|
301
|
+
truncated: false,
|
|
302
|
+
truncatedRepos: [],
|
|
303
|
+
summary: {
|
|
304
|
+
direct: 0,
|
|
305
|
+
processes_affected: 0,
|
|
306
|
+
modules_affected: 0,
|
|
307
|
+
cross_repo_hits: 0,
|
|
308
|
+
},
|
|
309
|
+
risk: 'LOW',
|
|
310
|
+
timeoutMs,
|
|
311
|
+
crossDepthWarning,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const { uids } = collectImpactSymbolUids(local, servicePrefix);
|
|
316
|
+
if (uids.length === 0) {
|
|
317
|
+
const s = local?.summary || {};
|
|
318
|
+
return {
|
|
319
|
+
local,
|
|
320
|
+
group: name,
|
|
321
|
+
cross: [],
|
|
322
|
+
outOfScope: [],
|
|
323
|
+
truncated: Boolean(local.partial),
|
|
324
|
+
truncatedRepos: [],
|
|
325
|
+
summary: {
|
|
326
|
+
direct: s.direct ?? 0,
|
|
327
|
+
processes_affected: s.processes_affected ?? 0,
|
|
328
|
+
modules_affected: s.modules_affected ?? 0,
|
|
329
|
+
cross_repo_hits: 0,
|
|
330
|
+
},
|
|
331
|
+
risk: String(local.risk ?? 'LOW'),
|
|
332
|
+
timeoutMs,
|
|
333
|
+
truncationReason: local.partial ? 'partial' : undefined,
|
|
334
|
+
crossDepthWarning,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
const bridgePrep = await ensureBridgeReady(groupDir);
|
|
338
|
+
if ('error' in bridgePrep)
|
|
339
|
+
return { error: bridgePrep.error };
|
|
340
|
+
const handle = bridgePrep.handle;
|
|
341
|
+
const cross = [];
|
|
342
|
+
const outOfScope = [];
|
|
343
|
+
const truncatedRepos = [];
|
|
344
|
+
try {
|
|
345
|
+
const cypher = direction === 'upstream' ? CY_NEIGHBORS_UPSTREAM : CY_NEIGHBORS_DOWNSTREAM;
|
|
346
|
+
const rows = await queryBridge(handle, cypher, {
|
|
347
|
+
localRepo: repoPath,
|
|
348
|
+
uids,
|
|
349
|
+
});
|
|
350
|
+
const neighbors = [];
|
|
351
|
+
for (const raw of rows) {
|
|
352
|
+
const n = rowToNeighbor(raw);
|
|
353
|
+
if (n)
|
|
354
|
+
neighbors.push(n);
|
|
355
|
+
}
|
|
356
|
+
neighbors.sort((a, b) => b.confidence - a.confidence);
|
|
357
|
+
const seen = new Set();
|
|
358
|
+
for (const n of neighbors) {
|
|
359
|
+
if (servicePrefix && !fileMatchesServicePrefix(n.neighborFilePath, servicePrefix)) {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (!repoInSubgroup(n.neighborRepo, subgroup)) {
|
|
363
|
+
outOfScope.push({
|
|
364
|
+
from: direction === 'upstream' ? n.neighborRepo : repoPath,
|
|
365
|
+
to: direction === 'upstream' ? repoPath : n.neighborRepo,
|
|
366
|
+
contractId: n.contractId,
|
|
367
|
+
confidence: n.confidence,
|
|
368
|
+
});
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
const key = `${n.neighborRepo}\0${n.neighborUid}\0${n.contractId}`;
|
|
372
|
+
if (seen.has(key))
|
|
373
|
+
continue;
|
|
374
|
+
seen.add(key);
|
|
375
|
+
if (Date.now() > deadline) {
|
|
376
|
+
truncatedRepos.push(n.neighborRepo);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
const regName = config.repos[n.neighborRepo];
|
|
380
|
+
if (!regName)
|
|
381
|
+
continue;
|
|
382
|
+
let neighborHandle;
|
|
383
|
+
try {
|
|
384
|
+
neighborHandle = await deps.port.resolveRepo(regName);
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
truncatedRepos.push(n.neighborRepo);
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
const fan = await deps.port.impactByUid(neighborHandle.id, n.neighborUid, direction, {
|
|
391
|
+
maxDepth,
|
|
392
|
+
relationTypes: relationTypes ?? [],
|
|
393
|
+
minConfidence,
|
|
394
|
+
includeTests,
|
|
395
|
+
});
|
|
396
|
+
if (fan == null) {
|
|
397
|
+
truncatedRepos.push(n.neighborRepo);
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
cross.push({
|
|
401
|
+
repo: regName,
|
|
402
|
+
repo_path: n.neighborRepo,
|
|
403
|
+
contract: {
|
|
404
|
+
id: n.contractId,
|
|
405
|
+
type: n.contractType,
|
|
406
|
+
match_type: n.matchType || 'exact',
|
|
407
|
+
confidence: n.confidence,
|
|
408
|
+
},
|
|
409
|
+
by_depth: (fan.byDepth ?? {}),
|
|
410
|
+
affected_processes: extractProcessNames(fan),
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
finally {
|
|
415
|
+
await closeBridgeDb(handle);
|
|
416
|
+
}
|
|
417
|
+
const localSum = local?.summary || {};
|
|
418
|
+
const localRisk = String(local.risk ?? 'LOW');
|
|
419
|
+
const localPartial = Boolean(local.partial);
|
|
420
|
+
const truncated = truncatedRepos.length > 0 || localPartial;
|
|
421
|
+
const result = {
|
|
422
|
+
local,
|
|
423
|
+
group: name,
|
|
424
|
+
cross,
|
|
425
|
+
outOfScope,
|
|
426
|
+
truncated,
|
|
427
|
+
truncatedRepos: [...new Set(truncatedRepos)],
|
|
428
|
+
summary: {
|
|
429
|
+
direct: localSum.direct ?? 0,
|
|
430
|
+
processes_affected: localSum.processes_affected ?? 0,
|
|
431
|
+
modules_affected: localSum.modules_affected ?? 0,
|
|
432
|
+
cross_repo_hits: cross.length,
|
|
433
|
+
},
|
|
434
|
+
risk: mergeRisk(localRisk, cross),
|
|
435
|
+
timeoutMs,
|
|
436
|
+
truncationReason: truncated ? 'partial' : undefined,
|
|
437
|
+
crossDepthWarning,
|
|
438
|
+
};
|
|
439
|
+
return result;
|
|
440
|
+
}
|
|
441
|
+
export { normalizeServicePrefix, fileMatchesServicePrefix } from './group-path-utils.js';
|
|
@@ -1,27 +1,71 @@
|
|
|
1
1
|
import PHP from 'tree-sitter-php';
|
|
2
2
|
import { compilePatterns, runCompiledPatterns, unquoteLiteral, } from '../tree-sitter-scanner.js';
|
|
3
3
|
/**
|
|
4
|
-
* PHP HTTP plugin
|
|
4
|
+
* PHP HTTP plugin.
|
|
5
|
+
*
|
|
6
|
+
* Providers:
|
|
7
|
+
* - Laravel `Route::get/post/...`
|
|
8
|
+
*
|
|
9
|
+
* Consumers (string-literal URLs only):
|
|
10
|
+
* - Laravel HTTP client: `Http::get/post/put/delete/patch($url)`
|
|
11
|
+
* - Guzzle / generic object method: `$client->get/post/...($url)`
|
|
12
|
+
* - `file_get_contents($url)`
|
|
5
13
|
*
|
|
6
14
|
* The pipeline already uses `PHP.php_only` for ingesting plain `.php`
|
|
7
15
|
* files (see `core/tree-sitter/parser-loader.ts`), and we do the same
|
|
8
16
|
* here so Laravel route files are parsed with the right grammar dialect.
|
|
17
|
+
*
|
|
18
|
+
* Scope notes: consumer patterns match string literals only. URLs built
|
|
19
|
+
* via binary concatenation (`$base . '/path'`), `sprintf`, or config
|
|
20
|
+
* lookup (`config('services.foo.base').'/path'`) are intentionally left
|
|
21
|
+
* for a follow-up — they require constant-folding the surrounding
|
|
22
|
+
* scope to be meaningful.
|
|
9
23
|
*/
|
|
10
|
-
const
|
|
11
|
-
|
|
24
|
+
const LARAVEL_ROUTE_SPEC = {
|
|
25
|
+
meta: {},
|
|
26
|
+
query: `
|
|
27
|
+
(scoped_call_expression
|
|
28
|
+
scope: (name) @scope (#eq? @scope "Route")
|
|
29
|
+
name: (name) @method (#match? @method "^(get|post|put|delete|patch)$")
|
|
30
|
+
arguments: (arguments . (argument (string) @path)))
|
|
31
|
+
`,
|
|
32
|
+
};
|
|
33
|
+
const HTTP_FACADE_SPEC = {
|
|
34
|
+
meta: {},
|
|
35
|
+
query: `
|
|
36
|
+
(scoped_call_expression
|
|
37
|
+
scope: (name) @scope (#eq? @scope "Http")
|
|
38
|
+
name: (name) @method (#match? @method "^(get|post|put|delete|patch)$")
|
|
39
|
+
arguments: (arguments . (argument (string) @path)))
|
|
40
|
+
`,
|
|
41
|
+
};
|
|
42
|
+
const GUZZLE_MEMBER_SPEC = {
|
|
43
|
+
meta: {},
|
|
44
|
+
query: `
|
|
45
|
+
(member_call_expression
|
|
46
|
+
name: (name) @method (#match? @method "^(get|post|put|delete|patch)$")
|
|
47
|
+
arguments: (arguments . (argument (string) @path)))
|
|
48
|
+
`,
|
|
49
|
+
};
|
|
50
|
+
const FILE_GET_CONTENTS_SPEC = {
|
|
51
|
+
meta: {},
|
|
52
|
+
query: `
|
|
53
|
+
(function_call_expression
|
|
54
|
+
function: (name) @fn (#eq? @fn "file_get_contents")
|
|
55
|
+
arguments: (arguments . (argument (string) @path)))
|
|
56
|
+
`,
|
|
57
|
+
};
|
|
58
|
+
const mk = (spec, suffix) => compilePatterns({
|
|
59
|
+
name: `php-${suffix}`,
|
|
12
60
|
language: PHP.php_only,
|
|
13
|
-
patterns: [
|
|
14
|
-
{
|
|
15
|
-
meta: {},
|
|
16
|
-
query: `
|
|
17
|
-
(scoped_call_expression
|
|
18
|
-
scope: (name) @scope (#eq? @scope "Route")
|
|
19
|
-
name: (name) @method (#match? @method "^(get|post|put|delete|patch)$")
|
|
20
|
-
arguments: (arguments . (argument (string) @path)))
|
|
21
|
-
`,
|
|
22
|
-
},
|
|
23
|
-
],
|
|
61
|
+
patterns: [spec],
|
|
24
62
|
});
|
|
63
|
+
const PHP_PATTERNS = {
|
|
64
|
+
laravelRoute: mk(LARAVEL_ROUTE_SPEC, 'laravel-route'),
|
|
65
|
+
httpFacade: mk(HTTP_FACADE_SPEC, 'http-facade'),
|
|
66
|
+
guzzleMember: mk(GUZZLE_MEMBER_SPEC, 'guzzle-member'),
|
|
67
|
+
fileGetContents: mk(FILE_GET_CONTENTS_SPEC, 'file-get-contents'),
|
|
68
|
+
};
|
|
25
69
|
/**
|
|
26
70
|
* Extract the inner text of a PHP `string` node. The tree-sitter-php
|
|
27
71
|
* grammar wraps single / double-quoted literals differently depending
|
|
@@ -30,12 +74,9 @@ const LARAVEL_PATTERNS = compilePatterns({
|
|
|
30
74
|
* child nodes.
|
|
31
75
|
*/
|
|
32
76
|
function phpStringText(node) {
|
|
33
|
-
// Most single-quoted strings expose their inner content through the
|
|
34
|
-
// full node text (including quotes), which unquoteLiteral strips.
|
|
35
77
|
const direct = unquoteLiteral(node.text);
|
|
36
78
|
if (direct !== null && direct !== node.text)
|
|
37
79
|
return direct;
|
|
38
|
-
// Fall back to child string_content / string_value node if present.
|
|
39
80
|
for (const child of node.children) {
|
|
40
81
|
if (child.type === 'string_content' || child.type === 'string_value') {
|
|
41
82
|
return child.text;
|
|
@@ -43,12 +84,29 @@ function phpStringText(node) {
|
|
|
43
84
|
}
|
|
44
85
|
return direct;
|
|
45
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* HTTP client helpers (`Http::`, Guzzle) are almost always called with
|
|
89
|
+
* a path relative to a configured base URL, or a full URL. File paths
|
|
90
|
+
* are rare. Accept both relative (`/api/...`) and absolute (`http(s)://`).
|
|
91
|
+
*/
|
|
92
|
+
function isHttpClientPath(path) {
|
|
93
|
+
return path.startsWith('/') || path.startsWith('http://') || path.startsWith('https://');
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* `file_get_contents` is used for both HTTP and filesystem reads. Only
|
|
97
|
+
* emit a consumer contract when the URL is an absolute HTTP(S) URL to
|
|
98
|
+
* avoid false positives for local file paths and stream wrappers
|
|
99
|
+
* (`php://input`, `file://`, `data:`, ...).
|
|
100
|
+
*/
|
|
101
|
+
function isHttpUrlLiteral(path) {
|
|
102
|
+
return path.startsWith('http://') || path.startsWith('https://');
|
|
103
|
+
}
|
|
46
104
|
export const PHP_HTTP_PLUGIN = {
|
|
47
105
|
name: 'php-http',
|
|
48
106
|
language: PHP.php_only,
|
|
49
107
|
scan(tree) {
|
|
50
108
|
const out = [];
|
|
51
|
-
for (const match of runCompiledPatterns(
|
|
109
|
+
for (const match of runCompiledPatterns(PHP_PATTERNS.laravelRoute, tree)) {
|
|
52
110
|
const methodNode = match.captures.method;
|
|
53
111
|
const pathNode = match.captures.path;
|
|
54
112
|
if (!methodNode || !pathNode)
|
|
@@ -65,6 +123,56 @@ export const PHP_HTTP_PLUGIN = {
|
|
|
65
123
|
confidence: 0.8,
|
|
66
124
|
});
|
|
67
125
|
}
|
|
126
|
+
for (const match of runCompiledPatterns(PHP_PATTERNS.httpFacade, tree)) {
|
|
127
|
+
const methodNode = match.captures.method;
|
|
128
|
+
const pathNode = match.captures.path;
|
|
129
|
+
if (!methodNode || !pathNode)
|
|
130
|
+
continue;
|
|
131
|
+
const path = phpStringText(pathNode);
|
|
132
|
+
if (path === null || !isHttpClientPath(path))
|
|
133
|
+
continue;
|
|
134
|
+
out.push({
|
|
135
|
+
role: 'consumer',
|
|
136
|
+
framework: 'laravel-http',
|
|
137
|
+
method: methodNode.text.toUpperCase(),
|
|
138
|
+
path,
|
|
139
|
+
name: null,
|
|
140
|
+
confidence: 0.7,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
for (const match of runCompiledPatterns(PHP_PATTERNS.guzzleMember, tree)) {
|
|
144
|
+
const methodNode = match.captures.method;
|
|
145
|
+
const pathNode = match.captures.path;
|
|
146
|
+
if (!methodNode || !pathNode)
|
|
147
|
+
continue;
|
|
148
|
+
const path = phpStringText(pathNode);
|
|
149
|
+
if (path === null || !isHttpClientPath(path))
|
|
150
|
+
continue;
|
|
151
|
+
out.push({
|
|
152
|
+
role: 'consumer',
|
|
153
|
+
framework: 'guzzle',
|
|
154
|
+
method: methodNode.text.toUpperCase(),
|
|
155
|
+
path,
|
|
156
|
+
name: null,
|
|
157
|
+
confidence: 0.7,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
for (const match of runCompiledPatterns(PHP_PATTERNS.fileGetContents, tree)) {
|
|
161
|
+
const pathNode = match.captures.path;
|
|
162
|
+
if (!pathNode)
|
|
163
|
+
continue;
|
|
164
|
+
const path = phpStringText(pathNode);
|
|
165
|
+
if (path === null || !isHttpUrlLiteral(path))
|
|
166
|
+
continue;
|
|
167
|
+
out.push({
|
|
168
|
+
role: 'consumer',
|
|
169
|
+
framework: 'file-get-contents',
|
|
170
|
+
method: 'GET',
|
|
171
|
+
path,
|
|
172
|
+
name: null,
|
|
173
|
+
confidence: 0.7,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
68
176
|
return out;
|
|
69
177
|
},
|
|
70
178
|
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared service-path normalization for group tools (`service` monorepo filter)
|
|
3
|
+
* and subgroup membership checks.
|
|
4
|
+
*
|
|
5
|
+
* Inputs may originate from tree-sitter, the OS file API, or user-supplied
|
|
6
|
+
* MCP arguments, so both `\` and `/` separators are accepted. Internally we
|
|
7
|
+
* normalize to POSIX-style `/` for case-sensitive segment comparisons.
|
|
8
|
+
*/
|
|
9
|
+
export declare function normalizeServicePrefix(service: unknown): string | undefined;
|
|
10
|
+
export declare function fileMatchesServicePrefix(filePath: string | undefined, prefix: string | undefined): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* True if `repoPath` is at or beneath `subgroup` (member-path prefix in
|
|
13
|
+
* `group.yaml`). Empty / missing `subgroup` matches every repo.
|
|
14
|
+
*
|
|
15
|
+
* @param exact When set, requires an exact equality match (no descendant repos).
|
|
16
|
+
*/
|
|
17
|
+
export declare function repoInSubgroup(repoPath: string, subgroup?: string, exact?: boolean): boolean;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared service-path normalization for group tools (`service` monorepo filter)
|
|
3
|
+
* and subgroup membership checks.
|
|
4
|
+
*
|
|
5
|
+
* Inputs may originate from tree-sitter, the OS file API, or user-supplied
|
|
6
|
+
* MCP arguments, so both `\` and `/` separators are accepted. Internally we
|
|
7
|
+
* normalize to POSIX-style `/` for case-sensitive segment comparisons.
|
|
8
|
+
*/
|
|
9
|
+
function toPosix(p) {
|
|
10
|
+
return p.replace(/\\/g, '/');
|
|
11
|
+
}
|
|
12
|
+
export function normalizeServicePrefix(service) {
|
|
13
|
+
if (service === undefined || service === null)
|
|
14
|
+
return undefined;
|
|
15
|
+
const s = toPosix(String(service)).trim().replace(/\/+$/, '');
|
|
16
|
+
return s.length > 0 ? s : undefined;
|
|
17
|
+
}
|
|
18
|
+
export function fileMatchesServicePrefix(filePath, prefix) {
|
|
19
|
+
if (!prefix)
|
|
20
|
+
return true;
|
|
21
|
+
if (!filePath)
|
|
22
|
+
return false;
|
|
23
|
+
const normalized = toPosix(filePath);
|
|
24
|
+
return normalized === prefix || normalized.startsWith(`${prefix}/`);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* True if `repoPath` is at or beneath `subgroup` (member-path prefix in
|
|
28
|
+
* `group.yaml`). Empty / missing `subgroup` matches every repo.
|
|
29
|
+
*
|
|
30
|
+
* @param exact When set, requires an exact equality match (no descendant repos).
|
|
31
|
+
*/
|
|
32
|
+
export function repoInSubgroup(repoPath, subgroup, exact) {
|
|
33
|
+
if (!subgroup?.trim())
|
|
34
|
+
return true;
|
|
35
|
+
const s = toPosix(subgroup).replace(/\/+$/, '');
|
|
36
|
+
const r = toPosix(repoPath);
|
|
37
|
+
if (exact)
|
|
38
|
+
return r === s;
|
|
39
|
+
return r === s || r.startsWith(`${s}/`);
|
|
40
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map MCP/CLI `@groupName` or `@groupName/memberPath` to a concrete member path in group.yaml.
|
|
3
|
+
*/
|
|
4
|
+
export declare function resolveAtGroupMemberRepoPath(groupName: string, explicitMemberPath: string | undefined): Promise<{
|
|
5
|
+
ok: true;
|
|
6
|
+
repoPath: string;
|
|
7
|
+
} | {
|
|
8
|
+
ok: false;
|
|
9
|
+
error: string;
|
|
10
|
+
}>;
|