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
4 changes: 2 additions & 2 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ 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.5.x` requires API `0.5.x` and Plan `firstdraft.foundation-plan.sketch/0.21`. Plan `0.21` adds the authored
`appearance.theme: "toggle"` choice and replaces the sole accepted `0.20` input format. The new API and CLI minor
CLI `0.6.x` requires API `0.6.x` and Plan `firstdraft.foundation-plan.sketch/0.22`. Plan `0.22` adds the optional
`application.pwa` boolean and replaces the sole accepted `0.21` input format. The new API and CLI minor
versions record that input and artifact compatibility break; they add no migration or compatibility bridge.
The target stays `rails-sketch/2026-09`.

Expand Down
18 changes: 12 additions & 6 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.5.x` source line contains the auditable command shell, local Foundation Plan initialization, local
The current `0.6.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.5.x`
requires the service's `0.5.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.6.x`
requires the service's `0.6.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
Expand Down Expand Up @@ -45,7 +45,7 @@ From the project that the Plan describes:
firstdraft plan init --name "Oscar Party"
```

This creates an empty `firstdraft.foundation-plan.sketch/0.21` Plan targeting `rails-sketch/2026-09` and a
This creates an empty `firstdraft.foundation-plan.sketch/0.22` Plan targeting `rails-sketch/2026-09` 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.
Expand All @@ -54,6 +54,12 @@ 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.

### Generate an application key

To inspect the name-to-key derivation without initializing a project, run:
Expand Down Expand Up @@ -296,7 +302,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.5.x` progress-reason allowlist is `github.configuration_missing`, `github.oauth_unavailable`,
The closed API `0.6.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`,
Expand Down Expand Up @@ -345,7 +351,7 @@ 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.21` and target profile
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.
An earlier format or profile is rejected before materialization, even when the retained status names that profile.

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@firstdraft.com/cli",
"version": "0.5.0",
"version": "0.6.0",
"description": "Command-line interface for First Draft",
"license": "MIT",
"type": "module",
Expand Down
6 changes: 3 additions & 3 deletions release/compatibility.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"format": "firstdraft.release-compatibility/1",
"component": "cli",
"version": "0.5.0",
"version": "0.6.0",
"requires": {
"api_contract": [">= 0.5.0", "< 0.6.0"],
"foundation_plan_formats": ["firstdraft.foundation-plan.sketch/0.21"]
"api_contract": [">= 0.6.0", "< 0.7.0"],
"foundation_plan_formats": ["firstdraft.foundation-plan.sketch/0.22"]
}
}
8 changes: 4 additions & 4 deletions scripts/smoke-package.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ try {
name: initializedPlan.application.name,
},
{
format: "firstdraft.foundation-plan.sketch/0.21",
format: "firstdraft.foundation-plan.sketch/0.22",
target: { id: "rails", profile: "rails-sketch/2026-09" },
key: "oscar_party",
name: "Oscar Party",
Expand Down Expand Up @@ -343,7 +343,7 @@ async function exercisePackedCompilation(projectDirectory) {
const analyzerRelease =
"foundation-plan-rails/application-2026-09-19-conventions";
const compilerRelease =
"foundation-plan-rails/compiler-application-2026-09-19-conventions";
"foundation-plan-rails/compiler-application-2026-09-23-pwa";
const target = { id: "rails", profile: "rails-sketch/2026-09" };
const gapSet = {
format: "firstdraft.foundation-gaps/2",
Expand Down Expand Up @@ -399,7 +399,7 @@ async function exercisePackedCompilation(projectDirectory) {
graph_version: 1,
head_source_sha256: headSha256,
foundation_plan: {
format: "firstdraft.foundation-plan.sketch/0.21",
format: "firstdraft.foundation-plan.sketch/0.22",
sha256: foundationPlanSha256,
},
analysis: {
Expand Down Expand Up @@ -533,7 +533,7 @@ async function exercisePackedCompilation(projectDirectory) {
{
project: { id: projectId, graph_version: 1 },
foundation_plan: {
format: "firstdraft.foundation-plan.sketch/0.21",
format: "firstdraft.foundation-plan.sketch/0.22",
source_sha256: headSha256,
},
diagnostics: [],
Expand Down
2 changes: 1 addition & 1 deletion src/compilation-artifact.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ 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.21";
export const FOUNDATION_PLAN_FORMAT = "firstdraft.foundation-plan.sketch/0.22";
export const RAILS_TARGET_PROFILE = "rails-sketch/2026-09";
export const MAX_ARTIFACT_BYTES = 128 * 1024 * 1024;

Expand Down
4 changes: 2 additions & 2 deletions test/compilation-artifact.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ 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-22-theme";
"foundation-plan-rails/compiler-application-2026-09-23-pwa";
const TARGET = { id: "rails", profile: "rails-sketch/2026-09" };
const EXPECTED = {
projectId: PROJECT_ID,
Expand Down Expand Up @@ -262,7 +262,7 @@ test("pins external provenance identities and validates nested metadata", () =>
artifactFixture({
provenance: {
foundation_plan: {
format: "firstdraft.foundation-plan.sketch/0.20",
format: "firstdraft.foundation-plan.sketch/0.21",
sha256: FOUNDATION_PLAN_SHA256,
},
},
Expand Down
4 changes: 2 additions & 2 deletions test/compilation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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-22-theme";
"foundation-plan-rails/compiler-application-2026-09-23-pwa";
const TARGET = { id: "rails", profile: "rails-sketch/2026-09" };
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.21"}\n',
'{"format":"firstdraft.foundation-plan.sketch/0.22"}\n',
);
const headSourceSha256 = sha256(planSource);
const etag = `"sha256:${headSourceSha256}"`;
Expand Down
6 changes: 3 additions & 3 deletions test/plan-compile.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.21","application":{"key":"movie_catalog","name":"Movie Catalog"}}\n',
'{"format":"firstdraft.foundation-plan.sketch/0.22","application":{"key":"movie_catalog","name":"Movie Catalog"}}\n',
);
const HEAD_SHA256 = sha256(PLAN_SOURCE);
const ETAG = `"sha256:${HEAD_SHA256}"`;
Expand All @@ -38,7 +38,7 @@ 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 COMPILER_RELEASE =
"foundation-plan-rails/compiler-application-2026-09-22-theme";
"foundation-plan-rails/compiler-application-2026-09-23-pwa";
const TARGET = { id: "rails", profile: "rails-sketch/2026-09" };
const SUCCESS_PROGRESS = `First Draft: Analyzing Foundation Plan...
First Draft: Foundation Plan analysis valid.
Expand Down Expand Up @@ -1100,7 +1100,7 @@ function acceptedPlanBody(graphVersion = 1) {
return {
project: { id: PROJECT_ID, graph_version: graphVersion },
foundation_plan: {
format: "firstdraft.foundation-plan.sketch/0.21",
format: "firstdraft.foundation-plan.sketch/0.22",
source_sha256: HEAD_SHA256,
},
diagnostics: [],
Expand Down
2 changes: 1 addition & 1 deletion test/plan-init.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ const PLAN_INIT_ERROR = jsonOutput({
});

const EXPECTED_PLAN = `{
"format": "firstdraft.foundation-plan.sketch/0.21",
"format": "firstdraft.foundation-plan.sketch/0.22",
"target": {
"id": "rails",
"profile": "rails-sketch/2026-09"
Expand Down
4 changes: 2 additions & 2 deletions test/plan-publish.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.21"}\n',
'{"format":"firstdraft.foundation-plan.sketch/0.22"}\n',
);
const HEAD_SHA256 = sha256(PLAN_SOURCE);
const ETAG = `"sha256:${HEAD_SHA256}"`;
Expand All @@ -26,7 +26,7 @@ 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 COMPILER_RELEASE =
"foundation-plan-rails/compiler-application-2026-09-22-theme";
"foundation-plan-rails/compiler-application-2026-09-23-pwa";
const TARGET = { id: "rails", profile: "rails-sketch/2026-09" };
const ARTIFACT = {
sha256: "1".repeat(64),
Expand Down
31 changes: 30 additions & 1 deletion test/plan-push.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,35 @@ 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("the initial push defaults to the First Draft production origin", async (context) => {
const cwd = await initializedDirectory(context);
const source = planSource(cwd);
Expand Down Expand Up @@ -1274,7 +1303,7 @@ function acceptedBody(source, diagnostics = []) {
return {
project: { id: PROJECT_ID, graph_version: 1 },
foundation_plan: {
format: "firstdraft.foundation-plan.sketch/0.21",
format: "firstdraft.foundation-plan.sketch/0.22",
source_sha256: sha256(source),
},
diagnostics,
Expand Down
4 changes: 2 additions & 2 deletions test/release-compatibility.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ test("release compatibility declares the coordinated CLI contract", () => {
"foundation_plan_formats",
]);
assert.deepEqual(compatibility.requires.api_contract, [
">= 0.5.0",
"< 0.6.0",
">= 0.6.0",
"< 0.7.0",
]);
assert.deepEqual(compatibility.requires.foundation_plan_formats, [
FOUNDATION_PLAN_FORMAT,
Expand Down
Loading