pi-revit 0.4.0 → 0.5.1
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/AGENTS.md +167 -0
- package/CHANGELOG.md +44 -0
- package/README.md +644 -553
- package/bin/pi-revit.js +9 -9
- package/docs/architecture.md +271 -0
- package/docs/evaluation.md +434 -0
- package/docs/invariants.json +147 -0
- package/extensions/pi-revit/completion-monitor.ts +55 -0
- package/extensions/pi-revit/contracts.ts +146 -0
- package/extensions/pi-revit/discovery.ts +93 -0
- package/extensions/pi-revit/index.ts +342 -255
- package/extensions/pi-revit/instance-router.ts +86 -86
- package/extensions/pi-revit/platform-prompt.ts +40 -0
- package/extensions/pi-revit/scope-monitor.ts +114 -0
- package/extensions/pi-revit/script-library.ts +144 -144
- package/extensions/pi-revit/tool-catalog.ts +113 -14
- package/extensions/pi-revit/tool-documentation.ts +72 -0
- package/extensions/pi-revit/tool-schema.ts +8 -0
- package/package.json +8 -2
- package/scripts/build.ps1 +9 -9
- package/scripts/check-sdk.ps1 +66 -66
- package/scripts/check-tool-documentation.mjs +287 -0
- package/scripts/deploy.ps1 +16 -16
- package/scripts/generate-contracts.mjs +80 -0
- package/scripts/lib/platform.mjs +226 -0
- package/scripts/test-extension.mjs +15 -0
- package/skills/pi-revit/SKILL.md +30 -218
- package/skills/pi-revit/contracts.generated.json +3524 -0
- package/skills/pi-revit/references/execution-rules.md +41 -0
- package/skills/pi-revit/references/model-audit-export.md +38 -27
- package/skills/pi-revit/references/operation-recovery.md +33 -0
- package/skills/pi-revit/references/room-documentation.md +37 -26
- package/skills/pi-revit/references/tool-index.md +89 -0
- package/skills/pi-revit/references/tools/capture_view.md +62 -0
- package/skills/pi-revit/references/tools/change_element_types.md +65 -0
- package/skills/pi-revit/references/tools/create_tags.md +85 -0
- package/skills/pi-revit/references/tools/delete_elements.md +66 -0
- package/skills/pi-revit/references/tools/execute_csharp.md +81 -0
- package/skills/pi-revit/references/tools/export_documents.md +75 -0
- package/skills/pi-revit/references/tools/find_revit_tools.md +96 -0
- package/skills/pi-revit/references/tools/get_element_details.md +66 -0
- package/skills/pi-revit/references/tools/get_element_relationships.md +61 -0
- package/skills/pi-revit/references/tools/get_element_types.md +67 -0
- package/skills/pi-revit/references/tools/get_elements.md +87 -0
- package/skills/pi-revit/references/tools/get_linked_elements.md +79 -0
- package/skills/pi-revit/references/tools/get_linked_models.md +57 -0
- package/skills/pi-revit/references/tools/get_model_coordinates.md +64 -0
- package/skills/pi-revit/references/tools/get_model_health.md +53 -0
- package/skills/pi-revit/references/tools/get_model_overview.md +57 -0
- package/skills/pi-revit/references/tools/get_revit_operation.md +54 -0
- package/skills/pi-revit/references/tools/get_schedule_fields.md +62 -0
- package/skills/pi-revit/references/tools/get_schedules.md +71 -0
- package/skills/pi-revit/references/tools/manage_element_sets.md +92 -0
- package/skills/pi-revit/references/tools/manage_revit_instances.md +63 -0
- package/skills/pi-revit/references/tools/manage_revit_scripts.md +109 -0
- package/skills/pi-revit/references/tools/manage_schedules.md +90 -0
- package/skills/pi-revit/references/tools/manage_selection.md +66 -0
- package/skills/pi-revit/references/tools/manage_sheet_placements.md +82 -0
- package/skills/pi-revit/references/tools/manage_sheets.md +71 -0
- package/skills/pi-revit/references/tools/manage_views.md +95 -0
- package/skills/pi-revit/references/tools/measure_geometry.md +71 -0
- package/skills/pi-revit/references/tools/open_view.md +59 -0
- package/skills/pi-revit/references/tools/ping.md +41 -0
- package/skills/pi-revit/references/tools/query_spatial_elements.md +74 -0
- package/skills/pi-revit/references/tools/read_revit_result.md +53 -0
- package/skills/pi-revit/references/tools/search_api_docs.md +65 -0
- package/skills/pi-revit/references/tools/set_parameters.md +75 -0
- package/skills/pi-revit/references/tools/summarize_elements.md +64 -0
- package/skills/pi-revit/references/tools/transform_elements.md +79 -0
- package/skills/pi-revit/references/visual-verification.md +36 -0
- package/skills/pi-revit/tool-manifest.json +338 -0
- package/src/Revit/BridgeServer.cs +93 -87
- package/src/Revit/OperationStore.cs +178 -178
- package/src/Revit/ToolRegistry.cs +88 -57
- package/src/Revit/Tools/CaptureView.cs +10 -2
- package/src/Revit/Tools/ChangeElementTypes.cs +74 -60
- package/src/Revit/Tools/ChangeSet.cs +39 -0
- package/src/Revit/Tools/CreateTags.cs +107 -95
- package/src/Revit/Tools/DeleteElements.cs +53 -44
- package/src/Revit/Tools/DocumentGuard.cs +74 -64
- package/src/Revit/Tools/ElementNames.cs +103 -0
- package/src/Revit/Tools/ElementQueryScope.cs +27 -27
- package/src/Revit/Tools/ElementTraits.cs +53 -0
- package/src/Revit/Tools/ExecuteCsharp.cs +54 -45
- package/src/Revit/Tools/ExportDocuments.cs +129 -121
- package/src/Revit/Tools/GetElementDetails.cs +37 -41
- package/src/Revit/Tools/GetElementRelationships.cs +82 -76
- package/src/Revit/Tools/GetElementTypes.cs +8 -0
- package/src/Revit/Tools/GetElements.cs +75 -86
- package/src/Revit/Tools/GetLinkedElements.cs +89 -82
- package/src/Revit/Tools/GetLinkedModels.cs +73 -66
- package/src/Revit/Tools/GetModelCoordinates.cs +56 -49
- package/src/Revit/Tools/GetModelHealth.cs +7 -0
- package/src/Revit/Tools/GetModelOverview.cs +185 -158
- package/src/Revit/Tools/GetScheduleFields.cs +44 -37
- package/src/Revit/Tools/GetSchedules.cs +96 -89
- package/src/Revit/Tools/InheritedState.Summary.cs +57 -0
- package/src/Revit/Tools/InheritedState.cs +144 -0
- package/src/Revit/Tools/ManageElementSets.cs +114 -106
- package/src/Revit/Tools/ManageSchedules.cs +174 -164
- package/src/Revit/Tools/ManageSelection.cs +45 -37
- package/src/Revit/Tools/ManageSheetPlacements.cs +113 -97
- package/src/Revit/Tools/ManageSheets.cs +72 -63
- package/src/Revit/Tools/ManageViews.cs +115 -100
- package/src/Revit/Tools/MeasureGeometry.cs +60 -54
- package/src/Revit/Tools/ModelChanges.cs +154 -0
- package/src/Revit/Tools/ModelEditBatch.cs +105 -102
- package/src/Revit/Tools/ModelEditInputs.cs +49 -49
- package/src/Revit/Tools/OpenView.cs +9 -2
- package/src/Revit/Tools/ParameterResolver.cs +94 -0
- package/src/Revit/Tools/QuerySpatialElements.cs +70 -63
- package/src/Revit/Tools/SearchApiDocs.cs +72 -4
- package/src/Revit/Tools/SetParameters.cs +60 -79
- package/src/Revit/Tools/SpatialBounds.cs +30 -30
- package/src/Revit/Tools/SummarizeElements.cs +94 -87
- package/src/Revit/Tools/ToolContract.cs +48 -0
- package/src/Revit/Tools/ToolSupport.cs +5 -1
- package/src/Revit/Tools/TransformElements.cs +72 -57
- package/workspace/AGENTS.md +26 -20
|
@@ -1,86 +1,86 @@
|
|
|
1
|
-
import { readFile, readdir } from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import { createHash } from "node:crypto";
|
|
5
|
-
|
|
6
|
-
export interface BridgeInfo {
|
|
7
|
-
baseUrl: string;
|
|
8
|
-
token: string;
|
|
9
|
-
pid?: number;
|
|
10
|
-
revitVersion?: string;
|
|
11
|
-
bridgeId?: string;
|
|
12
|
-
supportsOperationTracking?: boolean;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** Selection belongs to one extension instance. A selected session never silently falls back. */
|
|
16
|
-
export function createInstanceRouter(readLegacy: () => Promise<BridgeInfo>, probe: (info: BridgeInfo) => Promise<Record<string, unknown>>) {
|
|
17
|
-
let selected: string | undefined;
|
|
18
|
-
// Older bridges have no generation ID. Derive a stable opaque selector from
|
|
19
|
-
// their per-start credentials without exposing the credential itself.
|
|
20
|
-
const identity = (info: BridgeInfo) => info.bridgeId ?? createHash("sha256").update(info.baseUrl + "\0" + info.token).digest("hex").slice(0, 32);
|
|
21
|
-
const directory = () => path.join(process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming"), "RevitBridge", "instances");
|
|
22
|
-
async function candidates(): Promise<BridgeInfo[]> {
|
|
23
|
-
const entries = new Map<string, BridgeInfo>();
|
|
24
|
-
let files: string[] = [];
|
|
25
|
-
try { files = (await readdir(directory())).filter(name => /^[0-9a-f]{32}\.json$/.test(name)); } catch { }
|
|
26
|
-
// Old crash records may accumulate; probe only records whose process still exists.
|
|
27
|
-
for (const file of files) {
|
|
28
|
-
try {
|
|
29
|
-
const info = JSON.parse(await readFile(path.join(directory(), file), "utf8")) as BridgeInfo;
|
|
30
|
-
if (info.bridgeId !== file.slice(0, -5) || !info.baseUrl || !info.token || !Number.isSafeInteger(info.pid)) continue;
|
|
31
|
-
try { process.kill(info.pid!, 0); } catch { continue; }
|
|
32
|
-
entries.set(info.bridgeId, info);
|
|
33
|
-
} catch { }
|
|
34
|
-
}
|
|
35
|
-
try {
|
|
36
|
-
const legacy = await readLegacy();
|
|
37
|
-
entries.set(identity(legacy), legacy);
|
|
38
|
-
} catch { }
|
|
39
|
-
return [...entries.values()];
|
|
40
|
-
}
|
|
41
|
-
async function live() {
|
|
42
|
-
const found = await Promise.all((await candidates()).map(async info => {
|
|
43
|
-
try {
|
|
44
|
-
const ping = await probe(info);
|
|
45
|
-
if (info.bridgeId && ping.bridgeId !== info.bridgeId) return null;
|
|
46
|
-
return { info, ping };
|
|
47
|
-
} catch { return null; }
|
|
48
|
-
}));
|
|
49
|
-
return found.filter((entry): entry is NonNullable<typeof entry> => entry !== null);
|
|
50
|
-
}
|
|
51
|
-
async function resolve(operationId?: string): Promise<BridgeInfo> {
|
|
52
|
-
const target = operationId?.split(":")[0] ?? selected;
|
|
53
|
-
if (target) {
|
|
54
|
-
const info = (await candidates()).find(entry => identity(entry) === target);
|
|
55
|
-
if (!info) throw new Error("The original or selected Revit bridge session is unavailable. Its outcome is unknown here; no action was sent to another instance. Use manage_revit_instances to select an available session.");
|
|
56
|
-
return info;
|
|
57
|
-
}
|
|
58
|
-
const entries = await candidates();
|
|
59
|
-
// Bind only a verified live session; a crash record must not prevent
|
|
60
|
-
// startup discovery from recovering when Revit is launched later.
|
|
61
|
-
if (entries.length === 1) {
|
|
62
|
-
const ping = await probe(entries[0]);
|
|
63
|
-
if (entries[0].bridgeId && ping.bridgeId !== entries[0].bridgeId)
|
|
64
|
-
throw new Error("Revit discovery points to a different bridge generation. Refresh the instance list.");
|
|
65
|
-
selected = identity(entries[0]); return entries[0];
|
|
66
|
-
}
|
|
67
|
-
const available = await live();
|
|
68
|
-
if (available.length > 1) throw new Error("Several Revit instances are open. Use manage_revit_instances to list and select the intended bridge_id before calling model tools.");
|
|
69
|
-
if (available.length === 1) { selected = identity(available[0].info); return available[0].info; }
|
|
70
|
-
return readLegacy();
|
|
71
|
-
}
|
|
72
|
-
async function list() {
|
|
73
|
-
return (await live()).map(({ info, ping }) => ({ bridge_id: identity(info), pid: info.pid ?? null,
|
|
74
|
-
revit_version: info.revitVersion ?? null, addin_version: ping.addinVersion ?? null,
|
|
75
|
-
selected: selected === identity(info), supports_operation_tracking: info.supportsOperationTracking === true }));
|
|
76
|
-
}
|
|
77
|
-
async function select(id: string) {
|
|
78
|
-
if (!/^[0-9a-f]{32}$/.test(id)) throw new Error("bridge_id must be an exact session ID from manage_revit_instances.");
|
|
79
|
-
const match = (await live()).find(entry => identity(entry.info) === id);
|
|
80
|
-
if (!match) throw new Error("That bridge session is no longer reachable; selection was unchanged.");
|
|
81
|
-
selected = id;
|
|
82
|
-
return { bridge_id: id, pid: match.info.pid, addin_version: match.ping.addinVersion,
|
|
83
|
-
instructions: "Selection applies to this Pi extension session. Read get_model_overview for a fresh exact document identity before editing. Operation receipts and identical retries are routed to their original bridge session." };
|
|
84
|
-
}
|
|
85
|
-
return { resolve, list, select };
|
|
86
|
-
}
|
|
1
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
|
|
6
|
+
export interface BridgeInfo {
|
|
7
|
+
baseUrl: string;
|
|
8
|
+
token: string;
|
|
9
|
+
pid?: number;
|
|
10
|
+
revitVersion?: string;
|
|
11
|
+
bridgeId?: string;
|
|
12
|
+
supportsOperationTracking?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Selection belongs to one extension instance. A selected session never silently falls back. */
|
|
16
|
+
export function createInstanceRouter(readLegacy: () => Promise<BridgeInfo>, probe: (info: BridgeInfo) => Promise<Record<string, unknown>>) {
|
|
17
|
+
let selected: string | undefined;
|
|
18
|
+
// Older bridges have no generation ID. Derive a stable opaque selector from
|
|
19
|
+
// their per-start credentials without exposing the credential itself.
|
|
20
|
+
const identity = (info: BridgeInfo) => info.bridgeId ?? createHash("sha256").update(info.baseUrl + "\0" + info.token).digest("hex").slice(0, 32);
|
|
21
|
+
const directory = () => path.join(process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming"), "RevitBridge", "instances");
|
|
22
|
+
async function candidates(): Promise<BridgeInfo[]> {
|
|
23
|
+
const entries = new Map<string, BridgeInfo>();
|
|
24
|
+
let files: string[] = [];
|
|
25
|
+
try { files = (await readdir(directory())).filter(name => /^[0-9a-f]{32}\.json$/.test(name)); } catch { }
|
|
26
|
+
// Old crash records may accumulate; probe only records whose process still exists.
|
|
27
|
+
for (const file of files) {
|
|
28
|
+
try {
|
|
29
|
+
const info = JSON.parse(await readFile(path.join(directory(), file), "utf8")) as BridgeInfo;
|
|
30
|
+
if (info.bridgeId !== file.slice(0, -5) || !info.baseUrl || !info.token || !Number.isSafeInteger(info.pid)) continue;
|
|
31
|
+
try { process.kill(info.pid!, 0); } catch { continue; }
|
|
32
|
+
entries.set(info.bridgeId, info);
|
|
33
|
+
} catch { }
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const legacy = await readLegacy();
|
|
37
|
+
entries.set(identity(legacy), legacy);
|
|
38
|
+
} catch { }
|
|
39
|
+
return [...entries.values()];
|
|
40
|
+
}
|
|
41
|
+
async function live() {
|
|
42
|
+
const found = await Promise.all((await candidates()).map(async info => {
|
|
43
|
+
try {
|
|
44
|
+
const ping = await probe(info);
|
|
45
|
+
if (info.bridgeId && ping.bridgeId !== info.bridgeId) return null;
|
|
46
|
+
return { info, ping };
|
|
47
|
+
} catch { return null; }
|
|
48
|
+
}));
|
|
49
|
+
return found.filter((entry): entry is NonNullable<typeof entry> => entry !== null);
|
|
50
|
+
}
|
|
51
|
+
async function resolve(operationId?: string): Promise<BridgeInfo> {
|
|
52
|
+
const target = operationId?.split(":")[0] ?? selected;
|
|
53
|
+
if (target) {
|
|
54
|
+
const info = (await candidates()).find(entry => identity(entry) === target);
|
|
55
|
+
if (!info) throw new Error("The original or selected Revit bridge session is unavailable. Its outcome is unknown here; no action was sent to another instance. Use manage_revit_instances to select an available session.");
|
|
56
|
+
return info;
|
|
57
|
+
}
|
|
58
|
+
const entries = await candidates();
|
|
59
|
+
// Bind only a verified live session; a crash record must not prevent
|
|
60
|
+
// startup discovery from recovering when Revit is launched later.
|
|
61
|
+
if (entries.length === 1) {
|
|
62
|
+
const ping = await probe(entries[0]);
|
|
63
|
+
if (entries[0].bridgeId && ping.bridgeId !== entries[0].bridgeId)
|
|
64
|
+
throw new Error("Revit discovery points to a different bridge generation. Refresh the instance list.");
|
|
65
|
+
selected = identity(entries[0]); return entries[0];
|
|
66
|
+
}
|
|
67
|
+
const available = await live();
|
|
68
|
+
if (available.length > 1) throw new Error("Several Revit instances are open. Use manage_revit_instances to list and select the intended bridge_id before calling model tools.");
|
|
69
|
+
if (available.length === 1) { selected = identity(available[0].info); return available[0].info; }
|
|
70
|
+
return readLegacy();
|
|
71
|
+
}
|
|
72
|
+
async function list() {
|
|
73
|
+
return (await live()).map(({ info, ping }) => ({ bridge_id: identity(info), pid: info.pid ?? null,
|
|
74
|
+
revit_version: info.revitVersion ?? null, addin_version: ping.addinVersion ?? null,
|
|
75
|
+
selected: selected === identity(info), supports_operation_tracking: info.supportsOperationTracking === true }));
|
|
76
|
+
}
|
|
77
|
+
async function select(id: string) {
|
|
78
|
+
if (!/^[0-9a-f]{32}$/.test(id)) throw new Error("bridge_id must be an exact session ID from manage_revit_instances.");
|
|
79
|
+
const match = (await live()).find(entry => identity(entry.info) === id);
|
|
80
|
+
if (!match) throw new Error("That bridge session is no longer reachable; selection was unchanged.");
|
|
81
|
+
selected = id;
|
|
82
|
+
return { bridge_id: id, pid: match.info.pid, addin_version: match.ping.addinVersion,
|
|
83
|
+
instructions: "Selection applies to this Pi extension session. Read get_model_overview for a fresh exact document identity before editing. Operation receipts and identical retries are routed to their original bridge session." };
|
|
84
|
+
}
|
|
85
|
+
return { resolve, list, select };
|
|
86
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The PI-Revit platform section: cross-cutting protocols stated exactly once, always in
|
|
5
|
+
* context (injected through before_agent_start), independent of whether the skill is read
|
|
6
|
+
* and of how many tools exist. Tool-specific facts stay in each tool's own guidelines.
|
|
7
|
+
*/
|
|
8
|
+
export const PLATFORM_PROTOCOL = [
|
|
9
|
+
"PI-Revit protocol. It applies to every Revit tool, present or future, whether or not the pi-revit skill has been read.",
|
|
10
|
+
"1. Capability: before saying PI-Revit can or cannot do something, check. find_revit_tools (scope=documentation also works while Revit is closed) returns matching tools, their declared limits with alternatives, and related workflows. If a tool does not cover the request, follow its declared alternative. If nothing dedicated fits, verify all needed API members with search_api_docs, up to 10 names per call separated by ';' and use execute_csharp within the requested scope. Say an operation is not possible only after that check, and name what you checked; distinguish \"no dedicated tool\", \"the Revit API does not offer it\" and \"needs the user\".",
|
|
11
|
+
"2. Scope and completion: explanations do not change the model and inspections do not repair it. For a change, list the request's explicit requirements, do only those, and verify each with the tool's declared verification method. Then stop and report what changed, how it was verified and anything unmet. Offer further improvements as suggestions instead of making them. Never hide or remove content the request asks to show. An object made from an existing one inherits its state: results report it as inherited_state, and model_changes lists what each call added, modified or deleted, with the visibility of new views. Check that state against the request and say what you derived from.",
|
|
12
|
+
"3. Existing objects: objects that existed before the request are not yours. If one already has a name the request asks you to create, or a creation is rejected as a name collision, do not edit, reuse, replace or delete it: ask the user, or use a distinct name and report the collision.",
|
|
13
|
+
"4. Evidence: base claims about capabilities and model state on tool results, manuals or API documentation checked in this session, and say which.",
|
|
14
|
+
"5. Identity: for model changes, previews, exports, view activation and selection changes, pass project.documentId from get_model_overview unchanged as expected_document_id; project.documentKind says whether it is a project or a family, and tools refuse kinds they do not declare. Refresh it after reopening, restarting or switching instances. Save As keeps the ID but changes which file a later save affects.",
|
|
15
|
+
"6. Language: the user may write in any language, and the model's Revit UI may be localized. Reply in the user's language. Search tools and API documentation with English terms. Read localized category and parameter names from tool results, and prefer exact identities (BuiltInParameter names, guid:<GUID>) over translated display names.",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export function buildPlatformSection(options: { manualDirectory: string; skillRoot: string; sharedRules: readonly string[] }): string {
|
|
19
|
+
const lines = [
|
|
20
|
+
...PLATFORM_PROTOCOL,
|
|
21
|
+
`7. Guidance: each tool's manual is ${path.join(options.manualDirectory, "<tool_name>.md")}; read only the manual of a tool you will use. find_revit_tools returns exact paths and whether a manual matches the selected bridge's contract. Shared rules: ${path.join(options.skillRoot, "references", "execution-rules.md")}. Uncertain outcomes: ${path.join(options.skillRoot, "references", "operation-recovery.md")}.`,
|
|
22
|
+
];
|
|
23
|
+
if (options.sharedRules.length) lines.push("Rules shared by several Revit tools:", ...options.sharedRules.map(rule => `- ${rule}`));
|
|
24
|
+
return lines.join("\n");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Split bridge guidelines into per-tool rules and rules shared by two or more tools.
|
|
29
|
+
* A tool's own name is normalized away first, so "set_parameters: use X" and
|
|
30
|
+
* "open_view: use X" are recognized as one shared rule. Works for any bridge version.
|
|
31
|
+
*/
|
|
32
|
+
export function hoistSharedGuidelines(descriptors: readonly { name: string; promptGuidelines?: string[] | null }[]) {
|
|
33
|
+
const normalized = (name: string, rule: string) => rule.split(name).join("{tool}").replace(/^\{tool\}:\s*/, "").trim();
|
|
34
|
+
const counts = new Map<string, number>();
|
|
35
|
+
for (const d of descriptors) for (const rule of new Set((d.promptGuidelines ?? []).map(r => normalized(d.name, r)))) counts.set(rule, (counts.get(rule) ?? 0) + 1);
|
|
36
|
+
const shared = [...counts].filter(([, count]) => count >= 2).map(([rule]) => rule);
|
|
37
|
+
const sharedSet = new Set(shared);
|
|
38
|
+
const perTool = new Map(descriptors.map(d => [d.name, (d.promptGuidelines ?? []).filter(rule => !sharedSet.has(normalized(d.name, rule)))]));
|
|
39
|
+
return { shared: shared.map(rule => rule.replaceAll("{tool}", "the tool")), perTool };
|
|
40
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scope monitor (inv:existing-objects-not-reused). Tool-agnostic: it reads the model_changes
|
|
3
|
+
* report the bridge attaches to every model-changing result, and the name_collision facts a
|
|
4
|
+
* rejected creation carries, never tool names. Per user request it keeps the IDs of objects
|
|
5
|
+
* created in that request. When a call changes an object that existed before the request and
|
|
6
|
+
* whose name the request mentions, or a name the request uses is already taken, it returns a
|
|
7
|
+
* note that the object predates the request; the platform protocol then requires asking or
|
|
8
|
+
* reporting. It steers and never blocks. Reset at the start of every user prompt.
|
|
9
|
+
*/
|
|
10
|
+
export interface ChangedItem { id: number; name?: string | null; category?: string | null }
|
|
11
|
+
export interface ModelChanges {
|
|
12
|
+
observed?: boolean;
|
|
13
|
+
rolled_back?: boolean;
|
|
14
|
+
/** Family types and parameters (not elements) added, removed or changed by the call, in a family document. */
|
|
15
|
+
family?: { types_added?: string[]; types_removed?: string[]; types_changed?: string[]; parameters_added?: string[]; parameters_removed?: string[] };
|
|
16
|
+
added?: { count: number; items?: ChangedItem[] };
|
|
17
|
+
modified?: { count: number; items?: ChangedItem[] };
|
|
18
|
+
deleted?: { count: number; ids?: number[] };
|
|
19
|
+
}
|
|
20
|
+
export interface NameCollision { existing_id: number | null; kind?: string; name?: string; sheet_number?: string }
|
|
21
|
+
|
|
22
|
+
/** The model_changes report of a bridge payload, or null (older bridge, read tool, non-object payload). */
|
|
23
|
+
export function readModelChanges(payload: unknown): ModelChanges | null {
|
|
24
|
+
const value = payload !== null && typeof payload === "object" ? (payload as { model_changes?: unknown }).model_changes : undefined;
|
|
25
|
+
return value !== null && typeof value === "object" ? value as ModelChanges : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Whether a call actually changed the model, from model_changes: a report without observed
|
|
30
|
+
* document changes means nothing changed (a read through a write-capable tool). Undefined only
|
|
31
|
+
* when there is no report (an older bridge), so declared metadata decides.
|
|
32
|
+
*/
|
|
33
|
+
export function changedModel(payload: unknown): boolean | undefined {
|
|
34
|
+
const changes = readModelChanges(payload);
|
|
35
|
+
if (!changes) return undefined;
|
|
36
|
+
if (!changes.observed) return false;
|
|
37
|
+
return (changes.added?.count ?? 0) + (changes.modified?.count ?? 0) + (changes.deleted?.count ?? 0) > 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Every name_collision object anywhere in a payload (rejected rows keep it next to their reason). */
|
|
41
|
+
export function findCollisions(payload: unknown, found: NameCollision[] = [], depth = 0): NameCollision[] {
|
|
42
|
+
if (depth > 6 || payload === null || typeof payload !== "object") return found;
|
|
43
|
+
for (const [key, value] of Object.entries(payload as Record<string, unknown>)) {
|
|
44
|
+
if (key === "name_collision" && value && typeof value === "object" && "existing_id" in value) found.push(value as NameCollision);
|
|
45
|
+
else if (key !== "model_changes") findCollisions(value, found, depth + 1);
|
|
46
|
+
}
|
|
47
|
+
return found;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const normalize = (text: string) => text.normalize("NFKC").toLowerCase().replace(/\s+/g, " ").trim();
|
|
51
|
+
const words = new Intl.Segmenter(undefined, { granularity: "word" });
|
|
52
|
+
const graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Word boundaries of a text in any writing system, from Unicode segmentation (ICU): spaces and
|
|
56
|
+
* punctuation where a script uses them, dictionary boundaries where it does not (Chinese,
|
|
57
|
+
* Japanese, Thai, Khmer, ...). No language or script list is involved.
|
|
58
|
+
*/
|
|
59
|
+
export function wordBoundaries(text: string): Set<number> {
|
|
60
|
+
const bounds = new Set([0, text.length]);
|
|
61
|
+
for (const segment of words.segment(text)) { bounds.add(segment.index); bounds.add(segment.index + segment.segment.length); }
|
|
62
|
+
return bounds;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Whether name occurs in text as whole words: 'Site' is not mentioned by 'opposite'. */
|
|
66
|
+
export function mentionsName(text: string, bounds: Set<number>, name: string | null | undefined, minGraphemes = 2): boolean {
|
|
67
|
+
const value = normalize(name ?? "");
|
|
68
|
+
if ([...graphemes.segment(value)].length < minGraphemes) return false;
|
|
69
|
+
for (let at = text.indexOf(value); at >= 0; at = text.indexOf(value, at + 1))
|
|
70
|
+
if (bounds.has(at) && bounds.has(at + value.length)) return true;
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createScopeMonitor(options: { minNameGraphemes?: number } = {}) {
|
|
75
|
+
let request = "";
|
|
76
|
+
let bounds = new Set<number>();
|
|
77
|
+
const created = new Set<number>();
|
|
78
|
+
const createdFamilyTypes = new Set<string>();
|
|
79
|
+
const notedFamilyTypes = new Set<string>();
|
|
80
|
+
const noted = new Set<number>();
|
|
81
|
+
const notedCollisions = new Set<number | string>();
|
|
82
|
+
const mentioned = (name: string | null | undefined) => mentionsName(request, bounds, name, options.minNameGraphemes ?? 2);
|
|
83
|
+
return {
|
|
84
|
+
/** Start of a new user request; its text decides which object names the request mentions. */
|
|
85
|
+
reset(prompt: string) {
|
|
86
|
+
request = normalize(prompt ?? ""); bounds = wordBoundaries(request);
|
|
87
|
+
created.clear(); noted.clear(); notedCollisions.clear(); createdFamilyTypes.clear(); notedFamilyTypes.clear();
|
|
88
|
+
},
|
|
89
|
+
/** Record one tool result; returns a scope note to append, or null. */
|
|
90
|
+
afterResult(payload: unknown): string | null {
|
|
91
|
+
const notes: string[] = [];
|
|
92
|
+
const changes = readModelChanges(payload);
|
|
93
|
+
for (const item of changes?.added?.items ?? []) created.add(item.id);
|
|
94
|
+
for (const name of changes?.family?.types_added ?? []) createdFamilyTypes.add(name);
|
|
95
|
+
// Family types have no element ID; the same rule applies to them by name.
|
|
96
|
+
const familyTypes = (changes?.family?.types_changed ?? []).filter(name => !createdFamilyTypes.has(name) && !notedFamilyTypes.has(name) && mentioned(name));
|
|
97
|
+
for (const name of familyTypes) notedFamilyTypes.add(name);
|
|
98
|
+
const preexisting = [...(changes?.modified?.items ?? []).filter(item => !created.has(item.id) && !noted.has(item.id) && mentioned(item.name)),
|
|
99
|
+
...familyTypes.map(name => ({ id: null as unknown as number, name, category: "family type" }))];
|
|
100
|
+
for (const item of preexisting) if (item.id !== null) noted.add(item.id);
|
|
101
|
+
if (preexisting.length) notes.push(`PI-Revit scope note: this call changed ${preexisting.map(item => item.id === null ? `family type '${item.name}'` : `'${item.name}' (id ${item.id})`).join(", ")}, which existed before this request: no call in this request created it. `
|
|
102
|
+
+ "If the request asked you to create an object with this name, you changed an existing object instead: tell the user exactly what you changed and ask before changing it further. If the request asked you to change this existing object, continue.");
|
|
103
|
+
// Family types have no element ID; they are noted by name instead.
|
|
104
|
+
const key = (c: NameCollision) => c.existing_id ?? `${c.kind}:${c.name}`;
|
|
105
|
+
const collisions = findCollisions(payload).filter(collision => !notedCollisions.has(key(collision)));
|
|
106
|
+
for (const collision of collisions) notedCollisions.add(key(collision));
|
|
107
|
+
if (collisions.length) notes.push(`PI-Revit scope note: ${collisions.map(c => `'${c.name ?? c.sheet_number}' is already used by ${c.kind ?? "an object"}${c.existing_id !== null ? ` ${c.existing_id}` : ""}`).join("; ")}. `
|
|
108
|
+
+ "That object existed before this call. Do not edit, rename, reuse, replace or delete it to get past the collision: ask the user which object or name to use, or create under a distinct name and report the collision.");
|
|
109
|
+
return notes.length ? notes.join("\n") : null;
|
|
110
|
+
},
|
|
111
|
+
/** IDs of objects created in this request, as reported by model_changes. */
|
|
112
|
+
created: () => [...created],
|
|
113
|
+
};
|
|
114
|
+
}
|