backend-skeleton 1.3.0 → 1.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/bin/bskel.mjs +308 -0
- package/contracts/emit.mjs +22 -6
- package/handles/providers/java-spring/plan.mjs +24 -3
- package/handles/providers/java-spring/rules.mjs +143 -0
- package/handles/providers/java-spring/templates/EnforceRules.java.tmpl +44 -0
- package/handles/providers/java-spring/templates/RuleCheck.java.tmpl +215 -0
- package/handles/providers/java-spring/templates/RuleEnforcementAspect.java.tmpl +132 -0
- package/handles/providers/java-spring/templates/RuleSetLoader.java.tmpl +138 -0
- package/handles/providers/python-fastapi/rules.mjs +133 -0
- package/handles/providers/python-fastapi/templates/enforce_rules.py.tmpl +128 -0
- package/handles/providers/python-fastapi/templates/rule_check.py.tmpl +178 -0
- package/handles/providers/python-fastapi/templates/rule_set.py.tmpl +59 -0
- package/handles/providers/typescript-express/plan.mjs +15 -2
- package/handles/providers/typescript-express/rules.mjs +129 -0
- package/handles/providers/typescript-express/templates/enforceRules.ts.tmpl +93 -0
- package/handles/providers/typescript-express/templates/ruleCheck.ts.tmpl +209 -0
- package/handles/providers/typescript-express/templates/ruleSet.ts.tmpl +61 -0
- package/lib/cli.mjs +37 -0
- package/lib/gate-definitions.mjs +27 -1
- package/lib/workflow.mjs +9 -0
- package/package.json +2 -1
- package/rules/compile.mjs +433 -0
- package/rules/derived.mjs +87 -0
- package/rules/diagnostics.mjs +147 -0
- package/rules/store.mjs +141 -0
- package/rules/vocabulary.mjs +172 -0
- package/scanners/adapters/_java-spring-analyzer.mjs +49 -0
- package/scanners/adapters/java-spring.mjs +154 -3
- package/scanners/index.mjs +10 -0
- package/schemas/feature-contract.schema.json +12 -1
- package/schemas/feature-rules.schema.json +139 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// Generated by backend-skeleton (bskel rules emit). Do not hand-edit -- change the source rule
|
|
2
|
+
// and regenerate.
|
|
3
|
+
//
|
|
4
|
+
// D-business-rules (R3/R9): pure, dependency-free executor for one feature's compiled business
|
|
5
|
+
// rules. A DUMB executor by design -- every semantic decision (which assertions exist, whether one
|
|
6
|
+
// applies to a field's type, whether two fields are comparable, whether a transition's states are
|
|
7
|
+
// real) was already made in JS by rules/compile.mjs at `bskel rules check` time. Mirrors
|
|
8
|
+
// handles/providers/java-spring/templates/RuleCheck.java.tmpl (and its python-fastapi sibling
|
|
9
|
+
// rule_check.py.tmpl) field-for-field, so all three runtimes are guaranteed to agree.
|
|
10
|
+
//
|
|
11
|
+
// Redaction invariant, inherited from contractCheck.ts and equally binding here: `Violation.message`
|
|
12
|
+
// MUST NEVER interpolate an observed payload value -- only the JSON Pointer, the rule id, and the
|
|
13
|
+
// rule's own declared bound.
|
|
14
|
+
|
|
15
|
+
export interface Violation {
|
|
16
|
+
ruleId: string;
|
|
17
|
+
pointer: string;
|
|
18
|
+
kind: string;
|
|
19
|
+
message: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface FieldRule {
|
|
23
|
+
id: string;
|
|
24
|
+
pointer: string;
|
|
25
|
+
assert: string;
|
|
26
|
+
value: unknown;
|
|
27
|
+
origin: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CrossRule {
|
|
31
|
+
id: string;
|
|
32
|
+
pointers: string[];
|
|
33
|
+
assert: string;
|
|
34
|
+
types: string[];
|
|
35
|
+
origin: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface TransitionRule {
|
|
39
|
+
id: string;
|
|
40
|
+
pointer: string;
|
|
41
|
+
from: string[];
|
|
42
|
+
to: string[];
|
|
43
|
+
origin: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface RuleOperation {
|
|
47
|
+
field: FieldRule[];
|
|
48
|
+
cross: CrossRule[];
|
|
49
|
+
transition: TransitionRule[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Compiled pointers are always a single top-level "/field" segment (rules/compile.mjs refuses
|
|
53
|
+
* anything deeper) -- this is a lookup, not a JSON Pointer implementation. */
|
|
54
|
+
function at(body: Record<string, unknown>, pointer: string): unknown {
|
|
55
|
+
const value = body[pointer.slice(1)];
|
|
56
|
+
return value === null || value === undefined ? undefined : value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const CROSS_SYMBOLS: Record<string, string> = { lt: '<', lte: '<=', gt: '>', gte: '>=', eq: 'equal to', neq: 'different from' };
|
|
60
|
+
|
|
61
|
+
/** Runs every FIELD and CROSS rule in `rules` against a request body. TRANSITION rules are
|
|
62
|
+
* deliberately NOT run here -- see checkTransitions() for why they need an argument this function
|
|
63
|
+
* does not have. */
|
|
64
|
+
export function check(rules: RuleOperation, body: unknown): Violation[] {
|
|
65
|
+
const violations: Violation[] = [];
|
|
66
|
+
if (typeof body !== 'object' || body === null || Array.isArray(body)) return violations;
|
|
67
|
+
const record = body as Record<string, unknown>;
|
|
68
|
+
for (const rule of rules.field) violations.push(...checkField(rule, record));
|
|
69
|
+
for (const rule of rules.cross) violations.push(...checkCross(rule, record));
|
|
70
|
+
return violations;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function checkField(rule: FieldRule, body: Record<string, unknown>): Violation[] {
|
|
74
|
+
const value = at(body, rule.pointer);
|
|
75
|
+
// An absent optional field is not a violation -- requiredness is the contract's own statement,
|
|
76
|
+
// enforced by contractCheck.ts, deliberately not duplicated in this vocabulary.
|
|
77
|
+
if (value === undefined) return [];
|
|
78
|
+
|
|
79
|
+
switch (rule.assert) {
|
|
80
|
+
case 'minLength': {
|
|
81
|
+
const bound = rule.value as number;
|
|
82
|
+
if (typeof value === 'string' && value.length < bound) {
|
|
83
|
+
return [{ ruleId: rule.id, pointer: rule.pointer, kind: 'minLength', message: `shorter than the required minimum length of ${bound}` }];
|
|
84
|
+
}
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
case 'maxLength': {
|
|
88
|
+
const bound = rule.value as number;
|
|
89
|
+
if (typeof value === 'string' && value.length > bound) {
|
|
90
|
+
return [{ ruleId: rule.id, pointer: rule.pointer, kind: 'maxLength', message: `longer than the permitted maximum length of ${bound}` }];
|
|
91
|
+
}
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
case 'minimum':
|
|
95
|
+
return compareNumber(rule, value, (a, b) => a < b, 'is below the permitted minimum of ');
|
|
96
|
+
case 'maximum':
|
|
97
|
+
return compareNumber(rule, value, (a, b) => a > b, 'is above the permitted maximum of ');
|
|
98
|
+
case 'exclusiveMinimum':
|
|
99
|
+
return compareNumber(rule, value, (a, b) => a <= b, 'must be strictly greater than ');
|
|
100
|
+
case 'exclusiveMaximum':
|
|
101
|
+
return compareNumber(rule, value, (a, b) => a >= b, 'must be strictly less than ');
|
|
102
|
+
case 'multipleOf': {
|
|
103
|
+
const bound = rule.value as number;
|
|
104
|
+
if (typeof value === 'number' && Number.isFinite(value) && bound) {
|
|
105
|
+
const remainder = value % bound;
|
|
106
|
+
if (Math.min(Math.abs(remainder), Math.abs(remainder - bound)) > 1e-9) {
|
|
107
|
+
return [{ ruleId: rule.id, pointer: rule.pointer, kind: 'multipleOf', message: `is not an exact multiple of ${bound}` }];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
case 'enum': {
|
|
113
|
+
const allowed = (rule.value as unknown[]) ?? [];
|
|
114
|
+
if (!allowed.includes(value)) {
|
|
115
|
+
return [{ ruleId: rule.id, pointer: rule.pointer, kind: 'enum', message: `is not one of the ${allowed.length} permitted values` }];
|
|
116
|
+
}
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
default:
|
|
120
|
+
// No default branch that passes silently: an assertion name this executor does not know
|
|
121
|
+
// can only mean the artifact was produced by a NEWER bskel than the code generated here.
|
|
122
|
+
return [{ ruleId: rule.id, pointer: rule.pointer, kind: 'unsupported', message: `rule assertion "${rule.assert}" is not understood by this generated checker -- re-run \`bskel rules emit\` to regenerate it` }];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function compareNumber(rule: FieldRule, value: unknown, violates: (a: number, b: number) => boolean, messagePrefix: string): Violation[] {
|
|
127
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return [];
|
|
128
|
+
const bound = rule.value as number;
|
|
129
|
+
if (violates(value, bound)) {
|
|
130
|
+
return [{ ruleId: rule.id, pointer: rule.pointer, kind: rule.assert, message: `${messagePrefix}${bound}` }];
|
|
131
|
+
}
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function checkCross(rule: CrossRule, body: Record<string, unknown>): Violation[] {
|
|
136
|
+
const [first, second] = rule.pointers;
|
|
137
|
+
const a = at(body, first);
|
|
138
|
+
|
|
139
|
+
if (rule.assert === 'requiredIf') {
|
|
140
|
+
if (a !== undefined && at(body, second) === undefined) {
|
|
141
|
+
return [{ ruleId: rule.id, pointer: second, kind: 'requiredIf', message: `is required because ${first} is present` }];
|
|
142
|
+
}
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
if (rule.assert === 'mutuallyExclusive') {
|
|
146
|
+
const present = rule.pointers.filter((p) => at(body, p) !== undefined);
|
|
147
|
+
if (present.length > 1) {
|
|
148
|
+
return [{ ruleId: rule.id, pointer: first, kind: 'mutuallyExclusive', message: `at most one of ${rule.pointers.join(', ')} may be present, got ${present.length}` }];
|
|
149
|
+
}
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const b = at(body, second);
|
|
154
|
+
// Either side absent means there is nothing to compare -- same reasoning as RuleCheck.java.
|
|
155
|
+
if (a === undefined || b === undefined) return [];
|
|
156
|
+
|
|
157
|
+
let cmp: number;
|
|
158
|
+
if (typeof a === 'number' && typeof b === 'number') {
|
|
159
|
+
cmp = a === b ? 0 : a > b ? 1 : -1;
|
|
160
|
+
} else if (typeof a === 'string' && typeof b === 'string') {
|
|
161
|
+
cmp = a === b ? 0 : a > b ? 1 : -1;
|
|
162
|
+
} else {
|
|
163
|
+
// rules/compile.mjs proved these two pointers were mutually comparable against the contract's
|
|
164
|
+
// declared types, so reaching here means the real payload disagrees with the contract.
|
|
165
|
+
return [{ ruleId: rule.id, pointer: first, kind: 'incomparable', message: `could not be compared with ${second} -- the payload types differ from the contract's declared types` }];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!(rule.assert in CROSS_SYMBOLS)) {
|
|
169
|
+
return [{ ruleId: rule.id, pointer: first, kind: 'unsupported', message: `rule assertion "${rule.assert}" is not understood by this generated checker -- re-run \`bskel rules emit\` to regenerate it` }];
|
|
170
|
+
}
|
|
171
|
+
const violated = ({ lt: cmp >= 0, lte: cmp > 0, gt: cmp <= 0, gte: cmp < 0, eq: cmp !== 0, neq: cmp === 0 } as Record<string, boolean>)[rule.assert];
|
|
172
|
+
if (violated) {
|
|
173
|
+
return [{ ruleId: rule.id, pointer: first, kind: rule.assert, message: `must be ${CROSS_SYMBOLS[rule.assert]} ${second}` }];
|
|
174
|
+
}
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Runs TRANSITION rules. Separate from check() because a transition guard is inherently a
|
|
180
|
+
* statement about a change, and a request body only carries the DESTINATION state -- the current
|
|
181
|
+
* state lives in the application's own datastore, which this function has no access to and must
|
|
182
|
+
* never acquire.
|
|
183
|
+
*
|
|
184
|
+
* `currentStates` maps a rule's pointer (e.g. "/status") to that resource's CURRENT persisted
|
|
185
|
+
* value, supplied by the caller. This is the one place a human must wire something -- the same
|
|
186
|
+
* honest boundary resolver.ts's own patchField draws.
|
|
187
|
+
*
|
|
188
|
+
* Fail-closed: a pointer with no entry in `currentStates` produces an "unchecked" violation, never
|
|
189
|
+
* a silent pass.
|
|
190
|
+
*/
|
|
191
|
+
export function checkTransitions(rules: RuleOperation, body: unknown, currentStates: Record<string, string> | null): Violation[] {
|
|
192
|
+
const violations: Violation[] = [];
|
|
193
|
+
if (typeof body !== 'object' || body === null || Array.isArray(body)) return violations;
|
|
194
|
+
const record = body as Record<string, unknown>;
|
|
195
|
+
for (const rule of rules.transition) {
|
|
196
|
+
const target = at(record, rule.pointer);
|
|
197
|
+
if (typeof target !== 'string') continue; // not changing this field
|
|
198
|
+
if (currentStates === null || !(rule.pointer in currentStates)) {
|
|
199
|
+
violations.push({ ruleId: rule.id, pointer: rule.pointer, kind: 'unchecked', message: 'a transition guard is declared but the current state was not supplied, so it could not be evaluated -- pass it via checkTransitions(..., currentStates)' });
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const fromState = currentStates[rule.pointer];
|
|
203
|
+
const toState = target;
|
|
204
|
+
if (fromState === toState) continue; // no transition is occurring
|
|
205
|
+
if (rule.from.includes(fromState) && rule.to.includes(toState)) continue; // explicitly allowed
|
|
206
|
+
violations.push({ ruleId: rule.id, pointer: rule.pointer, kind: 'transition', message: `this state change is not permitted -- allowed transitions are from {${rule.from.join(', ')}} to {${rule.to.join(', ')}}` });
|
|
207
|
+
}
|
|
208
|
+
return violations;
|
|
209
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Generated by backend-skeleton (bskel rules emit). Do not hand-edit -- change the source rule
|
|
2
|
+
// and regenerate.
|
|
3
|
+
//
|
|
4
|
+
// D-business-rules (R9): loads every `<feature-id>.rules.json` file under this package's own
|
|
5
|
+
// `rulesSchemas/` directory (one per feature `bskel rules emit` has been run against) at MODULE
|
|
6
|
+
// LOAD time and merges them into one flat map keyed by operationId. Mirrors observedSchema.ts's
|
|
7
|
+
// own shape exactly -- same `fs.readdirSync` discovery, same CommonJS `__dirname` anchor (see that
|
|
8
|
+
// module's own doc comment for the full reasoning).
|
|
9
|
+
//
|
|
10
|
+
// This module does NO interpretation. It deserializes an artifact whose every semantic decision
|
|
11
|
+
// was already made and verified in JS by rules/compile.mjs -- see ruleCheck.ts's own doc comment
|
|
12
|
+
// for that boundary.
|
|
13
|
+
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import type { RuleOperation } from './ruleCheck';
|
|
17
|
+
|
|
18
|
+
const SCHEMAS_DIR = path.join(__dirname, 'rulesSchemas');
|
|
19
|
+
|
|
20
|
+
const EMPTY_OPERATION: RuleOperation = { field: [], cross: [], transition: [] };
|
|
21
|
+
|
|
22
|
+
function loadOne(filePath: string): Record<string, RuleOperation> {
|
|
23
|
+
let raw: any;
|
|
24
|
+
try {
|
|
25
|
+
raw = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
26
|
+
} catch (err) {
|
|
27
|
+
console.warn(`ruleSet: could not read/parse ${filePath} -- its rules will not be enforced`, err);
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
const operations: Record<string, RuleOperation> = {};
|
|
31
|
+
for (const [operationId, entry] of Object.entries<any>(raw.operations ?? {})) {
|
|
32
|
+
operations[operationId] = {
|
|
33
|
+
field: entry.field ?? [],
|
|
34
|
+
cross: entry.cross ?? [],
|
|
35
|
+
transition: entry.transition ?? [],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return operations;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function loadAll(): Map<string, RuleOperation> {
|
|
42
|
+
const merged = new Map<string, RuleOperation>();
|
|
43
|
+
if (!fs.existsSync(SCHEMAS_DIR)) return merged;
|
|
44
|
+
const files = fs.readdirSync(SCHEMAS_DIR).filter((f) => f.endsWith('.rules.json')).sort();
|
|
45
|
+
for (const file of files) {
|
|
46
|
+
for (const [operationId, ops] of Object.entries(loadOne(path.join(SCHEMAS_DIR, file)))) {
|
|
47
|
+
if (merged.has(operationId)) {
|
|
48
|
+
console.warn(`ruleSet: operationId "${operationId}" is declared by more than one *.rules.json on disk -- the last one loaded wins`);
|
|
49
|
+
}
|
|
50
|
+
merged.set(operationId, ops);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return merged;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const OPERATIONS = loadAll();
|
|
57
|
+
|
|
58
|
+
/** Never undefined -- an operation with no rules returns the shared empty operation object. */
|
|
59
|
+
export function forOperation(operationId: string): RuleOperation {
|
|
60
|
+
return OPERATIONS.get(operationId) ?? EMPTY_OPERATION;
|
|
61
|
+
}
|
package/lib/cli.mjs
CHANGED
|
@@ -339,6 +339,43 @@ export const COMMANDS = {
|
|
|
339
339
|
json: { type: 'boolean', default: false },
|
|
340
340
|
},
|
|
341
341
|
},
|
|
342
|
+
// D-business-rules: `check` compiles + verifies + establishes the `rules` gate; `list`/`explain`
|
|
343
|
+
// are read-only. `--init` writes a starter rules.yaml only when none exists (never overwrites).
|
|
344
|
+
'rules check': {
|
|
345
|
+
usage: 'bskel rules check --feature <id> [--init] [--json]',
|
|
346
|
+
options: {
|
|
347
|
+
feature: { type: 'string', default: null, required: true },
|
|
348
|
+
init: { type: 'boolean', default: false },
|
|
349
|
+
json: { type: 'boolean', default: false },
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
'rules list': {
|
|
353
|
+
usage: 'bskel rules list --feature <id> [--json]',
|
|
354
|
+
options: {
|
|
355
|
+
feature: { type: 'string', default: null, required: true },
|
|
356
|
+
json: { type: 'boolean', default: false },
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
'rules explain': {
|
|
360
|
+
usage: 'bskel rules explain --feature <id> --rule <id> [--json]',
|
|
361
|
+
options: {
|
|
362
|
+
feature: { type: 'string', default: null, required: true },
|
|
363
|
+
rule: { type: 'string', default: null, required: true },
|
|
364
|
+
json: { type: 'boolean', default: false },
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
'rules emit': {
|
|
368
|
+
usage: 'bskel rules emit --feature <id> [--module <name>] [--check] [--diff] [--force --reason "..."] [--json]',
|
|
369
|
+
options: {
|
|
370
|
+
feature: { type: 'string', default: null, required: true },
|
|
371
|
+
module: { type: 'string', default: null },
|
|
372
|
+
check: { type: 'boolean', default: false },
|
|
373
|
+
diff: { type: 'boolean', default: false },
|
|
374
|
+
force: { type: 'boolean', default: false },
|
|
375
|
+
reason: { type: 'string', default: '' },
|
|
376
|
+
json: { type: 'boolean', default: false },
|
|
377
|
+
},
|
|
378
|
+
},
|
|
342
379
|
'stack apply': {
|
|
343
380
|
usage: 'bskel stack apply --choice <id> [--apply] [--port N] [--force --reason "..."] [--json]',
|
|
344
381
|
options: {
|
package/lib/gate-definitions.mjs
CHANGED
|
@@ -295,6 +295,32 @@ export const GATE_DEFINITIONS = Object.freeze({
|
|
|
295
295
|
return inputs;
|
|
296
296
|
},
|
|
297
297
|
},
|
|
298
|
+
// D-business-rules (R7): a feature's compiled business rules. Its own gate rather than a
|
|
299
|
+
// widening of `dependencies` -- the same call cross_feature/dependencies/patch_transactions/
|
|
300
|
+
// conformance each made rather than overloading a neighbour, and the two genuinely differ:
|
|
301
|
+
// a dependency is an edge between two fields, a rule is a constraint checked against one
|
|
302
|
+
// feature's own contract. REQUIRED_WHEN_PRESENT because most features will never declare one.
|
|
303
|
+
//
|
|
304
|
+
// Positioned between `dependencies` and `handles`: after dependencies because a derived-field
|
|
305
|
+
// rule references a declared dependency edge, before handles because that is where generated
|
|
306
|
+
// code appears.
|
|
307
|
+
//
|
|
308
|
+
// contract_hash is an input, not just the two rules files: every pointer, scalar type, and
|
|
309
|
+
// enum state in a compiled rule was verified against the contract at compile time, so
|
|
310
|
+
// re-emitting the contract can invalidate a rule that still looks fine on its own. Without
|
|
311
|
+
// this token a contract change could silently leave a rule enforcing against a field that no
|
|
312
|
+
// longer exists. (`contract` is itself a REQUIRED gate, so this is defence in depth, not the
|
|
313
|
+
// only line -- the same relationship `handles`'s own contract_hash token already has.)
|
|
314
|
+
rules: {
|
|
315
|
+
name: 'rules',
|
|
316
|
+
scope: SCOPE.FEATURE,
|
|
317
|
+
verifyPolicy: VERIFY_POLICY.REQUIRED_WHEN_PRESENT,
|
|
318
|
+
recompute: (root, featureId) => ({
|
|
319
|
+
rules_source_hash: sha256File(specPath(root, featureId, 'rules.yaml')),
|
|
320
|
+
rules_compiled_hash: sha256File(specPath(root, featureId, 'rules', `${featureId}.rules.json`)),
|
|
321
|
+
contract_hash: sha256File(specPath(root, featureId, 'contracts', `${featureId}.schema.json`)),
|
|
322
|
+
}),
|
|
323
|
+
},
|
|
298
324
|
// Staleness = the generated Java (or the contract it was generated from) has moved since
|
|
299
325
|
// emit -- NOT "does the migration still match the DB schema" (unknowable without a live DB
|
|
300
326
|
// connection this tool deliberately never opens on its own, see D-migration-scope). Note
|
|
@@ -403,7 +429,7 @@ export const GATE_DEFINITIONS = Object.freeze({
|
|
|
403
429
|
// test/gate-definitions.test.mjs asserts this stays exactly in sync with GATE_DEFINITIONS' own
|
|
404
430
|
// key set, so a gate added to one and not the other fails loudly instead of silently vanishing
|
|
405
431
|
// from `bskel verify` the way `stack` did before this module existed.
|
|
406
|
-
export const GATE_NAMES = Object.freeze(['preflight', 'scan', 'cross_feature', 'contract', 'dependencies', 'handles', 'stack', 'patch_transactions', 'conformance']);
|
|
432
|
+
export const GATE_NAMES = Object.freeze(['preflight', 'scan', 'cross_feature', 'contract', 'dependencies', 'rules', 'handles', 'stack', 'patch_transactions', 'conformance']);
|
|
407
433
|
|
|
408
434
|
export function getGateDefinition(name) {
|
|
409
435
|
return Object.hasOwn(GATE_DEFINITIONS, name) ? GATE_DEFINITIONS[name] : null;
|
package/lib/workflow.mjs
CHANGED
|
@@ -44,6 +44,11 @@ const ESTABLISH_COMMAND = {
|
|
|
44
44
|
// directly on the first real stale occurrence and throws a raw TypeError instead of a clean
|
|
45
45
|
// stale report.
|
|
46
46
|
dependencies: (id) => `bskel dependency declare --feature ${id} --resource <Type> --field <name> --source-feature <id> --source-resource <Type> --source-field <name> --reason "..."`,
|
|
47
|
+
// D-business-rules (R7): wired in the SAME commit as the `rules` gate itself, per the repeated
|
|
48
|
+
// warnings above -- this exact gap has now been found live three times (dependencies,
|
|
49
|
+
// conformance, and the cross_feature near-miss) and is the single most reliably-recurring
|
|
50
|
+
// mistake in this file.
|
|
51
|
+
rules: (id) => `bskel rules check --feature ${id}`,
|
|
47
52
|
handles: (id) => `bskel handles plan --feature ${id} # then: bskel handles emit --feature ${id}`,
|
|
48
53
|
stack: () => 'bskel stack apply --choice <id> --apply',
|
|
49
54
|
// D-patch-transactions: added in the SAME commit as the gate itself -- per the
|
|
@@ -89,6 +94,10 @@ const MUTATING_PREFIXES = [
|
|
|
89
94
|
'bskel gate force', 'bskel dependency declare', 'bskel dependency remove', 'bskel handles emit',
|
|
90
95
|
'bskel handles plan', 'bskel stack apply', 'bskel observe emit', 'bskel observe import',
|
|
91
96
|
'bskel patch propose', 'bskel patch approve', 'bskel patch apply', 'bskel patch rollback',
|
|
97
|
+
// D-business-rules (R7): `rules check` writes the compiled artifact and passes its gate;
|
|
98
|
+
// `rules emit` writes generated source. `rules list`/`rules explain` are read-only and
|
|
99
|
+
// correctly absent -- the same per-verb split `pattern list|show|suggest` was checked against.
|
|
100
|
+
'bskel rules check', 'bskel rules emit',
|
|
92
101
|
];
|
|
93
102
|
|
|
94
103
|
// Exported (not just inlined into action()) so lib/workflow.mjs's own test suite can assert the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "backend-skeleton",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deterministic gate layer for AI-assisted backend changes -- blocks brownfield collisions and contract/handle drift via disk-hash checks before code ships. Scaffolding codegen included (Java/Spring, Python/FastAPI, TypeScript/Express).",
|
|
6
6
|
"license": "AGPL-3.0-or-later",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"new/",
|
|
28
28
|
"stack/",
|
|
29
29
|
"patterns/",
|
|
30
|
+
"rules/",
|
|
30
31
|
"schemas/",
|
|
31
32
|
"scripts/preflight-base-ref.sh",
|
|
32
33
|
"LICENSE",
|