openuispec 0.1.29 → 0.1.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,7 +14,7 @@ import { fileURLToPath } from "node:url";
14
14
  import { createRequire } from "node:module";
15
15
  import type { ErrorObject } from "ajv";
16
16
  import YAML from "yaml";
17
- import { runSemanticLint, type Includes } from "./semantic-lint.js";
17
+ import { runSemanticLint, collectSemanticLint, type Includes } from "./semantic-lint.js";
18
18
 
19
19
  const require = createRequire(import.meta.url);
20
20
  const Ajv2020 = require("ajv/dist/2020") as typeof import("ajv").default;
@@ -311,6 +311,71 @@ function lintFlowFile(dataPath: string): number {
311
311
  return errors.length;
312
312
  }
313
313
 
314
+ // ── collect variants (structured errors for --json) ──────────────────
315
+
316
+ interface JsonError {
317
+ file: string;
318
+ path: string;
319
+ message: string;
320
+ }
321
+
322
+ function collectValidateFile(
323
+ ajv: AjvInstance,
324
+ dataPath: string,
325
+ schemaId: string,
326
+ label?: string,
327
+ ): JsonError[] {
328
+ const name = label ?? basename(dataPath);
329
+ const data = loadData(dataPath);
330
+ const validate = ajv.getSchema(schemaId);
331
+
332
+ if (!validate) {
333
+ return [{ file: name, path: "(root)", message: `schema ${schemaId} not found` }];
334
+ }
335
+
336
+ const valid = validate(data);
337
+ if (valid) return [];
338
+
339
+ const errors: ErrorObject[] = validate.errors ?? [];
340
+ return errors.map((e) => ({
341
+ file: name,
342
+ path: e.instancePath || "(root)",
343
+ message: e.message ?? "unknown error",
344
+ }));
345
+ }
346
+
347
+ function collectLintScreenFile(dataPath: string): JsonError[] {
348
+ const root = getSingleRootValue(loadData(dataPath));
349
+ const errors = lintScreenLikeDefinition(root, basename(dataPath));
350
+ return errors.map((e) => ({
351
+ file: basename(dataPath),
352
+ path: e.path,
353
+ message: e.message,
354
+ }));
355
+ }
356
+
357
+ function collectLintFlowFile(dataPath: string): JsonError[] {
358
+ const root = getSingleRootValue(loadData(dataPath));
359
+ if (!isRecord(root) || !isRecord(root.screens)) return [];
360
+
361
+ const errors: UsageLint[] = [];
362
+ for (const [screenId, screenEntry] of Object.entries(root.screens)) {
363
+ if (!isRecord(screenEntry) || !isRecord(screenEntry.screen_inline)) continue;
364
+ errors.push(
365
+ ...lintScreenLikeDefinition(
366
+ screenEntry.screen_inline,
367
+ `${basename(dataPath)}/screens/${screenId}/screen_inline`,
368
+ ),
369
+ );
370
+ }
371
+
372
+ return errors.map((e) => ({
373
+ file: basename(dataPath),
374
+ path: e.path,
375
+ message: e.message,
376
+ }));
377
+ }
378
+
314
379
  // ── build Ajv instance with all schemas ──────────────────────────────
315
380
 
316
381
  function buildAjv(): AjvInstance {
@@ -436,9 +501,15 @@ function resolveInclude(projectDir: string, includePath: string): string {
436
501
 
437
502
  // ── validation groups ────────────────────────────────────────────────
438
503
 
504
+ interface JsonGroupResult {
505
+ group: string;
506
+ errors: JsonError[];
507
+ }
508
+
439
509
  interface ValidationGroup {
440
510
  label: string;
441
511
  run(ajv: AjvInstance, projectDir: string, includes: Includes): number;
512
+ collectJson(ajv: AjvInstance, projectDir: string, includes: Includes, groupKey: string): JsonGroupResult;
442
513
  }
443
514
 
444
515
  const GROUPS: Record<string, ValidationGroup> = {
@@ -451,6 +522,12 @@ const GROUPS: Record<string, ValidationGroup> = {
451
522
  `${BASE}openuispec.schema.json`,
452
523
  );
453
524
  },
525
+ collectJson(ajv, projectDir, _includes, groupKey) {
526
+ return {
527
+ group: groupKey,
528
+ errors: collectValidateFile(ajv, join(projectDir, "openuispec.yaml"), `${BASE}openuispec.schema.json`),
529
+ };
530
+ },
454
531
  },
455
532
 
456
533
  tokens: {
@@ -476,6 +553,27 @@ const GROUPS: Record<string, ValidationGroup> = {
476
553
  }
477
554
  return errors;
478
555
  },
556
+ collectJson(ajv, projectDir, includes, groupKey) {
557
+ const errors: JsonError[] = [];
558
+ const tokensDir = resolveInclude(projectDir, includes.tokens);
559
+ const tokenMap: Record<string, string> = {
560
+ "color.yaml": "color.schema.json",
561
+ "typography.yaml": "typography.schema.json",
562
+ "spacing.yaml": "spacing.schema.json",
563
+ "elevation.yaml": "elevation.schema.json",
564
+ "motion.yaml": "motion.schema.json",
565
+ "layout.yaml": "layout.schema.json",
566
+ "themes.yaml": "themes.schema.json",
567
+ "icons.yaml": "icons.schema.json",
568
+ };
569
+ for (const [data, schema] of Object.entries(tokenMap)) {
570
+ const filePath = join(tokensDir, data);
571
+ if (existsSync(filePath)) {
572
+ errors.push(...collectValidateFile(ajv, filePath, `${BASE}tokens/${schema}`));
573
+ }
574
+ }
575
+ return { group: groupKey, errors };
576
+ },
479
577
  },
480
578
 
481
579
  screens: {
@@ -492,6 +590,18 @@ const GROUPS: Record<string, ValidationGroup> = {
492
590
  }
493
591
  return errors;
494
592
  },
593
+ collectJson(ajv, projectDir, includes, groupKey) {
594
+ const errors: JsonError[] = [];
595
+ const dir = resolveInclude(projectDir, includes.screens);
596
+ for (const f of listFiles(dir, ".yaml")) {
597
+ const schemaErrors = collectValidateFile(ajv, f, `${BASE}screen.schema.json`);
598
+ errors.push(...schemaErrors);
599
+ if (schemaErrors.length === 0) {
600
+ errors.push(...collectLintScreenFile(f));
601
+ }
602
+ }
603
+ return { group: groupKey, errors };
604
+ },
495
605
  },
496
606
 
497
607
  flows: {
@@ -508,6 +618,18 @@ const GROUPS: Record<string, ValidationGroup> = {
508
618
  }
509
619
  return errors;
510
620
  },
621
+ collectJson(ajv, projectDir, includes, groupKey) {
622
+ const errors: JsonError[] = [];
623
+ const dir = resolveInclude(projectDir, includes.flows);
624
+ for (const f of listFiles(dir, ".yaml")) {
625
+ const schemaErrors = collectValidateFile(ajv, f, `${BASE}flow.schema.json`);
626
+ errors.push(...schemaErrors);
627
+ if (schemaErrors.length === 0) {
628
+ errors.push(...collectLintFlowFile(f));
629
+ }
630
+ }
631
+ return { group: groupKey, errors };
632
+ },
511
633
  },
512
634
 
513
635
  platform: {
@@ -520,6 +642,14 @@ const GROUPS: Record<string, ValidationGroup> = {
520
642
  }
521
643
  return errors;
522
644
  },
645
+ collectJson(ajv, projectDir, includes, groupKey) {
646
+ const errors: JsonError[] = [];
647
+ const dir = resolveInclude(projectDir, includes.platform);
648
+ for (const f of listFiles(dir, ".yaml")) {
649
+ errors.push(...collectValidateFile(ajv, f, `${BASE}platform.schema.json`));
650
+ }
651
+ return { group: groupKey, errors };
652
+ },
523
653
  },
524
654
 
525
655
  locales: {
@@ -532,6 +662,14 @@ const GROUPS: Record<string, ValidationGroup> = {
532
662
  }
533
663
  return errors;
534
664
  },
665
+ collectJson(ajv, projectDir, includes, groupKey) {
666
+ const errors: JsonError[] = [];
667
+ const dir = resolveInclude(projectDir, includes.locales);
668
+ for (const f of listFiles(dir, ".json")) {
669
+ errors.push(...collectValidateFile(ajv, f, `${BASE}locale.schema.json`));
670
+ }
671
+ return { group: groupKey, errors };
672
+ },
535
673
  },
536
674
 
537
675
  contracts: {
@@ -549,6 +687,19 @@ const GROUPS: Record<string, ValidationGroup> = {
549
687
  }
550
688
  return errors;
551
689
  },
690
+ collectJson(ajv, projectDir, includes, groupKey) {
691
+ const errors: JsonError[] = [];
692
+ const dir = resolveInclude(projectDir, includes.contracts);
693
+ for (const f of listFiles(dir, ".yaml")) {
694
+ const name = basename(f);
695
+ if (name.startsWith("x_")) {
696
+ errors.push(...collectValidateFile(ajv, f, `${BASE}custom-contract.schema.json`));
697
+ } else {
698
+ errors.push(...collectValidateFile(ajv, f, `${BASE}contract.schema.json`));
699
+ }
700
+ }
701
+ return { group: groupKey, errors };
702
+ },
552
703
  },
553
704
 
554
705
  semantic: {
@@ -556,6 +707,17 @@ const GROUPS: Record<string, ValidationGroup> = {
556
707
  run(_ajv, projectDir, includes) {
557
708
  return runSemanticLint(projectDir, includes);
558
709
  },
710
+ collectJson(_ajv, projectDir, includes, groupKey) {
711
+ const lintErrors = collectSemanticLint(projectDir, includes);
712
+ return {
713
+ group: groupKey,
714
+ errors: lintErrors.map((e) => ({
715
+ file: e.path.includes("/") ? e.path.split("/")[0] : e.path,
716
+ path: e.path,
717
+ message: e.message,
718
+ })),
719
+ };
720
+ },
559
721
  },
560
722
  };
561
723
 
@@ -590,22 +752,47 @@ function findProjectDir(cwd: string): string {
590
752
 
591
753
  // ── main ─────────────────────────────────────────────────────────────
592
754
 
755
+ export { buildAjv, readIncludes, GROUPS };
756
+ export type { JsonGroupResult, JsonError };
757
+
593
758
  export function runValidate(argv: string[]): void {
759
+ const jsonMode = argv.includes("--json");
760
+ const filteredArgs = argv.filter((a) => a !== "--json");
761
+
594
762
  const selected =
595
- argv.length > 0
596
- ? argv.filter((a) => a in GROUPS)
763
+ filteredArgs.length > 0
764
+ ? filteredArgs.filter((a) => a in GROUPS)
597
765
  : Object.keys(GROUPS);
598
766
 
599
767
  if (selected.length === 0) {
600
768
  console.error(
601
769
  `Unknown group(s). Available: ${Object.keys(GROUPS).join(", ")}`,
602
770
  );
603
- process.exit(2);
771
+ process.exit(1);
604
772
  }
605
773
 
606
774
  const projectDir = findProjectDir(process.cwd());
607
775
  const includes = readIncludes(projectDir);
608
776
  const ajv = buildAjv();
777
+
778
+ if (jsonMode) {
779
+ const groups: JsonGroupResult[] = [];
780
+ let totalErrors = 0;
781
+
782
+ for (const key of selected) {
783
+ const result = GROUPS[key].collectJson(ajv, projectDir, includes, key);
784
+ groups.push(result);
785
+ totalErrors += result.errors.length;
786
+ }
787
+
788
+ console.log(JSON.stringify({ groups, total_errors: totalErrors }, null, 2));
789
+
790
+ if (totalErrors > 0) {
791
+ process.exit(2);
792
+ }
793
+ return;
794
+ }
795
+
609
796
  let totalErrors = 0;
610
797
 
611
798
  for (const key of selected) {
@@ -617,7 +804,7 @@ export function runValidate(argv: string[]): void {
617
804
  console.log(`\n${"=".repeat(50)}`);
618
805
  if (totalErrors > 0) {
619
806
  console.log(`FAILED: ${totalErrors} total validation error(s)`);
620
- process.exit(1);
807
+ process.exit(2);
621
808
  } else {
622
809
  console.log("ALL PASSED: Every example file validates successfully");
623
810
  }
package/status/index.ts CHANGED
@@ -40,6 +40,8 @@ interface TargetStatus {
40
40
  removed: number;
41
41
  behind: boolean;
42
42
  explain_available: boolean;
43
+ status: "up to date" | "behind" | "needs baseline" | "needs generation";
44
+ recommended_next_step: string;
43
45
  note?: string;
44
46
  }
45
47
 
@@ -96,6 +98,10 @@ function buildTargetStatus(cwd: string, projectDir: string, projectName: string,
96
98
  removed: 0,
97
99
  behind: false,
98
100
  explain_available: false,
101
+ status: outputExists ? "needs baseline" : "needs generation",
102
+ recommended_next_step: outputExists
103
+ ? `Run \`openuispec drift --snapshot --target ${target}\` after reviewing the generated output.`
104
+ : `Run code generation for "${target}", then \`openuispec prepare --target ${target}\` to build the target work bundle.`,
99
105
  note: outputExists
100
106
  ? "No snapshot found for this target."
101
107
  : `Output directory not found. Run code generation for "${target}" first.`,
@@ -126,6 +132,11 @@ function buildTargetStatus(cwd: string, projectDir: string, projectName: string,
126
132
  removed,
127
133
  behind: changed + added + removed > 0,
128
134
  explain_available: explanation.available,
135
+ status: changed + added + removed > 0 ? "behind" : "up to date",
136
+ recommended_next_step:
137
+ changed + added + removed > 0
138
+ ? `Run \`openuispec prepare --target ${target}\` to build the target work bundle for the pending spec changes.`
139
+ : `No immediate action required for "${target}". Re-run \`openuispec status\` after spec changes or after re-baselining.`,
129
140
  note: explanation.available ? undefined : explanation.note,
130
141
  };
131
142
  }
@@ -156,12 +167,6 @@ function printReport(result: StatusResult): void {
156
167
  : target.output_exists
157
168
  ? "no snapshot"
158
169
  : "output missing";
159
- const status = target.snapshot
160
- ? (target.behind ? "behind" : "up to date")
161
- : target.output_exists
162
- ? "needs baseline"
163
- : "needs generation";
164
-
165
170
  console.log(`${target.target}`);
166
171
  console.log(` output: ${target.output_dir}`);
167
172
  console.log(` output exists: ${target.output_exists ? "yes" : "no"}`);
@@ -170,11 +175,12 @@ function printReport(result: StatusResult): void {
170
175
  console.log(` baseline: ${target.baseline.label}`);
171
176
  }
172
177
  console.log(` drift: ${summary}`);
173
- console.log(` status: ${status}`);
178
+ console.log(` status: ${target.status}`);
174
179
  console.log(` explain: ${target.explain_available ? "available" : "unavailable"}`);
175
180
  if (target.note) {
176
181
  console.log(` note: ${target.note}`);
177
182
  }
183
+ console.log(` next: ${target.recommended_next_step}`);
178
184
  console.log("");
179
185
  }
180
186
  }