Skip to content

Implement ObjectStack protocol specification with Zod schemas and TypeScript interfaces - #3

Merged
huangyiirene merged 4 commits into
mainfrom
copilot/set-up-copilot-instructions
Jan 18, 2026
Merged

huangyiirene merged 4 commits into
mainfrom
copilot/set-up-copilot-instructions

Conversation

Copilot AI commented Jan 18, 2026 •

Copy link
Copy Markdown
Contributor

Establishes the foundational type system and conventions for the ObjectStack ecosystem. This defines the "Constitution" - shared interfaces, validation schemas, and directory conventions used by ObjectOS, ObjectStudio, ObjectCloud, and third-party plugins.

Core Components

  • Manifest Schema (src/schemas/manifest.zod.ts)

    • Zod-first schema with type inference for package configuration
    • Defines package metadata (id, type, version), permissions, menu structure, entity patterns, and extension points
    • Validates app/plugin/driver/module packages
  • Plugin Runtime Interface (src/types/plugin.ts)

    • Lifecycle contract: onInstall, onEnable, onDisable
    • PluginContext provides ql (ObjectQLClient), os (ObjectOSKernel), and logger
    • Defines ObjectQL query/mutation interface and kernel event bus
  • Directory Conventions (src/constants/paths.ts)

    • Hardcoded paths: src/schemas, src/triggers, src/client/pages, assets
    • Standard files: objectstack.config.ts, src/index.ts
    • Type-safe path constants for runtime and tooling

Usage

import { ManifestSchema, ObjectStackPlugin, PKG_CONVENTIONS } from '@objectstack/spec';

// Validate package manifest
const manifest = ManifestSchema.parse({
  id: 'com.example.crm',
  version: '1.0.0',
  type: 'plugin',
  permissions: ['system.user.read']
});

// Implement plugin
export default function(): ObjectStackPlugin {
  return {
    async onInstall(ctx) { /* setup */ },
    async onEnable(ctx) { /* start */ },
    async onDisable(ctx) { /* cleanup */ }
  };
}

All types include comprehensive TSDoc for IntelliSense. No runtime dependencies except Zod. Universal compatibility (Node.js/Browser/Electron).

Original prompt

This section details on the original issue you should resolve

<issue_title>✨ Set up Copilot instructions</issue_title>
<issue_description>📜 ObjectStack Protocol & Specification Context
Role: You are the Chief Architect and Standards Committee for the ObjectStack Ecosystem.
Mission: Define the "Constitution" of the system. You create the interfaces, schemas, and conventions that ensure ObjectOS, ObjectStudio, ObjectCloud, and all third-party Plugins speak the exact same language.
Guiding Principle: "Strict Types, No Logic."
This repository contains NO database connections, NO UI components, and NO runtime business logic. It contains only:

  • TypeScript Interfaces (Shared types).
  • JSON Schemas / Zod Schemas (Validation rules).
  • Constants (Convention configurations).
  1. The "Manifest" Standard (Core Responsibility)
    You define what a "Package" looks like in ObjectStack.
  • Schema Location: src/schemas/manifest.zod.ts (Export to JSON Schema).
  • Key Definition: The ObjectStackManifest interface.
    • id: Unique identifier (e.g., com.example.crm).
    • type: app | plugin | driver | module.
    • permissions: Array of permission strings requested (e.g., ["system.user.read"]).
    • menus: Navigation structure injection.
    • entities: Glob patterns for ObjectQL files (e.g., ["./src/schema/*.gql"]).
    • extensions: Extension points (e.g., contributions to the UI).
  1. Directory Conventions (Law of Location)
    You define "Where things must be". Hardcode these paths so CLI and Runtime match perfectly.
  • File: src/constants/paths.ts
  • Rules:
    • Schemas MUST be in src/schemas.
    • Server triggers MUST be in src/triggers.
    • Client pages MUST be in src/client/pages.
    • Assets MUST be in assets.
  1. Runtime Interfaces (The Contract)
    You define the interface that every plugin must implement to be loaded by ObjectOS.
  • File: src/types/plugin.ts
  • Interface: ObjectStackPlugin
    • onInstall(ctx: PluginContext): Promise
    • onEnable(ctx: PluginContext): Promise
    • onDisable(ctx: PluginContext): Promise
  • Context: PluginContext
    • Must expose ql (ObjectQLClient), os (ObjectOSKernel), logger.
  1. Coding Rules for AI
    A. Zod First Strategy
    When defining schemas (like the Manifest), ALWAYS use Zod first.
  • Why: Zod allows us to infer the TypeScript type (z.infer) AND generate the JSON Schema for the VS Code extension/CLI validator from a single source of truth.
    B. Universal Compatibility
  • The code generated here must run in Node.js (CLI/OS), Browser (Studio/UI), and Electron.
  • Do not import Node.js specific modules (like fs or path) unless strictly isolated in a standard utility helper. Ideally, keep it pure JS/TS.
    C. Documentation is Code
    Since this is the protocol, every interface property must have TSDoc comments (/** ... */). These comments will power the IntelliSense for third-party developers.
  1. Mock Examples (Reference)
    Example: Defining the Manifest Schema (Zod)
    import { z } from 'zod';

export const ManifestSchema = z.object({
id: z.string().describe("Unique package identifier (reverse domain style)"),
version: z.string().regex(/^\d+.\d+.\d+$/),
type: z.enum(['app', 'plugin', 'driver']),
menus: z.array(z.object({
label: z.string(),
path: z.string(),
icon: z.string().optional()
})).optional()
});

export type ObjectStackManifest = z.infer;

Example: Defining Directory Constants
export const PKG_CONVENTIONS = {
// The Source of Truth for where the Engine looks for files
DIRS: {
SCHEMA: 'src/schemas',
SERVER: 'src/server',
CLIENT: 'src/client'
},
FILES: {
MANIFEST: 'objectstack.config.ts',
ENTRY: 'src/index.ts'
}
} as const;</issue_description>

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 2 commits January 18, 2026 08:52
Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
Co-authored-by: huangyiirene <7665279+huangyiirene@users.noreply.github.com>
Copilot AI changed the title [WIP] Set up Copilot instructions for ObjectStack Implement ObjectStack protocol specification with Zod schemas and TypeScript interfaces Jan 18, 2026
Copilot AI requested a review from huangyiirene January 18, 2026 08:57
@huangyiirene
huangyiirene marked this pull request as ready for review January 18, 2026 09:20
@huangyiirene
huangyiirene merged commit a116aef into main Jan 18, 2026
1 check failed
Copilot AI added a commit that referenced this pull request Jan 25, 2026
- Added Chinese meta files for all website protocol sections
- Updated references meta files to include website protocol
- Updated README to document the 6 core protocol modules
- Added Website Protocol as module #3 with preview release date March 2026

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
xuyushun441-sys pushed a commit that referenced this pull request May 22, 2026
Introduces an opt-in path in ObjectStackProtocolImplementation.saveMetaItem
that writes overlay metadata through SysMetadataRepository.put instead of
the raw engine, so writes append to the change-log and emit HMR seq events.

Behavioural changes (all behind options.useRepositoryWritePath /
OBJECTSTACK_USE_REPOSITORY_WRITE_PATH=1):
- saveMetaItem request gained optional parentVersion (If-Match) and
  actor fields. ConflictError -> 409 metadata_conflict.
- Plural type aliases (views, dashboards, ...) normalized to singular
  before the repo's overlay-allowlist gate (rubber-duck #5).
- Object-registry mutation moved AFTER successful put() so a conflict
  does not leave the in-memory registry stale (rubber-duck #3 invariant
  test added).

Repo/test-fake fixes uncovered by rubber-duck review:
- SysMetadataRepository.put/delete now update/delete by row id because
  the engine's strict .update requires id or multi:true (rubber-duck #1).
- sys_metadata.checksum column widened from 64 -> 71 chars to hold the
  sha256: prefix produced by hashSpec() (rubber-duck #2).
- Three test fake engines extended to support both overlay-tuple and
  id-based where lookups.

333/333 objectql tests pass.

Deferred to PR-10d.4: REST plumbing for parentVersion/actor
(rubber-duck #6), race-window retry for omitted parentVersion
(rubber-duck #4), default flag flip + legacy path removal.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
xuyushun441-sys pushed a commit that referenced this pull request May 23, 2026
…s (PR #3)

Resolves the gap left by PR #2: cache + storage adapters accepted an
optional MetricsRegistry but their respective Plugin classes never
forwarded one, so any host that registered observability via the
dispatcher saw zero cache/storage data.

Adds:

  packages/observability/src/service-names.ts
    OBSERVABILITY_METRICS_SERVICE = 'observability:metrics'
    OBSERVABILITY_ERRORS_SERVICE  = 'observability:errors'

  packages/runtime/src/observability/observability-service-plugin.ts
    ObservabilityServicePlugin — registers the host's MetricsRegistry
    and ErrorReporter under the canonical names. Defaults each to its
    respective Noop exporter so the services are always present.
    Also exports resolveMetrics() / resolveErrorReporter() helpers for
    consumers inside the runtime package.

CacheServicePlugin + StorageServicePlugin:
  - new `metrics?: MetricsRegistry` option (escape hatch for tests)
  - canonical resolution chain at init():
      option override → observability:metrics service → NoopMetricsRegistry
  - StorageServicePlugin's `buildAdapterFromValues` (the settings
    live-rebuild path) now also threads metrics into the freshly built
    adapter, so adapter swaps don't drop instrumentation
  - log line now reports the resolved registry class name for diagnostics

Helpers (resolveMetrics) are inlined as private functions in each
service to avoid a circular dep (services must not depend on runtime).
Constants live in @objectstack/observability which both services
already depend on.

New tests:
  - cache-service-plugin.metrics.test.ts (4 tests, resolution chain + override precedence)
  - storage-service-plugin.metrics.test.ts (4 tests, incl. settings-rebuild path)
  - observability-service-plugin.test.ts  (3 tests, registration + defaults)

All 22 service-cache + 48 service-storage + 282 runtime tests pass
(2 pre-existing i18n failures in app-plugin.test.ts on main unchanged).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
xuyushun441-sys pushed a commit that referenced this pull request May 29, 2026
…s/skills

Persist package enable/disable (#2):
- SchemaRegistry.setInitialDisabledPackageIds() seeds a disabled-id set that
  installPackage honors, so every registration path (boot artifact, marketplace
  rehydrate, local import) applies persisted disable uniformly — no fragile
  post-boot reapply hook.
- New runtime/package-state-store.ts persists the disabled set to
  <OS_HOME>/package-state/<environmentId>.json, keyed per environment.
- AppPlugin.init seeds the registry before the manifest is decomposed.
- handlePackages enable/disable persist the new state.

Round-trip tools/skills on export & import (#3):
- PLURAL_TO_SINGULAR (spec) gains tools->tool, skills->skill (drives export
  via assemblePackageManifest and the auto-derived reverse map).
- engine.registerApp metadataArrayKeys consume tools/skills on import.
- metadata ARTIFACT_FIELD_TO_TYPE gains tools->tool (skills already present).
- ObjectStackDefinition gains a top-level tools field beside agents/skills.
Covers metadata round-trip/visibility; executable ToolRegistry wiring is out
of scope.

Docs: ADR-0016 §§9.5–9.8 updated (disable now persists; tools/skills round-trip).

Verified live: disable -> restart -> stays disabled & app hidden; enable
clears state & app returns. registerApp materializes tools/skills.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
xuyushun441-sys added a commit that referenced this pull request Jun 1, 2026
* feat(spec): structured plugin manifest schema (ADR-0025 F1)

Extend ManifestSchema with the authoritative plugin-distribution shapes
so the cloud control plane can drop its stopgap mirror and import the
canonical schemas.

- PluginPermissionsSchema: structured { services, hooks, network, fs }
  (.strict()); ADR-0025 §3.2
- PluginEnginesSchema: { platform, protocol } (protocol-first, §3.10 #3)
- PluginRuntimeSchema: node | sandbox | worker (trust tier, §3.6)
- PluginPackagingSchema: bundled | manifest-deps (§3.3)
- PluginIntegritySchema: Record<path, digest> (§3.2)

ManifestSchema.permissions becomes a backward-compatible union of the
legacy string[] and the structured block; new optional runtime /
packaging / integrity / engines fields added. Legacy engine:{objectstack}
retained and superseded by engines.

Shapes match cloud's stopgap (service-cloud/src/plugin-artifact.ts) so
cloud's swap is a one-line import from @objectstack/spec/kernel.

Verified: tsc clean, 6609 spec tests pass, exports surface in
dist/kernel, runtime parse smoke (legacy + structured + strict reject).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(cli): `os plugin build` + .osplugin packaging (ADR-0025 F2)

Add the build half of the plugin distribution pipeline (ADR-0025 §3.4):

- src/utils/osplugin.ts — dependency-free packaging primitives:
  - sriDigest(): canonical per-file integrity string `sha256-<base64>`
    (matches ADR §3.2's example; the format cloud/runtime align to).
  - computeIntegrity(): builds the manifest `integrity` map (excludes the
    manifest itself + SIGNATURE; deterministic key order).
  - createTar()/createTarGz(): reproducible ustar+gzip writer (mtime pinned
    to 0, sorted entries) so any tar reader can unpack the artifact and
    identical inputs yield byte-identical blobs.

- src/commands/plugin/build.ts — `os plugin build`:
  1. validate objectstack.plugin.json against the canonical ManifestSchema
     (@objectstack/spec/kernel — F1), failing fast with zod diagnostics;
  2. esbuild-bundle the entry to dist/index.mjs, externalizing
     @objectstack/* (and declared deps for packaging: manifest-deps);
  3. compute per-file integrity + emit the compiled manifest;
  4. pack dist/ (+assets, +package.json/lockfile for manifest-deps,
     +SIGNATURE placeholder) into <id>-<version>.osplugin.

Signing is a separate step; this emits an unsigned artifact.

Tests (6, all green; full CLI suite 143 green): SRI vector, integrity
exclusion+ordering, ustar round-trip with valid checksums, gzip validity,
reproducibility, and an end-to-end build that bundles a fixture plugin and
reads the .osplugin back — exercising F1's schema through the CLI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(core,cli): Ed25519 plugin signature contract + `os plugin sign` (ADR-0025 F3)

Land the canonical signature half of the plugin distribution pipeline,
byte-for-byte aligned with the cloud control plane's package-signing so
the two never drift.

core/src/security/plugin-artifact-signature.ts — the shared Ed25519
detached-signature contract:
  - format `ed25519:<keyId>:<base64url>`; sign/verify via node:crypto
    (`sign(null,…)`/`verify(null,…)`), keyId as the rotation handle.
  - verifyPublisherSignature(): publisher sig over raw artifact bytes,
    keyId-resolved key, mirroring cloud's publish-time policy
    (no sig → unverified-but-ok; malformed/unknown-key/mismatch → not ok).
  - counterSignPayload()/verifyPlatformSignature(): platform counter-sign
    over [package_id, version, blob_key, signature].join("\n") — identical
    to cloud's payload.
  - verifyPluginArtifact(): runs both trust chains at load time
    (ADR §3.7); requirePlatform=false for first-party/local builds.
  Exported from @objectstack/core/security.

cli plugin/sign.ts — `os plugin sign <artifact> --key <pem> [--key-id]`:
  detached publisher signature over the EXACT artifact bytes (the bytes
  cloud verifies at publish), written to a `<artifact>.sig` sidecar, with
  a self-verify guard. Completes build → sign → publish.

plugin-loader.ts: replace the placeholder verifyPluginSignature with an
honest check — artifact-bytes/counter-sign verification belongs at
materialize time (no artifact bytes exist at loadPlugin()), so the loader
now validates signature well-formedness via parseSignature and fails fast
on a malformed value, pointing at verifyPluginArtifact for the real chains.

Verified: core 269 tests (incl. 14 signature: format, determinism, tamper,
cloud-contract alignment, publisher policy, counter-sign, combined chains,
KeyObject), cli 138 (incl. build→sign→verify e2e against the exact bytes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(core): enforce install-time granted_permissions (ADR-0025 F4)

Bridge the cloud control plane's persisted consent into runtime
enforcement. `PluginPermissionEnforcer.registerGrantedPermissions()` and
`buildPermissionsFromGrants()` turn the structured grant set cloud writes
to `sys_package_installation.granted_permissions`
(`{ services, hooks, network, fs }`, ADR §3.2) into the runtime
`PluginPermissions` bag that `SecurePluginContext` checks.

Matching: exact value, glob (`*` / `**`), or wildcard `*`; network grants
match the request URL's host; `fs` governs read and write; a null/empty
grant set denies everything (least privilege). This enforces what was
GRANTED at install, not merely what the manifest declared — the right
default for distributed third-party plugins.

Verified: 6 new tests (service/hook/fs/network matching, least-privilege
default, enforcer + SecurePluginContext gating); full core suite 275 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(spec): plugin fields on uploadArtifact contract (ADR-0025 F5)

Extend UploadArtifactInput/Result so the IPackageService upload path
carries code-bearing `.osplugin` plugins alongside metadata packages,
aligned with the cloud control plane's publish flow:

- Input: `kind` ('metadata' | 'plugin'), detached `signature`
  (`ed25519:<keyId>:<base64url>`), `expectedChecksum` (artifact sha256).
- Result: `versionId`, `listingStatus` (e.g. pending_review), and
  `signatureVerified`.

All additive + optional — metadata packages are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Sep 28, 2026
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…lic form a walled posture cannot take intake for says why (objectstack-ai#21608)

Fixes objectstack-ai#21476
Clause-②: yes (widening)

This is part 2 of 2 of objectstack-ai#21476, the publish half. Part 1 (PR objectstack-ai#21580,
`a7ab047cf`) delivered the anonymous doors and the administrator's read.
This PR delivers the seat's answer A (ACCEPT `5968878000`) to part 1's
open question:

- a gate-local warning advisory in `runtime-authoring-gate.ts`;
- the predicate moved into `@objectstack/metadata-core`;
- the gate fed the posture in force from `protocol.ts`.

With this, every surface triage's ruling `5962758813` names is
delivered: both doors, the administrator's read, and the administrator's
publish.

## What

On a walled posture in force (`group` or `isolated`), saving a view
(`PUT /meta/view/:name`) or publishing its draft (`POST
/meta/view/:name/publish`, and the package batch publish) can carry an
open public form whose object is walled by an organization column. That
write now answers success with **one `warning` advisory per such form**
under `advisories`:

- rule `public-form-intake-unavailable`;
- located at the form's `sharing`, under the write's root
(`views[0].formViews.contact.sharing`, `views[0].config.sharing` or
`views[0].form.sharing`);
- `message` is the administrator's read's reason, byte for byte;
- `hint` is the remedy: declare `tenancy: { enabled: false }` if the
rows belong to no organization.

It never blocks and never 422s. It is omitted-when-empty as before, and
a draft save is not judged (objectstack-ai#4463 D1).

- **One predicate, moved.** `anonymousFormIntakeUnavailability(object,
posture, readObjectSchema)` now lives in `@objectstack/metadata-core`
(`anonymous-form-intake.ts`). Next to it are:
  - its posture reader `anonymousFormIntakePosture(tenancy)`;
- the reason, `anonymousFormIntakeUnavailableMessage`, built from
`anonymousFormIntakeUnavailableRemedy`;
  - the location, `anonymousFormSharingPath`;
  - the target object, `anonymousFormObjectName`;
  - the type `AnonymousFormIntakeUnavailable`.

Three readers call those exports: both anonymous doors, the admin read
(`rest-server.ts`), and the gate rule. No copy is left in `rest`. The
reason text is the same bytes as part 1's, proven by evaluating part 1's
function from `$BASE` against the export over 8 inputs: byte-identical.
- **The gate rule** is `findPublicFormIntakeGaps`, beside
`findPlatformScheduleOrgGaps`. It is pure, and it reads only what the
gate already holds:
- the object universe `assertRuntimeAuthoringRules` already gathers
(registry plus stored rows), folded with this batch's pending drafts,
now computed once and shared with the shared rules;
  - one new pure input, `tenancyPostureInForce`.

  It adds no network or engine call.
- **The posture input** is `tenancyPostureInForce()` in `protocol.ts`.
It reads
`anonymousFormIntakePosture(this.getServicesRegistry().get('tenancy'))`,
the same service and the same reader the doors use, and the same channel
`anonymousFormIntakeOrgScopeRefusal` already reads `tenancy` through.

### Two deviations from the dispatch's mechanism hypotheses, each
measured

1. **The predicate judges the object's EFFECTIVE schema.** It now
applies metadata-core's `applyInjectedSystemColumns` before resolving
the wall column. The doors read served object documents, which already
carry the injected `organization_id`, so for them this is the same
reference and their answers are unchanged. The rest suite is 4883 / 4883
before and after, the same count as part 1.

The gate's universe is different. Its stored-row winners and a batch's
pending drafts are raw bodies, because `foldStoredCollection` does not
apply the read exits' `governServedItem`. Judged raw, a Studio-authored
object reads as unwalled, and the advisory would disagree with the
doors.
2. **The predicate is synchronous for a synchronous reader.** The gate
is pure and synchronous. The doors need the object read to stay lazy:
part 1's pin asserts that the single posture reads no object. So the
export has two overloads:
   - a synchronous reader gets a synchronous answer;
- a reader returning a promise gets a promise, or `null` without reading
when no wall is in force.

   The doors' call sites are unchanged.

### `orgWallEnforced()` is NOT aligned (Zone 2 objectstack-ai#3: measured, then left
as is)

The advisory reads the posture IN FORCE. The objectstack-ai#6285 schedule refusal
keeps reading the REQUESTED posture through `orgWallEnforced()`.

I measured the alternative with a one-off mutation: `orgWallEnforced()`
reading the in-force posture, its throw arm kept. My prediction was that
every objectstack-ai#6285 refusal row driven through `saveMetaItem` with no tenancy
service would turn red. Observed: **6 red / 28 passed**, across
`protocol.platform-schedule-org-gate.test.ts` and
`protocol.bracketed-refusal-opener-absence.test.ts`:
- `refuses the publish …`;
- `… under the group posture`;
- `… refuses the publish that promotes it`;
- `OS_ALLOW_UNLINTED … loud log`;
- `[objectstack-ai#6710] DOES gate an unscoped kernel`;
- `survives a deployment whose OS_TENANCY_POSTURE is unparseable …`.

So aligning would narrow a refusal that the docblock and ADR-0105 defend
(the unparseable-posture row). It would also contradict the objectstack-ai#6155 Q3=A
ruling, which names `postureEnforcesWall(resolveTenancyPosture())` as
that input verbatim. Per the dispatch, it stays. The split is documented
on both inputs, and it is reported as a finding below. The mutation was
restored, proven by blob == HEAD and an empty `git diff HEAD`.

## Pins

- `packages/metadata-core/src/anonymous-form-intake.test.ts`, +15 cases
(13 → 28):
  - both walled postures;
  - the effective schema;
  - a declared `tenancy.tenantField`;
  - controls: tenancy-disabled, absent object, no wall column;
  - `single` and no tenancy service, with zero object reads;
  - the asynchronous reader;
  - posture-in-force reading (degraded reads `single`, legacy `multi`);
  - sharing path across all three form shapes;
  - object name;
  - message ending with the remedy.
-
`packages/metadata-protocol/src/runtime-authoring-gate.public-form-intake.test.ts`
(new, pure), 12 cases:
- per walled posture, exactly one advisory, compared with `toEqual`
against the metadata-core reason and remedy;
  - each form shape's path;
  - a pending raw object in the batch;
- controls: tenancy-disabled, `single`, no posture, a withdrawn form, a
draft, a non-view write;
  - `orgWallEnforced: true` with `single` in force raises nothing;
- a refused view write (`422 INVALID_METADATA`) discloses the rule in
`rulesRun`.
-
`packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts`,
+8 end-to-end rows through `saveMetaItem` and `publishMetaItem`, with a
real `tenancy` service in the services table:
- `isolated` and `group` × PUT and publish: success, exactly one
advisory, the reason equal to `anonymousFormIntakeUnavailableMessage`,
and the row landed active;
- controls on PUT and publish: tenancy-disabled object, `single`, no
tenancy service, and a **degraded** deployment
(`OS_TENANCY_POSTURE=isolated`, service in force `single`), where the
doors serve the form and nothing is raised.
- Part 1's `packages/rest/src/public-form-intake-availability.test.ts`
is **unchanged** and green (16/16). It now exercises the moved export
through `dist/`.
- **Real-boot measurement** (a one-off file, not committed):
`bootStack(showcaseStack, { multiTenant: 'posture-only' })`, posture
`isolated`.
  - The admin read warning is `config.sharing` with the reason.
- `PUT /meta/view/showcase_inquiry.contact` answered 200 with exactly
one advisory: `path: "views[0].config.sharing"` and `message` identical
(`toBe`) to the admin read's warning.
  - `PUT ?mode=draft` answered 200 with no advisories.
  - `POST …/publish` answered 200 with the same advisory.

## Ablations, direction predicted before each run

| Ablation | Predicted | Observed |
|---|---|---|
| A: the gate rule's findings removed from the verdict
(`runtime-authoring-gate.ts`, src) | only the advisory rows: 8 red (4
pure, 4 end-to-end) | full metadata-protocol suite **8 failed / 3204
passed**, exactly those 8 |
| B: the predicate answers "available" everywhere (metadata-core,
rebuilt) | 20 red: metadata-core 5, the rest door and admin-read rows 7,
advisory rows 8; every control green | **5 + 7 + 8 = 20 red**, every
control green |

Both mutations went through `scripts/ablation-replace.mjs` in WRAP mode:
the anchor hit 1 → 0, and the restore was proven by blob == HEAD and an
empty `git diff HEAD`.

B is dist-mediated, so it used a type-valid mutation carrying a
string-literal marker (part 1 measured that a DTS refusal leaves the
mutated JS in `dist/`):
- `ablation-dist-preflight` found the marker in 2 built files before the
run.
- After restore and rebuild, `--absent` reported the marker absent from
all 12 built files and the tree clean against HEAD.
- Positive control: the restored guard is present in `dist/index.js` and
`dist/index.cjs`.

## Tests and gates

The code is final at `dc0a93d4c4`. `0687a7f17e` adds only docs and the
changeset (`git diff dc0a93d 0687a7f` touches no `packages/`
path).

- metadata-core `test`: 17 files, **326 passed**.
- metadata-protocol full suite: 208 files, **3212 passed**, 19 skipped
(at `dc0a93d4c4`).
- rest `test` (`--project local`): 258 files, **4883 passed**, 326
skipped. `test:repo`: 5 files, 177 passed.
- typecheck: metadata-core, metadata-protocol and rest all clean. rest's
includes `check:test-typecheck`.
- Five public-form dogfood files (walled intake, walled withdrawal,
showcase withdrawal, showcase public form, read-back masking): **5 files
/ 20 passed**.
- `dispatch-gates --repo objectstack-ai/objectstack --commands` at
`0687a7f17e` derived 95 commands. All 95 exit 0.
- Two first answered exit 3 `PREREQUISITE NOT MET`:
`check:skill-examples` (client-react unbuilt) and
`check:dual-build-cjs-loads` (8 packages unbuilt). Both were re-run
green after building those packages, so they are measured, not skipped.
  - `--ran`: 95 derived, 95 run, 0 NOT-MEASURED, 0 UNRUN.
- eslint, narrowed: `eslint --no-inline-config --format json` on the 7
changed `.ts` files gave 7 files, 0 errors, 0 warnings, none ignored.
`eslint.config.mjs` never enables type-aware linting (no
`parserOptions.project`), so the narrowing cannot move an untouched
file's verdict. Full `pnpm lint` is CI's.
- `main` moved 4 commits past `$BASE` (`6c5697dffb`). The only overlap
with these packages is one new metadata-protocol test file (the
spec-validation 422 face inventory), which does not touch the authoring
gate. The branch is not merged; CI runs the merge ref.

## Docs

- `content/docs/deployment/validating-metadata.mdx`:
- adds the runtime-only row "Public-form anonymous intake on this
deployment's tenancy posture — advisory only" (`✓ᵛ`);
- rewrites the sentence that called the platform-schedule row "the one
deliberate exception". There are now two deployment-fact rows.
- `content/docs/ui/forms.mdx`: the "wires the anonymous REST endpoints
automatically" rule list gains the walled-posture rule. The form is not
offered, the admin read and the save/publish response say why, and the
remedy is given.
- `skills/**` is governed and not edited. Two published skill sentences
are already false, made so by the `sharing.enabled` rule and by part 1,
not by this PR:
- `skills/objectstack-api/SKILL.md` "Any `FormView` declared with
`sharing.allowAnonymous: true` and a `publicLink` slug is auto-mounted";
  - `skills/objectstack-ui/SKILL.md`'s "Public / anonymous form" row.

## Acceptance notes

- **Finding, the posture split, kept deliberately.** The objectstack-ai#6285 refusal
reads the REQUESTED posture, while the doors, the engine and this
advisory read the posture IN FORCE. On a degraded deployment the refusal
turns away a schedule-flow publish the engine would stamp. Aligning it
is a ruling's call (objectstack-ai#6155 Q3=A names the input), measured above at 6
pinned refusals. This is a code read with no public-door reach measured,
so it is not filed; it is noted for the seat.
- The advisory's object read is the gate's universe. A form bound to an
object that is in neither the live universe nor this batch gets no
advisory, and the doors offer such a form too, so the two agree.
- The intake reason still rides only RestServer's single-item view read
and the write responses. The list read (`GET /meta/view`), `/layers` and
the runtime dispatcher's `/meta` read carry none (part 1's note,
unchanged).
- The console's rendering of `advisories` and `_diagnostics.warnings`
for this rule is NOT MEASURED: no objectui checkout.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…tead of a tracker number (stage 11) (objectstack-ai#21736)

Part of objectstack-ai#20749
Clause-②: no

Stage 11 of this card, and the second area of class (e): the test
strings shipped under `packages/spec/src`, as ruled in `5902360492` on
objectstack-ai#20513. This stage takes the whole `kernel/` directory. Its 102
test-title and test-message literals carried 106 tracker ids citing 69
records. Each id now either states what its record decided, in words
(form D), or is dropped where the title already says it. Text only: no
assertion, fixture value, test count or code comment changes.

## Census at the base (`3fa850cf00`, the claim's base)

Instrument: stage 10's `census10.cjs` (md5
`9d08602ab972b4b8643c90d64d40fa41`) and stage 9's `census.cjs` (md5
`6e42a45a926d375013c32d62f16a296e`), both byte-identical to the copies
stage 10 used. A literal counts as a test title when its folded message
is argument 0 of a `describe` / `it` / `test` call, `.each` / `.skip` /
`.only` chains included. Everything else is an "other" string.

Both instruments read **1696 messages / 1807 ids in 405 files at the
base**, which is stage 10's reading at its head exactly. `kernel/` reads
102 / 106, also stage 10's figure.

| directory | files | messages / ids | titles | other |
|:--|--:|--:|--:|--:|
| `data/` | 95 | 468 / 501 | 445 / 475 | 23 / 26 |
| `ui/` | 81 | 392 / 415 | 374 / 397 | 18 / 18 |
| `api/` | 40 | 189 / 201 | 181 / 193 | 8 / 8 |
| `system/` | 34 | 154 / 165 | 128 / 138 | 26 / 27 |
| (files directly in `src/`) | 30 | 118 / 120 | 117 / 119 | 1 / 1 |
| **`kernel/`** (this PR) | 36 | **102 / 106** | 92 / 96 | 10 / 10 |
| `shared/` | 21 | 85 / 95 | 73 / 81 | 12 / 14 |
| `contracts/` | 25 | 63 / 74 | 59 / 70 | 4 / 4 |
| `conversions/` | 9 | 34 / 34 | 34 / 34 | 0 |
| `security/` | 8 | 28 / 28 | 28 / 28 | 0 |
| `ai/` | 9 | 18 / 20 | 13 / 15 | 5 / 5 |
| `identity/` | 6 | 15 / 15 | 14 / 14 | 1 / 1 |
| `integration/` | 4 | 14 / 14 | 13 / 13 | 1 / 1 |
| `migrations/` | 2 | 9 / 12 | 9 / 12 | 0 |
| `marketplace/`, `meta-spelling/`, `studio/` | 5 | 7 / 7 | 7 / 7 | 0 |
| **total** | **405** | **1696 / 1807** | **1587 / 1692** | **109 /
115** |

- **Controls.** Lit, a title:
`kernel/capability-metadata-kind.test.ts:66` reads one message with
objectstack-ai#5961. Lit, a template-literal expect message:
`kernel/cli-command-contribution-retirement.test.ts:74` reads one
message. Dark: the `// ─── [objectstack-ai#17178] …` comment at
`kernel/execution-context.test.ts:205` reads 0 (the file's messages sit
at `:72`, `:167`, `:219`, `:233` and `:237`). Planted in a scratch copy:
an id added to a title reads 1 / 1, and an id in an added comment reads
0.
- **A wider pattern** (any `#` plus digits) reads 107 / 111 under
`kernel/` at the base. The five extra hits are hex colours (`#94A3B8`,
`#0f0`) in `functional-completeness.test.ts` and one `§3.10 objectstack-ai#3` section
reference in `manifest.zod.ts`, a non-test file. None is a tracker id.
At the head the wider pattern reads only those five, and the gate
pattern reads 0 / 0.
- **At the head:** 1594 messages / 1701 ids in 369 files. `kernel/`
reads 0 / 0. Nothing else moved.

## How the area was chosen

Stage 10's rule, applied before any card was read: rank whole
first-level directories by ids, and take the busiest one within about
10% of the ~100-id bound. The four busiest each exceed the bound alone:
`data/` (501), `ui/` (415), `api/` (201) and `system/` (165). The files
directly in `src/` (120) are 20% over. `kernel/` (106) is the busiest
whole directory within the bound, and its census reads exactly stage
10's 106, so the rule needed no second pass.

**Named for the next stages:** `data/` (about five stages, by
subdirectory or file group; `data/driver/` alone is 52), `ui/` (about
four), `api/` (two), `system/` (two), the files directly in `src/` (one,
120), `shared/` (one, 95), `contracts/` with `conversions/` (one, 108),
and `security/`, `ai/`, `identity/`, `integration/`, `migrations/`,
`marketplace/`, `meta-spelling/` and `studio/` together (one, 96).

## What each id became

Of the 106 ids, 32 now state a decision in words, in 31 literals. 74 are
dropped where the title already explains them; two of those (objectstack-ai#14478) sit
in literals that also gained words for another id. Every record was read
with its comments through REST. 58 answer 200. Ten answer 404, and their
decisions were read from what landed. One is in a repository not
attached to this session.

| record | ids | result |
|:--|--:|:--|
| objectstack-ai#12007, objectstack-ai#11825, objectstack-ai#12340, objectstack-ai#4914, objectstack-ai#15932, objectstack-ai#11846, objectstack-ai#16059 | 10 of 20 |
Every expect message that read "must have zero holders after #N" or
"must not be exported after #N" now reads "after its retirement": each
record retired the names it lists. The other 10 sit in `describe` / `it`
titles that already say what was retired, and were dropped. objectstack-ai#11846
answers 404; its retirement was read from the CHANGELOG entry for
landing `0c2334f`. |
| objectstack-ai#7280 | 1 | "the ADR-0069 gate posture is a declared field":
`authGate` is declared on `ExecutionContextSchema`, not spread behind an
`as any`. |
| objectstack-ai#17178 | 1 | "SEED_WRITE_EXECUTION_CONTEXT — one spelling of the seed
posture": one exported constant replaced the private copies. |
| cloud#687 | 1 | "(the founding case: a roll-up that reads 0 forever)".
The cloud repository is not attached to this session (403). The decision
was read from ADR-0078's "Surfaced by" line and the CHANGELOG paragraph
on the founding case: a bare `{ type: 'summary' }` field read 0 forever,
and the rule now flags it as an error. |
| objectstack-ai#14192 (404) | 4 | "(the silent-drop measurement, inverted)", where
the title said "the card's measurement". Dropped from 3 titles that
state the refusal. Decision read from landing `4d0d944`:
`ManifestSchema` goes strict and refuses unknown keys inside
`manifest:`. |
| objectstack-ai#10726 (404) | 2 | "removed for the `http.server` mount,
maintainer-ruled 2026-08-22", where the title said "Option B". Read from
landing `bc56e18` and PR objectstack-ai#12417's body: Option B removes
`contributes.routes` and points authors at the imperative `http.server`
mount. Dropped once. |
| objectstack-ai#4148 | 1 | "the object/field unknown-key warnings survive the
generalization", where the title said "the objectstack-ai#4148 behaviours". |
| objectstack-ai#4001 | 3 | "(the evidence base for the strict tiers)", where the
title said "objectstack-ai#4001 evidence phase". Dropped from 2 titles that state the
pinned rule. |
| objectstack-ai#4167, objectstack-ai#8687 | 3 | "top-level stack keys (named, then refused at
parse)": objectstack-ai#4167 made an undeclared top-level key say so instead of
vanishing, and objectstack-ai#8687 ruled Shape B, a strict top level. Dropped once
more for objectstack-ai#8687. |
| objectstack-ai#15624 | 2 | "cache.ttl → deleted (the unread outer block is
retired)". Dropped once. |
| objectstack-ai#14478 | 3 | Dropped. The `→ ttlMs` / `→ timeoutMs` renames and "carry
their unit" already state the rule: the unit lives in the key name. |
| objectstack-ai#15939 | 1 | "RuntimeConfig.resourceLimits.timeout → timeoutMs (its
unit was named in JSDoc only)", where the title said "ruling A". Ruling
A renames the keys whose unit was named only in JSDoc, per file. |
| objectstack-ai#5086 | 2 | "(PUT /meta refuses the inlet)": a code-only kind's create
is refused with 403 `NOT_CREATABLE` before anything persists. |
| objectstack-ai#7743 | 1 | "UNCHANGED, the field overlay refusal stays", where the
title said "objectstack-ai#7743's overlay refusal". |
| objectstack-ai#8154 | 1 | "(the consumer contract of the per-type redaction hook)".
|
| objectstack-ai#21120 | 1 | "stored metadata ROWS — the family-wide seam every exit
routes through". This says only what the card's public summary says: one
shared seam, which every surface routes through or refuses. |
| objectstack-ai#6245 | 1 | "the bound-but-unregistered fence, pinned": schemas are
bound for those kinds WITHOUT registering them. |
| objectstack-ai#11263 | 1 | "PLATFORM_PLUGIN_WIRED_RUNTIMES — runtimes wired by
plugins[], not by a token": a sibling roster was added, and no token was
minted. |
| objectstack-ai#3366 | 1 | "classifyRequiredCapability — preflight for an installable
provider in this edition". |
| objectstack-ai#17676 | 1 | "package-registry carve-out — its persistence is
always-on core, split from `marketplace`", where the title said "ruling
A′". |
| objectstack-ai#16365 | 1 | "accepts %s, which the regex refused before the SemVer
widening", where the title said "the pre-objectstack-ai#16365 regex". The `%s` values
do not change. |
| objectstack-ai#17227 | 1 | "dashboard.header.actions stays titled — the first
carrier given item-level names". |
| dropped only (live) | 45 | objectstack-ai#3308 (2), objectstack-ai#3433, objectstack-ai#3760, objectstack-ai#3786, objectstack-ai#4212,
objectstack-ai#4509, objectstack-ai#4587, objectstack-ai#4657, objectstack-ai#4741, objectstack-ai#4834, objectstack-ai#4939, objectstack-ai#5488, objectstack-ai#5961, objectstack-ai#6881, objectstack-ai#6931,
objectstack-ai#7893, objectstack-ai#8586, objectstack-ai#10039 (2), objectstack-ai#11169, objectstack-ai#12032, objectstack-ai#12428, objectstack-ai#13613, objectstack-ai#15678 (7),
objectstack-ai#16328, objectstack-ai#16334, objectstack-ai#16449, objectstack-ai#17232 (2), objectstack-ai#17445, objectstack-ai#17780, objectstack-ai#18124 (2), objectstack-ai#18791
(3), objectstack-ai#19630, objectstack-ai#20102: each title already states the pinned decision. |
| dropped only (404) | 8 | objectstack-ai#10194 (landing `2306a76`: each bound entry
is its stack collection's schema), objectstack-ai#10338 (`d2619fd`: `target` optional,
the gate holds the flow requirement), objectstack-ai#10724 (`be21955`: the nine dead
members tombstoned), objectstack-ai#11330 (`a9ee98992`: the trust-tier text tells the
truth), objectstack-ai#11332 (`dce5cd4`: the three dead containers retired), objectstack-ai#13135
(`9e0ba21`: the paper customization protocol retired), objectstack-ai#17147 (2,
`aaacf1d5c`: the granted set is registered and refuses nothing, said
truthfully). Each title already carries what landed. |

## Readers

- **Test-name filters:** none. A tracked-tree search for `-t` and
`--testNamePattern` finds only `packages/qa/dogfood/README.md:142` (`-t
"owner-scoped"`), which is unrelated.
- **Snapshots:** none. `kernel/` has no `__snapshots__`, and no `.snap`
file is tracked under `packages/spec`.
- **Titles by substring:** every old title, plus a window around each id
(256 needles), was searched across the tracked tree outside its own
file. No gate, doc or script matches one. At the head, 8 hits remain:
three sibling titles in other lanes' or stages' files
(`metadata-protocol/src/protocol.capability-write-door.test.ts:183`,
`spec/src/api/contract.test.ts:813`,
`spec/src/system/auth-config.test.ts:520`), three released
`packages/spec/CHANGELOG.md` entries, and one code comment in
`kernel/metadata-authoring-lint.ts:268`, which belongs to the comment
lane.

## Text-only proof

Stage 10's scratch tool (`textonly10.cjs`, md5
`d5e4801dbb4329ab1984da91e92fc47c`) compares base and head file by file
on three legs:
1. **Skeleton:** the full AST, with string pieces masked. It must be
identical.
2. **Comments:** every comment, byte-equal.
3. **Strings:** each string leaf that changed must sit in a test-call
title position, or on one of the 10 declared lines. Those are the expect
messages at `cli-command-contribution-retirement.test.ts:74` and `:86`,
`plugin-lifecycle-advanced-retirement.test.ts:103`, `:124` and `:141`,
`plugin-loading-retirement.test.ts:125`,
`plugin-security-scan-result-retirement.test.ts:123`,
`preview-mode-retirement.test.ts:193` and
`startup-orchestrator-retirement.test.ts:85` and `:106`. Each changed
leaf must carry a tracker id before and no `#` plus digits after.

- **Result:** 36 of 36 files SAME, 102 changed (92 title, 10 declared),
on all three legs.
- **Diff hunks:** exactly the 102 planned lines, with every file keeping
its line count.
- **Controls (10 of 10 as predicted, on scratch copies, each anchor hit
once):** identifier rename DIFF; numeric literal DIFF; comment edit
COMMENT DIFF; a non-title string with an id VIOLATION; a rewritten title
given a new id VIOLATION; a title that was id-free at base edited
VIOLATION; one title reverted to base SAME; a declared string keeping an
id VIOLATION; an undeclared expect message changed VIOLATION; a title
re-split into a `+` chain DIFF.

**Test counts:** the 36 files were run at the base (in a separate base
worktree) and at the head: 841 / 841 tests on both sides, with the same
count and status sequence per file in 36 of 36. 388 full test names
change, and each equals the base name with the planned replacements
applied.

## Changeset: `skip-changeset`

Measured, not assumed:
- `npm pack --dry-run` of `@objectstack/spec` lists 2068 files under
`files[]`. 0 of the 36 touched files are in it, and 0 `*.test.ts` at
all. The controls `src/kernel/manifest.zod.ts` and
`src/kernel/execution-context.zod.ts` are in it.
- In `dist/`, three new phrases and three old ones each read in 0 files.
The control `Plugin compatibility ranges (ADR-0025` reads in 20.

So this PR publishes nothing, and no changeset is added.

## Verification (at `e0ad8f50af`)

- `pnpm turbo run build` over all packages: 71 / 71 (at the first
commit), then `@objectstack/spec` rebuilt at `e0ad8f50af`.
- `@objectstack/spec`: `vitest run --project local`, 613 files and 18215
passed, 1 todo. `typecheck` exit 0, including `check:test-typecheck`,
whose program holds all 36 touched files.
- **Gates:** `dispatch-gates --commands` derived 79 families at
`e0ad8f50af`, and all 79 exit 0. `--ran` reconciles: 79 derived, 79 run,
0 NOT-MEASURED, 0 UNRUN.
- **ESLint, a proven narrowing:** `--no-inline-config` over the 36
files, 0 errors and 0 warnings. The population comes from ESLint's own
config: 36 configured, 0 ignored. No `parserOptions.project` or
`projectService`, so no untouched file's verdict can move.
- `check-governed-merges --test`: NOT governed, 204 changed lines.

## Acceptance notes

- **Code comments still carry ids** in these 36 files and in the
`kernel/` sources, for example `execution-context.test.ts:205`,
`metadata-authoring-lint.ts:268` and `functional-completeness.ts:148`.
They are the comment lane's, untouched here.
- **A sibling title in another package** repeats `objectstack-ai#5961 — capability`
(`packages/metadata-protocol/src/protocol.capability-write-door.test.ts:183`).
It is that package's test-string stage, not this one.
- **The second commit** (`e0ad8f50af`) rewords one title from this PR's
first commit, "the one carrier already titled", which read as a
tautology, to "the first carrier given item-level names". Every proof
above was re-run at that head.
- **`origin/main` moved** three commits past the base before this PR
opened (objectstack-ai#21717, objectstack-ai#21715, objectstack-ai#21660). None touches `packages/spec`, so
nothing was merged.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…cision in words instead of a tracker number (stage 23) (objectstack-ai#21947)

Part of objectstack-ai#20749
Clause-②: no

Stage 23 of this card: the next area of class (e), the test strings
shipped under `packages/spec/src`, as ruled in `5902360492` on objectstack-ai#20513.
This stage takes the last name-ordered `ui/` group: the 16 id-bearing
test files directly under `packages/spec/src/ui/` from
`view-item-config-type.test.ts` to `widget.test.ts`. Those files carried
100 messages and 106 tracker ids, citing 53 records. All 106 now either
state what their record decided, in words (form D), or are dropped where
the title already says it. No needle sits in this group. Text only: no
assertion, identifier, test count or code comment changes, and no file
is renamed.

## Census at the base (`9e33ee7c59`)

Instruments: `census10.cjs` (md5 `9d08602ab972b4b8643c90d64d40fa41`),
`census.cjs` (md5 `6e42a45a926d375013c32d62f16a296e`), `census-wide.cjs`
(md5 `c98410a19529c439adb0afbfb00026a2`) and `dirtable.cjs` (md5
`dda605c54745b4a60cc14c9a686e4eff`), byte-identical to the copies stages
10 to 22 used. A literal counts as a test title when its folded message
is argument 0 of a `describe` / `it` / `test` call, `.each` / `.skip` /
`.only` chains included. Everything else is an "other" string.

The worktree was cut from `origin/main` at `9e33ee7c59`, the claim's
base. Both instruments read **571 messages / 604 ids in 127 files**, the
seat's reading and stage 22's head reading.

| directory | files | messages / ids | titles | other |
|:--|--:|--:|--:|--:|
| `api/` | 40 | 189 / 201 | 181 / 193 | 8 / 8 |
| `system/` | 34 | 154 / 167 | 128 / 138 | 26 / 29 |
| (files directly in `src/`) | 30 | 118 / 120 | 117 / 119 | 1 / 1 |
| `ui/` (this PR: 16 of the 21 files) | 21 | 107 / 113 | 97 / 103 | 10 /
10 |
| `ai/` | 1 | 2 / 2 | 0 | 2 / 2 |
| `contracts/` | 1 | 1 / 1 | 0 | 1 / 1 |
| **total** | **127** | **571 / 604** | **523 / 553** | **48 / 51** |

The group reads **100 messages / 106 ids in 16 files**, the seat's
figures file for file:

| file (under `ui/`) | messages / ids | titles | other |
|:--|--:|--:|--:|
| `view-item-config-type.test.ts` | 1 / 1 | 1 / 1 | 0 |
| `view-metadata-schema.test.ts` | 8 / 8 | 8 / 8 | 0 |
| `view-metadata-type.test.ts` | 2 / 2 | 2 / 2 | 0 |
| `view-overlay-options-bag.test.ts` | 6 / 6 | 6 / 6 | 0 |
| `view-overlay-options-type.test.ts` | 1 / 1 | 1 / 1 | 0 |
| `view-overlay-owner-hidden-retirement.test.ts` | 1 / 1 | 1 / 1 | 0 |
| `view-overlay-viewkind-arm.test.ts` | 10 / 10 | 10 / 10 | 0 |
| `view-overlay-viewkind-type.test.ts` | 1 / 1 | 1 / 1 | 0 |
| `view-strictness-batch18.test.ts` | 10 / 11 | 10 / 11 | 0 |
| `view-submit-redirect-url.test.ts` | 5 / 5 | 5 / 5 | 0 |
| `view-union-branch-focus.test.ts` | 7 / 7 | 6 / 6 | 1 / 1 |
| `view-union-diagnostics.test.ts` | 4 / 5 | 4 / 5 | 0 |
| `view-union-retirement-prescription.test.ts` | 1 / 1 | 1 / 1 | 0 |
| `view.test.ts` | 39 / 43 | 38 / 42 | 1 / 1 |
| `widget-i18n-retirement.test.ts` | 3 / 3 | 2 / 2 | 1 / 1 |
| `widget.test.ts` | 1 / 1 | 1 / 1 | 0 |
| **16 files** | **100 / 106** | **97 / 103** | **3 / 3** |

Three more test files sit in the same name range and carry no id
(`view-item-owner-hidden-retirement.test.ts`,
`view-list-tabs-retirement.test.ts`, `vocabulary-derivation.test.ts`).
The three "other" strings are rewritten and declared to the text-only
tool: the table label at `view-union-branch-focus.test.ts:139`, which
prints inside two `for … of` test titles, and the expect messages at
`view.test.ts:4099` and `widget-i18n-retirement.test.ts:135`.

- **Controls.** Lit: `ui/notification.test.ts` and
`api/api-error-code-type.test.ts`, outside the group, read 1 id each at
the base and at the head. Dark: `view.test.ts` reads 0 at the head while
92 of its comment lines still carry a number. Planted in a scratch tree:
an id put into a `widget.test.ts` title reads 1 / 1 (`title:describe`),
and an id put into a `view-metadata-type.test.ts` comment reads 0.
- **A wider pattern** (any `#` plus digits) reads the same as the gate
pattern in 15 of the 16 files at the base. `view.test.ts` reads 2 more,
and keeps them at the head: the CSS colours `'#00cc00'` (`:2770`) and
`'#22c55e'` (`:3537`), fixture values that cite nothing.
- **At the head:** 471 messages / 498 ids in 111 files. The 16 files
read 0 / 0, `ui/` reads 7 / 7, and no other file moved.

## How the area was chosen

`ui/` has no subdirectory test file with an id, so it is taken in
name-ordered file groups near the ~100-id bound. Stage 22's re-cut named
this group at 106 ids, and this census reads 106, so no re-cut was
needed. `view.test.ts` (43 ids) is one file inside one text-only proof
here, so it is not split.

**`ui/` after this PR** reads 7 / 7, all kept items: stage 20's
`component-props-unknown-members.pin.test.ts:322`, stage 21's four
colour literals (`dashboard-chart-structure-refusal.test.ts:94`,
`dashboard.test.ts:124`), and stage 22's two needles
(`notification.test.ts:123`, `strictness-batch14.test.ts:395`).

**Named for the next stages** (cut from the head census, 471 / 498):
- **`api/`, 201 ids in 40 files**, with no subdirectory. Its first
name-ordered group near the bound is `ai-agents-envelope.test.ts`
through `package-lifecycle.test.ts`: 27 files, 100 messages / 106 ids
(95 / 101 titles, 5 / 5 other: `auth.test.ts`,
`discovery-environment-subset.pin.test.ts` and three in
`export-job-family-retirement.test.ts`). The second is
`plugin-rest-api.handler-status-retirement.test.ts` through
`zod-issues-to-fields.test.ts`: 13 files, 89 / 95, `protocol.test.ts`
alone 50.
- `system/` 167, two stages. The files directly in `src/`, 120, one.
- The needles: the three docblock needles, the kept `:322` and stage
22's two. One stage, with an at-tier review.

## What each id became

- **25 literals (27 ids)** now state a decision in words.
- **20 literals (22 ids)** get their subject back in words, where the
number stood for a thing.
- **55 literals (57 ids)** drop a number the title already explains.

Every cited record was fetched with all its comments through REST, and
its decision was read from its ruling, ACCEPT and landing comments: 53
records, 51 answer 200 and 2 answer 404. Five citations are objectui's
and were read from objectui: `objectui#5233`, `objectui#2231` (cited
bare at `view-strictness-batch18.test.ts:309`), `objectui#6237` (cited
bare at `view.test.ts:949`), `objectui#5435` and `objectui#3289`. Three
same-number records in the other repository were fetched first and set
aside: `objectstack#2231` is a version-packages PR, `objectstack#6237` a
datasource PR, and `objectui#2998` a form PR; `framework#1894 / objectstack-ai#2998`
are this repository's objectstack-ai#1894 and objectstack-ai#2998 under its old name. The two that
answer 404 were read from what landed:
- **objectstack-ai#9933**, from its landing commit `d5552ca13f` ("admit columnState as
an explicitly runtime-only view-overlay key") and the CHANGELOG entry
for `d5552ca`;
- **objectstack-ai#11195**, from PR objectstack-ai#11458, the PR that closed it
("UserActionsConfigSchema adopts group / hideFields / rowColor (ruled A
on objectui#5435)").

One citation names the wrong record, and the titles now state what
landed instead. `objectstack-ai#3896 close-out` (twice in `view.test.ts`) names objectstack-ai#3896,
the sharing-rule criteria card, which records no decision about these
keys; the two titles state the decision from the landed tombstones of
`form.defaultSort` and `view.responsive` / `view.performance`, as stage
20 did for `action.test.ts`.

Where a record's first decision was corrected later, the title follows
the correction:
- **objectstack-ai#6926:** its first triage direction retired the `groups` alias; the
measurement found live consumers, and the maintainer re-ruled A, a fold
at the producer. The two titles say "the producer-side `groups` fold"
and "folds onto `sections`".
- **objectstack-ai#7025 and objectstack-ai#7741:** objectstack-ai#7025 froze the acceptance face; objectstack-ai#7741's ruling
then moved it, and later retirements moved it again, each pinned. The
title says "frozen by the diagnostics work; every move since is
deliberate and pinned", not "moved only once".
- **objectstack-ai#7510:** the "[objectstack-ai#7510] ⛔ the acceptance face did not move" describe
sits beside two ruled moves recorded in its own comments, so the title
now names what did not move it: "⛔ the branch focusing did not move the
acceptance face".

**Stated in words:**

| record | literal (under `ui/`) | now reads | the decision |
|:--|:--|:--|:--|
| objectstack-ai#5599 | `view-metadata-schema.test.ts:95` | "REJECTS a bare `{}` — the
pin this line used to make, reversed by the identity precondition" |
Maintainer ruling 2026-08-06, direction B: a minimal identity
precondition ahead of the union's four members; each member's `.strip()`
is untouched. |
| objectstack-ai#7741 | `view-metadata-schema.test.ts:125` | "… NO object binding — a
row no read path could serve, with located guidance" | Maintainer ruling
2026-08-12, direction B: a row that cannot be expanded or served by any
read path is not stored and badged valid; the inline arm requires the
binding, refused with `defineView`'s guidance. |
| objectstack-ai#5599 | `view-metadata-schema.test.ts:215` | "identity precondition —
a body must read as a view before any member judges it" | The same
direction B. |
| `objectui#5233` | `view-metadata-schema.test.ts:413` | "… a
`columnState`-only patch (the patch-only write the console persists)" |
Maintainer ruling 2026-08-12 (on objectstack-ai#7494): `persistViewPatch` stores the
patch only, not the merged base. |
| objectstack-ai#17152 | `view-overlay-owner-hidden-retirement.test.ts:335` | "… names
the family's D2 conversion (ruled: a D3 entry per family, even beside a
lossless D2)" | Ruling B (director seat, 2026-09-10, upheld 2026-09-11):
one D3 semantic entry per retired family, beside its D2 conversion even
when D2 is lossless. |
| objectstack-ai#7494 | `view-overlay-viewkind-arm.test.ts:102` | "the console %s
toggle (a patch-only write, as ruled) is ACCEPTED on listOverlay" |
Maintainer ruling 2026-08-12: the overlay store is org-wide, and the
toolbar write stores the patch only. |
| objectstack-ai#4001 | `view-strictness-batch18.test.ts:91` | "批 18, unknown keys
refused — the doors these shapes are reachable through" | The strictness
campaign: an unknown key on the authorable surface is refused, not
silently stripped. |
| objectstack-ai#15469 | `view-strictness-batch18.test.ts:149` | "… a CLOSED entry,
and since the renderer-ahead `.passthrough()` was removed a CLOSED
parent too" | Maintainer ruling A (decision batch objectstack-ai#41, 2026-09-05):
every key the gantt and tree renderers read is declared, and both
`.passthrough()` calls go. |
| objectstack-ai#5074 | `view-strictness-batch18.test.ts:364` | "[RESOLVED by the
ruled split] ViewItemSchema SPLIT — …" | Maintainer ruling A
(2026-08-04): split — a strict authoring `ViewItemSchema` and a reopened
wire member in the union. |
| objectstack-ai#5074 | `view-strictness-batch18.test.ts:403` | "[RESOLVED with the
ruled split] ListViewSchema.sort CLOSED — …" | The split's scope
addendum: the wire door strips the console's decoration keys before
validating, so `sort[]` closed again with no declared `id`. |
| objectstack-ai#7025, objectstack-ai#7741 | `view-union-diagnostics.test.ts:246` | "the acceptance
face of ViewMetadataSchema — frozen by the diagnostics work; every move
since is deliberate and pinned" | objectstack-ai#7025's sweep rule: the diagnostic
face improves, the acceptance face does not move; objectstack-ai#7741's ruled binding
requirement is the first pinned move since. |
| objectstack-ai#9463 | `view.test.ts:342` | "viewMode — the granularities the gantt
renderer honours, measured" | Declare `viewMode` with exactly the
granularities objectui's `GanttView` honours, measured, not invented
(the spec half of objectui#5074's ruling). |
| objectstack-ai#17053 | `view.test.ts:441` | "the legacy string `sort` clause is
retired — one spelling, the array" | objectui's ruling (director batch
objectstack-ai#77, option B): one spelling, the array; the spec stops producing the
string. |
| objectstack-ai#13704 | `view.test.ts:873` | "wizard tightening — sections are the
steps, the inert step keys are refused, no key is added" | The ruled
shape of objectstack-ai#13622 (2026-08-31): sections are the steps, the wizard-inert
step keys are refused at parse, zero new keys. |
| `objectui#6237` | `view.test.ts:949` | "… stay accepted on
tabbed/simple (the ruled split confines it to wizard steps)" |
Maintainer ruling 2026-08-30 (director batch objectstack-ai#3): `FormSectionConfig` is
split, so tabbed sections take a predicate and wizard steps carry none.
|
| `objectui#2231` | `view.test.ts:2876` | "ListColumnSchema summary
object form and prefix — spec-owned, no longer an objectui-local
extension" | The derive-by-reference unification: `677b591` moved
`prefix` and the `{ type, field }` `summary` form into the spec, closing
objectui's local `.extend()`. |
| objectstack-ai#3896 (see above) | `view.test.ts:3144` | "FormViewSchema — retired
defaultSort (audit close-out: nothing read it)" | The landed tombstone:
`form.defaultSort` was removed because nothing read it. |
| `objectui#5435` | `view.test.ts:3386` | "… defaults asymmetry, copied
from what the renderer reads" | Ruling A (2026-08-22): the spec adopts
`group` / `hideFields` / `rowColor`, with the defaults copied from
`ListView`'s reads. |
| objectstack-ai#3896 (see above) | `view.test.ts:3826` | "ListViewSchema — retired
responsive/performance (audit close-out: no renderer read them)" | The
landed tombstones: no renderer or runtime read either key. |
| objectstack-ai#7176 | `view.test.ts:3847` | "ListViewSchema — retired
striped/bordered/virtualScroll (every reader only passed them through)"
| Maintainer ruling 2026-08-10: retire under ADR-0049, since every
measured reader copied the keys forward and none applied them. |
| objectstack-ai#5832 | `view.test.ts:4099` (expect message) | "`HttpMethodType` was
renamed to `HttpMethodSubset`" | Maintainer ruling 2026-08-06: rename
the 5-value subset; the 7-value `HttpMethod` keeps its name and its wire
contract. |
| objectstack-ai#16577, objectstack-ai#13817 | `view.test.ts:4708` | "… the `type: 'calendar'` axis
is NOT gated by the `allowedVisualizations` check (ruled: a completeness
warning)" | Ruling B (director seat, 2026-09-11): the objectstack-ai#13817 guard keeps
gating `allowedVisualizations` only; the `type: 'calendar'` route is
carried at warning by `checkViewCompleteness`. |
| objectstack-ai#19228 | `view.test.ts:4793` | "view row bound — `pagination.pageSize`
is the one bound; no per-kind `limit` on the view configs" | Maintainer
ruling D (2026-09-23): one row bound per view, `pagination.pageSize`;
the per-kind `limit` was removed before it shipped. |
| objectstack-ai#5055 | `widget-i18n-retirement.test.ts:70` | "ui/ widget + i18n
family retirement — doorless vocabularies removed, not tightened" |
Maintainer ruling A (2026-08-06): ADR-0049 enforce-or-remove retires the
unreachable widget and locale vocabularies; closing them would only
dress a dead slot as a checked one. |
| `objectui#3289` | `widget-i18n-retirement.test.ts:196` | "the
surviving `error` slot is exactly the one objectui renamed its own slot
onto, with no alias" | objectui followed the spec: its widget
`errorMessage` slot became the spec's `error`, with no alias, and the
form renderer produces it. |

**Subject back in words** (20 literals): "(binding pair, objectstack-ai#7741)" becomes
"(the object + viewKind binding pair)"; "union error behaviour (objectstack-ai#5014)"
becomes "union error behaviour (where a branch prescription gets
buried)"; the five `objectstack-ai#7496` prefixes become "the ruled redirect `url`
shape —" (twice) and "ruled bullet 1 / 2 / 3 —", the file's own name for
the ruling's three bullets; "the pre-objectstack-ai#7510 ranking" becomes "the pre-fix
ranking"; "the objectstack-ai#4001 wrap prescription" becomes "the `defineView` wrap
prescription"; the acceptance-face describe at
`view-union-branch-focus.test.ts:261` (above); "the objectstack-ai#6926 fold" becomes
"the producer-side `groups` fold"; "(objectstack-ai#7025 membership)" becomes "and the
union corpus pins it accepted"; "(objectstack-ai#8321/objectstack-ai#12174)" becomes "(a negative or
fractional scale)", what that test probes; "the exact declaration objectstack-ai#9340
exists to make legal" and "the gap objectstack-ai#9340 closes" name "this block";
"(acceptance criterion, objectstack#11195)" becomes "(the acceptance
criterion for adopting the three keys)"; "(objectstack-ai#7176 rides …)" becomes "(the
retirement rides …)"; "the axis objectstack-ai#13817 does not gate" becomes "the axis
the `allowedVisualizations` check does not gate"; "zero holders after
objectstack-ai#5055" becomes "after the widget + i18n retirement"; "objectstack-ai#5055 — the one
surviving shape" becomes "the widget retirement — the one surviving
shape".

**Dropped where already stated** (55 literals, 57 ids). A number goes
only where the title already says its decision. Examples: the four
`[objectstack-ai#19920]` prefixes ("… typed by its arm, not unknown", "… a parsed view
body, not unknown") and `[objectstack-ai#19871]`; the six `[objectstack-ai#20051]` describes on the
`options` bag and the one in
`view-union-retirement-prescription.test.ts`; the eight `objectstack-ai#20186`
describes ("a column-less list PATCH is judged by the list member",
"each member judges ONE viewKind", …); the three `[objectstack-ai#6391]` describes,
three `[objectstack-ai#7510]` titles and the `[objectstack-ai#21180]` table label ("the retired
`publicPicker` key itself"); "(objectstack-ai#3095)", "(objectstack-ai#5074)" after "`.strip()`
round-tripping is untouched", "(objectstack-ai#9933)" after "runtime-only overlay
key", and the `view.test.ts` tails `(objectstack-ai#15469)`, `(objectstack-ai#6926)`, `(objectstack-ai#12174)`,
`(objectstack-ai#19088)`, `(objectstack-ai#7084)`, `(objectstack-ai#9340 — …)`, `(objectstack-ai#17499)`, `(objectstack-ai#18791)`,
`(framework#1894 / objectstack-ai#2998)`, `(objectstack-ai#5073 — …)` x2, `(objectstack-ai#8010)`, `[objectstack-ai#4688]`,
`[objectstack-ai#4691]`, `(objectstack-ai#6416 / objectstack-ai#6619)`, `(objectstack-ai#17063)`, `(objectstack-ai#16885)`, `(objectstack-ai#13817)` and
`(objectstack-ai#16577)`. The batch label `批 18` stays, in stage 20's "批 19, unknown
keys refused" form on the file's first describe and bare on the other
four. `W2` stays: the file's own header defines W1 and W2. The commit
`ce70876e` stays in "(measured on origin/main ce70876)": a commit, not
a tracker id.

**No file is renamed.**

## Readers

- **Test-name filters:** none. No tracked script, workflow or package
config passes `-t` / `--testNamePattern` (the 3 hits are `docker build
-t`, `type -t` and `lsof -t`).
- **Snapshots:** none. No `__snapshots__` directory is tracked under
`packages/spec`, and none of the 16 files calls a snapshot matcher.
- **Projects:** none of the 16 files is in the `repo` project
(`packages/spec/vitest.repo-tests.json`); all run in `local`.
- **By substring:** every old literal, its id-bearing fragment and a
window around each id (299 needles) was searched with `git grep` at the
base, across the tracked tree outside its own file. No gate, doc,
filter, snapshot, QA checklist entry or `scripts/check-*.mjs` self-test
reads one. The 16 hits are windows that share wording with code comments
and one CHANGELOG line: 15 comments in the migration registry, its
semantic entries and `view-list-tabs-retirement.test.ts` read "(ruling B
on objectstack-ai#17152)", and `packages/spec/CHANGELOG.md:30342` reads "runtime-only
overlay key (objectstack-ai#9933)".
- **Cross-references by id:** two places name the `columnState` section
of `view-metadata-schema.test.ts` as "§objectstack-ai#9933": a code comment at
`packages/spec/scripts/strictness-ledger.test.ts:380` and the
`view.zod.ts` row of
`docs/audits/2026-07-unknown-key-strictness-ledger.md`. Neither matches
a string; both point a reader at the section, which still carries
"columnState — runtime-only overlay key" in its title and its banner
comment. A code comment and an audit record are not this card's share,
so neither is edited.

## Text-only proof

Stage 10's scratch tool (`textonly10.cjs`, md5
`d5e4801dbb4329ab1984da91e92fc47c`) compares base and head file by file
on three legs:
1. **Skeleton:** the full AST, with string pieces masked. It must be
identical.
2. **Comments:** every comment, byte-equal.
3. **Strings:** each changed string leaf must sit in a test-call title
position or on a declared line, must carry a tracker id before, and must
carry no `#` plus digits after. This stage declares three lines:
`view-union-branch-focus.test.ts:139`, `view.test.ts:4099` and
`widget-i18n-retirement.test.ts:135`.

- **Result:** 16 of 16 files SAME on all three legs, with the per-file
counts predicted in writing before the run.
- **Totals:** 100 changed string leaves in 100 literals: 97 titles and 3
declared. The diff's `+` and `-` lines are exactly the 100 planned lines
as multisets, and every file keeps its line count.
- **Controls (14 of 14 as predicted on the first run, on scratch copies,
each anchor hit once):** identifier rename DIFF; numeric literal DIFF;
comment edit COMMENT DIFF; a non-title string given an id VIOLATION; a
rewritten title given a new id VIOLATION; a title that was id-free at
base edited VIOLATION; one title reverted to base SAME; an `it.each` row
given an id VIOLATION; an undeclared expect message changed VIOLATION; a
title re-split into a `+` chain DIFF; a declared expect message reverted
to base SAME; a declared expect message given a new id VIOLATION; the
declared table label given a new id VIOLATION; a template-literal title
given a new id VIOLATION.
- **Templates and tables:** one `.each` title changes,
`view-overlay-viewkind-arm.test.ts:102`, a `%s` template whose
placeholder and rows are untouched. The table label at
`view-union-branch-focus.test.ts:139` feeds two `for … of` template
titles, which print it whole. The template-literal title at `:173`
changes only its text before `${label}`.

**Test counts:** the 16 files were run at the base, in a separate base
worktree, and at the head, with `--project local --project repo`. Both
sides read 862 tests in 16 files, all passed, with the same count and
status sequence per file in 16 of 16. 574 full test names change, and
each changed name equals the base name with the planned replacements
applied (0 mismatches). No full name repeats on either side.

## Changeset: `skip-changeset`

Measured, not assumed:
- `npm pack --dry-run` of `@objectstack/spec` lists 2068 files. 0 of the
16 touched files are in it, and no `*.test.ts` at all. The controls
`src/ui/view.zod.ts`, `src/ui/widget.zod.ts` and `dist/index.mjs` are in
it.
- In the built `dist/`, a new phrase and an old one each read in 0
files. The control `Unrecognized key` reads in 42.

So this PR publishes nothing, and no changeset is added.

## Verification (at `75022207b3`)

- `pnpm turbo run build` over all packages: 71 / 71, through the shared
verify lock (`VERDICT command-exit 0`).
- `@objectstack/spec`:
  - `vitest run --project local`: 619 files, 18471 passed, 1 todo.
- `typecheck`: exit 0, including `check:test-typecheck` (52 files / 246
errors / 135 pinned signatures held). Its program holds all 16 group
files, counted by path with `tsc --listFilesOnly -p tsconfig.test.json`.
- `check:generated`: all 15 generated artifacts up to date, against the
`dist/` the build above wrote.
- **Gates:** `dispatch-gates --commands` derived 79 families, the same
set as stage 22, and all 79 exit 0. `--ran` reconciles: 79 derived, 79
run, 0 NOT-MEASURED, 0 UNRUN, every family with its exit code recorded.
The five roster families whose rosters sit under a touched directory
were also run, and each exits 0: `check:meta-url-spelling`,
`check:spec-changes`, `check:authz-resolver`, `check:error-code-casing`
and `check:filter-alias-parity`.
- **ESLint, a proven narrowing:** `--no-inline-config` over the 16 files
reads 0 errors and 0 warnings. The population comes from ESLint's own
config: 16 configured, 0 ignored. No file sets `parserOptions.project`
or `projectService`, so no untouched file's verdict can move.
- `check-governed-merges --test`: NOT governed, 200 changed lines (+100
/ -100).
- A control-byte scan over the 16 changed files finds none.

## `main` since the base

Re-fetched just before this PR opened, `origin/main` was four commits
past the base (`1f0469655f`: objectstack-ai#21939, objectstack-ai#21937, objectstack-ai#21943, objectstack-ai#21928). They touch
32 files, none of the 16; two are under `packages/spec` (a step-18
semantic migration entry and the migration registry, neither a test
file). So `main` was not merged. The census on that tree still reads 571
/ 604 in test files and 0 elsewhere. `git merge-tree` onto `1f0469655f`
is clean, and none of the 8 open PRs touches any of the 16 files.

## Acceptance notes

- **The `{{record.FIELD}}` title.**
`view-submit-redirect-url.test.ts:187` keeps its literal placeholder
after "ruled bullet 2 —"; this body spells it with `FIELD` because the
platform strips angle-bracket fragments from PR text.
- **Same-id test titles in this card's later stages** go with those
stages: 5 lines in `packages/spec/src`,
`api/api-error-code-type.test.ts:71` ("[objectstack-ai#19920] …"),
`stack.test.ts:1510` ("(objectstack-ai#17063)"), `system/stack-server.test.ts:93` and
`system/translation.test.ts:672` / `:767` ("(objectstack-ai#4001)").
- **Same-id test titles in other packages** stay: 46 lines in 11
packages (`metadata-protocol` 11, `objectql` 8, `spec/scripts` 8, `lint`
6, `rest` 4, `plugin-auth` 3, `cli` 2, and one each in
`plugin-security`, `plugin-sharing`, `qa/dogfood` and
`service-automation`), each package's share under the objectstack-ai#20513 lane
children. The three `plugin-auth` titles cite objectstack's objectstack-ai#5233, a
different record from `objectui#5233`.
- **Code comments with live ids** remain in these files and their
sources, among them the "§objectstack-ai#9933" cross-references above, the `[objectstack-ai#7741]` /
`[objectstack-ai#21180]` corpus notes in `view-union-branch-focus.test.ts` and
`view-union-diagnostics.test.ts`, and the `objectstack-ai#5055` banners in
`widget-i18n-retirement.test.ts`. Code comments are not this card's
share.

---

_Generated by [Claude
Code](https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ)_

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨ Set up Copilot instructions

3 participants