Skip to content

finding: record-view auditing has no content/docs page — the shipped compliance capability is documented only in a package README #9540

Description

@os-project-manager

Observation-class finding, recorded while completing #9517's residual half (documenting the read-audit surface in packages/plugins/plugin-audit/README.md). ⛔ Not a defect claim — nothing states anything false. Filing because the measurement is cheap to lose and expensive to re-take.

Measurement

Record-view auditing landed with #8992 / PR #9515 (5126e795d). On origin/main at 53fc09922, the docs corpus has no page for it:

grep -rn "readAudit\|record_views\|record-view" content/docs   # no hits

The seven content/docs pages that mention sys_audit_log at all are ui/setup-app.mdx, protocol/kernel/config-resolution.mdx, kernel/runtime-services/audit-service.mdx, deployment/production-readiness.mdx, plugins/packages.mdx and two release pages. None describes the read action, the per-object opt-in, or the record-detail scope.

content/docs/capabilities/permissions.mdx:23 describes audit as "records who changed what, when, with old and new values". That is not false — it is a write-side description that has simply not been extended. I am deliberately not calling it drift.

Why it might matter, stated narrowly

The capability was built for a regulated-industry buyer, and the only place its scope is written down is a package README on npm. The scope has real edges someone evaluating coverage needs — record-detail views only, per-object opt-in, no field values recorded, system-elevated reads excluded — and a coverage question answered from the docs site alone currently gets silence rather than those edges.

⇒ Two honest dispositions, both fine, which is why this is a finding and not queued:

  • Add a docs page (or a section on an existing capability page) mirroring the README's new "Record-view auditing" section. The content already exists and is source-verified.
  • Decide the package README is the right home for a plugin-level opt-in and close this. check:affected-docs is green on the PR that documented it, so no gate believes a docs page is owed.

Refs: #9517 (the README card) · #8992 / PR #9515 (the capability) · #9534 (a separate, real mismatch on audit-service.mdx).

Activity

  1. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    First-touch grading: promoted to pm:queue, domain:devx, type Task. Ruling between the card's two dispositions: add the docs page. Rationale on the axes — the capability was built for a regulated-industry buyer and a coverage evaluation from the docs site currently gets silence on exactly the edges that matter (record-detail scope, per-object opt-in, no field values, system-elevated exclusion); the content already exists source-verified in the plugin README, so the cost is a mirror, not new research. Scope: one page (or a section on an existing capability page) under content/docs/, mirroring the README's "Record-view auditing" section; no code, no contract change.

    Size/model suggestion: S, mode:subagent.


    Generated by Claude Code

  2. os-steve commented on Aug 19, 2026

    @os-steve
    Collaborator

    Claiming this card.

    • Session: session_01XqDQYVU5smx29ts9pAErja
    • Branch: claude/issue-9540-record-view-audit-docs
    • Container & model: M, mode:subagent, model: opus

    Plan: first measure the shipped record-view auditing surface against the actual plugin-audit implementation (H1), then decide whether this is a new page in content/docs/kernel/runtime-services/, a section on an existing page, or somewhere else entirely (H3 — a page in that chapter needs a real registerService slot to satisfy the index gate's fifth check). Reporting back on this issue as a comment before the final report.

    Generated by Claude Code


    Generated by Claude Code

  3. os-steve commented on Aug 19, 2026

    @os-steve
    Collaborator
    {
      "issue": 9540,
      "status": "done",
      "branch": "claude/issue-9540-record-view-audit-docs",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/9860",
      "premise_still_valid": true,
      "summary": "H1 (shipped behaviour, from read-audit.ts / audit-plugin.ts / sys-audit-log.object.ts): a record-view row is written by ONE afterFind hook that AuditPlugin registers on exactly the objects named in readAudit.objects, filtered against the derived AUDIT_EXCLUDED_OBJECTS list (refusals warn, naming the object). A read qualifies only when it materialized exactly one record AND its predicate pinned the primary key through an AND-closure walked to depth 8; $or/$not anywhere disqualifies; list/search never qualify. The hook enqueues and returns, awaiting nothing; a batcher persists via engine.insert('sys_audit_log', rows, {context:{isSystem:true}}), flushing at maxBatchSize (50) or flushIntervalMs (2000ms), with destroy() draining the tail. The row carries action:'read', created_at = the VIEW instant (not the flush instant), user_id, object_name, record_id, tenant_id (the viewed record's organization_id, falling back to the session's), plus organization_id and actor where the columns exist; old_value/new_value stay null deliberately because afterFind runs ahead of SecurityPlugin's masking and ctx.result is pre-mask plaintext. Two declared boundaries write no row: session.isSystem === true, and a read with neither userId nor actor. Failure posture is post-PR #9750 and I documented it as it is now: buffer overflow past a fixed 10000 ceiling drops the OLDEST and warns once; a failed write loses the batch and reports once, reaching for logger.error and falling back to logger.warn (never into silence), never retried. maxBufferedEvents is NOT forwarded by AuditPlugin, so through the plugin that ceiling is fixed, not a knob -- the README's 'three keys' is right and I stated the ceiling as fixed. H3 VERDICT: this does NOT belong in content/docs/kernel/runtime-services/. That chapter's gate holds every page's declared Registry slot to a real production registerService call, and record-view auditing has no slot -- it is a constructor-installed hook. The plugin's only slot is 'audit', whose whole surface is recordAuthEvent() for login/logout. A page there would have required inventing a slot to satisfy check #5, so I did not put one there; check:runtime-services-index stays green and untouched (ruling 3 honoured). It also does not belong ON audit-service.mdx, which documents the slot -- merging a non-slot surface in re-creates the exact confusion #9534/PR #9587 fixed. The page went to content/docs/permissions/ (Permissions & Identity), the chapter capabilities/permissions.mdx already routes readers to for 'the full model' including audit, and the chapter holding field-level-security and system-context, the two pages this capability interacts with directly. I added a 3-line pointer on audit-service.mdx instead, matching the disambiguation that page already does for the settings audit sink. H2 (user-facing surface): reachable but NOT configurable on the shipped CLI path -- serve.ts:2440 auto-registers `new AuditPlugin()` with no options and there is no appAuditPluginOptions(config) helper, unlike SecurityPlugin's; zero call sites supply readAudit anywhere in packages/, examples/ or apps/. An app config's plugins array CAN supersede it, but only via an undeclared name-keyed last-wins overwrite in ObjectKernel.use (LiteKernel.use throws on the same input), so the page deliberately does not teach that; it says the opt-in is given where you compose the kernel and that os serve passes no options. Filed as #9863 and #9864. H4 (docs-audit scope): check:docs-audit-scope went red on the new page exactly as predicted (179 -> 180) and I regenerated the block with the gate's own --write; the diff is one line. NOTE A CONFLICT for the PM: that block lives in .claude/workflows/docs-accuracy-audit.js, which ruling 5 puts off-limits, while H4 asks for the scope to be updated. I resolved it by precedent rather than preference -- it is a generated artifact the gate rejects if hand-edited, and every prior page-adding PR regenerates it identically (e2b3c831e, f049f7fd4, ece873001, the last adding system-context.mdx to this same chapter). Called out in the PR body so it can be reverted in one line if the ruling was meant to cover generated blocks too. One unrelated observation: a bot appended a second signature footer to the PR body after creation; I left it alone per basic rule 2.",
      "tests": "Docs-only card: no package build/test applies, so no heavy-verify lock was taken. Gate union re-run AFTER the final commit and quoted from that run -- HEAD ae67ad006, all 15 PASS: doc-anchors, doc-authoring, docs-audit-scope, docs-redirects, published-readme-links, role-word, runtime-services-index, pm-governed-merges, skill-frame-sync, cross-package-test-inputs, nul-bytes, and spec: empty-state, liveness, strictness-ledger, variant-docs (UNION_FAIL=0 at ae67ad006). Gate list was DERIVED from the actual changed paths via `node scripts/pm/dispatch-gates.mjs`, not recalled -- that derivation added pm-governed-merges, skill-frame-sync, doc-formula-expressions and the four spec-liveness gates off the .claude/ and content/docs/ triggers, and I ran everything it named that my diff touches. Baselines before editing were green too, so the greens are not pre-existing-red masking: check-doc-anchors 251 fragments across 398 files; check-runtime-services-index '8 chapter pages ... all enumerations agree'; check-audit-scope '179 hand-written doc(s)' -> after the change '180 hand-written doc(s) (+1 added, -0 removed)'. Ruling 3 verified specifically: check:runtime-services-index PASS both before and after the audit-service.mdx pointer, which changes prose only and touches no enumeration, Registry slot bullet or Stability label. Control-byte discipline: targeted `grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]'` over all six changed files returned no matches, and check:nul-bytes scanned 6282 files clean. No ablation applies to a docs card, so none is claimed.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #9863: record-view auditing has no configuration path on the `os serve` boot path -- the CLI registers AuditPlugin with no options and there is no appAuditPluginOptions(config) counterpart to the SecurityPlugin helper",
        "filed as #9864: duplicate plugin registration throws on LiteKernel and silently overwrites on ObjectKernel -- a fourth instance of the documented two-kernel semantic split (#5170 / #5282 family), and the undeclared behaviour #9863's half-path depends on",
        "filed as #9865: sys_audit_log's shipped `record_views` list view declares an `ip_address` column that a `read` row can never populate -- ip_address is stamped only by auth-event-audit.ts, so the column is empty for every row that view can show"
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions