@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.cjs CHANGED
@@ -22,6 +22,7 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  ACCENT_SCALE: () => ACCENT_SCALE,
24
24
  ACCENT_SHARED: () => ACCENT_SHARED,
25
+ ADDITIONS: () => ADDITIONS,
25
26
  AI_PHRASES: () => AI_PHRASES,
26
27
  AI_SOURCE_TYPES: () => AI_SOURCE_TYPES,
27
28
  AI_VIOLET: () => AI_VIOLET,
@@ -30,8 +31,11 @@ __export(index_exports, {
30
31
  BUZZWORDS: () => BUZZWORDS,
31
32
  CATALOGUE: () => CATALOGUE,
32
33
  CATALOGUE_VERSION: () => CATALOGUE_VERSION,
34
+ CHANGE_CLASSES: () => CHANGE_CLASSES,
35
+ CHANGE_STEPS: () => CHANGE_STEPS,
33
36
  CHOICE_KEYS: () => CHOICE_KEYS,
34
37
  COMPONENT_ROLES: () => COMPONENT_ROLES,
38
+ CONFIDENCE_LENSES: () => CONFIDENCE_LENSES,
35
39
  CONSIDERATION: () => CONSIDERATION,
36
40
  CONTENT_FEATURES: () => CONTENT_FEATURES,
37
41
  CONVENTION: () => CONVENTION,
@@ -48,6 +52,7 @@ __export(index_exports, {
48
52
  EASE_TUPLE: () => EASE_TUPLE,
49
53
  EDGE_STEP: () => EDGE_STEP,
50
54
  ESTATE_VERSION: () => ESTATE_VERSION,
55
+ EVIDENCE_KINDS: () => EVIDENCE_KINDS,
51
56
  EXIT_RATIO: () => EXIT_RATIO,
52
57
  FINGERPRINT_VERSION: () => FINGERPRINT_VERSION,
53
58
  FLAGGED_TARGET: () => FLAGGED_TARGET,
@@ -63,6 +68,7 @@ __export(index_exports, {
63
68
  HOUSE_TYPE: () => HOUSE_TYPE,
64
69
  HUMAN_TYPICAL_TARGET: () => HUMAN_TYPICAL_TARGET,
65
70
  IMAGERY_TELLS: () => IMAGERY_TELLS,
71
+ INSPECTED_KINDS: () => INSPECTED_KINDS,
66
72
  JSON_SCHEMA_VERSION: () => JSON_SCHEMA_VERSION,
67
73
  LABELS: () => LABELS2,
68
74
  LEXICAL_FEATURES: () => LEXICAL_FEATURES,
@@ -83,6 +89,8 @@ __export(index_exports, {
83
89
  PROFILE_MEASURED: () => PROFILE_MEASURED,
84
90
  PROFILE_PARAGRAPHS: () => PROFILE_PARAGRAPHS,
85
91
  PROMO_BADGE: () => PROMO_BADGE,
92
+ PROOF_DEPTHS: () => PROOF_DEPTHS,
93
+ PROOF_KINDS: () => PROOF_KINDS,
86
94
  PROOF_NAMES: () => PROOF_NAMES,
87
95
  PROPOSED: () => PROPOSED,
88
96
  PROVENANCE_SCAN_BYTES: () => PROVENANCE_SCAN_BYTES,
@@ -96,7 +104,9 @@ __export(index_exports, {
96
104
  REGISTER_IDS: () => REGISTER_IDS,
97
105
  RENDERED_PATHS: () => RENDERED_PATHS,
98
106
  RENDERED_TELLS: () => RENDERED_TELLS,
107
+ REPORTED_KINDS: () => REPORTED_KINDS,
99
108
  REVEAL_LIMIT: () => REVEAL_LIMIT,
109
+ REVIEW_BASES: () => REVIEW_BASES,
100
110
  REVIEW_PHRASES: () => REVIEW_PHRASES,
101
111
  SAMPLES_PER_SEGMENT: () => SAMPLES_PER_SEGMENT,
102
112
  SCALE: () => SCALE,
@@ -125,12 +135,15 @@ __export(index_exports, {
125
135
  TYPE_STEPS: () => TYPE_STEPS,
126
136
  TYPICAL_AT: () => TYPICAL_AT,
127
137
  VAGUE_WORDS: () => VAGUE_WORDS,
138
+ VIEWPORTS: () => VIEWPORTS,
128
139
  accentFork: () => accentFork,
129
140
  addSite: () => addSite,
130
141
  apcaContrast: () => apcaContrast,
131
142
  applyBaseline: () => applyBaseline,
132
143
  applyHouseGate: () => applyHouseGate,
133
144
  applySeverity: () => applySeverity,
145
+ assessConfidence: () => assessConfidence,
146
+ assessReview: () => assessReview,
134
147
  auditSnapshot: () => auditSnapshot,
135
148
  bandOf: () => bandOf,
136
149
  briefTerms: () => briefTerms,
@@ -218,6 +231,7 @@ __export(index_exports, {
218
231
  isCategoryLabel: () => isCategoryLabel,
219
232
  isCream: () => isCream,
220
233
  isFlagged: () => isFlagged,
234
+ isStructural: () => isStructural,
221
235
  jobWords: () => jobWords,
222
236
  layoutDistance: () => layoutDistance,
223
237
  lexicalOf: () => lexicalOf,
@@ -288,9 +302,11 @@ __export(index_exports, {
288
302
  typeFeatureTokens: () => typeFeatureTokens,
289
303
  typicality: () => typicality,
290
304
  unprovenClaims: () => unprovenClaims,
305
+ validateConfidence: () => validateConfidence,
291
306
  validateDirection: () => validateDirection,
292
307
  validateHierarchy: () => validateHierarchy,
293
308
  validateJob: () => validateJob,
309
+ viewportOf: () => viewportOf,
294
310
  visibleText: () => visibleText,
295
311
  visualMeasures: () => visualMeasures,
296
312
  vocabulary: () => vocabulary,
@@ -6857,14 +6873,14 @@ function cv(xs, min) {
6857
6873
  }
6858
6874
  function shapeOf(text) {
6859
6875
  const ss = splitSentences(text);
6860
- const wordCount = words(text).length;
6861
- const per100 = (n) => wordCount === 0 ? 0 : 100 * n / wordCount;
6876
+ const wordCount2 = words(text).length;
6877
+ const per100 = (n) => wordCount2 === 0 ? 0 : 100 * n / wordCount2;
6862
6878
  const share2 = (re) => ss.length === 0 ? 0 : ss.filter((s) => re.test(s)).length / ss.length;
6863
6879
  const tri = ss.flatMap(tricolons);
6864
6880
  const commas = (text.match(/,/g) ?? []).length;
6865
6881
  return {
6866
6882
  sentences: ss.length,
6867
- words: wordCount,
6883
+ words: wordCount2,
6868
6884
  tricolons: tri.length,
6869
6885
  parallelTriads: tri.filter((t) => t.parallel).length,
6870
6886
  stackedConditionals: ss.filter(stackedConditional).length,
@@ -8567,7 +8583,18 @@ var SOURCE_KINDS = [
8567
8583
  var CHOICE_KEYS = ["accent", "ground", "display", "body", "shape", "motif", "signature"];
8568
8584
  var CONSIDERATION = ["low", "considered", "high"];
8569
8585
  var PAGE_TYPES = ["home", "service", "about", "pricing", "booking", "contact", "article", "landing"];
8586
+ var PROOF_KINDS = ["work", "people", "credentials", "reviews", "experience", "outcomes"];
8587
+ var PROOF_DEPTHS = ["first-look", "on-request"];
8588
+ var VIEWPORTS = ["mobile", "tablet", "desktop"];
8570
8589
  var HIERARCHY_FIELDS = ["primaryAction", "order", "lead", "journey"];
8590
+ var CONFIDENCE_LENSES = ["relevance", "hierarchy", "evidence", "identity", "action", "disclosure", "personality", "differentiation", "mobile"];
8591
+ var REVIEW_BASES = ["observed", "inferred", "preference", "unknown"];
8592
+ var INSPECTED_KINDS = ["screenshot", "rendered", "source", "measurement", "content"];
8593
+ var REPORTED_KINDS = ["audit", "stakeholder", "customer"];
8594
+ var EVIDENCE_KINDS = [...INSPECTED_KINDS, ...REPORTED_KINDS];
8595
+ var CHANGE_CLASSES = ["confirmed-correction", "design-hypothesis", "optional-refinement", "no-change"];
8596
+ var CHANGE_STEPS = ["copy", "relocate", "group", "imagery", "trust-placement", "adjust-component", "remove-redundancy", "restructure", "new-component"];
8597
+ var ADDITIONS = ["carousel", "javascript", "dependency", "animation", "testimonial", "cta", "duplicate-content"];
8571
8598
 
8572
8599
  // src/direction/reason.ts
8573
8600
  var PROPOSED = "PROPOSED:";
@@ -8863,6 +8890,429 @@ function checkOrder(declared, page, snapshot, strict = false) {
8863
8890
  return [{ severity: strict ? "error" : "warn", at: `hierarchy.${page}.order`, message: `the page runs ${rendered.join(", ")}; the file says ${said.join(", ")}` }];
8864
8891
  }
8865
8892
 
8893
+ // src/direction/review.ts
8894
+ var isObject = (x) => Boolean(x) && typeof x === "object" && !Array.isArray(x);
8895
+ var wordCount = (x) => typeof x === "string" ? x.trim().split(/\s+/).filter(Boolean).length : 0;
8896
+ var includes = (list, x) => list.includes(x);
8897
+ var STEP_RANK = new Map(CHANGE_STEPS.map((s, i) => [s, i]));
8898
+ var CLASS_RANK = { "no-change": 0, "optional-refinement": 1, "design-hypothesis": 2, "confirmed-correction": 3 };
8899
+ var RENDERED_KINDS = ["screenshot", "rendered", "measurement"];
8900
+ var isStructural = (step) => step === "restructure" || step === "new-component";
8901
+ var GALLERY = /\b(?:gallery|galleries|carousel|slideshow|slider|swipe(?:able)?|rotating)\b/i;
8902
+ function plain2(at, text) {
8903
+ if (wordCount(text) < 8) return [{ severity: "error", at, message: "a sentence at least: say why, in this visitor's terms" }];
8904
+ const t = text;
8905
+ if (PREFERENCE.test(t)) return [{ severity: "error", at, message: "that is a preference, not a reason. Say what it does for this visitor." }];
8906
+ const c = t.match(CONVENTION);
8907
+ 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.` }];
8908
+ return [];
8909
+ }
8910
+ function checkProposalShape(at, p, required) {
8911
+ const problems = [];
8912
+ const err = (rest, message) => problems.push({ severity: "error", at: `${at}${rest}`, message });
8913
+ if (!isObject(p)) {
8914
+ if (required) err("", "a proposal is an object: change, step, and optionally adds and touches");
8915
+ return problems;
8916
+ }
8917
+ if (wordCount(p.change) < 3) err(".change", "say what changes, in a few words");
8918
+ if (!includes(CHANGE_STEPS, p.step)) err(".step", `one of ${CHANGE_STEPS.join(", ")}: the smallest step that does it`);
8919
+ if (p.adds !== void 0) {
8920
+ if (!Array.isArray(p.adds) || p.adds.some((a) => !includes(ADDITIONS, a))) err(".adds", `a list drawn from ${ADDITIONS.join(", ")}`);
8921
+ }
8922
+ if (p.comparisons !== void 0) {
8923
+ if (!Array.isArray(p.comparisons)) err(".comparisons", "a list comparing each relevant existing component");
8924
+ else p.comparisons.forEach((c, i) => {
8925
+ if (!isObject(c) || typeof c.existing !== "string" || !c.existing.trim() || typeof c.samePurpose !== "boolean") {
8926
+ err(`.comparisons[${i}]`, "existing, samePurpose and because are required");
8927
+ } else problems.push(...plain2(`${at}.comparisons[${i}].because`, c.because));
8928
+ });
8929
+ }
8930
+ if (p.touches !== void 0 && !includes(SECTION_ROLES, p.touches)) err(".touches", `a section role: ${SECTION_ROLES.join(", ")}`);
8931
+ return problems;
8932
+ }
8933
+ function checkEvidenceShape(at, e) {
8934
+ const problems = [];
8935
+ const err = (rest, message) => problems.push({ severity: "error", at: `${at}${rest}`, message });
8936
+ if (!isObject(e)) return [{ severity: "error", at, message: "evidence is an object: kind, ref, and a viewport for anything rendered" }];
8937
+ if (!includes(EVIDENCE_KINDS, e.kind)) err(".kind", `one of ${EVIDENCE_KINDS.join(", ")}`);
8938
+ if (typeof e.ref !== "string" || !e.ref.trim()) err(".ref", "where it can be found: a screenshot path, a selector, a quotation, a report");
8939
+ if (e.viewport !== void 0 && !includes(VIEWPORTS, e.viewport)) err(".viewport", `one of ${VIEWPORTS.join(", ")}`);
8940
+ 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");
8941
+ return problems;
8942
+ }
8943
+ function checkNoteShape(i, n) {
8944
+ const base = `notes[${i}]`;
8945
+ const problems = [];
8946
+ const err = (rest, message) => problems.push({ severity: "error", at: `${base}${rest}`, message });
8947
+ if (typeof n.id !== "string" || !n.id.trim()) err(".id", "a short name for this note");
8948
+ if (n.page !== void 0 && !includes(PAGE_TYPES, n.page)) err(".page", `one of ${PAGE_TYPES.join(", ")}`);
8949
+ if (!includes(CONFIDENCE_LENSES, n.lens)) err(".lens", `one of ${CONFIDENCE_LENSES.join(", ")}`);
8950
+ if (wordCount(n.observation) < 3) err(".observation", "what was said or seen, as it was said");
8951
+ if (typeof n.raisedBy !== "string" || !n.raisedBy.trim()) err(".raisedBy", "who said it, or what found it");
8952
+ if (!includes(REVIEW_BASES, n.basis)) err(".basis", `one of ${REVIEW_BASES.join(", ")}`);
8953
+ if (!Array.isArray(n.evidence)) err(".evidence", "a list, possibly empty: what supports the note");
8954
+ else n.evidence.forEach((e, j) => problems.push(...checkEvidenceShape(`${base}.evidence[${j}]`, e)));
8955
+ if (n.appliesTo !== void 0 && (!Array.isArray(n.appliesTo) || n.appliesTo.some((v) => !includes(VIEWPORTS, v)))) err(".appliesTo", `a list drawn from ${VIEWPORTS.join(", ")}`);
8956
+ 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");
8957
+ if (n.suggested !== void 0) problems.push(...checkProposalShape(`${base}.suggested`, n.suggested, false));
8958
+ const r = n.recommend;
8959
+ if (!isObject(r)) {
8960
+ err(".recommend", "the recommendation: a class, and for anything but no-change a change, a step and a reason");
8961
+ return problems;
8962
+ }
8963
+ if (!includes(CHANGE_CLASSES, r.class)) {
8964
+ err(".recommend.class", `one of ${CHANGE_CLASSES.join(", ")}. no-change is a result, not a failure to answer.`);
8965
+ return problems;
8966
+ }
8967
+ if (r.class === "no-change") {
8968
+ if (r.because !== void 0) problems.push(...plain2(`${base}.recommend.because`, r.because));
8969
+ return problems;
8970
+ }
8971
+ problems.push(...checkProposalShape(`${base}.recommend`, r, true));
8972
+ problems.push(...plain2(`${base}.recommend.because`, r.because));
8973
+ const bigger = isStructural(r.step) || Array.isArray(r.adds) && r.adds.length > 0;
8974
+ if (bigger) {
8975
+ const smaller = Array.isArray(r.smaller) ? r.smaller : [];
8976
+ 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");
8977
+ const limit = STEP_RANK.get(r.step) ?? 0;
8978
+ smaller.forEach((s, j) => {
8979
+ const ok = isObject(s) && includes(CHANGE_STEPS, s.step) && (limit === 0 || (STEP_RANK.get(s.step) ?? 99) < limit);
8980
+ if (!ok) err(`.recommend.smaller[${j}].step`, "a step on the ladder below the one recommended");
8981
+ if (isObject(s)) problems.push(...plain2(`${base}.recommend.smaller[${j}].whyNot`, s.whyNot));
8982
+ });
8983
+ } else {
8984
+ problems.push(...plain2(`${base}.recommend.whyEnough`, r.whyEnough));
8985
+ }
8986
+ 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");
8987
+ if (r.requires !== void 0) {
8988
+ 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");
8989
+ }
8990
+ return problems;
8991
+ }
8992
+ function whyNot(p, ctx) {
8993
+ const reasons = [];
8994
+ const adds = Array.isArray(p.adds) ? p.adds : [];
8995
+ if (adds.includes("carousel")) {
8996
+ for (const have of new Set(ctx.existing.filter((e) => GALLERY.test(e)))) {
8997
+ const compared = p.comparisons?.filter((c) => c.existing === have) ?? [];
8998
+ 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.`);
8999
+ else if (compared[0].samePurpose) reasons.push(`the comparison identifies the same purpose as "${have}": ${compared[0].because}. Reuse or adjust that component.`);
9000
+ }
9001
+ }
9002
+ const defensible = (ctx.basis === "observed" || ctx.basis === "inferred") && ctx.impact;
9003
+ if ((isStructural(p.step) || adds.length > 0) && !defensible) {
9004
+ 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.");
9005
+ }
9006
+ if (p.step === "remove-redundancy" && !(ctx.basis === "observed" && ctx.impact)) {
9007
+ 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.");
9008
+ }
9009
+ if (p.touches && ctx.placed.has(p.touches) && ctx.basis !== "observed") {
9010
+ reasons.push(`the declared order already places "${p.touches}" with a reason. Only something observed overrides a reasoned decision.`);
9011
+ }
9012
+ return reasons;
9013
+ }
9014
+ function widthsOf(evidence) {
9015
+ const seen = /* @__PURE__ */ new Set();
9016
+ for (const e of evidence) {
9017
+ if (e.viewport) seen.add(e.viewport);
9018
+ else if (e.kind === "content") VIEWPORTS.forEach((v) => seen.add(v));
9019
+ }
9020
+ return VIEWPORTS.filter((v) => seen.has(v));
9021
+ }
9022
+ function assessReview(input, direction) {
9023
+ const problems = [];
9024
+ const results = [];
9025
+ const fail = (at, message) => problems.push({ severity: "error", at, message });
9026
+ const report2 = () => {
9027
+ const n = (o) => results.filter((r) => r.outcome === o).length;
9028
+ const summary = { change: n("change"), hold: n("hold"), noChange: n("no-change"), gatherEvidence: n("gather-evidence"), verdict: "" };
9029
+ 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.`;
9030
+ return { valid: !problems.some((p) => p.severity === "error"), problems, results, summary };
9031
+ };
9032
+ if (!isObject(input)) {
9033
+ fail("", "a review is an object: version, subject and notes");
9034
+ return report2();
9035
+ }
9036
+ if (input.context !== void 0 && !["publication", "protected-staging"].includes(input.context)) fail("context", "publication or protected-staging");
9037
+ if (input.version !== 1) fail("version", "version is 1");
9038
+ if (typeof input.subject !== "string" || !input.subject.trim()) fail("subject", "who or what was reviewed");
9039
+ if (!Array.isArray(input.notes)) {
9040
+ fail("notes", "a list of notes");
9041
+ return report2();
9042
+ }
9043
+ const job = direction.job;
9044
+ if (!job) {
9045
+ 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).");
9046
+ return report2();
9047
+ }
9048
+ const ids = /* @__PURE__ */ new Set();
9049
+ input.notes.forEach((raw, i) => {
9050
+ if (!isObject(raw)) return fail(`notes[${i}]`, "a note is an object");
9051
+ const shape = checkNoteShape(i, raw);
9052
+ if (typeof raw.id === "string") {
9053
+ if (ids.has(raw.id)) shape.push({ severity: "error", at: `notes[${i}].id`, message: `"${raw.id}" is used twice` });
9054
+ ids.add(raw.id);
9055
+ }
9056
+ problems.push(...shape);
9057
+ if (shape.length > 0) return;
9058
+ const note = raw;
9059
+ const page = note.page ?? "home";
9060
+ const adjustments = [];
9061
+ const hierarchy = direction.hierarchy?.[page];
9062
+ const sources = Array.isArray(direction.sources) ? direction.sources : [];
9063
+ const inspected = note.evidence.filter((e) => includes(INSPECTED_KINDS, e.kind));
9064
+ let basis = note.basis;
9065
+ if (basis === "observed" && inspected.length === 0) {
9066
+ basis = note.evidence.length > 0 ? "inferred" : "unknown";
9067
+ adjustments.push(
9068
+ 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."
9069
+ );
9070
+ } else if (basis === "inferred" && note.evidence.length === 0) {
9071
+ basis = "unknown";
9072
+ adjustments.push("declared inferred, but nothing supports it. Read as unknown.");
9073
+ }
9074
+ const impactProblems = typeof note.impact === "string" ? checkReason({ at: `notes[${i}].impact`, because: note.impact, sources, tie: "job", job }) : [];
9075
+ const impact = typeof note.impact === "string" && impactProblems.length === 0;
9076
+ const wantsChange = note.recommend.class !== "no-change";
9077
+ if (!impact && (wantsChange || note.suggested)) {
9078
+ adjustments.push(
9079
+ 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."
9080
+ );
9081
+ if (basis === "inferred") basis = "preference";
9082
+ }
9083
+ const ctx = {
9084
+ note,
9085
+ basis,
9086
+ impact,
9087
+ existing: [...note.existing ?? [], ...(hierarchy?.confidence?.proof ?? []).map((p) => p.what).filter((w) => typeof w === "string")],
9088
+ placed: new Set((hierarchy?.order ?? []).map((o) => o.role))
9089
+ };
9090
+ const examined = widthsOf(note.evidence);
9091
+ const wanted = note.appliesTo ?? (examined.length > 0 ? examined : [...VIEWPORTS]);
9092
+ const claimed = wanted.filter((v) => examined.includes(v));
9093
+ const unknown = VIEWPORTS.filter((v) => wanted.includes(v) && !examined.includes(v));
9094
+ 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.`);
9095
+ const rec = note.recommend;
9096
+ const recReasons = wantsChange ? whyNot(rec, ctx) : [];
9097
+ const sugReasons = note.suggested ? whyNot(note.suggested, ctx) : [];
9098
+ const ceiling = basis === "observed" ? impact ? "confirmed-correction" : "optional-refinement" : basis === "inferred" ? "design-hypothesis" : basis === "preference" ? "optional-refinement" : "no-change";
9099
+ let effective = wantsChange ? rec.class : "no-change";
9100
+ if (basis !== "unknown" && CLASS_RANK[effective] > CLASS_RANK[ceiling]) {
9101
+ adjustments.push(`declared ${rec.class}, but a problem that is ${basis}${basis === "observed" && !impact ? " with no stated visitor impact" : ""} supports at most ${ceiling}.`);
9102
+ effective = ceiling;
9103
+ }
9104
+ let withdrawn;
9105
+ if (wantsChange && recReasons.length > 0) {
9106
+ withdrawn = recReasons;
9107
+ effective = "no-change";
9108
+ adjustments.push("the recommended change is withdrawn: " + recReasons.join(" "));
9109
+ }
9110
+ const missing = (rec.requires ?? []).filter((q) => !q.source || !sources.some((s) => s.id === q.source));
9111
+ const outstanding = missing.map((q) => q.what);
9112
+ const blocking = missing.filter((q) => !(input.context === "protected-staging" && q.kind === "asset" && q.phase === "publication" && q.placeholder));
9113
+ 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.");
9114
+ let outcome;
9115
+ if (basis === "unknown") {
9116
+ outcome = "gather-evidence";
9117
+ effective = "no-change";
9118
+ if (wantsChange) adjustments.push(`nothing is recommended until the problem is established${rec.validation ? `: ${rec.validation}` : ". Ask the client, or look, first."}`);
9119
+ } else if (effective === "no-change") outcome = "no-change";
9120
+ else if (blocking.length > 0) outcome = "hold";
9121
+ else outcome = "change";
9122
+ 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.`);
9123
+ let suggested;
9124
+ if (note.suggested) {
9125
+ const recRank = STEP_RANK.get(rec.step ?? "") ?? -1;
9126
+ const sugRank = STEP_RANK.get(note.suggested.step) ?? 0;
9127
+ const sugAdds = (note.suggested.adds ?? []).length;
9128
+ let standing;
9129
+ const reasons = [...sugReasons];
9130
+ if (sugReasons.length > 0) standing = "not-supported";
9131
+ else if (effective === "no-change") {
9132
+ standing = "not-needed";
9133
+ reasons.push("the existing design already answers the problem, or nothing is yet established to answer.");
9134
+ } else if (sugRank > recRank || sugAdds > 0 && (rec.adds ?? []).length === 0) {
9135
+ standing = "comparison-needed";
9136
+ 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.");
9137
+ } else standing = "supported";
9138
+ suggested = { standing, reasons };
9139
+ }
9140
+ results.push({
9141
+ id: note.id,
9142
+ page,
9143
+ lens: note.lens,
9144
+ basis: { declared: note.basis, effective: basis },
9145
+ ...suggested ? { suggested } : {},
9146
+ recommend: { declared: rec.class, effective, ...wantsChange ? { step: rec.step } : {}, ...withdrawn ? { withdrawn } : {} },
9147
+ outcome,
9148
+ widths: { examined, claimed, unknown },
9149
+ adjustments,
9150
+ outstanding,
9151
+ publicationReady: outstanding.length === 0
9152
+ });
9153
+ });
9154
+ return report2();
9155
+ }
9156
+
9157
+ // src/direction/confidence.ts
9158
+ function viewportOf(width) {
9159
+ return width < 768 ? "mobile" : width < 1024 ? "tablet" : "desktop";
9160
+ }
9161
+ var isObject2 = (x) => Boolean(x) && typeof x === "object" && !Array.isArray(x);
9162
+ function validateConfidence(page, confidence, job, sources) {
9163
+ const problems = [];
9164
+ const base = `hierarchy.${page}.confidence`;
9165
+ const err = (at, message) => problems.push({ severity: "error", at: `${base}${at}`, message });
9166
+ if (!isObject2(confidence)) {
9167
+ err("", "confidence is an object: `examined` (the widths looked at) and `proof` (what the page offers)");
9168
+ return problems;
9169
+ }
9170
+ if (confidence.examined !== void 0) {
9171
+ if (!Array.isArray(confidence.examined)) err(".examined", `a list of widths: ${VIEWPORTS.join(", ")}`);
9172
+ 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(", ")}`);
9173
+ }
9174
+ if (confidence.proof !== void 0 && !Array.isArray(confidence.proof)) err(".proof", "a list of evidence items");
9175
+ const proof = Array.isArray(confidence.proof) ? confidence.proof : [];
9176
+ proof.forEach((raw, i) => {
9177
+ const at = `.proof[${i}]`;
9178
+ if (!isObject2(raw)) return err(at, "an evidence item is an object: kind, what, position, depth, because");
9179
+ if (!PROOF_KINDS.includes(raw.kind)) err(`${at}.kind`, `one of ${PROOF_KINDS.join(", ")}`);
9180
+ 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"');
9181
+ if (typeof raw.position !== "string" || !POSITIONS.has(raw.position)) err(`${at}.position`, 'the section it sits in, or "header" or "sticky"');
9182
+ if (!PROOF_DEPTHS.includes(raw.depth)) err(`${at}.depth`, `${PROOF_DEPTHS.join(" or ")}: met without being sought, or detail a visitor opens`);
9183
+ problems.push(...checkReason({ at: `${base}${at}`, because: raw.because, evidence: raw.evidence, sources, tie: "job", job }));
9184
+ });
9185
+ return problems;
9186
+ }
9187
+ 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;
9188
+ var LATE_SECTION = 3;
9189
+ 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(" ");
9190
+ var lensOf = (kind) => kind === "people" ? "identity" : "evidence";
9191
+ function decisionText(h) {
9192
+ const proof = Array.isArray(h.confidence?.proof) ? h.confidence.proof : [];
9193
+ return [
9194
+ h.primaryAction?.value,
9195
+ h.primaryAction?.because,
9196
+ h.lead?.value,
9197
+ h.lead?.because,
9198
+ ...Array.isArray(h.order) ? h.order.map((o) => o?.because) : [],
9199
+ ...Array.isArray(h.journey) ? h.journey.map((s) => s?.because) : [],
9200
+ ...proof.flatMap((p) => isObject2(p) ? [p.what, p.because] : [])
9201
+ ].filter((x) => typeof x === "string").join(" ");
9202
+ }
9203
+ function assessConfidence(hierarchy, job, ctx = {}) {
9204
+ const findings = [];
9205
+ if (!isObject2(hierarchy)) return findings;
9206
+ for (const [key, raw] of Object.entries(hierarchy)) {
9207
+ const h = raw;
9208
+ if (!PAGE_TYPES.includes(key) || !isObject2(h) || !isObject2(h.confidence)) continue;
9209
+ const page = key;
9210
+ const at = (rest) => `hierarchy.${page}.confidence${rest}`;
9211
+ const add = (f4) => findings.push({ page, ...f4 });
9212
+ const c = h.confidence;
9213
+ const proof = (Array.isArray(c.proof) ? c.proof : []).filter(isObject2);
9214
+ const order = new Set((Array.isArray(h.order) ? h.order : []).map((o) => o?.role));
9215
+ const bands = new Set((Array.isArray(c.examined) ? c.examined : []).filter((v) => VIEWPORTS.includes(v)));
9216
+ if (ctx.snapshot && (ctx.snapshotPage ?? "home") === page) bands.add(viewportOf(ctx.snapshot.viewport.width));
9217
+ for (const pos of Array.isArray(h.primaryAction?.positions) ? h.primaryAction.positions : []) {
9218
+ if (POSITIONS.has(pos) && pos !== "header" && pos !== "sticky" && !order.has(pos)) {
9219
+ add({
9220
+ lens: "action",
9221
+ basis: "observed",
9222
+ at: `hierarchy.${page}.primaryAction.positions`,
9223
+ message: `the ask "${h.primaryAction.value}" sits in "${pos}", and the declared order has no such section`,
9224
+ why: "A visitor can only take the next step where the page offers it. Either the order or the position is out of date."
9225
+ });
9226
+ }
9227
+ }
9228
+ proof.forEach((p, i) => {
9229
+ const where = at(`.proof[${i}]`);
9230
+ if (POSITIONS.has(p.position) && p.position !== "header" && p.position !== "sticky" && !order.has(p.position)) {
9231
+ add({
9232
+ lens: "hierarchy",
9233
+ basis: "observed",
9234
+ at: `${where}.position`,
9235
+ message: `${p.kind} evidence "${p.what}" sits in "${p.position}", and the declared order has no such section`,
9236
+ why: "Evidence a visitor is meant to meet has to be on the page, in a section the order names."
9237
+ });
9238
+ }
9239
+ const cited = Array.isArray(p.evidence) ? p.evidence : [];
9240
+ if (cited.length === 0) {
9241
+ add({
9242
+ lens: lensOf(p.kind),
9243
+ basis: "unknown",
9244
+ at: `${where}.evidence`,
9245
+ message: `no source in the file shows that "${p.what}" exists`,
9246
+ 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."
9247
+ });
9248
+ }
9249
+ });
9250
+ const firstLook = new Set(proof.filter((p) => p.depth === "first-look").map((p) => p.kind));
9251
+ proof.forEach((p, i) => {
9252
+ if (p.depth === "on-request" && !firstLook.has(p.kind)) {
9253
+ add({
9254
+ lens: "disclosure",
9255
+ basis: "inferred",
9256
+ at: at(`.proof[${i}].depth`),
9257
+ message: `${p.kind} evidence "${p.what}" is held back for visitors who seek it, and nothing of the same kind is met first`,
9258
+ 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."
9259
+ });
9260
+ }
9261
+ });
9262
+ if (job && IDENTITY_WORRY.test(identityText(job))) {
9263
+ const sections = Array.isArray(h.order) ? h.order.map((o) => o?.role) : [];
9264
+ const before = (pos) => pos === "header" || pos === "sticky" ? 0 : sections.indexOf(pos);
9265
+ const met = proof.filter((p) => p.kind === "people" && before(p.position) >= 0).map((p) => ({ position: p.position, before: before(p.position) }));
9266
+ const earliest = met.length > 0 ? met.reduce((a, b) => b.before < a.before ? b : a) : void 0;
9267
+ if (earliest && earliest.before >= LATE_SECTION) {
9268
+ add({
9269
+ lens: "identity",
9270
+ basis: "inferred",
9271
+ at: at(".proof"),
9272
+ 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`,
9273
+ 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."}`
9274
+ });
9275
+ }
9276
+ }
9277
+ if (job && Array.isArray(job.objections)) {
9278
+ const said = contentWords(decisionText(h));
9279
+ job.objections.forEach((o, i) => {
9280
+ if (typeof o !== "string") return;
9281
+ if (shares(contentWords(o), said).length === 0) {
9282
+ add({
9283
+ lens: "relevance",
9284
+ basis: "unknown",
9285
+ at: `job.objections[${i}]`,
9286
+ message: `no decision recorded for this page names the worry "${o}"`,
9287
+ 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."
9288
+ });
9289
+ }
9290
+ });
9291
+ }
9292
+ if (bands.size === 0) {
9293
+ add({
9294
+ lens: "mobile",
9295
+ basis: "unknown",
9296
+ at: at(".examined"),
9297
+ message: "no width recorded: say which widths these decisions were examined at",
9298
+ 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."
9299
+ });
9300
+ } else {
9301
+ for (const band of VIEWPORTS) {
9302
+ if (bands.has(band)) continue;
9303
+ add({
9304
+ lens: "mobile",
9305
+ basis: "unknown",
9306
+ at: at(".examined"),
9307
+ 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`,
9308
+ 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."
9309
+ });
9310
+ }
9311
+ }
9312
+ }
9313
+ return findings;
9314
+ }
9315
+
8866
9316
  // src/direction/research.ts
8867
9317
  function tradeOf(brief) {
8868
9318
  const text = brief.toLowerCase();
@@ -8893,14 +9343,17 @@ function researchPrompt(input) {
8893
9343
  "4. **Their words.** The phrases they actually use for the problem and for what they want. Quote them exactly.",
8894
9344
  "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?",
8895
9345
  "6. **Consideration.** Do they decide in minutes, days or months? What do they compare before deciding?",
8896
- "7. **Where they look.** What do they search for, and where do they ask?",
9346
+ "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?",
9347
+ "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.",
9348
+ "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?",
8897
9349
  "",
8898
9350
  "## Do not",
8899
9351
  "",
8900
9352
  `- Do not describe, list, rank or summarise what ${category} websites look like, or how they are laid out.`,
8901
9353
  '- Do not suggest colours, fonts, layouts, sections or visual "inspiration" from this category.',
8902
9354
  "- Do not summarise competitors' marketing. Their claims are the category average; the customer's words are the point.",
8903
- "- Do not invent quotes. If you cannot find a source for something, say so."
9355
+ "- Do not invent quotes. If you cannot find a source for something, say so.",
9356
+ "- 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."
8904
9357
  ];
8905
9358
  if (input.visual) {
8906
9359
  lines.push(
@@ -9231,6 +9684,7 @@ function validateDirection(input, ctx = {}) {
9231
9684
  }
9232
9685
  let job = false;
9233
9686
  let hierarchy = [];
9687
+ let confidence;
9234
9688
  if (d.version === 2) {
9235
9689
  const j = validateJob(d.job, sources, { brief: typeof d.brief === "string" ? d.brief : "", readSource: ctx.readSource });
9236
9690
  problems.push(...j.problems);
@@ -9238,6 +9692,10 @@ function validateDirection(input, ctx = {}) {
9238
9692
  const h = validateHierarchy(d.hierarchy, d.job, sources);
9239
9693
  problems.push(...h.problems);
9240
9694
  hierarchy = h.pages;
9695
+ const pages = d.hierarchy && typeof d.hierarchy === "object" && !Array.isArray(d.hierarchy) ? d.hierarchy : {};
9696
+ const recorded = Object.entries(pages).filter(([k, p]) => PAGE_TYPES.includes(k) && p && typeof p === "object" && p.confidence !== void 0);
9697
+ for (const [page, p] of recorded) problems.push(...validateConfidence(page, p.confidence, d.job, sources));
9698
+ if (recorded.length) confidence = assessConfidence(d.hierarchy, d.job, { snapshot: ctx.snapshot, snapshotPage: ctx.page });
9241
9699
  if (ctx.snapshot) problems.push(...checkOrder(d.hierarchy?.[ctx.page ?? "home"], ctx.page ?? "home", ctx.snapshot, ctx.strict));
9242
9700
  }
9243
9701
  if (ctx.estate?.length) {
@@ -9254,6 +9712,7 @@ function validateDirection(input, ctx = {}) {
9254
9712
  valid: problems.every((p) => p.severity !== "error"),
9255
9713
  problems,
9256
9714
  decided,
9715
+ ...confidence ? { confidence } : {},
9257
9716
  layers: { job, hierarchy, tokens: { decided, total: CHOICE_KEYS.length }, complete: complete && problems.every((p) => p.severity !== "error") }
9258
9717
  };
9259
9718
  }
@@ -9812,6 +10271,7 @@ function retrofitPlan(report2, direction) {
9812
10271
  0 && (module.exports = {
9813
10272
  ACCENT_SCALE,
9814
10273
  ACCENT_SHARED,
10274
+ ADDITIONS,
9815
10275
  AI_PHRASES,
9816
10276
  AI_SOURCE_TYPES,
9817
10277
  AI_VIOLET,
@@ -9820,8 +10280,11 @@ function retrofitPlan(report2, direction) {
9820
10280
  BUZZWORDS,
9821
10281
  CATALOGUE,
9822
10282
  CATALOGUE_VERSION,
10283
+ CHANGE_CLASSES,
10284
+ CHANGE_STEPS,
9823
10285
  CHOICE_KEYS,
9824
10286
  COMPONENT_ROLES,
10287
+ CONFIDENCE_LENSES,
9825
10288
  CONSIDERATION,
9826
10289
  CONTENT_FEATURES,
9827
10290
  CONVENTION,
@@ -9838,6 +10301,7 @@ function retrofitPlan(report2, direction) {
9838
10301
  EASE_TUPLE,
9839
10302
  EDGE_STEP,
9840
10303
  ESTATE_VERSION,
10304
+ EVIDENCE_KINDS,
9841
10305
  EXIT_RATIO,
9842
10306
  FINGERPRINT_VERSION,
9843
10307
  FLAGGED_TARGET,
@@ -9853,6 +10317,7 @@ function retrofitPlan(report2, direction) {
9853
10317
  HOUSE_TYPE,
9854
10318
  HUMAN_TYPICAL_TARGET,
9855
10319
  IMAGERY_TELLS,
10320
+ INSPECTED_KINDS,
9856
10321
  JSON_SCHEMA_VERSION,
9857
10322
  LABELS,
9858
10323
  LEXICAL_FEATURES,
@@ -9873,6 +10338,8 @@ function retrofitPlan(report2, direction) {
9873
10338
  PROFILE_MEASURED,
9874
10339
  PROFILE_PARAGRAPHS,
9875
10340
  PROMO_BADGE,
10341
+ PROOF_DEPTHS,
10342
+ PROOF_KINDS,
9876
10343
  PROOF_NAMES,
9877
10344
  PROPOSED,
9878
10345
  PROVENANCE_SCAN_BYTES,
@@ -9886,7 +10353,9 @@ function retrofitPlan(report2, direction) {
9886
10353
  REGISTER_IDS,
9887
10354
  RENDERED_PATHS,
9888
10355
  RENDERED_TELLS,
10356
+ REPORTED_KINDS,
9889
10357
  REVEAL_LIMIT,
10358
+ REVIEW_BASES,
9890
10359
  REVIEW_PHRASES,
9891
10360
  SAMPLES_PER_SEGMENT,
9892
10361
  SCALE,
@@ -9915,12 +10384,15 @@ function retrofitPlan(report2, direction) {
9915
10384
  TYPE_STEPS,
9916
10385
  TYPICAL_AT,
9917
10386
  VAGUE_WORDS,
10387
+ VIEWPORTS,
9918
10388
  accentFork,
9919
10389
  addSite,
9920
10390
  apcaContrast,
9921
10391
  applyBaseline,
9922
10392
  applyHouseGate,
9923
10393
  applySeverity,
10394
+ assessConfidence,
10395
+ assessReview,
9924
10396
  auditSnapshot,
9925
10397
  bandOf,
9926
10398
  briefTerms,
@@ -10008,6 +10480,7 @@ function retrofitPlan(report2, direction) {
10008
10480
  isCategoryLabel,
10009
10481
  isCream,
10010
10482
  isFlagged,
10483
+ isStructural,
10011
10484
  jobWords,
10012
10485
  layoutDistance,
10013
10486
  lexicalOf,
@@ -10078,9 +10551,11 @@ function retrofitPlan(report2, direction) {
10078
10551
  typeFeatureTokens,
10079
10552
  typicality,
10080
10553
  unprovenClaims,
10554
+ validateConfidence,
10081
10555
  validateDirection,
10082
10556
  validateHierarchy,
10083
10557
  validateJob,
10558
+ viewportOf,
10084
10559
  visibleText,
10085
10560
  visualMeasures,
10086
10561
  vocabulary,