agentic-scorecard 0.2.0 → 0.3.1

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.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 readFile3, writeFile } from "fs/promises";
5
- import { dirname as dirname2, join as join2, resolve as resolve4 } from "path";
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: z.enum(["platform", "organization", "outcome"]).default("organization"),
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.2");
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
- async function loadAttestations(path, benchmarkVersion) {
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
- return AgentEvidenceFileSchema.parse(await readYaml(path));
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 (benchmark.version === "0.2.0" && control.evidence.some(({ type }) => type === "content_any" || type === "content_all")) {
236
- throw new Error(`${control.id} uses a legacy broad content collector in benchmark v0.2.0`);
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 && !control.evidence.some(({ type }) => type === "manual")) {
239
- throw new Error(`${control.id} allows agent evidence without an external evidence check`);
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.allow_attestation && !control.evidence.some(({ type }) => type === "manual")) {
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
  );
@@ -337,6 +537,10 @@ function toMarkdown(report) {
337
537
  (control) => ["platform", "organization"].includes(controlScope(control))
338
538
  );
339
539
  const outcomeControls = unresolved.filter((control) => controlScope(control) === "outcome");
540
+ const repositoryOnlyBaseline = !report.controls.some(
541
+ ({ agent_evidence: agentEvidence, attestation }) => agentEvidence !== null || attestation !== null
542
+ );
543
+ const resolvedEvidence = report.evidence_summary.resolved !== void 0 && report.evidence_summary.total !== void 0 ? `; ${report.evidence_summary.resolved}/${report.evidence_summary.total} controls resolved` : "";
340
544
  const lines = [
341
545
  "# Agentic Development Readiness Assessment",
342
546
  "",
@@ -346,11 +550,24 @@ function toMarkdown(report) {
346
550
  `- Git commit: ${report.target.git_head ? `\`${report.target.git_head}\`` : "unavailable"}`,
347
551
  `- Working tree dirty: ${report.target.working_tree_dirty === null ? "unknown" : String(report.target.working_tree_dirty)}`,
348
552
  `- Assessed: ${report.assessed_at}`,
349
- `- Score: **${report.score.total}/${report.score.maximum} (${report.score.percentage}%)**`,
553
+ ...repositoryOnlyBaseline ? [
554
+ "- Assessment mode: **repository-only baseline** \u2014 platform, organization, and outcome evidence has not been established"
555
+ ] : ["- Assessment mode: **evidence-assisted assessment**"],
556
+ ...report.score.repository ? [
557
+ `- 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`
558
+ ] : [],
559
+ `- Normative readiness score: **${report.score.total}/${report.score.maximum} (${report.score.percentage}%)**`,
350
560
  `- Highest readiness profile: **${report.readiness.highest_profile ?? "none"}**`,
351
- `- Target \`${report.target.profile}\`: **${report.readiness.target_passed ? "PASS" : "FAIL"}**`,
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`,
561
+ `- Target \`${report.target.profile}\`: **${report.readiness.target_passed ? `PASS${targetProvenance}` : "FAIL"}**`,
562
+ `- Established 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${resolvedEvidence}`,
563
+ ...report.warnings && report.warnings.length > 0 ? [`- Warnings: **${report.warnings.length} \u2014 review before using this assessment**`] : [],
353
564
  "",
565
+ ...report.warnings && report.warnings.length > 0 ? [
566
+ "## Warnings",
567
+ "",
568
+ ...report.warnings.map((warning) => `- WARNING: ${safeText(warning)}`),
569
+ ""
570
+ ] : [],
354
571
  "## Dimensions",
355
572
  "",
356
573
  "| Dimension | Score | Controls met |",
@@ -453,10 +670,11 @@ async function git(repo, args) {
453
670
  async function createRepositoryContext(repository, scope, excludedPaths = []) {
454
671
  const requestedRoot = resolve2(repository);
455
672
  const root = await realpath(requestedRoot);
456
- const [headOutput, remoteOutput, statusOutput] = await Promise.all([
673
+ const [headOutput, remoteOutput, statusOutput, trackedStatusOutput] = await Promise.all([
457
674
  git(root, ["rev-parse", "HEAD"]),
458
675
  git(root, ["config", "--get", "remote.origin.url"]),
459
- git(root, ["status", "--porcelain"])
676
+ git(root, ["status", "--porcelain"]),
677
+ git(root, ["status", "--porcelain", "--untracked-files=no"])
460
678
  ]);
461
679
  let includedPaths = null;
462
680
  if (scope === "tracked") {
@@ -474,7 +692,8 @@ async function createRepositoryContext(repository, scope, excludedPaths = []) {
474
692
  scope,
475
693
  git_head: headOutput?.trim() || null,
476
694
  git_remote: sanitizeRemote(remoteOutput?.trim() || null),
477
- working_tree_dirty: statusOutput === null ? null : statusOutput.length > 0
695
+ working_tree_dirty: statusOutput === null ? null : statusOutput.length > 0,
696
+ tracked_tree_dirty: trackedStatusOutput === null ? null : trackedStatusOutput.length > 0
478
697
  },
479
698
  includedPaths,
480
699
  excludedPaths: new Set(
@@ -492,6 +711,10 @@ function repositoryEvidenceTarget(metadata) {
492
711
  };
493
712
  }
494
713
 
714
+ // src/score.ts
715
+ import { readFile as readFile3 } from "fs/promises";
716
+ import { join as join2 } from "path";
717
+
495
718
  // src/evidence.ts
496
719
  import { lstat, readFile as readFile2, realpath as realpath2, stat } from "fs/promises";
497
720
  import { resolve as resolve3, sep as sep2 } from "path";
@@ -553,9 +776,9 @@ async function evaluatePathAll(context, check) {
553
776
  found
554
777
  );
555
778
  }
556
- async function readSearchableFiles(context, patterns) {
779
+ async function readSearchableFiles(context, patterns, maxFilesPerPattern) {
557
780
  const root = context.metadata.root;
558
- const paths = (await matches(context, patterns)).slice(0, maxContentFiles);
781
+ const paths = maxFilesPerPattern ? await prioritizedMatches(context, patterns, maxFilesPerPattern) : (await matches(context, patterns)).slice(0, maxContentFiles);
559
782
  const files = [];
560
783
  let totalBytes = 0;
561
784
  for (const path of paths) {
@@ -577,6 +800,26 @@ async function readSearchableFiles(context, patterns) {
577
800
  }
578
801
  return files;
579
802
  }
803
+ async function prioritizedMatches(context, patterns, maxFilesPerPattern) {
804
+ const selected = [];
805
+ const seen = /* @__PURE__ */ new Set();
806
+ const groups = await Promise.all(
807
+ patterns.map(
808
+ async (pattern) => (await matches(context, [pattern])).slice(0, maxFilesPerPattern)
809
+ )
810
+ );
811
+ for (let candidateIndex = 0; candidateIndex < maxFilesPerPattern; candidateIndex += 1) {
812
+ for (const group of groups) {
813
+ const path = group[candidateIndex];
814
+ if (!path) continue;
815
+ if (seen.has(path)) continue;
816
+ seen.add(path);
817
+ selected.push(path);
818
+ if (selected.length === maxContentFiles) return selected;
819
+ }
820
+ }
821
+ return selected;
822
+ }
580
823
  function isGeneratedAssessment(text) {
581
824
  const normalized = text.trimStart();
582
825
  if (normalized.startsWith("# agentic development readiness assessment")) return true;
@@ -609,29 +852,72 @@ async function evaluateLegacyContent(context, check) {
609
852
  );
610
853
  }
611
854
  async function evaluateContentTerms(context, check) {
612
- const files = await readSearchableFiles(context, check.files);
855
+ const files = await readSearchableFiles(context, check.files, check.max_files_per_pattern);
613
856
  const matchesByFile = files.map(({ path, text }) => ({
614
857
  path,
615
- matched: check.terms.filter((term) => containsTerm(text, term)).length,
616
- requiredMatched: check.required_any_terms?.filter((term) => containsTerm(text, term)).length ?? 0
858
+ ...strongestContentMatch(
859
+ text,
860
+ check.terms,
861
+ check.required_any_terms ?? [],
862
+ check.min_terms,
863
+ check.max_span_lines
864
+ )
617
865
  }));
618
- const qualifying = matchesByFile.filter(
619
- ({ matched, requiredMatched }) => matched >= check.min_terms && (check.required_any_terms === void 0 || requiredMatched > 0)
620
- );
866
+ const qualifying = matchesByFile.filter(({ qualifies }) => qualifies);
621
867
  const strongest = matchesByFile.reduce((maximum, file) => Math.max(maximum, file.matched), 0);
622
868
  const strongestRequired = matchesByFile.reduce(
623
869
  (maximum, file) => Math.max(maximum, file.requiredMatched),
624
870
  0
625
871
  );
626
872
  const requiredSummary = check.required_any_terms ? `; strongest required match ${strongestRequired}/${check.required_any_terms.length}` : "";
873
+ const proximitySummary = check.max_span_lines ? ` within ${check.max_span_lines}-line window(s)` : "";
627
874
  return result(
628
875
  check.type,
629
876
  check.scope,
630
877
  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}`,
878
+ `${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
879
  qualifying.map(({ path }) => path)
633
880
  );
634
881
  }
882
+ function strongestContentMatch(text, terms, requiredTerms, minTerms, maxSpanLines) {
883
+ if (!maxSpanLines) {
884
+ const matched = terms.filter((term) => containsTerm(text, term)).length;
885
+ const requiredMatched = requiredTerms.filter((term) => containsTerm(text, term)).length;
886
+ return {
887
+ matched,
888
+ requiredMatched,
889
+ qualifies: matched >= minTerms && (requiredTerms.length === 0 || requiredMatched > 0)
890
+ };
891
+ }
892
+ const termCounts = terms.map(() => 0);
893
+ const requiredCounts = requiredTerms.map(() => 0);
894
+ const lines = text.split(/\r?\n/);
895
+ let strongest = 0;
896
+ let strongestRequired = 0;
897
+ let qualifies = false;
898
+ const update = (line, direction) => {
899
+ terms.forEach((term, index) => {
900
+ if (containsTerm(line, term)) termCounts[index] = (termCounts[index] ?? 0) + direction;
901
+ });
902
+ requiredTerms.forEach((term, index) => {
903
+ if (containsTerm(line, term)) {
904
+ requiredCounts[index] = (requiredCounts[index] ?? 0) + direction;
905
+ }
906
+ });
907
+ };
908
+ for (let index = 0; index < lines.length; index += 1) {
909
+ update(lines[index] ?? "", 1);
910
+ if (index >= maxSpanLines) update(lines[index - maxSpanLines] ?? "", -1);
911
+ const matched = termCounts.filter((count) => count > 0).length;
912
+ const requiredMatched = requiredCounts.filter((count) => count > 0).length;
913
+ strongest = Math.max(strongest, matched);
914
+ strongestRequired = Math.max(strongestRequired, requiredMatched);
915
+ if (matched >= minTerms && (requiredTerms.length === 0 || requiredMatched > 0)) {
916
+ qualifies = true;
917
+ }
918
+ }
919
+ return { matched: strongest, requiredMatched: strongestRequired, qualifies };
920
+ }
635
921
  function containsTerm(text, term) {
636
922
  const pattern = term.trim().split(/\s+/).map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\s_-]+");
637
923
  return new RegExp(`(^|[^a-z0-9])${pattern}(?=$|[^a-z0-9])`, "i").test(text);
@@ -753,6 +1039,46 @@ function dimensionScore(controls, dimension) {
753
1039
  }
754
1040
  return score;
755
1041
  }
1042
+ function repositoryScore(catalog, results, dimensions) {
1043
+ let achieved = 0;
1044
+ let ceiling = 0;
1045
+ const resultsById = new Map(results.map((result2) => [result2.id, result2]));
1046
+ for (const { id: dimension } of dimensions) {
1047
+ let dimensionAchieved = 0;
1048
+ let dimensionCeiling = 0;
1049
+ let achievedOpen = true;
1050
+ let ceilingOpen = true;
1051
+ for (const level of [1, 2, 3, 4]) {
1052
+ const controlsAtLevel = catalog.filter(
1053
+ (control) => control.dimension === dimension && control.level === level
1054
+ );
1055
+ const repositoryDetectable = controlsAtLevel.every(
1056
+ (control) => control.evidence.every((evidence) => evidence.scope === "repository")
1057
+ );
1058
+ if (ceilingOpen && repositoryDetectable) {
1059
+ dimensionCeiling = level;
1060
+ } else {
1061
+ ceilingOpen = false;
1062
+ }
1063
+ const repositoryEstablished = controlsAtLevel.every((control) => {
1064
+ const result2 = resultsById.get(control.id);
1065
+ return result2?.status === "met" && result2.confidence === "repository-detected";
1066
+ });
1067
+ if (achievedOpen && repositoryDetectable && repositoryEstablished) {
1068
+ dimensionAchieved = level;
1069
+ } else {
1070
+ achievedOpen = false;
1071
+ }
1072
+ }
1073
+ achieved += dimensionAchieved;
1074
+ ceiling += dimensionCeiling;
1075
+ }
1076
+ return {
1077
+ achieved,
1078
+ ceiling,
1079
+ percentage: ceiling === 0 ? 0 : Math.round(achieved / ceiling * 100)
1080
+ };
1081
+ }
756
1082
  function assessProfiles(benchmark, dimensions, controls) {
757
1083
  return benchmark.readiness_profiles.map((profile) => {
758
1084
  const blockers = benchmark.dimensions.flatMap(({ id }) => {
@@ -770,7 +1096,23 @@ function assessProfiles(benchmark, dimensions, controls) {
770
1096
  }
771
1097
  ];
772
1098
  });
773
- return { id: profile.id, title: profile.title, passed: blockers.length === 0, blockers };
1099
+ const requiredControls = controls.filter(
1100
+ (control) => control.level <= profile.floors[control.dimension] && controlPasses(control)
1101
+ );
1102
+ return {
1103
+ id: profile.id,
1104
+ title: profile.title,
1105
+ passed: blockers.length === 0,
1106
+ blockers,
1107
+ ...benchmark.version === "0.3.0" ? {
1108
+ evidence_dependencies: {
1109
+ agent_collected: requiredControls.filter(
1110
+ ({ confidence }) => confidence === "agent-collected"
1111
+ ).length,
1112
+ attested: requiredControls.filter(({ confidence }) => confidence === "attested").length
1113
+ }
1114
+ } : {}
1115
+ };
774
1116
  });
775
1117
  }
776
1118
  async function assess(repo, benchmark, catalog, profileId, options = {}) {
@@ -783,7 +1125,7 @@ async function assess(repo, benchmark, catalog, profileId, options = {}) {
783
1125
  const scope = options.scope ?? "tracked";
784
1126
  const now = options.now ?? /* @__PURE__ */ new Date();
785
1127
  const context = await createRepositoryContext(repo, scope, options.excludedPaths);
786
- validateAgentEvidence(benchmark, catalog, context, options.agentEvidence ?? null, now);
1128
+ await validateAgentEvidence(benchmark, catalog, context, options.agentEvidence ?? null, now);
787
1129
  const controls = await Promise.all(
788
1130
  catalog.map(
789
1131
  async (control) => evaluateControl(
@@ -795,6 +1137,27 @@ async function assess(repo, benchmark, catalog, profileId, options = {}) {
795
1137
  )
796
1138
  )
797
1139
  );
1140
+ const warnings = [...options.warnings ?? []];
1141
+ if (benchmark.version === "0.3.0") {
1142
+ if (scope === "tracked" && context.metadata.tracked_tree_dirty) {
1143
+ warnings.push(
1144
+ "Tracked assessment includes uncommitted tracked-file contents, so the result is not reproducible from git_head alone. Use a clean worktree before comparing scores or collecting agent evidence."
1145
+ );
1146
+ }
1147
+ const hasActiveSupplementalEvidence = controls.some(
1148
+ ({ agent_evidence: agentEvidence, attestation }) => agentEvidence !== null || attestation !== null
1149
+ );
1150
+ const unresolvedExternalOrOutcome = controls.some(
1151
+ ({ evidence, status }) => (status === "unknown" || status === "not_met") && evidence.some(
1152
+ ({ scope: evidenceScope }) => ["platform", "organization", "outcome"].includes(evidenceScope)
1153
+ )
1154
+ );
1155
+ if (!hasActiveSupplementalEvidence && unresolvedExternalOrOutcome) {
1156
+ warnings.push(
1157
+ "Repository-only baseline: no active agent-collected or human-attested evidence was supplied. Platform, organization, and outcome evidence remains unresolved until authorized evidence is collected with init-evidence or supplied by accountable owners."
1158
+ );
1159
+ }
1160
+ }
798
1161
  const dimensions = benchmark.dimensions.map(({ id, title }) => {
799
1162
  const dimensionControls = controls.filter((control) => control.dimension === id);
800
1163
  return {
@@ -807,10 +1170,11 @@ async function assess(repo, benchmark, catalog, profileId, options = {}) {
807
1170
  });
808
1171
  const profiles = assessProfiles(benchmark, dimensions, controls);
809
1172
  const total = dimensions.reduce((sum, { score }) => sum + score, 0);
1173
+ const repository = benchmark.version === "0.3.0" ? repositoryScore(catalog, controls, benchmark.dimensions) : void 0;
810
1174
  const highestProfile = [...profiles].reverse().find(({ passed }) => passed)?.id ?? null;
811
1175
  const targetPassed = profiles.find(({ id }) => id === profileId)?.passed ?? false;
812
1176
  return {
813
- schema_version: "0.2.0",
1177
+ schema_version: benchmark.version === "0.3.0" ? "0.3.0" : "0.2.0",
814
1178
  benchmark: { id: benchmark.id, version: benchmark.version },
815
1179
  target: {
816
1180
  repository: repo,
@@ -821,7 +1185,13 @@ async function assess(repo, benchmark, catalog, profileId, options = {}) {
821
1185
  working_tree_dirty: context.metadata.working_tree_dirty
822
1186
  },
823
1187
  assessed_at: now.toISOString(),
824
- score: { total, maximum: 40, percentage: Math.round(total / 40 * 100) },
1188
+ ...benchmark.version === "0.3.0" ? { warnings } : {},
1189
+ score: {
1190
+ total,
1191
+ maximum: 40,
1192
+ percentage: Math.round(total / 40 * 100),
1193
+ ...repository ? { repository } : {}
1194
+ },
825
1195
  evidence_summary: {
826
1196
  repository_detected: controls.filter(
827
1197
  ({ confidence, status }) => confidence === "repository-detected" && status === "met"
@@ -833,7 +1203,11 @@ async function assess(repo, benchmark, catalog, profileId, options = {}) {
833
1203
  ({ confidence, status }) => confidence === "attested" && status === "met"
834
1204
  ).length,
835
1205
  unmet: controls.filter(({ status }) => status === "not_met").length,
836
- unknown: controls.filter(({ status }) => status === "unknown").length
1206
+ unknown: controls.filter(({ status }) => status === "unknown").length,
1207
+ ...benchmark.version === "0.3.0" ? {
1208
+ resolved: controls.filter(({ status }) => status !== "unknown").length,
1209
+ total: controls.length
1210
+ } : {}
837
1211
  },
838
1212
  dimensions,
839
1213
  controls,
@@ -842,12 +1216,16 @@ async function assess(repo, benchmark, catalog, profileId, options = {}) {
842
1216
  limitations: [
843
1217
  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
1218
  "Repository-detected evidence proves a qualifying artifact match, not consistent practice or external enforcement.",
1219
+ ...benchmark.version === "0.3.0" ? [
1220
+ "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.",
1221
+ "Agent-collected repository evidence is semantic, target-bound, and source-backed but is not independently verified or relabelled as repository-detected."
1222
+ ] : [],
845
1223
  "Agent-collected evidence and human attestations are reported separately and are not independently verified.",
846
1224
  "This assessment does not grant production access, deployment authority, or certification."
847
1225
  ]
848
1226
  };
849
1227
  }
850
- function validateAgentEvidence(benchmark, catalog, context, evidence, now) {
1228
+ async function validateAgentEvidence(benchmark, catalog, context, evidence, now) {
851
1229
  if (!evidence) return;
852
1230
  if (evidence.benchmark_version !== benchmark.version) {
853
1231
  throw new Error(
@@ -865,6 +1243,9 @@ function validateAgentEvidence(benchmark, catalog, context, evidence, now) {
865
1243
  `Agent evidence commit ${evidence.target.git_head ?? "unavailable"} does not match ${expectedTarget.git_head ?? "an unavailable Git commit"}`
866
1244
  );
867
1245
  }
1246
+ if (benchmark.version === "0.3.0" && context.metadata.tracked_tree_dirty) {
1247
+ throw new Error("ADRB v0.3 agent evidence requires tracked files to match the bound commit");
1248
+ }
868
1249
  const controls = new Map(catalog.map((control) => [control.id, control]));
869
1250
  if (Object.keys(evidence.claims).length > 0 && (evidence.collector.name.startsWith("TODO") || evidence.collector.version.startsWith("TODO"))) {
870
1251
  throw new Error("Agent evidence with claims must identify the collector name and version");
@@ -875,10 +1256,39 @@ function validateAgentEvidence(benchmark, catalog, context, evidence, now) {
875
1256
  if (!control.allow_agent_evidence) {
876
1257
  throw new Error(`${controlId} does not permit agent-collected evidence`);
877
1258
  }
878
- const allowedScopes = control.evidence.filter(({ type }) => type === "manual").map(({ scope: evidenceScope }) => evidenceScope);
1259
+ const manualScopes = control.evidence.filter(({ type }) => type === "manual").map(({ scope: evidenceScope }) => evidenceScope);
1260
+ const allowedScopes = control.agent_evidence_scopes.length > 0 ? control.agent_evidence_scopes : manualScopes;
879
1261
  if (!allowedScopes.includes(claim.scope)) {
880
1262
  throw new Error(`${controlId} does not accept ${claim.scope} evidence`);
881
1263
  }
1264
+ if (claim.scope === "repository") {
1265
+ if (context.includedPaths === null) {
1266
+ throw new Error(
1267
+ `${controlId} uses repository-scoped agent evidence, which requires --scope tracked`
1268
+ );
1269
+ }
1270
+ for (const reference of claim.references) {
1271
+ const parsedReference = repositoryReference(reference);
1272
+ if (!parsedReference || !context.includedPaths.has(parsedReference.path) || context.excludedPaths.has(parsedReference.path)) {
1273
+ throw new Error(`${controlId} references an unavailable tracked path: ${reference}`);
1274
+ }
1275
+ if (parsedReference.lines) {
1276
+ const contents = await readFile3(
1277
+ join2(context.metadata.root, parsedReference.path),
1278
+ "utf8"
1279
+ );
1280
+ const lineCount = countLines(contents);
1281
+ if (parsedReference.lines.start < 1 || parsedReference.lines.end < parsedReference.lines.start) {
1282
+ throw new Error(`${controlId} references an invalid line range: ${reference}`);
1283
+ }
1284
+ if (parsedReference.lines.end > lineCount) {
1285
+ throw new Error(
1286
+ `${controlId} references lines beyond ${parsedReference.path}'s ${lineCount} lines: ${reference}`
1287
+ );
1288
+ }
1289
+ }
1290
+ }
1291
+ }
882
1292
  if (new Date(claim.collected_at) > new Date(claim.expires_at)) {
883
1293
  throw new Error(`${controlId} expires before it was collected`);
884
1294
  }
@@ -890,10 +1300,30 @@ function validateAgentEvidence(benchmark, catalog, context, evidence, now) {
890
1300
  }
891
1301
  }
892
1302
  }
1303
+ function repositoryReference(reference) {
1304
+ if (!reference.startsWith("repo:")) return null;
1305
+ const withoutPrefix = reference.slice("repo:".length);
1306
+ const lineMatch = withoutPrefix.match(/#L(\d+)(?:-L?(\d+))?$/);
1307
+ const path = lineMatch ? withoutPrefix.slice(0, lineMatch.index) : withoutPrefix;
1308
+ if (path.length === 0 || path.startsWith("/") || path.includes("#") || path.includes("\\") || path.split("/").some((part) => part === ".." || part === ".")) {
1309
+ return null;
1310
+ }
1311
+ const start = lineMatch ? Number(lineMatch[1]) : null;
1312
+ const end = lineMatch ? Number(lineMatch[2] ?? lineMatch[1]) : null;
1313
+ return {
1314
+ path,
1315
+ lines: start === null || end === null ? null : { start, end }
1316
+ };
1317
+ }
1318
+ function countLines(contents) {
1319
+ if (contents.length === 0) return 0;
1320
+ const lines = contents.split("\n").length;
1321
+ return contents.endsWith("\n") ? lines - 1 : lines;
1322
+ }
893
1323
 
894
1324
  // src/cli.ts
895
1325
  var program = new Command();
896
- program.name("agentic-scorecard").description("Evidence-backed readiness assessment for agentic software development harnesses").version("0.2.0");
1326
+ program.name("agentic-scorecard").description("Evidence-backed readiness assessment for agentic software development harnesses").version("0.3.1");
897
1327
  program.command("validate").description("Validate the bundled benchmark catalog").action(async () => {
898
1328
  const { benchmark, controls } = await loadBenchmark();
899
1329
  process.stdout.write(
@@ -920,10 +1350,10 @@ ${control.evidence.map((check) => `- ${stringify(check).trim().replaceAll("\n",
920
1350
  });
921
1351
  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
1352
  const repo = resolve4(repository);
923
- const path = join2(repo, ".agentic", "attestations.yaml");
1353
+ const path = join3(repo, ".agentic", "attestations.yaml");
924
1354
  if (!options.force) {
925
1355
  try {
926
- await readFile3(path, "utf8");
1356
+ await readFile4(path, "utf8");
927
1357
  throw new Error(`${path} already exists; use --force to replace it`);
928
1358
  } catch (error) {
929
1359
  if (error.code !== "ENOENT") throw error;
@@ -960,12 +1390,12 @@ ${stringify({ benchmark_version: benchmark.version, attestations })}`,
960
1390
  process.stdout.write(`Created ${path}
961
1391
  `);
962
1392
  });
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 external evidence").action(
1393
+ 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
1394
  async (repository, options) => {
965
1395
  const repo = resolve4(repository);
966
- const path = resolve4(options.output ?? join2(repo, ".agentic", "agent-evidence.yaml"));
1396
+ const path = resolve4(options.output ?? join3(repo, ".agentic", "agent-evidence.yaml"));
967
1397
  const requestPath = resolve4(
968
- options.requestOutput ?? join2(repo, ".agentic", "evidence-request.md")
1398
+ options.requestOutput ?? join3(repo, ".agentic", "evidence-request.md")
969
1399
  );
970
1400
  if (path === requestPath) {
971
1401
  throw new Error("Agent evidence bundle and request paths must be different");
@@ -973,7 +1403,7 @@ program.command("init-evidence").argument("[repository]", "repository to prepare
973
1403
  if (!options.force) {
974
1404
  for (const candidate of [path, requestPath]) {
975
1405
  try {
976
- await readFile3(candidate, "utf8");
1406
+ await readFile4(candidate, "utf8");
977
1407
  throw new Error(`${candidate} already exists; use --force to replace it`);
978
1408
  } catch (error) {
979
1409
  if (error.code !== "ENOENT") throw error;
@@ -994,9 +1424,22 @@ program.command("init-evidence").argument("[repository]", "repository to prepare
994
1424
  "init-evidence requires a Git commit so the bundle can be target-bound. Commit the assessed state and try again."
995
1425
  );
996
1426
  }
997
- const eligibleControls = controls.filter(({ allow_agent_evidence: allowed }) => allowed);
1427
+ if (context.metadata.tracked_tree_dirty) {
1428
+ throw new Error(
1429
+ "init-evidence requires tracked files to match HEAD so every claim binds to the exact assessed commit."
1430
+ );
1431
+ }
1432
+ const baseline = await assess(repo, benchmark, controls, "read-only-analysis", {
1433
+ scope: "tracked"
1434
+ });
1435
+ const unresolved = new Set(
1436
+ baseline.controls.filter(({ status }) => status !== "met" && status !== "not_applicable").map(({ id }) => id)
1437
+ );
1438
+ const eligibleControls = controls.filter(
1439
+ ({ allow_agent_evidence: allowed, id }) => allowed && unresolved.has(id)
1440
+ );
998
1441
  const bundle = {
999
- schema_version: "0.2.0",
1442
+ schema_version: benchmark.version,
1000
1443
  benchmark_version: benchmark.version,
1001
1444
  target: repositoryEvidenceTarget(context.metadata),
1002
1445
  collector: { name: "TODO: agent or adapter name", version: "TODO" },
@@ -1013,22 +1456,23 @@ ${stringify(bundle)}`,
1013
1456
  await writeFile(
1014
1457
  requestPath,
1015
1458
  [
1016
- "# ADRB v0.2 external evidence request",
1459
+ "# ADRB v0.3 evidence request",
1017
1460
  "",
1018
1461
  `- Repository: ${bundle.target.repository}`,
1019
1462
  `- Git commit: ${bundle.target.git_head ?? "unavailable"}`,
1020
1463
  `- Benchmark: ${benchmark.version}`,
1021
1464
  "",
1022
- "Obtain authorization before accessing connected systems. Use read-only, least-privileged tools. Add only attempted claims to the bundle; errors remain `unknown`. Never paste secrets or raw sensitive content.",
1465
+ "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
1466
  "",
1024
1467
  ...eligibleControls.flatMap((control) => {
1025
1468
  const manualCheck = control.evidence.find(
1026
1469
  (check) => check.type === "manual"
1027
1470
  );
1471
+ const scopes = control.agent_evidence_scopes.length > 0 ? control.agent_evidence_scopes : [manualCheck?.scope ?? "organization"];
1028
1472
  return [
1029
1473
  `## ${control.id} \u2014 ${control.title}`,
1030
1474
  "",
1031
- `- Scope: ${manualCheck?.scope ?? "organization"}`,
1475
+ `- Scope: ${scopes.join(", ")}`,
1032
1476
  `- Request: ${manualCheck?.prompt ?? control.outcome}`,
1033
1477
  `- Risk: ${control.risk}`,
1034
1478
  ""
@@ -1042,7 +1486,7 @@ Created ${requestPath}
1042
1486
  `);
1043
1487
  }
1044
1488
  );
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(
1489
+ 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
1490
  async (repository, options) => {
1047
1491
  if (!["json", "markdown"].includes(options.format)) {
1048
1492
  throw new Error("--format must be json or markdown");
@@ -1052,19 +1496,27 @@ program.command("assess").argument("[repository]", "repository to assess", ".").
1052
1496
  }
1053
1497
  const repo = resolve4(repository);
1054
1498
  const { benchmark, controls } = await loadBenchmark();
1499
+ const warnings = [];
1055
1500
  const attestationPath = resolve4(
1056
- options.attestations ?? join2(repo, ".agentic", "attestations.yaml")
1501
+ options.attestations ?? join3(repo, ".agentic", "attestations.yaml")
1057
1502
  );
1058
- const attestations = await loadAttestations(attestationPath, benchmark.version);
1503
+ const attestations = await loadAttestations(attestationPath, benchmark.version, {
1504
+ ignoreVersionMismatch: options.attestations === void 0,
1505
+ onWarning: (warning) => warnings.push(warning)
1506
+ });
1059
1507
  const agentEvidencePath = resolve4(
1060
- options.agentEvidence ?? join2(repo, ".agentic", "agent-evidence.yaml")
1508
+ options.agentEvidence ?? join3(repo, ".agentic", "agent-evidence.yaml")
1061
1509
  );
1062
- const agentEvidence = await loadAgentEvidence(agentEvidencePath);
1510
+ const agentEvidence = await loadAgentEvidence(agentEvidencePath, benchmark.version, {
1511
+ ignoreVersionMismatch: options.agentEvidence === void 0,
1512
+ onWarning: (warning) => warnings.push(warning)
1513
+ });
1063
1514
  const reportPath = options.output ? resolve4(options.output) : null;
1064
1515
  const report = await assess(repo, benchmark, controls, options.profile, {
1065
1516
  scope: options.scope,
1066
1517
  attestations,
1067
1518
  agentEvidence,
1519
+ warnings,
1068
1520
  excludedPaths: [attestationPath, agentEvidencePath, ...reportPath ? [reportPath] : []]
1069
1521
  });
1070
1522
  const output = options.format === "json" ? `${JSON.stringify(report, null, 2)}
@@ -1084,6 +1536,9 @@ program.command("assess").argument("[repository]", "repository to assess", ".").
1084
1536
  githubOutput,
1085
1537
  `score=${report.score.total}
1086
1538
  percentage=${report.score.percentage}
1539
+ repository_score=${report.score.repository?.achieved ?? ""}
1540
+ repository_ceiling=${report.score.repository?.ceiling ?? ""}
1541
+ repository_percentage=${report.score.repository?.percentage ?? ""}
1087
1542
  highest_profile=${report.readiness.highest_profile ?? "none"}
1088
1543
  target_passed=${String(report.readiness.target_passed)}
1089
1544
  report_path=${reportPath ?? ""}