From ea4b94e09f67f1b11a633795d0b00e2c06e4a64a Mon Sep 17 00:00:00 2001 From: Affan Khan Date: Wed, 12 Nov 2025 14:39:19 -0500 Subject: [PATCH 01/17] Add new recipe element handling and improve geometry change events in rebase tests --- .../backend/src/test/hubaccess/Rebase.test.ts | 223 ++++++++++++++++-- 1 file changed, 208 insertions(+), 15 deletions(-) diff --git a/core/backend/src/test/hubaccess/Rebase.test.ts b/core/backend/src/test/hubaccess/Rebase.test.ts index 0bcd3e9d83e..e683650cf59 100644 --- a/core/backend/src/test/hubaccess/Rebase.test.ts +++ b/core/backend/src/test/hubaccess/Rebase.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Guid, Id64Array, Id64String } from "@itwin/core-bentley"; -import { Code, GeometricElement2dProps, IModel, QueryBinder, RelatedElementProps, SubCategoryAppearance } from "@itwin/core-common"; +import { Code, GeometricElement2dProps, GeometricModelProps, IModel, ModelIdAndGeometryGuid, QueryBinder, RelatedElementProps, SubCategoryAppearance } from "@itwin/core-common"; import * as chai from "chai"; import * as chaiAsPromised from "chai-as-promised"; import { Suite } from "mocha"; @@ -36,6 +36,10 @@ class TestIModel { bis:GraphicalElement2d + + bis:TemplateRecipe2d + + bis:ElementOwnsChildElements @@ -58,6 +62,7 @@ class TestIModel { if (undefined === drawingCategoryId) drawingCategoryId = DrawingCategory.insert(b1, IModel.dictionaryId, "MyDrawingCategory", new SubCategoryAppearance()); this.drawingCategoryId = drawingCategoryId; + b1.saveChanges(); await b1.pushChanges({ description: "drawing category" }); b1.close(); @@ -69,6 +74,35 @@ class TestIModel { this.briefcases.push(b); return b; } + public async insertRecipe2d(b: BriefcaseDb, markAsIndirect?: true) { + await b.locks.acquireLocks({ shared: [IModel.dictionaryId] }); + const baseProps = { + classFullName: "TestDomain:A1Recipe2d", + model: IModel.dictionaryId, + code: Code.createEmpty(), + }; + + let id: Id64String = ""; + if (markAsIndirect) { + b.txns.withIndirectTxnMode(() => { + id = b.elements.insertElement({ ...baseProps, prop1: `${this._data++}` } as any); + }); + return id; + } + return b.elements.insertElement({ ...baseProps, prop1: `${this._data++}` } as any); + } + public async updateRecipe2d(b: BriefcaseDb, id: Id64String, markAsIndirect?: true) { + await b.locks.acquireLocks({ shared: [IModel.dictionaryId], exclusive: [id] }); + const elProps = b.elements.getElementProps(id); + + if (markAsIndirect) { + b.txns.withIndirectTxnMode(() => { + b.elements.updateElement({ ...elProps, prop1: `${this._data++}` } as any); + }); + } else { + b.elements.updateElement({ ...elProps, prop1: `${this._data++}` } as any); + } + } public async insertElement(b: BriefcaseDb, markAsIndirect?: true) { await b.locks.acquireLocks({ shared: [this.drawingModelId] }); const baseProps = { @@ -86,7 +120,7 @@ class TestIModel { } return b.elements.insertElement({ ...baseProps, prop1: `${this._data++}` } as any); } - public async insertElement2(b: BriefcaseDb, args?: { prop1?: string, markAsIndirect?: true, parent?: RelatedElementProps }) { + public async insertElementEx(b: BriefcaseDb, args?: { prop1?: string, markAsIndirect?: true, parent?: RelatedElementProps }) { await b.locks.acquireLocks({ shared: [this.drawingModelId] }); const props: GeometricElement2dProps & { prop1: string } = { @@ -458,7 +492,7 @@ describe("rebase changes & stashing api", function (this: Suite) { await b1.importSchemaStrings([schema]); b1.saveChanges(); - await b1.pushChanges({description: "import schema"}); + await b1.pushChanges({ description: "import schema" }); }); it("should fail to saveChanges() & pushChanges() in indirect scope", async () => { @@ -477,10 +511,10 @@ describe("rebase changes & stashing api", function (this: Suite) { b1.saveChanges(); await chai.expect(b1.txns.withIndirectTxnModeAsync(async () => { - await b1.pushChanges({description: "test"}); + await b1.pushChanges({ description: "test" }); })).to.be.rejectedWith("Cannot push changeset while in an indirect change scope"); - await b1.pushChanges({description: "test"}); + await b1.pushChanges({ description: "test" }); }); it("should fail to saveFileProperty/deleteFileProperty in indirect scope", async () => { @@ -873,7 +907,7 @@ describe("rebase changes & stashing api", function (this: Suite) { await testIModel.insertElement(b1); await testIModel.insertElement(b1); b1.saveChanges(); - await b1.pushChanges({description: "inserted element"}); + await b1.pushChanges({ description: "inserted element" }); await testIModel.insertElement(b2); await testIModel.insertElement(b2); @@ -905,7 +939,7 @@ describe("rebase changes & stashing api", function (this: Suite) { const b2 = await testIModel.openBriefcase(); const parentId = await testIModel.insertElement(b1); - const childId = await testIModel.insertElement2(b1, { parent: { id: parentId, relClassName: "TestDomain:A1OwnsA1" } }); + const childId = await testIModel.insertElementEx(b1, { parent: { id: parentId, relClassName: "TestDomain:A1OwnsA1" } }); b1.saveChanges("insert parent and child"); await b1.pushChanges({ description: `inserted parent ${parentId} and child ${childId}` }); await b2.pullChanges(); @@ -914,7 +948,7 @@ describe("rebase changes & stashing api", function (this: Suite) { await testIModel.deleteElement(b1, childId); b1.saveChanges("delete child"); // no exclusive lock required on child1 - const grandChildId = await testIModel.insertElement2(b2, { parent: { id: childId, relClassName: "TestDomain:A1OwnsA1" }, markAsIndirect: true }); + const grandChildId = await testIModel.insertElementEx(b2, { parent: { id: childId, relClassName: "TestDomain:A1OwnsA1" }, markAsIndirect: true }); b2.saveChanges("delete child and insert grandchild"); await b1.pushChanges({ description: `deleted child ${childId}` }); @@ -971,7 +1005,7 @@ describe("rebase changes & stashing api", function (this: Suite) { const e3Props = await findElement(e3); chai.expect(e3Props).to.exist; }); -it("enum txn changes in recompute", async () => { + it("enum txn changes in recompute", async () => { const b1 = await testIModel.openBriefcase(); const b2 = await testIModel.openBriefcase(); @@ -1006,23 +1040,23 @@ it("enum txn changes in recompute", async () => { return true; }, recompute: async (txn: TxnProps): Promise => { - const reader = SqliteChangesetReader.openTxn({txnId: txn.id, db: b2, disableSchemaCheck: true}); + const reader = SqliteChangesetReader.openTxn({ txnId: txn.id, db: b2, disableSchemaCheck: true }); const adaptor = new ChangesetECAdaptor(reader); adaptor.acceptClass("TestDomain:a1"); const ids = new Set(); - while(adaptor.step()) { + while (adaptor.step()) { if (!adaptor.reader.isIndirect) ids.add(adaptor.inserted?.ECInstanceId || adaptor.deleted?.ECInstanceId as Id64String); } adaptor.close(); - if (txn.props.description === "first change") { + if (txn.props.description === "first change") { chai.expect(Array.from(ids.keys())).deep.equal(["0x40000000001"]); txnVerified++; - } else if (txn.props.description === "second change") { + } else if (txn.props.description === "second change") { chai.expect(Array.from(ids.keys())).deep.equal(["0x40000000003"]); txnVerified++; - } else if (txn.props.description === "third change") { + } else if (txn.props.description === "third change") { chai.expect(Array.from(ids.keys())).deep.equal(["0x40000000005"]); txnVerified++; } else { @@ -1033,7 +1067,7 @@ it("enum txn changes in recompute", async () => { await b2.pullChanges(); chai.expect(txnVerified).to.equal(3); }); -it("before and after rebase events", async () => { + it("before and after rebase events", async () => { const b1 = await testIModel.openBriefcase(); const b2 = await testIModel.openBriefcase(); @@ -1151,5 +1185,164 @@ it("before and after rebase events", async () => { chai.expect(events.rebaseHandler.shouldReinstate.map((txn) => txn.id)).to.deep.equal(["0x100000000", "0x100000001", "0x100000002", "0x100000003"]); chai.expect(events.rebaseHandler.recompute.map((txn) => txn.id)).to.deep.equal(["0x100000000", "0x100000001", "0x100000002", "0x100000003"]); }); + it("onModelGeometryChanged() fired during pullChanges() with no local changes", async () => { + const b1 = await testIModel.openBriefcase(); + const b2 = await testIModel.openBriefcase(); + + const pushChangeFromB2 = async () => { + await b2.pullChanges(); + await testIModel.insertElement(b2) + b2.saveChanges(); + await b2.pushChanges({ description: "insert element on b2" }); + }; + + const events = { + modelGeometryChanged: [] as ReadonlyArray[], + }; + + const getGeometryGuidFromB1 = (modelId: string) => { + const modelProps = b1.models.tryGetModelProps(modelId); + return modelProps?.geometryGuid; + }; + + const clearEvents = () => { + events.modelGeometryChanged = []; + }; + + b1.txns.onModelGeometryChanged.addListener((changes: ReadonlyArray) => { + events.modelGeometryChanged.push(changes); + }); + + clearEvents(); + + b1.txns.rebaser.setCustomHandler({ + shouldReinstate: (_txn: TxnProps) => { + return true; + }, + recompute: async (_txn: TxnProps) => { + // await testIModel.insertElement(b1); + }, + }); + + await pushChangeFromB2(); + + clearEvents(); + const geomGuidBeforePull = getGeometryGuidFromB1("0x20000000001"); + chai.expect(geomGuidBeforePull).is.undefined; + await b1.pushChanges({ description: "push changes on b1" }); + const geomGuidAfterPull = getGeometryGuidFromB1("0x20000000001"); + chai.expect(geomGuidAfterPull).is.undefined; + chai.expect(events.modelGeometryChanged.length).to.equal(0); + }); + it("onModelGeometryChanged() fired during rebase with geometric local change ", async () => { + // Test implementation here + const b1 = await testIModel.openBriefcase(); + const b2 = await testIModel.openBriefcase(); + + const pushChangeFromB2 = async () => { + await b2.pullChanges(); + await testIModel.insertElement(b2) + b2.saveChanges(); + await b2.pushChanges({ description: "insert element on b2" }); + }; + + const events = { + modelGeometryChanged: [] as ReadonlyArray[], + }; + + const getGeometryGuidFromB1 = (modelId: string) => { + const modelProps = b1.models.tryGetModelProps(modelId); + return modelProps?.geometryGuid; + }; + + const clearEvents = () => { + events.modelGeometryChanged = []; + }; + + b1.txns.onModelGeometryChanged.addListener((changes: ReadonlyArray) => { + events.modelGeometryChanged.push(changes); + }); + + clearEvents(); + const e1 = await testIModel.insertElement(b1); + const e2 = await testIModel.insertElement(b1, true); + chai.expect(e1).to.exist; + chai.expect(e2).to.exist; + b1.saveChanges(`insert element ${e1} and ${e2}`); + + chai.expect(events.modelGeometryChanged.length).to.equal(1); + chai.expect(events.modelGeometryChanged[0].length).to.equal(1); + chai.expect(events.modelGeometryChanged[0][0].id).to.equal("0x20000000001"); + chai.assert(Guid.isGuid(events.modelGeometryChanged[0][0].guid)); + + b1.txns.rebaser.setCustomHandler({ + shouldReinstate: (_txn: TxnProps) => { + return true; + }, + recompute: async (_txn: TxnProps) => { + await testIModel.insertElement(b1); + }, + }); + + await pushChangeFromB2(); + + clearEvents(); + const geomGuidBeforePull = getGeometryGuidFromB1("0x20000000001"); + await b1.pushChanges({ description: "push changes on b1" }); + const geomGuidAfterPull = getGeometryGuidFromB1("0x20000000001"); + chai.expect(geomGuidBeforePull).to.not.equal(geomGuidAfterPull); + chai.expect(events.modelGeometryChanged.length).to.equal(4); + }); + it.only("onModelGeometryChanged() fired during rebase with none-geometric local change", async () => { + const b1 = await testIModel.openBriefcase(); + const b2 = await testIModel.openBriefcase(); + + const pushChangeFromB2 = async () => { + await b2.pullChanges(); + await testIModel.insertElement(b2) + b2.saveChanges(); + await b2.pushChanges({ description: "insert element on b2" }); + }; + + const events = { + modelGeometryChanged: [] as ReadonlyArray[], + }; + + const getGeometryGuidFromB1 = (modelId: string) => { + const modelProps = b1.models.tryGetModelProps(modelId); + return modelProps?.geometryGuid; + }; + + const clearEvents = () => { + events.modelGeometryChanged = []; + }; + + b1.txns.onModelGeometryChanged.addListener((changes: ReadonlyArray) => { + events.modelGeometryChanged.push(changes); + }); + + clearEvents(); + + b1.txns.rebaser.setCustomHandler({ + shouldReinstate: (_txn: TxnProps) => { + return true; + }, + recompute: async (_txn: TxnProps) => { + // await testIModel.insertElement(b1); + }, + }); + + await pushChangeFromB2(); + await testIModel.insertRecipe2d(b1); + b1.saveChanges(); + + clearEvents(); + const geomGuidBeforePull = getGeometryGuidFromB1("0x20000000001"); + chai.expect(geomGuidBeforePull).is.undefined; + await b1.pushChanges({ description: "push changes on b1" }); + const geomGuidAfterPull = getGeometryGuidFromB1("0x20000000001"); + chai.expect(geomGuidAfterPull).is.exist; + chai.expect(events.modelGeometryChanged.length).to.equal(1); + }); }); From 63de21d69559b7dee2e7e9383043ad86786cb389 Mon Sep 17 00:00:00 2001 From: Affan Khan Date: Wed, 12 Nov 2025 14:40:40 -0500 Subject: [PATCH 02/17] Update test descriptions for geometry change events during rebase operations --- core/backend/src/test/hubaccess/Rebase.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/backend/src/test/hubaccess/Rebase.test.ts b/core/backend/src/test/hubaccess/Rebase.test.ts index e683650cf59..b4db1555dc3 100644 --- a/core/backend/src/test/hubaccess/Rebase.test.ts +++ b/core/backend/src/test/hubaccess/Rebase.test.ts @@ -1185,7 +1185,7 @@ describe("rebase changes & stashing api", function (this: Suite) { chai.expect(events.rebaseHandler.shouldReinstate.map((txn) => txn.id)).to.deep.equal(["0x100000000", "0x100000001", "0x100000002", "0x100000003"]); chai.expect(events.rebaseHandler.recompute.map((txn) => txn.id)).to.deep.equal(["0x100000000", "0x100000001", "0x100000002", "0x100000003"]); }); - it("onModelGeometryChanged() fired during pullChanges() with no local changes", async () => { + it("onModelGeometryChanged() fired during rebase/pullMerge with no local change", async () => { const b1 = await testIModel.openBriefcase(); const b2 = await testIModel.openBriefcase(); @@ -1234,7 +1234,7 @@ describe("rebase changes & stashing api", function (this: Suite) { chai.expect(geomGuidAfterPull).is.undefined; chai.expect(events.modelGeometryChanged.length).to.equal(0); }); - it("onModelGeometryChanged() fired during rebase with geometric local change ", async () => { + it("onModelGeometryChanged() fired during rebase with geometric local change", async () => { // Test implementation here const b1 = await testIModel.openBriefcase(); const b2 = await testIModel.openBriefcase(); From 64118ed8048abd81155951e9c2e43ce71a292609 Mon Sep 17 00:00:00 2001 From: Affan Khan Date: Wed, 12 Nov 2025 14:44:20 -0500 Subject: [PATCH 03/17] Add test for onModelGeometryChanged during rebase with geometric local change --- .../backend/src/test/hubaccess/Rebase.test.ts | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/core/backend/src/test/hubaccess/Rebase.test.ts b/core/backend/src/test/hubaccess/Rebase.test.ts index b4db1555dc3..7d7f0830603 100644 --- a/core/backend/src/test/hubaccess/Rebase.test.ts +++ b/core/backend/src/test/hubaccess/Rebase.test.ts @@ -1293,7 +1293,7 @@ describe("rebase changes & stashing api", function (this: Suite) { chai.expect(geomGuidBeforePull).to.not.equal(geomGuidAfterPull); chai.expect(events.modelGeometryChanged.length).to.equal(4); }); - it.only("onModelGeometryChanged() fired during rebase with none-geometric local change", async () => { + it("onModelGeometryChanged() fired during rebase with none-geometric local change", async () => { const b1 = await testIModel.openBriefcase(); const b2 = await testIModel.openBriefcase(); @@ -1344,5 +1344,54 @@ describe("rebase changes & stashing api", function (this: Suite) { chai.expect(geomGuidAfterPull).is.exist; chai.expect(events.modelGeometryChanged.length).to.equal(1); }); + it.only("onModelGeometryChanged() fired during rebase with geometric local change", async () => { + const b1 = await testIModel.openBriefcase(); + const b2 = await testIModel.openBriefcase(); + + const pushChangeFromB2 = async () => { + await b2.pullChanges(); + await testIModel.insertRecipe2d(b2) + b2.saveChanges(); + await b2.pushChanges({ description: "insert element on b2" }); + }; + + const events = { + modelGeometryChanged: [] as ReadonlyArray[], + }; + + const getGeometryGuidFromB1 = (modelId: string) => { + const modelProps = b1.models.tryGetModelProps(modelId); + return modelProps?.geometryGuid; + }; + + const clearEvents = () => { + events.modelGeometryChanged = []; + }; + + b1.txns.onModelGeometryChanged.addListener((changes: ReadonlyArray) => { + events.modelGeometryChanged.push(changes); + }); + + clearEvents(); + + b1.txns.rebaser.setCustomHandler({ + shouldReinstate: (_txn: TxnProps) => { + return true; + }, + recompute: async (_txn: TxnProps) => { + await testIModel.insertElement(b1); + }, + }); + + await pushChangeFromB2(); + + clearEvents(); + const geomGuidBeforePull = getGeometryGuidFromB1("0x20000000001"); + chai.expect(geomGuidBeforePull).is.undefined; + await b1.pushChanges({ description: "push changes on b1" }); + const geomGuidAfterPull = getGeometryGuidFromB1("0x20000000001"); + chai.expect(geomGuidAfterPull).is.undefined; + chai.expect(events.modelGeometryChanged.length).to.equal(0); + }); }); From 25e3df41a885788e449ae23c15a1b38c2c29d860 Mon Sep 17 00:00:00 2001 From: Affan Khan Date: Tue, 18 Nov 2025 15:23:36 -0500 Subject: [PATCH 04/17] fix --- .github/copilot-instructions.md | 326 ++++++++++++++++++ .../backend/src/test/hubaccess/Rebase.test.ts | 62 +++- 2 files changed, 377 insertions(+), 11 deletions(-) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000000..311d1f602db --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,326 @@ +# iTwin.js Core - AI Coding Agent Instructions + +## Project Overview + +iTwin.js is a TypeScript monorepo for creating, querying, and displaying Infrastructure Digital Twins. The codebase follows a **frontend-backend architecture** with RPC communication, built using [Rush](http://rushjs.io/) for monorepo management. + +### Key Architecture Layers + +- **`core/bentley`**: Foundation utilities (no dependencies on other iTwin packages) +- **`core/geometry`**: Computational geometry (depends only on bentley) +- **`core/common`**: Shared types/interfaces between frontend and backend +- **`core/backend`**: Node.js backend with native library access via `@bentley/imodeljs-native` +- **`core/frontend`**: Browser/Electron frontend with WebGL rendering +- **Domains**: Domain-specific packages (analytical, linear-referencing, physical-material) +- **Presentation**: Data presentation layer with backend/frontend/common split +- **Editor**: Editing tools with backend/frontend/common split + +### Critical Data Flow Pattern + +Frontend ↔️ RPC Interface ↔️ Backend interaction is central: + +```typescript +// Frontend calls backend via RPC +const client = RpcManager.getClientForInterface(SomeRpcInterface); +const result = await client.someMethod(params); + +// Backend implements RPC +export class SomeRpcImpl extends SomeRpcInterface { + public async someMethod(params: Params): Promise { + // Implementation + } +} +``` + +**Key points:** + +- RPC interfaces are defined in `*-common` packages +- Implementations are in `*-backend` packages +- Clients are consumed in `*-frontend` packages +- Always version RPC interfaces (see `@beta` decorators below) + +### iModel Connection Patterns + +Three main connection types with distinct use cases: + +```typescript +// 1. CheckpointConnection - Readonly remote iModel checkpoint +const checkpoint = await CheckpointConnection.openRemote(iTwinId, iModelId); + +// 2. SnapshotConnection - Local read-only snapshot file +const snapshot = await SnapshotConnection.openFile(filePath); + +// 3. BriefcaseConnection - Editable briefcase with change tracking +const briefcase = await BriefcaseConnection.openFile(briefcaseProps); +``` + +**Backend equivalents:** + +- `SnapshotDb.openFile()` / `SnapshotDb.openCheckpoint()` - Readonly operations +- `BriefcaseDb.open()` - Editable with changeset support + +Always close connections: `await connection.close()` + +## Build & Development Workflow + +### Essential Commands + +```bash +# Initial setup (required after git clone or pull) +rush install # Install dependencies (uses pnpm) +rush build # Incremental build of changed packages +rush rebuild # Full rebuild of all packages + +# Development workflow +rush build # Build changed packages only +rush test # Run all tests +rush cover # Run tests with coverage +rush lint # Lint all packages +rush clean # Clean build artifacts + +# Package-specific (faster for isolated work) +cd core/frontend +rushx build # Build only this package +rushx test # Test only this package +rushx cover # Test with coverage +rushx lint # Lint this package +``` + +### Making Changes + +1. Branch naming: `/descriptive-name` (all lowercase, dash-separated) +2. Make changes and build: `rush build` +3. Update API signatures if public APIs changed: `rush extract-api` + - Must run `rush clean && rush build` first for accurate extraction + - Review diffs in `common/api/*.api.md` files +4. Add changelog entry: `rush change` + - Only for published packages and public API changes + - CI will fail if this step is skipped +5. Commit and push + +### Monorepo Structure Details + +- Each package has **independent** `node_modules` with symlinks managed by Rush +- Use `rushx` instead of `npm run` for package scripts +- TypeScript configs extend from `@itwin/build-tools/tsconfig-base.json` +- All packages output to `lib/cjs` (CommonJS) and `lib/esm` (ES Modules) + +## Testing Conventions + +### Test Framework by Package + +- **Mocha**: `core/backend`, `presentation/*`, `full-stack-tests/*` +- **Vitest**: `core/frontend`, `core/bentley`, `core/geometry`, `core/common` + +### Mocha Test Pattern + +```typescript +import { expect } from "chai"; +import * as sinon from "sinon"; + +describe("ComponentName", () => { + let component: ComponentType; + + before(async () => { + // One-time setup for entire suite + await IModelHost.startup(); + }); + + after(async () => { + // One-time cleanup + await IModelHost.shutdown(); + }); + + beforeEach(() => { + // Setup before each test + component = new ComponentType(); + }); + + afterEach(() => { + // Cleanup after each test + sinon.restore(); + }); + + it("should do something", () => { + expect(component.value).to.equal(expected); + }); +}); +``` + +### Vitest Test Pattern + +```typescript +import { afterAll, beforeAll, describe, expect, it } from "vitest"; + +describe("ComponentName", () => { + beforeAll(async () => { + await IModelApp.startup({ localization: new EmptyLocalization() }); + }); + + afterAll(async () => { + await IModelApp.shutdown(); + }); + + it("should do something", () => { + expect(component.value).toBe(expected); + }); +}); +``` + +### Full-Stack Test Pattern + +```typescript +// Backend setup in full-stack-tests/*/src/backend +await IModelHost.startup({ cacheDir }); +RpcManager.registerImpl(SomeRpcInterface, SomeRpcImpl); + +// Frontend setup in full-stack-tests/*/src/frontend +await IModelApp.startup(); +RpcManager.initializeClient({}, [SomeRpcInterface]); +``` + +## API Documentation Standards + +Use JSDoc tags to document API surface: + +- `@public` - Public stable API +- `@beta` - Public but may change before stable +- `@alpha` - Experimental, likely to change +- `@internal` - Internal only, not part of public API +- `@deprecated in X.X` - Marked for removal (include removal timeline) + +Example: + +```typescript +/** + * Opens a checkpoint connection to an iModel. + * @param iTwinId - The iTwin GUID + * @param iModelId - The iModel GUID + * @returns A promise resolving to the connection + * @public + */ +export async function openCheckpoint( + iTwinId: string, + iModelId: string +): Promise; +``` + +**Critical:** Changes to `@public` or `@beta` APIs require running `rush extract-api` and updating changelog with `rush change`. + +## Common Patterns & Conventions + +### Error Handling + +```typescript +import { IModelError, IModelStatus } from "@itwin/core-common"; + +// Throw typed errors +throw new IModelError(IModelStatus.NotFound, "Element not found"); + +// Check for specific errors +if ( + error instanceof IModelError && + error.errorNumber === IModelStatus.NotFound +) { + // Handle not found +} +``` + +### Async Patterns + +Prefer async/await over promises. Most APIs are async: + +```typescript +// Good +const element = await iModel.elements.getElement(id); + +// Avoid +iModel.elements.getElement(id).then(element => { ... }); +``` + +### Logging + +```typescript +import { Logger, LogLevel } from "@itwin/core-bentley"; + +Logger.logError("MyCategory", "Error message", () => ({ detail: value })); +Logger.logWarning("MyCategory", "Warning", () => metadata); +Logger.logInfo("MyCategory", "Info", () => metadata); +Logger.logTrace("MyCategory", "Trace", () => metadata); +``` + +### Working with IDs + +Use `Id64String` type for element IDs: + +```typescript +import { Id64, Id64String } from "@itwin/core-bentley"; + +const id: Id64String = "0x123"; +if (Id64.isValidId64(id)) { + // Valid ID +} +``` + +## Important Files & Locations + +- `rush.json` - Monorepo configuration and package list +- `common/api/*.api.md` - Generated API signatures (don't edit manually) +- `common/changes/@itwin/*.json` - Changelog entries (generated by `rush change`) +- `.vscode/launch.json` - Debug configurations for VSCode +- `CONTRIBUTING.md` - Detailed contribution guidelines +- Package-specific: + - `package.json` - Dependencies and scripts + - `tsconfig.json` - TypeScript configuration + - `vitest.config.mts` or `.mocharc.json` - Test configuration + +## RPC Interface Design Best Practices + +1. **Version each interface** - Use `static interfaceVersion = "X.Y.Z"` +2. **Chunky not chatty** - Minimize round trips, batch operations +3. **Use paging for large results** - Always paginate with `limit`/`offset` +4. **Stateless operations** - No server-side state between requests +5. **Include authorization** - Every request carries auth credentials + +See: `docs/learning/backend/BestPractices.md` + +## When Editing Existing Code + +- Preserve existing code style and patterns +- Maintain API compatibility for `@public` APIs +- Add `@deprecated` with timeline rather than breaking changes +- Update tests alongside code changes +- Run `rush lint` to ensure style compliance + +## Debugging Tests + +**Mocha tests:** Add `.only` to `describe()` or `it()` to run specific tests: + +```typescript +describe.only("MyComponent", () => { ... }); +it.only("specific test", () => { ... }); +``` + +**Vitest tests:** + +1. Use `.only` modifier like Mocha +2. Or edit `vitest.config.mts` and set `include: ["**/MyFile.test.ts"]` +3. Or use Vitest Explorer VSCode extension + +VSCode launch configurations in `.vscode/launch.json` attach debuggers for each package. + +## Key Dependencies + +- `@bentley/imodeljs-native` - Native C++ bindings (backend only) +- `@itwin/core-bentley` - Foundation utilities +- `@itwin/core-geometry` - Geometry library +- RPC communication over HTTP for web, IPC for Electron/mobile + +## Notes for AI Agents + +- This is a **mature, production codebase** - breaking changes require careful consideration +- API surface is carefully versioned - respect `@public`, `@beta`, `@alpha` tags +- Tests are required for all changes to core functionality +- Backend and frontend code **never** directly import each other - use RPC/IPC interfaces +- Rush is non-negotiable - never use `npm install` directly (it will break) +- When in doubt about patterns, search for similar code: `grep -r "pattern" core/*/src` diff --git a/core/backend/src/test/hubaccess/Rebase.test.ts b/core/backend/src/test/hubaccess/Rebase.test.ts index 7d7f0830603..889971e3737 100644 --- a/core/backend/src/test/hubaccess/Rebase.test.ts +++ b/core/backend/src/test/hubaccess/Rebase.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Guid, Id64Array, Id64String } from "@itwin/core-bentley"; -import { Code, GeometricElement2dProps, GeometricModelProps, IModel, ModelIdAndGeometryGuid, QueryBinder, RelatedElementProps, SubCategoryAppearance } from "@itwin/core-common"; +import { Code, GeometricElement2dProps, GeometricModelProps, GeometryStreamBuilder, IModel, ModelGeometryChangesProps, ModelIdAndGeometryGuid, QueryBinder, RelatedElementProps, SubCategoryAppearance } from "@itwin/core-common"; import * as chai from "chai"; import * as chaiAsPromised from "chai-as-promised"; import { Suite } from "mocha"; @@ -14,6 +14,7 @@ import { HubMock } from "../../internal/HubMock"; import { StashManager } from "../../StashManager"; import { existsSync, unlinkSync, writeFileSync } from "fs"; import * as path from "path"; +import { LineSegment3d, Point3d } from "@itwin/core-geometry"; chai.use(chaiAsPromised); class TestIModel { @@ -105,16 +106,25 @@ class TestIModel { } public async insertElement(b: BriefcaseDb, markAsIndirect?: true) { await b.locks.acquireLocks({ shared: [this.drawingModelId] }); + const builder = new GeometryStreamBuilder(); + const p1 = Point3d.createZero(); + const p2 = Point3d.createFrom({ x: Math.random() * 10.0 + 5.0, y: 0.0, z: 0.0 }); + const circle = LineSegment3d.create(p1, p2); + builder.appendGeometry(circle); + const baseProps = { classFullName: "TestDomain:a1", model: this.drawingModelId, category: this.drawingCategoryId, code: Code.createEmpty(), - }; + geom: builder.geometryStream, + prop1: `${this._data++}`, + } as GeometricElement2dProps & { prop1: string }; + let id: Id64String = ""; if (markAsIndirect) { b.txns.withIndirectTxnMode(() => { - id = b.elements.insertElement({ ...baseProps, prop1: `${this._data++}` } as any); + id = b.elements.insertElement(baseProps); }); return id; } @@ -122,6 +132,11 @@ class TestIModel { } public async insertElementEx(b: BriefcaseDb, args?: { prop1?: string, markAsIndirect?: true, parent?: RelatedElementProps }) { await b.locks.acquireLocks({ shared: [this.drawingModelId] }); + const builder = new GeometryStreamBuilder(); + const p1 = Point3d.createZero(); + const p2 = Point3d.createFrom({ x: Math.random() * 10.0 + 5.0, y: 0.0, z: 0.0 }); + const circle = LineSegment3d.create(p1, p2); + builder.appendGeometry(circle); const props: GeometricElement2dProps & { prop1: string } = { classFullName: "TestDomain:a1", @@ -129,6 +144,7 @@ class TestIModel { category: this.drawingCategoryId, code: Code.createEmpty(), parent: args?.parent, + geom: builder.geometryStream, prop1: args?.prop1 ?? `${this._data++}` }; @@ -141,10 +157,18 @@ class TestIModel { } return b.elements.insertElement(props as any); } - public async updateElement(b: BriefcaseDb, id: Id64String, markAsIndirect?: true) { + public async updateElement(b: BriefcaseDb, id: Id64String, markAsIndirect?: true, updateGeom?: boolean) { await b.locks.acquireLocks({ shared: [this.drawingModelId], exclusive: [id] }); - const elProps = b.elements.getElementProps(id); - + const elProps = b.elements.getElementProps(id); + + if (updateGeom) { + const builder = new GeometryStreamBuilder(); + const p1 = Point3d.createZero(); + const p2 = Point3d.createFrom({ x: Math.random() * 10.0 + 10.0, y: 0.0, z: 0.0 }); + const circle = LineSegment3d.create(p1, p2); + builder.appendGeometry(circle); + elProps.geom = builder.geometryStream; + } if (markAsIndirect) { b.txns.withIndirectTxnMode(() => { b.elements.updateElement({ ...elProps, prop1: `${this._data++}` } as any); @@ -169,7 +193,7 @@ class TestIModel { } } -describe("rebase changes & stashing api", function (this: Suite) { +describe.only("rebase changes & stashing api", function (this: Suite) { let testIModel: TestIModel; before(async () => { if (!IModelHost.isValid) @@ -811,12 +835,12 @@ describe("rebase changes & stashing api", function (this: Suite) { chai.expect(b2.changeset.index).to.equals(4); const elBefore = b2.elements.tryGetElementProps(e1); - chai.expect((elBefore as any).prop1).to.equals("2"); + chai.expect((elBefore as any).prop1).to.equals("3"); // restore stash should succeed as now it can obtain lock await StashManager.restore({ db: b2, stash: b2Stash1 }); const elAfter = b2.elements.tryGetElementProps(e1); - chai.expect((elAfter as any).prop1).to.equals("1"); + chai.expect((elAfter as any).prop1).to.equals("2"); await b2.pushChanges({ description: `${e1} updated` }); }); it("schema change should not be stashed", async () => { @@ -829,6 +853,10 @@ describe("rebase changes & stashing api", function (this: Suite) { + + bis:TemplateRecipe2d + + bis:ElementOwnsChildElements @@ -1248,6 +1276,7 @@ describe("rebase changes & stashing api", function (this: Suite) { const events = { modelGeometryChanged: [] as ReadonlyArray[], + onGeometryChanged: [] as ModelGeometryChangesProps[][], }; const getGeometryGuidFromB1 = (modelId: string) => { @@ -1257,11 +1286,15 @@ describe("rebase changes & stashing api", function (this: Suite) { const clearEvents = () => { events.modelGeometryChanged = []; + events.onGeometryChanged = []; }; b1.txns.onModelGeometryChanged.addListener((changes: ReadonlyArray) => { events.modelGeometryChanged.push(changes); }); + b1.txns.onGeometryChanged.addListener((changes: ModelGeometryChangesProps[]) => { + events.onGeometryChanged.push(changes); + }); clearEvents(); const e1 = await testIModel.insertElement(b1); @@ -1280,7 +1313,8 @@ describe("rebase changes & stashing api", function (this: Suite) { return true; }, recompute: async (_txn: TxnProps) => { - await testIModel.insertElement(b1); + await testIModel.updateElement(b1, e1); + await testIModel.updateElement(b1, e2); }, }); @@ -1344,7 +1378,7 @@ describe("rebase changes & stashing api", function (this: Suite) { chai.expect(geomGuidAfterPull).is.exist; chai.expect(events.modelGeometryChanged.length).to.equal(1); }); - it.only("onModelGeometryChanged() fired during rebase with geometric local change", async () => { + it("onModelGeometryChanged() fired during rebase with geometric local change", async () => { const b1 = await testIModel.openBriefcase(); const b2 = await testIModel.openBriefcase(); @@ -1357,6 +1391,7 @@ describe("rebase changes & stashing api", function (this: Suite) { const events = { modelGeometryChanged: [] as ReadonlyArray[], + onGeometryChanged: [] as ModelGeometryChangesProps[][], }; const getGeometryGuidFromB1 = (modelId: string) => { @@ -1366,12 +1401,17 @@ describe("rebase changes & stashing api", function (this: Suite) { const clearEvents = () => { events.modelGeometryChanged = []; + events.onGeometryChanged = []; }; b1.txns.onModelGeometryChanged.addListener((changes: ReadonlyArray) => { events.modelGeometryChanged.push(changes); }); + b1.txns.onGeometryChanged.addListener((changes: ModelGeometryChangesProps[]) => { + events.onGeometryChanged.push(changes); + }); + clearEvents(); b1.txns.rebaser.setCustomHandler({ From d7a1472fd1385a26b3ac8e9891810cc7ad1310eb Mon Sep 17 00:00:00 2001 From: Affan Khan Date: Thu, 20 Nov 2025 11:12:45 -0500 Subject: [PATCH 05/17] Remove 'only' from describe block for rebase changes & stashing API tests --- core/backend/src/test/hubaccess/Rebase.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/backend/src/test/hubaccess/Rebase.test.ts b/core/backend/src/test/hubaccess/Rebase.test.ts index 08e115fc080..2e326e1beeb 100644 --- a/core/backend/src/test/hubaccess/Rebase.test.ts +++ b/core/backend/src/test/hubaccess/Rebase.test.ts @@ -193,7 +193,7 @@ class TestIModel { } } -describe.only("rebase changes & stashing api", function (this: Suite) { +describe("rebase changes & stashing api", function (this: Suite) { let testIModel: TestIModel; before(async () => { if (!IModelHost.isValid) From 4504647cffd0a2734a9aa087c5f71110e32183a7 Mon Sep 17 00:00:00 2001 From: Affan Khan Date: Thu, 20 Nov 2025 14:12:38 -0500 Subject: [PATCH 06/17] rush change --- ...fank-rebase-geometric-changes_2025-11-20-19-12.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 common/changes/@itwin/core-backend/affank-rebase-geometric-changes_2025-11-20-19-12.json diff --git a/common/changes/@itwin/core-backend/affank-rebase-geometric-changes_2025-11-20-19-12.json b/common/changes/@itwin/core-backend/affank-rebase-geometric-changes_2025-11-20-19-12.json new file mode 100644 index 00000000000..e2b4db59f43 --- /dev/null +++ b/common/changes/@itwin/core-backend/affank-rebase-geometric-changes_2025-11-20-19-12.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/core-backend", + "comment": "Add unit test to ensure onModelGeometryChanged() is called during rebase", + "type": "none" + } + ], + "packageName": "@itwin/core-backend" +} \ No newline at end of file From 06fc4d8c5ce80bc46b964aa6f4759a44af5d46fe Mon Sep 17 00:00:00 2001 From: affank Date: Thu, 20 Nov 2025 14:13:08 -0500 Subject: [PATCH 07/17] Update core/backend/src/test/hubaccess/Rebase.test.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- core/backend/src/test/hubaccess/Rebase.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/backend/src/test/hubaccess/Rebase.test.ts b/core/backend/src/test/hubaccess/Rebase.test.ts index 2e326e1beeb..3a3eeacd316 100644 --- a/core/backend/src/test/hubaccess/Rebase.test.ts +++ b/core/backend/src/test/hubaccess/Rebase.test.ts @@ -1219,7 +1219,7 @@ describe("rebase changes & stashing api", function (this: Suite) { const pushChangeFromB2 = async () => { await b2.pullChanges(); - await testIModel.insertElement(b2) + await testIModel.insertElement(b2); b2.saveChanges(); await b2.pushChanges({ description: "insert element on b2" }); }; From 291a23ac77d6d6763e6a76996b274b17c3433fcf Mon Sep 17 00:00:00 2001 From: affank Date: Thu, 20 Nov 2025 14:13:59 -0500 Subject: [PATCH 08/17] Update core/backend/src/test/hubaccess/Rebase.test.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- core/backend/src/test/hubaccess/Rebase.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/core/backend/src/test/hubaccess/Rebase.test.ts b/core/backend/src/test/hubaccess/Rebase.test.ts index 3a3eeacd316..c36469f41ef 100644 --- a/core/backend/src/test/hubaccess/Rebase.test.ts +++ b/core/backend/src/test/hubaccess/Rebase.test.ts @@ -1263,7 +1263,6 @@ describe("rebase changes & stashing api", function (this: Suite) { chai.expect(events.modelGeometryChanged.length).to.equal(0); }); it("onModelGeometryChanged() fired during rebase with geometric local change", async () => { - // Test implementation here const b1 = await testIModel.openBriefcase(); const b2 = await testIModel.openBriefcase(); From 96e6c932a37c38b97a991c6104670fab8df5d551 Mon Sep 17 00:00:00 2001 From: affank Date: Thu, 20 Nov 2025 14:14:11 -0500 Subject: [PATCH 09/17] Update core/backend/src/test/hubaccess/Rebase.test.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- core/backend/src/test/hubaccess/Rebase.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/backend/src/test/hubaccess/Rebase.test.ts b/core/backend/src/test/hubaccess/Rebase.test.ts index c36469f41ef..53fbbf2d1c6 100644 --- a/core/backend/src/test/hubaccess/Rebase.test.ts +++ b/core/backend/src/test/hubaccess/Rebase.test.ts @@ -1332,7 +1332,7 @@ describe("rebase changes & stashing api", function (this: Suite) { const pushChangeFromB2 = async () => { await b2.pullChanges(); - await testIModel.insertElement(b2) + await testIModel.insertElement(b2); b2.saveChanges(); await b2.pushChanges({ description: "insert element on b2" }); }; From 57fe01291252b3a5f638bb4968d8cd038dd4f665 Mon Sep 17 00:00:00 2001 From: affank Date: Thu, 20 Nov 2025 14:14:21 -0500 Subject: [PATCH 10/17] Update core/backend/src/test/hubaccess/Rebase.test.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- core/backend/src/test/hubaccess/Rebase.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/backend/src/test/hubaccess/Rebase.test.ts b/core/backend/src/test/hubaccess/Rebase.test.ts index 53fbbf2d1c6..f74a21dcbb9 100644 --- a/core/backend/src/test/hubaccess/Rebase.test.ts +++ b/core/backend/src/test/hubaccess/Rebase.test.ts @@ -1268,7 +1268,7 @@ describe("rebase changes & stashing api", function (this: Suite) { const pushChangeFromB2 = async () => { await b2.pullChanges(); - await testIModel.insertElement(b2) + await testIModel.insertElement(b2); b2.saveChanges(); await b2.pushChanges({ description: "insert element on b2" }); }; From b470b385e8f6b59be0955f974acf6736ffd58823 Mon Sep 17 00:00:00 2001 From: affank Date: Thu, 20 Nov 2025 14:14:31 -0500 Subject: [PATCH 11/17] Update core/backend/src/test/hubaccess/Rebase.test.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- core/backend/src/test/hubaccess/Rebase.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/backend/src/test/hubaccess/Rebase.test.ts b/core/backend/src/test/hubaccess/Rebase.test.ts index f74a21dcbb9..308f28813d5 100644 --- a/core/backend/src/test/hubaccess/Rebase.test.ts +++ b/core/backend/src/test/hubaccess/Rebase.test.ts @@ -1383,7 +1383,7 @@ describe("rebase changes & stashing api", function (this: Suite) { const pushChangeFromB2 = async () => { await b2.pullChanges(); - await testIModel.insertRecipe2d(b2) + await testIModel.insertRecipe2d(b2); b2.saveChanges(); await b2.pushChanges({ description: "insert element on b2" }); }; From 0cc3cab9f49d81b1780417eb4d7519f0517d4315 Mon Sep 17 00:00:00 2001 From: affank Date: Thu, 20 Nov 2025 14:14:40 -0500 Subject: [PATCH 12/17] Update core/backend/src/test/hubaccess/Rebase.test.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- core/backend/src/test/hubaccess/Rebase.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/backend/src/test/hubaccess/Rebase.test.ts b/core/backend/src/test/hubaccess/Rebase.test.ts index 308f28813d5..b0d4fc35a78 100644 --- a/core/backend/src/test/hubaccess/Rebase.test.ts +++ b/core/backend/src/test/hubaccess/Rebase.test.ts @@ -1374,7 +1374,7 @@ describe("rebase changes & stashing api", function (this: Suite) { chai.expect(geomGuidBeforePull).is.undefined; await b1.pushChanges({ description: "push changes on b1" }); const geomGuidAfterPull = getGeometryGuidFromB1("0x20000000001"); - chai.expect(geomGuidAfterPull).is.exist; + chai.expect(geomGuidAfterPull).to.exist; chai.expect(events.modelGeometryChanged.length).to.equal(1); }); it("onModelGeometryChanged() fired during rebase with geometric local change", async () => { From 9bc31fecb8df0f5499f938e67c67826e102b2c56 Mon Sep 17 00:00:00 2001 From: affank Date: Thu, 20 Nov 2025 14:14:52 -0500 Subject: [PATCH 13/17] Update core/backend/src/test/hubaccess/Rebase.test.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- core/backend/src/test/hubaccess/Rebase.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/backend/src/test/hubaccess/Rebase.test.ts b/core/backend/src/test/hubaccess/Rebase.test.ts index b0d4fc35a78..b25b8f01aa5 100644 --- a/core/backend/src/test/hubaccess/Rebase.test.ts +++ b/core/backend/src/test/hubaccess/Rebase.test.ts @@ -1326,7 +1326,7 @@ describe("rebase changes & stashing api", function (this: Suite) { chai.expect(geomGuidBeforePull).to.not.equal(geomGuidAfterPull); chai.expect(events.modelGeometryChanged.length).to.equal(4); }); - it("onModelGeometryChanged() fired during rebase with none-geometric local change", async () => { + it("onModelGeometryChanged() fired during rebase with non-geometric local change", async () => { const b1 = await testIModel.openBriefcase(); const b2 = await testIModel.openBriefcase(); From 92f65526ec704cbadfe7dc7aa2292f196aa7d736 Mon Sep 17 00:00:00 2001 From: affank Date: Thu, 20 Nov 2025 14:14:58 -0500 Subject: [PATCH 14/17] Update core/backend/src/test/hubaccess/Rebase.test.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- core/backend/src/test/hubaccess/Rebase.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/core/backend/src/test/hubaccess/Rebase.test.ts b/core/backend/src/test/hubaccess/Rebase.test.ts index b25b8f01aa5..20677ce607d 100644 --- a/core/backend/src/test/hubaccess/Rebase.test.ts +++ b/core/backend/src/test/hubaccess/Rebase.test.ts @@ -1248,7 +1248,6 @@ describe("rebase changes & stashing api", function (this: Suite) { return true; }, recompute: async (_txn: TxnProps) => { - // await testIModel.insertElement(b1); }, }); From 5b4c5ba5594f2815f835256d5bcbf2e435e885b7 Mon Sep 17 00:00:00 2001 From: affank Date: Thu, 20 Nov 2025 14:15:07 -0500 Subject: [PATCH 15/17] Update core/backend/src/test/hubaccess/Rebase.test.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- core/backend/src/test/hubaccess/Rebase.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/core/backend/src/test/hubaccess/Rebase.test.ts b/core/backend/src/test/hubaccess/Rebase.test.ts index 20677ce607d..6c2d47acc92 100644 --- a/core/backend/src/test/hubaccess/Rebase.test.ts +++ b/core/backend/src/test/hubaccess/Rebase.test.ts @@ -1360,7 +1360,6 @@ describe("rebase changes & stashing api", function (this: Suite) { return true; }, recompute: async (_txn: TxnProps) => { - // await testIModel.insertElement(b1); }, }); From 8b484a481cb1856ca624ea2fb07f36b22c8ab142 Mon Sep 17 00:00:00 2001 From: Affan Khan Date: Thu, 20 Nov 2025 14:33:59 -0500 Subject: [PATCH 16/17] Remove AI coding agent instructions document --- .github/copilot-instructions.md | 326 -------------------------------- 1 file changed, 326 deletions(-) delete mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index 311d1f602db..00000000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,326 +0,0 @@ -# iTwin.js Core - AI Coding Agent Instructions - -## Project Overview - -iTwin.js is a TypeScript monorepo for creating, querying, and displaying Infrastructure Digital Twins. The codebase follows a **frontend-backend architecture** with RPC communication, built using [Rush](http://rushjs.io/) for monorepo management. - -### Key Architecture Layers - -- **`core/bentley`**: Foundation utilities (no dependencies on other iTwin packages) -- **`core/geometry`**: Computational geometry (depends only on bentley) -- **`core/common`**: Shared types/interfaces between frontend and backend -- **`core/backend`**: Node.js backend with native library access via `@bentley/imodeljs-native` -- **`core/frontend`**: Browser/Electron frontend with WebGL rendering -- **Domains**: Domain-specific packages (analytical, linear-referencing, physical-material) -- **Presentation**: Data presentation layer with backend/frontend/common split -- **Editor**: Editing tools with backend/frontend/common split - -### Critical Data Flow Pattern - -Frontend ↔️ RPC Interface ↔️ Backend interaction is central: - -```typescript -// Frontend calls backend via RPC -const client = RpcManager.getClientForInterface(SomeRpcInterface); -const result = await client.someMethod(params); - -// Backend implements RPC -export class SomeRpcImpl extends SomeRpcInterface { - public async someMethod(params: Params): Promise { - // Implementation - } -} -``` - -**Key points:** - -- RPC interfaces are defined in `*-common` packages -- Implementations are in `*-backend` packages -- Clients are consumed in `*-frontend` packages -- Always version RPC interfaces (see `@beta` decorators below) - -### iModel Connection Patterns - -Three main connection types with distinct use cases: - -```typescript -// 1. CheckpointConnection - Readonly remote iModel checkpoint -const checkpoint = await CheckpointConnection.openRemote(iTwinId, iModelId); - -// 2. SnapshotConnection - Local read-only snapshot file -const snapshot = await SnapshotConnection.openFile(filePath); - -// 3. BriefcaseConnection - Editable briefcase with change tracking -const briefcase = await BriefcaseConnection.openFile(briefcaseProps); -``` - -**Backend equivalents:** - -- `SnapshotDb.openFile()` / `SnapshotDb.openCheckpoint()` - Readonly operations -- `BriefcaseDb.open()` - Editable with changeset support - -Always close connections: `await connection.close()` - -## Build & Development Workflow - -### Essential Commands - -```bash -# Initial setup (required after git clone or pull) -rush install # Install dependencies (uses pnpm) -rush build # Incremental build of changed packages -rush rebuild # Full rebuild of all packages - -# Development workflow -rush build # Build changed packages only -rush test # Run all tests -rush cover # Run tests with coverage -rush lint # Lint all packages -rush clean # Clean build artifacts - -# Package-specific (faster for isolated work) -cd core/frontend -rushx build # Build only this package -rushx test # Test only this package -rushx cover # Test with coverage -rushx lint # Lint this package -``` - -### Making Changes - -1. Branch naming: `/descriptive-name` (all lowercase, dash-separated) -2. Make changes and build: `rush build` -3. Update API signatures if public APIs changed: `rush extract-api` - - Must run `rush clean && rush build` first for accurate extraction - - Review diffs in `common/api/*.api.md` files -4. Add changelog entry: `rush change` - - Only for published packages and public API changes - - CI will fail if this step is skipped -5. Commit and push - -### Monorepo Structure Details - -- Each package has **independent** `node_modules` with symlinks managed by Rush -- Use `rushx` instead of `npm run` for package scripts -- TypeScript configs extend from `@itwin/build-tools/tsconfig-base.json` -- All packages output to `lib/cjs` (CommonJS) and `lib/esm` (ES Modules) - -## Testing Conventions - -### Test Framework by Package - -- **Mocha**: `core/backend`, `presentation/*`, `full-stack-tests/*` -- **Vitest**: `core/frontend`, `core/bentley`, `core/geometry`, `core/common` - -### Mocha Test Pattern - -```typescript -import { expect } from "chai"; -import * as sinon from "sinon"; - -describe("ComponentName", () => { - let component: ComponentType; - - before(async () => { - // One-time setup for entire suite - await IModelHost.startup(); - }); - - after(async () => { - // One-time cleanup - await IModelHost.shutdown(); - }); - - beforeEach(() => { - // Setup before each test - component = new ComponentType(); - }); - - afterEach(() => { - // Cleanup after each test - sinon.restore(); - }); - - it("should do something", () => { - expect(component.value).to.equal(expected); - }); -}); -``` - -### Vitest Test Pattern - -```typescript -import { afterAll, beforeAll, describe, expect, it } from "vitest"; - -describe("ComponentName", () => { - beforeAll(async () => { - await IModelApp.startup({ localization: new EmptyLocalization() }); - }); - - afterAll(async () => { - await IModelApp.shutdown(); - }); - - it("should do something", () => { - expect(component.value).toBe(expected); - }); -}); -``` - -### Full-Stack Test Pattern - -```typescript -// Backend setup in full-stack-tests/*/src/backend -await IModelHost.startup({ cacheDir }); -RpcManager.registerImpl(SomeRpcInterface, SomeRpcImpl); - -// Frontend setup in full-stack-tests/*/src/frontend -await IModelApp.startup(); -RpcManager.initializeClient({}, [SomeRpcInterface]); -``` - -## API Documentation Standards - -Use JSDoc tags to document API surface: - -- `@public` - Public stable API -- `@beta` - Public but may change before stable -- `@alpha` - Experimental, likely to change -- `@internal` - Internal only, not part of public API -- `@deprecated in X.X` - Marked for removal (include removal timeline) - -Example: - -```typescript -/** - * Opens a checkpoint connection to an iModel. - * @param iTwinId - The iTwin GUID - * @param iModelId - The iModel GUID - * @returns A promise resolving to the connection - * @public - */ -export async function openCheckpoint( - iTwinId: string, - iModelId: string -): Promise; -``` - -**Critical:** Changes to `@public` or `@beta` APIs require running `rush extract-api` and updating changelog with `rush change`. - -## Common Patterns & Conventions - -### Error Handling - -```typescript -import { IModelError, IModelStatus } from "@itwin/core-common"; - -// Throw typed errors -throw new IModelError(IModelStatus.NotFound, "Element not found"); - -// Check for specific errors -if ( - error instanceof IModelError && - error.errorNumber === IModelStatus.NotFound -) { - // Handle not found -} -``` - -### Async Patterns - -Prefer async/await over promises. Most APIs are async: - -```typescript -// Good -const element = await iModel.elements.getElement(id); - -// Avoid -iModel.elements.getElement(id).then(element => { ... }); -``` - -### Logging - -```typescript -import { Logger, LogLevel } from "@itwin/core-bentley"; - -Logger.logError("MyCategory", "Error message", () => ({ detail: value })); -Logger.logWarning("MyCategory", "Warning", () => metadata); -Logger.logInfo("MyCategory", "Info", () => metadata); -Logger.logTrace("MyCategory", "Trace", () => metadata); -``` - -### Working with IDs - -Use `Id64String` type for element IDs: - -```typescript -import { Id64, Id64String } from "@itwin/core-bentley"; - -const id: Id64String = "0x123"; -if (Id64.isValidId64(id)) { - // Valid ID -} -``` - -## Important Files & Locations - -- `rush.json` - Monorepo configuration and package list -- `common/api/*.api.md` - Generated API signatures (don't edit manually) -- `common/changes/@itwin/*.json` - Changelog entries (generated by `rush change`) -- `.vscode/launch.json` - Debug configurations for VSCode -- `CONTRIBUTING.md` - Detailed contribution guidelines -- Package-specific: - - `package.json` - Dependencies and scripts - - `tsconfig.json` - TypeScript configuration - - `vitest.config.mts` or `.mocharc.json` - Test configuration - -## RPC Interface Design Best Practices - -1. **Version each interface** - Use `static interfaceVersion = "X.Y.Z"` -2. **Chunky not chatty** - Minimize round trips, batch operations -3. **Use paging for large results** - Always paginate with `limit`/`offset` -4. **Stateless operations** - No server-side state between requests -5. **Include authorization** - Every request carries auth credentials - -See: `docs/learning/backend/BestPractices.md` - -## When Editing Existing Code - -- Preserve existing code style and patterns -- Maintain API compatibility for `@public` APIs -- Add `@deprecated` with timeline rather than breaking changes -- Update tests alongside code changes -- Run `rush lint` to ensure style compliance - -## Debugging Tests - -**Mocha tests:** Add `.only` to `describe()` or `it()` to run specific tests: - -```typescript -describe.only("MyComponent", () => { ... }); -it.only("specific test", () => { ... }); -``` - -**Vitest tests:** - -1. Use `.only` modifier like Mocha -2. Or edit `vitest.config.mts` and set `include: ["**/MyFile.test.ts"]` -3. Or use Vitest Explorer VSCode extension - -VSCode launch configurations in `.vscode/launch.json` attach debuggers for each package. - -## Key Dependencies - -- `@bentley/imodeljs-native` - Native C++ bindings (backend only) -- `@itwin/core-bentley` - Foundation utilities -- `@itwin/core-geometry` - Geometry library -- RPC communication over HTTP for web, IPC for Electron/mobile - -## Notes for AI Agents - -- This is a **mature, production codebase** - breaking changes require careful consideration -- API surface is carefully versioned - respect `@public`, `@beta`, `@alpha` tags -- Tests are required for all changes to core functionality -- Backend and frontend code **never** directly import each other - use RPC/IPC interfaces -- Rush is non-negotiable - never use `npm install` directly (it will break) -- When in doubt about patterns, search for similar code: `grep -r "pattern" core/*/src` From 64f6eb8c7546038264262be7ccbb39f15b770d45 Mon Sep 17 00:00:00 2001 From: Arun George <11051042+aruniverse@users.noreply.github.com> Date: Thu, 20 Nov 2025 14:44:30 -0500 Subject: [PATCH 17/17] Apply suggestion from @aruniverse --- .../affank-rebase-geometric-changes_2025-11-20-19-12.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/changes/@itwin/core-backend/affank-rebase-geometric-changes_2025-11-20-19-12.json b/common/changes/@itwin/core-backend/affank-rebase-geometric-changes_2025-11-20-19-12.json index e2b4db59f43..99b35bb89b6 100644 --- a/common/changes/@itwin/core-backend/affank-rebase-geometric-changes_2025-11-20-19-12.json +++ b/common/changes/@itwin/core-backend/affank-rebase-geometric-changes_2025-11-20-19-12.json @@ -2,7 +2,7 @@ "changes": [ { "packageName": "@itwin/core-backend", - "comment": "Add unit test to ensure onModelGeometryChanged() is called during rebase", + "comment": "", "type": "none" } ],