@mossbear/projections 0.1.0-alpha.26

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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +7 -0
  3. package/dist/agents-dir.d.ts +13 -0
  4. package/dist/agents-dir.d.ts.map +1 -0
  5. package/dist/agents-dir.js +36 -0
  6. package/dist/agents-dir.js.map +1 -0
  7. package/dist/agents-dir.test.d.ts +2 -0
  8. package/dist/agents-dir.test.d.ts.map +1 -0
  9. package/dist/agents-dir.test.js +30 -0
  10. package/dist/agents-dir.test.js.map +1 -0
  11. package/dist/agents-md.d.ts +12 -0
  12. package/dist/agents-md.d.ts.map +1 -0
  13. package/dist/agents-md.js +47 -0
  14. package/dist/agents-md.js.map +1 -0
  15. package/dist/agents-md.test.d.ts +2 -0
  16. package/dist/agents-md.test.d.ts.map +1 -0
  17. package/dist/agents-md.test.js +44 -0
  18. package/dist/agents-md.test.js.map +1 -0
  19. package/dist/claude-md.d.ts +17 -0
  20. package/dist/claude-md.d.ts.map +1 -0
  21. package/dist/claude-md.js +43 -0
  22. package/dist/claude-md.js.map +1 -0
  23. package/dist/claude-md.test.d.ts +2 -0
  24. package/dist/claude-md.test.d.ts.map +1 -0
  25. package/dist/claude-md.test.js +61 -0
  26. package/dist/claude-md.test.js.map +1 -0
  27. package/dist/claude-rules.d.ts +17 -0
  28. package/dist/claude-rules.d.ts.map +1 -0
  29. package/dist/claude-rules.js +43 -0
  30. package/dist/claude-rules.js.map +1 -0
  31. package/dist/claude-rules.test.d.ts +2 -0
  32. package/dist/claude-rules.test.d.ts.map +1 -0
  33. package/dist/claude-rules.test.js +34 -0
  34. package/dist/claude-rules.test.js.map +1 -0
  35. package/dist/compatibility-attribution.d.ts +7 -0
  36. package/dist/compatibility-attribution.d.ts.map +1 -0
  37. package/dist/compatibility-attribution.js +11 -0
  38. package/dist/compatibility-attribution.js.map +1 -0
  39. package/dist/cursor-rules.d.ts +17 -0
  40. package/dist/cursor-rules.d.ts.map +1 -0
  41. package/dist/cursor-rules.js +36 -0
  42. package/dist/cursor-rules.js.map +1 -0
  43. package/dist/cursor-rules.test.d.ts +2 -0
  44. package/dist/cursor-rules.test.d.ts.map +1 -0
  45. package/dist/cursor-rules.test.js +58 -0
  46. package/dist/cursor-rules.test.js.map +1 -0
  47. package/dist/detect-format.d.ts +15 -0
  48. package/dist/detect-format.d.ts.map +1 -0
  49. package/dist/detect-format.js +88 -0
  50. package/dist/detect-format.js.map +1 -0
  51. package/dist/detect-format.test.d.ts +2 -0
  52. package/dist/detect-format.test.d.ts.map +1 -0
  53. package/dist/detect-format.test.js +104 -0
  54. package/dist/detect-format.test.js.map +1 -0
  55. package/dist/eval-path.d.ts +91 -0
  56. package/dist/eval-path.d.ts.map +1 -0
  57. package/dist/eval-path.js +110 -0
  58. package/dist/eval-path.js.map +1 -0
  59. package/dist/index.d.ts +17 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +17 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/path-rules.d.ts +45 -0
  64. package/dist/path-rules.d.ts.map +1 -0
  65. package/dist/path-rules.js +65 -0
  66. package/dist/path-rules.js.map +1 -0
  67. package/dist/skill-member-path.d.ts +51 -0
  68. package/dist/skill-member-path.d.ts.map +1 -0
  69. package/dist/skill-member-path.js +76 -0
  70. package/dist/skill-member-path.js.map +1 -0
  71. package/dist/skill-member-path.test.d.ts +2 -0
  72. package/dist/skill-member-path.test.d.ts.map +1 -0
  73. package/dist/skill-member-path.test.js +0 -0
  74. package/dist/skill-member-path.test.js.map +1 -0
  75. package/package.json +47 -0
@@ -0,0 +1,58 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { renderCursorRules } from './cursor-rules.js';
3
+ const agent = {
4
+ id: 'reviewer',
5
+ name: 'Code Reviewer',
6
+ description: 'Reviews code for quality issues.',
7
+ capabilities: ['no trailing whitespace', 'prefer const over let'],
8
+ };
9
+ describe('renderCursorRules', () => {
10
+ it('renders capabilities as prefixed rule lines', () => {
11
+ const output = renderCursorRules([agent]);
12
+ expect(output).toContain('[Code Reviewer] no trailing whitespace');
13
+ expect(output).toContain('[Code Reviewer] prefer const over let');
14
+ });
15
+ it('includes the auto-generated header', () => {
16
+ const output = renderCursorRules([agent]);
17
+ expect(output).toContain('Auto-generated by mossbear');
18
+ });
19
+ it('does not include attribution by default', () => {
20
+ const output = renderCursorRules([agent]);
21
+ expect(output).not.toContain('Guides managed by Mossbear');
22
+ });
23
+ it('appends attribution when enabled', () => {
24
+ const output = renderCursorRules([agent], { includeAttribution: true });
25
+ expect(output).toContain('# Guides managed by Mossbear (mossbear.io)');
26
+ });
27
+ it('uses a custom attribution string when provided', () => {
28
+ const output = renderCursorRules([agent], {
29
+ includeAttribution: true,
30
+ attribution: 'Managed by Mossbear — 94% alignment score',
31
+ });
32
+ expect(output).toContain('# Managed by Mossbear — 94% alignment score');
33
+ });
34
+ it('returns a valid string for an empty agent list', () => {
35
+ const output = renderCursorRules([]);
36
+ expect(typeof output).toBe('string');
37
+ });
38
+ it('skips agents with no capabilities', () => {
39
+ const noCapAgent = { ...agent, capabilities: [] };
40
+ const output = renderCursorRules([noCapAgent]);
41
+ expect(output).not.toContain('# Code Reviewer');
42
+ expect(output).not.toContain('[Code Reviewer]');
43
+ });
44
+ it('renders multiple agents in order', () => {
45
+ const second = {
46
+ id: 'formatter',
47
+ name: 'Formatter',
48
+ description: '',
49
+ capabilities: ['use 2-space indentation'],
50
+ };
51
+ const output = renderCursorRules([agent, second]);
52
+ const reviewerPos = output.indexOf('# Code Reviewer');
53
+ const formatterPos = output.indexOf('# Formatter');
54
+ expect(reviewerPos).toBeLessThan(formatterPos);
55
+ expect(output).toContain('[Formatter] use 2-space indentation');
56
+ });
57
+ });
58
+ //# sourceMappingURL=cursor-rules.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cursor-rules.test.js","sourceRoot":"","sources":["../src/cursor-rules.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import type { GuideFileFormat } from '@mossbear/protocol';
2
+ export declare function detectFormat(path: string): GuideFileFormat | null;
3
+ export declare function isSupportedGuidePath(path: string): boolean;
4
+ /**
5
+ * True for a path inside a `skills/<name>/` tree that is NOT itself the
6
+ * SKILL.md — i.e. skill *member* content (scripts, references, assets).
7
+ * `detectFormat` deliberately returns `null` for these, so a consumer that
8
+ * resolves a member edit to its owning skill uses this to recognise them
9
+ * rather than dropping them as unknown. No caller in this repo today: the
10
+ * incremental re-sync that used it (`rules watch`) was deleted, and the full
11
+ * discovery scan groups members by walking the tree instead. Kept as part of
12
+ * this module's path-classification surface, alongside `isSupportedGuidePath`.
13
+ */
14
+ export declare function isSkillMemberPath(path: string): boolean;
15
+ //# sourceMappingURL=detect-format.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detect-format.d.ts","sourceRoot":"","sources":["../src/detect-format.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AA0BzD,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CA+DjE;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,WAEhD;AAED;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,WAG7C"}
@@ -0,0 +1,88 @@
1
+ function normalizeGuidePath(path) {
2
+ return path.replaceAll('\\', '/').replace(/\/+/g, '/').toLowerCase();
3
+ }
4
+ function isUnderSkillsDir(normalizedPath) {
5
+ return (normalizedPath.startsWith('.claude/skills/') ||
6
+ normalizedPath.includes('/.claude/skills/') ||
7
+ normalizedPath.startsWith('.agents/skills/') ||
8
+ normalizedPath.includes('/.agents/skills/'));
9
+ }
10
+ function basenameOf(normalizedPath) {
11
+ return normalizedPath.slice(normalizedPath.lastIndexOf('/') + 1);
12
+ }
13
+ function isUnderCursorRulesDir(normalizedPath) {
14
+ return (normalizedPath.startsWith('.cursor/rules/') ||
15
+ normalizedPath.includes('/.cursor/rules/'));
16
+ }
17
+ export function detectFormat(path) {
18
+ const normalizedPath = normalizeGuidePath(path);
19
+ // Checked ahead of the generic `.agents/` rule below: a SKILL.md nested
20
+ // under a `skills/` directory is a distinct artifact (npx-skills / Claude
21
+ // Code skill installs), not a freeform `.agents/` guide entry. Every other
22
+ // file in a skills/ tree (scripts, references, assets) is skill *member*
23
+ // content — imported alongside its SKILL.md by discovery, never as a
24
+ // standalone guide — so the whole subtree is opaque to the rules below
25
+ // (previously `.agents/skills/<skill>/scripts/…` leaked through the
26
+ // `.agents/` catch-all as its own agents-dir guide).
27
+ if (isUnderSkillsDir(normalizedPath)) {
28
+ return normalizedPath.endsWith('/skill.md') ? 'skill-md' : null;
29
+ }
30
+ if (normalizedPath === 'agents.md' || normalizedPath.endsWith('/agents.md')) {
31
+ return 'agents-md';
32
+ }
33
+ if (normalizedPath === 'claude.md' || normalizedPath.endsWith('/claude.md')) {
34
+ return 'claude-md';
35
+ }
36
+ if (normalizedPath === '.cursorrules' || normalizedPath.endsWith('/.cursorrules')) {
37
+ return 'cursorrules';
38
+ }
39
+ // Dot-files inside a recognised location are tooling scaffolding, not
40
+ // guidance: `mossbear init` writes `.agents/.gitkeep` so git tracks the
41
+ // otherwise-empty directory (`commands/init.ts`), and editors and VCS leave
42
+ // `.DS_Store`/`.gitignore`/`.gitattributes` beside real guides. Importing
43
+ // them files a guide the user never wrote — an empty card on the guides
44
+ // screen, an extra head on the run summary's guide count, and a body-less
45
+ // guide handed to the grader.
46
+ //
47
+ // Ordering is load-bearing: this sits *below* the `.cursorrules` branch,
48
+ // the one dot-file that is a genuine guide and is matched by exact name. A
49
+ // future dot-named format must be matched above this line too. Same shape
50
+ // as the skills branch at the top — a recognised location whose contents
51
+ // still are not, individually, guides.
52
+ if (basenameOf(normalizedPath).startsWith('.'))
53
+ return null;
54
+ // Modern Cursor "Project Rules": a directory of `.mdc` files under
55
+ // `.cursor/rules/` (project or `~/.cursor/rules/` global), distinct from the
56
+ // legacy single-file `.cursorrules` above. Cursor only reads `.mdc` files
57
+ // here, so other files (README.md, assets) fall through to `null`. Nested
58
+ // rule directories (`.cursor/rules/frontend/*.mdc`) are matched too.
59
+ if (isUnderCursorRulesDir(normalizedPath) && normalizedPath.endsWith('.mdc')) {
60
+ return 'cursor-rules';
61
+ }
62
+ if (normalizedPath.startsWith('.agents/') || normalizedPath.includes('/.agents/')) {
63
+ return 'agents-dir';
64
+ }
65
+ if (normalizedPath.startsWith('.claude/rules/') ||
66
+ normalizedPath.includes('/.claude/rules/')) {
67
+ return 'claude-rules';
68
+ }
69
+ return null;
70
+ }
71
+ export function isSupportedGuidePath(path) {
72
+ return detectFormat(path) !== null;
73
+ }
74
+ /**
75
+ * True for a path inside a `skills/<name>/` tree that is NOT itself the
76
+ * SKILL.md — i.e. skill *member* content (scripts, references, assets).
77
+ * `detectFormat` deliberately returns `null` for these, so a consumer that
78
+ * resolves a member edit to its owning skill uses this to recognise them
79
+ * rather than dropping them as unknown. No caller in this repo today: the
80
+ * incremental re-sync that used it (`rules watch`) was deleted, and the full
81
+ * discovery scan groups members by walking the tree instead. Kept as part of
82
+ * this module's path-classification surface, alongside `isSupportedGuidePath`.
83
+ */
84
+ export function isSkillMemberPath(path) {
85
+ const normalizedPath = normalizeGuidePath(path);
86
+ return isUnderSkillsDir(normalizedPath) && !normalizedPath.endsWith('/skill.md');
87
+ }
88
+ //# sourceMappingURL=detect-format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detect-format.js","sourceRoot":"","sources":["../src/detect-format.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=detect-format.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detect-format.test.d.ts","sourceRoot":"","sources":["../src/detect-format.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,104 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { detectFormat, isSkillMemberPath, isSupportedGuidePath } from './detect-format.js';
3
+ describe('detectFormat', () => {
4
+ it('detects canonical single-file formats', () => {
5
+ expect(detectFormat('AGENTS.md')).toBe('agents-md');
6
+ expect(detectFormat('/repo/CLAUDE.md')).toBe('claude-md');
7
+ expect(detectFormat('.cursorrules')).toBe('cursorrules');
8
+ });
9
+ it('detects directory-backed formats by path only', () => {
10
+ expect(detectFormat('.agents/reviewer/SKILL.md')).toBe('agents-dir');
11
+ expect(detectFormat('/repo/.claude/rules/backend.md')).toBe('claude-rules');
12
+ });
13
+ it('detects a SKILL.md nested under a skills/ directory as skill-md', () => {
14
+ expect(detectFormat('.claude/skills/reviewer/SKILL.md')).toBe('skill-md');
15
+ expect(detectFormat('/repo/.claude/skills/reviewer/SKILL.md')).toBe('skill-md');
16
+ expect(detectFormat('.agents/skills/reviewer/SKILL.md')).toBe('skill-md');
17
+ expect(detectFormat('/repo/.agents/skills/reviewer/SKILL.md')).toBe('skill-md');
18
+ });
19
+ it('ignores non-SKILL.md files inside a skills/ directory', () => {
20
+ expect(detectFormat('.claude/skills/reviewer/script.py')).toBeNull();
21
+ expect(detectFormat('.claude/skills/reviewer/README.md')).toBeNull();
22
+ // The whole skills/ subtree is opaque: under .agents/skills/ member files
23
+ // must not leak through the generic .agents/ catch-all as agents-dir
24
+ // guides — they import with their skill, as members.
25
+ expect(detectFormat('.agents/skills/reviewer/scripts/lint.py')).toBeNull();
26
+ expect(detectFormat('.agents/skills/reviewer/references/api.md')).toBeNull();
27
+ // Even an AGENTS.md nested in a skill directory is skill member content.
28
+ expect(detectFormat('.claude/skills/reviewer/references/AGENTS.md')).toBeNull();
29
+ });
30
+ it('ignores dot-files in every recognised guide location', () => {
31
+ // `mossbear init` writes .agents/.gitkeep so git tracks the directory; it is
32
+ // scaffolding, not guidance, and importing it produced an empty guide.
33
+ expect(detectFormat('.agents/.gitkeep')).toBeNull();
34
+ expect(detectFormat('/repo/.agents/.gitkeep')).toBeNull();
35
+ expect(detectFormat('C:\\repo\\.agents\\.gitkeep')).toBeNull();
36
+ expect(detectFormat('.agents/team/.DS_Store')).toBeNull();
37
+ expect(detectFormat('.claude/rules/.gitkeep')).toBeNull();
38
+ expect(detectFormat('.cursor/rules/.gitkeep')).toBeNull();
39
+ // Even a dot-file carrying a rules-directory extension stays out.
40
+ expect(detectFormat('.cursor/rules/.hidden.mdc')).toBeNull();
41
+ });
42
+ it('still detects .cursorrules, the one dot-file that is a guide', () => {
43
+ // Guards the ordering: the dot-file rejection must sit below this branch.
44
+ expect(detectFormat('.cursorrules')).toBe('cursorrules');
45
+ expect(detectFormat('/repo/.cursorrules')).toBe('cursorrules');
46
+ expect(detectFormat('C:\\repo\\.cursorrules')).toBe('cursorrules');
47
+ expect(isSupportedGuidePath('.cursorrules')).toBe(true);
48
+ });
49
+ it('detects .mdc files under .cursor/rules/ as cursor-rules', () => {
50
+ expect(detectFormat('.cursor/rules/backend.mdc')).toBe('cursor-rules');
51
+ expect(detectFormat('/repo/.cursor/rules/backend.mdc')).toBe('cursor-rules');
52
+ // Nested rule directories are supported.
53
+ expect(detectFormat('.cursor/rules/frontend/api.mdc')).toBe('cursor-rules');
54
+ // Global (~/.cursor/rules/) paths resolve to absolute home paths.
55
+ expect(detectFormat('/home/user/.cursor/rules/style.mdc')).toBe('cursor-rules');
56
+ });
57
+ it('ignores non-.mdc files under .cursor/rules/ and the legacy single file', () => {
58
+ expect(detectFormat('.cursor/rules/README.md')).toBeNull();
59
+ expect(detectFormat('.cursor/rules/assets/logo.png')).toBeNull();
60
+ // The legacy single-file format stays distinct from the directory format.
61
+ expect(detectFormat('.cursorrules')).toBe('cursorrules');
62
+ });
63
+ it('normalizes windows-style paths', () => {
64
+ expect(detectFormat('C:\\repo\\AGENTS.md')).toBe('agents-md');
65
+ expect(detectFormat('C:\\repo\\CLAUDE.md')).toBe('claude-md');
66
+ expect(detectFormat('C:\\repo\\.agents\\reviewer\\SKILL.md')).toBe('agents-dir');
67
+ expect(detectFormat('C:\\repo\\.claude\\rules\\backend.md')).toBe('claude-rules');
68
+ expect(detectFormat('C:\\repo\\.claude\\skills\\reviewer\\SKILL.md')).toBe('skill-md');
69
+ expect(detectFormat('C:\\repo\\.cursor\\rules\\backend.mdc')).toBe('cursor-rules');
70
+ });
71
+ it('returns null for unsupported paths', () => {
72
+ expect(detectFormat('README.md')).toBeNull();
73
+ expect(detectFormat('.agents')).toBeNull();
74
+ expect(detectFormat('.claude/settings.json')).toBeNull();
75
+ });
76
+ });
77
+ describe('isSupportedGuidePath', () => {
78
+ it('tracks detectFormat support', () => {
79
+ expect(isSupportedGuidePath('AGENTS.md')).toBe(true);
80
+ expect(isSupportedGuidePath('docs/guide.md')).toBe(false);
81
+ });
82
+ });
83
+ describe('isSkillMemberPath', () => {
84
+ it('is true for member content inside a skills/ tree', () => {
85
+ expect(isSkillMemberPath('.claude/skills/reviewer/script.py')).toBe(true);
86
+ expect(isSkillMemberPath('.claude/skills/reviewer/references/api.md')).toBe(true);
87
+ expect(isSkillMemberPath('/repo/.claude/skills/reviewer/assets/logo.png')).toBe(true);
88
+ expect(isSkillMemberPath('.agents/skills/reviewer/scripts/lint.py')).toBe(true);
89
+ // Even a nested AGENTS.md is member content, matching detectFormat.
90
+ expect(isSkillMemberPath('.claude/skills/reviewer/references/AGENTS.md')).toBe(true);
91
+ });
92
+ it('is false for the SKILL.md itself', () => {
93
+ expect(isSkillMemberPath('.claude/skills/reviewer/SKILL.md')).toBe(false);
94
+ expect(isSkillMemberPath('/repo/.agents/skills/reviewer/SKILL.md')).toBe(false);
95
+ expect(isSkillMemberPath('C:\\repo\\.claude\\skills\\reviewer\\SKILL.md')).toBe(false);
96
+ });
97
+ it('is false for paths outside a skills/ tree', () => {
98
+ expect(isSkillMemberPath('AGENTS.md')).toBe(false);
99
+ expect(isSkillMemberPath('.claude/rules/backend.md')).toBe(false);
100
+ expect(isSkillMemberPath('.cursor/rules/backend.mdc')).toBe(false);
101
+ expect(isSkillMemberPath('docs/guide.md')).toBe(false);
102
+ });
103
+ });
104
+ //# sourceMappingURL=detect-format.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detect-format.test.js","sourceRoot":"","sources":["../src/detect-format.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Where a **plain guide's** evaluation criteria live on disk: the eval shadow
3
+ * tree at `<scopeRoot>/.mossbear/evals/<the guide's scope-relative path>`
4
+ * (docs/plans/eval-criteria-on-disk-plan.md → Approach).
5
+ *
6
+ * One rule — *the eval file lives with its guide* — expressed two ways, because
7
+ * the two guide shapes have different structures:
8
+ *
9
+ * - a **skill** is a directory, so its criteria sit inside it as a positional
10
+ * `EVAL.md` (already shipped: `skillMemberContentType`,
11
+ * `apps/dashboard/src/server/rules/skillMembers.ts`). Nothing here touches
12
+ * that half;
13
+ * - a **plain guide** is a bare file with nowhere to put a sibling that would
14
+ * not clutter — and land inside — a discovery root, so its criteria go in
15
+ * the shadow tree, which mirrors the guide's own path.
16
+ *
17
+ * Mirroring the path is what removes the mapping table: two guides cannot share
18
+ * a path, so their shadow entries cannot collide, and the scope root carries the
19
+ * rest — a `global` guide's criteria land in `~/.mossbear/evals/.claude/CLAUDE.md`
20
+ * by the same rule that puts a `project` guide's in the repo.
21
+ *
22
+ * **The shadow tree is never guidance.** `.mossbear/` is not a discovery root
23
+ * (`discover.ts`), so nothing under it is imported as a guide — that is the
24
+ * property `rules-discover.test.ts` and `rules-manifest.test.ts` pin down. But
25
+ * "not discovered" is not the same as "not classifiable", and `detectFormat` is
26
+ * path-only: hand it `.mossbear/evals/AGENTS.md` and it answers `agents-md`,
27
+ * because the path *ends* in `agents.md`. Any future call site that classifies a
28
+ * local file it did not get from discovery — the reconciler this item unblocks —
29
+ * must therefore ask `classifyLocalItemPath` rather than `detectFormat`, which
30
+ * is why the shadow-tree check lives inside it and cannot be forgotten.
31
+ *
32
+ * **Why this lives in `projections` rather than the CLI or the protocol.**
33
+ * The server needs it too: a guide pull has to tell each client where that
34
+ * guide's criteria belong, and deriving the path on each client instead would
35
+ * be the per-clone divergence `unified-item-sync` exists to remove. Of the
36
+ * three homes that could serve both sides, this is the cheapest — the rule is
37
+ * *about* format and path semantics, which is this package's subject, and it
38
+ * needs `detectFormat`, which is already here. Putting it in
39
+ * `@mossbear/protocol` would drag this package into the dependency graph of a
40
+ * published, deliberately valibot-only contract package; re-expressing the
41
+ * skill check without `detectFormat` would duplicate the rule this module was
42
+ * written to keep in one place (Planning Principle 14).
43
+ */
44
+ import type { GuideFileFormat } from '@mossbear/protocol';
45
+ /** Scope-root-relative POSIX directory holding the eval shadow tree. */
46
+ export declare const EVAL_SHADOW_DIR = ".mossbear/evals";
47
+ /**
48
+ * The shadow-tree path for a **plain** guide, relative to the same scope root
49
+ * the guide's own path is relative to. `null` when the guide path could never
50
+ * have been discovered as a guide in the first place (absolute, escaping,
51
+ * control characters, over-long, or already inside `.mossbear/`) — and `null` for a
52
+ * skill, which keeps its criteria positionally.
53
+ */
54
+ export declare function evalShadowPath(guidePath: string): string | null;
55
+ /**
56
+ * The inverse: the guide whose criteria a shadow path holds, or `null` when the
57
+ * path is not in the shadow tree (or names the directory itself).
58
+ *
59
+ * A true inverse over the paths `evalShadowPath` accepts, and deliberately no
60
+ * wider: a remainder this function would recover but that function would never
61
+ * emit — `.mossbear/evals/.mossbear/evals/AGENTS.md`, `.mossbear/evals/.claude/skills/review/SKILL.md` —
62
+ * names no guide whose criteria belong here, so it is refused rather than
63
+ * reported as valid eval content. Without that, the shadow tree could hold an
64
+ * entry the derivation says is unreachable.
65
+ */
66
+ export declare function guidePathFromEvalShadow(shadowPath: string): string | null;
67
+ /**
68
+ * What a scope-relative path on disk *is*, for a caller holding a path rather
69
+ * than a discovery result.
70
+ *
71
+ * `eval` is checked before anything else so no path under `.mossbear/` can come
72
+ * back as a guide — see this module's header for why `detectFormat` alone gets
73
+ * that wrong.
74
+ */
75
+ export type LocalItemClassification =
76
+ /** Criteria for the guide at `guidePath`, held in the shadow tree. */
77
+ {
78
+ kind: 'eval';
79
+ guidePath: string;
80
+ }
81
+ /** A guide file, in the format `detectFormat` recognised. */
82
+ | {
83
+ kind: 'guide';
84
+ format: GuideFileFormat;
85
+ }
86
+ /** Neither: Mossbear's own state, a skill member, an unrelated file. */
87
+ | {
88
+ kind: 'other';
89
+ };
90
+ export declare function classifyLocalItemPath(path: string): LocalItemClassification;
91
+ //# sourceMappingURL=eval-path.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eval-path.d.ts","sourceRoot":"","sources":["../src/eval-path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAWH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAEzD,wEAAwE;AACxE,eAAO,MAAM,eAAe,oBAAoB,CAAA;AAIhD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAY/D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAYzE;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,uBAAuB;AACjC,sEAAsE;AACpE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE;AACrC,6DAA6D;GAC3D;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE;AAC5C,wEAAwE;GACtE;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAA;AAErB,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,uBAAuB,CAU3E"}
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Where a **plain guide's** evaluation criteria live on disk: the eval shadow
3
+ * tree at `<scopeRoot>/.mossbear/evals/<the guide's scope-relative path>`
4
+ * (docs/plans/eval-criteria-on-disk-plan.md → Approach).
5
+ *
6
+ * One rule — *the eval file lives with its guide* — expressed two ways, because
7
+ * the two guide shapes have different structures:
8
+ *
9
+ * - a **skill** is a directory, so its criteria sit inside it as a positional
10
+ * `EVAL.md` (already shipped: `skillMemberContentType`,
11
+ * `apps/dashboard/src/server/rules/skillMembers.ts`). Nothing here touches
12
+ * that half;
13
+ * - a **plain guide** is a bare file with nowhere to put a sibling that would
14
+ * not clutter — and land inside — a discovery root, so its criteria go in
15
+ * the shadow tree, which mirrors the guide's own path.
16
+ *
17
+ * Mirroring the path is what removes the mapping table: two guides cannot share
18
+ * a path, so their shadow entries cannot collide, and the scope root carries the
19
+ * rest — a `global` guide's criteria land in `~/.mossbear/evals/.claude/CLAUDE.md`
20
+ * by the same rule that puts a `project` guide's in the repo.
21
+ *
22
+ * **The shadow tree is never guidance.** `.mossbear/` is not a discovery root
23
+ * (`discover.ts`), so nothing under it is imported as a guide — that is the
24
+ * property `rules-discover.test.ts` and `rules-manifest.test.ts` pin down. But
25
+ * "not discovered" is not the same as "not classifiable", and `detectFormat` is
26
+ * path-only: hand it `.mossbear/evals/AGENTS.md` and it answers `agents-md`,
27
+ * because the path *ends* in `agents.md`. Any future call site that classifies a
28
+ * local file it did not get from discovery — the reconciler this item unblocks —
29
+ * must therefore ask `classifyLocalItemPath` rather than `detectFormat`, which
30
+ * is why the shadow-tree check lives inside it and cannot be forgotten.
31
+ *
32
+ * **Why this lives in `projections` rather than the CLI or the protocol.**
33
+ * The server needs it too: a guide pull has to tell each client where that
34
+ * guide's criteria belong, and deriving the path on each client instead would
35
+ * be the per-clone divergence `unified-item-sync` exists to remove. Of the
36
+ * three homes that could serve both sides, this is the cheapest — the rule is
37
+ * *about* format and path semantics, which is this package's subject, and it
38
+ * needs `detectFormat`, which is already here. Putting it in
39
+ * `@mossbear/protocol` would drag this package into the dependency graph of a
40
+ * published, deliberately valibot-only contract package; re-expressing the
41
+ * skill check without `detectFormat` would duplicate the rule this module was
42
+ * written to keep in one place (Planning Principle 14).
43
+ */
44
+ import { detectFormat } from './detect-format.js';
45
+ import { isSafeRelativePath, isSkillGuidePath, isUnderMossbearDir, segmentsOf, toPosix, } from './path-rules.js';
46
+ /** Scope-root-relative POSIX directory holding the eval shadow tree. */
47
+ export const EVAL_SHADOW_DIR = '.mossbear/evals';
48
+ const MOSSBEAR_DIR_SEGMENT = '.mossbear';
49
+ /**
50
+ * The shadow-tree path for a **plain** guide, relative to the same scope root
51
+ * the guide's own path is relative to. `null` when the guide path could never
52
+ * have been discovered as a guide in the first place (absolute, escaping,
53
+ * control characters, over-long, or already inside `.mossbear/`) — and `null` for a
54
+ * skill, which keeps its criteria positionally.
55
+ */
56
+ export function evalShadowPath(guidePath) {
57
+ const normalized = toPosix(guidePath);
58
+ if (!isSafeRelativePath(normalized))
59
+ return null;
60
+ if (isUnderMossbearDir(normalized))
61
+ return null;
62
+ if (isSkillGuidePath(normalized))
63
+ return null;
64
+ const shadowPath = `${EVAL_SHADOW_DIR}/${normalized}`;
65
+ // The shadow path is itself carried in payloads and sidecars, so a guide path
66
+ // that is safe on its own but pushes the shadow path past the 512-char bound
67
+ // has no representable home — say so rather than emitting a path that fails
68
+ // validation one layer later.
69
+ return isSafeRelativePath(shadowPath) ? shadowPath : null;
70
+ }
71
+ /**
72
+ * The inverse: the guide whose criteria a shadow path holds, or `null` when the
73
+ * path is not in the shadow tree (or names the directory itself).
74
+ *
75
+ * A true inverse over the paths `evalShadowPath` accepts, and deliberately no
76
+ * wider: a remainder this function would recover but that function would never
77
+ * emit — `.mossbear/evals/.mossbear/evals/AGENTS.md`, `.mossbear/evals/.claude/skills/review/SKILL.md` —
78
+ * names no guide whose criteria belong here, so it is refused rather than
79
+ * reported as valid eval content. Without that, the shadow tree could hold an
80
+ * entry the derivation says is unreachable.
81
+ */
82
+ export function guidePathFromEvalShadow(shadowPath) {
83
+ const normalized = toPosix(shadowPath);
84
+ const segments = segmentsOf(normalized);
85
+ if (segments[0] !== MOSSBEAR_DIR_SEGMENT || segments[1] !== 'evals')
86
+ return null;
87
+ // Slice the original (not the lower-cased copy) so the guide path keeps the
88
+ // casing it was written with — `AGENTS.md` must not come back as `agents.md`.
89
+ const remainder = normalized.split('/').slice(2).join('/');
90
+ if (remainder.length === 0)
91
+ return null;
92
+ if (!isSafeRelativePath(remainder))
93
+ return null;
94
+ if (isUnderMossbearDir(remainder) || isSkillGuidePath(remainder))
95
+ return null;
96
+ return remainder;
97
+ }
98
+ export function classifyLocalItemPath(path) {
99
+ const guidePath = guidePathFromEvalShadow(path);
100
+ if (guidePath !== null)
101
+ return { kind: 'eval', guidePath };
102
+ // Everything else under `.mossbear/` is Mossbear's own state (`docs.json`, the repo
103
+ // config, pulled `context/`) — and an unusable shadow path (`.mossbear/evals/..`)
104
+ // lands here too. Never guidance either way.
105
+ if (isUnderMossbearDir(path))
106
+ return { kind: 'other' };
107
+ const format = detectFormat(toPosix(path));
108
+ return format === null ? { kind: 'other' } : { kind: 'guide', format };
109
+ }
110
+ //# sourceMappingURL=eval-path.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eval-path.js","sourceRoot":"","sources":["../src/eval-path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACjD,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,kBAAkB,EAClB,UAAU,EACV,OAAO,GACR,MAAM,iBAAiB,CAAA;AAIxB,wEAAwE;AACxE,MAAM,CAAC,MAAM,eAAe,GAAG,iBAAiB,CAAA;AAEhD,MAAM,oBAAoB,GAAG,WAAW,CAAA;AAExC;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,SAAiB;IAC9C,MAAM,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAAA;IACrC,IAAI,CAAC,kBAAkB,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAA;IAChD,IAAI,kBAAkB,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAA;IAC/C,IAAI,gBAAgB,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAA;IAE7C,MAAM,UAAU,GAAG,GAAG,eAAe,IAAI,UAAU,EAAE,CAAA;IACrD,8EAA8E;IAC9E,6EAA6E;IAC7E,4EAA4E;IAC5E,8BAA8B;IAC9B,OAAO,kBAAkB,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAA;AAC3D,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,uBAAuB,CAAC,UAAkB;IACxD,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAAA;IACtC,MAAM,QAAQ,GAAG,UAAU,CAAC,UAAU,CAAC,CAAA;IACvC,IAAI,QAAQ,CAAC,CAAC,CAAC,KAAK,oBAAoB,IAAI,QAAQ,CAAC,CAAC,CAAC,KAAK,OAAO;QAAE,OAAO,IAAI,CAAA;IAEhF,4EAA4E;IAC5E,8EAA8E;IAC9E,MAAM,SAAS,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IAC1D,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IACvC,IAAI,CAAC,kBAAkB,CAAC,SAAS,CAAC;QAAE,OAAO,IAAI,CAAA;IAC/C,IAAI,kBAAkB,CAAC,SAAS,CAAC,IAAI,gBAAgB,CAAC,SAAS,CAAC;QAAE,OAAO,IAAI,CAAA;IAC7E,OAAO,SAAS,CAAA;AAClB,CAAC;AAkBD,MAAM,UAAU,qBAAqB,CAAC,IAAY;IAChD,MAAM,SAAS,GAAG,uBAAuB,CAAC,IAAI,CAAC,CAAA;IAC/C,IAAI,SAAS,KAAK,IAAI;QAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,CAAA;IAC1D,oFAAoF;IACpF,kFAAkF;IAClF,6CAA6C;IAC7C,IAAI,kBAAkB,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAA;IAEtD,MAAM,MAAM,GAAG,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA;IAC1C,OAAO,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,CAAA;AACxE,CAAC"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @mossbear/projections
3
+ *
4
+ * Pure projection logic for canonical AGENTS.md and .agents/ outputs,
5
+ * plus compatibility exporters for platform-native formats.
6
+ * This package must remain importable outside the CLI package.
7
+ */
8
+ export * from './agents-md.js';
9
+ export * from './eval-path.js';
10
+ export * from './agents-dir.js';
11
+ export * from './claude-md.js';
12
+ export * from './claude-rules.js';
13
+ export * from './compatibility-attribution.js';
14
+ export * from './cursor-rules.js';
15
+ export * from './detect-format.js';
16
+ export * from './skill-member-path.js';
17
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,mBAAmB,CAAA;AACjC,cAAc,gCAAgC,CAAA;AAC9C,cAAc,mBAAmB,CAAA;AACjC,cAAc,oBAAoB,CAAA;AAClC,cAAc,wBAAwB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @mossbear/projections
3
+ *
4
+ * Pure projection logic for canonical AGENTS.md and .agents/ outputs,
5
+ * plus compatibility exporters for platform-native formats.
6
+ * This package must remain importable outside the CLI package.
7
+ */
8
+ export * from './agents-md.js';
9
+ export * from './eval-path.js';
10
+ export * from './agents-dir.js';
11
+ export * from './claude-md.js';
12
+ export * from './claude-rules.js';
13
+ export * from './compatibility-attribution.js';
14
+ export * from './cursor-rules.js';
15
+ export * from './detect-format.js';
16
+ export * from './skill-member-path.js';
17
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,mBAAmB,CAAA;AACjC,cAAc,gCAAgC,CAAA;AAC9C,cAAc,mBAAmB,CAAA;AACjC,cAAc,oBAAoB,CAAA;AAClC,cAAc,wBAAwB,CAAA"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The scope-relative path predicates that both attached-file derivations share.
3
+ *
4
+ * A guide's attached files land in two different places depending on the guide's
5
+ * shape — a plain guide's criteria go to the eval shadow tree (`eval-path.ts`), a
6
+ * skill's members go inside its own directory (`skill-member-path.ts`) — but both
7
+ * derivations answer the same safety questions first, and a second copy of those
8
+ * answers is a second thing to get wrong. They live here so the two derivations
9
+ * cannot drift on what counts as a safe path or as a skill.
10
+ */
11
+ export declare function toPosix(path: string): string;
12
+ export declare function isSafeRelativePath(path: string): boolean;
13
+ /**
14
+ * Split a scope-relative path into its lower-cased segments.
15
+ *
16
+ * Lower-cased because recognition and derivation pull in opposite directions:
17
+ * a shadow path is *written* as exactly `.mossbear/evals/…`, but on a
18
+ * case-insensitive filesystem `.Mossbear/Evals/AGENTS.md` names the same file, and
19
+ * the two ways of being wrong about it are not symmetric — classifying eval
20
+ * criteria as guidance hands a rubric to an agent as instructions, while
21
+ * classifying a stray capitalised path as eval content merely declines to treat
22
+ * it as a guide. Recognition therefore leans the safe way.
23
+ */
24
+ export declare function segmentsOf(path: string): string[];
25
+ /** True when `path` is inside `.mossbear/`, whatever it holds. */
26
+ export declare function isUnderMossbearDir(path: string): boolean;
27
+ /**
28
+ * True when a path is a skill's own `SKILL.md`.
29
+ *
30
+ * The rule is *the attached file lives with its guide*, and a skill's guide is a
31
+ * directory — so its criteria are the positional `EVAL.md` inside it and its
32
+ * members are the rest of that directory
33
+ * (`skillMemberContentType`, `apps/dashboard/src/server/rules/skillMembers.ts:60-66`).
34
+ * Shadowing a skill as well would put one skill's rubric in two places, split it
35
+ * out of the directory that makes a skill portable, and hand the next reconciler
36
+ * two rows to disagree over — the "two shapes of one fact" CLAUDE.md → Simplicity
37
+ * refuses. `detectFormat` is the same test discovery uses, so what counts as a
38
+ * skill here cannot drift from what counts as one there.
39
+ *
40
+ * This one predicate partitions the two derivations: `evalShadowPath` returns
41
+ * null exactly where this is true, and `skillMemberPath` returns null exactly
42
+ * where it is false.
43
+ */
44
+ export declare function isSkillGuidePath(path: string): boolean;
45
+ //# sourceMappingURL=path-rules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-rules.d.ts","sourceRoot":"","sources":["../src/path-rules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAeH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5C;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAExD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAEjD;AAED,kEAAkE;AAClE,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAExD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtD"}