Skip to content

Commit a2ef549

Browse files
Add guardrail for functionalities
1 parent c88ea9f commit a2ef549

3 files changed

Lines changed: 173 additions & 11 deletions

File tree

comcheck_api/ai/skill/SKILL.md

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,19 @@
22
name: comcheck-api
33
description: Use this skill when the user is writing Python code that uses
44
the comcheck_api package to build COMcheck project JSON, run compliance
5-
simulations against the PNNL COMcheck Web API, or work with envelope,
6-
lighting, mechanical, or building-area operations. Triggers on imports
7-
of `comcheck_api`, mentions of COMcheck/ASHRAE 90.1/IECC compliance,
8-
or requests to validate building energy code compliance.
5+
simulations against the PNNL COMcheck Web API, or work with envelope
6+
or building-area operations. Triggers on imports of `comcheck_api`,
7+
mentions of COMcheck/ASHRAE 90.1/IECC compliance, or requests to
8+
validate building energy code compliance.
9+
10+
NOTE: implemented surface area is the `COMcheckClient` user
11+
methods (`list_projects`, `get_project`, `update_project`,
12+
`start_run_simulation`, `get_simulation_status`,
13+
`get_simulation_result`, `set_api_key`) plus the project-mutation
14+
operations for **building areas** and **envelope**. Interior-
15+
lighting fixtures (under `activityUse[]`), exterior lighting,
16+
mechanical/HVAC, and renewable-energy operations are not
17+
available — do not write code that adds, updates, or removes them.
918
---
1019

1120
# COMcheck API Python Client Skill
@@ -30,6 +39,13 @@ Triggers:
3039
`envelope`, `lighting` (which contains `wholeBldgUse[]` — the
3140
building areas), `hvac`, `renewable`, and `control` (energy code).
3241
No `Project`/`Control` PascalCase aliases exist.
42+
The fields `hvac`, `renewable`, and the **interior-lighting fixtures
43+
inside `activityUse[]`**, plus exterior lighting (`exteriorUse[]`)
44+
and the shared `fixtureSchedule[]`, exist on the model but have
45+
**no operation functions** — leave them at template defaults. Only
46+
`lighting.wholeBldgUse[]` (building areas, including each area's
47+
own `interiorLightingSpace` singleton) is mutable, via
48+
`project_building_area_operations`.
3349
- **Operation modules (functional)**: building areas and envelope
3450
components are added/updated/removed via free functions in
3551
`project_building_area_operations` and `project_envelope_operations`.
@@ -133,6 +149,21 @@ print(result["performanceRating"])
133149
bypass the validation logic in the operation modules. Always go
134150
through `project_envelope_operations` and
135151
`project_building_area_operations` instead.
152+
- Don't add, update, or remove interior lighting (the
153+
`activityUse[]` fixtures), exterior lighting (`exteriorUse[]`,
154+
`fixtureSchedule[]`), HVAC/mechanical, or renewable-energy
155+
components — no operations exist for them. The whole-building
156+
`interiorLightingSpace` singleton on each `WholeBldgUse` *is*
157+
editable through `project_building_area_operations`; the per-
158+
activity lighting nested under `activityUse[]` is not. The
159+
`COMcheckClient` user methods (`list_projects`, `get_project`,
160+
`update_project`, `start_run_simulation`, `get_simulation_status`,
161+
`get_simulation_result`, `set_api_key`) are fully supported and
162+
fine to use. If asked for an unsupported mutation area, tell the
163+
user it's not implemented and offer building-area / envelope /
164+
simulation instead. Confirm operation scope with
165+
`comcheck_api.list_operations()` (only `building_area` and
166+
`envelope` groups exist).
136167

137168
## Common patterns
138169

@@ -228,6 +259,9 @@ else:
228259

229260
## When you need more detail
230261

262+
- For the authoritative list of what's implemented → call
263+
`comcheck_api.list_operations()`. Only operations it returns are
264+
supported.
231265
- For envelope assemblies (roof, walls, floor, windows, doors,
232266
skylights, thermal bridges) → read `reference/operations.md`.
233267
- For Pydantic model field-level details → read `reference/types.md`.

comcheck_api/ai/skill/reference/types.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ aliases exist).
1313
| `project` | `Project` | Project metadata. Title is `project.project.projectTitle`; address fields use `projectAddress` / `projectCity` / etc. |
1414
| `location` | `Location` | State, city, climate zone. |
1515
| `envelope` | `Envelope` | Roofs (`roof[]`), AG walls (`agWall[]`), BG walls (`bgWall[]`), floors, windows, doors, skylights. |
16-
| `lighting` | `Lighting` | Holds `wholeBldgUse[]` — the **building areas / zones**. Also holds exterior lighting fields. |
17-
| `hvac` | `HVAC` | Mechanical systems. |
18-
| `renewable` | `Renewable` | Renewable energy systems. |
16+
| `lighting` | `Lighting` | Holds `wholeBldgUse[]` — the **building areas / zones**. `wholeBldgUse[]` (including each area's `interiorLightingSpace` singleton) is operable; `activityUse[]`, `exteriorUse[]`, and `fixtureSchedule[]` have no operations. |
17+
| `hvac` | `HVAC` | Mechanical systems — no operations; not editable via this SDK. |
18+
| `renewable` | `Renewable` | Renewable energy systems — no operations; not editable via this SDK. |
1919
| `control` | `Control` | Energy code (`control.code`, e.g. `CEZ_IECC2018`, `CEZ_90_1_2022`). |
2020

2121
Building areas (`WholeBldgUse`) are **not top-level** — they live
@@ -42,13 +42,16 @@ them.
4242
| Model | Purpose |
4343
|---|---|
4444
| `WholeBldgUse` | One building area / zone. Lives in `project.lighting.wholeBldgUse[]`. Has `key`, `areaDescription`, `floorArea`, `ceilingHeight`, `interiorLightingSpace`, etc. |
45-
| `InteriorLightingSpace` | Lighting configuration for one area. |
45+
| `InteriorLightingSpace` | Lighting configuration for one area. The singleton attached directly to a `WholeBldgUse` is editable via `update_building_area_in_project`; the same model nested under `activityUse[]` is **not** operable (interior-lighting fixtures live there). |
4646

4747
Every envelope component has a `bldgUseKey` field that ties it to
4848
one of these area keys.
4949

5050
## HVAC
5151

52+
⚠️ Documented for shape only — no add/update/remove operations exist
53+
for any of these models. Leave `project.hvac` at its template default.
54+
5255
| Model | Purpose |
5356
|---|---|
5457
| `HVAC` | Container. |

comcheck_api/ai/skill/scripts/validate_code.py

Lines changed: 128 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,58 @@ def _read_input(arg: str) -> str:
2828
return Path(arg).read_text()
2929

3030

31+
UNSUPPORTED_PROJECT_ATTRS = {"hvac", "renewable"}
32+
UNSUPPORTED_LIGHTING_ATTRS = {
33+
"activityUse",
34+
"exteriorUse",
35+
"fixtureSchedule",
36+
}
37+
38+
39+
def _supported_operation_names() -> set[str]:
40+
"""Live set of supported operation function names from the SDK."""
41+
try:
42+
from comcheck_api import list_operations
43+
except Exception: # noqa: BLE001
44+
return set()
45+
return {op.name for op in list_operations()}
46+
47+
48+
def _is_attr_chain(node, head: str, tail: str) -> bool:
49+
"""True if `node` is `<head>.<tail>` — e.g. `project.hvac`."""
50+
import ast
51+
52+
return (
53+
isinstance(node, ast.Attribute)
54+
and node.attr == tail
55+
and isinstance(node.value, ast.Name)
56+
and node.value.id == head
57+
)
58+
59+
60+
def _is_lighting_chain(node, attr: str) -> bool:
61+
"""True if `node` is `project.lighting.<attr>`."""
62+
import ast
63+
64+
return (
65+
isinstance(node, ast.Attribute)
66+
and node.attr == attr
67+
and isinstance(node.value, ast.Attribute)
68+
and node.value.attr == "lighting"
69+
and isinstance(node.value.value, ast.Name)
70+
and node.value.value.id == "project"
71+
)
72+
73+
3174
def validate(code: str) -> dict:
3275
"""Compile-check the provided code and report errors as a dict.
3376
34-
This is intentionally limited to syntax + import checks for now.
35-
Runtime validation (executing the code against a mocked SDK client)
36-
is added in a follow-up phase, where the sandbox is hardened.
77+
Runs three passes:
78+
1. Syntax check via :func:`compile`.
79+
2. Import check on every imported module name.
80+
3. Scope check that the code only uses operations actually exposed
81+
by the SDK and does not mutate the unsupported `hvac`,
82+
`renewable`, or non-`wholeBldgUse` lighting subtrees.
3783
"""
3884
errors: list[dict] = []
3985

@@ -68,6 +114,85 @@ def validate(code: str) -> dict:
68114
except ImportError as e:
69115
errors.append({"kind": "import", "module": mod, "message": str(e)})
70116

117+
# 3. Scope check: cross-reference symbols against list_operations()
118+
# so the guard auto-tracks SDK growth, and forbid direct mutation
119+
# of the unsupported subtrees.
120+
supported_ops = _supported_operation_names()
121+
122+
# Collect the local names that resolve into the operation modules.
123+
op_module_aliases: set[str] = set()
124+
for node in ast.walk(tree):
125+
if isinstance(node, ast.ImportFrom) and node.module == "comcheck_api":
126+
for alias in node.names:
127+
if alias.name in {
128+
"project_envelope_operations",
129+
"project_building_area_operations",
130+
}:
131+
op_module_aliases.add(alias.asname or alias.name)
132+
elif isinstance(node, ast.ImportFrom) and node.module == (
133+
"comcheck_api.project_operations"
134+
):
135+
for alias in node.names:
136+
if alias.name in {
137+
"project_envelope_operations",
138+
"project_building_area_operations",
139+
}:
140+
op_module_aliases.add(alias.asname or alias.name)
141+
142+
for node in ast.walk(tree):
143+
# Forbid `project.hvac` / `project.renewable` access in any context.
144+
if isinstance(node, ast.Attribute) and any(
145+
_is_attr_chain(node, "project", attr) for attr in UNSUPPORTED_PROJECT_ATTRS
146+
):
147+
errors.append(
148+
{
149+
"kind": "unsupported-scope",
150+
"line": node.lineno,
151+
"message": (
152+
f"`project.{node.attr}` has no operations in this SDK; "
153+
"leave it at template defaults."
154+
),
155+
}
156+
)
157+
158+
# Forbid `project.lighting.<unsupported>`.
159+
if isinstance(node, ast.Attribute) and any(
160+
_is_lighting_chain(node, attr) for attr in UNSUPPORTED_LIGHTING_ATTRS
161+
):
162+
errors.append(
163+
{
164+
"kind": "unsupported-scope",
165+
"line": node.lineno,
166+
"message": (
167+
f"`project.lighting.{node.attr}` has no operations; "
168+
"only `lighting.wholeBldgUse[]` is editable."
169+
),
170+
}
171+
)
172+
173+
# Calls of the form `<op_module>.<name>(...)` must hit a real op.
174+
if (
175+
supported_ops
176+
and isinstance(node, ast.Call)
177+
and isinstance(node.func, ast.Attribute)
178+
and isinstance(node.func.value, ast.Name)
179+
and node.func.value.id in op_module_aliases
180+
):
181+
fn_name = node.func.attr
182+
if fn_name not in supported_ops and not fn_name.startswith("_"):
183+
errors.append(
184+
{
185+
"kind": "unknown-operation",
186+
"line": node.lineno,
187+
"operation": fn_name,
188+
"message": (
189+
f"`{node.func.value.id}.{fn_name}` is not a supported "
190+
"operation. Run comcheck_api.list_operations() for the "
191+
"current set."
192+
),
193+
}
194+
)
195+
71196
return {"ok": not errors, "errors": errors}
72197

73198

0 commit comments

Comments
 (0)