agentic-scorecard 0.3.1 → 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.
- package/AGENT_PROMPT.md +11 -11
- package/README.md +35 -29
- package/benchmark/v0.4/adapter-contract.md +129 -0
- package/benchmark/v0.4/adapters/known-agent-harnesses.yaml +266 -0
- package/benchmark/v0.4/adapters/known-ci-providers.yaml +57 -0
- package/benchmark/v0.4/adapters/known-command-wrappers.yaml +37 -0
- package/benchmark/v0.4/adapters/known-environment-tooling.yaml +17 -0
- package/benchmark/v0.4/adapters/known-github-conventions.yaml +27 -0
- package/benchmark/v0.4/adapters/known-python-tooling.yaml +15 -0
- package/benchmark/v0.4/adapters/known-security-tools.yaml +45 -0
- package/benchmark/v0.4/adapters/known-verification-tools.yaml +65 -0
- package/benchmark/v0.4/agent-evidence-schema.json +74 -0
- package/benchmark/v0.4/attestation-schema.json +35 -0
- package/benchmark/v0.4/benchmark.yaml +127 -0
- package/benchmark/v0.4/calibration-matrix.md +60 -0
- package/benchmark/v0.4/controls/context.yaml +75 -0
- package/benchmark/v0.4/controls/environment.yaml +93 -0
- package/benchmark/v0.4/controls/governance.yaml +132 -0
- package/benchmark/v0.4/controls/learning.yaml +78 -0
- package/benchmark/v0.4/controls/observability.yaml +91 -0
- package/benchmark/v0.4/controls/resilience.yaml +170 -0
- package/benchmark/v0.4/controls/security.yaml +143 -0
- package/benchmark/v0.4/controls/specification.yaml +65 -0
- package/benchmark/v0.4/controls/testing.yaml +82 -0
- package/benchmark/v0.4/controls/tooling.yaml +69 -0
- package/benchmark/v0.4/report-schema.json +89 -0
- package/benchmark/v0.4/scoring-policy.md +175 -0
- package/dist/cli.js +2287 -162
- package/package.json +3 -2
- package/templates/agent-evidence.yaml +2 -2
- 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
|
|
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
|
-
|
|
139
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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,15 @@ function toMarkdown(report) {
|
|
|
532
774
|
const unresolved = report.controls.filter(
|
|
533
775
|
({ status }) => status === "not_met" || status === "unknown"
|
|
534
776
|
);
|
|
535
|
-
const
|
|
536
|
-
const
|
|
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 =
|
|
785
|
+
const outcomeControls = requiredControls.filter((control) => controlScope(control) === "outcome");
|
|
540
786
|
const repositoryOnlyBaseline = !report.controls.some(
|
|
541
787
|
({ agent_evidence: agentEvidence, attestation }) => agentEvidence !== null || attestation !== null
|
|
542
788
|
);
|
|
@@ -597,9 +843,27 @@ function toMarkdown(report) {
|
|
|
597
843
|
);
|
|
598
844
|
}
|
|
599
845
|
}
|
|
600
|
-
appendControlDetails(lines, "Repository evidence gaps", repositoryGaps);
|
|
601
|
-
|
|
602
|
-
|
|
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
|
+
);
|
|
603
867
|
lines.push(
|
|
604
868
|
"",
|
|
605
869
|
"## Limitations",
|
|
@@ -710,6 +974,11 @@ function repositoryEvidenceTarget(metadata) {
|
|
|
710
974
|
git_head: metadata.git_head
|
|
711
975
|
};
|
|
712
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
|
+
}
|
|
713
982
|
|
|
714
983
|
// src/score.ts
|
|
715
984
|
import { readFile as readFile3 } from "fs/promises";
|
|
@@ -717,21 +986,148 @@ import { join as join2 } from "path";
|
|
|
717
986
|
|
|
718
987
|
// src/evidence.ts
|
|
719
988
|
import { lstat, readFile as readFile2, realpath as realpath2, stat } from "fs/promises";
|
|
720
|
-
import { resolve as resolve3, sep as sep2 } from "path";
|
|
989
|
+
import { basename, resolve as resolve3, sep as sep2 } from "path";
|
|
721
990
|
import fg2 from "fast-glob";
|
|
991
|
+
import { parse as parse2 } from "yaml";
|
|
722
992
|
var maxContentFileBytes = 512e3;
|
|
723
993
|
var maxContentFiles = 250;
|
|
724
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();
|
|
725
1113
|
async function matches(context, patterns) {
|
|
726
|
-
const
|
|
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, {
|
|
727
1120
|
cwd: context.metadata.root,
|
|
728
1121
|
dot: true,
|
|
729
1122
|
onlyFiles: true,
|
|
730
1123
|
unique: true,
|
|
731
1124
|
followSymbolicLinks: false,
|
|
732
1125
|
ignore: generatedEvidenceIgnores
|
|
733
|
-
})
|
|
734
|
-
|
|
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;
|
|
735
1131
|
}
|
|
736
1132
|
async function safeFileSize(root, path) {
|
|
737
1133
|
try {
|
|
@@ -776,30 +1172,274 @@ async function evaluatePathAll(context, check) {
|
|
|
776
1172
|
found
|
|
777
1173
|
);
|
|
778
1174
|
}
|
|
779
|
-
async function
|
|
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) {
|
|
780
1405
|
const root = context.metadata.root;
|
|
781
1406
|
const paths = maxFilesPerPattern ? await prioritizedMatches(context, patterns, maxFilesPerPattern) : (await matches(context, patterns)).slice(0, maxContentFiles);
|
|
782
1407
|
const files = [];
|
|
1408
|
+
const seenCanonicalPaths = /* @__PURE__ */ new Set();
|
|
783
1409
|
let totalBytes = 0;
|
|
784
1410
|
for (const path of paths) {
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
if (isGeneratedAssessment(text)) continue;
|
|
796
|
-
totalBytes += metadata.size;
|
|
797
|
-
files.push({ path, text });
|
|
798
|
-
} catch {
|
|
799
|
-
}
|
|
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 });
|
|
800
1421
|
}
|
|
801
1422
|
return files;
|
|
802
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
|
+
}
|
|
803
1443
|
async function prioritizedMatches(context, patterns, maxFilesPerPattern) {
|
|
804
1444
|
const selected = [];
|
|
805
1445
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -822,7 +1462,9 @@ async function prioritizedMatches(context, patterns, maxFilesPerPattern) {
|
|
|
822
1462
|
}
|
|
823
1463
|
function isGeneratedAssessment(text) {
|
|
824
1464
|
const normalized = text.trimStart();
|
|
825
|
-
if (normalized.startsWith("# agentic development readiness assessment"))
|
|
1465
|
+
if (normalized.toLowerCase().startsWith("# agentic development readiness assessment")) {
|
|
1466
|
+
return true;
|
|
1467
|
+
}
|
|
826
1468
|
if (!normalized.startsWith("{")) return false;
|
|
827
1469
|
try {
|
|
828
1470
|
const candidate = JSON.parse(normalized);
|
|
@@ -879,90 +1521,1433 @@ async function evaluateContentTerms(context, check) {
|
|
|
879
1521
|
qualifying.map(({ path }) => path)
|
|
880
1522
|
);
|
|
881
1523
|
}
|
|
882
|
-
function
|
|
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) {
|
|
883
1548
|
if (!maxSpanLines) {
|
|
884
|
-
const
|
|
885
|
-
const
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
qualifies: matched >= minTerms && (requiredTerms.length === 0 || requiredMatched > 0)
|
|
890
|
-
};
|
|
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 };
|
|
891
1554
|
}
|
|
892
|
-
const
|
|
893
|
-
const requiredCounts = requiredTerms.map(() => 0);
|
|
1555
|
+
const groupCounts = groups.map(() => 0);
|
|
894
1556
|
const lines = text.split(/\r?\n/);
|
|
895
|
-
let
|
|
896
|
-
let strongestRequired = 0;
|
|
897
|
-
let qualifies = false;
|
|
1557
|
+
let strongestGroups = [];
|
|
898
1558
|
const update = (line, direction) => {
|
|
899
|
-
|
|
900
|
-
if (
|
|
901
|
-
|
|
902
|
-
requiredTerms.forEach((term, index) => {
|
|
903
|
-
if (containsTerm(line, term)) {
|
|
904
|
-
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;
|
|
905
1562
|
}
|
|
906
1563
|
});
|
|
907
1564
|
};
|
|
908
1565
|
for (let index = 0; index < lines.length; index += 1) {
|
|
909
1566
|
update(lines[index] ?? "", 1);
|
|
910
1567
|
if (index >= maxSpanLines) update(lines[index - maxSpanLines] ?? "", -1);
|
|
911
|
-
const
|
|
912
|
-
|
|
913
|
-
strongest = Math.max(strongest, matched);
|
|
914
|
-
strongestRequired = Math.max(strongestRequired, requiredMatched);
|
|
915
|
-
if (matched >= minTerms && (requiredTerms.length === 0 || requiredMatched > 0)) {
|
|
916
|
-
qualifies = true;
|
|
917
|
-
}
|
|
1568
|
+
const activeGroups = groups.filter((_, groupIndex) => (groupCounts[groupIndex] ?? 0) > 0).map(({ id }) => id);
|
|
1569
|
+
if (activeGroups.length > strongestGroups.length) strongestGroups = activeGroups;
|
|
918
1570
|
}
|
|
919
|
-
return {
|
|
920
|
-
}
|
|
921
|
-
function containsTerm(text, term) {
|
|
922
|
-
const pattern = term.trim().split(/\s+/).map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\s_-]+");
|
|
923
|
-
return new RegExp(`(^|[^a-z0-9])${pattern}(?=$|[^a-z0-9])`, "i").test(text);
|
|
1571
|
+
return { matchedGroups: strongestGroups, qualifies: strongestGroups.length >= minGroups };
|
|
924
1572
|
}
|
|
925
|
-
async function
|
|
926
|
-
const
|
|
927
|
-
|
|
928
|
-
const
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
const
|
|
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}`;
|
|
936
1619
|
return result(
|
|
937
1620
|
check.type,
|
|
938
1621
|
check.scope,
|
|
939
1622
|
passed ? "met" : "not_met",
|
|
940
|
-
`${
|
|
941
|
-
|
|
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)
|
|
942
1625
|
);
|
|
943
1626
|
}
|
|
944
|
-
function
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
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() };
|
|
962
1663
|
}
|
|
963
1664
|
}
|
|
964
|
-
function
|
|
965
|
-
|
|
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) {
|
|
2950
|
+
if (!control.allow_attestation) return null;
|
|
966
2951
|
const attestation = attestations?.attestations[control.id];
|
|
967
2952
|
if (!attestation) return null;
|
|
968
2953
|
if (attestation.expires_at && new Date(attestation.expires_at) < now) return null;
|
|
@@ -974,40 +2959,127 @@ function activeAgentEvidence(control, claim, now) {
|
|
|
974
2959
|
if (new Date(claim.expires_at) < now) return null;
|
|
975
2960
|
return claim;
|
|
976
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
|
+
}
|
|
977
3076
|
async function evaluateControl(context, control, attestations, agentClaim, now = /* @__PURE__ */ new Date()) {
|
|
978
3077
|
const evidence = await Promise.all(
|
|
979
3078
|
control.evidence.map(async (check) => evaluateCheck(context, check))
|
|
980
3079
|
);
|
|
981
3080
|
const attestation = activeAttestation(control, attestations, now);
|
|
982
3081
|
const agentEvidence = activeAgentEvidence(control, agentClaim, now);
|
|
983
|
-
const
|
|
984
|
-
const hasManualCheck = control.evidence.some(({ type }) => type === "manual");
|
|
985
|
-
let status = checksPassed ? "met" : "not_met";
|
|
986
|
-
let confidence = checksPassed && !hasManualCheck ? "repository-detected" : "none";
|
|
987
|
-
const attestationStatus = attestation?.status === "unknown" ? null : attestation?.status ?? null;
|
|
988
|
-
const hasExternalConflict = agentEvidence !== null && agentEvidence.status !== "unknown" && attestationStatus !== null && agentEvidence.status !== attestationStatus;
|
|
989
|
-
if (checksPassed && !hasManualCheck) {
|
|
990
|
-
} else if (hasExternalConflict) {
|
|
991
|
-
status = "unknown";
|
|
992
|
-
confidence = "none";
|
|
993
|
-
} else if (agentEvidence && agentEvidence.status !== "unknown") {
|
|
994
|
-
status = agentEvidence.status;
|
|
995
|
-
confidence = "agent-collected";
|
|
996
|
-
} else if (attestation?.status === "not_applicable") {
|
|
997
|
-
status = "not_applicable";
|
|
998
|
-
confidence = "attested";
|
|
999
|
-
} else if (attestation?.status === "met") {
|
|
1000
|
-
status = "met";
|
|
1001
|
-
confidence = "attested";
|
|
1002
|
-
} else if (attestation?.status === "not_met" || attestation?.status === "unknown") {
|
|
1003
|
-
status = attestation.status;
|
|
1004
|
-
confidence = "attested";
|
|
1005
|
-
} else if (agentEvidence?.status === "unknown") {
|
|
1006
|
-
status = "unknown";
|
|
1007
|
-
confidence = "agent-collected";
|
|
1008
|
-
} else if (evidence.some(({ status: checkStatus }) => checkStatus === "unknown")) {
|
|
1009
|
-
status = "unknown";
|
|
1010
|
-
}
|
|
3082
|
+
const { confidence, status } = resolveControl(control, evidence, attestation, agentEvidence);
|
|
1011
3083
|
return {
|
|
1012
3084
|
id: control.id,
|
|
1013
3085
|
dimension: control.dimension,
|
|
@@ -1017,6 +3089,7 @@ async function evaluateControl(context, control, attestations, agentClaim, now =
|
|
|
1017
3089
|
risk: control.risk,
|
|
1018
3090
|
status,
|
|
1019
3091
|
confidence,
|
|
3092
|
+
...control.evidence_mode === "any" ? { evidence_mode: "any" } : {},
|
|
1020
3093
|
evidence,
|
|
1021
3094
|
agent_evidence: agentEvidence,
|
|
1022
3095
|
attestation,
|
|
@@ -1028,6 +3101,15 @@ async function evaluateControl(context, control, attestations, agentClaim, now =
|
|
|
1028
3101
|
function controlPasses(control) {
|
|
1029
3102
|
return control.status === "met" || control.status === "not_applicable";
|
|
1030
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
|
+
}
|
|
1031
3113
|
function dimensionScore(controls, dimension) {
|
|
1032
3114
|
let score = 0;
|
|
1033
3115
|
for (const level of [1, 2, 3, 4]) {
|
|
@@ -1039,6 +3121,10 @@ function dimensionScore(controls, dimension) {
|
|
|
1039
3121
|
}
|
|
1040
3122
|
return score;
|
|
1041
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
|
+
}
|
|
1042
3128
|
function repositoryScore(catalog, results, dimensions) {
|
|
1043
3129
|
let achieved = 0;
|
|
1044
3130
|
let ceiling = 0;
|
|
@@ -1052,9 +3138,7 @@ function repositoryScore(catalog, results, dimensions) {
|
|
|
1052
3138
|
const controlsAtLevel = catalog.filter(
|
|
1053
3139
|
(control) => control.dimension === dimension && control.level === level
|
|
1054
3140
|
);
|
|
1055
|
-
const repositoryDetectable = controlsAtLevel.every(
|
|
1056
|
-
(control) => control.evidence.every((evidence) => evidence.scope === "repository")
|
|
1057
|
-
);
|
|
3141
|
+
const repositoryDetectable = controlsAtLevel.every(isRepositoryDetectable);
|
|
1058
3142
|
if (ceilingOpen && repositoryDetectable) {
|
|
1059
3143
|
dimensionCeiling = level;
|
|
1060
3144
|
} else {
|
|
@@ -1104,7 +3188,7 @@ function assessProfiles(benchmark, dimensions, controls) {
|
|
|
1104
3188
|
title: profile.title,
|
|
1105
3189
|
passed: blockers.length === 0,
|
|
1106
3190
|
blockers,
|
|
1107
|
-
...benchmark.version
|
|
3191
|
+
...usesModernEvidence(benchmark.version) ? {
|
|
1108
3192
|
evidence_dependencies: {
|
|
1109
3193
|
agent_collected: requiredControls.filter(
|
|
1110
3194
|
({ confidence }) => confidence === "agent-collected"
|
|
@@ -1124,7 +3208,10 @@ async function assess(repo, benchmark, catalog, profileId, options = {}) {
|
|
|
1124
3208
|
}
|
|
1125
3209
|
const scope = options.scope ?? "tracked";
|
|
1126
3210
|
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
3211
|
+
const modernEvidence = usesModernEvidence(benchmark.version);
|
|
3212
|
+
const schemaVersion = reportSchemaVersion(benchmark.version);
|
|
1127
3213
|
const context = await createRepositoryContext(repo, scope, options.excludedPaths);
|
|
3214
|
+
validateAttestations(benchmark, catalog, context, options.attestations ?? null, now);
|
|
1128
3215
|
await validateAgentEvidence(benchmark, catalog, context, options.agentEvidence ?? null, now);
|
|
1129
3216
|
const controls = await Promise.all(
|
|
1130
3217
|
catalog.map(
|
|
@@ -1138,7 +3225,7 @@ async function assess(repo, benchmark, catalog, profileId, options = {}) {
|
|
|
1138
3225
|
)
|
|
1139
3226
|
);
|
|
1140
3227
|
const warnings = [...options.warnings ?? []];
|
|
1141
|
-
if (
|
|
3228
|
+
if (modernEvidence) {
|
|
1142
3229
|
if (scope === "tracked" && context.metadata.tracked_tree_dirty) {
|
|
1143
3230
|
warnings.push(
|
|
1144
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."
|
|
@@ -1170,11 +3257,11 @@ async function assess(repo, benchmark, catalog, profileId, options = {}) {
|
|
|
1170
3257
|
});
|
|
1171
3258
|
const profiles = assessProfiles(benchmark, dimensions, controls);
|
|
1172
3259
|
const total = dimensions.reduce((sum, { score }) => sum + score, 0);
|
|
1173
|
-
const repository =
|
|
3260
|
+
const repository = modernEvidence ? repositoryScore(catalog, controls, benchmark.dimensions) : void 0;
|
|
1174
3261
|
const highestProfile = [...profiles].reverse().find(({ passed }) => passed)?.id ?? null;
|
|
1175
3262
|
const targetPassed = profiles.find(({ id }) => id === profileId)?.passed ?? false;
|
|
1176
3263
|
return {
|
|
1177
|
-
schema_version:
|
|
3264
|
+
schema_version: schemaVersion,
|
|
1178
3265
|
benchmark: { id: benchmark.id, version: benchmark.version },
|
|
1179
3266
|
target: {
|
|
1180
3267
|
repository: repo,
|
|
@@ -1185,7 +3272,7 @@ async function assess(repo, benchmark, catalog, profileId, options = {}) {
|
|
|
1185
3272
|
working_tree_dirty: context.metadata.working_tree_dirty
|
|
1186
3273
|
},
|
|
1187
3274
|
assessed_at: now.toISOString(),
|
|
1188
|
-
...
|
|
3275
|
+
...modernEvidence ? { warnings } : {},
|
|
1189
3276
|
score: {
|
|
1190
3277
|
total,
|
|
1191
3278
|
maximum: 40,
|
|
@@ -1204,7 +3291,7 @@ async function assess(repo, benchmark, catalog, profileId, options = {}) {
|
|
|
1204
3291
|
).length,
|
|
1205
3292
|
unmet: controls.filter(({ status }) => status === "not_met").length,
|
|
1206
3293
|
unknown: controls.filter(({ status }) => status === "unknown").length,
|
|
1207
|
-
...
|
|
3294
|
+
...modernEvidence ? {
|
|
1208
3295
|
resolved: controls.filter(({ status }) => status !== "unknown").length,
|
|
1209
3296
|
total: controls.length
|
|
1210
3297
|
} : {}
|
|
@@ -1216,7 +3303,7 @@ async function assess(repo, benchmark, catalog, profileId, options = {}) {
|
|
|
1216
3303
|
limitations: [
|
|
1217
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.",
|
|
1218
3305
|
"Repository-detected evidence proves a qualifying artifact match, not consistent practice or external enforcement.",
|
|
1219
|
-
...
|
|
3306
|
+
...modernEvidence ? [
|
|
1220
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.",
|
|
1221
3308
|
"Agent-collected repository evidence is semantic, target-bound, and source-backed but is not independently verified or relabelled as repository-detected."
|
|
1222
3309
|
] : [],
|
|
@@ -1225,6 +3312,38 @@ async function assess(repo, benchmark, catalog, profileId, options = {}) {
|
|
|
1225
3312
|
]
|
|
1226
3313
|
};
|
|
1227
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
|
+
}
|
|
1228
3347
|
async function validateAgentEvidence(benchmark, catalog, context, evidence, now) {
|
|
1229
3348
|
if (!evidence) return;
|
|
1230
3349
|
if (evidence.benchmark_version !== benchmark.version) {
|
|
@@ -1234,17 +3353,17 @@ async function validateAgentEvidence(benchmark, catalog, context, evidence, now)
|
|
|
1234
3353
|
}
|
|
1235
3354
|
const expectedTarget = repositoryEvidenceTarget(context.metadata);
|
|
1236
3355
|
if (evidence.target.repository !== expectedTarget.repository) {
|
|
1237
|
-
throw new Error(
|
|
1238
|
-
`Agent evidence target ${evidence.target.repository} does not match ${expectedTarget.repository}`
|
|
1239
|
-
);
|
|
3356
|
+
throw new Error("Agent evidence target does not match the assessed repository");
|
|
1240
3357
|
}
|
|
1241
3358
|
if (evidence.target.git_head !== expectedTarget.git_head) {
|
|
1242
3359
|
throw new Error(
|
|
1243
3360
|
`Agent evidence commit ${evidence.target.git_head ?? "unavailable"} does not match ${expectedTarget.git_head ?? "an unavailable Git commit"}`
|
|
1244
3361
|
);
|
|
1245
3362
|
}
|
|
1246
|
-
if (benchmark.version
|
|
1247
|
-
throw new Error(
|
|
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
|
+
);
|
|
1248
3367
|
}
|
|
1249
3368
|
const controls = new Map(catalog.map((control) => [control.id, control]));
|
|
1250
3369
|
if (Object.keys(evidence.claims).length > 0 && (evidence.collector.name.startsWith("TODO") || evidence.collector.version.startsWith("TODO"))) {
|
|
@@ -1323,7 +3442,7 @@ function countLines(contents) {
|
|
|
1323
3442
|
|
|
1324
3443
|
// src/cli.ts
|
|
1325
3444
|
var program = new Command();
|
|
1326
|
-
program.name("agentic-scorecard").description("Evidence-backed readiness assessment for agentic software development harnesses").version("0.
|
|
3445
|
+
program.name("agentic-scorecard").description("Evidence-backed readiness assessment for agentic software development harnesses").version("0.4.0");
|
|
1327
3446
|
program.command("validate").description("Validate the bundled benchmark catalog").action(async () => {
|
|
1328
3447
|
const { benchmark, controls } = await loadBenchmark();
|
|
1329
3448
|
process.stdout.write(
|
|
@@ -1360,11 +3479,17 @@ program.command("init").argument("[repository]", "repository to initialize", "."
|
|
|
1360
3479
|
}
|
|
1361
3480
|
}
|
|
1362
3481
|
const { benchmark, controls } = await loadBenchmark();
|
|
3482
|
+
const context = await createRepositoryContext(repo, "workspace");
|
|
3483
|
+
const target = {
|
|
3484
|
+
repository: normalizeRepositoryTarget(repositoryEvidenceTarget(context.metadata).repository)
|
|
3485
|
+
};
|
|
1363
3486
|
const reviewedAt = /* @__PURE__ */ new Date();
|
|
1364
3487
|
const expiresAt = new Date(reviewedAt);
|
|
1365
3488
|
expiresAt.setDate(expiresAt.getDate() + 90);
|
|
1366
3489
|
const attestations = Object.fromEntries(
|
|
1367
|
-
controls.filter(
|
|
3490
|
+
controls.filter(
|
|
3491
|
+
(control) => control.allow_attestation && control.evidence.some(({ type }) => type === "manual")
|
|
3492
|
+
).map((control) => {
|
|
1368
3493
|
const manualCheck = control.evidence.find(
|
|
1369
3494
|
(check) => check.type === "manual"
|
|
1370
3495
|
);
|
|
@@ -1384,7 +3509,7 @@ program.command("init").argument("[repository]", "repository to initialize", "."
|
|
|
1384
3509
|
await writeFile(
|
|
1385
3510
|
path,
|
|
1386
3511
|
`# Claims are visibly human-attested. Link durable evidence; do not paste secrets.
|
|
1387
|
-
${stringify({ benchmark_version: benchmark.version, attestations })}`,
|
|
3512
|
+
${stringify({ benchmark_version: benchmark.version, target, attestations })}`,
|
|
1388
3513
|
"utf8"
|
|
1389
3514
|
);
|
|
1390
3515
|
process.stdout.write(`Created ${path}
|
|
@@ -1456,7 +3581,7 @@ ${stringify(bundle)}`,
|
|
|
1456
3581
|
await writeFile(
|
|
1457
3582
|
requestPath,
|
|
1458
3583
|
[
|
|
1459
|
-
|
|
3584
|
+
`# ADRB v${benchmark.version} evidence request`,
|
|
1460
3585
|
"",
|
|
1461
3586
|
`- Repository: ${bundle.target.repository}`,
|
|
1462
3587
|
`- Git commit: ${bundle.target.git_head ?? "unavailable"}`,
|