simt-dev 0.7.0 → 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 (235) hide show
  1. package/CHANGELOG.md +191 -870
  2. package/README.md +188 -213
  3. package/bin/simt.mjs +19 -55
  4. package/docs/decisions/ADR-014-workspace-is-configuration.md +2 -2
  5. package/docs/decisions/ADR-025-a-baseline-is-a-region-in-a-file-the-repo-owns.md +1 -1
  6. package/docs/decisions/ADR-026-a-config-without-a-region-is-a-file-the-repo-owns.md +1 -1
  7. package/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +1 -1
  8. package/docs/decisions/ADR-028-a-base-file-is-a-floor-so-the-disagreement-it-cannot-repair-becomes-a-check.md +1 -1
  9. package/docs/decisions/ADR-029-the-pins-a-repository-declares-are-its-own.md +1 -1
  10. package/docs/decisions/ADR-030-what-a-plan-does-not-ship-is-a-decision-with-a-record.md +1 -1
  11. package/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +1 -1
  12. package/docs/decisions/ADR-082-the-tracker-override-is-a-manifest-key.md +1 -1
  13. package/docs/decisions/ADR-099-the-release-verifies-served-bytes-and-the-formula-mirrors-them.md +1 -1
  14. package/docs/decisions/ADR-117-the-darwin-release-builds-on-supported-macos-15-runners-and-a-re-run-is-idempotent.md +81 -0
  15. package/docs/decisions/ADR-118-the-bench-corpus-is-keyed-on-history-is-append-only-and-is-portable-as-a-sealed-document.md +91 -0
  16. package/docs/decisions/ADR-119-the-environment-variable-points-into-a-workspace-the-repo-declared.md +90 -0
  17. package/docs/decisions/ADR-120-a-pre-push-whose-tree-is-already-proven-green-skips-the-test-gate-on-that-evidence.md +96 -0
  18. package/docs/decisions/ADR-122-the-shims-move-into-the-state-directory-and-the-legacy-directory-removes-itself.md +94 -0
  19. package/docs/decisions/ADR-123-a-surplus-positional-is-not-refused-because-arity-is-not-in-the-source.md +57 -0
  20. package/docs/decisions/ADR-124-the-manifest-has-a-schema-in-the-tool-and-its-unknown-keys-stay-the-repositorys.md +64 -0
  21. package/docs/decisions/ADR-125-a-shared-context-belongs-to-a-read-and-a-writer-reads-directly.md +65 -0
  22. package/docs/decisions/ADR-126-the-mcp-surface-stays-a-reader-so-there-is-no-write-path-to-build.md +68 -0
  23. package/docs/decisions/ADR-127-the-mcp-tool-list-is-written-by-hand-and-roots-is-not-asked-for.md +65 -0
  24. package/docs/decisions/ADR-128-a-research-plan-is-a-plan-and-the-tracker-reads-it.md +42 -0
  25. package/ecosystems/bun.json +6 -0
  26. package/ecosystems/cargo.json +25 -0
  27. package/ecosystems/composer.json +6 -0
  28. package/ecosystems/dart.json +24 -0
  29. package/ecosystems/deno.json +21 -0
  30. package/ecosystems/devcontainers.json +6 -0
  31. package/ecosystems/docker-compose.json +6 -0
  32. package/ecosystems/docker.json +6 -0
  33. package/ecosystems/dotnet.json +25 -0
  34. package/ecosystems/elixir.json +23 -0
  35. package/ecosystems/gitsubmodule.json +6 -0
  36. package/ecosystems/go.json +25 -0
  37. package/ecosystems/gradle.json +13 -0
  38. package/ecosystems/maven.json +23 -0
  39. package/ecosystems/node.json +25 -0
  40. package/ecosystems/python.json +20 -0
  41. package/ecosystems/ruby.json +23 -0
  42. package/ecosystems/swift.json +25 -0
  43. package/ecosystems/terraform.json +6 -0
  44. package/ecosystems/uv.json +6 -0
  45. package/harnesses/antigravity.json +162 -0
  46. package/harnesses/claude.json +206 -0
  47. package/harnesses/codex.json +136 -0
  48. package/harnesses/copilot.json +88 -0
  49. package/harnesses/cursor.json +140 -0
  50. package/harnesses/deepseek.json +99 -0
  51. package/harnesses/gemini.json +122 -0
  52. package/harnesses/opencode.json +173 -0
  53. package/package.json +12 -2
  54. package/rules/core.md +7 -0
  55. package/rules/mdc/intake-questions.mdc +8 -0
  56. package/rules/mdc/slice-commit.mdc +57 -4
  57. package/src/advice/advice.mjs +3 -2
  58. package/src/agenthooks/prompt.mjs +5 -2
  59. package/src/agenthooks/render.mjs +86 -180
  60. package/src/agenthooks/shim.mjs +1 -1
  61. package/src/agents/render.mjs +15 -18
  62. package/src/api.mjs +31 -0
  63. package/src/argv.mjs +48 -3
  64. package/src/bench/ablate.mjs +3 -4
  65. package/src/bench/adapters.json +163 -105
  66. package/src/bench/adapters.mjs +466 -0
  67. package/src/bench/arms.mjs +333 -0
  68. package/src/bench/grade.mjs +33 -73
  69. package/src/bench/graders.mjs +42 -0
  70. package/src/bench/mine.mjs +121 -475
  71. package/src/bench/probe-ledger.mjs +149 -0
  72. package/src/bench/probes.catalog.mjs +918 -0
  73. package/src/bench/probes.mjs +79 -1064
  74. package/src/bench/render.mjs +111 -0
  75. package/src/bench/report.mjs +120 -962
  76. package/src/bench/run.mjs +185 -802
  77. package/src/bench/variants.mjs +4 -5
  78. package/src/bench/workspace.mjs +160 -0
  79. package/src/brief/assemble.mjs +48 -15
  80. package/src/ci/dependabot.mjs +25 -55
  81. package/src/ci/workflow.mjs +30 -7
  82. package/src/cli.mjs +45 -0
  83. package/src/commands/agent.mjs +4 -13
  84. package/src/commands/agenthook.mjs +70 -40
  85. package/src/commands/bench.mjs +191 -18
  86. package/src/commands/brief.mjs +5 -4
  87. package/src/commands/ci.mjs +13 -14
  88. package/src/commands/compare.mjs +42 -42
  89. package/src/commands/completions.mjs +2 -6
  90. package/src/commands/decide.mjs +4 -4
  91. package/src/commands/distill.mjs +4 -3
  92. package/src/commands/docs.mjs +88 -0
  93. package/src/commands/doctor.mjs +11 -7
  94. package/src/commands/evidence.mjs +6 -9
  95. package/src/commands/explain.mjs +0 -1
  96. package/src/commands/gate.mjs +26 -25
  97. package/src/commands/graph.mjs +2 -3
  98. package/src/commands/harness.mjs +6 -5
  99. package/src/commands/help.mjs +168 -19
  100. package/src/commands/hooks.mjs +119 -49
  101. package/src/commands/ignore.mjs +13 -8
  102. package/src/commands/impact.mjs +8 -15
  103. package/src/commands/index.mjs +318 -193
  104. package/src/commands/init-scan.mjs +90 -0
  105. package/src/commands/init.mjs +124 -105
  106. package/src/commands/lease.mjs +73 -25
  107. package/src/commands/lessons.mjs +10 -9
  108. package/src/commands/local.mjs +3 -3
  109. package/src/commands/map.mjs +24 -14
  110. package/src/commands/mcp.mjs +2 -18
  111. package/src/commands/pack.mjs +9 -6
  112. package/src/commands/plan.mjs +6 -5
  113. package/src/commands/plugin.mjs +2 -2
  114. package/src/commands/prbody.mjs +3 -3
  115. package/src/commands/repoindex.mjs +12 -11
  116. package/src/commands/review.mjs +2 -2
  117. package/src/commands/rules.mjs +37 -16
  118. package/src/commands/shared.mjs +14 -19
  119. package/src/commands/skills.mjs +33 -32
  120. package/src/commands/skillscompile.mjs +7 -8
  121. package/src/commands/skillsinstall.mjs +7 -6
  122. package/src/commands/slice.mjs +220 -74
  123. package/src/commands/spec.mjs +167 -0
  124. package/src/commands/status.mjs +27 -18
  125. package/src/commands/sync.mjs +27 -25
  126. package/src/commands/tests.mjs +18 -21
  127. package/src/commands/tokens.mjs +21 -26
  128. package/src/commands/undo.mjs +5 -15
  129. package/src/commands/upgrade.mjs +15 -7
  130. package/src/commands/why.mjs +15 -21
  131. package/src/compare/hooksstate.mjs +2 -6
  132. package/src/compare/learn.mjs +3 -1
  133. package/src/compare/row.mjs +0 -2
  134. package/src/context.mjs +64 -0
  135. package/src/decisions/index-doc.mjs +46 -0
  136. package/src/decisions/index.mjs +3 -2
  137. package/src/diff.mjs +1 -1
  138. package/src/distill/distill.mjs +14 -1
  139. package/src/distill/engine.mjs +5 -1
  140. package/src/docs/commands.mjs +65 -0
  141. package/src/ecosystem/records.mjs +219 -0
  142. package/src/explain.mjs +31 -0
  143. package/src/gates/index.mjs +385 -0
  144. package/src/graph/compile.mjs +8 -4
  145. package/src/graph/load.mjs +6 -2
  146. package/src/graph/spec.mjs +10 -8
  147. package/src/harness/records.mjs +143 -0
  148. package/src/hooks/capability.mjs +7 -5
  149. package/src/hooks/dispatcher.mjs +209 -60
  150. package/src/hooks/gates.mjs +10 -43
  151. package/src/hooks/resolve.mjs +4 -14
  152. package/src/hooks/shim.mjs +5 -2
  153. package/src/hooks/where.mjs +2 -7
  154. package/src/identity.mjs +31 -12
  155. package/src/index/coverage.mjs +58 -0
  156. package/src/index/extract.mjs +6 -16
  157. package/src/index/gate.mjs +1 -1
  158. package/src/index/govern.mjs +26 -9
  159. package/src/index/impact.mjs +4 -8
  160. package/src/index/map.mjs +14 -15
  161. package/src/index/open.mjs +173 -0
  162. package/src/index/persist.mjs +224 -57
  163. package/src/index/store.mjs +82 -39
  164. package/src/index/tests.mjs +2 -6
  165. package/src/journal.mjs +94 -20
  166. package/src/kernel/card.mjs +37 -63
  167. package/src/kernel/checks.mjs +4 -10
  168. package/src/lease.mjs +102 -7
  169. package/src/ledger/index.mjs +150 -14
  170. package/src/local/client.mjs +4 -6
  171. package/src/locate.mjs +33 -9
  172. package/src/manifest/schema.mjs +213 -0
  173. package/src/mcp/paginate.mjs +33 -0
  174. package/src/mcp/resources.mjs +7 -10
  175. package/src/mcp/schemas.mjs +512 -0
  176. package/src/mcp/server.mjs +119 -25
  177. package/src/mcp/tools-facts.mjs +326 -0
  178. package/src/mcp/tools-read.mjs +221 -0
  179. package/src/mcp/tools.mjs +4 -967
  180. package/src/measure/corpus.mjs +150 -0
  181. package/src/measure/metrics.mjs +236 -0
  182. package/src/measure/records.mjs +211 -0
  183. package/src/measure/session.mjs +3 -2
  184. package/src/measure/stats.mjs +538 -0
  185. package/src/measure/task-schema.mjs +125 -0
  186. package/src/measure/tasks.mjs +69 -0
  187. package/src/memory/decide.mjs +6 -15
  188. package/src/memory/lesson-text.mjs +61 -0
  189. package/src/memory/lessons.mjs +29 -82
  190. package/src/name.mjs +7 -0
  191. package/src/node-version.mjs +37 -0
  192. package/src/output/index.mjs +209 -10
  193. package/src/packs/manage.mjs +32 -12
  194. package/src/packs.mjs +20 -2
  195. package/src/plan/index.mjs +1 -1
  196. package/src/plugins.mjs +22 -16
  197. package/src/prbody.mjs +7 -11
  198. package/src/projection/loaders.mjs +7 -692
  199. package/src/projection/lock.mjs +11 -1
  200. package/src/projection/manifest.mjs +29 -468
  201. package/src/projection/mdc.mjs +7 -8
  202. package/src/projection/project.mjs +126 -221
  203. package/src/projection/region.mjs +561 -0
  204. package/src/projection/rules.mjs +20 -550
  205. package/src/projection/surfaces.mjs +43 -210
  206. package/src/projection/targets.mjs +5 -5
  207. package/src/repo-root.mjs +31 -0
  208. package/src/resolve/eval.mjs +2 -1
  209. package/src/resolve/fingerprint.mjs +19 -22
  210. package/src/resolve/resolve.mjs +1 -41
  211. package/src/review/critic.mjs +10 -13
  212. package/src/run.mjs +98 -0
  213. package/src/secrets.mjs +14 -22
  214. package/src/state/events.mjs +129 -0
  215. package/src/status-snapshot.mjs +233 -0
  216. package/src/status.mjs +2 -6
  217. package/src/sys/git.mjs +82 -0
  218. package/src/sys/proc.mjs +151 -0
  219. package/src/testing/parsers.mjs +265 -0
  220. package/src/testing/patches.mjs +23 -0
  221. package/src/testing/paths.mjs +49 -0
  222. package/src/testing/runners.mjs +186 -0
  223. package/src/testing/vault.mjs +68 -0
  224. package/src/text/frontmatter.mjs +49 -0
  225. package/src/text/tokens.mjs +52 -0
  226. package/src/toolwords.mjs +3 -0
  227. package/src/util/fs.mjs +20 -0
  228. package/src/util/sort.mjs +12 -0
  229. package/src/workspace.mjs +58 -11
  230. package/src/write/plan.mjs +82 -0
  231. package/templates/plan/tracker.json +1 -0
  232. package/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +0 -82
  233. package/docs/decisions/ADR-102-the-two-queued-review-rows-are-closed-by-ruling.md +0 -49
  234. package/docs/decisions/README.md +0 -600
  235. package/src/bench/tasks.mjs +0 -36
@@ -27,10 +27,13 @@ import { dirname, join, resolve } from 'node:path';
27
27
 
28
28
  import { fingerprint, KNOWN_TAGS } from '../resolve/fingerprint.mjs';
29
29
  import { loadMdcRules, renderMdc } from './mdc.mjs';
30
- import { foldsRules, frontmatterBlock, resolveSurfaces, rulePath, canScope } from './surfaces.mjs';
30
+ import { foldsRules, resolveSurfaces, rulePath, canScope } from './surfaces.mjs';
31
31
  import { ruleTargets } from './targets.mjs';
32
32
  import { compose } from './markers.mjs';
33
- import { declaredBudget, declaredShared, manifestPath, readManifest } from './manifest.mjs';
33
+ import { desiredRegion } from './region.mjs';
34
+ export { desiredRegion } from './region.mjs';
35
+ import { declaredBudget, declaredShared, readManifest } from './manifest.mjs';
36
+ import { loadManifest } from '../manifest/schema.mjs';
34
37
  import { commandCard, CARD_LIMIT_BYTES } from '../kernel/card.mjs';
35
38
  import { packToBudget } from '../kernel/budget.mjs';
36
39
  import { filterInferable, inferableDefect, inferableProofs } from '../kernel/inferability.mjs';
@@ -41,6 +44,7 @@ import { CLI, MANIFEST_FILE } from '../name.mjs';
41
44
  import { installedSkills } from './disclosure.mjs';
42
45
  import { PACK_DIR, resolvePacks } from '../packs.mjs';
43
46
  import { discoverPackages } from '../monorepo.mjs';
47
+ import { parseFrontmatter as splitBlock, stripFrontmatter as stripBlock } from '../text/frontmatter.mjs';
44
48
 
45
49
  const HERE = dirname(fileURLToPath(import.meta.url));
46
50
  export const ROOT = resolve(HERE, '..', '..');
@@ -255,7 +259,7 @@ export function isEligible(rule, signals, optedIn) {
255
259
  export async function readDeclared(root) {
256
260
  let parsed = {};
257
261
  try {
258
- parsed = JSON.parse(await readFile(await manifestPath(root), 'utf8')) ?? {};
262
+ parsed = (await loadManifest(root)).manifest ?? {};
259
263
  } catch {
260
264
  parsed = {};
261
265
  }
@@ -267,8 +271,6 @@ export async function readDeclared(root) {
267
271
  };
268
272
  }
269
273
 
270
-
271
-
272
274
  /**
273
275
  * Render every canonical `.mdc` rule into its own generated region.
274
276
  *
@@ -350,7 +352,7 @@ export function ruleValues(rule, declared, surfaces) {
350
352
  return values;
351
353
  }
352
354
 
353
- async function ruleBodies(root, manifest, flags) {
355
+ export async function ruleBodies(root, manifest, flags) {
354
356
  const problems = [];
355
357
  const notes = [];
356
358
  const skipped = [];
@@ -444,7 +446,7 @@ async function ruleBodies(root, manifest, flags) {
444
446
  * card comes back alongside because the caller that needs the proofs also needs the card's
445
447
  * bytes, and building it twice would read the manifest twice for no second answer.
446
448
  */
447
- async function inferableEvidence(root, { stack, facts, manifest }) {
449
+ export async function inferableEvidence(root, { stack, facts, manifest }) {
448
450
  const card = await commandCard({ root, manifest, stack, facts });
449
451
  return { card, proofs: inferableProofs({ facts, stack, card, manifest }) };
450
452
  }
@@ -465,540 +467,10 @@ async function inferableEvidence(root, { stack, facts, manifest }) {
465
467
  * shared file, which is how the first version of this predicate lost three of five rules
466
468
  * in a two-repository measurement without failing anything.
467
469
  */
468
- function toolRule(rule) {
470
+ export function toolRule(rule) {
469
471
  return rule.origin !== undefined ? rule.origin === 'central' : rule.source.startsWith('rules/');
470
472
  }
471
473
 
472
- export async function desiredRegion(root, flags) {
473
- const { stack, facts, manifest } = await fingerprint(root);
474
- const lockedVariants =
475
- flags?.variants !== undefined ? flags.variants : await readLockedVariants(root);
476
- const all = await loadRules(root);
477
- const { selected, fallback, notes } = select(all, stack, facts);
478
- const { resolved, bodies, problems, notes: ruleNotes } = await ruleBodies(
479
- root,
480
- manifest,
481
- { ...(flags ?? {}), variants: lockedVariants },
482
- );
483
- notes.push(...ruleNotes);
484
-
485
- // A profile that declares a scope leaves the region only when every selected surface
486
- // can load it narrowly. The alternative is a profile written only as a scoped file for
487
- // a reader whose surface cannot scope — a rule nobody loads, which is worse than one
488
- // that loads eagerly (ADR-071). A surface that cannot scope it therefore keeps the
489
- // profile in the region and says so, rather than letting the region quietly shrink.
490
- const narrowing = resolved.surfaces.every((s) => canScope(s));
491
- const scoped = [];
492
- const inRegion = [];
493
- for (const r of selected) {
494
- const declares = Array.isArray(r.scope) && r.scope.length > 0 && r.error === null;
495
- if (!declares) {
496
- inRegion.push(r);
497
- continue;
498
- }
499
- // A profile only leaves the region when a narrowed file can replace it. When any
500
- // selected surface cannot narrow, the file would be the only home for a rule that
501
- // reader cannot reach — so it stays, and says why.
502
- if (narrowing) {
503
- const vf = foldVariants(r.body, r.id ?? '', lockedVariants);
504
- scoped.push(vf.changed ? { ...r, body: vf.body } : r);
505
- continue;
506
- }
507
- inRegion.push(r);
508
- notes.push(
509
- `${r.id} declares simt-scope, but ${resolved.source} selects a surface that cannot narrow a rule, so it stays in the region`,
510
- );
511
- }
512
-
513
- // The kernel. In `kernel` mode only the hard and command classes are packed into the
514
- // region and every other rule is deferred to `rules show <id>`, which is what makes
515
- // R08's accept criterion reachable at all: a budget cannot be met by a projection that
516
- // never tries. `full` is the default and stays it until R02 has measured that a
517
- // kernel does not lose compliance, because nobody has measured it yet.
518
- //
519
- // A rule with no stated class is deferred, not packed. The two mistakes are not
520
- // symmetric: packing a line that should have been deferred spends the budget the mode
521
- // exists to save, while deferring a line that should have been packed costs a reader
522
- // one `rules show`. The mode is a promise about bytes, so it is the bytes that win.
523
- // An explicit `mode` flag overrides the declared one. The caller is the MCP
524
- // `simt://kernel` resource, which by name promises the kernel's bytes for any
525
- // repository: the declared mode governs what `sync` writes to disk, not what a read
526
- // path may look at, so the read renders in the mode the URI names instead of
527
- // re-reading a manifest it must not obey (ADR-002).
528
- const mode = typeof flags?.mode === 'string' ? flags.mode : await projectionMode(root);
529
- const packed = [];
530
- const deferred = [];
531
- for (const r of inRegion) {
532
- if (mode === 'kernel' && (r.error !== null || !KERNEL_CLASSES.has(r.priority))) deferred.push(r);
533
- else packed.push(r);
534
- }
535
- // The command card leads the region. The exact commands for this repository are the
536
- // bytes a reader cannot infer from rules that are identical everywhere (plan 0003 R10),
537
- // so they go first — above even the core rules. It is derived from committed evidence
538
- // only, in one place, so `plan` and `checkTarget` cannot disagree about it and two
539
- // machines render it byte-identically.
540
- const { card, proofs } = await inferableEvidence(root, { stack, facts, manifest });
541
- if (card.dropped.length > 0) {
542
- // Reported where every other projection note is reported. A card that lost a command
543
- // in silence is a card a reader cannot tell is incomplete.
544
- notes.push(
545
- `command card: dropped ${card.dropped.join(', ')} to stay within ${CARD_LIMIT_BYTES} bytes`,
546
- );
547
- }
548
- // Inferability (R09). A marked line leaves when the repository proves the evidence it
549
- // names. `shipped` is a separate list from `packed` because they are separate decisions:
550
- // the kernel is about which classes cost a reader nothing to skip, and this is about
551
- // which lines the reader already knows.
552
- const inferable = [];
553
- const shipped = [];
554
- const variantsSelected = [];
555
- let enforced = 0;
556
- for (const r of packed) {
557
- const variantFolded = foldVariants(r.body, r.id ?? '', lockedVariants);
558
- const filtered = filterInferable(variantFolded.body, proofs);
559
- const folded = foldChecks(filtered.body);
560
- enforced += folded.enforced.length;
561
- variantsSelected.push(...variantFolded.selected);
562
- if (!variantFolded.changed && !filtered.changed && !folded.changed) {
563
- shipped.push(r);
564
- continue;
565
- }
566
- for (const x of filtered.removed) inferable.push({ rule: r.id, text: x.text, expr: x.expr });
567
- shipped.push({ ...r, body: folded.body });
568
- }
569
- // A body that points the reader at a profile they cannot reach is a rule-text defect:
570
- // the region renders, but the reference dangles. Kept out of `problems` because that
571
- // array is the mdc list `sync` already reports, and one fault must not print twice.
572
- const crossRefs = [];
573
- const selectedIds = new Set(selected.map((r) => r.id));
574
- for (const r of shipped) {
575
- for (const p of crossRefProblems(r.body, all, selectedIds)) {
576
- crossRefs.push(`${r.id}: ${p}`);
577
- }
578
- }
579
- const lockedNonDefault = variantsSelected.filter((v) => v.locked && v.winner !== v.baseline);
580
- if (lockedNonDefault.length > 0) {
581
- notes.push(
582
- `variants: applied ${lockedNonDefault.length} locked variant(s) (${lockedNonDefault.map((v) => `${v.slotId}=${v.winner}`).join(', ')})`,
583
- );
584
- }
585
- if (enforced > 0) {
586
- notes.push(
587
- `enforced: ${enforced} line(s) left the region because a checker runs them`,
588
- );
589
- }
590
- if (inferable.length > 0) {
591
- // Never silent, and never a heading. A line that left with no trace repeats the
592
- // mistake ADR-073 rejected for deferred rules, and a `## Dropped` section spends region
593
- // bytes on lines the reader is being told not to read. A count they can act on, plus —
594
- // with `--explain` — the evidence behind each one, is the third option.
595
- notes.push(
596
- `inferability: dropped ${inferable.length} line(s) this repository already proves; ${CLI} sync --explain names each one and the evidence behind it`,
597
- );
598
- if (flags?.explain === true) {
599
- for (const x of inferable) {
600
- notes.push(`inferable: ${x.rule} dropped "${x.text}" — proved by ${x.expr}`);
601
- }
602
- }
603
- }
604
- const folded = foldsRules(resolved) ? bodies.map(({ rule }) => rule.id) : [];
605
-
606
- // The workspace-shared rules this repository declared (ADR-084).
607
- //
608
- // Read from the manifest and never from the reader's home directory, because this
609
- // function is what `sync` and `check` both call: a region that depended on what
610
- // somebody had installed would render differently on a machine where they had not,
611
- // and CI would report drift on a tree `sync` had just written. The ids travel in the
612
- // manifest so the answer to "what leaves this region" is a fact about the repository.
613
- const declared = declaredShared(manifest);
614
- const carriedIds = new Set(declared.level === null ? [] : [...declared.rules]);
615
-
616
- // Every rule the workspace carries, as the rule objects the index below names. Both
617
- // lists, because a surface that folds its rules into the primary document carries the
618
- // canonical process bodies in that document too — and a reader who cannot find
619
- // `slice-commit` anywhere near their repository will look for it in AGENTS.md.
620
- const carried = [
621
- ...shipped.filter((r) => carriedIds.has(r.id)).map((rule) => ({ rule, kind: 'profile' })),
622
- ...bodies.filter(({ rule }) => carriedIds.has(rule.id)).map(({ rule }) => ({ rule, kind: 'process' })),
623
- ];
624
-
625
- // The rules this root would carry in its region, with the bytes it would carry them
626
- // in — the input ADR-084's workspace intersection compares.
627
- //
628
- // Computed from `shipped` rather than from what the region ends up carrying, and that
629
- // is the whole reason it is computed at all: a rule already declared shared must still
630
- // appear here, or a second `shared install` would intersect against a list that no
631
- // longer contained it and shrink the shared file every time anybody ran the command.
632
- // The declaration is an input to the region; it is not an input to what is shareable.
633
- const shareable = [
634
- // Only the rules this package ships. A repository's own profile, or a pack it
635
- // installed, is policy somebody chose to write: carrying it into a shared home file
636
- // would mean editing that file to change a repository's rules, and the copy would
637
- // reach every other repository in the estate that declared the same id. A rule
638
- // somebody can override must stay somewhere they own.
639
- ...shipped
640
- .filter((r) => r.error === null && toolRule(r) && typeof r.id === 'string' && r.id !== '')
641
- .map((r) => ({
642
- id: r.id,
643
- source: r.source,
644
- priority: r.priority ?? null,
645
- rendered: frameSource(r, stripFrontmatter(r.body).trim(), 'profile'),
646
- })),
647
- // The folded process bodies, and only while they are folded. A surface that writes
648
- // one file per rule expects that file at a path inside the repository, so a
649
- // user-level copy cannot stand in for it — but when the surface folds the whole region
650
- // into the primary document, those bytes are as repeated across a workspace as the
651
- // profiles are, and `tokens --repos` named them as the last duplicate block in a
652
- // two-repository measurement.
653
- ...bodies
654
- .filter(({ rule }) => folded.includes(rule.id) && toolRule(rule))
655
- .map(({ rule, body }) => ({
656
- id: rule.id,
657
- source: rule.source,
658
- priority: rule.priority ?? null,
659
- rendered: frameSource(rule, body, 'process'),
660
- })),
661
- ].filter((r) => r.rendered !== '');
662
-
663
- // The selected rules that are not shareable, with the reason. `shared plan` reporting
664
- // only what it carries would imply every profile in the workspace had been accounted
665
- // for, and a reader whose own house rule is missing from the list would conclude it
666
- // moved — so the names it did not take, and why, are data rather than an omission.
667
- const shareableIds = new Set(shareable.map((r) => r.id));
668
- const excluded = selected
669
- .filter((r) => typeof r.id === 'string' && r.id !== '' && !shareableIds.has(r.id))
670
- .map((r) => ({
671
- id: r.id,
672
- why: r.error !== null ? 'error' : toolRule(r) ? 'empty' : 'policy',
673
- }));
674
-
675
- // The budget, and the bytes held back for what loads beside the region.
676
- //
677
- // Read from the manifest rather than from the plan's spelling of it. Plan 0003 R08
678
- // wrote `projection.budget.bytes`, and ADR-072 moved the key to the top level because
679
- // `projection` is a generated key `upgrade` replaces wholesale - a budget that reset
680
- // itself on upgrade would reset the reader's context size without anyone asking. The
681
- // plan is not edited: the key it names is wrong, and a ruling that supersedes it is
682
- // the record of why.
683
- const budget = declaredBudget(manifest);
684
- // Names and triggers only. Read once, outside the packer, because a skill does not
685
- // change as rules are cut and reading its body here would put the cost back into the
686
- // region the index exists to keep it out of (ADR-085).
687
- const skills = await installedSkills(root);
688
-
689
- // The digest is needed before the region can be weighed rather than after, because the
690
- // provenance header is part of what lands in the file. `provenanceDigest` is a pure
691
- // function of the bytes this package ships, so computing it once and passing it down is
692
- // the same value the tail of this function was getting on its own.
693
- const sha = await provenanceDigest();
694
-
695
- /**
696
- * The region, assembled from whatever survived the budget (ADR-083).
697
- *
698
- * One function, called once for the whole projection and again for each attempt the
699
- * packer makes, so a cut region and an uncut one are the same document by construction
700
- * rather than by two functions agreeing. The card, the inferability notes, the folded
701
- * process bodies and the disclosure index are not candidates for a cut: a budget is a
702
- * decision about rules, and dropping the index would hide the rules the cut removed.
703
- */
704
- const assemble = (cut) => {
705
- // A rule the workspace carries once leaves the region, and so does a rule the packer
706
- // could not afford. Both are exclusions from the same list, so the section below can
707
- // name all three reasons — which is the only reason the section exists: a reader who
708
- // opens AGENTS.md and does not find a rule they know is there will look for it.
709
- const kept = shipped.filter((r) => !cut.has(r.id) && !carriedIds.has(r.id));
710
- // Stable bytes first (ADR-086). Core, then the profiles this package ships, then
711
- // everything this repository added — the card, its own profiles, the process
712
- // bodies. A line the repository edits then changes only the tail, and the prefix
713
- // a reader has cached stays byte-identical. The card used to lead (ADR-077 ruling
714
- // 1); it now leads the tail, because a command this repository owns is the thing
715
- // an edit is most likely to move.
716
- const stable = kept.filter((r) => toolRule(r)).sort(stableOrder);
717
- const repoRules = kept.filter((r) => !toolRule(r)).sort((a, b) => String(a.id).localeCompare(String(b.id)));
718
- let text = render(stable);
719
- if (card.text) text += `${text === '' ? '' : '\n\n'}${card.text}`;
720
- // The pointer follows the card: the card says what this repository runs, and the
721
- // pointer says how the tool serves a task. Adjacent, they read as one block of
722
- // repo-facing advice instead of two facts the reader has to connect.
723
- text += `${text === '' ? '' : '\n\n'}${ORIENTATION_POINTER}`;
724
- const repoText = render(repoRules);
725
- if (repoText) text += `\n\n${repoText}`;
726
- const sources = [...stable, ...repoRules].map((r) => r.source);
727
-
728
- // Every reason a rule is missing lands in this one section, and each line says which
729
- // it was. A rule deferred by its class, a rule cut to reach a byte count and a rule
730
- // the workspace carries once are three different decisions, but they are the same fact
731
- // to a reader — it is not in this file — and three headings would spend region bytes
732
- // on structure nobody can act on. The line names the command that shows the rule,
733
- // because for all three the remedy is the same single read.
734
- //
735
- // The label is the rule's own frame title, which is why each entry carries the kind it
736
- // is rendered as: a canonical rule that the surface folds in is `Process: <id>` in the
737
- // shared file, and an index that calls it `Stack:` sends the reader looking for the
738
- // wrong thing.
739
- const omitted = [
740
- ...deferred.map((r) => ({ rule: r, kind: 'profile', why: `${r.priority ?? 'unclassified'}, deferred in kernel mode` })),
741
- ...shipped
742
- .filter((r) => cut.has(r.id))
743
- .map((r) => ({
744
- rule: r,
745
- kind: 'profile',
746
- why: `${r.priority ?? 'unclassified'}, cut to fit the declared budget of ${budget} bytes`,
747
- })),
748
- ...carried.map(({ rule, kind }) => ({
749
- rule,
750
- kind,
751
- // No path here, deliberately. This text is rendered by `sync` in CI, where the
752
- // reader's home directory is somebody else's, and a path is the one thing in a
753
- // remedy that cannot be true on both. The remedy is a command that works on the
754
- // machine the reader is on.
755
- why: `${rule.priority ?? 'unclassified'}, carried once per workspace in your reader's user-level rules file instead; if your reader has no copy, run: ${CLI} shared install`,
756
- })),
757
- ];
758
- if (omitted.length > 0) {
759
- // The pointer is the same shape as the scoped one above, and for the same reason:
760
- // a reader who opens AGENTS.md and does not find a rule they know is there will
761
- // look for it. What it costs to find it has to be one command, named per rule.
762
- const lines = omitted
763
- .sort((a, b) => byPriorityThenId(a.rule, b.rule))
764
- .map(
765
- ({ rule, kind, why }) =>
766
- `- ${sourceTitle(rule, kind)} — ${why}. Show it with: ${CLI} rules show ${rule.id}`,
767
- );
768
- // `## Rules not in this region`, not `## Deferred rules`. The heading was written
769
- // when there was one reason to be here (ADR-073) and it is region text a reader
770
- // reads, so a heading that names one of three reasons misdescribes the two others
771
- // (ADR-084).
772
- text += `\n\n## Rules not in this region\n\n${lines.join('\n')}`;
773
- for (const { rule } of omitted) sources.push(rule.source);
774
- }
775
-
776
- if (folded.length > 0) {
777
- for (const { rule, body: ruleBody } of bodies) {
778
- // A body the workspace carries once is not folded in here either. The heading is
779
- // about how the surface loads the region, so it stays; the rule under it does not,
780
- // and a region that carried the same bytes twice is the redundancy ADR-084 exists
781
- // to remove — reintroduced by the feature that removes it.
782
- if (carriedIds.has(rule.id)) continue;
783
- // `## Process:`, not `### id`. The body already uses `##`, and a third-level
784
- // heading in front of a second-level one is the break a reader cannot repair.
785
- text += `\n\n${frameSource(rule, ruleBody, 'process')}`;
786
- sources.push(rule.source);
787
- }
788
- }
789
-
790
- // Last, because this is the index of what the file deliberately does not contain
791
- // (ADR-085). A scoped profile keeps the pointer ADR-071 requires — its patterns and
792
- // the files it moved to — and a skill keeps its name and the description that says
793
- // when to open it. The body of either is the thing that was moved out.
794
- const disclosure = [];
795
- if (scoped.length > 0 && narrowing) {
796
- for (const r of scoped) {
797
- disclosure.push(
798
- `- ${sourceTitle(r, 'profile')} — loads only when a matching file is in play (${r.scope.join(', ')}): ${scopedPaths(r, resolved).join(', ')}`,
799
- );
800
- sources.push(r.source);
801
- }
802
- }
803
- const skillNames = new Map();
804
- for (const skill of skills) skillNames.set(skill.name, (skillNames.get(skill.name) ?? 0) + 1);
805
- for (const skill of skills) {
806
- const label = skillNames.get(skill.name) > 1 ? `${skill.name} (${skill.dir})` : skill.name;
807
- disclosure.push(
808
- skill.trigger === ''
809
- ? `- ${label} — no description, so nothing says when to open it. Show it with: ${CLI} skills show ${skill.name}`
810
- : `- ${label} — ${skill.trigger}`,
811
- );
812
- }
813
- if (disclosure.length > 0) text += `\n\n## Disclosure\n\n${disclosure.join('\n')}`;
814
-
815
- return { body: text, sources };
816
- };
817
-
818
- // What the region will weigh in the file it lands in, measured the way the meter
819
- // measures it: the composed region text, markers and provenance header included. One
820
- // byte conservative, because the file's own trailing newline falls outside the span —
821
- // and conservative in that direction is the only direction a ceiling can afford.
822
- const regionBytes = (assembled) =>
823
- Buffer.byteLength(compose(assembled.body, { sha, sources: assembled.sources }));
824
-
825
- let assembled = assemble(new Set());
826
- let packing = null;
827
- if (mode === 'kernel') {
828
- const reserve = await budgetReserve(root, resolved, bodies, sha);
829
- const result = packToBudget({
830
- // Least valuable first: the comparator the deferred section already uses, reversed.
831
- // Class decides and the id breaks the tie, so the cut order is a fact about the
832
- // repository rather than about the order the filesystem listed files in.
833
- //
834
- // The workspace-shared rules are not candidates, and that is not a rounding: cutting
835
- // one cannot make the region smaller by more than the index line it adds to the
836
- // section below, because the rule is carried once per workspace whatever this region
837
- // does. A packer that spent its budget on them would report a saving it did not make.
838
- candidates: shipped
839
- .filter((r) => !carriedIds.has(r.id))
840
- .sort((a, b) => -byPriorityThenId(a, b)),
841
- ceiling: budget - reserve,
842
- costs: (cut) => regionBytes(assemble(cut)),
843
- });
844
- packing = { ...result, bytes: budget, ceiling: budget - reserve, reserve };
845
- if (result.cut.length > 0) assembled = assemble(new Set(result.cut));
846
- if (result.cut.length > 0) {
847
- notes.push(
848
- `budget: cut ${result.cut.length} kernel rule(s) — ${result.cut.join(', ')} — from ${result.before} to ${result.after} bytes against a declared budget of ${budget}, with ${reserve} reserved for the generated files that load beside the region`,
849
- );
850
- }
851
- if (result.floorHeld) {
852
- // The one case the packer cannot fix, said where the reader will see it. `hard` is
853
- // the floor, so the bytes still over the line are this repository's own hard
854
- // constraints and the only levers left are its own: a larger budget, or a rule that
855
- // is not as hard as it is declared. ADR-072's kernel finding says both; this says
856
- // they were tried first.
857
- notes.push(
858
- `budget: still ${result.after} bytes after every rule outside the hard class was cut, against a ceiling of ${budget - reserve}; a hard constraint is not cut to reach a byte count, so the kernel itself is over the line`,
859
- );
860
- }
861
- if (result.floorHeld && reserve >= budget) {
862
- // A ceiling of zero or less is arithmetically true and useless to read: it says the
863
- // region is allowed no bytes, without saying that the files loading beside it have
864
- // already spent the whole budget. One number in that manifest is the only lever, and
865
- // a reader left to infer it from a subtraction is a reader who will demote a hard
866
- // rule to fix a budget that was set below the files it covers.
867
- notes.push(
868
- `budget: the ${reserve} bytes reserved for the generated files that load beside the region already reach the ${budget} declared, so no cut order meets this budget and the region is cut as far as the hard class allows`,
869
- );
870
- }
871
- }
872
-
873
- const meta = {
874
- sha,
875
- sources: assembled.sources,
876
- };
877
- return {
878
- stack,
879
- facts,
880
- manifest,
881
- all,
882
- profiles: selected.map((r) => r.id),
883
- folded,
884
- mode,
885
- deferred: deferred.map((r) => r.id),
886
- // What the workspace carries once per reader instead of here (ADR-084), as ids. The
887
- // declaration is what `check` and `sync` both read to decide the region, so the ids
888
- // that actually left are reported for the same reason the budget's cuts are: a rule
889
- // that disappeared needs a name in the output, or a reader looks for it in vain.
890
- carried: carried.map(({ rule }) => rule.id),
891
- // What this root could contribute to a workspace's shared file, with the bytes it
892
- // would contribute. Read by `simt shared`, never by the projection — which is why
893
- // computing it costs a frame per rule and buys nothing on `sync`.
894
- shareable,
895
- // What it selected and cannot contribute, with the reason. Reported so that a rule
896
- // staying in the region is always a decision somebody can see rather than an absence.
897
- excluded,
898
- // What the budget cost this projection (ADR-083), or null in `full` mode where no
899
- // ceiling is applied because nothing is cut. The finding and `simt tokens` both read
900
- // this rather than re-deriving it, so a cut cannot be reported one way and measured
901
- // another.
902
- budget: packing === null
903
- ? null
904
- : {
905
- bytes: packing.bytes,
906
- ceiling: packing.ceiling,
907
- reserve: packing.reserve,
908
- before: packing.before,
909
- after: packing.after,
910
- floorHeld: packing.floorHeld,
911
- cuts: shipped
912
- .filter((r) => packing.cut.includes(r.id))
913
- .map((r) => ({ id: r.id, priority: r.priority ?? 'unclassified' })),
914
- },
915
- // The marked lines that left, each with the evidence that dropped it. The region is
916
- // written without them; `--explain` is what reads this list, so a removal is reportable
917
- // without spending region bytes and without a second computation to find it.
918
- inferable,
919
- // The profiles that left the region, and the surfaces that accepted them as files.
920
- // `plan` writes them and `checkTarget` gates them through this list, so a projected
921
- // file and a drifted one are decided by the same computation.
922
- scoped,
923
- narrowing,
924
- surfaces: resolved.ids,
925
- surfaceSource: resolved.source,
926
- problems,
927
- crossRefs,
928
- fallback,
929
- notes,
930
- body: assembled.body,
931
- meta,
932
- };
933
- }
934
-
935
- /**
936
- * The bytes held back from the region's own ceiling for the always-on files that load
937
- * beside it.
938
- *
939
- * The budget is one number for a repository, not for one file, so packing the region
940
- * against the whole number would ship a projection that fits on its own and pushes the
941
- * total over the line the moment its rule files are written. Both terms below are lower
942
- * bounds on what that costs, and the larger is taken because the failure each one covers
943
- * is the one that must not happen: under-reserving reports compliance for a projection
944
- * that is over budget.
945
- *
946
- * The region's own span is deliberately not in either. It is the one number that moves
947
- * when this projection is packed, and a ceiling that depended on the thing it governs
948
- * would rewrite the region on the next run and report drift on a tree `sync` had just
949
- * written - the step-6 defect, rebuilt one layer up.
950
- *
951
- * @param {string} root
952
- * @param {object} resolved the surfaces this projection writes for
953
- * @param {{rule: object, body: string}[]} bodies the rule bodies being projected
954
- * @param {string} sha the provenance digest those files will be stamped with
955
- * @returns {Promise<number>}
956
- */
957
- async function budgetReserve(root, resolved, bodies, sha) {
958
- // Imported here rather than at the top of the module: `measure/context.mjs` reaches back
959
- // into `projection/project.mjs` for the read-limit findings, so a static import from
960
- // `rules.mjs` closes a loop through three modules that all have to survive being loaded
961
- // in any order. The loop between the other two already exists and is load-bearing in
962
- // neither direction; this one is walked only when a kernel is being packed.
963
- const { measureRoot } = await import('../measure/context.mjs');
964
- const measured = await measureRoot(root);
965
- const region = measured.documents.find((d) => d.channel === 'region');
966
- const installed = Math.max(0, measured.generatedBytes - (region?.generatedBytes ?? 0));
967
-
968
- // What this projection is about to write, for a repository that has not written it yet.
969
- // `ruleTargets` is the one place a rule file is turned into bytes, so the number comes
970
- // from there rather than from a second measurement of the same file — and its notes go
971
- // nowhere, because `mdcTargets` reports the same ones for the same files in the same run.
972
- let planned = 0;
973
- for (const { rule, body } of bodies) {
974
- for (const target of (await ruleTargets(root, rule, body, sha, resolved, [])).targets) {
975
- planned += target.bytes;
976
- }
977
- }
978
- return Math.max(installed, planned);
979
- }
980
-
981
- /**
982
- * Every per-rule file a scoped profile is written to, as paths a reader can open.
983
- *
984
- * Only the surfaces that can scope get one: a Cursor `.mdc` with `globs` and
985
- * `alwaysApply: false`, an Antigravity file with `trigger: glob`, a Copilot
986
- * `.instructions.md` with `applyTo`. A surface that reads the region already (opencode)
987
- * has no file channel at all, and Claude's scoping key is the documented-defect one,
988
- * so neither appears here.
989
- */
990
- function scopedPaths(rule, resolved) {
991
- const out = [];
992
- for (const s of resolved.surfaces) {
993
- if (!canScope(s) || !s.rule) continue;
994
- // A folded surface already carries the whole region; it is the reason this profile
995
- // is only a file when folding is off, which is the caller's question to answer.
996
- if (s.foldRules === true && s.readsPrimary === true && foldsRules(resolved)) continue;
997
- out.push(rulePath(s.id, rule.id));
998
- }
999
- return out;
1000
- }
1001
-
1002
474
  /**
1003
475
  * Read every rule file once, with its frontmatter parsed, plus any repo-local
1004
476
  * profile under `.simt/profiles/`.
@@ -1287,7 +759,7 @@ export function render(selected) {
1287
759
  * yet says a kernel does not lose compliance (plan 0003 R02), so the mode that changes
1288
760
  * what a reader gets cannot be the one nobody chose.
1289
761
  */
1290
- async function projectionMode(root) {
762
+ export async function projectionMode(root) {
1291
763
  const { manifest } = await readManifest(root);
1292
764
  return manifest?.mode === 'kernel' ? 'kernel' : 'full';
1293
765
  }
@@ -1301,7 +773,7 @@ async function projectionMode(root) {
1301
773
  */
1302
774
  /** Core, then the remaining shipped profiles by id. Id, not load order: a directory
1303
775
  * listing is not a fact about the rules, and two machines must not disagree about it. */
1304
- function stableOrder(a, b) {
776
+ export function stableOrder(a, b) {
1305
777
  if (a.id === 'core') return b.id === 'core' ? 0 : -1;
1306
778
  if (b.id === 'core') return 1;
1307
779
  return String(a.id).localeCompare(String(b.id));
@@ -1325,7 +797,7 @@ export function byPriorityThenId(a, b) {
1325
797
  * substitutes it, so a rename of the file is a change in one place (MANIFEST_FILE),
1326
798
  * not a search across every rule body.
1327
799
  */
1328
- function frameSource(rule, body, kind) {
800
+ export function frameSource(rule, body, kind) {
1329
801
  const text = demoteHeadings(body.replaceAll('{manifest}', MANIFEST_FILE)).trim();
1330
802
  if (!text) return '';
1331
803
  const title = sourceTitle(rule, kind);
@@ -1333,7 +805,7 @@ function frameSource(rule, body, kind) {
1333
805
  return `## ${title}\n\n${text}`;
1334
806
  }
1335
807
 
1336
- function sourceTitle(rule, kind) {
808
+ export function sourceTitle(rule, kind) {
1337
809
  if (!rule?.id) return null;
1338
810
  if (kind === 'process') return `Process: ${rule.id}`;
1339
811
  if (rule.id === 'core') return 'Core';
@@ -1348,7 +820,7 @@ function sourceTitle(rule, kind) {
1348
820
  * backtick-quoted `<id> profile` in prose, and the `simt rules show <id>`
1349
821
  * command itself - an id that form names must exist, or the command 404s.
1350
822
  */
1351
- function crossRefProblems(body, all, selectedIds) {
823
+ export function crossRefProblems(body, all, selectedIds) {
1352
824
  const problems = [];
1353
825
  const seen = new Set();
1354
826
  for (const m of String(body).matchAll(/`([a-z0-9][a-z0-9-]*)`\s+profile|rules show ([a-z0-9][a-z0-9-]*)/g)) {
@@ -1385,10 +857,10 @@ function demoteHeadings(text) {
1385
857
  }
1386
858
 
1387
859
  function parseFrontmatter(text) {
1388
- const m = /^---\n([\s\S]*?)\n---\n?/.exec(text);
1389
- if (!m) return {};
860
+ const { front } = splitBlock(text);
861
+ if (front === null) return {};
1390
862
  const out = {};
1391
- for (const line of m[1].split('\n')) {
863
+ for (const line of front.split('\n')) {
1392
864
  const kv = /^([a-zA-Z-]+):\s*(.*)$/.exec(line);
1393
865
  if (!kv) continue;
1394
866
  const [, key, raw] = kv;
@@ -1414,12 +886,10 @@ function parseFrontmatter(text) {
1414
886
 
1415
887
  /** Frontmatter and the duplicated H1 are dropped: the region is embedded in a
1416
888
  * host document that already has a title, so repeating it wastes context. */
1417
- function stripFrontmatter(text) {
889
+ export function stripFrontmatter(text) {
1418
890
  // The frontmatter strip must also swallow the blank lines that follow it, or
1419
891
  // the H1 strip's `^` anchor lands on a newline and silently matches nothing.
1420
- return text
1421
- .replace(/^---\n[\s\S]*?\n---\n*/, '')
892
+ return stripBlock(text)
1422
893
  .replace(/^#\s+.*\n+/, '')
1423
894
  .trim();
1424
895
  }
1425
-