homegraph 1.5.10 → 1.6.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/CHANGELOG.md +27 -0
- package/README.md +64 -14
- package/dist/db/queries.d.ts +4 -0
- package/dist/db/queries.js +15 -0
- package/dist/extraction/grammars.d.ts +19 -0
- package/dist/extraction/grammars.js +44 -1
- package/dist/graph/evidence-paths.d.ts +2 -0
- package/dist/graph/evidence-paths.js +20 -3
- package/dist/mcp/arkts-evidence-packs.d.ts +17 -0
- package/dist/mcp/arkts-evidence-packs.js +219 -10
- package/dist/mcp/daemon.d.ts +21 -3
- package/dist/mcp/daemon.js +60 -6
- package/dist/mcp/engine.d.ts +26 -0
- package/dist/mcp/engine.js +143 -7
- package/dist/mcp/evidence-audit.d.ts +3 -0
- package/dist/mcp/evidence-audit.js +40 -0
- package/dist/mcp/implementation-context.d.ts +35 -0
- package/dist/mcp/implementation-context.js +232 -0
- package/dist/mcp/index-availability.d.ts +22 -0
- package/dist/mcp/index-availability.js +40 -1
- package/dist/mcp/index.js +9 -0
- package/dist/mcp/indexable-root.d.ts +22 -0
- package/dist/mcp/indexable-root.js +140 -0
- package/dist/mcp/liveness-watchdog.d.ts +6 -1
- package/dist/mcp/liveness-watchdog.js +17 -5
- package/dist/mcp/locate-contract.d.ts +50 -0
- package/dist/mcp/locate-contract.js +146 -0
- package/dist/mcp/query-cache.d.ts +1 -15
- package/dist/mcp/query-cache.js +20 -11
- package/dist/mcp/request-evidence.d.ts +69 -0
- package/dist/mcp/request-evidence.js +297 -0
- package/dist/mcp/server-instructions.d.ts +2 -2
- package/dist/mcp/server-instructions.js +12 -9
- package/dist/mcp/session.js +15 -0
- package/dist/mcp/tools.d.ts +67 -12
- package/dist/mcp/tools.js +841 -134
- package/dist/project-map/index.d.ts +45 -0
- package/dist/project-map/index.js +371 -3
- package/dist/resolution/callback-synthesizer.js +251 -0
- package/dist/resolution/frameworks/arkts-entry.d.ts +35 -6
- package/dist/resolution/frameworks/arkts-entry.js +513 -30
- package/dist/resolution/index.js +25 -21
- package/dist/runtime-log.d.ts +52 -0
- package/dist/runtime-log.js +199 -0
- package/dist/search/literal-evidence.js +11 -3
- package/dist/search/query-plan-provider.js +15 -6
- package/dist/search/query-plan.d.ts +3 -1
- package/dist/search/query-plan.js +25 -16
- package/dist/search/query-utils.d.ts +13 -0
- package/dist/search/query-utils.js +64 -0
- package/dist/search/request-contract.d.ts +29 -0
- package/dist/search/request-contract.js +76 -0
- package/package.json +1 -1
package/dist/mcp/tools.js
CHANGED
|
@@ -1,33 +1,40 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* MCP Tool Definitions
|
|
4
|
-
*
|
|
5
|
-
* Defines the tools exposed by the HomeGraph MCP server.
|
|
6
|
-
*/
|
|
7
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
exports.ToolHandler = exports.tools = exports.PathRefusalError = exports.GraphSourcesDisabledError = exports.NotIndexedError = void 0;
|
|
3
|
+
exports.ToolHandler = exports.DEFAULT_MCP_TOOL_SHORT_NAMES = exports.tools = exports.PathRefusalError = exports.GraphSourcesDisabledError = exports.NotIndexedError = void 0;
|
|
9
4
|
exports.__setLoadHomeGraphForTests = __setLoadHomeGraphForTests;
|
|
10
5
|
exports.normalizeQuerySpelling = normalizeQuerySpelling;
|
|
11
6
|
exports.getExploreBudget = getExploreBudget;
|
|
12
7
|
exports.getExploreOutputBudget = getExploreOutputBudget;
|
|
13
8
|
exports.tightenExploreBudgetForQuery = tightenExploreBudgetForQuery;
|
|
14
9
|
exports.exploreLocatorDigestEnabled = exploreLocatorDigestEnabled;
|
|
10
|
+
exports.formatHarmonyRegistrationSources = formatHarmonyRegistrationSources;
|
|
11
|
+
exports.formatHarmonyResourceHits = formatHarmonyResourceHits;
|
|
12
|
+
exports.formatHarmonyCapabilityProfiles = formatHarmonyCapabilityProfiles;
|
|
13
|
+
exports.isHarmonyStubBody = isHarmonyStubBody;
|
|
14
|
+
exports.formatHarmonySeamNotes = formatHarmonySeamNotes;
|
|
15
15
|
exports.formatStaleBanner = formatStaleBanner;
|
|
16
16
|
exports.formatStaleFooter = formatStaleFooter;
|
|
17
17
|
exports.formatDegradedBanner = formatDegradedBanner;
|
|
18
|
+
exports.resolveMcpToolAllowlist = resolveMcpToolAllowlist;
|
|
18
19
|
exports.getStaticTools = getStaticTools;
|
|
19
20
|
exports.extractBareUsageSymbols = extractBareUsageSymbols;
|
|
20
21
|
exports.queryAsBareSymbolInventory = queryAsBareSymbolInventory;
|
|
21
22
|
exports.hasPositiveAnswerNowDirective = hasPositiveAnswerNowDirective;
|
|
22
23
|
exports.reconcilePartialAnswerNow = reconcilePartialAnswerNow;
|
|
24
|
+
const evidence_audit_1 = require("./evidence-audit");
|
|
25
|
+
const implementation_context_1 = require("./implementation-context");
|
|
26
|
+
const request_contract_1 = require("../search/request-contract");
|
|
23
27
|
const query_pool_1 = require("./query-pool");
|
|
24
28
|
const source_slice_identity_1 = require("./source-slice-identity");
|
|
25
29
|
const evidence_rendering_1 = require("./evidence-rendering");
|
|
26
30
|
const arkts_evidence_packs_1 = require("./arkts-evidence-packs");
|
|
27
31
|
const evidence_paths_1 = require("../graph/evidence-paths");
|
|
28
32
|
const query_plan_1 = require("../search/query-plan");
|
|
33
|
+
const grammars_1 = require("../extraction/grammars");
|
|
34
|
+
const project_map_1 = require("../project-map");
|
|
29
35
|
const index_availability_1 = require("./index-availability");
|
|
30
36
|
const directory_1 = require("../directory");
|
|
37
|
+
const runtime_log_1 = require("../runtime-log");
|
|
31
38
|
// Lazy-load the heavy HomeGraph chain off the MCP startup path — see the same
|
|
32
39
|
// helper in engine.ts. ToolHandler must load to answer tools/list (static
|
|
33
40
|
// schemas), but it must NOT drag in sqlite/query layers before the daemon binds;
|
|
@@ -55,6 +62,7 @@ const arkts_1 = require("../extraction/languages/arkts");
|
|
|
55
62
|
const explore_session_state_1 = require("./explore-session-state");
|
|
56
63
|
const explore_dedup_1 = require("./explore-dedup");
|
|
57
64
|
const explore_repeat_guard_1 = require("./explore-repeat-guard");
|
|
65
|
+
const locate_contract_1 = require("./locate-contract");
|
|
58
66
|
const dynamic_boundaries_1 = require("./dynamic-boundaries");
|
|
59
67
|
const query_cache_1 = require("./query-cache");
|
|
60
68
|
const diff_impact_1 = require("./diff-impact");
|
|
@@ -565,6 +573,258 @@ function fileSectionHeader(filePath, suffix) {
|
|
|
565
573
|
? `${FILE_SECTION_PREFIX}${filePath}\`** — ${suffix}`
|
|
566
574
|
: `${FILE_SECTION_PREFIX}${filePath}\`**`;
|
|
567
575
|
}
|
|
576
|
+
/**
|
|
577
|
+
* Spec 0039 — short Registration sources table from indexed Harmony profile routes.
|
|
578
|
+
* Returns null when nothing to show.
|
|
579
|
+
*/
|
|
580
|
+
function formatHarmonyRegistrationSources(cg, maxRows = 24) {
|
|
581
|
+
const routes = cg
|
|
582
|
+
.getNodesByKind('route')
|
|
583
|
+
.filter((n) => (0, grammars_1.isHarmonyRouteProfileJson)(n.filePath))
|
|
584
|
+
.sort((a, b) => a.filePath.localeCompare(b.filePath) || a.startLine - b.startLine);
|
|
585
|
+
if (routes.length === 0)
|
|
586
|
+
return null;
|
|
587
|
+
const byFile = new Map();
|
|
588
|
+
for (const r of routes) {
|
|
589
|
+
const fp = r.filePath.replace(/\\/g, '/');
|
|
590
|
+
const list = byFile.get(fp) ?? [];
|
|
591
|
+
list.push(r);
|
|
592
|
+
byFile.set(fp, list);
|
|
593
|
+
}
|
|
594
|
+
const lines = [
|
|
595
|
+
'**Registration sources** (indexed Harmony route profiles — evidence from these files; do not re-Read unless editing)',
|
|
596
|
+
];
|
|
597
|
+
let rows = 0;
|
|
598
|
+
for (const [fp, list] of byFile) {
|
|
599
|
+
lines.push(`- \`${fp}\``);
|
|
600
|
+
for (const r of list) {
|
|
601
|
+
if (rows >= maxRows) {
|
|
602
|
+
lines.push(` - … +more routes`);
|
|
603
|
+
return lines.join('\n');
|
|
604
|
+
}
|
|
605
|
+
const page = typeof r.signature === 'string'
|
|
606
|
+
? /pageSourceFile=([^;]+)/.exec(r.signature)?.[1]?.trim()
|
|
607
|
+
: undefined;
|
|
608
|
+
const builder = typeof r.signature === 'string'
|
|
609
|
+
? /buildFunction=([^;]+)/.exec(r.signature)?.[1]?.trim()
|
|
610
|
+
: undefined;
|
|
611
|
+
if (page) {
|
|
612
|
+
lines.push(` - \`${r.name}\` → \`${page}\`${builder ? ` (${builder})` : ''} @${fp}:${r.startLine}`);
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
lines.push(` - \`${r.name}\` @${fp}:${r.startLine}`);
|
|
616
|
+
}
|
|
617
|
+
rows++;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return lines.join('\n');
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Spec 0041 / 0048 §3 — short Resource hits table for element/string.json constants.
|
|
624
|
+
* Spec 0048: append up to 2 in-repo `.ets` binding anchors when signature/docstring
|
|
625
|
+
* mention `app.string.<key>`.
|
|
626
|
+
*/
|
|
627
|
+
function formatHarmonyResourceHits(cg, query, maxRows = 8) {
|
|
628
|
+
const constants = cg
|
|
629
|
+
.getNodesByKind('constant')
|
|
630
|
+
.filter((n) => (0, grammars_1.isHarmonyElementStringJson)(n.filePath));
|
|
631
|
+
if (constants.length === 0)
|
|
632
|
+
return null;
|
|
633
|
+
const q = query.trim();
|
|
634
|
+
const namedFile = /string\.json/i.test(q);
|
|
635
|
+
const tokens = [
|
|
636
|
+
...new Set(q
|
|
637
|
+
.split(/[\s,;|]+/)
|
|
638
|
+
.map((t) => t.trim())
|
|
639
|
+
.filter((t) => t.length >= 2 && t.length <= 160 && !/^string\.json$/i.test(t))),
|
|
640
|
+
].slice(0, 12);
|
|
641
|
+
const matched = constants.filter((n) => {
|
|
642
|
+
if (namedFile && tokens.length === 0)
|
|
643
|
+
return true;
|
|
644
|
+
const hay = `${n.name}\n${n.qualifiedName ?? ''}\n${n.docstring ?? ''}\n${n.signature ?? ''}`.toLocaleLowerCase();
|
|
645
|
+
return tokens.some((t) => hay.includes(t.toLocaleLowerCase()));
|
|
646
|
+
});
|
|
647
|
+
const rows = (matched.length > 0 ? matched : namedFile ? constants : [])
|
|
648
|
+
.sort((a, b) => a.filePath.localeCompare(b.filePath) || a.startLine - b.startLine)
|
|
649
|
+
.slice(0, maxRows);
|
|
650
|
+
if (rows.length === 0)
|
|
651
|
+
return null;
|
|
652
|
+
const projectRoot = cg.getProjectRoot();
|
|
653
|
+
const etsFiles = cg.getFiles()
|
|
654
|
+
.map((f) => f.path.replace(/\\/g, '/'))
|
|
655
|
+
.filter((p) => /\.ets$/i.test(p))
|
|
656
|
+
.slice(0, 80);
|
|
657
|
+
const findBindings = (key, stringFile) => {
|
|
658
|
+
const needle = `app.string.${key}`;
|
|
659
|
+
const hits = [];
|
|
660
|
+
const modRoot = (() => {
|
|
661
|
+
const marker = '/src/main/';
|
|
662
|
+
const i = stringFile.indexOf(marker);
|
|
663
|
+
return i >= 0 ? stringFile.slice(0, i) : '';
|
|
664
|
+
})();
|
|
665
|
+
for (const rel of etsFiles) {
|
|
666
|
+
if (hits.length >= 2)
|
|
667
|
+
break;
|
|
668
|
+
if (modRoot && !(rel === modRoot || rel.startsWith(`${modRoot}/`)))
|
|
669
|
+
continue;
|
|
670
|
+
const abs = (0, path_1.join)(projectRoot, rel);
|
|
671
|
+
if (!(0, fs_1.existsSync)(abs))
|
|
672
|
+
continue;
|
|
673
|
+
let text;
|
|
674
|
+
try {
|
|
675
|
+
text = (0, fs_1.readFileSync)(abs, 'utf-8');
|
|
676
|
+
}
|
|
677
|
+
catch {
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
const lines = text.split(/\n/);
|
|
681
|
+
for (let i = 0; i < lines.length; i++) {
|
|
682
|
+
if (hits.length >= 2)
|
|
683
|
+
break;
|
|
684
|
+
if (lines[i].includes(needle)) {
|
|
685
|
+
hits.push(`\`${rel}:${i + 1}\``);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
return hits;
|
|
690
|
+
};
|
|
691
|
+
const lines = [
|
|
692
|
+
'**Resource hits** (element/string.json — searchable literals, no graph edges)',
|
|
693
|
+
];
|
|
694
|
+
for (const n of rows) {
|
|
695
|
+
const fp = n.filePath.replace(/\\/g, '/');
|
|
696
|
+
const value = (n.docstring ?? '').slice(0, 80);
|
|
697
|
+
const shown = value ? JSON.stringify(value) : '(empty)';
|
|
698
|
+
const bounds = findBindings(n.name, fp);
|
|
699
|
+
const boundNote = bounds.length ? ` · bound ${bounds.join(', ')}` : '';
|
|
700
|
+
lines.push(`- \`${fp}:${n.startLine}\` — ${shown} → \`${n.name}\`${boundNote} · Grep \`$r('app.string.${n.name}')\` in \`.ets\``);
|
|
701
|
+
}
|
|
702
|
+
if ((matched.length || constants.length) > rows.length) {
|
|
703
|
+
lines.push(`- … +more string resources`);
|
|
704
|
+
}
|
|
705
|
+
return lines.join('\n');
|
|
706
|
+
}
|
|
707
|
+
const FORM_QUERY_RE = /form_config|FormExtension|服务卡片|卡片/i;
|
|
708
|
+
const SHORTCUT_QUERY_RE = /shortcuts_config|\bshortcuts\b|快捷方式|长按|快捷入口/i;
|
|
709
|
+
/**
|
|
710
|
+
* Spec 0048 §1–2 — Capability profiles table, or form negative evidence.
|
|
711
|
+
*/
|
|
712
|
+
function formatHarmonyCapabilityProfiles(cg, query, maxRows = 8) {
|
|
713
|
+
const wantForm = FORM_QUERY_RE.test(query);
|
|
714
|
+
const wantShortcut = SHORTCUT_QUERY_RE.test(query);
|
|
715
|
+
if (!wantForm && !wantShortcut)
|
|
716
|
+
return null;
|
|
717
|
+
const constants = cg.getNodesByKind('constant');
|
|
718
|
+
const routes = cg.getNodesByKind('route');
|
|
719
|
+
const formNodes = [
|
|
720
|
+
...constants.filter((n) => (0, grammars_1.isHarmonyCapabilityProfileJson)(n.filePath)
|
|
721
|
+
&& /form_config\.json$/i.test(n.filePath.replace(/\\/g, '/'))),
|
|
722
|
+
...constants.filter((n) => (n.qualifiedName ?? '').includes('harmony.capability.form')),
|
|
723
|
+
...routes.filter((n) => (n.name ?? '').startsWith('formAbility:')),
|
|
724
|
+
];
|
|
725
|
+
const shortcutNodes = [
|
|
726
|
+
...constants.filter((n) => (0, grammars_1.isHarmonyCapabilityProfileJson)(n.filePath)
|
|
727
|
+
&& /shortcuts_config\.json$/i.test(n.filePath.replace(/\\/g, '/'))),
|
|
728
|
+
...constants.filter((n) => (n.qualifiedName ?? '').includes('harmony.capability.shortcut')
|
|
729
|
+
|| (n.qualifiedName ?? '').includes('harmony.shortcut.')),
|
|
730
|
+
];
|
|
731
|
+
const lines = [];
|
|
732
|
+
if (wantForm && formNodes.length === 0) {
|
|
733
|
+
lines.push('**Capability profiles:** No in-repo form_config / FormExtensionAbility (do not treat SDK `.d.ts` as project wiring).');
|
|
734
|
+
}
|
|
735
|
+
const rows = [];
|
|
736
|
+
const pushRows = (kind, nodes) => {
|
|
737
|
+
for (const n of nodes.sort((a, b) => a.filePath.localeCompare(b.filePath) || a.startLine - b.startLine)) {
|
|
738
|
+
if (rows.length >= maxRows)
|
|
739
|
+
break;
|
|
740
|
+
const fp = n.filePath.replace(/\\/g, '/');
|
|
741
|
+
rows.push(`- (${kind}) \`${n.name}\` @\`${fp}:${n.startLine}\`${n.signature ? ` — ${n.signature}` : ''}`);
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
if (wantForm)
|
|
745
|
+
pushRows('form', formNodes);
|
|
746
|
+
if (wantShortcut)
|
|
747
|
+
pushRows('shortcut', shortcutNodes);
|
|
748
|
+
if (rows.length > 0) {
|
|
749
|
+
lines.push('**Capability profiles** (form_config / shortcuts_config — paths only, no UI edges)');
|
|
750
|
+
lines.push(...rows);
|
|
751
|
+
}
|
|
752
|
+
return lines.length ? lines.join('\n') : null;
|
|
753
|
+
}
|
|
754
|
+
/** Spec 0048 §5 — detect empty / log-only method bodies. */
|
|
755
|
+
function isHarmonyStubBody(source) {
|
|
756
|
+
const body = source
|
|
757
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
758
|
+
.replace(/\/\/[^\n]*/g, '')
|
|
759
|
+
.trim();
|
|
760
|
+
if (!body)
|
|
761
|
+
return true;
|
|
762
|
+
const lines = body.split(/\n/).map((l) => l.trim()).filter(Boolean);
|
|
763
|
+
if (lines.length === 0)
|
|
764
|
+
return true;
|
|
765
|
+
if (lines.length > 8)
|
|
766
|
+
return false;
|
|
767
|
+
const code = lines.join('\n');
|
|
768
|
+
// Strip common wrapper braces from a method slice.
|
|
769
|
+
const inner = code.replace(/^[^{]*\{/, '').replace(/\}[^}]*$/, '').trim();
|
|
770
|
+
const stmts = inner.split(/;|\n/).map((s) => s.trim()).filter(Boolean);
|
|
771
|
+
if (stmts.length === 0)
|
|
772
|
+
return true;
|
|
773
|
+
return stmts.every((s) => /^(return(\s+[^;]*)?|hilog\.[a-zA-Z]+\s*\(|console\.(log|info|warn|error|debug)\s*\(|\/\/)/i.test(s)
|
|
774
|
+
|| s === '{'
|
|
775
|
+
|| s === '}');
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Spec 0048 §5 — seam notes for located explore anchors.
|
|
779
|
+
*/
|
|
780
|
+
function formatHarmonySeamNotes(cg, projectRoot, located, maxAnchors = 6) {
|
|
781
|
+
if (!located.length)
|
|
782
|
+
return null;
|
|
783
|
+
const lines = [];
|
|
784
|
+
const capabilityFiles = new Set([
|
|
785
|
+
...cg.getNodesByKind('constant').filter((n) => (0, grammars_1.isHarmonyCapabilityProfileJson)(n.filePath)),
|
|
786
|
+
...cg.getNodesByKind('route').filter((n) => (0, grammars_1.isHarmonyRouteProfileJson)(n.filePath)),
|
|
787
|
+
].map((n) => n.filePath.replace(/\\/g, '/')));
|
|
788
|
+
for (const loc of located.slice(0, maxAnchors)) {
|
|
789
|
+
const fp = loc.filePath.replace(/\\/g, '/');
|
|
790
|
+
const modRoot = (() => {
|
|
791
|
+
const marker = '/src/main/';
|
|
792
|
+
const i = fp.indexOf(marker);
|
|
793
|
+
return i >= 0 ? fp.slice(0, i) : fp.includes('/') ? fp.slice(0, fp.lastIndexOf('/')) : '';
|
|
794
|
+
})();
|
|
795
|
+
let profileNote = 0;
|
|
796
|
+
for (const cap of capabilityFiles) {
|
|
797
|
+
if (profileNote >= 2)
|
|
798
|
+
break;
|
|
799
|
+
if (modRoot && (cap === modRoot || cap.startsWith(`${modRoot}/`))) {
|
|
800
|
+
lines.push(`- profile near \`${loc.name}\`: \`${cap}\``);
|
|
801
|
+
profileNote++;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
try {
|
|
805
|
+
const node = loc.id ? cg.getNode(loc.id) : null;
|
|
806
|
+
const start = node?.startLine ?? loc.startLine;
|
|
807
|
+
const end = node?.endLine ?? start + 12;
|
|
808
|
+
const abs = (0, path_1.join)(projectRoot, fp);
|
|
809
|
+
if ((0, fs_1.existsSync)(abs)) {
|
|
810
|
+
const content = (0, fs_1.readFileSync)(abs, 'utf-8');
|
|
811
|
+
const allLines = content.split(/\n/);
|
|
812
|
+
const slice = allLines.slice(Math.max(0, start - 1), Math.min(allLines.length, end)).join('\n');
|
|
813
|
+
if (isHarmonyStubBody(slice)) {
|
|
814
|
+
lines.push(`- stub: \`${loc.name}\` @\`${fp}:${start}\``);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
catch {
|
|
819
|
+
/* omit */
|
|
820
|
+
}
|
|
821
|
+
if (lines.length >= maxAnchors * 3)
|
|
822
|
+
break;
|
|
823
|
+
}
|
|
824
|
+
if (lines.length === 0)
|
|
825
|
+
return null;
|
|
826
|
+
return ['**Seam notes** (config paths / empty stubs — digests still count as Read)', ...lines.slice(0, 18)].join('\n');
|
|
827
|
+
}
|
|
568
828
|
/**
|
|
569
829
|
* Per-file staleness banner emitted at the top of a tool response when the
|
|
570
830
|
* file watcher has pending events for files referenced by the response.
|
|
@@ -648,10 +908,11 @@ const READ_ONLY_ANNOTATIONS = {
|
|
|
648
908
|
openWorldHint: false,
|
|
649
909
|
};
|
|
650
910
|
/**
|
|
651
|
-
* All HomeGraph MCP
|
|
911
|
+
* All HomeGraph MCP tool definitions (handlers stay registered).
|
|
652
912
|
*
|
|
653
|
-
*
|
|
654
|
-
* explore
|
|
913
|
+
* Default tools/list is the slim pair in `DEFAULT_MCP_TOOL_SHORT_NAMES`
|
|
914
|
+
* (explore / project). Opt into the full catalog with
|
|
915
|
+
* `HOMEGRAPH_MCP_TOOLS=all`. Skip HomeGraph entirely for topic file-lists,
|
|
655
916
|
* concept compares, SDK catalogs, and literal greps.
|
|
656
917
|
*
|
|
657
918
|
* All tools support cross-project queries via the optional `projectPath` parameter.
|
|
@@ -921,11 +1182,11 @@ exports.tools = [
|
|
|
921
1182
|
{
|
|
922
1183
|
name: 'homegraph_explore',
|
|
923
1184
|
description: 'Optional graph evidence for a concrete unresolved cross-symbol mechanism in THIS repo. Required: `query`. ' +
|
|
1185
|
+
'For module/route-profile **paths** and engineering overview use homegraph_project first (not this tool). ' +
|
|
924
1186
|
'Use ordinary bash/search/read for paths, symbols, literal strings and local changes; continue editing when that evidence suffices. ' +
|
|
925
1187
|
'Do not call for routine pre-edit orientation or merely because implementation is difficult. ' +
|
|
926
|
-
'
|
|
927
|
-
'
|
|
928
|
-
'Returns call paths and compact line-numbered source. ArkTS symbol evidence uses complete declarations and bounded directed paths with intermediate source dependencies; explicit Gaps and stop reasons name omitted or unverified evidence. Qualify ambiguous symbols by owning type or file. State the missing relation with known anchors, requested action, scope and constraints; taskContext can carry the full task. ' +
|
|
1188
|
+
'Located ArkTS code may include render scope, imported types, module configuration, indexed SDK signatures and control-state gaps. ' +
|
|
1189
|
+
'Returns call paths and compact line-numbered source (Harmony route_map queries may lead with Registration sources; form/shortcuts queries may lead with Capability profiles; element/string.json literals may lead with Resource hits + optional bound .ets anchors; Seam notes may flag stubs). ArkTS symbol evidence uses complete declarations and bounded directed paths with intermediate source dependencies; explicit Gaps and stop reasons name omitted or unverified evidence. Qualify ambiguous symbols by owning type or file. State the missing relation with known anchors, requested action, scope and constraints; taskContext can carry the full task. ' +
|
|
929
1190
|
'Reuse unchanged complete ranges; refresh missing, edited or truncated evidence. ' +
|
|
930
1191
|
'No new evidence → change to a targeted source inspection, not a paraphrased explore. ' +
|
|
931
1192
|
'Partial/busy → at most one focused recovery for the named gap; budgets are ceilings, not required calls. ' +
|
|
@@ -937,7 +1198,7 @@ exports.tools = [
|
|
|
937
1198
|
type: 'string',
|
|
938
1199
|
description: 'Required. For pre-edit orientation or how/mechanism: pass the user task or domain keywords (page/module/feature words). ' +
|
|
939
1200
|
'For named flows, include Type / Type.member / component names. For @kit mechanism/flow, include module/export tokens; ' +
|
|
940
|
-
'
|
|
1201
|
+
'not SDK feature catalogs.',
|
|
941
1202
|
},
|
|
942
1203
|
taskContext: {
|
|
943
1204
|
type: 'string',
|
|
@@ -967,9 +1228,12 @@ exports.tools = [
|
|
|
967
1228
|
},
|
|
968
1229
|
{
|
|
969
1230
|
name: 'homegraph_project',
|
|
970
|
-
description: 'Shallow
|
|
971
|
-
'
|
|
972
|
-
'
|
|
1231
|
+
description: 'Shallow engineering map: modules + per-module files. On Harmony repos also prints skeleton pointers ' +
|
|
1232
|
+
'(bundleName from app.json5, modules from build-profile.json5, per-module route_map/router_map/main_pages paths, oh-package name) ' +
|
|
1233
|
+
'and a Module roster (local file: deps) plus bounded HarmonyOS resources path inventory (string.json / capability profiles form_config|shortcuts_config / rawfile / media dirs / on-disk modules not in the graph). ' +
|
|
1234
|
+
'GIVES navigation only — NOT symbol bodies, call graphs, or JSON/file contents (use homegraph_explore for route→page / Resource hits / Capability profiles; Read to edit). ' +
|
|
1235
|
+
'PRIMARY overview while the full index is still building; also useful after full index. ' +
|
|
1236
|
+
'Optional `module` filters by name/path; `includeFiles` defaults true (resources section still prints when false).',
|
|
973
1237
|
inputSchema: {
|
|
974
1238
|
type: 'object',
|
|
975
1239
|
properties: {
|
|
@@ -1156,6 +1420,28 @@ function withRequiredProjectPath(defs) {
|
|
|
1156
1420
|
};
|
|
1157
1421
|
});
|
|
1158
1422
|
}
|
|
1423
|
+
/**
|
|
1424
|
+
* Default MCP tools/list surface (product slim). Handlers for other tools remain
|
|
1425
|
+
* in-tree; restore the full catalog with `HOMEGRAPH_MCP_TOOLS=all` (or `*`), or
|
|
1426
|
+
* name a comma list (e.g. `explore,node,search,arkui_migrate`).
|
|
1427
|
+
*/
|
|
1428
|
+
exports.DEFAULT_MCP_TOOL_SHORT_NAMES = ['explore', 'project'];
|
|
1429
|
+
/** Resolve the exposed-tool allowlist from env (default = product slim pair). */
|
|
1430
|
+
function resolveMcpToolAllowlist(raw = process.env.HOMEGRAPH_MCP_TOOLS) {
|
|
1431
|
+
if (!raw || !raw.trim()) {
|
|
1432
|
+
return new Set(exports.DEFAULT_MCP_TOOL_SHORT_NAMES);
|
|
1433
|
+
}
|
|
1434
|
+
const trimmed = raw.trim();
|
|
1435
|
+
if (trimmed === '*' || /^all$/i.test(trimmed))
|
|
1436
|
+
return 'all';
|
|
1437
|
+
const set = new Set(trimmed.split(',').map((s) => s.trim().replace(/^homegraph_/, '')).filter(Boolean));
|
|
1438
|
+
return set.size ? set : new Set(exports.DEFAULT_MCP_TOOL_SHORT_NAMES);
|
|
1439
|
+
}
|
|
1440
|
+
function filterToolsByAllowlist(defs, allow) {
|
|
1441
|
+
if (allow === 'all')
|
|
1442
|
+
return defs;
|
|
1443
|
+
return defs.filter((t) => allow.has(t.name.replace(/^homegraph_/, '')));
|
|
1444
|
+
}
|
|
1159
1445
|
/**
|
|
1160
1446
|
* Allowlist-filtered tool definitions WITHOUT an engine — the static surface the
|
|
1161
1447
|
* proxy answers `tools/list` with before any project is open. Mirrors
|
|
@@ -1163,12 +1449,7 @@ function withRequiredProjectPath(defs) {
|
|
|
1163
1449
|
* note in a description only adds once `cg` is loaded; the schemas are static).
|
|
1164
1450
|
*/
|
|
1165
1451
|
function getStaticTools() {
|
|
1166
|
-
|
|
1167
|
-
if (!raw || !raw.trim()) {
|
|
1168
|
-
return exports.tools;
|
|
1169
|
-
}
|
|
1170
|
-
const allow = new Set(raw.split(',').map(s => s.trim().replace(/^homegraph_/, '').replace(/^homegraph_/, '')).filter(Boolean));
|
|
1171
|
-
return allow.size ? exports.tools.filter(t => allow.has(t.name.replace(/^homegraph_/, ''))) : exports.tools;
|
|
1452
|
+
return filterToolsByAllowlist(exports.tools, resolveMcpToolAllowlist());
|
|
1172
1453
|
}
|
|
1173
1454
|
/** Prose that reads like an identifier but never names a symbol worth scanning. */
|
|
1174
1455
|
const BARE_USAGE_STOPWORDS = new Set([
|
|
@@ -1326,6 +1607,10 @@ class ToolHandler {
|
|
|
1326
1607
|
// The directory the server last searched for a default project. Surfaced in
|
|
1327
1608
|
// the "not initialized" error so users can see why detection missed.
|
|
1328
1609
|
defaultProjectHint = null;
|
|
1610
|
+
// Spec 0043: when a tool `projectPath` would open a different index root than
|
|
1611
|
+
// the session's default bound project, we soft-pin to the default and stash
|
|
1612
|
+
// an English notice to prepend on the success reply (consumed once).
|
|
1613
|
+
boundProjectPathPinNotice = null;
|
|
1329
1614
|
// Per-start-path cache of the git worktree/index mismatch (issue #155). The
|
|
1330
1615
|
// mismatch is a fixed property of (where the request came from → which
|
|
1331
1616
|
// .homegraph/ it resolves to), so the up-to-two `git rev-parse` spawns run
|
|
@@ -1428,26 +1713,20 @@ class ToolHandler {
|
|
|
1428
1713
|
return this.cg !== null;
|
|
1429
1714
|
}
|
|
1430
1715
|
/**
|
|
1431
|
-
* Optional allowlist of exposed tools, parsed from
|
|
1432
|
-
*
|
|
1433
|
-
*
|
|
1434
|
-
* exposed. Lets an operator (or an A/B harness) trim the tool surface
|
|
1435
|
-
* without rebuilding the client config; the ablated tool is then truly
|
|
1436
|
-
* absent from ListTools rather than merely denied on call.
|
|
1716
|
+
* Optional allowlist of exposed tools, parsed from HOMEGRAPH_MCP_TOOLS.
|
|
1717
|
+
* Unset/empty → product slim default (`explore`, `project`).
|
|
1718
|
+
* `all` / `*` → full catalog. Comma list → only those short names.
|
|
1437
1719
|
* Matching is on the short form, so "node" and "homegraph_node" both work.
|
|
1438
1720
|
*/
|
|
1439
1721
|
toolAllowlist() {
|
|
1440
|
-
|
|
1441
|
-
if (!raw || !raw.trim())
|
|
1442
|
-
return null;
|
|
1443
|
-
const short = (s) => s.trim().replace(/^homegraph_/, '');
|
|
1444
|
-
const set = new Set(raw.split(',').map(short).filter(Boolean));
|
|
1445
|
-
return set.size ? set : null;
|
|
1722
|
+
return resolveMcpToolAllowlist();
|
|
1446
1723
|
}
|
|
1447
|
-
/** Whether a tool name passes the HOMEGRAPH_MCP_TOOLS allowlist
|
|
1724
|
+
/** Whether a tool name passes the HOMEGRAPH_MCP_TOOLS allowlist. */
|
|
1448
1725
|
isToolAllowed(name) {
|
|
1449
1726
|
const allow = this.toolAllowlist();
|
|
1450
|
-
|
|
1727
|
+
if (allow === 'all')
|
|
1728
|
+
return true;
|
|
1729
|
+
return allow.has(name.replace(/^homegraph_/, ''));
|
|
1451
1730
|
}
|
|
1452
1731
|
/**
|
|
1453
1732
|
* Get tool definitions with dynamic descriptions based on project size.
|
|
@@ -1457,11 +1736,7 @@ class ToolHandler {
|
|
|
1457
1736
|
*/
|
|
1458
1737
|
getTools() {
|
|
1459
1738
|
const allow = this.toolAllowlist();
|
|
1460
|
-
|
|
1461
|
-
// the surface to only the listed short names.
|
|
1462
|
-
let visible = allow
|
|
1463
|
-
? exports.tools.filter(t => allow.has(t.name.replace(/^homegraph_/, '')))
|
|
1464
|
-
: exports.tools;
|
|
1739
|
+
let visible = filterToolsByAllowlist(exports.tools, allow);
|
|
1465
1740
|
// No default project loaded → no-root-index case (#993): a gateway server
|
|
1466
1741
|
// started outside any repo, or a monorepo root whose indexes live in
|
|
1467
1742
|
// sub-projects. With nothing to fall back to, EVERY call needs an explicit
|
|
@@ -1476,29 +1751,13 @@ class ToolHandler {
|
|
|
1476
1751
|
return withRequiredProjectPath(visible);
|
|
1477
1752
|
try {
|
|
1478
1753
|
const stats = this.cg.getStats();
|
|
1479
|
-
// Tiny-repo tool gating
|
|
1480
|
-
//
|
|
1481
|
-
//
|
|
1482
|
-
// to one grep. (Historical note: the audit below ran when context and
|
|
1483
|
-
// trace still existed; its "5 core tools" are today's trio.)
|
|
1484
|
-
//
|
|
1485
|
-
// n=2 audits ruled out cutting below 5 tools:
|
|
1486
|
-
// - 3-tool gate (search + context + trace): cost regressed on
|
|
1487
|
-
// cobra/ky/sinatra. The agent fell back to raw Reads to cover
|
|
1488
|
-
// what homegraph_node + homegraph_explore would have answered.
|
|
1489
|
-
// - 1-tool gate (search only): catastrophic regression — express
|
|
1490
|
-
// went from -43% WIN to +107% LOSS. With only search, the agent
|
|
1491
|
-
// can't navigate the call graph structurally and reads everything.
|
|
1754
|
+
// Tiny-repo tool gating applies only when the host asked for the full
|
|
1755
|
+
// catalog (`HOMEGRAPH_MCP_TOOLS=all`). The product default is already the
|
|
1756
|
+
// slim pair; an explicit comma allowlist is left untouched.
|
|
1492
1757
|
//
|
|
1493
|
-
//
|
|
1494
|
-
//
|
|
1495
|
-
// on
|
|
1496
|
-
// ITER4: raise threshold 150 → 500 so single-file frameworks
|
|
1497
|
-
// (sinatra at 159, slim_framework around 200) also get the
|
|
1498
|
-
// 5-tool surface. The empirical 5-tool floor was set on <150
|
|
1499
|
-
// probes; iter3 measurement showed sinatra is structurally the
|
|
1500
|
-
// SAME problem as cobra (single-file WITHOUT-arm Read wins),
|
|
1501
|
-
// so it deserves the same gating.
|
|
1758
|
+
// Historical note: n=2 audits ruled out cutting below ~5 tools on the
|
|
1759
|
+
// *full* catalog for tiny OSS repos (search+node+explore+…). That trim
|
|
1760
|
+
// still matters when someone opts into `all` on a <500-file tree.
|
|
1502
1761
|
const TINY_REPO_FILE_THRESHOLD = 500;
|
|
1503
1762
|
const TINY_REPO_CORE_TOOLS = new Set([
|
|
1504
1763
|
'homegraph_explore',
|
|
@@ -1507,9 +1766,9 @@ class ToolHandler {
|
|
|
1507
1766
|
'homegraph_diff_impact',
|
|
1508
1767
|
'homegraph_project',
|
|
1509
1768
|
]);
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
if (
|
|
1769
|
+
const envRaw = (process.env.HOMEGRAPH_MCP_TOOLS ?? '').trim();
|
|
1770
|
+
const askedForFullCatalog = allow === 'all' && (/^all$/i.test(envRaw) || envRaw === '*');
|
|
1771
|
+
if (askedForFullCatalog && stats.fileCount < TINY_REPO_FILE_THRESHOLD) {
|
|
1513
1772
|
visible = visible.filter(t => TINY_REPO_CORE_TOOLS.has(t.name));
|
|
1514
1773
|
}
|
|
1515
1774
|
return visible.map(tool => {
|
|
@@ -1580,6 +1839,11 @@ class ToolHandler {
|
|
|
1580
1839
|
*
|
|
1581
1840
|
* Walks up parent directories to find the nearest .homegraph/ folder,
|
|
1582
1841
|
* similar to how git finds .git/ directories.
|
|
1842
|
+
*
|
|
1843
|
+
* Spec 0043: when a default project is already bound, a `projectPath` whose
|
|
1844
|
+
* resolved index root differs from that bound root is soft-pinned back to
|
|
1845
|
+
* the default (no DB switch) and a success-shaped English notice is stashed
|
|
1846
|
+
* for the reply preamble.
|
|
1583
1847
|
*/
|
|
1584
1848
|
getHomeGraph(projectPath) {
|
|
1585
1849
|
const sourcesMode = (0, graph_sources_1.resolveGraphSources)();
|
|
@@ -1626,25 +1890,34 @@ class ToolHandler {
|
|
|
1626
1890
|
// (#926). The DB connection itself is still cached (by resolved root,
|
|
1627
1891
|
// below), so re-resolving costs only the stat walk, never a reopen.
|
|
1628
1892
|
const resolvedRoot = (0, directory_1.findNearestHomeGraphRoot)(projectPath);
|
|
1893
|
+
// Spec 0043: with a bound default root, never switch the graph to a
|
|
1894
|
+
// different index (sibling / parent / wrong absolute path). Soft-pin and
|
|
1895
|
+
// stash a notice; security refusals above still win.
|
|
1896
|
+
if (this.cg) {
|
|
1897
|
+
const boundRoot = (0, path_1.resolve)(this.cg.getProjectRoot());
|
|
1898
|
+
const resolvedAbs = resolvedRoot ? (0, path_1.resolve)(resolvedRoot) : null;
|
|
1899
|
+
if (resolvedAbs === null || resolvedAbs !== boundRoot) {
|
|
1900
|
+
this.boundProjectPathPinNotice = (0, index_availability_1.formatBoundProjectPathPinNotice)({
|
|
1901
|
+
boundRoot,
|
|
1902
|
+
requestedPath: projectPath,
|
|
1903
|
+
resolvedRoot: resolvedAbs,
|
|
1904
|
+
});
|
|
1905
|
+
return this.freshen(this.cg);
|
|
1906
|
+
}
|
|
1907
|
+
// Same index root (incl. nested path under the bound project) — reuse the
|
|
1908
|
+
// default instance so we never open a second connection to the same DB
|
|
1909
|
+
// (#238).
|
|
1910
|
+
return this.freshen(this.cg);
|
|
1911
|
+
}
|
|
1629
1912
|
if (!resolvedRoot) {
|
|
1630
1913
|
throw new NotIndexedError(`The project at ${projectPath} isn't indexed with homegraph (no .homegraph/ directory found ` +
|
|
1631
1914
|
'walking up from it), so homegraph cannot query it. Use your built-in tools (Read/Grep/Glob) ' +
|
|
1632
1915
|
"for that codebase instead, and don't call homegraph for it again this session. " +
|
|
1633
1916
|
"Indexing is the user's decision — they can run 'homegraph init' in that project to enable it.");
|
|
1634
1917
|
}
|
|
1635
|
-
//
|
|
1636
|
-
//
|
|
1637
|
-
//
|
|
1638
|
-
// writes; when WAL isn't in effect (e.g. a filesystem without shared-memory
|
|
1639
|
-
// support) that surfaces as intermittent
|
|
1640
|
-
// "database is locked" on concurrent tool calls. See issue #238. The
|
|
1641
|
-
// default instance is owned/closed by the server, so it's never cached.
|
|
1642
|
-
if (this.cg && this.cg.getProjectRoot() === resolvedRoot) {
|
|
1643
|
-
return this.freshen(this.cg);
|
|
1644
|
-
}
|
|
1645
|
-
// Cache the open DB connection by RESOLVED ROOT only — never by the input
|
|
1646
|
-
// path. One key per instance means closeAll() closes each exactly once, and
|
|
1647
|
-
// a changed resolution maps to a different entry instead of a stale hit.
|
|
1918
|
+
// No default project — cross-project open (cached by resolved root).
|
|
1919
|
+
// One key per instance means closeAll() closes each exactly once, and a
|
|
1920
|
+
// changed resolution maps to a different entry instead of a stale hit.
|
|
1648
1921
|
const cached = this.projectCache.get(resolvedRoot);
|
|
1649
1922
|
if (cached)
|
|
1650
1923
|
return this.freshen(cached);
|
|
@@ -1889,6 +2162,15 @@ class ToolHandler {
|
|
|
1889
2162
|
catch {
|
|
1890
2163
|
reason = null;
|
|
1891
2164
|
}
|
|
2165
|
+
try {
|
|
2166
|
+
(0, runtime_log_1.logLifecycle)('watcher.degraded', {
|
|
2167
|
+
projectRoot: cg.getProjectRoot(),
|
|
2168
|
+
reason: reason ?? undefined,
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
2171
|
+
catch {
|
|
2172
|
+
/* ignore */
|
|
2173
|
+
}
|
|
1892
2174
|
const composed = `${formatDegradedBanner(reason)}\n\n${head.text}`;
|
|
1893
2175
|
return { ...result, content: [{ type: 'text', text: composed }, ...tail] };
|
|
1894
2176
|
}
|
|
@@ -1898,8 +2180,9 @@ class ToolHandler {
|
|
|
1898
2180
|
return result;
|
|
1899
2181
|
}
|
|
1900
2182
|
/**
|
|
1901
|
-
*
|
|
1902
|
-
*
|
|
2183
|
+
* Decorate successful tool text (Spec 0035 status footer + Spec 0038 project-root hint
|
|
2184
|
+
* + Spec 0043 bound projectPath pin notice).
|
|
2185
|
+
* Prepends absolute project root + join guidance when known; appends status when missing.
|
|
1903
2186
|
*/
|
|
1904
2187
|
withProductStatusFooter(result, projectPath) {
|
|
1905
2188
|
if (result.isError)
|
|
@@ -1907,8 +2190,6 @@ class ToolHandler {
|
|
|
1907
2190
|
const [first, ...rest] = result.content;
|
|
1908
2191
|
if (!first || first.type !== 'text')
|
|
1909
2192
|
return result;
|
|
1910
|
-
if ((0, index_availability_1.textAlreadyHasProductStatus)(first.text))
|
|
1911
|
-
return result;
|
|
1912
2193
|
let cg;
|
|
1913
2194
|
try {
|
|
1914
2195
|
cg = this.getHomeGraph(projectPath);
|
|
@@ -1926,26 +2207,52 @@ class ToolHandler {
|
|
|
1926
2207
|
/* leave cg */
|
|
1927
2208
|
}
|
|
1928
2209
|
}
|
|
1929
|
-
let
|
|
1930
|
-
|
|
1931
|
-
|
|
2210
|
+
let text = first.text;
|
|
2211
|
+
// Spec 0038: absolute project root + how to join repo-relative paths (idempotent).
|
|
2212
|
+
if (!(0, index_availability_1.textAlreadyHasProjectRootHint)(text)) {
|
|
2213
|
+
try {
|
|
2214
|
+
const absRoot = (0, path_1.resolve)(cg.getProjectRoot());
|
|
2215
|
+
if (absRoot) {
|
|
2216
|
+
text = `${(0, index_availability_1.formatProjectRootPathHint)(absRoot)}\n\n${text}`;
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
catch {
|
|
2220
|
+
/* no root — skip hint */
|
|
2221
|
+
}
|
|
1932
2222
|
}
|
|
1933
|
-
|
|
1934
|
-
|
|
2223
|
+
// Spec 0043: bound-root soft-pin notice at the very top (consume once).
|
|
2224
|
+
const pinNotice = this.boundProjectPathPinNotice;
|
|
2225
|
+
this.boundProjectPathPinNotice = null;
|
|
2226
|
+
if (pinNotice && !(0, index_availability_1.textAlreadyHasBoundProjectPathPinNotice)(text)) {
|
|
2227
|
+
text = `${pinNotice}\n\n${text}`;
|
|
1935
2228
|
}
|
|
1936
|
-
|
|
1937
|
-
if (
|
|
2229
|
+
// Spec 0035: one-line product index status footer (skip if already present).
|
|
2230
|
+
if (!(0, index_availability_1.textAlreadyHasProductStatus)(text)) {
|
|
2231
|
+
let state;
|
|
1938
2232
|
try {
|
|
1939
|
-
|
|
2233
|
+
state = (0, index_availability_1.resolveProductIndexState)(cg);
|
|
1940
2234
|
}
|
|
1941
2235
|
catch {
|
|
1942
|
-
|
|
2236
|
+
return {
|
|
2237
|
+
...result,
|
|
2238
|
+
content: [{ type: 'text', text }, ...rest],
|
|
2239
|
+
};
|
|
1943
2240
|
}
|
|
2241
|
+
let pendingPaths;
|
|
2242
|
+
if (state === 'dirty') {
|
|
2243
|
+
try {
|
|
2244
|
+
pendingPaths = (cg.getPendingFiles?.() ?? []).map((p) => p.path);
|
|
2245
|
+
}
|
|
2246
|
+
catch {
|
|
2247
|
+
pendingPaths = [];
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
const line = (0, index_availability_1.formatProductStatusLine)(state, { pendingPaths });
|
|
2251
|
+
text = `${text}\n\n${line}`;
|
|
1944
2252
|
}
|
|
1945
|
-
const line = (0, index_availability_1.formatProductStatusLine)(state, { pendingPaths });
|
|
1946
2253
|
return {
|
|
1947
2254
|
...result,
|
|
1948
|
-
content: [{ type: 'text', text
|
|
2255
|
+
content: [{ type: 'text', text }, ...rest],
|
|
1949
2256
|
};
|
|
1950
2257
|
}
|
|
1951
2258
|
/**
|
|
@@ -1956,10 +2263,47 @@ class ToolHandler {
|
|
|
1956
2263
|
*/
|
|
1957
2264
|
async execute(toolName, args, sessionState) {
|
|
1958
2265
|
const requestStartedAt = Date.now();
|
|
2266
|
+
const result = await this.executeCore(toolName, args, sessionState, requestStartedAt);
|
|
2267
|
+
this.traceToolCall(toolName, args, result, requestStartedAt);
|
|
2268
|
+
return result;
|
|
2269
|
+
}
|
|
2270
|
+
/** Spec 0046: HOMEGRAPH_DEBUG tool summary → stderr + daemon.log. */
|
|
2271
|
+
traceToolCall(toolName, args, result, requestStartedAt) {
|
|
2272
|
+
try {
|
|
2273
|
+
let projectRoot;
|
|
2274
|
+
try {
|
|
2275
|
+
projectRoot = this.getHomeGraph(args.projectPath).getProjectRoot();
|
|
2276
|
+
}
|
|
2277
|
+
catch {
|
|
2278
|
+
projectRoot = typeof args.projectPath === 'string' ? args.projectPath : undefined;
|
|
2279
|
+
}
|
|
2280
|
+
const q = typeof args.query === 'string'
|
|
2281
|
+
? args.query
|
|
2282
|
+
: typeof args.symbol === 'string'
|
|
2283
|
+
? args.symbol
|
|
2284
|
+
: undefined;
|
|
2285
|
+
const evidenceStatus = result._meta?.homegraphEvidence?.status
|
|
2286
|
+
?? result[explore_session_state_1.EXPLORE_EMISSION_KEY]?.evidenceStatus;
|
|
2287
|
+
(0, runtime_log_1.logToolDebug)(toolName, {
|
|
2288
|
+
projectRoot,
|
|
2289
|
+
durationMs: Date.now() - requestStartedAt,
|
|
2290
|
+
isError: !!result.isError,
|
|
2291
|
+
evidenceStatus,
|
|
2292
|
+
query: q,
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
2295
|
+
catch {
|
|
2296
|
+
/* logging never fails the tool */
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
async executeCore(toolName, args, sessionState, requestStartedAt) {
|
|
1959
2300
|
args = { ...args };
|
|
1960
2301
|
for (const key of [QUERY_PLAN_ARG, QUERY_DEADLINE_ARG, QUERY_STARTED_ARG, QUERY_INDEX_STATE_ARG, QUERY_FAST_ATTEMPTED_ARG, '_hgEvidenceMaxChars'])
|
|
1961
2302
|
delete args[key];
|
|
1962
2303
|
try {
|
|
2304
|
+
// Spec 0043: drop any leftover pin notice from a prior call that exited
|
|
2305
|
+
// before withProductStatusFooter (gate / defer / refuse).
|
|
2306
|
+
this.boundProjectPathPinNotice = null;
|
|
1963
2307
|
// Block the first tool call on the engine's post-open reconcile so we
|
|
1964
2308
|
// never serve rows for files deleted/edited while no MCP server was
|
|
1965
2309
|
// running. The wait is time-boxed (#905): a huge-repo reconcile takes
|
|
@@ -2047,7 +2391,8 @@ class ToolHandler {
|
|
|
2047
2391
|
// would re-serve the first call's full source and defeat CG-18 dedup.
|
|
2048
2392
|
const skipCacheForSession = toolName === 'homegraph_explore' && !!sessionState;
|
|
2049
2393
|
const requestPlan = readQueryPlan(args);
|
|
2050
|
-
const cacheEnabled = !skipCacheForSession && requestPlan?.source !== 'llm'
|
|
2394
|
+
const cacheEnabled = !skipCacheForSession && !(toolName === 'homegraph_explore' && process.env.HOMEGRAPH_ARKTS_EVIDENCE_LOG === '1') && requestPlan?.source !== 'llm'
|
|
2395
|
+
&& !(requestPlan?.requestContract && ((0, request_contract_1.accuracyTargetsEnabled)() || (0, request_contract_1.accuracyCoverageEnabled)()))
|
|
2051
2396
|
&& !requestPlan?.telemetry.fallbackReason && (0, query_cache_1.isMcpQueryCacheEnabled)() && (0, query_cache_1.isCacheableMcpTool)(toolName);
|
|
2052
2397
|
let cacheKey;
|
|
2053
2398
|
let cacheQueries;
|
|
@@ -2069,7 +2414,7 @@ class ToolHandler {
|
|
|
2069
2414
|
const cached = cacheIndex.getEntry(cacheQueries, cacheKey);
|
|
2070
2415
|
const packMeta = cached?._meta?.homegraphEvidencePacks;
|
|
2071
2416
|
const cachedFiles = cached?._meta?.homegraphEvidence?.files;
|
|
2072
|
-
if (cached && (!packMeta || (packMeta.status === 'complete'
|
|
2417
|
+
if (cached && (!packMeta || (packMeta.status === 'complete' && !packMeta.implementationContext?.length
|
|
2073
2418
|
&& this.areEvidenceFilesCurrent(cachedFiles ?? [], cacheCg.getProjectRoot())))) {
|
|
2074
2419
|
const diagnosed = this.withQueryPlanDiagnostics(cached, args, true);
|
|
2075
2420
|
const withWorktree = this.withWorktreeNotice(diagnosed, projectPath);
|
|
@@ -2222,15 +2567,19 @@ class ToolHandler {
|
|
|
2222
2567
|
try {
|
|
2223
2568
|
const cg = this.getHomeGraph(args.projectPath);
|
|
2224
2569
|
const root = cg.getProjectRoot();
|
|
2225
|
-
const
|
|
2226
|
-
|
|
2227
|
-
return null;
|
|
2228
|
-
const hint = typeof args.symbol === 'string'
|
|
2570
|
+
const prior = sessionState.forProject(root);
|
|
2571
|
+
const symbolHint = typeof args.symbol === 'string'
|
|
2229
2572
|
? args.symbol
|
|
2230
2573
|
: typeof args.file === 'string'
|
|
2231
2574
|
? args.file
|
|
2232
2575
|
: undefined;
|
|
2233
|
-
|
|
2576
|
+
// Spec 0044 §10.2: symbols already on the locate list bypass the Partial depth cap.
|
|
2577
|
+
if ((0, locate_contract_1.shouldExemptDepthFuseForLocatedSymbol)(prior, symbolHint))
|
|
2578
|
+
return null;
|
|
2579
|
+
const decision = (0, explore_repeat_guard_1.decideDepthToolFuse)(prior, sessionState.depthToolCount(root), toolName);
|
|
2580
|
+
if (!decision.refuse)
|
|
2581
|
+
return null;
|
|
2582
|
+
return this.textResult((0, explore_repeat_guard_1.formatDepthToolRefuse)(decision, toolName, symbolHint));
|
|
2234
2583
|
}
|
|
2235
2584
|
catch {
|
|
2236
2585
|
return null;
|
|
@@ -2302,6 +2651,10 @@ class ToolHandler {
|
|
|
2302
2651
|
const last = prior?.calls[prior.calls.length - 1];
|
|
2303
2652
|
if (!last || (0, explore_session_state_1.inferExploreEvidenceStatus)(last) === 'complete')
|
|
2304
2653
|
return;
|
|
2654
|
+
const symbolHint = typeof args.symbol === 'string' ? args.symbol : undefined;
|
|
2655
|
+
// Spec 0044 §10.2: located-symbol drills do not consume the Partial depth budget.
|
|
2656
|
+
if ((0, locate_contract_1.shouldExemptDepthFuseForLocatedSymbol)(prior, symbolHint))
|
|
2657
|
+
return;
|
|
2305
2658
|
sessionState.recordDepthTool(root);
|
|
2306
2659
|
}
|
|
2307
2660
|
catch { /* bookkeeping only */ }
|
|
@@ -3150,6 +3503,13 @@ class ToolHandler {
|
|
|
3150
3503
|
registeredAt,
|
|
3151
3504
|
};
|
|
3152
3505
|
}
|
|
3506
|
+
if (m?.synthesizedBy === 'arkts-route-map') {
|
|
3507
|
+
return {
|
|
3508
|
+
label: `Harmony route registration (config → page/builder)`,
|
|
3509
|
+
compact: `route-map registration${at}`,
|
|
3510
|
+
registeredAt,
|
|
3511
|
+
};
|
|
3512
|
+
}
|
|
3153
3513
|
if (m?.synthesizedBy === 'goframe-route') {
|
|
3154
3514
|
const route = m.route ? `\`${String(m.route)}\`` : 'a route';
|
|
3155
3515
|
return {
|
|
@@ -5375,7 +5735,7 @@ class ToolHandler {
|
|
|
5375
5735
|
...prior, version: plan.version, source: plan.source, intent: plan.intent, route: plan.route,
|
|
5376
5736
|
confidence: plan.confidence,
|
|
5377
5737
|
plannerSeeds: { anchors: plan.anchors, searchTerms: plan.searchTerms, literalTexts: plan.literalTexts,
|
|
5378
|
-
sourceScope: plan.sourceScope, relation: plan.relation },
|
|
5738
|
+
sourceScope: plan.sourceScope, relation: plan.relation, requestContract: plan.requestContract },
|
|
5379
5739
|
hasTaskContext: !!plan.taskContext,
|
|
5380
5740
|
matchedFeatures: Object.entries(plan.features).filter(([, matched]) => matched).map(([name]) => name).slice(0, 12),
|
|
5381
5741
|
planningMs: plan.telemetry.durationMs, durationMs: Math.max(plan.telemetry.durationMs, Date.now() - started),
|
|
@@ -5400,6 +5760,10 @@ class ToolHandler {
|
|
|
5400
5760
|
if (plan.route === 'usages' || plan.route === 'modules' || plan.route === 'native') {
|
|
5401
5761
|
return this.runSpecializedExploreRoute(plan.route, cg, query, root, plan);
|
|
5402
5762
|
}
|
|
5763
|
+
// Constrained ArkTS retrieval needs the full, verified-source renderer.
|
|
5764
|
+
if (plan.requestContract && ((0, request_contract_1.accuracyTargetsEnabled)() || (0, request_contract_1.accuracyCoverageEnabled)())
|
|
5765
|
+
&& plan.sourceScope !== 'sdk' && cg.getFiles().some(f => /\.ets$/i.test(f.path)))
|
|
5766
|
+
return null;
|
|
5403
5767
|
// These legacy paths re-extract seeds from text and cannot consume bound
|
|
5404
5768
|
// node identity / step hints. Model general/flow plans use full explore;
|
|
5405
5769
|
// rule/default and specialized routes retain their existing fast behavior.
|
|
@@ -6260,20 +6624,58 @@ class ToolHandler {
|
|
|
6260
6624
|
if (!nodes.some(n => /\.ets$/i.test(n.filePath) && !n.filePath.startsWith('ohos-sdk:')))
|
|
6261
6625
|
return null;
|
|
6262
6626
|
const budget = getExploreOutputBudget(cg.getStats().fileCount);
|
|
6263
|
-
const
|
|
6627
|
+
const constrained = !!plan?.requestContract && ((0, request_contract_1.accuracyTargetsEnabled)() || (0, request_contract_1.accuracyCoverageEnabled)());
|
|
6628
|
+
const queryPaths = (!constrained || plan?.intent === 'flow' || !!plan?.bindings?.length) && process.env.HOMEGRAPH_ARKTS_QUERY_PATHS !== '0';
|
|
6264
6629
|
if (queryPaths)
|
|
6265
6630
|
nodes = (0, evidence_paths_1.completeEvidencePathCandidates)(query, nodes, (name, limit) => cg.getQueryBuilder().getNodesByQualifiedNameExact(name, limit), plan);
|
|
6631
|
+
if ((0, implementation_context_1.implementationContextEnabled)()) {
|
|
6632
|
+
const boundIds = new Set(focusIds);
|
|
6633
|
+
nodes = nodes.map(n => {
|
|
6634
|
+
if (n.kind !== 'struct')
|
|
6635
|
+
return n;
|
|
6636
|
+
const component = cg.getNodesInFile(n.filePath).find(c => c.kind === 'component' && c.name === n.name && c.startLine === n.startLine);
|
|
6637
|
+
if (component && boundIds.has(n.id))
|
|
6638
|
+
boundIds.add(component.id);
|
|
6639
|
+
return component ?? n;
|
|
6640
|
+
});
|
|
6641
|
+
focusIds = boundIds;
|
|
6642
|
+
}
|
|
6266
6643
|
const pathSearch = !queryPaths ? undefined : (0, evidence_paths_1.searchEvidencePaths)({
|
|
6267
6644
|
getNode: id => cg.getNode(id),
|
|
6268
|
-
getEdges: (id, direction, kinds, limit, preferred) =>
|
|
6645
|
+
getEdges: (id, direction, kinds, limit, preferred) => {
|
|
6646
|
+
const read = (nodeId) => cg.getQueryBuilder().getEvidenceEdges(nodeId, direction, kinds, limit, preferred);
|
|
6647
|
+
if (!(0, implementation_context_1.implementationContextEnabled)())
|
|
6648
|
+
return read(id);
|
|
6649
|
+
const owner = cg.getNode(id);
|
|
6650
|
+
const aliases = owner && ['struct', 'component'].includes(owner.kind)
|
|
6651
|
+
? cg.getNodesInFile(owner.filePath).filter(n => ['struct', 'component'].includes(n.kind) && n.name === owner.name
|
|
6652
|
+
&& n.startLine === owner.startLine && n.endLine === owner.endLine).slice(0, 2) : [];
|
|
6653
|
+
const canonical = (nodeId) => {
|
|
6654
|
+
const n = cg.getNode(nodeId);
|
|
6655
|
+
if (!n || n.kind !== 'struct')
|
|
6656
|
+
return nodeId;
|
|
6657
|
+
return cg.getNodesInFile(n.filePath).find(c => c.kind === 'component' && c.name === n.name
|
|
6658
|
+
&& c.startLine === n.startLine && c.endLine === n.endLine)?.id ?? nodeId;
|
|
6659
|
+
};
|
|
6660
|
+
// ArkAnalyzer stores struct/component roles for the same declaration. Project
|
|
6661
|
+
// their ownership edges to one identity; this is not a new runtime call.
|
|
6662
|
+
const edges = (aliases.length ? aliases.flatMap(n => read(n.id)) : read(id)).map(e => ({ ...e,
|
|
6663
|
+
source: canonical(e.source), target: canonical(e.target) })).filter(e => e.source !== e.target);
|
|
6664
|
+
return [...new Map(edges.map(e => [JSON.stringify([e.source, e.target, e.kind, e.metadata]), e])).values()].slice(0, limit);
|
|
6665
|
+
},
|
|
6269
6666
|
}, (0, evidence_paths_1.resolveEvidencePathGoal)(query, nodes, focusIds, plan));
|
|
6270
6667
|
const result = (0, arkts_evidence_packs_1.buildArktsEvidencePacks)(cg, { projectRoot, query, nodes, focusIds,
|
|
6271
|
-
maxChars: Math.min(budget.maxOutputChars, maxChars ?? budget.maxOutputChars), maxFiles, pathSearch
|
|
6668
|
+
maxChars: Math.min(budget.maxOutputChars, maxChars ?? budget.maxOutputChars), maxFiles, pathSearch, requestContract: plan?.requestContract,
|
|
6669
|
+
sdkModule: cg.getGraphSources() === 'project' || cg.getGraphSources() === 'none' ? undefined : module => {
|
|
6670
|
+
const nodes = cg.getQueryBuilder().getOhosApiModuleNodes(module);
|
|
6671
|
+
return nodes.length ? { nodes, version: cg.getOhosApiBinding()?.version ?? 'unknown' } : undefined;
|
|
6672
|
+
} });
|
|
6272
6673
|
if (!result)
|
|
6273
6674
|
return null;
|
|
6675
|
+
const audit = (0, evidence_audit_1.recordEvidencePack)(projectRoot, { query, requestContract: plan?.requestContract }, result);
|
|
6274
6676
|
// The pack renderer already supplies neutral guidance and exact source bytes.
|
|
6275
6677
|
return { content: [{ type: 'text', text: result.text }], [explore_session_state_1.EXPLORE_EMISSION_KEY]: result.emission,
|
|
6276
|
-
_meta: { homegraphEvidencePacks: result.metadata } };
|
|
6678
|
+
_meta: { homegraphEvidencePacks: result.metadata, ...(audit !== 'disabled' ? { homegraphEvidenceAudit: audit } : {}) } };
|
|
6277
6679
|
}
|
|
6278
6680
|
/** Render a compact symbol-bounded slice on the legacy mechanism path. */
|
|
6279
6681
|
renderLightMechanismSource(projectRoot, filePath, nodes, maxChars) {
|
|
@@ -6318,7 +6720,13 @@ class ToolHandler {
|
|
|
6318
6720
|
const isTestPath = (p) => /(^|\/)(tests?|spec)\//i.test(p) || /\.(test|spec)\./i.test(p);
|
|
6319
6721
|
const fileNodes = new Map();
|
|
6320
6722
|
const seedIds = new Set();
|
|
6321
|
-
const domainPathTokens = (0,
|
|
6723
|
+
const domainPathTokens = (0, locate_contract_1.shouldSuppressSynonymExpansion)({
|
|
6724
|
+
hasExactAnchorHit: (0, query_utils_1.extractInRepoLocateAnchors)(query).length > 0
|
|
6725
|
+
|| (0, query_utils_1.extractTypeNamesFromQuery)(query).length > 0,
|
|
6726
|
+
hasLiteralWitness: false,
|
|
6727
|
+
})
|
|
6728
|
+
? []
|
|
6729
|
+
: (0, query_utils_1.mechanismDomainPathTokens)(query);
|
|
6322
6730
|
const addNode = (n) => {
|
|
6323
6731
|
if ((0, arkts_1.isOhosApiFilePath)(n.filePath))
|
|
6324
6732
|
return;
|
|
@@ -6777,9 +7185,8 @@ class ToolHandler {
|
|
|
6777
7185
|
* Static AA class/interface nodes that look like domain Managers/Services.
|
|
6778
7186
|
*/
|
|
6779
7187
|
collectDomainRoleSymbols(cg, query, domainTokens) {
|
|
6780
|
-
|
|
6781
|
-
|
|
6782
|
-
: (0, query_utils_1.mechanismDomainPathTokens)(query);
|
|
7188
|
+
// Spec 0044 §9: callers may pass [] to suppress CJK→ASCII synonym expansion.
|
|
7189
|
+
const tokens = domainTokens;
|
|
6783
7190
|
if (tokens.length === 0)
|
|
6784
7191
|
return [];
|
|
6785
7192
|
const byId = new Map();
|
|
@@ -7226,7 +7633,9 @@ class ToolHandler {
|
|
|
7226
7633
|
}
|
|
7227
7634
|
if (!callersOnly) {
|
|
7228
7635
|
for (const { node: c } of cg.getCallees(id).slice(0, 10)) {
|
|
7229
|
-
|
|
7636
|
+
if ((0, locate_contract_1.shouldDemoteLogToastSpine)(c.name, query)) {
|
|
7637
|
+
continue;
|
|
7638
|
+
}
|
|
7230
7639
|
if (/^log(?:Info|Error|Warn|Debug|Fatal)?$/i.test(c.name) || /^hilog$/i.test(c.name)) {
|
|
7231
7640
|
continue;
|
|
7232
7641
|
}
|
|
@@ -7415,11 +7824,15 @@ class ToolHandler {
|
|
|
7415
7824
|
continue;
|
|
7416
7825
|
trail.push(`- \`${seedNode.name}\` (${seedNode.kind}) — ${seedNode.filePath}:${seedNode.startLine}`);
|
|
7417
7826
|
for (const c of callers) {
|
|
7827
|
+
if ((0, query_utils_1.isMcpNoiseNode)(c.node))
|
|
7828
|
+
continue;
|
|
7418
7829
|
const loc = c.node.startLine ? `:${c.node.startLine}` : '';
|
|
7419
7830
|
trail.push(` ← used by \`${c.node.name}\` (${c.node.kind}) — ${c.node.filePath}${loc}`);
|
|
7420
7831
|
trailBullets++;
|
|
7421
7832
|
}
|
|
7422
7833
|
for (const c of callees) {
|
|
7834
|
+
if ((0, query_utils_1.isMcpNoiseNode)(c.node))
|
|
7835
|
+
continue;
|
|
7423
7836
|
const loc = c.node.startLine ? `:${c.node.startLine}` : '';
|
|
7424
7837
|
trail.push(` → calls \`${c.node.name}\` (${c.node.kind}) — ${c.node.filePath}${loc}`);
|
|
7425
7838
|
trailBullets++;
|
|
@@ -9638,6 +10051,8 @@ class ToolHandler {
|
|
|
9638
10051
|
const explicitMaxFiles = typeof args.maxFiles === 'number' && !Number.isNaN(args.maxFiles);
|
|
9639
10052
|
let maxFiles = (0, utils_1.clamp)(args.maxFiles || budget.defaultMaxFiles, 1, 20);
|
|
9640
10053
|
const queryFileBasenames = (0, query_utils_1.extractFileBasenamesFromQuery)(query);
|
|
10054
|
+
const locateAnchors = (0, query_utils_1.extractInRepoLocateAnchors)(query);
|
|
10055
|
+
const exploreSourceScope = (0, query_utils_1.resolveExploreSourceScope)(query, plan?.sourceScope);
|
|
9641
10056
|
const interpretationQuery = feature('queryAsInterpretationSurvey', query_utils_1.queryAsInterpretationSurvey);
|
|
9642
10057
|
const testOnlyInterpretation = feature('queryAsTestOnlyInterpretation', query_utils_1.queryAsTestOnlyInterpretation);
|
|
9643
10058
|
const crossModuleFlow = feature('queryAsCrossModuleFlowSurvey', query_utils_1.queryAsCrossModuleFlowSurvey) || plan?.intent === 'flow';
|
|
@@ -9649,19 +10064,29 @@ class ToolHandler {
|
|
|
9649
10064
|
? `${queryFileBasenames[0]} ${query}`
|
|
9650
10065
|
: queryFileBasenames.length === 1
|
|
9651
10066
|
? `${queryFileBasenames[0]} ${query}`
|
|
9652
|
-
:
|
|
10067
|
+
: locateAnchors.length === 1
|
|
10068
|
+
? `${locateAnchors[0]} ${query}`
|
|
10069
|
+
: query;
|
|
10070
|
+
const wantHints = !!(plan && (plan.source === 'llm' || plan.literalTexts?.length || plan.anchors?.length || plan.requestContract))
|
|
10071
|
+
|| exploreSourceScope !== 'all';
|
|
9653
10072
|
const subgraph = await cg.findRelevantContext(contextQuery, {
|
|
9654
10073
|
...contextOpts,
|
|
9655
|
-
...(
|
|
9656
|
-
symbols:
|
|
9657
|
-
|
|
10074
|
+
...(wantHints ? { retrievalHints: {
|
|
10075
|
+
symbols: [
|
|
10076
|
+
...(plan?.anchors ?? []).filter((anchor) => !(plan?.bindings ?? []).some((node) => anchor === node.name || anchor === node.qualifiedName)),
|
|
10077
|
+
...locateAnchors,
|
|
10078
|
+
].slice(0, 16),
|
|
10079
|
+
searchTerms: plan?.searchTerms ?? [], literalTexts: [...new Set([...(plan?.literalTexts ?? []),
|
|
10080
|
+
...((0, request_contract_1.accuracyTargetsEnabled)() && !plan?.bindings?.length ? (0, request_contract_1.contractLiteralTexts)(plan?.requestContract) : [])])].slice(0, 8),
|
|
10081
|
+
sourceScope: exploreSourceScope,
|
|
10082
|
+
nodeIds: (plan?.bindings ?? []).map((node) => node.id),
|
|
9658
10083
|
} } : {}),
|
|
9659
10084
|
});
|
|
9660
|
-
// Path-first
|
|
9661
|
-
//
|
|
9662
|
-
|
|
9663
|
-
|
|
9664
|
-
for (const base of
|
|
10085
|
+
// Path-first + Spec 0042 identifier seeding: Foo.ets and PascalCase ≥8 exact hits
|
|
10086
|
+
// become roots so taskContext-polluted FTS cannot drop the named in-repo file.
|
|
10087
|
+
if (exploreSourceScope !== 'sdk') {
|
|
10088
|
+
const seedNames = [...new Set([...queryFileBasenames, ...locateAnchors])].slice(0, 6);
|
|
10089
|
+
for (const base of seedNames) {
|
|
9665
10090
|
let hits = [];
|
|
9666
10091
|
try {
|
|
9667
10092
|
hits = cg.searchNodes(base, { limit: 50 });
|
|
@@ -9670,7 +10095,15 @@ class ToolHandler {
|
|
|
9670
10095
|
continue;
|
|
9671
10096
|
}
|
|
9672
10097
|
for (const r of hits) {
|
|
9673
|
-
if (
|
|
10098
|
+
if ((0, query_utils_1.isMcpNoiseNode)(r.node))
|
|
10099
|
+
continue;
|
|
10100
|
+
const fp = r.node.filePath.replace(/\\/g, '/');
|
|
10101
|
+
const stem = fp.split('/').pop()?.replace(/\.[^.]+$/, '') ?? '';
|
|
10102
|
+
const exactFile = stem.toLowerCase() === base.toLowerCase()
|
|
10103
|
+
|| (0, query_utils_1.fileMatchesQueryBasename)(r.node.filePath, [base]);
|
|
10104
|
+
const exactSymbol = r.node.name === base
|
|
10105
|
+
|| (r.node.qualifiedName?.split(/::|\./).pop() === base);
|
|
10106
|
+
if (!exactFile && !exactSymbol)
|
|
9674
10107
|
continue;
|
|
9675
10108
|
if (!subgraph.nodes.has(r.node.id)) {
|
|
9676
10109
|
subgraph.nodes.set(r.node.id, r.node);
|
|
@@ -9679,12 +10112,48 @@ class ToolHandler {
|
|
|
9679
10112
|
}
|
|
9680
10113
|
}
|
|
9681
10114
|
}
|
|
10115
|
+
if (plan?.requestContract && ((0, request_contract_1.accuracyTargetsEnabled)() || (0, request_contract_1.accuracyCoverageEnabled)()) && plan.sourceScope !== 'sdk'
|
|
10116
|
+
// Resource values need the existing resource + reference renderer; a bare
|
|
10117
|
+
// declaration would silently drop the value-to-key witness.
|
|
10118
|
+
&& !subgraph.literalEvidence?.hits.some(hit => hit.kind === 'resource_reference')) {
|
|
10119
|
+
// One existing retrieval pass; raw literal hits affect candidate order only.
|
|
10120
|
+
// Actual target evidence is checked against hash-verified source in the packer.
|
|
10121
|
+
const hits = subgraph.literalEvidence?.hits ?? [];
|
|
10122
|
+
const fileHits = new Map();
|
|
10123
|
+
for (const hit of hits) {
|
|
10124
|
+
const labels = fileHits.get(hit.filePath) ?? new Set();
|
|
10125
|
+
labels.add(hit.literal);
|
|
10126
|
+
fileHits.set(hit.filePath, labels);
|
|
10127
|
+
}
|
|
10128
|
+
const candidates = [...subgraph.nodes.values()];
|
|
10129
|
+
if ((0, request_contract_1.accuracyTargetsEnabled)())
|
|
10130
|
+
candidates.sort((a, b) => (fileHits.get(b.filePath)?.size ?? 0) - (fileHits.get(a.filePath)?.size ?? 0));
|
|
10131
|
+
const packed = this.tryArktsEvidenceExplore(cg, query, projectRoot, candidates, new Set(plan.bindings?.length ? plan.bindings.map(b => b.id) : subgraph.roots), args._hgEvidenceMaxChars, maxFiles, plan);
|
|
10132
|
+
if (packed)
|
|
10133
|
+
return packed;
|
|
10134
|
+
}
|
|
9682
10135
|
const literalSource = this.renderLiteralSource(cg, subgraph);
|
|
9683
10136
|
if (subgraph.nodes.size === 0) {
|
|
9684
|
-
|
|
9685
|
-
|
|
9686
|
-
|
|
9687
|
-
|
|
10137
|
+
// Spec 0044 §9: empty exact evidence → Miss (paths only), not a source dump.
|
|
10138
|
+
let fuzzyPaths = [];
|
|
10139
|
+
try {
|
|
10140
|
+
fuzzyPaths = [...new Set(cg.searchNodes(query.slice(0, 64), { limit: 12 })
|
|
10141
|
+
.map((r) => r.node.filePath.replace(/\\/g, '/'))
|
|
10142
|
+
.filter((p) => p && !(0, arkts_1.isOhosApiFilePath)(p)))].slice(0, 8);
|
|
10143
|
+
}
|
|
10144
|
+
catch { /* ignore */ }
|
|
10145
|
+
const text = literalSource.text
|
|
10146
|
+
|| (0, locate_contract_1.formatMissBanner)(fuzzyPaths);
|
|
10147
|
+
return this.exploreResult(text, {
|
|
10148
|
+
projectRoot,
|
|
10149
|
+
query,
|
|
10150
|
+
files: literalSource.files,
|
|
10151
|
+
sourceBytes: literalSource.files.reduce((sum, file) => sum + file.bytes, 0),
|
|
10152
|
+
responseBytes: text.length,
|
|
10153
|
+
locatedNodes: literalSource.nodes,
|
|
10154
|
+
partial: true,
|
|
10155
|
+
evidenceStatus: literalSource.text ? 'partial' : 'empty',
|
|
10156
|
+
});
|
|
9688
10157
|
}
|
|
9689
10158
|
// Seed import nodes for @kit.* / *Kit names (and named symbols like taskpool).
|
|
9690
10159
|
const importTerms = (0, query_utils_1.extractImportSearchTerms)(query);
|
|
@@ -9852,7 +10321,7 @@ class ToolHandler {
|
|
|
9852
10321
|
const raw = isQual ? this.findAllSymbols(cg, t).nodes : cg.getNodesByName(t);
|
|
9853
10322
|
let cands = raw
|
|
9854
10323
|
.filter((n) => SEED_KINDS.has(n.kind) && !isTestPath(n.filePath)
|
|
9855
|
-
&& !(
|
|
10324
|
+
&& !(exploreSourceScope === 'local' && (0, arkts_1.isOhosApiFilePath)(n.filePath)))
|
|
9856
10325
|
.sort((a, b) => {
|
|
9857
10326
|
// Prefer callables over types when both share a name, then body size.
|
|
9858
10327
|
const ac = CALLABLE.has(a.kind) ? 1 : 0;
|
|
@@ -10213,10 +10682,12 @@ class ToolHandler {
|
|
|
10213
10682
|
// dropped, so the budget never fills with incidental files. Guarded so it
|
|
10214
10683
|
// never prunes below 2.
|
|
10215
10684
|
if (maxGraph > 0) {
|
|
10685
|
+
const litKeep = new Set((subgraph.literalEvidence?.hits ?? []).map((h) => h.filePath).filter(Boolean));
|
|
10216
10686
|
const gated = relevantFiles.filter(([fp]) => (fileGraphScore.get(fp) ?? 0) >= maxGraph * 0.06
|
|
10217
10687
|
|| centralFiles.has(fp)
|
|
10218
10688
|
|| entryFiles.has(fp)
|
|
10219
10689
|
|| changeSurfaceFiles.has(fp)
|
|
10690
|
+
|| litKeep.has(fp)
|
|
10220
10691
|
|| (fileTermHits.get(fp) ?? 0) >= 2);
|
|
10221
10692
|
if (gated.length >= 2)
|
|
10222
10693
|
relevantFiles = gated;
|
|
@@ -10241,6 +10712,22 @@ class ToolHandler {
|
|
|
10241
10712
|
// tier so it isn't buried under files that merely share surface words (#1064).
|
|
10242
10713
|
for (const fp of changeSurfaceFiles)
|
|
10243
10714
|
namedSeedFiles.add(fp);
|
|
10715
|
+
// Spec 0044 §7: literal witness files must stay in the pack and rank with named seeds.
|
|
10716
|
+
{
|
|
10717
|
+
const litPaths = [...new Set((subgraph.literalEvidence?.hits ?? []).map((h) => h.filePath).filter(Boolean))];
|
|
10718
|
+
for (const fp of litPaths) {
|
|
10719
|
+
entryFiles.add(fp);
|
|
10720
|
+
namedSeedFiles.add(fp);
|
|
10721
|
+
if (relevantFiles.some(([f]) => f === fp))
|
|
10722
|
+
continue;
|
|
10723
|
+
let group = fileGroups.get(fp);
|
|
10724
|
+
if (!group) {
|
|
10725
|
+
group = { nodes: [], score: 1000 };
|
|
10726
|
+
fileGroups.set(fp, group);
|
|
10727
|
+
}
|
|
10728
|
+
relevantFiles.push([fp, group]);
|
|
10729
|
+
}
|
|
10730
|
+
}
|
|
10244
10731
|
// Multi-term corroboration tier: a file that is BOTH (a) an entry/central file
|
|
10245
10732
|
// (a search root, named seed, or graph-central hub — i.e. structurally part of
|
|
10246
10733
|
// the answer) AND (b) matched by ≥2 DISTINCT query terms must not be buried by
|
|
@@ -10264,7 +10751,7 @@ class ToolHandler {
|
|
|
10264
10751
|
const sortedFiles = relevantFiles.sort((a, b) => {
|
|
10265
10752
|
const aPath = a[0].toLowerCase();
|
|
10266
10753
|
const bPath = b[0].toLowerCase();
|
|
10267
|
-
if (
|
|
10754
|
+
if (exploreSourceScope === 'local') {
|
|
10268
10755
|
const sdkOrder = Number((0, arkts_1.isOhosApiFilePath)(a[0])) - Number((0, arkts_1.isOhosApiFilePath)(b[0]));
|
|
10269
10756
|
if (sdkOrder)
|
|
10270
10757
|
return sdkOrder;
|
|
@@ -10273,6 +10760,17 @@ class ToolHandler {
|
|
|
10273
10760
|
- Number((subgraph.literalEvidence?.hits ?? []).some((hit) => hit.filePath === a[0]));
|
|
10274
10761
|
if (literalOrder)
|
|
10275
10762
|
return literalOrder;
|
|
10763
|
+
// Spec 0044 §7: demote Logger/hilog/Toast-only files when query did not name them.
|
|
10764
|
+
const logToastDemote = (fp) => {
|
|
10765
|
+
const group = fileGroups.get(fp);
|
|
10766
|
+
if (!group?.nodes.length)
|
|
10767
|
+
return 0;
|
|
10768
|
+
const allNoise = group.nodes.every((n) => (0, locate_contract_1.shouldDemoteLogToastSpine)(n.name, query));
|
|
10769
|
+
return allNoise ? 1 : 0;
|
|
10770
|
+
};
|
|
10771
|
+
const logOrder = logToastDemote(a[0]) - logToastDemote(b[0]);
|
|
10772
|
+
if (logOrder)
|
|
10773
|
+
return logOrder;
|
|
10276
10774
|
// Query-named file (LocationController.ets in the question) before partial
|
|
10277
10775
|
// substring matches (control.ets matching "Controller" inside LocationController).
|
|
10278
10776
|
const aExactBase = (0, query_utils_1.fileMatchesQueryBasename)(a[0], queryFileBasenames) ? 1 : 0;
|
|
@@ -11345,7 +11843,15 @@ class ToolHandler {
|
|
|
11345
11843
|
const headerSuffix = omittedCount > 0
|
|
11346
11844
|
? `${headerSymbols.join(', ')}, +${omittedCount} more`
|
|
11347
11845
|
: headerSymbols.join(', ');
|
|
11348
|
-
|
|
11846
|
+
let fileHeader = fileSectionHeader(filePath, headerSuffix);
|
|
11847
|
+
// Spec 0044 §10.1: warn when basename ≠ primary declaration.
|
|
11848
|
+
{
|
|
11849
|
+
const primary = group?.nodes.find((n) => ['class', 'struct', 'component', 'interface', 'enum'].includes(n.kind)
|
|
11850
|
+
&& !n.name.startsWith('%')) ?? group?.nodes.find((n) => ['function', 'method'].includes(n.kind) && !n.name.startsWith('%'));
|
|
11851
|
+
const mismatch = (0, locate_contract_1.formatFilenameDeclarationMismatch)(filePath, primary?.name);
|
|
11852
|
+
if (mismatch)
|
|
11853
|
+
fileHeader = `${fileHeader}\n${mismatch}`;
|
|
11854
|
+
}
|
|
11349
11855
|
// The total cap bounds INCIDENTAL files only. A file that DEFINES a symbol
|
|
11350
11856
|
// the agent named (or that's on the flow spine) renders even when the
|
|
11351
11857
|
// nominal total is used up — it's the answer, and the set is bounded by
|
|
@@ -11595,7 +12101,10 @@ class ToolHandler {
|
|
|
11595
12101
|
const nodes = [];
|
|
11596
12102
|
const seen = new Set();
|
|
11597
12103
|
let chars = 0;
|
|
11598
|
-
|
|
12104
|
+
// Preserve value-to-key provenance when a plain label in the same file
|
|
12105
|
+
// would otherwise consume its single source-witness slot first.
|
|
12106
|
+
const literalHits = [...(subgraph.literalEvidence?.hits ?? [])].sort((a, b) => Number(!!b.resource) - Number(!!a.resource));
|
|
12107
|
+
for (const hit of literalHits) {
|
|
11599
12108
|
if (files.length >= 2 || seen.has(hit.filePath))
|
|
11600
12109
|
continue;
|
|
11601
12110
|
const absolute = (0, utils_1.validatePathWithinRoot)(cg.getProjectRoot(), hit.filePath);
|
|
@@ -11639,6 +12148,106 @@ class ToolHandler {
|
|
|
11639
12148
|
}
|
|
11640
12149
|
return { text: sections.join('\n\n'), files, nodes };
|
|
11641
12150
|
}
|
|
12151
|
+
/**
|
|
12152
|
+
* Spec 0039: when the query names Harmony route profiles, lead with a short
|
|
12153
|
+
* Registration sources table so agents see JSON was indexed (avoid re-Read).
|
|
12154
|
+
*/
|
|
12155
|
+
prependHarmonyRegistrationSources(result, projectRoot, query) {
|
|
12156
|
+
if (!/route_map|router_map|main_pages/i.test(query))
|
|
12157
|
+
return result;
|
|
12158
|
+
const [first, ...rest] = result.content;
|
|
12159
|
+
if (!first || first.type !== 'text')
|
|
12160
|
+
return result;
|
|
12161
|
+
if (/^\*\*Registration sources\*\*/m.test(first.text))
|
|
12162
|
+
return result;
|
|
12163
|
+
let cg;
|
|
12164
|
+
try {
|
|
12165
|
+
cg = this.getHomeGraph(projectRoot);
|
|
12166
|
+
}
|
|
12167
|
+
catch {
|
|
12168
|
+
return result;
|
|
12169
|
+
}
|
|
12170
|
+
const section = formatHarmonyRegistrationSources(cg);
|
|
12171
|
+
if (!section)
|
|
12172
|
+
return result;
|
|
12173
|
+
return {
|
|
12174
|
+
...result,
|
|
12175
|
+
content: [{ type: 'text', text: `${section}\n\n${first.text}` }, ...rest],
|
|
12176
|
+
};
|
|
12177
|
+
}
|
|
12178
|
+
/** Spec 0041: lead with Resource hits when string.json is named or matched. */
|
|
12179
|
+
prependHarmonyResourceHits(result, projectRoot, query) {
|
|
12180
|
+
const [first, ...rest] = result.content;
|
|
12181
|
+
if (!first || first.type !== 'text')
|
|
12182
|
+
return result;
|
|
12183
|
+
if (/^\*\*Resource hits\*\*/m.test(first.text) || /\n\*\*Resource hits\*\*/m.test(first.text)) {
|
|
12184
|
+
return result;
|
|
12185
|
+
}
|
|
12186
|
+
let cg;
|
|
12187
|
+
try {
|
|
12188
|
+
cg = this.getHomeGraph(projectRoot);
|
|
12189
|
+
}
|
|
12190
|
+
catch {
|
|
12191
|
+
return result;
|
|
12192
|
+
}
|
|
12193
|
+
const section = formatHarmonyResourceHits(cg, query);
|
|
12194
|
+
if (!section)
|
|
12195
|
+
return result;
|
|
12196
|
+
return {
|
|
12197
|
+
...result,
|
|
12198
|
+
content: [{ type: 'text', text: `${section}\n\n${first.text}` }, ...rest],
|
|
12199
|
+
};
|
|
12200
|
+
}
|
|
12201
|
+
/** Spec 0048 §1–2: Capability profiles or form negative evidence. */
|
|
12202
|
+
prependHarmonyCapabilityProfiles(result, projectRoot, query) {
|
|
12203
|
+
const [first, ...rest] = result.content;
|
|
12204
|
+
if (!first || first.type !== 'text')
|
|
12205
|
+
return result;
|
|
12206
|
+
if (/^\*\*Capability profiles/m.test(first.text)
|
|
12207
|
+
|| /\n\*\*Capability profiles/m.test(first.text)) {
|
|
12208
|
+
return result;
|
|
12209
|
+
}
|
|
12210
|
+
let cg;
|
|
12211
|
+
try {
|
|
12212
|
+
cg = this.getHomeGraph(projectRoot);
|
|
12213
|
+
}
|
|
12214
|
+
catch {
|
|
12215
|
+
return result;
|
|
12216
|
+
}
|
|
12217
|
+
const section = formatHarmonyCapabilityProfiles(cg, query);
|
|
12218
|
+
if (!section)
|
|
12219
|
+
return result;
|
|
12220
|
+
return {
|
|
12221
|
+
...result,
|
|
12222
|
+
content: [{ type: 'text', text: `${section}\n\n${first.text}` }, ...rest],
|
|
12223
|
+
};
|
|
12224
|
+
}
|
|
12225
|
+
/** Spec 0048 §5: seam notes for located anchors. */
|
|
12226
|
+
prependHarmonySeamNotes(result, projectRoot, emission) {
|
|
12227
|
+
const located = emission?.locatedNodes;
|
|
12228
|
+
if (!located || located.length === 0)
|
|
12229
|
+
return result;
|
|
12230
|
+
const [first, ...rest] = result.content;
|
|
12231
|
+
if (!first || first.type !== 'text')
|
|
12232
|
+
return result;
|
|
12233
|
+
if (/^\*\*Seam notes\*\*/m.test(first.text) || /\n\*\*Seam notes\*\*/m.test(first.text)) {
|
|
12234
|
+
return result;
|
|
12235
|
+
}
|
|
12236
|
+
let cg;
|
|
12237
|
+
try {
|
|
12238
|
+
cg = this.getHomeGraph(projectRoot);
|
|
12239
|
+
}
|
|
12240
|
+
catch {
|
|
12241
|
+
return result;
|
|
12242
|
+
}
|
|
12243
|
+
const section = formatHarmonySeamNotes(cg, projectRoot, located);
|
|
12244
|
+
if (!section)
|
|
12245
|
+
return result;
|
|
12246
|
+
return {
|
|
12247
|
+
...result,
|
|
12248
|
+
content: [{ type: 'text', text: `${section}\n\n${first.text}` }, ...rest],
|
|
12249
|
+
};
|
|
12250
|
+
}
|
|
11642
12251
|
/**
|
|
11643
12252
|
* An explore response plus the record of what it emitted (CG-17). The record
|
|
11644
12253
|
* rides the result only as far as {@link execute}, which files it into the
|
|
@@ -11646,17 +12255,42 @@ class ToolHandler {
|
|
|
11646
12255
|
*/
|
|
11647
12256
|
exploreResult(text, emission) {
|
|
11648
12257
|
const meta = (0, explore_repeat_guard_1.inferExplorePartialMeta)(text);
|
|
11649
|
-
|
|
12258
|
+
let evidenceStatus = emission.evidenceStatus
|
|
12259
|
+
?? (emission.sourceBytes > 0 ? (meta.partial ? 'partial' : 'complete') : 'partial');
|
|
12260
|
+
let body = text;
|
|
12261
|
+
// Spec 0044 §8: Located banner when complete exact evidence; soften Partial wording.
|
|
12262
|
+
if (evidenceStatus === 'complete' && !body.includes(locate_contract_1.LOCATED_MARKER)) {
|
|
12263
|
+
if (/\*\*Partial locator\*\*/i.test(body)) {
|
|
12264
|
+
body = body.replace(/>\s*\*\*Partial locator\*\*[^\n]*/gi, (0, locate_contract_1.formatLocatedBanner)());
|
|
12265
|
+
}
|
|
12266
|
+
else if (!/\*\*ANSWER NOW/i.test(body) && !body.includes(locate_contract_1.MISS_MARKER)) {
|
|
12267
|
+
body = `${(0, locate_contract_1.formatLocatedBanner)()}\n\n${body}`;
|
|
12268
|
+
}
|
|
12269
|
+
}
|
|
12270
|
+
else if ((evidenceStatus === 'partial' || meta.partial)
|
|
12271
|
+
&& /\*\*Partial locator\*\*/i.test(body)
|
|
12272
|
+
&& !body.includes(locate_contract_1.LOCATED_MARKER)) {
|
|
12273
|
+
// Spec 0044 §8: Partial next-step → node/usages/search (not Grep same names).
|
|
12274
|
+
body = body.replace(/>\s*\*\*Partial locator\*\*[^\n]*/gi, (0, locate_contract_1.formatPartialBanner)());
|
|
12275
|
+
}
|
|
12276
|
+
let result = this.textResult(body);
|
|
12277
|
+
result = this.prependHarmonyRegistrationSources(result, emission.projectRoot, emission.query);
|
|
12278
|
+
result = this.prependHarmonyCapabilityProfiles(result, emission.projectRoot, emission.query);
|
|
12279
|
+
result = this.prependHarmonyResourceHits(result, emission.projectRoot, emission.query);
|
|
11650
12280
|
result[explore_session_state_1.EXPLORE_EMISSION_KEY] = {
|
|
11651
12281
|
...emission,
|
|
11652
|
-
evidenceStatus
|
|
11653
|
-
partial: emission.partial ?? (
|
|
12282
|
+
evidenceStatus,
|
|
12283
|
+
partial: emission.partial ?? (evidenceStatus !== 'complete' ? true : meta.partial),
|
|
11654
12284
|
nextAnchor: emission.nextAnchor ?? meta.nextAnchor,
|
|
11655
12285
|
};
|
|
12286
|
+
result = this.prependHarmonySeamNotes(result, emission.projectRoot, result[explore_session_state_1.EXPLORE_EMISSION_KEY]);
|
|
11656
12287
|
return result;
|
|
11657
12288
|
}
|
|
11658
12289
|
/** Attach a minimal session emission when a compact/light path used textResult. */
|
|
11659
12290
|
ensureExploreEmission(result, projectRoot, query) {
|
|
12291
|
+
result = this.prependHarmonyRegistrationSources(result, projectRoot, query);
|
|
12292
|
+
result = this.prependHarmonyCapabilityProfiles(result, projectRoot, query);
|
|
12293
|
+
result = this.prependHarmonyResourceHits(result, projectRoot, query);
|
|
11660
12294
|
if (result[explore_session_state_1.EXPLORE_EMISSION_KEY]) {
|
|
11661
12295
|
const em = result[explore_session_state_1.EXPLORE_EMISSION_KEY];
|
|
11662
12296
|
if (em.partial === undefined || !em.nextAnchor) {
|
|
@@ -11666,6 +12300,7 @@ class ToolHandler {
|
|
|
11666
12300
|
if (!em.nextAnchor && meta.nextAnchor)
|
|
11667
12301
|
em.nextAnchor = meta.nextAnchor;
|
|
11668
12302
|
}
|
|
12303
|
+
result = this.prependHarmonySeamNotes(result, projectRoot, em);
|
|
11669
12304
|
return result;
|
|
11670
12305
|
}
|
|
11671
12306
|
const text = result.content?.[0]?.text ?? '';
|
|
@@ -11839,7 +12474,8 @@ class ToolHandler {
|
|
|
11839
12474
|
const nodes = cg.getNodesInFile(filePath)
|
|
11840
12475
|
.filter((n) => n.kind !== 'file' && n.kind !== 'import' && n.kind !== 'export')
|
|
11841
12476
|
.sort((a, b) => a.startLine - b.startLine);
|
|
11842
|
-
const dependents = cg.getFileDependents(filePath)
|
|
12477
|
+
const dependents = cg.getFileDependents(filePath)
|
|
12478
|
+
.filter((p) => !p.includes('@dummy') && !p.replace(/\\/g, '/').split('/').pop()?.startsWith('@dummy'));
|
|
11843
12479
|
// Compact, one-line blast radius (homegraph's value-add over a plain Read).
|
|
11844
12480
|
const depSummary = dependents.length
|
|
11845
12481
|
? `used by ${dependents.length} file${dependents.length === 1 ? '' : 's'}: ${dependents.slice(0, 8).join(', ')}${dependents.length > 8 ? `, +${dependents.length - 8} more` : ''}`
|
|
@@ -12019,6 +12655,8 @@ class ToolHandler {
|
|
|
12019
12655
|
for (const e of edges) {
|
|
12020
12656
|
if (seen.has(e.node.id))
|
|
12021
12657
|
continue;
|
|
12658
|
+
if ((0, query_utils_1.isMcpNoiseNode)(e.node))
|
|
12659
|
+
continue;
|
|
12022
12660
|
seen.add(e.node.id);
|
|
12023
12661
|
out.push(e);
|
|
12024
12662
|
}
|
|
@@ -12194,14 +12832,47 @@ class ToolHandler {
|
|
|
12194
12832
|
return this.textResult('No modules found in the project map.');
|
|
12195
12833
|
}
|
|
12196
12834
|
const FILE_CAP = 80;
|
|
12835
|
+
const projectRoot = cg.getProjectRoot();
|
|
12836
|
+
const hasHarmony = map.modules.some((m) => m.kind === 'harmony');
|
|
12197
12837
|
const lines = [
|
|
12198
12838
|
`**Project map** (phase=${map.phase})`,
|
|
12199
12839
|
`modules: ${map.modules.length} · files: ${map.fileCount}`,
|
|
12200
|
-
'',
|
|
12201
12840
|
];
|
|
12841
|
+
// Spec 0040 — Harmony skeleton summary (navigation pointers, not call edges).
|
|
12842
|
+
try {
|
|
12843
|
+
const bundle = (0, project_map_1.readHarmonyAppBundleName)(projectRoot);
|
|
12844
|
+
if (bundle)
|
|
12845
|
+
lines.push(`bundle: \`${bundle}\` (from app.json5)`);
|
|
12846
|
+
}
|
|
12847
|
+
catch {
|
|
12848
|
+
/* omit */
|
|
12849
|
+
}
|
|
12850
|
+
if (hasHarmony) {
|
|
12851
|
+
lines.push('modules from `build-profile.json5` — skeleton map only (not call edges; use `homegraph_explore` for route_map → page)');
|
|
12852
|
+
}
|
|
12853
|
+
lines.push('');
|
|
12202
12854
|
for (const m of map.modules) {
|
|
12203
12855
|
const rootLabel = m.rootPath || '.';
|
|
12204
12856
|
lines.push(`### ${m.name} (\`${rootLabel}\`) · ${m.kind} · ${m.fileCount} files`);
|
|
12857
|
+
if (m.kind === 'harmony' || m.rootPath) {
|
|
12858
|
+
try {
|
|
12859
|
+
const profiles = (0, project_map_1.listHarmonyRouteProfilesUnderModule)(projectRoot, m.rootPath || '');
|
|
12860
|
+
for (const p of profiles) {
|
|
12861
|
+
lines.push(`- route profile: \`${p}\``);
|
|
12862
|
+
}
|
|
12863
|
+
}
|
|
12864
|
+
catch {
|
|
12865
|
+
/* omit */
|
|
12866
|
+
}
|
|
12867
|
+
try {
|
|
12868
|
+
const ohpm = (0, project_map_1.readModuleOhPackageName)(projectRoot, m.rootPath || '');
|
|
12869
|
+
if (ohpm)
|
|
12870
|
+
lines.push(`- oh-package: \`${ohpm}\``);
|
|
12871
|
+
}
|
|
12872
|
+
catch {
|
|
12873
|
+
/* omit */
|
|
12874
|
+
}
|
|
12875
|
+
}
|
|
12205
12876
|
if (m.files && m.files.length > 0) {
|
|
12206
12877
|
const shown = m.files.slice(0, FILE_CAP);
|
|
12207
12878
|
for (const f of shown) {
|
|
@@ -12213,6 +12884,42 @@ class ToolHandler {
|
|
|
12213
12884
|
}
|
|
12214
12885
|
lines.push('');
|
|
12215
12886
|
}
|
|
12887
|
+
// Spec 0048 §4 — module roster with local file: deps.
|
|
12888
|
+
try {
|
|
12889
|
+
const roster = (0, project_map_1.formatHarmonyModuleRoster)(projectRoot, map.modules);
|
|
12890
|
+
if (roster) {
|
|
12891
|
+
lines.push(roster);
|
|
12892
|
+
lines.push('');
|
|
12893
|
+
}
|
|
12894
|
+
}
|
|
12895
|
+
catch {
|
|
12896
|
+
/* omit roster on failure */
|
|
12897
|
+
}
|
|
12898
|
+
// Spec 0042 B — path inventory (string/rawfile/media + unindexed package dirs).
|
|
12899
|
+
// Skip when the caller filtered to a single non-Harmony module? Still useful
|
|
12900
|
+
// project-wide; keep always on Harmony-shaped trees or whenever scan finds hits.
|
|
12901
|
+
try {
|
|
12902
|
+
const indexedPaths = map.modules.flatMap((m) => (m.files ?? []).map((f) => f.path));
|
|
12903
|
+
// When includeFiles=false, files arrays are empty — fall back to full map paths.
|
|
12904
|
+
let pathsForUnindexed = indexedPaths;
|
|
12905
|
+
if (pathsForUnindexed.length === 0) {
|
|
12906
|
+
try {
|
|
12907
|
+
const full = cg.getProjectMap({ includeFiles: true });
|
|
12908
|
+
pathsForUnindexed = full.modules.flatMap((m) => (m.files ?? []).map((f) => f.path));
|
|
12909
|
+
}
|
|
12910
|
+
catch {
|
|
12911
|
+
pathsForUnindexed = [];
|
|
12912
|
+
}
|
|
12913
|
+
}
|
|
12914
|
+
const inv = (0, project_map_1.scanHarmonyResourceInventory)(projectRoot, { indexedPaths: pathsForUnindexed });
|
|
12915
|
+
const section = (0, project_map_1.formatHarmonyResourceInventory)(inv);
|
|
12916
|
+
if (section) {
|
|
12917
|
+
lines.push(section);
|
|
12918
|
+
}
|
|
12919
|
+
}
|
|
12920
|
+
catch {
|
|
12921
|
+
/* omit inventory on scan failure */
|
|
12922
|
+
}
|
|
12216
12923
|
return this.textResult(lines.join('\n').trimEnd());
|
|
12217
12924
|
}
|
|
12218
12925
|
/**
|