@protoyard/blueprint 0.0.0-stage → 1.1.1
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/LICENSE +21 -0
- package/README.md +1145 -2
- package/dist/aggregate.d.ts +83 -0
- package/dist/aggregate.js +268 -0
- package/dist/app-model.d.ts +76 -0
- package/dist/app-model.js +287 -0
- package/dist/archetypes/index.d.ts +1 -0
- package/dist/archetypes/index.js +1 -0
- package/dist/archetypes/schema.d.ts +126 -0
- package/dist/archetypes/schema.js +77 -0
- package/dist/audit.d.ts +52 -0
- package/dist/audit.js +149 -0
- package/dist/authoring/deck-lint.d.ts +32 -0
- package/dist/authoring/deck-lint.js +567 -0
- package/dist/authoring/index.d.ts +1 -0
- package/dist/authoring/index.js +1 -0
- package/dist/authoring/select-deck-layouts.d.ts +50 -0
- package/dist/authoring/select-deck-layouts.js +213 -0
- package/dist/authoring-apis.d.ts +13 -0
- package/dist/authoring-apis.js +13 -0
- package/dist/authoring-validation.d.ts +65 -0
- package/dist/authoring-validation.js +267 -0
- package/dist/avatar.d.ts +25 -0
- package/dist/avatar.js +30 -0
- package/dist/brand.d.ts +70 -0
- package/dist/brand.js +294 -0
- package/dist/brief/archetypes.d.ts +28 -0
- package/dist/brief/archetypes.js +798 -0
- package/dist/brief/content-synthesize.d.ts +31 -0
- package/dist/brief/content-synthesize.js +210 -0
- package/dist/brief/deck-outlines.d.ts +96 -0
- package/dist/brief/deck-outlines.js +561 -0
- package/dist/brief/draft-bodies.d.ts +29 -0
- package/dist/brief/draft-bodies.js +224 -0
- package/dist/brief/fields.d.ts +19 -0
- package/dist/brief/fields.js +229 -0
- package/dist/brief/index.d.ts +8 -0
- package/dist/brief/index.js +8 -0
- package/dist/brief/kind-detection.d.ts +69 -0
- package/dist/brief/kind-detection.js +353 -0
- package/dist/brief/limits.d.ts +117 -0
- package/dist/brief/limits.js +217 -0
- package/dist/brief/page-doc-outlines.d.ts +80 -0
- package/dist/brief/page-doc-outlines.js +485 -0
- package/dist/brief/synthesize.d.ts +59 -0
- package/dist/brief/synthesize.js +511 -0
- package/dist/brief/text.d.ts +13 -0
- package/dist/brief/text.js +59 -0
- package/dist/canvas-screen.d.ts +31 -0
- package/dist/canvas-screen.js +33 -0
- package/dist/canvas-seed.d.ts +58 -0
- package/dist/canvas-seed.js +82 -0
- package/dist/computed/evaluate.d.ts +11 -0
- package/dist/computed/evaluate.js +101 -0
- package/dist/computed/index.d.ts +4 -0
- package/dist/computed/index.js +3 -0
- package/dist/computed/public.d.ts +14 -0
- package/dist/computed/public.js +19 -0
- package/dist/computed/schema.d.ts +10 -0
- package/dist/computed/schema.js +140 -0
- package/dist/computed/types.d.ts +70 -0
- package/dist/computed/types.js +1 -0
- package/dist/computed/validate.d.ts +6 -0
- package/dist/computed/validate.js +146 -0
- package/dist/csv-guard.d.ts +15 -0
- package/dist/csv-guard.js +29 -0
- package/dist/csv.d.ts +85 -0
- package/dist/csv.js +288 -0
- package/dist/files.d.ts +35 -0
- package/dist/files.js +246 -0
- package/dist/form-schema.d.ts +124 -0
- package/dist/form-schema.js +300 -0
- package/dist/generator-version.d.ts +11 -0
- package/dist/generator-version.js +51 -0
- package/dist/generator.d.ts +34 -0
- package/dist/generator.js +445 -0
- package/dist/identity.d.ts +34 -0
- package/dist/identity.js +91 -0
- package/dist/import/csv-shape.d.ts +40 -0
- package/dist/import/csv-shape.js +265 -0
- package/dist/import/json-shape.d.ts +55 -0
- package/dist/import/json-shape.js +491 -0
- package/dist/import/limits.d.ts +13 -0
- package/dist/import/limits.js +14 -0
- package/dist/import/sqlite-schema.d.ts +36 -0
- package/dist/import/sqlite-schema.js +729 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +58 -0
- package/dist/kinds/anchors.d.ts +18 -0
- package/dist/kinds/anchors.js +46 -0
- package/dist/kinds/bodies.d.ts +6 -0
- package/dist/kinds/bodies.js +32 -0
- package/dist/kinds/brand-runtime.d.ts +19 -0
- package/dist/kinds/brand-runtime.js +35 -0
- package/dist/kinds/deck.d.ts +8 -0
- package/dist/kinds/deck.js +34 -0
- package/dist/kinds/doc.d.ts +19 -0
- package/dist/kinds/doc.js +41 -0
- package/dist/kinds/embed/index.d.ts +39 -0
- package/dist/kinds/embed/index.js +91 -0
- package/dist/kinds/envelope.d.ts +159 -0
- package/dist/kinds/envelope.js +78 -0
- package/dist/kinds/index.d.ts +11 -0
- package/dist/kinds/index.js +11 -0
- package/dist/kinds/kind.d.ts +36 -0
- package/dist/kinds/kind.js +55 -0
- package/dist/kinds/limits.d.ts +64 -0
- package/dist/kinds/limits.js +190 -0
- package/dist/kinds/page-doc-runtime.d.ts +37 -0
- package/dist/kinds/page-doc-runtime.js +55 -0
- package/dist/kinds/page.d.ts +16 -0
- package/dist/kinds/page.js +67 -0
- package/dist/kinds/parse.d.ts +18 -0
- package/dist/kinds/parse.js +27 -0
- package/dist/kinds/runtime.d.ts +128 -0
- package/dist/kinds/runtime.js +206 -0
- package/dist/kinds/services.d.ts +49 -0
- package/dist/kinds/services.js +249 -0
- package/dist/kinds/testing.d.ts +9 -0
- package/dist/kinds/testing.js +16 -0
- package/dist/migrations.d.ts +97 -0
- package/dist/migrations.js +351 -0
- package/dist/motion.d.ts +50 -0
- package/dist/motion.js +52 -0
- package/dist/notifications.d.ts +19 -0
- package/dist/notifications.js +177 -0
- package/dist/patch.d.ts +122 -0
- package/dist/patch.js +322 -0
- package/dist/policy.d.ts +23 -0
- package/dist/policy.js +105 -0
- package/dist/runtime-definition.d.ts +39 -0
- package/dist/runtime-definition.js +74 -0
- package/dist/scenarios.d.ts +9 -0
- package/dist/scenarios.js +49 -0
- package/dist/schema/blueprint.schema.json +10191 -0
- package/dist/schema-json.d.ts +6 -0
- package/dist/schema-json.js +36 -0
- package/dist/schema.d.ts +6348 -0
- package/dist/schema.js +1269 -0
- package/dist/search.d.ts +16 -0
- package/dist/search.js +96 -0
- package/dist/services.d.ts +89 -0
- package/dist/services.js +898 -0
- package/dist/share.d.ts +78 -0
- package/dist/share.js +234 -0
- package/dist/sheet-screen.d.ts +49 -0
- package/dist/sheet-screen.js +91 -0
- package/dist/storage.d.ts +43 -0
- package/dist/storage.js +179 -0
- package/dist/stream.d.ts +26 -0
- package/dist/stream.js +173 -0
- package/dist/untrusted.d.ts +63 -0
- package/dist/untrusted.js +502 -0
- package/dist/validation-tasks.d.ts +178 -0
- package/dist/validation-tasks.js +199 -0
- package/dist/validation.d.ts +35 -0
- package/dist/validation.js +312 -0
- package/dist/variants.d.ts +112 -0
- package/dist/variants.js +283 -0
- package/dist/workflows/index.d.ts +3 -0
- package/dist/workflows/index.js +3 -0
- package/dist/workflows/schema.d.ts +50 -0
- package/dist/workflows/schema.js +34 -0
- package/dist/workflows/transition.d.ts +15 -0
- package/dist/workflows/transition.js +25 -0
- package/dist/workflows/validate.d.ts +17 -0
- package/dist/workflows/validate.js +140 -0
- package/package.json +120 -3
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type BlueprintEntity } from '../schema.ts';
|
|
2
|
+
export { CSV_SHAPE_MAX_CELL_BYTES, CSV_SHAPE_MAX_COLUMNS, CSV_SHAPE_MAX_SAMPLE_ROWS, } from '../csv-guard.ts';
|
|
3
|
+
/** Options for bounded, data-shape-only CSV inference. */
|
|
4
|
+
export interface CsvShapeOptions {
|
|
5
|
+
/** Name for the returned entity; normalized to blueprint kebab-case. */
|
|
6
|
+
entityName?: string;
|
|
7
|
+
/** Optional human label for the returned entity. */
|
|
8
|
+
entityLabel?: string;
|
|
9
|
+
/** Maximum bytes accepted by the existing CSV parser. Never exceeds 2 MiB. */
|
|
10
|
+
maxBytes?: number;
|
|
11
|
+
/** Number of non-empty data rows inspected for inference. Never exceeds 100. */
|
|
12
|
+
maxSampleRows?: number;
|
|
13
|
+
/** Maximum number of header columns. Never exceeds 100. */
|
|
14
|
+
maxColumns?: number;
|
|
15
|
+
/** Maximum UTF-8 bytes accepted for a header or sampled cell. */
|
|
16
|
+
maxCellBytes?: number;
|
|
17
|
+
}
|
|
18
|
+
export type CsvShapeErrorCode = 'too-many-columns' | 'cell-too-large' | 'invalid-options';
|
|
19
|
+
export declare class CsvShapeError extends Error {
|
|
20
|
+
readonly code: CsvShapeErrorCode;
|
|
21
|
+
constructor(code: CsvShapeErrorCode, message: string);
|
|
22
|
+
}
|
|
23
|
+
export interface CsvShapeResult {
|
|
24
|
+
/** A validated entity draft; imported cell values are deliberately absent. */
|
|
25
|
+
entity: BlueprintEntity;
|
|
26
|
+
/** Review notes for uncertain or intentionally conservative inferences. */
|
|
27
|
+
notes: string[];
|
|
28
|
+
/** Number of non-empty data rows inspected, not retained. */
|
|
29
|
+
rowsInspected: number;
|
|
30
|
+
/** Number of columns represented by the draft. */
|
|
31
|
+
columns: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Infer a validated, reviewable entity from CSV headers and a bounded sample.
|
|
35
|
+
* The parser may read the complete bounded CSV input, but the result contains
|
|
36
|
+
* only names, labels, kinds and conservative metadata — never cell values.
|
|
37
|
+
*/
|
|
38
|
+
export declare function inferCsvEntity(text: string, options?: CsvShapeOptions): CsvShapeResult;
|
|
39
|
+
/** Alias emphasizing the shape-only authoring operation. */
|
|
40
|
+
export declare const inferCsvShape: typeof inferCsvEntity;
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { parseCsv } from "../csv.js";
|
|
2
|
+
import { CSV_SHAPE_MAX_CELL_BYTES, CSV_SHAPE_MAX_COLUMNS, CSV_SHAPE_MAX_SAMPLE_ROWS, unguardCsvFormula, } from "../csv-guard.js";
|
|
3
|
+
import { parseBlueprint } from "../schema.js";
|
|
4
|
+
import { IMPORT_MAX_BYTES, IMPORT_MAX_ROWS } from "./limits.js";
|
|
5
|
+
export { CSV_SHAPE_MAX_CELL_BYTES, CSV_SHAPE_MAX_COLUMNS, CSV_SHAPE_MAX_SAMPLE_ROWS, } from "../csv-guard.js";
|
|
6
|
+
export class CsvShapeError extends Error {
|
|
7
|
+
code;
|
|
8
|
+
constructor(code, message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'CsvShapeError';
|
|
11
|
+
this.code = code;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const TITLE_KINDS = new Set(['text', 'email', 'url', 'phone', 'enum', 'integer']);
|
|
15
|
+
const SEARCHABLE_KINDS = new Set(['text', 'email', 'url', 'phone', 'enum']);
|
|
16
|
+
const RESERVED_NAMES = new Set(['id', 'createdAt', 'updatedAt', 'deletedAt', 'deletedBy']);
|
|
17
|
+
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
18
|
+
const DATETIME_PATTERN = /^\d{4}-\d{2}-\d{2}T.*(?:Z|[+-]\d{2}:?\d{2})$/;
|
|
19
|
+
const URL_PATTERN = /^https?:\/\/[^\s]+$/i;
|
|
20
|
+
const PHONE_PATTERN = /^\+?[0-9().\-\s]{7,}$/;
|
|
21
|
+
const INTEGER_PATTERN = /^[+-]?\d+$/;
|
|
22
|
+
const NUMBER_PATTERN = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
|
|
23
|
+
function bytes(text) {
|
|
24
|
+
return typeof TextEncoder === 'undefined' ? text.length : new TextEncoder().encode(text).length;
|
|
25
|
+
}
|
|
26
|
+
function bounded(value, fallback, maximum) {
|
|
27
|
+
if (value === undefined)
|
|
28
|
+
return fallback;
|
|
29
|
+
if (!Number.isFinite(value) || value < 1) {
|
|
30
|
+
throw new CsvShapeError('invalid-options', 'CSV shape limits must be positive finite numbers');
|
|
31
|
+
}
|
|
32
|
+
return Math.min(Math.floor(value), maximum);
|
|
33
|
+
}
|
|
34
|
+
function words(value) {
|
|
35
|
+
return value
|
|
36
|
+
.normalize('NFKD')
|
|
37
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
38
|
+
.replace(/[^a-zA-Z0-9]+/g, ' ')
|
|
39
|
+
.trim()
|
|
40
|
+
.split(/\s+/)
|
|
41
|
+
.filter(Boolean);
|
|
42
|
+
}
|
|
43
|
+
function fieldName(header, index, used) {
|
|
44
|
+
const parts = words(header);
|
|
45
|
+
let name = parts
|
|
46
|
+
.map((part, partIndex) => {
|
|
47
|
+
const lower = part.toLowerCase();
|
|
48
|
+
return partIndex === 0 ? lower : `${lower[0]?.toUpperCase() ?? ''}${lower.slice(1)}`;
|
|
49
|
+
})
|
|
50
|
+
.join('');
|
|
51
|
+
if (!name)
|
|
52
|
+
name = `field${index + 1}`;
|
|
53
|
+
if (/^\d/.test(name))
|
|
54
|
+
name = `field${name}`;
|
|
55
|
+
if (RESERVED_NAMES.has(name))
|
|
56
|
+
name = `field${name[0]?.toUpperCase() ?? ''}${name.slice(1)}`;
|
|
57
|
+
const base = name;
|
|
58
|
+
let suffix = 2;
|
|
59
|
+
while (used.has(name))
|
|
60
|
+
name = `${base}${suffix++}`;
|
|
61
|
+
used.add(name);
|
|
62
|
+
return name;
|
|
63
|
+
}
|
|
64
|
+
function entityName(value) {
|
|
65
|
+
const parts = words(value ?? 'Imported records');
|
|
66
|
+
const name = parts.map((part) => part.toLowerCase()).join('-');
|
|
67
|
+
return name.replace(/^-+|-+$/g, '') || 'imported-record';
|
|
68
|
+
}
|
|
69
|
+
function humanLabel(value, fallback) {
|
|
70
|
+
const label = (value ?? fallback).trim().replace(/\s+/g, ' ');
|
|
71
|
+
return label.slice(0, 60) || fallback;
|
|
72
|
+
}
|
|
73
|
+
function validDate(value) {
|
|
74
|
+
if (!DATE_PATTERN.test(value))
|
|
75
|
+
return false;
|
|
76
|
+
const parsed = new Date(`${value}T00:00:00.000Z`);
|
|
77
|
+
return !Number.isNaN(parsed.getTime()) && parsed.toISOString().startsWith(value);
|
|
78
|
+
}
|
|
79
|
+
/** Checks an email-shaped value in one pass; avoid backtracking on hostile cells. */
|
|
80
|
+
function isEmailAddress(value) {
|
|
81
|
+
if (value.length > 254)
|
|
82
|
+
return false;
|
|
83
|
+
let at = -1;
|
|
84
|
+
let domainDot = false;
|
|
85
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
86
|
+
const character = value[index];
|
|
87
|
+
if (character === undefined || /\s/u.test(character))
|
|
88
|
+
return false;
|
|
89
|
+
if (character === '@') {
|
|
90
|
+
if (at !== -1)
|
|
91
|
+
return false;
|
|
92
|
+
at = index;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (character === '.' && at !== -1) {
|
|
96
|
+
if (index === at + 1 || index === value.length - 1 || value[index - 1] === '.')
|
|
97
|
+
return false;
|
|
98
|
+
domainDot = true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return at > 0 && at < value.length - 1 && value[at - 1] !== '.' && domainDot;
|
|
102
|
+
}
|
|
103
|
+
function allMatch(values, pattern, check) {
|
|
104
|
+
return (values.length > 0 && values.every((value) => pattern.test(value) && (check?.(value) ?? true)));
|
|
105
|
+
}
|
|
106
|
+
function headerMatches(header, pattern) {
|
|
107
|
+
return pattern.test(header.toLowerCase().replace(/[^a-z0-9]+/g, ' '));
|
|
108
|
+
}
|
|
109
|
+
function inferField(header, samples, label, notes) {
|
|
110
|
+
const normalized = samples.map((sample) => unguardCsvFormula(sample).trim()).filter(Boolean);
|
|
111
|
+
if (normalized.length === 0) {
|
|
112
|
+
notes.push(`${label}: no non-empty sample values; kept as text for review.`);
|
|
113
|
+
return 'text';
|
|
114
|
+
}
|
|
115
|
+
if (headerMatches(header, /e ?mail|email address/) && normalized.every(isEmailAddress)) {
|
|
116
|
+
return 'email';
|
|
117
|
+
}
|
|
118
|
+
if (headerMatches(header, /url|uri|website|link/) && allMatch(normalized, URL_PATTERN)) {
|
|
119
|
+
return 'url';
|
|
120
|
+
}
|
|
121
|
+
if (headerMatches(header, /phone|mobile|telephone/) && allMatch(normalized, PHONE_PATTERN)) {
|
|
122
|
+
return 'phone';
|
|
123
|
+
}
|
|
124
|
+
if (headerMatches(header, /date|day|birthday|due/) &&
|
|
125
|
+
allMatch(normalized, DATE_PATTERN, validDate)) {
|
|
126
|
+
return 'date';
|
|
127
|
+
}
|
|
128
|
+
if (headerMatches(header, /time|timestamp|created at|updated at/) &&
|
|
129
|
+
allMatch(normalized, DATETIME_PATTERN, (value) => !Number.isNaN(Date.parse(value)))) {
|
|
130
|
+
return 'datetime';
|
|
131
|
+
}
|
|
132
|
+
if (headerMatches(header, /boolean|active|enabled|verified|archived|done/) &&
|
|
133
|
+
allMatch(normalized, /^(?:true|false)$/i)) {
|
|
134
|
+
return 'boolean';
|
|
135
|
+
}
|
|
136
|
+
if (headerMatches(header, /tag|label|keyword/) &&
|
|
137
|
+
normalized.some((value) => value.includes(','))) {
|
|
138
|
+
return 'tags';
|
|
139
|
+
}
|
|
140
|
+
if (allMatch(normalized, INTEGER_PATTERN)) {
|
|
141
|
+
if (headerMatches(header, /amount|price|cost|revenue|total|budget|fee|salary|money/)) {
|
|
142
|
+
return 'money';
|
|
143
|
+
}
|
|
144
|
+
return 'integer';
|
|
145
|
+
}
|
|
146
|
+
if (allMatch(normalized, NUMBER_PATTERN)) {
|
|
147
|
+
if (headerMatches(header, /amount|price|cost|revenue|total|budget|fee|salary|money/)) {
|
|
148
|
+
return 'money';
|
|
149
|
+
}
|
|
150
|
+
return 'number';
|
|
151
|
+
}
|
|
152
|
+
if (normalized.some((value) => value.length > 200) ||
|
|
153
|
+
headerMatches(header, /body|description|notes|comment|details/)) {
|
|
154
|
+
return 'longText';
|
|
155
|
+
}
|
|
156
|
+
if (headerMatches(header, /email|url|phone|date|time|number|amount|price|active|enabled|tag/) ||
|
|
157
|
+
normalized.some((value) => DATE_PATTERN.test(value) || NUMBER_PATTERN.test(value))) {
|
|
158
|
+
notes.push(`${label}: sample values were mixed or ambiguous; kept as text for review.`);
|
|
159
|
+
}
|
|
160
|
+
return 'text';
|
|
161
|
+
}
|
|
162
|
+
function fieldFor(name, label, kind) {
|
|
163
|
+
if (kind === 'money')
|
|
164
|
+
return { name, label, kind, currency: 'USD' };
|
|
165
|
+
if (kind === 'tags')
|
|
166
|
+
return { name, label, kind, maxItems: 8 };
|
|
167
|
+
if (kind === 'text')
|
|
168
|
+
return { name, label, kind, maxLength: 200 };
|
|
169
|
+
if (kind === 'longText')
|
|
170
|
+
return { name, label, kind, maxLength: 10_000 };
|
|
171
|
+
return { name, label, kind };
|
|
172
|
+
}
|
|
173
|
+
function parseLimits(options) {
|
|
174
|
+
return {
|
|
175
|
+
maxBytes: bounded(options.maxBytes, IMPORT_MAX_BYTES, IMPORT_MAX_BYTES),
|
|
176
|
+
maxSampleRows: bounded(options.maxSampleRows, CSV_SHAPE_MAX_SAMPLE_ROWS, CSV_SHAPE_MAX_SAMPLE_ROWS),
|
|
177
|
+
maxColumns: bounded(options.maxColumns, CSV_SHAPE_MAX_COLUMNS, CSV_SHAPE_MAX_COLUMNS),
|
|
178
|
+
maxCellBytes: bounded(options.maxCellBytes, CSV_SHAPE_MAX_CELL_BYTES, CSV_SHAPE_MAX_CELL_BYTES),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Infer a validated, reviewable entity from CSV headers and a bounded sample.
|
|
183
|
+
* The parser may read the complete bounded CSV input, but the result contains
|
|
184
|
+
* only names, labels, kinds and conservative metadata — never cell values.
|
|
185
|
+
*/
|
|
186
|
+
export function inferCsvEntity(text, options = {}) {
|
|
187
|
+
const limits = parseLimits(options);
|
|
188
|
+
const parseOptions = { maxBytes: limits.maxBytes, maxRows: IMPORT_MAX_ROWS };
|
|
189
|
+
const [headers = [], ...rows] = parseCsv(text, parseOptions);
|
|
190
|
+
if (headers.length === 0 || headers.every((header) => header.trim() === '')) {
|
|
191
|
+
throw new CsvShapeError('invalid-options', 'CSV shape inference needs a non-empty header row');
|
|
192
|
+
}
|
|
193
|
+
if (headers.length > limits.maxColumns) {
|
|
194
|
+
throw new CsvShapeError('too-many-columns', `CSV has ${headers.length.toLocaleString()} columns; the limit is ${limits.maxColumns.toLocaleString()}`);
|
|
195
|
+
}
|
|
196
|
+
if (headers.some((header) => bytes(header) > limits.maxCellBytes)) {
|
|
197
|
+
throw new CsvShapeError('cell-too-large', `A CSV header exceeds the ${limits.maxCellBytes.toLocaleString()}-byte limit`);
|
|
198
|
+
}
|
|
199
|
+
const rowsWithData = rows.filter((row) => row.some((cell) => cell.trim() !== ''));
|
|
200
|
+
for (const row of rowsWithData.slice(0, limits.maxSampleRows)) {
|
|
201
|
+
for (const cell of row.slice(0, headers.length)) {
|
|
202
|
+
if (bytes(cell) > limits.maxCellBytes) {
|
|
203
|
+
throw new CsvShapeError('cell-too-large', `A CSV sample cell exceeds the ${limits.maxCellBytes.toLocaleString()}-byte limit`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const notes = [];
|
|
208
|
+
if (rowsWithData.length > limits.maxSampleRows) {
|
|
209
|
+
notes.push(`Only the first ${limits.maxSampleRows.toLocaleString()} non-empty data rows were inspected.`);
|
|
210
|
+
}
|
|
211
|
+
const sampledRows = rowsWithData.slice(0, limits.maxSampleRows);
|
|
212
|
+
const usedNames = new Set();
|
|
213
|
+
const fields = [];
|
|
214
|
+
const fieldKinds = [];
|
|
215
|
+
headers.forEach((rawHeader, index) => {
|
|
216
|
+
const header = rawHeader.trim();
|
|
217
|
+
const label = humanLabel(header, `Field ${index + 1}`);
|
|
218
|
+
const name = fieldName(header, index, usedNames);
|
|
219
|
+
if (name !== (words(header).join('') || `field${index + 1}`)) {
|
|
220
|
+
notes.push(`${label}: normalized to the field name "${name}".`);
|
|
221
|
+
}
|
|
222
|
+
const samples = sampledRows.map((row) => row[index] ?? '');
|
|
223
|
+
const kind = inferField(header, samples, label, notes);
|
|
224
|
+
fieldKinds.push(kind);
|
|
225
|
+
fields.push(fieldFor(name, label, kind));
|
|
226
|
+
});
|
|
227
|
+
let titleIndex = fieldKinds.findIndex((kind) => TITLE_KINDS.has(kind));
|
|
228
|
+
if (titleIndex < 0) {
|
|
229
|
+
const name = fieldName('Name', fields.length, usedNames);
|
|
230
|
+
fields.unshift({ name, label: 'Name', kind: 'text', maxLength: 200 });
|
|
231
|
+
fieldKinds.unshift('text');
|
|
232
|
+
titleIndex = 0;
|
|
233
|
+
notes.push('Added a blank Name field because no imported column was title-compatible.');
|
|
234
|
+
}
|
|
235
|
+
const name = entityName(options.entityName);
|
|
236
|
+
const entity = parseBlueprint({
|
|
237
|
+
formatVersion: 1,
|
|
238
|
+
name: humanLabel(options.entityLabel, name.replace(/-/g, ' ')),
|
|
239
|
+
entities: [
|
|
240
|
+
{
|
|
241
|
+
name,
|
|
242
|
+
plural: `${name}s`,
|
|
243
|
+
label: humanLabel(options.entityLabel, name.replace(/-/g, ' ')),
|
|
244
|
+
fields,
|
|
245
|
+
titleField: fields[titleIndex].name,
|
|
246
|
+
searchable: fields
|
|
247
|
+
.filter((_field, index) => index !== titleIndex && SEARCHABLE_KINDS.has(fieldKinds[index]))
|
|
248
|
+
.slice(0, 3)
|
|
249
|
+
.map((field) => field.name),
|
|
250
|
+
features: { csv: true },
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
roles: [{ name: 'owner', permissions: { '*': ['*'] } }],
|
|
254
|
+
}).entities[0];
|
|
255
|
+
if (!entity)
|
|
256
|
+
throw new CsvShapeError('invalid-options', 'CSV shape inference produced no entity');
|
|
257
|
+
return {
|
|
258
|
+
entity,
|
|
259
|
+
notes,
|
|
260
|
+
rowsInspected: sampledRows.length,
|
|
261
|
+
columns: headers.length,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
/** Alias emphasizing the shape-only authoring operation. */
|
|
265
|
+
export const inferCsvShape = inferCsvEntity;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type BlueprintEntity } from '../schema.ts';
|
|
2
|
+
export declare const JSON_SHAPE_MAX_BYTES: number;
|
|
3
|
+
export declare const JSON_SHAPE_MAX_SAMPLE_OBJECTS = 100;
|
|
4
|
+
export declare const JSON_SHAPE_MAX_DEPTH = 8;
|
|
5
|
+
export declare const JSON_SHAPE_MAX_OBJECTS = 2000;
|
|
6
|
+
export declare const JSON_SHAPE_MAX_FIELDS = 100;
|
|
7
|
+
export declare const JSON_SHAPE_MAX_STRING_BYTES: number;
|
|
8
|
+
export interface JsonShapeOptions {
|
|
9
|
+
/** Name for the root entity; normalized to blueprint kebab-case. */
|
|
10
|
+
entityName?: string;
|
|
11
|
+
/** Optional human label for the root entity. */
|
|
12
|
+
entityLabel?: string;
|
|
13
|
+
/** Maximum UTF-8 input bytes. Overrides are clamped to the hard maximum. */
|
|
14
|
+
maxBytes?: number;
|
|
15
|
+
/** Maximum representative objects sampled per entity. */
|
|
16
|
+
maxSampleObjects?: number;
|
|
17
|
+
/** Maximum accepted nested JSON depth. */
|
|
18
|
+
maxDepth?: number;
|
|
19
|
+
/** Maximum object and array nodes traversed. */
|
|
20
|
+
maxObjects?: number;
|
|
21
|
+
/** Maximum field keys inferred per entity. */
|
|
22
|
+
maxFields?: number;
|
|
23
|
+
/** Maximum bytes in any one JSON string value. */
|
|
24
|
+
maxStringBytes?: number;
|
|
25
|
+
}
|
|
26
|
+
export type JsonShapeErrorCode = 'invalid-json' | 'invalid-shape' | 'input-too-large' | 'too-deep' | 'too-many-objects' | 'too-many-fields' | 'string-too-large' | 'unsafe-key' | 'invalid-options';
|
|
27
|
+
export declare class JsonShapeError extends Error {
|
|
28
|
+
readonly code: JsonShapeErrorCode;
|
|
29
|
+
constructor(code: JsonShapeErrorCode, message: string);
|
|
30
|
+
}
|
|
31
|
+
export interface JsonShapeRelationCandidate {
|
|
32
|
+
sourceEntity: string;
|
|
33
|
+
field: string;
|
|
34
|
+
targetEntity: string;
|
|
35
|
+
cardinality: 'many';
|
|
36
|
+
reason: string;
|
|
37
|
+
}
|
|
38
|
+
export interface JsonShapeResult {
|
|
39
|
+
/** Validated entity drafts; the first is the supplied object or array shape. */
|
|
40
|
+
entities: BlueprintEntity[];
|
|
41
|
+
/** Normalized review notes that never include sampled values. */
|
|
42
|
+
notes: string[];
|
|
43
|
+
/** Candidate one-to-many links inferred from arrays of objects. */
|
|
44
|
+
relationCandidates: JsonShapeRelationCandidate[];
|
|
45
|
+
/** Number of representative record objects used across the inferred entities. */
|
|
46
|
+
objectsInspected: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Infer a validated, reviewable draft from one JSON record or an array of
|
|
50
|
+
* records. Only names and shape metadata are returned; source values never
|
|
51
|
+
* enter entities, relation candidates, notes, or errors.
|
|
52
|
+
*/
|
|
53
|
+
export declare function inferJsonShape(text: string, options?: JsonShapeOptions): JsonShapeResult;
|
|
54
|
+
/** Alias emphasizing this is authoring-only shape inference. */
|
|
55
|
+
export declare const inferJsonEntity: typeof inferJsonShape;
|