Repository navigation
Redesign homepage based on shadcn and fumadocs specifications - #39
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
|
@copilot 浅色模式下,首页代码看不清 |
|
@copilot 继续 |
…olors Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
@copilot 你截图的浅色模式实际上是深色效果呀,是不是哪里的代码出问题了? |
抱歉截图有误!这是正确的对比: 代码预览在浅色模式下现在可以清晰看到了,使用了主题感知的颜色类(如 |
| @@ -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
Show autofix suggestion
Hide autofix suggestion
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.
| @@ -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 { |
| @@ -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
Show autofix suggestion
Hide autofix suggestion
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.
| @@ -1,5 +1,4 @@ | ||
| import Link from 'next/link'; | ||
| import { Badge } from '@/components/ui/badge'; | ||
| import { cn } from '@/lib/utils'; | ||
|
|
||
| interface HeroSectionProps { |
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>
…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&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&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&D email address` and `O&objectstack-ai#39;Brien invited you to R&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 `&` for a URL carrying an ampersand, `<` 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 `&`. - **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&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&D <Lab>`. - **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>
…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>
…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>




Homepage Redesign - Based on shadcn & fumadocs Specifications
Goal
重新开发首页,基于 shadcn 和 fumadocs 规范 (Redevelop homepage based on shadcn and fumadocs specifications)
Implementation Checklist
Setup Phase
Component Development
Homepage Enhancement
Testing & Validation
Screenshots
Desktop View (English)
Before:

After (Dark Mode):

After (Light Mode):

Mobile View (375px)
Chinese Version (中文)
Key Improvements
✨ Modern shadcn/ui Component System
🎨 Enhanced Visual Design
🚀 Smooth Animations & Interactions
📱 Responsive Design
🌐 i18n Support
🎯 Fumadocs Best Practices
🌓 Light/Dark Mode Support
Technical Details
New Dependencies:
class-variance-authority- Component variant managementclsx- Conditional className utilitiestailwind-merge- Tailwind class mergingNew Components:
components/ui/button.tsx- Reusable button componentcomponents/ui/card.tsx- Card component systemcomponents/ui/badge.tsx- Badge componentcomponents/feature-card.tsx- Feature showcase cardscomponents/persona-card.tsx- Persona profile cardscomponents/code-preview.tsx- Enhanced code preview with theme-aware colorscomponents/hero-section.tsx- Hero section componentlib/utils.ts- Utility functionsUpdated Files:
app/[lang]/page.tsx- Refactored to use new componentstailwind.config.js- Enhanced with shadcn theme configapp/global.css- Added CSS variables and utilitiesOriginal prompt
💡 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.