Skip to content

refactor(layout): a directory under src/ is a package — move every authored file into sales / service / revenue / marketing - #1910

Merged
os-elon-musk merged 4 commits into
mainfrom
claude/issue-1905-package-directory-layout
Sep 14, 2026
Merged

os-elon-musk merged 4 commits into
mainfrom
claude/issue-1905-package-directory-layout

Conversation

@os-elon-musk

Copy link
Copy Markdown
Collaborator

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/ (the type: app package), src/service/, src/revenue/, src/marketing/.
A directory under src/ is a package — no packages/ level, no shared/ — per
ADR-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-package
index.ts, no navigation change, no behaviour change. objectstack.config.ts keeps its
single defineStack(). The compiled artifact is byte-identical — proven below, not argued.

Type of Change

  • Code refactoring
  • Documentation update

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 the
same class; that card is not addressed by this PR and remains open)

Changes Made

  • The move. 205 files under src/, by git mv so 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, whose files[] already
    carried a module per file.
  • The named exceptions. campaign.hook.ts split three ways (the two hooks on
    crm_lead / crm_opportunity moved to src/sales/objects/ beside those objects);
    global.actions.ts became src/sales/actions/activity-actions.ts with the crm_case
    triple instantiated from it by src/service/actions/case-activity.actions.ts;
    billing-handoff.flow.ts split by trigger object across sales and revenue; the three
    *.seed.ts family files split by object ownership.
  • objectstack.composition.ts is new. It collects the four packages' barrels into the
    arrays defineStack() takes. It is a separate module because objectstack.config.ts
    may carry no named export — see A premise the card did not know about below.
  • The token gate (scripts/check-source-token-ratchet.mjs) measures src/sales/,
    prints a reading for each of the four package directories, and re-anchors all three
    ceilings. See item 5 below.
  • One roster helper, test/helpers/src-roster.ts, is where a suite learns the package
    layout; the app-wide sweeps read their file rosters from it instead of hand-listing
    directories. No pin is deleted or weakened.
  • Docs, briefs and configuration follow the tree: AGENTS.md Project Architecture,
    README.md, docs/README.md · ARCHITECTURE.md · STATUS.md · MAINTENANCE.md ·
    DEPLOYMENT.md · developers/*, content/docs/getting-started/for-developers*.mdx and
    content/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_edits entry plus re-pointed
    roster in docs/architecture/module-split-inventory.json.

Acceptance

1. The artifact is byte-identical

$ pnpm build            # on origin/main (3118ccf), in a second worktree at that commit
$ pnpm build            # on this branch
$ cmp base/dist/objectstack.json dist/objectstack.json; echo "exit=$?"
exit=0
$ sha256sum base/dist/objectstack.json dist/objectstack.json
7165de9aba7aa4ef27e8f05bf77236832eb693fe83e949e52da32c87ae594506  base(3118ccf)/dist/objectstack.json
7165de9aba7aa4ef27e8f05bf77236832eb693fe83e949e52da32c87ae594506  dist/objectstack.json

cmp exits 0, so there is no normalised diff to show: the bytes are the same. The builder
is deterministic on identical input — measured first, by building the base twice and
comparing (cmp exit 0) — which is what makes byte-identity the meaningful test rather
than 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() to objects, hooks and
views in the unmodified base config and rebuilding produces a different artifact. So
objectstack.composition.ts reproduces the exact pre-move order two ways — byExportName()
for the collections that used to be a single Object.values(barrel) (a module namespace
enumerates in ascending code-unit order of the export NAME, which is why the pre-move
pages[] began with account_detail_page although the barrel declared LeadDetailPage
first), and an explicit ordered list for hooks, flows, skills, sharingRules and
data, which were explicit ordered lists before too and whose order interleaves the
packages.

2. pnpm verify green

Each step run separately, exit code captured before any output was read:

0 pnpm validate
0 pnpm typecheck
0 pnpm lint
0 pnpm lint:i18n-gate
0 pnpm hygiene
0 pnpm hygiene:tokens
0 pnpm build
0 pnpm test      # 165 files, 3474 passed, 1 skipped

3. find src -maxdepth 1

$ find src -maxdepth 1 | sort
src
src/docs
src/marketing
src/revenue
src/sales
src/service

Four packages, no shared/, no packages/ — and src/docs/, which is a declared
deviation from the card's acceptance wording, not an oversight. It is the one directory
under src/ that is not authored metadata: objectstack build compiles
(config dir)/src/docs/*.md into the artifact's docs[] (ADR-0046) from that fixed path
and no other. Moving those four files to src/sales/docs/ was tried first and is
silent — the build stays exit 0, prints its usual "Collecting package docs" line, and
the artifact simply loses its docs key. Keeping them where the platform reads them is
what 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. Recorded
in AGENTS.md beside the tree, and reported for an upstream card.

4. Every cross-directory import under src/ targets src/sales/

$ grep -rnoE "from '(\.\./)+[a-z]+/" src/ | grep -vE "from '\.\./[a-z]" | sed "s|.*from '||" | sort | uniq -c
     32 ../../sales/

A specifier with a single ../ stays inside its own package (../objects/foo from
src/sales/views/); every specifier that leaves a package directory is in that count, and
all 32 point into src/sales/. Never sideways between modules, never upward.

5. The gate prints four readings, and the ceilings are re-derived

$ node scripts/check-source-token-ratchet.mjs
Source token ratchet — ratcheted surface: src/sales/**/*.ts minus src/sales/translations, src/sales/data

  scope                    files   lines     chars   ~tokens
  business semantics          44   5,088   199,910    49,978
  interaction layer           25   3,494   117,374    29,344
  other authored metadata     38     992    50,357    12,589
  ──────────────────────────────────────────────────────────
  authored total             107   9,574   367,641    91,910

  Per package — informational, no ceiling:

  package             business semantics     interaction layer  authored total
  src/sales                       49,978                29,344          91,910   <- ratcheted
  src/service                     12,646                 6,228          20,423
  src/revenue                     15,196                 2,136          17,485
  src/marketing                    8,001                   876           9,096

  ✓ business semantics ~49,978 tokens (ceiling ~53,000; headroom ~3,022).
  ✓ interaction layer ~29,344 tokens (ceiling ~31,000; headroom ~1,656).
  ✓ authored total ~91,910 tokens (ceiling ~97,000; headroom ~5,090).

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.05 rounded up to the next
1,000:

  business semantics   49,978 × 1.05 =  52,477 -> ceil 1k ->  53,000  (headroom 3,022, 6.0%)
  interaction layer    29,344 × 1.05 =  30,811 -> ceil 1k ->  31,000  (headroom 1,656, 5.6%)
  authored total       91,910 × 1.05 =  96,506 -> ceil 1k ->  97,000  (headroom 5,090, 5.5%)

business semantics was a RULED ceiling of 100,000 (#1601), and the script's own
discipline 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:

「所以应该先分拆基础的crm, 比如 sales 和 support, token 门禁: 放到 sales」

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 it
ready and no seat arms auto-merge.

A premise the card did not know about

objectstack.config.ts may carry no named export. objectstack build parses the
config module against ObjectStackDefinitionSchema, which is .strict, so any export
beside 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:

$ printf '\nexport const ProbeNamedExport = [1, 2, 3];\n' >> objectstack.config.ts   # at 590b095
$ pnpm build
  ✗ Validation failed
    unrecognized_keys: Unrecognized key(s) on this stack definition: `ProbeNamedExport`.

About a dozen suites assert against the AUTHORED collections upstream of defineStack() —
which is the only side of that call where CrmSeedData and the stack's data are
distinguishable at all, as test/saas-composition.test.ts says in its own words. So the
collection step lives in objectstack.composition.ts, a sibling module that is allowed to
export, and objectstack.config.ts stays exactly what it was: the manifest, the
capabilities, the composition knob and one defineStack().

Testing

  • Unit tests pass (pnpm test — 165 files, 3474 passed, 1 skipped)
  • Linting passes (pnpm lint, pnpm lint:i18n-gate, pnpm hygiene, pnpm hygiene:tokens)
  • Build succeeds (pnpm build, and the artifact is byte-identical to the base)
  • New tests added — test/helpers/src-roster.ts plus the re-scoped
    test/source-token-ratchet.test.ts, which now exercises the RULED branch against a
    sandbox gate that declares one (no committed ceiling is ruled any more, and the
    branch is still the gate's)

Checklist

  • I have added a changeset (empty frontmatter — this PR releases nothing to users)
  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published

维护者速读(草稿)

改了什么

把 src/ 下的每一个源文件,搬进「拥有它的那个包」的目录里:src/sales/(就是 app 包本身,
客户核心 + 销售管道 + 活动记录)、src/service/(工单与知识库)、src/revenue/(产品、报价、
合同)、src/marketing/(市场活动)。src/ 下的一个目录就是一个包,没有 packages/ 这一
层,也没有 shared/。

除了搬家,什么都没做:没有加 manifest,没有 composeStacks,没有每个包自己的 index.ts,
导航没动,行为没动。构建产物 dist/objectstack.json 与搬家前逐字节相同。

为什么改

这是把您 2026-09-14 的裁决落到代码里的第一步,也是后面拆包的前提:

「是不是拆的太细。是否可以去掉 packages 这一层」 / 「shared 有必要吗?是否后期会导致ai的混乱」 /
「为什么需要app ,我觉得是另外一种混乱」 / 「token 门禁: 放到 sales」

按元数据类型平铺的老结构,把模块边界藏在了 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、流程、视图、页面、导航、权限、种子数据的回放
顺序,全部一致。

代价有两处,都写在正文里:

  1. src/docs/ 这四个产品内文档没有搬进 src/sales/。平台的 objectstack build 只认
    src/docs 这一个固定路径(ADR-0046),搬走之后构建照样退出 0、产物里的 docs 却整个
    消失——这是试出来的,不是猜的。所以它留在原地,并在 AGENTS.md 里写明原因。这条会作为平台
    侧的一个缺口上报,不在本仓库绕过。
  2. objectstack.config.ts 不允许有任何具名导出(平台的既有约束,在未修改的旧树上实测确认)。
    所以新增了一个同级文件 objectstack.composition.ts 专门做「把四个包汇总起来」这件事,config
    本身仍然只有一个 defineStack()。

回滚:本 PR 是一条独立分支上的几个提交,没有数据迁移、没有发版内容(changeset 是空
frontmatter 的「不发布」声明)。直接 revert 即可,产物不会有任何变化——因为它本来就没变。

席位意见

(留空,待席位填写)

你要做的

  1. 确认「一个目录就是一个包」这个落地形状符合您的裁决——尤其是 src/sales/ 同时是 app 包、又
    装着客户核心和活动对象这一点。
  2. 确认 token 门禁降到 sales 之后的三条新上限(53,000 / 31,000 / 97,000)可以接受;README 的
    对外宣称数字也随之从「~85k / ~37k(整树)」改成「~50k / ~29k(sales 包)」。
  3. 这是受管 PR(动了 AGENTS.md),由您自己合并——席位不会把它从 draft 翻成 ready,也不会
    开 auto-merge。

Generated by Claude Code

claude Bot and others added 4 commits September 14, 2026 09:25
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
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3YsvpK1PvYf9n1YUhYP6W
@vercel

vercel Bot commented Sep 14, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated
hotcrm Ignored Ignored Sep 14, 2026 10:02am UTC

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation ci/cd CI plumbing and the verification pipeline metadata Declarative metadata — schema, security posture, UI surfaces configuration Build and app configuration files backend Server-side behaviour — hooks, flows, actions labels Sep 14, 2026
Comment on lines +134 to +138
import {
resolveComposition,
accounts, contacts, leads, opportunities,
tasks, events, eventAttendeesFromContacts, eventAttendeesFromLeads, forecasts,
} from './src/sales/data/index.js';

Copy link
Copy Markdown
Collaborator Author

维护者速读(终稿)

席位复核记录在 #1905 的 ACCEPT 评论里;本条是给你读的版本。

改了什么

把 src/ 下的每一个源文件搬进「拥有它的那个包」的目录:src/sales/(app 包本身:客户核心、销售管道、活动记录)、src/service/、src/revenue/、src/marketing/。一个目录就是一个包,没有 packages/ 层,没有 shared/。除搬家外没有任何行为改动:没有 manifest、没有 composeStacks、导航没动。

为什么改

落实你 2026-09-14 的四条裁决(「去掉 packages 这一层」「shared 有必要吗」「为什么需要 app」「token 门禁放到 sales」),是后面拆包(#1907)的前提。边界从 barrel 文件里搬到目录上,肉眼可见;hook 必须与它挂的对象并排,import 只能朝自己目录或 src/sales/ 走。

风险与代价(含回滚)

风险极低,有硬证据。 构建产物逐字节相同——dev 量过一次,席位在两个全新 worktree 里独立重建再量一次,sha256 同为 7165de9a…4506。PR 上 10 项检查全绿(含 Build and Test 3474 个用例)。

代价三处,都已写明:

  1. src/docs/ 留在 src/ 顶层。平台只从这一个固定路径读产品内文档,搬走后构建仍退出 0、产物却整个丢掉 docs[]——试出来的。已报平台卡 objectstack#18170,本仓库不绕行。
  2. 新增 objectstack.composition.ts,因为 objectstack.config.ts 不允许有任何具名导出(在未修改的旧树上实测)。已报平台卡 objectstack#18171。
  3. token 门禁改为只量 src/sales/,三条上限从新读数重新锚定:业务语义 53,000 / 交互层 31,000 / 总量 97,000。其中业务语义原为你裁的 100,000,降低它引用的就是「token 门禁: 放到 sales」这句。README 对外宣称的数字随之从「~85k / ~37k(整树)」改为「~50k / ~29k(sales 包)」。

回滚:直接 revert 这条分支即可,产物不变,无数据迁移,changeset 是空 frontmatter 的"不发布"声明。

席位意见

建议合并。理由:验收一(产物逐字节相同)由席位独立复现;受管面 AGENTS.md 的改动只描述布局与三条规则,没有触碰协议语义;三处代价都是平台约束下的最小路径,且各自已上报上游而不是在本仓库绕过。一个提醒:README 的对外数字变的是口径(整树 → sales 包),不只是数值,仓库的 GitHub description 仍写着"business logic under 100k tokens",那是仓库设置,需要你顺手改。

你要做的

一个动作:确认上述口径后,合并本 PR(受管 PR,席位不会翻 ready 或挂 auto-merge)。合并后 #1907(sales 成 app 包、service 成模块)会由席位自动认领派发。


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Browser verification (seat, 2026-09-14T14:05Z) — pnpm dev source path, head e06226c vs main 3118ccf

The maintainer asked whether this had been run in a browser. It had not — the review rested on the byte-identical artifact (the objectstack start path) plus CI's Playwright job, which also starts from the built artifact. objectstack dev boots from source, so that path is now exercised directly, on two fresh instances (fresh SQLite each, --seed-admin, ports 4105 / 4106), driven headless with the repository's own Chromium build. Same script on both sides; every reading below is head vs main.

Authoritative server metadata, identical. GET /api/v1/meta/app?id=crm_enterprise → 33 navigation nodes, byte-for-byte the same flattened tree (Home; Sales ×9; My Work ×6 incl. the approvals:inbox component; Activity ×2; Marketing ×1; Service ×3; Insights ×5). GET /api/v1/meta/objects → 98 objects, 18 crm_*, identical name sets.

Browser walk, 29 steps, all OK on both, 0 page errors on both:

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

os-zhuang pushed a commit that referenced this pull request Oct 9, 2026
… 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend Server-side behaviour — hooks, flows, actions ci/cd CI plumbing and the verification pipeline configuration Build and app configuration files documentation Improvements or additions to documentation metadata Declarative metadata — schema, security posture, UI surfaces

Projects

None yet

3 participants