@gr8ful/spf 0.3.0 → 0.5.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/README.md +177 -9
- package/assets/defaults/spf.config.yaml +22 -0
- package/assets/prompts/refiner/system.md +53 -0
- package/assets/prompts/refiner/user.md +70 -0
- package/assets/prompts/reviewer/system.md +1 -1
- package/assets/skill/SKILL.md +1 -0
- package/assets/skill/cookbooks/authoring_chains.md +90 -7
- package/assets/skill/cookbooks/ocr_reviewer.md +196 -0
- package/assets/skill/cookbooks/roster.md +15 -4
- package/assets/skill/cookbooks/spf_overview.md +1 -0
- package/assets/skill/references/config.md +104 -3
- package/assets/skill/references/observability.md +11 -2
- package/assets/templates/ts-cc.spf.config.yaml +3 -3
- package/assets/templates/ts-flue-ollama.spf.config.yaml +67 -0
- package/assets/templates/ts.spf.config.yaml +15 -2
- package/dist/chains/context.d.ts +39 -0
- package/dist/chains/index.d.ts +94 -10
- package/dist/chains/index.js +75 -5
- package/dist/chains/repo_chains.d.ts +139 -0
- package/dist/chains/repo_chains.js +428 -0
- package/dist/chains/simple_sdlc.d.ts +74 -1
- package/dist/chains/simple_sdlc.js +134 -4
- package/dist/chains/steps.d.ts +237 -18
- package/dist/chains/steps.js +477 -58
- package/dist/cli/ask.d.ts +14 -1
- package/dist/cli/ask.js +32 -2
- package/dist/cli/commands/doctor.d.ts +1 -1
- package/dist/cli/commands/doctor.js +324 -10
- package/dist/cli/commands/init.d.ts +12 -0
- package/dist/cli/commands/init.js +108 -4
- package/dist/cli/commands/install-skill.js +5 -2
- package/dist/cli/commands/list.js +43 -5
- package/dist/cli/commands/run.js +29 -2
- package/dist/cli/commands/watch.d.ts +18 -0
- package/dist/cli/commands/watch.js +214 -16
- package/dist/cli/index.js +63 -6
- package/dist/cli/interview.js +81 -9
- package/dist/core/agent_cc.d.ts +40 -1
- package/dist/core/agent_cc.js +51 -4
- package/dist/core/agent_flue.js +28 -4
- package/dist/core/agents.d.ts +8 -0
- package/dist/core/agents.js +43 -3
- package/dist/core/data_types.d.ts +182 -4
- package/dist/core/data_types.js +141 -2
- package/dist/core/gates.d.ts +13 -0
- package/dist/core/gates.js +103 -0
- package/dist/core/git_helper.d.ts +29 -0
- package/dist/core/git_helper.js +41 -1
- package/dist/core/issues/github_provider.d.ts +35 -9
- package/dist/core/issues/github_provider.js +76 -28
- package/dist/core/issues/jira_provider.d.ts +14 -1
- package/dist/core/issues/jira_provider.js +9 -7
- package/dist/core/issues/provider.d.ts +77 -15
- package/dist/core/issues/provider.js +7 -4
- package/dist/core/notify/channel.d.ts +1 -1
- package/dist/core/ollama_provider.d.ts +70 -0
- package/dist/core/ollama_provider.js +208 -0
- package/dist/core/otel.d.ts +352 -0
- package/dist/core/otel.js +793 -0
- package/dist/core/providers.js +4 -0
- package/dist/core/refine.d.ts +39 -0
- package/dist/core/refine.js +152 -0
- package/dist/core/session.js +39 -2
- package/dist/core/tracer.d.ts +31 -2
- package/dist/core/tracer.js +69 -11
- package/dist/core/watch.d.ts +67 -1
- package/dist/core/watch.js +217 -13
- package/dist/test/chains.test.js +9 -3
- package/dist/test/data_types.test.js +140 -2
- package/dist/test/git_helper.test.d.ts +1 -0
- package/dist/test/git_helper.test.js +59 -0
- package/dist/test/hermetic_git.d.ts +1 -0
- package/dist/test/hermetic_git.js +22 -0
- package/dist/test/init_command.test.d.ts +14 -1
- package/dist/test/init_command.test.js +71 -1
- package/dist/test/interview.test.d.ts +15 -1
- package/dist/test/interview.test.js +131 -3
- package/dist/test/ollama_provider.test.d.ts +1 -0
- package/dist/test/ollama_provider.test.js +103 -0
- package/dist/test/otel.test.d.ts +26 -0
- package/dist/test/otel.test.js +512 -0
- package/dist/test/refine.test.d.ts +1 -0
- package/dist/test/refine.test.js +189 -0
- package/dist/test/repo_chains.test.d.ts +21 -0
- package/dist/test/repo_chains.test.js +416 -0
- package/dist/test/signoff.test.d.ts +1 -0
- package/dist/test/signoff.test.js +329 -0
- package/dist/test/ui_server.test.d.ts +7 -1
- package/dist/test/ui_server.test.js +1 -0
- package/dist/test/watch.test.js +297 -6
- package/package.json +5 -5
package/dist/chains/index.d.ts
CHANGED
|
@@ -1,14 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The chain registry — the one place a CLI-facing short name
|
|
3
|
-
* chain
|
|
4
|
-
*
|
|
2
|
+
* The chain registry — the one place a CLI-facing short name resolves to a
|
|
3
|
+
* chain. `spf list` reads it; `spf <name>`/`spf run <name>` dispatch through
|
|
4
|
+
* it. Replaces guessing a module filename from the CLI argument.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `
|
|
9
|
-
*
|
|
10
|
-
* `
|
|
11
|
-
*
|
|
6
|
+
* TWO TIERS, and the distinction is load-bearing:
|
|
7
|
+
*
|
|
8
|
+
* 1. BUILT-INS — `CHAINS`, below. Compiled in, always present, the same in
|
|
9
|
+
* every repo spf is ever run against. Every one but `simple-sdlc` is a
|
|
10
|
+
* flat `steps` list composed from `./steps.ts`'s primitives — no module
|
|
11
|
+
* of its own, and no hand-written `phases`/`requiredAgents`/
|
|
12
|
+
* `requiredSuites` to keep in sync with what actually runs; those are
|
|
13
|
+
* derived from the step list itself. `simple-sdlc`'s three-commit,
|
|
14
|
+
* pinned-baseline shape is still imperative — see `./simple_sdlc.ts` —
|
|
15
|
+
* via the `run` escape hatch below.
|
|
16
|
+
* 2. REPO-LOCAL CHAINS — registered at CLI startup from
|
|
17
|
+
* `<repo>/.spf/chains/*.yaml` by `registerRepoChains()`, having been
|
|
18
|
+
* read by `./repo_chains.ts`. These are DATA: a yaml file names built-in
|
|
19
|
+
* step factories and passes them params. It cannot introduce code, and
|
|
20
|
+
* it runs on the same `steps.runSteps()` driver as a built-in, because
|
|
21
|
+
* "agent proposes, code disposes" only means anything if the code that
|
|
22
|
+
* disposes is SPF's code — not the target repo's.
|
|
23
|
+
*
|
|
24
|
+
* `CHAINS` stays a built-in-only const: nothing ever pushes into it, and
|
|
25
|
+
* `src/test/chains.test.ts` pins its exact name set. Repo chains live in a
|
|
26
|
+
* separate module-level list that `registerRepoChains()` REPLACES (never
|
|
27
|
+
* appends to), so a second registration — a long-lived process, `spf watch`
|
|
28
|
+
* re-reading after an edit, a test — cannot double-register a name. Read
|
|
29
|
+
* both tiers through `allChains()`; built-ins always come first, and
|
|
30
|
+
* `findChain()` resolves them first, so a built-in name can never be
|
|
31
|
+
* shadowed by a file on disk.
|
|
12
32
|
*/
|
|
13
33
|
import type { ChainContext } from "./context.ts";
|
|
14
34
|
import * as steps from "./steps.ts";
|
|
@@ -19,15 +39,79 @@ export interface ChainDefinition {
|
|
|
19
39
|
phases: string;
|
|
20
40
|
/** Static for most chains; prompt's depends on --agent, so it's a function there. */
|
|
21
41
|
requiredAgents: string[] | ((options: Record<string, string>) => string[]);
|
|
22
|
-
|
|
42
|
+
/** Static unless a step's suite can be overridden by --suite (qualityCheck/fixLoop), in which case it's a function — see steps.deriveRequiredSuites. */
|
|
43
|
+
requiredSuites: string[] | ((options: Record<string, string>) => string[]);
|
|
23
44
|
/** The declarative path — a flat step list, run by runChain() via steps.runSteps(). */
|
|
24
45
|
steps?: steps.Step[];
|
|
25
46
|
/** The imperative escape hatch for a chain too shaped by its own logic to be a flat list (simple-sdlc). */
|
|
26
47
|
run?: (ctx: ChainContext, options?: Record<string, string>) => Promise<number>;
|
|
48
|
+
/**
|
|
49
|
+
* Absolute path of the `.spf/chains/*.yaml` this chain was loaded from —
|
|
50
|
+
* set only for a repo-local chain, `undefined` for a built-in (a built-in
|
|
51
|
+
* has no source file: it IS spf). The CLI copies it into
|
|
52
|
+
* `ChainContext.chain_source` so the run's trace records whose definition
|
|
53
|
+
* ran, and `spf list`/`spf doctor` use its presence to mark which chains
|
|
54
|
+
* came from the repo.
|
|
55
|
+
*/
|
|
56
|
+
source?: string;
|
|
27
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Build a step-based ChainDefinition, deriving phases/requiredAgents/
|
|
60
|
+
* requiredSuites from its steps.
|
|
61
|
+
*
|
|
62
|
+
* Exported so `./repo_chains.ts` builds a yaml-defined chain through the
|
|
63
|
+
* EXACT same function as a built-in. That is deliberate and should not be
|
|
64
|
+
* "simplified" into a second, parallel constructor: a repo chain must be
|
|
65
|
+
* indistinguishable downstream — same derivation of phases/agents/suites,
|
|
66
|
+
* same `steps` array, same `runSteps` driver, one run path — or the
|
|
67
|
+
* guarantees the built-ins are tested for stop applying to it.
|
|
68
|
+
*/
|
|
69
|
+
export declare function stepChain(name: string, describe: string, list: steps.Step[]): ChainDefinition;
|
|
28
70
|
export declare const CHAINS: ChainDefinition[];
|
|
71
|
+
/**
|
|
72
|
+
* Every built-in name, reserved. `repo_chains.ts` checks against this to
|
|
73
|
+
* refuse a repo chain that reuses one — a collision is reported as a
|
|
74
|
+
* problem, never resolved by shadowing either way round.
|
|
75
|
+
*/
|
|
76
|
+
export declare const BUILTIN_CHAIN_NAMES: ReadonlySet<string>;
|
|
77
|
+
/**
|
|
78
|
+
* Install the repo-local chains (and the problems found reading them) for
|
|
79
|
+
* this process. Call once, early, from the CLI entry point.
|
|
80
|
+
*
|
|
81
|
+
* REPLACES rather than appends, so it is idempotent: calling it twice with
|
|
82
|
+
* the same input leaves exactly one copy of each chain. That matters because
|
|
83
|
+
* a long-running `spf watch` may re-read `.spf/chains/` after an edit, and
|
|
84
|
+
* because a partial second registration (say, one file fixed) must not leave
|
|
85
|
+
* the previous file's stale definition behind.
|
|
86
|
+
*
|
|
87
|
+
* The `problems` list is stored, not thrown and not printed: loading is
|
|
88
|
+
* never allowed to fail a command that has nothing to do with the broken
|
|
89
|
+
* file (`spf trace`, `spf sessions`, `spf --version` must all still work in
|
|
90
|
+
* a repo with one malformed yaml). `spf list` and `spf doctor` surface them;
|
|
91
|
+
* dispatch surfaces them when the name someone typed is missing.
|
|
92
|
+
*/
|
|
93
|
+
export declare function registerRepoChains(chains: ChainDefinition[], problems: {
|
|
94
|
+
file: string;
|
|
95
|
+
message: string;
|
|
96
|
+
}[]): void;
|
|
97
|
+
/** Built-ins first, then repo-local chains — the order `spf list` prints and `findChain` resolves in. */
|
|
98
|
+
export declare function allChains(): ChainDefinition[];
|
|
99
|
+
/** What went wrong reading `.spf/chains/*.yaml`, for `spf list`/`spf doctor` to report. */
|
|
100
|
+
export declare function repoChainProblems(): {
|
|
101
|
+
file: string;
|
|
102
|
+
message: string;
|
|
103
|
+
}[];
|
|
104
|
+
/**
|
|
105
|
+
* Resolve a CLI-facing name. Built-ins are consulted FIRST and win
|
|
106
|
+
* unconditionally — a repo chain that reuses a built-in name is rejected at
|
|
107
|
+
* load time (see `repo_chains.ts`), so this ordering is a second line of
|
|
108
|
+
* defence rather than the policy itself, and it is what makes `spf build` in
|
|
109
|
+
* any repo mean what the docs say it means.
|
|
110
|
+
*/
|
|
29
111
|
export declare function findChain(name: string): ChainDefinition | undefined;
|
|
30
112
|
export declare function resolveRequiredAgents(chain: ChainDefinition, options: Record<string, string>): string[];
|
|
113
|
+
/** Same idea as resolveRequiredAgents(), for the suite(s) --suite can override before validate() ever runs. */
|
|
114
|
+
export declare function resolveRequiredSuites(chain: ChainDefinition, options: Record<string, string>): string[];
|
|
31
115
|
/**
|
|
32
116
|
* Run a chain, whichever path it defines: `run` (the imperative escape
|
|
33
117
|
* hatch) if it has one, otherwise `steps` through the shared driver. Both
|
package/dist/chains/index.js
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import * as steps from "./steps.js";
|
|
2
2
|
import * as simpleSdlc from "./simple_sdlc.js";
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Build a step-based ChainDefinition, deriving phases/requiredAgents/
|
|
5
|
+
* requiredSuites from its steps.
|
|
6
|
+
*
|
|
7
|
+
* Exported so `./repo_chains.ts` builds a yaml-defined chain through the
|
|
8
|
+
* EXACT same function as a built-in. That is deliberate and should not be
|
|
9
|
+
* "simplified" into a second, parallel constructor: a repo chain must be
|
|
10
|
+
* indistinguishable downstream — same derivation of phases/agents/suites,
|
|
11
|
+
* same `steps` array, same `runSteps` driver, one run path — or the
|
|
12
|
+
* guarantees the built-ins are tested for stop applying to it.
|
|
13
|
+
*/
|
|
14
|
+
export function stepChain(name, describe, list) {
|
|
5
15
|
return {
|
|
6
16
|
name,
|
|
7
17
|
describe,
|
|
@@ -54,23 +64,83 @@ export const CHAINS = [
|
|
|
54
64
|
steps.qualityCheck({ suite: "all" }),
|
|
55
65
|
]),
|
|
56
66
|
stepChain("document", "write up the work that was just done, from the diff", [steps.request(), steps.changes(), steps.document()]),
|
|
67
|
+
stepChain("refine", "decompose a product spec into a feature/story-or-bug tree of tracker issues — spf watch's spec-ready lane", [
|
|
68
|
+
steps.request(),
|
|
69
|
+
steps.refine(),
|
|
70
|
+
steps.publishIssues(),
|
|
71
|
+
]),
|
|
57
72
|
{
|
|
58
73
|
name: "simple-sdlc",
|
|
59
74
|
describe: "the work is real and its shape is not obvious — plan, build, test, review, document; 3 commits",
|
|
60
75
|
phases: "engineer(request) -> planner -> git(commit_plan) -> builder -> code(test) [-> builder(fix) -> code(test) ...] " +
|
|
61
|
-
"-> reviewer [-> builder(revise) -> reviewer ...] -> code(retest, if revised) -> git(commit_build) " +
|
|
76
|
+
"-> reviewer [-> builder(revise) -> reviewer ...] -> code(retest, if revised) -> engineer(signoff) -> git(commit_build) " +
|
|
62
77
|
"-> code(changes) -> documenter -> git(commit_docs)",
|
|
63
78
|
requiredAgents: simpleSdlc.REQUIRED_AGENTS,
|
|
64
79
|
requiredSuites: simpleSdlc.REQUIRED_SUITES,
|
|
65
80
|
run: simpleSdlc.main,
|
|
66
81
|
},
|
|
67
82
|
];
|
|
83
|
+
/**
|
|
84
|
+
* Every built-in name, reserved. `repo_chains.ts` checks against this to
|
|
85
|
+
* refuse a repo chain that reuses one — a collision is reported as a
|
|
86
|
+
* problem, never resolved by shadowing either way round.
|
|
87
|
+
*/
|
|
88
|
+
export const BUILTIN_CHAIN_NAMES = new Set(CHAINS.map((c) => c.name));
|
|
89
|
+
// ── tier 2: repo-local chains, registered at startup ────────────────────
|
|
90
|
+
/**
|
|
91
|
+
* Module-level, not a parameter threaded through every call site: `spf list`,
|
|
92
|
+
* `spf <chain>`, `spf watch`, `spf doctor` and the interview all resolve
|
|
93
|
+
* chains by name from unrelated places, and passing a registry object to
|
|
94
|
+
* each would mean four more chances for one of them to be looking at the
|
|
95
|
+
* built-ins only. One process, one repo, one registration.
|
|
96
|
+
*/
|
|
97
|
+
let REPO_CHAINS = [];
|
|
98
|
+
let REPO_CHAIN_PROBLEMS = [];
|
|
99
|
+
/**
|
|
100
|
+
* Install the repo-local chains (and the problems found reading them) for
|
|
101
|
+
* this process. Call once, early, from the CLI entry point.
|
|
102
|
+
*
|
|
103
|
+
* REPLACES rather than appends, so it is idempotent: calling it twice with
|
|
104
|
+
* the same input leaves exactly one copy of each chain. That matters because
|
|
105
|
+
* a long-running `spf watch` may re-read `.spf/chains/` after an edit, and
|
|
106
|
+
* because a partial second registration (say, one file fixed) must not leave
|
|
107
|
+
* the previous file's stale definition behind.
|
|
108
|
+
*
|
|
109
|
+
* The `problems` list is stored, not thrown and not printed: loading is
|
|
110
|
+
* never allowed to fail a command that has nothing to do with the broken
|
|
111
|
+
* file (`spf trace`, `spf sessions`, `spf --version` must all still work in
|
|
112
|
+
* a repo with one malformed yaml). `spf list` and `spf doctor` surface them;
|
|
113
|
+
* dispatch surfaces them when the name someone typed is missing.
|
|
114
|
+
*/
|
|
115
|
+
export function registerRepoChains(chains, problems) {
|
|
116
|
+
REPO_CHAINS = [...chains];
|
|
117
|
+
REPO_CHAIN_PROBLEMS = [...problems];
|
|
118
|
+
}
|
|
119
|
+
/** Built-ins first, then repo-local chains — the order `spf list` prints and `findChain` resolves in. */
|
|
120
|
+
export function allChains() {
|
|
121
|
+
return [...CHAINS, ...REPO_CHAINS];
|
|
122
|
+
}
|
|
123
|
+
/** What went wrong reading `.spf/chains/*.yaml`, for `spf list`/`spf doctor` to report. */
|
|
124
|
+
export function repoChainProblems() {
|
|
125
|
+
return [...REPO_CHAIN_PROBLEMS];
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Resolve a CLI-facing name. Built-ins are consulted FIRST and win
|
|
129
|
+
* unconditionally — a repo chain that reuses a built-in name is rejected at
|
|
130
|
+
* load time (see `repo_chains.ts`), so this ordering is a second line of
|
|
131
|
+
* defence rather than the policy itself, and it is what makes `spf build` in
|
|
132
|
+
* any repo mean what the docs say it means.
|
|
133
|
+
*/
|
|
68
134
|
export function findChain(name) {
|
|
69
|
-
return CHAINS.find((c) => c.name === name);
|
|
135
|
+
return CHAINS.find((c) => c.name === name) ?? REPO_CHAINS.find((c) => c.name === name);
|
|
70
136
|
}
|
|
71
137
|
export function resolveRequiredAgents(chain, options) {
|
|
72
138
|
return typeof chain.requiredAgents === "function" ? chain.requiredAgents(options) : chain.requiredAgents;
|
|
73
139
|
}
|
|
140
|
+
/** Same idea as resolveRequiredAgents(), for the suite(s) --suite can override before validate() ever runs. */
|
|
141
|
+
export function resolveRequiredSuites(chain, options) {
|
|
142
|
+
return typeof chain.requiredSuites === "function" ? chain.requiredSuites(options) : chain.requiredSuites;
|
|
143
|
+
}
|
|
74
144
|
/**
|
|
75
145
|
* Run a chain, whichever path it defines: `run` (the imperative escape
|
|
76
146
|
* hatch) if it has one, otherwise `steps` through the shared driver. Both
|
|
@@ -81,5 +151,5 @@ export function resolveRequiredAgents(chain, options) {
|
|
|
81
151
|
export async function runChain(chain, ctx, options = {}) {
|
|
82
152
|
if (chain.run)
|
|
83
153
|
return chain.run(ctx, options);
|
|
84
|
-
return steps.runSteps(ctx, resolveRequiredAgents(chain, options), chain
|
|
154
|
+
return steps.runSteps(ctx, resolveRequiredAgents(chain, options), resolveRequiredSuites(chain, options), chain.steps, options);
|
|
85
155
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo-local chains: `<repo>/.spf/chains/*.yaml` read as DATA.
|
|
3
|
+
*
|
|
4
|
+
* WHAT THIS IS. A target repo can compose its own chain out of the step
|
|
5
|
+
* factories `./steps.ts` already exports, by naming them in a yaml file and
|
|
6
|
+
* passing them params. One file, one chain:
|
|
7
|
+
*
|
|
8
|
+
* # .spf/chains/ship-it.yaml
|
|
9
|
+
* name: ship-it
|
|
10
|
+
* describe: plan, build, test, land — with our own reviewer in the loop
|
|
11
|
+
* steps:
|
|
12
|
+
* - step: request
|
|
13
|
+
* - step: plan
|
|
14
|
+
* owner: architect
|
|
15
|
+
* - step: build
|
|
16
|
+
* retries: 2
|
|
17
|
+
* extraGates: [jsonParses]
|
|
18
|
+
* - step: fixLoop
|
|
19
|
+
* suite: test
|
|
20
|
+
* - step: commit
|
|
21
|
+
* onlyIfAccepted: true
|
|
22
|
+
*
|
|
23
|
+
* WHY DATA, AND NOT CODE. SPF's contract is "agent proposes, code disposes",
|
|
24
|
+
* and the code that disposes is SPF'S code. A chain that could `import`
|
|
25
|
+
* something out of the target repo would move the disposer into the repo
|
|
26
|
+
* being worked on — the agent's own blast radius — and there would be
|
|
27
|
+
* nothing left holding the line. So a repo chain names existing factories
|
|
28
|
+
* and nothing else: no expressions, no shell, no module paths, no way to
|
|
29
|
+
* introduce behavior that isn't already compiled into spf and covered by
|
|
30
|
+
* spf's tests. It is also what keeps the promise that a target repo is
|
|
31
|
+
* zero-setup and language-agnostic: the only thing it ever gains is `.spf/`,
|
|
32
|
+
* with no build step, no dependency on this package, and nothing to compile.
|
|
33
|
+
*
|
|
34
|
+
* The counterpart rule lives in `steps.ts` (see GATE_ALLOWLIST): params may
|
|
35
|
+
* only ADD. A repo chain can demand more checking than a built-in does; it
|
|
36
|
+
* can never demand less.
|
|
37
|
+
*
|
|
38
|
+
* ONE RUN PATH. Every chain here is built with `index.ts`'s `stepChain()` —
|
|
39
|
+
* the very same function the built-ins use — so it is a plain
|
|
40
|
+
* `ChainDefinition` with a `steps` array, its `phases`/`requiredAgents`/
|
|
41
|
+
* `requiredSuites` derived by the same code, executed by the same
|
|
42
|
+
* `steps.runSteps()` driver. There is deliberately no second interpreter,
|
|
43
|
+
* no "yaml runtime", and no branch anywhere downstream on "is this a repo
|
|
44
|
+
* chain": the only difference a repo chain carries is `source`, the file it
|
|
45
|
+
* came from, which exists so a run can be traced back to a definition that
|
|
46
|
+
* may since have been edited.
|
|
47
|
+
*
|
|
48
|
+
* NEVER THROWS. `loadRepoChains()` returns `{ chains, problems }`. That is
|
|
49
|
+
* not politeness: this function runs at CLI startup for EVERY command (see
|
|
50
|
+
* `cli/index.ts`), so a single malformed yaml must not be able to break
|
|
51
|
+
* `spf sessions`, `spf trace`, `spf doctor` or `spf --version` in a repo
|
|
52
|
+
* that has one. Anything that goes wrong — unreadable directory, invalid
|
|
53
|
+
* yaml, unknown step, wrong param type, bad description, unknown gate,
|
|
54
|
+
* colliding name — becomes a `{ file, message }` problem carrying enough
|
|
55
|
+
* detail to fix the file, and the other files still load.
|
|
56
|
+
*
|
|
57
|
+
* THE VOCABULARY IS THE FACTORY SIGNATURE. A step's yaml name is its
|
|
58
|
+
* exported function name (`fixLoop`, `promptOnly`, `publishIssues`) and its
|
|
59
|
+
* params are that function's `opts` keys, camelCase and all (`extraGates`,
|
|
60
|
+
* `onlyIfAccepted`, `fromPlan`). No snake_case aliasing, no renaming layer:
|
|
61
|
+
* a second spelling of the same thing is a second thing to keep in sync, and
|
|
62
|
+
* a chain author reading `steps.ts` (or `spf list`) would be reading a
|
|
63
|
+
* vocabulary that isn't the one they type. The schemas below are the ONE
|
|
64
|
+
* place that mapping is written down; each mirrors exactly one factory's
|
|
65
|
+
* `opts`, and adding a param to a factory without adding it here simply
|
|
66
|
+
* means yaml cannot reach it yet (a safe, loud default: unknown params are
|
|
67
|
+
* rejected, never ignored).
|
|
68
|
+
*/
|
|
69
|
+
import * as v from "valibot";
|
|
70
|
+
import type * as paths from "../core/paths.ts";
|
|
71
|
+
import * as steps from "./steps.ts";
|
|
72
|
+
import { type ChainDefinition } from "./index.ts";
|
|
73
|
+
/** What went wrong in one file, and which file. */
|
|
74
|
+
export interface RepoChainProblem {
|
|
75
|
+
/** Absolute path — the thing the operator has to open and edit. */
|
|
76
|
+
file: string;
|
|
77
|
+
message: string;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* A step's yaml surface: the schema its params must satisfy, and how to
|
|
81
|
+
* build the actual `Step` from them.
|
|
82
|
+
*
|
|
83
|
+
* `params` is redundant with the schema's own entries, and kept anyway: it
|
|
84
|
+
* lets an unknown param be reported as `unknown param "ownr" — allowed:
|
|
85
|
+
* owner, description, ...`, which is what an author needs, instead of
|
|
86
|
+
* valibot's structural complaint about an unexpected key.
|
|
87
|
+
*/
|
|
88
|
+
export interface StepSpec {
|
|
89
|
+
schema: v.GenericSchema;
|
|
90
|
+
build: (params: any) => steps.Step;
|
|
91
|
+
params: readonly string[];
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* One entry per exported flat-step factory in `steps.ts`. The key is what a
|
|
95
|
+
* yaml `step:` names.
|
|
96
|
+
*
|
|
97
|
+
* `qualityCheck`/`fixLoop` default their `suite` HERE as well as in the
|
|
98
|
+
* factory signature: the factory's default lives on the whole `opts` object
|
|
99
|
+
* (`opts = { suite: "test" }`), which a yaml-supplied `{}` would satisfy
|
|
100
|
+
* without ever supplying `suite`. Defaulting in the schema is what makes
|
|
101
|
+
* `- step: fixLoop` with no params mean the same thing as `fixLoop()`.
|
|
102
|
+
*/
|
|
103
|
+
export declare const STEP_SPECS: Record<string, StepSpec>;
|
|
104
|
+
/** Every step name a yaml file may use — for schema validation and for error messages. */
|
|
105
|
+
export declare const STEP_NAMES: readonly string[];
|
|
106
|
+
/**
|
|
107
|
+
* The whole file. One document, one chain — deliberately not a list: the
|
|
108
|
+
* filename then documents which chain lives where, and a problem can point
|
|
109
|
+
* at a file the operator can open, which is the only handle they have.
|
|
110
|
+
*
|
|
111
|
+
* `steps` is validated loosely here (each entry only has to be an object
|
|
112
|
+
* naming a `step`); the per-step params are validated against that step's
|
|
113
|
+
* own schema afterwards, because which schema applies depends on the value
|
|
114
|
+
* of `step`.
|
|
115
|
+
*/
|
|
116
|
+
export declare const RepoChainFileSchema: v.StrictObjectSchema<{
|
|
117
|
+
readonly name: v.SchemaWithPipe<readonly [v.StringSchema<"name is required — it is what `spf <name>` types">, v.RegexAction<string, "name must be lowercase letters/digits/._- and start with a letter or digit (it is typed on the command line)">]>;
|
|
118
|
+
readonly describe: v.SchemaWithPipe<readonly [v.StringSchema<"describe is required — it is the line `spf list` prints">, v.MinLengthAction<string, 1, "describe must not be empty">]>;
|
|
119
|
+
readonly steps: v.SchemaWithPipe<readonly [v.ArraySchema<v.LooseObjectSchema<{
|
|
120
|
+
readonly step: v.StringSchema<"each step entry needs a `step:` naming a step factory">;
|
|
121
|
+
}, undefined>, "steps must be a list">, v.MinLengthAction<({
|
|
122
|
+
step: string;
|
|
123
|
+
} & {
|
|
124
|
+
[key: string]: unknown;
|
|
125
|
+
})[], 1, "steps must name at least one step — a chain with no steps would run nothing and report success">]>;
|
|
126
|
+
}, undefined>;
|
|
127
|
+
export type RepoChainFile = v.InferOutput<typeof RepoChainFileSchema>;
|
|
128
|
+
/**
|
|
129
|
+
* Read every chain in `<anchor.spf_dir>/chains/`.
|
|
130
|
+
*
|
|
131
|
+
* Never throws (see the module header). Returns the chains that loaded and a
|
|
132
|
+
* problem per file that didn't; a file contributes at most one chain and, on
|
|
133
|
+
* failure, exactly one problem — the first thing wrong with it, because the
|
|
134
|
+
* second is usually a consequence of the first.
|
|
135
|
+
*/
|
|
136
|
+
export declare function loadRepoChains(anchor: paths.RepoAnchor): {
|
|
137
|
+
chains: ChainDefinition[];
|
|
138
|
+
problems: RepoChainProblem[];
|
|
139
|
+
};
|