Skip to content

Commit 982300c

Browse files
authored
Merge pull request #517 from Multiplier-Labs/feat/goal-runs
feat: GoalRun loop engine — durable act→verify→continue loops with maker-checker + deterministic PR finalization
2 parents 62e020d + ef0bc66 commit 982300c

23 files changed

Lines changed: 3467 additions & 2 deletions

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
"bin/",
2727
"dist/",
2828
"server/dist/",
29-
"server/workflows/"
29+
"server/workflows/",
30+
"server/loops/"
3031
],
3132
"scripts": {
3233
"dev": "vite",

server/glob-match.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/** Tests for the glob matcher used by Goal Run readonly constraints. */
2+
import { describe, it, expect } from 'vitest'
3+
import { globToRegExp, matchesAnyGlob } from './glob-match.js'
4+
5+
describe('globToRegExp', () => {
6+
it('matches a directory tree with **', () => {
7+
const re = globToRegExp('.github/workflows/**')
8+
expect(re.test('.github/workflows/ci.yml')).toBe(true)
9+
expect(re.test('.github/workflows/nested/deploy.yml')).toBe(true)
10+
expect(re.test('.github/other/ci.yml')).toBe(false)
11+
})
12+
13+
it('* does not cross path separators', () => {
14+
const re = globToRegExp('src/*.ts')
15+
expect(re.test('src/index.ts')).toBe(true)
16+
expect(re.test('src/sub/index.ts')).toBe(false)
17+
})
18+
19+
it('? matches a single non-slash character', () => {
20+
const re = globToRegExp('v?.txt')
21+
expect(re.test('v1.txt')).toBe(true)
22+
expect(re.test('v12.txt')).toBe(false)
23+
expect(re.test('v/.txt')).toBe(false)
24+
})
25+
26+
it('matches an extension glob', () => {
27+
const re = globToRegExp('*.lock')
28+
expect(re.test('yarn.lock')).toBe(true)
29+
expect(re.test('package-lock.json')).toBe(false)
30+
})
31+
32+
it('escapes regex metacharacters in literals', () => {
33+
const re = globToRegExp('a.b+c')
34+
expect(re.test('a.b+c')).toBe(true)
35+
expect(re.test('axbxc')).toBe(false)
36+
})
37+
})
38+
39+
describe('matchesAnyGlob', () => {
40+
it('returns true when any pattern matches', () => {
41+
const globs = ['.github/workflows/**', 'tests/security/**']
42+
expect(matchesAnyGlob('tests/security/auth.test.ts', globs)).toBe(true)
43+
expect(matchesAnyGlob('.github/workflows/ci.yml', globs)).toBe(true)
44+
expect(matchesAnyGlob('src/index.ts', globs)).toBe(false)
45+
})
46+
47+
it('returns false for an empty pattern list', () => {
48+
expect(matchesAnyGlob('anything', [])).toBe(false)
49+
})
50+
})

server/glob-match.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* Minimal glob matcher for Goal Run readonly constraints.
3+
*
4+
* Supports the subset needed by loop specs (e.g. `.github/workflows/**`,
5+
* `tests/security/**`, `*.lock`):
6+
* ** — any run of characters, including '/'
7+
* * — any run of characters except '/'
8+
* ? — a single character except '/'
9+
* Everything else is matched literally. Patterns are anchored to the full path.
10+
*
11+
* Intentionally dependency-free — the codebase ships no glob library, and the
12+
* constraint set is small and author-controlled, so a focused matcher beats
13+
* pulling in micromatch.
14+
*/
15+
16+
const REGEX_SPECIAL = new Set(['.', '+', '^', '$', '{', '}', '(', ')', '|', '[', ']', '\\'])
17+
18+
/** Convert a glob to a RegExp anchored to the whole string. */
19+
export function globToRegExp(glob: string): RegExp {
20+
let out = ''
21+
for (let i = 0; i < glob.length; i++) {
22+
const ch = glob[i]
23+
if (ch === '*') {
24+
if (glob[i + 1] === '*') {
25+
out += '.*'
26+
i++ // consume the second '*'
27+
} else {
28+
out += '[^/]*'
29+
}
30+
} else if (ch === '?') {
31+
out += '[^/]'
32+
} else if (REGEX_SPECIAL.has(ch)) {
33+
out += `\\${ch}`
34+
} else {
35+
out += ch
36+
}
37+
}
38+
return new RegExp(`^${out}$`)
39+
}
40+
41+
/** True when `path` matches any of the supplied globs. */
42+
export function matchesAnyGlob(path: string, globs: string[]): boolean {
43+
return globs.some((g) => globToRegExp(g).test(path))
44+
}

0 commit comments

Comments
 (0)