From 8443f83e61db0a2ce81bd9cce34d66e498e32b7c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 13 Sep 2026 02:12:41 +0000 Subject: [PATCH 1/3] feat(lint): name retired permission-lifecycle residue at the authoring door MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ObjectPermissionSchema` accepts `allowRestore: false` / `allowPurge: false` as inert residue and strips them in silence (#12840, the retired-default residue tolerance — its ruling is not re-adjudicable and nothing here moves it). The silence is deliberate so that artifacts built by the published 17.x toolchain keep parsing, and `acceptRetiredDefaultResidue`'s own docblock names the channels that stay loud for authored sources: tsc `never`, `os migrate meta`, the ADR-0087 D2 conversion. Against a non-TypeScript author that list is one entry short. `tsc never` is a TypeScript channel; the conversion and `os migrate meta` are the same channel twice, and it is declared `retiredFromLoadPath`, so it never fires on the load path. An author writing the key in a JSON/YAML source and not running the migration gets a clean parse and no signal at all. Adds `validateRetiredPermissionResidue` — one advisory `warning` rule on the `normalized` tier, registered in `AUTHORING_RULES` so `os validate`, `os build` and `os lint` run it. It fires on the captured residue value and nothing else; every other value is already refused at the parse with the prescription attached. The hint is READ from the tombstone's own published description rather than retyped, so it cannot drift from the parse-time wording. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH --- packages/lint/src/authoring-rules.ts | 35 ++++ packages/lint/src/index.ts | 10 + ...alidate-retired-permission-residue.test.ts | 178 ++++++++++++++++++ .../validate-retired-permission-residue.ts | 149 +++++++++++++++ 4 files changed, 372 insertions(+) create mode 100644 packages/lint/src/validate-retired-permission-residue.test.ts create mode 100644 packages/lint/src/validate-retired-permission-residue.ts diff --git a/packages/lint/src/authoring-rules.ts b/packages/lint/src/authoring-rules.ts index b24aabbeb0..b128b88675 100644 --- a/packages/lint/src/authoring-rules.ts +++ b/packages/lint/src/authoring-rules.ts @@ -137,6 +137,7 @@ import { lintFlowPatterns } from './lint-flow-patterns.js'; import { lintLivenessProperties } from './lint-liveness-properties.js'; import { lintAutonumberFormats } from './lint-autonumber-formats.js'; import { lintViewRefs } from './lint-view-refs.js'; +import { validateRetiredPermissionResidue } from './validate-retired-permission-residue.js'; import { lintUniqueDeclarations, lintUnscopedDeclaredIndexes, @@ -1247,6 +1248,40 @@ export const AUTHORING_RULES: readonly AuthoringRule[] = [ hint: f.hint, })), }, + // [#17425, director ruling D] The author-time half of #12840's retired-default + // residue tolerance. The parse ACCEPTS `allowRestore: false` / `allowPurge: + // false` and strips them in silence — deliberately, so that built artifacts + // survive — which leaves a non-TypeScript author writing the key a clean pass + // and no signal at all. `input: 'normalized'` is load-bearing rather than + // conventional here: the evidence is a key the residue stage removes, so a + // `parsed` rule would read a stack that can never carry it. Measured: the + // ADR-0087 conversion that would otherwise strip it (`permission-allow- + // restore-purge-removed`) is `retiredFromLoadPath: true`, so it does not run + // inside `normalizeStackInput` and the key reaches this tier intact. + { + name: 'validateRetiredPermissionResidue', + tier: 'advisory', + input: 'normalized', + commands: ALL, + source: 'packages/lint/src/validate-retired-permission-residue.ts', + surfaces: CLI_ONLY, + surfaceReason: + 'Ruled scope (#17425 D): the signal belongs at the authoring door over RAW SOURCE, which is ' + + 'where the authored and the built path are distinguishable. Crossing it needs a measurement ' + + "this round did not take — whether the gate's `body` reaches it BEFORE the per-type " + + '`safeParse`, whose residue stage strips the only evidence this rule reads. Post-parse the ' + + 'rule is structurally silent, so wiring it there without that reading would publish a ' + + 'phantom check, not coverage.', + run: (stack) => + validateRetiredPermissionResidue(stack).map((f) => ({ + severity: f.severity, + rule: f.rule, + where: f.where, + path: f.path, + message: f.message, + hint: f.hint, + })), + }, // A format like `{plan_no}{000}` makes the referenced field part of the // counter scope, so it must exist and be set at create time. Unknown field → // broken (error); optional field → fragile (warning). diff --git a/packages/lint/src/index.ts b/packages/lint/src/index.ts index db55d05cba..443a1c316f 100644 --- a/packages/lint/src/index.ts +++ b/packages/lint/src/index.ts @@ -839,6 +839,16 @@ export { LIVENESS_LIVE_ELSEWHERE_PROPERTY, } from './lint-liveness-properties.js'; +export { validateRetiredPermissionResidue } from './validate-retired-permission-residue.js'; +export type { RetiredPermissionResidueFinding } from './validate-retired-permission-residue.js'; +export { + // [#17425] The one value #12840's residue stage accepts in silence, named at + // the authoring door. Published because `f.rule` is what `--json` consumers + // and `suppressWarnings` compare against — a rule id no barrel re-exports is + // unreachable (`rule-id-barrel-exports.test.ts`). + PERMISSION_RETIRED_LIFECYCLE_RESIDUE, +} from './validate-retired-permission-residue.js'; + export { lintAutonumberFormats } from './lint-autonumber-formats.js'; export type { AutonumberLintFinding } from './lint-autonumber-formats.js'; export { diff --git a/packages/lint/src/validate-retired-permission-residue.test.ts b/packages/lint/src/validate-retired-permission-residue.test.ts new file mode 100644 index 0000000000..fb2bbff2d3 --- /dev/null +++ b/packages/lint/src/validate-retired-permission-residue.test.ts @@ -0,0 +1,178 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// #17425, director ruling D. The rule's job is to be the ONE author-facing +// voice on a value the parse consumes in silence, so almost every assertion +// here is paired: a LIT control that must fire and a DARK control that must +// not. A rule of this shape fails in two directions and only one of them is +// noisy — a rule that never fires looks exactly like a clean codebase. + +import { describe, expect, it } from 'vitest'; +import { ObjectPermissionSchema } from '@objectstack/spec/security'; +import { normalizeStackInput } from '@objectstack/spec'; + +import { + PERMISSION_RETIRED_LIFECYCLE_RESIDUE, + retiredKeyPrescription, + validateRetiredPermissionResidue, +} from './validate-retired-permission-residue.js'; +import { AUTHORING_COMMANDS, authoringRulesFor, runAuthoringRules } from './authoring-rules.js'; + +type AnyRec = Record; + +/** The two keys and the single value each one's residue stage swallows. */ +const RETIRED: ReadonlyArray = [ + ['allowRestore', false], + ['allowPurge', false], +]; + +/** A raw authored stack, map-shaped, exactly as a non-TypeScript source spells it. */ +function rawStack(entry: AnyRec): AnyRec { + return { + permissions: { + support_agent: { + label: 'Support Agent', + objects: { crm_ticket: { allowRead: true, ...entry } }, + }, + }, + }; +} + +/** The `normalizeStackInput` output every authoring command hands a `normalized` rule. */ +function normalized(entry: AnyRec): AnyRec { + return normalizeStackInput(structuredClone(rawStack(entry)) as AnyRec) as AnyRec; +} + +describe('validateRetiredPermissionResidue (#17425)', () => { + describe('the premise the rule stands on', () => { + // Everything below is vacuous if the load path strips the key before a rule + // can see it — which is what the ADR-0087 conversion does when it is asked + // to. It is `retiredFromLoadPath`, so it is not asked to here. + it('LIT — the residue survives normalizeStackInput, which is the tier this rule reads', () => { + const perm = (normalized({ allowRestore: false, allowPurge: false }).permissions as AnyRec[])[0]; + const ticket = (perm.objects as AnyRec).crm_ticket as AnyRec; + expect(Object.prototype.hasOwnProperty.call(ticket, 'allowRestore')).toBe(true); + expect(Object.prototype.hasOwnProperty.call(ticket, 'allowPurge')).toBe(true); + }); + + it('DARK — the same key does NOT survive the parse, which is why a `parsed` rule could not do this', () => { + const parsed = ObjectPermissionSchema.safeParse({ allowRead: true, allowRestore: false }); + expect(parsed.success).toBe(true); + expect(Object.prototype.hasOwnProperty.call(parsed.data!, 'allowRestore')).toBe(false); + }); + + it.each(RETIRED)('the accept set for `%s` is exactly the value this rule fires on', (key, residue) => { + // The rule's private table has to agree with the schema's captured + // literal, and the schema does not export it — so the agreement is + // asserted through the behaviour the capture produces. + expect(ObjectPermissionSchema.safeParse({ allowRead: true, [key]: residue }).success).toBe(true); + for (const other of [true, 'false', 0, null, '']) { + const refused = ObjectPermissionSchema.safeParse({ allowRead: true, [key]: other }); + expect(refused.success, `${key}: ${JSON.stringify(other)} must stay refused`).toBe(false); + } + }); + }); + + describe('the finding', () => { + it('LIT — fires on the residue, once per key, naming the site', () => { + const findings = validateRetiredPermissionResidue(normalized({ allowRestore: false, allowPurge: false })); + expect(findings.map((f) => f.rule)).toEqual([ + PERMISSION_RETIRED_LIFECYCLE_RESIDUE, + PERMISSION_RETIRED_LIFECYCLE_RESIDUE, + ]); + expect(findings.map((f) => f.path)).toEqual([ + 'permissions[0].objects.crm_ticket.allowRestore', + 'permissions[0].objects.crm_ticket.allowPurge', + ]); + expect(findings[0].where).toBe("permission set 'support_agent' · object 'crm_ticket'"); + expect(findings.every((f) => f.severity === 'warning')).toBe(true); + }); + + it('carries the retirement’s OWN prescription, not a second wording', () => { + const [finding] = validateRetiredPermissionResidue(normalized({ allowRestore: false })); + const fromSchema = String( + (ObjectPermissionSchema as unknown as { shape: Record }) + .shape.allowRestore.description, + ).replace(/^\[REMOVED\]\s*/, ''); + expect(finding.hint).toBe(fromSchema); + // Anti-vacuity: an empty derivation would make the assertion above true + // and the rule silent. The prescription's two load-bearing clauses. + expect(finding.hint).toContain('Delete the key'); + expect(finding.hint).toContain('os migrate meta --from 17'); + }); + + it('the prescription resolves for every key the rule knows about', () => { + for (const [key] of RETIRED) { + expect(retiredKeyPrescription(key), `no prescription resolved for ${key}`).not.toBeNull(); + } + // The resolver is not a constant function: a key with no tombstone has none. + expect(retiredKeyPrescription('allowTransfer')).not.toContain('was removed'); + expect(retiredKeyPrescription('allowTeleport')).toBeNull(); + }); + }); + + describe('DARK controls — what must stay silent', () => { + it('a clean permission set earns nothing', () => { + expect(validateRetiredPermissionResidue(normalized({}))).toEqual([]); + }); + + it('COST DIRECTION — a live lifecycle bit set falsy is NOT residue', () => { + // `allowTransfer` is the surviving lifecycle key (#3004, enforced). It is + // the nearest miss in the shape: same family, same object, same `false`. + // Flagging it would tell an author to delete an enforced grant. + expect(validateRetiredPermissionResidue(normalized({ allowTransfer: false }))).toEqual([]); + expect(validateRetiredPermissionResidue(normalized({ allowCreate: false, allowDelete: false }))).toEqual([]); + }); + + it('a non-residue VALUE is the tombstone’s business, not this rule’s', () => { + // Each of these is refused at the parse with the prescription attached. + for (const other of [true, 'false', 0, null]) { + expect( + validateRetiredPermissionResidue(normalized({ allowRestore: other })), + `${JSON.stringify(other)} must not be double-reported`, + ).toEqual([]); + } + }); + + it('a fabricated key earns nothing', () => { + expect(validateRetiredPermissionResidue(normalized({ allowTeleport: false }))).toEqual([]); + }); + + it('never throws on malformed input, and reports nothing about it', () => { + for (const junk of [{}, { permissions: null }, { permissions: [null, 7] }, { permissions: [{ objects: 3 }] }, + { permissions: [{ objects: { a: null } }] }]) { + expect(validateRetiredPermissionResidue(junk as AnyRec)).toEqual([]); + } + }); + }); + + describe('wiring — the rule really runs, on every command', () => { + it.each([...AUTHORING_COMMANDS])('os %s runs it', (command) => { + expect(authoringRulesFor(command).map((r) => r.name)).toContain('validateRetiredPermissionResidue'); + }); + + it('LIT — reaches an author through the registry runner on all three commands', () => { + for (const command of AUTHORING_COMMANDS) { + const findings = runAuthoringRules(command, { + normalized: normalized({ allowRestore: false }), + // The parsed tier CANNOT carry the evidence; handing it over proves + // the entry reads `normalized` rather than falling back. + parsed: normalized({}), + }).filter((f) => f.rule === PERMISSION_RETIRED_LIFECYCLE_RESIDUE); + expect(findings.map((f) => f.path), `os ${command}`).toEqual([ + 'permissions[0].objects.crm_ticket.allowRestore', + ]); + expect(findings[0].severity).toBe('warning'); + } + }); + + it('DARK — the same runner is silent on a clean stack', () => { + for (const command of AUTHORING_COMMANDS) { + expect( + runAuthoringRules(command, { normalized: normalized({}) }) + .filter((f) => f.rule === PERMISSION_RETIRED_LIFECYCLE_RESIDUE), + `os ${command}`, + ).toEqual([]); + } + }); + }); +}); diff --git a/packages/lint/src/validate-retired-permission-residue.ts b/packages/lint/src/validate-retired-permission-residue.ts new file mode 100644 index 0000000000..7e87df2ffa --- /dev/null +++ b/packages/lint/src/validate-retired-permission-residue.ts @@ -0,0 +1,149 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Author-time signal for the ONE value the retired `allowRestore` / + * `allowPurge` tombstones accept in silence (#17425, director ruling D). + * + * ## The gap this closes, and the gap it deliberately does NOT close + * + * `ObjectPermissionSchema` wraps its closed shape in + * `acceptRetiredDefaultResidue(...)` — the class helper of #12840, at the exact + * keys it was written for. That ruling is marked NOT re-adjudicable and nothing + * here touches it: the parse keeps accepting the retired default and keeps + * refusing every other value. The helper's own docblock states why the accept + * is silent, and in the same sentence names the channels that stay loud: + * + * > the strip is deliberately SILENT — real artifacts carry the residue once + * > per permission entry, and a per-occurrence notice would be a 75-line storm + * > that teaches operators to skim; the loud channels for authored sources + * > (tsc `never`, `os migrate meta`, the D2 conversion) are unchanged. + * + * Read that list against a JSON/YAML author and the gap is exactly one entry + * wide. `tsc never` is a TypeScript channel — an author using `definePermissionSet` + * cannot write the key at all. `os migrate meta` and the ADR-0087 D2 conversion + * are the same channel twice: `permission-allow-restore-purge-removed` is + * declared `retiredFromLoadPath: true`, so it fires only when someone RUNS the + * migration, never on the load path (measured: `normalizeStackInput` on a raw + * stack carrying `allowRestore: false` emits zero conversion notices and hands + * the key straight through). So an author who writes the key in a non-TypeScript + * source and never runs `os migrate meta` gets a clean parse and no signal — the + * thing a tombstone exists to prevent, which is the whole of #17425's complaint. + * + * This rule is that missing channel, sited where the two paths ARE + * distinguishable: BEFORE the parse (`input: 'normalized'`), on the raw authored + * stack, where the residue is still present and still attributable to a line + * somebody wrote. It says nothing about built artifacts, because a built + * artifact never reaches an authoring command. + * + * ## Why only the residue VALUE + * + * The rule fires on the captured residue literal and on nothing else. Every + * other value — `true`, `'false'`, `0`, `null` — already lands on the + * tombstone's own refusal with the prescription attached, at the key's own path; + * repeating it here would be a second voice saying the same thing one layer + * earlier. The residue value is precisely the one the parse consumes without a + * word, so it is precisely the one an author-time rule is needed for. + * + * ## Why the prescription is READ, not retyped + * + * `retiredKey()` publishes its guidance as the key's own `description` + * (`[REMOVED] `), and that string is the retirement's single wording — + * pinned class-wide by `retired-key-migrate-sentence.test.ts` over in + * `packages/spec`. A copy here would be a second wording free to drift from the + * parse-time one the same author sees through the other door, so the hint is + * resolved from `ObjectPermissionSchema`'s own shape at call time. An + * unresolvable prescription yields NO finding rather than a hint this module + * invented — the same posture `lintLivenessProperties` takes to an unreadable + * ledger, and the reason this module's test carries an anti-vacuity guard. + */ + +import { ObjectPermissionSchema } from '@objectstack/spec/security'; +import { recordsOf } from './object-graph.js'; + +type AnyRec = Record; + +export interface RetiredPermissionResidueFinding { + where: string; + /** Positional config path, e.g. `permissions[0].objects.crm_ticket.allowRestore`. */ + path: string; + message: string; + hint: string; + rule: string; + severity: 'warning'; +} + +export const PERMISSION_RETIRED_LIFECYCLE_RESIDUE = 'permission-retired-lifecycle-residue'; + +/** + * The retired object-permission keys and the ONE value each one's residue stage + * swallows — the same discrimination `OBJECT_PERMISSION_RETIRED_KEY_RESIDUE` + * makes, by identity against the literal captured at retirement time. + * + * ⛔ Not derived from anything live, for the reason `RetiredDefaultResidue` + * states: the default no longer exists in the schema, so a literal written down + * at retirement is the only trustworthy record of what the released toolchain + * materialized. This table is the authoring-door copy of that same capture, and + * `validate-retired-permission-residue.test.ts` holds it equal to the spec's. + */ +const RETIRED_LIFECYCLE_RESIDUE: ReadonlyArray = [ + ['allowRestore', false], + ['allowPurge', false], +]; + +/** Strip the `[REMOVED] ` marker `retiredKey()` prefixes onto its guidance. */ +const REMOVED_MARKER = /^\[REMOVED\]\s*/; + +/** + * The retirement's own prescription for `key`, read from the tombstone's + * published description. `null` when the shape or the description cannot be + * resolved — see the module docblock for why that is silence rather than a + * substitute wording. + */ +export function retiredKeyPrescription(key: string): string | null { + const shape = (ObjectPermissionSchema as unknown as { shape?: Record }).shape; + const described = shape?.[key]?.description; + if (typeof described !== 'string' || described.length === 0) return null; + const prescription = described.replace(REMOVED_MARKER, '').trim(); + return prescription.length > 0 ? prescription : null; +} + +/** + * Flag every authored object-permission entry carrying a retired lifecycle + * key at its inert residue value. Advisory only — returns findings, never + * throws, never emits `error`. + */ +export function validateRetiredPermissionResidue(stack: AnyRec): RetiredPermissionResidueFinding[] { + const findings: RetiredPermissionResidueFinding[] = []; + const sets = recordsOf(stack.permissions); + for (let i = 0; i < sets.length; i++) { + const set = sets[i]; + const objects = set.objects; + if (!objects || typeof objects !== 'object' || Array.isArray(objects)) continue; + const setName = typeof set.name === 'string' ? set.name : '(unnamed permission set)'; + for (const [objectName, perm] of Object.entries(objects as AnyRec)) { + if (!perm || typeof perm !== 'object' || Array.isArray(perm)) continue; + const entry = perm as AnyRec; + for (const [key, residue] of RETIRED_LIFECYCLE_RESIDUE) { + if (!Object.prototype.hasOwnProperty.call(entry, key)) continue; + // Identity against the captured literal, exactly as the residue stage + // compares. `0`/`''`/`null` are NOT this value and are refused at the + // parse with the prescription already attached. + if (entry[key] !== residue) continue; + const hint = retiredKeyPrescription(key); + if (hint === null) continue; + findings.push({ + rule: PERMISSION_RETIRED_LIFECYCLE_RESIDUE, + severity: 'warning', + where: `permission set '${setName}' · object '${objectName}'`, + path: `permissions[${i}].objects.${objectName}.${key}`, + message: + `sets \`${key}: ${JSON.stringify(residue)}\`, the retired default of a key removed in ` + + '@objectstack/spec 17 (ADR-0049). It is accepted as inert residue and silently stripped ' + + 'on parse, so it grants nothing and nothing reports it — this line has no effect.', + hint, + }); + } + } + } + return findings; +} From 1e805dac82652fc320f41a1207b494acdc770906 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 13 Sep 2026 02:23:44 +0000 Subject: [PATCH 2/3] chore(changeset): @objectstack/lint minor for the retired-residue authoring rule Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH --- .../17425-retired-permission-residue-lint.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .changeset/17425-retired-permission-residue-lint.md diff --git a/.changeset/17425-retired-permission-residue-lint.md b/.changeset/17425-retired-permission-residue-lint.md new file mode 100644 index 0000000000..3853225439 --- /dev/null +++ b/.changeset/17425-retired-permission-residue-lint.md @@ -0,0 +1,15 @@ +--- +"@objectstack/lint": minor +--- + +feat(lint): `permission-retired-lifecycle-residue` — the retired `allowRestore` / `allowPurge` bits are now named at the authoring door (#17425) + +`ObjectPermissionSchema` accepts `allowRestore: false` / `allowPurge: false` as inert residue and strips them silently. That tolerance is #12840's class ruling and is unchanged here: the accept set does not move, no schema is touched, and every other value keeps the tombstone's loud refusal. + +The silence is deliberate — every artifact the published 17.x toolchain built has the retired default materialized in every permission entry, and a per-occurrence notice would be a storm. But `acceptRetiredDefaultResidue`'s own docblock names the channels that stay loud for authored sources — tsc `never`, `os migrate meta`, the ADR-0087 D2 conversion — and against a non-TypeScript author that list is one entry short. `tsc never` is a TypeScript channel. The conversion and `os migrate meta` are the same channel twice, and `permission-allow-restore-purge-removed` is declared `retiredFromLoadPath`, so it never fires while a stack loads. An author who writes the key in a JSON or YAML source and does not run the migration gets a clean parse and no signal at all — which is what a tombstone exists to prevent. + +`os validate`, `os build` and `os lint` now emit one advisory `warning` per carrying entry, on the raw pre-parse stack where the key is still present and still attributable to a line somebody wrote. The hint is the retirement's own prescription, read from the tombstone's published description rather than retyped, so it cannot drift from the parse-time wording the same author sees through the other door. + +It fires on the captured residue value and on nothing else: `true`, `"false"`, `0` and `null` are already refused at the parse with the prescription attached, and the surviving enforced lifecycle bit `allowTransfer: false` is not residue and is never named. + +New published exports on `@objectstack/lint`: `validateRetiredPermissionResidue`, `PERMISSION_RETIRED_LIFECYCLE_RESIDUE` and the `RetiredPermissionResidueFinding` type. Nothing is removed and no existing finding changes shape or severity. From d7d22bf4bebc4f0065b932556b07a9330d7822b2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 13 Sep 2026 02:41:37 +0000 Subject: [PATCH 3/3] fix(lint): strip the tracker id from the rule's runtime string; refresh four CLI transcripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `check:doc-authoring` refuses an internal issue id inside customer-facing string prose — a runtime string reaches authors and generated surfaces, none of whom can resolve it. The reference moves to the adjacent comment, where the reader who can resolve it already looks. `check:docs-transcript-drift` derives the author-time rule count from `AUTHORING_RULES` and compares it against the transcripts the docs quote. The new entry moves it 44 -> 45, so the four pages that print it are refreshed. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH --- content/docs/deployment/cli.mdx | 2 +- content/docs/deployment/validating-metadata.mdx | 2 +- content/docs/getting-started/build-with-claude-code.mdx | 2 +- content/docs/ui/react-pages.mdx | 2 +- packages/lint/src/authoring-rules.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/content/docs/deployment/cli.mdx b/content/docs/deployment/cli.mdx index ffbe20acbc..9a34d83634 100644 --- a/content/docs/deployment/cli.mdx +++ b/content/docs/deployment/cli.mdx @@ -589,7 +589,7 @@ os compile --json # JSON output for CI pipelines → Normalizing stack definition... → Lowering inline handlers... → Validating protocol compliance... - → Running author-time rules (44)... + → Running author-time rules (45)... → Checking capability providers (#3366)... → Collecting package docs (ADR-0046)... → Writing artifact... diff --git a/content/docs/deployment/validating-metadata.mdx b/content/docs/deployment/validating-metadata.mdx index c0d9a0dbce..8a18b827c9 100644 --- a/content/docs/deployment/validating-metadata.mdx +++ b/content/docs/deployment/validating-metadata.mdx @@ -555,7 +555,7 @@ A clean run walks the registry and reports timing: Config: /path/to/support-desk/objectstack.config.ts Load time: 21ms → Validating against ObjectStack Protocol... - → Running author-time rules (44)... + → Running author-time rules (45)... → Checking capability providers (#3366)... → Checking package docs (ADR-0046)... diff --git a/content/docs/getting-started/build-with-claude-code.mdx b/content/docs/getting-started/build-with-claude-code.mdx index c3b0855efd..ca967711f9 100644 --- a/content/docs/getting-started/build-with-claude-code.mdx +++ b/content/docs/getting-started/build-with-claude-code.mdx @@ -265,7 +265,7 @@ visible: 'status != "resolved"' ◆ Validate ──────────────────────────────────────── → Validating against ObjectStack Protocol... - → Running author-time rules (44)... + → Running author-time rules (45)... ✗ Author-time rules failed (1 issue) • stack · action 'resolve_ticket' visible: bare reference `status` — a diff --git a/content/docs/ui/react-pages.mdx b/content/docs/ui/react-pages.mdx index 762fb48f88..392d5c5d11 100644 --- a/content/docs/ui/react-pages.mdx +++ b/content/docs/ui/react-pages.mdx @@ -381,7 +381,7 @@ objectstack validate ──────────────────────────────────────── → Loading configuration... → Validating against ObjectStack Protocol... - → Running author-time rules (44)... + → Running author-time rules (45)... → Checking capability providers (#3366)... → Checking package docs (ADR-0046)... diff --git a/packages/lint/src/authoring-rules.ts b/packages/lint/src/authoring-rules.ts index b128b88675..83fe7775c1 100644 --- a/packages/lint/src/authoring-rules.ts +++ b/packages/lint/src/authoring-rules.ts @@ -1266,7 +1266,7 @@ export const AUTHORING_RULES: readonly AuthoringRule[] = [ source: 'packages/lint/src/validate-retired-permission-residue.ts', surfaces: CLI_ONLY, surfaceReason: - 'Ruled scope (#17425 D): the signal belongs at the authoring door over RAW SOURCE, which is ' + + 'Ruled scope: the signal belongs at the authoring door over RAW SOURCE, which is ' + 'where the authored and the built path are distinguishable. Crossing it needs a measurement ' + "this round did not take — whether the gate's `body` reaches it BEFORE the per-type " + '`safeParse`, whose residue stage strips the only evidence this rule reads. Post-parse the ' +