fix(framework): detect installed tool drift (#1194) (#1195)
ci/woodpecker/push/publish Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful

Co-authored-by: coder3 <[email protected]>
This commit was merged in pull request #1195.
This commit is contained in:
coder3
2026-08-13 10:43:11 +00:00
committed by Mos
parent 120af4e193
commit 41749bbd33
10 changed files with 499 additions and 5 deletions
@@ -146,6 +146,21 @@ describe('resolveOwnership (deny-wins + fail-safe)', () => {
expect(resolveOwnership(m, 'tools/git/pr-create.sh')).toBe('framework');
});
it('limits an exact operator carve-out to that path while requiring /** for subtrees', () => {
const exact: FrameworkManifest = {
framework: ['tools/**'],
operator: ['tools/git'],
};
expect(resolveOwnership(exact, 'tools/git')).toBe('operator');
expect(resolveOwnership(exact, 'tools/git/guard.sh')).toBe('framework');
const subtree: FrameworkManifest = {
framework: ['tools/**'],
operator: ['tools/git/**'],
};
expect(resolveOwnership(subtree, 'tools/git/guard.sh')).toBe('operator');
});
it('framework-declared paths resolve to framework', () => {
expect(resolveOwnership(m, 'guides/E2E-DELIVERY.md')).toBe('framework');
expect(resolveOwnership(m, 'CONSTITUTION.md')).toBe('framework');
+14 -1
View File
@@ -154,12 +154,25 @@ export function matchesAny(globs: readonly string[], relPath: string): boolean {
return globs.some((g) => matchGlob(g, relPath));
}
/**
* Operator entries without wildcards are exact file carve-outs. Treating them
* as directory prefixes would let one bare entry hide an entire framework
* subtree from reconciliation and drift detection. Operator subtree ownership
* remains explicit through `dir/**`.
*/
function matchesOperatorGlob(glob: string, relPath: string): boolean {
const pattern = normalizeRel(glob);
if (pattern === '') return false;
if (!pattern.includes('*')) return normalizeRel(relPath) === pattern;
return matchGlob(pattern, relPath);
}
/**
* Resolve ownership of a mosaic-home-relative path (deny-wins / fail-safe):
* operator globs win, then framework globs, else operator by default.
*/
export function resolveOwnership(manifest: FrameworkManifest, relPath: string): Ownership {
if (matchesAny(manifest.operator, relPath)) return 'operator';
if (manifest.operator.some((glob) => matchesOperatorGlob(glob, relPath))) return 'operator';
if (matchesAny(manifest.framework, relPath)) return 'framework';
return 'operator';
}