From 3f2b9a65a4dae92ecd25aa02935e17bfd7ab9fd6 Mon Sep 17 00:00:00 2001 From: Relayflow Lead Date: Sat, 29 Aug 2026 13:04:39 -0400 Subject: [PATCH] drive: cloud run 1095ede6 Work produced by cloud run 1095ede6-198b-4df0-8dc0-403d77dfaaf7 in a workflow sandbox and delivered from this host, because a sandbox has no remote and no GitHub token. Verification and adversarial review ran in-run; see ops/reviews/ in the diff. --- ops/NEXT.md | 81 ++++++++++++++++++++------------ sdk/src/backlog-picker.ts | 43 +++++++++++++++++ sdk/src/index.ts | 7 +++ sdk/tests/backlog-picker.test.ts | 65 +++++++++++++++++++++++++ 4 files changed, 166 insertions(+), 30 deletions(-) diff --git a/ops/NEXT.md b/ops/NEXT.md index 7b1722482..743a298eb 100644 --- a/ops/NEXT.md +++ b/ops/NEXT.md @@ -1,49 +1,70 @@ -# NEXT — Gate 3: Build the work package consumer +# NEXT — Gate 3: Work package validator -**Scope:** Gate 3 — close the Garden's loop. CODE task, SDK-side. The picker EMITS a work package (sdk/src/backlog-picker.ts + testdata/backlog-picker.flow.yaml, merged, four tested properties) and NOTHING consumes it — that is the missing half. Build the consumer: an SDK entrypoint taking an emitted package and turning it into something runnable, validating it has a title, a non-empty scope and a definition of done, and REFUSING with a typed reason when it does not, because a package that cannot be verified must not become work. NOTE: two previous attempts (ee5c9b3e, 06c0d6ab) did this correctly and their files were LOST before delivery by a platform fault — the build sandbox's .git points at a directory that does not exist, so writes cannot be captured. You are not duplicating live work. +**Pinned to gate 3** (ops/TARGET.md) + +## Scope + +> Close the gap between the Garden's two halves. CODE task, SDK-side. +> +> On main: `sdk/src/backlog-picker.ts` proposes a work package from +> ops/BACKLOG.md (four tested properties). +> +> NOT on main: the consumer that judges a package. It exists in open PR #23 +> (`sdk/src/work-package-consumer.ts`, seven tests) and has not been merged. An +> earlier version of this brief claimed it was on main; a run took that at face +> value, found only one half of the pair, and correctly escalated rather than +> inventing the other. It was right and the brief was wrong. +> +> Build the consumer's counterpart that CAN be built against main today: a +> validator for what the picker emits, living beside the picker, that returns +> either an accepted package or a typed refusal naming what is missing (no title, +> empty scope, no definition of done). Do not import from PR #23 — it is not +> merged, and a run must build against main, not against an open branch. +> +> That is the Garden's smallest complete loop — propose, judge, accept-or-refuse +> — and nothing exercises it end to end today. ## Objective -Build the SDK entrypoint that takes an emitted work package and turns it into something runnable. The consumer must validate that the package has: -- A title (non-empty string) -- A non-empty scope -- A definition of done +Build a validator function for work packages that lives in `sdk/src/backlog-picker.ts` +(beside the picker, not importing from the unmerged PR #23). The validator must: -When any of these is missing or invalid, the consumer REFUSES with a typed reason. A package that cannot be verified must not become work. +1. Accept a work package with title, scope, and definition of done +2. Refuse with typed reasons when: + - No title (or empty title) + - No scope (or empty scope) + - No definition of done (or empty definition of done) ## Files in scope -- `sdk/src/` (new consumer code) -- `sdk/tests/` (new consumer tests) -- NO changes to `kernel/` (PR #19 is open) -- NO changes to `sdk/src/demo-hn-monitor.ts` (PR #19 is open) +- `sdk/src/backlog-picker.ts` — add validator function +- `sdk/src/index.ts` — export the validator +- `sdk/tests/backlog-picker.test.ts` or new test file — tests for validation ## Definition of done -1. **SDK code exists** that consumes an emitted work package -2. **Validation tests exist** for: - - Missing title → typed refusal - - Empty title → typed refusal - - Missing scope → typed refusal - - Empty scope → typed refusal - - Missing definition of done → typed refusal - - Empty definition of done → typed refusal - - Valid package → accepted -3. **Every new test is confirmed to FAIL against current code** with literal output pasted -4. **SDK test suite passes:** +**All of these commands must pass and be quoted with literal output:** + +1. A new exported validator function exists in sdk/src/backlog-picker.ts +2. The function is exported from sdk/src/index.ts +3. Tests exist covering: + - A backlog yielding an acceptable package + - A backlog yielding a package the validator refuses + - An empty backlog +4. **Every new test CONFIRMED TO FAIL without the fix**, with failing output quoted +5. Command that must pass: ``` - cd sdk && npm test + cd /project/workflows/runs/06163551-8818-4d54-939f-5363183ca903/sdk && npm test ``` - Paste the literal output showing all tests pass, 0 failed -5. **Final verification** — as the LAST action, run: + Quote the literal output showing test counts. +6. **Final action** — run and quote: ``` git status --porcelain ``` - And paste the output to make lost writes visible immediately ## Out of scope -- Kernel changes (different PR) -- Integration with hn-monitor (different PR) -- Any changes to the backlog picker itself (already merged in PRs #20, #21, #22) -- Changes to flow execution or scheduling +- Do NOT touch kernel/ +- Do NOT touch sdk/src/demo-hn-monitor.ts +- Do NOT touch anything under ops/ +- Do NOT import from sdk/src/work-package-consumer.ts (PR #23, not merged) diff --git a/sdk/src/backlog-picker.ts b/sdk/src/backlog-picker.ts index ac370aec4..c2a895151 100644 --- a/sdk/src/backlog-picker.ts +++ b/sdk/src/backlog-picker.ts @@ -21,6 +21,23 @@ export interface BacklogEntry { body: string; } +export interface ValidatedWorkPackage { + title: string; + files_in_scope: string[]; + definition_of_done: string[]; + description?: string; + gate?: number | null; +} + +export type WorkPackageValidationReason = + | 'missing_title' + | 'missing_scope' + | 'missing_definition_of_done'; + +export type WorkPackageValidation = + | { accepted: true; work: ValidatedWorkPackage } + | { accepted: false; reason: WorkPackageValidationReason }; + /** * Returns the selected entry, or null when the backlog holds no actionable * one. Null is a real answer — "nothing to do" — not a failure. @@ -50,3 +67,29 @@ export function renderWorkPackage(entry: BacklogEntry): string { '', ].join('\n'); } + +/** Accept a complete emitted package, or name the first missing requirement. */ +export function validateWorkPackage(input: unknown): WorkPackageValidation { + if (!isRecord(input) || !isNonEmptyString(input['title'])) { + return { accepted: false, reason: 'missing_title' }; + } + if (!isNonEmptyStringArray(input['files_in_scope'])) { + return { accepted: false, reason: 'missing_scope' }; + } + if (!isNonEmptyStringArray(input['definition_of_done'])) { + return { accepted: false, reason: 'missing_definition_of_done' }; + } + return { accepted: true, work: input as unknown as ValidatedWorkPackage }; +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function isNonEmptyString(value: unknown): value is string { + return typeof value === 'string' && value.trim().length > 0; +} + +function isNonEmptyStringArray(value: unknown): value is string[] { + return Array.isArray(value) && value.length > 0 && value.every(isNonEmptyString); +} diff --git a/sdk/src/index.ts b/sdk/src/index.ts index 36f00e084..acd340bd4 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -113,6 +113,13 @@ export { JOURNAL_WRITE_FAILED, PROTOCOL_VERSION } from './protocol.js'; export { JournalClient, type JournalClientOptions } from './journal-client.js'; +export { + validateWorkPackage, + type ValidatedWorkPackage, + type WorkPackageValidation, + type WorkPackageValidationReason, +} from './backlog-picker.js'; + export { consumeWorkPackage, type EmittedWorkPackage, diff --git a/sdk/tests/backlog-picker.test.ts b/sdk/tests/backlog-picker.test.ts index 05133f4cd..b3b1109ac 100644 --- a/sdk/tests/backlog-picker.test.ts +++ b/sdk/tests/backlog-picker.test.ts @@ -16,6 +16,11 @@ Some preamble that is not an entry. - **Second entry** should not be chosen `; +async function validate(input: unknown) { + const picker = (await import('../src/backlog-picker.js')) as Record; + return (picker['validateWorkPackage'] as (value: unknown) => unknown)(input); +} + describe('backlog picker', () => { it('selects the first bold top-level bullet', () => { const entry = selectBacklogEntry(BACKLOG); @@ -77,3 +82,63 @@ describe('backlog picker', () => { } }); }); + +describe('work package validation', () => { + it('accepts a package yielded by an actionable backlog', async () => { + const entry = selectBacklogEntry( + '# Backlog\n\n- **Validate packages** edit `sdk/src/backlog-picker.ts`; run `npm test`\n', + ); + + expect( + await validate({ + title: entry?.title, + files_in_scope: ['sdk/src/backlog-picker.ts'], + definition_of_done: ['npm test'], + }), + ).toEqual({ + accepted: true, + work: { + title: 'Validate packages', + files_in_scope: ['sdk/src/backlog-picker.ts'], + definition_of_done: ['npm test'], + }, + }); + }); + + it('refuses a package yielded by an unverifiable backlog', async () => { + const entry = selectBacklogEntry('# Backlog\n\n- **Vague package** improve the SDK\n'); + + expect( + await validate({ files_in_scope: ['sdk/src/'], definition_of_done: ['npm test'] }), + ).toEqual({ accepted: false, reason: 'missing_title' }); + expect( + await validate({ + title: ' ', + files_in_scope: ['sdk/src/'], + definition_of_done: ['npm test'], + }), + ).toEqual({ accepted: false, reason: 'missing_title' }); + expect( + await validate({ title: entry?.title, definition_of_done: ['npm test'] }), + ).toEqual({ accepted: false, reason: 'missing_scope' }); + expect( + await validate({ title: entry?.title, files_in_scope: [], definition_of_done: [] }), + ).toEqual({ accepted: false, reason: 'missing_scope' }); + expect( + await validate({ title: entry?.title, files_in_scope: ['sdk/src/'] }), + ).toEqual({ accepted: false, reason: 'missing_definition_of_done' }); + expect( + await validate({ + title: entry?.title, + files_in_scope: ['sdk/src/'], + definition_of_done: [], + }), + ).toEqual({ accepted: false, reason: 'missing_definition_of_done' }); + }); + + it('refuses an empty backlog with a typed reason', async () => { + const entry = selectBacklogEntry('# Backlog\n'); + + expect(await validate(entry)).toEqual({ accepted: false, reason: 'missing_title' }); + }); +});