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
49 changes: 40 additions & 9 deletions .github/workflows/validate-sdk-compliance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ on:
description: Comma-separated search paths for griffe relative to repo root (python only)
type: string
default: ""
typedoc-packages:
description: Comma-separated package dirs (relative to SDK root), each defining a `docs:json` script. Required when language is javascript.
type: string
default: ""

jobs:
validate:
Expand Down Expand Up @@ -99,6 +103,42 @@ jobs:
run: npm ci
working-directory: _sdk-spec/scripts/capability-matrix

# TypeScript (pnpm): the only TS SDK is the supabase-js pnpm monorepo.
# Install it, run each package's `docs:json` to emit TypeDoc JSON (each
# package owns its own entrypoints there), then normalize + merge all
# packages into one ParseResult per branch. The package list comes from
# the caller via `typedoc-packages`.
- name: Enable corepack (TypeScript)
if: inputs.language == 'javascript'
run: corepack enable

- name: Generate and normalize TypeScript symbols
if: inputs.language == 'javascript'
env:
TYPEDOC_PACKAGES: ${{ inputs.typedoc-packages }}
run: |
set -euo pipefail
if [ -z "${TYPEDOC_PACKAGES:-}" ]; then
echo "::error::typedoc-packages input is required when language is javascript"
exit 1
fi
IFS=',' read -ra PKGS <<< "$(echo "$TYPEDOC_PACKAGES" | tr -d '[:space:]')"
for b in pr base; do
ROOT="$GITHUB_WORKSPACE/_sdk-$b"
( cd "$ROOT" && pnpm install --frozen-lockfile )
INPUTS=()
for pkg in "${PKGS[@]}"; do
[ -z "$pkg" ] && continue
( cd "$ROOT" && pnpm -C "$pkg" run docs:json )
# TypeDoc emits repo-root-relative file paths, so the merged
# output stays unambiguous without any prefixing.
INPUTS+=("$ROOT/$pkg/docs/v2/spec.json")
done
( cd "$GITHUB_WORKSPACE/_sdk-spec/scripts/capability-matrix" \
&& npm run --silent normalize-typedoc -- \
--out "$GITHUB_WORKSPACE/$b-symbols.json" "${INPUTS[@]}" )
done

- name: Set up Python
if: inputs.language == 'python'
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
Expand Down Expand Up @@ -184,15 +224,6 @@ jobs:
done
working-directory: _sdk-spec/scripts/capability-matrix

- name: Parse JavaScript symbols
if: inputs.language == 'javascript'
run: |
for b in pr base; do
npm run --silent parse-ts -- "$GITHUB_WORKSPACE/_sdk-$b" \
> "$GITHUB_WORKSPACE/$b-symbols.json"
done
working-directory: _sdk-spec/scripts/capability-matrix

- name: Check new symbols against capability matrix
run: |
npm run check-api-symbols -- \
Expand Down
9 changes: 4 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,19 +59,18 @@ capabilities/*.yaml → validate (AJV schema) → aggregate (GitHub API fetc
- `aggregate.ts` — Fetches compliance files from all SDK repos via Octokit
- `generate-site.ts` — Builds the static HTML matrix site
- `report.ts` — Calculates parity percentages per feature/area/language
- `ts-parser.ts` — Syntactic TypeScript AST walker; extracts public symbols without requiring `node_modules`
- `swift-parser.ts` — Line-by-line Swift scanner; extracts public/open symbols from classes, structs, actors, enums, extensions
- `parse-ts.ts` — CLI wrapper for `ts-parser.ts`; takes an SDK root path and emits `ParseResult` JSON
- `parse-swift.ts` — CLI wrapper for `swift-parser.ts`; same contract as `parse-ts.ts`
- `normalize-typedoc.ts` — TypeDoc JSON normalizer; maps TypeDoc reflection kinds to `ParseResult`; defines `ParsedSymbol` and `ParseResult` types
- `normalize-typedoc-cli.ts` — CLI wrapper. Legacy form `<in.json> <out.json>` normalizes one project; merge form `--out <out.json> <in.json>…` normalizes and concatenates several TypeDoc JSONs into one `ParseResult` (for monorepos; TypeDoc emits repo-relative paths so no rewriting is needed)
- `scripts/dart_symbol_extractor/` (sibling Dart package) — Small `package:analyzer` tool that walks `lib/**.dart` syntactically and emits the same `ParseResult` JSON; run directly with `dart run bin/extract.dart <sdk-root>`. Parses without `pub get`; supports extension types and enhanced enums
- `parse-ignore.ts` — Loads `.sdk-parse-ignore` (gitignore syntax) to exclude paths from symbol parsing
- `parse-ignore.ts` — Loads `.sdk-parse-ignore` (gitignore syntax) to exclude paths from Swift symbol parsing (TypeScript uses TypeDoc entrypoint resolution instead)
- `api-check.ts` — Diff logic: `checkNewSymbols(base, pr, compliance)` returns symbols added in PR not in the compliance file
- `check-api-symbols.ts` — CLI; compares two `ParseResult` files against `sdk-compliance.yaml`, exits 1 with a clear error on uncovered symbols

### CI Workflows

- `validate-capabilities.yml` — Runs on push to main, PRs, and nightly; Tier 1: schema/tests/typecheck/structural; Tier 2 (PRs + nightly): reference checks against GitHub
- `validate-sdk-compliance.yml` — **Reusable workflow** called by SDK repos; validates `sdk-compliance.yaml` and blocks PRs that add public symbols not registered in the compliance file (requires `language` input: `swift`, `javascript`, or `dart`)
- `validate-sdk-compliance.yml` — **Reusable workflow** called by SDK repos; validates `sdk-compliance.yaml` and blocks PRs that add public symbols not registered in the compliance file (requires `language` input: `swift`, `javascript`, `python`, or `dart`). For `javascript` (the supabase-js pnpm monorepo) pass `typedoc-packages` — comma-separated package dirs, each with a `docs:json` script that owns its TypeDoc entrypoints; the JS path installs with pnpm and merges all packages
- `aggregate-capabilities.yml` — Hourly cron that fetches all SDK compliance data and rebuilds the site
- `deploy-pages.yml` — Deploys to GitHub Pages on main push

Expand Down
2 changes: 1 addition & 1 deletion scripts/capability-matrix/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"validate": "tsx src/cli.ts validate",
"validate:online": "tsx src/cli.ts validate --online",
"validate-compliance": "tsx src/compliance-cli.ts",
"parse-ts": "tsx src/parse-ts.ts",
"normalize-typedoc": "tsx src/normalize-typedoc-cli.ts",
"normalize-symbolgraph": "tsx src/normalize-symbolgraph-cli.ts",
"normalize-griffe": "tsx src/normalize-griffe-cli.ts",
"check-api-symbols": "tsx src/check-api-symbols.ts",
Expand Down
2 changes: 1 addition & 1 deletion scripts/capability-matrix/src/api-check.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { buildSymbolIndex } from "./compliance.js";
import type { RawCompliance } from "./compliance.js";
import type { ParsedSymbol } from "./ts-parser.js";
import type { ParsedSymbol } from "./normalize-typedoc.js";

export interface CheckResult {
newSymbols: string[];
Expand Down
2 changes: 1 addition & 1 deletion scripts/capability-matrix/src/check-api-symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { resolve } from "node:path";
import { parse } from "yaml";
import { checkNewSymbols, formatErrorMessage, formatRemovedMessage } from "./api-check.js";
import type { RawCompliance } from "./compliance.js";
import type { ParseResult } from "./ts-parser.js";
import type { ParseResult } from "./normalize-typedoc.js";

async function main(): Promise<void> {
const [prFile, baseFile, compliancePath] = process.argv.slice(2);
Expand Down
2 changes: 1 addition & 1 deletion scripts/capability-matrix/src/normalize-griffe.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { basename, relative } from "node:path";
import type { ParsedSymbol, ParseResult } from "./ts-parser.js";
import type { ParsedSymbol, ParseResult } from "./normalize-typedoc.js";
import { loadIgnore, type Ignore } from "./parse-ignore.js";

export type { ParsedSymbol, ParseResult };
Expand Down
2 changes: 1 addition & 1 deletion scripts/capability-matrix/src/normalize-symbolgraph.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { relative } from "node:path";
import type { ParsedSymbol, ParseResult } from "./ts-parser.js";
import type { ParsedSymbol, ParseResult } from "./normalize-typedoc.js";
export type { ParsedSymbol, ParseResult };

export interface SymbolGraphSymbol {
Expand Down
41 changes: 41 additions & 0 deletions scripts/capability-matrix/src/normalize-typedoc-cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { readFileSync, writeFileSync } from "node:fs";
import { mergeProjects } from "./normalize-typedoc.js";

// Usage:
// normalize-typedoc <input.json> <output.json> (single project)
// normalize-typedoc --out <output.json> <input.json>... (merge N projects)
//
// The merge form concatenates the symbols of several TypeDoc project JSONs into
// one ParseResult — used for monorepos where each package is documented
// separately. TypeDoc emits repo-root-relative file paths, so no path rewriting
// is needed.

const argv = process.argv.slice(2);
const outIdx = argv.indexOf("--out");

let inputs: string[];
let outputPath: string | undefined;

if (outIdx !== -1) {
outputPath = argv[outIdx + 1];
inputs = argv.filter((_, i) => i !== outIdx && i !== outIdx + 1);
} else {
// Legacy single-project form: <input> <output>
const [inputPath, legacyOut] = argv;
inputs = inputPath ? [inputPath] : [];
outputPath = legacyOut;
}

if (!outputPath || inputs.length === 0) {
console.error(
[
"Usage:",
" normalize-typedoc <input.json> <output.json>",
" normalize-typedoc --out <output.json> <input.json>...",
].join("\n"),
);
process.exit(1);
}

const projects = inputs.map((path) => JSON.parse(readFileSync(path, "utf8")));
writeFileSync(outputPath, JSON.stringify(mergeProjects(projects), null, 2));
113 changes: 113 additions & 0 deletions scripts/capability-matrix/src/normalize-typedoc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
export interface ParsedSymbol {
name: string;
kind: "class" | "method" | "property" | "function" | "variable";
file: string;
line?: number;
}

export interface ParseResult {
symbols: ParsedSymbol[];
}

const Kind = {
Module: 2,
Namespace: 4,
Enum: 8,
EnumMember: 16,
Variable: 32,
Function: 64,
Class: 128,
Interface: 256,
Constructor: 512,
Property: 1024,
Method: 2048,
Accessor: 262144,
TypeAlias: 2097152,
Reference: 4194304,
} as const;

interface TdReflection {
name: string;
kind: number;
flags?: { isPrivate?: boolean; isProtected?: boolean };
sources?: Array<{ fileName: string; line?: number }>;
children?: TdReflection[];
}

function sourceOf(r: TdReflection): { file: string; line?: number } {
const src = r.sources?.[0];
return { file: src?.fileName ?? "", line: src?.line };
}

function isExcluded(r: TdReflection): boolean {
return !!(r.flags?.isPrivate || r.flags?.isProtected);
}

function extractMembers(
parent: string,
children: TdReflection[],
out: ParsedSymbol[],
): void {
for (const child of children) {
if (isExcluded(child)) continue;
if (child.kind === Kind.Constructor) continue;
const qualName = `${parent}.${child.name}`;
const { file, line } = sourceOf(child);
if (child.kind === Kind.Method) {
out.push({ name: qualName, kind: "method", file, line });
} else if (child.kind === Kind.Property) {
out.push({ name: qualName, kind: "property", file, line });
} else if (child.kind === Kind.Accessor) {
out.push({ name: qualName, kind: "method", file, line });
} else if (child.kind === Kind.EnumMember) {
out.push({ name: qualName, kind: "property", file, line });
}
}
}

function extractDeclarations(
children: TdReflection[],
out: ParsedSymbol[],
): void {
for (const child of children) {
if (isExcluded(child)) continue;
const { file, line } = sourceOf(child);
if (child.kind === Kind.Module || child.kind === Kind.Namespace) {
if (child.children) extractDeclarations(child.children, out);
} else if (child.kind === Kind.Reference) {
continue;
} else if (
child.kind === Kind.Class ||
child.kind === Kind.Interface ||
child.kind === Kind.Enum
) {
out.push({ name: child.name, kind: "class", file, line });
if (child.children) extractMembers(child.name, child.children, out);
} else if (child.kind === Kind.Function) {
out.push({ name: child.name, kind: "function", file, line });
} else if (child.kind === Kind.Variable || child.kind === Kind.TypeAlias) {
out.push({ name: child.name, kind: "variable", file, line });
}
}
}

export function normalize(json: unknown): ParseResult {
const project = json as TdReflection;
const symbols: ParsedSymbol[] = [];
if (project.children) extractDeclarations(project.children, symbols);
return { symbols };
}

/**
* Normalize one or more TypeDoc project JSONs into a single `ParseResult` by
* concatenating their symbols. Used for monorepos where each package is
* documented separately. TypeDoc already emits repo-root-relative `fileName`s,
* so no path rewriting is needed. Symbol names are kept as-is (the API-check
* diff is name-based), so this is a plain concatenation — no cross-package
* deduping; duplicate re-exports are collapsed by name downstream.
*/
export function mergeProjects(projects: unknown[]): ParseResult {
const symbols: ParsedSymbol[] = [];
for (const json of projects) symbols.push(...normalize(json).symbols);
return { symbols };
}
19 changes: 0 additions & 19 deletions scripts/capability-matrix/src/parse-ts.ts

This file was deleted.

Loading