simt-dev 0.2.0 → 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 +173 -7
- package/README.md +4 -4
- package/docs/decisions/ADR-019-a-declared-command-is-the-runner.md +27 -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 +17 -0
- 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 +173 -57
- package/src/commands/index.mjs +53 -45
- package/src/commands/init.mjs +89 -13
- package/src/commands/rules.mjs +54 -3
- package/src/commands/skills.mjs +143 -15
- package/src/commands/skillsinstall.mjs +3 -3
- package/src/commands/sync.mjs +153 -11
- package/src/compare/hooksstate.mjs +96 -9
- package/src/compare/row.mjs +34 -5
- package/src/decisions/index.mjs +21 -6
- package/src/graph/load.mjs +125 -14
- package/src/hooks/capability.mjs +48 -4
- package/src/hooks/dispatcher.mjs +50 -5
- package/src/hooks/gates.mjs +0 -2
- package/src/hooks/resolve.mjs +24 -25
- package/src/hooks/shim.mjs +29 -5
- package/src/identity.mjs +0 -11
- package/src/mcp/tools.mjs +2 -2
- package/src/output/index.mjs +8 -7
- package/src/package-version.mjs +74 -0
- package/src/projection/conflicts.mjs +56 -5
- package/src/projection/manifest.mjs +247 -25
- package/src/projection/mdc.mjs +4 -13
- package/src/projection/project.mjs +1 -1
- package/src/projection/rules.mjs +21 -8
- package/src/projection/targets.mjs +53 -7
- package/src/resolve/eval.mjs +64 -0
- package/src/resolve/fingerprint.mjs +1 -0
- package/src/resolve/near.mjs +26 -7
- package/src/resolve/resolve.mjs +1 -1
- package/src/resolve/source.mjs +1 -1
- package/src/workspace.mjs +0 -9
- package/graph/skills.graph.json +0 -1622
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# ADR-062 — A fresh adoption seeds the canonical gates it can run, and never more
|
|
2
|
+
|
|
3
|
+
Status: **accepted** · 2026-10-13 · filed as S40
|
|
4
|
+
**Affects:** `simt init`, `src/projection/manifest.mjs` (the `ECOSYSTEMS` table, `defaultGates`),
|
|
5
|
+
`src/hooks/capability.mjs` (the tool probe, the `tools` field), `src/commands/init.mjs`,
|
|
6
|
+
eight new files in `rules/stacks/`
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
S40's finding: the eight common ecosystems outside the JavaScript world — Rust, Go, .NET,
|
|
11
|
+
Swift, Deno, Ruby, Dart and Elixir — emit no stack tag. The fingerprint recognises them as
|
|
12
|
+
`has-*` facts, and the only reader of those facts was the Dependabot configuration. So a
|
|
13
|
+
fresh Rust repository received the core essay and a manifest that runs nothing: no profile
|
|
14
|
+
was selected because there was none to select, and no gate was proposed because no row keyed
|
|
15
|
+
on a fact. The asymmetry is the defect — the ecosystems the tool recognises were the ones it
|
|
16
|
+
served least, while the ecosystems that do emit stack tags got the full treatment.
|
|
17
|
+
|
|
18
|
+
Two existing mechanisms make the fix data-only, per ADR-013:
|
|
19
|
+
|
|
20
|
+
- **Profiles select on tags or facts.** `simt-applies` already accepts a fact, and the
|
|
21
|
+
genericity tests already assert that a profile may select on a fact no stack tag names.
|
|
22
|
+
- **Gates are planned by signal.** `planGates` takes the union of stack tags and facts, so a
|
|
23
|
+
row can key on either; the two never cross-match, because facts are `has-*` and tags are
|
|
24
|
+
not.
|
|
25
|
+
|
|
26
|
+
What was missing is one artefact per ecosystem: the profile file — and for gates, a row that
|
|
27
|
+
says what a fresh adoption of one should declare. The second is the harder half, because
|
|
28
|
+
declaring a gate is not free:
|
|
29
|
+
|
|
30
|
+
A declared gate that cannot run is worse than an absent one. Absent, an author sees nothing
|
|
31
|
+
and knows nothing is being checked; declared-and-unrunnable, the dispatcher fails closed on
|
|
32
|
+
every commit, and the only exit an author can find is `--no-verify`, which teaches the whole
|
|
33
|
+
team that the gate is theatre. So the question a seeding design has to answer is not "which
|
|
34
|
+
ecosystem is here" — the marker file answers that — but "can this machine run what the gate
|
|
35
|
+
says it runs". That is ADR-017's probe-before-declare, and it shapes every ruling below.
|
|
36
|
+
|
|
37
|
+
## Rulings
|
|
38
|
+
|
|
39
|
+
1. **Every ecosystem the fingerprint names gets a profile.** Eight new stack profiles, each
|
|
40
|
+
`simt-applies:` on its own fact (the files are named in the Affects line), so one marker
|
|
41
|
+
file reaches both the region the repository reads and the gates its commits run. Maven's profile (`jvm`) predates
|
|
42
|
+
this ruling and applies on the same fact, so the table has nine rows and eight new files.
|
|
43
|
+
An ecosystem arriving later is a profile file and a row: no engine change (ADR-013).
|
|
44
|
+
2. **A fresh adoption seeds the canonical commands, and only where the probe found the
|
|
45
|
+
tool.** On `init`, when the fingerprint emits an ecosystem fact, the row's commands land in
|
|
46
|
+
`gates.commands` for each gate it proposes — but only when the probe found `tool`. The
|
|
47
|
+
probe is the same discipline ADR-017 applies to node tools: a binary on PATH, or a file in
|
|
48
|
+
the repository when the row says `file` (the Maven wrapper, probed the way `gradlew`
|
|
49
|
+
already is). A marker says the ecosystem is here; the probe says its program can run. Both
|
|
50
|
+
must be true for a seed to land. `readCapability` reports the results in a `tools` field
|
|
51
|
+
next to `binaries`, so the plan can say which seeds fired and which did not.
|
|
52
|
+
3. **A seed is a default, and a default loses to every form of declaration.** A command the
|
|
53
|
+
repository already declared in `gates.commands` is never re-seeded. An npm runner keeps its
|
|
54
|
+
own scripts untouched: `package.json` is the repository's own answer to "how do I test and
|
|
55
|
+
format", and a seed that outranked a script would replace a choice with a default —
|
|
56
|
+
precisely in the mixed repository (a `package.json` and a `Cargo.toml`) where the script is
|
|
57
|
+
the deliberate answer. The escape hatch for a repository whose runner differs from the
|
|
58
|
+
canonical one (a Rails app, a Flutter module) is to declare its own command, and
|
|
59
|
+
declaration beats seed everywhere.
|
|
60
|
+
4. **Where the probe fails, the gate is dropped with a why and a remedy, never declared.**
|
|
61
|
+
This is the invariant `planGates` already holds for every proposed gate; seeding inherits
|
|
62
|
+
it rather than creating an exception to it. A fresh Rust checkout on a machine without
|
|
63
|
+
cargo gets `dropped gate test: …` with a remedy that names the install, not a gate that
|
|
64
|
+
blocks its first commit.
|
|
65
|
+
|
|
66
|
+
## Consequences
|
|
67
|
+
|
|
68
|
+
- `simt init` on a fresh Rust, Go, .NET, Swift, Deno, Ruby, Dart or Elixir repository declares
|
|
69
|
+
runnable gates when the toolchain is present — a `seeded` line in the plan output names each
|
|
70
|
+
command that landed — and drops them with a remedy when it is not.
|
|
71
|
+
- `readCapability` gains the probe and the `tools` field: a PATH lookup, plus an executable
|
|
72
|
+
check in the repository root for a `file` row.
|
|
73
|
+
- `defaultGates`'s expansion of an ecosystem row tolerates a missing phase key: the Maven row
|
|
74
|
+
proposes only pre-push, and absence is "none", not a defect.
|
|
75
|
+
- The seed picks one canonical command per gate (`bundle exec rspec`, `dart test`). A
|
|
76
|
+
repository whose runner differs declares its own; ruling 3 makes that declaration win.
|
|
77
|
+
- Acceptance: one test per ecosystem asserting marker → fact → profile selection; a seeding
|
|
78
|
+
matrix (every row seeds when the tool can run, every row drops when it cannot, an npm
|
|
79
|
+
repository takes no seed, a declaration beats a seed); and two end-to-end Rust inits, with
|
|
80
|
+
and without `cargo` on PATH.
|
|
81
|
+
|
|
82
|
+
## Rejected
|
|
83
|
+
|
|
84
|
+
- **Seed on the marker's word alone.** The marker is a file in the repository; the tool is a
|
|
85
|
+
program on the machine. A repository can have one without the other — a freshly cloned Rust
|
|
86
|
+
checkout on a machine that has never installed the toolchain is the common case — and
|
|
87
|
+
seeding there would declare a gate that fails closed on every commit. ADR-017 exists for
|
|
88
|
+
exactly this.
|
|
89
|
+
- **Seed into an npm repository's scripts.** The scripts are the repository's own answer, and
|
|
90
|
+
a seed that outranks them replaces a choice with a default. The mixed repository is where
|
|
91
|
+
this matters most, because there the script and the marker coexist on purpose.
|
|
92
|
+
- **A second gate table or a new engine path for ecosystems.** The fact was already flowing
|
|
93
|
+
into `planGates`, and the union of tags and facts never cross-matches. What was missing was
|
|
94
|
+
a row and a file, which is data; the engine that reads it already existed.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# ADR-063 — The adoption escape hatch is opt-in, and the default writes in place
|
|
2
|
+
|
|
3
|
+
Status: **accepted** · 2026-10-13 · filed after a consumer asked why every adoption branched
|
|
4
|
+
**Affects:** `simt init`, `src/commands/init.mjs` (`establishEscapeHatch` and its call site),
|
|
5
|
+
`src/argv.mjs`, `src/commands/help.mjs`; amends plan 0002 §8.2 item 3
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
Plan 0002 §8.2 item 3 ruled: "Create a branch and a `backup-before-simt-dev` tag before the
|
|
10
|
+
first write, always." The tool complied — `simt init --yes` in a git repository tagged the
|
|
11
|
+
pre-adoption commit, checked out a branch named after the tool, and wrote every adoption file
|
|
12
|
+
onto it. The reviewable-diff argument is sound: adoption writes a dozen files into a
|
|
13
|
+
repository the tool does not own, and doing it on a dedicated branch leaves the mainline
|
|
14
|
+
untouched with two named undo paths.
|
|
15
|
+
|
|
16
|
+
The "always" was the part that failed. Consumers kept reporting the branch switch as a
|
|
17
|
+
surprise: they ran the documented command to adopt their repository, and found themselves on
|
|
18
|
+
a branch they had not chosen, with a tag they had not asked for. Review 0001's S15 fixed the
|
|
19
|
+
silence — the output now names both operations and the command that undoes each — but a
|
|
20
|
+
well-announced surprise is still a surprise. The branch and the tag are an undo path for
|
|
21
|
+
readers who want one; making them unconditional turned a safety feature into a default that
|
|
22
|
+
every consumer has to explain away.
|
|
23
|
+
|
|
24
|
+
## Rulings
|
|
25
|
+
|
|
26
|
+
1. **The default adoption writes in place and creates no git object.** `simt init --yes`
|
|
27
|
+
without the new flag leaves the reader on the branch they were on, tags nothing, and
|
|
28
|
+
writes the adoption files as uncommitted changes. The reviewable diff survives: `git diff`
|
|
29
|
+
shows the whole adoption, and discarding it is `git checkout -- .` plus a clean. The run
|
|
30
|
+
says so in an `in place` line that names the flag which restores the undo path.
|
|
31
|
+
2. **The escape hatch moves behind a flag named for what it gives.** `--backup` is the
|
|
32
|
+
opt-in: tag the pre-adoption commit as the backup tag and branch onto the adopt branch
|
|
33
|
+
before the first write, exactly as the tool did before. The name is the promise — a way
|
|
34
|
+
back to what the repository was — and it already appears in the output the tool prints, so
|
|
35
|
+
the flag and its effect share one word.
|
|
36
|
+
3. **Whatever a run does to git state is reported, including doing nothing.** Plan 0002's
|
|
37
|
+
heading over the adoption rules is "Nothing is silent". With the hatch opt-in, the
|
|
38
|
+
in-place run is a git-state event too — the deliberate absence of one — and it gets a line
|
|
39
|
+
that says what was not done and how to get it done. The `--backup` run keeps the two lines
|
|
40
|
+
S15 established: what was tagged, and where the branch came from.
|
|
41
|
+
|
|
42
|
+
## Consequences
|
|
43
|
+
|
|
44
|
+
- `simt init --yes` on a git repository no longer moves the reader or writes a tag; consumers
|
|
45
|
+
who scripted against the old default add `--backup`. This is the one behaviour the change
|
|
46
|
+
invalidates, and it is named here rather than discovered in a broken pipeline.
|
|
47
|
+
- The `in place` line appears on every real adoption that does not pass the flag, and the
|
|
48
|
+
help text for `init` and for the flag say what it means.
|
|
49
|
+
- The dry-run path is untouched: it already created no git object, and it still says `apply
|
|
50
|
+
with: simt init --yes`.
|
|
51
|
+
- Acceptance: the S15 test now runs with the flag and asserts both operations are reported;
|
|
52
|
+
a new test runs without it and asserts no tag, no branch, the reader stays put, and the
|
|
53
|
+
in-place line names the flag; the not-a-git-work-tree test runs with the flag, because that
|
|
54
|
+
is where the best-effort note lives now.
|
|
55
|
+
|
|
56
|
+
## Rejected
|
|
57
|
+
|
|
58
|
+
- **Keep "always" and add a flag to opt out.** An opt-out flag fixes the surprise for those
|
|
59
|
+
who read the help, and it still surprises everyone else — which is every consumer of a tool
|
|
60
|
+
whose default action is to touch someone's repository. The cost of the wrong default is
|
|
61
|
+
paid on every adoption; the cost of the right default is one flag on the few that want it.
|
|
62
|
+
- **Two flags, one per operation.** The tag and the branch arrive together in the code and in
|
|
63
|
+
the undo story: the tag is the reset target, the branch is where the adoption sits while it
|
|
64
|
+
is reviewed. Splitting them lets a reader build a state the tool has never tested — a branch
|
|
65
|
+
without a tag to reset to, or a tag with nothing isolated under it.
|
|
66
|
+
- **Move the decision into the dry-run preview.** The dry run shows what would change in the
|
|
67
|
+
files; git state is not a file, and a preview line for an operation that now does not happen
|
|
68
|
+
by default would describe the opt-in as if it were the plan. The `in place` line after
|
|
69
|
+
`--yes` is where the reader learns what the real run did not do.
|
package/docs/decisions/README.md
CHANGED
|
@@ -66,6 +66,8 @@ record's status line — never by editing the ruling in place.
|
|
|
66
66
|
| [056](ADR-056-three-names-one-of-which-is-cheap-to-move.md) | The command, the package and the project are three names, and only one of them is cheap to move | accepted · amends ADR-023 |
|
|
67
67
|
| [057](ADR-057-an-identity-is-per-developer-and-a-repository-does-not-own-it.md) | An identity is per-developer, and a repository does not own it | accepted |
|
|
68
68
|
| [058](ADR-058-one-tool-three-channels-and-the-order-is-forced.md) | One tool, three channels, and the order is forced | accepted |
|
|
69
|
+
| [059](ADR-059-the-version-a-repository-runs-is-the-one-it-pinned.md) | The version a repository runs is the one it pinned, and the gate refuses otherwise | accepted |
|
|
70
|
+
| [060](ADR-060-a-repository-with-nothing-to-run-is-adopted-and-says-so.md) | A repository with nothing to run is adopted and says so; a gate is verified where it runs | accepted |
|
|
69
71
|
|
|
70
72
|
ADR-001 to ADR-010 resolve the contradictions found across the 16 repos in the 2026-10-05
|
|
71
73
|
audit; each record names the repos whose text it replaces. ADR-011 and ADR-012 are the
|
|
@@ -402,6 +404,21 @@ docstring that used to claim the whole output "never contains a path" is withdra
|
|
|
402
404
|
asserted a property the function never had, which is the same defect as a citation that
|
|
403
405
|
cannot be resolved — a sentence that reads like evidence and carries none.
|
|
404
406
|
|
|
407
|
+
ADR-059 is the first record here whose subject is the version of the tool itself, and it
|
|
408
|
+
exists because a check that runs from the wrong binary is a pass that never happened. ADR-010
|
|
409
|
+
allowed a tool that is not a project dependency to be invoked directly, *provided its version
|
|
410
|
+
is pinned* — and for a repository without npm that "pinned" half had no mechanism. The version
|
|
411
|
+
on `PATH` was whatever the last global install left there, and the gate that ran on commit was
|
|
412
|
+
the one in it: a green tick from a check running a different gate than CI is worse than a red
|
|
413
|
+
build, because it is the failure that looks like evidence. The record rules that the version a
|
|
414
|
+
repository adopted is recorded in the manifest, that the gate refuses when the running binary
|
|
415
|
+
differs, and that the install and the remedy are per kind — so the check CI runs is the one the
|
|
416
|
+
machine remedy names. Its most load-bearing consequence is the one that keeps it from becoming a
|
|
417
|
+
trap: a repository adopted before the field existed has nothing to compare against, so it is
|
|
418
|
+
skipped rather than failed, and reported as drift instead — because a gate that turns red on the
|
|
419
|
+
day a check ships, for a mismatch nobody can resolve until they re-adopt, is a gate no one will
|
|
420
|
+
trust again.
|
|
421
|
+
|
|
405
422
|
## The rule these records enforce
|
|
406
423
|
|
|
407
424
|
Every shared rule has exactly **one** canonical home. Every other location is either a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "simt-dev",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Shared agent rules, skills and git hooks for a repository: one command adopts a repo, and every gate it declares is probed before it is written.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Said Elimam",
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
"files": [
|
|
20
20
|
"bin",
|
|
21
21
|
"src",
|
|
22
|
-
"graph",
|
|
23
22
|
"rules",
|
|
24
23
|
"docs/decisions",
|
|
25
24
|
"CHANGELOG.md"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
simt-id: dart
|
|
3
|
+
simt-title: Dart
|
|
4
|
+
simt-applies: [has-pubspec]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Stack profile: Dart
|
|
8
|
+
|
|
9
|
+
Applies to: `has-pubspec` — a `pubspec.yaml`. A Flutter project is a Dart project
|
|
10
|
+
with the `flutter` SDK dependency; the Flutter-specific rules live with the tools
|
|
11
|
+
that build it, not here.
|
|
12
|
+
|
|
13
|
+
## Build and gates
|
|
14
|
+
|
|
15
|
+
- `dart analyze`, `dart test` are the entrypoints; `dart format --output=none
|
|
16
|
+
--set-exit-if-changed .` is the formatting gate. A Flutter project runs the same
|
|
17
|
+
commands through `flutter`, and the two must not be mixed in one change.
|
|
18
|
+
- Dependencies are declared in `pubspec.yaml`; `pub get` resolves them into
|
|
19
|
+
`pubspec.lock`, which is committed for an application. A package that imports a
|
|
20
|
+
library the manifest does not list resolves only by accident.
|
|
21
|
+
|
|
22
|
+
## Style
|
|
23
|
+
|
|
24
|
+
- `package:lints` (or the repo's stricter set) is the lint source; a rule the repo
|
|
25
|
+
disabled is a decision, and re-enabling it is a change, not a fix.
|
|
26
|
+
- No `dynamic` where a real type exists. A `dynamic` that is guessed rather than
|
|
27
|
+
checked is a hole the analyzer was told to stop looking through.
|
|
28
|
+
- `late` and `?` are decisions: a field that is `late` because the author did not
|
|
29
|
+
know when it is set is a defect, not a convenience.
|
|
30
|
+
- Prefer a small `sealed` class hierarchy over booleans; a pair of `bool` fields is
|
|
31
|
+
four states, one of which is always wrong.
|
|
32
|
+
|
|
33
|
+
## Testing
|
|
34
|
+
|
|
35
|
+
- `dart test` is the gate; a library with no tests is allowed to have none, but a
|
|
36
|
+
change that touches one must not break its neighbours.
|
|
37
|
+
- Assert on values through the public API; a test that reaches into a private field
|
|
38
|
+
is coupled to an implementation it should not own.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
simt-id: deno
|
|
3
|
+
simt-title: Deno
|
|
4
|
+
simt-applies: [has-deno-config]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Stack profile: Deno
|
|
8
|
+
|
|
9
|
+
Applies to: `has-deno-config` — a `deno.json` or `deno.jsonc`.
|
|
10
|
+
|
|
11
|
+
## Build and gates
|
|
12
|
+
|
|
13
|
+
- `deno test`, `deno check .` are the entrypoints; `deno fmt --check` and
|
|
14
|
+
`deno lint` are the formatting and lint gates. A repo that vendors dependencies
|
|
15
|
+
runs them with `--vendor`; one that does not must not add the flag.
|
|
16
|
+
- The lockfile (`deno.lock`) is committed where it exists; a build that ignores it
|
|
17
|
+
is not reproducible. A lockfile the repo chose to delete is a decision, and
|
|
18
|
+
re-adding it is a change, not a fix.
|
|
19
|
+
- Import from `npm:` specifiers where the repo does; do not introduce a second
|
|
20
|
+
registry convention into a repo that has one.
|
|
21
|
+
|
|
22
|
+
## Style
|
|
23
|
+
|
|
24
|
+
- Types are checked with `deno check .`; a file that compiles under `deno run` but
|
|
25
|
+
not under `deno check` is a defect the gate exists to catch.
|
|
26
|
+
- No `any`. An `unknown` that is narrowed at the boundary is a type; an `any` is a
|
|
27
|
+
hole the checker was told to stop looking through.
|
|
28
|
+
- Prefer a small `enum`-shaped union over booleans; a pair of `boolean` fields is
|
|
29
|
+
four states, one of which is always wrong.
|
|
30
|
+
|
|
31
|
+
## Testing
|
|
32
|
+
|
|
33
|
+
- `Deno.test` is the runner; a test that needs a fixture reads it from a path
|
|
34
|
+
relative to the test file, never from a machine-specific location.
|
|
35
|
+
- Assert on values through the public API; a test that reaches into module-private
|
|
36
|
+
state is coupled to an implementation it should not own.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
simt-id: dotnet
|
|
3
|
+
simt-title: .NET
|
|
4
|
+
simt-applies: [has-nuget]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Stack profile: .NET
|
|
8
|
+
|
|
9
|
+
Applies to: `has-nuget` — a `.csproj` or solution.
|
|
10
|
+
|
|
11
|
+
## Build and gates
|
|
12
|
+
|
|
13
|
+
- `dotnet build`, `dotnet test` are the entrypoints; `dotnet format
|
|
14
|
+
--verify-no-changes` is the formatting gate. A solution builds with `dotnet build
|
|
15
|
+
<solution>`, never a bare project path that skips the solution's configuration.
|
|
16
|
+
- Build with `-warnaserror` where the repo sets it; a warning that is not an error
|
|
17
|
+
is a defect the gate is being asked to ignore.
|
|
18
|
+
- Restore is deterministic: commit `global.json` and any NuGet.config the repo has,
|
|
19
|
+
so a build does not depend on a machine's cache.
|
|
20
|
+
|
|
21
|
+
## Style
|
|
22
|
+
|
|
23
|
+
- Prefer `required` members and `init` over a public setter that exists to satisfy
|
|
24
|
+
the constructor. A mutable property nobody should write is a hole in the type.
|
|
25
|
+
- `async`/`await` all the way: no `.Result`, no `.Wait()`. A synchronous call into
|
|
26
|
+
an async boundary is a deadlock waiting for the thread pool.
|
|
27
|
+
- Nullability is on: `#nullable enable` in new code, and a `?` that is guessed
|
|
28
|
+
rather than checked is a defect.
|
|
29
|
+
|
|
30
|
+
## Testing
|
|
31
|
+
|
|
32
|
+
- `dotnet test` runs xUnit, NUnit or MSTest — whichever the project references; do
|
|
33
|
+
not add a second framework to a repo that has one.
|
|
34
|
+
- Assert on behaviour through the public surface; a test that reaches into a private
|
|
35
|
+
field is coupled to an implementation it should not own.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
simt-id: elixir
|
|
3
|
+
simt-title: Elixir
|
|
4
|
+
simt-applies: [has-mix]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Stack profile: Elixir
|
|
8
|
+
|
|
9
|
+
Applies to: `has-mix` — a `mix.exs`.
|
|
10
|
+
|
|
11
|
+
## Build and gates
|
|
12
|
+
|
|
13
|
+
- `mix test` is the entrypoint; `mix format --check-formatted` is the formatting
|
|
14
|
+
gate. A repo that uses `mix credo` runs it with `mix credo --strict` where the
|
|
15
|
+
`.credo.exs` exists; a check the repo configured is a decision, and skipping it
|
|
16
|
+
is a change, not a fix.
|
|
17
|
+
- Dependencies are declared in `mix.exs`; `mix deps.get` resolves them into
|
|
18
|
+
`mix.lock`, which is committed. A module that imports a library the manifest does
|
|
19
|
+
not list compiles only by accident.
|
|
20
|
+
- A release is built with `mix release` where the repo defines one; do not run a
|
|
21
|
+
`mix run` against a release's expectations.
|
|
22
|
+
|
|
23
|
+
## Style
|
|
24
|
+
|
|
25
|
+
- Pattern-match on the way in, not guard-and-raise on the way out. A function that
|
|
26
|
+
takes a map and digs three levels deep is a boundary somebody refused to name;
|
|
27
|
+
match the shape at the edge and pass values in.
|
|
28
|
+
- Errors are returned, not raised, for expected failures; `raise` is for bugs, not
|
|
29
|
+
for a missing record. A caller that cannot handle the failure is a defect in the
|
|
30
|
+
caller's contract, not a reason to crash.
|
|
31
|
+
- Prefer a small `struct` with named fields over a map of strings; a `%{}` that is
|
|
32
|
+
passed through five functions is a type somebody refused to give a name.
|
|
33
|
+
|
|
34
|
+
## Testing
|
|
35
|
+
|
|
36
|
+
- `mix test` is the gate; ExUnit is the runner, and a change that touches a module
|
|
37
|
+
must not break its neighbours.
|
|
38
|
+
- Assert on behaviour through the public API; a test that reaches into a private
|
|
39
|
+
field is coupled to an implementation it should not own.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
simt-id: go
|
|
3
|
+
simt-title: Go
|
|
4
|
+
simt-applies: [has-go-mod]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Stack profile: Go
|
|
8
|
+
|
|
9
|
+
Applies to: `has-go-mod` — a module.
|
|
10
|
+
|
|
11
|
+
## Build and gates
|
|
12
|
+
|
|
13
|
+
- `go build ./...`, `go test ./...` are the entrypoints; `go vet ./...` runs with
|
|
14
|
+
them. Never rely on a file the build does not include.
|
|
15
|
+
- Format with `gofmt` (or `go fmt ./...`). A diff that is only formatting is a
|
|
16
|
+
defect: the gate fails closed on it.
|
|
17
|
+
- Depend on the module, not on a `GOPATH` layout. A package import that resolves
|
|
18
|
+
only because of a local checkout is a broken dependency.
|
|
19
|
+
|
|
20
|
+
## Style
|
|
21
|
+
|
|
22
|
+
- Errors are values, not exceptions. Wrap with `fmt.Errorf("...: %w", err)` so the
|
|
23
|
+
caller can `errors.Is`/`errors.As`; a bare string loses the chain.
|
|
24
|
+
- `context.Context` is the first parameter of anything that can be cancelled; never
|
|
25
|
+
store one in a struct.
|
|
26
|
+
- Table-driven tests: one `for range` over named cases beats ten copy-pasted
|
|
27
|
+
subtests.
|
|
28
|
+
|
|
29
|
+
## Testing
|
|
30
|
+
|
|
31
|
+
- `go test ./...` is the gate; a package with no test file is allowed to have none,
|
|
32
|
+
but a change that touches one must not break its neighbours.
|
|
33
|
+
- Benchmark with `-bench` only where the number is a decision; an unowned benchmark
|
|
34
|
+
is decoration.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
simt-id: ruby
|
|
3
|
+
simt-title: Ruby
|
|
4
|
+
simt-applies: [has-gemfile]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Stack profile: Ruby
|
|
8
|
+
|
|
9
|
+
Applies to: `has-gemfile` — a Bundled application or gem.
|
|
10
|
+
|
|
11
|
+
## Build and gates
|
|
12
|
+
|
|
13
|
+
- `bundle install` then `bundle exec rspec` are the entrypoints; run every command
|
|
14
|
+
through `bundle exec`, never a bare binary, so the Gemfile.lock is the only
|
|
15
|
+
source of truth for versions.
|
|
16
|
+
- `bundle exec rubocop` is the lint gate where the repo has a `.rubocop.yml`; a
|
|
17
|
+
cop the repo disabled is a decision, and re-enabling it is a change, not a fix.
|
|
18
|
+
- `Gemfile.lock` is committed for an application; for a library it is not, and the
|
|
19
|
+
distinction is a decision the repo made, not one to reverse silently.
|
|
20
|
+
|
|
21
|
+
## Style
|
|
22
|
+
|
|
23
|
+
- Frozen string literals (`# frozen_string_literal: true`) at the top of new files
|
|
24
|
+
where the repo already uses them; a mixed repo is a transition, not a licence to
|
|
25
|
+
pick either.
|
|
26
|
+
- Prefer `case`/`when` or a hash over a chain of `if`/`elsif` on one variable; a
|
|
27
|
+
five-branch if-chain is a lookup table somebody refused to name.
|
|
28
|
+
- `raise` with a message that names the failure and the value that caused it; a
|
|
29
|
+
bare `raise` is a defect the next reader cannot debug.
|
|
30
|
+
|
|
31
|
+
## Testing
|
|
32
|
+
|
|
33
|
+
- RSpec is the runner where the repo has one; do not add a second framework to a
|
|
34
|
+
repo that has one.
|
|
35
|
+
- Assert on behaviour through the public interface; a spec that stubs everything
|
|
36
|
+
and asserts on the stub is testing the stub, not the code.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
simt-id: rust
|
|
3
|
+
simt-title: Rust
|
|
4
|
+
simt-applies: [has-cargo-toml]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Stack profile: Rust
|
|
8
|
+
|
|
9
|
+
Applies to: `has-cargo-toml` — a Cargo workspace or crate.
|
|
10
|
+
|
|
11
|
+
## Build and gates
|
|
12
|
+
|
|
13
|
+
- `cargo build`, `cargo test` are the entrypoints; a workspace builds with
|
|
14
|
+
`cargo build --workspace`. Never invoke a toolchain the manifest does not pin.
|
|
15
|
+
- Format with `cargo fmt`; lint with `cargo clippy -- -D warnings`. A warning is a
|
|
16
|
+
defect, not a note: fix it or the gate fails.
|
|
17
|
+
- Pin the toolchain in `rust-toolchain.toml` where one is committed; a rustup
|
|
18
|
+
default on the developer's machine is not a floor anything enforces.
|
|
19
|
+
|
|
20
|
+
## Style
|
|
21
|
+
|
|
22
|
+
- No `unwrap()` or `expect()` outside tests. A panic path in library code is an
|
|
23
|
+
unhandled error; return a `Result` and let the caller decide.
|
|
24
|
+
- `#[must_use]` on fallible constructors; a `Result` that is ignored is a bug the
|
|
25
|
+
compiler should flag.
|
|
26
|
+
- Prefer a small `enum` of states over booleans; a pair of `bool` fields is four
|
|
27
|
+
states, one of which is always wrong.
|
|
28
|
+
|
|
29
|
+
## Testing
|
|
30
|
+
|
|
31
|
+
- Unit tests live in `#[cfg(test)]` next to the code; integration tests in `tests/`
|
|
32
|
+
exercise the public API only.
|
|
33
|
+
- Assert on values, not on panic messages. A test that asserts a panic string has
|
|
34
|
+
coupled itself to an implementation detail it should not own.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
simt-id: swift
|
|
3
|
+
simt-title: Swift
|
|
4
|
+
simt-applies: [has-swift-package]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Stack profile: Swift
|
|
8
|
+
|
|
9
|
+
Applies to: `has-swift-package` — a Swift package.
|
|
10
|
+
|
|
11
|
+
## Build and gates
|
|
12
|
+
|
|
13
|
+
- `swift build`, `swift test` are the entrypoints; `swift format lint -r .` is the
|
|
14
|
+
formatting gate. A platform build (Xcode) is a different question from a package
|
|
15
|
+
build — do not conflate the two in one command.
|
|
16
|
+
- Pin the toolchain where the repo does (a `Package.swift` tools version or a
|
|
17
|
+
pinned Xcode); a compiler on the developer's machine is not a floor anything
|
|
18
|
+
enforces.
|
|
19
|
+
- Dependencies are declared in `Package.swift`; a package that imports a module the
|
|
20
|
+
manifest does not list builds only by accident.
|
|
21
|
+
|
|
22
|
+
## Style
|
|
23
|
+
|
|
24
|
+
- Concurrency is checked, not assumed: `Sendable` conformance is a decision. A
|
|
25
|
+
`@unchecked Sendable` that papers over a real race is a defect the compiler was
|
|
26
|
+
told to stop reporting.
|
|
27
|
+
- `throws` for expected failures, not `fatalError`. A crash path in library code is
|
|
28
|
+
an unhandled error; return a `Result` or throw.
|
|
29
|
+
- Prefer a small `enum` of states over optionals of optionals; a triple `??` is a
|
|
30
|
+
state machine somebody refused to name.
|
|
31
|
+
|
|
32
|
+
## Testing
|
|
33
|
+
|
|
34
|
+
- `swift test` is the gate; a target with no tests is allowed to have none, but a
|
|
35
|
+
change that touches one must not break its neighbours.
|
|
36
|
+
- Assert on values through the public API; a test that reaches into an internal
|
|
37
|
+
field is coupled to an implementation it should not own.
|
package/src/argv.mjs
CHANGED
|
@@ -12,23 +12,34 @@
|
|
|
12
12
|
|
|
13
13
|
import { EXIT, fail } from './output/index.mjs';
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
// `--quiet`, `--semantic` and `--mermaid` were here and were read by nothing (review
|
|
16
|
+
// 0001 filed it; it survived every change to this file since). Each was a plan's
|
|
17
|
+
// intention rather than a feature: `--semantic` was to be an explicitly non-canonical
|
|
18
|
+
// escape hatch, `--mermaid` belonged to a `skills graph` command that was never built,
|
|
19
|
+
// and `--quiet` was to shorten `help`. The plans keep those records; the parser does not
|
|
20
|
+
// need to keep a promise no code makes.
|
|
21
|
+
//
|
|
22
|
+
// An accepted flag that changes nothing is worse than an absent one. `--nonsense` exits
|
|
23
|
+
// 2 and tells the reader the flag does not exist; `--quiet` exits 0 and tells them
|
|
24
|
+
// nothing happened, which is indistinguishable from the output being quiet on purpose.
|
|
25
|
+
// Removing them makes `test/help.test.mjs`'s "every flag is described" assertion able to
|
|
26
|
+
// mean something, which it could not while three accepted flags had no description.
|
|
27
|
+
|
|
28
|
+
export const BOOL_FLAGS = new Set([
|
|
16
29
|
'json',
|
|
17
30
|
'all',
|
|
18
31
|
'dry-run',
|
|
19
32
|
'yes',
|
|
20
33
|
'check',
|
|
21
|
-
'quiet',
|
|
22
34
|
'plan',
|
|
23
|
-
'semantic',
|
|
24
|
-
'mermaid',
|
|
25
35
|
'help',
|
|
26
36
|
'version',
|
|
27
37
|
'accept-conflicts',
|
|
38
|
+
'backup',
|
|
28
39
|
'list',
|
|
29
40
|
]);
|
|
30
41
|
|
|
31
|
-
const VALUE_FLAGS = new Set([
|
|
42
|
+
export const VALUE_FLAGS = new Set([
|
|
32
43
|
'budget',
|
|
33
44
|
'max-bytes',
|
|
34
45
|
'paths',
|
|
@@ -43,6 +54,21 @@ const VALUE_FLAGS = new Set([
|
|
|
43
54
|
'surfaces',
|
|
44
55
|
]);
|
|
45
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Flags that accumulate rather than overwrite.
|
|
59
|
+
*
|
|
60
|
+
* `--skill` was documented as "(repeatable)" and was not: `flags.skill = value` meant
|
|
61
|
+
* `--skill a --skill b` resolved to `b`, with `a` silently dropped. No error, no
|
|
62
|
+
* warning, and a brief that quietly lacked a skill the caller named — which is the one
|
|
63
|
+
* failure mode a tool whose job is to be explainable cannot have (ADR-009, "the answer
|
|
64
|
+
* is a function of what was asked").
|
|
65
|
+
*
|
|
66
|
+
* Comma separation still works, because the consumer splits on commas either way. This
|
|
67
|
+
* is a superset of the old behaviour, not a replacement: `--skill a,b` parsed before it
|
|
68
|
+
* parses now.
|
|
69
|
+
*/
|
|
70
|
+
export const REPEATABLE_FLAGS = new Set(['skill']);
|
|
71
|
+
|
|
46
72
|
/**
|
|
47
73
|
* @param {string[]} argv raw arguments, excluding node and the script path
|
|
48
74
|
* @returns {{ok: true, value: object} | {ok: false, code: number}}
|
|
@@ -96,7 +122,10 @@ export function parse(argv) {
|
|
|
96
122
|
}
|
|
97
123
|
flags[camel(raw)] = n;
|
|
98
124
|
} else {
|
|
99
|
-
|
|
125
|
+
const key = camel(raw);
|
|
126
|
+
flags[key] = REPEATABLE_FLAGS.has(raw)
|
|
127
|
+
? [...(Array.isArray(flags[key]) ? flags[key] : []), value]
|
|
128
|
+
: value;
|
|
100
129
|
}
|
|
101
130
|
continue;
|
|
102
131
|
}
|
package/src/ci/dependabot.mjs
CHANGED
|
@@ -142,7 +142,7 @@ function header(own) {
|
|
|
142
142
|
# only writer here, so an entry is added deliberately rather than as a side effect.
|
|
143
143
|
`;
|
|
144
144
|
}
|
|
145
|
-
return `# Written by simt-dev when this repo was adopted. Yours from here:
|
|
145
|
+
return `# Written by simt-dev when this repo was adopted. Yours from here: ${CLI}
|
|
146
146
|
# creates this file once and never rewrites it, so your own cadence, ignore rules
|
|
147
147
|
# and groups stay yours.
|
|
148
148
|
#
|