homegraph 1.1.2 → 1.2.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/LICENSE +21 -21
- package/README.md +268 -243
- package/dist/arkts/ohos-api-index.d.ts +15 -0
- package/dist/arkts/ohos-api-index.d.ts.map +1 -0
- package/dist/arkts/ohos-api-index.js +190 -0
- package/dist/arkts/ohos-api-index.js.map +1 -0
- package/dist/arkts/ohos-sdk-input.d.ts +36 -0
- package/dist/arkts/ohos-sdk-input.d.ts.map +1 -0
- package/dist/arkts/ohos-sdk-input.js +214 -0
- package/dist/arkts/ohos-sdk-input.js.map +1 -0
- package/dist/bin/command-supervision.d.ts +12 -0
- package/dist/bin/command-supervision.d.ts.map +1 -0
- package/dist/bin/command-supervision.js +86 -0
- package/dist/bin/command-supervision.js.map +1 -0
- package/dist/bin/homegraph.d.ts +1 -1
- package/dist/bin/homegraph.js +971 -71
- package/dist/bin/homegraph.js.map +1 -1
- package/dist/db/index.d.ts +20 -0
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +39 -0
- package/dist/db/index.js.map +1 -1
- package/dist/db/migrations.d.ts +1 -1
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/migrations.js +58 -19
- package/dist/db/migrations.js.map +1 -1
- package/dist/db/queries.d.ts +14 -0
- package/dist/db/queries.d.ts.map +1 -1
- package/dist/db/queries.js +314 -143
- package/dist/db/queries.js.map +1 -1
- package/dist/db/schema.sql +172 -152
- package/dist/directory.d.ts +32 -0
- package/dist/directory.d.ts.map +1 -1
- package/dist/directory.js +88 -5
- package/dist/directory.js.map +1 -1
- package/dist/extraction/arkts-batch-worker.d.ts +2 -0
- package/dist/extraction/arkts-batch-worker.d.ts.map +1 -0
- package/dist/extraction/arkts-batch-worker.js +28 -0
- package/dist/extraction/arkts-batch-worker.js.map +1 -0
- package/dist/extraction/context.d.ts +11 -0
- package/dist/extraction/context.d.ts.map +1 -1
- package/dist/extraction/context.js +20 -0
- package/dist/extraction/context.js.map +1 -1
- package/dist/extraction/index.d.ts +16 -2
- package/dist/extraction/index.d.ts.map +1 -1
- package/dist/extraction/index.js +619 -380
- package/dist/extraction/index.js.map +1 -1
- package/dist/extraction/languages/arkts-state-decorators.d.ts +13 -0
- package/dist/extraction/languages/arkts-state-decorators.d.ts.map +1 -0
- package/dist/extraction/languages/arkts-state-decorators.js +26 -0
- package/dist/extraction/languages/arkts-state-decorators.js.map +1 -0
- package/dist/extraction/languages/arkts-viewtree.d.ts +4 -2
- package/dist/extraction/languages/arkts-viewtree.d.ts.map +1 -1
- package/dist/extraction/languages/arkts-viewtree.js +21 -6
- package/dist/extraction/languages/arkts-viewtree.js.map +1 -1
- package/dist/extraction/languages/arkts.d.ts +98 -2
- package/dist/extraction/languages/arkts.d.ts.map +1 -1
- package/dist/extraction/languages/arkts.js +1290 -61
- package/dist/extraction/languages/arkts.js.map +1 -1
- package/dist/extraction/languages/c-cpp.d.ts +56 -0
- package/dist/extraction/languages/c-cpp.d.ts.map +1 -1
- package/dist/extraction/languages/c-cpp.js +198 -1
- package/dist/extraction/languages/c-cpp.js.map +1 -1
- package/dist/extraction/languages/ohos-api-consumer.d.ts +34 -0
- package/dist/extraction/languages/ohos-api-consumer.d.ts.map +1 -0
- package/dist/extraction/languages/ohos-api-consumer.js +283 -0
- package/dist/extraction/languages/ohos-api-consumer.js.map +1 -0
- package/dist/extraction/parse-pool.d.ts +126 -0
- package/dist/extraction/parse-pool.d.ts.map +1 -0
- package/dist/extraction/parse-pool.js +319 -0
- package/dist/extraction/parse-pool.js.map +1 -0
- package/dist/extraction/tree-sitter-types.d.ts +17 -0
- package/dist/extraction/tree-sitter-types.d.ts.map +1 -1
- package/dist/extraction/tree-sitter.d.ts +21 -0
- package/dist/extraction/tree-sitter.d.ts.map +1 -1
- package/dist/extraction/tree-sitter.js +198 -27
- package/dist/extraction/tree-sitter.js.map +1 -1
- package/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
- package/dist/graph/traversal.d.ts.map +1 -1
- package/dist/graph/traversal.js +76 -17
- package/dist/graph/traversal.js.map +1 -1
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +78 -4
- package/dist/index.js.map +1 -1
- package/dist/installer/instructions-template.js +9 -9
- package/dist/installer/targets/shared.d.ts +5 -6
- package/dist/installer/targets/shared.d.ts.map +1 -1
- package/dist/installer/targets/shared.js +5 -6
- package/dist/installer/targets/shared.js.map +1 -1
- package/dist/mcp/daemon-paths.d.ts +30 -3
- package/dist/mcp/daemon-paths.d.ts.map +1 -1
- package/dist/mcp/daemon-paths.js +50 -10
- package/dist/mcp/daemon-paths.js.map +1 -1
- package/dist/mcp/daemon-registry.d.ts.map +1 -1
- package/dist/mcp/daemon-registry.js +7 -3
- package/dist/mcp/daemon-registry.js.map +1 -1
- package/dist/mcp/daemon.d.ts +48 -0
- package/dist/mcp/daemon.d.ts.map +1 -1
- package/dist/mcp/daemon.js +196 -32
- package/dist/mcp/daemon.js.map +1 -1
- package/dist/mcp/engine.d.ts +17 -0
- package/dist/mcp/engine.d.ts.map +1 -1
- package/dist/mcp/engine.js +73 -1
- package/dist/mcp/engine.js.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +25 -43
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/liveness-watchdog.js +16 -16
- package/dist/mcp/ppid-watchdog.d.ts +18 -0
- package/dist/mcp/ppid-watchdog.d.ts.map +1 -1
- package/dist/mcp/ppid-watchdog.js +37 -0
- package/dist/mcp/ppid-watchdog.js.map +1 -1
- package/dist/mcp/query-cache.d.ts +25 -0
- package/dist/mcp/query-cache.d.ts.map +1 -0
- package/dist/mcp/query-cache.js +191 -0
- package/dist/mcp/query-cache.js.map +1 -0
- package/dist/mcp/query-pool.d.ts +94 -0
- package/dist/mcp/query-pool.d.ts.map +1 -0
- package/dist/mcp/query-pool.js +297 -0
- package/dist/mcp/query-pool.js.map +1 -0
- package/dist/mcp/query-worker.d.ts +24 -0
- package/dist/mcp/query-worker.d.ts.map +1 -0
- package/dist/mcp/query-worker.js +87 -0
- package/dist/mcp/query-worker.js.map +1 -0
- package/dist/mcp/server-instructions.d.ts +5 -7
- package/dist/mcp/server-instructions.d.ts.map +1 -1
- package/dist/mcp/server-instructions.js +72 -74
- package/dist/mcp/server-instructions.js.map +1 -1
- package/dist/mcp/tools.d.ts +94 -4
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +877 -71
- package/dist/mcp/tools.js.map +1 -1
- package/dist/project-config.d.ts +20 -0
- package/dist/project-config.d.ts.map +1 -1
- package/dist/project-config.js +42 -2
- package/dist/project-config.js.map +1 -1
- package/dist/reasoning/login.js +1 -1
- package/dist/reasoning/login.js.map +1 -1
- package/dist/reasoning/reasoner.js +32 -32
- package/dist/resolution/c-fnptr-synthesizer.d.ts +0 -28
- package/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -1
- package/dist/resolution/c-fnptr-synthesizer.js +765 -79
- package/dist/resolution/c-fnptr-synthesizer.js.map +1 -1
- package/dist/resolution/callback-synthesizer.d.ts +1 -1
- package/dist/resolution/callback-synthesizer.d.ts.map +1 -1
- package/dist/resolution/callback-synthesizer.js +72 -11
- package/dist/resolution/callback-synthesizer.js.map +1 -1
- package/dist/resolution/cooperative-yield.d.ts +32 -0
- package/dist/resolution/cooperative-yield.d.ts.map +1 -0
- package/dist/resolution/cooperative-yield.js +42 -0
- package/dist/resolution/cooperative-yield.js.map +1 -0
- package/dist/resolution/index.d.ts +11 -2
- package/dist/resolution/index.d.ts.map +1 -1
- package/dist/resolution/index.js +72 -4
- package/dist/resolution/index.js.map +1 -1
- package/dist/resolution/name-matcher.d.ts +22 -0
- package/dist/resolution/name-matcher.d.ts.map +1 -1
- package/dist/resolution/name-matcher.js +317 -20
- package/dist/resolution/name-matcher.js.map +1 -1
- package/dist/spec/config.d.ts +39 -0
- package/dist/spec/config.d.ts.map +1 -0
- package/dist/spec/config.js +304 -0
- package/dist/spec/config.js.map +1 -0
- package/dist/spec/db/commit-node.d.ts +23 -0
- package/dist/spec/db/commit-node.d.ts.map +1 -0
- package/dist/spec/db/commit-node.js +62 -0
- package/dist/spec/db/commit-node.js.map +1 -0
- package/dist/spec/db/fragment-node.d.ts +24 -0
- package/dist/spec/db/fragment-node.d.ts.map +1 -0
- package/dist/spec/db/fragment-node.js +128 -0
- package/dist/spec/db/fragment-node.js.map +1 -0
- package/dist/spec/db/fts.d.ts +74 -0
- package/dist/spec/db/fts.d.ts.map +1 -0
- package/dist/spec/db/fts.js +324 -0
- package/dist/spec/db/fts.js.map +1 -0
- package/dist/spec/db/index.d.ts +13 -0
- package/dist/spec/db/index.d.ts.map +1 -0
- package/dist/spec/db/index.js +50 -0
- package/dist/spec/db/index.js.map +1 -0
- package/dist/spec/db/relations.d.ts +55 -0
- package/dist/spec/db/relations.d.ts.map +1 -0
- package/dist/spec/db/relations.js +158 -0
- package/dist/spec/db/relations.js.map +1 -0
- package/dist/spec/db/schema.d.ts +33 -0
- package/dist/spec/db/schema.d.ts.map +1 -0
- package/dist/spec/db/schema.js +119 -0
- package/dist/spec/db/schema.js.map +1 -0
- package/dist/spec/db/schema.sql +117 -0
- package/dist/spec/db/spec-node.d.ts +41 -0
- package/dist/spec/db/spec-node.d.ts.map +1 -0
- package/dist/spec/db/spec-node.js +114 -0
- package/dist/spec/db/spec-node.js.map +1 -0
- package/dist/spec/evolve/impact-locator.d.ts +13 -0
- package/dist/spec/evolve/impact-locator.d.ts.map +1 -0
- package/dist/spec/evolve/impact-locator.js +25 -0
- package/dist/spec/evolve/impact-locator.js.map +1 -0
- package/dist/spec/evolve/llm-client.d.ts +50 -0
- package/dist/spec/evolve/llm-client.d.ts.map +1 -0
- package/dist/spec/evolve/llm-client.js +176 -0
- package/dist/spec/evolve/llm-client.js.map +1 -0
- package/dist/spec/evolve/logic-checker.d.ts +12 -0
- package/dist/spec/evolve/logic-checker.d.ts.map +1 -0
- package/dist/spec/evolve/logic-checker.js +24 -0
- package/dist/spec/evolve/logic-checker.js.map +1 -0
- package/dist/spec/evolve/pipeline.d.ts +42 -0
- package/dist/spec/evolve/pipeline.d.ts.map +1 -0
- package/dist/spec/evolve/pipeline.js +567 -0
- package/dist/spec/evolve/pipeline.js.map +1 -0
- package/dist/spec/evolve/spec-rewriter.d.ts +42 -0
- package/dist/spec/evolve/spec-rewriter.d.ts.map +1 -0
- package/dist/spec/evolve/spec-rewriter.js +230 -0
- package/dist/spec/evolve/spec-rewriter.js.map +1 -0
- package/dist/spec/graph/queries.d.ts +155 -0
- package/dist/spec/graph/queries.d.ts.map +1 -0
- package/dist/spec/graph/queries.js +440 -0
- package/dist/spec/graph/queries.js.map +1 -0
- package/dist/spec/llm/client.d.ts +29 -0
- package/dist/spec/llm/client.d.ts.map +1 -0
- package/dist/spec/llm/client.js +123 -0
- package/dist/spec/llm/client.js.map +1 -0
- package/dist/spec/llm/index.d.ts +3 -0
- package/dist/spec/llm/index.d.ts.map +1 -0
- package/dist/spec/llm/index.js +11 -0
- package/dist/spec/llm/index.js.map +1 -0
- package/dist/spec/llm/prompts.d.ts +13 -0
- package/dist/spec/llm/prompts.d.ts.map +1 -0
- package/dist/spec/llm/prompts.js +75 -0
- package/dist/spec/llm/prompts.js.map +1 -0
- package/dist/spec/mining/diff-parser.d.ts +33 -0
- package/dist/spec/mining/diff-parser.d.ts.map +1 -0
- package/dist/spec/mining/diff-parser.js +166 -0
- package/dist/spec/mining/diff-parser.js.map +1 -0
- package/dist/spec/mining/git-scanner.d.ts +103 -0
- package/dist/spec/mining/git-scanner.d.ts.map +1 -0
- package/dist/spec/mining/git-scanner.js +307 -0
- package/dist/spec/mining/git-scanner.js.map +1 -0
- package/dist/spec/mining/pipeline.d.ts +53 -0
- package/dist/spec/mining/pipeline.d.ts.map +1 -0
- package/dist/spec/mining/pipeline.js +178 -0
- package/dist/spec/mining/pipeline.js.map +1 -0
- package/dist/spec/mining/scope-resolver.d.ts +45 -0
- package/dist/spec/mining/scope-resolver.d.ts.map +1 -0
- package/dist/spec/mining/scope-resolver.js +103 -0
- package/dist/spec/mining/scope-resolver.js.map +1 -0
- package/dist/spec/mining/spec-extractor.d.ts +69 -0
- package/dist/spec/mining/spec-extractor.d.ts.map +1 -0
- package/dist/spec/mining/spec-extractor.js +369 -0
- package/dist/spec/mining/spec-extractor.js.map +1 -0
- package/dist/spec/types.d.ts +149 -0
- package/dist/spec/types.d.ts.map +1 -0
- package/dist/spec/types.js +15 -0
- package/dist/spec/types.js.map +1 -0
- package/dist/spec/utils.d.ts +167 -0
- package/dist/spec/utils.d.ts.map +1 -0
- package/dist/spec/utils.js +463 -0
- package/dist/spec/utils.js.map +1 -0
- package/dist/sync/worktree.d.ts +9 -0
- package/dist/sync/worktree.d.ts.map +1 -1
- package/dist/sync/worktree.js +40 -0
- package/dist/sync/worktree.js.map +1 -1
- package/dist/types.d.ts +6 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/shimmer-progress.d.ts +2 -0
- package/dist/ui/shimmer-progress.d.ts.map +1 -1
- package/dist/ui/shimmer-progress.js +19 -2
- package/dist/ui/shimmer-progress.js.map +1 -1
- package/dist/upgrade/index.js +1 -1
- package/dist/upgrade/index.js.map +1 -1
- package/package.json +58 -57
- package/scripts/add-lang/bench.sh +60 -60
- package/scripts/add-lang/check-grammar.mjs +75 -75
- package/scripts/add-lang/dump-ast.mjs +103 -103
- package/scripts/add-lang/verify-extraction.mjs +70 -70
- package/scripts/agent-eval/ab-adoption.sh +91 -91
- package/scripts/agent-eval/ab-hook.sh +86 -86
- package/scripts/agent-eval/ab-impl.sh +78 -78
- package/scripts/agent-eval/ab-new-vs-baseline.sh +102 -102
- package/scripts/agent-eval/ab-sufficiency.sh +78 -78
- package/scripts/agent-eval/arms-F.sh +21 -21
- package/scripts/agent-eval/arms-matrix.sh +37 -37
- package/scripts/agent-eval/audit.sh +68 -68
- package/scripts/agent-eval/bench-readme.sh +28 -28
- package/scripts/agent-eval/bench-why-repo.sh +22 -22
- package/scripts/agent-eval/block-read-hook.sh +19 -19
- package/scripts/agent-eval/hook-settings.json +15 -15
- package/scripts/agent-eval/itrun.sh +120 -120
- package/scripts/agent-eval/offload-eval-3arm.sh +72 -72
- package/scripts/agent-eval/offload-eval-cost.mjs +133 -133
- package/scripts/agent-eval/offload-eval-effort.mjs +108 -108
- package/scripts/agent-eval/offload-eval-frontload-matrix.sh +25 -25
- package/scripts/agent-eval/offload-eval-frontload.sh +47 -47
- package/scripts/agent-eval/offload-eval-ground-truth.json +18 -18
- package/scripts/agent-eval/offload-eval-hook.mjs +84 -84
- package/scripts/agent-eval/offload-eval-judge.mjs +103 -103
- package/scripts/agent-eval/offload-eval-matrix.sh +20 -20
- package/scripts/agent-eval/offload-eval-metrics.mjs +94 -94
- package/scripts/agent-eval/offload-eval-refs1.sh +50 -50
- package/scripts/agent-eval/offload-eval-setup.sh +24 -24
- package/scripts/agent-eval/offload-eval-styles.sh +71 -71
- package/scripts/agent-eval/offload-eval-summarize.mjs +68 -68
- package/scripts/agent-eval/offload-eval.md +76 -76
- package/scripts/agent-eval/parse-arms.mjs +116 -116
- package/scripts/agent-eval/parse-bench-readme.mjs +84 -84
- package/scripts/agent-eval/parse-run.mjs +45 -45
- package/scripts/agent-eval/parse-session.mjs +93 -93
- package/scripts/agent-eval/probe-context.mjs +21 -21
- package/scripts/agent-eval/probe-explore.mjs +40 -40
- package/scripts/agent-eval/probe-node.mjs +20 -20
- package/scripts/agent-eval/probe-sweep.mjs +119 -119
- package/scripts/agent-eval/probe-trace.mjs +20 -20
- package/scripts/agent-eval/redirect-read-hook.sh +38 -38
- package/scripts/agent-eval/repro-concurrent-explore.mjs +119 -0
- package/scripts/agent-eval/repro-daemon-clients.mjs +125 -0
- package/scripts/agent-eval/run-agent.sh +34 -34
- package/scripts/agent-eval/run-all.sh +69 -69
- package/scripts/agent-eval/run-arms.sh +56 -56
- package/scripts/agent-eval/seq-matrix.mjs +137 -137
- package/scripts/build-bundle.sh +118 -118
- package/scripts/exp_boundary_eval/README.md +247 -0
- package/scripts/exp_boundary_eval/__pycache__/_utils.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/__pycache__/analyze.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/__pycache__/deveco_arm.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/__pycache__/run_one.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/__pycache__/run_session.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/__pycache__/setup.cpython-310.pyc +0 -0
- package/scripts/exp_boundary_eval/_test_mcp_chain.py +78 -0
- package/scripts/exp_boundary_eval/_test_stdin.py +8 -0
- package/scripts/exp_boundary_eval/_utils.py +1116 -0
- package/scripts/exp_boundary_eval/analyze.py +1313 -0
- package/scripts/exp_boundary_eval/data/agents.json +109 -0
- package/scripts/exp_boundary_eval/data/experiments.json +140 -0
- package/scripts/exp_boundary_eval/deveco_arm.py +519 -0
- package/scripts/exp_boundary_eval/run_all.py +378 -0
- package/scripts/exp_boundary_eval/run_one.py +165 -0
- package/scripts/exp_boundary_eval/run_session.py +158 -0
- package/scripts/exp_boundary_eval/setup.py +120 -0
- package/scripts/exp_boundary_eval/win_mcp_launcher.py +73 -0
- package/scripts/exp_boundary_eval/win_mcp_stdio_wrap.js +36 -0
- package/scripts/exp_boundary_eval/win_node_launcher.py +24 -0
- package/scripts/extract-release-notes.mjs +130 -130
- package/scripts/local-install.sh +41 -41
- package/scripts/npm-sdk.js +75 -75
- package/scripts/npm-shim.js +268 -246
- package/scripts/ohos-sdk-publish.mjs +133 -0
- package/scripts/pack-npm.sh +119 -119
- package/scripts/prepare-release.mjs +270 -270
- package/scripts/qa_eval/README.md +407 -404
- package/scripts/qa_eval/_test_deveco_probe.py +41 -41
- package/scripts/qa_eval/agent_runner.py +526 -526
- package/scripts/qa_eval/data/.gitignore +4 -4
- package/scripts/qa_eval/data/test-set.jsonl +2 -22
- package/scripts/qa_eval/eval_metrics.py +274 -233
- package/scripts/qa_eval/external_agent.py +976 -671
- package/scripts/qa_eval/llm_config.py +92 -92
- package/scripts/qa_eval/memory_monitor.py +132 -132
- package/scripts/qa_eval/my_answer_accuracy.py +187 -187
- package/scripts/qa_eval/requirements.txt +2 -2
- package/scripts/qa_eval/run_pipeline.py +804 -711
- package/scripts/qa_eval/stats_efficiency.py +279 -279
- package/scripts/qa_eval/stats_scores.py +207 -207
package/dist/bin/homegraph.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* homegraph callers <symbol> Find what calls a function/method
|
|
21
21
|
* homegraph callees <symbol> Find what a function/method calls
|
|
22
22
|
* homegraph impact <symbol> Analyze what code is affected by changing a symbol
|
|
23
|
-
* homegraph
|
|
23
|
+
* homegraph index-api <tools> Build OHOS API db from command-line-tools
|
|
24
24
|
* homegraph upgrade [version] Update HomeGraph to the latest release
|
|
25
25
|
*/
|
|
26
26
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
@@ -61,12 +61,14 @@ const commander_1 = require("commander");
|
|
|
61
61
|
const path = __importStar(require("path"));
|
|
62
62
|
const fs = __importStar(require("fs"));
|
|
63
63
|
const directory_1 = require("../directory");
|
|
64
|
+
const utils_1 = require("../spec/utils");
|
|
64
65
|
const worktree_1 = require("../sync/worktree");
|
|
65
66
|
const shimmer_progress_1 = require("../ui/shimmer-progress");
|
|
66
67
|
const glyphs_1 = require("../ui/glyphs");
|
|
67
68
|
const node_version_check_1 = require("./node-version-check");
|
|
68
69
|
const fatal_handler_1 = require("./fatal-handler");
|
|
69
70
|
const wasm_runtime_flags_1 = require("../extraction/wasm-runtime-flags");
|
|
71
|
+
const command_supervision_1 = require("./command-supervision");
|
|
70
72
|
const extraction_version_1 = require("../extraction/extraction-version");
|
|
71
73
|
const telemetry_1 = require("../telemetry");
|
|
72
74
|
// Lazy-load heavy modules (HomeGraph, runInstaller) to keep CLI startup fast.
|
|
@@ -235,6 +237,38 @@ function main() {
|
|
|
235
237
|
// Not found - return original path (will fail later with helpful error)
|
|
236
238
|
return absolutePath;
|
|
237
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* Resolve the project root for spec operations by walking up from a path
|
|
242
|
+
* looking for `${SPEC_DATA_DIR}/meta.json` or `${SPEC_DATA_DIR}/commit4spec.db`.
|
|
243
|
+
* Falls back to the starting path when neither marker is found (callers
|
|
244
|
+
* handle the not-found case).
|
|
245
|
+
*/
|
|
246
|
+
function resolveSpecProjectPath(pathArg) {
|
|
247
|
+
const startPath = path.resolve(pathArg || process.cwd());
|
|
248
|
+
// Direct hit
|
|
249
|
+
const checkDirs = [
|
|
250
|
+
`${utils_1.SPEC_DATA_DIR}/meta.json`,
|
|
251
|
+
`${utils_1.SPEC_DATA_DIR}/commit4spec.db`,
|
|
252
|
+
];
|
|
253
|
+
for (const check of checkDirs) {
|
|
254
|
+
if (fs.existsSync(path.join(startPath, check)))
|
|
255
|
+
return startPath;
|
|
256
|
+
}
|
|
257
|
+
// Walk up
|
|
258
|
+
let current = startPath;
|
|
259
|
+
const root = path.parse(current).root;
|
|
260
|
+
while (current !== root) {
|
|
261
|
+
const parent = path.dirname(current);
|
|
262
|
+
if (parent === current)
|
|
263
|
+
break;
|
|
264
|
+
current = parent;
|
|
265
|
+
for (const check of checkDirs) {
|
|
266
|
+
if (fs.existsSync(path.join(current, check)))
|
|
267
|
+
return current;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return startPath; // fallback — callers handle not-found
|
|
271
|
+
}
|
|
238
272
|
/**
|
|
239
273
|
* Format a number with commas
|
|
240
274
|
*/
|
|
@@ -383,6 +417,19 @@ function main() {
|
|
|
383
417
|
fs.unlinkSync(logPath);
|
|
384
418
|
}
|
|
385
419
|
}
|
|
420
|
+
const ohosApiWarnings = result.errors.filter((e) => e.severity === 'warning' && e.code?.startsWith('ohos_api_'));
|
|
421
|
+
if (ohosApiWarnings.length > 0) {
|
|
422
|
+
clack.log.warn('OHOS SDK API db not attached — project index is complete; explore uses project code only.');
|
|
423
|
+
clack.note(ohosApiWarnings.map((e) => e.message).join('\n'), 'OHOS API notice');
|
|
424
|
+
}
|
|
425
|
+
const arktsDegraded = result.errors.filter((e) => e.code === 'arkts_degraded');
|
|
426
|
+
if (arktsDegraded.length > 0) {
|
|
427
|
+
clack.log.warn('ArkTS index quality notice — see details below.');
|
|
428
|
+
clack.note(`${arktsDegraded[0].message}\n\n` +
|
|
429
|
+
'Large ArkTS repos on Windows use an isolated indexer with enableMethodBodyBuild=true.\n' +
|
|
430
|
+
'If indexing failed, set HOMEGRAPH_ARKTS_STACK_SIZES_KB=65536,131072,262144 and retry:\n' +
|
|
431
|
+
' node --stack-size=65536 dist/bin/homegraph.js index <path>', 'ArkTS quality notice');
|
|
432
|
+
}
|
|
386
433
|
}
|
|
387
434
|
/**
|
|
388
435
|
* Write detailed error log to .homegraph/errors.log
|
|
@@ -479,20 +526,27 @@ function main() {
|
|
|
479
526
|
// Indexing runs by default now. The legacy -i/--index flag is still
|
|
480
527
|
// accepted (so existing muscle memory and scripts don't break) but is a
|
|
481
528
|
// no-op — initializing always builds the initial index.
|
|
529
|
+
// Supervise the index: self-terminate if orphaned or wedged (#999).
|
|
530
|
+
const supervision = (0, command_supervision_1.installCommandSupervision)('init');
|
|
482
531
|
let result;
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
532
|
+
try {
|
|
533
|
+
if (options.verbose) {
|
|
534
|
+
result = await cg.indexAll({
|
|
535
|
+
onProgress: createVerboseProgress(),
|
|
536
|
+
verbose: true,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
process.stdout.write(`${colors.dim}${(0, glyphs_1.getGlyphs)().rail}${colors.reset}\n`);
|
|
541
|
+
const progress = (0, shimmer_progress_1.createShimmerProgress)();
|
|
542
|
+
result = await cg.indexAll({
|
|
543
|
+
onProgress: progress.onProgress,
|
|
544
|
+
});
|
|
545
|
+
await progress.stop();
|
|
546
|
+
}
|
|
488
547
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
const progress = (0, shimmer_progress_1.createShimmerProgress)();
|
|
492
|
-
result = await cg.indexAll({
|
|
493
|
-
onProgress: progress.onProgress,
|
|
494
|
-
});
|
|
495
|
-
await progress.stop();
|
|
548
|
+
finally {
|
|
549
|
+
supervision.stop();
|
|
496
550
|
}
|
|
497
551
|
printIndexResult(clack, result, projectPath);
|
|
498
552
|
await recordIndexTelemetry(cg, result);
|
|
@@ -587,47 +641,56 @@ function main() {
|
|
|
587
641
|
process.exit(1);
|
|
588
642
|
}
|
|
589
643
|
const { default: HomeGraph } = await loadHomeGraph();
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
644
|
+
// `index` is a FULL re-index — identical to a fresh `init`. RECREATE the
|
|
645
|
+
// database from scratch (discard .homegraph/homegraph.db + its WAL) rather
|
|
646
|
+
// than opening the old graph and DELETE-ing every row. The clear-then-index
|
|
647
|
+
// approach reported "0 nodes" without the clear (#874); the recreate keeps
|
|
648
|
+
// that fixed AND avoids the failure mode where, on a large or pre-fix
|
|
649
|
+
// poisoned index, the per-row FTS delete churn wedged the main thread long
|
|
650
|
+
// enough to trip the liveness watchdog before scanning even began (#1067).
|
|
651
|
+
// recreate() hands back a fresh, empty instance — no clear() needed. For
|
|
652
|
+
// fast incremental updates use `sync`.
|
|
653
|
+
const cg = await HomeGraph.recreate(projectPath);
|
|
654
|
+
// Supervise the indexer: self-terminate if orphaned (parent shim killed)
|
|
655
|
+
// or if the main thread wedges — neither was guarded on this path (#999).
|
|
656
|
+
const supervision = (0, command_supervision_1.installCommandSupervision)('index');
|
|
657
|
+
try {
|
|
658
|
+
if (options.quiet) {
|
|
659
|
+
// Quiet mode: no UI, just run against the freshly-recreated graph.
|
|
660
|
+
const result = await cg.indexAll();
|
|
661
|
+
if (!result.success)
|
|
662
|
+
process.exit(1);
|
|
663
|
+
cg.destroy();
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
const clack = await importESM('@clack/prompts');
|
|
667
|
+
clack.intro('Indexing project');
|
|
668
|
+
let result;
|
|
669
|
+
if (options.verbose) {
|
|
670
|
+
result = await cg.indexAll({
|
|
671
|
+
onProgress: createVerboseProgress(),
|
|
672
|
+
verbose: true,
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
else {
|
|
676
|
+
process.stdout.write(`${colors.dim}${(0, glyphs_1.getGlyphs)().rail}${colors.reset}\n`);
|
|
677
|
+
const progress = (0, shimmer_progress_1.createShimmerProgress)();
|
|
678
|
+
result = await cg.indexAll({
|
|
679
|
+
onProgress: progress.onProgress,
|
|
680
|
+
});
|
|
681
|
+
await progress.stop();
|
|
682
|
+
}
|
|
683
|
+
printIndexResult(clack, result, projectPath);
|
|
684
|
+
await recordIndexTelemetry(cg, result);
|
|
685
|
+
if (!result.success) {
|
|
597
686
|
process.exit(1);
|
|
687
|
+
}
|
|
688
|
+
clack.outro('Done');
|
|
598
689
|
cg.destroy();
|
|
599
|
-
return;
|
|
600
|
-
}
|
|
601
|
-
const clack = await importESM('@clack/prompts');
|
|
602
|
-
clack.intro('Indexing project');
|
|
603
|
-
// `index` is a FULL re-index: clear the existing graph and rebuild it from
|
|
604
|
-
// scratch so the result is identical to a fresh `init`. Without the clear,
|
|
605
|
-
// indexAll() skips every unchanged file by its content hash and reports
|
|
606
|
-
// "0 nodes, 0 edges" against the already-populated graph — which reads as
|
|
607
|
-
// "index wiped my index" (#874). For fast incremental updates use `sync`.
|
|
608
|
-
cg.clear();
|
|
609
|
-
let result;
|
|
610
|
-
if (options.verbose) {
|
|
611
|
-
result = await cg.indexAll({
|
|
612
|
-
onProgress: createVerboseProgress(),
|
|
613
|
-
verbose: true,
|
|
614
|
-
});
|
|
615
|
-
}
|
|
616
|
-
else {
|
|
617
|
-
process.stdout.write(`${colors.dim}${(0, glyphs_1.getGlyphs)().rail}${colors.reset}\n`);
|
|
618
|
-
const progress = (0, shimmer_progress_1.createShimmerProgress)();
|
|
619
|
-
result = await cg.indexAll({
|
|
620
|
-
onProgress: progress.onProgress,
|
|
621
|
-
});
|
|
622
|
-
await progress.stop();
|
|
623
690
|
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
if (!result.success) {
|
|
627
|
-
process.exit(1);
|
|
691
|
+
finally {
|
|
692
|
+
supervision.stop();
|
|
628
693
|
}
|
|
629
|
-
clack.outro('Done');
|
|
630
|
-
cg.destroy();
|
|
631
694
|
}
|
|
632
695
|
catch (err) {
|
|
633
696
|
error(`Failed to index: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -730,6 +793,7 @@ function main() {
|
|
|
730
793
|
const journalMode = cg.getJournalMode();
|
|
731
794
|
const buildInfo = cg.getIndexBuildInfo();
|
|
732
795
|
const reindexRecommended = cg.isIndexStale();
|
|
796
|
+
const ohosApi = cg.getOhosApiBinding();
|
|
733
797
|
// JSON output mode
|
|
734
798
|
if (options.json) {
|
|
735
799
|
const lastIndexedMs = cg.getLastIndexedAt();
|
|
@@ -755,6 +819,9 @@ function main() {
|
|
|
755
819
|
worktreeMismatch: worktreeMismatch
|
|
756
820
|
? { worktreeRoot: worktreeMismatch.worktreeRoot, indexRoot: worktreeMismatch.indexRoot }
|
|
757
821
|
: null,
|
|
822
|
+
ohosApi: ohosApi
|
|
823
|
+
? { version: ohosApi.version, packageName: ohosApi.packageName, dbPath: ohosApi.dbPath }
|
|
824
|
+
: null,
|
|
758
825
|
index: {
|
|
759
826
|
builtWithVersion: buildInfo.version,
|
|
760
827
|
builtWithExtractionVersion: buildInfo.extractionVersion,
|
|
@@ -809,6 +876,13 @@ function main() {
|
|
|
809
876
|
console.log(` ${lang.padEnd(15)} ${formatNumber(count)}`);
|
|
810
877
|
}
|
|
811
878
|
console.log();
|
|
879
|
+
if (ohosApi) {
|
|
880
|
+
console.log(chalk.bold('OHOS API Database:'));
|
|
881
|
+
console.log(` Version: ${ohosApi.version}`);
|
|
882
|
+
console.log(` Package: ${ohosApi.packageName}`);
|
|
883
|
+
console.log(` DB: ${ohosApi.dbPath}`);
|
|
884
|
+
console.log();
|
|
885
|
+
}
|
|
812
886
|
// Pending changes
|
|
813
887
|
const totalChanges = changes.added.length + changes.modified.length + changes.removed.length;
|
|
814
888
|
if (totalChanges > 0) {
|
|
@@ -885,13 +959,16 @@ function main() {
|
|
|
885
959
|
}
|
|
886
960
|
else {
|
|
887
961
|
console.log(chalk.bold(`\nSearch Results for "${search}":\n`));
|
|
962
|
+
// Results arrive already ranked by relevance, so the order conveys
|
|
963
|
+
// it. We don't print the raw score: it's an unbounded BM25/FTS value
|
|
964
|
+
// (relative-ranking only), and the old `(score * 100)%` rendered it
|
|
965
|
+
// as nonsensical percentages like "12042%" (#1045). The MCP search
|
|
966
|
+
// tool likewise shows no score. Raw `score` stays in --json output.
|
|
888
967
|
for (const result of results) {
|
|
889
968
|
const node = result.node;
|
|
890
969
|
const location = `${node.filePath}:${node.startLine}`;
|
|
891
|
-
const score = chalk.dim(`(${(result.score * 100).toFixed(0)}%)`);
|
|
892
970
|
console.log(chalk.cyan(node.kind.padEnd(12)) +
|
|
893
|
-
chalk.white(node.name)
|
|
894
|
-
' ' + score);
|
|
971
|
+
chalk.white(node.name));
|
|
895
972
|
console.log(chalk.dim(` ${location}`));
|
|
896
973
|
if (node.signature) {
|
|
897
974
|
console.log(chalk.dim(` ${node.signature}`));
|
|
@@ -986,11 +1063,15 @@ function main() {
|
|
|
986
1063
|
return;
|
|
987
1064
|
}
|
|
988
1065
|
const prompt = String(input.prompt || '');
|
|
989
|
-
// Gate: only structural / flow / impact / where-how prompts get context
|
|
990
|
-
//
|
|
991
|
-
//
|
|
992
|
-
|
|
993
|
-
|
|
1066
|
+
// Gate: only structural / flow / impact / where-how prompts get context, so
|
|
1067
|
+
// every other prompt ("fix this typo") stays a zero-cost no-op. Language-aware
|
|
1068
|
+
// (English + CJK keywords, plus code-shaped tokens) so it fires for non-English
|
|
1069
|
+
// prompts too (issue #994). A keyword fires on its own; a code-token is only a
|
|
1070
|
+
// CANDIDATE — verified against the graph below, so a tech brand ("JavaScript")
|
|
1071
|
+
// that looks like a symbol but isn't one here doesn't inject spurious context.
|
|
1072
|
+
const keyworded = (0, directory_1.hasStructuralKeyword)(prompt);
|
|
1073
|
+
const codeTokens = keyworded ? [] : (0, directory_1.extractCodeTokens)(prompt);
|
|
1074
|
+
if (!keyworded && codeTokens.length === 0)
|
|
994
1075
|
return;
|
|
995
1076
|
// Decide what to inject, shaped by WHERE the index(es) are: the nearest
|
|
996
1077
|
// indexed ancestor of cwd, or — when cwd is an un-indexed workspace root
|
|
@@ -1010,6 +1091,13 @@ function main() {
|
|
|
1010
1091
|
const { default: HomeGraph } = await loadHomeGraph();
|
|
1011
1092
|
const cg = await HomeGraph.open(plan.exploreRoot);
|
|
1012
1093
|
try {
|
|
1094
|
+
// Code-token-only prompt: require that at least one token is a REAL symbol
|
|
1095
|
+
// in THIS index before front-loading. Without it, a brand name or common
|
|
1096
|
+
// word that merely looks like code ("JavaScript", "GitHub") would run
|
|
1097
|
+
// explore and inject ~16KB of low-relevance context (issue #994 follow-up).
|
|
1098
|
+
// A keyword-bearing prompt skips this — the keyword is signal enough.
|
|
1099
|
+
if (!keyworded && !codeTokens.some((t) => cg.getNodesByName(t).length > 0))
|
|
1100
|
+
return;
|
|
1013
1101
|
const { ToolHandler } = await Promise.resolve().then(() => __importStar(require('../mcp/tools')));
|
|
1014
1102
|
const handler = new ToolHandler(cg);
|
|
1015
1103
|
const result = await handler.execute('homegraph_explore', { query: prompt });
|
|
@@ -1045,14 +1133,17 @@ function main() {
|
|
|
1045
1133
|
}
|
|
1046
1134
|
});
|
|
1047
1135
|
/**
|
|
1048
|
-
* homegraph node
|
|
1136
|
+
* homegraph node [name]
|
|
1049
1137
|
*
|
|
1050
1138
|
* The CLI face of the MCP homegraph_node tool: one symbol's source +
|
|
1051
1139
|
* caller/callee trail, or a whole file with line numbers + dependents
|
|
1052
1140
|
* (Read-parity). Same subagent/non-MCP rationale as `explore`.
|
|
1141
|
+
*
|
|
1142
|
+
* `name` is OPTIONAL because `--file` (file-read mode) carries no symbol —
|
|
1143
|
+
* a required `<name>` made `codegraph node -f <file>` unreachable (#1044).
|
|
1053
1144
|
*/
|
|
1054
1145
|
program
|
|
1055
|
-
.command('node
|
|
1146
|
+
.command('node [name]')
|
|
1056
1147
|
.description('One symbol\'s source + caller/callee trail, or read a file with line numbers + dependents (same output as the homegraph_node MCP tool)')
|
|
1057
1148
|
.option('-p, --path <path>', 'Project path')
|
|
1058
1149
|
.option('-f, --file <file>', 'Treat as file mode (or disambiguate a symbol to this file)')
|
|
@@ -1060,6 +1151,13 @@ function main() {
|
|
|
1060
1151
|
.option('--limit <number>', 'File mode: maximum lines')
|
|
1061
1152
|
.option('--symbols-only', 'File mode: just the symbol map + dependents')
|
|
1062
1153
|
.action(async (name, options) => {
|
|
1154
|
+
// Need a symbol (positional) OR a file (--file / a path-like positional).
|
|
1155
|
+
// With [name] optional, a bare `codegraph node` reaches here with neither
|
|
1156
|
+
// and must be told what to pass, rather than crashing downstream.
|
|
1157
|
+
if (!name && !options.file) {
|
|
1158
|
+
error("Pass a symbol name (e.g. 'codegraph node parseToken') or a file (e.g. 'codegraph node -f src/auth.ts', or 'codegraph node src/auth.ts').");
|
|
1159
|
+
process.exit(1);
|
|
1160
|
+
}
|
|
1063
1161
|
const projectPath = resolveProjectPath(options.path);
|
|
1064
1162
|
try {
|
|
1065
1163
|
if (!(0, directory_1.isInitialized)(projectPath)) {
|
|
@@ -1081,10 +1179,10 @@ function main() {
|
|
|
1081
1179
|
if (name && name !== options.file)
|
|
1082
1180
|
args.symbol = name;
|
|
1083
1181
|
}
|
|
1084
|
-
else if (name.includes('/') || name.includes('\\')) {
|
|
1182
|
+
else if (name && (name.includes('/') || name.includes('\\'))) {
|
|
1085
1183
|
args.file = name.replace(/\\/g, '/');
|
|
1086
1184
|
}
|
|
1087
|
-
else {
|
|
1185
|
+
else if (name) {
|
|
1088
1186
|
args.symbol = name;
|
|
1089
1187
|
args.includeCode = true;
|
|
1090
1188
|
}
|
|
@@ -1393,15 +1491,15 @@ function main() {
|
|
|
1393
1491
|
console.error(chalk.bold('\nHomeGraph MCP Server\n'));
|
|
1394
1492
|
console.error(chalk.blue((0, glyphs_1.getGlyphs)().info) + ' Use --mcp flag to start the MCP server');
|
|
1395
1493
|
console.error('\nTo use with Claude Code, add to your MCP configuration:');
|
|
1396
|
-
console.error(chalk.dim(`
|
|
1397
|
-
{
|
|
1398
|
-
"mcpServers": {
|
|
1399
|
-
"homegraph": {
|
|
1400
|
-
"command": "homegraph",
|
|
1401
|
-
"args": ["serve", "--mcp"]
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1494
|
+
console.error(chalk.dim(`
|
|
1495
|
+
{
|
|
1496
|
+
"mcpServers": {
|
|
1497
|
+
"homegraph": {
|
|
1498
|
+
"command": "homegraph",
|
|
1499
|
+
"args": ["serve", "--mcp"]
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1405
1503
|
`));
|
|
1406
1504
|
console.error('Available tools:');
|
|
1407
1505
|
console.error(chalk.cyan(' homegraph_explore') + ' - Primary: source of the relevant symbols for any question');
|
|
@@ -1903,6 +2001,751 @@ function main() {
|
|
|
1903
2001
|
process.exit(1);
|
|
1904
2002
|
}
|
|
1905
2003
|
});
|
|
2004
|
+
// =============================================================================
|
|
2005
|
+
// Spec commands (Commit4Spec knowledge graph)
|
|
2006
|
+
// =============================================================================
|
|
2007
|
+
const specCommand = program
|
|
2008
|
+
.command('spec')
|
|
2009
|
+
.description('Manage spec knowledge graph (reverse-mining, search, self-evolve)');
|
|
2010
|
+
/**
|
|
2011
|
+
* homegraph spec mine
|
|
2012
|
+
*/
|
|
2013
|
+
specCommand
|
|
2014
|
+
.command('mine')
|
|
2015
|
+
.description('Reverse-mine spec knowledge from Git history')
|
|
2016
|
+
.option('-p, --path <path>', 'Path to the repository')
|
|
2017
|
+
.option('--spec-storage-path <path>', 'Path to the .spec directory')
|
|
2018
|
+
.option('--db-path <path>', 'Path to the SQLite database file')
|
|
2019
|
+
.option('-v, --verbose', 'Show detailed output')
|
|
2020
|
+
.option('-j, --json', 'Output as JSON')
|
|
2021
|
+
.action(async (options) => {
|
|
2022
|
+
try {
|
|
2023
|
+
const repoPath = path.resolve(options.path || process.cwd());
|
|
2024
|
+
const specStoragePath = options.specStoragePath ?? path.join(repoPath, '.spec');
|
|
2025
|
+
const { createDatabase } = await Promise.resolve().then(() => __importStar(require('../db/sqlite-adapter')));
|
|
2026
|
+
const { runMiningPipeline } = await Promise.resolve().then(() => __importStar(require('../spec/mining/pipeline')));
|
|
2027
|
+
const { isGitRepo } = await Promise.resolve().then(() => __importStar(require('../spec/mining/git-scanner')));
|
|
2028
|
+
const { resolveDbPath } = await Promise.resolve().then(() => __importStar(require('../spec/utils')));
|
|
2029
|
+
const dbPath = resolveDbPath(repoPath, options.dbPath);
|
|
2030
|
+
if (!isGitRepo(repoPath)) {
|
|
2031
|
+
error(`Not a git repository: ${repoPath}`);
|
|
2032
|
+
process.exit(1);
|
|
2033
|
+
}
|
|
2034
|
+
if (options.verbose) {
|
|
2035
|
+
info(`Repository: ${repoPath}`);
|
|
2036
|
+
info(`Spec storage: ${specStoragePath}`);
|
|
2037
|
+
info(`Database: ${dbPath}`);
|
|
2038
|
+
}
|
|
2039
|
+
const { db } = createDatabase(dbPath);
|
|
2040
|
+
const result = runMiningPipeline(repoPath, specStoragePath, db);
|
|
2041
|
+
if (options.json) {
|
|
2042
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2043
|
+
}
|
|
2044
|
+
else {
|
|
2045
|
+
info(`Specs found: ${result.specsFound}`);
|
|
2046
|
+
info(`Commits found: ${result.commitsFound}`);
|
|
2047
|
+
info(`Fragments: ${result.fragmentsFound}`);
|
|
2048
|
+
info(`Relations: ${result.relationsCreated}`);
|
|
2049
|
+
info(`Total entries: ${result.totalEntries}`);
|
|
2050
|
+
info(`Skipped: ${result.skippedEntries.length}`);
|
|
2051
|
+
if (result.skippedEntries.length > 0) {
|
|
2052
|
+
for (const entry of result.skippedEntries) {
|
|
2053
|
+
warn(`${entry.specId}: ${entry.reason}`);
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
catch (err) {
|
|
2059
|
+
error(`Mining failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2060
|
+
process.exit(1);
|
|
2061
|
+
}
|
|
2062
|
+
});
|
|
2063
|
+
/**
|
|
2064
|
+
* homegraph spec match <text>
|
|
2065
|
+
*/
|
|
2066
|
+
specCommand
|
|
2067
|
+
.command('match <text>')
|
|
2068
|
+
.description('Search the spec knowledge graph for a term or phrase')
|
|
2069
|
+
.option('-p, --path <path>', 'Path to the repository')
|
|
2070
|
+
.option('--db-path <path>', 'Path to the SQLite database file')
|
|
2071
|
+
.option('--top-k <number>', 'Number of results to return', '5')
|
|
2072
|
+
.option('--no-fragments', 'Exclude code fragments from results')
|
|
2073
|
+
.option('-j, --json', 'Output as JSON')
|
|
2074
|
+
.action(async (text, options) => {
|
|
2075
|
+
let db;
|
|
2076
|
+
try {
|
|
2077
|
+
const repoPath = resolveSpecProjectPath(options.path);
|
|
2078
|
+
const { createDatabase } = await Promise.resolve().then(() => __importStar(require('../db/sqlite-adapter')));
|
|
2079
|
+
const { resolveDbPath, computeBudgetProfile } = await Promise.resolve().then(() => __importStar(require('../spec/utils')));
|
|
2080
|
+
const { searchAndGetContext } = await Promise.resolve().then(() => __importStar(require('../spec/graph/queries')));
|
|
2081
|
+
const dbPath = resolveDbPath(repoPath, options.dbPath);
|
|
2082
|
+
if (!fs.existsSync(dbPath)) {
|
|
2083
|
+
error(`Database not found at ${dbPath}. Run 'homegraph spec mine' first.`);
|
|
2084
|
+
process.exit(1);
|
|
2085
|
+
}
|
|
2086
|
+
const created = createDatabase(dbPath);
|
|
2087
|
+
db = created.db;
|
|
2088
|
+
const topK = Math.max(1, Math.min(parseInt(options.topK || '5', 10) || 5, 50));
|
|
2089
|
+
const includeFragments = options.fragments !== false;
|
|
2090
|
+
const contexts = searchAndGetContext(db, text, topK, includeFragments);
|
|
2091
|
+
if (contexts.length === 0) {
|
|
2092
|
+
info('No specs matched your query.');
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
const profile = computeBudgetProfile(contexts.length);
|
|
2096
|
+
const results = contexts.map((ctx) => ({
|
|
2097
|
+
spec_id: ctx.spec.id,
|
|
2098
|
+
title: ctx.spec.title,
|
|
2099
|
+
subtitles: ctx.spec.subtitles,
|
|
2100
|
+
file_path: ctx.spec.filePath,
|
|
2101
|
+
commits: ctx.commits.map((c) => ({
|
|
2102
|
+
hash: c.commit.hash,
|
|
2103
|
+
message: c.commit.message,
|
|
2104
|
+
relation_type: c.relationType,
|
|
2105
|
+
fragments: c.fragments.map((f) => ({
|
|
2106
|
+
file_path: f.filePath,
|
|
2107
|
+
change_type: f.changeType,
|
|
2108
|
+
start_line: f.startLine,
|
|
2109
|
+
end_line: f.endLine,
|
|
2110
|
+
code_diff: f.codeDiff,
|
|
2111
|
+
})),
|
|
2112
|
+
})),
|
|
2113
|
+
}));
|
|
2114
|
+
if (options.json) {
|
|
2115
|
+
console.log(JSON.stringify({
|
|
2116
|
+
query: text.slice(0, 200),
|
|
2117
|
+
matched_count: contexts.length,
|
|
2118
|
+
results,
|
|
2119
|
+
}, null, 2));
|
|
2120
|
+
}
|
|
2121
|
+
else {
|
|
2122
|
+
console.log(chalk.bold(`\n${contexts.length} spec${contexts.length !== 1 ? 's' : ''} matched:\n`));
|
|
2123
|
+
for (const r of results) {
|
|
2124
|
+
console.log(chalk.bold(r.title));
|
|
2125
|
+
for (const commit of r.commits) {
|
|
2126
|
+
console.log(` ${chalk.yellow(commit.hash.slice(0, 7))} ${commit.message}`);
|
|
2127
|
+
if (includeFragments && commit.fragments && commit.fragments.length > 0) {
|
|
2128
|
+
for (const fragment of commit.fragments.slice(0, profile.maxFragments || 3)) {
|
|
2129
|
+
console.log(chalk.dim(` ${fragment.file_path}:${fragment.start_line}-${fragment.end_line} [${fragment.change_type}]`));
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
console.log();
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
catch (err) {
|
|
2138
|
+
error(`Match failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2139
|
+
process.exit(1);
|
|
2140
|
+
}
|
|
2141
|
+
finally {
|
|
2142
|
+
try {
|
|
2143
|
+
db?.close();
|
|
2144
|
+
}
|
|
2145
|
+
catch { /* best effort */ }
|
|
2146
|
+
}
|
|
2147
|
+
});
|
|
2148
|
+
/**
|
|
2149
|
+
* homegraph spec find <filePath>
|
|
2150
|
+
*/
|
|
2151
|
+
specCommand
|
|
2152
|
+
.command('find <filePath>')
|
|
2153
|
+
.description('Find specs related to the given file path via code-fragment matching')
|
|
2154
|
+
.option('-p, --path <path>', 'Path to the repository')
|
|
2155
|
+
.option('--db-path <path>', 'Path to the SQLite database file')
|
|
2156
|
+
.option('-j, --json', 'Output as JSON')
|
|
2157
|
+
.action(async (filePath, options) => {
|
|
2158
|
+
let db;
|
|
2159
|
+
try {
|
|
2160
|
+
const repoPath = resolveSpecProjectPath(options.path);
|
|
2161
|
+
const { createDatabase } = await Promise.resolve().then(() => __importStar(require('../db/sqlite-adapter')));
|
|
2162
|
+
const { resolveDbPath } = await Promise.resolve().then(() => __importStar(require('../spec/utils')));
|
|
2163
|
+
const { findSpecsByFilePath } = await Promise.resolve().then(() => __importStar(require('../spec/graph/queries')));
|
|
2164
|
+
const dbPath = resolveDbPath(repoPath, options.dbPath);
|
|
2165
|
+
if (!fs.existsSync(dbPath)) {
|
|
2166
|
+
error(`Database not found at ${dbPath}. Run 'homegraph spec mine' first.`);
|
|
2167
|
+
process.exit(1);
|
|
2168
|
+
}
|
|
2169
|
+
const created = createDatabase(dbPath);
|
|
2170
|
+
db = created.db;
|
|
2171
|
+
const result = findSpecsByFilePath(db, filePath);
|
|
2172
|
+
if (options.json) {
|
|
2173
|
+
console.log(JSON.stringify({
|
|
2174
|
+
filePath,
|
|
2175
|
+
matched_count: result.matched_count,
|
|
2176
|
+
truncated: result.truncated,
|
|
2177
|
+
results: result.results,
|
|
2178
|
+
}, null, 2));
|
|
2179
|
+
}
|
|
2180
|
+
else {
|
|
2181
|
+
console.log(chalk.bold(`\n${result.matched_count} spec${result.matched_count !== 1 ? 's' : ''} matched for ${filePath}:\n`));
|
|
2182
|
+
for (const r of result.results) {
|
|
2183
|
+
console.log(` ${chalk.cyan(r.id.padEnd(16))} ${r.title.padEnd(32)} ${chalk.green(r.status.padEnd(12))} v${r.version} ${chalk.dim(r.filePath)}`);
|
|
2184
|
+
}
|
|
2185
|
+
console.log();
|
|
2186
|
+
if (result.truncated) {
|
|
2187
|
+
console.log(chalk.yellow(` ... and more (showing first ${result.matched_count} of >${result.matched_count} results)`));
|
|
2188
|
+
console.log();
|
|
2189
|
+
}
|
|
2190
|
+
if (result.matched_count === 0) {
|
|
2191
|
+
info(`No specs found for file path "${filePath}". Try a partial path (e.g. "src/auth" instead of "src/auth/login.ts").`);
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
catch (err) {
|
|
2196
|
+
error(`Find failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2197
|
+
process.exit(1);
|
|
2198
|
+
}
|
|
2199
|
+
finally {
|
|
2200
|
+
try {
|
|
2201
|
+
db?.close();
|
|
2202
|
+
}
|
|
2203
|
+
catch { /* best effort */ }
|
|
2204
|
+
}
|
|
2205
|
+
});
|
|
2206
|
+
/**
|
|
2207
|
+
* homegraph spec trace <symbol>
|
|
2208
|
+
*/
|
|
2209
|
+
specCommand
|
|
2210
|
+
.command('trace <symbol>')
|
|
2211
|
+
.description('Trace a code symbol back to its associated design Specs')
|
|
2212
|
+
.option('-f, --file <path>', 'File path for symbol disambiguation')
|
|
2213
|
+
.option('-l, --line <number>', 'Line number for symbol disambiguation')
|
|
2214
|
+
.option('-p, --path <path>', 'Path to the repository')
|
|
2215
|
+
.option('--db-path <path>', 'Path to the SQLite database file')
|
|
2216
|
+
.option('--top-k <number>', 'Number of results to return', '10')
|
|
2217
|
+
.option('-j, --json', 'Output as JSON')
|
|
2218
|
+
.action(async (symbol, options) => {
|
|
2219
|
+
let db;
|
|
2220
|
+
let cg;
|
|
2221
|
+
try {
|
|
2222
|
+
const repoPath = resolveSpecProjectPath(options.path);
|
|
2223
|
+
// isNaN check so --line 0 is handled correctly (parseInt('0') → 0 is falsy)
|
|
2224
|
+
const lineRaw = options.line !== undefined ? parseInt(options.line, 10) : NaN;
|
|
2225
|
+
const line = isNaN(lineRaw) ? undefined : lineRaw;
|
|
2226
|
+
// Open HomeGraph code index
|
|
2227
|
+
const { default: HomeGraph } = await Promise.resolve().then(() => __importStar(require('../index')));
|
|
2228
|
+
cg = await HomeGraph.open(repoPath);
|
|
2229
|
+
// Inline symbol resolution (mirrors findSymbolMatches in tools.ts)
|
|
2230
|
+
const isQualified = /[.\/]|::/.test(symbol);
|
|
2231
|
+
const { isGeneratedFile } = await Promise.resolve().then(() => __importStar(require('../extraction/generated-detection')));
|
|
2232
|
+
let nodes = [];
|
|
2233
|
+
if (!isQualified) {
|
|
2234
|
+
const exact = cg.getNodesByName(symbol);
|
|
2235
|
+
if (exact.length > 0) {
|
|
2236
|
+
nodes = [...exact].sort((a, b) => (isGeneratedFile(a.filePath) ? 1 : 0) - (isGeneratedFile(b.filePath) ? 1 : 0));
|
|
2237
|
+
}
|
|
2238
|
+
else {
|
|
2239
|
+
const fuzzy = cg.searchNodes(symbol, { limit: 10 });
|
|
2240
|
+
if (fuzzy[0])
|
|
2241
|
+
nodes = [fuzzy[0].node];
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
else {
|
|
2245
|
+
let results = cg.searchNodes(symbol, { limit: 50 });
|
|
2246
|
+
if (results.length === 0) {
|
|
2247
|
+
const parts = symbol.split(/::|[./]/).filter((p) => p.length > 0);
|
|
2248
|
+
const tail = parts[parts.length - 1];
|
|
2249
|
+
if (tail && tail !== symbol)
|
|
2250
|
+
results = cg.searchNodes(tail, { limit: 50 });
|
|
2251
|
+
}
|
|
2252
|
+
// Rough matchesSymbol equivalent for CLI
|
|
2253
|
+
const exactMatches = results.filter((r) => {
|
|
2254
|
+
const n = r.node;
|
|
2255
|
+
if (n.name === symbol)
|
|
2256
|
+
return true;
|
|
2257
|
+
const fileBase = n.filePath.split('/').pop()?.replace(/\.[^.]+$/, '');
|
|
2258
|
+
if (fileBase === symbol)
|
|
2259
|
+
return true;
|
|
2260
|
+
const parts = symbol.split(/::|[./]/);
|
|
2261
|
+
if (parts[parts.length - 1] === n.name) {
|
|
2262
|
+
const firstPart = parts[0];
|
|
2263
|
+
if (n.qualifiedName.includes(firstPart))
|
|
2264
|
+
return true;
|
|
2265
|
+
if (n.filePath.includes(firstPart))
|
|
2266
|
+
return true;
|
|
2267
|
+
return parts.length === 1;
|
|
2268
|
+
}
|
|
2269
|
+
return false;
|
|
2270
|
+
});
|
|
2271
|
+
if (exactMatches.length > 0) {
|
|
2272
|
+
nodes = exactMatches
|
|
2273
|
+
.sort((a, b) => (isGeneratedFile(a.node.filePath) ? 1 : 0) - (isGeneratedFile(b.node.filePath) ? 1 : 0))
|
|
2274
|
+
.map((r) => r.node);
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
if (nodes.length === 0) {
|
|
2278
|
+
console.log(chalk.yellow(`No code entities found for symbol "${symbol}".`));
|
|
2279
|
+
return;
|
|
2280
|
+
}
|
|
2281
|
+
// Disambiguate
|
|
2282
|
+
if (options.file) {
|
|
2283
|
+
// Use endsWith for precise file name matching (not substring matching)
|
|
2284
|
+
const filePattern = options.file;
|
|
2285
|
+
nodes = nodes.filter((n) => n.filePath.endsWith(filePattern));
|
|
2286
|
+
}
|
|
2287
|
+
if (line !== undefined && nodes.length > 1) {
|
|
2288
|
+
const closest = nodes.reduce((best, n) => {
|
|
2289
|
+
const bestDist = Math.abs(best.startLine - line) + Math.abs(best.endLine - line);
|
|
2290
|
+
const curDist = Math.abs(n.startLine - line) + Math.abs(n.endLine - line);
|
|
2291
|
+
return curDist < bestDist ? n : best;
|
|
2292
|
+
});
|
|
2293
|
+
nodes = [closest];
|
|
2294
|
+
}
|
|
2295
|
+
const node = nodes[0];
|
|
2296
|
+
if (!node) {
|
|
2297
|
+
console.log(chalk.yellow(`Could not resolve symbol "${symbol}" to a specific code entity.`));
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
2300
|
+
// Open Spec database
|
|
2301
|
+
const { createDatabase } = await Promise.resolve().then(() => __importStar(require('../db/sqlite-adapter')));
|
|
2302
|
+
const { resolveDbPath } = await Promise.resolve().then(() => __importStar(require('../spec/utils')));
|
|
2303
|
+
const { findSpecsByCodeSymbol } = await Promise.resolve().then(() => __importStar(require('../spec/graph/queries')));
|
|
2304
|
+
const { initSpecSchema, runSpecMigrations, getCurrentSpecVersion, CURRENT_SPEC_SCHEMA_VERSION } = await Promise.resolve().then(() => __importStar(require('../spec/db/schema')));
|
|
2305
|
+
const dbPath = resolveDbPath(repoPath, options.dbPath);
|
|
2306
|
+
if (!fs.existsSync(dbPath)) {
|
|
2307
|
+
console.log(chalk.yellow(`Database not found at ${dbPath}. Run 'homegraph spec mine' first.`));
|
|
2308
|
+
return;
|
|
2309
|
+
}
|
|
2310
|
+
const created = createDatabase(dbPath);
|
|
2311
|
+
db = created.db;
|
|
2312
|
+
initSpecSchema(db);
|
|
2313
|
+
const currentVersion = getCurrentSpecVersion(db);
|
|
2314
|
+
if (currentVersion < CURRENT_SPEC_SCHEMA_VERSION) {
|
|
2315
|
+
runSpecMigrations(db, currentVersion);
|
|
2316
|
+
}
|
|
2317
|
+
const topKRaw = parseInt(options.topK || '10', 10);
|
|
2318
|
+
const topK = Math.max(1, Math.min(isNaN(topKRaw) ? 10 : topKRaw, 50));
|
|
2319
|
+
console.log(chalk.dim(`Resolved: ${node.name} (${node.kind}) in ${node.filePath}:${node.startLine}-${node.endLine}`));
|
|
2320
|
+
const result = findSpecsByCodeSymbol(db, {
|
|
2321
|
+
name: node.name,
|
|
2322
|
+
qualifiedName: node.qualifiedName,
|
|
2323
|
+
kind: node.kind,
|
|
2324
|
+
filePath: node.filePath,
|
|
2325
|
+
startLine: node.startLine,
|
|
2326
|
+
endLine: node.endLine,
|
|
2327
|
+
}, topK);
|
|
2328
|
+
if (options.json) {
|
|
2329
|
+
console.log(JSON.stringify({
|
|
2330
|
+
symbol,
|
|
2331
|
+
entity: {
|
|
2332
|
+
name: node.name,
|
|
2333
|
+
qualifiedName: node.qualifiedName,
|
|
2334
|
+
kind: node.kind,
|
|
2335
|
+
filePath: node.filePath,
|
|
2336
|
+
startLine: node.startLine,
|
|
2337
|
+
endLine: node.endLine,
|
|
2338
|
+
},
|
|
2339
|
+
matched_count: result.matches.length,
|
|
2340
|
+
total_candidates: result.totalCandidates,
|
|
2341
|
+
matches: result.matches.map((m) => ({
|
|
2342
|
+
spec_id: m.spec.id,
|
|
2343
|
+
title: m.spec.title,
|
|
2344
|
+
status: m.spec.status,
|
|
2345
|
+
version: m.spec.version,
|
|
2346
|
+
file_path: m.spec.filePath,
|
|
2347
|
+
score: Math.round(m.score * 1000) / 1000,
|
|
2348
|
+
score_detail: {
|
|
2349
|
+
file_path: Math.round(m.scoreDetail.filePathScore * 1000) / 1000,
|
|
2350
|
+
content: Math.round(m.scoreDetail.contentScore * 1000) / 1000,
|
|
2351
|
+
name: Math.round(m.scoreDetail.nameScore * 1000) / 1000,
|
|
2352
|
+
recency: Math.round(m.scoreDetail.recencyScore * 1000) / 1000,
|
|
2353
|
+
line_overlap: Math.round(m.scoreDetail.overlapScore * 1000) / 1000,
|
|
2354
|
+
},
|
|
2355
|
+
fragment_count: m.fragmentCount,
|
|
2356
|
+
commit_count: m.commitCount,
|
|
2357
|
+
})),
|
|
2358
|
+
}, null, 2));
|
|
2359
|
+
}
|
|
2360
|
+
else {
|
|
2361
|
+
console.log(chalk.bold(`\n${result.matches.length} spec${result.matches.length !== 1 ? 's' : ''} matched (${result.totalCandidates} candidates):\n`));
|
|
2362
|
+
for (const m of result.matches) {
|
|
2363
|
+
const scoreColor = m.score >= 0.7 ? chalk.green : m.score >= 0.4 ? chalk.yellow : chalk.red;
|
|
2364
|
+
console.log(` ${chalk.cyan(m.spec.id.padEnd(14))} ${m.spec.title.padEnd(36)} ${scoreColor((m.score * 100).toFixed(1) + '%')}`);
|
|
2365
|
+
console.log(chalk.dim(` fp=${m.scoreDetail.filePathScore.toFixed(2)} content=${m.scoreDetail.contentScore.toFixed(2)} name=${m.scoreDetail.nameScore.toFixed(2)} recency=${m.scoreDetail.recencyScore.toFixed(2)} overlap=${m.scoreDetail.overlapScore.toFixed(2)}`));
|
|
2366
|
+
console.log();
|
|
2367
|
+
}
|
|
2368
|
+
if (result.matches.length === 0) {
|
|
2369
|
+
info(`No Specs found for symbol "${symbol}". Try a more specific file path or check that specs have been mined for this project.`);
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
catch (err) {
|
|
2374
|
+
error(`Trace failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2375
|
+
process.exit(1);
|
|
2376
|
+
}
|
|
2377
|
+
finally {
|
|
2378
|
+
try {
|
|
2379
|
+
await cg?.close();
|
|
2380
|
+
}
|
|
2381
|
+
catch { /* best effort */ }
|
|
2382
|
+
try {
|
|
2383
|
+
db?.close();
|
|
2384
|
+
}
|
|
2385
|
+
catch { /* best effort */ }
|
|
2386
|
+
}
|
|
2387
|
+
});
|
|
2388
|
+
/**
|
|
2389
|
+
* homegraph spec stats
|
|
2390
|
+
*/
|
|
2391
|
+
specCommand
|
|
2392
|
+
.command('stats')
|
|
2393
|
+
.description('Show statistics about the spec knowledge graph')
|
|
2394
|
+
.option('-p, --path <path>', 'Path to the repository')
|
|
2395
|
+
.option('--db-path <path>', 'Path to the SQLite database file')
|
|
2396
|
+
.option('-j, --json', 'Output as JSON')
|
|
2397
|
+
.action(async (options) => {
|
|
2398
|
+
let db;
|
|
2399
|
+
try {
|
|
2400
|
+
const repoPath = resolveSpecProjectPath(options.path);
|
|
2401
|
+
const { createDatabase } = await Promise.resolve().then(() => __importStar(require('../db/sqlite-adapter')));
|
|
2402
|
+
const { resolveDbPath } = await Promise.resolve().then(() => __importStar(require('../spec/utils')));
|
|
2403
|
+
const { getSpecStats } = await Promise.resolve().then(() => __importStar(require('../spec/graph/queries')));
|
|
2404
|
+
const dbPath = resolveDbPath(repoPath, options.dbPath);
|
|
2405
|
+
if (!fs.existsSync(dbPath)) {
|
|
2406
|
+
error(`Database not found at ${dbPath}. Run 'homegraph spec mine' first.`);
|
|
2407
|
+
process.exit(1);
|
|
2408
|
+
}
|
|
2409
|
+
const created = createDatabase(dbPath);
|
|
2410
|
+
db = created.db;
|
|
2411
|
+
const stats = getSpecStats(db);
|
|
2412
|
+
if (options.json) {
|
|
2413
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
2414
|
+
}
|
|
2415
|
+
else {
|
|
2416
|
+
console.log(chalk.bold('\nSpec Knowledge Graph Stats\n'));
|
|
2417
|
+
console.log(` Specs: ${chalk.cyan(String(stats.specCount))}`);
|
|
2418
|
+
console.log(` Active: ${chalk.green(String(stats.activeSpecCount))}`);
|
|
2419
|
+
console.log(` Deprecated: ${chalk.yellow(String(stats.deprecatedSpecCount))}`);
|
|
2420
|
+
console.log(` Commits: ${stats.commitCount}`);
|
|
2421
|
+
console.log(` Fragments: ${stats.fragmentCount}`);
|
|
2422
|
+
console.log(` Relations: ${stats.relationCount}`);
|
|
2423
|
+
console.log();
|
|
2424
|
+
if (stats.specCount === 0) {
|
|
2425
|
+
info("No specs yet. Run 'homegraph spec mine' to build the knowledge graph.");
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
catch (err) {
|
|
2430
|
+
error(`Stats failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2431
|
+
process.exit(1);
|
|
2432
|
+
}
|
|
2433
|
+
finally {
|
|
2434
|
+
try {
|
|
2435
|
+
db?.close();
|
|
2436
|
+
}
|
|
2437
|
+
catch { /* best effort */ }
|
|
2438
|
+
}
|
|
2439
|
+
});
|
|
2440
|
+
/**
|
|
2441
|
+
* homegraph spec evolve
|
|
2442
|
+
*/
|
|
2443
|
+
const evolveCommand = specCommand
|
|
2444
|
+
.command('evolve')
|
|
2445
|
+
.description('Self-evolve specs after code changes');
|
|
2446
|
+
/**
|
|
2447
|
+
* homegraph spec evolve install
|
|
2448
|
+
*/
|
|
2449
|
+
evolveCommand
|
|
2450
|
+
.command('install')
|
|
2451
|
+
.description('Install a git post-commit hook that triggers spec evolution')
|
|
2452
|
+
.option('-p, --path <path>', 'Path to the repository')
|
|
2453
|
+
.option('-f, --force', 'Overwrite existing hook without prompting')
|
|
2454
|
+
.action(async (options) => {
|
|
2455
|
+
try {
|
|
2456
|
+
const repoPath = resolveSpecProjectPath(options.path);
|
|
2457
|
+
const { isGitRepo } = await Promise.resolve().then(() => __importStar(require('../spec/mining/git-scanner')));
|
|
2458
|
+
if (!isGitRepo(repoPath)) {
|
|
2459
|
+
error(`Not a git repository: ${repoPath}`);
|
|
2460
|
+
process.exit(1);
|
|
2461
|
+
}
|
|
2462
|
+
const { execFileSync } = await Promise.resolve().then(() => __importStar(require('child_process')));
|
|
2463
|
+
// Resolve the absolute path to homegraph so the hook works
|
|
2464
|
+
// regardless of PATH (matching Python's shutil.which("c4s")).
|
|
2465
|
+
let homegraphBin;
|
|
2466
|
+
try {
|
|
2467
|
+
homegraphBin = execFileSync('command', ['-v', 'homegraph'], {
|
|
2468
|
+
encoding: 'utf8',
|
|
2469
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
2470
|
+
}).trim();
|
|
2471
|
+
}
|
|
2472
|
+
catch {
|
|
2473
|
+
homegraphBin = '';
|
|
2474
|
+
}
|
|
2475
|
+
if (!homegraphBin) {
|
|
2476
|
+
error('homegraph command not found in PATH.\n' +
|
|
2477
|
+
' Make sure homegraph is installed globally: npm install -g homegraph');
|
|
2478
|
+
process.exit(1);
|
|
2479
|
+
}
|
|
2480
|
+
const hooksDir = execFileSync('git', ['rev-parse', '--git-path', 'hooks'], {
|
|
2481
|
+
cwd: repoPath,
|
|
2482
|
+
encoding: 'utf8',
|
|
2483
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
2484
|
+
windowsHide: true,
|
|
2485
|
+
}).trim();
|
|
2486
|
+
const resolvedHooksDir = path.isAbsolute(hooksDir)
|
|
2487
|
+
? hooksDir
|
|
2488
|
+
: path.resolve(repoPath, hooksDir);
|
|
2489
|
+
fs.mkdirSync(resolvedHooksDir, { recursive: true });
|
|
2490
|
+
const hookPath = path.join(resolvedHooksDir, 'post-commit');
|
|
2491
|
+
if (fs.existsSync(hookPath) && !options.force) {
|
|
2492
|
+
const readline = await Promise.resolve().then(() => __importStar(require('readline')));
|
|
2493
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
2494
|
+
const answer = await new Promise((resolve) => {
|
|
2495
|
+
rl.question(chalk.yellow('A post-commit hook already exists. Overwrite? (y/N) '), resolve);
|
|
2496
|
+
});
|
|
2497
|
+
rl.close();
|
|
2498
|
+
if (answer.toLowerCase() !== 'y') {
|
|
2499
|
+
info('Cancelled');
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
// Ensure logs directory
|
|
2504
|
+
fs.mkdirSync(path.join(repoPath, utils_1.SPEC_DATA_DIR, 'logs'), { recursive: true });
|
|
2505
|
+
// Marker constants matching the git-hooks pattern
|
|
2506
|
+
const MARKER_BEGIN = '# >>> homegraph spec evolve hook >>>';
|
|
2507
|
+
const MARKER_END = '# <<< homegraph spec evolve hook <<<';
|
|
2508
|
+
const hookBlock = [
|
|
2509
|
+
MARKER_BEGIN,
|
|
2510
|
+
'# Triggers spec self-evolution after each commit. Runs in background.',
|
|
2511
|
+
'# Installed by: homegraph spec evolve install',
|
|
2512
|
+
`# Logs: ${utils_1.SPEC_DATA_DIR}/logs/evolve-hook.log`,
|
|
2513
|
+
'# Runtime guard: skip if homegraph is not available',
|
|
2514
|
+
`HOMEGRAPH_BIN="${homegraphBin}"`,
|
|
2515
|
+
'if [ -x "$HOMEGRAPH_BIN" ] || command -v homegraph >/dev/null 2>&1; then',
|
|
2516
|
+
' "${HOMEGRAPH_BIN:-homegraph}" spec evolve process --path "$(pwd)" --json \\',
|
|
2517
|
+
` >> ${utils_1.SPEC_DATA_DIR}/logs/evolve-hook.log 2>&1 &`,
|
|
2518
|
+
'fi',
|
|
2519
|
+
MARKER_END,
|
|
2520
|
+
].join('\n');
|
|
2521
|
+
let content;
|
|
2522
|
+
if (fs.existsSync(hookPath)) {
|
|
2523
|
+
// Strip any prior marker block, then re-append the current one
|
|
2524
|
+
const existing = fs.readFileSync(hookPath, 'utf8');
|
|
2525
|
+
const lines = existing.split('\n');
|
|
2526
|
+
const kept = [];
|
|
2527
|
+
let inBlock = false;
|
|
2528
|
+
for (const line of lines) {
|
|
2529
|
+
const trimmed = line.trim();
|
|
2530
|
+
if (trimmed === MARKER_BEGIN) {
|
|
2531
|
+
inBlock = true;
|
|
2532
|
+
continue;
|
|
2533
|
+
}
|
|
2534
|
+
if (trimmed === MARKER_END) {
|
|
2535
|
+
inBlock = false;
|
|
2536
|
+
continue;
|
|
2537
|
+
}
|
|
2538
|
+
if (!inBlock)
|
|
2539
|
+
kept.push(line);
|
|
2540
|
+
}
|
|
2541
|
+
const base = kept.join('\n').replace(/\s*$/, '');
|
|
2542
|
+
content = base.length > 0
|
|
2543
|
+
? `${base}\n\n${hookBlock}\n`
|
|
2544
|
+
: `#!/bin/sh\n${hookBlock}\n`;
|
|
2545
|
+
}
|
|
2546
|
+
else {
|
|
2547
|
+
content = `#!/bin/sh\n${hookBlock}\n`;
|
|
2548
|
+
}
|
|
2549
|
+
fs.writeFileSync(hookPath, content);
|
|
2550
|
+
fs.chmodSync(hookPath, 0o755);
|
|
2551
|
+
success(`Post-commit hook installed at ${hookPath}`);
|
|
2552
|
+
info(`Logs written to ${utils_1.SPEC_DATA_DIR}/logs/evolve-hook.log`);
|
|
2553
|
+
}
|
|
2554
|
+
catch (err) {
|
|
2555
|
+
error(`Hook install failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2556
|
+
process.exit(1);
|
|
2557
|
+
}
|
|
2558
|
+
});
|
|
2559
|
+
/**
|
|
2560
|
+
* homegraph spec evolve uninstall
|
|
2561
|
+
*/
|
|
2562
|
+
evolveCommand
|
|
2563
|
+
.command('uninstall')
|
|
2564
|
+
.description('Remove the git post-commit hook installed by spec evolve')
|
|
2565
|
+
.option('-p, --path <path>', 'Path to the repository')
|
|
2566
|
+
.action(async (options) => {
|
|
2567
|
+
try {
|
|
2568
|
+
const repoPath = resolveSpecProjectPath(options.path);
|
|
2569
|
+
const { isGitRepo } = await Promise.resolve().then(() => __importStar(require('../spec/mining/git-scanner')));
|
|
2570
|
+
if (!isGitRepo(repoPath)) {
|
|
2571
|
+
error(`Not a git repository: ${repoPath}`);
|
|
2572
|
+
process.exit(1);
|
|
2573
|
+
}
|
|
2574
|
+
const { execFileSync } = await Promise.resolve().then(() => __importStar(require('child_process')));
|
|
2575
|
+
const hooksDir = execFileSync('git', ['rev-parse', '--git-path', 'hooks'], {
|
|
2576
|
+
cwd: repoPath,
|
|
2577
|
+
encoding: 'utf8',
|
|
2578
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
2579
|
+
windowsHide: true,
|
|
2580
|
+
}).trim();
|
|
2581
|
+
const resolvedHooksDir = path.isAbsolute(hooksDir)
|
|
2582
|
+
? hooksDir
|
|
2583
|
+
: path.resolve(repoPath, hooksDir);
|
|
2584
|
+
const hookPath = path.join(resolvedHooksDir, 'post-commit');
|
|
2585
|
+
if (!fs.existsSync(hookPath)) {
|
|
2586
|
+
info('No post-commit hook found — nothing to uninstall.');
|
|
2587
|
+
return;
|
|
2588
|
+
}
|
|
2589
|
+
const MARKER_BEGIN = '# >>> homegraph spec evolve hook >>>';
|
|
2590
|
+
const MARKER_END = '# <<< homegraph spec evolve hook <<<';
|
|
2591
|
+
const existing = fs.readFileSync(hookPath, 'utf8');
|
|
2592
|
+
const lines = existing.split('\n');
|
|
2593
|
+
const kept = [];
|
|
2594
|
+
let inBlock = false;
|
|
2595
|
+
let removedBlock = false;
|
|
2596
|
+
for (const line of lines) {
|
|
2597
|
+
const trimmed = line.trim();
|
|
2598
|
+
if (trimmed === MARKER_BEGIN) {
|
|
2599
|
+
inBlock = true;
|
|
2600
|
+
removedBlock = true;
|
|
2601
|
+
continue;
|
|
2602
|
+
}
|
|
2603
|
+
if (trimmed === MARKER_END) {
|
|
2604
|
+
inBlock = false;
|
|
2605
|
+
continue;
|
|
2606
|
+
}
|
|
2607
|
+
if (!inBlock)
|
|
2608
|
+
kept.push(line);
|
|
2609
|
+
}
|
|
2610
|
+
if (!removedBlock) {
|
|
2611
|
+
info('No homegraph spec evolve hook block found — nothing to uninstall.');
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2614
|
+
const remaining = kept.join('\n').trim();
|
|
2615
|
+
if (remaining.length === 0 || /^#!\/bin\/sh\s*$/.test(remaining)) {
|
|
2616
|
+
// Hook only contained our block (or just a shebang) — delete entirely
|
|
2617
|
+
fs.unlinkSync(hookPath);
|
|
2618
|
+
success(`Removed post-commit hook at ${hookPath}`);
|
|
2619
|
+
}
|
|
2620
|
+
else {
|
|
2621
|
+
fs.writeFileSync(hookPath, remaining + '\n');
|
|
2622
|
+
success(`Removed spec evolve hook block from ${hookPath} (user content preserved)`);
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
catch (err) {
|
|
2626
|
+
error(`Hook uninstall failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2627
|
+
process.exit(1);
|
|
2628
|
+
}
|
|
2629
|
+
});
|
|
2630
|
+
/**
|
|
2631
|
+
* homegraph spec evolve process
|
|
2632
|
+
*/
|
|
2633
|
+
evolveCommand
|
|
2634
|
+
.command('process')
|
|
2635
|
+
.description('Process commits through the spec self-evolve pipeline since last evolve')
|
|
2636
|
+
.option('-p, --path <path>', 'Path to the repository')
|
|
2637
|
+
.option('--db-path <path>', 'Path to the SQLite database file')
|
|
2638
|
+
.option('-j, --json', 'Output as JSON')
|
|
2639
|
+
.action(async (options) => {
|
|
2640
|
+
let db;
|
|
2641
|
+
try {
|
|
2642
|
+
const repoPath = resolveSpecProjectPath(options.path);
|
|
2643
|
+
const { createDatabase } = await Promise.resolve().then(() => __importStar(require('../db/sqlite-adapter')));
|
|
2644
|
+
const { resolveDbPath } = await Promise.resolve().then(() => __importStar(require('../spec/utils')));
|
|
2645
|
+
const { initSpecSchema } = await Promise.resolve().then(() => __importStar(require('../spec/db/schema')));
|
|
2646
|
+
const { loadSpecConfig } = await Promise.resolve().then(() => __importStar(require('../spec/config')));
|
|
2647
|
+
const { runBatchEvolvePipeline } = await Promise.resolve().then(() => __importStar(require('../spec/evolve/pipeline')));
|
|
2648
|
+
const { isGitRepo } = await Promise.resolve().then(() => __importStar(require('../spec/mining/git-scanner')));
|
|
2649
|
+
if (!isGitRepo(repoPath)) {
|
|
2650
|
+
error(`Not a git repository: ${repoPath}`);
|
|
2651
|
+
process.exit(1);
|
|
2652
|
+
}
|
|
2653
|
+
const dbPath = resolveDbPath(repoPath, options.dbPath);
|
|
2654
|
+
const created = createDatabase(dbPath);
|
|
2655
|
+
db = created.db;
|
|
2656
|
+
initSpecSchema(db);
|
|
2657
|
+
const config = loadSpecConfig(repoPath);
|
|
2658
|
+
const llmConfig = config.llm;
|
|
2659
|
+
if (!llmConfig) {
|
|
2660
|
+
warn('LLM not configured — Path B (LLM-based spec evolution) will be skipped.');
|
|
2661
|
+
warn('Only Path A (commit message scope → GENERATE) will be processed.');
|
|
2662
|
+
warn('Configure LLM in .homegraph/commit4spec/configs.json for full functionality:\n' +
|
|
2663
|
+
'{\n' +
|
|
2664
|
+
' "llm": {\n' +
|
|
2665
|
+
' "provider": "openai",\n' +
|
|
2666
|
+
' "apiKeyEnv": "OPENAI_API_KEY",\n' +
|
|
2667
|
+
' "model": "gpt-4o"\n' +
|
|
2668
|
+
' }\n' +
|
|
2669
|
+
'}');
|
|
2670
|
+
}
|
|
2671
|
+
const result = await runBatchEvolvePipeline(repoPath, db, llmConfig ?? undefined);
|
|
2672
|
+
if (options.json) {
|
|
2673
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2674
|
+
}
|
|
2675
|
+
else {
|
|
2676
|
+
if (result.commitsProcessed === 0) {
|
|
2677
|
+
console.log(chalk.green('No new commits to evolve.'));
|
|
2678
|
+
console.log(`Last evolved commit: ${result.fromCommit ? result.fromCommit.slice(0, 7) : 'none'}`);
|
|
2679
|
+
console.log(`Current HEAD: ${result.toCommit.slice(0, 7)}`);
|
|
2680
|
+
}
|
|
2681
|
+
else {
|
|
2682
|
+
console.log(chalk.bold(`Evolve complete: ${result.commitsProcessed} commit(s) processed`));
|
|
2683
|
+
console.log(`Range: ${result.fromCommit ? result.fromCommit.slice(0, 7) : 'none'} → ${result.toCommit.slice(0, 7)}`);
|
|
2684
|
+
if (result.metaUpdated) {
|
|
2685
|
+
console.log(chalk.green(`meta.json updated (currentCommitID = ${result.toCommit.slice(0, 7)})`));
|
|
2686
|
+
}
|
|
2687
|
+
else if (result.failures > 0) {
|
|
2688
|
+
console.log(chalk.yellow(`⚠ meta.json NOT updated — ${result.failures} commit(s) failed`));
|
|
2689
|
+
}
|
|
2690
|
+
else if (result.skippedCommits > 0) {
|
|
2691
|
+
console.log(chalk.yellow('⚠ meta.json NOT updated — all commits were skipped (no Path A match, no LLM)'));
|
|
2692
|
+
}
|
|
2693
|
+
else {
|
|
2694
|
+
console.log(chalk.yellow('⚠ meta.json NOT updated'));
|
|
2695
|
+
}
|
|
2696
|
+
// Show per-commit summary
|
|
2697
|
+
for (const r of result.perCommitResults) {
|
|
2698
|
+
let prefix;
|
|
2699
|
+
if (r.skipped) {
|
|
2700
|
+
prefix = chalk.yellow(' ⚠');
|
|
2701
|
+
}
|
|
2702
|
+
else {
|
|
2703
|
+
prefix = r.persisted ? chalk.green(' ✓') : chalk.red(' ✗');
|
|
2704
|
+
}
|
|
2705
|
+
console.log(`${prefix} ${r.commitHash.slice(0, 7)}`);
|
|
2706
|
+
if (r.skipped) {
|
|
2707
|
+
console.log(` skipped: ${r.skipReason}`);
|
|
2708
|
+
}
|
|
2709
|
+
if (r.isLogicChange) {
|
|
2710
|
+
console.log(` logic change: ${r.logicCheckReason}`);
|
|
2711
|
+
}
|
|
2712
|
+
if (r.generateSpecId) {
|
|
2713
|
+
console.log(` Path A - GENERATE ${r.generateSpecId}`);
|
|
2714
|
+
}
|
|
2715
|
+
for (const ev of r.evolvedSpecs) {
|
|
2716
|
+
console.log(` ${ev.specId}: ${ev.action}`);
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
// Aggregate summary
|
|
2720
|
+
const totalFragments = result.perCommitResults.reduce((s, r) => s + r.fragmentsCount, 0);
|
|
2721
|
+
const totalRelations = result.perCommitResults.reduce((s, r) => s + r.relationsCreated, 0);
|
|
2722
|
+
if (result.skippedCommits > 0) {
|
|
2723
|
+
console.log(chalk.yellow(`${result.skippedCommits} commit(s) skipped — no LLM configured, did not match Path A`));
|
|
2724
|
+
}
|
|
2725
|
+
if (result.failures > 0) {
|
|
2726
|
+
console.log(chalk.red(`${result.failures} commit(s) failed — see details above`));
|
|
2727
|
+
}
|
|
2728
|
+
console.log(`Summary: fragments=${totalFragments}, relations=${totalRelations}`);
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
catch (err) {
|
|
2733
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2734
|
+
if (message.includes('No meta.json found')) {
|
|
2735
|
+
error("No meta.json found. Run 'homegraph spec mine' first.");
|
|
2736
|
+
}
|
|
2737
|
+
else {
|
|
2738
|
+
error(`Evolve failed: ${message}`);
|
|
2739
|
+
}
|
|
2740
|
+
process.exit(1);
|
|
2741
|
+
}
|
|
2742
|
+
finally {
|
|
2743
|
+
try {
|
|
2744
|
+
db?.close();
|
|
2745
|
+
}
|
|
2746
|
+
catch { /* best effort */ }
|
|
2747
|
+
}
|
|
2748
|
+
});
|
|
1906
2749
|
/**
|
|
1907
2750
|
* homegraph telemetry [on|off|status]
|
|
1908
2751
|
*/
|
|
@@ -1942,6 +2785,63 @@ function main() {
|
|
|
1942
2785
|
console.log(`Config: ${s.configPath}`);
|
|
1943
2786
|
console.log(chalk.dim(`\nExactly what is collected (and never collected): ${telemetry_1.TELEMETRY_DOCS}\n`));
|
|
1944
2787
|
});
|
|
2788
|
+
/**
|
|
2789
|
+
* homegraph index-api <input> [version]
|
|
2790
|
+
* Build a standalone OHOS API database from command-line-tools SDK.
|
|
2791
|
+
*/
|
|
2792
|
+
program
|
|
2793
|
+
.command('index-api <input> [version]')
|
|
2794
|
+
.description('Index OpenHarmony SDK API declarations into a standalone SQLite db')
|
|
2795
|
+
.option('-o, --output <path>', 'Output database path')
|
|
2796
|
+
.option('-q, --quiet', 'Suppress progress output')
|
|
2797
|
+
.action(async (input, versionArg, options) => {
|
|
2798
|
+
try {
|
|
2799
|
+
const { resolveOhosSdkInput, ohosApiDbFilename, indexOhosApiDb } = await Promise.resolve().then(() => __importStar(require('../extraction/languages/arkts')));
|
|
2800
|
+
const resolved = resolveOhosSdkInput({
|
|
2801
|
+
inputPath: input,
|
|
2802
|
+
versionOverride: versionArg,
|
|
2803
|
+
});
|
|
2804
|
+
const outputPath = options.output ?? path.join(process.cwd(), ohosApiDbFilename(resolved.version));
|
|
2805
|
+
if (!options.quiet) {
|
|
2806
|
+
info(`SDK home: ${resolved.sdkHome}`);
|
|
2807
|
+
info(`API version: ${resolved.version}`);
|
|
2808
|
+
info(`Output: ${outputPath}`);
|
|
2809
|
+
}
|
|
2810
|
+
const result = await indexOhosApiDb({
|
|
2811
|
+
sdkHome: resolved.sdkHome,
|
|
2812
|
+
version: resolved.version,
|
|
2813
|
+
outputPath,
|
|
2814
|
+
onProgress: options.quiet
|
|
2815
|
+
? undefined
|
|
2816
|
+
: (progress) => {
|
|
2817
|
+
if (progress.phase === 'arkts-batch') {
|
|
2818
|
+
const label = progress.subphase === 'scene' ? 'Scene' : 'Persist';
|
|
2819
|
+
process.stdout.write(`\r${label}: ${progress.current}/${progress.total} ${progress.currentFile ?? ''}`.padEnd(80));
|
|
2820
|
+
}
|
|
2821
|
+
},
|
|
2822
|
+
});
|
|
2823
|
+
resolved.cleanup?.();
|
|
2824
|
+
if (!options.quiet) {
|
|
2825
|
+
process.stdout.write('\n');
|
|
2826
|
+
if (result.success) {
|
|
2827
|
+
info(`Indexed ${result.filesIndexed} SDK files → ${result.nodesCreated} nodes, ${result.edgesCreated} edges (${result.durationMs}ms)`);
|
|
2828
|
+
}
|
|
2829
|
+
else {
|
|
2830
|
+
error('OHOS API indexing failed');
|
|
2831
|
+
for (const err of result.errors.filter((e) => e.severity === 'error')) {
|
|
2832
|
+
error(err.message);
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
if (!result.success) {
|
|
2837
|
+
process.exit(1);
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
catch (err) {
|
|
2841
|
+
error(err instanceof Error ? err.message : String(err));
|
|
2842
|
+
process.exit(1);
|
|
2843
|
+
}
|
|
2844
|
+
});
|
|
1945
2845
|
/**
|
|
1946
2846
|
* homegraph upgrade [version]
|
|
1947
2847
|
*
|