Skip to content

Commit 5673b67

Browse files
authored
done: fwd meta architecture
Feat/v1.1
2 parents 851dca9 + 1f19b6f commit 5673b67

23 files changed

+1193
-20
lines changed

.github/workflows/ci.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: ci
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
test:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@v5
12+
- name: Set up MoonBit
13+
run: |
14+
curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash
15+
echo "$HOME/.moon/bin" >> $GITHUB_PATH
16+
17+
- name: Update MoonBit dependencies
18+
run: |
19+
moon version --all
20+
moon update
21+
- name: ci script
22+
run: bash scripts/ci.sh

FWD-META-ARCHITECTURE.md

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ rules:
6363
```
6464
6565
対応する L0 実装は **純粋関数**。
66+
v1 の `noBreakingChanges` は **state / transition の削除検出のみ**を対象とし、from/to 変更や rename 判定は v1.2+ に繰り越す。
67+
`noBreakingChangesOrMigrationDefined` は v1 では **breaking が検出された場合の migrate 指定**のみを要求する。
68+
- transition modified: 該当 transition に `effects: [migrate]`
69+
- transition removed / state removed: グローバル `effects: [migrate]`
6670

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

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

395399
---
396400

401+
## CLI Output Format: Validation JSON (v1.1)
402+
403+
`fwdc validate` は `--json` または `--format json` 指定時に、標準出力へ機械可読な JSON を出力する。
404+
このとき human-readable なログは出力しない。
405+
406+
### Exit Codes
407+
408+
- `0`: ok
409+
- `1`: validation failed (reasons returned)
410+
411+
### Success JSON
412+
413+
```json
414+
{ "ok": true }
415+
```
416+
417+
### Failure JSON
418+
419+
```json
420+
{
421+
"ok": false,
422+
"errorCount": 3,
423+
"reasons": [
424+
{
425+
"code": "BreakingChange",
426+
"message": "transition modified",
427+
"context": {
428+
"kind": "transitionModified",
429+
"subject": "submit",
430+
"scope": "transition",
431+
"field": "from",
432+
"baseline": "Draft",
433+
"candidate": "Reviewing"
434+
}
435+
},
436+
{
437+
"code": "MigrationRequired",
438+
"message": "breaking change requires migration",
439+
"context": {
440+
"count": "1",
441+
"kinds": "transitionModified",
442+
"subjects": "submit",
443+
"scopes": "transition"
444+
}
445+
}
446+
]
447+
}
448+
```
449+
450+
### Notes
451+
452+
- `errorCount == reasons.length`(集約後)
453+
- `context` は v1.1 では `Map[String,String]` 相当(JSONでは string→string object)
454+
- JSON のキー順は実装で安定化させる(テスト・CIの再現性のため)
455+
456+
### JSON Mode Output Contract (v1.1)
457+
When `--json` or `--format json` is specified:
458+
459+
- **stdout**: JSON only (no extra human-readable text)
460+
- **stderr**: empty on expected validation failures (reserved for unexpected runtime errors)
461+
- All failure modes (including file read/parse errors) are reported as JSON with `"ok": false`.
462+
397463
## 次の実装ステップ(推奨)
398464

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

404-
→ ここまでで **自己記述が実際に動く**

README.mbt.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@ moon run cli -- presets
1111
- If `output.json` is omitted, JSON IR is printed to stdout.
1212
- Example input: `examples/schema_v1.yaml`.
1313

14+
### Validation JSON Output (v1.1)
15+
16+
```
17+
moon run cli -- validate <schema.yaml> --json
18+
moon run cli -- validate <schema.yaml> --format json
19+
moon run cli -- validate <schema.yaml> --baseline <baseline.yaml> --json
20+
```
21+
22+
- `stdout` is JSON only (no extra human-readable text).
23+
- Exit code is `0` on success, `1` on failure.
24+
- Expected validation failures are reported as JSON with `"ok": false`.
25+
1426
## Builtin Rule Presets
1527

1628
These preset rule names are reserved and provided by the compiler resolve stage:

cli/app.mbt

Lines changed: 174 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
///|
22
fn usage() -> String {
3-
"usage: fwdc <schema.yaml> [output.json]\n fwdc presets\n fwdc validate <schema.yaml>"
3+
"usage: fwdc <schema.yaml> [output.json]\n fwdc presets\n fwdc validate <schema.yaml> [--baseline <baseline.yaml>] [--format json|--json]"
44
}
55

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

1313
///|
1414
fn read_input(path : String) -> String? {
15-
let input_result : Result[String, @fs.IOError] = try? @fs.read_file_to_string(path)
16-
match input_result {
15+
match read_input_result(path) {
1716
Ok(text) => Some(text)
1817
Err(err) => {
1918
println("read error: " + err.to_string())
@@ -22,6 +21,11 @@ fn read_input(path : String) -> String? {
2221
}
2322
}
2423

24+
///|
25+
fn read_input_result(path : String) -> Result[String, @fs.IOError] {
26+
try? @fs.read_file_to_string(path)
27+
}
28+
2529
///|
2630
fn write_output(path : String, output : String) -> Bool {
2731
let write_result : Result[Unit, @fs.IOError] =
@@ -35,6 +39,32 @@ fn write_output(path : String, output : String) -> Bool {
3539
}
3640
}
3741

42+
///|
43+
fn format_reason(reason : @core.Reason) -> String {
44+
@compiler.reason_to_human_line(reason)
45+
}
46+
47+
///|
48+
fn has_breaking(reasons : Array[@core.Reason]) -> Bool {
49+
for reason in reasons {
50+
if reason.code() == "BreakingChange" || reason.code() == "MigrationRequired" {
51+
return true
52+
}
53+
}
54+
false
55+
}
56+
57+
///|
58+
fn reason_from_compile_error(err : @compiler.CompileError) -> @core.Reason {
59+
match err {
60+
@compiler.CompileError::ParseFailure(msg) =>
61+
@core.Reason::new("ParseFailure", msg)
62+
@compiler.CompileError::ResolveFailure(msg) =>
63+
@core.Reason::new("ResolveFailure", msg)
64+
@compiler.CompileError::ValidationFailure(reason) => reason
65+
}
66+
}
67+
3868
///|
3969
pub fn run(args : Array[String]) -> Int {
4070
if args.length() == 0 {
@@ -51,37 +81,162 @@ pub fn run(args : Array[String]) -> Int {
5181
return 1
5282
}
5383
let input_path = args[1]
54-
let input_opt = read_input(input_path)
55-
match input_opt {
56-
Some(input) => {
57-
match @compiler.parse_yaml(input) {
58-
Ok(schema) => {
59-
match @compiler.resolve_schema(schema) {
60-
Ok(resolved) => {
61-
match @compiler.validate_schema(resolved) {
84+
let mut baseline_path : String? = None
85+
let mut json_output = false
86+
for i = 2; i < args.length(); {
87+
let arg = args[i]
88+
if arg == "--baseline" {
89+
if i + 1 >= args.length() {
90+
println(usage())
91+
return 1
92+
}
93+
baseline_path = Some(args[i + 1])
94+
continue i + 2
95+
} else if arg == "--format" {
96+
if i + 1 >= args.length() {
97+
println(usage())
98+
return 1
99+
}
100+
if args[i + 1] == "json" {
101+
json_output = true
102+
continue i + 2
103+
}
104+
println(usage())
105+
return 1
106+
} else if arg == "--json" {
107+
json_output = true
108+
continue i + 1
109+
} else {
110+
println(usage())
111+
return 1
112+
}
113+
} else {}
114+
let input_result = read_input_result(input_path)
115+
match input_result {
116+
Ok(input) => {
117+
match baseline_path {
118+
Some(path) => {
119+
let baseline_result = read_input_result(path)
120+
match baseline_result {
121+
Ok(baseline) => {
122+
match @compiler.validate_with_baseline(input, baseline) {
62123
Ok(_) => {
63-
println("ok")
124+
if json_output {
125+
println(@compiler.validation_json_string(true, []))
126+
} else {
127+
println("ok")
128+
}
64129
0
65130
}
66-
Err(reason) => {
67-
println("validation error: " + reason.code() + ": " + reason.message())
131+
Err(reasons) => {
132+
if json_output {
133+
println(@compiler.validation_json_string(false, reasons))
134+
} else {
135+
let header = if has_breaking(reasons) {
136+
"error: breaking changes detected ("
137+
} else {
138+
"error: validation failed ("
139+
}
140+
println(header + reasons.length().to_string() + ")")
141+
for reason in reasons {
142+
println("- " + format_reason(reason))
143+
}
144+
}
68145
1
69146
}
70147
}
71148
}
72149
Err(err) => {
73-
println(@compiler.describe_error(err))
150+
if json_output {
151+
let reasons : Array[@core.Reason] = []
152+
reasons.push(@core.Reason::new(
153+
"ReadFailure",
154+
"read error: " + err.to_string(),
155+
))
156+
println(@compiler.validation_json_string(false, reasons))
157+
} else {
158+
println("read error: " + err.to_string())
159+
}
74160
1
75161
}
76162
}
77163
}
78-
Err(err) => {
79-
println(@compiler.describe_error(err))
80-
1
164+
None => {
165+
if json_output {
166+
match @compiler.parse_yaml(input) {
167+
Ok(schema) => {
168+
match @compiler.resolve_schema(schema) {
169+
Ok(resolved) => {
170+
match @compiler.validate_schema(resolved) {
171+
Ok(_) => {
172+
println(@compiler.validation_json_string(true, []))
173+
0
174+
}
175+
Err(reason) => {
176+
println(@compiler.validation_json_string(false, [reason]))
177+
1
178+
}
179+
}
180+
}
181+
Err(err) => {
182+
let reason = reason_from_compile_error(err)
183+
println(@compiler.validation_json_string(false, [reason]))
184+
1
185+
}
186+
}
187+
}
188+
Err(err) => {
189+
let reason = reason_from_compile_error(err)
190+
println(@compiler.validation_json_string(false, [reason]))
191+
1
192+
}
193+
}
194+
} else {
195+
match @compiler.parse_yaml(input) {
196+
Ok(schema) => {
197+
match @compiler.resolve_schema(schema) {
198+
Ok(resolved) => {
199+
match @compiler.validate_schema(resolved) {
200+
Ok(_) => {
201+
println("ok")
202+
0
203+
}
204+
Err(reason) => {
205+
println(
206+
"validation error: " + reason.code() + ": " + reason.message(),
207+
)
208+
1
209+
}
210+
}
211+
}
212+
Err(err) => {
213+
println(@compiler.describe_error(err))
214+
1
215+
}
216+
}
217+
}
218+
Err(err) => {
219+
println(@compiler.describe_error(err))
220+
1
221+
}
222+
}
223+
}
81224
}
82225
}
83226
}
84-
None => 1
227+
Err(err) => {
228+
if json_output {
229+
let reasons : Array[@core.Reason] = []
230+
reasons.push(@core.Reason::new(
231+
"ReadFailure",
232+
"read error: " + err.to_string(),
233+
))
234+
println(@compiler.validation_json_string(false, reasons))
235+
} else {
236+
println("read error: " + err.to_string())
237+
}
238+
1
239+
}
85240
}
86241
} else {
87242
let input_path = args[0]

0 commit comments

Comments
 (0)