speccle 0.16.0 → 0.17.0

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
@@ -347,6 +347,13 @@ Joins three inputs into one number per **acceptance criterion**: the `SPEC.md` f
347
347
  criterion by carrying its `[KEY-n]` token anywhere in its full concatenated name, describe
348
348
  titles included.
349
349
 
350
+ This is the one command bound to a single **test dialect**. The join has to know which
351
+ tests covered each mutant — nothing else can credit a kill to a criterion — and only
352
+ StrykerJS produces that, so `strength` scores a `ts-vitest` repo and no other. On a
353
+ dialect it cannot score, `doctor` reports the stack as `not applicable` rather than
354
+ nagging toward `strength init`. Every other command — `lint`, `claims`, `verify`,
355
+ `risk` — is multi-language regardless.
356
+
350
357
  ```
351
358
  features/checkout/SPEC.md
352
359
  CHECKOUT-1 ████████████████████ 100.0% 14/14 Tax rounds half-up to 2dp per line item
package/dist/dialects.js CHANGED
@@ -2,6 +2,7 @@ const TS_TEST_FILE = /\.(test|spec)\.[cm]?[jt]sx?$/;
2
2
  const TS_TEST_TITLE = /\b(?:describe|it|test)(?:\.[\w.]+)*\s*\(\s*(["'`])((?:\\.|(?!\1).)*)\1/g;
3
3
  const TS_VITEST = {
4
4
  name: "ts-vitest",
5
+ supportsStrength: true,
5
6
  isTestFile: (file) => TS_TEST_FILE.test(file),
6
7
  readTestNames: (source) => [...source.matchAll(TS_TEST_TITLE)].map((m) => ({ name: m[2], spelling: "bracketed" })),
7
8
  };
@@ -15,6 +16,7 @@ const XCTEST_METHOD = /\bfunc\s+(test[A-Za-z0-9_]*)\s*\(/g;
15
16
  const SWIFT_ANNOTATED_DECL = /@(?:Test|Suite)\b(?!\s*\(\s*")(?:\s*\([^()]*\))?(?:\s+(?:@\w+|open|public|internal|fileprivate|private|final|static))*\s+(?:actor|class|enum|func|struct)\s+([A-Za-z_]\w*)/g;
16
17
  const SWIFT = {
17
18
  name: "swift",
19
+ supportsStrength: false,
18
20
  isTestFile: (file) => SWIFT_TEST_FILE.test(file) || (file.endsWith(".swift") && file.split("/").includes("Tests")),
19
21
  readTestNames(source) {
20
22
  const found = [...source.matchAll(SWIFT_DISPLAY_NAME)].map((m) => ({ at: m.index, name: m[1], spelling: "bracketed" }));
package/dist/doctor.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { access, readdir, readFile, stat } from "node:fs/promises";
2
2
  import { join, resolve } from "node:path";
3
3
  import { readConfig } from "./config.js";
4
+ import { DEFAULT_DIALECT, resolveDialect } from "./dialects.js";
4
5
  import { ownVersion, STRENGTH_DEPS, STRYKER_CONFIG_NAMES } from "./init.js";
5
6
  import { LENSES_DIR } from "./lenses.js";
6
7
  import { pinnedVersion, WORKFLOW_FILE } from "./reviewinit.js";
@@ -25,14 +26,17 @@ export async function doctor(target) {
25
26
  const workflow = await readMaybe(join(root, WORKFLOW_FILE));
26
27
  const driverPin = workflow === undefined ? null : (pinnedVersion(workflow) ?? null);
27
28
  const driverStatus = derivePayloadStatus(workflow !== undefined, driverPin, cli);
29
+ const dialect = config?.dialect ?? DEFAULT_DIALECT;
28
30
  const provisioned = await anyPresent(root, STRYKER_CONFIG_NAMES);
29
31
  const declared = await declaredDeps(root);
30
32
  const deps = STRENGTH_DEPS.map((spec) => checkDep(spec, declared));
31
- const stackStatus = !provisioned
32
- ? "absent"
33
- : deps.some((dep) => dep.status !== "ok")
34
- ? "drift"
35
- : "current";
33
+ const stackStatus = !scorable(config, dialect)
34
+ ? "unsupported"
35
+ : !provisioned
36
+ ? "absent"
37
+ : deps.some((dep) => dep.status !== "ok")
38
+ ? "drift"
39
+ : "current";
36
40
  const current = (status) => status === "current" || status === "absent";
37
41
  const ok = current(skillsStatus) &&
38
42
  current(lensesStatus) &&
@@ -44,10 +48,20 @@ export async function doctor(target) {
44
48
  skills: { recorded: skillsRecorded, bundled: cli, status: skillsStatus },
45
49
  lenses: { recorded: lensesRecorded, bundled: cli, status: lensesStatus },
46
50
  driver: { recorded: driverPin, bundled: cli, status: driverStatus },
47
- stack: { provisioned, deps, status: stackStatus },
51
+ stack: { dialect, provisioned, deps, status: stackStatus },
48
52
  ok,
49
53
  };
50
54
  }
55
+ /**
56
+ * Whether any dialect in force in this repo can be scored. A mixed tree keeps its stack: an
57
+ * override may put one subtree on ts-vitest under a swift default (ADR-0040), and that
58
+ * subtree's heatmap is real. Only a repo with no scorable dialect anywhere has nothing to
59
+ * provision — reporting that as `absent` would nag it toward a stack it can never run.
60
+ */
61
+ function scorable(config, dialect) {
62
+ const inForce = [dialect, ...(config?.overrides ?? []).map((o) => o.dialect ?? dialect)];
63
+ return inForce.some((name) => resolveDialect(name).supportsStrength);
64
+ }
51
65
  function derivePayloadStatus(present, recorded, bundled) {
52
66
  if (!present)
53
67
  return "absent";
package/dist/render.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { DEFAULT_DIALECT } from "./dialects.js";
1
2
  export function renderConfigInit(report) {
2
3
  const lines = [
3
4
  report.action === "written"
@@ -99,10 +100,12 @@ export function renderDoctor(report) {
99
100
  }
100
101
  }
101
102
  lines.push("");
103
+ // A repo whose dialect can never carry a stack still counts as bare when nothing else
104
+ // is installed — `init` is the right nudge, and it is not a stack it is missing.
102
105
  const allAbsent = report.skills.status === "absent" &&
103
106
  report.lenses.status === "absent" &&
104
107
  report.driver.status === "absent" &&
105
- report.stack.status === "absent";
108
+ (report.stack.status === "absent" || report.stack.status === "unsupported");
106
109
  if (allAbsent) {
107
110
  lines.push("Speccle is not set up here — run `speccle init`");
108
111
  }
@@ -148,6 +151,9 @@ function describeDriver(driver) {
148
151
  }
149
152
  }
150
153
  function describeStack(stack) {
154
+ if (stack.status === "unsupported") {
155
+ return `not applicable — oracle strength needs the ${DEFAULT_DIALECT} dialect, this repo is ${stack.dialect}`;
156
+ }
151
157
  if (stack.status === "absent")
152
158
  return "not provisioned — run `speccle strength init`";
153
159
  if (stack.status === "current")
@@ -185,7 +191,10 @@ export function renderUpdate(report) {
185
191
  else {
186
192
  lines.push(`driver ${report.driver.from ?? "unpinned"} → ${report.driver.to} — review & commit the diff`);
187
193
  }
188
- if (report.stack.status === "absent") {
194
+ if (report.stack.status === "unsupported") {
195
+ lines.push(`stack not applicable — oracle strength needs the ${DEFAULT_DIALECT} dialect`);
196
+ }
197
+ else if (report.stack.status === "absent") {
189
198
  lines.push("stack not provisioned — run `speccle strength init`");
190
199
  }
191
200
  else if (report.stack.status === "current") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "speccle",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Speccle's deterministic tooling: spec lint and the oracle-strength heatmap. No LLM, bar the opt-in CI review driver.",
5
5
  "license": "MIT",
6
6
  "author": {