@venizia/vulcan 0.1.0-0 → 0.1.0-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.
- package/README.md +1 -1
- package/base/rules.md +7 -6
- package/package.json +3 -3
- package/src/doctor/checks.ts +43 -9
- package/src/doctor/makefile.ts +9 -0
- package/src/sync/planner.ts +4 -1
- package/src/sync/settings.ts +31 -0
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ An unknown key is an error; every error names the file and the field.
|
|
|
46
46
|
|
|
47
47
|
## What ships (`base/`)
|
|
48
48
|
|
|
49
|
-
- `rules.md` - the base rules: write boundaries, security, process, build, and the IGNIS coding style V-C-01..V-C-18.
|
|
49
|
+
- `rules.md` - the base rules: write boundaries, security, process, build, and the IGNIS coding style V-C-01..V-C-18 (37 rules; V-W-05, V-C-03 and V-C-05 are retired).
|
|
50
50
|
- `slots.json` - rules a project picks a wording for (V-W-01 today).
|
|
51
51
|
- `skills/` - `knowledge-sync`, `docs-writing` (project-neutral; a project-specific skill stays in its project).
|
|
52
52
|
- `hooks/` - `session-start.ts` (prints the rules digest) and `guard-writes.ts` (asks before a database write, a migration, a deploy or a mutating HTTP call).
|
package/base/rules.md
CHANGED
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- Shipped by @venizia/vulcan. Never edit in a project: add extensions in .agents/rules.md.
|
|
4
4
|
W, S, P, B are INTERIM until the base-rules work reconciles the rules IGNIS, ARDOR and BANA share.
|
|
5
|
-
C is IGNIS's coding style,
|
|
5
|
+
C is IGNIS's coding style, shipped to every project; the rules that bind IGNIS's own stack live in IGNIS. -->
|
|
6
6
|
|
|
7
7
|
Cite by ID. Every rule has "Enforced by". Base IDs carry the `V-` namespace (`V-W-01`, `V-C-04`).
|
|
8
8
|
A project extends this set in its own `.agents/rules.md` with bare IDs (`W-06`, `C-19`), which can
|
|
9
9
|
never collide with a base ID; it never claims the `V-` namespace (`vulcan doctor` enforces it).
|
|
10
10
|
|
|
11
|
+
Retired IDs are never reused: `V-W-05` (task-manager loop), `V-C-03` (`@inject` on container-built
|
|
12
|
+
classes) and `V-C-05` (`getError` / `ApplicationError`) - the last two, with the hard stack once in
|
|
13
|
+
`V-C-18`, are IGNIS rules now (2026-10-17).
|
|
14
|
+
|
|
11
15
|
## W - Write boundaries
|
|
12
16
|
|
|
13
17
|
| ID | Rule | Enforced by |
|
|
@@ -16,7 +20,6 @@ never collide with a base ID; it never claims the `V-` namespace (`vulcan doctor
|
|
|
16
20
|
| V-W-02 | Never write to a database. Write the migration file, hand over. Read-only SELECT ok. | HARD - guard-writes hook (asks) |
|
|
17
21
|
| V-W-03 | Never deploy or hot-apply (kubectl apply, docker compose up, restarts). | HARD - guard-writes hook (asks) |
|
|
18
22
|
| V-W-04 | Ask before anything irreversible: deleting files you didn't create, side-effect APIs, publishing. | SOFT |
|
|
19
|
-
| V-W-05 | Task-manager writes limited to the workflow loop. Bulk edits/deletions need preview + approval. | SOFT |
|
|
20
23
|
|
|
21
24
|
## S - Security
|
|
22
25
|
|
|
@@ -35,7 +38,7 @@ never collide with a base ID; it never claims the `V-` namespace (`vulcan doctor
|
|
|
35
38
|
| ID | Rule | Enforced by |
|
|
36
39
|
|----|------|-------------|
|
|
37
40
|
| V-P-01 | Read `AGENTS.md`, these base rules, the project's `.agents/rules.md` and the relevant `.agents/knowledge/` before acting. | SOFT - session-start hook |
|
|
38
|
-
| V-P-02 |
|
|
41
|
+
| V-P-02 | **Design the surface before the body.** For a feature, propose what its users see first - signatures, types, options, config, output - and get a yes before implementing. Priorities, in order: simplicity > flexibility > completeness. | SOFT |
|
|
39
42
|
| V-P-03 | Plans need human approval before implementation. | SOFT |
|
|
40
43
|
| V-P-04 | Fact changes → update `.agents/knowledge/` in the same change. | SOFT |
|
|
41
44
|
| V-P-05 | Partner, not stenographer. Wrong instruction → say so with reasoning + better option; human decides. Never overrides W rules or V-P-03. | SOFT |
|
|
@@ -56,9 +59,7 @@ never collide with a base ID; it never claims the `V-` namespace (`vulcan doctor
|
|
|
56
59
|
|----|------|-------------|
|
|
57
60
|
| V-C-01 | **English only in code** - identifiers, comments, commit messages, logs, docs. Vietnamese belongs in chat. | SOFT |
|
|
58
61
|
| V-C-02 | **Options objects everywhere:** `fn({ key, value })`, never `fn(key, value)`. **One parameter is not an exception** - `isValidName({ name })`, not `isValidName(name)`. A single positional parameter reads fine until the second one arrives, and by then the method is published and every call site pays. The cost is highest on an interface others implement: `IStorageHelper` carried three positional methods for exactly this reason and the migration touched 72 call sites. Mixing the two styles in one interface is itself the defect - a reader cannot tell which shape a method takes without opening it. | SOFT |
|
|
59
|
-
| V-C-03 | **Every constructor parameter of a container-instantiated class carries `@inject`.** Mixing decorated and undecorated parameters is refused at boot - the container has no channel to supply an undecorated one. Options a controller needs go in `super({ scope: X.name })`, never as an undecorated parameter. | SOFT |
|
|
60
62
|
| V-C-04 | **Never abbreviate identifiers:** `ProductRepository` not `ProductRepo`, `ProductDocument` not `ProductDoc`; type parameters too (`TDocument`, not `TDoc`). A project lists its own exceptions in its rules (IGNIS: `tz` for a time zone). | SOFT |
|
|
61
|
-
| V-C-05 | **Errors are `getError` / `ApplicationError`. Never `new Error`.** Never `instanceof ApplicationError` across a package boundary - the dual CJS+ESM build gives the class several identities; use `isApplicationError()`. | SOFT |
|
|
62
63
|
| V-C-06 | **Never a silent catch - log it.** Always braces. Early return. `switch` with `default` over a long if-else chain. Strict TypeScript; no `any` where a type exists. | LINT (braces, floating promises) + SOFT |
|
|
63
64
|
| V-C-07 | **A function name opens with its verb** - `generate` `build` `to` `is` `has` `assert` `extract` `enrich` `get` `resolve`. `is*` returns a boolean and leaves the branch to the caller; `assert*` throws and returns `void`, so the code after it needs none - reach for `assert*` when every caller would throw on `false` anyway. `has*` is the ownership question (`hasBucket`, never `isBucketExists`). That list is for UTILITY functions; service methods lean on a wider set. **Arrow functions, not `function` declarations** (do not churn existing ones). State plus the operations on it live in a class with static members - the `LoggerFactory` shape - never module-level `let` plus exported arrows. | SOFT |
|
|
64
65
|
| V-C-08 | **Enumerable strings are a const class plus `TConstValue`,** never a raw string-literal union. Every such class carries `static readonly SCHEME_SET = new Set([...])` over its values and `static isValid(value: string): boolean` reading it, so a runtime check exists wherever a value crosses a boundary (`IsolationLevels` in connectors is the model; a subclass redeclares `SCHEME_SET` with `...Parent.SCHEME_SET` first). The `T`/`I` prefix follows the declaration keyword: `type` gets `T`, `interface` gets `I`; an interface turned into a type alias is renamed, and no tool catches it - grep both directions. | SOFT |
|
|
@@ -71,4 +72,4 @@ never collide with a base ID; it never claims the `V-` namespace (`vulcan doctor
|
|
|
71
72
|
| V-C-15 | **One method with a mode option, not several specialized methods.** Payload variants are zod-per-variant with `z.infer`, discriminated. | SOFT |
|
|
72
73
|
| V-C-16 | **One concept, one implementation.** When the same question is answered in several places, the bug lives at the seam and no tool catches it. Consolidate to one home before building on top. | SOFT |
|
|
73
74
|
| V-C-17 | **A file is named by its role, never by repeating its folder:** `winston/logger.ts`, not `winston/winston-logger.ts`. | SOFT |
|
|
74
|
-
| V-C-18 | **
|
|
75
|
+
| V-C-18 | **Derived types over duplicates.** Prefer compile-time types derived from their definitions (`typeof schema`, `z.infer<typeof Schema>`, `ReturnType<typeof build>`) over hand-maintained copies - a copy drifts the day its source changes. A project's stack (framework, ORM, validator) is a project rule, not a base rule. | SOFT |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@venizia/vulcan",
|
|
3
|
-
"version": "0.1.0-
|
|
3
|
+
"version": "0.1.0-1",
|
|
4
4
|
"description": "VULCAN - one setup, one workflow for every project: init, sync, doctor, upgrade, and `vulcan x` for the shipped tools. Bun-only.",
|
|
5
5
|
"homepage": "https://github.com/VENIZIA-AI/vulcan/tree/main/packages/cli",
|
|
6
6
|
"repository": {
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@venizia/ignis-helpers": "^0.2.2-5",
|
|
47
|
-
"@venizia/vulcan-atlas": "0.1.0-
|
|
48
|
-
"@venizia/vulcan-okf": "0.1.0-
|
|
47
|
+
"@venizia/vulcan-atlas": "0.1.0-1",
|
|
48
|
+
"@venizia/vulcan-okf": "0.1.0-1",
|
|
49
49
|
"zod": "^4.5.4"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
package/src/doctor/checks.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Glob } from 'bun';
|
|
1
2
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
3
|
import { join, relative } from 'node:path';
|
|
3
4
|
import { BASE_DIRECTORY, ProjectLayout } from '../common/index.ts';
|
|
@@ -6,7 +7,7 @@ import { parseRuleIds, readSlots, RuleConstants } from '../rules/index.ts';
|
|
|
6
7
|
import { planSync, SyncStatuses } from '../sync/index.ts';
|
|
7
8
|
import { DoctorChecks, DoctorConstants } from './common/index.ts';
|
|
8
9
|
import type { IViolation, TDoctorCheck } from './common/index.ts';
|
|
9
|
-
import { parseMakeTargets } from './makefile.ts';
|
|
10
|
+
import { parseMakeIncludes, parseMakeTargets } from './makefile.ts';
|
|
10
11
|
|
|
11
12
|
export interface ICheckContext {
|
|
12
13
|
root: string;
|
|
@@ -119,6 +120,35 @@ export const checkSkills = (opts: ICheckContext): IViolation[] => {
|
|
|
119
120
|
);
|
|
120
121
|
};
|
|
121
122
|
|
|
123
|
+
/**
|
|
124
|
+
* The project's Makefile and every file it includes, recursively, relative to the root. Includes
|
|
125
|
+
* under node_modules are shipped (vulcan.mk), never project targets.
|
|
126
|
+
*/
|
|
127
|
+
const findProjectMakefiles = (opts: { root: string }): string[] => {
|
|
128
|
+
const found: string[] = [];
|
|
129
|
+
const pending = ['Makefile'];
|
|
130
|
+
|
|
131
|
+
while (pending.length > 0) {
|
|
132
|
+
const file = pending.shift() ?? '';
|
|
133
|
+
|
|
134
|
+
if (found.includes(file) || !existsSync(join(opts.root, file))) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
found.push(file);
|
|
139
|
+
|
|
140
|
+
for (const pattern of parseMakeIncludes({
|
|
141
|
+
makefile: readText({ file: join(opts.root, file) }),
|
|
142
|
+
})) {
|
|
143
|
+
const matches = [...new Glob(pattern).scanSync({ cwd: opts.root, onlyFiles: true })].sort();
|
|
144
|
+
|
|
145
|
+
pending.push(...matches.filter(match => !match.split('/').includes('node_modules')));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return found;
|
|
150
|
+
};
|
|
151
|
+
|
|
122
152
|
export const checkMake = (opts: ICheckContext): IViolation[] => {
|
|
123
153
|
const makefile = readText({ file: join(opts.root, 'Makefile') });
|
|
124
154
|
const violations: IViolation[] = [];
|
|
@@ -137,14 +167,18 @@ export const checkMake = (opts: ICheckContext): IViolation[] => {
|
|
|
137
167
|
parseMakeTargets({ makefile: readText({ file: join(BASE_DIRECTORY, 'make', 'vulcan.mk') }) }),
|
|
138
168
|
);
|
|
139
169
|
|
|
140
|
-
for (const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
170
|
+
for (const file of findProjectMakefiles({ root: opts.root })) {
|
|
171
|
+
const targets = parseMakeTargets({ makefile: readText({ file: join(opts.root, file) }) });
|
|
172
|
+
|
|
173
|
+
for (const target of targets.filter(name => shipped.has(name))) {
|
|
174
|
+
violations.push(
|
|
175
|
+
createViolation({
|
|
176
|
+
check: DoctorChecks.MAKE,
|
|
177
|
+
file,
|
|
178
|
+
message: `'${target}' is a shipped target - give the project target another name`,
|
|
179
|
+
}),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
148
182
|
}
|
|
149
183
|
|
|
150
184
|
return violations;
|
package/src/doctor/makefile.ts
CHANGED
|
@@ -6,3 +6,12 @@ export const parseMakeTargets = (opts: { makefile: string }): string[] =>
|
|
|
6
6
|
.split('\n')
|
|
7
7
|
.map(line => RULE_TARGET.exec(line)?.[1])
|
|
8
8
|
.filter((target): target is string => target !== undefined);
|
|
9
|
+
|
|
10
|
+
const INCLUDE_LINE = /^\s*(?:-include|sinclude|include)\s+(.+?)\s*$/;
|
|
11
|
+
|
|
12
|
+
/** The paths and globs an `include`, `-include` or `sinclude` line names; one built from a variable is skipped. */
|
|
13
|
+
export const parseMakeIncludes = (opts: { makefile: string }): string[] =>
|
|
14
|
+
opts.makefile
|
|
15
|
+
.split('\n')
|
|
16
|
+
.flatMap(line => INCLUDE_LINE.exec(line)?.[1].split(/\s+/) ?? [])
|
|
17
|
+
.filter(path => !path.includes('$('));
|
package/src/sync/planner.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type { ISyncAction, TSettings } from './common/index.ts';
|
|
|
19
19
|
import {
|
|
20
20
|
buildProjectHookSettings,
|
|
21
21
|
isSettingsObject,
|
|
22
|
+
keepKeyOrder,
|
|
22
23
|
mergeSettings,
|
|
23
24
|
removeManagedSettings,
|
|
24
25
|
} from './settings.ts';
|
|
@@ -118,11 +119,13 @@ const planSettings = (opts: { root: string; config: TResolvedConfig }): ISyncAct
|
|
|
118
119
|
|
|
119
120
|
const previous = readSettingsFile({ path: managedPath }) ?? {};
|
|
120
121
|
const overlay = buildManagedOverlay({ config: opts.config });
|
|
121
|
-
const
|
|
122
|
+
const merged = mergeSettings({
|
|
122
123
|
base: removeManagedSettings({ settings: current, managed: previous }),
|
|
123
124
|
overlay,
|
|
124
125
|
});
|
|
125
126
|
|
|
127
|
+
const wanted = keepKeyOrder({ settings: merged, reference: current });
|
|
128
|
+
|
|
126
129
|
const text = `${JSON.stringify(wanted, null, 2)}\n`;
|
|
127
130
|
const managedText = `${JSON.stringify(overlay, null, 2)}\n`;
|
|
128
131
|
const before = existsSync(path) ? readFileSync(path, 'utf8') : undefined;
|
package/src/sync/settings.ts
CHANGED
|
@@ -48,6 +48,37 @@ export const mergeSettings = (opts: { base: TSettings; overlay: TSettings }): TS
|
|
|
48
48
|
|
|
49
49
|
const toKey = (opts: { value: unknown }): string => JSON.stringify(opts.value);
|
|
50
50
|
|
|
51
|
+
/** `settings` with its keys in `reference`'s order: keys `reference` has keep their place, new keys follow. */
|
|
52
|
+
const orderKeysLike = (opts: { settings: TSettings; reference: TSettings }): TSettings => {
|
|
53
|
+
const keys = [
|
|
54
|
+
...Object.keys(opts.reference).filter(key => key in opts.settings),
|
|
55
|
+
...Object.keys(opts.settings).filter(key => !(key in opts.reference)),
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
return Object.fromEntries(keys.map(key => [key, opts.settings[key]]));
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* `settings` in the key order the user's file already has, here and one level down: a sync that only
|
|
63
|
+
* reorders keys rewrites the file, and `doctor` then calls a fresh sync stale.
|
|
64
|
+
*/
|
|
65
|
+
export const keepKeyOrder = (opts: { settings: TSettings; reference: TSettings }): TSettings => {
|
|
66
|
+
const ordered = orderKeysLike(opts);
|
|
67
|
+
|
|
68
|
+
for (const key of Object.keys(ordered).filter(entry =>
|
|
69
|
+
SyncConstants.MERGE_ONE_LEVEL.has(entry),
|
|
70
|
+
)) {
|
|
71
|
+
const value = { value: ordered[key] };
|
|
72
|
+
const previous = { value: opts.reference[key] };
|
|
73
|
+
|
|
74
|
+
if (isSettingsObject(value) && isSettingsObject(previous)) {
|
|
75
|
+
ordered[key] = orderKeysLike({ settings: value.value, reference: previous.value });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return ordered;
|
|
80
|
+
};
|
|
81
|
+
|
|
51
82
|
/** `settings` without what `managed` (the overlay of a previous sync) put there; anything else stays. */
|
|
52
83
|
export const removeManagedSettings = (opts: {
|
|
53
84
|
settings: TSettings;
|