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
|
@@ -6,16 +6,15 @@
|
|
|
6
6
|
// duplicated-source-of-truth problem the tool exists to remove. `compare` reads it
|
|
7
7
|
// for N repos, so it must not shell out per repo per event.
|
|
8
8
|
|
|
9
|
-
import { execFile } from 'node:child_process';
|
|
10
9
|
import { readFile, stat } from 'node:fs/promises';
|
|
11
10
|
import { basename, join } from 'node:path';
|
|
12
|
-
import { promisify } from 'node:util';
|
|
13
11
|
|
|
14
12
|
import { CHAIN_MARKER, EVENTS, HOOKS_DIR, HOOKS_PATH_KEY, SHIM_MARKER, fileFor, renderChainedShim, renderShim } from '../hooks/shim.mjs';
|
|
15
13
|
import { foreignHooks, resolveHooksDir } from '../hooks/where.mjs';
|
|
16
14
|
import { packageVersion, hasNpm } from '../package-version.mjs';
|
|
17
15
|
import { resolveNodeRunner } from '../ecosystem/node-runner.mjs';
|
|
18
16
|
import { fingerprint } from '../resolve/fingerprint.mjs';
|
|
17
|
+
import { gitText } from '../sys/git.mjs';
|
|
19
18
|
|
|
20
19
|
/**
|
|
21
20
|
* The Node manager id for this repository, or null without a package.json.
|
|
@@ -34,8 +33,6 @@ async function nodeManagerFor(root) {
|
|
|
34
33
|
return resolveNodeRunner({ facts: detected.facts, packageManager }).id;
|
|
35
34
|
}
|
|
36
35
|
|
|
37
|
-
const exec = promisify(execFile);
|
|
38
|
-
|
|
39
36
|
/**
|
|
40
37
|
* The chained shim at `path`, read back out of the file itself.
|
|
41
38
|
*
|
|
@@ -120,8 +117,7 @@ export function wantsShim(manifest, exists, event) {
|
|
|
120
117
|
/** `core.hooksPath`, or null when unset or the repo is not a git repo. */
|
|
121
118
|
export async function readHooksPath(root) {
|
|
122
119
|
try {
|
|
123
|
-
const
|
|
124
|
-
const value = stdout.trim();
|
|
120
|
+
const value = await gitText(root, ['config', '--get', HOOKS_PATH_KEY]);
|
|
125
121
|
return value === '' ? null : value;
|
|
126
122
|
} catch {
|
|
127
123
|
return null;
|
package/src/compare/learn.mjs
CHANGED
|
@@ -19,7 +19,9 @@ import { mapCapped, DEFAULT_CONCURRENCY } from './parallel.mjs';
|
|
|
19
19
|
|
|
20
20
|
import { policiesFor } from '../agenthooks/policy.mjs';
|
|
21
21
|
import { DEL_R_TOLERANCE, parseAblationArmName } from '../bench/ablate.mjs';
|
|
22
|
-
import {
|
|
22
|
+
import { evaluateRunRecord, loadPrices } from '../measure/metrics.mjs';
|
|
23
|
+
import { loadRunRecords } from '../measure/records.mjs';
|
|
24
|
+
import { compareArms } from '../measure/stats.mjs';
|
|
23
25
|
import { GATES } from '../hooks/gates.mjs';
|
|
24
26
|
import { readLedger } from '../ledger/index.mjs';
|
|
25
27
|
import { readLessonCandidates } from '../memory/lessons.mjs';
|
package/src/decisions/index.mjs
CHANGED
|
@@ -28,7 +28,8 @@ import { packageVersion } from '../package-version.mjs';
|
|
|
28
28
|
// evaluation would read an initialising binding on one of the two load
|
|
29
29
|
// orders. Every use below happens at runtime, when both sides are loaded.
|
|
30
30
|
import { COMMANDS } from '../commands/index.mjs';
|
|
31
|
-
import { PARTIAL_CLAUSE, parseDecisionFrontmatter
|
|
31
|
+
import { PARTIAL_CLAUSE, parseDecisionFrontmatter } from '../memory/decide.mjs';
|
|
32
|
+
import { parseFrontmatter } from '../text/frontmatter.mjs';
|
|
32
33
|
import { affectsTokens } from '../index/govern.mjs';
|
|
33
34
|
|
|
34
35
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -189,7 +190,7 @@ export function citations(text) {
|
|
|
189
190
|
*/
|
|
190
191
|
export function parseDecision(file, text, { all = false } = {}) {
|
|
191
192
|
const fm = parseDecisionFrontmatter(text);
|
|
192
|
-
const { body } =
|
|
193
|
+
const { body } = parseFrontmatter(text);
|
|
193
194
|
const fileId = /^ADR-(\d{3,})/i.exec(file)?.[0]?.toUpperCase();
|
|
194
195
|
const heading = /^#\s+(.*)$/m.exec(body)?.[1] ?? null;
|
|
195
196
|
const headId = heading ? /^(ADR-\d{3,})/i.exec(heading)?.[1]?.toUpperCase() : null;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// The README's command list, rendered from the spec (review 0003 Q60).
|
|
2
|
+
//
|
|
3
|
+
// The README carried the fifth copy of the command list this review found. The other
|
|
4
|
+
// four now come from `spec.mjs`; this one could not, because a README line is a sentence
|
|
5
|
+
// for a reader rather than a table row for a parser. It does not need to be: a row's
|
|
6
|
+
// `path` is the command and its `summary` is already one phrase, written for help. So the
|
|
7
|
+
// list is rendered from those two fields, and `test/docs.test.mjs` fails when the README
|
|
8
|
+
// and the spec stop agreeing.
|
|
9
|
+
//
|
|
10
|
+
// The group comment is the first word of the path, which is what a reader scans for. The
|
|
11
|
+
// order is the order the spec lists groups in, so a new group lands where its author put
|
|
12
|
+
// it rather than wherever it sorts.
|
|
13
|
+
import { SPEC } from '../commands/spec.mjs';
|
|
14
|
+
import { CLI } from '../name.mjs';
|
|
15
|
+
|
|
16
|
+
export const README_FILE = 'README.md';
|
|
17
|
+
export const BEGIN = '<!-- simt:commands:begin -->';
|
|
18
|
+
export const END = '<!-- simt:commands:end -->';
|
|
19
|
+
|
|
20
|
+
/** The fenced command list, markers included, exactly as the README must carry it. */
|
|
21
|
+
export function renderCommandBlock() {
|
|
22
|
+
const groups = new Map();
|
|
23
|
+
for (const row of SPEC) {
|
|
24
|
+
const group = row.path.split(' ')[0];
|
|
25
|
+
if (!groups.has(group)) groups.set(group, []);
|
|
26
|
+
groups.get(group).push(row);
|
|
27
|
+
}
|
|
28
|
+
// The invocation, not the path: `brief` without `"<task>"` is a command a reader has
|
|
29
|
+
// to go and look up, which is what the hand-written list it replaced was for.
|
|
30
|
+
const command = (row) => `${row.path}${row.usage === undefined ? '' : ` ${row.usage}`}`;
|
|
31
|
+
const width = Math.max(...SPEC.map((row) => command(row).length));
|
|
32
|
+
const lines = [];
|
|
33
|
+
for (const [group, rows] of groups) {
|
|
34
|
+
lines.push('', `# ${group}`);
|
|
35
|
+
for (const row of [...rows].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))) {
|
|
36
|
+
lines.push(`${CLI} ${command(row).padEnd(width)} # ${row.summary}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// The first group's leading blank line would sit directly under the fence, which reads
|
|
40
|
+
// as a stray newline in the block rather than a separation.
|
|
41
|
+
if (lines[0] === '') lines.shift();
|
|
42
|
+
return [
|
|
43
|
+
BEGIN,
|
|
44
|
+
'<!-- Rendered from the spec by `simt docs readme --yes`; do not edit by hand. -->',
|
|
45
|
+
'',
|
|
46
|
+
'```sh',
|
|
47
|
+
...lines,
|
|
48
|
+
'```',
|
|
49
|
+
END,
|
|
50
|
+
].join('\n');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Replace the region between the markers, or return null when the file carries none.
|
|
55
|
+
*
|
|
56
|
+
* Null rather than an appended block: a README without the markers has the list somewhere
|
|
57
|
+
* else, and writing a second one beside it is the duplicated list this rendering exists
|
|
58
|
+
* to remove.
|
|
59
|
+
*/
|
|
60
|
+
export function replaceBlock(text, block) {
|
|
61
|
+
const start = text.indexOf(BEGIN);
|
|
62
|
+
const end = text.indexOf(END);
|
|
63
|
+
if (start === -1 || end === -1 || end < start) return null;
|
|
64
|
+
return `${text.slice(0, start)}${block}${text.slice(end + END.length)}`;
|
|
65
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// Ecosystem and language data records.
|
|
2
|
+
//
|
|
3
|
+
// ADR-013: content is data, the engine is code. An ecosystem is a record under
|
|
4
|
+
// `ecosystems/`, not a row in a table the engine edits. A new ecosystem arrives as a
|
|
5
|
+
// new file; the engine — this loader and the views over it — never changes shape to
|
|
6
|
+
// make a product work. The Dependabot entry, the card's build/focused/fallback lines,
|
|
7
|
+
// the language tables and the manifest's gate rows are all READ from the same records,
|
|
8
|
+
// so a product and its facets land together in one file and cannot drift apart.
|
|
9
|
+
//
|
|
10
|
+
// The shipped records live in `ecosystems/` at the package root. A repository may
|
|
11
|
+
// override them under `.simt/ecosystems/`: a same-id record is a shallow merge over the
|
|
12
|
+
// shipped one (arrays replaced whole), a new id is appended, and a record that does not
|
|
13
|
+
// parse is skipped, never a crash — the shipped default wins and the reader fixes the
|
|
14
|
+
// file on the next run.
|
|
15
|
+
//
|
|
16
|
+
// The package root is computed here rather than imported from workspace.mjs because
|
|
17
|
+
// workspace.mjs imports projection/manifest.mjs, which imports this module: an import
|
|
18
|
+
// cycle that would leave the binding in the temporal dead zone when manifest.mjs builds
|
|
19
|
+
// its table at evaluation time. This file is two levels below the root (src/ecosystem/),
|
|
20
|
+
// so two levels up is the root — a fixed path, not a heuristic.
|
|
21
|
+
|
|
22
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
23
|
+
import { dirname, join, resolve } from 'node:path';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
|
|
26
|
+
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
27
|
+
const SHIPPED_DIR = join(PACKAGE_ROOT, 'ecosystems');
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The records in force for a repository: the shipped set, with a repo-local
|
|
31
|
+
* `.simt/ecosystems/` merged over it. `null` is the shipped set alone, which is what the
|
|
32
|
+
* const-iterating tests and every call without a root read.
|
|
33
|
+
*/
|
|
34
|
+
export function loadEcosystems(root = null) {
|
|
35
|
+
const base = shipped();
|
|
36
|
+
if (!root) return base;
|
|
37
|
+
const localDir = join(root, '.simt', 'ecosystems');
|
|
38
|
+
if (!existsSync(localDir)) return base;
|
|
39
|
+
const byId = new Map(base.map((r) => [r.id, r]));
|
|
40
|
+
for (const file of readdirSync(localDir)) {
|
|
41
|
+
if (!file.endsWith('.json')) continue;
|
|
42
|
+
const id = file.slice(0, -5);
|
|
43
|
+
const rec = parse(join(localDir, file));
|
|
44
|
+
if (!rec) continue;
|
|
45
|
+
// Same id: a shallow merge over the shipped record, so a local override that omits
|
|
46
|
+
// a field keeps the shipped one. New id: the record as-is.
|
|
47
|
+
byId.set(id, byId.has(id) ? { ...byId.get(id), ...rec } : rec);
|
|
48
|
+
}
|
|
49
|
+
return [...byId.values()];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The shipped records, read once per process and cached. */
|
|
53
|
+
function shipped() {
|
|
54
|
+
if (shippedCache) return shippedCache;
|
|
55
|
+
const map = new Map();
|
|
56
|
+
if (existsSync(SHIPPED_DIR)) readRecords(SHIPPED_DIR, map);
|
|
57
|
+
shippedCache = [...map.values()];
|
|
58
|
+
return shippedCache;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let shippedCache = null;
|
|
62
|
+
|
|
63
|
+
/** Read one directory of records into `into`, keyed by the file stem. */
|
|
64
|
+
function readRecords(dir, into) {
|
|
65
|
+
for (const file of readdirSync(dir)) {
|
|
66
|
+
if (!file.endsWith('.json')) continue;
|
|
67
|
+
const id = file.slice(0, -5);
|
|
68
|
+
const rec = parse(join(dir, file));
|
|
69
|
+
// The id must match the file stem: a mismatch is a record pointing at the wrong
|
|
70
|
+
// ecosystem, and shipping it would make two names mean one record.
|
|
71
|
+
if (rec && rec.id === id) into.set(id, rec);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** One record, or null when the file is absent, unreadable or does not parse. */
|
|
76
|
+
function parse(path) {
|
|
77
|
+
try {
|
|
78
|
+
const rec = JSON.parse(readFileSync(path, 'utf8'));
|
|
79
|
+
return rec && typeof rec === 'object' ? rec : null;
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The gate rows, in manifest order: one per record that declares a `manifestOrder`, the
|
|
87
|
+
* nine tool ecosystems a fresh adoption seeds gates from. `preCommit` is emitted only
|
|
88
|
+
* when present (the Maven row has none) and `file` only when set, so the rows keep the
|
|
89
|
+
* exact shape the const-iterating tests pin.
|
|
90
|
+
*/
|
|
91
|
+
export function ecosystemGateRows(root = null) {
|
|
92
|
+
return loadEcosystems(root)
|
|
93
|
+
.filter((r) => r.manifestOrder != null)
|
|
94
|
+
.sort((a, b) => a.manifestOrder - b.manifestOrder)
|
|
95
|
+
.map((r) => {
|
|
96
|
+
const row = { fact: r.facts[0], tool: r.tool };
|
|
97
|
+
if (r.file === true) row.file = true;
|
|
98
|
+
if (Array.isArray(r.gates?.preCommit)) row.preCommit = r.gates.preCommit;
|
|
99
|
+
if (Array.isArray(r.gates?.prePush)) row.prePush = r.gates.prePush;
|
|
100
|
+
row.commands = r.gates?.commands ?? {};
|
|
101
|
+
return row;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The Dependabot entries in the table's order. Each record contributes zero or more
|
|
107
|
+
* `{fact?|stack?, value, manifest}` rows; the order is the records' `order`, not
|
|
108
|
+
* detection's, so a config whose entry order followed readdir could not be diffed.
|
|
109
|
+
* `ecosystems()` downstream deduplicates by `value`; this view only orders and flattens.
|
|
110
|
+
*/
|
|
111
|
+
export function dependabotEntries(root = null) {
|
|
112
|
+
const entries = [];
|
|
113
|
+
for (const r of loadEcosystems(root)) {
|
|
114
|
+
for (const e of r.dependabot ?? []) entries.push({ ...e });
|
|
115
|
+
}
|
|
116
|
+
entries.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
117
|
+
return entries.map(({ order, ...rest }) => rest);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The card's build lines, in their first-match-wins order. */
|
|
121
|
+
export function cardBuilds(root = null) {
|
|
122
|
+
const builds = [];
|
|
123
|
+
for (const r of loadEcosystems(root)) if (r.card?.build) builds.push({ ...r.card.build });
|
|
124
|
+
builds.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
125
|
+
return builds.map(({ order, ...rest }) => rest);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The card's focused-test lines, in their first-match-wins order. */
|
|
129
|
+
export function cardFocused(root = null) {
|
|
130
|
+
const focused = [];
|
|
131
|
+
for (const r of loadEcosystems(root)) for (const e of r.card?.focused ?? []) focused.push({ ...e });
|
|
132
|
+
focused.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
133
|
+
return focused.map(({ order, ...rest }) => rest);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The card's fallback lines — verbs the gate table does not seed, used by the card alone. */
|
|
137
|
+
export function cardFallbacks(root = null) {
|
|
138
|
+
const fallbacks = [];
|
|
139
|
+
for (const r of loadEcosystems(root)) for (const e of r.card?.fallback ?? []) fallbacks.push({ ...e });
|
|
140
|
+
fallbacks.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
141
|
+
return fallbacks.map(({ order, ...rest }) => rest);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The extension table, keyed by language. The records' `exts` are the single source; a
|
|
146
|
+
* language named by more than one record unions, so the same table the indexer extracts
|
|
147
|
+
* from and the resolver maps from cannot desynchronize.
|
|
148
|
+
*/
|
|
149
|
+
export function languageExts(root = null) {
|
|
150
|
+
const exts = {};
|
|
151
|
+
for (const r of loadEcosystems(root)) {
|
|
152
|
+
for (const l of r.languages ?? []) {
|
|
153
|
+
if (!Array.isArray(l.exts)) continue;
|
|
154
|
+
exts[l.lang] = exts[l.lang] ? [...new Set([...exts[l.lang], ...l.exts])] : [...l.exts];
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return exts;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The extension-to-language map: an extension without its dot, to the language that owns
|
|
162
|
+
* it. `canonical` names the extensions that resolve to the language (the JavaScript
|
|
163
|
+
* record claims `.ts` for extraction but not for resolution); absent it, `exts` does.
|
|
164
|
+
*/
|
|
165
|
+
export function languageResolver(root = null) {
|
|
166
|
+
const map = {};
|
|
167
|
+
for (const r of loadEcosystems(root)) {
|
|
168
|
+
for (const l of r.languages ?? []) {
|
|
169
|
+
for (const ext of l.canonical ?? l.exts ?? []) {
|
|
170
|
+
map[ext.startsWith('.') ? ext.slice(1) : ext] = l.lang;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return map;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The per-language test-path conventions: one entry per language that declares
|
|
179
|
+
* `testConventions`. The miner compiles the `base` sources to regexes; a language may
|
|
180
|
+
* contribute a pattern that also matches a sibling's files (the JVM pattern matches both
|
|
181
|
+
* `.java` and `.kt`), which the caller de-duplicates.
|
|
182
|
+
*/
|
|
183
|
+
export function testConventions(root = null) {
|
|
184
|
+
const out = [];
|
|
185
|
+
for (const r of loadEcosystems(root)) {
|
|
186
|
+
for (const l of r.languages ?? []) {
|
|
187
|
+
if (l.testConventions) out.push({ lang: l.lang, ...l.testConventions });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Does a data condition match a repository's signals? A condition is one of:
|
|
195
|
+
* {fact: 'has-x'} one named fact
|
|
196
|
+
* {facts: ['a','b']} any of the listed facts
|
|
197
|
+
* {stack: 'gradle'} one named stack tag
|
|
198
|
+
*/
|
|
199
|
+
export function matches(condition, { stack = new Set(), facts = new Set() } = {}) {
|
|
200
|
+
if (!condition) return false;
|
|
201
|
+
if (condition.fact) return facts.has(condition.fact);
|
|
202
|
+
if (Array.isArray(condition.facts)) return condition.facts.some((f) => facts.has(f));
|
|
203
|
+
if (condition.stack) return stack.has(condition.stack);
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Resolve a card template. A string is the whole template, its `{placeholder}`s left for
|
|
209
|
+
* the reader. An object is the Node shape: a static default, and a dynamic form when the
|
|
210
|
+
* repository's own test script matches — the manager is the repo's committed runner, so
|
|
211
|
+
* `<manager> test -- <file>` is the ADR-010 shape (never `npx`).
|
|
212
|
+
*/
|
|
213
|
+
export function evalTemplate(template, { scripts = {}, runner = null } = {}) {
|
|
214
|
+
if (typeof template === 'string') return template;
|
|
215
|
+
const test = scripts?.test ?? '';
|
|
216
|
+
if (!new RegExp(template.ifScriptMatches).test(test)) return template.static;
|
|
217
|
+
const run = runner?.run ?? 'npm';
|
|
218
|
+
return template.dynamic.replaceAll('{runner}', run);
|
|
219
|
+
}
|
package/src/explain.mjs
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* @type {Record<string, CodeRow>}
|
|
16
16
|
*/
|
|
17
17
|
export const EXPLAIN_CODES = {
|
|
18
|
+
|
|
18
19
|
absent: {
|
|
19
20
|
what: 'The file the gate reads does not exist: no AGENTS.md at the repository root, so there is no region to compare.',
|
|
20
21
|
why: 'ADR-025: the region is the baseline in a file the repository owns. Absence is the pre-adoption state — there is nothing to verify against yet, which is why the gate reports it instead of passing a repository it never inspected.',
|
|
@@ -93,6 +94,18 @@ export const EXPLAIN_CODES = {
|
|
|
93
94
|
fix: 'Point gates.cwd at a directory inside this repository, or remove it.',
|
|
94
95
|
example: 'gates.cwd.test set to "../shared" in a monorepo member.',
|
|
95
96
|
},
|
|
97
|
+
generated: {
|
|
98
|
+
what: 'The path carries bytes this tool generates, so a write there is discarded by the next sync instead of applied.',
|
|
99
|
+
why: 'ADR-036: the region is generated text and the repository owns the prose around it. An edit inside the markers is an edit the next writer overwrites, which is why the hook refuses the write and names the source rather than letting the work disappear quietly.',
|
|
100
|
+
fix: 'Edit the source the region comes from - the rule body, or this repository manifest - then run: simt sync --yes.',
|
|
101
|
+
example: 'A hand edit inside the simt region of AGENTS.md: the write is refused with this code.',
|
|
102
|
+
},
|
|
103
|
+
'hooks-disable': {
|
|
104
|
+
what: 'The command removes or reroutes the git hooks the gates run in: core.hooksPath is unset or repointed, or the hooks directory is deleted.',
|
|
105
|
+
why: 'ADR-104: the gates run from the hooks, so a quiet reroute turns every declared gate into a suggestion while check still reports the repository as adopted. The policy reports it before the command runs, not after the gates have stopped.',
|
|
106
|
+
fix: 'If the reroute is deliberate, run: simt hooks install --yes afterwards, so the gates keep running.',
|
|
107
|
+
example: 'git config core.hooksPath .git/hooks: refused, naming the config key that matched.',
|
|
108
|
+
},
|
|
96
109
|
inconsistent: {
|
|
97
110
|
what: 'The project\u2019s tsconfig spells `moduleResolution` in a form the TypeScript parser accepts but the documentation does not, so it compiles and quietly disagrees with every repository that spells it the documented way.',
|
|
98
111
|
why: 'The parser is case-insensitive, which is the whole reason a workspace drifts on this key: both spellings work, so nothing ever said the other one did not. Consistency with the documentation is the only spelling checkable in one repository at a time \u2014 consistency with the other repositories would need a workspace, and `check` runs in a repository on its own.',
|
|
@@ -123,6 +136,12 @@ export const EXPLAIN_CODES = {
|
|
|
123
136
|
fix: 'Renumber the newer record to a free number, then rerun the check.',
|
|
124
137
|
example: 'ADR-002-first.md and ADR-002-second.md: DUPLICATE-DECISION-ID names both.',
|
|
125
138
|
},
|
|
139
|
+
lease: {
|
|
140
|
+
what: 'The path is leased by another session, so a write here collides with work happening right now.',
|
|
141
|
+
why: 'ADR-101: two sessions editing one file is the conflict no merge repairs. The lease makes the collision visible before the edit, and the report names the holder and the expiry so a wait is a number rather than a guess.',
|
|
142
|
+
fix: 'Wait for the lease TTL, edit a different file, or ask the holder to release the claim.',
|
|
143
|
+
example: 'A write to a file another session holds until 12:04: the message names the holder and the time.',
|
|
144
|
+
},
|
|
126
145
|
'lock-absent': {
|
|
127
146
|
what: 'No lock is recorded: the projection was never locked, so there is nothing `--frozen` can compare against.',
|
|
128
147
|
why: 'ADR-086: the lock is the byte-identical contract across machines, and `--frozen` without one would compare the projection against nothing and call it current. Reported only under --frozen, because a repository that never locked is not drift.',
|
|
@@ -213,6 +232,12 @@ export const EXPLAIN_CODES = {
|
|
|
213
232
|
fix: 'Declare simt-supersedes: [{ adr: "ADR-MMM", ruling: N }] in the record\u2019s frontmatter.',
|
|
214
233
|
example: 'A status line "ruling 2 narrowed by ADR-024" with empty frontmatter: the lint names the raw clause.',
|
|
215
234
|
},
|
|
235
|
+
push: {
|
|
236
|
+
what: 'The command pushes: a remote mutation, which needs explicit approval before it runs.',
|
|
237
|
+
why: 'ADR-001: a push can deploy, migrate, or rewrite a branch the remote treats as production, and the agent that ran the tests is not the one who decides to publish them.',
|
|
238
|
+
fix: 'Ask for approval first; commit locally and report the SHA in the meantime.',
|
|
239
|
+
example: 'git push origin main: refused before it runs, with the approval line to ask for.',
|
|
240
|
+
},
|
|
216
241
|
reformatted: {
|
|
217
242
|
what: 'The manifest\u2019s content is what simt-dev would write, but the bytes differ: another formatter owns this file\u2019s shape.',
|
|
218
243
|
why: 'ADR-050: a formatter that rewrites the file on every run and a tool that rewrites it back would fight forever, with init as the only repair — which re-runs adoption to change one file\u2019s formatting. Comparing content-to-content isolates the real question: does another tool own this file.',
|
|
@@ -225,6 +250,12 @@ export const EXPLAIN_CODES = {
|
|
|
225
250
|
fix: 'Add the file, or amend the rule if the requirement moved on.',
|
|
226
251
|
example: 'A rule requiring SECURITY.md in a repo without one.',
|
|
227
252
|
},
|
|
253
|
+
rewrite: {
|
|
254
|
+
what: 'The command amends, rebases, resets --hard, force-pushes or passes --no-verify: each one rewrites history or skips a declared gate.',
|
|
255
|
+
why: 'ADR-017: the gates are the contract, and every one of these forms is the escape that turns them into a suggestion. A skipped gate leaves no trace for the next reader, which is why the policy refuses rather than warns.',
|
|
256
|
+
fix: 'Commit a new commit instead, or ask for approval before rewriting history.',
|
|
257
|
+
example: 'git commit --amend, git reset --hard, git push --force: each reports this code and the flag that matched.',
|
|
258
|
+
},
|
|
228
259
|
'stale-generated-field': {
|
|
229
260
|
what: 'Generated manifest fields do not match what simt-dev writes: the projection SHA, the stack, or the version pin drifted without a re-run.',
|
|
230
261
|
why: 'ADR-050: sync repairs exactly this field set and nothing else, so init\u2019s re-adoption is never the repair for a stale field. check names the fields; sync rewrites them.',
|