simt-dev 0.6.3 → 0.7.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.
Files changed (137) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +2 -2
  3. package/agents/simt-agent.md +84 -0
  4. package/docs/decisions/ADR-009-skill-layout.md +1 -1
  5. package/docs/decisions/ADR-086-the-stable-prefix-comes-first-and-the-lock-records-both-digests.md +1 -1
  6. package/docs/decisions/ADR-106-the-harness-hook-runs-through-a-repo-shim.md +46 -0
  7. package/docs/decisions/ADR-107-an-install-can-name-one-harness-and-the-skipped-ones-are-named.md +51 -0
  8. package/docs/decisions/ADR-108-an-adoption-builds-the-index-and-a-command-refreshes-it-on-read.md +59 -0
  9. package/docs/decisions/ADR-109-simt-dev-is-its-own-consumer-and-the-guards-that-kept-it-from-being-one-are-retired.md +47 -0
  10. package/docs/decisions/ADR-110-the-lock-file-follows-the-manifest-name-and-the-old-name-is-read-until-the-next-write.md +43 -0
  11. package/docs/decisions/ADR-111-the-tool-ships-one-agent-rendered-per-harness-and-installs-it-with-init-and-a-command.md +75 -0
  12. package/docs/decisions/ADR-112-the-registry-is-what-simt-dev-ships-and-a-repo-owns-its-own-skills.md +81 -0
  13. package/docs/decisions/ADR-113-a-pre-push-that-carries-only-documentation-skips-the-test-gate-and-says-so.md +87 -0
  14. package/docs/decisions/ADR-114-the-bench-pairs-at-the-task-level-a-missing-cost-is-not-zero-and-an-unasked-cross-group-compare-is-refused.md +74 -0
  15. package/docs/decisions/ADR-115-the-index-stamps-its-schema-version-migrates-older-databases-in-place-and-serves-newer-ones-from-a-sidecar.md +69 -0
  16. package/docs/decisions/ADR-116-rust-support-reaches-the-example-path-the-indent-and-the-bench-runner.md +100 -0
  17. package/docs/decisions/README.md +9 -0
  18. package/package.json +4 -2
  19. package/rules/core.md +2 -2
  20. package/rules/mdc/test-gate.mdc +2 -0
  21. package/rules/stacks/android.md +3 -3
  22. package/rules/stacks/connectiq.md +2 -2
  23. package/rules/stacks/dart.md +1 -1
  24. package/rules/stacks/deno.md +1 -1
  25. package/rules/stacks/dotnet.md +1 -1
  26. package/rules/stacks/elixir.md +1 -1
  27. package/rules/stacks/go.md +1 -1
  28. package/rules/stacks/jvm.md +3 -2
  29. package/rules/stacks/node.md +2 -2
  30. package/rules/stacks/python.md +7 -6
  31. package/rules/stacks/ruby.md +1 -1
  32. package/rules/stacks/rust.md +1 -1
  33. package/rules/stacks/swift.md +1 -1
  34. package/rules/stacks/typescript.md +2 -2
  35. package/rules/stacks/uv.md +3 -2
  36. package/skills/simt-commands/SKILL.md +70 -0
  37. package/src/advice/advice.mjs +7 -3
  38. package/src/agenthooks/decide.mjs +141 -9
  39. package/src/agenthooks/opencode.mjs +72 -0
  40. package/src/agenthooks/prompt.mjs +6 -2
  41. package/src/agenthooks/render.mjs +186 -19
  42. package/src/agenthooks/shim.mjs +55 -0
  43. package/src/agents/render.mjs +277 -0
  44. package/src/argv.mjs +8 -0
  45. package/src/bench/ablate.mjs +31 -14
  46. package/src/bench/mine.mjs +48 -1
  47. package/src/bench/probes.mjs +5 -6
  48. package/src/bench/report.mjs +243 -80
  49. package/src/bench/run.mjs +12 -11
  50. package/src/bench/variants.mjs +112 -40
  51. package/src/brief/assemble.mjs +6 -0
  52. package/src/ci/workflow.mjs +16 -7
  53. package/src/commands/agent.mjs +206 -0
  54. package/src/commands/agenthook.mjs +109 -15
  55. package/src/commands/bench.mjs +2 -1
  56. package/src/commands/compare.mjs +8 -7
  57. package/src/commands/decide.mjs +6 -5
  58. package/src/commands/evidence.mjs +13 -3
  59. package/src/commands/gate.mjs +85 -19
  60. package/src/commands/graph.mjs +18 -5
  61. package/src/commands/help.mjs +35 -14
  62. package/src/commands/hooks.mjs +68 -25
  63. package/src/commands/impact.mjs +4 -2
  64. package/src/commands/index.mjs +17 -4
  65. package/src/commands/init.mjs +169 -37
  66. package/src/commands/lease.mjs +203 -35
  67. package/src/commands/lessons.mjs +65 -9
  68. package/src/commands/map.mjs +33 -18
  69. package/src/commands/plugin.mjs +1 -1
  70. package/src/commands/repoindex.mjs +1 -0
  71. package/src/commands/rules.mjs +92 -18
  72. package/src/commands/skills.mjs +60 -25
  73. package/src/commands/skillscompile.mjs +8 -7
  74. package/src/commands/skillsinstall.mjs +12 -0
  75. package/src/commands/slice.mjs +377 -18
  76. package/src/commands/sync.mjs +37 -22
  77. package/src/commands/tests.mjs +5 -2
  78. package/src/commands/undo.mjs +24 -16
  79. package/src/commands/upgrade.mjs +33 -1
  80. package/src/commands/why.mjs +10 -4
  81. package/src/compare/hooksstate.mjs +23 -1
  82. package/src/compare/learn.mjs +82 -57
  83. package/src/compare/parallel.mjs +50 -0
  84. package/src/compare/row.mjs +26 -1
  85. package/src/completions.mjs +5 -0
  86. package/src/decisions/dir.mjs +84 -0
  87. package/src/decisions/index.mjs +109 -11
  88. package/src/distill/distill.mjs +3 -1
  89. package/src/ecosystem/node-runner.mjs +144 -0
  90. package/src/explain.mjs +18 -6
  91. package/src/graph/load.mjs +38 -19
  92. package/src/hooks/capability.mjs +14 -1
  93. package/src/hooks/dispatcher.mjs +335 -13
  94. package/src/hooks/resolve.mjs +9 -3
  95. package/src/hooks/shim.mjs +12 -7
  96. package/src/identity.mjs +67 -14
  97. package/src/index/extract.mjs +192 -11
  98. package/src/index/persist.mjs +307 -47
  99. package/src/index/store.mjs +263 -60
  100. package/src/index/tests.mjs +6 -35
  101. package/src/journal.mjs +296 -77
  102. package/src/kernel/card.mjs +37 -12
  103. package/src/kernel/checks.mjs +44 -9
  104. package/src/kernel/variants.mjs +27 -16
  105. package/src/lease.mjs +97 -15
  106. package/src/ledger/index.mjs +91 -25
  107. package/src/mcp/resources.mjs +5 -3
  108. package/src/mcp/server.mjs +81 -22
  109. package/src/mcp/tools.mjs +45 -9
  110. package/src/memory/decide.mjs +98 -43
  111. package/src/memory/lessons.mjs +168 -33
  112. package/src/name.mjs +9 -0
  113. package/src/plan/index.mjs +31 -8
  114. package/src/plan/template.mjs +27 -5
  115. package/src/plugins.mjs +6 -2
  116. package/src/prbody.mjs +7 -5
  117. package/src/projection/baselines.mjs +1 -1
  118. package/src/projection/disclosure.mjs +2 -2
  119. package/src/projection/loaders.mjs +41 -1
  120. package/src/projection/lock.mjs +21 -14
  121. package/src/projection/manifest.mjs +69 -19
  122. package/src/projection/markers.mjs +43 -3
  123. package/src/projection/mdc.mjs +34 -7
  124. package/src/projection/project.mjs +162 -18
  125. package/src/projection/rules.mjs +54 -3
  126. package/src/projection/surfaces.mjs +9 -1
  127. package/src/repo-root.mjs +69 -0
  128. package/src/resolve/body.mjs +4 -0
  129. package/src/resolve/fingerprint.mjs +5 -0
  130. package/src/resolve/local.mjs +58 -0
  131. package/src/review/critic.mjs +4 -2
  132. package/src/secrets.mjs +165 -22
  133. package/src/state.mjs +76 -0
  134. package/src/status.mjs +9 -4
  135. package/src/toolwords.mjs +5 -0
  136. package/src/tsconfig.mjs +39 -99
  137. package/templates/plan/tracker.json +1 -1
@@ -11,6 +11,7 @@ import { mkdir, readFile, rm, unlink, writeFile } from 'node:fs/promises';
11
11
  import { dirname, join, resolve } from 'node:path';
12
12
 
13
13
  import { MANIFEST_FILE, STATE_DIR } from '../name.mjs';
14
+ import { LOCAL_DIR } from '../state.mjs';
14
15
  import { readManifest } from '../projection/manifest.mjs';
15
16
  import { inject, parse as parseRegion } from '../projection/markers.mjs';
16
17
  import { loadMdcRules, renderMdc, ruleName } from '../projection/mdc.mjs';
@@ -23,6 +24,7 @@ import {
23
24
  DEFAULT_BOOTSTRAP_SEED,
24
25
  compareArms,
25
26
  evaluateRunRecord,
27
+ holmAdjust,
26
28
  loadPrices,
27
29
  loadRunRecords,
28
30
  } from './report.mjs';
@@ -340,11 +342,14 @@ export async function ablateSectionInWorkspace(workDir, ablate) {
340
342
 
341
343
  /**
342
344
  * Decide whether a canary comparison warrants escalating the section arm to the full task suite.
345
+ *
346
+ * `pValue` overrides the comparison's raw p-values when the caller supplies a
347
+ * multiplicity-adjusted value (Holm over the family of sections tested in one run).
343
348
  */
344
- export function shouldEscalateCanary(comparison, { alpha = CANARY_ALPHA } = {}) {
349
+ export function shouldEscalateCanary(comparison, { alpha = CANARY_ALPHA, pValue } = {}) {
345
350
  if (!comparison || !comparison.pairedTasks) return false;
346
- if (comparison.mcnemar.pValue < alpha) return true;
347
- if (comparison.wilcoxon.pValue < alpha) return true;
351
+ const p = pValue ?? Math.min(comparison.rTest.pValue, comparison.wilcoxon.pValue);
352
+ if (p < alpha) return true;
348
353
  if (comparison.ci95.R[0] > 0 || comparison.ci95.R[1] < 0) return true;
349
354
  if (Math.abs(comparison.deltas.R) >= 0.2) return true;
350
355
  return false;
@@ -469,15 +474,27 @@ export async function ablateBench(
469
474
  const canaryEvaluated = canaryRecords.map((rec) => evaluateRunRecord(rec, prices));
470
475
  const currentCanary = canaryEvaluated.filter((r) => r.arm === 'current');
471
476
 
477
+ const canaryComparisons = [];
472
478
  for (const sec of sections) {
473
479
  const armCanary = canaryEvaluated.filter((r) => r.arm === sec.arm);
474
480
  if (currentCanary.length > 0 && armCanary.length > 0) {
475
- const cmp = compareArms(sec.arm, armCanary, 'current', currentCanary, { samples, seed });
476
- if (shouldEscalateCanary(cmp)) {
477
- escalatedArms.add(sec.arm);
478
- }
481
+ canaryComparisons.push({
482
+ sec,
483
+ cmp: compareArms(sec.arm, armCanary, 'current', currentCanary, { samples, seed }),
484
+ });
479
485
  }
480
486
  }
487
+ // Family = every section tested in this one run: testing ~N sections at α 0.05
488
+ // each inflates the false-positive rate, so the p-values are Holm-adjusted
489
+ // before the escalation decision (review 0003 Q46).
490
+ const adjusted = holmAdjust(
491
+ canaryComparisons.map(({ cmp }) => Math.min(cmp.rTest.pValue, cmp.wilcoxon.pValue)),
492
+ );
493
+ canaryComparisons.forEach(({ sec, cmp }, i) => {
494
+ if (shouldEscalateCanary(cmp, { pValue: adjusted[i] })) {
495
+ escalatedArms.add(sec.arm);
496
+ }
497
+ });
481
498
 
482
499
  if (escalatedArms.size > 0) {
483
500
  await execRunBench(resolvedRoot, {
@@ -529,7 +546,7 @@ export async function ablateBench(
529
546
  dC: [0, 0],
530
547
  valuePerKb: [0, 0],
531
548
  },
532
- mcnemar: { statistic: 0, pValue: 1, b: 0, c: 0 },
549
+ rTest: { statistic: 0, pValue: 1, plus: 0, minus: 0 },
533
550
  wilcoxon: { statistic: 0, pValue: 1, wPlus: 0, wMinus: 0 },
534
551
  deletionCandidate: false,
535
552
  };
@@ -566,11 +583,11 @@ export async function ablateBench(
566
583
  dC: cmp.ci95.C,
567
584
  valuePerKb: vCi,
568
585
  },
569
- mcnemar: {
570
- statistic: cmp.mcnemar.statistic,
571
- pValue: cmp.mcnemar.pValue,
572
- b: cmp.mcnemar.b,
573
- c: cmp.mcnemar.c,
586
+ rTest: {
587
+ statistic: cmp.rTest.statistic,
588
+ pValue: cmp.rTest.pValue,
589
+ plus: cmp.rTest.plus,
590
+ minus: cmp.rTest.minus,
574
591
  },
575
592
  wilcoxon: {
576
593
  statistic: cmp.wilcoxon.statistic,
@@ -616,7 +633,7 @@ export async function ablateBench(
616
633
  const markdown = formatAblationMarkdown(result);
617
634
 
618
635
  if (apply) {
619
- const outPath = join(resolvedRoot, STATE_DIR, 'bench', 'ablation.json');
636
+ const outPath = join(resolvedRoot, LOCAL_DIR, 'bench', 'ablation.json');
620
637
  await mkdir(dirname(outPath), { recursive: true });
621
638
  await writeFile(outPath, `${JSON.stringify({ schemaVersion: 1, ...result }, null, 2)}\n`, 'utf8');
622
639
  }
@@ -29,11 +29,12 @@ export const DEFAULT_MINE_TARGET = 20;
29
29
  /**
30
30
  * Classify whether a repository-relative path is a test file.
31
31
  *
32
- * Covers the five ecosystems named in plan 0003 §5 R01:
32
+ * Covers the ecosystems named in plan 0003 §5 R01:
33
33
  * - `test/`, `tests/`, `spec/`, `__tests__/`, `src/test/`
34
34
  * - `*.test.*`, `*.spec.*`, `*_test.*`, `*_spec.*`
35
35
  * - `*_test.go` (Go)
36
36
  * - `test_*.py` (pytest)
37
+ * - `tests/*.rs` (Cargo integration tests)
37
38
  * - `*Test.java`, `*Tests.java`, `*Test.kt`, `*Tests.kt` (Gradle / Maven)
38
39
  *
39
40
  * @param {string} relPath forward-slash repository-relative path
@@ -259,6 +260,31 @@ export function parseGoTestJson(text) {
259
260
  };
260
261
  }
261
262
 
263
+ /**
264
+ * Parse libtest's text output, which `cargo test` prints for every test binary.
265
+ *
266
+ * A line is a result only when it has the `test <name> ... <status>` shape.
267
+ * Harness summaries (`test result: ok.`), compile output and doctest prose are
268
+ * not results. `ignored` is neither a pass nor a failure: counting it as one
269
+ * would make a skipped test look like evidence the change works.
270
+ *
271
+ * @param {string} text
272
+ * @returns {{passed: string[], failed: string[]}}
273
+ */
274
+ export function parseCargoTestOutput(text) {
275
+ const passed = [];
276
+ const failed = [];
277
+ for (const line of String(text ?? '').split('\n')) {
278
+ const match = /^test\s+(.+?)\s+\.\.\.\s+(ok|FAILED|ignored)\s*$/.exec(line.trimEnd());
279
+ if (!match || match[2] === 'ignored') continue;
280
+ (match[2] === 'ok' ? passed : failed).push(match[1]);
281
+ }
282
+ return {
283
+ passed: dedupeSorted(passed),
284
+ failed: dedupeSorted(failed),
285
+ };
286
+ }
287
+
262
288
  /**
263
289
  * Detect the test runner for a repository and build the command for a set of
264
290
  * changed test files.
@@ -302,6 +328,27 @@ export async function detectTestRunner(root, testFiles) {
302
328
  };
303
329
  }
304
330
 
331
+ if (existsSync(join(root, 'Cargo.toml'))) {
332
+ // Cargo has no "run this file" argument. An integration test is a target
333
+ // named after its file in `tests/`, so it can be selected with `--test`.
334
+ // A source file carries inline unit tests and can only be reached by the
335
+ // crate's test binary, so one such file means the whole crate runs.
336
+ const targets = dedupeSorted(
337
+ files.filter((f) => /^tests\/[^/]+\.rs$/.test(f)).map((f) => basename(f, '.rs')),
338
+ );
339
+ const integrationOnly = files.length > 0 && files.every((f) => /^tests\/[^/]+\.rs$/.test(f));
340
+ const args = ['test'];
341
+ if (integrationOnly) {
342
+ for (const target of targets) args.push('--test', target);
343
+ }
344
+ return {
345
+ runner: 'cargo-test',
346
+ displayCmd: ['cargo', ...args].join(' '),
347
+ buildExec: () => ({ cmd: 'cargo', args }),
348
+ parse: (stdout) => parseCargoTestOutput(stdout),
349
+ };
350
+ }
351
+
305
352
  if (existsSync(join(root, 'go.mod'))) {
306
353
  const pkgs = dedupeSorted(
307
354
  files.map((f) => {
@@ -25,7 +25,7 @@ import { chmod, mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs
25
25
  import { tmpdir } from 'node:os';
26
26
  import { dirname, join, resolve } from 'node:path';
27
27
 
28
- import { STATE_DIR } from '../name.mjs';
28
+ import { LOCAL_DIR } from '../state.mjs';
29
29
  import { parse as parseRegion } from '../projection/markers.mjs';
30
30
  import { materialiseArm, parseArmSpec } from './run.mjs';
31
31
 
@@ -92,7 +92,7 @@ function runNode(args, { cwd, env = {} } = {}) {
92
92
  * @returns {Promise<Array<object>>}
93
93
  */
94
94
  export async function readLedgerEntries(workDir) {
95
- const ledgerPath = join(workDir, STATE_DIR, 'ledger.jsonl');
95
+ const ledgerPath = join(workDir, LOCAL_DIR, 'ledger.jsonl');
96
96
  try {
97
97
  const text = await readFile(ledgerPath, 'utf8');
98
98
  const entries = [];
@@ -115,7 +115,7 @@ export async function readLedgerEntries(workDir) {
115
115
  * Append a ledger entry to `.simt-dev/ledger.jsonl` in `workDir`.
116
116
  */
117
117
  export async function appendLedgerEntry(workDir, entry) {
118
- const ledgerPath = join(workDir, STATE_DIR, 'ledger.jsonl');
118
+ const ledgerPath = join(workDir, LOCAL_DIR, 'ledger.jsonl');
119
119
  await mkdir(dirname(ledgerPath), { recursive: true });
120
120
  const treeRes = await runGit(['write-tree'], { cwd: workDir });
121
121
  const record = {
@@ -1148,8 +1148,7 @@ export async function gradeProbe(probeOrId, ctx) {
1148
1148
  * @param {string} root
1149
1149
  * @returns {Promise<Array<object>>}
1150
1150
  */
1151
- export async function loadProbeRecords(root) {
1152
- const probesRoot = join(resolve(root), STATE_DIR, 'bench', 'probes');
1151
+ export async function loadProbeRecords(root) { const probesRoot = join(resolve(root), LOCAL_DIR, 'bench', 'probes');
1153
1152
  if (!existsSync(probesRoot)) return [];
1154
1153
 
1155
1154
  const entries = await readdir(probesRoot, { withFileTypes: true }).catch(() => []);
@@ -1242,7 +1241,7 @@ export async function runBenchProbes(
1242
1241
  for (let rep = 1; rep <= reps; rep += 1) {
1243
1242
  const runDir = join(
1244
1243
  resolvedRoot,
1245
- STATE_DIR,
1244
+ LOCAL_DIR,
1246
1245
  'bench',
1247
1246
  'probes',
1248
1247
  `${harness}-${armSpec.name}-r${rep}`,