From 4d884ab0bdfb04fe6df6703af6c6dfe2d4574595 Mon Sep 17 00:00:00 2001 From: Matvey Arye Date: Mon, 5 Jan 2026 13:22:03 +0100 Subject: [PATCH 1/2] Add PostGIS skill with upstream sync checking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add PostGIS skill imported from gitea.osgeo.org/postgis/postgis - Add upstream-skills.json to track upstream sources with attribution - Add check script to detect upstream changes - Add GitHub Actions workflow to run weekly checks and create issues Thanks to Darafei Praliaskouski for the original PostGIS SKILL.md. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/check-upstream-skills.yaml | 81 ++++++++++ package.json | 3 +- scripts/check-upstream-skills.ts | 153 +++++++++++++++++++ skills/postgis/SKILL.md | 67 ++++++++ upstream-skills.json | 11 ++ 5 files changed, 314 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/check-upstream-skills.yaml create mode 100644 scripts/check-upstream-skills.ts create mode 100644 skills/postgis/SKILL.md create mode 100644 upstream-skills.json diff --git a/.github/workflows/check-upstream-skills.yaml b/.github/workflows/check-upstream-skills.yaml new file mode 100644 index 0000000..c8c9377 --- /dev/null +++ b/.github/workflows/check-upstream-skills.yaml @@ -0,0 +1,81 @@ +name: Check Upstream Skills + +on: + # Run weekly on Monday at 9am UTC + schedule: + - cron: '0 9 * * 1' + # Allow manual trigger + workflow_dispatch: + +jobs: + check-upstream: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v5 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Check upstream skills for updates + id: check + run: npm run check:upstream-skills + continue-on-error: true + + - name: Create or update issue if upstream changed + if: steps.check.outcome == 'failure' + uses: actions/github-script@v7 + with: + script: | + const title = 'Upstream skill files have changed'; + const body = `The upstream skill check has detected changes in one or more skill files. + + Please review the upstream changes and update the local skill files if needed. + + **Latest workflow run:** ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId} + + **After updating:** + 1. Review the changes in the upstream repository + 2. Update the local SKILL.md file with any relevant changes + 3. Update the \`pinned_commit\` in UPSTREAM.json to the new commit hash + `; + + // Check if issue already exists + const issues = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + labels: 'upstream-sync' + }); + + const existingIssue = issues.data.find(issue => issue.title === title); + + if (existingIssue) { + // Update existing issue with latest run info + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: existingIssue.number, + body: `Upstream changes still detected. Latest workflow run: ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}` + }); + console.log(`Updated existing issue #${existingIssue.number}`); + } else { + // Create new issue + const newIssue = await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: title, + body: body, + labels: ['upstream-sync'] + }); + console.log(`Created new issue #${newIssue.data.number}`); + } + + - name: Fail if upstream changed + if: steps.check.outcome == 'failure' + run: exit 1 diff --git a/package.json b/package.json index a5edf08..88de7d2 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,8 @@ "lint:fix": "eslint --fix", "prettier:check": "prettier --check .", "prettier:write": "prettier --write .", - "migrate": "migrate" + "migrate": "migrate", + "check:upstream-skills": "tsx scripts/check-upstream-skills.ts" }, "dependencies": { "@ai-sdk/openai": "^2.0.80", diff --git a/scripts/check-upstream-skills.ts b/scripts/check-upstream-skills.ts new file mode 100644 index 0000000..a3fc066 --- /dev/null +++ b/scripts/check-upstream-skills.ts @@ -0,0 +1,153 @@ +#!/usr/bin/env npx tsx + +/** + * Script to check if upstream skill files have been updated. + * Reads upstream-skills.json from the project root and compares + * the pinned content with the current upstream content. + * + * Exit codes: + * 0 - All skills are up to date + * 1 - One or more skills have upstream changes + * 2 - Error occurred during check + */ + +import { readFile } from 'node:fs/promises'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const CONFIG_PATH = join(__dirname, '..', 'upstream-skills.json'); + +interface UpstreamConfig { + source_url: string; + pinned_commit: string; + pinned_url: string; + local_path: string; + notes?: string; +} + +type UpstreamSkillsConfig = Record; + +async function fetchContent(url: string): Promise { + const response = await fetch(url); + if (!response.ok) { + throw new Error( + `Failed to fetch ${url}: ${response.status} ${response.statusText}`, + ); + } + return response.text(); +} + +async function checkSkill( + name: string, + config: UpstreamConfig, +): Promise<{ name: string; changed: boolean; error?: string }> { + try { + // Fetch pinned version + console.log(` Fetching pinned version from: ${config.pinned_url}`); + const pinnedContent = await fetchContent(config.pinned_url); + + // Fetch current version from default branch + console.log(` Fetching current version from: ${config.source_url}`); + const currentContent = await fetchContent(config.source_url); + + if (pinnedContent !== currentContent) { + return { + name, + changed: true, + }; + } + + return { + name, + changed: false, + }; + } catch (error) { + return { + name, + changed: false, + error: error instanceof Error ? error.message : String(error), + }; + } +} + +async function main(): Promise { + console.log('Checking upstream skill files for updates...\n'); + + let config: UpstreamSkillsConfig; + try { + const configContent = await readFile(CONFIG_PATH, 'utf-8'); + config = JSON.parse(configContent); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + console.log('No upstream-skills.json found. Nothing to check.'); + process.exit(0); + } + throw error; + } + + const skillNames = Object.keys(config); + if (skillNames.length === 0) { + console.log('No upstream skills configured. Nothing to check.'); + process.exit(0); + } + + const results: Array<{ name: string; changed: boolean; error?: string }> = []; + + for (const skillName of skillNames) { + console.log(`Checking skill: ${skillName}`); + const result = await checkSkill(skillName, config[skillName]); + results.push(result); + console.log(); + } + + // Report results + console.log('=== Results ===\n'); + + const errors = results.filter((r) => r.error); + const changed = results.filter((r) => r.changed && !r.error); + const upToDate = results.filter((r) => !r.changed && !r.error); + + if (upToDate.length > 0) { + console.log('Up to date:'); + for (const r of upToDate) { + console.log(` - ${r.name}`); + } + console.log(); + } + + if (errors.length > 0) { + console.log('Errors:'); + for (const r of errors) { + console.log(` - ${r.name}: ${r.error}`); + } + console.log(); + } + + if (changed.length > 0) { + console.log('UPSTREAM CHANGES DETECTED:'); + for (const r of changed) { + console.log(` - ${r.name}`); + } + console.log(); + console.log( + 'Please review the upstream changes and update the local skill files if needed.', + ); + console.log( + 'After updating, update the pinned_commit in upstream-skills.json to the new commit hash.', + ); + process.exit(1); + } + + if (errors.length > 0) { + process.exit(2); + } + + console.log('All skills are up to date with their upstream sources.'); + process.exit(0); +} + +main().catch((error) => { + console.error('Fatal error:', error); + process.exit(2); +}); diff --git a/skills/postgis/SKILL.md b/skills/postgis/SKILL.md new file mode 100644 index 0000000..6e8ec91 --- /dev/null +++ b/skills/postgis/SKILL.md @@ -0,0 +1,67 @@ +--- +name: postgis-skill +description: PostGIS-focused SQL tips, tricks and gotchas. Use when in need of dealing with geospatial data in Postgres. +--- + +## Documentation + +- Make sure every create statement or CTE has descriptive comment `--` in front of it. +- Write enough comments so you can deduce what was a requirement in the future and not walk in circles. +- Every feature needs to have comprehensive up-to-date documentation near it. + +## Style + +- PostGIS functions follow their spelling from the manual (`st_segmentize` -> `ST_Segmentize`). +- SQL is lowercase unless instructed otherwise. +- Values in databases and layers should be absolute as much as possible: store "birthday" or "construction date" instead of "age". +- Do not mix tabs and spaces in code. +- Add empty lines between logical blocks. +- Format the code nicely and consistently. +- Call geometry column `geom`; geography column `geog`. + +## Indexing + +- Create brin for all columns when creating large table that will be used for ad-hoc queries. +- If you have cache table that has a primary key, it makes sense to add values into `including` on same index for faster lookup. + +## Debugging + +- Make sure that error messages towards developer are better than just "500 Internal server error". +- Don't stub stuff out with insane fallbacks (like lat/lon=0) - instead make the rest of the code work around data absence and inform user. +- SQL files should to be idempotent: drop table if exists + create table as; add some comments to make people grasp queries faster. +- Create both "up' and "down/rollback" migration when creating new migrations for ease of iteration. +- Check `select postgis_full_version();` to see if all upgrades happened successfully. +- Don't run one SQL file from other SQL file - this quickly becomes a mess with relative file paths. + +## Raster + +- Do not work with GDAL on the filesystem. Import things into database and deal with data there. + +## SQL gotchas + +- `sum(case when A then 1 else 0 end)` is just `count() filter (where A)` +- `row_number() ... = 1` can likely be redone as `order by + limit 1` (possibly with `distinct on` or `lateral`) +- `exists(select 1 from ...)` is just `exists(select from ...)` +- `tags ->> 'key' = 'value'` is just `tags @> '{"key": "value"}` - works faster for indexes +- you can't just create ordered table and then rely on it to be ordered on scan without `order by` + +## PostGIS gotchas + +- Do not use geometry typmod unless requested (things like `geometry(multilinestring, 4326)`) - use plain `geometry` or `geography` instead. This removes clutter of `ST_Multi` and errors via `ST_SetSRID`. +- `ST_UnaryUnion(ST_Collect(geom))` is just `ST_Union(geom)` +- `ST_Buffer(geom, 0)` should be `ST_MakeValid(geom)` +- `select min(ST_Distance(..))` should be `select ST_Distance() ... order by a <-> b limit 1` to enable knn gist +- `order by ST_Distance(c.geog, t.geog)` should be `order by c.geog <-> t.geog` +- `ST_UnaryUnion` is a sign you're doing something wrong +- `ST_MakeValid` is a sign you're doing something wrong on the previous step +- be extra attintive when calling `ST_SetSRID`: check the actual projection of input data, check if it can be set correctly during input (`ST_GeomFromGeoJSON`, `EWKT`-style `SRID=4326;POINT(...`, `EWKB` allow that). Check if `ST_Transform` is needed instead. +- when looking for relation between point and polygon, prefer `ST_Intersects` to other topology predicates +- when generating complex geometry by walking raster or grid, may make sense to `ST_Simplify(geom, 0)` +- to generate neighbourhoods of predictable size, use `ST_ClusterKMeans` with k=2 and `max_radius` set to your distance. +- use `ST_AsEWKB` for binary representation instead of `ST_AsWKB` to keep SRID. +- Choosing projection: + SRID=4326 (2D longlat) when input or output is longitude and latitude and coordinate value is to be shown to user. + SRID=3857 (2D Spherical Mercator) when output will be shown on web map, ST_AsMVT, or 2D KNN requests of short distance are to be executed. + SRID=4978 (3D XYZ) when performing internal computations, line-of-sight, clustering and averaging across antimeridian. Beware: only use 3D-aware operations, ST_Force3DZ on 2D CRS data before calling ST_Transform to it. +- Instead of using `ST_Hexagon` / `ST_HexagonGrid` use `h3` extension. +- When you know the data is going to be dumped in binary form, gzipped and moved around, consider using `ST_QuantizeCoordinates` if precision is known. diff --git a/upstream-skills.json b/upstream-skills.json new file mode 100644 index 0000000..bcc1881 --- /dev/null +++ b/upstream-skills.json @@ -0,0 +1,11 @@ +{ + "postgis": { + "source_url": "https://gitea.osgeo.org/postgis/postgis/raw/branch/master/doc/SKILL.md", + "pinned_commit": "2cdf739b02ec00ff6b784e15ed8d3d87bf3f788f", + "pinned_url": "https://gitea.osgeo.org/postgis/postgis/raw/commit/2cdf739b02ec00ff6b784e15ed8d3d87bf3f788f/doc/SKILL.md", + "local_path": "skills/postgis/SKILL.md", + "author": "Darafei Praliaskouski", + "author_url": "https://gitea.osgeo.org/komzpa", + "thanks": "Thank you to Darafei Praliaskouski for creating and maintaining the PostGIS SKILL.md guide." + } +} From b89a807e09fb66150cffeec54432376c3979bb5c Mon Sep 17 00:00:00 2001 From: Matvey Arye Date: Mon, 5 Jan 2026 13:27:20 +0100 Subject: [PATCH 2/2] Remove general SQL advice from PostGIS skill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep only PostGIS-specific guidance, removing generic documentation, indexing, and SQL tips that are already covered by other skills. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- skills/postgis/SKILL.md | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/skills/postgis/SKILL.md b/skills/postgis/SKILL.md index 6e8ec91..f976059 100644 --- a/skills/postgis/SKILL.md +++ b/skills/postgis/SKILL.md @@ -3,48 +3,21 @@ name: postgis-skill description: PostGIS-focused SQL tips, tricks and gotchas. Use when in need of dealing with geospatial data in Postgres. --- -## Documentation - -- Make sure every create statement or CTE has descriptive comment `--` in front of it. -- Write enough comments so you can deduce what was a requirement in the future and not walk in circles. -- Every feature needs to have comprehensive up-to-date documentation near it. - ## Style - PostGIS functions follow their spelling from the manual (`st_segmentize` -> `ST_Segmentize`). - SQL is lowercase unless instructed otherwise. -- Values in databases and layers should be absolute as much as possible: store "birthday" or "construction date" instead of "age". -- Do not mix tabs and spaces in code. -- Add empty lines between logical blocks. -- Format the code nicely and consistently. - Call geometry column `geom`; geography column `geog`. -## Indexing - -- Create brin for all columns when creating large table that will be used for ad-hoc queries. -- If you have cache table that has a primary key, it makes sense to add values into `including` on same index for faster lookup. - ## Debugging -- Make sure that error messages towards developer are better than just "500 Internal server error". - Don't stub stuff out with insane fallbacks (like lat/lon=0) - instead make the rest of the code work around data absence and inform user. -- SQL files should to be idempotent: drop table if exists + create table as; add some comments to make people grasp queries faster. -- Create both "up' and "down/rollback" migration when creating new migrations for ease of iteration. - Check `select postgis_full_version();` to see if all upgrades happened successfully. -- Don't run one SQL file from other SQL file - this quickly becomes a mess with relative file paths. ## Raster - Do not work with GDAL on the filesystem. Import things into database and deal with data there. -## SQL gotchas - -- `sum(case when A then 1 else 0 end)` is just `count() filter (where A)` -- `row_number() ... = 1` can likely be redone as `order by + limit 1` (possibly with `distinct on` or `lateral`) -- `exists(select 1 from ...)` is just `exists(select from ...)` -- `tags ->> 'key' = 'value'` is just `tags @> '{"key": "value"}` - works faster for indexes -- you can't just create ordered table and then rely on it to be ordered on scan without `order by` - ## PostGIS gotchas - Do not use geometry typmod unless requested (things like `geometry(multilinestring, 4326)`) - use plain `geometry` or `geography` instead. This removes clutter of `ST_Multi` and errors via `ST_SetSRID`.