simt-dev 0.3.1 → 0.4.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 (52) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/README.md +14 -4
  3. package/package.json +2 -2
  4. package/rules/core.md +1 -1
  5. package/src/agenthooks/policy.mjs +10 -2
  6. package/src/agenthooks/prompt.mjs +81 -0
  7. package/src/agenthooks/render.mjs +138 -17
  8. package/src/argv.mjs +16 -1
  9. package/src/bench/grade.mjs +3 -0
  10. package/src/bench/run.mjs +85 -39
  11. package/src/bench/variants.mjs +456 -0
  12. package/src/brief/assemble.mjs +346 -0
  13. package/src/brief/kind.mjs +134 -0
  14. package/src/brief/packet.mjs +197 -0
  15. package/src/commands/agenthook.mjs +84 -5
  16. package/src/commands/bench.mjs +40 -15
  17. package/src/commands/brief.mjs +78 -0
  18. package/src/commands/compare.mjs +10 -2
  19. package/src/commands/decide.mjs +116 -0
  20. package/src/commands/help.mjs +34 -3
  21. package/src/commands/index.mjs +17 -1
  22. package/src/commands/lessons.mjs +127 -0
  23. package/src/commands/local.mjs +97 -0
  24. package/src/commands/map.mjs +42 -7
  25. package/src/commands/repoindex.mjs +4 -1
  26. package/src/commands/rules.mjs +17 -2
  27. package/src/commands/skills.mjs +3 -17
  28. package/src/commands/skillscompile.mjs +255 -0
  29. package/src/commands/sync.mjs +15 -0
  30. package/src/commands/tests.mjs +2 -49
  31. package/src/commands/why.mjs +18 -23
  32. package/src/compare/learn.mjs +680 -0
  33. package/src/decisions/index.mjs +116 -65
  34. package/src/federation/client.mjs +188 -0
  35. package/src/federation/config.mjs +134 -0
  36. package/src/federation/query.mjs +48 -0
  37. package/src/graph/compile.mjs +475 -0
  38. package/src/index/gate.mjs +1 -13
  39. package/src/index/govern.mjs +23 -11
  40. package/src/index/map.mjs +251 -17
  41. package/src/index/persist.mjs +74 -7
  42. package/src/index/store.mjs +222 -8
  43. package/src/kernel/variants.mjs +353 -0
  44. package/src/ledger/index.mjs +36 -1
  45. package/src/local/client.mjs +482 -0
  46. package/src/local/config.mjs +234 -0
  47. package/src/mcp/tools.mjs +138 -1
  48. package/src/memory/decide.mjs +462 -0
  49. package/src/memory/lessons.mjs +858 -0
  50. package/src/projection/lock.mjs +39 -7
  51. package/src/projection/rules.mjs +32 -8
  52. package/src/toolwords.mjs +6 -0
@@ -8,9 +8,10 @@
8
8
  // rewritten.
9
9
 
10
10
  import { createHash } from 'node:crypto';
11
- import { readFile, readdir } from 'node:fs/promises';
11
+ import { readFile, readdir, writeFile } from 'node:fs/promises';
12
12
  import { join } from 'node:path';
13
13
 
14
+ import { readLockedVariants } from '../kernel/variants.mjs';
14
15
  import { fingerprint } from '../resolve/fingerprint.mjs';
15
16
  import { desiredRegion, provenanceDigest, LOCAL_PROFILE_DIR } from './rules.mjs';
16
17
  import { readManifest } from './manifest.mjs';
@@ -29,11 +30,25 @@ const MANIFEST_KEYS = ['mode', 'budget', 'shared', 'surfaces', 'gates'];
29
30
  * body that was written and not a body for a different set of surfaces
30
31
  */
31
32
  export async function computeLock(root, flags = {}) {
32
- const desired = await desiredRegion(root, flags);
33
- return {
34
- inputs: await inputDigest(root),
35
- outputs: createHash('sha256').update(desired.body).digest('hex'),
36
- };
33
+ const variants =
34
+ flags?.variants !== undefined ? flags.variants : await readLockedVariants(root);
35
+ const desired = await desiredRegion(root, { ...flags, variants });
36
+ const inputs = await inputDigest(root, variants);
37
+ const outputs = createHash('sha256').update(desired.body).digest('hex');
38
+ const hasVariants =
39
+ variants && typeof variants === 'object' && Object.keys(variants).length > 0;
40
+ return hasVariants ? { inputs, outputs, variants } : { inputs, outputs };
41
+ }
42
+
43
+ /**
44
+ * Record locked variant winners in `simt-dev.lock.json` alongside updated digests.
45
+ */
46
+ export async function writeLockVariants(root, nextVariants, flags = {}) {
47
+ const existing = await readLockedVariants(root);
48
+ const merged = { ...existing, ...(nextVariants ?? {}) };
49
+ const lock = await computeLock(root, { ...flags, variants: merged });
50
+ await writeFile(join(root, LOCK_FILE), `${JSON.stringify(lock, null, 2)}\n`, 'utf8');
51
+ return lock;
37
52
  }
38
53
 
39
54
  /** Read a lock, or null when the file is not there. A file that does not parse is an
@@ -52,13 +67,19 @@ export async function readLock(root) {
52
67
  if (typeof lock?.inputs !== 'string' || typeof lock?.outputs !== 'string') {
53
68
  return { lock: null, error: 'must be an object with inputs and outputs' };
54
69
  }
70
+ if (
71
+ lock.variants !== undefined &&
72
+ (typeof lock.variants !== 'object' || lock.variants === null || Array.isArray(lock.variants))
73
+ ) {
74
+ return { lock: null, error: 'variants must be an object' };
75
+ }
55
76
  return { lock, error: null };
56
77
  } catch (error) {
57
78
  return { lock: null, error: error.message };
58
79
  }
59
80
  }
60
81
 
61
- async function inputDigest(root) {
82
+ async function inputDigest(root, variants = {}) {
62
83
  const hash = createHash('sha256');
63
84
  hash.update(`provenance:${await provenanceDigest()}\n`);
64
85
  const { stack, facts } = await fingerprint(root);
@@ -89,5 +110,16 @@ async function inputDigest(root) {
89
110
  } catch (error) {
90
111
  if (error.code !== 'ENOENT' && !(error instanceof SyntaxError)) throw error;
91
112
  }
113
+ const variantKeys =
114
+ variants && typeof variants === 'object' ? Object.keys(variants).sort() : [];
115
+ if (variantKeys.length > 0) {
116
+ const winners = {};
117
+ for (const key of variantKeys) {
118
+ const entry = variants[key];
119
+ winners[key] =
120
+ typeof entry === 'string' ? entry : (entry?.winner ?? entry?.variant ?? null);
121
+ }
122
+ hash.update(`variants:${JSON.stringify(winners)}\n`);
123
+ }
92
124
  return hash.digest('hex');
93
125
  }
@@ -35,6 +35,7 @@ import { commandCard, CARD_LIMIT_BYTES } from '../kernel/card.mjs';
35
35
  import { packToBudget } from '../kernel/budget.mjs';
36
36
  import { filterInferable, inferableDefect, inferableProofs } from '../kernel/inferability.mjs';
37
37
  import { checkDefect, foldChecks } from '../kernel/checks.mjs';
38
+ import { foldVariants, readLockedVariants, variantsDefect } from '../kernel/variants.mjs';
38
39
  import { BASELINES } from './baselines.mjs';
39
40
  import { CLI } from '../name.mjs';
40
41
  import { installedSkills } from './disclosure.mjs';
@@ -403,12 +404,14 @@ async function ruleBodies(root, manifest, flags) {
403
404
  for (const p of renderProblems) problems.push(`${rule.file}: ${p}`);
404
405
  if (renderProblems.length > 0) continue;
405
406
 
407
+ const variantFolded = foldVariants(body, rule.id ?? '', flags?.variants ?? null);
408
+
406
409
  // One canonical body, one target per selected surface. The body is identical
407
410
  // everywhere; only the frontmatter the editor reads before any rule text
408
411
  // differs. Without this loop a repo in a Claude Code session receives
409
412
  // AGENTS.md and none of the always-on process rules, because those lived only
410
413
  // in a directory only Cursor reads.
411
- bodies.push({ rule, body });
414
+ bodies.push({ rule, body: variantFolded.body });
412
415
  }
413
416
 
414
417
  return { resolved, bodies, problems, notes, skipped };
@@ -449,12 +452,14 @@ function toolRule(rule) {
449
452
 
450
453
  export async function desiredRegion(root, flags) {
451
454
  const { stack, facts, manifest } = await fingerprint(root);
455
+ const lockedVariants =
456
+ flags?.variants !== undefined ? flags.variants : await readLockedVariants(root);
452
457
  const all = await loadRules(root);
453
458
  const { selected, fallback, notes } = select(all, stack, facts);
454
459
  const { resolved, bodies, problems, notes: ruleNotes } = await ruleBodies(
455
460
  root,
456
461
  manifest,
457
- flags ?? {},
462
+ { ...(flags ?? {}), variants: lockedVariants },
458
463
  );
459
464
  notes.push(...ruleNotes);
460
465
 
@@ -476,7 +481,8 @@ export async function desiredRegion(root, flags) {
476
481
  // selected surface cannot narrow, the file would be the only home for a rule that
477
482
  // reader cannot reach — so it stays, and says why.
478
483
  if (narrowing) {
479
- scoped.push(r);
484
+ const vf = foldVariants(r.body, r.id ?? '', lockedVariants);
485
+ scoped.push(vf.changed ? { ...r, body: vf.body } : r);
480
486
  continue;
481
487
  }
482
488
  inRegion.push(r);
@@ -521,18 +527,27 @@ export async function desiredRegion(root, flags) {
521
527
  // which lines the reader already knows.
522
528
  const inferable = [];
523
529
  const shipped = [];
530
+ const variantsSelected = [];
524
531
  let enforced = 0;
525
532
  for (const r of packed) {
526
- const filtered = filterInferable(r.body, proofs);
533
+ const variantFolded = foldVariants(r.body, r.id ?? '', lockedVariants);
534
+ const filtered = filterInferable(variantFolded.body, proofs);
527
535
  const folded = foldChecks(filtered.body);
528
536
  enforced += folded.enforced.length;
529
- if (!filtered.changed && !folded.changed) {
537
+ variantsSelected.push(...variantFolded.selected);
538
+ if (!variantFolded.changed && !filtered.changed && !folded.changed) {
530
539
  shipped.push(r);
531
540
  continue;
532
541
  }
533
542
  for (const x of filtered.removed) inferable.push({ rule: r.id, text: x.text, expr: x.expr });
534
543
  shipped.push({ ...r, body: folded.body });
535
544
  }
545
+ const lockedNonDefault = variantsSelected.filter((v) => v.locked && v.winner !== v.baseline);
546
+ if (lockedNonDefault.length > 0) {
547
+ notes.push(
548
+ `variants: applied ${lockedNonDefault.length} locked variant(s) (${lockedNonDefault.map((v) => `${v.slotId}=${v.winner}`).join(', ')})`,
549
+ );
550
+ }
536
551
  if (enforced > 0) {
537
552
  notes.push(
538
553
  `enforced: ${enforced} line(s) left the region because a checker runs them`,
@@ -1046,7 +1061,11 @@ export async function loadRules(root = null) {
1046
1061
  // it travels the field that already reaches `check` and `rules list`. The
1047
1062
  // frontmatter defect wins, because a rule that cannot say what it applies to cannot
1048
1063
  // be reasoned about at all.
1049
- error: describeDefect(meta) ?? inferableDefect(body) ?? checkDefect(body),
1064
+ error:
1065
+ describeDefect(meta) ??
1066
+ inferableDefect(body) ??
1067
+ checkDefect(body) ??
1068
+ variantsDefect(body, meta['simt-id'] ?? ''),
1050
1069
  });
1051
1070
  }
1052
1071
  return out;
@@ -1081,6 +1100,8 @@ export async function packageRegions(root, flags, rootProfiles = null) {
1081
1100
  if (packages.length === 0) return { regions: [], notes };
1082
1101
 
1083
1102
  const rules = await loadRules(root);
1103
+ const lockedVariants =
1104
+ flags?.variants !== undefined ? flags.variants : await readLockedVariants(root);
1084
1105
  const sha = await provenanceDigest();
1085
1106
  // The ROOT's proofs, not the package's. The rule set is the root's, so which lines it
1086
1107
  // already proves is a fact about the repository rather than about the directory a file
@@ -1094,9 +1115,12 @@ export async function packageRegions(root, flags, rootProfiles = null) {
1094
1115
  const delta = selected.filter((r) => !shared.includes(r.id));
1095
1116
  if (delta.length === 0) continue;
1096
1117
  const shipped = delta.map((r) => {
1097
- const filtered = filterInferable(r.body, proofs);
1118
+ const variantFolded = foldVariants(r.body, r.id ?? '', lockedVariants);
1119
+ const filtered = filterInferable(variantFolded.body, proofs);
1098
1120
  const folded = foldChecks(filtered.body);
1099
- return filtered.changed || folded.changed ? { ...r, body: folded.body } : r;
1121
+ return variantFolded.changed || filtered.changed || folded.changed
1122
+ ? { ...r, body: folded.body }
1123
+ : r;
1100
1124
  });
1101
1125
  regions.push({
1102
1126
  path: pkg.path,
package/src/toolwords.mjs CHANGED
@@ -18,6 +18,9 @@ import { KNOWN_KEYS } from './projection/mdc.mjs';
18
18
  import { PROFILE_KEYS } from './projection/rules.mjs';
19
19
  import { INFERABLE_MARKER } from './kernel/inferability.mjs';
20
20
  import { CHECK_MARKER } from './kernel/checks.mjs';
21
+ import { VARIANTS_MARKER } from './kernel/variants.mjs';
22
+ import { LESSON_KEYS } from './memory/lessons.mjs';
23
+ import { DECISION_KEYS } from './memory/decide.mjs';
21
24
 
22
25
  /**
23
26
  * `simt-` names that belong to this tool and never to a repository.
@@ -31,8 +34,11 @@ import { CHECK_MARKER } from './kernel/checks.mjs';
31
34
  export const TOOL_WORDS = new Set([
32
35
  ...KNOWN_KEYS,
33
36
  ...PROFILE_KEYS,
37
+ ...LESSON_KEYS,
38
+ ...DECISION_KEYS,
34
39
  INFERABLE_MARKER,
35
40
  CHECK_MARKER,
41
+ VARIANTS_MARKER,
36
42
  'simt-dev',
37
43
  'simt-dev-check',
38
44
  'simt-hooks',