simt-dev 0.7.1 → 0.8.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 (186) hide show
  1. package/CHANGELOG.md +36 -1
  2. package/README.md +169 -80
  3. package/docs/decisions/ADR-014-workspace-is-configuration.md +1 -1
  4. package/docs/decisions/ADR-025-a-baseline-is-a-region-in-a-file-the-repo-owns.md +1 -1
  5. package/docs/decisions/ADR-026-a-config-without-a-region-is-a-file-the-repo-owns.md +1 -1
  6. package/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +1 -1
  7. package/docs/decisions/ADR-028-a-base-file-is-a-floor-so-the-disagreement-it-cannot-repair-becomes-a-check.md +1 -1
  8. package/docs/decisions/ADR-029-the-pins-a-repository-declares-are-its-own.md +1 -1
  9. package/docs/decisions/ADR-030-what-a-plan-does-not-ship-is-a-decision-with-a-record.md +1 -1
  10. package/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +1 -1
  11. package/docs/decisions/ADR-082-the-tracker-override-is-a-manifest-key.md +1 -1
  12. package/docs/decisions/ADR-126-the-mcp-surface-stays-a-reader-so-there-is-no-write-path-to-build.md +68 -0
  13. package/docs/decisions/ADR-127-the-mcp-tool-list-is-written-by-hand-and-roots-is-not-asked-for.md +65 -0
  14. package/docs/decisions/ADR-128-a-research-plan-is-a-plan-and-the-tracker-reads-it.md +42 -0
  15. package/ecosystems/bun.json +6 -0
  16. package/ecosystems/cargo.json +25 -0
  17. package/ecosystems/composer.json +6 -0
  18. package/ecosystems/dart.json +24 -0
  19. package/ecosystems/deno.json +21 -0
  20. package/ecosystems/devcontainers.json +6 -0
  21. package/ecosystems/docker-compose.json +6 -0
  22. package/ecosystems/docker.json +6 -0
  23. package/ecosystems/dotnet.json +25 -0
  24. package/ecosystems/elixir.json +23 -0
  25. package/ecosystems/gitsubmodule.json +6 -0
  26. package/ecosystems/go.json +25 -0
  27. package/ecosystems/gradle.json +13 -0
  28. package/ecosystems/maven.json +23 -0
  29. package/ecosystems/node.json +25 -0
  30. package/ecosystems/python.json +20 -0
  31. package/ecosystems/ruby.json +23 -0
  32. package/ecosystems/swift.json +25 -0
  33. package/ecosystems/terraform.json +6 -0
  34. package/ecosystems/uv.json +6 -0
  35. package/harnesses/antigravity.json +162 -0
  36. package/harnesses/claude.json +206 -0
  37. package/harnesses/codex.json +136 -0
  38. package/harnesses/copilot.json +88 -0
  39. package/harnesses/cursor.json +140 -0
  40. package/harnesses/deepseek.json +99 -0
  41. package/harnesses/gemini.json +122 -0
  42. package/harnesses/opencode.json +173 -0
  43. package/package.json +3 -1
  44. package/rules/mdc/intake-questions.mdc +8 -0
  45. package/src/advice/advice.mjs +3 -2
  46. package/src/agenthooks/render.mjs +86 -180
  47. package/src/agents/render.mjs +15 -18
  48. package/src/api.mjs +7 -5
  49. package/src/argv.mjs +19 -0
  50. package/src/bench/ablate.mjs +3 -4
  51. package/src/bench/adapters.json +155 -113
  52. package/src/bench/adapters.mjs +466 -0
  53. package/src/bench/arms.mjs +333 -0
  54. package/src/bench/grade.mjs +12 -26
  55. package/src/bench/graders.mjs +42 -0
  56. package/src/bench/mine.mjs +17 -535
  57. package/src/bench/probe-ledger.mjs +149 -0
  58. package/src/bench/probes.catalog.mjs +918 -0
  59. package/src/bench/probes.mjs +15 -1037
  60. package/src/bench/render.mjs +111 -0
  61. package/src/bench/report.mjs +116 -971
  62. package/src/bench/run.mjs +164 -881
  63. package/src/bench/variants.mjs +4 -5
  64. package/src/bench/workspace.mjs +160 -0
  65. package/src/brief/assemble.mjs +11 -13
  66. package/src/ci/dependabot.mjs +25 -55
  67. package/src/commands/agent.mjs +1 -9
  68. package/src/commands/agenthook.mjs +66 -35
  69. package/src/commands/bench.mjs +2 -2
  70. package/src/commands/ci.mjs +11 -13
  71. package/src/commands/docs.mjs +88 -0
  72. package/src/commands/gate.mjs +10 -13
  73. package/src/commands/harness.mjs +3 -3
  74. package/src/commands/help.mjs +44 -3
  75. package/src/commands/hooks.mjs +20 -20
  76. package/src/commands/ignore.mjs +11 -7
  77. package/src/commands/impact.mjs +4 -8
  78. package/src/commands/index.mjs +49 -9
  79. package/src/commands/init-scan.mjs +90 -0
  80. package/src/commands/init.mjs +122 -104
  81. package/src/commands/lease.mjs +67 -18
  82. package/src/commands/map.mjs +10 -5
  83. package/src/commands/mcp.mjs +2 -17
  84. package/src/commands/repoindex.mjs +8 -8
  85. package/src/commands/rules.mjs +8 -4
  86. package/src/commands/shared.mjs +12 -16
  87. package/src/commands/skills.mjs +5 -6
  88. package/src/commands/skillscompile.mjs +3 -3
  89. package/src/commands/skillsinstall.mjs +5 -4
  90. package/src/commands/slice.mjs +199 -64
  91. package/src/commands/spec.mjs +78 -62
  92. package/src/commands/status.mjs +24 -12
  93. package/src/commands/sync.mjs +14 -14
  94. package/src/commands/tests.mjs +12 -12
  95. package/src/commands/tokens.mjs +19 -24
  96. package/src/commands/upgrade.mjs +8 -4
  97. package/src/commands/why.mjs +6 -13
  98. package/src/compare/hooksstate.mjs +2 -6
  99. package/src/compare/learn.mjs +3 -1
  100. package/src/decisions/index.mjs +3 -2
  101. package/src/docs/commands.mjs +65 -0
  102. package/src/ecosystem/records.mjs +219 -0
  103. package/src/explain.mjs +31 -0
  104. package/src/gates/index.mjs +385 -0
  105. package/src/graph/compile.mjs +8 -4
  106. package/src/graph/load.mjs +2 -1
  107. package/src/graph/spec.mjs +10 -8
  108. package/src/harness/records.mjs +143 -0
  109. package/src/hooks/capability.mjs +5 -3
  110. package/src/hooks/dispatcher.mjs +26 -32
  111. package/src/hooks/gates.mjs +12 -2
  112. package/src/hooks/resolve.mjs +2 -7
  113. package/src/hooks/where.mjs +2 -7
  114. package/src/identity.mjs +2 -11
  115. package/src/index/coverage.mjs +58 -0
  116. package/src/index/extract.mjs +6 -16
  117. package/src/index/gate.mjs +1 -1
  118. package/src/index/govern.mjs +26 -9
  119. package/src/index/impact.mjs +4 -8
  120. package/src/index/map.mjs +14 -15
  121. package/src/index/open.mjs +173 -0
  122. package/src/index/persist.mjs +224 -57
  123. package/src/index/store.mjs +82 -39
  124. package/src/index/tests.mjs +2 -6
  125. package/src/journal.mjs +16 -11
  126. package/src/kernel/card.mjs +37 -63
  127. package/src/kernel/checks.mjs +4 -10
  128. package/src/lease.mjs +102 -7
  129. package/src/ledger/index.mjs +124 -17
  130. package/src/mcp/paginate.mjs +33 -0
  131. package/src/mcp/schemas.mjs +512 -0
  132. package/src/mcp/server.mjs +49 -9
  133. package/src/mcp/tools-facts.mjs +326 -0
  134. package/src/mcp/tools-read.mjs +221 -0
  135. package/src/mcp/tools.mjs +4 -1005
  136. package/src/{bench → measure}/corpus.mjs +1 -1
  137. package/src/measure/metrics.mjs +236 -0
  138. package/src/measure/records.mjs +211 -0
  139. package/src/measure/stats.mjs +538 -0
  140. package/src/measure/task-schema.mjs +125 -0
  141. package/src/measure/tasks.mjs +69 -0
  142. package/src/memory/decide.mjs +6 -15
  143. package/src/memory/lesson-text.mjs +61 -0
  144. package/src/memory/lessons.mjs +29 -82
  145. package/src/name.mjs +7 -0
  146. package/src/output/index.mjs +122 -31
  147. package/src/packs/manage.mjs +15 -9
  148. package/src/plan/index.mjs +1 -1
  149. package/src/plugins.mjs +22 -16
  150. package/src/prbody.mjs +7 -11
  151. package/src/projection/loaders.mjs +7 -692
  152. package/src/projection/manifest.mjs +10 -435
  153. package/src/projection/mdc.mjs +7 -8
  154. package/src/projection/project.mjs +126 -221
  155. package/src/projection/region.mjs +561 -0
  156. package/src/projection/rules.mjs +16 -547
  157. package/src/projection/surfaces.mjs +43 -210
  158. package/src/projection/targets.mjs +5 -5
  159. package/src/repo-root.mjs +13 -1
  160. package/src/resolve/eval.mjs +2 -1
  161. package/src/resolve/fingerprint.mjs +0 -7
  162. package/src/resolve/resolve.mjs +1 -41
  163. package/src/review/critic.mjs +8 -12
  164. package/src/run.mjs +98 -0
  165. package/src/secrets.mjs +14 -22
  166. package/src/state/events.mjs +129 -0
  167. package/src/status-snapshot.mjs +233 -0
  168. package/src/status.mjs +2 -6
  169. package/src/sys/git.mjs +82 -0
  170. package/src/{bench/process.mjs → sys/proc.mjs} +28 -5
  171. package/src/testing/parsers.mjs +265 -0
  172. package/src/testing/patches.mjs +23 -0
  173. package/src/testing/paths.mjs +49 -0
  174. package/src/testing/runners.mjs +186 -0
  175. package/src/testing/vault.mjs +68 -0
  176. package/src/text/frontmatter.mjs +49 -0
  177. package/src/text/tokens.mjs +52 -0
  178. package/src/toolwords.mjs +1 -1
  179. package/src/util/fs.mjs +20 -0
  180. package/src/util/sort.mjs +12 -0
  181. package/src/workspace.mjs +3 -1
  182. package/src/write/plan.mjs +82 -0
  183. package/templates/plan/tracker.json +1 -0
  184. package/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +0 -82
  185. package/docs/decisions/ADR-102-the-two-queued-review-rows-are-closed-by-ruling.md +0 -49
  186. package/src/bench/tasks.mjs +0 -36
@@ -29,9 +29,9 @@ import { diff, parse, inject, BEGIN, END } from './markers.mjs';
29
29
  import { budgetFinding, readManifest } from './manifest.mjs';
30
30
  import { budgetCutFinding } from '../kernel/budget.mjs';
31
31
  import { measureRoot } from '../measure/context.mjs';
32
- import { loadMdcRules, renderMdc } from './mdc.mjs';
32
+ import { loadMdcRules } from './mdc.mjs';
33
33
  import { foldsRules, resolveSurfaces, SURFACES, SURFACE_IDS } from './surfaces.mjs';
34
- import { pointerState, pointerTargets, ruleTargets, scopedTargets } from './targets.mjs';
34
+ import { pointerState, pointerTargets, scopedTargets } from './targets.mjs';
35
35
  import { CLI, LOCK_FILE, LOCK_LEGACY, PACKAGE_NAME } from '../name.mjs';
36
36
  import { MANIFEST_FILE, WORKFLOW_FILE_NAME } from '../name.mjs';
37
37
  import { resolveNodeRunner } from '../ecosystem/node-runner.mjs';
@@ -101,21 +101,7 @@ async function workflowResolvable(root) {
101
101
  };
102
102
  }
103
103
  import { computeLock } from './lock.mjs';
104
- import {
105
- desiredRegion,
106
- provenanceDigest,
107
- isEligible,
108
- loadRules,
109
- packageRegions,
110
- LOCAL_PROFILE_DIR,
111
- mdcTargets,
112
- readDeclared,
113
- ruleValues,
114
- render,
115
- ROOT,
116
- select,
117
- SUBSTITUTIONS_KEY,
118
- } from './rules.mjs';
104
+ import { desiredRegion, provenanceDigest, isEligible, loadRules, packageRegions, LOCAL_PROFILE_DIR, mdcTargets, readDeclared, render, select, SUBSTITUTIONS_KEY } from './rules.mjs';
119
105
 
120
106
  /** The one file every repo gets. Chosen deliberately: AGENTS.md is read by the
121
107
  * most agents, including ones that ignore .cursor/ entirely. */
@@ -131,7 +117,6 @@ function regionVerdict(action) {
131
117
  : { status: 'drift', detail: `region is ${action}; run: ${CLI} sync --yes` };
132
118
  }
133
119
 
134
-
135
120
  /**
136
121
  * @param {string} root repo root
137
122
  * @returns {Promise<{root:string, stack:string[], facts:string[], profiles:string[],
@@ -236,10 +221,12 @@ async function targetFor(root, dir, body, meta, label = PRIMARY_TARGET) {
236
221
  const path = join(dir, PRIMARY_TARGET);
237
222
  let existing = '';
238
223
  let exists = true;
224
+ let readError = null;
239
225
  try {
240
226
  existing = await readFile(path, 'utf8');
241
- } catch {
227
+ } catch (error) {
242
228
  exists = false;
229
+ readError = error.code ?? String(error);
243
230
  }
244
231
 
245
232
  const result = inject(existing, body, meta, await regionOpts(root));
@@ -248,6 +235,7 @@ async function targetFor(root, dir, body, meta, label = PRIMARY_TARGET) {
248
235
  // document once per package, and a plan that printed `AGENTS.md` twice with no
249
236
  // directory on either line would leave the reader unable to tell which is which.
250
237
  file: label,
238
+ kind: 'region',
251
239
  path,
252
240
  exists,
253
241
  action: result.action,
@@ -261,6 +249,7 @@ async function targetFor(root, dir, body, meta, label = PRIMARY_TARGET) {
261
249
  line: result.line,
262
250
  bytes: Buffer.byteLength(result.text),
263
251
  previousBytes: Buffer.byteLength(existing),
252
+ readError,
264
253
  sha: meta.sha,
265
254
  body,
266
255
  text: result.text,
@@ -268,33 +257,123 @@ async function targetFor(root, dir, body, meta, label = PRIMARY_TARGET) {
268
257
  };
269
258
  }
270
259
 
260
+ /**
261
+ * The findings one write target produces, read off the target itself.
262
+ *
263
+ * This is what makes "the gate equals the writer" true by construction rather than by
264
+ * two implementations happening to agree. `plan` builds every file this tool writes,
265
+ * each through the same `inject` call; the gate asks each target what it would have
266
+ * done instead of rebuilding the file and comparing. The version this replaced rebuilt
267
+ * the region itself with a second `diff` call, and a second construction is invisible
268
+ * while it agrees — the only way to notice is the disagreement some future edit
269
+ * produces, reported as drift on a tree `sync` has just written.
270
+ *
271
+ * Returns an array: a baseline whose region has moved down the file is both current and
272
+ * misplaced, and both findings name the same file.
273
+ *
274
+ * @param {object} target one entry from `plan(root).targets`
275
+ * @returns {object[]} findings, in the order they should be printed
276
+ */
277
+ function targetFindings(target) {
278
+ if (target === undefined) return [];
279
+ // The lock is written here and compared by the frozen check, not reported as drift;
280
+ // a pointer is a symlink the writer makes, reported by its own loop below.
281
+ if (target.kind === 'lock' || target.kind === 'pointer') return [];
282
+
283
+ if (target.kind === 'baseline') {
284
+ if (target.action === 'malformed') {
285
+ return [{ file: target.file, status: 'malformed', detail: target.reason, line: target.line }];
286
+ }
287
+ if (!target.exists) {
288
+ return [
289
+ {
290
+ file: target.file,
291
+ status: 'absent',
292
+ detail: `no baseline file; run: ${CLI} sync --yes`,
293
+ remedy: `${CLI} sync --yes`,
294
+ },
295
+ ];
296
+ }
297
+ const state = parse(target.previous, target.style);
298
+ if (state.status === 'absent') {
299
+ return [
300
+ {
301
+ file: target.file,
302
+ status: 'unmanaged',
303
+ detail: `the file exists with no simt-dev region; run: ${CLI} sync --yes`,
304
+ remedy: `${CLI} sync --yes`,
305
+ },
306
+ ];
307
+ }
308
+ const off = misplaced(state, target.style);
309
+ return [
310
+ { file: target.file, ...regionVerdict(target.action), recordedSha: state.sha ?? null },
311
+ ...(off ? [{ file: target.file, status: 'misplaced', ...off }] : []),
312
+ ];
313
+ }
314
+
315
+ const surface = target.surface ? { surface: target.surface } : {};
316
+
317
+ if (!target.exists) {
318
+ if (target.readError !== null && target.readError !== undefined && target.readError !== 'ENOENT') {
319
+ return [{ file: target.file, status: 'unreadable', detail: target.readError }];
320
+ }
321
+ return [
322
+ target.surface
323
+ ? {
324
+ file: target.file,
325
+ ...surface,
326
+ status: 'absent',
327
+ detail: `no ${target.surface} rule here; run: ${CLI} sync --yes`,
328
+ }
329
+ : { file: target.file, status: 'absent', detail: `no AGENTS.md; run: ${CLI} sync --yes` },
330
+ ];
331
+ }
332
+
333
+ const state = parse(target.previous, target.style);
334
+ if (state.status === 'malformed') {
335
+ return [{ file: target.file, ...surface, status: 'malformed', detail: state.reason, line: state.line }];
336
+ }
337
+ if (state.status === 'absent') {
338
+ return [
339
+ {
340
+ file: target.file,
341
+ ...surface,
342
+ status: 'unmanaged',
343
+ detail: `no generated region; run: ${CLI} sync --yes`,
344
+ },
345
+ ];
346
+ }
347
+ const finding = { file: target.file, ...surface, ...regionVerdict(target.action), recordedSha: state.sha ?? null };
348
+ // Only a region can say what it expected to find: a rule file's marker carries a
349
+ // source list instead, and inventing the field here would describe a shape it never has.
350
+ if (target.kind === 'region') finding.expectedSha = target.sha;
351
+ return [finding];
352
+ }
353
+
271
354
  /** Check one repo without writing. Returns findings rather than an exit code,
272
355
  * so commands/check decides severity and commands/sync decides whether to act. */
273
356
  export async function checkTargets(root, flags = {}) {
357
+ const projection = await plan(root, flags);
274
358
  const findings = [];
275
- const primary = await checkPrimary(root, flags);
276
- findings.push(primary);
277
- if (primary.status !== 'absent' && primary.status !== 'unreadable' && primary.status !== 'malformed') {
278
- let text = '';
279
- try {
280
- text = await readFile(join(root, PRIMARY_TARGET), 'utf8');
281
- } catch {
282
- text = '';
283
- }
284
- findings.push(...readLimitFindings(text, PRIMARY_TARGET));
285
- }
359
+ // The plan's own targets, in the order a reader meets them: the root region, then the
360
+ // packages. Nothing here rebuilds a file.
361
+ const primary = projection.targets.find((t) => t.kind === 'region' && t.file === PRIMARY_TARGET);
362
+ findings.push(...targetFindings(primary));
363
+ findings.push(...readLimitFindings(primary.previous ?? '', PRIMARY_TARGET));
286
364
 
287
365
  // Every package of a monorepo is a projection root in its own right, so the gate
288
366
  // covers it the way it covers the root. A nested region that drifts is as stale as
289
- // the root's region, and the reader working in that package reads only that file.
367
+ // the root region, and the reader working in that package reads only that file.
290
368
  //
291
369
  // Gated on the root having a region at all: a monorepo that has not adopted has no
292
370
  // nested regions either, and reporting one as absent would describe a half-adoption
293
371
  // that nobody performed.
294
- const { regions: packageBodies } = await packageRegions(root, flags);
295
- for (const region of packageBodies) {
296
- const file = `${region.path}/${PRIMARY_TARGET}`;
297
- findings.push({ ...(await checkRegion(root, join(root, region.path), region.body, region.meta)), file, profiles: region.profiles });
372
+ const profilesOf = new Map(projection.packages.map((p) => [p.path + '/' + PRIMARY_TARGET, p.profiles]));
373
+ for (const target of projection.targets.filter((t) => t.kind === 'region' && t.file !== PRIMARY_TARGET)) {
374
+ for (const finding of targetFindings(target)) {
375
+ findings.push({ ...finding, profiles: profilesOf.get(target.file) });
376
+ }
298
377
  }
299
378
 
300
379
  // The gate has to run where the merge happens, not only on the machine of whoever
@@ -410,35 +489,9 @@ export async function checkTargets(root, flags = {}) {
410
489
  const sha = await provenanceDigest();
411
490
  const notes = [];
412
491
 
413
- // The profiles that left the region, gated through the same construction that wrote
414
- // them. Without this loop a scoped file is planned and written but never checked, so a
415
- // repository that edits or deletes one shows a clean `check` and a stale reader — the
416
- // exact asymmetry the region-versus-file gate exists to prevent.
417
- const scopedRules = desired.scoped ?? [];
418
- if (desired.narrowing === true) {
419
- for (const target of (await scopedTargets(root, scopedRules, sha, resolved, notes)).targets) {
420
- if (!target.exists) {
421
- findings.push({
422
- file: target.file,
423
- surface: target.surface,
424
- status: 'absent',
425
- detail: `no ${target.surface} rule here; run: ${CLI} sync --yes`,
426
- });
427
- continue;
428
- }
429
- // The writer's own verdict, from the same inject call `plan` will make. A second
430
- // construction here would be a third place computing the same file, which is the
431
- // defect this whole function exists to avoid.
432
- const action = target.action;
433
- findings.push({
434
- file: target.file,
435
- surface: target.surface,
436
- ...regionVerdict(action),
437
- recordedSha: parse(target.previous).sha ?? null,
438
- });
439
- }
440
- }
441
-
492
+ // A rule with a defect is a gate failure even when its files happen to be correct,
493
+ // because the next `sync` will not reproduce it. The defect is a property of the
494
+ // rule; the verdict on a file is below, and it is the writer's own.
442
495
  for (const rule of rules) {
443
496
  if (rule.error !== null) {
444
497
  findings.push({
@@ -446,106 +499,20 @@ export async function checkTargets(root, flags = {}) {
446
499
  status: 'invalid',
447
500
  detail: rule.error,
448
501
  });
449
- continue;
450
- }
451
- // The writer's own construction, not a second one: a body rendered without the
452
- // surface-agreed substitutions would make `check` report drift on a tree that
453
- // `init` just wrote (S45's askTool).
454
- const { body, problems } = renderMdc(
455
- rule,
456
- ruleValues(rule, declared.values[rule.id] ?? {}, resolved.surfaces),
457
- signals,
458
- );
459
- if (problems.length > 0) {
460
- findings.push({
461
- file: rule.target,
462
- status: 'invalid',
463
- detail: problems[0],
464
- });
465
- continue;
466
502
  }
503
+ }
467
504
 
468
- for (const target of (await ruleTargets(root, rule, body, sha, resolved, notes)).targets) {
469
- const rel = target.file;
470
- if (!target.exists) {
471
- findings.push({
472
- file: rel,
473
- surface: target.surface,
474
- status: 'absent',
475
- detail: `no ${target.surface} rule here; run: ${CLI} sync --yes`,
476
- });
477
- continue;
478
- }
479
- const state = parse(target.previous);
480
- if (state.status === 'malformed') {
481
- findings.push({
482
- file: rel,
483
- surface: target.surface,
484
- status: 'malformed',
485
- detail: state.reason,
486
- line: state.line,
487
- });
488
- continue;
489
- }
490
- if (state.status === 'absent') {
491
- findings.push({
492
- file: rel,
493
- surface: target.surface,
494
- status: 'unmanaged',
495
- detail: `no generated region; run: ${CLI} sync --yes`,
496
- });
497
- continue;
498
- }
499
- // `target.action` is `ruleTargets`' own verdict, produced by the same inject
500
- // call `plan` will make. This used to reconstruct a reference here — existing
501
- // frontmatter plus a fresh inject — which was a THIRD construction of the same
502
- // file inside the one function whose entire job is to say whether the other two
503
- // agree. It happened to agree, which is what makes this duplication dangerous:
504
- // it is only detectable by the disagreement some future edit produces.
505
- const action = target.action;
506
- findings.push({
507
- file: rel,
508
- surface: target.surface,
509
- ...regionVerdict(action),
510
- recordedSha: state.sha ?? null,
511
- });
512
- }
505
+ // Every rule file the plan would write, in the plan's order: the surfaces' files,
506
+ // then the profiles that narrowed out of the region.
507
+ for (const target of projection.targets.filter((t) => t.surface)) {
508
+ findings.push(...targetFindings(target));
513
509
  }
514
510
 
515
511
  // Baselines are checked against the same construction `sync` writes from, and they
516
512
  // block for the same reason the rule files block: an adopting repo whose baseline was
517
513
  // deleted or reverted is a repo whose editors have quietly stopped agreeing.
518
- for (const t of (await baselineTargets(root, await provenanceDigest())).targets) {
519
- if (t.action === 'malformed') {
520
- findings.push({ file: t.file, status: 'malformed', detail: t.reason, line: t.line });
521
- continue;
522
- }
523
- if (!t.exists) {
524
- findings.push({
525
- file: t.file,
526
- status: 'absent',
527
- detail: `no baseline file; run: ${CLI} sync --yes`,
528
- remedy: `${CLI} sync --yes`,
529
- });
530
- continue;
531
- }
532
- const state = parse(t.previous, t.style);
533
- if (state.status === 'absent') {
534
- findings.push({
535
- file: t.file,
536
- status: 'unmanaged',
537
- detail: `the file exists with no simt-dev region; run: ${CLI} sync --yes`,
538
- remedy: `${CLI} sync --yes`,
539
- });
540
- continue;
541
- }
542
- findings.push({
543
- file: t.file,
544
- ...regionVerdict(t.action),
545
- recordedSha: state.sha ?? null,
546
- });
547
- const off = misplaced(state, t.style);
548
- if (off) findings.push({ file: t.file, status: 'misplaced', ...off });
514
+ for (const target of projection.targets.filter((t) => t.kind === 'baseline')) {
515
+ findings.push(...targetFindings(target));
549
516
  }
550
517
 
551
518
  // Managed rule files this run would NOT write, found by scanning the rule
@@ -665,72 +632,10 @@ async function orphanedRuleFiles(root, resolved) {
665
632
  }
666
633
 
667
634
  export async function checkTarget(root, flags = {}) {
668
- const findings = await checkPrimary(root, flags);
669
- return findings;
670
- }
671
-
672
- async function checkPrimary(root, flags = {}) {
673
- const { body, meta } = await desiredRegion(root, flags);
674
- return checkRegion(root, root, body, meta);
635
+ const projection = await plan(root, flags);
636
+ return targetFindings(projection.targets.find((t) => t.kind === 'region' && t.file === PRIMARY_TARGET))[0];
675
637
  }
676
638
 
677
- /**
678
- * One region, compared against what this simt-dev would write for it.
679
- *
680
- * `dir` is the projection root that owns the document: the repository for the root
681
- * region, a package for a nested one. `regionOpts` is read from the repository
682
- * either way, because placement is a repository-level decision.
683
- */
684
- async function checkRegion(root, dir, body, meta) {
685
- const path = join(dir, PRIMARY_TARGET);
686
- let existing = '';
687
- try {
688
- existing = await readFile(path, 'utf8');
689
- } catch (e) {
690
- // Only ENOENT is absence. A bare `catch` here once reported "no AGENTS.md" for a
691
- // repo that has one, because the failure was a ReferenceError from a missing
692
- // import — the gate answered confidently about a file it had never read. A check
693
- // that is wrong about existence is worse than one that crashes.
694
- if (e.code !== 'ENOENT') {
695
- return { file: PRIMARY_TARGET, status: 'unreadable', detail: e.code ?? String(e) };
696
- }
697
- return { file: PRIMARY_TARGET, status: 'absent', detail: `no AGENTS.md; run: ${CLI} sync --yes` };
698
- }
699
- const state = parse(existing);
700
- if (state.status === 'malformed') {
701
- return {
702
- file: PRIMARY_TARGET,
703
- status: 'malformed',
704
- detail: state.reason,
705
- line: state.line,
706
- };
707
- }
708
- if (state.status === 'absent') {
709
- return { file: PRIMARY_TARGET, status: 'unmanaged', detail: `no generated region; run: ${CLI} sync --yes` };
710
- }
711
- const d = diff(existing, body, meta, await regionOpts(root));
712
- return {
713
- file: PRIMARY_TARGET,
714
- ...regionVerdict(d.action),
715
- recordedSha: state.sha ?? null,
716
- expectedSha: meta.sha,
717
- };
718
- }
719
- /**
720
- * The bytes of always-on projection this repository carries, or null if it has not
721
- * adopted at all.
722
- *
723
- * Delegates to the meter rather than counting here, and that is the whole of the
724
- * design. A first version summed the region plus the rule files this function already
725
- * compares, which sounds cheaper and is wrong: the meter also counts the managed skills
726
- * and the `.mdc` files a reader loads, so the gate and the report came to different
727
- * totals for one repository. Two definitions of one budget is the defect ADR-034 was
728
- * filed about, rebuilt one layer up. The meter is the definition; the gate quotes it.
729
- *
730
- * The cost is that `check` now reads the documents the meter reads, which it did not
731
- * do before. That is a real cost, paid knowingly: the alternative is a gate that fails
732
- * on a number the report does not print, which is worse than a slower gate.
733
- */
734
639
  async function alwaysOnBytes(root) {
735
640
  const { exists } = await readManifest(root);
736
641
  if (!exists) return null;