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
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: ci

on:
push:
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Set up MoonBit
run: |
curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash
echo "$HOME/.moon/bin" >> $GITHUB_PATH

- name: Update MoonBit dependencies
run: |
moon version --all
moon update
- name: ci script
run: bash scripts/ci.sh
67 changes: 66 additions & 1 deletion FWD-META-ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ rules:
```

対応する L0 実装は **純粋関数**。
v1 の `noBreakingChanges` は **state / transition の削除検出のみ**を対象とし、from/to 変更や rename 判定は v1.2+ に繰り越す。
`noBreakingChangesOrMigrationDefined` は v1 では **breaking が検出された場合の migrate 指定**のみを要求する。
- transition modified: 該当 transition に `effects: [migrate]`
- transition removed / state removed: グローバル `effects: [migrate]`

### 2.2 Escape Hatch(実装関数)

Expand Down Expand Up @@ -394,11 +398,72 @@ v1 における自己記述の成立は、次の条件で定義する:

---

## CLI Output Format: Validation JSON (v1.1)

`fwdc validate` は `--json` または `--format json` 指定時に、標準出力へ機械可読な JSON を出力する。
このとき human-readable なログは出力しない。

### Exit Codes

- `0`: ok
- `1`: validation failed (reasons returned)

### Success JSON

```json
{ "ok": true }
```

### Failure JSON

```json
{
"ok": false,
"errorCount": 3,
"reasons": [
{
"code": "BreakingChange",
"message": "transition modified",
"context": {
"kind": "transitionModified",
"subject": "submit",
"scope": "transition",
"field": "from",
"baseline": "Draft",
"candidate": "Reviewing"
}
},
{
"code": "MigrationRequired",
"message": "breaking change requires migration",
"context": {
"count": "1",
"kinds": "transitionModified",
"subjects": "submit",
"scopes": "transition"
}
}
]
}
```

### Notes

- `errorCount == reasons.length`(集約後)
- `context` は v1.1 では `Map[String,String]` 相当(JSONでは string→string object)
- JSON のキー順は実装で安定化させる(テスト・CIの再現性のため)

### JSON Mode Output Contract (v1.1)
When `--json` or `--format json` is specified:

- **stdout**: JSON only (no extra human-readable text)
- **stderr**: empty on expected validation failures (reserved for unexpected runtime errors)
- All failure modes (including file read/parse errors) are reported as JSON with `"ok": false`.

## 次の実装ステップ(推奨)

1. **L0 Core の最小実装(MoonBit)**
2. **L1 スキーマを YAML / MoonBit 定義で記述**
3. **最小コンパイラ(Parse → Validate → Emit)**
4. 「FWD が FWD を処理できる」ことを実証

→ ここまでで **自己記述が実際に動く**。
12 changes: 12 additions & 0 deletions README.mbt.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ moon run cli -- presets
- If `output.json` is omitted, JSON IR is printed to stdout.
- Example input: `examples/schema_v1.yaml`.

### Validation JSON Output (v1.1)

```
moon run cli -- validate <schema.yaml> --json
moon run cli -- validate <schema.yaml> --format json
moon run cli -- validate <schema.yaml> --baseline <baseline.yaml> --json
```

- `stdout` is JSON only (no extra human-readable text).
- Exit code is `0` on success, `1` on failure.
- Expected validation failures are reported as JSON with `"ok": false`.

## Builtin Rule Presets

These preset rule names are reserved and provided by the compiler resolve stage:
Expand Down
193 changes: 174 additions & 19 deletions cli/app.mbt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
///|
fn usage() -> String {
"usage: fwdc <schema.yaml> [output.json]\n fwdc presets\n fwdc validate <schema.yaml>"
"usage: fwdc <schema.yaml> [output.json]\n fwdc presets\n fwdc validate <schema.yaml> [--baseline <baseline.yaml>] [--format json|--json]"
}

///|
Expand All @@ -12,8 +12,7 @@ fn print_presets() -> Unit {

///|
fn read_input(path : String) -> String? {
let input_result : Result[String, @fs.IOError] = try? @fs.read_file_to_string(path)
match input_result {
match read_input_result(path) {
Ok(text) => Some(text)
Err(err) => {
println("read error: " + err.to_string())
Expand All @@ -22,6 +21,11 @@ fn read_input(path : String) -> String? {
}
}

///|
fn read_input_result(path : String) -> Result[String, @fs.IOError] {
try? @fs.read_file_to_string(path)
}

///|
fn write_output(path : String, output : String) -> Bool {
let write_result : Result[Unit, @fs.IOError] =
Expand All @@ -35,6 +39,32 @@ fn write_output(path : String, output : String) -> Bool {
}
}

///|
fn format_reason(reason : @core.Reason) -> String {
@compiler.reason_to_human_line(reason)
}

///|
fn has_breaking(reasons : Array[@core.Reason]) -> Bool {
for reason in reasons {
if reason.code() == "BreakingChange" || reason.code() == "MigrationRequired" {
return true
}
}
false
}

///|
fn reason_from_compile_error(err : @compiler.CompileError) -> @core.Reason {
match err {
@compiler.CompileError::ParseFailure(msg) =>
@core.Reason::new("ParseFailure", msg)
@compiler.CompileError::ResolveFailure(msg) =>
@core.Reason::new("ResolveFailure", msg)
@compiler.CompileError::ValidationFailure(reason) => reason
}
}

///|
pub fn run(args : Array[String]) -> Int {
if args.length() == 0 {
Expand All @@ -51,37 +81,162 @@ pub fn run(args : Array[String]) -> Int {
return 1
}
let input_path = args[1]
let input_opt = read_input(input_path)
match input_opt {
Some(input) => {
match @compiler.parse_yaml(input) {
Ok(schema) => {
match @compiler.resolve_schema(schema) {
Ok(resolved) => {
match @compiler.validate_schema(resolved) {
let mut baseline_path : String? = None
let mut json_output = false
for i = 2; i < args.length(); {
let arg = args[i]
if arg == "--baseline" {
if i + 1 >= args.length() {
println(usage())
return 1
}
baseline_path = Some(args[i + 1])
continue i + 2
} else if arg == "--format" {
if i + 1 >= args.length() {
println(usage())
return 1
}
if args[i + 1] == "json" {
json_output = true
continue i + 2
}
println(usage())
return 1
} else if arg == "--json" {
json_output = true
continue i + 1
} else {
println(usage())
return 1
}
} else {}
let input_result = read_input_result(input_path)
match input_result {
Ok(input) => {
match baseline_path {
Some(path) => {
let baseline_result = read_input_result(path)
match baseline_result {
Ok(baseline) => {
match @compiler.validate_with_baseline(input, baseline) {
Ok(_) => {
println("ok")
if json_output {
println(@compiler.validation_json_string(true, []))
} else {
println("ok")
}
0
}
Err(reason) => {
println("validation error: " + reason.code() + ": " + reason.message())
Err(reasons) => {
if json_output {
println(@compiler.validation_json_string(false, reasons))
} else {
let header = if has_breaking(reasons) {
"error: breaking changes detected ("
} else {
"error: validation failed ("
}
println(header + reasons.length().to_string() + ")")
for reason in reasons {
println("- " + format_reason(reason))
}
}
1
}
}
}
Err(err) => {
println(@compiler.describe_error(err))
if json_output {
let reasons : Array[@core.Reason] = []
reasons.push(@core.Reason::new(
"ReadFailure",
"read error: " + err.to_string(),
))
println(@compiler.validation_json_string(false, reasons))
} else {
println("read error: " + err.to_string())
}
1
}
}
}
Err(err) => {
println(@compiler.describe_error(err))
1
None => {
if json_output {
match @compiler.parse_yaml(input) {
Ok(schema) => {
match @compiler.resolve_schema(schema) {
Ok(resolved) => {
match @compiler.validate_schema(resolved) {
Ok(_) => {
println(@compiler.validation_json_string(true, []))
0
}
Err(reason) => {
println(@compiler.validation_json_string(false, [reason]))
1
}
}
}
Err(err) => {
let reason = reason_from_compile_error(err)
println(@compiler.validation_json_string(false, [reason]))
1
}
}
}
Err(err) => {
let reason = reason_from_compile_error(err)
println(@compiler.validation_json_string(false, [reason]))
1
}
}
} else {
match @compiler.parse_yaml(input) {
Ok(schema) => {
match @compiler.resolve_schema(schema) {
Ok(resolved) => {
match @compiler.validate_schema(resolved) {
Ok(_) => {
println("ok")
0
}
Err(reason) => {
println(
"validation error: " + reason.code() + ": " + reason.message(),
)
1
}
}
}
Err(err) => {
println(@compiler.describe_error(err))
1
}
}
}
Err(err) => {
println(@compiler.describe_error(err))
1
}
}
}
}
}
}
None => 1
Err(err) => {
if json_output {
let reasons : Array[@core.Reason] = []
reasons.push(@core.Reason::new(
"ReadFailure",
"read error: " + err.to_string(),
))
println(@compiler.validation_json_string(false, reasons))
} else {
println("read error: " + err.to_string())
}
1
}
}
} else {
let input_path = args[0]
Expand Down
Loading