@aestheticfunction/dspack-gen 0.1.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/CHANGELOG.md +19 -0
- package/LICENSE +202 -0
- package/README.md +147 -0
- package/dist/adapters/anthropic.d.ts +16 -0
- package/dist/adapters/anthropic.js +71 -0
- package/dist/adapters/fake.d.ts +21 -0
- package/dist/adapters/fake.js +32 -0
- package/dist/adapters/index.d.ts +6 -0
- package/dist/adapters/index.js +11 -0
- package/dist/adapters/ollama.d.ts +16 -0
- package/dist/adapters/ollama.js +91 -0
- package/dist/adapters/types.d.ts +66 -0
- package/dist/adapters/types.js +47 -0
- package/dist/audit/report.d.ts +84 -0
- package/dist/audit/report.js +62 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +182 -0
- package/dist/core/compiler.d.ts +39 -0
- package/dist/core/compiler.js +142 -0
- package/dist/core/contract.d.ts +175 -0
- package/dist/core/contract.js +62 -0
- package/dist/core/generation-schema.d.ts +32 -0
- package/dist/core/generation-schema.js +85 -0
- package/dist/core/index.d.ts +12 -0
- package/dist/core/index.js +12 -0
- package/dist/core/lint/findings.d.ts +46 -0
- package/dist/core/lint/findings.js +28 -0
- package/dist/core/lint/index.d.ts +7 -0
- package/dist/core/lint/index.js +67 -0
- package/dist/core/lint/rules.d.ts +27 -0
- package/dist/core/lint/rules.js +212 -0
- package/dist/core/lint/vocabulary.d.ts +13 -0
- package/dist/core/lint/vocabulary.js +71 -0
- package/dist/core/lint/walk.d.ts +14 -0
- package/dist/core/lint/walk.js +30 -0
- package/dist/core/surface-schema.d.ts +78 -0
- package/dist/core/surface-schema.js +85 -0
- package/dist/eval/assert.d.ts +2 -0
- package/dist/eval/assert.js +50 -0
- package/dist/eval/run.d.ts +2 -0
- package/dist/eval/run.js +60 -0
- package/dist/eval/runner.d.ts +36 -0
- package/dist/eval/runner.js +310 -0
- package/dist/eval/types.d.ts +143 -0
- package/dist/eval/types.js +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +14 -0
- package/dist/repair/render.d.ts +20 -0
- package/dist/repair/render.js +28 -0
- package/dist/run/orchestrator.d.ts +90 -0
- package/dist/run/orchestrator.js +191 -0
- package/dist/serve.d.ts +25 -0
- package/dist/serve.js +144 -0
- package/package.json +76 -0
- package/src/adapters/anthropic.ts +88 -0
- package/src/adapters/fake.ts +32 -0
- package/src/adapters/index.ts +13 -0
- package/src/adapters/ollama.ts +123 -0
- package/src/adapters/types.ts +91 -0
- package/src/audit/report.ts +139 -0
- package/src/cli.ts +191 -0
- package/src/core/compiler.ts +205 -0
- package/src/core/contract.ts +205 -0
- package/src/core/generation-schema.ts +99 -0
- package/src/core/index.ts +12 -0
- package/src/core/lint/findings.ts +80 -0
- package/src/core/lint/index.ts +80 -0
- package/src/core/lint/rules.ts +320 -0
- package/src/core/lint/vocabulary.ts +80 -0
- package/src/core/lint/walk.ts +44 -0
- package/src/core/surface-schema.ts +86 -0
- package/src/eval/assert.ts +55 -0
- package/src/eval/run.ts +62 -0
- package/src/eval/runner.ts +366 -0
- package/src/eval/types.ts +143 -0
- package/src/index.ts +15 -0
- package/src/repair/render.ts +68 -0
- package/src/run/orchestrator.ts +272 -0
- package/src/serve.ts +164 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal dspack v0.3/v0.4 contract types — exactly the fields the pipeline
|
|
3
|
+
* reads. Deliberately local: `core` (compiler + linter) is protocol-neutral
|
|
4
|
+
* and depends on no emitter and no network module (enforced by core-boundary
|
|
5
|
+
* tests). dspack documents may carry more; unknown properties are ignored per
|
|
6
|
+
* dspack conformance rules.
|
|
7
|
+
*/
|
|
8
|
+
export interface Contract {
|
|
9
|
+
dspack: string;
|
|
10
|
+
name: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
tokens?: Record<string, {
|
|
13
|
+
values?: Record<string, {
|
|
14
|
+
value?: unknown;
|
|
15
|
+
}>;
|
|
16
|
+
}>;
|
|
17
|
+
components?: Record<string, ContractComponent>;
|
|
18
|
+
/** v0.4: contract-defined category registry (spec v0.4 §3). */
|
|
19
|
+
categories?: Record<string, {
|
|
20
|
+
name?: string;
|
|
21
|
+
description: string;
|
|
22
|
+
}>;
|
|
23
|
+
patterns?: ContractPattern[];
|
|
24
|
+
intents?: IntentEntry[];
|
|
25
|
+
rules?: RuleEntry[];
|
|
26
|
+
examples?: ExampleEntry[];
|
|
27
|
+
[k: string]: unknown;
|
|
28
|
+
}
|
|
29
|
+
export interface ContractComponent {
|
|
30
|
+
name: string;
|
|
31
|
+
description: string;
|
|
32
|
+
props?: Record<string, ContractProp>;
|
|
33
|
+
composition?: {
|
|
34
|
+
subComponents?: SubComponent[];
|
|
35
|
+
notes?: string;
|
|
36
|
+
};
|
|
37
|
+
/** v0.4: membership in registered categories. */
|
|
38
|
+
categories?: string[];
|
|
39
|
+
[k: string]: unknown;
|
|
40
|
+
}
|
|
41
|
+
export interface ContractProp {
|
|
42
|
+
type: string;
|
|
43
|
+
description?: string;
|
|
44
|
+
values?: Array<string | number | boolean | {
|
|
45
|
+
value: unknown;
|
|
46
|
+
description?: string;
|
|
47
|
+
}>;
|
|
48
|
+
default?: unknown;
|
|
49
|
+
[k: string]: unknown;
|
|
50
|
+
}
|
|
51
|
+
export interface SubComponent {
|
|
52
|
+
id: string;
|
|
53
|
+
name: string;
|
|
54
|
+
description?: string;
|
|
55
|
+
required?: boolean;
|
|
56
|
+
slot?: string;
|
|
57
|
+
acceptsChildren?: string;
|
|
58
|
+
/** v0.4: membership in registered categories. */
|
|
59
|
+
categories?: string[];
|
|
60
|
+
}
|
|
61
|
+
export interface ContractPattern {
|
|
62
|
+
id: string;
|
|
63
|
+
name?: string;
|
|
64
|
+
guidance?: string;
|
|
65
|
+
[k: string]: unknown;
|
|
66
|
+
}
|
|
67
|
+
export interface IntentEntry {
|
|
68
|
+
id: string;
|
|
69
|
+
name?: string;
|
|
70
|
+
description: string;
|
|
71
|
+
relatedPatterns?: string[];
|
|
72
|
+
tags?: string[];
|
|
73
|
+
}
|
|
74
|
+
export type RuleSeverity = "must" | "should";
|
|
75
|
+
export type RuleType = "component-choice" | "required-composition" | "forbidden-composition" | "required-props";
|
|
76
|
+
export interface RuleBase {
|
|
77
|
+
id: string;
|
|
78
|
+
type: string;
|
|
79
|
+
severity: RuleSeverity;
|
|
80
|
+
rationale: string;
|
|
81
|
+
appliesTo?: {
|
|
82
|
+
intents: string[];
|
|
83
|
+
};
|
|
84
|
+
examples?: string[];
|
|
85
|
+
tags?: string[];
|
|
86
|
+
}
|
|
87
|
+
export interface ComponentChoiceRule extends RuleBase {
|
|
88
|
+
type: "component-choice";
|
|
89
|
+
require?: string[];
|
|
90
|
+
forbid?: string[];
|
|
91
|
+
}
|
|
92
|
+
export interface RequiredCompositionRule extends RuleBase {
|
|
93
|
+
type: "required-composition";
|
|
94
|
+
component: string;
|
|
95
|
+
requiredSubComponents?: Array<{
|
|
96
|
+
id: string;
|
|
97
|
+
min?: number;
|
|
98
|
+
}>;
|
|
99
|
+
requiredProps?: Array<{
|
|
100
|
+
on?: string;
|
|
101
|
+
prop: string;
|
|
102
|
+
oneOf: unknown[];
|
|
103
|
+
}>;
|
|
104
|
+
}
|
|
105
|
+
export interface ForbiddenCompositionRule extends RuleBase {
|
|
106
|
+
type: "forbidden-composition";
|
|
107
|
+
component: string;
|
|
108
|
+
forbiddenDescendants?: string[];
|
|
109
|
+
forbiddenProps?: Array<{
|
|
110
|
+
on?: string;
|
|
111
|
+
prop: string;
|
|
112
|
+
values: unknown[];
|
|
113
|
+
}>;
|
|
114
|
+
/** v0.4: forbid descendants by registered category (spec v0.4 §4.2). */
|
|
115
|
+
forbiddenCategories?: string[];
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* v0.4 required-props (spec v0.4 §4.1): content every instance of `component`
|
|
119
|
+
* must carry DIRECTLY — its own `text` field and/or directly-present props.
|
|
120
|
+
* The one rule type whose `component` accepts a sub-component id.
|
|
121
|
+
*/
|
|
122
|
+
export interface RequiredPropsRule extends RuleBase {
|
|
123
|
+
type: "required-props";
|
|
124
|
+
component: string;
|
|
125
|
+
within?: string;
|
|
126
|
+
requiredText?: true;
|
|
127
|
+
/** v0.4 amendment (2026-07-04): where requiredText looks. Default "self". */
|
|
128
|
+
textScope?: "self" | "subtree";
|
|
129
|
+
requiredProps?: Array<{
|
|
130
|
+
prop: string;
|
|
131
|
+
oneOf?: unknown[];
|
|
132
|
+
}>;
|
|
133
|
+
}
|
|
134
|
+
export type RuleEntry = ComponentChoiceRule | RequiredCompositionRule | ForbiddenCompositionRule | RequiredPropsRule | RuleBase;
|
|
135
|
+
export interface ExampleEntry {
|
|
136
|
+
id: string;
|
|
137
|
+
intent: string;
|
|
138
|
+
name?: string;
|
|
139
|
+
description?: string;
|
|
140
|
+
prompt?: string;
|
|
141
|
+
surface: Surface;
|
|
142
|
+
}
|
|
143
|
+
/** dspack surface v0.1 (schema: dspack repo, dspack.surface.v0_1.schema.json). */
|
|
144
|
+
export interface Surface {
|
|
145
|
+
dspackSurface: string;
|
|
146
|
+
system: string;
|
|
147
|
+
intent: string;
|
|
148
|
+
root: SurfaceNode;
|
|
149
|
+
}
|
|
150
|
+
export interface SurfaceNode {
|
|
151
|
+
component: string;
|
|
152
|
+
id?: string;
|
|
153
|
+
props?: Record<string, unknown>;
|
|
154
|
+
text?: string;
|
|
155
|
+
children?: SurfaceNode[];
|
|
156
|
+
slots?: Record<string, SurfaceNode[]>;
|
|
157
|
+
}
|
|
158
|
+
/** Bare enum values from a prop descriptor (valueDescriptor objects unwrapped). */
|
|
159
|
+
export declare function enumValues(prop: ContractProp): unknown[] | null;
|
|
160
|
+
/** All sub-component ids of the contract, mapped to their parent component id. */
|
|
161
|
+
export declare function subComponentIndex(contract: Contract): Map<string, string>;
|
|
162
|
+
/**
|
|
163
|
+
* Sub-component ids declared by more than one component, with all declaring
|
|
164
|
+
* parents. Spec v0.3 §5 makes document-wide uniqueness normative for
|
|
165
|
+
* contracts using governance blocks: S2 and rule resolution work by id alone
|
|
166
|
+
* and must never depend on object iteration order.
|
|
167
|
+
*/
|
|
168
|
+
export declare function duplicateSubComponentIds(contract: Contract): Map<string, string[]>;
|
|
169
|
+
/**
|
|
170
|
+
* Category memberships by component/sub-component id (v0.4). Ids without
|
|
171
|
+
* memberships are absent. Resolution is through the contract at lint time —
|
|
172
|
+
* categories never appear in surfaces (spec v0.4 §3).
|
|
173
|
+
*/
|
|
174
|
+
export declare function categoryIndex(contract: Contract): Map<string, string[]>;
|
|
175
|
+
export declare function getIntent(contract: Contract, intentId: string): IntentEntry;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal dspack v0.3/v0.4 contract types — exactly the fields the pipeline
|
|
3
|
+
* reads. Deliberately local: `core` (compiler + linter) is protocol-neutral
|
|
4
|
+
* and depends on no emitter and no network module (enforced by core-boundary
|
|
5
|
+
* tests). dspack documents may carry more; unknown properties are ignored per
|
|
6
|
+
* dspack conformance rules.
|
|
7
|
+
*/
|
|
8
|
+
/** Bare enum values from a prop descriptor (valueDescriptor objects unwrapped). */
|
|
9
|
+
export function enumValues(prop) {
|
|
10
|
+
if (prop.type !== "enum" || !Array.isArray(prop.values))
|
|
11
|
+
return null;
|
|
12
|
+
return prop.values.map((v) => (v && typeof v === "object" ? v.value : v));
|
|
13
|
+
}
|
|
14
|
+
/** All sub-component ids of the contract, mapped to their parent component id. */
|
|
15
|
+
export function subComponentIndex(contract) {
|
|
16
|
+
const index = new Map();
|
|
17
|
+
for (const [id, component] of Object.entries(contract.components ?? {})) {
|
|
18
|
+
for (const sub of component.composition?.subComponents ?? [])
|
|
19
|
+
index.set(sub.id, id);
|
|
20
|
+
}
|
|
21
|
+
return index;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Sub-component ids declared by more than one component, with all declaring
|
|
25
|
+
* parents. Spec v0.3 §5 makes document-wide uniqueness normative for
|
|
26
|
+
* contracts using governance blocks: S2 and rule resolution work by id alone
|
|
27
|
+
* and must never depend on object iteration order.
|
|
28
|
+
*/
|
|
29
|
+
export function duplicateSubComponentIds(contract) {
|
|
30
|
+
const parents = new Map();
|
|
31
|
+
for (const [id, component] of Object.entries(contract.components ?? {})) {
|
|
32
|
+
for (const sub of component.composition?.subComponents ?? []) {
|
|
33
|
+
parents.set(sub.id, [...(parents.get(sub.id) ?? []), id]);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return new Map([...parents].filter(([, declaredBy]) => declaredBy.length > 1));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Category memberships by component/sub-component id (v0.4). Ids without
|
|
40
|
+
* memberships are absent. Resolution is through the contract at lint time —
|
|
41
|
+
* categories never appear in surfaces (spec v0.4 §3).
|
|
42
|
+
*/
|
|
43
|
+
export function categoryIndex(contract) {
|
|
44
|
+
const index = new Map();
|
|
45
|
+
for (const [id, component] of Object.entries(contract.components ?? {})) {
|
|
46
|
+
if (component.categories?.length)
|
|
47
|
+
index.set(id, component.categories);
|
|
48
|
+
for (const sub of component.composition?.subComponents ?? []) {
|
|
49
|
+
if (sub.categories?.length)
|
|
50
|
+
index.set(sub.id, sub.categories);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return index;
|
|
54
|
+
}
|
|
55
|
+
export function getIntent(contract, intentId) {
|
|
56
|
+
const intent = (contract.intents ?? []).find((i) => i.id === intentId);
|
|
57
|
+
if (!intent) {
|
|
58
|
+
const known = (contract.intents ?? []).map((i) => i.id).join(", ") || "(none)";
|
|
59
|
+
throw new Error(`intent '${intentId}' is not registered in the contract (known: ${known})`);
|
|
60
|
+
}
|
|
61
|
+
return intent;
|
|
62
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-contract generation schema: the JSON Schema handed to constrained
|
|
3
|
+
* decoding (Ollama `format`, hosted output schemas).
|
|
4
|
+
*
|
|
5
|
+
* Derivation from the contract: full component + sub-component vocabulary as
|
|
6
|
+
* anyOf branches, per-component prop enums, `intent`/`system`/`dspackSurface`
|
|
7
|
+
* as consts, and recursion depth-unrolled ($defs node_0..node_{D-1}; the last
|
|
8
|
+
* level is a leaf) so the schema is non-recursive by construction. Default
|
|
9
|
+
* depth 6 was confirmed by the S0 spike (docs/spike-structured-outputs.md):
|
|
10
|
+
* depths 3–8 compile and are enforced on grammar-backed engines.
|
|
11
|
+
*
|
|
12
|
+
* The three layers, deliberately: this schema encodes vocabulary and shape
|
|
13
|
+
* ONLY — never governance. Encoding rules here would make violations
|
|
14
|
+
* unobservable and the audit trail vacuous. Gate S2 is *defined* as a check on
|
|
15
|
+
* the produced surface; this schema may be reused to implement it, but S2 is
|
|
16
|
+
* always reported independently.
|
|
17
|
+
*
|
|
18
|
+
* Spike-inherited simplifications (documented): children accept the full
|
|
19
|
+
* vocabulary at every level (per-parent child constraints are S3 territory),
|
|
20
|
+
* `text` is allowed on every node, and `slots` are not generated (composition
|
|
21
|
+
* is expressed via children; the surface schema still accepts slots on
|
|
22
|
+
* hand-authored surfaces).
|
|
23
|
+
*/
|
|
24
|
+
import { type Contract } from "./contract.js";
|
|
25
|
+
export declare const DEFAULT_UNROLL_DEPTH = 6;
|
|
26
|
+
export interface GenerationSchemaOptions {
|
|
27
|
+
/** Levels of tree nesting the schema admits. S0-confirmed default: 6. */
|
|
28
|
+
depth?: number;
|
|
29
|
+
}
|
|
30
|
+
type Json = Record<string, unknown>;
|
|
31
|
+
export declare function buildGenerationSchema(contract: Contract, intentId: string, options?: GenerationSchemaOptions): Json;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-contract generation schema: the JSON Schema handed to constrained
|
|
3
|
+
* decoding (Ollama `format`, hosted output schemas).
|
|
4
|
+
*
|
|
5
|
+
* Derivation from the contract: full component + sub-component vocabulary as
|
|
6
|
+
* anyOf branches, per-component prop enums, `intent`/`system`/`dspackSurface`
|
|
7
|
+
* as consts, and recursion depth-unrolled ($defs node_0..node_{D-1}; the last
|
|
8
|
+
* level is a leaf) so the schema is non-recursive by construction. Default
|
|
9
|
+
* depth 6 was confirmed by the S0 spike (docs/spike-structured-outputs.md):
|
|
10
|
+
* depths 3–8 compile and are enforced on grammar-backed engines.
|
|
11
|
+
*
|
|
12
|
+
* The three layers, deliberately: this schema encodes vocabulary and shape
|
|
13
|
+
* ONLY — never governance. Encoding rules here would make violations
|
|
14
|
+
* unobservable and the audit trail vacuous. Gate S2 is *defined* as a check on
|
|
15
|
+
* the produced surface; this schema may be reused to implement it, but S2 is
|
|
16
|
+
* always reported independently.
|
|
17
|
+
*
|
|
18
|
+
* Spike-inherited simplifications (documented): children accept the full
|
|
19
|
+
* vocabulary at every level (per-parent child constraints are S3 territory),
|
|
20
|
+
* `text` is allowed on every node, and `slots` are not generated (composition
|
|
21
|
+
* is expressed via children; the surface schema still accepts slots on
|
|
22
|
+
* hand-authored surfaces).
|
|
23
|
+
*/
|
|
24
|
+
import { enumValues } from "./contract.js";
|
|
25
|
+
export const DEFAULT_UNROLL_DEPTH = 6;
|
|
26
|
+
export function buildGenerationSchema(contract, intentId, options = {}) {
|
|
27
|
+
const depth = options.depth ?? DEFAULT_UNROLL_DEPTH;
|
|
28
|
+
if (!Number.isInteger(depth) || depth < 1)
|
|
29
|
+
throw new Error(`invalid unroll depth ${depth}`);
|
|
30
|
+
const components = contract.components ?? {};
|
|
31
|
+
const $defs = {};
|
|
32
|
+
for (let level = 0; level < depth; level++) {
|
|
33
|
+
const branches = [];
|
|
34
|
+
for (const [id, component] of Object.entries(components)) {
|
|
35
|
+
branches.push(branch(id, componentProps(component), level, depth));
|
|
36
|
+
for (const sub of component.composition?.subComponents ?? []) {
|
|
37
|
+
branches.push(branch(sub.id, null, level, depth));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
$defs[`node_${level}`] = { anyOf: branches };
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
44
|
+
type: "object",
|
|
45
|
+
additionalProperties: false,
|
|
46
|
+
required: ["dspackSurface", "system", "intent", "root"],
|
|
47
|
+
properties: {
|
|
48
|
+
dspackSurface: { const: "0.1" },
|
|
49
|
+
system: { const: contract.name },
|
|
50
|
+
intent: { const: intentId },
|
|
51
|
+
root: { $ref: "#/$defs/node_0" },
|
|
52
|
+
},
|
|
53
|
+
$defs,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function componentProps(component) {
|
|
57
|
+
const entries = Object.entries(component.props ?? {});
|
|
58
|
+
if (entries.length === 0)
|
|
59
|
+
return null;
|
|
60
|
+
const properties = {};
|
|
61
|
+
for (const [name, descriptor] of entries) {
|
|
62
|
+
const values = enumValues(descriptor);
|
|
63
|
+
properties[name] = values
|
|
64
|
+
? { enum: values }
|
|
65
|
+
: descriptor.type === "boolean"
|
|
66
|
+
? { type: "boolean" }
|
|
67
|
+
: descriptor.type === "number"
|
|
68
|
+
? { type: "number" }
|
|
69
|
+
: { type: "string" };
|
|
70
|
+
}
|
|
71
|
+
return { type: "object", additionalProperties: false, properties };
|
|
72
|
+
}
|
|
73
|
+
function branch(id, props, level, depth) {
|
|
74
|
+
const properties = {
|
|
75
|
+
component: { const: id },
|
|
76
|
+
id: { type: "string" },
|
|
77
|
+
text: { type: "string" },
|
|
78
|
+
};
|
|
79
|
+
if (props)
|
|
80
|
+
properties.props = props;
|
|
81
|
+
if (level < depth - 1) {
|
|
82
|
+
properties.children = { type: "array", items: { $ref: `#/$defs/node_${level + 1}` } };
|
|
83
|
+
}
|
|
84
|
+
return { type: "object", additionalProperties: false, required: ["component"], properties };
|
|
85
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @aestheticfunction/dspack-gen/core — the zero-network, emitter-free subset:
|
|
3
|
+
* prompt/context compiler (+ the governance linter, from PR-4 on).
|
|
4
|
+
*
|
|
5
|
+
* ds-mcp's `get-generation-context` / `validate-ui` tools import exactly this
|
|
6
|
+
* subpath; its no-network/read-only invariants depend on this boundary, which
|
|
7
|
+
* is enforced by src/core/core-boundary.test.ts.
|
|
8
|
+
*/
|
|
9
|
+
export * from "./contract.js";
|
|
10
|
+
export * from "./generation-schema.js";
|
|
11
|
+
export * from "./compiler.js";
|
|
12
|
+
export * from "./lint/index.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @aestheticfunction/dspack-gen/core — the zero-network, emitter-free subset:
|
|
3
|
+
* prompt/context compiler (+ the governance linter, from PR-4 on).
|
|
4
|
+
*
|
|
5
|
+
* ds-mcp's `get-generation-context` / `validate-ui` tools import exactly this
|
|
6
|
+
* subpath; its no-network/read-only invariants depend on this boundary, which
|
|
7
|
+
* is enforced by src/core/core-boundary.test.ts.
|
|
8
|
+
*/
|
|
9
|
+
export * from "./contract.js";
|
|
10
|
+
export * from "./generation-schema.js";
|
|
11
|
+
export * from "./compiler.js";
|
|
12
|
+
export * from "./lint/index.js";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lint findings and the gate report — ONE object, two serializations (ADR-7):
|
|
3
|
+
* the JSON here is embedded verbatim in audit reports and rendered
|
|
4
|
+
* deterministically into repair feedback; `renderText` is the human CLI view.
|
|
5
|
+
*
|
|
6
|
+
* Severity carries both faces (ADR-11): `requirement` is the contract's
|
|
7
|
+
* RFC 2119 term (must|should), `level` the tool mapping (error|warn).
|
|
8
|
+
*/
|
|
9
|
+
import type { RuleSeverity } from "../contract.js";
|
|
10
|
+
export type FindingLevel = "error" | "warn";
|
|
11
|
+
export declare const LEVEL_OF: Record<RuleSeverity, FindingLevel>;
|
|
12
|
+
export interface FindingLocation {
|
|
13
|
+
/** Path from the surface root, e.g. `$.root.children[0]`; `$.root` for surface-wide findings. */
|
|
14
|
+
path: string;
|
|
15
|
+
/** The component id at the location, or "surface" for surface-wide findings. */
|
|
16
|
+
component: string;
|
|
17
|
+
nodeId?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface Finding {
|
|
20
|
+
ruleId: string;
|
|
21
|
+
type: string;
|
|
22
|
+
requirement: RuleSeverity;
|
|
23
|
+
level: FindingLevel;
|
|
24
|
+
message: string;
|
|
25
|
+
rationale: string;
|
|
26
|
+
location: FindingLocation;
|
|
27
|
+
exampleIds: string[];
|
|
28
|
+
}
|
|
29
|
+
export type GateName = "S1" | "S2" | "S3";
|
|
30
|
+
export type GateStatus = "PASS" | "FAIL" | "SKIPPED";
|
|
31
|
+
export interface GateReport {
|
|
32
|
+
gate: GateName;
|
|
33
|
+
name: string;
|
|
34
|
+
status: GateStatus;
|
|
35
|
+
/** S1/S2 error strings; S3 uses `findings` instead. */
|
|
36
|
+
errors?: string[];
|
|
37
|
+
}
|
|
38
|
+
export interface LintReport {
|
|
39
|
+
gates: GateReport[];
|
|
40
|
+
findings: Finding[];
|
|
41
|
+
/** Errors only (warn findings never fail the lint in v0.3). */
|
|
42
|
+
errorCount: number;
|
|
43
|
+
warnCount: number;
|
|
44
|
+
pass: boolean;
|
|
45
|
+
}
|
|
46
|
+
export declare function renderText(report: LintReport): string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const LEVEL_OF = {
|
|
2
|
+
must: "error",
|
|
3
|
+
should: "warn",
|
|
4
|
+
};
|
|
5
|
+
export function renderText(report) {
|
|
6
|
+
const lines = [];
|
|
7
|
+
for (const gate of report.gates) {
|
|
8
|
+
lines.push(`gate ${gate.gate} ${gate.name.padEnd(21)} ${gate.status}`);
|
|
9
|
+
for (const error of gate.errors ?? [])
|
|
10
|
+
lines.push(` ✖ ${error}`);
|
|
11
|
+
}
|
|
12
|
+
for (const finding of report.findings) {
|
|
13
|
+
const mark = finding.level === "error" ? "✖" : "▲";
|
|
14
|
+
lines.push(`${mark} ${finding.level} [${finding.requirement}] ${finding.ruleId} [${finding.type}]`);
|
|
15
|
+
const where = finding.location.nodeId
|
|
16
|
+
? `${finding.location.path} (component: ${finding.location.component}, id: "${finding.location.nodeId}")`
|
|
17
|
+
: `${finding.location.path} (component: ${finding.location.component})`;
|
|
18
|
+
lines.push(` at ${where}`);
|
|
19
|
+
lines.push(` ${finding.message}`);
|
|
20
|
+
lines.push(` Rationale: ${finding.rationale}`);
|
|
21
|
+
if (finding.exampleIds.length)
|
|
22
|
+
lines.push(` See example: ${finding.exampleIds.join(", ")}`);
|
|
23
|
+
}
|
|
24
|
+
lines.push(report.pass
|
|
25
|
+
? `lint: PASS — 0 error(s), ${report.warnCount} warning(s)`
|
|
26
|
+
: `lint: FAIL — ${report.errorCount} error(s), ${report.warnCount} warning(s)`);
|
|
27
|
+
return lines.join("\n");
|
|
28
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Contract } from "../contract.js";
|
|
2
|
+
import { type LintReport } from "./findings.js";
|
|
3
|
+
export { renderText, LEVEL_OF } from "./findings.js";
|
|
4
|
+
export type { Finding, FindingLevel, FindingLocation, GateName, GateReport, GateStatus, LintReport } from "./findings.js";
|
|
5
|
+
export { UnknownRuleTypeError } from "./rules.js";
|
|
6
|
+
export { walkSurface } from "./walk.js";
|
|
7
|
+
export declare function lintSurface(surface: unknown, contract: Contract): LintReport;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The surface gates, run in order and independently reported (never implicit
|
|
3
|
+
* steps inside generation):
|
|
4
|
+
* S1 — generic surface schema (vendored dspack.surface.v0_1)
|
|
5
|
+
* S2 — contract vocabulary
|
|
6
|
+
* S3 — governance rules (typed evaluators, registry in rules.ts)
|
|
7
|
+
*
|
|
8
|
+
* S1 failure skips S2/S3 (the tree shape is not trustworthy); S2 failure
|
|
9
|
+
* still evaluates S3 (the walk is safe and more findings help repair).
|
|
10
|
+
* `pass` is false iff any gate FAILs; warn-level findings never fail (v0.3).
|
|
11
|
+
*/
|
|
12
|
+
import Ajv2020 from "ajv/dist/2020.js";
|
|
13
|
+
import { surfaceSchemaV0_1 } from "../surface-schema.js";
|
|
14
|
+
import { checkVocabulary } from "./vocabulary.js";
|
|
15
|
+
import { evaluateRules } from "./rules.js";
|
|
16
|
+
export { renderText, LEVEL_OF } from "./findings.js";
|
|
17
|
+
export { UnknownRuleTypeError } from "./rules.js";
|
|
18
|
+
export { walkSurface } from "./walk.js";
|
|
19
|
+
const ajv = new Ajv2020({ strict: false, allErrors: true });
|
|
20
|
+
const validateSurfaceSchema = ajv.compile(surfaceSchemaV0_1);
|
|
21
|
+
export function lintSurface(surface, contract) {
|
|
22
|
+
const gates = [];
|
|
23
|
+
let findings = [];
|
|
24
|
+
// S1 — generic surface schema.
|
|
25
|
+
const s1Ok = validateSurfaceSchema(surface);
|
|
26
|
+
gates.push({
|
|
27
|
+
gate: "S1",
|
|
28
|
+
name: "surface-schema",
|
|
29
|
+
status: s1Ok ? "PASS" : "FAIL",
|
|
30
|
+
errors: s1Ok
|
|
31
|
+
? undefined
|
|
32
|
+
: (validateSurfaceSchema.errors ?? []).map((e) => `${e.instancePath || "(root)"} ${e.message ?? ""}`.trim()),
|
|
33
|
+
});
|
|
34
|
+
if (!s1Ok) {
|
|
35
|
+
gates.push({ gate: "S2", name: "contract-vocabulary", status: "SKIPPED" });
|
|
36
|
+
gates.push({ gate: "S3", name: "governance", status: "SKIPPED" });
|
|
37
|
+
return summarize(gates, findings);
|
|
38
|
+
}
|
|
39
|
+
const typed = surface;
|
|
40
|
+
// S2 — contract vocabulary (a check on the artifact, whatever produced it).
|
|
41
|
+
const s2Errors = checkVocabulary(typed, contract);
|
|
42
|
+
gates.push({
|
|
43
|
+
gate: "S2",
|
|
44
|
+
name: "contract-vocabulary",
|
|
45
|
+
status: s2Errors.length === 0 ? "PASS" : "FAIL",
|
|
46
|
+
errors: s2Errors.length ? s2Errors : undefined,
|
|
47
|
+
});
|
|
48
|
+
// S3 — governance. Unknown rule types throw (CLI exit 4), never skip.
|
|
49
|
+
findings = evaluateRules(typed, contract);
|
|
50
|
+
gates.push({
|
|
51
|
+
gate: "S3",
|
|
52
|
+
name: "governance",
|
|
53
|
+
status: findings.some((f) => f.level === "error") ? "FAIL" : "PASS",
|
|
54
|
+
});
|
|
55
|
+
return summarize(gates, findings);
|
|
56
|
+
}
|
|
57
|
+
function summarize(gates, findings) {
|
|
58
|
+
const errorCount = findings.filter((f) => f.level === "error").length;
|
|
59
|
+
const warnCount = findings.filter((f) => f.level === "warn").length;
|
|
60
|
+
return {
|
|
61
|
+
gates,
|
|
62
|
+
findings,
|
|
63
|
+
errorCount,
|
|
64
|
+
warnCount,
|
|
65
|
+
pass: gates.every((g) => g.status !== "FAIL"),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate S3 — the rule-type registry and evaluators (normative semantics:
|
|
3
|
+
* spec/dspack-v0.3.md §5.3 and spec/dspack-v0.4.md §4).
|
|
4
|
+
*
|
|
5
|
+
* The registry is the thesis-bearing seam: new rule types land additively in
|
|
6
|
+
* v0.4 by adding an entry — never by touching existing evaluators. v0.4 kept
|
|
7
|
+
* that promise for `required-props` (a new entry); `forbiddenCategories` is
|
|
8
|
+
* the spec'd exception — a new OPTIONAL field on forbidden-composition
|
|
9
|
+
* (v0.4 §4.2; existing fields' semantics frozen). A rule whose type has no
|
|
10
|
+
* registry entry is a HARD error (UnknownRuleTypeError → CLI exit 4):
|
|
11
|
+
* silently skipping would misreport a surface as governed.
|
|
12
|
+
*
|
|
13
|
+
* All three v0.3 evaluators are implemented. (The M1 plan deferred
|
|
14
|
+
* `forbidden-composition` to M2, but the v0.3 shadcn contract carries a
|
|
15
|
+
* UNIVERSAL forbidden-composition rule and spec §5.4 forbids skipping — a
|
|
16
|
+
* two-evaluator linter would hard-error on every lint of the real contract.
|
|
17
|
+
* Deviation flagged for maintainer review in the PR; the evaluator is ~40
|
|
18
|
+
* lines and fixture F5 activates with it.)
|
|
19
|
+
*/
|
|
20
|
+
import type { Contract, Surface } from "../contract.js";
|
|
21
|
+
import { type Finding } from "./findings.js";
|
|
22
|
+
export declare class UnknownRuleTypeError extends Error {
|
|
23
|
+
readonly ruleId: string;
|
|
24
|
+
readonly ruleType: string;
|
|
25
|
+
constructor(ruleId: string, ruleType: string, detail: string);
|
|
26
|
+
}
|
|
27
|
+
export declare function evaluateRules(surface: Surface, contract: Contract): Finding[];
|