Repository navigation
services.security is documented nowhere under content/docs/, yet the runtime-services chapter advertises a canonical source for it #9629
Description
Split out of #9604, which fixed the services.sms half of that card and was explicitly told not to guess the Security half. This card records the measurement so a maintainer can decide.
What was measured
#9604 asked which of three outcomes applies to the Security row in content/docs/kernel/runtime-services/index.mdx: the accessor is documented elsewhere (mis-filed row), documented nowhere (bigger gap), or internal and not meant to be public (drop the row). Measured against main at e8dba8a8a:
| probe | result |
|---|---|
grep -rn 'services\.security' content/docs/ |
0 hits |
grep -rn 'contracts/security-service' content/docs/ |
1 hit — runtime-services/index.mdx:39, the row itself |
security-service.mdx anywhere under content/docs/ |
does not exist |
ISecurityService in content/docs/ |
kernel/contracts/index.mdx:44 (interface table) and a v17 release note |
getService('security') in content/docs/ |
permissions/explain.mdx:53, spelled kernel.getService('security') |
So the answer is outcome 2: documented nowhere. The slot is real and registered — packages/plugins/plugin-security/src/security-plugin.ts:1157 runs ctx.registerService('security', registeredSecurityService), and packages/spec/src/contracts/security-service.ts:18 names that registration in its own header — but no page in content/docs/ introduces the services.security accessor.
The two near-misses are worth naming because neither closes the gap:
kernel/contracts/index.mdxdocuments the interfaceISecurityServicein the contracts chapter. That is the "what must an implementation provide" surface, not the "how do I call it" accessor surface the runtime-services chapter owns.permissions/explain.mdxshowskernel.getService('security')for one specific task (access explanation). It is a task recipe in another chapter, not an accessor reference.
Why this is larger than the row
The Source-of-Truth list states its own purpose: "Each page links the canonical TypeScript source used to derive signatures." For the Security row there is no page, so the sentence is false for that row — a reader following it lands on a contract file with nothing explaining the accessor. The chapter is advertising a canonical source for a service it never introduces.
Needs a maintainer decision
The three outcomes are mutually exclusive and only one is a docs edit:
- Give it a page —
security-service.mdxin the chapter, plusmeta.json, the chapter list and thekernel/index.mdxtable. Correct ifservices.securityis meant to be a public runtime accessor. Largest option; the contract is wide (getReadFilter,getReadableFields,canExport,checkAuthoredRowWrite,resolvePermissionSetNames,explain, ...). - Move the row — cheapest, but the measurement above says there is no destination page documenting the accessor, so this is only viable together with deciding which chapter should own it (
permissions/is the nearest neighbour). - Drop the row — right if the accessor is internal and callers are meant to reach security through
permissions/docs andkernel.getServicerather than a documentedservices.*slot.
Which one is right depends on whether services.security is intended to be a documented runtime accessor at all. That is a product-surface call, not a docs cleanup.
Note on the gate
PR for #9604 adds pnpm check:runtime-services-index, which holds the chapter list, meta.json and the kernel/index.mdx table to the pages on disk. It deliberately leaves the Source-of-Truth list out of scope, precisely so it does not pre-judge this card — an allowlist entry for Security would be one of the three answers above, encoded in a gate. Once this is decided, extending that gate to the Source-of-Truth list is the natural follow-up and is a small change.
Activity
os-support-ai commentedon Aug 18, 2026
Triage: kept in the decision inbox (
needs-user-decision,domain:devx); removed the contradictoryfindinglabel (a card cannot be both awaiting first grading and already classified as a decision). Four-prism block, since the card lacked one:services.securityanywhere incontent/docs/, and the one real usage shown to readers is thepermissions/explaintask recipe, which already works without an accessor page.getReadFilter,checkAuthoredRowWrite, …) invites app-authoring agents to call the security service directly instead of going through the permission surfaces; keeping the accessor undocumented keeps the narrow path the obvious path.security-service.mdxpage is a permanent doc-maintenance obligation for a wide, evolving contract with no reader demand today.Recommendation: drop the row (outcome 3), and note in the row's removal commit that the interface surface stays documented in
kernel/contracts/and the task recipe inpermissions/explain.mdx. Revisit a dedicated page on first real consumer pull. Extendingcheck:runtime-services-indexto the Source-of-Truth list is the natural rider once ruled.Generated by Claude Code
docs(kernel): list services.sms in the runtime-services stability mat…
os-support-ai commentedon Aug 18, 2026
Maintainer ruling recorded (2026-08-18, live chat with the triage session
session_014v3dpZ2224xgf98dfY4rw4; authorization verbatim: 「其他接受你的建议」, accepting the recommendation presented in that session, restated as the operative ruling).Ruling: drop the row.
services.securityis ruled an internal accessor, not a publicly documented runtime accessor: theSecurityrow leaves the runtime-services Source-of-Truth list, and nosecurity-service.mdxpage is created. Task-level usage stays where it is (permissions/explain.mdxteachingkernel.getService('security')for access explanation); theISecurityServiceinterface remains documented in the contracts chapter as the implementation surface. Rationale on record: the contract is wide (getReadFilter,canExport,explain, …) and publicly documenting the accessor is a permanent maintenance obligation inviting external dependence nothing pulls on today — if a plugin-ecosystem need materializes, it is promoted then, deliberately.Follow-up in the same card: extend
check:runtime-services-indexto the Source-of-Truth list now that the answer exists (a row requires a page; the deliberate out-of-scope in #9604's gate is discharged by this ruling).Label:
needs-user-decision→pm:queue, type Task,domain:devx.Generated by Claude Code
os-steve commentedon Aug 19, 2026
Claiming this card.
session_01XqDQYVU5smx29ts9pAErjaclaude/issue-9629-security-source-rowWorking the 2026-08-18 maintainer ruling: drop the
Securityrow from the runtime-services Source-of-Truth list (internal accessor, no page created, no relocation), plus the in-card follow-up of extendingcheck:runtime-services-indexto hold that list.Generated by Claude Code
os-steve commentedon Aug 19, 2026
{ "issue": 9629, "status": "done", "branch": "claude/issue-9629-security-source-row", "pr": "https://github.com/objectstack-ai/objectstack/pull/9861", "premise_still_valid": true, "summary": "H1 (swept first): of the six enumerations of this chapter, exactly ONE named security -- the Source-of-Truth row at index.mdx:39. meta.json pages, the chapter list, the kernel/index.mdx services.* table, versioning.mdx's Current Matrix and the Stability Legend all already omitted it (the Legend enumerates levels, not services), so the drop is the whole edit and leaves nothing self-contradicting; grep -rn 'services\\.security' content/docs/ was 0 before and after, and 'contracts/security-service' went 1 -> 0. Row dropped per the ruling: no page created, no relocation. H2: gating the list is worth it and is now built as an additive sixth claim -- membership both ways, no duplicate rows, and every row's path must exist; the price was checked first and two sub-rules were deliberately NOT built (see open_questions). H3: nothing contradicts 'internal' -- the only other in-tree teaching is permissions/explain.mdx:53's kernel.getService('security') task recipe, which the ruling explicitly preserved, and the capability is public by REST (/api/v1/security/explain) while only the services.* accessor framing is internal; not touched. H4: the drop did leave a real gap (the row was the chapter's only pointer to security), so a one-paragraph prose note took its place naming permissions/, permissions/explain and ISecurityService in kernel/contracts/ -- prose, not a list row, so no enumeration grows back.", "tests": "All at final commit 7a08cbfe2 (worktree clean, git status --porcelain empty). REVERSE VERIFICATION on the real tree, as a pair: with the Security row restored, the new gate goes RED with exactly one finding -- 'content/docs/kernel/runtime-services/index.mdx: \"Source of Truth\" row \"Security\" names no page in this chapter' (exit=1) -- while main's copy of the script run against that SAME tree is GREEN ('... (Source-of-Truth list not in scope).' exit=0). That pair is the drift, measured. Row restored with git checkout HEAD -- (no stash); tree byte-identical afterwards. GREEN at 7a08cbfe2: 'check-runtime-services-index: 8 chapter page(s) ... 8 canonical-source row(s), and 8 declared registry slot(s) vs 55 registered key(s)'; '--self-test: 36 assertions' (was 24). Gate family derived with scripts/pm/dispatch-gates.mjs from the 3 changed paths, all green: check:runtime-services-index, check:doc-anchors (251 fragment links), check:docs-audit-scope, check:docs-redirects, check:published-readme-links, check:role-word, check:cross-package-test-inputs, check:node-version, check:required-contexts, check:shard-attestation, check:workflow-status-functions, check:type-check-coverage, check:nul-bytes, spec liveness (check:variant-docs, check:empty-state, check:liveness, check:strictness-ledger), eslint on the edited script. ONE derived gate not run locally: check:type-check-debt -- it refuses --re-measure without a full built package closure (55 workspace deps) and this diff has zero TypeScript; it was pulled in only because a COMMENT-ONLY lint.yml edit matches its source glob. check:type-check-coverage (same script, no --re-measure) is green. No ablation in this card. Labels read back after an additive POST: documentation, ci/cd, size/m, skip-changeset.", "open_questions": [ { "question": "NON-BLOCKING, already shipped one way: should the chapter say anything at all where the row was? The drop removes the chapter's only pointer to security, so H4's gap is real -- but a replacement sentence is a judgement the ruling did not spell out.", "options": [ "A (shipped) one prose paragraph: 'services.security is not on this list ... internal accessor ... no page', pointing at /docs/permissions, /docs/permissions/explain and /docs/kernel/contracts", "B drop the row silently and say nothing" ], "recommendation": "A, because it is prose rather than a list row (so no enumeration regrows and the new gate cannot see it), it states the accessor is deliberately absent -- the opposite of advertising it -- and it is what stops the next docs author re-adding the row that already survived two deliberate passes. Trivially revertible to B by deleting the paragraph if you want the chapter silent." }, { "question": "NON-BLOCKING: two sub-rules of the new gate were deliberately NOT built. Confirm the omissions.", "options": [ "row ORDER is not held -- unlike the chapter list and the matrix, this list does not follow meta.json nav order today (Data, Sharing, Queue, Email, SMS, Storage, Settings, Audit vs nav data, sharing, audit, queue, email, sms, settings, storage) and never claimed to; holding it would force a re-sort of a published list to a convention it never had", "whether a path is the RIGHT canonical source is not held -- 3 of the 8 rows point outside packages/spec/src/contracts/ on purpose (Data at packages/client, Settings at service-settings, Audit at plugin-audit by #9605), so a 'contracts/ only' rule would have been false on day one" ], "recommendation": "Keep both omissions. Each is documented in the script header with its measurement, so the next author does not re-litigate them; the checkable half (the file exists) is built, the judgement half (which file is canonical) is not, and order can be added later in a docs PR that re-sorts the list on purpose." } ], "out_of_scope_findings": [] }Context the PM asked for, outside the JSON:
services.smsin the runtime-services stability matrix, and gate the matrix and every label it publishes (#9684) #9753's two self-test assertions pinned "the parser cannot reach the Security row". They are re-aimed rather than removed: the page-less row is now caught, and the assertion that it is caught as a canonical-source row — never misread as a stability claim againstversioning.mdxor the kernel table — is the half of the old pin with lasting value, and it is still pinned directly on the parser. The existing six checks are untouched (ruling 2).40fb55eea, docs(kernel): name the registry slot on every runtime-services page, and gate it #96816cf12acaf, docs(kernel): listservices.smsin the runtime-services stability matrix, and gate the matrix and every label it publishes (#9684) #97531a3336a52), plus docs(kernel): point the runtime-services index at the audit slot's real registrant #960585a067b12— which is why the script header's old objection that row labels are prose (Audit bridge) no longer applies: docs(kernel): point the runtime-services index at the audit slot's real registrant #9605 renamed that label toAudit, and today every label is the accessor.content/docs/releases/**untouched;docs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.mduntouched.Generated by Claude Code
Generated by Claude Code
docs(kernel): list services.sms in the runtime-services stability mat…