agentic-scorecard 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT_PROMPT.md +33 -28
- package/README.md +44 -22
- package/benchmark/v0.3/adapter-contract.md +44 -0
- package/benchmark/v0.3/adapters/known-agent-harnesses.yaml +177 -0
- package/benchmark/v0.3/adapters/known-python-tooling.yaml +18 -0
- package/benchmark/v0.3/adapters/known-security-tools.yaml +6 -0
- package/benchmark/v0.3/agent-evidence-schema.json +67 -0
- package/benchmark/v0.3/attestation-schema.json +26 -0
- package/benchmark/v0.3/benchmark.yaml +127 -0
- package/benchmark/v0.3/controls/context.yaml +79 -0
- package/benchmark/v0.3/controls/environment.yaml +93 -0
- package/benchmark/v0.3/controls/governance.yaml +97 -0
- package/benchmark/v0.3/controls/learning.yaml +80 -0
- package/benchmark/v0.3/controls/observability.yaml +91 -0
- package/benchmark/v0.3/controls/resilience.yaml +107 -0
- package/benchmark/v0.3/controls/security.yaml +129 -0
- package/benchmark/v0.3/controls/specification.yaml +83 -0
- package/benchmark/v0.3/controls/testing.yaml +85 -0
- package/benchmark/v0.3/controls/tooling.yaml +69 -0
- package/benchmark/v0.3/report-schema.json +89 -0
- package/benchmark/v0.3/scoring-policy.md +100 -0
- package/dist/cli.js +479 -52
- package/package.json +1 -1
- package/templates/agent-evidence.yaml +3 -3
- package/templates/attestations.yaml +1 -1
- package/templates/baseline.csv +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { appendFile, mkdir, readFile as
|
|
5
|
-
import { dirname as dirname2, join as
|
|
4
|
+
import { appendFile, mkdir, readFile as readFile4, writeFile } from "fs/promises";
|
|
5
|
+
import { dirname as dirname2, join as join3, resolve as resolve4 } from "path";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
import { stringify } from "yaml";
|
|
8
8
|
|
|
@@ -30,6 +30,7 @@ var dimensionIds = [
|
|
|
30
30
|
var DimensionIdSchema = z.enum(dimensionIds);
|
|
31
31
|
var LevelSchema = z.union([z.literal(0), z.literal(1), z.literal(2), z.literal(3), z.literal(4)]);
|
|
32
32
|
var EvidenceScopeSchema = z.enum(["repository", "platform", "organization", "outcome"]);
|
|
33
|
+
var ManualEvidenceScopeSchema = z.enum(["platform", "organization", "outcome"]);
|
|
33
34
|
var AssessmentScopeSchema = z.enum(["tracked", "workspace"]);
|
|
34
35
|
var PathAnySchema = z.object({
|
|
35
36
|
type: z.literal("path_any"),
|
|
@@ -61,7 +62,9 @@ var ContentTermsSchema = z.object({
|
|
|
61
62
|
files: z.array(z.string().min(1)).min(1),
|
|
62
63
|
terms: z.array(z.string().min(1)).min(1),
|
|
63
64
|
min_terms: z.number().int().positive(),
|
|
64
|
-
required_any_terms: z.array(z.string().min(1)).min(1).optional()
|
|
65
|
+
required_any_terms: z.array(z.string().min(1)).min(1).optional(),
|
|
66
|
+
max_span_lines: z.number().int().positive().max(200).optional(),
|
|
67
|
+
max_files_per_pattern: z.number().int().positive().max(250).optional()
|
|
65
68
|
});
|
|
66
69
|
var MaxBytesSchema = z.object({
|
|
67
70
|
type: z.literal("max_bytes"),
|
|
@@ -71,7 +74,7 @@ var MaxBytesSchema = z.object({
|
|
|
71
74
|
});
|
|
72
75
|
var ManualSchema = z.object({
|
|
73
76
|
type: z.literal("manual"),
|
|
74
|
-
scope:
|
|
77
|
+
scope: ManualEvidenceScopeSchema.default("organization"),
|
|
75
78
|
prompt: z.string().min(1)
|
|
76
79
|
});
|
|
77
80
|
var EvidenceCheckSchema = z.discriminatedUnion("type", [
|
|
@@ -94,7 +97,8 @@ var RawControlSchema = z.object({
|
|
|
94
97
|
references: z.array(z.string().min(1)).default([]),
|
|
95
98
|
allow_attestation: z.boolean().default(false),
|
|
96
99
|
allow_not_applicable: z.boolean().default(false),
|
|
97
|
-
allow_agent_evidence: z.boolean().default(false)
|
|
100
|
+
allow_agent_evidence: z.boolean().default(false),
|
|
101
|
+
agent_evidence_scopes: z.array(EvidenceScopeSchema).default([])
|
|
98
102
|
});
|
|
99
103
|
var LegacyRawControlSchema = RawControlSchema.extend({
|
|
100
104
|
allow_attestation: z.boolean().default(true)
|
|
@@ -107,6 +111,32 @@ var LegacyControlFileSchema = z.object({
|
|
|
107
111
|
dimension: DimensionIdSchema,
|
|
108
112
|
controls: z.array(LegacyRawControlSchema).min(1)
|
|
109
113
|
});
|
|
114
|
+
var DetectorAdapterExtensionSchema = z.object({
|
|
115
|
+
control_id: z.string().regex(/^ADRB-[A-Z]{3}-\d{3}$/),
|
|
116
|
+
evidence_index: z.number().int().nonnegative(),
|
|
117
|
+
patterns: z.array(z.string().min(1)).min(1).optional(),
|
|
118
|
+
files: z.array(z.string().min(1)).min(1).optional(),
|
|
119
|
+
terms: z.array(z.string().min(1)).min(1).optional(),
|
|
120
|
+
required_any_terms: z.array(z.string().min(1)).min(1).optional()
|
|
121
|
+
}).strict().superRefine((extension, context) => {
|
|
122
|
+
const extensionKinds = [
|
|
123
|
+
extension.patterns,
|
|
124
|
+
extension.files,
|
|
125
|
+
extension.terms,
|
|
126
|
+
extension.required_any_terms
|
|
127
|
+
].filter(Boolean).length;
|
|
128
|
+
if (extensionKinds !== 1) {
|
|
129
|
+
context.addIssue({
|
|
130
|
+
code: z.ZodIssueCode.custom,
|
|
131
|
+
message: "A detector extension must declare exactly one of patterns, files, terms, or required_any_terms"
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
var DetectorAdapterSchema = z.object({
|
|
136
|
+
id: z.string().regex(/^[a-z0-9-]+$/),
|
|
137
|
+
benchmark_version: z.string().regex(/^\d+\.\d+\.\d+$/),
|
|
138
|
+
extensions: z.array(DetectorAdapterExtensionSchema).min(1)
|
|
139
|
+
}).strict();
|
|
110
140
|
var DimensionSchema = z.object({
|
|
111
141
|
id: DimensionIdSchema,
|
|
112
142
|
title: z.string().min(1),
|
|
@@ -186,10 +216,47 @@ var AgentEvidenceFileSchema = z.object({
|
|
|
186
216
|
}).strict(),
|
|
187
217
|
claims: z.record(z.string().regex(/^ADRB-[A-Z]{3}-\d{3}$/), AgentEvidenceClaimSchema)
|
|
188
218
|
}).strict();
|
|
219
|
+
var AgentEvidenceClaimV03Schema = z.object({
|
|
220
|
+
status: z.enum(["met", "not_met", "unknown"]),
|
|
221
|
+
scope: EvidenceScopeSchema,
|
|
222
|
+
summary: z.string().min(1),
|
|
223
|
+
references: z.array(z.string().min(1)).min(1),
|
|
224
|
+
collected_at: z.string().datetime(),
|
|
225
|
+
expires_at: z.string().datetime(),
|
|
226
|
+
error: z.string().min(1).nullable().default(null)
|
|
227
|
+
}).strict().superRefine((claim, context) => {
|
|
228
|
+
if (claim.error && claim.status !== "unknown") {
|
|
229
|
+
context.addIssue({
|
|
230
|
+
code: z.ZodIssueCode.custom,
|
|
231
|
+
message: "A claim with an error must have unknown status",
|
|
232
|
+
path: ["status"]
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
if (claim.scope === "repository" && claim.references.some((reference) => !reference.startsWith("repo:"))) {
|
|
236
|
+
context.addIssue({
|
|
237
|
+
code: z.ZodIssueCode.custom,
|
|
238
|
+
message: "Repository claims must use repo:<tracked-path>[#Lx-Ly] references",
|
|
239
|
+
path: ["references"]
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
var AgentEvidenceFileV03Schema = z.object({
|
|
244
|
+
schema_version: z.literal("0.3.0"),
|
|
245
|
+
benchmark_version: z.literal("0.3.0"),
|
|
246
|
+
target: z.object({
|
|
247
|
+
repository: z.string().min(1),
|
|
248
|
+
git_head: z.string().min(1)
|
|
249
|
+
}).strict(),
|
|
250
|
+
collector: z.object({
|
|
251
|
+
name: z.string().min(1),
|
|
252
|
+
version: z.string().min(1)
|
|
253
|
+
}).strict(),
|
|
254
|
+
claims: z.record(z.string().regex(/^ADRB-[A-Z]{3}-\d{3}$/), AgentEvidenceClaimV03Schema)
|
|
255
|
+
}).strict();
|
|
189
256
|
|
|
190
257
|
// src/load.ts
|
|
191
258
|
var packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
192
|
-
var defaultBenchmarkRoot = join(packageRoot, "benchmark", "v0.
|
|
259
|
+
var defaultBenchmarkRoot = join(packageRoot, "benchmark", "v0.3");
|
|
193
260
|
async function readYaml(path) {
|
|
194
261
|
return parse(await readFile(path, "utf8"));
|
|
195
262
|
}
|
|
@@ -201,12 +268,85 @@ async function loadBenchmark(root = defaultBenchmarkRoot) {
|
|
|
201
268
|
const file = benchmark.version === "0.1.0" ? LegacyControlFileSchema.parse(await readYaml(path)) : ControlFileSchema.parse(await readYaml(path));
|
|
202
269
|
controls.push(...file.controls.map((control) => ({ ...control, dimension: file.dimension })));
|
|
203
270
|
}
|
|
271
|
+
const adapterPaths = await fg("adapters/*.yaml", {
|
|
272
|
+
cwd: root,
|
|
273
|
+
absolute: true,
|
|
274
|
+
onlyFiles: true
|
|
275
|
+
});
|
|
276
|
+
for (const path of adapterPaths.sort()) {
|
|
277
|
+
const adapter = DetectorAdapterSchema.parse(await readYaml(path));
|
|
278
|
+
applyDetectorAdapter(benchmark, controls, adapter);
|
|
279
|
+
}
|
|
204
280
|
validateCatalog(benchmark, controls);
|
|
205
281
|
return { benchmark, controls };
|
|
206
282
|
}
|
|
207
|
-
|
|
283
|
+
function applyDetectorAdapter(benchmark, controls, adapter) {
|
|
284
|
+
if (adapter.benchmark_version !== benchmark.version) {
|
|
285
|
+
throw new Error(
|
|
286
|
+
`Detector adapter ${adapter.id} targets ${adapter.benchmark_version}, not ${benchmark.version}`
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
for (const extension of adapter.extensions) {
|
|
290
|
+
const control = controls.find(({ id }) => id === extension.control_id);
|
|
291
|
+
if (!control) {
|
|
292
|
+
throw new Error(`Detector adapter ${adapter.id} references unknown ${extension.control_id}`);
|
|
293
|
+
}
|
|
294
|
+
const check = control.evidence[extension.evidence_index];
|
|
295
|
+
if (!check) {
|
|
296
|
+
throw new Error(
|
|
297
|
+
`Detector adapter ${adapter.id} references missing evidence index ${extension.evidence_index} on ${control.id}`
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
if (extension.patterns) {
|
|
301
|
+
if (!("patterns" in check)) {
|
|
302
|
+
throw new Error(
|
|
303
|
+
`Detector adapter ${adapter.id} cannot add patterns to ${control.id} evidence ${extension.evidence_index}`
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
check.patterns = [.../* @__PURE__ */ new Set([...check.patterns, ...extension.patterns])];
|
|
307
|
+
}
|
|
308
|
+
if (extension.files) {
|
|
309
|
+
if (!("files" in check)) {
|
|
310
|
+
throw new Error(
|
|
311
|
+
`Detector adapter ${adapter.id} cannot add files to ${control.id} evidence ${extension.evidence_index}`
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
check.files = [.../* @__PURE__ */ new Set([...check.files, ...extension.files])];
|
|
315
|
+
}
|
|
316
|
+
if (extension.terms) {
|
|
317
|
+
if (check.type !== "content_terms") {
|
|
318
|
+
throw new Error(
|
|
319
|
+
`Detector adapter ${adapter.id} cannot add terms to ${control.id} evidence ${extension.evidence_index}`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
check.terms = [.../* @__PURE__ */ new Set([...check.terms, ...extension.terms])];
|
|
323
|
+
}
|
|
324
|
+
if (extension.required_any_terms) {
|
|
325
|
+
if (check.type !== "content_terms") {
|
|
326
|
+
throw new Error(
|
|
327
|
+
`Detector adapter ${adapter.id} cannot add required terms to ${control.id} evidence ${extension.evidence_index}`
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
check.required_any_terms = [
|
|
331
|
+
.../* @__PURE__ */ new Set([...check.required_any_terms ?? [], ...extension.required_any_terms])
|
|
332
|
+
];
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
async function loadAttestations(path, benchmarkVersion, options = {}) {
|
|
208
337
|
try {
|
|
209
338
|
const rawFile = await readYaml(path);
|
|
339
|
+
const artifactVersion = versionField(rawFile, "benchmark_version");
|
|
340
|
+
if (artifactVersion && handleVersionMismatch(
|
|
341
|
+
"Attestation",
|
|
342
|
+
path,
|
|
343
|
+
artifactVersion,
|
|
344
|
+
benchmarkVersion,
|
|
345
|
+
"init --force",
|
|
346
|
+
options
|
|
347
|
+
)) {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
210
350
|
const file = benchmarkVersion === "0.1.0" ? LegacyAttestationFileSchema.parse(rawFile) : AttestationFileSchema.parse(rawFile);
|
|
211
351
|
if (file.benchmark_version !== benchmarkVersion) {
|
|
212
352
|
throw new Error(
|
|
@@ -219,26 +359,80 @@ async function loadAttestations(path, benchmarkVersion) {
|
|
|
219
359
|
throw error;
|
|
220
360
|
}
|
|
221
361
|
}
|
|
222
|
-
async function loadAgentEvidence(path) {
|
|
362
|
+
async function loadAgentEvidence(path, benchmarkVersion, options = {}) {
|
|
223
363
|
try {
|
|
224
|
-
|
|
364
|
+
const rawFile = await readYaml(path);
|
|
365
|
+
const artifactVersion = versionField(rawFile, "benchmark_version");
|
|
366
|
+
const schemaVersion = versionField(rawFile, "schema_version");
|
|
367
|
+
const mismatchedVersion = [artifactVersion, schemaVersion].find(
|
|
368
|
+
(version) => version && version !== benchmarkVersion
|
|
369
|
+
);
|
|
370
|
+
if (mismatchedVersion && handleVersionMismatch(
|
|
371
|
+
"Agent evidence",
|
|
372
|
+
path,
|
|
373
|
+
mismatchedVersion,
|
|
374
|
+
benchmarkVersion,
|
|
375
|
+
"init-evidence --force",
|
|
376
|
+
options
|
|
377
|
+
)) {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
const file = (() => {
|
|
381
|
+
if (benchmarkVersion === "0.2.0") return AgentEvidenceFileSchema.parse(rawFile);
|
|
382
|
+
if (benchmarkVersion === "0.3.0") return AgentEvidenceFileV03Schema.parse(rawFile);
|
|
383
|
+
throw new Error(`Agent evidence bundles are unsupported for benchmark ${benchmarkVersion}`);
|
|
384
|
+
})();
|
|
385
|
+
if (file.benchmark_version !== benchmarkVersion) {
|
|
386
|
+
throw new Error(
|
|
387
|
+
`Agent evidence benchmark version ${file.benchmark_version} does not match ${benchmarkVersion}`
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
return file;
|
|
225
391
|
} catch (error) {
|
|
226
392
|
if (error.code === "ENOENT") return null;
|
|
227
393
|
throw error;
|
|
228
394
|
}
|
|
229
395
|
}
|
|
396
|
+
function versionField(value, field) {
|
|
397
|
+
if (!value || typeof value !== "object" || !(field in value)) return null;
|
|
398
|
+
const version = value[field];
|
|
399
|
+
return typeof version === "string" ? version : null;
|
|
400
|
+
}
|
|
401
|
+
function handleVersionMismatch(label, path, artifactVersion, benchmarkVersion, regenerateCommand, options) {
|
|
402
|
+
if (artifactVersion === benchmarkVersion) return false;
|
|
403
|
+
const mismatch = `${label} file ${path} targets ADRB v${artifactVersion}, not v${benchmarkVersion}`;
|
|
404
|
+
if (!options.ignoreVersionMismatch) {
|
|
405
|
+
throw new Error(
|
|
406
|
+
`${mismatch}. Regenerate it with \`agentic-scorecard ${regenerateCommand}\` or pass a v${benchmarkVersion} file.`
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
options.onWarning?.(
|
|
410
|
+
`Ignored auto-loaded ${label.toLowerCase()} file ${path} because it targets ADRB v${artifactVersion}, not v${benchmarkVersion}. Regenerate it with \`agentic-scorecard ${regenerateCommand}\` before relying on its claims.`
|
|
411
|
+
);
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
230
414
|
function validateCatalog(benchmark, controls) {
|
|
231
415
|
const ids = /* @__PURE__ */ new Set();
|
|
232
416
|
for (const control of controls) {
|
|
233
417
|
if (ids.has(control.id)) throw new Error(`Duplicate control id: ${control.id}`);
|
|
234
418
|
ids.add(control.id);
|
|
235
|
-
if (
|
|
236
|
-
throw new Error(
|
|
419
|
+
if (["0.2.0", "0.3.0"].includes(benchmark.version) && control.evidence.some(({ type }) => type === "content_any" || type === "content_all")) {
|
|
420
|
+
throw new Error(
|
|
421
|
+
`${control.id} uses a legacy broad content collector in benchmark ${benchmark.version}`
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
const manualScopes = control.evidence.filter(({ type }) => type === "manual").map(({ scope }) => scope);
|
|
425
|
+
const allowedAgentScopes = control.agent_evidence_scopes.length > 0 ? control.agent_evidence_scopes : manualScopes;
|
|
426
|
+
if (control.allow_agent_evidence && allowedAgentScopes.length === 0) {
|
|
427
|
+
throw new Error(`${control.id} allows agent evidence without an eligible evidence scope`);
|
|
237
428
|
}
|
|
238
|
-
if (control.allow_agent_evidence &&
|
|
239
|
-
throw new Error(`${control.id}
|
|
429
|
+
if (!control.allow_agent_evidence && control.agent_evidence_scopes.length > 0) {
|
|
430
|
+
throw new Error(`${control.id} declares agent evidence scopes but does not allow it`);
|
|
240
431
|
}
|
|
241
|
-
if (benchmark.version === "0.2.0" && control.
|
|
432
|
+
if (benchmark.version === "0.2.0" && control.agent_evidence_scopes.some((scope) => scope === "repository")) {
|
|
433
|
+
throw new Error(`${control.id} changes immutable v0.2 repository evidence semantics`);
|
|
434
|
+
}
|
|
435
|
+
if (["0.2.0", "0.3.0"].includes(benchmark.version) && control.allow_attestation && !control.evidence.some(({ type }) => type === "manual")) {
|
|
242
436
|
throw new Error(`${control.id} allows attestation for repository-detected evidence`);
|
|
243
437
|
}
|
|
244
438
|
for (const check of control.evidence) {
|
|
@@ -326,6 +520,12 @@ function appendControlDetails(lines, heading, controls) {
|
|
|
326
520
|
}
|
|
327
521
|
function toMarkdown(report) {
|
|
328
522
|
const target = report.profiles.find(({ id }) => id === report.target.profile);
|
|
523
|
+
const targetDependencies = target?.evidence_dependencies;
|
|
524
|
+
const dependencyCount = (targetDependencies?.agent_collected ?? 0) + (targetDependencies?.attested ?? 0);
|
|
525
|
+
const targetProvenance = target?.passed && dependencyCount > 0 ? ` (depends on ${[
|
|
526
|
+
targetDependencies?.agent_collected ? `${targetDependencies.agent_collected} agent-collected` : null,
|
|
527
|
+
targetDependencies?.attested ? `${targetDependencies.attested} human-attested` : null
|
|
528
|
+
].filter(Boolean).join(" and ")} required ${dependencyCount === 1 ? "control" : "controls"})` : "";
|
|
329
529
|
const established = report.controls.filter(
|
|
330
530
|
({ status }) => status === "met" || status === "not_applicable"
|
|
331
531
|
);
|
|
@@ -347,10 +547,20 @@ function toMarkdown(report) {
|
|
|
347
547
|
`- Working tree dirty: ${report.target.working_tree_dirty === null ? "unknown" : String(report.target.working_tree_dirty)}`,
|
|
348
548
|
`- Assessed: ${report.assessed_at}`,
|
|
349
549
|
`- Score: **${report.score.total}/${report.score.maximum} (${report.score.percentage}%)**`,
|
|
550
|
+
...report.score.repository ? [
|
|
551
|
+
`- Repository-detected progress: **${report.score.repository.achieved}/${report.score.repository.ceiling} (${report.score.repository.percentage}%)** of the maturity levels the offline repository collector can establish`
|
|
552
|
+
] : [],
|
|
350
553
|
`- Highest readiness profile: **${report.readiness.highest_profile ?? "none"}**`,
|
|
351
|
-
`- Target \`${report.target.profile}\`: **${report.readiness.target_passed ?
|
|
352
|
-
`- Evidence: ${report.evidence_summary.repository_detected} repository-detected, ${report.evidence_summary.agent_collected} agent-collected, ${report.evidence_summary.attested} human-attested, ${report.evidence_summary.unmet} unmet, ${report.evidence_summary.unknown} unknown`,
|
|
554
|
+
`- Target \`${report.target.profile}\`: **${report.readiness.target_passed ? `PASS${targetProvenance}` : "FAIL"}**`,
|
|
555
|
+
`- Evidence: ${report.evidence_summary.repository_detected} repository-detected, ${report.evidence_summary.agent_collected} agent-collected, ${report.evidence_summary.attested} human-attested, ${report.evidence_summary.unmet} unmet, ${report.evidence_summary.unknown} unknown${report.evidence_summary.resolved !== void 0 && report.evidence_summary.total !== void 0 ? `; ${report.evidence_summary.resolved}/${report.evidence_summary.total} controls resolved` : ""}`,
|
|
556
|
+
...report.warnings && report.warnings.length > 0 ? [`- Warnings: **${report.warnings.length} \u2014 review before using this assessment**`] : [],
|
|
353
557
|
"",
|
|
558
|
+
...report.warnings && report.warnings.length > 0 ? [
|
|
559
|
+
"## Warnings",
|
|
560
|
+
"",
|
|
561
|
+
...report.warnings.map((warning) => `- WARNING: ${safeText(warning)}`),
|
|
562
|
+
""
|
|
563
|
+
] : [],
|
|
354
564
|
"## Dimensions",
|
|
355
565
|
"",
|
|
356
566
|
"| Dimension | Score | Controls met |",
|
|
@@ -453,10 +663,11 @@ async function git(repo, args) {
|
|
|
453
663
|
async function createRepositoryContext(repository, scope, excludedPaths = []) {
|
|
454
664
|
const requestedRoot = resolve2(repository);
|
|
455
665
|
const root = await realpath(requestedRoot);
|
|
456
|
-
const [headOutput, remoteOutput, statusOutput] = await Promise.all([
|
|
666
|
+
const [headOutput, remoteOutput, statusOutput, trackedStatusOutput] = await Promise.all([
|
|
457
667
|
git(root, ["rev-parse", "HEAD"]),
|
|
458
668
|
git(root, ["config", "--get", "remote.origin.url"]),
|
|
459
|
-
git(root, ["status", "--porcelain"])
|
|
669
|
+
git(root, ["status", "--porcelain"]),
|
|
670
|
+
git(root, ["status", "--porcelain", "--untracked-files=no"])
|
|
460
671
|
]);
|
|
461
672
|
let includedPaths = null;
|
|
462
673
|
if (scope === "tracked") {
|
|
@@ -474,7 +685,8 @@ async function createRepositoryContext(repository, scope, excludedPaths = []) {
|
|
|
474
685
|
scope,
|
|
475
686
|
git_head: headOutput?.trim() || null,
|
|
476
687
|
git_remote: sanitizeRemote(remoteOutput?.trim() || null),
|
|
477
|
-
working_tree_dirty: statusOutput === null ? null : statusOutput.length > 0
|
|
688
|
+
working_tree_dirty: statusOutput === null ? null : statusOutput.length > 0,
|
|
689
|
+
tracked_tree_dirty: trackedStatusOutput === null ? null : trackedStatusOutput.length > 0
|
|
478
690
|
},
|
|
479
691
|
includedPaths,
|
|
480
692
|
excludedPaths: new Set(
|
|
@@ -492,6 +704,10 @@ function repositoryEvidenceTarget(metadata) {
|
|
|
492
704
|
};
|
|
493
705
|
}
|
|
494
706
|
|
|
707
|
+
// src/score.ts
|
|
708
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
709
|
+
import { join as join2 } from "path";
|
|
710
|
+
|
|
495
711
|
// src/evidence.ts
|
|
496
712
|
import { lstat, readFile as readFile2, realpath as realpath2, stat } from "fs/promises";
|
|
497
713
|
import { resolve as resolve3, sep as sep2 } from "path";
|
|
@@ -553,9 +769,9 @@ async function evaluatePathAll(context, check) {
|
|
|
553
769
|
found
|
|
554
770
|
);
|
|
555
771
|
}
|
|
556
|
-
async function readSearchableFiles(context, patterns) {
|
|
772
|
+
async function readSearchableFiles(context, patterns, maxFilesPerPattern) {
|
|
557
773
|
const root = context.metadata.root;
|
|
558
|
-
const paths = (await matches(context, patterns)).slice(0, maxContentFiles);
|
|
774
|
+
const paths = maxFilesPerPattern ? await prioritizedMatches(context, patterns, maxFilesPerPattern) : (await matches(context, patterns)).slice(0, maxContentFiles);
|
|
559
775
|
const files = [];
|
|
560
776
|
let totalBytes = 0;
|
|
561
777
|
for (const path of paths) {
|
|
@@ -577,6 +793,26 @@ async function readSearchableFiles(context, patterns) {
|
|
|
577
793
|
}
|
|
578
794
|
return files;
|
|
579
795
|
}
|
|
796
|
+
async function prioritizedMatches(context, patterns, maxFilesPerPattern) {
|
|
797
|
+
const selected = [];
|
|
798
|
+
const seen = /* @__PURE__ */ new Set();
|
|
799
|
+
const groups = await Promise.all(
|
|
800
|
+
patterns.map(
|
|
801
|
+
async (pattern) => (await matches(context, [pattern])).slice(0, maxFilesPerPattern)
|
|
802
|
+
)
|
|
803
|
+
);
|
|
804
|
+
for (let candidateIndex = 0; candidateIndex < maxFilesPerPattern; candidateIndex += 1) {
|
|
805
|
+
for (const group of groups) {
|
|
806
|
+
const path = group[candidateIndex];
|
|
807
|
+
if (!path) continue;
|
|
808
|
+
if (seen.has(path)) continue;
|
|
809
|
+
seen.add(path);
|
|
810
|
+
selected.push(path);
|
|
811
|
+
if (selected.length === maxContentFiles) return selected;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
return selected;
|
|
815
|
+
}
|
|
580
816
|
function isGeneratedAssessment(text) {
|
|
581
817
|
const normalized = text.trimStart();
|
|
582
818
|
if (normalized.startsWith("# agentic development readiness assessment")) return true;
|
|
@@ -609,29 +845,72 @@ async function evaluateLegacyContent(context, check) {
|
|
|
609
845
|
);
|
|
610
846
|
}
|
|
611
847
|
async function evaluateContentTerms(context, check) {
|
|
612
|
-
const files = await readSearchableFiles(context, check.files);
|
|
848
|
+
const files = await readSearchableFiles(context, check.files, check.max_files_per_pattern);
|
|
613
849
|
const matchesByFile = files.map(({ path, text }) => ({
|
|
614
850
|
path,
|
|
615
|
-
|
|
616
|
-
|
|
851
|
+
...strongestContentMatch(
|
|
852
|
+
text,
|
|
853
|
+
check.terms,
|
|
854
|
+
check.required_any_terms ?? [],
|
|
855
|
+
check.min_terms,
|
|
856
|
+
check.max_span_lines
|
|
857
|
+
)
|
|
617
858
|
}));
|
|
618
|
-
const qualifying = matchesByFile.filter(
|
|
619
|
-
({ matched, requiredMatched }) => matched >= check.min_terms && (check.required_any_terms === void 0 || requiredMatched > 0)
|
|
620
|
-
);
|
|
859
|
+
const qualifying = matchesByFile.filter(({ qualifies }) => qualifies);
|
|
621
860
|
const strongest = matchesByFile.reduce((maximum, file) => Math.max(maximum, file.matched), 0);
|
|
622
861
|
const strongestRequired = matchesByFile.reduce(
|
|
623
862
|
(maximum, file) => Math.max(maximum, file.requiredMatched),
|
|
624
863
|
0
|
|
625
864
|
);
|
|
626
865
|
const requiredSummary = check.required_any_terms ? `; strongest required match ${strongestRequired}/${check.required_any_terms.length}` : "";
|
|
866
|
+
const proximitySummary = check.max_span_lines ? ` within ${check.max_span_lines}-line window(s)` : "";
|
|
627
867
|
return result(
|
|
628
868
|
check.type,
|
|
629
869
|
check.scope,
|
|
630
870
|
qualifying.length > 0 ? "met" : "not_met",
|
|
631
|
-
`${qualifying.length} qualifying file(s); strongest co-located match ${strongest}/${check.terms.length} term(s)${requiredSummary} across ${files.length} candidate file(s); threshold ${check.min_terms}`,
|
|
871
|
+
`${qualifying.length} qualifying file(s); strongest co-located match ${strongest}/${check.terms.length} term(s)${requiredSummary}${proximitySummary} across ${files.length} candidate file(s); threshold ${check.min_terms}`,
|
|
632
872
|
qualifying.map(({ path }) => path)
|
|
633
873
|
);
|
|
634
874
|
}
|
|
875
|
+
function strongestContentMatch(text, terms, requiredTerms, minTerms, maxSpanLines) {
|
|
876
|
+
if (!maxSpanLines) {
|
|
877
|
+
const matched = terms.filter((term) => containsTerm(text, term)).length;
|
|
878
|
+
const requiredMatched = requiredTerms.filter((term) => containsTerm(text, term)).length;
|
|
879
|
+
return {
|
|
880
|
+
matched,
|
|
881
|
+
requiredMatched,
|
|
882
|
+
qualifies: matched >= minTerms && (requiredTerms.length === 0 || requiredMatched > 0)
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
const termCounts = terms.map(() => 0);
|
|
886
|
+
const requiredCounts = requiredTerms.map(() => 0);
|
|
887
|
+
const lines = text.split(/\r?\n/);
|
|
888
|
+
let strongest = 0;
|
|
889
|
+
let strongestRequired = 0;
|
|
890
|
+
let qualifies = false;
|
|
891
|
+
const update = (line, direction) => {
|
|
892
|
+
terms.forEach((term, index) => {
|
|
893
|
+
if (containsTerm(line, term)) termCounts[index] = (termCounts[index] ?? 0) + direction;
|
|
894
|
+
});
|
|
895
|
+
requiredTerms.forEach((term, index) => {
|
|
896
|
+
if (containsTerm(line, term)) {
|
|
897
|
+
requiredCounts[index] = (requiredCounts[index] ?? 0) + direction;
|
|
898
|
+
}
|
|
899
|
+
});
|
|
900
|
+
};
|
|
901
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
902
|
+
update(lines[index] ?? "", 1);
|
|
903
|
+
if (index >= maxSpanLines) update(lines[index - maxSpanLines] ?? "", -1);
|
|
904
|
+
const matched = termCounts.filter((count) => count > 0).length;
|
|
905
|
+
const requiredMatched = requiredCounts.filter((count) => count > 0).length;
|
|
906
|
+
strongest = Math.max(strongest, matched);
|
|
907
|
+
strongestRequired = Math.max(strongestRequired, requiredMatched);
|
|
908
|
+
if (matched >= minTerms && (requiredTerms.length === 0 || requiredMatched > 0)) {
|
|
909
|
+
qualifies = true;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return { matched: strongest, requiredMatched: strongestRequired, qualifies };
|
|
913
|
+
}
|
|
635
914
|
function containsTerm(text, term) {
|
|
636
915
|
const pattern = term.trim().split(/\s+/).map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\s_-]+");
|
|
637
916
|
return new RegExp(`(^|[^a-z0-9])${pattern}(?=$|[^a-z0-9])`, "i").test(text);
|
|
@@ -753,6 +1032,46 @@ function dimensionScore(controls, dimension) {
|
|
|
753
1032
|
}
|
|
754
1033
|
return score;
|
|
755
1034
|
}
|
|
1035
|
+
function repositoryScore(catalog, results, dimensions) {
|
|
1036
|
+
let achieved = 0;
|
|
1037
|
+
let ceiling = 0;
|
|
1038
|
+
const resultsById = new Map(results.map((result2) => [result2.id, result2]));
|
|
1039
|
+
for (const { id: dimension } of dimensions) {
|
|
1040
|
+
let dimensionAchieved = 0;
|
|
1041
|
+
let dimensionCeiling = 0;
|
|
1042
|
+
let achievedOpen = true;
|
|
1043
|
+
let ceilingOpen = true;
|
|
1044
|
+
for (const level of [1, 2, 3, 4]) {
|
|
1045
|
+
const controlsAtLevel = catalog.filter(
|
|
1046
|
+
(control) => control.dimension === dimension && control.level === level
|
|
1047
|
+
);
|
|
1048
|
+
const repositoryDetectable = controlsAtLevel.every(
|
|
1049
|
+
(control) => control.evidence.every((evidence) => evidence.scope === "repository")
|
|
1050
|
+
);
|
|
1051
|
+
if (ceilingOpen && repositoryDetectable) {
|
|
1052
|
+
dimensionCeiling = level;
|
|
1053
|
+
} else {
|
|
1054
|
+
ceilingOpen = false;
|
|
1055
|
+
}
|
|
1056
|
+
const repositoryEstablished = controlsAtLevel.every((control) => {
|
|
1057
|
+
const result2 = resultsById.get(control.id);
|
|
1058
|
+
return result2?.status === "met" && result2.confidence === "repository-detected";
|
|
1059
|
+
});
|
|
1060
|
+
if (achievedOpen && repositoryDetectable && repositoryEstablished) {
|
|
1061
|
+
dimensionAchieved = level;
|
|
1062
|
+
} else {
|
|
1063
|
+
achievedOpen = false;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
achieved += dimensionAchieved;
|
|
1067
|
+
ceiling += dimensionCeiling;
|
|
1068
|
+
}
|
|
1069
|
+
return {
|
|
1070
|
+
achieved,
|
|
1071
|
+
ceiling,
|
|
1072
|
+
percentage: ceiling === 0 ? 0 : Math.round(achieved / ceiling * 100)
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
756
1075
|
function assessProfiles(benchmark, dimensions, controls) {
|
|
757
1076
|
return benchmark.readiness_profiles.map((profile) => {
|
|
758
1077
|
const blockers = benchmark.dimensions.flatMap(({ id }) => {
|
|
@@ -770,7 +1089,23 @@ function assessProfiles(benchmark, dimensions, controls) {
|
|
|
770
1089
|
}
|
|
771
1090
|
];
|
|
772
1091
|
});
|
|
773
|
-
|
|
1092
|
+
const requiredControls = controls.filter(
|
|
1093
|
+
(control) => control.level <= profile.floors[control.dimension] && controlPasses(control)
|
|
1094
|
+
);
|
|
1095
|
+
return {
|
|
1096
|
+
id: profile.id,
|
|
1097
|
+
title: profile.title,
|
|
1098
|
+
passed: blockers.length === 0,
|
|
1099
|
+
blockers,
|
|
1100
|
+
...benchmark.version === "0.3.0" ? {
|
|
1101
|
+
evidence_dependencies: {
|
|
1102
|
+
agent_collected: requiredControls.filter(
|
|
1103
|
+
({ confidence }) => confidence === "agent-collected"
|
|
1104
|
+
).length,
|
|
1105
|
+
attested: requiredControls.filter(({ confidence }) => confidence === "attested").length
|
|
1106
|
+
}
|
|
1107
|
+
} : {}
|
|
1108
|
+
};
|
|
774
1109
|
});
|
|
775
1110
|
}
|
|
776
1111
|
async function assess(repo, benchmark, catalog, profileId, options = {}) {
|
|
@@ -783,7 +1118,7 @@ async function assess(repo, benchmark, catalog, profileId, options = {}) {
|
|
|
783
1118
|
const scope = options.scope ?? "tracked";
|
|
784
1119
|
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
785
1120
|
const context = await createRepositoryContext(repo, scope, options.excludedPaths);
|
|
786
|
-
validateAgentEvidence(benchmark, catalog, context, options.agentEvidence ?? null, now);
|
|
1121
|
+
await validateAgentEvidence(benchmark, catalog, context, options.agentEvidence ?? null, now);
|
|
787
1122
|
const controls = await Promise.all(
|
|
788
1123
|
catalog.map(
|
|
789
1124
|
async (control) => evaluateControl(
|
|
@@ -807,10 +1142,11 @@ async function assess(repo, benchmark, catalog, profileId, options = {}) {
|
|
|
807
1142
|
});
|
|
808
1143
|
const profiles = assessProfiles(benchmark, dimensions, controls);
|
|
809
1144
|
const total = dimensions.reduce((sum, { score }) => sum + score, 0);
|
|
1145
|
+
const repository = benchmark.version === "0.3.0" ? repositoryScore(catalog, controls, benchmark.dimensions) : void 0;
|
|
810
1146
|
const highestProfile = [...profiles].reverse().find(({ passed }) => passed)?.id ?? null;
|
|
811
1147
|
const targetPassed = profiles.find(({ id }) => id === profileId)?.passed ?? false;
|
|
812
1148
|
return {
|
|
813
|
-
schema_version: "0.2.0",
|
|
1149
|
+
schema_version: benchmark.version === "0.3.0" ? "0.3.0" : "0.2.0",
|
|
814
1150
|
benchmark: { id: benchmark.id, version: benchmark.version },
|
|
815
1151
|
target: {
|
|
816
1152
|
repository: repo,
|
|
@@ -821,7 +1157,13 @@ async function assess(repo, benchmark, catalog, profileId, options = {}) {
|
|
|
821
1157
|
working_tree_dirty: context.metadata.working_tree_dirty
|
|
822
1158
|
},
|
|
823
1159
|
assessed_at: now.toISOString(),
|
|
824
|
-
|
|
1160
|
+
...benchmark.version === "0.3.0" ? { warnings: options.warnings ?? [] } : {},
|
|
1161
|
+
score: {
|
|
1162
|
+
total,
|
|
1163
|
+
maximum: 40,
|
|
1164
|
+
percentage: Math.round(total / 40 * 100),
|
|
1165
|
+
...repository ? { repository } : {}
|
|
1166
|
+
},
|
|
825
1167
|
evidence_summary: {
|
|
826
1168
|
repository_detected: controls.filter(
|
|
827
1169
|
({ confidence, status }) => confidence === "repository-detected" && status === "met"
|
|
@@ -833,7 +1175,11 @@ async function assess(repo, benchmark, catalog, profileId, options = {}) {
|
|
|
833
1175
|
({ confidence, status }) => confidence === "attested" && status === "met"
|
|
834
1176
|
).length,
|
|
835
1177
|
unmet: controls.filter(({ status }) => status === "not_met").length,
|
|
836
|
-
unknown: controls.filter(({ status }) => status === "unknown").length
|
|
1178
|
+
unknown: controls.filter(({ status }) => status === "unknown").length,
|
|
1179
|
+
...benchmark.version === "0.3.0" ? {
|
|
1180
|
+
resolved: controls.filter(({ status }) => status !== "unknown").length,
|
|
1181
|
+
total: controls.length
|
|
1182
|
+
} : {}
|
|
837
1183
|
},
|
|
838
1184
|
dimensions,
|
|
839
1185
|
controls,
|
|
@@ -842,12 +1188,16 @@ async function assess(repo, benchmark, catalog, profileId, options = {}) {
|
|
|
842
1188
|
limitations: [
|
|
843
1189
|
scope === "tracked" ? "Tracked mode considers only Git-tracked paths, using current working-tree contents; uncommitted edits to tracked files can affect the result." : "Workspace mode includes untracked local files and is provisional; do not compare it directly with tracked-mode reports.",
|
|
844
1190
|
"Repository-detected evidence proves a qualifying artifact match, not consistent practice or external enforcement.",
|
|
1191
|
+
...benchmark.version === "0.3.0" ? [
|
|
1192
|
+
"Repository-detected progress uses only deterministic offline evidence and its attainable ceiling; it is explanatory and does not replace the normative score or readiness floors.",
|
|
1193
|
+
"Agent-collected repository evidence is semantic, target-bound, and source-backed but is not independently verified or relabelled as repository-detected."
|
|
1194
|
+
] : [],
|
|
845
1195
|
"Agent-collected evidence and human attestations are reported separately and are not independently verified.",
|
|
846
1196
|
"This assessment does not grant production access, deployment authority, or certification."
|
|
847
1197
|
]
|
|
848
1198
|
};
|
|
849
1199
|
}
|
|
850
|
-
function validateAgentEvidence(benchmark, catalog, context, evidence, now) {
|
|
1200
|
+
async function validateAgentEvidence(benchmark, catalog, context, evidence, now) {
|
|
851
1201
|
if (!evidence) return;
|
|
852
1202
|
if (evidence.benchmark_version !== benchmark.version) {
|
|
853
1203
|
throw new Error(
|
|
@@ -865,6 +1215,9 @@ function validateAgentEvidence(benchmark, catalog, context, evidence, now) {
|
|
|
865
1215
|
`Agent evidence commit ${evidence.target.git_head ?? "unavailable"} does not match ${expectedTarget.git_head ?? "an unavailable Git commit"}`
|
|
866
1216
|
);
|
|
867
1217
|
}
|
|
1218
|
+
if (benchmark.version === "0.3.0" && context.metadata.tracked_tree_dirty) {
|
|
1219
|
+
throw new Error("ADRB v0.3 agent evidence requires tracked files to match the bound commit");
|
|
1220
|
+
}
|
|
868
1221
|
const controls = new Map(catalog.map((control) => [control.id, control]));
|
|
869
1222
|
if (Object.keys(evidence.claims).length > 0 && (evidence.collector.name.startsWith("TODO") || evidence.collector.version.startsWith("TODO"))) {
|
|
870
1223
|
throw new Error("Agent evidence with claims must identify the collector name and version");
|
|
@@ -875,10 +1228,39 @@ function validateAgentEvidence(benchmark, catalog, context, evidence, now) {
|
|
|
875
1228
|
if (!control.allow_agent_evidence) {
|
|
876
1229
|
throw new Error(`${controlId} does not permit agent-collected evidence`);
|
|
877
1230
|
}
|
|
878
|
-
const
|
|
1231
|
+
const manualScopes = control.evidence.filter(({ type }) => type === "manual").map(({ scope: evidenceScope }) => evidenceScope);
|
|
1232
|
+
const allowedScopes = control.agent_evidence_scopes.length > 0 ? control.agent_evidence_scopes : manualScopes;
|
|
879
1233
|
if (!allowedScopes.includes(claim.scope)) {
|
|
880
1234
|
throw new Error(`${controlId} does not accept ${claim.scope} evidence`);
|
|
881
1235
|
}
|
|
1236
|
+
if (claim.scope === "repository") {
|
|
1237
|
+
if (context.includedPaths === null) {
|
|
1238
|
+
throw new Error(
|
|
1239
|
+
`${controlId} uses repository-scoped agent evidence, which requires --scope tracked`
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
for (const reference of claim.references) {
|
|
1243
|
+
const parsedReference = repositoryReference(reference);
|
|
1244
|
+
if (!parsedReference || !context.includedPaths.has(parsedReference.path) || context.excludedPaths.has(parsedReference.path)) {
|
|
1245
|
+
throw new Error(`${controlId} references an unavailable tracked path: ${reference}`);
|
|
1246
|
+
}
|
|
1247
|
+
if (parsedReference.lines) {
|
|
1248
|
+
const contents = await readFile3(
|
|
1249
|
+
join2(context.metadata.root, parsedReference.path),
|
|
1250
|
+
"utf8"
|
|
1251
|
+
);
|
|
1252
|
+
const lineCount = countLines(contents);
|
|
1253
|
+
if (parsedReference.lines.start < 1 || parsedReference.lines.end < parsedReference.lines.start) {
|
|
1254
|
+
throw new Error(`${controlId} references an invalid line range: ${reference}`);
|
|
1255
|
+
}
|
|
1256
|
+
if (parsedReference.lines.end > lineCount) {
|
|
1257
|
+
throw new Error(
|
|
1258
|
+
`${controlId} references lines beyond ${parsedReference.path}'s ${lineCount} lines: ${reference}`
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
882
1264
|
if (new Date(claim.collected_at) > new Date(claim.expires_at)) {
|
|
883
1265
|
throw new Error(`${controlId} expires before it was collected`);
|
|
884
1266
|
}
|
|
@@ -890,10 +1272,30 @@ function validateAgentEvidence(benchmark, catalog, context, evidence, now) {
|
|
|
890
1272
|
}
|
|
891
1273
|
}
|
|
892
1274
|
}
|
|
1275
|
+
function repositoryReference(reference) {
|
|
1276
|
+
if (!reference.startsWith("repo:")) return null;
|
|
1277
|
+
const withoutPrefix = reference.slice("repo:".length);
|
|
1278
|
+
const lineMatch = withoutPrefix.match(/#L(\d+)(?:-L?(\d+))?$/);
|
|
1279
|
+
const path = lineMatch ? withoutPrefix.slice(0, lineMatch.index) : withoutPrefix;
|
|
1280
|
+
if (path.length === 0 || path.startsWith("/") || path.includes("#") || path.includes("\\") || path.split("/").some((part) => part === ".." || part === ".")) {
|
|
1281
|
+
return null;
|
|
1282
|
+
}
|
|
1283
|
+
const start = lineMatch ? Number(lineMatch[1]) : null;
|
|
1284
|
+
const end = lineMatch ? Number(lineMatch[2] ?? lineMatch[1]) : null;
|
|
1285
|
+
return {
|
|
1286
|
+
path,
|
|
1287
|
+
lines: start === null || end === null ? null : { start, end }
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
function countLines(contents) {
|
|
1291
|
+
if (contents.length === 0) return 0;
|
|
1292
|
+
const lines = contents.split("\n").length;
|
|
1293
|
+
return contents.endsWith("\n") ? lines - 1 : lines;
|
|
1294
|
+
}
|
|
893
1295
|
|
|
894
1296
|
// src/cli.ts
|
|
895
1297
|
var program = new Command();
|
|
896
|
-
program.name("agentic-scorecard").description("Evidence-backed readiness assessment for agentic software development harnesses").version("0.
|
|
1298
|
+
program.name("agentic-scorecard").description("Evidence-backed readiness assessment for agentic software development harnesses").version("0.3.0");
|
|
897
1299
|
program.command("validate").description("Validate the bundled benchmark catalog").action(async () => {
|
|
898
1300
|
const { benchmark, controls } = await loadBenchmark();
|
|
899
1301
|
process.stdout.write(
|
|
@@ -920,10 +1322,10 @@ ${control.evidence.map((check) => `- ${stringify(check).trim().replaceAll("\n",
|
|
|
920
1322
|
});
|
|
921
1323
|
program.command("init").argument("[repository]", "repository to initialize", ".").option("--force", "replace an existing attestation file", false).description("Create a manual-attestation template").action(async (repository, options) => {
|
|
922
1324
|
const repo = resolve4(repository);
|
|
923
|
-
const path =
|
|
1325
|
+
const path = join3(repo, ".agentic", "attestations.yaml");
|
|
924
1326
|
if (!options.force) {
|
|
925
1327
|
try {
|
|
926
|
-
await
|
|
1328
|
+
await readFile4(path, "utf8");
|
|
927
1329
|
throw new Error(`${path} already exists; use --force to replace it`);
|
|
928
1330
|
} catch (error) {
|
|
929
1331
|
if (error.code !== "ENOENT") throw error;
|
|
@@ -960,12 +1362,12 @@ ${stringify({ benchmark_version: benchmark.version, attestations })}`,
|
|
|
960
1362
|
process.stdout.write(`Created ${path}
|
|
961
1363
|
`);
|
|
962
1364
|
});
|
|
963
|
-
program.command("init-evidence").argument("[repository]", "repository to prepare external evidence for", ".").option("--output <path>", "agent evidence bundle path").option("--request-output <path>", "human-readable evidence request path").option("--force", "replace an existing agent evidence bundle", false).description("Create a target-bound template for agent-collected
|
|
1365
|
+
program.command("init-evidence").argument("[repository]", "repository to prepare external evidence for", ".").option("--output <path>", "agent evidence bundle path").option("--request-output <path>", "human-readable evidence request path").option("--force", "replace an existing agent evidence bundle", false).description("Create a target-bound template for unresolved agent-collected evidence").action(
|
|
964
1366
|
async (repository, options) => {
|
|
965
1367
|
const repo = resolve4(repository);
|
|
966
|
-
const path = resolve4(options.output ??
|
|
1368
|
+
const path = resolve4(options.output ?? join3(repo, ".agentic", "agent-evidence.yaml"));
|
|
967
1369
|
const requestPath = resolve4(
|
|
968
|
-
options.requestOutput ??
|
|
1370
|
+
options.requestOutput ?? join3(repo, ".agentic", "evidence-request.md")
|
|
969
1371
|
);
|
|
970
1372
|
if (path === requestPath) {
|
|
971
1373
|
throw new Error("Agent evidence bundle and request paths must be different");
|
|
@@ -973,7 +1375,7 @@ program.command("init-evidence").argument("[repository]", "repository to prepare
|
|
|
973
1375
|
if (!options.force) {
|
|
974
1376
|
for (const candidate of [path, requestPath]) {
|
|
975
1377
|
try {
|
|
976
|
-
await
|
|
1378
|
+
await readFile4(candidate, "utf8");
|
|
977
1379
|
throw new Error(`${candidate} already exists; use --force to replace it`);
|
|
978
1380
|
} catch (error) {
|
|
979
1381
|
if (error.code !== "ENOENT") throw error;
|
|
@@ -994,9 +1396,22 @@ program.command("init-evidence").argument("[repository]", "repository to prepare
|
|
|
994
1396
|
"init-evidence requires a Git commit so the bundle can be target-bound. Commit the assessed state and try again."
|
|
995
1397
|
);
|
|
996
1398
|
}
|
|
997
|
-
|
|
1399
|
+
if (context.metadata.tracked_tree_dirty) {
|
|
1400
|
+
throw new Error(
|
|
1401
|
+
"init-evidence requires tracked files to match HEAD so every claim binds to the exact assessed commit."
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
1404
|
+
const baseline = await assess(repo, benchmark, controls, "read-only-analysis", {
|
|
1405
|
+
scope: "tracked"
|
|
1406
|
+
});
|
|
1407
|
+
const unresolved = new Set(
|
|
1408
|
+
baseline.controls.filter(({ status }) => status !== "met" && status !== "not_applicable").map(({ id }) => id)
|
|
1409
|
+
);
|
|
1410
|
+
const eligibleControls = controls.filter(
|
|
1411
|
+
({ allow_agent_evidence: allowed, id }) => allowed && unresolved.has(id)
|
|
1412
|
+
);
|
|
998
1413
|
const bundle = {
|
|
999
|
-
schema_version:
|
|
1414
|
+
schema_version: benchmark.version,
|
|
1000
1415
|
benchmark_version: benchmark.version,
|
|
1001
1416
|
target: repositoryEvidenceTarget(context.metadata),
|
|
1002
1417
|
collector: { name: "TODO: agent or adapter name", version: "TODO" },
|
|
@@ -1013,22 +1428,23 @@ ${stringify(bundle)}`,
|
|
|
1013
1428
|
await writeFile(
|
|
1014
1429
|
requestPath,
|
|
1015
1430
|
[
|
|
1016
|
-
"# ADRB v0.
|
|
1431
|
+
"# ADRB v0.3 evidence request",
|
|
1017
1432
|
"",
|
|
1018
1433
|
`- Repository: ${bundle.target.repository}`,
|
|
1019
1434
|
`- Git commit: ${bundle.target.git_head ?? "unavailable"}`,
|
|
1020
1435
|
`- Benchmark: ${benchmark.version}`,
|
|
1021
1436
|
"",
|
|
1022
|
-
"Obtain authorization before accessing connected systems. Use read-only, least-privileged tools. Add only attempted claims to the bundle;
|
|
1437
|
+
"Repository claims may cite only tracked paths from the bound commit and remain agent-collected, not repository-detected. Obtain authorization before accessing connected systems. Use read-only, least-privileged tools. Add only attempted claims to the bundle; partial or inconclusive evidence remains `unknown`. Never paste source excerpts, secrets, prompts, personal data, or raw sensitive content.",
|
|
1023
1438
|
"",
|
|
1024
1439
|
...eligibleControls.flatMap((control) => {
|
|
1025
1440
|
const manualCheck = control.evidence.find(
|
|
1026
1441
|
(check) => check.type === "manual"
|
|
1027
1442
|
);
|
|
1443
|
+
const scopes = control.agent_evidence_scopes.length > 0 ? control.agent_evidence_scopes : [manualCheck?.scope ?? "organization"];
|
|
1028
1444
|
return [
|
|
1029
1445
|
`## ${control.id} \u2014 ${control.title}`,
|
|
1030
1446
|
"",
|
|
1031
|
-
`- Scope: ${
|
|
1447
|
+
`- Scope: ${scopes.join(", ")}`,
|
|
1032
1448
|
`- Request: ${manualCheck?.prompt ?? control.outcome}`,
|
|
1033
1449
|
`- Risk: ${control.risk}`,
|
|
1034
1450
|
""
|
|
@@ -1042,7 +1458,7 @@ Created ${requestPath}
|
|
|
1042
1458
|
`);
|
|
1043
1459
|
}
|
|
1044
1460
|
);
|
|
1045
|
-
program.command("assess").argument("[repository]", "repository to assess", ".").option("--profile <profile>", "target autonomy profile", "pr-creation").option("--format <format>", "json or markdown", "markdown").option("--output <path>", "write the report to a file").option("--attestations <path>", "manual attestation file").option("--agent-evidence <path>", "agent-collected external evidence bundle").option("--scope <scope>", "tracked or workspace", "tracked").option("--enforce", "exit non-zero when the target profile fails", false).option("--github-output", "append summary values to $GITHUB_OUTPUT", false).description("Assess a repository using local, read-only evidence collection").action(
|
|
1461
|
+
program.command("assess").argument("[repository]", "repository to assess", ".").option("--profile <profile>", "target autonomy profile", "pr-creation").option("--format <format>", "json or markdown", "markdown").option("--output <path>", "write the report to a file").option("--attestations <path>", "manual attestation file").option("--agent-evidence <path>", "agent-collected repository or external evidence bundle").option("--scope <scope>", "tracked or workspace", "tracked").option("--enforce", "exit non-zero when the target profile fails", false).option("--github-output", "append summary values to $GITHUB_OUTPUT", false).description("Assess a repository using local, read-only evidence collection").action(
|
|
1046
1462
|
async (repository, options) => {
|
|
1047
1463
|
if (!["json", "markdown"].includes(options.format)) {
|
|
1048
1464
|
throw new Error("--format must be json or markdown");
|
|
@@ -1052,19 +1468,27 @@ program.command("assess").argument("[repository]", "repository to assess", ".").
|
|
|
1052
1468
|
}
|
|
1053
1469
|
const repo = resolve4(repository);
|
|
1054
1470
|
const { benchmark, controls } = await loadBenchmark();
|
|
1471
|
+
const warnings = [];
|
|
1055
1472
|
const attestationPath = resolve4(
|
|
1056
|
-
options.attestations ??
|
|
1473
|
+
options.attestations ?? join3(repo, ".agentic", "attestations.yaml")
|
|
1057
1474
|
);
|
|
1058
|
-
const attestations = await loadAttestations(attestationPath, benchmark.version
|
|
1475
|
+
const attestations = await loadAttestations(attestationPath, benchmark.version, {
|
|
1476
|
+
ignoreVersionMismatch: options.attestations === void 0,
|
|
1477
|
+
onWarning: (warning) => warnings.push(warning)
|
|
1478
|
+
});
|
|
1059
1479
|
const agentEvidencePath = resolve4(
|
|
1060
|
-
options.agentEvidence ??
|
|
1480
|
+
options.agentEvidence ?? join3(repo, ".agentic", "agent-evidence.yaml")
|
|
1061
1481
|
);
|
|
1062
|
-
const agentEvidence = await loadAgentEvidence(agentEvidencePath
|
|
1482
|
+
const agentEvidence = await loadAgentEvidence(agentEvidencePath, benchmark.version, {
|
|
1483
|
+
ignoreVersionMismatch: options.agentEvidence === void 0,
|
|
1484
|
+
onWarning: (warning) => warnings.push(warning)
|
|
1485
|
+
});
|
|
1063
1486
|
const reportPath = options.output ? resolve4(options.output) : null;
|
|
1064
1487
|
const report = await assess(repo, benchmark, controls, options.profile, {
|
|
1065
1488
|
scope: options.scope,
|
|
1066
1489
|
attestations,
|
|
1067
1490
|
agentEvidence,
|
|
1491
|
+
warnings,
|
|
1068
1492
|
excludedPaths: [attestationPath, agentEvidencePath, ...reportPath ? [reportPath] : []]
|
|
1069
1493
|
});
|
|
1070
1494
|
const output = options.format === "json" ? `${JSON.stringify(report, null, 2)}
|
|
@@ -1084,6 +1508,9 @@ program.command("assess").argument("[repository]", "repository to assess", ".").
|
|
|
1084
1508
|
githubOutput,
|
|
1085
1509
|
`score=${report.score.total}
|
|
1086
1510
|
percentage=${report.score.percentage}
|
|
1511
|
+
repository_score=${report.score.repository?.achieved ?? ""}
|
|
1512
|
+
repository_ceiling=${report.score.repository?.ceiling ?? ""}
|
|
1513
|
+
repository_percentage=${report.score.repository?.percentage ?? ""}
|
|
1087
1514
|
highest_profile=${report.readiness.highest_profile ?? "none"}
|
|
1088
1515
|
target_passed=${String(report.readiness.target_passed)}
|
|
1089
1516
|
report_path=${reportPath ?? ""}
|