simt-dev 0.2.0__py3-none-any.whl
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.
- python/simt_dev/__init__.py +32 -0
- python/simt_dev/cli.py +225 -0
- simt_dev/__init__.py +32 -0
- simt_dev/cli.py +225 -0
- simt_dev/vendored/CHANGELOG.md +166 -0
- simt_dev/vendored/bin/simt-dev.mjs +28 -0
- simt_dev/vendored/docs/decisions/ADR-001-push-requires-approval.md +42 -0
- simt_dev/vendored/docs/decisions/ADR-002-refactor-blast-radius.md +45 -0
- simt_dev/vendored/docs/decisions/ADR-003-test-breadth.md +46 -0
- simt_dev/vendored/docs/decisions/ADR-004-hook-owns-format.md +45 -0
- simt_dev/vendored/docs/decisions/ADR-005-file-length.md +42 -0
- simt_dev/vendored/docs/decisions/ADR-006-coverage-bar.md +59 -0
- simt_dev/vendored/docs/decisions/ADR-007-section-skeleton.md +54 -0
- simt_dev/vendored/docs/decisions/ADR-008-verdict-vocabulary.md +59 -0
- simt_dev/vendored/docs/decisions/ADR-009-skill-layout.md +48 -0
- simt_dev/vendored/docs/decisions/ADR-010-runner-invocation.md +56 -0
- simt_dev/vendored/docs/decisions/ADR-011-windows-support.md +52 -0
- simt_dev/vendored/docs/decisions/ADR-012-drop-windows-support.md +59 -0
- simt_dev/vendored/docs/decisions/ADR-013-genericity-by-facts-and-local-profiles.md +114 -0
- simt_dev/vendored/docs/decisions/ADR-014-workspace-is-configuration.md +211 -0
- simt_dev/vendored/docs/decisions/ADR-015-a-source-is-content-not-a-location.md +117 -0
- simt_dev/vendored/docs/decisions/ADR-016-a-divergent-rule-is-retired-not-merged.md +180 -0
- simt_dev/vendored/docs/decisions/ADR-017-a-declared-gate-must-be-runnable.md +89 -0
- simt_dev/vendored/docs/decisions/ADR-018-an-unset-hooks-path-is-not-an-absent-hook.md +183 -0
- simt_dev/vendored/docs/decisions/ADR-019-a-declared-command-is-the-runner.md +110 -0
- simt_dev/vendored/docs/decisions/ADR-020-a-rule-is-written-per-surface-and-pointed-not-copied.md +100 -0
- simt_dev/vendored/docs/decisions/ADR-021-one-surface-per-repo.md +94 -0
- simt_dev/vendored/docs/decisions/ADR-022-a-key-simt-dev-reads-is-prefixed.md +98 -0
- simt_dev/vendored/docs/decisions/ADR-023-the-gate-ships-as-a-file-the-repo-owns.md +106 -0
- simt_dev/vendored/docs/decisions/ADR-024-simt-dev-overwrites-its-own-gate-file.md +111 -0
- simt_dev/vendored/docs/decisions/ADR-025-a-baseline-is-a-region-in-a-file-the-repo-owns.md +153 -0
- simt_dev/vendored/docs/decisions/ADR-026-a-config-without-a-region-is-a-file-the-repo-owns.md +117 -0
- simt_dev/vendored/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +130 -0
- simt_dev/vendored/docs/decisions/ADR-028-a-base-file-is-a-floor-so-the-disagreement-it-cannot-repair-becomes-a-check.md +131 -0
- simt_dev/vendored/docs/decisions/ADR-029-the-pins-a-repository-declares-are-its-own.md +120 -0
- simt_dev/vendored/docs/decisions/ADR-030-what-a-plan-does-not-ship-is-a-decision-with-a-record.md +114 -0
- simt_dev/vendored/docs/decisions/ADR-031-the-suite-stages-the-environment-it-needs.md +129 -0
- simt_dev/vendored/docs/decisions/ADR-032-provenance-is-a-content-digest-not-a-commit.md +141 -0
- simt_dev/vendored/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +82 -0
- simt_dev/vendored/docs/decisions/ADR-034-a-staged-tool-refuses-a-partially-staged-file.md +91 -0
- simt_dev/vendored/docs/decisions/ADR-035-one-write-decision-and-one-json-document.md +119 -0
- simt_dev/vendored/docs/decisions/ADR-036-a-writer-that-writes-says-so-and-the-instructions-naming-it-move-with-it.md +103 -0
- simt_dev/vendored/docs/decisions/ADR-037-unchanged-means-writing-would-not-change-a-byte.md +100 -0
- simt_dev/vendored/docs/decisions/ADR-038-the-gate-filter-belongs-to-every-manifest-writer.md +95 -0
- simt_dev/vendored/docs/decisions/ADR-039-a-ci-step-runs-a-form-that-can-fail.md +101 -0
- simt_dev/vendored/docs/decisions/ADR-040-the-last-writer-that-wrote-by-default-was-not-the-one-named.md +158 -0
- simt_dev/vendored/docs/decisions/ADR-041-the-suite-installs-the-package-in-order-to-test-it.md +92 -0
- simt_dev/vendored/docs/decisions/ADR-042-a-declared-glob-must-match-a-path-built-from-it.md +94 -0
- simt_dev/vendored/docs/decisions/ADR-043-a-keyword-is-a-phrase-and-two-letters-is-a-word.md +90 -0
- simt_dev/vendored/docs/decisions/ADR-044-compatibility-is-not-evidence.md +116 -0
- simt_dev/vendored/docs/decisions/ADR-045-a-dashboard-that-counts-a-defect.md +119 -0
- simt_dev/vendored/docs/decisions/ADR-046-one-rule-one-copy.md +140 -0
- simt_dev/vendored/docs/decisions/ADR-047-a-profile-selects-on-what-a-repository-runs-on.md +141 -0
- simt_dev/vendored/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +150 -0
- simt_dev/vendored/docs/decisions/ADR-049-a-ruling-that-a-later-record-moved-says-so-where-it-is-printed.md +153 -0
- simt_dev/vendored/docs/decisions/ADR-050-a-byte-count-is-not-a-region-and-updated-is-not-a-change-of-rules.md +102 -0
- simt_dev/vendored/docs/decisions/ADR-051-a-pointer-is-the-readers-only-route-so-it-is-not-a-convenience.md +134 -0
- simt_dev/vendored/docs/decisions/ADR-052-a-dependency-is-counted-against-the-budget-and-there-is-one-budget.md +101 -0
- simt_dev/vendored/docs/decisions/ADR-053-the-node-floor-is-22-20-0.md +49 -0
- simt_dev/vendored/docs/decisions/ADR-054-the-digest-covers-every-byte-that-leaves-the-package.md +100 -0
- simt_dev/vendored/docs/decisions/ADR-055-a-status-line-travels-so-it-may-not-name-a-location.md +98 -0
- simt_dev/vendored/docs/decisions/ADR-056-three-names-one-of-which-is-cheap-to-move.md +160 -0
- simt_dev/vendored/docs/decisions/ADR-057-an-identity-is-per-developer-and-a-repository-does-not-own-it.md +115 -0
- simt_dev/vendored/docs/decisions/ADR-058-one-tool-three-channels-and-the-order-is-forced.md +148 -0
- simt_dev/vendored/docs/decisions/README.md +530 -0
- simt_dev/vendored/graph/skills.graph.json +1622 -0
- simt_dev/vendored/package.json +43 -0
- simt_dev/vendored/rules/core.md +76 -0
- simt_dev/vendored/rules/mdc/intake-questions.mdc +50 -0
- simt_dev/vendored/rules/mdc/preview-postflight.mdc +61 -0
- simt_dev/vendored/rules/mdc/slice-commit.mdc +52 -0
- simt_dev/vendored/rules/mdc/test-gate.mdc +54 -0
- simt_dev/vendored/rules/stacks/android.md +63 -0
- simt_dev/vendored/rules/stacks/cloudflare-workers.md +65 -0
- simt_dev/vendored/rules/stacks/connectiq.md +62 -0
- simt_dev/vendored/rules/stacks/container.md +31 -0
- simt_dev/vendored/rules/stacks/jvm.md +54 -0
- simt_dev/vendored/rules/stacks/node.md +45 -0
- simt_dev/vendored/rules/stacks/python.md +48 -0
- simt_dev/vendored/rules/stacks/react-native.md +58 -0
- simt_dev/vendored/rules/stacks/react-web.md +61 -0
- simt_dev/vendored/rules/stacks/typescript.md +21 -0
- simt_dev/vendored/rules/stacks/uv.md +32 -0
- simt_dev/vendored/src/argv.mjs +116 -0
- simt_dev/vendored/src/ci/dependabot.mjs +268 -0
- simt_dev/vendored/src/ci/workflow.mjs +360 -0
- simt_dev/vendored/src/commands/ci.mjs +285 -0
- simt_dev/vendored/src/commands/compare.mjs +195 -0
- simt_dev/vendored/src/commands/graph.mjs +73 -0
- simt_dev/vendored/src/commands/help.mjs +108 -0
- simt_dev/vendored/src/commands/hooks.mjs +471 -0
- simt_dev/vendored/src/commands/index.mjs +241 -0
- simt_dev/vendored/src/commands/init.mjs +632 -0
- simt_dev/vendored/src/commands/mcp.mjs +63 -0
- simt_dev/vendored/src/commands/rules.mjs +374 -0
- simt_dev/vendored/src/commands/skills.mjs +647 -0
- simt_dev/vendored/src/commands/skillsinstall.mjs +515 -0
- simt_dev/vendored/src/commands/sync.mjs +331 -0
- simt_dev/vendored/src/commands/upgrade.mjs +202 -0
- simt_dev/vendored/src/compare/discover.mjs +212 -0
- simt_dev/vendored/src/compare/hooksstate.mjs +159 -0
- simt_dev/vendored/src/compare/row.mjs +299 -0
- simt_dev/vendored/src/decisions/index.mjs +536 -0
- simt_dev/vendored/src/graph/load.mjs +541 -0
- simt_dev/vendored/src/hooks/capability.mjs +129 -0
- simt_dev/vendored/src/hooks/dispatcher.mjs +268 -0
- simt_dev/vendored/src/hooks/gates.mjs +144 -0
- simt_dev/vendored/src/hooks/resolve.mjs +194 -0
- simt_dev/vendored/src/hooks/shim.mjs +140 -0
- simt_dev/vendored/src/hooks/where.mjs +118 -0
- simt_dev/vendored/src/identity.mjs +218 -0
- simt_dev/vendored/src/locate.mjs +75 -0
- simt_dev/vendored/src/mcp/server.mjs +167 -0
- simt_dev/vendored/src/mcp/tools.mjs +410 -0
- simt_dev/vendored/src/name.mjs +95 -0
- simt_dev/vendored/src/output/index.mjs +96 -0
- simt_dev/vendored/src/projection/baselines.mjs +209 -0
- simt_dev/vendored/src/projection/conflicts.mjs +205 -0
- simt_dev/vendored/src/projection/manifest.mjs +478 -0
- simt_dev/vendored/src/projection/markers.mjs +349 -0
- simt_dev/vendored/src/projection/mdc.mjs +327 -0
- simt_dev/vendored/src/projection/project.mjs +504 -0
- simt_dev/vendored/src/projection/rules.mjs +640 -0
- simt_dev/vendored/src/projection/surfaces.mjs +398 -0
- simt_dev/vendored/src/projection/targets.mjs +221 -0
- simt_dev/vendored/src/resolve/fingerprint.mjs +553 -0
- simt_dev/vendored/src/resolve/near.mjs +61 -0
- simt_dev/vendored/src/resolve/resolve.mjs +457 -0
- simt_dev/vendored/src/resolve/source.mjs +105 -0
- simt_dev/vendored/src/toolchain.mjs +202 -0
- simt_dev/vendored/src/tsconfig.mjs +314 -0
- simt_dev/vendored/src/upgrade/plan.mjs +203 -0
- simt_dev/vendored/src/workspace.mjs +289 -0
- simt_dev-0.2.0.dist-info/METADATA +150 -0
- simt_dev-0.2.0.dist-info/RECORD +138 -0
- simt_dev-0.2.0.dist-info/WHEEL +4 -0
- simt_dev-0.2.0.dist-info/entry_points.txt +2 -0
- simt_dev-0.2.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// The MCP transport: newline-delimited JSON-RPC 2.0 on stdio.
|
|
2
|
+
//
|
|
3
|
+
// This file is the PROTOCOL and nothing else. It knows about `initialize`,
|
|
4
|
+
// `tools/list`, `tools/call` and `ping`; it knows nothing about skills, rules or repos.
|
|
5
|
+
// The tools themselves live in `src/mcp/tools.mjs`.
|
|
6
|
+
//
|
|
7
|
+
// Three decisions, each of which is a place where the obvious implementation is wrong:
|
|
8
|
+
//
|
|
9
|
+
// 1. STDOUT IS THE PROTOCOL. Every diagnostic goes to stderr. A single byte of human
|
|
10
|
+
// prose on stdout corrupts the stream, and the failure mode is a client that shows
|
|
11
|
+
// nothing and says nothing about why - so `emit()` is never used here, and the tools
|
|
12
|
+
// return VALUES rather than writing to the process streams.
|
|
13
|
+
// 2. TOOL FAILURES ARE RESULTS, NOT PROTOCOL ERRORS. A malformed request is a
|
|
14
|
+
// JSON-RPC error; a repo with no region is a successful call whose answer says the
|
|
15
|
+
// region is unmanaged. An MCP client that renders an exception will not render an
|
|
16
|
+
// answer, and the answer is the useful part.
|
|
17
|
+
// 3. NO MUTATING TOOL IS EXPOSED. `init`, `sync`, `hooks install`, `skills install`
|
|
18
|
+
// and `upgrade` are absent on purpose: an editor that can call a tool can call it
|
|
19
|
+
// without a human reading a diff first, and ADR-001 makes pushing approval-sensitive
|
|
20
|
+
// for a reason that does not weaken because the caller is a model.
|
|
21
|
+
|
|
22
|
+
import { createInterface } from 'node:readline';
|
|
23
|
+
import { CLI } from '../name.mjs';
|
|
24
|
+
|
|
25
|
+
export const PROTOCOL_VERSION = '2025-06-18';
|
|
26
|
+
|
|
27
|
+
/** JSON-RPC codes. -32700/-32600/-32601 are the spec's parse/request/method set. */
|
|
28
|
+
export const RPC = {
|
|
29
|
+
PARSE: -32700,
|
|
30
|
+
REQUEST: -32600,
|
|
31
|
+
METHOD: -32601,
|
|
32
|
+
INTERNAL: -32603,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const SERVER_INFO = { name: 'simt-dev', title: 'SIMT developer and agent tooling' };
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Handle one parsed request. Pure with respect to the process: returns the response
|
|
39
|
+
* object or null (for a notification), and never writes anywhere. Testable without a
|
|
40
|
+
* pipe, which is the only way to test a line protocol honestly.
|
|
41
|
+
*/
|
|
42
|
+
export async function handle(request, tools, { version } = {}) {
|
|
43
|
+
if (request === null || typeof request !== 'object' || Array.isArray(request)) {
|
|
44
|
+
return error(null, RPC.REQUEST, 'request must be a JSON object');
|
|
45
|
+
}
|
|
46
|
+
const { id, method, params } = request;
|
|
47
|
+
// A notification is a request with no id, and the spec's rule is unconditional: it
|
|
48
|
+
// never gets a response, whatever its method. Answering one anyway desynchronises a
|
|
49
|
+
// client that is waiting for the next real answer in the stream.
|
|
50
|
+
const isNotification = id === undefined || id === null;
|
|
51
|
+
|
|
52
|
+
if (typeof method !== 'string') {
|
|
53
|
+
return isNotification ? null : error(id, RPC.REQUEST, 'request needs a string method');
|
|
54
|
+
}
|
|
55
|
+
if (isNotification) return null;
|
|
56
|
+
|
|
57
|
+
switch (method) {
|
|
58
|
+
case 'initialize':
|
|
59
|
+
return result(id, {
|
|
60
|
+
protocolVersion: negotiate(params?.protocolVersion),
|
|
61
|
+
capabilities: { tools: { listChanged: false } },
|
|
62
|
+
serverInfo: { ...SERVER_INFO, version: version ?? '0.0.0' },
|
|
63
|
+
instructions:
|
|
64
|
+
`Read-only. Every answer is JSON. \`${CLI} skills brief\` is the one to call first: it returns the pre-assembled set for a task, plus what it deliberately left out and why.`,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
case 'notifications/initialized':
|
|
68
|
+
case 'initialized':
|
|
69
|
+
return null;
|
|
70
|
+
|
|
71
|
+
case 'ping':
|
|
72
|
+
return result(id, {});
|
|
73
|
+
|
|
74
|
+
case 'tools/list':
|
|
75
|
+
return result(id, { tools: tools.map(describe) });
|
|
76
|
+
|
|
77
|
+
case 'tools/call': {
|
|
78
|
+
const name = params?.name;
|
|
79
|
+
const tool = tools.find((t) => t.name === name);
|
|
80
|
+
if (!tool) {
|
|
81
|
+
// A named tool that does not exist is the client's mistake, so it is a protocol
|
|
82
|
+
// error - unlike a tool that runs and reports a bad repo.
|
|
83
|
+
return error(id, RPC.METHOD, `no tool named "${name}"; call tools/list first`);
|
|
84
|
+
}
|
|
85
|
+
const args = params?.arguments ?? {};
|
|
86
|
+
try {
|
|
87
|
+
const payload = await tool.run(args);
|
|
88
|
+
return result(id, { content: [contentOf(payload)], isError: false });
|
|
89
|
+
} catch (failure) {
|
|
90
|
+
return result(id, {
|
|
91
|
+
content: [contentOf({ error: failure.message, remedy: failure.remedy ?? null })],
|
|
92
|
+
isError: true,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
default:
|
|
98
|
+
return isNotification ? null : error(id, RPC.METHOD, `unknown method "${method}"`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Echo the client's protocol version when it is one this server implements.
|
|
104
|
+
*
|
|
105
|
+
* Echoing a version we do not implement would be a lie the client cannot detect until a
|
|
106
|
+
* feature silently misbehaves; answering with our own is the spec's own guidance and
|
|
107
|
+
* fails loudly at the one place the client checks.
|
|
108
|
+
*/
|
|
109
|
+
function negotiate(requested) {
|
|
110
|
+
const SUPPORTED = new Set([PROTOCOL_VERSION]);
|
|
111
|
+
return typeof requested === 'string' && SUPPORTED.has(requested) ? requested : PROTOCOL_VERSION;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function describe(tool) {
|
|
115
|
+
return {
|
|
116
|
+
name: tool.name,
|
|
117
|
+
description: tool.description,
|
|
118
|
+
inputSchema: tool.inputSchema,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Content is one text block. Bounded, and truncation is announced in the text itself -
|
|
123
|
+
* the same rule the CLI follows, because a model that cannot tell a truncated answer
|
|
124
|
+
* from a complete one will guess. */
|
|
125
|
+
function contentOf(payload) {
|
|
126
|
+
let text = typeof payload === 'string' ? payload : JSON.stringify(payload, null, 2);
|
|
127
|
+
const limit = typeof payload === 'object' && payload !== null ? payload.maxBytes : 0;
|
|
128
|
+
if (Number.isInteger(limit) && limit > 0 && Buffer.byteLength(text) > limit) {
|
|
129
|
+
const slice = Buffer.from(text).subarray(0, limit).toString('utf8');
|
|
130
|
+
text = `${slice}\nTRUNCATED ${Buffer.byteLength(text) - limit} bytes omitted; raise maxBytes`;
|
|
131
|
+
}
|
|
132
|
+
return { type: 'text', text };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function result(id, value) {
|
|
136
|
+
return { jsonrpc: '2.0', id: id ?? null, result: value };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function error(id, code, message) {
|
|
140
|
+
return { jsonrpc: '2.0', id: id ?? null, error: { code, message } };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Serve until stdin ends.
|
|
145
|
+
*
|
|
146
|
+
* Lines are processed strictly in order. A server that interleaves responses would let a
|
|
147
|
+
* client match the wrong answer to the wrong question, and the tools here are already
|
|
148
|
+
* sequential and cheap - parallelising them would buy nothing and cost correctness.
|
|
149
|
+
*/
|
|
150
|
+
export async function serve(tools, { input = process.stdin, output = process.stdout, version, onError } = {}) {
|
|
151
|
+
const rl = createInterface({ input, crlfDelay: Infinity });
|
|
152
|
+
for await (const line of rl) {
|
|
153
|
+
if (line.trim() === '') continue;
|
|
154
|
+
|
|
155
|
+
let response;
|
|
156
|
+
try {
|
|
157
|
+
response = await handle(JSON.parse(line), tools, { version });
|
|
158
|
+
} catch (parseFailure) {
|
|
159
|
+
response = error(null, RPC.PARSE, `invalid JSON: ${parseFailure.message}`);
|
|
160
|
+
}
|
|
161
|
+
if (response === null) continue;
|
|
162
|
+
output.write(`${JSON.stringify(response)}\n`);
|
|
163
|
+
}
|
|
164
|
+
return EXIT_OK;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const EXIT_OK = 0;
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
// The MCP tool surface: a second front door onto the same logic the CLI uses.
|
|
2
|
+
//
|
|
3
|
+
// Deliberately a subset. Every tool here is READ-ONLY, because the commands that write
|
|
4
|
+
// (`init`, `sync`, `hooks install`, `skills install`, `upgrade`) each exist to be read as
|
|
5
|
+
// a diff by a human before they touch anything (ADR-001, ADR-004). A model can call a
|
|
6
|
+
// tool; a model cannot review a diff it never showed you.
|
|
7
|
+
//
|
|
8
|
+
// The tools build their own payloads from the same modules the commands use rather than
|
|
9
|
+
// calling the command functions. That is not duplication for its own sake: the commands
|
|
10
|
+
// render for terminals, these return JSON for a model, and forcing one renderer through a
|
|
11
|
+
// transport it was not written for is how a "clean" abstraction starts lying about bytes.
|
|
12
|
+
// What is shared is the LOGIC - fingerprint, the registry, the resolver, the region
|
|
13
|
+
// planner - and that is what has to be shared, because two resolvers would be two
|
|
14
|
+
// answers to the same question.
|
|
15
|
+
//
|
|
16
|
+
// Bodies are never inlined by `skills_brief`. Tier 1 is a set of pointers and reasons;
|
|
17
|
+
// the body is a separate call. A 384 KB reference inlined into a tool result is the exact
|
|
18
|
+
// failure progressive disclosure exists to prevent, and a transport makes it easier to
|
|
19
|
+
// cause by accident because the caller cannot see the size until it has paid for it.
|
|
20
|
+
|
|
21
|
+
import { fingerprint } from '../resolve/fingerprint.mjs';
|
|
22
|
+
import { loadRegistry, REGISTRY_PATH } from '../graph/load.mjs';
|
|
23
|
+
import { tokenize, rankSkills, closure, selectWithinBudget, scoreSkill, selectionVerdict, stackCompatible } from '../resolve/resolve.mjs';
|
|
24
|
+
import { loadRules, select, checkTargets, provenanceDigest } from '../projection/project.mjs';
|
|
25
|
+
import { loadDecisions, byId, citations } from '../decisions/index.mjs';
|
|
26
|
+
import { discover } from '../compare/discover.mjs';
|
|
27
|
+
import { buildRow, summarise } from '../compare/row.mjs';
|
|
28
|
+
import { GATES } from '../hooks/gates.mjs';
|
|
29
|
+
import { dirtyPaths, parsePathList, readBody } from '../commands/skills.mjs';
|
|
30
|
+
import { resolveWorkspace } from '../workspace.mjs';
|
|
31
|
+
import { measureSkill } from '../graph/load.mjs';
|
|
32
|
+
import { CLI } from '../name.mjs';
|
|
33
|
+
|
|
34
|
+
const GATE_KINDS = new Map(Object.entries(GATES).map(([name, g]) => [name, g.kind]));
|
|
35
|
+
|
|
36
|
+
/** A schema with no required fields still needs the object declared. */
|
|
37
|
+
const noArgs = { type: 'object', properties: {}, required: [] };
|
|
38
|
+
const rootArg = {
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: { root: { type: 'string', description: 'repository root; defaults to the editor cwd' } },
|
|
41
|
+
required: [],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function rootOf(args) {
|
|
45
|
+
return typeof args?.root === 'string' && args.root.length > 0 ? args.root : process.cwd();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function registry(args) {
|
|
49
|
+
const { registry, errors, ok, verified, workspace } = await loadRegistry(REGISTRY_PATH, args ?? {});
|
|
50
|
+
if (!ok) {
|
|
51
|
+
throw Object.assign(new Error(errors[0]), {
|
|
52
|
+
remedy: `run: ${CLI} graph lint`,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return { registry, verified, workspace };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const TOOLS = [
|
|
59
|
+
{
|
|
60
|
+
name: 'doctor',
|
|
61
|
+
description:
|
|
62
|
+
'Classify a repository: its stack tags, the emitted facts with the file that proved each, and whether a simt-dev manifest is present. Call this first when you do not yet know what a repo is.',
|
|
63
|
+
inputSchema: rootArg,
|
|
64
|
+
async run(args) {
|
|
65
|
+
const root = rootOf(args);
|
|
66
|
+
const ws = await resolveWorkspace(args ?? {});
|
|
67
|
+
const result = await fingerprint(root);
|
|
68
|
+
return {
|
|
69
|
+
root,
|
|
70
|
+
workspace: { root: ws.root, source: ws.source, detail: ws.detail },
|
|
71
|
+
stack: result.stack,
|
|
72
|
+
facts: Object.fromEntries(result.facts.map((f) => [f, result.factEvidence[f] ?? null])),
|
|
73
|
+
signals: result.signals,
|
|
74
|
+
evidence: result.evidence,
|
|
75
|
+
manifest: result.manifest,
|
|
76
|
+
error: result.error ?? null,
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
{
|
|
82
|
+
name: 'skills_index',
|
|
83
|
+
description:
|
|
84
|
+
'The full skill catalogue: every skill with its summary, size, stack tags and whether it is deferred. Cheap enough to always afford, and the fallback when brief returns nothing useful.',
|
|
85
|
+
inputSchema: noArgs,
|
|
86
|
+
async run(args) {
|
|
87
|
+
const { registry: reg, verified, workspace } = await registry(args);
|
|
88
|
+
return {
|
|
89
|
+
skills: reg.skills.length,
|
|
90
|
+
totalBytes: reg.skills.reduce((n, s) => n + s.bytes, 0),
|
|
91
|
+
deferred: reg.skills.filter((s) => s.deferred).map((s) => s.name),
|
|
92
|
+
workspace,
|
|
93
|
+
verified,
|
|
94
|
+
entries: [...reg.skills]
|
|
95
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
96
|
+
.map((s) => ({ name: s.name, summary: s.summary, bytes: s.bytes, stack: s.stack, deferred: Boolean(s.deferred) })),
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
{
|
|
102
|
+
name: 'skills_brief',
|
|
103
|
+
description:
|
|
104
|
+
'THE entry point. Resolve the skill set for a task in a repository. Returns what was selected, why each was selected, what was deferred, and what was NOT selected with the reason - the last part matters, because it is what lets you override the resolver instead of guessing at it.',
|
|
105
|
+
inputSchema: {
|
|
106
|
+
type: 'object',
|
|
107
|
+
properties: {
|
|
108
|
+
root: { type: 'string' },
|
|
109
|
+
task: { type: 'string', description: 'what you are about to do, in your own words' },
|
|
110
|
+
paths: { type: 'string', description: 'comma-separated globs; defaults to the git diff' },
|
|
111
|
+
budget: { type: 'number', description: 'context budget in bytes (default 20480)' },
|
|
112
|
+
skill: { type: 'string', description: 'force a skill in, comma-separated' },
|
|
113
|
+
all: { type: 'boolean', description: 'ignore the budget' },
|
|
114
|
+
},
|
|
115
|
+
required: [],
|
|
116
|
+
},
|
|
117
|
+
async run(args) {
|
|
118
|
+
const root = rootOf(args);
|
|
119
|
+
const { registry: reg } = await registry(args);
|
|
120
|
+
const repo = await fingerprint(root);
|
|
121
|
+
const paths = args?.paths ? parsePathList(args.paths) : await dirtyPaths(root);
|
|
122
|
+
const forced = parsePathList(args?.skill);
|
|
123
|
+
const budget = Number.isInteger(args?.budget) ? args.budget : 20480;
|
|
124
|
+
|
|
125
|
+
const context = { repoStack: repo.signals, taskTokens: tokenize(args?.task ?? ''), paths };
|
|
126
|
+
const { selected, excluded } = rankSkills(reg.skills, context, { forced });
|
|
127
|
+
const expanded = closure(selected, reg.skills);
|
|
128
|
+
|
|
129
|
+
// The budget decides what is INLINED, and it is the same function the CLI
|
|
130
|
+
// uses — not a second copy of the same loop. The two copies had drifted: this one
|
|
131
|
+
// checked `deferred` *after* the budget and inlined a deferred dependency, while
|
|
132
|
+
// `skills brief` checked it first and did not. A caller reaching for the MCP tool
|
|
133
|
+
// and a caller reaching for the command got different answers to one question, and
|
|
134
|
+
// neither could see the other (ADR-035's shape, one level down).
|
|
135
|
+
//
|
|
136
|
+
// No body is ever returned by this tool: a caller that wants one calls
|
|
137
|
+
// skills_show, which makes the cost visible before it is paid.
|
|
138
|
+
const { picked, deferred, used } = selectWithinBudget(expanded, {
|
|
139
|
+
budget,
|
|
140
|
+
all: args?.all === true,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const closureNames = new Set(expanded.map((e) => e.skill.name));
|
|
144
|
+
return {
|
|
145
|
+
repo: { root, stack: repo.stack, facts: repo.facts },
|
|
146
|
+
task: args?.task ?? null,
|
|
147
|
+
budget,
|
|
148
|
+
inlineBytes: used,
|
|
149
|
+
selected: picked.map((e) => ({
|
|
150
|
+
name: e.skill.name,
|
|
151
|
+
bytes: e.skill.bytes,
|
|
152
|
+
summary: e.skill.summary,
|
|
153
|
+
why: [...e.reasons, ...[...e.via].map((v) => `requires:${v}`)],
|
|
154
|
+
})),
|
|
155
|
+
deferred: deferred.map((e) => ({
|
|
156
|
+
name: e.skill.name,
|
|
157
|
+
bytes: e.skill.bytes,
|
|
158
|
+
reason: e.skill.deferred ? 'marked deferred' : 'over budget',
|
|
159
|
+
})),
|
|
160
|
+
notSelected: excluded
|
|
161
|
+
.filter((e) => !closureNames.has(e.skill.name))
|
|
162
|
+
.map((e) => ({ name: e.skill.name, reason: e.reason })),
|
|
163
|
+
};
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
{
|
|
168
|
+
name: 'skills_show',
|
|
169
|
+
description: 'One skill body, with the metadata needed to decide whether to trust it: source path, which of the two locations it was read from, and its cited decisions.',
|
|
170
|
+
inputSchema: {
|
|
171
|
+
type: 'object',
|
|
172
|
+
properties: {
|
|
173
|
+
name: { type: 'string' },
|
|
174
|
+
root: { type: 'string' },
|
|
175
|
+
maxBytes: { type: 'number', description: 'hard ceiling on the returned text' },
|
|
176
|
+
},
|
|
177
|
+
required: ['name'],
|
|
178
|
+
},
|
|
179
|
+
async run(args) {
|
|
180
|
+
const name = args?.name;
|
|
181
|
+
if (typeof name !== 'string' || name.length === 0) throw new Error('skills_show needs a skill name');
|
|
182
|
+
const { registry: reg } = await registry(args);
|
|
183
|
+
const skill = reg.skills.find((s) => s.name === name);
|
|
184
|
+
if (!skill) throw new Error(`no skill named "${name}"; call skills_index or skills_find`);
|
|
185
|
+
|
|
186
|
+
const root = rootOf(args);
|
|
187
|
+
const ws = await resolveWorkspace(args ?? {});
|
|
188
|
+
const body = await readBody(skill, ws.root, root);
|
|
189
|
+
const source = skill.sources?.find((s) => s.role === 'canonical') ?? skill.sources?.[0];
|
|
190
|
+
const located = body.startsWith('(body unavailable') ? null : body;
|
|
191
|
+
return {
|
|
192
|
+
name,
|
|
193
|
+
summary: skill.summary,
|
|
194
|
+
bytes: skill.bytes,
|
|
195
|
+
capabilities: skill.capabilities,
|
|
196
|
+
stack: skill.stack,
|
|
197
|
+
deferred: Boolean(skill.deferred),
|
|
198
|
+
source: source ? `${source.repo}/${source.path}` : null,
|
|
199
|
+
readFrom: located === null ? 'unavailable' : body.length === 0 ? 'empty' : 'ok',
|
|
200
|
+
requires: skill.requires ?? [],
|
|
201
|
+
variants: (skill.variants ?? []).map((v) => ({ id: v.id, stack: v.stack, bytes: v.bytes })),
|
|
202
|
+
maxBytes: Number.isInteger(args?.maxBytes) ? args.maxBytes : 0,
|
|
203
|
+
body: body || null,
|
|
204
|
+
};
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
{
|
|
209
|
+
name: 'skills_find',
|
|
210
|
+
description: 'Raw search across capability, keyword and summary. Use when skills_brief has nothing and you suspect a skill exists.',
|
|
211
|
+
inputSchema: {
|
|
212
|
+
type: 'object',
|
|
213
|
+
properties: { query: { type: 'string' } },
|
|
214
|
+
required: ['query'],
|
|
215
|
+
},
|
|
216
|
+
async run(args) {
|
|
217
|
+
const query = typeof args?.query === 'string' ? args.query : '';
|
|
218
|
+
if (query.trim() === '') throw new Error('skills_find needs a query');
|
|
219
|
+
const { registry: reg } = await registry(args);
|
|
220
|
+
const tokens = tokenize(query);
|
|
221
|
+
return {
|
|
222
|
+
query,
|
|
223
|
+
results: reg.skills
|
|
224
|
+
.map((skill) => ({ skill, ...scoreSkill(skill, { repoStack: [], taskTokens: tokens, paths: [] }) }))
|
|
225
|
+
.filter((r) => selectionVerdict(r.evidence, r.reasons) === null)
|
|
226
|
+
.sort((a, b) => b.score - a.score || a.skill.name.localeCompare(b.skill.name))
|
|
227
|
+
.map((r) => ({ name: r.skill.name, score: r.score, summary: r.skill.summary, why: r.reasons })),
|
|
228
|
+
};
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
{
|
|
233
|
+
name: 'skills_why',
|
|
234
|
+
description:
|
|
235
|
+
'The signal chain for one skill in one repository: its score, the reasons behind it, whether the stack gate admits it at all, and what it would take to make it selected.',
|
|
236
|
+
inputSchema: {
|
|
237
|
+
type: 'object',
|
|
238
|
+
properties: { name: { type: 'string' }, root: { type: 'string' }, task: { type: 'string' } },
|
|
239
|
+
required: ['name'],
|
|
240
|
+
},
|
|
241
|
+
async run(args) {
|
|
242
|
+
const { registry: reg } = await registry(args);
|
|
243
|
+
const skill = reg.skills.find((s) => s.name === args?.name);
|
|
244
|
+
if (!skill) throw new Error(`no skill named "${args?.name}"; call skills_find`);
|
|
245
|
+
const root = rootOf(args);
|
|
246
|
+
const repo = await fingerprint(root);
|
|
247
|
+
const paths = await dirtyPaths(root);
|
|
248
|
+
const { score, reasons } = scoreSkill(skill, {
|
|
249
|
+
repoStack: repo.signals,
|
|
250
|
+
taskTokens: tokenize(args?.task ?? ''),
|
|
251
|
+
paths,
|
|
252
|
+
});
|
|
253
|
+
return {
|
|
254
|
+
name: skill.name,
|
|
255
|
+
score,
|
|
256
|
+
compatible: stackCompatible(skill, repo.signals),
|
|
257
|
+
reasons,
|
|
258
|
+
repo: { root, signals: repo.signals, evidence: repo.evidence },
|
|
259
|
+
task: args?.task ?? null,
|
|
260
|
+
pathsSeen: paths.slice(0, 20),
|
|
261
|
+
};
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
{
|
|
266
|
+
name: 'rules_list',
|
|
267
|
+
description:
|
|
268
|
+
'The rule catalogue, and which rules a given repository actually receives. Pass `root` to get the selection rather than just the catalogue.',
|
|
269
|
+
inputSchema: {
|
|
270
|
+
type: 'object',
|
|
271
|
+
properties: { root: { type: 'string' } },
|
|
272
|
+
required: [],
|
|
273
|
+
},
|
|
274
|
+
async run(args) {
|
|
275
|
+
const root = args?.root;
|
|
276
|
+
const rules = await loadRules(typeof root === 'string' && root ? root : null);
|
|
277
|
+
const repo = typeof root === 'string' && root ? await fingerprint(root) : null;
|
|
278
|
+
const { selected, notes } = repo
|
|
279
|
+
? select(rules, repo.stack, repo.facts)
|
|
280
|
+
: { selected: [], notes: [] };
|
|
281
|
+
const chosen = new Set(selected.map((r) => r.id));
|
|
282
|
+
return {
|
|
283
|
+
sha: await provenanceDigest(),
|
|
284
|
+
notes,
|
|
285
|
+
rules: [...rules]
|
|
286
|
+
.sort((a, b) => String(a.id).localeCompare(String(b.id)))
|
|
287
|
+
.map((r) => ({
|
|
288
|
+
id: r.id,
|
|
289
|
+
title: r.title,
|
|
290
|
+
applies: r.applies,
|
|
291
|
+
origin: r.origin,
|
|
292
|
+
source: r.source,
|
|
293
|
+
bytes: r.bytes,
|
|
294
|
+
adrs: citations(r.body),
|
|
295
|
+
error: r.error,
|
|
296
|
+
selected: chosen.has(r.id),
|
|
297
|
+
})),
|
|
298
|
+
};
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
{
|
|
303
|
+
name: 'rules_why',
|
|
304
|
+
description:
|
|
305
|
+
'Why a rule exists. Give a rule id for the decisions that settled it and whether each is still in force, or an ADR id for the rules that decision restsates. A rule citing no decision reports that - it is a finding, not an empty answer.',
|
|
306
|
+
inputSchema: {
|
|
307
|
+
type: 'object',
|
|
308
|
+
properties: { what: { type: 'string', description: 'a rule id or ADR-NNN' } },
|
|
309
|
+
required: ['what'],
|
|
310
|
+
},
|
|
311
|
+
async run(args) {
|
|
312
|
+
const what = typeof args?.what === 'string' ? args.what : '';
|
|
313
|
+
const rules = await loadRules(null);
|
|
314
|
+
const { decisions } = await loadDecisions();
|
|
315
|
+
const index = byId(decisions);
|
|
316
|
+
|
|
317
|
+
if (/^ADR-\d{3}$/i.test(what)) {
|
|
318
|
+
const id = what.toUpperCase();
|
|
319
|
+
const d = decisions.find((x) => x.id === id);
|
|
320
|
+
if (!d) throw new Error(`no decision record ${id}`);
|
|
321
|
+
return {
|
|
322
|
+
direction: 'decision',
|
|
323
|
+
id: d.id,
|
|
324
|
+
title: d.title,
|
|
325
|
+
status: d.status,
|
|
326
|
+
// The record travels inside the installed package. A relative path would resolve
|
|
327
|
+
// against the consuming repo and name a file that is not there, which is
|
|
328
|
+
// worse than giving no path at all.
|
|
329
|
+
readWith: `${CLI} rules why ${d.id}`,
|
|
330
|
+
supersededBy: d.supersededBy,
|
|
331
|
+
supersedes: d.supersedes,
|
|
332
|
+
partial: d.partial,
|
|
333
|
+
rules: rules.filter((r) => citations(r.body).includes(id)).map((r) => r.id),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const rule = rules.find((r) => r.id === what);
|
|
338
|
+
if (!rule) throw new Error(`no rule with id "${what}"; call rules_list`);
|
|
339
|
+
const cited = citations(rule.body);
|
|
340
|
+
return {
|
|
341
|
+
direction: 'rule',
|
|
342
|
+
id: rule.id,
|
|
343
|
+
title: rule.title,
|
|
344
|
+
source: rule.source,
|
|
345
|
+
origin: rule.origin,
|
|
346
|
+
uncited: cited.length === 0,
|
|
347
|
+
decisions: cited.map((c) => {
|
|
348
|
+
const d = index.get(c);
|
|
349
|
+
return d
|
|
350
|
+
? { id: d.id, title: d.title, status: d.status, supersededBy: d.supersededBy }
|
|
351
|
+
: { id: c, missing: true };
|
|
352
|
+
}),
|
|
353
|
+
};
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
|
|
357
|
+
{
|
|
358
|
+
name: 'check',
|
|
359
|
+
description:
|
|
360
|
+
'The gate for one repository: is its generated region current, unmanaged, drifted or malformed, and does any rule file lack an id. Read-only - it never repairs.',
|
|
361
|
+
inputSchema: rootArg,
|
|
362
|
+
async run(args) {
|
|
363
|
+
const root = rootOf(args);
|
|
364
|
+
const findings = await checkTargets(root);
|
|
365
|
+
for (const r of await loadRules()) {
|
|
366
|
+
if (r.error) findings.push({ file: r.id ?? r.file, status: 'invalid', detail: r.error });
|
|
367
|
+
}
|
|
368
|
+
const bad = findings.filter((f) => f.status !== 'current');
|
|
369
|
+
return { root, ok: bad.length === 0, findings };
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
|
|
373
|
+
{
|
|
374
|
+
name: 'compare',
|
|
375
|
+
description:
|
|
376
|
+
'The organisation-wide matrix: every repository under the root, its adoption, region state, pinned simt-dev SHA, hook routing and declared gates, plus the problems found. This is the command that replaces reconstructing the estate by hand.',
|
|
377
|
+
inputSchema: {
|
|
378
|
+
type: 'object',
|
|
379
|
+
properties: {
|
|
380
|
+
root: { type: 'string', description: 'a workspace directory, or one repository' },
|
|
381
|
+
workspace: { type: 'string', description: 'workspace root when root is not given' },
|
|
382
|
+
},
|
|
383
|
+
required: [],
|
|
384
|
+
},
|
|
385
|
+
async run(args) {
|
|
386
|
+
const root = args?.root ?? args?.workspace ?? (await resolveWorkspace(args ?? {})).root ?? process.cwd();
|
|
387
|
+
const found = await discover(root);
|
|
388
|
+
if (found.error) throw Object.assign(new Error(`${root}: ${found.error}`), { remedy: 'pass root' });
|
|
389
|
+
const expectedSha = await provenanceDigest();
|
|
390
|
+
const context = { expectedSha, gates: new Set(Object.keys(GATES)), gateKinds: GATE_KINDS };
|
|
391
|
+
const rows = [];
|
|
392
|
+
for (const repo of found.repos) rows.push(await buildRow(repo, context));
|
|
393
|
+
return {
|
|
394
|
+
root,
|
|
395
|
+
shape: found.shape,
|
|
396
|
+
expectedSha,
|
|
397
|
+
summary: summarise(rows, { root, shape: found.shape, expectedSha }),
|
|
398
|
+
repos: rows,
|
|
399
|
+
worktrees: found.worktrees.map((w) => ({ name: w.name, owner: w.ownerName })),
|
|
400
|
+
directories: found.directories,
|
|
401
|
+
};
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
];
|
|
405
|
+
|
|
406
|
+
export function toolNames() {
|
|
407
|
+
return TOOLS.map((t) => t.name);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export { measureSkill };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Every name this tool answers to, in one file.
|
|
2
|
+
//
|
|
3
|
+
// They are collected here because the alternative was measured rather than assumed:
|
|
4
|
+
// 276 occurrences of the command name across 33 files under src/, threaded through
|
|
5
|
+
// remedy strings, help text, shim source and workflow templates. A rename touching
|
|
6
|
+
// that many literals is a rename nobody finishes, and a half-finished one is worse
|
|
7
|
+
// than an unrenamed tool — it ships remedies telling a reader to run a command that
|
|
8
|
+
// does not exist, which is the exact failure this whole tool exists to prevent.
|
|
9
|
+
//
|
|
10
|
+
// `test/naming.test.mjs` enforces the half that a comment cannot: a command string may
|
|
11
|
+
// not SPELL the command, only interpolate CLI. Without that guard this file is a
|
|
12
|
+
// suggestion, and a suggestion about a name is not a constraint.
|
|
13
|
+
//
|
|
14
|
+
// The names split into two groups, and which group a name is in decides whether it can
|
|
15
|
+
// move at all. Getting that backwards is the expensive mistake: the publish name can
|
|
16
|
+
// change at any time, the on-disk names cannot.
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The project: the repository, and every name it has already written into somebody
|
|
20
|
+
* else's repository. LOCKED — not because renaming is forbidden, but because these
|
|
21
|
+
* strings are on disk in repositories this tool does not own, and changing one is a
|
|
22
|
+
* breaking change to files rather than to a package.
|
|
23
|
+
*/
|
|
24
|
+
export const PROJECT = 'simt-dev';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The manifest an adopting repository owns. LOCKED: fourteen repositories carry a
|
|
28
|
+
* `simt-dev.json`, and `read` would fail rather than migrate in every one of them.
|
|
29
|
+
*/
|
|
30
|
+
export const MANIFEST_FILE = 'simt.json';
|
|
31
|
+
|
|
32
|
+
/** What the manifest file was called, and still is in every adopted repo. */
|
|
33
|
+
export const MANIFEST_LEGACY = 'simt-dev.json';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The namespace the generated region writes into a repository's files: the begin and end
|
|
37
|
+
* markers, and the provenance header inside them. It follows the command, like the
|
|
38
|
+
* manifest: the region is the command's footprint in a file the repo owns.
|
|
39
|
+
*/
|
|
40
|
+
export const MARKER_PREFIX = 'simt';
|
|
41
|
+
|
|
42
|
+
/** What the region was namespaced under, and still is in every adopted repo. */
|
|
43
|
+
export const MARKER_PREFIX_LEGACY = 'simt-dev';
|
|
44
|
+
|
|
45
|
+
/** The config directory on the adopting engineer's machine. LOCKED, same argument. */
|
|
46
|
+
export const CONFIG_DIR = PROJECT;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The gate this tool writes into a repository. LOCKED: it is a path in `.github`, and
|
|
50
|
+
* a repo that renames the file silently stops running the gate it was adopted with.
|
|
51
|
+
*/
|
|
52
|
+
export const WORKFLOW_FILE_NAME = `${PROJECT}-check.yml`;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The environment variable that points a shim at a binary it cannot find. LOCKED, and
|
|
56
|
+
* deliberately still spelled with the project name: it is read by shims already sitting
|
|
57
|
+
* in repositories' `.git` directories, so renaming it would strand every one of them
|
|
58
|
+
* with no way to find the tool again.
|
|
59
|
+
*/
|
|
60
|
+
export const BIN_ENV = 'SIMT_DEV_BIN';
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The command a reader types.
|
|
64
|
+
*
|
|
65
|
+
* This is the name that moved, and it is deliberately not the project name. The
|
|
66
|
+
* project keeps `simt-dev` in the places on disk the tool cannot rewrite in place -
|
|
67
|
+
* the gate file, the hooks directory, the shim's environment variable, the entry
|
|
68
|
+
* point - and renaming any of those is a breaking change to files rather than to a
|
|
69
|
+
* package. The command appears only in text the tool itself writes - remedies, region
|
|
70
|
+
* headers, the manifest comment - and that text migrates on the next write: read-both
|
|
71
|
+
* covers the gap in between. That is what makes it the cheap name to change, and the
|
|
72
|
+
* one worth spending.
|
|
73
|
+
*
|
|
74
|
+
* `test/naming.test.mjs` is what stops this from drifting back into a hundred
|
|
75
|
+
* literals. Without it this constant is a comment with an export.
|
|
76
|
+
*/
|
|
77
|
+
export const CLI = 'simt';
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The package a repository installs to obtain the command.
|
|
81
|
+
*
|
|
82
|
+
* Unscoped, and equal to the project name, because that name was reserved on the npm
|
|
83
|
+
* registry before this package existed and holds only an empty `1.0.0`. Reserving it
|
|
84
|
+
* and then publishing under a different spelling would leave the one name anyone can
|
|
85
|
+
* find pointing at nothing.
|
|
86
|
+
*
|
|
87
|
+
* Not derived from CLI. The command moved to `simt`; the package did not, and a
|
|
88
|
+
* derivation that silently picked between them would turn a one-line rename into a
|
|
89
|
+
* one-line diff whose meaning depends on a comparison nobody reading it would think
|
|
90
|
+
* to check.
|
|
91
|
+
*/
|
|
92
|
+
export const PACKAGE_NAME = PROJECT;
|
|
93
|
+
|
|
94
|
+
/** The entry point, relative to the package root. LOCKED: `bin` in package.json. */
|
|
95
|
+
export const BIN_REL = 'bin/simt-dev.mjs';
|