@plainconceptsplatform/agent-harness 3.8.0 → 3.8.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.
@@ -8,50 +8,102 @@ const LEGACY_MANIFEST = '.platform/manifest'
8
8
 
9
9
  export class ContractError extends Error {}
10
10
 
11
- const escapeRegExp = text => text.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&')
12
-
13
- export function globToRegExp(glob) {
14
- const directory = glob.endsWith('/')
15
- const body = glob.replace(/\/$/, '')
16
- const anchored = body.includes('/')
17
- const pattern = body.replace(/^\//, '')
18
- let source = ''
19
- for (let i = 0; i < pattern.length; i++) {
20
- const c = pattern[i]
21
- if (c === '*' && pattern[i + 1] === '*' && (i === 0 || pattern[i - 1] === '/')) {
22
- if (i + 2 === pattern.length) {
23
- source += '.*'
24
- i += 1
25
- continue
26
- }
27
- if (pattern[i + 2] === '/') {
28
- source += '(?:.*/)?'
29
- i += 2
30
- continue
31
- }
11
+ function bracket(segment, start) {
12
+ const end = segment.indexOf(']', start + 2)
13
+ if (end === -1) return null
14
+ let body = segment.slice(start + 1, end)
15
+ const negated = body[0] === '!' || body[0] === '^'
16
+ if (negated) body = body.slice(1)
17
+ const ranges = []
18
+ for (let i = 0; i < body.length; i++) {
19
+ const from = body[i] === '\\' && i + 1 < body.length ? body[++i] : body[i]
20
+ let to = from
21
+ if (body[i + 1] === '-' && i + 2 < body.length) {
22
+ i += 2
23
+ to = body[i] === '\\' && i + 1 < body.length ? body[++i] : body[i]
32
24
  }
25
+ ranges.push([from, to])
26
+ }
27
+ return { token: { negated, ranges }, end }
28
+ }
29
+
30
+ const STAR = Symbol('*')
31
+
32
+ function segmentTokens(segment) {
33
+ const tokens = []
34
+ for (let i = 0; i < segment.length; i++) {
35
+ const c = segment[i]
36
+ const set = c === '[' ? bracket(segment, i) : null
33
37
  if (c === '*') {
34
- source += '[^/]*'
35
- while (pattern[i + 1] === '*') i++
36
- } else if (c === '?') source += '[^/]'
37
- else if (c === '[' && pattern.indexOf(']', i + 2) !== -1) {
38
- const end = pattern.indexOf(']', i + 2)
39
- source += `[${pattern.slice(i + 1, end).replace(/^!/, '^')}]`
40
- i = end
41
- } else if (c === '\\' && i + 1 < pattern.length) source += escapeRegExp(pattern[++i])
42
- else source += escapeRegExp(c)
38
+ if (tokens.at(-1) !== STAR) tokens.push(STAR)
39
+ } else if (c === '?') tokens.push({ negated: true, ranges: [] })
40
+ else if (set) {
41
+ tokens.push(set.token)
42
+ i = set.end
43
+ } else tokens.push(c === '\\' && i + 1 < segment.length ? segment[++i] : c)
44
+ }
45
+ return tokens
46
+ }
47
+
48
+ const tokenMatches = (token, c) => typeof token === 'string'
49
+ ? token === c
50
+ : token.ranges.some(([from, to]) => from <= c && c <= to) !== token.negated
51
+
52
+ function matchSegment(tokens, text) {
53
+ let t = 0
54
+ let i = 0
55
+ let star = -1
56
+ let resume = 0
57
+ while (i < text.length) {
58
+ if (tokens[t] === STAR) {
59
+ star = t++
60
+ resume = i
61
+ } else if (t < tokens.length && tokenMatches(tokens[t], text[i])) {
62
+ t++
63
+ i++
64
+ } else if (star !== -1) {
65
+ t = star + 1
66
+ i = ++resume
67
+ } else return false
68
+ }
69
+ while (tokens[t] === STAR) t++
70
+ return t === tokens.length
71
+ }
72
+
73
+ // gitwildmatch over `/` segments; a RegExp built from contract text is a blocking finding in the apps' Semgrep scan.
74
+ export function compileGlob(glob) {
75
+ const directory = glob.endsWith('/')
76
+ const body = directory ? glob.slice(0, -1) : glob
77
+ const segments = (body.startsWith('/') ? body.slice(1) : body).split('/')
78
+ return { anchored: body.includes('/'), directory, segments: segments.map(s => (s === '**' ? s : segmentTokens(s))) }
79
+ }
80
+
81
+ function matchFrom(segments, s, parts, p, directory) {
82
+ if (s === segments.length) return parts.length - p >= (directory ? 1 : 0)
83
+ if (segments[s] !== '**') return p < parts.length && matchSegment(segments[s], parts[p]) && matchFrom(segments, s + 1, parts, p + 1, directory)
84
+ for (let next = s + 1 === segments.length ? p + 1 : p; next <= parts.length; next++) {
85
+ if (matchFrom(segments, s + 1, parts, next, directory)) return true
86
+ }
87
+ return false
88
+ }
89
+
90
+ export function matchGlob(compiled, file) {
91
+ const { anchored, directory, segments } = typeof compiled === 'string' ? compileGlob(compiled) : compiled
92
+ const parts = file.split('/')
93
+ for (let start = 0; start < (anchored ? 1 : parts.length); start++) {
94
+ if (matchFrom(segments, 0, parts, start, directory)) return true
43
95
  }
44
- return new RegExp(`^${anchored ? '' : '(?:.*/)?'}${source}${directory ? '/.*' : '(?:/.*)?'}$`)
96
+ return false
45
97
  }
46
98
 
47
99
  const toRules = globs => globs.map(glob => {
48
100
  const negated = glob.startsWith('!')
49
- return { negated, regex: globToRegExp(negated ? glob.slice(1) : glob) }
101
+ return { negated, glob: compileGlob(negated ? glob.slice(1) : glob) }
50
102
  })
51
103
 
52
104
  function matchesRules(rules, file) {
53
105
  let matched = false
54
- for (const rule of rules) if (rule.regex.test(file)) matched = !rule.negated
106
+ for (const rule of rules) if (matchGlob(rule.glob, file)) matched = !rule.negated
55
107
  return matched
56
108
  }
57
109
 
@@ -223,7 +275,7 @@ export function isOwned(contract, file, today = localDate()) {
223
275
  const relative = file.replace(/\\/g, '/').replace(/^\.\//, '')
224
276
  if (!matchesRules(contract.rules, relative)) return false
225
277
  return !contract.exceptionList.some(e =>
226
- isText(e.path) && /^\d{4}-\d{2}-\d{2}$/.test(String(e.expires ?? '')) && e.expires >= today && globToRegExp(e.path).test(relative))
278
+ isText(e.path) && /^\d{4}-\d{2}-\d{2}$/.test(String(e.expires ?? '')) && e.expires >= today && matchGlob(e.path, relative))
227
279
  }
228
280
 
229
281
  export function describeOwner(contract) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plainconceptsplatform/agent-harness",
3
- "version": "3.8.0",
3
+ "version": "3.8.1",
4
4
  "description": "Installs the Plain Concepts Platform Harness into any codebase, and keeps it up to date. Wires OpenCode, OpenSpec, codegraph, and agentmemory into a multi-agent workflow that runs on native parallel subagents.",
5
5
  "keywords": [
6
6
  "opencode",