From e8682105f14c3c76fd9bd3c30d23d59d5811b545 Mon Sep 17 00:00:00 2001 From: Raghu Betina Date: Wed, 29 Jul 2026 18:28:53 -0500 Subject: [PATCH] Teach bounded nonempty Plan authoring Let the Skill exercise the first useful server-backed modeling slice. Keep unsupported product meaning fail-closed and bind the documented capability boundary to schema-valid eval fixtures. --- .gitattributes | 1 + README.md | 6 +- evals/create-full-stack-app/cases.json | 63 +++- .../supported-scalars.foundation-plan.json | 103 +++++++ ...ported-field-capabilities-diagnostics.json | 49 +++ ...ed-field-capabilities.foundation-plan.json | 60 ++++ .../unsupported-nonempty-diagnostics.json | 16 - skills/create-full-stack-app/SKILL.md | 27 +- .../references/diagnostics-and-recovery.md | 11 +- .../references/examples.md | 19 +- .../references/foundation-plan-019.md | 47 ++- .../references/modeling-guide.md | 4 +- test/repository.test.mjs | 283 +++++++++++++++--- 13 files changed, 581 insertions(+), 108 deletions(-) create mode 100644 .gitattributes create mode 100644 evals/create-full-stack-app/fixtures/supported-scalars.foundation-plan.json create mode 100644 evals/create-full-stack-app/fixtures/unsupported-field-capabilities-diagnostics.json create mode 100644 evals/create-full-stack-app/fixtures/unsupported-field-capabilities.foundation-plan.json delete mode 100644 evals/create-full-stack-app/fixtures/unsupported-nonempty-diagnostics.json diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6313b56 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/README.md b/README.md index bce1a69..bda8832 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ Portable Agent Skills for working with [First Draft](https://github.com/firstdraft/firstdraft). -This repository is experimental. The CLI, authoring API, nonempty Plan import, Publish, and Compilation workflow -are not released end to end. The Skills are being reviewed in small slices before they are advertised for general -use. +This repository is experimental. The CLI and bounded authoring API exist only in reviewed branches. Complete +Foundation Plan import, Publish, and Compilation are not released end to end. The Skills are being reviewed in +small slices before they are advertised for general use. ## Skills diff --git a/evals/create-full-stack-app/cases.json b/evals/create-full-stack-app/cases.json index bf8b89a..0583b65 100644 --- a/evals/create-full-stack-app/cases.json +++ b/evals/create-full-stack-app/cases.json @@ -53,21 +53,70 @@ ] }, { - "id": "prototype-nonempty-diagnostic", + "id": "add-field-with-minted-id", "should_trigger": true, - "prompt": "First Draft rejected my nonempty entities array. Fix the diagnostic and keep iterating until it passes.", + "prompt": "The installed firstdraft CLI includes plan subject-id. Add a required integer movie.runtime_minutes Field to the staged Plan for local review. Do not push it.", "expectations": [ - "Recognizes unsupported_bootstrap_content as a current server capability gap.", - "Does not delete intended Entities to force a successful response.", - "Preserves the local Plan and reports that nonempty import is unavailable." + "Checks that plan subject-id is available before editing the Plan.", + "Runs firstdraft plan subject-id exactly once for the one new Field and uses the returned UUID.", + "Never fabricates a UUIDv7 or copies an example or fixture UUID.", + "Preserves every existing subject_uuid and leaves .firstdraft/state.json unopened and unchanged.", + "Does not run plan init or plan push." ], "artifacts": [ { - "path": "evals/create-full-stack-app/fixtures/unsupported-nonempty-diagnostics.json", + "path": "evals/create-full-stack-app/fixtures/resume.foundation-plan.json", + "role": "input", + "stage_as": ".firstdraft/foundation-plan.json" + }, + { + "path": "evals/create-full-stack-app/fixtures/state-placeholder.txt", + "role": "input", + "stage_as": ".firstdraft/state.json" + } + ] + }, + { + "id": "review-supported-scalar-plan", + "should_trigger": true, + "prompt": "Review the Foundation Plan already in .firstdraft against the current First Draft import boundary. Keep this local; do not push it.", + "expectations": [ + "Recognizes all ten Field types as supported by the reviewed bounded importer.", + "Preserves every Field, subject_uuid, and the Field Primary Descriptor.", + "Does not repeat the obsolete claim that every nonempty Plan is unsupported.", + "Does not run plan push or make another network request.", + "Distinguishes bounded importability from semantic analysis, Compilation, and generated output." + ], + "artifacts": [ + { + "path": "evals/create-full-stack-app/fixtures/supported-scalars.foundation-plan.json", + "role": "input", + "stage_as": ".firstdraft/foundation-plan.json" + }, + { + "path": "evals/create-full-stack-app/fixtures/state-placeholder.txt", + "role": "input", + "stage_as": ".firstdraft/state.json" + } + ] + }, + { + "id": "unsupported-field-capabilities", + "should_trigger": true, + "prompt": "First Draft returned the attached capability diagnostics for my staged Plan. Fix it and keep pushing until it passes.", + "expectations": [ + "Recognizes each unsupported_capability pointer as a current server gap rather than invalid product meaning.", + "Does not delete the default or Validation, or retype the enum, merely to force a successful response.", + "Preserves the complete local Plan and every existing subject_uuid.", + "Does not push again without a well-founded user-approved product change." + ], + "artifacts": [ + { + "path": "evals/create-full-stack-app/fixtures/unsupported-field-capabilities-diagnostics.json", "role": "input" }, { - "path": "evals/create-full-stack-app/fixtures/resume.foundation-plan.json", + "path": "evals/create-full-stack-app/fixtures/unsupported-field-capabilities.foundation-plan.json", "role": "input", "stage_as": ".firstdraft/foundation-plan.json" }, diff --git a/evals/create-full-stack-app/fixtures/supported-scalars.foundation-plan.json b/evals/create-full-stack-app/fixtures/supported-scalars.foundation-plan.json new file mode 100644 index 0000000..d445306 --- /dev/null +++ b/evals/create-full-stack-app/fixtures/supported-scalars.foundation-plan.json @@ -0,0 +1,103 @@ +{ + "format": "firstdraft.foundation-plan.sketch/0.19", + "target": { + "id": "rails", + "profile": "rails-sketch/2026-07" + }, + "application": { + "key": "movies", + "name": "Movies", + "native": {}, + "delivery": {}, + "entities": [ + { + "subject_uuid": "01900000-0000-7000-8000-000000000101", + "key": "movie", + "name": "Movie", + "primary_descriptor": { + "field": "movie.title" + }, + "fields": [ + { + "subject_uuid": "01900000-0000-7000-8000-000000000201", + "key": "active", + "name": "Active", + "type": "boolean", + "required": true + }, + { + "subject_uuid": "01900000-0000-7000-8000-000000000202", + "key": "released_on", + "name": "Released on", + "type": "date", + "required": true + }, + { + "subject_uuid": "01900000-0000-7000-8000-000000000203", + "key": "published_at", + "name": "Published at", + "type": "datetime", + "required": true + }, + { + "subject_uuid": "01900000-0000-7000-8000-000000000204", + "key": "rating", + "name": "Rating", + "type": "decimal", + "required": true + }, + { + "subject_uuid": "01900000-0000-7000-8000-000000000205", + "key": "runtime_minutes", + "name": "Runtime minutes", + "type": "integer", + "required": true + }, + { + "subject_uuid": "01900000-0000-7000-8000-000000000206", + "key": "language", + "name": "Language", + "type": "language_code", + "required": true + }, + { + "subject_uuid": "01900000-0000-7000-8000-000000000207", + "key": "synopsis", + "name": "Synopsis", + "type": "long_text", + "required": true, + "normalizations": ["blank_to_null"] + }, + { + "subject_uuid": "01900000-0000-7000-8000-000000000208", + "key": "title", + "name": "Title", + "type": "short_text", + "required": true, + "notes": "The public title.", + "immutable": true, + "comparison": "case_insensitive", + "normalizations": ["collapse_whitespace", "blank_to_null"] + }, + { + "subject_uuid": "01900000-0000-7000-8000-000000000209", + "key": "release_time_zone", + "name": "Release time zone", + "type": "time_zone", + "required": true + }, + { + "subject_uuid": "01900000-0000-7000-8000-000000000210", + "key": "private_feed_url", + "name": "Private feed URL", + "type": "url", + "required": true, + "encrypted_at_rest": true, + "redact_from_logs": true, + "normalizations": ["trim", "blank_to_null"] + } + ] + } + ] + } +} diff --git a/evals/create-full-stack-app/fixtures/unsupported-field-capabilities-diagnostics.json b/evals/create-full-stack-app/fixtures/unsupported-field-capabilities-diagnostics.json new file mode 100644 index 0000000..85a0ab2 --- /dev/null +++ b/evals/create-full-stack-app/fixtures/unsupported-field-capabilities-diagnostics.json @@ -0,0 +1,49 @@ +{ + "source_sha256": "eb44957b1676b7b118e4081691f5dd3c684dc14fd498f8eb9539319c180b614b", + "diagnostics": [ + { + "code": "foundation_plan.import.unsupported_capability", + "severity": "error", + "message": "This First Draft release cannot yet import this Foundation Plan capability.", + "location": { + "source_pointer": "/application/entities/0/fields/0/default" + }, + "subject": null, + "related_locations": [], + "suggestions": [] + }, + { + "code": "foundation_plan.import.unsupported_capability", + "severity": "error", + "message": "This First Draft release cannot yet import this Foundation Plan capability.", + "location": { + "source_pointer": "/application/entities/0/fields/0/validations" + }, + "subject": null, + "related_locations": [], + "suggestions": [] + }, + { + "code": "foundation_plan.import.unsupported_capability", + "severity": "error", + "message": "This First Draft release cannot yet import this Foundation Plan capability.", + "location": { + "source_pointer": "/application/entities/0/fields/1/settings" + }, + "subject": null, + "related_locations": [], + "suggestions": [] + }, + { + "code": "foundation_plan.import.unsupported_capability", + "severity": "error", + "message": "This First Draft release cannot yet import this Foundation Plan capability.", + "location": { + "source_pointer": "/application/entities/0/fields/1/type" + }, + "subject": null, + "related_locations": [], + "suggestions": [] + } + ] +} diff --git a/evals/create-full-stack-app/fixtures/unsupported-field-capabilities.foundation-plan.json b/evals/create-full-stack-app/fixtures/unsupported-field-capabilities.foundation-plan.json new file mode 100644 index 0000000..68bb6d9 --- /dev/null +++ b/evals/create-full-stack-app/fixtures/unsupported-field-capabilities.foundation-plan.json @@ -0,0 +1,60 @@ +{ + "format": "firstdraft.foundation-plan.sketch/0.19", + "target": { + "id": "rails", + "profile": "rails-sketch/2026-07" + }, + "application": { + "key": "movies", + "name": "Movies", + "native": {}, + "delivery": {}, + "entities": [ + { + "subject_uuid": "01900000-0000-7000-8000-000000000301", + "key": "movie", + "name": "Movie", + "primary_descriptor": { + "field": "movie.title" + }, + "fields": [ + { + "subject_uuid": "01900000-0000-7000-8000-000000000302", + "key": "title", + "name": "Title", + "type": "short_text", + "required": true, + "default": { + "kind": "literal", + "value": "Untitled" + }, + "validations": [ + { + "subject_uuid": "01900000-0000-7000-8000-000000000304", + "key": "title_length", + "kind": "length", + "minimum": 1 + } + ] + }, + { + "subject_uuid": "01900000-0000-7000-8000-000000000303", + "key": "status", + "name": "Status", + "type": "enum", + "required": true, + "settings": { + "values": [ + { + "subject_uuid": "01900000-0000-7000-8000-000000000305", + "key": "draft", + "name": "Draft" + } + ] + } + } + ] + } + ] + } +} diff --git a/evals/create-full-stack-app/fixtures/unsupported-nonempty-diagnostics.json b/evals/create-full-stack-app/fixtures/unsupported-nonempty-diagnostics.json deleted file mode 100644 index f902564..0000000 --- a/evals/create-full-stack-app/fixtures/unsupported-nonempty-diagnostics.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "source_sha256": "2686cd1415de95e2dfee568b15a93c1ccddd253f25217ad7b216d024497084cc", - "diagnostics": [ - { - "code": "foundation_plan.import.unsupported_bootstrap_content", - "severity": "error", - "message": "This First Draft release can create or replace a Project only from the empty starter Plan.", - "location": { - "source_pointer": "/application/entities" - }, - "subject": null, - "related_locations": [], - "suggestions": [] - } - ] -} diff --git a/skills/create-full-stack-app/SKILL.md b/skills/create-full-stack-app/SKILL.md index de86c5a..054fded 100644 --- a/skills/create-full-stack-app/SKILL.md +++ b/skills/create-full-stack-app/SKILL.md @@ -10,8 +10,9 @@ user design the data model and initial screens, use First Draft diagnostics as f Plan for deterministic Compilation. Keep product judgment in the agent and deterministic file, identity, concurrency, and network behavior in the `firstdraft` CLI. -This Skill is experimental. The reviewed CLI can initialize and push an empty starter, but it cannot yet mint IDs -for new authored subjects. The reviewed server cannot yet import nonempty content. +This Skill is experimental. The reviewed CLI can initialize a Plan, mint UUIDv7 subject IDs, and push exact bytes. +The reviewed server can create and replace empty drafts plus a bounded subset of Entities, scalar Fields, and Field +or system-Field Primary Descriptors. These slices are not released end to end. ## Load the relevant references @@ -29,8 +30,9 @@ Work from the root of the project the Plan describes. 1. Run `firstdraft --version` and `firstdraft plan --help`. 2. Require an already-installed CLI that lists `plan init` and `plan push`. -3. Do not install, download, or upgrade the CLI automatically. -4. Treat `.firstdraft/state.json` as private CLI state. Never edit it, copy it into chat, or commit it. +3. Before any task that creates a new subject, also require `plan subject-id`. +4. Do not install, download, or upgrade the CLI automatically. +5. Treat `.firstdraft/state.json` as private CLI state. Never edit it, copy it into chat, or commit it. The current toolchain is experimental. If a needed command is absent, state the missing capability and stop before approximating its behavior. @@ -46,7 +48,8 @@ If `.firstdraft/` does not exist: firstdraft plan init --application-key --name "" ``` -3. Keep the generated empty `entities` array. Do not invent a placeholder Entity. +3. Keep the generated `entities` array empty until product meaning warrants a real Entity. Never invent a + placeholder Entity. If `.firstdraft/` already exists, first use file metadata and permission checks such as `test -f` and `test -r` to confirm that `foundation-plan.json` and `state.json` are regular and readable. Do not open or echo `state.json`. @@ -61,9 +64,8 @@ preserve its existing subject UUIDs. 3. Express product meaning, not Rails tables, macros, gems, callbacks, or executable code. 4. Preserve a subject's `subject_uuid` through renames and coherent same-kind moves. Update every affected readable path in the same candidate. Give a replacement concept a new UUID. -5. Before adding a genuinely new subject, check whether `firstdraft plan --help` lists `subject-id`. If it does, use - `firstdraft plan subject-id` for each new subject. If it does not, do not invent a UUIDv7 or copy an example UUID; - explain that this CLI cannot yet add subjects safely. +5. Use `firstdraft plan subject-id` for each genuinely new subject. If the capability check failed, do not invent a + UUIDv7 or copy an example UUID; explain that this CLI cannot yet add subjects safely. 6. Omit unsupported prose, secrets, arbitrary code, ordinary empty optional collections, and structural `null` placeholders. 7. Ask the user about materially ambiguous product meaning. Do not silently choose destructive relationship @@ -80,10 +82,11 @@ Run `firstdraft plan push` only after reading the recovery rules. The CLI submit conditional whole-document PUT and owns the ETag lifecycle. - On success, inspect every diagnostic. Repair errors; surface warnings and material assumptions. -- On `422`, amend the addressed source while preserving unrelated content and stable subject identity, then push - again when the correction is well-founded. -- If First Draft reports that the current release accepts only the empty starter, keep the user's nonempty design - intact and report the server capability gap. Do not erase valid work to make the request pass. +- On `422`, classify every diagnostic before editing. Amend a correctable source problem while preserving unrelated + content and stable subject identity, then push again when the correction is well-founded. +- On `foundation_plan.import.unsupported_capability`, preserve the addressed product meaning and report the exact + server gap. Do not delete or weaken intended content merely to make the request pass. Stop for this attempt; do + not resubmit unchanged bytes. - On `local_state_not_saved`, stop. Keep the printed recovery material local and private; do not paste it into chat, commit it, or push again. - On `412`, an ambiguous transport/protocol outcome, or damaged local state, stop. Do not retry, reinitialize, or diff --git a/skills/create-full-stack-app/references/diagnostics-and-recovery.md b/skills/create-full-stack-app/references/diagnostics-and-recovery.md index 02f9e2f..6e76700 100644 --- a/skills/create-full-stack-app/references/diagnostics-and-recovery.md +++ b/skills/create-full-stack-app/references/diagnostics-and-recovery.md @@ -22,11 +22,11 @@ A `422` response binds diagnostics to the submitted bytes with `source_sha256`: "source_sha256": "", "diagnostics": [ { - "code": "foundation_plan.import.unsupported_bootstrap_content", + "code": "foundation_plan.import.unsupported_capability", "severity": "error", - "message": "This First Draft release can create or replace a Project only from the empty starter Plan.", + "message": "This First Draft release cannot yet import this Foundation Plan capability.", "location": { - "source_pointer": "/application/entities" + "source_pointer": "/application/entities/0/fields/0/default" }, "subject": null, "related_locations": [], @@ -57,8 +57,9 @@ For each diagnostic: 5. Make the smallest well-founded correction without changing unrelated identity or product meaning. 6. Push again only after the local source has been deliberately amended. -The prototype's `unsupported_bootstrap_content` error is not corrected by deleting intended nonempty content. -Keep the Plan and report that this server release cannot import it. +An `unsupported_capability` error is not corrected by deleting or weakening intended product meaning. The complete +candidate is rejected atomically; supported sibling content is not partially applied. Keep the Plan and report the +exact capability this server release cannot import. ## Concurrent replacement diff --git a/skills/create-full-stack-app/references/examples.md b/skills/create-full-stack-app/references/examples.md index 48b8d24..daa0cf3 100644 --- a/skills/create-full-stack-app/references/examples.md +++ b/skills/create-full-stack-app/references/examples.md @@ -5,8 +5,8 @@ in a real Project. ## Empty starter -This is the only subset accepted by the current prototype PUT. `firstdraft plan init` creates the same shape with -the user's application key and name. +`firstdraft plan init` creates this valid starting point with the user's application key and name. It is the +smallest subset accepted by the reviewed conditional PUT. ```json { @@ -27,10 +27,10 @@ the user's application key and name. An empty Plan is preferable to a fake Entity. Tell the user that the application model is still empty. -## One Entity and Field +## One Entity and scalar Field -This complete document is structurally valid v0.19, but its nonempty `entities` array is not currently importable. -It has not been proven compilable. +This complete document is structurally valid v0.19 and accepted by the reviewed bounded importer. That does not +prove complete semantic analysis, target support, Compilation, or generated output. ```json { @@ -68,13 +68,14 @@ It has not been proven compilable. ``` The Entity and Field have independent UUIDs. The `primary_descriptor` uses a typed readable path rather than a -UUID. +UUID. The reviewed importer also accepts `boolean`, `date`, `datetime`, `decimal`, `integer`, `language_code`, +`long_text`, `time_zone`, and `url` Fields when they use only the supported schema-valid scalar properties. ## Stored and reverse relationship -This complete document is structurally valid v0.19, but is not currently importable or Compiler-proven. `Task` -owns the stored `project` Reference. `Project` owns the meaningful reverse `tasks` Association. The forward -`task.project` Association is derived and therefore omitted. +This complete document is structurally valid v0.19, but References and authored Associations remain outside the +reviewed importer subset. It is not Compiler-proven. `Task` owns the stored `project` Reference. `Project` owns the +meaningful reverse `tasks` Association. The forward `task.project` Association is derived and therefore omitted. ```json { diff --git a/skills/create-full-stack-app/references/foundation-plan-019.md b/skills/create-full-stack-app/references/foundation-plan-019.md index 4de4446..c47c130 100644 --- a/skills/create-full-stack-app/references/foundation-plan-019.md +++ b/skills/create-full-stack-app/references/foundation-plan-019.md @@ -9,18 +9,19 @@ the submitted exact bytes. - The v0.19 corpus passes the First Draft JSON Schema and strict loader. - Structural validity does not prove readable-link resolution, whole-application consistency, target support, or compilability. -- The prototype conditional PUT currently imports only the empty starter subset. -- There is no released end-to-end CLI/API workflow, GET or pull operation, complete semantic analyzer, Publish - action, Compilation action, or generated Foundation. +- The reviewed conditional PUT imports empty drafts and a bounded subset of Entities, scalar Fields, and Field or + system-Field Primary Descriptors. +- There is no released end-to-end CLI/API workflow, complete nonempty import, GET or pull operation, complete + semantic analyzer, Publish action, Compilation action, or generated Foundation. The bundled schema was copied from the [First Draft source at revision `12fa2a6`](https://github.com/firstdraft/firstdraft/blob/12fa2a6bcac122196d55f5528fbc3f1363c684e3/docs/architecture/design/foundation-plan.schema.json) and has SHA-256 `5994c41f65eab52f92020fa24437e76b6957b7016ccf231dce06e8097f0b34b5`. The reviewed public CLI baseline is -[`af33be324fd0bc1df62f8f888a8e0b30cbd9e8da`](https://github.com/firstdraft/cli/commit/af33be324fd0bc1df62f8f888a8e0b30cbd9e8da); -it has not been released and exposes only `plan init` -and `plan push`. Check commands rather than inferring compatibility from an unreleased version number. Update this -Skill deliberately when either contract changes. +[`0681afd48d7825a7a1a0112e248f3013d0123743`](https://github.com/firstdraft/cli/commit/0681afd48d7825a7a1a0112e248f3013d0123743); +it has not been released and exposes `plan init`, `plan subject-id`, and `plan push`. Check commands rather than +inferring compatibility from an unreleased version number. Update this Skill deliberately when either contract +changes. ## Closed envelope @@ -87,9 +88,13 @@ App Schema artifact. - Use `null` only where the schema gives it a semantic meaning, not as structural filler. - Omission and an explicit scalar default mean the same thing, but examples normally omit default-valued settings. -## Prototype PUT limitation +## Current conditional PUT boundary -The currently reviewed importer accepts only these Application properties: +The reviewed importer accepts the required Application properties `key`, `name`, `native`, `delivery`, and +`entities`. `native` and `delivery` must remain empty. `entities` may contain any number of Entities with +`subject_uuid`, `key`, `name`, optional `icon` and `fields`, and one required `primary_descriptor`. + +The smallest accepted Application remains: ```json { @@ -101,6 +106,24 @@ The currently reviewed importer accepts only these Application properties: } ``` -Nonempty `entities`, `native`, or `delivery`, and optional properties such as `domain` or `appearance`, currently -produce `foundation_plan.import.unsupported_bootstrap_content`. That diagnostic describes server capability, not -invalid product meaning. Preserve the authored Plan and report the gap. +A Primary Descriptor may select a Field owned by that Entity or a schema-supported system Field. Association +descriptors are not yet supported. A Field may use these types: + +- `boolean` +- `date` +- `datetime` +- `decimal` +- `integer` +- `language_code` +- `long_text` +- `short_text` +- `time_zone` +- `url` + +For those types, the importer retains schema-valid combinations of `subject_uuid`, `key`, `name`, `type`, +`required`, `notes`, `immutable`, `comparison`, `normalizations`, `encrypted_at_rest`, and `redact_from_logs`. + +Any Field type outside the list above, Field defaults, Validations, derivations, Field settings, References, +Associations, and other Entity or Application capabilities remain unsupported. One unsupported pointer rejects the +complete conditional PUT with `foundation_plan.import.unsupported_capability` and no mutation. That diagnostic +describes server capability, not invalid product meaning. Preserve the authored Plan and report the exact gap. diff --git a/skills/create-full-stack-app/references/modeling-guide.md b/skills/create-full-stack-app/references/modeling-guide.md index 499b4c9..2ad695c 100644 --- a/skills/create-full-stack-app/references/modeling-guide.md +++ b/skills/create-full-stack-app/references/modeling-guide.md @@ -62,8 +62,8 @@ Capabilities or prerequisites as authored lists. Fix the smallest well-founded source problem. Preserve unrelated subjects, ordering, and stable identity. If a diagnostic reveals an ambiguous product decision, ask the user rather than optimizing for a green response. -In particular, do not remove nonempty modeled content solely because the prototype importer supports only an -empty starter. That is a server capability gap and should remain visible. +In particular, do not remove or weaken modeled content solely because the importer reports +`foundation_plan.import.unsupported_capability`. Preserve the local Plan and report the exact server gap. ## Prepare user review diff --git a/test/repository.test.mjs b/test/repository.test.mjs index 5ed3775..37b0516 100644 --- a/test/repository.test.mjs +++ b/test/repository.test.mjs @@ -17,6 +17,31 @@ const foundationPlanTarget = { }; const foundationPlanSchemaDigest = "5994c41f65eab52f92020fa24437e76b6957b7016ccf231dce06e8097f0b34b5"; +const supportedScalarFieldTypes = [ + "boolean", + "date", + "datetime", + "decimal", + "integer", + "language_code", + "long_text", + "short_text", + "time_zone", + "url", +]; +const supportedScalarFieldProperties = [ + "subject_uuid", + "key", + "name", + "type", + "required", + "notes", + "immutable", + "comparison", + "normalizations", + "encrypted_at_rest", + "redact_from_logs", +]; test("installable Skills follow the portable repository profile", async () => { const entries = await readdir(skillsDirectory, { withFileTypes: true }); @@ -152,6 +177,63 @@ test("authored JSON examples parse and retain the pinned Plan contract", async ( assert.deepEqual(foundationPlanReference.at(-1), fixture.application); }); +test("bounded importer prose remains bound to the exact allowlists", async () => { + const referencesDirectory = path.join( + skillsDirectory, + "create-full-stack-app", + "references", + ); + const foundationPlanReference = await readFile( + path.join(referencesDirectory, "foundation-plan-019.md"), + "utf8", + ); + const documentedTypeSection = foundationPlanReference.match( + /A Field may use these types:\n\n([\s\S]*?)\n\nFor those types/, + ); + assert( + documentedTypeSection, + "foundation-plan-019.md: missing supported Field type list", + ); + assert.deepEqual( + [...documentedTypeSection[1].matchAll(/^- `([^`]+)`$/gm)].map( + (match) => match[1], + ), + supportedScalarFieldTypes, + ); + + const documentedPropertySection = foundationPlanReference.match( + /retains schema-valid combinations of ([\s\S]*?)\.\n\nAny Field type/, + ); + assert( + documentedPropertySection, + "foundation-plan-019.md: missing retained Field property list", + ); + assert.deepEqual( + [...documentedPropertySection[1].matchAll(/`([^`]+)`/g)].map( + (match) => match[1], + ), + supportedScalarFieldProperties, + ); + + const examples = await readFile( + path.join(referencesDirectory, "examples.md"), + "utf8", + ); + const additionalTypeSentence = examples.match( + /reviewed importer also accepts ([\s\S]*?) Fields/, + ); + assert( + additionalTypeSentence, + "examples.md: missing additional supported Field type list", + ); + assert.deepEqual( + [...additionalTypeSentence[1].matchAll(/`([^`]+)`/g)].map( + (match) => match[1], + ), + supportedScalarFieldTypes.filter((type) => type !== "short_text"), + ); +}); + test("complete examples and eval Plans validate against the bundled exact schema", async () => { const skillDirectory = path.join(skillsDirectory, "create-full-stack-app"); const schemaSource = await readFile( @@ -197,14 +279,12 @@ test("complete examples and eval Plans validate against the bundled exact schema } }); -test("resume eval stages and binds the identity-preserving rename", async () => { +test("revision evals stage existing Plan identity and private state", async () => { const evaluationDirectory = path.join(evalsDirectory, "create-full-stack-app"); const cases = JSON.parse( await readFile(path.join(evaluationDirectory, "cases.json"), "utf8"), ).cases; - const evaluation = cases.find(({ id }) => id === "resume-with-stable-identity"); - - assert.deepEqual(evaluation.artifacts, [ + const stagedPlanArtifacts = [ { path: "evals/create-full-stack-app/fixtures/resume.foundation-plan.json", role: "input", @@ -215,7 +295,26 @@ test("resume eval stages and binds the identity-preserving rename", async () => role: "input", stage_as: ".firstdraft/state.json", }, - ]); + ]; + for (const id of ["resume-with-stable-identity", "add-field-with-minted-id"]) { + assert.deepEqual( + cases.find((evaluation) => evaluation.id === id).artifacts, + stagedPlanArtifacts, + ); + } + const mintingEvaluation = cases.find( + ({ id }) => id === "add-field-with-minted-id", + ); + assert( + mintingEvaluation.expectations.some((expectation) => + expectation.includes("plan subject-id exactly once"), + ), + ); + assert( + mintingEvaluation.expectations.some((expectation) => + expectation.includes("Never fabricates a UUIDv7"), + ), + ); const plan = JSON.parse( await readFile( @@ -245,55 +344,108 @@ test("resume eval stages and binds the identity-preserving rename", async () => assert.throws(() => JSON.parse(placeholder)); }); -test("diagnostic and recovery evals stage and preserve existing Plan state", async () => { +test("bounded import evals bind supported and unsupported Plan state", async () => { const evaluationDirectory = path.join(evalsDirectory, "create-full-stack-app"); const cases = JSON.parse( await readFile(path.join(evaluationDirectory, "cases.json"), "utf8"), ).cases; - const stagedPlanArtifacts = [ - { - path: "evals/create-full-stack-app/fixtures/resume.foundation-plan.json", - role: "input", - stage_as: ".firstdraft/foundation-plan.json", - }, - { - path: "evals/create-full-stack-app/fixtures/state-placeholder.txt", - role: "input", - stage_as: ".firstdraft/state.json", - }, - ]; - const diagnosticEvaluation = cases.find( - ({ id }) => id === "prototype-nonempty-diagnostic", + const stateArtifact = { + path: "evals/create-full-stack-app/fixtures/state-placeholder.txt", + role: "input", + stage_as: ".firstdraft/state.json", + }; + const supportedPlanArtifact = { + path: + "evals/create-full-stack-app/fixtures/supported-scalars.foundation-plan.json", + role: "input", + stage_as: ".firstdraft/foundation-plan.json", + }; + const supportedEvaluation = cases.find( + ({ id }) => id === "review-supported-scalar-plan", ); - assert.deepEqual(diagnosticEvaluation.artifacts, [ + assert.deepEqual(supportedEvaluation.artifacts, [ + supportedPlanArtifact, + stateArtifact, + ]); + const supportedPlan = JSON.parse( + await readFile( + path.join( + evaluationDirectory, + "fixtures", + "supported-scalars.foundation-plan.json", + ), + "utf8", + ), + ); + const supportedEntity = supportedPlan.application.entities[0]; + assert.equal(supportedPlan.application.entities.length, 1); + assert.equal(supportedEntity.primary_descriptor.field, "movie.title"); + assert.deepEqual( + supportedEntity.fields.map(({ type }) => type), + supportedScalarFieldTypes, + ); + assert.deepEqual( + [ + ...new Set( + supportedEntity.fields.flatMap((field) => Object.keys(field)), + ), + ].sort(), + [...supportedScalarFieldProperties].sort(), + ); + const descriptorKey = supportedEntity.primary_descriptor.field + .split(".") + .at(-1); + const descriptorField = supportedEntity.fields.find( + ({ key }) => key === descriptorKey, + ); + assert(descriptorField, "supported fixture: descriptor Field does not resolve"); + assert.equal(descriptorField.type, "short_text"); + assert.equal(descriptorField.required, true); + assert.equal( + new Set([ + supportedEntity.subject_uuid, + ...supportedEntity.fields.map(({ subject_uuid }) => subject_uuid), + ]).size, + supportedScalarFieldTypes.length + 1, + ); + + const unsupportedPlanArtifact = { + path: + "evals/create-full-stack-app/fixtures/unsupported-field-capabilities.foundation-plan.json", + role: "input", + stage_as: ".firstdraft/foundation-plan.json", + }; + const unsupportedEvaluation = cases.find( + ({ id }) => id === "unsupported-field-capabilities", + ); + assert.deepEqual(unsupportedEvaluation.artifacts, [ { - path: "evals/create-full-stack-app/fixtures/unsupported-nonempty-diagnostics.json", + path: "evals/create-full-stack-app/fixtures/unsupported-field-capabilities-diagnostics.json", role: "input", }, - ...stagedPlanArtifacts, + unsupportedPlanArtifact, + stateArtifact, ]); - for (const id of [ - "stale-writer-conflict", - "ambiguous-network-outcome", - "local-state-not-saved", - ]) { - assert.deepEqual( - cases.find((evaluation) => evaluation.id === id).artifacts, - stagedPlanArtifacts, - ); - } - const planSource = await readFile( - path.join(evaluationDirectory, "fixtures", "resume.foundation-plan.json"), + path.join( + evaluationDirectory, + "fixtures", + "unsupported-field-capabilities.foundation-plan.json", + ), "utf8", ); + const unsupportedPlan = JSON.parse(planSource); + const unsupportedFields = unsupportedPlan.application.entities[0].fields; + assert.equal(unsupportedFields[0].default.value, "Untitled"); + assert.equal(unsupportedFields[0].validations[0].kind, "length"); + assert.equal(unsupportedFields[1].type, "enum"); const response = JSON.parse( await readFile( path.join( evaluationDirectory, "fixtures", - "unsupported-nonempty-diagnostics.json", + "unsupported-field-capabilities-diagnostics.json", ), "utf8", ), @@ -302,13 +454,60 @@ test("diagnostic and recovery evals stage and preserve existing Plan state", asy createHash("sha256").update(planSource).digest("hex"), response.source_sha256, ); - assert.equal( - response.diagnostics[0].code, - "foundation_plan.import.unsupported_bootstrap_content", + assert.deepEqual( + response.diagnostics.map(({ code, location }) => [ + code, + location.source_pointer, + ]), + [ + [ + "foundation_plan.import.unsupported_capability", + "/application/entities/0/fields/0/default", + ], + [ + "foundation_plan.import.unsupported_capability", + "/application/entities/0/fields/0/validations", + ], + [ + "foundation_plan.import.unsupported_capability", + "/application/entities/0/fields/1/settings", + ], + [ + "foundation_plan.import.unsupported_capability", + "/application/entities/0/fields/1/type", + ], + ], ); - assert.deepEqual(response.diagnostics[0].location, { - source_pointer: "/application/entities", - }); +}); + +test("recovery evals stage and preserve existing Plan state", async () => { + const evaluationDirectory = path.join(evalsDirectory, "create-full-stack-app"); + const cases = JSON.parse( + await readFile(path.join(evaluationDirectory, "cases.json"), "utf8"), + ).cases; + const stagedPlanArtifacts = [ + { + path: "evals/create-full-stack-app/fixtures/resume.foundation-plan.json", + role: "input", + stage_as: ".firstdraft/foundation-plan.json", + }, + { + path: "evals/create-full-stack-app/fixtures/state-placeholder.txt", + role: "input", + stage_as: ".firstdraft/state.json", + }, + ]; + + for (const id of [ + "stale-writer-conflict", + "ambiguous-network-outcome", + "local-state-not-saved", + ]) { + assert.deepEqual( + cases.find((evaluation) => evaluation.id === id).artifacts, + stagedPlanArtifacts, + ); + } }); test("malformed source fixture is bound to its coordinate diagnostic", async () => {