harness-everything 0.43.1 → 0.44.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.
@@ -10,7 +10,7 @@
10
10
  "name": "harness-everything",
11
11
  "source": "./",
12
12
  "description": "Harness OS: an orchestrated agent operating system — tier routing, circuit breaker guardrails, verification gates, WAL session memory, and self-evolving workspace rules.",
13
- "version": "0.43.1",
13
+ "version": "0.44.0",
14
14
  "author": {
15
15
  "name": "Miya Daniel"
16
16
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "harness-everything",
3
3
  "description": "Harness OS: an orchestrated agent operating system — tier routing, circuit breaker guardrails, verification gates, WAL session memory, and self-evolving workspace rules.",
4
- "version": "0.43.1",
4
+ "version": "0.44.0",
5
5
  "author": {
6
6
  "name": "Miya Daniel"
7
7
  },
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## [0.44.0](https://github.com/dyphn1/Harness-everything/compare/v0.43.1...v0.44.0) (2026-10-27)
6
+
7
+ ### Features
8
+
9
+ * **governance:** accept git-aware globs in the base-owned guard ([674a96d](https://github.com/dyphn1/Harness-everything/commit/674a96d7295555926ab3bb1b8ca0204cf59dbad5)), closes [#325](https://github.com/dyphn1/Harness-everything/issues/325)
10
+
5
11
  ## [0.43.1](https://github.com/dyphn1/Harness-everything/compare/v0.43.0...v0.43.1) (2026-10-27)
6
12
 
7
13
  ### Bug Fixes
@@ -12,7 +18,7 @@ All notable changes to this project are documented in this file.
12
18
 
13
19
  ### Fixed
14
20
 
15
- - **doc-governance:** allow regular governed-content revisions while retaining review for structural, policy, trust-surface, fixture, and derived-output changes; run the guard in its own CI job.
21
+ - **doc-governance:** allow regular governed-content revisions and expand base-owned governed/derived globs against Git trees, while retaining review for membership, structural, policy, trust-surface, fixture, and derived-output changes; run the guard in its own CI job.
16
22
 
17
23
  ## [0.43.0](https://github.com/dyphn1/Harness-everything/compare/v0.42.2...v0.43.0) (2026-10-27)
18
24
 
@@ -1667,8 +1667,8 @@ checkCase('2ax. frozen Git-object guard with bootstrap and tamper controls', ()
1667
1667
  return fixtureGit(root, ['rev-parse', 'HEAD']).toString('ascii').trim();
1668
1668
  }
1669
1669
 
1670
- function createFixture() {
1671
- const root = workspace('governance-guard-git');
1670
+ function createFixture(name = 'governance-guard-git') {
1671
+ const root = workspace(name);
1672
1672
  fs.mkdirSync(path.join(root, '.fixture-hooks'), { recursive: true });
1673
1673
  fixtureGit(root, ['init', '--quiet']);
1674
1674
  fixtureGit(root, ['config', 'user.name', 'Harness Guard Test']);
@@ -1820,6 +1820,190 @@ checkCase('2ax. frozen Git-object guard with bootstrap and tamper controls', ()
1820
1820
  assert.deepStrictEqual(derivedEntry.base, configuredIdentity(fixture.base, 'docs/governed-index.md'));
1821
1821
  assert.deepStrictEqual(derivedEntry.candidate, configuredIdentity(derivedCandidate, 'docs/governed-index.md'));
1822
1822
 
1823
+ const globPatterns = [
1824
+ 'docs/adr/*.md',
1825
+ 'empty/*.md',
1826
+ 'corpus/one*.md', // portability-lint: allow glob pattern string, never created as a file
1827
+ 'corpus/*.md',
1828
+ 'deep/**/*.md',
1829
+ 'repeat/**/repeat/*.md',
1830
+ 'literal.v1/**/*.md'
1831
+ ];
1832
+ const corpusPaths = [
1833
+ 'docs/adr/one.md', 'docs/adr/two.md', 'generated/view.md', 'generated/nested/view.md',
1834
+ 'corpus/one.md', 'corpus/many-a.md', 'corpus/many-b.md',
1835
+ 'deep/zero.md', 'deep/a/b.md',
1836
+ 'repeat/repeat/first.md', 'repeat/a/repeat/second.md',
1837
+ 'literal.v1/top.md', 'literal.v1/sub/nested.md', 'literalXv1/no.md'
1838
+ ];
1839
+ const globFixture = createFixture('governance-guard-glob-git');
1840
+ resetFixture(globFixture);
1841
+ const globPolicy = {
1842
+ governed: globPatterns.map(pattern => ({ path: pattern, role: 'reference' })),
1843
+ derived: [{ path: 'generated/**/*.md', generator: 'build' }]
1844
+ };
1845
+ writeRelative(globFixture.root, '.agents/harness-everything/project-docs.json', `${JSON.stringify(globPolicy, null, 2)}\n`);
1846
+ for (const relativePath of corpusPaths) writeRelative(globFixture.root, relativePath, `baseline ${relativePath}\n`);
1847
+ globFixture.base = commitFixture(globFixture.root, 'glob policy baseline');
1848
+
1849
+ const globAccepted = runFrozenGuard(globFixture, globFixture.base, globFixture.base);
1850
+ assert.strictEqual(globAccepted.report.verdict, 'PASS', globAccepted.report.errors[0]?.message);
1851
+ const globSummaries = globAccepted.report.policyPreservation.configuredPaths.patterns;
1852
+ assert.ok(globSummaries, 'glob records report their base and candidate member counts');
1853
+ assert.strictEqual(globAccepted.report.policyPreservation.configuredPaths.paths.find(item => item.path === 'docs/adr/one.md').state, 'pass', 'unchanged glob members pass');
1854
+ assert.deepStrictEqual(runFrozenGuard(globFixture, globFixture.base, globFixture.base), globAccepted, 'glob reports are deterministic for identical Git inputs');
1855
+ const lifecycleSource = fs.readFileSync(modulePath, 'utf8');
1856
+ const lifecycleMatcherSource = lifecycleSource.match(/function globToRegExp\(glob\) \{[\s\S]*?\n\}/);
1857
+ assert.ok(lifecycleMatcherSource, 'doc-lifecycle globToRegExp remains available for the consistency check');
1858
+ const lifecycleGlobToRegExp = new Function(`${lifecycleMatcherSource[0]}\nreturn globToRegExp;`)();
1859
+ for (const pattern of globPatterns) {
1860
+ const expected = corpusPaths.filter(relativePath => lifecycleGlobToRegExp(pattern).test(relativePath)).length;
1861
+ const summary = globSummaries.find(item => item.kind === 'governed' && item.path === pattern);
1862
+ assert.ok(summary, `glob summary exists for ${pattern}`);
1863
+ assert.strictEqual(summary.baseMembers, expected, `guard matcher agrees with doc-lifecycle for ${pattern}`);
1864
+ assert.strictEqual(summary.candidateMembers, expected, `candidate matcher agrees with doc-lifecycle for ${pattern}`);
1865
+ const expectedPaths = corpusPaths.filter(relativePath => lifecycleGlobToRegExp(pattern).test(relativePath)).sort((left, right) => Buffer.compare(Buffer.from(left), Buffer.from(right)));
1866
+ const actualPaths = globAccepted.report.policyPreservation.configuredPaths.paths
1867
+ .filter(item => item.kind === 'governed' && item.pattern === pattern).map(item => item.path);
1868
+ assert.deepStrictEqual(actualPaths, expectedPaths, `guard matcher returns the same members as doc-lifecycle for ${pattern}`);
1869
+ }
1870
+ assert.strictEqual(globSummaries.find(item => item.path === 'empty/*.md').baseMembers, 0, 'an empty glob is legal and reports zero members');
1871
+
1872
+ resetFixture(globFixture);
1873
+ writeRelative(globFixture.root, 'docs/adr/one.md', 'revised governed glob member\n');
1874
+ const globRevisionCandidate = commitFixture(globFixture.root, 'revise glob member');
1875
+ const globRevision = runFrozenGuard(globFixture, globFixture.base, globRevisionCandidate);
1876
+ assert.strictEqual(globRevision.status, 0, 'a regular governed glob member revision passes');
1877
+ const globRevisionEntry = globRevision.report.policyPreservation.configuredPaths.paths.find(item => item.path === 'docs/adr/one.md');
1878
+ assert.strictEqual(globRevisionEntry.state, 'revised');
1879
+
1880
+ resetFixture(globFixture);
1881
+ writeRelative(globFixture.root, 'docs/adr/new.md', 'new governed glob member\n');
1882
+ const globAdditionCandidate = commitFixture(globFixture.root, 'add glob member');
1883
+ const globAddition = runFrozenGuard(globFixture, globFixture.base, globAdditionCandidate);
1884
+ assert.strictEqual(globAddition.status, 2, 'a new glob member requires review');
1885
+ assert.strictEqual(globAddition.report.policyPreservation.configuredPaths.paths.find(item => item.path === 'docs/adr/new.md').state, 'review_required');
1886
+
1887
+ resetFixture(globFixture);
1888
+ fixtureGit(globFixture.root, ['rm', '--quiet', '--', 'docs/adr/two.md']);
1889
+ const globDeletionCandidate = commitFixture(globFixture.root, 'delete glob member');
1890
+ const globDeletion = runFrozenGuard(globFixture, globFixture.base, globDeletionCandidate);
1891
+ assert.strictEqual(globDeletion.status, 2, 'a deleted glob member requires review');
1892
+ assert.strictEqual(globDeletion.report.policyPreservation.configuredPaths.paths.find(item => item.path === 'docs/adr/two.md').candidate, null);
1893
+
1894
+ resetFixture(globFixture);
1895
+ fixtureGit(globFixture.root, ['mv', '--', 'docs/adr/one.md', 'docs/adr/renamed.md']);
1896
+ const globRenameCandidate = commitFixture(globFixture.root, 'rename glob member');
1897
+ const globRename = runFrozenGuard(globFixture, globFixture.base, globRenameCandidate);
1898
+ const globRenamePaths = globRename.report.policyPreservation.configuredPaths.paths;
1899
+ assert.strictEqual(globRename.status, 2, 'a rename within a glob requires review');
1900
+ assert.strictEqual(globRenamePaths.find(item => item.path === 'docs/adr/one.md').candidate, null, 'rename retains the old endpoint');
1901
+ assert.strictEqual(globRenamePaths.find(item => item.path === 'docs/adr/renamed.md').base, null, 'rename retains the new endpoint');
1902
+
1903
+ for (const mutation of ['mode', 'symlink', 'gitlink']) {
1904
+ resetFixture(globFixture);
1905
+ if (mutation === 'mode') {
1906
+ const candidate = commitFixture(globFixture.root, 'change glob member mode', 'docs/adr/one.md');
1907
+ const result = runFrozenGuard(globFixture, globFixture.base, candidate);
1908
+ assert.strictEqual(result.status, 2, 'a glob member mode change requires review');
1909
+ assert.strictEqual(result.report.policyPreservation.configuredPaths.paths.find(item => item.path === 'docs/adr/one.md').state, 'review_required');
1910
+ continue;
1911
+ }
1912
+ fs.rmSync(path.join(globFixture.root, 'docs', 'adr', 'one.md'));
1913
+ if (mutation === 'symlink') {
1914
+ const oid = fixtureGit(globFixture.root, ['hash-object', '-w', '--stdin'], Buffer.from('../outside-target.md\n')).toString('ascii').trim();
1915
+ fixtureGit(globFixture.root, ['add', '-A']);
1916
+ fixtureGit(globFixture.root, ['update-index', '--add', '--cacheinfo', `120000,${oid},docs/adr/one.md`]);
1917
+ } else {
1918
+ fixtureGit(globFixture.root, ['add', '-A']);
1919
+ fixtureGit(globFixture.root, ['update-index', '--add', '--cacheinfo', `160000,${globFixture.base},docs/adr/one.md`]);
1920
+ }
1921
+ fixtureGit(globFixture.root, ['commit', '--quiet', '-m', `replace glob member with ${mutation}`]);
1922
+ const candidate = fixtureGit(globFixture.root, ['rev-parse', 'HEAD']).toString('ascii').trim();
1923
+ const result = runFrozenGuard(globFixture, globFixture.base, candidate);
1924
+ assert.strictEqual(result.status, 2, `a glob member ${mutation} requires review`);
1925
+ assert.strictEqual(result.report.policyPreservation.configuredPaths.paths.find(item => item.path === 'docs/adr/one.md').state, 'review_required');
1926
+ }
1927
+
1928
+ resetFixture(globFixture);
1929
+ writeRelative(globFixture.root, 'generated/view.md', 'changed derived glob output\n');
1930
+ const derivedGlobCandidate = commitFixture(globFixture.root, 'change derived glob member');
1931
+ const derivedGlob = runFrozenGuard(globFixture, globFixture.base, derivedGlobCandidate);
1932
+ assert.strictEqual(derivedGlob.status, 2, 'a derived glob member content change requires review');
1933
+ assert.strictEqual(derivedGlob.report.policyPreservation.configuredPaths.paths.find(item => item.path === 'generated/view.md').state, 'review_required');
1934
+
1935
+ if (process.platform !== 'win32') {
1936
+ resetFixture(globFixture);
1937
+ const unusualPath = 'docs/adr/new\nfile.md';
1938
+ const oid = fixtureGit(globFixture.root, ['hash-object', '-w', '--stdin'], Buffer.from('unusual glob member\n')).toString('ascii').trim();
1939
+ fixtureGit(globFixture.root, ['update-index', '--add', '--cacheinfo', `100644,${oid},${unusualPath}`]);
1940
+ fixtureGit(globFixture.root, ['commit', '--quiet', '-m', 'add unusual glob member']);
1941
+ const unusualCandidate = fixtureGit(globFixture.root, ['rev-parse', 'HEAD']).toString('ascii').trim();
1942
+ const unusual = runFrozenGuard(globFixture, globFixture.base, unusualCandidate);
1943
+ assert.strictEqual(unusual.status, 2, 'a glob matches paths containing unusual LF bytes');
1944
+ assert.strictEqual(unusual.report.policyPreservation.configuredPaths.paths.find(item => item.path === unusualPath).state, 'review_required');
1945
+ }
1946
+
1947
+ if (process.platform !== 'win32') {
1948
+ // Known divergence: doc-lifecycle.js still excludes line terminators in glob segments.
1949
+ const newlineGlobFixture = createFixture('governance-guard-newline-glob-git');
1950
+ resetFixture(newlineGlobFixture);
1951
+ writeRelative(newlineGlobFixture.root, '.agents/harness-everything/project-docs.json', `${JSON.stringify({
1952
+ governed: [{ path: 'docs/**/*.md', role: 'reference' }],
1953
+ derived: []
1954
+ }, null, 2)}\n`);
1955
+ writeRelative(newlineGlobFixture.root, 'docs/anchor.md', 'baseline direct child\n');
1956
+ writeRelative(newlineGlobFixture.root, 'docs/a\nb/file.md', 'baseline newline directory member\n');
1957
+ writeRelative(newlineGlobFixture.root, 'docs/a/b/file.md', 'baseline regular directory member\n');
1958
+ newlineGlobFixture.base = commitFixture(newlineGlobFixture.root, 'newline glob policy baseline');
1959
+
1960
+ fixtureGit(newlineGlobFixture.root, ['rm', '--quiet', '--', 'docs/a\nb/file.md']);
1961
+ const newlineDeletionCandidate = commitFixture(newlineGlobFixture.root, 'delete newline directory member');
1962
+ const newlineDeletion = runFrozenGuard(newlineGlobFixture, newlineGlobFixture.base, newlineDeletionCandidate);
1963
+
1964
+ resetFixture(newlineGlobFixture);
1965
+ writeRelative(newlineGlobFixture.root, 'docs/a\nb/new.md', 'new newline directory member\n');
1966
+ const newlineAdditionCandidate = commitFixture(newlineGlobFixture.root, 'add newline directory member');
1967
+ const newlineAddition = runFrozenGuard(newlineGlobFixture, newlineGlobFixture.base, newlineAdditionCandidate);
1968
+
1969
+ resetFixture(newlineGlobFixture);
1970
+ fixtureGit(newlineGlobFixture.root, ['rm', '--quiet', '--', 'docs/a/b/file.md']);
1971
+ const regularDeletionCandidate = commitFixture(newlineGlobFixture.root, 'delete regular directory member');
1972
+ const regularDeletion = runFrozenGuard(newlineGlobFixture, newlineGlobFixture.base, regularDeletionCandidate);
1973
+
1974
+ assert.deepStrictEqual({
1975
+ newlineDirectoryDeletion: newlineDeletion.status,
1976
+ newlineDirectoryAddition: newlineAddition.status,
1977
+ regularDirectoryDeletion: regularDeletion.status
1978
+ }, {
1979
+ newlineDirectoryDeletion: 2,
1980
+ newlineDirectoryAddition: 2,
1981
+ regularDirectoryDeletion: 2
1982
+ }, 'newline-directory deletions and additions require review; regular-directory deletion is the positive control');
1983
+ }
1984
+
1985
+ resetFixture(globFixture);
1986
+ const narrowedPolicy = JSON.parse(fs.readFileSync(path.join(globFixture.root, '.agents/harness-everything/project-docs.json'), 'utf8'));
1987
+ narrowedPolicy.governed[0].path = 'docs/adr/one.md';
1988
+ writeRelative(globFixture.root, '.agents/harness-everything/project-docs.json', `${JSON.stringify(narrowedPolicy, null, 2)}\n`);
1989
+ fixtureGit(globFixture.root, ['rm', '--quiet', '--', 'docs/adr/two.md']);
1990
+ const narrowedCandidate = commitFixture(globFixture.root, 'narrow governed glob');
1991
+ const narrowed = runFrozenGuard(globFixture, globFixture.base, narrowedCandidate);
1992
+ assert.strictEqual(narrowed.status, 2, 'candidate policy cannot narrow base glob coverage');
1993
+ assert.strictEqual(narrowed.report.policyPreservation.governed.state, 'review_required', 'candidate glob changes remain policy review');
1994
+ assert.strictEqual(narrowed.report.policyPreservation.configuredPaths.paths.find(item => item.path === 'docs/adr/two.md').candidate, null);
1995
+
1996
+ for (const unsupported of ['docs/adr/*.md?', 'docs/adr/[a].md', 'docs/adr/{a}.md', '/docs/adr/*.md', 'docs/../*.md']) {
1997
+ resetFixture(globFixture);
1998
+ const invalidPolicy = JSON.parse(fs.readFileSync(path.join(globFixture.root, '.agents/harness-everything/project-docs.json'), 'utf8'));
1999
+ invalidPolicy.governed.push({ path: unsupported, role: 'reference' });
2000
+ writeRelative(globFixture.root, '.agents/harness-everything/project-docs.json', `${JSON.stringify(invalidPolicy, null, 2)}\n`);
2001
+ const invalidCandidate = commitFixture(globFixture.root, `unsupported pattern ${unsupported}`);
2002
+ const invalid = runFrozenGuard(globFixture, globFixture.base, invalidCandidate);
2003
+ assert.strictEqual(invalid.status, 1, `unsupported glob syntax fails closed: ${unsupported}`);
2004
+ assert.strictEqual(invalid.report.verdict, 'FAIL');
2005
+ }
2006
+
1823
2007
  for (const mutation of ['rename', 'mode', 'symlink']) {
1824
2008
  resetFixture(fixture);
1825
2009
  const relativePath = 'docs/architecture.md';
@@ -236,6 +236,8 @@ Private byte-identical backups of all three queue files and this turn's scope, p
236
236
  | Evidence / limits | These are deterministic mechanism and local step-simulation results, not hosted GitHub Actions or live-host evidence. Full `npm test` and `verify:all` are not established here. The independent review of the earlier frozen guard found the fixture-oracle gap and left semantic MUST preservation unresolved; independent re-review of the fixed guard against the final exact SHAs remains pending. Candidate-controlled `.github/workflows/ci.yml` can remove the invocation, and no external required-status check is enabled. |
237
237
  | E1 / authority | P2 is CI engineering evidence only. E1 remains at 0 qualifying cycles; status is NOT GO / owner GO is NOT ISSUED, and #320 remains unauthorized. |
238
238
 
239
+ On 2026-10-27, the base-owned guard added a self-contained POSIX `*`/`**` matcher and expands governed and derived globs from the base and candidate Git trees. New or missing members and non-regular or mode-changed entries remain `REVIEW_REQUIRED`; regular governed revisions remain informational, derived object changes still require review, empty globs report zero members, and candidate policy changes remain review-required.
240
+
239
241
  ### Next session
240
242
 
241
243
  | Remaining item | Next action / boundary |
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "harness-enforcement",
3
3
  "description": "Repository inventory for the opencode enforcement plugin and its standalone helper",
4
- "version": "0.43.1",
4
+ "version": "0.44.0",
5
5
  "runtime": "index.mjs",
6
6
  "hooks": {
7
7
  "verify": "hooks/verify.js"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harness-everything",
3
- "version": "0.43.1",
3
+ "version": "0.44.0",
4
4
  "description": "Harness OS: An Orchestrated Agent Operating System for AI-driven Development",
5
5
  "main": "harness-everything/scripts/bootstrap.js",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harness-everything",
3
- "version": "0.43.1",
3
+ "version": "0.44.0",
4
4
  "description": "Invariant-first engineering harness for routing, verification, and failure recovery while preserving agent autonomy.",
5
5
  "author": {
6
6
  "name": "Miya Daniel",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "harness-everything",
4
- "version": "0.43.1",
4
+ "version": "0.44.0",
5
5
  "description": "Invariant-first engineering harness for routing, verification, and failure recovery while preserving agent autonomy.",
6
6
  "author": {
7
7
  "name": "Miya Daniel",
package/sbom.cdx.json CHANGED
@@ -2,23 +2,23 @@
2
2
  "$schema": "https://cyclonedx.org/schema/bom-1.7.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.7",
5
- "serialNumber": "urn:uuid:e4de25fd-21a9-593b-841e-c8e0c3a9d6ad",
5
+ "serialNumber": "urn:uuid:062d938e-8477-5564-9f89-fa21e9121182",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-10-27T09:10:22.642Z",
8
+ "timestamp": "2026-10-27T10:11:43.769Z",
9
9
  "tools": [
10
10
  {
11
11
  "vendor": "Harness Everything",
12
12
  "name": "generate-sbom.js",
13
- "version": "0.43.1"
13
+ "version": "0.44.0"
14
14
  }
15
15
  ],
16
16
  "component": {
17
17
  "type": "application",
18
- "bom-ref": "pkg:npm/harness-everything@0.43.1",
18
+ "bom-ref": "pkg:npm/harness-everything@0.44.0",
19
19
  "name": "harness-everything",
20
- "version": "0.43.1",
21
- "purl": "pkg:npm/harness-everything@0.43.1"
20
+ "version": "0.44.0",
21
+ "purl": "pkg:npm/harness-everything@0.44.0"
22
22
  }
23
23
  },
24
24
  "components": [
@@ -2033,7 +2033,7 @@
2033
2033
  ]
2034
2034
  },
2035
2035
  {
2036
- "ref": "pkg:npm/harness-everything@0.43.1",
2036
+ "ref": "pkg:npm/harness-everything@0.44.0",
2037
2037
  "dependsOn": [
2038
2038
  "pkg:npm/%40commitlint/cli@19.8.1",
2039
2039
  "pkg:npm/%40commitlint/config-conventional@19.8.1",
@@ -121,9 +121,8 @@ function validateProjectDocs(value, label) {
121
121
  if (!isRecord(record) || typeof record.path !== 'string' || !record.path || typeof record[field] !== 'string' || !record[field]) {
122
122
  throw new Error(`${label} has a malformed ${kind} record`);
123
123
  }
124
- if (record.path.startsWith('/') || record.path.split('/').includes('..') || /[*?\[\]]/.test(record.path)) {
125
- // shortcut: the baseline registry uses exact Git paths; glob support needs a Git-aware matcher before adoption.
126
- throw new Error(`${label} has a non-exact governed or derived path: ${record.path}`);
124
+ if (record.path.startsWith('/') || record.path.split('/').includes('..') || ['?', '[', ']', '{', '}'].some(char => record.path.includes(char))) {
125
+ throw new Error(`${label} has an unsupported governed or derived path pattern: ${record.path}`);
127
126
  }
128
127
  const key = `${kind}:${record.path}`;
129
128
  if (seen.has(key)) throw new Error(`${label} has duplicate ${kind} path: ${record.path}`);
@@ -159,8 +158,9 @@ function policyChanges(base, candidate, kind) {
159
158
  path: record.path,
160
159
  base: record,
161
160
  candidate: byPath.get(record.path) || null
162
- }));
163
- const added = candidate[kind].filter(record => !basePaths.has(record.path));
161
+ })).sort((left, right) => Buffer.compare(Buffer.from(left.path), Buffer.from(right.path)));
162
+ const added = candidate[kind].filter(record => !basePaths.has(record.path))
163
+ .sort((left, right) => Buffer.compare(Buffer.from(left.path), Buffer.from(right.path)));
164
164
  return { state: missingOrChanged.length || added.length ? 'review_required' : 'pass', preserved: base[kind].length - missingOrChanged.length, missingOrChanged, added };
165
165
  }
166
166
 
@@ -173,6 +173,32 @@ function pathText(pathBytes) {
173
173
  return Buffer.from(value, 'utf8').equals(pathBytes) ? value : null;
174
174
  }
175
175
 
176
+ function globToRegExp(glob) {
177
+ const source = Buffer.from(String(glob).replace(/\\/g, '/'), 'utf8').toString('latin1');
178
+ let pattern = '^';
179
+ for (let index = 0; index < source.length; index++) {
180
+ const char = source[index];
181
+ if (char === '*' && source[index + 1] === '*') {
182
+ if (source[index + 2] === '/') {
183
+ pattern += '(?:[\\s\\S]*/)?';
184
+ index += 2;
185
+ } else {
186
+ pattern += '[\\s\\S]*';
187
+ index++;
188
+ }
189
+ } else if (char === '*') {
190
+ pattern += '[^/]*';
191
+ } else {
192
+ pattern += char.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
193
+ }
194
+ }
195
+ return new RegExp(`${pattern}$`);
196
+ }
197
+
198
+ function matchesConfiguredPath(record, pathBytes, matcher) {
199
+ return matcher ? matcher.test(pathBytes.toString('latin1')) : Buffer.from(record.path, 'utf8').equals(pathBytes);
200
+ }
201
+
176
202
  function identity(entry) {
177
203
  return entry ? { mode: entry.mode, type: entry.type, oid: entry.oid } : null;
178
204
  }
@@ -223,27 +249,57 @@ function unknownNewDocuments(baseTree, candidateTree) {
223
249
 
224
250
  function checkConfiguredPaths(baseTree, candidateTree, config) {
225
251
  const details = [];
252
+ const patterns = [];
226
253
  for (const kind of ['governed', 'derived']) {
227
254
  for (const record of config[kind]) {
228
- const key = pathKey(record.path);
229
- const before = baseTree.get(key);
230
- const after = candidateTree.get(key);
231
- if (!regularBlob(before)) throw new Error(`baseline ${kind} path is missing or not a regular file: ${record.path}`);
232
- const sameRegularMode = regularBlob(after) && before.mode === after.mode;
233
- const revised = kind === 'governed' && sameRegularMode && before.oid !== after.oid;
234
- const identityPreserved = sameRegularMode && before.oid === after.oid;
235
- details.push({
236
- kind,
237
- path: record.path,
238
- state: revised ? 'revised' : identityPreserved ? 'pass' : 'review_required',
239
- base: identity(before),
240
- candidate: identity(after),
241
- ...(revised ? { reason: 'Content revisions are tracked by Git identity but are not semantically validated by the guard.' } : {})
242
- });
255
+ const glob = record.path.includes('*');
256
+ const matcher = glob ? globToRegExp(record.path) : null;
257
+ const collect = sourceTree => {
258
+ if (!glob) {
259
+ const key = pathKey(record.path);
260
+ const entry = sourceTree.get(key);
261
+ return new Map(entry ? [[key, entry]] : []);
262
+ }
263
+ const matches = new Map();
264
+ for (const [key, entry] of sourceTree) {
265
+ if (matchesConfiguredPath(record, entry.pathBytes, matcher)) matches.set(key, entry);
266
+ }
267
+ return matches;
268
+ };
269
+ const baseMembers = collect(baseTree);
270
+ const candidateMembers = collect(candidateTree);
271
+ if (!glob && !regularBlob(baseMembers.values().next().value)) {
272
+ throw new Error(`baseline ${kind} path is missing or not a regular file: ${record.path}`);
273
+ }
274
+ if (glob) patterns.push({ kind, path: record.path, baseMembers: baseMembers.size, candidateMembers: candidateMembers.size });
275
+ const keys = new Set([...baseMembers.keys(), ...candidateMembers.keys()]);
276
+ for (const key of keys) {
277
+ const before = baseMembers.get(key) || null;
278
+ const after = candidateMembers.get(key) || null;
279
+ const entry = before || after;
280
+ const sameRegularMode = regularBlob(before) && regularBlob(after) && before.mode === after.mode;
281
+ const revised = kind === 'governed' && sameRegularMode && before.oid !== after.oid;
282
+ const identityPreserved = sameRegularMode && before.oid === after.oid;
283
+ const memberPath = pathText(entry.pathBytes);
284
+ details.push({
285
+ kind,
286
+ ...(glob ? { pattern: record.path } : {}),
287
+ path: memberPath,
288
+ ...(memberPath === null ? { pathBase64: key } : {}),
289
+ state: revised ? 'revised' : identityPreserved ? 'pass' : 'review_required',
290
+ base: identity(before),
291
+ candidate: identity(after),
292
+ ...(revised ? { reason: 'Content revisions are tracked by Git identity but are not semantically validated by the guard.' } : {})
293
+ });
294
+ }
243
295
  }
244
296
  }
297
+ patterns.sort((left, right) => Buffer.compare(Buffer.from(left.kind), Buffer.from(right.kind)) || Buffer.compare(Buffer.from(left.path), Buffer.from(right.path)));
298
+ details.sort((left, right) => Buffer.compare(Buffer.from(left.pathBase64 || Buffer.from(left.path, 'utf8').toString('base64'), 'base64'), Buffer.from(right.pathBase64 || Buffer.from(right.path, 'utf8').toString('base64'), 'base64'))
299
+ || Buffer.compare(Buffer.from(left.kind), Buffer.from(right.kind))
300
+ || Buffer.compare(Buffer.from(left.pattern || ''), Buffer.from(right.pattern || '')));
245
301
  const failures = details.filter(item => item.state === 'review_required');
246
- return { state: failures.length ? 'review_required' : 'pass', paths: details, missingOrChanged: failures };
302
+ return { state: failures.length ? 'review_required' : 'pass', patterns, paths: details, missingOrChanged: failures };
247
303
  }
248
304
 
249
305
  function parseArgs(argv) {