homegraph 1.5.8 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/dist/extraction/grammars.d.ts +19 -0
- package/dist/extraction/grammars.js +44 -1
- package/dist/extraction/index.js +17 -12
- package/dist/extraction/languages/arkts.d.ts +40 -7
- package/dist/extraction/languages/arkts.js +290 -84
- package/dist/extraction/tree-sitter.js +12 -3
- package/dist/index.js +23 -12
- package/dist/mcp/arkts-evidence-packs.js +1 -0
- 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/index-availability.d.ts +40 -10
- package/dist/mcp/index-availability.js +89 -23
- 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/server-instructions.d.ts +2 -6
- package/dist/mcp/server-instructions.js +15 -5
- package/dist/mcp/session.js +15 -0
- package/dist/mcp/tools.d.ts +52 -0
- package/dist/mcp/tools.js +750 -103
- package/dist/project-map/index.d.ts +45 -0
- package/dist/project-map/index.js +373 -45
- 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/query-plan-provider.js +3 -2
- package/dist/search/query-plan.js +21 -19
- package/dist/search/query-utils.d.ts +13 -0
- package/dist/search/query-utils.js +64 -0
- package/package.json +2 -2
package/dist/mcp/tools.js
CHANGED
|
@@ -12,6 +12,11 @@ exports.getExploreBudget = getExploreBudget;
|
|
|
12
12
|
exports.getExploreOutputBudget = getExploreOutputBudget;
|
|
13
13
|
exports.tightenExploreBudgetForQuery = tightenExploreBudgetForQuery;
|
|
14
14
|
exports.exploreLocatorDigestEnabled = exploreLocatorDigestEnabled;
|
|
15
|
+
exports.formatHarmonyRegistrationSources = formatHarmonyRegistrationSources;
|
|
16
|
+
exports.formatHarmonyResourceHits = formatHarmonyResourceHits;
|
|
17
|
+
exports.formatHarmonyCapabilityProfiles = formatHarmonyCapabilityProfiles;
|
|
18
|
+
exports.isHarmonyStubBody = isHarmonyStubBody;
|
|
19
|
+
exports.formatHarmonySeamNotes = formatHarmonySeamNotes;
|
|
15
20
|
exports.formatStaleBanner = formatStaleBanner;
|
|
16
21
|
exports.formatStaleFooter = formatStaleFooter;
|
|
17
22
|
exports.formatDegradedBanner = formatDegradedBanner;
|
|
@@ -26,9 +31,11 @@ const evidence_rendering_1 = require("./evidence-rendering");
|
|
|
26
31
|
const arkts_evidence_packs_1 = require("./arkts-evidence-packs");
|
|
27
32
|
const evidence_paths_1 = require("../graph/evidence-paths");
|
|
28
33
|
const query_plan_1 = require("../search/query-plan");
|
|
29
|
-
const
|
|
34
|
+
const grammars_1 = require("../extraction/grammars");
|
|
35
|
+
const project_map_1 = require("../project-map");
|
|
30
36
|
const index_availability_1 = require("./index-availability");
|
|
31
37
|
const directory_1 = require("../directory");
|
|
38
|
+
const runtime_log_1 = require("../runtime-log");
|
|
32
39
|
// Lazy-load the heavy HomeGraph chain off the MCP startup path — see the same
|
|
33
40
|
// helper in engine.ts. ToolHandler must load to answer tools/list (static
|
|
34
41
|
// schemas), but it must NOT drag in sqlite/query layers before the daemon binds;
|
|
@@ -56,6 +63,7 @@ const arkts_1 = require("../extraction/languages/arkts");
|
|
|
56
63
|
const explore_session_state_1 = require("./explore-session-state");
|
|
57
64
|
const explore_dedup_1 = require("./explore-dedup");
|
|
58
65
|
const explore_repeat_guard_1 = require("./explore-repeat-guard");
|
|
66
|
+
const locate_contract_1 = require("./locate-contract");
|
|
59
67
|
const dynamic_boundaries_1 = require("./dynamic-boundaries");
|
|
60
68
|
const query_cache_1 = require("./query-cache");
|
|
61
69
|
const diff_impact_1 = require("./diff-impact");
|
|
@@ -566,6 +574,258 @@ function fileSectionHeader(filePath, suffix) {
|
|
|
566
574
|
? `${FILE_SECTION_PREFIX}${filePath}\`** — ${suffix}`
|
|
567
575
|
: `${FILE_SECTION_PREFIX}${filePath}\`**`;
|
|
568
576
|
}
|
|
577
|
+
/**
|
|
578
|
+
* Spec 0039 — short Registration sources table from indexed Harmony profile routes.
|
|
579
|
+
* Returns null when nothing to show.
|
|
580
|
+
*/
|
|
581
|
+
function formatHarmonyRegistrationSources(cg, maxRows = 24) {
|
|
582
|
+
const routes = cg
|
|
583
|
+
.getNodesByKind('route')
|
|
584
|
+
.filter((n) => (0, grammars_1.isHarmonyRouteProfileJson)(n.filePath))
|
|
585
|
+
.sort((a, b) => a.filePath.localeCompare(b.filePath) || a.startLine - b.startLine);
|
|
586
|
+
if (routes.length === 0)
|
|
587
|
+
return null;
|
|
588
|
+
const byFile = new Map();
|
|
589
|
+
for (const r of routes) {
|
|
590
|
+
const fp = r.filePath.replace(/\\/g, '/');
|
|
591
|
+
const list = byFile.get(fp) ?? [];
|
|
592
|
+
list.push(r);
|
|
593
|
+
byFile.set(fp, list);
|
|
594
|
+
}
|
|
595
|
+
const lines = [
|
|
596
|
+
'**Registration sources** (indexed Harmony route profiles — evidence from these files; do not re-Read unless editing)',
|
|
597
|
+
];
|
|
598
|
+
let rows = 0;
|
|
599
|
+
for (const [fp, list] of byFile) {
|
|
600
|
+
lines.push(`- \`${fp}\``);
|
|
601
|
+
for (const r of list) {
|
|
602
|
+
if (rows >= maxRows) {
|
|
603
|
+
lines.push(` - … +more routes`);
|
|
604
|
+
return lines.join('\n');
|
|
605
|
+
}
|
|
606
|
+
const page = typeof r.signature === 'string'
|
|
607
|
+
? /pageSourceFile=([^;]+)/.exec(r.signature)?.[1]?.trim()
|
|
608
|
+
: undefined;
|
|
609
|
+
const builder = typeof r.signature === 'string'
|
|
610
|
+
? /buildFunction=([^;]+)/.exec(r.signature)?.[1]?.trim()
|
|
611
|
+
: undefined;
|
|
612
|
+
if (page) {
|
|
613
|
+
lines.push(` - \`${r.name}\` → \`${page}\`${builder ? ` (${builder})` : ''} @${fp}:${r.startLine}`);
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
lines.push(` - \`${r.name}\` @${fp}:${r.startLine}`);
|
|
617
|
+
}
|
|
618
|
+
rows++;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return lines.join('\n');
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Spec 0041 / 0048 §3 — short Resource hits table for element/string.json constants.
|
|
625
|
+
* Spec 0048: append up to 2 in-repo `.ets` binding anchors when signature/docstring
|
|
626
|
+
* mention `app.string.<key>`.
|
|
627
|
+
*/
|
|
628
|
+
function formatHarmonyResourceHits(cg, query, maxRows = 8) {
|
|
629
|
+
const constants = cg
|
|
630
|
+
.getNodesByKind('constant')
|
|
631
|
+
.filter((n) => (0, grammars_1.isHarmonyElementStringJson)(n.filePath));
|
|
632
|
+
if (constants.length === 0)
|
|
633
|
+
return null;
|
|
634
|
+
const q = query.trim();
|
|
635
|
+
const namedFile = /string\.json/i.test(q);
|
|
636
|
+
const tokens = [
|
|
637
|
+
...new Set(q
|
|
638
|
+
.split(/[\s,;|]+/)
|
|
639
|
+
.map((t) => t.trim())
|
|
640
|
+
.filter((t) => t.length >= 2 && t.length <= 160 && !/^string\.json$/i.test(t))),
|
|
641
|
+
].slice(0, 12);
|
|
642
|
+
const matched = constants.filter((n) => {
|
|
643
|
+
if (namedFile && tokens.length === 0)
|
|
644
|
+
return true;
|
|
645
|
+
const hay = `${n.name}\n${n.qualifiedName ?? ''}\n${n.docstring ?? ''}\n${n.signature ?? ''}`.toLocaleLowerCase();
|
|
646
|
+
return tokens.some((t) => hay.includes(t.toLocaleLowerCase()));
|
|
647
|
+
});
|
|
648
|
+
const rows = (matched.length > 0 ? matched : namedFile ? constants : [])
|
|
649
|
+
.sort((a, b) => a.filePath.localeCompare(b.filePath) || a.startLine - b.startLine)
|
|
650
|
+
.slice(0, maxRows);
|
|
651
|
+
if (rows.length === 0)
|
|
652
|
+
return null;
|
|
653
|
+
const projectRoot = cg.getProjectRoot();
|
|
654
|
+
const etsFiles = cg.getFiles()
|
|
655
|
+
.map((f) => f.path.replace(/\\/g, '/'))
|
|
656
|
+
.filter((p) => /\.ets$/i.test(p))
|
|
657
|
+
.slice(0, 80);
|
|
658
|
+
const findBindings = (key, stringFile) => {
|
|
659
|
+
const needle = `app.string.${key}`;
|
|
660
|
+
const hits = [];
|
|
661
|
+
const modRoot = (() => {
|
|
662
|
+
const marker = '/src/main/';
|
|
663
|
+
const i = stringFile.indexOf(marker);
|
|
664
|
+
return i >= 0 ? stringFile.slice(0, i) : '';
|
|
665
|
+
})();
|
|
666
|
+
for (const rel of etsFiles) {
|
|
667
|
+
if (hits.length >= 2)
|
|
668
|
+
break;
|
|
669
|
+
if (modRoot && !(rel === modRoot || rel.startsWith(`${modRoot}/`)))
|
|
670
|
+
continue;
|
|
671
|
+
const abs = (0, path_1.join)(projectRoot, rel);
|
|
672
|
+
if (!(0, fs_1.existsSync)(abs))
|
|
673
|
+
continue;
|
|
674
|
+
let text;
|
|
675
|
+
try {
|
|
676
|
+
text = (0, fs_1.readFileSync)(abs, 'utf-8');
|
|
677
|
+
}
|
|
678
|
+
catch {
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
const lines = text.split(/\n/);
|
|
682
|
+
for (let i = 0; i < lines.length; i++) {
|
|
683
|
+
if (hits.length >= 2)
|
|
684
|
+
break;
|
|
685
|
+
if (lines[i].includes(needle)) {
|
|
686
|
+
hits.push(`\`${rel}:${i + 1}\``);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return hits;
|
|
691
|
+
};
|
|
692
|
+
const lines = [
|
|
693
|
+
'**Resource hits** (element/string.json — searchable literals, no graph edges)',
|
|
694
|
+
];
|
|
695
|
+
for (const n of rows) {
|
|
696
|
+
const fp = n.filePath.replace(/\\/g, '/');
|
|
697
|
+
const value = (n.docstring ?? '').slice(0, 80);
|
|
698
|
+
const shown = value ? JSON.stringify(value) : '(empty)';
|
|
699
|
+
const bounds = findBindings(n.name, fp);
|
|
700
|
+
const boundNote = bounds.length ? ` · bound ${bounds.join(', ')}` : '';
|
|
701
|
+
lines.push(`- \`${fp}:${n.startLine}\` — ${shown} → \`${n.name}\`${boundNote} · Grep \`$r('app.string.${n.name}')\` in \`.ets\``);
|
|
702
|
+
}
|
|
703
|
+
if ((matched.length || constants.length) > rows.length) {
|
|
704
|
+
lines.push(`- … +more string resources`);
|
|
705
|
+
}
|
|
706
|
+
return lines.join('\n');
|
|
707
|
+
}
|
|
708
|
+
const FORM_QUERY_RE = /form_config|FormExtension|服务卡片|卡片/i;
|
|
709
|
+
const SHORTCUT_QUERY_RE = /shortcuts_config|\bshortcuts\b|快捷方式|长按|快捷入口/i;
|
|
710
|
+
/**
|
|
711
|
+
* Spec 0048 §1–2 — Capability profiles table, or form negative evidence.
|
|
712
|
+
*/
|
|
713
|
+
function formatHarmonyCapabilityProfiles(cg, query, maxRows = 8) {
|
|
714
|
+
const wantForm = FORM_QUERY_RE.test(query);
|
|
715
|
+
const wantShortcut = SHORTCUT_QUERY_RE.test(query);
|
|
716
|
+
if (!wantForm && !wantShortcut)
|
|
717
|
+
return null;
|
|
718
|
+
const constants = cg.getNodesByKind('constant');
|
|
719
|
+
const routes = cg.getNodesByKind('route');
|
|
720
|
+
const formNodes = [
|
|
721
|
+
...constants.filter((n) => (0, grammars_1.isHarmonyCapabilityProfileJson)(n.filePath)
|
|
722
|
+
&& /form_config\.json$/i.test(n.filePath.replace(/\\/g, '/'))),
|
|
723
|
+
...constants.filter((n) => (n.qualifiedName ?? '').includes('harmony.capability.form')),
|
|
724
|
+
...routes.filter((n) => (n.name ?? '').startsWith('formAbility:')),
|
|
725
|
+
];
|
|
726
|
+
const shortcutNodes = [
|
|
727
|
+
...constants.filter((n) => (0, grammars_1.isHarmonyCapabilityProfileJson)(n.filePath)
|
|
728
|
+
&& /shortcuts_config\.json$/i.test(n.filePath.replace(/\\/g, '/'))),
|
|
729
|
+
...constants.filter((n) => (n.qualifiedName ?? '').includes('harmony.capability.shortcut')
|
|
730
|
+
|| (n.qualifiedName ?? '').includes('harmony.shortcut.')),
|
|
731
|
+
];
|
|
732
|
+
const lines = [];
|
|
733
|
+
if (wantForm && formNodes.length === 0) {
|
|
734
|
+
lines.push('**Capability profiles:** No in-repo form_config / FormExtensionAbility (do not treat SDK `.d.ts` as project wiring).');
|
|
735
|
+
}
|
|
736
|
+
const rows = [];
|
|
737
|
+
const pushRows = (kind, nodes) => {
|
|
738
|
+
for (const n of nodes.sort((a, b) => a.filePath.localeCompare(b.filePath) || a.startLine - b.startLine)) {
|
|
739
|
+
if (rows.length >= maxRows)
|
|
740
|
+
break;
|
|
741
|
+
const fp = n.filePath.replace(/\\/g, '/');
|
|
742
|
+
rows.push(`- (${kind}) \`${n.name}\` @\`${fp}:${n.startLine}\`${n.signature ? ` — ${n.signature}` : ''}`);
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
if (wantForm)
|
|
746
|
+
pushRows('form', formNodes);
|
|
747
|
+
if (wantShortcut)
|
|
748
|
+
pushRows('shortcut', shortcutNodes);
|
|
749
|
+
if (rows.length > 0) {
|
|
750
|
+
lines.push('**Capability profiles** (form_config / shortcuts_config — paths only, no UI edges)');
|
|
751
|
+
lines.push(...rows);
|
|
752
|
+
}
|
|
753
|
+
return lines.length ? lines.join('\n') : null;
|
|
754
|
+
}
|
|
755
|
+
/** Spec 0048 §5 — detect empty / log-only method bodies. */
|
|
756
|
+
function isHarmonyStubBody(source) {
|
|
757
|
+
const body = source
|
|
758
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
759
|
+
.replace(/\/\/[^\n]*/g, '')
|
|
760
|
+
.trim();
|
|
761
|
+
if (!body)
|
|
762
|
+
return true;
|
|
763
|
+
const lines = body.split(/\n/).map((l) => l.trim()).filter(Boolean);
|
|
764
|
+
if (lines.length === 0)
|
|
765
|
+
return true;
|
|
766
|
+
if (lines.length > 8)
|
|
767
|
+
return false;
|
|
768
|
+
const code = lines.join('\n');
|
|
769
|
+
// Strip common wrapper braces from a method slice.
|
|
770
|
+
const inner = code.replace(/^[^{]*\{/, '').replace(/\}[^}]*$/, '').trim();
|
|
771
|
+
const stmts = inner.split(/;|\n/).map((s) => s.trim()).filter(Boolean);
|
|
772
|
+
if (stmts.length === 0)
|
|
773
|
+
return true;
|
|
774
|
+
return stmts.every((s) => /^(return(\s+[^;]*)?|hilog\.[a-zA-Z]+\s*\(|console\.(log|info|warn|error|debug)\s*\(|\/\/)/i.test(s)
|
|
775
|
+
|| s === '{'
|
|
776
|
+
|| s === '}');
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Spec 0048 §5 — seam notes for located explore anchors.
|
|
780
|
+
*/
|
|
781
|
+
function formatHarmonySeamNotes(cg, projectRoot, located, maxAnchors = 6) {
|
|
782
|
+
if (!located.length)
|
|
783
|
+
return null;
|
|
784
|
+
const lines = [];
|
|
785
|
+
const capabilityFiles = new Set([
|
|
786
|
+
...cg.getNodesByKind('constant').filter((n) => (0, grammars_1.isHarmonyCapabilityProfileJson)(n.filePath)),
|
|
787
|
+
...cg.getNodesByKind('route').filter((n) => (0, grammars_1.isHarmonyRouteProfileJson)(n.filePath)),
|
|
788
|
+
].map((n) => n.filePath.replace(/\\/g, '/')));
|
|
789
|
+
for (const loc of located.slice(0, maxAnchors)) {
|
|
790
|
+
const fp = loc.filePath.replace(/\\/g, '/');
|
|
791
|
+
const modRoot = (() => {
|
|
792
|
+
const marker = '/src/main/';
|
|
793
|
+
const i = fp.indexOf(marker);
|
|
794
|
+
return i >= 0 ? fp.slice(0, i) : fp.includes('/') ? fp.slice(0, fp.lastIndexOf('/')) : '';
|
|
795
|
+
})();
|
|
796
|
+
let profileNote = 0;
|
|
797
|
+
for (const cap of capabilityFiles) {
|
|
798
|
+
if (profileNote >= 2)
|
|
799
|
+
break;
|
|
800
|
+
if (modRoot && (cap === modRoot || cap.startsWith(`${modRoot}/`))) {
|
|
801
|
+
lines.push(`- profile near \`${loc.name}\`: \`${cap}\``);
|
|
802
|
+
profileNote++;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
try {
|
|
806
|
+
const node = loc.id ? cg.getNode(loc.id) : null;
|
|
807
|
+
const start = node?.startLine ?? loc.startLine;
|
|
808
|
+
const end = node?.endLine ?? start + 12;
|
|
809
|
+
const abs = (0, path_1.join)(projectRoot, fp);
|
|
810
|
+
if ((0, fs_1.existsSync)(abs)) {
|
|
811
|
+
const content = (0, fs_1.readFileSync)(abs, 'utf-8');
|
|
812
|
+
const allLines = content.split(/\n/);
|
|
813
|
+
const slice = allLines.slice(Math.max(0, start - 1), Math.min(allLines.length, end)).join('\n');
|
|
814
|
+
if (isHarmonyStubBody(slice)) {
|
|
815
|
+
lines.push(`- stub: \`${loc.name}\` @\`${fp}:${start}\``);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
catch {
|
|
820
|
+
/* omit */
|
|
821
|
+
}
|
|
822
|
+
if (lines.length >= maxAnchors * 3)
|
|
823
|
+
break;
|
|
824
|
+
}
|
|
825
|
+
if (lines.length === 0)
|
|
826
|
+
return null;
|
|
827
|
+
return ['**Seam notes** (config paths / empty stubs — digests still count as Read)', ...lines.slice(0, 18)].join('\n');
|
|
828
|
+
}
|
|
569
829
|
/**
|
|
570
830
|
* Per-file staleness banner emitted at the top of a tool response when the
|
|
571
831
|
* file watcher has pending events for files referenced by the response.
|
|
@@ -922,11 +1182,12 @@ exports.tools = [
|
|
|
922
1182
|
{
|
|
923
1183
|
name: 'homegraph_explore',
|
|
924
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). ' +
|
|
925
1186
|
'Use ordinary bash/search/read for paths, symbols, literal strings and local changes; continue editing when that evidence suffices. ' +
|
|
926
1187
|
'Do not call for routine pre-edit orientation or merely because implementation is difficult. ' +
|
|
927
1188
|
'For a missing usage, dependency/cycle or native-registration relation, use ' +
|
|
928
1189
|
'homegraph_usages, homegraph_modules, or homegraph_native instead. ' +
|
|
929
|
-
'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. ' +
|
|
1190
|
+
'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. ' +
|
|
930
1191
|
'Reuse unchanged complete ranges; refresh missing, edited or truncated evidence. ' +
|
|
931
1192
|
'No new evidence → change to a targeted source inspection, not a paraphrased explore. ' +
|
|
932
1193
|
'Partial/busy → at most one focused recovery for the named gap; budgets are ceilings, not required calls. ' +
|
|
@@ -968,9 +1229,12 @@ exports.tools = [
|
|
|
968
1229
|
},
|
|
969
1230
|
{
|
|
970
1231
|
name: 'homegraph_project',
|
|
971
|
-
description: 'Shallow
|
|
972
|
-
'
|
|
973
|
-
'
|
|
1232
|
+
description: 'Shallow engineering map: modules + per-module files. On Harmony repos also prints skeleton pointers ' +
|
|
1233
|
+
'(bundleName from app.json5, modules from build-profile.json5, per-module route_map/router_map/main_pages paths, oh-package name) ' +
|
|
1234
|
+
'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). ' +
|
|
1235
|
+
'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). ' +
|
|
1236
|
+
'PRIMARY overview while the full index is still building; also useful after full index. ' +
|
|
1237
|
+
'Optional `module` filters by name/path; `includeFiles` defaults true (resources section still prints when false).',
|
|
974
1238
|
inputSchema: {
|
|
975
1239
|
type: 'object',
|
|
976
1240
|
properties: {
|
|
@@ -1327,6 +1591,10 @@ class ToolHandler {
|
|
|
1327
1591
|
// The directory the server last searched for a default project. Surfaced in
|
|
1328
1592
|
// the "not initialized" error so users can see why detection missed.
|
|
1329
1593
|
defaultProjectHint = null;
|
|
1594
|
+
// Spec 0043: when a tool `projectPath` would open a different index root than
|
|
1595
|
+
// the session's default bound project, we soft-pin to the default and stash
|
|
1596
|
+
// an English notice to prepend on the success reply (consumed once).
|
|
1597
|
+
boundProjectPathPinNotice = null;
|
|
1330
1598
|
// Per-start-path cache of the git worktree/index mismatch (issue #155). The
|
|
1331
1599
|
// mismatch is a fixed property of (where the request came from → which
|
|
1332
1600
|
// .homegraph/ it resolves to), so the up-to-two `git rev-parse` spawns run
|
|
@@ -1560,12 +1828,11 @@ class ToolHandler {
|
|
|
1560
1828
|
if (state === 'empty') {
|
|
1561
1829
|
return (0, index_availability_1.productIndexGuidance)('empty');
|
|
1562
1830
|
}
|
|
1563
|
-
if (state === 'fast'
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
: "HomeGraph is still building this project's index. A call right now returns build-progress guidance; use `homegraph_project` for the module/file map, then retry once indexing finishes.";
|
|
1831
|
+
if (state === 'fast') {
|
|
1832
|
+
return (0, index_availability_1.productIndexGuidance)('fast');
|
|
1833
|
+
}
|
|
1834
|
+
if (state === 'syncing') {
|
|
1835
|
+
return (0, index_availability_1.productIndexGuidance)('syncing');
|
|
1569
1836
|
}
|
|
1570
1837
|
}
|
|
1571
1838
|
catch {
|
|
@@ -1582,6 +1849,11 @@ class ToolHandler {
|
|
|
1582
1849
|
*
|
|
1583
1850
|
* Walks up parent directories to find the nearest .homegraph/ folder,
|
|
1584
1851
|
* similar to how git finds .git/ directories.
|
|
1852
|
+
*
|
|
1853
|
+
* Spec 0043: when a default project is already bound, a `projectPath` whose
|
|
1854
|
+
* resolved index root differs from that bound root is soft-pinned back to
|
|
1855
|
+
* the default (no DB switch) and a success-shaped English notice is stashed
|
|
1856
|
+
* for the reply preamble.
|
|
1585
1857
|
*/
|
|
1586
1858
|
getHomeGraph(projectPath) {
|
|
1587
1859
|
const sourcesMode = (0, graph_sources_1.resolveGraphSources)();
|
|
@@ -1628,25 +1900,34 @@ class ToolHandler {
|
|
|
1628
1900
|
// (#926). The DB connection itself is still cached (by resolved root,
|
|
1629
1901
|
// below), so re-resolving costs only the stat walk, never a reopen.
|
|
1630
1902
|
const resolvedRoot = (0, directory_1.findNearestHomeGraphRoot)(projectPath);
|
|
1903
|
+
// Spec 0043: with a bound default root, never switch the graph to a
|
|
1904
|
+
// different index (sibling / parent / wrong absolute path). Soft-pin and
|
|
1905
|
+
// stash a notice; security refusals above still win.
|
|
1906
|
+
if (this.cg) {
|
|
1907
|
+
const boundRoot = (0, path_1.resolve)(this.cg.getProjectRoot());
|
|
1908
|
+
const resolvedAbs = resolvedRoot ? (0, path_1.resolve)(resolvedRoot) : null;
|
|
1909
|
+
if (resolvedAbs === null || resolvedAbs !== boundRoot) {
|
|
1910
|
+
this.boundProjectPathPinNotice = (0, index_availability_1.formatBoundProjectPathPinNotice)({
|
|
1911
|
+
boundRoot,
|
|
1912
|
+
requestedPath: projectPath,
|
|
1913
|
+
resolvedRoot: resolvedAbs,
|
|
1914
|
+
});
|
|
1915
|
+
return this.freshen(this.cg);
|
|
1916
|
+
}
|
|
1917
|
+
// Same index root (incl. nested path under the bound project) — reuse the
|
|
1918
|
+
// default instance so we never open a second connection to the same DB
|
|
1919
|
+
// (#238).
|
|
1920
|
+
return this.freshen(this.cg);
|
|
1921
|
+
}
|
|
1631
1922
|
if (!resolvedRoot) {
|
|
1632
1923
|
throw new NotIndexedError(`The project at ${projectPath} isn't indexed with homegraph (no .homegraph/ directory found ` +
|
|
1633
1924
|
'walking up from it), so homegraph cannot query it. Use your built-in tools (Read/Grep/Glob) ' +
|
|
1634
1925
|
"for that codebase instead, and don't call homegraph for it again this session. " +
|
|
1635
1926
|
"Indexing is the user's decision — they can run 'homegraph init' in that project to enable it.");
|
|
1636
1927
|
}
|
|
1637
|
-
//
|
|
1638
|
-
//
|
|
1639
|
-
//
|
|
1640
|
-
// writes; when WAL isn't in effect (e.g. a filesystem without shared-memory
|
|
1641
|
-
// support) that surfaces as intermittent
|
|
1642
|
-
// "database is locked" on concurrent tool calls. See issue #238. The
|
|
1643
|
-
// default instance is owned/closed by the server, so it's never cached.
|
|
1644
|
-
if (this.cg && this.cg.getProjectRoot() === resolvedRoot) {
|
|
1645
|
-
return this.freshen(this.cg);
|
|
1646
|
-
}
|
|
1647
|
-
// Cache the open DB connection by RESOLVED ROOT only — never by the input
|
|
1648
|
-
// path. One key per instance means closeAll() closes each exactly once, and
|
|
1649
|
-
// a changed resolution maps to a different entry instead of a stale hit.
|
|
1928
|
+
// No default project — cross-project open (cached by resolved root).
|
|
1929
|
+
// One key per instance means closeAll() closes each exactly once, and a
|
|
1930
|
+
// changed resolution maps to a different entry instead of a stale hit.
|
|
1650
1931
|
const cached = this.projectCache.get(resolvedRoot);
|
|
1651
1932
|
if (cached)
|
|
1652
1933
|
return this.freshen(cached);
|
|
@@ -1891,58 +2172,98 @@ class ToolHandler {
|
|
|
1891
2172
|
catch {
|
|
1892
2173
|
reason = null;
|
|
1893
2174
|
}
|
|
2175
|
+
try {
|
|
2176
|
+
(0, runtime_log_1.logLifecycle)('watcher.degraded', {
|
|
2177
|
+
projectRoot: cg.getProjectRoot(),
|
|
2178
|
+
reason: reason ?? undefined,
|
|
2179
|
+
});
|
|
2180
|
+
}
|
|
2181
|
+
catch {
|
|
2182
|
+
/* ignore */
|
|
2183
|
+
}
|
|
1894
2184
|
const composed = `${formatDegradedBanner(reason)}\n\n${head.text}`;
|
|
1895
2185
|
return { ...result, content: [{ type: 'text', text: composed }, ...tail] };
|
|
1896
2186
|
}
|
|
1897
|
-
//
|
|
1898
|
-
//
|
|
1899
|
-
|
|
2187
|
+
// Spec 0035: pending/dirty is a one-line status footer (withProductStatusFooter),
|
|
2188
|
+
// not the long ⚠️ stale banner. Keep formatStaleBanner helpers for tests /
|
|
2189
|
+
// status tool; do not prepend them on every read tool.
|
|
2190
|
+
return result;
|
|
2191
|
+
}
|
|
2192
|
+
/**
|
|
2193
|
+
* Decorate successful tool text (Spec 0035 status footer + Spec 0038 project-root hint
|
|
2194
|
+
* + Spec 0043 bound projectPath pin notice).
|
|
2195
|
+
* Prepends absolute project root + join guidance when known; appends status when missing.
|
|
2196
|
+
*/
|
|
2197
|
+
withProductStatusFooter(result, projectPath) {
|
|
2198
|
+
if (result.isError)
|
|
2199
|
+
return result;
|
|
2200
|
+
const [first, ...rest] = result.content;
|
|
2201
|
+
if (!first || first.type !== 'text')
|
|
2202
|
+
return result;
|
|
2203
|
+
let cg;
|
|
1900
2204
|
try {
|
|
1901
|
-
|
|
2205
|
+
cg = this.getHomeGraph(projectPath);
|
|
1902
2206
|
}
|
|
1903
2207
|
catch {
|
|
1904
2208
|
return result;
|
|
1905
2209
|
}
|
|
1906
|
-
if (
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
// Substring match against the project-relative POSIX path — that's
|
|
1916
|
-
// exactly the format both the watcher and every homegraph response
|
|
1917
|
-
// emit, so a plain includes() is sufficient and avoids regex pitfalls.
|
|
1918
|
-
if (text.includes(p.path))
|
|
1919
|
-
inResponse.push(p);
|
|
1920
|
-
else
|
|
1921
|
-
elsewhere.push(p);
|
|
2210
|
+
if (this.cg && cg !== this.cg) {
|
|
2211
|
+
try {
|
|
2212
|
+
const sameProject = (0, path_1.resolve)(this.cg.getProjectRoot()) === (0, path_1.resolve)(cg.getProjectRoot());
|
|
2213
|
+
if (sameProject)
|
|
2214
|
+
cg = this.cg;
|
|
2215
|
+
}
|
|
2216
|
+
catch {
|
|
2217
|
+
/* leave cg */
|
|
2218
|
+
}
|
|
1922
2219
|
}
|
|
1923
|
-
let
|
|
1924
|
-
|
|
1925
|
-
|
|
2220
|
+
let text = first.text;
|
|
2221
|
+
// Spec 0038: absolute project root + how to join repo-relative paths (idempotent).
|
|
2222
|
+
if (!(0, index_availability_1.textAlreadyHasProjectRootHint)(text)) {
|
|
1926
2223
|
try {
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
2224
|
+
const absRoot = (0, path_1.resolve)(cg.getProjectRoot());
|
|
2225
|
+
if (absRoot) {
|
|
2226
|
+
text = `${(0, index_availability_1.formatProjectRootPathHint)(absRoot)}\n\n${text}`;
|
|
2227
|
+
}
|
|
1930
2228
|
}
|
|
1931
2229
|
catch {
|
|
1932
|
-
|
|
2230
|
+
/* no root — skip hint */
|
|
1933
2231
|
}
|
|
1934
|
-
banner = formatStaleBanner(inResponse, {
|
|
1935
|
-
catchUpDeferred: (0, memory_budget_1.shouldSkipCatchUpSync)(dbPath),
|
|
1936
|
-
});
|
|
1937
2232
|
}
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
2233
|
+
// Spec 0043: bound-root soft-pin notice at the very top (consume once).
|
|
2234
|
+
const pinNotice = this.boundProjectPathPinNotice;
|
|
2235
|
+
this.boundProjectPathPinNotice = null;
|
|
2236
|
+
if (pinNotice && !(0, index_availability_1.textAlreadyHasBoundProjectPathPinNotice)(text)) {
|
|
2237
|
+
text = `${pinNotice}\n\n${text}`;
|
|
1941
2238
|
}
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
2239
|
+
// Spec 0035: one-line product index status footer (skip if already present).
|
|
2240
|
+
if (!(0, index_availability_1.textAlreadyHasProductStatus)(text)) {
|
|
2241
|
+
let state;
|
|
2242
|
+
try {
|
|
2243
|
+
state = (0, index_availability_1.resolveProductIndexState)(cg);
|
|
2244
|
+
}
|
|
2245
|
+
catch {
|
|
2246
|
+
return {
|
|
2247
|
+
...result,
|
|
2248
|
+
content: [{ type: 'text', text }, ...rest],
|
|
2249
|
+
};
|
|
2250
|
+
}
|
|
2251
|
+
let pendingPaths;
|
|
2252
|
+
if (state === 'dirty') {
|
|
2253
|
+
try {
|
|
2254
|
+
pendingPaths = (cg.getPendingFiles?.() ?? []).map((p) => p.path);
|
|
2255
|
+
}
|
|
2256
|
+
catch {
|
|
2257
|
+
pendingPaths = [];
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
const line = (0, index_availability_1.formatProductStatusLine)(state, { pendingPaths });
|
|
2261
|
+
text = `${text}\n\n${line}`;
|
|
2262
|
+
}
|
|
2263
|
+
return {
|
|
2264
|
+
...result,
|
|
2265
|
+
content: [{ type: 'text', text }, ...rest],
|
|
2266
|
+
};
|
|
1946
2267
|
}
|
|
1947
2268
|
/**
|
|
1948
2269
|
* Execute a tool by name.
|
|
@@ -1952,10 +2273,47 @@ class ToolHandler {
|
|
|
1952
2273
|
*/
|
|
1953
2274
|
async execute(toolName, args, sessionState) {
|
|
1954
2275
|
const requestStartedAt = Date.now();
|
|
2276
|
+
const result = await this.executeCore(toolName, args, sessionState, requestStartedAt);
|
|
2277
|
+
this.traceToolCall(toolName, args, result, requestStartedAt);
|
|
2278
|
+
return result;
|
|
2279
|
+
}
|
|
2280
|
+
/** Spec 0046: HOMEGRAPH_DEBUG tool summary → stderr + daemon.log. */
|
|
2281
|
+
traceToolCall(toolName, args, result, requestStartedAt) {
|
|
2282
|
+
try {
|
|
2283
|
+
let projectRoot;
|
|
2284
|
+
try {
|
|
2285
|
+
projectRoot = this.getHomeGraph(args.projectPath).getProjectRoot();
|
|
2286
|
+
}
|
|
2287
|
+
catch {
|
|
2288
|
+
projectRoot = typeof args.projectPath === 'string' ? args.projectPath : undefined;
|
|
2289
|
+
}
|
|
2290
|
+
const q = typeof args.query === 'string'
|
|
2291
|
+
? args.query
|
|
2292
|
+
: typeof args.symbol === 'string'
|
|
2293
|
+
? args.symbol
|
|
2294
|
+
: undefined;
|
|
2295
|
+
const evidenceStatus = result._meta?.homegraphEvidence?.status
|
|
2296
|
+
?? result[explore_session_state_1.EXPLORE_EMISSION_KEY]?.evidenceStatus;
|
|
2297
|
+
(0, runtime_log_1.logToolDebug)(toolName, {
|
|
2298
|
+
projectRoot,
|
|
2299
|
+
durationMs: Date.now() - requestStartedAt,
|
|
2300
|
+
isError: !!result.isError,
|
|
2301
|
+
evidenceStatus,
|
|
2302
|
+
query: q,
|
|
2303
|
+
});
|
|
2304
|
+
}
|
|
2305
|
+
catch {
|
|
2306
|
+
/* logging never fails the tool */
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
async executeCore(toolName, args, sessionState, requestStartedAt) {
|
|
1955
2310
|
args = { ...args };
|
|
1956
2311
|
for (const key of [QUERY_PLAN_ARG, QUERY_DEADLINE_ARG, QUERY_STARTED_ARG, QUERY_INDEX_STATE_ARG, QUERY_FAST_ATTEMPTED_ARG, '_hgEvidenceMaxChars'])
|
|
1957
2312
|
delete args[key];
|
|
1958
2313
|
try {
|
|
2314
|
+
// Spec 0043: drop any leftover pin notice from a prior call that exited
|
|
2315
|
+
// before withProductStatusFooter (gate / defer / refuse).
|
|
2316
|
+
this.boundProjectPathPinNotice = null;
|
|
1959
2317
|
// Block the first tool call on the engine's post-open reconcile so we
|
|
1960
2318
|
// never serve rows for files deleted/edited while no MCP server was
|
|
1961
2319
|
// running. The wait is time-boxed (#905): a huge-repo reconcile takes
|
|
@@ -2069,7 +2427,7 @@ class ToolHandler {
|
|
|
2069
2427
|
&& this.areEvidenceFilesCurrent(cachedFiles ?? [], cacheCg.getProjectRoot())))) {
|
|
2070
2428
|
const diagnosed = this.withQueryPlanDiagnostics(cached, args, true);
|
|
2071
2429
|
const withWorktree = this.withWorktreeNotice(diagnosed, projectPath);
|
|
2072
|
-
return this.withStalenessNotice(withWorktree, projectPath);
|
|
2430
|
+
return this.withProductStatusFooter(this.withStalenessNotice(withWorktree, projectPath), projectPath);
|
|
2073
2431
|
}
|
|
2074
2432
|
}
|
|
2075
2433
|
catch {
|
|
@@ -2133,7 +2491,7 @@ class ToolHandler {
|
|
|
2133
2491
|
cacheIndex.setEntry(cacheQueries, cacheKey, toolName, served);
|
|
2134
2492
|
}
|
|
2135
2493
|
const withWorktree = this.withWorktreeNotice(served, projectPath);
|
|
2136
|
-
return this.withStalenessNotice(withWorktree, projectPath);
|
|
2494
|
+
return this.withProductStatusFooter(this.withStalenessNotice(withWorktree, projectPath), projectPath);
|
|
2137
2495
|
}
|
|
2138
2496
|
if (requestPlan)
|
|
2139
2497
|
args[QUERY_FAST_ATTEMPTED_ARG] = true;
|
|
@@ -2164,7 +2522,7 @@ class ToolHandler {
|
|
|
2164
2522
|
cacheIndex.setEntry(cacheQueries, cacheKey, toolName, result);
|
|
2165
2523
|
}
|
|
2166
2524
|
const withWorktree = this.withWorktreeNotice(result, projectPath);
|
|
2167
|
-
return this.withStalenessNotice(withWorktree, projectPath);
|
|
2525
|
+
return this.withProductStatusFooter(this.withStalenessNotice(withWorktree, projectPath), projectPath);
|
|
2168
2526
|
}
|
|
2169
2527
|
catch (err) {
|
|
2170
2528
|
// Expected condition, not a malfunction: answer as a SUCCESS so the
|
|
@@ -2218,15 +2576,19 @@ class ToolHandler {
|
|
|
2218
2576
|
try {
|
|
2219
2577
|
const cg = this.getHomeGraph(args.projectPath);
|
|
2220
2578
|
const root = cg.getProjectRoot();
|
|
2221
|
-
const
|
|
2222
|
-
|
|
2223
|
-
return null;
|
|
2224
|
-
const hint = typeof args.symbol === 'string'
|
|
2579
|
+
const prior = sessionState.forProject(root);
|
|
2580
|
+
const symbolHint = typeof args.symbol === 'string'
|
|
2225
2581
|
? args.symbol
|
|
2226
2582
|
: typeof args.file === 'string'
|
|
2227
2583
|
? args.file
|
|
2228
2584
|
: undefined;
|
|
2229
|
-
|
|
2585
|
+
// Spec 0044 §10.2: symbols already on the locate list bypass the Partial depth cap.
|
|
2586
|
+
if ((0, locate_contract_1.shouldExemptDepthFuseForLocatedSymbol)(prior, symbolHint))
|
|
2587
|
+
return null;
|
|
2588
|
+
const decision = (0, explore_repeat_guard_1.decideDepthToolFuse)(prior, sessionState.depthToolCount(root), toolName);
|
|
2589
|
+
if (!decision.refuse)
|
|
2590
|
+
return null;
|
|
2591
|
+
return this.textResult((0, explore_repeat_guard_1.formatDepthToolRefuse)(decision, toolName, symbolHint));
|
|
2230
2592
|
}
|
|
2231
2593
|
catch {
|
|
2232
2594
|
return null;
|
|
@@ -2298,6 +2660,10 @@ class ToolHandler {
|
|
|
2298
2660
|
const last = prior?.calls[prior.calls.length - 1];
|
|
2299
2661
|
if (!last || (0, explore_session_state_1.inferExploreEvidenceStatus)(last) === 'complete')
|
|
2300
2662
|
return;
|
|
2663
|
+
const symbolHint = typeof args.symbol === 'string' ? args.symbol : undefined;
|
|
2664
|
+
// Spec 0044 §10.2: located-symbol drills do not consume the Partial depth budget.
|
|
2665
|
+
if ((0, locate_contract_1.shouldExemptDepthFuseForLocatedSymbol)(prior, symbolHint))
|
|
2666
|
+
return;
|
|
2301
2667
|
sessionState.recordDepthTool(root);
|
|
2302
2668
|
}
|
|
2303
2669
|
catch { /* bookkeeping only */ }
|
|
@@ -2509,7 +2875,7 @@ class ToolHandler {
|
|
|
2509
2875
|
*/
|
|
2510
2876
|
maybeDeepToolPhaseGate(cg, _toolName) {
|
|
2511
2877
|
const state = (0, index_availability_1.resolveProductIndexState)(cg);
|
|
2512
|
-
if (state === 'full')
|
|
2878
|
+
if (state === 'full' || state === 'dirty')
|
|
2513
2879
|
return null;
|
|
2514
2880
|
if (state === 'syncing') {
|
|
2515
2881
|
return this.textResult((0, index_availability_1.productIndexGuidance)('syncing'));
|
|
@@ -3146,6 +3512,13 @@ class ToolHandler {
|
|
|
3146
3512
|
registeredAt,
|
|
3147
3513
|
};
|
|
3148
3514
|
}
|
|
3515
|
+
if (m?.synthesizedBy === 'arkts-route-map') {
|
|
3516
|
+
return {
|
|
3517
|
+
label: `Harmony route registration (config → page/builder)`,
|
|
3518
|
+
compact: `route-map registration${at}`,
|
|
3519
|
+
registeredAt,
|
|
3520
|
+
};
|
|
3521
|
+
}
|
|
3149
3522
|
if (m?.synthesizedBy === 'goframe-route') {
|
|
3150
3523
|
const route = m.route ? `\`${String(m.route)}\`` : 'a route';
|
|
3151
3524
|
return {
|
|
@@ -6314,7 +6687,13 @@ class ToolHandler {
|
|
|
6314
6687
|
const isTestPath = (p) => /(^|\/)(tests?|spec)\//i.test(p) || /\.(test|spec)\./i.test(p);
|
|
6315
6688
|
const fileNodes = new Map();
|
|
6316
6689
|
const seedIds = new Set();
|
|
6317
|
-
const domainPathTokens = (0,
|
|
6690
|
+
const domainPathTokens = (0, locate_contract_1.shouldSuppressSynonymExpansion)({
|
|
6691
|
+
hasExactAnchorHit: (0, query_utils_1.extractInRepoLocateAnchors)(query).length > 0
|
|
6692
|
+
|| (0, query_utils_1.extractTypeNamesFromQuery)(query).length > 0,
|
|
6693
|
+
hasLiteralWitness: false,
|
|
6694
|
+
})
|
|
6695
|
+
? []
|
|
6696
|
+
: (0, query_utils_1.mechanismDomainPathTokens)(query);
|
|
6318
6697
|
const addNode = (n) => {
|
|
6319
6698
|
if ((0, arkts_1.isOhosApiFilePath)(n.filePath))
|
|
6320
6699
|
return;
|
|
@@ -6773,9 +7152,8 @@ class ToolHandler {
|
|
|
6773
7152
|
* Static AA class/interface nodes that look like domain Managers/Services.
|
|
6774
7153
|
*/
|
|
6775
7154
|
collectDomainRoleSymbols(cg, query, domainTokens) {
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
: (0, query_utils_1.mechanismDomainPathTokens)(query);
|
|
7155
|
+
// Spec 0044 §9: callers may pass [] to suppress CJK→ASCII synonym expansion.
|
|
7156
|
+
const tokens = domainTokens;
|
|
6779
7157
|
if (tokens.length === 0)
|
|
6780
7158
|
return [];
|
|
6781
7159
|
const byId = new Map();
|
|
@@ -7222,7 +7600,9 @@ class ToolHandler {
|
|
|
7222
7600
|
}
|
|
7223
7601
|
if (!callersOnly) {
|
|
7224
7602
|
for (const { node: c } of cg.getCallees(id).slice(0, 10)) {
|
|
7225
|
-
|
|
7603
|
+
if ((0, locate_contract_1.shouldDemoteLogToastSpine)(c.name, query)) {
|
|
7604
|
+
continue;
|
|
7605
|
+
}
|
|
7226
7606
|
if (/^log(?:Info|Error|Warn|Debug|Fatal)?$/i.test(c.name) || /^hilog$/i.test(c.name)) {
|
|
7227
7607
|
continue;
|
|
7228
7608
|
}
|
|
@@ -7411,11 +7791,15 @@ class ToolHandler {
|
|
|
7411
7791
|
continue;
|
|
7412
7792
|
trail.push(`- \`${seedNode.name}\` (${seedNode.kind}) — ${seedNode.filePath}:${seedNode.startLine}`);
|
|
7413
7793
|
for (const c of callers) {
|
|
7794
|
+
if ((0, query_utils_1.isMcpNoiseNode)(c.node))
|
|
7795
|
+
continue;
|
|
7414
7796
|
const loc = c.node.startLine ? `:${c.node.startLine}` : '';
|
|
7415
7797
|
trail.push(` ← used by \`${c.node.name}\` (${c.node.kind}) — ${c.node.filePath}${loc}`);
|
|
7416
7798
|
trailBullets++;
|
|
7417
7799
|
}
|
|
7418
7800
|
for (const c of callees) {
|
|
7801
|
+
if ((0, query_utils_1.isMcpNoiseNode)(c.node))
|
|
7802
|
+
continue;
|
|
7419
7803
|
const loc = c.node.startLine ? `:${c.node.startLine}` : '';
|
|
7420
7804
|
trail.push(` → calls \`${c.node.name}\` (${c.node.kind}) — ${c.node.filePath}${loc}`);
|
|
7421
7805
|
trailBullets++;
|
|
@@ -9634,6 +10018,8 @@ class ToolHandler {
|
|
|
9634
10018
|
const explicitMaxFiles = typeof args.maxFiles === 'number' && !Number.isNaN(args.maxFiles);
|
|
9635
10019
|
let maxFiles = (0, utils_1.clamp)(args.maxFiles || budget.defaultMaxFiles, 1, 20);
|
|
9636
10020
|
const queryFileBasenames = (0, query_utils_1.extractFileBasenamesFromQuery)(query);
|
|
10021
|
+
const locateAnchors = (0, query_utils_1.extractInRepoLocateAnchors)(query);
|
|
10022
|
+
const exploreSourceScope = (0, query_utils_1.resolveExploreSourceScope)(query, plan?.sourceScope);
|
|
9637
10023
|
const interpretationQuery = feature('queryAsInterpretationSurvey', query_utils_1.queryAsInterpretationSurvey);
|
|
9638
10024
|
const testOnlyInterpretation = feature('queryAsTestOnlyInterpretation', query_utils_1.queryAsTestOnlyInterpretation);
|
|
9639
10025
|
const crossModuleFlow = feature('queryAsCrossModuleFlowSurvey', query_utils_1.queryAsCrossModuleFlowSurvey) || plan?.intent === 'flow';
|
|
@@ -9645,19 +10031,28 @@ class ToolHandler {
|
|
|
9645
10031
|
? `${queryFileBasenames[0]} ${query}`
|
|
9646
10032
|
: queryFileBasenames.length === 1
|
|
9647
10033
|
? `${queryFileBasenames[0]} ${query}`
|
|
9648
|
-
:
|
|
10034
|
+
: locateAnchors.length === 1
|
|
10035
|
+
? `${locateAnchors[0]} ${query}`
|
|
10036
|
+
: query;
|
|
10037
|
+
const wantHints = !!(plan && (plan.source === 'llm' || plan.literalTexts?.length || plan.anchors?.length))
|
|
10038
|
+
|| exploreSourceScope !== 'all';
|
|
9649
10039
|
const subgraph = await cg.findRelevantContext(contextQuery, {
|
|
9650
10040
|
...contextOpts,
|
|
9651
|
-
...(
|
|
9652
|
-
symbols:
|
|
9653
|
-
|
|
10041
|
+
...(wantHints ? { retrievalHints: {
|
|
10042
|
+
symbols: [
|
|
10043
|
+
...(plan?.anchors ?? []).filter((anchor) => !(plan?.bindings ?? []).some((node) => anchor === node.name || anchor === node.qualifiedName)),
|
|
10044
|
+
...locateAnchors,
|
|
10045
|
+
].slice(0, 16),
|
|
10046
|
+
searchTerms: plan?.searchTerms ?? [], literalTexts: plan?.literalTexts,
|
|
10047
|
+
sourceScope: exploreSourceScope,
|
|
10048
|
+
nodeIds: (plan?.bindings ?? []).map((node) => node.id),
|
|
9654
10049
|
} } : {}),
|
|
9655
10050
|
});
|
|
9656
|
-
// Path-first
|
|
9657
|
-
//
|
|
9658
|
-
|
|
9659
|
-
|
|
9660
|
-
for (const base of
|
|
10051
|
+
// Path-first + Spec 0042 identifier seeding: Foo.ets and PascalCase ≥8 exact hits
|
|
10052
|
+
// become roots so taskContext-polluted FTS cannot drop the named in-repo file.
|
|
10053
|
+
if (exploreSourceScope !== 'sdk') {
|
|
10054
|
+
const seedNames = [...new Set([...queryFileBasenames, ...locateAnchors])].slice(0, 6);
|
|
10055
|
+
for (const base of seedNames) {
|
|
9661
10056
|
let hits = [];
|
|
9662
10057
|
try {
|
|
9663
10058
|
hits = cg.searchNodes(base, { limit: 50 });
|
|
@@ -9666,7 +10061,15 @@ class ToolHandler {
|
|
|
9666
10061
|
continue;
|
|
9667
10062
|
}
|
|
9668
10063
|
for (const r of hits) {
|
|
9669
|
-
if (
|
|
10064
|
+
if ((0, query_utils_1.isMcpNoiseNode)(r.node))
|
|
10065
|
+
continue;
|
|
10066
|
+
const fp = r.node.filePath.replace(/\\/g, '/');
|
|
10067
|
+
const stem = fp.split('/').pop()?.replace(/\.[^.]+$/, '') ?? '';
|
|
10068
|
+
const exactFile = stem.toLowerCase() === base.toLowerCase()
|
|
10069
|
+
|| (0, query_utils_1.fileMatchesQueryBasename)(r.node.filePath, [base]);
|
|
10070
|
+
const exactSymbol = r.node.name === base
|
|
10071
|
+
|| (r.node.qualifiedName?.split(/::|\./).pop() === base);
|
|
10072
|
+
if (!exactFile && !exactSymbol)
|
|
9670
10073
|
continue;
|
|
9671
10074
|
if (!subgraph.nodes.has(r.node.id)) {
|
|
9672
10075
|
subgraph.nodes.set(r.node.id, r.node);
|
|
@@ -9677,10 +10080,26 @@ class ToolHandler {
|
|
|
9677
10080
|
}
|
|
9678
10081
|
const literalSource = this.renderLiteralSource(cg, subgraph);
|
|
9679
10082
|
if (subgraph.nodes.size === 0) {
|
|
9680
|
-
|
|
9681
|
-
|
|
9682
|
-
|
|
9683
|
-
|
|
10083
|
+
// Spec 0044 §9: empty exact evidence → Miss (paths only), not a source dump.
|
|
10084
|
+
let fuzzyPaths = [];
|
|
10085
|
+
try {
|
|
10086
|
+
fuzzyPaths = [...new Set(cg.searchNodes(query.slice(0, 64), { limit: 12 })
|
|
10087
|
+
.map((r) => r.node.filePath.replace(/\\/g, '/'))
|
|
10088
|
+
.filter((p) => p && !(0, arkts_1.isOhosApiFilePath)(p)))].slice(0, 8);
|
|
10089
|
+
}
|
|
10090
|
+
catch { /* ignore */ }
|
|
10091
|
+
const text = literalSource.text
|
|
10092
|
+
|| (0, locate_contract_1.formatMissBanner)(fuzzyPaths);
|
|
10093
|
+
return this.exploreResult(text, {
|
|
10094
|
+
projectRoot,
|
|
10095
|
+
query,
|
|
10096
|
+
files: literalSource.files,
|
|
10097
|
+
sourceBytes: literalSource.files.reduce((sum, file) => sum + file.bytes, 0),
|
|
10098
|
+
responseBytes: text.length,
|
|
10099
|
+
locatedNodes: literalSource.nodes,
|
|
10100
|
+
partial: true,
|
|
10101
|
+
evidenceStatus: literalSource.text ? 'partial' : 'empty',
|
|
10102
|
+
});
|
|
9684
10103
|
}
|
|
9685
10104
|
// Seed import nodes for @kit.* / *Kit names (and named symbols like taskpool).
|
|
9686
10105
|
const importTerms = (0, query_utils_1.extractImportSearchTerms)(query);
|
|
@@ -9848,7 +10267,7 @@ class ToolHandler {
|
|
|
9848
10267
|
const raw = isQual ? this.findAllSymbols(cg, t).nodes : cg.getNodesByName(t);
|
|
9849
10268
|
let cands = raw
|
|
9850
10269
|
.filter((n) => SEED_KINDS.has(n.kind) && !isTestPath(n.filePath)
|
|
9851
|
-
&& !(
|
|
10270
|
+
&& !(exploreSourceScope === 'local' && (0, arkts_1.isOhosApiFilePath)(n.filePath)))
|
|
9852
10271
|
.sort((a, b) => {
|
|
9853
10272
|
// Prefer callables over types when both share a name, then body size.
|
|
9854
10273
|
const ac = CALLABLE.has(a.kind) ? 1 : 0;
|
|
@@ -10209,10 +10628,12 @@ class ToolHandler {
|
|
|
10209
10628
|
// dropped, so the budget never fills with incidental files. Guarded so it
|
|
10210
10629
|
// never prunes below 2.
|
|
10211
10630
|
if (maxGraph > 0) {
|
|
10631
|
+
const litKeep = new Set((subgraph.literalEvidence?.hits ?? []).map((h) => h.filePath).filter(Boolean));
|
|
10212
10632
|
const gated = relevantFiles.filter(([fp]) => (fileGraphScore.get(fp) ?? 0) >= maxGraph * 0.06
|
|
10213
10633
|
|| centralFiles.has(fp)
|
|
10214
10634
|
|| entryFiles.has(fp)
|
|
10215
10635
|
|| changeSurfaceFiles.has(fp)
|
|
10636
|
+
|| litKeep.has(fp)
|
|
10216
10637
|
|| (fileTermHits.get(fp) ?? 0) >= 2);
|
|
10217
10638
|
if (gated.length >= 2)
|
|
10218
10639
|
relevantFiles = gated;
|
|
@@ -10237,6 +10658,22 @@ class ToolHandler {
|
|
|
10237
10658
|
// tier so it isn't buried under files that merely share surface words (#1064).
|
|
10238
10659
|
for (const fp of changeSurfaceFiles)
|
|
10239
10660
|
namedSeedFiles.add(fp);
|
|
10661
|
+
// Spec 0044 §7: literal witness files must stay in the pack and rank with named seeds.
|
|
10662
|
+
{
|
|
10663
|
+
const litPaths = [...new Set((subgraph.literalEvidence?.hits ?? []).map((h) => h.filePath).filter(Boolean))];
|
|
10664
|
+
for (const fp of litPaths) {
|
|
10665
|
+
entryFiles.add(fp);
|
|
10666
|
+
namedSeedFiles.add(fp);
|
|
10667
|
+
if (relevantFiles.some(([f]) => f === fp))
|
|
10668
|
+
continue;
|
|
10669
|
+
let group = fileGroups.get(fp);
|
|
10670
|
+
if (!group) {
|
|
10671
|
+
group = { nodes: [], score: 1000 };
|
|
10672
|
+
fileGroups.set(fp, group);
|
|
10673
|
+
}
|
|
10674
|
+
relevantFiles.push([fp, group]);
|
|
10675
|
+
}
|
|
10676
|
+
}
|
|
10240
10677
|
// Multi-term corroboration tier: a file that is BOTH (a) an entry/central file
|
|
10241
10678
|
// (a search root, named seed, or graph-central hub — i.e. structurally part of
|
|
10242
10679
|
// the answer) AND (b) matched by ≥2 DISTINCT query terms must not be buried by
|
|
@@ -10260,7 +10697,7 @@ class ToolHandler {
|
|
|
10260
10697
|
const sortedFiles = relevantFiles.sort((a, b) => {
|
|
10261
10698
|
const aPath = a[0].toLowerCase();
|
|
10262
10699
|
const bPath = b[0].toLowerCase();
|
|
10263
|
-
if (
|
|
10700
|
+
if (exploreSourceScope === 'local') {
|
|
10264
10701
|
const sdkOrder = Number((0, arkts_1.isOhosApiFilePath)(a[0])) - Number((0, arkts_1.isOhosApiFilePath)(b[0]));
|
|
10265
10702
|
if (sdkOrder)
|
|
10266
10703
|
return sdkOrder;
|
|
@@ -10269,6 +10706,17 @@ class ToolHandler {
|
|
|
10269
10706
|
- Number((subgraph.literalEvidence?.hits ?? []).some((hit) => hit.filePath === a[0]));
|
|
10270
10707
|
if (literalOrder)
|
|
10271
10708
|
return literalOrder;
|
|
10709
|
+
// Spec 0044 §7: demote Logger/hilog/Toast-only files when query did not name them.
|
|
10710
|
+
const logToastDemote = (fp) => {
|
|
10711
|
+
const group = fileGroups.get(fp);
|
|
10712
|
+
if (!group?.nodes.length)
|
|
10713
|
+
return 0;
|
|
10714
|
+
const allNoise = group.nodes.every((n) => (0, locate_contract_1.shouldDemoteLogToastSpine)(n.name, query));
|
|
10715
|
+
return allNoise ? 1 : 0;
|
|
10716
|
+
};
|
|
10717
|
+
const logOrder = logToastDemote(a[0]) - logToastDemote(b[0]);
|
|
10718
|
+
if (logOrder)
|
|
10719
|
+
return logOrder;
|
|
10272
10720
|
// Query-named file (LocationController.ets in the question) before partial
|
|
10273
10721
|
// substring matches (control.ets matching "Controller" inside LocationController).
|
|
10274
10722
|
const aExactBase = (0, query_utils_1.fileMatchesQueryBasename)(a[0], queryFileBasenames) ? 1 : 0;
|
|
@@ -11341,7 +11789,15 @@ class ToolHandler {
|
|
|
11341
11789
|
const headerSuffix = omittedCount > 0
|
|
11342
11790
|
? `${headerSymbols.join(', ')}, +${omittedCount} more`
|
|
11343
11791
|
: headerSymbols.join(', ');
|
|
11344
|
-
|
|
11792
|
+
let fileHeader = fileSectionHeader(filePath, headerSuffix);
|
|
11793
|
+
// Spec 0044 §10.1: warn when basename ≠ primary declaration.
|
|
11794
|
+
{
|
|
11795
|
+
const primary = group?.nodes.find((n) => ['class', 'struct', 'component', 'interface', 'enum'].includes(n.kind)
|
|
11796
|
+
&& !n.name.startsWith('%')) ?? group?.nodes.find((n) => ['function', 'method'].includes(n.kind) && !n.name.startsWith('%'));
|
|
11797
|
+
const mismatch = (0, locate_contract_1.formatFilenameDeclarationMismatch)(filePath, primary?.name);
|
|
11798
|
+
if (mismatch)
|
|
11799
|
+
fileHeader = `${fileHeader}\n${mismatch}`;
|
|
11800
|
+
}
|
|
11345
11801
|
// The total cap bounds INCIDENTAL files only. A file that DEFINES a symbol
|
|
11346
11802
|
// the agent named (or that's on the flow spine) renders even when the
|
|
11347
11803
|
// nominal total is used up — it's the answer, and the set is bounded by
|
|
@@ -11635,6 +12091,106 @@ class ToolHandler {
|
|
|
11635
12091
|
}
|
|
11636
12092
|
return { text: sections.join('\n\n'), files, nodes };
|
|
11637
12093
|
}
|
|
12094
|
+
/**
|
|
12095
|
+
* Spec 0039: when the query names Harmony route profiles, lead with a short
|
|
12096
|
+
* Registration sources table so agents see JSON was indexed (avoid re-Read).
|
|
12097
|
+
*/
|
|
12098
|
+
prependHarmonyRegistrationSources(result, projectRoot, query) {
|
|
12099
|
+
if (!/route_map|router_map|main_pages/i.test(query))
|
|
12100
|
+
return result;
|
|
12101
|
+
const [first, ...rest] = result.content;
|
|
12102
|
+
if (!first || first.type !== 'text')
|
|
12103
|
+
return result;
|
|
12104
|
+
if (/^\*\*Registration sources\*\*/m.test(first.text))
|
|
12105
|
+
return result;
|
|
12106
|
+
let cg;
|
|
12107
|
+
try {
|
|
12108
|
+
cg = this.getHomeGraph(projectRoot);
|
|
12109
|
+
}
|
|
12110
|
+
catch {
|
|
12111
|
+
return result;
|
|
12112
|
+
}
|
|
12113
|
+
const section = formatHarmonyRegistrationSources(cg);
|
|
12114
|
+
if (!section)
|
|
12115
|
+
return result;
|
|
12116
|
+
return {
|
|
12117
|
+
...result,
|
|
12118
|
+
content: [{ type: 'text', text: `${section}\n\n${first.text}` }, ...rest],
|
|
12119
|
+
};
|
|
12120
|
+
}
|
|
12121
|
+
/** Spec 0041: lead with Resource hits when string.json is named or matched. */
|
|
12122
|
+
prependHarmonyResourceHits(result, projectRoot, query) {
|
|
12123
|
+
const [first, ...rest] = result.content;
|
|
12124
|
+
if (!first || first.type !== 'text')
|
|
12125
|
+
return result;
|
|
12126
|
+
if (/^\*\*Resource hits\*\*/m.test(first.text) || /\n\*\*Resource hits\*\*/m.test(first.text)) {
|
|
12127
|
+
return result;
|
|
12128
|
+
}
|
|
12129
|
+
let cg;
|
|
12130
|
+
try {
|
|
12131
|
+
cg = this.getHomeGraph(projectRoot);
|
|
12132
|
+
}
|
|
12133
|
+
catch {
|
|
12134
|
+
return result;
|
|
12135
|
+
}
|
|
12136
|
+
const section = formatHarmonyResourceHits(cg, query);
|
|
12137
|
+
if (!section)
|
|
12138
|
+
return result;
|
|
12139
|
+
return {
|
|
12140
|
+
...result,
|
|
12141
|
+
content: [{ type: 'text', text: `${section}\n\n${first.text}` }, ...rest],
|
|
12142
|
+
};
|
|
12143
|
+
}
|
|
12144
|
+
/** Spec 0048 §1–2: Capability profiles or form negative evidence. */
|
|
12145
|
+
prependHarmonyCapabilityProfiles(result, projectRoot, query) {
|
|
12146
|
+
const [first, ...rest] = result.content;
|
|
12147
|
+
if (!first || first.type !== 'text')
|
|
12148
|
+
return result;
|
|
12149
|
+
if (/^\*\*Capability profiles/m.test(first.text)
|
|
12150
|
+
|| /\n\*\*Capability profiles/m.test(first.text)) {
|
|
12151
|
+
return result;
|
|
12152
|
+
}
|
|
12153
|
+
let cg;
|
|
12154
|
+
try {
|
|
12155
|
+
cg = this.getHomeGraph(projectRoot);
|
|
12156
|
+
}
|
|
12157
|
+
catch {
|
|
12158
|
+
return result;
|
|
12159
|
+
}
|
|
12160
|
+
const section = formatHarmonyCapabilityProfiles(cg, query);
|
|
12161
|
+
if (!section)
|
|
12162
|
+
return result;
|
|
12163
|
+
return {
|
|
12164
|
+
...result,
|
|
12165
|
+
content: [{ type: 'text', text: `${section}\n\n${first.text}` }, ...rest],
|
|
12166
|
+
};
|
|
12167
|
+
}
|
|
12168
|
+
/** Spec 0048 §5: seam notes for located anchors. */
|
|
12169
|
+
prependHarmonySeamNotes(result, projectRoot, emission) {
|
|
12170
|
+
const located = emission?.locatedNodes;
|
|
12171
|
+
if (!located || located.length === 0)
|
|
12172
|
+
return result;
|
|
12173
|
+
const [first, ...rest] = result.content;
|
|
12174
|
+
if (!first || first.type !== 'text')
|
|
12175
|
+
return result;
|
|
12176
|
+
if (/^\*\*Seam notes\*\*/m.test(first.text) || /\n\*\*Seam notes\*\*/m.test(first.text)) {
|
|
12177
|
+
return result;
|
|
12178
|
+
}
|
|
12179
|
+
let cg;
|
|
12180
|
+
try {
|
|
12181
|
+
cg = this.getHomeGraph(projectRoot);
|
|
12182
|
+
}
|
|
12183
|
+
catch {
|
|
12184
|
+
return result;
|
|
12185
|
+
}
|
|
12186
|
+
const section = formatHarmonySeamNotes(cg, projectRoot, located);
|
|
12187
|
+
if (!section)
|
|
12188
|
+
return result;
|
|
12189
|
+
return {
|
|
12190
|
+
...result,
|
|
12191
|
+
content: [{ type: 'text', text: `${section}\n\n${first.text}` }, ...rest],
|
|
12192
|
+
};
|
|
12193
|
+
}
|
|
11638
12194
|
/**
|
|
11639
12195
|
* An explore response plus the record of what it emitted (CG-17). The record
|
|
11640
12196
|
* rides the result only as far as {@link execute}, which files it into the
|
|
@@ -11642,17 +12198,42 @@ class ToolHandler {
|
|
|
11642
12198
|
*/
|
|
11643
12199
|
exploreResult(text, emission) {
|
|
11644
12200
|
const meta = (0, explore_repeat_guard_1.inferExplorePartialMeta)(text);
|
|
11645
|
-
|
|
12201
|
+
let evidenceStatus = emission.evidenceStatus
|
|
12202
|
+
?? (emission.sourceBytes > 0 ? (meta.partial ? 'partial' : 'complete') : 'partial');
|
|
12203
|
+
let body = text;
|
|
12204
|
+
// Spec 0044 §8: Located banner when complete exact evidence; soften Partial wording.
|
|
12205
|
+
if (evidenceStatus === 'complete' && !body.includes(locate_contract_1.LOCATED_MARKER)) {
|
|
12206
|
+
if (/\*\*Partial locator\*\*/i.test(body)) {
|
|
12207
|
+
body = body.replace(/>\s*\*\*Partial locator\*\*[^\n]*/gi, (0, locate_contract_1.formatLocatedBanner)());
|
|
12208
|
+
}
|
|
12209
|
+
else if (!/\*\*ANSWER NOW/i.test(body) && !body.includes(locate_contract_1.MISS_MARKER)) {
|
|
12210
|
+
body = `${(0, locate_contract_1.formatLocatedBanner)()}\n\n${body}`;
|
|
12211
|
+
}
|
|
12212
|
+
}
|
|
12213
|
+
else if ((evidenceStatus === 'partial' || meta.partial)
|
|
12214
|
+
&& /\*\*Partial locator\*\*/i.test(body)
|
|
12215
|
+
&& !body.includes(locate_contract_1.LOCATED_MARKER)) {
|
|
12216
|
+
// Spec 0044 §8: Partial next-step → node/usages/search (not Grep same names).
|
|
12217
|
+
body = body.replace(/>\s*\*\*Partial locator\*\*[^\n]*/gi, (0, locate_contract_1.formatPartialBanner)());
|
|
12218
|
+
}
|
|
12219
|
+
let result = this.textResult(body);
|
|
12220
|
+
result = this.prependHarmonyRegistrationSources(result, emission.projectRoot, emission.query);
|
|
12221
|
+
result = this.prependHarmonyCapabilityProfiles(result, emission.projectRoot, emission.query);
|
|
12222
|
+
result = this.prependHarmonyResourceHits(result, emission.projectRoot, emission.query);
|
|
11646
12223
|
result[explore_session_state_1.EXPLORE_EMISSION_KEY] = {
|
|
11647
12224
|
...emission,
|
|
11648
|
-
evidenceStatus
|
|
11649
|
-
partial: emission.partial ?? (
|
|
12225
|
+
evidenceStatus,
|
|
12226
|
+
partial: emission.partial ?? (evidenceStatus !== 'complete' ? true : meta.partial),
|
|
11650
12227
|
nextAnchor: emission.nextAnchor ?? meta.nextAnchor,
|
|
11651
12228
|
};
|
|
12229
|
+
result = this.prependHarmonySeamNotes(result, emission.projectRoot, result[explore_session_state_1.EXPLORE_EMISSION_KEY]);
|
|
11652
12230
|
return result;
|
|
11653
12231
|
}
|
|
11654
12232
|
/** Attach a minimal session emission when a compact/light path used textResult. */
|
|
11655
12233
|
ensureExploreEmission(result, projectRoot, query) {
|
|
12234
|
+
result = this.prependHarmonyRegistrationSources(result, projectRoot, query);
|
|
12235
|
+
result = this.prependHarmonyCapabilityProfiles(result, projectRoot, query);
|
|
12236
|
+
result = this.prependHarmonyResourceHits(result, projectRoot, query);
|
|
11656
12237
|
if (result[explore_session_state_1.EXPLORE_EMISSION_KEY]) {
|
|
11657
12238
|
const em = result[explore_session_state_1.EXPLORE_EMISSION_KEY];
|
|
11658
12239
|
if (em.partial === undefined || !em.nextAnchor) {
|
|
@@ -11662,6 +12243,7 @@ class ToolHandler {
|
|
|
11662
12243
|
if (!em.nextAnchor && meta.nextAnchor)
|
|
11663
12244
|
em.nextAnchor = meta.nextAnchor;
|
|
11664
12245
|
}
|
|
12246
|
+
result = this.prependHarmonySeamNotes(result, projectRoot, em);
|
|
11665
12247
|
return result;
|
|
11666
12248
|
}
|
|
11667
12249
|
const text = result.content?.[0]?.text ?? '';
|
|
@@ -11835,7 +12417,8 @@ class ToolHandler {
|
|
|
11835
12417
|
const nodes = cg.getNodesInFile(filePath)
|
|
11836
12418
|
.filter((n) => n.kind !== 'file' && n.kind !== 'import' && n.kind !== 'export')
|
|
11837
12419
|
.sort((a, b) => a.startLine - b.startLine);
|
|
11838
|
-
const dependents = cg.getFileDependents(filePath)
|
|
12420
|
+
const dependents = cg.getFileDependents(filePath)
|
|
12421
|
+
.filter((p) => !p.includes('@dummy') && !p.replace(/\\/g, '/').split('/').pop()?.startsWith('@dummy'));
|
|
11839
12422
|
// Compact, one-line blast radius (homegraph's value-add over a plain Read).
|
|
11840
12423
|
const depSummary = dependents.length
|
|
11841
12424
|
? `used by ${dependents.length} file${dependents.length === 1 ? '' : 's'}: ${dependents.slice(0, 8).join(', ')}${dependents.length > 8 ? `, +${dependents.length - 8} more` : ''}`
|
|
@@ -12015,6 +12598,8 @@ class ToolHandler {
|
|
|
12015
12598
|
for (const e of edges) {
|
|
12016
12599
|
if (seen.has(e.node.id))
|
|
12017
12600
|
continue;
|
|
12601
|
+
if ((0, query_utils_1.isMcpNoiseNode)(e.node))
|
|
12602
|
+
continue;
|
|
12018
12603
|
seen.add(e.node.id);
|
|
12019
12604
|
out.push(e);
|
|
12020
12605
|
}
|
|
@@ -12190,21 +12775,47 @@ class ToolHandler {
|
|
|
12190
12775
|
return this.textResult('No modules found in the project map.');
|
|
12191
12776
|
}
|
|
12192
12777
|
const FILE_CAP = 80;
|
|
12193
|
-
const
|
|
12778
|
+
const projectRoot = cg.getProjectRoot();
|
|
12779
|
+
const hasHarmony = map.modules.some((m) => m.kind === 'harmony');
|
|
12194
12780
|
const lines = [
|
|
12195
|
-
`**Project map** (
|
|
12781
|
+
`**Project map** (phase=${map.phase})`,
|
|
12196
12782
|
`modules: ${map.modules.length} · files: ${map.fileCount}`,
|
|
12197
|
-
'',
|
|
12198
12783
|
];
|
|
12199
|
-
|
|
12200
|
-
|
|
12784
|
+
// Spec 0040 — Harmony skeleton summary (navigation pointers, not call edges).
|
|
12785
|
+
try {
|
|
12786
|
+
const bundle = (0, project_map_1.readHarmonyAppBundleName)(projectRoot);
|
|
12787
|
+
if (bundle)
|
|
12788
|
+
lines.push(`bundle: \`${bundle}\` (from app.json5)`);
|
|
12789
|
+
}
|
|
12790
|
+
catch {
|
|
12791
|
+
/* omit */
|
|
12201
12792
|
}
|
|
12202
|
-
if (
|
|
12203
|
-
lines.push('
|
|
12793
|
+
if (hasHarmony) {
|
|
12794
|
+
lines.push('modules from `build-profile.json5` — skeleton map only (not call edges; use `homegraph_explore` for route_map → page)');
|
|
12204
12795
|
}
|
|
12796
|
+
lines.push('');
|
|
12205
12797
|
for (const m of map.modules) {
|
|
12206
12798
|
const rootLabel = m.rootPath || '.';
|
|
12207
12799
|
lines.push(`### ${m.name} (\`${rootLabel}\`) · ${m.kind} · ${m.fileCount} files`);
|
|
12800
|
+
if (m.kind === 'harmony' || m.rootPath) {
|
|
12801
|
+
try {
|
|
12802
|
+
const profiles = (0, project_map_1.listHarmonyRouteProfilesUnderModule)(projectRoot, m.rootPath || '');
|
|
12803
|
+
for (const p of profiles) {
|
|
12804
|
+
lines.push(`- route profile: \`${p}\``);
|
|
12805
|
+
}
|
|
12806
|
+
}
|
|
12807
|
+
catch {
|
|
12808
|
+
/* omit */
|
|
12809
|
+
}
|
|
12810
|
+
try {
|
|
12811
|
+
const ohpm = (0, project_map_1.readModuleOhPackageName)(projectRoot, m.rootPath || '');
|
|
12812
|
+
if (ohpm)
|
|
12813
|
+
lines.push(`- oh-package: \`${ohpm}\``);
|
|
12814
|
+
}
|
|
12815
|
+
catch {
|
|
12816
|
+
/* omit */
|
|
12817
|
+
}
|
|
12818
|
+
}
|
|
12208
12819
|
if (m.files && m.files.length > 0) {
|
|
12209
12820
|
const shown = m.files.slice(0, FILE_CAP);
|
|
12210
12821
|
for (const f of shown) {
|
|
@@ -12216,6 +12827,42 @@ class ToolHandler {
|
|
|
12216
12827
|
}
|
|
12217
12828
|
lines.push('');
|
|
12218
12829
|
}
|
|
12830
|
+
// Spec 0048 §4 — module roster with local file: deps.
|
|
12831
|
+
try {
|
|
12832
|
+
const roster = (0, project_map_1.formatHarmonyModuleRoster)(projectRoot, map.modules);
|
|
12833
|
+
if (roster) {
|
|
12834
|
+
lines.push(roster);
|
|
12835
|
+
lines.push('');
|
|
12836
|
+
}
|
|
12837
|
+
}
|
|
12838
|
+
catch {
|
|
12839
|
+
/* omit roster on failure */
|
|
12840
|
+
}
|
|
12841
|
+
// Spec 0042 B — path inventory (string/rawfile/media + unindexed package dirs).
|
|
12842
|
+
// Skip when the caller filtered to a single non-Harmony module? Still useful
|
|
12843
|
+
// project-wide; keep always on Harmony-shaped trees or whenever scan finds hits.
|
|
12844
|
+
try {
|
|
12845
|
+
const indexedPaths = map.modules.flatMap((m) => (m.files ?? []).map((f) => f.path));
|
|
12846
|
+
// When includeFiles=false, files arrays are empty — fall back to full map paths.
|
|
12847
|
+
let pathsForUnindexed = indexedPaths;
|
|
12848
|
+
if (pathsForUnindexed.length === 0) {
|
|
12849
|
+
try {
|
|
12850
|
+
const full = cg.getProjectMap({ includeFiles: true });
|
|
12851
|
+
pathsForUnindexed = full.modules.flatMap((m) => (m.files ?? []).map((f) => f.path));
|
|
12852
|
+
}
|
|
12853
|
+
catch {
|
|
12854
|
+
pathsForUnindexed = [];
|
|
12855
|
+
}
|
|
12856
|
+
}
|
|
12857
|
+
const inv = (0, project_map_1.scanHarmonyResourceInventory)(projectRoot, { indexedPaths: pathsForUnindexed });
|
|
12858
|
+
const section = (0, project_map_1.formatHarmonyResourceInventory)(inv);
|
|
12859
|
+
if (section) {
|
|
12860
|
+
lines.push(section);
|
|
12861
|
+
}
|
|
12862
|
+
}
|
|
12863
|
+
catch {
|
|
12864
|
+
/* omit inventory on scan failure */
|
|
12865
|
+
}
|
|
12219
12866
|
return this.textResult(lines.join('\n').trimEnd());
|
|
12220
12867
|
}
|
|
12221
12868
|
/**
|