@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,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt/context compiler: dspack v0.3 contract × declared intent →
|
|
3
|
+
* { system, schema, fewshot }.
|
|
4
|
+
*
|
|
5
|
+
* - `system`: the compiled system prompt — vocabulary, governance rules
|
|
6
|
+
* rendered as instructions with rationales, and design-intent guidance.
|
|
7
|
+
* Deterministic (golden-file tested) and immutable across repair attempts
|
|
8
|
+
* (ADR-7: the only delta between attempts is the repair feedback).
|
|
9
|
+
* - `schema`: the generation schema (see generation-schema.ts). Shape only,
|
|
10
|
+
* never governance — the prompt's rules section is steering; the guarantee
|
|
11
|
+
* is gate S3.
|
|
12
|
+
* - `fewshot`: user/assistant message pairs from the contract's examples for
|
|
13
|
+
* the intent, verbatim (ADR-3: the surface format IS the generation format,
|
|
14
|
+
* so exemplars are exactly in-distribution).
|
|
15
|
+
*/
|
|
16
|
+
import {
|
|
17
|
+
type Contract,
|
|
18
|
+
type ExampleEntry,
|
|
19
|
+
type IntentEntry,
|
|
20
|
+
type RuleEntry,
|
|
21
|
+
categoryIndex,
|
|
22
|
+
enumValues,
|
|
23
|
+
getIntent,
|
|
24
|
+
} from "./contract.js";
|
|
25
|
+
import { buildGenerationSchema, type GenerationSchemaOptions } from "./generation-schema.js";
|
|
26
|
+
|
|
27
|
+
export interface FewshotMessage {
|
|
28
|
+
role: "user" | "assistant";
|
|
29
|
+
content: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CompiledContext {
|
|
33
|
+
system: string;
|
|
34
|
+
schema: Record<string, unknown>;
|
|
35
|
+
fewshot: FewshotMessage[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface CompileOptions extends GenerationSchemaOptions {
|
|
39
|
+
/**
|
|
40
|
+
* Omit the governance-rules section from the system prompt. The linter (S3)
|
|
41
|
+
* is unaffected — this only removes prompt steering. Used by demos/evals to
|
|
42
|
+
* observe first-attempt violation rates honestly.
|
|
43
|
+
*/
|
|
44
|
+
omitRuleSteering?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Rules that fire for an intent: universal rules plus intent-scoped ones. */
|
|
48
|
+
export function applicableRules(contract: Contract, intentId: string): RuleEntry[] {
|
|
49
|
+
return (contract.rules ?? []).filter(
|
|
50
|
+
(rule) => !rule.appliesTo || rule.appliesTo.intents.includes(intentId),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function compileContext(
|
|
55
|
+
contract: Contract,
|
|
56
|
+
intentId: string,
|
|
57
|
+
options: CompileOptions = {},
|
|
58
|
+
): CompiledContext {
|
|
59
|
+
const intent = getIntent(contract, intentId);
|
|
60
|
+
const rules = applicableRules(contract, intentId);
|
|
61
|
+
const examples = (contract.examples ?? []).filter((e) => e.intent === intentId);
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
system: renderSystemPrompt(contract, intent, options.omitRuleSteering ? [] : rules, options),
|
|
65
|
+
schema: buildGenerationSchema(contract, intentId, options),
|
|
66
|
+
fewshot: examples.flatMap((example) => fewshotPair(example)),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function fewshotPair(example: ExampleEntry): FewshotMessage[] {
|
|
71
|
+
return [
|
|
72
|
+
{ role: "user", content: example.prompt ?? example.description ?? example.id },
|
|
73
|
+
{ role: "assistant", content: JSON.stringify(example.surface) },
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function renderSystemPrompt(
|
|
78
|
+
contract: Contract,
|
|
79
|
+
intent: IntentEntry,
|
|
80
|
+
rules: RuleEntry[],
|
|
81
|
+
options: CompileOptions,
|
|
82
|
+
): string {
|
|
83
|
+
const lines: string[] = [
|
|
84
|
+
`You generate user interface surfaces for the "${contract.name}" design system. You must respond`,
|
|
85
|
+
"with a single JSON object conforming to the provided schema — a dspack surface document.",
|
|
86
|
+
"",
|
|
87
|
+
"## Component vocabulary",
|
|
88
|
+
"You may use only these components (with the listed props and allowed values):",
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
for (const [id, component] of Object.entries(contract.components ?? {})) {
|
|
92
|
+
const props = Object.entries(component.props ?? {})
|
|
93
|
+
.map(([name, descriptor]) => {
|
|
94
|
+
const values = enumValues(descriptor);
|
|
95
|
+
return values ? `${name} ∈ {${values.map(String).join(", ")}}` : name;
|
|
96
|
+
})
|
|
97
|
+
.join("; ");
|
|
98
|
+
const subs = (component.composition?.subComponents ?? []).map((s) => s.id).join(", ");
|
|
99
|
+
let line = `- ${id} — ${component.description}`;
|
|
100
|
+
if (props) line += ` Props: ${props}.`;
|
|
101
|
+
if (subs) line += ` Sub-components (used as children): ${subs}.`;
|
|
102
|
+
lines.push(line);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (rules.length > 0) {
|
|
106
|
+
lines.push(
|
|
107
|
+
"",
|
|
108
|
+
`## Governance rules in effect (intent: ${intent.id})`,
|
|
109
|
+
"These are hard requirements. Surfaces violating them will be rejected:",
|
|
110
|
+
);
|
|
111
|
+
rules.forEach((rule, i) => {
|
|
112
|
+
lines.push(`${i + 1}. [${rule.id} / ${rule.severity}] ${ruleInstruction(rule, contract)} Why: ${rule.rationale}`);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
lines.push("", "## Design intent", `Intent "${intent.id}": ${intent.description}`);
|
|
117
|
+
for (const patternId of intent.relatedPatterns ?? []) {
|
|
118
|
+
const pattern = (contract.patterns ?? []).find((p) => p.id === patternId);
|
|
119
|
+
if (pattern?.guidance) lines.push(`Related pattern "${pattern.name ?? pattern.id}": ${pattern.guidance}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
lines.push("", "Output only the JSON object. No commentary.");
|
|
123
|
+
void options;
|
|
124
|
+
return lines.join("\n");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** One-sentence imperative rendering of a rule (the rationale is appended by the caller). */
|
|
128
|
+
export function ruleInstruction(rule: RuleEntry, contract?: Contract): string {
|
|
129
|
+
switch (rule.type) {
|
|
130
|
+
case "component-choice": {
|
|
131
|
+
const r = rule as { require?: string[]; forbid?: string[] };
|
|
132
|
+
const parts: string[] = [];
|
|
133
|
+
if (r.require?.length) parts.push(`Use ${r.require.join(", ")} for this surface`);
|
|
134
|
+
if (r.forbid?.length) parts.push(`${r.forbid.join(", ")} ${r.forbid.length === 1 ? "is" : "are"} forbidden`);
|
|
135
|
+
return `${parts.join("; ")}.`;
|
|
136
|
+
}
|
|
137
|
+
case "required-composition": {
|
|
138
|
+
const r = rule as { component: string; requiredSubComponents?: Array<{ id: string }>; requiredProps?: Array<{ prop: string; oneOf: unknown[] }> };
|
|
139
|
+
const needs: string[] = [];
|
|
140
|
+
if (r.requiredSubComponents?.length) needs.push(r.requiredSubComponents.map((s) => s.id).join(" and "));
|
|
141
|
+
if (r.requiredProps?.length)
|
|
142
|
+
needs.push(r.requiredProps.map((p) => `${p.prop} set to one of ${p.oneOf.map(String).join("/")}`).join(" and "));
|
|
143
|
+
return `Every ${r.component} must contain ${needs.join(", plus ")}.`;
|
|
144
|
+
}
|
|
145
|
+
case "forbidden-composition": {
|
|
146
|
+
const r = rule as {
|
|
147
|
+
component: string;
|
|
148
|
+
forbiddenDescendants?: string[];
|
|
149
|
+
forbiddenProps?: Array<{ prop: string; values: unknown[] }>;
|
|
150
|
+
forbiddenCategories?: string[];
|
|
151
|
+
};
|
|
152
|
+
const parts: string[] = [];
|
|
153
|
+
if (r.forbiddenDescendants?.length)
|
|
154
|
+
parts.push(`Never place ${r.forbiddenDescendants.join(" or ")} inside a ${r.component}`);
|
|
155
|
+
// Steering names the category AND its resolved member ids, so the
|
|
156
|
+
// model sees concrete vocabulary (mirrors the finding message).
|
|
157
|
+
const memberships = r.forbiddenCategories?.length && contract ? categoryIndex(contract) : undefined;
|
|
158
|
+
for (const category of r.forbiddenCategories ?? []) {
|
|
159
|
+
const members = memberships
|
|
160
|
+
? [...memberships].filter(([, cats]) => cats.includes(category)).map(([id]) => id)
|
|
161
|
+
: [];
|
|
162
|
+
parts.push(
|
|
163
|
+
`never place ${category}-category components${members.length ? ` (${members.join(", ")})` : ""} inside ${r.component}`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
if (r.forbiddenProps?.length)
|
|
167
|
+
parts.push(
|
|
168
|
+
r.forbiddenProps
|
|
169
|
+
.map((p) => `never set ${p.prop} to ${p.values.map(String).join("/")} on ${r.component}`)
|
|
170
|
+
.join("; "),
|
|
171
|
+
);
|
|
172
|
+
return `${parts.join("; ")}.`;
|
|
173
|
+
}
|
|
174
|
+
case "required-props": {
|
|
175
|
+
const r = rule as {
|
|
176
|
+
component: string;
|
|
177
|
+
within?: string;
|
|
178
|
+
requiredText?: true;
|
|
179
|
+
textScope?: "self" | "subtree";
|
|
180
|
+
requiredProps?: Array<{ prop: string; oneOf?: unknown[] }>;
|
|
181
|
+
};
|
|
182
|
+
const scope = r.within ? `At least one ${r.component} inside each ${r.within}` : `Every ${r.component}`;
|
|
183
|
+
const needs: string[] = [];
|
|
184
|
+
if (r.requiredText) {
|
|
185
|
+
needs.push(
|
|
186
|
+
r.textScope === "subtree"
|
|
187
|
+
? "contain non-empty text (its own `text` field or a descendant's)"
|
|
188
|
+
: "carry its label as its own `text` field (never nested in a child component)",
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
for (const p of r.requiredProps ?? []) {
|
|
192
|
+
needs.push(p.oneOf ? `set ${p.prop} to one of ${p.oneOf.map(String).join("/")}` : `set ${p.prop} directly`);
|
|
193
|
+
}
|
|
194
|
+
// Schema-invalid but defensive: no constraints to phrase → generic steering,
|
|
195
|
+
// never a malformed "must ;" sentence (the linter still evaluates the rule).
|
|
196
|
+
if (needs.length === 0) return `Follow rule ${rule.id}.`;
|
|
197
|
+
const existence = r.within ? `; every ${r.within} must contain a ${r.component}` : "";
|
|
198
|
+
return `${scope} must ${needs.join(", and ")}${existence}.`;
|
|
199
|
+
}
|
|
200
|
+
default:
|
|
201
|
+
// The linter hard-errors on unknown types (exit 4); the compiler simply
|
|
202
|
+
// does not render steering it cannot phrase.
|
|
203
|
+
return `Follow rule ${rule.id}.`;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
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
|
+
|
|
9
|
+
export interface Contract {
|
|
10
|
+
dspack: string;
|
|
11
|
+
name: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
tokens?: Record<string, { values?: Record<string, { value?: unknown }> }>;
|
|
14
|
+
components?: Record<string, ContractComponent>;
|
|
15
|
+
/** v0.4: contract-defined category registry (spec v0.4 §3). */
|
|
16
|
+
categories?: Record<string, { name?: string; description: string }>;
|
|
17
|
+
patterns?: ContractPattern[];
|
|
18
|
+
intents?: IntentEntry[];
|
|
19
|
+
rules?: RuleEntry[];
|
|
20
|
+
examples?: ExampleEntry[];
|
|
21
|
+
[k: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ContractComponent {
|
|
25
|
+
name: string;
|
|
26
|
+
description: string;
|
|
27
|
+
props?: Record<string, ContractProp>;
|
|
28
|
+
composition?: { subComponents?: SubComponent[]; notes?: string };
|
|
29
|
+
/** v0.4: membership in registered categories. */
|
|
30
|
+
categories?: string[];
|
|
31
|
+
[k: string]: unknown;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ContractProp {
|
|
35
|
+
type: string;
|
|
36
|
+
description?: string;
|
|
37
|
+
values?: Array<string | number | boolean | { value: unknown; description?: string }>;
|
|
38
|
+
default?: unknown;
|
|
39
|
+
[k: string]: unknown;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SubComponent {
|
|
43
|
+
id: string;
|
|
44
|
+
name: string;
|
|
45
|
+
description?: string;
|
|
46
|
+
required?: boolean;
|
|
47
|
+
slot?: string;
|
|
48
|
+
acceptsChildren?: string;
|
|
49
|
+
/** v0.4: membership in registered categories. */
|
|
50
|
+
categories?: string[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ContractPattern {
|
|
54
|
+
id: string;
|
|
55
|
+
name?: string;
|
|
56
|
+
guidance?: string;
|
|
57
|
+
[k: string]: unknown;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface IntentEntry {
|
|
61
|
+
id: string;
|
|
62
|
+
name?: string;
|
|
63
|
+
description: string;
|
|
64
|
+
relatedPatterns?: string[];
|
|
65
|
+
tags?: string[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type RuleSeverity = "must" | "should";
|
|
69
|
+
export type RuleType = "component-choice" | "required-composition" | "forbidden-composition" | "required-props";
|
|
70
|
+
|
|
71
|
+
export interface RuleBase {
|
|
72
|
+
id: string;
|
|
73
|
+
type: string; // validated against RuleType by the linter; unknown => hard error
|
|
74
|
+
severity: RuleSeverity;
|
|
75
|
+
rationale: string;
|
|
76
|
+
appliesTo?: { intents: string[] };
|
|
77
|
+
examples?: string[];
|
|
78
|
+
tags?: string[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface ComponentChoiceRule extends RuleBase {
|
|
82
|
+
type: "component-choice";
|
|
83
|
+
require?: string[];
|
|
84
|
+
forbid?: string[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface RequiredCompositionRule extends RuleBase {
|
|
88
|
+
type: "required-composition";
|
|
89
|
+
component: string;
|
|
90
|
+
requiredSubComponents?: Array<{ id: string; min?: number }>;
|
|
91
|
+
requiredProps?: Array<{ on?: string; prop: string; oneOf: unknown[] }>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface ForbiddenCompositionRule extends RuleBase {
|
|
95
|
+
type: "forbidden-composition";
|
|
96
|
+
component: string;
|
|
97
|
+
forbiddenDescendants?: string[];
|
|
98
|
+
forbiddenProps?: Array<{ on?: string; prop: string; values: unknown[] }>;
|
|
99
|
+
/** v0.4: forbid descendants by registered category (spec v0.4 §4.2). */
|
|
100
|
+
forbiddenCategories?: string[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* v0.4 required-props (spec v0.4 §4.1): content every instance of `component`
|
|
105
|
+
* must carry DIRECTLY — its own `text` field and/or directly-present props.
|
|
106
|
+
* The one rule type whose `component` accepts a sub-component id.
|
|
107
|
+
*/
|
|
108
|
+
export interface RequiredPropsRule extends RuleBase {
|
|
109
|
+
type: "required-props";
|
|
110
|
+
component: string;
|
|
111
|
+
within?: string;
|
|
112
|
+
requiredText?: true;
|
|
113
|
+
/** v0.4 amendment (2026-07-04): where requiredText looks. Default "self". */
|
|
114
|
+
textScope?: "self" | "subtree";
|
|
115
|
+
requiredProps?: Array<{ prop: string; oneOf?: unknown[] }>;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type RuleEntry =
|
|
119
|
+
| ComponentChoiceRule
|
|
120
|
+
| RequiredCompositionRule
|
|
121
|
+
| ForbiddenCompositionRule
|
|
122
|
+
| RequiredPropsRule
|
|
123
|
+
| RuleBase;
|
|
124
|
+
|
|
125
|
+
export interface ExampleEntry {
|
|
126
|
+
id: string;
|
|
127
|
+
intent: string;
|
|
128
|
+
name?: string;
|
|
129
|
+
description?: string;
|
|
130
|
+
prompt?: string;
|
|
131
|
+
surface: Surface;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** dspack surface v0.1 (schema: dspack repo, dspack.surface.v0_1.schema.json). */
|
|
135
|
+
export interface Surface {
|
|
136
|
+
dspackSurface: string;
|
|
137
|
+
system: string;
|
|
138
|
+
intent: string;
|
|
139
|
+
root: SurfaceNode;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface SurfaceNode {
|
|
143
|
+
component: string;
|
|
144
|
+
id?: string;
|
|
145
|
+
props?: Record<string, unknown>;
|
|
146
|
+
text?: string;
|
|
147
|
+
children?: SurfaceNode[];
|
|
148
|
+
slots?: Record<string, SurfaceNode[]>;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Bare enum values from a prop descriptor (valueDescriptor objects unwrapped). */
|
|
152
|
+
export function enumValues(prop: ContractProp): unknown[] | null {
|
|
153
|
+
if (prop.type !== "enum" || !Array.isArray(prop.values)) return null;
|
|
154
|
+
return prop.values.map((v) => (v && typeof v === "object" ? (v as { value: unknown }).value : v));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** All sub-component ids of the contract, mapped to their parent component id. */
|
|
158
|
+
export function subComponentIndex(contract: Contract): Map<string, string> {
|
|
159
|
+
const index = new Map<string, string>();
|
|
160
|
+
for (const [id, component] of Object.entries(contract.components ?? {})) {
|
|
161
|
+
for (const sub of component.composition?.subComponents ?? []) index.set(sub.id, id);
|
|
162
|
+
}
|
|
163
|
+
return index;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Sub-component ids declared by more than one component, with all declaring
|
|
168
|
+
* parents. Spec v0.3 §5 makes document-wide uniqueness normative for
|
|
169
|
+
* contracts using governance blocks: S2 and rule resolution work by id alone
|
|
170
|
+
* and must never depend on object iteration order.
|
|
171
|
+
*/
|
|
172
|
+
export function duplicateSubComponentIds(contract: Contract): Map<string, string[]> {
|
|
173
|
+
const parents = new Map<string, string[]>();
|
|
174
|
+
for (const [id, component] of Object.entries(contract.components ?? {})) {
|
|
175
|
+
for (const sub of component.composition?.subComponents ?? []) {
|
|
176
|
+
parents.set(sub.id, [...(parents.get(sub.id) ?? []), id]);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return new Map([...parents].filter(([, declaredBy]) => declaredBy.length > 1));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Category memberships by component/sub-component id (v0.4). Ids without
|
|
184
|
+
* memberships are absent. Resolution is through the contract at lint time —
|
|
185
|
+
* categories never appear in surfaces (spec v0.4 §3).
|
|
186
|
+
*/
|
|
187
|
+
export function categoryIndex(contract: Contract): Map<string, string[]> {
|
|
188
|
+
const index = new Map<string, string[]>();
|
|
189
|
+
for (const [id, component] of Object.entries(contract.components ?? {})) {
|
|
190
|
+
if (component.categories?.length) index.set(id, component.categories);
|
|
191
|
+
for (const sub of component.composition?.subComponents ?? []) {
|
|
192
|
+
if (sub.categories?.length) index.set(sub.id, sub.categories);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return index;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function getIntent(contract: Contract, intentId: string): IntentEntry {
|
|
199
|
+
const intent = (contract.intents ?? []).find((i) => i.id === intentId);
|
|
200
|
+
if (!intent) {
|
|
201
|
+
const known = (contract.intents ?? []).map((i) => i.id).join(", ") || "(none)";
|
|
202
|
+
throw new Error(`intent '${intentId}' is not registered in the contract (known: ${known})`);
|
|
203
|
+
}
|
|
204
|
+
return intent;
|
|
205
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
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, type ContractComponent, enumValues } from "./contract.js";
|
|
25
|
+
|
|
26
|
+
export const DEFAULT_UNROLL_DEPTH = 6;
|
|
27
|
+
|
|
28
|
+
export interface GenerationSchemaOptions {
|
|
29
|
+
/** Levels of tree nesting the schema admits. S0-confirmed default: 6. */
|
|
30
|
+
depth?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type Json = Record<string, unknown>;
|
|
34
|
+
|
|
35
|
+
export function buildGenerationSchema(
|
|
36
|
+
contract: Contract,
|
|
37
|
+
intentId: string,
|
|
38
|
+
options: GenerationSchemaOptions = {},
|
|
39
|
+
): Json {
|
|
40
|
+
const depth = options.depth ?? DEFAULT_UNROLL_DEPTH;
|
|
41
|
+
if (!Number.isInteger(depth) || depth < 1) throw new Error(`invalid unroll depth ${depth}`);
|
|
42
|
+
|
|
43
|
+
const components = contract.components ?? {};
|
|
44
|
+
const $defs: Json = {};
|
|
45
|
+
for (let level = 0; level < depth; level++) {
|
|
46
|
+
const branches: Json[] = [];
|
|
47
|
+
for (const [id, component] of Object.entries(components)) {
|
|
48
|
+
branches.push(branch(id, componentProps(component), level, depth));
|
|
49
|
+
for (const sub of component.composition?.subComponents ?? []) {
|
|
50
|
+
branches.push(branch(sub.id, null, level, depth));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
$defs[`node_${level}`] = { anyOf: branches };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
58
|
+
type: "object",
|
|
59
|
+
additionalProperties: false,
|
|
60
|
+
required: ["dspackSurface", "system", "intent", "root"],
|
|
61
|
+
properties: {
|
|
62
|
+
dspackSurface: { const: "0.1" },
|
|
63
|
+
system: { const: contract.name },
|
|
64
|
+
intent: { const: intentId },
|
|
65
|
+
root: { $ref: "#/$defs/node_0" },
|
|
66
|
+
},
|
|
67
|
+
$defs,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function componentProps(component: ContractComponent): Json | null {
|
|
72
|
+
const entries = Object.entries(component.props ?? {});
|
|
73
|
+
if (entries.length === 0) return null;
|
|
74
|
+
const properties: Json = {};
|
|
75
|
+
for (const [name, descriptor] of entries) {
|
|
76
|
+
const values = enumValues(descriptor);
|
|
77
|
+
properties[name] = values
|
|
78
|
+
? { enum: values }
|
|
79
|
+
: descriptor.type === "boolean"
|
|
80
|
+
? { type: "boolean" }
|
|
81
|
+
: descriptor.type === "number"
|
|
82
|
+
? { type: "number" }
|
|
83
|
+
: { type: "string" };
|
|
84
|
+
}
|
|
85
|
+
return { type: "object", additionalProperties: false, properties };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function branch(id: string, props: Json | null, level: number, depth: number): Json {
|
|
89
|
+
const properties: Json = {
|
|
90
|
+
component: { const: id },
|
|
91
|
+
id: { type: "string" },
|
|
92
|
+
text: { type: "string" },
|
|
93
|
+
};
|
|
94
|
+
if (props) properties.props = props;
|
|
95
|
+
if (level < depth - 1) {
|
|
96
|
+
properties.children = { type: "array", items: { $ref: `#/$defs/node_${level + 1}` } };
|
|
97
|
+
}
|
|
98
|
+
return { type: "object", additionalProperties: false, required: ["component"], properties };
|
|
99
|
+
}
|
|
@@ -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,80 @@
|
|
|
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
|
+
|
|
11
|
+
export type FindingLevel = "error" | "warn";
|
|
12
|
+
|
|
13
|
+
export const LEVEL_OF: Record<RuleSeverity, FindingLevel> = {
|
|
14
|
+
must: "error",
|
|
15
|
+
should: "warn",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export interface FindingLocation {
|
|
19
|
+
/** Path from the surface root, e.g. `$.root.children[0]`; `$.root` for surface-wide findings. */
|
|
20
|
+
path: string;
|
|
21
|
+
/** The component id at the location, or "surface" for surface-wide findings. */
|
|
22
|
+
component: string;
|
|
23
|
+
nodeId?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface Finding {
|
|
27
|
+
ruleId: string;
|
|
28
|
+
type: string;
|
|
29
|
+
requirement: RuleSeverity;
|
|
30
|
+
level: FindingLevel;
|
|
31
|
+
message: string;
|
|
32
|
+
rationale: string;
|
|
33
|
+
location: FindingLocation;
|
|
34
|
+
exampleIds: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type GateName = "S1" | "S2" | "S3";
|
|
38
|
+
export type GateStatus = "PASS" | "FAIL" | "SKIPPED";
|
|
39
|
+
|
|
40
|
+
export interface GateReport {
|
|
41
|
+
gate: GateName;
|
|
42
|
+
name: string;
|
|
43
|
+
status: GateStatus;
|
|
44
|
+
/** S1/S2 error strings; S3 uses `findings` instead. */
|
|
45
|
+
errors?: string[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface LintReport {
|
|
49
|
+
gates: GateReport[];
|
|
50
|
+
findings: Finding[];
|
|
51
|
+
/** Errors only (warn findings never fail the lint in v0.3). */
|
|
52
|
+
errorCount: number;
|
|
53
|
+
warnCount: number;
|
|
54
|
+
pass: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function renderText(report: LintReport): string {
|
|
58
|
+
const lines: string[] = [];
|
|
59
|
+
for (const gate of report.gates) {
|
|
60
|
+
lines.push(`gate ${gate.gate} ${gate.name.padEnd(21)} ${gate.status}`);
|
|
61
|
+
for (const error of gate.errors ?? []) lines.push(` ✖ ${error}`);
|
|
62
|
+
}
|
|
63
|
+
for (const finding of report.findings) {
|
|
64
|
+
const mark = finding.level === "error" ? "✖" : "▲";
|
|
65
|
+
lines.push(`${mark} ${finding.level} [${finding.requirement}] ${finding.ruleId} [${finding.type}]`);
|
|
66
|
+
const where = finding.location.nodeId
|
|
67
|
+
? `${finding.location.path} (component: ${finding.location.component}, id: "${finding.location.nodeId}")`
|
|
68
|
+
: `${finding.location.path} (component: ${finding.location.component})`;
|
|
69
|
+
lines.push(` at ${where}`);
|
|
70
|
+
lines.push(` ${finding.message}`);
|
|
71
|
+
lines.push(` Rationale: ${finding.rationale}`);
|
|
72
|
+
if (finding.exampleIds.length) lines.push(` See example: ${finding.exampleIds.join(", ")}`);
|
|
73
|
+
}
|
|
74
|
+
lines.push(
|
|
75
|
+
report.pass
|
|
76
|
+
? `lint: PASS — 0 error(s), ${report.warnCount} warning(s)`
|
|
77
|
+
: `lint: FAIL — ${report.errorCount} error(s), ${report.warnCount} warning(s)`,
|
|
78
|
+
);
|
|
79
|
+
return lines.join("\n");
|
|
80
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
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 type { Contract, Surface } from "../contract.js";
|
|
14
|
+
import { surfaceSchemaV0_1 } from "../surface-schema.js";
|
|
15
|
+
import { type Finding, type GateReport, type LintReport } from "./findings.js";
|
|
16
|
+
import { checkVocabulary } from "./vocabulary.js";
|
|
17
|
+
import { evaluateRules } from "./rules.js";
|
|
18
|
+
|
|
19
|
+
export { renderText, LEVEL_OF } from "./findings.js";
|
|
20
|
+
export type { Finding, FindingLevel, FindingLocation, GateName, GateReport, GateStatus, LintReport } from "./findings.js";
|
|
21
|
+
export { UnknownRuleTypeError } from "./rules.js";
|
|
22
|
+
export { walkSurface } from "./walk.js";
|
|
23
|
+
|
|
24
|
+
const ajv = new Ajv2020({ strict: false, allErrors: true });
|
|
25
|
+
const validateSurfaceSchema = ajv.compile(surfaceSchemaV0_1 as unknown as Record<string, unknown>);
|
|
26
|
+
|
|
27
|
+
export function lintSurface(surface: unknown, contract: Contract): LintReport {
|
|
28
|
+
const gates: GateReport[] = [];
|
|
29
|
+
let findings: Finding[] = [];
|
|
30
|
+
|
|
31
|
+
// S1 — generic surface schema.
|
|
32
|
+
const s1Ok = validateSurfaceSchema(surface) as boolean;
|
|
33
|
+
gates.push({
|
|
34
|
+
gate: "S1",
|
|
35
|
+
name: "surface-schema",
|
|
36
|
+
status: s1Ok ? "PASS" : "FAIL",
|
|
37
|
+
errors: s1Ok
|
|
38
|
+
? undefined
|
|
39
|
+
: (validateSurfaceSchema.errors ?? []).map((e) => `${e.instancePath || "(root)"} ${e.message ?? ""}`.trim()),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (!s1Ok) {
|
|
43
|
+
gates.push({ gate: "S2", name: "contract-vocabulary", status: "SKIPPED" });
|
|
44
|
+
gates.push({ gate: "S3", name: "governance", status: "SKIPPED" });
|
|
45
|
+
return summarize(gates, findings);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const typed = surface as Surface;
|
|
49
|
+
|
|
50
|
+
// S2 — contract vocabulary (a check on the artifact, whatever produced it).
|
|
51
|
+
const s2Errors = checkVocabulary(typed, contract);
|
|
52
|
+
gates.push({
|
|
53
|
+
gate: "S2",
|
|
54
|
+
name: "contract-vocabulary",
|
|
55
|
+
status: s2Errors.length === 0 ? "PASS" : "FAIL",
|
|
56
|
+
errors: s2Errors.length ? s2Errors : undefined,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// S3 — governance. Unknown rule types throw (CLI exit 4), never skip.
|
|
60
|
+
findings = evaluateRules(typed, contract);
|
|
61
|
+
gates.push({
|
|
62
|
+
gate: "S3",
|
|
63
|
+
name: "governance",
|
|
64
|
+
status: findings.some((f) => f.level === "error") ? "FAIL" : "PASS",
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return summarize(gates, findings);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function summarize(gates: GateReport[], findings: Finding[]): LintReport {
|
|
71
|
+
const errorCount = findings.filter((f) => f.level === "error").length;
|
|
72
|
+
const warnCount = findings.filter((f) => f.level === "warn").length;
|
|
73
|
+
return {
|
|
74
|
+
gates,
|
|
75
|
+
findings,
|
|
76
|
+
errorCount,
|
|
77
|
+
warnCount,
|
|
78
|
+
pass: gates.every((g) => g.status !== "FAIL"),
|
|
79
|
+
};
|
|
80
|
+
}
|