@domandigital/craft 0.20.2 → 0.20.4

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/dist/index.js CHANGED
@@ -6555,14 +6555,14 @@ function cv(xs, min) {
6555
6555
  }
6556
6556
  function shapeOf(text) {
6557
6557
  const ss = splitSentences(text);
6558
- const wordCount = words(text).length;
6559
- const per100 = (n) => wordCount === 0 ? 0 : 100 * n / wordCount;
6558
+ const wordCount2 = words(text).length;
6559
+ const per100 = (n) => wordCount2 === 0 ? 0 : 100 * n / wordCount2;
6560
6560
  const share2 = (re) => ss.length === 0 ? 0 : ss.filter((s) => re.test(s)).length / ss.length;
6561
6561
  const tri = ss.flatMap(tricolons);
6562
6562
  const commas = (text.match(/,/g) ?? []).length;
6563
6563
  return {
6564
6564
  sentences: ss.length,
6565
- words: wordCount,
6565
+ words: wordCount2,
6566
6566
  tricolons: tri.length,
6567
6567
  parallelTriads: tri.filter((t) => t.parallel).length,
6568
6568
  stackedConditionals: ss.filter(stackedConditional).length,
@@ -8265,7 +8265,18 @@ var SOURCE_KINDS = [
8265
8265
  var CHOICE_KEYS = ["accent", "ground", "display", "body", "shape", "motif", "signature"];
8266
8266
  var CONSIDERATION = ["low", "considered", "high"];
8267
8267
  var PAGE_TYPES = ["home", "service", "about", "pricing", "booking", "contact", "article", "landing"];
8268
+ var PROOF_KINDS = ["work", "people", "credentials", "reviews", "experience", "outcomes"];
8269
+ var PROOF_DEPTHS = ["first-look", "on-request"];
8270
+ var VIEWPORTS = ["mobile", "tablet", "desktop"];
8268
8271
  var HIERARCHY_FIELDS = ["primaryAction", "order", "lead", "journey"];
8272
+ var CONFIDENCE_LENSES = ["relevance", "hierarchy", "evidence", "identity", "action", "disclosure", "personality", "differentiation", "mobile"];
8273
+ var REVIEW_BASES = ["observed", "inferred", "preference", "unknown"];
8274
+ var INSPECTED_KINDS = ["screenshot", "rendered", "source", "measurement", "content"];
8275
+ var REPORTED_KINDS = ["audit", "stakeholder", "customer"];
8276
+ var EVIDENCE_KINDS = [...INSPECTED_KINDS, ...REPORTED_KINDS];
8277
+ var CHANGE_CLASSES = ["confirmed-correction", "design-hypothesis", "optional-refinement", "no-change"];
8278
+ var CHANGE_STEPS = ["copy", "relocate", "group", "imagery", "trust-placement", "adjust-component", "remove-redundancy", "restructure", "new-component"];
8279
+ var ADDITIONS = ["carousel", "javascript", "dependency", "animation", "testimonial", "cta", "duplicate-content"];
8269
8280
 
8270
8281
  // src/direction/reason.ts
8271
8282
  var PROPOSED = "PROPOSED:";
@@ -8561,6 +8572,429 @@ function checkOrder(declared, page, snapshot, strict = false) {
8561
8572
  return [{ severity: strict ? "error" : "warn", at: `hierarchy.${page}.order`, message: `the page runs ${rendered.join(", ")}; the file says ${said.join(", ")}` }];
8562
8573
  }
8563
8574
 
8575
+ // src/direction/review.ts
8576
+ var isObject = (x) => Boolean(x) && typeof x === "object" && !Array.isArray(x);
8577
+ var wordCount = (x) => typeof x === "string" ? x.trim().split(/\s+/).filter(Boolean).length : 0;
8578
+ var includes = (list, x) => list.includes(x);
8579
+ var STEP_RANK = new Map(CHANGE_STEPS.map((s, i) => [s, i]));
8580
+ var CLASS_RANK = { "no-change": 0, "optional-refinement": 1, "design-hypothesis": 2, "confirmed-correction": 3 };
8581
+ var RENDERED_KINDS = ["screenshot", "rendered", "measurement"];
8582
+ var isStructural = (step) => step === "restructure" || step === "new-component";
8583
+ var GALLERY = /\b(?:gallery|galleries|carousel|slideshow|slider|swipe(?:able)?|rotating)\b/i;
8584
+ function plain2(at, text) {
8585
+ if (wordCount(text) < 8) return [{ severity: "error", at, message: "a sentence at least: say why, in this visitor's terms" }];
8586
+ const t = text;
8587
+ if (PREFERENCE.test(t)) return [{ severity: "error", at, message: "that is a preference, not a reason. Say what it does for this visitor." }];
8588
+ const c = t.match(CONVENTION);
8589
+ if (c) return [{ severity: "error", at, message: `"${c[0]}" is the category's default speaking, not a reason. Say what in this visitor's job it answers.` }];
8590
+ return [];
8591
+ }
8592
+ function checkProposalShape(at, p, required) {
8593
+ const problems = [];
8594
+ const err = (rest, message) => problems.push({ severity: "error", at: `${at}${rest}`, message });
8595
+ if (!isObject(p)) {
8596
+ if (required) err("", "a proposal is an object: change, step, and optionally adds and touches");
8597
+ return problems;
8598
+ }
8599
+ if (wordCount(p.change) < 3) err(".change", "say what changes, in a few words");
8600
+ if (!includes(CHANGE_STEPS, p.step)) err(".step", `one of ${CHANGE_STEPS.join(", ")}: the smallest step that does it`);
8601
+ if (p.adds !== void 0) {
8602
+ if (!Array.isArray(p.adds) || p.adds.some((a) => !includes(ADDITIONS, a))) err(".adds", `a list drawn from ${ADDITIONS.join(", ")}`);
8603
+ }
8604
+ if (p.comparisons !== void 0) {
8605
+ if (!Array.isArray(p.comparisons)) err(".comparisons", "a list comparing each relevant existing component");
8606
+ else p.comparisons.forEach((c, i) => {
8607
+ if (!isObject(c) || typeof c.existing !== "string" || !c.existing.trim() || typeof c.samePurpose !== "boolean") {
8608
+ err(`.comparisons[${i}]`, "existing, samePurpose and because are required");
8609
+ } else problems.push(...plain2(`${at}.comparisons[${i}].because`, c.because));
8610
+ });
8611
+ }
8612
+ if (p.touches !== void 0 && !includes(SECTION_ROLES, p.touches)) err(".touches", `a section role: ${SECTION_ROLES.join(", ")}`);
8613
+ return problems;
8614
+ }
8615
+ function checkEvidenceShape(at, e) {
8616
+ const problems = [];
8617
+ const err = (rest, message) => problems.push({ severity: "error", at: `${at}${rest}`, message });
8618
+ if (!isObject(e)) return [{ severity: "error", at, message: "evidence is an object: kind, ref, and a viewport for anything rendered" }];
8619
+ if (!includes(EVIDENCE_KINDS, e.kind)) err(".kind", `one of ${EVIDENCE_KINDS.join(", ")}`);
8620
+ if (typeof e.ref !== "string" || !e.ref.trim()) err(".ref", "where it can be found: a screenshot path, a selector, a quotation, a report");
8621
+ if (e.viewport !== void 0 && !includes(VIEWPORTS, e.viewport)) err(".viewport", `one of ${VIEWPORTS.join(", ")}`);
8622
+ if (includes(RENDERED_KINDS, e.kind) && e.viewport === void 0) err(".viewport", "say which width this was seen at: what is seen at one width is not evidence about another");
8623
+ return problems;
8624
+ }
8625
+ function checkNoteShape(i, n) {
8626
+ const base = `notes[${i}]`;
8627
+ const problems = [];
8628
+ const err = (rest, message) => problems.push({ severity: "error", at: `${base}${rest}`, message });
8629
+ if (typeof n.id !== "string" || !n.id.trim()) err(".id", "a short name for this note");
8630
+ if (n.page !== void 0 && !includes(PAGE_TYPES, n.page)) err(".page", `one of ${PAGE_TYPES.join(", ")}`);
8631
+ if (!includes(CONFIDENCE_LENSES, n.lens)) err(".lens", `one of ${CONFIDENCE_LENSES.join(", ")}`);
8632
+ if (wordCount(n.observation) < 3) err(".observation", "what was said or seen, as it was said");
8633
+ if (typeof n.raisedBy !== "string" || !n.raisedBy.trim()) err(".raisedBy", "who said it, or what found it");
8634
+ if (!includes(REVIEW_BASES, n.basis)) err(".basis", `one of ${REVIEW_BASES.join(", ")}`);
8635
+ if (!Array.isArray(n.evidence)) err(".evidence", "a list, possibly empty: what supports the note");
8636
+ else n.evidence.forEach((e, j) => problems.push(...checkEvidenceShape(`${base}.evidence[${j}]`, e)));
8637
+ if (n.appliesTo !== void 0 && (!Array.isArray(n.appliesTo) || n.appliesTo.some((v) => !includes(VIEWPORTS, v)))) err(".appliesTo", `a list drawn from ${VIEWPORTS.join(", ")}`);
8638
+ if (n.existing !== void 0 && (!Array.isArray(n.existing) || n.existing.some((x) => typeof x !== "string"))) err(".existing", "a list of what the page already has");
8639
+ if (n.suggested !== void 0) problems.push(...checkProposalShape(`${base}.suggested`, n.suggested, false));
8640
+ const r = n.recommend;
8641
+ if (!isObject(r)) {
8642
+ err(".recommend", "the recommendation: a class, and for anything but no-change a change, a step and a reason");
8643
+ return problems;
8644
+ }
8645
+ if (!includes(CHANGE_CLASSES, r.class)) {
8646
+ err(".recommend.class", `one of ${CHANGE_CLASSES.join(", ")}. no-change is a result, not a failure to answer.`);
8647
+ return problems;
8648
+ }
8649
+ if (r.class === "no-change") {
8650
+ if (r.because !== void 0) problems.push(...plain2(`${base}.recommend.because`, r.because));
8651
+ return problems;
8652
+ }
8653
+ problems.push(...checkProposalShape(`${base}.recommend`, r, true));
8654
+ problems.push(...plain2(`${base}.recommend.because`, r.because));
8655
+ const bigger = isStructural(r.step) || Array.isArray(r.adds) && r.adds.length > 0;
8656
+ if (bigger) {
8657
+ const smaller = Array.isArray(r.smaller) ? r.smaller : [];
8658
+ if (smaller.length === 0) err(".recommend.smaller", "a structural change, or one that adds a carousel, script, dependency, animation, testimonial, call to action or repeated content, says which smaller steps were considered and why none was enough");
8659
+ const limit = STEP_RANK.get(r.step) ?? 0;
8660
+ smaller.forEach((s, j) => {
8661
+ const ok = isObject(s) && includes(CHANGE_STEPS, s.step) && (limit === 0 || (STEP_RANK.get(s.step) ?? 99) < limit);
8662
+ if (!ok) err(`.recommend.smaller[${j}].step`, "a step on the ladder below the one recommended");
8663
+ if (isObject(s)) problems.push(...plain2(`${base}.recommend.smaller[${j}].whyNot`, s.whyNot));
8664
+ });
8665
+ } else {
8666
+ problems.push(...plain2(`${base}.recommend.whyEnough`, r.whyEnough));
8667
+ }
8668
+ if (r.class === "design-hypothesis" && wordCount(r.validation) < 4) err(".recommend.validation", "a hypothesis says how it would be checked: a phone test, five visitors, a read of the enquiries");
8669
+ if (r.requires !== void 0) {
8670
+ if (!Array.isArray(r.requires) || r.requires.some((q) => !isObject(q) || wordCount(q.what) < 2)) err(".recommend.requires", "a list of {what, source?}: facts or assets the change depends on");
8671
+ }
8672
+ return problems;
8673
+ }
8674
+ function whyNot(p, ctx) {
8675
+ const reasons = [];
8676
+ const adds = Array.isArray(p.adds) ? p.adds : [];
8677
+ if (adds.includes("carousel")) {
8678
+ for (const have of new Set(ctx.existing.filter((e) => GALLERY.test(e)))) {
8679
+ const compared = p.comparisons?.filter((c) => c.existing === have) ?? [];
8680
+ if (compared.length !== 1) reasons.push(`the page already has "${have}". Compare its purpose, content and placement before adding another carousel; duplication is not established by its name.`);
8681
+ else if (compared[0].samePurpose) reasons.push(`the comparison identifies the same purpose as "${have}": ${compared[0].because}. Reuse or adjust that component.`);
8682
+ }
8683
+ }
8684
+ const defensible = (ctx.basis === "observed" || ctx.basis === "inferred") && ctx.impact;
8685
+ if ((isStructural(p.step) || adds.length > 0) && !defensible) {
8686
+ reasons.push("it is a structural change or adds something that costs, and the problem under it is neither observed nor inferred with a stated visitor impact. A preference does not justify it.");
8687
+ }
8688
+ if (p.step === "remove-redundancy" && !(ctx.basis === "observed" && ctx.impact)) {
8689
+ reasons.push("it removes information, and nothing observed shows it is redundant or what it costs the visitor. Useful detail stays unless it is shown not to be.");
8690
+ }
8691
+ if (p.touches && ctx.placed.has(p.touches) && ctx.basis !== "observed") {
8692
+ reasons.push(`the declared order already places "${p.touches}" with a reason. Only something observed overrides a reasoned decision.`);
8693
+ }
8694
+ return reasons;
8695
+ }
8696
+ function widthsOf(evidence) {
8697
+ const seen = /* @__PURE__ */ new Set();
8698
+ for (const e of evidence) {
8699
+ if (e.viewport) seen.add(e.viewport);
8700
+ else if (e.kind === "content") VIEWPORTS.forEach((v) => seen.add(v));
8701
+ }
8702
+ return VIEWPORTS.filter((v) => seen.has(v));
8703
+ }
8704
+ function assessReview(input, direction) {
8705
+ const problems = [];
8706
+ const results = [];
8707
+ const fail = (at, message) => problems.push({ severity: "error", at, message });
8708
+ const report2 = () => {
8709
+ const n = (o) => results.filter((r) => r.outcome === o).length;
8710
+ const summary = { change: n("change"), hold: n("hold"), noChange: n("no-change"), gatherEvidence: n("gather-evidence"), verdict: "" };
8711
+ summary.verdict = results.length === 0 ? "The review has no notes to assess." : summary.change + summary.hold + summary.gatherEvidence === 0 ? "The existing design is already appropriate for what was reviewed. No change is recommended." : `${summary.change} change${summary.change === 1 ? "" : "s"} recommended, ${summary.hold} held for something nobody has supplied, ${summary.gatherEvidence} to establish first, ${summary.noChange} needing no change.`;
8712
+ return { valid: !problems.some((p) => p.severity === "error"), problems, results, summary };
8713
+ };
8714
+ if (!isObject(input)) {
8715
+ fail("", "a review is an object: version, subject and notes");
8716
+ return report2();
8717
+ }
8718
+ if (input.context !== void 0 && !["publication", "protected-staging"].includes(input.context)) fail("context", "publication or protected-staging");
8719
+ if (input.version !== 1) fail("version", "version is 1");
8720
+ if (typeof input.subject !== "string" || !input.subject.trim()) fail("subject", "who or what was reviewed");
8721
+ if (!Array.isArray(input.notes)) {
8722
+ fail("notes", "a list of notes");
8723
+ return report2();
8724
+ }
8725
+ const job = direction.job;
8726
+ if (!job) {
8727
+ fail("", "a review is read against the visitor's job, and the art direction has no job map yet. Record it first (craft direction research).");
8728
+ return report2();
8729
+ }
8730
+ const ids = /* @__PURE__ */ new Set();
8731
+ input.notes.forEach((raw, i) => {
8732
+ if (!isObject(raw)) return fail(`notes[${i}]`, "a note is an object");
8733
+ const shape = checkNoteShape(i, raw);
8734
+ if (typeof raw.id === "string") {
8735
+ if (ids.has(raw.id)) shape.push({ severity: "error", at: `notes[${i}].id`, message: `"${raw.id}" is used twice` });
8736
+ ids.add(raw.id);
8737
+ }
8738
+ problems.push(...shape);
8739
+ if (shape.length > 0) return;
8740
+ const note = raw;
8741
+ const page = note.page ?? "home";
8742
+ const adjustments = [];
8743
+ const hierarchy = direction.hierarchy?.[page];
8744
+ const sources = Array.isArray(direction.sources) ? direction.sources : [];
8745
+ const inspected = note.evidence.filter((e) => includes(INSPECTED_KINDS, e.kind));
8746
+ let basis = note.basis;
8747
+ if (basis === "observed" && inspected.length === 0) {
8748
+ basis = note.evidence.length > 0 ? "inferred" : "unknown";
8749
+ adjustments.push(
8750
+ note.evidence.length > 0 ? "declared observed, but everything cited is reported (an audit, a stakeholder, a customer) and nothing was inspected. Read as inferred until someone looks." : "declared observed, but nothing is cited. Read as unknown."
8751
+ );
8752
+ } else if (basis === "inferred" && note.evidence.length === 0) {
8753
+ basis = "unknown";
8754
+ adjustments.push("declared inferred, but nothing supports it. Read as unknown.");
8755
+ }
8756
+ const impactProblems = typeof note.impact === "string" ? checkReason({ at: `notes[${i}].impact`, because: note.impact, sources, tie: "job", job }) : [];
8757
+ const impact = typeof note.impact === "string" && impactProblems.length === 0;
8758
+ const wantsChange = note.recommend.class !== "no-change";
8759
+ if (!impact && (wantsChange || note.suggested)) {
8760
+ adjustments.push(
8761
+ typeof note.impact === "string" ? `no visitor impact tied to this client's job (${impactProblems[0]?.message ?? "none given"}).` : "no visitor impact stated. Something that costs the visitor nothing they can name is a fact or a preference, not an issue, and is not read as one."
8762
+ );
8763
+ if (basis === "inferred") basis = "preference";
8764
+ }
8765
+ const ctx = {
8766
+ note,
8767
+ basis,
8768
+ impact,
8769
+ existing: [...note.existing ?? [], ...(hierarchy?.confidence?.proof ?? []).map((p) => p.what).filter((w) => typeof w === "string")],
8770
+ placed: new Set((hierarchy?.order ?? []).map((o) => o.role))
8771
+ };
8772
+ const examined = widthsOf(note.evidence);
8773
+ const wanted = note.appliesTo ?? (examined.length > 0 ? examined : [...VIEWPORTS]);
8774
+ const claimed = wanted.filter((v) => examined.includes(v));
8775
+ const unknown = VIEWPORTS.filter((v) => wanted.includes(v) && !examined.includes(v));
8776
+ if (unknown.length > 0) adjustments.push(`nothing here was looked at on ${unknown.join(" or ")}, so the note says nothing about it. That stays unknown until it is examined there.`);
8777
+ const rec = note.recommend;
8778
+ const recReasons = wantsChange ? whyNot(rec, ctx) : [];
8779
+ const sugReasons = note.suggested ? whyNot(note.suggested, ctx) : [];
8780
+ const ceiling = basis === "observed" ? impact ? "confirmed-correction" : "optional-refinement" : basis === "inferred" ? "design-hypothesis" : basis === "preference" ? "optional-refinement" : "no-change";
8781
+ let effective = wantsChange ? rec.class : "no-change";
8782
+ if (basis !== "unknown" && CLASS_RANK[effective] > CLASS_RANK[ceiling]) {
8783
+ adjustments.push(`declared ${rec.class}, but a problem that is ${basis}${basis === "observed" && !impact ? " with no stated visitor impact" : ""} supports at most ${ceiling}.`);
8784
+ effective = ceiling;
8785
+ }
8786
+ let withdrawn;
8787
+ if (wantsChange && recReasons.length > 0) {
8788
+ withdrawn = recReasons;
8789
+ effective = "no-change";
8790
+ adjustments.push("the recommended change is withdrawn: " + recReasons.join(" "));
8791
+ }
8792
+ const missing = (rec.requires ?? []).filter((q) => !q.source || !sources.some((s) => s.id === q.source));
8793
+ const outstanding = missing.map((q) => q.what);
8794
+ const blocking = missing.filter((q) => !(input.context === "protected-staging" && q.kind === "asset" && q.phase === "publication" && q.placeholder));
8795
+ if (missing.length > blocking.length) adjustments.push("Protected staging may reserve the explicitly marked placeholder. Required assets remain outstanding for publication; no fact or photograph has been supplied by this review.");
8796
+ let outcome;
8797
+ if (basis === "unknown") {
8798
+ outcome = "gather-evidence";
8799
+ effective = "no-change";
8800
+ if (wantsChange) adjustments.push(`nothing is recommended until the problem is established${rec.validation ? `: ${rec.validation}` : ". Ask the client, or look, first."}`);
8801
+ } else if (effective === "no-change") outcome = "no-change";
8802
+ else if (blocking.length > 0) outcome = "hold";
8803
+ else outcome = "change";
8804
+ if (outcome === "hold") adjustments.push(`held: it depends on ${blocking.map((o) => `"${o.what}"`).join(", ")}, and no source supplies it. Do not write it in. Ask the client.`);
8805
+ let suggested;
8806
+ if (note.suggested) {
8807
+ const recRank = STEP_RANK.get(rec.step ?? "") ?? -1;
8808
+ const sugRank = STEP_RANK.get(note.suggested.step) ?? 0;
8809
+ const sugAdds = (note.suggested.adds ?? []).length;
8810
+ let standing;
8811
+ const reasons = [...sugReasons];
8812
+ if (sugReasons.length > 0) standing = "not-supported";
8813
+ else if (effective === "no-change") {
8814
+ standing = "not-needed";
8815
+ reasons.push("the existing design already answers the problem, or nothing is yet established to answer.");
8816
+ } else if (sugRank > recRank || sugAdds > 0 && (rec.adds ?? []).length === 0) {
8817
+ standing = "comparison-needed";
8818
+ reasons.push("Compare the proposals before selecting one. A smaller implementation is not evidence of a better visitor experience; compare their adequacy against the stated problem and rendered result.");
8819
+ } else standing = "supported";
8820
+ suggested = { standing, reasons };
8821
+ }
8822
+ results.push({
8823
+ id: note.id,
8824
+ page,
8825
+ lens: note.lens,
8826
+ basis: { declared: note.basis, effective: basis },
8827
+ ...suggested ? { suggested } : {},
8828
+ recommend: { declared: rec.class, effective, ...wantsChange ? { step: rec.step } : {}, ...withdrawn ? { withdrawn } : {} },
8829
+ outcome,
8830
+ widths: { examined, claimed, unknown },
8831
+ adjustments,
8832
+ outstanding,
8833
+ publicationReady: outstanding.length === 0
8834
+ });
8835
+ });
8836
+ return report2();
8837
+ }
8838
+
8839
+ // src/direction/confidence.ts
8840
+ function viewportOf(width) {
8841
+ return width < 768 ? "mobile" : width < 1024 ? "tablet" : "desktop";
8842
+ }
8843
+ var isObject2 = (x) => Boolean(x) && typeof x === "object" && !Array.isArray(x);
8844
+ function validateConfidence(page, confidence, job, sources) {
8845
+ const problems = [];
8846
+ const base = `hierarchy.${page}.confidence`;
8847
+ const err = (at, message) => problems.push({ severity: "error", at: `${base}${at}`, message });
8848
+ if (!isObject2(confidence)) {
8849
+ err("", "confidence is an object: `examined` (the widths looked at) and `proof` (what the page offers)");
8850
+ return problems;
8851
+ }
8852
+ if (confidence.examined !== void 0) {
8853
+ if (!Array.isArray(confidence.examined)) err(".examined", `a list of widths: ${VIEWPORTS.join(", ")}`);
8854
+ 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(", ")}`);
8855
+ }
8856
+ if (confidence.proof !== void 0 && !Array.isArray(confidence.proof)) err(".proof", "a list of evidence items");
8857
+ const proof = Array.isArray(confidence.proof) ? confidence.proof : [];
8858
+ proof.forEach((raw, i) => {
8859
+ const at = `.proof[${i}]`;
8860
+ if (!isObject2(raw)) return err(at, "an evidence item is an object: kind, what, position, depth, because");
8861
+ if (!PROOF_KINDS.includes(raw.kind)) err(`${at}.kind`, `one of ${PROOF_KINDS.join(", ")}`);
8862
+ 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"');
8863
+ if (typeof raw.position !== "string" || !POSITIONS.has(raw.position)) err(`${at}.position`, 'the section it sits in, or "header" or "sticky"');
8864
+ if (!PROOF_DEPTHS.includes(raw.depth)) err(`${at}.depth`, `${PROOF_DEPTHS.join(" or ")}: met without being sought, or detail a visitor opens`);
8865
+ problems.push(...checkReason({ at: `${base}${at}`, because: raw.because, evidence: raw.evidence, sources, tie: "job", job }));
8866
+ });
8867
+ return problems;
8868
+ }
8869
+ 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;
8870
+ var LATE_SECTION = 3;
8871
+ 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(" ");
8872
+ var lensOf = (kind) => kind === "people" ? "identity" : "evidence";
8873
+ function decisionText(h) {
8874
+ const proof = Array.isArray(h.confidence?.proof) ? h.confidence.proof : [];
8875
+ return [
8876
+ h.primaryAction?.value,
8877
+ h.primaryAction?.because,
8878
+ h.lead?.value,
8879
+ h.lead?.because,
8880
+ ...Array.isArray(h.order) ? h.order.map((o) => o?.because) : [],
8881
+ ...Array.isArray(h.journey) ? h.journey.map((s) => s?.because) : [],
8882
+ ...proof.flatMap((p) => isObject2(p) ? [p.what, p.because] : [])
8883
+ ].filter((x) => typeof x === "string").join(" ");
8884
+ }
8885
+ function assessConfidence(hierarchy, job, ctx = {}) {
8886
+ const findings = [];
8887
+ if (!isObject2(hierarchy)) return findings;
8888
+ for (const [key, raw] of Object.entries(hierarchy)) {
8889
+ const h = raw;
8890
+ if (!PAGE_TYPES.includes(key) || !isObject2(h) || !isObject2(h.confidence)) continue;
8891
+ const page = key;
8892
+ const at = (rest) => `hierarchy.${page}.confidence${rest}`;
8893
+ const add = (f4) => findings.push({ page, ...f4 });
8894
+ const c = h.confidence;
8895
+ const proof = (Array.isArray(c.proof) ? c.proof : []).filter(isObject2);
8896
+ const order = new Set((Array.isArray(h.order) ? h.order : []).map((o) => o?.role));
8897
+ const bands = new Set((Array.isArray(c.examined) ? c.examined : []).filter((v) => VIEWPORTS.includes(v)));
8898
+ if (ctx.snapshot && (ctx.snapshotPage ?? "home") === page) bands.add(viewportOf(ctx.snapshot.viewport.width));
8899
+ for (const pos of Array.isArray(h.primaryAction?.positions) ? h.primaryAction.positions : []) {
8900
+ if (POSITIONS.has(pos) && pos !== "header" && pos !== "sticky" && !order.has(pos)) {
8901
+ add({
8902
+ lens: "action",
8903
+ basis: "observed",
8904
+ at: `hierarchy.${page}.primaryAction.positions`,
8905
+ message: `the ask "${h.primaryAction.value}" sits in "${pos}", and the declared order has no such section`,
8906
+ why: "A visitor can only take the next step where the page offers it. Either the order or the position is out of date."
8907
+ });
8908
+ }
8909
+ }
8910
+ proof.forEach((p, i) => {
8911
+ const where = at(`.proof[${i}]`);
8912
+ if (POSITIONS.has(p.position) && p.position !== "header" && p.position !== "sticky" && !order.has(p.position)) {
8913
+ add({
8914
+ lens: "hierarchy",
8915
+ basis: "observed",
8916
+ at: `${where}.position`,
8917
+ message: `${p.kind} evidence "${p.what}" sits in "${p.position}", and the declared order has no such section`,
8918
+ why: "Evidence a visitor is meant to meet has to be on the page, in a section the order names."
8919
+ });
8920
+ }
8921
+ const cited = Array.isArray(p.evidence) ? p.evidence : [];
8922
+ if (cited.length === 0) {
8923
+ add({
8924
+ lens: lensOf(p.kind),
8925
+ basis: "unknown",
8926
+ at: `${where}.evidence`,
8927
+ message: `no source in the file shows that "${p.what}" exists`,
8928
+ 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."
8929
+ });
8930
+ }
8931
+ });
8932
+ const firstLook = new Set(proof.filter((p) => p.depth === "first-look").map((p) => p.kind));
8933
+ proof.forEach((p, i) => {
8934
+ if (p.depth === "on-request" && !firstLook.has(p.kind)) {
8935
+ add({
8936
+ lens: "disclosure",
8937
+ basis: "inferred",
8938
+ at: at(`.proof[${i}].depth`),
8939
+ message: `${p.kind} evidence "${p.what}" is held back for visitors who seek it, and nothing of the same kind is met first`,
8940
+ 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."
8941
+ });
8942
+ }
8943
+ });
8944
+ if (job && IDENTITY_WORRY.test(identityText(job))) {
8945
+ const sections = Array.isArray(h.order) ? h.order.map((o) => o?.role) : [];
8946
+ const before = (pos) => pos === "header" || pos === "sticky" ? 0 : sections.indexOf(pos);
8947
+ const met = proof.filter((p) => p.kind === "people" && before(p.position) >= 0).map((p) => ({ position: p.position, before: before(p.position) }));
8948
+ const earliest = met.length > 0 ? met.reduce((a, b) => b.before < a.before ? b : a) : void 0;
8949
+ if (earliest && earliest.before >= LATE_SECTION) {
8950
+ add({
8951
+ lens: "identity",
8952
+ basis: "inferred",
8953
+ at: at(".proof"),
8954
+ 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`,
8955
+ 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."}`
8956
+ });
8957
+ }
8958
+ }
8959
+ if (job && Array.isArray(job.objections)) {
8960
+ const said = contentWords(decisionText(h));
8961
+ job.objections.forEach((o, i) => {
8962
+ if (typeof o !== "string") return;
8963
+ if (shares(contentWords(o), said).length === 0) {
8964
+ add({
8965
+ lens: "relevance",
8966
+ basis: "unknown",
8967
+ at: `job.objections[${i}]`,
8968
+ message: `no decision recorded for this page names the worry "${o}"`,
8969
+ 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."
8970
+ });
8971
+ }
8972
+ });
8973
+ }
8974
+ if (bands.size === 0) {
8975
+ add({
8976
+ lens: "mobile",
8977
+ basis: "unknown",
8978
+ at: at(".examined"),
8979
+ message: "no width recorded: say which widths these decisions were examined at",
8980
+ 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."
8981
+ });
8982
+ } else {
8983
+ for (const band of VIEWPORTS) {
8984
+ if (bands.has(band)) continue;
8985
+ add({
8986
+ lens: "mobile",
8987
+ basis: "unknown",
8988
+ at: at(".examined"),
8989
+ 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`,
8990
+ 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."
8991
+ });
8992
+ }
8993
+ }
8994
+ }
8995
+ return findings;
8996
+ }
8997
+
8564
8998
  // src/direction/research.ts
8565
8999
  function tradeOf(brief) {
8566
9000
  const text = brief.toLowerCase();
@@ -8591,14 +9025,17 @@ function researchPrompt(input) {
8591
9025
  "4. **Their words.** The phrases they actually use for the problem and for what they want. Quote them exactly.",
8592
9026
  "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?",
8593
9027
  "6. **Consideration.** Do they decide in minutes, days or months? What do they compare before deciding?",
8594
- "7. **Where they look.** What do they search for, and where do they ask?",
9028
+ "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?",
9029
+ "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.",
9030
+ "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?",
8595
9031
  "",
8596
9032
  "## Do not",
8597
9033
  "",
8598
9034
  `- Do not describe, list, rank or summarise what ${category} websites look like, or how they are laid out.`,
8599
9035
  '- Do not suggest colours, fonts, layouts, sections or visual "inspiration" from this category.',
8600
9036
  "- Do not summarise competitors' marketing. Their claims are the category average; the customer's words are the point.",
8601
- "- Do not invent quotes. If you cannot find a source for something, say so."
9037
+ "- Do not invent quotes. If you cannot find a source for something, say so.",
9038
+ "- 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."
8602
9039
  ];
8603
9040
  if (input.visual) {
8604
9041
  lines.push(
@@ -8929,6 +9366,7 @@ function validateDirection(input, ctx = {}) {
8929
9366
  }
8930
9367
  let job = false;
8931
9368
  let hierarchy = [];
9369
+ let confidence;
8932
9370
  if (d.version === 2) {
8933
9371
  const j = validateJob(d.job, sources, { brief: typeof d.brief === "string" ? d.brief : "", readSource: ctx.readSource });
8934
9372
  problems.push(...j.problems);
@@ -8936,6 +9374,10 @@ function validateDirection(input, ctx = {}) {
8936
9374
  const h = validateHierarchy(d.hierarchy, d.job, sources);
8937
9375
  problems.push(...h.problems);
8938
9376
  hierarchy = h.pages;
9377
+ const pages = d.hierarchy && typeof d.hierarchy === "object" && !Array.isArray(d.hierarchy) ? d.hierarchy : {};
9378
+ const recorded = Object.entries(pages).filter(([k, p]) => PAGE_TYPES.includes(k) && p && typeof p === "object" && p.confidence !== void 0);
9379
+ for (const [page, p] of recorded) problems.push(...validateConfidence(page, p.confidence, d.job, sources));
9380
+ if (recorded.length) confidence = assessConfidence(d.hierarchy, d.job, { snapshot: ctx.snapshot, snapshotPage: ctx.page });
8939
9381
  if (ctx.snapshot) problems.push(...checkOrder(d.hierarchy?.[ctx.page ?? "home"], ctx.page ?? "home", ctx.snapshot, ctx.strict));
8940
9382
  }
8941
9383
  if (ctx.estate?.length) {
@@ -8952,6 +9394,7 @@ function validateDirection(input, ctx = {}) {
8952
9394
  valid: problems.every((p) => p.severity !== "error"),
8953
9395
  problems,
8954
9396
  decided,
9397
+ ...confidence ? { confidence } : {},
8955
9398
  layers: { job, hierarchy, tokens: { decided, total: CHOICE_KEYS.length }, complete: complete && problems.every((p) => p.severity !== "error") }
8956
9399
  };
8957
9400
  }
@@ -9509,6 +9952,7 @@ function retrofitPlan(report2, direction) {
9509
9952
  export {
9510
9953
  ACCENT_SCALE,
9511
9954
  ACCENT_SHARED,
9955
+ ADDITIONS,
9512
9956
  AI_PHRASES,
9513
9957
  AI_SOURCE_TYPES,
9514
9958
  AI_VIOLET,
@@ -9517,8 +9961,11 @@ export {
9517
9961
  BUZZWORDS,
9518
9962
  CATALOGUE,
9519
9963
  CATALOGUE_VERSION,
9964
+ CHANGE_CLASSES,
9965
+ CHANGE_STEPS,
9520
9966
  CHOICE_KEYS,
9521
9967
  COMPONENT_ROLES,
9968
+ CONFIDENCE_LENSES,
9522
9969
  CONSIDERATION,
9523
9970
  CONTENT_FEATURES,
9524
9971
  CONVENTION,
@@ -9535,6 +9982,7 @@ export {
9535
9982
  EASE_TUPLE,
9536
9983
  EDGE_STEP,
9537
9984
  ESTATE_VERSION,
9985
+ EVIDENCE_KINDS,
9538
9986
  EXIT_RATIO,
9539
9987
  FINGERPRINT_VERSION,
9540
9988
  FLAGGED_TARGET,
@@ -9550,6 +9998,7 @@ export {
9550
9998
  HOUSE_TYPE,
9551
9999
  HUMAN_TYPICAL_TARGET,
9552
10000
  IMAGERY_TELLS,
10001
+ INSPECTED_KINDS,
9553
10002
  JSON_SCHEMA_VERSION,
9554
10003
  LABELS2 as LABELS,
9555
10004
  LEXICAL_FEATURES,
@@ -9570,6 +10019,8 @@ export {
9570
10019
  PROFILE_MEASURED,
9571
10020
  PROFILE_PARAGRAPHS,
9572
10021
  PROMO_BADGE,
10022
+ PROOF_DEPTHS,
10023
+ PROOF_KINDS,
9573
10024
  PROOF_NAMES,
9574
10025
  PROPOSED,
9575
10026
  PROVENANCE_SCAN_BYTES,
@@ -9583,7 +10034,9 @@ export {
9583
10034
  REGISTER_IDS,
9584
10035
  RENDERED_PATHS,
9585
10036
  RENDERED_TELLS,
10037
+ REPORTED_KINDS,
9586
10038
  REVEAL_LIMIT,
10039
+ REVIEW_BASES,
9587
10040
  REVIEW_PHRASES,
9588
10041
  SAMPLES_PER_SEGMENT,
9589
10042
  SCALE,
@@ -9612,12 +10065,15 @@ export {
9612
10065
  TYPE_STEPS,
9613
10066
  TYPICAL_AT,
9614
10067
  VAGUE_WORDS,
10068
+ VIEWPORTS,
9615
10069
  accentFork,
9616
10070
  addSite,
9617
10071
  apcaContrast,
9618
10072
  applyBaseline,
9619
10073
  applyHouseGate,
9620
10074
  applySeverity,
10075
+ assessConfidence,
10076
+ assessReview,
9621
10077
  auditSnapshot,
9622
10078
  bandOf,
9623
10079
  briefTerms,
@@ -9705,6 +10161,7 @@ export {
9705
10161
  isCategoryLabel,
9706
10162
  isCream,
9707
10163
  isFlagged,
10164
+ isStructural,
9708
10165
  jobWords,
9709
10166
  layoutDistance,
9710
10167
  lexicalOf,
@@ -9775,9 +10232,11 @@ export {
9775
10232
  typeFeatureTokens,
9776
10233
  typicality,
9777
10234
  unprovenClaims,
10235
+ validateConfidence,
9778
10236
  validateDirection,
9779
10237
  validateHierarchy,
9780
10238
  validateJob,
10239
+ viewportOf,
9781
10240
  visibleText,
9782
10241
  visualMeasures,
9783
10242
  vocabulary,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@domandigital/craft",
3
- "version": "0.20.2",
3
+ "version": "0.20.4",
4
4
  "private": false,
5
5
  "description": "The Doman Digital house craft standard as numbers: OKLCh colour ramps that snap to existing anchors, derived semantic tokens with measured WCAG and APCA contrast, one shared motion vocabulary for CSS and JS, and a base stylesheet. Zero dependencies.",
6
6
  "license": "Apache-2.0",
@@ -48,11 +48,13 @@
48
48
  "./package.json": "./package.json",
49
49
  "./craft.css": "./css/craft.css",
50
50
  "./craft.tailwind.css": "./css/craft.tailwind.css",
51
- "./art-direction.schema.json": "./art-direction.schema.json"
51
+ "./art-direction.schema.json": "./art-direction.schema.json",
52
+ "./design-review.schema.json": "./design-review.schema.json"
52
53
  },
53
54
  "files": [
54
55
  "CHANGELOG.md",
55
56
  "art-direction.schema.json",
57
+ "design-review.schema.json",
56
58
  "CHARACTER.md",
57
59
  "COPY.md",
58
60
  "LICENSE",