diff --git a/.changeset/18552-build-progress-docblock-provenance.md b/.changeset/18552-build-progress-docblock-provenance.md new file mode 100644 index 00000000000..3a26195311c --- /dev/null +++ b/.changeset/18552-build-progress-docblock-provenance.md @@ -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. diff --git a/content/docs/references/ai/build-progress.mdx b/content/docs/references/ai/build-progress.mdx index 039020e025b..3939d97cd4a 100644 --- a/content/docs/references/ai/build-progress.mdx +++ b/content/docs/references/ai/build-progress.mdx @@ -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 @@ -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. + **Source:** `packages/spec/src/ai/build-progress.zod.ts` diff --git a/packages/spec/src/ai/build-progress.test.ts b/packages/spec/src/ai/build-progress.test.ts index 3f48b90477b..9add52a3a9d 100644 --- a/packages/spec/src/ai/build-progress.test.ts +++ b/packages/spec/src/ai/build-progress.test.ts @@ -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 { @@ -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'); + }); +}); diff --git a/packages/spec/src/ai/build-progress.zod.ts b/packages/spec/src/ai/build-progress.zod.ts index 53034bc672c..d1f4bb211c4 100644 --- a/packages/spec/src/ai/build-progress.zod.ts +++ b/packages/spec/src/ai/build-progress.zod.ts @@ -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 * @@ -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 @@ -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'), /** @@ -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'), }));