Repository navigation
Layout: move every authored file into its package directory (src/sales · src/service · src/revenue · src/marketing) — one stack still registers everything, artifact bit-identical #1905
Description
Activity
- addedpm:epicParent delegated to a dedicated epic PM — other PMs never dispatch into its subtreeParent delegated to a dedicated epic PM — other PMs never dispatch into its subtreepm:dispatchedDispatched to a dev agent by /pm-dispatchDispatched to a dev agent by /pm-dispatch
on Sep 14, 2026 os-elon-musk commented
on Sep 14, 2026 CollaboratorAuthorMore actionsClaim: PM loop round 1 (epic #1904)
Session:session_01T3YsvpK1PvYf9n1YUhYP6W
Branch:claude/issue-1905-package-directory-layout
Worktree:hotcrm-issue-1905
Domain:repo:hotcrm(single-lane repo; epic #1904 subtree, nodomain:*)
File surface:src/**(moves and import re-pointing),objectstack.config.ts,scripts/check-source-token-ratchet.mjs,scripts/check-source-hygiene.mjs(only if it names directories),test/**(path re-pointing through one roster helper),.github/labeler.yml,.github/tasks/*.md,.github/instructions/*.md,AGENTS.md,docs/README.md,docs/ARCHITECTURE.md,docs/STATUS.md,docs/architecture/module-split-inventory.json,content/docs/getting-started/**,README.md,.changeset/*.md(stop on breach; explain in the report)
Container & model:L(a whole-tree move — 18 objects and every item authored against them, plus ~105 test files that namesrc/paths; correctness judged by the bit-identical artifact and the gate farm, design judgement in the seed-order split and the gate re-scope),mode:subagent(the os-dev role file and both repository checkouts live in this session's container; a cloud session's source set is unverified in this environment — recorded as the reason for not using cloud on an L card),model: default judgment tier (the os-dev floor). hotcrm carries no scripts/pm/dispatch-gates.mjs, so there is no --tier output to cite; AGENTS.md is a governed surface but not a protocol-semantics surface, so construction runs at the default tier and the governed hunk is reviewed at the contract-review tier by the seat.
Clause-②: no
Thread-read: none (no comments on the card at the moment this claim is written; 2026-09-14T08:51Z)
Serial constraints cleared:#1893 (draft, claude[bot], head claude/issue-1185-notify-template-conversion, in flight since 2026-09-11) touches src/flows/** and src/translations/** — files this card moves. Disposition: not folded (different defect shape, different card family); serial at landing — the dev merges origin/main immediately before opening the PR and again if #1893 lands first (git rename detection carries its hunks across the move); a conflict goes to the merge queue, never hand-ordered. #1906 (docs only, this session) and the five dependabot PRs (package.json / lockfile only) do not intersect. No other claim exists on the #1904 subtree. origin/main tip at this reading: 590b095 (2026-09-14T08:49Z); src/ still flat by metadata type — the premise holds.
Generated by Claude Code
os-elon-musk commented
on Sep 14, 2026 CollaboratorAuthorMore actionsos-dev-report
{ "issue": 1905, "status": "done", "branch": "claude/issue-1905-package-directory-layout", "pr": "https://github.com/objectstack-ai/hotcrm/pull/1910", "premise_still_valid": true, "summary": "Every authored file under src/ moved by git mv into its package directory (src/sales as the type:app package, src/service, src/revenue, src/marketing) per plan items 6-9; the named exceptions were split by hand (campaign.hook.ts three ways, global.actions.ts into a sales factory plus a service instantiation, billing-handoff.flow.ts by trigger object, the three seed family files by object ownership). dist/objectstack.json is BYTE-IDENTICAL to the build on origin/main (3118ccf) - same sha256 - which took a deliberate ordering design: the builder writes each collection in the order it is handed (measured by a reverse-and-rebuild ablation on the untouched base), so the new objectstack.composition.ts reproduces the pre-move order with byExportName() for the namespace-derived collections and explicit ordered lists for hooks/flows/skills/sharingRules/data. Two premises of the card were falsified by measurement and are recorded in the PR body: (1) objectstack.config.ts may carry NO named export - the build parses it against a strict stack schema - which is why the collection step is a sibling module rather than the top of the config, confirmed by appending a probe export to the UNMODIFIED config at 590b095 and watching the build fail by name; (2) src/docs/ cannot move under src/sales/ - objectstack build reads package docs from (config dir)/src/docs and no other path (ADR-0046), and moving them is silent: exit 0, usual 'Collecting package docs' line, artifact loses its docs[] key. src/docs/ therefore stays at the top of src/, which is a declared deviation from acceptance item 3, taken because acceptance item 1 (bit-identical artifact) is the card's headline and compensating in this repo would be platform tooling AGENTS.md forbids.", "gates": { "pnpm validate": "exit 0", "pnpm typecheck": "exit 0", "pnpm lint": "exit 0", "pnpm lint:i18n-gate": "exit 0", "pnpm hygiene": "exit 0", "pnpm hygiene:tokens": "exit 0 - business semantics ~49,978 (ceiling 53,000) / interaction layer ~29,344 (ceiling 31,000) / authored total ~91,910 (ceiling 97,000), plus the four per-package readings", "pnpm build": "exit 0", "pnpm test": "exit 0 - 165 files, 3474 passed, 1 skipped", "artifact diff": "cmp exit 0 against origin/main 3118ccf; sha256 7165de9aba7aa4ef27e8f05bf77236832eb693fe83e949e52da32c87ae594506 both sides", "note": "every step run separately with its exit code written to a file before any output was read; all eight re-run after merging origin/main (3118ccf) and all still 0" }, "line_budget": "n/a - no skills/** file touched, so no line ratchet applies to this PR", "tests": "pnpm test 165 files / 3474 passed / 1 skipped. New: test/helpers/src-roster.ts (the one place a suite learns the package layout; app-wide sweeps take their file rosters from it). Re-scoped: test/source-token-ratchet.test.ts now derives LAYER_DIRS/EXCLUDED_DIRS from the gate's own exported constants instead of a second hand-written copy, and exercises the RULED-ceiling branch against a sandbox gate patched to declare one (no committed ceiling is ruled after the re-anchor, and the branch is still the gate's). Ablations, both on the UNMODIFIED base worktree with a trap-based restore and a git-diff-empty check afterwards: (a) determinism - two builds of the base are byte-identical; (b) order sensitivity - appending .reverse() to objects/hooks/views in the base config changes the artifact, which is what makes the ordering design load-bearing rather than decorative; (c) the named-export probe described in summary. No permanent test file was left behind by any of them.", "mcp_calls": "4 - issue_read get, issue_read get_comments, create_pull_request, pull_request_read get (the body read-back). Plus this comment.", "api_writes": "3 git pushes to the branch (the empty-branch write probe, then two content pushes). No REST proxy writes and no label write - this repo's labeler applies what it applies and the card required none.", "deviations": [ "src/docs/ stays at the top of src/ rather than moving to src/sales/docs/, so `find src -maxdepth 1` lists five entries, not four. Reason measured, not assumed: the CLI resolves package docs as path.join(dirname(configPath), 'src', 'docs') and reads nothing else, so under a single-config build no src/<pkg>/docs/ is ever read. Moving them keeps the build at exit 0 and silently drops the artifact's whole docs[] array. Acceptance 1 (bit-identical artifact) and AGENTS.md rules 1-3 (a platform gap is filed upstream, never compensated for here) both point the same way. Written into AGENTS.md beside the tree and into the PR body as a declared deviation.", "The collection step lives in a NEW root module, objectstack.composition.ts, rather than at the top of objectstack.config.ts as the card's wording implies. Forced: the config may carry no named export (strict stack schema), and about a dozen suites assert against the AUTHORED collections upstream of defineStack() - the only side of that call where CrmSeedData and the stack's data are distinguishable, as test/saas-composition.test.ts states in its own comment. The file is enrolled in ROOT_TEXT_FILES (scripts/lib/source-hygiene-surface.mjs) and in the labeler's `configuration` globs.", "Per-package hook barrels (<pkg>/objects/hooks.ts) are named re-export barrels, like every other barrel in this tree, and the assembled ordered list lives in objectstack.composition.ts rather than being four package lists concatenated at the root. Concatenation cannot reproduce the pre-move flat hook order, which interleaves the packages (account=sales, article_feedback=service, campaign=marketing, campaign_member=marketing, case=service, contact=sales, ...), and the builder writes that order into the artifact. Same reasoning for flows, skills, sharingRules and the seed replay order.", "The token gate's `business semantics` ceiling was a RULED 100,000 (#1601). All three ceilings are re-anchored from the sales reading, which for that one is a LOWERING and therefore needs a ruling quoted in the lowering PR - the ruling is the one that put the gate on sales, quoted verbatim and untranslated in both the script header and the PR body.", "The README banner's two headline figures change meaning, not just value: ~85k/~37k (whole tree) becomes ~50k/~29k (the sales package), because the gate now measures the unit ADR-0130 makes the claim about. Flagged in the maintainer section for confirmation. The repository's GitHub DESCRIPTION still reads 'business logic under 100k tokens' - a repo setting, not a file, so outside this PR's diff; listed under out_of_scope_findings." ], "files_changed": "374 files, +6971 / -5711 across 4 commits. 205 files moved under src/ (git mv, history preserved); new: objectstack.composition.ts, test/helpers/src-roster.ts, src/sales/objects/{lead,opportunity}.campaign-metrics.hook.ts, src/service/actions/case-activity.actions.ts, src/revenue/flows/billing-handoff-contract-activated.flow.ts, src/revenue/data/opportunity-line-item.seed.ts, src/sales/data/{activity,forecast}.seed.ts, four <pkg>/objects/hooks.ts, the per-package per-type barrels, .changeset/a-directory-under-src-is-a-package.md; retired: src/hooks/index.ts and the fifteen flat src/<type>/index.ts barrels.", "open_questions": [], "out_of_scope_findings": [ "to file (3 classes, dedupe words: package docs, src/docs, ADR-0046, collect-docs, multi-package): PLATFORM - the ADR-0046 package-docs collector resolves exactly path.join(dirname(configPath), 'src', 'docs'), so under the ADR-0130 multi-package layout no per-package docs directory is reachable, and moving the directory produces a GREEN build (exit 0, the usual 'Collecting package docs' step printed) whose artifact has lost its docs[] entirely. Silent metadata loss on an exit-0 build is the hazard class, not the fixed path itself. Belongs upstream in objectstack alongside the two gaps plan item 7 already filed (#18163, #18164); this repo must not compensate for it.", "to file (3 classes, dedupe words: objectstack.config, named export, strict stack schema, unrecognized_keys, config exports): PLATFORM - objectstack.config.ts may carry no export beside the default, because the build parses the whole config MODULE against the strict ObjectStackDefinitionSchema. The diagnostic is good ('Unrecognized key(s) on this stack definition'), but it names a config-authoring constraint nothing documents, and it is what forces any app wanting importable composition helpers into a sibling module. Worth an upstream decision: either document the constraint on the config surface, or read only the default export. Measured on hotcrm at 590b095, unmodified.", "noted, not filed: the repository's GitHub description still reads 'business logic under 100k tokens', which was the whole-tree claim; the gate now commits 53,000 for the sales package and the README banner says ~50k. It is a repo setting rather than a file, so no PR can carry it. Successor: the maintainer, when confirming the new ceilings in PR #1910 (the maintainer section asks about exactly these figures).", "noted, not filed: src/sales/interfaces/index.ts is an empty barrel - a copyright header and nothing else - registered by nothing and imported by nothing. It moved with the tree unchanged because this PR is a move. Successor: the packaging PR (#1904 step 2), which decides each package's index.ts surface and will have to decide whether this one has a reason to exist.", "noted, not filed: docs/architecture/module-split-inventory.json files[] is short by 35 files that postdate its measured_at commit (three flows and the 32 per-locale translation modules). Left out deliberately per the file's own $maintenance rules - it is evidence for one measurement, not an index of the tree - and recorded in the $hand_edits entry this PR adds. Successor: whoever next re-measures the split; there is no live pointer broken by it." ] }
Generated by Claude Code
os-elon-musk commented
on Sep 14, 2026 CollaboratorAuthorMore actionsVerdict: ACCEPT — PR #1910 (draft, governed: touches
AGENTS.md; awaiting the maintainer's own merge). Reviewed by the epic PM, sessionsession_01T3YsvpK1PvYf9n1YUhYP6W, at 2026-09-14T10:14Z against PR heade06226c(base3118ccf).Implemented-by: os-dev subagent, this session (branch
claude/issue-1905-package-directory-layout)
Reviewed-by: epic PM, this sessionChecklist conclusions (verified on GitHub and on the fetched branch, not on the report's narrative)
- PR shape: draft ✓, base
main✓, first lineFixes #1905✓; the epic isPart of #1904and.github/tasks/new-package.mdstill prescribes the retiredpackages/scaffold — #1518's class, the site that card did not reach #1819 / docs(architecture): PSA module design, REQ-0002, and the final ADR-0130 module shape #1906 appear with no closing keyword adjacent ✓. - Scope: 374 files — 197 renames, 36 adds, 4 deletes under
src/(the flatdata/,flows/,hooks/,skills/index barrels), plus the declared re-pointing surface. Two files outside the claim's declared surface, both mechanical import re-pointing forced by the move:scripts/demo-staff.ts,scripts/analytics-reconcile/run.ts(read: they merge the per-package barrels).docs/DEPLOYMENT.md,docs/MAINTENANCE.md,docs/developers/*,content/docs/customization/*are the same class (docs namingsrc/<dir>/). Nocontent/docs/releases/change ✓. Changeset: empty frontmatter with the zero-release declaration ✓ — correct for this repo since the artifact is unchanged. - Gate evidence, pinned to head
e06226c: all 10 check runscompleted: success— Quality Checks, Typecheck and Build, Build and Test (22.x), Check Changeset, Playwright, link-check, CodeQL, Analyze Code, Label Pull Request, Vercel ✓. - Artifact byte-identity, re-measured independently: two fresh worktrees (
3118ccfande06226c),pnpm install --frozen-lockfile+pnpm buildin each,cmpexit 0, sha2567165de9aba7aa4ef27e8f05bf77236832eb693fe83e949e52da32c87ae594506on both sides — the dev's headline reading reproduces ✓. - Import direction, re-measured on the branch tree: 32 cross-directory specifiers, all
../../sales/✓.src/top level:docs,marketing,revenue,sales,service✓ (see deviation 1). - Governed hunk (
AGENTS.mdProject Architecture), read at the contract-review tier: states the directory-is-a-package rule, the three layout rules (hook beside its object, imports along the edges only, item lives with its object), keeps and explains thepackages/<x>/src/prohibition as the retired monorepo shape, and records thesrc/docs/exception with its reason. Consistent with plan items 6–8; no protocol-semantics text changed ✓. - Gate script: scope
src/sales/, per-package readings printed, all three ceilings re-anchored byanchor()with worked rows; the lowering of the ruledbusiness semanticsceiling quotes the ruling 「所以应该先分拆基础的crm, 比如 sales 和 support, token 门禁: 放到 sales」 in the header ✓.
Deviations, all accepted
src/docs/stays at the top ofsrc/— the CLI reads package docs from<config dir>/src/docsonly (confirmed in the pinned@objectstack/clidist: "compile flatsrc/docs/*.md"); moving it is a silent loss. The plan is amended beside item 6 (claude/gifted-planck-behk4r, PR docs(architecture): PSA module design, REQ-0002, and the final ADR-0130 module shape #1906) and the gap is filed as cli: the ADR-0046 package-docs collector reads only<config dir>/src/docs— under an ADR-0130 package layout a moved docs directory produces a green build whose artifact has silently lostdocs[]objectstack#18170.objectstack.composition.tsas a sibling module — forced by the config module's strict parse (measured on the unmodified base); filed as cli:objectstack.config.tsmay carry no named export — the build parses the whole config module against the strict stack schema, and nothing documents that constraint objectstack#18171.- Hook / flow / skill / sharing / seed ordering assembled in the composition module rather than concatenated per package — required for byte-identity; the packaging card decides the per-package
index.tssurface. - README banner figures now describe the sales package (~50k / ~29k) instead of the whole tree — for the maintainer to confirm in the quick-read.
Findings from the report: the two "to file" platform findings are now filed (#18170, #18171); the three "noted, not filed" items stay noted (repo description text; the empty
src/sales/interfaces/index.tsbarrel — for the packaging card; the inventory's stalefiles[]— recorded in its$hand_edits).Next:
needs-user-decisionon this card = the maintainer's merge decision on #1910 (governed PR, no seat flips it ready). #1907 stays blocked until #1910 is MERGED.
Generated by Claude Code
- PR shape: draft ✓, base
- addedneeds-user-decisionNeeds the maintainer's call before work proceedsNeeds the maintainer's call before work proceedsand removedpm:dispatchedDispatched to a dev agent by /pm-dispatchDispatched to a dev agent by /pm-dispatch
on Sep 14, 2026 - removedneeds-user-decisionNeeds the maintainer's call before work proceedsNeeds the maintainer's call before work proceeds
on Sep 14, 2026 - added 3 commits that reference this issue
on Sep 15, 2026
Part of #1904 (step 1 of the sequence). Spec of record:
docs/architecture/module-split-plan.md, Decisions recorded (2026-09-14), items 6–9 — on branchclaude/gifted-planck-behk4runtil PR #1906 merges. Raw URL of the spec on that branch (the markdown link form gets mangled by the body sanitizer):https://github.com/objectstack-ai/hotcrm/blob/claude/gifted-planck-behk4r/docs/architecture/module-split-plan.md
Read it in full before the first edit; this card is the pointer, not a second copy.
Ruling (verbatim, untranslated)
What this PR does — a move, nothing else
Every authored file under
src/moves (git mv, history kept) into the directory of the package that will own it. No manifest is added,composeStacksis not introduced, no navigation changes. Rootobjectstack.config.tskeeps its singledefineStackwith the app manifest and imports the collections from the four directories, concatenating them. The compiled artifact must come out the same as today's.Target tree — top-level directories under
src/are exactly the packages:Inside each: the metadata-type subdirectories that package uses (
objects/,views/,pages/,flows/,actions/,datasets/,reports/,dashboards/,mappings/,skills/,sharing/,data/, and for sales alsoapps/,profiles/,translations/,docs/,interfaces/), each with an explicit file-by-file barrel exactly as today.*.hook.tssits beside its*.object.ts; the hook registration list for a package lives at<pkg>/objects/hooks.ts(replacessrc/hooks/index.ts), and the root config concatenates the four lists. Oneindex.tsper package directory is not created in this PR (that is the packaging card) — the root config imports the per-type barrels directly.Assignment rule and the named exceptions
An item lives with the object it is authored against (view, page, action, dataset, report, sharing rule, seed rows, import mapping, skill, hook, flow by trigger object). Items with no single object go to
sales/: the app (apps/crm.app.ts),home/app_launcher/utility_barpages,executiveandcrm(overview) dashboards, the cross-domain skills, all profiles,positions.ts, the four locale packs,src/docs/*.md,demo-bootstrap.flow.ts. Exceptions that need a hand:src/objects/campaign.hook.tscarries four hooks; the two attached tocrm_leadandcrm_opportunitymove tosales/objects/(one file each, e.g.lead.campaign-metrics.hook.ts,opportunity.campaign-metrics.hook.ts); the two oncrm_campaignstay inmarketing/objects/campaign.hook.ts.test/campaign-member-lifecycle.test.tspins the four bodies character-identical — keep the pin, re-point its paths.src/actions/global.actions.ts: the factory becomessales/actions/activity-actions.tsand instantiates the four sales-hosted triples there;service/actions/instantiates thecrm_case-hosted triple by importing the factory fromsales/(the only allowed direction).src/flows/billing-handoff.flow.tscarries two flows on two trigger objects: theclosed_wonflow goes tosales/flows/, thecontract_activatedflow torevenue/flows/;_billing-endpoint.tsand_guarded-iteration.tsgo tosales/flows/and are imported from there.sales.seed.tscarriesopportunityLineItems;service.seed.tscarries tasks and events;revenue.seed.tscarries forecasts): split them along object ownership, and keep today's replay order in the root config'sdatalist — rows reference rows seeded earlier and the seed tests pin that._hook-api.ts,_picklists.ts,_thresholds.ts,shared-widgets.ts,data/_shared.ts,mappings/_shared.ts→sales/(their current subdirectory names); consumers in other packages import them fromsales/— never copied. One copy of each is an acceptance criterion.Re-pointed in the same PR
scripts/check-source-token-ratchet.mjs: scope becomessrc/sales/(the two layers keyed by subdirectory name —objects/flows/actionsbusiness,views/pages/dashboards/appsinteraction;translations/anddata/stay excluded); print a reading for each of the four package directories; re-derive the sales ceilings from the new reading with the script's ownanchor()rule, retiring the whole-tree ceilings with a worked row. Lowering the ruledbusiness semanticsceiling is authorised by the ruling quoted above — quote it in the PR body and in the script header.test/source-token-ratchet.test.tslearns the scope.scripts/check-source-hygiene.mjsif it names directories (test/source-hygiene-scan-surface.test.tssays)..github/labeler.yml: every glob must still match something (test/labeler-config.test.ts).test/**: path references go through one roster helper undertest/helpers/rather than 100 hand-edited strings; no pin is deleted or weakened.src/<dir>/:AGENTS.mdProject Architecture (rewrite: a directory is a package; the import rule "own directory orsrc/sales/, never sideways"; hooks beside objects),docs/README.mdCurrent Repository Shape,docs/ARCHITECTURE.md,docs/STATUS.md,content/docs/getting-started/for-developers*.mdx(all three locales),.github/tasks/new-feature.mdandnew-package.md(.github/tasks/new-package.mdstill prescribes the retiredpackages/scaffold — #1518's class, the site that card did not reach #1819 is the same class — do notFixesit),.github/instructions/*.mdwhere they citesrc/{type}/.test/docs-src-tree-paths.test.tsis the guard.docs/architecture/module-split-inventory.json: add a$hand_editsentry recording the move (the file forbids regeneration).Acceptance
pnpm buildon the base commit and on the PR head produce the same artifact: same objects with the same FQNs, fields, hooks, flows, views, pages, navigation and seed order. Prove it with a canonical-JSON diff ofdist/objectstack.jsonin the PR body (byte-identical if the builder is deterministic; otherwise the normalised diff is empty).pnpm verifygreen (validate, typecheck, lint, i18n gate, hygiene, token ratchet, build, test).find src -maxdepth 1lists exactlysales service revenue marketing; noshared/, nopackages/.src/targetssrc/sales/.AGENTS.md): stays draft, body carries## 维护者速读(草稿); the maintainer merges.Out of scope
Manifests,
composeStacks, navigation contributions, per-packageindex.ts(all #1904 step 2). Any behaviour change. Filing the two upstream platform gaps named in plan item 7 (filed: objectstack-ai/objectstack#18163, objectstack-ai/objectstack#18164).