@domandigital/craft 0.20.2 → 0.20.3

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.
@@ -39,7 +39,18 @@ var SOURCE_KINDS = [
39
39
  var CHOICE_KEYS = ["accent", "ground", "display", "body", "shape", "motif", "signature"];
40
40
  var CONSIDERATION = ["low", "considered", "high"];
41
41
  var PAGE_TYPES = ["home", "service", "about", "pricing", "booking", "contact", "article", "landing"];
42
+ var PROOF_KINDS = ["work", "people", "credentials", "reviews", "experience", "outcomes"];
43
+ var PROOF_DEPTHS = ["first-look", "on-request"];
44
+ var VIEWPORTS = ["mobile", "tablet", "desktop"];
42
45
  var HIERARCHY_FIELDS = ["primaryAction", "order", "lead", "journey"];
46
+ var CONFIDENCE_LENSES = ["relevance", "hierarchy", "evidence", "identity", "action", "disclosure", "personality", "differentiation", "mobile"];
47
+ var REVIEW_BASES = ["observed", "inferred", "preference", "unknown"];
48
+ var INSPECTED_KINDS = ["screenshot", "rendered", "source", "measurement", "content"];
49
+ var REPORTED_KINDS = ["audit", "stakeholder", "customer"];
50
+ var EVIDENCE_KINDS = [...INSPECTED_KINDS, ...REPORTED_KINDS];
51
+ var CHANGE_CLASSES = ["confirmed-correction", "design-hypothesis", "optional-refinement", "no-change"];
52
+ var CHANGE_STEPS = ["copy", "relocate", "group", "imagery", "trust-placement", "adjust-component", "remove-redundancy", "restructure", "new-component"];
53
+ var ADDITIONS = ["carousel", "javascript", "dependency", "animation", "testimonial", "cta", "duplicate-content"];
43
54
 
44
55
  // src/direction/research.ts
45
56
  function tradeOf(brief) {
@@ -71,14 +82,17 @@ function researchPrompt(input) {
71
82
  "4. **Their words.** The phrases they actually use for the problem and for what they want. Quote them exactly.",
72
83
  "5. **Who else is involved.** Who do they have to explain or justify the choice to (a partner, a landlord, a tenant, a manager), and what would make them look good or bad to that person?",
73
84
  "6. **Consideration.** Do they decide in minutes, days or months? What do they compare before deciding?",
74
- "7. **Where they look.** What do they search for, and where do they ask?",
85
+ "7. **Where they look.** What do they search for, and where do they ask? On a phone or a computer, and at what moment: standing in a kitchen, on a commute, at a desk?",
86
+ "8. **What would settle it.** Before they would get in touch, what do they check or want to see: work that was actually done, who they would deal with, a registration they can look up, what other customers said, how long the person has done this? Say which of these the people you found actually mention, and do not assume all of them matter.",
87
+ "9. **The next step.** How do they usually take it: ring, book, send an enquiry, ask for a quotation, look further first? What stops them taking it, and what do they want to know before they do?",
75
88
  "",
76
89
  "## Do not",
77
90
  "",
78
91
  `- Do not describe, list, rank or summarise what ${category} websites look like, or how they are laid out.`,
79
92
  '- Do not suggest colours, fonts, layouts, sections or visual "inspiration" from this category.',
80
93
  "- Do not summarise competitors' marketing. Their claims are the category average; the customer's words are the point.",
81
- "- Do not invent quotes. If you cannot find a source for something, say so."
94
+ "- Do not invent quotes. If you cannot find a source for something, say so.",
95
+ "- Do not assume the next step is a phone call, or that a business needs a founder's photograph, reviews, credentials or a gallery. Report what these customers say they need, including that they need none of it."
82
96
  ];
83
97
  if (input.visual) {
84
98
  lines.push(
@@ -370,6 +384,165 @@ function checkOrder(declared, page, snapshot, strict = false) {
370
384
  return [{ severity: strict ? "error" : "warn", at: `hierarchy.${page}.order`, message: `the page runs ${rendered.join(", ")}; the file says ${said.join(", ")}` }];
371
385
  }
372
386
 
387
+ // src/direction/confidence.ts
388
+ function viewportOf(width) {
389
+ return width < 768 ? "mobile" : width < 1024 ? "tablet" : "desktop";
390
+ }
391
+ var isObject = (x) => Boolean(x) && typeof x === "object" && !Array.isArray(x);
392
+ function validateConfidence(page, confidence, job, sources) {
393
+ const problems = [];
394
+ const base = `hierarchy.${page}.confidence`;
395
+ const err = (at, message) => problems.push({ severity: "error", at: `${base}${at}`, message });
396
+ if (!isObject(confidence)) {
397
+ err("", "confidence is an object: `examined` (the widths looked at) and `proof` (what the page offers)");
398
+ return problems;
399
+ }
400
+ if (confidence.examined !== void 0) {
401
+ if (!Array.isArray(confidence.examined)) err(".examined", `a list of widths: ${VIEWPORTS.join(", ")}`);
402
+ else for (const v of confidence.examined) if (!VIEWPORTS.includes(v)) err(".examined", `"${String(v)}" is not a width craft knows. Widths are: ${VIEWPORTS.join(", ")}`);
403
+ }
404
+ if (confidence.proof !== void 0 && !Array.isArray(confidence.proof)) err(".proof", "a list of evidence items");
405
+ const proof = Array.isArray(confidence.proof) ? confidence.proof : [];
406
+ proof.forEach((raw, i) => {
407
+ const at = `.proof[${i}]`;
408
+ if (!isObject(raw)) return err(at, "an evidence item is an object: kind, what, position, depth, because");
409
+ if (!PROOF_KINDS.includes(raw.kind)) err(`${at}.kind`, `one of ${PROOF_KINDS.join(", ")}`);
410
+ if (typeof raw.what !== "string" || contentWords(raw.what).size < 2) err(`${at}.what`, 'what it is, in a few words: "photographs of three finished bathrooms", not "proof"');
411
+ if (typeof raw.position !== "string" || !POSITIONS.has(raw.position)) err(`${at}.position`, 'the section it sits in, or "header" or "sticky"');
412
+ if (!PROOF_DEPTHS.includes(raw.depth)) err(`${at}.depth`, `${PROOF_DEPTHS.join(" or ")}: met without being sought, or detail a visitor opens`);
413
+ problems.push(...checkReason({ at: `${base}${at}`, because: raw.because, evidence: raw.evidence, sources, tie: "job", job }));
414
+ });
415
+ return problems;
416
+ }
417
+ var IDENTITY_WORRY = /\b(?:strangers?|who(?:'s| is| would| will) (?:coming|doing|treating|seeing|dealing|there)|(?:someone|somebody) (?:i|they|we) (?:have not|haven't|do not|don't) (?:met|know)|have not met|haven't met|not met|in (?:my|their|the) (?:home|house)|into (?:my|their|the) (?:home|house)|accountab\w+)\b/i;
418
+ var LATE_SECTION = 3;
419
+ var identityText = (job) => [job.emotional?.value, job.social?.value, ...Array.isArray(job.objections) ? job.objections : [], ...Array.isArray(job.language) ? job.language : []].filter((x) => typeof x === "string").join(" ");
420
+ var lensOf = (kind) => kind === "people" ? "identity" : "evidence";
421
+ function decisionText(h) {
422
+ const proof = Array.isArray(h.confidence?.proof) ? h.confidence.proof : [];
423
+ return [
424
+ h.primaryAction?.value,
425
+ h.primaryAction?.because,
426
+ h.lead?.value,
427
+ h.lead?.because,
428
+ ...Array.isArray(h.order) ? h.order.map((o) => o?.because) : [],
429
+ ...Array.isArray(h.journey) ? h.journey.map((s) => s?.because) : [],
430
+ ...proof.flatMap((p) => isObject(p) ? [p.what, p.because] : [])
431
+ ].filter((x) => typeof x === "string").join(" ");
432
+ }
433
+ function assessConfidence(hierarchy, job, ctx = {}) {
434
+ const findings = [];
435
+ if (!isObject(hierarchy)) return findings;
436
+ for (const [key, raw] of Object.entries(hierarchy)) {
437
+ const h = raw;
438
+ if (!PAGE_TYPES.includes(key) || !isObject(h) || !isObject(h.confidence)) continue;
439
+ const page = key;
440
+ const at = (rest) => `hierarchy.${page}.confidence${rest}`;
441
+ const add = (f) => findings.push({ page, ...f });
442
+ const c = h.confidence;
443
+ const proof = (Array.isArray(c.proof) ? c.proof : []).filter(isObject);
444
+ const order = new Set((Array.isArray(h.order) ? h.order : []).map((o) => o?.role));
445
+ const bands = new Set((Array.isArray(c.examined) ? c.examined : []).filter((v) => VIEWPORTS.includes(v)));
446
+ if (ctx.snapshot && (ctx.snapshotPage ?? "home") === page) bands.add(viewportOf(ctx.snapshot.viewport.width));
447
+ for (const pos of Array.isArray(h.primaryAction?.positions) ? h.primaryAction.positions : []) {
448
+ if (POSITIONS.has(pos) && pos !== "header" && pos !== "sticky" && !order.has(pos)) {
449
+ add({
450
+ lens: "action",
451
+ basis: "observed",
452
+ at: `hierarchy.${page}.primaryAction.positions`,
453
+ message: `the ask "${h.primaryAction.value}" sits in "${pos}", and the declared order has no such section`,
454
+ why: "A visitor can only take the next step where the page offers it. Either the order or the position is out of date."
455
+ });
456
+ }
457
+ }
458
+ proof.forEach((p, i) => {
459
+ const where = at(`.proof[${i}]`);
460
+ if (POSITIONS.has(p.position) && p.position !== "header" && p.position !== "sticky" && !order.has(p.position)) {
461
+ add({
462
+ lens: "hierarchy",
463
+ basis: "observed",
464
+ at: `${where}.position`,
465
+ message: `${p.kind} evidence "${p.what}" sits in "${p.position}", and the declared order has no such section`,
466
+ why: "Evidence a visitor is meant to meet has to be on the page, in a section the order names."
467
+ });
468
+ }
469
+ const cited = Array.isArray(p.evidence) ? p.evidence : [];
470
+ if (cited.length === 0) {
471
+ add({
472
+ lens: lensOf(p.kind),
473
+ basis: "unknown",
474
+ at: `${where}.evidence`,
475
+ message: `no source in the file shows that "${p.what}" exists`,
476
+ why: "Confidence built on evidence that is only planned or imagined is the opposite of confidence. Confirm it with the client, and cite what shows it, before the page claims it."
477
+ });
478
+ }
479
+ });
480
+ const firstLook = new Set(proof.filter((p) => p.depth === "first-look").map((p) => p.kind));
481
+ proof.forEach((p, i) => {
482
+ if (p.depth === "on-request" && !firstLook.has(p.kind)) {
483
+ add({
484
+ lens: "disclosure",
485
+ basis: "inferred",
486
+ at: at(`.proof[${i}].depth`),
487
+ message: `${p.kind} evidence "${p.what}" is held back for visitors who seek it, and nothing of the same kind is met first`,
488
+ why: "Detail can wait only if the visitor can tell it is there. Nothing at first look means the evidence may never be found. A line in the header or a short sample in the hero would do; this is a hypothesis, not a rule."
489
+ });
490
+ }
491
+ });
492
+ if (job && IDENTITY_WORRY.test(identityText(job))) {
493
+ const sections = Array.isArray(h.order) ? h.order.map((o) => o?.role) : [];
494
+ const before = (pos) => pos === "header" || pos === "sticky" ? 0 : sections.indexOf(pos);
495
+ const met = proof.filter((p) => p.kind === "people" && before(p.position) >= 0).map((p) => ({ position: p.position, before: before(p.position) }));
496
+ const earliest = met.length > 0 ? met.reduce((a, b) => b.before < a.before ? b : a) : void 0;
497
+ if (earliest && earliest.before >= LATE_SECTION) {
498
+ add({
499
+ lens: "identity",
500
+ basis: "inferred",
501
+ at: at(".proof"),
502
+ message: `the job worries about who the visitor would deal with, and the first person is met in "${earliest.position}", after ${earliest.before} other sections`,
503
+ why: `A visitor who wants to know who is coming may leave or ask before getting there. The smallest change is a name and a line where the ask is first made, not the whole biography moved up: it can stay where it is. This is a hypothesis about the order as declared; check it on the width where it matters.${bands.has("mobile") ? "" : " It has not been examined on a phone."}`
504
+ });
505
+ }
506
+ }
507
+ if (job && Array.isArray(job.objections)) {
508
+ const said = contentWords(decisionText(h));
509
+ job.objections.forEach((o, i) => {
510
+ if (typeof o !== "string") return;
511
+ if (shares(contentWords(o), said).length === 0) {
512
+ add({
513
+ lens: "relevance",
514
+ basis: "unknown",
515
+ at: `job.objections[${i}]`,
516
+ message: `no decision recorded for this page names the worry "${o}"`,
517
+ why: "A visitor who arrives with this worry needs something that meets it. Either a decision here does, and its reason should say so, or this page does not have to answer it. This reads wording only: it can miss an answer that uses other words."
518
+ });
519
+ }
520
+ });
521
+ }
522
+ if (bands.size === 0) {
523
+ add({
524
+ lens: "mobile",
525
+ basis: "unknown",
526
+ at: at(".examined"),
527
+ message: "no width recorded: say which widths these decisions were examined at",
528
+ why: "The phone is the base layout (STANDARD.md section 10). What was seen at one width says nothing about another until it has been looked at there."
529
+ });
530
+ } else {
531
+ for (const band of VIEWPORTS) {
532
+ if (bands.has(band)) continue;
533
+ add({
534
+ lens: "mobile",
535
+ basis: "unknown",
536
+ at: at(".examined"),
537
+ message: band === "mobile" ? "not examined on a phone" : `not examined at ${band} width; what was seen at ${[...bands].join(" and ")} is not carried to it`,
538
+ why: band === "mobile" ? "The phone is the base layout every wider one is derived from (STANDARD.md section 10). A page not looked at there has not been looked at where it is designed first." : "Order, prominence and what fits can differ at another width, and nothing recorded shows they do not. Examine it there before treating the finding as true of it."
539
+ });
540
+ }
541
+ }
542
+ }
543
+ return findings;
544
+ }
545
+
373
546
  // src/direction/job.ts
374
547
  var JOB_VERBS = new Set(
375
548
  "find choose pick book get fix stop avoid know feel understand compare decide keep protect prepare plan sort arrange replace repair learn prove show recover reduce save spend hire check see buy sell move start finish handle manage meet reach return treat look end make give take bring hand leave trust rely feel prove explain secure settle solve restore improve change become stay pass sign agree ask call reassure justify confirm".split(" ")
@@ -700,6 +873,7 @@ function validateDirection(input, ctx = {}) {
700
873
  }
701
874
  let job = false;
702
875
  let hierarchy = [];
876
+ let confidence;
703
877
  if (d.version === 2) {
704
878
  const j = validateJob(d.job, sources, { brief: typeof d.brief === "string" ? d.brief : "", readSource: ctx.readSource });
705
879
  problems.push(...j.problems);
@@ -707,6 +881,10 @@ function validateDirection(input, ctx = {}) {
707
881
  const h = validateHierarchy(d.hierarchy, d.job, sources);
708
882
  problems.push(...h.problems);
709
883
  hierarchy = h.pages;
884
+ const pages = d.hierarchy && typeof d.hierarchy === "object" && !Array.isArray(d.hierarchy) ? d.hierarchy : {};
885
+ const recorded = Object.entries(pages).filter(([k, p]) => PAGE_TYPES.includes(k) && p && typeof p === "object" && p.confidence !== void 0);
886
+ for (const [page, p] of recorded) problems.push(...validateConfidence(page, p.confidence, d.job, sources));
887
+ if (recorded.length) confidence = assessConfidence(d.hierarchy, d.job, { snapshot: ctx.snapshot, snapshotPage: ctx.page });
710
888
  if (ctx.snapshot) problems.push(...checkOrder(d.hierarchy?.[ctx.page ?? "home"], ctx.page ?? "home", ctx.snapshot, ctx.strict));
711
889
  }
712
890
  if (ctx.estate?.length) {
@@ -723,6 +901,7 @@ function validateDirection(input, ctx = {}) {
723
901
  valid: problems.every((p) => p.severity !== "error"),
724
902
  problems,
725
903
  decided,
904
+ ...confidence ? { confidence } : {},
726
905
  layers: { job, hierarchy, tokens: { decided, total: CHOICE_KEYS.length }, complete: complete && problems.every((p) => p.severity !== "error") }
727
906
  };
728
907
  }
@@ -730,8 +909,20 @@ function validateDirection(input, ctx = {}) {
730
909
  export {
731
910
  DIRECTION_VERSION,
732
911
  PAGE_TYPES,
912
+ VIEWPORTS,
913
+ CONFIDENCE_LENSES,
914
+ REVIEW_BASES,
915
+ INSPECTED_KINDS,
916
+ EVIDENCE_KINDS,
917
+ CHANGE_CLASSES,
918
+ CHANGE_STEPS,
919
+ ADDITIONS,
733
920
  PROPOSED,
921
+ PREFERENCE,
922
+ CONVENTION,
923
+ checkReason,
734
924
  researchPrompt,
925
+ SECTION_ROLES,
735
926
  summariseDirection,
736
927
  compareDirections,
737
928
  compareToDeclared,