Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions docs/adr/0130-release-artifact-as-co-ownership-boundary.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,10 @@
**Status**: Proposed (2026-09-01) — awaiting the maintainer's hand-merge, which is itself the
acceptance act for a governed surface (Prime Directive #14). ⛔ Nothing below is settled until
this record merges; the implementation cards are cut **from** the merged ADR, never ahead of it.
**Scope bounded by the 2026-09-02 addendum**
([#14487](https://github.com/objectstack-ai/objectstack/issues/14487)): the permission matrix
§1.3(a) measures is **not** part of this boundary's payoff — permission sets stay whole in the
`type: app` package.
**Deciders**: ObjectStack maintainer, 2026-09-01, live PM chat, verbatim and untranslated:
「立 ADR 起草卡派发」and 「14122 作为epic 任务集中跟踪」— approving the proposal in
[#14122](https://github.com/objectstack-ai/objectstack/issues/14122) into the ADR-drafting lane
Expand DownExpand Up@@ -507,3 +511,92 @@ a reader deserves to know which of its anchors were re-measured:
`packages/objectql/src/plugin.ts`, `packages/core/src/plugin-order.ts`,
`packages/spec/src/stack.zod.ts`, `packages/spec/src/kernel/manifest.zod.ts`,
`packages/cli/src/commands/compile.ts`

---

## Addendum (2026-09-02, #14487) — the permission matrix is outside this boundary's payoff; permission sets stay whole in the app package

**Provenance.** Maintainer ruling, 2026-09-02, live PM chat, recorded the same day on
[#14454](https://github.com/objectstack-ai/objectstack/issues/14454#issuecomment-5507189677)
and quoted here in the words that record carries — the PM seat's, not a transcript of the
maintainer's:「第 3 项(权限集)维护者 2026-09-02 拍板:取 B。」The same comment names what this
section must say:「权限集整体留在 app 包,权限矩阵不在包边界收益范围内」. The question ruled
on is item 3 of [#14454](https://github.com/objectstack-ai/objectstack/issues/14454) (carried
verbatim onto
[#14457](https://github.com/objectstack-ai/objectstack/issues/14457) as its decision card),
raised by the HotCRM split plan
[objectstack-ai/hotcrm#1449](https://github.com/objectstack-ai/hotcrm/pull/1449),
§上游缺口 / Upstream gaps item 3, which asked the maintainer to *"decide whether permission sets
can be composed per package (a module contributing its own object grants into a role the app
owns), or record that the permission matrix is explicitly out of scope for the boundary ADR-0130
creates."* **B is the second half of that sentence, and this section is that record.**

This section lands while the record is still **Proposed**: it bounds what the record claims and
settles nothing else — the maintainer's hand-merge remains the acceptance act, exactly as the
Status line says.

### What §1.3(a) measured, and what this boundary does not fix

§1.3(a) counts, among the three measurable consequences of having no boundary, *"a permission
matrix of 30 rows × 9 CRUD columns × 6 permission sets that interleaves `客户/联系人/商机` with
`运费标准/等级政策/工厂成本`"*, and §4 draws the payoff from that section: *"Studio's scope is the
package, so package boundaries **are** the grouping Studio has never had (§1.3a)"*. **The payoff
does not extend to the permission matrix.** Splitting a product into co-owning packages leaves
that matrix exactly as flat as §1.3(a) found it. This is said here, in the record, because
§1.3(a) lists the matrix as a pain and §4 answers §1.3(a) as a whole — a reader is otherwise
entitled to read a promise this record cannot keep.

**Why — measured, not reasoned.** A permission set grants across domains *by nature*: it is
authored per **role**, not per module, so no module owns it. hotcrm#1449 measured the standard
HotCRM product against its six planned modules — `core`, `sales`, `cpq`, `service`, `marketing`,
`activity`, all inside the one `crm` namespace D1 makes co-ownable, with the `type: app` package
declaring no objects at all. Of its **six** permission sets, **four span five or six of the six
modules** (`sales_rep`, `sales_manager` and `system_admin` at six; `service_agent` at five); the
remaining two span four (`marketing_user`) and two (`guest_portal`). **Not one is confined to a
single module.** The split therefore has nothing to distribute — whatever the module boundaries
are, every set still names objects on both sides of them. This is a second instance, independent
of the 黑猫 fork §1.3(a) measured: two products, the same shape.

**Where the sets live, and what Studio shows.** The six sets stay **whole in the `type: app`
package**. That is not a new rule but the standing one:
[ADR-0086](./0086-authz-metadata-config-boundary-and-cross-package-composition.md) D3 gives a
permission set exactly one owning `packageId` (implemented — `spec/security/permission.zod.ts`,
tagged `[ADR-0086 D3]`), so a set is in one package or in another and cannot be in several. In
Studio's **Access** pillar — the matrix of
[ADR-0084](./0084-application-builder-information-architecture.md), reached per package through
ADR-0086 D7's package door — the sets therefore appear **under the app package only**, and their
matrix stays as wide as the product. Modules group Data, Automation and Interface; they do not
group Access.

⛔ This section changes no decision: D1–D8, §1 and §3's non-goal on grouping keys stand exactly
as written. In particular **no `module` or grouping key is added to a permission set** — §1.4
rejected that key on measured cost, and nothing here reopens it.

### What was NOT decided

Per-package composition of grants — a module contributing **its own** objects' grants into a role
the app package owns, by analogy with `navigationContributions` — is the other half of
hotcrm#1449's question. It is **filed, not decided**:
[#14488](https://github.com/objectstack-ai/objectstack/issues/14488), for the phase in which a
module ships on its own (the §1.3(c) CPQ case, where a module's objects would otherwise arrive
with no grants at all). It is out of this release and carries **no commitment** — neither that it
will be built, nor that the contribution shape is the one it will take.

Three inputs it inherits, recorded here because they are this record's own and would otherwise be
rediscovered:

- **ADR-0086 D4 stands until amended.** D4 chose Shape B — a package ships its own sets — and
states flatly: *"A package never writes into a shared/foreign record."* A contribution
mechanism is exactly such a write, so #14488 is an **amendment to that decision**, not an
addition beside it.
- **D4's conflict-freedom argument does not survive co-ownership unexamined.** It is
conflict-free *"because each set only grants `objects.<own-namespace>_*` keys"* — one namespace
per package. D1 lets N packages co-own one namespace, so inside one artifact that
discriminator is gone and #14488 must supply its own. (Both hotcrm#1449 and #14488 propose
**refusing** a doubly-contributed `(set, object)` rather than unioning it — a shape to measure,
not a decision this section makes.)
- **Which door edits a split product's app-owned sets is unmeasured.** ADR-0086 D7 scopes the
package Access door to *"this package's own object slice"* and keeps the cross-package
all-objects matrix at the environment-admin door. After a split the app package owns the sets
but no objects, so where a *packaged* cross-module set's grants are authored is a UI question
this record does not answer and #14488 inherits.
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions docs/adr/0130-release-artifact-as-co-ownership-boundary.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,10 @@
**Status**: Proposed (2026-09-01) — awaiting the maintainer's hand-merge, which is itself the
acceptance act for a governed surface (Prime Directive #14). ⛔ Nothing below is settled until
this record merges; the implementation cards are cut **from** the merged ADR, never ahead of it.
**Scope bounded by the 2026-09-02 addendum**
([#14487](https://github.com/objectstack-ai/objectstack/issues/14487)): the permission matrix
§1.3(a) measures is **not** part of this boundary's payoff — permission sets stay whole in the
`type: app` package.
**Deciders**: ObjectStack maintainer, 2026-09-01, live PM chat, verbatim and untranslated:
「立 ADR 起草卡派发」and 「14122 作为epic 任务集中跟踪」— approving the proposal in
[#14122](https://github.com/objectstack-ai/objectstack/issues/14122) into the ADR-drafting lane
Expand DownExpand Up@@ -507,3 +511,92 @@ a reader deserves to know which of its anchors were re-measured:
`packages/objectql/src/plugin.ts`, `packages/core/src/plugin-order.ts`,
`packages/spec/src/stack.zod.ts`, `packages/spec/src/kernel/manifest.zod.ts`,
`packages/cli/src/commands/compile.ts`

---

## Addendum (2026-09-02, #14487) — the permission matrix is outside this boundary's payoff; permission sets stay whole in the app package

**Provenance.** Maintainer ruling, 2026-09-02, live PM chat, recorded the same day on
[#14454](https://github.com/objectstack-ai/objectstack/issues/14454#issuecomment-5507189677)
and quoted here in the words that record carries — the PM seat's, not a transcript of the
maintainer's:「第 3 项(权限集)维护者 2026-09-02 拍板:取 B。」The same comment names what this
section must say:「权限集整体留在 app 包,权限矩阵不在包边界收益范围内」. The question ruled
on is item 3 of [#14454](https://github.com/objectstack-ai/objectstack/issues/14454) (carried
verbatim onto
[#14457](https://github.com/objectstack-ai/objectstack/issues/14457) as its decision card),
raised by the HotCRM split plan
[objectstack-ai/hotcrm#1449](https://github.com/objectstack-ai/hotcrm/pull/1449),
§上游缺口 / Upstream gaps item 3, which asked the maintainer to *"decide whether permission sets
can be composed per package (a module contributing its own object grants into a role the app
owns), or record that the permission matrix is explicitly out of scope for the boundary ADR-0130
creates."* **B is the second half of that sentence, and this section is that record.**

This section lands while the record is still **Proposed**: it bounds what the record claims and
settles nothing else — the maintainer's hand-merge remains the acceptance act, exactly as the
Status line says.

### What §1.3(a) measured, and what this boundary does not fix

§1.3(a) counts, among the three measurable consequences of having no boundary, *"a permission
matrix of 30 rows × 9 CRUD columns × 6 permission sets that interleaves `客户/联系人/商机` with
`运费标准/等级政策/工厂成本`"*, and §4 draws the payoff from that section: *"Studio's scope is the
package, so package boundaries **are** the grouping Studio has never had (§1.3a)"*. **The payoff
does not extend to the permission matrix.** Splitting a product into co-owning packages leaves
that matrix exactly as flat as §1.3(a) found it. This is said here, in the record, because
§1.3(a) lists the matrix as a pain and §4 answers §1.3(a) as a whole — a reader is otherwise
entitled to read a promise this record cannot keep.

**Why — measured, not reasoned.** A permission set grants across domains *by nature*: it is
authored per **role**, not per module, so no module owns it. hotcrm#1449 measured the standard
HotCRM product against its six planned modules — `core`, `sales`, `cpq`, `service`, `marketing`,
`activity`, all inside the one `crm` namespace D1 makes co-ownable, with the `type: app` package
declaring no objects at all. Of its **six** permission sets, **four span five or six of the six
modules** (`sales_rep`, `sales_manager` and `system_admin` at six; `service_agent` at five); the
remaining two span four (`marketing_user`) and two (`guest_portal`). **Not one is confined to a
single module.** The split therefore has nothing to distribute — whatever the module boundaries
are, every set still names objects on both sides of them. This is a second instance, independent
of the 黑猫 fork §1.3(a) measured: two products, the same shape.

**Where the sets live, and what Studio shows.** The six sets stay **whole in the `type: app`
package**. That is not a new rule but the standing one:
[ADR-0086](./0086-authz-metadata-config-boundary-and-cross-package-composition.md) D3 gives a
permission set exactly one owning `packageId` (implemented — `spec/security/permission.zod.ts`,
tagged `[ADR-0086 D3]`), so a set is in one package or in another and cannot be in several. In
Studio's **Access** pillar — the matrix of
[ADR-0084](./0084-application-builder-information-architecture.md), reached per package through
ADR-0086 D7's package door — the sets therefore appear **under the app package only**, and their
matrix stays as wide as the product. Modules group Data, Automation and Interface; they do not
group Access.

⛔ This section changes no decision: D1–D8, §1 and §3's non-goal on grouping keys stand exactly
as written. In particular **no `module` or grouping key is added to a permission set** — §1.4
rejected that key on measured cost, and nothing here reopens it.

### What was NOT decided

Per-package composition of grants — a module contributing **its own** objects' grants into a role
the app package owns, by analogy with `navigationContributions` — is the other half of
hotcrm#1449's question. It is **filed, not decided**:
[#14488](https://github.com/objectstack-ai/objectstack/issues/14488), for the phase in which a
module ships on its own (the §1.3(c) CPQ case, where a module's objects would otherwise arrive
with no grants at all). It is out of this release and carries **no commitment** — neither that it
will be built, nor that the contribution shape is the one it will take.

Three inputs it inherits, recorded here because they are this record's own and would otherwise be
rediscovered:

- **ADR-0086 D4 stands until amended.** D4 chose Shape B — a package ships its own sets — and
states flatly: *"A package never writes into a shared/foreign record."* A contribution
mechanism is exactly such a write, so #14488 is an **amendment to that decision**, not an
addition beside it.
- **D4's conflict-freedom argument does not survive co-ownership unexamined.** It is
conflict-free *"because each set only grants `objects.<own-namespace>_*` keys"* — one namespace
per package. D1 lets N packages co-own one namespace, so inside one artifact that
discriminator is gone and #14488 must supply its own. (Both hotcrm#1449 and #14488 propose
**refusing** a doubly-contributed `(set, object)` rather than unioning it — a shape to measure,
not a decision this section makes.)
- **Which door edits a split product's app-owned sets is unmeasured.** ADR-0086 D7 scopes the
package Access door to *"this package's own object slice"* and keeps the cross-package
all-objects matrix at the environment-admin door. After a split the app package owns the sets
but no objects, so where a *packaged* cross-module set's grants are authored is a UI question
this record does not answer and #14488 inherits.
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions docs/adr/0130-release-artifact-as-co-ownership-boundary.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,10 @@
**Status**: Proposed (2026-09-01) — awaiting the maintainer's hand-merge, which is itself the
acceptance act for a governed surface (Prime Directive #14). ⛔ Nothing below is settled until
this record merges; the implementation cards are cut **from** the merged ADR, never ahead of it.
**Scope bounded by the 2026-09-02 addendum**
([#14487](https://github.com/objectstack-ai/objectstack/issues/14487)): the permission matrix
§1.3(a) measures is **not** part of this boundary's payoff — permission sets stay whole in the
`type: app` package.
**Deciders**: ObjectStack maintainer, 2026-09-01, live PM chat, verbatim and untranslated:
「立 ADR 起草卡派发」and 「14122 作为epic 任务集中跟踪」— approving the proposal in
[#14122](https://github.com/objectstack-ai/objectstack/issues/14122) into the ADR-drafting lane
Expand DownExpand Up@@ -507,3 +511,92 @@ a reader deserves to know which of its anchors were re-measured:
`packages/objectql/src/plugin.ts`, `packages/core/src/plugin-order.ts`,
`packages/spec/src/stack.zod.ts`, `packages/spec/src/kernel/manifest.zod.ts`,
`packages/cli/src/commands/compile.ts`

---

## Addendum (2026-09-02, #14487) — the permission matrix is outside this boundary's payoff; permission sets stay whole in the app package

**Provenance.** Maintainer ruling, 2026-09-02, live PM chat, recorded the same day on
[#14454](https://github.com/objectstack-ai/objectstack/issues/14454#issuecomment-5507189677)
and quoted here in the words that record carries — the PM seat's, not a transcript of the
maintainer's:「第 3 项(权限集)维护者 2026-09-02 拍板:取 B。」The same comment names what this
section must say:「权限集整体留在 app 包,权限矩阵不在包边界收益范围内」. The question ruled
on is item 3 of [#14454](https://github.com/objectstack-ai/objectstack/issues/14454) (carried
verbatim onto
[#14457](https://github.com/objectstack-ai/objectstack/issues/14457) as its decision card),
raised by the HotCRM split plan
[objectstack-ai/hotcrm#1449](https://github.com/objectstack-ai/hotcrm/pull/1449),
§上游缺口 / Upstream gaps item 3, which asked the maintainer to *"decide whether permission sets
can be composed per package (a module contributing its own object grants into a role the app
owns), or record that the permission matrix is explicitly out of scope for the boundary ADR-0130
creates."* **B is the second half of that sentence, and this section is that record.**

This section lands while the record is still **Proposed**: it bounds what the record claims and
settles nothing else — the maintainer's hand-merge remains the acceptance act, exactly as the
Status line says.

### What §1.3(a) measured, and what this boundary does not fix

§1.3(a) counts, among the three measurable consequences of having no boundary, *"a permission
matrix of 30 rows × 9 CRUD columns × 6 permission sets that interleaves `客户/联系人/商机` with
`运费标准/等级政策/工厂成本`"*, and §4 draws the payoff from that section: *"Studio's scope is the
package, so package boundaries **are** the grouping Studio has never had (§1.3a)"*. **The payoff
does not extend to the permission matrix.** Splitting a product into co-owning packages leaves
that matrix exactly as flat as §1.3(a) found it. This is said here, in the record, because
§1.3(a) lists the matrix as a pain and §4 answers §1.3(a) as a whole — a reader is otherwise
entitled to read a promise this record cannot keep.

**Why — measured, not reasoned.** A permission set grants across domains *by nature*: it is
authored per **role**, not per module, so no module owns it. hotcrm#1449 measured the standard
HotCRM product against its six planned modules — `core`, `sales`, `cpq`, `service`, `marketing`,
`activity`, all inside the one `crm` namespace D1 makes co-ownable, with the `type: app` package
declaring no objects at all. Of its **six** permission sets, **four span five or six of the six
modules** (`sales_rep`, `sales_manager` and `system_admin` at six; `service_agent` at five); the
remaining two span four (`marketing_user`) and two (`guest_portal`). **Not one is confined to a
single module.** The split therefore has nothing to distribute — whatever the module boundaries
are, every set still names objects on both sides of them. This is a second instance, independent
of the 黑猫 fork §1.3(a) measured: two products, the same shape.

**Where the sets live, and what Studio shows.** The six sets stay **whole in the `type: app`
package**. That is not a new rule but the standing one:
[ADR-0086](./0086-authz-metadata-config-boundary-and-cross-package-composition.md) D3 gives a
permission set exactly one owning `packageId` (implemented — `spec/security/permission.zod.ts`,
tagged `[ADR-0086 D3]`), so a set is in one package or in another and cannot be in several. In
Studio's **Access** pillar — the matrix of
[ADR-0084](./0084-application-builder-information-architecture.md), reached per package through
ADR-0086 D7's package door — the sets therefore appear **under the app package only**, and their
matrix stays as wide as the product. Modules group Data, Automation and Interface; they do not
group Access.

⛔ This section changes no decision: D1–D8, §1 and §3's non-goal on grouping keys stand exactly
as written. In particular **no `module` or grouping key is added to a permission set** — §1.4
rejected that key on measured cost, and nothing here reopens it.

### What was NOT decided

Per-package composition of grants — a module contributing **its own** objects' grants into a role
the app package owns, by analogy with `navigationContributions` — is the other half of
hotcrm#1449's question. It is **filed, not decided**:
[#14488](https://github.com/objectstack-ai/objectstack/issues/14488), for the phase in which a
module ships on its own (the §1.3(c) CPQ case, where a module's objects would otherwise arrive
with no grants at all). It is out of this release and carries **no commitment** — neither that it
will be built, nor that the contribution shape is the one it will take.

Three inputs it inherits, recorded here because they are this record's own and would otherwise be
rediscovered:

- **ADR-0086 D4 stands until amended.** D4 chose Shape B — a package ships its own sets — and
states flatly: *"A package never writes into a shared/foreign record."* A contribution
mechanism is exactly such a write, so #14488 is an **amendment to that decision**, not an
addition beside it.
- **D4's conflict-freedom argument does not survive co-ownership unexamined.** It is
conflict-free *"because each set only grants `objects.<own-namespace>_*` keys"* — one namespace
per package. D1 lets N packages co-own one namespace, so inside one artifact that
discriminator is gone and #14488 must supply its own. (Both hotcrm#1449 and #14488 propose
**refusing** a doubly-contributed `(set, object)` rather than unioning it — a shape to measure,
not a decision this section makes.)
- **Which door edits a split product's app-owned sets is unmeasured.** ADR-0086 D7 scopes the
package Access door to *"this package's own object slice"* and keeps the cross-package
all-objects matrix at the environment-admin door. After a split the app package owns the sets
but no objects, so where a *packaged* cross-module set's grants are authored is a UI question
this record does not answer and #14488 inherits.
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions docs/adr/0130-release-artifact-as-co-ownership-boundary.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,10 @@
**Status**: Proposed (2026-09-01) — awaiting the maintainer's hand-merge, which is itself the
acceptance act for a governed surface (Prime Directive #14). ⛔ Nothing below is settled until
this record merges; the implementation cards are cut **from** the merged ADR, never ahead of it.
**Scope bounded by the 2026-09-02 addendum**
([#14487](https://github.com/objectstack-ai/objectstack/issues/14487)): the permission matrix
§1.3(a) measures is **not** part of this boundary's payoff — permission sets stay whole in the
`type: app` package.
**Deciders**: ObjectStack maintainer, 2026-09-01, live PM chat, verbatim and untranslated:
「立 ADR 起草卡派发」and 「14122 作为epic 任务集中跟踪」— approving the proposal in
[#14122](https://github.com/objectstack-ai/objectstack/issues/14122) into the ADR-drafting lane
Expand DownExpand Up@@ -507,3 +511,92 @@ a reader deserves to know which of its anchors were re-measured:
`packages/objectql/src/plugin.ts`, `packages/core/src/plugin-order.ts`,
`packages/spec/src/stack.zod.ts`, `packages/spec/src/kernel/manifest.zod.ts`,
`packages/cli/src/commands/compile.ts`

---

## Addendum (2026-09-02, #14487) — the permission matrix is outside this boundary's payoff; permission sets stay whole in the app package

**Provenance.** Maintainer ruling, 2026-09-02, live PM chat, recorded the same day on
[#14454](https://github.com/objectstack-ai/objectstack/issues/14454#issuecomment-5507189677)
and quoted here in the words that record carries — the PM seat's, not a transcript of the
maintainer's:「第 3 项(权限集)维护者 2026-09-02 拍板:取 B。」The same comment names what this
section must say:「权限集整体留在 app 包,权限矩阵不在包边界收益范围内」. The question ruled
on is item 3 of [#14454](https://github.com/objectstack-ai/objectstack/issues/14454) (carried
verbatim onto
[#14457](https://github.com/objectstack-ai/objectstack/issues/14457) as its decision card),
raised by the HotCRM split plan
[objectstack-ai/hotcrm#1449](https://github.com/objectstack-ai/hotcrm/pull/1449),
§上游缺口 / Upstream gaps item 3, which asked the maintainer to *"decide whether permission sets
can be composed per package (a module contributing its own object grants into a role the app
owns), or record that the permission matrix is explicitly out of scope for the boundary ADR-0130
creates."* **B is the second half of that sentence, and this section is that record.**

This section lands while the record is still **Proposed**: it bounds what the record claims and
settles nothing else — the maintainer's hand-merge remains the acceptance act, exactly as the
Status line says.

### What §1.3(a) measured, and what this boundary does not fix

§1.3(a) counts, among the three measurable consequences of having no boundary, *"a permission
matrix of 30 rows × 9 CRUD columns × 6 permission sets that interleaves `客户/联系人/商机` with
`运费标准/等级政策/工厂成本`"*, and §4 draws the payoff from that section: *"Studio's scope is the
package, so package boundaries **are** the grouping Studio has never had (§1.3a)"*. **The payoff
does not extend to the permission matrix.** Splitting a product into co-owning packages leaves
that matrix exactly as flat as §1.3(a) found it. This is said here, in the record, because
§1.3(a) lists the matrix as a pain and §4 answers §1.3(a) as a whole — a reader is otherwise
entitled to read a promise this record cannot keep.

**Why — measured, not reasoned.** A permission set grants across domains *by nature*: it is
authored per **role**, not per module, so no module owns it. hotcrm#1449 measured the standard
HotCRM product against its six planned modules — `core`, `sales`, `cpq`, `service`, `marketing`,
`activity`, all inside the one `crm` namespace D1 makes co-ownable, with the `type: app` package
declaring no objects at all. Of its **six** permission sets, **four span five or six of the six
modules** (`sales_rep`, `sales_manager` and `system_admin` at six; `service_agent` at five); the
remaining two span four (`marketing_user`) and two (`guest_portal`). **Not one is confined to a
single module.** The split therefore has nothing to distribute — whatever the module boundaries
are, every set still names objects on both sides of them. This is a second instance, independent
of the 黑猫 fork §1.3(a) measured: two products, the same shape.

**Where the sets live, and what Studio shows.** The six sets stay **whole in the `type: app`
package**. That is not a new rule but the standing one:
[ADR-0086](./0086-authz-metadata-config-boundary-and-cross-package-composition.md) D3 gives a
permission set exactly one owning `packageId` (implemented — `spec/security/permission.zod.ts`,
tagged `[ADR-0086 D3]`), so a set is in one package or in another and cannot be in several. In
Studio's **Access** pillar — the matrix of
[ADR-0084](./0084-application-builder-information-architecture.md), reached per package through
ADR-0086 D7's package door — the sets therefore appear **under the app package only**, and their
matrix stays as wide as the product. Modules group Data, Automation and Interface; they do not
group Access.

⛔ This section changes no decision: D1–D8, §1 and §3's non-goal on grouping keys stand exactly
as written. In particular **no `module` or grouping key is added to a permission set** — §1.4
rejected that key on measured cost, and nothing here reopens it.

### What was NOT decided

Per-package composition of grants — a module contributing **its own** objects' grants into a role
the app package owns, by analogy with `navigationContributions` — is the other half of
hotcrm#1449's question. It is **filed, not decided**:
[#14488](https://github.com/objectstack-ai/objectstack/issues/14488), for the phase in which a
module ships on its own (the §1.3(c) CPQ case, where a module's objects would otherwise arrive
with no grants at all). It is out of this release and carries **no commitment** — neither that it
will be built, nor that the contribution shape is the one it will take.

Three inputs it inherits, recorded here because they are this record's own and would otherwise be
rediscovered:

- **ADR-0086 D4 stands until amended.** D4 chose Shape B — a package ships its own sets — and
states flatly: *"A package never writes into a shared/foreign record."* A contribution
mechanism is exactly such a write, so #14488 is an **amendment to that decision**, not an
addition beside it.
- **D4's conflict-freedom argument does not survive co-ownership unexamined.** It is
conflict-free *"because each set only grants `objects.<own-namespace>_*` keys"* — one namespace
per package. D1 lets N packages co-own one namespace, so inside one artifact that
discriminator is gone and #14488 must supply its own. (Both hotcrm#1449 and #14488 propose
**refusing** a doubly-contributed `(set, object)` rather than unioning it — a shape to measure,
not a decision this section makes.)
- **Which door edits a split product's app-owned sets is unmeasured.** ADR-0086 D7 scopes the
package Access door to *"this package's own object slice"* and keeps the cross-package
all-objects matrix at the environment-admin door. After a split the app package owns the sets
but no objects, so where a *packaged* cross-module set's grants are authored is a UI question
this record does not answer and #14488 inherits.
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions docs/adr/0130-release-artifact-as-co-ownership-boundary.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,10 @@
**Status**: Proposed (2026-09-01) — awaiting the maintainer's hand-merge, which is itself the
acceptance act for a governed surface (Prime Directive #14). ⛔ Nothing below is settled until
this record merges; the implementation cards are cut **from** the merged ADR, never ahead of it.
**Scope bounded by the 2026-09-02 addendum**
([#14487](https://github.com/objectstack-ai/objectstack/issues/14487)): the permission matrix
§1.3(a) measures is **not** part of this boundary's payoff — permission sets stay whole in the
`type: app` package.
**Deciders**: ObjectStack maintainer, 2026-09-01, live PM chat, verbatim and untranslated:
「立 ADR 起草卡派发」and 「14122 作为epic 任务集中跟踪」— approving the proposal in
[#14122](https://github.com/objectstack-ai/objectstack/issues/14122) into the ADR-drafting lane
Expand DownExpand Up@@ -507,3 +511,92 @@ a reader deserves to know which of its anchors were re-measured:
`packages/objectql/src/plugin.ts`, `packages/core/src/plugin-order.ts`,
`packages/spec/src/stack.zod.ts`, `packages/spec/src/kernel/manifest.zod.ts`,
`packages/cli/src/commands/compile.ts`

---

## Addendum (2026-09-02, #14487) — the permission matrix is outside this boundary's payoff; permission sets stay whole in the app package

**Provenance.** Maintainer ruling, 2026-09-02, live PM chat, recorded the same day on
[#14454](https://github.com/objectstack-ai/objectstack/issues/14454#issuecomment-5507189677)
and quoted here in the words that record carries — the PM seat's, not a transcript of the
maintainer's:「第 3 项(权限集)维护者 2026-09-02 拍板:取 B。」The same comment names what this
section must say:「权限集整体留在 app 包,权限矩阵不在包边界收益范围内」. The question ruled
on is item 3 of [#14454](https://github.com/objectstack-ai/objectstack/issues/14454) (carried
verbatim onto
[#14457](https://github.com/objectstack-ai/objectstack/issues/14457) as its decision card),
raised by the HotCRM split plan
[objectstack-ai/hotcrm#1449](https://github.com/objectstack-ai/hotcrm/pull/1449),
§上游缺口 / Upstream gaps item 3, which asked the maintainer to *"decide whether permission sets
can be composed per package (a module contributing its own object grants into a role the app
owns), or record that the permission matrix is explicitly out of scope for the boundary ADR-0130
creates."* **B is the second half of that sentence, and this section is that record.**

This section lands while the record is still **Proposed**: it bounds what the record claims and
settles nothing else — the maintainer's hand-merge remains the acceptance act, exactly as the
Status line says.

### What §1.3(a) measured, and what this boundary does not fix

§1.3(a) counts, among the three measurable consequences of having no boundary, *"a permission
matrix of 30 rows × 9 CRUD columns × 6 permission sets that interleaves `客户/联系人/商机` with
`运费标准/等级政策/工厂成本`"*, and §4 draws the payoff from that section: *"Studio's scope is the
package, so package boundaries **are** the grouping Studio has never had (§1.3a)"*. **The payoff
does not extend to the permission matrix.** Splitting a product into co-owning packages leaves
that matrix exactly as flat as §1.3(a) found it. This is said here, in the record, because
§1.3(a) lists the matrix as a pain and §4 answers §1.3(a) as a whole — a reader is otherwise
entitled to read a promise this record cannot keep.

**Why — measured, not reasoned.** A permission set grants across domains *by nature*: it is
authored per **role**, not per module, so no module owns it. hotcrm#1449 measured the standard
HotCRM product against its six planned modules — `core`, `sales`, `cpq`, `service`, `marketing`,
`activity`, all inside the one `crm` namespace D1 makes co-ownable, with the `type: app` package
declaring no objects at all. Of its **six** permission sets, **four span five or six of the six
modules** (`sales_rep`, `sales_manager` and `system_admin` at six; `service_agent` at five); the
remaining two span four (`marketing_user`) and two (`guest_portal`). **Not one is confined to a
single module.** The split therefore has nothing to distribute — whatever the module boundaries
are, every set still names objects on both sides of them. This is a second instance, independent
of the 黑猫 fork §1.3(a) measured: two products, the same shape.

**Where the sets live, and what Studio shows.** The six sets stay **whole in the `type: app`
package**. That is not a new rule but the standing one:
[ADR-0086](./0086-authz-metadata-config-boundary-and-cross-package-composition.md) D3 gives a
permission set exactly one owning `packageId` (implemented — `spec/security/permission.zod.ts`,
tagged `[ADR-0086 D3]`), so a set is in one package or in another and cannot be in several. In
Studio's **Access** pillar — the matrix of
[ADR-0084](./0084-application-builder-information-architecture.md), reached per package through
ADR-0086 D7's package door — the sets therefore appear **under the app package only**, and their
matrix stays as wide as the product. Modules group Data, Automation and Interface; they do not
group Access.

⛔ This section changes no decision: D1–D8, §1 and §3's non-goal on grouping keys stand exactly
as written. In particular **no `module` or grouping key is added to a permission set** — §1.4
rejected that key on measured cost, and nothing here reopens it.

### What was NOT decided

Per-package composition of grants — a module contributing **its own** objects' grants into a role
the app package owns, by analogy with `navigationContributions` — is the other half of
hotcrm#1449's question. It is **filed, not decided**:
[#14488](https://github.com/objectstack-ai/objectstack/issues/14488), for the phase in which a
module ships on its own (the §1.3(c) CPQ case, where a module's objects would otherwise arrive
with no grants at all). It is out of this release and carries **no commitment** — neither that it
will be built, nor that the contribution shape is the one it will take.

Three inputs it inherits, recorded here because they are this record's own and would otherwise be
rediscovered:

- **ADR-0086 D4 stands until amended.** D4 chose Shape B — a package ships its own sets — and
states flatly: *"A package never writes into a shared/foreign record."* A contribution
mechanism is exactly such a write, so #14488 is an **amendment to that decision**, not an
addition beside it.
- **D4's conflict-freedom argument does not survive co-ownership unexamined.** It is
conflict-free *"because each set only grants `objects.<own-namespace>_*` keys"* — one namespace
per package. D1 lets N packages co-own one namespace, so inside one artifact that
discriminator is gone and #14488 must supply its own. (Both hotcrm#1449 and #14488 propose
**refusing** a doubly-contributed `(set, object)` rather than unioning it — a shape to measure,
not a decision this section makes.)
- **Which door edits a split product's app-owned sets is unmeasured.** ADR-0086 D7 scopes the
package Access door to *"this package's own object slice"* and keeps the cross-package
all-objects matrix at the environment-admin door. After a split the app package owns the sets
but no objects, so where a *packaged* cross-module set's grants are authored is a UI question
this record does not answer and #14488 inherits.
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions docs/adr/0130-release-artifact-as-co-ownership-boundary.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,10 @@
**Status**: Proposed (2026-09-01) — awaiting the maintainer's hand-merge, which is itself the
acceptance act for a governed surface (Prime Directive #14). ⛔ Nothing below is settled until
this record merges; the implementation cards are cut **from** the merged ADR, never ahead of it.
**Scope bounded by the 2026-09-02 addendum**
([#14487](https://github.com/objectstack-ai/objectstack/issues/14487)): the permission matrix
§1.3(a) measures is **not** part of this boundary's payoff — permission sets stay whole in the
`type: app` package.
**Deciders**: ObjectStack maintainer, 2026-09-01, live PM chat, verbatim and untranslated:
「立 ADR 起草卡派发」and 「14122 作为epic 任务集中跟踪」— approving the proposal in
[#14122](https://github.com/objectstack-ai/objectstack/issues/14122) into the ADR-drafting lane
Expand DownExpand Up@@ -507,3 +511,92 @@ a reader deserves to know which of its anchors were re-measured:
`packages/objectql/src/plugin.ts`, `packages/core/src/plugin-order.ts`,
`packages/spec/src/stack.zod.ts`, `packages/spec/src/kernel/manifest.zod.ts`,
`packages/cli/src/commands/compile.ts`

---

## Addendum (2026-09-02, #14487) — the permission matrix is outside this boundary's payoff; permission sets stay whole in the app package

**Provenance.** Maintainer ruling, 2026-09-02, live PM chat, recorded the same day on
[#14454](https://github.com/objectstack-ai/objectstack/issues/14454#issuecomment-5507189677)
and quoted here in the words that record carries — the PM seat's, not a transcript of the
maintainer's:「第 3 项(权限集)维护者 2026-09-02 拍板:取 B。」The same comment names what this
section must say:「权限集整体留在 app 包,权限矩阵不在包边界收益范围内」. The question ruled
on is item 3 of [#14454](https://github.com/objectstack-ai/objectstack/issues/14454) (carried
verbatim onto
[#14457](https://github.com/objectstack-ai/objectstack/issues/14457) as its decision card),
raised by the HotCRM split plan
[objectstack-ai/hotcrm#1449](https://github.com/objectstack-ai/hotcrm/pull/1449),
§上游缺口 / Upstream gaps item 3, which asked the maintainer to *"decide whether permission sets
can be composed per package (a module contributing its own object grants into a role the app
owns), or record that the permission matrix is explicitly out of scope for the boundary ADR-0130
creates."* **B is the second half of that sentence, and this section is that record.**

This section lands while the record is still **Proposed**: it bounds what the record claims and
settles nothing else — the maintainer's hand-merge remains the acceptance act, exactly as the
Status line says.

### What §1.3(a) measured, and what this boundary does not fix

§1.3(a) counts, among the three measurable consequences of having no boundary, *"a permission
matrix of 30 rows × 9 CRUD columns × 6 permission sets that interleaves `客户/联系人/商机` with
`运费标准/等级政策/工厂成本`"*, and §4 draws the payoff from that section: *"Studio's scope is the
package, so package boundaries **are** the grouping Studio has never had (§1.3a)"*. **The payoff
does not extend to the permission matrix.** Splitting a product into co-owning packages leaves
that matrix exactly as flat as §1.3(a) found it. This is said here, in the record, because
§1.3(a) lists the matrix as a pain and §4 answers §1.3(a) as a whole — a reader is otherwise
entitled to read a promise this record cannot keep.

**Why — measured, not reasoned.** A permission set grants across domains *by nature*: it is
authored per **role**, not per module, so no module owns it. hotcrm#1449 measured the standard
HotCRM product against its six planned modules — `core`, `sales`, `cpq`, `service`, `marketing`,
`activity`, all inside the one `crm` namespace D1 makes co-ownable, with the `type: app` package
declaring no objects at all. Of its **six** permission sets, **four span five or six of the six
modules** (`sales_rep`, `sales_manager` and `system_admin` at six; `service_agent` at five); the
remaining two span four (`marketing_user`) and two (`guest_portal`). **Not one is confined to a
single module.** The split therefore has nothing to distribute — whatever the module boundaries
are, every set still names objects on both sides of them. This is a second instance, independent
of the 黑猫 fork §1.3(a) measured: two products, the same shape.

**Where the sets live, and what Studio shows.** The six sets stay **whole in the `type: app`
package**. That is not a new rule but the standing one:
[ADR-0086](./0086-authz-metadata-config-boundary-and-cross-package-composition.md) D3 gives a
permission set exactly one owning `packageId` (implemented — `spec/security/permission.zod.ts`,
tagged `[ADR-0086 D3]`), so a set is in one package or in another and cannot be in several. In
Studio's **Access** pillar — the matrix of
[ADR-0084](./0084-application-builder-information-architecture.md), reached per package through
ADR-0086 D7's package door — the sets therefore appear **under the app package only**, and their
matrix stays as wide as the product. Modules group Data, Automation and Interface; they do not
group Access.

⛔ This section changes no decision: D1–D8, §1 and §3's non-goal on grouping keys stand exactly
as written. In particular **no `module` or grouping key is added to a permission set** — §1.4
rejected that key on measured cost, and nothing here reopens it.

### What was NOT decided

Per-package composition of grants — a module contributing **its own** objects' grants into a role
the app package owns, by analogy with `navigationContributions` — is the other half of
hotcrm#1449's question. It is **filed, not decided**:
[#14488](https://github.com/objectstack-ai/objectstack/issues/14488), for the phase in which a
module ships on its own (the §1.3(c) CPQ case, where a module's objects would otherwise arrive
with no grants at all). It is out of this release and carries **no commitment** — neither that it
will be built, nor that the contribution shape is the one it will take.

Three inputs it inherits, recorded here because they are this record's own and would otherwise be
rediscovered:

- **ADR-0086 D4 stands until amended.** D4 chose Shape B — a package ships its own sets — and
states flatly: *"A package never writes into a shared/foreign record."* A contribution
mechanism is exactly such a write, so #14488 is an **amendment to that decision**, not an
addition beside it.
- **D4's conflict-freedom argument does not survive co-ownership unexamined.** It is
conflict-free *"because each set only grants `objects.<own-namespace>_*` keys"* — one namespace
per package. D1 lets N packages co-own one namespace, so inside one artifact that
discriminator is gone and #14488 must supply its own. (Both hotcrm#1449 and #14488 propose
**refusing** a doubly-contributed `(set, object)` rather than unioning it — a shape to measure,
not a decision this section makes.)
- **Which door edits a split product's app-owned sets is unmeasured.** ADR-0086 D7 scopes the
package Access door to *"this package's own object slice"* and keeps the cross-package
all-objects matrix at the environment-admin door. After a split the app package owns the sets
but no objects, so where a *packaged* cross-module set's grants are authored is a UI question
this record does not answer and #14488 inherits.
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions docs/adr/0130-release-artifact-as-co-ownership-boundary.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,10 @@
**Status**: Proposed (2026-09-01) — awaiting the maintainer's hand-merge, which is itself the
acceptance act for a governed surface (Prime Directive #14). ⛔ Nothing below is settled until
this record merges; the implementation cards are cut **from** the merged ADR, never ahead of it.
**Scope bounded by the 2026-09-02 addendum**
([#14487](https://github.com/objectstack-ai/objectstack/issues/14487)): the permission matrix
§1.3(a) measures is **not** part of this boundary's payoff — permission sets stay whole in the
`type: app` package.
**Deciders**: ObjectStack maintainer, 2026-09-01, live PM chat, verbatim and untranslated:
「立 ADR 起草卡派发」and 「14122 作为epic 任务集中跟踪」— approving the proposal in
[#14122](https://github.com/objectstack-ai/objectstack/issues/14122) into the ADR-drafting lane
Expand DownExpand Up@@ -507,3 +511,92 @@ a reader deserves to know which of its anchors were re-measured:
`packages/objectql/src/plugin.ts`, `packages/core/src/plugin-order.ts`,
`packages/spec/src/stack.zod.ts`, `packages/spec/src/kernel/manifest.zod.ts`,
`packages/cli/src/commands/compile.ts`

---

## Addendum (2026-09-02, #14487) — the permission matrix is outside this boundary's payoff; permission sets stay whole in the app package

**Provenance.** Maintainer ruling, 2026-09-02, live PM chat, recorded the same day on
[#14454](https://github.com/objectstack-ai/objectstack/issues/14454#issuecomment-5507189677)
and quoted here in the words that record carries — the PM seat's, not a transcript of the
maintainer's:「第 3 项(权限集)维护者 2026-09-02 拍板:取 B。」The same comment names what this
section must say:「权限集整体留在 app 包,权限矩阵不在包边界收益范围内」. The question ruled
on is item 3 of [#14454](https://github.com/objectstack-ai/objectstack/issues/14454) (carried
verbatim onto
[#14457](https://github.com/objectstack-ai/objectstack/issues/14457) as its decision card),
raised by the HotCRM split plan
[objectstack-ai/hotcrm#1449](https://github.com/objectstack-ai/hotcrm/pull/1449),
§上游缺口 / Upstream gaps item 3, which asked the maintainer to *"decide whether permission sets
can be composed per package (a module contributing its own object grants into a role the app
owns), or record that the permission matrix is explicitly out of scope for the boundary ADR-0130
creates."* **B is the second half of that sentence, and this section is that record.**

This section lands while the record is still **Proposed**: it bounds what the record claims and
settles nothing else — the maintainer's hand-merge remains the acceptance act, exactly as the
Status line says.

### What §1.3(a) measured, and what this boundary does not fix

§1.3(a) counts, among the three measurable consequences of having no boundary, *"a permission
matrix of 30 rows × 9 CRUD columns × 6 permission sets that interleaves `客户/联系人/商机` with
`运费标准/等级政策/工厂成本`"*, and §4 draws the payoff from that section: *"Studio's scope is the
package, so package boundaries **are** the grouping Studio has never had (§1.3a)"*. **The payoff
does not extend to the permission matrix.** Splitting a product into co-owning packages leaves
that matrix exactly as flat as §1.3(a) found it. This is said here, in the record, because
§1.3(a) lists the matrix as a pain and §4 answers §1.3(a) as a whole — a reader is otherwise
entitled to read a promise this record cannot keep.

**Why — measured, not reasoned.** A permission set grants across domains *by nature*: it is
authored per **role**, not per module, so no module owns it. hotcrm#1449 measured the standard
HotCRM product against its six planned modules — `core`, `sales`, `cpq`, `service`, `marketing`,
`activity`, all inside the one `crm` namespace D1 makes co-ownable, with the `type: app` package
declaring no objects at all. Of its **six** permission sets, **four span five or six of the six
modules** (`sales_rep`, `sales_manager` and `system_admin` at six; `service_agent` at five); the
remaining two span four (`marketing_user`) and two (`guest_portal`). **Not one is confined to a
single module.** The split therefore has nothing to distribute — whatever the module boundaries
are, every set still names objects on both sides of them. This is a second instance, independent
of the 黑猫 fork §1.3(a) measured: two products, the same shape.

**Where the sets live, and what Studio shows.** The six sets stay **whole in the `type: app`
package**. That is not a new rule but the standing one:
[ADR-0086](./0086-authz-metadata-config-boundary-and-cross-package-composition.md) D3 gives a
permission set exactly one owning `packageId` (implemented — `spec/security/permission.zod.ts`,
tagged `[ADR-0086 D3]`), so a set is in one package or in another and cannot be in several. In
Studio's **Access** pillar — the matrix of
[ADR-0084](./0084-application-builder-information-architecture.md), reached per package through
ADR-0086 D7's package door — the sets therefore appear **under the app package only**, and their
matrix stays as wide as the product. Modules group Data, Automation and Interface; they do not
group Access.

⛔ This section changes no decision: D1–D8, §1 and §3's non-goal on grouping keys stand exactly
as written. In particular **no `module` or grouping key is added to a permission set** — §1.4
rejected that key on measured cost, and nothing here reopens it.

### What was NOT decided

Per-package composition of grants — a module contributing **its own** objects' grants into a role
the app package owns, by analogy with `navigationContributions` — is the other half of
hotcrm#1449's question. It is **filed, not decided**:
[#14488](https://github.com/objectstack-ai/objectstack/issues/14488), for the phase in which a
module ships on its own (the §1.3(c) CPQ case, where a module's objects would otherwise arrive
with no grants at all). It is out of this release and carries **no commitment** — neither that it
will be built, nor that the contribution shape is the one it will take.

Three inputs it inherits, recorded here because they are this record's own and would otherwise be
rediscovered:

- **ADR-0086 D4 stands until amended.** D4 chose Shape B — a package ships its own sets — and
states flatly: *"A package never writes into a shared/foreign record."* A contribution
mechanism is exactly such a write, so #14488 is an **amendment to that decision**, not an
addition beside it.
- **D4's conflict-freedom argument does not survive co-ownership unexamined.** It is
conflict-free *"because each set only grants `objects.<own-namespace>_*` keys"* — one namespace
per package. D1 lets N packages co-own one namespace, so inside one artifact that
discriminator is gone and #14488 must supply its own. (Both hotcrm#1449 and #14488 propose
**refusing** a doubly-contributed `(set, object)` rather than unioning it — a shape to measure,
not a decision this section makes.)
- **Which door edits a split product's app-owned sets is unmeasured.** ADR-0086 D7 scopes the
package Access door to *"this package's own object slice"* and keeps the cross-package
all-objects matrix at the environment-admin door. After a split the app package owns the sets
but no objects, so where a *packaged* cross-module set's grants are authored is a UI question
this record does not answer and #14488 inherits.
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions docs/adr/0130-release-artifact-as-co-ownership-boundary.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,10 @@
**Status**: Proposed (2026-09-01) — awaiting the maintainer's hand-merge, which is itself the
acceptance act for a governed surface (Prime Directive #14). ⛔ Nothing below is settled until
this record merges; the implementation cards are cut **from** the merged ADR, never ahead of it.
**Scope bounded by the 2026-09-02 addendum**
([#14487](https://github.com/objectstack-ai/objectstack/issues/14487)): the permission matrix
§1.3(a) measures is **not** part of this boundary's payoff — permission sets stay whole in the
`type: app` package.
**Deciders**: ObjectStack maintainer, 2026-09-01, live PM chat, verbatim and untranslated:
「立 ADR 起草卡派发」and 「14122 作为epic 任务集中跟踪」— approving the proposal in
[#14122](https://github.com/objectstack-ai/objectstack/issues/14122) into the ADR-drafting lane
Expand DownExpand Up@@ -507,3 +511,92 @@ a reader deserves to know which of its anchors were re-measured:
`packages/objectql/src/plugin.ts`, `packages/core/src/plugin-order.ts`,
`packages/spec/src/stack.zod.ts`, `packages/spec/src/kernel/manifest.zod.ts`,
`packages/cli/src/commands/compile.ts`

---

## Addendum (2026-09-02, #14487) — the permission matrix is outside this boundary's payoff; permission sets stay whole in the app package

**Provenance.** Maintainer ruling, 2026-09-02, live PM chat, recorded the same day on
[#14454](https://github.com/objectstack-ai/objectstack/issues/14454#issuecomment-5507189677)
and quoted here in the words that record carries — the PM seat's, not a transcript of the
maintainer's:「第 3 项(权限集)维护者 2026-09-02 拍板:取 B。」The same comment names what this
section must say:「权限集整体留在 app 包,权限矩阵不在包边界收益范围内」. The question ruled
on is item 3 of [#14454](https://github.com/objectstack-ai/objectstack/issues/14454) (carried
verbatim onto
[#14457](https://github.com/objectstack-ai/objectstack/issues/14457) as its decision card),
raised by the HotCRM split plan
[objectstack-ai/hotcrm#1449](https://github.com/objectstack-ai/hotcrm/pull/1449),
§上游缺口 / Upstream gaps item 3, which asked the maintainer to *"decide whether permission sets
can be composed per package (a module contributing its own object grants into a role the app
owns), or record that the permission matrix is explicitly out of scope for the boundary ADR-0130
creates."* **B is the second half of that sentence, and this section is that record.**

This section lands while the record is still **Proposed**: it bounds what the record claims and
settles nothing else — the maintainer's hand-merge remains the acceptance act, exactly as the
Status line says.

### What §1.3(a) measured, and what this boundary does not fix

§1.3(a) counts, among the three measurable consequences of having no boundary, *"a permission
matrix of 30 rows × 9 CRUD columns × 6 permission sets that interleaves `客户/联系人/商机` with
`运费标准/等级政策/工厂成本`"*, and §4 draws the payoff from that section: *"Studio's scope is the
package, so package boundaries **are** the grouping Studio has never had (§1.3a)"*. **The payoff
does not extend to the permission matrix.** Splitting a product into co-owning packages leaves
that matrix exactly as flat as §1.3(a) found it. This is said here, in the record, because
§1.3(a) lists the matrix as a pain and §4 answers §1.3(a) as a whole — a reader is otherwise
entitled to read a promise this record cannot keep.

**Why — measured, not reasoned.** A permission set grants across domains *by nature*: it is
authored per **role**, not per module, so no module owns it. hotcrm#1449 measured the standard
HotCRM product against its six planned modules — `core`, `sales`, `cpq`, `service`, `marketing`,
`activity`, all inside the one `crm` namespace D1 makes co-ownable, with the `type: app` package
declaring no objects at all. Of its **six** permission sets, **four span five or six of the six
modules** (`sales_rep`, `sales_manager` and `system_admin` at six; `service_agent` at five); the
remaining two span four (`marketing_user`) and two (`guest_portal`). **Not one is confined to a
single module.** The split therefore has nothing to distribute — whatever the module boundaries
are, every set still names objects on both sides of them. This is a second instance, independent
of the 黑猫 fork §1.3(a) measured: two products, the same shape.

**Where the sets live, and what Studio shows.** The six sets stay **whole in the `type: app`
package**. That is not a new rule but the standing one:
[ADR-0086](./0086-authz-metadata-config-boundary-and-cross-package-composition.md) D3 gives a
permission set exactly one owning `packageId` (implemented — `spec/security/permission.zod.ts`,
tagged `[ADR-0086 D3]`), so a set is in one package or in another and cannot be in several. In
Studio's **Access** pillar — the matrix of
[ADR-0084](./0084-application-builder-information-architecture.md), reached per package through
ADR-0086 D7's package door — the sets therefore appear **under the app package only**, and their
matrix stays as wide as the product. Modules group Data, Automation and Interface; they do not
group Access.

⛔ This section changes no decision: D1–D8, §1 and §3's non-goal on grouping keys stand exactly
as written. In particular **no `module` or grouping key is added to a permission set** — §1.4
rejected that key on measured cost, and nothing here reopens it.

### What was NOT decided

Per-package composition of grants — a module contributing **its own** objects' grants into a role
the app package owns, by analogy with `navigationContributions` — is the other half of
hotcrm#1449's question. It is **filed, not decided**:
[#14488](https://github.com/objectstack-ai/objectstack/issues/14488), for the phase in which a
module ships on its own (the §1.3(c) CPQ case, where a module's objects would otherwise arrive
with no grants at all). It is out of this release and carries **no commitment** — neither that it
will be built, nor that the contribution shape is the one it will take.

Three inputs it inherits, recorded here because they are this record's own and would otherwise be
rediscovered:

- **ADR-0086 D4 stands until amended.** D4 chose Shape B — a package ships its own sets — and
states flatly: *"A package never writes into a shared/foreign record."* A contribution
mechanism is exactly such a write, so #14488 is an **amendment to that decision**, not an
addition beside it.
- **D4's conflict-freedom argument does not survive co-ownership unexamined.** It is
conflict-free *"because each set only grants `objects.<own-namespace>_*` keys"* — one namespace
per package. D1 lets N packages co-own one namespace, so inside one artifact that
discriminator is gone and #14488 must supply its own. (Both hotcrm#1449 and #14488 propose
**refusing** a doubly-contributed `(set, object)` rather than unioning it — a shape to measure,
not a decision this section makes.)
- **Which door edits a split product's app-owned sets is unmeasured.** ADR-0086 D7 scopes the
package Access door to *"this package's own object slice"* and keeps the cross-package
all-objects matrix at the environment-admin door. After a split the app package owns the sets
but no objects, so where a *packaged* cross-module set's grants are authored is a UI question
this record does not answer and #14488 inherits.
Loading