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.
- package/CHANGELOG.md +191 -870
- package/README.md +188 -213
- package/bin/simt.mjs +19 -55
- package/docs/decisions/ADR-014-workspace-is-configuration.md +2 -2
- package/docs/decisions/ADR-025-a-baseline-is-a-region-in-a-file-the-repo-owns.md +1 -1
- package/docs/decisions/ADR-026-a-config-without-a-region-is-a-file-the-repo-owns.md +1 -1
- package/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +1 -1
- package/docs/decisions/ADR-028-a-base-file-is-a-floor-so-the-disagreement-it-cannot-repair-becomes-a-check.md +1 -1
- package/docs/decisions/ADR-029-the-pins-a-repository-declares-are-its-own.md +1 -1
- package/docs/decisions/ADR-030-what-a-plan-does-not-ship-is-a-decision-with-a-record.md +1 -1
- package/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +1 -1
- package/docs/decisions/ADR-082-the-tracker-override-is-a-manifest-key.md +1 -1
- package/docs/decisions/ADR-099-the-release-verifies-served-bytes-and-the-formula-mirrors-them.md +1 -1
- package/docs/decisions/ADR-117-the-darwin-release-builds-on-supported-macos-15-runners-and-a-re-run-is-idempotent.md +81 -0
- 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
- package/docs/decisions/ADR-119-the-environment-variable-points-into-a-workspace-the-repo-declared.md +90 -0
- package/docs/decisions/ADR-120-a-pre-push-whose-tree-is-already-proven-green-skips-the-test-gate-on-that-evidence.md +96 -0
- package/docs/decisions/ADR-122-the-shims-move-into-the-state-directory-and-the-legacy-directory-removes-itself.md +94 -0
- package/docs/decisions/ADR-123-a-surplus-positional-is-not-refused-because-arity-is-not-in-the-source.md +57 -0
- package/docs/decisions/ADR-124-the-manifest-has-a-schema-in-the-tool-and-its-unknown-keys-stay-the-repositorys.md +64 -0
- package/docs/decisions/ADR-125-a-shared-context-belongs-to-a-read-and-a-writer-reads-directly.md +65 -0
- package/docs/decisions/ADR-126-the-mcp-surface-stays-a-reader-so-there-is-no-write-path-to-build.md +68 -0
- package/docs/decisions/ADR-127-the-mcp-tool-list-is-written-by-hand-and-roots-is-not-asked-for.md +65 -0
- package/docs/decisions/ADR-128-a-research-plan-is-a-plan-and-the-tracker-reads-it.md +42 -0
- package/ecosystems/bun.json +6 -0
- package/ecosystems/cargo.json +25 -0
- package/ecosystems/composer.json +6 -0
- package/ecosystems/dart.json +24 -0
- package/ecosystems/deno.json +21 -0
- package/ecosystems/devcontainers.json +6 -0
- package/ecosystems/docker-compose.json +6 -0
- package/ecosystems/docker.json +6 -0
- package/ecosystems/dotnet.json +25 -0
- package/ecosystems/elixir.json +23 -0
- package/ecosystems/gitsubmodule.json +6 -0
- package/ecosystems/go.json +25 -0
- package/ecosystems/gradle.json +13 -0
- package/ecosystems/maven.json +23 -0
- package/ecosystems/node.json +25 -0
- package/ecosystems/python.json +20 -0
- package/ecosystems/ruby.json +23 -0
- package/ecosystems/swift.json +25 -0
- package/ecosystems/terraform.json +6 -0
- package/ecosystems/uv.json +6 -0
- package/harnesses/antigravity.json +162 -0
- package/harnesses/claude.json +206 -0
- package/harnesses/codex.json +136 -0
- package/harnesses/copilot.json +88 -0
- package/harnesses/cursor.json +140 -0
- package/harnesses/deepseek.json +99 -0
- package/harnesses/gemini.json +122 -0
- package/harnesses/opencode.json +173 -0
- package/package.json +12 -2
- package/rules/core.md +7 -0
- package/rules/mdc/intake-questions.mdc +8 -0
- package/rules/mdc/slice-commit.mdc +57 -4
- package/src/advice/advice.mjs +3 -2
- package/src/agenthooks/prompt.mjs +5 -2
- package/src/agenthooks/render.mjs +86 -180
- package/src/agenthooks/shim.mjs +1 -1
- package/src/agents/render.mjs +15 -18
- package/src/api.mjs +31 -0
- package/src/argv.mjs +48 -3
- package/src/bench/ablate.mjs +3 -4
- package/src/bench/adapters.json +163 -105
- package/src/bench/adapters.mjs +466 -0
- package/src/bench/arms.mjs +333 -0
- package/src/bench/grade.mjs +33 -73
- package/src/bench/graders.mjs +42 -0
- package/src/bench/mine.mjs +121 -475
- package/src/bench/probe-ledger.mjs +149 -0
- package/src/bench/probes.catalog.mjs +918 -0
- package/src/bench/probes.mjs +79 -1064
- package/src/bench/render.mjs +111 -0
- package/src/bench/report.mjs +120 -962
- package/src/bench/run.mjs +185 -802
- package/src/bench/variants.mjs +4 -5
- package/src/bench/workspace.mjs +160 -0
- package/src/brief/assemble.mjs +48 -15
- package/src/ci/dependabot.mjs +25 -55
- package/src/ci/workflow.mjs +30 -7
- package/src/cli.mjs +45 -0
- package/src/commands/agent.mjs +4 -13
- package/src/commands/agenthook.mjs +70 -40
- package/src/commands/bench.mjs +191 -18
- package/src/commands/brief.mjs +5 -4
- package/src/commands/ci.mjs +13 -14
- package/src/commands/compare.mjs +42 -42
- package/src/commands/completions.mjs +2 -6
- package/src/commands/decide.mjs +4 -4
- package/src/commands/distill.mjs +4 -3
- package/src/commands/docs.mjs +88 -0
- package/src/commands/doctor.mjs +11 -7
- package/src/commands/evidence.mjs +6 -9
- package/src/commands/explain.mjs +0 -1
- package/src/commands/gate.mjs +26 -25
- package/src/commands/graph.mjs +2 -3
- package/src/commands/harness.mjs +6 -5
- package/src/commands/help.mjs +168 -19
- package/src/commands/hooks.mjs +119 -49
- package/src/commands/ignore.mjs +13 -8
- package/src/commands/impact.mjs +8 -15
- package/src/commands/index.mjs +318 -193
- package/src/commands/init-scan.mjs +90 -0
- package/src/commands/init.mjs +124 -105
- package/src/commands/lease.mjs +73 -25
- package/src/commands/lessons.mjs +10 -9
- package/src/commands/local.mjs +3 -3
- package/src/commands/map.mjs +24 -14
- package/src/commands/mcp.mjs +2 -18
- package/src/commands/pack.mjs +9 -6
- package/src/commands/plan.mjs +6 -5
- package/src/commands/plugin.mjs +2 -2
- package/src/commands/prbody.mjs +3 -3
- package/src/commands/repoindex.mjs +12 -11
- package/src/commands/review.mjs +2 -2
- package/src/commands/rules.mjs +37 -16
- package/src/commands/shared.mjs +14 -19
- package/src/commands/skills.mjs +33 -32
- package/src/commands/skillscompile.mjs +7 -8
- package/src/commands/skillsinstall.mjs +7 -6
- package/src/commands/slice.mjs +220 -74
- package/src/commands/spec.mjs +167 -0
- package/src/commands/status.mjs +27 -18
- package/src/commands/sync.mjs +27 -25
- package/src/commands/tests.mjs +18 -21
- package/src/commands/tokens.mjs +21 -26
- package/src/commands/undo.mjs +5 -15
- package/src/commands/upgrade.mjs +15 -7
- package/src/commands/why.mjs +15 -21
- package/src/compare/hooksstate.mjs +2 -6
- package/src/compare/learn.mjs +3 -1
- package/src/compare/row.mjs +0 -2
- package/src/context.mjs +64 -0
- package/src/decisions/index-doc.mjs +46 -0
- package/src/decisions/index.mjs +3 -2
- package/src/diff.mjs +1 -1
- package/src/distill/distill.mjs +14 -1
- package/src/distill/engine.mjs +5 -1
- package/src/docs/commands.mjs +65 -0
- package/src/ecosystem/records.mjs +219 -0
- package/src/explain.mjs +31 -0
- package/src/gates/index.mjs +385 -0
- package/src/graph/compile.mjs +8 -4
- package/src/graph/load.mjs +6 -2
- package/src/graph/spec.mjs +10 -8
- package/src/harness/records.mjs +143 -0
- package/src/hooks/capability.mjs +7 -5
- package/src/hooks/dispatcher.mjs +209 -60
- package/src/hooks/gates.mjs +10 -43
- package/src/hooks/resolve.mjs +4 -14
- package/src/hooks/shim.mjs +5 -2
- package/src/hooks/where.mjs +2 -7
- package/src/identity.mjs +31 -12
- package/src/index/coverage.mjs +58 -0
- package/src/index/extract.mjs +6 -16
- package/src/index/gate.mjs +1 -1
- package/src/index/govern.mjs +26 -9
- package/src/index/impact.mjs +4 -8
- package/src/index/map.mjs +14 -15
- package/src/index/open.mjs +173 -0
- package/src/index/persist.mjs +224 -57
- package/src/index/store.mjs +82 -39
- package/src/index/tests.mjs +2 -6
- package/src/journal.mjs +94 -20
- package/src/kernel/card.mjs +37 -63
- package/src/kernel/checks.mjs +4 -10
- package/src/lease.mjs +102 -7
- package/src/ledger/index.mjs +150 -14
- package/src/local/client.mjs +4 -6
- package/src/locate.mjs +33 -9
- package/src/manifest/schema.mjs +213 -0
- package/src/mcp/paginate.mjs +33 -0
- package/src/mcp/resources.mjs +7 -10
- package/src/mcp/schemas.mjs +512 -0
- package/src/mcp/server.mjs +119 -25
- package/src/mcp/tools-facts.mjs +326 -0
- package/src/mcp/tools-read.mjs +221 -0
- package/src/mcp/tools.mjs +4 -967
- package/src/measure/corpus.mjs +150 -0
- package/src/measure/metrics.mjs +236 -0
- package/src/measure/records.mjs +211 -0
- package/src/measure/session.mjs +3 -2
- package/src/measure/stats.mjs +538 -0
- package/src/measure/task-schema.mjs +125 -0
- package/src/measure/tasks.mjs +69 -0
- package/src/memory/decide.mjs +6 -15
- package/src/memory/lesson-text.mjs +61 -0
- package/src/memory/lessons.mjs +29 -82
- package/src/name.mjs +7 -0
- package/src/node-version.mjs +37 -0
- package/src/output/index.mjs +209 -10
- package/src/packs/manage.mjs +32 -12
- package/src/packs.mjs +20 -2
- package/src/plan/index.mjs +1 -1
- package/src/plugins.mjs +22 -16
- package/src/prbody.mjs +7 -11
- package/src/projection/loaders.mjs +7 -692
- package/src/projection/lock.mjs +11 -1
- package/src/projection/manifest.mjs +29 -468
- package/src/projection/mdc.mjs +7 -8
- package/src/projection/project.mjs +126 -221
- package/src/projection/region.mjs +561 -0
- package/src/projection/rules.mjs +20 -550
- package/src/projection/surfaces.mjs +43 -210
- package/src/projection/targets.mjs +5 -5
- package/src/repo-root.mjs +31 -0
- package/src/resolve/eval.mjs +2 -1
- package/src/resolve/fingerprint.mjs +19 -22
- package/src/resolve/resolve.mjs +1 -41
- package/src/review/critic.mjs +10 -13
- package/src/run.mjs +98 -0
- package/src/secrets.mjs +14 -22
- package/src/state/events.mjs +129 -0
- package/src/status-snapshot.mjs +233 -0
- package/src/status.mjs +2 -6
- package/src/sys/git.mjs +82 -0
- package/src/sys/proc.mjs +151 -0
- package/src/testing/parsers.mjs +265 -0
- package/src/testing/patches.mjs +23 -0
- package/src/testing/paths.mjs +49 -0
- package/src/testing/runners.mjs +186 -0
- package/src/testing/vault.mjs +68 -0
- package/src/text/frontmatter.mjs +49 -0
- package/src/text/tokens.mjs +52 -0
- package/src/toolwords.mjs +3 -0
- package/src/util/fs.mjs +20 -0
- package/src/util/sort.mjs +12 -0
- package/src/workspace.mjs +58 -11
- package/src/write/plan.mjs +82 -0
- package/templates/plan/tracker.json +1 -0
- package/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +0 -82
- package/docs/decisions/ADR-102-the-two-queued-review-rows-are-closed-by-ruling.md +0 -49
- package/docs/decisions/README.md +0 -600
- package/src/bench/tasks.mjs +0 -36
package/src/commands/undo.mjs
CHANGED
|
@@ -11,24 +11,21 @@
|
|
|
11
11
|
// the rest around a hand edit would silently merge two authors' intentions.
|
|
12
12
|
// Like every other writer this command previews without `--yes`.
|
|
13
13
|
|
|
14
|
-
import { EXIT, emit, emitJson, isJson, writeMode } from '../output/index.mjs';
|
|
14
|
+
import { EXIT, emit, emitJson, isJson, writeMode, present } from '../output/index.mjs';
|
|
15
15
|
import { CLI } from '../name.mjs';
|
|
16
16
|
import { beforeText, entryLabel, listJournalSets, readGitValue, readJournalSet, undoLastJournalSet } from '../journal.mjs';
|
|
17
17
|
import { diffField, emitDiffs, readBefore } from '../diff.mjs';
|
|
18
18
|
import { join } from 'node:path';
|
|
19
19
|
import { invalidateCheckCache } from '../status.mjs';
|
|
20
|
+
import { commandRoot } from '../repo-root.mjs';
|
|
20
21
|
|
|
21
22
|
export async function undoCommand(positional, flags) {
|
|
22
|
-
const root = flags
|
|
23
|
+
const root = commandRoot(flags, positional);
|
|
23
24
|
const mode = writeMode(flags);
|
|
24
25
|
|
|
25
26
|
const [id] = await listJournalSets(root);
|
|
26
27
|
if (!id) {
|
|
27
|
-
|
|
28
|
-
emitJson({ ok: true, command: 'undo', root, set: null, restored: [], dryRun: !mode.apply });
|
|
29
|
-
return EXIT.OK;
|
|
30
|
-
}
|
|
31
|
-
emit('nothing to undo: no journaled writes', { maxBytes: flags.maxBytes ?? 0 });
|
|
28
|
+
present({ ok: true, command: 'undo', root, set: null, restored: [], dryRun: !mode.apply }, () => 'nothing to undo: no journaled writes', flags);
|
|
32
29
|
return EXIT.OK;
|
|
33
30
|
}
|
|
34
31
|
|
|
@@ -64,7 +61,6 @@ export async function undoCommand(positional, flags) {
|
|
|
64
61
|
'',
|
|
65
62
|
`would restore ${files} file(s)${keys > 0 ? ` and ${keys} git config key(s)` : ''}; apply with: ${CLI} undo --yes`,
|
|
66
63
|
].join('\n'),
|
|
67
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
68
64
|
);
|
|
69
65
|
// After the report: the report says what, the diff shows it (R50).
|
|
70
66
|
emitDiffs(diffs, flags);
|
|
@@ -73,11 +69,7 @@ export async function undoCommand(positional, flags) {
|
|
|
73
69
|
|
|
74
70
|
const result = await undoLastJournalSet(root);
|
|
75
71
|
if (result.empty) {
|
|
76
|
-
|
|
77
|
-
emitJson({ ok: true, command: 'undo', root, set: null, restored: [] });
|
|
78
|
-
return EXIT.OK;
|
|
79
|
-
}
|
|
80
|
-
emit('nothing to undo: no journaled writes', { maxBytes: flags.maxBytes ?? 0 });
|
|
72
|
+
present({ ok: true, command: 'undo', root, set: null, restored: [] }, () => 'nothing to undo: no journaled writes', flags);
|
|
81
73
|
return EXIT.OK;
|
|
82
74
|
}
|
|
83
75
|
if (result.refused) {
|
|
@@ -92,7 +84,6 @@ export async function undoCommand(positional, flags) {
|
|
|
92
84
|
'',
|
|
93
85
|
'nothing was restored; re-run the writing command or merge by hand, then undo again',
|
|
94
86
|
].join('\n'),
|
|
95
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
96
87
|
);
|
|
97
88
|
return EXIT.FAIL;
|
|
98
89
|
}
|
|
@@ -106,7 +97,6 @@ export async function undoCommand(positional, flags) {
|
|
|
106
97
|
}
|
|
107
98
|
emit(
|
|
108
99
|
[`undone ${result.set}`, ...result.restored.map((p) => `restored ${p}`)].join('\n'),
|
|
109
|
-
{ maxBytes: flags.maxBytes ?? 0 },
|
|
110
100
|
);
|
|
111
101
|
// After the report: the report says what, the diff shows it (R50).
|
|
112
102
|
emitDiffs(diffs, flags);
|
package/src/commands/upgrade.mjs
CHANGED
|
@@ -26,12 +26,14 @@
|
|
|
26
26
|
// default resolved the workspace and a dry run inside one repo planned all sixteen.
|
|
27
27
|
|
|
28
28
|
import { mkdir } from 'node:fs/promises';
|
|
29
|
-
import { commitJournal
|
|
29
|
+
import { commitJournal } from '../journal.mjs';
|
|
30
|
+
import { applyPlan, change } from '../write/plan.mjs';
|
|
30
31
|
import { invalidateCheckCache } from '../status.mjs';
|
|
31
32
|
|
|
32
33
|
import { basename, dirname, resolve } from 'node:path';
|
|
33
34
|
|
|
34
35
|
import { EXIT, emit, emitJson, fail, isJson, note, writeMode } from '../output/index.mjs';
|
|
36
|
+
import { namedRoot } from '../repo-root.mjs';
|
|
35
37
|
import { provenanceDigest, plan } from '../projection/project.mjs';
|
|
36
38
|
import { MANIFEST, MANIFEST_COMMENT, manifestPath, manifestText, readManifest, writeManifest } from '../projection/manifest.mjs';
|
|
37
39
|
import { diffField, emitDiffs, readBefore } from '../diff.mjs';
|
|
@@ -42,7 +44,7 @@ import { CLI } from '../name.mjs';
|
|
|
42
44
|
import { MANIFEST_FILE } from '../name.mjs';
|
|
43
45
|
|
|
44
46
|
export async function upgradeCommand(positional, flags) {
|
|
45
|
-
const explicit = flags
|
|
47
|
+
const explicit = namedRoot(flags, positional);
|
|
46
48
|
|
|
47
49
|
// `--all` is a claim about SCOPE, so it is checked rather than inferred. "All" of
|
|
48
50
|
// nothing is a contradiction, and defaulting it to the current directory would make
|
|
@@ -179,12 +181,14 @@ export async function upgradeCommand(positional, flags) {
|
|
|
179
181
|
// `undo` in one consumer must never reach into another (R49).
|
|
180
182
|
const journal = [];
|
|
181
183
|
const { targets } = await plan(p.path);
|
|
184
|
+
const targetChanges = [];
|
|
182
185
|
for (const t of targets) {
|
|
183
186
|
if (t.action === 'unchanged' || t.action === 'malformed') continue;
|
|
184
|
-
await
|
|
185
|
-
await journaledWriteFile(p.path, journal, t.path, t.text);
|
|
187
|
+
targetChanges.push(change({ path: t.path, before: await readBefore(t.path), after: t.text, owner: 'upgrade', reason: t.action }));
|
|
186
188
|
writtenFiles.add(t.file);
|
|
187
189
|
}
|
|
190
|
+
// The caller's journal, committed once when the workspace walk ends.
|
|
191
|
+
await applyPlan(p.path, targetChanges, { mode: { apply: true }, journal, commit: false });
|
|
188
192
|
const { manifest } = await readManifest(p.path);
|
|
189
193
|
// Preserve what the repo chose; refresh what simt-dev generates. $comment is
|
|
190
194
|
// ours - it describes this file's provenance - so carrying the previous copy
|
|
@@ -216,8 +220,12 @@ export async function upgradeCommand(positional, flags) {
|
|
|
216
220
|
}
|
|
217
221
|
}
|
|
218
222
|
|
|
223
|
+
// One verdict, stated once. `ok` used to be `skipped === 0`, so a workspace with a
|
|
224
|
+
// repo that never adopted answered ok:false and exited 0: the document and the process
|
|
225
|
+
// disagreed, and a caller had no way to tell which one was the answer (review 0003 Q65).
|
|
226
|
+
const code = exitFor(plans, failed);
|
|
219
227
|
const payload = {
|
|
220
|
-
ok:
|
|
228
|
+
ok: code === EXIT.OK,
|
|
221
229
|
command: 'upgrade',
|
|
222
230
|
root,
|
|
223
231
|
shape: found.shape,
|
|
@@ -232,7 +240,7 @@ export async function upgradeCommand(positional, flags) {
|
|
|
232
240
|
|
|
233
241
|
if (isJson(flags)) {
|
|
234
242
|
emitJson(payload);
|
|
235
|
-
return
|
|
243
|
+
return code;
|
|
236
244
|
}
|
|
237
245
|
|
|
238
246
|
emit(render(plans, summary, { root, shape: found.shape, source: target.source, sha, applied, failed }));
|
|
@@ -256,7 +264,7 @@ export async function upgradeCommand(positional, flags) {
|
|
|
256
264
|
} else {
|
|
257
265
|
note('note every consumer already runs this simt-dev');
|
|
258
266
|
}
|
|
259
|
-
return
|
|
267
|
+
return code;
|
|
260
268
|
}
|
|
261
269
|
|
|
262
270
|
/**
|
package/src/commands/why.mjs
CHANGED
|
@@ -5,33 +5,30 @@
|
|
|
5
5
|
// is `git ls-files`. The index is not rebuilt: governance is the records, and
|
|
6
6
|
// writing the cache to answer a question would change the tree.
|
|
7
7
|
|
|
8
|
-
import { execFile } from 'node:child_process';
|
|
9
8
|
import { readFile, readdir } from 'node:fs/promises';
|
|
10
9
|
import { join } from 'node:path';
|
|
11
|
-
import { promisify } from 'node:util';
|
|
12
10
|
|
|
13
11
|
import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
|
|
14
|
-
import { CLI
|
|
12
|
+
import { CLI } from '../name.mjs';
|
|
15
13
|
import { FORMATTABLE, GATES } from '../hooks/gates.mjs';
|
|
14
|
+
import { loadManifest } from '../manifest/schema.mjs';
|
|
16
15
|
import { decisionsDir } from '../decisions/dir.mjs';
|
|
17
16
|
import {
|
|
18
|
-
|
|
19
|
-
scopeHits,
|
|
17
|
+
governs,
|
|
20
18
|
scopePaths,
|
|
21
|
-
sortHits,
|
|
22
19
|
sourceRoots,
|
|
23
20
|
unresolvedAffects,
|
|
24
21
|
} from '../index/govern.mjs';
|
|
25
|
-
import {
|
|
22
|
+
import { parseFrontmatter } from '../text/frontmatter.mjs';
|
|
26
23
|
import { listPromotedLessons } from '../memory/lessons.mjs';
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
import { commandRoot } from '../repo-root.mjs';
|
|
25
|
+
import { gitPaths } from '../sys/git.mjs';
|
|
29
26
|
|
|
30
27
|
const RULE_DIRS = ['rules', 'rules/stacks', 'rules/mdc', '.simt/profiles'];
|
|
31
28
|
|
|
32
29
|
export async function whyCommand(args, flags, commands = []) {
|
|
33
30
|
if (args.length > 1) return fail(`unknown why argument ${args[1]}`, EXIT.USAGE, `run: ${CLI} why [path]`);
|
|
34
|
-
const root = flags
|
|
31
|
+
const root = commandRoot(flags);
|
|
35
32
|
const listed = await tracked(root);
|
|
36
33
|
if (listed.error) return listed.error;
|
|
37
34
|
const files = new Set(listed.files);
|
|
@@ -41,26 +38,25 @@ export async function whyCommand(args, flags, commands = []) {
|
|
|
41
38
|
if (!target) return reportUnresolved(root, flags, unresolvedAffects(records, ctx));
|
|
42
39
|
if (!files.has(target)) return fail(`not in the tree: ${target}`, EXIT.USAGE, `pass a path git tracks`);
|
|
43
40
|
const corpus = await loadCorpus(root);
|
|
44
|
-
const
|
|
45
|
-
const items = sortHits([...hits, ...scopeHits(target, corpus)]);
|
|
41
|
+
const items = governs(target, { records, ...corpus, ctx });
|
|
46
42
|
const payload = { ok: true, command: 'why', root, path: target, items };
|
|
47
43
|
if (isJson(flags)) {
|
|
48
|
-
emitJson(payload
|
|
44
|
+
emitJson(payload);
|
|
49
45
|
return EXIT.OK;
|
|
50
46
|
}
|
|
51
47
|
const lines = items.map((item) => `${item.kind.padEnd(10)}${item.id} ${item.detail}`);
|
|
52
|
-
emit(lines.length > 0 ? lines.join('\n') : 'none'
|
|
48
|
+
emit(lines.length > 0 ? lines.join('\n') : 'none');
|
|
53
49
|
return EXIT.OK;
|
|
54
50
|
}
|
|
55
51
|
|
|
56
52
|
function reportUnresolved(root, flags, unresolved) {
|
|
57
53
|
const payload = { ok: true, command: 'why', root, unresolved };
|
|
58
54
|
if (isJson(flags)) {
|
|
59
|
-
emitJson(payload
|
|
55
|
+
emitJson(payload);
|
|
60
56
|
return EXIT.OK;
|
|
61
57
|
}
|
|
62
58
|
const lines = unresolved.map((item) => `UNRESOLVED-AFFECTS ${item.token} ${item.id}`);
|
|
63
|
-
emit([`unresolved ${unresolved.length}`, ...lines].join('\n')
|
|
59
|
+
emit([`unresolved ${unresolved.length}`, ...lines].join('\n'));
|
|
64
60
|
return EXIT.OK;
|
|
65
61
|
}
|
|
66
62
|
|
|
@@ -70,8 +66,7 @@ export async function tracked(root) {
|
|
|
70
66
|
try {
|
|
71
67
|
// `-z`: without it git C-quotes a non-ASCII path and the reader's real
|
|
72
68
|
// filename never matches it (review 0003 Q39).
|
|
73
|
-
|
|
74
|
-
return { files: stdout.split('\0').filter(Boolean) };
|
|
69
|
+
return { files: await gitPaths(root, ['ls-files']) };
|
|
75
70
|
} catch (error) {
|
|
76
71
|
return {
|
|
77
72
|
error: fail(
|
|
@@ -97,7 +92,7 @@ export async function decisionRecords(root) {
|
|
|
97
92
|
const records = [];
|
|
98
93
|
for (const name of names) {
|
|
99
94
|
const text = await readFile(join(dir, name), 'utf8');
|
|
100
|
-
const { body } =
|
|
95
|
+
const { body } = parseFrontmatter(text);
|
|
101
96
|
const id = /^ADR-\d{3,}/i.exec(name)[0].toUpperCase();
|
|
102
97
|
const heading = /^#\s+(.*)$/m.exec(body)?.[1] ?? id;
|
|
103
98
|
const title = heading.replace(/^ADR-\d{3,}\s*[—–-]\s*/i, '').trim();
|
|
@@ -172,8 +167,7 @@ export async function lessonScopes(root) {
|
|
|
172
167
|
|
|
173
168
|
async function gateCommands(root) {
|
|
174
169
|
try {
|
|
175
|
-
const
|
|
176
|
-
const commands = parsed?.gates?.commands;
|
|
170
|
+
const commands = (await loadManifest(root)).manifest?.gates?.commands;
|
|
177
171
|
return commands && typeof commands === 'object' ? commands : {};
|
|
178
172
|
} catch {
|
|
179
173
|
return {};
|
|
@@ -6,16 +6,15 @@
|
|
|
6
6
|
// duplicated-source-of-truth problem the tool exists to remove. `compare` reads it
|
|
7
7
|
// for N repos, so it must not shell out per repo per event.
|
|
8
8
|
|
|
9
|
-
import { execFile } from 'node:child_process';
|
|
10
9
|
import { readFile, stat } from 'node:fs/promises';
|
|
11
10
|
import { basename, join } from 'node:path';
|
|
12
|
-
import { promisify } from 'node:util';
|
|
13
11
|
|
|
14
12
|
import { CHAIN_MARKER, EVENTS, HOOKS_DIR, HOOKS_PATH_KEY, SHIM_MARKER, fileFor, renderChainedShim, renderShim } from '../hooks/shim.mjs';
|
|
15
13
|
import { foreignHooks, resolveHooksDir } from '../hooks/where.mjs';
|
|
16
14
|
import { packageVersion, hasNpm } from '../package-version.mjs';
|
|
17
15
|
import { resolveNodeRunner } from '../ecosystem/node-runner.mjs';
|
|
18
16
|
import { fingerprint } from '../resolve/fingerprint.mjs';
|
|
17
|
+
import { gitText } from '../sys/git.mjs';
|
|
19
18
|
|
|
20
19
|
/**
|
|
21
20
|
* The Node manager id for this repository, or null without a package.json.
|
|
@@ -34,8 +33,6 @@ async function nodeManagerFor(root) {
|
|
|
34
33
|
return resolveNodeRunner({ facts: detected.facts, packageManager }).id;
|
|
35
34
|
}
|
|
36
35
|
|
|
37
|
-
const exec = promisify(execFile);
|
|
38
|
-
|
|
39
36
|
/**
|
|
40
37
|
* The chained shim at `path`, read back out of the file itself.
|
|
41
38
|
*
|
|
@@ -120,8 +117,7 @@ export function wantsShim(manifest, exists, event) {
|
|
|
120
117
|
/** `core.hooksPath`, or null when unset or the repo is not a git repo. */
|
|
121
118
|
export async function readHooksPath(root) {
|
|
122
119
|
try {
|
|
123
|
-
const
|
|
124
|
-
const value = stdout.trim();
|
|
120
|
+
const value = await gitText(root, ['config', '--get', HOOKS_PATH_KEY]);
|
|
125
121
|
return value === '' ? null : value;
|
|
126
122
|
} catch {
|
|
127
123
|
return null;
|
package/src/compare/learn.mjs
CHANGED
|
@@ -19,7 +19,9 @@ import { mapCapped, DEFAULT_CONCURRENCY } from './parallel.mjs';
|
|
|
19
19
|
|
|
20
20
|
import { policiesFor } from '../agenthooks/policy.mjs';
|
|
21
21
|
import { DEL_R_TOLERANCE, parseAblationArmName } from '../bench/ablate.mjs';
|
|
22
|
-
import {
|
|
22
|
+
import { evaluateRunRecord, loadPrices } from '../measure/metrics.mjs';
|
|
23
|
+
import { loadRunRecords } from '../measure/records.mjs';
|
|
24
|
+
import { compareArms } from '../measure/stats.mjs';
|
|
23
25
|
import { GATES } from '../hooks/gates.mjs';
|
|
24
26
|
import { readLedger } from '../ledger/index.mjs';
|
|
25
27
|
import { readLessonCandidates } from '../memory/lessons.mjs';
|
package/src/compare/row.mjs
CHANGED
|
@@ -21,7 +21,6 @@ import { checkTargets, PRIMARY_TARGET } from '../projection/project.mjs';
|
|
|
21
21
|
import { manifestDrift, readManifest } from '../projection/manifest.mjs';
|
|
22
22
|
import { loadRules, select } from '../projection/rules.mjs';
|
|
23
23
|
import { resolvePacks } from '../packs.mjs';
|
|
24
|
-
import { knownGates } from '../hooks/resolve.mjs';
|
|
25
24
|
import { fingerprint } from '../resolve/fingerprint.mjs';
|
|
26
25
|
import { readHooksState, wantsShim } from './hooksstate.mjs';
|
|
27
26
|
import { CLI } from '../name.mjs';
|
|
@@ -421,4 +420,3 @@ export function summarise(rows, extra = {}) {
|
|
|
421
420
|
};
|
|
422
421
|
}
|
|
423
422
|
|
|
424
|
-
export { knownGates };
|
package/src/context.mjs
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// The per-run memo a command reads its repository through (review 0003 Q67).
|
|
2
|
+
//
|
|
3
|
+
// `check` did the same work twice in one run: `fingerprint(root)` at two sites, a
|
|
4
|
+
// manifest read that `manifestDrift` repeated, and a rules load that every consumer
|
|
5
|
+
// paid for again. Each fingerprint spawns `git ls-files` and walks the tree once per
|
|
6
|
+
// glob fact, so the second call was never cheap - it was invisible.
|
|
7
|
+
//
|
|
8
|
+
// The cache lives for one command, not for the process. `bench` writes manifests and
|
|
9
|
+
// rewrites trees mid-run; a process-lifetime memo would hand a benchmark the answer
|
|
10
|
+
// for a repository it had just changed, which is the one way a cache can be worse
|
|
11
|
+
// than no cache. A caller that wants a fresh answer makes a fresh context.
|
|
12
|
+
//
|
|
13
|
+
// A rejected computation is not cached: a transient failure (a git lock, a permission
|
|
14
|
+
// error) should not become the answer for the rest of the run, and every one of these
|
|
15
|
+
// readers is safe to retry.
|
|
16
|
+
|
|
17
|
+
import { fingerprint } from './resolve/fingerprint.mjs';
|
|
18
|
+
import { readManifest } from './projection/manifest.mjs';
|
|
19
|
+
import { loadRules, provenanceDigest } from './projection/rules.mjs';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* One repository, read once per question.
|
|
23
|
+
*
|
|
24
|
+
* Each accessor is a promise, and two calls with the same key share it rather than
|
|
25
|
+
* racing: the first caller to ask starts the work, and everyone else awaits it.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} root the repository this context answers for
|
|
28
|
+
*/
|
|
29
|
+
export function repoContext(root) {
|
|
30
|
+
const cache = new Map();
|
|
31
|
+
const counts = { hits: 0, misses: 0 };
|
|
32
|
+
|
|
33
|
+
const once = (key, compute) => {
|
|
34
|
+
if (cache.has(key)) {
|
|
35
|
+
counts.hits += 1;
|
|
36
|
+
return cache.get(key);
|
|
37
|
+
}
|
|
38
|
+
counts.misses += 1;
|
|
39
|
+
const pending = Promise.resolve()
|
|
40
|
+
.then(compute)
|
|
41
|
+
.catch((error) => {
|
|
42
|
+
// Not cached, or one failed read would answer every later caller.
|
|
43
|
+
cache.delete(key);
|
|
44
|
+
throw error;
|
|
45
|
+
});
|
|
46
|
+
cache.set(key, pending);
|
|
47
|
+
return pending;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
root,
|
|
52
|
+
/** What the cache did, for a test or a measurement - never printed by a command. */
|
|
53
|
+
get stats() {
|
|
54
|
+
return { ...counts, entries: cache.size };
|
|
55
|
+
},
|
|
56
|
+
fingerprint: () => once('fingerprint', () => fingerprint(root)),
|
|
57
|
+
// The legacy shape, because that is what the commands print: `error` is the
|
|
58
|
+
// unreadable-file case and every caller already renders it. Memoising the loader's
|
|
59
|
+
// `problems` array here instead gave doctor `undefined` where it tests for null.
|
|
60
|
+
manifest: () => once('manifest', () => readManifest(root)),
|
|
61
|
+
rules: () => once('rules', () => loadRules(root)),
|
|
62
|
+
digest: () => once('digest', () => provenanceDigest()),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// The decisions index document, rendered from the records (review 0003 Q91).
|
|
2
|
+
//
|
|
3
|
+
// The file was 590 hand-written lines: a table of every ADR kept in step by whoever
|
|
4
|
+
// remembered. It agreed with the records until it did not, and nothing read it, so
|
|
5
|
+
// nothing noticed - the failure the generated rule regions exist to prevent, one
|
|
6
|
+
// directory over. The table is output now: this module renders it, `simt rules
|
|
7
|
+
// decisions --yes` writes it, and test/decisions-index.test.mjs fails on drift.
|
|
8
|
+
//
|
|
9
|
+
// The narrative that trailed the table moved to docs/architecture/history.md: prose a
|
|
10
|
+
// reader consults is not prose a command rewrites.
|
|
11
|
+
|
|
12
|
+
const HEADER = `# Decisions
|
|
13
|
+
|
|
14
|
+
Architecture decision records for the SIMT development ecosystem. One canonical home per
|
|
15
|
+
decision; nothing here is duplicated into a repo's \`AGENTS.md\`.
|
|
16
|
+
|
|
17
|
+
A decision is superseded by writing a new ADR with a higher number and editing the old
|
|
18
|
+
record's status line — never by editing the ruling in place.
|
|
19
|
+
|
|
20
|
+
This table is generated from the records: \`simt rules decisions --yes\` writes it, and
|
|
21
|
+
\`test/decisions-index.test.mjs\` fails when it drifts. The history behind the numbers is
|
|
22
|
+
in [architecture/history.md](architecture/history.md).
|
|
23
|
+
|
|
24
|
+
| ADR | Decision | Status |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The index document, from the records.
|
|
30
|
+
*
|
|
31
|
+
* Ordered by number, not by filename: the number IS the order, and a directory listing
|
|
32
|
+
* is not (`ADR-100` sorts before `ADR-99` as text).
|
|
33
|
+
*
|
|
34
|
+
* @param {{id: string, title: string, status: string, file: string}[]} rows
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
export function renderDecisionsIndex(rows) {
|
|
38
|
+
const ordered = [...rows].sort((a, b) => number(a.id) - number(b.id));
|
|
39
|
+
const lines = ordered.map((row) => `| [${number(row.id)}](decisions/${row.file}) | ${row.title} | ${row.status} |`);
|
|
40
|
+
return `${HEADER}${lines.join('\n')}\n`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** `ADR-007` -> `007`, the number the table and the ordering both use. */
|
|
44
|
+
function number(id) {
|
|
45
|
+
return id.replace(/^ADR-/i, '').padStart(3, '0');
|
|
46
|
+
}
|
package/src/decisions/index.mjs
CHANGED
|
@@ -28,7 +28,8 @@ import { packageVersion } from '../package-version.mjs';
|
|
|
28
28
|
// evaluation would read an initialising binding on one of the two load
|
|
29
29
|
// orders. Every use below happens at runtime, when both sides are loaded.
|
|
30
30
|
import { COMMANDS } from '../commands/index.mjs';
|
|
31
|
-
import { PARTIAL_CLAUSE, parseDecisionFrontmatter
|
|
31
|
+
import { PARTIAL_CLAUSE, parseDecisionFrontmatter } from '../memory/decide.mjs';
|
|
32
|
+
import { parseFrontmatter } from '../text/frontmatter.mjs';
|
|
32
33
|
import { affectsTokens } from '../index/govern.mjs';
|
|
33
34
|
|
|
34
35
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -189,7 +190,7 @@ export function citations(text) {
|
|
|
189
190
|
*/
|
|
190
191
|
export function parseDecision(file, text, { all = false } = {}) {
|
|
191
192
|
const fm = parseDecisionFrontmatter(text);
|
|
192
|
-
const { body } =
|
|
193
|
+
const { body } = parseFrontmatter(text);
|
|
193
194
|
const fileId = /^ADR-(\d{3,})/i.exec(file)?.[0]?.toUpperCase();
|
|
194
195
|
const heading = /^#\s+(.*)$/m.exec(body)?.[1] ?? null;
|
|
195
196
|
const headId = heading ? /^(ADR-\d{3,})/i.exec(heading)?.[1]?.toUpperCase() : null;
|
package/src/diff.mjs
CHANGED
|
@@ -49,7 +49,7 @@ export async function readBefore(path) {
|
|
|
49
49
|
export function emitDiffs(pairs, flags) {
|
|
50
50
|
if (isJson(flags)) return;
|
|
51
51
|
const text = renderDiffs(pairs);
|
|
52
|
-
if (text !== '') emit(text
|
|
52
|
+
if (text !== '') emit(text);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/** The `--diff` bytes for a JSON payload: absent entirely without the flag, so
|
package/src/distill/distill.mjs
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
|
|
36
36
|
export const DEFAULT_BUDGET = 2048;
|
|
37
37
|
import { LOCAL_DIR } from '../state.mjs';
|
|
38
|
+
import { commandRoot } from '../repo-root.mjs';
|
|
38
39
|
|
|
39
40
|
const DISTILL_DIR = join(LOCAL_DIR, 'distilled');
|
|
40
41
|
|
|
@@ -175,7 +176,7 @@ export async function distillTarget(args, flags) {
|
|
|
175
176
|
);
|
|
176
177
|
}
|
|
177
178
|
|
|
178
|
-
const root = resolve(flags
|
|
179
|
+
const root = resolve(commandRoot(flags));
|
|
179
180
|
const source = await resolveTarget(target, root, flags);
|
|
180
181
|
if (source.exit !== undefined) return source.exit;
|
|
181
182
|
|
|
@@ -192,6 +193,16 @@ export async function distillTarget(args, flags) {
|
|
|
192
193
|
const cached = flags.refresh === true ? null : await readFile(absProposal, 'utf8').catch(() => null);
|
|
193
194
|
if (cached !== null) {
|
|
194
195
|
const fields = parseExistingFrontmatter(cached);
|
|
196
|
+
let faults = [];
|
|
197
|
+
if (typeof fields.faults === 'string') {
|
|
198
|
+
try {
|
|
199
|
+
const parsedFaults = JSON.parse(fields.faults);
|
|
200
|
+
if (Array.isArray(parsedFaults)) faults = parsedFaults.map(String);
|
|
201
|
+
} catch {
|
|
202
|
+
// A malformed faults line is not a reason to refuse the proposal; the proposal
|
|
203
|
+
// still says it is unverified, and `--refresh` re-derives the list.
|
|
204
|
+
}
|
|
205
|
+
}
|
|
195
206
|
return {
|
|
196
207
|
ok: true,
|
|
197
208
|
command: 'distill',
|
|
@@ -201,6 +212,7 @@ export async function distillTarget(args, flags) {
|
|
|
201
212
|
budget: Number(fields.budget ?? budget),
|
|
202
213
|
citations: countCitations(cached),
|
|
203
214
|
verified: fields.verified !== 'false',
|
|
215
|
+
faults,
|
|
204
216
|
metric: { note: METRIC_NOTE },
|
|
205
217
|
proposal: { path: relProposal, state: 'cached', bytes: Buffer.byteLength(cached, 'utf8') },
|
|
206
218
|
};
|
|
@@ -227,6 +239,7 @@ export async function distillTarget(args, flags) {
|
|
|
227
239
|
engine: provenance,
|
|
228
240
|
budget,
|
|
229
241
|
verified: faults.length === 0,
|
|
242
|
+
faults,
|
|
230
243
|
}) + `\n${outcome.body}\n`;
|
|
231
244
|
|
|
232
245
|
await mkdir(join(root, DISTILL_DIR), { recursive: true });
|
package/src/distill/engine.mjs
CHANGED
|
@@ -167,7 +167,7 @@ export function verifyCitations(body, sourceLineCount) {
|
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
/** The proposal file's frontmatter. No wall-clock value here: byte-stable (P4). */
|
|
170
|
-
export function proposalFrontmatter({ source, digest, engine, budget, prompt, verified, verdict, advisory }) {
|
|
170
|
+
export function proposalFrontmatter({ source, digest, engine, budget, prompt, verified, verdict, advisory, faults = [] }) {
|
|
171
171
|
return [
|
|
172
172
|
'---',
|
|
173
173
|
`source: ${source}`,
|
|
@@ -178,6 +178,10 @@ export function proposalFrontmatter({ source, digest, engine, budget, prompt, ve
|
|
|
178
178
|
verdict ? `verdict: ${verdict}` : null,
|
|
179
179
|
advisory ? 'advisory: true' : null,
|
|
180
180
|
verified === false ? 'verified: false' : 'verified: true',
|
|
181
|
+
// The faults belong to the proposal, not only to the report that wrote it: a cached
|
|
182
|
+
// proposal re-read without them reported `verified:false` and nothing to fix
|
|
183
|
+
// (review 0003 Q52).
|
|
184
|
+
Array.isArray(faults) && faults.length > 0 ? `faults: ${JSON.stringify(faults)}` : null,
|
|
181
185
|
'status: proposal',
|
|
182
186
|
'---',
|
|
183
187
|
'',
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// The README's command list, rendered from the spec (review 0003 Q60).
|
|
2
|
+
//
|
|
3
|
+
// The README carried the fifth copy of the command list this review found. The other
|
|
4
|
+
// four now come from `spec.mjs`; this one could not, because a README line is a sentence
|
|
5
|
+
// for a reader rather than a table row for a parser. It does not need to be: a row's
|
|
6
|
+
// `path` is the command and its `summary` is already one phrase, written for help. So the
|
|
7
|
+
// list is rendered from those two fields, and `test/docs.test.mjs` fails when the README
|
|
8
|
+
// and the spec stop agreeing.
|
|
9
|
+
//
|
|
10
|
+
// The group comment is the first word of the path, which is what a reader scans for. The
|
|
11
|
+
// order is the order the spec lists groups in, so a new group lands where its author put
|
|
12
|
+
// it rather than wherever it sorts.
|
|
13
|
+
import { SPEC } from '../commands/spec.mjs';
|
|
14
|
+
import { CLI } from '../name.mjs';
|
|
15
|
+
|
|
16
|
+
export const README_FILE = 'README.md';
|
|
17
|
+
export const BEGIN = '<!-- simt:commands:begin -->';
|
|
18
|
+
export const END = '<!-- simt:commands:end -->';
|
|
19
|
+
|
|
20
|
+
/** The fenced command list, markers included, exactly as the README must carry it. */
|
|
21
|
+
export function renderCommandBlock() {
|
|
22
|
+
const groups = new Map();
|
|
23
|
+
for (const row of SPEC) {
|
|
24
|
+
const group = row.path.split(' ')[0];
|
|
25
|
+
if (!groups.has(group)) groups.set(group, []);
|
|
26
|
+
groups.get(group).push(row);
|
|
27
|
+
}
|
|
28
|
+
// The invocation, not the path: `brief` without `"<task>"` is a command a reader has
|
|
29
|
+
// to go and look up, which is what the hand-written list it replaced was for.
|
|
30
|
+
const command = (row) => `${row.path}${row.usage === undefined ? '' : ` ${row.usage}`}`;
|
|
31
|
+
const width = Math.max(...SPEC.map((row) => command(row).length));
|
|
32
|
+
const lines = [];
|
|
33
|
+
for (const [group, rows] of groups) {
|
|
34
|
+
lines.push('', `# ${group}`);
|
|
35
|
+
for (const row of [...rows].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))) {
|
|
36
|
+
lines.push(`${CLI} ${command(row).padEnd(width)} # ${row.summary}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// The first group's leading blank line would sit directly under the fence, which reads
|
|
40
|
+
// as a stray newline in the block rather than a separation.
|
|
41
|
+
if (lines[0] === '') lines.shift();
|
|
42
|
+
return [
|
|
43
|
+
BEGIN,
|
|
44
|
+
'<!-- Rendered from the spec by `simt docs readme --yes`; do not edit by hand. -->',
|
|
45
|
+
'',
|
|
46
|
+
'```sh',
|
|
47
|
+
...lines,
|
|
48
|
+
'```',
|
|
49
|
+
END,
|
|
50
|
+
].join('\n');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Replace the region between the markers, or return null when the file carries none.
|
|
55
|
+
*
|
|
56
|
+
* Null rather than an appended block: a README without the markers has the list somewhere
|
|
57
|
+
* else, and writing a second one beside it is the duplicated list this rendering exists
|
|
58
|
+
* to remove.
|
|
59
|
+
*/
|
|
60
|
+
export function replaceBlock(text, block) {
|
|
61
|
+
const start = text.indexOf(BEGIN);
|
|
62
|
+
const end = text.indexOf(END);
|
|
63
|
+
if (start === -1 || end === -1 || end < start) return null;
|
|
64
|
+
return `${text.slice(0, start)}${block}${text.slice(end + END.length)}`;
|
|
65
|
+
}
|