agentic-scorecard 0.3.0 → 0.4.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.
Files changed (31) hide show
  1. package/AGENT_PROMPT.md +68 -52
  2. package/README.md +50 -37
  3. package/benchmark/v0.4/adapter-contract.md +129 -0
  4. package/benchmark/v0.4/adapters/known-agent-harnesses.yaml +266 -0
  5. package/benchmark/v0.4/adapters/known-ci-providers.yaml +57 -0
  6. package/benchmark/v0.4/adapters/known-command-wrappers.yaml +37 -0
  7. package/benchmark/v0.4/adapters/known-environment-tooling.yaml +17 -0
  8. package/benchmark/v0.4/adapters/known-github-conventions.yaml +27 -0
  9. package/benchmark/v0.4/adapters/known-python-tooling.yaml +15 -0
  10. package/benchmark/v0.4/adapters/known-security-tools.yaml +45 -0
  11. package/benchmark/v0.4/adapters/known-verification-tools.yaml +65 -0
  12. package/benchmark/v0.4/agent-evidence-schema.json +74 -0
  13. package/benchmark/v0.4/attestation-schema.json +35 -0
  14. package/benchmark/v0.4/benchmark.yaml +127 -0
  15. package/benchmark/v0.4/calibration-matrix.md +60 -0
  16. package/benchmark/v0.4/controls/context.yaml +75 -0
  17. package/benchmark/v0.4/controls/environment.yaml +93 -0
  18. package/benchmark/v0.4/controls/governance.yaml +132 -0
  19. package/benchmark/v0.4/controls/learning.yaml +78 -0
  20. package/benchmark/v0.4/controls/observability.yaml +91 -0
  21. package/benchmark/v0.4/controls/resilience.yaml +170 -0
  22. package/benchmark/v0.4/controls/security.yaml +143 -0
  23. package/benchmark/v0.4/controls/specification.yaml +65 -0
  24. package/benchmark/v0.4/controls/testing.yaml +82 -0
  25. package/benchmark/v0.4/controls/tooling.yaml +69 -0
  26. package/benchmark/v0.4/report-schema.json +89 -0
  27. package/benchmark/v0.4/scoring-policy.md +175 -0
  28. package/dist/cli.js +2315 -162
  29. package/package.json +3 -2
  30. package/templates/agent-evidence.yaml +2 -2
  31. package/templates/attestations.yaml +3 -1
package/dist/cli.js CHANGED
@@ -44,6 +44,11 @@ var PathAllSchema = z.object({
44
44
  patterns: z.array(z.string().min(1)).min(1),
45
45
  min_bytes: z.number().int().positive().default(1)
46
46
  });
47
+ var OwnershipMapSchema = z.object({
48
+ type: z.literal("ownership_map"),
49
+ scope: z.literal("repository").default("repository"),
50
+ patterns: z.array(z.string().min(1)).min(1)
51
+ });
47
52
  var ContentAnySchema = z.object({
48
53
  type: z.literal("content_any"),
49
54
  scope: z.literal("repository").default("repository"),
@@ -66,6 +71,82 @@ var ContentTermsSchema = z.object({
66
71
  max_span_lines: z.number().int().positive().max(200).optional(),
67
72
  max_files_per_pattern: z.number().int().positive().max(250).optional()
68
73
  });
74
+ var ContentGroupsSchema = z.object({
75
+ type: z.literal("content_groups"),
76
+ scope: z.literal("repository").default("repository"),
77
+ files: z.array(z.string().min(1)).min(1),
78
+ groups: z.array(
79
+ z.object({
80
+ id: z.string().regex(/^[a-z0-9-]+$/),
81
+ terms: z.array(z.string().min(1)).min(1)
82
+ })
83
+ ).min(1),
84
+ min_groups: z.number().int().positive(),
85
+ max_span_lines: z.number().int().positive().max(200).optional(),
86
+ max_files_per_pattern: z.number().int().positive().max(250).optional()
87
+ });
88
+ var CiProviderSchema = z.object({
89
+ id: z.enum(["github-actions", "gitlab-ci", "azure-pipelines"]),
90
+ files: z.array(z.string().min(1)).min(1)
91
+ });
92
+ var SourcePatternSchema = z.string().min(1).refine((pattern) => {
93
+ try {
94
+ new RegExp(pattern, "u");
95
+ return true;
96
+ } catch {
97
+ return false;
98
+ }
99
+ }, "Source patterns must be valid regular expressions");
100
+ var CiCommandSignatureSchema = z.object({
101
+ executables: z.array(z.string().min(1)).min(1),
102
+ repository_executables: z.array(z.string().min(1)).default([]),
103
+ argument_groups: z.array(z.array(z.string().min(1)).min(1)).default([]),
104
+ source_content_groups: z.array(z.array(z.string().min(1)).min(1)).default([]),
105
+ source_pattern_groups: z.array(z.array(SourcePatternSchema).min(1)).default([]),
106
+ source_max_span_lines: z.number().int().positive().max(200).default(120),
107
+ required_argument_prefixes: z.array(z.array(z.string().min(1)).min(1)).default([]),
108
+ max_arguments: z.number().int().nonnegative().optional(),
109
+ prohibited_arguments: z.array(z.string().min(1)).default([]),
110
+ prohibited_argument_sequences: z.array(z.array(z.string().min(1)).min(2)).default([])
111
+ });
112
+ var CiToolSchema = z.object({
113
+ id: z.string().regex(/^[a-z0-9-]+$/),
114
+ commands: z.array(CiCommandSignatureSchema).default([]),
115
+ standalone_executables: z.array(z.string().min(1)).default([]),
116
+ actions: z.array(z.string().regex(/^[^/@\s]+\/[^/@\s]+$/)).default([]),
117
+ prohibited_arguments: z.array(z.string().min(1)).default([]),
118
+ prohibited_argument_sequences: z.array(z.array(z.string().min(1)).min(2)).default([]),
119
+ requires_final_exit_status: z.boolean().default(false)
120
+ }).superRefine((tool, context) => {
121
+ const commandConfigured = tool.commands.length > 0 || tool.standalone_executables.length > 0;
122
+ if (!commandConfigured && tool.actions.length === 0) {
123
+ context.addIssue({
124
+ code: z.ZodIssueCode.custom,
125
+ message: "A CI tool must define a command signature, a standalone executable, or a full action identity"
126
+ });
127
+ }
128
+ });
129
+ var CiCommandWrapperSchema = z.object({
130
+ executable_patterns: z.array(SourcePatternSchema).min(1),
131
+ command_prefixes: z.array(z.array(z.string().min(1))).min(1)
132
+ }).strict();
133
+ var CiInvocationGrammarSchema = z.object({
134
+ wrappers: z.array(CiCommandWrapperSchema).default([]),
135
+ wrapper_options_with_values: z.array(z.string().min(1)).default([])
136
+ }).strict();
137
+ var CiCommandSchema = z.object({
138
+ type: z.literal("ci_command"),
139
+ scope: z.literal("repository").default("repository"),
140
+ providers: z.array(CiProviderSchema).default([]),
141
+ tools: z.array(CiToolSchema).default([]),
142
+ min_tools: z.number().int().positive().default(1),
143
+ tool_match_mode: z.enum(["aggregate", "same-execution"]).default("aggregate"),
144
+ invocation_grammar: CiInvocationGrammarSchema.default({
145
+ wrappers: [],
146
+ wrapper_options_with_values: []
147
+ }),
148
+ max_files_per_pattern: z.number().int().positive().max(250).optional()
149
+ });
69
150
  var MaxBytesSchema = z.object({
70
151
  type: z.literal("max_bytes"),
71
152
  scope: z.literal("repository").default("repository"),
@@ -80,9 +161,12 @@ var ManualSchema = z.object({
80
161
  var EvidenceCheckSchema = z.discriminatedUnion("type", [
81
162
  PathAnySchema,
82
163
  PathAllSchema,
164
+ OwnershipMapSchema,
83
165
  ContentAnySchema,
84
166
  ContentAllSchema,
85
167
  ContentTermsSchema,
168
+ ContentGroupsSchema,
169
+ CiCommandSchema,
86
170
  MaxBytesSchema,
87
171
  ManualSchema
88
172
  ]);
@@ -93,6 +177,7 @@ var RawControlSchema = z.object({
93
177
  outcome: z.string().min(1),
94
178
  risk: z.string().min(1),
95
179
  evidence: z.array(EvidenceCheckSchema).min(1),
180
+ evidence_mode: z.enum(["all", "any"]).default("all"),
96
181
  remediation: z.string().min(1),
97
182
  references: z.array(z.string().min(1)).default([]),
98
183
  allow_attestation: z.boolean().default(false),
@@ -117,26 +202,38 @@ var DetectorAdapterExtensionSchema = z.object({
117
202
  patterns: z.array(z.string().min(1)).min(1).optional(),
118
203
  files: z.array(z.string().min(1)).min(1).optional(),
119
204
  terms: z.array(z.string().min(1)).min(1).optional(),
120
- required_any_terms: z.array(z.string().min(1)).min(1).optional()
205
+ required_any_terms: z.array(z.string().min(1)).min(1).optional(),
206
+ ci_providers: z.array(CiProviderSchema).min(1).optional(),
207
+ ci_tools: z.array(CiToolSchema).min(1).optional()
121
208
  }).strict().superRefine((extension, context) => {
122
209
  const extensionKinds = [
123
210
  extension.patterns,
124
211
  extension.files,
125
212
  extension.terms,
126
- extension.required_any_terms
213
+ extension.required_any_terms,
214
+ extension.ci_providers,
215
+ extension.ci_tools
127
216
  ].filter(Boolean).length;
128
217
  if (extensionKinds !== 1) {
129
218
  context.addIssue({
130
219
  code: z.ZodIssueCode.custom,
131
- message: "A detector extension must declare exactly one of patterns, files, terms, or required_any_terms"
220
+ message: "A detector extension must declare exactly one of patterns, files, terms, required_any_terms, ci_providers, or ci_tools"
132
221
  });
133
222
  }
134
223
  });
135
224
  var DetectorAdapterSchema = z.object({
136
225
  id: z.string().regex(/^[a-z0-9-]+$/),
137
226
  benchmark_version: z.string().regex(/^\d+\.\d+\.\d+$/),
138
- extensions: z.array(DetectorAdapterExtensionSchema).min(1)
139
- }).strict();
227
+ ci_invocation_grammar: CiInvocationGrammarSchema.optional(),
228
+ extensions: z.array(DetectorAdapterExtensionSchema).default([])
229
+ }).strict().superRefine((adapter, context) => {
230
+ if (adapter.extensions.length === 0 && !adapter.ci_invocation_grammar) {
231
+ context.addIssue({
232
+ code: z.ZodIssueCode.custom,
233
+ message: "A detector adapter must declare extensions or CI invocation grammar"
234
+ });
235
+ }
236
+ });
140
237
  var DimensionSchema = z.object({
141
238
  id: DimensionIdSchema,
142
239
  title: z.string().min(1),
@@ -175,6 +272,11 @@ var AttestationSchema = z.object({
175
272
  reviewed_at: z.string().date(),
176
273
  expires_at: z.string().date()
177
274
  });
275
+ var AttestationV04Schema = AttestationSchema.strict();
276
+ var AttestationTargetV04Schema = z.object({
277
+ repository: z.string().min(1)
278
+ }).strict();
279
+ var AttestationControlIdV04Schema = z.string().regex(/^ADRB-[A-Z]{3}-\d{3}$/);
178
280
  var LegacyAttestationSchema = AttestationSchema.extend({
179
281
  expires_at: z.string().date().nullable().default(null)
180
282
  });
@@ -182,6 +284,11 @@ var AttestationFileSchema = z.object({
182
284
  benchmark_version: z.string().min(1),
183
285
  attestations: z.record(z.string(), AttestationSchema).default({})
184
286
  });
287
+ var AttestationFileV04Schema = z.object({
288
+ benchmark_version: z.literal("0.4.0"),
289
+ target: AttestationTargetV04Schema,
290
+ attestations: z.record(AttestationControlIdV04Schema, AttestationV04Schema)
291
+ }).strict();
185
292
  var LegacyAttestationFileSchema = z.object({
186
293
  benchmark_version: z.string().min(1),
187
294
  attestations: z.record(z.string(), LegacyAttestationSchema).default({})
@@ -240,23 +347,27 @@ var AgentEvidenceClaimV03Schema = z.object({
240
347
  });
241
348
  }
242
349
  });
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();
350
+ function modernAgentEvidenceFileSchema(version) {
351
+ return z.object({
352
+ schema_version: z.literal(version),
353
+ benchmark_version: z.literal(version),
354
+ target: z.object({
355
+ repository: z.string().min(1),
356
+ git_head: z.string().min(1)
357
+ }).strict(),
358
+ collector: z.object({
359
+ name: z.string().min(1),
360
+ version: z.string().min(1)
361
+ }).strict(),
362
+ claims: z.record(z.string().regex(/^ADRB-[A-Z]{3}-\d{3}$/), AgentEvidenceClaimV03Schema)
363
+ }).strict();
364
+ }
365
+ var AgentEvidenceFileV03Schema = modernAgentEvidenceFileSchema("0.3.0");
366
+ var AgentEvidenceFileV04Schema = modernAgentEvidenceFileSchema("0.4.0");
256
367
 
257
368
  // src/load.ts
258
369
  var packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
259
- var defaultBenchmarkRoot = join(packageRoot, "benchmark", "v0.3");
370
+ var defaultBenchmarkRoot = join(packageRoot, "benchmark", "v0.4");
260
371
  async function readYaml(path) {
261
372
  return parse(await readFile(path, "utf8"));
262
373
  }
@@ -286,6 +397,28 @@ function applyDetectorAdapter(benchmark, controls, adapter) {
286
397
  `Detector adapter ${adapter.id} targets ${adapter.benchmark_version}, not ${benchmark.version}`
287
398
  );
288
399
  }
400
+ if (adapter.ci_invocation_grammar) {
401
+ for (const control of controls) {
402
+ for (const check of control.evidence) {
403
+ if (check.type !== "ci_command") continue;
404
+ check.invocation_grammar = {
405
+ wrappers: [
406
+ ...new Map(
407
+ [...check.invocation_grammar.wrappers, ...adapter.ci_invocation_grammar.wrappers].map(
408
+ (wrapper) => [JSON.stringify(wrapper), wrapper]
409
+ )
410
+ ).values()
411
+ ],
412
+ wrapper_options_with_values: [
413
+ .../* @__PURE__ */ new Set([
414
+ ...check.invocation_grammar.wrapper_options_with_values,
415
+ ...adapter.ci_invocation_grammar.wrapper_options_with_values
416
+ ])
417
+ ]
418
+ };
419
+ }
420
+ }
421
+ }
289
422
  for (const extension of adapter.extensions) {
290
423
  const control = controls.find(({ id }) => id === extension.control_id);
291
424
  if (!control) {
@@ -331,6 +464,67 @@ function applyDetectorAdapter(benchmark, controls, adapter) {
331
464
  .../* @__PURE__ */ new Set([...check.required_any_terms ?? [], ...extension.required_any_terms])
332
465
  ];
333
466
  }
467
+ if (extension.ci_providers) {
468
+ if (check.type !== "ci_command") {
469
+ throw new Error(
470
+ `Detector adapter ${adapter.id} cannot add CI providers to ${control.id} evidence ${extension.evidence_index}`
471
+ );
472
+ }
473
+ const providers = new Map(check.providers.map((provider) => [provider.id, provider]));
474
+ for (const extensionProvider of extension.ci_providers) {
475
+ const provider = providers.get(extensionProvider.id);
476
+ providers.set(extensionProvider.id, {
477
+ id: extensionProvider.id,
478
+ files: [.../* @__PURE__ */ new Set([...provider?.files ?? [], ...extensionProvider.files])]
479
+ });
480
+ }
481
+ check.providers = [...providers.values()];
482
+ }
483
+ if (extension.ci_tools) {
484
+ if (check.type !== "ci_command") {
485
+ throw new Error(
486
+ `Detector adapter ${adapter.id} cannot add CI tools to ${control.id} evidence ${extension.evidence_index}`
487
+ );
488
+ }
489
+ const tools = new Map(check.tools.map((tool) => [tool.id, tool]));
490
+ for (const extensionTool of extension.ci_tools) {
491
+ const tool = tools.get(extensionTool.id);
492
+ tools.set(extensionTool.id, {
493
+ id: extensionTool.id,
494
+ commands: [
495
+ ...new Map(
496
+ [...tool?.commands ?? [], ...extensionTool.commands].map((command) => [
497
+ JSON.stringify(command),
498
+ command
499
+ ])
500
+ ).values()
501
+ ],
502
+ standalone_executables: [
503
+ .../* @__PURE__ */ new Set([
504
+ ...tool?.standalone_executables ?? [],
505
+ ...extensionTool.standalone_executables
506
+ ])
507
+ ],
508
+ actions: [.../* @__PURE__ */ new Set([...tool?.actions ?? [], ...extensionTool.actions])],
509
+ prohibited_arguments: [
510
+ .../* @__PURE__ */ new Set([
511
+ ...tool?.prohibited_arguments ?? [],
512
+ ...extensionTool.prohibited_arguments
513
+ ])
514
+ ],
515
+ prohibited_argument_sequences: [
516
+ ...new Map(
517
+ [
518
+ ...tool?.prohibited_argument_sequences ?? [],
519
+ ...extensionTool.prohibited_argument_sequences
520
+ ].map((sequence) => [JSON.stringify(sequence), sequence])
521
+ ).values()
522
+ ],
523
+ requires_final_exit_status: (tool?.requires_final_exit_status ?? false) || extensionTool.requires_final_exit_status
524
+ });
525
+ }
526
+ check.tools = [...tools.values()];
527
+ }
334
528
  }
335
529
  }
336
530
  async function loadAttestations(path, benchmarkVersion, options = {}) {
@@ -347,7 +541,7 @@ async function loadAttestations(path, benchmarkVersion, options = {}) {
347
541
  )) {
348
542
  return null;
349
543
  }
350
- const file = benchmarkVersion === "0.1.0" ? LegacyAttestationFileSchema.parse(rawFile) : AttestationFileSchema.parse(rawFile);
544
+ const file = benchmarkVersion === "0.1.0" ? LegacyAttestationFileSchema.parse(rawFile) : benchmarkVersion === "0.4.0" ? AttestationFileV04Schema.parse(rawFile) : AttestationFileSchema.parse(rawFile);
351
545
  if (file.benchmark_version !== benchmarkVersion) {
352
546
  throw new Error(
353
547
  `Attestation benchmark version ${file.benchmark_version} does not match ${benchmarkVersion}`
@@ -380,6 +574,7 @@ async function loadAgentEvidence(path, benchmarkVersion, options = {}) {
380
574
  const file = (() => {
381
575
  if (benchmarkVersion === "0.2.0") return AgentEvidenceFileSchema.parse(rawFile);
382
576
  if (benchmarkVersion === "0.3.0") return AgentEvidenceFileV03Schema.parse(rawFile);
577
+ if (benchmarkVersion === "0.4.0") return AgentEvidenceFileV04Schema.parse(rawFile);
383
578
  throw new Error(`Agent evidence bundles are unsupported for benchmark ${benchmarkVersion}`);
384
579
  })();
385
580
  if (file.benchmark_version !== benchmarkVersion) {
@@ -416,7 +611,7 @@ function validateCatalog(benchmark, controls) {
416
611
  for (const control of controls) {
417
612
  if (ids.has(control.id)) throw new Error(`Duplicate control id: ${control.id}`);
418
613
  ids.add(control.id);
419
- if (["0.2.0", "0.3.0"].includes(benchmark.version) && control.evidence.some(({ type }) => type === "content_any" || type === "content_all")) {
614
+ if (["0.2.0", "0.3.0", "0.4.0"].includes(benchmark.version) && control.evidence.some(({ type }) => type === "content_any" || type === "content_all")) {
420
615
  throw new Error(
421
616
  `${control.id} uses a legacy broad content collector in benchmark ${benchmark.version}`
422
617
  );
@@ -432,13 +627,34 @@ function validateCatalog(benchmark, controls) {
432
627
  if (benchmark.version === "0.2.0" && control.agent_evidence_scopes.some((scope) => scope === "repository")) {
433
628
  throw new Error(`${control.id} changes immutable v0.2 repository evidence semantics`);
434
629
  }
435
- if (["0.2.0", "0.3.0"].includes(benchmark.version) && control.allow_attestation && !control.evidence.some(({ type }) => type === "manual")) {
630
+ if (["0.2.0", "0.3.0", "0.4.0"].includes(benchmark.version) && control.allow_attestation && !control.evidence.some(({ type }) => type === "manual")) {
436
631
  throw new Error(`${control.id} allows attestation for repository-detected evidence`);
437
632
  }
633
+ if (control.evidence_mode === "any" && control.evidence.length < 2) {
634
+ throw new Error(`${control.id} uses alternative evidence without multiple evidence checks`);
635
+ }
438
636
  for (const check of control.evidence) {
439
637
  if (check.type === "content_terms" && check.min_terms > check.terms.length) {
440
638
  throw new Error(`${control.id} requires more content terms than it defines`);
441
639
  }
640
+ if (check.type === "ci_command" && check.providers.length === 0) {
641
+ throw new Error(`${control.id} defines a CI command collector without a provider adapter`);
642
+ }
643
+ if (check.type === "ci_command" && check.tools.length === 0) {
644
+ throw new Error(`${control.id} defines a CI command collector without a tool adapter`);
645
+ }
646
+ if (check.type === "ci_command" && check.min_tools > check.tools.length) {
647
+ throw new Error(`${control.id} requires more CI tools than it defines`);
648
+ }
649
+ if (check.type === "content_groups") {
650
+ const groupIds = check.groups.map(({ id }) => id);
651
+ if (new Set(groupIds).size !== groupIds.length) {
652
+ throw new Error(`${control.id} defines duplicate semantic evidence groups`);
653
+ }
654
+ if (check.min_groups > check.groups.length) {
655
+ throw new Error(`${control.id} requires more semantic groups than it defines`);
656
+ }
657
+ }
442
658
  }
443
659
  }
444
660
  const benchmarkDimensions = benchmark.dimensions.map(({ id }) => id);
@@ -463,6 +679,12 @@ var statusIcon = {
463
679
  not_applicable: "N/A"
464
680
  };
465
681
  function controlScope(control) {
682
+ if (control.confidence === "repository-detected") return "repository";
683
+ if (control.confidence === "agent-collected" && control.agent_evidence?.status === "met") {
684
+ return control.agent_evidence.scope;
685
+ }
686
+ const establishedEvidence = control.evidence.find(({ status }) => status === "met");
687
+ if (establishedEvidence) return establishedEvidence.scope;
466
688
  return control.evidence.find(({ scope }) => scope !== "repository")?.scope ?? "repository";
467
689
  }
468
690
  function safeText(value) {
@@ -497,13 +719,28 @@ function evidenceLines(control) {
497
719
  }
498
720
  return lines;
499
721
  }
500
- function appendControlDetails(lines, heading, controls) {
722
+ function confidenceRule(control) {
723
+ if (controlLevelConfidence(control) !== "none") return "";
724
+ return control.evidence_mode === "any" ? " \u2014 one evidence alternative must pass" : " \u2014 all required evidence checks must pass";
725
+ }
726
+ function controlLevelConfidence(control) {
727
+ return control.status === "unknown" ? "none" : control.confidence;
728
+ }
729
+ function appendControlDetails(lines, heading, controls, showCheckSummary) {
501
730
  lines.push("", `## ${heading}`, "");
502
731
  if (controls.length === 0) {
503
732
  lines.push("None.");
504
733
  return;
505
734
  }
506
735
  for (const control of controls) {
736
+ const establishedChecks = control.evidence.filter(({ status }) => status === "met").length;
737
+ const blockingChecks = control.evidence.flatMap(
738
+ ({ scope, status, type }, index) => status === "met" ? [] : [`#${index + 1} ${scope}/${type}`]
739
+ );
740
+ const blockingSummary = blockingChecks.map((check) => `\`${check}\``).join(", ");
741
+ const checkSummary = control.evidence_mode === "any" ? `Alternative evidence checks established: ${establishedChecks}/${control.evidence.length}; one required.` : `Required evidence checks established: ${establishedChecks}/${control.evidence.length}.`;
742
+ const blockingLabel = control.evidence_mode === "any" ? "Unresolved alternatives" : "Blocking checks";
743
+ const confidenceExplanation = confidenceRule(control);
507
744
  lines.push(
508
745
  `### ${statusIcon[control.status]} ${control.id} \u2014 ${control.title}`,
509
746
  "",
@@ -511,7 +748,11 @@ function appendControlDetails(lines, heading, controls) {
511
748
  "",
512
749
  `**Improve:** ${control.remediation}`,
513
750
  "",
514
- `Evidence confidence: ${control.confidence}.`,
751
+ ...showCheckSummary ? [
752
+ checkSummary,
753
+ ...blockingChecks.length > 0 ? [`${blockingLabel}: ${blockingSummary}.`] : [],
754
+ `Control confidence: ${controlLevelConfidence(control)}${confidenceExplanation}.`
755
+ ] : [`Evidence confidence: ${control.confidence}.`],
515
756
  "",
516
757
  ...evidenceLines(control),
517
758
  ""
@@ -519,6 +760,7 @@ function appendControlDetails(lines, heading, controls) {
519
760
  }
520
761
  }
521
762
  function toMarkdown(report) {
763
+ const showCheckSummary = report.benchmark.version === "0.4.0";
522
764
  const target = report.profiles.find(({ id }) => id === report.target.profile);
523
765
  const targetDependencies = target?.evidence_dependencies;
524
766
  const dependencyCount = (targetDependencies?.agent_collected ?? 0) + (targetDependencies?.attested ?? 0);
@@ -532,11 +774,19 @@ function toMarkdown(report) {
532
774
  const unresolved = report.controls.filter(
533
775
  ({ status }) => status === "not_met" || status === "unknown"
534
776
  );
535
- const repositoryGaps = unresolved.filter((control) => controlScope(control) === "repository");
536
- const externalControls = unresolved.filter(
777
+ const alternativeControls = unresolved.filter(({ evidence_mode: mode }) => mode === "any");
778
+ const requiredControls = unresolved.filter(({ evidence_mode: mode }) => mode !== "any");
779
+ const repositoryGaps = requiredControls.filter(
780
+ (control) => controlScope(control) === "repository"
781
+ );
782
+ const externalControls = requiredControls.filter(
537
783
  (control) => ["platform", "organization"].includes(controlScope(control))
538
784
  );
539
- const outcomeControls = unresolved.filter((control) => controlScope(control) === "outcome");
785
+ const outcomeControls = requiredControls.filter((control) => controlScope(control) === "outcome");
786
+ const repositoryOnlyBaseline = !report.controls.some(
787
+ ({ agent_evidence: agentEvidence, attestation }) => agentEvidence !== null || attestation !== null
788
+ );
789
+ const resolvedEvidence = report.evidence_summary.resolved !== void 0 && report.evidence_summary.total !== void 0 ? `; ${report.evidence_summary.resolved}/${report.evidence_summary.total} controls resolved` : "";
540
790
  const lines = [
541
791
  "# Agentic Development Readiness Assessment",
542
792
  "",
@@ -546,13 +796,16 @@ function toMarkdown(report) {
546
796
  `- Git commit: ${report.target.git_head ? `\`${report.target.git_head}\`` : "unavailable"}`,
547
797
  `- Working tree dirty: ${report.target.working_tree_dirty === null ? "unknown" : String(report.target.working_tree_dirty)}`,
548
798
  `- Assessed: ${report.assessed_at}`,
549
- `- Score: **${report.score.total}/${report.score.maximum} (${report.score.percentage}%)**`,
799
+ ...repositoryOnlyBaseline ? [
800
+ "- Assessment mode: **repository-only baseline** \u2014 platform, organization, and outcome evidence has not been established"
801
+ ] : ["- Assessment mode: **evidence-assisted assessment**"],
550
802
  ...report.score.repository ? [
551
803
  `- 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
804
  ] : [],
805
+ `- Normative readiness score: **${report.score.total}/${report.score.maximum} (${report.score.percentage}%)**`,
553
806
  `- Highest readiness profile: **${report.readiness.highest_profile ?? "none"}**`,
554
807
  `- 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` : ""}`,
808
+ `- 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}`,
556
809
  ...report.warnings && report.warnings.length > 0 ? [`- Warnings: **${report.warnings.length} \u2014 review before using this assessment**`] : [],
557
810
  "",
558
811
  ...report.warnings && report.warnings.length > 0 ? [
@@ -590,9 +843,27 @@ function toMarkdown(report) {
590
843
  );
591
844
  }
592
845
  }
593
- appendControlDetails(lines, "Repository evidence gaps", repositoryGaps);
594
- appendControlDetails(lines, "External controls not established", externalControls);
595
- appendControlDetails(lines, "Outcome evidence not established", outcomeControls);
846
+ appendControlDetails(lines, "Repository evidence gaps", repositoryGaps, showCheckSummary);
847
+ if (alternativeControls.length > 0) {
848
+ appendControlDetails(
849
+ lines,
850
+ "Alternative evidence paths not established",
851
+ alternativeControls,
852
+ showCheckSummary
853
+ );
854
+ }
855
+ appendControlDetails(
856
+ lines,
857
+ "External controls not established",
858
+ externalControls,
859
+ showCheckSummary
860
+ );
861
+ appendControlDetails(
862
+ lines,
863
+ "Outcome evidence not established",
864
+ outcomeControls,
865
+ showCheckSummary
866
+ );
596
867
  lines.push(
597
868
  "",
598
869
  "## Limitations",
@@ -703,6 +974,11 @@ function repositoryEvidenceTarget(metadata) {
703
974
  git_head: metadata.git_head
704
975
  };
705
976
  }
977
+ function normalizeRepositoryTarget(repository) {
978
+ const target = repository.trim();
979
+ const remoteLike = target.includes("://") || /^[^/\\]+@[^:]+:/.test(target);
980
+ return remoteLike ? target.replace(/\/+$/, "").replace(/\.git$/i, "") : target;
981
+ }
706
982
 
707
983
  // src/score.ts
708
984
  import { readFile as readFile3 } from "fs/promises";
@@ -710,21 +986,148 @@ import { join as join2 } from "path";
710
986
 
711
987
  // src/evidence.ts
712
988
  import { lstat, readFile as readFile2, realpath as realpath2, stat } from "fs/promises";
713
- import { resolve as resolve3, sep as sep2 } from "path";
989
+ import { basename, resolve as resolve3, sep as sep2 } from "path";
714
990
  import fg2 from "fast-glob";
991
+ import { parse as parse2 } from "yaml";
715
992
  var maxContentFileBytes = 512e3;
716
993
  var maxContentFiles = 250;
717
994
  var maxContentTotalBytes = 5e6;
995
+ var namedOwnerRolePattern = /^(.{2,80})\s+(?:team|owners?|reviewers?|maintainers?)$/i;
996
+ var ownerRoleAssignmentPattern = /^(?:owner|reviewer|maintainer|team)\s*[:=-]\s*(.{2,80})$/i;
997
+ var repositoryWideOwnershipTargets = /* @__PURE__ */ new Set([
998
+ "all files",
999
+ "default",
1000
+ "entire repository",
1001
+ "global",
1002
+ "repo",
1003
+ "repository",
1004
+ "root"
1005
+ ]);
1006
+ var namedOwnershipTargetPrefixes = /* @__PURE__ */ new Set(["area", "component", "module", "path", "scope"]);
1007
+ var placeholderOwnerValues = /* @__PURE__ */ new Set([
1008
+ "tbd",
1009
+ "to be assigned",
1010
+ "to be determined",
1011
+ "not assigned",
1012
+ "unassigned",
1013
+ "n/a",
1014
+ "na",
1015
+ "not applicable",
1016
+ "none",
1017
+ "no owner",
1018
+ "no owners",
1019
+ "no designated owner",
1020
+ "no designated owners",
1021
+ "without owner",
1022
+ "without owners",
1023
+ "without a owner",
1024
+ "without an owner",
1025
+ "without a owners",
1026
+ "without an owners",
1027
+ "nobody",
1028
+ "unknown",
1029
+ "pending",
1030
+ "vacant"
1031
+ ]);
1032
+ var semanticPlaceholderQualifiers = [
1033
+ "n/a",
1034
+ "none",
1035
+ "pending",
1036
+ "tbd",
1037
+ "to be assigned",
1038
+ "to be determined",
1039
+ "unassigned",
1040
+ "unknown",
1041
+ "vacant"
1042
+ ];
1043
+ var supportedGitlabRuleKeys = /* @__PURE__ */ new Set(["allow_failure", "if", "when"]);
1044
+ var gitlabReservedKeys = /* @__PURE__ */ new Set([
1045
+ "after_script",
1046
+ "before_script",
1047
+ "cache",
1048
+ "default",
1049
+ "image",
1050
+ "include",
1051
+ "services",
1052
+ "stages",
1053
+ "variables",
1054
+ "workflow"
1055
+ ]);
1056
+ var nonExecutingCommandArguments = /* @__PURE__ */ new Set([
1057
+ "--co",
1058
+ "--collect-only",
1059
+ "--help",
1060
+ "--list",
1061
+ "--list-tests",
1062
+ "--listtests",
1063
+ "--print-config",
1064
+ "--showconfig",
1065
+ "--version",
1066
+ "-h",
1067
+ "-list",
1068
+ "help",
1069
+ "list",
1070
+ "version"
1071
+ ]);
1072
+ var packageContextOptions = /* @__PURE__ */ new Set([
1073
+ "--cwd",
1074
+ "--dir",
1075
+ "--filter",
1076
+ "--prefix",
1077
+ "--workspace",
1078
+ "-c",
1079
+ "-w"
1080
+ ]);
1081
+ var packageGlobalOptionsWithValues = /* @__PURE__ */ new Set([
1082
+ "--cache",
1083
+ "--cafile",
1084
+ "--color",
1085
+ "--config",
1086
+ "--https-proxy",
1087
+ "--key",
1088
+ "--location",
1089
+ "--loglevel",
1090
+ "--otp",
1091
+ "--proxy",
1092
+ "--registry",
1093
+ "--scope",
1094
+ "--tag",
1095
+ "--userconfig"
1096
+ ]);
1097
+ var npmImplicitScripts = /* @__PURE__ */ new Map([
1098
+ ["restart", "restart"],
1099
+ ["start", "start"],
1100
+ ["stop", "stop"],
1101
+ ["t", "test"],
1102
+ ["test", "test"],
1103
+ ["tst", "test"]
1104
+ ]);
1105
+ var packageManagerDirectToolCommands = /* @__PURE__ */ new Map([
1106
+ ["bun", /* @__PURE__ */ new Set(["install"])],
1107
+ ["npm", /* @__PURE__ */ new Set(["ci"])],
1108
+ ["pnpm", /* @__PURE__ */ new Set(["install"])],
1109
+ ["yarn", /* @__PURE__ */ new Set(["install"])]
1110
+ ]);
1111
+ var matchCache = /* @__PURE__ */ new WeakMap();
1112
+ var searchableFileCache = /* @__PURE__ */ new WeakMap();
718
1113
  async function matches(context, patterns) {
719
- const found = await fg2(patterns, {
1114
+ const cache = matchCache.get(context) ?? /* @__PURE__ */ new Map();
1115
+ matchCache.set(context, cache);
1116
+ const key = JSON.stringify(patterns);
1117
+ const cached = cache.get(key);
1118
+ if (cached) return cached;
1119
+ const scan = fg2(patterns, {
720
1120
  cwd: context.metadata.root,
721
1121
  dot: true,
722
1122
  onlyFiles: true,
723
1123
  unique: true,
724
1124
  followSymbolicLinks: false,
725
1125
  ignore: generatedEvidenceIgnores
726
- });
727
- return found.filter((path) => context.includedPaths === null || context.includedPaths.has(path)).filter((path) => !context.excludedPaths.has(path)).sort();
1126
+ }).then(
1127
+ (found) => found.filter((path) => context.includedPaths === null || context.includedPaths.has(path)).filter((path) => !context.excludedPaths.has(path)).sort()
1128
+ );
1129
+ cache.set(key, scan);
1130
+ return scan;
728
1131
  }
729
1132
  async function safeFileSize(root, path) {
730
1133
  try {
@@ -769,30 +1172,274 @@ async function evaluatePathAll(context, check) {
769
1172
  found
770
1173
  );
771
1174
  }
772
- async function readSearchableFiles(context, patterns, maxFilesPerPattern) {
1175
+ async function evaluateOwnershipMap(context, check) {
1176
+ const files = await readSearchableFiles(context, check.patterns);
1177
+ const inspected = files.map(({ path, text }) => ({
1178
+ path,
1179
+ entries: ownershipEntries(path, text)
1180
+ }));
1181
+ const qualifying = inspected.filter(({ entries }) => entries > 0);
1182
+ const entryCount = qualifying.reduce((total, { entries }) => total + entries, 0);
1183
+ return result(
1184
+ check.type,
1185
+ check.scope,
1186
+ qualifying.length > 0 ? "met" : "not_met",
1187
+ `${qualifying.length} ownership mapping file(s) with ${entryCount} structurally identifiable assignment(s) across ${files.length} candidate file(s)`,
1188
+ qualifying.map(({ path }) => path)
1189
+ );
1190
+ }
1191
+ function ownershipEntries(path, text) {
1192
+ const name = basename(path).toLowerCase();
1193
+ const lines = text.split(/\r?\n/).map((line) => line.trim());
1194
+ if (name === "codeowners") {
1195
+ return lines.filter((line) => !line.startsWith("#")).filter((line) => {
1196
+ const fields = line.split(/\s+/);
1197
+ const inlineComment = fields.findIndex(
1198
+ (field, index) => index > 0 && field.startsWith("#")
1199
+ );
1200
+ const ownerFields = fields.slice(1, inlineComment < 0 ? void 0 : inlineComment);
1201
+ return ownerFields.length > 0 && isCodeownersTarget(fields[0] ?? "") && ownerFields.every((field) => isOwnerHandle(field) || isExactEmailContact(field));
1202
+ }).length;
1203
+ }
1204
+ if (["owners", "owners.md", "maintainers", "maintainers.md"].includes(name)) {
1205
+ return conventionalOwnershipEntries(activeOwnershipLines(lines));
1206
+ }
1207
+ const contentLines = activeOwnershipLines(lines);
1208
+ return markdownOwnershipRows(contentLines) + explicitOwnershipMappings(contentLines);
1209
+ }
1210
+ function conventionalOwnershipEntries(lines) {
1211
+ const listEntries = lines.filter(isConventionalOwnerListEntry).length;
1212
+ return listEntries + markdownOwnershipRows(lines) + explicitOwnershipMappings(lines);
1213
+ }
1214
+ function activeOwnershipLines(lines) {
1215
+ const active = [];
1216
+ let inactiveHeadingLevel = null;
1217
+ for (const line of lines) {
1218
+ const section = ownershipSectionState(line, inactiveHeadingLevel);
1219
+ if (section.handled) {
1220
+ inactiveHeadingLevel = section.inactiveHeadingLevel;
1221
+ active.push("");
1222
+ continue;
1223
+ }
1224
+ if (inactiveHeadingLevel === null) active.push(line);
1225
+ }
1226
+ return active;
1227
+ }
1228
+ function ownershipSectionState(line, inactiveHeadingLevel) {
1229
+ const heading = markdownHeading(line);
1230
+ if (heading) {
1231
+ if (inactiveHeadingLevel !== null && heading.level > inactiveHeadingLevel) {
1232
+ return { handled: true, inactiveHeadingLevel };
1233
+ }
1234
+ return {
1235
+ handled: true,
1236
+ inactiveHeadingLevel: inactiveOwnershipTitle(heading.title) ? heading.level : null
1237
+ };
1238
+ }
1239
+ const plainHeading = plainOwnershipHeading(line);
1240
+ if (!plainHeading) return { handled: false, inactiveHeadingLevel };
1241
+ if (inactiveHeadingLevel !== null && inactiveHeadingLevel <= 6) {
1242
+ return { handled: true, inactiveHeadingLevel };
1243
+ }
1244
+ return {
1245
+ handled: true,
1246
+ inactiveHeadingLevel: inactiveOwnershipTitle(plainHeading) ? 7 : null
1247
+ };
1248
+ }
1249
+ function inactiveOwnershipTitle(value) {
1250
+ return /\b(?:former|inactive|past|retired)\b/i.test(value);
1251
+ }
1252
+ function plainOwnershipHeading(line) {
1253
+ if (!line.endsWith(":") || line.includes("@")) return null;
1254
+ const title = line.slice(0, -1).trim();
1255
+ return /^[a-z][a-z0-9 &/_-]{1,80}$/i.test(title) ? title : null;
1256
+ }
1257
+ function markdownHeading(line) {
1258
+ let level = 0;
1259
+ while (level < 6 && line[level] === "#") level += 1;
1260
+ if (level === 0 || !/\s/.test(line[level] ?? "")) return null;
1261
+ const title = line.slice(level).trim();
1262
+ return title.length > 0 ? { level, title } : null;
1263
+ }
1264
+ function markdownOwnershipRows(lines) {
1265
+ let entries = 0;
1266
+ for (let index = 0; index < lines.length - 2; index += 1) {
1267
+ const columns = ownershipTableColumns(lines[index] ?? "", lines[index + 1] ?? "");
1268
+ if (!columns) continue;
1269
+ entries += countOwnershipTableRows(lines, index + 2, columns);
1270
+ }
1271
+ return entries;
1272
+ }
1273
+ function ownershipTableColumns(headerLine, separatorLine) {
1274
+ const header = markdownCells(headerLine);
1275
+ const separator = markdownCells(separatorLine);
1276
+ if (header.length < 2 || separator.length !== header.length) return null;
1277
+ if (!separator.every((cell) => /^:?-{3,}:?$/.test(cell))) return null;
1278
+ const scope = header.findIndex(
1279
+ (cell) => /\b(path|component|module|area|scope|repository)\b/i.test(cell)
1280
+ );
1281
+ const owner = header.findIndex((cell) => /\b(owner|reviewer|maintainer|team)\b/i.test(cell));
1282
+ return scope < 0 || owner < 0 ? null : { count: header.length, owner, scope };
1283
+ }
1284
+ function countOwnershipTableRows(lines, start, columns) {
1285
+ let entries = 0;
1286
+ for (let index = start; index < lines.length; index += 1) {
1287
+ const row = markdownCells(lines[index] ?? "");
1288
+ if (row.length !== columns.count) break;
1289
+ const scope = row[columns.scope] ?? "";
1290
+ const owner = row[columns.owner] ?? "";
1291
+ if (isOwnershipTableTarget(scope) && isOwnerReference(owner)) entries += 1;
1292
+ }
1293
+ return entries;
1294
+ }
1295
+ function isOwnershipTableTarget(value) {
1296
+ const target = value.trim();
1297
+ if (isOwnershipTarget(target)) return true;
1298
+ const firstSpace = target.indexOf(" ");
1299
+ const prefix = firstSpace > 0 ? target.slice(0, firstSpace).toLowerCase() : "";
1300
+ if (namedOwnershipTargetPrefixes.has(prefix)) return false;
1301
+ if (/^(?:\.{0,2}\/|\/)/.test(target) || /[/*]/.test(target)) return false;
1302
+ return !isPlaceholderOwner(target) && target.length >= 2 && target.length <= 100 && /^[a-z0-9][a-z0-9 _-]*$/i.test(target);
1303
+ }
1304
+ function markdownCells(line) {
1305
+ if (!line.includes("|")) return [];
1306
+ return line.replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim());
1307
+ }
1308
+ function explicitOwnershipMappings(lines) {
1309
+ return lines.filter((line) => {
1310
+ const mapping = parseOwnershipMapping(line);
1311
+ if (!mapping) return false;
1312
+ const { owner, target } = mapping;
1313
+ return isOwnershipTarget(target) && isOwnerReference(owner);
1314
+ }).length;
1315
+ }
1316
+ function isOwnershipTarget(value) {
1317
+ const target = value.trim();
1318
+ if (repositoryWideOwnershipTargets.has(target.toLowerCase())) return true;
1319
+ const firstSpace = target.indexOf(" ");
1320
+ if (firstSpace > 0) {
1321
+ const prefix = target.slice(0, firstSpace).toLowerCase();
1322
+ if (namedOwnershipTargetPrefixes.has(prefix)) {
1323
+ const namedScope = target.slice(firstSpace + 1);
1324
+ return !isPlaceholderOwner(namedScope) && /[a-z0-9_-]/i.test(namedScope);
1325
+ }
1326
+ }
1327
+ if (/^(?:\.{0,2}\/|\/)/.test(target) || /[/*]/.test(target)) {
1328
+ return /[a-z0-9_-]/i.test(target.replace(/^\.{0,2}\//, ""));
1329
+ }
1330
+ return /^[a-z0-9_.-]+\.[a-z0-9]{1,10}$/i.test(target);
1331
+ }
1332
+ function isCodeownersTarget(value) {
1333
+ const target = value.trim();
1334
+ if (["*", "**", "/*", "/**"].includes(target)) return true;
1335
+ return target.length > 0 && target.length <= 200 && !target.startsWith("!") && !target.includes("@") && /^[^\s]+$/.test(target) && /[a-z0-9_]/i.test(target);
1336
+ }
1337
+ function parseOwnershipMapping(line) {
1338
+ const normalized = line.replace(/^[-*]\s*/, "").trim();
1339
+ const separators = ["=>", "->", ":"];
1340
+ const separator = separators.map((value) => ({ index: normalized.indexOf(value), value })).filter(({ index }) => index > 0).sort((left, right) => left.index - right.index)[0];
1341
+ if (!separator) return null;
1342
+ const target = normalized.slice(0, separator.index).trim();
1343
+ const owner = normalized.slice(separator.index + separator.value.length).trim();
1344
+ if (target.length === 0 || target.length > 100 || owner.length === 0 || owner.length > 120) {
1345
+ return null;
1346
+ }
1347
+ return { owner, target };
1348
+ }
1349
+ function isOwnerReference(value) {
1350
+ const normalized = value.replace(/^[-*]\s*/, "").replace(/[*_`]/g, "").trim();
1351
+ if (isPlaceholderOwner(normalized) || hasNegativeOwnerAssignment(normalized)) return false;
1352
+ if (isDirectOwnerContact(normalized)) return true;
1353
+ const namedRole = namedOwnerRolePattern.exec(normalized);
1354
+ if (namedRole) return isNamedOwnerIdentity(namedRole[1] ?? "");
1355
+ const roleAssignment = ownerRoleAssignmentPattern.exec(normalized);
1356
+ return roleAssignment ? isNamedOwnerIdentity(roleAssignment[1] ?? "") : false;
1357
+ }
1358
+ function isConventionalOwnerListEntry(value) {
1359
+ const normalized = value.replace(/^[-*]\s*/, "").trim();
1360
+ return !hasNegativeOwnerAssignment(normalized) && isDirectOwnerContact(normalized);
1361
+ }
1362
+ function isDirectOwnerContact(value) {
1363
+ const namedEmail = /^([^<>]+?)\s*<([^<>\s]+)>$/.exec(value.trim());
1364
+ if (namedEmail) {
1365
+ return isNamedOwnerIdentity(namedEmail[1] ?? "") && isExactEmailContact(namedEmail[2] ?? "");
1366
+ }
1367
+ const contacts = value.replace(/\band\b/gi, " ").split(/[\s,&]+/).filter(Boolean);
1368
+ return contacts.length > 0 && contacts.every((contact) => isOwnerHandle(contact) || isExactEmailContact(contact));
1369
+ }
1370
+ function isOwnerHandle(value) {
1371
+ return /^@[a-z0-9][a-z0-9_/-]*$/i.test(value);
1372
+ }
1373
+ function isExactEmailContact(value) {
1374
+ if (/\s/.test(value)) return false;
1375
+ const at = value.indexOf("@");
1376
+ if (at <= 0 || at !== value.lastIndexOf("@")) return false;
1377
+ const domain = value.slice(at + 1);
1378
+ const dot = domain.indexOf(".");
1379
+ return dot > 0 && dot < domain.length - 1 && !domain.endsWith(".");
1380
+ }
1381
+ function isNamedOwnerIdentity(value) {
1382
+ const normalized = value.trim();
1383
+ return !isPlaceholderOwner(normalized) && !hasNegativeOwnerAssignment(normalized) && /^[a-z0-9][a-z0-9 ._/-]{1,79}$/i.test(normalized);
1384
+ }
1385
+ function hasNegativeOwnerAssignment(value) {
1386
+ const inactiveRole = /\b(?:former|inactive|retired|unassigned|vacant|deprecated)\b/i.test(value);
1387
+ const absentRole = /\b(?:no|without)\s+(?:designated\s+)?(?:owner|maintainer|reviewer|team)s?\b/i.test(value);
1388
+ return inactiveRole || absentRole;
1389
+ }
1390
+ function isPlaceholderOwner(value) {
1391
+ const normalized = value.trim().toLowerCase().replace(/\s+/g, " ");
1392
+ return placeholderOwnerValues.has(normalized) || normalized.length > 0 && normalized.replaceAll("-", "").length === 0;
1393
+ }
1394
+ async function readSearchableFiles(context, patterns, maxFilesPerPattern, preserveCase = false) {
1395
+ const cache = searchableFileCache.get(context) ?? /* @__PURE__ */ new Map();
1396
+ searchableFileCache.set(context, cache);
1397
+ const key = JSON.stringify([patterns, maxFilesPerPattern ?? null, preserveCase]);
1398
+ const cached = cache.get(key);
1399
+ if (cached) return cached;
1400
+ const read = readSearchableFilesUncached(context, patterns, maxFilesPerPattern, preserveCase);
1401
+ cache.set(key, read);
1402
+ return read;
1403
+ }
1404
+ async function readSearchableFilesUncached(context, patterns, maxFilesPerPattern, preserveCase = false) {
773
1405
  const root = context.metadata.root;
774
1406
  const paths = maxFilesPerPattern ? await prioritizedMatches(context, patterns, maxFilesPerPattern) : (await matches(context, patterns)).slice(0, maxContentFiles);
775
1407
  const files = [];
1408
+ const seenCanonicalPaths = /* @__PURE__ */ new Set();
776
1409
  let totalBytes = 0;
777
1410
  for (const path of paths) {
778
- try {
779
- const requestedPath = resolve3(root, path);
780
- if ((await lstat(requestedPath)).isSymbolicLink()) continue;
781
- const canonicalPath = await realpath2(requestedPath);
782
- if (canonicalPath !== root && !canonicalPath.startsWith(`${root}${sep2}`)) continue;
783
- const metadata = await stat(canonicalPath);
784
- if (!metadata.isFile() || metadata.size === 0 || metadata.size > maxContentFileBytes || totalBytes + metadata.size > maxContentTotalBytes) {
785
- continue;
786
- }
787
- const text = (await readFile2(canonicalPath, "utf8")).toLowerCase();
788
- if (isGeneratedAssessment(text)) continue;
789
- totalBytes += metadata.size;
790
- files.push({ path, text });
791
- } catch {
792
- }
1411
+ const candidate = await readSearchableFile(
1412
+ root,
1413
+ path,
1414
+ preserveCase,
1415
+ seenCanonicalPaths,
1416
+ maxContentTotalBytes - totalBytes
1417
+ );
1418
+ if (!candidate) continue;
1419
+ totalBytes += candidate.size;
1420
+ files.push({ path, text: candidate.text });
793
1421
  }
794
1422
  return files;
795
1423
  }
1424
+ async function readSearchableFile(root, path, preserveCase, seenCanonicalPaths, remainingBytes) {
1425
+ try {
1426
+ const requestedPath = resolve3(root, path);
1427
+ if ((await lstat(requestedPath)).isSymbolicLink()) return null;
1428
+ const canonicalPath = await realpath2(requestedPath);
1429
+ if (canonicalPath !== root && !canonicalPath.startsWith(`${root}${sep2}`)) return null;
1430
+ if (seenCanonicalPaths.has(canonicalPath)) return null;
1431
+ const metadata = await stat(canonicalPath);
1432
+ if (!metadata.isFile() || metadata.size === 0 || metadata.size > maxContentFileBytes)
1433
+ return null;
1434
+ if (metadata.size > remainingBytes) return null;
1435
+ const rawText = await readFile2(canonicalPath, "utf8");
1436
+ if (isGeneratedAssessment(rawText)) return null;
1437
+ seenCanonicalPaths.add(canonicalPath);
1438
+ return { size: metadata.size, text: preserveCase ? rawText : rawText.toLowerCase() };
1439
+ } catch {
1440
+ return null;
1441
+ }
1442
+ }
796
1443
  async function prioritizedMatches(context, patterns, maxFilesPerPattern) {
797
1444
  const selected = [];
798
1445
  const seen = /* @__PURE__ */ new Set();
@@ -815,7 +1462,9 @@ async function prioritizedMatches(context, patterns, maxFilesPerPattern) {
815
1462
  }
816
1463
  function isGeneratedAssessment(text) {
817
1464
  const normalized = text.trimStart();
818
- if (normalized.startsWith("# agentic development readiness assessment")) return true;
1465
+ if (normalized.toLowerCase().startsWith("# agentic development readiness assessment")) {
1466
+ return true;
1467
+ }
819
1468
  if (!normalized.startsWith("{")) return false;
820
1469
  try {
821
1470
  const candidate = JSON.parse(normalized);
@@ -872,89 +1521,1432 @@ async function evaluateContentTerms(context, check) {
872
1521
  qualifying.map(({ path }) => path)
873
1522
  );
874
1523
  }
875
- function strongestContentMatch(text, terms, requiredTerms, minTerms, maxSpanLines) {
1524
+ async function evaluateContentGroups(context, check) {
1525
+ const files = await readSearchableFiles(context, check.files, check.max_files_per_pattern);
1526
+ const matchesByFile = files.map(({ path, text }) => ({
1527
+ path,
1528
+ ...strongestGroupMatch(text, check.groups, check.min_groups, check.max_span_lines)
1529
+ }));
1530
+ const qualifying = matchesByFile.filter(({ qualifies }) => qualifies);
1531
+ const strongest = matchesByFile.reduce(
1532
+ (maximum, candidate) => candidate.matchedGroups.length > maximum.matchedGroups.length ? candidate : maximum,
1533
+ { path: null, matchedGroups: [], qualifies: false }
1534
+ );
1535
+ const matched = new Set(strongest.matchedGroups);
1536
+ const missing = check.groups.map(({ id }) => id).filter((id) => !matched.has(id));
1537
+ const proximitySummary = check.max_span_lines ? ` within ${check.max_span_lines}-line window(s)` : "";
1538
+ const partialReferences = qualifying.length > 0 ? qualifying.map(({ path }) => path) : strongest.path && strongest.matchedGroups.length > 0 ? [strongest.path] : [];
1539
+ return result(
1540
+ check.type,
1541
+ check.scope,
1542
+ qualifying.length > 0 ? "met" : "not_met",
1543
+ `${qualifying.length} qualifying file(s); strongest semantic coverage ${strongest.matchedGroups.length}/${check.groups.length} group(s)${proximitySummary} across ${files.length} candidate file(s); matched: ${strongest.matchedGroups.join(", ") || "none"}; missing: ${missing.join(", ") || "none"}; threshold ${check.min_groups}`,
1544
+ partialReferences
1545
+ );
1546
+ }
1547
+ function strongestGroupMatch(text, groups, minGroups, maxSpanLines) {
876
1548
  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
- };
1549
+ const lines2 = text.split(/\r?\n/);
1550
+ const matchedGroups = groups.filter(
1551
+ ({ terms }) => terms.some((term) => lines2.some((line) => containsPositiveTerm(line, term)))
1552
+ ).map(({ id }) => id);
1553
+ return { matchedGroups, qualifies: matchedGroups.length >= minGroups };
884
1554
  }
885
- const termCounts = terms.map(() => 0);
886
- const requiredCounts = requiredTerms.map(() => 0);
1555
+ const groupCounts = groups.map(() => 0);
887
1556
  const lines = text.split(/\r?\n/);
888
- let strongest = 0;
889
- let strongestRequired = 0;
890
- let qualifies = false;
1557
+ let strongestGroups = [];
891
1558
  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;
1559
+ groups.forEach(({ terms }, index) => {
1560
+ if (terms.some((term) => containsPositiveTerm(line, term))) {
1561
+ groupCounts[index] = (groupCounts[index] ?? 0) + direction;
898
1562
  }
899
1563
  });
900
1564
  };
901
1565
  for (let index = 0; index < lines.length; index += 1) {
902
1566
  update(lines[index] ?? "", 1);
903
1567
  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
- }
1568
+ const activeGroups = groups.filter((_, groupIndex) => (groupCounts[groupIndex] ?? 0) > 0).map(({ id }) => id);
1569
+ if (activeGroups.length > strongestGroups.length) strongestGroups = activeGroups;
911
1570
  }
912
- return { matched: strongest, requiredMatched: strongestRequired, qualifies };
913
- }
914
- function containsTerm(text, term) {
915
- const pattern = term.trim().split(/\s+/).map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\s_-]+");
916
- return new RegExp(`(^|[^a-z0-9])${pattern}(?=$|[^a-z0-9])`, "i").test(text);
1571
+ return { matchedGroups: strongestGroups, qualifies: strongestGroups.length >= minGroups };
917
1572
  }
918
- async function evaluateMaxBytes(context, check) {
919
- const paths = await matches(context, check.patterns);
920
- let total = 0;
921
- const inspected = [];
922
- for (const path of paths) {
923
- const size = await safeFileSize(context.metadata.root, path);
924
- if (size === null) continue;
925
- total += size;
926
- inspected.push(path);
927
- }
928
- const passed = inspected.length > 0 && total <= check.max_bytes;
1573
+ async function evaluateCiCommand(context, check) {
1574
+ const bindings = await repositoryCommandBindings(context, check.tools);
1575
+ const patterns = check.providers.flatMap(({ files: files2 }) => files2);
1576
+ const files = await readSearchableFiles(context, patterns, check.max_files_per_pattern, true);
1577
+ const providerPaths = await Promise.all(
1578
+ check.providers.map(async (provider) => ({
1579
+ id: provider.id,
1580
+ paths: new Set(await matches(context, provider.files))
1581
+ }))
1582
+ );
1583
+ const inspected = files.map(({ path, text }) => {
1584
+ const invocations = providerPaths.flatMap(
1585
+ ({ id, paths }) => paths.has(path) ? ciIntegrationInvocations(id, text) : []
1586
+ );
1587
+ const matchesByExecution = /* @__PURE__ */ new Map();
1588
+ for (const invocation of invocations) {
1589
+ const matches2 = matchesByExecution.get(invocation.executionGroup) ?? /* @__PURE__ */ new Set();
1590
+ for (const tool of check.tools) {
1591
+ if (invocationMatchesTool(invocation, tool, bindings, check.invocation_grammar)) {
1592
+ matches2.add(tool.id);
1593
+ }
1594
+ }
1595
+ matchesByExecution.set(invocation.executionGroup, matches2);
1596
+ }
1597
+ const matchedToolIds2 = new Set(
1598
+ [...matchesByExecution.values()].flatMap((identities) => [...identities])
1599
+ );
1600
+ return {
1601
+ path,
1602
+ matchedTools: check.tools.filter(({ id }) => matchedToolIds2.has(id)),
1603
+ strongestExecutionMatch: Math.max(
1604
+ 0,
1605
+ ...[...matchesByExecution.values()].map(({ size }) => size)
1606
+ )
1607
+ };
1608
+ });
1609
+ const matchedToolIds = new Set(
1610
+ inspected.flatMap(({ matchedTools }) => matchedTools.map(({ id }) => id))
1611
+ );
1612
+ const contributing = inspected.filter(({ matchedTools }) => matchedTools.length > 0);
1613
+ const strongestExecutionMatch = Math.max(
1614
+ 0,
1615
+ ...inspected.map(({ strongestExecutionMatch: strongestExecutionMatch2 }) => strongestExecutionMatch2)
1616
+ );
1617
+ const passed = check.tool_match_mode === "same-execution" ? strongestExecutionMatch >= check.min_tools : matchedToolIds.size >= check.min_tools;
1618
+ const matchSummary = check.tool_match_mode === "same-execution" ? `strongest execution-group command-class match ${strongestExecutionMatch}/${check.tools.length}` : `aggregate command-class match ${matchedToolIds.size}/${check.tools.length}`;
929
1619
  return result(
930
1620
  check.type,
931
1621
  check.scope,
932
1622
  passed ? "met" : "not_met",
933
- `${total} byte(s) across ${inspected.length} safe matching file(s); maximum ${check.max_bytes}`,
934
- inspected
1623
+ `${contributing.length} contributing CI configuration file(s) contain enabled integration-triggered recognized commands; ${matchSummary} across ${files.length} candidate file(s); threshold ${check.min_tools}`,
1624
+ contributing.map(({ path }) => path)
935
1625
  );
936
1626
  }
937
- function result(type, scope, status, summary, references2) {
938
- return { type, scope, status, summary, references: references2 };
939
- }
940
- async function evaluateCheck(context, check) {
941
- switch (check.type) {
942
- case "path_any":
943
- return evaluatePathAny(context, check);
944
- case "path_all":
945
- return evaluatePathAll(context, check);
946
- case "content_any":
947
- case "content_all":
948
- return evaluateLegacyContent(context, check);
949
- case "content_terms":
950
- return evaluateContentTerms(context, check);
951
- case "max_bytes":
952
- return evaluateMaxBytes(context, check);
953
- case "manual":
954
- return result("manual", check.scope, "unknown", check.prompt, []);
1627
+ async function repositoryCommandBindings(context, tools) {
1628
+ const commandPaths = [
1629
+ ...new Set(
1630
+ tools.flatMap(
1631
+ ({ commands }) => commands.flatMap(({ argument_groups, repository_executables }) => [
1632
+ ...repository_executables.map(normalizeCommandPath),
1633
+ ...argument_groups.flatMap(
1634
+ (arguments_) => arguments_.filter(isRepositoryCommandPath).map(normalizeCommandPath)
1635
+ )
1636
+ ])
1637
+ )
1638
+ )
1639
+ ];
1640
+ const commandSources = new Map(
1641
+ (await readSearchableFiles(context, commandPaths, void 0, true)).map(({ path, text }) => [
1642
+ normalizeCommandPath(path),
1643
+ text
1644
+ ])
1645
+ );
1646
+ const availableCommandPaths = new Set(commandSources.keys());
1647
+ const packageFile = (await readSearchableFiles(context, ["package.json"], void 0, true)).find(
1648
+ ({ path }) => path === "package.json"
1649
+ );
1650
+ if (!packageFile) return { availableCommandPaths, commandSources, packageScripts: /* @__PURE__ */ new Map() };
1651
+ try {
1652
+ const document = asRecord(JSON.parse(packageFile.text));
1653
+ const scripts = asRecord(document?.scripts);
1654
+ return {
1655
+ availableCommandPaths,
1656
+ commandSources,
1657
+ packageScripts: new Map(
1658
+ Object.entries(scripts ?? {}).filter((entry) => typeof entry[1] === "string").map(([name, command]) => [name, command])
1659
+ )
1660
+ };
1661
+ } catch {
1662
+ return { availableCommandPaths, commandSources, packageScripts: /* @__PURE__ */ new Map() };
955
1663
  }
956
1664
  }
957
- function activeAttestation(control, attestations, now) {
1665
+ function isRepositoryCommandPath(value) {
1666
+ return /(?:^|\/)scripts?\/|^\.{1,2}\/.+/i.test(value);
1667
+ }
1668
+ function normalizeCommandPath(value) {
1669
+ return value.replace(/^\.\//, "");
1670
+ }
1671
+ function ciIntegrationInvocations(provider, text) {
1672
+ try {
1673
+ const document = asRecord(parse2(text, { maxAliasCount: 50 }));
1674
+ if (!document) return [];
1675
+ if (provider === "github-actions") return githubIntegrationInvocations(document);
1676
+ if (provider === "gitlab-ci") return gitlabIntegrationInvocations(document);
1677
+ return azureIntegrationInvocations(document);
1678
+ } catch {
1679
+ return [];
1680
+ }
1681
+ }
1682
+ function githubIntegrationInvocations(document) {
1683
+ const events = githubIntegrationTriggers(document.on);
1684
+ if (events.size === 0) return [];
1685
+ const jobs = asRecord(document.jobs);
1686
+ if (!jobs) return [];
1687
+ const workflowDefaults = githubRunDefaults(document.defaults);
1688
+ if (!workflowDefaults.valid) return [];
1689
+ return Object.entries(jobs).flatMap(
1690
+ ([name, job]) => githubJobInvocations(job, events, workflowDefaults, `github:${name}`)
1691
+ );
1692
+ }
1693
+ function githubJobInvocations(value, parentEvents, workflowDefaults, executionGroup) {
1694
+ const job = asRecord(value);
1695
+ if (!job || isDisabledCiNode(job)) return [];
1696
+ const jobCondition = githubConditionEvents(job.if, parentEvents);
1697
+ if (!jobCondition.certain || jobCondition.events.size === 0) return [];
1698
+ const events = jobCondition.events;
1699
+ const steps = asArray(job.steps);
1700
+ if (steps.length === 0 || !hasGithubRunner(job["runs-on"])) return [];
1701
+ const jobDefaults = githubRunDefaults(job.defaults);
1702
+ if (!jobDefaults.valid) return [];
1703
+ const runDefaults = mergeGithubRunDefaults(workflowDefaults, jobDefaults);
1704
+ const checkoutEvents = /* @__PURE__ */ new Set();
1705
+ return steps.flatMap((stepValue) => {
1706
+ const step = asRecord(stepValue);
1707
+ if (!step) return [];
1708
+ const condition = githubConditionEvents(step.if, events);
1709
+ const checkout = githubCheckoutDisposition(step);
1710
+ if (checkout) {
1711
+ if (checkout === "preserve") return [];
1712
+ const affectedEvents = condition.certain ? condition.events : events;
1713
+ affectedEvents.forEach((event) => {
1714
+ if (condition.certain && !isDisabledCiNode(step) && checkout === "target") {
1715
+ checkoutEvents.add(event);
1716
+ } else checkoutEvents.delete(event);
1717
+ });
1718
+ return [];
1719
+ }
1720
+ if (!condition.certain || isDisabledCiNode(step)) return [];
1721
+ const stepEvents = condition.events;
1722
+ if (stepEvents.size === 0) return [];
1723
+ const executableEvents = new Set([...stepEvents].filter((event) => checkoutEvents.has(event)));
1724
+ if (executableEvents.size === 0) return [];
1725
+ return githubStepInvocations(
1726
+ step,
1727
+ runDefaults.workingDirectory,
1728
+ runDefaults.shell,
1729
+ job["runs-on"],
1730
+ executionGroup,
1731
+ executableEvents
1732
+ );
1733
+ });
1734
+ }
1735
+ function hasGithubRunner(value) {
1736
+ if (typeof value === "string") return value.trim().length > 0;
1737
+ return Array.isArray(value) && value.some((entry) => typeof entry === "string" && entry.length > 0);
1738
+ }
1739
+ function githubCheckoutDisposition(step) {
1740
+ if (typeof step.uses !== "string") return null;
1741
+ if (!/^actions\/checkout@[^@\s]+$/i.test(step.uses.trim())) return null;
1742
+ if (step.run !== void 0) return "replace";
1743
+ if (step.with === void 0) return "target";
1744
+ const inputs = asRecord(step.with);
1745
+ if (!inputs) return "replace";
1746
+ if (Object.hasOwn(inputs, "path")) {
1747
+ return githubCheckoutPathIsSeparate(inputs.path) ? "preserve" : "replace";
1748
+ }
1749
+ return ["filter", "path", "ref", "repository", "sparse-checkout"].some(
1750
+ (field) => Object.hasOwn(inputs, field)
1751
+ ) ? "replace" : "target";
1752
+ }
1753
+ function githubCheckoutPathIsSeparate(value) {
1754
+ if (typeof value !== "string") return false;
1755
+ const normalized = value.trim();
1756
+ if (normalized.length === 0 || githubWorkingDirectoryIsRoot(normalized) || normalized.includes("${{") || normalized.startsWith("/") || /^[a-z]:[\\/]/i.test(normalized)) {
1757
+ return false;
1758
+ }
1759
+ return !normalized.split(/[\\/]+/).includes("..");
1760
+ }
1761
+ function githubRunDefaults(value) {
1762
+ if (value === void 0) return { shell: void 0, valid: true, workingDirectory: void 0 };
1763
+ const defaults = asRecord(value);
1764
+ if (!defaults) return { shell: void 0, valid: false, workingDirectory: void 0 };
1765
+ if (defaults.run === void 0) {
1766
+ return { shell: void 0, valid: true, workingDirectory: void 0 };
1767
+ }
1768
+ const run = asRecord(defaults.run);
1769
+ return run ? { shell: run.shell, valid: true, workingDirectory: run["working-directory"] } : { shell: void 0, valid: false, workingDirectory: void 0 };
1770
+ }
1771
+ function mergeGithubRunDefaults(workflow, job) {
1772
+ return {
1773
+ valid: workflow.valid && job.valid,
1774
+ shell: job.shell ?? workflow.shell,
1775
+ workingDirectory: job.workingDirectory ?? workflow.workingDirectory
1776
+ };
1777
+ }
1778
+ function githubStepInvocations(value, defaultWorkingDirectory, defaultShell, runner, executionGroup, events) {
1779
+ const step = asRecord(value);
1780
+ if (!step || isDisabledCiNode(step)) return [];
1781
+ if (step.uses !== void 0 && step.run !== void 0) return [];
1782
+ const action = invocationFromField(step, "uses", "action", executionGroup);
1783
+ const shell = githubShellSemantics(step.shell ?? defaultShell, runner);
1784
+ const command = githubWorkingDirectoryIsRoot(step["working-directory"] ?? defaultWorkingDirectory) && shell.supported ? invocationFromField(step, "run", "command", executionGroup, shell.failFast) : null;
1785
+ const invocations = [action, command].filter(
1786
+ (invocation) => invocation !== null
1787
+ );
1788
+ return [...events].flatMap(
1789
+ (event) => invocations.map((invocation) => ({
1790
+ ...invocation,
1791
+ executionGroup: `${executionGroup}:event-${event}`
1792
+ }))
1793
+ );
1794
+ }
1795
+ function githubShellSemantics(value, runner) {
1796
+ if (value === void 0) {
1797
+ return githubRunnerUsesFailFastDefaultShell(runner) ? { failFast: true, supported: true } : { failFast: false, supported: false };
1798
+ }
1799
+ if (typeof value !== "string") return { failFast: false, supported: false };
1800
+ const normalized = value.trim().toLowerCase().replace(/\s+/g, " ");
1801
+ if (["bash", "sh"].includes(normalized)) return { failFast: true, supported: true };
1802
+ if (["bash {0}", "sh {0}"].includes(normalized)) {
1803
+ return { failFast: false, supported: true };
1804
+ }
1805
+ return { failFast: false, supported: false };
1806
+ }
1807
+ function githubRunnerUsesFailFastDefaultShell(value) {
1808
+ const labels = (Array.isArray(value) ? value : [value]).filter(
1809
+ (label) => typeof label === "string"
1810
+ );
1811
+ if (labels.length === 0 || labels.some((label) => label.includes("${{"))) return false;
1812
+ const normalized = labels.map((label) => label.trim().toLowerCase());
1813
+ if (normalized.some((label) => label.includes("windows"))) return false;
1814
+ return normalized.some(
1815
+ (label) => ["ubuntu", "linux", "macos"].some((operatingSystem) => label.includes(operatingSystem))
1816
+ );
1817
+ }
1818
+ function githubWorkingDirectoryIsRoot(value) {
1819
+ if (value === void 0) return true;
1820
+ if (typeof value !== "string") return false;
1821
+ const normalized = value.trim().toLowerCase().replace(/\s+/g, "");
1822
+ return [".", "./", "${{github.workspace}}", "$github_workspace", "${github_workspace}"].includes(
1823
+ normalized
1824
+ );
1825
+ }
1826
+ function invocationFromField(node, field, kind, executionGroup, failFast) {
1827
+ const value = node[field];
1828
+ return typeof value === "string" ? {
1829
+ kind,
1830
+ value,
1831
+ executionGroup,
1832
+ ...kind === "command" && failFast !== void 0 ? { failFast } : {}
1833
+ } : null;
1834
+ }
1835
+ function gitlabIntegrationInvocations(document) {
1836
+ if (document.include !== void 0) return [];
1837
+ if (document.before_script !== void 0) return [];
1838
+ if (!validGitlabWorkflowShape(document.workflow)) return [];
1839
+ const workflow = asRecord(document.workflow);
1840
+ const hasWorkflowRules = asArray(workflow?.rules).length > 0;
1841
+ const workflowAllowsMergeRequests = hasGitlabMergeRequestRule(workflow?.rules);
1842
+ if (hasWorkflowRules && !workflowAllowsMergeRequests) return [];
1843
+ if (hasRiskyGitlabDefaults(document.default)) return [];
1844
+ return Object.entries(document).flatMap(
1845
+ ([name, value]) => gitlabJobInvocations(name, value, workflowAllowsMergeRequests, document.variables)
1846
+ );
1847
+ }
1848
+ function validGitlabWorkflowShape(value) {
1849
+ if (value === void 0) return true;
1850
+ const workflow = asRecord(value);
1851
+ return workflow !== null && (workflow.rules === void 0 || Array.isArray(workflow.rules));
1852
+ }
1853
+ function gitlabJobInvocations(name, value, workflowAllowsMergeRequests, globalVariables) {
1854
+ if (name.startsWith(".") || gitlabReservedKeys.has(name)) return [];
1855
+ const job = asRecord(value);
1856
+ if (!job || isDisabledCiNode(job)) return [];
1857
+ if (job.rules !== void 0 && !Array.isArray(job.rules)) return [];
1858
+ if (job.before_script !== void 0 || job.extends !== void 0 || job.inherit !== void 0 || job.except !== void 0) {
1859
+ return [];
1860
+ }
1861
+ if (!isSupportedBlockingGitlabWhen(job.when)) return [];
1862
+ if (!gitlabRepositoryAvailable(globalVariables, job.variables)) return [];
1863
+ const hasJobTriggerRules = asArray(job.rules).length > 0 || job.only !== void 0;
1864
+ const jobAllowsMergeRequests = hasGitlabMergeRequestRule(job.rules) || hasUnconditionallyNamedTrigger(job.only, ["merge_requests"]);
1865
+ if (hasJobTriggerRules ? !jobAllowsMergeRequests : !workflowAllowsMergeRequests) return [];
1866
+ return stringValues(job.script).map((command) => ({
1867
+ executionGroup: `gitlab:${name}`,
1868
+ kind: "command",
1869
+ value: command
1870
+ }));
1871
+ }
1872
+ function isSupportedBlockingGitlabWhen(value) {
1873
+ return value === void 0 || typeof value === "string" && ["always", "on_success"].includes(value.toLowerCase());
1874
+ }
1875
+ function gitlabRepositoryAvailable(globalValue, jobValue) {
1876
+ const globalVariables = asRecord(globalValue);
1877
+ const jobVariables = asRecord(jobValue);
1878
+ const strategy = effectiveCiVariable(jobVariables, globalVariables, "GIT_STRATEGY");
1879
+ const checkout = effectiveCiVariable(jobVariables, globalVariables, "GIT_CHECKOUT");
1880
+ if (checkout === false || typeof checkout === "string" && checkout.toLowerCase() === "false") {
1881
+ return false;
1882
+ }
1883
+ return strategy === void 0 || typeof strategy === "string" && ["clone", "fetch"].includes(strategy.toLowerCase());
1884
+ }
1885
+ function effectiveCiVariable(primary, fallback, name) {
1886
+ const keys = [name, name.toLowerCase()];
1887
+ for (const source of [primary, fallback]) {
1888
+ if (!source) continue;
1889
+ for (const key of keys) {
1890
+ if (Object.hasOwn(source, key)) return source[key];
1891
+ }
1892
+ }
1893
+ return void 0;
1894
+ }
1895
+ function hasRiskyGitlabDefaults(value) {
1896
+ if (value === void 0) return false;
1897
+ const defaults = asRecord(value);
1898
+ if (!defaults) return true;
1899
+ if (Object.hasOwn(defaults, "allow_failure") && defaults.allow_failure !== false) return true;
1900
+ return ["before_script", "except", "only", "rules", "script", "when"].some(
1901
+ (key) => Object.hasOwn(defaults, key)
1902
+ );
1903
+ }
1904
+ function azureIntegrationInvocations(document) {
1905
+ if (!hasAzurePullRequestTrigger(document.pr)) return [];
1906
+ return collectAzureInvocations(document, "root", "azure:root");
1907
+ }
1908
+ function collectAzureInvocations(node, kind, executionGroup) {
1909
+ if (isDisabledCiNode(node) || azurePullRequestCondition(node.condition) !== "allow") return [];
1910
+ if (kind === "step") return azureStepInvocations(node, executionGroup);
1911
+ if (kind === "stage") return azureChildInvocations(node.jobs, "job", executionGroup);
1912
+ if (kind === "job") return azureStepsInvocations(node.steps, executionGroup);
1913
+ const rootCollections = [
1914
+ { kind: "stage", value: node.stages },
1915
+ { kind: "job", value: node.jobs },
1916
+ { kind: "step", value: node.steps }
1917
+ ].filter((collection2) => collection2.value !== void 0);
1918
+ if (rootCollections.length !== 1) return [];
1919
+ const collection = rootCollections[0];
1920
+ if (!collection) return [];
1921
+ return collection.kind === "step" ? azureStepsInvocations(collection.value, executionGroup) : azureChildInvocations(collection.value, collection.kind, executionGroup);
1922
+ }
1923
+ function azureChildInvocations(value, kind, parentGroup) {
1924
+ return asArray(value).flatMap((childValue, index) => {
1925
+ const child = asRecord(childValue);
1926
+ return child ? collectAzureInvocations(child, kind, `${parentGroup}:${kind}-${index}`) : [];
1927
+ });
1928
+ }
1929
+ function azureStepInvocations(node, executionGroup) {
1930
+ const commandFields = ["script", "bash", "pwsh", "powershell"].filter(
1931
+ (field) => typeof node[field] === "string"
1932
+ );
1933
+ if (commandFields.length !== 1 || !azureWorkingDirectoryIsRoot(node.workingDirectory)) return [];
1934
+ const commandField = commandFields[0] ?? "";
1935
+ const value = node[commandField];
1936
+ return [
1937
+ {
1938
+ executionGroup,
1939
+ kind: "command",
1940
+ value,
1941
+ failFast: azureScriptIsFailFast(value, commandField)
1942
+ }
1943
+ ];
1944
+ }
1945
+ function azureScriptIsFailFast(value, commandField) {
1946
+ if (["pwsh", "powershell"].includes(commandField)) {
1947
+ return value.split(/\r?\n/).some((line) => /^\s*\$erroractionpreference\s*=\s*['"]stop['"]\s*$/i.test(line));
1948
+ }
1949
+ return value.split(/\r?\n/).some((line) => /^\s*set\s+(?:-e(?:o\s+pipefail)?|-o\s+errexit)\s*$/i.test(line));
1950
+ }
1951
+ function azureWorkingDirectoryIsRoot(value) {
1952
+ if (value === void 0) return true;
1953
+ if (typeof value !== "string") return false;
1954
+ const normalized = value.trim().toLowerCase().replace(/\s+/g, "");
1955
+ return [
1956
+ ".",
1957
+ "./",
1958
+ "$(build.repository.localpath)",
1959
+ "$(build.sourcesdirectory)",
1960
+ "$(pipeline.workspace)/s",
1961
+ "$(system.defaultworkingdirectory)"
1962
+ ].includes(normalized);
1963
+ }
1964
+ function azureStepsInvocations(value, executionGroup) {
1965
+ const steps = asArray(value);
1966
+ let repositoryAvailable = !steps.some((stepValue) => {
1967
+ const step = asRecord(stepValue);
1968
+ return step !== null && Object.hasOwn(step, "checkout");
1969
+ });
1970
+ return steps.flatMap((stepValue) => {
1971
+ const step = asRecord(stepValue);
1972
+ if (!step) return [];
1973
+ const condition = azurePullRequestCondition(step.condition);
1974
+ if (Object.hasOwn(step, "checkout")) {
1975
+ if (condition === "unknown") repositoryAvailable = false;
1976
+ else if (condition === "allow") {
1977
+ repositoryAvailable = !isDisabledCiNode(step) && typeof step.checkout === "string" && step.checkout.toLowerCase() === "self";
1978
+ }
1979
+ return [];
1980
+ }
1981
+ if (condition !== "allow" || isDisabledCiNode(step)) return [];
1982
+ return repositoryAvailable ? azureStepInvocations(step, executionGroup) : [];
1983
+ });
1984
+ }
1985
+ function hasNamedTrigger(value, names) {
1986
+ if (typeof value === "string") return names.includes(value.toLowerCase());
1987
+ if (Array.isArray(value)) {
1988
+ return value.some((entry) => typeof entry === "string" && names.includes(entry.toLowerCase()));
1989
+ }
1990
+ const record = asRecord(value);
1991
+ if (!record) return false;
1992
+ if (names.some((name) => Object.hasOwn(record, name))) return true;
1993
+ return Object.hasOwn(record, "refs") && hasNamedTrigger(record.refs, names);
1994
+ }
1995
+ function hasUnconditionallyNamedTrigger(value, names) {
1996
+ if (typeof value === "string" || Array.isArray(value)) return hasNamedTrigger(value, names);
1997
+ const record = asRecord(value);
1998
+ if (!record || Object.keys(record).some((key) => key !== "refs")) return false;
1999
+ return hasNamedTrigger(record.refs, names);
2000
+ }
2001
+ function githubIntegrationTriggers(value) {
2002
+ return new Set(
2003
+ ["pull_request", "merge_group"].filter((name) => githubTriggerAllowsIntegration(value, name))
2004
+ );
2005
+ }
2006
+ function githubTriggerAllowsIntegration(value, name) {
2007
+ if (typeof value === "string" || Array.isArray(value)) return hasNamedTrigger(value, [name]);
2008
+ const triggers = asRecord(value);
2009
+ if (!triggers || !Object.hasOwn(triggers, name)) return false;
2010
+ const configuration = triggers[name];
2011
+ if (configuration === null || configuration === void 0) return true;
2012
+ const trigger = asRecord(configuration);
2013
+ if (!trigger) return false;
2014
+ if (["paths", "paths-ignore"].some((key) => Object.hasOwn(trigger, key))) return false;
2015
+ if (trigger.types === void 0) return true;
2016
+ const types = new Set(stringValues(trigger.types).map((type) => type.toLowerCase()));
2017
+ const requiredTypes = name === "pull_request" ? ["opened", "reopened", "synchronize"] : ["checks_requested"];
2018
+ return requiredTypes.every((type) => types.has(type));
2019
+ }
2020
+ function githubConditionEvents(value, parentEvents) {
2021
+ if (value === void 0 || value === true) {
2022
+ return { certain: true, events: new Set(parentEvents) };
2023
+ }
2024
+ if (value === false) return { certain: true, events: /* @__PURE__ */ new Set() };
2025
+ if (typeof value !== "string") return { certain: false, events: /* @__PURE__ */ new Set() };
2026
+ return githubStringConditionEvents(value, parentEvents);
2027
+ }
2028
+ function githubStringConditionEvents(value, parentEvents) {
2029
+ const condition = value.toLowerCase();
2030
+ const normalized = condition.replace(/[\s${}]/g, "");
2031
+ if (!condition.includes("github.event_name")) {
2032
+ return githubStatusConditionEvents(normalized, parentEvents);
2033
+ }
2034
+ const equals = [...condition.matchAll(/github\.event_name\s*==\s*['"]([^'"]+)['"]/g)].map(
2035
+ (match) => match[1] ?? ""
2036
+ );
2037
+ const excludes = [...condition.matchAll(/github\.event_name\s*!=\s*['"]([^'"]+)['"]/g)].map(
2038
+ (match) => match[1] ?? ""
2039
+ );
2040
+ if (equals.length === 0 && excludes.length === 0) {
2041
+ return { certain: false, events: /* @__PURE__ */ new Set() };
2042
+ }
2043
+ const unsupported = condition.replace(/github\.event_name\s*(?:==|!=)\s*['"][^'"]+['"]/g, "").replaceAll("!cancelled()", "").replace(/\b(?:always|success)\(\)/g, "").replace(/[\s${}()&|]/g, "");
2044
+ if (unsupported.length > 0) return { certain: false, events: /* @__PURE__ */ new Set() };
2045
+ const hasConjunction = condition.includes("&&");
2046
+ const hasDisjunction = condition.includes("||");
2047
+ if (hasConjunction && hasDisjunction) return { certain: false, events: /* @__PURE__ */ new Set() };
2048
+ if (new Set(equals).size > 1 && !hasDisjunction) {
2049
+ return { certain: true, events: /* @__PURE__ */ new Set() };
2050
+ }
2051
+ const candidates = equals.length > 0 ? equals.filter((event) => parentEvents.has(event)) : [...parentEvents];
2052
+ return {
2053
+ certain: true,
2054
+ events: new Set(candidates.filter((event) => !excludes.includes(event)))
2055
+ };
2056
+ }
2057
+ function githubStatusConditionEvents(normalized, parentEvents) {
2058
+ if (["true", "always()", "success()", "!cancelled()"].includes(normalized)) {
2059
+ return { certain: true, events: new Set(parentEvents) };
2060
+ }
2061
+ if (["false", "never"].includes(normalized)) return { certain: true, events: /* @__PURE__ */ new Set() };
2062
+ return { certain: false, events: /* @__PURE__ */ new Set() };
2063
+ }
2064
+ function hasGitlabMergeRequestRule(value) {
2065
+ for (const ruleValue of asArray(value)) {
2066
+ const disposition = gitlabMergeRequestRuleDisposition(ruleValue);
2067
+ if (disposition === "unsupported") return false;
2068
+ if (disposition === "skip") continue;
2069
+ return disposition === "allow";
2070
+ }
2071
+ return false;
2072
+ }
2073
+ function gitlabMergeRequestRuleDisposition(value) {
2074
+ const rule = asRecord(value);
2075
+ if (!rule || Object.keys(rule).some((key) => !supportedGitlabRuleKeys.has(key))) {
2076
+ return "unsupported";
2077
+ }
2078
+ const applies = gitlabRuleAppliesToMergeRequest(rule.if);
2079
+ if (applies === null) return "unsupported";
2080
+ if (!applies) return "skip";
2081
+ if (!isSupportedBlockingGitlabRule(rule)) return "deny";
2082
+ return "allow";
2083
+ }
2084
+ function gitlabRuleAppliesToMergeRequest(value) {
2085
+ if (value === void 0) return true;
2086
+ if (typeof value !== "string") return null;
2087
+ const comparison = /^\s*\$?ci_pipeline_source\s*(==|!=)\s*['"]([^'"]+)['"]\s*$/i.exec(value);
2088
+ if (!comparison) return null;
2089
+ const event = comparison[2]?.toLowerCase();
2090
+ return comparison[1] === "==" ? event === "merge_request_event" : event !== "merge_request_event";
2091
+ }
2092
+ function isSupportedBlockingGitlabRule(rule) {
2093
+ if (isDisabledCiNode(rule)) return false;
2094
+ if (rule.when === void 0) return true;
2095
+ return typeof rule.when === "string" && ["always", "on_success"].includes(rule.when.toLowerCase());
2096
+ }
2097
+ function hasAzurePullRequestTrigger(value) {
2098
+ if (value === false || value === null || value === void 0) return false;
2099
+ if (typeof value === "string") return !["none", "false"].includes(value.toLowerCase());
2100
+ if (Array.isArray(value)) return value.length > 0;
2101
+ const trigger = asRecord(value);
2102
+ if (!trigger) return false;
2103
+ if (Object.hasOwn(trigger, "paths")) return false;
2104
+ const branches = asRecord(trigger.branches);
2105
+ if (!branches) return true;
2106
+ const include = stringValues(branches.include).map((branch) => branch.toLowerCase());
2107
+ const exclude = stringValues(branches.exclude).map((branch) => branch.toLowerCase());
2108
+ if (exclude.includes("*")) return false;
2109
+ if (include.length > 0) return include.some((branch) => !["none", "false"].includes(branch));
2110
+ return true;
2111
+ }
2112
+ function azurePullRequestCondition(value) {
2113
+ if (value === void 0 || value === true) return "allow";
2114
+ if (value === false) return "deny";
2115
+ if (typeof value !== "string") return "unknown";
2116
+ const condition = value.toLowerCase().replace(/\s+/g, "");
2117
+ if (["always()", "succeeded()", "succeededorfailed()"].includes(condition)) return "allow";
2118
+ const direct = azureReasonComparison(condition);
2119
+ if (direct !== null) return direct ? "allow" : "deny";
2120
+ const conjunction = /^and\((?:always|succeeded|succeededorfailed)\(\),(.+)\)$/.exec(condition);
2121
+ if (!conjunction) return "unknown";
2122
+ const nested = azureReasonComparison(conjunction[1] ?? "");
2123
+ if (nested === null) return "unknown";
2124
+ return nested ? "allow" : "deny";
2125
+ }
2126
+ function azureReasonComparison(condition) {
2127
+ const comparison = /^(eq|ne)\(variables\[['"]build\.reason['"]\],['"]([^'"]+)['"]\)$/.exec(
2128
+ condition
2129
+ );
2130
+ if (!comparison) return null;
2131
+ const equalsPullRequest = comparison[2] === "pullrequest";
2132
+ return comparison[1] === "eq" ? equalsPullRequest : !equalsPullRequest;
2133
+ }
2134
+ function isDisabledCiNode(node) {
2135
+ if (node.enabled === false || configuredNonBlocking(node, ["allow_failure"]) || configuredNonBlocking(node, ["continue-on-error", "continueonerror", "continueOnError"])) {
2136
+ return true;
2137
+ }
2138
+ if (typeof node.when === "string" && ["never", "manual"].includes(node.when.toLowerCase())) {
2139
+ return true;
2140
+ }
2141
+ return [node.if, node.condition].some((condition) => {
2142
+ if (condition === false) return true;
2143
+ if (typeof condition !== "string") return false;
2144
+ const normalized = condition.toLowerCase().replace(/[\s${}]/g, "");
2145
+ return normalized === "false" || normalized === "0" || normalized === "never";
2146
+ });
2147
+ }
2148
+ function configuredNonBlocking(node, fields) {
2149
+ return fields.some((field) => Object.hasOwn(node, field) && node[field] !== false);
2150
+ }
2151
+ function invocationMatchesTool(invocation, tool, bindings, grammar) {
2152
+ if (invocation.kind === "action") {
2153
+ const action = /^([^@\s]+)@([^@\s]+)$/.exec(invocation.value.trim());
2154
+ if (!action) return false;
2155
+ const identity = action[1]?.toLowerCase() ?? "";
2156
+ return tool.actions.some((action2) => action2.toLowerCase() === identity);
2157
+ }
2158
+ return commandTextMatchesTool(
2159
+ invocation.value,
2160
+ tool,
2161
+ bindings,
2162
+ /* @__PURE__ */ new Set(),
2163
+ grammar,
2164
+ invocation.failFast ?? false
2165
+ );
2166
+ }
2167
+ function commandTextMatchesTool(value, tool, bindings, visitedScripts, grammar, failFast = false) {
2168
+ return shellStatements(value, tool.requires_final_exit_status, failFast).some((statement) => {
2169
+ if (statement.includes("||") || /(^|[^|])\|(?!\|)/.test(statement) || /(^|[^&])&(?!&)/.test(statement)) {
2170
+ return false;
2171
+ }
2172
+ const commands = statement.split("&&").map((command) => command.trim());
2173
+ for (const rawCommand of commands) {
2174
+ const command = rawCommand.replace(/^(?:[a-z_][a-z0-9_]*=[^\s]+\s+)*/i, "").trim();
2175
+ if (/^(?:false|exit)(?:\s|$)/i.test(command)) return false;
2176
+ if (/^(?:cd|chdir|pushd|popd|set-location)\b/i.test(command)) return false;
2177
+ if (missingPackageTaskPreventsContinuation(command, bindings, grammar)) return false;
2178
+ if (commandMatchesTool(command, tool, bindings, visitedScripts, grammar)) return true;
2179
+ }
2180
+ return false;
2181
+ });
2182
+ }
2183
+ function missingPackageTaskPreventsContinuation(command, bindings, grammar) {
2184
+ const tokens = command.split(/\s+/).filter(Boolean);
2185
+ return tokens.some((token, executableIndex) => {
2186
+ if (!["bun", "npm", "pnpm", "yarn"].includes(executableIdentity(token))) return false;
2187
+ if (!isSupportedExecutablePosition(tokens, executableIndex, grammar)) return false;
2188
+ const invocation = packageScriptInvocation(tokens.slice(executableIndex));
2189
+ return invocation !== null && !bindings.packageScripts.has(invocation.task);
2190
+ });
2191
+ }
2192
+ function commandMatchesTool(command, tool, bindings, visitedScripts, grammar) {
2193
+ if (!command || /^(?:echo|printf|write-host|write-output|cat|grep|rg|sed|awk)\b/i.test(command)) {
2194
+ return false;
2195
+ }
2196
+ const tokens = command.split(/\s+/).filter(Boolean);
2197
+ if (tokens.some(isNonExecutingCommandArgument)) return false;
2198
+ const packageMatch = packageScriptMatchesTool(tokens, tool, bindings, visitedScripts, grammar);
2199
+ if (packageMatch !== null) return packageMatch;
2200
+ return tokens.some(
2201
+ (token, executableIndex) => (tool.standalone_executables.some(
2202
+ (executable) => unqualifiedExecutableTokenMatches(token, executable)
2203
+ ) || tool.commands.some(
2204
+ (signature) => commandExecutableTokenMatches(token, signature, bindings)
2205
+ )) && executablePositionMatchesTool(
2206
+ tokens,
2207
+ executableIndex,
2208
+ tool,
2209
+ bindings,
2210
+ visitedScripts,
2211
+ grammar
2212
+ )
2213
+ );
2214
+ }
2215
+ function commandExecutableTokenMatches(token, signature, bindings) {
2216
+ if (!/[\\/]/.test(token) && !token.startsWith(".")) {
2217
+ return signature.executables.some(
2218
+ (executable) => unqualifiedExecutableTokenMatches(token, executable)
2219
+ );
2220
+ }
2221
+ const path = normalizeCommandPath(token);
2222
+ if (!bindings.availableCommandPaths.has(path)) return false;
2223
+ return signature.repository_executables.some(
2224
+ (candidate) => normalizeCommandPath(candidate) === path
2225
+ );
2226
+ }
2227
+ function unqualifiedExecutableTokenMatches(token, executable) {
2228
+ if (/[\\/]/.test(token) || token.startsWith(".")) return false;
2229
+ return executableIdentity(token) === executable.toLowerCase();
2230
+ }
2231
+ function executablePositionMatchesTool(tokens, executableIndex, tool, bindings, visitedScripts, grammar) {
2232
+ if (!isSupportedExecutablePosition(tokens, executableIndex, grammar)) return false;
2233
+ const wrappedPackageMatch = packageScriptMatchesTool(
2234
+ tokens.slice(executableIndex),
2235
+ tool,
2236
+ bindings,
2237
+ visitedScripts,
2238
+ grammar
2239
+ );
2240
+ if (wrappedPackageMatch !== null) return wrappedPackageMatch;
2241
+ const arguments_ = tokens.slice(executableIndex + 1).map(normalizeCommandArgument);
2242
+ const executableToken = tokens[executableIndex] ?? "";
2243
+ if (hasProhibitedArguments(tool, arguments_)) return false;
2244
+ if (tool.standalone_executables.some(
2245
+ (standalone) => unqualifiedExecutableTokenMatches(executableToken, standalone)
2246
+ ))
2247
+ return true;
2248
+ return tool.commands.filter((signature) => commandExecutableTokenMatches(executableToken, signature, bindings)).some((signature) => commandSignatureMatches(signature, arguments_, bindings));
2249
+ }
2250
+ function isNonExecutingCommandArgument(value) {
2251
+ const normalized = value.toLowerCase();
2252
+ return nonExecutingCommandArguments.has(normalized) || normalized.startsWith("--help=") || normalized.startsWith("--version=");
2253
+ }
2254
+ function packageScriptMatchesTool(tokens, tool, bindings, visitedScripts, grammar) {
2255
+ const managerToken = tokens[0] ?? "";
2256
+ if (/[\\/]/.test(managerToken) || managerToken.startsWith(".")) return null;
2257
+ const manager = executableIdentity(managerToken);
2258
+ if (!["bun", "npm", "pnpm", "yarn"].includes(manager)) return null;
2259
+ if (tokens.slice(1).some(isPackageContextOption)) return false;
2260
+ const invocation = packageScriptInvocation(tokens);
2261
+ if (!invocation) {
2262
+ if (manager !== "npm" || isPackageExecutionWrapper(tokens)) return null;
2263
+ const arguments_ = tokens.slice(1);
2264
+ const subcommand = arguments_[skipPackageOptions(arguments_, 0)]?.toLowerCase() ?? "";
2265
+ return packageManagerDirectToolCommands.get(manager)?.has(subcommand) ? null : false;
2266
+ }
2267
+ if (invocation.manager === "bun" && invocation.task === "test") return null;
2268
+ if (invocation.hasForwardedArguments) return false;
2269
+ if (visitedScripts.has(invocation.task) || visitedScripts.size >= 4) return false;
2270
+ const script = bindings.packageScripts.get(invocation.task);
2271
+ if (!script) return false;
2272
+ return commandTextMatchesTool(
2273
+ script,
2274
+ tool,
2275
+ bindings,
2276
+ new Set(visitedScripts).add(invocation.task),
2277
+ grammar
2278
+ );
2279
+ }
2280
+ function isPackageExecutionWrapper(tokens) {
2281
+ const arguments_ = tokens.slice(1);
2282
+ const index = skipPackageOptions(arguments_, 0);
2283
+ return ["dlx", "exec", "x"].includes(arguments_[index]?.toLowerCase() ?? "");
2284
+ }
2285
+ function isPackageContextOption(value) {
2286
+ return packageContextOptions.has(value.toLowerCase().split("=")[0] ?? "");
2287
+ }
2288
+ function isSupportedExecutablePosition(tokens, executableIndex, grammar) {
2289
+ if (executableIndex === 0) return true;
2290
+ const wrapper = tokens[0] ?? "";
2291
+ const definition = grammar.wrappers.find(
2292
+ ({ executable_patterns }) => executable_patterns.some((pattern) => new RegExp(pattern, "iu").test(wrapper))
2293
+ );
2294
+ if (!definition) return false;
2295
+ const prefixIndex = wrapperCommandPrefixIndex(tokens, grammar.wrapper_options_with_values);
2296
+ return definition.command_prefixes.some(
2297
+ (prefix) => executableIndex === prefixIndex + prefix.length && prefix.every(
2298
+ (argument, offset) => normalizeCommandArgument(tokens[prefixIndex + offset] ?? "") === argument.toLowerCase()
2299
+ )
2300
+ );
2301
+ }
2302
+ function wrapperCommandPrefixIndex(tokens, optionsWithValues) {
2303
+ const valueOptions = new Set(optionsWithValues.map((option) => option.toLowerCase()));
2304
+ let index = 1;
2305
+ while (index < tokens.length) {
2306
+ const argument = tokens[index]?.toLowerCase() ?? "";
2307
+ if (argument === "--") return index + 1;
2308
+ if (!argument.startsWith("-")) return index;
2309
+ const option = argument.split("=")[0] ?? "";
2310
+ index += 1;
2311
+ if (!argument.includes("=") && valueOptions.has(option)) index += 1;
2312
+ }
2313
+ return index;
2314
+ }
2315
+ function commandSignatureMatches(signature, arguments_, bindings) {
2316
+ if (hasProhibitedArguments(signature, arguments_)) return false;
2317
+ if (signature.max_arguments !== void 0 && arguments_.length > signature.max_arguments) {
2318
+ return false;
2319
+ }
2320
+ if (signature.required_argument_prefixes.length > 0 && !signature.required_argument_prefixes.some(
2321
+ (prefix) => startsWithArgumentSequence(
2322
+ arguments_,
2323
+ prefix.map((argument) => argument.toLowerCase())
2324
+ )
2325
+ )) {
2326
+ return false;
2327
+ }
2328
+ const argumentsMatch = signature.argument_groups.every(
2329
+ (group) => group.some((argument) => commandArgumentMatches(argument, arguments_, bindings))
2330
+ );
2331
+ return argumentsMatch && commandSourceMatches(signature, arguments_, bindings);
2332
+ }
2333
+ function hasProhibitedArguments(configuration, arguments_) {
2334
+ return configuration.prohibited_arguments.some((argument) => {
2335
+ const prohibited = argument.toLowerCase();
2336
+ return arguments_.some(
2337
+ (actual) => actual === prohibited || actual.startsWith(`${prohibited}=`)
2338
+ );
2339
+ }) || configuration.prohibited_argument_sequences.some(
2340
+ (sequence) => containsArgumentSequence(
2341
+ arguments_,
2342
+ sequence.map((argument) => argument.toLowerCase())
2343
+ )
2344
+ );
2345
+ }
2346
+ function commandSourceMatches(signature, arguments_, bindings) {
2347
+ if (signature.source_content_groups.length === 0 && signature.source_pattern_groups.length === 0) {
2348
+ return true;
2349
+ }
2350
+ const sourcePaths = signature.argument_groups.flat().filter(isRepositoryCommandPath).map(normalizeCommandPath).filter((path) => arguments_.some((argument) => normalizeCommandPath(argument) === path));
2351
+ return sourcePaths.some((path) => {
2352
+ const source = bindings.commandSources.get(path);
2353
+ if (source === void 0) return false;
2354
+ const uncommented = stripSourceComments(source, path);
2355
+ if (hasUnresolvedJavascriptCallable(uncommented, path)) return false;
2356
+ if (hasObviouslyUnreachableBranch(uncommented, path)) return false;
2357
+ const executableSource = executableValidationSource(uncommented, path);
2358
+ return sourceGroupsAreCoLocated(
2359
+ executableSource,
2360
+ signature.source_content_groups,
2361
+ signature.source_max_span_lines
2362
+ ) && sourcePatternGroupsAreCoLocated(
2363
+ executableSource,
2364
+ signature.source_pattern_groups,
2365
+ signature.source_max_span_lines
2366
+ );
2367
+ });
2368
+ }
2369
+ function executableValidationSource(source, path) {
2370
+ const lines = source.split(/\r?\n/);
2371
+ const blocks = sourceFunctionBlocks(lines, path);
2372
+ const reachable = reachableSourceFunctionNames(lines, blocks, path);
2373
+ return lines.filter((_, index) => sourceLineIsExecutable(index, blocks, reachable)).join("\n");
2374
+ }
2375
+ function sourceLineIsExecutable(index, blocks, reachable) {
2376
+ const containingBlocks = blocks.filter((block) => index >= block.start && index <= block.end);
2377
+ return containingBlocks.every((block) => reachable.has(block.name));
2378
+ }
2379
+ function reachableSourceFunctionNames(lines, blocks, path) {
2380
+ const topLevelLines = topLevelSourceLines(lines, blocks);
2381
+ const reachable = new Set(
2382
+ blocks.filter((block) => sourceFunctionIsCalled(topLevelLines, block.name, path)).map(({ name }) => name)
2383
+ );
2384
+ const pending = [...reachable];
2385
+ while (pending.length > 0) {
2386
+ const callerName = pending.shift();
2387
+ const caller = blocks.find(({ name }) => name === callerName);
2388
+ if (!caller) continue;
2389
+ const body = sourceFunctionExecutionLines(lines, caller, blocks);
2390
+ for (const candidate of blocks) {
2391
+ if (reachable.has(candidate.name)) continue;
2392
+ if (!sourceFunctionIsCalled(body, candidate.name, path)) continue;
2393
+ reachable.add(candidate.name);
2394
+ pending.push(candidate.name);
2395
+ }
2396
+ }
2397
+ return reachable;
2398
+ }
2399
+ function sourceFunctionExecutionLines(lines, caller, blocks) {
2400
+ return lines.filter(
2401
+ (_, index) => index >= caller.start && index <= caller.end && blocks.every(
2402
+ (block) => block === caller || index < block.start || index > block.end || block.start < caller.start || block.end > caller.end
2403
+ )
2404
+ );
2405
+ }
2406
+ function topLevelSourceLines(lines, blocks) {
2407
+ return lines.filter(
2408
+ (_, index) => blocks.every((block) => index < block.start || index > block.end)
2409
+ );
2410
+ }
2411
+ function sourceFunctionBlocks(lines, path) {
2412
+ if (/\.py$/i.test(path)) return pythonFunctionBlocks(lines);
2413
+ return braceDelimitedFunctionBlocks(lines, /\.[cm]?[jt]sx?$/i.test(path));
2414
+ }
2415
+ function hasUnresolvedJavascriptCallable(source, path) {
2416
+ if (!/\.[cm]?[jt]sx?$/i.test(path)) return false;
2417
+ const method = String.raw`(?:^|[;{}])\s*(?:(?:abstract|async|get|override|private|protected|public|set|static)\s+)*(?!(?:catch|for|if|switch|while|with)\b)#?[a-z_$][a-z0-9_$]*\s*\([^)]*\)\s*\{`;
2418
+ const propertyArrow = String.raw`(?:^|[,;{}])\s*[a-z_$][a-z0-9_$]*\s*:\s*(?:async\s*)?(?:\([^)]*\)|[a-z_$][a-z0-9_$]*)\s*=>`;
2419
+ return source.split(/\r?\n/).some(
2420
+ (line) => executableSourcePatternMatches(line, method) || executableSourcePatternMatches(line, propertyArrow)
2421
+ );
2422
+ }
2423
+ function braceDelimitedFunctionBlocks(lines, javascript) {
2424
+ const blocks = [];
2425
+ for (let start = 0; start < lines.length; start += 1) {
2426
+ const name = sourceFunctionName(lines[start] ?? "", javascript);
2427
+ if (!name) continue;
2428
+ let depth = unquotedBraceDelta(lines[start] ?? "");
2429
+ if (depth <= 0) {
2430
+ blocks.push({ end: start, name, start });
2431
+ continue;
2432
+ }
2433
+ for (let end = start + 1; end < lines.length; end += 1) {
2434
+ depth += unquotedBraceDelta(lines[end] ?? "");
2435
+ if (depth > 0) continue;
2436
+ blocks.push({ end, name, start });
2437
+ break;
2438
+ }
2439
+ }
2440
+ return blocks.filter(({ name }) => name.length > 0);
2441
+ }
2442
+ function sourceFunctionName(line, javascript) {
2443
+ if (!javascript) return line.includes("{") ? shellFunctionName(line) : null;
2444
+ const declaration = /\bfunction\s+([a-z_$][a-z0-9_$]*)\s*\(/i.exec(line);
2445
+ if (declaration && line.includes("{")) return declaration[1] ?? null;
2446
+ const assignment = /\b(?:const|let|var)\s+([a-z_$][a-z0-9_$]*)\s*=/i.exec(line);
2447
+ if (!assignment) return null;
2448
+ const remainder = line.slice(assignment.index + assignment[0].length);
2449
+ return remainder.includes("=>") ? assignment[1] ?? null : null;
2450
+ }
2451
+ function shellFunctionName(line) {
2452
+ let declaration = line.trim();
2453
+ if (declaration.startsWith("function ")) declaration = declaration.slice("function ".length);
2454
+ const parentheses = declaration.indexOf("()");
2455
+ if (parentheses < 1 || !declaration.slice(parentheses + 2).trimStart().startsWith("{")) {
2456
+ return null;
2457
+ }
2458
+ const name = declaration.slice(0, parentheses).trim();
2459
+ return /^[a-z_][a-z0-9_]*$/i.test(name) ? name : null;
2460
+ }
2461
+ function unquotedBraceDelta(line) {
2462
+ const quoted = sourceQuotedIndices(line);
2463
+ let delta = 0;
2464
+ for (let index = 0; index < line.length; index += 1) {
2465
+ if (quoted[index] === 1) continue;
2466
+ if (line[index] === "{") delta += 1;
2467
+ else if (line[index] === "}") delta -= 1;
2468
+ }
2469
+ return delta;
2470
+ }
2471
+ function pythonFunctionBlocks(lines) {
2472
+ const blocks = [];
2473
+ for (let start = 0; start < lines.length; start += 1) {
2474
+ const declaration = /^(\s*)def\s+([a-z_]\w*)\s*\(/i.exec(lines[start] ?? "");
2475
+ if (!declaration) continue;
2476
+ const indentation = (declaration[1] ?? "").length;
2477
+ let end = start;
2478
+ while (end + 1 < lines.length) {
2479
+ const next = lines[end + 1] ?? "";
2480
+ if (next.trim().length > 0 && leadingWhitespace(next) <= indentation) break;
2481
+ end += 1;
2482
+ }
2483
+ blocks.push({ end, name: declaration[2] ?? "", start });
2484
+ }
2485
+ return blocks.filter(({ name }) => name.length > 0);
2486
+ }
2487
+ function leadingWhitespace(value) {
2488
+ return /^\s*/.exec(value)?.[0].length ?? 0;
2489
+ }
2490
+ function sourceFunctionIsCalled(lines, functionName, path) {
2491
+ const name = functionName.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
2492
+ const callPattern = /\.py$|\.[cm]?[jt]sx?$/i.test(path) ? String.raw`(?:^|[^\w$])${name}\s*\(` : String.raw`^\s*${name}(?:\s|$)`;
2493
+ return lines.some((line) => executableSourcePatternMatches(line, callPattern));
2494
+ }
2495
+ function hasObviouslyUnreachableBranch(source, path) {
2496
+ let pattern = String.raw`^\s*if\s+(?:false|\[\s+(?:false|0)\s+\])\s*;?\s*then\b`;
2497
+ if (/\.[cm]?[jt]sx?$/i.test(path)) pattern = String.raw`\bif\s*\(\s*(?:false|0)\s*\)`;
2498
+ else if (/\.py$/i.test(path)) pattern = String.raw`^\s*if\s+(?:false|0)\s*:`;
2499
+ return source.split(/\r?\n/).some((line) => executableSourcePatternMatches(line, pattern));
2500
+ }
2501
+ function stripSourceComments(source, path) {
2502
+ return /\.[cm]?[jt]sx?$/i.test(path) ? stripCStyleComments(source) : stripHashComments(source);
2503
+ }
2504
+ function stripCStyleComments(source) {
2505
+ let output = "";
2506
+ let quote = "";
2507
+ let lineComment = false;
2508
+ let blockComment = false;
2509
+ let escaped = false;
2510
+ for (let index = 0; index < source.length; index += 1) {
2511
+ const character = source[index] ?? "";
2512
+ const next = source[index + 1] ?? "";
2513
+ if (lineComment) {
2514
+ const update2 = cLineCommentUpdate(character);
2515
+ lineComment = update2.active;
2516
+ output += update2.output;
2517
+ continue;
2518
+ }
2519
+ if (blockComment) {
2520
+ const update2 = cBlockCommentUpdate(character, next);
2521
+ blockComment = update2.active;
2522
+ output += update2.output;
2523
+ index += update2.advance;
2524
+ continue;
2525
+ }
2526
+ if (quote) {
2527
+ const update2 = quotedSourceUpdate(character, quote, escaped);
2528
+ output += quote === "`" && character !== "\n" && update2.quote !== "" ? " " : character;
2529
+ quote = update2.quote;
2530
+ escaped = update2.escaped;
2531
+ continue;
2532
+ }
2533
+ const update = cCodeUpdate(character, next);
2534
+ lineComment = update.lineComment;
2535
+ blockComment = update.blockComment;
2536
+ quote = update.quote;
2537
+ output += update.output;
2538
+ index += update.advance;
2539
+ }
2540
+ return output;
2541
+ }
2542
+ function cCodeUpdate(character, next) {
2543
+ if (character === "/" && next === "/") {
2544
+ return { advance: 1, blockComment: false, lineComment: true, output: "", quote: "" };
2545
+ }
2546
+ if (character === "/" && next === "*") {
2547
+ return { advance: 1, blockComment: true, lineComment: false, output: "", quote: "" };
2548
+ }
2549
+ return {
2550
+ advance: 0,
2551
+ blockComment: false,
2552
+ lineComment: false,
2553
+ output: character,
2554
+ quote: ['"', "'", "`"].includes(character) ? character : ""
2555
+ };
2556
+ }
2557
+ function cLineCommentUpdate(character) {
2558
+ return character === "\n" ? { active: false, advance: 0, output: character } : { active: true, advance: 0, output: "" };
2559
+ }
2560
+ function cBlockCommentUpdate(character, next) {
2561
+ if (character === "*" && next === "/") return { active: false, advance: 1, output: "" };
2562
+ return { active: true, advance: 0, output: character === "\n" ? character : "" };
2563
+ }
2564
+ function quotedSourceUpdate(character, quote, escaped) {
2565
+ if (escaped) return { escaped: false, quote };
2566
+ if (character === "\\") return { escaped: true, quote };
2567
+ return { escaped: false, quote: character === quote ? "" : quote };
2568
+ }
2569
+ function stripHashComments(source) {
2570
+ return source.split(/\r?\n/).map(stripHashComment).join("\n");
2571
+ }
2572
+ function stripHashComment(line) {
2573
+ let quote = "";
2574
+ let escaped = false;
2575
+ for (let index = 0; index < line.length; index += 1) {
2576
+ const character = line[index] ?? "";
2577
+ if (quote) {
2578
+ if (escaped) escaped = false;
2579
+ else if (character === "\\") escaped = true;
2580
+ else if (character === quote) quote = "";
2581
+ continue;
2582
+ }
2583
+ if (['"', "'", "`"].includes(character)) quote = character;
2584
+ else if (character === "#") return line.slice(0, index);
2585
+ }
2586
+ return line;
2587
+ }
2588
+ function sourceGroupsAreCoLocated(source, groups, maxSpanLines) {
2589
+ const counts = groups.map(() => 0);
2590
+ const lines = source.split(/\r?\n/);
2591
+ const update = (line, direction) => {
2592
+ const normalizedLine = line.toLowerCase();
2593
+ groups.forEach((terms, index) => {
2594
+ if (terms.some((term) => normalizedLine.includes(term.toLowerCase()))) {
2595
+ counts[index] = (counts[index] ?? 0) + direction;
2596
+ }
2597
+ });
2598
+ };
2599
+ for (let index = 0; index < lines.length; index += 1) {
2600
+ update(lines[index] ?? "", 1);
2601
+ if (index >= maxSpanLines) update(lines[index - maxSpanLines] ?? "", -1);
2602
+ if (counts.every((count) => count > 0)) return true;
2603
+ }
2604
+ return false;
2605
+ }
2606
+ function sourcePatternGroupsAreCoLocated(source, groups, maxSpanLines) {
2607
+ if (groups.length === 0) return true;
2608
+ const counts = groups.map(() => 0);
2609
+ const lines = source.split(/\r?\n/);
2610
+ const update = (line, direction) => {
2611
+ groups.forEach((patterns, index) => {
2612
+ if (patterns.some((pattern) => executableSourcePatternMatches(line, pattern))) {
2613
+ counts[index] = (counts[index] ?? 0) + direction;
2614
+ }
2615
+ });
2616
+ };
2617
+ for (let index = 0; index < lines.length; index += 1) {
2618
+ update(lines[index] ?? "", 1);
2619
+ if (index >= maxSpanLines) update(lines[index - maxSpanLines] ?? "", -1);
2620
+ if (counts.every((count) => count > 0)) return true;
2621
+ }
2622
+ return false;
2623
+ }
2624
+ function executableSourcePatternMatches(line, pattern) {
2625
+ const expression = new RegExp(pattern, "giu");
2626
+ const quoted = sourceQuotedIndices(line);
2627
+ for (const match of line.matchAll(expression)) {
2628
+ if (quoted[match.index] !== 1) return true;
2629
+ }
2630
+ return false;
2631
+ }
2632
+ function sourceQuotedIndices(line) {
2633
+ const quoted = new Uint8Array(line.length);
2634
+ let quote = "";
2635
+ let escaped = false;
2636
+ for (let index = 0; index < line.length; index += 1) {
2637
+ const character = line[index] ?? "";
2638
+ if (quote) quoted[index] = 1;
2639
+ if (escaped) {
2640
+ escaped = false;
2641
+ continue;
2642
+ }
2643
+ if (character === "\\") {
2644
+ escaped = true;
2645
+ continue;
2646
+ }
2647
+ if (quote) {
2648
+ if (character === quote) quote = "";
2649
+ } else if (['"', "'", "`"].includes(character)) {
2650
+ quote = character;
2651
+ quoted[index] = 1;
2652
+ }
2653
+ }
2654
+ return quoted;
2655
+ }
2656
+ function commandArgumentMatches(argument, actualArguments, bindings) {
2657
+ const normalizedArgument = argument.toLowerCase();
2658
+ const matchesArgument = isRepositoryCommandPath(argument) ? actualArguments.some(
2659
+ (candidate) => normalizeCommandPath(candidate) === normalizeCommandPath(normalizedArgument)
2660
+ ) : actualArguments.includes(normalizedArgument);
2661
+ if (!matchesArgument) return false;
2662
+ return !isRepositoryCommandPath(argument) || bindings.availableCommandPaths.has(normalizeCommandPath(argument));
2663
+ }
2664
+ function containsArgumentSequence(arguments_, sequence) {
2665
+ return arguments_.some(
2666
+ (_, index) => sequence.every((argument, offset) => arguments_[index + offset] === argument)
2667
+ );
2668
+ }
2669
+ function startsWithArgumentSequence(arguments_, sequence) {
2670
+ return sequence.every((argument, index) => arguments_[index] === argument);
2671
+ }
2672
+ function normalizeCommandArgument(value) {
2673
+ let normalized = value.toLowerCase();
2674
+ const enclosingQuote = normalized[0];
2675
+ if (normalized.length >= 2 && (enclosingQuote === '"' || enclosingQuote === "'") && normalized.at(-1) === enclosingQuote) {
2676
+ normalized = normalized.slice(1, -1);
2677
+ }
2678
+ return normalized.replace(/=(["'])([^"']*)\1$/, "=$2");
2679
+ }
2680
+ function packageScriptInvocation(tokens) {
2681
+ const manager = executableIdentity(tokens[0] ?? "");
2682
+ if (!["bun", "npm", "pnpm", "yarn"].includes(manager)) return null;
2683
+ const arguments_ = tokens.slice(1);
2684
+ let index = skipPackageOptions(arguments_, 0);
2685
+ const subcommand = arguments_[index]?.toLowerCase() ?? "";
2686
+ if (["dlx", "exec", "x"].includes(subcommand)) return null;
2687
+ if (packageManagerDirectToolCommands.get(manager)?.has(subcommand)) return null;
2688
+ if (["run", "run-script"].includes(subcommand)) {
2689
+ index = skipPackageOptions(arguments_, index + 1);
2690
+ } else if (manager === "npm") {
2691
+ const implicitTask = npmImplicitScripts.get(subcommand);
2692
+ return implicitTask ? {
2693
+ hasForwardedArguments: hasForwardedPackageArguments(arguments_, index),
2694
+ manager,
2695
+ task: implicitTask
2696
+ } : null;
2697
+ }
2698
+ const task = packageTaskName(arguments_[index]);
2699
+ return task ? { hasForwardedArguments: hasForwardedPackageArguments(arguments_, index), manager, task } : null;
2700
+ }
2701
+ function packageTaskName(value) {
2702
+ if (!value) return null;
2703
+ const quote = value[0];
2704
+ if (value.length >= 2 && ['"', "'"].includes(quote ?? "") && value.at(-1) === quote) {
2705
+ return value.slice(1, -1);
2706
+ }
2707
+ return value;
2708
+ }
2709
+ function hasForwardedPackageArguments(arguments_, taskIndex) {
2710
+ return arguments_.slice(taskIndex + 1).some((argument) => argument !== "--");
2711
+ }
2712
+ function skipPackageOptions(arguments_, start) {
2713
+ let index = start;
2714
+ while (index < arguments_.length) {
2715
+ const argument = arguments_[index]?.toLowerCase() ?? "";
2716
+ if (argument === "--") {
2717
+ index += 1;
2718
+ break;
2719
+ }
2720
+ if (!argument.startsWith("-")) break;
2721
+ const option = argument.split("=")[0] ?? "";
2722
+ index += 1;
2723
+ if (!argument.includes("=") && packageGlobalOptionsWithValues.has(option)) index += 1;
2724
+ }
2725
+ return index;
2726
+ }
2727
+ function executableIdentity(value) {
2728
+ return value.split("/").at(-1)?.replace(/\.exe$/i, "").toLowerCase() ?? "";
2729
+ }
2730
+ function shellStatements(value, requiresFinalExitStatus, failFast = false) {
2731
+ if (value.includes(";") || hasUnsupportedShellStructure(value)) return [];
2732
+ const statements = value.split(/\r?\n/).map((statement) => statement.trim()).filter((statement) => statement.length > 0 && !statement.startsWith("#"));
2733
+ const terminatingIndex = statements.findIndex(hasUnconditionalShellTermination);
2734
+ const reachable = terminatingIndex < 0 ? statements : statements.slice(0, terminatingIndex + 1);
2735
+ if (failFast) return reachable;
2736
+ if (!requiresFinalExitStatus && reachable.length <= 1) return reachable;
2737
+ return reachable.length > 0 ? [reachable.at(-1) ?? ""] : [];
2738
+ }
2739
+ function hasUnconditionalShellTermination(value) {
2740
+ return value.split("&&").some(
2741
+ (command) => /^(?:[a-z_][a-z0-9_]*=[^\s]+\s+)*(?:exit|return)(?:\s|$)/i.test(command.trim())
2742
+ );
2743
+ }
2744
+ function hasUnsupportedShellStructure(value) {
2745
+ if (value.includes("<<")) return true;
2746
+ return value.split(/\r?\n/).some((line) => {
2747
+ const trimmed = line.trimEnd();
2748
+ return trimmed.endsWith("\\") || trimmed.endsWith("`");
2749
+ });
2750
+ }
2751
+ function stringValues(value) {
2752
+ if (typeof value === "string") return [value];
2753
+ return asArray(value).filter((entry) => typeof entry === "string");
2754
+ }
2755
+ function asRecord(value) {
2756
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
2757
+ }
2758
+ function asArray(value) {
2759
+ return Array.isArray(value) ? value : [];
2760
+ }
2761
+ function strongestContentMatch(text, terms, requiredTerms, minTerms, maxSpanLines) {
2762
+ if (!maxSpanLines) {
2763
+ const matched = terms.filter((term) => containsTerm(text, term)).length;
2764
+ const requiredMatched = requiredTerms.filter((term) => containsTerm(text, term)).length;
2765
+ return {
2766
+ matched,
2767
+ requiredMatched,
2768
+ qualifies: matched >= minTerms && (requiredTerms.length === 0 || requiredMatched > 0)
2769
+ };
2770
+ }
2771
+ const termCounts = terms.map(() => 0);
2772
+ const requiredCounts = requiredTerms.map(() => 0);
2773
+ const lines = text.split(/\r?\n/);
2774
+ let strongest = 0;
2775
+ let strongestRequired = 0;
2776
+ let qualifies = false;
2777
+ const update = (line, direction) => {
2778
+ terms.forEach((term, index) => {
2779
+ if (containsTerm(line, term)) termCounts[index] = (termCounts[index] ?? 0) + direction;
2780
+ });
2781
+ requiredTerms.forEach((term, index) => {
2782
+ if (containsTerm(line, term)) {
2783
+ requiredCounts[index] = (requiredCounts[index] ?? 0) + direction;
2784
+ }
2785
+ });
2786
+ };
2787
+ for (let index = 0; index < lines.length; index += 1) {
2788
+ update(lines[index] ?? "", 1);
2789
+ if (index >= maxSpanLines) update(lines[index - maxSpanLines] ?? "", -1);
2790
+ const matched = termCounts.filter((count) => count > 0).length;
2791
+ const requiredMatched = requiredCounts.filter((count) => count > 0).length;
2792
+ strongest = Math.max(strongest, matched);
2793
+ strongestRequired = Math.max(strongestRequired, requiredMatched);
2794
+ if (matched >= minTerms && (requiredTerms.length === 0 || requiredMatched > 0)) {
2795
+ qualifies = true;
2796
+ }
2797
+ }
2798
+ return { matched: strongest, requiredMatched: strongestRequired, qualifies };
2799
+ }
2800
+ function containsTerm(text, term) {
2801
+ const pattern = termPattern(term);
2802
+ return new RegExp(`(^|[^a-z0-9])${pattern}(?=$|[^a-z0-9])`, "i").test(text);
2803
+ }
2804
+ function containsPositiveTerm(text, term) {
2805
+ const pattern = termPattern(term);
2806
+ const expression = new RegExp(`(^|[^a-z0-9])(${pattern})(?=$|[^a-z0-9])`, "gi");
2807
+ for (const match of text.matchAll(expression)) {
2808
+ const termStart = match.index + (match[1]?.length ?? 0);
2809
+ const prefix = containingClausePrefix(text, termStart);
2810
+ const suffix = containingClauseSuffix(text, termStart + (match[2]?.length ?? 0));
2811
+ if (!hasNegativePrefix(prefix) && !hasNegativeSuffix(suffix) && !hasExplicitlyUnboundedQualifier(prefix, suffix) && !hasPlaceholderQualifier(prefix, suffix) && !hasNonHumanAuthorityPrefix(prefix, term)) {
2812
+ return true;
2813
+ }
2814
+ }
2815
+ return false;
2816
+ }
2817
+ function hasExplicitlyUnboundedQualifier(prefix, suffix) {
2818
+ const precedingQualifier = /\b(?:unbounded|unlimited)(?:\s+[a-z0-9_-]+){0,2}\s*$/i.test(prefix);
2819
+ const followingQualifier = /^\s*(?:(?:is|are|remains?|stays?|=|:)\s*)?(?:explicitly\s+)?(?:unbounded|unlimited)\b/i.test(
2820
+ suffix
2821
+ );
2822
+ return precedingQualifier || followingQualifier;
2823
+ }
2824
+ function hasPlaceholderQualifier(prefix, suffix) {
2825
+ return hasPrecedingPlaceholder(prefix) || hasFollowingPlaceholder(suffix);
2826
+ }
2827
+ function normalizedQualifierPhrase(value) {
2828
+ return value.trim().toLowerCase().replace(/\s+/g, " ").replaceAll(" / ", "/").replaceAll("/ ", "/").replaceAll(" /", "/");
2829
+ }
2830
+ function hasPrecedingPlaceholder(prefix) {
2831
+ const normalized = normalizedQualifierPhrase(prefix);
2832
+ for (const placeholder of semanticPlaceholderQualifiers) {
2833
+ const index = normalized.lastIndexOf(placeholder);
2834
+ if (index < 0) continue;
2835
+ const preceding = normalized[index - 1] ?? "";
2836
+ if (/[a-z0-9_]/i.test(preceding)) continue;
2837
+ const trailingWords = normalized.slice(index + placeholder.length).trim().split(/\s+/);
2838
+ if (trailingWords.length <= 2 && trailingWords.every(isQualifierBridgeWord)) return true;
2839
+ }
2840
+ return false;
2841
+ }
2842
+ function isQualifierBridgeWord(value) {
2843
+ return value.length === 0 || /^[a-z0-9_-]+$/i.test(value);
2844
+ }
2845
+ function hasFollowingPlaceholder(suffix) {
2846
+ let normalized = normalizedQualifierPhrase(suffix);
2847
+ for (const link of [
2848
+ "remains ",
2849
+ "remain ",
2850
+ "stays ",
2851
+ "stay ",
2852
+ "are ",
2853
+ "is ",
2854
+ "= ",
2855
+ "=",
2856
+ ": ",
2857
+ ":"
2858
+ ]) {
2859
+ if (!normalized.startsWith(link)) continue;
2860
+ normalized = normalized.slice(link.length).trimStart();
2861
+ break;
2862
+ }
2863
+ if (normalized.startsWith("explicitly ")) normalized = normalized.slice("explicitly ".length);
2864
+ return semanticPlaceholderQualifiers.some(
2865
+ (placeholder) => normalized === placeholder || normalized.startsWith(`${placeholder} `)
2866
+ );
2867
+ }
2868
+ function hasNonHumanAuthorityPrefix(prefix, term) {
2869
+ if (!/\b(?:maintainers?|owners?|reviewers?)\b/i.test(term)) return false;
2870
+ return /\b(?:ai|agents?|bots?|automated|automation)\s+$/i.test(prefix);
2871
+ }
2872
+ function hasNegativePrefix(value) {
2873
+ const normalized = withoutRestrictiveUpperBound(value);
2874
+ const negativeWord = /\b(?:cannot|forbidden|lacks?|lacking|missing|never|no|not|prohibited|without)\b/i.test(
2875
+ normalized
2876
+ );
2877
+ const negativeModal = /\b(?:can|do|does|may|must)\s+not\b/i.test(normalized);
2878
+ return negativeWord || negativeModal;
2879
+ }
2880
+ function hasNegativeSuffix(value) {
2881
+ const normalized = withoutRestrictiveUpperBound(value);
2882
+ const negativeWord = /\b(?:absent|cannot|forbidden|lacking|missing|never|not|prohibited|unavailable|without)\b/i.test(
2883
+ normalized
2884
+ );
2885
+ return negativeWord || /:\s*none\b/i.test(normalized);
2886
+ }
2887
+ function withoutRestrictiveUpperBound(value) {
2888
+ return value.replace(/\b(?:cannot|may not|must not|shall not|should not)\s+exceed\b/gi, "");
2889
+ }
2890
+ function containingClausePrefix(text, end) {
2891
+ const before = text.slice(0, end);
2892
+ const boundaries = [...before.matchAll(/[.;\n]|\bbut\b/gi)];
2893
+ const lastBoundary = boundaries.at(-1);
2894
+ return before.slice(lastBoundary ? lastBoundary.index + lastBoundary[0].length : 0);
2895
+ }
2896
+ function containingClauseSuffix(text, start) {
2897
+ const after = text.slice(start);
2898
+ const boundary = /[.;\n]|\bbut\b/i.exec(after);
2899
+ return after.slice(0, boundary?.index ?? after.length);
2900
+ }
2901
+ function termPattern(term) {
2902
+ return term.trim().split(/\s+/).map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\s_-]+");
2903
+ }
2904
+ async function evaluateMaxBytes(context, check) {
2905
+ const paths = await matches(context, check.patterns);
2906
+ let total = 0;
2907
+ const inspected = [];
2908
+ for (const path of paths) {
2909
+ const size = await safeFileSize(context.metadata.root, path);
2910
+ if (size === null) continue;
2911
+ total += size;
2912
+ inspected.push(path);
2913
+ }
2914
+ const passed = inspected.length > 0 && total <= check.max_bytes;
2915
+ return result(
2916
+ check.type,
2917
+ check.scope,
2918
+ passed ? "met" : "not_met",
2919
+ `${total} byte(s) across ${inspected.length} safe matching file(s); maximum ${check.max_bytes}`,
2920
+ inspected
2921
+ );
2922
+ }
2923
+ function result(type, scope, status, summary, references2) {
2924
+ return { type, scope, status, summary, references: references2 };
2925
+ }
2926
+ async function evaluateCheck(context, check) {
2927
+ switch (check.type) {
2928
+ case "path_any":
2929
+ return evaluatePathAny(context, check);
2930
+ case "path_all":
2931
+ return evaluatePathAll(context, check);
2932
+ case "ownership_map":
2933
+ return evaluateOwnershipMap(context, check);
2934
+ case "content_any":
2935
+ case "content_all":
2936
+ return evaluateLegacyContent(context, check);
2937
+ case "content_terms":
2938
+ return evaluateContentTerms(context, check);
2939
+ case "content_groups":
2940
+ return evaluateContentGroups(context, check);
2941
+ case "ci_command":
2942
+ return evaluateCiCommand(context, check);
2943
+ case "max_bytes":
2944
+ return evaluateMaxBytes(context, check);
2945
+ case "manual":
2946
+ return result("manual", check.scope, "unknown", check.prompt, []);
2947
+ }
2948
+ }
2949
+ function activeAttestation(control, attestations, now) {
958
2950
  if (!control.allow_attestation) return null;
959
2951
  const attestation = attestations?.attestations[control.id];
960
2952
  if (!attestation) return null;
@@ -967,40 +2959,127 @@ function activeAgentEvidence(control, claim, now) {
967
2959
  if (new Date(claim.expires_at) < now) return null;
968
2960
  return claim;
969
2961
  }
2962
+ function evidenceChecksPass(control, evidence) {
2963
+ return control.evidence_mode === "any" ? evidence.some(({ status }) => status === "met") : evidence.every(({ status }) => status === "met");
2964
+ }
2965
+ function repositoryEvidencePasses(control, evidence, checksPassed) {
2966
+ const hasRepositoryMatch = evidence.some(
2967
+ ({ scope, status }) => scope === "repository" && status === "met"
2968
+ );
2969
+ const hasManualCheck = control.evidence.some(({ type }) => type === "manual");
2970
+ return checksPassed && hasRepositoryMatch && (!hasManualCheck || control.evidence_mode === "any");
2971
+ }
2972
+ function externalEvidenceConflicts(attestation, agentEvidence) {
2973
+ const attestationStatus = attestation?.status === "unknown" ? null : attestation?.status ?? null;
2974
+ return agentEvidence !== null && agentEvidence.status !== "unknown" && attestationStatus !== null && agentEvidence.status !== attestationStatus;
2975
+ }
2976
+ function supplementalResolution(attestation, agentEvidence) {
2977
+ if (externalEvidenceConflicts(attestation, agentEvidence)) {
2978
+ return { confidence: "none", status: "unknown" };
2979
+ }
2980
+ if (agentEvidence && agentEvidence.status !== "unknown") {
2981
+ return { confidence: "agent-collected", status: agentEvidence.status };
2982
+ }
2983
+ if (attestation && attestation.status !== "unknown") {
2984
+ return { confidence: "attested", status: attestation.status };
2985
+ }
2986
+ if (agentEvidence?.status === "unknown" || attestation?.status === "unknown") {
2987
+ return { confidence: "none", status: "unknown" };
2988
+ }
2989
+ return null;
2990
+ }
2991
+ function alternativeSupplementalResolution(evidence, attestation, agentEvidence) {
2992
+ if (attestation?.status === "not_applicable") {
2993
+ return { confidence: "attested", status: "not_applicable" };
2994
+ }
2995
+ const statuses = evidence.map(({ status }) => status);
2996
+ const sources = evidence.map(() => null);
2997
+ const agentIndexes = matchingAlternativeIndexes(evidence, agentEvidence?.scope ?? null);
2998
+ const attestationIndexes = manualAlternativeIndexes(evidence);
2999
+ if (alternativeSupplementalEvidenceConflicts(
3000
+ evidence,
3001
+ attestationIndexes,
3002
+ attestation,
3003
+ agentEvidence
3004
+ )) {
3005
+ return { confidence: "none", status: "unknown" };
3006
+ }
3007
+ applyAlternativeStatus(
3008
+ statuses,
3009
+ sources,
3010
+ agentIndexes,
3011
+ agentEvidence?.status ?? null,
3012
+ "agent-collected"
3013
+ );
3014
+ applyAlternativeStatus(
3015
+ statuses,
3016
+ sources,
3017
+ attestationIndexes,
3018
+ attestation?.status ?? null,
3019
+ "attested"
3020
+ );
3021
+ if (statuses.includes("met")) {
3022
+ return { confidence: decisiveAlternativeConfidence(statuses, sources, "met"), status: "met" };
3023
+ }
3024
+ if (statuses.every((status) => status === "not_met")) {
3025
+ return {
3026
+ confidence: decisiveAlternativeConfidence(statuses, sources, "not_met"),
3027
+ status: "not_met"
3028
+ };
3029
+ }
3030
+ return { confidence: "none", status: "unknown" };
3031
+ }
3032
+ function matchingAlternativeIndexes(evidence, scope) {
3033
+ if (scope === null) return [];
3034
+ return evidence.flatMap(({ scope: candidate }, index) => candidate === scope ? [index] : []);
3035
+ }
3036
+ function manualAlternativeIndexes(evidence) {
3037
+ return evidence.flatMap(({ type }, index) => type === "manual" ? [index] : []);
3038
+ }
3039
+ function alternativeSupplementalEvidenceConflicts(evidence, attestationIndexes, attestation, agentEvidence) {
3040
+ if (!agentEvidence || agentEvidence.status === "unknown") return false;
3041
+ if (!attestation || ["not_applicable", "unknown"].includes(attestation.status)) return false;
3042
+ const sameAlternative = attestationIndexes.some(
3043
+ (index) => evidence[index]?.scope === agentEvidence.scope
3044
+ );
3045
+ return sameAlternative && agentEvidence.status !== attestation.status;
3046
+ }
3047
+ function applyAlternativeStatus(statuses, sources, indexes, status, source) {
3048
+ if (status === null || status === "not_applicable") return;
3049
+ for (const index of indexes) {
3050
+ statuses[index] = status;
3051
+ sources[index] = source;
3052
+ }
3053
+ }
3054
+ function decisiveAlternativeConfidence(statuses, sources, decisiveStatus) {
3055
+ const decisiveSources = sources.filter(
3056
+ (source, index) => statuses[index] === decisiveStatus && source !== null
3057
+ );
3058
+ if (decisiveSources.includes("agent-collected")) return "agent-collected";
3059
+ if (decisiveSources.includes("attested")) return "attested";
3060
+ return "none";
3061
+ }
3062
+ function resolveControl(control, evidence, attestation, agentEvidence) {
3063
+ const checksPassed = evidenceChecksPass(control, evidence);
3064
+ if (repositoryEvidencePasses(control, evidence, checksPassed)) {
3065
+ return { confidence: "repository-detected", status: "met" };
3066
+ }
3067
+ if (control.evidence_mode === "any") {
3068
+ return alternativeSupplementalResolution(evidence, attestation, agentEvidence);
3069
+ }
3070
+ const supplemental = supplementalResolution(attestation, agentEvidence);
3071
+ if (supplemental) return supplemental;
3072
+ const hasUnknownCheck = evidence.some(({ status }) => status === "unknown");
3073
+ if (hasUnknownCheck) return { confidence: "none", status: "unknown" };
3074
+ return { confidence: "none", status: checksPassed ? "met" : "not_met" };
3075
+ }
970
3076
  async function evaluateControl(context, control, attestations, agentClaim, now = /* @__PURE__ */ new Date()) {
971
3077
  const evidence = await Promise.all(
972
3078
  control.evidence.map(async (check) => evaluateCheck(context, check))
973
3079
  );
974
3080
  const attestation = activeAttestation(control, attestations, now);
975
3081
  const agentEvidence = activeAgentEvidence(control, agentClaim, now);
976
- const checksPassed = evidence.every(({ status: status2 }) => status2 === "met");
977
- const hasManualCheck = control.evidence.some(({ type }) => type === "manual");
978
- let status = checksPassed ? "met" : "not_met";
979
- let confidence = checksPassed && !hasManualCheck ? "repository-detected" : "none";
980
- const attestationStatus = attestation?.status === "unknown" ? null : attestation?.status ?? null;
981
- const hasExternalConflict = agentEvidence !== null && agentEvidence.status !== "unknown" && attestationStatus !== null && agentEvidence.status !== attestationStatus;
982
- if (checksPassed && !hasManualCheck) {
983
- } else if (hasExternalConflict) {
984
- status = "unknown";
985
- confidence = "none";
986
- } else if (agentEvidence && agentEvidence.status !== "unknown") {
987
- status = agentEvidence.status;
988
- confidence = "agent-collected";
989
- } else if (attestation?.status === "not_applicable") {
990
- status = "not_applicable";
991
- confidence = "attested";
992
- } else if (attestation?.status === "met") {
993
- status = "met";
994
- confidence = "attested";
995
- } else if (attestation?.status === "not_met" || attestation?.status === "unknown") {
996
- status = attestation.status;
997
- confidence = "attested";
998
- } else if (agentEvidence?.status === "unknown") {
999
- status = "unknown";
1000
- confidence = "agent-collected";
1001
- } else if (evidence.some(({ status: checkStatus }) => checkStatus === "unknown")) {
1002
- status = "unknown";
1003
- }
3082
+ const { confidence, status } = resolveControl(control, evidence, attestation, agentEvidence);
1004
3083
  return {
1005
3084
  id: control.id,
1006
3085
  dimension: control.dimension,
@@ -1010,6 +3089,7 @@ async function evaluateControl(context, control, attestations, agentClaim, now =
1010
3089
  risk: control.risk,
1011
3090
  status,
1012
3091
  confidence,
3092
+ ...control.evidence_mode === "any" ? { evidence_mode: "any" } : {},
1013
3093
  evidence,
1014
3094
  agent_evidence: agentEvidence,
1015
3095
  attestation,
@@ -1021,6 +3101,15 @@ async function evaluateControl(context, control, attestations, agentClaim, now =
1021
3101
  function controlPasses(control) {
1022
3102
  return control.status === "met" || control.status === "not_applicable";
1023
3103
  }
3104
+ function usesModernEvidence(version) {
3105
+ return version === "0.3.0" || version === "0.4.0";
3106
+ }
3107
+ function reportSchemaVersion(version) {
3108
+ if (usesModernEvidence(version)) {
3109
+ return version;
3110
+ }
3111
+ return "0.2.0";
3112
+ }
1024
3113
  function dimensionScore(controls, dimension) {
1025
3114
  let score = 0;
1026
3115
  for (const level of [1, 2, 3, 4]) {
@@ -1032,6 +3121,10 @@ function dimensionScore(controls, dimension) {
1032
3121
  }
1033
3122
  return score;
1034
3123
  }
3124
+ function isRepositoryDetectable(control) {
3125
+ const repositoryChecks = control.evidence.filter(({ scope }) => scope === "repository");
3126
+ return control.evidence_mode === "any" ? repositoryChecks.length > 0 : repositoryChecks.length === control.evidence.length;
3127
+ }
1035
3128
  function repositoryScore(catalog, results, dimensions) {
1036
3129
  let achieved = 0;
1037
3130
  let ceiling = 0;
@@ -1045,9 +3138,7 @@ function repositoryScore(catalog, results, dimensions) {
1045
3138
  const controlsAtLevel = catalog.filter(
1046
3139
  (control) => control.dimension === dimension && control.level === level
1047
3140
  );
1048
- const repositoryDetectable = controlsAtLevel.every(
1049
- (control) => control.evidence.every((evidence) => evidence.scope === "repository")
1050
- );
3141
+ const repositoryDetectable = controlsAtLevel.every(isRepositoryDetectable);
1051
3142
  if (ceilingOpen && repositoryDetectable) {
1052
3143
  dimensionCeiling = level;
1053
3144
  } else {
@@ -1097,7 +3188,7 @@ function assessProfiles(benchmark, dimensions, controls) {
1097
3188
  title: profile.title,
1098
3189
  passed: blockers.length === 0,
1099
3190
  blockers,
1100
- ...benchmark.version === "0.3.0" ? {
3191
+ ...usesModernEvidence(benchmark.version) ? {
1101
3192
  evidence_dependencies: {
1102
3193
  agent_collected: requiredControls.filter(
1103
3194
  ({ confidence }) => confidence === "agent-collected"
@@ -1117,7 +3208,10 @@ async function assess(repo, benchmark, catalog, profileId, options = {}) {
1117
3208
  }
1118
3209
  const scope = options.scope ?? "tracked";
1119
3210
  const now = options.now ?? /* @__PURE__ */ new Date();
3211
+ const modernEvidence = usesModernEvidence(benchmark.version);
3212
+ const schemaVersion = reportSchemaVersion(benchmark.version);
1120
3213
  const context = await createRepositoryContext(repo, scope, options.excludedPaths);
3214
+ validateAttestations(benchmark, catalog, context, options.attestations ?? null, now);
1121
3215
  await validateAgentEvidence(benchmark, catalog, context, options.agentEvidence ?? null, now);
1122
3216
  const controls = await Promise.all(
1123
3217
  catalog.map(
@@ -1130,6 +3224,27 @@ async function assess(repo, benchmark, catalog, profileId, options = {}) {
1130
3224
  )
1131
3225
  )
1132
3226
  );
3227
+ const warnings = [...options.warnings ?? []];
3228
+ if (modernEvidence) {
3229
+ if (scope === "tracked" && context.metadata.tracked_tree_dirty) {
3230
+ warnings.push(
3231
+ "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."
3232
+ );
3233
+ }
3234
+ const hasActiveSupplementalEvidence = controls.some(
3235
+ ({ agent_evidence: agentEvidence, attestation }) => agentEvidence !== null || attestation !== null
3236
+ );
3237
+ const unresolvedExternalOrOutcome = controls.some(
3238
+ ({ evidence, status }) => (status === "unknown" || status === "not_met") && evidence.some(
3239
+ ({ scope: evidenceScope }) => ["platform", "organization", "outcome"].includes(evidenceScope)
3240
+ )
3241
+ );
3242
+ if (!hasActiveSupplementalEvidence && unresolvedExternalOrOutcome) {
3243
+ warnings.push(
3244
+ "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."
3245
+ );
3246
+ }
3247
+ }
1133
3248
  const dimensions = benchmark.dimensions.map(({ id, title }) => {
1134
3249
  const dimensionControls = controls.filter((control) => control.dimension === id);
1135
3250
  return {
@@ -1142,11 +3257,11 @@ async function assess(repo, benchmark, catalog, profileId, options = {}) {
1142
3257
  });
1143
3258
  const profiles = assessProfiles(benchmark, dimensions, controls);
1144
3259
  const total = dimensions.reduce((sum, { score }) => sum + score, 0);
1145
- const repository = benchmark.version === "0.3.0" ? repositoryScore(catalog, controls, benchmark.dimensions) : void 0;
3260
+ const repository = modernEvidence ? repositoryScore(catalog, controls, benchmark.dimensions) : void 0;
1146
3261
  const highestProfile = [...profiles].reverse().find(({ passed }) => passed)?.id ?? null;
1147
3262
  const targetPassed = profiles.find(({ id }) => id === profileId)?.passed ?? false;
1148
3263
  return {
1149
- schema_version: benchmark.version === "0.3.0" ? "0.3.0" : "0.2.0",
3264
+ schema_version: schemaVersion,
1150
3265
  benchmark: { id: benchmark.id, version: benchmark.version },
1151
3266
  target: {
1152
3267
  repository: repo,
@@ -1157,7 +3272,7 @@ async function assess(repo, benchmark, catalog, profileId, options = {}) {
1157
3272
  working_tree_dirty: context.metadata.working_tree_dirty
1158
3273
  },
1159
3274
  assessed_at: now.toISOString(),
1160
- ...benchmark.version === "0.3.0" ? { warnings: options.warnings ?? [] } : {},
3275
+ ...modernEvidence ? { warnings } : {},
1161
3276
  score: {
1162
3277
  total,
1163
3278
  maximum: 40,
@@ -1176,7 +3291,7 @@ async function assess(repo, benchmark, catalog, profileId, options = {}) {
1176
3291
  ).length,
1177
3292
  unmet: controls.filter(({ status }) => status === "not_met").length,
1178
3293
  unknown: controls.filter(({ status }) => status === "unknown").length,
1179
- ...benchmark.version === "0.3.0" ? {
3294
+ ...modernEvidence ? {
1180
3295
  resolved: controls.filter(({ status }) => status !== "unknown").length,
1181
3296
  total: controls.length
1182
3297
  } : {}
@@ -1188,7 +3303,7 @@ async function assess(repo, benchmark, catalog, profileId, options = {}) {
1188
3303
  limitations: [
1189
3304
  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.",
1190
3305
  "Repository-detected evidence proves a qualifying artifact match, not consistent practice or external enforcement.",
1191
- ...benchmark.version === "0.3.0" ? [
3306
+ ...modernEvidence ? [
1192
3307
  "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
3308
  "Agent-collected repository evidence is semantic, target-bound, and source-backed but is not independently verified or relabelled as repository-detected."
1194
3309
  ] : [],
@@ -1197,6 +3312,38 @@ async function assess(repo, benchmark, catalog, profileId, options = {}) {
1197
3312
  ]
1198
3313
  };
1199
3314
  }
3315
+ function validateAttestations(benchmark, catalog, context, attestations, now) {
3316
+ if (!attestations || benchmark.version !== "0.4.0") return;
3317
+ if (attestations.benchmark_version !== benchmark.version) {
3318
+ throw new Error(
3319
+ `Attestation benchmark ${attestations.benchmark_version} does not match ${benchmark.version}`
3320
+ );
3321
+ }
3322
+ const expectedTarget = normalizeRepositoryTarget(
3323
+ repositoryEvidenceTarget(context.metadata).repository
3324
+ );
3325
+ if (!attestations.target) {
3326
+ throw new Error("ADRB v0.4 attestations require a repository target");
3327
+ }
3328
+ if (normalizeRepositoryTarget(attestations.target.repository) !== expectedTarget) {
3329
+ throw new Error("Attestation target does not match the assessed repository");
3330
+ }
3331
+ const controlIds = new Set(catalog.map(({ id }) => id));
3332
+ for (const [controlId, attestation] of Object.entries(attestations.attestations)) {
3333
+ if (!/^ADRB-[A-Z]{3}-\d{3}$/.test(controlId)) {
3334
+ throw new Error(`Attestation uses malformed control ID ${controlId}`);
3335
+ }
3336
+ if (!controlIds.has(controlId)) {
3337
+ throw new Error(`Attestation references unknown control ${controlId}`);
3338
+ }
3339
+ if (/* @__PURE__ */ new Date(`${attestation.reviewed_at}T00:00:00.000Z`) > now) {
3340
+ throw new Error(`${controlId} has a future review date`);
3341
+ }
3342
+ if (attestation.status !== "unknown" && [attestation.owner, attestation.evidence].some((value) => /^\s*TODO(?:\b|:)/i.test(value))) {
3343
+ throw new Error(`${controlId} contains unresolved TODO attestation evidence`);
3344
+ }
3345
+ }
3346
+ }
1200
3347
  async function validateAgentEvidence(benchmark, catalog, context, evidence, now) {
1201
3348
  if (!evidence) return;
1202
3349
  if (evidence.benchmark_version !== benchmark.version) {
@@ -1206,17 +3353,17 @@ async function validateAgentEvidence(benchmark, catalog, context, evidence, now)
1206
3353
  }
1207
3354
  const expectedTarget = repositoryEvidenceTarget(context.metadata);
1208
3355
  if (evidence.target.repository !== expectedTarget.repository) {
1209
- throw new Error(
1210
- `Agent evidence target ${evidence.target.repository} does not match ${expectedTarget.repository}`
1211
- );
3356
+ throw new Error("Agent evidence target does not match the assessed repository");
1212
3357
  }
1213
3358
  if (evidence.target.git_head !== expectedTarget.git_head) {
1214
3359
  throw new Error(
1215
3360
  `Agent evidence commit ${evidence.target.git_head ?? "unavailable"} does not match ${expectedTarget.git_head ?? "an unavailable Git commit"}`
1216
3361
  );
1217
3362
  }
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");
3363
+ if (usesModernEvidence(benchmark.version) && context.metadata.tracked_tree_dirty) {
3364
+ throw new Error(
3365
+ `ADRB v${benchmark.version} agent evidence requires tracked files to match the bound commit`
3366
+ );
1220
3367
  }
1221
3368
  const controls = new Map(catalog.map((control) => [control.id, control]));
1222
3369
  if (Object.keys(evidence.claims).length > 0 && (evidence.collector.name.startsWith("TODO") || evidence.collector.version.startsWith("TODO"))) {
@@ -1295,7 +3442,7 @@ function countLines(contents) {
1295
3442
 
1296
3443
  // src/cli.ts
1297
3444
  var program = new Command();
1298
- program.name("agentic-scorecard").description("Evidence-backed readiness assessment for agentic software development harnesses").version("0.3.0");
3445
+ program.name("agentic-scorecard").description("Evidence-backed readiness assessment for agentic software development harnesses").version("0.4.0");
1299
3446
  program.command("validate").description("Validate the bundled benchmark catalog").action(async () => {
1300
3447
  const { benchmark, controls } = await loadBenchmark();
1301
3448
  process.stdout.write(
@@ -1332,11 +3479,17 @@ program.command("init").argument("[repository]", "repository to initialize", "."
1332
3479
  }
1333
3480
  }
1334
3481
  const { benchmark, controls } = await loadBenchmark();
3482
+ const context = await createRepositoryContext(repo, "workspace");
3483
+ const target = {
3484
+ repository: normalizeRepositoryTarget(repositoryEvidenceTarget(context.metadata).repository)
3485
+ };
1335
3486
  const reviewedAt = /* @__PURE__ */ new Date();
1336
3487
  const expiresAt = new Date(reviewedAt);
1337
3488
  expiresAt.setDate(expiresAt.getDate() + 90);
1338
3489
  const attestations = Object.fromEntries(
1339
- controls.filter((control) => control.evidence.some(({ type }) => type === "manual")).map((control) => {
3490
+ controls.filter(
3491
+ (control) => control.allow_attestation && control.evidence.some(({ type }) => type === "manual")
3492
+ ).map((control) => {
1340
3493
  const manualCheck = control.evidence.find(
1341
3494
  (check) => check.type === "manual"
1342
3495
  );
@@ -1356,7 +3509,7 @@ program.command("init").argument("[repository]", "repository to initialize", "."
1356
3509
  await writeFile(
1357
3510
  path,
1358
3511
  `# Claims are visibly human-attested. Link durable evidence; do not paste secrets.
1359
- ${stringify({ benchmark_version: benchmark.version, attestations })}`,
3512
+ ${stringify({ benchmark_version: benchmark.version, target, attestations })}`,
1360
3513
  "utf8"
1361
3514
  );
1362
3515
  process.stdout.write(`Created ${path}
@@ -1428,7 +3581,7 @@ ${stringify(bundle)}`,
1428
3581
  await writeFile(
1429
3582
  requestPath,
1430
3583
  [
1431
- "# ADRB v0.3 evidence request",
3584
+ `# ADRB v${benchmark.version} evidence request`,
1432
3585
  "",
1433
3586
  `- Repository: ${bundle.target.repository}`,
1434
3587
  `- Git commit: ${bundle.target.git_head ?? "unavailable"}`,