pi-revit 0.3.1 → 0.5.0

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.
Files changed (120) hide show
  1. package/AGENTS.md +167 -0
  2. package/CHANGELOG.md +114 -42
  3. package/README.md +598 -138
  4. package/bin/pi-revit.js +9 -9
  5. package/docs/architecture.md +271 -0
  6. package/docs/evaluation.md +434 -0
  7. package/docs/invariants.json +147 -0
  8. package/extensions/pi-revit/completion-monitor.ts +55 -0
  9. package/extensions/pi-revit/contracts.ts +146 -0
  10. package/extensions/pi-revit/discovery.ts +93 -0
  11. package/extensions/pi-revit/index.ts +231 -85
  12. package/extensions/pi-revit/instance-router.ts +86 -0
  13. package/extensions/pi-revit/platform-prompt.ts +40 -0
  14. package/extensions/pi-revit/scope-monitor.ts +114 -0
  15. package/extensions/pi-revit/script-library.ts +146 -0
  16. package/extensions/pi-revit/tool-catalog.ts +166 -0
  17. package/extensions/pi-revit/tool-documentation.ts +72 -0
  18. package/extensions/pi-revit/tool-schema.ts +8 -0
  19. package/package.json +65 -59
  20. package/scripts/build.ps1 +9 -9
  21. package/scripts/check-sdk.ps1 +66 -66
  22. package/scripts/check-tool-documentation.mjs +287 -0
  23. package/scripts/deploy.ps1 +16 -16
  24. package/scripts/generate-contracts.mjs +80 -0
  25. package/scripts/lib/platform.mjs +226 -0
  26. package/scripts/test-extension.mjs +15 -0
  27. package/skills/pi-revit/SKILL.md +39 -63
  28. package/skills/pi-revit/contracts.generated.json +3524 -0
  29. package/skills/pi-revit/references/execution-rules.md +41 -0
  30. package/skills/pi-revit/references/model-audit-export.md +40 -0
  31. package/skills/pi-revit/references/operation-recovery.md +33 -0
  32. package/skills/pi-revit/references/room-documentation.md +39 -0
  33. package/skills/pi-revit/references/tool-index.md +89 -0
  34. package/skills/pi-revit/references/tools/capture_view.md +62 -0
  35. package/skills/pi-revit/references/tools/change_element_types.md +65 -0
  36. package/skills/pi-revit/references/tools/create_tags.md +85 -0
  37. package/skills/pi-revit/references/tools/delete_elements.md +66 -0
  38. package/skills/pi-revit/references/tools/execute_csharp.md +81 -0
  39. package/skills/pi-revit/references/tools/export_documents.md +75 -0
  40. package/skills/pi-revit/references/tools/find_revit_tools.md +96 -0
  41. package/skills/pi-revit/references/tools/get_element_details.md +66 -0
  42. package/skills/pi-revit/references/tools/get_element_relationships.md +61 -0
  43. package/skills/pi-revit/references/tools/get_element_types.md +67 -0
  44. package/skills/pi-revit/references/tools/get_elements.md +87 -0
  45. package/skills/pi-revit/references/tools/get_linked_elements.md +79 -0
  46. package/skills/pi-revit/references/tools/get_linked_models.md +57 -0
  47. package/skills/pi-revit/references/tools/get_model_coordinates.md +64 -0
  48. package/skills/pi-revit/references/tools/get_model_health.md +53 -0
  49. package/skills/pi-revit/references/tools/get_model_overview.md +57 -0
  50. package/skills/pi-revit/references/tools/get_revit_operation.md +54 -0
  51. package/skills/pi-revit/references/tools/get_schedule_fields.md +62 -0
  52. package/skills/pi-revit/references/tools/get_schedules.md +71 -0
  53. package/skills/pi-revit/references/tools/manage_element_sets.md +92 -0
  54. package/skills/pi-revit/references/tools/manage_revit_instances.md +63 -0
  55. package/skills/pi-revit/references/tools/manage_revit_scripts.md +109 -0
  56. package/skills/pi-revit/references/tools/manage_schedules.md +90 -0
  57. package/skills/pi-revit/references/tools/manage_selection.md +66 -0
  58. package/skills/pi-revit/references/tools/manage_sheet_placements.md +82 -0
  59. package/skills/pi-revit/references/tools/manage_sheets.md +71 -0
  60. package/skills/pi-revit/references/tools/manage_views.md +95 -0
  61. package/skills/pi-revit/references/tools/measure_geometry.md +71 -0
  62. package/skills/pi-revit/references/tools/open_view.md +59 -0
  63. package/skills/pi-revit/references/tools/ping.md +41 -0
  64. package/skills/pi-revit/references/tools/query_spatial_elements.md +74 -0
  65. package/skills/pi-revit/references/tools/read_revit_result.md +53 -0
  66. package/skills/pi-revit/references/tools/search_api_docs.md +65 -0
  67. package/skills/pi-revit/references/tools/set_parameters.md +75 -0
  68. package/skills/pi-revit/references/tools/summarize_elements.md +64 -0
  69. package/skills/pi-revit/references/tools/transform_elements.md +79 -0
  70. package/skills/pi-revit/references/visual-verification.md +36 -0
  71. package/skills/pi-revit/tool-manifest.json +338 -0
  72. package/src/Revit/BridgeServer.cs +75 -19
  73. package/src/Revit/OperationStore.cs +178 -0
  74. package/src/Revit/ToolRegistry.cs +61 -8
  75. package/src/Revit/Tools/CaptureView.cs +9 -0
  76. package/src/Revit/Tools/ChangeElementTypes.cs +74 -0
  77. package/src/Revit/Tools/ChangeSet.cs +39 -0
  78. package/src/Revit/Tools/CreateTags.cs +107 -0
  79. package/src/Revit/Tools/DeleteElements.cs +53 -0
  80. package/src/Revit/Tools/DocumentGuard.cs +12 -2
  81. package/src/Revit/Tools/ElementNames.cs +103 -0
  82. package/src/Revit/Tools/ElementQueryScope.cs +27 -0
  83. package/src/Revit/Tools/ElementTraits.cs +53 -0
  84. package/src/Revit/Tools/ExecuteCsharp.cs +26 -7
  85. package/src/Revit/Tools/ExportDocuments.cs +9 -0
  86. package/src/Revit/Tools/FailureGuard.cs +26 -26
  87. package/src/Revit/Tools/GetElementDetails.cs +28 -2
  88. package/src/Revit/Tools/GetElementRelationships.cs +82 -0
  89. package/src/Revit/Tools/GetElementTypes.cs +8 -0
  90. package/src/Revit/Tools/GetElements.cs +58 -55
  91. package/src/Revit/Tools/GetLinkedElements.cs +89 -0
  92. package/src/Revit/Tools/GetLinkedModels.cs +73 -0
  93. package/src/Revit/Tools/GetModelCoordinates.cs +56 -0
  94. package/src/Revit/Tools/GetModelHealth.cs +7 -0
  95. package/src/Revit/Tools/GetModelOverview.cs +187 -160
  96. package/src/Revit/Tools/GetScheduleFields.cs +44 -0
  97. package/src/Revit/Tools/GetSchedules.cs +96 -0
  98. package/src/Revit/Tools/InheritedState.Summary.cs +57 -0
  99. package/src/Revit/Tools/InheritedState.cs +144 -0
  100. package/src/Revit/Tools/ManageElementSets.cs +114 -0
  101. package/src/Revit/Tools/ManageSchedules.cs +174 -0
  102. package/src/Revit/Tools/ManageSelection.cs +9 -0
  103. package/src/Revit/Tools/ManageSheetPlacements.cs +113 -0
  104. package/src/Revit/Tools/ManageSheets.cs +72 -0
  105. package/src/Revit/Tools/ManageViews.cs +115 -0
  106. package/src/Revit/Tools/MeasureGeometry.cs +60 -0
  107. package/src/Revit/Tools/ModelChanges.cs +154 -0
  108. package/src/Revit/Tools/ModelEditBatch.cs +105 -0
  109. package/src/Revit/Tools/ModelEditInputs.cs +49 -0
  110. package/src/Revit/Tools/OpenView.cs +8 -0
  111. package/src/Revit/Tools/ParameterResolver.cs +94 -0
  112. package/src/Revit/Tools/QuerySpatialElements.cs +70 -0
  113. package/src/Revit/Tools/SearchApiDocs.cs +72 -4
  114. package/src/Revit/Tools/SetParameters.cs +50 -119
  115. package/src/Revit/Tools/SpatialBounds.cs +30 -0
  116. package/src/Revit/Tools/SummarizeElements.cs +94 -0
  117. package/src/Revit/Tools/ToolContract.cs +48 -0
  118. package/src/Revit/Tools/ToolSupport.cs +4 -0
  119. package/src/Revit/Tools/TransformElements.cs +73 -0
  120. package/workspace/AGENTS.md +54 -48
@@ -0,0 +1,146 @@
1
+ import { createHash } from "node:crypto";
2
+ import snapshot from "../../skills/pi-revit/contracts.generated.json";
3
+
4
+ /**
5
+ * Resource contract v2. Bridge tools declare it in C# (ITool.Keywords/Limits/Verification);
6
+ * Pi-native tools declare it here. scripts/generate-contracts.mjs snapshots both into
7
+ * skills/pi-revit/contracts.generated.json for offline discovery and manual generation.
8
+ * Code is the single owner; the snapshot is generated and checked, never hand-edited.
9
+ */
10
+ export type AlternativeKind = "tool" | "api" | "user" | "revit_unsupported";
11
+ export type VerificationKind = "reread" | "capture" | "inspect_output" | "none";
12
+ export interface ToolLimit { what: string; alternative: { kind: AlternativeKind; ref: string | null } }
13
+ export interface ToolContract {
14
+ name: string;
15
+ source: "bridge" | "native";
16
+ tier: string | null;
17
+ write: boolean;
18
+ effects: string[];
19
+ requires_document: boolean;
20
+ /** Document kinds a bridge tool works in ("project", "family"); null for native utilities. */
21
+ document_kinds: string[] | null;
22
+ keywords: string[];
23
+ limits: ToolLimit[];
24
+ verification: VerificationKind | null;
25
+ contract_hash: string;
26
+ parameters: unknown;
27
+ }
28
+ export const ALTERNATIVE_KINDS: readonly AlternativeKind[] = ["tool", "api", "user", "revit_unsupported"];
29
+ export const VERIFICATION_KINDS: readonly VerificationKind[] = ["reread", "capture", "inspect_output", "none"];
30
+
31
+ const limit = (what: string, kind: AlternativeKind, ref: string): ToolLimit => ({ what, alternative: { kind, ref } });
32
+
33
+ /** Contracts of tools implemented by this extension (not advertised by a bridge). */
34
+ export const NATIVE_CONTRACTS: Record<string, Pick<ToolContract, "keywords" | "limits" | "verification" | "effects" | "write">> = {
35
+ ping: {
36
+ keywords: ["connection", "status", "version", "revit running", "bridge", "loaded version"],
37
+ limits: [limit("Whether a project document is open", "tool", "get_model_overview")],
38
+ verification: null, effects: [], write: false,
39
+ },
40
+ manage_revit_instances: {
41
+ keywords: ["instances", "sessions", "multiple revit", "switch revit", "select session"],
42
+ limits: [limit("Activating a document inside a Revit session", "user", "Open or activate the document in Revit")],
43
+ verification: "reread", effects: ["session"], write: false,
44
+ },
45
+ find_revit_tools: {
46
+ keywords: ["capabilities", "what can you do", "tools", "manuals", "documentation", "help", "workflows"],
47
+ limits: [limit("Reading manual contents", "tool", "read (open the returned path)")],
48
+ verification: "none", effects: ["session"], write: false,
49
+ },
50
+ read_revit_result: {
51
+ keywords: ["large result", "saved result", "continue result", "next fragment"],
52
+ limits: [limit("Paging the original query", "tool", "the original query tool (for example get_elements) with its next_offset")],
53
+ verification: null, effects: [], write: false,
54
+ },
55
+ get_revit_operation: {
56
+ keywords: ["receipt", "timeout", "operation status", "did it run", "retry"],
57
+ limits: [limit("Operations from an earlier bridge session", "user", "Inspect the model state directly; receipts do not survive a restart")],
58
+ verification: null, effects: [], write: false,
59
+ },
60
+ manage_revit_scripts: {
61
+ keywords: ["saved scripts", "script library", "reusable script", "run script", "script history"],
62
+ limits: [limit("Preview or rollback of a script run", "tool", "dedicated tools with preview (set_parameters, transform_elements, delete_elements, manage_views, ...)")],
63
+ verification: "reread", effects: ["model", "ui", "files", "external"], write: true,
64
+ },
65
+ };
66
+
67
+ /** Deterministic JSON with sorted object keys; arrays keep their order. */
68
+ export function canonicalJson(value: unknown): string {
69
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
70
+ if (value !== null && typeof value === "object")
71
+ return `{${Object.keys(value as object).sort().filter(key => (value as Record<string, unknown>)[key] !== undefined)
72
+ .map(key => `${JSON.stringify(key)}:${canonicalJson((value as Record<string, unknown>)[key])}`).join(",")}}`;
73
+ return JSON.stringify(value ?? null);
74
+ }
75
+
76
+ /**
77
+ * Executable-contract hash: the input schema and declared effects only. Descriptions,
78
+ * guidelines, keywords and limits are guidance, so rewording them never flags a bridge
79
+ * as incompatible. Runtime discovery and the generator share this one function.
80
+ */
81
+ export function contractHash(descriptor: { parameters?: unknown; write?: boolean; effects?: string[]; requiresDocument?: boolean; documentKinds?: string[] | null }): string {
82
+ const write = descriptor.write === true;
83
+ // Support for both document kinds is the default and leaves the hash unchanged; a restriction
84
+ // is executable behavior, so a project-only tool differs from a bridge that does not declare it.
85
+ const kinds = [...(descriptor.documentKinds ?? [])].sort();
86
+ const restricted = kinds.length > 0 && kinds.join(",") !== "family,project";
87
+ const payload = canonicalJson({
88
+ parameters: withoutGuidance(descriptor.parameters ?? { type: "object", properties: {} }),
89
+ write,
90
+ effects: descriptor.effects ?? (write ? ["model"] : []),
91
+ requiresDocument: descriptor.requiresDocument ?? true,
92
+ ...(restricted ? { documentKinds: kinds } : {}),
93
+ });
94
+ return createHash("sha256").update(payload).digest("hex").slice(0, 16);
95
+ }
96
+
97
+ /**
98
+ * Schema annotations are guidance: rewording a property description, title or example
99
+ * must not flag a bridge as incompatible. Types, required lists, enums, constants,
100
+ * bounds and defaults remain part of the executable contract. Property names are kept
101
+ * even when a property is literally called "description".
102
+ */
103
+ function withoutGuidance(schema: unknown, isPropertyMap = false): unknown {
104
+ if (Array.isArray(schema)) return schema.map(item => withoutGuidance(item));
105
+ if (schema === null || typeof schema !== "object") return schema;
106
+ const result: Record<string, unknown> = {};
107
+ for (const [key, value] of Object.entries(schema as Record<string, unknown>)) {
108
+ if (!isPropertyMap && (key === "description" || key === "title" || key === "examples")) continue;
109
+ result[key] = withoutGuidance(value, !isPropertyMap && (key === "properties" || key === "patternProperties" || key === "$defs" || key === "definitions"));
110
+ }
111
+ return result;
112
+ }
113
+
114
+ export const packagedContracts = new Map((snapshot.tools as ToolContract[]).map(tool => [tool.name, tool]));
115
+
116
+ /**
117
+ * The one-call API lookup for an "api" limit: the API names in its reference joined with "; ",
118
+ * the multi-member form of search_api_docs, so an alternative's members are verified in one call
119
+ * instead of one search per member. Dotted members and multi-word CamelCase types count; prose
120
+ * words do not. Null when the reference names no API member. The documentation gate checks every
121
+ * name against the installed RevitAPI.xml.
122
+ */
123
+ export function apiLookupQuery(ref: string | null | undefined): string | null {
124
+ if (!ref) return null;
125
+ const names = [...ref.matchAll(/\b[A-Z][A-Za-z0-9]*(?:\.[A-Z_][A-Za-z0-9_]*)+\b|\b[A-Z][a-z0-9]+(?:[A-Z][A-Za-z0-9]*)+\b/g)].map(match => match[0]);
126
+ const unique = [...new Set(names)].slice(0, 10);
127
+ return unique.length ? unique.join("; ") : null;
128
+ }
129
+
130
+ /** Limits as shown to the agent: an "api" alternative carries its one-call lookup. */
131
+ export function withApiLookups(limits: ToolLimit[]): (ToolLimit & { lookup?: { tool: "search_api_docs"; query: string } })[] {
132
+ return limits.map(limit => {
133
+ const query = limit.alternative.kind === "api" ? apiLookupQuery(limit.alternative.ref) : null;
134
+ return query ? { ...limit, lookup: { tool: "search_api_docs" as const, query } } : limit;
135
+ });
136
+ }
137
+
138
+ /** Normalize a live bridge descriptor's optional v2 fields; absent fields stay unknown. */
139
+ export function descriptorLimits(value: unknown): ToolLimit[] | null {
140
+ if (!Array.isArray(value)) return null;
141
+ return value.flatMap(item => {
142
+ const what = (item as ToolLimit)?.what, kind = (item as ToolLimit)?.alternative?.kind;
143
+ return typeof what === "string" && ALTERNATIVE_KINDS.includes(kind)
144
+ ? [{ what, alternative: { kind, ref: typeof (item as ToolLimit).alternative.ref === "string" ? (item as ToolLimit).alternative.ref : null } }] : [];
145
+ });
146
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Discovery over every PI-Revit resource kind: tools, workflows, shared references and
3
+ * subject skills (present and future). Tool vocabulary is English, one canonical language.
4
+ * There are no per-language rules: the model translates a request written in any language
5
+ * into English search words, replies in the user's language, and reads localized Revit
6
+ * names from results. Matching only normalizes the English vocabulary: case, accents,
7
+ * simple inflection and word forms, with ranked all-word then partial matches.
8
+ */
9
+ export interface DiscoveryDocument {
10
+ name: string;
11
+ /** Strong fields: name, keywords. Medium: summary, declared limits and input names. Weak: description. */
12
+ keywords?: readonly string[];
13
+ summary?: string | null;
14
+ /** Text of declared limits, so a request just outside a tool finds the tool and its alternative. */
15
+ limits?: readonly string[];
16
+ /** Input property names, e.g. "level" or "sheet_id": what a tool can be asked about. */
17
+ inputs?: readonly string[];
18
+ description?: string | null;
19
+ }
20
+ export interface DiscoveryMatch<T> { item: T; matched: number; score: number; complete: boolean }
21
+ export interface DiscoveryResult<T> { mode: "all" | "partial" | "none"; groups: number; matches: DiscoveryMatch<T>[] }
22
+
23
+ /** Words that carry no tool meaning in English search phrases. */
24
+ const STOPWORDS = new Set(["the", "and", "for", "with", "all", "this", "that", "these", "those", "please", "can", "could", "you", "are", "was",
25
+ "how", "what", "which", "does", "from", "into", "each", "every", "some", "any", "get", "give", "tell", "want", "need", "would", "should",
26
+ "its", "there", "their", "them", "use", "using", "revit", "model", "project", "pi", "via", "about", "then", "than", "also", "one", "two"]);
27
+
28
+ export function normalizeText(text: string): string {
29
+ return text.toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g, "").replace(/c#/g, "csharp");
30
+ }
31
+
32
+ export function tokenize(text: string): string[] {
33
+ return normalizeText(text).split(/[^a-z0-9]+/).filter(token => token.length >= 3 || /\d/.test(token));
34
+ }
35
+
36
+ /** Simple English inflection stripping. */
37
+ export function stem(token: string): string {
38
+ if (token.length > 4 && token.endsWith("ies")) return token.slice(0, -3) + "y";
39
+ if (token.length > 4 && /(ches|shes|xes|sses|zes)$/.test(token)) return token.slice(0, -2);
40
+ if (token.length > 3 && token.endsWith("s") && !token.endsWith("ss") && !token.endsWith("us")) return token.slice(0, -1);
41
+ return token;
42
+ }
43
+
44
+ /** Same word, different form: "connected"/"connection", "rotate"/"rotation", "create"/"creating". */
45
+ function hit(variant: string, term: string) {
46
+ if (variant === term) return true;
47
+ if (variant.length < 5 || term.length < 5) return false;
48
+ let common = 0;
49
+ while (common < variant.length && common < term.length && variant[common] === term[common]) common++;
50
+ return common === Math.min(variant.length, term.length) || common >= Math.max(5, Math.min(variant.length, term.length) - 3);
51
+ }
52
+
53
+ const WEIGHTS = { strong: 3, medium: 2, weak: 1 } as const;
54
+
55
+ export function createDiscoveryIndex() {
56
+ /** Meaningful query words; numbers are call arguments, not capability words. */
57
+ function groups(query: string): string[] {
58
+ return [...new Set(normalizeText(query).split(/[^a-z0-9]+/)
59
+ .filter(word => word.length >= 3 && !/^\d+$/.test(word) && !STOPWORDS.has(word)).map(stem))];
60
+ }
61
+ function terms(document: DiscoveryDocument) {
62
+ const strong = [...tokenize(document.name.replaceAll("_", " ")), ...(document.keywords ?? []).flatMap(tokenize)].map(stem);
63
+ const medium = [document.summary ?? "", ...(document.limits ?? []), ...(document.inputs ?? []).map(name => name.replaceAll("_", " "))]
64
+ .flatMap(tokenize).map(stem);
65
+ const weak = tokenize(document.description ?? "").map(stem);
66
+ return [[WEIGHTS.strong, new Set(strong)], [WEIGHTS.medium, new Set(medium)], [WEIGHTS.weak, new Set(weak)]] as const;
67
+ }
68
+ function search<T>(items: readonly T[], query: string, toDocument: (item: T) => DiscoveryDocument): DiscoveryResult<T> {
69
+ const queryGroups = groups(query);
70
+ if (queryGroups.length === 0) return { mode: "none", groups: 0, matches: [] };
71
+ const scored = items.map(item => {
72
+ const fields = terms(toDocument(item));
73
+ let matched = 0, score = 0, strongHit = false;
74
+ for (const word of queryGroups) {
75
+ let best = 0;
76
+ for (const [weight, set] of fields) {
77
+ if (weight <= best) continue;
78
+ for (const term of set) if (hit(word, term)) { best = weight; break; }
79
+ }
80
+ if (best > 0) { matched++; score += best; if (best === WEIGHTS.strong) strongHit = true; }
81
+ }
82
+ return { item, matched, score, strongHit, complete: matched === queryGroups.length };
83
+ });
84
+ // Complete matches first; then partial matches with most words and a hit on a name or keyword.
85
+ const needed = Math.max(1, Math.floor(queryGroups.length / 2));
86
+ const ranked = scored.filter(entry => entry.complete || (entry.matched >= needed && entry.strongHit))
87
+ .sort((a, b) => Number(b.complete) - Number(a.complete) || b.matched - a.matched || b.score - a.score
88
+ || toDocument(a.item).name.localeCompare(toDocument(b.item).name));
89
+ const mode = ranked.some(entry => entry.complete) ? "all" : ranked.length ? "partial" : "none";
90
+ return { mode, groups: queryGroups.length, matches: ranked.map(({ item, matched, score, complete }) => ({ item, matched, score, complete })) };
91
+ }
92
+ return { search, groups };
93
+ }