|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | + |
| 3 | +import type { SemanticMigration } from '../../types.js'; |
| 4 | + |
| 5 | +// ADR-0055 §1, judged on the object the runtime runs. An accept-set narrowing |
| 6 | +// of the stack schema: an objectExtensions entry that leaves a |
| 7 | +// controlled_by_parent object the same stack declares with anything but one |
| 8 | +// required master_detail is refused, as the same object declared directly |
| 9 | +// already was (cbp-master-relation-tiers-retired). No D2 conversion: which |
| 10 | +// master stays is the owner's pick, and turning one into a lookup changes what |
| 11 | +// deleting its master does. No stored row carries the shape — an extension is |
| 12 | +// merged only when a stack or artifact registers — so the consequence at rest |
| 13 | +// is an artifact built before 18, which its load now refuses. |
| 14 | +export const entry: SemanticMigration = { |
| 15 | + id: 'cbp-extension-merged-master-refused', |
| 16 | + // No backticks in `surface` — build-upgrade-guide renders it inside a code |
| 17 | + // span already, and a nested backtick would close it. |
| 18 | + surface: |
| 19 | + 'objectExtensions[].fields (also packages[].manifest.objectExtensions[].fields) merged into a ' |
| 20 | + + 'sharingModel: controlled_by_parent object the same stack or artifact declares', |
| 21 | + replacement: |
| 22 | + 'The object as merged keeps exactly one master_detail field declared required: true. An extension of a ' |
| 23 | + + 'controlled_by_parent object adds every other parent as a lookup field (drop the second master, or ' |
| 24 | + + 'declare it type: \'lookup\'), and never redeclares the master field with required: false or another ' |
| 25 | + + 'type. If a second parent must govern the rows, that is a different object design, decided by the ' |
| 26 | + + 'owner of the object, not by an extension.', |
| 27 | + reason: |
| 28 | + 'The registry merges every objectExtensions entry onto its target at boot, a field replaced whole by name, ' |
| 29 | + + 'so an object that passed its own parse could run with two required master_detail fields, or none. The ' |
| 30 | + + 'runtime then derives no master: every read of the object is denied and every write refused with 422 ' |
| 31 | + + 'INVALID_METADATA, while defineStack, objectstack validate and the security-posture lint said nothing, ' |
| 32 | + + 'although they refused the same object declared directly. ObjectStackDefinitionSchema now judges the ' |
| 33 | + + 'ADR-0055 §1 rule on the merged field set of every target the stack declares (in a multi-package ' |
| 34 | + + 'artifact, any package\'s object), and refuses at the extension\'s fields with the message the declared ' |
| 35 | + + 'object gets; the lint reports security-controlled-by-parent-ambiguous-relation or -no-relation at the ' |
| 36 | + + 'same extension. A target the stack does not declare is not judged, so an extension of another ' |
| 37 | + + 'package\'s object still meets the runtime\'s refusal instead. Not convertible: which master stays, and ' |
| 38 | + + 'whether a parent becomes a lookup, are the owner\'s decisions.', |
| 39 | + acceptanceCriteria: |
| 40 | + 'defineStack, objectstack validate and objectstack build load the stack with no issue at an ' |
| 41 | + + 'objectExtensions entry\'s fields of the form "declares sharingModel \'controlled_by_parent\' with N ' |
| 42 | + + 'required master_detail fields" or "but no required master_detail field", and objectstack lint reports ' |
| 43 | + + 'neither security-controlled-by-parent rule at an objectExtensions path. At runtime the security plugin ' |
| 44 | + + 'logs no "resolves no master relation" line for the extended object, and a member who can read its ' |
| 45 | + + 'master reads its rows. An artifact built before 18 that carries the shape is refused at load until it ' |
| 46 | + + 'is rebuilt from the corrected source.', |
| 47 | + relevantWhen: { kind: 'stack-declares', keys: ['objectExtensions'] }, |
| 48 | +}; |
0 commit comments