@things-factory/figure-service 10.1.170 → 10.1.171

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.170",
3
+ "version": "10.1.171",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "@things-factory:registry": "https://registry.npmjs.org"
@@ -38,5 +38,5 @@
38
38
  "jszip": "^3.10.1",
39
39
  "koa-router": "^7.4.0"
40
40
  },
41
- "gitHead": "1956b73789eace6376f1d79443306e1bb8e61908"
41
+ "gitHead": "e88536db0bc3cbb759fe58f9efeef66a67fef2c3"
42
42
  }
@@ -8,7 +8,7 @@
8
8
  import { FIGURE_PROPOSAL_GRAMMAR, addFigureBox, applyFigureAuthoring, applyFigureProposal, createFigureAsset, inspectFigureAsset, proposeFigureOccupancy } from '@hatiolab/figure-model'
9
9
  import { AIAnswerNotJSON, type AIClient } from '@things-factory/ai-client-base'
10
10
 
11
- import { ProposeFigureFailure, judgeFigureAnswer, proposalFailureForPerson, proposeFigureSteps, rulesPromptFigure } from './figure-propose-steps'
11
+ import { ProposeFigureFailure, WORKED_EXAMPLE, judgeFigureAnswer, proposalFailureForPerson, proposeFigureSteps, rulesPromptFigure } from './figure-propose-steps'
12
12
 
13
13
  function stubClient(answers: unknown[]): AIClient & { calls: number; sent: any[][]; system?: string } {
14
14
  const client = {
@@ -61,6 +61,23 @@ describe('the model is told the kernel\'s commands and the open draft', () => {
61
61
  frame rail by hand, sixty steps, and fastened a rail twice (ATTACH_REPLACED at steps[59]); another named a part it
62
62
  had not added. Both are rules of writing, so they are taught with the rules.
63
63
  */
64
+ /*
65
+ Three walks on :3300 (2026-10-16) stopped at three fastening rules in turn -- a part not added, flush on the mounting
66
+ plane, a face that does not meet -- each refusal saying what to do, and none put right in three tries. A rule a line
67
+ at a time did not end it; one whole answer the kernel takes shows the rules together. It is held to the kernel here,
68
+ on the path an answer takes, so it cannot teach what the kernel refuses.
69
+ */
70
+ it('shows one whole answer, and that answer is one the kernel and the gate take', () => {
71
+ expect(rulesPromptFigure()).toContain(JSON.stringify({ steps: WORKED_EXAMPLE }))
72
+ const judged = judgeFigureAnswer(createFigureAsset('Roller conveyor'), { steps: WORKED_EXAMPLE }, false)
73
+ expect(judged.reasons).toEqual([])
74
+ expect(judged.source!.appearance.map(a => a.target)).toEqual(['frame', 'support', 'roller'])
75
+ const kinds = WORKED_EXAMPLE.map(step => step.kind)
76
+ expect(kinds).toContain('make-repeat')
77
+ expect(kinds.at(-1)).toBe('fit-room')
78
+ expect(WORKED_EXAMPLE.filter(step => step.kind === 'attach' && 'plane' in (step as any).to).every(step => !('facing' in step))).toBe(true)
79
+ })
80
+
64
81
  it('teaches a row of the same part as one part repeated, and each part fastened once', () => {
65
82
  const rules = rulesPromptFigure()
66
83
  expect(rules).toMatch(/same part .*make-repeat/)
@@ -77,6 +77,25 @@ export function proposalFailureForPerson(failure: ProposeFigureFailure, t: (key:
77
77
  return [t('error.figure-proposal-failed', { defaultValue: failure.message }), ...failure.reasons].join('\n')
78
78
  }
79
79
 
80
+ /**
81
+ * One whole answer the kernel and the gate take: a roller conveyor on two end supports. Three walks on :3300
82
+ * (2026-10-16) stopped at three fastening rules in turn, each refusal saying what to do and none put right; one answer
83
+ * shows them together -- the plane taken without facing, a part fastened under another, a row made once. Held to the
84
+ * kernel by its test, on the path an answer takes.
85
+ */
86
+ export const WORKED_EXAMPLE: FigureProposalStep[] = [
87
+ { kind: 'add-part', part: { id: 'frame', kind: 'box', dimensions: { width: 2000, height: 100, depth: 600 } } },
88
+ { kind: 'attach', part: 'frame', face: 'bottom', to: { plane: 'mounting-plane' }, gap: 700 },
89
+ { kind: 'add-part', part: { id: 'support', kind: 'box', dimensions: { width: 80, height: 700, depth: 560 } } },
90
+ { kind: 'attach', part: 'support', face: 'top', to: { part: 'frame', face: 'bottom' }, align: { x: 'min' } },
91
+ { kind: 'make-repeat', part: 'support', axis: 'x', pitch: 1920, count: 2 },
92
+ { kind: 'add-part', part: { id: 'roller', kind: 'box', dimensions: { width: 60, height: 60, depth: 560 } } },
93
+ { kind: 'attach', part: 'roller', face: 'bottom', to: { part: 'frame', face: 'top' } },
94
+ { kind: 'make-repeat', part: 'roller', axis: 'x', pitch: 160 },
95
+ { kind: 'set-material', target: 'frame', material: { token: 'palette.frame' }, palette: { 'palette.frame': '#5b6770' } },
96
+ { kind: 'fit-room', placement: 'floor' }
97
+ ] as FigureProposalStep[]
98
+
80
99
  /** The rules and the words, all from figure-model's constants. */
81
100
  export function rulesPromptFigure(): string {
82
101
  return [
@@ -100,7 +119,12 @@ export function rulesPromptFigure(): string {
100
119
  'When revising, write only the commands the request needs. Everything not touched stays as it is.',
101
120
  '',
102
121
  'The commands (shape, then what it means):',
103
- ...FIGURE_PROPOSAL_GRAMMAR.map(word => `- ${word.shape}\n ${word.means}`)
122
+ ...FIGURE_PROPOSAL_GRAMMAR.map(word => `- ${word.shape}\n ${word.means}`),
123
+ '',
124
+ 'One whole answer the modeller takes, for a new roller conveyor -- an example of the form, not of the figure asked for:',
125
+ 'the frame stands 700 mm up on the mounting plane, a support is fastened under it at one end and repeated to two, a',
126
+ 'roller is fastened on top and repeated along it.',
127
+ JSON.stringify({ steps: WORKED_EXAMPLE })
104
128
  ].join('\n')
105
129
  }
106
130