@orkestrel/scaffold 0.0.49 → 0.0.51

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.
package/README.md CHANGED
@@ -55,8 +55,10 @@ npx scaffold repair
55
55
  ```
56
56
 
57
57
  Restores each planned path the target is missing or has let drift, then re-audits. A file the
58
- workspace owns — its manifest, its source, its tests, its README — is written once at creation and
59
- is never rewritten here.
58
+ workspace owns — its source, its own proofs, its README — is written once at creation and is never
59
+ rewritten here. Two paths are not owned that way: `tests/distribution.test.ts` is restored when it
60
+ is absent and left alone when the workspace has replaced it, and the manifest's script region is
61
+ rewritten when its chain is the one scaffold generated and refused without a write when it is not.
60
62
 
61
63
  ### `catalog` — refresh the package table and the guide mirrors
62
64
 
package/dist/bin/main.js CHANGED
@@ -3,7 +3,7 @@ import { align, renderTable, strip, stripControls, width } from "@orkestrel/cons
3
3
  import { attempt, isRecord, isString, parseJSON } from "@orkestrel/contract";
4
4
  import { createMarkdown, flattenText, isTableNode } from "@orkestrel/markdown";
5
5
  import { executeSync } from "@orkestrel/process/server";
6
- import { BIN_ENTRY_PATH, CATALOG_AGENT_PATH, CONFORMANCE_TEST_PATH, Compiler, DEPENDENCY_NAME_PATTERN, DISTRIBUTION_TEST_PATH, ENVIRONMENTS, GLOBAL_SETUP_PATH, GROUPS, GUIDES_TEST_PATH, INTEGRATION_TEST_PATH, MAX_MANIFEST_BYTES, SERVICE_SETUP_PATH, SHOWCASE_CONFIG_PATH, ScaffoldError, blueprintToDevDependencies, blueprintToRootVite, blueprintToScripts, compareVersions, createBlueprint, extractRangeMajor, extractVersion, isDeferredPath, isScaffoldError, manifestToDependencies, manifestToName, nameToGuide, replacePlanRanges } from "../src/core/index.js";
6
+ import { BIN_ENTRY_PATH, CATALOG_AGENT_PATH, CONFORMANCE_TEST_PATH, Compiler, DEPENDENCY_NAME_PATTERN, ENVIRONMENTS, GLOBAL_SETUP_PATH, GROUPS, GUIDES_TEST_PATH, HOST_PATHS, INTEGRATION_TEST_PATH, MAX_MANIFEST_BYTES, SERVICE_SETUP_PATH, SHOWCASE_CONFIG_PATH, ScaffoldError, blueprintToDevDependencies, blueprintToRootVite, blueprintToScripts, blueprintToTestArtifacts, blueprintToWritableScripts, compareVersions, createBlueprint, extractRangeMajor, extractVersion, isDeferredPath, isScaffoldError, manifestToDependencies, manifestToName, nameToGuide, replaceManifestScripts, replacePlanRanges } from "../src/core/index.js";
7
7
  import { Materializer, Upstream, filesToHost, isExactCaseFile, isPhysicalDirectory, isWorktree, listFiles, readFileText, readHostFloor, readSnapshot, resolveContainedPath } from "../src/server/index.js";
8
8
  import { parseArgs } from "node:util";
9
9
  //#region src/bin/constants.ts
@@ -463,27 +463,43 @@ function auditToExit(audit) {
463
463
  return blocked || drifted ? 1 : 0;
464
464
  }
465
465
  /**
466
- * Read the fleet rows and planned foreign tools a target manifest declares.
466
+ * Read the runtime and development rows a writing verb may raise.
467
467
  *
468
468
  * @param manifest - The target manifest text.
469
469
  * @param blueprint - The workspace shape that supplies the planned tool set.
470
- * @returns The declared fleet rows followed by declared planned foreign rows.
470
+ * @returns The declared fleet rows and planned foreign tools in their writable sections.
471
471
  */
472
- function manifestToPlannedDependencies(manifest, blueprint) {
473
- const dependencies = [...dependenciesToFleet(manifestToDependencies(manifest))];
472
+ function manifestToWritableDependencies(manifest, blueprint) {
473
+ const declared = manifestToDependencies(manifest);
474
+ const runtime = [...dependenciesToFleet(declared.runtime)];
475
+ const development = [...dependenciesToFleet(declared.development)];
474
476
  const parsed = parseJSON(manifest);
475
- if (!isRecord(parsed)) return dependencies;
476
- const runtime = isRecord(parsed.dependencies) ? parsed.dependencies : {};
477
- const development = isRecord(parsed.devDependencies) ? parsed.devDependencies : {};
477
+ if (!isRecord(parsed)) return {
478
+ runtime,
479
+ development
480
+ };
481
+ const runtimeRecord = isRecord(parsed.dependencies) ? parsed.dependencies : {};
482
+ const developmentRecord = isRecord(parsed.devDependencies) ? parsed.devDependencies : {};
478
483
  for (const name of Object.keys(blueprintToDevDependencies(blueprint)).sort()) {
479
484
  if (name.startsWith("@orkestrel/")) continue;
480
- const range = runtime[name] ?? development[name];
481
- if (isString(range)) dependencies.push({
485
+ const runtimeRange = runtimeRecord[name];
486
+ if (isString(runtimeRange)) {
487
+ runtime.push({
488
+ name,
489
+ range: runtimeRange
490
+ });
491
+ continue;
492
+ }
493
+ const developmentRange = developmentRecord[name];
494
+ if (isString(developmentRange)) development.push({
482
495
  name,
483
- range
496
+ range: developmentRange
484
497
  });
485
498
  }
486
- return dependencies;
499
+ return {
500
+ runtime,
501
+ development
502
+ };
487
503
  }
488
504
  /**
489
505
  * Keep only dependencies published by the Orkestrel fleet.
@@ -730,7 +746,11 @@ var CLI = class CLI {
730
746
  const plan = this.#compile(blueprint);
731
747
  const manifest = plan.artifacts.find((artifact) => artifact.path === "package.json" && artifact.origin !== "host");
732
748
  if (manifest === void 0 || manifest.origin === "host") throw new ScaffoldError("BLOCKED", "The compiled plan carries no replaceable manifest.");
733
- const versions = await this.#versions(dependenciesToFleet(manifestToDependencies(manifest.content)), command.offline === true);
749
+ const declarations = manifestToDependencies(manifest.content);
750
+ const versions = await this.#versions({
751
+ runtime: dependenciesToFleet(declarations.runtime),
752
+ development: dependenciesToFleet(declarations.development)
753
+ }, command.offline === true);
734
754
  this.#assertVersions(versions);
735
755
  const resolved = replacePlanRanges(plan, versions.pins);
736
756
  if (resolved === void 0) throw new ScaffoldError("BLOCKED", "The compiled plan ranges could not be replaced.");
@@ -760,9 +780,9 @@ var CLI = class CLI {
760
780
  const manifest = this.#manifest(target);
761
781
  const blueprint = this.#derive(target);
762
782
  const groups = this.#groups(command.groups);
763
- const declared = manifestToPlannedDependencies(manifest, blueprint);
783
+ const declared = manifestToWritableDependencies(manifest, blueprint);
764
784
  const versions = await this.#versions(declared, command.offline === true);
765
- const questions = [...this.#targetQuestions(target, blueprint), ...releasesToQuestions(versions.releases)];
785
+ const questions = [...this.#targetQuestions(target, blueprint, groups), ...releasesToQuestions(versions.releases)];
766
786
  const host = await this.#host(command.from, target, command.offline === true);
767
787
  try {
768
788
  const [measured] = this.#survey(host.materializer, blueprint, target, groups);
@@ -794,7 +814,7 @@ var CLI = class CLI {
794
814
  const target = command.target ?? ".";
795
815
  const groups = this.#groups(command.groups);
796
816
  const blueprint = this.#derive(target);
797
- this.#assertTarget(target, blueprint);
817
+ this.#assertTarget(target, blueprint, groups);
798
818
  const host = await this.#host(command.from, target, command.offline === true);
799
819
  try {
800
820
  const [audit, plan] = this.#survey(host.materializer, blueprint, target, groups);
@@ -806,10 +826,15 @@ var CLI = class CLI {
806
826
  else this.#present(audit);
807
827
  return 1;
808
828
  }
809
- const versions = await this.#versions(manifestToPlannedDependencies(this.#manifest(target), blueprint), command.offline === true);
829
+ const versions = await this.#versions(manifestToWritableDependencies(this.#manifest(target), blueprint), command.offline === true);
810
830
  this.#assertVersions(versions);
811
- const result = this.#merge(host.materializer.repair(plan, audit, target), host.materializer.declare(versions.pins, target));
812
- const [terminal] = this.#survey(host.materializer, this.#derive(target), target, groups);
831
+ const result = this.#merge(host.materializer.repair(plan, audit, target), host.materializer.declare({
832
+ pins: versions.pins,
833
+ scripts: blueprintToWritableScripts(blueprint)
834
+ }, target));
835
+ const terminalBlueprint = this.#derive(target);
836
+ const [measured] = this.#survey(host.materializer, terminalBlueprint, target, groups);
837
+ const terminal = this.#appendQuestions(measured, target, terminalBlueprint, groups);
813
838
  const outcome = {
814
839
  ...result,
815
840
  audit: terminal,
@@ -838,14 +863,22 @@ var CLI = class CLI {
838
863
  if (extra.length > 0) this.#warn(`Read the data root from ${String(host)}. The other ${String(extra.length)} local root${extra.length === 1 ? "" : "s"} named by --from reach nothing this run does.`);
839
864
  const previous = this.#previous(target);
840
865
  const fetched = await this.#fetch(target, command.all === true);
841
- const releases = this.#catalogReleases(dependenciesToFleet(manifestToDependencies(this.#manifest(target))), fetched.entries);
842
- const pins = this.#pin(releases);
866
+ const declarations = manifestToDependencies(this.#manifest(target));
867
+ const writable = {
868
+ runtime: dependenciesToFleet(declarations.runtime),
869
+ development: dependenciesToFleet(declarations.development)
870
+ };
871
+ const releases = this.#catalogReleases([...writable.runtime, ...writable.development], fetched.entries);
872
+ const pins = this.#pin(releases, writable);
843
873
  this.#assertFetched(fetched.entries, fetched.mirrors);
844
874
  const guides = fetched.mirrors.length === 0 ? void 0 : fetched.mirrors.some((mirror) => mirror.lookup === "failed") ? "floor" : "live";
845
875
  const materializer = new Materializer({ host: host ?? readHostFloor() });
846
876
  let result;
847
877
  try {
848
- result = this.#merge(this.#publish(materializer, target, fetched.entries, fetched.mirrors), materializer.declare(pins, target));
878
+ result = this.#merge(this.#publish(materializer, target, fetched.entries, fetched.mirrors), materializer.declare({
879
+ pins,
880
+ scripts: []
881
+ }, target));
849
882
  } finally {
850
883
  materializer.destroy();
851
884
  }
@@ -868,7 +901,7 @@ var CLI = class CLI {
868
901
  const target = command.target ?? ".";
869
902
  const groups = this.#groups(command.groups);
870
903
  const blueprint = this.#derive(target);
871
- this.#assertTarget(target, blueprint);
904
+ this.#assertTarget(target, blueprint, groups);
872
905
  const worktree = this.#worktree(target);
873
906
  if (worktree.dirty.length > 0 && command.dirty !== true) throw new ScaffoldError("TARGET", `The target at ${target} carries ${String(worktree.dirty.length)} uncommitted change${worktree.dirty.length === 1 ? "" : "s"}. Commit them, or pass --dirty to waive the refusal.`, {
874
907
  target,
@@ -885,7 +918,10 @@ var CLI = class CLI {
885
918
  else this.#present(audit);
886
919
  return 1;
887
920
  }
888
- const declared = manifestToPlannedDependencies(this.#manifest(target), blueprint);
921
+ const declared = {
922
+ pins: manifestToWritableDependencies(this.#manifest(target), blueprint),
923
+ scripts: blueprintToWritableScripts(blueprint)
924
+ };
889
925
  const repaired = host.materializer.repair(plan, audit, target);
890
926
  const removed = host.materializer.remove(plan, audit, command.dirty === true ? {
891
927
  tracked: worktree.tracked,
@@ -893,7 +929,8 @@ var CLI = class CLI {
893
929
  } : worktree, target);
894
930
  const offline = this.#merge(repaired, removed);
895
931
  const online = command.offline === true ? await this.#offline(host.materializer, target, declared, host.baseline) : await this.#reconcile(host.materializer, target, declared, host.baseline, host.forced);
896
- const [terminal] = this.#survey(host.materializer, blueprint, target, groups);
932
+ const [measured] = this.#survey(host.materializer, blueprint, target, groups);
933
+ const terminal = this.#appendQuestions(measured, target, blueprint, groups);
897
934
  const outcome = {
898
935
  ...online,
899
936
  ...this.#merge(offline, online),
@@ -913,10 +950,13 @@ var CLI = class CLI {
913
950
  }
914
951
  }
915
952
  async #offline(materializer, target, declared, host) {
916
- const versions = await this.#versions(declared, true);
953
+ const versions = await this.#versions(declared.pins, true);
917
954
  this.#assertVersions(versions);
918
955
  return {
919
- ...materializer.declare(versions.pins, target),
956
+ ...materializer.declare({
957
+ pins: versions.pins,
958
+ scripts: declared.scripts
959
+ }, target),
920
960
  entries: [],
921
961
  mirrors: [],
922
962
  dropped: [],
@@ -933,7 +973,7 @@ var CLI = class CLI {
933
973
  let releases = [];
934
974
  let provenance = { ...host === void 0 ? {} : { host } };
935
975
  try {
936
- const versions = await this.#versions(declared, false);
976
+ const versions = await this.#versions(declared.pins, false);
937
977
  releases = versions.releases;
938
978
  provenance = {
939
979
  ...versions.baseline === void 0 ? {} : { versions: versions.baseline },
@@ -948,7 +988,10 @@ var CLI = class CLI {
948
988
  ...guides === void 0 ? {} : { guides },
949
989
  ...host === void 0 ? {} : { host }
950
990
  };
951
- const written = this.#merge(this.#publish(materializer, target, fetched.entries, fetched.mirrors), materializer.declare(versions.pins, target));
991
+ const written = this.#merge(this.#publish(materializer, target, fetched.entries, fetched.mirrors), materializer.declare({
992
+ pins: versions.pins,
993
+ scripts: declared.scripts
994
+ }, target));
952
995
  const floors = [];
953
996
  if (hostForced) floors.push("host");
954
997
  if (versions.forced) floors.push("versions");
@@ -1004,54 +1047,71 @@ var CLI = class CLI {
1004
1047
  }
1005
1048
  }
1006
1049
  async #versions(declared, offline) {
1007
- if (declared.length === 0) return {
1050
+ const dependencies = [...declared.runtime, ...declared.development];
1051
+ if (dependencies.length === 0) return {
1008
1052
  releases: [],
1009
- pins: [],
1053
+ pins: {
1054
+ runtime: [],
1055
+ development: []
1056
+ },
1010
1057
  forced: false,
1011
1058
  complete: true
1012
1059
  };
1013
- const floors = dependenciesToFloors(declared);
1060
+ const floors = dependenciesToFloors(dependencies);
1061
+ const floorPins = floors === void 0 ? void 0 : {
1062
+ runtime: floors.slice(0, declared.runtime.length).map((release) => ({
1063
+ name: release.name,
1064
+ range: release.range
1065
+ })),
1066
+ development: floors.slice(declared.runtime.length).map((release) => ({
1067
+ name: release.name,
1068
+ range: release.range
1069
+ }))
1070
+ };
1014
1071
  if (offline) {
1015
- if (floors === void 0) return {
1072
+ if (floors === void 0 || floorPins === void 0) return {
1016
1073
  releases: [],
1017
- pins: [],
1074
+ pins: {
1075
+ runtime: [],
1076
+ development: []
1077
+ },
1018
1078
  baseline: "floor",
1019
1079
  forced: false,
1020
1080
  complete: false
1021
1081
  };
1022
1082
  return {
1023
1083
  releases: floors,
1024
- pins: floors.map((release) => ({
1025
- name: release.name,
1026
- range: release.range
1027
- })),
1084
+ pins: floorPins,
1028
1085
  baseline: "floor",
1029
1086
  forced: false,
1030
1087
  complete: true
1031
1088
  };
1032
1089
  }
1033
- const releases = await this.#lookup(declared);
1090
+ const releases = await this.#lookup(dependencies);
1034
1091
  if (releases.filter((release) => release.lookup === "missing" || release.lookup === "unmatched").length > 0) return {
1035
1092
  releases,
1036
- pins: [],
1093
+ pins: {
1094
+ runtime: [],
1095
+ development: []
1096
+ },
1037
1097
  baseline: "live",
1038
1098
  forced: false,
1039
1099
  complete: false
1040
1100
  };
1041
1101
  if (releases.some((release) => release.lookup === "failed")) {
1042
- if (floors === void 0) return {
1102
+ if (floorPins === void 0) return {
1043
1103
  releases,
1044
- pins: [],
1104
+ pins: {
1105
+ runtime: [],
1106
+ development: []
1107
+ },
1045
1108
  baseline: "floor",
1046
1109
  forced: true,
1047
1110
  complete: false
1048
1111
  };
1049
1112
  return {
1050
1113
  releases,
1051
- pins: floors.map((release) => ({
1052
- name: release.name,
1053
- range: release.range
1054
- })),
1114
+ pins: floorPins,
1055
1115
  baseline: "floor",
1056
1116
  forced: true,
1057
1117
  complete: true
@@ -1059,7 +1119,7 @@ var CLI = class CLI {
1059
1119
  }
1060
1120
  return {
1061
1121
  releases,
1062
- pins: this.#pin(releases),
1122
+ pins: this.#pin(releases, declared),
1063
1123
  baseline: "live",
1064
1124
  forced: false,
1065
1125
  complete: true
@@ -1100,7 +1160,12 @@ var CLI = class CLI {
1100
1160
  async #fetch(target, all) {
1101
1161
  const manifest = this.#manifest(target);
1102
1162
  const own = manifestToName(manifest);
1103
- const declared = manifestToDependencies(manifest).map((dependency) => dependency.name);
1163
+ const dependencies = manifestToDependencies(manifest);
1164
+ const declared = [...new Set([
1165
+ ...dependencies.runtime,
1166
+ ...dependencies.development,
1167
+ ...dependencies.peer
1168
+ ].map((dependency) => dependency.name))];
1104
1169
  const upstream = new Upstream(this.#upstream);
1105
1170
  try {
1106
1171
  const entries = await upstream.catalog();
@@ -1136,16 +1201,26 @@ var CLI = class CLI {
1136
1201
  if (failed.length === 0) return;
1137
1202
  throw new ScaffoldError("FETCH", `Upstream produced no complete catalog answer for ${failed.join(", ")}.`, { names: failed.length });
1138
1203
  }
1139
- #pin(releases) {
1204
+ #pin(releases, declared) {
1140
1205
  const refused = releases.filter((release) => release.lookup !== "found");
1141
1206
  if (refused.length > 0) throw new ScaffoldError("FETCH", `The registry named no release for ${refused.map((release) => release.name).join(", ")}.`, { names: refused.length });
1142
- return releases.map((release) => {
1207
+ const dependencies = [...declared.runtime, ...declared.development];
1208
+ const pins = [];
1209
+ for (let index = 0; index < releases.length; index += 1) {
1210
+ const release = releases[index];
1211
+ const dependency = dependencies[index];
1212
+ if (release === void 0 || dependency === void 0) throw new ScaffoldError("FETCH", "The release answer has no matching declaration.");
1143
1213
  if (release.lookup !== "found") throw new ScaffoldError("FETCH", `The registry named no release for ${release.name}.`);
1144
- return {
1145
- name: release.name,
1214
+ pins.push({
1215
+ name: dependency.name,
1146
1216
  range: `^${release.latest}`
1147
- };
1148
- });
1217
+ });
1218
+ }
1219
+ if (pins.length !== dependencies.length) throw new ScaffoldError("FETCH", "The release answer does not match the declaration set.");
1220
+ return {
1221
+ runtime: pins.slice(0, declared.runtime.length),
1222
+ development: pins.slice(declared.runtime.length)
1223
+ };
1149
1224
  }
1150
1225
  #compile(blueprint, groups) {
1151
1226
  const compiler = new Compiler();
@@ -1178,7 +1253,6 @@ var CLI = class CLI {
1178
1253
  const bin = resolveContainedPath(target, BIN_ENTRY_PATH);
1179
1254
  const tests = resolveContainedPath(target, "tests");
1180
1255
  const guides = resolveContainedPath(target, GUIDES_TEST_PATH);
1181
- const distribution = resolveContainedPath(target, DISTRIBUTION_TEST_PATH);
1182
1256
  const integration = resolveContainedPath(target, INTEGRATION_TEST_PATH);
1183
1257
  const conformance = resolveContainedPath(target, CONFORMANCE_TEST_PATH);
1184
1258
  const service = resolveContainedPath(target, SERVICE_SETUP_PATH);
@@ -1187,7 +1261,7 @@ var CLI = class CLI {
1187
1261
  return createBlueprint(declared.slice(declared.lastIndexOf("/") + 1), {
1188
1262
  src: this.#probe(target, "src"),
1189
1263
  app: this.#probe(target, "app"),
1190
- dependencies: manifestToDependencies(manifest),
1264
+ dependencies: manifestToDependencies(manifest).runtime,
1191
1265
  bin: bin !== void 0 && isExactCaseFile(bin),
1192
1266
  setup: tests !== void 0 && listFiles(tests).some((path) => {
1193
1267
  if (path.includes("/") || !path.startsWith("setup") || !path.endsWith(".test.ts")) return false;
@@ -1195,7 +1269,6 @@ var CLI = class CLI {
1195
1269
  return proof !== void 0 && isExactCaseFile(proof);
1196
1270
  }),
1197
1271
  guides: guides !== void 0 && isExactCaseFile(guides),
1198
- distribution: distribution !== void 0 && isExactCaseFile(distribution),
1199
1272
  integration: integration !== void 0 && isExactCaseFile(integration),
1200
1273
  conformance: conformance !== void 0 && isExactCaseFile(conformance),
1201
1274
  service: service !== void 0 && isExactCaseFile(service),
@@ -1326,7 +1399,8 @@ var CLI = class CLI {
1326
1399
  };
1327
1400
  }
1328
1401
  #projectQuestion(target, blueprint, writing = false) {
1329
- const manifest = this.#manifest(target);
1402
+ const text = this.#manifest(target);
1403
+ const manifest = replaceManifestScripts(text, blueprintToWritableScripts(blueprint)) ?? text;
1330
1404
  const planned = blueprintToRootVite(blueprint);
1331
1405
  const parsed = parseJSON(manifest);
1332
1406
  const scripts = isRecord(parsed) && isRecord(parsed.scripts) ? parsed.scripts : {};
@@ -1346,13 +1420,15 @@ var CLI = class CLI {
1346
1420
  const projects = [...absent].sort();
1347
1421
  if (unresolved) return {
1348
1422
  field: "projects",
1349
- message: `The manifest at ${target} contains a Vitest project expression that cannot be resolved statically.${projects.length === 0 ? "" : ` It also names projects the planned configuration does not register: ${projects.join(", ")}.`} ${writing ? "Replace it with a literal --project value or remove the script before using a scaffold writing verb." : "Replace it with a literal --project value before relying on the planned configuration."}`,
1350
- blocking: false
1423
+ message: `The manifest at ${target} contains a Vitest project expression that cannot be resolved statically.${projects.length === 0 ? "" : ` It also names projects the planned vite.config.ts does not register: ${projects.join(", ")}.`} ${writing ? "The configs group is blocked. Replace the expression with a literal --project value before selecting configs, or exclude configs from --groups." : "Replace it with a literal --project value before relying on the planned configuration."}`,
1424
+ blocking: false,
1425
+ groups: ["configs"]
1351
1426
  };
1352
1427
  if (projects.length > 0) return {
1353
1428
  field: "projects",
1354
- message: writing ? `The manifest at ${target} names ${projects.length === 1 ? "a Vitest project" : "Vitest projects"} the planned configuration does not register: ${projects.join(", ")}. To continue, remove the ${projects.length === 1 ? "script that names it" : "scripts that name them"} or do not use scaffold writing verbs on a workspace that needs ${projects.length === 1 ? "a custom Vitest project" : "custom Vitest projects"}.` : `The manifest at ${target} names ${projects.length === 1 ? "a Vitest project" : "Vitest projects"} the planned configuration does not register: ${projects.join(", ")}. Add ${projects.length === 1 ? "the project" : "each project"} to vite.config.ts or remove the ${projects.length === 1 ? "script that names it" : "scripts that name them"}.`,
1355
- blocking: false
1429
+ message: writing ? `The configs group is blocked because the manifest at ${target} names ${projects.length === 1 ? "a Vitest project" : "Vitest projects"} the planned vite.config.ts does not register: ${projects.join(", ")}. Remove the ${projects.length === 1 ? "script that names it" : "scripts that name them"} before selecting configs, or exclude configs from --groups.` : `The manifest at ${target} names ${projects.length === 1 ? "a Vitest project" : "Vitest projects"} the planned configuration does not register: ${projects.join(", ")}. Add ${projects.length === 1 ? "the project" : "each project"} to vite.config.ts or remove the ${projects.length === 1 ? "script that names it" : "scripts that name them"}.`,
1430
+ blocking: false,
1431
+ groups: ["configs"]
1356
1432
  };
1357
1433
  const expected = blueprintToScripts(blueprint);
1358
1434
  const expectedLines = /* @__PURE__ */ new Map();
@@ -1386,27 +1462,44 @@ var CLI = class CLI {
1386
1462
  }
1387
1463
  if (unresolved) return {
1388
1464
  field: "projects",
1389
- message: `The manifest at ${target} contains a ${gateNames} chain that cannot be resolved statically. ${writing ? "Replace it with literal npm run script names before using a scaffold writing verb." : "Replace it with literal npm run script names before relying on the planned configuration."}`,
1390
- blocking: false
1465
+ message: `The manifest at ${target} contains a ${gateNames} chain that cannot be resolved statically. ${writing ? "The configs group is blocked. Replace it with literal npm run script names before selecting configs, or exclude configs from --groups." : "Replace it with literal npm run script names before relying on the planned configuration."}`,
1466
+ blocking: false,
1467
+ groups: ["configs"]
1391
1468
  };
1392
- const missing = [...expectedLines].filter(([project]) => !reachable.has(project)).sort(([left], [right]) => left.localeCompare(right));
1393
- if (missing.length === 0) return void 0;
1394
- const names = missing.map(([project]) => project);
1395
- const unscripted = missing.filter(([project]) => !isString(scripts[`test:${project}`]));
1396
- const ungated = missing.filter(([project]) => isString(scripts[`test:${project}`]));
1397
- const remedies = [];
1398
- if (ungated.length > 0) {
1399
- const declared = ungated.map(([project]) => `test:${project}`);
1400
- remedies.push(`${declared.join(", ")} ${declared.length === 1 ? "is" : "are"} already declared, so the gate is missing rather than the script: invoke ${declared.length === 1 ? "it" : "each of them"} by name from the ${gateNames} chain.`);
1401
- }
1402
- if (unscripted.length > 0) {
1403
- const lines = unscripted.map(([, line]) => line);
1404
- remedies.push(`${writing ? "To continue, add" : "Add"} ${lines.length === 1 ? "this exact script line" : "these exact script lines"} to package.json: ${lines.join(" ")}`);
1405
- }
1469
+ const ungated = [...expectedLines].filter(([project]) => !reachable.has(project)).sort(([left], [right]) => left.localeCompare(right)).filter(([project]) => isString(scripts[`test:${project}`]));
1470
+ if (ungated.length === 0) return void 0;
1471
+ const names = ungated.map(([project]) => project);
1472
+ const declared = ungated.map(([project]) => `test:${project}`);
1406
1473
  return {
1407
1474
  field: "projects",
1408
- message: `The manifest at ${target} does not reach ${names.length === 1 ? "a Vitest project" : "Vitest projects"} the planned configuration registers: ${names.join(", ")}. No chain from ${gateNames} invokes ${names.length === 1 ? "it" : "them"}. ${remedies.join(" ")}`,
1409
- blocking: false
1475
+ message: `${writing ? "The configs group is blocked because " : ""}the manifest at ${target} does not reach ${names.length === 1 ? "a Vitest project" : "Vitest projects"} the planned configuration registers: ${names.join(", ")}. No chain from ${gateNames} invokes ${names.length === 1 ? "it" : "them"}. ${declared.join(", ")} ${declared.length === 1 ? "is" : "are"} already declared, so the gate is missing rather than the script: invoke ${declared.length === 1 ? "it" : "each of them"} by name from the ${gateNames} chain.${writing ? " Exclude configs from --groups to write another group." : ""}`,
1476
+ blocking: false,
1477
+ groups: ["configs"]
1478
+ };
1479
+ }
1480
+ #scriptQuestion(target, blueprint) {
1481
+ const text = this.#manifest(target);
1482
+ const writable = blueprintToWritableScripts(blueprint);
1483
+ const parsed = parseJSON(text);
1484
+ const scripts = isRecord(parsed) && isRecord(parsed.scripts) ? parsed.scripts : {};
1485
+ const missing = writable.filter((script) => !Object.hasOwn(scripts, script.name));
1486
+ const differing = writable.filter((script) => {
1487
+ const value = scripts[script.name];
1488
+ return isString(value) && value !== script.command && !script.accepted.includes(value);
1489
+ });
1490
+ if (missing.length === 0 && differing.length === 0) return void 0;
1491
+ const missingNames = missing.map((script) => script.name);
1492
+ const missingLines = missing.map((script) => `${JSON.stringify(script.name)}: ${JSON.stringify(script.command)},`);
1493
+ const differingNames = differing.map((script) => script.name);
1494
+ const differingLines = differing.map((script) => {
1495
+ const value = scripts[script.name];
1496
+ return `${JSON.stringify(script.name)} declares ${JSON.stringify(value)}; planned ${JSON.stringify(script.command)}.`;
1497
+ });
1498
+ return {
1499
+ field: "scripts",
1500
+ message: [missing.length === 0 ? "" : `The manifest at ${target} does not declare ${missingNames.length === 1 ? "a planned script" : "planned scripts"}: ${missingNames.join(", ")}. Add ${missingLines.length === 1 ? "this exact script line" : "these exact script lines"} to package.json: ${missingLines.join(" ")}`, differing.length === 0 ? "" : `The manifest at ${target} declares ${differingNames.length === 1 ? "a planned script" : "planned scripts"} with ${differingNames.length === 1 ? "a differing value" : "differing values"}: ${differingNames.join(", ")}. Keep ${differingNames.length === 1 ? "the declared value" : "each declared value"} unchanged or replace ${differingNames.length === 1 ? "it" : "them"} with the planned ${differingNames.length === 1 ? "value" : "values"}: ${differingLines.join(" ")}`].filter((message) => message !== "").join(" "),
1501
+ blocking: false,
1502
+ groups: ["manifest"]
1410
1503
  };
1411
1504
  }
1412
1505
  #dependencyQuestion(target, blueprint, writing = false) {
@@ -1415,8 +1508,9 @@ var CLI = class CLI {
1415
1508
  const malformed = ["dependencies", "devDependencies"].filter((section) => parsed[section] !== void 0 && !isRecord(parsed[section]));
1416
1509
  if (malformed.length > 0) return {
1417
1510
  field: "dependencies",
1418
- message: `The manifest at ${target} declares ${malformed.join(" and ")} as ${malformed.length === 1 ? "a value" : "values"} that ${malformed.length === 1 ? "is" : "are"} not ${malformed.length === 1 ? "an object" : "objects"}. Replace ${malformed.length === 1 ? "it" : "them"} with ${malformed.length === 1 ? "an object" : "objects"} before ${writing ? "using a scaffold writing verb" : "relying on the planned dependency set"}.`,
1419
- blocking: false
1511
+ message: `The manifest at ${target} declares ${malformed.join(" and ")} as ${malformed.length === 1 ? "a value" : "values"} that ${malformed.length === 1 ? "is" : "are"} not ${malformed.length === 1 ? "an object" : "objects"}. ${writing ? `The configs and tests groups are blocked. Replace ${malformed.length === 1 ? "it" : "them"} with ${malformed.length === 1 ? "an object" : "objects"} before selecting configs or tests, or exclude those groups from --groups.` : `Replace ${malformed.length === 1 ? "it" : "them"} with ${malformed.length === 1 ? "an object" : "objects"} before relying on the planned dependency set.`}`,
1512
+ blocking: false,
1513
+ groups: ["configs", "tests"]
1420
1514
  };
1421
1515
  const dependencies = isRecord(parsed.dependencies) ? parsed.dependencies : {};
1422
1516
  const development = isRecord(parsed.devDependencies) ? parsed.devDependencies : {};
@@ -1426,20 +1520,67 @@ var CLI = class CLI {
1426
1520
  const lines = missing.map(([name, range]) => `${JSON.stringify(name)}: ${JSON.stringify(range)},`);
1427
1521
  return {
1428
1522
  field: "dependencies",
1429
- message: `The manifest at ${target} does not declare ${names.length === 1 ? "a planned dependency" : "planned dependencies"}: ${names.join(", ")}. ${writing ? "To continue, add" : "Add"} ${lines.length === 1 ? "this exact dependency line" : "these exact dependency lines"} to dependencies or devDependencies in package.json: ${lines.join(" ")}`,
1430
- blocking: false
1523
+ message: `The manifest at ${target} does not declare ${names.length === 1 ? "a planned dependency" : "planned dependencies"}: ${names.join(", ")}. ${writing ? "The configs and tests groups are blocked. Add" : "Add"} ${lines.length === 1 ? "this exact dependency line" : "these exact dependency lines"} to dependencies or devDependencies in package.json: ${lines.join(" ")}${writing ? " Add the dependency before selecting configs or tests, or exclude those groups from --groups." : ""}`,
1524
+ blocking: false,
1525
+ groups: ["configs", "tests"]
1526
+ };
1527
+ }
1528
+ #setupQuestion(target, blueprint) {
1529
+ const tests = resolveContainedPath(target, "tests");
1530
+ if (tests === void 0) return void 0;
1531
+ const entries = listFiles(tests).filter((path) => !path.includes("/"));
1532
+ const proofs = new Set(entries.filter((path) => path.endsWith(".test.ts")));
1533
+ const seeds = new Map(blueprintToTestArtifacts(blueprint).map(({ path, content }) => [path, content.trim()]));
1534
+ const modules = entries.filter((path) => {
1535
+ if (!path.startsWith("setup") || !path.endsWith(".ts")) return false;
1536
+ if (path.endsWith(".test.ts") || HOST_PATHS.includes(`tests/${path}`)) return false;
1537
+ if (proofs.has(`${path.slice(0, -3)}.test.ts`)) return false;
1538
+ if (resolveContainedPath(tests, path) === void 0) return false;
1539
+ const content = (readFileText(tests, path) ?? "").trim();
1540
+ return content !== "" && content !== (seeds.get(`tests/${path}`) ?? "");
1541
+ }).sort();
1542
+ if (modules.length === 0) return void 0;
1543
+ const single = modules.length === 1;
1544
+ const named = modules.map((path) => `tests/${path}`).join(", ");
1545
+ const remedies = modules.map((path) => `tests/${path.slice(0, -3)}.test.ts`).join(", ");
1546
+ const remedy = single ? `Add ${remedies} to cover it.` : `Add ${remedies}, each covering the module of the same name.`;
1547
+ return {
1548
+ field: "setup",
1549
+ message: `The target at ${target} carries ${single ? "a test setup module" : "test setup modules"} that no proof covers: ${named}. ${remedy} The proof's subject is behavior only this workspace can assert, so scaffold does not write it.`,
1550
+ blocking: false,
1551
+ groups: ["tests"]
1431
1552
  };
1432
1553
  }
1433
- #targetQuestions(target, blueprint, writing = false) {
1554
+ #targetQuestions(target, blueprint, groups, writing = false) {
1434
1555
  const questions = [];
1556
+ if (!writing) {
1557
+ const script = this.#scriptQuestion(target, blueprint);
1558
+ if (script !== void 0) questions.push(script);
1559
+ }
1435
1560
  const project = this.#projectQuestion(target, blueprint, writing);
1436
1561
  if (project !== void 0) questions.push(project);
1437
1562
  const dependency = this.#dependencyQuestion(target, blueprint, writing);
1438
1563
  if (dependency !== void 0) questions.push(dependency);
1439
- return questions;
1564
+ if (!writing) {
1565
+ const setup = this.#setupQuestion(target, blueprint);
1566
+ if (setup !== void 0) questions.push(setup);
1567
+ }
1568
+ return questions.filter((question) => groups === void 0 || question.groups.some((group) => groups.includes(group))).map(({ field, message, blocking, candidates }) => ({
1569
+ field,
1570
+ message,
1571
+ blocking,
1572
+ ...candidates === void 0 ? {} : { candidates }
1573
+ }));
1574
+ }
1575
+ #appendQuestions(audit, target, blueprint, groups) {
1576
+ const questions = this.#targetQuestions(target, blueprint, groups);
1577
+ return questions.length === 0 ? audit : {
1578
+ ...audit,
1579
+ questions: [...audit.questions, ...questions]
1580
+ };
1440
1581
  }
1441
- #assertTarget(target, blueprint) {
1442
- const questions = this.#targetQuestions(target, blueprint, true);
1582
+ #assertTarget(target, blueprint, groups) {
1583
+ const questions = this.#targetQuestions(target, blueprint, groups, true);
1443
1584
  if (questions.length === 0) return;
1444
1585
  throw new ScaffoldError("TARGET", questions.map((question) => question.message).join(" "), { target });
1445
1586
  }