@wowok/agent-mcp 2.5.4 → 2.5.5

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.
Files changed (53) hide show
  1. package/README.md +33 -26
  2. package/dist/knowledge/reward-confirm.d.ts +3 -0
  3. package/dist/knowledge/reward-confirm.js +30 -0
  4. package/dist/knowledge/reward-puzzle.d.ts +1 -0
  5. package/dist/knowledge/reward-puzzle.js +12 -0
  6. package/dist/knowledge/reward-risk.d.ts +4 -1
  7. package/dist/knowledge/reward-risk.js +85 -3
  8. package/dist/knowledge/tools-reference.js +5 -4
  9. package/dist/project/evaluation.d.ts +53 -0
  10. package/dist/project/evaluation.js +853 -0
  11. package/dist/project/graph-builder.d.ts +43 -0
  12. package/dist/project/graph-builder.js +572 -0
  13. package/dist/project/handlers.d.ts +152 -0
  14. package/dist/project/handlers.js +458 -0
  15. package/dist/project/index.d.ts +1 -0
  16. package/dist/project/index.js +24 -1
  17. package/dist/project/query.d.ts +1 -0
  18. package/dist/project/query.js +2 -0
  19. package/dist/project/semantic-assembler.d.ts +93 -0
  20. package/dist/project/semantic-assembler.js +345 -0
  21. package/dist/schema/call/base.js +1 -1
  22. package/dist/schema/project/index.d.ts +1558 -3885
  23. package/dist/schema/project/index.js +357 -961
  24. package/dist/schema/trust/index.d.ts +14 -14
  25. package/dist/schemas/bridge_operation.schema.json +1 -1
  26. package/dist/schemas/guard2file.schema.json +1 -1
  27. package/dist/schemas/index.json +2 -2
  28. package/dist/schemas/machineNode2file.schema.json +1 -1
  29. package/dist/schemas/onchain_operations.schema.json +5 -5
  30. package/dist/schemas/onchain_operations_allocation.schema.json +1 -1
  31. package/dist/schemas/onchain_operations_arbitration.schema.json +1 -1
  32. package/dist/schemas/onchain_operations_contact.schema.json +1 -1
  33. package/dist/schemas/onchain_operations_demand.schema.json +1 -1
  34. package/dist/schemas/onchain_operations_gen_passport.schema.json +2 -2
  35. package/dist/schemas/onchain_operations_gen_proof.schema.json +2 -2
  36. package/dist/schemas/onchain_operations_guard.schema.json +1 -1
  37. package/dist/schemas/onchain_operations_machine.schema.json +1 -1
  38. package/dist/schemas/onchain_operations_order.schema.json +1 -1
  39. package/dist/schemas/onchain_operations_payment.schema.json +1 -1
  40. package/dist/schemas/onchain_operations_permission.schema.json +1 -1
  41. package/dist/schemas/onchain_operations_personal.schema.json +1 -1
  42. package/dist/schemas/onchain_operations_progress.schema.json +1 -1
  43. package/dist/schemas/onchain_operations_proof.schema.json +1 -1
  44. package/dist/schemas/onchain_operations_repository.schema.json +1 -1
  45. package/dist/schemas/onchain_operations_reward.schema.json +1 -1
  46. package/dist/schemas/onchain_operations_service.schema.json +1 -1
  47. package/dist/schemas/onchain_operations_treasury.schema.json +1 -1
  48. package/dist/schemas/project_operation.output.json +975 -2471
  49. package/dist/schemas/project_operation.schema.json +207 -534
  50. package/dist/tools/handlers/onchain.js +97 -19
  51. package/dist/tools/handlers/project.js +34 -1046
  52. package/dist/tools/index.js +84 -55
  53. package/package.json +2 -2
@@ -0,0 +1,93 @@
1
+ import { type Project, type ProjectObject, type GraphEdge } from "@wowok/wowok";
2
+ import type { ReputationData } from "./graph-builder.js";
3
+ export interface SemanticObjectContext {
4
+ address: string;
5
+ name: string | null;
6
+ objectType: string | null;
7
+ role: string | null;
8
+ description: string;
9
+ keyFields: Record<string, unknown>;
10
+ cacheFreshness: "fresh" | "stale" | "unknown";
11
+ }
12
+ export interface SemanticGraphContext {
13
+ nodeCount: number;
14
+ edgeCount: number;
15
+ edgesByType: Record<string, number>;
16
+ edgesBySource: Record<string, number>;
17
+ directDependencies: Array<{
18
+ target: string;
19
+ targetType: string | null;
20
+ fieldName: string | null;
21
+ relationshipType: string;
22
+ }>;
23
+ potentialGaps: Array<{
24
+ target: string;
25
+ targetType: string | null;
26
+ fieldName: string;
27
+ reason: string;
28
+ }>;
29
+ }
30
+ export interface SemanticReputationContext {
31
+ address: string;
32
+ likes: number;
33
+ dislikes: number;
34
+ affiliations: number;
35
+ sentimentScore: number;
36
+ notableVoters: Array<{
37
+ who: string;
38
+ like: boolean;
39
+ dislike: boolean;
40
+ affiliation: boolean;
41
+ }>;
42
+ }
43
+ export interface SemanticProjectContext {
44
+ project: {
45
+ prefix: string;
46
+ name: string;
47
+ description: string | null;
48
+ industry: string | null;
49
+ perspective: string;
50
+ status: string;
51
+ network: string;
52
+ serviceAddress: string | null;
53
+ currentVersion: string | null;
54
+ };
55
+ objects: SemanticObjectContext[];
56
+ graph: SemanticGraphContext;
57
+ reputation: SemanticReputationContext[];
58
+ freshnessSummary: {
59
+ freshCount: number;
60
+ staleCount: number;
61
+ unknownCount: number;
62
+ oldestFetch: number | null;
63
+ newestFetch: number | null;
64
+ };
65
+ }
66
+ export interface EvaluationPrompt {
67
+ evaluationType: "risk" | "trust" | "worth_buying" | "completeness" | "value";
68
+ context: SemanticProjectContext;
69
+ prompt: string;
70
+ scoringCriteria: ScoringCriterion[];
71
+ }
72
+ export interface ScoringCriterion {
73
+ name: string;
74
+ description: string;
75
+ weight: number;
76
+ minScore: number;
77
+ maxScore: number;
78
+ }
79
+ export declare class SemanticAssembler {
80
+ private localProject;
81
+ constructor();
82
+ assemble(project: Project, objects: ProjectObject[], edges: GraphEdge[], reputation?: Map<string, ReputationData>): Promise<SemanticProjectContext>;
83
+ private assembleObjects;
84
+ private describeObject;
85
+ private extractKeyFields;
86
+ private assembleGraph;
87
+ private assembleReputation;
88
+ private classifyFreshness;
89
+ private computeFreshnessSummary;
90
+ buildEvaluationPrompt(evaluationType: EvaluationPrompt["evaluationType"], context: SemanticProjectContext): EvaluationPrompt;
91
+ private getScoringCriteria;
92
+ private formatPrompt;
93
+ }
@@ -0,0 +1,345 @@
1
+ import { LocalProject, } from "@wowok/wowok";
2
+ const FRESH_THRESHOLD_MS = 1 * 60 * 60 * 1000;
3
+ const STALE_THRESHOLD_MS = 24 * 60 * 60 * 1000;
4
+ export class SemanticAssembler {
5
+ localProject;
6
+ constructor() {
7
+ this.localProject = LocalProject.Instance();
8
+ }
9
+ async assemble(project, objects, edges, reputation) {
10
+ const objectContexts = await this.assembleObjects(objects);
11
+ const graphContext = this.assembleGraph(edges);
12
+ const reputationContexts = this.assembleReputation(objects, reputation);
13
+ const freshnessSummary = this.computeFreshnessSummary(objectContexts);
14
+ return {
15
+ project: {
16
+ prefix: project.prefix,
17
+ name: project.name,
18
+ description: project.description,
19
+ industry: project.industry,
20
+ perspective: project.perspective,
21
+ status: project.status,
22
+ network: project.network,
23
+ serviceAddress: project.serviceAddress,
24
+ currentVersion: project.currentVersion,
25
+ },
26
+ objects: objectContexts,
27
+ graph: graphContext,
28
+ reputation: reputationContexts,
29
+ freshnessSummary,
30
+ };
31
+ }
32
+ async assembleObjects(objects) {
33
+ const result = [];
34
+ for (const obj of objects) {
35
+ const cached = await this.localProject.getCachedObject(obj.objectAddress);
36
+ const description = cached
37
+ ? this.describeObject(obj, cached)
38
+ : `Object ${obj.objectAddress} (no cached on-chain data)`;
39
+ const keyFields = cached
40
+ ? this.extractKeyFields(obj.objectType, cached)
41
+ : {};
42
+ const cacheFreshness = cached
43
+ ? this.classifyFreshness(cached.fetchedAt)
44
+ : "unknown";
45
+ result.push({
46
+ address: obj.objectAddress,
47
+ name: obj.objectName,
48
+ objectType: obj.objectType,
49
+ role: obj.role,
50
+ description,
51
+ keyFields,
52
+ cacheFreshness,
53
+ });
54
+ }
55
+ return result;
56
+ }
57
+ describeObject(obj, cached) {
58
+ const type = obj.objectType ?? "unknown";
59
+ const name = obj.objectName ?? abbreviateAddress(obj.objectAddress);
60
+ let desc = `${type} "${name}" at ${abbreviateAddress(obj.objectAddress)}`;
61
+ try {
62
+ const data = JSON.parse(cached.data);
63
+ switch (type) {
64
+ case "Service": {
65
+ const published = data.bPublished;
66
+ const paused = data.bPaused;
67
+ const sales = data.sales;
68
+ desc += published
69
+ ? paused
70
+ ? " (published but PAUSED)"
71
+ : " (published, active)"
72
+ : " (not yet published)";
73
+ if (sales && Array.isArray(sales) && sales.length > 0) {
74
+ desc += ` with ${sales.length} sale option(s)`;
75
+ }
76
+ break;
77
+ }
78
+ case "Machine": {
79
+ const nodeCount = data.node_count ?? data.nodeCount;
80
+ const published = data.bPublished;
81
+ desc += published
82
+ ? ` (published, ${nodeCount} nodes)`
83
+ : ` (draft, ${nodeCount} nodes)`;
84
+ break;
85
+ }
86
+ case "Treasury": {
87
+ desc += " (compensation fund)";
88
+ break;
89
+ }
90
+ case "Guard": {
91
+ desc += " (validation rules)";
92
+ break;
93
+ }
94
+ default:
95
+ break;
96
+ }
97
+ }
98
+ catch {
99
+ }
100
+ return desc;
101
+ }
102
+ extractKeyFields(objectType, cached) {
103
+ const fields = {};
104
+ try {
105
+ const data = JSON.parse(cached.data);
106
+ switch (objectType) {
107
+ case "Service":
108
+ fields.bPublished = data.bPublished;
109
+ fields.bPaused = data.bPaused;
110
+ fields.sales = data.sales;
111
+ fields.machine = data.machine;
112
+ fields.buy_guard = data.buy_guard;
113
+ fields.permission = data.permission;
114
+ fields.compensation_fund = data.compensation_fund;
115
+ fields.order_allocators = data.order_allocators;
116
+ fields.arbitrations = data.arbitrations;
117
+ fields.rewards = data.rewards;
118
+ fields.repositories = data.repositories;
119
+ break;
120
+ case "Machine":
121
+ fields.bPublished = data.bPublished;
122
+ fields.bPaused = data.bPaused;
123
+ fields.node_count = data.node_count ?? data.nodeCount;
124
+ fields.permission = data.permission;
125
+ break;
126
+ case "Treasury":
127
+ fields.permission = data.permission;
128
+ fields.guard = data.guard;
129
+ break;
130
+ case "Allocation":
131
+ case "Arbitration":
132
+ case "Reward":
133
+ case "Repository":
134
+ case "Demand":
135
+ fields.permission = data.permission;
136
+ fields.guard = data.guard;
137
+ break;
138
+ default:
139
+ break;
140
+ }
141
+ }
142
+ catch {
143
+ }
144
+ return fields;
145
+ }
146
+ assembleGraph(edges) {
147
+ const edgesByType = {};
148
+ const edgesBySource = {};
149
+ const directDependencies = [];
150
+ const potentialGaps = [];
151
+ for (const edge of edges) {
152
+ edgesByType[edge.relationshipType] = (edgesByType[edge.relationshipType] ?? 0) + 1;
153
+ edgesBySource[edge.edgeSource] = (edgesBySource[edge.edgeSource] ?? 0) + 1;
154
+ if (edge.edgeSource === "internal" && edge.depth === 0) {
155
+ directDependencies.push({
156
+ target: edge.targetObject,
157
+ targetType: null,
158
+ fieldName: edge.fieldName,
159
+ relationshipType: edge.relationshipType,
160
+ });
161
+ }
162
+ if (edge.edgeSource === "potential") {
163
+ const reason = edge.metadata?.reason;
164
+ potentialGaps.push({
165
+ target: edge.targetObject,
166
+ targetType: edge.metadata?.expected_target_type,
167
+ fieldName: edge.fieldName ?? "unknown",
168
+ reason: typeof reason === "string" ? reason : "expected_relationship_missing",
169
+ });
170
+ }
171
+ }
172
+ return {
173
+ nodeCount: new Set([...edges.map((e) => e.sourceObject), ...edges.map((e) => e.targetObject)]).size,
174
+ edgeCount: edges.length,
175
+ edgesByType,
176
+ edgesBySource,
177
+ directDependencies,
178
+ potentialGaps,
179
+ };
180
+ }
181
+ assembleReputation(objects, reputation) {
182
+ if (!reputation || reputation.size === 0)
183
+ return [];
184
+ const result = [];
185
+ for (const obj of objects) {
186
+ const rep = reputation.get(obj.objectAddress);
187
+ if (!rep)
188
+ continue;
189
+ const total = rep.likes + rep.dislikes;
190
+ const sentimentScore = total > 0 ? rep.likes / total : 0.5;
191
+ result.push({
192
+ address: obj.objectAddress,
193
+ likes: rep.likes,
194
+ dislikes: rep.dislikes,
195
+ affiliations: rep.affiliations,
196
+ sentimentScore,
197
+ notableVoters: rep.voters.slice(0, 5).map((v) => ({
198
+ who: abbreviateAddress(v.who),
199
+ like: v.like,
200
+ dislike: v.dislike,
201
+ affiliation: v.affiliation,
202
+ })),
203
+ });
204
+ }
205
+ return result;
206
+ }
207
+ classifyFreshness(fetchedAt) {
208
+ const age = Date.now() - fetchedAt;
209
+ if (age < FRESH_THRESHOLD_MS)
210
+ return "fresh";
211
+ if (age < STALE_THRESHOLD_MS)
212
+ return "stale";
213
+ return "stale";
214
+ }
215
+ computeFreshnessSummary(objects) {
216
+ let freshCount = 0;
217
+ let staleCount = 0;
218
+ let unknownCount = 0;
219
+ let oldestFetch = null;
220
+ let newestFetch = null;
221
+ for (const obj of objects) {
222
+ switch (obj.cacheFreshness) {
223
+ case "fresh":
224
+ freshCount++;
225
+ break;
226
+ case "stale":
227
+ staleCount++;
228
+ break;
229
+ case "unknown":
230
+ unknownCount++;
231
+ break;
232
+ }
233
+ }
234
+ return { freshCount, staleCount, unknownCount, oldestFetch, newestFetch };
235
+ }
236
+ buildEvaluationPrompt(evaluationType, context) {
237
+ const criteria = this.getScoringCriteria(evaluationType);
238
+ const prompt = this.formatPrompt(evaluationType, context, criteria);
239
+ return {
240
+ evaluationType,
241
+ context,
242
+ prompt,
243
+ scoringCriteria: criteria,
244
+ };
245
+ }
246
+ getScoringCriteria(evaluationType) {
247
+ switch (evaluationType) {
248
+ case "risk":
249
+ return [
250
+ { name: "structural_risk", description: "Risk from missing bindings, broken workflows, incomplete setup", weight: 0.3, minScore: 0, maxScore: 100 },
251
+ { name: "fund_safety", description: "Risk from Treasury/Allocation misconfiguration, missing compensation fund", weight: 0.25, minScore: 0, maxScore: 100 },
252
+ { name: "operational_risk", description: "Risk from Machine/Guard complexity, pause state, missing arbitration", weight: 0.25, minScore: 0, maxScore: 100 },
253
+ { name: "governance_risk", description: "Risk from Permission setup, missing UM, overly permissive rules", weight: 0.2, minScore: 0, maxScore: 100 },
254
+ ];
255
+ case "trust":
256
+ return [
257
+ { name: "reputation", description: "Registrar like/dislike ratio and affiliation count", weight: 0.3, minScore: 0, maxScore: 100 },
258
+ { name: "arbitration_coverage", description: "Presence and quality of arbitration arrangements", weight: 0.25, minScore: 0, maxScore: 100 },
259
+ { name: "guard_quality", description: "Guard rule complexity and coverage of business scenarios", weight: 0.25, minScore: 0, maxScore: 100 },
260
+ { name: "transparency", description: "Object descriptions, repository data, contact information availability", weight: 0.2, minScore: 0, maxScore: 100 },
261
+ ];
262
+ case "worth_buying":
263
+ return [
264
+ { name: "price_reasonableness", description: "Service pricing vs. comparable services and market norms", weight: 0.3, minScore: 0, maxScore: 100 },
265
+ { name: "refund_protection", description: "Compensation fund balance, lock duration, refund workflow availability", weight: 0.25, minScore: 0, maxScore: 100 },
266
+ { name: "risk_adjusted_value", description: "Value proposition adjusted for project risk score", weight: 0.25, minScore: 0, maxScore: 100 },
267
+ { name: "trust_adjusted_value", description: "Value proposition adjusted for merchant trust score", weight: 0.2, minScore: 0, maxScore: 100 },
268
+ ];
269
+ case "completeness":
270
+ return [
271
+ { name: "object_coverage", description: "Expected objects present vs. missing (based on EXPECTED_RELATIONSHIPS)", weight: 0.3, minScore: 0, maxScore: 100 },
272
+ { name: "binding_completeness", description: "Required bindings established (Service→Machine, Guard, Permission, etc.)", weight: 0.3, minScore: 0, maxScore: 100 },
273
+ { name: "workflow_rationality", description: "Machine node topology is sound, no dangling nodes or cycles", weight: 0.2, minScore: 0, maxScore: 100 },
274
+ { name: "deployment_readiness", description: "Service publishable, all prerequisites met", weight: 0.2, minScore: 0, maxScore: 100 },
275
+ ];
276
+ case "value":
277
+ return [
278
+ { name: "market_positioning", description: "Service differentiation and competitive positioning", weight: 0.3, minScore: 0, maxScore: 100 },
279
+ { name: "revenue_potential", description: "Pricing strategy and revenue model viability", weight: 0.3, minScore: 0, maxScore: 100 },
280
+ { name: "scalability", description: "Ability to handle growth (Machine capacity, Allocation flexibility)", weight: 0.2, minScore: 0, maxScore: 100 },
281
+ { name: "ecosystem_fit", description: "Integration with WoWok ecosystem (rewards, demands, repositories)", weight: 0.2, minScore: 0, maxScore: 100 },
282
+ ];
283
+ }
284
+ }
285
+ formatPrompt(evaluationType, context, criteria) {
286
+ const lines = [];
287
+ lines.push(`# Project Evaluation: ${evaluationType.toUpperCase()}`);
288
+ lines.push("");
289
+ lines.push(`## Project: ${context.project.name} (${context.project.prefix})`);
290
+ lines.push(`- Perspective: ${context.project.perspective}`);
291
+ lines.push(`- Status: ${context.project.status}`);
292
+ lines.push(`- Network: ${context.project.network}`);
293
+ lines.push(`- Industry: ${context.project.industry ?? "unspecified"}`);
294
+ lines.push(`- Service: ${context.project.serviceAddress ?? "not set"}`);
295
+ lines.push("");
296
+ lines.push("## Objects");
297
+ for (const obj of context.objects) {
298
+ lines.push(`- ${obj.description}`);
299
+ }
300
+ lines.push("");
301
+ lines.push("## Relationship Graph");
302
+ lines.push(`- Total nodes: ${context.graph.nodeCount}`);
303
+ lines.push(`- Total edges: ${context.graph.edgeCount}`);
304
+ lines.push(`- Edge sources: ${JSON.stringify(context.graph.edgesBySource)}`);
305
+ lines.push(`- Edge types: ${JSON.stringify(context.graph.edgesByType)}`);
306
+ lines.push(`- Direct dependencies: ${context.graph.directDependencies.length}`);
307
+ if (context.graph.potentialGaps.length > 0) {
308
+ lines.push(`- **Missing relationships (potential gaps): ${context.graph.potentialGaps.length}**`);
309
+ for (const gap of context.graph.potentialGaps.slice(0, 10)) {
310
+ lines.push(` - ${gap.fieldName} → ${gap.targetType ?? "unknown"} (${gap.reason})`);
311
+ }
312
+ }
313
+ lines.push("");
314
+ if (context.reputation.length > 0) {
315
+ lines.push("## Reputation Data (from Registrar)");
316
+ for (const rep of context.reputation) {
317
+ lines.push(`- ${abbreviateAddress(rep.address)}: ${rep.likes} likes, ${rep.dislikes} dislikes, ${rep.affiliations} affiliations (sentiment: ${(rep.sentimentScore * 100).toFixed(0)}%)`);
318
+ }
319
+ lines.push("");
320
+ }
321
+ lines.push("## Cache Freshness");
322
+ lines.push(`- Fresh: ${context.freshnessSummary.freshCount}`);
323
+ lines.push(`- Stale: ${context.freshnessSummary.staleCount}`);
324
+ lines.push(`- Unknown: ${context.freshnessSummary.unknownCount}`);
325
+ lines.push("");
326
+ lines.push("## Scoring Criteria");
327
+ lines.push("Please score each criterion (0-100, higher = better) and provide a brief justification:");
328
+ for (const c of criteria) {
329
+ lines.push(`- **${c.name}** (weight: ${c.weight}): ${c.description}`);
330
+ }
331
+ lines.push("");
332
+ lines.push("## Instructions");
333
+ lines.push(`1. Analyze the project from the "${evaluationType}" perspective.`);
334
+ lines.push("2. Score each criterion above (0-100).");
335
+ lines.push("3. Compute a weighted overall score.");
336
+ lines.push("4. Provide a 1-2 sentence summary of your assessment.");
337
+ lines.push("5. Return the result as JSON: { score: number, summary: string, details: { criterion_scores: {...}, justification: string } }");
338
+ return lines.join("\n");
339
+ }
340
+ }
341
+ function abbreviateAddress(address) {
342
+ if (!address || address.length < 12)
343
+ return address;
344
+ return `${address.slice(0, 6)}...${address.slice(-4)}`;
345
+ }
@@ -66,7 +66,7 @@ export const CallEnvSchema = z.object({
66
66
  referrer: NameOrAddressSchema.optional().describe("Referrer ID. If the user is using the network for the first time, the referrer ID will be recorded."),
67
67
  confirmed: z.boolean().optional().describe("Phase 2 ConfirmGate: set to true to proceed after a 'pending_confirmation' result. The operation is NOT executed until you re-call with confirmed=true after user consent."),
68
68
  user_intent_phrases: z.array(z.string()).optional().describe("Phase 2 ConfirmGate: explicit user intent phrases collected from conversation context (e.g. 'use default account', 'force publish'). Used to bypass confirmation rules whose bypass_on_explicit_intent matches."),
69
- project: z.string().optional().describe("Phase 2 ProjectService: explicit project prefix to assign this object to (e.g. 'myshop'). When set, the object is tagged with 'project:<prefix>' automatically. Overrides the prefix parsed from the object name."),
69
+ project: z.string().optional().describe("Project prefix to assign this object to (e.g. 'myshop'). When set: 1) the object is tagged with 'project:<prefix>' on-chain automatically via ProjectService, 2) the newly created object is auto-associated to the SQLite project for local management, evaluation, and graph tracking. The project MUST exist (create via project_operation action='create_project' first). Use project_operation to manage projects."),
70
70
  client_schema_version: z.string().optional().describe("Client-side schema version (semver, e.g. '1.0.0'). When provided, the server checks compatibility and attaches warnings if mismatched."),
71
71
  schema_strict_mode: z.boolean().optional().describe("When true, schema incompatibility returns an error instead of a warning. Default: false (warn mode)."),
72
72
  dry_run: z.boolean().optional().describe("Phase 1 dry_run: when true, SIMULATE the transaction without signing or submitting. Returns effects + events showing what WOULD happen — no on-chain state is modified, no gas is charged. Use this to verify an operation will succeed before committing. Essential for the 5-step loop's Plan step. The returned digest will be 'dry_run:<timestamp>' and objectChanges will be empty (simulation only)."),