@vectoriox/iox-ui 5.46.0 → 5.48.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.
Files changed (32) hide show
  1. package/design-core/index.d.ts +5 -3
  2. package/fesm2022/vectoriox-iox-ui-design-core.mjs +7 -4
  3. package/fesm2022/vectoriox-iox-ui-design-core.mjs.map +1 -1
  4. package/fesm2022/vectoriox-iox-ui-generation-core.mjs +351 -78
  5. package/fesm2022/vectoriox-iox-ui-generation-core.mjs.map +1 -1
  6. package/fesm2022/vectoriox-iox-ui-harness-core.mjs +4179 -0
  7. package/fesm2022/vectoriox-iox-ui-harness-core.mjs.map +1 -0
  8. package/fesm2022/vectoriox-iox-ui.mjs +29 -1
  9. package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
  10. package/generation-core/index.d.ts +252 -18
  11. package/harness-core/index.d.ts +2498 -0
  12. package/harness-core/package.json +3 -0
  13. package/index.d.ts +22 -3
  14. package/package.json +5 -1
  15. package/src/design-core/design-apply.ts +8 -4
  16. package/src/generation-core/entrances.ts +110 -0
  17. package/src/generation-core/plan-data.ts +71 -0
  18. package/src/generation-core/public-api.ts +3 -0
  19. package/src/generation-core/shape-check.ts +92 -0
  20. package/src/generation-core/site-spec.generated.ts +131 -2
  21. package/src/generation-core/validate-site-plan.ts +9 -62
  22. package/src/harness-core/business-types.ts +141 -0
  23. package/src/harness-core/harness-spec.generated.ts +3528 -0
  24. package/src/harness-core/intake.ts +85 -0
  25. package/src/harness-core/lint.ts +94 -0
  26. package/src/harness-core/look.ts +32 -0
  27. package/src/harness-core/motion.ts +67 -0
  28. package/src/harness-core/needs.ts +42 -0
  29. package/src/harness-core/prompt-context.ts +91 -0
  30. package/src/harness-core/public-api.ts +19 -0
  31. package/src/harness-core/rank.ts +226 -0
  32. package/src/harness-core/site-analysis.ts +180 -0
@@ -0,0 +1,180 @@
1
+ import { MOOD_IDS, MoodId } from '@vectoriox/iox-ui/design-core';
2
+ import {
3
+ FACT_KEYS, FactKey, isPlainObject, PathIssue, PathIssueCollector, pathAt, PlainObject, requiredShapeKeys, ShapeEntry,
4
+ shapeKeys,
5
+ } from '@vectoriox/iox-ui/generation-core';
6
+
7
+ import { BUSINESS_TYPE_IDS, NEED_IDS, USER_AVOID_IDS, UserAvoidId } from './business-types';
8
+ import { SPEC_ANALYSIS_SHAPE } from './harness-spec.generated';
9
+ import { NeedSource, SiteNeed } from './needs';
10
+
11
+ /**
12
+ * What the reading of a prompt returns (AI plan rev 3 §3d): one of the 30 kinds of business (and the two
13
+ * nearest), who it is for, what the site needs, the facts the prompt STATES (each with the words it came from)
14
+ * and those it does not, the owner's voice, moods, what they asked to avoid, a hero line, the language. A model
15
+ * writes it; `validateSiteAnalysis` is the gate — it refuses with a path and never repairs (a model gets the
16
+ * issues back and rewrites; the caller decides what to drop).
17
+ */
18
+
19
+ export enum AnalysisLocale {
20
+ He = 'he',
21
+ En = 'en',
22
+ }
23
+ export enum VoicePerson {
24
+ We = 'we',
25
+ I = 'I',
26
+ }
27
+ export enum VoiceFormality {
28
+ Casual = 'casual',
29
+ Neutral = 'neutral',
30
+ Formal = 'formal',
31
+ }
32
+
33
+ export interface FactRead {
34
+ value: string;
35
+ /** The words of the prompt it came from — checked: every quote is in the prompt. */
36
+ quote: string;
37
+ }
38
+
39
+ export interface SiteAnalysis {
40
+ businessType: string;
41
+ alternatives: [string, string];
42
+ business: { name: string; audience: string };
43
+ needs: SiteNeed[];
44
+ facts: Partial<Record<FactKey, FactRead>>;
45
+ missingFacts: FactKey[];
46
+ voice: { tone: string; person: `${VoicePerson}`; formality: `${VoiceFormality}`; avoidWords: string[] };
47
+ moods: MoodId[];
48
+ avoid: UserAvoidId[];
49
+ hero: { headline: string; lead: string };
50
+ locale: `${AnalysisLocale}`;
51
+ }
52
+
53
+ export enum AnalysisIssueCode {
54
+ Shape = 'shape',
55
+ UnknownKey = 'unknown-key',
56
+ Duplicate = 'duplicate',
57
+ UnknownBusinessType = 'unknown-business-type',
58
+ UnknownNeed = 'unknown-need',
59
+ UnknownFact = 'unknown-fact',
60
+ /** A fact's quote is not words of the prompt: it was not stated, so it may not be filled. */
61
+ QuoteNotInPrompt = 'quote-not-in-prompt',
62
+ /** A fact reported both found and missing. */
63
+ FactFoundAndMissing = 'fact-found-and-missing',
64
+ UnknownMood = 'unknown-mood',
65
+ TooManyMoods = 'too-many-moods',
66
+ UnknownAvoid = 'unknown-avoid',
67
+ InvalidValue = 'invalid-value',
68
+ }
69
+
70
+ export type AnalysisIssue = PathIssue<AnalysisIssueCode>;
71
+
72
+ export interface AnalysisValidation {
73
+ valid: boolean;
74
+ issues: AnalysisIssue[];
75
+ }
76
+
77
+ export interface AnalysisContext {
78
+ /** The owner's own words — every fact's quote must be found in them. */
79
+ prompt: string;
80
+ }
81
+
82
+ /** At most this many moods (the closed list a preset carries too). */
83
+ export const MAX_ANALYSIS_MOODS = 3;
84
+ const ALTERNATIVES = 2;
85
+
86
+ const SHAPE: Readonly<Record<string, ShapeEntry>> = SPEC_ANALYSIS_SHAPE;
87
+ const KEYS = Object.fromEntries(Object.keys(SHAPE).map(k => [k, shapeKeys(SHAPE[k])])) as Record<string, string[]>;
88
+ const REQUIRED = Object.fromEntries(Object.keys(SHAPE).map(k => [k, requiredShapeKeys(SHAPE[k])])) as Record<string, string[]>;
89
+
90
+ class Collector extends PathIssueCollector<AnalysisIssueCode> {
91
+ constructor() {
92
+ super('analysis', { shape: AnalysisIssueCode.Shape, unknownKey: AnalysisIssueCode.UnknownKey, duplicate: AnalysisIssueCode.Duplicate });
93
+ }
94
+ /** An object of `shape` at `path`: no unknown key, every required key present. */
95
+ object(value: unknown, shape: string, path: string): PlainObject | null {
96
+ if (!isPlainObject(value)) { this.add(AnalysisIssueCode.Shape, path, 'must be an object'); return null; }
97
+ this.keys(value, KEYS[shape], path);
98
+ for (const key of REQUIRED[shape]) if (value[key] === undefined) this.add(AnalysisIssueCode.Shape, pathAt(path, key), 'is required');
99
+ return value;
100
+ }
101
+ oneOf(obj: PlainObject, key: string, allowed: ReadonlyArray<string>, path: string, code = AnalysisIssueCode.InvalidValue): void {
102
+ const v = obj[key];
103
+ if (v !== undefined && (typeof v !== 'string' || !allowed.includes(v))) this.add(code, pathAt(path, key), `must be one of ${allowed.join(', ')}`);
104
+ }
105
+ strings(obj: PlainObject, key: string, path: string): string[] {
106
+ const list = this.array(obj, key, path);
107
+ list.forEach((v, i) => { if (typeof v !== 'string') this.add(AnalysisIssueCode.Shape, `${pathAt(path, key)}[${i}]`, 'must be a string'); });
108
+ return list.filter((v): v is string => typeof v === 'string');
109
+ }
110
+ }
111
+
112
+ /** Whitespace and case never decide whether words are the prompt's. */
113
+ const normalise = (s: string) => s.toLowerCase().replace(/\s+/g, ' ').trim();
114
+
115
+ export function validateSiteAnalysis(value: unknown, ctx: AnalysisContext): AnalysisValidation {
116
+ const c = new Collector();
117
+ const a = c.object(value, 'analysis', '');
118
+ if (!a) return { valid: false, issues: c.issues };
119
+
120
+ c.oneOf(a, 'businessType', BUSINESS_TYPE_IDS, '', AnalysisIssueCode.UnknownBusinessType);
121
+ const alternatives = c.strings(a, 'alternatives', '');
122
+ if (Array.isArray(a['alternatives']) && a['alternatives'].length !== ALTERNATIVES) c.add(AnalysisIssueCode.Shape, 'alternatives', `must name ${ALTERNATIVES} business types`);
123
+ alternatives.forEach((id, i) => {
124
+ if (!BUSINESS_TYPE_IDS.includes(id)) c.add(AnalysisIssueCode.UnknownBusinessType, `alternatives[${i}]`, `"${id}" is not a business type`);
125
+ else if (id === a['businessType']) c.add(AnalysisIssueCode.Duplicate, `alternatives[${i}]`, 'is the business type itself');
126
+ });
127
+
128
+ const business = c.object(a['business'], 'business', 'business');
129
+ if (business) { c.string(business, 'name', 'business'); c.string(business, 'audience', 'business'); }
130
+
131
+ const needs = c.array(a, 'needs', '');
132
+ c.unique(needs.map(n => (isPlainObject(n) && typeof n['id'] === 'string' ? n['id'] : null)), i => `needs[${i}].id`, 'need');
133
+ needs.forEach((raw, i) => {
134
+ const path = `needs[${i}]`;
135
+ const need = c.object(raw, 'need', path);
136
+ if (!need) return;
137
+ c.oneOf(need, 'id', NEED_IDS, path, AnalysisIssueCode.UnknownNeed);
138
+ c.oneOf(need, 'from', Object.values(NeedSource), path);
139
+ c.string(need, 'name', path, false);
140
+ });
141
+
142
+ const promptWords = normalise(ctx.prompt ?? '');
143
+ const found = new Set<string>();
144
+ if (!isPlainObject(a['facts'])) c.add(AnalysisIssueCode.Shape, 'facts', 'must be an object');
145
+ else for (const [key, raw] of Object.entries(a['facts'])) {
146
+ const path = `facts.${key}`;
147
+ if (!(FACT_KEYS as ReadonlyArray<string>).includes(key)) { c.add(AnalysisIssueCode.UnknownFact, path, `"${key}" is not a fact (${FACT_KEYS.join(', ')})`); continue; }
148
+ const fact = c.object(raw, 'factRead', path);
149
+ if (!fact) continue;
150
+ c.string(fact, 'value', path);
151
+ const quote = c.string(fact, 'quote', path);
152
+ if (quote && !promptWords.includes(normalise(quote))) c.add(AnalysisIssueCode.QuoteNotInPrompt, pathAt(path, 'quote'), 'is not words of the prompt — a fact the prompt does not state is missing, never filled');
153
+ found.add(key);
154
+ }
155
+ c.strings(a, 'missingFacts', '').forEach((key, i) => {
156
+ if (!(FACT_KEYS as ReadonlyArray<string>).includes(key)) c.add(AnalysisIssueCode.UnknownFact, `missingFacts[${i}]`, `"${key}" is not a fact`);
157
+ else if (found.has(key)) c.add(AnalysisIssueCode.FactFoundAndMissing, `missingFacts[${i}]`, `"${key}" is also in facts`);
158
+ });
159
+
160
+ const voice = c.object(a['voice'], 'voice', 'voice');
161
+ if (voice) {
162
+ c.string(voice, 'tone', 'voice');
163
+ c.oneOf(voice, 'person', Object.values(VoicePerson), 'voice');
164
+ c.oneOf(voice, 'formality', Object.values(VoiceFormality), 'voice');
165
+ c.strings(voice, 'avoidWords', 'voice');
166
+ }
167
+
168
+ const moods = c.strings(a, 'moods', '');
169
+ if (moods.length > MAX_ANALYSIS_MOODS) c.add(AnalysisIssueCode.TooManyMoods, 'moods', `at most ${MAX_ANALYSIS_MOODS}`);
170
+ moods.forEach((m, i) => { if (!(MOOD_IDS as ReadonlyArray<string>).includes(m)) c.add(AnalysisIssueCode.UnknownMood, `moods[${i}]`, `"${m}" is not a mood`); });
171
+ c.strings(a, 'avoid', '').forEach((id, i) => {
172
+ if (!(USER_AVOID_IDS as ReadonlyArray<string>).includes(id)) c.add(AnalysisIssueCode.UnknownAvoid, `avoid[${i}]`, `"${id}" is not something an owner may ask to avoid`);
173
+ });
174
+
175
+ const hero = c.object(a['hero'], 'hero', 'hero');
176
+ if (hero) { c.string(hero, 'headline', 'hero'); c.string(hero, 'lead', 'hero'); }
177
+ c.oneOf(a, 'locale', Object.values(AnalysisLocale), '');
178
+
179
+ return { valid: c.issues.length === 0, issues: c.issues };
180
+ }