@miroshnik/elves 0.0.0 → 2026.1018.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 (68) hide show
  1. package/AGENTS.md +310 -0
  2. package/README.md +344 -0
  3. package/bin/ai-dev.ts +7 -0
  4. package/bin/elves.ts +2847 -0
  5. package/bin/shims/timeout +113 -0
  6. package/claude/CLAUDE.md +71 -0
  7. package/dist/github/scripts/github.js +3428 -0
  8. package/dist/spec/scripts/architecture.js +628 -0
  9. package/dist/spec/scripts/eslint-config.js +54 -0
  10. package/dist/spec/scripts/harness-qzb932c8.js +959 -0
  11. package/dist/spec/scripts/harness-v9s1v9w3.js +16045 -0
  12. package/dist/spec/scripts/harness.js +53 -0
  13. package/dist/spec/scripts/knip-hints.cjs +34 -0
  14. package/docs/ci-concurrency.md +82 -0
  15. package/docs/cloud-sessions.md +204 -0
  16. package/docs/judgment.md +81 -0
  17. package/docs/parallel-checkouts.md +220 -0
  18. package/docs/pr-checks.md +117 -0
  19. package/docs/testing.md +304 -0
  20. package/package.json +70 -1
  21. package/skills/ci-wait/SKILL.md +180 -0
  22. package/skills/ci-wait/scripts/wait-ci.sh +377 -0
  23. package/skills/dashboard/SKILL.md +212 -0
  24. package/skills/dashboard/scripts/dashboard.ts +912 -0
  25. package/skills/dashboard/scripts/sessions.ts +772 -0
  26. package/skills/est/SKILL.md +222 -0
  27. package/skills/est/reference.md +381 -0
  28. package/skills/est/scripts/est.ts +4867 -0
  29. package/skills/github/SKILL.md +206 -0
  30. package/skills/github/locales/en.json +449 -0
  31. package/skills/github/locales/ru.json +449 -0
  32. package/skills/github/reference.md +1073 -0
  33. package/skills/github/scripts/github.ts +4390 -0
  34. package/skills/github/scripts/locale.ts +495 -0
  35. package/skills/github/scripts/package.json +3 -0
  36. package/skills/slot/SKILL.md +145 -0
  37. package/skills/slot/scripts/package.json +3 -0
  38. package/skills/slot/scripts/slot.ts +279 -0
  39. package/skills/spec/SKILL.md +247 -0
  40. package/skills/spec/canon.md +219 -0
  41. package/skills/spec/checks.md +709 -0
  42. package/skills/spec/ci.md +378 -0
  43. package/skills/spec/migration.md +125 -0
  44. package/skills/spec/reference.md +889 -0
  45. package/skills/spec/scripts/architecture.ts +839 -0
  46. package/skills/spec/scripts/c4.ts +184 -0
  47. package/skills/spec/scripts/eslint-config.ts +82 -0
  48. package/skills/spec/scripts/harness.ts +1483 -0
  49. package/skills/spec/scripts/knip-hints.cjs +34 -0
  50. package/skills/spec/scripts/knip-hints.d.cts +17 -0
  51. package/skills/spec/scripts/package-run.ts +22 -0
  52. package/skills/spec/scripts/package.json +3 -0
  53. package/skills/spec/scripts/spec-break.ts +232 -0
  54. package/skills/spec/scripts/spec-claims.ts +198 -0
  55. package/skills/spec/scripts/spec-diff.ts +1111 -0
  56. package/skills/spec/scripts/spec-doc.ts +744 -0
  57. package/skills/spec/scripts/spec-exceptions.ts +233 -0
  58. package/skills/spec/scripts/spec-publish.ts +214 -0
  59. package/skills/spec/scripts/spec-run.ts +235 -0
  60. package/skills/spec/scripts/spec-shard.ts +267 -0
  61. package/skills/spec/scripts/speclib.ts +826 -0
  62. package/skills/spec/scripts/vendor/babel-parser.LICENSE +19 -0
  63. package/skills/spec/scripts/vendor/babel-parser.cjs +14615 -0
  64. package/skills/spec/scripts/vendor/babel-parser.d.cts +3 -0
  65. package/skills/task-env/SKILL.md +229 -0
  66. package/skills/task-env/proxy/compose.yaml +48 -0
  67. package/skills/task-env/reference.md +246 -0
  68. package/skills/task-env/scripts/task-env.ts +981 -0
@@ -0,0 +1,184 @@
1
+ /**
2
+ * c4 — architecture from the model `tests/architecture/model.ts` in Markdown: three C4 levels as Mermaid diagrams (C1 —
3
+ * `C4Context`, C2 — `C4Container`, C3 — `C4Component`) and a table of modules. The diagrams are built on every
4
+ * documentation build — there is no hand-written one to fall behind the code. Element order is the declaration order in
5
+ * the model.
6
+ *
7
+ * A mark `<!-- spec: c4-… -->` on its own line in `<folder>.md` is replaced with the same diagram (`embed`). Labels are
8
+ * in the project language `lang` (the github skill's dictionary), model data — as the project wrote it.
9
+ */
10
+
11
+ import locale from "../../github/scripts/locale.ts";
12
+ import { adaptersOf, moduleContainers } from "./architecture.ts";
13
+ import type { Container, External, Model, Module } from "./architecture.ts";
14
+ import { mdText } from "./speclib.ts";
15
+
16
+ const entries = <T>(r?: Record<string, T>): [string, T][] => Object.entries(r ?? {});
17
+ /** Mermaid identifier: Latin letters, digits and `_`; the prefix separates a same-named container and module. */
18
+ const id = (prefix: string, name: string): string => prefix + name.replace(/[^A-Za-z0-9_]/g, "_");
19
+ /** A Mermaid string: in quotes, without quotes or line breaks inside. */
20
+ const q = (s: string): string => `"${s.replace(/"/g, "'").replace(/\s+/g, " ").trim()}"`;
21
+ const paths = (m: Module): string[] => (Array.isArray(m.path) ? m.path : [m.path]).map((p) => p.replace(/\/+$/, ""));
22
+ const hosts = (x: { hosts?: string[] }): string => (x.hosts ?? []).join(", ");
23
+ const systemName = (m: Model, lang: string): string => m.name ?? locale.names(lang).spec.c4.system;
24
+
25
+ function rel(from: string, to: string, label: string, techn = ""): string {
26
+ return ` Rel(${from}, ${to}, ${q(label)}${techn ? ", " + q(techn) : ""})`;
27
+ }
28
+
29
+ const isStorage = (c: Container): boolean => !!c.clients?.length;
30
+
31
+ function containerLine(name: string, c: Container, indent: string): string {
32
+ return `${indent}${isStorage(c) ? "ContainerDb" : "Container"}(${id("c_", name)}, ${q(name)}, ${q((c.deploy ?? []).join(", "))}, ${q(c.purpose)})`;
33
+ }
34
+
35
+ const externalLines = (m: Model): string[] =>
36
+ entries(m.externals).map(([n, x]) => ` System_Ext(${id("ext_", n)}, ${q(n)}, ${q(x.purpose)})`);
37
+
38
+ /** "via module a" / "via modules a, b". */
39
+ function via(a: string[], lang: string): string {
40
+ const n = locale.names(lang);
41
+ return locale.fill(a.length === 1 ? n.spec.c4.viaModule : n.spec.c4.viaModules, { modules: a.join(", ") });
42
+ }
43
+
44
+ const fence = (kind: string, title: string, body: string[]): string[] => ["```mermaid", kind, ` title ${title}`, ...body, "```"];
45
+
46
+ /**
47
+ * C1: the system and external systems; a link is labeled with the adapter modules and hosts, a perimeter one (no
48
+ * adapter) comes before the system.
49
+ */
50
+ export function c4Context(m: Model, lang: string): string[] {
51
+ const n = locale.names(lang);
52
+ const link = ([name, x]: [string, External]) => {
53
+ const a = adaptersOf(x);
54
+ return a.length ? rel("system", id("ext_", name), via(a, lang), hosts(x)) : rel(id("ext_", name), "system", n.spec.c4.perimeter, hosts(x));
55
+ };
56
+ return fence("C4Context", n.spec.c4.context, [` System(system, ${q(systemName(m, lang))})`, ...externalLines(m), ...entries(m.externals).map(link)]);
57
+ }
58
+
59
+ /**
60
+ * C2: containers inside the system (a storage — a database), `uses` links, an external system — at every container
61
+ * with its adapter (a library adapter — in all its containers); the perimeter has no links to containers.
62
+ */
63
+ export function c4Container(m: Model, lang: string): string[] {
64
+ const n = locale.names(lang);
65
+ const body = [` System_Boundary(system, ${q(systemName(m, lang))}) {`];
66
+ for (const [name, c] of entries(m.containers)) body.push(containerLine(name, c, " "));
67
+ body.push(" }", ...externalLines(m));
68
+ for (const [name, c] of entries(m.containers)) for (const u of c.uses ?? []) body.push(rel(id("c_", name), id("c_", u), n.spec.c4.uses));
69
+ const of = moduleContainers(m);
70
+ for (const [name, x] of entries(m.externals)) {
71
+ for (const [cn] of entries(m.containers)) {
72
+ const a = adaptersOf(x).filter((ad) => (of.get(ad) ?? []).includes(cn));
73
+ if (a.length) body.push(rel(id("c_", cn), id("ext_", name), via(a, lang), hosts(x)));
74
+ }
75
+ }
76
+ return fence("C4Container", n.spec.c4.containers, body);
77
+ }
78
+
79
+ /**
80
+ * C3: modules by container, a library — outside their boundaries (its containers are seen through the modules that
81
+ * depend on it), `dependsOn` dependencies; a storage — at the module whose packages are its clients; an external
82
+ * system — at every adapter module.
83
+ */
84
+ export function c4Component(m: Model, lang: string): string[] {
85
+ const n = locale.names(lang);
86
+ const component = (name: string, indent: string) => {
87
+ const x = m.modules[name]!;
88
+ const techn = [...paths(x), ...(x.library ? [n.spec.c4.library] : [])].join(", ");
89
+ return `${indent}Component(${id("m_", name)}, ${q(name)}, ${q(techn)}, ${q(x.purpose)})`;
90
+ };
91
+ const body: string[] = [];
92
+ const placed = new Set<string>();
93
+ for (const [cn, c] of entries(m.containers)) {
94
+ const mods = (c.modules ?? []).filter((x) => m.modules[x] && !m.modules[x].library);
95
+ if (!mods.length) continue;
96
+ body.push(` Container_Boundary(${id("c_", cn)}, ${q(cn)}) {`, ...mods.map((x) => component(x, " ")), " }");
97
+ for (const x of mods) placed.add(x);
98
+ }
99
+ for (const name of Object.keys(m.modules)) if (!placed.has(name)) body.push(component(name, " "));
100
+
101
+ const storageRels: string[] = [];
102
+ const storages = new Set<string>();
103
+ for (const [name, x] of entries(m.modules)) {
104
+ for (const [cn, c] of entries(m.containers)) {
105
+ if (c.modules?.length) continue;
106
+ const clients = (c.clients ?? []).filter((p) => (x.packages ?? []).includes(p));
107
+ if (!clients.length) continue;
108
+ storages.add(cn);
109
+ storageRels.push(rel(id("m_", name), id("c_", cn), clients.join(", ")));
110
+ }
111
+ }
112
+ for (const [cn, c] of entries(m.containers)) if (storages.has(cn)) body.push(containerLine(cn, c, " "));
113
+ body.push(...externalLines(m));
114
+ for (const [name, x] of entries(m.modules)) for (const d of x.dependsOn ?? []) body.push(rel(id("m_", name), id("m_", d), n.spec.c4.dependsOn));
115
+ body.push(...storageRels);
116
+ for (const [name, x] of entries(m.externals)) for (const a of adaptersOf(x)) body.push(rel(id("m_", a), id("ext_", name), n.spec.c4.calls, hosts(x)));
117
+ return fence("C4Component", n.spec.c4.components, body);
118
+ }
119
+
120
+ /** Module table: purpose, directory, public API, which modules it depends on, which packages it imports. */
121
+ export function modulesTable(m: Model, lang: string): string[] {
122
+ const cell = (s: string) => mdText(s).replace(/\|/g, "\\|");
123
+ const code = (xs: string[]) => (xs.length ? xs.map((x) => "`" + x + "`").join(", ") : "—");
124
+ return [
125
+ locale.names(lang).spec.c4.table,
126
+ "|---|---|---|---|---|---|",
127
+ ...entries(m.modules).map(
128
+ ([n, x]) =>
129
+ `| ${cell(n)} | ${cell(x.purpose)} | ${code(paths(x))} | ${code(x.api ? [x.api] : [])} | ${(x.dependsOn ?? []).map(cell).join(", ") || "—"} | ${code(x.packages ?? [])} |`,
130
+ ),
131
+ ];
132
+ }
133
+
134
+ /**
135
+ * Architecture page starting from a heading of level `level`; a level with no data in the model (no external systems,
136
+ * containers) isn't drawn.
137
+ */
138
+ export function architecturePage(m: Model, level: number, lang: string): string[] {
139
+ const n = locale.names(lang);
140
+ const h = (l: number, t: string) => "#".repeat(Math.min(l, 6)) + " " + t;
141
+ const out = [h(level, n.spec.c4.title), "", n.spec.c4.intro, ""];
142
+ if (entries(m.externals).length) out.push(h(level + 1, `${n.spec.c4.context} (C1)`), "", ...c4Context(m, lang), "");
143
+ if (entries(m.containers).length) out.push(h(level + 1, `${n.spec.c4.containers} (C2)`), "", ...c4Container(m, lang), "");
144
+ out.push(h(level + 1, `${n.spec.c4.components} (C3)`), "", ...c4Component(m, lang), "", ...modulesTable(m, lang), "");
145
+ return out;
146
+ }
147
+
148
+ /** Table of contents line: how many modules, containers and external systems the model has. */
149
+ export function modelSummary(m: Model, lang: string): string {
150
+ const n = locale.names(lang);
151
+ const parts = [locale.count(lang, n.spec.c4.moduleCount, Object.keys(m.modules).length)];
152
+ const nc = entries(m.containers).length;
153
+ const ne = entries(m.externals).length;
154
+ if (nc) parts.push(locale.count(lang, n.spec.c4.containerCount, nc));
155
+ if (ne) parts.push(locale.count(lang, n.spec.c4.externalCount, ne));
156
+ return locale.fill(n.spec.c4.summary, { parts: parts.join(", ") });
157
+ }
158
+
159
+ export const MARKS: Record<string, (m: Model, lang: string) => string[]> = {
160
+ "c4-context": c4Context,
161
+ "c4-container": c4Container,
162
+ "c4-component": c4Component,
163
+ "c4-modules": modulesTable,
164
+ };
165
+
166
+ const MARK_LINE = /^[ \t]*<!--\s*spec:\s*([\w-]+)\s*-->[ \t]*$/gm;
167
+
168
+ /**
169
+ * Marks `<!-- spec: <name> -->` on their own line → a diagram from the model in language `lang`. A mark without a model
170
+ * or an unknown one stays (an HTML comment isn't visible on the page) and is returned in `bad` — spec-doc names it.
171
+ */
172
+ export function embed(text: string, model: Model | null, lang: string): { text: string; bad: { mark: string; missing: "model" | "mark" }[] } {
173
+ const bad: { mark: string; missing: "model" | "mark" }[] = [];
174
+ const out = text.replace(MARK_LINE, (line, name: string) => {
175
+ // a scenario diagram doesn't come from the model: spec-doc puts it in from the test's trace
176
+ if (name.startsWith("sequence-")) return line;
177
+ const render = Object.hasOwn(MARKS, name) ? MARKS[name] : undefined;
178
+ if (!render) bad.push({ mark: name, missing: "mark" });
179
+ else if (!model) bad.push({ mark: name, missing: "model" });
180
+ else return render(model, lang).join("\n");
181
+ return line;
182
+ });
183
+ return { text: out, bad };
184
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * eslint-config — assembling ESLint rules from the spec tree: a standard's rule lives as an `eslint.ts` fragment
3
+ * (default — a flat config object or array) in its folder, next to the examples and the reason, and the project's
4
+ * `eslint.config.*` only collects the fragments — `lint` and the editor see the rule as usual:
5
+ *
6
+ * import { collectEslint } from "@miroshnik/elves/spec/eslint-config";
7
+ * export default [...base, ...(await collectEslint(import.meta.dirname))];
8
+ *
9
+ * `files` paths in a fragment are from the project root. Fragments go in path order: the config is the same on any
10
+ * machine. `tests/lib` holds helpers, not fragments. Node loads an `eslint.mts` fragment as ESM even in a project
11
+ * without `"type": "module"`. Only `node:` APIs and erasable TypeScript: the project's ESLint loads this module and the
12
+ * fragments under Node, and Node ≥ 22.18 imports a `.ts` file without a build.
13
+ *
14
+ * A standard's ban is `restrict(...)`: its own rule `spec/<id>` per standard. Flat config doesn't merge one rule's
15
+ * options: with two fragments setting `no-restricted-syntax` on the same files, the last one wins and the first one's
16
+ * bans silently vanish.
17
+ */
18
+
19
+ import { readdirSync } from "node:fs";
20
+ import path from "node:path";
21
+ import { pathToFileURL } from "node:url";
22
+
23
+ const FRAGMENT = new Set(["eslint.ts", "eslint.mts", "eslint.mjs", "eslint.js"]);
24
+ const SKIP = new Set(["node_modules", ".git"]);
25
+ // test helpers, not the spec: `spec-doc` skips them too
26
+ const SKIP_REL = new Set(["tests/lib"]);
27
+
28
+ /** Paths of `eslint.ts` fragments in the directories (by default `tests/`), in order. */
29
+ export function eslintFragments(root: string, dirs: string[] = ["tests"]): string[] {
30
+ const out: string[] = [];
31
+ const walk = (dir: string): void => {
32
+ let entries;
33
+ try {
34
+ entries = readdirSync(dir, { withFileTypes: true });
35
+ } catch {
36
+ return;
37
+ }
38
+ for (const e of entries) {
39
+ const full = path.join(dir, e.name);
40
+ if (e.isDirectory()) {
41
+ if (!SKIP.has(e.name) && !SKIP_REL.has(path.relative(root, full).split(path.sep).join("/"))) walk(full);
42
+ } else if (FRAGMENT.has(e.name)) out.push(full);
43
+ }
44
+ };
45
+ for (const d of dirs) walk(path.join(root, d));
46
+ return out.sort();
47
+ }
48
+
49
+ /**
50
+ * Flat config fragments from the spec tree — for the project's `eslint.config.*`. First — the exceptions ratchet: a rule
51
+ * disabled in code that no longer silences anything is an error (the exception must go, the debt only shrinks).
52
+ */
53
+ export async function collectEslint(root: string, dirs: string[] = ["tests"]): Promise<object[]> {
54
+ const configs: object[] = [{ linterOptions: { reportUnusedDisableDirectives: "error" } }];
55
+ for (const file of eslintFragments(root, dirs)) {
56
+ const mod = (await import(pathToFileURL(file).href)) as { default?: object | object[] };
57
+ if (!mod.default) throw new Error(`${path.relative(root, file)}: no export default — not a flat config fragment`);
58
+ configs.push(...(Array.isArray(mod.default) ? mod.default : [mod.default]));
59
+ }
60
+ return configs;
61
+ }
62
+
63
+ type RuleContext = { report(d: { node: unknown; message: string }): void };
64
+
65
+ /** The shared plugin of standards' bans: one object for all fragments — ESLint doesn't allow redefining a plugin. */
66
+ const SPEC_PLUGIN: { meta: { name: string }; rules: Record<string, object> } = { meta: { name: "spec" }, rules: {} };
67
+
68
+ /**
69
+ * A standard's ban by AST selectors (as in `no-restricted-syntax`) — as its own rule `spec/<id>` in the shared `spec`
70
+ * plugin: bans of different standards on the same files all apply. A standard's `eslint.ts` fragment:
71
+ *
72
+ * export default restrict({ id: "no-alert", selectors: ["CallExpression[callee.name='alert']"], message: "…", files: ["src/**"] });
73
+ */
74
+ export function restrict(o: { id: string; selectors: string[]; message: string; files: string[]; ignores?: string[] }): object[] {
75
+ SPEC_PLUGIN.rules[o.id] = {
76
+ meta: { type: "problem", schema: [], docs: { description: o.message } },
77
+ create(context: RuleContext) {
78
+ return Object.fromEntries(o.selectors.map((sel) => [sel, (node: unknown) => context.report({ node, message: o.message })]));
79
+ },
80
+ };
81
+ return [{ files: o.files, ...(o.ignores ? { ignores: o.ignores } : {}), plugins: { spec: SPEC_PLUGIN }, rules: { [`spec/${o.id}`]: "error" } }];
82
+ }