simt-dev 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -1
- package/README.md +169 -80
- package/docs/decisions/ADR-014-workspace-is-configuration.md +1 -1
- 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-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 +3 -1
- package/rules/mdc/intake-questions.mdc +8 -0
- package/src/advice/advice.mjs +3 -2
- package/src/agenthooks/render.mjs +86 -180
- package/src/agents/render.mjs +15 -18
- package/src/api.mjs +7 -5
- package/src/argv.mjs +19 -0
- package/src/bench/ablate.mjs +3 -4
- package/src/bench/adapters.json +155 -113
- package/src/bench/adapters.mjs +466 -0
- package/src/bench/arms.mjs +333 -0
- package/src/bench/grade.mjs +12 -26
- package/src/bench/graders.mjs +42 -0
- package/src/bench/mine.mjs +17 -535
- package/src/bench/probe-ledger.mjs +149 -0
- package/src/bench/probes.catalog.mjs +918 -0
- package/src/bench/probes.mjs +15 -1037
- package/src/bench/render.mjs +111 -0
- package/src/bench/report.mjs +116 -971
- package/src/bench/run.mjs +164 -881
- package/src/bench/variants.mjs +4 -5
- package/src/bench/workspace.mjs +160 -0
- package/src/brief/assemble.mjs +11 -13
- package/src/ci/dependabot.mjs +25 -55
- package/src/commands/agent.mjs +1 -9
- package/src/commands/agenthook.mjs +66 -35
- package/src/commands/bench.mjs +2 -2
- package/src/commands/ci.mjs +11 -13
- package/src/commands/docs.mjs +88 -0
- package/src/commands/gate.mjs +10 -13
- package/src/commands/harness.mjs +3 -3
- package/src/commands/help.mjs +44 -3
- package/src/commands/hooks.mjs +20 -20
- package/src/commands/ignore.mjs +11 -7
- package/src/commands/impact.mjs +4 -8
- package/src/commands/index.mjs +49 -9
- package/src/commands/init-scan.mjs +90 -0
- package/src/commands/init.mjs +122 -104
- package/src/commands/lease.mjs +67 -18
- package/src/commands/map.mjs +10 -5
- package/src/commands/mcp.mjs +2 -17
- package/src/commands/repoindex.mjs +8 -8
- package/src/commands/rules.mjs +8 -4
- package/src/commands/shared.mjs +12 -16
- package/src/commands/skills.mjs +5 -6
- package/src/commands/skillscompile.mjs +3 -3
- package/src/commands/skillsinstall.mjs +5 -4
- package/src/commands/slice.mjs +199 -64
- package/src/commands/spec.mjs +78 -62
- package/src/commands/status.mjs +24 -12
- package/src/commands/sync.mjs +14 -14
- package/src/commands/tests.mjs +12 -12
- package/src/commands/tokens.mjs +19 -24
- package/src/commands/upgrade.mjs +8 -4
- package/src/commands/why.mjs +6 -13
- package/src/compare/hooksstate.mjs +2 -6
- package/src/compare/learn.mjs +3 -1
- package/src/decisions/index.mjs +3 -2
- 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 +2 -1
- package/src/graph/spec.mjs +10 -8
- package/src/harness/records.mjs +143 -0
- package/src/hooks/capability.mjs +5 -3
- package/src/hooks/dispatcher.mjs +26 -32
- package/src/hooks/gates.mjs +12 -2
- package/src/hooks/resolve.mjs +2 -7
- package/src/hooks/where.mjs +2 -7
- package/src/identity.mjs +2 -11
- 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 +16 -11
- 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 +124 -17
- package/src/mcp/paginate.mjs +33 -0
- package/src/mcp/schemas.mjs +512 -0
- package/src/mcp/server.mjs +49 -9
- package/src/mcp/tools-facts.mjs +326 -0
- package/src/mcp/tools-read.mjs +221 -0
- package/src/mcp/tools.mjs +4 -1005
- package/src/{bench → measure}/corpus.mjs +1 -1
- package/src/measure/metrics.mjs +236 -0
- package/src/measure/records.mjs +211 -0
- 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/output/index.mjs +122 -31
- package/src/packs/manage.mjs +15 -9
- 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/manifest.mjs +10 -435
- 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 +16 -547
- package/src/projection/surfaces.mjs +43 -210
- package/src/projection/targets.mjs +5 -5
- package/src/repo-root.mjs +13 -1
- package/src/resolve/eval.mjs +2 -1
- package/src/resolve/fingerprint.mjs +0 -7
- package/src/resolve/resolve.mjs +1 -41
- package/src/review/critic.mjs +8 -12
- 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/{bench/process.mjs → sys/proc.mjs} +28 -5
- 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 +1 -1
- package/src/util/fs.mjs +20 -0
- package/src/util/sort.mjs +12 -0
- package/src/workspace.mjs +3 -1
- 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/src/bench/tasks.mjs +0 -36
package/src/hooks/dispatcher.mjs
CHANGED
|
@@ -18,13 +18,12 @@
|
|
|
18
18
|
// Report what did not run. The summary names every gate, its disposition and
|
|
19
19
|
// its reason, so "passed" can never be mistaken for "only some of it passed".
|
|
20
20
|
|
|
21
|
-
import {
|
|
21
|
+
import { spawn } from 'node:child_process';
|
|
22
22
|
import { mkdtemp, readFile, rm, stat, symlink } from 'node:fs/promises';
|
|
23
23
|
import { tmpdir } from 'node:os';
|
|
24
24
|
import { basename, join, matchesGlob, resolve, sep } from 'node:path';
|
|
25
|
-
import { promisify } from 'node:util';
|
|
26
25
|
|
|
27
|
-
import { GATES } from './gates.mjs';
|
|
26
|
+
import { GATES, getGate } from './gates.mjs';
|
|
28
27
|
import { resolveGate } from './resolve.mjs';
|
|
29
28
|
import { resolveNodeRunner } from '../ecosystem/node-runner.mjs';
|
|
30
29
|
import { CLI } from '../name.mjs';
|
|
@@ -32,8 +31,7 @@ import { MANIFEST_FILE } from '../name.mjs';
|
|
|
32
31
|
import { assertVersionPin } from '../package-version.mjs';
|
|
33
32
|
import { hasTestEvidence, readLedger, recordRun } from '../ledger/index.mjs';
|
|
34
33
|
import { renderFindings, scanStaged } from '../secrets.mjs';
|
|
35
|
-
|
|
36
|
-
const exec = promisify(execFile);
|
|
34
|
+
import { git, gitPaths, gitText } from '../sys/git.mjs';
|
|
37
35
|
|
|
38
36
|
/**
|
|
39
37
|
* The default bound on one gate, in seconds, and the grace a killed gate gets before
|
|
@@ -194,15 +192,14 @@ export function parsePushedRefs(text) {
|
|
|
194
192
|
async function tagCommitAccountedFor(root, tagSha, pushingTips) {
|
|
195
193
|
let commit;
|
|
196
194
|
try {
|
|
197
|
-
|
|
195
|
+
commit = await gitText(root, ['rev-parse', `${tagSha}^{commit}`]);
|
|
198
196
|
} catch (error) {
|
|
199
197
|
return { ok: false, reason: `the tag could not be resolved to a commit: ${String(error.stderr ?? error.message).trim()}` };
|
|
200
198
|
}
|
|
201
|
-
commit = commit.trim();
|
|
202
199
|
|
|
203
200
|
let remoteRefs;
|
|
204
201
|
try {
|
|
205
|
-
|
|
202
|
+
remoteRefs = await gitText(root, ['for-each-ref', 'refs/remotes/', '--format=%(refname)']);
|
|
206
203
|
} catch (error) {
|
|
207
204
|
return { ok: false, reason: `the remote refs could not be listed: ${String(error.stderr ?? error.message).trim()}` };
|
|
208
205
|
}
|
|
@@ -215,7 +212,7 @@ async function tagCommitAccountedFor(root, tagSha, pushingTips) {
|
|
|
215
212
|
}
|
|
216
213
|
for (const candidate of candidates) {
|
|
217
214
|
try {
|
|
218
|
-
await
|
|
215
|
+
await git(root, ['merge-base', '--is-ancestor', commit, candidate]);
|
|
219
216
|
return { ok: true, reason: null };
|
|
220
217
|
} catch {
|
|
221
218
|
// not reachable from this ref; try the next candidate
|
|
@@ -257,17 +254,17 @@ export async function classifyPush(root, refs, { maxCommits, paths }) {
|
|
|
257
254
|
}
|
|
258
255
|
let count, names;
|
|
259
256
|
try {
|
|
260
|
-
|
|
261
|
-
|
|
257
|
+
count = await gitText(root, ['rev-list', '--count', `${remoteSha}..${localSha}`]);
|
|
258
|
+
names = await gitPaths(root, ['diff', '--name-only', `${remoteSha}..${localSha}`]);
|
|
262
259
|
} catch (error) {
|
|
263
260
|
return { docsOnly: false, reason: `the push could not be diffed: ${String(error.stderr ?? error.message).trim()}` };
|
|
264
261
|
}
|
|
265
|
-
commits += Number(count
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
for (const name of names
|
|
270
|
-
|
|
262
|
+
commits += Number(count);
|
|
263
|
+
// NUL-separated paths because a path can contain a newline, and git quotes such a
|
|
264
|
+
// name in the line-terminated form, so splitting on newlines compares a quoted path
|
|
265
|
+
// against an unquoted glob and never matches.
|
|
266
|
+
for (const name of names) {
|
|
267
|
+
files.add(name);
|
|
271
268
|
}
|
|
272
269
|
}
|
|
273
270
|
|
|
@@ -339,11 +336,10 @@ async function resolveDocsOnlySkip(root, manifest, pushed) {
|
|
|
339
336
|
async function resolveAlreadyPassedSkip(root) {
|
|
340
337
|
let headTree;
|
|
341
338
|
try {
|
|
342
|
-
|
|
339
|
+
headTree = await gitText(root, ['rev-parse', 'HEAD^{tree}']);
|
|
343
340
|
} catch {
|
|
344
341
|
return null;
|
|
345
342
|
}
|
|
346
|
-
headTree = headTree.trim();
|
|
347
343
|
if (!/^[0-9a-f]{40}$/.test(headTree)) return null;
|
|
348
344
|
const { entries, errors } = await readLedger(root);
|
|
349
345
|
if (errors.length > 0) return null;
|
|
@@ -508,8 +504,8 @@ async function runGatesAt(event, { root, ledgerRoot, manifest, declared, key, sk
|
|
|
508
504
|
let formatted = [];
|
|
509
505
|
|
|
510
506
|
for (const name of declared) {
|
|
511
|
-
const gate =
|
|
512
|
-
if (gate ===
|
|
507
|
+
const gate = getGate(name, commands);
|
|
508
|
+
if (gate === null) {
|
|
513
509
|
results.push({
|
|
514
510
|
name,
|
|
515
511
|
status: 'failed',
|
|
@@ -727,7 +723,7 @@ async function runGatesAt(event, { root, ledgerRoot, manifest, declared, key, sk
|
|
|
727
723
|
async function materializeHeadWorktree(root) {
|
|
728
724
|
const path = await mkdtemp(join(tmpdir(), 'head-gate-'));
|
|
729
725
|
try {
|
|
730
|
-
await
|
|
726
|
+
await git(root, ['worktree', 'add', '--detach', path, 'HEAD']);
|
|
731
727
|
} catch (error) {
|
|
732
728
|
await rm(path, { recursive: true, force: true }).catch(() => {});
|
|
733
729
|
return { path: null, error: `git worktree add failed: ${String(error.stderr ?? error.message).trim()}` };
|
|
@@ -752,9 +748,9 @@ async function materializeHeadWorktree(root) {
|
|
|
752
748
|
* @param {string} path the worktree to remove
|
|
753
749
|
*/
|
|
754
750
|
async function removeHeadWorktree(root, path) {
|
|
755
|
-
await
|
|
751
|
+
await git(root, ['worktree', 'remove', '--force', path]).catch(async () => {
|
|
756
752
|
await rm(path, { recursive: true, force: true }).catch(() => {});
|
|
757
|
-
await
|
|
753
|
+
await git(root, ['worktree', 'prune']).catch(() => {});
|
|
758
754
|
});
|
|
759
755
|
}
|
|
760
756
|
|
|
@@ -877,10 +873,9 @@ async function readPkg(root) {
|
|
|
877
873
|
/** Staged paths, or [] when git cannot answer (an unborn branch, for instance). */
|
|
878
874
|
export async function stagedFiles(root) {
|
|
879
875
|
try {
|
|
880
|
-
//
|
|
881
|
-
// quoted name is not the path the index holds, so it never matches a staged blob.
|
|
882
|
-
|
|
883
|
-
return stdout.split('\0').filter(Boolean);
|
|
876
|
+
// NUL-separated: git quotes a path containing a newline or a quote in the line form,
|
|
877
|
+
// and a quoted name is not the path the index holds, so it never matches a staged blob.
|
|
878
|
+
return await gitPaths(root, ['diff', '--cached', '--name-only', '--diff-filter=ACMR']);
|
|
884
879
|
} catch {
|
|
885
880
|
return [];
|
|
886
881
|
}
|
|
@@ -896,8 +891,7 @@ export async function stagedFiles(root) {
|
|
|
896
891
|
*/
|
|
897
892
|
async function unstagedFiles(root) {
|
|
898
893
|
try {
|
|
899
|
-
|
|
900
|
-
return new Set(stdout.split('\0').filter(Boolean));
|
|
894
|
+
return new Set(await gitPaths(root, ['diff', '--name-only']));
|
|
901
895
|
} catch {
|
|
902
896
|
return new Set();
|
|
903
897
|
}
|
|
@@ -907,7 +901,7 @@ async function unstagedFiles(root) {
|
|
|
907
901
|
async function stagedBlobs(root, files) {
|
|
908
902
|
if (files.length === 0) return new Map();
|
|
909
903
|
try {
|
|
910
|
-
const
|
|
904
|
+
const stdout = await git(root, ['ls-files', '-s', '--', ...files]);
|
|
911
905
|
const blobs = new Map();
|
|
912
906
|
for (const line of stdout.split('\n')) {
|
|
913
907
|
const [meta, ...rest] = line.split('\t');
|
|
@@ -932,7 +926,7 @@ async function restage(root, files) {
|
|
|
932
926
|
if (files.length === 0) return { restaged: [], error: null };
|
|
933
927
|
const before = await stagedBlobs(root, files);
|
|
934
928
|
try {
|
|
935
|
-
await
|
|
929
|
+
await git(root, ['add', '--', ...files]);
|
|
936
930
|
} catch (error) {
|
|
937
931
|
return { restaged: [], error: String(error.stderr ?? error.message ?? error).trim() };
|
|
938
932
|
}
|
package/src/hooks/gates.mjs
CHANGED
|
@@ -101,8 +101,18 @@ export const GATES = {
|
|
|
101
101
|
},
|
|
102
102
|
};
|
|
103
103
|
|
|
104
|
-
export function getGate(name) {
|
|
105
|
-
|
|
104
|
+
export function getGate(name, commands = {}) {
|
|
105
|
+
const declared = GATES[name];
|
|
106
|
+
if (declared) return declared;
|
|
107
|
+
// Q70: a command the repository declared under gates.commands is a valid gate even when
|
|
108
|
+
// no central row knows it. The manifest is the source of truth for a repo's gates, and
|
|
109
|
+
// a name that resolves to a declared command must not read as unknown. Synthesized, not
|
|
110
|
+
// added to GATES: the central table stays the estate's gates, the record supplies the rest.
|
|
111
|
+
const cmd = commands?.[name];
|
|
112
|
+
if (typeof cmd === 'string' && cmd.trim() !== '') {
|
|
113
|
+
return { name, kind: 'command', adr: 'ADR-010', why: 'declared by the repository under gates.commands' };
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
106
116
|
}
|
|
107
117
|
|
|
108
118
|
export function gateNames() {
|
package/src/hooks/resolve.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { access } from 'node:fs/promises';
|
|
14
14
|
import { constants } from 'node:fs';
|
|
15
|
-
import { join } from 'node:path';
|
|
15
|
+
import { extname, join } from 'node:path';
|
|
16
16
|
|
|
17
17
|
import { FORMATTABLE, GATES, gateNames } from './gates.mjs';
|
|
18
18
|
import { MANIFEST_FILE } from '../name.mjs';
|
|
@@ -139,7 +139,7 @@ export async function resolveGate(gate, { root, pkg, staged, commands, runner =
|
|
|
139
139
|
remedy: `run the repo install command, or remove "format" from ${MANIFEST_FILE} gates`,
|
|
140
140
|
};
|
|
141
141
|
}
|
|
142
|
-
const files = staged.filter((f) => FORMATTABLE.has(extname(f)));
|
|
142
|
+
const files = staged.filter((f) => FORMATTABLE.has(extname(f).toLowerCase()));
|
|
143
143
|
if (files.length === 0) {
|
|
144
144
|
return {
|
|
145
145
|
ok: true,
|
|
@@ -200,9 +200,4 @@ export async function resolveGate(gate, { root, pkg, staged, commands, runner =
|
|
|
200
200
|
reason: `unknown gate kind "${gate.kind}"`,
|
|
201
201
|
remedy: `this is a bug in simt-dev; )}', ')}`,
|
|
202
202
|
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function extname(path) {
|
|
206
|
-
const dot = path.lastIndexOf('.');
|
|
207
|
-
return dot > 0 ? path.slice(dot).toLowerCase() : '';
|
|
208
203
|
}
|
package/src/hooks/where.mjs
CHANGED
|
@@ -19,17 +19,12 @@
|
|
|
19
19
|
// ADR-012 dropped Windows, but nothing here is POSIX-specific; the worktree
|
|
20
20
|
// handling is the only subtlety and it is the same on every platform.
|
|
21
21
|
|
|
22
|
-
import { execFile } from 'node:child_process';
|
|
23
22
|
import { readdir, stat } from 'node:fs/promises';
|
|
24
23
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
25
|
-
import { promisify } from 'node:util';
|
|
26
24
|
|
|
27
|
-
|
|
25
|
+
import { gitText } from '../sys/git.mjs';
|
|
28
26
|
|
|
29
|
-
|
|
30
|
-
const { stdout } = await exec('git', args, { cwd: root });
|
|
31
|
-
return stdout.trim();
|
|
32
|
-
}
|
|
27
|
+
const git = (root, args) => gitText(root, args);
|
|
33
28
|
|
|
34
29
|
/**
|
|
35
30
|
* The directory git runs hooks from, and which of the two rules produced it.
|
package/src/identity.mjs
CHANGED
|
@@ -24,14 +24,12 @@
|
|
|
24
24
|
// one; a repository that already tracks it is left that way, because removing a
|
|
25
25
|
// tracked file is not something an adopting command should do silently.
|
|
26
26
|
|
|
27
|
-
import { execFile } from 'node:child_process';
|
|
28
27
|
import { readFile, stat, writeFile } from 'node:fs/promises';
|
|
29
28
|
import { join } from 'node:path';
|
|
30
|
-
import { promisify } from 'node:util';
|
|
31
29
|
|
|
32
30
|
import { journaledGitConfig, journaledWriteFile } from './journal.mjs';
|
|
31
|
+
import { gitText } from './sys/git.mjs';
|
|
33
32
|
|
|
34
|
-
const exec = promisify(execFile);
|
|
35
33
|
|
|
36
34
|
/** The file, at the repository root. Named for what it is: a git config fragment. */
|
|
37
35
|
export const IDENTITY_FILE = 'git.config';
|
|
@@ -50,14 +48,7 @@ export const INCLUDE_KEY = 'include.path';
|
|
|
50
48
|
* @param {string[]} args
|
|
51
49
|
* @returns {Promise<string|null>}
|
|
52
50
|
*/
|
|
53
|
-
|
|
54
|
-
try {
|
|
55
|
-
const { stdout } = await exec('git', args, { cwd });
|
|
56
|
-
return stdout.trim();
|
|
57
|
-
} catch {
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
51
|
+
const git = (cwd, args) => gitText(cwd, args, { allowFailure: true });
|
|
61
52
|
|
|
62
53
|
/**
|
|
63
54
|
* Who last committed here.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Which tests a change should run, from one place (Q77).
|
|
2
|
+
//
|
|
3
|
+
// Four callers used to answer this question four ways: `tests` walked the stored test
|
|
4
|
+
// edges directly, `gate` walked the import graph transitively over a re-read worktree,
|
|
5
|
+
// `impact` asked `dependents` which asked `selectTests`, and `brief` used the edges again.
|
|
6
|
+
// The answers disagreed — `simt tests src/index/persist.mjs` named one test where `simt
|
|
7
|
+
// gate` on the same file named dozens — and every disagreement was a reader trusting the
|
|
8
|
+
// wrong one.
|
|
9
|
+
//
|
|
10
|
+
// The two walks are not the same question, so they are not merged into one answer:
|
|
11
|
+
// `transitive` is the import closure over changed paths, and the direct walk is the test
|
|
12
|
+
// edges the index stores. Each caller keeps its default; what changes is that there is
|
|
13
|
+
// now one implementation of each.
|
|
14
|
+
|
|
15
|
+
import { selectTests } from './gate.mjs';
|
|
16
|
+
import { testEdges, testsFor } from './tests.mjs';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Tests that exercise `paths`.
|
|
20
|
+
*
|
|
21
|
+
* @param {{files?: {path: string}[]|string[], imports?: {src: string, dst: string, kind?: string}[],
|
|
22
|
+
* coverage?: string[], commits?: string[][]}} graph
|
|
23
|
+
* @param {Iterable<string>} paths
|
|
24
|
+
* @param {{transitive?: boolean, cochange?: boolean}} [options]
|
|
25
|
+
* `transitive` (default true) is the import closure; `cochange` adds files that land in
|
|
26
|
+
* the same commits, and is only ever on when a caller asked for it.
|
|
27
|
+
* @returns {string[]}
|
|
28
|
+
*/
|
|
29
|
+
export function coverage(graph, paths, { transitive = true, cochange = false } = {}) {
|
|
30
|
+
const changed = [...new Set([...paths].filter(Boolean))];
|
|
31
|
+
const imports = graph.imports ?? [];
|
|
32
|
+
if (transitive) {
|
|
33
|
+
const selected = selectTests(changed, imports);
|
|
34
|
+
if (!cochange || graph.files === undefined) return selected;
|
|
35
|
+
const edges = testEdges({
|
|
36
|
+
files: graph.files,
|
|
37
|
+
imports,
|
|
38
|
+
coverage: graph.coverage ?? [],
|
|
39
|
+
commits: graph.commits ?? [],
|
|
40
|
+
});
|
|
41
|
+
const found = new Set(selected);
|
|
42
|
+
for (const path of changed) {
|
|
43
|
+
for (const test of testsFor(path, edges, { cochange: true })) found.add(test);
|
|
44
|
+
}
|
|
45
|
+
return [...found].sort();
|
|
46
|
+
}
|
|
47
|
+
const edges = testEdges({
|
|
48
|
+
files: graph.files ?? [],
|
|
49
|
+
imports,
|
|
50
|
+
coverage: graph.coverage ?? [],
|
|
51
|
+
commits: graph.commits ?? [],
|
|
52
|
+
});
|
|
53
|
+
const found = new Set();
|
|
54
|
+
for (const path of changed) {
|
|
55
|
+
for (const test of testsFor(path, edges, { cochange })) found.add(test);
|
|
56
|
+
}
|
|
57
|
+
return [...found].sort();
|
|
58
|
+
}
|
package/src/index/extract.mjs
CHANGED
|
@@ -4,21 +4,14 @@
|
|
|
4
4
|
// line at a time: an export, a definition, an import. That is enough for a map
|
|
5
5
|
// and an import graph, and it is the same result on every machine.
|
|
6
6
|
|
|
7
|
+
import { cmp } from '../util/sort.mjs';
|
|
8
|
+
// Q70: the extension table is the records' `languages[].exts` (ADR-013: content is data,
|
|
9
|
+
// the engine is code). A new language lands in a record, not a row the extractor edits.
|
|
10
|
+
import { languageExts } from '../ecosystem/records.mjs';
|
|
11
|
+
|
|
7
12
|
const IDENT = '[A-Za-z_$][\\w$]*';
|
|
8
13
|
|
|
9
|
-
const EXTS =
|
|
10
|
-
javascript: ['.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx'],
|
|
11
|
-
typescript: ['.ts', '.tsx', '.js', '.mjs', '.jsx'],
|
|
12
|
-
kotlin: ['.kt', '.kts'],
|
|
13
|
-
java: ['.java'],
|
|
14
|
-
python: ['.py'],
|
|
15
|
-
go: ['.go'],
|
|
16
|
-
rust: ['.rs'],
|
|
17
|
-
swift: ['.swift'],
|
|
18
|
-
dart: ['.dart'],
|
|
19
|
-
ruby: ['.rb'],
|
|
20
|
-
csharp: ['.cs'],
|
|
21
|
-
};
|
|
14
|
+
const EXTS = languageExts();
|
|
22
15
|
|
|
23
16
|
/**
|
|
24
17
|
* Symbols and import specifiers in one blob.
|
|
@@ -261,9 +254,6 @@ function byEdge(a, b) {
|
|
|
261
254
|
return cmp(a.src, b.src) || cmp(a.dst, b.dst) || cmp(a.kind, b.kind);
|
|
262
255
|
}
|
|
263
256
|
|
|
264
|
-
function cmp(a, b) {
|
|
265
|
-
return a < b ? -1 : a > b ? 1 : 0;
|
|
266
|
-
}
|
|
267
257
|
|
|
268
258
|
function sigOf(line) {
|
|
269
259
|
return line.trim().slice(0, 200);
|
package/src/index/gate.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { readFile } from 'node:fs/promises';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
|
|
11
|
-
import { isTestPath } from '../
|
|
11
|
+
import { isTestPath } from '../testing/paths.mjs';
|
|
12
12
|
import { langOf } from './store.mjs';
|
|
13
13
|
import { importsOf } from './tests.mjs';
|
|
14
14
|
|
package/src/index/govern.mjs
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
// and a lesson do the same with their own path list. Nothing here is stored: the
|
|
7
7
|
// answer is the records on disk.
|
|
8
8
|
|
|
9
|
-
import { parseDecisionFrontmatter
|
|
9
|
+
import { parseDecisionFrontmatter } from '../memory/decide.mjs';
|
|
10
|
+
import { parseFrontmatter } from '../text/frontmatter.mjs';
|
|
11
|
+
import { cmp } from '../util/sort.mjs';
|
|
10
12
|
|
|
11
13
|
const SIBLING = /^\*{0,2}[A-Z][\w-]*(?:[ \t]+[\w-]+){0,3}:\*{0,2}(?:[ \t]|$)/;
|
|
12
14
|
const HEADING = /^#{1,6}\s/;
|
|
@@ -18,7 +20,7 @@ const TOOL = /^(?:simt-dev|simt)\b/;
|
|
|
18
20
|
*/
|
|
19
21
|
export function affectsTokens(text) {
|
|
20
22
|
const fm = parseDecisionFrontmatter(text);
|
|
21
|
-
const { body } =
|
|
23
|
+
const { body } = parseFrontmatter(text);
|
|
22
24
|
const lines = String(body).split('\n');
|
|
23
25
|
const start = lines.findIndex((line) => /^\s*\*{0,2}Affects:\*{0,2}/.test(line));
|
|
24
26
|
const proseTokens = [];
|
|
@@ -131,7 +133,27 @@ export function scopeHits(file, { rules = [], skills = [], gates = [], lessons =
|
|
|
131
133
|
return hits;
|
|
132
134
|
}
|
|
133
135
|
|
|
134
|
-
|
|
136
|
+
/**
|
|
137
|
+
* What governs one file: decisions whose Affects line names it, then the rules, skills,
|
|
138
|
+
* gates and lessons whose path lists match, in one stable order.
|
|
139
|
+
*
|
|
140
|
+
* `why`, `impact` and the critic each assembled this themselves, with different corpora
|
|
141
|
+
* and - in `why`'s case - a hard-coded list of this repository's own rule directories
|
|
142
|
+
* (Q77). The corpus is now the caller's, so a receiving repository's directories are the
|
|
143
|
+
* ones read.
|
|
144
|
+
*
|
|
145
|
+
* @param {string} file
|
|
146
|
+
* @param {{records?: {id: string, title: string, text: string}[], ctx: {files: Set<string>,
|
|
147
|
+
* commands: Set<string>, roots: string[]}, rules?: object[], skills?: object[],
|
|
148
|
+
* gates?: object[], lessons?: object[], commands?: Record<string, string>}} corpus
|
|
149
|
+
*/
|
|
150
|
+
export function governs(file, corpus) {
|
|
151
|
+
const { records = [], ctx, ...scopes } = corpus;
|
|
152
|
+
const { hits } = decisionHits(records, file, ctx);
|
|
153
|
+
return sortHits([...hits, ...scopeHits(file, scopes)]);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function sortHits(hits) {
|
|
135
157
|
const order = { decision: 0, rule: 1, skill: 2, gate: 3, lesson: 4 };
|
|
136
158
|
return [...hits].sort((a, b) => (order[a.kind] - order[b.kind]) || cmp(a.id, b.id));
|
|
137
159
|
}
|
|
@@ -218,7 +240,7 @@ function extension(file) {
|
|
|
218
240
|
}
|
|
219
241
|
|
|
220
242
|
function frontmatter(text) {
|
|
221
|
-
return
|
|
243
|
+
return parseFrontmatter(text).front ?? '';
|
|
222
244
|
}
|
|
223
245
|
|
|
224
246
|
function quotedList(front, key) {
|
|
@@ -227,8 +249,3 @@ function quotedList(front, key) {
|
|
|
227
249
|
return [...match[1].matchAll(/['"]([^'"]+)['"]/g)].map((item) => item[1]);
|
|
228
250
|
}
|
|
229
251
|
|
|
230
|
-
function cmp(a, b) {
|
|
231
|
-
if (a < b) return -1;
|
|
232
|
-
if (a > b) return 1;
|
|
233
|
-
return 0;
|
|
234
|
-
}
|
package/src/index/impact.mjs
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
// touches. The walk stops at a fixed count: an unbounded report is not an
|
|
6
6
|
// answer a reader can use.
|
|
7
7
|
|
|
8
|
-
import { isTestPath } from '../
|
|
9
|
-
import {
|
|
8
|
+
import { isTestPath } from '../testing/paths.mjs';
|
|
9
|
+
import { coverage } from './coverage.mjs';
|
|
10
|
+
import { cmp } from '../util/sort.mjs';
|
|
10
11
|
|
|
11
12
|
const IMPACT_LIMIT = 64;
|
|
12
13
|
|
|
@@ -50,7 +51,7 @@ export function dependents(file, imports, limit = IMPACT_LIMIT) {
|
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
importers.sort();
|
|
53
|
-
const tests =
|
|
54
|
+
const tests = coverage({ imports }, [file]).filter((path) => path !== file);
|
|
54
55
|
const shown = tests.length > limit ? tests.slice(0, limit) : tests;
|
|
55
56
|
if (tests.length > limit) truncated = true;
|
|
56
57
|
return { importers, tests: shown, truncated };
|
|
@@ -179,8 +180,3 @@ function glob(pattern, text) {
|
|
|
179
180
|
return new RegExp(`^${body}$`).test(text);
|
|
180
181
|
}
|
|
181
182
|
|
|
182
|
-
function cmp(a, b) {
|
|
183
|
-
if (a < b) return -1;
|
|
184
|
-
if (a > b) return 1;
|
|
185
|
-
return 0;
|
|
186
|
-
}
|
package/src/index/map.mjs
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
import { readdir, readFile } from 'node:fs/promises';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
|
|
12
|
-
import { isTestPath
|
|
12
|
+
import { isTestPath } from '../testing/paths.mjs';
|
|
13
|
+
import { resolveVaultDir } from '../testing/vault.mjs';
|
|
13
14
|
import { readLocalConfig } from '../local/config.mjs';
|
|
14
15
|
import { localEmbed } from '../local/client.mjs';
|
|
15
16
|
import {
|
|
@@ -22,6 +23,8 @@ import {
|
|
|
22
23
|
scoreFtsBm25,
|
|
23
24
|
searchFtsBm25,
|
|
24
25
|
} from './store.mjs';
|
|
26
|
+
import { cmp } from '../util/sort.mjs';
|
|
27
|
+
import { INDEX_STOP as STOP } from '../text/tokens.mjs';
|
|
25
28
|
|
|
26
29
|
export const ITERATIONS = 20;
|
|
27
30
|
export const DAMPING = 0.85;
|
|
@@ -30,14 +33,6 @@ export const EMBED_RECALL_GAIN_BAR = 0.1;
|
|
|
30
33
|
const TOP = 10;
|
|
31
34
|
export const DEFAULT_BUDGET = 2048;
|
|
32
35
|
|
|
33
|
-
const STOP = new Set([
|
|
34
|
-
'this', 'that', 'with', 'from', 'have', 'been', 'into', 'only', 'when', 'what',
|
|
35
|
-
'which', 'their', 'about', 'after', 'before', 'where', 'there', 'these', 'those',
|
|
36
|
-
'would', 'could', 'should', 'every', 'other', 'than', 'then', 'them', 'they',
|
|
37
|
-
'your', 'will', 'does', 'file', 'files', 'test', 'tests', 'line', 'lines', 'code',
|
|
38
|
-
'also', 'just', 'more', 'some', 'such', 'each', 'same', 'over', 'used', 'using',
|
|
39
|
-
]);
|
|
40
|
-
|
|
41
36
|
/**
|
|
42
37
|
* Reciprocal-rank fusion (RRF) over multiple ranked lists.
|
|
43
38
|
*
|
|
@@ -405,16 +400,25 @@ function pageRank(nodes, links, scores) {
|
|
|
405
400
|
let rank = pers.slice();
|
|
406
401
|
for (let iter = 0; iter < ITERATIONS; iter += 1) {
|
|
407
402
|
const next = pers.map((value) => (1 - DAMPING) * value);
|
|
403
|
+
// Dangling nodes (no outgoing edges) spread their rank over the whole
|
|
404
|
+
// distribution. Accumulating their mass first makes that one pass over j
|
|
405
|
+
// instead of one pass per dangling node - the no-task rank of a 30k-file
|
|
406
|
+
// repo used to go quadratic here (Q76).
|
|
407
|
+
let danglingMass = 0;
|
|
408
408
|
for (let i = 0; i < n; i += 1) {
|
|
409
409
|
const outs = links[i];
|
|
410
410
|
if (outs.length === 0) {
|
|
411
|
-
|
|
411
|
+
danglingMass += rank[i];
|
|
412
412
|
} else {
|
|
413
413
|
let sum = 0;
|
|
414
414
|
for (const [, weight] of outs) sum += weight;
|
|
415
415
|
for (const [j, weight] of outs) next[j] += (DAMPING * rank[i] * weight) / sum;
|
|
416
416
|
}
|
|
417
417
|
}
|
|
418
|
+
if (danglingMass > 0) {
|
|
419
|
+
const share = DAMPING * danglingMass;
|
|
420
|
+
for (let j = 0; j < n; j += 1) next[j] += share * pers[j];
|
|
421
|
+
}
|
|
418
422
|
rank = next;
|
|
419
423
|
}
|
|
420
424
|
return rank;
|
|
@@ -620,8 +624,3 @@ function round6(value) {
|
|
|
620
624
|
return Math.round(value * 1e6) / 1e6;
|
|
621
625
|
}
|
|
622
626
|
|
|
623
|
-
function cmp(a, b) {
|
|
624
|
-
if (a < b) return -1;
|
|
625
|
-
if (a > b) return 1;
|
|
626
|
-
return 0;
|
|
627
|
-
}
|