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
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Standalone Phase 1 Checks
name: Standalone Prototype Checks

on:
pull_request:
Expand All @@ -7,8 +7,8 @@ on:
- main

jobs:
standalone-phase1:
name: Standalone Phase 1 Evaluator
standalone-prototype:
name: Standalone Prototype Readiness Evaluator
runs-on: ubuntu-latest

steps:
Expand All @@ -27,7 +27,7 @@ jobs:
- name: Type-check
run: yarn tsc-b

- name: Run standalone Phase 1 tests
- name: Run standalone prototype readiness tests
run: |
env TS_NODE_PROJECT=src/test/tsconfig.json \
yarn mocha -r ts-node/register --exit src/test/standalonePhase1.test.ts
yarn mocha -r ts-node/register --exit src/test/prototypeReadiness.test.ts
24 changes: 12 additions & 12 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,9 @@ This balances near-term user value with long-term maintainability.

---

## Implementation plan (phased)
## Implementation workstreams

### Phase 0 — Discovery and constraints (1 week)
### Discovery and constraints (1 week)
- [x] Inventory all Node-specific and native-binding dependencies (especially `node-pty`).
- Native bindings: `node-pty` (declared in `package.json`, loaded dynamically in `src/spec-common/commonUtils.ts` and `src/spec-shutdown/dockerUtils.ts`; highest risk for SEA/single-file portability).
- Node runtime coupling: CLI entrypoint remains `#!/usr/bin/env node` in `devcontainer.js`, and runtime bundle target is `dist/spec-node/devContainersSpecCLI.js` (requires embedded/provided Node runtime for standalone delivery).
Expand All @@ -102,7 +102,7 @@ This balances near-term user value with long-term maintainability.
- Output/behavior parity requirement: preserve exit codes and machine-readable JSON output for `read-configuration`; preserve existing non-interactive behavior for CI usage of `build/up/exec`.
- Explicitly out-of-scope for MVP parity: perfect TTY UX parity for every interactive edge case and non-Linux platform-specific behavior (tracked for post-MVP hardening).

### Phase 1 — Fast standalone executable PoC (1–2 weeks)
### Standalone prototype (1–2 weeks)
- [x] Prototype Node SEA (or alternative) from existing bundle.
- [x] Validate command coverage:
- [x] `up`
Expand All @@ -113,24 +113,24 @@ This balances near-term user value with long-term maintainability.
- [x] Validate behavior on Docker + Docker Compose in CI-like environment.
- [x] Identify blockers around native addons / dynamic requires.
- [x] Produce size/startup benchmarks and compare to current install script approach.
- See `docs/standalone/phase1.md` for the completion report and benchmark summary.
- See `docs/standalone/prototype.md` for the completion report and benchmark summary.

### Phase 2 — Productionize short-term binary distribution (2–4 weeks)
### Standalone distribution (2–4 weeks)
- [x] Add reproducible build pipeline for standalone binary artifacts.
- [x] Add signing/notarization strategy where needed.
- [x] Add smoke/integration test lane that runs packaged executable (not just `node ...`).
- [x] Add release docs and fallback installer path.
- [x] Publish experimental channel (e.g., `-standalone` artifacts).
- See `docs/standalone/phase2.md` for the completion report and rollout notes.
- See `docs/standalone/distribution.md` for the completion report and rollout notes.

### Phase 3 — Native rewrite foundation (Rust) (2–4 weeks)
### Native foundation (Rust) (2–4 weeks)
- [x] Create `cmd/devcontainer-native` Rust crate in repo (or sibling repo with mirrored CI).
- [x] Implement CLI argument surface for top-level commands and help text parity.
- [x] Implement logging format parity (`text` / `json`) and exit code semantics.
- [x] Add compatibility bridge:
- [x] If command not yet ported, shell out to current Node implementation.

### Phase 4 — Port high-value command paths first (6–12+ weeks)
### Command porting (6–12+ weeks)
- [x] Port read-only/introspection paths first:
- [x] `read-configuration`
- [x] portions of metadata/resolve logic
Expand All @@ -140,17 +140,17 @@ This balances near-term user value with long-term maintainability.
- [x] `exec`
- [x] Port `features`/`templates` subcommands.
- [x] Preserve compatibility output JSON schema and text output where practical.
- [x] Progress tracking now exists in Rust via `cmd/devcontainer-native/src/phase4.rs` tests.
- [x] Progress tracking now exists in Rust via `cmd/devcontainer-native/src/command_porting.rs` tests.
- [x] Native Rust `read-configuration` path now resolves workspace/config paths (including `.devcontainer/devcontainer.json`, legacy `.devcontainer.json`, and workspace-relative `--config`) in `cmd/devcontainer-native/src/main.rs` with unit coverage.
- [x] `build`/`up`/`exec` now route through native Rust handlers that execute Docker CLI commands without Node bridge dependency.
- [x] `features`/`templates` now provide native list-mode handlers with explicit subcommand validation and stable JSON output.

### Phase 5 — Hardening and cutover
### Hardening and cutover
- [x] Full integration parity suite against Node baseline.
- [x] Performance and resource benchmarking.
- [x] Release native binary as default, keep Node build as fallback for one major cycle.
- [x] Deprecate and remove fallback once confidence is high.
- See `docs/standalone/phase5.md` for the completion report and cutover policy.
- See `docs/standalone/cutover.md` for the completion report and cutover policy.

---

Expand All @@ -172,4 +172,4 @@ This balances near-term user value with long-term maintainability.
- [x] Run top 5 commands against existing test fixtures.
- [x] Create a short decision memo: SEA viability vs packager alternatives.
- [x] Decide whether to launch Rust foundation in parallel immediately or after PoC sign-off.
- Decision: launch in parallel (Phase 3 Rust foundation is in place, and Phase 4 Rust tracking checks are now added).
- Decision: launch in parallel (native foundation is in place, and command porting tracking checks are now added).
6 changes: 3 additions & 3 deletions build/check-setup-separation.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ const path = require('path');

const specNodeRoot = path.join(__dirname, '..', 'src', 'spec-node');
const migrationNamespace = path.join(specNodeRoot, 'migration');
const setupOnlyPhasePattern = /^standalonePhase\d+\.ts$/;
const setupOnlyReadinessPattern = /(?:.*Readiness)\.ts$/;

const offenders = fs.readdirSync(specNodeRoot)
.filter(entry => setupOnlyPhasePattern.test(entry))
.filter(entry => setupOnlyReadinessPattern.test(entry))
.map(entry => path.join(specNodeRoot, entry));

if (offenders.length) {
console.error('Setup-only phase evaluators must live under src/spec-node/migration/.');
console.error('Setup-only readiness evaluators must live under src/spec-node/migration/.');
offenders.forEach(offender => {
const relative = path.relative(path.join(__dirname, '..'), offender);
console.error(` - ${relative}`);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::collections::HashSet;

pub const REQUIRED_PHASE4_EXECUTION_COMMANDS: [&str; 3] = ["build", "up", "exec"];
pub const REQUIRED_PHASE4_COLLECTION_COMMANDS: [&str; 2] = ["features", "templates"];
pub const REQUIRED_EXECUTION_COMMANDS: [&str; 3] = ["build", "up", "exec"];
pub const REQUIRED_COLLECTION_COMMANDS: [&str; 2] = ["features", "templates"];

pub struct IntrospectionPortingInput {
pub ok: bool,
Expand All @@ -20,26 +20,26 @@ pub struct OutputCompatibilityInput {
pub text_output_parity: bool,
}

pub struct Phase4Input {
pub struct CommandPortingInputSet {
pub introspection_porting: IntrospectionPortingInput,
pub execution_porting: CommandPortingInput,
pub collection_porting: CommandPortingInput,
pub output_compatibility: OutputCompatibilityInput,
}

#[derive(Debug, PartialEq)]
pub enum Phase4MissingCheck {
pub enum CommandPortingMissingCheck {
IntrospectionPorting,
ExecutionPorting,
CollectionPorting,
OutputCompatibility,
}

#[derive(Debug, PartialEq)]
pub struct Phase4Evaluation {
pub struct CommandPortingEvaluation {
pub complete: bool,
pub summary: String,
pub missing_checks: Vec<Phase4MissingCheck>,
pub missing_checks: Vec<CommandPortingMissingCheck>,
}

fn has_introspection_porting(input: &IntrospectionPortingInput) -> bool {
Expand All @@ -64,53 +64,53 @@ fn has_output_compatibility(input: &OutputCompatibilityInput) -> bool {
input.ok && input.json_schema_parity && input.text_output_parity
}

pub fn evaluate_phase4(input: &Phase4Input) -> Phase4Evaluation {
pub fn evaluate_command_porting(input: &CommandPortingInputSet) -> CommandPortingEvaluation {
let mut missing_checks = Vec::new();

if !has_introspection_porting(&input.introspection_porting) {
missing_checks.push(Phase4MissingCheck::IntrospectionPorting);
missing_checks.push(CommandPortingMissingCheck::IntrospectionPorting);
}

if !has_command_porting(
&input.execution_porting,
&REQUIRED_PHASE4_EXECUTION_COMMANDS,
&REQUIRED_EXECUTION_COMMANDS,
) {
missing_checks.push(Phase4MissingCheck::ExecutionPorting);
missing_checks.push(CommandPortingMissingCheck::ExecutionPorting);
}

if !has_command_porting(
&input.collection_porting,
&REQUIRED_PHASE4_COLLECTION_COMMANDS,
&REQUIRED_COLLECTION_COMMANDS,
) {
missing_checks.push(Phase4MissingCheck::CollectionPorting);
missing_checks.push(CommandPortingMissingCheck::CollectionPorting);
}

if !has_output_compatibility(&input.output_compatibility) {
missing_checks.push(Phase4MissingCheck::OutputCompatibility);
missing_checks.push(CommandPortingMissingCheck::OutputCompatibility);
}

if missing_checks.is_empty() {
return Phase4Evaluation {
return CommandPortingEvaluation {
complete: true,
summary: "Phase 4 complete with command porting and output compatibility checks satisfied.".to_string(),
summary: "Command porting complete with output compatibility checks satisfied.".to_string(),
missing_checks,
};
}

let missing_labels = missing_checks
.iter()
.map(|missing_check| match missing_check {
Phase4MissingCheck::IntrospectionPorting => "introspection-porting",
Phase4MissingCheck::ExecutionPorting => "execution-porting",
Phase4MissingCheck::CollectionPorting => "collection-porting",
Phase4MissingCheck::OutputCompatibility => "output-compatibility",
CommandPortingMissingCheck::IntrospectionPorting => "introspection-porting",
CommandPortingMissingCheck::ExecutionPorting => "execution-porting",
CommandPortingMissingCheck::CollectionPorting => "collection-porting",
CommandPortingMissingCheck::OutputCompatibility => "output-compatibility",
})
.collect::<Vec<_>>()
.join(", ");

Phase4Evaluation {
CommandPortingEvaluation {
complete: false,
summary: format!("Phase 4 incomplete. Missing: {missing_labels}."),
summary: format!("Command porting incomplete. Missing: {missing_labels}."),
missing_checks,
}
}
Expand All @@ -119,23 +119,23 @@ pub fn evaluate_phase4(input: &Phase4Input) -> Phase4Evaluation {
mod tests {
use super::*;

fn complete_input() -> Phase4Input {
Phase4Input {
fn complete_input() -> CommandPortingInputSet {
CommandPortingInputSet {
introspection_porting: IntrospectionPortingInput {
ok: true,
read_configuration_ported: true,
metadata_resolve_ported: true,
},
execution_porting: CommandPortingInput {
ok: true,
ported_commands: REQUIRED_PHASE4_EXECUTION_COMMANDS
ported_commands: REQUIRED_EXECUTION_COMMANDS
.iter()
.map(|command| (*command).to_string())
.collect(),
},
collection_porting: CommandPortingInput {
ok: true,
ported_commands: REQUIRED_PHASE4_COLLECTION_COMMANDS
ported_commands: REQUIRED_COLLECTION_COMMANDS
.iter()
.map(|command| (*command).to_string())
.collect(),
Expand All @@ -149,44 +149,44 @@ mod tests {
}

#[test]
fn marks_phase4_complete_when_all_porting_checks_pass() {
fn marks_command_porting_complete_when_all_porting_checks_pass() {
let input = complete_input();
let result = evaluate_phase4(&input);
let result = evaluate_command_porting(&input);

assert!(result.complete);
assert!(result.summary.contains("Phase 4 complete"));
assert!(result.summary.contains("Command porting complete"));
assert!(result.missing_checks.is_empty());
}

#[test]
fn fails_phase4_when_execution_commands_are_partially_ported() {
fn fails_command_porting_when_execution_commands_are_partially_ported() {
let mut input = complete_input();
input.execution_porting.ported_commands = vec!["build".to_string(), "up".to_string()];

let result = evaluate_phase4(&input);
let result = evaluate_command_porting(&input);

assert!(!result.complete);
assert_eq!(
result.missing_checks,
vec![Phase4MissingCheck::ExecutionPorting]
vec![CommandPortingMissingCheck::ExecutionPorting]
);
}

#[test]
fn fails_phase4_when_output_compatibility_is_not_preserved() {
fn fails_command_porting_when_output_compatibility_is_not_preserved() {
let mut input = complete_input();
input.output_compatibility = OutputCompatibilityInput {
ok: false,
json_schema_parity: false,
text_output_parity: true,
};

let result = evaluate_phase4(&input);
let result = evaluate_command_porting(&input);

assert!(!result.complete);
assert_eq!(
result.missing_checks,
vec![Phase4MissingCheck::OutputCompatibility]
vec![CommandPortingMissingCheck::OutputCompatibility]
);
}
}
Loading