Repository navigation
refactor(layout): a directory under src/ is a package — move every authored file into sales / service / revenue / marketing - #1910
Conversation
Every file under `src/` moves into the directory of the package that owns it — `src/sales/` (the `type: app` package), `src/service/`, `src/revenue/`, `src/marketing/` — per ADR-0130 and module-split-plan.md items 6-9. A directory under `src/` is now a package; there is no `packages/` level and no `shared/`. No manifest, no `composeStacks`, no per-package `index.ts`, no navigation or behaviour change. `objectstack.config.ts` keeps its single `defineStack`, and the collection step it consumes is the new `objectstack.composition.ts` — the config itself may carry no named export (the build parses it against a strict stack schema), measured on the pre-move tree. `dist/objectstack.json` is byte-identical to the pre-move build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3YsvpK1PvYf9n1YUhYP6W
…ackage tree The half of the layout move that is not `git mv`: - `scripts/check-source-token-ratchet.mjs` measures `src/sales/` instead of the whole tree, prints a reading for each of the four package directories (informational, no ceiling) and re-anchors all three ceilings from the reading the move produced. Lowering the ruled `business semantics` ceiling is authorised by the 2026-09-14 ruling, quoted in the script header. - `test/helpers/src-roster.ts` is the one place a suite learns the package layout; the app-wide sweeps read their file rosters from it. - `.github/labeler.yml`, `scripts/lib/source-hygiene-surface.mjs`, every doc that names or draws `src/<dir>/`, the agent briefs and the two task files follow the tree. `docs/architecture/module-split-inventory.json` records the move in `$hand_edits` and re-points its roster. - The README banner restates its two figures from the gate: the sales package, the unit ADR-0130 makes the claim about. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3YsvpK1PvYf9n1YUhYP6W
…kage-directory-layout
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3YsvpK1PvYf9n1YUhYP6W
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
| import { | ||
| resolveComposition, | ||
| accounts, contacts, leads, opportunities, | ||
| tasks, events, eventAttendeesFromContacts, eventAttendeesFromLeads, forecasts, | ||
| } from './src/sales/data/index.js'; |
维护者速读(终稿)席位复核记录在 #1905 的 ACCEPT 评论里;本条是给你读的版本。 改了什么把 为什么改落实你 2026-09-14 的四条裁决(「去掉 packages 这一层」「shared 有必要吗」「为什么需要 app」「token 门禁放到 sales」),是后面拆包(#1907)的前提。边界从 barrel 文件里搬到目录上,肉眼可见;hook 必须与它挂的对象并排,import 只能朝自己目录或 风险与代价(含回滚)风险极低,有硬证据。 构建产物逐字节相同——dev 量过一次,席位在两个全新 worktree 里独立重建再量一次,sha256 同为 代价三处,都已写明:
回滚:直接 revert 这条分支即可,产物不变,无数据迁移,changeset 是空 frontmatter 的"不发布"声明。 席位意见建议合并。理由:验收一(产物逐字节相同)由席位独立复现;受管面 你要做的一个动作:确认上述口径后,合并本 PR(受管 PR,席位不会翻 ready 或挂 auto-merge)。合并后 #1907(sales 成 app 包、service 成模块)会由席位自动认领派发。 Generated by Claude Code |
Browser verification (seat, 2026-09-14T14:05Z) —
|
| Surface | head | main |
|---|---|---|
Login (admin@objectos.ai) → app home |
✓ | ✓ |
| List pages: lead 21 rows · account 9 · contact 9 (grouped by account) · opportunity 10 · quote 6 · contract 4 · product 13 · task 7 · event 27 · campaign 7 · case 38 · knowledge 4 · forecast 7 | identical counts | identical counts |
| Record pages, same named record on both sides: account Lattice Education (Details · Related 5 · Attachments), opportunity Lattice Education Renewal (Details · Related 4 · Activity), knowledge KA-0004 Legacy SSO Setup, lead Alice Martinez (Details · Related · Activity · History), case CASE-00025 | identical tabs, related counts and body length | identical |
| Dashboards (DOM probe for drawn Recharts marks, after hydration): executive 9 · sales 13 · service 14 · CRM overview 13 · sales activity 13 | identical | identical |
Approvals inbox (approvals:inbox component) mounts |
✓ | ✓ |
| Failed requests during the walk | 1 | 1 |
The single failed request on each side is the script's own probe — a POST /api/v1/analytics/dataset/query with a deliberately minimal body, refused 400 VALIDATION_FAILED identically by both; the dashboards' own dataset queries succeeded (the tiles drew). Not a finding.
Boot diagnostics: one identical warning on both ([Seeder] Inline seed exceeded 8000ms budget … continuing in background), pre-existing on main.
Conclusion: the source-boot path shows no difference from main on any surface walked — navigation, lists, records, dashboards, approvals. Screenshots are in the seat's scratchpad and were shown to the maintainer in chat; the seat's ACCEPT on #1905 stands. Both instances torn down after the run.
Generated by Claude Code
… dead Link Check has been red on every push to main since 36b27dd (#1910, 2026-09-14): the push run scans every .md file, and CHANGELOG.md's closed pre-3.0.0 section still linked eight files at the flat src/<kind>/ paths the package move renamed. Each target now names the file's package home (git log --follow: every one is a pure rename in 36b27dd). Only the link targets change - the record keeps its words, including the two link texts that spell the old path, per #1932's keep-the-record, repoint-the-pointer rule. Claude-Session: https://claude.ai/code/session_012zh91QzFgePbkmuHnugLN3 Co-authored-by: Claude <noreply@anthropic.com>
Fixes #1905
Part of #1904 (step 1 of the sequence — this card is not the packaging one).
Description
Every authored file under
src/moves into the directory of the package that owns it:src/sales/(thetype: apppackage),src/service/,src/revenue/,src/marketing/.A directory under
src/is a package — nopackages/level, noshared/— perADR-0130 and
docs/architecture/module-split-plan.md, Decisions recorded (2026-09-14),items 6–9.
This is a move and nothing else. No manifest, no
composeStacks, no per-packageindex.ts, no navigation change, no behaviour change.objectstack.config.tskeeps itssingle
defineStack(). The compiled artifact is byte-identical — proven below, not argued.Type of Change
Related Issues
Fixes #1905
Related to #1904 · #1906 (the plan document this implements) · #1819 (the
.github/tasks/brief that still described the retired
packages/layout is rewritten here as part of thesame class; that card is not addressed by this PR and remains open)
Changes Made
src/, bygit mvso history follows (git log --follow).Assignment is the plan's rule — an item lives with the object it is authored against —
taken from
docs/architecture/module-split-inventory.json, whosefiles[]alreadycarried a module per file.
campaign.hook.tssplit three ways (the two hooks oncrm_lead/crm_opportunitymoved tosrc/sales/objects/beside those objects);global.actions.tsbecamesrc/sales/actions/activity-actions.tswith thecrm_casetriple instantiated from it by
src/service/actions/case-activity.actions.ts;billing-handoff.flow.tssplit by trigger object across sales and revenue; the three*.seed.tsfamily files split by object ownership.objectstack.composition.tsis new. It collects the four packages' barrels into thearrays
defineStack()takes. It is a separate module becauseobjectstack.config.tsmay carry no named export — see A premise the card did not know about below.
scripts/check-source-token-ratchet.mjs) measuressrc/sales/,prints a reading for each of the four package directories, and re-anchors all three
ceilings. See item 5 below.
test/helpers/src-roster.ts, is where a suite learns the packagelayout; the app-wide sweeps read their file rosters from it instead of hand-listing
directories. No pin is deleted or weakened.
AGENTS.mdProject Architecture,README.md,docs/README.md·ARCHITECTURE.md·STATUS.md·MAINTENANCE.md·DEPLOYMENT.md·developers/*,content/docs/getting-started/for-developers*.mdxandcontent/docs/customization/index*.mdx(all three locales each),.github/labeler.yml,.github/tasks/*.md,.github/instructions/*.md,scripts/lib/source-hygiene-surface.mjs, and a$hand_editsentry plus re-pointedroster in
docs/architecture/module-split-inventory.json.Acceptance
1. The artifact is byte-identical
cmpexits 0, so there is no normalised diff to show: the bytes are the same. The builderis deterministic on identical input — measured first, by building the base twice and
comparing (
cmpexit 0) — which is what makes byte-identity the meaningful test ratherthan merely a lucky one.
Order was the work. The builder writes each collection into the artifact in the order
it is handed. Measured, not assumed: appending
.reverse()toobjects,hooksandviewsin the unmodified base config and rebuilding produces a different artifact. Soobjectstack.composition.tsreproduces the exact pre-move order two ways —byExportName()for the collections that used to be a single
Object.values(barrel)(a module namespaceenumerates in ascending code-unit order of the export NAME, which is why the pre-move
pages[]began withaccount_detail_pagealthough the barrel declaredLeadDetailPagefirst), and an explicit ordered list for
hooks,flows,skills,sharingRulesanddata, which were explicit ordered lists before too and whose order interleaves thepackages.
2.
pnpm verifygreenEach step run separately, exit code captured before any output was read:
3.
find src -maxdepth 1Four packages, no
shared/, nopackages/— andsrc/docs/, which is a declareddeviation from the card's acceptance wording, not an oversight. It is the one directory
under
src/that is not authored metadata:objectstack buildcompiles(config dir)/src/docs/*.mdinto the artifact'sdocs[](ADR-0046) from that fixed pathand no other. Moving those four files to
src/sales/docs/was tried first and issilent — the build stays exit 0, prints its usual "Collecting package docs" line, and
the artifact simply loses its
docskey. Keeping them where the platform reads them iswhat acceptance 1 costs; compensating for it in this repo (walking the tree and declaring
docs:inline) would be building platform tooling here, which AGENTS.md forbids. Recordedin
AGENTS.mdbeside the tree, and reported for an upstream card.4. Every cross-directory import under
src/targetssrc/sales/A specifier with a single
../stays inside its own package (../objects/foofromsrc/sales/views/); every specifier that leaves a package directory is in that count, andall 32 point into
src/sales/. Never sideways between modules, never upward.5. The gate prints four readings, and the ceilings are re-derived
The three whole-tree ceilings retire with a worked row each, and the three sales ceilings
are
anchor(reading)— the script's own rule,reading × 1.05rounded up to the next1,000:
business semanticswas a RULED ceiling of 100,000 (#1601), and the script's owndiscipline makes a ruled ceiling symmetric: lowering one needs a maintainer ruling
quoted in the lowering PR's body, exactly as raising it does. This is that ruling, verbatim
and untranslated — it is the ruling that puts the gate on sales in the first place:
With the scope moved there is no reading the 100,000 grant could still be about, so the
layer returns to the ANCHORED kind. The same quote heads the script's ceiling section.
The README banner restates its two figures from this reading, and now says which unit they
are about: the sales package, which is what ADR-0130 §1.3(b) makes the claim about — a CRM
sales module fits whole in an AI context window.
6. Changeset
.changeset/a-directory-under-src-is-a-package.md, empty frontmatter — the sanctioned"releases nothing" declaration. The artifact is byte-identical, so nothing ships to users.
7. Governed PR
Touches
AGENTS.md, so it stays a draft and the maintainer merges it. No seat flips itready and no seat arms auto-merge.
A premise the card did not know about
objectstack.config.tsmay carry no named export.objectstack buildparses theconfig module against
ObjectStackDefinitionSchema, which is.strict, so any exportbeside the default is an unrecognised top-level stack key. Measured on the unmodified
pre-move tree, so it is a standing platform constraint and not something this PR
introduced:
About a dozen suites assert against the AUTHORED collections upstream of
defineStack()—which is the only side of that call where
CrmSeedDataand the stack'sdataaredistinguishable at all, as
test/saas-composition.test.tssays in its own words. So thecollection step lives in
objectstack.composition.ts, a sibling module that is allowed toexport, and
objectstack.config.tsstays exactly what it was: the manifest, thecapabilities, the composition knob and one
defineStack().Testing
pnpm test— 165 files, 3474 passed, 1 skipped)pnpm lint,pnpm lint:i18n-gate,pnpm hygiene,pnpm hygiene:tokens)pnpm build, and the artifact is byte-identical to the base)test/helpers/src-roster.tsplus the re-scopedtest/source-token-ratchet.test.ts, which now exercises the RULED branch against asandbox gate that declares one (no committed ceiling is ruled any more, and the
branch is still the gate's)
Checklist
维护者速读(草稿)
改了什么
把
src/下的每一个源文件,搬进「拥有它的那个包」的目录里:src/sales/(就是 app 包本身,客户核心 + 销售管道 + 活动记录)、
src/service/(工单与知识库)、src/revenue/(产品、报价、合同)、
src/marketing/(市场活动)。src/下的一个目录就是一个包,没有packages/这一层,也没有
shared/。除了搬家,什么都没做:没有加 manifest,没有
composeStacks,没有每个包自己的index.ts,导航没动,行为没动。构建产物
dist/objectstack.json与搬家前逐字节相同。为什么改
这是把您 2026-09-14 的裁决落到代码里的第一步,也是后面拆包的前提:
按元数据类型平铺的老结构,把模块边界藏在了 barrel 文件里——而那恰恰是 ADR-0130 的「每个模块自己
的上下文预算」最需要看得见的东西。搬完之后边界就是目录,肉眼可见:hook 必须放在它所挂的对象旁边
(这就是「一个模块的 hook 不能挂到别人的对象上」这条规矩的物理保障),import 只能朝自己目录或
src/sales/走,不能横着走。token 门禁按您的裁决改成只量
src/sales/——客户真正安装的那个单元——并顺带打印四个包各自的读数,将来给每个模块单独定预算时有现成数字。sales 的三条上限按脚本自己的
anchor()规则从新读数重新推导:业务语义 53,000 / 交互层 31,000 / 授权总量 97,000。其中「业务语义」原本是 100,000 的裁决
上限,而降低一条裁决上限同样需要您的裁决——就是上面那句「token 门禁: 放到 sales」,PR 正文和
脚本头部都原样引用了。
风险与代价(含回滚)
风险很低,因为有一个非常硬的验证:产物逐字节相同(sha256 一致),
pnpm verify全绿(validate / typecheck / lint / i18n / hygiene / token 门禁 / build / 3474 个测试)。也就是说
运行时看到的东西一个比特都没变——对象、字段、hook、流程、视图、页面、导航、权限、种子数据的回放
顺序,全部一致。
代价有两处,都写在正文里:
src/docs/这四个产品内文档没有搬进src/sales/。平台的objectstack build只认src/docs这一个固定路径(ADR-0046),搬走之后构建照样退出 0、产物里的docs却整个消失——这是试出来的,不是猜的。所以它留在原地,并在
AGENTS.md里写明原因。这条会作为平台侧的一个缺口上报,不在本仓库绕过。
objectstack.config.ts不允许有任何具名导出(平台的既有约束,在未修改的旧树上实测确认)。所以新增了一个同级文件
objectstack.composition.ts专门做「把四个包汇总起来」这件事,config本身仍然只有一个
defineStack()。回滚:本 PR 是一条独立分支上的几个提交,没有数据迁移、没有发版内容(changeset 是空
frontmatter 的「不发布」声明)。直接 revert 即可,产物不会有任何变化——因为它本来就没变。
席位意见
(留空,待席位填写)
你要做的
src/sales/同时是 app 包、又装着客户核心和活动对象这一点。
对外宣称数字也随之从「~85k / ~37k(整树)」改成「~50k / ~29k(sales 包)」。
AGENTS.md),由您自己合并——席位不会把它从 draft 翻成 ready,也不会开 auto-merge。
Generated by Claude Code