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,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
|
-
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// The index as a handle (review 0003 Q76).
|
|
2
|
+
//
|
|
3
|
+
// `openIndex` returns a handle whose methods answer with pure data — rows and
|
|
4
|
+
// counts, no exit codes, no formatted lines — and with a structured error,
|
|
5
|
+
// `{ ok: false, error: { code, message } }`, when they cannot. The methods are
|
|
6
|
+
// thin over the same primitives the commands use, so a handle and a command
|
|
7
|
+
// cannot disagree about the same index.
|
|
8
|
+
//
|
|
9
|
+
// The graph loads once at open: files, symbols, edges. FTS bodies run in
|
|
10
|
+
// SQLite (`search` delegates to `searchFtsBm25`) and stored vectors are not
|
|
11
|
+
// loaded at all: a map without `--embed` does not touch them (Q76).
|
|
12
|
+
|
|
13
|
+
import { coverage } from './coverage.mjs';
|
|
14
|
+
import { governs } from './govern.mjs';
|
|
15
|
+
import { dependents } from './impact.mjs';
|
|
16
|
+
import { rankFiles } from './map.mjs';
|
|
17
|
+
import { isIndexStale, readEdges, readFiles, readSymbols, reindex, searchFtsBm25 } from './store.mjs';
|
|
18
|
+
|
|
19
|
+
const INDEX_CODES = {
|
|
20
|
+
BUILD: 'E_BUILD',
|
|
21
|
+
CLOSED: 'E_CLOSED',
|
|
22
|
+
NO_INDEX: 'E_NO_INDEX',
|
|
23
|
+
READ: 'E_READ',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function ok(data) {
|
|
27
|
+
return { ok: true, data };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function fail(code, message) {
|
|
31
|
+
return { ok: false, error: { code, message } };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The files, symbols and edges the index holds. The lazy projection of
|
|
36
|
+
* `loadGraph`: no FTS bodies, no vectors, because nothing on this handle
|
|
37
|
+
* scores them.
|
|
38
|
+
*/
|
|
39
|
+
async function loadGraphData(root) {
|
|
40
|
+
const [files, symbols, edges] = await Promise.all([
|
|
41
|
+
readFiles(root),
|
|
42
|
+
readSymbols(root),
|
|
43
|
+
readEdges(root),
|
|
44
|
+
]);
|
|
45
|
+
return { files, symbols, edges };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Open the repository's index.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} root
|
|
52
|
+
* @param {{build?: boolean, refresh?: boolean}} [options]
|
|
53
|
+
* `build` (default true) refreshes the index from the git index before the
|
|
54
|
+
* first read — incrementally, so a current repository pays for one
|
|
55
|
+
* `git ls-files -s`, not a walk of the tree. `refresh` re-reads the graph on
|
|
56
|
+
* every call instead of caching it at open.
|
|
57
|
+
* @returns {Promise<IndexHandle | {ok: false, error: {code: string, message: string}}>}
|
|
58
|
+
*/
|
|
59
|
+
export async function openIndex(root, { build = true, refresh = false } = {}) {
|
|
60
|
+
if (build) {
|
|
61
|
+
try {
|
|
62
|
+
await reindex(root);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return fail(INDEX_CODES.BUILD, `could not build the index (${error.message})`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let closed = false;
|
|
69
|
+
let cached = null;
|
|
70
|
+
|
|
71
|
+
async function graph() {
|
|
72
|
+
if (closed) throw new Error(INDEX_CODES.CLOSED);
|
|
73
|
+
if (cached === null || refresh) cached = await loadGraphData(root);
|
|
74
|
+
return cached;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function answered(fn) {
|
|
78
|
+
try {
|
|
79
|
+
return await fn();
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (error.message === INDEX_CODES.CLOSED) return fail(INDEX_CODES.CLOSED, 'the index is closed');
|
|
82
|
+
return fail(INDEX_CODES.READ, error.message);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const handle = {
|
|
87
|
+
ok: true,
|
|
88
|
+
|
|
89
|
+
/** Freshness and row counts of the open index. */
|
|
90
|
+
status: () =>
|
|
91
|
+
answered(async () => {
|
|
92
|
+
const g = await graph();
|
|
93
|
+
if (g.files.length === 0) return fail(INDEX_CODES.NO_INDEX, 'the index is empty');
|
|
94
|
+
const stale = await isIndexStale(root);
|
|
95
|
+
return ok({ stale, counts: { files: g.files.length, symbols: g.symbols.length, edges: g.edges.length } });
|
|
96
|
+
}),
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* BM25 rows for a task string, best first.
|
|
100
|
+
* @param {string} task
|
|
101
|
+
* @param {{limit?: number}} [options]
|
|
102
|
+
* @returns {Promise<{ok: true, data: {path: string, kind: string, score: number}[]}|{ok: false, error: {code: string, message: string}}>}
|
|
103
|
+
*/
|
|
104
|
+
search: (task, { limit } = {}) =>
|
|
105
|
+
answered(async () => {
|
|
106
|
+
if (closed) throw new Error(INDEX_CODES.CLOSED);
|
|
107
|
+
const rows = await searchFtsBm25(root, task);
|
|
108
|
+
if (rows.error) return fail(INDEX_CODES.READ, rows.error);
|
|
109
|
+
return ok(limit === undefined ? rows : rows.slice(0, limit));
|
|
110
|
+
}),
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Rank the repository's files for a task string (R22): graph proximity
|
|
114
|
+
* fused with the BM25 rows.
|
|
115
|
+
* @param {string} task
|
|
116
|
+
* @param {{limit?: number, embed?: boolean, allowRemote?: boolean}} [options]
|
|
117
|
+
* @returns {Promise<{ok: true, data: {path: string, rank: number, score: number}[]}|{ok: false, error: {code: string, message: string}}>}
|
|
118
|
+
*/
|
|
119
|
+
rank: (task, { limit, embed = false, allowRemote = false } = {}) =>
|
|
120
|
+
answered(async () => {
|
|
121
|
+
const g = await graph();
|
|
122
|
+
if (g.files.length === 0) return fail(INDEX_CODES.NO_INDEX, 'the index is empty');
|
|
123
|
+
const bm25 = String(task ?? '').trim().length > 0 ? await searchFtsBm25(root, task) : [];
|
|
124
|
+
if (bm25.error) return fail(INDEX_CODES.READ, bm25.error);
|
|
125
|
+
const ranked = rankFiles({ ...g, bm25, task });
|
|
126
|
+
return ok(limit === undefined ? ranked : ranked.slice(0, limit));
|
|
127
|
+
}),
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The tests that exercise `paths` (Q77): the import closure when
|
|
131
|
+
* `transitive` (default true), the stored test edges otherwise.
|
|
132
|
+
* @param {Iterable<string>} paths
|
|
133
|
+
* @param {{transitive?: boolean, cochange?: boolean}} [options]
|
|
134
|
+
* @returns {Promise<{ok: true, data: string[]}|{ok: false, error: {code: string, message: string}}>}
|
|
135
|
+
*/
|
|
136
|
+
coverage: (paths, options) =>
|
|
137
|
+
answered(async () => {
|
|
138
|
+
const g = await graph();
|
|
139
|
+
return ok(coverage({ files: g.files, imports: g.edges }, paths, options));
|
|
140
|
+
}),
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Who a change to `file` reaches (Q77): the reverse import closure and
|
|
144
|
+
* the tests that import it.
|
|
145
|
+
* @param {string} file
|
|
146
|
+
* @param {{limit?: number}} [options]
|
|
147
|
+
* @returns {Promise<{ok: true, data: {importers: string[], tests: string[], truncated: boolean}}|{ok: false, error: {code: string, message: string}}>}
|
|
148
|
+
*/
|
|
149
|
+
impact: (file, { limit } = {}) =>
|
|
150
|
+
answered(async () => {
|
|
151
|
+
const g = await graph();
|
|
152
|
+
return ok(dependents(file, g.edges, limit));
|
|
153
|
+
}),
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* What governs `file` (Q77): the records in `corpus` whose Affects line
|
|
157
|
+
* names it, plus the rules, skills, gates and lessons whose path lists
|
|
158
|
+
* match.
|
|
159
|
+
* @param {string} file
|
|
160
|
+
* @param {{records?: {id: string, title: string, text: string}[], ctx?: {files: Set<string>, commands: Set<string>, roots: string[]}}} [corpus]
|
|
161
|
+
* @returns {Promise<{ok: true, data: {kind: string, id: string, detail?: string}[]}|{ok: false, error: {code: string, message: string}}>}
|
|
162
|
+
*/
|
|
163
|
+
governs: (file, corpus) => answered(() => ok(governs(file, corpus ?? {}))),
|
|
164
|
+
|
|
165
|
+
/** Release the handle. Calls after it answer `E_CLOSED`. */
|
|
166
|
+
close() {
|
|
167
|
+
closed = true;
|
|
168
|
+
cached = null;
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
return handle;
|
|
173
|
+
}
|