diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2911a4e8be..8ee62d1ede 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -455,6 +455,21 @@ jobs: - name: Quick-reference section counts match their tables run: pnpm check:quick-reference-counts + # #9604: the runtime-services chapter's pages are enumerated in three + # hand-written places (meta.json "pages", the chapter list in + # runtime-services/index.mdx, the services.* table in kernel/index.mdx) and + # nothing read any of them -- check:docs-audit-scope derives WHICH pages the + # accuracy audit covers, never whether an index enumerates them. #9604 found + # services.sms with a page, a meta.json entry, a registered slot and a + # canonical-source row, missing from BOTH index lists; #9588 was the same + # page drifting on a different line. The pages on disk are the source of + # truth and the gate holds all three enumerations (and the chapter list's + # order) to them. It lives in this job with the other docs guards: the edit + # that breaks it is a docs edit, so a packages/** paths filter would blind + # it to its own failure mode. + - name: Runtime-services indexes enumerate the chapter's real pages + run: pnpm check:runtime-services-index + # #3723 ADR anchors: code an accepted ADR governs must keep naming it. # That incident reversed three accepted ADRs with a patch-level changeset, # and the mechanism was simply that the edited file never mentioned them — diff --git a/content/docs/kernel/index.mdx b/content/docs/kernel/index.mdx index f58127586e..9141f5d6b7 100644 --- a/content/docs/kernel/index.mdx +++ b/content/docs/kernel/index.mdx @@ -18,6 +18,7 @@ The kernel is ObjectStack's runtime: it loads your metadata artifact, hosts plug | [`services.data`](/docs/kernel/runtime-services/data-service) | stable | CRUD and queries with the caller's permission context | | [`services.sharing`](/docs/kernel/runtime-services/sharing-service) | stable | `buildReadFilter`, `canEdit`, `canDelete`, `canManageShares`, `grant`/`revoke`, `listShares` | | [`services.email`](/docs/kernel/runtime-services/email-service) | stable | `send`, `sendTemplate` | +| [`services.sms`](/docs/kernel/runtime-services/sms-service) | stable | Outbound SMS delivery through pluggable providers (Aliyun SMS, Twilio) | | [`services.queue`](/docs/kernel/runtime-services/queue-service) | stable | Background work and queues | | [`services.settings`](/docs/kernel/runtime-services/settings-service) | stable | App/environment settings | | [`services.storage`](/docs/kernel/runtime-services/storage-service) | stable | File storage | diff --git a/content/docs/kernel/runtime-services/index.mdx b/content/docs/kernel/runtime-services/index.mdx index f070f8ca23..df2b76e449 100644 --- a/content/docs/kernel/runtime-services/index.mdx +++ b/content/docs/kernel/runtime-services/index.mdx @@ -21,6 +21,7 @@ This chapter documents the runtime `services.*` APIs used in hook/action/flow/pl - `services.audit` - `services.queue` - `services.email` +- `services.sms` - `services.settings` - `services.storage` diff --git a/package.json b/package.json index 9d1d378aac..7b4ce6120a 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "check:docs-image-tag-sync": "node scripts/sync-docs-image-tags.mjs --self-test", "check:role-word": "node scripts/check-role-word.mjs --self-test && node scripts/check-role-word.mjs", "check:quick-reference-counts": "node scripts/check-quick-reference-counts.mjs --self-test && node scripts/check-quick-reference-counts.mjs", + "check:runtime-services-index": "node scripts/check-runtime-services-index.mjs --self-test && node scripts/check-runtime-services-index.mjs", "check:skill-frame-sync": "node scripts/check-skill-frame-sync.mjs --self-test && node scripts/check-skill-frame-sync.mjs", "check:skill-frame-freshness": "node scripts/check-skill-frame-freshness.mjs --self-test && node scripts/check-skill-frame-freshness.mjs", "check:skill-compatibility": "node scripts/check-skill-compatibility-version.mjs --self-test && node scripts/check-skill-compatibility-version.mjs", diff --git a/scripts/check-runtime-services-index.mjs b/scripts/check-runtime-services-index.mjs new file mode 100644 index 0000000000..689dc761b7 --- /dev/null +++ b/scripts/check-runtime-services-index.mjs @@ -0,0 +1,261 @@ +#!/usr/bin/env node +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// check-runtime-services-index (#9604) -- hold the runtime-services chapter's +// two INDEX lists to the pages that actually exist. +// +// node scripts/check-runtime-services-index.mjs +// node scripts/check-runtime-services-index.mjs --self-test # verify the checker itself +// +// ## What it guards +// +// `content/docs/kernel/runtime-services/` publishes one `-service.mdx` +// page per documented `services.` accessor, and THREE hand-written places +// claim to enumerate them: +// +// 1. `runtime-services/meta.json` -> `pages` (chapter nav order) +// 2. `runtime-services/index.mdx` -> "This chapter documents ..." bullets +// 3. `kernel/index.mdx` -> the `services.*` table +// +// None of the three is generated, so each drifts from the tree one edit at a +// time, and nothing reads them: `check:docs-audit-scope` derives WHICH pages the +// docs-accuracy audit covers, never whether an index enumerates them. #9604 +// measured the result -- `services.sms` had a page, a `meta.json` entry, a +// registered slot (`sms-plugin.ts:181`) and a canonical-source row, and was +// still missing from BOTH index lists. It shipped that way and every gate was +// green. #9588 was the same page drifting on a different line. +// +// Nothing breaks at runtime; the cost is that an index page's whole job is to be +// a trustworthy map. A reader who does not find SMS in the list concludes the +// chapter has no SMS page. `content/docs/` is also the corpus humans and AIs +// copy from, so a short list is read as a fact about the platform's surface. +// Declared = enforced. +// +// ## What "derived" means here +// +// The pages on disk are the source of truth -- they are the thing a reader can +// actually open. Each page also has to declare its own accessor +// (`title: services.` matching its filename), which is checked first: it +// is the premise the other three comparisons rest on, so a page that lies about +// its own name must go red here rather than silently redefine the expected set. +// +// Order is enforced too, not just membership. The chapter list currently +// follows `meta.json`'s `pages` order exactly, and that convention is the only +// thing that tells the next author WHERE a new bullet goes. Membership-only +// checking would have accepted `services.sms` appended at the end, next to a +// list whose order encodes the nav -- so the gate keeps the answer mechanical. +// +// ## Deliberately NOT checked: the "Source of Truth" list +// +// `index.mdx` carries a fourth list -- `-