homegraph 1.5.10 → 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 +18 -0
- package/dist/extraction/grammars.d.ts +19 -0
- package/dist/extraction/grammars.js +44 -1
- 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 +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/server-instructions.d.ts +2 -2
- package/dist/mcp/server-instructions.js +7 -4
- package/dist/mcp/session.js +15 -0
- package/dist/mcp/tools.d.ts +49 -2
- package/dist/mcp/tools.js +714 -64
- 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/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 +1 -1
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,8 +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");
|
|
34
|
+
const grammars_1 = require("../extraction/grammars");
|
|
35
|
+
const project_map_1 = require("../project-map");
|
|
29
36
|
const index_availability_1 = require("./index-availability");
|
|
30
37
|
const directory_1 = require("../directory");
|
|
38
|
+
const runtime_log_1 = require("../runtime-log");
|
|
31
39
|
// Lazy-load the heavy HomeGraph chain off the MCP startup path — see the same
|
|
32
40
|
// helper in engine.ts. ToolHandler must load to answer tools/list (static
|
|
33
41
|
// schemas), but it must NOT drag in sqlite/query layers before the daemon binds;
|
|
@@ -55,6 +63,7 @@ const arkts_1 = require("../extraction/languages/arkts");
|
|
|
55
63
|
const explore_session_state_1 = require("./explore-session-state");
|
|
56
64
|
const explore_dedup_1 = require("./explore-dedup");
|
|
57
65
|
const explore_repeat_guard_1 = require("./explore-repeat-guard");
|
|
66
|
+
const locate_contract_1 = require("./locate-contract");
|
|
58
67
|
const dynamic_boundaries_1 = require("./dynamic-boundaries");
|
|
59
68
|
const query_cache_1 = require("./query-cache");
|
|
60
69
|
const diff_impact_1 = require("./diff-impact");
|
|
@@ -565,6 +574,258 @@ function fileSectionHeader(filePath, suffix) {
|
|
|
565
574
|
? `${FILE_SECTION_PREFIX}${filePath}\`** — ${suffix}`
|
|
566
575
|
: `${FILE_SECTION_PREFIX}${filePath}\`**`;
|
|
567
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
|
+
}
|
|
568
829
|
/**
|
|
569
830
|
* Per-file staleness banner emitted at the top of a tool response when the
|
|
570
831
|
* file watcher has pending events for files referenced by the response.
|
|
@@ -921,11 +1182,12 @@ 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
1188
|
'For a missing usage, dependency/cycle or native-registration relation, use ' +
|
|
927
1189
|
'homegraph_usages, homegraph_modules, or homegraph_native instead. ' +
|
|
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. ' +
|
|
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. ' +
|
|
929
1191
|
'Reuse unchanged complete ranges; refresh missing, edited or truncated evidence. ' +
|
|
930
1192
|
'No new evidence → change to a targeted source inspection, not a paraphrased explore. ' +
|
|
931
1193
|
'Partial/busy → at most one focused recovery for the named gap; budgets are ceilings, not required calls. ' +
|
|
@@ -967,9 +1229,12 @@ exports.tools = [
|
|
|
967
1229
|
},
|
|
968
1230
|
{
|
|
969
1231
|
name: 'homegraph_project',
|
|
970
|
-
description: 'Shallow
|
|
971
|
-
'
|
|
972
|
-
'
|
|
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).',
|
|
973
1238
|
inputSchema: {
|
|
974
1239
|
type: 'object',
|
|
975
1240
|
properties: {
|
|
@@ -1326,6 +1591,10 @@ class ToolHandler {
|
|
|
1326
1591
|
// The directory the server last searched for a default project. Surfaced in
|
|
1327
1592
|
// the "not initialized" error so users can see why detection missed.
|
|
1328
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;
|
|
1329
1598
|
// Per-start-path cache of the git worktree/index mismatch (issue #155). The
|
|
1330
1599
|
// mismatch is a fixed property of (where the request came from → which
|
|
1331
1600
|
// .homegraph/ it resolves to), so the up-to-two `git rev-parse` spawns run
|
|
@@ -1580,6 +1849,11 @@ class ToolHandler {
|
|
|
1580
1849
|
*
|
|
1581
1850
|
* Walks up parent directories to find the nearest .homegraph/ folder,
|
|
1582
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.
|
|
1583
1857
|
*/
|
|
1584
1858
|
getHomeGraph(projectPath) {
|
|
1585
1859
|
const sourcesMode = (0, graph_sources_1.resolveGraphSources)();
|
|
@@ -1626,25 +1900,34 @@ class ToolHandler {
|
|
|
1626
1900
|
// (#926). The DB connection itself is still cached (by resolved root,
|
|
1627
1901
|
// below), so re-resolving costs only the stat walk, never a reopen.
|
|
1628
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
|
+
}
|
|
1629
1922
|
if (!resolvedRoot) {
|
|
1630
1923
|
throw new NotIndexedError(`The project at ${projectPath} isn't indexed with homegraph (no .homegraph/ directory found ` +
|
|
1631
1924
|
'walking up from it), so homegraph cannot query it. Use your built-in tools (Read/Grep/Glob) ' +
|
|
1632
1925
|
"for that codebase instead, and don't call homegraph for it again this session. " +
|
|
1633
1926
|
"Indexing is the user's decision — they can run 'homegraph init' in that project to enable it.");
|
|
1634
1927
|
}
|
|
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.
|
|
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.
|
|
1648
1931
|
const cached = this.projectCache.get(resolvedRoot);
|
|
1649
1932
|
if (cached)
|
|
1650
1933
|
return this.freshen(cached);
|
|
@@ -1889,6 +2172,15 @@ class ToolHandler {
|
|
|
1889
2172
|
catch {
|
|
1890
2173
|
reason = null;
|
|
1891
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
|
+
}
|
|
1892
2184
|
const composed = `${formatDegradedBanner(reason)}\n\n${head.text}`;
|
|
1893
2185
|
return { ...result, content: [{ type: 'text', text: composed }, ...tail] };
|
|
1894
2186
|
}
|
|
@@ -1898,8 +2190,9 @@ class ToolHandler {
|
|
|
1898
2190
|
return result;
|
|
1899
2191
|
}
|
|
1900
2192
|
/**
|
|
1901
|
-
*
|
|
1902
|
-
*
|
|
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.
|
|
1903
2196
|
*/
|
|
1904
2197
|
withProductStatusFooter(result, projectPath) {
|
|
1905
2198
|
if (result.isError)
|
|
@@ -1907,8 +2200,6 @@ class ToolHandler {
|
|
|
1907
2200
|
const [first, ...rest] = result.content;
|
|
1908
2201
|
if (!first || first.type !== 'text')
|
|
1909
2202
|
return result;
|
|
1910
|
-
if ((0, index_availability_1.textAlreadyHasProductStatus)(first.text))
|
|
1911
|
-
return result;
|
|
1912
2203
|
let cg;
|
|
1913
2204
|
try {
|
|
1914
2205
|
cg = this.getHomeGraph(projectPath);
|
|
@@ -1926,26 +2217,52 @@ class ToolHandler {
|
|
|
1926
2217
|
/* leave cg */
|
|
1927
2218
|
}
|
|
1928
2219
|
}
|
|
1929
|
-
let
|
|
1930
|
-
|
|
1931
|
-
|
|
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)) {
|
|
2223
|
+
try {
|
|
2224
|
+
const absRoot = (0, path_1.resolve)(cg.getProjectRoot());
|
|
2225
|
+
if (absRoot) {
|
|
2226
|
+
text = `${(0, index_availability_1.formatProjectRootPathHint)(absRoot)}\n\n${text}`;
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
catch {
|
|
2230
|
+
/* no root — skip hint */
|
|
2231
|
+
}
|
|
1932
2232
|
}
|
|
1933
|
-
|
|
1934
|
-
|
|
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}`;
|
|
1935
2238
|
}
|
|
1936
|
-
|
|
1937
|
-
if (
|
|
2239
|
+
// Spec 0035: one-line product index status footer (skip if already present).
|
|
2240
|
+
if (!(0, index_availability_1.textAlreadyHasProductStatus)(text)) {
|
|
2241
|
+
let state;
|
|
1938
2242
|
try {
|
|
1939
|
-
|
|
2243
|
+
state = (0, index_availability_1.resolveProductIndexState)(cg);
|
|
1940
2244
|
}
|
|
1941
2245
|
catch {
|
|
1942
|
-
|
|
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
|
+
}
|
|
1943
2259
|
}
|
|
2260
|
+
const line = (0, index_availability_1.formatProductStatusLine)(state, { pendingPaths });
|
|
2261
|
+
text = `${text}\n\n${line}`;
|
|
1944
2262
|
}
|
|
1945
|
-
const line = (0, index_availability_1.formatProductStatusLine)(state, { pendingPaths });
|
|
1946
2263
|
return {
|
|
1947
2264
|
...result,
|
|
1948
|
-
content: [{ type: 'text', text
|
|
2265
|
+
content: [{ type: 'text', text }, ...rest],
|
|
1949
2266
|
};
|
|
1950
2267
|
}
|
|
1951
2268
|
/**
|
|
@@ -1956,10 +2273,47 @@ class ToolHandler {
|
|
|
1956
2273
|
*/
|
|
1957
2274
|
async execute(toolName, args, sessionState) {
|
|
1958
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) {
|
|
1959
2310
|
args = { ...args };
|
|
1960
2311
|
for (const key of [QUERY_PLAN_ARG, QUERY_DEADLINE_ARG, QUERY_STARTED_ARG, QUERY_INDEX_STATE_ARG, QUERY_FAST_ATTEMPTED_ARG, '_hgEvidenceMaxChars'])
|
|
1961
2312
|
delete args[key];
|
|
1962
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;
|
|
1963
2317
|
// Block the first tool call on the engine's post-open reconcile so we
|
|
1964
2318
|
// never serve rows for files deleted/edited while no MCP server was
|
|
1965
2319
|
// running. The wait is time-boxed (#905): a huge-repo reconcile takes
|
|
@@ -2222,15 +2576,19 @@ class ToolHandler {
|
|
|
2222
2576
|
try {
|
|
2223
2577
|
const cg = this.getHomeGraph(args.projectPath);
|
|
2224
2578
|
const root = cg.getProjectRoot();
|
|
2225
|
-
const
|
|
2226
|
-
|
|
2227
|
-
return null;
|
|
2228
|
-
const hint = typeof args.symbol === 'string'
|
|
2579
|
+
const prior = sessionState.forProject(root);
|
|
2580
|
+
const symbolHint = typeof args.symbol === 'string'
|
|
2229
2581
|
? args.symbol
|
|
2230
2582
|
: typeof args.file === 'string'
|
|
2231
2583
|
? args.file
|
|
2232
2584
|
: undefined;
|
|
2233
|
-
|
|
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));
|
|
2234
2592
|
}
|
|
2235
2593
|
catch {
|
|
2236
2594
|
return null;
|
|
@@ -2302,6 +2660,10 @@ class ToolHandler {
|
|
|
2302
2660
|
const last = prior?.calls[prior.calls.length - 1];
|
|
2303
2661
|
if (!last || (0, explore_session_state_1.inferExploreEvidenceStatus)(last) === 'complete')
|
|
2304
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;
|
|
2305
2667
|
sessionState.recordDepthTool(root);
|
|
2306
2668
|
}
|
|
2307
2669
|
catch { /* bookkeeping only */ }
|
|
@@ -3150,6 +3512,13 @@ class ToolHandler {
|
|
|
3150
3512
|
registeredAt,
|
|
3151
3513
|
};
|
|
3152
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
|
+
}
|
|
3153
3522
|
if (m?.synthesizedBy === 'goframe-route') {
|
|
3154
3523
|
const route = m.route ? `\`${String(m.route)}\`` : 'a route';
|
|
3155
3524
|
return {
|
|
@@ -6318,7 +6687,13 @@ class ToolHandler {
|
|
|
6318
6687
|
const isTestPath = (p) => /(^|\/)(tests?|spec)\//i.test(p) || /\.(test|spec)\./i.test(p);
|
|
6319
6688
|
const fileNodes = new Map();
|
|
6320
6689
|
const seedIds = new Set();
|
|
6321
|
-
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);
|
|
6322
6697
|
const addNode = (n) => {
|
|
6323
6698
|
if ((0, arkts_1.isOhosApiFilePath)(n.filePath))
|
|
6324
6699
|
return;
|
|
@@ -6777,9 +7152,8 @@ class ToolHandler {
|
|
|
6777
7152
|
* Static AA class/interface nodes that look like domain Managers/Services.
|
|
6778
7153
|
*/
|
|
6779
7154
|
collectDomainRoleSymbols(cg, query, domainTokens) {
|
|
6780
|
-
|
|
6781
|
-
|
|
6782
|
-
: (0, query_utils_1.mechanismDomainPathTokens)(query);
|
|
7155
|
+
// Spec 0044 §9: callers may pass [] to suppress CJK→ASCII synonym expansion.
|
|
7156
|
+
const tokens = domainTokens;
|
|
6783
7157
|
if (tokens.length === 0)
|
|
6784
7158
|
return [];
|
|
6785
7159
|
const byId = new Map();
|
|
@@ -7226,7 +7600,9 @@ class ToolHandler {
|
|
|
7226
7600
|
}
|
|
7227
7601
|
if (!callersOnly) {
|
|
7228
7602
|
for (const { node: c } of cg.getCallees(id).slice(0, 10)) {
|
|
7229
|
-
|
|
7603
|
+
if ((0, locate_contract_1.shouldDemoteLogToastSpine)(c.name, query)) {
|
|
7604
|
+
continue;
|
|
7605
|
+
}
|
|
7230
7606
|
if (/^log(?:Info|Error|Warn|Debug|Fatal)?$/i.test(c.name) || /^hilog$/i.test(c.name)) {
|
|
7231
7607
|
continue;
|
|
7232
7608
|
}
|
|
@@ -7415,11 +7791,15 @@ class ToolHandler {
|
|
|
7415
7791
|
continue;
|
|
7416
7792
|
trail.push(`- \`${seedNode.name}\` (${seedNode.kind}) — ${seedNode.filePath}:${seedNode.startLine}`);
|
|
7417
7793
|
for (const c of callers) {
|
|
7794
|
+
if ((0, query_utils_1.isMcpNoiseNode)(c.node))
|
|
7795
|
+
continue;
|
|
7418
7796
|
const loc = c.node.startLine ? `:${c.node.startLine}` : '';
|
|
7419
7797
|
trail.push(` ← used by \`${c.node.name}\` (${c.node.kind}) — ${c.node.filePath}${loc}`);
|
|
7420
7798
|
trailBullets++;
|
|
7421
7799
|
}
|
|
7422
7800
|
for (const c of callees) {
|
|
7801
|
+
if ((0, query_utils_1.isMcpNoiseNode)(c.node))
|
|
7802
|
+
continue;
|
|
7423
7803
|
const loc = c.node.startLine ? `:${c.node.startLine}` : '';
|
|
7424
7804
|
trail.push(` → calls \`${c.node.name}\` (${c.node.kind}) — ${c.node.filePath}${loc}`);
|
|
7425
7805
|
trailBullets++;
|
|
@@ -9638,6 +10018,8 @@ class ToolHandler {
|
|
|
9638
10018
|
const explicitMaxFiles = typeof args.maxFiles === 'number' && !Number.isNaN(args.maxFiles);
|
|
9639
10019
|
let maxFiles = (0, utils_1.clamp)(args.maxFiles || budget.defaultMaxFiles, 1, 20);
|
|
9640
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);
|
|
9641
10023
|
const interpretationQuery = feature('queryAsInterpretationSurvey', query_utils_1.queryAsInterpretationSurvey);
|
|
9642
10024
|
const testOnlyInterpretation = feature('queryAsTestOnlyInterpretation', query_utils_1.queryAsTestOnlyInterpretation);
|
|
9643
10025
|
const crossModuleFlow = feature('queryAsCrossModuleFlowSurvey', query_utils_1.queryAsCrossModuleFlowSurvey) || plan?.intent === 'flow';
|
|
@@ -9649,19 +10031,28 @@ class ToolHandler {
|
|
|
9649
10031
|
? `${queryFileBasenames[0]} ${query}`
|
|
9650
10032
|
: queryFileBasenames.length === 1
|
|
9651
10033
|
? `${queryFileBasenames[0]} ${query}`
|
|
9652
|
-
:
|
|
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';
|
|
9653
10039
|
const subgraph = await cg.findRelevantContext(contextQuery, {
|
|
9654
10040
|
...contextOpts,
|
|
9655
|
-
...(
|
|
9656
|
-
symbols:
|
|
9657
|
-
|
|
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),
|
|
9658
10049
|
} } : {}),
|
|
9659
10050
|
});
|
|
9660
|
-
// Path-first
|
|
9661
|
-
//
|
|
9662
|
-
|
|
9663
|
-
|
|
9664
|
-
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) {
|
|
9665
10056
|
let hits = [];
|
|
9666
10057
|
try {
|
|
9667
10058
|
hits = cg.searchNodes(base, { limit: 50 });
|
|
@@ -9670,7 +10061,15 @@ class ToolHandler {
|
|
|
9670
10061
|
continue;
|
|
9671
10062
|
}
|
|
9672
10063
|
for (const r of hits) {
|
|
9673
|
-
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)
|
|
9674
10073
|
continue;
|
|
9675
10074
|
if (!subgraph.nodes.has(r.node.id)) {
|
|
9676
10075
|
subgraph.nodes.set(r.node.id, r.node);
|
|
@@ -9681,10 +10080,26 @@ class ToolHandler {
|
|
|
9681
10080
|
}
|
|
9682
10081
|
const literalSource = this.renderLiteralSource(cg, subgraph);
|
|
9683
10082
|
if (subgraph.nodes.size === 0) {
|
|
9684
|
-
|
|
9685
|
-
|
|
9686
|
-
|
|
9687
|
-
|
|
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
|
+
});
|
|
9688
10103
|
}
|
|
9689
10104
|
// Seed import nodes for @kit.* / *Kit names (and named symbols like taskpool).
|
|
9690
10105
|
const importTerms = (0, query_utils_1.extractImportSearchTerms)(query);
|
|
@@ -9852,7 +10267,7 @@ class ToolHandler {
|
|
|
9852
10267
|
const raw = isQual ? this.findAllSymbols(cg, t).nodes : cg.getNodesByName(t);
|
|
9853
10268
|
let cands = raw
|
|
9854
10269
|
.filter((n) => SEED_KINDS.has(n.kind) && !isTestPath(n.filePath)
|
|
9855
|
-
&& !(
|
|
10270
|
+
&& !(exploreSourceScope === 'local' && (0, arkts_1.isOhosApiFilePath)(n.filePath)))
|
|
9856
10271
|
.sort((a, b) => {
|
|
9857
10272
|
// Prefer callables over types when both share a name, then body size.
|
|
9858
10273
|
const ac = CALLABLE.has(a.kind) ? 1 : 0;
|
|
@@ -10213,10 +10628,12 @@ class ToolHandler {
|
|
|
10213
10628
|
// dropped, so the budget never fills with incidental files. Guarded so it
|
|
10214
10629
|
// never prunes below 2.
|
|
10215
10630
|
if (maxGraph > 0) {
|
|
10631
|
+
const litKeep = new Set((subgraph.literalEvidence?.hits ?? []).map((h) => h.filePath).filter(Boolean));
|
|
10216
10632
|
const gated = relevantFiles.filter(([fp]) => (fileGraphScore.get(fp) ?? 0) >= maxGraph * 0.06
|
|
10217
10633
|
|| centralFiles.has(fp)
|
|
10218
10634
|
|| entryFiles.has(fp)
|
|
10219
10635
|
|| changeSurfaceFiles.has(fp)
|
|
10636
|
+
|| litKeep.has(fp)
|
|
10220
10637
|
|| (fileTermHits.get(fp) ?? 0) >= 2);
|
|
10221
10638
|
if (gated.length >= 2)
|
|
10222
10639
|
relevantFiles = gated;
|
|
@@ -10241,6 +10658,22 @@ class ToolHandler {
|
|
|
10241
10658
|
// tier so it isn't buried under files that merely share surface words (#1064).
|
|
10242
10659
|
for (const fp of changeSurfaceFiles)
|
|
10243
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
|
+
}
|
|
10244
10677
|
// Multi-term corroboration tier: a file that is BOTH (a) an entry/central file
|
|
10245
10678
|
// (a search root, named seed, or graph-central hub — i.e. structurally part of
|
|
10246
10679
|
// the answer) AND (b) matched by ≥2 DISTINCT query terms must not be buried by
|
|
@@ -10264,7 +10697,7 @@ class ToolHandler {
|
|
|
10264
10697
|
const sortedFiles = relevantFiles.sort((a, b) => {
|
|
10265
10698
|
const aPath = a[0].toLowerCase();
|
|
10266
10699
|
const bPath = b[0].toLowerCase();
|
|
10267
|
-
if (
|
|
10700
|
+
if (exploreSourceScope === 'local') {
|
|
10268
10701
|
const sdkOrder = Number((0, arkts_1.isOhosApiFilePath)(a[0])) - Number((0, arkts_1.isOhosApiFilePath)(b[0]));
|
|
10269
10702
|
if (sdkOrder)
|
|
10270
10703
|
return sdkOrder;
|
|
@@ -10273,6 +10706,17 @@ class ToolHandler {
|
|
|
10273
10706
|
- Number((subgraph.literalEvidence?.hits ?? []).some((hit) => hit.filePath === a[0]));
|
|
10274
10707
|
if (literalOrder)
|
|
10275
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;
|
|
10276
10720
|
// Query-named file (LocationController.ets in the question) before partial
|
|
10277
10721
|
// substring matches (control.ets matching "Controller" inside LocationController).
|
|
10278
10722
|
const aExactBase = (0, query_utils_1.fileMatchesQueryBasename)(a[0], queryFileBasenames) ? 1 : 0;
|
|
@@ -11345,7 +11789,15 @@ class ToolHandler {
|
|
|
11345
11789
|
const headerSuffix = omittedCount > 0
|
|
11346
11790
|
? `${headerSymbols.join(', ')}, +${omittedCount} more`
|
|
11347
11791
|
: headerSymbols.join(', ');
|
|
11348
|
-
|
|
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
|
+
}
|
|
11349
11801
|
// The total cap bounds INCIDENTAL files only. A file that DEFINES a symbol
|
|
11350
11802
|
// the agent named (or that's on the flow spine) renders even when the
|
|
11351
11803
|
// nominal total is used up — it's the answer, and the set is bounded by
|
|
@@ -11639,6 +12091,106 @@ class ToolHandler {
|
|
|
11639
12091
|
}
|
|
11640
12092
|
return { text: sections.join('\n\n'), files, nodes };
|
|
11641
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
|
+
}
|
|
11642
12194
|
/**
|
|
11643
12195
|
* An explore response plus the record of what it emitted (CG-17). The record
|
|
11644
12196
|
* rides the result only as far as {@link execute}, which files it into the
|
|
@@ -11646,17 +12198,42 @@ class ToolHandler {
|
|
|
11646
12198
|
*/
|
|
11647
12199
|
exploreResult(text, emission) {
|
|
11648
12200
|
const meta = (0, explore_repeat_guard_1.inferExplorePartialMeta)(text);
|
|
11649
|
-
|
|
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);
|
|
11650
12223
|
result[explore_session_state_1.EXPLORE_EMISSION_KEY] = {
|
|
11651
12224
|
...emission,
|
|
11652
|
-
evidenceStatus
|
|
11653
|
-
partial: emission.partial ?? (
|
|
12225
|
+
evidenceStatus,
|
|
12226
|
+
partial: emission.partial ?? (evidenceStatus !== 'complete' ? true : meta.partial),
|
|
11654
12227
|
nextAnchor: emission.nextAnchor ?? meta.nextAnchor,
|
|
11655
12228
|
};
|
|
12229
|
+
result = this.prependHarmonySeamNotes(result, emission.projectRoot, result[explore_session_state_1.EXPLORE_EMISSION_KEY]);
|
|
11656
12230
|
return result;
|
|
11657
12231
|
}
|
|
11658
12232
|
/** Attach a minimal session emission when a compact/light path used textResult. */
|
|
11659
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);
|
|
11660
12237
|
if (result[explore_session_state_1.EXPLORE_EMISSION_KEY]) {
|
|
11661
12238
|
const em = result[explore_session_state_1.EXPLORE_EMISSION_KEY];
|
|
11662
12239
|
if (em.partial === undefined || !em.nextAnchor) {
|
|
@@ -11666,6 +12243,7 @@ class ToolHandler {
|
|
|
11666
12243
|
if (!em.nextAnchor && meta.nextAnchor)
|
|
11667
12244
|
em.nextAnchor = meta.nextAnchor;
|
|
11668
12245
|
}
|
|
12246
|
+
result = this.prependHarmonySeamNotes(result, projectRoot, em);
|
|
11669
12247
|
return result;
|
|
11670
12248
|
}
|
|
11671
12249
|
const text = result.content?.[0]?.text ?? '';
|
|
@@ -11839,7 +12417,8 @@ class ToolHandler {
|
|
|
11839
12417
|
const nodes = cg.getNodesInFile(filePath)
|
|
11840
12418
|
.filter((n) => n.kind !== 'file' && n.kind !== 'import' && n.kind !== 'export')
|
|
11841
12419
|
.sort((a, b) => a.startLine - b.startLine);
|
|
11842
|
-
const dependents = cg.getFileDependents(filePath)
|
|
12420
|
+
const dependents = cg.getFileDependents(filePath)
|
|
12421
|
+
.filter((p) => !p.includes('@dummy') && !p.replace(/\\/g, '/').split('/').pop()?.startsWith('@dummy'));
|
|
11843
12422
|
// Compact, one-line blast radius (homegraph's value-add over a plain Read).
|
|
11844
12423
|
const depSummary = dependents.length
|
|
11845
12424
|
? `used by ${dependents.length} file${dependents.length === 1 ? '' : 's'}: ${dependents.slice(0, 8).join(', ')}${dependents.length > 8 ? `, +${dependents.length - 8} more` : ''}`
|
|
@@ -12019,6 +12598,8 @@ class ToolHandler {
|
|
|
12019
12598
|
for (const e of edges) {
|
|
12020
12599
|
if (seen.has(e.node.id))
|
|
12021
12600
|
continue;
|
|
12601
|
+
if ((0, query_utils_1.isMcpNoiseNode)(e.node))
|
|
12602
|
+
continue;
|
|
12022
12603
|
seen.add(e.node.id);
|
|
12023
12604
|
out.push(e);
|
|
12024
12605
|
}
|
|
@@ -12194,14 +12775,47 @@ class ToolHandler {
|
|
|
12194
12775
|
return this.textResult('No modules found in the project map.');
|
|
12195
12776
|
}
|
|
12196
12777
|
const FILE_CAP = 80;
|
|
12778
|
+
const projectRoot = cg.getProjectRoot();
|
|
12779
|
+
const hasHarmony = map.modules.some((m) => m.kind === 'harmony');
|
|
12197
12780
|
const lines = [
|
|
12198
12781
|
`**Project map** (phase=${map.phase})`,
|
|
12199
12782
|
`modules: ${map.modules.length} · files: ${map.fileCount}`,
|
|
12200
|
-
'',
|
|
12201
12783
|
];
|
|
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 */
|
|
12792
|
+
}
|
|
12793
|
+
if (hasHarmony) {
|
|
12794
|
+
lines.push('modules from `build-profile.json5` — skeleton map only (not call edges; use `homegraph_explore` for route_map → page)');
|
|
12795
|
+
}
|
|
12796
|
+
lines.push('');
|
|
12202
12797
|
for (const m of map.modules) {
|
|
12203
12798
|
const rootLabel = m.rootPath || '.';
|
|
12204
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
|
+
}
|
|
12205
12819
|
if (m.files && m.files.length > 0) {
|
|
12206
12820
|
const shown = m.files.slice(0, FILE_CAP);
|
|
12207
12821
|
for (const f of shown) {
|
|
@@ -12213,6 +12827,42 @@ class ToolHandler {
|
|
|
12213
12827
|
}
|
|
12214
12828
|
lines.push('');
|
|
12215
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
|
+
}
|
|
12216
12866
|
return this.textResult(lines.join('\n').trimEnd());
|
|
12217
12867
|
}
|
|
12218
12868
|
/**
|