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/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
  );
@@ -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 ? "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`,
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
- matched: check.terms.filter((term) => containsTerm(text, term)).length,
616
- requiredMatched: check.required_any_terms?.filter((term) => containsTerm(text, term)).length ?? 0
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
- return { id: profile.id, title: profile.title, passed: blockers.length === 0, blockers };
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
- score: { total, maximum: 40, percentage: Math.round(total / 40 * 100) },
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 allowedScopes = control.evidence.filter(({ type }) => type === "manual").map(({ scope: evidenceScope }) => evidenceScope);
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.2.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 = join2(repo, ".agentic", "attestations.yaml");
1325
+ const path = join3(repo, ".agentic", "attestations.yaml");
924
1326
  if (!options.force) {
925
1327
  try {
926
- await readFile3(path, "utf8");
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 external evidence").action(
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 ?? join2(repo, ".agentic", "agent-evidence.yaml"));
1368
+ const path = resolve4(options.output ?? join3(repo, ".agentic", "agent-evidence.yaml"));
967
1369
  const requestPath = resolve4(
968
- options.requestOutput ?? join2(repo, ".agentic", "evidence-request.md")
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 readFile3(candidate, "utf8");
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
- const eligibleControls = controls.filter(({ allow_agent_evidence: allowed }) => allowed);
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: "0.2.0",
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.2 external evidence request",
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; errors remain `unknown`. Never paste secrets or raw sensitive content.",
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: ${manualCheck?.scope ?? "organization"}`,
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 ?? join2(repo, ".agentic", "attestations.yaml")
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 ?? join2(repo, ".agentic", "agent-evidence.yaml")
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 ?? ""}