simt-dev 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +209 -7
- package/README.md +32 -5
- package/docs/decisions/ADR-001-push-requires-approval.md +5 -5
- package/docs/decisions/ADR-002-refactor-blast-radius.md +1 -1
- package/docs/decisions/ADR-003-test-breadth.md +4 -4
- package/docs/decisions/ADR-004-hook-owns-format.md +5 -5
- package/docs/decisions/ADR-005-file-length.md +5 -5
- package/docs/decisions/ADR-006-coverage-bar.md +3 -3
- package/docs/decisions/ADR-007-section-skeleton.md +5 -5
- package/docs/decisions/ADR-008-verdict-vocabulary.md +1 -1
- package/docs/decisions/ADR-009-skill-layout.md +1 -1
- package/docs/decisions/ADR-010-runner-invocation.md +1 -1
- package/docs/decisions/ADR-012-drop-windows-support.md +2 -2
- package/docs/decisions/ADR-015-a-source-is-content-not-a-location.md +2 -2
- package/docs/decisions/ADR-016-a-divergent-rule-is-retired-not-merged.md +13 -13
- package/docs/decisions/ADR-017-a-declared-gate-must-be-runnable.md +2 -2
- package/docs/decisions/ADR-018-an-unset-hooks-path-is-not-an-absent-hook.md +9 -9
- package/docs/decisions/ADR-019-a-declared-command-is-the-runner.md +34 -7
- package/docs/decisions/ADR-023-the-gate-ships-as-a-file-the-repo-owns.md +2 -2
- package/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +5 -5
- package/docs/decisions/ADR-028-a-base-file-is-a-floor-so-the-disagreement-it-cannot-repair-becomes-a-check.md +2 -2
- package/docs/decisions/ADR-029-the-pins-a-repository-declares-are-its-own.md +1 -1
- package/docs/decisions/ADR-030-what-a-plan-does-not-ship-is-a-decision-with-a-record.md +2 -2
- package/docs/decisions/ADR-031-the-suite-stages-the-environment-it-needs.md +3 -3
- package/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +4 -4
- package/docs/decisions/ADR-041-the-suite-installs-the-package-in-order-to-test-it.md +2 -2
- package/docs/decisions/ADR-045-a-dashboard-that-counts-a-defect.md +3 -3
- package/docs/decisions/ADR-046-one-rule-one-copy.md +5 -5
- package/docs/decisions/ADR-047-a-profile-selects-on-what-a-repository-runs-on.md +4 -4
- package/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +5 -5
- package/docs/decisions/ADR-057-an-identity-is-per-developer-and-a-repository-does-not-own-it.md +115 -0
- package/docs/decisions/ADR-058-one-tool-three-channels-and-the-order-is-forced.md +148 -0
- package/docs/decisions/ADR-059-the-version-a-repository-runs-is-the-one-it-pinned.md +75 -0
- package/docs/decisions/ADR-060-a-repository-with-nothing-to-run-is-adopted-and-says-so.md +131 -0
- package/docs/decisions/ADR-061-the-skill-registry-is-workspace-data.md +113 -0
- package/docs/decisions/ADR-062-a-fresh-adoption-seeds-the-gates-it-can-run.md +94 -0
- package/docs/decisions/ADR-063-the-adoption-escape-hatch-is-opt-in.md +69 -0
- package/docs/decisions/README.md +20 -1
- package/package.json +1 -2
- package/rules/stacks/dart.md +38 -0
- package/rules/stacks/deno.md +36 -0
- package/rules/stacks/dotnet.md +35 -0
- package/rules/stacks/elixir.md +39 -0
- package/rules/stacks/go.md +34 -0
- package/rules/stacks/ruby.md +36 -0
- package/rules/stacks/rust.md +34 -0
- package/rules/stacks/swift.md +37 -0
- package/src/argv.mjs +35 -6
- package/src/ci/dependabot.mjs +1 -1
- package/src/ci/workflow.mjs +33 -11
- package/src/commands/graph.mjs +13 -4
- package/src/commands/help.mjs +22 -4
- package/src/commands/hooks.mjs +183 -67
- package/src/commands/index.mjs +53 -45
- package/src/commands/init.mjs +156 -14
- package/src/commands/rules.mjs +54 -3
- package/src/commands/skills.mjs +143 -15
- package/src/commands/skillsinstall.mjs +4 -4
- package/src/commands/sync.mjs +153 -11
- package/src/compare/discover.mjs +1 -1
- package/src/compare/hooksstate.mjs +96 -9
- package/src/compare/row.mjs +36 -7
- package/src/decisions/index.mjs +51 -12
- package/src/graph/load.mjs +126 -15
- package/src/hooks/capability.mjs +49 -5
- package/src/hooks/dispatcher.mjs +51 -6
- package/src/hooks/gates.mjs +1 -3
- package/src/hooks/resolve.mjs +24 -25
- package/src/hooks/shim.mjs +29 -5
- package/src/hooks/where.mjs +1 -1
- package/src/identity.mjs +207 -0
- package/src/mcp/tools.mjs +2 -2
- package/src/output/index.mjs +8 -7
- package/src/package-version.mjs +74 -0
- package/src/projection/baselines.mjs +1 -1
- package/src/projection/conflicts.mjs +57 -6
- package/src/projection/manifest.mjs +248 -26
- package/src/projection/markers.mjs +3 -3
- package/src/projection/mdc.mjs +6 -15
- package/src/projection/project.mjs +1 -1
- package/src/projection/rules.mjs +21 -8
- package/src/projection/surfaces.mjs +2 -2
- package/src/projection/targets.mjs +53 -7
- package/src/resolve/eval.mjs +64 -0
- package/src/resolve/fingerprint.mjs +4 -3
- package/src/resolve/near.mjs +26 -7
- package/src/resolve/resolve.mjs +1 -1
- package/src/resolve/source.mjs +2 -2
- package/src/tsconfig.mjs +1 -1
- package/src/workspace.mjs +0 -9
- package/graph/skills.graph.json +0 -1622
package/docs/decisions/README.md
CHANGED
|
@@ -64,6 +64,10 @@ record's status line — never by editing the ruling in place.
|
|
|
64
64
|
| [054](ADR-054-the-digest-covers-every-byte-that-leaves-the-package.md) | The digest covers every byte that leaves the package, and a cause is read off the body | accepted · amends ADR-032 |
|
|
65
65
|
| [055](ADR-055-a-status-line-travels-so-it-may-not-name-a-location.md) | A status line travels, so it may not name a location; prose is the record's own argument | accepted |
|
|
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
|
+
| [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
|
+
| [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 |
|
|
67
71
|
|
|
68
72
|
ADR-001 to ADR-010 resolve the contradictions found across the 16 repos in the 2026-10-05
|
|
69
73
|
audit; each record names the repos whose text it replaces. ADR-011 and ADR-012 are the
|
|
@@ -400,6 +404,21 @@ docstring that used to claim the whole output "never contains a path" is withdra
|
|
|
400
404
|
asserted a property the function never had, which is the same defect as a citation that
|
|
401
405
|
cannot be resolved — a sentence that reads like evidence and carries none.
|
|
402
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
|
+
|
|
403
422
|
## The rule these records enforce
|
|
404
423
|
|
|
405
424
|
Every shared rule has exactly **one** canonical home. Every other location is either a
|
|
@@ -418,7 +437,7 @@ ADR-018's guard could not see the default hooks directory because it looked only
|
|
|
418
437
|
key, and `hooks status` carried its own copy of the read logic the shared module already had.
|
|
419
438
|
|
|
420
439
|
A fourth joins them. ADR-019 is the same shape one layer over: `simt init` dropped all
|
|
421
|
-
five gates on `
|
|
440
|
+
five gates on `repo-05` and told the reader to add a script to a `package.json` the
|
|
422
441
|
repository will never have. The facts were right, the remedy was not followable, and once
|
|
423
442
|
the repository declared what it does have, the dispatcher, `check` and `hooks status`
|
|
424
443
|
gave three different answers about one manifest. Every one of these shipped green because
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "simt-dev",
|
|
3
|
-
"version": "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
|
-
|
|
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
|
#
|
package/src/ci/workflow.mjs
CHANGED
|
@@ -50,6 +50,7 @@ import { PACKAGE_ROOT } from '../workspace.mjs';
|
|
|
50
50
|
import { PACKAGE_NAME, WORKFLOW_FILE_NAME } from '../name.mjs';
|
|
51
51
|
import { renderLocator } from '../locate.mjs';
|
|
52
52
|
import { CLI } from '../name.mjs';
|
|
53
|
+
import { packageVersion } from '../package-version.mjs';
|
|
53
54
|
|
|
54
55
|
/** Directory whose presence means this repo runs GitHub Actions. */
|
|
55
56
|
export const WORKFLOWS_DIR = '.github/workflows';
|
|
@@ -169,12 +170,16 @@ function skeleton(name, steps, { matrix = null } = {}) {
|
|
|
169
170
|
const job = matrix?.node ? '\${{ matrix.os }} node \${{ matrix.node }}' : name;
|
|
170
171
|
return `name: ${name}
|
|
171
172
|
|
|
173
|
+
# Pull requests only, and that is a decision rather than a default.
|
|
174
|
+
#
|
|
175
|
+
# A gate that runs on every push re-runs itself for every commit that pushes a
|
|
176
|
+
# branch forward, including the ones nobody asked it about, and its verdict has to
|
|
177
|
+
# be cleared before the next one lands. \`pull_request\` asks one question — may this
|
|
178
|
+
# merge — and answers it once. \`main\` here is the branch protection list, not a
|
|
179
|
+
# trigger: the gate that has to pass is the one the pull request runs, and the one
|
|
180
|
+
# on \`main\` is whatever CI the repository already has there.
|
|
172
181
|
on:
|
|
173
182
|
pull_request:
|
|
174
|
-
push:
|
|
175
|
-
branches:
|
|
176
|
-
- main
|
|
177
|
-
- master
|
|
178
183
|
|
|
179
184
|
# Read-only is enough to check a repo, and a gate job does not need to write to it.
|
|
180
185
|
permissions:
|
|
@@ -235,7 +240,7 @@ function indent(text, spaces) {
|
|
|
235
240
|
* @param {{own?: boolean}} [options]
|
|
236
241
|
* @returns {string} YAML, LF-terminated
|
|
237
242
|
*/
|
|
238
|
-
export function renderWorkflow({ own = false } = {}) {
|
|
243
|
+
export function renderWorkflow({ own = false, npm = true, version } = {}) {
|
|
239
244
|
if (own) {
|
|
240
245
|
const floor = nodeFloor();
|
|
241
246
|
return `${header(true)}${skeleton(
|
|
@@ -267,17 +272,34 @@ export function renderWorkflow({ own = false } = {}) {
|
|
|
267
272
|
)}\n`;
|
|
268
273
|
}
|
|
269
274
|
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
275
|
+
const v = version ?? packageVersion();
|
|
276
|
+
// The install step is the whole of S35's consumer CI: a repository with npm pins
|
|
277
|
+
// simt-dev as a devDependency and `npm ci` restores it; one without (Gradle,
|
|
278
|
+
// Connect IQ) has no node_modules to pin in, so CI installs the same pinned global
|
|
279
|
+
// binary the machine remedy names. The two must agree, or the gate passes in one
|
|
280
|
+
// place and cannot start in the other.
|
|
281
|
+
const install = npm
|
|
282
|
+
? [
|
|
283
|
+
' - name: install dependencies',
|
|
284
|
+
' # `npm ci` needs a lockfile; a repo without one is still gated, not skipped.',
|
|
285
|
+
' run: npm ci || npm install',
|
|
286
|
+
]
|
|
287
|
+
: [
|
|
288
|
+
' - name: install the pinned gate binary',
|
|
289
|
+
' # No node_modules to pin in, so a pinned global install: the same bytes',
|
|
290
|
+
' # the machine remedy names, so the gate cannot pass locally and fail here.',
|
|
291
|
+
` run: npm i -g ${PACKAGE_NAME}@${v}`,
|
|
292
|
+
];
|
|
293
|
+
const remedy = npm
|
|
294
|
+
? `REMEDY run: npm i -D ${PACKAGE_NAME} (the gate runs from the pinned dependency)`
|
|
295
|
+
: `REMEDY run: npm i -g ${PACKAGE_NAME}@${v} (the gate runs from a pinned global install)`;
|
|
296
|
+
const locator = renderLocator('check --root "$ROOT"', { remedy });
|
|
273
297
|
return `${header(false)}${skeleton(
|
|
274
298
|
`${CLI} check`,
|
|
275
299
|
[
|
|
276
300
|
uses('checkout'),
|
|
277
301
|
setupNode(nodeFloor()),
|
|
278
|
-
|
|
279
|
-
' # \`npm ci\` needs a lockfile; a repo without one is still gated, not skipped.',
|
|
280
|
-
' run: npm ci || npm install',
|
|
302
|
+
...install,
|
|
281
303
|
` - name: ${CLI} check`,
|
|
282
304
|
' run: |',
|
|
283
305
|
indent(`set -eu\nROOT="\${GITHUB_WORKSPACE}"\n${locator}`, 10),
|
package/src/commands/graph.mjs
CHANGED
|
@@ -7,11 +7,17 @@
|
|
|
7
7
|
// the offending node, the file it disagrees with, and both numbers.
|
|
8
8
|
|
|
9
9
|
import { EXIT, emit, fail, isJson } from '../output/index.mjs';
|
|
10
|
-
import { loadRegistry
|
|
10
|
+
import { loadRegistry } from '../graph/load.mjs';
|
|
11
|
+
import { CLI } from '../name.mjs';
|
|
11
12
|
|
|
12
13
|
export async function lintCommand(positional, flags) {
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
// The resolved path comes back from the loader, because when no positional is given it
|
|
15
|
+
// falls back to the member that holds the registry in this workspace (ADR-061) - and the
|
|
16
|
+
// reader should see where it actually looked, not a constant that no longer exists.
|
|
17
|
+
const { ok, registry, errors, warnings, verified, workspace, path } = await loadRegistry(
|
|
18
|
+
positional[0] ?? null,
|
|
19
|
+
flags,
|
|
20
|
+
);
|
|
15
21
|
|
|
16
22
|
if (isJson(flags)) {
|
|
17
23
|
emit(
|
|
@@ -40,7 +46,10 @@ export async function lintCommand(positional, flags) {
|
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
if (!registry) {
|
|
43
|
-
|
|
49
|
+
// The default remedy for this exit code is `run: simt-dev graph lint`, which from
|
|
50
|
+
// inside `graph lint` is circular: the reader just ran it and got this. Name the
|
|
51
|
+
// argument the command takes, because an explicit path is the way out (ADR-061).
|
|
52
|
+
return fail(errors[0] ?? `${path}: unreadable`, EXIT.REGISTRY, `run: ${CLI} graph lint <path-to-registry>`);
|
|
44
53
|
}
|
|
45
54
|
|
|
46
55
|
const lines = [
|
package/src/commands/help.mjs
CHANGED
|
@@ -4,7 +4,8 @@ import { CLI } from '../name.mjs';
|
|
|
4
4
|
|
|
5
5
|
const COMMANDS = `${CLI} init [path] adopt or scaffold a repo into the ecosystem
|
|
6
6
|
(dry-run unless --yes; conflicts block without
|
|
7
|
-
--accept-conflicts;
|
|
7
|
+
--accept-conflicts; in place unless --backup;
|
|
8
|
+
never commits or pushes)
|
|
8
9
|
${CLI} doctor classify a repo and check config health
|
|
9
10
|
${CLI} check the gate: projection drift, manifest validity, policy
|
|
10
11
|
${CLI} sync [--yes] regenerate the projection (idempotent)
|
|
@@ -19,6 +20,10 @@ ${CLI} skills brief --task tier-1 pre-assembled brief <- the common case
|
|
|
19
20
|
${CLI} skills show <name> tier-2 body (respects --max-bytes)
|
|
20
21
|
${CLI} skills why <name> the signal chain that selected it
|
|
21
22
|
${CLI} skills find <query> search across capability and keyword
|
|
23
|
+
${CLI} skills eval [corpus] measure the resolver against a gold corpus
|
|
24
|
+
(reports precision and recall; exits non-zero on
|
|
25
|
+
regression; without an argument, the corpus in this
|
|
26
|
+
checkout)
|
|
22
27
|
${CLI} skills install <name> pin a skill into the repo as a local override
|
|
23
28
|
(dry-run unless --yes; a multi-variant skill needs
|
|
24
29
|
--variant or --from rather than a guess)
|
|
@@ -26,9 +31,15 @@ ${CLI} skills install <name> pin a skill into the repo as a local override
|
|
|
26
31
|
${CLI} rules list the rule catalogue (--root marks what applies to a repo)
|
|
27
32
|
${CLI} rules show <id> one rule body
|
|
28
33
|
${CLI} rules why <id|ADR-NNN> the decision that settled it, and whether it is still in force
|
|
34
|
+
(--all prints every ruling, not the first two)
|
|
29
35
|
${CLI} hooks install [--yes] install the shared dispatcher; report what it would run
|
|
30
36
|
(dry-run unless --yes; refuses to take over routing git
|
|
31
37
|
already uses, and --yes is that consent)
|
|
38
|
+
${CLI} hooks status what each git event routes to, and whether it is wired
|
|
39
|
+
${CLI} hooks run <event> run one event's gates now, as git would
|
|
40
|
+
(pre-commit, pre-push, post-merge, ...)
|
|
41
|
+
${CLI} graph lint check the skill registry against the skills on disk
|
|
42
|
+
(byte counts, dangling edges, unresolvable variants)
|
|
32
43
|
${CLI} ci simt-dev's own .github files: create or refresh them
|
|
33
44
|
(dry-run unless --yes; refreshes the templates' own copies
|
|
34
45
|
and no repository else's)
|
|
@@ -55,9 +66,16 @@ const FLAGS = `--json structured output
|
|
|
55
66
|
file, then the sibling heuristic if it verifies)
|
|
56
67
|
--dry-run report what would change, change nothing
|
|
57
68
|
--yes assume yes; required in automation, never prompts
|
|
58
|
-
--
|
|
69
|
+
--check report drift and write nothing (sync, ci)
|
|
70
|
+
--all ignore a cap: every ruling of a record, or the skills budget
|
|
59
71
|
--variant <id> which body of a multi-variant skill to use
|
|
60
|
-
--from <repo/path> install from a declared source by name
|
|
72
|
+
--from <repo/path> install from a declared source by name
|
|
73
|
+
--list print the MCP tool surface without reading stdin (mcp)
|
|
74
|
+
--accept-conflicts init: adopt rules that collide with hand-written ones
|
|
75
|
+
--backup init: tag the pre-adoption commit and branch onto
|
|
76
|
+
simt-dev/adopt before writing (the undo path)
|
|
77
|
+
--help this text
|
|
78
|
+
--version the version, and the SHA it was built from`;
|
|
61
79
|
|
|
62
80
|
const PRINCIPLES = `design rules:
|
|
63
81
|
deterministic same repo + same task = same answer, forever
|
|
@@ -78,7 +96,7 @@ export async function help(argv) {
|
|
|
78
96
|
[
|
|
79
97
|
'simt-dev - shared agent rules, skills and gates for a repo ecosystem',
|
|
80
98
|
'',
|
|
81
|
-
|
|
99
|
+
`usage: ${CLI} <command> [args] [flags]`,
|
|
82
100
|
'',
|
|
83
101
|
'commands:',
|
|
84
102
|
COMMANDS,
|