Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
4cb5235
feat(core): add_fence on every agent surface
Aymericr Oct 12, 2026
4580304
feat: roofs live on the building's roof level
Aymericr Oct 12, 2026
082e6bc
feat(nodes): a window dragged in the plan joins a corner as in 3D
Aymericr Oct 12, 2026
936d104
fix(core): a roof level only tops its stack; overlapping plates are o…
Aymericr Oct 13, 2026
068b0c6
fix(editor): a roof placed by hand stays in view; the site plan draws…
Aymericr Oct 13, 2026
72ca397
fix(editor): the site plan's house label does not count the roof leve…
Aymericr Oct 13, 2026
c778645
chore: comments say why, without bench runs, ticket numbers or dates
Aymericr Oct 13, 2026
1d74b67
fix(editor): Merge windows says why in the editor's words and units
Aymericr Oct 13, 2026
b7f1fda
fix(editor): the HUD's note at a wall end that is no corner is a "!" row
Aymericr Oct 13, 2026
8f1c9e1
fix(nodes): an edit a corner window shares with its partner undoes in…
Aymericr Oct 13, 2026
5f07bf0
feat(nodes): the plan draws a corner window through its corner
Aymericr Oct 13, 2026
68053a4
fix(editor): a window placed from a preset at a corner in the plan st…
Aymericr Oct 13, 2026
e0cf1f7
fix: the corner-window review fixes on Bundle 2's copy; the corner-sn…
Aymericr Oct 13, 2026
376d5da
fix(core): the roof-level rule on Bundle 2's create_roof, one test of…
Aymericr Oct 13, 2026
ec6dec5
fix: corner windows, rooms, storey heights and counts after review
Aymericr Oct 13, 2026
7913d86
fix(core): create_roof sizes its roof level to the lifted eave and sa…
Aymericr Oct 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -613,8 +613,8 @@ export const CREATE_ROOM_CASES: AgentToolCase[] = [
},
},
},
// The owner (7 October): a room a person named is never renamed silently, and never twinned: the
// refusal names it, and the agent either passes rename: true or asks the person.
// A room a person named is never renamed silently, and never twinned: the refusal names it, and
// the agent either passes rename: true or asks the person.
{
name: 'a room a person named is refused with a choice, never twinned or renamed silently',
tool: 'create_room',
Expand Down
10 changes: 9 additions & 1 deletion packages/core/src/agent-operations/__fixtures__/cases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,17 @@ import {
import { CREATE_ROOM_CASES, FURNISH_ROOM_CASES } from './build-room-cases'
import { PLACE_ITEMS_CASES } from './place-items-cases'
import { FIND_BY_TYPE_CASES } from './find-by-type-cases'
import { CREATE_ROOF_CASES } from './roof-cases'
import { ROOM_CASES } from './room-cases'
import { SEARCH_ASSETS_CASES } from './search-assets-cases'
import { CORNER_WINDOW_CASES } from './corner-window-cases'
import { MERGE_WINDOWS_CASES } from './merge-windows-cases'
import { ADD_LEVEL_CASES, ADD_WALL_CASES, CREATE_STAIR_CASES } from './structure-cases'
import {
ADD_FENCE_CASES,
ADD_LEVEL_CASES,
ADD_WALL_CASES,
CREATE_STAIR_CASES,
} from './structure-cases'
import { VERIFY_SCENE_CASES } from './verify-scene-cases'

/**
Expand Down Expand Up @@ -894,6 +900,8 @@ export const AGENT_TOOL_CASES: readonly AgentToolCase[] = [
...FURNISH_ROOM_CASES,
...SEARCH_ASSETS_CASES,
...ADD_WALL_CASES,
...ADD_FENCE_CASES,
...CREATE_ROOF_CASES,
...ADD_LEVEL_CASES,
...CREATE_STAIR_CASES,
...PLACE_ITEMS_CASES,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { BuildingNode, LevelNode, WallNode, type WindowNode } from '../../schema
import type { AgentToolCase, SceneGraph } from './cases'

/**
* `add_corner_window` (L65): the 290's Bed 4 window wraps the corner of its bay; run 4 built one
* window on one face and recorded it built. What goes wrong, written first: the two sides not
* joined or not reaching the corner; a bay's 135° refused or squared; a side wider than its wall,
* a point no two walls meet at, or a corner too shallow to be one, built anyway.
* `add_corner_window`: a window that wraps the corner of a bay, built as one window on one face and
* recorded built until this tool. What goes wrong, written first: the two sides not joined or not
* reaching the corner; a bay's 135° refused or squared; a side wider than its wall, a point no two
* walls meet at, or a corner too shallow to be one, built anyway.
*/

/** Wall A runs into the corner at the origin; wall B leaves it at `degrees` from A. */
Expand All @@ -27,6 +27,34 @@ function cornerScene(degrees: number): SceneGraph {
}
}

/** Two storeys of the same square corner, stacked: their walls end at the same plan points. */
function stackedScene(): SceneGraph {
const ground = cornerScene(90)
const walls = ['wall_ca', 'wall_cb'].map((id) =>
WallNode.parse({
...(ground.nodes[id] as WallNode),
id: `${id}_up`,
parentId: 'level_u',
}),
)
const upper = LevelNode.parse({
id: 'level_u',
parentId: 'building_c',
level: 1,
children: walls.map((wall) => wall.id),
})
const building = BuildingNode.parse({ id: 'building_c', children: ['level_c', upper.id] })
return {
nodes: {
...ground.nodes,
building_c: building,
[upper.id]: upper,
...Object.fromEntries(walls.map((wall) => [wall.id, wall])),
},
rootNodeIds: ground.rootNodeIds,
}
}

const joined = (result: Record<string, unknown>, nodes: Readonly<Record<string, unknown>>) => {
const windows = (result.windowIds as string[]).map((id) => nodes[id] as WindowNode | undefined)
const [a, b] = windows
Expand Down Expand Up @@ -88,4 +116,49 @@ export const CORNER_WINDOW_CASES: AgentToolCase[] = [
input: { corner: [0, 0], width: 1, windowType: 'sliding', style: 'double-hung' },
expect: { refusal: 'style_needs_fixed_window', mentions: ["'fixed'", 'sliding'] },
},
// Stacked storeys share their corners' points: with no level named the corner was ambiguous on
// every multi-storey house, and two walls named from two floors made one window across both.
{
name: 'on a two-storey house, the corner of the level named',
tool: 'add_corner_window',
scene: stackedScene,
input: { corner: [0, 0], width: 1.2, levelId: 'level_u' },
expect: {
result: { ok: true, wallIds: ['wall_ca_up', 'wall_cb_up'] },
check: joined,
},
},
{
name: 'on a two-storey house, with no level named, the corner of the floor in view',
tool: 'add_corner_window',
scene: stackedScene,
// The MCP has no floor in view.
surfaces: ['core'],
context: { activeLevelId: 'level_u' },
input: { corner: [0, 0], width: 1.2 },
expect: { result: { ok: true, wallIds: ['wall_ca_up', 'wall_cb_up'] } },
},
{
name: 'two walls named from two floors are no corner',
tool: 'add_corner_window',
scene: stackedScene,
context: { activeLevelId: 'level_c' },
input: { corner: [0, 0], width: 1.2, wallIds: ['wall_ca', 'wall_cb_up'] },
expect: { refusal: 'no_corner', mentions: ['level_c'] },
},
// A casement keeps its frames at the corner: "fused glass" was offered, and drawn as frames.
{
name: 'a casement corner window meets at a post',
tool: 'add_corner_window',
scene: () => cornerScene(90),
input: { corner: [0, 0], width: 1, windowType: 'casement' },
expect: { result: { ok: true, post: 'post' }, check: joined },
},
{
name: 'fused glass on a casement corner window is refused',
tool: 'add_corner_window',
scene: () => cornerScene(90),
input: { corner: [0, 0], width: 1, windowType: 'casement', post: 'none' },
expect: { refusal: 'corner_not_fused', mentions: ['casement', 'fixed'] },
},
]
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { BuildingNode, LevelNode, WallNode, WindowNode } from '../../schema'
import type { AgentToolCase, SceneGraph } from './cases'

/**
* `merge_windows` (the owner, 8 October): two windows that already exist become one. On two walls
* meeting at a corner, each reaching it within 0.30 m, they become one corner window (adopted, slid
* to the corner, the glass fused, one sill); side by side on one wall with at most 0.30 m between
* them and nothing between, one window spanning both. What goes wrong, written first: a window
* recreated instead of adopted; the sides left on two sills; the second window's type or style
* winning over the first's; a door, a window or an item between them swallowed; two windows that
* are nowhere near each other joined anyway; a window already joined to another stolen.
* `merge_windows`: two windows that already exist become one. On two walls meeting at a corner,
* each reaching it within 0.30 m, they become one corner window (adopted, slid to the corner, the
* glass fused, one sill); side by side on one wall with at most 0.30 m between them and nothing
* between, one window spanning both. What goes wrong, written first: a window recreated instead of
* adopted; the sides left on two sills; the second window's type or style winning over the first's;
* a door, a window or an item between them swallowed; two windows that are nowhere near each other
* joined anyway; a window already joined to another stolen.
*/

type WindowSpec = {
Expand Down Expand Up @@ -282,7 +282,8 @@ export const MERGE_WINDOWS_CASES: AgentToolCase[] = [
tool: 'merge_windows',
scene: () =>
corner(90, [
{ ...nearA, extra: { corner: { end: 'end', partnerId: 'window_c', post: 'none' } } },
// At the corner, joined to window_c: a pair that holds.
{ ...nearA, x: 3.4, extra: { corner: { end: 'end', partnerId: 'window_c', post: 'none' } } },
nearB,
{
id: 'window_c',
Expand All @@ -297,6 +298,58 @@ export const MERGE_WINDOWS_CASES: AgentToolCase[] = [
input: { windowIds: ['window_a', 'window_b'] },
expect: { refusal: 'window_already_joined', mentions: ['window_a', 'window_c'] },
},
// A window dragged off its corner left both sides naming each other: the old pair held the window
// hostage ("unwrap it first") and its partner kept a link to a window gone elsewhere.
{
name: "a window whose old pair no longer holds joins, and the old partner's link is cleared",
tool: 'merge_windows',
scene: () =>
corner(90, [
// 0.10 m short of the corner: no longer one side of its pair with window_c.
{ ...nearA, extra: { corner: { end: 'end', partnerId: 'window_c', post: 'none' } } },
nearB,
{
id: 'window_c',
wall: 'wall_cb',
x: 2.2,
width: 0.6,
sill: 0.5,
height: 1.5,
extra: { corner: { end: 'start', partnerId: 'window_a', post: 'none' } },
},
]),
input: { windowIds: ['window_a', 'window_b'] },
expect: {
result: { ok: true, kind: 'corner', windowIds: ['window_a', 'window_b'] },
check: (_result, nodes) => {
const [a, c] = [nodes.window_a, nodes.window_c] as WindowNode[]
return [
...(a?.corner?.partnerId === 'window_b' ? [] : [`window_a joined to ${a?.corner?.partnerId}`]),
...(c?.corner ? [`window_c still names ${c.corner.partnerId}`] : []),
]
},
},
},
{
name: 'a window inside another on one wall merges into their union, not a shrunk window',
tool: 'merge_windows',
scene: () =>
scene(
[wall5],
[
{ id: 'window_l', wall: 'wall_m', x: 2.5, width: 4, sill: 0.5, height: 1.5 },
{ id: 'window_r', wall: 'wall_m', x: 3.5, width: 1, sill: 0.5, height: 1.5 },
],
),
input: { windowIds: ['window_l', 'window_r'] },
expect: {
result: { ok: true, kind: 'same_wall', width: 4 },
check: (_result, nodes) => {
const merged = nodes.window_l as WindowNode
return merged?.position[0] === 2.5 ? [] : [`centre ${merged?.position[0]}`]
},
},
},
{
name: 'the same window twice is refused',
tool: 'merge_windows',
Expand Down
Loading
Loading