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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ dashboard/.env
.idea/
dashboard/tsconfig.tsbuildinfo

# RFC-089: build-time copies of docs/llms.txt + docs/llm-integration.md.
# RFC-089: build-time copies of docs/llms.txt + docs/llm-integration.md,
# and the concatenated llms-full.txt bundle generated by sync-llm-docs.mjs.
# Canonical source is docs/ — never edit these.
dashboard/public/llms.txt
dashboard/public/llm-integration.md
dashboard/public/llms-full.txt
# Added by code-review-graph
.code-review-graph/

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ ContractGate is a high-performance validation gateway that enforces rich semanti
## Using Claude, Cursor, or Codex? Paste this

```
https://datacontractgate.com/llm-integration.md
https://app.datacontractgate.com/llm-integration.md
https://github.com/nightmoose/contractgate

implement this in my repo
Expand Down
11 changes: 11 additions & 0 deletions dashboard/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,17 @@ export const metadata: Metadata = {
title: TITLE,
description: DESCRIPTION,
},
// Machine-discoverable markdown surfaces for AI coding agents. Agents that
// scrape a page for `<link rel="alternate" type="text/markdown">` find the
// LLM index and full documentation bundle without any heuristics.
alternates: {
types: {
"text/markdown": [
{ url: "/llms.txt", title: "LLM Index" },
{ url: "/llms-full.txt", title: "Full LLM Documentation" },
],
},
},
};

export default function RootLayout({
Expand Down
27 changes: 27 additions & 0 deletions dashboard/app/robots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { MetadataRoute } from "next";

const SITE_URL =
process.env.NEXT_PUBLIC_APP_URL ?? "https://app.datacontractgate.com";

// Named AI crawlers are allowed explicitly so operators reading robots.txt can
// see intent, even though `User-agent: *` already covers them. The playbook
// and llms-full.txt are the primary discovery surface for these agents.
const AI_CRAWLERS = [
"GPTBot",
"ClaudeBot",
"Claude-Web",
"PerplexityBot",
"Google-Extended",
"CCBot",
"anthropic-ai",
];

export default function robots(): MetadataRoute.Robots {
return {
rules: [
{ userAgent: "*", allow: "/" },
...AI_CRAWLERS.map((userAgent) => ({ userAgent, allow: "/" })),
],
host: SITE_URL,
};
}
2 changes: 1 addition & 1 deletion dashboard/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const nextConfig: NextConfig = {
async headers() {
return [
{
source: "/:path(llms.txt|llm-integration.md)",
source: "/:path(llms.txt|llms-full.txt|llm-integration.md)",
headers: [
{ key: "Content-Type", value: "text/plain; charset=utf-8" },
{ key: "Cache-Control", value: "public, max-age=300" },
Expand Down
42 changes: 40 additions & 2 deletions dashboard/scripts/sync-llm-docs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@
* RFC-089 — copy the agent-facing docs into public/ so they are served raw at
* https://app.datacontractgate.com/llms.txt
* https://app.datacontractgate.com/llm-integration.md
* https://app.datacontractgate.com/llms-full.txt
*
* Canonical source is docs/ at the repo root. The copies in public/ are
* gitignored so the repo file can never drift from what is served.
*
* llms-full.txt is a concatenation of the integration playbook plus every
* reference doc linked from llms.txt. Large-context agents (Claude, Gemini)
* can ingest it in one HTTP round-trip instead of following links.
*
* The Docker demo image builds with context ./dashboard, so ../docs does not
* exist there. That build is allowed to proceed without the copies; a Vercel
* build is not (missing files there means the paste-a-URL flow 404s in prod).
*/
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

Expand All @@ -20,7 +25,20 @@ const publicDir = join(here, "..", "public");

const FILES = ["llms.txt", "llm-integration.md"];

const missing = FILES.filter((f) => !existsSync(join(docsDir, f)));
// Order matches the "Reference" section of llms.txt. Playbook first so agents
// hit the executable flow before the deep reference material.
const FULL_BUNDLE = [
"llm-integration.md",
"v1-ingest-reference.md",
"deploy-contract-reference.md",
"csv-inference-reference.md",
"quarantine-replay-reference.md",
"pii-masking-reference.md",
];

const missing = [...new Set([...FILES, ...FULL_BUNDLE])].filter(
(f) => !existsSync(join(docsDir, f)),
);

if (missing.length > 0) {
const msg = `sync-llm-docs: missing in ${docsDir}: ${missing.join(", ")}`;
Expand All @@ -37,3 +55,23 @@ for (const f of FILES) {
copyFileSync(join(docsDir, f), join(publicDir, f));
console.log(`sync-llm-docs: docs/${f} -> public/${f}`);
}

const bundleHeader = `# ContractGate — Full LLM Documentation Bundle

> One-shot ingestion for large-context agents. Concatenates the integration
> playbook and every reference doc linked from llms.txt in the order an agent
> is likely to need them. For a lightweight index, use llms.txt instead.

Source of each section: https://github.com/nightmoose/contractgate/blob/main/docs/<file>

`;

const bundleBody = FULL_BUNDLE.map((f) => {
const body = readFileSync(join(docsDir, f), "utf8").trimEnd();
return `\n\n---\n\n<!-- source: docs/${f} -->\n\n${body}\n`;
}).join("");

writeFileSync(join(publicDir, "llms-full.txt"), bundleHeader + bundleBody);
console.log(
`sync-llm-docs: bundled ${FULL_BUNDLE.length} docs -> public/llms-full.txt`,
);
26 changes: 26 additions & 0 deletions docs/agent-rules/AGENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# ContractGate rules (generic agent instructions)

Portable version for Claude Code, Codex, Aider, Antigravity, or any agent
whose instruction format is plain markdown. Paste into your project's agent
instructions file (e.g. `CLAUDE.md`, `AGENTS.md`, `.aider.conf.yml`).

---

This repository ships event data behind ContractGate. Every event that leaves
a producer must satisfy a YAML contract in the `contracts/` directory.

**When editing an event publisher, ingestion path, or schema that feeds one:**

1. Read `contracts/<name>.yaml` for the affected event. If none exists, infer
one: `contractgate infer --from-stdin --name <name> --out
contracts/<name>.yaml` (feed real sample events on stdin).
2. Reflect every new or renamed producer field in the contract in the same
change.
3. Run `contractgate test --contract contracts/<name>.yaml --data <fixture>`
before committing. Fix violations by tightening the code, not by loosening
the contract.
4. To deploy: `contractgate deploy-contract contracts/<name>.yaml --json`.
Never edit a deployed version in place — bump `version:` in the YAML and
redeploy.

Full playbook: <https://app.datacontractgate.com/llm-integration.md>.
21 changes: 21 additions & 0 deletions docs/agent-rules/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Agent rule presets

Drop-in rule files for coding agents that operate on repos using ContractGate.
Each file tells the agent to consult the contracts in `contracts/` and run
`contractgate test` before committing changes that affect event publishers,
ingestion paths, or schemas.

Copy the file that matches your tool into the destination path below. All
presets carry the same core rule — pick the one your team uses.

| Agent | Preset | Destination |
|---|---|---|
| Cursor | `cursor.mdc` | `.cursor/rules/contractgate.mdc` |
| Windsurf | `windsurf.md` | `.windsurf/rules/contractgate.md` |
| GitHub Copilot | `copilot-instructions.md` | `.github/copilot-instructions.md` |
| Claude Code, Codex, others | `AGENT.md` | Paste into your project's agent instructions file |

Every preset points the agent at the canonical playbook:
<https://app.datacontractgate.com/llm-integration.md>. Nothing here duplicates
that content — the presets are short by design so they can be dropped into
a repo without becoming a maintenance burden.
21 changes: 21 additions & 0 deletions docs/agent-rules/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# ContractGate rules (GitHub Copilot)

This repository ships event data behind ContractGate. Every event that leaves
a producer must satisfy a YAML contract in the `contracts/` directory.

When you edit an event publisher, an ingestion path, or a schema that feeds
one of these:

- Read the contract for the affected event in `contracts/<name>.yaml`. If none
exists, infer one: `contractgate infer --from-stdin --name <name> --out
contracts/<name>.yaml` with real sample events on stdin.
- Any new or renamed field in the producer must be reflected in the contract
in the same change.
- Run `contractgate test --contract contracts/<name>.yaml --data <fixture>`
before opening a PR. Fix violations by tightening the code, not by loosening
the contract.
- To deploy a contract change: `contractgate deploy-contract
contracts/<name>.yaml --json`. Never edit a deployed version in place — bump
`version:` in the YAML and redeploy.

Full playbook: <https://app.datacontractgate.com/llm-integration.md>.
36 changes: 36 additions & 0 deletions docs/agent-rules/cursor.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
description: Enforce ContractGate contracts on event publishers and ingestion paths.
globs:
- "**/contracts/**/*.yaml"
- "**/*producer*"
- "**/*publisher*"
- "**/*ingest*"
alwaysApply: false
---

# ContractGate rules

This repository ships event data behind ContractGate. Every event that leaves
a producer must satisfy a YAML contract in `contracts/`.

## Before editing an event publisher or ingestion path

1. Read the contract for the affected event in `contracts/<name>.yaml`. If none
exists, infer one first: `contractgate infer --from-stdin --name <name>
--out contracts/<name>.yaml` (feed real sample events on stdin).
2. Any new or renamed field in the producer must be reflected in the contract
in the same change. Missing or misnamed fields are validation failures at
ingest, not code review comments.

## Before committing

Run `contractgate test --contract contracts/<name>.yaml --data <fixture>` on
representative fixtures. Fix violations by tightening the code, not by
loosening the contract.

## Deploying a contract change

Use the CLI: `contractgate deploy-contract contracts/<name>.yaml --json`.
Never edit a deployed version in place — bump `version:` in the YAML and
redeploy. See <https://app.datacontractgate.com/llm-integration.md> for the
full playbook.
27 changes: 27 additions & 0 deletions docs/agent-rules/windsurf.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# ContractGate rules (Windsurf)

Trigger: any change under `contracts/`, or to files whose name contains
`producer`, `publisher`, or `ingest`.

This repository ships event data behind ContractGate. Every event that leaves
a producer must satisfy a YAML contract in `contracts/`.

## Before editing an event publisher or ingestion path

1. Read the contract for the affected event in `contracts/<name>.yaml`. If none
exists, infer one first: `contractgate infer --from-stdin --name <name>
--out contracts/<name>.yaml` (feed real sample events on stdin).
2. Any new or renamed field in the producer must be reflected in the contract
in the same change.

## Before committing

Run `contractgate test --contract contracts/<name>.yaml --data <fixture>` on
representative fixtures. Fix violations by tightening the code, not by
loosening the contract.

## Deploying a contract change

Use `contractgate deploy-contract contracts/<name>.yaml --json`. Never edit a
deployed version in place — bump `version:` in the YAML and redeploy. Full
playbook: <https://app.datacontractgate.com/llm-integration.md>.
58 changes: 37 additions & 21 deletions docs/llm-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@ response shape:
| `POST /contracts/infer/proto` | Protobuf schema |
| `POST /contracts/infer/openapi` | OpenAPI spec |

CLI alternative — runs entirely locally, no key needed, pipes into stdout or a
file:

```bash
curl -sS "https://api.example.com/events?limit=50" \
| contractgate infer --from-stdin --name user_events --out contracts/user_events.yaml
```

Inference is a **starting point, not the answer.** Continue to §3.

---
Expand Down Expand Up @@ -180,29 +188,21 @@ Optional top-level keys you may set when the user asks for them, not by default:

## §4 — Deploy the contract as a stable version

```
POST /contracts/deploy
X-Api-Key: cg_live_…
Content-Type: application/json
Preferred: use the `contractgate` CLI. It reads the YAML file directly, so
there is nothing to escape and no `jq` dependency. Install once:

```bash
cargo install --git https://github.com/nightmoose/contractgate contractgate
```

Build the JSON body from the file — never hand-escape YAML into a shell literal:
Then deploy:

```bash
jq -n \
--arg name "user_events" \
--rawfile yaml "contracts/user_events.yaml" \
'{name: $name, yaml_content: $yaml, source: "app-backend", deployed_by: "claude-code"}' \
| curl -sS -X POST "https://app.datacontractgate.com/contracts/deploy" \
-H "X-Api-Key: $CONTRACTGATE_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @-
contractgate deploy-contract contracts/user_events.yaml \
--source app-backend --deployed-by "$USER" --json
```

(No `jq`? Any equivalent works — the only requirement is that `yaml_content` is
the file's contents as a JSON string.)

Request: `{ name, yaml_content, source?, deployed_by? }`. Response:
Response (identical to the HTTP endpoint):

```json
{
Expand All @@ -229,12 +229,28 @@ version (`deprecated_count`).
Put it in the user's config or environment (e.g. `CONTRACTGATE_CONTRACT_ID`) —
it is not a secret.

CLI alternative, if the user already has the binary
(`cargo install --git https://github.com/nightmoose/contractgate contractgate`):
HTTP fallback if the CLI is not available (Windows without a Rust toolchain,
sandboxed CI, etc.):

```
POST /contracts/deploy
X-Api-Key: cg_live_…
Content-Type: application/json
```

Body: `{ name, yaml_content, source?, deployed_by? }`. `yaml_content` is the
file contents as a JSON string; a language runtime is the reliable way to
escape it. Any shell with `jq` works too:

```bash
contractgate deploy-contract contracts/user_events.yaml \
--source app-backend --deployed-by "$USER" --json
jq -n \
--arg name "user_events" \
--rawfile yaml "contracts/user_events.yaml" \
'{name: $name, yaml_content: $yaml, source: "app-backend", deployed_by: "claude-code"}' \
| curl -sS -X POST "https://app.datacontractgate.com/contracts/deploy" \
-H "X-Api-Key: $CONTRACTGATE_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @-
```

---
Expand Down
1 change: 1 addition & 0 deletions docs/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Source: https://github.com/nightmoose/contractgate
## Start here

- [Integration playbook for coding agents](https://app.datacontractgate.com/llm-integration.md): Paste this into Claude, Cursor, or Codex. End-to-end, executable: get a key, infer a contract from real samples, write and deploy the contract YAML, wire the producer to POST /v1/ingest, verify with a dry run.
- [Full documentation bundle](https://app.datacontractgate.com/llms-full.txt): One-shot ingestion for large-context agents — the playbook and every reference doc below, concatenated. Skip the per-link round-trips.

## Reference

Expand Down
Loading
Loading