@port60/template-kit 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -1
- package/package.json +1 -1
- package/src/commands/create.mjs +11 -0
- package/src/lib/agentsMd.mjs +176 -0
- package/src/vendor/contract/v2/dialect.json +2 -1
- package/src/vendor/contract/v2/manifest.schema.json +73 -2
- package/src/vendor/contract/v2/presentation.json +107 -0
- package/src/vendor/contract/v2/sections.json +120 -4
- package/src/vendor/contract/v2.lock.json +559 -7
- package/src/vendor/engine/budgets.mjs +0 -6
- package/src/vendor/engine/colour-roles.mjs +68 -0
- package/src/vendor/engine/content-footprint.mjs +0 -11
- package/src/vendor/engine/design-settings.mjs +34 -0
- package/src/vendor/engine/dialect.mjs +3 -10
- package/src/vendor/engine/locale.mjs +0 -5
- package/src/vendor/engine/majors.mjs +0 -5
- package/src/vendor/engine/presentation-capabilities.mjs +0 -2
- package/src/vendor/engine/section-fields.mjs +64 -0
- package/src/vendor/engine/section-heading-alignment.mjs +0 -2
- package/src/vendor/engine/section-presentation.mjs +127 -0
- package/src/vendor/validator/behaviors-runtime.js +1 -1
- package/src/vendor/validator/collection-link-visibility.mjs +0 -1
- package/src/vendor/validator/colour-treatment.mjs +154 -0
- package/src/vendor/validator/fixture-art.mjs +0 -15
- package/src/vendor/validator/focus.mjs +0 -5
- package/src/vendor/validator/fonts.mjs +0 -6
- package/src/vendor/validator/heading-alignment.mjs +0 -3
- package/src/vendor/validator/icons.mjs +0 -4
- package/src/vendor/validator/image-overlay.mjs +141 -0
- package/src/vendor/validator/intro-photo-framing.mjs +101 -0
- package/src/vendor/validator/model-reference-v2.mjs +0 -4
- package/src/vendor/validator/model-reference.mjs +0 -4
- package/src/vendor/validator/navigation-highlights.mjs +0 -3
- package/src/vendor/validator/presentation-proof.mjs +0 -2
- package/src/vendor/validator/preview-v1.mjs +3 -103
- package/src/vendor/validator/preview-v2.mjs +11 -105
- package/src/vendor/validator/section-fields.mjs +41 -0
- package/src/vendor/validator/section-layout.mjs +156 -0
- package/src/vendor/validator/section-presentation.mjs +225 -0
- package/src/vendor/validator/site-context-v2.mjs +17 -2
- package/src/vendor/validator/site-context.mjs +0 -14
- package/src/vendor/validator/validate-v1.mjs +4 -98
- package/src/vendor/validator/validate-v2.mjs +44 -103
- package/src/vendor/validator/validate.mjs +0 -2
- package/starter/assets/theme.css +55 -0
- package/starter/manifest.json +20 -0
- package/starter/sections/cta.liquid +2 -2
- package/starter/sections/hero.liquid +5 -3
- package/starter/sections/homeHero.liquid +6 -6
- package/starter/sections/values.liquid +2 -2
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
// The Port60 template CONFORMANCE VALIDATOR as a PURE MODULE (developer program T1.3): the same
|
|
2
|
-
// checks the publish-time CLI has always run, callable with an in-memory file map, no fs, no
|
|
3
|
-
// argv, no process.exit, so the studio upload lane (an HTTP endpoint) and the CLI share ONE
|
|
4
|
-
// implementation, and "validated ⇒ renders in production" keeps holding: the Liquid instance is
|
|
5
|
-
// configured identically to the engine's, dialect enforcement is the same shared module, and the
|
|
6
|
-
// render budgets here match production's.
|
|
7
|
-
//
|
|
8
|
-
// validateArtifact(files) → { errors: string[], warnings: string[], manifest: object|null }
|
|
9
|
-
//
|
|
10
|
-
// `files` is a plain object of artifact-relative path → string content (manifest.json,
|
|
11
|
-
// layout.liquid, sections/*.liquid, pages/*.liquid, assets/theme.css).
|
|
12
1
|
import { Liquid } from 'liquidjs';
|
|
13
2
|
import Ajv2020 from 'ajv/dist/2020.js';
|
|
14
3
|
import { CONTENT_SLOT, configureDialect, splitIslandParts } from '../engine/dialect.mjs';
|
|
@@ -29,11 +18,6 @@ const Ajv = Ajv2020.default ?? Ajv2020;
|
|
|
29
18
|
|
|
30
19
|
export { dialect as contractDialect, sectionCatalogue, islandRegistry, contextContract, behaviourCatalogue };
|
|
31
20
|
|
|
32
|
-
// The behaviour to opt-in-attribute map is DERIVED from the catalogue (FR-5): `primaryAttribute` on
|
|
33
|
-
// each entry feeds the checks below, the generated reference and the kit, so adding a behaviour is
|
|
34
|
-
// one catalogue entry plus its runtime initialiser. The trailing word boundary keeps the original
|
|
35
|
-
// semantics: a companion attribute that starts with the primary one (data-p60-reveal-group,
|
|
36
|
-
// data-p60-nav-item) counts as the behaviour in use.
|
|
37
21
|
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
38
22
|
export const BEHAVIOUR_PRIMARY_ATTR = Object.fromEntries(
|
|
39
23
|
behaviourCatalogue.behaviours.map((b) => [b.name, b.primaryAttribute])
|
|
@@ -42,10 +26,6 @@ const PRIMARY_ATTR = Object.fromEntries(
|
|
|
42
26
|
behaviourCatalogue.behaviours.map((b) => [b.name, [new RegExp(`${escapeRegExp(b.primaryAttribute)}\\b`), b.primaryAttribute]])
|
|
43
27
|
);
|
|
44
28
|
|
|
45
|
-
// Templates are markup and attributes, NEVER code (docs/template-behaviours.md). These are hard
|
|
46
|
-
// errors over the RAW liquid source, even inside comments, because there is no legitimate reason
|
|
47
|
-
// for the tokens to appear at all. The handler pattern names real DOM event families rather than
|
|
48
|
-
// matching any on* word, so attributes like `once` or `online` never false-positive.
|
|
49
29
|
const FORBIDDEN_MARKUP = [
|
|
50
30
|
[/<script\b/i, 'a <script> tag'],
|
|
51
31
|
[/<(iframe|object|embed)\b/i, 'an embedded frame or plugin element'],
|
|
@@ -60,7 +40,6 @@ export async function validateArtifact(files) {
|
|
|
60
40
|
const has = (path) => Object.hasOwn(files, path);
|
|
61
41
|
const read = (path) => files[path];
|
|
62
42
|
|
|
63
|
-
// 1. Manifest against the schema.
|
|
64
43
|
let manifest = null;
|
|
65
44
|
if (!has('manifest.json')) {
|
|
66
45
|
return { errors: ['manifest.json is missing, every artifact starts with its manifest'], warnings, manifest };
|
|
@@ -70,14 +49,10 @@ export async function validateArtifact(files) {
|
|
|
70
49
|
} catch (e) {
|
|
71
50
|
return { errors: [`manifest.json unreadable: ${e.message}`], warnings, manifest: null };
|
|
72
51
|
}
|
|
73
|
-
// preview-content.json is a DEV-ONLY data override: package excludes it and the intake
|
|
74
|
-
// refuses it, an artifact must never carry data, only shape.
|
|
75
52
|
if (has('preview-content.json')) {
|
|
76
53
|
errors.push('preview-content.json: development preview data never ships in an artifact, remove it (package excludes it automatically)');
|
|
77
54
|
}
|
|
78
55
|
|
|
79
|
-
// Content model v1: the footprint is decidable from the sources (closed dialect); unknown
|
|
80
|
-
// paths, dynamic indexing and tree aliasing are errors from site-context.mjs.
|
|
81
56
|
const contentAnalysis = extractContentFootprint(files);
|
|
82
57
|
errors.push(...contentAnalysis.errors);
|
|
83
58
|
const siteFx = buildSiteFixture(manifest);
|
|
@@ -90,10 +65,6 @@ export async function validateArtifact(files) {
|
|
|
90
65
|
}
|
|
91
66
|
}
|
|
92
67
|
|
|
93
|
-
// Capability declarations are catalogue MATCHING metadata, not an entitlement shortcut. Keep
|
|
94
|
-
// them honest by requiring one corresponding template-facing surface. The mapping is deliberately
|
|
95
|
-
// structural: it proves the design can present a capability without inspecting tenant data or
|
|
96
|
-
// crossing the platform-owned transaction, identity and consent boundaries.
|
|
97
68
|
{
|
|
98
69
|
const sections = new Set(manifest?.supports?.sections ?? []);
|
|
99
70
|
const islands = new Set(manifest?.supports?.islands ?? []);
|
|
@@ -123,10 +94,6 @@ export async function validateArtifact(files) {
|
|
|
123
94
|
}
|
|
124
95
|
}
|
|
125
96
|
|
|
126
|
-
// Site focus honesty (docs/volunteering.md): a template that claims it can lead with
|
|
127
|
-
// volunteering must give the volunteer sign-up a way into the hero, either the
|
|
128
|
-
// primary_action_widget island (which becomes the sign-up when volunteering leads) or the
|
|
129
|
-
// volunteer_signup island placed directly.
|
|
130
97
|
{
|
|
131
98
|
const focusKinds = new Set(manifest?.supports?.focus ?? []);
|
|
132
99
|
if (focusKinds.has('volunteer')) {
|
|
@@ -137,9 +104,6 @@ export async function validateArtifact(files) {
|
|
|
137
104
|
}
|
|
138
105
|
}
|
|
139
106
|
|
|
140
|
-
// Markup and attributes, NEVER code, the machine-enforced JavaScript ban over every liquid
|
|
141
|
-
// source (docs/template-behaviours.md). Behaviour is engine-owned; a template wanting motion
|
|
142
|
-
// declares supports.behaviors and uses the data-p60-* grammar.
|
|
143
107
|
for (const [path, source] of Object.entries(files)) {
|
|
144
108
|
if (!path.endsWith('.liquid')) continue;
|
|
145
109
|
for (const [pattern, what] of FORBIDDEN_MARKUP) {
|
|
@@ -149,9 +113,6 @@ export async function validateArtifact(files) {
|
|
|
149
113
|
}
|
|
150
114
|
}
|
|
151
115
|
|
|
152
|
-
// Behaviour declaration and usage must agree in BOTH directions. Source-level, deliberately:
|
|
153
|
-
// usage often sits inside content-dependent branches the fixtures never take, so the grammar's
|
|
154
|
-
// presence in the source is the honest minimal proof.
|
|
155
116
|
{
|
|
156
117
|
const declaredBehaviours = new Set(manifest?.supports?.behaviors ?? []);
|
|
157
118
|
const liquidSource = Object.entries(files)
|
|
@@ -198,8 +159,6 @@ export async function validateArtifact(files) {
|
|
|
198
159
|
}
|
|
199
160
|
}
|
|
200
161
|
|
|
201
|
-
// Font knobs: the default family must be a real catalogue entry (the tenant-unset render uses
|
|
202
|
-
// it), and the slot must declare the weights the template's typographic system needs.
|
|
203
162
|
{
|
|
204
163
|
const familyNames = new Set(fontCatalogue.families.map((f) => f.name));
|
|
205
164
|
for (const knob of manifest?.settings?.schema ?? []) {
|
|
@@ -212,8 +171,6 @@ export async function validateArtifact(files) {
|
|
|
212
171
|
}
|
|
213
172
|
}
|
|
214
173
|
|
|
215
|
-
// Looks: every value must target a declared knob and be valid for it, a look that half-applies
|
|
216
|
-
// would leave the tenant in a state no author designed.
|
|
217
174
|
{
|
|
218
175
|
const knobByKey = new Map((manifest?.settings?.schema ?? []).map((k) => [k.key, k]));
|
|
219
176
|
const familyNames = new Set(fontCatalogue.families.map((f) => f.name));
|
|
@@ -236,8 +193,6 @@ export async function validateArtifact(files) {
|
|
|
236
193
|
}
|
|
237
194
|
}
|
|
238
195
|
|
|
239
|
-
// Engine identical to production: escape-by-default, strict filters, restricted dialect,
|
|
240
|
-
// and the SAME DoS budgets the live renderer runs with.
|
|
241
196
|
const availableIslands = new Set(
|
|
242
197
|
islandRegistry.islands.filter((i) => i.status === 'available').map((i) => i.name)
|
|
243
198
|
);
|
|
@@ -248,18 +203,9 @@ export async function validateArtifact(files) {
|
|
|
248
203
|
const catalogueByType = new Map(sectionCatalogue.sections.map((s) => [s.type, s]));
|
|
249
204
|
const declaredIslands = new Set(manifest?.supports?.islands ?? []);
|
|
250
205
|
const placedIslands = new Set();
|
|
251
|
-
|
|
252
|
-
let
|
|
253
|
-
let homeHeroSingleShows = null; // single-photo variant: probe rendered directly
|
|
206
|
+
let homeHeroMultiShows = null;
|
|
207
|
+
let homeHeroSingleShows = null;
|
|
254
208
|
|
|
255
|
-
// Author-renderable widget sections (the flip): each carries a curated data context; a template
|
|
256
|
-
// either places the section's DEFAULT ISLAND (which owns rendering + empty states) or renders
|
|
257
|
-
// the data itself, in which case both directions are proven behaviourally (the worship
|
|
258
|
-
// pattern): the populated fixture's sentinel must appear, and the EMPTY context must render it
|
|
259
|
-
// away (derive or omit, nothing invented, nothing dangling).
|
|
260
|
-
// Sentinels are DERIVED from the canonical fixtures (the first item's display field), never
|
|
261
|
-
// hardcoded, the fixture data is free to become richer without touching a proof, and a proof
|
|
262
|
-
// can never drift from the data it renders.
|
|
263
209
|
const sentinelOf = (type, dataKey, field) =>
|
|
264
210
|
(contextContract.fixtures.sections?.[type]?.[dataKey] ?? [])[0]?.[field] ?? null;
|
|
265
211
|
const WIDGET_SECTIONS = {
|
|
@@ -271,8 +217,6 @@ export async function validateArtifact(files) {
|
|
|
271
217
|
locations: { island: null, dataKey: 'locations', sentinel: sentinelOf('locations', 'locations', 'name') }
|
|
272
218
|
};
|
|
273
219
|
|
|
274
|
-
// 2b. Compositions (site editor stage 4): each page must be a supported page, each type a
|
|
275
|
-
// supported section the catalogue assigns to that page, listed once, with a role.
|
|
276
220
|
const compositions = manifest?.compositions ?? {};
|
|
277
221
|
for (const [page, entries] of Object.entries(compositions)) {
|
|
278
222
|
if (!(manifest?.supports?.pages ?? []).includes(page)) {
|
|
@@ -295,7 +239,6 @@ export async function validateArtifact(files) {
|
|
|
295
239
|
}
|
|
296
240
|
}
|
|
297
241
|
|
|
298
|
-
// 2-4. Sections: catalogue membership, parse, fixture renders.
|
|
299
242
|
for (const type of manifest?.supports?.sections ?? []) {
|
|
300
243
|
const entry = catalogueByType.get(type);
|
|
301
244
|
if (!entry) {
|
|
@@ -314,8 +257,6 @@ export async function validateArtifact(files) {
|
|
|
314
257
|
errors.push(`section '${type}': does not parse under the dialect, ${e.message}`);
|
|
315
258
|
continue;
|
|
316
259
|
}
|
|
317
|
-
// Widget-section proof (independent of the minimal/sample loop): island placed → the island
|
|
318
|
-
// owns everything; hand-rendered → sentinel appears with data, vanishes without.
|
|
319
260
|
const widget = WIDGET_SECTIONS[type];
|
|
320
261
|
if (widget) {
|
|
321
262
|
const dataFixture = contextContract.fixtures.sections?.[type] ?? {};
|
|
@@ -360,17 +301,12 @@ export async function validateArtifact(files) {
|
|
|
360
301
|
section: fixture,
|
|
361
302
|
brand: contextContract.fixtures.brand,
|
|
362
303
|
site: siteFx,
|
|
363
|
-
// Widget data rides the ordinary fixture renders too, so a hand-rendering section
|
|
364
|
-
// doesn't fail the generic pass for want of its context.
|
|
365
304
|
...(widget ? (contextContract.fixtures.sections?.[type] ?? {}) : {})
|
|
366
305
|
});
|
|
367
306
|
if (type === 'homeHero' && fixtureName === 'sample') {
|
|
368
|
-
// The sample's own first image reference is the probe: `p60fixture:` refs are quote-free
|
|
369
|
-
// and survive HTML escaping, so "does the hero display the photos?" stays a substring check.
|
|
370
307
|
const probe = (fixture.images ?? [])[0]?.imageUrl ?? 'p60fixture:';
|
|
371
308
|
homeHeroMultiShows = html.includes(probe)
|
|
372
309
|
|| splitIslandParts(html).some((p) => p.island === 'hero_carousel');
|
|
373
|
-
// The single-photo path proven separately: same fixture, first photo only.
|
|
374
310
|
try {
|
|
375
311
|
const single = await liquid.render(parsed, {
|
|
376
312
|
section: { ...fixture, images: (fixture.images ?? []).slice(0, 1) },
|
|
@@ -395,11 +331,6 @@ export async function validateArtifact(files) {
|
|
|
395
331
|
}
|
|
396
332
|
}
|
|
397
333
|
|
|
398
|
-
// 5. FIELD MARKERS: which node shows which field, so the editor can put the caret on the page
|
|
399
|
-
// instead of in a side panel. Source-level, like behaviours: a marker often sits inside a
|
|
400
|
-
// content-dependent branch the fixtures never take, and its presence in the source is the honest
|
|
401
|
-
// proof. A marker is an address inside that section's own content: `title`, or `items.0.label`
|
|
402
|
-
// for a list, where the index is usually a Liquid expression and stands for any position.
|
|
403
334
|
{
|
|
404
335
|
const MARKER = /data-p60-field="([^"]*)"/g;
|
|
405
336
|
const fieldsOf = (type) => new Map((catalogueByType.get(type)?.fields ?? []).map((f) => [f.name, f]));
|
|
@@ -409,7 +340,6 @@ export async function validateArtifact(files) {
|
|
|
409
340
|
const fields = fieldsOf(type);
|
|
410
341
|
for (const [, raw] of source.matchAll(MARKER)) {
|
|
411
342
|
marked.push(type);
|
|
412
|
-
// An index written as Liquid stands for whichever item this is.
|
|
413
343
|
const steps = raw.replace(/\{\{[^}]*\}\}/g, '#').split('.');
|
|
414
344
|
const field = fields.get(steps[0]);
|
|
415
345
|
const named = steps.length === 1
|
|
@@ -437,14 +367,6 @@ export async function validateArtifact(files) {
|
|
|
437
367
|
}
|
|
438
368
|
}
|
|
439
369
|
|
|
440
|
-
// Hero-imagery honesty, checked BEHAVIOURALLY (the worship pattern). The homeHero sample
|
|
441
|
-
// fixture carries photographs whose data URIs embed a quote-free marker that survives HTML
|
|
442
|
-
// escaping, so "does the rendered hero display the tenant's photos?" is a substring check,
|
|
443
|
-
// and with several photos, placing the hero_carousel island IS displaying them (the island
|
|
444
|
-
// renders the slides at runtime). The single-photo path is proven separately: images[0] must
|
|
445
|
-
// appear directly. Declaration and behaviour must agree; the choosers badge photo-led tenants
|
|
446
|
-
// by supports.heroImagery. Legacy imageUrl-only renderers never match (the fixture's photos
|
|
447
|
-
// ride `images`), so they pass undeclared, they just don't earn the badge.
|
|
448
370
|
{
|
|
449
371
|
const declaresHero = manifest?.supports?.heroImagery === true;
|
|
450
372
|
if (declaresHero && !(manifest?.supports?.sections ?? []).includes('homeHero')) {
|
|
@@ -462,7 +384,6 @@ export async function validateArtifact(files) {
|
|
|
462
384
|
}
|
|
463
385
|
}
|
|
464
386
|
|
|
465
|
-
// 7. Layout (when declared): parse + render the layout fixture + exactly one content slot.
|
|
466
387
|
if (manifest?.supports?.worship && !manifest?.supports?.layout) {
|
|
467
388
|
errors.push('manifest: supports.worship requires supports.layout, the worship rail is layout chrome');
|
|
468
389
|
}
|
|
@@ -513,11 +434,6 @@ export async function validateArtifact(files) {
|
|
|
513
434
|
errors.push(...highlights.errors);
|
|
514
435
|
warnings.push(...highlights.warnings);
|
|
515
436
|
|
|
516
|
-
// Two-sided worship honesty, checked BEHAVIOURALLY: does the rendered layout actually
|
|
517
|
-
// display the worship fixture's times? Declaration and behaviour must agree, the
|
|
518
|
-
// choosers steer worship-enabled tenants by supports.worship, so a false declaration
|
|
519
|
-
// either hides their times (undeclared but rendered is fine to fix by declaring) or
|
|
520
|
-
// promises a rail that never appears.
|
|
521
437
|
const worshipProbe = contextContract.fixtures.layout.worship?.times?.[0]?.name;
|
|
522
438
|
if (worshipProbe) {
|
|
523
439
|
const rendersWorship = html.includes(worshipProbe);
|
|
@@ -551,7 +467,6 @@ export async function validateArtifact(files) {
|
|
|
551
467
|
warnings.push('layout.liquid present but manifest.supports.layout is not true, it will be ignored');
|
|
552
468
|
}
|
|
553
469
|
|
|
554
|
-
// 8. Page templates (when declared): file exists, parses, renders the page's data fixture.
|
|
555
470
|
for (const pageName of manifest?.supports?.pageTemplates ?? []) {
|
|
556
471
|
const fixture = contextContract.fixtures.pages?.[pageName];
|
|
557
472
|
if (!fixture) {
|
|
@@ -584,7 +499,6 @@ export async function validateArtifact(files) {
|
|
|
584
499
|
}
|
|
585
500
|
}
|
|
586
501
|
|
|
587
|
-
// 5. Island discipline: placed ⊆ declared ⊆ registry (and available).
|
|
588
502
|
for (const name of placedIslands) {
|
|
589
503
|
if (!declaredIslands.has(name)) {
|
|
590
504
|
errors.push(`island '${name}': placed in a section but not declared in manifest.supports.islands`);
|
|
@@ -598,18 +512,13 @@ export async function validateArtifact(files) {
|
|
|
598
512
|
}
|
|
599
513
|
}
|
|
600
514
|
|
|
601
|
-
// 6. Theme.
|
|
602
515
|
if (!has('assets/theme.css') || read('assets/theme.css').trim() === '') {
|
|
603
516
|
errors.push('assets/theme.css missing or empty, a template must ship its look');
|
|
604
517
|
}
|
|
605
518
|
|
|
606
|
-
// 6b. Layout contract (contract/v1/layout.json). Platform pages render through the content SEAM
|
|
607
|
-
// (.container / .full); a template STYLES those to place content, never a parallel content container.
|
|
608
|
-
// The rule is DATA, the seam token, the allowed selectors and the message all come from the contract
|
|
609
|
-
// file; this only implements the check KIND (a non-seam selector sizing its width off the token).
|
|
610
519
|
{
|
|
611
520
|
const css = has('assets/theme.css') ? read('assets/theme.css') : '';
|
|
612
|
-
const stripped = css.replace(/\/\*[\s\S]*?\*\//g, ' ');
|
|
521
|
+
const stripped = css.replace(/\/\*[\s\S]*?\*\//g, ' ');
|
|
613
522
|
const RULE = /([^{}]+)\{([^{}]*)\}/g;
|
|
614
523
|
for (const rule of layoutContract.rules ?? []) {
|
|
615
524
|
if (rule.kind !== 'css-width-off-token') continue;
|
|
@@ -620,7 +529,7 @@ export async function validateArtifact(files) {
|
|
|
620
529
|
let m;
|
|
621
530
|
RULE.lastIndex = 0;
|
|
622
531
|
while ((m = RULE.exec(stripped)) !== null) {
|
|
623
|
-
if (!sizesOffToken.test(m[2])) continue;
|
|
532
|
+
if (!sizesOffToken.test(m[2])) continue;
|
|
624
533
|
for (const sel of m[1].split(',')) {
|
|
625
534
|
const s = sel.trim();
|
|
626
535
|
if (s && !isSeam.test(s)) offenders.add(s);
|
|
@@ -632,9 +541,6 @@ export async function validateArtifact(files) {
|
|
|
632
541
|
}
|
|
633
542
|
}
|
|
634
543
|
|
|
635
|
-
// Open enums, proven survivable (content model v1 discipline 2): a template whose footprint
|
|
636
|
-
// reads site.content.events must survive a registration mode it has never heard of, new modes
|
|
637
|
-
// WILL arrive within the major. No throw, and no undefined/null literal leaking into markup.
|
|
638
544
|
if (contentAnalysis.footprint.includes('content.events')) {
|
|
639
545
|
const futureEvent = {
|
|
640
546
|
...(siteFx.content.events[0] ?? {}),
|