structured-context 0.12.0 → 0.13.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/README.md +29 -216
- package/dist/api.d.ts +17 -17
- package/dist/api.js +12 -12
- package/dist/commands/docs.js +1 -0
- package/dist/commands/providers.d.ts +4 -0
- package/dist/commands/providers.js +52 -0
- package/dist/commands/schemas.js +21 -2
- package/dist/commands/validate-file.d.ts +3 -0
- package/dist/commands/validate-file.js +17 -2
- package/dist/commands/validate.d.ts +4 -0
- package/dist/commands/validate.js +42 -3
- package/dist/config.d.ts +7 -0
- package/dist/config.js +26 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +59 -18
- package/dist/judge/context.d.ts +20 -0
- package/dist/judge/context.js +105 -0
- package/dist/judge/factory.d.ts +44 -0
- package/dist/judge/factory.js +148 -0
- package/dist/judge/index.d.ts +13 -0
- package/dist/judge/index.js +8 -0
- package/dist/judge/judge-node.d.ts +27 -0
- package/dist/judge/judge-node.js +122 -0
- package/dist/judge/prompt.d.ts +10 -0
- package/dist/judge/prompt.js +92 -0
- package/dist/judge/provider/anthropic.d.ts +17 -0
- package/dist/judge/provider/anthropic.js +43 -0
- package/dist/judge/provider/fake.d.ts +26 -0
- package/dist/judge/provider/fake.js +56 -0
- package/dist/judge/provider/jev.d.ts +37 -0
- package/dist/judge/provider/jev.js +169 -0
- package/dist/judge/provider/openai-decisions.d.ts +21 -0
- package/dist/judge/provider/openai-decisions.js +84 -0
- package/dist/judge/provider/openai.d.ts +20 -0
- package/dist/judge/provider/openai.js +53 -0
- package/dist/judge/provider/per-rule.d.ts +10 -0
- package/dist/judge/provider/per-rule.js +18 -0
- package/dist/judge/trim.d.ts +27 -0
- package/dist/judge/trim.js +62 -0
- package/dist/judge/types.d.ts +92 -0
- package/dist/judge/types.js +9 -0
- package/dist/judge/verdicts.d.ts +24 -0
- package/dist/judge/verdicts.js +82 -0
- package/dist/load-space-graph.d.ts +3 -3
- package/dist/load-space-graph.js +4 -4
- package/dist/plugins/loader.js +1 -1
- package/dist/plugins/markdown/parse-embedded.d.ts +2 -0
- package/dist/plugins/markdown/parse-embedded.js +161 -24
- package/dist/plugins/markdown/read-space.js +4 -9
- package/dist/plugins/markdown/render-directory.js +5 -5
- package/dist/plugins/markdown/template-sync.js +3 -4
- package/dist/plugins/markdown/util.d.ts +26 -5
- package/dist/plugins/markdown/util.js +56 -15
- package/dist/render/render.d.ts +1 -1
- package/dist/render/render.js +1 -1
- package/dist/schema/evaluate-rule.d.ts +33 -2
- package/dist/schema/evaluate-rule.js +58 -4
- package/dist/schema/gating.d.ts +69 -0
- package/dist/schema/gating.js +253 -0
- package/dist/schema/metadata-contract.d.ts +625 -12
- package/dist/schema/metadata-contract.js +110 -4
- package/dist/schema/rule-filter.d.ts +12 -0
- package/dist/schema/rule-filter.js +46 -0
- package/dist/schema/rule-severity.d.ts +13 -0
- package/dist/schema/rule-severity.js +33 -0
- package/dist/schema/schema.js +31 -1
- package/dist/schema/validate-rules.d.ts +17 -8
- package/dist/schema/validate-rules.js +91 -48
- package/dist/schemas/_knowledge_wiki.json +4 -4
- package/dist/schemas/_okf.json +172 -0
- package/dist/schemas/_ost_strict.json +15 -6
- package/dist/schemas/_sctx_base.json +1 -1
- package/dist/schemas/generated/0.13.0/_structured_context_schema_meta.json +400 -0
- package/dist/schemas/generated/latest/_structured_context_schema_meta.json +177 -6
- package/dist/schemas/okf.json +8 -0
- package/dist/space-context.d.ts +2 -2
- package/dist/space-context.js +3 -3
- package/dist/space-graph.d.ts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/types.d.ts +11 -4
- package/dist/validate.d.ts +59 -9
- package/dist/validate.js +225 -120
- package/docs/concepts.md +52 -6
- package/docs/config.md +70 -1
- package/docs/rules.md +140 -49
- package/docs/schemas.md +43 -18
- package/package.json +6 -5
- package/schemas/_knowledge_wiki.json +4 -4
- package/schemas/_okf.json +172 -0
- package/schemas/_ost_strict.json +15 -6
- package/schemas/_sctx_base.json +1 -1
- package/schemas/generated/0.13.0/_structured_context_schema_meta.json +400 -0
- package/schemas/generated/latest/_structured_context_schema_meta.json +177 -6
- package/schemas/okf.json +8 -0
|
@@ -43,20 +43,126 @@ const HIERARCHY_LEVEL_SCHEMA = {
|
|
|
43
43
|
required: ['type'],
|
|
44
44
|
additionalProperties: false,
|
|
45
45
|
};
|
|
46
|
+
const SEVERITY_SCHEMA = { enum: ['error', 'warning', 'info'] };
|
|
47
|
+
/** The verdicts the evaluation reserves besides a rule's own; a verdict map may set a severity for them. */
|
|
48
|
+
export const RESERVED_VERDICTS = ['missing-context'];
|
|
49
|
+
/** Related-node kinds a semantic rule's `scope` can add: main-hierarchy edges only. `branch` is ancestors plus descendants. */
|
|
50
|
+
export const SEMANTIC_SCOPES = ['parent', 'siblings', 'children', 'ancestors', 'descendants', 'branch'];
|
|
51
|
+
const VERDICT_SEVERITY_SCHEMA = { enum: ['error', 'warning', 'info', 'ignore'] };
|
|
52
|
+
/** The semantic part of a rule: a model evaluates the node against a rubric and returns a verdict. */
|
|
53
|
+
const SEMANTIC_SCHEMA = {
|
|
54
|
+
type: 'object',
|
|
55
|
+
properties: {
|
|
56
|
+
/** What the model judges the node against. Falls back to the rule's `description`. */
|
|
57
|
+
rubric: { type: 'string', minLength: 1 },
|
|
58
|
+
/**
|
|
59
|
+
* The closed set of verdicts the model may return. Each is a description (what the verdict means, for the
|
|
60
|
+
* model) or `{ description, guidance }`, where `guidance` is shown to the user when it fires.
|
|
61
|
+
* Defaults to `pass`/`fail`.
|
|
62
|
+
*/
|
|
63
|
+
verdicts: {
|
|
64
|
+
type: 'object',
|
|
65
|
+
additionalProperties: {
|
|
66
|
+
oneOf: [
|
|
67
|
+
{ type: 'string', minLength: 1 },
|
|
68
|
+
{
|
|
69
|
+
type: 'object',
|
|
70
|
+
properties: { description: { type: 'string', minLength: 1 }, guidance: { type: 'string', minLength: 1 } },
|
|
71
|
+
required: ['description'],
|
|
72
|
+
additionalProperties: false,
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
minProperties: 2,
|
|
77
|
+
},
|
|
78
|
+
/**
|
|
79
|
+
* Severity per verdict in effect (declared, default or reserved). A verdict mapped to `ignore` is judged but
|
|
80
|
+
* not reported, and a verdict left out passes. Required with `verdicts`; with the default verdicts, `fail`
|
|
81
|
+
* takes the rule's own `severity` instead.
|
|
82
|
+
*/
|
|
83
|
+
severity: {
|
|
84
|
+
type: 'object',
|
|
85
|
+
additionalProperties: VERDICT_SEVERITY_SCHEMA,
|
|
86
|
+
minProperties: 1,
|
|
87
|
+
},
|
|
88
|
+
/**
|
|
89
|
+
* Related nodes, along main-hierarchy edges, sent with the evaluated node. Additive; left out or empty means
|
|
90
|
+
* the evaluated node alone.
|
|
91
|
+
*/
|
|
92
|
+
scope: {
|
|
93
|
+
type: 'array',
|
|
94
|
+
items: { enum: SEMANTIC_SCOPES },
|
|
95
|
+
uniqueItems: true,
|
|
96
|
+
},
|
|
97
|
+
/**
|
|
98
|
+
* Which node bodies are sent: `nodeOnly` (the evaluated node's), `fullScope` (it and every related node's),
|
|
99
|
+
* or `true` for `fullScope`. Left out: no bodies.
|
|
100
|
+
*/
|
|
101
|
+
includeContent: { enum: ['nodeOnly', 'fullScope', true] },
|
|
102
|
+
},
|
|
103
|
+
additionalProperties: false,
|
|
104
|
+
};
|
|
46
105
|
const RULE_SCHEMA = {
|
|
47
106
|
type: 'object',
|
|
48
107
|
properties: {
|
|
49
108
|
id: { type: 'string', minLength: 1 },
|
|
50
109
|
category: { enum: ['validation', 'coherence', 'workflow', 'best-practice'] },
|
|
51
|
-
|
|
110
|
+
/**
|
|
111
|
+
* Severity of a failed `expression` and of the default `fail` verdict. Defaults from the category.
|
|
112
|
+
*/
|
|
113
|
+
severity: SEVERITY_SCHEMA,
|
|
52
114
|
description: { type: 'string', minLength: 1 },
|
|
53
|
-
|
|
115
|
+
/** JSONata test a node must pass. When the rule also has `semantic`, a failing node is reported without being evaluated by the model. */
|
|
116
|
+
expression: { type: 'string', minLength: 1 },
|
|
117
|
+
/** Optional JSONata applicability condition, evaluated per node before the rule runs. The rule applies only when it returns true. */
|
|
118
|
+
when: { type: 'string', minLength: 1 },
|
|
119
|
+
/** A model evaluates the node against a rubric. `{}` takes every default; `true` means the same. */
|
|
120
|
+
semantic: { oneOf: [{ const: true }, SEMANTIC_SCHEMA] },
|
|
54
121
|
type: { type: 'string', minLength: 1 },
|
|
55
|
-
|
|
122
|
+
/** Run once for the whole space instead of once per node. */
|
|
123
|
+
global: { type: 'boolean' },
|
|
56
124
|
override: { type: 'boolean' },
|
|
125
|
+
/** Whether a failure of this rule gates (suppresses) downstream rules on the node's hierarchy subtree. Defaults to true when the failure's severity is `error`. */
|
|
126
|
+
gatesDescendants: { type: 'boolean' },
|
|
57
127
|
},
|
|
58
|
-
required: ['id', 'category', 'description'
|
|
128
|
+
required: ['id', 'category', 'description'],
|
|
59
129
|
additionalProperties: false,
|
|
130
|
+
allOf: [
|
|
131
|
+
// A rule has an expression, a semantic part, or both.
|
|
132
|
+
{ anyOf: [{ required: ['expression'] }, { required: ['semantic'] }] },
|
|
133
|
+
// A global rule has no node to apply to or to evaluate.
|
|
134
|
+
{
|
|
135
|
+
if: { required: ['global'], properties: { global: { const: true } } },
|
|
136
|
+
// biome-ignore lint/suspicious/noThenProperty: JSON Schema `if`/`then` keyword, not a thenable
|
|
137
|
+
then: { not: { anyOf: [{ required: ['when'] }, { required: ['type'] }, { required: ['semantic'] }] } },
|
|
138
|
+
},
|
|
139
|
+
// Custom verdicts need a severity for each failing verdict.
|
|
140
|
+
{
|
|
141
|
+
if: { required: ['semantic'], properties: { semantic: { type: 'object', required: ['verdicts'] } } },
|
|
142
|
+
// biome-ignore lint/suspicious/noThenProperty: JSON Schema `if`/`then` keyword, not a thenable
|
|
143
|
+
then: { properties: { semantic: { type: 'object', required: ['severity'] } } },
|
|
144
|
+
},
|
|
145
|
+
// With the default verdicts, `fail` takes the rule's severity.
|
|
146
|
+
{
|
|
147
|
+
if: { required: ['semantic'], properties: { semantic: { type: 'object', not: { required: ['verdicts'] } } } },
|
|
148
|
+
// biome-ignore lint/suspicious/noThenProperty: JSON Schema `if`/`then` keyword, not a thenable
|
|
149
|
+
then: {
|
|
150
|
+
properties: {
|
|
151
|
+
semantic: { type: 'object', properties: { severity: { type: 'object', not: { required: ['fail'] } } } },
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
// A rule with custom verdicts and no expression has no failure for the top-level severity to describe.
|
|
156
|
+
{
|
|
157
|
+
if: {
|
|
158
|
+
required: ['semantic'],
|
|
159
|
+
properties: { semantic: { type: 'object', required: ['verdicts'] } },
|
|
160
|
+
not: { required: ['expression'] },
|
|
161
|
+
},
|
|
162
|
+
// biome-ignore lint/suspicious/noThenProperty: JSON Schema `if`/`then` keyword, not a thenable
|
|
163
|
+
then: { not: { required: ['severity'] } },
|
|
164
|
+
},
|
|
165
|
+
],
|
|
60
166
|
};
|
|
61
167
|
const REF_SCHEMA = {
|
|
62
168
|
type: 'object',
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Rule } from '../types';
|
|
2
|
+
export declare const RULE_FILTER_DIMENSIONS: readonly ["category", "severity", "type", "id"];
|
|
3
|
+
export type RuleFilterDimension = (typeof RULE_FILTER_DIMENSIONS)[number];
|
|
4
|
+
/** Narrows which rules are evaluated. Dimensions combine with AND; values within a dimension with OR. */
|
|
5
|
+
export type RuleFilter = Partial<Record<RuleFilterDimension, string[]>>;
|
|
6
|
+
/** Parse `dimension=value[,value]` specs (e.g. from repeated `--rule-filter` flags); repeats of a dimension merge. */
|
|
7
|
+
export declare function parseRuleFilter(specs: string[]): RuleFilter;
|
|
8
|
+
/**
|
|
9
|
+
* Keep the rules matching every dimension of the filter. A rule with no `type` never matches a `type` filter,
|
|
10
|
+
* and a rule with a verdict map matches a `severity` filter on any severity it can report.
|
|
11
|
+
*/
|
|
12
|
+
export declare function filterRules(rules: Rule[], filter?: RuleFilter): Rule[];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ruleSeverities } from './rule-severity';
|
|
2
|
+
export const RULE_FILTER_DIMENSIONS = ['category', 'severity', 'type', 'id'];
|
|
3
|
+
/** Parse `dimension=value[,value]` specs (e.g. from repeated `--rule-filter` flags); repeats of a dimension merge. */
|
|
4
|
+
export function parseRuleFilter(specs) {
|
|
5
|
+
const filter = {};
|
|
6
|
+
for (const spec of specs) {
|
|
7
|
+
const eq = spec.indexOf('=');
|
|
8
|
+
const dimension = spec.slice(0, eq);
|
|
9
|
+
const values = spec
|
|
10
|
+
.slice(eq + 1)
|
|
11
|
+
.split(',')
|
|
12
|
+
.map((v) => v.trim())
|
|
13
|
+
.filter(Boolean);
|
|
14
|
+
if (eq < 0 || values.length === 0 || !RULE_FILTER_DIMENSIONS.includes(dimension)) {
|
|
15
|
+
throw new Error(`Invalid rule filter "${spec}". Use <${RULE_FILTER_DIMENSIONS.join('|')}>=<value>[,<value>...].`);
|
|
16
|
+
}
|
|
17
|
+
filter[dimension] = [...(filter[dimension] ?? []), ...values];
|
|
18
|
+
}
|
|
19
|
+
return filter;
|
|
20
|
+
}
|
|
21
|
+
function dimensionValues(rule, dimension) {
|
|
22
|
+
switch (dimension) {
|
|
23
|
+
case 'severity':
|
|
24
|
+
return ruleSeverities(rule);
|
|
25
|
+
case 'category':
|
|
26
|
+
return [rule.category];
|
|
27
|
+
case 'type':
|
|
28
|
+
return rule.type ? [rule.type] : [];
|
|
29
|
+
case 'id':
|
|
30
|
+
return [rule.id];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Keep the rules matching every dimension of the filter. A rule with no `type` never matches a `type` filter,
|
|
35
|
+
* and a rule with a verdict map matches a `severity` filter on any severity it can report.
|
|
36
|
+
*/
|
|
37
|
+
export function filterRules(rules, filter) {
|
|
38
|
+
if (!filter)
|
|
39
|
+
return rules;
|
|
40
|
+
return rules.filter((rule) => RULE_FILTER_DIMENSIONS.every((dim) => {
|
|
41
|
+
const wanted = filter[dim];
|
|
42
|
+
if (!wanted)
|
|
43
|
+
return true;
|
|
44
|
+
return dimensionValues(rule, dim).some((value) => wanted.includes(value));
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Rule, RuleCategory, RuleSeverity } from '../types';
|
|
2
|
+
import type { RuleSemantic } from './metadata-contract';
|
|
3
|
+
/** Default severity for each rule category. A rule's own `severity` field overrides this. */
|
|
4
|
+
export declare const DEFAULT_SEVERITY_BY_CATEGORY: Record<RuleCategory, RuleSeverity>;
|
|
5
|
+
/** The rule's semantic part as an object (`semantic: true` means `{}`), or undefined for a rule without one. */
|
|
6
|
+
export declare function ruleSemantic(rule: Rule): RuleSemantic | undefined;
|
|
7
|
+
/** Severity of a failed `expression` and of the default `fail` verdict: the rule's `severity`, else its category default. */
|
|
8
|
+
export declare function ruleSeverity(rule: Rule): RuleSeverity;
|
|
9
|
+
/**
|
|
10
|
+
* Every severity this rule can report: that of its failed `expression` (or default `fail` verdict), plus those
|
|
11
|
+
* its semantic verdict map sets. `ignore` entries report nothing. Most severe first.
|
|
12
|
+
*/
|
|
13
|
+
export declare function ruleSeverities(rule: Rule): RuleSeverity[];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Default severity for each rule category. A rule's own `severity` field overrides this. */
|
|
2
|
+
export const DEFAULT_SEVERITY_BY_CATEGORY = {
|
|
3
|
+
validation: 'error',
|
|
4
|
+
coherence: 'warning',
|
|
5
|
+
workflow: 'warning',
|
|
6
|
+
'best-practice': 'info',
|
|
7
|
+
};
|
|
8
|
+
const SEVERITY_ORDER = ['error', 'warning', 'info'];
|
|
9
|
+
/** The rule's semantic part as an object (`semantic: true` means `{}`), or undefined for a rule without one. */
|
|
10
|
+
export function ruleSemantic(rule) {
|
|
11
|
+
if (rule.semantic === undefined)
|
|
12
|
+
return undefined;
|
|
13
|
+
return rule.semantic === true ? {} : rule.semantic;
|
|
14
|
+
}
|
|
15
|
+
/** Severity of a failed `expression` and of the default `fail` verdict: the rule's `severity`, else its category default. */
|
|
16
|
+
export function ruleSeverity(rule) {
|
|
17
|
+
return rule.severity ?? DEFAULT_SEVERITY_BY_CATEGORY[rule.category];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Every severity this rule can report: that of its failed `expression` (or default `fail` verdict), plus those
|
|
21
|
+
* its semantic verdict map sets. `ignore` entries report nothing. Most severe first.
|
|
22
|
+
*/
|
|
23
|
+
export function ruleSeverities(rule) {
|
|
24
|
+
const semantic = ruleSemantic(rule);
|
|
25
|
+
const found = new Set();
|
|
26
|
+
if (rule.expression !== undefined || (semantic && !semantic.verdicts))
|
|
27
|
+
found.add(ruleSeverity(rule));
|
|
28
|
+
for (const severity of Object.values(semantic?.severity ?? {})) {
|
|
29
|
+
if (severity !== 'ignore')
|
|
30
|
+
found.add(severity);
|
|
31
|
+
}
|
|
32
|
+
return SEVERITY_ORDER.filter((s) => found.has(s));
|
|
33
|
+
}
|
package/dist/schema/schema.js
CHANGED
|
@@ -183,10 +183,40 @@ export function buildFullRegistry(schemaPath, pluginSchemas) {
|
|
|
183
183
|
}
|
|
184
184
|
return schemaRefRegistry;
|
|
185
185
|
}
|
|
186
|
+
const DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
187
|
+
const DATE_TIME_PATTERN = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(\.\d+)?)?(Z|[+-](\d{2}):(\d{2}))?$/;
|
|
188
|
+
function isCalendarDate(year, month, day) {
|
|
189
|
+
const d = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
|
|
190
|
+
return d.getUTCMonth() === Number(month) - 1 && d.getUTCDate() === Number(day);
|
|
191
|
+
}
|
|
192
|
+
/** ISO 8601 date, e.g. 2026-03-31. */
|
|
193
|
+
function isDate(value) {
|
|
194
|
+
const m = DATE_PATTERN.exec(value);
|
|
195
|
+
return m !== null && isCalendarDate(m[1], m[2], m[3]);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* ISO 8601 datetime. `strict` requires seconds and a UTC offset (e.g. 2026-03-31T09:15:00+10:00);
|
|
199
|
+
* otherwise both are optional, as in the local datetimes Obsidian writes (e.g. 2026-03-31T09:15).
|
|
200
|
+
*/
|
|
201
|
+
function isDateTime(value, strict) {
|
|
202
|
+
const m = DATE_TIME_PATTERN.exec(value);
|
|
203
|
+
if (!m || !isCalendarDate(m[1], m[2], m[3]))
|
|
204
|
+
return false;
|
|
205
|
+
const [, , , , hour, minute, second, , offset, offsetHour, offsetMinute] = m;
|
|
206
|
+
if (strict && (second === undefined || offset === undefined))
|
|
207
|
+
return false;
|
|
208
|
+
return (Number(hour) < 24 &&
|
|
209
|
+
Number(minute) < 60 &&
|
|
210
|
+
Number(second ?? 0) < 60 &&
|
|
211
|
+
Number(offsetHour ?? 0) < 24 &&
|
|
212
|
+
Number(offsetMinute ?? 0) < 60);
|
|
213
|
+
}
|
|
186
214
|
function compileValidator(targetSchema, schemaRefRegistry) {
|
|
187
215
|
const ajv = new Ajv2020();
|
|
188
216
|
ajv.addFormat('path', (value) => value.length > 0 && !value.includes('\0'));
|
|
189
|
-
ajv.addFormat('date',
|
|
217
|
+
ajv.addFormat('date', isDate);
|
|
218
|
+
ajv.addFormat('date-time', (value) => isDateTime(value, true));
|
|
219
|
+
ajv.addFormat('date-or-date-time', (value) => isDate(value) || isDateTime(value, false));
|
|
190
220
|
ajv.addFormat('wikilink', (value) => /^\[\[.+\]\]$/.test(value));
|
|
191
221
|
ajv.addKeyword({
|
|
192
222
|
keyword: '$metadata',
|
|
@@ -1,14 +1,23 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
export
|
|
1
|
+
import type { TrimReport } from '../judge/trim';
|
|
2
|
+
import type { SemanticJudgeProvider } from '../judge/types';
|
|
3
|
+
import type { Rule, RuleViolation, SpaceNode } from '../types';
|
|
4
|
+
export { DEFAULT_SEVERITY_BY_CATEGORY, ruleSeverity } from './rule-severity';
|
|
5
|
+
export interface ValidateRulesOptions {
|
|
6
|
+
/** Evaluates semantic rules. Without one, a rule runs only its `expression`, and a semantic-only rule is skipped. */
|
|
7
|
+
judge?: SemanticJudgeProvider;
|
|
8
|
+
/** Label of the only node to judge (`validate-file`); other nodes' semantic rules are skipped. */
|
|
9
|
+
judgeTarget?: string;
|
|
10
|
+
/** Set of node labels that have already failed upstream structural checks. */
|
|
11
|
+
gatedNodeLabels?: Set<string>;
|
|
12
|
+
trimReport?: TrimReport;
|
|
13
|
+
}
|
|
6
14
|
/**
|
|
7
15
|
* Validate nodes against rules metadata.
|
|
8
|
-
*
|
|
16
|
+
* Evaluates expressions and (when opt-in) semantic parts.
|
|
9
17
|
*
|
|
10
18
|
* @param nodes - All nodes in the space
|
|
11
19
|
* @param rules - Rules metadata list
|
|
12
|
-
* @
|
|
20
|
+
* @param options - Validation options, including the judge for semantic rules
|
|
21
|
+
* @returns Rule violations
|
|
13
22
|
*/
|
|
14
|
-
export declare function validateRules(nodes: SpaceNode[], rules: Rule[]): Promise<RuleViolation[]>;
|
|
23
|
+
export declare function validateRules(nodes: SpaceNode[], rules: Rule[], options?: ValidateRulesOptions): Promise<RuleViolation[]>;
|
|
@@ -1,25 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
workflow: 'warning',
|
|
7
|
-
'best-practice': 'info',
|
|
8
|
-
};
|
|
9
|
-
/** Effective severity of a rule: its explicit `severity`, else its category default. */
|
|
10
|
-
export function ruleSeverity(rule) {
|
|
11
|
-
return rule.severity ?? DEFAULT_SEVERITY_BY_CATEGORY[rule.category];
|
|
12
|
-
}
|
|
1
|
+
import { judgeNodes } from '../judge/judge-node';
|
|
2
|
+
import { buildSpaceGraph } from '../space-graph';
|
|
3
|
+
import { buildEvalContext, evaluateRuleExpression, expressionErrorViolation, tryEvaluateExpression, } from './evaluate-rule';
|
|
4
|
+
import { ruleSeverities, ruleSeverity } from './rule-severity';
|
|
5
|
+
export { DEFAULT_SEVERITY_BY_CATEGORY, ruleSeverity } from './rule-severity';
|
|
13
6
|
/**
|
|
14
7
|
* Validate nodes against rules metadata.
|
|
15
|
-
*
|
|
8
|
+
* Evaluates expressions and (when opt-in) semantic parts.
|
|
16
9
|
*
|
|
17
10
|
* @param nodes - All nodes in the space
|
|
18
11
|
* @param rules - Rules metadata list
|
|
19
|
-
* @
|
|
12
|
+
* @param options - Validation options, including the judge for semantic rules
|
|
13
|
+
* @returns Rule violations
|
|
20
14
|
*/
|
|
21
|
-
export async function validateRules(nodes, rules) {
|
|
15
|
+
export async function validateRules(nodes, rules, options = {}) {
|
|
22
16
|
const violations = [];
|
|
17
|
+
const judge = options.judge;
|
|
23
18
|
// Build node index for efficient lookups
|
|
24
19
|
const nodeIndex = new Map();
|
|
25
20
|
for (const node of nodes) {
|
|
@@ -28,43 +23,91 @@ export async function validateRules(nodes, rules) {
|
|
|
28
23
|
nodeIndex.set(title, node);
|
|
29
24
|
}
|
|
30
25
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
description: rule.description,
|
|
48
|
-
});
|
|
26
|
+
const sentinel = nodes[0];
|
|
27
|
+
const runnable = rules.filter((r) => r.expression !== undefined || (r.semantic !== undefined && judge));
|
|
28
|
+
// The nodes each per-node rule applies to: matching `type` and passing the applicability condition (`when`).
|
|
29
|
+
const applicable = new Map();
|
|
30
|
+
for (const rule of runnable) {
|
|
31
|
+
if (rule.global)
|
|
32
|
+
continue;
|
|
33
|
+
const matching = [];
|
|
34
|
+
for (const node of nodes) {
|
|
35
|
+
if (rule.type && rule.type !== node.resolvedType)
|
|
36
|
+
continue;
|
|
37
|
+
if (rule.when) {
|
|
38
|
+
const when = await tryEvaluateExpression(rule.when, buildEvalContext(node, nodes, nodeIndex));
|
|
39
|
+
if ('error' in when) {
|
|
40
|
+
violations.push(expressionErrorViolation(rule, node.label, when.error));
|
|
41
|
+
continue;
|
|
49
42
|
}
|
|
43
|
+
if (when.value !== true)
|
|
44
|
+
continue;
|
|
50
45
|
}
|
|
46
|
+
matching.push(node);
|
|
51
47
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
48
|
+
applicable.set(rule, matching);
|
|
49
|
+
}
|
|
50
|
+
// 1. Expressions, in rule order. A global rule is evaluated once against the full node set; a sentinel node
|
|
51
|
+
// provides the evaluation context (the nodes array is what matters).
|
|
52
|
+
const failedNodeLabels = new Set(options.gatedNodeLabels); // nodes that failed an expression (or upstream)
|
|
53
|
+
const notEvaluable = new Set(); // `${rule.id}\0${label}` whose own expression failed or errored
|
|
54
|
+
for (const rule of runnable) {
|
|
55
|
+
if (rule.expression === undefined)
|
|
56
|
+
continue;
|
|
57
|
+
if (rule.global) {
|
|
58
|
+
if (!sentinel)
|
|
59
|
+
continue;
|
|
60
|
+
const result = await evaluateRuleExpression(rule, sentinel, nodes, nodeIndex, '');
|
|
61
|
+
if (!result.passed)
|
|
62
|
+
violations.push(result.violation);
|
|
63
|
+
continue;
|
|
67
64
|
}
|
|
65
|
+
for (const node of applicable.get(rule) ?? []) {
|
|
66
|
+
const result = await evaluateRuleExpression(rule, node, nodes, nodeIndex);
|
|
67
|
+
if (result.passed)
|
|
68
|
+
continue;
|
|
69
|
+
violations.push(result.violation);
|
|
70
|
+
notEvaluable.add(`${rule.id}\0${node.label}`);
|
|
71
|
+
if (result.failed)
|
|
72
|
+
failedNodeLabels.add(node.label);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// 2. Semantic parts, for nodes that passed their own expression. A node that failed an expression or
|
|
76
|
+
// upstream is left out (safe cascading default).
|
|
77
|
+
if (judge) {
|
|
78
|
+
const perNode = []; // kept in node order
|
|
79
|
+
const pending = [];
|
|
80
|
+
for (const node of nodes) {
|
|
81
|
+
if (options.judgeTarget !== undefined && node.label !== options.judgeTarget)
|
|
82
|
+
continue;
|
|
83
|
+
const found = [];
|
|
84
|
+
perNode.push(found);
|
|
85
|
+
if (failedNodeLabels.has(node.label))
|
|
86
|
+
continue;
|
|
87
|
+
const semantic = runnable.filter((r) => r.semantic !== undefined && applicable.get(r)?.includes(node) && !notEvaluable.has(`${r.id}\0${node.label}`));
|
|
88
|
+
if (semantic.length > 0)
|
|
89
|
+
pending.push({ node, rules: semantic, found });
|
|
90
|
+
}
|
|
91
|
+
// One provider call for every node, so a provider with `evaluateNodes` can share model calls.
|
|
92
|
+
const judged = await judgeNodes(judge, pending, buildSpaceGraph(nodes, undefined), options.trimReport);
|
|
93
|
+
pending.forEach(({ node, found }, i) => {
|
|
94
|
+
for (const j of judged[i] ?? []) {
|
|
95
|
+
if (!j.failed)
|
|
96
|
+
continue;
|
|
97
|
+
found.push({
|
|
98
|
+
file: node.label,
|
|
99
|
+
ruleId: j.rule.id,
|
|
100
|
+
category: j.rule.category,
|
|
101
|
+
severity: j.severity ?? ruleSeverities(j.rule)[0] ?? ruleSeverity(j.rule),
|
|
102
|
+
description: j.error ? `${j.rule.description} (Semantic evaluation error: ${j.error})` : j.rule.description,
|
|
103
|
+
evidence: j.evidence,
|
|
104
|
+
...(j.verdict !== undefined && { verdict: j.verdict }),
|
|
105
|
+
...(j.guidance !== undefined && { guidance: j.guidance }),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
for (const found of perNode)
|
|
110
|
+
violations.push(...found);
|
|
68
111
|
}
|
|
69
112
|
return violations;
|
|
70
113
|
}
|
|
@@ -50,14 +50,14 @@
|
|
|
50
50
|
"category": "coherence",
|
|
51
51
|
"description": "Syntheses should reference at least one contributing source in their sources array",
|
|
52
52
|
"type": "synthesis",
|
|
53
|
-
"
|
|
53
|
+
"expression": "$exists(current.sources) and $count(current.sources) >= 1"
|
|
54
54
|
},
|
|
55
55
|
{
|
|
56
56
|
"id": "concept-has-summary",
|
|
57
57
|
"category": "best-practice",
|
|
58
58
|
"description": "Concept pages should have a short summary for index discoverability",
|
|
59
59
|
"type": "concept",
|
|
60
|
-
"
|
|
60
|
+
"expression": "$exists(current.summary) and $length(current.summary) > 0"
|
|
61
61
|
}
|
|
62
62
|
]
|
|
63
63
|
},
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
},
|
|
90
90
|
"published_date": {
|
|
91
91
|
"type": "string",
|
|
92
|
-
"format": "date",
|
|
92
|
+
"format": "date-or-date-time",
|
|
93
93
|
"description": "Original publication date"
|
|
94
94
|
},
|
|
95
95
|
"source_type": {
|
|
@@ -213,7 +213,7 @@
|
|
|
213
213
|
"type": { "const": "note" },
|
|
214
214
|
"date": {
|
|
215
215
|
"type": "string",
|
|
216
|
-
"format": "date",
|
|
216
|
+
"format": "date-or-date-time",
|
|
217
217
|
"description": "Date this entry was created or relates to"
|
|
218
218
|
}
|
|
219
219
|
},
|