SuperRepo local preview: 12 gaps hit while building a function-backed write path against an imported ontology

We are building an ingest app on SuperRepo (beta): a user tags the entities a source document mentions (customers, devices, sites — types imported read-only from an Ontology Manager ontology of ~95 types) and one function-backed action commits creates, updates and links across about ten imported types plus three code-born ones (document, provenance row, receipt). Everything below was found on the **local preview**; several questions ask what holds when deployed. All but one gap have a workaround (G11 is an accepted degradation); none blocked us. The underscore link-id failure we posted earlier is fixed in `@osdk/maker` 0.67.0 — thank you. Every name below is a stand-in.

## Environment

- foundry-cli 0.253.0 (local ontology server `lohi-lib` 0.635.0, platform-api-proxy) · macOS 27.0 arm64 · Node 25.9.0 (`.nvmrc` pins 24) · pnpm 11.12.0

- `@osdk/maker` 0.67.0 · `@osdk/maker-experimental` 0.61.0 · `@osdk/maker-import` 0.37.0 · `@osdk/generator-converters.preview` 0.45.0 · `@osdk/generator-converters.ontologyir` 2.67.0 · `@osdk/generator` 2.67.0 · `@osdk/seed-helpers` 0.27.0

- `@osdk/client` / `@osdk/api` 2.57.0 (workspace override; maker 0.67.0 declares `@osdk/api ~2.67.0` but builds against it) · `@osdk/functions` 1.20.0 · `@foundry/functions-typescript-runtime` 0.299.0 · `@foundry/functions-typescript-osdk-discovery` 1.35.0

- `@osdk/vite-plugin-superrepo` 0.10.0 (dev smart client)

- Imports via `foundry import ontology`; new types code-born under `com.example.app.*`.

## Summary

| # | Component (version) | Symptom | Local workaround |

|—|—|—|—|

| G1 | `@osdk/maker-experimental` 0.61.0 `api/defineOntologyV2.js:37` | Link with an imported end: imported side gets a temp RID → `generate-sdk: Unable to find entity` | `void importedType.apiName` at module top level |

| G2 | preview converter + local ontology server 0.635.0 | Once built, a link with an imported end is not served (M2M and 1:N); for M2M, no reverse side in the OSDK and writes get `400` | Code-born join object + property filters |

| G3 | maker-experimental `cli/generateBackingDataset.js:311` | `includeEmptyBackingDatasource` on such a link: `Could not find object types for link` | Omit the flag (leaves an unbound dataset input) |

| G4 | maker-experimental `cli/main.js:126` | Block-data function discovery sees no imported types → `unsupportedType` → function-backed action cannot register | Code-born-only declared return type + cast |

| G5 | functions runtime 0.299.0 `PropertyTypeConverter` ↔ local `metadata.json` | Array and geopoint edits fail on every type | Omit locally behind a positive preview check; report the omission |

| G9 | functions runtime ↔ local ontology server | Timestamp edits rejected (`Ontologies:InvalidPropertyValue`) | Same guard, by property name |

| G6 | local ontology server | Non-atomic apply; duplicate PK → `500 Default:Internal` | Validation-first commit; receipt is the last edit |

| G10 | local ontology server | A failed persist can answer `200` with zero edits | App confirms success by reading the receipt back |

| G7 | `@osdk/vite-plugin-superrepo` 0.10.0 `smartClient.transformResponseFromLocal` | Query `UserFacingError` text lost; struct results still wire-wrapped | Errors as data; JSON-string returns |

| G8 | local ontology server | Same-type M2M links stored side-swapped; intermediary links are plain tables | Avoid directional self-links; write relationship rows |

| G11 | functions runtime / `@osdk/functions` 1.20.0 | No calling-user identity reaches a function locally (live route not tested) | None (no user attribution on the function’s writes) |

| G12 | platform-api-proxy + local ontology server | Media sets cannot be written locally (`501` / `404` / `400`); attachments work | Attachment property |

## 1. Links with an imported end (G1–G3)

```ts

import { customer } from “#generated-imports”;

defineLink({ apiName: “documentCustomer”,

many: { object: IngestDocument, metadata: { apiName: “customers” } },

toMany: { object: customer, metadata: { apiName: “documents” } },

editsEnabled: true, includeEmptyBackingDatasource: true });

```

### G1 — imported link ends get temporary RIDs

**Observed:** `generate-sdk … ERROR Unable to find entity`; the IR names the imported side `ri.ontology-metadata.temp.object-type.`. `OntologyRidGeneratorImpl` snapshots `getImportedTypes()` at construction (`defineOntologyV2.js:37`); import stubs are lazy proxies registered on first property access, which `convertLink` triggers too late.

**Expected:** the stub resolves to its `ridHint`.

**Workaround:** `void customer.apiName;` at module top level.

**Question:** is eager stub registration or a lazy RID lookup an acceptable fix, and is the top-level `void` safe in deployed block data (it seems only to change registration order)?

### G2 — the built link is not served locally

**Repro:** G1 workaround applied; start the preview.

**Observed:**

```

GET objectTypes/com.example.app.ingestDocument/outgoingLinkTypes → 200 {“data”:[]}

GET objectTypes/customer/outgoingLinkTypes/documents → 404 Ontologies:LinkTypeNotFound

POST actions/…/apply (batch.link(doc, “customers”, customer))

→ 400 {“errorName”:“Default:InvalidArgument”,“parameters”:{“linkTypeRid”:“”,“objectType”:“com.example.app.ingestDocument”}}

```

A 1:N with an imported `one` side builds but is likewise not served (`404 LinkTypeNotFound` on the imported side; write not attempted). Imported↔imported and code-born↔code-born links work.

**Expected:** both sides served, `batch.link` accepted.

**Workaround:** a code-born join object `documentLink { id, documentId, targetType, targetPk }`, linked 1:N to `ingestDocument`, queried by `where({ targetType, targetPk })` plus a pivot.

**Question:** are links with an imported end meant to work locally? When deployed?

### G3 — `includeEmptyBackingDatasource` on that link fails the build

**Observed:** `Error: Could not find object types for link “documentCustomer”` — `generateBackingDataset.js:311` searches only code-born `ontologyIr.ontology.objectTypes`.

**Expected:** the flag binds the link’s datasource as for code-born object types.

**Workaround:** omit it; the datasource becomes an unbound block input (`inputs[“dataset-datasource-link.documentCustomer”]`) an install must map.

**Question:** should the flag be legal here, and what does the unbound input do at install?

## 2. Function discovery ignores imports (G4)

**Repro:** `commitIngest` declares its honest edit union (`Edits.Object | Edits.Link<customer, “device_owner_2”> | Edits.Object`) and a `defineFunctionBackedAction` names it.

**Observed:** `{“code”:“unsupportedType”,“message”:“\“Edits.Object\” is not a supported type.”,“severity”:“error”}`, then `ERROR Rebuild failed: Invariant failed: Function “commitIngest” not found in functions IR`; the whole ontology build fails and the previous one keeps serving. `cli/main.js:126` calls `getPreviewFullMetadataFromBlockData(blockDataJson)` without the import JSON; the SDK-stage pass, which gets imports, accepts the same function.

**Expected:** both passes see the same entities.

**Workaround:** declare code-born edits only and cast:

```ts

type Declared = Edits.Object; // batch is still createEditBatch(client)

return batch.getEdits() as unknown as Declared[];

```

The spec then lists `editedObjects: {“com.example.app.ingestReceipt”: {}}` only, yet the local server applies the `customer` edits — provenance is not enforced locally.

**Question:** does deployed Actions reject edits outside the declared provenance? If so this cast is a deploy-time trap and the import JSON at `main.js:126` is the real fix.

## 3. Property types the local runtime cannot write (G5, G9)

### G5 — arrays and geopoints, on any type

**Repro:** `batch.create(customer, { …, aliases: [“a”, “b”] })`; `batch.update(site, { location: { type: “Point”, coordinates: [10.0, 20.0] } })`.

**Observed:**

```

aliases → 400 Ontologies:ApplyActionFailed {“kind”:“runtimeError”,“message”:“Cannot read properties of undefined (reading ‘itemType’)”} PropertyTypeConverter.js:176

location → 400 Ontologies:ApplyActionFailed {“kind”:“runtimeError”,“message”:“Unexpected property type for ontology edits”} PropertyTypeConverter.js:250

```

The runtime’s `PropertyTypeConverter` expects OMS shapes (`array.itemType`, `geohash`); the build’s `.dev-server/var/conf/metadata.json` carries API-v2 shapes (`array.subType`, `geopoint`), consumed unconverted. Empty arrays pass (the per-item map callback never runs), so the fault is item-type conversion. Same under preview converter 0.31.0, so not a regression. Arrays and geopoints seed and read back fine, and arrays filter with `$contains`; only function writes fail.

**Expected:** function edits accept what the seed accepts.

**Workaround:** none without patching; our wrapper omits array/geopoint values **only** after positively detecting the preview (`GET /api/v2/ontologies` lists exactly one ontology, `ri.ontology.main.ontology.0`) and reports the omission. Any doubt ⇒ write everything.

**Questions:** does the deployed runtime get OMS-shaped metadata, making this local-only? Is a shim planned? Is there a supported way to detect the preview from inside a function?

### G9 — timestamps

**Repro:** a function edit setting a code-born `timestamp` property from an ISO-8601 string, e.g. `batch.create(ingestReceipt, { …, committedAt: new Date().toISOString() })`. For comparison, the seed validator (`@osdk/seed-helpers` 0.27.0) requires `timestamp` values as RFC 3339.

**Observed:** the runtime emits epoch millis; the local server accepts only ISO-8601 and rejects the edit as `Ontologies:InvalidPropertyValue` (value shown as `U64(…)`).

**Expected:** one agreed wire shape.

**Workaround:** the same guard, by property name.

**Question:** which shape does the deployed runtime/OMS pair use for timestamp edits — epoch millis or ISO-8601 — and will the local server accept what the runtime emits? Is there a supported way to write a timestamp from a local function today?

## 4. Apply semantics on the local server (G6, G10)

### G6 — non-atomic apply; duplicate PK is a 500

**Repro:** one batch creates the `ingestReceipt`, then a `customer` with an existing PK.

**Observed:**

```

POST actions/com.example.app.commit-ingest-action/apply

→ 500 {“errorName”:“Default:Internal”,“parameters”:{“objectType”:“customer”,“primaryKey”:“00000000-0000-4000-8000-000000000001”}}

server log: cause “Cannot add object that already exists in the store”

GET objects/com.example.app.ingestReceipt/ → 200 ← the first edit persisted

```

Edits apply in array order and stop mid-batch without rollback. The template’s notes say a plain create action returns `Ontologies:ObjectAlreadyExists` here; we did not re-verify that, but a function-backed action does not.

**Expected:** all-or-nothing, as documented for deployed actions, and a typed duplicate-PK error.

**Workaround:** all reads and validation (idempotency, natural-key collisions via `$in`) before the first edit; the receipt is the **last** edit, so a partial apply leaves none and a retry re-plans.

**Question:** are deployed function-backed actions editing imported types transactional? Could the local server roll back and return `ObjectAlreadyExists`?

### G10 — HTTP 200 with zero edits after a failed persist

- Observed during development: an apply whose edits did not persist answered `200` with an empty edit list, so locally a 200 is not proof of a write. We have not isolated a minimal repro to share.

**Expected:** a non-2xx status when persistence fails.

**Workaround:** the app treats a commit as successful only when the receipt reads back.

**Question:** is this known? A pointer to a server path that swallows a persist failure, or a condition yielding a 200 with no edits, would help us isolate a repro.

## 5. Error plumbing in the dev smart client (G7)

**Repro:** `throw new UserFacingError(“…”)` in a query called via `useOsdkFunction`.

**Observed:** the runtime returns `{“executionResult”:{“type”:“failed”,“failed”:{“type”:“userFacingError”,“userFacingError”:{“message”:“…”}}}}`, but `transformResponseFromLocal` reads only `failed.runtimeError.message`, so the app gets `Error(“Function execution failed with no error message”)`. REST, plain `@osdk/client` and actions over the Vite `/api` path all deliver `error.parameters.message` (`409 Ontologies:QueryEncounteredUserFacingError`, `400 Ontologies:ApplyActionFailed`). (The transform also unwraps only the top level, so struct returns should stay wire-wrapped — plugin source, not browser-confirmed.)

**Expected:** parity with the `/api` path.

**Workaround:** the preview query returns `{ok:false, errors:[…]}` as data; every function returns a JSON string.

**Question:** is a fix planned? Is `error.parameters.message` the stable location when deployed too (`error.message` is only `Failed to fetch 400 Bad Request`)?

## 6. Link storage quirks (G8)

**Repro:** `batch.link(a, “related_customer_2”, b)` on an imported same-type M2M.

**Observed:** reads back as `b.related_customer_2 → a`, `a.related_customer_1 → b`, `a.related_customer_2 → []`; cross-type links are unaffected. Separately, a live link backed by a relationship object (`DeviceSiteRelationship`) is served locally as an independent table: creating the row does not populate `device.sites`, while a direct `batch.link(device, “sites”, site)` succeeds.

**Expected:** direction preserved; intermediary links derived from their rows.

**Workaround:** avoid directional self-links until deployed; write relationship rows and read two-hop.

**Question:** is the side-swap local-only? Is a direct link on an intermediary link type rejected when deployed?

## 7. No current-user identity in functions, locally (G11)

**Observed:** `@osdk/functions` 1.20.0 exposes no identity API — only the `UserId`/`GroupId`/`Principal` *types*; `FunctionContext` is `{ foundryToken, traceId, spanId, parentSpanId, sampled }` and the local bearer (`mock` / `mock-token-for-local-dev` / `fake-local-dev-token`) carries no identity. The one route, Admin `GET /api/v2/admin/users/getCurrent`, sits in the template’s `platformApiProxy.passthrough`, so forwards to the live enrollment; we did not call it.

**Expected:** a fixed mock principal locally.

**Workaround:** no user attribution on the function’s writes; the platform’s edit history is the record of who committed.

**Question:** is there, or will there be, a supported way to read the calling user inside a function, locally and deployed? Should the template’s passthrough be documented as live traffic?

## 8. Media sets (G12)

**Repro:** `{ type: “mediaReference”, includeEmptyBackingMediaSet: true, editOnly: true }` on a code-born type; a create action with `{ fileName, data: Blob }`.

**Observed:** the client sends `PUT /api/v2/mediasets/media/upload?preview=true`; the proxy answers `501 PlatformApiProxyPathNotAllowlisted` (`x-foundry-platform-api-proxy: blocked`) with a `config_hint` to add the path to `passthrough` — which would ship the bytes to the live enrollment. Direct to the local server: `404` (no `/v2/mediasets/*`); `POST …/objectTypes//media//upload` → `400 Default:InvalidArgument`, `uploadMediaContent in Ontologies namespace not supported. Use uploadMedia in MediaSets namespace instead.` A fabricated `ri.mio.main.*` reference is accepted (`200`); `fetchMetadata` then gives `404 MediaSets:MediaItemNotFound`. Also: `mediaReference`/`attachment` on an `includeEmptyBackingDatasource` type need `editOnly: true`, and a `MediaReference` function parameter is silently dropped (use `Media` from `@osdk/api`). **Attachments work end to end** (plain, modify and function-backed actions; byte-exact to 30 MiB from Node and a browser `File`; readable in a function), though bytes do not survive a dev-loop restart and seeds cannot carry them.

**Expected:** a local media store, or a loud error.

**Workaround:** an attachment property behind a thin upload seam; media sets deferred to post-deploy.

**Questions:** is a local MediaSets store planned? Could the `config_hint` warn that the path is live traffic? Should the dropped parameter be an error?

—

## What would help adopters most

1. Local runtime parity for array, geopoint and timestamp edits (G5/G9), or a documented shim.

2. Import JSON passed to block-data function discovery (G4).

3. Links with an imported end served locally, with a deploy story for their datasource (G1–G3).

4. Smart client surfacing `userFacingError` text and unwrapping structs (G7).

5. Atomic local apply, `ObjectAlreadyExists` on duplicates, never a 200 on failure (G6/G10).

6. A mock identity for functions (G11) and a local media store (G12).

7. A documented “local preview limitations” page — half of this list would have been a lookup; we would gladly review a draft.

Happy to share minimal repros (synthetic data) for all but G10, for which we have not isolated one.

Hey,

Thank you for the detailed report and the repros. Good news, I’ve checked the latest changes, and several fixes have already shipped based on other reports.

The imported-link RID issue (G1) is fixed for CLI imports in @osdk/maker-experimental 0.62.0. We’re still tracking the missing imported links (G2), which remain reproducible on latest main, and the backing dataset generation failure (G3). An unbound datasource requiring a mapping during installation is expected behaviour.

Function discovery for imported types (G4) is also fixed in @osdk/maker-experimental 0.62.0, so the narrowed edit-union workaround should no longer be necessary.

Array and geopoint edits (G5) are fixed in @osdk/generator-converters.preview 0.50.0, and timestamp edits (G9) are fixed in foundry-cli 0.293.0.

We’re tracking atomic local apply and typed duplicate-key errors (G6), and investigating the empty-success response (G10). A minimal repro for G10 would help.

The smart-client issues (G7) are confirmed. We’re tracking both preserving user-facing error messages and correctly decoding struct and collection results.

Intermediary link metadata and SDK generation have now been fixed and will be released shortly in @osdk/generator-converters.ontologyir and @osdk/generator-converters.preview. For G8, we’re still tracking self-link direction and updating local links when intermediary-object rows change.

For user identity (G11), functions can already call the platform SDK’s current-user endpoint. Locally, this returns the user signed into the CLI.

Local MediaSets uploads (G12) are not supported yet.

We’re also tracking a local-preview limitations page and clearer documentation that passthrough routes send traffic to the live enrolment.

Thanks,
Andras