@theholocron/astromech 4.19.0 → 5.0.0-alpha.100

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 CHANGED
@@ -3,8 +3,7 @@
3
3
  The Holocron task runner. One **task manifest** per repo, and every
4
4
  derived surface comes from it: `holocron run` (local), `holocron ci` (the
5
5
  CI suite run locally), the generated GitHub Actions workflows, the
6
- `package.json` scripts, the linter set, and the branch-protection
7
- required-checks list.
6
+ `package.json` scripts, and the branch-protection required-checks list.
8
7
 
9
8
  > An astromech droid runs a starfighter's maintenance, diagnostics and
10
9
  > system wiring while the pilot flies. This does that for a repo.
@@ -25,27 +24,77 @@ import { createAstromech } from "@theholocron/astromech";
25
24
 
26
25
  const astromech = createAstromech({ cwd });
27
26
 
28
- const report = astromech.run("test", { passthrough: ["--watch"] });
27
+ const report = astromech.run("verification.unitTests", { passthrough: ["--watch"] });
29
28
  // → { status: "ok" | "fail" | "skip" | "dry-run" | "unknown", command?, message? }
30
29
  ```
31
30
 
32
31
  ### `holocron run <task>` resolution
33
32
 
34
- `holocron run test` runs your tests — you don't tell it turbo vs pnpm vs
35
- npm, or which runner:
33
+ `holocron run verification.unitTests` runs your tests — you don't tell it
34
+ turbo vs pnpm vs npm, or which runner:
36
35
 
37
- ```
36
+ ```text
38
37
  1. turbo.json defines the task → turbo run <task>
39
38
  2. package.json has a <task> script → <detected pm> run <task>
40
39
  (a "holocron run …" thin caller is skipped — no recursion)
41
40
  3. the registry has a local runner → <tool> <args> <org-flags> (e.g. --coverage)
42
- 4. known task, nothing to run → "no <task> task", exit 0 (exit 1 with --required)
43
- 5. unknown task → error, exit 1
41
+ 3b. the task is a linterGroup → each resolved linter, run natively
42
+ 4. the task is a container of jobs → each job, in declared order
43
+ 5. known task, nothing to run → "no <task> task", exit 0 (exit 1 with --required)
44
+ 6. unknown task → error, exit 1
44
45
  ```
45
46
 
46
- The registry (`TASKS`) covers `test` / `typecheck` / `lint` / `build` /
47
- `sync` / `wiki`; `codeql` / `deploy` have no local equivalent. Adding a
48
- task here gives every repo that task.
47
+ ## Intent → technology
48
+
49
+ Tasks are named by **intent**, not by tool — a repo declares
50
+ `verification.unitTests`, not `vitest`. The table below is the full
51
+ registry (`TASKS` in `src/registry.ts`): every task name astromech knows,
52
+ what it actually runs, and why. Tool names never appear in
53
+ `holocron.config.ts` — they're an implementation detail this table
54
+ documents, not a naming convention repos need to follow.
55
+
56
+ | Task | Runs | Notes |
57
+ | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
58
+ | `verification.unitTests` | vitest | carries the `--coverage` org default |
59
+ | `verification.typeSafety` | tsc | `tsc --noEmit` |
60
+ | `verification.performance` | Lighthouse CI | only runs with a `lighthouse.config.*` present |
61
+ | `sourceQuality.staticAnalysis` | eslint, actionlint, git-merge-conflict-markers | a `linterGroup` — see below |
62
+ | `sourceQuality.formatting` | prettier, editorconfig-checker, markdownlint-cli2 | a `linterGroup` |
63
+ | `sourceQuality.structuredDataValidation` | yamllint | |
64
+ | `sourceQuality.deadCodeAnalysis` | knip | |
65
+ | `security.secretDetection` | gitleaks | |
66
+ | `security.codeScanning` | CodeQL | no local equivalent — CI only |
67
+ | `security.dependencyReview` | GitHub's native Dependabot alerts/graph | a capability-model method (`Source.enableVulnerabilityAlerts()`), not a task |
68
+ | `delivery.build` | tsdown / vite / rollup / tsc, detected from the repo's own config file, or `package.json`'s own `dependencies`/`devDependencies` if that config file isn't there | |
69
+ | `delivery.publish` | semantic-release | no local equivalent — CI only; carries `preview` (npm dist-tags) |
70
+ | `delivery.deploy` | Cloudflare Pages / Vercel | no local equivalent — CI only; carries `preview` |
71
+ | `delivery.bundleSize` | bundle-size upload to Codecov | no local equivalent — CI only |
72
+ | `platform.repoSync` | `holocron sync` | keeps generated files current |
73
+ | `platform.commitStandards` | commitlint | no local equivalent — enforced by the `commit-msg` hook locally, over the PR's commit range in CI |
74
+ | `platform.repoValidation` | `scripts/validate-adrs.mjs`, `scripts/validate-registry.mjs`, `scripts/validate-docs-presence.mjs` | a job-bearing task — spec/ADR frontmatter, registry-doc completeness, and docs-presence, not linters |
75
+ | `knowledge.wiki` | Fern | publishes `docs/wiki/*.md` — not a sync, a publish; carries `preview` |
76
+ | `knowledge.docs` | Astro build | the docs site itself; carries `preview` (defaults on) |
77
+ | `knowledge.components` | Storybook build | a browsable component catalog — "Storybook" is the tool, not the intent; carries `preview` (defaults on) |
78
+
79
+ **`preview` is a cross-cutting feature, not a namespace.** npm has staging
80
+ dist-tags, Cloudflare/Vercel do per-PR preview deploys, Fern previews
81
+ docs — none of these earn their own task or verb; it's one capability any
82
+ publish/deploy-shaped task can carry, toggled via that task's `with:`.
83
+
84
+ **`linterGroup` tasks** (`sourceQuality.staticAnalysis`,
85
+ `sourceQuality.formatting`) bundle more than one tool under a single
86
+ required check. Each tool is still gated by its own detection rule — e.g.
87
+ `eslint` runs when either a local `eslint.config.*` is present, or the
88
+ package has no local file but `@theholocron/eslint-config` resolves
89
+ (the same shared-config splice the `tool`/`detect` runner path uses) — the
90
+ `tool name → detection rule → local binary` mapping lives in one place,
91
+ `src/linters.ts`. A repo's choice of which of these run is based on which
92
+ tasks it includes in `tasks: [...]`, same as any other task.
93
+
94
+ **No super-linter.** Each task above runs its own tool directly, through
95
+ the same `holocron` composite action that resolves it locally — CI runs
96
+ the identical command a developer runs, not a third-party action bundling
97
+ a tool version this org doesn't control.
49
98
 
50
99
  ## Config — `@theholocron/astromech/config`
51
100
 
@@ -59,9 +108,9 @@ import { defineConfig } from "@theholocron/astromech/config";
59
108
 
60
109
  export default defineConfig({
61
110
  tasks: [
62
- "typecheck",
63
- { name: "test", required: true, with: { "run-coverage": true } },
64
- { name: "audit", ci: true, local: false }, // CI-only
111
+ "verification.typeSafety",
112
+ { name: "verification.unitTests", required: true, with: { "run-coverage": true } },
113
+ { name: "security.codeScanning", ci: true, local: false }, // CI-only
65
114
  ],
66
115
  });
67
116
  ```
@@ -69,6 +118,9 @@ export default defineConfig({
69
118
  `loadTasksConfig(cwd)` resolves the manifest: the `tasks` key of
70
119
  `holocron.config.*` (a bare item array), then a dedicated
71
120
  `astromech.config.*` merged on top (dedicated wins; arrays concatenate).
121
+ `mergeTasksLayers(parentTasks, dedicated)` is that merge step on its own,
122
+ for callers that load the two sources some other way (Sentinel reads them
123
+ through the GitHub API).
72
124
 
73
125
  | `TaskEntry` field | Effect |
74
126
  | ------------------------ | ----------------------------------------------------------- |
@@ -77,7 +129,6 @@ export default defineConfig({
77
129
  | `local: false` | `holocron run <name>` → "CI-only task", exit 0 |
78
130
  | `required` | the task's check context is a required status check |
79
131
  | `with` | per-repo overrides on the reusable-workflow channel |
80
- | `linters` (`lint` only) | explicit linter list; omitted → auto-detect |
81
132
 
82
133
  Top-level keys: `syncScripts: false` disables the `package.json` script
83
134
  writes entirely; `holocronScript` sets the command the synced `"holocron"`
@@ -89,39 +140,43 @@ script runs (default `"holocron"`).
89
140
  const astromech = createAstromech({ cwd, config, orgContext: { org, domain } });
90
141
 
91
142
  astromech.thinCallers(); // Map<"<name>.yml", yaml> — one per templated, ci-enabled task
92
- astromech.packageScripts(); // { holocron: "holocron", lint: "holocron run lint", … }
93
- astromech.requiredChecks(); // ["Lint / Conclusion", "codecov/patch", …] — branch-protection contexts
143
+ astromech.packageScripts(); // { holocron: "holocron", "verification.unitTests": "holocron run verification.unitTests", … }
144
+ astromech.requiredChecks(); // ["Typecheck / tsc --noEmit", "codecov/patch", …] — branch-protection contexts
94
145
  astromech.codecovConfig(existing); // codecov.yml content — merges into `existing`, or scaffolds fresh when null
95
146
  astromech.ci({ scope: "required" }); // CiReport — run the gating checks locally, in CI order
96
147
  ```
97
148
 
98
- `ci()` runs every `required: true` task (else every `ci: true` task) through the
99
- same resolution as `run()`, in `CI_ORDER`, and returns `{ status, jobs }`. A
100
- `required` task whose local runner can't run is a failure; `local: null` tasks
101
- (`audit` / `codeql` / `deploy`) are reported skipped. `holocron ci` sets the
149
+ `ci()` runs every `required: true` task (else every `ci: true` task) through
150
+ the same resolution as `run()`, in `CI_ORDER`, and returns `{ status, jobs }`.
151
+ A `required` task whose local runner can't run is a failure — **except** a
152
+ task that's genuinely CI-only (`local: null`, or a `linterGroup` whose every
153
+ member lacks a local binary entirely, like `platform.commitStandards`),
154
+ which is reported skipped even when required. `holocron ci` sets the
102
155
  process exit code from `status`.
103
156
 
104
157
  `thinCallers()` returns the raw `.github/workflows/*.yml` content (no
105
- generated-by header — the caller prefixes its own). `deploy` with
106
- `preview:` shorthand produces the combined push-to-Pages / PR-to-preview
107
- workflow. `packageScripts()` emits the `holocron` entry
108
- (`holocronScript ?? "holocron"`) plus one `"<task>": "holocron run <task>"`
109
- per runnable task; it skips `local: false` entries and tasks with no local
110
- runner (`codeql`, `deploy`), and returns `{}` when `syncScripts: false` or
111
- there is no config.
158
+ generated-by header — the caller prefixes its own). `delivery.deploy` /
159
+ `knowledge.docs` / `knowledge.components` with `preview:` produce the
160
+ combined push-to-Pages / PR-to-preview workflow —
161
+ `knowledge.docs`/`knowledge.components` default `preview` on, since
162
+ neither has a plain-production-only fallback template.
163
+ `packageScripts()` emits the `holocron` entry (`holocronScript ?? "holocron"`)
164
+ plus one `"<task>": "holocron run <task>"` per runnable task; it skips
165
+ `local: false` entries and tasks with no local runner at all, and returns
166
+ `{}` when `syncScripts: false` or there is no config.
112
167
 
113
168
  ### Required checks
114
169
 
115
170
  `requiredChecks()` derives the branch-protection required-status-check list
116
- from the manifest: every `{ required: true }` task's check context (the
117
- `… / Conclusion` aggregate job, from `WORKFLOW_CHECK_CONTEXTS`), ordered by
118
- `CI_ORDER`, then `config.extraRequiredChecks` (codecov gates, the
119
- bundle-build check, …), de-duplicated. `holocron setup` prepends `"DCO"` and
120
- applies the list for `protection: "strict"` repos. Policy-free — manifest
121
- only.
122
-
123
- `holocron run` itself does not read the config yet — that (and
124
- `holocron ci`) come in later phases (epic #581).
171
+ from the manifest: every `{ required: true }` task's check context, ordered
172
+ by `CI_ORDER`, then `config.extraRequiredChecks` (codecov gates, …),
173
+ de-duplicated. Most tasks are single always-run jobs now, so their context
174
+ names that job directly (`"Typecheck / tsc --noEmit"`) — the `… /
175
+ Conclusion` fan-in aggregate is only used where a task genuinely has
176
+ several conditionally-run jobs feeding one check
177
+ (`verification.unitTests`, `platform.repoValidation`). `holocron setup`
178
+ prepends `"DCO"` and applies the list for `protection: "strict"` repos.
179
+ Policy-free — manifest only.
125
180
 
126
181
  ### `codecov.yml`
127
182
 
@@ -135,56 +190,16 @@ current file's content (or `null`) and it either merges the
135
190
  thresholds, flags, custom rules) or scaffolds a fresh file from the base
136
191
  template. `holocron setup` writes the result via the `source` capability;
137
192
  this method never touches the filesystem beyond reading `packages/*` and
138
- `apps/*` under `cwd`. Moved here from `@theholocron/cli` (#650) — the
139
- coverage setup tracks the `test` task, the same way required checks track
140
- `tasks`.
141
-
142
- ## Lint parity
143
-
144
- One linter list drives both CI and local — no asymmetry. Source: the
145
- `lint` task's `linters` array, or auto-detection from the config files
146
- present. The `linter name → super-linter VALIDATE_* keys` mapping lives in
147
- one place, `src/linters.ts`.
148
-
149
- | linter | `VALIDATE_*` | always-on | local binary |
150
- | ---------------------------- | ----------------------------------------- | ----------------------------------- | ---------------------------------------- |
151
- | `eslint` | `JAVASCRIPT_ES`, `TYPESCRIPT_ES` | on `eslint.config.*` / `.eslintrc*` | `eslint .` |
152
- | `prettier` | `*_PRETTIER` (JS/JSX/TS/TSX/MD) + `FIX_*` | yes | `prettier --check .` |
153
- | `yamllint` | `YAML` | yes | `yamllint .` (usually CI-only) |
154
- | `actionlint` | `GITHUB_ACTIONS` | yes | `actionlint` (usually CI-only) |
155
- | `gitleaks` | `GITLEAKS` | yes | `gitleaks dir` (usually CI-only) |
156
- | `editorconfig` | `EDITORCONFIG` | yes | `editorconfig-checker` (usually CI-only) |
157
- | `commitlint` | `GIT_COMMITLINT` | yes | — (commit-msg hook + CI) |
158
- | `git-merge-conflict-markers` | `GIT_MERGE_CONFLICT_MARKERS` | yes | — (CI only) |
159
- | `markdownlint` | `MARKDOWN` | on `.markdownlint*` | `markdownlint-cli2` |
160
-
161
- ```ts
162
- import { superLinterConfig, resolveLinters } from "@theholocron/astromech";
163
-
164
- superLinterConfig({ explicit: ["eslint", "prettier"], rootFiles: fs.readdirSync(cwd) });
165
- // → { env: { VALIDATE_JAVASCRIPT_ES: "true", … }, linters: ["eslint","prettier"], configInputs: { … } }
166
- ```
167
-
168
- `superLinterConfig().env` is the exact `VALIDATE_*`/`FIX_*` map the CI
169
- `lint` job needs — the CLI serializes it as the `super-linter-env` input on
170
- each repo's generated `lint` thin caller. Setting any `VALIDATE_*` puts
171
- super-linter in allow-list mode, so emitting only the enabled keys makes it
172
- run exactly the resolved set.
173
-
174
- `holocron run lint` (later phase) runs the same set natively: `turbo run
175
- lint` for the eslint portion (cached), then each other linter whose binary
176
- resolves; linters with a binary that is not on `PATH` are flagged with an
177
- install hint; the rest print "CI only".
193
+ `apps/*` under `cwd`.
178
194
 
179
195
  ## Development
180
196
 
181
- | Script | Description |
182
- | -------------------- | ----------------------- |
183
- | `pnpm build` | Bundle with tsdown |
184
- | `pnpm test` | Run the vitest suite |
185
- | `pnpm test:coverage` | Run tests with coverage |
186
- | `pnpm typecheck` | `tsc --noEmit` |
187
- | `pnpm lint` | ESLint |
197
+ | Script | Description |
198
+ | --------------------------------------- | ------------------------------------------- |
199
+ | `pnpm run delivery.build` | Bundle with tsdown |
200
+ | `pnpm run verification.unitTests` | Run the vitest suite (always with coverage) |
201
+ | `pnpm run verification.typeSafety` | `tsc --noEmit` |
202
+ | `pnpm run sourceQuality.staticAnalysis` | ESLint |
188
203
 
189
204
  ## Releases
190
205
 
@@ -1,4 +1,4 @@
1
- import { i as normalizeTaskEntry, n as TaskEntry, r as TasksConfig, t as TaskConfigItem } from "../schema-BegK4MsY.mjs";
1
+ import { i as normalizeTaskEntry, n as TaskEntry, r as TasksConfig, t as TaskConfigItem } from "../schema-D01VYg43.mjs";
2
2
  //#region src/config/define.d.ts
3
3
  /**
4
4
  * Typed identity helper for `astromech.config.ts`:
@@ -14,6 +14,13 @@ import { i as normalizeTaskEntry, n as TaskEntry, r as TasksConfig, t as TaskCon
14
14
  declare const defineConfig: <C extends TasksConfig>(config: C) => C;
15
15
  //#endregion
16
16
  //#region src/config/load.d.ts
17
+ /**
18
+ * The `tasks` value in each source. A dedicated `astromech.config.*`
19
+ * default-exports the full {@link TasksConfig} object; the `tasks` key of
20
+ * `holocron.config.*` is the bare item array (it mirrors the old
21
+ * `workflows` key). Either form is accepted from either source.
22
+ */
23
+ type TasksInput = TasksConfig | TaskConfigItem[];
17
24
  /**
18
25
  * Resolve the task manifest for a repo: the `tasks` key of
19
26
  * `holocron.config.*`, then a dedicated `astromech.config.*` merged on
@@ -21,5 +28,13 @@ declare const defineConfig: <C extends TasksConfig>(config: C) => C;
21
28
  * neither source is present.
22
29
  */
23
30
  declare function loadTasksConfig(cwd: string): Promise<TasksConfig>;
31
+ /**
32
+ * The merge step of {@link loadTasksConfig}, for callers that load the two
33
+ * sources some other way (Sentinel reads them from the GitHub API, not
34
+ * from disk, holocron#916): `holocron.config.*`'s `tasks` value first,
35
+ * then a dedicated `astromech.config.*`'s default export merged on top.
36
+ * Either may be `undefined` (source absent); returns `{}` when both are.
37
+ */
38
+ declare function mergeTasksLayers(parentTasks: TasksInput | undefined, dedicated: TasksInput | undefined): TasksConfig;
24
39
  //#endregion
25
- export { type TaskConfigItem, type TaskEntry, type TasksConfig, defineConfig, loadTasksConfig, normalizeTaskEntry };
40
+ export { type TaskConfigItem, type TaskEntry, type TasksConfig, defineConfig, loadTasksConfig, mergeTasksLayers, normalizeTaskEntry };
@@ -26,14 +26,24 @@ async function loadTasksConfig(cwd) {
26
26
  cwd,
27
27
  name: "astromech"
28
28
  });
29
- return [coerce((await loadConfigFile({
29
+ return mergeTasksLayers((await loadConfigFile({
30
30
  cwd,
31
31
  name: "holocron"
32
- }))?.config.tasks), coerce(dedicated?.config)].filter((layer) => layer !== void 0).reduce((acc, layer) => mergeConfig(acc, layer), {});
32
+ }))?.config.tasks, dedicated?.config);
33
+ }
34
+ /**
35
+ * The merge step of {@link loadTasksConfig}, for callers that load the two
36
+ * sources some other way (Sentinel reads them from the GitHub API, not
37
+ * from disk, holocron#916): `holocron.config.*`'s `tasks` value first,
38
+ * then a dedicated `astromech.config.*`'s default export merged on top.
39
+ * Either may be `undefined` (source absent); returns `{}` when both are.
40
+ */
41
+ function mergeTasksLayers(parentTasks, dedicated) {
42
+ return [coerce(parentTasks), coerce(dedicated)].filter((layer) => layer !== void 0).reduce((acc, layer) => mergeConfig(acc, layer), {});
33
43
  }
34
44
  function coerce(value) {
35
45
  if (value == null) return void 0;
36
46
  return Array.isArray(value) ? { tasks: value } : value;
37
47
  }
38
48
  //#endregion
39
- export { defineConfig, loadTasksConfig, normalizeTaskEntry };
49
+ export { defineConfig, loadTasksConfig, mergeTasksLayers, normalizeTaskEntry };