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.
Files changed (138) hide show
  1. python/simt_dev/__init__.py +32 -0
  2. python/simt_dev/cli.py +225 -0
  3. simt_dev/__init__.py +32 -0
  4. simt_dev/cli.py +225 -0
  5. simt_dev/vendored/CHANGELOG.md +166 -0
  6. simt_dev/vendored/bin/simt-dev.mjs +28 -0
  7. simt_dev/vendored/docs/decisions/ADR-001-push-requires-approval.md +42 -0
  8. simt_dev/vendored/docs/decisions/ADR-002-refactor-blast-radius.md +45 -0
  9. simt_dev/vendored/docs/decisions/ADR-003-test-breadth.md +46 -0
  10. simt_dev/vendored/docs/decisions/ADR-004-hook-owns-format.md +45 -0
  11. simt_dev/vendored/docs/decisions/ADR-005-file-length.md +42 -0
  12. simt_dev/vendored/docs/decisions/ADR-006-coverage-bar.md +59 -0
  13. simt_dev/vendored/docs/decisions/ADR-007-section-skeleton.md +54 -0
  14. simt_dev/vendored/docs/decisions/ADR-008-verdict-vocabulary.md +59 -0
  15. simt_dev/vendored/docs/decisions/ADR-009-skill-layout.md +48 -0
  16. simt_dev/vendored/docs/decisions/ADR-010-runner-invocation.md +56 -0
  17. simt_dev/vendored/docs/decisions/ADR-011-windows-support.md +52 -0
  18. simt_dev/vendored/docs/decisions/ADR-012-drop-windows-support.md +59 -0
  19. simt_dev/vendored/docs/decisions/ADR-013-genericity-by-facts-and-local-profiles.md +114 -0
  20. simt_dev/vendored/docs/decisions/ADR-014-workspace-is-configuration.md +211 -0
  21. simt_dev/vendored/docs/decisions/ADR-015-a-source-is-content-not-a-location.md +117 -0
  22. simt_dev/vendored/docs/decisions/ADR-016-a-divergent-rule-is-retired-not-merged.md +180 -0
  23. simt_dev/vendored/docs/decisions/ADR-017-a-declared-gate-must-be-runnable.md +89 -0
  24. simt_dev/vendored/docs/decisions/ADR-018-an-unset-hooks-path-is-not-an-absent-hook.md +183 -0
  25. simt_dev/vendored/docs/decisions/ADR-019-a-declared-command-is-the-runner.md +110 -0
  26. simt_dev/vendored/docs/decisions/ADR-020-a-rule-is-written-per-surface-and-pointed-not-copied.md +100 -0
  27. simt_dev/vendored/docs/decisions/ADR-021-one-surface-per-repo.md +94 -0
  28. simt_dev/vendored/docs/decisions/ADR-022-a-key-simt-dev-reads-is-prefixed.md +98 -0
  29. simt_dev/vendored/docs/decisions/ADR-023-the-gate-ships-as-a-file-the-repo-owns.md +106 -0
  30. simt_dev/vendored/docs/decisions/ADR-024-simt-dev-overwrites-its-own-gate-file.md +111 -0
  31. simt_dev/vendored/docs/decisions/ADR-025-a-baseline-is-a-region-in-a-file-the-repo-owns.md +153 -0
  32. simt_dev/vendored/docs/decisions/ADR-026-a-config-without-a-region-is-a-file-the-repo-owns.md +117 -0
  33. simt_dev/vendored/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +130 -0
  34. 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
  35. simt_dev/vendored/docs/decisions/ADR-029-the-pins-a-repository-declares-are-its-own.md +120 -0
  36. simt_dev/vendored/docs/decisions/ADR-030-what-a-plan-does-not-ship-is-a-decision-with-a-record.md +114 -0
  37. simt_dev/vendored/docs/decisions/ADR-031-the-suite-stages-the-environment-it-needs.md +129 -0
  38. simt_dev/vendored/docs/decisions/ADR-032-provenance-is-a-content-digest-not-a-commit.md +141 -0
  39. simt_dev/vendored/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +82 -0
  40. simt_dev/vendored/docs/decisions/ADR-034-a-staged-tool-refuses-a-partially-staged-file.md +91 -0
  41. simt_dev/vendored/docs/decisions/ADR-035-one-write-decision-and-one-json-document.md +119 -0
  42. simt_dev/vendored/docs/decisions/ADR-036-a-writer-that-writes-says-so-and-the-instructions-naming-it-move-with-it.md +103 -0
  43. simt_dev/vendored/docs/decisions/ADR-037-unchanged-means-writing-would-not-change-a-byte.md +100 -0
  44. simt_dev/vendored/docs/decisions/ADR-038-the-gate-filter-belongs-to-every-manifest-writer.md +95 -0
  45. simt_dev/vendored/docs/decisions/ADR-039-a-ci-step-runs-a-form-that-can-fail.md +101 -0
  46. simt_dev/vendored/docs/decisions/ADR-040-the-last-writer-that-wrote-by-default-was-not-the-one-named.md +158 -0
  47. simt_dev/vendored/docs/decisions/ADR-041-the-suite-installs-the-package-in-order-to-test-it.md +92 -0
  48. simt_dev/vendored/docs/decisions/ADR-042-a-declared-glob-must-match-a-path-built-from-it.md +94 -0
  49. simt_dev/vendored/docs/decisions/ADR-043-a-keyword-is-a-phrase-and-two-letters-is-a-word.md +90 -0
  50. simt_dev/vendored/docs/decisions/ADR-044-compatibility-is-not-evidence.md +116 -0
  51. simt_dev/vendored/docs/decisions/ADR-045-a-dashboard-that-counts-a-defect.md +119 -0
  52. simt_dev/vendored/docs/decisions/ADR-046-one-rule-one-copy.md +140 -0
  53. simt_dev/vendored/docs/decisions/ADR-047-a-profile-selects-on-what-a-repository-runs-on.md +141 -0
  54. simt_dev/vendored/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +150 -0
  55. simt_dev/vendored/docs/decisions/ADR-049-a-ruling-that-a-later-record-moved-says-so-where-it-is-printed.md +153 -0
  56. 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
  57. simt_dev/vendored/docs/decisions/ADR-051-a-pointer-is-the-readers-only-route-so-it-is-not-a-convenience.md +134 -0
  58. simt_dev/vendored/docs/decisions/ADR-052-a-dependency-is-counted-against-the-budget-and-there-is-one-budget.md +101 -0
  59. simt_dev/vendored/docs/decisions/ADR-053-the-node-floor-is-22-20-0.md +49 -0
  60. simt_dev/vendored/docs/decisions/ADR-054-the-digest-covers-every-byte-that-leaves-the-package.md +100 -0
  61. simt_dev/vendored/docs/decisions/ADR-055-a-status-line-travels-so-it-may-not-name-a-location.md +98 -0
  62. simt_dev/vendored/docs/decisions/ADR-056-three-names-one-of-which-is-cheap-to-move.md +160 -0
  63. simt_dev/vendored/docs/decisions/ADR-057-an-identity-is-per-developer-and-a-repository-does-not-own-it.md +115 -0
  64. simt_dev/vendored/docs/decisions/ADR-058-one-tool-three-channels-and-the-order-is-forced.md +148 -0
  65. simt_dev/vendored/docs/decisions/README.md +530 -0
  66. simt_dev/vendored/graph/skills.graph.json +1622 -0
  67. simt_dev/vendored/package.json +43 -0
  68. simt_dev/vendored/rules/core.md +76 -0
  69. simt_dev/vendored/rules/mdc/intake-questions.mdc +50 -0
  70. simt_dev/vendored/rules/mdc/preview-postflight.mdc +61 -0
  71. simt_dev/vendored/rules/mdc/slice-commit.mdc +52 -0
  72. simt_dev/vendored/rules/mdc/test-gate.mdc +54 -0
  73. simt_dev/vendored/rules/stacks/android.md +63 -0
  74. simt_dev/vendored/rules/stacks/cloudflare-workers.md +65 -0
  75. simt_dev/vendored/rules/stacks/connectiq.md +62 -0
  76. simt_dev/vendored/rules/stacks/container.md +31 -0
  77. simt_dev/vendored/rules/stacks/jvm.md +54 -0
  78. simt_dev/vendored/rules/stacks/node.md +45 -0
  79. simt_dev/vendored/rules/stacks/python.md +48 -0
  80. simt_dev/vendored/rules/stacks/react-native.md +58 -0
  81. simt_dev/vendored/rules/stacks/react-web.md +61 -0
  82. simt_dev/vendored/rules/stacks/typescript.md +21 -0
  83. simt_dev/vendored/rules/stacks/uv.md +32 -0
  84. simt_dev/vendored/src/argv.mjs +116 -0
  85. simt_dev/vendored/src/ci/dependabot.mjs +268 -0
  86. simt_dev/vendored/src/ci/workflow.mjs +360 -0
  87. simt_dev/vendored/src/commands/ci.mjs +285 -0
  88. simt_dev/vendored/src/commands/compare.mjs +195 -0
  89. simt_dev/vendored/src/commands/graph.mjs +73 -0
  90. simt_dev/vendored/src/commands/help.mjs +108 -0
  91. simt_dev/vendored/src/commands/hooks.mjs +471 -0
  92. simt_dev/vendored/src/commands/index.mjs +241 -0
  93. simt_dev/vendored/src/commands/init.mjs +632 -0
  94. simt_dev/vendored/src/commands/mcp.mjs +63 -0
  95. simt_dev/vendored/src/commands/rules.mjs +374 -0
  96. simt_dev/vendored/src/commands/skills.mjs +647 -0
  97. simt_dev/vendored/src/commands/skillsinstall.mjs +515 -0
  98. simt_dev/vendored/src/commands/sync.mjs +331 -0
  99. simt_dev/vendored/src/commands/upgrade.mjs +202 -0
  100. simt_dev/vendored/src/compare/discover.mjs +212 -0
  101. simt_dev/vendored/src/compare/hooksstate.mjs +159 -0
  102. simt_dev/vendored/src/compare/row.mjs +299 -0
  103. simt_dev/vendored/src/decisions/index.mjs +536 -0
  104. simt_dev/vendored/src/graph/load.mjs +541 -0
  105. simt_dev/vendored/src/hooks/capability.mjs +129 -0
  106. simt_dev/vendored/src/hooks/dispatcher.mjs +268 -0
  107. simt_dev/vendored/src/hooks/gates.mjs +144 -0
  108. simt_dev/vendored/src/hooks/resolve.mjs +194 -0
  109. simt_dev/vendored/src/hooks/shim.mjs +140 -0
  110. simt_dev/vendored/src/hooks/where.mjs +118 -0
  111. simt_dev/vendored/src/identity.mjs +218 -0
  112. simt_dev/vendored/src/locate.mjs +75 -0
  113. simt_dev/vendored/src/mcp/server.mjs +167 -0
  114. simt_dev/vendored/src/mcp/tools.mjs +410 -0
  115. simt_dev/vendored/src/name.mjs +95 -0
  116. simt_dev/vendored/src/output/index.mjs +96 -0
  117. simt_dev/vendored/src/projection/baselines.mjs +209 -0
  118. simt_dev/vendored/src/projection/conflicts.mjs +205 -0
  119. simt_dev/vendored/src/projection/manifest.mjs +478 -0
  120. simt_dev/vendored/src/projection/markers.mjs +349 -0
  121. simt_dev/vendored/src/projection/mdc.mjs +327 -0
  122. simt_dev/vendored/src/projection/project.mjs +504 -0
  123. simt_dev/vendored/src/projection/rules.mjs +640 -0
  124. simt_dev/vendored/src/projection/surfaces.mjs +398 -0
  125. simt_dev/vendored/src/projection/targets.mjs +221 -0
  126. simt_dev/vendored/src/resolve/fingerprint.mjs +553 -0
  127. simt_dev/vendored/src/resolve/near.mjs +61 -0
  128. simt_dev/vendored/src/resolve/resolve.mjs +457 -0
  129. simt_dev/vendored/src/resolve/source.mjs +105 -0
  130. simt_dev/vendored/src/toolchain.mjs +202 -0
  131. simt_dev/vendored/src/tsconfig.mjs +314 -0
  132. simt_dev/vendored/src/upgrade/plan.mjs +203 -0
  133. simt_dev/vendored/src/workspace.mjs +289 -0
  134. simt_dev-0.2.0.dist-info/METADATA +150 -0
  135. simt_dev-0.2.0.dist-info/RECORD +138 -0
  136. simt_dev-0.2.0.dist-info/WHEEL +4 -0
  137. simt_dev-0.2.0.dist-info/entry_points.txt +2 -0
  138. 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
+ }