simt-dev 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python/simt_dev/__init__.py +32 -0
- python/simt_dev/cli.py +225 -0
- simt_dev/__init__.py +32 -0
- simt_dev/cli.py +225 -0
- simt_dev/vendored/CHANGELOG.md +166 -0
- simt_dev/vendored/bin/simt-dev.mjs +28 -0
- simt_dev/vendored/docs/decisions/ADR-001-push-requires-approval.md +42 -0
- simt_dev/vendored/docs/decisions/ADR-002-refactor-blast-radius.md +45 -0
- simt_dev/vendored/docs/decisions/ADR-003-test-breadth.md +46 -0
- simt_dev/vendored/docs/decisions/ADR-004-hook-owns-format.md +45 -0
- simt_dev/vendored/docs/decisions/ADR-005-file-length.md +42 -0
- simt_dev/vendored/docs/decisions/ADR-006-coverage-bar.md +59 -0
- simt_dev/vendored/docs/decisions/ADR-007-section-skeleton.md +54 -0
- simt_dev/vendored/docs/decisions/ADR-008-verdict-vocabulary.md +59 -0
- simt_dev/vendored/docs/decisions/ADR-009-skill-layout.md +48 -0
- simt_dev/vendored/docs/decisions/ADR-010-runner-invocation.md +56 -0
- simt_dev/vendored/docs/decisions/ADR-011-windows-support.md +52 -0
- simt_dev/vendored/docs/decisions/ADR-012-drop-windows-support.md +59 -0
- simt_dev/vendored/docs/decisions/ADR-013-genericity-by-facts-and-local-profiles.md +114 -0
- simt_dev/vendored/docs/decisions/ADR-014-workspace-is-configuration.md +211 -0
- simt_dev/vendored/docs/decisions/ADR-015-a-source-is-content-not-a-location.md +117 -0
- simt_dev/vendored/docs/decisions/ADR-016-a-divergent-rule-is-retired-not-merged.md +180 -0
- simt_dev/vendored/docs/decisions/ADR-017-a-declared-gate-must-be-runnable.md +89 -0
- simt_dev/vendored/docs/decisions/ADR-018-an-unset-hooks-path-is-not-an-absent-hook.md +183 -0
- simt_dev/vendored/docs/decisions/ADR-019-a-declared-command-is-the-runner.md +110 -0
- simt_dev/vendored/docs/decisions/ADR-020-a-rule-is-written-per-surface-and-pointed-not-copied.md +100 -0
- simt_dev/vendored/docs/decisions/ADR-021-one-surface-per-repo.md +94 -0
- simt_dev/vendored/docs/decisions/ADR-022-a-key-simt-dev-reads-is-prefixed.md +98 -0
- simt_dev/vendored/docs/decisions/ADR-023-the-gate-ships-as-a-file-the-repo-owns.md +106 -0
- simt_dev/vendored/docs/decisions/ADR-024-simt-dev-overwrites-its-own-gate-file.md +111 -0
- simt_dev/vendored/docs/decisions/ADR-025-a-baseline-is-a-region-in-a-file-the-repo-owns.md +153 -0
- simt_dev/vendored/docs/decisions/ADR-026-a-config-without-a-region-is-a-file-the-repo-owns.md +117 -0
- simt_dev/vendored/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +130 -0
- simt_dev/vendored/docs/decisions/ADR-028-a-base-file-is-a-floor-so-the-disagreement-it-cannot-repair-becomes-a-check.md +131 -0
- simt_dev/vendored/docs/decisions/ADR-029-the-pins-a-repository-declares-are-its-own.md +120 -0
- simt_dev/vendored/docs/decisions/ADR-030-what-a-plan-does-not-ship-is-a-decision-with-a-record.md +114 -0
- simt_dev/vendored/docs/decisions/ADR-031-the-suite-stages-the-environment-it-needs.md +129 -0
- simt_dev/vendored/docs/decisions/ADR-032-provenance-is-a-content-digest-not-a-commit.md +141 -0
- simt_dev/vendored/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +82 -0
- simt_dev/vendored/docs/decisions/ADR-034-a-staged-tool-refuses-a-partially-staged-file.md +91 -0
- simt_dev/vendored/docs/decisions/ADR-035-one-write-decision-and-one-json-document.md +119 -0
- simt_dev/vendored/docs/decisions/ADR-036-a-writer-that-writes-says-so-and-the-instructions-naming-it-move-with-it.md +103 -0
- simt_dev/vendored/docs/decisions/ADR-037-unchanged-means-writing-would-not-change-a-byte.md +100 -0
- simt_dev/vendored/docs/decisions/ADR-038-the-gate-filter-belongs-to-every-manifest-writer.md +95 -0
- simt_dev/vendored/docs/decisions/ADR-039-a-ci-step-runs-a-form-that-can-fail.md +101 -0
- simt_dev/vendored/docs/decisions/ADR-040-the-last-writer-that-wrote-by-default-was-not-the-one-named.md +158 -0
- simt_dev/vendored/docs/decisions/ADR-041-the-suite-installs-the-package-in-order-to-test-it.md +92 -0
- simt_dev/vendored/docs/decisions/ADR-042-a-declared-glob-must-match-a-path-built-from-it.md +94 -0
- simt_dev/vendored/docs/decisions/ADR-043-a-keyword-is-a-phrase-and-two-letters-is-a-word.md +90 -0
- simt_dev/vendored/docs/decisions/ADR-044-compatibility-is-not-evidence.md +116 -0
- simt_dev/vendored/docs/decisions/ADR-045-a-dashboard-that-counts-a-defect.md +119 -0
- simt_dev/vendored/docs/decisions/ADR-046-one-rule-one-copy.md +140 -0
- simt_dev/vendored/docs/decisions/ADR-047-a-profile-selects-on-what-a-repository-runs-on.md +141 -0
- simt_dev/vendored/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +150 -0
- simt_dev/vendored/docs/decisions/ADR-049-a-ruling-that-a-later-record-moved-says-so-where-it-is-printed.md +153 -0
- simt_dev/vendored/docs/decisions/ADR-050-a-byte-count-is-not-a-region-and-updated-is-not-a-change-of-rules.md +102 -0
- simt_dev/vendored/docs/decisions/ADR-051-a-pointer-is-the-readers-only-route-so-it-is-not-a-convenience.md +134 -0
- simt_dev/vendored/docs/decisions/ADR-052-a-dependency-is-counted-against-the-budget-and-there-is-one-budget.md +101 -0
- simt_dev/vendored/docs/decisions/ADR-053-the-node-floor-is-22-20-0.md +49 -0
- simt_dev/vendored/docs/decisions/ADR-054-the-digest-covers-every-byte-that-leaves-the-package.md +100 -0
- simt_dev/vendored/docs/decisions/ADR-055-a-status-line-travels-so-it-may-not-name-a-location.md +98 -0
- simt_dev/vendored/docs/decisions/ADR-056-three-names-one-of-which-is-cheap-to-move.md +160 -0
- simt_dev/vendored/docs/decisions/ADR-057-an-identity-is-per-developer-and-a-repository-does-not-own-it.md +115 -0
- simt_dev/vendored/docs/decisions/ADR-058-one-tool-three-channels-and-the-order-is-forced.md +148 -0
- simt_dev/vendored/docs/decisions/README.md +530 -0
- simt_dev/vendored/graph/skills.graph.json +1622 -0
- simt_dev/vendored/package.json +43 -0
- simt_dev/vendored/rules/core.md +76 -0
- simt_dev/vendored/rules/mdc/intake-questions.mdc +50 -0
- simt_dev/vendored/rules/mdc/preview-postflight.mdc +61 -0
- simt_dev/vendored/rules/mdc/slice-commit.mdc +52 -0
- simt_dev/vendored/rules/mdc/test-gate.mdc +54 -0
- simt_dev/vendored/rules/stacks/android.md +63 -0
- simt_dev/vendored/rules/stacks/cloudflare-workers.md +65 -0
- simt_dev/vendored/rules/stacks/connectiq.md +62 -0
- simt_dev/vendored/rules/stacks/container.md +31 -0
- simt_dev/vendored/rules/stacks/jvm.md +54 -0
- simt_dev/vendored/rules/stacks/node.md +45 -0
- simt_dev/vendored/rules/stacks/python.md +48 -0
- simt_dev/vendored/rules/stacks/react-native.md +58 -0
- simt_dev/vendored/rules/stacks/react-web.md +61 -0
- simt_dev/vendored/rules/stacks/typescript.md +21 -0
- simt_dev/vendored/rules/stacks/uv.md +32 -0
- simt_dev/vendored/src/argv.mjs +116 -0
- simt_dev/vendored/src/ci/dependabot.mjs +268 -0
- simt_dev/vendored/src/ci/workflow.mjs +360 -0
- simt_dev/vendored/src/commands/ci.mjs +285 -0
- simt_dev/vendored/src/commands/compare.mjs +195 -0
- simt_dev/vendored/src/commands/graph.mjs +73 -0
- simt_dev/vendored/src/commands/help.mjs +108 -0
- simt_dev/vendored/src/commands/hooks.mjs +471 -0
- simt_dev/vendored/src/commands/index.mjs +241 -0
- simt_dev/vendored/src/commands/init.mjs +632 -0
- simt_dev/vendored/src/commands/mcp.mjs +63 -0
- simt_dev/vendored/src/commands/rules.mjs +374 -0
- simt_dev/vendored/src/commands/skills.mjs +647 -0
- simt_dev/vendored/src/commands/skillsinstall.mjs +515 -0
- simt_dev/vendored/src/commands/sync.mjs +331 -0
- simt_dev/vendored/src/commands/upgrade.mjs +202 -0
- simt_dev/vendored/src/compare/discover.mjs +212 -0
- simt_dev/vendored/src/compare/hooksstate.mjs +159 -0
- simt_dev/vendored/src/compare/row.mjs +299 -0
- simt_dev/vendored/src/decisions/index.mjs +536 -0
- simt_dev/vendored/src/graph/load.mjs +541 -0
- simt_dev/vendored/src/hooks/capability.mjs +129 -0
- simt_dev/vendored/src/hooks/dispatcher.mjs +268 -0
- simt_dev/vendored/src/hooks/gates.mjs +144 -0
- simt_dev/vendored/src/hooks/resolve.mjs +194 -0
- simt_dev/vendored/src/hooks/shim.mjs +140 -0
- simt_dev/vendored/src/hooks/where.mjs +118 -0
- simt_dev/vendored/src/identity.mjs +218 -0
- simt_dev/vendored/src/locate.mjs +75 -0
- simt_dev/vendored/src/mcp/server.mjs +167 -0
- simt_dev/vendored/src/mcp/tools.mjs +410 -0
- simt_dev/vendored/src/name.mjs +95 -0
- simt_dev/vendored/src/output/index.mjs +96 -0
- simt_dev/vendored/src/projection/baselines.mjs +209 -0
- simt_dev/vendored/src/projection/conflicts.mjs +205 -0
- simt_dev/vendored/src/projection/manifest.mjs +478 -0
- simt_dev/vendored/src/projection/markers.mjs +349 -0
- simt_dev/vendored/src/projection/mdc.mjs +327 -0
- simt_dev/vendored/src/projection/project.mjs +504 -0
- simt_dev/vendored/src/projection/rules.mjs +640 -0
- simt_dev/vendored/src/projection/surfaces.mjs +398 -0
- simt_dev/vendored/src/projection/targets.mjs +221 -0
- simt_dev/vendored/src/resolve/fingerprint.mjs +553 -0
- simt_dev/vendored/src/resolve/near.mjs +61 -0
- simt_dev/vendored/src/resolve/resolve.mjs +457 -0
- simt_dev/vendored/src/resolve/source.mjs +105 -0
- simt_dev/vendored/src/toolchain.mjs +202 -0
- simt_dev/vendored/src/tsconfig.mjs +314 -0
- simt_dev/vendored/src/upgrade/plan.mjs +203 -0
- simt_dev/vendored/src/workspace.mjs +289 -0
- simt_dev-0.2.0.dist-info/METADATA +150 -0
- simt_dev-0.2.0.dist-info/RECORD +138 -0
- simt_dev-0.2.0.dist-info/WHEEL +4 -0
- simt_dev-0.2.0.dist-info/entry_points.txt +2 -0
- simt_dev-0.2.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
simt-id: python
|
|
3
|
+
simt-title: Python
|
|
4
|
+
simt-applies: [python]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Stack profile: Python
|
|
8
|
+
|
|
9
|
+
Applies to: `python`. The environment rules — how to invoke tools, and which lockfile
|
|
10
|
+
owns them — are in the `uv` profile, which selects on `has-uv-lock`.
|
|
11
|
+
|
|
12
|
+
## Invocation
|
|
13
|
+
|
|
14
|
+
- Run through the repository's own runner: `uv run` where a `uv.lock` is committed,
|
|
15
|
+
`poetry run` where a `poetry.lock` is, and the repository's documented wrapper
|
|
16
|
+
otherwise. See the `uv` profile for the form that must not be used.
|
|
17
|
+
- Never invoke a tool from whatever environment the shell happens to have. It will
|
|
18
|
+
usually be a different copy of the package than the one under test, and the failure
|
|
19
|
+
it produces looks like a bug in the code.
|
|
20
|
+
|
|
21
|
+
## Typing
|
|
22
|
+
|
|
23
|
+
- No `typing.Any`. Accept `unknown`-equivalent types (`object`) at genuinely untrusted
|
|
24
|
+
boundaries and narrow explicitly.
|
|
25
|
+
- `Mapping` for a read-only parameter, `dict` for a value that is mutated. Returning a
|
|
26
|
+
concrete `dict` from a function annotated `Mapping` forces a copy on every caller.
|
|
27
|
+
- Every public function annotated. An unannotated signature is a review finding.
|
|
28
|
+
|
|
29
|
+
## Tests
|
|
30
|
+
|
|
31
|
+
- `fail_under` is set per repo in `simt-dev.json` (ADR-006). Where a repo requires
|
|
32
|
+
100 %, treat it as a behavioural gate: an uncovered path is either a missing test
|
|
33
|
+
or dead code, and both are defects.
|
|
34
|
+
- No `pytest.skip` without a pre-existing written justification.
|
|
35
|
+
- Test the narrowest layer that proves the behaviour; reach for an integration test
|
|
36
|
+
only when the behaviour is genuinely cross-system.
|
|
37
|
+
|
|
38
|
+
## Diff hygiene
|
|
39
|
+
|
|
40
|
+
- `git diff --check` before handoff: no whitespace errors, no conflict markers, no
|
|
41
|
+
stray debug prints.
|
|
42
|
+
- Generated artifacts (coverage output, build directories, caches) are not committed.
|
|
43
|
+
|
|
44
|
+
## Async
|
|
45
|
+
|
|
46
|
+
- `Awaitable` in annotations, never a bare coroutine where an `Awaitable` is the contract.
|
|
47
|
+
- No blocking I/O on the event loop.
|
|
48
|
+
- No unbounded task creation: a fan-out needs a concurrency limit.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
simt-id: react-native
|
|
3
|
+
simt-title: React Native
|
|
4
|
+
# Selected on the runtime, not on the component library. React Native and Expo
|
|
5
|
+
# arrived with the tag `react` and therefore received the React *web* profile —
|
|
6
|
+
# CSS logical properties, a browser matrix, WCAG reflow at 400 % — none of which a
|
|
7
|
+
# native surface has (S28, ADR-047).
|
|
8
|
+
simt-applies: [react-native]
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Stack profile: React Native
|
|
12
|
+
|
|
13
|
+
Applies to: `react-native` — a `react-native` or `expo` dependency. A repository whose
|
|
14
|
+
UI is rendered in a browser gets the `react-web` profile instead; see that profile for
|
|
15
|
+
the rules this one does not repeat.
|
|
16
|
+
|
|
17
|
+
## What does not apply here
|
|
18
|
+
|
|
19
|
+
- **No CSS.** There is no cascade, no `media` query and no logical property. Layout is
|
|
20
|
+
Yoga; a stylesheet is a `StyleSheet` object. Do not port a web stylesheet.
|
|
21
|
+
- **No DOM and no browser matrix.** No `document`, no `window`, no engine-compatibility
|
|
22
|
+
run. The platform is native.
|
|
23
|
+
- **No URL as shared state.** Navigation state lives in the navigator; deep links are
|
|
24
|
+
the one shareable form, and they are read as parameters.
|
|
25
|
+
|
|
26
|
+
## Platform boundaries
|
|
27
|
+
|
|
28
|
+
- A native module is a dependency, not a `npm install`. Adding one changes the
|
|
29
|
+
prebuild output, and the app must be rebuilt to run — `reload` is not enough.
|
|
30
|
+
- Permissions come from the platform manifest (`app.json` / `Info.plist`), not from a
|
|
31
|
+
runtime prompt a library raises for itself. State the reason string; a bare
|
|
32
|
+
"allow" teaches nobody anything.
|
|
33
|
+
- Secrets never reach the bundle. A value in JavaScript is a value in the app store's
|
|
34
|
+
copy of the app.
|
|
35
|
+
|
|
36
|
+
## Accessibility
|
|
37
|
+
|
|
38
|
+
- WCAG 2.2 AA where the platform can express it, and the platform's own semantics where
|
|
39
|
+
it cannot: `accessibilityLabel`, `accessibilityRole` and `accessibilityHint` carry
|
|
40
|
+
what `alt` and `aria` carry on the web.
|
|
41
|
+
- Respect the OS text size, bold text and reduced-motion settings. A fixed `fontSize`
|
|
42
|
+
breaks all three.
|
|
43
|
+
- Touch targets meet the platform minimum, and the minimum is asserted rather than
|
|
44
|
+
eyeballed.
|
|
45
|
+
|
|
46
|
+
## Types
|
|
47
|
+
|
|
48
|
+
- `strict` plus the full strictness set. No `any`, no unsafe casts, no non-null
|
|
49
|
+
boundary bypasses, no floating promises, no `console.*`.
|
|
50
|
+
|
|
51
|
+
## Testing
|
|
52
|
+
|
|
53
|
+
- Unit and component tests with `jest` or the repo's declared runner; journeys that
|
|
54
|
+
cross a real boundary — a deep link, a permission, a native module — are the ones
|
|
55
|
+
that need a device or an emulator, and they say so.
|
|
56
|
+
- Assert behaviour, not implementation. Never weaken an assertion, an accessibility
|
|
57
|
+
check or a fixture to match current behaviour.
|
|
58
|
+
- No arbitrary sleeps. No `.only`. No committed `.skip`. No flaky reruns.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
simt-id: react-web
|
|
3
|
+
simt-title: React web
|
|
4
|
+
# Browser evidence only, and the evidence is the renderer. `react` alone does not
|
|
5
|
+
# distinguish a browser app from React Native or Expo — `react-dom` does — and
|
|
6
|
+
# listing the bare language tags (`typescript`, `vitest`, `node-web`, `less`) put
|
|
7
|
+
# this profile on backend and static repositories that share them (S28, ADR-047).
|
|
8
|
+
simt-applies: [react-dom, react-router, maplibre, firebase]
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Stack profile: React web
|
|
12
|
+
|
|
13
|
+
Applies to: `react-dom`, `react-router`, `maplibre`, `firebase` — a repository whose
|
|
14
|
+
UI is rendered in a browser.
|
|
15
|
+
|
|
16
|
+
## Types
|
|
17
|
+
|
|
18
|
+
- `strict` plus the full strictness set listed in the Workers profile. Generated API DTOs only;
|
|
19
|
+
never hand-write a duplicate of a type the API already produces.
|
|
20
|
+
- No `any`, no unsafe casts, no non-null boundary bypasses, no floating promises, no
|
|
21
|
+
`console.*`.
|
|
22
|
+
|
|
23
|
+
## State
|
|
24
|
+
|
|
25
|
+
- Server state lives in the query layer, keyed by the query key the API defines. Do not mirror
|
|
26
|
+
it into a second store.
|
|
27
|
+
- URL holds shareable state; component state holds transient state; nothing else.
|
|
28
|
+
- Any global client state pattern requires a recorded decision explaining why the query layer
|
|
29
|
+
and URL are insufficient.
|
|
30
|
+
|
|
31
|
+
## CSS
|
|
32
|
+
|
|
33
|
+
- Logical properties (`margin-inline-start`, `padding-block`) rather than physical ones, so RTL
|
|
34
|
+
is a consequence of the stylesheet rather than a patch.
|
|
35
|
+
- Semantic element names. A `div` with a click handler is a button.
|
|
36
|
+
- Match tokens from the design system. Raw hex, `rgb()` and `hsl()` are a review finding.
|
|
37
|
+
|
|
38
|
+
## RTL and localisation
|
|
39
|
+
|
|
40
|
+
- Arabic mirrors application chrome only. North, coordinate order, geographic pan and zoom,
|
|
41
|
+
map symbols, media and universal status meaning do not mirror.
|
|
42
|
+
- Every user-visible string exists in every supported locale in the same change. Adding a
|
|
43
|
+
locale touches tests, which assert locale parity.
|
|
44
|
+
|
|
45
|
+
## Accessibility
|
|
46
|
+
|
|
47
|
+
- WCAG 2.2 AA. Keyboard reachable, screen-reader named, 400% reflow without loss of function,
|
|
48
|
+
`prefers-reduced-motion` honoured.
|
|
49
|
+
- Run a full browser-compatibility matrix only for browser APIs, SSR or hydration, shared
|
|
50
|
+
layout, MapLibre/WebGL, media, storage, crypto, a known engine divergence, or a release gate.
|
|
51
|
+
Engine-neutral changes take focused checks.
|
|
52
|
+
|
|
53
|
+
## Testing
|
|
54
|
+
|
|
55
|
+
- `vitest` for unit and component; `playwright` for journeys that cross a real boundary.
|
|
56
|
+
- Assert behaviour, not implementation. Never weaken an assertion, an accessibility check or a
|
|
57
|
+
fixture to match current behaviour.
|
|
58
|
+
- No arbitrary sleeps. No `.only`. No committed `.skip`. No flaky reruns.
|
|
59
|
+
- Inject a sentinel token, UID, text, gender, coordinate, URL and private media value, and
|
|
60
|
+
prove its absence from unauthorised UI, storage, cache, telemetry, URL history and
|
|
61
|
+
artifacts.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
simt-id: typescript
|
|
3
|
+
simt-title: TypeScript
|
|
4
|
+
# A fallback, not a specific profile. `select` drops every fallback once any specific
|
|
5
|
+
# profile matches, and `node` is itself a fallback. A specific profile here would
|
|
6
|
+
# suppress `node` on every TypeScript repository, which is the runner rules those
|
|
7
|
+
# repositories are selected for. Two fallbacks both apply when nothing more specific
|
|
8
|
+
# matched.
|
|
9
|
+
simt-applies: [typescript]
|
|
10
|
+
simt-fallback: true
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Stack profile: TypeScript
|
|
14
|
+
|
|
15
|
+
Applies to: `typescript`, together with the `node` profile when no more specific
|
|
16
|
+
profile matches.
|
|
17
|
+
|
|
18
|
+
## Types
|
|
19
|
+
|
|
20
|
+
- No `any`, no unsafe casts, no non-null boundary bypasses, no floating promises, no
|
|
21
|
+
`console.*` in application code.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
simt-id: uv
|
|
3
|
+
simt-title: uv
|
|
4
|
+
# Selected on the lockfile, not on the language. `python` used to carry `uv run` and
|
|
5
|
+
# `uv.lock` in its body while selecting on any Python marker, so a Poetry project was
|
|
6
|
+
# told to run a tool manager it does not use (S28, ADR-047).
|
|
7
|
+
simt-applies: [has-uv-lock]
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Stack profile: uv
|
|
11
|
+
|
|
12
|
+
Applies to: `has-uv-lock` — a `uv.lock` is committed, here or in a workspace package.
|
|
13
|
+
The language rules are in the `python` profile, which every Python repository gets.
|
|
14
|
+
|
|
15
|
+
## Invocation
|
|
16
|
+
|
|
17
|
+
- `uv run -m pytest`. **Never** `uv run pytest`: the console entrypoint can miss the
|
|
18
|
+
`uv` import path and silently run against the wrong environment, importing a
|
|
19
|
+
different copy of the package than the one under test.
|
|
20
|
+
- `uv run ruff check src tests` for lint; never a bare `ruff` from a different
|
|
21
|
+
environment.
|
|
22
|
+
- The same rule in every form: `-m <module>`, never the console script name.
|
|
23
|
+
|
|
24
|
+
## The lockfile
|
|
25
|
+
|
|
26
|
+
- Do not run `uv sync` unless explicitly asked. It mutates the lockfile mid-task, and
|
|
27
|
+
the resulting diff is not yours to review.
|
|
28
|
+
- `uv add` and `uv remove` change `pyproject.toml` and the lockfile together. Never
|
|
29
|
+
hand-edit one without the other; a lockfile that disagrees with its project is a
|
|
30
|
+
build that reproduces nothing.
|
|
31
|
+
- `uv.lock` is committed. An uncommitted lockfile means the resolution a CI run
|
|
32
|
+
performs is not the one the last developer tested.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Minimal argv parser. Zero dependencies is a hard requirement (ADR: principle 5):
|
|
2
|
+
// a DX tool that needs an install step is a DX tool that fails on a fresh machine.
|
|
3
|
+
//
|
|
4
|
+
// Supported forms:
|
|
5
|
+
// --flag boolean
|
|
6
|
+
// --key value string
|
|
7
|
+
// --key=value string
|
|
8
|
+
// --no-flag sets flag false
|
|
9
|
+
// -- everything after is positional
|
|
10
|
+
// Anything unrecognised is an error, not a silent ignore: a typo'd gate name
|
|
11
|
+
// that is ignored is worse than a typo'd gate name that fails.
|
|
12
|
+
|
|
13
|
+
import { EXIT, fail } from './output/index.mjs';
|
|
14
|
+
|
|
15
|
+
const BOOL_FLAGS = new Set([
|
|
16
|
+
'json',
|
|
17
|
+
'all',
|
|
18
|
+
'dry-run',
|
|
19
|
+
'yes',
|
|
20
|
+
'check',
|
|
21
|
+
'quiet',
|
|
22
|
+
'plan',
|
|
23
|
+
'semantic',
|
|
24
|
+
'mermaid',
|
|
25
|
+
'help',
|
|
26
|
+
'version',
|
|
27
|
+
'accept-conflicts',
|
|
28
|
+
'list',
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
const VALUE_FLAGS = new Set([
|
|
32
|
+
'budget',
|
|
33
|
+
'max-bytes',
|
|
34
|
+
'paths',
|
|
35
|
+
'skill',
|
|
36
|
+
'task',
|
|
37
|
+
'workspace',
|
|
38
|
+
'root',
|
|
39
|
+
// skills install: which body of a multi-variant skill, and which source when the
|
|
40
|
+
// bytes alone do not identify one.
|
|
41
|
+
'variant',
|
|
42
|
+
'from',
|
|
43
|
+
'surfaces',
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {string[]} argv raw arguments, excluding node and the script path
|
|
48
|
+
* @returns {{ok: true, value: object} | {ok: false, code: number}}
|
|
49
|
+
*/
|
|
50
|
+
export function parse(argv) {
|
|
51
|
+
const flags = {};
|
|
52
|
+
const positional = [];
|
|
53
|
+
let passthrough = false;
|
|
54
|
+
|
|
55
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
56
|
+
const arg = argv[i];
|
|
57
|
+
|
|
58
|
+
if (passthrough || !arg.startsWith('--')) {
|
|
59
|
+
positional.push(arg);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (arg === '--') {
|
|
63
|
+
passthrough = true;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const eq = arg.indexOf('=');
|
|
68
|
+
const raw = (eq === -1 ? arg : arg.slice(0, eq)).slice(2);
|
|
69
|
+
const inlineValue = eq === -1 ? undefined : arg.slice(eq + 1);
|
|
70
|
+
|
|
71
|
+
if (raw.startsWith('no-')) {
|
|
72
|
+
const positive = raw.slice(3);
|
|
73
|
+
if (!BOOL_FLAGS.has(positive)) {
|
|
74
|
+
return { ok: false, code: usage(`unknown flag --${raw}`) };
|
|
75
|
+
}
|
|
76
|
+
flags[camel(positive)] = false;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (BOOL_FLAGS.has(raw)) {
|
|
80
|
+
if (inlineValue !== undefined) {
|
|
81
|
+
return { ok: false, code: usage(`--${raw} takes no value`) };
|
|
82
|
+
}
|
|
83
|
+
flags[camel(raw)] = true;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (VALUE_FLAGS.has(raw)) {
|
|
87
|
+
const value = inlineValue ?? argv[i + 1];
|
|
88
|
+
if (value === undefined || (inlineValue === undefined && value.startsWith('--'))) {
|
|
89
|
+
return { ok: false, code: usage(`--${raw} requires a value`) };
|
|
90
|
+
}
|
|
91
|
+
if (inlineValue === undefined) i += 1;
|
|
92
|
+
if (raw === 'budget' || raw === 'max-bytes') {
|
|
93
|
+
const n = Number(value);
|
|
94
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
95
|
+
return { ok: false, code: usage(`--${raw} must be a non-negative integer, received ${value}`) };
|
|
96
|
+
}
|
|
97
|
+
flags[camel(raw)] = n;
|
|
98
|
+
} else {
|
|
99
|
+
flags[camel(raw)] = value;
|
|
100
|
+
}
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
return { ok: false, code: usage(`unknown flag --${raw}`) };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { ok: true, value: { flags, positional } };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** `--max-bytes` -> `maxBytes`. Callers use camelCase; users type kebab-case. */
|
|
110
|
+
function camel(name) {
|
|
111
|
+
return name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function usage(message) {
|
|
115
|
+
return fail(message, EXIT.USAGE);
|
|
116
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// Dependency updates, as a file a repo owns.
|
|
2
|
+
//
|
|
3
|
+
// The gap this closes is real and dated: no repository in the estate had a
|
|
4
|
+
// Dependabot config, and plan 1 named it as still missing after the CI gate
|
|
5
|
+
// landed. A repo with no Dependabot config does not get security patches as
|
|
6
|
+
// pull requests - it finds out from an advisory.
|
|
7
|
+
//
|
|
8
|
+
// Two decisions are load-bearing, and both come from what this file IS rather
|
|
9
|
+
// than from taste.
|
|
10
|
+
//
|
|
11
|
+
// - It is a WHOLE FILE, not a region. ADR-025 ruled that a baseline is a
|
|
12
|
+
// marker-managed region, and that ruling is right for a file whose grammar
|
|
13
|
+
// has a place to put one. This file has no such place: `updates:` is a YAML
|
|
14
|
+
// list, and two lists cannot be concatenated. There is no region shape that
|
|
15
|
+
// means "these entries are mine and those are yours", so there is nothing to
|
|
16
|
+
// merge and nothing to preserve - which is not an accident to work around but
|
|
17
|
+
// the reason the ownership question has to be answered differently rather
|
|
18
|
+
// than the same.
|
|
19
|
+
//
|
|
20
|
+
// - So it inherits ADR-023's ownership rule wholesale: written once, never
|
|
21
|
+
// rewritten, their file from the first commit. A repo that wants a different
|
|
22
|
+
// cadence, a private registry or an `ignore` rule edits it, and the edit
|
|
23
|
+
// stays. simt-dev's own repository is the ADR-024 exception and is refreshed
|
|
24
|
+
// by `simt-dev ci`.
|
|
25
|
+
//
|
|
26
|
+
// The content is detection-driven, which is the ADR-013 shape: the ecosystems
|
|
27
|
+
// listed are the ones this tree actually has a manifest for. Writing an `npm`
|
|
28
|
+
// entry into a repo with no `package.json` looks configured and configures
|
|
29
|
+
// nothing - Dependabot finds no manifest and silently opens no pull request,
|
|
30
|
+
// which is the failure mode a generated file is supposed to make impossible.
|
|
31
|
+
//
|
|
32
|
+
// `github-actions` is the one unconditional entry, and it is unconditional
|
|
33
|
+
// because of what this package ships: simt-dev writes a workflow into every
|
|
34
|
+
// repo it adopts (ADR-023), so every repo that adopted it has Actions to
|
|
35
|
+
// watch. A repo with no workflow has no Actions, and is not an entry here.
|
|
36
|
+
|
|
37
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
38
|
+
import { join } from 'node:path';
|
|
39
|
+
|
|
40
|
+
import { fingerprint } from '../resolve/fingerprint.mjs';
|
|
41
|
+
import { isOwnRepo, WORKFLOWS_DIR } from './workflow.mjs';
|
|
42
|
+
import { CLI } from '../name.mjs';
|
|
43
|
+
|
|
44
|
+
/** The one file this module writes. */
|
|
45
|
+
export const DEPENDABOT_FILE = '.github/dependabot.yml';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* fact -> the `package-ecosystem` value Dependabot documents for it.
|
|
49
|
+
*
|
|
50
|
+
* The fact names are already in the fingerprint vocabulary and already carry
|
|
51
|
+
* evidence, so this table adds no new observation vocabulary: it says which
|
|
52
|
+
* Dependabot value watches which observable manifest. Facts absent from this
|
|
53
|
+
* table are not ecosystems Dependabot updates, and adding one is a new ruling
|
|
54
|
+
* about the estate rather than a code detail.
|
|
55
|
+
*
|
|
56
|
+
* Several facts map to one value on purpose. `has-pyproject` and
|
|
57
|
+
* `has-requirements` are two ways of declaring python dependencies and
|
|
58
|
+
* Dependabot reads both under `pip`; emitting two `pip` entries would be a
|
|
59
|
+
* duplicate the config rejects or silently collapses, and the reader would have
|
|
60
|
+
* no way to tell which one won.
|
|
61
|
+
*
|
|
62
|
+
* A `stack` tag is used where no fact exists and one is needed. npm is the
|
|
63
|
+
* clearest case: `node` is a stack tag in `fingerprint.mjs` and `typescript` is
|
|
64
|
+
* a dependency name, so a repo's package manager is either of those and adding
|
|
65
|
+
* `has-node-manifest` would be a third name for one observation - the exact
|
|
66
|
+
* duplication that table's own header rules out. `gradle` is the same shape:
|
|
67
|
+
* the stack tag already names the build file. Every other entry below is a
|
|
68
|
+
* real `has-*` fact.
|
|
69
|
+
*
|
|
70
|
+
* `has-build-system` is deliberately NOT mapped: Makefile, CMakeLists and
|
|
71
|
+
* meson.build are build files, not dependency manifests, and Dependabot has no
|
|
72
|
+
* ecosystem that reads them. Mapping it to `maven` would be a lie of exactly the
|
|
73
|
+
* kind the detection-driven ruling exists to prevent.
|
|
74
|
+
*/
|
|
75
|
+
const ECOSYSTEMS = [
|
|
76
|
+
{ stack: 'node', value: 'npm', manifest: 'package.json' },
|
|
77
|
+
{ stack: 'typescript', value: 'npm', manifest: 'package.json' },
|
|
78
|
+
{ fact: 'has-pyproject', value: 'pip', manifest: 'pyproject.toml' },
|
|
79
|
+
{ fact: 'has-requirements', value: 'pip', manifest: 'requirements.txt' },
|
|
80
|
+
{ fact: 'has-go-mod', value: 'gomod', manifest: 'go.mod' },
|
|
81
|
+
{ fact: 'has-cargo-toml', value: 'cargo', manifest: 'Cargo.toml' },
|
|
82
|
+
{ fact: 'has-gemfile', value: 'bundler', manifest: 'Gemfile' },
|
|
83
|
+
{ fact: 'has-composer', value: 'composer', manifest: 'composer.json' },
|
|
84
|
+
{ fact: 'has-maven-pom', value: 'maven', manifest: 'pom.xml' },
|
|
85
|
+
{ fact: 'has-dockerfile', value: 'docker', manifest: 'Dockerfile' },
|
|
86
|
+
{ fact: 'has-compose', value: 'docker-compose', manifest: 'compose file' },
|
|
87
|
+
{ fact: 'has-terraform', value: 'terraform', manifest: '*.tf' },
|
|
88
|
+
{ stack: 'gradle', value: 'gradle', manifest: 'build.gradle' },
|
|
89
|
+
{ fact: 'has-submodules', value: 'gitsubmodule', manifest: '.gitmodules' },
|
|
90
|
+
{ fact: 'has-nuget', value: 'nuget', manifest: '*.csproj' },
|
|
91
|
+
{ fact: 'has-pubspec', value: 'pub', manifest: 'pubspec.yaml' },
|
|
92
|
+
{ fact: 'has-swift-package', value: 'swift', manifest: 'Package.swift' },
|
|
93
|
+
{ fact: 'has-mix', value: 'mix', manifest: 'mix.exs' },
|
|
94
|
+
{ fact: 'has-uv-lock', value: 'uv', manifest: 'uv.lock' },
|
|
95
|
+
{ fact: 'has-bun-lock', value: 'bun', manifest: 'bun.lock' },
|
|
96
|
+
{ fact: 'has-devcontainer', value: 'devcontainers', manifest: 'devcontainer.json' },
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The ecosystems this tree has a manifest for, in a stable order.
|
|
101
|
+
*
|
|
102
|
+
* The order is the table's, not detection's: `fingerprint` returns arrays built
|
|
103
|
+
* from sets and file listings, and a config whose entry order changed with
|
|
104
|
+
* readdir order could not be diffed. Determinism is a feature here - a pull
|
|
105
|
+
* request that adds a manifest must show one added entry, not a reshuffle.
|
|
106
|
+
*
|
|
107
|
+
* @param {Iterable<string>} facts emitted `has-*` facts
|
|
108
|
+
* @param {Iterable<string>} [stack] emitted stack tags
|
|
109
|
+
* @returns {{value: string, manifest: string}[]}
|
|
110
|
+
*/
|
|
111
|
+
export function ecosystems(facts, stack = []) {
|
|
112
|
+
const seenFacts = new Set(facts);
|
|
113
|
+
const seenStack = new Set(stack);
|
|
114
|
+
const out = [];
|
|
115
|
+
for (const row of ECOSYSTEMS) {
|
|
116
|
+
const found = row.fact ? seenFacts.has(row.fact) : seenStack.has(row.stack);
|
|
117
|
+
if (!found || out.some((e) => e.value === row.value)) continue;
|
|
118
|
+
out.push({ value: row.value, manifest: row.manifest });
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The header.
|
|
125
|
+
*
|
|
126
|
+
* @param {boolean} own whether this is simt-dev's own repository
|
|
127
|
+
* @returns {string}
|
|
128
|
+
*/
|
|
129
|
+
function header(own) {
|
|
130
|
+
if (own) {
|
|
131
|
+
return `# Written by simt-dev, and refreshed by it. This repository is simt-dev's
|
|
132
|
+
# own, so this file is a copy of the template rather than a repository's own: a diff
|
|
133
|
+
# here means the TEMPLATE changed, not that somebody hand-edited their updates.
|
|
134
|
+
# Anywhere else this file is written once by \`${CLI} init\` and never touched again.
|
|
135
|
+
#
|
|
136
|
+
# To change which ecosystems are watched, change \`src/ci/dependabot.mjs\` in the same
|
|
137
|
+
# commit and run \`${CLI} ci --yes\`. Editing this file by hand works until the
|
|
138
|
+
# next template change overwrites it.
|
|
139
|
+
#
|
|
140
|
+
# The entries below are the ecosystems this repository has a manifest for. A repo
|
|
141
|
+
# that adds a manifest does not get an entry from this tool: \`${CLI} ci\` is the
|
|
142
|
+
# only writer here, so an entry is added deliberately rather than as a side effect.
|
|
143
|
+
`;
|
|
144
|
+
}
|
|
145
|
+
return `# Written by simt-dev when this repo was adopted. Yours from here: simt-dev
|
|
146
|
+
# creates this file once and never rewrites it, so your own cadence, ignore rules
|
|
147
|
+
# and groups stay yours.
|
|
148
|
+
#
|
|
149
|
+
# Each entry is a package manager this repo actually has a manifest for, so every
|
|
150
|
+
# one of them opens pull requests. Adding a manifest does not add an entry - the
|
|
151
|
+
# file is yours now, and the entry is a line you write on purpose.
|
|
152
|
+
#
|
|
153
|
+
# To add one: copy any block below, set \`package-ecosystem\` to your package manager,
|
|
154
|
+
# point \`directory\` at the folder holding the manifest, and leave the schedule at
|
|
155
|
+
# weekly unless you have a reason not to.
|
|
156
|
+
`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** One `updates:` entry. `directory` is "/" for everything: a monorepo's packages
|
|
160
|
+
* live under the root, and Dependabot searches the directory it is given plus its
|
|
161
|
+
* subdirectories only when told to with a glob, which is the repo's decision to
|
|
162
|
+
* make rather than this tool's. */
|
|
163
|
+
function entry({ value }) {
|
|
164
|
+
return ` - package-ecosystem: "${value}"
|
|
165
|
+
directory: "/"
|
|
166
|
+
schedule:
|
|
167
|
+
interval: "weekly"`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The config, as bytes.
|
|
172
|
+
*
|
|
173
|
+
* @param {Iterable<string>} facts emitted `has-*` facts for the target repo
|
|
174
|
+
* @param {{stack?: Iterable<string>, own?: boolean}} [options]
|
|
175
|
+
* @returns {string} YAML, LF-terminated
|
|
176
|
+
*/
|
|
177
|
+
export function renderDependabot(facts, { stack = [], own = false } = {}) {
|
|
178
|
+
const found = ecosystems(facts, stack);
|
|
179
|
+
const entries = [entry({ value: 'github-actions' }), ...found.map(entry)];
|
|
180
|
+
return `${header(own)}version: 2
|
|
181
|
+
updates:
|
|
182
|
+
${entries.join('\n')}
|
|
183
|
+
`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* What this repo's dependabot file should be, and what should happen to it.
|
|
188
|
+
*
|
|
189
|
+
* Mirrors `planWorkflow` deliberately: two files, one ownership rule, one place
|
|
190
|
+
* that knows it. A reader of `src/commands/ci.mjs` should not have to work out
|
|
191
|
+
* which of these two files behaves differently.
|
|
192
|
+
*
|
|
193
|
+
* @param {string} root repo root
|
|
194
|
+
* @returns {Promise<{file:string, path:string, action:'create'|'refresh'|'unchanged'|'skip',
|
|
195
|
+
* own:boolean, exists:boolean, bytes:number, current:boolean,
|
|
196
|
+
* workflows:boolean, ecosystems:string[], reason:string|null}>}
|
|
197
|
+
*/
|
|
198
|
+
export async function planDependabot(root) {
|
|
199
|
+
const path = join(root, DEPENDABOT_FILE);
|
|
200
|
+
const own = isOwnRepo(root);
|
|
201
|
+
let text = null;
|
|
202
|
+
try {
|
|
203
|
+
text = await readFile(path, 'utf8');
|
|
204
|
+
} catch {
|
|
205
|
+
text = null;
|
|
206
|
+
}
|
|
207
|
+
const exists = text !== null;
|
|
208
|
+
const bytes = exists ? Buffer.byteLength(text) : 0;
|
|
209
|
+
|
|
210
|
+
// ENOENT is absence; anything else is a file this tool cannot read, and calling
|
|
211
|
+
// that "create" hands the writer an EACCES instead of saying what is wrong.
|
|
212
|
+
let workflows = false;
|
|
213
|
+
try {
|
|
214
|
+
workflows = (await stat(join(root, WORKFLOWS_DIR))).isDirectory();
|
|
215
|
+
} catch {
|
|
216
|
+
workflows = false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Detection runs even when the file exists, because the refresh comparison needs
|
|
220
|
+
// the bytes this repo would get today. Skipping it would make `refresh` mean
|
|
221
|
+
// "the file differs" instead of "the file differs from what this repo should get",
|
|
222
|
+
// and those are different questions when a manifest appeared since.
|
|
223
|
+
const { facts, stack } = await fingerprint(root);
|
|
224
|
+
const watched = ecosystems(facts, stack).map((e) => e.value);
|
|
225
|
+
// Carried on the plan so the writer renders from the same observation this
|
|
226
|
+
// comparison used. Re-detecting at write time would make `ci` and `check`
|
|
227
|
+
// disagree whenever a manifest appeared between the two calls, and the file
|
|
228
|
+
// would be written from facts nobody ever saw in the plan.
|
|
229
|
+
const detection = { facts, stack };
|
|
230
|
+
|
|
231
|
+
if (exists) {
|
|
232
|
+
// Both sides of the comparison, for the same reason as the workflow plan: a reader
|
|
233
|
+
// told the file differs can be shown how, and re-rendering for a diff would let
|
|
234
|
+
// the two answers disagree.
|
|
235
|
+
const desired = renderDependabot(facts, { stack, own: true });
|
|
236
|
+
const current = own ? text === desired : true;
|
|
237
|
+
return {
|
|
238
|
+
file: DEPENDABOT_FILE,
|
|
239
|
+
path,
|
|
240
|
+
action: current ? 'unchanged' : 'refresh',
|
|
241
|
+
own,
|
|
242
|
+
exists,
|
|
243
|
+
bytes,
|
|
244
|
+
current,
|
|
245
|
+
source: { text, desired },
|
|
246
|
+
workflows,
|
|
247
|
+
ecosystems: watched,
|
|
248
|
+
...detection,
|
|
249
|
+
reason: current ? null : `this repository is simt-dev's own, and the dependabot template has changed since this copy was written`,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
file: DEPENDABOT_FILE,
|
|
254
|
+
path,
|
|
255
|
+
action: workflows || own ? 'create' : 'skip',
|
|
256
|
+
own,
|
|
257
|
+
exists,
|
|
258
|
+
bytes,
|
|
259
|
+
current: false,
|
|
260
|
+
workflows,
|
|
261
|
+
ecosystems: watched,
|
|
262
|
+
...detection,
|
|
263
|
+
reason:
|
|
264
|
+
workflows || own
|
|
265
|
+
? null
|
|
266
|
+
: `no ${WORKFLOWS_DIR} directory, so this repo does not run GitHub Actions yet`,
|
|
267
|
+
};
|
|
268
|
+
}
|