@cad0p/pi-steering 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/LICENSE +29 -0
- package/README.md +920 -0
- package/dist/__test-helpers__.d.ts +95 -0
- package/dist/__test-helpers__.d.ts.map +1 -0
- package/dist/__test-helpers__.js +174 -0
- package/dist/__test-helpers__.js.map +1 -0
- package/dist/bin/pi-steering.d.ts +9 -0
- package/dist/bin/pi-steering.d.ts.map +1 -0
- package/dist/bin/pi-steering.js +506 -0
- package/dist/bin/pi-steering.js.map +1 -0
- package/dist/compat.d.ts +50 -0
- package/dist/compat.d.ts.map +1 -0
- package/dist/compat.js +180 -0
- package/dist/compat.js.map +1 -0
- package/dist/defaults.d.ts +61 -0
- package/dist/defaults.d.ts.map +1 -0
- package/dist/defaults.js +106 -0
- package/dist/defaults.js.map +1 -0
- package/dist/define-config.d.ts +235 -0
- package/dist/define-config.d.ts.map +1 -0
- package/dist/define-config.js +118 -0
- package/dist/define-config.js.map +1 -0
- package/dist/define-predicate.d.ts +49 -0
- package/dist/define-predicate.d.ts.map +1 -0
- package/dist/define-predicate.js +35 -0
- package/dist/define-predicate.js.map +1 -0
- package/dist/evaluator-internals/context.d.ts +186 -0
- package/dist/evaluator-internals/context.d.ts.map +1 -0
- package/dist/evaluator-internals/context.js +219 -0
- package/dist/evaluator-internals/context.js.map +1 -0
- package/dist/evaluator-internals/override.d.ts +19 -0
- package/dist/evaluator-internals/override.d.ts.map +1 -0
- package/dist/evaluator-internals/override.js +79 -0
- package/dist/evaluator-internals/override.js.map +1 -0
- package/dist/evaluator-internals/predicates.d.ts +152 -0
- package/dist/evaluator-internals/predicates.d.ts.map +1 -0
- package/dist/evaluator-internals/predicates.js +849 -0
- package/dist/evaluator-internals/predicates.js.map +1 -0
- package/dist/evaluator-internals/speculative-synthesis.d.ts +102 -0
- package/dist/evaluator-internals/speculative-synthesis.d.ts.map +1 -0
- package/dist/evaluator-internals/speculative-synthesis.js +101 -0
- package/dist/evaluator-internals/speculative-synthesis.js.map +1 -0
- package/dist/evaluator.d.ts +101 -0
- package/dist/evaluator.d.ts.map +1 -0
- package/dist/evaluator.js +714 -0
- package/dist/evaluator.js.map +1 -0
- package/dist/helpers/walker-unknown-cwd-reason.d.ts +68 -0
- package/dist/helpers/walker-unknown-cwd-reason.d.ts.map +1 -0
- package/dist/helpers/walker-unknown-cwd-reason.js +73 -0
- package/dist/helpers/walker-unknown-cwd-reason.js.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +93 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/drop-unused-observers.d.ts +43 -0
- package/dist/internal/drop-unused-observers.d.ts.map +1 -0
- package/dist/internal/drop-unused-observers.js +50 -0
- package/dist/internal/drop-unused-observers.js.map +1 -0
- package/dist/internal/finalize-plugin-state.d.ts +22 -0
- package/dist/internal/finalize-plugin-state.d.ts.map +1 -0
- package/dist/internal/finalize-plugin-state.js +14 -0
- package/dist/internal/finalize-plugin-state.js.map +1 -0
- package/dist/internal/merge-observers.d.ts +30 -0
- package/dist/internal/merge-observers.d.ts.map +1 -0
- package/dist/internal/merge-observers.js +27 -0
- package/dist/internal/merge-observers.js.map +1 -0
- package/dist/internal/pattern-utils.d.ts +33 -0
- package/dist/internal/pattern-utils.d.ts.map +1 -0
- package/dist/internal/pattern-utils.js +20 -0
- package/dist/internal/pattern-utils.js.map +1 -0
- package/dist/internal/ref-text.d.ts +11 -0
- package/dist/internal/ref-text.d.ts.map +1 -0
- package/dist/internal/ref-text.js +15 -0
- package/dist/internal/ref-text.js.map +1 -0
- package/dist/internal/session-runtime.d.ts +44 -0
- package/dist/internal/session-runtime.d.ts.map +1 -0
- package/dist/internal/session-runtime.js +143 -0
- package/dist/internal/session-runtime.js.map +1 -0
- package/dist/internal/watch-matcher.d.ts +56 -0
- package/dist/internal/watch-matcher.d.ts.map +1 -0
- package/dist/internal/watch-matcher.js +179 -0
- package/dist/internal/watch-matcher.js.map +1 -0
- package/dist/loader.d.ts +97 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +527 -0
- package/dist/loader.js.map +1 -0
- package/dist/observer-dispatcher.d.ts +69 -0
- package/dist/observer-dispatcher.d.ts.map +1 -0
- package/dist/observer-dispatcher.js +184 -0
- package/dist/observer-dispatcher.js.map +1 -0
- package/dist/plugin-merger.d.ts +175 -0
- package/dist/plugin-merger.d.ts.map +1 -0
- package/dist/plugin-merger.js +450 -0
- package/dist/plugin-merger.js.map +1 -0
- package/dist/plugins/git/branch-tracker.d.ts +85 -0
- package/dist/plugins/git/branch-tracker.d.ts.map +1 -0
- package/dist/plugins/git/branch-tracker.js +181 -0
- package/dist/plugins/git/branch-tracker.js.map +1 -0
- package/dist/plugins/git/cwd-extensions.d.ts +10 -0
- package/dist/plugins/git/cwd-extensions.d.ts.map +1 -0
- package/dist/plugins/git/cwd-extensions.js +155 -0
- package/dist/plugins/git/cwd-extensions.js.map +1 -0
- package/dist/plugins/git/git-ops.d.ts +89 -0
- package/dist/plugins/git/git-ops.d.ts.map +1 -0
- package/dist/plugins/git/git-ops.js +107 -0
- package/dist/plugins/git/git-ops.js.map +1 -0
- package/dist/plugins/git/index.d.ts +225 -0
- package/dist/plugins/git/index.d.ts.map +1 -0
- package/dist/plugins/git/index.js +52 -0
- package/dist/plugins/git/index.js.map +1 -0
- package/dist/plugins/git/predicates.d.ts +312 -0
- package/dist/plugins/git/predicates.d.ts.map +1 -0
- package/dist/plugins/git/predicates.js +580 -0
- package/dist/plugins/git/predicates.js.map +1 -0
- package/dist/plugins/git/rules.d.ts +209 -0
- package/dist/plugins/git/rules.d.ts.map +1 -0
- package/dist/plugins/git/rules.js +267 -0
- package/dist/plugins/git/rules.js.map +1 -0
- package/dist/schema.d.ts +1799 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +4 -0
- package/dist/schema.js.map +1 -0
- package/dist/testing/index.d.ts +621 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +747 -0
- package/dist/testing/index.js.map +1 -0
- package/examples/README.md +83 -0
- package/examples/combined-git-discipline/README.md +52 -0
- package/examples/combined-git-discipline/node_modules/.bin/pi-steering +43 -0
- package/examples/combined-git-discipline/package.json +18 -0
- package/examples/combined-git-discipline/steering.json +27 -0
- package/examples/combined-git-discipline/steering.test.ts +42 -0
- package/examples/combined-git-discipline/steering.ts +64 -0
- package/examples/combined-git-discipline/tsconfig.json +7 -0
- package/examples/draft-prs-only/README.md +42 -0
- package/examples/draft-prs-only/node_modules/.bin/pi-steering +43 -0
- package/examples/draft-prs-only/package.json +18 -0
- package/examples/draft-prs-only/steering.json +12 -0
- package/examples/draft-prs-only/steering.test.ts +48 -0
- package/examples/draft-prs-only/steering.ts +36 -0
- package/examples/draft-prs-only/tsconfig.json +7 -0
- package/examples/dynamic-reason-runtime-cwd/README.md +148 -0
- package/examples/dynamic-reason-runtime-cwd/node_modules/.bin/pi-steering +43 -0
- package/examples/dynamic-reason-runtime-cwd/package.json +18 -0
- package/examples/dynamic-reason-runtime-cwd/steering.test.ts +130 -0
- package/examples/dynamic-reason-runtime-cwd/steering.ts +82 -0
- package/examples/dynamic-reason-runtime-cwd/tsconfig.json +7 -0
- package/examples/force-push-strict/README.md +75 -0
- package/examples/force-push-strict/node_modules/.bin/pi-steering +43 -0
- package/examples/force-push-strict/package.json +18 -0
- package/examples/force-push-strict/steering.json +12 -0
- package/examples/force-push-strict/steering.test.ts +51 -0
- package/examples/force-push-strict/steering.ts +54 -0
- package/examples/force-push-strict/tsconfig.json +7 -0
- package/examples/no-amend/README.md +52 -0
- package/examples/no-amend/node_modules/.bin/pi-steering +43 -0
- package/examples/no-amend/package.json +18 -0
- package/examples/no-amend/steering.cwd-scoped.json +12 -0
- package/examples/no-amend/steering.json +11 -0
- package/examples/no-amend/steering.test.ts +49 -0
- package/examples/no-amend/steering.ts +38 -0
- package/examples/no-amend/tsconfig.json +7 -0
- package/examples/work-item-plugin/README.md +103 -0
- package/examples/work-item-plugin/node_modules/.bin/pi-steering +43 -0
- package/examples/work-item-plugin/package.json +19 -0
- package/examples/work-item-plugin/src/index.test.ts +168 -0
- package/examples/work-item-plugin/src/index.ts +122 -0
- package/examples/work-item-plugin/src/observers/npm-test-tracker.test.ts +95 -0
- package/examples/work-item-plugin/src/observers/npm-test-tracker.ts +88 -0
- package/examples/work-item-plugin/src/observers/retest-required-tracker.test.ts +81 -0
- package/examples/work-item-plugin/src/observers/retest-required-tracker.ts +81 -0
- package/examples/work-item-plugin/src/predicates/work-item-format.test.ts +157 -0
- package/examples/work-item-plugin/src/predicates/work-item-format.ts +99 -0
- package/examples/work-item-plugin/src/rules/commit-description-check.test.ts +107 -0
- package/examples/work-item-plugin/src/rules/commit-description-check.ts +83 -0
- package/examples/work-item-plugin/src/rules/commit-requires-work-item.test.ts +79 -0
- package/examples/work-item-plugin/src/rules/commit-requires-work-item.ts +56 -0
- package/examples/work-item-plugin/src/rules/push-requires-tests.test.ts +245 -0
- package/examples/work-item-plugin/src/rules/push-requires-tests.ts +62 -0
- package/examples/work-item-plugin/tsconfig.json +8 -0
- package/package.json +88 -0
- package/skills/steering-authoring/SKILL.md +102 -0
- package/src/INVARIANTS.md +116 -0
- package/src/__test-helpers__.ts +243 -0
- package/src/bin/pi-steering.test.ts +920 -0
- package/src/bin/pi-steering.ts +584 -0
- package/src/compat.test.ts +431 -0
- package/src/compat.ts +282 -0
- package/src/defaults.test.ts +585 -0
- package/src/defaults.ts +148 -0
- package/src/define-config.test.ts +888 -0
- package/src/define-config.ts +375 -0
- package/src/define-predicate.test.ts +70 -0
- package/src/define-predicate.ts +57 -0
- package/src/evaluator-internals/context.ts +290 -0
- package/src/evaluator-internals/override.test.ts +99 -0
- package/src/evaluator-internals/override.ts +83 -0
- package/src/evaluator-internals/predicates.ts +1037 -0
- package/src/evaluator-internals/speculative-synthesis.test.ts +411 -0
- package/src/evaluator-internals/speculative-synthesis.ts +199 -0
- package/src/evaluator.test.ts +6296 -0
- package/src/evaluator.ts +1106 -0
- package/src/factory-time-load.test.ts +630 -0
- package/src/helpers/walker-unknown-cwd-reason.test.ts +73 -0
- package/src/helpers/walker-unknown-cwd-reason.ts +83 -0
- package/src/index.test.ts +836 -0
- package/src/index.ts +229 -0
- package/src/internal/drop-unused-observers.test.ts +141 -0
- package/src/internal/drop-unused-observers.ts +81 -0
- package/src/internal/finalize-plugin-state.ts +43 -0
- package/src/internal/merge-observers.ts +51 -0
- package/src/internal/pattern-utils.test.ts +52 -0
- package/src/internal/pattern-utils.ts +38 -0
- package/src/internal/ref-text.test.ts +54 -0
- package/src/internal/ref-text.ts +17 -0
- package/src/internal/session-runtime.test.ts +629 -0
- package/src/internal/session-runtime.ts +201 -0
- package/src/internal/watch-matcher.test.ts +326 -0
- package/src/internal/watch-matcher.ts +200 -0
- package/src/jsdoc-propagation.test.ts +277 -0
- package/src/loader.test.ts +1098 -0
- package/src/loader.ts +581 -0
- package/src/not-block-onunknown.test.ts +554 -0
- package/src/observer-dispatcher.test.ts +939 -0
- package/src/observer-dispatcher.ts +290 -0
- package/src/per-predicate-typing.test.ts +361 -0
- package/src/plugin-merger.test.ts +790 -0
- package/src/plugin-merger.ts +598 -0
- package/src/plugins/git/README.md +485 -0
- package/src/plugins/git/branch-tracker.test.ts +281 -0
- package/src/plugins/git/branch-tracker.ts +187 -0
- package/src/plugins/git/cwd-extensions.test.ts +230 -0
- package/src/plugins/git/cwd-extensions.ts +160 -0
- package/src/plugins/git/git-ops.test.ts +397 -0
- package/src/plugins/git/git-ops.ts +173 -0
- package/src/plugins/git/index.ts +253 -0
- package/src/plugins/git/integration.test.ts +705 -0
- package/src/plugins/git/predicates.test.ts +1401 -0
- package/src/plugins/git/predicates.ts +693 -0
- package/src/plugins/git/rules.test.ts +1001 -0
- package/src/plugins/git/rules.ts +307 -0
- package/src/schema.test.ts +259 -0
- package/src/schema.ts +1963 -0
- package/src/testing/index.test.ts +1696 -0
- package/src/testing/index.ts +1472 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Part of pi-steering.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `defineConfig` — compile-time-typed config builder.
|
|
6
|
+
*
|
|
7
|
+
* Two supported authoring styles per the accepted ADR ("Design →
|
|
8
|
+
* `defineConfig` and compile-time inference"):
|
|
9
|
+
*
|
|
10
|
+
* 1. **`defineConfig`** — uses `const`-generics on plugins / observers
|
|
11
|
+
* to infer the union of observer names, then constrains
|
|
12
|
+
* {@link Rule.observer} string references to that union. Typos in
|
|
13
|
+
* `observer: "description-read"` (when the plugin registers
|
|
14
|
+
* `description-reads`) produce a compile error.
|
|
15
|
+
*
|
|
16
|
+
* 2. **`satisfies SteeringConfig`** — plain TypeScript construct users
|
|
17
|
+
* can fall back to when they don't want the generic inference
|
|
18
|
+
* complexity. Gets shape validation but no cross-reference name
|
|
19
|
+
* checking.
|
|
20
|
+
*
|
|
21
|
+
* The function itself does minimal runtime work — it just returns the
|
|
22
|
+
* config unchanged. All the value is in the types.
|
|
23
|
+
*
|
|
24
|
+
* Generics threaded through (ADR §8):
|
|
25
|
+
* - `AllObserverNames<P, Inline>` — for `Rule.observer` string refs.
|
|
26
|
+
* - `AllWrites<P, R, Inline>` — for `Rule.when.happened.event`.
|
|
27
|
+
* - `AllRuleNames<P, R>` — for `config.disabledRules`.
|
|
28
|
+
* - `AllPluginNames<P>` — for `config.disabledPlugins`.
|
|
29
|
+
*
|
|
30
|
+
* All four helpers are exported from this module but NOT re-exported
|
|
31
|
+
* from the package root; they're internal plumbing, not user-facing
|
|
32
|
+
* API. Stable enough that plugin authors who import them directly can
|
|
33
|
+
* rely on their shape within a single minor version, but the contract
|
|
34
|
+
* is "use via defineConfig".
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import type { DEFAULT_PLUGINS, DEFAULT_RULES } from "./defaults.ts";
|
|
38
|
+
import type {
|
|
39
|
+
BuiltInWhenLeavesOuter,
|
|
40
|
+
Observer,
|
|
41
|
+
Plugin,
|
|
42
|
+
Rule,
|
|
43
|
+
SteeringConfig,
|
|
44
|
+
} from "./schema.ts";
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Type-level plumbing: project `name` / `writes` literals off tuples of
|
|
48
|
+
// rules, observers, or plugins.
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Pull a single projection off every element of an array type.
|
|
53
|
+
*
|
|
54
|
+
* - `K = "name"` — value is the element's `name` literal
|
|
55
|
+
* (`{ name: N }` → `N`).
|
|
56
|
+
* - `K = "writes"` — value is each element of the element's
|
|
57
|
+
* `writes` tuple (`{ writes: readonly [..., S] }`
|
|
58
|
+
* → `S`).
|
|
59
|
+
*
|
|
60
|
+
* Elements missing the field (optional `writes`, widened `name`)
|
|
61
|
+
* contribute `never`. Non-tuple `T` inputs short-circuit to `never`.
|
|
62
|
+
*/
|
|
63
|
+
type ProjectField<
|
|
64
|
+
T,
|
|
65
|
+
K extends "name" | "writes",
|
|
66
|
+
> = T extends readonly (infer E)[]
|
|
67
|
+
? E extends Record<K, infer V>
|
|
68
|
+
? K extends "writes"
|
|
69
|
+
? V extends readonly (infer S extends string)[]
|
|
70
|
+
? S
|
|
71
|
+
: never
|
|
72
|
+
: V extends string
|
|
73
|
+
? V
|
|
74
|
+
: never
|
|
75
|
+
: never
|
|
76
|
+
: never;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Walk a tuple of plugins and union a {@link ProjectField} projection
|
|
80
|
+
* across every plugin's `Source` array (`"rules"` or `"observers"`).
|
|
81
|
+
*
|
|
82
|
+
* Replaces four near-identical recursive walkers that differed only in
|
|
83
|
+
* `(Source, K)` pair — see git blame for the pre-R2 shape.
|
|
84
|
+
*/
|
|
85
|
+
type FromPluginField<
|
|
86
|
+
P extends readonly Plugin[],
|
|
87
|
+
Source extends "rules" | "observers",
|
|
88
|
+
K extends "name" | "writes",
|
|
89
|
+
> = P extends readonly [infer First, ...infer Rest]
|
|
90
|
+
?
|
|
91
|
+
| (First extends Plugin
|
|
92
|
+
? First[Source] extends infer X
|
|
93
|
+
? X extends readonly (Rule | Observer)[]
|
|
94
|
+
? ProjectField<X, K>
|
|
95
|
+
: never
|
|
96
|
+
: never
|
|
97
|
+
: never)
|
|
98
|
+
| (Rest extends readonly Plugin[]
|
|
99
|
+
? FromPluginField<Rest, Source, K>
|
|
100
|
+
: never)
|
|
101
|
+
: never;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Extract the union of observer names registered across:
|
|
105
|
+
* - every plugin's `observers: Observer[]` array, AND
|
|
106
|
+
* - the top-level inline `observers: Observer[]` array.
|
|
107
|
+
*
|
|
108
|
+
* Used to constrain string references in {@link Rule.observer} so typos
|
|
109
|
+
* surface as compile errors in `defineConfig` call sites.
|
|
110
|
+
*
|
|
111
|
+
* Falls back to `never` when no observers are registered (correct:
|
|
112
|
+
* string references should be rejected entirely when there's nothing
|
|
113
|
+
* to reference).
|
|
114
|
+
*/
|
|
115
|
+
export type AllObserverNames<
|
|
116
|
+
P extends readonly Plugin[],
|
|
117
|
+
Inline extends readonly Observer[],
|
|
118
|
+
> = FromPluginField<P, "observers", "name"> | ProjectField<Inline, "name">;
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Default-rule + default-plugin name unions
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Union of {@link DEFAULT_RULES} `name` literals — the names of rules
|
|
126
|
+
* shipped by the engine itself. Folded into {@link AllRuleNames}; the
|
|
127
|
+
* live list is the `name` field of each entry in {@link DEFAULT_RULES}.
|
|
128
|
+
*
|
|
129
|
+
* Relies on {@link DEFAULT_RULES} being authored as
|
|
130
|
+
* `as const satisfies readonly Rule[]` so the literal `name` values
|
|
131
|
+
* survive through the array — a bare `Rule[]` annotation widens to
|
|
132
|
+
* `string` and collapses this union.
|
|
133
|
+
*/
|
|
134
|
+
export type DefaultRuleName = (typeof DEFAULT_RULES)[number]["name"];
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Union of {@link DEFAULT_PLUGINS} `name` literals. Folded into
|
|
138
|
+
* {@link AllPluginNames} so `disabledPlugins` typechecks against
|
|
139
|
+
* shipped defaults without a cast.
|
|
140
|
+
*/
|
|
141
|
+
export type DefaultPluginName = (typeof DEFAULT_PLUGINS)[number]["name"];
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// AllPluginNames — union of plugin `.name` literals across loaded plugins.
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Extract the union of plugin names registered in the `plugins` tuple,
|
|
149
|
+
* **plus** the names of {@link DEFAULT_PLUGINS} (those plugins ship
|
|
150
|
+
* pre-loaded by the engine and `disabledPlugins` honors them at
|
|
151
|
+
* runtime).
|
|
152
|
+
*
|
|
153
|
+
* Used to constrain {@link SteeringConfig.disabledPlugins} so typos
|
|
154
|
+
* surface as compile errors. Default-plugin names are always part of
|
|
155
|
+
* this union, even when the user passes no `plugins` themselves.
|
|
156
|
+
*
|
|
157
|
+
* Falls back to just {@link DefaultPluginName} when no user plugins
|
|
158
|
+
* are registered — typing `disabledPlugins` against an empty
|
|
159
|
+
* user-tuple still accepts the engine defaults.
|
|
160
|
+
*/
|
|
161
|
+
export type AllPluginNames<P extends readonly Plugin[]> =
|
|
162
|
+
| DefaultPluginName
|
|
163
|
+
| ProjectField<P, "name">;
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// AllRuleNames — union of rule `.name` literals across plugins + user rules.
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Extract the union of rule names across:
|
|
171
|
+
* - every {@link DEFAULT_RULES} entry (engine-shipped defaults),
|
|
172
|
+
* - every plugin's `rules: Rule[]` array, AND
|
|
173
|
+
* - the top-level inline `rules: Rule[]` array.
|
|
174
|
+
*
|
|
175
|
+
* Used to constrain {@link SteeringConfig.disabledRules} so typos
|
|
176
|
+
* surface as compile errors. Default rule names are always part of
|
|
177
|
+
* this union — disabling a default (`disabledRules: ["no-force-push"]`)
|
|
178
|
+
* typechecks the same as disabling a user or plugin rule.
|
|
179
|
+
*
|
|
180
|
+
* Falls back to just {@link DefaultRuleName} when no plugin or user
|
|
181
|
+
* rules are registered.
|
|
182
|
+
*/
|
|
183
|
+
export type AllRuleNames<
|
|
184
|
+
P extends readonly Plugin[],
|
|
185
|
+
R extends readonly Rule[],
|
|
186
|
+
> =
|
|
187
|
+
| DefaultRuleName
|
|
188
|
+
| FromPluginField<P, "rules", "name">
|
|
189
|
+
| ProjectField<R, "name">;
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// AllWrites — union of `writes[]` literals across rules + observers.
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Extract the union of session-entry custom types declared via `writes`
|
|
197
|
+
* arrays across:
|
|
198
|
+
* - every plugin's `rules: Rule[]` (rule-side writes via `onFire`),
|
|
199
|
+
* - every plugin's `observers: Observer[]` (observer-side writes),
|
|
200
|
+
* - the top-level inline `rules`, AND
|
|
201
|
+
* - the top-level inline `observers`.
|
|
202
|
+
*
|
|
203
|
+
* Used to constrain {@link BuiltInWhenLeavesOuter.happened} `event` so typos
|
|
204
|
+
* (e.g., `happened: { event: "sync-don" }` when the observer writes
|
|
205
|
+
* `"sync-done"`) surface as compile errors.
|
|
206
|
+
*
|
|
207
|
+
* Authors who omit `writes` on a rule/observer don't contribute to the
|
|
208
|
+
* union — the rule's write is undeclared, and any downstream
|
|
209
|
+
* `when.happened.event` referencing it will be rejected. Matches the
|
|
210
|
+
* "declare your writes" discipline that `writes[]` encourages.
|
|
211
|
+
*/
|
|
212
|
+
export type AllWrites<
|
|
213
|
+
P extends readonly Plugin[],
|
|
214
|
+
R extends readonly Rule[],
|
|
215
|
+
Inline extends readonly Observer[] = readonly [],
|
|
216
|
+
> =
|
|
217
|
+
| FromPluginField<P, "rules", "writes">
|
|
218
|
+
| FromPluginField<P, "observers", "writes">
|
|
219
|
+
| ProjectField<R, "writes">
|
|
220
|
+
| ProjectField<Inline, "writes">;
|
|
221
|
+
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
// DefineConfigInput
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Config author surface — the shape `defineConfig` accepts. Matches
|
|
228
|
+
* {@link SteeringConfig} but with `const`-generic tuple slots on
|
|
229
|
+
* `plugins` / `rules` / `observers` so tuple literal types survive
|
|
230
|
+
* through the call and drive name inference.
|
|
231
|
+
*
|
|
232
|
+
* Generic constraints:
|
|
233
|
+
* - `disabledRules` / `disabledPlugins` typed against the rule / plugin
|
|
234
|
+
* name unions — typos rejected at compile time.
|
|
235
|
+
* - `rules[].when.happened.event` and `rules[].when.happened.since`
|
|
236
|
+
* are both typed against `AllWrites` — typos rejected at compile
|
|
237
|
+
* time.
|
|
238
|
+
*/
|
|
239
|
+
export interface DefineConfigInput<
|
|
240
|
+
P extends readonly Plugin[],
|
|
241
|
+
Inline extends readonly Observer[],
|
|
242
|
+
R extends readonly Rule<
|
|
243
|
+
AllObserverNames<P, Inline>,
|
|
244
|
+
AllWrites<P, R, Inline>
|
|
245
|
+
>[],
|
|
246
|
+
> extends SteeringConfig {
|
|
247
|
+
disabledRules?: readonly AllRuleNames<P, R>[];
|
|
248
|
+
disabledPlugins?: readonly AllPluginNames<P>[];
|
|
249
|
+
plugins?: P;
|
|
250
|
+
rules?: R;
|
|
251
|
+
observers?: Inline;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Build a {@link SteeringConfig} with cross-reference name checking.
|
|
256
|
+
*
|
|
257
|
+
* Observer references in {@link Rule.observer} are typed against the
|
|
258
|
+
* union of observer names gathered from `plugins[*].observers` AND the
|
|
259
|
+
* top-level `observers` array — a typo produces a compile error.
|
|
260
|
+
*
|
|
261
|
+
* The `disabledRules` / `disabledPlugins` arrays are typed against the unions
|
|
262
|
+
* of registered rule / plugin names — typos rejected.
|
|
263
|
+
*
|
|
264
|
+
* `rules[].when.happened.event` and `rules[].when.happened.since` are
|
|
265
|
+
* both typed against the union of all `writes` declarations across
|
|
266
|
+
* plugin rules, plugin observers, user rules, and user observers —
|
|
267
|
+
* typos rejected. (The `since` field on the `Writes` union enforces
|
|
268
|
+
* the same contract as `event`: the sentinel event must be known to
|
|
269
|
+
* the config, not a free-form string.)
|
|
270
|
+
*
|
|
271
|
+
* Runtime behavior: returns a shallow copy of the input with optional
|
|
272
|
+
* fields normalized from `readonly` arrays to mutable arrays (the
|
|
273
|
+
* {@link SteeringConfig} shape doesn't constrain mutability). The
|
|
274
|
+
* return value is safe to pass to the loader / buildConfig.
|
|
275
|
+
*
|
|
276
|
+
* ## Authoring pattern — preserving observer/plugin names for inference
|
|
277
|
+
*
|
|
278
|
+
* For compile-time typo detection on rule `observer` references, declare
|
|
279
|
+
* your observers and plugins with `as const satisfies` so TypeScript
|
|
280
|
+
* preserves the literal `name` values through to `AllObserverNames`:
|
|
281
|
+
*
|
|
282
|
+
* const myObs = {
|
|
283
|
+
* name: "description-read",
|
|
284
|
+
* onResult: (event, ctx) => { ... },
|
|
285
|
+
* } as const satisfies Observer;
|
|
286
|
+
*
|
|
287
|
+
* const myPlugin = {
|
|
288
|
+
* name: "my-plugin",
|
|
289
|
+
* observers: [{ name: "sync-done", onResult: ... }],
|
|
290
|
+
* } as const satisfies Plugin;
|
|
291
|
+
*
|
|
292
|
+
* Authors who prefer type annotations (`const myObs: Observer = ...`)
|
|
293
|
+
* get widened `name: string`, which collapses `AllObserverNames` to
|
|
294
|
+
* `string` and silently disables typo detection. Use `as const satisfies`
|
|
295
|
+
* to keep the inference.
|
|
296
|
+
*
|
|
297
|
+
* ## Behavior with no observers declared
|
|
298
|
+
*
|
|
299
|
+
* When no plugins contribute observers AND no inline `observers[]` is
|
|
300
|
+
* passed, `AllObserverNames` resolves to `never`, which causes ANY
|
|
301
|
+
* string `observer` reference on a Rule to be a compile error. This is
|
|
302
|
+
* deliberate — fail-closed on unknown observer names. For configs that
|
|
303
|
+
* deliberately reference observers by name without registering them
|
|
304
|
+
* inline (e.g., deferred to runtime), use `satisfies SteeringConfig`
|
|
305
|
+
* as a fallback; you lose typo detection but regain flexibility.
|
|
306
|
+
*
|
|
307
|
+
* ## Hover ergonomics for plugin-predicate JSDoc
|
|
308
|
+
*
|
|
309
|
+
* The `const R extends readonly Rule[]` signature narrows the
|
|
310
|
+
* contextual type of inline rule literals to their `const`-inferred
|
|
311
|
+
* shape, bypassing the homomorphic mapped-type linkage that surfaces
|
|
312
|
+
* source-declared JSDoc on hover (e.g. on `when.isClean:`). Factor
|
|
313
|
+
* rules out into `const myRule = { ... } as const satisfies Rule`
|
|
314
|
+
* bindings before passing them to `defineConfig` to keep the
|
|
315
|
+
* hover-rich shape; see the
|
|
316
|
+
* `examples/dynamic-reason-runtime-cwd/steering.ts` example. The
|
|
317
|
+
* `as const` modifier on the binding (and the `const R` modifier on
|
|
318
|
+
* the signature) preserves each rule's literal `name` so
|
|
319
|
+
* `disabledRules` typo detection fires — the alternatives `: Rule`
|
|
320
|
+
* and bare `satisfies Rule` restore hover but widen the inferred
|
|
321
|
+
* type and collapse typo detection (and `when.happened.event`
|
|
322
|
+
* narrowing across declared `writes`).
|
|
323
|
+
*
|
|
324
|
+
* @example
|
|
325
|
+
* export default defineConfig({
|
|
326
|
+
* plugins: [gitPlugin],
|
|
327
|
+
* observers: [descriptionReadObserver],
|
|
328
|
+
* rules: [
|
|
329
|
+
* { name: "must-read-docs", ..., observer: "description-read" },
|
|
330
|
+
* ],
|
|
331
|
+
* });
|
|
332
|
+
*/
|
|
333
|
+
export function defineConfig<
|
|
334
|
+
const P extends readonly Plugin[] = [],
|
|
335
|
+
const Inline extends readonly Observer[] = [],
|
|
336
|
+
const R extends readonly Rule<
|
|
337
|
+
AllObserverNames<P, Inline>,
|
|
338
|
+
AllWrites<P, R, Inline>
|
|
339
|
+
>[] = [],
|
|
340
|
+
>(config: DefineConfigInput<P, Inline, R>): SteeringConfig {
|
|
341
|
+
// Runtime work is minimal: copy the supplied config, widening the
|
|
342
|
+
// `readonly` tuple slots back to plain arrays for downstream
|
|
343
|
+
// consumers (loader, evaluator) that don't care about the tuple
|
|
344
|
+
// literal types. The generic machinery's job is done at the call
|
|
345
|
+
// site — once we return, we return plain SteeringConfig.
|
|
346
|
+
const out: SteeringConfig = {};
|
|
347
|
+
if (config.defaultNoOverride !== undefined) {
|
|
348
|
+
out.defaultNoOverride = config.defaultNoOverride;
|
|
349
|
+
}
|
|
350
|
+
if (config.disabledRules !== undefined) {
|
|
351
|
+
out.disabledRules = [...config.disabledRules];
|
|
352
|
+
}
|
|
353
|
+
if (config.disabledPlugins !== undefined) {
|
|
354
|
+
out.disabledPlugins = [...config.disabledPlugins];
|
|
355
|
+
}
|
|
356
|
+
if (config.disableDefaults !== undefined) {
|
|
357
|
+
out.disableDefaults = config.disableDefaults;
|
|
358
|
+
}
|
|
359
|
+
if (config.failOnWarnings !== undefined) {
|
|
360
|
+
out.failOnWarnings = config.failOnWarnings;
|
|
361
|
+
}
|
|
362
|
+
if (config.plugins !== undefined) {
|
|
363
|
+
// Cast: `readonly Plugin[]` → `Plugin[]` (shape is identical;
|
|
364
|
+
// the loader never mutates the array, but SteeringConfig
|
|
365
|
+
// doesn't require readonly).
|
|
366
|
+
out.plugins = [...config.plugins];
|
|
367
|
+
}
|
|
368
|
+
if (config.rules !== undefined) {
|
|
369
|
+
out.rules = [...config.rules] as Rule[];
|
|
370
|
+
}
|
|
371
|
+
if (config.observers !== undefined) {
|
|
372
|
+
out.observers = [...config.observers];
|
|
373
|
+
}
|
|
374
|
+
return out;
|
|
375
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Part of pi-steering.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tests for `definePredicate<T>` — shape smoke + typed-argument flow.
|
|
6
|
+
*
|
|
7
|
+
* `definePredicate` is purely type-level sugar; at runtime it returns
|
|
8
|
+
* its handler unchanged. These tests pin the runtime contract (call
|
|
9
|
+
* with typed args gets through unchanged) and the type signature by
|
|
10
|
+
* constructing examples that would fail to compile if the generic
|
|
11
|
+
* were mishandled.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { describe, it } from "node:test";
|
|
16
|
+
import { definePredicate } from "./define-predicate.ts";
|
|
17
|
+
import type { Plugin, PredicateContext, PredicateHandler } from "./schema.ts";
|
|
18
|
+
|
|
19
|
+
describe("definePredicate", () => {
|
|
20
|
+
it("returns the handler unchanged (pure pass-through)", () => {
|
|
21
|
+
const handler = (_args: { pattern: RegExp }, _ctx: PredicateContext) =>
|
|
22
|
+
true;
|
|
23
|
+
const wrapped = definePredicate<{ pattern: RegExp }>(handler);
|
|
24
|
+
assert.equal(wrapped, handler as unknown);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("resulting handler is assignable into Plugin.predicates", () => {
|
|
28
|
+
interface CommitFormatArgs {
|
|
29
|
+
pattern: RegExp;
|
|
30
|
+
onUnknown?: "allow" | "block";
|
|
31
|
+
}
|
|
32
|
+
const commitFormat = definePredicate<CommitFormatArgs>((args, _ctx) =>
|
|
33
|
+
args.pattern.test("conventional: subject"),
|
|
34
|
+
);
|
|
35
|
+
// If definePredicate returned PredicateHandler<CommitFormatArgs>
|
|
36
|
+
// instead of the loose PredicateHandler, this assignment would
|
|
37
|
+
// fail because Plugin.predicates is keyed with the loose
|
|
38
|
+
// handler type.
|
|
39
|
+
const plugin: Plugin = {
|
|
40
|
+
name: "git",
|
|
41
|
+
predicates: { commitFormat },
|
|
42
|
+
};
|
|
43
|
+
assert.equal(plugin.name, "git");
|
|
44
|
+
assert.equal(typeof plugin.predicates?.commitFormat, "function");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("handler still invokes with typed args at runtime", async () => {
|
|
48
|
+
interface Args {
|
|
49
|
+
flag: string;
|
|
50
|
+
}
|
|
51
|
+
let sawArgs: Args | null = null;
|
|
52
|
+
const handler = definePredicate<Args>((args, _ctx) => {
|
|
53
|
+
sawArgs = args;
|
|
54
|
+
return args.flag === "yes";
|
|
55
|
+
});
|
|
56
|
+
const loose = handler as PredicateHandler;
|
|
57
|
+
const ctx: PredicateContext = {
|
|
58
|
+
cwd: "/",
|
|
59
|
+
tool: "bash",
|
|
60
|
+
input: { tool: "bash", command: "" },
|
|
61
|
+
agentLoopIndex: 0,
|
|
62
|
+
exec: async () => ({ stdout: "", stderr: "", exitCode: 0 }),
|
|
63
|
+
appendEntry: () => {},
|
|
64
|
+
findEntries: () => [],
|
|
65
|
+
};
|
|
66
|
+
const r = await loose({ flag: "yes" }, ctx);
|
|
67
|
+
assert.equal(r, true);
|
|
68
|
+
assert.deepEqual(sawArgs, { flag: "yes" });
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Part of pi-steering.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `definePredicate<T>` — ~5-LOC helper for declaring typed plugin
|
|
6
|
+
* predicate handlers.
|
|
7
|
+
*
|
|
8
|
+
* Parallels {@link defineConfig} — pure pass-through at runtime, all
|
|
9
|
+
* the value is in the type signature. Narrows the handler's first
|
|
10
|
+
* argument to the supplied type parameter `T` so the body reads like
|
|
11
|
+
* a plain function on typed args, without the author needing to
|
|
12
|
+
* write the annotation twice (once on the declaration, once on the
|
|
13
|
+
* handler's `args` parameter). The `PredicateHandler<T>` return
|
|
14
|
+
* preserves that narrowing when the result is assigned to a local
|
|
15
|
+
* variable; the {@link Plugin.predicates} registry slot then accepts
|
|
16
|
+
* it cast-free via {@link AnyPredicateHandler} (= `PredicateHandler<any>`),
|
|
17
|
+
* which uses TS bivariance to admit typed handlers directly.
|
|
18
|
+
*
|
|
19
|
+
* See ADR §10 for the motivating usage.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { PredicateHandler } from "./schema.ts";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Sugar for declaring a typed {@link PredicateHandler}. The handler
|
|
26
|
+
* is returned unchanged at runtime; the generic parameter `T`
|
|
27
|
+
* narrows the handler's `args` parameter to the author's intended
|
|
28
|
+
* shape. Return type is `PredicateHandler<T>`, so authors threading
|
|
29
|
+
* the result through local variables keep the narrowed arg type.
|
|
30
|
+
* The registry slot at {@link Plugin.predicates} accepts the result
|
|
31
|
+
* cast-free via {@link AnyPredicateHandler}.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* interface CommitFormatArgs {
|
|
35
|
+
* pattern: RegExp;
|
|
36
|
+
* onUnknown?: "allow" | "block";
|
|
37
|
+
* }
|
|
38
|
+
*
|
|
39
|
+
* export const commitFormat = definePredicate<CommitFormatArgs>(
|
|
40
|
+
* (args, ctx) => {
|
|
41
|
+
* // `args` is narrowed to CommitFormatArgs here.
|
|
42
|
+
* const msg = extractCommitMessage(ctx.input.args ?? []);
|
|
43
|
+
* return args.pattern.test(msg);
|
|
44
|
+
* },
|
|
45
|
+
* );
|
|
46
|
+
*
|
|
47
|
+
* // Plug into a plugin:
|
|
48
|
+
* export const gitPlugin: Plugin = {
|
|
49
|
+
* name: "git",
|
|
50
|
+
* predicates: { commitFormat },
|
|
51
|
+
* };
|
|
52
|
+
*/
|
|
53
|
+
export function definePredicate<T>(
|
|
54
|
+
handler: PredicateHandler<T>,
|
|
55
|
+
): PredicateHandler<T> {
|
|
56
|
+
return handler;
|
|
57
|
+
}
|