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