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.
Files changed (119) hide show
  1. package/AGENTS.md +167 -0
  2. package/CHANGELOG.md +44 -0
  3. package/README.md +644 -553
  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 +342 -255
  12. package/extensions/pi-revit/instance-router.ts +86 -86
  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 +144 -144
  16. package/extensions/pi-revit/tool-catalog.ts +113 -14
  17. package/extensions/pi-revit/tool-documentation.ts +72 -0
  18. package/extensions/pi-revit/tool-schema.ts +8 -0
  19. package/package.json +8 -2
  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 +30 -218
  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 +38 -27
  31. package/skills/pi-revit/references/operation-recovery.md +33 -0
  32. package/skills/pi-revit/references/room-documentation.md +37 -26
  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 +93 -87
  73. package/src/Revit/OperationStore.cs +178 -178
  74. package/src/Revit/ToolRegistry.cs +88 -57
  75. package/src/Revit/Tools/CaptureView.cs +10 -2
  76. package/src/Revit/Tools/ChangeElementTypes.cs +74 -60
  77. package/src/Revit/Tools/ChangeSet.cs +39 -0
  78. package/src/Revit/Tools/CreateTags.cs +107 -95
  79. package/src/Revit/Tools/DeleteElements.cs +53 -44
  80. package/src/Revit/Tools/DocumentGuard.cs +74 -64
  81. package/src/Revit/Tools/ElementNames.cs +103 -0
  82. package/src/Revit/Tools/ElementQueryScope.cs +27 -27
  83. package/src/Revit/Tools/ElementTraits.cs +53 -0
  84. package/src/Revit/Tools/ExecuteCsharp.cs +54 -45
  85. package/src/Revit/Tools/ExportDocuments.cs +129 -121
  86. package/src/Revit/Tools/GetElementDetails.cs +37 -41
  87. package/src/Revit/Tools/GetElementRelationships.cs +82 -76
  88. package/src/Revit/Tools/GetElementTypes.cs +8 -0
  89. package/src/Revit/Tools/GetElements.cs +75 -86
  90. package/src/Revit/Tools/GetLinkedElements.cs +89 -82
  91. package/src/Revit/Tools/GetLinkedModels.cs +73 -66
  92. package/src/Revit/Tools/GetModelCoordinates.cs +56 -49
  93. package/src/Revit/Tools/GetModelHealth.cs +7 -0
  94. package/src/Revit/Tools/GetModelOverview.cs +185 -158
  95. package/src/Revit/Tools/GetScheduleFields.cs +44 -37
  96. package/src/Revit/Tools/GetSchedules.cs +96 -89
  97. package/src/Revit/Tools/InheritedState.Summary.cs +57 -0
  98. package/src/Revit/Tools/InheritedState.cs +144 -0
  99. package/src/Revit/Tools/ManageElementSets.cs +114 -106
  100. package/src/Revit/Tools/ManageSchedules.cs +174 -164
  101. package/src/Revit/Tools/ManageSelection.cs +45 -37
  102. package/src/Revit/Tools/ManageSheetPlacements.cs +113 -97
  103. package/src/Revit/Tools/ManageSheets.cs +72 -63
  104. package/src/Revit/Tools/ManageViews.cs +115 -100
  105. package/src/Revit/Tools/MeasureGeometry.cs +60 -54
  106. package/src/Revit/Tools/ModelChanges.cs +154 -0
  107. package/src/Revit/Tools/ModelEditBatch.cs +105 -102
  108. package/src/Revit/Tools/ModelEditInputs.cs +49 -49
  109. package/src/Revit/Tools/OpenView.cs +9 -2
  110. package/src/Revit/Tools/ParameterResolver.cs +94 -0
  111. package/src/Revit/Tools/QuerySpatialElements.cs +70 -63
  112. package/src/Revit/Tools/SearchApiDocs.cs +72 -4
  113. package/src/Revit/Tools/SetParameters.cs +60 -79
  114. package/src/Revit/Tools/SpatialBounds.cs +30 -30
  115. package/src/Revit/Tools/SummarizeElements.cs +94 -87
  116. package/src/Revit/Tools/ToolContract.cs +48 -0
  117. package/src/Revit/Tools/ToolSupport.cs +5 -1
  118. package/src/Revit/Tools/TransformElements.cs +72 -57
  119. package/workspace/AGENTS.md +26 -20
@@ -1,66 +1,66 @@
1
- #Requires -Version 5.1
2
-
3
- function Test-PiRevitSdk {
4
- param(
5
- [string[]]$TargetFrameworks,
6
- [string]$Context = 'Revit bridge build',
7
- [switch]$OfferDownload
8
- )
9
-
10
- # Check the SDK selected in the build's working directory, not just installed
11
- # SDKs: global.json or PATH can still select an older SDK.
12
- $requiredMajor = 0
13
- foreach ($framework in $TargetFrameworks) {
14
- if ($framework -notmatch '^net(\d+)\.') {
15
- throw "Cannot determine the required .NET SDK for '$framework'."
16
- }
17
- $requiredMajor = [Math]::Max($requiredMajor, [int]$matches[1])
18
- }
19
- if ($requiredMajor -eq 0) { throw 'No target frameworks were supplied for the SDK check.' }
20
-
21
- $selectedVersion = $null
22
- if (Get-Command dotnet -ErrorAction SilentlyContinue) {
23
- try {
24
- $versionOutput = @(& dotnet --version 2>&1)
25
- if ($LASTEXITCODE -eq 0) {
26
- foreach ($line in $versionOutput) {
27
- if ("$line".Trim() -match '^(\d+)\.\d+\.\d+(?:-[\w.-]+)?$') {
28
- $selectedVersion = "$line".Trim()
29
- if ([int]$matches[1] -ge $requiredMajor) { return $true }
30
- }
31
- }
32
- }
33
- }
34
- catch {
35
- # A runtime-only installation or an unresolved global.json can make
36
- # --version fail. Present the same actionable prerequisite message.
37
- }
38
- }
39
-
40
- $downloadUrl = "https://dotnet.microsoft.com/en-us/download/dotnet/$requiredMajor.0"
41
- Write-Host "`npi-revit prerequisite check: $Context" -ForegroundColor Yellow
42
- Write-Host "Building this add-in requires the .NET $requiredMajor SDK or a newer compatible SDK."
43
- if ($selectedVersion) {
44
- Write-Host "Your build tools currently use .NET SDK $selectedVersion."
45
- }
46
- else {
47
- Write-Host 'No usable .NET SDK could be selected in this terminal.'
48
- }
49
- Write-Host 'Revit can run normally with its runtime; compiling the pi-revit add-in also needs the SDK.'
50
- Write-Host "Install the .NET $requiredMajor SDK for Windows x64 (choose SDK, not Runtime)."
51
- Write-Host 'You can keep your existing .NET versions installed.'
52
- Write-Host "Download: $downloadUrl"
53
- Write-Host 'Then reopen PowerShell and rerun your install or build command.'
54
- Write-Host 'If the SDK is already installed, check dotnet --list-sdks, PATH, and any global.json selecting an older SDK.'
55
-
56
- if ($OfferDownload -and [Environment]::UserInteractive -and -not [Console]::IsInputRedirected) {
57
- try {
58
- $answer = Read-Host 'Open the SDK download page now? [y/N]'
59
- if ($answer -match '^(?i:y|yes)$') { Start-Process $downloadUrl | Out-Null }
60
- }
61
- catch {
62
- Write-Host "Open the download link above in your browser."
63
- }
64
- }
65
- return $false
66
- }
1
+ #Requires -Version 5.1
2
+
3
+ function Test-PiRevitSdk {
4
+ param(
5
+ [string[]]$TargetFrameworks,
6
+ [string]$Context = 'Revit bridge build',
7
+ [switch]$OfferDownload
8
+ )
9
+
10
+ # Check the SDK selected in the build's working directory, not just installed
11
+ # SDKs: global.json or PATH can still select an older SDK.
12
+ $requiredMajor = 0
13
+ foreach ($framework in $TargetFrameworks) {
14
+ if ($framework -notmatch '^net(\d+)\.') {
15
+ throw "Cannot determine the required .NET SDK for '$framework'."
16
+ }
17
+ $requiredMajor = [Math]::Max($requiredMajor, [int]$matches[1])
18
+ }
19
+ if ($requiredMajor -eq 0) { throw 'No target frameworks were supplied for the SDK check.' }
20
+
21
+ $selectedVersion = $null
22
+ if (Get-Command dotnet -ErrorAction SilentlyContinue) {
23
+ try {
24
+ $versionOutput = @(& dotnet --version 2>&1)
25
+ if ($LASTEXITCODE -eq 0) {
26
+ foreach ($line in $versionOutput) {
27
+ if ("$line".Trim() -match '^(\d+)\.\d+\.\d+(?:-[\w.-]+)?$') {
28
+ $selectedVersion = "$line".Trim()
29
+ if ([int]$matches[1] -ge $requiredMajor) { return $true }
30
+ }
31
+ }
32
+ }
33
+ }
34
+ catch {
35
+ # A runtime-only installation or an unresolved global.json can make
36
+ # --version fail. Present the same actionable prerequisite message.
37
+ }
38
+ }
39
+
40
+ $downloadUrl = "https://dotnet.microsoft.com/en-us/download/dotnet/$requiredMajor.0"
41
+ Write-Host "`npi-revit prerequisite check: $Context" -ForegroundColor Yellow
42
+ Write-Host "Building this add-in requires the .NET $requiredMajor SDK or a newer compatible SDK."
43
+ if ($selectedVersion) {
44
+ Write-Host "Your build tools currently use .NET SDK $selectedVersion."
45
+ }
46
+ else {
47
+ Write-Host 'No usable .NET SDK could be selected in this terminal.'
48
+ }
49
+ Write-Host 'Revit can run normally with its runtime; compiling the pi-revit add-in also needs the SDK.'
50
+ Write-Host "Install the .NET $requiredMajor SDK for Windows x64 (choose SDK, not Runtime)."
51
+ Write-Host 'You can keep your existing .NET versions installed.'
52
+ Write-Host "Download: $downloadUrl"
53
+ Write-Host 'Then reopen PowerShell and rerun your install or build command.'
54
+ Write-Host 'If the SDK is already installed, check dotnet --list-sdks, PATH, and any global.json selecting an older SDK.'
55
+
56
+ if ($OfferDownload -and [Environment]::UserInteractive -and -not [Console]::IsInputRedirected) {
57
+ try {
58
+ $answer = Read-Host 'Open the SDK download page now? [y/N]'
59
+ if ($answer -match '^(?i:y|yes)$') { Start-Process $downloadUrl | Out-Null }
60
+ }
61
+ catch {
62
+ Write-Host "Open the download link above in your browser."
63
+ }
64
+ }
65
+ return $false
66
+ }
@@ -0,0 +1,287 @@
1
+ import assert from 'node:assert/strict';
2
+ import { existsSync } from 'node:fs';
3
+ import { access, readFile, readdir } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { pathToFileURL } from 'node:url';
6
+ import {
7
+ CONTRACT_END, CONTRACT_START, createLoader, evaluateCorpus, extractBridgeDescriptors, manifestPath, manualRoot, registerExtension,
8
+ resolvePiDependencies, revitApiXmlPaths, root, skillRoot, snapshotPath,
9
+ } from './lib/platform.mjs';
10
+ import { buildArtifacts } from './generate-contracts.mjs';
11
+
12
+ // Offline platform gates: no model execution, real bridge connection or package
13
+ // installation. Source metadata, generated artifacts, schemas, guidance structure and
14
+ // prompt size are checked; Revit bodies and agent behavior need their own tests.
15
+ // Budgets change deliberately, never by accident: raise them only with a reviewed reason
16
+ // in the change log. The startup total covers the platform section plus the snippets and
17
+ // guidelines of tools active at session start; it must not grow with the tool count.
18
+ const PROMPT_BUDGET = { startupTotal: 7000, perToolGuidelines: 1200 };
19
+ const problems = [];
20
+ const warnings = [];
21
+ const fail = message => problems.push(message);
22
+
23
+ const piRequire = resolvePiDependencies();
24
+ const loader = createLoader(piRequire);
25
+ const { Check, Errors } = await import(pathToFileURL(piRequire.resolve('typebox/value')).href);
26
+ assert.equal(Check({ type: 'integer', minimum: 1 }, 0), false, 'schema validator must enforce numeric constraints');
27
+ assert.equal(Check({ type: 'object', required: ['id'], properties: { id: { type: 'integer' } } }, {}), false,
28
+ 'schema validator must enforce required properties');
29
+
30
+ // 1. Generated artifacts are current: code is the single owner of contracts.
31
+ const { files: generated } = await buildArtifacts();
32
+ for (const [file, content] of generated)
33
+ if (!existsSync(file) || await readFile(file, 'utf8') !== content) fail(`${path.relative(root, file)}: generated artifact is stale; run npm run generate:contracts`);
34
+
35
+ const packageInfo = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8'));
36
+ const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
37
+ const snapshot = JSON.parse(await readFile(snapshotPath, 'utf8'));
38
+ const descriptors = await extractBridgeDescriptors();
39
+ const bridgeNames = new Set(descriptors.map(tool => tool.name));
40
+ assert.ok(descriptors.length > 0, 'real registry must expose bridge descriptors');
41
+ assert.equal(bridgeNames.size, descriptors.length, 'bridge tool names must be unique');
42
+
43
+ // 2. Documentation manifest v2 structure.
44
+ assert.equal(manifest.schema_version, 2, 'unrecognized documentation manifest schema');
45
+ assert.ok(typeof manifest.documentation_revision === 'string' && manifest.documentation_revision.trim(), 'documentation revision is required');
46
+ const groupIds = new Set(manifest.groups.map(group => group.id));
47
+ const manifestNames = new Set(manifest.tools.map(tool => tool.name));
48
+ assert.equal(manifestNames.size, manifest.tools.length, 'manifest tool names must be unique');
49
+ for (const tool of manifest.tools) {
50
+ assert.match(tool.name, /^[a-z][a-z0-9_]*$/, 'tool names must be safe filenames');
51
+ assert.ok(['bridge', 'native'].includes(tool.source), `${tool.name}: source must be bridge or native`);
52
+ assert.ok(typeof tool.summary === 'string' && tool.summary.trim(), `${tool.name}: summary is required`);
53
+ if (!groupIds.has(tool.group)) fail(`${tool.name}: unknown manifest group ${tool.group}`);
54
+ }
55
+ assert.deepEqual(manifest.tools.filter(tool => tool.source === 'bridge').map(tool => tool.name).sort(), [...bridgeNames].sort(),
56
+ 'manifest bridge inventory must equal the real C# registry');
57
+ const guidanceNames = new Set();
58
+ for (const guide of manifest.guidance) {
59
+ if (guidanceNames.has(guide.name)) fail(`guidance ${guide.name}: duplicate name`);
60
+ guidanceNames.add(guide.name);
61
+ if (!['workflow', 'reference', 'skill'].includes(guide.kind)) fail(`guidance ${guide.name}: kind must be workflow, reference or skill`);
62
+ if (!existsSync(path.join(skillRoot, guide.path))) fail(`guidance ${guide.name}: missing ${guide.path}`);
63
+ if (!guide.summary?.trim() || !(guide.keywords?.length >= 3)) fail(`guidance ${guide.name}: needs a summary and at least 3 keywords`);
64
+ }
65
+
66
+ // 3. Real registration against the extracted bridge catalogue.
67
+ const fixture = await registerExtension(loader, descriptors, packageInfo.version);
68
+ const { publicBridgeSchema } = await loader.import(path.join(root, 'extensions', 'pi-revit', 'tool-schema.ts'));
69
+ const { ALTERNATIVE_KINDS, VERIFICATION_KINDS, apiLookupQuery } = await loader.import(path.join(root, 'extensions', 'pi-revit', 'contracts.ts'));
70
+ const registered = fixture.registered;
71
+ let promptReport;
72
+ try {
73
+ assert.ok(fixture.routes.includes('/tools'), 'connector must register real extracted bridge schemas');
74
+ assert.deepEqual([...registered.keys()].sort(), [...manifestNames].sort(), 'manifest must cover every registered public tool');
75
+ assert.deepEqual(manifest.tools.filter(tool => tool.source === 'native').map(tool => tool.name).sort(),
76
+ [...registered.keys()].filter(name => !bridgeNames.has(name)).sort(), 'native inventory must match actual extension registrations');
77
+ for (const descriptor of descriptors)
78
+ assert.deepEqual(JSON.parse(JSON.stringify(registered.get(descriptor.name).parameters)), publicBridgeSchema(descriptor.parameters),
79
+ `${descriptor.name}: registered parameters must include the actual extension schema overlay`);
80
+
81
+ // 8 (measured here while registered). Prompt budget: the startup contribution must not grow with tool count.
82
+ await fixture.events.get('session_start')?.({}, { ui: { notify() {} } });
83
+ const options = { sections: {}, selectedTools: fixture.activeTools() };
84
+ await fixture.events.get('before_agent_start')?.({ systemPromptOptions: options }, {});
85
+ const section = options.sections.pi_revit ?? '';
86
+ const perTool = fixture.activeTools().map(name => ({ name, snippet: registered.get(name)?.promptSnippet ?? '', guidelines: (registered.get(name)?.promptGuidelines ?? []).join('\n') }));
87
+ const toolChars = perTool.reduce((sum, tool) => sum + tool.snippet.length + tool.guidelines.length, 0);
88
+ promptReport = { sectionChars: section.length, toolChars, activeTools: perTool.length, total: section.length + toolChars,
89
+ largestGuidelines: perTool.map(t => [t.name, t.guidelines.length]).sort((a, b) => b[1] - a[1])[0] };
90
+ if (!section.includes('PI-Revit protocol')) fail('platform section was not injected by before_agent_start');
91
+ if (/For \w+, read .*\.md when detailed usage/.test(perTool.map(t => t.guidelines).join('\n'))) fail('per-tool manual pointer lines must not return; the platform section holds one pointer');
92
+ for (const tool of perTool) if (tool.guidelines.length > PROMPT_BUDGET.perToolGuidelines) fail(`${tool.name}: ${tool.guidelines.length} guideline characters exceed the per-tool budget of ${PROMPT_BUDGET.perToolGuidelines}`);
93
+ if (promptReport.total > PROMPT_BUDGET.startupTotal) fail(`PI-Revit startup prompt contribution ${promptReport.total} characters exceeds the budget of ${PROMPT_BUDGET.startupTotal}`);
94
+ } finally { await fixture.restore(); }
95
+
96
+ // 4. Contract v2 validity for every tool, including limit references.
97
+ const publicNames = new Set([...registered.keys(), 'read']);
98
+ const apiXml = await Promise.all(revitApiXmlPaths().map(file => readFile(file, 'utf8')));
99
+ if (!apiXml.length) warnings.push('RevitAPI.xml not found; API references in declared limits were not verified (set REVIT_API_PATH).');
100
+ const apiMemberExists = member => apiXml.some(xml => new RegExp(`name="[TMPFE]:Autodesk\\.Revit\\.(?:[A-Za-z0-9]+\\.)*${member.replace(/\./g, '\\.')}(?:["(])`).test(xml));
101
+ for (const tool of snapshot.tools) {
102
+ if (!(tool.keywords?.length >= 3)) fail(`${tool.name}: declare at least 3 discovery keywords`);
103
+ if ((tool.write || tool.effects?.length) && !VERIFICATION_KINDS.includes(tool.verification))
104
+ fail(`${tool.name}: a tool that can write or has effects must declare verification (${VERIFICATION_KINDS.join(', ')})`);
105
+ if (tool.verification !== null && !VERIFICATION_KINDS.includes(tool.verification)) fail(`${tool.name}: unknown verification ${tool.verification}`);
106
+ if (!tool.limits.length) fail(`${tool.name}: declare at least one limit with its alternative`);
107
+ for (const limit of tool.limits) {
108
+ if (!limit.what?.trim() || !ALTERNATIVE_KINDS.includes(limit.alternative?.kind) || !limit.alternative.ref?.trim())
109
+ { fail(`${tool.name}: every limit needs what, a known alternative kind and a reference`); continue; }
110
+ const ref = limit.alternative.ref;
111
+ if (limit.alternative.kind === 'tool') {
112
+ const named = [...ref.matchAll(/\b[a-z][a-z0-9]*(?:_[a-z0-9]+)+\b|\bread\b/g)].map(m => m[0]).filter(name => !['host_bounds', 'include_instance_count', 'count_only', 'next_offset'].includes(name));
113
+ if (!named.length) fail(`${tool.name}: tool alternative "${ref}" names no public tool`);
114
+ for (const name of named) if (!publicNames.has(name)) fail(`${tool.name}: tool alternative names unknown tool ${name}`);
115
+ }
116
+ // Every name in the generated one-call lookup must resolve, so the lookup never sends the agent searching.
117
+ if (limit.alternative.kind === 'api' && apiXml.length)
118
+ for (const member of (apiLookupQuery(ref) ?? '').split('; ').filter(Boolean))
119
+ if (!apiMemberExists(member)) fail(`${tool.name}: API alternative ${member} was not found in the installed RevitAPI.xml`);
120
+ }
121
+ // Required inputs must not be described as optional anywhere in their schema text.
122
+ for (const name of tool.parameters?.required ?? [])
123
+ if (/\boptional\b/i.test(tool.parameters.properties?.[name]?.description ?? '')) fail(`${tool.name}.${name}: a required input's description says "optional"`);
124
+ }
125
+
126
+ // 5. Manuals: one per tool, generated block present, examples valid, limitation language declared.
127
+ const manualFiles = (await readdir(manualRoot)).filter(file => file.endsWith('.md')).sort();
128
+ assert.deepEqual(manualFiles, [...manifestNames].map(name => `${name}.md`).sort(), 'one manual per public tool; no orphan manuals');
129
+ const snapshotByName = new Map(snapshot.tools.map(tool => [tool.name, tool]));
130
+ let exampleCount = 0;
131
+ for (const tool of manifest.tools) {
132
+ const file = path.join(manualRoot, `${tool.name}.md`);
133
+ const markdown = await readFile(file, 'utf8');
134
+ if (!markdown.includes(CONTRACT_START) || !markdown.includes(CONTRACT_END)) fail(`${tool.name}: manual lacks its generated contract block`);
135
+ const handWritten = markdown.slice(0, markdown.indexOf(CONTRACT_START)) + markdown.slice(markdown.indexOf(CONTRACT_END) + CONTRACT_END.length);
136
+ if (/outside this tool|\bunsupported\b|not supported/i.test(handWritten) && !snapshotByName.get(tool.name)?.limits.length)
137
+ fail(`${tool.name}: the manual states a limitation but the tool declares no limits with alternatives`);
138
+ const examples = [...markdown.matchAll(/^```json[ \t]*\r?\n([\s\S]*?)^```[ \t]*\r?$/gm)];
139
+ if (examples.length === 0) fail(`${tool.name}: missing JSON input example`);
140
+ const schema = registered.get(tool.name).parameters;
141
+ for (const match of examples) {
142
+ const label = `${path.relative(root, file)}:${markdown.slice(0, match.index).split('\n').length + 1}`;
143
+ try {
144
+ const input = JSON.parse(match[1]);
145
+ assert.ok(input !== null && typeof input === 'object' && !Array.isArray(input), 'example must be an input argument object');
146
+ if (!Check(schema, input)) throw new Error(JSON.stringify([...Errors(schema, input)]));
147
+ assertDocumentedKeys(schema, input, label);
148
+ // Negative controls exercise the actual composed schemas rather than a copied test schema.
149
+ for (const key of schema.required ?? []) {
150
+ const missingRequired = { ...input };
151
+ delete missingRequired[key];
152
+ assert.equal(Check(schema, missingRequired), false, `${tool.name}: required ${key} was not enforced`);
153
+ }
154
+ if (schema.properties?._operation_id) assert.equal(Check(schema, { ...input, _operation_id: 42 }), false, `${tool.name}: retry ID must remain a string`);
155
+ exampleCount++;
156
+ } catch (error) { fail(`${label}: ${error.message}`); }
157
+ }
158
+ }
159
+
160
+ // 6. Invariant register: every rule has an owner and an enforcement, and guidance tags resolve.
161
+ const register = JSON.parse(await readFile(path.join(root, 'docs', 'invariants.json'), 'utf8'));
162
+ const invariants = new Map(register.invariants.map(item => [item.id, item]));
163
+ const guidanceFiles = [...await markdownUnder(path.join(root, 'skills')), ...await markdownUnder(path.join(root, 'docs')),
164
+ ...['AGENTS.md', 'README.md', 'workspace/AGENTS.md'].map(file => path.join(root, file))];
165
+ const tagged = new Set();
166
+ const testSources = (await Promise.all((await filesUnder(path.join(root, 'tests'))).filter(file => /\.(mjs|cjs|cs)$/.test(file)).map(file => readFile(file, 'utf8')))).join('\n')
167
+ + await readFile(new URL(import.meta.url), 'utf8');
168
+ for (const file of guidanceFiles) {
169
+ const text = await readFile(file, 'utf8');
170
+ for (const match of text.matchAll(/<!-- inv:([a-z0-9-]+) -->/g)) {
171
+ tagged.add(match[1]);
172
+ if (!invariants.has(match[1])) fail(`${path.relative(root, file)}: tag inv:${match[1]} is not in docs/invariants.json`);
173
+ }
174
+ }
175
+ const coreRules = ['skills/pi-revit/SKILL.md', 'skills/pi-revit/references/execution-rules.md', 'skills/pi-revit/references/operation-recovery.md',
176
+ 'skills/pi-revit/references/visual-verification.md', 'workspace/AGENTS.md'];
177
+ for (const relative of coreRules)
178
+ for (const [index, line] of (await readFile(path.join(root, relative), 'utf8')).split(/\r?\n/).entries())
179
+ if (/\b(never|must not)\b/i.test(line) && !/<!-- inv:[a-z0-9-]+ -->/.test(line)) fail(`${relative}:${index + 1}: a never/must-not rule needs an <!-- inv:<id> --> tag and a register entry`);
180
+ for (const item of register.invariants) {
181
+ if (!['safety', 'correctness', 'guidance'].includes(item.severity)) fail(`inv:${item.id}: unknown severity`);
182
+ if (!tagged.has(item.id)) fail(`inv:${item.id}: no guidance sentence carries this tag`);
183
+ if (item.enforcement === 'code') {
184
+ if (!item.test || !testSources.includes(item.test)) fail(`inv:${item.id}: enforcement=code needs an existing test titled "${item.test}"`);
185
+ for (const location of item.location ?? []) if (!existsSync(path.join(root, location))) fail(`inv:${item.id}: missing location ${location}`);
186
+ } else if (item.enforcement === 'advisory') {
187
+ if (!item.reason) fail(`inv:${item.id}: advisory rules must state why code cannot enforce them`);
188
+ if (item.severity === 'safety' && !item.eval_scenario) fail(`inv:${item.id}: an advisory safety rule must name the agent-eval scenario that measures it`);
189
+ } else fail(`inv:${item.id}: enforcement must be code or advisory`);
190
+ }
191
+ const scenarioFile = path.join(root, 'tests', 'agent-eval', 'scenarios.json');
192
+ const scenarioIds = existsSync(scenarioFile) ? new Set(JSON.parse(await readFile(scenarioFile, 'utf8')).scenarios.map(s => s.id)) : new Set();
193
+ for (const item of register.invariants) if (item.eval_scenario && !scenarioIds.has(item.eval_scenario)) fail(`inv:${item.id}: eval scenario ${item.eval_scenario} is not defined in tests/agent-eval/scenarios.json`);
194
+
195
+ // Architecture gates over every bridge source file, present and future. Shared primitives
196
+ // own cross-cutting policy; a new tool cannot reintroduce a private variant.
197
+ for (const file of (await readdir(path.join(root, 'src', 'Revit', 'Tools'))).filter(name => name.endsWith('.cs'))) {
198
+ const source = (await readFile(path.join(root, 'src', 'Revit', 'Tools', file), 'utf8')).replace(/\/\/.*$/gm, '').replace(/"(?:[^"\\]|\\.)*"/g, '""');
199
+ // inv:no-tool-saves-model
200
+ if (/\.\s*(Save|SaveAs|Close|SynchronizeWithCentral)\s*\(/.test(source)) fail(`architecture: no bridge tool saves, closes or synchronizes a document (${file})`);
201
+ // inv:parameter-ambiguity: LookupParameter returns an arbitrary one of several same-named parameters.
202
+ if (file !== 'ParameterResolver.cs' && /\b(LookupParameter|GetParameters)\s*\(/.test(source))
203
+ fail(`architecture: ${file} resolves parameters by name directly; use ParameterResolver (one ambiguity policy for every tool)`);
204
+ // inv:special-objects-flagged: sheet-owned schedule instances include the titleblock's revision schedule.
205
+ if (file !== 'ElementTraits.cs' && /\bScheduleSheetInstance\b/.test(source) && !/\bElementTraits\./.test(source))
206
+ fail(`architecture: ${file} handles ScheduleSheetInstance without ElementTraits classification`);
207
+ // inv:derived-state-reported: an object made from an existing one carries its hidden content, overrides and values.
208
+ if (file !== 'InheritedState.cs' && /\.\s*Duplicate\s*\(|\bCopyElements?\s*\(|\bMirrorElements\s*\(|\.\s*ChangeTypeId\s*\(/.test(source) && !/\bInheritedState\./.test(source))
209
+ fail(`architecture: a tool that duplicates, copies or retypes reports inherited state; ${file} creates from an existing object without InheritedState`);
210
+ // inv:existing-objects-not-reused: names and sheet numbers go through the one collision check.
211
+ if (file !== 'ElementNames.cs' && /\.\s*(Name|SheetNumber)\s*=(?!=)|\.\s*(NewType|RenameCurrentType)\s*\(/.test(source))
212
+ fail(`architecture: names are assigned through ElementNames; ${file} assigns a Name, SheetNumber or family type name directly`);
213
+ // inv:document-kind-declared: every tool states where it works, so the dispatcher can refuse the rest.
214
+ if (/:\s*ITool\b/.test(source) && !/\bDocumentKinds\s*=>/.test(source))
215
+ fail(`architecture: every bridge tool declares the document kinds it supports; ${file} has no DocumentKinds`);
216
+ }
217
+ // inv:model-changes-reported: the dispatcher, not each tool, attaches model_changes to every model-changing call.
218
+ {
219
+ const server = await readFile(path.join(root, 'src', 'Revit', 'BridgeServer.cs'), 'utf8');
220
+ if (!/ModelChangeRecorder\.For\(\s*tool\b/.test(server) || !/\.Attach\(/.test(server))
221
+ fail('architecture: the bridge dispatcher records model changes for every tool call (ModelChangeRecorder.For(tool, ...) and Attach)');
222
+ }
223
+
224
+ // 7. Discovery corpus: English task phrasings (the model translates other languages).
225
+ // Every public tool needs coverage; overall top-N recall must stay above the threshold.
226
+ const corpus = JSON.parse(await readFile(path.join(root, 'tests', 'discovery', 'corpus.json'), 'utf8'));
227
+ const corpusResults = await evaluateCorpus(loader, corpus, packageInfo.version);
228
+ const recall = corpusResults.filter(r => r.pass).length / corpusResults.length;
229
+ if (recall < corpus.min_recall) fail(`discovery corpus recall ${recall.toFixed(3)} is below ${corpus.min_recall}; run node tests/discovery/run-corpus.mjs for the misses`);
230
+ for (const tool of manifest.tools) {
231
+ const count = corpus.entries.filter(entry => entry.expect === tool.name).length;
232
+ if (count < corpus.min_per_tool) fail(`${tool.name}: the discovery corpus needs at least ${corpus.min_per_tool} phrasings (has ${count})`);
233
+ }
234
+ for (const guide of manifest.guidance.filter(g => g.kind === 'workflow'))
235
+ if (!corpus.entries.some(entry => entry.expect_guidance === guide.name)) fail(`workflow ${guide.name}: the discovery corpus needs at least one phrasing`);
236
+ if (!corpus.entries.some(entry => entry.expect_fallback)) fail('the discovery corpus needs at least one query that must return the capability route');
237
+
238
+ // 9. Local links in all guidance and contributor documents.
239
+ let localLinkCount = 0;
240
+ for (const file of [...guidanceFiles, path.join(root, 'tests/tool-documentation/README.md')]) {
241
+ if (!existsSync(file)) continue;
242
+ const markdown = await readFile(file, 'utf8');
243
+ const prose = markdown.replace(/^```[^\n]*\r?\n[\s\S]*?^```[ \t]*\r?$/gm, '');
244
+ for (const match of prose.matchAll(/\[[^\]]*\]\((?:<([^>]+)>|([^\s)]+))(?:\s+"[^"]*")?\)/g)) {
245
+ const destination = match[1] ?? match[2];
246
+ if (/^(?:[a-z][a-z0-9+.-]*:|#)/i.test(destination)) continue;
247
+ const relative = decodeURIComponent(destination.split('#')[0]);
248
+ if (!relative) continue;
249
+ try { await access(path.resolve(path.dirname(file), relative)); localLinkCount++; }
250
+ catch { fail(`${path.relative(root, file)}: broken local link ${destination}`); }
251
+ }
252
+ }
253
+
254
+ for (const warning of warnings) console.warn(`WARNING: ${warning}`);
255
+ assert.equal(problems.length, 0, `Platform validation failed:\n${problems.join('\n')}`);
256
+ console.log(`PASS: ${descriptors.length} bridge + ${registered.size - descriptors.length} native contracts; ${manualFiles.length} manuals with generated contracts; ${exampleCount} valid input examples; ${register.invariants.length} invariants; discovery recall ${corpusResults.filter(r => r.pass).length}/${corpusResults.length}; ${localLinkCount} local links.`);
257
+ console.log(`Prompt: platform section ${promptReport.sectionChars} + ${promptReport.activeTools} active tools ${promptReport.toolChars} = ${promptReport.total} characters (budget ${PROMPT_BUDGET.startupTotal}).`);
258
+ console.log('Offline structure verification only: conditional runtime rules, Revit behavior, and agent effectiveness require their own tests.');
259
+
260
+ function assertDocumentedKeys(schema, value, location) {
261
+ if (!schema || typeof schema !== 'object' || value === null) return;
262
+ if (Array.isArray(value)) {
263
+ if (schema.items) value.forEach((item, index) => assertDocumentedKeys(schema.items, item, `${location}[${index}]`));
264
+ } else if (typeof value === 'object' && schema.properties) {
265
+ for (const [key, child] of Object.entries(value)) {
266
+ // Some schemas permit additional JSON properties, but manual argument examples must use advertised keys.
267
+ assert.ok(Object.hasOwn(schema.properties, key) || schema.additionalProperties === true || schema.patternProperties,
268
+ `${location}: undocumented argument ${key}`);
269
+ if (schema.properties[key]) assertDocumentedKeys(schema.properties[key], child, `${location}.${key}`);
270
+ }
271
+ }
272
+ }
273
+
274
+ async function filesUnder(directory) {
275
+ const files = [];
276
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
277
+ if (['bin', 'obj', 'node_modules'].includes(entry.name)) continue;
278
+ const file = path.join(directory, entry.name);
279
+ if (entry.isDirectory()) files.push(...await filesUnder(file));
280
+ else if (entry.isFile()) files.push(file);
281
+ }
282
+ return files;
283
+ }
284
+
285
+ async function markdownUnder(directory) {
286
+ return (await filesUnder(directory)).filter(file => file.endsWith('.md'));
287
+ }
@@ -16,16 +16,16 @@ Usage:
16
16
  scripts\deploy.ps1 # auto-detect + deploy to all installed
17
17
  scripts\deploy.ps1 -RevitVersion 2026
18
18
  scripts\deploy.ps1 -RevitVersion 2027 -RevitApiPath "D:\Autodesk\Revit 2027"
19
- scripts\deploy.ps1 -SkipBuild
20
- scripts\deploy.ps1 -CheckOnly # check prerequisites without installing
19
+ scripts\deploy.ps1 -SkipBuild
20
+ scripts\deploy.ps1 -CheckOnly # check prerequisites without installing
21
21
  #>
22
22
  param(
23
23
  [string]$RevitVersion = '',
24
24
  [string]$Configuration = 'Release',
25
25
  [string]$RevitApiPath = '',
26
- [switch]$SkipBuild,
27
- [switch]$CheckOnly,
28
- [switch]$OfferDownload
26
+ [switch]$SkipBuild,
27
+ [switch]$CheckOnly,
28
+ [switch]$OfferDownload
29
29
  )
30
30
 
31
31
  $ErrorActionPreference = 'Stop'
@@ -71,20 +71,20 @@ else {
71
71
  Write-Host ("Detected Revit: " + (($targets | ForEach-Object { $_.Version }) -join ', ')) -ForegroundColor Cyan
72
72
  }
73
73
 
74
- # Validate every target before starting any build or deployment.
75
- if ($CheckOnly -or -not $SkipBuild) {
76
- . (Join-Path $PSScriptRoot 'check-sdk.ps1')
77
- $context = 'Detected Revit: ' + (($targets | ForEach-Object { $_.Version }) -join ', ')
78
- if (-not (Test-PiRevitSdk -TargetFrameworks @($targets.Tfm) -Context $context -OfferDownload:$OfferDownload)) { exit 1 }
79
- }
80
- if ($CheckOnly) { exit 0 }
81
-
82
- # Build once per distinct target framework, compiling against a matching RevitAPI.dll.
74
+ # Validate every target before starting any build or deployment.
75
+ if ($CheckOnly -or -not $SkipBuild) {
76
+ . (Join-Path $PSScriptRoot 'check-sdk.ps1')
77
+ $context = 'Detected Revit: ' + (($targets | ForEach-Object { $_.Version }) -join ', ')
78
+ if (-not (Test-PiRevitSdk -TargetFrameworks @($targets.Tfm) -Context $context -OfferDownload:$OfferDownload)) { exit 1 }
79
+ }
80
+ if ($CheckOnly) { exit 0 }
81
+
82
+ # Build once per distinct target framework, compiling against a matching RevitAPI.dll.
83
83
  if (-not $SkipBuild) {
84
84
  foreach ($group in ($targets | Group-Object Tfm)) {
85
85
  $apiPath = ($group.Group | Select-Object -First 1).Path
86
- & (Join-Path $PSScriptRoot 'build.ps1') -Configuration $Configuration -RevitApiPath $apiPath -TargetFramework $group.Name
87
- if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
86
+ & (Join-Path $PSScriptRoot 'build.ps1') -Configuration $Configuration -RevitApiPath $apiPath -TargetFramework $group.Name
87
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
88
88
  }
89
89
  }
90
90
 
@@ -0,0 +1,80 @@
1
+ // Generates every derived documentation artifact from code, the single owner of
2
+ // executable contracts:
3
+ // skills/pi-revit/contracts.generated.json offline contract snapshot for discovery
4
+ // "Contract (generated)" block in each tool manual
5
+ // generated tables in skills/pi-revit/references/tool-index.md
6
+ // Usage: node scripts/generate-contracts.mjs [--check]
7
+ // --check writes nothing and fails when any artifact is stale.
8
+ import { existsSync } from 'node:fs';
9
+ import { readFile, writeFile } from 'node:fs/promises';
10
+ import path from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+ import {
13
+ applyIndexBlock, applyManualBlock, createLoader, extractBridgeDescriptors, manifestPath, manualRoot,
14
+ registerExtension, renderManualBlock, renderToolIndex, resolvePiDependencies, root, snapshotPath, toolIndexPath,
15
+ } from './lib/platform.mjs';
16
+
17
+ export async function buildArtifacts() {
18
+ const piRequire = resolvePiDependencies();
19
+ const loader = createLoader(piRequire);
20
+ // The extension imports the snapshot; bootstrap an empty one on a first run.
21
+ if (!existsSync(snapshotPath)) await writeFile(snapshotPath, JSON.stringify({ schema_version: 1, tools: [] }, null, 2) + '\n');
22
+ const packageInfo = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8'));
23
+ const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
24
+ const descriptors = await extractBridgeDescriptors();
25
+ const { contractHash, NATIVE_CONTRACTS, withApiLookups } = await loader.import(path.join(root, 'extensions', 'pi-revit', 'contracts.ts'));
26
+ const fixture = await registerExtension(loader, descriptors, packageInfo.version);
27
+ let tools;
28
+ try {
29
+ const bridge = new Map(descriptors.map(d => [d.name, d]));
30
+ tools = [...fixture.registered.keys()].sort().map(name => {
31
+ const d = bridge.get(name);
32
+ if (d) return {
33
+ name, source: 'bridge', tier: d.tier ?? 'core', write: d.write === true, effects: d.effects ?? [], requires_document: d.requiresDocument !== false,
34
+ document_kinds: d.documentKinds ?? null,
35
+ keywords: d.keywords ?? [], limits: (d.limits ?? []).map(l => ({ what: l.what, alternative: { kind: l.alternative.kind, ref: l.alternative.ref ?? null } })),
36
+ verification: d.verification ?? null, contract_hash: contractHash(d), parameters: d.parameters,
37
+ };
38
+ const native = NATIVE_CONTRACTS[name];
39
+ if (!native) throw new Error(`Native tool ${name} has no NATIVE_CONTRACTS entry in extensions/pi-revit/contracts.ts`);
40
+ const parameters = JSON.parse(JSON.stringify(fixture.registered.get(name).parameters));
41
+ return {
42
+ name, source: 'native', tier: 'core', write: native.write, effects: native.effects, requires_document: false, document_kinds: null,
43
+ keywords: native.keywords, limits: native.limits, verification: native.verification,
44
+ contract_hash: contractHash({ parameters, write: native.write, effects: native.effects, requiresDocument: false }), parameters,
45
+ };
46
+ });
47
+ } finally { await fixture.restore(); }
48
+
49
+ const snapshot = JSON.stringify({
50
+ schema_version: 1,
51
+ generated_by: 'scripts/generate-contracts.mjs',
52
+ note: 'Generated from src/Revit/Tools/*.cs (bridge) and extensions/pi-revit/contracts.ts (native). Do not edit; change the code and regenerate.',
53
+ tools,
54
+ }, null, 2) + '\n';
55
+ const files = new Map([[snapshotPath, snapshot]]);
56
+ for (const tool of tools) {
57
+ const file = path.join(manualRoot, `${tool.name}.md`);
58
+ if (!existsSync(file)) continue; // missing manuals are reported by the documentation checker
59
+ files.set(file, applyManualBlock(await readFile(file, 'utf8'), renderManualBlock({ ...tool, limits: withApiLookups(tool.limits) })));
60
+ }
61
+ files.set(toolIndexPath, applyIndexBlock(await readFile(toolIndexPath, 'utf8'), renderToolIndex(manifest, tools)));
62
+ return { files, tools, descriptors };
63
+ }
64
+
65
+ if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
66
+ const check = process.argv.includes('--check');
67
+ const { files } = await buildArtifacts();
68
+ const stale = [];
69
+ for (const [file, content] of files) {
70
+ const current = existsSync(file) ? await readFile(file, 'utf8') : null;
71
+ if (current === content) continue;
72
+ stale.push(path.relative(root, file));
73
+ if (!check) await writeFile(file, content);
74
+ }
75
+ if (check && stale.length) {
76
+ console.error(`Generated artifacts are stale. Run npm run generate:contracts:\n${stale.join('\n')}`);
77
+ process.exit(1);
78
+ }
79
+ console.log(check ? `Generated artifacts are current (${files.size} files).` : `Updated ${stale.length} of ${files.size} generated artifacts.`);
80
+ }