Skip to content

Commit d24dfcd

Browse files
committed
Adopt reviewed GapSet API contract
Move the CLI onto its 0.2 compatibility line so agents can review the complete canonical GapSet before Compile. Preserve the ordinary response bound while giving Analysis and artifacts generous endpoint-specific limits.
1 parent ecba5f1 commit d24dfcd

17 files changed

Lines changed: 756 additions & 74 deletions

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44
Foundation Plans with their users, then submit a valid Plan for the current bounded compilation and private GitHub
55
publication journey.
66

7-
The current `0.1.x` line includes local Plan initialization, application-key and UUID generation, conditional
7+
The current `0.2.x` source line includes local Plan initialization, application-key and UUID generation, conditional
88
whole-document push, whole-graph analysis polling, compile-and-publish orchestration, and read-only retained
9-
Compilation download. Remote commands require a compatible First Draft service and are intended for coordinated
10-
trials; publishing this CLI does not make the wider service generally available.
9+
Compilation download. Analysis status exposes the complete reviewed GapSet before Compile. Remote commands require
10+
a compatible First Draft service and are intended for coordinated trials; publishing this CLI does not make the
11+
wider service generally available.
1112

1213
## Install
1314

RELEASING.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,16 @@ binds its Foundation Plan format to the implemented CLI constant.
3636

3737
The `script/release_compatibility_check` evaluator in `firstdraft/firstdraft` reads this declaration with the matching
3838
declarations from exact, clean checkouts of `firstdraft/firstdraft` and `firstdraft/skills`. It implements SemVer 2.0
39-
precedence. This CLI requires the service's `0.2.0` API contract because the always-present GitHub Publication
40-
progress projection is incompatible with the strict response shape accepted by the published
41-
`@firstdraft.com/cli@0.1.0-alpha.2`. Comparator arrays form one conjunction, while `foundation_plan_formats` lists
42-
alternatives. A prerelease satisfies a comparator set only when a comparator explicitly names a prerelease with the
43-
same major, minor, and patch numbers. Skills names the candidate CLI version explicitly, so a stale comparator makes
44-
the three-repository candidate ineligible. `firstdraft.release-compatibility/1` is intentionally closed. The
45-
evaluator in `firstdraft/firstdraft` rejects an unrecognized format and unknown keys, so adding a key requires a
46-
coordinated compatibility-format bump rather than silently changing version 1.
39+
precedence. CLI `0.2.x` requires the service's `0.3.x` API contract because Analysis now returns the complete reviewed
40+
GapSet and digest. The released CLI `0.1.0` accepts only API `0.2.x`, uses a generic 2 MiB response bound for Analysis,
41+
and cannot safely consume every schema-valid API `0.3.x` result. CLI `0.2.x` retains that generic bound but gives
42+
Analysis and Compilation artifacts dedicated 128 MiB bounds. Comparator arrays form one conjunction, while
43+
`foundation_plan_formats` lists alternatives. A prerelease satisfies a comparator set only when a comparator
44+
explicitly names a prerelease with the same major, minor, and patch numbers. Skills names the candidate CLI version
45+
explicitly, so a stale comparator makes the three-repository candidate ineligible.
46+
`firstdraft.release-compatibility/1` is intentionally closed. The evaluator in `firstdraft/firstdraft` rejects an
47+
unrecognized format and unknown keys, so adding a key requires a coordinated compatibility-format bump rather than
48+
silently changing version 1.
4749

4850
A compatible result establishes candidate eligibility, not authorization or runtime proof. Exact Git SHAs identify
4951
the three-repository candidate. A merge to `main` is integration only: report the merged SHA and ask the user whether

docs/commands.md

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
This page owns the detailed public semantics of the current command surface. Run `firstdraft --help` or a command
44
group's `--help` for concise executable syntax. See [Errors and recovery](errors.md) before retrying a failed mutation.
55

6-
The current `0.1.x` line contains the auditable command shell, local Foundation Plan initialization, local
6+
The current `0.2.x` source line contains the auditable command shell, local Foundation Plan initialization, local
77
application-key and UUID generation, conditional whole-document push, whole-graph analysis status polling,
8-
compile-and-publish orchestration, and read-only retained-Compilation download. The current `0.1.0` CLI requires the
9-
service's `0.2.x` API contract. See the [release policy](../RELEASING.md) for versioning and channel semantics and
8+
compile-and-publish orchestration, and read-only retained-Compilation download. CLI `0.2.x` requires the service's
9+
`0.3.x` API contract. See the [release policy](../RELEASING.md) for versioning and channel semantics and
1010
[release history](release-history.md) for the transition from prereleases.
1111

1212
## Command map
@@ -116,11 +116,19 @@ or `superseded`. Every validated analysis status is a successful read with exit
116116
`analysis.status` value and inspect `analysis.diagnostics` rather than treating a completed analysis with issues as
117117
a transport failure.
118118

119+
The projection includes the exact Head digest, Analyzer and Compiler releases, selected target, and
120+
`analysis.gap_set` plus `analysis.gap_set_sha256`. A `valid` run always returns the complete parsed canonical
121+
`firstdraft.foundation-gaps/2` object, including every ordered gap record and an empty `gaps` array when nothing is
122+
missing. Both GapSet fields are `null` for every other status. The CLI validates the GapSet's Head, Project,
123+
generation, releases, target, canonical digest, and complete record shapes, then prints the records without
124+
truncating or rewriting them.
125+
119126
Status reads require the API origin pinned by a successful push. They never select an origin from the current
120127
environment, expose the private ETag, follow redirects, or modify local state. Each request has a bounded timeout,
121-
every response is byte-bounded and fully validated, and polling will not silently switch to a replacement analysis.
122-
The wait repeats only validated `processing` responses and stops on its first failed read. A network failure is safe
123-
to retry a bounded number of times because the command sends only `GET` requests. See
128+
ordinary response reads retain a 2 MiB bound, while this potentially gap-heavy response has a dedicated 128 MiB
129+
bound. Every response is fully validated, and polling will not silently switch to a replacement analysis. The wait
130+
repeats only validated `processing` responses and stops on its first failed read. A network failure is safe to retry
131+
a bounded number of times because the command sends only `GET` requests. See
124132
[read-only failures](errors.md#read-only-status-failures) if the problem persists.
125133

126134
## Compile and publish the current Plan
@@ -133,10 +141,10 @@ firstdraft plan compile
133141

134142
`plan compile` is the single terminal action. It first pushes the exact current bytes in
135143
`.firstdraft/foundation-plan.json`, even when those bytes are unchanged, and saves the accepted ETag using the same
136-
contract as `plan push`. It then waits up to two minutes for an analysis whose graph version exactly matches that
137-
accepted push, polling past a terminal result retained for an older Head. Invalid JSON, schema diagnostics, semantic
138-
diagnostics, a failed analysis, a superseded analysis, or a recurring diagnostic stop the command with structured
139-
output; no Compilation or Publication is requested.
144+
contract as `plan push`. It then waits up to two minutes for an analysis whose graph version and
145+
`head_source_sha256` exactly match that accepted push, polling past a terminal result retained for an older Head.
146+
Invalid JSON, schema diagnostics, semantic diagnostics, a failed analysis, a superseded analysis, or a recurring
147+
diagnostic stop the command with structured output; no Compilation or Publication is requested.
140148

141149
Only a `valid` analysis proceeds to the internal GitHub Publication lifecycle. Invoking `plan compile` is the
142150
authorization to request that lifecycle. Immediately before its conditional mutation, the CLI re-reads the local
@@ -152,7 +160,7 @@ URLs, raw server projections, local paths, or environment values. Success writes
152160
GitHub repository URL plus a newline to stdout. If the command fails after progress has begun, its structured JSON
153161
error envelope is the final stderr document after the progress lines.
154162

155-
The closed API `0.2.x` progress-reason allowlist is `github.configuration_missing`, `github.oauth_unavailable`,
163+
The closed API `0.3.x` progress-reason allowlist is `github.configuration_missing`, `github.oauth_unavailable`,
156164
`github.api_unavailable`, `github.reauthorization_required`, `github.account_mismatch`,
157165
`github.installation_unavailable`, `github.installation_not_ready`, `github.preflight_unavailable`, the legacy-only
158166
`github.preflight_unclassified`, and these stage-specific fallbacks: `github.preflight_unavailable.configuration`,
@@ -208,4 +216,5 @@ ETag, exact-byte SHA-256, canonical UTF-8 JSON envelope, provenance, metadata-on
208216
strict Base64 contents, file digests, modes, owners, and source-subject UUIDs. It writes only into a uniquely created
209217
sibling directory, verifies the complete tree, and atomically renames it into the still-absent destination. On
210218
POSIX, directories use mode `0755` and files use artifact-declared `0644` or `0755`; Windows verifies structure,
211-
contents, and digests without claiming POSIX mode bits.
219+
contents, and digests without claiming POSIX mode bits. The declared and streamed artifact envelope is bounded at
220+
128 MiB.

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@firstdraft.com/cli",
3-
"version": "0.1.0",
3+
"version": "0.2.0",
44
"description": "Command-line interface for First Draft",
55
"license": "MIT",
66
"type": "module",

release/compatibility.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
22
"format": "firstdraft.release-compatibility/1",
33
"component": "cli",
4-
"version": "0.1.0",
4+
"version": "0.2.0",
55
"requires": {
6-
"api_contract": [">= 0.2.0", "< 0.3.0"],
6+
"api_contract": [">= 0.3.0", "< 0.4.0"],
77
"foundation_plan_formats": ["firstdraft.foundation-plan.sketch/0.19"]
88
}
99
}

scripts/smoke-package.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,7 @@ async function exercisePackedCompilation(projectDirectory) {
332332
);
333333
const statusPath = `/v1/projects/${projectId}/compilations/${compilationId}`;
334334
const artifactPath = `${statusPath}/artifact`;
335+
const analyzerRelease = "foundation-plan-rails/application-2026-08";
335336
const compilerRelease = "foundation-plan-rails/compiler-scalar-2026-08";
336337
const target = { id: "rails", profile: "rails-sketch/2026-08" };
337338
const contents = Buffer.from("class Movie < ApplicationRecord\nend\n");
@@ -406,14 +407,30 @@ async function exercisePackedCompilation(projectDirectory) {
406407
completed_at: "2026-07-30T12:00:02.000000Z",
407408
},
408409
};
410+
const gapSet = {
411+
format: "firstdraft.foundation-gaps/2",
412+
source: { sha256: headSha256 },
413+
project: { id: projectId, graph_version: 1 },
414+
analysis: { release: analyzerRelease },
415+
compiler_release: compilerRelease,
416+
target,
417+
gaps: [],
418+
};
409419
const analysis = {
410420
project: { id: projectId, graph_version: 1 },
411421
analysis: {
412422
id: analysisId,
413423
graph_version: 1,
414-
analyzer_release: "foundation-plan-analyzer/2026-08",
424+
head_source_sha256: headSha256,
425+
analyzer_release: analyzerRelease,
426+
compiler_release: compilerRelease,
427+
target,
415428
status: "valid",
416429
diagnostics: [],
430+
gap_set: gapSet,
431+
gap_set_sha256: sha256(
432+
Buffer.from(`${JSON.stringify(gapSet, null, 2)}\n`),
433+
),
417434
started_at: "2026-07-30T12:00:00.000Z",
418435
completed_at: "2026-07-30T12:00:01.000Z",
419436
},

src/api-response.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ export async function sendRequest(fetchFunction, endpoint, request) {
3636
}
3737
}
3838

39-
/** @param {Response} response */
40-
export async function readResponseBody(response) {
41-
const bytes = await readResponseBytes(response);
39+
/** @param {Response} response @param {number} [maximumBytes] */
40+
export async function readResponseBody(response, maximumBytes) {
41+
const bytes = await readResponseBytes(response, maximumBytes);
4242

4343
let text;
4444
try {

src/commands/plan-compile.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ export async function compilePlan({
105105
const acceptedGraphVersion = /** @type {{graph_version: number}} */ (
106106
pushed.body.project
107107
).graph_version;
108+
const acceptedHeadSourceSha256 = /** @type {{source_sha256: string}} */ (
109+
pushed.body.foundation_plan
110+
).source_sha256;
108111

109112
let status;
110113
onProgress({ phase: "analysis", status: "waiting" });
@@ -133,7 +136,8 @@ export async function compilePlan({
133136
}
134137
if (
135138
status.body.project.graph_version !== acceptedGraphVersion ||
136-
status.body.analysis.graph_version !== acceptedGraphVersion
139+
status.body.analysis.graph_version !== acceptedGraphVersion ||
140+
status.body.analysis.head_source_sha256 !== acceptedHeadSourceSha256
137141
) {
138142
throw new PlanStatusChangedError(status.body);
139143
}

0 commit comments

Comments
 (0)