Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .claude/rules/adapter-writeback-discovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ Source schemas from the strongest available contract for the integration: JSON S
New resources must use file-native writeback, not magic filenames. Declare an `idPattern` in `src/resources.ts`, ship a full-record `.schema.json` beside the resource, mark provider-managed fields with `readOnly: true`, and provide a `.create.example.json` for minimal create documents. Creates happen by writing to a non-canonical filename; patches happen by writing mutable fields to a canonical `<id>.json`; deletes happen by removing a canonical `<id>.json`.

Run `npm run test:writeback-discovery` before opening a PR that touches adapter writeback behavior or creates a new adapter.

Keep tracking docs current. When adding a new adapter, adding/removing writeback resources, or changing whether a writeback schema is sourced from OpenAPI, JSON Schema, Postman, provider docs, or inline adapter code, update `docs/writeback-spec-coverage.md` in the same PR. If new tracking docs are added for integration scope, schema provenance, provider permissions, or generated discovery coverage, update those docs alongside the source change instead of leaving the state implicit in code.
2 changes: 1 addition & 1 deletion .claude/rules/alias-subtrees.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ For `by-state`, group records under a state subdirectory:

## Collision handling

Always use `aliasCollisionSuffix` from `packages/github/src/alias-slug.ts` (an 8-char hex of `sha256(id)`). Append it to the alias slug whenever a slug collides with an existing alias. NEVER pick "first writer wins" — the alias must be deterministic across sync runs so that re-emitting the same entity produces the same alias path.
Always use `aliasCollisionSuffix` from `packages/core/src/alias-slug.ts` (an 8-char hex of `sha256(id)`). Append it to the alias slug whenever a slug collides with an existing alias. NEVER pick "first writer wins" — the alias must be deterministic across sync runs so that re-emitting the same entity produces the same alias path.

## Alias file content

Expand Down
26 changes: 26 additions & 0 deletions .claude/rules/generated-adapter-paths.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated adapter paths

When this applies: adding or editing generated mapping YAML, workflow prompts, README path examples, discovery docs, or `path-mapper.ts` for any adapter.

## Generated templates can be stale

Do not trust generated examples that emit `/<provider>/<resource>/<id>/metadata.json`. That shape is legacy scaffolding from early adapters. The adapter contract wins.

Before merging:

1. Decide whether the entity owns child files.
2. If it owns child files, emit `<id>__<slug>/meta.json`.
3. If it does not own child files, emit `<slug>__<id>.json` or `<id>.json` when there is no useful slug.
4. Update every path example in mapping YAML, README, workflow prompts, discovery docs, and writeback tracking docs.
5. Add path-mapper tests that assert the canonical shape and a back-compat parser test for any old shape consumers may still read.

## GitLab-specific warning

GitLab merge requests, issues, pipelines, and commits all own child files:

- merge requests: `diff.patch`, `discussions/*.json`, `approvals.json`
- issues: `comments/*.json`
- pipelines: `jobs/*.json`
- commits: `comments/*.json`

These must never emit `metadata.json` as their canonical path.
2 changes: 1 addition & 1 deletion .claude/rules/naming-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Note the ID/slug order flips between the two shapes: flat puts the slug first (s

## Slug rules

Always go through `slugifyAlias` from `packages/github/src/alias-slug.ts`:
Always go through `slugifyAlias` from `packages/core/src/alias-slug.ts`:

- ASCII only, lowercase, hyphen-separated.
- Truncate to 80 characters at a word boundary.
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ dist/
.agent-relay/
.relay/
.workflow-artifacts/
.codex/

# Compiled test artifacts
packages/*/tests/**/*.js
Expand Down
16 changes: 16 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@ Every adapter under `packages/<name>` MUST:
- Provide `by-*` alias subtree views when the underlying entity has a natural human-readable lookup key distinct from its stable ID (titles, names, keys, statuses, parents). Each alias path resolves to the canonical record; alias content is the minimal pointer `{ id, canonicalPath, ...minimal pointer fields }`.
- Use `packages/core/src/alias-slug.ts` (`slugifyAlias`, `aliasCollisionSuffix`) for slug normalization and collision suffixes. Provider-local alias modules should re-export those helpers for backward compatibility. NEVER write a new slugifier.

### Generated adapter path templates are not authoritative

Older mapping specs and generated workflow prompts may still contain the legacy
`/<provider>/<resource>/<id>/metadata.json` shape. Treat those as historical
scaffolding only. Before shipping any adapter path change, compare every emitted
canonical path against the contract below:

- Entities with child artifacts MUST use directory records ending in `meta.json`.
- Entities without child artifacts MUST use flat `.json` records.
- If a generated template disagrees with `path-mapper.ts`, update the template,
README, discovery docs, and tests in the same PR.
- Add a regression test that fails on the legacy `metadata.json` shape whenever
the entity owns child files.

### Naming convention

The cross-adapter joiner between a human-readable slug and the provider's stable ID is **`<slug>__<id>`** (double underscore). The shape depends on whether the entity owns child files:
Expand Down Expand Up @@ -94,6 +108,8 @@ Each adapter must also ship `<adapter>/.adapter.md` in its discovery tree with a

New resources must not introduce a magic `new.json` create path. Creates happen by writing a valid JSON document to any non-canonical filename in the resource directory; edits happen by writing mutable fields to a canonical `<id>.json`; deletes happen by removing a canonical `<id>.json`. When adding a new adapter or writeback route, update `scripts/writeback-discovery-data.mjs`, regenerate the discovery files with `node scripts/generate-writeback-discovery.mjs`, and run `npm run test:writeback-discovery`. Do not rely on prompts alone to describe writeback shapes.

Tracking docs are part of the adapter contract. When adding a new integration, adding/removing writeback resources, or changing whether an endpoint is backed by OpenAPI, JSON Schema, Postman, provider docs, or inline code, update `docs/writeback-spec-coverage.md` in the same PR. If a change affects any other integration tracking document, such as scope/permission inventories, schema provenance notes, provider capability matrices, or generated discovery coverage, update that tracking doc alongside the code and mention the doc update in the PR summary.

<!-- PRPM_MANIFEST_START -->

<skills_system priority="1">
Expand Down
49 changes: 49 additions & 0 deletions docs/writeback-spec-coverage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Writeback Spec Coverage

This file tracks which file-native writeback discovery schemas are backed by provider contracts rather than hand-authored inline schema objects in `scripts/writeback-discovery-data.mjs`.

Contract-backed means the endpoint uses `contractEndpoint(...)`, loads its request schema through `scripts/writeback-contracts.mjs`, and emits `x-relayfile-source` provenance into the generated `.schema.json`.

## Current Coverage

| Adapter | Contract source | Contract-backed endpoints | Inline endpoints | Notes |
|---|---|---:|---:|---|
| github | OpenAPI snapshot in `scripts/integration-contracts/github/source/openapi.yaml`, selected by `scripts/integration-contracts/github/writeback.openapi.json` | 3 | 0 | `issues/create`, `issues/create-comment`, and `pulls/create-review` are spec-backed with small relayfile overlays. |
| asana | None | 0 | 4 | Inline JS schemas. |
| azure-blob | None | 0 | 2 | Inline JS schemas. |
| box | None | 0 | 2 | Inline JS schemas. |
| clickup | None | 0 | 5 | Inline JS schemas. |
| confluence | None | 0 | 2 | Inline JS schemas. |
| dropbox | None | 0 | 2 | Inline JS schemas. |
| gcs | None | 0 | 2 | Inline JS schemas. |
| gitlab | None | 0 | 2 | Inline JS schemas. |
| gmail | None | 0 | 3 | Inline JS schemas. |
| google-calendar | None | 0 | 1 | Inline JS schemas. |
| google-drive | None | 0 | 2 | Inline JS schemas. |
| hubspot | None | 0 | 4 | Inline JS schemas. |
| intercom | None | 0 | 3 | Inline JS schemas. |
| jira | None | 0 | 4 | Inline JS schemas. |
| linear | None | 0 | 2 | Inline JS schemas; provider source is GraphQL, not OpenAPI. |
| notion | None | 0 | 1 | Inline JS schemas. |
| onedrive | None | 0 | 2 | Inline JS schemas. |
| pipedrive | None | 0 | 4 | Inline JS schemas. |
| postgres | None | 0 | 2 | Inline JS schemas; database/table shape is runtime-native rather than provider OpenAPI. |
| redis | None | 0 | 2 | Inline JS schemas; key/value shape is runtime-native rather than provider OpenAPI. |
| s3 | None | 0 | 2 | Inline JS schemas. |
| salesforce | None | 0 | 5 | Inline JS schemas. |
| sharepoint | None | 0 | 2 | Inline JS schemas. |
| slack | None | 0 | 4 | Inline JS schemas. |
| teams | None | 0 | 3 | Inline JS schemas. |
| zendesk | None | 0 | 3 | Inline JS schemas. |

## Updating This File

When an adapter moves an endpoint from `endpoint(...)` to `contractEndpoint(...)`:

1. Add or update a contract manifest under `scripts/integration-contracts/<adapter>/`.
2. Keep full upstream specs under a nested directory such as `source/` so only the manifest is auto-loaded.
3. Regenerate discovery with `node scripts/generate-writeback-discovery.mjs`.
4. Run `npm run test:writeback-discovery`.
5. Update this table with the new contract-backed and inline endpoint counts.

Use overlays only for relayfile-specific behavior or provider spec gaps. If most of a schema is still described in an overlay, leave it marked inline until the contract carries the bulk of the shape.
3 changes: 2 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@
"test": "npm run test:publish-targets && npm run test:internal-dependencies && npm run test:writeback-discovery && npx turbo test",
"test:publish-targets": "node --test scripts/resolve-publish-targets.test.mjs",
"test:internal-dependencies": "node scripts/sync-internal-dependencies.mjs --check",
"test:writeback-discovery": "node scripts/verify-writeback-discovery.mjs",
"test:writeback-discovery": "node --test scripts/writeback-discovery-normalizer.test.mjs && node scripts/verify-writeback-discovery.mjs",
"test:writeback-e2e": "node scripts/verify-file-native-writeback-e2e.mjs",
"typecheck": "npx turbo typecheck"
},
"devDependencies": {
"@agentworkforce/workload-router": "^0.1.3",
"agent-trajectories": "^0.5.8",
"turbo": "^2.5.0",
"typescript": "^5.7.0"
"typescript": "^5.7.0",
"yaml": "^2.8.1"
}
}
32 changes: 9 additions & 23 deletions packages/confluence/src/layout.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,14 @@
export type MaterializationMode = 'eager' | 'lazy';
import type { LayoutManifestProvider as CoreLayoutManifestProvider } from '@relayfile/adapter-core';

export interface WritebackResourceManifest {
readonly path: string;
readonly schemaId: string;
}
export type {
LayoutManifest,
LayoutManifestProvider,
LayoutResourceManifest,
MaterializationMode,
WritebackResourceManifest,
} from '@relayfile/adapter-core';

export interface LayoutResourceManifest {
readonly path: string;
readonly title: string;
readonly materialization: MaterializationMode;
readonly aliasSegments: readonly string[];
readonly writebackResources: readonly WritebackResourceManifest[];
}

export interface LayoutManifest {
readonly provider: string;
readonly filenameConvention: string;
readonly aliasSegments: readonly string[];
readonly resources: readonly LayoutResourceManifest[];
}

export type LayoutManifestProvider = () => LayoutManifest;

export const layoutManifest: LayoutManifestProvider = () => ({
export const layoutManifest: CoreLayoutManifestProvider = () => ({
provider: 'confluence',
filenameConvention: '<slug>__<id>.json',
aliasSegments: ['by-id', 'by-title', 'by-state'],
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ export * from "./storage-bridge/index.js";
export * from "./atomic-index/index.js";
export * from "./emit-auxiliary/index.js";
export * from "./alias-slug.js";
export * from "./layout-contract.js";
55 changes: 55 additions & 0 deletions packages/core/src/layout-contract.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";

import type {
LayoutManifest,
LayoutManifestProvider,
MaterializationMode,
} from "./layout-contract.js";

describe("layout manifest contract", () => {
it("types provider manifests with eager and lazy resources", () => {
const provider: LayoutManifestProvider = () => ({
provider: "example",
filenameConvention: "<slug>__<id>.json",
aliasSegments: ["by-title"],
resources: [
{
path: "example/pages",
title: "Pages",
materialization: "eager",
aliasSegments: ["by-title"],
writebackResources: [
{ path: "example/pages", schemaId: "example/page" },
],
},
{
path: "example/users",
title: "Users",
materialization: "lazy",
aliasSegments: [],
writebackResources: [],
},
],
});

const manifest = provider();
const modes: readonly MaterializationMode[] = manifest.resources.map(
(resource) => resource.materialization,
);

assert.equal(manifest.provider, "example");
assert.deepEqual(modes, ["eager", "lazy"]);
});

it("accepts readonly manifest literals", () => {
const manifest = {
provider: "example",
filenameConvention: "<id>__<slug>/meta.json",
aliasSegments: ["by-name"],
resources: [],
} as const satisfies LayoutManifest;

assert.equal(manifest.filenameConvention, "<id>__<slug>/meta.json");
});
});
23 changes: 23 additions & 0 deletions packages/core/src/layout-contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export type MaterializationMode = "eager" | "lazy";

export interface WritebackResourceManifest {
readonly path: string;
readonly schemaId: string;
}

export interface LayoutResourceManifest {
readonly path: string;
readonly title: string;
readonly materialization: MaterializationMode;
readonly aliasSegments: readonly string[];
readonly writebackResources: readonly WritebackResourceManifest[];
}

export interface LayoutManifest {
readonly provider: string;
readonly filenameConvention: string;
readonly aliasSegments: readonly string[];
readonly resources: readonly LayoutResourceManifest[];
}

export type LayoutManifestProvider = () => LayoutManifest;
Loading
Loading