From ebfe641273c3677baf783f087ea7210956106f1c Mon Sep 17 00:00:00 2001 From: Raghu Betina Date: Sun, 27 Sep 2026 16:33:11 -0500 Subject: [PATCH] Adopt the bookmark-assets Plan contract Service replaces the authored PWA choice with ordinary bookmark assets. Initialize and accept Plan 0.23 and its new Rails profile so the CLI can materialize the coordinated artifact without retaining the old contract. Prepare unpublished CLI 0.8.0 for API 0.7.x, align command and package fixtures, and document the replacement without claiming publication. --- RELEASING.md | 16 +++++++++------ docs/commands.md | 23 +++++++++++----------- package-lock.json | 4 ++-- package.json | 2 +- release/compatibility.json | 6 +++--- scripts/smoke-package.js | 20 +++++++++++-------- src/compilation-artifact.js | 4 ++-- test/compilation-artifact.test.js | 13 +++++++------ test/compilation.test.js | 8 ++++---- test/plan-compile.test.js | 11 ++++++----- test/plan-init.test.js | 4 ++-- test/plan-publish.test.js | 9 +++++---- test/plan-push.test.js | 31 +----------------------------- test/plan-status.test.js | 8 ++++---- test/release-compatibility.test.js | 4 ++-- 15 files changed, 72 insertions(+), 91 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index 72fb710..b4836a1 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -16,15 +16,19 @@ release tag. An unpublished, untagged candidate can retain its proposed version The published version may remain in source during documentation and test maintenance; recording its release history does not require preparing another version. Choose an unused version when preparing the next publication. -CLI `0.7.x` requires API `0.6.x` and Plan `firstdraft.foundation-plan.sketch/0.22`. CLI `0.7.0` adds `--staging` -and requires `FIRSTDRAFT_STAGING_API_TOKEN` for the staging origin, including existing pinned Projects. This token -configuration change warrants a new CLI minor version; the API and Plan contracts stay unchanged. Production -remains the default and uses `FIRSTDRAFT_API_TOKEN`, as do custom origins. No Project is migrated. The target stays -`rails-sketch/2026-09`. +CLI `0.8.x` requires API `0.7.x`, Plan `firstdraft.foundation-plan.sketch/0.23`, and target +`rails-sketch/2026-09-bookmark-assets`. This breaking contract retires `application.pwa`; the Rails target retains +ordinary bookmark assets and metadata without a Plan switch. Earlier Plan formats and target profiles are not +accepted for artifact materialization. Existing applications and old Plans are not migrated. + +Before publishing CLI `0.8.0`, align the Service and Skills companions for the bookmark-assets contract. Source +checks and packed-package smokes do not establish a published or deployed tuple. Staging continues to require +`FIRSTDRAFT_STAGING_API_TOKEN`, including existing pinned Projects. Production remains the default and uses +`FIRSTDRAFT_API_TOKEN`, as do custom origins. Local output remains the default introduced in CLI `0.4.x`: `firstdraft plan compile` is equivalent to `firstdraft plan compile --output .`, with GitHub publication selected by explicit `--github`. The root archive -remains `.firstdraft/design`. Existing applications and old Plans are not migrated. +remains `.firstdraft/design`. `release/compatibility.json` declares the package version, accepted API-contract range, and accepted Plan formats. It is source-only metadata, validated by the normal test suite and absent from the npm tarball. Coordinate the diff --git a/docs/commands.md b/docs/commands.md index 496b488..fc83e1d 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -3,10 +3,10 @@ This page owns the detailed public semantics of the current command surface. Run `firstdraft --help` or a command group's `--help` for concise executable syntax. See [Errors and recovery](errors.md) before retrying a failed mutation. -The current `0.7.x` source line contains the auditable command shell, local Foundation Plan initialization, local +The current `0.8.x` source line contains the auditable command shell, local Foundation Plan initialization, local application-key and UUID generation, conditional whole-document push, whole-graph analysis status polling, direct -Compile-and-materialize and private publish orchestration, and retained-Compilation inspection. CLI `0.7.x` -requires the service's `0.6.x` API contract. See the [release policy](../RELEASING.md) for versioning and channel +Compile-and-materialize and private publish orchestration, and retained-Compilation inspection. CLI `0.8.x` +requires the service's `0.7.x` API contract. See the [release policy](../RELEASING.md) for versioning and channel semantics and [release history](release-history.md) for the transition from prereleases. ## Command map @@ -75,7 +75,7 @@ From the project that the Plan describes: firstdraft plan init --name "Oscar Party" ``` -This creates an empty `firstdraft.foundation-plan.sketch/0.22` Plan targeting `rails-sketch/2026-09` and a +This creates an empty `firstdraft.foundation-plan.sketch/0.23` Plan targeting `rails-sketch/2026-09-bookmark-assets` and a client-generated Project ID under `.firstdraft/`. A nested ignore file keeps that local scratch area out of Git without changing the project's own `.gitignore`. Initialization makes no network request and refuses to replace an existing `.firstdraft` path. @@ -84,11 +84,10 @@ Provide either `--name`, `--application-key`, or both. Name-only initialization initialization derives a humanized display name. Supplying both preserves both values exactly after validating them against the Foundation Plan schema. -The initialized Plan omits `application.pwa`, which uses the Rails target's default of `true` for basic online -installation and Add to Home Screen support. Set `"pwa": false` within `application` to omit generated installation -metadata, or use `true` explicitly. The CLI sends the authored bytes unchanged, preserving omission versus either -boolean. This choice does not add offline caching, Web Push, or a native build; a browser may still save an ordinary -site when it is disabled. The CLI does not qualify browser or device installation. +Plan `0.23` has no `application.pwa` option. The Rails target includes ordinary icons and bookmark metadata for +the application owner to edit. The CLI sends authored bytes unchanged; the Service diagnoses unsupported Plan +fields rather than the CLI removing them. These assets do not establish offline support or a complete PWA, and +the CLI does not qualify browser or device installation. ### Generate an application key @@ -332,7 +331,7 @@ local paths, or environment values. Success writes exactly the validated private newline to stdout. If the command fails after progress has begun, its structured JSON error envelope is the final stderr document after the progress lines. -The closed API `0.6.x` progress-reason allowlist is `github.configuration_missing`, `github.oauth_unavailable`, +The closed API `0.7.x` progress-reason allowlist is `github.configuration_missing`, `github.oauth_unavailable`, `github.api_unavailable`, `github.reauthorization_required`, `github.account_mismatch`, `github.installation_unavailable`, `github.installation_not_ready`, `github.preflight_unavailable`, the legacy-only `github.preflight_unclassified`, and these stage-specific fallbacks: `github.preflight_unavailable.configuration`, @@ -381,8 +380,8 @@ when a retained direct Compilation succeeded but an earlier root materialization never starts replacement work. An incomplete rollback leaves `.firstdraft-root-output` and requires journal reconciliation before this command can run again. -Artifact validation accepts only Plan format `firstdraft.foundation-plan.sketch/0.22` and target profile -`rails-sketch/2026-09`, in addition to matching the retained Compilation's provenance and verifying file integrity. +Artifact validation accepts only Plan format `firstdraft.foundation-plan.sketch/0.23` and target profile +`rails-sketch/2026-09-bookmark-assets`, in addition to matching the retained Compilation's provenance and verifying file integrity. An earlier format or profile is rejected before materialization, even when the retained status names that profile. Successful root adoption is intentionally one-way. The original `.firstdraft` authoring state moves under diff --git a/package-lock.json b/package-lock.json index 226743a..236c6de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@firstdraft.com/cli", - "version": "0.7.0", + "version": "0.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@firstdraft.com/cli", - "version": "0.7.0", + "version": "0.8.0", "license": "MIT", "bin": { "firstdraft": "bin/firstdraft.js" diff --git a/package.json b/package.json index 548dcc7..3e9ac9b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@firstdraft.com/cli", - "version": "0.7.0", + "version": "0.8.0", "description": "Command-line interface for First Draft", "license": "MIT", "type": "module", diff --git a/release/compatibility.json b/release/compatibility.json index f574dd7..6d36bd4 100644 --- a/release/compatibility.json +++ b/release/compatibility.json @@ -1,9 +1,9 @@ { "format": "firstdraft.release-compatibility/1", "component": "cli", - "version": "0.7.0", + "version": "0.8.0", "requires": { - "api_contract": [">= 0.6.0", "< 0.7.0"], - "foundation_plan_formats": ["firstdraft.foundation-plan.sketch/0.22"] + "api_contract": [">= 0.7.0", "< 0.8.0"], + "foundation_plan_formats": ["firstdraft.foundation-plan.sketch/0.23"] } } diff --git a/scripts/smoke-package.js b/scripts/smoke-package.js index 547bd7b..024bf58 100644 --- a/scripts/smoke-package.js +++ b/scripts/smoke-package.js @@ -149,8 +149,8 @@ try { name: initializedPlan.application.name, }, { - format: "firstdraft.foundation-plan.sketch/0.22", - target: { id: "rails", profile: "rails-sketch/2026-09" }, + format: "firstdraft.foundation-plan.sketch/0.23", + target: { id: "rails", profile: "rails-sketch/2026-09-bookmark-assets" }, key: "oscar_party", name: "Oscar Party", }, @@ -389,10 +389,13 @@ async function exercisePackedCompilation(projectDirectory) { const statusPath = `/v1/projects/${projectId}/compilations/${compilationId}`; const artifactPath = `${statusPath}/artifact`; const analyzerRelease = - "foundation-plan-rails/application-2026-09-19-conventions"; + "foundation-plan-rails/application-2026-09-27-bookmark-assets"; const compilerRelease = - "foundation-plan-rails/compiler-application-2026-09-23-pwa"; - const target = { id: "rails", profile: "rails-sketch/2026-09" }; + "foundation-plan-rails/compiler-application-2026-09-27-bookmark-assets"; + const target = { + id: "rails", + profile: "rails-sketch/2026-09-bookmark-assets", + }; const gapSet = { format: "firstdraft.foundation-gaps/2", source: { sha256: headSha256 }, @@ -447,12 +450,13 @@ async function exercisePackedCompilation(projectDirectory) { graph_version: 1, head_source_sha256: headSha256, foundation_plan: { - format: "firstdraft.foundation-plan.sketch/0.22", + format: "firstdraft.foundation-plan.sketch/0.23", sha256: foundationPlanSha256, }, analysis: { id: analysisId, - release: "foundation-plan-rails/application-2026-09-19-conventions", + release: + "foundation-plan-rails/application-2026-09-27-bookmark-assets", }, compiler_release: compilerRelease, target, @@ -581,7 +585,7 @@ async function exercisePackedCompilation(projectDirectory) { { project: { id: projectId, graph_version: 1 }, foundation_plan: { - format: "firstdraft.foundation-plan.sketch/0.22", + format: "firstdraft.foundation-plan.sketch/0.23", source_sha256: headSha256, }, diagnostics: [], diff --git a/src/compilation-artifact.js b/src/compilation-artifact.js index a793d06..43c95bb 100644 --- a/src/compilation-artifact.js +++ b/src/compilation-artifact.js @@ -26,8 +26,8 @@ import { export const ARTIFACT_MEDIA_TYPE = "application/vnd.firstdraft.compilation-artifact+json"; export const ARTIFACT_FORMAT = "firstdraft.compilation-artifact/1"; -export const FOUNDATION_PLAN_FORMAT = "firstdraft.foundation-plan.sketch/0.22"; -export const RAILS_TARGET_PROFILE = "rails-sketch/2026-09"; +export const FOUNDATION_PLAN_FORMAT = "firstdraft.foundation-plan.sketch/0.23"; +export const RAILS_TARGET_PROFILE = "rails-sketch/2026-09-bookmark-assets"; export const MAX_ARTIFACT_BYTES = 128 * 1024 * 1024; const ARTIFACT_KEYS = ["format", "provenance", "manifest_sha256", "files"]; diff --git a/test/compilation-artifact.test.js b/test/compilation-artifact.test.js index f44f797..5e80619 100644 --- a/test/compilation-artifact.test.js +++ b/test/compilation-artifact.test.js @@ -36,8 +36,8 @@ const SUBJECT_ID = "01900000-0000-7000-8000-000000000804"; const HEAD_SHA256 = "1".repeat(64); const FOUNDATION_PLAN_SHA256 = "5".repeat(64); const COMPILER_RELEASE = - "foundation-plan-rails/compiler-application-2026-09-23-pwa"; -const TARGET = { id: "rails", profile: "rails-sketch/2026-09" }; + "foundation-plan-rails/compiler-application-2026-09-27-bookmark-assets"; +const TARGET = { id: "rails", profile: "rails-sketch/2026-09-bookmark-assets" }; const EXPECTED = { projectId: PROJECT_ID, compilationId: COMPILATION_ID, @@ -262,7 +262,7 @@ test("pins external provenance identities and validates nested metadata", () => artifactFixture({ provenance: { foundation_plan: { - format: "firstdraft.foundation-plan.sketch/0.21", + format: "firstdraft.foundation-plan.sketch/0.22", sha256: FOUNDATION_PLAN_SHA256, }, }, @@ -279,7 +279,8 @@ test("pins external provenance identities and validates nested metadata", () => provenance: { analysis: { id: "01900000-0000-7000-8000-000000000899", - release: "foundation-plan-rails/application-2026-09-22-theme", + release: + "foundation-plan-rails/application-2026-09-27-bookmark-assets", }, }, }), @@ -304,7 +305,7 @@ test("pins external provenance identities and validates nested metadata", () => }); test("rejects an older target profile even when retained status agrees", () => { - const target = { id: "rails", profile: "rails-sketch/2026-08" }; + const target = { id: "rails", profile: "rails-sketch/2026-09" }; const fixture = artifactFixture({ provenance: { target } }); assert.throws( @@ -482,7 +483,7 @@ function artifactFixture(changes = {}) { }, analysis: { id: ANALYSIS_ID, - release: "foundation-plan-rails/application-2026-09-22-theme", + release: "foundation-plan-rails/application-2026-09-27-bookmark-assets", }, compiler_release: COMPILER_RELEASE, target: TARGET, diff --git a/test/compilation.test.js b/test/compilation.test.js index 9feba85..73e5426 100644 --- a/test/compilation.test.js +++ b/test/compilation.test.js @@ -33,14 +33,14 @@ const CREATED_AT = "2026-08-04T12:00:00.000000Z"; const STARTED_AT = "2026-08-04T12:00:01.000000Z"; const COMPLETED_AT = "2026-08-04T12:00:02.000000Z"; const COMPILER_RELEASE = - "foundation-plan-rails/compiler-application-2026-09-23-pwa"; -const TARGET = { id: "rails", profile: "rails-sketch/2026-09" }; + "foundation-plan-rails/compiler-application-2026-09-27-bookmark-assets"; +const TARGET = { id: "rails", profile: "rails-sketch/2026-09-bookmark-assets" }; const STATUS_PATH = `/v1/projects/${PROJECT_ID}/compilations/${COMPILATION_ID}`; const ARTIFACT_PATH = `${STATUS_PATH}/artifact`; test("direct Compilation pins the reviewed Plan and materializes one retained artifact", async (context) => { const planSource = Buffer.from( - '{"format":"firstdraft.foundation-plan.sketch/0.22"}\n', + '{"format":"firstdraft.foundation-plan.sketch/0.23"}\n', ); const headSourceSha256 = sha256(planSource); const etag = `"sha256:${headSourceSha256}"`; @@ -716,7 +716,7 @@ function artifactFixture(changes = {}) { }, analysis: { id: ANALYSIS_ID, - release: "foundation-plan-rails/application-2026-09-22-theme", + release: "foundation-plan-rails/application-2026-09-27-bookmark-assets", }, compiler_release: COMPILER_RELEASE, target: TARGET, diff --git a/test/plan-compile.test.js b/test/plan-compile.test.js index 2d79d06..1c70bbe 100644 --- a/test/plan-compile.test.js +++ b/test/plan-compile.test.js @@ -28,7 +28,7 @@ const COMPILATION_ID = "01900000-0000-7000-8000-000000002003"; const PUBLICATION_ID = "01900000-0000-7000-8000-000000002004"; const API_TOKEN = `fd_${"a".repeat(43)}`; const PLAN_SOURCE = Buffer.from( - '{"format":"firstdraft.foundation-plan.sketch/0.22","application":{"key":"movie_catalog","name":"Movie Catalog"}}\n', + '{"format":"firstdraft.foundation-plan.sketch/0.23","application":{"key":"movie_catalog","name":"Movie Catalog"}}\n', ); const HEAD_SHA256 = sha256(PLAN_SOURCE); const ETAG = `"sha256:${HEAD_SHA256}"`; @@ -36,10 +36,11 @@ const CREATED_AT = "2026-08-04T12:00:00.000Z"; const STARTED_AT = "2026-08-04T12:00:01.000Z"; const COMPLETED_AT = "2026-08-04T12:00:02.000Z"; const REPOSITORY_URL = "https://github.com/octocat/movie-catalog"; -const ANALYZER_RELEASE = "foundation-plan-rails/application-2026-09-22-theme"; +const ANALYZER_RELEASE = + "foundation-plan-rails/application-2026-09-27-bookmark-assets"; const COMPILER_RELEASE = - "foundation-plan-rails/compiler-application-2026-09-23-pwa"; -const TARGET = { id: "rails", profile: "rails-sketch/2026-09" }; + "foundation-plan-rails/compiler-application-2026-09-27-bookmark-assets"; +const TARGET = { id: "rails", profile: "rails-sketch/2026-09-bookmark-assets" }; const SUCCESS_PROGRESS = `First Draft: Analyzing Foundation Plan... First Draft: Foundation Plan analysis valid. First Draft: Compiling application... @@ -1162,7 +1163,7 @@ function acceptedPlanBody(graphVersion = 1) { return { project: { id: PROJECT_ID, graph_version: graphVersion }, foundation_plan: { - format: "firstdraft.foundation-plan.sketch/0.22", + format: "firstdraft.foundation-plan.sketch/0.23", source_sha256: HEAD_SHA256, }, diagnostics: [], diff --git a/test/plan-init.test.js b/test/plan-init.test.js index 1caca33..64112da 100644 --- a/test/plan-init.test.js +++ b/test/plan-init.test.js @@ -62,10 +62,10 @@ const PLAN_INIT_ERROR = jsonOutput({ }); const EXPECTED_PLAN = `{ - "format": "firstdraft.foundation-plan.sketch/0.22", + "format": "firstdraft.foundation-plan.sketch/0.23", "target": { "id": "rails", - "profile": "rails-sketch/2026-09" + "profile": "rails-sketch/2026-09-bookmark-assets" }, "application": { "key": "oscar_party", diff --git a/test/plan-publish.test.js b/test/plan-publish.test.js index c8bd7e0..147fa73 100644 --- a/test/plan-publish.test.js +++ b/test/plan-publish.test.js @@ -15,7 +15,7 @@ const ANALYSIS_ID = "01900000-0000-7000-8000-000000001004"; const PUBLICATION_ID = "01900000-0000-7000-8000-000000001005"; const OTHER_PUBLICATION_ID = "01900000-0000-7000-8000-000000001006"; const PLAN_SOURCE = Buffer.from( - '{"format":"firstdraft.foundation-plan.sketch/0.22"}\n', + '{"format":"firstdraft.foundation-plan.sketch/0.23"}\n', ); const HEAD_SHA256 = sha256(PLAN_SOURCE); const ETAG = `"sha256:${HEAD_SHA256}"`; @@ -24,10 +24,11 @@ const CREATED_AT = "2026-08-01T12:00:00.000Z"; const STARTED_AT = "2026-08-01T12:00:01.000Z"; const COMPLETED_AT = "2026-08-01T12:00:02.000Z"; const RETRY_AT = "2026-08-07T16:15:00.000000Z"; -const ANALYZER_RELEASE = "foundation-plan-rails/application-2026-09-22-theme"; +const ANALYZER_RELEASE = + "foundation-plan-rails/application-2026-09-27-bookmark-assets"; const COMPILER_RELEASE = - "foundation-plan-rails/compiler-application-2026-09-23-pwa"; -const TARGET = { id: "rails", profile: "rails-sketch/2026-09" }; + "foundation-plan-rails/compiler-application-2026-09-27-bookmark-assets"; +const TARGET = { id: "rails", profile: "rails-sketch/2026-09-bookmark-assets" }; const ARTIFACT = { sha256: "1".repeat(64), manifest_sha256: "2".repeat(64), diff --git a/test/plan-push.test.js b/test/plan-push.test.js index deb0b5e..075e39c 100644 --- a/test/plan-push.test.js +++ b/test/plan-push.test.js @@ -157,35 +157,6 @@ test("the initial push sends exact bytes and saves its origin and ETag", async ( } }); -test("plan push preserves explicit PWA choices without rewriting the Plan", async (context) => { - for (const pwa of [true, false]) { - const cwd = await initializedDirectory(context); - const plan = JSON.parse(planSource(cwd).toString("utf8")); - plan.application.pwa = pwa; - const source = Buffer.from(`${JSON.stringify(plan, null, 2)}\n`); - writeFileSync( - path.join(cwd, ".firstdraft", "foundation-plan.json"), - source, - ); - /** @type {FetchCall[]} */ - const calls = []; - - const result = await invoke(["plan", "push"], { - cwd, - apiUrl: API_URL, - fetchFunction: recordingFetch( - acceptedResponse(source, 201, FIRST_ETAG), - calls, - ), - }); - - assert.equal(result.status, 0); - assert.equal(calls.length, 1); - assert.deepEqual(calls[0]?.init?.body, source); - assert.deepEqual(planSource(cwd), source); - } -}); - test("a first staging push pins staging and uses its separate credential", async (context) => { const stagingToken = "canary-staging-token"; const cwd = await initializedDirectory(context); @@ -1331,7 +1302,7 @@ function acceptedBody(source, diagnostics = []) { return { project: { id: PROJECT_ID, graph_version: 1 }, foundation_plan: { - format: "firstdraft.foundation-plan.sketch/0.22", + format: "firstdraft.foundation-plan.sketch/0.23", source_sha256: sha256(source), }, diagnostics, diff --git a/test/plan-status.test.js b/test/plan-status.test.js index 2fb37e7..13ae1ab 100644 --- a/test/plan-status.test.js +++ b/test/plan-status.test.js @@ -32,10 +32,10 @@ const STARTED_AT = "2026-07-30T12:00:00.123Z"; const COMPLETED_AT = "2026-07-30T12:00:01.456Z"; const HEAD_SOURCE_SHA256 = "1".repeat(64); const ANALYZER_RELEASE = - "foundation-plan-rails/application-2026-09-19-conventions"; + "foundation-plan-rails/application-2026-09-27-bookmark-assets"; const COMPILER_RELEASE = - "foundation-plan-rails/compiler-application-2026-09-19-conventions"; -const TARGET = { id: "rails", profile: "rails-sketch/2026-09" }; + "foundation-plan-rails/compiler-application-2026-09-27-bookmark-assets"; +const TARGET = { id: "rails", profile: "rails-sketch/2026-09-bookmark-assets" }; const PLAN_STATUS_HELP = `First Draft CLI Usage: @@ -248,7 +248,7 @@ test("plan status accepts the pinned Service zero-gap canonical digest", async ( const body = analysisBody("valid"); body.analysis.gap_set.gaps = []; body.analysis.gap_set_sha256 = - "f02a7aa00937e1a586775db8a9d68ecbde47338b975cfd9a3ad570cb23985d9e"; + "acf87b117814f28e52a85e6c13fb77e58166a38038bb36a6e0b6177995262618"; const result = await invoke(["plan", "status"], { cwd, fetchFunction: recordingFetch([jsonResponse(body)], []), diff --git a/test/release-compatibility.test.js b/test/release-compatibility.test.js index 84109df..c029109 100644 --- a/test/release-compatibility.test.js +++ b/test/release-compatibility.test.js @@ -35,8 +35,8 @@ test("release compatibility declares the coordinated CLI contract", () => { "foundation_plan_formats", ]); assert.deepEqual(compatibility.requires.api_contract, [ - ">= 0.6.0", - "< 0.7.0", + ">= 0.7.0", + "< 0.8.0", ]); assert.deepEqual(compatibility.requires.foundation_plan_formats, [ FOUNDATION_PLAN_FORMAT,