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