Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .changeset/18552-build-progress-docblock-provenance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
'@objectstack/spec': patch
---

fix(spec): label every producer claim in the `build-progress` docblock — measured, ruled, or inferred (#18552)

Clause-②: no

The module docblock on `ai/build-progress.zod.ts` stated three producer claims
as MEASUREMENTS. It ships in this tarball (the published `files[]` carries the
`.zod.ts` sources) and is rendered verbatim into the generated reference page,
and for a CLOSED vocabulary it is the audit trail the "re-measure before you
move the array" discipline reads. One of the three was false, and a reader
deciding whether a fifth phase is warranted would have read all three as
readings.

Each producer claim now carries exactly one of three labels, defined at the top
of the module: **measured on a named reachable source**, **declared by ruling**,
or **inferred**.

- Membership is no longer described as uniformly measured. `structure`, `data`
and `done` stay **measured** — the objectui reader's own union and coercion
default, cited with the tree they were read against. `verify` is **declared by
ruling** (cloud#2172, objectui#7388): at the read tree the chat panel has zero
occurrences of `'verify'` against a control of four files for `'structure'`,
and this repository emits no frame at all. That is a good reason for the
member; it is not an observation, and the docblock no longer says it is.
- The cloud#1838 window — "111 seconds and 9 tool calls", "one of them
`verify_build`" — is **inferred**: that record is not reachable from this
repository, so the figure is carried, not measured, and which tools those
calls were is recorded nowhere reachable. What is measured is narrower and
stated as such: `verify_build` is a registered platform tool.
- "A turn that seeds no sample data never reports `data`" and "`apply_edit`
turns need not report `structure`" are **inferred**. The consumer guidance
around them is unchanged and does not rest on them: treat every phase as
optional and compare by value.

A new `## Liveness watch` section records that `verify`, `hop` and `tool` are
declared ahead of any code that uses them, that cloud#2172 and objectui#7388
block 2 are the named carriers meant to close that, and that no gate watches it
— `BuildProgressFrame` is not a registered metadata type, so the ADR-0049
liveness ledger never sees it.

No schema, export or parse behaviour moves: `BUILD_PROGRESS_PHASES`,
`BuildProgressPhaseSchema` and `BuildProgressFrameSchema` accept and refuse
exactly what they did before.
61 changes: 54 additions & 7 deletions content/docs/references/ai/build-progress.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,45 @@ description: Build Progress protocol schemas
Build-progress PHASE vocabulary for the `data-build-progress` stream frame
(cloud#2172 ruling A).

## How to read the claims in this module

This vocabulary is CLOSED, so whoever next asks whether a fifth member is
warranted has to re-measure before moving the array — and can only do that
if they can tell a reading from a ruling. Every producer claim below
therefore carries exactly one of three labels, and none is left bare:

- **measured on a named reachable source** — a repository and path the
reader can open and re-measure, cited with the tree it was read against.
- **declared by ruling** — a maintainer decision. The authority is the
ruling; ⛔ it is not evidence the behaviour exists yet.
- **inferred** — deduced from something reachable, or carried from a record
this repository cannot open. ⛔ Never re-cite it as a measurement.

## Producer

The cloud AI-studio **agent loop** — deliberately not the tool it just ran.
**Declared by ruling**: cloud#2172 ruling A owns WHERE in the loop a frame is
emitted; this module declares only what such a frame may SAY.

A build turn applies its change through `apply_blueprint` / `apply_edit` and
then keeps working: the loop spends a further POST-APPLY VERIFICATION window
re-reading and re-seeding what it wrote (measured on cloud#1838: 111 seconds
and 9 tool calls *after* `apply_blueprint` returned, one of them the
registered `verify_build` tool — see `PLATFORM_TOOLS_BY_PACKAGE` in
`../system/constants/platform-tool-names`). A tool's own `ctx.onProgress`
handle dies when the tool returns, so a frame emitted in that window can only
come from the loop. WHERE in the loop it is emitted is cloud#2172's decision,
not this module's; what such a frame may SAY is declared here.
re-reading and re-seeding what it wrote. **Inferred.** The only record of
that window is the cloud#1838 turn, reported there as 111 seconds and 9 tool
calls *after* `apply_blueprint` returned — and `objectstack-ai/cloud` is
outside the set of repositories a reader of this file can open, so the
figure is carried here, not measured, and cannot be re-measured from this
repo. WHICH tools those 9 calls were is recorded nowhere reachable either:
neither objectstack#18451 nor objectui#7388 names them, so "one of them ran
the registered `verify_build` tool" is **inferred** as well. What is
**measured on a named reachable source** is narrower than that — only that
`verify_build` is a registered platform tool, listed in
`PLATFORM_TOOLS_BY_PACKAGE` in `../system/constants/platform-tool-names`.

A tool's own `ctx.onProgress` handle dies when the tool returns, so a frame
emitted in that window can only come from the loop. **Inferred** from a
reachable contract: `AIToolContext.onProgress` in `../contracts/ai-service`
declares the emit as happening WHILE the tool executes, before it returns;
that the handle is gone afterwards is the deduction, not the declaration.

## Consumer

Expand All @@ -40,6 +67,26 @@ Unchanged: the `data-`-prefixed custom part described on
under a stable part id. This module adds the vocabulary that prose has always
assumed and never declared; it moves no transport and renames nothing.

## Liveness watch — declared ahead, and ⛔ nothing gates it

Three surfaces here are declared ahead of any code that uses them: the
`verify` phase, and the frame's `hop` and `tool` fields. Across every
repository reachable from here they have zero emitters and zero readers
(**measured on named reachable sources** — each reading is recorded in the
source beside its surface: on `BUILD_PROGRESS_PHASES` for the phase, and on
the `hop` / `tool` field declarations). They stand on a ruling instead
(**declared by ruling**): cloud#2172 ruled the vocabulary in, and
objectui#7388 asked the panel to be able to name the phase. That is a good
reason, and it is not a measurement.

⛔ No gate watches this. The ADR-0049 liveness ledger is rooted in the
metadata-type registry, and `BuildProgressFrame` is not a registered
metadata type, so the liveness job stays green however long these three go
unused. Two named carriers are meant to close it — cloud#2172 for the
emitter, objectui#7388 block 2 for the consumer's strict parse. If neither
lands, the three become enforce-or-remove candidates with nothing watching
them, and the only thing that notices is a person reading this paragraph.

<Callout type="info">
**Source:** `packages/spec/src/ai/build-progress.zod.ts`
</Callout>
Expand Down
57 changes: 57 additions & 0 deletions packages/spec/src/ai/build-progress.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
* someone re-measured — not an edit to wave through.
*/

import { readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

import { describe, it, expect } from 'vitest';

import {
Expand Down Expand Up @@ -180,3 +184,56 @@ describe('BuildProgressPhase (type)', () => {
expect(everyPhase).toHaveLength(BUILD_PROGRESS_PHASES.length);
});
});

/**
* The provenance leg — and the one leg whose subject is TEXT on purpose.
*
* This module's docblock is not incidental prose. The published
* `@objectstack/spec` tarball ships the `.zod.ts` sources themselves, and the
* same text is rendered verbatim into
* `content/docs/references/ai/build-progress.mdx`. For a CLOSED vocabulary it
* is the audit trail the "re-measure before you move the array" discipline
* reads: a reader who cannot tell a MEASUREMENT from a RULING re-cites the
* ruling as evidence, and the next member goes in on a claim nobody ever made.
*
* So what is pinned here is narrow and load-bearing: that the three labels are
* defined, that `verify` is labelled as the ruling it is rather than as a
* measurement, and that the liveness watch is still present — ⛔ not the
* wording around any of them. Nothing else in this file asserts on text.
*/
describe('module docblock provenance', () => {
const SOURCE = readFileSync(
resolve(dirname(fileURLToPath(import.meta.url)), 'build-progress.zod.ts'),
'utf-8',
);

it('defines all three provenance labels', () => {
for (const label of ['measured on a named reachable source', 'declared by ruling', 'inferred']) {
expect(SOURCE).toContain(label);
}
});

it('labels `verify` as declared by ruling, and ⛔ never as measured membership', () => {
// The bullet itself, not the surrounding paragraph: `verify` is in the
// enum because cloud#2172 ruled it in, which is a good reason and is not
// an observation of any producer or consumer.
const bullet = SOURCE.slice(SOURCE.indexOf(' * - `verify`'));
expect(bullet.slice(0, 200)).toContain('**declared by ruling**');

// The retired headline claimed every member was measured against a real
// end of the channel. It was false for `verify` on the day it was written.
expect(SOURCE).not.toContain('Membership was MEASURED');
expect(SOURCE).not.toContain('every member below is one a real producer emits');
});

it('keeps the liveness watch for the three declared-ahead surfaces', () => {
expect(SOURCE).toContain('## Liveness watch');
for (const surface of ['`verify` phase', '`hop`', '`tool`']) {
expect(SOURCE).toContain(surface);
}
// The point of the watch: no gate notices, so the paragraph is the notice.
expect(SOURCE).toContain('ADR-0049');
expect(SOURCE).toContain('cloud#2172');
expect(SOURCE).toContain('objectui#7388 block 2');
});
});
112 changes: 90 additions & 22 deletions packages/spec/src/ai/build-progress.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,45 @@ import { lazySchema } from '../shared/lazy-schema';
* Build-progress PHASE vocabulary for the `data-build-progress` stream frame
* (cloud#2172 ruling A).
*
* ## How to read the claims in this module
*
* This vocabulary is CLOSED, so whoever next asks whether a fifth member is
* warranted has to re-measure before moving the array — and can only do that
* if they can tell a reading from a ruling. Every producer claim below
* therefore carries exactly one of three labels, and none is left bare:
*
* - **measured on a named reachable source** — a repository and path the
* reader can open and re-measure, cited with the tree it was read against.
* - **declared by ruling** — a maintainer decision. The authority is the
* ruling; ⛔ it is not evidence the behaviour exists yet.
* - **inferred** — deduced from something reachable, or carried from a record
* this repository cannot open. ⛔ Never re-cite it as a measurement.
*
* ## Producer
*
* The cloud AI-studio **agent loop** — deliberately not the tool it just ran.
* **Declared by ruling**: cloud#2172 ruling A owns WHERE in the loop a frame is
* emitted; this module declares only what such a frame may SAY.
*
* A build turn applies its change through `apply_blueprint` / `apply_edit` and
* then keeps working: the loop spends a further POST-APPLY VERIFICATION window
* re-reading and re-seeding what it wrote (measured on cloud#1838: 111 seconds
* and 9 tool calls *after* `apply_blueprint` returned, one of them the
* registered `verify_build` tool — see `PLATFORM_TOOLS_BY_PACKAGE` in
* `../system/constants/platform-tool-names`). A tool's own `ctx.onProgress`
* handle dies when the tool returns, so a frame emitted in that window can only
* come from the loop. WHERE in the loop it is emitted is cloud#2172's decision,
* not this module's; what such a frame may SAY is declared here.
* re-reading and re-seeding what it wrote. **Inferred.** The only record of
* that window is the cloud#1838 turn, reported there as 111 seconds and 9 tool
* calls *after* `apply_blueprint` returned — and `objectstack-ai/cloud` is
* outside the set of repositories a reader of this file can open, so the
* figure is carried here, not measured, and cannot be re-measured from this
* repo. WHICH tools those 9 calls were is recorded nowhere reachable either:
* neither objectstack#18451 nor objectui#7388 names them, so "one of them ran
* the registered `verify_build` tool" is **inferred** as well. What is
* **measured on a named reachable source** is narrower than that — only that
* `verify_build` is a registered platform tool, listed in
* `PLATFORM_TOOLS_BY_PACKAGE` in `../system/constants/platform-tool-names`.
*
* A tool's own `ctx.onProgress` handle dies when the tool returns, so a frame
* emitted in that window can only come from the loop. **Inferred** from a
* reachable contract: `AIToolContext.onProgress` in `../contracts/ai-service`
* declares the emit as happening WHILE the tool executes, before it returns;
* that the handle is gone afterwards is the deduction, not the declaration.
*
* ## Consumer
*
Expand All @@ -38,31 +65,64 @@ import { lazySchema } from '../shared/lazy-schema';
* `AIToolContext.onProgress` in `../contracts/ai-service`, reconciled in place
* under a stable part id. This module adds the vocabulary that prose has always
* assumed and never declared; it moves no transport and renames nothing.
*
* ## Liveness watch — declared ahead, and ⛔ nothing gates it
*
* Three surfaces here are declared ahead of any code that uses them: the
* `verify` phase, and the frame's `hop` and `tool` fields. Across every
* repository reachable from here they have zero emitters and zero readers
* (**measured on named reachable sources** — each reading is recorded in the
* source beside its surface: on `BUILD_PROGRESS_PHASES` for the phase, and on
* the `hop` / `tool` field declarations). They stand on a ruling instead
* (**declared by ruling**): cloud#2172 ruled the vocabulary in, and
* objectui#7388 asked the panel to be able to name the phase. That is a good
* reason, and it is not a measurement.
*
* ⛔ No gate watches this. The ADR-0049 liveness ledger is rooted in the
* metadata-type registry, and `BuildProgressFrame` is not a registered
* metadata type, so the liveness job stays green however long these three go
* unused. Two named carriers are meant to close it — cloud#2172 for the
* emitter, objectui#7388 block 2 for the consumer's strict parse. If neither
* lands, the three become enforce-or-remove candidates with nothing watching
* them, and the only thing that notices is a person reading this paragraph.
*/

/**
* The closed phase vocabulary, in emission-lifecycle order.
*
* Membership was MEASURED against the two ends of the channel, not designed —
* every member below is one a real producer emits or a real consumer already
* discriminates:
* Membership is MIXED-PROVENANCE, not uniformly measured. Each member carries
* its own label (the three are defined at the top of this module), because the
* question a re-measure has to answer is which members a real end of the
* channel already uses and which are standing on a ruling alone:
*
* - `structure`, `data`, `done` — the consumer's own declared union,
* - `structure`, `data`, `done` — **measured on a named reachable source**:
* the consumer's own declared union,
* `ChatBuildProgress['phase']` at `packages/plugin-chatbot/src/ChatbotEnhanced.tsx:163`
* in objectui, and the set its reader discriminates at
* `packages/plugin-chatbot/src/mapMessages.ts:744`
* `packages/plugin-chatbot/src/mapMessages.ts:743`
* (`d.phase === 'data' || d.phase === 'done' ? d.phase : 'structure'`).
* `structure` doubles as that reader's coercion default.
* - `verify` — the post-apply verification window objectui#7388 asks the panel
* to be able to name, and the reason cloud#2172 ruled this vocabulary into
* the spec. Corroborated in this repo by the `verify_build` tool that
* `service-ai-studio` actually registers
* (`../system/constants/platform-tool-names`).
* `structure` doubles as that reader's coercion default. Read at objectui
* `dda8f3815`; line numbers drift, the two symbols are the anchor.
* - `verify` — **declared by ruling**, ⛔ not measured: the post-apply
* verification window objectui#7388 asks the panel to be able to name, and
* the reason cloud#2172 ruled this vocabulary into the spec. At objectui
* `dda8f3815` `packages/plugin-chatbot` has ZERO occurrences of `'verify'`
* (bright control, same instrument: `'structure'` hits 4 files there), and
* this repository emits no frame at all, so `verify` has no reachable
* emitter and no reachable reader today. **Measured on a named reachable
* source** alongside it, and narrower than a corroboration of the phase:
* `service-ai-studio` registers a `verify_build` tool
* (`../system/constants/platform-tool-names`). See the liveness watch at the
* top of this module.
*
* Order is the order a build turn passes through these states; it is NOT a
* scale. ⛔ Consumers compare phases by VALUE — never by index, and never by
* assuming every phase occurs (a turn that applies no seed data never reports
* `data`, and `apply_edit` turns need not report `structure`).
* assuming every phase occurs. The two illustrations of a phase being skipped
* — a turn that applies no seed data never reporting `data`, and `apply_edit`
* turns not reporting `structure` — are **inferred**: no reachable repository
* records either, and cloud#2172's emitter has not landed. ⛔ The guidance
* does not rest on them; treat every phase as optional whatever a producer
* turns out to do.
*
* Exported as an array as well as a schema so a consumer can build an
* exhaustive per-phase label map without forcing the lazy schema to
Expand Down Expand Up @@ -107,13 +167,18 @@ export const BuildProgressFrameSchema = lazySchema(() => z.looseObject({
phase: BuildProgressPhaseSchema.describe('Build-turn phase this frame reports'),
/**
* Which hop of the post-apply verification loop this frame is on — the
* counter behind the "9 tool calls" cloud#1838 measured, so the panel can
* show motion through a window that is otherwise a single flat phase.
* counter behind the "9 tool calls" the cloud#1838 turn record reports, so
* the panel can show motion through a window that is otherwise a single flat
* phase. **Inferred** that a producer counts hops at all: that record is not
* reachable from this repository (see the Producer section).
*
* Typed as a non-negative integer rather than pinned to a base: whether the
* loop counts its first hop as 0 or 1 is part of the emitter's placement,
* which cloud#2172 owns and this card does not decide. ⛔ Consumers render it
* as progress, never as an index into anything here.
*
* ⛔ Zero emitters and zero readers in every reachable repository at this
* tree — see the liveness watch at the top of this module.
*/
hop: z.number().int().nonnegative().optional().describe('Post-apply verification hop this frame reports'),
/**
Expand All @@ -123,6 +188,9 @@ export const BuildProgressFrameSchema = lazySchema(() => z.looseObject({
* registered at boot and legitimately includes plugin-contributed names that
* `PLATFORM_PROVIDED_TOOL_NAMES` cannot know about
* (`../system/constants/platform-tool-names`). Consumers treat it as a label.
*
* ⛔ Zero emitters and zero readers in every reachable repository at this
* tree — see the liveness watch at the top of this module.
*/
tool: z.string().min(1).optional().describe('Name of the tool the current hop is running'),
}));
Expand Down
Loading