archgraph-argo 0.26.0 → 0.26.2
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.
|
@@ -46,16 +46,27 @@ function backendOf(tool) {
|
|
|
46
46
|
|
|
47
47
|
function classifyQuery(tool, input) {
|
|
48
48
|
const n = String(tool || '');
|
|
49
|
+
if (n.includes('question')) return 'human-wait';
|
|
50
|
+
if (n.includes('task')) return 'subagent';
|
|
51
|
+
if (n.includes('bash')) return 'bash';
|
|
49
52
|
if (n.includes('getSystemArchitecture') || n.includes('memory_search')) return 'semantic';
|
|
50
53
|
if (n.includes('queryNeo4jGraph')) return 'structured-cypher';
|
|
51
54
|
if (n.includes('getIntentElementContext') || n.includes('getArchitectureViewContext')) return 'structured-context';
|
|
55
|
+
if (n.includes('validateSystemArchitecture') || n.includes('runArchitectureTests')) return 'validate';
|
|
56
|
+
if (n.includes('initializeWorkspace')) return 'init';
|
|
57
|
+
if (n.includes('previewSystemArchitectureMutation') || GRAPH_WRITE_TOOLS.some(t => n.includes(t))) return 'mutation';
|
|
52
58
|
if (n.includes('read')) return 'file-read';
|
|
53
59
|
if (n.includes('grep')) return 'grep';
|
|
54
60
|
if (n.includes('glob') || n.includes('list')) return 'list';
|
|
55
|
-
if (
|
|
61
|
+
if (n.includes('edit') || n.includes('write')) return 'edit';
|
|
62
|
+
if (n.includes('todowrite')) return 'todo';
|
|
56
63
|
return 'other';
|
|
57
64
|
}
|
|
58
65
|
|
|
66
|
+
// Tools that block on a HUMAN (time spent waiting for the person, not the agent).
|
|
67
|
+
const HUMAN_WAIT_TOOLS = ['question'];
|
|
68
|
+
const CONTEXT_BLOWUP_TOKENS = 100000;
|
|
69
|
+
|
|
59
70
|
function estimateTokens(text) {
|
|
60
71
|
if (!text) return 0;
|
|
61
72
|
const s = String(text);
|
|
@@ -93,6 +104,31 @@ function eventTime(e) {
|
|
|
93
104
|
return typeof e.timestamp === 'number' ? e.timestamp : null;
|
|
94
105
|
}
|
|
95
106
|
|
|
107
|
+
function isExportJson(text) {
|
|
108
|
+
const t = String(text || '').trim();
|
|
109
|
+
if (!t.startsWith('{')) return false;
|
|
110
|
+
try { const j = JSON.parse(t); return !!(j && Array.isArray(j.messages)); } catch (_) { return false; }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Accept BOTH the live event stream (opencode run --format json, NDJSON) and the
|
|
114
|
+
// session export JSON (opencode export -> { info, messages:[{info, parts:[...]}] }).
|
|
115
|
+
function exportToNdjson(text) {
|
|
116
|
+
const j = JSON.parse(text);
|
|
117
|
+
const ev = [];
|
|
118
|
+
for (const m of j.messages || []) {
|
|
119
|
+
const info = m.info || {};
|
|
120
|
+
const ts = info.time && (info.time.created || info.time.completed);
|
|
121
|
+
for (const p of m.parts || []) {
|
|
122
|
+
if (!p || typeof p !== 'object') continue;
|
|
123
|
+
if (p.type === 'tool') ev.push({ type: 'tool', timestamp: (p.state && p.state.time && p.state.time.end) || ts, part: p });
|
|
124
|
+
else if (p.type === 'step-start') ev.push({ type: 'step_start', timestamp: ts, part: p });
|
|
125
|
+
else if (p.type === 'text') ev.push({ type: 'text', timestamp: ts, part: p });
|
|
126
|
+
}
|
|
127
|
+
if (info.role === 'assistant' && info.tokens) ev.push({ type: 'step_finish', timestamp: info.time && info.time.completed, part: { tokens: info.tokens, cost: info.cost } });
|
|
128
|
+
}
|
|
129
|
+
return ev.map(e => JSON.stringify(e)).join('\n') + '\n';
|
|
130
|
+
}
|
|
131
|
+
|
|
96
132
|
function parseSession(text) {
|
|
97
133
|
const raw = String(text || '');
|
|
98
134
|
const toolCalls = [];
|
|
@@ -150,13 +186,18 @@ function diagnose(session, opts = {}) {
|
|
|
150
186
|
const byQueryClass = {};
|
|
151
187
|
const sigCount = {};
|
|
152
188
|
const pathReads = {};
|
|
153
|
-
let toolMs = 0; let errors = 0; let empty = 0;
|
|
189
|
+
let toolMs = 0; let humanWaitMs = 0; let errors = 0; let empty = 0;
|
|
154
190
|
for (const c of tc) {
|
|
155
|
-
const
|
|
156
|
-
b.calls += 1; b.ms += c.durationMs || 0;
|
|
157
|
-
toolMs += c.durationMs || 0;
|
|
191
|
+
const isHuman = c.queryClass === 'human-wait' || HUMAN_WAIT_TOOLS.some(t => String(c.tool).includes(t));
|
|
158
192
|
const bt = byTool[c.tool] || (byTool[c.tool] = { calls: 0, ms: 0, tokens: 0, errors: 0 });
|
|
159
193
|
bt.calls += 1; bt.ms += c.durationMs || 0; bt.tokens += c.outputTokens || 0;
|
|
194
|
+
if (isHuman) {
|
|
195
|
+
humanWaitMs += c.durationMs || 0; // human time is NOT agent/tool work
|
|
196
|
+
} else {
|
|
197
|
+
const b = byBackend[c.backend] || (byBackend[c.backend] = { calls: 0, ms: 0 });
|
|
198
|
+
b.calls += 1; b.ms += c.durationMs || 0;
|
|
199
|
+
toolMs += c.durationMs || 0;
|
|
200
|
+
}
|
|
160
201
|
if (c.ok === false) { bt.errors += 1; errors += 1; }
|
|
161
202
|
if ((c.outputBytes || 0) < 2) empty += 1;
|
|
162
203
|
byQueryClass[c.queryClass] = (byQueryClass[c.queryClass] || 0) + 1;
|
|
@@ -185,17 +226,19 @@ function diagnose(session, opts = {}) {
|
|
|
185
226
|
const topByTokens = [...tc].sort((a, b) => (b.outputTokens || 0) - (a.outputTokens || 0)).slice(0, 5).map(c => ({ tool: c.tool, tokens: c.outputTokens, preview: c.inputPreview }));
|
|
186
227
|
|
|
187
228
|
const wallMs = opts.wallMs != null ? opts.wallMs : session.wallMs;
|
|
188
|
-
const modelMs = wallMs != null ? Math.max(0, wallMs - toolMs) : null;
|
|
229
|
+
const modelMs = wallMs != null ? Math.max(0, wallMs - toolMs - humanWaitMs) : null;
|
|
230
|
+
const blowups = cumulativeInput.map((v, i) => ({ step: i, input: v })).filter(x => x.input >= CONTEXT_BLOWUP_TOKENS).sort((a, b) => b.input - a.input);
|
|
189
231
|
|
|
190
232
|
return {
|
|
191
233
|
schemaVersion: SCHEMA_VERSION, bundleVersion: BUNDLE_VERSION, generatedAt: new Date().toISOString(),
|
|
192
234
|
workspace: opts.workspace || null, sessionId: opts.sessionId || null,
|
|
193
235
|
overview: {
|
|
194
236
|
steps: session.steps, toolCalls: tc.length, roundTrips: countRoundTrips(tc),
|
|
195
|
-
wallMs, modelMs, mcpMs: byBackend.graph.ms, repoToolMs: byBackend.repo.ms, toolMs,
|
|
237
|
+
wallMs, modelMs, mcpMs: byBackend.graph.ms, repoToolMs: byBackend.repo.ms, toolMs, humanWaitMs,
|
|
196
238
|
tokensIn: session.tokensIn, tokensOut: session.tokensOut, tokensReasoning: session.tokensReasoning, tokens: session.tokens,
|
|
197
239
|
cost: session.cost, toolErrors: errors, emptyResults: empty,
|
|
198
240
|
distinctSignatures: new Set(tc.map(c => c.signature)).size,
|
|
241
|
+
contextBlowup: { thresholdTokens: CONTEXT_BLOWUP_TOKENS, count: blowups.length, max: blowups.length ? blowups[0].input : 0, top: blowups.slice(0, 5) },
|
|
199
242
|
},
|
|
200
243
|
byTool, byBackend, byQueryClass,
|
|
201
244
|
tokenGrowth: { perStepInput: cumulativeInput, steps: cumulativeInput.length },
|
|
@@ -207,12 +250,13 @@ function diagnose(session, opts = {}) {
|
|
|
207
250
|
},
|
|
208
251
|
topOffendersByTime: topByMs,
|
|
209
252
|
topOffendersByTokens: topByTokens,
|
|
210
|
-
hints: buildHints({ toolCalls: tc.length, duplicates: duplicates.length, emptyOrError: errors + empty, repeatedReads: repeatedReads.length, noProgressStreak: best, modelMs, mcpMs: byBackend.graph.ms, repoToolMs: byBackend.repo.ms, roundTrips: countRoundTrips(tc) }),
|
|
253
|
+
hints: buildHints({ toolCalls: tc.length, duplicates: duplicates.length, emptyOrError: errors + empty, repeatedReads: repeatedReads.length, noProgressStreak: best, modelMs, mcpMs: byBackend.graph.ms, repoToolMs: byBackend.repo.ms, roundTrips: countRoundTrips(tc), blowups: blowups.length, blowupMax: blowups.length ? blowups[0].input : 0 }),
|
|
211
254
|
};
|
|
212
255
|
}
|
|
213
256
|
|
|
214
257
|
function buildHints(m) {
|
|
215
258
|
const hints = [];
|
|
259
|
+
if (m.blowups > 0) hints.push(`上下文尖峰 ${m.blowups} 个 step(最大 ${m.blowupMax} tokens)→ 多由大工具输出造成;优先考虑截断/分页/摘要工具输出(observation masking)、先摘要后精读(对所有场景统一生效,勿只针对单会话)。`);
|
|
216
260
|
if (m.duplicates > 0) hints.push(`重复/近似重复调用 ${m.duplicates} 组 → 考虑结果缓存或查询归一(同参不重搜)。`);
|
|
217
261
|
if (m.emptyOrError > 0) hints.push(`空结果/失败 ${m.emptyOrError} 次 → 考虑改进查询构造/回退策略,避免"空手→换词→再搜"的循环。`);
|
|
218
262
|
if (m.repeatedReads > 0) hints.push(`同一文件被重复读 ${m.repeatedReads} 处 → 考虑读取缓存或先摘要后精读。`);
|
|
@@ -238,7 +282,7 @@ function renderDiagnosis(m, meta) {
|
|
|
238
282
|
L.push(`| 轮次 steps | ${m.overview.steps} |`);
|
|
239
283
|
L.push(`| 工具调用 | ${m.overview.toolCalls}(去重签名 ${m.overview.distinctSignatures}) |`);
|
|
240
284
|
L.push(`| 图↔仓往返 | ${m.overview.roundTrips} |`);
|
|
241
|
-
L.push(`| 墙钟 | ${fmtMs(m.overview.wallMs)} = 模型 ${fmtMs(m.overview.modelMs)} + MCP ${fmtMs(m.overview.mcpMs)} + 仓 ${fmtMs(m.overview.repoToolMs)} |`);
|
|
285
|
+
L.push(`| 墙钟 | ${fmtMs(m.overview.wallMs)} = 模型 ${fmtMs(m.overview.modelMs)} + MCP ${fmtMs(m.overview.mcpMs)} + 仓 ${fmtMs(m.overview.repoToolMs)} + 人类等待 ${fmtMs(m.overview.humanWaitMs)} |`);
|
|
242
286
|
L.push(`| tokens | 总 ${m.overview.tokens}(in ${m.overview.tokensIn} / out ${m.overview.tokensOut} / reason ${m.overview.tokensReasoning}) |`);
|
|
243
287
|
L.push(`| 工具失败 / 空结果 | ${m.overview.toolErrors} / ${m.overview.emptyResults} |`);
|
|
244
288
|
L.push('');
|
|
@@ -264,6 +308,12 @@ function renderDiagnosis(m, meta) {
|
|
|
264
308
|
L.push('');
|
|
265
309
|
L.push(`每步 input tokens(累计上下文规模):${m.tokenGrowth.perStepInput.join(', ') || '(无)'}`);
|
|
266
310
|
L.push('');
|
|
311
|
+
const cb = m.overview.contextBlowup || { count: 0, max: 0, top: [] };
|
|
312
|
+
L.push(`## 上下文尖峰(≥ ${cb.thresholdTokens} tokens 的 step)`);
|
|
313
|
+
L.push('');
|
|
314
|
+
L.push(`- 尖峰数:${cb.count} 最大:${cb.max} tokens`);
|
|
315
|
+
for (const b of cb.top) L.push(` - step ${b.step}: ${b.input} input tokens`);
|
|
316
|
+
L.push('');
|
|
267
317
|
L.push(`## 诊断建议(启发式,供 Agent 复核)`);
|
|
268
318
|
L.push('');
|
|
269
319
|
for (const h of m.hints) L.push(`- ${h}`);
|
|
@@ -281,7 +331,8 @@ function fmtMs(ms) {
|
|
|
281
331
|
|
|
282
332
|
function writeBundle(opts) {
|
|
283
333
|
const workspace = opts.workspace || process.cwd();
|
|
284
|
-
const
|
|
334
|
+
const rawInput = fs.readFileSync(opts.session, 'utf8');
|
|
335
|
+
const sessionText = isExportJson(rawInput) ? exportToNdjson(rawInput) : rawInput;
|
|
285
336
|
const session = parseSession(sessionText);
|
|
286
337
|
const sessionId = opts.sessionId || inferSessionId(sessionText) || 'session';
|
|
287
338
|
const outDir = opts.out || path.join(workspace, '.argo', 'temp', 'diagnosis', sessionId);
|
|
@@ -351,6 +402,6 @@ function main(argv) {
|
|
|
351
402
|
return 0;
|
|
352
403
|
}
|
|
353
404
|
|
|
354
|
-
module.exports = { BUNDLE_VERSION, SCHEMA_VERSION, parseSession, classifyQuery, backendOf, signature, diagnose, renderDiagnosis, writeBundle, sliceCostLog, main };
|
|
405
|
+
module.exports = { BUNDLE_VERSION, SCHEMA_VERSION, parseSession, classifyQuery, backendOf, signature, diagnose, renderDiagnosis, writeBundle, sliceCostLog, isExportJson, exportToNdjson, main };
|
|
355
406
|
|
|
356
407
|
if (require.main === module) process.exit(main());
|
|
@@ -330,6 +330,8 @@ function intentElementContextInputSchema() {
|
|
|
330
330
|
dependentDepth: { type: 'number', description: 'Default: 1. Semantic dependents that rely on the focus element.' },
|
|
331
331
|
associationDepth: { type: 'number', description: 'Default: 1. Association neighbors are expanded at least one layer.' },
|
|
332
332
|
associationNeighborDependencyDepth: { type: 'number', description: 'Default: 0. Optional dependency expansion from association neighbors.' },
|
|
333
|
+
includeAttributes: { type: 'boolean', description: 'Default: false. Include `attributes` (commit/session/release ledgers) verbatim; omitted by default from this structural read (the focus element always keeps its own).' },
|
|
334
|
+
includeTestcases: { type: 'boolean', description: 'Default: false. Include member `testcases` verbatim; omitted by default from this structural read.' },
|
|
333
335
|
},
|
|
334
336
|
additionalProperties: false,
|
|
335
337
|
};
|
|
@@ -461,6 +461,8 @@ function intentElementContextInputSchema() {
|
|
|
461
461
|
dependentDepth: { type: 'number', description: 'Default: 1. Semantic dependents that rely on the focus element.' },
|
|
462
462
|
associationDepth: { type: 'number', description: 'Default: 1. Association neighbors are expanded at least one layer.' },
|
|
463
463
|
associationNeighborDependencyDepth: { type: 'number', description: 'Default: 0. Optional dependency expansion from association neighbors.' },
|
|
464
|
+
includeAttributes: { type: 'boolean', description: 'Default: false. Include `attributes` (commit/session/release ledgers) verbatim. Omitted by default from this structural read; the focus element always keeps its own. Semantic retrieval embeds attributes, so semantic hits keep them.' },
|
|
465
|
+
includeTestcases: { type: 'boolean', description: 'Default: false. Include member `testcases` verbatim. Omitted by default from this structural read (bookkeeping); pass true when you need acceptance cases. Semantic retrieval embeds testcase descriptions, so semantic hits keep them.' },
|
|
464
466
|
},
|
|
465
467
|
additionalProperties: false,
|
|
466
468
|
};
|
|
@@ -475,6 +477,8 @@ function viewContextInputSchema() {
|
|
|
475
477
|
view_id: { type: 'string', description: 'The id of the view to resolve.' },
|
|
476
478
|
includeParentElement: { type: 'boolean', description: 'Default: true. Resolve the parent element referenced by the view.' },
|
|
477
479
|
includeChildViews: { type: 'boolean', description: 'Default: false. Include child views declared by member elements through subdiagram_views.' },
|
|
480
|
+
includeAttributes: { type: 'boolean', description: 'Default: false. Include member/relationship `attributes` (commit/session/release ledgers) verbatim. Omitted by default from this structural read (bookkeeping); pass true when you need provenance.' },
|
|
481
|
+
includeTestcases: { type: 'boolean', description: 'Default: false. Include member `testcases` verbatim. Omitted by default from this structural read; pass true for acceptance-case lookups.' },
|
|
478
482
|
includeEaGeometry: { type: 'boolean', description: 'Default: false (opt-in). When true, additionally resolve the diagram GEOMETRY (element boxes + connector line routes) for this view from the workspace EA model (.qea) and return it under a `geometry` field aligned by schema id with the resolved members. Each geometry relationship carries: `path` (the EA route from t_diagramlinks.Path, "" when EA auto-routes), `points` (the parsed [{x,y}] waypoints), `edge` (the EDGE route-style token or null) and `geometry` (the raw SX/SY/EX/EY override string, which contains NO waypoints). By default the EA model is never touched and no `geometry` field is returned; a missing EA model/diagram yields geometry.present=false, never an error.' },
|
|
479
483
|
},
|
|
480
484
|
additionalProperties: false,
|
|
@@ -615,6 +619,55 @@ function validateDocument(document, schema, options = {}) {
|
|
|
615
619
|
return errors;
|
|
616
620
|
}
|
|
617
621
|
|
|
622
|
+
// Agent-facing projection of canonical records for STRUCTURAL reads (view
|
|
623
|
+
// membership / intent-element subgraph). `attributes` (commit/session/release
|
|
624
|
+
// ledgers) and `testcases` are bookkeeping: measured as ~52% of the element
|
|
625
|
+
// bytes but NOT needed to answer structural reads, so they are omitted by
|
|
626
|
+
// default and returned only on opt-in (includeAttributes / includeTestcases).
|
|
627
|
+
//
|
|
628
|
+
// This is NOT summarisation: every retained field is returned verbatim. It is
|
|
629
|
+
// also NOT the semantic match surface — semantic retrieval embeds attributes and
|
|
630
|
+
// testcase descriptions (semanticRecordText.js), so semantic results keep them;
|
|
631
|
+
// and the focus element of an intent-element read always keeps its own.
|
|
632
|
+
function byteLenLocal(value) {
|
|
633
|
+
try { return Buffer.byteLength(JSON.stringify(value)); } catch (_) { return 0; }
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function projectAgentFields(record, opts = {}) {
|
|
637
|
+
const value = clone(record);
|
|
638
|
+
let omittedAttributes = 0;
|
|
639
|
+
let omittedTestcases = 0;
|
|
640
|
+
let omittedBytes = 0;
|
|
641
|
+
if (!opts.includeAttributes && Array.isArray(value.attributes) && value.attributes.length) {
|
|
642
|
+
omittedAttributes = value.attributes.length;
|
|
643
|
+
omittedBytes += byteLenLocal(value.attributes);
|
|
644
|
+
delete value.attributes;
|
|
645
|
+
}
|
|
646
|
+
if (!opts.includeTestcases && Array.isArray(value.testcases) && value.testcases.length) {
|
|
647
|
+
omittedTestcases = value.testcases.length;
|
|
648
|
+
omittedBytes += byteLenLocal(value.testcases);
|
|
649
|
+
delete value.testcases;
|
|
650
|
+
}
|
|
651
|
+
return { value, omittedAttributes, omittedTestcases, omittedBytes };
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function buildAgentProjection(omitted) {
|
|
655
|
+
if (!omitted || (omitted.attributes === 0 && omitted.testcases === 0)) {
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
// Only attach the explanation when the omission actually saves more than the
|
|
659
|
+
// note costs, so trimming a read can NEVER make it larger (cross-scenario
|
|
660
|
+
// no-degradation). The opt-in flags are documented in the tool description.
|
|
661
|
+
if ((omitted.bytes || 0) < 160) {
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
return {
|
|
665
|
+
attributesOmitted: omitted.attributes,
|
|
666
|
+
testcasesOmitted: omitted.testcases,
|
|
667
|
+
note: 'includeAttributes/includeTestcases=true to include (omitted by default; semantic hits keep them).',
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
618
671
|
function buildIntentElementContext(context, args = {}) {
|
|
619
672
|
const profile = args.profile || 'generic-agent';
|
|
620
673
|
const focusResult = resolveFocusElement(context.document, args);
|
|
@@ -697,7 +750,29 @@ function buildIntentElementContext(context, args = {}) {
|
|
|
697
750
|
associationDepth,
|
|
698
751
|
});
|
|
699
752
|
|
|
700
|
-
|
|
753
|
+
const subgraph = buildNativeSubgraph(context.document, includedElementIds, includedRelationshipIds);
|
|
754
|
+
const includeAttributes = args.includeAttributes === true;
|
|
755
|
+
const includeTestcases = args.includeTestcases === true;
|
|
756
|
+
const omitted = { attributes: 0, testcases: 0, bytes: 0 };
|
|
757
|
+
if (!(includeAttributes && includeTestcases)) {
|
|
758
|
+
subgraph.elements = subgraph.elements.map((element) => {
|
|
759
|
+
if (element.id === focusElement.id) {
|
|
760
|
+
return element; // the focus element keeps its own bookkeeping
|
|
761
|
+
}
|
|
762
|
+
const projected = projectAgentFields(element, { includeAttributes, includeTestcases });
|
|
763
|
+
omitted.attributes += projected.omittedAttributes;
|
|
764
|
+
omitted.testcases += projected.omittedTestcases;
|
|
765
|
+
omitted.bytes += projected.omittedBytes;
|
|
766
|
+
return projected.value;
|
|
767
|
+
});
|
|
768
|
+
subgraph.relationships = subgraph.relationships.map((relationship) => {
|
|
769
|
+
const projected = projectAgentFields(relationship, { includeAttributes });
|
|
770
|
+
omitted.attributes += projected.omittedAttributes;
|
|
771
|
+
omitted.bytes += projected.omittedBytes;
|
|
772
|
+
return projected.value;
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
const result = {
|
|
701
776
|
status: 'passed',
|
|
702
777
|
query: {
|
|
703
778
|
architecturePath: context.graphPath.relativePath,
|
|
@@ -711,12 +786,15 @@ function buildIntentElementContext(context, args = {}) {
|
|
|
711
786
|
traversalMode: 'archimate-semantic',
|
|
712
787
|
},
|
|
713
788
|
focusElementId: focusElement.id,
|
|
714
|
-
subgraph
|
|
789
|
+
subgraph,
|
|
715
790
|
boundary,
|
|
716
791
|
explorationHints,
|
|
717
792
|
workContext: {},
|
|
718
793
|
diagnostics: [],
|
|
719
794
|
};
|
|
795
|
+
const projection = buildAgentProjection(omitted);
|
|
796
|
+
if (projection) result.projection = projection;
|
|
797
|
+
return result;
|
|
720
798
|
}
|
|
721
799
|
|
|
722
800
|
// ---------------------------------------------------------------------------
|
|
@@ -786,12 +864,23 @@ function buildViewContext(context, args = {}) {
|
|
|
786
864
|
const elementById = new Map((document.elements || []).map(element => [element.id, element]));
|
|
787
865
|
const relationshipById = new Map((document.relationships || []).map(relationship => [relationship.id, relationship]));
|
|
788
866
|
|
|
867
|
+
const includeAttributes = args.includeAttributes === true;
|
|
868
|
+
const includeTestcases = args.includeTestcases === true;
|
|
869
|
+
const omitted = { attributes: 0, testcases: 0, bytes: 0 };
|
|
870
|
+
const project = (record) => {
|
|
871
|
+
const projected = projectAgentFields(record, { includeAttributes, includeTestcases });
|
|
872
|
+
omitted.attributes += projected.omittedAttributes;
|
|
873
|
+
omitted.testcases += projected.omittedTestcases;
|
|
874
|
+
omitted.bytes += projected.omittedBytes;
|
|
875
|
+
return projected.value;
|
|
876
|
+
};
|
|
877
|
+
|
|
789
878
|
const elements = [];
|
|
790
879
|
const missingElementIds = [];
|
|
791
880
|
for (const elementId of view.included_elements || []) {
|
|
792
881
|
const element = elementById.get(elementId);
|
|
793
882
|
if (element) {
|
|
794
|
-
elements.push(
|
|
883
|
+
elements.push(project(element));
|
|
795
884
|
} else {
|
|
796
885
|
missingElementIds.push(elementId);
|
|
797
886
|
}
|
|
@@ -802,7 +891,7 @@ function buildViewContext(context, args = {}) {
|
|
|
802
891
|
for (const relationshipId of view.included_relationships || []) {
|
|
803
892
|
const relationship = relationshipById.get(relationshipId);
|
|
804
893
|
if (relationship) {
|
|
805
|
-
relationships.push(
|
|
894
|
+
relationships.push(project(relationship));
|
|
806
895
|
} else {
|
|
807
896
|
missingRelationshipIds.push(relationshipId);
|
|
808
897
|
}
|
|
@@ -812,7 +901,7 @@ function buildViewContext(context, args = {}) {
|
|
|
812
901
|
let parentElement = null;
|
|
813
902
|
if (includeParentElement && view.parent_element_id) {
|
|
814
903
|
const parent = elementById.get(view.parent_element_id);
|
|
815
|
-
parentElement = parent ?
|
|
904
|
+
parentElement = parent ? project(parent) : null;
|
|
816
905
|
}
|
|
817
906
|
|
|
818
907
|
const includeChildViews = args.includeChildViews === true;
|
|
@@ -847,6 +936,8 @@ function buildViewContext(context, args = {}) {
|
|
|
847
936
|
if (args.includeEaGeometry === true) {
|
|
848
937
|
result.geometry = readEaViewGeometry(context.workspaceRoot, viewId);
|
|
849
938
|
}
|
|
939
|
+
const projection = buildAgentProjection(omitted);
|
|
940
|
+
if (projection) result.projection = projection;
|
|
850
941
|
return result;
|
|
851
942
|
}
|
|
852
943
|
|
|
@@ -2750,7 +2841,7 @@ async function memorySearchTool(args = {}, dependencies = undefined) {
|
|
|
2750
2841
|
.filter(element => element && typeof element.semanticScore === 'number')
|
|
2751
2842
|
.sort((left, right) => right.semanticScore - left.semanticScore)
|
|
2752
2843
|
.slice(0, topK)
|
|
2753
|
-
.map(element => Object.freeze(memoryHitCard(element, maxDescLen)));
|
|
2844
|
+
.map(element => Object.freeze(memoryHitCard(element, maxDescLen, query)));
|
|
2754
2845
|
return {
|
|
2755
2846
|
status: 'passed',
|
|
2756
2847
|
query,
|
|
@@ -2763,7 +2854,7 @@ async function memorySearchTool(args = {}, dependencies = undefined) {
|
|
|
2763
2854
|
// Build one compact memory hit: id/name/type/score + an excerpt of the
|
|
2764
2855
|
// description bounded by maxDescLen, plus the full text length so the caller
|
|
2765
2856
|
// knows how much content exists and can decide whether to expand.
|
|
2766
|
-
function memoryHitCard(element, maxDescLen) {
|
|
2857
|
+
function memoryHitCard(element, maxDescLen, query) {
|
|
2767
2858
|
const description = typeof element.description === 'string' ? element.description : '';
|
|
2768
2859
|
const descriptionLength = description.length;
|
|
2769
2860
|
let excerpt = '';
|
|
@@ -2783,6 +2874,13 @@ function memoryHitCard(element, maxDescLen) {
|
|
|
2783
2874
|
card.truncated = true;
|
|
2784
2875
|
}
|
|
2785
2876
|
}
|
|
2877
|
+
// A memory match may be driven by an attribute/testcase (both are embedded);
|
|
2878
|
+
// surface the matching bookkeeping text so the agent sees WHY it hit without
|
|
2879
|
+
// a second call.
|
|
2880
|
+
const snippet = bookkeepingSnippet(element, 'Element', query);
|
|
2881
|
+
if (snippet) {
|
|
2882
|
+
card.matchedSnippet = snippet;
|
|
2883
|
+
}
|
|
2786
2884
|
return card;
|
|
2787
2885
|
}
|
|
2788
2886
|
|
|
@@ -3290,11 +3388,31 @@ function buildBusinessSemanticSummary(retrieved, query = {}) {
|
|
|
3290
3388
|
supplementaryReasons: Array.isArray(item.supplementaryReasons) ? [...item.supplementaryReasons] : [],
|
|
3291
3389
|
},
|
|
3292
3390
|
]));
|
|
3391
|
+
const recordById = new Map();
|
|
3392
|
+
const putRecord = (objectType, record, id) => { if (record && id) recordById.set(`${objectType}:${id}`, record); };
|
|
3393
|
+
for (const element of (source.closure && source.closure.elements) || []) putRecord('Element', element, element.id);
|
|
3394
|
+
for (const view of (source.viewClosure && source.viewClosure.views) || []) {
|
|
3395
|
+
putRecord('View', view, view.view_id);
|
|
3396
|
+
for (const member of view.memberElements || []) putRecord('Element', member, member.id);
|
|
3397
|
+
for (const member of view.memberRelationships || []) putRecord('ArchitectureRelationship', member, member.id);
|
|
3398
|
+
}
|
|
3399
|
+
for (const relationship of (source.endpointClosure && source.endpointClosure.relationships) || []) {
|
|
3400
|
+
putRecord('ArchitectureRelationship', relationship, relationship.id);
|
|
3401
|
+
if (relationship.source) putRecord('Element', relationship.source, relationship.source.id);
|
|
3402
|
+
if (relationship.target) putRecord('Element', relationship.target, relationship.target.id);
|
|
3403
|
+
}
|
|
3404
|
+
const ctx = {
|
|
3405
|
+
query,
|
|
3406
|
+
recordById,
|
|
3407
|
+
hitElementIds: buildHitIdSet(source, 'elements'),
|
|
3408
|
+
hitRelationshipIds: buildHitIdSet(source, 'relationships'),
|
|
3409
|
+
hitViewIds: buildHitIdSet(source, 'views'),
|
|
3410
|
+
};
|
|
3293
3411
|
const seedLimit = businessSummaryLimit(query);
|
|
3294
|
-
const semanticSeeds = summarizeSeeds(source.seedsByType, hitReasonByKey, seedLimit);
|
|
3295
|
-
const elements = summarizeElements(source, hitReasonByKey, seedLimit * 2);
|
|
3296
|
-
const relationships = summarizeRelationships(source, hitReasonByKey, seedLimit * 2);
|
|
3297
|
-
const views = summarizeViews(source, hitReasonByKey, seedLimit);
|
|
3412
|
+
const semanticSeeds = summarizeSeeds(source.seedsByType, hitReasonByKey, seedLimit, ctx);
|
|
3413
|
+
const elements = summarizeElements(source, hitReasonByKey, seedLimit * 2, ctx);
|
|
3414
|
+
const relationships = summarizeRelationships(source, hitReasonByKey, seedLimit * 2, ctx);
|
|
3415
|
+
const views = summarizeViews(source, hitReasonByKey, seedLimit, ctx);
|
|
3298
3416
|
const includedObjectIds = Object.freeze([
|
|
3299
3417
|
...elements.map(item => item.id),
|
|
3300
3418
|
...relationships.map(item => item.id),
|
|
@@ -3688,7 +3806,70 @@ function businessSummaryLimit(query) {
|
|
|
3688
3806
|
return Number.isInteger(supplied) && supplied > 0 ? Math.min(supplied, 50) : 8;
|
|
3689
3807
|
}
|
|
3690
3808
|
|
|
3691
|
-
|
|
3809
|
+
// A hit may be driven by an attribute/testcase (both are embedded), so surface
|
|
3810
|
+
// the matching bookkeeping text on HIT records only — so the agent sees WHY it
|
|
3811
|
+
// matched without a second lookup. Never attached to pruned neighbours.
|
|
3812
|
+
function tokenizeForMatch(text) {
|
|
3813
|
+
const tokens = new Set();
|
|
3814
|
+
const s = String(text || '');
|
|
3815
|
+
for (const word of s.toLowerCase().match(/[a-z0-9]{3,}/g) || []) tokens.add(word);
|
|
3816
|
+
for (const run of s.match(/[\u4e00-\u9fff]{2,}/g) || []) {
|
|
3817
|
+
tokens.add(run);
|
|
3818
|
+
for (let i = 0; i + 2 <= run.length; i += 1) tokens.add(run.slice(i, i + 2));
|
|
3819
|
+
}
|
|
3820
|
+
return [...tokens];
|
|
3821
|
+
}
|
|
3822
|
+
|
|
3823
|
+
function bookkeepingSnippet(record, objectType, query, maxLen = 240) {
|
|
3824
|
+
if (!record || typeof record !== 'object') return null;
|
|
3825
|
+
const candidates = [];
|
|
3826
|
+
if (objectType === 'ArchitectureRelationship' || objectType === 'Relationship') {
|
|
3827
|
+
if (typeof record.statement === 'string' && record.statement.trim()) candidates.push(record.statement.trim());
|
|
3828
|
+
if (typeof record.description === 'string' && record.description.trim()) candidates.push(record.description.trim());
|
|
3829
|
+
}
|
|
3830
|
+
for (const attribute of Array.isArray(record.attributes) ? record.attributes : []) {
|
|
3831
|
+
if (!attribute || typeof attribute.name !== 'string') continue;
|
|
3832
|
+
const text = (typeof attribute.value === 'string' && attribute.value.trim())
|
|
3833
|
+
|| (typeof attribute.description === 'string' && attribute.description.trim()) || '';
|
|
3834
|
+
if (text) candidates.push(`${attribute.name}: ${text}`);
|
|
3835
|
+
}
|
|
3836
|
+
for (const testcase of Array.isArray(record.testcases) ? record.testcases : []) {
|
|
3837
|
+
if (!testcase) continue;
|
|
3838
|
+
const text = typeof testcase === 'string'
|
|
3839
|
+
? testcase
|
|
3840
|
+
: (testcase.description || testcase.coverage || testcase.name || '');
|
|
3841
|
+
if (typeof text === 'string' && text.trim()) candidates.push(`AT ${testcase.name || ''}: ${text.trim()}`.trim());
|
|
3842
|
+
}
|
|
3843
|
+
if (!candidates.length) return null;
|
|
3844
|
+
const q = typeof query === 'string' ? query : (query && query.intent) || '';
|
|
3845
|
+
const tokens = tokenizeForMatch(q);
|
|
3846
|
+
const scored = candidates.map((text, index) => {
|
|
3847
|
+
const lower = text.toLowerCase();
|
|
3848
|
+
let overlap = 0;
|
|
3849
|
+
for (const token of tokens) if (lower.includes(token)) overlap += 1;
|
|
3850
|
+
return { text, overlap, index };
|
|
3851
|
+
}).sort((a, b) => (b.overlap - a.overlap) || (b.text.length - a.text.length) || (a.index - b.index));
|
|
3852
|
+
let out = '';
|
|
3853
|
+
for (const entry of scored) {
|
|
3854
|
+
if (out && out.length + entry.text.length + 2 > maxLen) break;
|
|
3855
|
+
out = out ? `${out} | ${entry.text}` : entry.text;
|
|
3856
|
+
if (out.length >= maxLen) break;
|
|
3857
|
+
}
|
|
3858
|
+
if (!out) return null;
|
|
3859
|
+
return out.length > maxLen ? `${out.slice(0, maxLen - 3)}...` : out;
|
|
3860
|
+
}
|
|
3861
|
+
|
|
3862
|
+
function buildHitIdSet(source, typeKey) {
|
|
3863
|
+
const set = new Set();
|
|
3864
|
+
const seeds = (source && source.seedsByType && source.seedsByType[typeKey]) || [];
|
|
3865
|
+
for (const seed of Array.isArray(seeds) ? seeds : []) {
|
|
3866
|
+
const raw = seed && (seed.id || seed.objectId || seed.canonicalIdentity);
|
|
3867
|
+
if (typeof raw === 'string' && raw) set.add(raw.includes(':') ? raw.split(':').pop() : raw);
|
|
3868
|
+
}
|
|
3869
|
+
return set;
|
|
3870
|
+
}
|
|
3871
|
+
|
|
3872
|
+
function summarizeSeeds(seedsByType = {}, hitReasonByKey, limit, ctx = {}) {
|
|
3692
3873
|
return Object.freeze(Object.fromEntries(Object.entries(seedsByType).map(([type, seeds]) => [
|
|
3693
3874
|
type,
|
|
3694
3875
|
Object.freeze((Array.isArray(seeds) ? seeds : [])
|
|
@@ -3697,14 +3878,18 @@ function summarizeSeeds(seedsByType = {}, hitReasonByKey, limit) {
|
|
|
3697
3878
|
.slice(0, limit)
|
|
3698
3879
|
.map(seed => {
|
|
3699
3880
|
const objectType = seed.objectType || seed.channel || inferObjectTypeFromSeedType(type);
|
|
3700
|
-
const
|
|
3881
|
+
const rawId = seed.id || seed.objectId || seed.canonicalIdentity;
|
|
3882
|
+
const objectId = typeof rawId === 'string' && rawId.includes(':') ? rawId.split(':').pop() : rawId;
|
|
3701
3883
|
const reasons = hitReasonByKey.get(`${objectType}:${objectId}`) || {};
|
|
3884
|
+
const record = ctx.recordById ? ctx.recordById.get(`${objectType}:${objectId}`) : null;
|
|
3885
|
+
const snippet = record ? bookkeepingSnippet(record, objectType, ctx.query) : null;
|
|
3702
3886
|
return Object.freeze({
|
|
3703
3887
|
objectId,
|
|
3704
3888
|
objectType,
|
|
3705
3889
|
score: typeof seed.score === 'number' ? seed.score : undefined,
|
|
3706
3890
|
hitReason: reasons.firstInclusionReason || 'semantic-seed',
|
|
3707
3891
|
supplementaryReasons: Object.freeze(reasons.supplementaryReasons || []),
|
|
3892
|
+
...(snippet ? { matchedSnippet: snippet } : {}),
|
|
3708
3893
|
});
|
|
3709
3894
|
})),
|
|
3710
3895
|
])));
|
|
@@ -3716,19 +3901,25 @@ function inferObjectTypeFromSeedType(type) {
|
|
|
3716
3901
|
return 'Element';
|
|
3717
3902
|
}
|
|
3718
3903
|
|
|
3719
|
-
function summarizeElements(source, hitReasonByKey, limit) {
|
|
3904
|
+
function summarizeElements(source, hitReasonByKey, limit, ctx = {}) {
|
|
3905
|
+
// Semantic subgraph rule: the attribute/testcase-DERIVED fields (status /
|
|
3906
|
+
// functionalPoints / testCoverage) and the matchedSnippet are the match
|
|
3907
|
+
// surface for the HIT elements only; closure neighbours drop them
|
|
3908
|
+
// (bookkeepingOmitted) — mirroring the structural-read projection.
|
|
3909
|
+
const hitIds = ctx.hitElementIds || buildHitIdSet(source, 'elements');
|
|
3720
3910
|
return uniqueById([
|
|
3721
3911
|
...(((source.closure && source.closure.elements) || [])),
|
|
3722
3912
|
...((((source.viewClosure && source.viewClosure.views) || []).flatMap(view => view.memberElements || []))),
|
|
3723
3913
|
...((((source.endpointClosure && source.endpointClosure.relationships) || []).flatMap(relationship => [relationship.source, relationship.target]).filter(Boolean))),
|
|
3724
|
-
], 'id').slice(0, limit).map(element => summarizeElement(element, hitReasonByKey));
|
|
3914
|
+
], 'id').slice(0, limit).map(element => summarizeElement(element, hitReasonByKey, hitIds, ctx));
|
|
3725
3915
|
}
|
|
3726
3916
|
|
|
3727
|
-
function summarizeRelationships(source, hitReasonByKey, limit) {
|
|
3917
|
+
function summarizeRelationships(source, hitReasonByKey, limit, ctx = {}) {
|
|
3918
|
+
const hitIds = ctx.hitRelationshipIds || buildHitIdSet(source, 'relationships');
|
|
3728
3919
|
return uniqueById([
|
|
3729
3920
|
...(((source.endpointClosure && source.endpointClosure.relationships) || [])),
|
|
3730
3921
|
...((((source.viewClosure && source.viewClosure.views) || []).flatMap(view => view.memberRelationships || []))),
|
|
3731
|
-
], 'id').slice(0, limit).map(relationship => summarizeRelationship(relationship, hitReasonByKey));
|
|
3922
|
+
], 'id').slice(0, limit).map(relationship => summarizeRelationship(relationship, hitReasonByKey, hitIds, ctx));
|
|
3732
3923
|
}
|
|
3733
3924
|
|
|
3734
3925
|
function summarizeViews(source, hitReasonByKey, limit) {
|
|
@@ -3737,27 +3928,38 @@ function summarizeViews(source, hitReasonByKey, limit) {
|
|
|
3737
3928
|
.map(view => summarizeView(view, hitReasonByKey));
|
|
3738
3929
|
}
|
|
3739
3930
|
|
|
3740
|
-
function summarizeElement(element, hitReasonByKey) {
|
|
3931
|
+
function summarizeElement(element, hitReasonByKey, hitIds = null, ctx = {}) {
|
|
3741
3932
|
const attributes = attributesMap(element);
|
|
3742
3933
|
const reasons = hitReasonByKey.get(`Element:${element.id}`) || {};
|
|
3743
|
-
|
|
3934
|
+
const isHit = !hitIds || hitIds.size === 0 || hitIds.has(element.id);
|
|
3935
|
+
const base = {
|
|
3744
3936
|
id: element.id,
|
|
3745
3937
|
name: element.name,
|
|
3746
3938
|
type: element.type,
|
|
3747
3939
|
descriptionSummary: summarizeText(element.description),
|
|
3748
|
-
status: attributes.deliveryStatus || attributes.status,
|
|
3749
|
-
functionalPoints: Object.freeze(Object.entries(attributes)
|
|
3750
|
-
.filter(([name]) => name.startsWith('functionalPoint'))
|
|
3751
|
-
.map(([, value]) => value)),
|
|
3752
|
-
testCoverage: summarizeTestcases(element.testcases),
|
|
3753
3940
|
hitReason: reasons.firstInclusionReason,
|
|
3754
3941
|
supplementaryReasons: Object.freeze(reasons.supplementaryReasons || []),
|
|
3755
|
-
}
|
|
3942
|
+
};
|
|
3943
|
+
if (isHit) {
|
|
3944
|
+
const snippet = bookkeepingSnippet(element, 'Element', ctx.query);
|
|
3945
|
+
return Object.freeze({
|
|
3946
|
+
...base,
|
|
3947
|
+
status: attributes.deliveryStatus || attributes.status,
|
|
3948
|
+
functionalPoints: Object.freeze(Object.entries(attributes)
|
|
3949
|
+
.filter(([name]) => name.startsWith('functionalPoint'))
|
|
3950
|
+
.map(([, value]) => value)),
|
|
3951
|
+
testCoverage: summarizeTestcases(element.testcases),
|
|
3952
|
+
...(snippet ? { matchedSnippet: snippet } : {}),
|
|
3953
|
+
});
|
|
3954
|
+
}
|
|
3955
|
+
const hasBookkeeping = Object.keys(attributes).length > 0
|
|
3956
|
+
|| (Array.isArray(element.testcases) && element.testcases.length > 0);
|
|
3957
|
+
return Object.freeze({ ...base, ...(hasBookkeeping ? { bookkeepingOmitted: true } : {}) });
|
|
3756
3958
|
}
|
|
3757
3959
|
|
|
3758
|
-
function summarizeRelationship(relationship, hitReasonByKey) {
|
|
3960
|
+
function summarizeRelationship(relationship, hitReasonByKey, hitIds = null, ctx = {}) {
|
|
3759
3961
|
const reasons = hitReasonByKey.get(`ArchitectureRelationship:${relationship.id}`) || {};
|
|
3760
|
-
|
|
3962
|
+
const base = {
|
|
3761
3963
|
id: relationship.id,
|
|
3762
3964
|
name: relationship.name,
|
|
3763
3965
|
type: relationship.type,
|
|
@@ -3765,7 +3967,13 @@ function summarizeRelationship(relationship, hitReasonByKey) {
|
|
|
3765
3967
|
target_id: relationship.target_id,
|
|
3766
3968
|
hitReason: reasons.firstInclusionReason,
|
|
3767
3969
|
supplementaryReasons: Object.freeze(reasons.supplementaryReasons || []),
|
|
3768
|
-
}
|
|
3970
|
+
};
|
|
3971
|
+
const isHit = !hitIds || hitIds.size === 0 || hitIds.has(relationship.id);
|
|
3972
|
+
if (!isHit) {
|
|
3973
|
+
return Object.freeze(base);
|
|
3974
|
+
}
|
|
3975
|
+
const snippet = bookkeepingSnippet(relationship, 'ArchitectureRelationship', ctx.query);
|
|
3976
|
+
return Object.freeze({ ...base, ...(snippet ? { matchedSnippet: snippet } : {}) });
|
|
3769
3977
|
}
|
|
3770
3978
|
|
|
3771
3979
|
function summarizeView(view, hitReasonByKey) {
|
|
@@ -4239,6 +4447,7 @@ module.exports = {
|
|
|
4239
4447
|
GET_SYSTEM_ARCHITECTURE_OUTPUT_SCHEMA,
|
|
4240
4448
|
TOOLS,
|
|
4241
4449
|
applyMutations,
|
|
4450
|
+
buildBusinessSemanticSummary,
|
|
4242
4451
|
buildSemanticDedupAdvisory,
|
|
4243
4452
|
selectCreatedElementAdds,
|
|
4244
4453
|
evaluateSemanticDedupGate,
|
|
@@ -39,6 +39,7 @@ node ~/.argo/scripts/agentSearchDiagnose.js --session <session.ndjson> \
|
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
脚本**只读**会话与日志,写出上面的 bundle 目录,输出概览与启发式 hints。
|
|
42
|
+
支持两种输入:`opencode export` 的会话 JSON(`{info, messages}`)与 `opencode run --format json` 的事件流 NDJSON。
|
|
42
43
|
|
|
43
44
|
### 3. 复核并补充诊断总结(Agent)
|
|
44
45
|
|
package/package.json
CHANGED