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
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Mined task descriptors, read back from the tool state's `bench/tasks/` directory.
|
|
2
|
+
//
|
|
3
|
+
// Extracted from bench/run.mjs on its second real reader (ADR-002): the advice engine
|
|
4
|
+
// needs the two facts a run record does not carry — the prompt, which classifies the
|
|
5
|
+
// task, and the gold stat, which says how many files the change touched. A second private
|
|
6
|
+
// loader beside the first would drift the first time one learned a filter the other
|
|
7
|
+
// lacked.
|
|
8
|
+
//
|
|
9
|
+
// Every descriptor passes through `task-schema.mjs`, so a reader never has to guess
|
|
10
|
+
// whether a field is present: an unreadable task is refused with its file named, and a v1
|
|
11
|
+
// descriptor is normalised rather than skipped. Deterministic by construction: the
|
|
12
|
+
// directory is read sorted, the filter is exact, and nothing here touches a model, a
|
|
13
|
+
// network or a clock.
|
|
14
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
15
|
+
import { join, resolve } from 'node:path';
|
|
16
|
+
|
|
17
|
+
import { STATE_DIR } from '../name.mjs';
|
|
18
|
+
import { normaliseTask } from './task-schema.mjs';
|
|
19
|
+
|
|
20
|
+
/** The directory a repository's mined tasks live in. */
|
|
21
|
+
export function tasksDir(root) {
|
|
22
|
+
return join(resolve(root), STATE_DIR, 'bench', 'tasks');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The file one task descriptor lives in. */
|
|
26
|
+
export function taskPath(root, id) {
|
|
27
|
+
return join(tasksDir(root), `${id}.json`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Read one task descriptor by id.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} root
|
|
34
|
+
* @param {string} id
|
|
35
|
+
* @returns {Promise<object|null>} the v2 descriptor, or null when the file is absent
|
|
36
|
+
*/
|
|
37
|
+
export async function readTask(root, id) {
|
|
38
|
+
const text = await readFile(taskPath(root, id), 'utf8').catch(() => null);
|
|
39
|
+
if (text === null) return null;
|
|
40
|
+
return normaliseTask(JSON.parse(text), { source: taskPath(root, id) });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Load mined benchmark task descriptors.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} root the repository
|
|
47
|
+
* @param {string[]} [filterIds] exact task ids to keep; empty means load them all
|
|
48
|
+
* @returns {Promise<object[]>} v2 descriptors sorted by id (id, base, prompt, stratum,
|
|
49
|
+
* oracle, f2p, p2p, gold {files, linesAdded, linesRemoved}, vault)
|
|
50
|
+
*/
|
|
51
|
+
export async function loadTasks(root, filterIds = []) {
|
|
52
|
+
const dir = tasksDir(root);
|
|
53
|
+
const entries = (await readdir(dir).catch(() => [])).filter((file) => file.endsWith('.json')).sort();
|
|
54
|
+
|
|
55
|
+
const filterSet = new Set(filterIds);
|
|
56
|
+
const tasks = [];
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
const file = join(dir, entry);
|
|
59
|
+
let raw;
|
|
60
|
+
try {
|
|
61
|
+
raw = JSON.parse(await readFile(file, 'utf8'));
|
|
62
|
+
} catch (error) {
|
|
63
|
+
throw new Error(`${file}: ${error.message ?? error}`);
|
|
64
|
+
}
|
|
65
|
+
if (filterSet.size > 0 && !filterSet.has(String(raw?.id ?? ''))) continue;
|
|
66
|
+
tasks.push(normaliseTask(raw, { source: file }));
|
|
67
|
+
}
|
|
68
|
+
return tasks.sort((a, b) => a.id.localeCompare(b.id));
|
|
69
|
+
}
|
package/src/memory/decide.mjs
CHANGED
|
@@ -3,14 +3,12 @@
|
|
|
3
3
|
// frontmatter first for `rules why` and `why`, and fail lint on partial
|
|
4
4
|
// supersessions stated only in prose.
|
|
5
5
|
|
|
6
|
-
import { execFile } from 'node:child_process';
|
|
7
6
|
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
8
7
|
import { join } from 'node:path';
|
|
9
|
-
import { promisify } from 'node:util';
|
|
10
8
|
import { recordCreatedFile } from '../journal.mjs';
|
|
11
9
|
import { decisionsDir } from '../decisions/dir.mjs';
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
import { parseFrontmatter } from '../text/frontmatter.mjs';
|
|
11
|
+
import { gitText } from '../sys/git.mjs';
|
|
14
12
|
|
|
15
13
|
export const DECISION_KEYS = ['simt-affects', 'simt-supersedes'];
|
|
16
14
|
|
|
@@ -39,13 +37,6 @@ function unquote(value) {
|
|
|
39
37
|
: t;
|
|
40
38
|
}
|
|
41
39
|
|
|
42
|
-
/** Strip leading YAML frontmatter (`---\n...\n---`) from a markdown document. */
|
|
43
|
-
export function splitFrontmatter(text) {
|
|
44
|
-
const raw = String(text ?? '');
|
|
45
|
-
const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(raw);
|
|
46
|
-
return match ? { front: match[1], body: raw.slice(match[0].length) } : { front: null, body: raw };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
40
|
function parseInlineStringList(inner) {
|
|
50
41
|
return inner.trim() ? inner.split(',').map(unquote).filter(Boolean) : [];
|
|
51
42
|
}
|
|
@@ -93,7 +84,7 @@ function normaliseSupersedesEntry(obj, errors) {
|
|
|
93
84
|
|
|
94
85
|
/** Parse `simt-affects` and `simt-supersedes` from an ADR's YAML frontmatter. */
|
|
95
86
|
export function parseDecisionFrontmatter(text) {
|
|
96
|
-
const { front } =
|
|
87
|
+
const { front } = parseFrontmatter(text);
|
|
97
88
|
if (front === null) {
|
|
98
89
|
return { hasFrontmatter: false, hasAffects: false, hasSupersedes: false, affects: [], supersedes: [], errors: [] };
|
|
99
90
|
}
|
|
@@ -255,7 +246,7 @@ export async function diffAffectedPaths(root) {
|
|
|
255
246
|
['diff', '--name-only'],
|
|
256
247
|
]) {
|
|
257
248
|
try {
|
|
258
|
-
const
|
|
249
|
+
const stdout = await gitText(root, args);
|
|
259
250
|
addLines(stdout);
|
|
260
251
|
} catch {
|
|
261
252
|
// Ignore if not a git repo or HEAD absent.
|
|
@@ -263,7 +254,7 @@ export async function diffAffectedPaths(root) {
|
|
|
263
254
|
}
|
|
264
255
|
if (paths.size === 0) {
|
|
265
256
|
try {
|
|
266
|
-
const
|
|
257
|
+
const stdout = await gitText(root, ['diff-tree', '--no-commit-id', '--name-only', '-r', 'HEAD']);
|
|
267
258
|
addLines(stdout);
|
|
268
259
|
} catch {
|
|
269
260
|
// Ignore.
|
|
@@ -455,7 +446,7 @@ export function lintDecision(text, { file = 'ADR-000.md', historicalCutoff = 0 }
|
|
|
455
446
|
const num = id ? Number(id.replace(/^ADR-/, '')) : null;
|
|
456
447
|
|
|
457
448
|
const fm = parseDecisionFrontmatter(text);
|
|
458
|
-
const { body } =
|
|
449
|
+
const { body } = parseFrontmatter(text);
|
|
459
450
|
const findings = fm.errors.map((err) => ({
|
|
460
451
|
file,
|
|
461
452
|
id,
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Pure helpers for a lesson record: its slug, its capped fix diff, and evidence equality.
|
|
2
|
+
//
|
|
3
|
+
// They touch no file and no clock, which is why they are separable from the store
|
|
4
|
+
// (review 0003 Q80).
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export function slugify(text) {
|
|
8
|
+
const slug = String(text ?? '')
|
|
9
|
+
.toLowerCase()
|
|
10
|
+
.replace(/^not ok\s+\d+\s*-?\s*/i, '')
|
|
11
|
+
.replace(/^(?:error|assertionerror|fail|revert|blocked)[:\s-]*/i, '')
|
|
12
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
13
|
+
.replace(/^-+|-+$/g, '')
|
|
14
|
+
.slice(0, 48)
|
|
15
|
+
.replace(/-+$/g, '');
|
|
16
|
+
return slug || 'lesson';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Read all candidate records from `.simt/lessons.jsonl`.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} root
|
|
23
|
+
* @returns {Promise<{candidates: Array<object>, errors: string[]}>}
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Truncate an unbounded diff to the evidence budget, keeping the head: the hunk
|
|
29
|
+
* headers that name the files are the part a reader acts on.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} diff
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
export function capFixDiff(diff) {
|
|
35
|
+
const text = String(diff ?? '');
|
|
36
|
+
if (text.length <= FIX_DIFF_BUDGET) return text;
|
|
37
|
+
return `${text.slice(0, FIX_DIFF_BUDGET)}\n… (truncated to ${FIX_DIFF_BUDGET} chars)`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
export function sameEvidence(a = [], b = []) {
|
|
42
|
+
if (a.length !== b.length) return false;
|
|
43
|
+
return a.every((v, i) => v === b[i]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Append one deterministic candidate to the lessons ledger, deduplicating by
|
|
48
|
+
* `(kind, signature, evidence)`.
|
|
49
|
+
*
|
|
50
|
+
* The stored text is redacted before anything else: candidates keep the raw
|
|
51
|
+
* commands that failed, and a command can carry a token. Redaction runs before
|
|
52
|
+
* the signature is hashed so the same failure redacts to the same signature
|
|
53
|
+
* (review 0003 Q36).
|
|
54
|
+
*
|
|
55
|
+
* @param {string} root
|
|
56
|
+
* @param {object} candidate
|
|
57
|
+
* @returns {Promise<object>}
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/** The cap on a stored fix diff, so one large commit cannot bloat a lesson. */
|
|
61
|
+
export const FIX_DIFF_BUDGET = 20000;
|
package/src/memory/lessons.mjs
CHANGED
|
@@ -11,25 +11,23 @@
|
|
|
11
11
|
// `simt lessons review` promotes a candidate to `docs/lessons/NNNN-<slug>.md` with the
|
|
12
12
|
// Appendix A frontmatter (`simt-trigger`, `simt-evidence`, `simt-expires`).
|
|
13
13
|
|
|
14
|
+
import { FIX_DIFF_BUDGET, capFixDiff, sameEvidence, slugify } from './lesson-text.mjs';
|
|
15
|
+
import { noteEvent } from '../state/events.mjs';
|
|
14
16
|
import { createHash } from 'node:crypto';
|
|
15
|
-
import { execFile } from 'node:child_process';
|
|
16
17
|
import { mkdir, open, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
17
18
|
import { dirname, join } from 'node:path';
|
|
18
|
-
import { promisify } from 'node:util';
|
|
19
19
|
|
|
20
20
|
import { postDeliveryJson } from '../agenthooks/render.mjs';
|
|
21
21
|
import { lessonPaths } from '../index/govern.mjs';
|
|
22
22
|
import { recordCreatedFile } from '../journal.mjs';
|
|
23
23
|
import { redactText } from '../secrets.mjs';
|
|
24
24
|
import { localRel, readStateFile } from '../state.mjs';
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
import { parseFrontmatter } from '../text/frontmatter.mjs';
|
|
26
|
+
import { LEDGER_FILE_LEGACY, LEDGER_FILE_REL } from '../name.mjs';
|
|
27
|
+
import { git } from '../sys/git.mjs';
|
|
28
28
|
|
|
29
29
|
export const LESSONS_FILE = localRel('lessons.jsonl');
|
|
30
30
|
const LESSONS_FILE_LEGACY = '.simt/lessons.jsonl';
|
|
31
|
-
const LEDGER_REL = localRel('ledger.jsonl');
|
|
32
|
-
const LEDGER_REL_LEGACY = '.simt/ledger.jsonl';
|
|
33
31
|
export const LESSONS_DIR = 'docs/lessons';
|
|
34
32
|
export const LESSON_KEYS = new Set(['simt-trigger', 'simt-evidence', 'simt-expires']);
|
|
35
33
|
|
|
@@ -38,7 +36,6 @@ export const LESSON_KEYS = new Set(['simt-trigger', 'simt-evidence', 'simt-expir
|
|
|
38
36
|
* capped so one generated-file revert cannot bloat every read of the ledger.
|
|
39
37
|
* Truncation keeps the head (the hunk headers that name the files) and says so.
|
|
40
38
|
*/
|
|
41
|
-
const FIX_DIFF_BUDGET = 20000;
|
|
42
39
|
|
|
43
40
|
/**
|
|
44
41
|
* The default expiry is relative to the promotion, not a date in the source:
|
|
@@ -167,25 +164,6 @@ function extractKeywords(text, paths = []) {
|
|
|
167
164
|
}
|
|
168
165
|
return [...words];
|
|
169
166
|
}
|
|
170
|
-
|
|
171
|
-
function slugify(text) {
|
|
172
|
-
const slug = String(text ?? '')
|
|
173
|
-
.toLowerCase()
|
|
174
|
-
.replace(/^not ok\s+\d+\s*-?\s*/i, '')
|
|
175
|
-
.replace(/^(?:error|assertionerror|fail|revert|blocked)[:\s-]*/i, '')
|
|
176
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
177
|
-
.replace(/^-+|-+$/g, '')
|
|
178
|
-
.slice(0, 48)
|
|
179
|
-
.replace(/-+$/g, '');
|
|
180
|
-
return slug || 'lesson';
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Read all candidate records from `.simt/lessons.jsonl`.
|
|
185
|
-
*
|
|
186
|
-
* @param {string} root
|
|
187
|
-
* @returns {Promise<{candidates: Array<object>, errors: string[]}>}
|
|
188
|
-
*/
|
|
189
167
|
export async function readLessonCandidates(root) {
|
|
190
168
|
// The pre-move path is a fallback for one release (review 0003 Q19).
|
|
191
169
|
let text;
|
|
@@ -255,38 +233,6 @@ export function lessonIdFor({ kind, signature, evidence = [], cmd = null }) {
|
|
|
255
233
|
.digest('hex');
|
|
256
234
|
return `l-${digest.slice(0, 12)}`;
|
|
257
235
|
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Truncate an unbounded diff to the evidence budget, keeping the head: the hunk
|
|
261
|
-
* headers that name the files are the part a reader acts on.
|
|
262
|
-
*
|
|
263
|
-
* @param {string} diff
|
|
264
|
-
* @returns {string}
|
|
265
|
-
*/
|
|
266
|
-
function capFixDiff(diff) {
|
|
267
|
-
const text = String(diff ?? '');
|
|
268
|
-
if (text.length <= FIX_DIFF_BUDGET) return text;
|
|
269
|
-
return `${text.slice(0, FIX_DIFF_BUDGET)}\n… (truncated to ${FIX_DIFF_BUDGET} chars)`;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function sameEvidence(a = [], b = []) {
|
|
273
|
-
if (a.length !== b.length) return false;
|
|
274
|
-
return a.every((v, i) => v === b[i]);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Append one deterministic candidate to the lessons ledger, deduplicating by
|
|
279
|
-
* `(kind, signature, evidence)`.
|
|
280
|
-
*
|
|
281
|
-
* The stored text is redacted before anything else: candidates keep the raw
|
|
282
|
-
* commands that failed, and a command can carry a token. Redaction runs before
|
|
283
|
-
* the signature is hashed so the same failure redacts to the same signature
|
|
284
|
-
* (review 0003 Q36).
|
|
285
|
-
*
|
|
286
|
-
* @param {string} root
|
|
287
|
-
* @param {object} candidate
|
|
288
|
-
* @returns {Promise<object>}
|
|
289
|
-
*/
|
|
290
236
|
export async function appendLessonCandidate(root, candidate) {
|
|
291
237
|
const { candidates } = await readLessonCandidates(root);
|
|
292
238
|
const kind = String(candidate.kind ?? 'fail-then-fix');
|
|
@@ -343,21 +289,15 @@ async function diffBetweenTrees(root, fromRef, toRef) {
|
|
|
343
289
|
}
|
|
344
290
|
try {
|
|
345
291
|
const [namesOut, diffOut] = await Promise.all([
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
maxBuffer: 8 << 20,
|
|
349
|
-
}),
|
|
350
|
-
exec('git', ['diff', '--unified=3', fromRef, toRef, '--', '.', ':(exclude).simt'], {
|
|
351
|
-
cwd: root,
|
|
352
|
-
maxBuffer: 8 << 20,
|
|
353
|
-
}),
|
|
292
|
+
git(root, ['diff', '--name-only', fromRef, toRef, '--', '.', ':(exclude).simt'], { maxBuffer: 8 << 20 }),
|
|
293
|
+
git(root, ['diff', '--unified=3', fromRef, toRef, '--', '.', ':(exclude).simt'], { maxBuffer: 8 << 20 }),
|
|
354
294
|
]);
|
|
355
|
-
const paths = namesOut
|
|
295
|
+
const paths = namesOut
|
|
356
296
|
.split('\n')
|
|
357
297
|
.map((s) => s.trim())
|
|
358
298
|
.filter(Boolean)
|
|
359
299
|
.sort();
|
|
360
|
-
return { paths, diff: diffOut.
|
|
300
|
+
return { paths, diff: diffOut.trim() };
|
|
361
301
|
} catch {
|
|
362
302
|
return { paths: [], diff: '' };
|
|
363
303
|
}
|
|
@@ -371,7 +311,8 @@ async function diffBetweenTrees(root, fromRef, toRef) {
|
|
|
371
311
|
* @param {{failEntry: object, passEntry: object, failOutput?: string, fixDiff?: string, paths?: string[]}} params
|
|
372
312
|
* @returns {Promise<object>}
|
|
373
313
|
*/
|
|
374
|
-
export async function recordFailThenFix(root, { failEntry, passEntry, failOutput, fixDiff, paths
|
|
314
|
+
export async function recordFailThenFix(root, { failEntry, passEntry, failOutput, fixDiff, paths
|
|
315
|
+
}) {
|
|
375
316
|
const rawError = failOutput ?? failEntry?.errorLine ?? failEntry?.summary ?? failEntry?.cmd ?? '';
|
|
376
317
|
const errorLine = normaliseErrorLine(failEntry?.errorLine ?? firstErrorLine(rawError));
|
|
377
318
|
const signature = failEntry?.signature ?? errorSignature(errorLine);
|
|
@@ -386,7 +327,7 @@ export async function recordFailThenFix(root, { failEntry, passEntry, failOutput
|
|
|
386
327
|
resolvedDiff ??= treeDiff.diff;
|
|
387
328
|
}
|
|
388
329
|
|
|
389
|
-
|
|
330
|
+
const record = await appendLessonCandidate(root, {
|
|
390
331
|
kind: 'fail-then-fix',
|
|
391
332
|
cmd: passEntry?.cmd ?? failEntry?.cmd ?? null,
|
|
392
333
|
signature,
|
|
@@ -396,6 +337,13 @@ export async function recordFailThenFix(root, { failEntry, passEntry, failOutput
|
|
|
396
337
|
paths: resolvedPaths,
|
|
397
338
|
fixDiff: resolvedDiff,
|
|
398
339
|
});
|
|
340
|
+
// The lessons file is the reader's view; the log is what a UI can tail (Q73).
|
|
341
|
+
await noteEvent(root, {
|
|
342
|
+
kind: 'lesson.captured',
|
|
343
|
+
subject: passEntry?.cmd ?? failEntry?.cmd ?? null,
|
|
344
|
+
data: { id: record?.id ?? null, signature, tree: fixTree },
|
|
345
|
+
});
|
|
346
|
+
return record;
|
|
399
347
|
}
|
|
400
348
|
|
|
401
349
|
/**
|
|
@@ -410,7 +358,7 @@ export async function captureFromLedger(root, entries = null) {
|
|
|
410
358
|
let list = entries;
|
|
411
359
|
if (!Array.isArray(list)) {
|
|
412
360
|
try {
|
|
413
|
-
const { text } = await readStateFile(root,
|
|
361
|
+
const { text } = await readStateFile(root, localRel(LEDGER_FILE_REL), [LEDGER_FILE_LEGACY]);
|
|
414
362
|
list = (text ?? '')
|
|
415
363
|
.split('\n')
|
|
416
364
|
.map((s) => s.trim())
|
|
@@ -453,12 +401,11 @@ export async function captureRevertedCommits(root, options = {}) {
|
|
|
453
401
|
const limit = options.limit ?? 50;
|
|
454
402
|
let stdout = '';
|
|
455
403
|
try {
|
|
456
|
-
|
|
457
|
-
|
|
404
|
+
stdout = await git(
|
|
405
|
+
root,
|
|
458
406
|
['log', '--no-merges', `-n`, String(limit), '--format=%H%x00%s%x00%b%x1e'],
|
|
459
|
-
{
|
|
407
|
+
{ maxBuffer: 8 << 20 },
|
|
460
408
|
);
|
|
461
|
-
stdout = res.stdout;
|
|
462
409
|
} catch {
|
|
463
410
|
return [];
|
|
464
411
|
}
|
|
@@ -482,16 +429,16 @@ export async function captureRevertedCommits(root, options = {}) {
|
|
|
482
429
|
let paths = [];
|
|
483
430
|
let fixDiff = '';
|
|
484
431
|
try {
|
|
485
|
-
const [
|
|
486
|
-
|
|
487
|
-
|
|
432
|
+
const [namesOut, diffOut] = await Promise.all([
|
|
433
|
+
git(root, ['diff-tree', '--no-commit-id', '--name-only', '-r', sha], { maxBuffer: 8 << 20 }),
|
|
434
|
+
git(root, ['show', '--format=', '--unified=3', sha], { maxBuffer: 8 << 20 }),
|
|
488
435
|
]);
|
|
489
|
-
paths =
|
|
436
|
+
paths = namesOut
|
|
490
437
|
.split('\n')
|
|
491
438
|
.map((s) => s.trim())
|
|
492
439
|
.filter(Boolean)
|
|
493
440
|
.sort();
|
|
494
|
-
fixDiff =
|
|
441
|
+
fixDiff = diffOut.trim();
|
|
495
442
|
} catch {
|
|
496
443
|
// ignore diff failure
|
|
497
444
|
}
|
|
@@ -634,7 +581,7 @@ export function renderLessonMarkdown(candidate, options = {}) {
|
|
|
634
581
|
* @returns {object}
|
|
635
582
|
*/
|
|
636
583
|
export function parseLessonFile(id, text, now = new Date()) {
|
|
637
|
-
const { front: rawFront, body: rawBody } =
|
|
584
|
+
const { front: rawFront, body: rawBody } = parseFrontmatter(text);
|
|
638
585
|
const front = rawFront ?? '';
|
|
639
586
|
const body = rawBody.trim();
|
|
640
587
|
|
package/src/name.mjs
CHANGED
|
@@ -46,6 +46,13 @@ export const LOCK_LEGACY = 'simt-dev.lock.json';
|
|
|
46
46
|
* markers, and the provenance header inside them. It follows the command, like the
|
|
47
47
|
* manifest: the region is the command's footprint in a file the repo owns.
|
|
48
48
|
*/
|
|
49
|
+
/** The ledger's pre-STATE_DIR home. Read, never written: state written under the old
|
|
50
|
+
* name is still read by the ledger and the lesson sources. */
|
|
51
|
+
export const LEDGER_FILE_LEGACY = '.simt/ledger.jsonl';
|
|
52
|
+
|
|
53
|
+
/** The ledger the evidence is appended to, under the config directory. */
|
|
54
|
+
export const LEDGER_FILE_REL = 'ledger.jsonl';
|
|
55
|
+
|
|
49
56
|
export const MARKER_PREFIX = 'simt';
|
|
50
57
|
|
|
51
58
|
/** What the region was namespaced under, and still is in every adopted repo. */
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// The runtime floor, in one place both entry points read (review 0003 Q54).
|
|
2
|
+
//
|
|
3
|
+
// `package.json` declares `engines.node` and `pyproject.toml` declares a python
|
|
4
|
+
// version; both are advisory to the installer, so an older runtime reached the CLI
|
|
5
|
+
// and failed somewhere inside it — a syntax error, a missing API — instead of the one
|
|
6
|
+
// sentence that says what to install. This module is deliberately tiny and has no
|
|
7
|
+
// imports: the bootstrap that calls it must not load the tool that cannot run.
|
|
8
|
+
|
|
9
|
+
/** The oldest Node this package runs on. Mirrors `package.json` `engines.node`. */
|
|
10
|
+
export const MIN_NODE = '22.20.0';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Whether `current` is at least `minimum`, compared component by component.
|
|
14
|
+
*
|
|
15
|
+
* A string compare is wrong ("9.0.0" > "22.20.0"), and `process.versions.node` may
|
|
16
|
+
* carry a pre-release suffix that `Number()` would turn into NaN, so the numeric
|
|
17
|
+
* prefix of each component is what counts.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} current a version such as `22.20.0` or `23.1.0-rc.1`
|
|
20
|
+
* @param {string} [minimum]
|
|
21
|
+
* @returns {boolean}
|
|
22
|
+
*/
|
|
23
|
+
export function satisfiesMinimum(current, minimum = MIN_NODE) {
|
|
24
|
+
const parse = (value) =>
|
|
25
|
+
String(value)
|
|
26
|
+
.split('.')
|
|
27
|
+
.map((part) => Number.parseInt(part, 10))
|
|
28
|
+
.map((part) => (Number.isFinite(part) ? part : 0));
|
|
29
|
+
const have = parse(current);
|
|
30
|
+
const want = parse(minimum);
|
|
31
|
+
for (let i = 0; i < Math.max(have.length, want.length); i += 1) {
|
|
32
|
+
const a = have[i] ?? 0;
|
|
33
|
+
const b = want[i] ?? 0;
|
|
34
|
+
if (a !== b) return a > b;
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
}
|