Skip to content

feat(packaging): one artifact, two packages — sales is the app package, service a module - #2011

Merged
os-zhuang merged 6 commits into
mainfrom
claude/issue-1907-app-and-service-packages
Oct 8, 2026
Merged

os-zhuang merged 6 commits into
mainfrom
claude/issue-1907-app-and-service-packages

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1907

Packaging step 2 of ADR-0130: src/sales/ becomes the type: app package and src/service/ a type: module package inside one release artifact. Rebuilt on main (c529de2b, @objectstack/* 17.7.0) with the blocked branch be11c07 as a read-only reference. No object, field or behaviour changes; no file moves.

⚠️ One acceptance reading is not met, and it is a platform gap, not an unfinished edit — acceptance 2's "no ownership diagnostic": a boot of the two-package artifact logs one [MetadataPlugin] WARN about the four src/docs/*.md pages. See Known gap below. Everything else is measured green.

What this PR does

  1. src/service/index.ts — app.objectstack.hotcrm.service, type: 'module', namespace: 'crm', version: '3.1.0', engines.protocol: '^17.7.0', dependencies: { 'app.objectstack.hotcrm': '^3.1.0' }, its own requires: ['automation', 'triggers', 'analytics', 'sharing'], and three navigationContributions into crm_enterprise (group_service: nav_case, nav_knowledge, nav_service_dashboard; group_work: nav_my_cases; group_insights: nav_report_sla). It registers its own barrels' items by name (3 objects, 8 hooks, 2 views, the case page, 7 flows, 8 actions, case_metrics, 3 reports, the Service dashboard, the case-triage skill, 3 sharing rules, the 2 case email templates) plus the seed families the root hands it.
  2. src/sales/index.ts — createHotCrmAppStack(collections, { artifactObjects }): the app manifest, requires, i18n; collections handed in by the root because src/revenue/ and src/marketing/ are still registered by the app package and a src/sales/ file may not import them. Permission sets stay whole here (ADR-0130 2026-09-02 addendum) and grant on service objects through DefineStackOptions.artifactObjects (objectstack#18202).
  3. objectstack.composition.ts — the app package's collections (appObjects, appHooks, appFlows, appSkills, appSharingRules, …), the artifact-wide unions the suites sweep (allHooks, allFlows, allSkills, CrmSharingRules), the seed split (ServiceSeedData / AppSeedData cut out of CrmSeedData), the HOTCRM_COMPOSITION knob (moved here from the config, unchanged), and the two built stacks serviceStack / appStack.
  4. objectstack.config.ts — export default composeStacks([serviceStack, appStack], { manifest: 'preserve' }), the app last, nothing else exported.
  5. src/sales/apps/crm.app.ts — the five service entries leave the app's navigation; group_service stays as an empty container the module fills.
  6. Tests — test/helpers/composed-stack.ts: the same two stacks under the platform's flattening composeStacks (what ~100 suites read, since the artifact no longer carries flattened collections), and servedApps() / servedApp(): the navigation folded by the platform's own SchemaRegistry.applyNavContributions. Eight suites taught the contribution channel (floors unchanged); docs-declared-versions taught every package of the artifact; saas-composition taught the per-package seed split; new test/package-ownership.test.ts.
  7. Docs — AGENTS.md Project Architecture (line 44 now true for sales + service, and says revenue/marketing are directories of the app package), the src/docs/ note (measured, see below) and its closing sentence that the boot's [MetadataPlugin] WARN about the four pages is not an instruction to move them (the platform's own builder put them there; reported upstream), docs/README.md, docs/ARCHITECTURE.md, docs/STATUS.md, content/docs/getting-started/for-developers*.mdx, the My Work row of quick-tour*.mdx (My Cases last), and — round 1 — every other file whose present-tense text still stated the one-defineStack shape (see Round 1 below).
  8. Changeset .changeset/1907-sales-app-service-module.md — patch: the marketplace listing gains a package row and My Cases moves inside My Work, so the consumer surface changed; the release note states the move.

What the artifact really emits (acceptance 1, re-read against the 2026-09-22 addendum)

dist/objectstack.json top-level keys are exactly manifest, packages, i18n, docs — no flattened objects / views / flows / … beside packages[]. manifest.id is app.objectstack.hotcrm.

packages[] slot id type objects collections in the body
0 app.objectstack.hotcrm.service module crm_article_feedback, crm_case, crm_knowledge_article objects 3, hooks 8, views 2, pages 1, flows 7, actions 8, datasets 1, reports 3, dashboards 1, skills 1, sharingRules 3, emailTemplates 8, data 2, requires 4, navigationContributions 3
1 app.objectstack.hotcrm app the other 15 objects 15, hooks 34, views 12, pages 7, flows 25, actions 23, datasets 8, reports 7, dashboards 4, skills 5, permissions 7, sharingRules 7, positions 12, mappings 3, translations 1, emailTemplates 68, data 18, requires 8

Every object keeps its FQN (same names, crm namespace). Ownership is read off the bodies and off GET /api/v1/packages (below), and pinned by test/package-ownership.test.ts (exactly two packages, the app last; one owner per object; every hook / sharing rule / seed family / dataset / view / action / page / flow authored against a service object ships in the service module; permission sets only in the app package; the composed view equals the bodies collection by collection). The one exception to "everything is in a body" is the top-level docs[].

The PM's five mechanism assumptions, measured

  1. 0 cross-reference issues on 17.7.0 — holds, through artifactObjects only. pnpm validate exit 0, "Running author-time rules per package (2)" passes (the attendee_contacts → crm_contact refusal is gone). The #18202 fix is OPT-IN: with artifactObjects: [] the app stack refuses exactly the 18 grants be11c07 hit (defineStack cross-reference validation failed (18 issues), 6 each on crm_case, crm_knowledge_article, crm_article_feedback). The option is verified, not trusted: a grant on crm_ghost listed in artifactObjects but defined by no package → composeStacks artifact cross-reference validation failed (1 issue). No new platform refusal.
  2. The addendum changes acceptance 1 — confirmed; see the section above.
  3. Contributed navigation under lint --strict — 0 warnings. pnpm lint: 0 errors, 0 warnings, 18 suggestions, line-for-line the same as main but for array indices. No translation-target-unknown (objectstack#18203 is in 17.7.0).
  4. be11c07's decisions re-checked on the new tree. (a) Seed rows registered by the owning package: kept. New on 17.7.0: artifactObjects widens data[] too, so route (a) is no longer refused — the platform accepts a service family registered by the app package (pnpm validate exit 0 with knowledgeArticles moved), which is why package-ownership pins the family's owner. (b) requires per stack: required — deleting the service module's line gives 3 defineStack trigger capability refusals. (c) Factories: createHotCrmAppStack is needed (revenue/marketing); createHotCrmServiceStack takes only data, so the knob is resolved once. (d) src/sales/interfaces/index.ts: kept — header-only (75 bytes), imported by nothing; dropping it means editing content/docs/customization/index*.mdx ×3 and docs/architecture/module-split-inventory.json, outside this card's file surface.
  5. AGENTS.md:44 now reads "one ObjectStack release artifact carrying two packages … src/revenue/ and src/marketing/ are package DIRECTORIES that are not packages of the artifact yet".

Acceptance

# Criterion Reading
1 packages[] of two entries, FQNs kept, ownership ✅ table above (as amended by the addendum)
2 boots from source; both rows writable: false; no nav_contribution_group_missing; no ownership diagnostic ✅ / ✅ / ✅ 0 hits / ❌ one [MetadataPlugin] WARN (Known gap)
3 browser walk equal to main ✅ table below; the one difference is the accepted nav_my_cases move
4 pnpm verify green; pins keep their floors; src/sales/ token reading ✅ on 5157d610; ✅ ablation 2; business semantics unchanged
5 changeset ✅ patch (consumer surface changed)
6 cross-package measurements ✅ section below
7 governed, draft, maintainer merges ✅

Browser walk (acceptance 3)

Both sides booted the same way: objectstack dev --fresh --seed-admin --no-watch --compile (fresh DB, from source), head on port 4870 at 9b3b6571, main on 4880 at c529de2b, Chromium /opt/pw-browsers/chromium, signed in as the seeded admin. Commits after 9b3b6571 change comments, tests and docs only (no non-comment line in src/ or the two root files).

Reading main this branch
GET /api/v1/packages (hotcrm rows) app.objectstack.hotcrm app, writable: false, 18 objects app.objectstack.hotcrm app, writable: false, 15 objects · app.objectstack.hotcrm.service module, writable: false, 3 objects (listed after the app: registration order follows dependencies, not the slot)
served navigation (GET /api/v1/meta/app) 33 nodes 33 nodes, same set; only diff: nav_my_cases 4th → last inside My Work
sidebar as rendered (My Work) My Tasks, My Deals, My Leads, My Cases, My Calendar, Inbox My Tasks, My Deals, My Leads, My Calendar, Inbox, My Cases
list rows (17 object entries) lead 21 · account 9 · contact 0 · opportunity 10 · quote 6 · contract 4 · product 13 · my tasks 9 · my deals 10 · my leads 21 · my cases 23 · my calendar 0 · event 29 · campaign 7 · case 38 · knowledge 4 · forecast 7 identical, every one
record tabs — case / knowledge article / account / opportunity (first row of each list, same record both sides) Details, Related, Activity / Details, Related1 / Details, Related21, Attachments / Details, Related4, Activity identical
dashboards — Recharts marks / charts executive 13/4 · sales 21/5 · sales activity 12/4 · service 14/4 · CRM overview 17/4 identical
approvals inbox mounts; tabs My Pending, Submitted by me, All identical
package docs (GET /api/v1/meta/doc) crm_admin, crm_overview, crm_sales, crm_service, setup_overview identical
page errors / failed /api/ responses 0 / 0 0 / 0
boot log WARN lines 22 23 — the extra one is the docs residual

The two zeros (Contacts, My Calendar) are the walk's tbody tr probe on both sides: My Calendar is a calendar view; the contact list page showed no grid rows to the probe on main too. Seeded rows resolve their lookups across the package boundary in both directions although each package's seeds replay from its own body — measured over GET /api/v1/data on a fresh boot of the branch (round 1, metadata identical to the head): 38/38 seeded cases resolve crm_account to a real account, 2/2 seeded events that name a case resolve related_to_case to a real case.

Cross-package measurements (acceptance 6, on the 17.7.0 pin)

Edge Verdict
App-owned permission sets granting on the 3 service objects (18 grants) ✅ accepted with artifactObjects; ❌ refused without it
Service seed families registered by the service package ✅ accepted; replay resolves lookups into app-owned objects
Service actions' attendee_contacts record-picker → crm_contact (objectstack#18204) ✅ accepted by the per-package author-time rules
case_detail page related list → crm_task ✅ accepted; case record page opens with the same tabs
App's own navigation → service object (R3) not authored — the five entries are navigationContributions
Contribution ordering priority orders contributions only; a contributed item is appended after the group's own children (nav_my_cases last)
Contributed nav labels under lint --strict ✅ 0 warnings
Sales-owned customer_360 skill naming crm_case / crm_knowledge_article; R1 lookups into crm_case ✅ accepted

Token gate (pnpm hygiene:tokens, src/sales/)

Business semantics 56,388 — unchanged. Interaction layer 28,136 → 27,966 (the five entries left crm.app.ts). Authored total 100,067 → 100,165 (the new src/sales/index.ts). src/service/ authored total 20,478 → 21,493 (its index.ts), headroom 507. No ceiling moved; "source token ratchet clean".

Gates (head 5157d610)

  • pnpm verify → exit 0 (os-verify-lock VERDICT command-exit 0, held 211s): ✓ Validation passed · typecheck exit 0 · lint 0 errors / 0 warnings / 18 suggestions · ✓ i18n lint gate: 0 i18n/missing-* issues · ✓ source hygiene clean · ✓ source token ratchet clean · ✓ Build complete (8 author-time warnings, the approver-staffing advisories main has too) · Test Files 179 passed (179), Tests 3832 passed | 1 skipped (3833).
  • Browser walk above.

Reverse verification (each mutation landed on disk via ablation-replace.mjs, restored to the HEAD blob, git diff HEAD empty)

  1. Config order swapped to [appStack, serviceStack] → package-ownership: 2 red ("the app last", "identifies as the app"). Predicted red, observed red.
  2. The three contributions re-aimed at app: 'crm_nowhere' → 19 red in 6 files (action-references 2, docs-analytics-vocabulary 4, docs-quick-tour-navigation 10, docs-service-index-analytics 1, docs-setup-navigation-names 2, i18n-references 1) — exactly the set that went red before the pins were taught, so the floors hold through the contribution channel. view-references and authorization-coverage stay green, as predicted: fewer entries cannot dangle.
  3. ServiceSeedData = [cases] (knowledge seeds fall to the app package) → pnpm validate exit 0 (the platform accepts it), package-ownership + saas-composition: 2 red.

Round 1 — the pre-#1907 shape, swept

The claim's surface was amended (#1907, Claim amendment (file surface)) to every file in which a sentence asserted the PRE-#1907 shape as current — one defineStack in objectstack.config.ts, or the collections handed to it as the registration path. The hit list was re-taken on f294875d (grep for objectstack.config, defineStack, composition.ts, the registration-list names, "single / one defineStack", "four packages" and their Chinese spellings, plus every removed export name such as allDatasets), and every hit classified by hand.

Rewritten (present-tense claims about the current shape) — 57 files:

  • Product docs, all three locales kept in step: customization/extending-objects (hooks and flows register through their package's list — appHooks / appFlows in objectstack.composition.ts, serviceHooks / serviceFlows in src/service/index.ts), customization/ai-skills (appSkills / serviceSkills), marketplace/fork-hotcrm (step 2 edits the two package manifests; step 4 drops the service stack from the composition and the config), guides/integrations (no package lists webhooks in requires; the hand-copied roster is gone), analytics/cubes (allDatasets no longer exists — datasets register through their package's stack), customization/index (one artifact, two packages), administration/sandbox-and-releases (seed data the packages register), reference/security-and-compliance (plugins belongs on the app package's stack).
  • Release and maintainer docs: docs/RELEASE_STRATEGY.md (release unit carries two packages; the manifest version to match by hand lives in src/sales/index.ts and src/service/index.ts), CONTRIBUTING.md, docs/MAINTENANCE.md (identity, protocol bump, releasing), docs/DEPLOYMENT.md, docs/developers/code_examples.md, docs/feature-inventory.md (ADM-015's source), README.md (badge link and layout tree), .github/tasks/new-package.md (step 4 now scaffolds a package stack the way src/service/index.ts is built).
  • Tooling comments: scripts/wow1-live-schema.sh, scripts/lib/source-hygiene-surface.mjs.
  • Source comments (no code line changed): the seed, flow and hook barrels of all four directories (src/*/data/index.ts, src/*/flows/index.ts, src/sales/objects/hooks.ts, src/sales/objects/index.ts), src/sales/profiles/tenant-admin.profile.ts, and the two seed modules whose "the root config replays them in order" is replaced by the measurement above (src/sales/data/activity.seed.ts, src/service/data/service.seed.ts).
  • Tests and helpers (docblocks and failure messages that named the old registration path; no assertion or floor changed): flow-registration-completeness, registration-list-completeness, helpers/registration-lists, docs-metadata-counts, docs-role-hierarchy, docs-src-tree-paths, import-mappings, persona-copy, demo-staffing, and e2e/opportunity-win-loss-capture.spec.ts.

Left as they are (each read, none asserts the old shape as current):

  • Dated records: the audit notes heading administration/sandbox-and-releases and reference/security-and-compliance ("checked claim by claim against … spec 17.0.0-rc.6"), src/service/objects/_case-assignment.ts ("measured against the full shipped stack"), test/docs-declared-versions.test.ts's [17.0-rc2验收] objectstack.config.ts 的 engines.protocol 仍是 ^17.0.0-rc.1,与 objectstack.manifest.json(rc.2)及 build 产物不一致 #728 history, test/sharing-coverage.test.ts's table history, docs/architecture/module-split-plan.md and psa-module-plan.md (recorded decisions and plans), README.legacy.md, CHANGELOG.md, .changeset/*, docs/archive/.
  • Still true as written: "objectstack.config.ts has no server block" (reference/performance-and-limits), "carries no auth block" (reference/security-and-compliance), "metadata registered by objectstack.config.ts" (customization/api-reference), "registered through objectstack.config.ts" (analytics/cubes opening), "objectstack.config.ts never imports this file" (scripts/demo-staff.ts), "not registered in objectstack.config.ts" (src/sales/sharing/demo-staffing.ts), the kernel suites' "boot … over objectstack.config.ts" (they boot the real artifact), package.json / tsconfig.json / .github/labeler.yml entries, and the "four packages" of the layout (customization/ui-extensions, the token-gate tests).
  • Generic ObjectStack guidance, not HotCRM's shape: marketplace/manifest-and-versioning and marketplace/publishing-your-first-app describe a single-package app (app.acme.crm) declaring its manifest in its own objectstack.config.ts, which remains a valid shape for such an app.

Known gap — the docs residual WARN (acceptance 2)

WARN [MetadataPlugin] artifact '…/dist/objectstack.json' carries 4 top-level metadata item(s) that none of its 2 package bodies declare. They were registered under the artifact's own manifest id ('app.objectstack.hotcrm') because no package in the artifact claims them, so every door will report that id as their owner. Rebuild the artifact so each collection it ships is carried by the package that owns it.

The four items are crm_admin, crm_overview, crm_sales, crm_service from src/docs/. os build writes flat src/docs/*.md to the artifact's top level by its own rule, and the runtime then flags any top-level item of a multi-package artifact. The owner it reports is the app package — the same owner as on main — and all four docs are served. The remedy it prints cannot be followed here: a per-package docs directory is collected only when its name is the package id or the id's last segment, and src/sales/ answers to neither (app.objectstack.hotcrm ends in hotcrm); measured: src/sales/docs/ → docs/uncollected-directory, src/service/docs/ → collected onto the service body. Per AGENTS.md §2 this is reported for the platform, not routed around (no src/hotcrm/ directory, no rename). Whether this WARN holds the merge is the maintainer's call.

Platform findings — filed upstream by the seat (no workaround here)

Acceptance notes

  • module-split-plan.md item 6's premise "the platform reads package docs from src/docs/ only" no longer holds for a module whose directory is its id's last segment (src/service/docs/ would be collected) — relevant to where crm_service.md lives once the app side is solved upstream. Not moved here (this card moves no files).
  • HOTCRM_COMPOSITION=saas was exercised by saas-composition (service package registers no rows), not booted.
  • hotcrm#2000 registers picklists; after this lands it goes into the app stack in objectstack.composition.ts rather than the config.

维护者速读(草稿)

改了什么
HotCRM 现在仍是一个产品、一个安装包,但包里分成了两个"模块包":HotCRM 主应用(销售,含客户、联系人、线索、商机等)和 HotCRM Service(服务:工单、知识库、文章反馈)。客户看到的还是同一个 HotCRM;对象名、数据表、接口地址一个都没改。服务相关的五个菜单项改由服务模块"挂"进主应用菜单,唯一可见变化:“我的工单”(My Cases) 从“我的工作”分组第 4 位移到最后一位。

为什么改
这是 2026-09-14 拆分裁决的第二步(先拆 sales 和 support)。拆开后,Studio 能按模块分组展示,服务模块有了自己的边界和预算,后面拆收入、营销、PSA 都照这个模式加目录即可。上一轮卡在平台的权限校验上,17.7.0 平台已提供正式开关(artifactObjects),本轮无需任何绕路。

风险与代价(含回滚)
风险低:浏览器对比 main,菜单 33 项、17 个列表行数、4 个记录页、5 个看板、审批收件箱、0 页面错误全部一致。代价有二:启动日志多出一条平台告警(4 篇应用文档挂在包外——平台自身的生产方与读取方不一致,已立上游卡 objectstack#22190);pnpm validate 对模块包里的能力声明少了一项预检(同为平台缺陷,已立 objectstack#22189)。所有仍描述旧结构的文档(产品文档三种语言、发版手册、开发者文档)已在本 PR 第 1 轮一并改写,发版时版本号改在两个包的清单里。回滚:直接 revert 本 PR,数据与对象不受影响。

席位意见

你要做的(一个动作)
决定这条启动告警是否阻塞合并:若接受(文档归属仍是主应用,与现在相同),合并本草稿 PR;若不接受,保持草稿,等平台修复后再合并。


Generated by Claude Code

claude added 4 commits October 8, 2026 04:48
…rtifact

The service directory becomes app.objectstack.hotcrm.service, a type:module
package with its own defineStack (src/service/index.ts), dependencies on the
app package and three navigationContributions carrying nav_case,
nav_knowledge, nav_service_dashboard, nav_my_cases and nav_report_sla into the
groups the app keeps. src/sales/index.ts is the app package's defineStack
factory; its permission sets stay whole and grant on the module's objects
through DefineStackOptions.artifactObjects. objectstack.composition.ts builds
the two package stacks and objectstack.config.ts composes them with
composeStacks(..., { manifest: 'preserve' }), the app last.

The suites read test/helpers/composed-stack.ts (the same two stacks under the
platform's flattening composition), since a multi-package artifact carries its
collections only in packages[]; the navigation pins read the served tree
(SchemaRegistry.applyNavContributions) rather than the authored one.

Claude-Session: https://claude.ai/code/session_012zh91QzFgePbkmuHnugLN3
Co-authored-by: Claude <noreply@anthropic.com>
…et for #1907

AGENTS.md said `objectstack build` reads `src/docs/` and no other path. With
the artifact carrying packages[], the build also collects a `src/<dir>/docs/`
whose name is a package id or its last segment, onto that package's body —
measured: `src/service/docs/` reaches the service module, `src/sales/docs/`
answers to no package and is reported as `docs/uncollected-directory`. The
four pages stay at the top of `src/`.

The patch changeset describes the two packages and the one navigation move
(My Cases last inside My Work).

Claude-Session: https://claude.ai/code/session_012zh91QzFgePbkmuHnugLN3
Co-authored-by: Claude <noreply@anthropic.com>
…old item ownership

Claude-Session: https://claude.ai/code/session_012zh91QzFgePbkmuHnugLN3
Co-authored-by: Claude <noreply@anthropic.com>
The service manifest's version comment no longer claims `changeset version`
moves it (it does not touch .ts files); the declared-versions pin holds it.
The app factory's artifactObjects note carries the measured refusal without
the option (18 grants) and the measured scope of composeStacks' verification
(a grant on an object no package defines is refused even when listed). The
quoted per-stack trigger refusal is the 17.7.0 wording.

Claude-Session: https://claude.ai/code/session_012zh91QzFgePbkmuHnugLN3
Co-authored-by: Claude <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 8, 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 Oct 8, 2026 5:40am 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 labels Oct 8, 2026
… move src/docs

A boot of the two-package artifact logs a MetadataPlugin WARN that the four
src/docs pages are claimed by no package body and tells the author to rebuild
them into one. The platform's builder put them there by its own rule, the
owner reported is the app package, and no directory under the ruled layout
can carry them into its body; the note keeps the next agent from acting on it.

Claude-Session: https://claude.ai/code/session_012zh91QzFgePbkmuHnugLN3
Co-authored-by: Claude <noreply@anthropic.com>
… shape now states two packages

The surface amendment on #1907: each present-tense claim that objectstack.config.ts
is the one defineStack, carries the manifest, the requires or the version, or is
handed the collections, is rewritten to the two package stacks it composes —
the product docs in all three locales (extending-objects, ai-skills, fork-hotcrm,
integrations, cubes, customization/index, sandbox-and-releases,
security-and-compliance), the release runbook (RELEASE_STRATEGY, CONTRIBUTING,
MAINTENANCE, DEPLOYMENT), code_examples, feature-inventory, README,
.github/tasks/new-package.md, scripts/wow1-live-schema.sh, the source-hygiene
surface note, the src barrel and seed comments, and the test docblocks and
messages that described the registration path. Dated records (audits,
measurements, the split plan's recorded decisions, generic single-package
marketplace guidance) are left as they are.

The seed comments carry a new measurement: on a fresh boot of the two-package
artifact every seeded case resolves its account (38/38) and every seeded event
that names a case resolves it (2/2).

Claude-Session: https://claude.ai/code/session_012zh91QzFgePbkmuHnugLN3
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the backend Server-side behaviour — hooks, flows, actions label Oct 8, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5157d6108b5c7846927397bcdda0fef7a783f5a6
Local-runs: none

Inputs read: card #1907 (body and all 9 comments, rounds 0 and 1 dev reports 6052979399 / 6053208554 included), PR #2011 body and its 172-file list, the net diff origin/main (c529de2b) ... 5157d610 (1,563 / 696 over 172 files; merge base is origin/main, so the three-dot diff is the PR's own change), the 10 check-runs on the head, AGENTS.md at origin/main and at the head, ADR-0130 with both addenda. Nothing was built, run or re-run; no GitHub write.

Check-runs on 5157d610: 10 of 10 completed / success (Build and Test (22.x), Typecheck and Build, Quality Checks, Check Changeset, Playwright, link-check, CodeQL, Analyze Code (javascript), Label Pull Request, Vercel Preview Comments). None in_progress. The PR is a draft with auto_merge: null, head repo = base repo, as hotcrm AGENTS.md How a green PR lands requires for a governed path.

① Derived judgments

The governed hunk (AGENTS.md, +29 / −14), sentence by sentence against the head's code.

  • "one ObjectStack release artifact carrying two packages (ADR-0130 D4): app.objectstack.hotcrm, the type: app package (src/sales/index.ts), and app.objectstack.hotcrm.service, a type: module (src/service/index.ts), one defineStack each, composed by objectstack.config.ts with composeStacks(..., { manifest: 'preserve' })" — TRUE. src/sales/index.ts is createHotCrmAppStack calling defineStack with { id: 'app.objectstack.hotcrm', type: 'app', namespace: 'crm', version: '3.1.0', engines.protocol '^17.7.0' }; src/service/index.ts is createHotCrmServiceStack calling defineStack with { id: 'app.objectstack.hotcrm.service', type: 'module', namespace: 'crm', version: '3.1.0', dependencies: { 'app.objectstack.hotcrm': '^3.1.0' } }; objectstack.config.ts is exactly one line of code, export default composeStacks([serviceStack, appStack], { manifest: 'preserve' }). This replaces main's sentence "assembled from four packages ... one defineStack", which was false on main (one defineStack, zero package manifests besides the app's). The hunk makes the governing sentence true. Right.
  • "src/revenue/ and src/marketing/ are package DIRECTORIES that are not packages of the artifact yet: the app package registers their metadata" — TRUE. objectstack.composition.ts builds appObjects / appViews / appActions / appDatasets / appEmailTemplates / appHooks / appFlows from the sales, revenue and marketing barrels and hands them to the app factory; neither directory has an index.ts or a manifest. Tree comments ("Registered by the app package until packaged") match. Right.
  • "A directory under src/ IS a package ... the top-level directories under src/ are exactly the packages, plus docs/" (kept from main) and "The four packages ... are recorded in docs/architecture/module-split-plan.md" (kept) — these now use "package" in the LAYOUT sense while the new opening paragraph uses it in the ARTIFACT sense. The head text names the distinction explicitly ("package DIRECTORIES that are not packages of the artifact yet"), so it is not a contradiction, but it is a two-sense word in a rule file; flagged under ③ as a precision item, not a defect.
  • Tree comments for objectstack.config.ts ("composeStacks() over the two package stacks — the artifact, nothing else") and objectstack.composition.ts ("the directories' barrels collected into the two package stacks; the composition knob") — TRUE: the HOTCRM_COMPOSITION resolution (resolveComposition, compositionPermissions, seedDataFor) moved verbatim from the config to the composition module; the config carries no named export, as the hotcrm build requires.
  • New paragraph "A package of the artifact has one index.ts that calls defineStack with its manifest" — TRUE for both packages.
  • "a module's navigation reaches the app ONLY through its manifest's navigationContributions, aimed at a group the app declares (an app's own navigation may not name another package's object, and a contributed item lands after the group's own children)" — consistent with ADR-0130 §1.5 ("a package's own app.navigation pointing at a foreign object ... REJECTED ... navigation crosses only through contributions") and with the diff: the five service entries left src/sales/apps/crm.app.ts (group_service kept as an empty, commented container) and reappear as three navigationContributions in src/service/index.ts aimed at group_service / group_work / group_insights. The append-after-own-children clause is the dev's round-0 (2026-09-14) measurement of applyNavContributions, re-observed in both browser walks (nav_my_cases 4th to last), and test/helpers/composed-stack.ts folds through the platform's own SchemaRegistry.registerAppNavContribution / getAllApps rather than re-implementing it. Right.
  • "the app package's permission sets, whole by the ADR-0130 2026-09-02 addendum, grant on module objects because the app stack is told the artifact's other objects (artifactObjects), which composeStacks verifies" — TRUE of the code: createHotCrmAppStack(..., { artifactObjects: ... }) with the list being every serviceStack.objects[].name and defineStack(..., { artifactObjects }); the permission sets are registered only on the app stack (pinned by test/package-ownership.test.ts: permissions empty on the service body, at least one app set granting on a service object). The "composeStacks verifies" half is the ARTIFACT pass the card body attributes to objectstack#18212 and the dev probed (crm_ghost refused); CI pnpm validate green on the head confirms the opt-in itself is honoured by the 17.7.0 pin. Consistent with the addendum ("permission sets stay whole in the type: app package"). Right.
  • src/docs/ note: "compiles src/docs/*.md into the artifact's top-level docs[] (ADR-0046), registered under the artifact's own id" — matches the quoted WARN text. "A per-package src/[dir]/docs/ reaches a package's body only when [dir] is that package's id or the id's last dot-separated segment: src/sales/ answers to neither" — a 17.7.0 platform rule measured by the dev (src/sales/docs to docs/uncollected-directory, src/service/docs collected onto the service body), stated identically in both rounds; it cannot be re-derived from the brief's inputs and is recorded here as the dev's measurement, not as independently verified. "Moving them there keeps the build at exit 0 behind one docs/uncollected-directory warning, and the artifact simply loses its docs" — a refinement of main's "silent" (main said no warning at all); the head is the more precise claim. "A boot logs a [MetadataPlugin] WARN ... the owner it reports is the app package, and it is not an instruction to move them — it is reported upstream from Packaging: sales becomes the type: app package and service a type: module — composeStacks(…, { manifest: 'preserve' }), navigation contributions, two-entry packages[] #1907" — TRUE per the PR body's quoted WARN and the walk (docs API 5 = 5, boot WARN 22 vs 23). The four files are still at src/docs/ on the head; nothing moved. Right, and it closes a real agent trap (an agent following the WARN's remedy would invent src/hotcrm/).
  • "Where the validation actually happens" and "A file missing from its barrel" rewrites — TRUE: per-package defineStack, composeStacks for cross-package references, src/service/index.ts names its items, the composition merges the app package's three directories, no glob discovery.
  • zh-Hant bullet: "supportedLocales in src/sales/index.ts carry none" — TRUE: the i18n block (en, zh-CN, ja-JP, es-ES) moved to the app factory; no Traditional locale.

Does any rule now contradict another, ADR-0130, or the layout? No. Rule 2 (imports only own directory or src/sales/) is honoured by the shape itself: src/sales/index.ts is a factory precisely so it never imports revenue/marketing; src/service/index.ts imports only ./; the root objectstack.composition.ts is the one module allowed to see everything. Rule 3 (items live with their object) is now pinned off the artifact bodies by test/package-ownership.test.ts (hooks, sharing rules, seed families, datasets, views, actions, pages, flows). The ADR-0130 2026-09-22 addendum (metadata once, in packages[]) is what forces the test/helpers/composed-stack.ts indirection; the helper uses the platform's composeStacks in flattening mode over the same two built stacks and package-ownership holds the two views equal collection by collection — hotcrm chapter 3 (no platform tooling re-implemented here) is respected. Chapter 2 (a platform defect means WAIT, no routing around) is respected: the diff contains no log filter, no src/hotcrm/ directory, no tolerance code; package.json, workflows, vitest.config.ts, objectstack.manifest.json are untouched.

Does the PR change what an agent following AGENTS.md would author? Yes, deliberately and correctly: a new hook / flow / skill / sharing rule goes into its PACKAGE's list (appFlows etc. in the composition for sales/revenue/marketing, serviceFlows etc. in src/service/index.ts), not the former allFlows; a module's menu entries are navigationContributions; a new package gets its own index.ts stack and its objects into artifactObjects (.github/tasks/new-package.md step 4 rewritten accordingly); the version bump at release now touches two manifests plus the module's dependencies range (RELEASE_STRATEGY / MAINTENANCE / CONTRIBUTING rewritten, test/docs-declared-versions.test.ts holds every packages[] body to one version and protocol). The registration-completeness pins were re-pointed at the per-package lists with floors unchanged; the eight navigation/docs pins read the SERVED tree. Ablation 2 in the PR body (contributions re-aimed at crm_nowhere: 19 red in 6 files, the same set that was red before teaching) is the evidence the floors still bite through the contribution channel.

Anything published changed without disclosure? No. objectstack.manifest.json (marketplace listing: manifestId, displayName, specVersion, engines.protocol) is byte-unchanged. No *.object.ts is in the diff; appObjects + the three service objects = the composed objects by name (pinned); every FQN keeps the crm namespace. The app manifest fields (id, name, description, version, protocol, requires, i18n) are carried verbatim from the old config into src/sales/index.ts. Disclosed changes: the new package id app.objectstack.hotcrm.service (one more row in GET /api/v1/packages and the Studio picker — an admin surface; ADR-0130 D2 keeps a module out of the consumer marketplace), nav_my_cases last inside My Work, the artifact's shape (manifest, packages, i18n, docs at top level, no flattened collections — the 2026-09-22 addendum's emitter), and two platform findings that change this repo's gate behaviour (the boot WARN; pnpm validate / pnpm build no longer running the installable-provider preflight over a package's requires on a multi-package artifact). The service body's emailTemplates 8 vs the body text's "2 case email templates" is 2 names across 4 locales, not a discrepancy.

Head-of-record consistency checks. The six commits carry the model-free trailer pair only (Claude-Session, Co-authored-by: Claude). Between the walk commit 9b3b6571 and the head, src/ and the two root files changed in 15 files, and every changed line is a comment or docblock (verified by stripping comment leaders from the diff: zero non-comment lines), so the browser-walk readings apply to the head's metadata. The PR body on the head is the round-1 corrected body (Gates heading 5157d610, item 7 names the WARN sentence, Round 1 section present); its round-0 staleness (dev deviation b) is resolved.

② Semver level

Changeset .changeset/1907-sales-app-service-module.md declares 'hotcrm': patch. Matches what the diff publishes. Nothing an author can write is added, removed or renamed (hotcrm publishes no schema; its accept set is the platform's), so Clause-②: no holds. The published surface changes are: one additional package row in the artifact and the package API (an admin/control-plane noun under ADR-0130 D2, not a consumer unit), one navigation reorder, and the artifact file's internal shape (read-both by the platform under D4, so no installed artifact or runtime path breaks). None removes or narrows anything; no FROM-to-TO migration is owed. Card acceptance 5 explicitly admitted patch "describing the two packages", and the changeset says which and why, including the one visible menu difference and the developer-facing registration change. A minor would also have been defensible for the added package row; patch is not wrong. One release-mechanics consequence is disclosed correctly: the next changeset version bumps package.json to 3.1.1 and the two manifest version fields plus the module's dependencies range must be matched by hand — docs-declared-versions reds until they are.

③ Boundary flags

Round 0 dev report 6052979399:

  • Deviation (a) stale objectstack.config.ts mentions outside the claim's surface — answered: the seat amended the surface (6053009952) and round 1 swept 57 files; the leftovers it kept were read: "four packages" in test/docs-readme-token-figures.test.ts, test/source-token-ratchet.test.ts, content/docs/customization/ui-extensions.mdx, content/docs/analytics/cubes.mdx are the layout sense and still true; module-split-plan.md is a dated plan.
  • Deviation (b) stale PR body — resolved; the head's body is the corrected one (see ①).
  • Deviation (c) walk ran on 9b3b6571 — verified: comments-only afterwards in src/ and the root files.
  • Deviation (d) boots via objectstack dev --fresh --seed-admin --no-watch --compile instead of pnpm dev — an equivalent from-source boot on both sides; accepted as disclosed.
  • Deviation (e) model-free commit trailers despite the harness reminder — verified on all six commits.
  • Deviation (f) a detached comparison worktree, removed — process note, nothing in the diff.
  • Item 4(d) src/sales/interfaces/index.ts KEPT — the 2026-09-14 Status decision (5) said drop; the R74 claim (6052101923) re-opened it as keep-or-drop; the dev kept it and said why (header-only, imported by nothing, ten doc/labeler references outside the surface). Disclosed; AGENTS.md's tree still lists mappings/ interfaces/, consistent with keeping. A later card may drop it.
  • Open question 1 (does the [MetadataPlugin] WARN hold the merge?) — escalated to the maintainer, correctly: the PR body states it in the acceptance table as the one red cell, quotes the WARN verbatim, explains why the printed remedy cannot be followed in the ruled layout, and the 维护者速读(草稿) names it as the single decision. Nothing in the diff routes around it (no layout move, no filter, no tolerance code). The AGENTS.md sentence tells agents not to act on it; that is chapter 2 applied, not a workaround.
  • Out-of-scope class-b findings (the docs residual WARN; the requires preflight reading only the artifact top level) — the PR body says the seat filed them as objectstack#22190 and objectstack#22189. Those two issues are outside this brief's inputs and were not opened here; the card thread itself carries no upstream link yet, so AGENTS.md's pointer "reported upstream from Packaging: sales becomes the type: app package and service a type: module — composeStacks(…, { manifest: 'preserve' }), navigation contributions, two-entry packages[] #1907" currently leads to a card whose newest comment lists the finding without a number. Precision item for the seat: add the upstream numbers to the card.
  • Noted-not-filed: module-split-plan.md item 6's premise (platform reads package docs from src/docs/ only) no longer holds for a module whose directory is its id's last segment — carried in the PR's acceptance notes for the next packaging card; the Contacts list tbody probe reading 0 on both sides — disclosed as a walk caveat, not a metadata finding.

Round 1 dev report 6053208554: "none beyond the amended surface"; the same open question and findings — covered above.

Flags I raise, none blocking:

  1. "Package" is now a two-sense word in AGENTS.md (layout directory vs artifact package). The head defines both senses in the opening paragraph; the maintainer may prefer one word per sense ("directory" / "package") the next time the section is touched.
  2. The src/docs/ paragraph states a 17.7.0 platform collection rule that only the dev measured; it is consistent across both rounds and with the WARN text, and it is what prevents the trap. If the platform changes the rule, this sentence is the one that goes stale; it carries its own pin (#1907).
  3. Two older PENDING changesets (flows-register-in-allflows.md, guard-the-remaining-registration-lists.md) still describe allFlows and the "five explicit ordered arrays" as the lists an author edits. They are dated records of their own PRs and the dev left them, but they will compile into the SAME release's CHANGELOG.md as Packaging: sales becomes the type: app package and service a type: module — composeStacks(…, { manifest: 'preserve' }), navigation contributions, two-entry packages[] #1907's changeset, so a reader of that release gets two instructions. Not this PR's defect; the release-notes compiler (or a docs-only follow-up) may reconcile.
  4. objectstack#22189 as described means pnpm validate / pnpm build on this repo no longer refuse requires: ['ai'] in a module — a gate this repo relied on is quietly weaker until the platform fix. Correctly not compensated here (chapter 2); the maintainer should know it when reading "pnpm verify green".
  5. "Fixes Packaging: sales becomes the type: app package and service a type: module — composeStacks(…, { manifest: 'preserve' }), navigation contributions, two-entry packages[] #1907" in the body while the dev's status is blocked on the WARN — the body is upfront; the maintainer's merge decision resolves both.

Implemented-by: claude/issue-1907-app-and-service-packages
Reviewed-by: session_012zh91QzFgePbkmuHnugLN3

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet objectstack-fleet Bot added the needs-user-decision Needs the maintainer's call before work proceeds label Oct 8, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读

repo:hotcrm 席位定稿,2026-10-08T05:56Z。审核记录:契约复核 PASS(本 PR 评论 6053366434),卡 #1907 的 ACCEPT 评论。

改了什么
HotCRM 仍是一个产品、一个安装包,但包里正式分成两个模块包:

  • 主应用(销售:客户、联系人、线索、商机等);
  • 服务模块(工单、知识库、文章反馈)。

对象名、数据表、接口地址一个都没改,客户看到的还是同一个 HotCRM。服务相关的五个菜单项改由服务模块"挂"进主应用菜单。唯一可见变化:「我的工单」从「我的工作」分组的第 4 位移到最后一位。另外,所有仍描述旧结构的文档(产品文档三种语言、发版手册、开发者文档,共 57 个文件)已一并改写。

为什么改
这是 2026-09-14 你的拆分裁决的第二步(「先分拆基础的crm, 比如 sales 和 support」),今天按批 hotcrm-R74 第 3 项 A 恢复。上一轮卡在平台的权限校验上;平台 17.7.0 已提供正式开关 artifactObjects,本轮没有任何绕路。拆开后,以后拆收入、营销、PSA 都按同一个模式来。

风险与代价(含回滚)

  • 风险低。 浏览器对比 main:菜单 33 项、17 个列表的行数、4 种记录页的页签、5 个看板、审批收件箱全部一致,页面错误 0。CI 10/10 全绿。
  • 代价一:启动日志多一条平台告警。 4 篇应用文档挂在包外,归属仍是主应用、全部照常提供,行为与现在相同。这是平台自身生产方与读取方不一致,已立上游卡 objectstack#22190。
  • 代价二:校验少一道预检。 在 objectstack#22189 修复前,pnpm validate / build 对模块包里的能力声明少一道"有无提供方"的预检。
  • 代价三:同一份发版说明里会出现两种说法。 两个更早的待发 changeset 还写着 allFlows,和本 PR 的说明并存。
  • 发版提醒: 两个包的 version 和模块的依赖范围要手工对齐,忘了会有测试报红提示。
  • 回滚: 直接 revert 本 PR,数据与对象不受影响。

席位意见
建议合并,接受这条启动告警。

  • 理由:契约复核 PASS,CI 全绿,浏览器对比除已接受的「我的工单」位置外完全一致。
  • 这条告警不改变任何行为,hotcrm 里也没有为它做任何绕路。
  • 等平台修复要再走一次平台发版和升级周期,对用户没有任何收益;平台修好后 hotcrm 不需要再改任何东西。

你要做的(一个动作)
在 PR #2011 上点 Approve。批准后由席位转正、进合并队列落地。若你不接受这条告警,留一句「等平台」即可,PR 保持草稿。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review October 8, 2026 06:58
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

2 participants