code-foundry 1.20.1 → 1.22.1

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 (44) hide show
  1. package/.github/CONTRIBUTING.md +5 -2
  2. package/.github/code-foundry.yml +1 -0
  3. package/.github/release-please-foundry.json +56 -0
  4. package/.github/workflows/ci.yml +61 -0
  5. package/.github/workflows/cloudflare-delivery.yml +9 -0
  6. package/.github/workflows/cloudflare-deploy.yml +8 -1
  7. package/.github/workflows/opencode-security_self-ci.yml +1 -1
  8. package/.github/workflows/qualified-foundry-publish.yml +6 -7
  9. package/.github/workflows/release.yml +58 -10
  10. package/.github/workflows/release_self-ci.yml +211 -8
  11. package/.github/workflows/test.yml +75 -0
  12. package/.github/workflows/validation-no-codeql.yml +7 -0
  13. package/.github/workflows/validation.yml +7 -0
  14. package/.gitignore +1 -1
  15. package/AGENTS.md +4 -1
  16. package/CHANGELOG.md +60 -0
  17. package/README.md +20 -17
  18. package/docs/CONFIGURATION.md +182 -152
  19. package/docs/EXTENSIONS.md +28 -7
  20. package/docs/INITIALIZATION.md +13 -9
  21. package/docs/PERFORMANCE.md +67 -59
  22. package/docs/PUBLISHING.md +39 -14
  23. package/docs/README.md +37 -22
  24. package/docs/RELEASES.md +18 -9
  25. package/docs/WORKFLOWS.md +11 -0
  26. package/docs/agent-validation.md +6 -5
  27. package/docs/cloudflare-delivery.md +20 -8
  28. package/docs/consumer-qualification.md +11 -9
  29. package/docs/fleet-release-eligibility.md +14 -15
  30. package/docs/fleet-rollouts.md +3 -3
  31. package/docs/merge-queues.md +21 -21
  32. package/docs/product-quality.md +9 -10
  33. package/docs/qualified-publication.md +166 -93
  34. package/docs/release-integrity.md +7 -6
  35. package/docs/required-capabilities.md +42 -15
  36. package/package.json +1 -1
  37. package/src/commands/cloudflare-delivery.mjs +6 -1
  38. package/src/commands/qualified-publication.mjs +23 -10
  39. package/src/commands/release-integrity.mjs +9 -0
  40. package/src/commands/sync.mjs +29 -24
  41. package/src/lib/product-quality.mjs +220 -16
  42. package/src/runtime-core.mjs +12 -8
  43. package/src/runtime.mjs +1 -1
  44. package/src/templates/gitignore +1 -1
@@ -81,21 +81,226 @@ function decode(value) {
81
81
  })
82
82
  }
83
83
 
84
+ /**
85
+ * Find an HTML comment terminator, including the parser's comment-end-bang
86
+ * form. An unterminated comment is treated as extending to the end of input.
87
+ * @param {string} html
88
+ * @param {number} start
89
+ * @returns {{ start: number, length: number } | null}
90
+ */
91
+ function findCommentEnd(html, start) {
92
+ const normal = html.indexOf('-->', start + 4)
93
+ const bang = html.indexOf('--!>', start + 4)
94
+ if (normal === -1 && bang === -1) return null
95
+ if (bang !== -1 && (normal === -1 || bang < normal)) return { start: bang, length: 4 }
96
+ return { start: normal, length: 3 }
97
+ }
98
+
99
+ /**
100
+ * Remove comments without leaving an unmatched comment opener that can expose
101
+ * markup to the lightweight tag reader.
102
+ * @param {string} html
103
+ */
104
+ function stripHtmlComments(html) {
105
+ let clean = ''
106
+ let cursor = 0
107
+ while (cursor < html.length) {
108
+ const start = html.indexOf('<!--', cursor)
109
+ if (start === -1) return clean + html.slice(cursor)
110
+ clean += html.slice(cursor, start)
111
+ const end = findCommentEnd(html, start)
112
+ if (!end) return clean
113
+ cursor = end.start + end.length
114
+ }
115
+ return clean
116
+ }
117
+
118
+ /** @param {string} html @param {number} start */
119
+ function findTagEnd(html, start) {
120
+ let quote = ''
121
+ for (let index = start + 1; index < html.length; index += 1) {
122
+ const character = html[index]
123
+ if (quote) {
124
+ if (character === quote) quote = ''
125
+ } else if (character === '"' || character === "'") {
126
+ quote = character
127
+ } else if (character === '>') {
128
+ return index
129
+ }
130
+ }
131
+ return -1
132
+ }
133
+
134
+ /** @param {string | undefined} character */
135
+ function isTagBoundary(character) {
136
+ return (
137
+ character === undefined || character === '>' || character === '/' || character.trim() === ''
138
+ )
139
+ }
140
+
141
+ /**
142
+ * Locate a named HTML tag while accepting parser-tolerated end-tag junk such
143
+ * as `</script\\t\\n data>`.
144
+ * @param {string} html
145
+ * @param {string} name
146
+ * @param {number} from
147
+ * @param {boolean} closing
148
+ * @returns {{ start: number, end: number } | null}
149
+ */
150
+ function findNamedTag(html, name, from, closing) {
151
+ const lowerName = name.toLowerCase()
152
+ for (let start = html.indexOf('<', from); start !== -1; start = html.indexOf('<', start + 1)) {
153
+ let nameStart = start + 1
154
+ if (closing) {
155
+ if (html[nameStart] !== '/') continue
156
+ nameStart += 1
157
+ } else if (html[nameStart] === '/' || html[nameStart] === '!' || html[nameStart] === '?') {
158
+ continue
159
+ }
160
+ if (html.slice(nameStart, nameStart + name.length).toLowerCase() !== lowerName) continue
161
+ if (!isTagBoundary(html[nameStart + name.length])) continue
162
+ return { start, end: findTagEnd(html, start) }
163
+ }
164
+ return null
165
+ }
166
+
167
+ const RAW_TEXT_TAGS = [
168
+ 'script',
169
+ 'style',
170
+ 'textarea',
171
+ 'template',
172
+ 'xmp',
173
+ 'iframe',
174
+ 'noembed',
175
+ 'noframes',
176
+ 'noscript',
177
+ ]
178
+
179
+ /**
180
+ * @param {string} html
181
+ * @param {number} from
182
+ * @returns {{ start: number, end: number, name: string } | null}
183
+ */
184
+ function findNextRawTextTag(html, from) {
185
+ let next = null
186
+ for (const name of RAW_TEXT_TAGS) {
187
+ const candidate = findNamedTag(html, name, from, false)
188
+ if (candidate && (!next || candidate.start < next.start)) {
189
+ next = { ...candidate, name }
190
+ }
191
+ }
192
+ return next
193
+ }
194
+
195
+ /**
196
+ * Remove comments and raw-text contents in document order. Comment markers
197
+ * inside raw-text elements are data, not document comments.
198
+ * @param {string} html
199
+ */
200
+ function stripCommentsAndRawText(html) {
201
+ let clean = ''
202
+ let cursor = 0
203
+ while (cursor < html.length) {
204
+ const commentStart = html.indexOf('<!--', cursor)
205
+ const rawText = findNextRawTextTag(html, cursor)
206
+ if (commentStart === -1 && !rawText) return clean + html.slice(cursor)
207
+ if (commentStart !== -1 && (!rawText || commentStart < rawText.start)) {
208
+ clean += html.slice(cursor, commentStart)
209
+ const commentEnd = findCommentEnd(html, commentStart)
210
+ if (!commentEnd) return clean
211
+ cursor = commentEnd.start + commentEnd.length
212
+ continue
213
+ }
214
+ if (!rawText) return clean + html.slice(cursor)
215
+ clean += html.slice(cursor, rawText.start)
216
+ if (rawText.end === -1) return clean
217
+ clean += html.slice(rawText.start, rawText.end + 1)
218
+ const close = findNamedTag(html, rawText.name, rawText.end + 1, true)
219
+ if (!close || close.end === -1) return clean
220
+ cursor = close.end + 1
221
+ }
222
+ return clean
223
+ }
224
+
225
+ /**
226
+ * Remove angle-bracket markup from title text. A dangling `<` is discarded
227
+ * through EOF instead of being left as a potentially active tag prefix.
228
+ * @param {string} value
229
+ */
230
+ function stripAngleBracketMarkup(value) {
231
+ let clean = ''
232
+ let cursor = 0
233
+ while (cursor < value.length) {
234
+ const start = value.indexOf('<', cursor)
235
+ if (start === -1) return clean + value.slice(cursor)
236
+ clean += value.slice(cursor, start)
237
+ const end = value.indexOf('>', start + 1)
238
+ if (end === -1) return clean
239
+ cursor = end + 1
240
+ }
241
+ return clean
242
+ }
243
+
244
+ /**
245
+ * Rewrite title bodies without relying on a multi-character HTML-filtering
246
+ * regexp. Malformed title elements are truncated so their contents cannot be
247
+ * interpreted as page metadata.
248
+ * @param {string} html
249
+ */
250
+ function rewriteTitleMarkup(html) {
251
+ let clean = ''
252
+ let cursor = 0
253
+ while (cursor < html.length) {
254
+ const open = findNamedTag(html, 'title', cursor, false)
255
+ if (!open) return clean + html.slice(cursor)
256
+ clean += html.slice(cursor, open.start)
257
+ if (open.end === -1) return clean
258
+ clean += html.slice(open.start, open.end + 1)
259
+ const close = findNamedTag(html, 'title', open.end + 1, true)
260
+ if (!close || close.end === -1) return clean
261
+ clean += stripAngleBracketMarkup(html.slice(open.end + 1, close.start))
262
+ clean += html.slice(close.start, close.end + 1)
263
+ cursor = close.end + 1
264
+ }
265
+ return clean
266
+ }
267
+
268
+ /**
269
+ * Return script bodies while respecting comments and parser-tolerated script
270
+ * end tags. An unclosed script consumes the remainder of the document.
271
+ * @param {string} html
272
+ */
273
+ function readInlineScripts(html) {
274
+ /** @type {string[]} */
275
+ const scripts = []
276
+ let cursor = 0
277
+ while (cursor < html.length) {
278
+ const script = findNamedTag(html, 'script', cursor, false)
279
+ const commentStart = html.indexOf('<!--', cursor)
280
+ if (commentStart !== -1 && (!script || commentStart < script.start)) {
281
+ const commentEnd = findCommentEnd(html, commentStart)
282
+ if (!commentEnd) return scripts
283
+ cursor = commentEnd.start + commentEnd.length
284
+ continue
285
+ }
286
+ if (!script || script.end === -1) return scripts
287
+ const contentStart = script.end + 1
288
+ const candidateClose = findNamedTag(html, 'script', contentStart, true)
289
+ const close = candidateClose?.end === -1 ? null : candidateClose
290
+ const contentEnd = close?.start ?? html.length
291
+ scripts.push(html.slice(contentStart, contentEnd))
292
+ if (!close || close.end === -1) return scripts
293
+ cursor = close.end + 1
294
+ }
295
+ return scripts
296
+ }
297
+
84
298
  /** Conservative generated-HTML tag reader, not a DOM implementation.
85
299
  * Raw-text HTML elements and comments cannot create fake metadata or links.
86
300
  * @param {string} html
87
301
  */
88
302
  export function readTags(html) {
89
- const clean = html
90
- .replace(/<!--[\s\S]*?-->/g, '')
91
- .replace(
92
- /(<(script|style|textarea|template|xmp|iframe|noembed|noframes|noscript)\b(?:"[^"]*"|'[^']*'|[^'">])*>)[\s\S]*?<\/\2\s*>/gi,
93
- '$1'
94
- )
95
- .replace(
96
- /(<title\b(?:"[^"]*"|'[^']*'|[^'">])*>)([\s\S]*?)<\/title\s*>/gi,
97
- (_, open, content) => `${open}${content.replace(/<[^>]*>/g, '')}</title>`
98
- )
303
+ const clean = rewriteTitleMarkup(stripCommentsAndRawText(html))
99
304
  /** @type {Array<{ name: string, attrs: Record<string,string> }>} */
100
305
  const tags = []
101
306
  for (const match of clean.matchAll(/<([a-z][a-z0-9:-]*)\b((?:"[^"]*"|'[^']*'|[^'">])*)>/gi)) {
@@ -245,11 +450,10 @@ export function checkStaticSite(root, profile) {
245
450
  requireValue(lstatSync(file).isFile(), 'Budget assets must be regular files')
246
451
  return sum + statSync(file).size
247
452
  }, 0)
248
- const inlineBytes = [
249
- ...readFileSync(page.file, 'utf8')
250
- .replace(/<!--[\s\S]*?-->/g, '')
251
- .matchAll(/<script\b(?:"[^"]*"|'[^']*'|[^'">])*?>([\s\S]*?)<\/script\s*>/gi),
252
- ].reduce((sum, match) => sum + Buffer.byteLength(match[1]), 0)
453
+ const inlineBytes = readInlineScripts(readFileSync(page.file, 'utf8')).reduce(
454
+ (sum, script) => sum + Buffer.byteLength(script),
455
+ 0
456
+ )
253
457
  const result = {
254
458
  route: path,
255
459
  htmlBytes: statSync(page.file).size,
@@ -267,7 +471,7 @@ export function checkStaticSite(root, profile) {
267
471
  if (profile.sitemap) {
268
472
  const sitemap = readFileSync(ownedPath(dist, profile.sitemap), 'utf8')
269
473
  requireValue(!/<!DOCTYPE|<!ENTITY/i.test(sitemap), 'Sitemap entities are not supported')
270
- const sitemapWithoutComments = sitemap.replace(/<!--[\s\S]*?-->/g, '')
474
+ const sitemapWithoutComments = stripHtmlComments(sitemap)
271
475
  const locations = new Set(
272
476
  [
273
477
  ...sitemapWithoutComments.matchAll(
@@ -98,7 +98,8 @@ function selectedPerformanceCommands() {
98
98
  const name = ['performance:check', 'perf:check'].find((candidate) => hasScript(candidate))
99
99
  if (name) {
100
100
  const [manager, args] = packageCommand(['run', name])
101
- return manager ? [{ source: `package-script:${name}`, argv: [manager, ...args] }] : []
101
+ if (!manager) throw new Error(`Cannot run ${name}: select a supported package_manager.`)
102
+ return [{ source: `package-script:${name}`, argv: [manager, ...args] }]
102
103
  }
103
104
  return performanceCommands().map((argv) => ({ source: 'configuration', argv }))
104
105
  }
@@ -600,15 +601,18 @@ function ci(task) {
600
601
 
601
602
  if (hasLanguage('rust') && hasRootRustProject()) {
602
603
  if (task === 'unit') {
603
- if (existsSync(resolve(root, 'src/lib.rs'))) run('cargo', ['test', '--lib'])
604
- if (existsSync(resolve(root, 'src/main.rs'))) run('cargo', ['test', '--bin', packageName()])
605
- if (!existsSync(resolve(root, 'src/lib.rs')) && !existsSync(resolve(root, 'src/main.rs')))
606
- run('cargo', ['test'])
604
+ // Batch exact prior targets through one Cargo graph.
605
+ const args = ['test']
606
+ if (existsSync(resolve(root, 'src/lib.rs'))) args.push('--lib')
607
+ if (existsSync(resolve(root, 'src/main.rs'))) args.push('--bin', packageName())
608
+ run('cargo', args)
607
609
  } else {
608
- for (const file of rustTests) {
610
+ // Keep --test selection narrow; --tests includes unit targets.
611
+ const targets = rustTests.flatMap((file) => {
609
612
  const match = file.match(/^tests\/(.+)\.rs$/)
610
- if (match && !match[1].includes('/')) run('cargo', ['test', '--test', match[1]])
611
- }
613
+ return match && !match[1].includes('/') ? ['--test', match[1]] : []
614
+ })
615
+ if (targets.length > 0) run('cargo', ['test', ...targets])
612
616
  }
613
617
  }
614
618
  }
package/src/runtime.mjs CHANGED
@@ -81,7 +81,7 @@ function sourceSha(root) {
81
81
  }
82
82
 
83
83
  /**
84
- * Stable public runtime; the original ecosystem executor is kept private and unchanged.
84
+ * Stable public runtime; the native executor stays separate from policy and evidence.
85
85
  * Tests can substitute an executor without installing consumer dependencies.
86
86
  * @param {string[]} args @param {string} [root] @param {string} [entry]
87
87
  * @returns {number}
@@ -46,7 +46,7 @@ cache/
46
46
  !.github/actions/cache/action.yml
47
47
  typechain/
48
48
  coverage.json
49
- performance-results/
49
+ .code-foundry/
50
50
  tests/.bin/
51
51
  **/.app/
52
52