diff --git a/AGENTS.md b/AGENTS.md index ad65f11..b4b5138 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,7 +11,7 @@ These instructions are binding for every agent working on Frameshift, whatever t - **Identity.** The project is named Frameshift, with exactly this capitalization. The CLI executable and command is `frameshift`. Do not introduce other names, temporary branding, or package identities. Naming conventions are in `docs/ENGINEERING.md` (section 8). - **Purpose.** A deterministic, rule-based engine that analyzes software projects and migrates them between frameworks while preserving behavior, with transparent reporting. The first migration path is Next.js (App Router) to Astro with Preact islands, as static output (`docs/MIGRATION_SPEC.md`). The product is a CLI. The engine is independent of it. -- **Current state: M0 through M4 complete; M5 implemented (`D-100`); migrate v0 is implemented under `D-101`, plan documents under `D-102`, and `frameshift.jsonc` under `D-103`.** A Cargo workspace, immutable source-text primitives, parser-directed JavaScript trivia, string, numeric, punctuation, template, identifier, and regular-expression literal scanners, complete regular-expression literal validation (including Annex B), identifier classification from generated Unicode 18.0.0 tables, character reference decoding with generated WHATWG and XHTML tables, a JavaScript parser with an arena tree for complete Script and Module source text (all statements, functions, classes, and modules, with sloppy mode, Annex B, and all early errors) with explicit JSX, TypeScript, TSX, and declaration-file dialects (the complete JSX specification grammar, and the TypeScript grammar of the pinned 6.0.3 compiler with structured type nodes, declarations, and extensions), a deterministic span edit core with provenance and tests, an Astro component printer with React JSX conversion and multi-source provenance, a development-only E2 driver with three fictional fixtures and golden files (`D-048`), a development-only E1 measurement harness with generated corpora and a directory mode for an approved real-world sample kept outside the repository (`D-043`), a development-only coverage-guided fuzzer (`D-044`), and a development-only processor-time helper in Go (`D-046`) exist. Rust 1.98.0 is pinned. A standard-library-only CLI shell provides help and version output (`D-050`). Native CI and gated release and packaging workflows exist (`D-051`, `D-052`); there is no migration engine pipeline. Version 0.1.0 is published (section 13 of `docs/IMPLEMENTATION.md`). All M1 foundation contracts are implemented (through `D-061`), and the owner accepted M1 on 2026-10-19 under `D-063`. M2 is complete (`D-076`, with the merge of its last increment): read-only discovery and Git-compatible `.gitignore` matching (`D-064`, `D-065`), the first product fixtures in `fixtures/discovery/`, a read-once snapshot store with a framed snapshot identity (`D-067`), a JSON and JSONC parser (`D-068`), a full YAML 1.2 syntax parser (`D-071`), a Yarn classic lockfile parser (`D-072`), the package model: npm versions and ranges with node-semver's semantics, the `package.json` model, package manager detection, and npm, Bun, pnpm 9.0, and Yarn classic and Berry lockfiles (`D-069`, `D-070`, `D-071`, `D-072`), workspace detection with the pattern semantics of eight package manager releases (`D-073`), Next.js framework, version, configuration, and router detection (`D-074`), `frameshift inspect` with the versioned `frameshift.inspection` document (`D-075`), and adversarial inspect fixtures (`D-076`). M3 includes the CSS tokenizer and generic structural parser with bounded recovery (`D-077`, `D-078`); the Sass parser now covers statements and SassScript for explicit SCSS and indented syntax modes (`D-079`), with structured selectors (`D-081`), validated module rules (`D-082`), structured import items with checked URLs and CSS-versus-Sass classification, structured media queries, supports conditions, and import modifiers (`D-083`), and structured `@extend` targets and `@at-root` queries (`D-084`); modern CSS-style `if()` branches and conditions and optional JavaScript diagnostic recovery (`D-085`); plain CSS function passthrough with raw result declarations (`D-086`); raw URL versus SassScript-call parsing (`D-087`); raw special functions (`D-088`); numeric unit boundaries and default-export expression diagnostic recovery (`D-089`); Sass syntax conformance is established against the pinned normal parser and full external corpus (`D-090`), with explicit evaluation-dependent evidence. M3 closure and the first M4 module-resolution and import-graph APIs are recorded in `D-091` and IMPLEMENTATION.md section 37; package scopes, `imports`, self-references, and directory entry points (section 38) scopes and bindings (section 39), the App Router route tree with render contexts (section 40), and the static evaluator with experiment E3 (section 41) follow under `D-092`, the four M4 semantic blocks delivered as stacked pull requests that the agent merges after green CI. The Node10 profile and module suffixes (`D-093`, `D-094`, sections 42 and 43), the assembled source model (`D-095`, section 44), the component graph and environment classification (`D-096`, sections 45 and 46), and configuration inheritance from linked workspace packages (`D-098`, section 48) follow. M4 closed under `D-099` (section 49), and M5 began with the feature catalog and its detection (section 50); read-only assessment, accounting, route readiness, knowledge, metrics, reports, and CLI commands now exist under `D-100` (section 51). A reduced first `frameshift migrate` (migrate v0, M6-lite and M7-lite) exists under `D-101` (section 52). E1 and E2 passed (`D-047`, `D-049`), `P-002` and `P-003` are accepted, and M0 is complete; the branch-protection policy requires green CI and PR integration (`D-051`). See `docs/IMPLEMENTATION.md` for actual code and contracts; the other documents still specify the remaining product. The remote `origin` is `eiserv/frameshift` (`D-026`), default branch `main`; preparation for public visibility and AGPL licensing is accepted in `D-066`, but the owner made the repository public on 2026-10-17. Work on branches and integrate through pull requests, subject to the explicit one-run exceptions in `D-010`. +- **Current state: M0 through M4 complete; M5 implemented (`D-100`); migrate v0 is implemented under `D-101`, and M6 planning under `D-102`, `D-103`, and `D-104`.** A Cargo workspace, immutable source-text primitives, parser-directed JavaScript trivia, string, numeric, punctuation, template, identifier, and regular-expression literal scanners, complete regular-expression literal validation (including Annex B), identifier classification from generated Unicode 18.0.0 tables, character reference decoding with generated WHATWG and XHTML tables, a JavaScript parser with an arena tree for complete Script and Module source text (all statements, functions, classes, and modules, with sloppy mode, Annex B, and all early errors) with explicit JSX, TypeScript, TSX, and declaration-file dialects (the complete JSX specification grammar, and the TypeScript grammar of the pinned 6.0.3 compiler with structured type nodes, declarations, and extensions), a deterministic span edit core with provenance and tests, an Astro component printer with React JSX conversion and multi-source provenance, a development-only E2 driver with three fictional fixtures and golden files (`D-048`), a development-only E1 measurement harness with generated corpora and a directory mode for an approved real-world sample kept outside the repository (`D-043`), a development-only coverage-guided fuzzer (`D-044`), and a development-only processor-time helper in Go (`D-046`) exist. Rust 1.98.0 is pinned. A standard-library-only CLI shell provides help and version output (`D-050`). Native CI and gated release and packaging workflows exist (`D-051`, `D-052`); there is no migration engine pipeline. Version 0.1.0 is published (section 13 of `docs/IMPLEMENTATION.md`). All M1 foundation contracts are implemented (through `D-061`), and the owner accepted M1 on 2026-10-19 under `D-063`. M2 is complete (`D-076`, with the merge of its last increment): read-only discovery and Git-compatible `.gitignore` matching (`D-064`, `D-065`), the first product fixtures in `fixtures/discovery/`, a read-once snapshot store with a framed snapshot identity (`D-067`), a JSON and JSONC parser (`D-068`), a full YAML 1.2 syntax parser (`D-071`), a Yarn classic lockfile parser (`D-072`), the package model: npm versions and ranges with node-semver's semantics, the `package.json` model, package manager detection, and npm, Bun, pnpm 9.0, and Yarn classic and Berry lockfiles (`D-069`, `D-070`, `D-071`, `D-072`), workspace detection with the pattern semantics of eight package manager releases (`D-073`), Next.js framework, version, configuration, and router detection (`D-074`), `frameshift inspect` with the versioned `frameshift.inspection` document (`D-075`), and adversarial inspect fixtures (`D-076`). M3 includes the CSS tokenizer and generic structural parser with bounded recovery (`D-077`, `D-078`); the Sass parser now covers statements and SassScript for explicit SCSS and indented syntax modes (`D-079`), with structured selectors (`D-081`), validated module rules (`D-082`), structured import items with checked URLs and CSS-versus-Sass classification, structured media queries, supports conditions, and import modifiers (`D-083`), and structured `@extend` targets and `@at-root` queries (`D-084`); modern CSS-style `if()` branches and conditions and optional JavaScript diagnostic recovery (`D-085`); plain CSS function passthrough with raw result declarations (`D-086`); raw URL versus SassScript-call parsing (`D-087`); raw special functions (`D-088`); numeric unit boundaries and default-export expression diagnostic recovery (`D-089`); Sass syntax conformance is established against the pinned normal parser and full external corpus (`D-090`), with explicit evaluation-dependent evidence. M3 closure and the first M4 module-resolution and import-graph APIs are recorded in `D-091` and IMPLEMENTATION.md section 37; package scopes, `imports`, self-references, and directory entry points (section 38) scopes and bindings (section 39), the App Router route tree with render contexts (section 40), and the static evaluator with experiment E3 (section 41) follow under `D-092`, the four M4 semantic blocks delivered as stacked pull requests that the agent merges after green CI. The Node10 profile and module suffixes (`D-093`, `D-094`, sections 42 and 43), the assembled source model (`D-095`, section 44), the component graph and environment classification (`D-096`, sections 45 and 46), and configuration inheritance from linked workspace packages (`D-098`, section 48) follow. M4 closed under `D-099` (section 49), and M5 began with the feature catalog and its detection (section 50); read-only assessment, accounting, route readiness, knowledge, metrics, reports, and CLI commands now exist under `D-100` (section 51). A reduced first `frameshift migrate` (migrate v0, M6-lite and M7-lite) exists under `D-101` (section 52). E1 and E2 passed (`D-047`, `D-049`), `P-002` and `P-003` are accepted, and M0 is complete; the branch-protection policy requires green CI and PR integration (`D-051`). See `docs/IMPLEMENTATION.md` for actual code and contracts; the other documents still specify the remaining product. The remote `origin` is `eiserv/frameshift` (`D-026`), default branch `main`; preparation for public visibility and AGPL licensing is accepted in `D-066`, but the owner made the repository public on 2026-10-17. Work on branches and integrate through pull requests, subject to the explicit one-run exceptions in `D-010`. - **Implementation language.** Rust is the primary language for engine and CLI (`D-018`). Other languages are allowed only for specific, important tasks with a demonstrable quality advantage, a decision record, and the requirements in `docs/ENGINEERING.md` section 2.7 (`D-019`); the current uses are the development-only processor-time helper (`D-046`) and release archive helper (`D-052`), both in Go. Effort and time are not arguments against quality (`D-020`). The M0 workspace used the Rust standard library only (`D-032`). The reviewed runtime SHA-256 dependency and its exact transitive graph are recorded in `D-058` and `.github/dependencies.json`. Do not add dependencies or bypass the E1/E2 experiment gates without the required decisions. - **Documentation map.** - `README.md`: overview and status. @@ -43,7 +43,7 @@ These instructions are binding for every agent working on Frameshift, whatever t - **M5 analysis (`D-100`).** `frameshift_engine::analysis` owns read-only assessment, counting/file accounting, transitive readiness, knowledge, exact metrics, and `frameshift.analysis` v1. Reuse `project::build_with`; no identifier-spelling inference or source execution. Unknown preconditions stay unknown, and assessment predicates never count as transformation coverage. Stored reports are bounded, validated data and never authorize reading their paths or executing operations. The CLI exposes `analyze`, stored `report`, `explain`, and assessment `rules`; explicit analysis report outputs are new files outside the source. Golden updates use `cargo test -p frameshift-engine --offline --locked --test analysis -- --ignored update_analysis_golden`. Corpus and reference-fixture commands, limits, current inference boundaries, and validated weights are in IMPLEMENTATION.md section 51. -- **Migrate v0 (`D-101`).** `frameshift_engine::migrate` plans only from `analysis::analyze_parts` (the analysis plus its snapshot, project model, and manifest); never read the source again or write outside `frameshift-output`. A conversion that moves bindings into the frontmatter scope must check them against that scope, and a relocation must refuse what the module graph has no edge for. A stored plan document is only compared with the recomputed one, never parsed or interpreted (`D-102`). Convert only units with status `auto` or `review` and strategy `astro` or `preact-static`; preserve everything else below `.frameshift/unmigrated/` with a manual task, and keep the preservation fixed point so the output never imports a file that was not written. Every rule refusal is a preserved file with a reason, never a guessed conversion. Fixtures are in `fixtures/migrate/` (IMPLEMENTATION.md section 52). +- **Migrate v0 (`D-101`).** `frameshift_engine::migrate` plans only from `analysis::analyze_parts` (the analysis plus its snapshot, project model, and manifest); never read the source again or write outside `frameshift-output`. A conversion that moves bindings into the frontmatter scope must check them against that scope, and a relocation must refuse what the module graph has no edge for. A stored plan document is only compared with the recomputed one, never parsed or interpreted (`D-102`). Every operation names a rule in `migrate/rules.rs` of its own kind, and one source file reaches one rule; raise a rule's version whenever its output can change (`D-104`). Convert only units with status `auto` or `review` and strategy `astro` or `preact-static`; preserve everything else below `.frameshift/unmigrated/` with a manual task, and keep the preservation fixed point so the output never imports a file that was not written. Every rule refusal is a preserved file with a reason, never a guessed conversion. Fixtures are in `fixtures/migrate/` (IMPLEMENTATION.md section 52). - **Component graph (`D-096`).** `frameshift_engine::project::Project::components` resolves render sites through bindings and the module graph's own edges (IMPLEMENTATION.md section 45). Recognize wrappers (`memo`, `forwardRef`, `lazy`, `next/dynamic`) only through binding provenance of their imports, confirmed by the import edge's package resolution, never by callee spelling; every other call is a `Call` reason. Follow `export *` with ECMAScript's rules and keep several or unknowable providers `AmbiguousStar`. Linking charges `modules.max-work` and returns no partial graph. Its golden changes only through `cargo test -p frameshift-engine --offline --locked --test components -- --ignored update_components_golden`. diff --git a/crates/frameshift-cli/src/analysis_cli.rs b/crates/frameshift-cli/src/analysis_cli.rs index 994c781..8e1abb4 100644 --- a/crates/frameshift-cli/src/analysis_cli.rs +++ b/crates/frameshift-cli/src/analysis_cli.rs @@ -10,6 +10,7 @@ use frameshift_engine::analysis::{ use frameshift_engine::config::PackageChoice; use frameshift_engine::foundation::{PathLimits, SourcePath}; use frameshift_engine::limits::Limits; +use frameshift_engine::migrate; use std::collections::BTreeSet; use std::ffi::OsString; use std::io::{self, Write}; @@ -379,6 +380,20 @@ pub(crate) fn run( feature.v1 )?; } + // Transformation rules exist for what migrates, not for what is unsupported. + if !unsupported { + for rule in migrate::rules::RULES { + writeln!( + stdout, + "{}/{} v{} {} {}", + frameshift_engine::catalog::PATH, + rule.id, + rule.version, + rule.kind.as_str(), + rule.summary + )?; + } + } Ok(0) } Request::Analyze(options) => { @@ -550,6 +565,20 @@ fn explain(id: &str, stdout: &mut impl Write, stderr: &mut impl Write) -> io::Re writeln!(stdout, "{id} {message}\n\n{explanation}\n\n{remediation}")?; return Ok(0); } + let transformation = id + .strip_prefix(frameshift_engine::catalog::PATH) + .and_then(|rest| rest.strip_prefix('/')) + .and_then(migrate::rules::find); + if let Some(rule) = transformation { + writeln!( + stdout, + "{id} version {}, operation {}\n\n{}", + rule.version, + rule.kind.as_str(), + rule.summary + )?; + return Ok(0); + } let feature = if let Some(name) = id.strip_prefix("nextjs-to-astro-preact/assessment.") { frameshift_engine::catalog::FEATURES.iter().find(|f| { matches!(f.v1, frameshift_engine::catalog::V1::Assessed(_)) diff --git a/crates/frameshift-cli/tests/analysis.rs b/crates/frameshift-cli/tests/analysis.rs index b5ec5cc..dcabe06 100644 --- a/crates/frameshift-cli/tests/analysis.rs +++ b/crates/frameshift-cli/tests/analysis.rs @@ -356,7 +356,9 @@ fn assessment_rule_metadata_excludes_out_of_scope_features() { .map(|l| l.split_whitespace().next().unwrap()) .collect(); let expected: Vec<_> = FEATURES.iter().filter(|f| matches!(f.v1, V1::Assessed(statuses) if !unsupported_only || statuses.contains(&Status::Unsupported))) - .map(|f| format!("{PATH}/assessment.{}", f.id.replace('.', "-"))).collect(); + .map(|f| format!("{PATH}/assessment.{}", f.id.replace('.', "-"))) + .chain(frameshift_engine::migrate::rules::RULES.iter().filter(|_| !unsupported_only).map(|r| format!("{PATH}/{}", r.id))) + .collect(); assert_eq!(actual, expected); } for feature in FEATURES.iter().filter(|f| f.v1 == V1::OutOfScope) { @@ -387,6 +389,8 @@ fn explanations_and_help_are_available_without_a_source_project() { "explain", "nextjs-to-astro-preact/assessment.nextjs-api-request", ], + vec!["explain", "nextjs-to-astro-preact/route.static-page"], + vec!["rules", "show", "nextjs-to-astro-preact/layout.chain"], vec!["rules"], vec!["help", "analyze"], vec!["analyze", "--help"], diff --git a/crates/frameshift-engine/src/migrate/mod.rs b/crates/frameshift-engine/src/migrate/mod.rs index f2bffce..92df891 100644 --- a/crates/frameshift-engine/src/migrate/mod.rs +++ b/crates/frameshift-engine/src/migrate/mod.rs @@ -11,6 +11,7 @@ mod convert; mod plan_document; +pub mod rules; mod scaffold; use std::collections::{BTreeMap, BTreeSet}; @@ -1099,6 +1100,7 @@ impl<'a> Planner<'a> { "two operations write one output path", )); } + check_rules(&operations)?; let snapshot = self.snapshot().identity().map_err(|error| MigrateError { kind: MigrateErrorKind::Input, message: error.to_string(), @@ -1123,6 +1125,29 @@ impl<'a> Planner<'a> { } } +// Every operation names a registered rule of its own kind, and rules are +// exclusive: one source file reaches at most one of them (FS4001 fails closed). +fn check_rules(operations: &[Operation]) -> Result<(), MigrateError> { + let mut sources = BTreeSet::new(); + for operation in operations { + if rules::find(operation.rule).is_none_or(|rule| rule.kind != operation.kind) { + return Err(MigrateError::internal( + "an operation names an unregistered rule", + )); + } + if operation + .source + .as_ref() + .is_some_and(|source| !sources.insert(source)) + { + return Err(MigrateError::internal( + "two rules produced output for one source file", + )); + } + } + Ok(()) +} + // Why a page is outside v0 before any conversion is attempted. fn page_refusal(route: &AppRoute) -> Option<&'static str> { if route.segments.iter().any(|s| { diff --git a/crates/frameshift-engine/src/migrate/plan_document.rs b/crates/frameshift-engine/src/migrate/plan_document.rs index 9296b04..7ed6212 100644 --- a/crates/frameshift-engine/src/migrate/plan_document.rs +++ b/crates/frameshift-engine/src/migrate/plan_document.rs @@ -8,7 +8,7 @@ use frameshift_core::{Sha256Digest, Sha256Hasher, SourcePath}; use frameshift_input::SnapshotId; -use super::{ManualTask, MigrateError, MigrateErrorKind, Operation}; +use super::{ManualTask, MigrateError, MigrateErrorKind, Operation, rules}; use crate::document::{self, Value as V}; use crate::limits::Limits; @@ -64,10 +64,13 @@ pub(super) fn print( let mut entries = Vec::with_capacity(operations.len()); for operation in operations { let digest = Sha256Digest::hash(operation.bytes(), limits.hash).map_err(|_| too_large())?; + let rule = rules::find(operation.rule) + .ok_or_else(|| MigrateError::internal("an operation names an unregistered rule"))?; entries.push(V::Object(vec![ ("kind", V::string(operation.kind.as_str())), ("output", V::string(operation.output.as_str())), - ("rule", V::string(operation.rule)), + ("rule", V::string(rule.id)), + ("ruleVersion", V::Number(rule.version)), ("source", optional_path(operation.source.as_ref())), ("review", V::Bool(operation.review)), ("size", V::count(operation.bytes().len())), diff --git a/crates/frameshift-engine/src/migrate/rules.rs b/crates/frameshift-engine/src/migrate/rules.rs new file mode 100644 index 0000000..7f371ba --- /dev/null +++ b/crates/frameshift-engine/src/migrate/rules.rs @@ -0,0 +1,110 @@ +//! The compile-time registry of migrate v0's transformation rules (`D-104`). +//! +//! A rule's version increases whenever its output can change. Planning checks +//! every operation against this registry, so an operation can only name a +//! registered rule of its own kind, and one source file can only reach one +//! rule (ARCHITECTURE.md section 12.2). + +use super::OperationKind; + +/// One transformation rule. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Rule { + /// The identifier below the catalog's migration path, such as + /// `route.static-page`. + pub id: &'static str, + /// Increases whenever the rule's output can change. + pub version: u64, + /// The operation that the rule produces. + pub kind: OperationKind, + /// One sentence that says what the rule does. + pub summary: &'static str, +} + +const fn rule(id: &'static str, kind: OperationKind, summary: &'static str) -> Rule { + Rule { + id, + version: 1, + kind, + summary, + } +} + +/// Every registered rule, sorted by identifier. +pub const RULES: &[Rule] = &[ + rule( + "asset.verbatim", + OperationKind::Copy, + "Copies a public asset or non-code data file unchanged.", + ), + rule( + "component.astro", + OperationKind::Create, + "Converts a server component into an Astro component.", + ), + rule( + "component.preact-static", + OperationKind::Transform, + "Keeps a component as a script for Preact and rewrites its imports.", + ), + rule( + "config.astro", + OperationKind::Config, + "Generates astro.config.mjs with the Preact integration.", + ), + rule( + "config.gitignore", + OperationKind::Config, + "Keeps the source .gitignore entries and adds Astro's build directories.", + ), + rule( + "config.package-manifest", + OperationKind::Config, + "Generates package.json without Next.js and React, with the pinned target profile.", + ), + rule( + "config.tsconfig", + OperationKind::Config, + "Generates tsconfig.json with Astro's strict settings and Preact's JSX settings.", + ), + rule( + "layout.chain", + OperationKind::Create, + "Converts a layout into an Astro layout that wraps its parent.", + ), + rule( + "metadata.favicon", + OperationKind::Copy, + "Copies the root favicon to public/.", + ), + rule( + "module.relocate", + OperationKind::Transform, + "Moves a module or declaration file and rewrites its local specifiers.", + ), + rule( + "preserve.unmigrated", + OperationKind::Preserve, + "Copies an unmigrated file below .frameshift/unmigrated/ and adds a manual task.", + ), + rule( + "report.summary", + OperationKind::Config, + "Generates .frameshift/SUMMARY.md with the results and manual tasks.", + ), + rule( + "route.static-page", + OperationKind::Create, + "Converts a static page into an Astro page inside its nearest layout.", + ), + rule( + "style.relocate", + OperationKind::Copy, + "Moves a stylesheet whose references stay valid at its new location.", + ), +]; + +/// Looks up a rule by its identifier below the catalog's migration path. +pub fn find(id: &str) -> Option<&'static Rule> { + RULES.iter().find(|rule| rule.id == id) +} diff --git a/crates/frameshift-engine/tests/migrate.rs b/crates/frameshift-engine/tests/migrate.rs index ca92092..f0c649f 100644 --- a/crates/frameshift-engine/tests/migrate.rs +++ b/crates/frameshift-engine/tests/migrate.rs @@ -16,6 +16,7 @@ use std::collections::BTreeMap; use std::fs; use std::path::{Path, PathBuf}; +use frameshift_engine::foundation::{IdentifierLimits, RuleId}; use frameshift_engine::migrate::{self, MigrateErrorKind, MigrateOptions, Plan}; use support::Json; @@ -247,6 +248,20 @@ fn execution_writes_exactly_the_plan_and_never_touches_the_source() { } } +#[test] +fn the_rule_registry_is_sorted_unique_and_catalog_spelled() { + let rules = migrate::rules::RULES; + assert!(rules.windows(2).all(|pair| pair[0].id < pair[1].id)); + for rule in rules { + let id = format!("{}/{}", frameshift_engine::catalog::PATH, rule.id); + RuleId::new(&id, IdentifierLimits::default()).unwrap_or_else(|_| panic!("{id}")); + assert!(rule.version >= 1, "{id}"); + assert!(rule.summary.ends_with('.'), "{id}"); + assert_eq!(migrate::rules::find(rule.id), Some(rule)); + } + assert_eq!(migrate::rules::find("route.unknown"), None); +} + #[test] fn plan_hashes_bind_the_source_and_the_selected_package() { let case = cases().into_iter().next().unwrap(); diff --git a/docs/CLI.md b/docs/CLI.md index 561b346..af9cf4f 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -231,7 +231,7 @@ frameshift rules [--to ] [--unsupported] frameshift rules show ``` -**Implemented for assessment metadata (`D-100`).** Lists only `V1::Assessed` feature predicates with their version, feature, and allowed catalog statuses. Out-of-scope features have no assessment rule: their invented full rule IDs are rejected, while `explain ` describes the scope exclusion. These predicates assess source; they emit no operations and do not count as transformation rule coverage. Transformation-rule listing and the detailed transformation examples below remain planned. `--unsupported` lists features that are detected but not migratable in the current stage. `show` prints the assessment predicate's catalog statuses, version, specification, and evidence boundary; detailed transformation examples and verification requirements remain planned. +**Implemented for assessment metadata (`D-100`).** Lists only `V1::Assessed` feature predicates with their version, feature, and allowed catalog statuses. Out-of-scope features have no assessment rule: their invented full rule IDs are rejected, while `explain ` describes the scope exclusion. These predicates assess source; they emit no operations and do not count as transformation rule coverage. Transformation rules are listed after them with their version, operation, and summary (`D-104`), and `explain` and `rules show` accept their full identifiers. The detailed transformation examples below remain planned. `--unsupported` lists features that are detected but not migratable in the current stage. `show` prints the assessment predicate's catalog statuses, version, specification, and evidence boundary; detailed transformation examples and verification requirements remain planned. ```text $ frameshift rules --to astro-preact diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 81ec59e..5b8b96e 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -111,6 +111,7 @@ Statuses change through an edit to this file under the integration workflow in D | D-101 | Migrate v0: a reduced first `migrate` for static App Router sites | Accepted | | D-102 | M6 plan document, plan hash, and `frameshift plan` | Accepted | | D-103 | The key set of `frameshift.jsonc` | Accepted | +| D-104 | The rule registry of migrate | Accepted | | P-001 | Go as the single implementation language, CGO disabled | Superseded by D-018 | | P-002 | Purpose-built parsers | Accepted | | P-003 | Span-based edits and targeted printers; no concrete syntax tree initially | Accepted | @@ -1200,6 +1201,14 @@ Statuses change through an edit to this file under the integration workflow in D - **Binding:** The plan document records the effective `to` and `package`. A file in the source root is also covered by the snapshot identity, while a file outside it affects a plan only through those options. - **Revisit if:** A real project needs `exclude`. +### D-104: The rule registry of migrate + +- **Status:** Accepted (2026-11-01), under the owner's instruction to finish M6 first. It closes M6 together with `D-102` and `D-103`. +- **Decision:** `frameshift_engine::migrate::rules::RULES` is a compile-time, sorted list of rule identifiers below the migration path, each with a version, an operation kind, and a summary. Planning checks every operation against it: an unregistered rule or a rule of another kind is an internal error, and so are two operations for one source file, because a source file must reach exactly one rule. Each plan operation records `ruleVersion`, so a rule change makes older plans stale. `frameshift rules` lists the registered rules after the assessment predicates, and `explain` and `rules show` accept their full identifiers. +- **Rationale:** Rule selection in migrate v0 is one classification function, so exclusivity holds by construction and the plan-time check is its fail-closed proof. The declarative preconditions and the feature ID index of ARCHITECTURE.md section 12 add nothing until several rules can match one occurrence, so they stay planned and are not simulated by metadata without an effect. +- **Tradeoffs:** Selection logic is not data yet, so `rules` lists what exists without its preconditions, and rule coverage metrics stay at their current meaning. +- **Revisit if:** M8 or M9 adds rules that can match the same occurrence. Then preconditions and the `excludes` relation become data. + ## Technique proposals and related accepted decisions diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md index cab5849..83b31d5 100644 --- a/docs/IMPLEMENTATION.md +++ b/docs/IMPLEMENTATION.md @@ -1968,7 +1968,7 @@ The hash is SHA-256 of the domain line `frameshift.plan.v1` and the printed docu **Validation.** `crates/frameshift-engine/tests/migrate.rs` compares `expected/plan.json` with each fixture's document, with `@ENGINE_VERSION@` and `@PLAN_HASH@` in place of the release-dependent values, and checks that a file no operation copies changes the snapshot identity and the hash. `crates/frameshift-cli/tests/migrate.rs` checks that `--out` and `--format json` agree, that an existing destination and a destination inside the source are refused, that a tampered, missing, or stale document exits with 3 and writes nothing, and that the matching document produces the golden tree. -**Boundaries.** The rule registry of M6 is outstanding, so rule versions are absent; `binding.options` holds the effective `to` and `package`. Later commands do not read a stored plan for anything except this comparison. +**Boundaries.** `binding.options` holds the effective `to` and `package`, and operations record their rule version (section 55). Later commands do not read a stored plan for anything except this comparison. ## 54. Project configuration (`D-103`) @@ -1979,3 +1979,9 @@ The hash is SHA-256 of the domain line `frameshift.plan.v1` and the printed docu **Validation.** `crates/frameshift-engine/tests/config.rs` covers the key set, comments, rejected keys and values without echoing them, and the admitted-file rule. `crates/frameshift-cli/tests/migrate.rs` covers precedence, `--config`, `--no-config`, and that the file stays out of the output. **Boundaries.** There is no `exclude` or `envNames` key (`D-103`). The planned `--env-names` option remains a usage error. + +## 55. Rule registry (`D-104`) + +`migrate/rules.rs` lists every transformation rule of migrate with its version, operation kind, and summary. `Planner::finish` rejects an operation whose rule is unregistered or of another kind, and two operations for one source file; both are internal errors, which fail closed. The plan document records `ruleVersion` for each operation. `frameshift rules` prints the rules after the assessment predicates, except with `--unsupported`, and `explain` and `rules show` print one rule by its full identifier, such as `nextjs-to-astro-preact/route.static-page`. + +Tests check that the registry is sorted and unique, that every identifier passes `RuleId`'s grammar, and that every fixture's plan passes the checks. With this section, M6's scope exists; the preconditions, `excludes` relation, and source spans of ARCHITECTURE.md section 12 remain planned until rules can compete for one occurrence. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 29e7156..06d4127 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,6 +1,6 @@ # Roadmap -**Status: plan with M0 through M4 complete and M5 implemented (`D-100`); a reduced migrate v0 (M6-lite and M7-lite) is implemented under `D-101`, plan documents under `D-102`, and the configuration file under `D-103`.** The minimal Rust workspace, source-text foundation, parser-directed JavaScript scanners, complete regular-expression validation, pinned Unicode identifier classification, span edit core, an Astro component printer with JSX conversion and an E2 driver with fictional fixtures, a JavaScript parser with an arena tree for complete Script and Module source text and explicit JSX, TypeScript, TSX, and declaration-file dialects, an E1 measurement harness with generated corpora and a real-world sample, a coverage-guided fuzzer, and a processor-time helper for its budget runs are implemented (`D-032`, `D-039`, `D-040`, `D-041`, `D-042`, `D-043`, `D-044`, `D-046`, `D-048`, [IMPLEMENTATION.md](IMPLEMENTATION.md)). E1 and E2 passed on 2026-10-13, `P-002` and `P-003` are accepted, and M0 is complete (`D-047`, `D-049`); M1 has started with the grouped engine facade and the help and version CLI shell (`D-050`). Native CI, E2 hash comparison, gated Release Please PR automation, and six-platform packaging now exist (`D-051`, `D-052`). Canonical source/output paths (`D-054`) and stable catalog identifier values (`D-055`), and percent-encoded unit identity builders (`D-056`) are implemented. Bounded finding records and complete report-local ordering are implemented (`D-057`). Runtime SHA-256 hashing and canonical digest values are implemented with a reviewed dependency graph and dependency-aware CI and release checks (`D-058`, `D-059`). Run limits with atomic budgets, a reconciled limit set, and deterministic collections complete the foundation contracts (`D-060`, `D-061`), and the product fixture layout is decided (`D-062`). M1 was accepted by the owner on 2026-10-19 under `D-063`; live CI and branch protection status are recorded in IMPLEMENTATION.md. M2 is complete (`D-076`): the read-only discovery walker and Git-compatible `.gitignore` matching, with the first product fixtures (`D-064`, `D-065`), followed by the read-once snapshot store and the JSON and JSONC parser (`D-067`, `D-068`), the package model with npm, Bun, pnpm, and Yarn lockfiles, and full YAML 1.2 syntax (`D-069` to `D-072`), workspace and Next.js detection (`D-073`, `D-074`), `frameshift inspect` with the versioned `frameshift.inspection` document (`D-075`), and adversarial inspect fixtures (`D-076`). Version 0.1.0 is published. M3 has started with the CSS tokenizer and generic structural parser (`D-076` to `D-078`), followed by the start of the SCSS and indented Sass parser (`D-079`), its structured selectors (`D-081`), module rules (`D-082`), media queries and supports conditions (`D-083`), and extension targets and `@at-root` queries (`D-084`). Milestones are defined by capabilities and technical dependencies, not by calendar dates (`D-014`). The three-stage strategy is an accepted decision (`D-016`). The contents of each stage are a plan and will be refined as work progresses. +**Status: plan with M0 through M4 complete and M5 implemented (`D-100`); a reduced migrate v0 (M6-lite and M7-lite) is implemented under `D-101`, and M6 planning is implemented under `D-102`, `D-103`, and `D-104`.** The minimal Rust workspace, source-text foundation, parser-directed JavaScript scanners, complete regular-expression validation, pinned Unicode identifier classification, span edit core, an Astro component printer with JSX conversion and an E2 driver with fictional fixtures, a JavaScript parser with an arena tree for complete Script and Module source text and explicit JSX, TypeScript, TSX, and declaration-file dialects, an E1 measurement harness with generated corpora and a real-world sample, a coverage-guided fuzzer, and a processor-time helper for its budget runs are implemented (`D-032`, `D-039`, `D-040`, `D-041`, `D-042`, `D-043`, `D-044`, `D-046`, `D-048`, [IMPLEMENTATION.md](IMPLEMENTATION.md)). E1 and E2 passed on 2026-10-13, `P-002` and `P-003` are accepted, and M0 is complete (`D-047`, `D-049`); M1 has started with the grouped engine facade and the help and version CLI shell (`D-050`). Native CI, E2 hash comparison, gated Release Please PR automation, and six-platform packaging now exist (`D-051`, `D-052`). Canonical source/output paths (`D-054`) and stable catalog identifier values (`D-055`), and percent-encoded unit identity builders (`D-056`) are implemented. Bounded finding records and complete report-local ordering are implemented (`D-057`). Runtime SHA-256 hashing and canonical digest values are implemented with a reviewed dependency graph and dependency-aware CI and release checks (`D-058`, `D-059`). Run limits with atomic budgets, a reconciled limit set, and deterministic collections complete the foundation contracts (`D-060`, `D-061`), and the product fixture layout is decided (`D-062`). M1 was accepted by the owner on 2026-10-19 under `D-063`; live CI and branch protection status are recorded in IMPLEMENTATION.md. M2 is complete (`D-076`): the read-only discovery walker and Git-compatible `.gitignore` matching, with the first product fixtures (`D-064`, `D-065`), followed by the read-once snapshot store and the JSON and JSONC parser (`D-067`, `D-068`), the package model with npm, Bun, pnpm, and Yarn lockfiles, and full YAML 1.2 syntax (`D-069` to `D-072`), workspace and Next.js detection (`D-073`, `D-074`), `frameshift inspect` with the versioned `frameshift.inspection` document (`D-075`), and adversarial inspect fixtures (`D-076`). Version 0.1.0 is published. M3 has started with the CSS tokenizer and generic structural parser (`D-076` to `D-078`), followed by the start of the SCSS and indented Sass parser (`D-079`), its structured selectors (`D-081`), module rules (`D-082`), media queries and supports conditions (`D-083`), and extension targets and `@at-root` queries (`D-084`). Milestones are defined by capabilities and technical dependencies, not by calendar dates (`D-014`). The three-stage strategy is an accepted decision (`D-016`). The contents of each stage are a plan and will be refined as work progresses. **Stages versus version numbers.** "V1", "V2", and "V3" name product stages. They are not semantic version numbers. The first stable release, 1.0.0, is planned for when V1's acceptance criteria are met. After that, major version numbers follow breaking changes to the CLI and document contracts, not stage boundaries (see [RELEASES.md](RELEASES.md)). @@ -50,7 +50,7 @@ Each milestone ends with a capability that is demonstrable and tested. | **M3** Parsing (complete) | Production parsers | JS, TS, JSX, and TSX parser evolved from E1. CSS tokenizer from the pinned CSS Syntax Level 3 Editor's Draft, implemented with conformance, differential, and fuzzing evidence (`D-077`). Generic CSS structural parser with nesting and bounded recovery, implemented (`D-078`). The SCSS and indented Sass parser now covers statements and SassScript with exact spans and explicit unchecked-grammar evidence (`D-079`), including legacy indented mixin shorthand (`D-080`) and structured selectors with Dart Sass's evaluation-time grammars (`D-081`). Module rules validate URL and namespace syntax and preserve explicit clauses (`D-082`), without module loading. Import items now preserve literal URLs and distinguish CSS and Sass with URL and context validation (`D-079`). `@media` and `@supports` conditions and import modifiers are structured with the reference's parse-time grammars, and value-dependent media queries carry evidence (`D-083`). `@extend` targets are structured with the evaluation selector grammar and `@at-root` queries with their parse-time grammar, with evidence where evaluation decides (`D-084`). Modern CSS-style `if()` branches and conditions are structured (`D-085`). Plain CSS functions retain unchecked headers and raw result declarations, and callable-name dispatch follows the pinned reference (`D-086`). Raw URLs and SassScript fallback use the reference's lexical dispatch, including escaped and vendor-prefixed names (`D-087`). Raw special functions follow decoded vendor dispatch and the shared declaration-value grammar while retaining unchecked evidence (`D-088`). Numeric units follow the reference's subtraction and identifier boundaries (`D-089`). Sass syntax conformance is established against the pinned normal parser on the full external corpus and generated cases (`D-090`, IMPLEMENTATION.md section 35). Evaluation-dependent selectors, conditions, and raw CSS retain explicit evidence for later analysis. Optional JavaScript diagnostic recovery includes conservative static module-item and default-export expression boundaries (`D-088`, `D-089`), preserves the first error, resumes at conservative outer-item boundaries with cumulative limits, and withholds erroneous trees (`D-085`, IMPLEMENTATION.md section 36). Position tables, comment lists, resource limits. Conformance suites. Continuous fuzzing. | M1 (and E1) | | **M4** Semantic analysis and project model (complete) | Facts about any App Router project | Snapshot-only local module resolution, inherited `tsconfig`/`jsconfig` paths, and a deterministic import graph are implemented (`D-091`, IMPLEMENTATION.md section 37). Package `imports`, self-references, and directory entry points with per-edge resolution modes follow (`D-092`, section 38). Scopes, bindings, references, imports, and exports of each file follow (`D-092`, section 39), as do the App Router route tree, convention exports, and render contexts (section 40), and the static evaluator with experiment E3 (section 41). A separate Node10 resolution profile for `"moduleResolution": "node"` is implemented (`D-093`, section 42); ordered module filename suffixes are implemented in both profiles (`D-094`, section 43); the assembled source model with inventory, per-module binding and render evidence, direct environment uses, bounded route closures, and a golden snapshot is implemented (`D-095`, section 44); the component graph resolves render sites through bindings, module edges, and recognized wrappers, and environment classification records where modules run and which variables and runtime APIs they use (`D-096`, sections 45 and 46); `tsconfig`/`jsconfig` inheritance from linked workspace packages is implemented (`D-098`, section 48). Module and component graphs. Route tree. Render contexts. Environment classification. Static evaluator (with experiment E3 measuring evaluation coverage). Next.js configuration, convention-export, and direct-import API facts are implemented (`D-097`, section 47), preserving unknown values and bundler evidence. Golden snapshots of the project model. M4 closed under `D-099` (IMPLEMENTATION.md section 49): custom bundler rules stay configuration evidence because V1 assesses them as `unsupported`, and indirect API value flow and component values from higher-order components and factories stay explicit unknown evidence. | M2, M3 | | **M5** Assessment and analysis report (implemented) | `frameshift analyze`: **first user value** | Feature catalog for the V1 matrix, implemented as embedded data with the detection of feature occurrences and their fixed statuses (`D-099`, IMPLEMENTATION.md section 50). Read-only assessment predicates, counting ownership and accounting, transitive route readiness with witness paths, a versioned knowledge base, corpus-validated `weights-v1`, exact integer metrics, `frameshift.analysis` v1 with bounded stored parsing, text/JSON/Markdown projections, `analyze`, `report`, `explain`, assessment `rules`, and fact-based verdicts are implemented under `D-100` (IMPLEMENTATION.md section 51). Unknown preconditions remain visible; no transformation coverage is claimed. | M4 | -| **M6** Planning (migrate v0 subset implemented, `D-101`) | `frameshift plan` | Configuration file `frameshift.jsonc` with its JSON Schema. Output path mapping for the conventional Astro layout, with collision detection. Rule registry with exclusivity checks. Operation generation. Plan document, hashing, and binding. Manual tasks. `migrate --dry-run` file listing. Migrate v0 (`D-101`) implements the output path mapping with collision detection, operations, manual tasks, and the dry-run listing as a pure function of the analysis, without the rule registry (IMPLEMENTATION.md section 52). The plan document, its hash and binding, and `frameshift plan` are implemented (`D-102`, section 53), and so is the configuration file (`D-103`, section 54). | M5 | +| **M6** Planning (implemented) | `frameshift plan` | Configuration file `frameshift.jsonc` with its JSON Schema. Output path mapping for the conventional Astro layout, with collision detection. Rule registry with exclusivity checks. Operation generation. Plan document, hashing, and binding. Manual tasks. `migrate --dry-run` file listing. Migrate v0 (`D-101`) implements the output path mapping with collision detection, operations, manual tasks, and the dry-run listing as a pure function of the analysis, first (IMPLEMENTATION.md section 52). The plan document, its hash and binding, and `frameshift plan` are implemented (`D-102`, section 53), as are the configuration file (`D-103`, section 54) and the rule registry with its exclusivity check (`D-104`, section 55). | M5 | | **M7** Execution and static sites | `frameshift migrate` for sites without client components | Executor: staging, atomic commit, overwrite policies, manifest, staging cleanup. Edit engine and Astro printer from E2. Structural verification. Rules: static routes, segments, groups, private folders, layout chain, static metadata and metadata files, Astro and static Preact components, `next/link`, global CSS, CSS Modules, Sass, public and imported assets, configuration generation, preservation of unmigrated files, `.frameshift/SUMMARY.md`. Migrate v0 (`D-101`) implements staging with an atomic rename and cleanup of its own staging directory, the E2 edit engine and Astro printer as engine rules for static routes, segments, groups, private folders, the layout chain, static `title` and `description` metadata, Astro and static Preact components, `next/link`, global CSS, public assets, configuration generation, preservation, and `.frameshift/SUMMARY.md`; overwrite policies, the manifest, structural verification, metadata files, CSS Modules, Sass, and imported assets remain open. | M6 (and E2) | | **M8** Dynamic routes, assets, styling | Common content sites | Dynamic routes with static parameters (including composition), catch-all routes, root not-found, special files, static GET route handlers, generated metadata routes, `next/image` (both strategies), `next/font`, `next/script`, Tailwind CSS v3 and v4, PostCSS, environment variable rewriting (`Q-003` resolved), metadata helper for non-literal metadata. | M7 | | **M9** Client components and islands | Interactive sites | Island selection, `client:load`, `client:only` for `ssr: false`, the compat strategy with package aliases and overrides for every supported package manager, boundary prop analysis and named slots, navigation helper, context scope classification, hook coverage, knowledge base entries for curated React libraries backed by fixtures. | M7 | @@ -83,6 +83,8 @@ M1's canonical source/output paths (`D-053`, `D-054`), stable catalog identifier **M2 completion (`D-076`).** M2 has no separate acceptance procedure: it is complete when its last increment, the adversarial inspect fixtures, reaches `main` through its pull request with a green `CI required` check. M3 follows, starting with the CSS tokenizer (`D-077`). +**M6 completion (`D-102`, `D-103`, `D-104`).** The plan document, the configuration file, and the rule registry exist in sections 53 to 55 of IMPLEMENTATION.md; completion takes effect with the green integration of the last of their pull requests. M7 follows: the output manifest, overwrite policies, structural verification, and the converter widening that closes the gap between the analysis and migrate v0. + **M5 completion (`D-100`).** The implementation and its fictional contracts exist in section 51 of IMPLEMENTATION.md. The approved pinned D-043 corpus is reused read-only for Q-006; fixed weights are checked for plausibility under the owner's Q-002 authorization. Completion takes effect with green integration of this pull request. M6 planning follows; no migration operations or verification are implied. **M4 completion (`D-099`).** The owner chose on 2026-10-25 to close M4 with the facts that exist. The capability review is in IMPLEMENTATION.md section 49; completion takes effect with the green integration of its pull request, which also begins M5 with the feature catalog and detection. diff --git a/fixtures/migrate/fail-closed/expected/plan.json b/fixtures/migrate/fail-closed/expected/plan.json index e844e5b..6d2386f 100644 --- a/fixtures/migrate/fail-closed/expected/plan.json +++ b/fixtures/migrate/fail-closed/expected/plan.json @@ -24,6 +24,7 @@ "kind": "config", "output": ".frameshift/SUMMARY.md", "rule": "report.summary", + "ruleVersion": 1, "source": null, "review": false, "size": 3026, @@ -33,6 +34,7 @@ "kind": "preserve", "output": ".frameshift/unmigrated/src/app/captured/page.tsx", "rule": "preserve.unmigrated", + "ruleVersion": 1, "source": "src/app/captured/page.tsx", "review": false, "size": 144, @@ -42,6 +44,7 @@ "kind": "preserve", "output": ".frameshift/unmigrated/src/app/counter/page.tsx", "rule": "preserve.unmigrated", + "ruleVersion": 1, "source": "src/app/counter/page.tsx", "review": false, "size": 158, @@ -51,6 +54,7 @@ "kind": "preserve", "output": ".frameshift/unmigrated/src/app/hoisted/page.tsx", "rule": "preserve.unmigrated", + "ruleVersion": 1, "source": "src/app/hoisted/page.tsx", "review": false, "size": 109, @@ -60,6 +64,7 @@ "kind": "preserve", "output": ".frameshift/unmigrated/src/app/lazy/page.tsx", "rule": "preserve.unmigrated", + "ruleVersion": 1, "source": "src/app/lazy/page.tsx", "review": false, "size": 137, @@ -69,6 +74,7 @@ "kind": "preserve", "output": ".frameshift/unmigrated/src/app/plain/page.tsx", "rule": "preserve.unmigrated", + "ruleVersion": 1, "source": "src/app/plain/page.tsx", "review": false, "size": 103, @@ -78,6 +84,7 @@ "kind": "preserve", "output": ".frameshift/unmigrated/src/app/shadow/page.tsx", "rule": "preserve.unmigrated", + "ruleVersion": 1, "source": "src/app/shadow/page.tsx", "review": false, "size": 133, @@ -87,6 +94,7 @@ "kind": "preserve", "output": ".frameshift/unmigrated/src/app/tagged/page.tsx", "rule": "preserve.unmigrated", + "ruleVersion": 1, "source": "src/app/tagged/page.tsx", "review": false, "size": 112, @@ -96,6 +104,7 @@ "kind": "preserve", "output": ".frameshift/unmigrated/src/components/Counter.tsx", "rule": "preserve.unmigrated", + "ruleVersion": 1, "source": "src/components/Counter.tsx", "review": false, "size": 224, @@ -105,6 +114,7 @@ "kind": "preserve", "output": ".frameshift/unmigrated/src/components/Tag.tsx", "rule": "preserve.unmigrated", + "ruleVersion": 1, "source": "src/components/Tag.tsx", "review": false, "size": 117, @@ -114,6 +124,7 @@ "kind": "preserve", "output": ".frameshift/unmigrated/src/lib/here.ts", "rule": "preserve.unmigrated", + "ruleVersion": 1, "source": "src/lib/here.ts", "review": false, "size": 66, @@ -123,6 +134,7 @@ "kind": "preserve", "output": ".frameshift/unmigrated/src/lib/label.ts", "rule": "preserve.unmigrated", + "ruleVersion": 1, "source": "src/lib/label.ts", "review": false, "size": 133, @@ -132,6 +144,7 @@ "kind": "preserve", "output": ".frameshift/unmigrated/src/types/augment.d.ts", "rule": "preserve.unmigrated", + "ruleVersion": 1, "source": "src/types/augment.d.ts", "review": false, "size": 70, @@ -141,6 +154,7 @@ "kind": "preserve", "output": ".frameshift/unmigrated/src/types/referenced.d.ts", "rule": "preserve.unmigrated", + "ruleVersion": 1, "source": "src/types/referenced.d.ts", "review": false, "size": 73, @@ -150,6 +164,7 @@ "kind": "config", "output": ".gitignore", "rule": "config.gitignore", + "ruleVersion": 1, "source": null, "review": false, "size": 28, @@ -159,6 +174,7 @@ "kind": "config", "output": "astro.config.mjs", "rule": "config.astro", + "ruleVersion": 1, "source": null, "review": false, "size": 162, @@ -168,6 +184,7 @@ "kind": "config", "output": "package.json", "rule": "config.package-manifest", + "ruleVersion": 1, "source": null, "review": false, "size": 434, @@ -177,6 +194,7 @@ "kind": "create", "output": "src/layouts/RootLayout.astro", "rule": "layout.chain", + "ruleVersion": 1, "source": "src/app/layout.tsx", "review": false, "size": 180, @@ -186,6 +204,7 @@ "kind": "transform", "output": "src/lib/greeting.ts", "rule": "module.relocate", + "ruleVersion": 1, "source": "src/lib/greeting.ts", "review": false, "size": 33, @@ -195,6 +214,7 @@ "kind": "create", "output": "src/pages/index.astro", "rule": "route.static-page", + "ruleVersion": 1, "source": "src/app/page.tsx", "review": true, "size": 163, @@ -204,6 +224,7 @@ "kind": "config", "output": "tsconfig.json", "rule": "config.tsconfig", + "ruleVersion": 1, "source": null, "review": false, "size": 193, diff --git a/fixtures/migrate/static-site/expected/plan.json b/fixtures/migrate/static-site/expected/plan.json index 5f34c9c..c3dae5a 100644 --- a/fixtures/migrate/static-site/expected/plan.json +++ b/fixtures/migrate/static-site/expected/plan.json @@ -24,6 +24,7 @@ "kind": "config", "output": ".frameshift/SUMMARY.md", "rule": "report.summary", + "ruleVersion": 1, "source": null, "review": false, "size": 2322, @@ -33,6 +34,7 @@ "kind": "preserve", "output": ".frameshift/unmigrated/app/blog/[slug]/page.tsx", "rule": "preserve.unmigrated", + "ruleVersion": 1, "source": "app/blog/[slug]/page.tsx", "review": false, "size": 153, @@ -42,6 +44,7 @@ "kind": "preserve", "output": ".frameshift/unmigrated/app/contact/page.tsx", "rule": "preserve.unmigrated", + "ruleVersion": 1, "source": "app/contact/page.tsx", "review": false, "size": 219, @@ -51,6 +54,7 @@ "kind": "preserve", "output": ".frameshift/unmigrated/tests/site.test.ts", "rule": "preserve.unmigrated", + "ruleVersion": 1, "source": "tests/site.test.ts", "review": false, "size": 85, @@ -60,6 +64,7 @@ "kind": "config", "output": ".gitignore", "rule": "config.gitignore", + "ruleVersion": 1, "source": ".gitignore", "review": false, "size": 35, @@ -69,6 +74,7 @@ "kind": "config", "output": "astro.config.mjs", "rule": "config.astro", + "ruleVersion": 1, "source": null, "review": false, "size": 162, @@ -78,6 +84,7 @@ "kind": "config", "output": "package.json", "rule": "config.package-manifest", + "ruleVersion": 1, "source": null, "review": false, "size": 419, @@ -87,6 +94,7 @@ "kind": "copy", "output": "public/paper.svg", "rule": "asset.verbatim", + "ruleVersion": 1, "source": "public/paper.svg", "review": false, "size": 111, @@ -96,6 +104,7 @@ "kind": "copy", "output": "public/robots.txt", "rule": "asset.verbatim", + "ruleVersion": 1, "source": "public/robots.txt", "review": false, "size": 23, @@ -105,6 +114,7 @@ "kind": "transform", "output": "src/components/Badge.tsx", "rule": "component.preact-static", + "ruleVersion": 1, "source": "components/Badge.tsx", "review": false, "size": 142, @@ -114,6 +124,7 @@ "kind": "create", "output": "src/components/SiteHeader.astro", "rule": "component.astro", + "ruleVersion": 1, "source": "components/SiteHeader.tsx", "review": true, "size": 104, @@ -123,6 +134,7 @@ "kind": "create", "output": "src/components/Stamp.astro", "rule": "component.astro", + "ruleVersion": 1, "source": "components/Stamp.tsx", "review": true, "size": 114, @@ -132,6 +144,7 @@ "kind": "create", "output": "src/layouts/DocsLayout.astro", "rule": "layout.chain", + "ruleVersion": 1, "source": "app/docs/layout.tsx", "review": false, "size": 328, @@ -141,6 +154,7 @@ "kind": "create", "output": "src/layouts/RootLayout.astro", "rule": "layout.chain", + "ruleVersion": 1, "source": "app/layout.tsx", "review": false, "size": 520, @@ -150,6 +164,7 @@ "kind": "transform", "output": "src/lib/site.ts", "rule": "module.relocate", + "ruleVersion": 1, "source": "lib/site.ts", "review": false, "size": 92, @@ -159,6 +174,7 @@ "kind": "transform", "output": "src/lib/types.ts", "rule": "module.relocate", + "ruleVersion": 1, "source": "lib/types.ts", "review": false, "size": 31, @@ -168,6 +184,7 @@ "kind": "create", "output": "src/pages/about.astro", "rule": "route.static-page", + "ruleVersion": 1, "source": "app/about/page.tsx", "review": false, "size": 302, @@ -177,6 +194,7 @@ "kind": "create", "output": "src/pages/changelog.astro", "rule": "route.static-page", + "ruleVersion": 1, "source": "app/changelog/page.tsx", "review": true, "size": 514, @@ -186,6 +204,7 @@ "kind": "create", "output": "src/pages/docs.astro", "rule": "route.static-page", + "ruleVersion": 1, "source": "app/docs/page.tsx", "review": false, "size": 203, @@ -195,6 +214,7 @@ "kind": "create", "output": "src/pages/docs/intro.astro", "rule": "route.static-page", + "ruleVersion": 1, "source": "app/docs/intro/page.tsx", "review": false, "size": 205, @@ -204,6 +224,7 @@ "kind": "create", "output": "src/pages/index.astro", "rule": "route.static-page", + "ruleVersion": 1, "source": "app/page.tsx", "review": true, "size": 480, @@ -213,6 +234,7 @@ "kind": "create", "output": "src/pages/pricing.astro", "rule": "route.static-page", + "ruleVersion": 1, "source": "app/(marketing)/pricing/page.tsx", "review": false, "size": 221, @@ -222,6 +244,7 @@ "kind": "copy", "output": "src/styles/globals.css", "rule": "style.relocate", + "ruleVersion": 1, "source": "app/globals.css", "review": false, "size": 93, @@ -231,6 +254,7 @@ "kind": "transform", "output": "src/types/api.d.ts", "rule": "module.relocate", + "ruleVersion": 1, "source": "types/api.d.ts", "review": false, "size": 89, @@ -240,6 +264,7 @@ "kind": "transform", "output": "src/types/assets.d.ts", "rule": "module.relocate", + "ruleVersion": 1, "source": "types/assets.d.ts", "review": false, "size": 70, @@ -249,6 +274,7 @@ "kind": "config", "output": "tsconfig.json", "rule": "config.tsconfig", + "ruleVersion": 1, "source": null, "review": false, "size": 193, diff --git a/schemas/frameshift.plan.schema.json b/schemas/frameshift.plan.schema.json index fd6809b..bf6184a 100644 --- a/schemas/frameshift.plan.schema.json +++ b/schemas/frameshift.plan.schema.json @@ -44,11 +44,12 @@ "items": { "type": "object", "additionalProperties": false, - "required": ["kind", "output", "rule", "source", "review", "size", "sha256"], + "required": ["kind", "output", "rule", "ruleVersion", "source", "review", "size", "sha256"], "properties": { "kind": { "enum": ["create", "transform", "copy", "config", "preserve"] }, "output": { "type": "string", "minLength": 1 }, "rule": { "type": "string", "minLength": 1 }, + "ruleVersion": { "type": "integer", "minimum": 1 }, "source": { "type": ["string", "null"] }, "review": { "type": "boolean" }, "size": { "type": "integer", "minimum": 0 },