@dzhechkov/harness-core 0.3.144 → 0.3.145
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/.dz-manifest.json +36 -12
- package/dist/guard-promotion.d.ts +364 -0
- package/dist/guard-promotion.d.ts.map +1 -0
- package/dist/guard-promotion.js +760 -0
- package/dist/guard-promotion.js.map +1 -0
- package/dist/guard.d.ts +33 -1
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +57 -3
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/sbom.json +71 -11
- package/src/guard-promotion.ts +958 -0
- package/src/guard.ts +76 -3
- package/src/index.ts +4 -0
package/src/guard.ts
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
// PURE: `evaluateGuard` operates over INJECTED FACTS (package.json deps, a drift result, lesson text, README
|
|
12
12
|
// counts, store size) that the CLI gathers. No filesystem here → deterministic + unit-testable without a repo.
|
|
13
13
|
|
|
14
|
+
import { type RuleTemplate, type TemplateParams, type ChangeSet, templateFires, validTemplateParams } from './guard-promotion.js';
|
|
15
|
+
|
|
14
16
|
export type GuardSeverity = 'hard' | 'soft';
|
|
15
17
|
export type GuardOp = 'publish' | 'teach' | 'consolidate' | 'reindex';
|
|
16
18
|
export type GuardVerdict = 'pass' | 'warn' | 'block';
|
|
@@ -23,6 +25,13 @@ export interface GuardRule {
|
|
|
23
25
|
readonly description: string;
|
|
24
26
|
/** false ⇒ the rule is disabled (config override). */
|
|
25
27
|
readonly enabled?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* A PROMOTED rule (`dz guard promote`) carries a template + params instead of a built-in checker.
|
|
30
|
+
* This is the ONLY way a rule id the engine does not know may enter the rule set — and such a rule
|
|
31
|
+
* is forced SOFT unconditionally (see {@link resolveRules}).
|
|
32
|
+
*/
|
|
33
|
+
readonly template?: RuleTemplate;
|
|
34
|
+
readonly params?: TemplateParams;
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
export interface Violation {
|
|
@@ -61,6 +70,16 @@ export interface GuardFacts {
|
|
|
61
70
|
* importer. `parsed:false` (or the fact absent) ⇒ the rule reports nothing — fail-open by construction,
|
|
62
71
|
* because a lockfile we could not read is not evidence of a defect.
|
|
63
72
|
*/
|
|
73
|
+
/**
|
|
74
|
+
* for TEMPLATE rules (promoted by `dz guard promote`): the change under evaluation — the file list
|
|
75
|
+
* of the working-tree diff, plus the text of those files when a `format-match` rule needs it.
|
|
76
|
+
* ABSENT ⇒ every template rule reports NOTHING (fail-open on missing evidence, the same contract
|
|
77
|
+
* `lockfile-in-sync` follows).
|
|
78
|
+
*/
|
|
79
|
+
readonly change?: {
|
|
80
|
+
readonly files: readonly string[];
|
|
81
|
+
readonly contents?: Readonly<Record<string, string>>;
|
|
82
|
+
};
|
|
64
83
|
readonly lockfile?: {
|
|
65
84
|
readonly parsed: boolean;
|
|
66
85
|
readonly importers?: readonly {
|
|
@@ -308,13 +327,63 @@ const CHECKERS: Record<string, (f: GuardFacts, sev: GuardSeverity) => Violation[
|
|
|
308
327
|
*/
|
|
309
328
|
export const SOFT_ONLY_RULES: readonly string[] = ['lockfile-in-sync'];
|
|
310
329
|
|
|
311
|
-
/**
|
|
330
|
+
/**
|
|
331
|
+
* A well-formed PROMOTED rule: an id the engine does not know, made enforceable by a template +
|
|
332
|
+
* params from the fixed `dz guard promote` vocabulary. Anything half-formed is NOT one, so a
|
|
333
|
+
* hand-edited config cannot smuggle an id past the un-enforceable-rule fail-safe by sprinkling a
|
|
334
|
+
* `template` key on it.
|
|
335
|
+
*/
|
|
336
|
+
export function isTemplateRule(r: Partial<GuardRule> | null | undefined): r is GuardRule & { template: RuleTemplate; params: TemplateParams } {
|
|
337
|
+
return !!r && typeof r === 'object' && validTemplateParams(r.template, r.params);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* The template checker: ONE predicate (`templateFires`) shared with the promoter's historical
|
|
342
|
+
* replay, so the rule the promoter promised and the rule the guard enforces can never diverge.
|
|
343
|
+
* Fail-open on missing evidence (no `change` fact ⇒ nothing reported) and on `undecidable`
|
|
344
|
+
* (a `format-match` whose file contents were not gathered is not a clean change, it is no evidence).
|
|
345
|
+
*/
|
|
346
|
+
function templateChecker(rule: GuardRule & { template: RuleTemplate; params: TemplateParams }): (f: GuardFacts, sev: GuardSeverity) => Violation[] {
|
|
347
|
+
return (f) => {
|
|
348
|
+
const ch = f.change;
|
|
349
|
+
if (!ch || typeof ch !== 'object' || !Array.isArray(ch.files)) return [];
|
|
350
|
+
const change: ChangeSet = { id: 'working-tree', ts: '', files: ch.files, ...(ch.contents !== undefined ? { contents: ch.contents } : {}) };
|
|
351
|
+
const r = templateFires(rule.template, rule.params, change);
|
|
352
|
+
if (Object.hasOwn(r, 'undecidable') || !(r as { fired?: boolean }).fired) return [];
|
|
353
|
+
// A promoted rule is ALWAYS soft, whatever severity reaches this point (belt to resolveRules' braces).
|
|
354
|
+
return [{ rule: rule.id, severity: 'soft', detail: `${(r as { detail?: string }).detail ?? 'template rule fired'} (promoted rule — advisory)` }];
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Merge a user config over the defaults: override severity, disable (enabled:false), never add an
|
|
360
|
+
* un-checked rule — EXCEPT a well-formed template rule (a `dz guard promote` promotion), which is
|
|
361
|
+
* enforceable by construction and is forced SOFT.
|
|
362
|
+
*/
|
|
312
363
|
export function resolveRules(userRules?: readonly Partial<GuardRule>[]): GuardRule[] {
|
|
313
364
|
const byId = new Map<string, GuardRule>(DEFAULT_RULES.map((r) => [r.id, r]));
|
|
314
365
|
for (const u of Array.isArray(userRules) ? userRules : []) {
|
|
315
366
|
if (!u || typeof u.id !== 'string') continue;
|
|
316
367
|
const base = byId.get(u.id);
|
|
317
|
-
if (!base)
|
|
368
|
+
if (!base) {
|
|
369
|
+
// A PROMOTED rule may introduce a new id — but only fully formed, and only SOFT. A promoted
|
|
370
|
+
// rule is derived by a text heuristic from an agent-written lesson: strictly weaker provenance
|
|
371
|
+
// than `lockfile-in-sync`'s tolerant parser, which is already SOFT-only. "I might be wrong"
|
|
372
|
+
// plus "block the publish" is the wrong pair (ADR-004).
|
|
373
|
+
if (isTemplateRule(u)) {
|
|
374
|
+
const ops = Array.isArray(u.ops) && u.ops.every((o) => ['publish', 'teach', 'consolidate', 'reindex'].includes(o as string)) && u.ops.length > 0 ? (u.ops as readonly GuardOp[]) : (['publish'] as const);
|
|
375
|
+
byId.set(u.id, {
|
|
376
|
+
id: u.id,
|
|
377
|
+
severity: 'soft',
|
|
378
|
+
ops,
|
|
379
|
+
description: typeof u.description === 'string' ? u.description : `promoted rule (${u.template})`,
|
|
380
|
+
...(typeof u.enabled === 'boolean' ? { enabled: u.enabled } : {}),
|
|
381
|
+
template: u.template,
|
|
382
|
+
params: u.params,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
continue; // a config rule with no built-in checker is ignored (fail-safe: no un-enforceable rules)
|
|
386
|
+
}
|
|
318
387
|
// A SOFT-ONLY rule keeps its severity even when the config asks for hard (see SOFT_ONLY_RULES).
|
|
319
388
|
const severity = u.severity === 'hard' || u.severity === 'soft' ? u.severity : undefined;
|
|
320
389
|
const allowedSeverity = severity !== undefined && !(severity === 'hard' && SOFT_ONLY_RULES.includes(u.id)) ? severity : undefined;
|
|
@@ -343,7 +412,11 @@ export function evaluateGuard(facts: GuardFacts, rules: readonly GuardRule[] = D
|
|
|
343
412
|
const checked: string[] = [];
|
|
344
413
|
for (const r of active) {
|
|
345
414
|
checked.push(r.id);
|
|
346
|
-
|
|
415
|
+
// A promoted (template) rule has no built-in checker by design — it is enforceable through the
|
|
416
|
+
// shared `templateFires` predicate instead. Without this branch a promoted rule written into
|
|
417
|
+
// `.dz/guard.json` would be INERT: present in the config, listed as checked, enforcing nothing —
|
|
418
|
+
// the exact false-green shape this feature exists to remove (ADR-004).
|
|
419
|
+
const checker = CHECKERS[r.id] ?? (isTemplateRule(r) ? templateChecker(r) : undefined);
|
|
347
420
|
if (!checker) {
|
|
348
421
|
// A rule the caller asked for that has no checker CANNOT silently pass while reporting as checked —
|
|
349
422
|
// that is the smuggled-rule hole. Fail closed: unenforceable ⇒ a HARD violation.
|
package/src/index.ts
CHANGED
|
@@ -316,6 +316,10 @@ export * from './routing-outcomes.js';
|
|
|
316
316
|
export * from './bto-optimize.js';
|
|
317
317
|
export * from './discrimination-gate.js';
|
|
318
318
|
export * from './guard.js';
|
|
319
|
+
// Lesson → guard-rule PROMOTION (feature guard-promotion, scout idea #1) — the cost-of-detection
|
|
320
|
+
// ladder's elevator: moves a lesson from layer 5 (agent memory) to layer 1 (a deterministic rule),
|
|
321
|
+
// but only after TWO consecutive shadow wins replayed over REAL commits. Never synthesises rule code.
|
|
322
|
+
export * from './guard-promotion.js';
|
|
319
323
|
export * from './delivery-check.js';
|
|
320
324
|
|
|
321
325
|
// Skill-registration gate (feature skills-verify, ADR-001) — static layout scan + the deterministic
|