@orkestrel/scaffold 0.0.49 → 0.0.50

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,9 +826,12 @@ 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));
831
+ const result = this.#merge(host.materializer.repair(plan, audit, target), host.materializer.declare({
832
+ pins: versions.pins,
833
+ scripts: blueprintToWritableScripts(blueprint)
834
+ }, target));
812
835
  const [terminal] = this.#survey(host.materializer, this.#derive(target), target, groups);
813
836
  const outcome = {
814
837
  ...result,
@@ -838,14 +861,22 @@ var CLI = class CLI {
838
861
  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
862
  const previous = this.#previous(target);
840
863
  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);
864
+ const declarations = manifestToDependencies(this.#manifest(target));
865
+ const writable = {
866
+ runtime: dependenciesToFleet(declarations.runtime),
867
+ development: dependenciesToFleet(declarations.development)
868
+ };
869
+ const releases = this.#catalogReleases([...writable.runtime, ...writable.development], fetched.entries);
870
+ const pins = this.#pin(releases, writable);
843
871
  this.#assertFetched(fetched.entries, fetched.mirrors);
844
872
  const guides = fetched.mirrors.length === 0 ? void 0 : fetched.mirrors.some((mirror) => mirror.lookup === "failed") ? "floor" : "live";
845
873
  const materializer = new Materializer({ host: host ?? readHostFloor() });
846
874
  let result;
847
875
  try {
848
- result = this.#merge(this.#publish(materializer, target, fetched.entries, fetched.mirrors), materializer.declare(pins, target));
876
+ result = this.#merge(this.#publish(materializer, target, fetched.entries, fetched.mirrors), materializer.declare({
877
+ pins,
878
+ scripts: []
879
+ }, target));
849
880
  } finally {
850
881
  materializer.destroy();
851
882
  }
@@ -868,7 +899,7 @@ var CLI = class CLI {
868
899
  const target = command.target ?? ".";
869
900
  const groups = this.#groups(command.groups);
870
901
  const blueprint = this.#derive(target);
871
- this.#assertTarget(target, blueprint);
902
+ this.#assertTarget(target, blueprint, groups);
872
903
  const worktree = this.#worktree(target);
873
904
  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
905
  target,
@@ -885,7 +916,10 @@ var CLI = class CLI {
885
916
  else this.#present(audit);
886
917
  return 1;
887
918
  }
888
- const declared = manifestToPlannedDependencies(this.#manifest(target), blueprint);
919
+ const declared = {
920
+ pins: manifestToWritableDependencies(this.#manifest(target), blueprint),
921
+ scripts: blueprintToWritableScripts(blueprint)
922
+ };
889
923
  const repaired = host.materializer.repair(plan, audit, target);
890
924
  const removed = host.materializer.remove(plan, audit, command.dirty === true ? {
891
925
  tracked: worktree.tracked,
@@ -913,10 +947,13 @@ var CLI = class CLI {
913
947
  }
914
948
  }
915
949
  async #offline(materializer, target, declared, host) {
916
- const versions = await this.#versions(declared, true);
950
+ const versions = await this.#versions(declared.pins, true);
917
951
  this.#assertVersions(versions);
918
952
  return {
919
- ...materializer.declare(versions.pins, target),
953
+ ...materializer.declare({
954
+ pins: versions.pins,
955
+ scripts: declared.scripts
956
+ }, target),
920
957
  entries: [],
921
958
  mirrors: [],
922
959
  dropped: [],
@@ -933,7 +970,7 @@ var CLI = class CLI {
933
970
  let releases = [];
934
971
  let provenance = { ...host === void 0 ? {} : { host } };
935
972
  try {
936
- const versions = await this.#versions(declared, false);
973
+ const versions = await this.#versions(declared.pins, false);
937
974
  releases = versions.releases;
938
975
  provenance = {
939
976
  ...versions.baseline === void 0 ? {} : { versions: versions.baseline },
@@ -948,7 +985,10 @@ var CLI = class CLI {
948
985
  ...guides === void 0 ? {} : { guides },
949
986
  ...host === void 0 ? {} : { host }
950
987
  };
951
- const written = this.#merge(this.#publish(materializer, target, fetched.entries, fetched.mirrors), materializer.declare(versions.pins, target));
988
+ const written = this.#merge(this.#publish(materializer, target, fetched.entries, fetched.mirrors), materializer.declare({
989
+ pins: versions.pins,
990
+ scripts: declared.scripts
991
+ }, target));
952
992
  const floors = [];
953
993
  if (hostForced) floors.push("host");
954
994
  if (versions.forced) floors.push("versions");
@@ -1004,54 +1044,71 @@ var CLI = class CLI {
1004
1044
  }
1005
1045
  }
1006
1046
  async #versions(declared, offline) {
1007
- if (declared.length === 0) return {
1047
+ const dependencies = [...declared.runtime, ...declared.development];
1048
+ if (dependencies.length === 0) return {
1008
1049
  releases: [],
1009
- pins: [],
1050
+ pins: {
1051
+ runtime: [],
1052
+ development: []
1053
+ },
1010
1054
  forced: false,
1011
1055
  complete: true
1012
1056
  };
1013
- const floors = dependenciesToFloors(declared);
1057
+ const floors = dependenciesToFloors(dependencies);
1058
+ const floorPins = floors === void 0 ? void 0 : {
1059
+ runtime: floors.slice(0, declared.runtime.length).map((release) => ({
1060
+ name: release.name,
1061
+ range: release.range
1062
+ })),
1063
+ development: floors.slice(declared.runtime.length).map((release) => ({
1064
+ name: release.name,
1065
+ range: release.range
1066
+ }))
1067
+ };
1014
1068
  if (offline) {
1015
- if (floors === void 0) return {
1069
+ if (floors === void 0 || floorPins === void 0) return {
1016
1070
  releases: [],
1017
- pins: [],
1071
+ pins: {
1072
+ runtime: [],
1073
+ development: []
1074
+ },
1018
1075
  baseline: "floor",
1019
1076
  forced: false,
1020
1077
  complete: false
1021
1078
  };
1022
1079
  return {
1023
1080
  releases: floors,
1024
- pins: floors.map((release) => ({
1025
- name: release.name,
1026
- range: release.range
1027
- })),
1081
+ pins: floorPins,
1028
1082
  baseline: "floor",
1029
1083
  forced: false,
1030
1084
  complete: true
1031
1085
  };
1032
1086
  }
1033
- const releases = await this.#lookup(declared);
1087
+ const releases = await this.#lookup(dependencies);
1034
1088
  if (releases.filter((release) => release.lookup === "missing" || release.lookup === "unmatched").length > 0) return {
1035
1089
  releases,
1036
- pins: [],
1090
+ pins: {
1091
+ runtime: [],
1092
+ development: []
1093
+ },
1037
1094
  baseline: "live",
1038
1095
  forced: false,
1039
1096
  complete: false
1040
1097
  };
1041
1098
  if (releases.some((release) => release.lookup === "failed")) {
1042
- if (floors === void 0) return {
1099
+ if (floorPins === void 0) return {
1043
1100
  releases,
1044
- pins: [],
1101
+ pins: {
1102
+ runtime: [],
1103
+ development: []
1104
+ },
1045
1105
  baseline: "floor",
1046
1106
  forced: true,
1047
1107
  complete: false
1048
1108
  };
1049
1109
  return {
1050
1110
  releases,
1051
- pins: floors.map((release) => ({
1052
- name: release.name,
1053
- range: release.range
1054
- })),
1111
+ pins: floorPins,
1055
1112
  baseline: "floor",
1056
1113
  forced: true,
1057
1114
  complete: true
@@ -1059,7 +1116,7 @@ var CLI = class CLI {
1059
1116
  }
1060
1117
  return {
1061
1118
  releases,
1062
- pins: this.#pin(releases),
1119
+ pins: this.#pin(releases, declared),
1063
1120
  baseline: "live",
1064
1121
  forced: false,
1065
1122
  complete: true
@@ -1100,7 +1157,12 @@ var CLI = class CLI {
1100
1157
  async #fetch(target, all) {
1101
1158
  const manifest = this.#manifest(target);
1102
1159
  const own = manifestToName(manifest);
1103
- const declared = manifestToDependencies(manifest).map((dependency) => dependency.name);
1160
+ const dependencies = manifestToDependencies(manifest);
1161
+ const declared = [...new Set([
1162
+ ...dependencies.runtime,
1163
+ ...dependencies.development,
1164
+ ...dependencies.peer
1165
+ ].map((dependency) => dependency.name))];
1104
1166
  const upstream = new Upstream(this.#upstream);
1105
1167
  try {
1106
1168
  const entries = await upstream.catalog();
@@ -1136,16 +1198,26 @@ var CLI = class CLI {
1136
1198
  if (failed.length === 0) return;
1137
1199
  throw new ScaffoldError("FETCH", `Upstream produced no complete catalog answer for ${failed.join(", ")}.`, { names: failed.length });
1138
1200
  }
1139
- #pin(releases) {
1201
+ #pin(releases, declared) {
1140
1202
  const refused = releases.filter((release) => release.lookup !== "found");
1141
1203
  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) => {
1204
+ const dependencies = [...declared.runtime, ...declared.development];
1205
+ const pins = [];
1206
+ for (let index = 0; index < releases.length; index += 1) {
1207
+ const release = releases[index];
1208
+ const dependency = dependencies[index];
1209
+ if (release === void 0 || dependency === void 0) throw new ScaffoldError("FETCH", "The release answer has no matching declaration.");
1143
1210
  if (release.lookup !== "found") throw new ScaffoldError("FETCH", `The registry named no release for ${release.name}.`);
1144
- return {
1145
- name: release.name,
1211
+ pins.push({
1212
+ name: dependency.name,
1146
1213
  range: `^${release.latest}`
1147
- };
1148
- });
1214
+ });
1215
+ }
1216
+ if (pins.length !== dependencies.length) throw new ScaffoldError("FETCH", "The release answer does not match the declaration set.");
1217
+ return {
1218
+ runtime: pins.slice(0, declared.runtime.length),
1219
+ development: pins.slice(declared.runtime.length)
1220
+ };
1149
1221
  }
1150
1222
  #compile(blueprint, groups) {
1151
1223
  const compiler = new Compiler();
@@ -1178,7 +1250,6 @@ var CLI = class CLI {
1178
1250
  const bin = resolveContainedPath(target, BIN_ENTRY_PATH);
1179
1251
  const tests = resolveContainedPath(target, "tests");
1180
1252
  const guides = resolveContainedPath(target, GUIDES_TEST_PATH);
1181
- const distribution = resolveContainedPath(target, DISTRIBUTION_TEST_PATH);
1182
1253
  const integration = resolveContainedPath(target, INTEGRATION_TEST_PATH);
1183
1254
  const conformance = resolveContainedPath(target, CONFORMANCE_TEST_PATH);
1184
1255
  const service = resolveContainedPath(target, SERVICE_SETUP_PATH);
@@ -1187,7 +1258,7 @@ var CLI = class CLI {
1187
1258
  return createBlueprint(declared.slice(declared.lastIndexOf("/") + 1), {
1188
1259
  src: this.#probe(target, "src"),
1189
1260
  app: this.#probe(target, "app"),
1190
- dependencies: manifestToDependencies(manifest),
1261
+ dependencies: manifestToDependencies(manifest).runtime,
1191
1262
  bin: bin !== void 0 && isExactCaseFile(bin),
1192
1263
  setup: tests !== void 0 && listFiles(tests).some((path) => {
1193
1264
  if (path.includes("/") || !path.startsWith("setup") || !path.endsWith(".test.ts")) return false;
@@ -1195,7 +1266,6 @@ var CLI = class CLI {
1195
1266
  return proof !== void 0 && isExactCaseFile(proof);
1196
1267
  }),
1197
1268
  guides: guides !== void 0 && isExactCaseFile(guides),
1198
- distribution: distribution !== void 0 && isExactCaseFile(distribution),
1199
1269
  integration: integration !== void 0 && isExactCaseFile(integration),
1200
1270
  conformance: conformance !== void 0 && isExactCaseFile(conformance),
1201
1271
  service: service !== void 0 && isExactCaseFile(service),
@@ -1326,7 +1396,8 @@ var CLI = class CLI {
1326
1396
  };
1327
1397
  }
1328
1398
  #projectQuestion(target, blueprint, writing = false) {
1329
- const manifest = this.#manifest(target);
1399
+ const text = this.#manifest(target);
1400
+ const manifest = replaceManifestScripts(text, blueprintToWritableScripts(blueprint)) ?? text;
1330
1401
  const planned = blueprintToRootVite(blueprint);
1331
1402
  const parsed = parseJSON(manifest);
1332
1403
  const scripts = isRecord(parsed) && isRecord(parsed.scripts) ? parsed.scripts : {};
@@ -1346,13 +1417,15 @@ var CLI = class CLI {
1346
1417
  const projects = [...absent].sort();
1347
1418
  if (unresolved) return {
1348
1419
  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
1420
+ 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."}`,
1421
+ blocking: false,
1422
+ groups: ["configs"]
1351
1423
  };
1352
1424
  if (projects.length > 0) return {
1353
1425
  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
1426
+ 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"}.`,
1427
+ blocking: false,
1428
+ groups: ["configs"]
1356
1429
  };
1357
1430
  const expected = blueprintToScripts(blueprint);
1358
1431
  const expectedLines = /* @__PURE__ */ new Map();
@@ -1386,8 +1459,9 @@ var CLI = class CLI {
1386
1459
  }
1387
1460
  if (unresolved) return {
1388
1461
  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
1462
+ 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."}`,
1463
+ blocking: false,
1464
+ groups: ["configs"]
1391
1465
  };
1392
1466
  const missing = [...expectedLines].filter(([project]) => !reachable.has(project)).sort(([left], [right]) => left.localeCompare(right));
1393
1467
  if (missing.length === 0) return void 0;
@@ -1405,8 +1479,9 @@ var CLI = class CLI {
1405
1479
  }
1406
1480
  return {
1407
1481
  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
1482
+ 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"}. ${remedies.join(" ")}${writing ? " Exclude configs from --groups to write another group." : ""}`,
1483
+ blocking: false,
1484
+ groups: ["configs"]
1410
1485
  };
1411
1486
  }
1412
1487
  #dependencyQuestion(target, blueprint, writing = false) {
@@ -1415,8 +1490,9 @@ var CLI = class CLI {
1415
1490
  const malformed = ["dependencies", "devDependencies"].filter((section) => parsed[section] !== void 0 && !isRecord(parsed[section]));
1416
1491
  if (malformed.length > 0) return {
1417
1492
  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
1493
+ 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.`}`,
1494
+ blocking: false,
1495
+ groups: ["configs", "tests"]
1420
1496
  };
1421
1497
  const dependencies = isRecord(parsed.dependencies) ? parsed.dependencies : {};
1422
1498
  const development = isRecord(parsed.devDependencies) ? parsed.devDependencies : {};
@@ -1426,20 +1502,56 @@ var CLI = class CLI {
1426
1502
  const lines = missing.map(([name, range]) => `${JSON.stringify(name)}: ${JSON.stringify(range)},`);
1427
1503
  return {
1428
1504
  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
1505
+ 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." : ""}`,
1506
+ blocking: false,
1507
+ groups: ["configs", "tests"]
1431
1508
  };
1432
1509
  }
1433
- #targetQuestions(target, blueprint, writing = false) {
1510
+ #setupQuestion(target, blueprint) {
1511
+ const tests = resolveContainedPath(target, "tests");
1512
+ if (tests === void 0) return void 0;
1513
+ const entries = listFiles(tests).filter((path) => !path.includes("/"));
1514
+ const proofs = new Set(entries.filter((path) => path.endsWith(".test.ts")));
1515
+ const seeds = new Map(blueprintToTestArtifacts(blueprint).map(({ path, content }) => [path, content.trim()]));
1516
+ const modules = entries.filter((path) => {
1517
+ if (!path.startsWith("setup") || !path.endsWith(".ts")) return false;
1518
+ if (path.endsWith(".test.ts") || HOST_PATHS.includes(`tests/${path}`)) return false;
1519
+ if (proofs.has(`${path.slice(0, -3)}.test.ts`)) return false;
1520
+ if (resolveContainedPath(tests, path) === void 0) return false;
1521
+ const content = (readFileText(tests, path) ?? "").trim();
1522
+ return content !== "" && content !== (seeds.get(`tests/${path}`) ?? "");
1523
+ }).sort();
1524
+ if (modules.length === 0) return void 0;
1525
+ const single = modules.length === 1;
1526
+ const named = modules.map((path) => `tests/${path}`).join(", ");
1527
+ const remedies = modules.map((path) => `tests/${path.slice(0, -3)}.test.ts`).join(", ");
1528
+ const remedy = single ? `Add ${remedies} to cover it.` : `Add ${remedies}, each covering the module of the same name.`;
1529
+ return {
1530
+ field: "setup",
1531
+ 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.`,
1532
+ blocking: false,
1533
+ groups: ["tests"]
1534
+ };
1535
+ }
1536
+ #targetQuestions(target, blueprint, groups, writing = false) {
1434
1537
  const questions = [];
1435
1538
  const project = this.#projectQuestion(target, blueprint, writing);
1436
1539
  if (project !== void 0) questions.push(project);
1437
1540
  const dependency = this.#dependencyQuestion(target, blueprint, writing);
1438
1541
  if (dependency !== void 0) questions.push(dependency);
1439
- return questions;
1542
+ if (!writing) {
1543
+ const setup = this.#setupQuestion(target, blueprint);
1544
+ if (setup !== void 0) questions.push(setup);
1545
+ }
1546
+ return questions.filter((question) => groups === void 0 || question.groups.some((group) => groups.includes(group))).map(({ field, message, blocking, candidates }) => ({
1547
+ field,
1548
+ message,
1549
+ blocking,
1550
+ ...candidates === void 0 ? {} : { candidates }
1551
+ }));
1440
1552
  }
1441
- #assertTarget(target, blueprint) {
1442
- const questions = this.#targetQuestions(target, blueprint, true);
1553
+ #assertTarget(target, blueprint, groups) {
1554
+ const questions = this.#targetQuestions(target, blueprint, groups, true);
1443
1555
  if (questions.length === 0) return;
1444
1556
  throw new ScaffoldError("TARGET", questions.map((question) => question.message).join(" "), { target });
1445
1557
  }