simt-dev 0.1.1 → 0.2.1
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 +209 -7
- package/README.md +32 -5
- package/docs/decisions/ADR-001-push-requires-approval.md +5 -5
- package/docs/decisions/ADR-002-refactor-blast-radius.md +1 -1
- package/docs/decisions/ADR-003-test-breadth.md +4 -4
- package/docs/decisions/ADR-004-hook-owns-format.md +5 -5
- package/docs/decisions/ADR-005-file-length.md +5 -5
- package/docs/decisions/ADR-006-coverage-bar.md +3 -3
- package/docs/decisions/ADR-007-section-skeleton.md +5 -5
- package/docs/decisions/ADR-008-verdict-vocabulary.md +1 -1
- package/docs/decisions/ADR-009-skill-layout.md +1 -1
- package/docs/decisions/ADR-010-runner-invocation.md +1 -1
- package/docs/decisions/ADR-012-drop-windows-support.md +2 -2
- package/docs/decisions/ADR-015-a-source-is-content-not-a-location.md +2 -2
- package/docs/decisions/ADR-016-a-divergent-rule-is-retired-not-merged.md +13 -13
- package/docs/decisions/ADR-017-a-declared-gate-must-be-runnable.md +2 -2
- package/docs/decisions/ADR-018-an-unset-hooks-path-is-not-an-absent-hook.md +9 -9
- package/docs/decisions/ADR-019-a-declared-command-is-the-runner.md +34 -7
- package/docs/decisions/ADR-023-the-gate-ships-as-a-file-the-repo-owns.md +2 -2
- package/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +5 -5
- package/docs/decisions/ADR-028-a-base-file-is-a-floor-so-the-disagreement-it-cannot-repair-becomes-a-check.md +2 -2
- 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 +2 -2
- package/docs/decisions/ADR-031-the-suite-stages-the-environment-it-needs.md +3 -3
- package/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +4 -4
- package/docs/decisions/ADR-041-the-suite-installs-the-package-in-order-to-test-it.md +2 -2
- package/docs/decisions/ADR-045-a-dashboard-that-counts-a-defect.md +3 -3
- package/docs/decisions/ADR-046-one-rule-one-copy.md +5 -5
- package/docs/decisions/ADR-047-a-profile-selects-on-what-a-repository-runs-on.md +4 -4
- package/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +5 -5
- package/docs/decisions/ADR-057-an-identity-is-per-developer-and-a-repository-does-not-own-it.md +115 -0
- package/docs/decisions/ADR-058-one-tool-three-channels-and-the-order-is-forced.md +148 -0
- package/docs/decisions/ADR-059-the-version-a-repository-runs-is-the-one-it-pinned.md +75 -0
- package/docs/decisions/ADR-060-a-repository-with-nothing-to-run-is-adopted-and-says-so.md +131 -0
- package/docs/decisions/ADR-061-the-skill-registry-is-workspace-data.md +113 -0
- package/docs/decisions/ADR-062-a-fresh-adoption-seeds-the-gates-it-can-run.md +94 -0
- package/docs/decisions/ADR-063-the-adoption-escape-hatch-is-opt-in.md +69 -0
- package/docs/decisions/README.md +20 -1
- package/package.json +1 -2
- package/rules/stacks/dart.md +38 -0
- package/rules/stacks/deno.md +36 -0
- package/rules/stacks/dotnet.md +35 -0
- package/rules/stacks/elixir.md +39 -0
- package/rules/stacks/go.md +34 -0
- package/rules/stacks/ruby.md +36 -0
- package/rules/stacks/rust.md +34 -0
- package/rules/stacks/swift.md +37 -0
- package/src/argv.mjs +35 -6
- package/src/ci/dependabot.mjs +1 -1
- package/src/ci/workflow.mjs +33 -11
- package/src/commands/graph.mjs +13 -4
- package/src/commands/help.mjs +22 -4
- package/src/commands/hooks.mjs +183 -67
- package/src/commands/index.mjs +53 -45
- package/src/commands/init.mjs +156 -14
- package/src/commands/rules.mjs +54 -3
- package/src/commands/skills.mjs +143 -15
- package/src/commands/skillsinstall.mjs +4 -4
- package/src/commands/sync.mjs +153 -11
- package/src/compare/discover.mjs +1 -1
- package/src/compare/hooksstate.mjs +96 -9
- package/src/compare/row.mjs +36 -7
- package/src/decisions/index.mjs +51 -12
- package/src/graph/load.mjs +126 -15
- package/src/hooks/capability.mjs +49 -5
- package/src/hooks/dispatcher.mjs +51 -6
- package/src/hooks/gates.mjs +1 -3
- package/src/hooks/resolve.mjs +24 -25
- package/src/hooks/shim.mjs +29 -5
- package/src/hooks/where.mjs +1 -1
- package/src/identity.mjs +207 -0
- package/src/mcp/tools.mjs +2 -2
- package/src/output/index.mjs +8 -7
- package/src/package-version.mjs +74 -0
- package/src/projection/baselines.mjs +1 -1
- package/src/projection/conflicts.mjs +57 -6
- package/src/projection/manifest.mjs +248 -26
- package/src/projection/markers.mjs +3 -3
- package/src/projection/mdc.mjs +6 -15
- package/src/projection/project.mjs +1 -1
- package/src/projection/rules.mjs +21 -8
- package/src/projection/surfaces.mjs +2 -2
- package/src/projection/targets.mjs +53 -7
- package/src/resolve/eval.mjs +64 -0
- package/src/resolve/fingerprint.mjs +4 -3
- package/src/resolve/near.mjs +26 -7
- package/src/resolve/resolve.mjs +1 -1
- package/src/resolve/source.mjs +2 -2
- package/src/tsconfig.mjs +1 -1
- package/src/workspace.mjs +0 -9
- package/graph/skills.graph.json +0 -1622
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Gold-corpus evaluation: run each case through the resolver and score it.
|
|
2
|
+
//
|
|
3
|
+
// Plan 0002 section 9: "A resolver that is not measured is a resolver that is merely
|
|
4
|
+
// felt." The corpus (test/fixtures/eval-corpus.json) states, for real repository signals
|
|
5
|
+
// and tasks, which skills must come back and which must not. Two callers must never
|
|
6
|
+
// disagree about the score - `simt skills eval` reports it to a reader, and
|
|
7
|
+
// test/eval.test.mjs asserts it in CI - so the arithmetic lives here once (ADR-002).
|
|
8
|
+
|
|
9
|
+
import { rankSkills, tokenize } from './resolve.mjs';
|
|
10
|
+
|
|
11
|
+
/** The skill names the resolver selects for one case's declared signals and task. */
|
|
12
|
+
function selectForCase(registry, c) {
|
|
13
|
+
const context = {
|
|
14
|
+
repoStack: c.repo?.stack ?? [],
|
|
15
|
+
paths: c.repo?.paths ?? [],
|
|
16
|
+
taskTokens: tokenize(c.task ?? ''),
|
|
17
|
+
};
|
|
18
|
+
const { selected } = rankSkills(registry.skills, context);
|
|
19
|
+
return selected.map((e) => e.skill.name);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Skill names the corpus cites that the registry does not declare. */
|
|
23
|
+
export function unknownSkills(registry, corpus) {
|
|
24
|
+
const names = new Set(registry.skills.map((s) => s.name));
|
|
25
|
+
return [
|
|
26
|
+
...new Set(
|
|
27
|
+
corpus.cases.flatMap((c) => [...(c.select ?? []), ...(c.reject ?? [])]).filter((n) => !names.has(n)),
|
|
28
|
+
),
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Score the whole corpus: per-case detail plus precision and recall.
|
|
34
|
+
*
|
|
35
|
+
* `unexpected` is the half of precision that named negatives cannot see: a case whose
|
|
36
|
+
* truth is "nothing" names only a sample of what must not come back, so for those the
|
|
37
|
+
* whole selected set is the miss. `clean` is what the exit code rests on; the two
|
|
38
|
+
* ratios are measurements, and a case can break `clean` while both still read 1.0 -
|
|
39
|
+
* the first version of this arithmetic scored that case a perfect precision (S19).
|
|
40
|
+
*/
|
|
41
|
+
export function scoreCorpus(registry, corpus) {
|
|
42
|
+
let expected = 0;
|
|
43
|
+
let negatives = 0;
|
|
44
|
+
const cases = corpus.cases.map((c) => {
|
|
45
|
+
expected += c.select?.length ?? 0;
|
|
46
|
+
negatives += c.reject?.length ?? 0;
|
|
47
|
+
const got = new Set(selectForCase(registry, c));
|
|
48
|
+
const missing = (c.select ?? []).filter((n) => !got.has(n));
|
|
49
|
+
const wronglySelected = (c.reject ?? []).filter((n) => got.has(n));
|
|
50
|
+
return {
|
|
51
|
+
name: c.name,
|
|
52
|
+
selected: [...got],
|
|
53
|
+
missing,
|
|
54
|
+
wronglySelected,
|
|
55
|
+
unexpected: c.select?.length === 0 ? [...got] : wronglySelected,
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
const missed = cases.reduce((n, c) => n + c.missing.length, 0);
|
|
59
|
+
const wrong = cases.reduce((n, c) => n + c.wronglySelected.length, 0);
|
|
60
|
+
const recall = expected === 0 ? 1 : (expected - missed) / expected;
|
|
61
|
+
const precision = negatives === 0 ? 1 : (negatives - wrong) / negatives;
|
|
62
|
+
const clean = missed === 0 && cases.every((c) => c.unexpected.length === 0);
|
|
63
|
+
return { cases, expected, missed, negatives, wrong, recall, precision, clean };
|
|
64
|
+
}
|
|
@@ -41,7 +41,7 @@ const MARKERS = [
|
|
|
41
41
|
/**
|
|
42
42
|
* Extensions are weaker evidence than marker files: a stray script must not
|
|
43
43
|
* define a repo's stack. `python` is deliberately absent here because
|
|
44
|
-
*
|
|
44
|
+
* a repository carries .py tooling scripts inside a Gradle project - a marker file
|
|
45
45
|
* is required for that tag. `kotlin` has no marker file, so it stays here.
|
|
46
46
|
*/
|
|
47
47
|
const EXTENSION_TAGS = new Map([
|
|
@@ -83,7 +83,7 @@ const DEP_TAGS = new Map([
|
|
|
83
83
|
*
|
|
84
84
|
* Three, and the reason is a convention rather than a preference: the Android Gradle
|
|
85
85
|
* Plugin puts a module's manifest at `<module>/src/main/AndroidManifest.xml`, and
|
|
86
|
-
*
|
|
86
|
+
* one common layout sits at `app/src/main/AndroidManifest.xml`. A bound shallower than the
|
|
87
87
|
* convention makes `has-android-manifest` the shape S07 found - a fact no repository
|
|
88
88
|
* could ever have. Deeper would be a scan rather than a probe, and ADR-013's promise
|
|
89
89
|
* is that adding an ecosystem is data, so a fact must not cost a search.
|
|
@@ -178,6 +178,7 @@ const FACTS = [
|
|
|
178
178
|
{ name: 'has-pubspec', kind: 'file', paths: ['pubspec.yaml'] },
|
|
179
179
|
{ name: 'has-swift-package', kind: 'file', paths: ['Package.swift'] },
|
|
180
180
|
{ name: 'has-mix', kind: 'file', paths: ['mix.exs'] },
|
|
181
|
+
{ name: 'has-deno-config', kind: 'file', paths: ['deno.json', 'deno.jsonc'] },
|
|
181
182
|
{ name: 'has-bun-lock', kind: 'file', paths: ['bun.lock', 'bun.lockb'] },
|
|
182
183
|
// A glob walk skips dot directories, and `.devcontainer` is one. Stat the path.
|
|
183
184
|
{ name: 'has-devcontainer', kind: 'path', paths: ['.devcontainer/devcontainer.json'] },
|
|
@@ -300,7 +301,7 @@ export async function fingerprint(root) {
|
|
|
300
301
|
}
|
|
301
302
|
}
|
|
302
303
|
|
|
303
|
-
//
|
|
304
|
+
// two repositories both carry a wear/ module; only one of them is the wearable.
|
|
304
305
|
if (names.has('wear') && (await isDir(join(root, 'wear')))) {
|
|
305
306
|
stack.add('wear-os');
|
|
306
307
|
evidence.push('wear/');
|
package/src/resolve/near.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// suggestion that far away is noise: an agent will install the wrong skill.
|
|
11
11
|
|
|
12
12
|
/** Levenshtein distance, two-row, with an early exit once the bound is passed. */
|
|
13
|
-
|
|
13
|
+
function editDistance(a, b, max = Infinity) {
|
|
14
14
|
if (a === b) return 0;
|
|
15
15
|
if (Math.abs(a.length - b.length) > max) return max + 1;
|
|
16
16
|
|
|
@@ -38,22 +38,41 @@ export function editDistance(a, b, max = Infinity) {
|
|
|
38
38
|
/**
|
|
39
39
|
* Candidates worth suggesting, nearest first.
|
|
40
40
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
41
|
+
* Two directions of substring, and they are not the same claim about what was typed.
|
|
42
|
+
*
|
|
43
|
+
* **The candidate contains the query** — `slice-commit` for `slice-commmit` — is a
|
|
44
|
+
* hit: the agent typed a prefix. It scores 1, not 0, because zero edits is what an
|
|
45
|
+
* exact match means and a prefix is not one. Scoring both 0 sorted `graph` ahead of
|
|
46
|
+
* `graphs` for the query `graphs`, and an agent that installs the first name in a
|
|
47
|
+
* "did you mean" list installs the wrong one.
|
|
48
|
+
*
|
|
49
|
+
* **The candidate sits inside the query** — `coverage` for `coverage-hunt` — is a hit
|
|
50
|
+
* only when it is at least half the query. Without that, the rule degenerates: `e` is
|
|
51
|
+
* a substring of `test`, so `nearMatches('test', ['e'])` returned `e` at distance 0
|
|
52
|
+
* and the message read `did you mean e?`.
|
|
53
|
+
*
|
|
54
|
+
* The bound also shrinks with the query, which is the same failure from the other side.
|
|
55
|
+
* Three edits is a typo in a name; three edits to a four-character query is a
|
|
56
|
+
* different string, and offering it violates the rule in the header of this file — "a
|
|
57
|
+
* suggestion that far away is noise: an agent will install the wrong skill". Under
|
|
58
|
+
* three characters nothing is a typo, so nothing is offered.
|
|
43
59
|
*/
|
|
44
60
|
export function nearMatches(needle, candidates, { max = 3, limit = 3 } = {}) {
|
|
45
61
|
if (!needle) return [];
|
|
46
62
|
const lower = needle.toLowerCase();
|
|
63
|
+
const bound = Math.max(1, Math.min(max, Math.floor(lower.length / 3)));
|
|
47
64
|
|
|
48
65
|
const scored = [];
|
|
49
66
|
for (const candidate of candidates) {
|
|
50
67
|
const cl = candidate.toLowerCase();
|
|
51
|
-
if (cl
|
|
68
|
+
if (cl === lower) {
|
|
52
69
|
scored.push({ candidate, distance: 0 });
|
|
53
|
-
|
|
70
|
+
} else if (cl.includes(lower) || (lower.includes(cl) && cl.length * 2 >= lower.length)) {
|
|
71
|
+
scored.push({ candidate, distance: 1 });
|
|
72
|
+
} else {
|
|
73
|
+
const distance = editDistance(lower, cl, bound);
|
|
74
|
+
if (distance <= bound) scored.push({ candidate, distance });
|
|
54
75
|
}
|
|
55
|
-
const distance = editDistance(lower, cl, max);
|
|
56
|
-
if (distance <= max) scored.push({ candidate, distance });
|
|
57
76
|
}
|
|
58
77
|
|
|
59
78
|
scored.sort((a, b) => a.distance - b.distance || a.candidate.localeCompare(b.candidate));
|
package/src/resolve/resolve.mjs
CHANGED
|
@@ -210,7 +210,7 @@ export function stackCompatible(skill, repoSignals) {
|
|
|
210
210
|
* agent knows it is standing in that repo, so name the overlapping tag when there
|
|
211
211
|
* is one and say plainly when there is not.
|
|
212
212
|
*/
|
|
213
|
-
|
|
213
|
+
function stackReason(skill, repoSignals) {
|
|
214
214
|
if (repoSignals.length === 0) return 'repo signals undetected, not excluded';
|
|
215
215
|
const repo = new Set(repoSignals);
|
|
216
216
|
const overlap = (skill.stack ?? []).filter((tag) => repo.has(tag));
|
package/src/resolve/source.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
//
|
|
9
9
|
// The second location is the one nobody wrote down: **the repository being operated
|
|
10
10
|
// on is itself a location for a declared source**. Someone who cloned only
|
|
11
|
-
//
|
|
11
|
+
// one repository has its body on disk at `.agents/skills/slice-commit`
|
|
12
12
|
// with no workspace anywhere. The registry entry is satisfied by that file.
|
|
13
13
|
//
|
|
14
14
|
// Bytes are what make this safe rather than hopeful. Every source declares its size,
|
|
@@ -35,7 +35,7 @@ import { measureSkill } from '../graph/load.mjs';
|
|
|
35
35
|
*
|
|
36
36
|
* @returns {{path: string, how: 'workspace'|'repo'}[]}
|
|
37
37
|
*/
|
|
38
|
-
|
|
38
|
+
function candidateLocations(source, { workspaceRoot = null, root = null } = {}) {
|
|
39
39
|
const out = [];
|
|
40
40
|
if (workspaceRoot) out.push({ path: join(workspaceRoot, source.repo, source.path), how: 'workspace' });
|
|
41
41
|
if (root) out.push({ path: join(root, source.path), how: 'repo' });
|
package/src/tsconfig.mjs
CHANGED
|
@@ -32,7 +32,7 @@ export const TSCONFIG_FILE = 'tsconfig.json';
|
|
|
32
32
|
* The strictness floor.
|
|
33
33
|
*
|
|
34
34
|
* Every key here is already set, and set the same way, by every `tsconfig.json` in the
|
|
35
|
-
* estate that could afford it.
|
|
35
|
+
* estate that could afford it. One repository is the outlier and it is not one on purpose: it
|
|
36
36
|
* is missing the seven flags below that the three social repos share, which is the gap
|
|
37
37
|
* plan section 3.1 row 5 named. Nothing in this object is a value anybody has argued about;
|
|
38
38
|
* it is the intersection of what four repositories independently wrote.
|
package/src/workspace.mjs
CHANGED
|
@@ -252,15 +252,6 @@ function absolute(p) {
|
|
|
252
252
|
return isAbsolute(expanded) ? expanded : resolve(expanded);
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
-
/**
|
|
256
|
-
* The workspace, or null. Callers that can degrade use this and report `source: 'none'`;
|
|
257
|
-
* callers that genuinely cannot proceed should use `resolveWorkspace` and fail with its detail.
|
|
258
|
-
*/
|
|
259
|
-
export async function workspaceOrNull(flags = {}, options = {}) {
|
|
260
|
-
const { root, error } = await resolveWorkspace(flags, options);
|
|
261
|
-
return error ? null : root;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
255
|
/**
|
|
265
256
|
* The path a multi-repo command should work on: `--root` or the first positional if
|
|
266
257
|
* given, else the resolved workspace, else the current directory.
|