homegraph 1.1.3 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -18
- 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 +709 -168
- package/dist/bin/homegraph.js.map +1 -1
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +85 -0
- package/dist/context/index.js.map +1 -1
- package/dist/db/index.d.ts +84 -5
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +217 -14
- 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 -1
- package/dist/db/migrations.js.map +1 -1
- package/dist/db/queries.d.ts +131 -2
- package/dist/db/queries.d.ts.map +1 -1
- package/dist/db/queries.js +473 -9
- package/dist/db/queries.js.map +1 -1
- package/dist/db/schema.sql +52 -1
- package/dist/db/wal-valve.d.ts +106 -0
- package/dist/db/wal-valve.d.ts.map +1 -0
- package/dist/db/wal-valve.js +208 -0
- package/dist/db/wal-valve.js.map +1 -0
- package/dist/directory.d.ts +36 -0
- package/dist/directory.d.ts.map +1 -1
- package/dist/directory.js +274 -0
- 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/cfml-extractor.d.ts +107 -0
- package/dist/extraction/cfml-extractor.d.ts.map +1 -0
- package/dist/extraction/cfml-extractor.js +494 -0
- package/dist/extraction/cfml-extractor.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/grammars.d.ts +25 -1
- package/dist/extraction/grammars.d.ts.map +1 -1
- package/dist/extraction/grammars.js +177 -24
- package/dist/extraction/grammars.js.map +1 -1
- package/dist/extraction/index.d.ts +55 -2
- package/dist/extraction/index.d.ts.map +1 -1
- package/dist/extraction/index.js +923 -381
- 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 +98 -0
- package/dist/extraction/languages/c-cpp.d.ts.map +1 -1
- package/dist/extraction/languages/c-cpp.js +563 -1
- package/dist/extraction/languages/c-cpp.js.map +1 -1
- package/dist/extraction/languages/cfquery.d.ts +12 -0
- package/dist/extraction/languages/cfquery.d.ts.map +1 -0
- package/dist/extraction/languages/cfquery.js +28 -0
- package/dist/extraction/languages/cfquery.js.map +1 -0
- package/dist/extraction/languages/cfscript.d.ts +3 -0
- package/dist/extraction/languages/cfscript.d.ts.map +1 -0
- package/dist/extraction/languages/cfscript.js +73 -0
- package/dist/extraction/languages/cfscript.js.map +1 -0
- package/dist/extraction/languages/cobol.d.ts +33 -0
- package/dist/extraction/languages/cobol.d.ts.map +1 -0
- package/dist/extraction/languages/cobol.js +499 -0
- package/dist/extraction/languages/cobol.js.map +1 -0
- package/dist/extraction/languages/erlang.d.ts +3 -0
- package/dist/extraction/languages/erlang.d.ts.map +1 -0
- package/dist/extraction/languages/erlang.js +350 -0
- package/dist/extraction/languages/erlang.js.map +1 -0
- package/dist/extraction/languages/index.d.ts.map +1 -1
- package/dist/extraction/languages/index.js +16 -0
- package/dist/extraction/languages/index.js.map +1 -1
- package/dist/extraction/languages/nix.d.ts +3 -0
- package/dist/extraction/languages/nix.d.ts.map +1 -0
- package/dist/extraction/languages/nix.js +294 -0
- package/dist/extraction/languages/nix.js.map +1 -0
- 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/languages/solidity.d.ts +3 -0
- package/dist/extraction/languages/solidity.d.ts.map +1 -0
- package/dist/extraction/languages/solidity.js +293 -0
- package/dist/extraction/languages/solidity.js.map +1 -0
- package/dist/extraction/languages/terraform.d.ts +3 -0
- package/dist/extraction/languages/terraform.d.ts.map +1 -0
- package/dist/extraction/languages/terraform.js +641 -0
- package/dist/extraction/languages/terraform.js.map +1 -0
- package/dist/extraction/languages/vbnet.d.ts +11 -0
- package/dist/extraction/languages/vbnet.d.ts.map +1 -0
- package/dist/extraction/languages/vbnet.js +141 -0
- package/dist/extraction/languages/vbnet.js.map +1 -0
- package/dist/extraction/mybatis-extractor.d.ts +30 -10
- package/dist/extraction/mybatis-extractor.d.ts.map +1 -1
- package/dist/extraction/mybatis-extractor.js +140 -38
- package/dist/extraction/mybatis-extractor.js.map +1 -1
- package/dist/extraction/parse-pool.d.ts +152 -0
- package/dist/extraction/parse-pool.d.ts.map +1 -0
- package/dist/extraction/parse-pool.js +387 -0
- package/dist/extraction/parse-pool.js.map +1 -0
- package/dist/extraction/parse-worker.js +9 -2
- package/dist/extraction/parse-worker.js.map +1 -1
- package/dist/extraction/tree-sitter-helpers.d.ts.map +1 -1
- package/dist/extraction/tree-sitter-helpers.js +1 -0
- package/dist/extraction/tree-sitter-helpers.js.map +1 -1
- package/dist/extraction/tree-sitter-types.d.ts +20 -1
- package/dist/extraction/tree-sitter-types.d.ts.map +1 -1
- package/dist/extraction/tree-sitter.d.ts +59 -0
- package/dist/extraction/tree-sitter.d.ts.map +1 -1
- package/dist/extraction/tree-sitter.js +952 -37
- package/dist/extraction/tree-sitter.js.map +1 -1
- package/dist/extraction/wasm/tree-sitter-arkts.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-terraform.wasm +0 -0
- package/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
- package/dist/graph/traversal.d.ts.map +1 -1
- package/dist/graph/traversal.js +77 -18
- package/dist/graph/traversal.js.map +1 -1
- package/dist/index.d.ts +85 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +426 -9
- package/dist/index.js.map +1 -1
- package/dist/installer/index.d.ts +42 -0
- package/dist/installer/index.d.ts.map +1 -1
- package/dist/installer/index.js +97 -2
- package/dist/installer/index.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 +73 -3
- package/dist/mcp/daemon.d.ts.map +1 -1
- package/dist/mcp/daemon.js +258 -40
- package/dist/mcp/daemon.js.map +1 -1
- package/dist/mcp/dynamic-boundaries.d.ts.map +1 -1
- package/dist/mcp/dynamic-boundaries.js +2 -1
- package/dist/mcp/dynamic-boundaries.js.map +1 -1
- package/dist/mcp/early-ppid.d.ts +26 -0
- package/dist/mcp/early-ppid.d.ts.map +1 -0
- package/dist/mcp/early-ppid.js +29 -0
- package/dist/mcp/early-ppid.js.map +1 -0
- package/dist/mcp/engine.d.ts +16 -0
- package/dist/mcp/engine.d.ts.map +1 -1
- package/dist/mcp/engine.js +100 -2
- package/dist/mcp/engine.js.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +66 -49
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/liveness-watchdog.d.ts +18 -1
- package/dist/mcp/liveness-watchdog.d.ts.map +1 -1
- package/dist/mcp/liveness-watchdog.js +73 -13
- package/dist/mcp/liveness-watchdog.js.map +1 -1
- package/dist/mcp/memory-budget.d.ts +30 -0
- package/dist/mcp/memory-budget.d.ts.map +1 -0
- package/dist/mcp/memory-budget.js +123 -0
- package/dist/mcp/memory-budget.js.map +1 -0
- 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/proxy.d.ts.map +1 -1
- package/dist/mcp/proxy.js +88 -4
- package/dist/mcp/proxy.js.map +1 -1
- package/dist/mcp/query-cache.d.ts +56 -0
- package/dist/mcp/query-cache.d.ts.map +1 -0
- package/dist/mcp/query-cache.js +261 -0
- package/dist/mcp/query-cache.js.map +1 -0
- package/dist/mcp/query-pool.d.ts +128 -0
- package/dist/mcp/query-pool.d.ts.map +1 -0
- package/dist/mcp/query-pool.js +374 -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 +4 -29
- package/dist/mcp/server-instructions.d.ts.map +1 -1
- package/dist/mcp/server-instructions.js +39 -86
- package/dist/mcp/server-instructions.js.map +1 -1
- package/dist/mcp/session.d.ts +14 -0
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +28 -1
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/startup-handshake.d.ts +44 -0
- package/dist/mcp/startup-handshake.d.ts.map +1 -0
- package/dist/mcp/startup-handshake.js +73 -0
- package/dist/mcp/startup-handshake.js.map +1 -0
- package/dist/mcp/tools.d.ts +179 -2
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +3027 -192
- package/dist/mcp/tools.js.map +1 -1
- package/dist/mcp/transport.d.ts.map +1 -1
- package/dist/mcp/transport.js +18 -0
- package/dist/mcp/transport.js.map +1 -1
- package/dist/project-config.d.ts +58 -0
- package/dist/project-config.d.ts.map +1 -1
- package/dist/project-config.js +141 -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/resolution/c-fnptr-synthesizer.d.ts +2 -29
- package/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -1
- package/dist/resolution/c-fnptr-synthesizer.js +866 -148
- package/dist/resolution/c-fnptr-synthesizer.js.map +1 -1
- package/dist/resolution/callback-synthesizer.d.ts +9 -1
- package/dist/resolution/callback-synthesizer.d.ts.map +1 -1
- package/dist/resolution/callback-synthesizer.js +1232 -183
- 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/frameworks/cics.d.ts +20 -0
- package/dist/resolution/frameworks/cics.d.ts.map +1 -0
- package/dist/resolution/frameworks/cics.js +90 -0
- package/dist/resolution/frameworks/cics.js.map +1 -0
- package/dist/resolution/frameworks/index.d.ts.map +1 -1
- package/dist/resolution/frameworks/index.js +3 -0
- package/dist/resolution/frameworks/index.js.map +1 -1
- package/dist/resolution/frameworks/java.d.ts.map +1 -1
- package/dist/resolution/frameworks/java.js +14 -6
- package/dist/resolution/frameworks/java.js.map +1 -1
- package/dist/resolution/frameworks/terraform.d.ts +38 -0
- package/dist/resolution/frameworks/terraform.d.ts.map +1 -0
- package/dist/resolution/frameworks/terraform.js +277 -0
- package/dist/resolution/frameworks/terraform.js.map +1 -0
- package/dist/resolution/goframe-synthesizer.d.ts +2 -1
- package/dist/resolution/goframe-synthesizer.d.ts.map +1 -1
- package/dist/resolution/goframe-synthesizer.js +8 -3
- package/dist/resolution/goframe-synthesizer.js.map +1 -1
- package/dist/resolution/import-resolver.d.ts +7 -0
- package/dist/resolution/import-resolver.d.ts.map +1 -1
- package/dist/resolution/import-resolver.js +129 -4
- package/dist/resolution/import-resolver.js.map +1 -1
- package/dist/resolution/index.d.ts +65 -3
- package/dist/resolution/index.d.ts.map +1 -1
- package/dist/resolution/index.js +477 -59
- package/dist/resolution/index.js.map +1 -1
- package/dist/resolution/name-matcher.d.ts +22 -3
- package/dist/resolution/name-matcher.d.ts.map +1 -1
- package/dist/resolution/name-matcher.js +657 -39
- package/dist/resolution/name-matcher.js.map +1 -1
- package/dist/resolution/strip-comments.d.ts +1 -1
- package/dist/resolution/strip-comments.d.ts.map +1 -1
- package/dist/resolution/strip-comments.js +49 -0
- package/dist/resolution/strip-comments.js.map +1 -1
- package/dist/resolution/types.d.ts +29 -0
- package/dist/resolution/types.d.ts.map +1 -1
- package/dist/resolution/workspace-packages.d.ts +10 -0
- package/dist/resolution/workspace-packages.d.ts.map +1 -1
- package/dist/resolution/workspace-packages.js +142 -4
- package/dist/resolution/workspace-packages.js.map +1 -1
- package/dist/search/identifier-segments.d.ts +60 -0
- package/dist/search/identifier-segments.d.ts.map +1 -0
- package/dist/search/identifier-segments.js +176 -0
- package/dist/search/identifier-segments.js.map +1 -0
- package/dist/search/query-utils.d.ts +201 -0
- package/dist/search/query-utils.d.ts.map +1 -1
- package/dist/search/query-utils.js +919 -2
- package/dist/search/query-utils.js.map +1 -1
- package/dist/spec/config.d.ts +7 -11
- package/dist/spec/config.d.ts.map +1 -1
- package/dist/spec/config.js +77 -48
- package/dist/spec/config.js.map +1 -1
- package/dist/spec/db/fragment-node.d.ts +1 -0
- package/dist/spec/db/fragment-node.d.ts.map +1 -1
- package/dist/spec/db/fragment-node.js +8 -0
- package/dist/spec/db/fragment-node.js.map +1 -1
- package/dist/spec/db/fts.d.ts +14 -0
- package/dist/spec/db/fts.d.ts.map +1 -1
- package/dist/spec/db/fts.js +43 -4
- package/dist/spec/db/fts.js.map +1 -1
- package/dist/spec/db/schema.d.ts +6 -3
- package/dist/spec/db/schema.d.ts.map +1 -1
- package/dist/spec/db/schema.js +36 -4
- package/dist/spec/db/schema.js.map +1 -1
- package/dist/spec/db/schema.sql +11 -1
- package/dist/spec/evolve/logic-checker.d.ts +2 -2
- package/dist/spec/evolve/logic-checker.d.ts.map +1 -1
- package/dist/spec/evolve/logic-checker.js +3 -27
- package/dist/spec/evolve/logic-checker.js.map +1 -1
- package/dist/spec/evolve/pipeline.d.ts +21 -19
- package/dist/spec/evolve/pipeline.d.ts.map +1 -1
- package/dist/spec/evolve/pipeline.js +284 -31
- package/dist/spec/evolve/pipeline.js.map +1 -1
- package/dist/spec/evolve/spec-rewriter.d.ts +2 -2
- package/dist/spec/evolve/spec-rewriter.d.ts.map +1 -1
- package/dist/spec/evolve/spec-rewriter.js +7 -31
- package/dist/spec/evolve/spec-rewriter.js.map +1 -1
- package/dist/spec/graph/queries.d.ts +97 -0
- package/dist/spec/graph/queries.d.ts.map +1 -1
- package/dist/spec/graph/queries.js +225 -0
- package/dist/spec/graph/queries.js.map +1 -1
- 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/git-scanner.d.ts +12 -0
- package/dist/spec/mining/git-scanner.d.ts.map +1 -1
- package/dist/spec/mining/git-scanner.js +41 -0
- package/dist/spec/mining/git-scanner.js.map +1 -1
- package/dist/spec/mining/pipeline.d.ts.map +1 -1
- package/dist/spec/mining/pipeline.js +14 -1
- package/dist/spec/mining/pipeline.js.map +1 -1
- package/dist/spec/utils.d.ts +2 -1
- package/dist/spec/utils.d.ts.map +1 -1
- package/dist/spec/utils.js +3 -1
- package/dist/spec/utils.js.map +1 -1
- package/dist/sync/git-hooks.d.ts.map +1 -1
- package/dist/sync/git-hooks.js +2 -0
- package/dist/sync/git-hooks.js.map +1 -1
- package/dist/sync/watcher.d.ts +10 -5
- package/dist/sync/watcher.d.ts.map +1 -1
- package/dist/sync/watcher.js +51 -14
- package/dist/sync/watcher.js.map +1 -1
- package/dist/sync/worktree.d.ts +9 -0
- package/dist/sync/worktree.d.ts.map +1 -1
- package/dist/sync/worktree.js +45 -0
- package/dist/sync/worktree.js.map +1 -1
- package/dist/types.d.ts +25 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +10 -0
- package/dist/types.js.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.d.ts +32 -0
- package/dist/upgrade/index.d.ts.map +1 -1
- package/dist/upgrade/index.js +163 -15
- package/dist/upgrade/index.js.map +1 -1
- package/dist/upgrade/remove-binary.d.ts +87 -0
- package/dist/upgrade/remove-binary.d.ts.map +1 -0
- package/dist/upgrade/remove-binary.js +289 -0
- package/dist/upgrade/remove-binary.js.map +1 -0
- package/dist/upgrade/update-check.d.ts +92 -0
- package/dist/upgrade/update-check.d.ts.map +1 -0
- package/dist/upgrade/update-check.js +258 -0
- package/dist/upgrade/update-check.js.map +1 -0
- package/package.json +4 -4
- 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-all.sh +6 -0
- package/scripts/build-bundle.sh +5 -0
- package/scripts/exp_boundary_eval/README.md +247 -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/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/npm-shim.js +32 -3
- package/scripts/ohos-sdk-publish.mjs +133 -0
- package/scripts/qa_eval/README.md +0 -407
- package/scripts/qa_eval/_test_deveco_probe.py +0 -41
- package/scripts/qa_eval/agent_runner.py +0 -526
- package/scripts/qa_eval/data/.gitignore +0 -4
- package/scripts/qa_eval/data/test-set.jsonl +0 -2
- package/scripts/qa_eval/eval_metrics.py +0 -274
- package/scripts/qa_eval/external_agent.py +0 -976
- package/scripts/qa_eval/llm_config.py +0 -92
- package/scripts/qa_eval/memory_monitor.py +0 -132
- package/scripts/qa_eval/my_answer_accuracy.py +0 -187
- package/scripts/qa_eval/requirements.txt +0 -2
- package/scripts/qa_eval/run_pipeline.py +0 -804
- package/scripts/qa_eval/stats_efficiency.py +0 -279
- package/scripts/qa_eval/stats_scores.py +0 -207
|
@@ -0,0 +1,1313 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Analyze experiment results and produce:
|
|
4
|
+
1. Terminal summary
|
|
5
|
+
2. Markdown report → results/analysis_report.md
|
|
6
|
+
|
|
7
|
+
The report includes narrative analysis, pattern detection, risk assessment,
|
|
8
|
+
and a scoring system — not just raw data tables.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import List, Optional
|
|
17
|
+
|
|
18
|
+
from _utils import (OUTPUT_DIR, GREEN, NC, header, is_homegraph_tool, parse_output,
|
|
19
|
+
parse_stream_json, read_run_manifest, resolve_deveco_model)
|
|
20
|
+
|
|
21
|
+
RESULTS_DIR = OUTPUT_DIR # default when no path argument given
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _configure_stdout():
|
|
25
|
+
"""Avoid UnicodeEncodeError on Windows consoles (default GBK)."""
|
|
26
|
+
for stream in (sys.stdout, sys.stderr):
|
|
27
|
+
reconfigure = getattr(stream, "reconfigure", None)
|
|
28
|
+
if reconfigure is not None:
|
|
29
|
+
try:
|
|
30
|
+
reconfigure(encoding="utf-8", errors="replace")
|
|
31
|
+
except (OSError, ValueError):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _memory_from_result(d: dict) -> dict:
|
|
36
|
+
mem = d.get("memory") or {}
|
|
37
|
+
return {
|
|
38
|
+
"peak_rss_mb": mem.get("peak_rss_mb", 0),
|
|
39
|
+
"peak_homegraph_rss_mb": mem.get("peak_homegraph_rss_mb", 0),
|
|
40
|
+
"peak_combined_rss_mb": mem.get("peak_combined_rss_mb", 0),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _format_mb(val) -> str:
|
|
45
|
+
if not val:
|
|
46
|
+
return "—"
|
|
47
|
+
return f"{float(val):.0f} MB"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _parser_for_agent(agent: str) -> str:
|
|
51
|
+
a = agent.lower()
|
|
52
|
+
if "deveco" in a:
|
|
53
|
+
return "deveco_json"
|
|
54
|
+
if "codex" in a:
|
|
55
|
+
return "codex_json"
|
|
56
|
+
if "opencode" in a:
|
|
57
|
+
return "opencode_json"
|
|
58
|
+
if "cursor" in a:
|
|
59
|
+
return "cursor_stream_json"
|
|
60
|
+
return "claude_stream_json"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _stream_stats(stream: Path, agent: str = ""):
|
|
64
|
+
parser = _parser_for_agent(agent) if agent else "auto"
|
|
65
|
+
return parse_output(stream, parser)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _is_placeholder_model(model: str) -> bool:
|
|
69
|
+
return not model or model in ("?", "--", "unknown", "Unknown")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _is_session_id(model: str) -> bool:
|
|
73
|
+
return bool(model) and model.startswith("ses_")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def format_model_display(model: str) -> str:
|
|
77
|
+
if _is_placeholder_model(model):
|
|
78
|
+
return "未知"
|
|
79
|
+
if _is_session_id(model):
|
|
80
|
+
return "未知(DevEco session 未导出)"
|
|
81
|
+
return model
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _normalize_model(raw: dict, exp_dir: Path, agent: str) -> str:
|
|
85
|
+
model = raw.get("model", "")
|
|
86
|
+
if not _is_placeholder_model(model) and not _is_session_id(model):
|
|
87
|
+
return model
|
|
88
|
+
if "deveco" not in agent.lower():
|
|
89
|
+
return model or "?"
|
|
90
|
+
deveco_sid = raw.get("deveco_session_id", "")
|
|
91
|
+
if not _is_session_id(deveco_sid):
|
|
92
|
+
deveco_sid = model if _is_session_id(model) else ""
|
|
93
|
+
if not deveco_sid:
|
|
94
|
+
stream = exp_dir / "stream_output.jsonl"
|
|
95
|
+
if not stream.exists():
|
|
96
|
+
stream = exp_dir / "round_1" / "stream_output.jsonl"
|
|
97
|
+
if stream.exists():
|
|
98
|
+
stats = parse_output(stream, "deveco_json")
|
|
99
|
+
deveco_sid = stats.deveco_session_id or (
|
|
100
|
+
stats.model if _is_session_id(stats.model) else ""
|
|
101
|
+
)
|
|
102
|
+
if deveco_sid:
|
|
103
|
+
resolved = resolve_deveco_model(deveco_sid)
|
|
104
|
+
if resolved:
|
|
105
|
+
return resolved
|
|
106
|
+
return model or "?"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def resolve_primary_model(summary: List[dict]) -> str:
|
|
110
|
+
"""Pick the most common real model name across oneshot experiments."""
|
|
111
|
+
real = [r["model"] for r in summary
|
|
112
|
+
if not _is_placeholder_model(r.get("model", "")) and not _is_session_id(r["model"])]
|
|
113
|
+
if real:
|
|
114
|
+
return max(set(real), key=real.count)
|
|
115
|
+
fallback = [r["model"] for r in summary if not _is_placeholder_model(r.get("model", ""))]
|
|
116
|
+
if fallback:
|
|
117
|
+
return format_model_display(fallback[0])
|
|
118
|
+
return "未知"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _has_experiment_outputs(path: Path) -> bool:
|
|
122
|
+
"""True if path looks like a directory of per-experiment result folders."""
|
|
123
|
+
if not path.is_dir():
|
|
124
|
+
return False
|
|
125
|
+
for exp_id in ("1-1", "2", "5"):
|
|
126
|
+
if (path / exp_id / "results.json").exists() or (path / exp_id / "session_results.json").exists():
|
|
127
|
+
return True
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def resolve_results_dir(path: Path) -> Path:
|
|
132
|
+
"""Accept artifact run root or results/ path; return the results directory."""
|
|
133
|
+
if _has_experiment_outputs(path):
|
|
134
|
+
return path
|
|
135
|
+
nested = path / "results"
|
|
136
|
+
if _has_experiment_outputs(nested):
|
|
137
|
+
return nested
|
|
138
|
+
return path
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def resolve_report_path(input_path: Path, results_dir: Path) -> Path:
|
|
142
|
+
"""Write analysis_report.md at artifact run root when layout allows."""
|
|
143
|
+
if input_path.resolve() != results_dir.resolve():
|
|
144
|
+
return input_path / "analysis_report.md"
|
|
145
|
+
parent = results_dir.parent
|
|
146
|
+
if (parent / "state").is_dir():
|
|
147
|
+
return parent / "analysis_report.md"
|
|
148
|
+
return results_dir / "analysis_report.md"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ═══════════════════════════════════════════════════════════
|
|
152
|
+
# Data collection (raw metrics from experiment outputs)
|
|
153
|
+
# ═══════════════════════════════════════════════════════════
|
|
154
|
+
|
|
155
|
+
def collect_summary(results_dir: Path) -> List[dict]:
|
|
156
|
+
rows = []
|
|
157
|
+
for exp_dir in sorted(results_dir.iterdir()):
|
|
158
|
+
if not exp_dir.is_dir(): continue
|
|
159
|
+
exp_id = exp_dir.name
|
|
160
|
+
rj, sj = exp_dir / "results.json", exp_dir / "session_results.json"
|
|
161
|
+
if rj.exists():
|
|
162
|
+
d = json.loads(rj.read_text(encoding="utf-8"))
|
|
163
|
+
fr = d.get("files_read", [])
|
|
164
|
+
fe = d.get("modified_files", d.get("files_edited", 0))
|
|
165
|
+
if isinstance(fe, list): fe = len(fe)
|
|
166
|
+
agent = d.get("agent", "?")
|
|
167
|
+
stream = exp_dir / "stream_output.jsonl"
|
|
168
|
+
stats = _stream_stats(stream, agent) if stream.exists() else None
|
|
169
|
+
tool_calls = d.get("tool_calls", 0)
|
|
170
|
+
files_read = len(fr) if isinstance(fr, list) else fr
|
|
171
|
+
input_tokens = d.get("input_tokens", 0)
|
|
172
|
+
if stats and stats.tool_calls:
|
|
173
|
+
tool_calls = stats.tool_calls
|
|
174
|
+
files_read = len(set(stats.files_read))
|
|
175
|
+
if stats.total_input_tokens:
|
|
176
|
+
input_tokens = stats.total_input_tokens
|
|
177
|
+
if not fe and stats.files_edited:
|
|
178
|
+
fe = len(set(stats.files_edited))
|
|
179
|
+
tool_names = d.get("tool_names", [])
|
|
180
|
+
if stats and stats.tool_names:
|
|
181
|
+
tool_names = stats.tool_names
|
|
182
|
+
hg_tools = _homegraph_tool_counts(tool_names)
|
|
183
|
+
hg_tc = d.get("homegraph_tool_calls", sum(hg_tools.values()))
|
|
184
|
+
if stats and stats.homegraph_tool_calls:
|
|
185
|
+
hg_tc = stats.homegraph_tool_calls
|
|
186
|
+
rows.append(dict(id=exp_id, title=d.get("title", "?"), type="oneshot",
|
|
187
|
+
duration_s=d.get("duration_s", "?"), tool_calls=tool_calls,
|
|
188
|
+
files_read=files_read, files_edited=fe,
|
|
189
|
+
input_tokens=input_tokens, max_turns_hit=d.get("max_turns_hit", False),
|
|
190
|
+
exit_code=d.get("exit_code", "?"), model=_normalize_model(d, exp_dir, agent),
|
|
191
|
+
tool_names=tool_names, errors=d.get("errors", []),
|
|
192
|
+
homegraph_tool_calls=hg_tc,
|
|
193
|
+
homegraph_tools=hg_tools,
|
|
194
|
+
homegraph_effective_calls=d.get("homegraph_effective_calls", 0),
|
|
195
|
+
homegraph_failed_calls=d.get("homegraph_failed_calls", 0),
|
|
196
|
+
used_homegraph=d.get("used_homegraph",
|
|
197
|
+
stats.used_homegraph if stats else bool(hg_tc)),
|
|
198
|
+
agent=agent, memory=_memory_from_result(d)))
|
|
199
|
+
elif sj.exists():
|
|
200
|
+
d = json.loads(sj.read_text(encoding="utf-8"))
|
|
201
|
+
agent = d.get("agent", "?")
|
|
202
|
+
total_tc = d.get("total_tool_calls", 0)
|
|
203
|
+
round_streams = sorted(exp_dir.glob("round_*/stream_output.jsonl"))
|
|
204
|
+
if round_streams:
|
|
205
|
+
total_tc = sum(_stream_stats(s, agent).tool_calls for s in round_streams)
|
|
206
|
+
model = _normalize_model(d, exp_dir, agent)
|
|
207
|
+
if _is_placeholder_model(model) or model == "?":
|
|
208
|
+
r1 = exp_dir / "round_1" / "stream_output.jsonl"
|
|
209
|
+
if r1.exists():
|
|
210
|
+
m = _stream_stats(r1, agent).model
|
|
211
|
+
if m:
|
|
212
|
+
model = m
|
|
213
|
+
rows.append(dict(id=exp_id, title="Session Persistence", type="session",
|
|
214
|
+
duration_s=f"{d.get('total_duration_ms', 0) / 1000:.0f}s",
|
|
215
|
+
tool_calls=total_tc, files_read="--", files_edited="--",
|
|
216
|
+
input_tokens=0, max_turns_hit=False, exit_code="--",
|
|
217
|
+
model=model,
|
|
218
|
+
tool_names=[], errors=[], agent=agent))
|
|
219
|
+
return rows
|
|
220
|
+
|
|
221
|
+
def collect_hallucination(results_dir: Path) -> dict:
|
|
222
|
+
r = {"exp": "3-1", "has_results": False, "tool_calls": 0, "files_explored": 0,
|
|
223
|
+
"hallucinated": False, "fab_score": 0, "pattern": "none",
|
|
224
|
+
"text_excerpt": "", "qualifiers": [], "real_code_explored": []}
|
|
225
|
+
stream = results_dir / "3-1" / "stream_output.jsonl"
|
|
226
|
+
if not stream.exists(): return r
|
|
227
|
+
r["has_results"] = True
|
|
228
|
+
agent = "?"
|
|
229
|
+
rj = results_dir / "3-1" / "results.json"
|
|
230
|
+
if rj.exists():
|
|
231
|
+
agent = json.loads(rj.read_text(encoding="utf-8")).get("agent", "?")
|
|
232
|
+
stats = _stream_stats(stream, agent)
|
|
233
|
+
r["tool_calls"] = stats.tool_calls
|
|
234
|
+
r["files_explored"] = len(set(stats.files_read))
|
|
235
|
+
text = "\n".join(stats.text_lines)
|
|
236
|
+
r["text_excerpt"] = text[:600]
|
|
237
|
+
r["module_mentioned"] = "PhotoWatermarkEngine" in text
|
|
238
|
+
# Fabrication markers: describing architecture for something nonexistent
|
|
239
|
+
fab = ["watermark", "水印", "sticker", "三层", "three-layer", "ArkTS.*C\\+\\+", "渲染引擎",
|
|
240
|
+
"HmcRenderEngine", "HmcEditorAsset", "WatermarkPicInfo", "跨层协作"]
|
|
241
|
+
r["fab_score"] = sum(1 for m in fab if re.search(m, text, re.I))
|
|
242
|
+
# Detect pattern type
|
|
243
|
+
if "不存在" in text or "not exist" in text.lower():
|
|
244
|
+
if r["fab_score"] >= 5:
|
|
245
|
+
r["pattern"] = "yes_and" # "doesn't exist, BUT here's how it works..."
|
|
246
|
+
elif r["fab_score"] >= 2:
|
|
247
|
+
r["pattern"] = "partial"
|
|
248
|
+
else:
|
|
249
|
+
r["pattern"] = "honest_denial"
|
|
250
|
+
else:
|
|
251
|
+
r["pattern"] = "pure_fabrication" if r["fab_score"] >= 3 else "none"
|
|
252
|
+
r["hallucinated"] = r["fab_score"] >= 3 and r["module_mentioned"]
|
|
253
|
+
# What real code was explored (wrongly attributed)
|
|
254
|
+
r["real_code_explored"] = list(set(stats.files_read))[:10]
|
|
255
|
+
qs = ["seems", "might", "may be", "possibly", "perhaps", "appears", "好像", "可能", "似乎"]
|
|
256
|
+
r["qualifiers"] = [q for q in qs if q in text.lower()]
|
|
257
|
+
return r
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def collect_share_coverage(results_dir: Path) -> dict:
|
|
261
|
+
r = {"exp": "3-2", "has_results": False, "tool_calls": 0, "files_explored": 0,
|
|
262
|
+
"found": [], "missed": [], "coverage": "0/6"}
|
|
263
|
+
stream = results_dir / "3-2" / "stream_output.jsonl"
|
|
264
|
+
if not stream.exists(): return r
|
|
265
|
+
r["has_results"] = True
|
|
266
|
+
agent = "?"
|
|
267
|
+
rj = results_dir / "3-2" / "results.json"
|
|
268
|
+
if rj.exists():
|
|
269
|
+
agent = json.loads(rj.read_text(encoding="utf-8")).get("agent", "?")
|
|
270
|
+
stats = _stream_stats(stream, agent)
|
|
271
|
+
all_files = set(stats.files_read) | set(stats.files_edited) | set(stats.files_grepped)
|
|
272
|
+
text = "\n".join(stats.text_lines)
|
|
273
|
+
r["tool_calls"] = stats.tool_calls
|
|
274
|
+
r["files_explored"] = len(all_files)
|
|
275
|
+
targets = ["KnockShareUtil", "ShareUrlUtil", "SwingShareUtil",
|
|
276
|
+
"harmonyShare", "systemShare", "ShareBrowserCustomDialog"]
|
|
277
|
+
for name in targets:
|
|
278
|
+
matched = [f for f in all_files if name in f]
|
|
279
|
+
if not matched and name in text:
|
|
280
|
+
matched = [name]
|
|
281
|
+
if matched: r["found"].append({"name": name, "path": matched[0]})
|
|
282
|
+
else: r["missed"].append(name)
|
|
283
|
+
r["coverage"] = f"{len(r['found'])}/{len(targets)}"
|
|
284
|
+
r["coverage_pct"] = len(r["found"]) / len(targets) * 100
|
|
285
|
+
return r
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def collect_cross_layer(results_dir: Path) -> dict:
|
|
289
|
+
r = {"exp": "2", "has_results": False, "tool_calls": 0, "arkts": 0, "napi": 0,
|
|
290
|
+
"cpp": 0, "cmake": 0, "layers_penetrated": 0, "max_turns_hit": False}
|
|
291
|
+
stream = results_dir / "2" / "stream_output.jsonl"
|
|
292
|
+
if not stream.exists(): return r
|
|
293
|
+
r["has_results"] = True
|
|
294
|
+
agent = "?"
|
|
295
|
+
rj = results_dir / "2" / "results.json"
|
|
296
|
+
if rj.exists():
|
|
297
|
+
agent = json.loads(rj.read_text(encoding="utf-8")).get("agent", "?")
|
|
298
|
+
stats = _stream_stats(stream, agent)
|
|
299
|
+
all_files = set(stats.files_read) | set(stats.files_edited)
|
|
300
|
+
r["tool_calls"] = stats.tool_calls
|
|
301
|
+
r["max_turns_hit"] = stats.max_turns_hit
|
|
302
|
+
r["arkts"] = len([f for f in all_files if f.endswith(".ets")])
|
|
303
|
+
r["napi"] = len([f for f in all_files if f.endswith(".ts") and "native" in f.lower()])
|
|
304
|
+
r["cpp"] = len([f for f in all_files if f.endswith((".cpp", ".h", ".hpp"))])
|
|
305
|
+
r["cmake"] = len([f for f in all_files if "CMakeLists" in f])
|
|
306
|
+
r["layers_penetrated"] = sum([r["arkts"] > 0, r["napi"] > 0, r["cpp"] > 0, r["cmake"] > 0])
|
|
307
|
+
return r
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def collect_over_exploration(results_dir: Path) -> dict:
|
|
311
|
+
r = {"exp": "3-3", "has_results": False, "tool_calls": 0, "files_explored": 0,
|
|
312
|
+
"utils_files": 0, "target_hit": False, "efficiency_pct": 0}
|
|
313
|
+
stream = results_dir / "3-3" / "stream_output.jsonl"
|
|
314
|
+
if not stream.exists(): return r
|
|
315
|
+
r["has_results"] = True
|
|
316
|
+
agent = "?"
|
|
317
|
+
rj = results_dir / "3-3" / "results.json"
|
|
318
|
+
if rj.exists():
|
|
319
|
+
agent = json.loads(rj.read_text(encoding="utf-8")).get("agent", "?")
|
|
320
|
+
stats = _stream_stats(stream, agent)
|
|
321
|
+
all_files = set(stats.files_read)
|
|
322
|
+
r["tool_calls"] = stats.tool_calls
|
|
323
|
+
r["files_explored"] = len(all_files)
|
|
324
|
+
util_files = sorted([f for f in all_files if "Util" in f and f.endswith(".ets")])
|
|
325
|
+
r["utils_files"] = len(util_files)
|
|
326
|
+
r["utils_list"] = [Path(f).name for f in util_files[:15]]
|
|
327
|
+
r["target_hit"] = any("ImageUtil" in f for f in all_files)
|
|
328
|
+
r["efficiency_pct"] = round(1 / max(r["utils_files"], 1) * 100)
|
|
329
|
+
return r
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def collect_multilang(results_dir: Path) -> dict:
|
|
333
|
+
r = {"exp": "4-1", "has_results": False, "count": 0, "has_zh": False, "has_base": False}
|
|
334
|
+
stream = results_dir / "4-1" / "stream_output.jsonl"
|
|
335
|
+
if not stream.exists(): return r
|
|
336
|
+
r["has_results"] = True
|
|
337
|
+
agent = "?"
|
|
338
|
+
rj = results_dir / "4-1" / "results.json"
|
|
339
|
+
if rj.exists():
|
|
340
|
+
agent = json.loads(rj.read_text(encoding="utf-8")).get("agent", "?")
|
|
341
|
+
stats = _stream_stats(stream, agent)
|
|
342
|
+
all_files = set(stats.files_read) | set(stats.files_edited)
|
|
343
|
+
langs = sorted([f for f in all_files if "string.json" in f])
|
|
344
|
+
r["count"] = len(langs)
|
|
345
|
+
r["has_base"] = any("base/element" in f for f in langs)
|
|
346
|
+
r["has_zh"] = any("zh_CN" in f for f in langs)
|
|
347
|
+
return r
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def collect_dependency_knowledge(results_dir: Path) -> dict:
|
|
351
|
+
r = {"exp": "4-3", "has_results": False, "oh_package": False, "ohpm": False,
|
|
352
|
+
"build_gn_wrong": False, "score": 0}
|
|
353
|
+
stream = results_dir / "4-3" / "stream_output.jsonl"
|
|
354
|
+
if not stream.exists(): return r
|
|
355
|
+
r["has_results"] = True
|
|
356
|
+
agent = "?"
|
|
357
|
+
rj = results_dir / "4-3" / "results.json"
|
|
358
|
+
if rj.exists():
|
|
359
|
+
agent = json.loads(rj.read_text(encoding="utf-8")).get("agent", "?")
|
|
360
|
+
stats = _stream_stats(stream, agent)
|
|
361
|
+
all_files = set(stats.files_read) | set(stats.files_edited)
|
|
362
|
+
text = "\n".join(stats.text_lines)
|
|
363
|
+
r["oh_package"] = "oh-package" in text or any("oh-package" in f for f in all_files)
|
|
364
|
+
r["ohpm"] = "ohpm" in text
|
|
365
|
+
r["build_gn_wrong"] = "BUILD.gn" in text or any("BUILD.gn" in f for f in all_files)
|
|
366
|
+
r["score"] = sum([r["oh_package"], r["ohpm"], not r["build_gn_wrong"]])
|
|
367
|
+
return r
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def collect_memory_curve(results_dir: Path) -> dict:
|
|
371
|
+
r = {"exp": "5", "has_results": False, "session_id": "", "rounds": [],
|
|
372
|
+
"decay_pct": 0, "decay_round": 0}
|
|
373
|
+
sj = results_dir / "5" / "session_results.json"
|
|
374
|
+
if not sj.exists(): return r
|
|
375
|
+
r["has_results"] = True
|
|
376
|
+
d = json.loads(sj.read_text(encoding="utf-8"))
|
|
377
|
+
agent = d.get("agent", "?")
|
|
378
|
+
r["session_id"] = d.get("session_id", "")[:16]
|
|
379
|
+
round_streams = sorted((results_dir / "5").glob("round_*/stream_output.jsonl"))
|
|
380
|
+
if round_streams:
|
|
381
|
+
for i, stream in enumerate(round_streams, 1):
|
|
382
|
+
stats = _stream_stats(stream, agent)
|
|
383
|
+
r["rounds"].append(dict(round=i, tool_calls=stats.tool_calls,
|
|
384
|
+
files_read=len(set(stats.files_read))))
|
|
385
|
+
else:
|
|
386
|
+
for rd in d.get("memory_retention_curve", []):
|
|
387
|
+
r["rounds"].append(dict(round=rd["round"], tool_calls=int(rd["tool_calls"]),
|
|
388
|
+
files_read=len(rd.get("files_read", []))))
|
|
389
|
+
if len(r["rounds"]) >= 2:
|
|
390
|
+
r1 = r["rounds"][0]["tool_calls"]
|
|
391
|
+
r2 = r["rounds"][1]["tool_calls"]
|
|
392
|
+
r["decay_pct"] = round((r1 - r2) / max(r1, 1) * 100)
|
|
393
|
+
r["decay_round"] = 1
|
|
394
|
+
prev = r1
|
|
395
|
+
for rd in r["rounds"][1:]:
|
|
396
|
+
drop = prev - rd["tool_calls"]
|
|
397
|
+
if drop > 0 and prev > 0:
|
|
398
|
+
pct = round(drop / prev * 100)
|
|
399
|
+
if pct > r["decay_pct"]:
|
|
400
|
+
r["decay_pct"] = pct
|
|
401
|
+
r["decay_round"] = rd["round"]
|
|
402
|
+
prev = rd["tool_calls"]
|
|
403
|
+
return r
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def collect_aggregated(results_dir: Path) -> dict:
|
|
407
|
+
total_dur = 0; total_tc = 0; total_read = 0; total_in = 0; count = 0; hits = 0
|
|
408
|
+
total_hg_tc = 0; total_hg_effective = 0; total_hg_failed = 0
|
|
409
|
+
peak_agent_mb = 0.0; peak_hg_mb = 0.0; peak_combined_mb = 0.0
|
|
410
|
+
for exp_dir in sorted(results_dir.iterdir()):
|
|
411
|
+
if not exp_dir.is_dir(): continue
|
|
412
|
+
rj = exp_dir / "results.json"
|
|
413
|
+
if rj.exists():
|
|
414
|
+
d = json.loads(rj.read_text(encoding="utf-8"))
|
|
415
|
+
agent = d.get("agent", "?")
|
|
416
|
+
total_dur += d.get("duration_ms", 0)
|
|
417
|
+
tool_calls = d.get("tool_calls", 0)
|
|
418
|
+
files_read = len(d.get("files_read", []))
|
|
419
|
+
input_tokens = d.get("input_tokens", 0)
|
|
420
|
+
hg_tc = d.get("homegraph_tool_calls", 0)
|
|
421
|
+
total_hg_effective += d.get("homegraph_effective_calls", 0)
|
|
422
|
+
total_hg_failed += d.get("homegraph_failed_calls", 0)
|
|
423
|
+
mem = _memory_from_result(d)
|
|
424
|
+
peak_agent_mb = max(peak_agent_mb, mem["peak_rss_mb"])
|
|
425
|
+
peak_hg_mb = max(peak_hg_mb, mem["peak_homegraph_rss_mb"])
|
|
426
|
+
peak_combined_mb = max(peak_combined_mb, mem["peak_combined_rss_mb"])
|
|
427
|
+
stream = exp_dir / "stream_output.jsonl"
|
|
428
|
+
if stream.exists():
|
|
429
|
+
stats = _stream_stats(stream, agent)
|
|
430
|
+
if stats.tool_calls:
|
|
431
|
+
tool_calls = stats.tool_calls
|
|
432
|
+
files_read = len(set(stats.files_read))
|
|
433
|
+
if stats.total_input_tokens:
|
|
434
|
+
input_tokens = stats.total_input_tokens
|
|
435
|
+
if stats.homegraph_tool_calls:
|
|
436
|
+
hg_tc = stats.homegraph_tool_calls
|
|
437
|
+
total_tc += tool_calls
|
|
438
|
+
total_hg_tc += hg_tc
|
|
439
|
+
total_read += files_read
|
|
440
|
+
total_in += input_tokens
|
|
441
|
+
if d.get("max_turns_hit"): hits += 1
|
|
442
|
+
count += 1
|
|
443
|
+
sj = results_dir / "5" / "session_results.json"
|
|
444
|
+
if sj.exists():
|
|
445
|
+
d = json.loads(sj.read_text(encoding="utf-8"))
|
|
446
|
+
agent = d.get("agent", "?")
|
|
447
|
+
total_dur += d.get("total_duration_ms", 0)
|
|
448
|
+
session_tc = d.get("total_tool_calls", 0)
|
|
449
|
+
round_streams = sorted((results_dir / "5").glob("round_*/stream_output.jsonl"))
|
|
450
|
+
if round_streams:
|
|
451
|
+
session_tc = sum(_stream_stats(s, agent).tool_calls for s in round_streams)
|
|
452
|
+
total_tc += session_tc
|
|
453
|
+
count += 1
|
|
454
|
+
for rd in sorted((results_dir / "5").glob("round_*/round_results.json")):
|
|
455
|
+
rr = json.loads(rd.read_text(encoding="utf-8"))
|
|
456
|
+
mem = _memory_from_result(rr)
|
|
457
|
+
peak_agent_mb = max(peak_agent_mb, mem["peak_rss_mb"])
|
|
458
|
+
peak_hg_mb = max(peak_hg_mb, mem["peak_homegraph_rss_mb"])
|
|
459
|
+
peak_combined_mb = max(peak_combined_mb, mem["peak_combined_rss_mb"])
|
|
460
|
+
manifest = read_run_manifest(results_dir)
|
|
461
|
+
return dict(
|
|
462
|
+
experiment_count=count, total_dur_ms=total_dur, total_tool_calls=total_tc,
|
|
463
|
+
total_homegraph_tool_calls=total_hg_tc,
|
|
464
|
+
total_homegraph_effective_calls=total_hg_effective,
|
|
465
|
+
total_homegraph_failed_calls=total_hg_failed,
|
|
466
|
+
total_files_read=total_read, total_input_tokens=total_in, max_turns_hit_count=hits,
|
|
467
|
+
peak_rss_mb=round(peak_agent_mb, 1), peak_homegraph_rss_mb=round(peak_hg_mb, 1),
|
|
468
|
+
peak_combined_rss_mb=round(peak_combined_mb, 1),
|
|
469
|
+
homegraph_index_ms=manifest.get("homegraph_index_ms", 0),
|
|
470
|
+
homegraph_index_success=manifest.get("homegraph_index_success"),
|
|
471
|
+
homegraph_index_error=manifest.get("homegraph_index_error", ""),
|
|
472
|
+
homegraph_index_command=manifest.get("homegraph_index_command", ""),
|
|
473
|
+
homegraph_file_count=manifest.get("homegraph_file_count", 0),
|
|
474
|
+
homegraph_arkts_nodes=manifest.get("homegraph_arkts_nodes", 0),
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
# ═══════════════════════════════════════════════════════════
|
|
479
|
+
# Analysis & interpretation (derives insights from raw data)
|
|
480
|
+
# ═══════════════════════════════════════════════════════════
|
|
481
|
+
|
|
482
|
+
def analyze_gear_switching(summary: List[dict]) -> dict:
|
|
483
|
+
"""Compare 1-1, 1-2, 1-3 to analyze exploration gear shifts."""
|
|
484
|
+
d = {r["id"]: r for r in summary}
|
|
485
|
+
e11 = d.get("1-1", {}); e12 = d.get("1-2", {}); e13 = d.get("1-3", {})
|
|
486
|
+
if not all([e11, e12, e13]): return {"valid": False}
|
|
487
|
+
|
|
488
|
+
tc_ratio = e13.get("tool_calls", 1) / max(e11.get("tool_calls", 1), 1)
|
|
489
|
+
dur_ratio = float(str(e13.get("duration_s", "1")).replace("s", "")) / max(float(str(e11.get("duration_s", "1")).replace("s", "")), 0.1)
|
|
490
|
+
|
|
491
|
+
# Check if 1-2 trap worked (few files read despite many tools)
|
|
492
|
+
trap_worked = e12.get("tool_calls", 0) > 10 and e12.get("files_read", 0) <= 1
|
|
493
|
+
# Check if 1-3 went deep (many files)
|
|
494
|
+
deep = e13.get("files_read", 0) >= 5
|
|
495
|
+
|
|
496
|
+
return dict(valid=True,
|
|
497
|
+
zero_gap_tools=e11.get("tool_calls", 0), zero_gap_dur=e11.get("duration_s", "?"),
|
|
498
|
+
weak_gap_tools=e12.get("tool_calls", 0), weak_gap_dur=e12.get("duration_s", "?"),
|
|
499
|
+
strong_gap_tools=e13.get("tool_calls", 0), strong_gap_dur=e13.get("duration_s", "?"),
|
|
500
|
+
tool_span=f"{e11.get('tool_calls', 0)} → {e13.get('tool_calls', 0)} ({tc_ratio:.0f}x)",
|
|
501
|
+
dur_span=f"{e11.get('duration_s', '?')} → {e13.get('duration_s', '?')} ({dur_ratio:.0f}x)",
|
|
502
|
+
trap_worked=trap_worked, deep_confirmed=deep,
|
|
503
|
+
rating=_score_to_stars(5 if tc_ratio >= 10 and deep else (4 if tc_ratio >= 5 else 3)))
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def analyze_hallucination_detail(h: dict, summary: List[dict]) -> dict:
|
|
507
|
+
"""Deep analysis of hallucination pattern."""
|
|
508
|
+
if not h.get("has_results"): return {"valid": False}
|
|
509
|
+
pattern_labels = {
|
|
510
|
+
"yes_and": "「Yes, and...」型 — 先承认不存在,再编造替代解释",
|
|
511
|
+
"pure_fabrication": "纯编造型 — 直接描述不存在的内容",
|
|
512
|
+
"partial": "部分编造 — 混合真实代码和虚假结论",
|
|
513
|
+
"honest_denial": "诚实否认 — 明确告知未找到,无编造",
|
|
514
|
+
"none": "无编造",
|
|
515
|
+
}
|
|
516
|
+
e11 = next((r for r in summary if r["id"] == "1-1"), {})
|
|
517
|
+
tools_vs_baseline = h["tool_calls"] / max(e11.get("tool_calls", 1), 1)
|
|
518
|
+
return dict(valid=True,
|
|
519
|
+
pattern=h["pattern"], pattern_label=pattern_labels.get(h["pattern"], "未知"),
|
|
520
|
+
fab_score=h["fab_score"],
|
|
521
|
+
tools_wasted=h["tool_calls"], files_wasted=h["files_explored"],
|
|
522
|
+
tools_vs_baseline=f"{tools_vs_baseline:.0f}x",
|
|
523
|
+
real_code_explored=h["real_code_explored"][:5],
|
|
524
|
+
risk_level="🔴 高" if h["fab_score"] >= 5 else ("🟡 中" if h["fab_score"] >= 3 else "🟢 低"),
|
|
525
|
+
rating=_score_to_stars(1 if h["hallucinated"] else 5))
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def analyze_memory_decay_detail(mc: dict) -> dict:
|
|
529
|
+
if not mc.get("has_results") or len(mc["rounds"]) < 2: return {"valid": False}
|
|
530
|
+
rounds = mc["rounds"]
|
|
531
|
+
r1_tc = rounds[0]["tool_calls"]
|
|
532
|
+
r_last_tc = rounds[-1]["tool_calls"]
|
|
533
|
+
overall_decay = round((r1_tc - r_last_tc) / max(r1_tc, 1) * 100)
|
|
534
|
+
# Check if any later round re-explored (tools went up again)
|
|
535
|
+
re_explored = any(i > 1 and rounds[i]["tool_calls"] > rounds[i-1]["tool_calls"]
|
|
536
|
+
for i in range(1, len(rounds)))
|
|
537
|
+
return dict(valid=True,
|
|
538
|
+
r1_tools=r1_tc, r_last_tools=r_last_tc,
|
|
539
|
+
overall_decay_pct=overall_decay,
|
|
540
|
+
decay_inflection_round=mc["decay_round"],
|
|
541
|
+
decay_inflection_pct=mc["decay_pct"],
|
|
542
|
+
re_explored=re_explored,
|
|
543
|
+
rating=_score_to_stars(5 if overall_decay >= 70 and not re_explored
|
|
544
|
+
else (4 if overall_decay >= 50 else 3)))
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def compute_scores(data: dict) -> dict:
|
|
548
|
+
"""Compute final scorecard across 7 dimensions."""
|
|
549
|
+
s = {}
|
|
550
|
+
# 1. Gear switching
|
|
551
|
+
gs = data.get("gear_analysis", {})
|
|
552
|
+
s["探索档位切换"] = 5 if gs.get("tool_span", "1x").endswith("x") and int(gs.get("tool_span", "1x").split("x")[0]) >= 10 else (4 if gs.get("deep_confirmed") else 3)
|
|
553
|
+
# 2. Cross-layer
|
|
554
|
+
cl = data["cross_layer"]
|
|
555
|
+
s["跨层穿透"] = min(cl["layers_penetrated"], 4) if cl["layers_penetrated"] >= 2 else 2
|
|
556
|
+
# 3. Hallucination risk (inverted: high hallucination = low score)
|
|
557
|
+
h = data.get("hallucination_detail", {})
|
|
558
|
+
s["幻觉风险"] = 1 if h.get("fab_score", 0) >= 5 else (2 if h.get("fab_score", 0) >= 3 else 5)
|
|
559
|
+
# 4. Omission risk
|
|
560
|
+
sh = data["share"]
|
|
561
|
+
s["遗漏风险"] = 5 if sh.get("coverage_pct", 0) == 100 else (3 if sh.get("coverage_pct", 0) >= 50 else 1)
|
|
562
|
+
# 5. Exploration efficiency
|
|
563
|
+
o = data["over_exploration"]
|
|
564
|
+
s["探索效率"] = 5 if o.get("efficiency_pct", 0) >= 50 else (4 if o.get("efficiency_pct", 0) >= 25 else 3)
|
|
565
|
+
# 6. Non-code assets
|
|
566
|
+
dk = data["dependency"]
|
|
567
|
+
ml = data["multilang"]
|
|
568
|
+
asset_score = dk.get("score", 0) + (1 if ml.get("has_zh") else 0)
|
|
569
|
+
s["非代码资产"] = min(asset_score + 1, 5)
|
|
570
|
+
# 7. Session memory
|
|
571
|
+
md = data.get("memory_detail", {})
|
|
572
|
+
s["会话记忆"] = 5 if md.get("overall_decay_pct", 0) >= 70 else (4 if md.get("overall_decay_pct", 0) >= 40 else 3)
|
|
573
|
+
return s
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _score_to_stars(n: int) -> str:
|
|
577
|
+
return "⭐" * n + "☆" * (5 - n)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
# ═══════════════════════════════════════════════════════════
|
|
581
|
+
# Markdown report builder
|
|
582
|
+
# ═══════════════════════════════════════════════════════════
|
|
583
|
+
|
|
584
|
+
def build_md(results_dir: Path, data: dict) -> str:
|
|
585
|
+
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
|
586
|
+
ag = data["aggregated"]
|
|
587
|
+
n = max(ag["experiment_count"], 1)
|
|
588
|
+
gs = data.get("gear_analysis", {})
|
|
589
|
+
hd = data.get("hallucination_detail", {})
|
|
590
|
+
md = data.get("memory_detail", {})
|
|
591
|
+
scores = data.get("scores", {})
|
|
592
|
+
|
|
593
|
+
# ── Header & Executive Summary ──
|
|
594
|
+
agent_label = data["summary"][0].get("agent", "Unknown") if data["summary"] else "Unknown"
|
|
595
|
+
model_label = resolve_primary_model(data["summary"])
|
|
596
|
+
md_text = f"""# Agent Exploration 实验分析报告
|
|
597
|
+
|
|
598
|
+
> **生成时间**: {now} | **Agent**: {agent_label} | **模型**: {model_label}
|
|
599
|
+
> **仓库**: OpenHarmony Photos | **实验数**: {ag['experiment_count']} | **总耗时**: {ag['total_dur_ms'] / 1000:.1f}s ({ag['total_dur_ms'] / 60000:.1f}min)
|
|
600
|
+
> **总工具调用**: {ag['total_tool_calls']} | **总输入 Token**: {ag['total_input_tokens']:,}
|
|
601
|
+
|
|
602
|
+
---
|
|
603
|
+
|
|
604
|
+
## 总评分
|
|
605
|
+
|
|
606
|
+
| 维度 | 评分 | 说明 |
|
|
607
|
+
|------|:--:|------|
|
|
608
|
+
| 探索档位切换 | {_score_to_stars(scores.get('探索档位切换', 0))} | {gs.get('tool_span', 'N/A')} 跨度,{'深度探索已验证' if gs.get('deep_confirmed') else '待确认'} |
|
|
609
|
+
| 跨层穿透 | {_score_to_stars(scores.get('跨层穿透', 0))} | {data['cross_layer']['layers_penetrated']}/4 层穿透 |
|
|
610
|
+
| 幻觉风险 | {_score_to_stars(scores.get('幻觉风险', 0))} | {hd.get('pattern_label', 'N/A')},编造评分 {hd.get('fab_score', 0)}/11 |
|
|
611
|
+
| 遗漏风险 | {_score_to_stars(scores.get('遗漏风险', 0))} | 覆盖率 {data['share'].get('coverage', 'N/A')} |
|
|
612
|
+
| 探索效率 | {_score_to_stars(scores.get('探索效率', 0))} | 过度探索率 {data['over_exploration'].get('files_explored', 0)} 文件 |
|
|
613
|
+
| 非代码资产 | {_score_to_stars(scores.get('非代码资产', 0))} | oh-package/ohpm/多语言感知 |
|
|
614
|
+
| 会话记忆 | {_score_to_stars(scores.get('会话记忆', 0))} | 衰减 {md.get('overall_decay_pct', 0)}%,{'无重探索' if not md.get('re_explored', True) else '有部分重探索'} |
|
|
615
|
+
|
|
616
|
+
> **核心结论**: {agent_label} 在探索任务中表现出可预测的档位切换行为、{
|
|
617
|
+
'完整' if data['cross_layer']['layers_penetrated'] >= 3 else '部分'}的跨层穿透能力和{
|
|
618
|
+
'优秀' if md.get('overall_decay_pct', 0) >= 70 else '一般'}的上下文记忆,但{
|
|
619
|
+
'「Yes, and...」型幻觉是最大风险' if hd.get('pattern') == 'yes_and' else '幻觉风险需关注'}——{
|
|
620
|
+
'用真实代码片段包装虚假结论,极难通过自动化检测发现' if hd.get('fab_score', 0) >= 5 else ''}
|
|
621
|
+
|
|
622
|
+
---
|
|
623
|
+
|
|
624
|
+
## 数据总览
|
|
625
|
+
|
|
626
|
+
| Exp | Title | Model | Duration | Tool Calls | Files Read | Files Edited | Input Tokens | Max Turns? |
|
|
627
|
+
|-----|-------|-------|----------|------------|------------|--------------|--------------|------------|
|
|
628
|
+
"""
|
|
629
|
+
for r in data["summary"]:
|
|
630
|
+
it = r["input_tokens"]
|
|
631
|
+
it_str = f"{it // 1000:,}k" if isinstance(it, int) else str(it)
|
|
632
|
+
hit = "⚠️ YES" if r["max_turns_hit"] else ""
|
|
633
|
+
md_text += f"| {r['id']} | {r['title'][:40]} | {format_model_display(r.get('model', '?'))} | {r['duration_s']} | {r['tool_calls']} | {r['files_read']} | {r['files_edited']} | {it_str} | {hit} |\n"
|
|
634
|
+
|
|
635
|
+
# ── Aggregated metrics ──
|
|
636
|
+
md_text += f"""
|
|
637
|
+
---
|
|
638
|
+
|
|
639
|
+
## 汇总指标
|
|
640
|
+
|
|
641
|
+
| 指标 | 数值 |
|
|
642
|
+
|------|------|
|
|
643
|
+
| 实验总数 | {ag['experiment_count']} |
|
|
644
|
+
| 使用模型 | {model_label} |
|
|
645
|
+
| 总耗时 | {ag['total_dur_ms'] / 1000:.1f}s ({ag['total_dur_ms'] / 60000:.1f}min) |
|
|
646
|
+
| 总工具调用 | {ag['total_tool_calls']} |
|
|
647
|
+
| 总输入 Token | {ag['total_input_tokens']:,} |
|
|
648
|
+
| Max turns 命中 | {ag['max_turns_hit_count']} 个实验 |
|
|
649
|
+
| Agent 进程峰值内存 | {_format_mb(ag.get('peak_rss_mb'))} |
|
|
650
|
+
| HomeGraph 进程峰值内存 | {_format_mb(ag.get('peak_homegraph_rss_mb'))} |
|
|
651
|
+
| 合计峰值内存 | {_format_mb(ag.get('peak_combined_rss_mb'))} |
|
|
652
|
+
| HomeGraph 索引耗时 | {_format_index_ms(ag.get('homegraph_index_ms', 0))} |
|
|
653
|
+
| 平均耗时/实验 | {ag['total_dur_ms'] / 1000 / n:.1f}s |
|
|
654
|
+
| 平均工具调用/实验 | {ag['total_tool_calls'] / n:.1f} |
|
|
655
|
+
| 平均输入 Token/实验 | {ag['total_input_tokens'] // n:,} |
|
|
656
|
+
|
|
657
|
+
---
|
|
658
|
+
|
|
659
|
+
## 一、档位切换模型分析
|
|
660
|
+
|
|
661
|
+
### 量化对比
|
|
662
|
+
|
|
663
|
+
| 实验 | 工具调用 | 耗时 | 文件探索 | 档位 |
|
|
664
|
+
|------|---------|------|---------|------|
|
|
665
|
+
| 1-1 零缺口 | **{gs.get('zero_gap_tools', '?')}** | {gs.get('zero_gap_dur', '?')} | 1 | 快速检索 |
|
|
666
|
+
| 1-2 弱缺口 | **{gs.get('weak_gap_tools', '?')}** | {gs.get('weak_gap_dur', '?')} | 1 | {'⚠️ 中深度(trap 生效:在 common 模块反复搜索)' if gs.get('trap_worked') else '中深度'} |
|
|
667
|
+
| 1-3 强缺口 | **{gs.get('strong_gap_tools', '?')}** | {gs.get('strong_gap_dur', '?')} | {data['summary'][2]['files_read'] if len(data['summary']) > 2 else '?'} | {'✓ 深度探索' if gs.get('deep_confirmed') else '中深度'} |
|
|
668
|
+
|
|
669
|
+
**工具调用跨度**: {gs.get('tool_span', 'N/A')}
|
|
670
|
+
**耗时跨度**: {gs.get('dur_span', 'N/A')}
|
|
671
|
+
|
|
672
|
+
### 结论
|
|
673
|
+
|
|
674
|
+
信息缺口大小与探索深度呈正比。{'1-2 的 trap(目标在 feature/thirdselect 而非 common)生效——Agent 在被误导的模块中消耗了额外探索预算。' if gs.get('trap_worked') else ''}
|
|
675
|
+
|
|
676
|
+
---
|
|
677
|
+
|
|
678
|
+
## 二、跨层穿透能力
|
|
679
|
+
|
|
680
|
+
### 层次覆盖
|
|
681
|
+
|
|
682
|
+
| 层 | 文件数 | 状态 |
|
|
683
|
+
|----|--------|------|
|
|
684
|
+
| ArkTS (.ets) | {data['cross_layer']['arkts']} | {'✓' if data['cross_layer']['arkts'] > 0 else '✗'} |
|
|
685
|
+
| NAPI (native.ts) | {data['cross_layer']['napi']} | {'✓ 找到唯一 bridge' if data['cross_layer']['napi'] > 0 else '✗'} |
|
|
686
|
+
| C++ (.cpp/.h) | {data['cross_layer']['cpp']} | {'✓ 深入引擎层' if data['cross_layer']['cpp'] > 0 else '✗'} |
|
|
687
|
+
| CMake | {data['cross_layer']['cmake']} | {'✓' if data['cross_layer']['cmake'] > 0 else '✗'} |
|
|
688
|
+
|
|
689
|
+
**穿透层数**: {data['cross_layer']['layers_penetrated']}/4
|
|
690
|
+
**工具调用**: {data['cross_layer']['tool_calls']} | Max turns: {'⚠️ 命中' if data['cross_layer']['max_turns_hit'] else '✓ 未触发'}
|
|
691
|
+
"""
|
|
692
|
+
|
|
693
|
+
# ── Hallucination ──
|
|
694
|
+
h = data["hallucination"]
|
|
695
|
+
if h["has_results"]:
|
|
696
|
+
risk_emoji = "🔴" if hd.get("risk_level", "").startswith("🔴") else ("🟡" if hd.get("risk_level", "").startswith("🟡") else "🟢")
|
|
697
|
+
md_text += f"""
|
|
698
|
+
---
|
|
699
|
+
|
|
700
|
+
## 三、幻觉分析 {risk_emoji}
|
|
701
|
+
|
|
702
|
+
### 检测结果
|
|
703
|
+
|
|
704
|
+
| 指标 | 数值 |
|
|
705
|
+
|------|------|
|
|
706
|
+
| 工具调用(浪费) | **{h['tool_calls']}**(是基线 1-1 的 {hd.get('tools_vs_baseline', '?')} 倍) |
|
|
707
|
+
| 文件探索(浪费) | **{h['files_explored']}** |
|
|
708
|
+
| 编造评分 | **{h['fab_score']}/11** |
|
|
709
|
+
| 幻觉模式 | **{hd.get('pattern_label', 'N/A')}** |
|
|
710
|
+
| 风险等级 | {hd.get('risk_level', 'N/A')} |
|
|
711
|
+
| 不确定性表达 | {', '.join(h['qualifiers']) if h.get('qualifiers') else '无'} |
|
|
712
|
+
|
|
713
|
+
### 模式分析
|
|
714
|
+
|
|
715
|
+
"""
|
|
716
|
+
if hd.get("pattern") == "yes_and":
|
|
717
|
+
md_text += f"""**「Yes, and...」型幻觉** — 最危险的谎言模式:
|
|
718
|
+
|
|
719
|
+
1. Agent 搜索 `PhotoWatermarkEngine` → 未找到
|
|
720
|
+
2. 搜索 `watermark` / `水印` → **找到真实存在的编辑引擎水印 sticker 代码**
|
|
721
|
+
3. 将编辑引擎的代码**错误归因**为 `PhotoWatermarkEngine` 模块
|
|
722
|
+
4. 输出包含真实文件路径、枚举值、C++ 数据结构的「完整架构分析」
|
|
723
|
+
|
|
724
|
+
**关键风险**: 这不是纯粹的编造,而是**对真实代码的错误语义解释**。只检查文件路径无法发现——需要人工验证输出的语义正确性。
|
|
725
|
+
|
|
726
|
+
**被错误引用的真实代码**(Agent 将这些代码归属到不存在的模块下):
|
|
727
|
+
"""
|
|
728
|
+
for f in hd.get("real_code_explored", [])[:5]:
|
|
729
|
+
md_text += f"- `{f}`\n"
|
|
730
|
+
elif hd.get("pattern") == "honest_denial":
|
|
731
|
+
md_text += "Agent 明确告知未找到目标模块,无编造行为。✓\n"
|
|
732
|
+
else:
|
|
733
|
+
md_text += f"幻觉模式: {hd.get('pattern', 'unknown')}\n"
|
|
734
|
+
|
|
735
|
+
# ── Share coverage ──
|
|
736
|
+
s = data["share"]
|
|
737
|
+
if s["has_results"]:
|
|
738
|
+
md_text += f"""
|
|
739
|
+
---
|
|
740
|
+
|
|
741
|
+
## 四、遗漏检测
|
|
742
|
+
|
|
743
|
+
| 指标 | 数值 |
|
|
744
|
+
|------|------|
|
|
745
|
+
| 工具调用 | {s['tool_calls']} |
|
|
746
|
+
| 文件探索 | {s['files_explored']} |
|
|
747
|
+
| 覆盖率 | **{s['coverage']}** ({s.get('coverage_pct', 0):.0f}%) |
|
|
748
|
+
| 搜索策略 | {'地毯式(' + str(s['tool_calls']) + ' tools)' if s['tool_calls'] > 100 else ('标准(' + str(s['tool_calls']) + ' tools)')} |
|
|
749
|
+
|
|
750
|
+
"""
|
|
751
|
+
if s["found"]:
|
|
752
|
+
md_text += "**命中的文件**:\n"
|
|
753
|
+
for f in s["found"]:
|
|
754
|
+
md_text += f"- ✓ `{f['name']}`\n"
|
|
755
|
+
if s["missed"]:
|
|
756
|
+
md_text += "\n**遗漏的文件**:\n"
|
|
757
|
+
for m in s["missed"]:
|
|
758
|
+
md_text += f"- ✗ `{m}` — **命名陷阱生效**\n"
|
|
759
|
+
if not s["missed"]:
|
|
760
|
+
md_text += "\n本次实验**零遗漏**,包括命名陷阱 `KnockShareUtil.ets` 也被正确找到。Agent 采用地毯式搜索策略(grep + 目录遍历 + 文件读取),覆盖率优秀。\n"
|
|
761
|
+
|
|
762
|
+
# ── Over-exploration ──
|
|
763
|
+
o = data["over_exploration"]
|
|
764
|
+
if o["has_results"]:
|
|
765
|
+
md_text += f"""
|
|
766
|
+
---
|
|
767
|
+
|
|
768
|
+
## 五、过度探索分析
|
|
769
|
+
|
|
770
|
+
| 指标 | 数值 |
|
|
771
|
+
|------|------|
|
|
772
|
+
| 工具调用 | {o['tool_calls']} |
|
|
773
|
+
| 总文件探索 | {o['files_explored']} |
|
|
774
|
+
| Utils 文件探索 | {o['utils_files']}(共 72 个可用) |
|
|
775
|
+
| 命中最优目标 (ImageUtil.ets) | {'✓' if o['target_hit'] else '✗'} |
|
|
776
|
+
| 探索效率 | {o.get('efficiency_pct', 0)}% |
|
|
777
|
+
|
|
778
|
+
**判定**: {'**无过度探索** — Agent 精确定位到最优文件,仅做一次对比阅读(ImageSizeUtil.ets)后即做出决策。' if o.get('efficiency_pct', 0) >= 50 else ('**轻微探索** — 读取了 ' + str(o['utils_files']) + ' 个 Utils 文件后定位目标。')}
|
|
779
|
+
|
|
780
|
+
"""
|
|
781
|
+
|
|
782
|
+
# ── Non-code assets ──
|
|
783
|
+
ml = data["multilang"]
|
|
784
|
+
dk = data["dependency"]
|
|
785
|
+
md_text += f"""
|
|
786
|
+
---
|
|
787
|
+
|
|
788
|
+
## 六、非代码资产感知
|
|
789
|
+
|
|
790
|
+
### 6.1 资源配置 (Exp 4-1)
|
|
791
|
+
|
|
792
|
+
"""
|
|
793
|
+
if ml["has_results"]:
|
|
794
|
+
md_text += f"""| 指标 | 数值 |
|
|
795
|
+
|------|------|
|
|
796
|
+
| 命中的 string.json | {ml['count']} 个 |
|
|
797
|
+
| 包含 base 语言 | {'✓' if ml['has_base'] else '✗'} |
|
|
798
|
+
| 包含中文 locale | {'✓' if ml['has_zh'] else '✗'} |
|
|
799
|
+
"""
|
|
800
|
+
if ml.get("has_zh"):
|
|
801
|
+
md_text += 'Agent 识别了中文 `zh_CN` 资源文件的差异(值为「图库」而非「Gallery」),做了正确的差异化处理。✓\n'
|
|
802
|
+
|
|
803
|
+
md_text += f"""
|
|
804
|
+
### 6.2 构建系统 (Exp 4-3)
|
|
805
|
+
|
|
806
|
+
| 检查项 | 结果 |
|
|
807
|
+
|--------|------|
|
|
808
|
+
| oh-package.json5 识别 | {'✓' if dk['oh_package'] else '✗'} |
|
|
809
|
+
| ohpm 包管理器识别 | {'✓' if dk['ohpm'] else '✗'} |
|
|
810
|
+
| BUILD.gn 错误引用 | {'⚠️ 提及了不存在的构建系统' if dk['build_gn_wrong'] else '✓ 未错误引用'} |
|
|
811
|
+
| 综合评分 | {dk.get('score', 0)}/3 |
|
|
812
|
+
"""
|
|
813
|
+
|
|
814
|
+
# ── Memory curve ──
|
|
815
|
+
mc = data["memory"]
|
|
816
|
+
if md.get("valid") and mc.get("has_results"):
|
|
817
|
+
md_text += f"""
|
|
818
|
+
---
|
|
819
|
+
|
|
820
|
+
## 七、会话持久性
|
|
821
|
+
|
|
822
|
+
### 记忆保留曲线
|
|
823
|
+
|
|
824
|
+
| Round | Tool Calls | Files Read | vs Previous | 行为 |
|
|
825
|
+
|-------|------------|------------|-------------|------|
|
|
826
|
+
"""
|
|
827
|
+
prev = mc["rounds"][0]["tool_calls"] if mc["rounds"] else 1
|
|
828
|
+
for i, r in enumerate(mc["rounds"]):
|
|
829
|
+
tc = r["tool_calls"]
|
|
830
|
+
delta_pct = round((tc - prev) / max(prev, 1) * 100)
|
|
831
|
+
if i == 0:
|
|
832
|
+
behavior = "建立上下文"
|
|
833
|
+
elif delta_pct < -50:
|
|
834
|
+
behavior = f"↓ 复用上下文({-delta_pct}% 减少)"
|
|
835
|
+
elif delta_pct > 20:
|
|
836
|
+
behavior = f"↑ 跨模块微增"
|
|
837
|
+
else:
|
|
838
|
+
behavior = "→ 记忆保留"
|
|
839
|
+
bar = "█" * (tc // 3)
|
|
840
|
+
md_text += f"| {r['round']} | {tc} | {r['files_read']} | {delta_pct:+d}% | {bar} {behavior} |\n"
|
|
841
|
+
prev = tc
|
|
842
|
+
|
|
843
|
+
md_text += f"""
|
|
844
|
+
### 衰减分析
|
|
845
|
+
|
|
846
|
+
| 指标 | 数值 |
|
|
847
|
+
|------|------|
|
|
848
|
+
| 初始探索量 (R1) | {md['r1_tools']} tools |
|
|
849
|
+
| 最终探索量 (R5) | {md['r_last_tools']} tools |
|
|
850
|
+
| 总体衰减率 | **{md['overall_decay_pct']}%** |
|
|
851
|
+
| 最大衰减拐点 | Round {md['decay_inflection_round']} → Round {md['decay_inflection_round'] + 1} ({md['decay_inflection_pct']}% 下降) |
|
|
852
|
+
| 重探索行为 | {'⚠️ 检测到重探索(后续轮工具调用反升)' if md.get('re_explored') else '✓ 无重探索,记忆持续保留'} |
|
|
853
|
+
|
|
854
|
+
**判定**: 上下文建立后,后续任务探索量下降 **{md['overall_decay_pct']}%**。{'跨模块任务(Round 3)有小幅回升,但回到同模块任务后(Round 4-5)继续维持低探索量。Session 持久性在 5 轮内表现优秀。' if md['overall_decay_pct'] >= 70 else ''}
|
|
855
|
+
"""
|
|
856
|
+
|
|
857
|
+
# ── Footer ──
|
|
858
|
+
md_text += f"""
|
|
859
|
+
---
|
|
860
|
+
|
|
861
|
+
*报告由 `analyze.py` 自动生成 | 原始数据: `{results_dir}`*
|
|
862
|
+
"""
|
|
863
|
+
return md_text
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
# ═══════════════════════════════════════════════════════════
|
|
867
|
+
# Terminal output
|
|
868
|
+
# ═══════════════════════════════════════════════════════════
|
|
869
|
+
|
|
870
|
+
def print_summary(rows: List[dict]):
|
|
871
|
+
agent_name = rows[0].get("agent", "?") if rows else "?"
|
|
872
|
+
hdr = f"{'Exp':<6} | {'Title':<34} | {'Dur':>6} | {'Tools':>6} | {'Read':>5} | {'Edit':>5} | {'InputTok':>9} | {'Turns?':>7}"
|
|
873
|
+
print(f"\n Agent: {agent_name}\n" + hdr); print("-" * len(hdr))
|
|
874
|
+
for r in rows:
|
|
875
|
+
it = r["input_tokens"]; it_str = f"{it // 1000}k" if isinstance(it, int) else str(it)
|
|
876
|
+
hit = "⚠️" if r["max_turns_hit"] else ""
|
|
877
|
+
print(f"{r['id']:<6} | {r['title'][:34]:<34} | {str(r['duration_s']):>6} | "
|
|
878
|
+
f"{str(r['tool_calls']):>6} | {str(r['files_read']):>5} | "
|
|
879
|
+
f"{str(r['files_edited']):>5} | {it_str:>9} | {hit:>7}")
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
def print_all(results_dir: Path, data: dict):
|
|
883
|
+
header("Agent Exploration — Results Analysis")
|
|
884
|
+
print_summary(data["summary"])
|
|
885
|
+
gs = data.get("gear_analysis", {})
|
|
886
|
+
if gs.get("valid"):
|
|
887
|
+
header("档位切换 (Exp 1-1 → 1-3)")
|
|
888
|
+
print(f" Tool span : {gs['tool_span']} | Dur span: {gs['dur_span']}")
|
|
889
|
+
print(f" Trap 1-2 : {'✓生效' if gs.get('trap_worked') else '✗未生效'}")
|
|
890
|
+
print(f" Deep 1-3 : {'✓深度探索' if gs.get('deep_confirmed') else '✗未达深度'}")
|
|
891
|
+
hd = data.get("hallucination_detail", {})
|
|
892
|
+
if hd.get("valid"):
|
|
893
|
+
header(f"幻觉检测 (Exp 3-1) — {hd.get('risk_level', 'N/A')}")
|
|
894
|
+
print(f" Pattern : {hd.get('pattern_label', 'N/A')}")
|
|
895
|
+
print(f" Score : {hd.get('fab_score', 0)}/11 | Tools wasted: {hd.get('tools_wasted', 0)}")
|
|
896
|
+
s = data["share"]
|
|
897
|
+
if s["has_results"]:
|
|
898
|
+
header(f"分享覆盖 (Exp 3-2) — {s.get('coverage', '?')}")
|
|
899
|
+
if s.get("missed"): print(f" Missed : {s['missed']}")
|
|
900
|
+
else: print(" All 6/6 found — 零遗漏")
|
|
901
|
+
cl = data["cross_layer"]
|
|
902
|
+
if cl["has_results"]:
|
|
903
|
+
header(f"跨层穿透 (Exp 2) — {cl['layers_penetrated']}/4 层")
|
|
904
|
+
print(f" ArkTS:{cl['arkts']} NAPI:{cl['napi']} C++:{cl['cpp']} CMake:{cl['cmake']}")
|
|
905
|
+
o = data["over_exploration"]
|
|
906
|
+
if o["has_results"]:
|
|
907
|
+
header(f"过度探索 (Exp 3-3) — 效率 {o.get('efficiency_pct', 0)}%")
|
|
908
|
+
print(f" Utils explored: {o['utils_files']} | Target: {'✓' if o['target_hit'] else '✗'}")
|
|
909
|
+
mc = data.get("memory", {})
|
|
910
|
+
if mc.get("has_results"):
|
|
911
|
+
header(f"记忆衰减 (Exp 5) — {data.get('memory_detail', {}).get('overall_decay_pct', 0)}%")
|
|
912
|
+
for r in mc["rounds"]:
|
|
913
|
+
print(f" R{r['round']}: {r['tool_calls']} tools ({'█' * (r['tool_calls'] // 3)})")
|
|
914
|
+
header("总评分")
|
|
915
|
+
for dim, score in data.get("scores", {}).items():
|
|
916
|
+
print(f" {dim:<12} : {_score_to_stars(score)}")
|
|
917
|
+
ag = data["aggregated"]
|
|
918
|
+
print(f"\n Total: {ag['total_tool_calls']} tools | {ag['total_input_tokens']:,} tokens | {ag['total_dur_ms'] / 60000:.1f}min")
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
def _parse_duration_s(val) -> float:
|
|
922
|
+
"""Parse duration_s field ('15.2', '163.7s', 15.2) to seconds."""
|
|
923
|
+
if isinstance(val, (int, float)):
|
|
924
|
+
return float(val)
|
|
925
|
+
s = str(val).strip().lower().rstrip("s")
|
|
926
|
+
try:
|
|
927
|
+
return float(s)
|
|
928
|
+
except ValueError:
|
|
929
|
+
return 0.0
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
def _pct_change(baseline: float, homegraph: float) -> str:
|
|
933
|
+
if baseline == 0:
|
|
934
|
+
return "—" if homegraph == 0 else "+∞"
|
|
935
|
+
pct = (homegraph - baseline) / baseline * 100
|
|
936
|
+
sign = "+" if pct > 0 else ""
|
|
937
|
+
return f"{sign}{pct:.0f}%"
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
def _delta_str(baseline: float, homegraph: float, unit: str = "") -> str:
|
|
941
|
+
d = homegraph - baseline
|
|
942
|
+
sign = "+" if d > 0 else ""
|
|
943
|
+
if unit == "s":
|
|
944
|
+
return f"{sign}{d:.1f}s"
|
|
945
|
+
if unit == "min":
|
|
946
|
+
return f"{sign}{d / 60:.1f}min"
|
|
947
|
+
if isinstance(baseline, int) or isinstance(homegraph, int):
|
|
948
|
+
return f"{sign}{int(d)}"
|
|
949
|
+
return f"{sign}{d:.1f}"
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
def _format_wall_duration(seconds: int) -> str:
|
|
953
|
+
return f"{seconds // 60}m {seconds % 60}s"
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
def gather_data(results_dir: Path) -> dict:
|
|
957
|
+
"""Collect metrics and derived insights for one run directory."""
|
|
958
|
+
data = dict(
|
|
959
|
+
summary=collect_summary(results_dir),
|
|
960
|
+
hallucination=collect_hallucination(results_dir),
|
|
961
|
+
share=collect_share_coverage(results_dir),
|
|
962
|
+
cross_layer=collect_cross_layer(results_dir),
|
|
963
|
+
over_exploration=collect_over_exploration(results_dir),
|
|
964
|
+
multilang=collect_multilang(results_dir),
|
|
965
|
+
dependency=collect_dependency_knowledge(results_dir),
|
|
966
|
+
memory=collect_memory_curve(results_dir),
|
|
967
|
+
aggregated=collect_aggregated(results_dir),
|
|
968
|
+
)
|
|
969
|
+
data["gear_analysis"] = analyze_gear_switching(data["summary"])
|
|
970
|
+
data["hallucination_detail"] = analyze_hallucination_detail(data["hallucination"], data["summary"])
|
|
971
|
+
data["memory_detail"] = analyze_memory_decay_detail(data["memory"])
|
|
972
|
+
data["scores"] = compute_scores(data)
|
|
973
|
+
return data
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
def analyze_run(results_dir: Path, *, input_path: Optional[Path] = None,
|
|
977
|
+
verbose: bool = True) -> Path:
|
|
978
|
+
"""Analyze one run and write analysis_report.md. Returns report path."""
|
|
979
|
+
results_dir = resolve_results_dir(Path(results_dir))
|
|
980
|
+
if not _has_experiment_outputs(results_dir):
|
|
981
|
+
raise ValueError(f"No experiment outputs under: {results_dir}")
|
|
982
|
+
|
|
983
|
+
input_path = Path(input_path) if input_path else results_dir
|
|
984
|
+
data = gather_data(results_dir)
|
|
985
|
+
if verbose:
|
|
986
|
+
print_all(results_dir, data)
|
|
987
|
+
report_path = resolve_report_path(input_path, results_dir)
|
|
988
|
+
report_path.write_text(build_md(results_dir, data), encoding="utf-8")
|
|
989
|
+
if verbose:
|
|
990
|
+
print(f"\n{GREEN}[ANALYZE]{NC} Report: {report_path}")
|
|
991
|
+
return report_path
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
def _format_index_ms(ms: int) -> str:
|
|
995
|
+
if not ms:
|
|
996
|
+
return "—(未记录或 baseline 组无此步骤)"
|
|
997
|
+
return f"{ms / 1000:.1f}s"
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
def _overhead_s(wall_s: int, agent_ms: int, index_ms: int = 0) -> str:
|
|
1001
|
+
if not wall_s:
|
|
1002
|
+
return "—"
|
|
1003
|
+
overhead = wall_s - agent_ms / 1000 - index_ms / 1000
|
|
1004
|
+
return f"≈{max(overhead, 0):.0f}s"
|
|
1005
|
+
|
|
1006
|
+
|
|
1007
|
+
def _overhead_s(wall_s: int, agent_ms: int, index_ms: int = 0) -> str:
|
|
1008
|
+
if not wall_s:
|
|
1009
|
+
return "—"
|
|
1010
|
+
overhead = wall_s - agent_ms / 1000 - index_ms / 1000
|
|
1011
|
+
return f"≈{max(overhead, 0):.0f}s"
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
def _homegraph_tool_counts(tool_names: list) -> dict:
|
|
1015
|
+
counts = {}
|
|
1016
|
+
for name in tool_names or []:
|
|
1017
|
+
if is_homegraph_tool(name):
|
|
1018
|
+
counts[name] = counts.get(name, 0) + 1
|
|
1019
|
+
return counts
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
def _format_hg_tool_detail(hg_tools: dict) -> str:
|
|
1023
|
+
if not hg_tools:
|
|
1024
|
+
return "—"
|
|
1025
|
+
return ", ".join(f"`{name}` ×{n}" for name, n in sorted(hg_tools.items()))
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
def _build_ab_validity_section(b_sum: dict, h_sum: dict, exp_ids: list,
|
|
1029
|
+
h_ag: dict) -> str:
|
|
1030
|
+
b_total = sum(r.get("homegraph_tool_calls", 0) for r in b_sum.values())
|
|
1031
|
+
h_total = sum(r.get("homegraph_tool_calls", 0) for r in h_sum.values())
|
|
1032
|
+
h_effective = sum(r.get("homegraph_effective_calls", 0) for r in h_sum.values())
|
|
1033
|
+
h_failed = sum(r.get("homegraph_failed_calls", 0) for r in h_sum.values())
|
|
1034
|
+
index_cmd = h_ag.get("homegraph_index_command", "index")
|
|
1035
|
+
index_ok = h_ag.get("homegraph_index_success")
|
|
1036
|
+
if index_ok is True:
|
|
1037
|
+
index_label = f"成功(`{index_cmd}`"
|
|
1038
|
+
if h_ag.get("homegraph_index_ms"):
|
|
1039
|
+
index_label += f", {h_ag['homegraph_index_ms'] / 1000:.1f}s"
|
|
1040
|
+
fc = h_ag.get("homegraph_file_count")
|
|
1041
|
+
if fc:
|
|
1042
|
+
index_label += f", {fc} files"
|
|
1043
|
+
index_label += ")"
|
|
1044
|
+
elif index_ok is False:
|
|
1045
|
+
index_label = f"失败(`{index_cmd}`)"
|
|
1046
|
+
err = h_ag.get("homegraph_index_error", "")
|
|
1047
|
+
if err:
|
|
1048
|
+
index_label += f":{err[:100]}"
|
|
1049
|
+
else:
|
|
1050
|
+
index_label = "—"
|
|
1051
|
+
md = f"""## A/B 有效性
|
|
1052
|
+
|
|
1053
|
+
**Baseline** HomeGraph 工具调用合计:**{b_total}**
|
|
1054
|
+
|
|
1055
|
+
**HomeGraph** HomeGraph 工具调用合计:**{h_total}**(有效 **{h_effective}** / 失败 **{h_failed}**)
|
|
1056
|
+
|
|
1057
|
+
**HomeGraph 索引**:**{index_label}**
|
|
1058
|
+
|
|
1059
|
+
| 实验 | Baseline HG | HomeGraph HG | HomeGraph 有效 | HomeGraph 失败 | HomeGraph 各工具(次数) |
|
|
1060
|
+
|------|------------|--------------|----------------|----------------|-------------------------|
|
|
1061
|
+
"""
|
|
1062
|
+
for eid in exp_ids:
|
|
1063
|
+
br, hr = b_sum.get(eid, {}), h_sum.get(eid, {})
|
|
1064
|
+
b_n = br.get("homegraph_tool_calls", 0)
|
|
1065
|
+
h_n = hr.get("homegraph_tool_calls", 0)
|
|
1066
|
+
h_eff = hr.get("homegraph_effective_calls", 0)
|
|
1067
|
+
h_fail = hr.get("homegraph_failed_calls", 0)
|
|
1068
|
+
h_detail = _format_hg_tool_detail(hr.get("homegraph_tools", {}))
|
|
1069
|
+
md += f"| {eid} | {b_n} | {h_n} | {h_eff} | {h_fail} | {h_detail} |\n"
|
|
1070
|
+
return md
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
_FIXED_EXPERIMENT_EXPECTATIONS = """## 附录:实验预期(普适分析)
|
|
1074
|
+
|
|
1075
|
+
> 本节为**固定参考**,与上方实测数据无关,用于解读档位切换实验的设计意图。
|
|
1076
|
+
|
|
1077
|
+
### Exp 1-1 — 零缺口任务(改常量)
|
|
1078
|
+
|
|
1079
|
+
- **任务特征**:Prompt 已给出完整文件路径,只需定位一行常量并修改。
|
|
1080
|
+
- **耗时预期**:Baseline 通常更快 — 一次 `read` + 一次 `edit` 即可,HomeGraph 的 MCP 往返是额外开销。
|
|
1081
|
+
- **Token 预期**:两组接近;Baseline 上下文更小,HomeGraph 组可能因 explore 返回结构化源码而略高或略低。
|
|
1082
|
+
- **工具预期**:Baseline 2–4 次工具调用;HomeGraph 组若仍调用 explore,工具数可能相当或略多。
|
|
1083
|
+
|
|
1084
|
+
### Exp 1-2 — 弱缺口任务(跨模块定位)
|
|
1085
|
+
|
|
1086
|
+
- **任务特征**:需找到 `ThirdSelectAlbumGridBase`,文件在 `feature/thirdselect` 而非 `common`(命名陷阱)。
|
|
1087
|
+
- **耗时预期**:不确定 — Baseline 可能多次 grep 试错;HomeGraph explore 若一次命中可更快,若 query 不准则相当。
|
|
1088
|
+
- **Token 预期**:HomeGraph 组有望更低 — 减少逐文件 read 的上下文累积。
|
|
1089
|
+
- **工具预期**:Baseline 工具调用可能较多(glob/grep/read 组合);HomeGraph 组 explore 成功时可显著减少 read 次数。
|
|
1090
|
+
|
|
1091
|
+
### Exp 1-3 — 强缺口任务(跨文件分析)
|
|
1092
|
+
|
|
1093
|
+
- **任务特征**:梳理图片加载失败的处理逻辑,涉及多个组件与错误状态,纯分析不改代码。
|
|
1094
|
+
- **耗时预期**:HomeGraph 组通常更有优势 — 一次 explore 可串联调用链,减少盲目 grep/read。
|
|
1095
|
+
- **Token 预期**:HomeGraph 组应明显更低 — 避免大量文件全文进入上下文。
|
|
1096
|
+
- **工具预期**:Baseline 可能需要 10+ 次 read/grep;HomeGraph 组 1–3 次 explore + 少量补充 read 即可覆盖。
|
|
1097
|
+
"""
|
|
1098
|
+
|
|
1099
|
+
|
|
1100
|
+
def build_compare_md(baseline_dir: Path, homegraph_dir: Path, baseline_data: dict,
|
|
1101
|
+
homegraph_data: dict, *, agent: str = "",
|
|
1102
|
+
baseline_wall_s: int = 0, homegraph_wall_s: int = 0) -> str:
|
|
1103
|
+
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
|
1104
|
+
b_ag = baseline_data["aggregated"]
|
|
1105
|
+
h_ag = homegraph_data["aggregated"]
|
|
1106
|
+
b_sum = {r["id"]: r for r in baseline_data["summary"]}
|
|
1107
|
+
h_sum = {r["id"]: r for r in homegraph_data["summary"]}
|
|
1108
|
+
exp_ids = sorted(set(b_sum) | set(h_sum))
|
|
1109
|
+
|
|
1110
|
+
agent_label = agent or baseline_data["summary"][0].get("agent", "?") if baseline_data["summary"] else "?"
|
|
1111
|
+
|
|
1112
|
+
md = f"""# HomeGraph A/B 对比报告
|
|
1113
|
+
|
|
1114
|
+
> **生成时间**: {now}
|
|
1115
|
+
> **Agent**: {agent_label}
|
|
1116
|
+
> **Baseline(无 HomeGraph)**: `{baseline_dir}`
|
|
1117
|
+
> **HomeGraph(有 MCP)**: `{homegraph_dir}`
|
|
1118
|
+
|
|
1119
|
+
---
|
|
1120
|
+
|
|
1121
|
+
## 汇总对比
|
|
1122
|
+
|
|
1123
|
+
| 指标 | Baseline | HomeGraph | 差值 | 相对变化 |
|
|
1124
|
+
|------|----------|-----------|------|----------|
|
|
1125
|
+
| 整组墙钟耗时 | {_format_wall_duration(baseline_wall_s) if baseline_wall_s else '—'} | {_format_wall_duration(homegraph_wall_s) if homegraph_wall_s else '—'} | {_delta_str(baseline_wall_s, homegraph_wall_s, 'min') if baseline_wall_s and homegraph_wall_s else '—'} | {_pct_change(baseline_wall_s, homegraph_wall_s) if baseline_wall_s and homegraph_wall_s else '—'} |
|
|
1126
|
+
| Agent 累计耗时 | {b_ag['total_dur_ms'] / 1000:.1f}s | {h_ag['total_dur_ms'] / 1000:.1f}s | {_delta_str(b_ag['total_dur_ms'] / 1000, h_ag['total_dur_ms'] / 1000, 's')} | {_pct_change(b_ag['total_dur_ms'] / 1000, h_ag['total_dur_ms'] / 1000)} |
|
|
1127
|
+
| 总工具调用 | {b_ag['total_tool_calls']} | {h_ag['total_tool_calls']} | {_delta_str(b_ag['total_tool_calls'], h_ag['total_tool_calls'])} | {_pct_change(b_ag['total_tool_calls'], h_ag['total_tool_calls'])} |
|
|
1128
|
+
| HomeGraph 工具调用 | {b_ag.get('total_homegraph_tool_calls', 0)} | {h_ag.get('total_homegraph_tool_calls', 0)} | {_delta_str(b_ag.get('total_homegraph_tool_calls', 0), h_ag.get('total_homegraph_tool_calls', 0))} | — |
|
|
1129
|
+
| 总读取文件数 | {b_ag['total_files_read']} | {h_ag['total_files_read']} | {_delta_str(b_ag['total_files_read'], h_ag['total_files_read'])} | {_pct_change(b_ag['total_files_read'], h_ag['total_files_read'])} |
|
|
1130
|
+
| 总 Input Token | {b_ag['total_input_tokens']:,} | {h_ag['total_input_tokens']:,} | {_delta_str(b_ag['total_input_tokens'], h_ag['total_input_tokens'])} | {_pct_change(b_ag['total_input_tokens'], h_ag['total_input_tokens'])} |
|
|
1131
|
+
| Max turns 命中 | {b_ag['max_turns_hit_count']} | {h_ag['max_turns_hit_count']} | {_delta_str(b_ag['max_turns_hit_count'], h_ag['max_turns_hit_count'])} | — |
|
|
1132
|
+
| Agent 进程峰值内存 | {_format_mb(b_ag.get('peak_rss_mb'))} | {_format_mb(h_ag.get('peak_rss_mb'))} | — | — |
|
|
1133
|
+
| HomeGraph 进程峰值内存 | {_format_mb(b_ag.get('peak_homegraph_rss_mb'))} | {_format_mb(h_ag.get('peak_homegraph_rss_mb'))} | — | — |
|
|
1134
|
+
| 合计峰值内存 | {_format_mb(b_ag.get('peak_combined_rss_mb'))} | {_format_mb(h_ag.get('peak_combined_rss_mb'))} | — | — |
|
|
1135
|
+
|
|
1136
|
+
> **说明**: 墙钟耗时 = `run_all.py` 整组实验起止时间;Agent 累计耗时 = 各实验 `deveco run` 进程时间之和;HomeGraph 索引在 homegraph 组开头执行一次,计入墙钟但不计入各实验 `duration_s`。内存为各实验轮询采样得到的 Working Set 峰值。
|
|
1137
|
+
|
|
1138
|
+
---
|
|
1139
|
+
|
|
1140
|
+
{_build_ab_validity_section(b_sum, h_sum, exp_ids, h_ag)}
|
|
1141
|
+
|
|
1142
|
+
---
|
|
1143
|
+
|
|
1144
|
+
## 耗时分解
|
|
1145
|
+
|
|
1146
|
+
| 阶段 | Baseline | HomeGraph | 说明 |
|
|
1147
|
+
|------|----------|-----------|------|
|
|
1148
|
+
| HomeGraph 索引 (`index`) | — | {_format_index_ms(h_ag.get('homegraph_index_ms', 0))} | 仅 homegraph 组开头执行一次 |
|
|
1149
|
+
| Agent 累计耗时 | {b_ag['total_dur_ms'] / 1000:.1f}s | {h_ag['total_dur_ms'] / 1000:.1f}s | 各实验 `duration_ms` 之和 |
|
|
1150
|
+
| 整组墙钟耗时 | {_format_wall_duration(baseline_wall_s) if baseline_wall_s else '—'} | {_format_wall_duration(homegraph_wall_s) if homegraph_wall_s else '—'} | 含 git 重置、setup、index、实验间隔 |
|
|
1151
|
+
| 墙钟 − Agent 累计 | {_overhead_s(baseline_wall_s, b_ag['total_dur_ms'])} | {_overhead_s(homegraph_wall_s, h_ag['total_dur_ms'], h_ag.get('homegraph_index_ms', 0))} | 近似 overhead |
|
|
1152
|
+
|
|
1153
|
+
---
|
|
1154
|
+
|
|
1155
|
+
## 逐实验对比
|
|
1156
|
+
|
|
1157
|
+
| Exp | Baseline 耗时 | HomeGraph 耗时 | Baseline 工具 | HomeGraph 工具 | Baseline HG | HomeGraph HG | Baseline Token | HomeGraph Token |
|
|
1158
|
+
|-----|--------------|----------------|---------------|----------------|-------------|--------------|----------------|-----------------|
|
|
1159
|
+
"""
|
|
1160
|
+
for eid in exp_ids:
|
|
1161
|
+
br, hr = b_sum.get(eid, {}), h_sum.get(eid, {})
|
|
1162
|
+
b_dur = br.get("duration_s", "—")
|
|
1163
|
+
h_dur = hr.get("duration_s", "—")
|
|
1164
|
+
b_tc = br.get("tool_calls", "—")
|
|
1165
|
+
h_tc = hr.get("tool_calls", "—")
|
|
1166
|
+
b_hg = br.get("homegraph_tool_calls", 0)
|
|
1167
|
+
h_hg = hr.get("homegraph_tool_calls", 0)
|
|
1168
|
+
b_tok = br.get("input_tokens", 0)
|
|
1169
|
+
h_tok = hr.get("input_tokens", 0)
|
|
1170
|
+
b_tok_s = f"{b_tok // 1000}k" if isinstance(b_tok, int) else str(b_tok)
|
|
1171
|
+
h_tok_s = f"{h_tok // 1000}k" if isinstance(h_tok, int) else str(h_tok)
|
|
1172
|
+
title = br.get("title", hr.get("title", ""))[:16]
|
|
1173
|
+
md += f"| {eid} {title} | {b_dur} | {h_dur} | {b_tc} | {h_tc} | {b_hg} | {h_hg} | {b_tok_s} | {h_tok_s} |\n"
|
|
1174
|
+
|
|
1175
|
+
# Gear switching comparison if 1-1/1-2/1-3 present
|
|
1176
|
+
b_gs = baseline_data.get("gear_analysis", {})
|
|
1177
|
+
h_gs = homegraph_data.get("gear_analysis", {})
|
|
1178
|
+
if b_gs.get("valid") or h_gs.get("valid"):
|
|
1179
|
+
md += """
|
|
1180
|
+
---
|
|
1181
|
+
|
|
1182
|
+
## 档位切换 (Exp 1-1 → 1-3)
|
|
1183
|
+
|
|
1184
|
+
| 指标 | Baseline | HomeGraph |
|
|
1185
|
+
|------|----------|-----------|
|
|
1186
|
+
"""
|
|
1187
|
+
md += f"| 工具跨度 | {b_gs.get('tool_span', '—')} | {h_gs.get('tool_span', '—')} |\n"
|
|
1188
|
+
md += f"| 耗时跨度 | {b_gs.get('dur_span', '—')} | {h_gs.get('dur_span', '—')} |\n"
|
|
1189
|
+
md += f"| 1-2 trap 生效 | {'是' if b_gs.get('trap_worked') else '否'} | {'是' if h_gs.get('trap_worked') else '否'} |\n"
|
|
1190
|
+
md += f"| 1-3 深度探索 | {'是' if b_gs.get('deep_confirmed') else '否'} | {'是' if h_gs.get('deep_confirmed') else '否'} |\n"
|
|
1191
|
+
|
|
1192
|
+
md += """
|
|
1193
|
+
---
|
|
1194
|
+
|
|
1195
|
+
## 评分对比(7 维度)
|
|
1196
|
+
|
|
1197
|
+
| 维度 | Baseline | HomeGraph |
|
|
1198
|
+
|------|----------|-----------|
|
|
1199
|
+
"""
|
|
1200
|
+
b_scores = baseline_data.get("scores", {})
|
|
1201
|
+
h_scores = homegraph_data.get("scores", {})
|
|
1202
|
+
for dim in sorted(set(b_scores) | set(h_scores)):
|
|
1203
|
+
bs = b_scores.get(dim, 0)
|
|
1204
|
+
hs = h_scores.get(dim, 0)
|
|
1205
|
+
md += f"| {dim} | {_score_to_stars(bs)} ({bs}/5) | {_score_to_stars(hs)} ({hs}/5) |\n"
|
|
1206
|
+
|
|
1207
|
+
md += f"""
|
|
1208
|
+
---
|
|
1209
|
+
|
|
1210
|
+
{_FIXED_EXPERIMENT_EXPECTATIONS}
|
|
1211
|
+
|
|
1212
|
+
*报告由 `analyze.py compare_runs` 自动生成*
|
|
1213
|
+
"""
|
|
1214
|
+
return md
|
|
1215
|
+
|
|
1216
|
+
|
|
1217
|
+
def compare_runs(baseline_dir: Path, homegraph_dir: Path, *,
|
|
1218
|
+
agent: str = "", baseline_wall_s: int = 0,
|
|
1219
|
+
homegraph_wall_s: int = 0, verbose: bool = False) -> Path:
|
|
1220
|
+
"""Write A/B compare report under output/compare/. Returns report path."""
|
|
1221
|
+
baseline_dir = resolve_results_dir(Path(baseline_dir))
|
|
1222
|
+
homegraph_dir = resolve_results_dir(Path(homegraph_dir))
|
|
1223
|
+
baseline_data = gather_data(baseline_dir)
|
|
1224
|
+
homegraph_data = gather_data(homegraph_dir)
|
|
1225
|
+
|
|
1226
|
+
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
1227
|
+
agent_key = agent or "default"
|
|
1228
|
+
compare_dir = OUTPUT_DIR / "compare" / agent_key / ts
|
|
1229
|
+
compare_dir.mkdir(parents=True, exist_ok=True)
|
|
1230
|
+
|
|
1231
|
+
report_path = compare_dir / "ab_compare_report.md"
|
|
1232
|
+
report_path.write_text(
|
|
1233
|
+
build_compare_md(baseline_dir, homegraph_dir, baseline_data, homegraph_data,
|
|
1234
|
+
agent=agent, baseline_wall_s=baseline_wall_s,
|
|
1235
|
+
homegraph_wall_s=homegraph_wall_s),
|
|
1236
|
+
encoding="utf-8",
|
|
1237
|
+
)
|
|
1238
|
+
|
|
1239
|
+
manifest = {
|
|
1240
|
+
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
1241
|
+
"agent": agent_key,
|
|
1242
|
+
"baseline_dir": str(baseline_dir),
|
|
1243
|
+
"homegraph_dir": str(homegraph_dir),
|
|
1244
|
+
"baseline_report": str(baseline_dir / "analysis_report.md"),
|
|
1245
|
+
"homegraph_report": str(homegraph_dir / "analysis_report.md"),
|
|
1246
|
+
"compare_report": str(report_path),
|
|
1247
|
+
"baseline_wall_s": baseline_wall_s,
|
|
1248
|
+
"homegraph_wall_s": homegraph_wall_s,
|
|
1249
|
+
}
|
|
1250
|
+
(compare_dir / "compare_manifest.json").write_text(
|
|
1251
|
+
json.dumps(manifest, indent=2, ensure_ascii=False), encoding="utf-8",
|
|
1252
|
+
)
|
|
1253
|
+
|
|
1254
|
+
if verbose:
|
|
1255
|
+
_configure_stdout()
|
|
1256
|
+
header("A/B 对比报告")
|
|
1257
|
+
print(f" Baseline wall : {_format_wall_duration(baseline_wall_s)}")
|
|
1258
|
+
print(f" HomeGraph wall: {_format_wall_duration(homegraph_wall_s)}")
|
|
1259
|
+
b_ag = baseline_data["aggregated"]
|
|
1260
|
+
h_ag = homegraph_data["aggregated"]
|
|
1261
|
+
print(f" Tools : {b_ag['total_tool_calls']} → {h_ag['total_tool_calls']} "
|
|
1262
|
+
f"({_pct_change(b_ag['total_tool_calls'], h_ag['total_tool_calls'])})")
|
|
1263
|
+
print(f" Input tokens : {b_ag['total_input_tokens']:,} → {h_ag['total_input_tokens']:,} "
|
|
1264
|
+
f"({_pct_change(b_ag['total_input_tokens'], h_ag['total_input_tokens'])})")
|
|
1265
|
+
|
|
1266
|
+
return report_path
|
|
1267
|
+
|
|
1268
|
+
|
|
1269
|
+
def print_report_locations(*, baseline_report: Optional[Path] = None,
|
|
1270
|
+
homegraph_report: Optional[Path] = None,
|
|
1271
|
+
compare_report: Optional[Path] = None,
|
|
1272
|
+
single_report: Optional[Path] = None):
|
|
1273
|
+
"""Print report paths to console after a run completes."""
|
|
1274
|
+
_configure_stdout()
|
|
1275
|
+
header("报告已生成")
|
|
1276
|
+
if single_report:
|
|
1277
|
+
print(f" 单组分析报告 : {single_report}")
|
|
1278
|
+
if baseline_report:
|
|
1279
|
+
print(f" Baseline 报告: {baseline_report}")
|
|
1280
|
+
if homegraph_report:
|
|
1281
|
+
print(f" HomeGraph 报告: {homegraph_report}")
|
|
1282
|
+
if compare_report:
|
|
1283
|
+
print(f" A/B 对比报告 : {compare_report}")
|
|
1284
|
+
print(f"\n{GREEN}[ALL]{NC} 所有报告位于 output/ 目录下,可用 Markdown 阅读器打开。")
|
|
1285
|
+
|
|
1286
|
+
|
|
1287
|
+
# ═══════════════════════════════════════════════════════════
|
|
1288
|
+
# Main
|
|
1289
|
+
# ═══════════════════════════════════════════════════════════
|
|
1290
|
+
|
|
1291
|
+
def main():
|
|
1292
|
+
_configure_stdout()
|
|
1293
|
+
input_path = Path(sys.argv[1]) if len(sys.argv) > 1 else RESULTS_DIR
|
|
1294
|
+
if not input_path.is_dir():
|
|
1295
|
+
print(f"ERROR: Results directory not found: {input_path}")
|
|
1296
|
+
sys.exit(1)
|
|
1297
|
+
|
|
1298
|
+
results_dir = resolve_results_dir(input_path)
|
|
1299
|
+
if not _has_experiment_outputs(results_dir):
|
|
1300
|
+
print(f"ERROR: No experiment outputs found under: {input_path}")
|
|
1301
|
+
if input_path != results_dir:
|
|
1302
|
+
print(f" Also checked: {results_dir}")
|
|
1303
|
+
sys.exit(1)
|
|
1304
|
+
|
|
1305
|
+
try:
|
|
1306
|
+
analyze_run(results_dir, input_path=input_path, verbose=True)
|
|
1307
|
+
except ValueError as e:
|
|
1308
|
+
print(f"ERROR: {e}")
|
|
1309
|
+
sys.exit(1)
|
|
1310
|
+
|
|
1311
|
+
|
|
1312
|
+
if __name__ == "__main__":
|
|
1313
|
+
main()
|