@orkestrel/scaffold 0.0.1 → 0.0.2

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.
@@ -440,6 +440,23 @@ export declare interface CompilerOptions {
440
440
  /** The three fixed pipeline phases, in order. */
441
441
  export declare type CompileStage = 'draft' | 'gate' | 'pin';
442
442
 
443
+ /**
444
+ * Measure a rendered fragment's column width, counting each literal tab as
445
+ * `JSON_TAB_WIDTH` columns (matching `.oxfmtrc.json`'s `tabWidth`) and every
446
+ * other character as one.
447
+ *
448
+ * @param text - The rendered fragment to measure.
449
+ * @returns The fragment's column width against `JSON_PRINT_WIDTH`.
450
+ *
451
+ * @example
452
+ * ```ts
453
+ * import { computeColumnWidth } from '@orkestrel/scaffold'
454
+ *
455
+ * computeColumnWidth('\t"a"') // 3 — one tab counted as JSON_TAB_WIDTH, plus two characters
456
+ * ```
457
+ */
458
+ export declare function computeColumnWidth(text: string): number;
459
+
443
460
  /**
444
461
  * Compute a canonical FNV-1a digest of a text string.
445
462
  *
@@ -660,14 +677,25 @@ export declare function coreViteConfig(): string;
660
677
  * @param plan - The plan whose artifacts are the source of truth.
661
678
  * @param current - The target's current content, keyed by artifact-relative path.
662
679
  * @remarks
663
- * A `template` / `computed` artifact whose rendered content the target does not
664
- * match is `stale`; one the target lacks is `missing`; a target file the plan
665
- * does not own is `foreign`. A `host`-origin artifact is audited by PRESENCE
666
- * only `missing` or `aligned`, never `stale` UNLESS it has been hydrated
667
- * with its real host bytes (`hydratePlan`'s `content`), in which case it is
668
- * content-compared exactly like a `template` / `computed` artifact and CAN be
680
+ * Audit semantics are per-origin. A `host`-origin artifact is audited by
681
+ * PRESENCE only `missing` or `aligned`, never `stale` UNLESS it has been
682
+ * hydrated with its real host bytes (`hydratePlan`'s `content`), in which case
683
+ * it is content-compared exactly like a `computed` artifact and CAN be
669
684
  * `stale`. A degrade-path or directory-shaped host artifact (never hydrated)
670
- * stays presence-only.
685
+ * stays presence-only. A `computed` artifact is content-aware canon —
686
+ * `missing` / `aligned` / `stale` — and gates the audit like any drifted
687
+ * finding. A `template`-origin artifact is BIRTH-ONLY and AUDIT-EXEMPT: it is
688
+ * always reported `aligned`, regardless of whether the target has it at all
689
+ * or what its bytes are. Starter files (source stubs, test stubs, starter
690
+ * guides, README) are written ONCE by `materialize` and are legitimately
691
+ * outgrown — real code replaces the stub, a hand-authored guide replaces the
692
+ * scaffold prose, an entity gets renamed. Content- or presence-comparing a
693
+ * mature package against its birth stub is a category error (the build and
694
+ * parity gates already police the package's substance) AND makes any
695
+ * unscoped repair a data-loss footgun — a stub overwrite would clobber real,
696
+ * hand-authored code. `template` findings therefore never contribute to
697
+ * `drifted` / `missing` / `clean`. A target file the plan does not own is
698
+ * `foreign`.
671
699
  * @returns The `Audit` of drift findings — pure, no I/O.
672
700
  *
673
701
  * @example
@@ -767,6 +795,31 @@ export declare function coreViteConfig(): string;
767
795
  readonly drift: Drift;
768
796
  }
769
797
 
798
+ /**
799
+ * Serialize a value to newline-terminated JSON that matches the fleet's own
800
+ * `oxfmt` output byte-for-byte — objects one key per line, arrays collapsed
801
+ * onto one line when they fit `JSON_PRINT_WIDTH`, one item per line
802
+ * otherwise.
803
+ *
804
+ * @param value - The value to serialize (config JSON — objects/arrays/primitives).
805
+ * @remarks
806
+ * `JSON.stringify(value, undefined, '\t')` always breaks arrays one item per
807
+ * line; `oxfmt` collapses short ones. Emitting through `formatJson` keeps
808
+ * computed config JSON format-stable by construction — `oxfmt --check` never
809
+ * has anything left to rewrite. The rendering itself is delegated to
810
+ * `renderValue` / `renderArray` / `renderObject` / `computeColumnWidth`, so
811
+ * `formatJson` is a thin orchestrator around them.
812
+ * @returns The rendered value, newline-terminated.
813
+ *
814
+ * @example
815
+ * ```ts
816
+ * import { formatJson } from '@orkestrel/scaffold'
817
+ *
818
+ * formatJson({ lib: ['ESNext', 'DOM'] }) // '{\n\t"lib": ["ESNext", "DOM"]\n}\n'
819
+ * ```
820
+ */
821
+ export declare function formatJson(value: unknown): string;
822
+
770
823
  /** The four `Freshness` values, frozen — the currency axis `Sync` reports on. */
771
824
  export declare const FRESHNESS: readonly ["current", "behind", "missing", "failed"];
772
825
 
@@ -1001,6 +1054,12 @@ export declare function coreViteConfig(): string;
1001
1054
  */
1002
1055
  export declare const isSyncReport: Guard<SyncReport>;
1003
1056
 
1057
+ /** The fleet's `.oxfmtrc.json` `printWidth` — `formatJson`'s array-collapse threshold. */
1058
+ export declare const JSON_PRINT_WIDTH = 100;
1059
+
1060
+ /** The fleet's `.oxfmtrc.json` `tabWidth` — the column width `formatJson` counts each tab as. */
1061
+ export declare const JSON_TAB_WIDTH = 2;
1062
+
1004
1063
  /**
1005
1064
  * Parse a `package.json` text into its declared `@orkestrel/*` dependencies.
1006
1065
  *
@@ -1501,6 +1560,62 @@ export declare function coreViteConfig(): string;
1501
1560
  */
1502
1561
  export declare function rangeToFreshness(range: string, latest: string): Freshness;
1503
1562
 
1563
+ /**
1564
+ * Render a JSON array through `formatJson`'s inline-or-broken rule — inline
1565
+ * when the rendered width (via `computeColumnWidth`) fits `JSON_PRINT_WIDTH`, one
1566
+ * item per line otherwise.
1567
+ *
1568
+ * @param entries - The array's elements, in order.
1569
+ * @param indent - The current indentation prefix.
1570
+ * @param prefix - The text already emitted on this line before the array.
1571
+ * @param suffix - The text that will follow the array on this line.
1572
+ * @returns The rendered array fragment (no trailing newline).
1573
+ *
1574
+ * @example
1575
+ * ```ts
1576
+ * import { renderArray } from '@orkestrel/scaffold'
1577
+ *
1578
+ * renderArray(['ESNext', 'DOM'], '', '', '') // '["ESNext", "DOM"]'
1579
+ * ```
1580
+ */
1581
+ export declare function renderArray(entries: readonly unknown[], indent: string, prefix: string, suffix: string): string;
1582
+
1583
+ /**
1584
+ * Render a JSON object through `formatJson`'s one-key-per-line rule.
1585
+ *
1586
+ * @param entry - The object to render.
1587
+ * @param indent - The current indentation prefix.
1588
+ * @returns The rendered object fragment (no trailing newline).
1589
+ *
1590
+ * @example
1591
+ * ```ts
1592
+ * import { renderObject } from '@orkestrel/scaffold'
1593
+ *
1594
+ * renderObject({ lib: ['ESNext'] }, '') // '{\n\t"lib": ["ESNext"]\n}'
1595
+ * ```
1596
+ */
1597
+ export declare function renderObject(entry: Readonly<Record<string, unknown>>, indent: string): string;
1598
+
1599
+ /**
1600
+ * Render one JSON value through `formatJson`'s dispatch — arrays via
1601
+ * `renderArray`, objects via `renderObject`, everything else via
1602
+ * `JSON.stringify`.
1603
+ *
1604
+ * @param entry - The value to render.
1605
+ * @param indent - The current indentation prefix.
1606
+ * @param prefix - The text already emitted on this line before `entry`.
1607
+ * @param suffix - The text that will follow `entry` on this line.
1608
+ * @returns The rendered fragment (no trailing newline).
1609
+ *
1610
+ * @example
1611
+ * ```ts
1612
+ * import { renderValue } from '@orkestrel/scaffold'
1613
+ *
1614
+ * renderValue('ESNext', '', '', '') // '"ESNext"'
1615
+ * ```
1616
+ */
1617
+ export declare function renderValue(entry: unknown, indent: string, prefix: string, suffix: string): string;
1618
+
1504
1619
  /**
1505
1620
  * The root `tsconfig.json` — one `@src/<surface>` path alias per declared
1506
1621
  * surface, in declared order.
@@ -1543,7 +1658,7 @@ export declare function coreViteConfig(): string;
1543
1658
  export declare function rootViteConfig(surfaces: readonly Surface[]): string;
1544
1659
 
1545
1660
  /** The devDependency range generated packages pin `@orkestrel/scaffold` at. */
1546
- export declare const SCAFFOLD_RANGE = "^0.0.1";
1661
+ export declare const SCAFFOLD_RANGE = "^0.0.2";
1547
1662
 
1548
1663
  /**
1549
1664
  * Carries a `ScaffoldErrorCode` + optional `context` (AGENTS §12).
@@ -440,6 +440,23 @@ export declare interface CompilerOptions {
440
440
  /** The three fixed pipeline phases, in order. */
441
441
  export declare type CompileStage = 'draft' | 'gate' | 'pin';
442
442
 
443
+ /**
444
+ * Measure a rendered fragment's column width, counting each literal tab as
445
+ * `JSON_TAB_WIDTH` columns (matching `.oxfmtrc.json`'s `tabWidth`) and every
446
+ * other character as one.
447
+ *
448
+ * @param text - The rendered fragment to measure.
449
+ * @returns The fragment's column width against `JSON_PRINT_WIDTH`.
450
+ *
451
+ * @example
452
+ * ```ts
453
+ * import { computeColumnWidth } from '@orkestrel/scaffold'
454
+ *
455
+ * computeColumnWidth('\t"a"') // 3 — one tab counted as JSON_TAB_WIDTH, plus two characters
456
+ * ```
457
+ */
458
+ export declare function computeColumnWidth(text: string): number;
459
+
443
460
  /**
444
461
  * Compute a canonical FNV-1a digest of a text string.
445
462
  *
@@ -660,14 +677,25 @@ export declare function coreViteConfig(): string;
660
677
  * @param plan - The plan whose artifacts are the source of truth.
661
678
  * @param current - The target's current content, keyed by artifact-relative path.
662
679
  * @remarks
663
- * A `template` / `computed` artifact whose rendered content the target does not
664
- * match is `stale`; one the target lacks is `missing`; a target file the plan
665
- * does not own is `foreign`. A `host`-origin artifact is audited by PRESENCE
666
- * only `missing` or `aligned`, never `stale` UNLESS it has been hydrated
667
- * with its real host bytes (`hydratePlan`'s `content`), in which case it is
668
- * content-compared exactly like a `template` / `computed` artifact and CAN be
680
+ * Audit semantics are per-origin. A `host`-origin artifact is audited by
681
+ * PRESENCE only `missing` or `aligned`, never `stale` UNLESS it has been
682
+ * hydrated with its real host bytes (`hydratePlan`'s `content`), in which case
683
+ * it is content-compared exactly like a `computed` artifact and CAN be
669
684
  * `stale`. A degrade-path or directory-shaped host artifact (never hydrated)
670
- * stays presence-only.
685
+ * stays presence-only. A `computed` artifact is content-aware canon —
686
+ * `missing` / `aligned` / `stale` — and gates the audit like any drifted
687
+ * finding. A `template`-origin artifact is BIRTH-ONLY and AUDIT-EXEMPT: it is
688
+ * always reported `aligned`, regardless of whether the target has it at all
689
+ * or what its bytes are. Starter files (source stubs, test stubs, starter
690
+ * guides, README) are written ONCE by `materialize` and are legitimately
691
+ * outgrown — real code replaces the stub, a hand-authored guide replaces the
692
+ * scaffold prose, an entity gets renamed. Content- or presence-comparing a
693
+ * mature package against its birth stub is a category error (the build and
694
+ * parity gates already police the package's substance) AND makes any
695
+ * unscoped repair a data-loss footgun — a stub overwrite would clobber real,
696
+ * hand-authored code. `template` findings therefore never contribute to
697
+ * `drifted` / `missing` / `clean`. A target file the plan does not own is
698
+ * `foreign`.
671
699
  * @returns The `Audit` of drift findings — pure, no I/O.
672
700
  *
673
701
  * @example
@@ -767,6 +795,31 @@ export declare function coreViteConfig(): string;
767
795
  readonly drift: Drift;
768
796
  }
769
797
 
798
+ /**
799
+ * Serialize a value to newline-terminated JSON that matches the fleet's own
800
+ * `oxfmt` output byte-for-byte — objects one key per line, arrays collapsed
801
+ * onto one line when they fit `JSON_PRINT_WIDTH`, one item per line
802
+ * otherwise.
803
+ *
804
+ * @param value - The value to serialize (config JSON — objects/arrays/primitives).
805
+ * @remarks
806
+ * `JSON.stringify(value, undefined, '\t')` always breaks arrays one item per
807
+ * line; `oxfmt` collapses short ones. Emitting through `formatJson` keeps
808
+ * computed config JSON format-stable by construction — `oxfmt --check` never
809
+ * has anything left to rewrite. The rendering itself is delegated to
810
+ * `renderValue` / `renderArray` / `renderObject` / `computeColumnWidth`, so
811
+ * `formatJson` is a thin orchestrator around them.
812
+ * @returns The rendered value, newline-terminated.
813
+ *
814
+ * @example
815
+ * ```ts
816
+ * import { formatJson } from '@orkestrel/scaffold'
817
+ *
818
+ * formatJson({ lib: ['ESNext', 'DOM'] }) // '{\n\t"lib": ["ESNext", "DOM"]\n}\n'
819
+ * ```
820
+ */
821
+ export declare function formatJson(value: unknown): string;
822
+
770
823
  /** The four `Freshness` values, frozen — the currency axis `Sync` reports on. */
771
824
  export declare const FRESHNESS: readonly ["current", "behind", "missing", "failed"];
772
825
 
@@ -1001,6 +1054,12 @@ export declare function coreViteConfig(): string;
1001
1054
  */
1002
1055
  export declare const isSyncReport: Guard<SyncReport>;
1003
1056
 
1057
+ /** The fleet's `.oxfmtrc.json` `printWidth` — `formatJson`'s array-collapse threshold. */
1058
+ export declare const JSON_PRINT_WIDTH = 100;
1059
+
1060
+ /** The fleet's `.oxfmtrc.json` `tabWidth` — the column width `formatJson` counts each tab as. */
1061
+ export declare const JSON_TAB_WIDTH = 2;
1062
+
1004
1063
  /**
1005
1064
  * Parse a `package.json` text into its declared `@orkestrel/*` dependencies.
1006
1065
  *
@@ -1501,6 +1560,62 @@ export declare function coreViteConfig(): string;
1501
1560
  */
1502
1561
  export declare function rangeToFreshness(range: string, latest: string): Freshness;
1503
1562
 
1563
+ /**
1564
+ * Render a JSON array through `formatJson`'s inline-or-broken rule — inline
1565
+ * when the rendered width (via `computeColumnWidth`) fits `JSON_PRINT_WIDTH`, one
1566
+ * item per line otherwise.
1567
+ *
1568
+ * @param entries - The array's elements, in order.
1569
+ * @param indent - The current indentation prefix.
1570
+ * @param prefix - The text already emitted on this line before the array.
1571
+ * @param suffix - The text that will follow the array on this line.
1572
+ * @returns The rendered array fragment (no trailing newline).
1573
+ *
1574
+ * @example
1575
+ * ```ts
1576
+ * import { renderArray } from '@orkestrel/scaffold'
1577
+ *
1578
+ * renderArray(['ESNext', 'DOM'], '', '', '') // '["ESNext", "DOM"]'
1579
+ * ```
1580
+ */
1581
+ export declare function renderArray(entries: readonly unknown[], indent: string, prefix: string, suffix: string): string;
1582
+
1583
+ /**
1584
+ * Render a JSON object through `formatJson`'s one-key-per-line rule.
1585
+ *
1586
+ * @param entry - The object to render.
1587
+ * @param indent - The current indentation prefix.
1588
+ * @returns The rendered object fragment (no trailing newline).
1589
+ *
1590
+ * @example
1591
+ * ```ts
1592
+ * import { renderObject } from '@orkestrel/scaffold'
1593
+ *
1594
+ * renderObject({ lib: ['ESNext'] }, '') // '{\n\t"lib": ["ESNext"]\n}'
1595
+ * ```
1596
+ */
1597
+ export declare function renderObject(entry: Readonly<Record<string, unknown>>, indent: string): string;
1598
+
1599
+ /**
1600
+ * Render one JSON value through `formatJson`'s dispatch — arrays via
1601
+ * `renderArray`, objects via `renderObject`, everything else via
1602
+ * `JSON.stringify`.
1603
+ *
1604
+ * @param entry - The value to render.
1605
+ * @param indent - The current indentation prefix.
1606
+ * @param prefix - The text already emitted on this line before `entry`.
1607
+ * @param suffix - The text that will follow `entry` on this line.
1608
+ * @returns The rendered fragment (no trailing newline).
1609
+ *
1610
+ * @example
1611
+ * ```ts
1612
+ * import { renderValue } from '@orkestrel/scaffold'
1613
+ *
1614
+ * renderValue('ESNext', '', '', '') // '"ESNext"'
1615
+ * ```
1616
+ */
1617
+ export declare function renderValue(entry: unknown, indent: string, prefix: string, suffix: string): string;
1618
+
1504
1619
  /**
1505
1620
  * The root `tsconfig.json` — one `@src/<surface>` path alias per declared
1506
1621
  * surface, in declared order.
@@ -1543,7 +1658,7 @@ export declare function coreViteConfig(): string;
1543
1658
  export declare function rootViteConfig(surfaces: readonly Surface[]): string;
1544
1659
 
1545
1660
  /** The devDependency range generated packages pin `@orkestrel/scaffold` at. */
1546
- export declare const SCAFFOLD_RANGE = "^0.0.1";
1661
+ export declare const SCAFFOLD_RANGE = "^0.0.2";
1547
1662
 
1548
1663
  /**
1549
1664
  * Carries a `ScaffoldErrorCode` + optional `context` (AGENTS §12).
@@ -128,9 +128,13 @@ var DEFAULT_VERSION = "0.0.1";
128
128
  /** The `engines.node` range the `blueprint` builder fills. */
129
129
  var DEFAULT_ENGINES = ">=22";
130
130
  /** The devDependency range generated packages pin `@orkestrel/scaffold` at. */
131
- var SCAFFOLD_RANGE = "^0.0.1";
131
+ var SCAFFOLD_RANGE = "^0.0.2";
132
132
  /** The default id for a `Compiler` orchestrator. */
133
133
  var COMPILER_ID = "compiler";
134
+ /** The fleet's `.oxfmtrc.json` `printWidth` — `formatJson`'s array-collapse threshold. */
135
+ var JSON_PRINT_WIDTH = 100;
136
+ /** The fleet's `.oxfmtrc.json` `tabWidth` — the column width `formatJson` counts each tab as. */
137
+ var JSON_TAB_WIDTH = 2;
134
138
  //#endregion
135
139
  //#region src/core/errors.ts
136
140
  /**
@@ -984,14 +988,25 @@ function inferGroup(path) {
984
988
  * @param plan - The plan whose artifacts are the source of truth.
985
989
  * @param current - The target's current content, keyed by artifact-relative path.
986
990
  * @remarks
987
- * A `template` / `computed` artifact whose rendered content the target does not
988
- * match is `stale`; one the target lacks is `missing`; a target file the plan
989
- * does not own is `foreign`. A `host`-origin artifact is audited by PRESENCE
990
- * only `missing` or `aligned`, never `stale` UNLESS it has been hydrated
991
- * with its real host bytes (`hydratePlan`'s `content`), in which case it is
992
- * content-compared exactly like a `template` / `computed` artifact and CAN be
991
+ * Audit semantics are per-origin. A `host`-origin artifact is audited by
992
+ * PRESENCE only `missing` or `aligned`, never `stale` UNLESS it has been
993
+ * hydrated with its real host bytes (`hydratePlan`'s `content`), in which case
994
+ * it is content-compared exactly like a `computed` artifact and CAN be
993
995
  * `stale`. A degrade-path or directory-shaped host artifact (never hydrated)
994
- * stays presence-only.
996
+ * stays presence-only. A `computed` artifact is content-aware canon —
997
+ * `missing` / `aligned` / `stale` — and gates the audit like any drifted
998
+ * finding. A `template`-origin artifact is BIRTH-ONLY and AUDIT-EXEMPT: it is
999
+ * always reported `aligned`, regardless of whether the target has it at all
1000
+ * or what its bytes are. Starter files (source stubs, test stubs, starter
1001
+ * guides, README) are written ONCE by `materialize` and are legitimately
1002
+ * outgrown — real code replaces the stub, a hand-authored guide replaces the
1003
+ * scaffold prose, an entity gets renamed. Content- or presence-comparing a
1004
+ * mature package against its birth stub is a category error (the build and
1005
+ * parity gates already police the package's substance) AND makes any
1006
+ * unscoped repair a data-loss footgun — a stub overwrite would clobber real,
1007
+ * hand-authored code. `template` findings therefore never contribute to
1008
+ * `drifted` / `missing` / `clean`. A target file the plan does not own is
1009
+ * `foreign`.
995
1010
  * @returns The `Audit` of drift findings — pure, no I/O.
996
1011
  *
997
1012
  * @example
@@ -1007,6 +1022,14 @@ function diffPlan(plan, current) {
1007
1022
  for (const artifact of plan.artifacts) {
1008
1023
  owned.add(artifact.path);
1009
1024
  const seen = current[artifact.path];
1025
+ if (artifact.origin === "template") {
1026
+ findings.push({
1027
+ path: artifact.path,
1028
+ group: artifact.group,
1029
+ drift: "aligned"
1030
+ });
1031
+ continue;
1032
+ }
1010
1033
  if (artifact.origin === "host") {
1011
1034
  let drift;
1012
1035
  if (seen === void 0) drift = "missing";
@@ -1379,6 +1402,125 @@ function stableStringify(value) {
1379
1402
  return JSON.stringify(value);
1380
1403
  }
1381
1404
  /**
1405
+ * Measure a rendered fragment's column width, counting each literal tab as
1406
+ * `JSON_TAB_WIDTH` columns (matching `.oxfmtrc.json`'s `tabWidth`) and every
1407
+ * other character as one.
1408
+ *
1409
+ * @param text - The rendered fragment to measure.
1410
+ * @returns The fragment's column width against `JSON_PRINT_WIDTH`.
1411
+ *
1412
+ * @example
1413
+ * ```ts
1414
+ * import { computeColumnWidth } from '@orkestrel/scaffold'
1415
+ *
1416
+ * computeColumnWidth('\t"a"') // 3 — one tab counted as JSON_TAB_WIDTH, plus two characters
1417
+ * ```
1418
+ */
1419
+ function computeColumnWidth(text) {
1420
+ let width = 0;
1421
+ for (const char of text) width += char === " " ? 2 : 1;
1422
+ return width;
1423
+ }
1424
+ /**
1425
+ * Render a JSON array through `formatJson`'s inline-or-broken rule — inline
1426
+ * when the rendered width (via `computeColumnWidth`) fits `JSON_PRINT_WIDTH`, one
1427
+ * item per line otherwise.
1428
+ *
1429
+ * @param entries - The array's elements, in order.
1430
+ * @param indent - The current indentation prefix.
1431
+ * @param prefix - The text already emitted on this line before the array.
1432
+ * @param suffix - The text that will follow the array on this line.
1433
+ * @returns The rendered array fragment (no trailing newline).
1434
+ *
1435
+ * @example
1436
+ * ```ts
1437
+ * import { renderArray } from '@orkestrel/scaffold'
1438
+ *
1439
+ * renderArray(['ESNext', 'DOM'], '', '', '') // '["ESNext", "DOM"]'
1440
+ * ```
1441
+ */
1442
+ function renderArray(entries, indent, prefix, suffix) {
1443
+ if (entries.length === 0) return "[]";
1444
+ const items = entries.map((entry) => renderValue(entry, indent, "", ""));
1445
+ const inline = `[${items.join(", ")}]`;
1446
+ if (computeColumnWidth(`${prefix}${inline}${suffix}`) <= 100) return inline;
1447
+ const childIndent = `${indent}\t`;
1448
+ return `[\n${items.map((item) => `${childIndent}${item}`).join(",\n")}\n${indent}]`;
1449
+ }
1450
+ /**
1451
+ * Render a JSON object through `formatJson`'s one-key-per-line rule.
1452
+ *
1453
+ * @param entry - The object to render.
1454
+ * @param indent - The current indentation prefix.
1455
+ * @returns The rendered object fragment (no trailing newline).
1456
+ *
1457
+ * @example
1458
+ * ```ts
1459
+ * import { renderObject } from '@orkestrel/scaffold'
1460
+ *
1461
+ * renderObject({ lib: ['ESNext'] }, '') // '{\n\t"lib": ["ESNext"]\n}'
1462
+ * ```
1463
+ */
1464
+ function renderObject(entry, indent) {
1465
+ const keys = Object.keys(entry);
1466
+ if (keys.length === 0) return "{}";
1467
+ const childIndent = `${indent}\t`;
1468
+ return `{\n${keys.map((key, index) => {
1469
+ const prefix = `${childIndent}${JSON.stringify(key)}: `;
1470
+ const suffix = index === keys.length - 1 ? "" : ",";
1471
+ return `${prefix}${renderValue(entry[key], childIndent, prefix, suffix)}${suffix}`;
1472
+ }).join("\n")}\n${indent}}`;
1473
+ }
1474
+ /**
1475
+ * Render one JSON value through `formatJson`'s dispatch — arrays via
1476
+ * `renderArray`, objects via `renderObject`, everything else via
1477
+ * `JSON.stringify`.
1478
+ *
1479
+ * @param entry - The value to render.
1480
+ * @param indent - The current indentation prefix.
1481
+ * @param prefix - The text already emitted on this line before `entry`.
1482
+ * @param suffix - The text that will follow `entry` on this line.
1483
+ * @returns The rendered fragment (no trailing newline).
1484
+ *
1485
+ * @example
1486
+ * ```ts
1487
+ * import { renderValue } from '@orkestrel/scaffold'
1488
+ *
1489
+ * renderValue('ESNext', '', '', '') // '"ESNext"'
1490
+ * ```
1491
+ */
1492
+ function renderValue(entry, indent, prefix, suffix) {
1493
+ if (Array.isArray(entry)) return renderArray(entry, indent, prefix, suffix);
1494
+ if (isRecord(entry)) return renderObject(entry, indent);
1495
+ return JSON.stringify(entry);
1496
+ }
1497
+ /**
1498
+ * Serialize a value to newline-terminated JSON that matches the fleet's own
1499
+ * `oxfmt` output byte-for-byte — objects one key per line, arrays collapsed
1500
+ * onto one line when they fit `JSON_PRINT_WIDTH`, one item per line
1501
+ * otherwise.
1502
+ *
1503
+ * @param value - The value to serialize (config JSON — objects/arrays/primitives).
1504
+ * @remarks
1505
+ * `JSON.stringify(value, undefined, '\t')` always breaks arrays one item per
1506
+ * line; `oxfmt` collapses short ones. Emitting through `formatJson` keeps
1507
+ * computed config JSON format-stable by construction — `oxfmt --check` never
1508
+ * has anything left to rewrite. The rendering itself is delegated to
1509
+ * `renderValue` / `renderArray` / `renderObject` / `computeColumnWidth`, so
1510
+ * `formatJson` is a thin orchestrator around them.
1511
+ * @returns The rendered value, newline-terminated.
1512
+ *
1513
+ * @example
1514
+ * ```ts
1515
+ * import { formatJson } from '@orkestrel/scaffold'
1516
+ *
1517
+ * formatJson({ lib: ['ESNext', 'DOM'] }) // '{\n\t"lib": ["ESNext", "DOM"]\n}\n'
1518
+ * ```
1519
+ */
1520
+ function formatJson(value) {
1521
+ return `${renderValue(value, "", "", "")}\n`;
1522
+ }
1523
+ /**
1382
1524
  * Return a fresh `Plan` with `trace` and `hash` filled.
1383
1525
  *
1384
1526
  * @param plan - The plan to pin.
@@ -2251,6 +2393,8 @@ function packageManifest(spec) {
2251
2393
  for (const peer of [...spec.peers].sort((a, b) => compareCodeUnit(a.name, b.name))) peerDependencies[peer.name] = peer.range;
2252
2394
  const peerDependenciesMeta = {};
2253
2395
  for (const peer of spec.peers) if (peer.optional === true) peerDependenciesMeta[peer.name] = { optional: true };
2396
+ const peerDevDependencies = {};
2397
+ for (const peer of [...spec.peers].sort((a, b) => compareCodeUnit(a.name, b.name))) peerDevDependencies[peer.name] = peer.range;
2254
2398
  const scripts = {
2255
2399
  clean: "node -e \"try{require('node:fs').rmSync('dist',{recursive:true,force:true})}catch{}\"",
2256
2400
  copy: "node -e \"const fs=require('node:fs'),p=require('node:path'),a=process.argv[1],b=process.argv[2];fs.mkdirSync(p.dirname(b),{recursive:true});fs.cpSync(a,b,{force:true});console.log('Copied: '+a+' to '+b)\"",
@@ -2294,7 +2438,10 @@ function packageManifest(spec) {
2294
2438
  publishConfig: { access: "public" },
2295
2439
  scripts,
2296
2440
  dependencies,
2297
- devDependencies: devDependenciesFor(spec.extras),
2441
+ devDependencies: Object.fromEntries(Object.entries({
2442
+ ...devDependenciesFor(spec.extras),
2443
+ ...peerDevDependencies
2444
+ }).sort(([a], [b]) => compareCodeUnit(a, b))),
2298
2445
  ...Object.keys(peerDependencies).length > 0 ? { peerDependencies } : {},
2299
2446
  ...Object.keys(peerDependenciesMeta).length > 0 ? { peerDependenciesMeta } : {},
2300
2447
  engines: { node: spec.engines }
@@ -2316,7 +2463,7 @@ function packageManifest(spec) {
2316
2463
  function rootTsconfig(surfaces) {
2317
2464
  const paths = {};
2318
2465
  for (const surface of surfaces) paths[`@src/${surface}`] = [`./src/${surface}/index.ts`];
2319
- return `${JSON.stringify({
2466
+ return formatJson({
2320
2467
  compilerOptions: {
2321
2468
  target: "ESNext",
2322
2469
  module: "ESNext",
@@ -2346,7 +2493,7 @@ function rootTsconfig(surfaces) {
2346
2493
  "dist",
2347
2494
  "tmp"
2348
2495
  ]
2349
- }, void 0, " ")}\n`;
2496
+ });
2350
2497
  }
2351
2498
  /**
2352
2499
  * The rendered import / `resolve` header block every `rootViteConfig` shape
@@ -2701,7 +2848,7 @@ export default defineConfig({
2701
2848
  * ```
2702
2849
  */
2703
2850
  function coreTsconfig() {
2704
- return `${JSON.stringify({
2851
+ return formatJson({
2705
2852
  extends: "../../tsconfig.json",
2706
2853
  compilerOptions: {
2707
2854
  lib: ["ESNext"],
@@ -2712,7 +2859,7 @@ function coreTsconfig() {
2712
2859
  outDir: "../../dist/src/core"
2713
2860
  },
2714
2861
  include: ["../../src/core/**/*.ts"]
2715
- }, void 0, " ")}\n`;
2862
+ });
2716
2863
  }
2717
2864
  /**
2718
2865
  * `configs/src/vite.core.config.ts` — inlines its own `build.lib` /
@@ -2767,7 +2914,7 @@ export default defineConfig(
2767
2914
  * ```
2768
2915
  */
2769
2916
  function surfaceTsconfig(surface) {
2770
- const config = {
2917
+ return formatJson({
2771
2918
  extends: "../../tsconfig.json",
2772
2919
  compilerOptions: {
2773
2920
  lib: surface === "browser" ? [
@@ -2783,8 +2930,7 @@ function surfaceTsconfig(surface) {
2783
2930
  outDir: "../../dist/src"
2784
2931
  },
2785
2932
  include: [`../../src/${surface}/**/*.ts`]
2786
- };
2787
- return `${JSON.stringify(config, void 0, " ")}\n`;
2933
+ });
2788
2934
  }
2789
2935
  /**
2790
2936
  * `configs/src/vite.<browser|server>.config.ts` — a thin `dts`-only wrapper;
@@ -3631,6 +3777,6 @@ function createBlueprint(data) {
3631
3777
  return candidate;
3632
3778
  }
3633
3779
  //#endregion
3634
- export { CATEGORIES, COMPILER_ID, COMPILE_STAGES, Compiler, DEFAULT_ENGINES, DEFAULT_VERSION, DEPENDENCY_NAME_PATTERN, EXTRA_NAME_PATTERN, FRESHNESS, GROUPS, HOST_PATHS, NAME_PATTERN, ORIGINS, PlanManager, SCAFFOLD_RANGE, SURFACES, SURFACE_MATRIX, ScaffoldError, TEMPLATES, alignTable, applyOverrides, artifactShape, auditToReview, blueprint, blueprintShape, blueprintToMembers, blueprintToPlan, catalogNames, catalogToBlock, compareCodeUnit, computeHash, configArtifacts, coreTsconfig, coreViteConfig, createBlueprint, createCompiler, createPlanManager, delimiterCell, dependency, dependencyShape, devDependenciesFor, diffPlan, dualCondition, entryFields, exportsMap, fillArtifact, guideArtifacts, guideMemberTable, hostGroup, inferGroup, isArtifact, isBehind, isBlueprint, isDependency, isMember, isOverride, isPlan, isRecord, isScaffoldError, isSyncReport, manifestToDependencies, member, memberShape, override, overrideShape, packageManifest, padCell, paritySpecifiers, parseBlueprint, parsePlan, parseSyncReport, pascalCase, pinPlan, planShape, planToReview, planToSummary, rangeToFreshness, rootTsconfig, rootViteConfig, singleSurfaceViteConfig, sourceArtifacts, splitTableRow, stableStringify, surfaceTsconfig, surfaceVariant, surfaceViteConfig, syncReportShape, syncToReview, testArtifacts, validateBlueprint, validateDependencyArray, viteHeader };
3780
+ export { CATEGORIES, COMPILER_ID, COMPILE_STAGES, Compiler, DEFAULT_ENGINES, DEFAULT_VERSION, DEPENDENCY_NAME_PATTERN, EXTRA_NAME_PATTERN, FRESHNESS, GROUPS, HOST_PATHS, JSON_PRINT_WIDTH, JSON_TAB_WIDTH, NAME_PATTERN, ORIGINS, PlanManager, SCAFFOLD_RANGE, SURFACES, SURFACE_MATRIX, ScaffoldError, TEMPLATES, alignTable, applyOverrides, artifactShape, auditToReview, blueprint, blueprintShape, blueprintToMembers, blueprintToPlan, catalogNames, catalogToBlock, compareCodeUnit, computeColumnWidth, computeHash, configArtifacts, coreTsconfig, coreViteConfig, createBlueprint, createCompiler, createPlanManager, delimiterCell, dependency, dependencyShape, devDependenciesFor, diffPlan, dualCondition, entryFields, exportsMap, fillArtifact, formatJson, guideArtifacts, guideMemberTable, hostGroup, inferGroup, isArtifact, isBehind, isBlueprint, isDependency, isMember, isOverride, isPlan, isRecord, isScaffoldError, isSyncReport, manifestToDependencies, member, memberShape, override, overrideShape, packageManifest, padCell, paritySpecifiers, parseBlueprint, parsePlan, parseSyncReport, pascalCase, pinPlan, planShape, planToReview, planToSummary, rangeToFreshness, renderArray, renderObject, renderValue, rootTsconfig, rootViteConfig, singleSurfaceViteConfig, sourceArtifacts, splitTableRow, stableStringify, surfaceTsconfig, surfaceVariant, surfaceViteConfig, syncReportShape, syncToReview, testArtifacts, validateBlueprint, validateDependencyArray, viteHeader };
3635
3781
 
3636
3782
  //# sourceMappingURL=index.js.map