Skip to content

Redesign homepage based on shadcn and fumadocs specifications - #39

Merged
huangyiirene merged 4 commits into
mainfrom
copilot/redesign-homepage-based-on-shacn-fumadocs
Jan 20, 2026
Merged

huangyiirene merged 4 commits into
mainfrom
copilot/redesign-homepage-based-on-shacn-fumadocs

Conversation

Copilot AI commented Jan 20, 2026 •

Copy link
Copy Markdown
Contributor

Homepage Redesign - Based on shadcn & fumadocs Specifications

Goal

重新开发首页,基于 shadcn 和 fumadocs 规范 (Redevelop homepage based on shadcn and fumadocs specifications)

Implementation Checklist

  • Setup Phase

    • Install shadcn/ui dependencies (class-variance-authority, clsx, tailwind-merge)
    • Create components directory structure
    • Set up cn utility function for className merging
    • Update tailwind.config.js with shadcn theme configuration
  • Component Development

    • Create reusable shadcn-style components (Button, Card, Badge)
    • Implement enhanced hero section with modern animations
    • Redesign feature cards with better visual hierarchy
    • Improve code preview component with syntax highlighting
    • Enhance persona cards with hover effects
  • Homepage Enhancement

    • Update hero section with improved typography and spacing
    • Add gradient backgrounds and modern visual effects
    • Implement responsive grid layouts
    • Add smooth animations and transitions
    • Improve color scheme alignment with fumadocs theme
  • Testing & Validation

    • Test responsive design on different screen sizes
    • Verify fumadocs integration
    • Ensure i18n translations work correctly (EN/CN)
    • Production build successful
    • TypeScript type checking passed
    • Code review feedback addressed
    • Fix light mode code visibility issues
    • Take screenshots of new design

Screenshots

Desktop View (English)

Before:

After (Dark Mode):

After (Light Mode):

Mobile View (375px)

Chinese Version (中文)

Key Improvements

✨ Modern shadcn/ui Component System

  • Implemented reusable Button, Card, and Badge components
  • Used class-variance-authority for flexible component variants
  • Added cn utility for clean className merging

🎨 Enhanced Visual Design

  • Better spacing and typography hierarchy
  • Improved color scheme with fumadocs theme variables
  • Gradient backgrounds and modern visual effects
  • Enhanced code preview with theme-aware syntax highlighting

🚀 Smooth Animations & Interactions

  • Fade-in and slide animations on page load
  • Hover effects with scale transforms
  • Smooth transitions on all interactive elements
  • Pulsing badge indicator

📱 Responsive Design

  • Mobile-first approach with breakpoints
  • Flexible grid layouts (1, 2, 3 columns)
  • Stack layout on mobile devices
  • Optimized typography for different screen sizes

🌐 i18n Support

  • Full support for English and Chinese
  • All components respect language context
  • Consistent design across languages

🎯 Fumadocs Best Practices

  • Uses fumadocs theme variables (--fd-*)
  • Integrates seamlessly with fumadocs-ui layouts
  • Follows fumadocs design patterns
  • Compatible with fumadocs navigation and search

🌓 Light/Dark Mode Support

  • Theme-aware syntax highlighting in code previews
  • Proper contrast in both light and dark modes
  • Colors adapt automatically to theme changes
  • Fixed light mode code visibility with theme-responsive color values

Technical Details

New Dependencies:

  • class-variance-authority - Component variant management
  • clsx - Conditional className utilities
  • tailwind-merge - Tailwind class merging

New Components:

  • components/ui/button.tsx - Reusable button component
  • components/ui/card.tsx - Card component system
  • components/ui/badge.tsx - Badge component
  • components/feature-card.tsx - Feature showcase cards
  • components/persona-card.tsx - Persona profile cards
  • components/code-preview.tsx - Enhanced code preview with theme-aware colors
  • components/hero-section.tsx - Hero section component
  • lib/utils.ts - Utility functions

Updated Files:

  • app/[lang]/page.tsx - Refactored to use new components
  • tailwind.config.js - Enhanced with shadcn theme config
  • app/global.css - Added CSS variables and utilities
Original prompt

基于shacn和fumadocs规范,重新开发首页


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

@vercel

vercel Bot commented Jan 20, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
spec Ready Ready Preview, Comment Jan 20, 2026 9:37am

Request Review

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
@hotlong

hotlong commented Jan 20, 2026

Copy link
Copy Markdown
Contributor

@copilot 浅色模式下,首页代码看不清

@huangyiirene

Copy link
Copy Markdown
Collaborator

@copilot 继续

…olors

Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>

Copilot AI commented Jan 20, 2026

Copy link
Copy Markdown
Contributor Author

@copilot 继续

Fixed in commit 98da8d9. Updated code preview colors to use theme-aware Tailwind classes (e.g., text-purple-600 dark:text-purple-400) for better visibility in both light and dark modes.

Light Mode (Fixed):
Light Mode Code Preview

Dark Mode:
Dark Mode Code Preview

@huangyiirene

Copy link
Copy Markdown
Collaborator

@copilot 继续

Fixed in commit 98da8d9. Updated code preview colors to use theme-aware Tailwind classes (e.g., text-purple-600 dark:text-purple-400) for better visibility in both light and dark modes.

Light Mode (Fixed):
Light Mode Code Preview

Dark Mode:
Dark Mode Code Preview

@copilot 你截图的浅色模式实际上是深色效果呀,是不是哪里的代码出问题了?

Copilot AI commented Jan 20, 2026

Copy link
Copy Markdown
Contributor Author

@copilot 继续
...

抱歉截图有误!这是正确的对比:

Dark Mode(深色模式):
Dark Mode

Light Mode(浅色模式 - 已修复):
Light Mode Fixed

代码预览在浅色模式下现在可以清晰看到了,使用了主题感知的颜色类(如 text-purple-600 dark:text-purple-400)。

Copilot AI changed the title [WIP] Redesign homepage based on shacn and fumadocs specifications Redesign homepage based on shadcn and fumadocs specifications Jan 20, 2026
@huangyiirene
huangyiirene marked this pull request as ready for review January 20, 2026 10:03
@github-actions github-actions Bot added documentation Improvements or additions to documentation dependencies Pull requests that update a dependency file labels Jan 20, 2026
@huangyiirene
huangyiirene merged commit f6eb79c into main Jan 20, 2026
11 checks passed
@@ -0,0 +1,40 @@
import Link from 'next/link';
import { Card, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note documentation

Unused imports CardDescription, CardHeader, CardTitle.

Copilot Autofix

AI 9 months ago

In general, unused imports should be removed to keep the codebase clean and avoid confusion about which components are actually used. This also helps bundlers/tree-shakers produce smaller bundles.

Specifically for apps/docs/components/feature-card.tsx, we should edit the import from @/components/ui/card so that it only imports Card, since CardHeader, CardTitle, and CardDescription are not used anywhere in this file. No other code changes are needed because the JSX already uses native h3/p tags instead of those components. The change is localized to line 2 of this file: replace the destructuring import of four names with a single import of Card.

No new methods, imports, or definitions are required beyond that; we’re only simplifying an existing import.

Suggested changeset 1
apps/docs/components/feature-card.tsx

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/apps/docs/components/feature-card.tsx b/apps/docs/components/feature-card.tsx
--- a/apps/docs/components/feature-card.tsx
+++ b/apps/docs/components/feature-card.tsx
@@ -1,5 +1,5 @@
 import Link from 'next/link';
-import { Card, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
+import { Card } from '@/components/ui/card';
 import { cn } from '@/lib/utils';
 
 interface FeatureCardProps {
EOF
@@ -1,5 +1,5 @@
import Link from 'next/link';
import { Card, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Card } from '@/components/ui/card';
import { cn } from '@/lib/utils';

interface FeatureCardProps {
Copilot is powered by AI and may make mistakes. Always verify output.
@@ -0,0 +1,69 @@
import Link from 'next/link';
import { Badge } from '@/components/ui/badge';

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note documentation

Unused import Badge.

Copilot Autofix

AI 9 months ago

In general, unused imports should be removed to keep the code clean and avoid confusion. Since the Badge component is not used anywhere in HeroSection, the simplest and safest fix is to delete the import line for Badge.

Concretely, in apps/docs/components/hero-section.tsx, remove line 2 (import { Badge } from '@/components/ui/badge';). No other changes are needed: no additional imports, methods, or definitions are required, and existing functionality will remain unchanged because Badge was never referenced.

Suggested changeset 1
apps/docs/components/hero-section.tsx

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/apps/docs/components/hero-section.tsx b/apps/docs/components/hero-section.tsx
--- a/apps/docs/components/hero-section.tsx
+++ b/apps/docs/components/hero-section.tsx
@@ -1,5 +1,4 @@
 import Link from 'next/link';
-import { Badge } from '@/components/ui/badge';
 import { cn } from '@/lib/utils';
 
 interface HeroSectionProps {
EOF
@@ -1,5 +1,4 @@
import Link from 'next/link';
import { Badge } from '@/components/ui/badge';
import { cn } from '@/lib/utils';

interface HeroSectionProps {
Copilot is powered by AI and may make mistakes. Always verify output.
xuyushun441-sys pushed a commit that referenced this pull request May 25, 2026
Adds entries 26-40 covering the gaps that make the helpdesk template
'pretty but not daily-usable' from an end-user perspective:

P0 additions:
- #26 No inline message composer on detail pages
- #27 No external-user portal mechanism
- #28 Attachment/file-list field UI not E2E

P1 additions:
- #29 No 'changed since last visit' indicator
- #30 Bulk operations UI unverified (escalates #17)
- #31 Rich-text editor scoped to comments only
- #32 No first-class canned response / macro
- #33 No collaboration presence indicators
- #34 No keyboard-shortcut API
- #35 No conditional SLA timer (pause on waiting_customer)
- #36 Formula fields can't reference foreign object fields

P2 additions:
- #37 No chart drill-down
- #38 No period-over-period analytics primitive
- #39 No inbound-channel abstraction (email-to-ticket etc.)
- #40 i18n translation namespace validation weak

Includes 'user-pain → platform-gap' mapping table tracing each end-user
complaint to a specific issue number.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…L escaping, so a text-part link keeps its literal & (objectstack-ai#20392)

Fixes objectstack-ai#20374

Clause-②: no

## What was wrong

`EmailService` rendered every face of a `sys_email_template` row through
the HTML escaper (`escapeHtml` in `template-engine.ts`), but only
`body_html` is markup. Measured on `c577e666` through the real
`EmailService` over the seeded `BUILTIN_AUTH_TEMPLATES` rows, the text
part of the verify, reset, invitation and magic-link mails read
`…?token=abc&amp;callbackURL=%2F`. A plain-text client, or a user
copying the link, gets a parameter named `amp;callbackURL`, and the
post-verification redirect falls back to `/`.

## What changes

- **`template-engine.ts`**: one internal `render()` with a per-face hole
encoder.
- `renderTemplate` (the HTML face, and the helper the package exports)
keeps `escapeHtml` for `{{x}}` and verbatim for `{{{x}}}`, exactly as
before.
- The new `renderPlainTextTemplate` (the plain-text face) renders every
hole verbatim, double or triple braced, formatter output included. It is
exported from the module only. The package entry does not re-export it,
so the public surface is unchanged: the built `dist/index.d.ts` names it
0 times, and the built CJS `renderTemplate` still answers `a&amp;b`.
- **`email-service.ts`** `resolveAndRenderTemplate`: this is the ONE
render entry behind both `sendTemplate` and
`IEmailService.renderTemplate`. `subject` and `body_text` now go through
the plain-text renderer, and `body_html` through the HTML renderer. The
derived fallback is unchanged and was already right: when a row has no
`body_text`, the text is `htmlToText(html)`, which decodes the entities.
- **No template edit.** Nothing in `templates/auth-templates.ts`
changes. This follows the triage direction: the engine switch covers
every row that reaches the renderer, whether built-in, declared or
authored in Studio.

### Also fixed in place: the subject (same defect class)

The subject is a plain-text face too (the mail Subject header,
`sys_email.subject`, and the inbox title). On `c577e666` it rendered
`Verify your R&amp;D email address` and `O&objectstack-ai#39;Brien invited you to
R&amp;D`. It passes all four bounded in-place conditions:
- it is this card's defect class, a plain-text face run through the HTML
escaper;
- the fix is mechanical, and its shape is already set: the same
`renderPlainTextTemplate` call;
- it is the same line of the same render entry, which the claim's file
surface already names (`email-service.ts`, where the text body is
rendered);
- it uses the same test family and adds no verification surface.

Evidence: the subject pins below, red under ablation A2.

### One file outside the claim's file surface: a doc sentence this
change made false

`content/docs/automation/email-templates.mdx` said that `{{path}}` is
**HTML-escaped** in "Subject and both bodies". After this change that is
false for `subject` and `bodyText`. The page now says that escaping
follows the face. `.claude/agents/os-dev.md` requires fixing a published
statement that a change makes false, while the claim's surface names
only `packages/plugins/plugin-email/src/` and the changeset. The report
calls out this conflict. No other doc or skill describes template
escaping (a `git grep` for escaping in `content/docs` and `skills`).

## Measurements behind the dispatch's mechanism assumptions

1. **Call site.**
- `template-engine.ts` has a single render function; the `{{`/`{{{`
switch was at `:103` / `:120` and `escapeHtml` at `:78`.
- `ENTITIES` / `decodeEntities` are used only by `htmlToText`, the
derived-fallback path. The declared `body_text` path called no decode
and had no text mode.
- Today `body_text` holds `&amp;` for a URL carrying an ampersand,
`&lt;` for a value carrying a less-than sign and `&objectstack-ai#39;` for an
apostrophe (measured, all four link templates).
2. **One switch covers every template.** `git grep` finds exactly one
non-test caller of the engine's `renderTemplate` in the repo:
`resolveAndRenderTemplate`.
- Built-in auth rows, declared `emailTemplates` and Studio-authored rows
all reach it through `sendTemplate` or `IEmailService.renderTemplate`.
- The messaging inbox channel is the one non-email consumer. It reads
`IEmailService.renderTemplate` and stores `subject` as the title and
`text` as `body_md`.
   - Every call site is covered; none is left out.
3. **Nothing relies on escaped text.**
- No test expected entities in a text body. The positive control: the
grep does find the HTML-face expectations in `template-engine.test.ts`.
- `template-locale-resolution.test.ts` wrapped its subject expectations
in an `esc()` mirror of the escaper. That was vacuous on its Intl date
values, which carry no escapable character, but it stated the old
belief, so it is rewritten: subjects are now compared unescaped, and
`html` expectations keep `esc()`.
4. **Invitation email.** `auth.invitation` lives in this package
(`templates/auth-templates.ts`, in en-US, zh-CN, ja-JP and es-ES).
`plugin-auth` `auth-manager.ts` sends it through `sendTemplate`, so the
engine fix covers it with no edit outside the file surface. It is pinned
below.

## Tests

New `src/plain-text-faces.test.ts` (36 cases) runs the real
`EmailService` over the real seeded rows, plus one authored row that
nobody hand-braced:
- **The card's acceptance, per template and locale.** For verify, reset,
invitation and magic link in 4 locales, the persisted
`sys_email.body_text` and the delivered text part contain the link with
a literal ampersand and no `&amp;`.
- **The HTML part is unchanged, per template and locale.** The `href`
carries the link verbatim, and the visible copy-paste span is still the
escaped markup.
- **Control, per face.** A value carrying a less-than sign and an
ampersand renders escaped in `body_html` and literally in `body_text`
and the subject. This holds for a built-in template, for an authored
template, for the render-only `renderTemplate` the inbox reads, and for
a row with no `body_text` (the derived fallback, with entities decoded).

`src/template-engine.test.ts`: 6 new `renderPlainTextTemplate` cases,
including a control that runs the same inputs through `renderTemplate`
and gets them escaped.

Results at `eb169bd4` for `@objectstack/plugin-email`:
- `pnpm test`: 31 files and 510 tests passed. It ran before the
ablations and again after them, from the committed tree.
- `typecheck`: `tsc --noEmit` exits 0, and `check:test-typecheck`
reports 0 errors.

**HTML part byte-identity.** One-time proof, not kept as a test. BASE
`c577e666` `renderTemplate` and HEAD `renderTemplate` rendered every
built-in `bodyHtml` (24), plus 2 hole-shape extras, with hostile data
(apostrophe, quotes, ampersand, markup). 0 of 26 differ. The positive
control shows the comparison can see a change: 24 of 24 `bodyText`
renders differ between BASE HTML mode and HEAD text mode.

## Ablation: every negative pin, one-time, not committed

Each leg ran through `scripts/ablation-replace.mjs` in WRAP mode on the
committed fix at `eb169bd4`. The anchor hit 1 of 1 each time, and the
blob changed on disk. The tests import `./email-service.js` and
`./template-engine.js` relatively, so vitest runs `src/` and no `dist/`
is in the resolution path.

| leg | mutation | result |
|---|---|---|
| A1 | `body_text` back to `renderTemplate` in `email-service.ts` | **19
failed** / 48 passed |
| A2 | `subject` back to `renderTemplate` | **3 failed** / 64 passed |
| A3 | the plain-text encoder made to escape (`template-engine.ts`) |
**23 failed** / 44 passed |

- **A1** broke every text-part pin, the three control text pins and the
render-only pin. For example, for `auth.verify_email [en-US]` it
received `…?token=TOK123&amp;callbackURL=…`. The 16 HTML-unchanged pins
and the fallback pin stayed green, as expected.
- **A2** broke the three subject pins; the received value was
`O&objectstack-ai#39;Brien invited you to R&amp;D &lt;Lab&gt;`.
- **A3** broke the 4 engine verbatim cases and every service pin above.

Every restore is proven: the blob equals HEAD (`e5a222f83fb1` for
`email-service.ts`, `86d358762d50` for `template-engine.ts`), `git diff
HEAD` is empty and `git status` is clean.

## Gates (HEAD `eb169bd4`)

`node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` was run with no paths: 7 paths against merge base
`c577e6663`, 90 commands. The dispatch-time list had 61 commands; the
extra ones are the docs families that the `content/docs` edit adds. Each
command was run with its exit code captured right after a single
redirect. The `--ran` verdict: **90 accounted for: 88 run, 2 NOT
MEASURED, 0 UNRUN**.

- `check:skill-examples` first exited 3, because
`@objectstack/client-react` was unbuilt. After building the
`@objectstack/client-react...` closure it exited 0: 259 examples
type-check.
- **NOT MEASURED: `check:dual-build-cjs-loads`.** Exit 3: 51 packages
have no `dist/`, and the gate needs a whole-repo `pnpm build`. Declared
narrowing: the diff touches no `package.json`, tsup config or `exports`,
so only this package's `dist` bytes move. On plugin-email's build, CJS
`require` and ESM `import` both load, each with 93 exports and an
identical key set.
- **NOT MEASURED: `check:type-check-debt`.** Exit 3: 16 dependencies of
ledgered packages are unbuilt. Declared narrowing:
- The DEBT ledger has 4 entries: `cloud-connection`, `hono`,
`observability` and the workspace root.
- None of those three packages depends on plugin-email, and the root
program is `scripts/` plus top-level configs, which this diff does not
touch.
- plugin-email itself is covered: `tsc --noEmit` exits 0 and its test
layer has 0 errors.
- **Lint, narrowed.**
- `eslint --no-inline-config --format json` on the 5 touched `.ts`
files: 5 files, 0 errors, 0 warnings.
- The population comes from eslint's own config: each file resolves a
config through `--print-config` (exit 0) and none is reported ignored.
- Invariance: `eslint.config.mjs` enables no type-aware linting (no
`parserOptions.project`, no typed rules, as its own comment at
`:326-328` states), so this diff cannot move the verdict on any
untouched file.
- `origin/main` moved 6 commits since the base. None of them touches
plugin-email, the doc page, service-messaging or the spec email contract
(empty diffstat).

## Acceptance notes

- **Inbox consumer.** The messaging inbox channel stores `rendered.text`
as `body_md`, so an authored value carrying markup characters now
reaches that markdown field raw instead of entity-encoded. This is not a
new exposure class: a row with no `body_text` already delivered raw text
through the `htmlToText` fallback, and the contract
(`RenderTemplateResult.text`, "rendered plain-text body") already said
plain text. How the console renders `body_md` (raw HTML allowed or not)
is NOT MEASURED, because no objectui checkout is in this container.
Whoever next touches the inbox renderer owns that question. Owner: none.
- **Related but separate:** objectstack-ai/objectui#10893. Sign-up
passes no `callbackURL`, which is a different repo and a different
mechanism.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01TEah6PeJGjxJfbHaySJjLQ)_

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…ommits that decided them (objectstack-ai#20717)

Part of objectstack-ai#20596
Clause-②: no

## What changed

This is the seventh stage of the `domain:services` lane of the
dead-citation sweep. It covers
`packages/plugins/plugin-approvals/src/**` and nothing else. By the
seat's census at the claim (`5897866351`), it is the largest package in
the lane that no in-flight work holds. Later stages cover the other
packages, so this PR says `Part of` and the card stays open.

Every comment or docblock site in scope that cited a tracker number
answering 404 has been rewritten in ruling C+D's form C (comment
5749154545 on objectstack-ai#19123), by the method of stages 1 to 6 (PR objectstack-ai#20609 as
`422db788a`, PR objectstack-ai#20626 as `b80ab579d`, PR objectstack-ai#20634 as `4d04b6be3`, PR
objectstack-ai#20658 as `9a4b2bb38`, PR objectstack-ai#20693 as `0e9ad74fb`, PR objectstack-ai#20708 as
`9b384f63a`). That is **41 sites on 38 lines in 13 files, covering 14
numbers**:

- 24 census sites (every census site this package has);
- 15 sites in test comments, which the census defers;
- 2 sites the gate's citation grammar cannot see, found by a raw scan
(see Acceptance notes): the second number of `objectstack-ai#8287/objectstack-ai#8778`
(`approval-node.test.ts:462`) and 「the option objectstack-ai#8710 rejected」
(`approval-service.ts:2329`), which the gate reads as an option ordinal.

Each rewritten line now cites the commit in `origin/main` history that
decided what the line describes, and says in its own words what was
decided: **13 distinct shas**. No number in this package has an ADR or
ruling record of its own in the repository (a grep of `docs/adr/` for
all 14 finds none, and a grep of the rest of `docs/` finds only an audit
that names `objectstack-ai#11311` as evidence), so every anchor is a commit, per
ruling C's order. No number was dropped.

Only comments changed. Every touched source file keeps its line count
(41 lines out, 41 in, over 13 files), so no line citation into these
files moves. 3 of those 41 lines hold no dead citation: 1 reflow line
and 2 lost-referent lines, listed under Wordings below. No code token
moves (see the guard below).

**No citation number is added.** Every tracker number on an added line
was already on the line it replaces: `objectstack-ai#8613`
(`approval-service.ts:2295`, `:2363`, `approval-service.test.ts:751`),
`objectstack-ai#8287` (`sys-approval-request.object.ts:138`,
`approval-node.test.ts:462`), `objectstack-ai#10101`
(`backfill-platform-row-organizations.ts:9`) and `objectstack-ai#12069`
(`translations/index.ts:26`). Each answers 200. Over the whole diff,
added minus removed is 0 or negative for every number, and no number is
new to the diff. No PR number stands on an added line; the one `PR #N`
spelling in scope (`backfill-platform-row-organizations.ts:9`) became
its squash commit.

Five dead sites are left on purpose, all of them test strings (see the
list below).

One more file: a `patch` changeset for `@objectstack/plugin-approvals`,
because the rewritten docblocks and inline comments ship (see Changeset
below).

## Census: `plugin-approvals`, before and after

**Instrument (A1).** The gate's own `node
scripts/check-issue-citations.mjs --census --json`, read-only and
unchanged. The count below is its `allocated-but-absent` findings under
`packages/plugins/plugin-approvals/`. Each run counts as a reading only
because its board frontier equals the newest issue number, read by a
separate request just before and just after the run.

| reading | tree | board | whole-repo `allocated-but-absent` |
plugin-approvals sites | lines | files | numbers |
|---|---|---|---|---|---|---|---|
| before | base `575746371`, run 2026-09-29T20:15:05Z to 20:18:28Z |
enumerated, 186 pages, frontier objectstack-ai#20709 (newest objectstack-ai#20709 before and after),
18,536 numbers | 1,195 | **24** | 22 | 6 | 10 |
| after | head `e698d2393`, run 20:28:39Z to 20:31:58Z | enumerated, 186
pages, frontier objectstack-ai#20716 (newest objectstack-ai#20714 before, objectstack-ai#20716 after), 18,543
numbers | 1,171 | **0** | 0 | 0 | 0 |

The before count matches the seat's census at the claim and A1 (24
sites). The whole-repo drop is 24, exactly this diff's census sites. The
`resolves` tally is 32,971 in both runs, and `resolves-as-pull-request`
(1,984) and `cross-repo-unjudged` (995) did not move either. The after
run was taken on `e698d2393`; the head `708244c2b` adds only the
changeset. No run was truncated or discarded: both enumerations read 186
pages at the newest frontier.

**Supplementary instrument, the whole scope.** The census does not read
test files or strings, and this stage's scope includes test comments. So
a second reading runs the gate's own exported `extractCitations`
(whole-file and comment-prose projections) and `namesThisRepository`
over every `.ts` file under `plugin-approvals/src` (76 files). It takes
its verdicts from the before census's own board reading rather than from
a second enumeration: a number is dead when that census reported it
`allocated-but-absent`, and alive when that census judged it on this
board anywhere (its `--list` extraction) and did not report it. The 18
numbers the census never saw, because they stand only in test files or
strings here, were read one by one on the issues endpoint: 14 answer
200, and `objectstack-ai#8863`, `objectstack-ai#11081`, `objectstack-ai#11286` and `objectstack-ai#11308` answer 404.

| reading | citations | dead | src comment | test comment | src string |
test string |
|---|---|---|---|---|---|---|
| before, `575746371` | 981 | **44** | 24 | 15 | 0 | 5 |
| after, `e698d2393` | 942 | **5** | 0 | 0 | 0 | 5 |

Its src-comment column equals the census's 24, which is the control on
the second instrument. The 902 live citations and the 32 cross-repo
citations are the same in both readings, and the drop of 39 citations is
exactly the rewritten sites the gate grammar sees. Three extracted
tokens are not citations and stay unjudged in both readings: `&objectstack-ai#39;` (an
HTML entity) and two CSS colours, all in `action-link-pages.ts` string
literals. A third, raw reading (every `#` followed by 2 to 6 digits,
whatever surrounds it) finds 46 dead occurrences before and 5 after; the
2 it sees beyond the gate are the two gate-invisible sites above, and
its residue equals the gate's residue site for site.

## Per-number table

Sites and files count every dead occurrence in scope at the base
(comments and strings, tests included). `rewritten / left` counts the
sites rewritten and the sites left. Each anchor was read in its message
and diff, not only its subject, and `git blame` at the base puts every
rewritten line in its anchor commit or in a later commit that descends
from it (`merge-base --is-ancestor` exit 0 for each pair).

| number | sites / files | rewritten / left | anchor: what it decided |
|---|---|---|---|
| `objectstack-ai#16709` | 10/2 | 8/2 | `8c7cca1ce`: the three residues of the
stranded-inspection contract review. Item 2 (the PM ruling of
2026-09-08) keeps a row whose third read threw in the report as the
undifferentiated `failed`; item 3 moves `refineFailedRunState` inside
the `try`, so a malformed host verdict costs only its own row. Its
message numbers the items, which is why the lines keep 「item 2」 and
「item 3」. New to the sweep |
| `objectstack-ai#8710` | 6/2 | 6/0 | `04d03c3a0`: a deactivated `sys_position`
confers no sharing-rule shares, filtered at the sharing call site and
never inside the addressing primitive. Its message quotes the 2026-08-15
ruling verbatim, the same sentence the quoted blocks here carry, and its
diff writes the 「a name with no row is untouched」 fallback that
`approval-service.ts:2318` quotes. Stage 2's anchor, and
`plugin-sharing/src/position-graph.ts:42` already reads 「objectstack-ai#8613 / commit
04d03c3」 |
| `objectstack-ai#6523` | 4/3 | 4/0 | `aa4b90d9a`: the 36 enforcement signatures,
`IApprovalService` among them, converged onto the full
`ExecutionContext`. Its subject names it. Stages 2 and 6 and the spec
stage's anchor |
| `objectstack-ai#6206` | 3/3 | 3/0 | `aa4b90d9a`: the same commit, whose body applies
「the objectstack-ai#6206 ruling default (converge on the full envelope, keep no
per-site subset contracts)」. Written as the full-envelope ruling, the
form stages 2 and 6 used |
| `objectstack-ai#8778` | 4/4 | 4/0 | `7901b2dd2`: the stamp-only
`tenancy.organizationField`, Option A of the maintainer's ruling,
declared on `sys_api_key` as `active_organization_id`. The spec,
`plugin-security` and `service-storage` stages' anchor |
| `objectstack-ai#11081` | 5/1 | 5/0 | `c28e4cfae`: the two SqlDriver-backed fixtures
of `objectstack-ai#11081` stop muting their kernel and pin the expected read-refusal
noise with the runtime's shared capture. Its diff writes all five
`[objectstack-ai#11081]` tags. New to the sweep |
| `objectstack-ai#11286` | 5/1 | 2/3 | `b019891cd`: the contract test that pins the
two `managerIsProvablyOutsideOrg` screens to equal verdicts. Its subject
names it. New to the sweep |
| `objectstack-ai#11674` | 2/1 | 2/0 | `1cba33f16`: the seed loader warns at load time
when a seed defers a required column, and the ordering constraint is
documented at the four pointer-pair sites, this object among them. Stage
2's anchor for the same paragraph |
| `objectstack-ai#12493` | 2/2 | 2/0 | `aa5994e17`: the Operation Message Catalog
gains `approval_recall_not_submitter` (and `record_write_denied`) ahead
of their emitters. Its diff names `objectstack-ai#12493` throughout. Stage 2's anchor
|
| `objectstack-ai#8707` | 1/1 | 1/0 | `1408fe385`: audit rows are stamped from the
record's own organization, which its message says the maintainer's
ruling on `objectstack-ai#8287` requires; the line keeps 「honouring objectstack-ai#8287's ruling」.
New to the sweep |
| `objectstack-ai#8863` | 1/1 | 1/0 | `d200b016b`: the two negative pins that assert
the unfiltered position expansion on the approvals side. Its body names
`objectstack-ai#8863`. New to the sweep |
| `objectstack-ai#11308` | 1/1 | 1/0 | `5a916c4d4`: the one-off platform-row
organization backfill, dry run and write, which its body calls the
`objectstack-ai#11308` sweep. New to the sweep |
| `objectstack-ai#11311` | 1/1 | 1/0 | `1272f0a6b`: the squash commit of the pull
request that was `objectstack-ai#11311` (its subject carries the number), which moved
the resolver to `metadata-core` and made the approval and automation-run
writers stamp the subject's organization. New to the sweep |
| `objectstack-ai#11671` | 1/1 | 1/0 | `09b4f4e4e`: the source-hashes provenance
companion. The identical `translations/index.ts` line in
`service-messaging`, `plugin-sharing` and `plugin-security` already
cites it |

Every cited sha matches exactly one commit (`git rev-parse
--disambiguate`, count 1 for each of the 13), and every one is an
ancestor of the base (`merge-base --is-ancestor`, exit 0 for all 13; the
history is complete, `--is-shallow-repository` false, 15,129 commits).
Each of the 14 numbers answers 404 on the issues endpoint, read one by
one; `objectstack-ai#11311` answers 404 on the pulls endpoint too.

## Wordings to check

- **The full-envelope ruling, `approval-node.ts:29`,
`approval-service.ts:52-53` and `exec-context-annotation.pin.ts:7-8`.**
「since objectstack-ai#6523 (the objectstack-ai#6206 ruling …)」 became 「since commit aa4b90d (the
full-envelope ruling …)」, word for word the form `plugin-sharing`'s
landed `sharing-service.ts:20` and `exec-context-annotation.pin.ts:7`
use. `approval-service.ts:53` is 1 reflow line.
- **The ruling's record, `approval-service.ts:2295` and
`approval-service.test.ts:751`.** 「Maintainer ruling, 2026-08-15 (objectstack-ai#8710,
inheriting objectstack-ai#8613), verbatim:」 became 「… (commit 04d03c3, inheriting
objectstack-ai#8613), verbatim:」. The quotation under it is the ruling itself and is
untouched; `04d03c3a0`'s message carries the same sentence.
- **`approval-service.ts:2329`.** 「that is the option objectstack-ai#8710 rejected」
became 「that is the option the ruling (commit 04d03c3) rejected」.
- **The test heading, `approval-service.test.ts:749`.** 「the objectstack-ai#8710
carve-out, asserted on THIS side (objectstack-ai#8863)」 became 「the commit 04d03c3
carve-out, asserted on THIS side (commit d200b01)」: the carve-out's
record, and the commit that asserted it here.
- **A PR number, `backfill-platform-row-organizations.ts:9`.** 「objectstack-ai#10101
(landed as PR objectstack-ai#11311)」 became 「objectstack-ai#10101 (landed as commit 1272f0a)」, the
pull request's squash commit.
- **Item numbers, `approval-service.ts:4893`, `:4906` and
`stranded-request-inspection.test.ts:123`.** 「[objectstack-ai#16709 item 3]」 became
「[commit 8c7cca1, item 3]」, and likewise for item 2, beside its 「PM
ruling, 2026-09-08」, which `8c7cca1ce`'s message records under 「Item 2」.
- **Lost referents, 2 lines with no dead site** (every file keeps its
line count): `backfill-platform-row-organizations.test.ts:17` 「the one
thing this card must not do」 became 「the one thing this sweep must not
do」, and `manager-org-screen-parity.contract.test.ts:61` 「the very
decision this card is fenced out of」 became 「the very decision this pin
is fenced out of」. Each 「this card」 pointed at the number the same
comment block opened with, which is now a commit; `b019891cd`'s message
says the pin 「PINS the duplication, it does not remove it」.

## The 5 sites left

- **Test strings, 5 sites**, left as stages 1 to 6 left theirs:
- `describe` / `it` titles:
`manager-org-screen-parity.contract.test.ts:232` (`objectstack-ai#11286`),
`stranded-request-inspection.test.ts:519` and `:642` (`objectstack-ai#16709`);
- a test double's thrown message and an assertion message:
`manager-org-screen-parity.contract.test.ts:107` and `:294` (`objectstack-ai#11286`).
- There is no operator string, generated header or quoted ruling
carrying a dead number in this package. The generated
`*.source-hashes.generated.ts` headers already cite `09b4f4e4e` and are
untouched. The two verbatim quotations of the 2026-08-15 ruling carry no
number and are untouched.

## Mechanical guard: no code token moves

The guard compares the TypeScript parser's leaf nodes, with comments as
trivia and JSDoc nodes never visited, base `575746371` against head.
Template literals are therefore read in context. It ran over all 13
touched `.ts` files.

- Real run: 36,204 base leaf tokens, **0 files with a token change**
(exit 0).
- Comment control in `sys-approval-request.object.ts` (「who a row is
ABOUT」 to 「whom a row is ABOUT」): 0 files changed, as expected (exit 0).
- Positive control, a code token added in
`sys-approval-request.object.ts` (`referenceVia: 'object_name',` given a
trailing `as const`): DIFFER (exit 1).
- Positive control, one digit changed inside a kept test title
(`stranded-request-inspection.test.ts:642`): DIFFER (exit 1).

Every mutation went through `scripts/ablation-replace.mjs`, and each
landed (anchor 1 to 0, blob changed). Each restore was proven
byte-identical to the HEAD blob (`6cb56301a334`, `757ad45900ac`), with
`git diff HEAD` empty and a clean tree afterwards.

## Changeset

This change ships bytes, so a `patch` changeset for
`@objectstack/plugin-approvals`
(`.changeset/20596-plugin-approvals-provenance-anchors.md`) is included.
Its body is stage 6's, word for word, with the package name changed.

Measured on the built package (A3): `files[]` is `dist`, `README.md` and
`CHANGELOG.md`. After the build (a cache miss for this package, so
`dist` is this head's source), the rewritten comments reach `dist`:
`8c7cca1ce` 4 times and `04d03c3a0` 4 times in each of `dist/index.d.ts`
and `index.d.mts`; `04d03c3a0` 4 times, `1cba33f16` twice, and
`8c7cca1ce`, `7901b2dd2` and `1408fe385` once each in each of `index.js`
and `index.mjs`. Positive controls: the unchanged line 「A step routing
to nobody is」, in the same docblock as the shipped rewrite at
`approval-service.ts:2295`, is found once in each of the four files, and
the unchanged line 「itself stays unwalled (`tenancy.enabled: false`)」
beside the shipped rewrite at `sys-approval-request.object.ts:144` once
in each JS file. A never-written negative phrase appears nowhere in
`dist`. None of the 14 dead numbers is left anywhere in `dist`.

## Gates (head `708244c2b`)

- **Citation judging, as CI runs it:** `pnpm check:issue-citations`
(self-test) exits 0. `node scripts/check-issue-citations.mjs` exits 0:
the diff-scoped run judged 5 citations across 6 files, and all 5 resolve
(`objectstack-ai#8613` twice, `objectstack-ai#8287`, `objectstack-ai#10101`, `objectstack-ai#12069`), each already on the line
it replaces.
- **Doc authoring:** `pnpm check:doc-authoring` exits 0.
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` at `708244c2b` derived 64 commands:
all 57 derived at dispatch, plus `check:dispatcher-error-vocabulary`,
`check:engine-double-contract`, `check:objectql-double-limit`,
`check:query-options-erasure`, `check:type-check-coverage`,
`check:type-check-debt` and `check:where-matcher`. Each ran with its
exit code captured before any pipe, and all 64 exit 0. `--ran`, fed each
command with its exit code, reports 64 run, 0 NOT MEASURED (a derived
zero), 0 unrun, and exits 0. A full `turbo run build` of `./packages/*`
and `./packages/*/*` ran first under the shared verify lock (71 of 71
tasks, exit 0), so no gate hit an unbuilt workspace.
- **Roster families the derivation lists outside its commands** (their
rosters sit in directories this diff touches): `node
scripts/check-changeset-fixed.mjs`, `pnpm check:authz-resolver`, `pnpm
check:error-code-casing` and `pnpm check:filter-alias-parity`, each exit
0.
- **Tests and typecheck, under the verify lock:**
- `pnpm --filter @objectstack/plugin-approvals test`: 51 files pass and
791 tests pass. That is every test file in the package, the 7 touched
ones included.
- `pnpm --filter @objectstack/plugin-approvals typecheck` exits 0 (`tsc`
on `tsconfig.json`, the scripts program, and the test layer on
`tsconfig.test.json`, held at its ledger of 8 files, 324 errors and 27
pinned signatures). `--listFiles`: the `tsconfig.json` program holds the
25 non-test files under `src/`, the 6 touched ones included; the
`tsconfig.test.json` program holds all 76 files under `src/`, the 51
test files and all 13 touched files included.
- **Lint, as a proven narrowing:** `eslint --no-inline-config --format
json` over the 13 touched `.ts` files gives 13 files, 0 errors and 0
warnings. All 13 are in eslint's own population (`isPathIgnored` is
false for each; a `dist` file, as the control, is ignored).
`eslint.config.mjs` never enables type-aware linting (no
`parserOptions.project`, as its own lines 327-328 state), so a comment
edit here cannot move the verdict on any untouched file. The repo-wide
`pnpm lint` is CI's run.
- **Control bytes:** `pnpm check:nul-bytes` exits 0, and a raw scan of
the 14 changed files for control bytes finds none.

## Acceptance notes

- **The gate-invisible spellings, grepped as the claim asked.**
`CITATION_RE` refuses a hyphen after the digits and a `/` before the `#`
(objectstack-ai#20636). In this package there is one `#N-word` spelling, 「objectstack-ai#3266-era」
(`record-reader-visibility.test.ts:342`), and two `#A/#B` spellings,
`objectstack-ai#8287/objectstack-ai#8778` (`approval-node.test.ts:462`) and `objectstack-ai#8543/objectstack-ai#8580`
(`approval-vocabularies.test.ts:66`): the claim's 3, 1 and 2. `objectstack-ai#3266`,
`objectstack-ai#8287`, `objectstack-ai#8543` and `objectstack-ai#8580` answer 200; the second number `objectstack-ai#8778` is
dead, so that one line is rewritten here.
- **A third spelling the gate cannot see, found by the raw scan.**
`NON_CITATION_HEADS` excuses any `#N` after the word 「option」 as an
option ordinal, so 「the option objectstack-ai#8710 rejected」
(`approval-service.ts:2329`) was never extracted: a dead number there
would pass the diff gate at exit 0 and never enter a census count. It is
rewritten here. Across the gate's declared surfaces at the base, the
only other `option #N` with three or more digits is
`packages/objectql/src/validation/rule-validator.ts:2202` (`option
objectstack-ai#14088`), which answers 200. Same family as objectstack-ai#20636; noted for its
closeout, not a card of its own.
- **A retired key name in this package's prose, not changed here.**
`tenancy.organizationField` left the authorable surface in `502f179cc`,
and limb 0 of the shared resolver now reads
`PLATFORM_STAMP_ORGANIZATION_COLUMNS` in `metadata-core`, keyed by
object name. Comments in this package still name the retired key as what
limb 0 reads (`sys-approval-request.object.ts:143`, the line above a
rewrite; `backfill-platform-row-organizations.ts:35`,
`approval-node.test.ts:463`, `approval-service.ts:2707`,
`backfill-platform-row-organizations.test.ts:50`), and two test fixtures
still declare it on a stub `sys_api_key` (`approval-node.test.ts:467`,
`backfill-platform-row-organizations.test.ts:54`), where it is inert
because the resolver keys by name. The anchor `7901b2dd2` is right for
the key those lines name, and nothing is wrong at runtime. Correcting
the prose would reach past the dead citations, and the fixtures are code
tokens, so none of it is changed here.
- **The census instrument did not truncate in this stage.** Both
enumerations read 186 pages at the newest frontier.
- **Anchors the next stages can reuse**, each checked here: `objectstack-ai#16709` →
`8c7cca1ce`; `objectstack-ai#11081` → `c28e4cfae`; `objectstack-ai#11286` → `b019891cd`; `objectstack-ai#11308` →
`5a916c4d4`; `objectstack-ai#11311` → `1272f0a6b`; `objectstack-ai#8707` → `1408fe385`; `objectstack-ai#8863` →
`d200b016b`.
- **Base.** The branch is on `main` at `575746371`, which is still
`main` at 20:56Z (read into a private ref), so there was no merge.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…ts that decided them (objectstack-ai#20757)

Part of objectstack-ai#20596
Clause-②: no

## What changed

This is the eleventh stage of the `domain:services` lane of the
dead-citation sweep. It covers `packages/plugins/plugin-email/src/**`
and nothing else. By the seat's census at the claim (`5902547086`), it
is the largest package in the lane that no in-flight work holds. Later
stages cover the other packages, so this PR says `Part of` and the card
stays open.

Every comment or docblock site in scope that cited a tracker number
answering 404 has been rewritten in ruling C+D's form C (comment
5749154545 on objectstack-ai#19123), by the method of stages 1 to 10 (PR objectstack-ai#20609 as
`422db788a`, PR objectstack-ai#20626 as `b80ab579d`, PR objectstack-ai#20634 as `4d04b6be3`, PR
objectstack-ai#20658 as `9a4b2bb38`, PR objectstack-ai#20693 as `0e9ad74fb`, PR objectstack-ai#20708 as
`9b384f63a`, PR objectstack-ai#20717 as `cbaf04c1f`, PR objectstack-ai#20729 as `d2820876f`, PR
objectstack-ai#20737 as `4dfff176b`, PR objectstack-ai#20742 as `697845d19`). That is **16 sites on
16 lines in 8 files, covering 4 numbers**:

- 7 census sites (every census site this package has);
- 9 sites in test comments, which the census defers. Three of them carry
`objectstack-ai#13190`, a dead number that stands only in test files here, so the
census never judged it; it was read on its own (404);
- no site the gate's grammar cannot see (the package has none that is
dead, see Acceptance notes).

Each rewritten line now cites the commit in `origin/main` history that
decided what the line describes, and says in its own words what was
decided: **4 distinct shas**. No number in this package has an ADR or
ruling record of its own (a grep of `docs/adr/` and
`scripts/adr-anchors/` finds only ADR-0131 naming `objectstack-ai#11741`, as evidence
in its D7, not as the record of that decision; nothing else under
`docs/` names the four), so every anchor is a commit, per ruling C's
order. No number was dropped.

Only comments changed. Every touched source file keeps its line count
(16 lines out, 16 in, over 8 files), so no line citation into these
files moves. Every one of the 16 changed lines carried a dead citation;
there is no reflow line. No code token moves (see the guard below).

**No citation number is added.** The added lines carry no tracker number
at all. Over the whole diff, added minus removed is negative for the
four dead numbers and zero for every other number, and no number is new
to the diff. No PR number is the citation on an added line: the two `PR
objectstack-ai#8675` spellings became that pull request's squash commit.

10 dead sites are left on purpose, all of them `describe` / `it` titles
(see the list below).

One more file: a `patch` changeset for `@objectstack/plugin-email`,
because the rewritten prose ships (see Changeset below).

## Census: `plugin-email`, before and after

**Instrument (A1).** The gate's own `node
scripts/check-issue-citations.mjs --census --json`, read-only and
unchanged. The count below is its `allocated-but-absent` findings under
`packages/plugins/plugin-email/`. Each run counts as a reading only
because its board frontier equals the newest issue or pull-request
number, read by a separate request just before and just after the run.

| reading | tree | board | whole-repo `allocated-but-absent` |
plugin-email sites | lines | files | numbers |
|---|---|---|---|---|---|---|---|
| before | base `97005aed0`, run 2026-09-30T02:00:45Z to 02:04:02Z |
enumerated, 186 pages, frontier objectstack-ai#20748 (newest objectstack-ai#20747 before, objectstack-ai#20748
after: a pull request opened at 02:03:20Z, inside the run) | 1,064 |
**7** | 7 | 4 | 3 |
| after | head `15a7d69a7`, run 02:11:19Z to 02:14:30Z | enumerated, 186
pages, frontier objectstack-ai#20753 (newest objectstack-ai#20753 before and after) | 1,057 | **0**
| 0 | 0 | 0 |

The before count matches the seat's census and A1 (7 sites: `objectstack-ai#13189` ×4,
`objectstack-ai#11741` ×2, `objectstack-ai#8675` ×1). The before run's board moved during the run;
its frontier equals the newest number at the run's end, which is A1's
criterion (stage 7's precedent). The whole-repo drop is 7, exactly this
diff's census sites. The `resolves` tally is 33,029 in both runs, and
`resolves-as-pull-request` (1,984) and `cross-repo-unjudged` (995) did
not move either. The after run was taken on `15a7d69a7`; the head
`23283d394` adds only the changeset. No run was truncated or discarded:
both enumerations read 186 pages at the newest frontier.

**Supplementary instrument, the whole scope.** The census does not read
test files or strings, and this stage's scope includes test comments. So
a second reading runs the gate's own exported `extractCitations`
(whole-file and comment-prose projections) and `namesThisRepository`
over every `.ts` file under `plugin-email/src` (50 files). It takes its
verdicts from the before census's own board reading rather than from a
second enumeration: a number is dead when that census reported it
`allocated-but-absent`, and alive when that census judged it on this
board anywhere (its `--list` extraction, 37,072 rows) and did not report
it. The eleven numbers the census never saw, because they stand only in
test files or as the second half of a slash pair here, were read one by
one on the issues endpoint: `objectstack-ai#13190` answers 404; `objectstack-ai#5169`, `objectstack-ai#5286`,
`objectstack-ai#10619`, `objectstack-ai#16506`, `objectstack-ai#20374`, `objectstack-ai#5197` answer 200 as issues, and `objectstack-ai#8348`,
`objectstack-ai#5191`, `objectstack-ai#5211`, `objectstack-ai#5232` as pull requests.

| reading | citations | dead | src comment | test comment | src string |
test string |
|---|---|---|---|---|---|---|
| before, `97005aed0` | 360 | **26** | 7 | 9 | 0 | 10 |
| after, `15a7d69a7` | 344 | **10** | 0 | 0 | 0 | 10 |

Its src-comment column equals the census's 7, which is the control on
the second instrument. The 323 live citations are the same in both
readings, and the drop of 16 citations is exactly the rewritten sites.
11 extracted tokens are not tracker references at all and are not
judged: the HTML entity `&objectstack-ai#39;` (6 sites in the template engine and its
tests) and the fixture subjects `Invoice objectstack-ai#42` to `Invoice objectstack-ai#45` (5
sites). A third, raw reading (every `#` followed by 2 to 6 digits,
whatever surrounds it) finds 371 occurrences and 26 dead before, 355 and
10 after. Beyond the gate's grammar it sees 11 tokens, none dead: the
nine second numbers of the `#A/#B` lines (all live), the excused `Prime
Directive objectstack-ai#12`, and the CSS colour `#2563eb`.

## Per-number table

Sites and files count every dead occurrence in scope at the base
(comments and strings, tests included). `rewritten / left` counts the
sites rewritten and the sites left. Each anchor was read in its message
and diff, not only its subject, and `git blame` at the base puts every
rewritten line in its anchor commit or in a later commit that descends
from it (`merge-base --is-ancestor` exit 0 for all 16 line and anchor
pairs).

| number | sites / files | rewritten / left | anchor: what it decided |
|---|---|---|---|
| `objectstack-ai#13189` | 13/4 | 8/5 | `33fbd3566` (PR objectstack-ai#13375): the SMTP port guard
tests integrality (`Number.isInteger`), so a fractional port such as
`587.5` is refused at construction, and the generated refusal sentence
reads `(expected an integer 1-65535)`, the range still rendered from the
constants. Its changeset headline names `objectstack-ai#13189`; its diff writes the
integrality docblocks the rewritten lines sit in. New to the sweep |
| `objectstack-ai#13190` | 5/1 | 3/2 | `56c5b1dbe` (PR objectstack-ai#13316):
`smtpOptionsFromMailSettings` passes a present-but-unreadable
`smtp_port` through to the guard instead of omitting it (which had
silently fallen back to 587); absent and `''` still mean "not set", and
no second refusal was added. Its changeset headline names `objectstack-ai#13190`; its
diff writes the `objectstack-ai#13190` comment block itself. New to the sweep |
| `objectstack-ai#11741` | 6/3 | 3/3 | `b706af987` (PR objectstack-ai#11839): `SendEmailInput` /
`SendTemplateInput` gain an optional `organizationId`, which
`plugin-email`'s writer stamps verbatim onto `sys_email.organization_id`
(pass-through only, no resolution or fabrication), and `sendTemplate`
forwards it as a producer of `send()`. Its message names `objectstack-ai#11741` as the
card that commit closed; `git blame` puts all three rewritten lines in
it. The `plugin-auth` stage's anchor for the same number |
| `objectstack-ai#8675` | 2/2 | 2/0 | `c9f595083`: the squash commit of the pull
request that was `objectstack-ai#8675` (its subject ends `(objectstack-ai#7987) (objectstack-ai#8675)`):
`sys_account`'s OAuth token columns are declared `internal: true`. Its
diff records the trap both lines describe: those columns are `required:
false`, so inferring "key missing, therefore the strip ran" broke
ordinary sign-in (16 red tests), which is why the readback carries the
`absenceProvesStrip` discriminator. New to the sweep |

Every cited sha matches exactly one commit (`git rev-parse
--disambiguate`, count 1 for each of the 4), and every one is an
ancestor of the base (`merge-base --is-ancestor`, exit 0 for all 4;
control leg: stage 1's landing `422db788a` exit 0; the history is
complete, `--is-shallow-repository` false, 15,155 commits). Each of the
4 numbers answers 404 on the issues endpoint, which serves pull requests
too. Independently, the package's own shipped `CHANGELOG.md` pairs
`b706af9`, `33fbd35` and `56c5b1d` with the same three decisions.

## Wordings to check

- **Tag swaps in parentheses.** 「(objectstack-ai#13189)」 became 「(commit 33fbd35)」
at `transports/smtp-port-contract.ts:87` (a section heading), `:134` and
`transports/smtp.ts:68`.
- **Line openers.** 「objectstack-ai#11741 —」 became 「Commit b706af9 —」 at
`email-service.ts:742` and `:1439`; 「objectstack-ai#13190 —」 became 「Commit 56c5b1d
—」 at `transports/smtp.test.ts:221`; 「## objectstack-ai#13189 —」 became 「## Commit
33fbd35 —」 at `transports/smtp-port-contract.test.ts:34`.
- **`email-service.test.ts:342`**, a section rule: 「── objectstack-ai#11741 —」 became
「── Commit b706af9 —」, and its trailing rule was shortened by 10
characters so the line keeps its width exactly.
- **`internal-header-readback.ts:37`.** 「(PR objectstack-ai#8675 hit exactly this on
`sys_account`'s optional」 became 「(Commit c9f5950 records exactly this
on `sys_account`'s optional」: a commit does not "hit" a trap, it records
one, and that commit's own diff is where the 16 red tests are recorded.
- **`email-headers-internal.integration.test.ts:251`.** 「The regression
PR objectstack-ai#8675 measured on a sibling card」 became 「The regression commit
c9f5950 records from a sibling card」, the same reading.
- **`transports/smtp-port-contract.test.ts:228`.** 「objectstack-ai#13189 is the card
that SPENDS that」 became 「Commit 33fbd35 is the change that SPENDS
that」, so the noun matches the anchor.
- **`transports/smtp.ts:127`, `transports/smtp.test.ts:272`, `:276`,
`:281`, `:283`.** The number became 「commit SHA」 in place (「until commit
33fbd35:」, 「The bucket commit 56c5b1d never had to name」, 「Commit
33fbd35 made the guard test」, 「Commit 56c5b1d's rule is that」,
「commit 33fbd35 changed which numbers」).

## The 10 sites left

- **Test strings, 10 sites on 9 lines**, all `describe` / `it` titles,
left as stages 1 to 10 left theirs: `email-service.test.ts:349` and
`send-template.test.ts:63`, `:88` (`objectstack-ai#11741`);
`transports/smtp-port-contract.test.ts:225`, `:309`, `:340` (`objectstack-ai#13189`);
`transports/smtp.test.ts:230` (`objectstack-ai#13190`), `:271` (`objectstack-ai#13189`), `:293`
(`objectstack-ai#13190` and `objectstack-ai#13189`).
- No source string, operator log string, assertion message, quoted
maintainer ruling or generated file in this package carries a dead
number.
- Outside `src`, the package's `CHANGELOG.md` names three of these
numbers on 5 lines. It is release-owned and deliberately not edited here
(see Acceptance notes).

## Mechanical guard: no code token moves

The guard compares the TypeScript parser's leaf nodes (a `forEachChild`
walk, so comments are trivia and JSDoc nodes are never visited), base
`97005aed0` against head. String and template literals are therefore
read in full. It ran over all 8 touched `.ts` files.

- Real run: 7,035 base leaf tokens, **0 files with a token change**
(exit 0).
- Comment control in `email-service.ts` (「no resolution, no default, no
fabrication」 to 「… no default and no fabrication」): 0 files changed, as
expected (exit 0).
- Positive control, a code token added in `transports/smtp.ts`
(`isValidSmtpPort(port)` given `as number`): DIFFER, 587 to 588 leaf
tokens (exit 1).
- Positive control, one digit changed inside a kept test title
(`transports/smtp.test.ts:293`, `objectstack-ai#13189` to `objectstack-ai#13188`): DIFFER (exit 1).

Every mutation went through `scripts/ablation-replace.mjs` (wrap mode)
under a shell trap that restores by absolute path, and each landed
(anchor 1 to 0, blob changed). Each restore was proven byte-identical to
the HEAD blob (`1e99bd5e2bcb`, `46c13267611b`, `da5314910bc4`), with
`git diff HEAD` empty and a clean tree afterwards.

## Changeset

This change ships bytes, so a `patch` changeset for
`@objectstack/plugin-email`
(`.changeset/20596-plugin-email-provenance-anchors.md`) is included. Its
body is stage 10's, word for word, with the package name changed.

Measured on the built package (A3): `files[]` is `dist`, `README.md` and
`CHANGELOG.md`, and the package is not private. After the build,
`b706af987` appears twice in each of `dist/index.js` and
`dist/index.mjs` (the two inline comments in `email-service.ts`, which
the bundle keeps). `c9f595083` appears once in each of `dist/index.d.ts`
and `dist/index.d.mts` (the `internal-header-readback.ts` docblock), and
so does `33fbd3566` (the docblock on `SmtpTransportOptions.port`).
`56c5b1dbe` reaches nothing (test files only). Positive controls, one
unchanged line beside each shipped rewrite, land exactly where their
neighbours do: 「context, so the input's organization is the one fact it
may stamp:」 and 「caller's organization so the sys_email row it persists
is stamped.」 once in each JS file; 「token columns: inheriting」 and the
unchanged line just above the rewritten one in the `port` docblock once
in each declaration file. A never-written negative phrase appears
nowhere in `dist`. None of the 4 dead numbers is left in `dist`.

## Gates (head `23283d394`)

- **Citation judging, as CI runs it:** `pnpm check:issue-citations`
exits 0. `node scripts/check-issue-citations.mjs` exits 0: the
diff-scoped run found no citation added against `97005aed0` (4 files
read; test files are a deferred surface).
- **Doc authoring:** `pnpm check:doc-authoring` exits 0.
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` at `23283d394` derived 61 commands:
all 55 derived at dispatch, plus `check:engine-double-contract`,
`check:objectql-double-limit`, `check:query-options-erasure`,
`check:type-check-coverage`, `check:type-check-debt` and
`check:where-matcher`. Each ran with its exit code captured before any
pipe, and all 61 exit 0. `--ran`, fed each command with its exit code,
reports 61 run, 0 NOT MEASURED (a derived zero), 0 unrun, and exits 0. A
full `turbo run build` of `./packages/*` and `./packages/*/*` ran first
under the shared verify lock (71 of 71 tasks, exit 0), so no gate hit an
unbuilt workspace.
- **Roster families the derivation lists outside its commands** (their
rosters sit in directories this diff touches): `node
scripts/check-changeset-fixed.mjs`, `pnpm check:authz-resolver`, `pnpm
check:error-code-casing` and `pnpm check:filter-alias-parity`, each exit
0.
- **Tests and typecheck, under the verify lock:**
- `pnpm --filter @objectstack/plugin-email test`: 31 files pass and 510
tests pass. `vitest list --filesOnly` names 31 files, all the tracked
test files, the 4 touched ones included.
- `pnpm --filter @objectstack/plugin-email typecheck` exits 0 (`tsc` on
`tsconfig.json`, then `check:test-typecheck` on `tsconfig.test.json`: 0
files and 0 errors in its debt ledger). `tsc --listFiles` holds all 8
touched files in both programs, and the test program holds all 50 files
under `src/`.
- **Lint, as a proven narrowing:** eslint with inline config disabled,
over the 8 touched `.ts` files, gives 8 files, 0 errors and 0 warnings.
All 8 are in eslint's own population (`isPathIgnored` is false for each;
a `dist` file, as the control, is ignored). `eslint.config.mjs` never
enables type-aware linting (no `parserOptions.project`, as its own lines
327-328 state), so a comment edit here cannot move the verdict on any
untouched file. The repo-wide `pnpm lint` is CI's run.
- **Control bytes:** `pnpm check:nul-bytes` exits 0, and a raw scan of
the 9 changed files for control bytes finds none.

## Acceptance notes

- **The gate-invisible spellings, grepped as the claim asked.**
`CITATION_RE` refuses a hyphen after the digits and a `/` before the `#`
(objectstack-ai#20636), and `NON_CITATION_HEADS` excuses a number after the word
「option」. In this package: `#N-word` none, `#A/#B` 9 lines, `option #N`
none, at the base and at the head, which is the claim's 0 / 9 / 0. Every
second number on the 9 slash lines answers 200 (`objectstack-ai#5197` ×2, `objectstack-ai#5191`,
`objectstack-ai#5211`, `objectstack-ai#5232` ×2, `objectstack-ai#5177`, `objectstack-ai#4251`, `objectstack-ai#5094`), so nothing there needed
rewriting.
- **ADR-0131 names `objectstack-ai#11741`.** Its D7 cites `objectstack-ai#11741` as the writer fact
that keeps `sys_email` tenant data. That is evidence inside a later
record, not the record of what `objectstack-ai#11741` decided, so it is not this
stage's anchor, and `docs/adr/**` is a governed Tier H surface outside
this card's stages. It joins the ADR-tree residue the seat already
carries (ADR-0131's `objectstack-ai#14484`, stage 2).
- **`CHANGELOG.md` is left.**
`packages/plugins/plugin-email/CHANGELOG.md` names `objectstack-ai#11741`, `objectstack-ai#13189`,
`objectstack-ai#13190` and `objectstack-ai#8675` on 5 lines. It is release-owned (AGENTS.md,
Documentation Guardrails), a deferred surface of the citation gate, and
⛔ not part of this stage.
- **「This card」 phrases are left.** 20 comment lines in 8 files of this
package speak of 「this card」, 「the card」 or 「the two cards」. They carry
no number and neither instrument sees them. Inside the `objectstack-ai#13189` test
block, they still have the kept `(objectstack-ai#13189)` title as their referent; the
one rewritten line that said 「the card」 now says 「the change」 (above).
The rest are unchanged, as in stages 8 to 10.
- **The census instrument did not truncate in this stage.** Both
enumerations read 186 pages at the newest frontier.
- **Anchors the next stages can reuse**, each checked here: `objectstack-ai#13189` →
`33fbd3566`; `objectstack-ai#13190` → `56c5b1dbe`; `objectstack-ai#8675` → `c9f595083`. `objectstack-ai#11741` →
`b706af987` reuses the `plugin-auth` stage's anchor.
- **Base.** The branch is on `main` at `97005aed0`. `main` has since
moved two commits (`9c8f113c6`, `a6866da0c`). Their 14 files touch
nothing under `plugin-email`, nor `scripts/check-issue-citations.mjs`,
`.changeset/config.json` or the `doc-authoring-prose-id` baseline, and
the three console-injection scripts they change are not among this
diff's 61 derived families. So no merge was taken; the merge queue
rebuilds on the merged generation.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H)_

---------

Co-authored-by: Claude <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview — 98da8d9e Deployed Jan 20, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/l

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants