@vltpkg/query 1.0.0-rc.32 → 1.0.0-rc.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/dist/index.js +10 -6
  2. package/dist/parser.js +2 -1
  3. package/dist/pseudo/hostname.js +3 -1
  4. package/dist/pseudo/malware.d.ts +3 -16
  5. package/dist/pseudo/malware.js +11 -171
  6. package/dist/pseudo/scripts.js +1 -1
  7. package/dist/pseudo/vuln.d.ts +22 -0
  8. package/dist/pseudo/vuln.js +220 -0
  9. package/dist/pseudo.js +3 -3
  10. package/dist/types.d.ts +3 -2
  11. package/package.json +19 -10
  12. package/skills/dss-query/REFERENCE.md +123 -0
  13. package/skills/dss-query/SKILL.md +190 -0
  14. package/skills/dss-query/evals/README.md +79 -0
  15. package/skills/dss-query/evals/evals.json +89 -0
  16. package/skills/dss-query/evals/grade.mjs +480 -0
  17. package/src/attribute.ts +186 -0
  18. package/src/combinator.ts +132 -0
  19. package/src/id.ts +46 -0
  20. package/src/index.ts +620 -0
  21. package/src/parser.ts +122 -0
  22. package/src/pseudo/abandoned.ts +9 -0
  23. package/src/pseudo/attr.ts +92 -0
  24. package/src/pseudo/built.ts +19 -0
  25. package/src/pseudo/confused.ts +29 -0
  26. package/src/pseudo/cve.ts +93 -0
  27. package/src/pseudo/cwe.ts +97 -0
  28. package/src/pseudo/debug.ts +9 -0
  29. package/src/pseudo/deprecated.ts +9 -0
  30. package/src/pseudo/dev.ts +18 -0
  31. package/src/pseudo/diff.ts +90 -0
  32. package/src/pseudo/dist.ts +144 -0
  33. package/src/pseudo/dynamic.ts +9 -0
  34. package/src/pseudo/empty.ts +15 -0
  35. package/src/pseudo/entropic.ts +9 -0
  36. package/src/pseudo/env.ts +6 -0
  37. package/src/pseudo/eval.ts +9 -0
  38. package/src/pseudo/fs.ts +9 -0
  39. package/src/pseudo/helpers.ts +106 -0
  40. package/src/pseudo/host.ts +111 -0
  41. package/src/pseudo/hostname.ts +156 -0
  42. package/src/pseudo/license.ts +134 -0
  43. package/src/pseudo/link.ts +30 -0
  44. package/src/pseudo/malware.ts +41 -0
  45. package/src/pseudo/minified.ts +9 -0
  46. package/src/pseudo/missing.ts +16 -0
  47. package/src/pseudo/native.ts +9 -0
  48. package/src/pseudo/network.ts +9 -0
  49. package/src/pseudo/obfuscated.ts +9 -0
  50. package/src/pseudo/optional.ts +18 -0
  51. package/src/pseudo/outdated.ts +305 -0
  52. package/src/pseudo/overridden.ts +20 -0
  53. package/src/pseudo/path.ts +146 -0
  54. package/src/pseudo/peer.ts +18 -0
  55. package/src/pseudo/prerelease.ts +47 -0
  56. package/src/pseudo/private.ts +19 -0
  57. package/src/pseudo/prod.ts +18 -0
  58. package/src/pseudo/published.ts +248 -0
  59. package/src/pseudo/registry.ts +29 -0
  60. package/src/pseudo/root.ts +19 -0
  61. package/src/pseudo/scanned.ts +20 -0
  62. package/src/pseudo/score.ts +186 -0
  63. package/src/pseudo/scripts.ts +54 -0
  64. package/src/pseudo/semver.ts +320 -0
  65. package/src/pseudo/severity.ts +231 -0
  66. package/src/pseudo/shell.ts +9 -0
  67. package/src/pseudo/shrinkwrap.ts +9 -0
  68. package/src/pseudo/spec.ts +131 -0
  69. package/src/pseudo/squat.ts +225 -0
  70. package/src/pseudo/suspicious.ts +9 -0
  71. package/src/pseudo/tracker.ts +9 -0
  72. package/src/pseudo/trivial.ts +9 -0
  73. package/src/pseudo/type.ts +26 -0
  74. package/src/pseudo/undesirable.ts +9 -0
  75. package/src/pseudo/unknown.ts +9 -0
  76. package/src/pseudo/unmaintained.ts +9 -0
  77. package/src/pseudo/unpopular.ts +9 -0
  78. package/src/pseudo/unstable.ts +9 -0
  79. package/src/pseudo/vuln.ts +273 -0
  80. package/src/pseudo/workspace.ts +22 -0
  81. package/src/pseudo.ts +413 -0
  82. package/src/types.ts +165 -0
  83. package/tap-snapshots/test/attribute.ts.test.cjs +304 -0
  84. package/tap-snapshots/test/combinator.ts.test.cjs +333 -0
  85. package/tap-snapshots/test/id.ts.test.cjs +118 -0
  86. package/tap-snapshots/test/parser.ts.test.cjs +344 -0
  87. package/tap-snapshots/test/pseudo/abandoned.ts.test.cjs +18 -0
  88. package/tap-snapshots/test/pseudo/attr.ts.test.cjs +161 -0
  89. package/tap-snapshots/test/pseudo/built.ts.test.cjs +70 -0
  90. package/tap-snapshots/test/pseudo/confused.ts.test.cjs +32 -0
  91. package/tap-snapshots/test/pseudo/cve.ts.test.cjs +49 -0
  92. package/tap-snapshots/test/pseudo/cwe.ts.test.cjs +49 -0
  93. package/tap-snapshots/test/pseudo/debug.ts.test.cjs +18 -0
  94. package/tap-snapshots/test/pseudo/deprecated.ts.test.cjs +18 -0
  95. package/tap-snapshots/test/pseudo/dist.ts.test.cjs +30 -0
  96. package/tap-snapshots/test/pseudo/dynamic.ts.test.cjs +18 -0
  97. package/tap-snapshots/test/pseudo/empty.ts.test.cjs +50 -0
  98. package/tap-snapshots/test/pseudo/entropic.ts.test.cjs +18 -0
  99. package/tap-snapshots/test/pseudo/env.ts.test.cjs +18 -0
  100. package/tap-snapshots/test/pseudo/eval.ts.test.cjs +18 -0
  101. package/tap-snapshots/test/pseudo/fs.ts.test.cjs +18 -0
  102. package/tap-snapshots/test/pseudo/helpers.ts.test.cjs +18 -0
  103. package/tap-snapshots/test/pseudo/hostname.ts.test.cjs +148 -0
  104. package/tap-snapshots/test/pseudo/license.ts.test.cjs +41 -0
  105. package/tap-snapshots/test/pseudo/link.ts.test.cjs +36 -0
  106. package/tap-snapshots/test/pseudo/malware.ts.test.cjs +22 -0
  107. package/tap-snapshots/test/pseudo/minified.ts.test.cjs +18 -0
  108. package/tap-snapshots/test/pseudo/missing.ts.test.cjs +30 -0
  109. package/tap-snapshots/test/pseudo/native.ts.test.cjs +18 -0
  110. package/tap-snapshots/test/pseudo/network.ts.test.cjs +18 -0
  111. package/tap-snapshots/test/pseudo/obfuscated.ts.test.cjs +18 -0
  112. package/tap-snapshots/test/pseudo/outdated.ts.test.cjs +130 -0
  113. package/tap-snapshots/test/pseudo/overridden.ts.test.cjs +75 -0
  114. package/tap-snapshots/test/pseudo/prerelease.ts.test.cjs +101 -0
  115. package/tap-snapshots/test/pseudo/private.ts.test.cjs +38 -0
  116. package/tap-snapshots/test/pseudo/published.ts.test.cjs +171 -0
  117. package/tap-snapshots/test/pseudo/registry.ts.test.cjs +68 -0
  118. package/tap-snapshots/test/pseudo/root.ts.test.cjs +24 -0
  119. package/tap-snapshots/test/pseudo/scanned.ts.test.cjs +18 -0
  120. package/tap-snapshots/test/pseudo/score.ts.test.cjs +239 -0
  121. package/tap-snapshots/test/pseudo/scripts.ts.test.cjs +61 -0
  122. package/tap-snapshots/test/pseudo/semver.ts.test.cjs +575 -0
  123. package/tap-snapshots/test/pseudo/severity.ts.test.cjs +95 -0
  124. package/tap-snapshots/test/pseudo/shell.ts.test.cjs +18 -0
  125. package/tap-snapshots/test/pseudo/shrinkwrap.ts.test.cjs +18 -0
  126. package/tap-snapshots/test/pseudo/spec.ts.test.cjs +148 -0
  127. package/tap-snapshots/test/pseudo/squat.ts.test.cjs +129 -0
  128. package/tap-snapshots/test/pseudo/suspicious.ts.test.cjs +18 -0
  129. package/tap-snapshots/test/pseudo/tracker.ts.test.cjs +18 -0
  130. package/tap-snapshots/test/pseudo/trivial.ts.test.cjs +18 -0
  131. package/tap-snapshots/test/pseudo/type.ts.test.cjs +51 -0
  132. package/tap-snapshots/test/pseudo/undesirable.ts.test.cjs +18 -0
  133. package/tap-snapshots/test/pseudo/unknown.ts.test.cjs +18 -0
  134. package/tap-snapshots/test/pseudo/unmaintained.ts.test.cjs +18 -0
  135. package/tap-snapshots/test/pseudo/unpopular.ts.test.cjs +18 -0
  136. package/tap-snapshots/test/pseudo/unstable.ts.test.cjs +18 -0
  137. package/tap-snapshots/test/pseudo/vuln.ts.test.cjs +208 -0
  138. package/tap-snapshots/test/pseudo/vulnerable.ts.test.cjs +20 -0
  139. package/tap-snapshots/test/pseudo/workspace.ts.test.cjs +19 -0
  140. package/tap-snapshots/test/pseudo.ts.test.cjs +1247 -0
  141. package/test/attribute.ts +266 -0
  142. package/test/combinator.ts +168 -0
  143. package/test/fixtures/graph.ts +999 -0
  144. package/test/fixtures/selector.ts +103 -0
  145. package/test/fixtures/types.ts +7 -0
  146. package/test/id.ts +102 -0
  147. package/test/index.ts +1000 -0
  148. package/test/parser.ts +117 -0
  149. package/test/pseudo/abandoned.ts +98 -0
  150. package/test/pseudo/attr.ts +298 -0
  151. package/test/pseudo/built.ts +214 -0
  152. package/test/pseudo/confused.ts +160 -0
  153. package/test/pseudo/cve.ts +249 -0
  154. package/test/pseudo/cwe.ts +255 -0
  155. package/test/pseudo/debug.ts +98 -0
  156. package/test/pseudo/deprecated.ts +98 -0
  157. package/test/pseudo/dev.ts +61 -0
  158. package/test/pseudo/diff.ts +417 -0
  159. package/test/pseudo/dist.ts +215 -0
  160. package/test/pseudo/dynamic.ts +98 -0
  161. package/test/pseudo/empty.ts +108 -0
  162. package/test/pseudo/entropic.ts +101 -0
  163. package/test/pseudo/env.ts +98 -0
  164. package/test/pseudo/eval.ts +98 -0
  165. package/test/pseudo/fs.ts +98 -0
  166. package/test/pseudo/helpers.ts +370 -0
  167. package/test/pseudo/host-context.ts +276 -0
  168. package/test/pseudo/hostname.ts +314 -0
  169. package/test/pseudo/license.ts +265 -0
  170. package/test/pseudo/link.ts +94 -0
  171. package/test/pseudo/malware.ts +184 -0
  172. package/test/pseudo/minified.ts +98 -0
  173. package/test/pseudo/missing.ts +111 -0
  174. package/test/pseudo/native.ts +98 -0
  175. package/test/pseudo/network.ts +98 -0
  176. package/test/pseudo/obfuscated.ts +98 -0
  177. package/test/pseudo/optional.ts +73 -0
  178. package/test/pseudo/outdated.ts +331 -0
  179. package/test/pseudo/overridden.ts +317 -0
  180. package/test/pseudo/path.ts +680 -0
  181. package/test/pseudo/peer.ts +101 -0
  182. package/test/pseudo/prerelease.ts +279 -0
  183. package/test/pseudo/private.ts +108 -0
  184. package/test/pseudo/prod.ts +61 -0
  185. package/test/pseudo/published.ts +557 -0
  186. package/test/pseudo/registry.ts +122 -0
  187. package/test/pseudo/root.ts +66 -0
  188. package/test/pseudo/scanned.ts +78 -0
  189. package/test/pseudo/score.ts +591 -0
  190. package/test/pseudo/scripts.ts +294 -0
  191. package/test/pseudo/semver.ts +822 -0
  192. package/test/pseudo/severity.ts +310 -0
  193. package/test/pseudo/shell.ts +98 -0
  194. package/test/pseudo/shrinkwrap.ts +98 -0
  195. package/test/pseudo/spec.ts +525 -0
  196. package/test/pseudo/squat.ts +565 -0
  197. package/test/pseudo/suspicious.ts +98 -0
  198. package/test/pseudo/tracker.ts +98 -0
  199. package/test/pseudo/trivial.ts +98 -0
  200. package/test/pseudo/type.ts +105 -0
  201. package/test/pseudo/undesirable.ts +98 -0
  202. package/test/pseudo/unknown.ts +98 -0
  203. package/test/pseudo/unmaintained.ts +98 -0
  204. package/test/pseudo/unpopular.ts +98 -0
  205. package/test/pseudo/unstable.ts +98 -0
  206. package/test/pseudo/vuln.ts +625 -0
  207. package/test/pseudo/vulnerable.ts +137 -0
  208. package/test/pseudo/workspace.ts +111 -0
  209. package/test/pseudo.ts +558 -0
  210. package/tsconfig.json +3 -0
  211. package/tsconfig.publish.json +19 -0
  212. package/tsconfig.publish.tsbuildinfo +1 -0
  213. package/typedoc.mjs +2 -0
  214. package/dist/pseudo/vulnerable.d.ts +0 -8
  215. package/dist/pseudo/vulnerable.js +0 -17
@@ -0,0 +1,480 @@
1
+ /**
2
+ * Deterministic grader for dss-query skill evals.
3
+ * Run from src/query (so @vltpkg/* workspace deps resolve):
4
+ * node skills/dss-query/evals/grade.mjs skills/dss-query-workspace/iteration-1
5
+ *
6
+ * Writes grading.json ({expectations: [{text, passed, evidence}]}) into each
7
+ * <eval>/<variant>/ directory.
8
+ */
9
+ import {
10
+ readFileSync,
11
+ writeFileSync,
12
+ readdirSync,
13
+ existsSync,
14
+ } from 'node:fs'
15
+ import { join } from 'node:path'
16
+ import { parse } from '@vltpkg/dss-parser'
17
+ import { Query } from '../../../src/index.ts'
18
+ import { getSimpleGraph } from '../../../test/fixtures/graph.ts'
19
+
20
+ const iterDir = process.argv[2]
21
+ if (!iterDir) throw new Error('usage: node grade.mjs <iteration-dir>')
22
+
23
+ // ---- selector extraction -------------------------------------------------
24
+ const extractSelectors = text => {
25
+ const out = new Set()
26
+ // vlt query '<sel>' / "<sel>" anywhere in the text
27
+ for (const m of text.matchAll(/vlt query '([^']+)'/g)) out.add(m[1])
28
+ for (const m of text.matchAll(/vlt query "([^"]+)"/g)) out.add(m[1])
29
+ // bare selectors on their own line inside fenced code blocks
30
+ for (const block of text.matchAll(/```[a-z]*\n([\s\S]*?)```/g)) {
31
+ for (const line of block[1].split('\n')) {
32
+ const t = line.trim()
33
+ // '# ...' is a comment, not an ID selector (real IDs: '#react', no space)
34
+ if (/^# /.test(t)) continue
35
+ if (/^[:#[*]/.test(t) && !t.includes('<')) out.add(t)
36
+ }
37
+ }
38
+ return [...out].filter(s => !s.includes('<'))
39
+ }
40
+
41
+ // ---- execution (offline only) ---------------------------------------------
42
+ // :outdated fetches registry packuments; security selectors need the archive.
43
+ const NO_EXEC = /:outdated|:missing/
44
+ const skipExec = s => Query.hasSecuritySelectors(s) || NO_EXEC.test(s)
45
+ // split top-level commas so loose mode can't mask invalid segments
46
+ const topLevelSplit = s => {
47
+ const parts = []
48
+ let depth = 0,
49
+ cur = ''
50
+ for (const ch of s) {
51
+ if (ch === '(' || ch === '[') depth++
52
+ else if (ch === ')' || ch === ']') depth--
53
+ if (ch === ',' && depth === 0) {
54
+ parts.push(cur)
55
+ cur = ''
56
+ } else cur += ch
57
+ }
58
+ parts.push(cur)
59
+ return parts.map(p => p.trim()).filter(Boolean)
60
+ }
61
+ const mockSignal = { throwIfAborted: () => {} }
62
+ const makeQuery = () => {
63
+ const graph = getSimpleGraph()
64
+ return new Query({
65
+ nodes: new Set(graph.nodes.values()),
66
+ edges: graph.edges,
67
+ importers: graph.importers,
68
+ securityArchive: undefined,
69
+ })
70
+ }
71
+
72
+ const parseAll = selectors => {
73
+ const failures = []
74
+ for (const s of selectors) {
75
+ try {
76
+ parse(s)
77
+ } catch (err) {
78
+ failures.push(`${s} → ${err.message}`)
79
+ }
80
+ }
81
+ return failures
82
+ }
83
+
84
+ const execAll = async selectors => {
85
+ const failures = []
86
+ const skipped = []
87
+ for (const s of selectors) {
88
+ if (skipExec(s)) {
89
+ skipped.push(s)
90
+ continue
91
+ }
92
+ for (const seg of topLevelSplit(s)) {
93
+ try {
94
+ await makeQuery().search(seg, { signal: mockSignal })
95
+ } catch (err) {
96
+ failures.push(`${seg} → ${err.message}`)
97
+ }
98
+ }
99
+ }
100
+ return { failures, skipped }
101
+ }
102
+
103
+ // ---- per-eval assertion logic ----------------------------------------------
104
+ const first = (arr, re) => arr.find(s => re.test(s))
105
+ const preCodeText = text =>
106
+ text.slice(0, Math.max(text.indexOf('```'), 0)).trim()
107
+
108
+ const graders = {
109
+ 0: async (text, sels) => {
110
+ const hit = first(sels, /:root\s*>\s*\*?:outdated/)
111
+ const parseFails = parseAll(sels)
112
+ const { failures: execFails, skipped } = await execAll(sels)
113
+ const pre = preCodeText(text)
114
+ return [
115
+ {
116
+ text: 'proposes-root-outdated-selector',
117
+ passed: !!hit,
118
+ evidence: hit ?? `selectors: ${sels.join(' | ') || '(none)'}`,
119
+ },
120
+ {
121
+ text: 'all-selectors-parse',
122
+ passed: parseFails.length === 0,
123
+ evidence:
124
+ parseFails.join('; ') || `${sels.length} selectors parsed`,
125
+ },
126
+ {
127
+ text: 'selector-executes-on-fixture-graph',
128
+ passed: execFails.length === 0,
129
+ evidence:
130
+ execFails.join('; ') ||
131
+ `executed ${sels.length - skipped.length}, skipped (network-dependent): ${skipped.length}`,
132
+ },
133
+ {
134
+ text: 'query-first-style',
135
+ passed: pre.length < 150,
136
+ evidence:
137
+ pre.length < 150 ?
138
+ `only ${pre.length} chars before first code block`
139
+ : `${pre.length} chars of preamble before first code block: "${pre.slice(0, 80)}..."`,
140
+ },
141
+ {
142
+ text: 'mentions-view-flag',
143
+ passed: /--view/.test(text),
144
+ evidence:
145
+ /--view/.test(text) ?
146
+ `matched "${text.match(/--view\S*/)[0]}"`
147
+ : 'no --view mention',
148
+ },
149
+ {
150
+ text: 'offers-related-queries',
151
+ passed: sels.length >= 2,
152
+ evidence: `${sels.length} distinct selectors offered`,
153
+ },
154
+ ]
155
+ },
156
+ 1: async (text, sels) => {
157
+ const attr = first(sels, /\[license\^?=\s*['"]?MIT/i)
158
+ const badPseudo = /:license\(\s*['"]?mit/i.test(text)
159
+ const anchored = first(sels, /:workspace\s*>\s*:prod/)
160
+ const parseFails = parseAll(sels)
161
+ return [
162
+ {
163
+ text: 'uses-license-attribute-form',
164
+ passed: !!attr,
165
+ evidence:
166
+ attr ?? `selectors: ${sels.join(' | ') || '(none)'}`,
167
+ },
168
+ {
169
+ text: 'avoids-invalid-license-pseudo',
170
+ passed: !badPseudo,
171
+ evidence:
172
+ badPseudo ?
173
+ 'answer proposes :license(mit), which throws "Expected a valid license kind"'
174
+ : 'no :license(mit) present',
175
+ },
176
+ {
177
+ text: 'workspace-prod-anchored',
178
+ passed: !!anchored,
179
+ evidence:
180
+ anchored ?? `selectors: ${sels.join(' | ') || '(none)'}`,
181
+ },
182
+ {
183
+ text: 'all-selectors-parse',
184
+ passed: parseFails.length === 0,
185
+ evidence:
186
+ parseFails.join('; ') || `${sels.length} selectors parsed`,
187
+ },
188
+ {
189
+ text: 'offers-related-queries',
190
+ passed: sels.length >= 2,
191
+ evidence: `${sels.length} distinct selectors offered`,
192
+ },
193
+ ]
194
+ },
195
+ 2: async (text, sels) => {
196
+ const inv = first(
197
+ sels,
198
+ /:has\(\s*>\s*(#react|\[name=react\])\s*\)/,
199
+ )
200
+ const correction =
201
+ /(#react\s*>\s*\*|wrong way|opposite direction|points? the wrong|selects? react'?s (own )?dependenc)/i.test(
202
+ text,
203
+ ) && /dependenc/i.test(text)
204
+ // the recommended (first extracted) selector must not be the wrong-direction one
205
+ const firstSel = sels[0] ?? ''
206
+ const wrongAsAnswer = /^#react\s*>\s*\*$/.test(firstSel)
207
+ const parseFails = parseAll(sels)
208
+ const evidenceLine =
209
+ text
210
+ .split('\n')
211
+ .find(l => /wrong|opposite|dependenc/i.test(l)) ?? ''
212
+ return [
213
+ {
214
+ text: 'uses-has-inversion',
215
+ passed: !!inv,
216
+ evidence: inv ?? `selectors: ${sels.join(' | ') || '(none)'}`,
217
+ },
218
+ {
219
+ text: 'corrects-direction-mistake',
220
+ passed: correction,
221
+ evidence:
222
+ evidenceLine.trim().slice(0, 160) ||
223
+ 'no direction-correction language found',
224
+ },
225
+ {
226
+ text: 'does-not-present-wrong-selector-as-answer',
227
+ passed: !wrongAsAnswer,
228
+ evidence: `first recommended selector: ${firstSel || '(none)'}`,
229
+ },
230
+ {
231
+ text: 'all-selectors-parse',
232
+ passed: parseFails.length === 0,
233
+ evidence:
234
+ parseFails.join('; ') || `${sels.length} selectors parsed`,
235
+ },
236
+ {
237
+ text: 'explains-combinator-rationale',
238
+ passed:
239
+ /point.{0,40}dependenc|dependent\s*(→|->|to)\s*dependenc|parent.{0,12}child/i.test(
240
+ text,
241
+ ),
242
+ evidence: (
243
+ text.split('\n').find(l => /point|parent|→|->/.test(l)) ??
244
+ 'no general direction rule stated'
245
+ )
246
+ .trim()
247
+ .slice(0, 160),
248
+ },
249
+ ]
250
+ },
251
+ 3: async text => {
252
+ const checks = [
253
+ ['covers-workspace-anchor', /workspace/i],
254
+ ['covers-direct-prod-segment', null], // special: needs both
255
+ ['covers-cve-segment', /cve/i],
256
+ ['mentions-security-data-source', /socket|network/i],
257
+ ]
258
+ return checks.map(([name, re]) => {
259
+ if (name === 'covers-direct-prod-segment') {
260
+ const ok = /direct/i.test(text) && /prod/i.test(text)
261
+ return {
262
+ text: name,
263
+ passed: ok,
264
+ evidence:
265
+ ok ?
266
+ 'mentions both "direct" and "prod"'
267
+ : `direct:${/direct/i.test(text)} prod:${/prod/i.test(text)}`,
268
+ }
269
+ }
270
+ const m = text.match(re)
271
+ return {
272
+ text: name,
273
+ passed: !!m,
274
+ evidence: m ? `matched "${m[0]}"` : `no match for ${re}`,
275
+ }
276
+ })
277
+ },
278
+ 4: async (text, sels) => {
279
+ const threat =
280
+ /:malware/.test(text) &&
281
+ /:squat|:obfuscated|:suspicious/.test(text)
282
+ const parseFails = parseAll(sels)
283
+ return [
284
+ {
285
+ text: 'uses-threat-selectors',
286
+ passed: threat,
287
+ evidence:
288
+ threat ?
289
+ `selectors: ${sels.join(' | ')}`
290
+ : `malware:${/:malware/.test(text)} squat/obf/susp:${/:squat|:obfuscated|:suspicious/.test(text)}`,
291
+ },
292
+ {
293
+ text: 'all-selectors-parse',
294
+ passed: parseFails.length === 0,
295
+ evidence:
296
+ parseFails.join('; ') || `${sels.length} selectors parsed`,
297
+ },
298
+ {
299
+ text: 'mentions-security-data-source',
300
+ passed: /socket|network/i.test(text),
301
+ evidence:
302
+ text.match(/socket|network/i)?.[0] ??
303
+ 'no Socket/network mention',
304
+ },
305
+ {
306
+ text: 'offers-related-queries',
307
+ passed: sels.length >= 2,
308
+ evidence: `${sels.length} distinct selectors offered`,
309
+ },
310
+ ]
311
+ },
312
+ 5: async (text, sels) => {
313
+ const cap =
314
+ /:dev[^\s,']*:(shell|network)|:dev:is\([^)]*:(shell|network)/.test(
315
+ text,
316
+ )
317
+ const both = /:shell/.test(text) && /:network/.test(text)
318
+ const parseFails = parseAll(sels)
319
+ return [
320
+ {
321
+ text: 'uses-capability-selectors',
322
+ passed: cap,
323
+ evidence:
324
+ cap ?
325
+ `selectors: ${sels.join(' | ')}`
326
+ : `selectors: ${sels.join(' | ') || '(none)'}`,
327
+ },
328
+ {
329
+ text: 'covers-both-capabilities',
330
+ passed: both,
331
+ evidence: `shell:${/:shell/.test(text)} network:${/:network/.test(text)}`,
332
+ },
333
+ {
334
+ text: 'all-selectors-parse',
335
+ passed: parseFails.length === 0,
336
+ evidence:
337
+ parseFails.join('; ') || `${sels.length} selectors parsed`,
338
+ },
339
+ {
340
+ text: 'explains-capability-meaning',
341
+ passed: /socket|static|capab|can\s/i.test(text),
342
+ evidence:
343
+ text.match(/socket|static|capab|can\s\S*/i)?.[0] ??
344
+ 'no capability explanation',
345
+ },
346
+ ]
347
+ },
348
+ 6: async (text, sels) => {
349
+ const sev =
350
+ first(sels, /:sev(erity)?\(/) ??
351
+ (/:sev(erity)?\(/.test(text) ?
352
+ text.match(/:sev(erity)?\([^)]*\)/)[0]
353
+ : undefined)
354
+ const anchored = /:workspace\s*>\s*:prod/.test(text)
355
+ const parseFails = parseAll(sels)
356
+ return [
357
+ {
358
+ text: 'uses-severity-filter',
359
+ passed: !!sev,
360
+ evidence: sev ?? `selectors: ${sels.join(' | ') || '(none)'}`,
361
+ },
362
+ {
363
+ text: 'anchors-workspace-prod',
364
+ passed: anchored,
365
+ evidence:
366
+ anchored ?
367
+ text.match(/:workspace\s*>\s*:prod\S*/)[0]
368
+ : `selectors: ${sels.join(' | ') || '(none)'}`,
369
+ },
370
+ {
371
+ text: 'all-selectors-parse',
372
+ passed: parseFails.length === 0,
373
+ evidence:
374
+ parseFails.join('; ') || `${sels.length} selectors parsed`,
375
+ },
376
+ {
377
+ text: 'mentions-security-data-source',
378
+ passed: /socket|network/i.test(text),
379
+ evidence:
380
+ text.match(/socket|network/i)?.[0] ??
381
+ 'no Socket/network mention',
382
+ },
383
+ ]
384
+ },
385
+ }
386
+
387
+ // ---- round-3 style assertions (Examples tail, docs deep links, concision) ---
388
+ const examplesHeading = text => ({
389
+ text: 'uses-examples-heading',
390
+ passed: /Examples:/.test(text) && !/Related:/.test(text),
391
+ evidence: `Examples: ${/Examples:/.test(text)}, Related: ${/Related:/.test(text)}`,
392
+ })
393
+ const docsLink = re => text => ({
394
+ text: 'ends-with-docs-link',
395
+ passed: re.test(text),
396
+ evidence: text.match(re)?.[0] ?? `no link matching ${re}`,
397
+ })
398
+ const conciseAnswer = max => text => {
399
+ const lines = text.split('\n').filter(l => l.trim()).length
400
+ return {
401
+ text: 'concise-answer',
402
+ passed: lines <= max,
403
+ evidence: `${lines} non-empty lines (max ${max})`,
404
+ }
405
+ }
406
+ const SELECTORS_DOC = /docs\.vlt\.io\/cli\/selectors/
407
+ const round3Extras = {
408
+ 0: [examplesHeading, docsLink(SELECTORS_DOC)],
409
+ 1: [
410
+ examplesHeading,
411
+ // license query: either the structural selectors page or the
412
+ // license-compliance section is a correct deep link
413
+ docsLink(
414
+ /docs\.vlt\.io\/cli\/(selectors|security#license-compliance)/,
415
+ ),
416
+ ],
417
+ 2: [docsLink(SELECTORS_DOC)],
418
+ 3: [
419
+ docsLink(/docs\.vlt\.io\/cli\/security#vulnerability-detection/),
420
+ ],
421
+ 4: [docsLink(/docs\.vlt\.io\/cli\/security#malware-detection/)],
422
+ 5: [
423
+ conciseAnswer(22),
424
+ docsLink(
425
+ /docs\.vlt\.io\/cli\/security#behavioral-security-risks/,
426
+ ),
427
+ ],
428
+ 6: [
429
+ examplesHeading,
430
+ docsLink(/docs\.vlt\.io\/cli\/security#vulnerability-detection/),
431
+ ],
432
+ }
433
+
434
+ // ---- run --------------------------------------------------------------------
435
+ for (const evalDir of readdirSync(iterDir).filter(d =>
436
+ d.startsWith('eval-'),
437
+ )) {
438
+ const id = Number(evalDir.match(/^eval-(\d+)/)[1])
439
+ const variants = readdirSync(join(iterDir, evalDir), {
440
+ withFileTypes: true,
441
+ })
442
+ .filter(d => d.isDirectory())
443
+ .map(d => d.name)
444
+ for (const variant of variants) {
445
+ const answerPath = join(
446
+ iterDir,
447
+ evalDir,
448
+ variant,
449
+ 'run-1',
450
+ 'outputs',
451
+ 'answer.md',
452
+ )
453
+ if (!existsSync(answerPath)) continue
454
+ const text = readFileSync(answerPath, 'utf8')
455
+ const sels = extractSelectors(text)
456
+ const expectations = [
457
+ ...(await graders[id](text, sels)),
458
+ ...(round3Extras[id] ?? []).map(fn => fn(text)),
459
+ ]
460
+ const passed = expectations.filter(e => e.passed).length
461
+ writeFileSync(
462
+ join(iterDir, evalDir, variant, 'run-1', 'grading.json'),
463
+ JSON.stringify(
464
+ {
465
+ summary: {
466
+ total: expectations.length,
467
+ passed,
468
+ pass_rate: passed / expectations.length,
469
+ },
470
+ expectations,
471
+ },
472
+ null,
473
+ 2,
474
+ ) + '\n',
475
+ )
476
+ console.log(
477
+ `${evalDir}/${variant}: ${passed}/${expectations.length}`,
478
+ )
479
+ }
480
+ }
@@ -0,0 +1,186 @@
1
+ import { asAttributeNode } from '@vltpkg/dss-parser'
2
+ import { error } from '@vltpkg/error-cause'
3
+ import { removeDanglingEdges } from './pseudo/helpers.ts'
4
+ import type { NodeLike, JSONField, Manifest } from '@vltpkg/types'
5
+ import type { ParserState } from './types.ts'
6
+
7
+ export type ComparatorFn = (attr: string, value?: string) => boolean
8
+
9
+ // JSONField has a mapped type constituent that would coerce to [object Object]
10
+ // when stringified, which is what we want in this case.
11
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
12
+ const jsonFieldToString = (v: JSONField) => String(v)
13
+
14
+ /**
15
+ * Retrieve the {@link Manifest} values found at the given `properties`
16
+ * location for a given {@link Node}.
17
+ */
18
+ export const getManifestPropertyValues = (
19
+ node: NodeLike,
20
+ properties: string[],
21
+ attribute: string,
22
+ ): string[] | undefined => {
23
+ if (!node.manifest) return
24
+
25
+ const traverse = new Set<JSONField>([node.manifest])
26
+ const props = new Set<JSONField>()
27
+ for (const key of properties) {
28
+ for (const prop of traverse) {
29
+ /* c8 ignore start - should be impossible */
30
+ if (!prop) {
31
+ throw error('failed to find nested property in :attr', {
32
+ found: properties,
33
+ })
34
+ }
35
+ /* c8 ignore stop */
36
+
37
+ // expand the result list to include nested array values
38
+ if (Array.isArray(prop)) {
39
+ for (const p of prop) {
40
+ traverse.add(p)
41
+ }
42
+ continue
43
+ }
44
+
45
+ // guard for inspecting keys of objects next
46
+ if (
47
+ typeof prop === 'string' ||
48
+ typeof prop === 'number' ||
49
+ typeof prop === 'boolean'
50
+ ) {
51
+ continue
52
+ }
53
+
54
+ // assign next value when found
55
+ if (key in prop) {
56
+ const nextValue = prop[key]
57
+ if (nextValue) {
58
+ if (key === attribute) {
59
+ props.add(nextValue)
60
+ } else {
61
+ traverse.delete(prop)
62
+ traverse.add(nextValue)
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
68
+ // if no value was found after trying a given key
69
+ // then there's nothing to be collected
70
+ if (!props.size) return
71
+
72
+ // expand the result to include array values
73
+ const collect = new Set<string>()
74
+ for (const prop of props) {
75
+ if (Array.isArray(prop)) {
76
+ for (const p of prop) {
77
+ collect.add(p ? jsonFieldToString(p) : '')
78
+ }
79
+ } else {
80
+ collect.add(jsonFieldToString(prop))
81
+ }
82
+ }
83
+
84
+ return [...collect]
85
+ }
86
+
87
+ // decorator style of function that will filter `ParserState` results
88
+ // based on a provided `comparator` function
89
+ export const filterAttributes = (
90
+ state: ParserState,
91
+ comparator: ComparatorFn | undefined,
92
+ value: string,
93
+ propertyName: string,
94
+ insensitive: boolean,
95
+ prefixProperties: string[] = [],
96
+ ): ParserState => {
97
+ const check = (attr: JSONField) =>
98
+ comparator?.(
99
+ insensitive ?
100
+ jsonFieldToString(attr).toLowerCase()
101
+ : jsonFieldToString(attr),
102
+ insensitive ? value.toLowerCase() : value,
103
+ )
104
+ const deleteNode = (node: NodeLike) => {
105
+ for (const edge of node.edgesIn) {
106
+ state.partial.edges.delete(edge)
107
+ }
108
+ state.partial.nodes.delete(node)
109
+ }
110
+
111
+ for (const node of state.partial.nodes) {
112
+ const prefixes =
113
+ prefixProperties.length ? prefixProperties : [propertyName]
114
+ const attrs = getManifestPropertyValues(
115
+ node,
116
+ prefixes,
117
+ propertyName,
118
+ )
119
+
120
+ // if no attribute value was found, that means the attribute won't match
121
+ if (!attrs?.length) {
122
+ deleteNode(node)
123
+ continue
124
+ }
125
+
126
+ // if the node attribute value won't match, then remove the node
127
+ if (comparator && !attrs.some(check)) {
128
+ deleteNode(node)
129
+ }
130
+ }
131
+
132
+ removeDanglingEdges(state)
133
+ return state
134
+ }
135
+
136
+ // ref: https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors
137
+ const attributeSelectors: Record<string, ComparatorFn> = {
138
+ '=': (attr: string, value = '') => attr === value,
139
+ '^=': (attr: string, value = '') => attr.startsWith(value),
140
+ '$=': (attr: string, value = '') => attr.endsWith(value),
141
+ '~=': (attr: string, value = '') =>
142
+ new Set<string>(attr.match(/\w+/g)).has(value),
143
+ '*=': (attr: string, value = '') => attr.includes(value),
144
+ '|=': (attr: string, value = '') =>
145
+ attr === value || attr.startsWith(`${value}-`),
146
+ undefined: (attr: string) => !!attr,
147
+ }
148
+ export const attributeSelectorsMap = new Map<string, ComparatorFn>(
149
+ Object.entries(attributeSelectors),
150
+ )
151
+
152
+ /**
153
+ * Parse attributes selectors, e.g: `[name]`, `[name=value]`, etc
154
+ */
155
+ export const attribute = async (
156
+ state: ParserState,
157
+ ): Promise<ParserState> => {
158
+ await state.cancellable()
159
+
160
+ const curr = asAttributeNode(state.current)
161
+ const operatorFn = attributeSelectorsMap.get(String(curr.operator))
162
+ if (!operatorFn) {
163
+ if (state.loose) {
164
+ return state
165
+ }
166
+
167
+ throw error(`Unsupported attribute operator: ${curr.operator}`, {
168
+ found: state.current,
169
+ })
170
+ }
171
+
172
+ const value = curr.value || ''
173
+ const propertyName = curr.attribute
174
+ const insensitive = !!curr.insensitive
175
+
176
+ // Increment the commonCounter for specificity
177
+ state.specificity.commonCounter += 1
178
+
179
+ return filterAttributes(
180
+ state,
181
+ operatorFn,
182
+ value,
183
+ propertyName,
184
+ insensitive,
185
+ )
186
+ }