@things-factory/figure-service 10.1.157 → 10.1.159

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/figure-service",
3
- "version": "10.1.157",
3
+ "version": "10.1.159",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "@things-factory:registry": "https://registry.npmjs.org"
@@ -27,16 +27,16 @@
27
27
  "dependencies": {
28
28
  "@hatiolab/figure-model": "^0.6.5",
29
29
  "@operato/typeorm-history": "^10.0.0",
30
- "@things-factory/ai-client-base": "^10.1.157",
31
- "@things-factory/attachment-base": "^10.1.157",
32
- "@things-factory/auth-base": "^10.1.157",
30
+ "@things-factory/ai-client-base": "^10.1.159",
31
+ "@things-factory/attachment-base": "^10.1.159",
32
+ "@things-factory/auth-base": "^10.1.159",
33
33
  "@things-factory/env": "^10.1.149",
34
- "@things-factory/setting-base": "^10.1.157",
34
+ "@things-factory/setting-base": "^10.1.159",
35
35
  "@things-factory/shell": "^10.1.157",
36
36
  "graphql-scalars": "^1.22.4",
37
37
  "graphql-upload": "^15.0.2",
38
38
  "jszip": "^3.10.1",
39
39
  "koa-router": "^7.4.0"
40
40
  },
41
- "gitHead": "bde810f804966549a7041457ad7ede40fcb78488"
41
+ "gitHead": "fc15cdcb18e562ca1e723a1e1bf042f9582cbbfb"
42
42
  }
package/server/index.ts CHANGED
@@ -9,6 +9,8 @@ import './routes.js'
9
9
  */
10
10
  import { registerFigureToolCategories } from './service/figure/figure-tools.js'
11
11
  registerFigureToolCategories()
12
+ /* The figure the modeller has open, said to the AI each turn (design/ai/screen-pattern.md §2-1). */
13
+ import './service/figure/figure-subject.js'
12
14
 
13
15
  /*
14
16
  * The recipe templates are read once at start (ADR-0095, amended): what was served, and any file whose form is wrong,
@@ -90,15 +90,25 @@ describe('the namespace a figure is created under', () => {
90
90
  expect(figure.type).toBe('conveyor')
91
91
  })
92
92
 
93
- it('is refused by name when the domain has none, or an invalid one', async () => {
94
- for (const value of [null, 'Hatiolab.COM']) {
95
- const { context } = await domainWith(value)
96
- await expect(
97
- new FigureMutation().saveFigure(releasable(), context, undefined, undefined, creationId(), undefined, 'conveyor')
98
- ).rejects.toMatchObject({ code: 'figure-namespace-missing' })
93
+ it('with no setting, or an empty one, is the subdomain', async () => {
94
+ for (const value of [null, '', ' ']) {
95
+ const { domain, context } = await domainWith(value)
96
+ const figure = await new FigureMutation().saveFigure(releasable(), context, undefined, undefined, creationId(), undefined, 'conveyor')
97
+ expect(figure.namespace).toBe(domain.subdomain)
99
98
  }
100
99
  })
101
100
 
101
+ it('is refused by name when the setting is not a namespace, or there is none and the subdomain is not one either', async () => {
102
+ const { context } = await domainWith('Hatiolab.COM')
103
+ await expect(
104
+ new FigureMutation().saveFigure(releasable(), context, undefined, undefined, creationId(), undefined, 'conveyor')
105
+ ).rejects.toMatchObject({ code: 'figure-namespace-missing' })
106
+ const odd = await dataSource!.getRepository(shell.Domain).save({ name: `odd-${++seq}`, subdomain: 'Odd_Sub' })
107
+ await expect(
108
+ new FigureMutation().saveFigure(releasable(), { state: { domain: odd, user: undefined, tx: dataSource!.manager } }, undefined, undefined, creationId(), undefined, 'conveyor')
109
+ ).rejects.toMatchObject({ code: 'figure-namespace-missing' })
110
+ })
111
+
102
112
  it('cannot change: a new figure or a release is refused while the setting disagrees with the figures already made', async () => {
103
113
  const { domain, context } = await domainWith('hatiolab.com')
104
114
  const draft = await new FigureMutation().saveFigure(releasable(), context, undefined, undefined, creationId(), undefined, 'conveyor')
@@ -13,14 +13,17 @@ export const FIGURE_NAMESPACE_SETTING = 'figure-namespace'
13
13
  * The namespace a new figure is created under.
14
14
  *
15
15
  * It is read from the domain setting and copied onto the figure, so the full name `{namespace}/{type}` a board stores
16
- * never moves. The setting is set once: a value that differs from the namespace this domain's figures already carry
17
- * is refused, so editing it on the settings screen cannot rename anything.
16
+ * never moves. A domain with no setting, or an empty one, takes its subdomain (user, 2026-10-13: 「기본적으로는
17
+ * subdomain 명을 사용하면 되는데」) -- refused only when the subdomain is not a namespace either. A value written in
18
+ * the setting is used as it is, and refused when it is not a namespace. Either way it is set once: a value that differs
19
+ * from the namespace this domain's figures already carry is refused, so editing the setting cannot rename anything.
18
20
  */
19
21
  export async function namespaceForNewFigure(tx: EntityManager, domain: Domain): Promise<string> {
20
22
  const setting = await tx.getRepository(Setting).findOne({
21
23
  where: { domain: { id: domain.id }, name: FIGURE_NAMESPACE_SETTING }
22
24
  })
23
- const namespace = setting?.value?.trim() ?? ''
25
+ const written = setting?.value?.trim() ?? ''
26
+ const namespace = written || (domain.subdomain ?? '').trim()
24
27
  if (!isFigureNamespace(namespace)) {
25
28
  throw new Refusal(
26
29
  'figure-namespace-missing',
@@ -0,0 +1,41 @@
1
+ /*
2
+ * The modeller's open figure, named for the AI (operato-application design/ai/screen-pattern.md §2-1).
3
+ */
4
+ const mockFigures: any[] = [
5
+ { id: 'f1', domain: 'plant-a', name: '선풍기', type: 'system/fan' },
6
+ { id: 'f2', domain: 'plant-a', name: '', type: 'system/agv-lift' },
7
+ { id: 'f3', domain: 'plant-b', name: '남의 도형', type: 'other/one' }
8
+ ]
9
+ const mockRegistered: any[] = []
10
+
11
+ jest.mock('@things-factory/shell', () => ({
12
+ getRepository: (name: string) => {
13
+ if (name !== 'Figure') throw new Error(`unexpected repository ${name}`)
14
+ return {
15
+ findOne: async ({ where }: any) => mockFigures.find(one => one.id === where.id && one.domain === where.domain.id) ?? null
16
+ }
17
+ }
18
+ }))
19
+ jest.mock('@things-factory/ai-client-base', () => ({ registerAssistantSubject: (one: any) => mockRegistered.push(one) }))
20
+
21
+ /* Required after the mocks hold their lists - an import would load it before them. */
22
+ const { figureSubject } = require('./figure-subject')
23
+
24
+ describe('the figure the modeller has open', () => {
25
+ it('★ is registered for the figure anchor when the package loads', () => {
26
+ expect(mockRegistered).toEqual([figureSubject])
27
+ expect(figureSubject.anchorType).toBe('figure')
28
+ })
29
+
30
+ it('★ is named by its name, in this domain', async () => {
31
+ expect(await figureSubject.describe({ anchorId: 'f1', domain: { id: 'plant-a' } })).toEqual({ noun: 'figure', name: '선풍기' })
32
+ })
33
+
34
+ it('a figure with no name yet is called by its type', async () => {
35
+ expect(await figureSubject.describe({ anchorId: 'f2', domain: { id: 'plant-a' } })).toEqual({ noun: 'figure', name: 'system/agv-lift' })
36
+ })
37
+
38
+ it('★ another domain\'s figure is not named', async () => {
39
+ expect(await figureSubject.describe({ anchorId: 'f3', domain: { id: 'plant-a' } })).toBeUndefined()
40
+ })
41
+ })
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The figure the modeller has open, named for the AI (operato-application design/ai/screen-pattern.md §2-1).
3
+ *
4
+ * A conversation in the modeller is anchored to the open figure. Each turn the door reads this to say which figure
5
+ * that is, so the model starts from "the figure 선풍기 is open" and passes its id to the tools. A figure of another
6
+ * domain, or one that is gone, is not named.
7
+ */
8
+ import { registerAssistantSubject } from '@things-factory/ai-client-base'
9
+ import { getRepository } from '@things-factory/shell'
10
+
11
+ /* By entity name - the entity's decorators stay out of what a test of this has to load. */
12
+ export const figureSubject = {
13
+ anchorType: 'figure',
14
+ async describe({ anchorId, domain }: { anchorId: string; domain: { id: string } }) {
15
+ const figure: any = await getRepository('Figure').findOne({ where: { id: anchorId, domain: { id: domain.id } } })
16
+ if (!figure) return undefined
17
+ return { noun: 'figure', name: figure.name || figure.type }
18
+ }
19
+ }
20
+
21
+ registerAssistantSubject(figureSubject)