@fugood/bricks-ctor 2.25.0-beta.46 → 2.25.0-beta.47

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.
@@ -64,6 +64,28 @@ describe('validateConfig', () => {
64
64
  test('does not throw when error/output are falsy and inputs are empty', () => {
65
65
  expect(() => validateConfig(baseConfig())).not.toThrow()
66
66
  })
67
+
68
+ test('throws when error and output target the same id', () => {
69
+ const config = baseConfig({ output: 'shared', error: 'shared' })
70
+ expect(() => validateConfig(config)).toThrow(/key: error\/output/)
71
+ })
72
+
73
+ test('throws when output collides with an outputs target id', () => {
74
+ const config = baseConfig({ output: 'shared', outputs: { x: ['shared'] } })
75
+ expect(() => validateConfig(config)).toThrow(/key: output\/outputs/)
76
+ })
77
+
78
+ test('throws when error collides with an outputs target id', () => {
79
+ const config = baseConfig({ error: 'shared', outputs: { x: ['shared'] } })
80
+ expect(() => validateConfig(config)).toThrow(/key: error\/outputs/)
81
+ })
82
+
83
+ // The same id reused across *different* outputs entries is a supported last-wins case
84
+ // (see generateCalulationMap test below) and must stay allowed.
85
+ test('allows the same id across multiple outputs entries', () => {
86
+ const config = baseConfig({ outputs: { first: ['pb1'], second: ['pb1'] } })
87
+ expect(() => validateConfig(config)).not.toThrow()
88
+ })
67
89
  })
68
90
 
69
91
  // generateCalulationMap now seeds command ids from the owning calc id; tests that don't
package/compile/util.ts CHANGED
@@ -27,6 +27,22 @@ export const validateConfig = (config: ScriptConfig) => {
27
27
  if (Object.values(config.outputs).some((value) => value.some((id) => config.inputs[id]))) {
28
28
  throw new Error(`${errorMsg}. key: outputs`)
29
29
  }
30
+ // The same data-node id reused across the output-side targets (output / error / outputs)
31
+ // also collides: generateCalulationMap spreads their node objects last-wins, so the later
32
+ // one silently overwrites the earlier wiring (e.g. error == output drops the error change
33
+ // link). The checks above only compare against `inputs`, so guard the output-side pairs
34
+ // too. (The same id appearing in multiple `outputs` entries is a supported last-wins case
35
+ // and stays allowed.)
36
+ if (config.error && config.output && config.error === config.output) {
37
+ throw new Error(`${errorMsg}. key: error/output`)
38
+ }
39
+ const outputsIds = Object.values(config.outputs).flat()
40
+ if (config.output && outputsIds.includes(config.output)) {
41
+ throw new Error(`${errorMsg}. key: output/outputs`)
42
+ }
43
+ if (config.error && outputsIds.includes(config.error)) {
44
+ throw new Error(`${errorMsg}. key: error/outputs`)
45
+ }
30
46
  }
31
47
 
32
48
  const padding = 15
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fugood/bricks-ctor",
3
- "version": "2.25.0-beta.46",
3
+ "version": "2.25.0-beta.47",
4
4
  "main": "index.ts",
5
5
  "scripts": {
6
6
  "typecheck": "tsc --noEmit",
@@ -29,5 +29,5 @@
29
29
  "peerDependencies": {
30
30
  "oxfmt": "^0.36.0"
31
31
  },
32
- "gitHead": "877bdee4953bcdc09b2dd04f28c8d7edc37824ab"
32
+ "gitHead": "95f194a3fb6331c0f25a879e55675e3ff331a412"
33
33
  }
@@ -0,0 +1,49 @@
1
+ import { buildGGUFSplitFiles } from '../huggingface'
2
+
3
+ declare const describe: (name: string, fn: () => void) => void
4
+ declare const it: (name: string, fn: () => void) => void
5
+ declare const expect: (actual: unknown) => { toEqual: (expected: unknown) => void }
6
+
7
+ describe('buildGGUFSplitFiles', () => {
8
+ it('builds split GGUF files with indexed sibling metadata lookup', () => {
9
+ const siblings = [
10
+ {
11
+ rfilename: 'other-file.txt',
12
+ size: 999,
13
+ },
14
+ {
15
+ rfilename: 'model-00001-of-00003.gguf',
16
+ size: 1,
17
+ lfs: { sha256: 'one' },
18
+ },
19
+ {
20
+ rfilename: 'model-00002-of-00003.gguf',
21
+ size: 2,
22
+ lfs: { sha256: 'two' },
23
+ },
24
+ {
25
+ rfilename: 'model-00002-of-00003.gguf',
26
+ size: 22,
27
+ lfs: { sha256: 'two-late' },
28
+ },
29
+ ]
30
+
31
+ expect(buildGGUFSplitFiles('model-00001-of-00003.gguf', '00003', siblings)).toEqual([
32
+ {
33
+ rfilename: 'model-00001-of-00003.gguf',
34
+ size: 1,
35
+ lfs: { sha256: 'one' },
36
+ },
37
+ {
38
+ rfilename: 'model-00002-of-00003.gguf',
39
+ size: 2,
40
+ lfs: { sha256: 'two' },
41
+ },
42
+ {
43
+ rfilename: 'model-00003-of-00003.gguf',
44
+ size: undefined,
45
+ lfs: undefined,
46
+ },
47
+ ])
48
+ })
49
+ })
@@ -0,0 +1,21 @@
1
+ import { searchIcons } from '../icons'
2
+
3
+ declare const describe: (name: string, fn: () => void) => void
4
+ declare const test: (name: string, fn: () => void) => void
5
+ declare const expect: (actual: unknown) => {
6
+ toBe: (expected: unknown) => void
7
+ toHaveLength: (expected: number) => void
8
+ }
9
+
10
+ describe('icon_search helpers', () => {
11
+ test('searches the selected style corpus directly', () => {
12
+ const results = searchIcons('x', 10, 'brands')
13
+
14
+ expect(results).toHaveLength(10)
15
+ expect(results.every((result) => result.item.styles.includes('brands'))).toBe(true)
16
+ })
17
+
18
+ test('keeps unstyled searches capped by limit', () => {
19
+ expect(searchIcons('arrow', 5)).toHaveLength(5)
20
+ })
21
+ })
@@ -269,6 +269,28 @@ const fetchHFModelDetails = async (modelId: string): Promise<HFModel> => {
269
269
  // Example: Mixtral-8x22B-v0.1.IQ3_XS-00001-of-00005.gguf
270
270
  const ggufSplitPattern = /-(\d{5})-of-(\d{5})\.gguf$/
271
271
 
272
+ export const buildGGUFSplitFiles = (
273
+ filename: string,
274
+ splitTotal: string,
275
+ siblings: HFSibling[],
276
+ ): HFSibling[] => {
277
+ const siblingByFilename = new Map<string, HFSibling>()
278
+ for (const sibling of siblings) {
279
+ if (!siblingByFilename.has(sibling.rfilename)) siblingByFilename.set(sibling.rfilename, sibling)
280
+ }
281
+
282
+ return Array.from({ length: Number(splitTotal) }, (_, i) => {
283
+ const split = String(i + 1).padStart(5, '0')
284
+ const splitRFilename = filename.replace(ggufSplitPattern, `-${split}-of-${splitTotal}.gguf`)
285
+ const sibling = siblingByFilename.get(splitRFilename)
286
+ return {
287
+ rfilename: splitRFilename,
288
+ size: sibling?.size,
289
+ lfs: sibling?.lfs,
290
+ }
291
+ })
292
+ }
293
+
272
294
  export function register(server: McpServer) {
273
295
  server.tool(
274
296
  'huggingface_search',
@@ -657,19 +679,7 @@ export function register(server: McpServer) {
657
679
 
658
680
  if (isSplit) {
659
681
  const [, , splitTotal] = matched!
660
- const splitFiles = Array.from({ length: Number(splitTotal) }, (_, i) => {
661
- const split = String(i + 1).padStart(5, '0')
662
- const splitRFilename = filename.replace(
663
- ggufSplitPattern,
664
- `-${split}-of-${splitTotal}.gguf`,
665
- )
666
- const sibling = siblings.find((sb) => sb.rfilename === splitRFilename)
667
- return {
668
- rfilename: splitRFilename,
669
- size: sibling?.size,
670
- lfs: sibling?.lfs,
671
- }
672
- })
682
+ const splitFiles = buildGGUFSplitFiles(filename, splitTotal, siblings)
673
683
 
674
684
  const first = splitFiles[0]
675
685
  const rest = splitFiles.slice(1)
@@ -36,11 +36,31 @@ const iconList = Object.entries(glyphmap as Record<string, number>).map(([name,
36
36
  return { name, code, styles }
37
37
  })
38
38
 
39
- const iconFuse = new Fuse(iconList, {
39
+ const fuseOptions = {
40
40
  keys: ['name'],
41
41
  threshold: 0.3,
42
42
  includeScore: true,
43
- })
43
+ }
44
+
45
+ const iconFuse = new Fuse(iconList, fuseOptions)
46
+ const iconFuseByStyle = new Map<IconStyle, Fuse<(typeof iconList)[number]>>()
47
+
48
+ function getStyleFuse(style: IconStyle) {
49
+ let fuse = iconFuseByStyle.get(style)
50
+ if (!fuse) {
51
+ fuse = new Fuse(
52
+ iconList.filter((icon) => icon.styles.includes(style)),
53
+ fuseOptions,
54
+ )
55
+ iconFuseByStyle.set(style, fuse)
56
+ }
57
+ return fuse
58
+ }
59
+
60
+ export function searchIcons(query: string, limit: number, style?: IconStyle) {
61
+ if (style) return getStyleFuse(style).search(query, { limit })
62
+ return iconFuse.search(query, { limit })
63
+ }
44
64
 
45
65
  export function register(server: McpServer) {
46
66
  server.tool(
@@ -54,11 +74,7 @@ export function register(server: McpServer) {
54
74
  .describe('Filter by icon style'),
55
75
  },
56
76
  async ({ query, limit, style }) => {
57
- let results = iconFuse.search(query, { limit: style ? limit * 3 : limit })
58
-
59
- if (style) {
60
- results = results.filter((r) => r.item.styles.includes(style)).slice(0, limit)
61
- }
77
+ const results = searchIcons(query, limit, style)
62
78
 
63
79
  const icons = results.map((r) => ({
64
80
  name: r.item.name,