@moda-ai/cli 1.55.27 → 1.55.28

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 (43) hide show
  1. package/dist/{agent-vp9prdma.js → agent-0w9rtdsw.js} +22 -16
  2. package/dist/cli-3n0gz4d0.js +17971 -0
  3. package/dist/{cli-q9nsvqqg.js → cli-3retgh64.js} +85 -1165
  4. package/dist/{cli-ccjfwzqx.js → cli-97jew729.js} +1 -1
  5. package/dist/{cli-r5pp7qny.js → cli-99p2jh28.js} +42 -456
  6. package/dist/{cli-0pkc8jhg.js → cli-9bgk5jeh.js} +8 -6
  7. package/dist/{cli-hmjdgxrn.js → cli-9bgnvth2.js} +5 -3
  8. package/dist/cli-9kfxkxmk.js +6994 -0
  9. package/dist/cli-cesds766.js +1516 -0
  10. package/dist/{cli-v8q7sd7g.js → cli-dr6x0zv7.js} +1 -1
  11. package/dist/{cli-pg5qfmqc.js → cli-e23g9ywa.js} +1 -1
  12. package/dist/{cli-vdkwkkp5.js → cli-fwpqghgc.js} +4 -2
  13. package/dist/cli-k4tqwah4.js +89 -0
  14. package/dist/{cli-6et4z504.js → cli-mg5zv4s6.js} +15 -13
  15. package/dist/{cli-wzds257x.js → cli-n2hng3gv.js} +6 -4
  16. package/dist/cli-ns5fcnkz.js +341 -0
  17. package/dist/{cli-4h0ave0f.js → cli-ny48a8de.js} +12 -9
  18. package/dist/{cli-qwgct6x7.js → cli-qpq84eee.js} +5220 -32716
  19. package/dist/cli-xq45etg1.js +1098 -0
  20. package/dist/cli-yf39avhh.js +754 -0
  21. package/dist/cli-yz8xcpyk.js +12 -0
  22. package/dist/cli.js +77 -153
  23. package/dist/doctor-k3ta6h2y.js +24 -0
  24. package/dist/{fix-75s9s0d6.js → fix-w5z02wt4.js} +8 -9
  25. package/dist/{harness-a7tb9a23.js → harness-7azmzytc.js} +15 -7
  26. package/dist/{harness-github-actions-6v4wydb8.js → harness-github-actions-bv2qrnqb.js} +11 -5
  27. package/dist/harness-report-rd9c4e9v.js +54 -0
  28. package/dist/{index-91bpc8g8.js → index-d1z50dsq.js} +41 -33
  29. package/dist/{production-intelligence-7nqddd4f.js → production-intelligence-wvsgjxth.js} +9 -6
  30. package/dist/{profile-commands-49yn02yg.js → profile-commands-h8evz9x0.js} +7 -4
  31. package/dist/prompt-attribution-1nytc9wj.js +148 -0
  32. package/dist/prompt-source-mcp.js +14 -7
  33. package/dist/prompt-sources-07tczyqa.js +19 -0
  34. package/dist/{prompts-17wyrst6.js → prompts-6y9wfq5z.js} +8 -7
  35. package/dist/prompts-ab-ezrd63sk.js +70 -0
  36. package/dist/prompts-propose-2k3enm2q.js +255 -0
  37. package/dist/{provision-bc3h68e1.js → provision-y5bdbh4y.js} +9 -6
  38. package/dist/{registry-fpzhjznj.js → registry-96aze5wp.js} +7 -4
  39. package/dist/{skills-ccdbqghj.js → skills-j8865009.js} +7 -5
  40. package/dist/{sync-hkxce5zh.js → sync-ew7yvy0e.js} +16 -10
  41. package/package.json +1 -1
  42. package/skills/integration/index.json +2 -2
  43. package/dist/doctor-wmre3mr7.js +0 -22
@@ -0,0 +1,1516 @@
1
+ import {
2
+ CliInputError
3
+ } from "./cli-k4tqwah4.js";
4
+ import {
5
+ __require
6
+ } from "./cli-0v6na3yp.js";
7
+
8
+ // src/harness-report.ts
9
+ import { createHash } from "node:crypto";
10
+ import {
11
+ existsSync,
12
+ lstatSync,
13
+ mkdirSync,
14
+ readFileSync,
15
+ writeFileSync
16
+ } from "node:fs";
17
+ import { basename, dirname, join, relative, resolve } from "node:path";
18
+ var HARNESS_REPORT_SCHEMA_VERSION = "harness_report.v0.1";
19
+ var HARNESS_REPORT_VALIDATION_SCHEMA_VERSION = "harness_report_validation.v0.1";
20
+ var HARNESS_REPORT_APPROVAL_SCHEMA_VERSION = "harness_report_approval.v0.1";
21
+ var HARNESS_REPORT_PATH = join(".moda", "harness-report.json");
22
+ var HARNESS_REPORT_MARKDOWN_PATH = join(".moda", "HARNESS_REPORT.md");
23
+ var HARNESS_REPORT_VALIDATION_PATH = join(".moda", "harness-report-validation.json");
24
+ var HARNESS_REPORT_APPROVAL_PATH = join(".moda", "harness-report-approval.json");
25
+ var HARNESS_ANALYZE_CONCERNS = ["topology", "tools", "prompts", "evals"];
26
+ var HARNESS_GRAPH_ARRAY_FIELDS = [
27
+ "agentFamilies",
28
+ "runtimeAgents",
29
+ "developerAgentAdapters",
30
+ "channelAdapters",
31
+ "artifacts",
32
+ "identityNamespaces",
33
+ "environments",
34
+ "agentFrameworks",
35
+ "modelProviders",
36
+ "deployments",
37
+ "relationships",
38
+ "unknowns"
39
+ ];
40
+ var HARNESS_REPORT_SUMMARY_FIELDS = [
41
+ "runtimeAgentCount",
42
+ "agentFamilyCount",
43
+ "artifactCount",
44
+ "sharedArtifactCount",
45
+ "channelAdapterCount",
46
+ "identityNamespaceCount",
47
+ "environmentCount",
48
+ "agentFrameworkCount",
49
+ "modelProviderCount",
50
+ "deploymentCount",
51
+ "relationshipCount",
52
+ "unknownCount"
53
+ ];
54
+ var HARNESS_REPORT_CITATION_TYPES = new Set([
55
+ "file",
56
+ "generated_doc",
57
+ "command_output",
58
+ "production_evidence"
59
+ ]);
60
+ var MAX_HARNESS_REPORT_BYTES = 12000000;
61
+ var MAX_HARNESS_REPORT_CLAIMS = 250;
62
+ var MAX_HARNESS_REPORT_CITATIONS = 400;
63
+ var MAX_HARNESS_REPORT_SUMMARY_LENGTH = 1200;
64
+ function createHarnessReport(options) {
65
+ const adapter = options.analystAdapter && options.analystAdapter !== "true" ? options.analystAdapter : "local-scan";
66
+ if (adapter !== "local-scan" && adapter !== "moda" && adapter !== "local" && adapter !== "scripted") {
67
+ throw new CliInputError(`Analyst adapter '${adapter}' is not implemented in this CLI build yet.`, "Use `moda harness analyze --analyst=claude|codex|cursor`, or `--analyst=local-scan` for the deterministic fallback.");
68
+ }
69
+ const normalizedAdapter = adapter === "moda" || adapter === "local" || adapter === "scripted" ? "local-scan" : adapter;
70
+ const citations = [];
71
+ const citationIndex = new Map;
72
+ const claimBuilders = [];
73
+ const citationIdsForEvidence = (evidence) => evidence.map((item) => upsertCitation(citations, citationIndex, item, options.rootDir)).filter((id) => Boolean(id));
74
+ for (const agent of options.graph.runtimeAgents) {
75
+ const citationIds = citationIdsForEvidence(agent.evidence);
76
+ claimBuilders.push({
77
+ id: stableId("claim", `runtime-agent:${agent.id}`),
78
+ kind: "runtime_agent",
79
+ subjectId: agent.id,
80
+ title: agent.name,
81
+ summary: `${agent.name} is a runtime agent rooted at ${agent.root}.`,
82
+ confidence: agent.confidence,
83
+ inference: inferenceFromConfidence(agent.confidence),
84
+ citationIds
85
+ });
86
+ }
87
+ for (const family of options.graph.agentFamilies) {
88
+ const citationIds = citationIdsForEvidence(family.evidence);
89
+ claimBuilders.push({
90
+ id: stableId("claim", `agent-family:${family.id}`),
91
+ kind: "agent_family",
92
+ subjectId: family.id,
93
+ title: family.name,
94
+ summary: `${family.name} groups ${family.runtimeAgentIds.length} runtime agent(s) and ${family.sharedArtifactIds.length} shared artifact(s).`,
95
+ confidence: family.confidence,
96
+ inference: inferenceFromConfidence(family.confidence),
97
+ citationIds
98
+ });
99
+ }
100
+ for (const artifact of options.graph.artifacts) {
101
+ const citationIds = citationIdsForEvidence(artifact.evidence);
102
+ claimBuilders.push({
103
+ id: stableId("claim", `artifact:${artifact.id}`),
104
+ kind: "artifact",
105
+ subjectId: artifact.id,
106
+ title: artifact.name,
107
+ summary: `${artifact.name} is a ${artifact.scope} ${artifact.type} artifact${artifact.sourcePath ? ` at ${artifact.sourcePath}` : ""}.`,
108
+ confidence: artifact.confidence,
109
+ inference: inferenceFromConfidence(artifact.confidence),
110
+ citationIds
111
+ });
112
+ }
113
+ for (const channel of options.graph.channelAdapters) {
114
+ const citationIds = citationIdsForEvidence(channel.evidence);
115
+ claimBuilders.push({
116
+ id: stableId("claim", `channel:${channel.id}`),
117
+ kind: "channel_adapter",
118
+ subjectId: channel.id,
119
+ title: channel.name,
120
+ summary: `${channel.name} is a ${channel.kind} channel adapter.`,
121
+ confidence: channel.confidence,
122
+ inference: inferenceFromConfidence(channel.confidence),
123
+ citationIds
124
+ });
125
+ }
126
+ for (const adapter2 of options.graph.developerAgentAdapters) {
127
+ const citationIds = citationIdsForEvidence(adapter2.evidence);
128
+ claimBuilders.push({
129
+ id: stableId("claim", `developer-adapter:${adapter2.id}`),
130
+ kind: "developer_agent_adapter",
131
+ subjectId: adapter2.id,
132
+ title: adapter2.name,
133
+ summary: `${adapter2.name} is developer-agent guidance at ${adapter2.sourcePath}.`,
134
+ confidence: adapter2.confidence,
135
+ inference: inferenceFromConfidence(adapter2.confidence),
136
+ citationIds
137
+ });
138
+ }
139
+ for (const identity of options.graph.identityNamespaces) {
140
+ const citationIds = citationIdsForEvidence(identity.evidence);
141
+ claimBuilders.push({
142
+ id: stableId("claim", `identity:${identity.id}`),
143
+ kind: "identity_namespace",
144
+ subjectId: identity.id,
145
+ title: identity.name,
146
+ summary: `${identity.name} is a ${identity.kind} identity namespace${identity.canonicalId ? ` resolving to ${identity.canonicalId}` : ""}.`,
147
+ confidence: identity.confidence,
148
+ inference: inferenceFromConfidence(identity.confidence),
149
+ citationIds
150
+ });
151
+ }
152
+ for (const environment of options.graph.environments) {
153
+ const citationIds = citationIdsForEvidence(environment.evidence);
154
+ claimBuilders.push({
155
+ id: stableId("claim", `environment:${environment.id}`),
156
+ kind: "environment",
157
+ subjectId: environment.id,
158
+ title: environment.name,
159
+ summary: `${environment.name} is a ${environment.kind} environment.`,
160
+ confidence: environment.confidence,
161
+ inference: inferenceFromConfidence(environment.confidence),
162
+ citationIds
163
+ });
164
+ }
165
+ for (const framework of options.graph.agentFrameworks) {
166
+ const citationIds = citationIdsForEvidence(framework.evidence);
167
+ claimBuilders.push({
168
+ id: stableId("claim", `framework:${framework.id}`),
169
+ kind: "agent_framework",
170
+ subjectId: framework.id,
171
+ title: framework.name,
172
+ summary: `${framework.name} appears to be used as an agent framework.`,
173
+ confidence: framework.confidence,
174
+ inference: "mixed",
175
+ citationIds
176
+ });
177
+ }
178
+ for (const provider of options.graph.modelProviders) {
179
+ const citationIds = citationIdsForEvidence(provider.evidence);
180
+ claimBuilders.push({
181
+ id: stableId("claim", `provider:${provider.id}`),
182
+ kind: "model_provider",
183
+ subjectId: provider.id,
184
+ title: provider.name,
185
+ summary: `${provider.name} appears to be used as a model provider.`,
186
+ confidence: provider.confidence,
187
+ inference: "mixed",
188
+ citationIds
189
+ });
190
+ }
191
+ for (const deployment of options.graph.deployments) {
192
+ const citationIds = citationIdsForEvidence(deployment.evidence);
193
+ claimBuilders.push({
194
+ id: stableId("claim", `deployment:${deployment.id}`),
195
+ kind: "deployment",
196
+ subjectId: deployment.id,
197
+ title: deployment.name,
198
+ summary: `${deployment.name} appears to be a deployment or CI artifact.`,
199
+ confidence: deployment.confidence,
200
+ inference: inferenceFromConfidence(deployment.confidence),
201
+ citationIds
202
+ });
203
+ }
204
+ for (const relationship of options.graph.relationships) {
205
+ const citationIds = citationIdsForEvidence(relationship.evidence);
206
+ claimBuilders.push({
207
+ id: stableId("claim", `relationship:${relationship.type}:${relationship.from}:${relationship.to}:${relationship.artifactId ?? ""}`),
208
+ kind: "relationship",
209
+ subjectId: `${relationship.from}->${relationship.to}`,
210
+ title: titleFromId(relationship.type),
211
+ summary: `${relationship.from} ${relationship.type.replace(/_/g, " ")} ${relationship.to}.`,
212
+ confidence: citationIds.length > 0 ? 0.76 : 0.4,
213
+ inference: "mixed",
214
+ citationIds
215
+ });
216
+ }
217
+ const unknowns = options.graph.unknowns.map((unknown) => ({
218
+ id: unknown.id,
219
+ question: unknown.question,
220
+ citationIds: citationIdsForEvidence(unknown.evidence)
221
+ }));
222
+ for (const unknown of options.graph.unknowns) {
223
+ const citationIds = citationIdsForEvidence(unknown.evidence);
224
+ claimBuilders.push({
225
+ id: stableId("claim", `unknown:${unknown.id}`),
226
+ kind: "unknown",
227
+ subjectId: unknown.id,
228
+ title: "Open Question",
229
+ summary: unknown.question,
230
+ confidence: 0.2,
231
+ inference: "inferred",
232
+ citationIds
233
+ });
234
+ }
235
+ const graphHash = hashStableJson(graphForReport(options.graph));
236
+ const limitedEvidence = limitReportEvidence(claimBuilders.filter((claim) => claim.citationIds.length > 0).sort((a, b) => a.id.localeCompare(b.id)), citations.sort((a, b) => a.id.localeCompare(b.id)));
237
+ const keptCitationIds = new Set(limitedEvidence.citations.map((citation) => citation.id));
238
+ const limitedUnknowns = unknowns.map((unknown) => ({
239
+ ...unknown,
240
+ citationIds: unknown.citationIds.filter((citationId) => keptCitationIds.has(citationId))
241
+ }));
242
+ return {
243
+ schemaVersion: HARNESS_REPORT_SCHEMA_VERSION,
244
+ reportId: stableId("hreport", `${options.graph.harnessId}:${graphHash}`),
245
+ harnessId: options.graph.harnessId,
246
+ createdAt: new Date().toISOString(),
247
+ analyst: {
248
+ id: "moda-local-scanner",
249
+ adapter: normalizedAdapter,
250
+ mode: "local",
251
+ version: "moda-cli"
252
+ },
253
+ repo: options.graph.repo,
254
+ tenant: options.graph.tenant,
255
+ source: {
256
+ graphSchemaVersion: options.graph.schemaVersion,
257
+ graphHash,
258
+ sourceCommit: options.sourceCommit,
259
+ readPlan: options.graph.scan.readPlan,
260
+ deterministicInventory: true
261
+ },
262
+ summary: summarizeGraph(options.graph),
263
+ graph: options.graph,
264
+ claims: limitedEvidence.claims,
265
+ citations: limitedEvidence.citations,
266
+ syncPlan: {
267
+ command: `moda harness sync --from-report ${HARNESS_REPORT_PATH}`,
268
+ dryRunCommand: `moda harness sync --from-report ${HARNESS_REPORT_PATH} --dry-run`,
269
+ uploads: [
270
+ "approved harness report",
271
+ "sanitized harness graph metadata",
272
+ "citation metadata",
273
+ "graph and report hashes"
274
+ ],
275
+ privacy: [
276
+ "does not upload broad source content by default",
277
+ "secret-looking files are rejected as citations",
278
+ "citations point to local paths and bounded excerpts"
279
+ ]
280
+ },
281
+ unknowns: limitedUnknowns
282
+ };
283
+ }
284
+ function validateHarnessReport(report, rootDir, options) {
285
+ const checks = [];
286
+ const errors = [];
287
+ const warnings = [];
288
+ const check = (id, pass, message) => {
289
+ checks.push({ id, status: pass ? "pass" : "fail", message });
290
+ if (!pass)
291
+ errors.push(message);
292
+ };
293
+ const warn = (id, message) => {
294
+ checks.push({ id, status: "warning", message });
295
+ warnings.push(message);
296
+ };
297
+ check("schema_version", report?.schemaVersion === HARNESS_REPORT_SCHEMA_VERSION, `Report schema must be ${HARNESS_REPORT_SCHEMA_VERSION}.`);
298
+ check("report_id", typeof report?.reportId === "string" && report.reportId.length > 0, "Report must include reportId.");
299
+ check("harness_id", typeof report?.harnessId === "string" && report.harnessId.length > 0, "Report must include harnessId.");
300
+ check("graph_present", Boolean(report?.graph?.schemaVersion), "Report must include the sanitized harness graph.");
301
+ check("citations_present", Array.isArray(report?.citations) && report.citations.length > 0, "Report must include citations.");
302
+ check("claims_present", Array.isArray(report?.claims) && report.claims.length > 0, "Report must include claims.");
303
+ const syncPlan = isRecord(report?.syncPlan) ? report.syncPlan : undefined;
304
+ check("sync_plan_present", Boolean(syncPlan), "Report must include syncPlan.");
305
+ if (syncPlan) {
306
+ check("sync_plan_command", typeof syncPlan.command === "string" && syncPlan.command.length > 0, "Report syncPlan must include command.");
307
+ check("sync_plan_dry_run_command", typeof syncPlan.dryRunCommand === "string" && syncPlan.dryRunCommand.length > 0, "Report syncPlan must include dryRunCommand.");
308
+ check("sync_plan_uploads", Array.isArray(syncPlan.uploads), "Report syncPlan uploads must be an array.");
309
+ check("sync_plan_privacy", Array.isArray(syncPlan.privacy), "Report syncPlan privacy must be an array.");
310
+ }
311
+ const reportBytes = Buffer.byteLength(JSON.stringify(report ?? null), "utf8");
312
+ check("report_size_bounded", reportBytes <= MAX_HARNESS_REPORT_BYTES, `Harness report must stay under ${MAX_HARNESS_REPORT_BYTES} bytes; got ${reportBytes}.`);
313
+ if (Array.isArray(report?.claims)) {
314
+ check("claim_count_bounded", report.claims.length <= MAX_HARNESS_REPORT_CLAIMS, `Harness report must include at most ${MAX_HARNESS_REPORT_CLAIMS} claims; got ${report.claims.length}.`);
315
+ }
316
+ if (Array.isArray(report?.citations)) {
317
+ check("citation_count_bounded", report.citations.length <= MAX_HARNESS_REPORT_CITATIONS, `Harness report must include at most ${MAX_HARNESS_REPORT_CITATIONS} citations; got ${report.citations.length}.`);
318
+ }
319
+ if (report?.graph && typeof report.graph === "object") {
320
+ check("graph_schema_version", report.graph.schemaVersion === "harness.v0.1", "Graph schemaVersion must be harness.v0.1.");
321
+ for (const field of HARNESS_GRAPH_ARRAY_FIELDS) {
322
+ check(`graph_${field}_array`, Array.isArray(report.graph[field]), `Graph field ${field} must be an array.`);
323
+ }
324
+ validateGraphItemShapes(report.graph, errors);
325
+ validateCaptureQuality(report.graph, warn);
326
+ }
327
+ if (report?.summary && typeof report.summary === "object") {
328
+ for (const field of HARNESS_REPORT_SUMMARY_FIELDS) {
329
+ check(`summary_${field}_number`, typeof report.summary[field] === "number", `Report summary field ${field} must be a number.`);
330
+ }
331
+ if (report?.graph && typeof report.graph === "object") {
332
+ validateSummaryMatchesGraph(report.summary, report.graph, check);
333
+ }
334
+ } else {
335
+ check("summary_present", false, "Report must include a numeric summary object.");
336
+ }
337
+ const citations = Array.isArray(report?.citations) ? report.citations : [];
338
+ const citationIds = new Set;
339
+ const requireLineCitations = report?.analyst?.mode === "agent";
340
+ for (const [index, citation] of citations.entries()) {
341
+ const label = citationLabel(citation, index);
342
+ if (!isRecord(citation)) {
343
+ errors.push(`Citation ${index + 1} must be an object.`);
344
+ continue;
345
+ }
346
+ if (typeof citation.type !== "string" || !HARNESS_REPORT_CITATION_TYPES.has(citation.type)) {
347
+ errors.push(`Citation ${label} has unsupported type ${String(citation.type)}.`);
348
+ }
349
+ if (typeof citation.id !== "string" || citation.id.length === 0) {
350
+ errors.push(`Citation ${index + 1} must include a string id.`);
351
+ } else if (citationIds.has(citation.id)) {
352
+ errors.push(`Duplicate citation id ${citation.id}.`);
353
+ } else {
354
+ citationIds.add(citation.id);
355
+ }
356
+ if (typeof citation.reason !== "string" || citation.reason.length === 0) {
357
+ errors.push(`Citation ${label} must include a reason.`);
358
+ }
359
+ if (requiresLocalCitationPath(citation.type) && (typeof citation.path !== "string" || citation.path.length === 0)) {
360
+ errors.push(`Citation ${label} must include a file path.`);
361
+ continue;
362
+ }
363
+ validateCitation(citation, rootDir, errors, warnings, { requireLine: requireLineCitations });
364
+ }
365
+ const claims = Array.isArray(report?.claims) ? report.claims : [];
366
+ const claimIds = new Set;
367
+ for (const [index, claim] of claims.entries()) {
368
+ const label = claimLabel(claim, index);
369
+ if (!isRecord(claim)) {
370
+ errors.push(`Claim ${index + 1} must be an object.`);
371
+ continue;
372
+ }
373
+ if (typeof claim.id !== "string" || claim.id.length === 0) {
374
+ errors.push(`Claim ${index + 1} must include a string id.`);
375
+ } else if (claimIds.has(claim.id)) {
376
+ errors.push(`Duplicate claim id ${claim.id}.`);
377
+ } else {
378
+ claimIds.add(claim.id);
379
+ }
380
+ if (typeof claim.kind !== "string" || claim.kind.length === 0) {
381
+ errors.push(`Claim ${label} must include kind.`);
382
+ } else if (!isValidClaimKind(claim.kind)) {
383
+ errors.push(`Claim ${label} has unsupported kind ${claim.kind}.`);
384
+ }
385
+ if (typeof claim.subjectId !== "string" || claim.subjectId.length === 0) {
386
+ errors.push(`Claim ${label} must include subjectId.`);
387
+ } else if (report?.graph && typeof claim.kind === "string" && isValidClaimKind(claim.kind)) {
388
+ validateClaimSubjectExists(report.graph, claim, errors, warnings);
389
+ }
390
+ if (typeof claim.title !== "string" || claim.title.length === 0) {
391
+ errors.push(`Claim ${label} must include title.`);
392
+ }
393
+ if (typeof claim.summary !== "string" || claim.summary.length === 0) {
394
+ errors.push(`Claim ${label} must include summary.`);
395
+ } else if (claim.summary.length > MAX_HARNESS_REPORT_SUMMARY_LENGTH) {
396
+ errors.push(`Claim ${label} summary is too long; keep claims under ${MAX_HARNESS_REPORT_SUMMARY_LENGTH} characters.`);
397
+ }
398
+ if (typeof claim.confidence !== "number" || !Number.isFinite(claim.confidence)) {
399
+ errors.push(`Claim ${label} confidence must be a number between 0 and 1.`);
400
+ } else if (claim.confidence < 0 || claim.confidence > 1) {
401
+ errors.push(`Claim ${label} confidence must be between 0 and 1.`);
402
+ }
403
+ if (typeof claim.inference !== "string" || !["direct", "inferred", "mixed"].includes(claim.inference)) {
404
+ errors.push(`Claim ${label} inference must be direct, inferred, or mixed.`);
405
+ } else if (typeof claim.confidence === "number" && claim.confidence >= 0.9 && claim.inference !== "direct") {
406
+ errors.push(`Claim ${label} has high confidence but inference=${claim.inference}; high-confidence claims must be directly cited.`);
407
+ }
408
+ if (!Array.isArray(claim.citationIds)) {
409
+ errors.push(`Claim ${label} citationIds must be an array.`);
410
+ continue;
411
+ }
412
+ if (claim.citationIds.length === 0) {
413
+ errors.push(`Claim ${label} has no citations.`);
414
+ continue;
415
+ }
416
+ const claimCitationIds = new Set;
417
+ for (const citationId of claim.citationIds) {
418
+ if (typeof citationId !== "string") {
419
+ errors.push(`Claim ${label} references a non-string citation id.`);
420
+ continue;
421
+ }
422
+ if (claimCitationIds.has(citationId)) {
423
+ errors.push(`Claim ${label} references duplicate citation ${citationId}.`);
424
+ }
425
+ claimCitationIds.add(citationId);
426
+ if (!citationIds.has(citationId)) {
427
+ errors.push(`Claim ${label} references missing citation ${citationId}.`);
428
+ }
429
+ }
430
+ }
431
+ validateClaimQuality(claims, report?.graph, warn);
432
+ if (report.source?.graphHash && report.graph) {
433
+ const actualGraphHash = hashStableJson(graphForReport(report.graph));
434
+ check("graph_hash", report.source.graphHash === actualGraphHash, "Report source graph hash must match the embedded graph.");
435
+ }
436
+ const unknowns = Array.isArray(report?.unknowns) ? report.unknowns : [];
437
+ if (unknowns.length > 0) {
438
+ warn("unknowns_present", `${unknowns.length} open harness question(s) require human review.`);
439
+ for (const [index, unknown] of unknowns.entries()) {
440
+ if (!isRecord(unknown)) {
441
+ errors.push(`Unknown ${index + 1} must be an object.`);
442
+ continue;
443
+ }
444
+ if (!Array.isArray(unknown.citationIds) || unknown.citationIds.length === 0) {
445
+ warnings.push(`Unknown ${unknown.id ?? index + 1} has no citations.`);
446
+ continue;
447
+ }
448
+ for (const citationId of unknown.citationIds) {
449
+ if (typeof citationId === "string" && !citationIds.has(citationId)) {
450
+ errors.push(`Unknown ${unknown.id ?? index + 1} references missing citation ${citationId}.`);
451
+ }
452
+ }
453
+ }
454
+ }
455
+ validateDeterministicFloor(report, options?.deterministicFloor, check, warn, options?.concern);
456
+ return {
457
+ schemaVersion: HARNESS_REPORT_VALIDATION_SCHEMA_VERSION,
458
+ reportId: report?.reportId ?? "unknown",
459
+ reportHash: hashHarnessReport(report),
460
+ status: errors.length === 0 ? "pass" : "fail",
461
+ checkedAt: new Date().toISOString(),
462
+ checks,
463
+ errors,
464
+ warnings
465
+ };
466
+ }
467
+ function validateDeterministicFloor(report, floor, check, warn, concern) {
468
+ if (!floor)
469
+ return;
470
+ if (process.env.MODA_HARNESS_FLOOR_CHECK === "0")
471
+ return;
472
+ const applyToolFloor = concern === undefined || concern === "tools";
473
+ const applyTopologyFloors = concern === undefined || concern === "topology";
474
+ const graph = isRecord(report?.graph) ? report.graph : {};
475
+ const toolArtifactCount = Array.isArray(graph.artifacts) ? graph.artifacts.filter((artifact) => isRecord(artifact) && artifact.type === "tool").length : 0;
476
+ const providerCount = safeArrayLength(graph.modelProviders);
477
+ const runtimeAgentCount = safeArrayLength(graph.runtimeAgents);
478
+ if (applyToolFloor && typeof floor.toolCandidates === "number" && floor.toolCandidates >= 5) {
479
+ const minimumTools = Math.ceil(0.5 * floor.toolCandidates);
480
+ check("floor_tools", toolArtifactCount >= minimumTools, `Deterministic scan found ${floor.toolCandidates} high-confidence tool definition candidate(s); the report graph must include at least ${minimumTools} tool artifact(s) but has ${toolArtifactCount}.`);
481
+ }
482
+ const providerPackages = Array.isArray(floor.providerPackages) ? floor.providerPackages.filter((name) => typeof name === "string" && name.length > 0) : [];
483
+ if (applyTopologyFloors && providerPackages.length > 0) {
484
+ check("floor_providers", providerCount > 0, `Manifest dependencies include model-provider package(s) ${providerPackages.join(", ")}, but the report graph.modelProviders is empty.`);
485
+ }
486
+ if (applyTopologyFloors && typeof floor.llmCallSites === "number" && floor.llmCallSites > 0 && runtimeAgentCount === 0) {
487
+ warn("floor_agents", `Deterministic scan found ${floor.llmCallSites} LLM call site(s) but the report graph.runtimeAgents is empty; sync will be blocked by the zero-agent gate.`);
488
+ }
489
+ }
490
+ async function approveHarnessReport(options) {
491
+ const report = readHarnessReport(options.reportPath);
492
+ const validation = validateHarnessReport(report, options.rootDir);
493
+ if (validation.status !== "pass") {
494
+ throw new CliInputError("Harness report validation failed; cannot approve.", validation.errors.slice(0, 5).join(`
495
+ `));
496
+ }
497
+ let approvedBy = "--yes";
498
+ if (!options.yes) {
499
+ if (!options.interactive || process.stdin.isTTY !== true) {
500
+ throw new CliInputError("Harness report approval requires an interactive confirmation or --yes.", "Review the report, then run `moda harness approve --yes`.");
501
+ }
502
+ const { confirm } = await import("@inquirer/prompts");
503
+ const approved = await confirm({
504
+ message: [
505
+ "Approve this harness report for Moda sync?",
506
+ `${report.summary.runtimeAgentCount} runtime agent(s), ${report.summary.artifactCount} artifact(s), ${report.claims.length} cited claim(s).`
507
+ ].join(" "),
508
+ default: false
509
+ });
510
+ if (!approved) {
511
+ throw new CliInputError("Harness report was not approved.", "No sync approval was written. Run `moda harness analyze --analyst=claude|codex|cursor --summary` to request another analyst pass, or edit the report and rerun `moda harness validate-report`.");
512
+ }
513
+ approvedBy = "local-human";
514
+ }
515
+ const approval = {
516
+ schemaVersion: HARNESS_REPORT_APPROVAL_SCHEMA_VERSION,
517
+ reportId: report.reportId,
518
+ reportHash: hashHarnessReport(report),
519
+ approved: true,
520
+ approvedAt: new Date().toISOString(),
521
+ approvedBy,
522
+ reportPath: normalizePath(relative(options.rootDir, options.reportPath)) || HARNESS_REPORT_PATH
523
+ };
524
+ writeJson(join(options.rootDir, HARNESS_REPORT_APPROVAL_PATH), approval);
525
+ return { report, validation, approval };
526
+ }
527
+ function loadHarnessReportForSync(rootDir, reportPath, options) {
528
+ const report = readHarnessReport(reportPath);
529
+ const validation = validateHarnessReport(report, rootDir, options);
530
+ if (validation.status !== "pass") {
531
+ throw new CliInputError("Harness report validation failed; cannot sync.", validation.errors.slice(0, 5).join(`
532
+ `));
533
+ }
534
+ const approvalPath = join(rootDir, HARNESS_REPORT_APPROVAL_PATH);
535
+ const approval = existsSync(approvalPath) ? JSON.parse(readFileSync(approvalPath, "utf8")) : null;
536
+ const approved = approval?.schemaVersion === HARNESS_REPORT_APPROVAL_SCHEMA_VERSION && approval.reportId === report.reportId && approval.reportHash === hashHarnessReport(report) && approval.approved === true;
537
+ return { report, validation, approval, approved };
538
+ }
539
+ function readHarnessReport(reportPath) {
540
+ return normalizeHarnessReport(JSON.parse(readFileSync(reportPath, "utf8")));
541
+ }
542
+ function normalizeHarnessReport(value) {
543
+ const report = cloneJson(value);
544
+ if (!isRecord(report.graph))
545
+ return report;
546
+ const graph = report.graph;
547
+ if (typeof graph.schemaVersion !== "string" && typeof graph.graphSchemaVersion === "string") {
548
+ graph.schemaVersion = graph.graphSchemaVersion;
549
+ }
550
+ if (typeof graph.schemaVersion !== "string" && isRecord(report.source) && typeof report.source.graphSchemaVersion === "string") {
551
+ graph.schemaVersion = report.source.graphSchemaVersion;
552
+ }
553
+ if (typeof graph.harnessId !== "string" && typeof report.harnessId === "string") {
554
+ graph.harnessId = report.harnessId;
555
+ }
556
+ if (!isRecord(graph.repo) && isRecord(report.repo)) {
557
+ graph.repo = report.repo;
558
+ }
559
+ if (!isRecord(graph.tenant) && isRecord(report.tenant)) {
560
+ graph.tenant = report.tenant;
561
+ }
562
+ if (!isRecord(graph.scan)) {
563
+ const source = isRecord(report.source) ? report.source : {};
564
+ const readPlan = isRecord(source.readPlan) ? source.readPlan : defaultHarnessReadPlan();
565
+ graph.scan = {
566
+ createdAt: typeof report.createdAt === "string" ? report.createdAt : new Date().toISOString(),
567
+ scannerVersion: analystAdapter(report),
568
+ mode: "permissioned",
569
+ rootPaths: ["."],
570
+ readPlan,
571
+ skipped: []
572
+ };
573
+ }
574
+ for (const field of HARNESS_GRAPH_ARRAY_FIELDS) {
575
+ if (!Array.isArray(graph[field]))
576
+ graph[field] = [];
577
+ for (const item of graph[field]) {
578
+ normalizeConfidenceField(item, requiresGraphItemConfidence(field) ? 0.5 : undefined);
579
+ normalizeGraphItemFieldAliases(field, item);
580
+ }
581
+ }
582
+ normalizeRelationshipEvidence(graph);
583
+ if (Array.isArray(report.claims)) {
584
+ report.claims = report.claims.map((claim) => normalizeHarnessClaim(claim, graph));
585
+ }
586
+ normalizeHarnessSyncPlan(report);
587
+ return report;
588
+ }
589
+ function normalizeHarnessSyncPlan(report) {
590
+ const syncPlan = isRecord(report.syncPlan) ? report.syncPlan : {};
591
+ if (typeof syncPlan.command !== "string" || syncPlan.command.length === 0) {
592
+ syncPlan.command = `moda harness sync --from-report ${HARNESS_REPORT_PATH}`;
593
+ }
594
+ if (typeof syncPlan.dryRunCommand !== "string" || syncPlan.dryRunCommand.length === 0) {
595
+ syncPlan.dryRunCommand = `moda harness sync --from-report ${HARNESS_REPORT_PATH} --dry-run`;
596
+ }
597
+ if (!Array.isArray(syncPlan.uploads)) {
598
+ syncPlan.uploads = ["approved harness report", "sanitized harness graph metadata", "citation metadata"];
599
+ }
600
+ if (!Array.isArray(syncPlan.privacy)) {
601
+ syncPlan.privacy = [
602
+ "does not upload broad source content by default",
603
+ "secret-looking files are rejected as citations",
604
+ "citations point to local paths and bounded excerpts"
605
+ ];
606
+ }
607
+ report.syncPlan = syncPlan;
608
+ }
609
+ function normalizeRelationshipEvidence(graph) {
610
+ const relationships = graph.relationships;
611
+ if (!Array.isArray(relationships))
612
+ return;
613
+ const evidenceById = graphEvidenceById(graph);
614
+ for (const relationship of relationships) {
615
+ if (!isRecord(relationship))
616
+ continue;
617
+ if (Array.isArray(relationship.evidence) && relationship.evidence.length > 0)
618
+ continue;
619
+ const inferred = [];
620
+ for (const key of ["from", "to", "artifactId"]) {
621
+ const id = relationship[key];
622
+ if (typeof id !== "string")
623
+ continue;
624
+ inferred.push(...evidenceById.get(id) ?? []);
625
+ }
626
+ relationship.evidence = dedupeEvidence(inferred).slice(0, 4);
627
+ }
628
+ }
629
+ function graphEvidenceById(graph) {
630
+ const byId = new Map;
631
+ for (const field of HARNESS_GRAPH_ARRAY_FIELDS) {
632
+ if (field === "relationships")
633
+ continue;
634
+ const collection = graph[field];
635
+ if (!Array.isArray(collection))
636
+ continue;
637
+ for (const item of collection) {
638
+ if (!isRecord(item) || typeof item.id !== "string" || !Array.isArray(item.evidence))
639
+ continue;
640
+ const evidence = item.evidence.filter((entry) => isRecord(entry) && typeof entry.path === "string" && typeof entry.reason === "string").map((entry) => normalizeEvidenceEntry(entry));
641
+ if (evidence.length > 0)
642
+ byId.set(item.id, evidence);
643
+ }
644
+ }
645
+ return byId;
646
+ }
647
+ function dedupeEvidence(evidence) {
648
+ const seen = new Set;
649
+ const deduped = [];
650
+ for (const item of evidence) {
651
+ const key = `${item.path}\x00${item.reason}`;
652
+ if (seen.has(key))
653
+ continue;
654
+ seen.add(key);
655
+ deduped.push(normalizeEvidenceEntry(item));
656
+ }
657
+ return deduped;
658
+ }
659
+ function normalizeEvidenceEntry(item) {
660
+ const line = evidenceLine(item);
661
+ return line ? { path: item.path, reason: item.reason, line } : { path: item.path, reason: item.reason };
662
+ }
663
+ function normalizeHarnessClaim(claim, graph) {
664
+ if (!isRecord(claim))
665
+ return claim;
666
+ if (typeof claim.subjectId !== "string" && Array.isArray(claim.subjectIds) && typeof claim.subjectIds[0] === "string") {
667
+ claim.subjectId = claim.subjectIds[0];
668
+ }
669
+ if (typeof claim.subjectId !== "string" && typeof graph.harnessId === "string") {
670
+ claim.subjectId = graph.harnessId;
671
+ }
672
+ if (typeof claim.summary !== "string" && typeof claim.statement === "string") {
673
+ claim.summary = claim.statement;
674
+ }
675
+ if (typeof claim.title !== "string") {
676
+ claim.title = typeof claim.subjectId === "string" ? titleFromId(claim.subjectId) : claimLabel(claim, 0);
677
+ }
678
+ claim.kind = normalizeClaimKind(claim.kind, claim.subjectId, graph);
679
+ if (typeof claim.id !== "string" || claim.id.length === 0) {
680
+ claim.id = stableId("claim", [
681
+ typeof claim.kind === "string" ? claim.kind : "unknown",
682
+ typeof claim.subjectId === "string" ? claim.subjectId : "",
683
+ typeof claim.title === "string" ? claim.title : "",
684
+ typeof claim.summary === "string" ? claim.summary : ""
685
+ ].join(":"));
686
+ }
687
+ if (typeof claim.inference !== "string" || !isValidClaimInference(claim.inference)) {
688
+ claim.inference = normalizeClaimInference(claim.inference, claim.confidence);
689
+ }
690
+ normalizeConfidenceField(claim, 0.5);
691
+ return claim;
692
+ }
693
+ function isValidClaimInference(value) {
694
+ return value === "direct" || value === "inferred" || value === "mixed";
695
+ }
696
+ function normalizeClaimInference(value, confidence) {
697
+ if (typeof value === "string") {
698
+ const normalized = value.toLowerCase();
699
+ if (normalized.includes("direct"))
700
+ return "direct";
701
+ if (normalized.includes("infer"))
702
+ return "inferred";
703
+ if (normalized.includes("mix"))
704
+ return "mixed";
705
+ }
706
+ return inferenceFromUnknownConfidence(confidence);
707
+ }
708
+ function validateGraphItemShapes(graph, errors) {
709
+ if (typeof graph.harnessId !== "string" || graph.harnessId.length === 0) {
710
+ errors.push("Graph must include harnessId.");
711
+ }
712
+ if (!isRecord(graph.repo)) {
713
+ errors.push("Graph must include repo metadata.");
714
+ }
715
+ if (!isRecord(graph.scan)) {
716
+ errors.push("Graph must include scan metadata.");
717
+ }
718
+ validateGraphCollection(graph, errors, "runtimeAgents", {
719
+ strings: ["id", "name", "kind", "root"],
720
+ arrays: ["entrypointIds", "channelAdapterIds", "artifactIds", "identityNamespaceIds", "deploymentIds", "frameworkIds", "modelProviderIds", "evidence"],
721
+ expected: { kind: "runtime_agent" },
722
+ confidence: true
723
+ });
724
+ validateGraphCollection(graph, errors, "agentFamilies", {
725
+ strings: ["id", "name"],
726
+ arrays: ["runtimeAgentIds", "sharedArtifactIds", "evidence"],
727
+ confidence: true
728
+ });
729
+ validateGraphCollection(graph, errors, "artifacts", {
730
+ strings: ["id", "type", "name", "scope"],
731
+ arrays: ["usedByAgentIds", "evidence"],
732
+ confidence: true
733
+ });
734
+ validateGraphCollection(graph, errors, "developerAgentAdapters", {
735
+ strings: ["id", "name", "kind", "sourcePath"],
736
+ arrays: ["evidence"],
737
+ expected: { kind: "developer_agent_adapter" },
738
+ confidence: true
739
+ });
740
+ validateGraphCollection(graph, errors, "channelAdapters", {
741
+ strings: ["id", "name", "kind"],
742
+ arrays: ["evidence"],
743
+ confidence: true
744
+ });
745
+ validateGraphCollection(graph, errors, "identityNamespaces", {
746
+ strings: ["id", "name", "kind"],
747
+ arrays: ["evidence"],
748
+ confidence: true
749
+ });
750
+ validateGraphCollection(graph, errors, "environments", {
751
+ strings: ["id", "name", "kind"],
752
+ arrays: ["evidence"],
753
+ confidence: true
754
+ });
755
+ validateGraphCollection(graph, errors, "agentFrameworks", {
756
+ strings: ["id", "name", "kind"],
757
+ arrays: ["packageNames", "runtimeAgentIds", "evidence"],
758
+ confidence: true
759
+ });
760
+ validateGraphCollection(graph, errors, "modelProviders", {
761
+ strings: ["id", "name", "kind"],
762
+ arrays: ["packageNames", "runtimeAgentIds", "envVars", "modelHints", "evidence"],
763
+ confidence: true
764
+ });
765
+ validateGraphCollection(graph, errors, "deployments", {
766
+ strings: ["id", "name"],
767
+ arrays: ["evidence"],
768
+ confidence: true
769
+ });
770
+ validateGraphCollection(graph, errors, "relationships", {
771
+ strings: ["type", "from", "to"],
772
+ arrays: ["evidence"]
773
+ });
774
+ validateGraphCollection(graph, errors, "unknowns", {
775
+ strings: ["id", "question"],
776
+ arrays: ["candidateIds", "evidence"]
777
+ });
778
+ }
779
+ function validateGraphCollection(graph, errors, field, shape) {
780
+ const collection = graph[field];
781
+ if (!Array.isArray(collection))
782
+ return;
783
+ for (const [index, item] of collection.entries()) {
784
+ const label = graphItemLabel(field, item, index);
785
+ if (!isRecord(item)) {
786
+ errors.push(`Graph ${field} item ${index + 1} must be an object.`);
787
+ continue;
788
+ }
789
+ for (const key of shape.strings) {
790
+ if (typeof item[key] !== "string" || item[key].length === 0) {
791
+ errors.push(`Graph ${label} must include string ${key}.`);
792
+ }
793
+ }
794
+ for (const key of shape.arrays) {
795
+ if (!Array.isArray(item[key])) {
796
+ errors.push(`Graph ${label} must include array ${key}.`);
797
+ }
798
+ }
799
+ for (const [key, expected] of Object.entries(shape.expected ?? {})) {
800
+ if (item[key] !== expected) {
801
+ errors.push(`Graph ${label} must have ${key}=${expected}.`);
802
+ }
803
+ }
804
+ if (shape.confidence && (typeof item.confidence !== "number" || !Number.isFinite(item.confidence))) {
805
+ errors.push(`Graph ${label} confidence must be a number between 0 and 1.`);
806
+ } else if (shape.confidence && typeof item.confidence === "number" && (item.confidence < 0 || item.confidence > 1)) {
807
+ errors.push(`Graph ${label} confidence must be between 0 and 1.`);
808
+ }
809
+ if (Array.isArray(item.evidence)) {
810
+ for (const [evidenceIndex, evidence] of item.evidence.entries()) {
811
+ if (!isRecord(evidence) || typeof evidence.path !== "string" || typeof evidence.reason !== "string") {
812
+ errors.push(`Graph ${label} evidence ${evidenceIndex + 1} must include path and reason.`);
813
+ continue;
814
+ }
815
+ if (evidence.line !== undefined && (typeof evidence.line !== "number" || !Number.isFinite(evidence.line) || evidence.line <= 0)) {
816
+ errors.push(`Graph ${label} evidence ${evidenceIndex + 1} line must be a positive number.`);
817
+ }
818
+ }
819
+ }
820
+ if (field === "artifacts" && requiresArtifactEvidenceLine(item.type) && !artifactHasRetrievableLocation(item)) {
821
+ errors.push(`Graph ${label} is a ${String(item.type)} artifact and must include a numeric evidence line (or a dedicated whole-file source) so its exact text can be retrieved.`);
822
+ }
823
+ if (field === "artifacts" && requiresArtifactBody(item.type) && !artifactHasBody(item)) {
824
+ errors.push(`Graph ${label} is a ${String(item.type)} artifact and must include a non-empty string body with its usable content.`);
825
+ }
826
+ }
827
+ }
828
+ function requiresArtifactBody(type) {
829
+ return type === "prompt" || type === "tool";
830
+ }
831
+ function artifactHasBody(item) {
832
+ return typeof item.body === "string" && item.body.trim().length > 0;
833
+ }
834
+ function requiresArtifactEvidenceLine(type) {
835
+ return type === "prompt" || type === "tool";
836
+ }
837
+ function artifactHasRetrievableLocation(item) {
838
+ if (hasEvidenceLine(item.evidence))
839
+ return true;
840
+ return isWholeFileArtifact(item);
841
+ }
842
+ function validateCaptureQuality(graph, warn) {
843
+ const scan = graph.scan;
844
+ if (!isRecord(scan))
845
+ return;
846
+ if (scan.truncated === true) {
847
+ const reasons = Array.isArray(scan.truncationReasons) && scan.truncationReasons.length > 0 ? scan.truncationReasons.map((reason) => String(reason)).join(", ") : "scan budget or upload cap reached";
848
+ warn("scan_truncated", `Harness scan is partial (truncated): ${reasons}. Re-run with --path=<agent-dir> or review skipped files in the report.`);
849
+ }
850
+ if (typeof scan.incompleteArtifactCount === "number" && scan.incompleteArtifactCount > 0) {
851
+ warn("artifacts_missing_body", `${scan.incompleteArtifactCount} prompt/tool artifact(s) are still missing captured body content after hydration.`);
852
+ }
853
+ }
854
+ function isWholeFileArtifact(item) {
855
+ if (typeof item.sourcePath !== "string" || item.sourcePath.length === 0)
856
+ return false;
857
+ if (!Array.isArray(item.evidence))
858
+ return false;
859
+ return item.evidence.some((entry) => isRecord(entry) && entry.path === item.sourcePath);
860
+ }
861
+ function hasEvidenceLine(evidence) {
862
+ if (!Array.isArray(evidence))
863
+ return false;
864
+ return evidence.some((entry) => isRecord(entry) && typeof entry.path === "string" && typeof entry.reason === "string" && evidenceLine({
865
+ path: entry.path,
866
+ reason: entry.reason,
867
+ line: typeof entry.line === "number" ? entry.line : undefined
868
+ }) !== undefined);
869
+ }
870
+ function normalizeGraphItemFieldAliases(collection, item) {
871
+ if (!isRecord(item))
872
+ return;
873
+ if (collection === "developerAgentAdapters") {
874
+ item.kind = "developer_agent_adapter";
875
+ if (typeof item.sourcePath !== "string")
876
+ item.sourcePath = firstEvidencePath(item) ?? ".";
877
+ }
878
+ if (collection === "channelAdapters" && typeof item.kind !== "string")
879
+ item.kind = "unknown";
880
+ if (collection === "identityNamespaces" && typeof item.kind !== "string")
881
+ item.kind = "unknown";
882
+ if (collection === "environments" && typeof item.kind !== "string")
883
+ item.kind = "environment";
884
+ if (collection === "agentFrameworks" && typeof item.kind !== "string")
885
+ item.kind = "agent_framework";
886
+ if (collection === "modelProviders" && typeof item.kind !== "string")
887
+ item.kind = "model_provider";
888
+ if (collection === "relationships") {
889
+ if (typeof item.from !== "string") {
890
+ if (typeof item.fromId === "string")
891
+ item.from = item.fromId;
892
+ else if (typeof item.sourceId === "string")
893
+ item.from = item.sourceId;
894
+ else if (typeof item.source === "string")
895
+ item.from = item.source;
896
+ }
897
+ if (typeof item.to !== "string") {
898
+ if (typeof item.toId === "string")
899
+ item.to = item.toId;
900
+ else if (typeof item.targetId === "string")
901
+ item.to = item.targetId;
902
+ else if (typeof item.target === "string")
903
+ item.to = item.target;
904
+ }
905
+ if (typeof item.type !== "string" && typeof item.kind === "string")
906
+ item.type = item.kind;
907
+ }
908
+ }
909
+ function firstEvidencePath(item) {
910
+ if (typeof item.sourcePath === "string" && item.sourcePath.length > 0)
911
+ return item.sourcePath;
912
+ if (!Array.isArray(item.evidence))
913
+ return;
914
+ for (const evidence of item.evidence) {
915
+ if (isRecord(evidence) && typeof evidence.path === "string" && evidence.path.length > 0) {
916
+ return evidence.path;
917
+ }
918
+ }
919
+ return;
920
+ }
921
+ function stampRemoteSnapshotTruncation(report) {
922
+ const scanRecord = isRecord(report.graph.scan) ? report.graph.scan : {};
923
+ const truncationReasons = Array.isArray(scanRecord.truncationReasons) ? scanRecord.truncationReasons.map(String) : [];
924
+ const stamped = {
925
+ ...report,
926
+ graph: {
927
+ ...report.graph,
928
+ scan: {
929
+ ...report.graph.scan,
930
+ truncated: true,
931
+ truncationReasons: [
932
+ ...truncationReasons,
933
+ "remote source snapshot truncated before upload"
934
+ ]
935
+ }
936
+ }
937
+ };
938
+ if (stamped.source?.graphHash) {
939
+ stamped.source = { ...stamped.source, graphHash: hashStableJson(graphForReport(stamped.graph)) };
940
+ }
941
+ return stamped;
942
+ }
943
+ function requiresGraphItemConfidence(collection) {
944
+ return collection !== "relationships" && collection !== "unknowns";
945
+ }
946
+ function graphItemLabel(collection, item, index) {
947
+ if (isRecord(item) && typeof item.id === "string" && item.id.length > 0)
948
+ return `${collection}.${item.id}`;
949
+ return `${collection} item ${index + 1}`;
950
+ }
951
+ function normalizeConfidenceField(value, fallback) {
952
+ if (!isRecord(value))
953
+ return;
954
+ const confidence = confidenceNumberFromUnknown(value.confidence);
955
+ if (confidence !== undefined)
956
+ value.confidence = confidence;
957
+ else if (fallback !== undefined)
958
+ value.confidence = fallback;
959
+ }
960
+ function confidenceNumberFromUnknown(value) {
961
+ if (typeof value === "number" && Number.isFinite(value))
962
+ return value;
963
+ if (typeof value !== "string")
964
+ return;
965
+ const normalized = value.trim().toLowerCase();
966
+ if (normalized === "high")
967
+ return 0.85;
968
+ if (normalized === "medium" || normalized === "med")
969
+ return 0.65;
970
+ if (normalized === "low")
971
+ return 0.35;
972
+ const parsed = Number(normalized);
973
+ return Number.isFinite(parsed) ? parsed : undefined;
974
+ }
975
+ function inferenceFromUnknownConfidence(value) {
976
+ const confidence = confidenceNumberFromUnknown(value);
977
+ return confidence === undefined ? "mixed" : inferenceFromConfidence(confidence);
978
+ }
979
+ function inferClaimKind(subjectId, graph) {
980
+ if (typeof subjectId === "string") {
981
+ const collectionKinds = [
982
+ ["runtimeAgents", "runtime_agent"],
983
+ ["agentFamilies", "agent_family"],
984
+ ["developerAgentAdapters", "developer_agent_adapter"],
985
+ ["artifacts", "artifact"],
986
+ ["channelAdapters", "channel_adapter"],
987
+ ["identityNamespaces", "identity_namespace"],
988
+ ["environments", "environment"],
989
+ ["agentFrameworks", "agent_framework"],
990
+ ["modelProviders", "model_provider"],
991
+ ["deployments", "deployment"],
992
+ ["relationships", "relationship"],
993
+ ["unknowns", "unknown"]
994
+ ];
995
+ for (const [field, kind] of collectionKinds) {
996
+ const collection = graph[field];
997
+ if (Array.isArray(collection) && collection.some((item) => isRecord(item) && item.id === subjectId)) {
998
+ return kind;
999
+ }
1000
+ }
1001
+ if (/^(agent|runtime)/i.test(subjectId))
1002
+ return "runtime_agent";
1003
+ if (/^(family|fam)/i.test(subjectId))
1004
+ return "agent_family";
1005
+ if (/^(developer|dev|daa)/i.test(subjectId))
1006
+ return "developer_agent_adapter";
1007
+ if (/^(artifact|prompt|tool|skill|eval|memory|guardrail)/i.test(subjectId))
1008
+ return "artifact";
1009
+ if (/^(channel|adapter)/i.test(subjectId))
1010
+ return "channel_adapter";
1011
+ if (/^(identity|user)/i.test(subjectId))
1012
+ return "identity_namespace";
1013
+ if (/^(environment|env)/i.test(subjectId))
1014
+ return "environment";
1015
+ if (/^(framework|fw)/i.test(subjectId))
1016
+ return "agent_framework";
1017
+ if (/^(provider|model)/i.test(subjectId))
1018
+ return "model_provider";
1019
+ if (/^(deployment|deploy)/i.test(subjectId))
1020
+ return "deployment";
1021
+ }
1022
+ return "unknown";
1023
+ }
1024
+ function normalizeClaimKind(value, subjectId, graph) {
1025
+ const inferred = inferClaimKind(subjectId, graph);
1026
+ if (typeof value === "string") {
1027
+ const normalized = value.trim().replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[\s-]+/g, "_").toLowerCase();
1028
+ const aliases = {
1029
+ agent: "runtime_agent",
1030
+ runtime: "runtime_agent",
1031
+ runtimeagent: "runtime_agent",
1032
+ runtime_agent: "runtime_agent",
1033
+ agentfamily: "agent_family",
1034
+ agent_family: "agent_family",
1035
+ family: "agent_family",
1036
+ developeragentadapter: "developer_agent_adapter",
1037
+ developer_agent_adapter: "developer_agent_adapter",
1038
+ developer_adapter: "developer_agent_adapter",
1039
+ dev_agent_adapter: "developer_agent_adapter",
1040
+ artifact: "artifact",
1041
+ prompt: "artifact",
1042
+ tool: "artifact",
1043
+ skill: "artifact",
1044
+ channel: "channel_adapter",
1045
+ channel_adapter: "channel_adapter",
1046
+ adapter: "channel_adapter",
1047
+ identity: "identity_namespace",
1048
+ identity_namespace: "identity_namespace",
1049
+ environment: "environment",
1050
+ env: "environment",
1051
+ framework: "agent_framework",
1052
+ agent_framework: "agent_framework",
1053
+ modelprovider: "model_provider",
1054
+ model_provider: "model_provider",
1055
+ provider: "model_provider",
1056
+ deployment: "deployment",
1057
+ deploy: "deployment",
1058
+ relationship: "relationship",
1059
+ unknown: "unknown",
1060
+ repo: "unknown",
1061
+ repository: "unknown",
1062
+ harness: "unknown"
1063
+ };
1064
+ const aliased = aliases[normalized];
1065
+ if (aliased) {
1066
+ if (aliased === "channel_adapter" && inferred === "developer_agent_adapter")
1067
+ return inferred;
1068
+ return aliased;
1069
+ }
1070
+ }
1071
+ return inferred;
1072
+ }
1073
+ function analystAdapter(report) {
1074
+ const analyst = isRecord(report.analyst) ? report.analyst : {};
1075
+ return typeof analyst.adapter === "string" ? analyst.adapter : "external-analyst";
1076
+ }
1077
+ function defaultHarnessReadPlan() {
1078
+ return {
1079
+ scope: "broad",
1080
+ rootPaths: ["."],
1081
+ willRead: [
1082
+ "package manifests",
1083
+ "source files under the selected path",
1084
+ "prompt files",
1085
+ "tool definitions",
1086
+ "skill/rule files",
1087
+ "eval/test files",
1088
+ "deployment and CI config"
1089
+ ],
1090
+ willSkip: [
1091
+ "secret-looking files",
1092
+ "symlinks",
1093
+ "node_modules and build output",
1094
+ ".moda runtime files",
1095
+ "large files over 512KB"
1096
+ ],
1097
+ skipsSecretFiles: true,
1098
+ followsSymlinks: false,
1099
+ uploadsSourceByDefault: false
1100
+ };
1101
+ }
1102
+ function cloneJson(value) {
1103
+ return JSON.parse(JSON.stringify(value));
1104
+ }
1105
+ function limitReportEvidence(claims, citations) {
1106
+ const citationsById = new Map(citations.map((citation) => [citation.id, citation]));
1107
+ const keptClaims = [];
1108
+ const keptCitationIds = new Set;
1109
+ for (const claim of claims) {
1110
+ if (keptClaims.length >= MAX_HARNESS_REPORT_CLAIMS)
1111
+ break;
1112
+ const claimCitationIds = [...new Set(claim.citationIds)].filter((citationId) => citationsById.has(citationId));
1113
+ if (claimCitationIds.length === 0)
1114
+ continue;
1115
+ const nextCitationCount = new Set([...keptCitationIds, ...claimCitationIds]).size;
1116
+ if (nextCitationCount > MAX_HARNESS_REPORT_CITATIONS)
1117
+ continue;
1118
+ keptClaims.push({
1119
+ ...claim,
1120
+ citationIds: claimCitationIds
1121
+ });
1122
+ for (const citationId of claimCitationIds)
1123
+ keptCitationIds.add(citationId);
1124
+ }
1125
+ return {
1126
+ claims: keptClaims,
1127
+ citations: citations.filter((citation) => keptCitationIds.has(citation.id))
1128
+ };
1129
+ }
1130
+ function resolveHarnessReportPath(rootDir, value) {
1131
+ return resolve(rootDir, value && value !== "true" ? value : HARNESS_REPORT_PATH);
1132
+ }
1133
+ function writeHarnessReportArtifacts(options) {
1134
+ const written = [];
1135
+ writeJson(join(options.rootDir, HARNESS_REPORT_PATH), options.report);
1136
+ written.push(HARNESS_REPORT_PATH);
1137
+ writeJson(join(options.rootDir, HARNESS_REPORT_VALIDATION_PATH), options.validation);
1138
+ written.push(HARNESS_REPORT_VALIDATION_PATH);
1139
+ if (options.markdown) {
1140
+ const markdownPath = join(options.rootDir, HARNESS_REPORT_MARKDOWN_PATH);
1141
+ mkdirSync(dirname(markdownPath), { recursive: true });
1142
+ writeFileSync(markdownPath, renderHarnessReportMarkdown(options.report, options.validation));
1143
+ written.push(HARNESS_REPORT_MARKDOWN_PATH);
1144
+ }
1145
+ return written;
1146
+ }
1147
+ function renderHarnessReportMarkdown(report, validation) {
1148
+ const summary = isRecord(report.summary) ? report.summary : {};
1149
+ const citations = Array.isArray(report.citations) ? report.citations : [];
1150
+ const claims = Array.isArray(report.claims) ? report.claims : [];
1151
+ const unknowns = Array.isArray(report.unknowns) ? report.unknowns : [];
1152
+ const lines = [
1153
+ "# Moda Harness Report",
1154
+ "",
1155
+ `Report ID: ${report.reportId}`,
1156
+ `Harness ID: ${report.harnessId}`,
1157
+ `Analyst: ${report.analyst.id} (${report.analyst.adapter})`,
1158
+ `Validation: ${validation.status}`,
1159
+ "",
1160
+ "## Summary",
1161
+ "",
1162
+ `- Runtime agents: ${formatSummaryCount(summary.runtimeAgentCount)}`,
1163
+ `- Agent families: ${formatSummaryCount(summary.agentFamilyCount)}`,
1164
+ `- Artifacts: ${formatSummaryCount(summary.artifactCount)}`,
1165
+ `- Citations: ${citations.length}`,
1166
+ `- Claims: ${claims.length}`,
1167
+ "",
1168
+ "## Claims",
1169
+ ""
1170
+ ];
1171
+ for (const [index, claim] of claims.slice(0, 80).entries()) {
1172
+ const claimRecord = isRecord(claim) ? claim : {};
1173
+ const citationIds = Array.isArray(claimRecord.citationIds) ? claimRecord.citationIds : [];
1174
+ const citedPaths = citationIds.map((id) => citations.find((citation) => citation.id === id)).filter((citation) => Boolean(citation)).map((citation) => `${citation.path ?? citation.id}${citation.line ? `:${citation.line}` : ""}`).join(", ");
1175
+ lines.push(`### ${typeof claimRecord.title === "string" ? claimRecord.title : claimLabel(claim, index)}`, "", `- Kind: ${typeof claimRecord.kind === "string" ? claimRecord.kind : "unknown"}`, `- Confidence: ${formatConfidence(claimRecord.confidence)}`, `- Evidence: ${citedPaths || "none"}`, `- Summary: ${typeof claimRecord.summary === "string" ? claimRecord.summary : "unknown"}`, "");
1176
+ }
1177
+ if (claims.length > 80) {
1178
+ lines.push(`_Only the first 80 of ${claims.length} claims are shown._`, "");
1179
+ }
1180
+ if (unknowns.length > 0) {
1181
+ lines.push("## Open Questions", "");
1182
+ for (const unknown of unknowns) {
1183
+ lines.push(`- ${isRecord(unknown) && typeof unknown.question === "string" ? unknown.question : "Unknown harness question"}`);
1184
+ }
1185
+ lines.push("");
1186
+ }
1187
+ lines.push("## Sync Plan", "", `- Dry run: \`${report.syncPlan.dryRunCommand}\``, `- Sync after approval: \`${report.syncPlan.command}\``, "");
1188
+ return `${lines.join(`
1189
+ `)}
1190
+ `;
1191
+ }
1192
+ function claimLabel(claim, index) {
1193
+ if (isRecord(claim)) {
1194
+ if (typeof claim.id === "string" && claim.id.length > 0)
1195
+ return claim.id;
1196
+ if (typeof claim.title === "string" && claim.title.length > 0)
1197
+ return claim.title;
1198
+ }
1199
+ return `claim #${index + 1}`;
1200
+ }
1201
+ function citationLabel(citation, index) {
1202
+ if (isRecord(citation) && typeof citation.id === "string" && citation.id.length > 0)
1203
+ return citation.id;
1204
+ return `#${index + 1}`;
1205
+ }
1206
+ function formatConfidence(value) {
1207
+ return typeof value === "number" && Number.isFinite(value) ? value.toFixed(2) : "invalid";
1208
+ }
1209
+ function formatSummaryCount(value) {
1210
+ return typeof value === "number" && Number.isFinite(value) ? String(value) : "unknown";
1211
+ }
1212
+ function hashHarnessReport(report) {
1213
+ return hashStableJson(report);
1214
+ }
1215
+ function hashStableJson(value) {
1216
+ return createHash("sha256").update(JSON.stringify(sortJson(value))).digest("hex");
1217
+ }
1218
+ function graphForReport(graph) {
1219
+ return sortJson({
1220
+ ...graph,
1221
+ scan: {
1222
+ ...graph.scan,
1223
+ createdAt: undefined
1224
+ }
1225
+ });
1226
+ }
1227
+ function summarizeGraph(graph) {
1228
+ return {
1229
+ runtimeAgentCount: graph.runtimeAgents.length,
1230
+ agentFamilyCount: graph.agentFamilies.length,
1231
+ artifactCount: graph.artifacts.length,
1232
+ sharedArtifactCount: graph.artifacts.filter((artifact) => artifact.scope === "shared").length,
1233
+ channelAdapterCount: graph.channelAdapters.length,
1234
+ identityNamespaceCount: graph.identityNamespaces.length,
1235
+ environmentCount: graph.environments.length,
1236
+ agentFrameworkCount: graph.agentFrameworks.length,
1237
+ modelProviderCount: graph.modelProviders.length,
1238
+ deploymentCount: graph.deployments.length,
1239
+ relationshipCount: graph.relationships.length,
1240
+ unknownCount: graph.unknowns.length
1241
+ };
1242
+ }
1243
+ function upsertCitation(citations, citationIndex, evidence, rootDir) {
1244
+ if (!evidence.path)
1245
+ return;
1246
+ if (evidence.path === ".")
1247
+ return;
1248
+ const line = evidenceLine(evidence);
1249
+ const key = `${evidence.path}:${line ?? ""}:${evidence.reason}`;
1250
+ const existing = citationIndex.get(key);
1251
+ if (existing)
1252
+ return existing;
1253
+ const id = stableId("cite", key);
1254
+ citations.push({
1255
+ id,
1256
+ type: "file",
1257
+ path: evidence.path,
1258
+ ...line ? { line } : {},
1259
+ reason: evidence.reason,
1260
+ ...citationExcerpt(rootDir, evidence.path, line)
1261
+ });
1262
+ citationIndex.set(key, id);
1263
+ return id;
1264
+ }
1265
+ function citationExcerpt(rootDir, path, line) {
1266
+ if (isUnsafeCitationPath(path))
1267
+ return {};
1268
+ const absPath = resolve(rootDir, path);
1269
+ if (!isInside(rootDir, absPath) || !existsSync(absPath))
1270
+ return {};
1271
+ try {
1272
+ const stat = lstatSync(absPath);
1273
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 131072)
1274
+ return {};
1275
+ const lines = readFileSync(absPath, "utf8").split(/\r?\n/);
1276
+ const selected = line ? lines[line - 1] : lines.find((candidate) => candidate.trim().length > 0);
1277
+ if (!selected)
1278
+ return {};
1279
+ return { excerpt: selected.trim() };
1280
+ } catch {
1281
+ return {};
1282
+ }
1283
+ }
1284
+ function validateSummaryMatchesGraph(summary, graph, check) {
1285
+ if (!isRecord(graph))
1286
+ return;
1287
+ const fields = {
1288
+ runtimeAgentCount: safeArrayLength(graph.runtimeAgents),
1289
+ agentFamilyCount: safeArrayLength(graph.agentFamilies),
1290
+ artifactCount: safeArrayLength(graph.artifacts),
1291
+ sharedArtifactCount: Array.isArray(graph.artifacts) ? graph.artifacts.filter((artifact) => isRecord(artifact) && artifact.scope === "shared").length : 0,
1292
+ channelAdapterCount: safeArrayLength(graph.channelAdapters),
1293
+ identityNamespaceCount: safeArrayLength(graph.identityNamespaces),
1294
+ environmentCount: safeArrayLength(graph.environments),
1295
+ agentFrameworkCount: safeArrayLength(graph.agentFrameworks),
1296
+ modelProviderCount: safeArrayLength(graph.modelProviders),
1297
+ deploymentCount: safeArrayLength(graph.deployments),
1298
+ relationshipCount: safeArrayLength(graph.relationships),
1299
+ unknownCount: safeArrayLength(graph.unknowns)
1300
+ };
1301
+ for (const [field, expected] of Object.entries(fields)) {
1302
+ const actual = summary[field];
1303
+ check(`summary_${field}_matches_graph`, actual === expected, `Report summary ${field} must match graph count ${expected}; got ${String(actual)}.`);
1304
+ }
1305
+ }
1306
+ function safeArrayLength(value) {
1307
+ return Array.isArray(value) ? value.length : 0;
1308
+ }
1309
+ function isValidClaimKind(kind) {
1310
+ return [
1311
+ "runtime_agent",
1312
+ "agent_family",
1313
+ "developer_agent_adapter",
1314
+ "artifact",
1315
+ "channel_adapter",
1316
+ "identity_namespace",
1317
+ "environment",
1318
+ "agent_framework",
1319
+ "model_provider",
1320
+ "deployment",
1321
+ "relationship",
1322
+ "unknown"
1323
+ ].includes(kind);
1324
+ }
1325
+ function validateClaimSubjectExists(graph, claim, errors, warnings) {
1326
+ if (claim.kind === "relationship")
1327
+ return;
1328
+ const collectionName = collectionNameForClaimKind(claim.kind);
1329
+ if (!collectionName)
1330
+ return;
1331
+ const collection = graph[collectionName];
1332
+ if (!Array.isArray(collection))
1333
+ return;
1334
+ if (collection.some((item) => isRecord(item) && item.id === claim.subjectId))
1335
+ return;
1336
+ const message = `Claim ${claim.id} subject ${claim.subjectId} is not present in graph.${collectionName}.`;
1337
+ if (claim.kind === "unknown")
1338
+ warnings.push(message);
1339
+ else
1340
+ errors.push(message);
1341
+ }
1342
+ function validateClaimQuality(claims, graph, warn) {
1343
+ const claimRecords = claims.filter(isRecord);
1344
+ if (claimRecords.length === 0)
1345
+ return;
1346
+ const graphClaimableItems = graph ? safeArrayLength(graph.runtimeAgents) + safeArrayLength(graph.agentFamilies) + safeArrayLength(graph.developerAgentAdapters) + safeArrayLength(graph.artifacts) + safeArrayLength(graph.channelAdapters) + safeArrayLength(graph.identityNamespaces) + safeArrayLength(graph.agentFrameworks) + safeArrayLength(graph.modelProviders) + safeArrayLength(graph.deployments) + safeArrayLength(graph.relationships) : 0;
1347
+ const unknownKindCount = claimRecords.filter((claim) => claim.kind === "unknown").length;
1348
+ if (graphClaimableItems > 0 && unknownKindCount === claimRecords.length) {
1349
+ warn("claims_all_unknown", "Every report claim is kind=unknown even though the graph has claimable items; claims should point at graph item ids and kinds.");
1350
+ }
1351
+ const titles = claimRecords.map((claim) => typeof claim.title === "string" ? claim.title.trim().toLowerCase() : "").filter(Boolean);
1352
+ const uniqueTitles = new Set(titles);
1353
+ if (titles.length >= 4 && uniqueTitles.size <= 1) {
1354
+ warn("claims_repeated_title", "Report claims all use the same title; claim titles should identify the specific agent, artifact, provider, adapter, or relationship.");
1355
+ }
1356
+ }
1357
+ function collectionNameForClaimKind(kind) {
1358
+ switch (kind) {
1359
+ case "runtime_agent":
1360
+ return "runtimeAgents";
1361
+ case "agent_family":
1362
+ return "agentFamilies";
1363
+ case "developer_agent_adapter":
1364
+ return "developerAgentAdapters";
1365
+ case "artifact":
1366
+ return "artifacts";
1367
+ case "channel_adapter":
1368
+ return "channelAdapters";
1369
+ case "identity_namespace":
1370
+ return "identityNamespaces";
1371
+ case "environment":
1372
+ return "environments";
1373
+ case "agent_framework":
1374
+ return "agentFrameworks";
1375
+ case "model_provider":
1376
+ return "modelProviders";
1377
+ case "deployment":
1378
+ return "deployments";
1379
+ case "unknown":
1380
+ return "unknowns";
1381
+ case "relationship":
1382
+ return;
1383
+ }
1384
+ }
1385
+ function requiresLocalCitationPath(type) {
1386
+ return type === "file" || type === "generated_doc";
1387
+ }
1388
+ function citationExcerptMatches(expected, actualLine) {
1389
+ const expectedNormalized = normalizeComparableText(expected);
1390
+ const actualNormalized = normalizeComparableText(actualLine);
1391
+ if (!expectedNormalized || !actualNormalized)
1392
+ return false;
1393
+ return expectedNormalized.includes(actualNormalized) || actualNormalized.includes(expectedNormalized);
1394
+ }
1395
+ function normalizeComparableText(value) {
1396
+ return value.replace(/\s+/g, " ").trim();
1397
+ }
1398
+ function validateCitation(citation, rootDir, errors, warnings, options) {
1399
+ if (citation.type === "command_output" || citation.type === "production_evidence") {
1400
+ if (!citation.excerpt || citation.excerpt.trim().length === 0) {
1401
+ warnings.push(`Citation ${citation.id} is ${citation.type} evidence without an excerpt; reviewers must inspect it manually.`);
1402
+ }
1403
+ return;
1404
+ }
1405
+ if (citation.type !== "file" && citation.type !== "generated_doc") {
1406
+ errors.push(`Citation ${citation.id} has unsupported type ${String(citation.type)}.`);
1407
+ return;
1408
+ }
1409
+ if (!citation.path || isUnsafeCitationPath(citation.path)) {
1410
+ errors.push(`Citation ${citation.id} points to an unsafe path.`);
1411
+ return;
1412
+ }
1413
+ const absPath = resolve(rootDir, citation.path);
1414
+ if (!isInside(rootDir, absPath)) {
1415
+ errors.push(`Citation ${citation.id} points outside the harness root.`);
1416
+ return;
1417
+ }
1418
+ if (!existsSync(absPath)) {
1419
+ errors.push(`Citation ${citation.id} file does not exist: ${citation.path}.`);
1420
+ return;
1421
+ }
1422
+ try {
1423
+ const stat = lstatSync(absPath);
1424
+ if (stat.isSymbolicLink())
1425
+ errors.push(`Citation ${citation.id} points to a symlink.`);
1426
+ if (stat.isDirectory()) {
1427
+ warnings.push(`Citation ${citation.id} points to a directory; no excerpt was captured.`);
1428
+ return;
1429
+ }
1430
+ if (!stat.isFile())
1431
+ errors.push(`Citation ${citation.id} does not point to a file.`);
1432
+ const lines = readFileSync(absPath, "utf8").split(/\r?\n/);
1433
+ if (citation.line !== undefined) {
1434
+ const lineCount = lines.length;
1435
+ if (citation.line < 1 || citation.line > lineCount) {
1436
+ errors.push(`Citation ${citation.id} line ${citation.line} is outside ${citation.path}.`);
1437
+ } else if (citation.excerpt && !citationExcerptMatches(citation.excerpt, lines[citation.line - 1] ?? "")) {
1438
+ errors.push(`Citation ${citation.id} excerpt no longer matches ${citation.path}:${citation.line}.`);
1439
+ }
1440
+ } else if (options.requireLine) {
1441
+ errors.push(`Citation ${citation.id} must include a line number for agent-authored reports.`);
1442
+ } else {
1443
+ warnings.push(`Citation ${citation.id} has no line number; confidence is lower for this evidence.`);
1444
+ }
1445
+ if (citation.line !== undefined && (!citation.excerpt || citation.excerpt.trim().length === 0)) {
1446
+ warnings.push(`Citation ${citation.id} has a line number but no excerpt.`);
1447
+ }
1448
+ } catch (error) {
1449
+ warnings.push(`Citation ${citation.id} could not be fully inspected: ${error instanceof Error ? error.message : String(error)}.`);
1450
+ }
1451
+ }
1452
+ function lineFromReason(reason) {
1453
+ const match = reason.match(/\bline\s+(\d+)\b/i);
1454
+ if (!match)
1455
+ return;
1456
+ const value = Number(match[1]);
1457
+ return Number.isFinite(value) && value > 0 ? value : undefined;
1458
+ }
1459
+ function evidenceLine(evidence) {
1460
+ if (typeof evidence.line === "number" && Number.isFinite(evidence.line) && evidence.line > 0) {
1461
+ return Math.trunc(evidence.line);
1462
+ }
1463
+ return lineFromReason(evidence.reason);
1464
+ }
1465
+ function inferenceFromConfidence(confidence) {
1466
+ if (confidence >= 0.85)
1467
+ return "direct";
1468
+ if (confidence >= 0.65)
1469
+ return "mixed";
1470
+ return "inferred";
1471
+ }
1472
+ function isUnsafeCitationPath(path) {
1473
+ return path.startsWith("/") || path.includes("..") || path.split(/[\\/]/).some((part) => part === "..") || isSecretishPath(path);
1474
+ }
1475
+ function isSecretishPath(path) {
1476
+ const name = basename(path).toLowerCase();
1477
+ if (name === ".env.example" || name === ".env.sample" || name === ".env.template" || name === ".env.defaults" || /^\.env\..*(example|sample|template)$/.test(name)) {
1478
+ return false;
1479
+ }
1480
+ return name === ".env" || name.startsWith(".env.") || name.includes("secret") || name.includes("private-key") || name.endsWith(".pem");
1481
+ }
1482
+ function isInside(rootDir, absPath) {
1483
+ const rel = relative(resolve(rootDir), resolve(absPath));
1484
+ return rel === "" || !rel.startsWith("..") && !rel.startsWith("/");
1485
+ }
1486
+ function writeJson(path, value) {
1487
+ mkdirSync(dirname(path), { recursive: true });
1488
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}
1489
+ `);
1490
+ }
1491
+ function stableId(prefix, input) {
1492
+ const slug = input.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 64);
1493
+ const digest = createHash("sha256").update(input).digest("hex").slice(0, 12);
1494
+ if (!slug)
1495
+ return `${prefix}_${digest}`;
1496
+ return slug.length >= 64 ? `${prefix}_${slug.slice(0, 51)}_${digest}` : `${prefix}_${slug}`;
1497
+ }
1498
+ function titleFromId(id) {
1499
+ return id.replace(/^[a-z]+_/, "").split(/[_\-/]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ") || id;
1500
+ }
1501
+ function sortJson(value) {
1502
+ if (Array.isArray(value))
1503
+ return value.map((item) => sortJson(item));
1504
+ if (value && typeof value === "object") {
1505
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, sortJson(item)]));
1506
+ }
1507
+ return value;
1508
+ }
1509
+ function isRecord(value) {
1510
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1511
+ }
1512
+ function normalizePath(path) {
1513
+ return path.replace(/\\/g, "/");
1514
+ }
1515
+
1516
+ export { HARNESS_REPORT_SCHEMA_VERSION, HARNESS_REPORT_VALIDATION_SCHEMA_VERSION, HARNESS_REPORT_APPROVAL_SCHEMA_VERSION, HARNESS_REPORT_PATH, HARNESS_REPORT_MARKDOWN_PATH, HARNESS_REPORT_VALIDATION_PATH, HARNESS_REPORT_APPROVAL_PATH, HARNESS_ANALYZE_CONCERNS, createHarnessReport, validateHarnessReport, approveHarnessReport, loadHarnessReportForSync, readHarnessReport, normalizeHarnessReport, requiresArtifactBody, artifactHasBody, firstEvidencePath, stampRemoteSnapshotTruncation, resolveHarnessReportPath, writeHarnessReportArtifacts, renderHarnessReportMarkdown, hashHarnessReport, hashStableJson, graphForReport };