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.
Files changed (56) hide show
  1. package/CHANGELOG.md +173 -7
  2. package/README.md +4 -4
  3. package/docs/decisions/ADR-019-a-declared-command-is-the-runner.md +27 -0
  4. package/docs/decisions/ADR-059-the-version-a-repository-runs-is-the-one-it-pinned.md +75 -0
  5. package/docs/decisions/ADR-060-a-repository-with-nothing-to-run-is-adopted-and-says-so.md +131 -0
  6. package/docs/decisions/ADR-061-the-skill-registry-is-workspace-data.md +113 -0
  7. package/docs/decisions/ADR-062-a-fresh-adoption-seeds-the-gates-it-can-run.md +94 -0
  8. package/docs/decisions/ADR-063-the-adoption-escape-hatch-is-opt-in.md +69 -0
  9. package/docs/decisions/README.md +17 -0
  10. package/package.json +1 -2
  11. package/rules/stacks/dart.md +38 -0
  12. package/rules/stacks/deno.md +36 -0
  13. package/rules/stacks/dotnet.md +35 -0
  14. package/rules/stacks/elixir.md +39 -0
  15. package/rules/stacks/go.md +34 -0
  16. package/rules/stacks/ruby.md +36 -0
  17. package/rules/stacks/rust.md +34 -0
  18. package/rules/stacks/swift.md +37 -0
  19. package/src/argv.mjs +35 -6
  20. package/src/ci/dependabot.mjs +1 -1
  21. package/src/ci/workflow.mjs +33 -11
  22. package/src/commands/graph.mjs +13 -4
  23. package/src/commands/help.mjs +22 -4
  24. package/src/commands/hooks.mjs +173 -57
  25. package/src/commands/index.mjs +53 -45
  26. package/src/commands/init.mjs +89 -13
  27. package/src/commands/rules.mjs +54 -3
  28. package/src/commands/skills.mjs +143 -15
  29. package/src/commands/skillsinstall.mjs +3 -3
  30. package/src/commands/sync.mjs +153 -11
  31. package/src/compare/hooksstate.mjs +96 -9
  32. package/src/compare/row.mjs +34 -5
  33. package/src/decisions/index.mjs +21 -6
  34. package/src/graph/load.mjs +125 -14
  35. package/src/hooks/capability.mjs +48 -4
  36. package/src/hooks/dispatcher.mjs +50 -5
  37. package/src/hooks/gates.mjs +0 -2
  38. package/src/hooks/resolve.mjs +24 -25
  39. package/src/hooks/shim.mjs +29 -5
  40. package/src/identity.mjs +0 -11
  41. package/src/mcp/tools.mjs +2 -2
  42. package/src/output/index.mjs +8 -7
  43. package/src/package-version.mjs +74 -0
  44. package/src/projection/conflicts.mjs +56 -5
  45. package/src/projection/manifest.mjs +247 -25
  46. package/src/projection/mdc.mjs +4 -13
  47. package/src/projection/project.mjs +1 -1
  48. package/src/projection/rules.mjs +21 -8
  49. package/src/projection/targets.mjs +53 -7
  50. package/src/resolve/eval.mjs +64 -0
  51. package/src/resolve/fingerprint.mjs +1 -0
  52. package/src/resolve/near.mjs +26 -7
  53. package/src/resolve/resolve.mjs +1 -1
  54. package/src/resolve/source.mjs +1 -1
  55. package/src/workspace.mjs +0 -9
  56. 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.
@@ -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.0",
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
- const BOOL_FLAGS = new Set([
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
- flags[camel(raw)] = value;
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
  }
@@ -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: simt-dev
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
  #