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
70 changes: 70 additions & 0 deletions .github/workflows/skills-paths.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: Skills Paths

# Why this is its own workflow instead of a step in `ci.yml` or `lint.yml`: this
# gate's ENTIRE scan surface is markdown (`skills/**`, guide prose), and both of
# those workflows list `'**/*.md'`, `content/**` and `docs/**` under the
# `paths-ignore` of their `push` trigger, with no per-job path filter available in
# GitHub Actions. A push that only edits a guide would therefore start neither —
# and editing only a guide is the single most likely way a stated path goes dead.
#
# This is the fourth instance of the shape in this repo, and the reasoning is
# borrowed rather than invented: `docs-links.yml`'s header records that the link
# check spent from #3213 to #3448 inside `ci.yml`'s `docs` job, unable to see the
# one class of PR most likely to break a link, and `control-bytes.yml`'s header
# names the consequence — a gate that cannot see a markdown-only change
# "rebuilds the hole it exists to close". `changeset-guard.yml` is the third.
#
# Hence: no `paths` and no `paths-ignore` here, deliberately.
# `scripts/__tests__/check-skills-paths.test.ts` fails if either is ever added,
# and fails too if a second workflow starts running the same script — one gate,
# one home.
#
# It needs no install and no build — a checkout plus one `node` call over 18
# markdown files, a couple of seconds — so keep it that way if you add checks to
# it.

on:
pull_request:
branches: [main, develop]
push:
branches: [main, develop]
# Merge queue (objectui#3523 — see `ci.yml`'s trigger block for the full note
# and the measurements behind it). A required check that does not report on a
# queue build stalls the queue until the ruleset's 60-minute timeout fails it,
# so an unfiltered gate that could become required subscribes here from the
# start. `types:` is named although `checks_requested` is currently the only
# activity type GitHub defines for `merge_group`.
merge_group:
types: [checks_requested]
workflow_dispatch:

concurrency:
group: skills-paths-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
skills-paths:
name: Skill Guide Path Check
runs-on: ubuntu-latest
timeout-minutes: 5

steps:
- name: Checkout code
uses: actions/checkout@v7

- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22.x'

# The guides under `skills/` are a direct input to every agent that writes
# code here, and their prose gives in-repo paths as coordinates. A dead one
# produces no compile error — just "file not found" from a Read, and a
# wasted lap re-locating a symbol that does exist (#3713 and #3730 were
# 13+ of these in one guide, both rounds found by eye). Reads the checkout
# and nothing else, so no install is required.
- name: Check paths stated in the skill guides
run: node scripts/check-skills-paths.mjs
57 changes: 57 additions & 0 deletions content/docs/guide/ci-cd-pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ one has its own section below.
| `changeset-presence.yml` | Changeset Declaration | PR to `main`, `develop` — **no path filter**; merge-queue builds | **Yes** — when a released package's `src/` changed and no changeset was added |
| `control-bytes.yml` | Control Byte Scan | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** |
| `docs-links.yml` | Internal Docs Link Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** |
| `skills-paths.yml` | Skill Guide Path Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a path stated in a `skills/` guide does not exist |
| `performance-budget.yml` | Bundle Analysis | Push / PR touching `packages/**`, `apps/console/**`, `pnpm-lock.yaml` | **Yes** — the console entry gzip budget |
| `live-e2e.yml` | Live E2E (informational) | PR to `main`, `develop` (code paths); nightly cron `30 6 * * *`; manual | No — informational lane, `continue-on-error` |
| `labeler.yml` | Auto Label PRs | PR `opened`, `synchronize`, `reopened` | No |
Expand Down Expand Up @@ -453,6 +454,62 @@ page it points at has moved or been renamed — fix the link, or restore the tar
checked as *routes*, so `/docs/guide/foo` is what belongs in the markdown, not
`content/docs/guide/foo.md`. Run it locally with `pnpm docs:check-links`.

## Skill Guide Paths (`skills-paths.yml`)

**Triggers:** Push and PR to `main`/`develop`, merge-queue builds, plus manual dispatch — with **no
path filter at all**, for the same reason as the two sections above: this gate's entire scan surface
is markdown, and `ci.yml` still lists `'**/*.md'` under the `paths-ignore` of its `push` trigger. It
appears in the checks list as **Skill Guide Path Check**.

Runs `scripts/check-skills-paths.mjs`, which reads every markdown file under `skills/` and asks, of
each in-repo path the prose states inside a backtick code span, whether it exists on disk. Those
guides are a direct input to every agent that writes code in this repository, and their prose gives
paths as coordinates.

**Why a dead coordinate costs more than its size suggests:** the symbol named next to it is usually
real and only the location is wrong, so nobody gets a compile error — an agent gets "file not found"
from a `Read`, assumes its own search was clumsy, and spends a full lap re-locating something the
guide claimed to have located for it. Two rounds were found by eye while reading:
[#3713](https://github.com/objectstack-ai/objectui/issues/3713) (PR #3729) and
[#3730](https://github.com/objectstack-ai/objectui/issues/3730) (PR #3734), the second one 13 real
symbols at coordinates that did not exist. It also recurs by construction — the app-shell extraction
commits moved code with nothing anywhere to say the guides had gone stale
([#3735](https://github.com/objectstack-ai/objectui/issues/3735)).

**What counts as a stated path:** a backtick span that opens with one of five top-level directories
(`apps/`, `packages/`, `examples/`, `scripts/`, `content/`) and contains no whitespace. Three
exclusions, each a *rule* rather than an exemption, because none of them claims that a file exists:

| Excluded | Example in the guides today | Why |
|---|---|---|
| whitespace inside the span | a `grep -rn … packages/app-shell/src` self-check command line | prose, a command line or a type — not a path |
| glob or placeholder segment | the protected-primitive glob under `packages/components/src/ui`, a schema path with a placeholder domain segment | a shape, not a location; `existsSync` on it would mean nothing |
| fenced code blocks | a `bash` block that creates a file | a worked example may legitimately name a file the reader is about to create |

Measured on `main@6422aa891`: 18 guide files, 91 candidate spans, 5 of them patterns — **86 stated
paths, of which 85 resolve**.

**The one exemption, and why it cannot rot.** `scripts/skills-path-baseline.json` lists paths a guide
states *deliberately as absent*. Today there is exactly one: the Key contexts section of
`console-development.md` exists to correct a recurring wrong guess and says there is no
`apps/console/src/context/` directory at all. That entry is a ratchet, red in **both** directions —
if the path ever appears on disk the gate fails and names it (the sentence has become false), and if
the scan stops meeting the entry the gate fails too (the prose was rewritten, so the entry is dead
weight). Entries are keyed by file and token, never by line number, because guide prose moves
constantly.

**Scope, stated so it is not mistaken for an oversight.** `content/docs/**` carries backtick paths
too and is **not** scanned here. Widening a scan surface arrives with its own batch of red to clear,
which `check-doc-links.mjs` learned three times over (#3479, #3490, #3545) — measure it first, in its
own change. The five-prefix list is the same kind of decision: adding this repository's other five
top-level directories was measured at +2 candidates and 0 new red, so it is cheap, but it stays
deliberate rather than assumed.

**If it fails:** it prints every `file:line — token`. Fix the prose. Add a baseline entry only when
the sentence's whole point is that the path does not exist. Run it locally with
`pnpm check:skills-paths`, or `node scripts/check-skills-paths.mjs --list` to see every candidate and
how it was classified.

## Link Checking (`check-links.yml`)

**Trigger:** Weekly cron (`17 4 * * 0` — Sundays, off the top of the hour, when the scheduled-run
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"check:control-bytes": "node scripts/check-control-bytes.mjs",
"check:i18n-keys": "node scripts/check-i18n-call-site-keys.mjs",
"check:i18n-drift": "node scripts/check-i18n-en-drift.mjs",
"check:skills-paths": "node scripts/check-skills-paths.mjs",
"cli": "node packages/cli/dist/cli.js",
"objectui": "node packages/cli/dist/cli.js",
"create-plugin": "node packages/create-plugin/dist/index.js",
Expand Down
Loading
Loading