Skip to content

Commit eda26ce

Browse files
claude[bot]claude
andauthored
fix(lint): split OWD_ALIAS_FIX provenance — three retired D4 aliases vs one wrong-layer fix-it (#16975)
* fix(lint): split OWD_ALIAS_FIX provenance — three retired D4 aliases vs one wrong-layer fix-it `OWD_ALIAS_FIX` labelled all four of its keys `[ADR-0090 D4] Legacy alias → canonical fix-it mapping`, but D4 names three: "The legacy aliases `read`, `read_write`, `full` are removed from the zod enum". `public` was never an accepted `sharingModel` value in any shipped schema, so it has no retirement behind it and no ADR-0087 conversion or semantic-migration entry could exist for it — the stored population is zero by construction. Split the map into `OWD_RETIRED_ALIAS_FIX` (the three D4 aliases) and `OWD_WRONG_LAYER_FIX` (`public`), keep the union as `OWD_ALIAS_FIX` so every key still earns the same rule id, path and fix-it, and give the finding's message one shared provenance clause so it stops telling authors that `public` is a retired D4 alias. Also correct the five `packages/cli` serve e2e fixtures that declared `sharingModel: 'public'` — a value `ObjectSchema` refuses and `effectiveSharingModel` folds to `private`. They now declare `private`, which is the posture those apps already had. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x * fix(lint): keep the receiver-coverage guard armed, and add the changeset The new provenance clause named the sibling keys dotted, and this rule's receiver-coverage meta-test scans the module's code text for `receiver.key` reads without being able to tell one inside a message string from a real read — so `access.default` / `publicSharing.allowedAudiences` presented as undeclared reads off receivers that do not exist. Name them undotted instead and record why, rather than excusing them in the guard's PLUMBING set, which would have disarmed a real check for a spelling choice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x * style(lint): reflow the intake docblock paragraph Comment rewrap only, no code change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x --------- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 1008be3 commit eda26ce

7 files changed

Lines changed: 83 additions & 12 deletions
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
`security-owd-alias` no longer tells authors that `sharingModel: 'public'` is a retired ADR-0090 D4 alias. It never was one — no shipped schema ever accepted it — and the map that said so is now split along the two histories it was conflating.
6+
7+
`OWD_ALIAS_FIX` carried four keys under one comment, `[ADR-0090 D4] Legacy alias → canonical fix-it mapping`, while D4 names three: "The legacy aliases `read`, `read_write`, `full` are **removed from the zod enum**". Those three have a retirement behind them — the `owd-legacy-read-aliases` ADR-0087 stored-row conversion for the two `read*` spellings, the `13.owd-full-alias-removed` semantic entry for `full`. `public` has neither, and correctly so: a conversion rewrites a spelling some shipped schema once took, and this one never was taken, so its stored population is zero by construction. The missing conversion was the mislabel's shadow, not a gap.
8+
9+
- **Two maps, one union.** `OWD_RETIRED_ALIAS_FIX` holds the three D4 aliases; `OWD_WRONG_LAYER_FIX` holds `public`; `OWD_ALIAS_FIX` stays as their union, so every key still earns the same rule id, the same path and the same fix-it. No accept set moves and no value starts or stops being reported.
10+
- **The `public` fix-it is KEPT.** It catches a real authoring mistake: three neighbouring keys on the same `ObjectSchema` take `'public'` legally — `access.default` (`z.enum(['public', 'private'])`, ADR-0066) and `publicSharing.allowedAudiences` (`z.enum(['public', 'link_only', 'signed_in', 'email'])`) — and off-schema so does the sharing runtime's own internal vocabulary, `effectiveSharingModel(): 'private' | 'read' | 'public'`. `sharingModel` is the one neighbour that refuses it, and it fails CLOSED to `private` with no notice on the read path, so this fix-it is the author's only signal.
11+
- **The message says which group it is in.** One shared clause, used by both the `sharingModel` and the `externalSharingModel` branch. A retired alias still reads `is a retired alias (ADR-0090 D4)`; `public` now reads that it is not an OWD value and never was, and names the neighbouring keys that do take it. The fix-it text, severity, rule id and path are byte-identical either way.
12+
13+
Why the wording mattered enough to change: a diagnostic that credits `public` to D4 sends its reader looking for the conversion and the semantic entry that would exist if the acceptance had happened, and finding them absent reads as a data-fidelity defect in the conversion registry. It is not one.

packages/cli/test/serve-mcp-capability-collision.e2e.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ export default {
122122
objects: [{
123123
name: 'mcpcollision_task',
124124
label: 'Task',
125-
sharingModel: 'public',
125+
sharingModel: 'private',
126126
fields: { title: { type: 'text', label: 'Title' } },
127127
}],
128128
// NOTE: \`mcp\` is deliberately NOT declared. The banner advertises the MCP

packages/cli/test/serve-mcp-stdio-answers.e2e.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export default {
105105
objects: [{
106106
name: 'stdioprobe_task',
107107
label: 'Task',
108-
sharingModel: 'public',
108+
sharingModel: 'private',
109109
fields: { title: { type: 'text', label: 'Title' } },
110110
}],
111111
};

packages/cli/test/serve-node-env-production-default.e2e.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ export default {
270270
objects: [{
271271
name: 'nodeenvdefault_task',
272272
label: 'Task',
273-
sharingModel: 'public',
273+
sharingModel: 'private',
274274
fields: { title: { type: 'text', label: 'Title' } },
275275
}],
276276
// Constructed here, by the HOST — not left to os serve's own auto-inject.

packages/cli/test/serve-process-child-env.e2e.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ export default {
145145
objects: [{
146146
name: 'childenv_task',
147147
label: 'Task',
148-
sharingModel: 'public',
148+
sharingModel: 'private',
149149
fields: { title: { type: 'text', label: 'Title' } },
150150
}],
151151
};

packages/cli/test/serve-stdio-stdout-purity.e2e.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export default {
102102
objects: [{
103103
name: 'stdoutpurity_task',
104104
label: 'Task',
105-
sharingModel: 'public',
105+
sharingModel: 'private',
106106
fields: { title: { type: 'text', label: 'Title' } },
107107
}],
108108
};

packages/lint/src/validate-security-posture.ts

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,9 @@
7979
* ## Intake — which doors can reach `security-owd-alias` at all (#16109)
8080
*
8181
* `sharingModel` and `externalSharingModel` are CLOSED enums on `ObjectSchema`
82-
* (ADR-0090 D4 / D11): every value `OWD_ALIAS_FIX` names, and every
82+
* (ADR-0090 D4 / D11): every value `OWD_ALIAS_FIX` names — both the D4
83+
* aliases a shipped schema once accepted and the wrong-layer spellings it
84+
* never did, two histories declared as two maps below — and every other
8385
* non-canonical string, is refused by the schema with `invalid_value`. So on
8486
* any door that PARSES before the registry runs, this rule's alias branches
8587
* are unreachable by construction — the object never arrives. Measured on
@@ -108,8 +110,9 @@
108110
* Read the two alias branches below accordingly: they are NOT a second
109111
* opinion on the enum, and they are dead on the parsed doors on purpose. They
110112
* exist so the UNPARSED doors — `os lint` first, the docs gate second — name
111-
* the canonical replacement instead of letting a retired spelling ride to
112-
* `os build`, where the enum's generic `invalid_value` is the only message. A
113+
* the canonical replacement instead of letting a retired or wrong-layer
114+
* spelling ride to `os build`, where the enum's generic `invalid_value` is the
115+
* only message. A
113116
* consumer crediting this rule id as live `error` coverage on a
114117
* `defineStack`-authored app is crediting the wrong gate: on that door the
115118
* credit belongs to the schema's closed enum.
@@ -152,13 +155,45 @@ export interface SecurityFinding {
152155
type AnyRec = Record<string, unknown>;
153156

154157
const CANONICAL_OWD = ['private', 'public_read', 'public_read_write', 'controlled_by_parent'] as const;
155-
/** [ADR-0090 D4] Legacy alias → canonical fix-it mapping. */
156-
const OWD_ALIAS_FIX: Record<string, string> = {
158+
/**
159+
* [ADR-0090 D4] The legacy `sharingModel` spellings a shipped schema once
160+
* ACCEPTED, and the canonical value each becomes. D4 names exactly these
161+
* THREE — "The legacy aliases `read`, `read_write`, `full` are **removed from
162+
* the zod enum**" — and only these three have a retirement behind them: the
163+
* `owd-legacy-read-aliases` ADR-0087 stored-row conversion for the two `read*`
164+
* spellings, and the `13.owd-full-alias-removed` semantic entry for `full`.
165+
*/
166+
const OWD_RETIRED_ALIAS_FIX: Record<string, string> = {
157167
read: 'public_read',
158168
read_write: 'public_read_write',
159169
full: 'public_read_write',
170+
};
171+
172+
/**
173+
* Wrong-layer / misspelling fix-its: values NO shipped schema ever accepted
174+
* *here*. They are NOT retired aliases, and nothing sits behind them to
175+
* retire — no ADR-0087 conversion, no semantic-migration entry, and none is
176+
* possible, because a conversion rewrites a spelling some shipped schema once
177+
* took and the stored population for these is zero by construction (#16517
178+
* read the enum's whole lifetime over complete history; `public` appears in no
179+
* version of it).
180+
*
181+
* `public` is kept here rather than deleted because it catches a real
182+
* authoring mistake. THREE neighbouring keys on the same `ObjectSchema` take
183+
* `'public'` legally — `access.default` (`z.enum(['public', 'private'])`,
184+
* ADR-0066) and `publicSharing.allowedAudiences`
185+
* (`z.enum(['public', 'link_only', 'signed_in', 'email'])`) — and off-schema
186+
* so does the sharing runtime's own internal vocabulary,
187+
* `effectiveSharingModel(): 'private' | 'read' | 'public'`. `sharingModel` is
188+
* the one neighbour that refuses it, and it fails CLOSED to `private` with no
189+
* notice on the read path, so this fix-it is the author's only signal.
190+
*/
191+
const OWD_WRONG_LAYER_FIX: Record<string, string> = {
160192
public: 'public_read_write',
161193
};
194+
195+
/** Every value this rule offers a fix-it for — both provenance groups. */
196+
const OWD_ALIAS_FIX: Record<string, string> = { ...OWD_RETIRED_ALIAS_FIX, ...OWD_WRONG_LAYER_FIX };
162197
/** D11 ordering for external ≤ internal (controlled_by_parent excluded). */
163198
const OWD_WIDTH: Record<string, number> = {
164199
private: 0,
@@ -182,6 +217,29 @@ function owdOf(obj: AnyRec): unknown {
182217
return obj.sharingModel;
183218
}
184219

220+
/**
221+
* The provenance half of an alias finding's message.
222+
*
223+
* The two halves of `OWD_ALIAS_FIX` do not share a history, so one sentence
224+
* cannot serve both: calling `public` "a retired alias (ADR-0090 D4)" asserts
225+
* an acceptance that never happened, and sends the reader looking for the
226+
* conversion and the semantic entry that would exist if it had. The fix-it is
227+
* identical either way; only this clause differs.
228+
*
229+
* The sibling keys are named undotted on purpose: the receiver-coverage
230+
* meta-test in this rule's test file scans the module's CODE text for
231+
* `receiver.key` reads and cannot tell one inside a message string from a real
232+
* read, so a dotted spelling here would present as an undeclared read off a
233+
* receiver that does not exist.
234+
*/
235+
function owdAliasProvenance(value: string): string {
236+
return OWD_RETIRED_ALIAS_FIX[value]
237+
? `is a retired alias (ADR-0090 D4)`
238+
: `is not an OWD value and never was — ADR-0090 D4 retired 'read', 'read_write' and 'full', ` +
239+
`not this. '${value}' is legal on the neighbouring keys 'access' (its 'default') and ` +
240+
`'publicSharing' (its 'allowedAudiences'), just not on this one`;
241+
}
242+
185243
/**
186244
* A platform / system object: one the tenant did not author.
187245
*
@@ -394,7 +452,7 @@ export function validateSecurityPosture(stack: AnyRec, opts?: { nowMs?: number }
394452
where: `object "${objName}"`,
395453
path: `${objPath}.sharingModel`,
396454
message:
397-
`sharingModel '${owd}' is a retired alias (ADR-0090 D4). The runtime fails CLOSED ` +
455+
`sharingModel '${owd}' ${owdAliasProvenance(owd)}. The runtime fails CLOSED ` +
398456
`to 'private' on unknown values, so this object is NOT ${owd === 'read' ? 'readable' : 'writable'} org-wide.`,
399457
hint: `Replace with the canonical value: sharingModel: '${OWD_ALIAS_FIX[owd]}'.`,
400458
});
@@ -511,7 +569,7 @@ export function validateSecurityPosture(stack: AnyRec, opts?: { nowMs?: number }
511569
rule: SECURITY_OWD_ALIAS,
512570
where: `object "${objName}"`,
513571
path: `${objPath}.externalSharingModel`,
514-
message: `externalSharingModel '${external}' is a retired alias (ADR-0090 D4).`,
572+
message: `externalSharingModel '${external}' ${owdAliasProvenance(external)}.`,
515573
hint: `Replace with the canonical value: externalSharingModel: '${OWD_ALIAS_FIX[external]}'.`,
516574
});
517575
} else if (

0 commit comments

Comments
 (0)