@drkmttr/adhere 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +194 -51
  2. package/package.json +9 -9
  3. package/presets/effect/basics/{instrument-with-pipe.md → instrument-with-pipe/RULE.md} +1 -1
  4. package/presets/effect/config/{business-logic-depends-on-config-service.md → business-logic-depends-on-config-service/RULE.md} +3 -3
  5. package/presets/effect/config/{secrets-are-redacted.md → secrets-are-redacted/RULE.md} +3 -3
  6. package/presets/effect/config/{validate-with-schema.md → validate-with-schema/RULE.md} +4 -4
  7. package/skills/adhere/SKILL.md +213 -118
  8. package/skills/adhere-fix/SKILL.md +9 -8
  9. package/skills/adhere-setup/SKILL.md +244 -0
  10. package/src/cli.ts +49 -33
  11. package/src/config.ts +64 -3
  12. package/src/index.ts +2 -0
  13. package/src/init.ts +36 -25
  14. package/src/install.ts +37 -26
  15. package/src/main.ts +1 -1
  16. package/src/markdown.ts +9 -1
  17. package/src/rules.ts +135 -56
  18. package/src/services/AdhereConfig.ts +15 -3
  19. package/src/services/Credentials.ts +4 -4
  20. package/src/services/Jev.http.ts +82 -10
  21. package/src/services/Jev.ts +41 -10
  22. package/src/workflows/audit.ts +33 -14
  23. package/src/workflows/format.ts +172 -35
  24. package/src/workspaces.ts +141 -0
  25. /package/presets/alchemy/apis/{match-remote-errors-by-tag.md → match-remote-errors-by-tag/RULE.md} +0 -0
  26. /package/presets/alchemy/apis/{schemas-at-trust-boundaries.md → schemas-at-trust-boundaries/RULE.md} +0 -0
  27. /package/presets/alchemy/data/{commit-migrations-then-deploy.md → commit-migrations-then-deploy/RULE.md} +0 -0
  28. /package/presets/alchemy/data/{hyperdrive-direct-origin.md → hyperdrive-direct-origin/RULE.md} +0 -0
  29. /package/presets/alchemy/data/{migrations-use-direct-url.md → migrations-use-direct-url/RULE.md} +0 -0
  30. /package/presets/alchemy/data/{never-disable-tls-verification.md → never-disable-tls-verification/RULE.md} +0 -0
  31. /package/presets/alchemy/data/{private-endpoint-at-runtime.md → private-endpoint-at-runtime/RULE.md} +0 -0
  32. /package/presets/alchemy/data/{single-migration-owner.md → single-migration-owner/RULE.md} +0 -0
  33. /package/presets/alchemy/durable/{idempotent-event-handlers.md → idempotent-event-handlers/RULE.md} +0 -0
  34. /package/presets/alchemy/durable/{io-inside-steps.md → io-inside-steps/RULE.md} +0 -0
  35. /package/presets/alchemy/durable/{no-self-triggering-bucket-writes.md → no-self-triggering-bucket-writes/RULE.md} +0 -0
  36. /package/presets/alchemy/durable/{stable-step-names.md → stable-step-names/RULE.md} +0 -0
  37. /package/presets/alchemy/durable/{webhook-state-is-truth.md → webhook-state-is-truth/RULE.md} +0 -0
  38. /package/presets/alchemy/providers/{diff-guards-unresolved-props.md → diff-guards-unresolved-props/RULE.md} +0 -0
  39. /package/presets/alchemy/providers/{idempotent-delete.md → idempotent-delete/RULE.md} +0 -0
  40. /package/presets/alchemy/providers/{lazy-layer-construction.md → lazy-layer-construction/RULE.md} +0 -0
  41. /package/presets/alchemy/providers/{reconcile-from-observed-state.md → reconcile-from-observed-state/RULE.md} +0 -0
  42. /package/presets/alchemy/resources/{anchor-paths-with-import-meta.md → anchor-paths-with-import-meta/RULE.md} +0 -0
  43. /package/presets/alchemy/resources/{idempotent-actions-and-seeds.md → idempotent-actions-and-seeds/RULE.md} +0 -0
  44. /package/presets/alchemy/resources/{previews-reference-shared-resources.md → previews-reference-shared-resources/RULE.md} +0 -0
  45. /package/presets/alchemy/resources/{protect-irreplaceable-data.md → protect-irreplaceable-data/RULE.md} +0 -0
  46. /package/presets/alchemy/resources/{remote-state-for-ci.md → remote-state-for-ci/RULE.md} +0 -0
  47. /package/presets/alchemy/resources/{stable-logical-ids.md → stable-logical-ids/RULE.md} +0 -0
  48. /package/presets/alchemy/resources/{stage-unique-physical-names.md → stage-unique-physical-names/RULE.md} +0 -0
  49. /package/presets/alchemy/runtime/{bind-tag-not-implementation.md → bind-tag-not-implementation/RULE.md} +0 -0
  50. /package/presets/alchemy/runtime/{deterministic-config-defaults.md → deterministic-config-defaults/RULE.md} +0 -0
  51. /package/presets/alchemy/runtime/{disposables-in-request-scope.md → disposables-in-request-scope/RULE.md} +0 -0
  52. /package/presets/alchemy/runtime/{lambda-no-post-response-work.md → lambda-no-post-response-work/RULE.md} +0 -0
  53. /package/presets/alchemy/runtime/{no-per-request-work-at-construction.md → no-per-request-work-at-construction/RULE.md} +0 -0
  54. /package/presets/alchemy/runtime/{resolve-config-and-bindings-at-construction.md → resolve-config-and-bindings-at-construction/RULE.md} +0 -0
  55. /package/presets/alchemy/secrets/{let-alchemy-mint-infra-secrets.md → let-alchemy-mint-infra-secrets/RULE.md} +0 -0
  56. /package/presets/alchemy/secrets/{none-in-browser-env.md → none-in-browser-env/RULE.md} +0 -0
  57. /package/presets/alchemy/secrets/{redacted-until-use.md → redacted-until-use/RULE.md} +0 -0
  58. /package/presets/alchemy/secrets/{unwrap-before-compare-or-send.md → unwrap-before-compare-or-send/RULE.md} +0 -0
  59. /package/presets/alchemy/security/{authorize-public-endpoints.md → authorize-public-endpoints/RULE.md} +0 -0
  60. /package/presets/alchemy/security/{github-secret-not-variable.md → github-secret-not-variable/RULE.md} +0 -0
  61. /package/presets/alchemy/security/{identity-from-verified-session.md → identity-from-verified-session/RULE.md} +0 -0
  62. /package/presets/alchemy/security/{keep-auth-protections-on.md → keep-auth-protections-on/RULE.md} +0 -0
  63. /package/presets/alchemy/security/{least-privilege-bindings.md → least-privilege-bindings/RULE.md} +0 -0
  64. /package/presets/alchemy/security/{least-privilege-ci-credentials.md → least-privilege-ci-credentials/RULE.md} +0 -0
  65. /package/presets/alchemy/security/{lookup-failure-is-not-anonymous.md → lookup-failure-is-not-anonymous/RULE.md} +0 -0
  66. /package/presets/effect/basics/{gen-for-sequencing.md → gen-for-sequencing/RULE.md} +0 -0
  67. /package/presets/effect/config/{tests-provide-values-directly.md → tests-provide-values-directly/RULE.md} +0 -0
  68. /package/presets/effect/data/{brand-meaningful-primitives.md → brand-meaningful-primitives/RULE.md} +0 -0
  69. /package/presets/effect/data/{variants-are-tagged-unions.md → variants-are-tagged-unions/RULE.md} +0 -0
  70. /package/presets/effect/errors/{catch-defects-at-boundaries-only.md → catch-defects-at-boundaries-only/RULE.md} +0 -0
  71. /package/presets/effect/errors/{defects-for-bugs.md → defects-for-bugs/RULE.md} +0 -0
  72. /package/presets/effect/errors/{wrap-external-errors.md → wrap-external-errors/RULE.md} +0 -0
  73. /package/presets/effect/services/{dependencies-through-layers.md → dependencies-through-layers/RULE.md} +0 -0
  74. /package/presets/effect/services/{fresh-layer-per-test.md → fresh-layer-per-test/RULE.md} +0 -0
  75. /package/presets/effect/services/{memoize-parameterized-layers.md → memoize-parameterized-layers/RULE.md} +0 -0
  76. /package/presets/effect/services/{no-mutable-state.md → no-mutable-state/RULE.md} +0 -0
  77. /package/presets/effect/services/{operations-have-no-requirements.md → operations-have-no-requirements/RULE.md} +0 -0
  78. /package/presets/effect/services/{test-layers-are-in-memory.md → test-layers-are-in-memory/RULE.md} +0 -0
  79. /package/presets/effect/testing/{test-clock-for-time.md → test-clock-for-time/RULE.md} +0 -0
  80. /package/presets/react/effects/{clean-up-what-they-start.md → clean-up-what-they-start/RULE.md} +0 -0
  81. /package/presets/react/effects/{event-logic-in-handlers.md → event-logic-in-handlers/RULE.md} +0 -0
  82. /package/presets/react/effects/{external-stores-use-sync-external-store.md → external-stores-use-sync-external-store/RULE.md} +0 -0
  83. /package/presets/react/effects/{fetches-ignore-stale-responses.md → fetches-ignore-stale-responses/RULE.md} +0 -0
  84. /package/presets/react/effects/{notify-parents-in-handlers.md → notify-parents-in-handlers/RULE.md} +0 -0
  85. /package/presets/react/server/{no-private-data-to-client.md → no-private-data-to-client/RULE.md} +0 -0
  86. /package/presets/react/server/{server-functions-authorize-and-validate.md → server-functions-authorize-and-validate/RULE.md} +0 -0
  87. /package/presets/react/state/{no-contradictory-state.md → no-contradictory-state/RULE.md} +0 -0
  88. /package/presets/react/state/{no-props-copied-into-state.md → no-props-copied-into-state/RULE.md} +0 -0
  89. /package/presets/react/state/{store-ids-not-copies.md → store-ids-not-copies/RULE.md} +0 -0
  90. /package/presets/security/{constant-time-secret-compare.md → constant-time-secret-compare/RULE.md} +0 -0
  91. /package/presets/security/{no-secrets-in-output.md → no-secrets-in-output/RULE.md} +0 -0
  92. /package/presets/security/{no-untrusted-input-in-commands-or-paths.md → no-untrusted-input-in-commands-or-paths/RULE.md} +0 -0
  93. /package/presets/security/{parameterized-queries.md → parameterized-queries/RULE.md} +0 -0
  94. /package/presets/typescript/async/{concurrent-independent-work.md → concurrent-independent-work/RULE.md} +0 -0
  95. /package/presets/typescript/async/{network-calls-have-timeouts.md → network-calls-have-timeouts/RULE.md} +0 -0
  96. /package/presets/typescript/design/{no-argument-mutation.md → no-argument-mutation/RULE.md} +0 -0
  97. /package/presets/typescript/errors/{no-swallowed-errors.md → no-swallowed-errors/RULE.md} +0 -0
  98. /package/presets/typescript/errors/{release-on-every-path.md → release-on-every-path/RULE.md} +0 -0
  99. /package/presets/typescript/testing/{assert-outcomes.md → assert-outcomes/RULE.md} +0 -0
  100. /package/presets/typescript/testing/{independent-tests.md → independent-tests/RULE.md} +0 -0
  101. /package/presets/typescript/types/{assertions-only-when-proven.md → assertions-only-when-proven/RULE.md} +0 -0
  102. /package/presets/typescript/types/{derive-dont-restate.md → derive-dont-restate/RULE.md} +0 -0
  103. /package/presets/typescript/types/{unrepresentable-invalid-states.md → unrepresentable-invalid-states/RULE.md} +0 -0
  104. /package/presets/typescript/types/{validate-external-data.md → validate-external-data/RULE.md} +0 -0
@@ -0,0 +1,141 @@
1
+ import { Effect, FileSystem, Path } from "effect";
2
+
3
+ /** A workspace's packages: each one's name, with its directory from the root. */
4
+ export type WorkspacePackages = Readonly<Record<string, string>>;
5
+
6
+ /** What JSON text holds, or nothing when it is not JSON. */
7
+ const parsed = (text: string): unknown => {
8
+ try {
9
+ return JSON.parse(text);
10
+ } catch {
11
+ return undefined;
12
+ }
13
+ };
14
+
15
+ /** The strings of a value that is an array of them, and none of any other value. */
16
+ const strings = (value: unknown): ReadonlyArray<string> =>
17
+ Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
18
+
19
+ /**
20
+ * A root package.json's workspace patterns: `workspaces` as an array, which
21
+ * npm, Bun, and Yarn read, or as Yarn's `{ packages: [...] }`.
22
+ */
23
+ const manifestPatterns = (manifest: unknown): ReadonlyArray<string> => {
24
+ const workspaces = (manifest as { readonly workspaces?: unknown } | undefined)?.workspaces;
25
+ return Array.isArray(workspaces)
26
+ ? strings(workspaces)
27
+ : strings((workspaces as { readonly packages?: unknown } | undefined)?.packages);
28
+ };
29
+
30
+ /**
31
+ * The patterns under `packages:` in a pnpm-workspace.yaml: the list items
32
+ * that follow it, up to the next key. Read line by line, since that list is
33
+ * all of the file adhere needs and the executable carries no YAML parser.
34
+ */
35
+ const pnpmPatterns = (yaml: string): ReadonlyArray<string> => {
36
+ const lines = yaml.split(/\r?\n/);
37
+ const start = lines.findIndex((line) => /^packages:\s*$/.test(line));
38
+ if (start < 0) return [];
39
+ const rest = lines.slice(start + 1);
40
+ const end = rest.findIndex((line) => /^\S/.test(line) && !line.startsWith("-"));
41
+ return (end < 0 ? rest : rest.slice(0, end)).flatMap((line) => {
42
+ const [, item] = /^\s*-\s*(.+?)\s*$/.exec(line) ?? [];
43
+ return item === undefined ? [] : [item.replace(/^["']|["']$/g, "")];
44
+ });
45
+ };
46
+
47
+ /** Whether a directory's name matches a pattern segment, where `*` stands for any run of characters. */
48
+ const matches = (segment: string, name: string): boolean =>
49
+ new RegExp(
50
+ `^${segment
51
+ .split("*")
52
+ .map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&"))
53
+ .join(".*")}$`,
54
+ ).test(name);
55
+
56
+ /**
57
+ * The packages of the workspace whose root is `root`, by name, sorted, so
58
+ * the same workspace always reads the same: from the root package.json's
59
+ * `workspaces` and from pnpm-workspace.yaml's `packages`. A pattern's `*` is
60
+ * any directory and `**` any depth of them, outside `node_modules` and dot
61
+ * directories, and one that starts with `!` leaves out what it matches. A
62
+ * root that has neither file, or names no workspaces, has no packages.
63
+ */
64
+ export const workspacePackagesOf = Effect.fn("workspacePackagesOf")(function* (root: string) {
65
+ const fs = yield* FileSystem.FileSystem;
66
+ const path = yield* Path.Path;
67
+ const read = (file: string) =>
68
+ fs.readFileString(path.join(root, file)).pipe(Effect.orElseSucceed(() => ""));
69
+ const children = (directory: string) =>
70
+ fs.readDirectory(path.join(root, directory)).pipe(
71
+ Effect.orElseSucceed((): ReadonlyArray<string> => []),
72
+ Effect.map((names) =>
73
+ names.filter((name) => name !== "node_modules" && !name.startsWith(".")),
74
+ ),
75
+ Effect.flatMap((names) =>
76
+ Effect.filter(names, (name) =>
77
+ fs.stat(path.join(root, directory, name)).pipe(
78
+ Effect.map((info) => info.type === "Directory"),
79
+ Effect.orElseSucceed(() => false),
80
+ ),
81
+ ),
82
+ ),
83
+ Effect.map((names) =>
84
+ names.map((name) => (directory === "" ? name : `${directory}/${name}`)),
85
+ ),
86
+ );
87
+ /** The directories under `directory` that the pattern's remaining segments lead to. */
88
+ const expand = (
89
+ directory: string,
90
+ segments: ReadonlyArray<string>,
91
+ ): Effect.Effect<ReadonlyArray<string>> => {
92
+ const [segment, ...rest] = segments;
93
+ if (segment === undefined) return Effect.succeed([directory]);
94
+ if (segment === "**") {
95
+ return Effect.gen(function* () {
96
+ const below = yield* Effect.forEach(yield* children(directory), (child) =>
97
+ expand(child, segments),
98
+ );
99
+ return [...(yield* expand(directory, rest)), ...below.flat()];
100
+ });
101
+ }
102
+ if (!segment.includes("*")) {
103
+ return expand(directory === "" ? segment : `${directory}/${segment}`, rest);
104
+ }
105
+ return Effect.flatMap(children(directory), (names) =>
106
+ Effect.map(
107
+ Effect.forEach(
108
+ names.filter((name) => matches(segment, name.slice(name.lastIndexOf("/") + 1))),
109
+ (child) => expand(child, rest),
110
+ ),
111
+ (found) => found.flat(),
112
+ ),
113
+ );
114
+ };
115
+ const segmentsOf = (pattern: string) =>
116
+ pattern.split("/").filter((segment) => segment !== "" && segment !== ".");
117
+ const patterns = [
118
+ ...manifestPatterns(parsed(yield* read("package.json"))),
119
+ ...pnpmPatterns(yield* read("pnpm-workspace.yaml")),
120
+ ];
121
+ const directoriesOf = (chosen: ReadonlyArray<string>) =>
122
+ Effect.map(
123
+ Effect.forEach(chosen, (pattern) => expand("", segmentsOf(pattern))),
124
+ (found) => new Set(found.flat()),
125
+ );
126
+ const included = yield* directoriesOf(patterns.filter((pattern) => !pattern.startsWith("!")));
127
+ const excluded = yield* directoriesOf(
128
+ patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => pattern.slice(1)),
129
+ );
130
+ const named = yield* Effect.forEach(
131
+ [...included].filter((directory) => !excluded.has(directory)),
132
+ (directory) =>
133
+ Effect.map(read(`${directory}/package.json`), (text) => {
134
+ const name = (parsed(text) as { readonly name?: unknown } | undefined)?.name;
135
+ return typeof name === "string" ? [[name, directory] as const] : [];
136
+ }),
137
+ );
138
+ return Object.fromEntries(
139
+ named.flat().sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),
140
+ ) as WorkspacePackages;
141
+ });