@dreamdata_io/verify-tracking 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +756 -0
  3. package/dist/ai/checker.js +222 -0
  4. package/dist/ai/checker.js.map +1 -0
  5. package/dist/ai/checklist.js +190 -0
  6. package/dist/ai/checklist.js.map +1 -0
  7. package/dist/ai/cli-presets.js +27 -0
  8. package/dist/ai/cli-presets.js.map +1 -0
  9. package/dist/ai/evidence-view.js +143 -0
  10. package/dist/ai/evidence-view.js.map +1 -0
  11. package/dist/ai/local-cli.js +58 -0
  12. package/dist/ai/local-cli.js.map +1 -0
  13. package/dist/ai/openai-compatible.js +78 -0
  14. package/dist/ai/openai-compatible.js.map +1 -0
  15. package/dist/ai/response-parser.js +105 -0
  16. package/dist/ai/response-parser.js.map +1 -0
  17. package/dist/ai/types.js +2 -0
  18. package/dist/ai/types.js.map +1 -0
  19. package/dist/analysis/consent-mode.js +225 -0
  20. package/dist/analysis/consent-mode.js.map +1 -0
  21. package/dist/analysis/deployment.js +2658 -0
  22. package/dist/analysis/deployment.js.map +1 -0
  23. package/dist/analysis/diagnosis/copy.en.js +303 -0
  24. package/dist/analysis/diagnosis/copy.en.js.map +1 -0
  25. package/dist/analysis/diagnosis/diagnose.js +172 -0
  26. package/dist/analysis/diagnosis/diagnose.js.map +1 -0
  27. package/dist/analysis/diagnosis/docs.js +118 -0
  28. package/dist/analysis/diagnosis/docs.js.map +1 -0
  29. package/dist/analysis/diagnosis/index.js +5 -0
  30. package/dist/analysis/diagnosis/index.js.map +1 -0
  31. package/dist/analysis/diagnosis/rules.js +656 -0
  32. package/dist/analysis/diagnosis/rules.js.map +1 -0
  33. package/dist/analysis/diagnosis/types.js +2 -0
  34. package/dist/analysis/diagnosis/types.js.map +1 -0
  35. package/dist/cli.js +344 -0
  36. package/dist/cli.js.map +1 -0
  37. package/dist/config/env.js +100 -0
  38. package/dist/config/env.js.map +1 -0
  39. package/dist/config/preflight.js +167 -0
  40. package/dist/config/preflight.js.map +1 -0
  41. package/dist/config/user-config.js +79 -0
  42. package/dist/config/user-config.js.map +1 -0
  43. package/dist/evidence/collect.js +527 -0
  44. package/dist/evidence/collect.js.map +1 -0
  45. package/dist/evidence/consent-accept.js +377 -0
  46. package/dist/evidence/consent-accept.js.map +1 -0
  47. package/dist/evidence/consent.js +340 -0
  48. package/dist/evidence/consent.js.map +1 -0
  49. package/dist/evidence/constants.js +615 -0
  50. package/dist/evidence/constants.js.map +1 -0
  51. package/dist/evidence/globals.js +132 -0
  52. package/dist/evidence/globals.js.map +1 -0
  53. package/dist/evidence/gtm-container.js +432 -0
  54. package/dist/evidence/gtm-container.js.map +1 -0
  55. package/dist/evidence/heuristic-banner.js +107 -0
  56. package/dist/evidence/heuristic-banner.js.map +1 -0
  57. package/dist/evidence/http-probe.js +139 -0
  58. package/dist/evidence/http-probe.js.map +1 -0
  59. package/dist/evidence/init-script.js +112 -0
  60. package/dist/evidence/init-script.js.map +1 -0
  61. package/dist/evidence/network.js +365 -0
  62. package/dist/evidence/network.js.map +1 -0
  63. package/dist/evidence/preference-center.js +368 -0
  64. package/dist/evidence/preference-center.js.map +1 -0
  65. package/dist/evidence/script.js +425 -0
  66. package/dist/evidence/script.js.map +1 -0
  67. package/dist/evidence/second-page.js +332 -0
  68. package/dist/evidence/second-page.js.map +1 -0
  69. package/dist/evidence/selector-probe.js +53 -0
  70. package/dist/evidence/selector-probe.js.map +1 -0
  71. package/dist/evidence/stealth.js +21 -0
  72. package/dist/evidence/stealth.js.map +1 -0
  73. package/dist/evidence/types.js +14 -0
  74. package/dist/evidence/types.js.map +1 -0
  75. package/dist/interactive/prompt.js +31 -0
  76. package/dist/interactive/prompt.js.map +1 -0
  77. package/dist/mcp/server.js +308 -0
  78. package/dist/mcp/server.js.map +1 -0
  79. package/dist/mcp/summary.js +41 -0
  80. package/dist/mcp/summary.js.map +1 -0
  81. package/dist/report/context.js +184 -0
  82. package/dist/report/context.js.map +1 -0
  83. package/dist/report/projection.js +125 -0
  84. package/dist/report/projection.js.map +1 -0
  85. package/dist/report/quote-check.js +90 -0
  86. package/dist/report/quote-check.js.map +1 -0
  87. package/dist/report/render.js +178 -0
  88. package/dist/report/render.js.map +1 -0
  89. package/dist/report/serializers.js +124 -0
  90. package/dist/report/serializers.js.map +1 -0
  91. package/dist/report/store.js +139 -0
  92. package/dist/report/store.js.map +1 -0
  93. package/dist/run/verify.js +77 -0
  94. package/dist/run/verify.js.map +1 -0
  95. package/dist/utils/duration.js +10 -0
  96. package/dist/utils/duration.js.map +1 -0
  97. package/dist/utils/errors.js +17 -0
  98. package/dist/utils/errors.js.map +1 -0
  99. package/dist/utils/logger.js +59 -0
  100. package/dist/utils/logger.js.map +1 -0
  101. package/dist/utils/runTimingsLog.js +27 -0
  102. package/dist/utils/runTimingsLog.js.map +1 -0
  103. package/dist/utils/slug.js +27 -0
  104. package/dist/utils/slug.js.map +1 -0
  105. package/dist/version.js +10 -0
  106. package/dist/version.js.map +1 -0
  107. package/package.json +57 -0
  108. package/scripts/install.sh +314 -0
@@ -0,0 +1,143 @@
1
+ import { mentionsDreamdata } from "../evidence/constants.js";
2
+ import { extractMessageId } from "../evidence/network.js";
3
+ function compactPhase(phase) {
4
+ return {
5
+ phase: phase.phase,
6
+ script: {
7
+ present: phase.script.present,
8
+ writeKey: phase.script.writeKey,
9
+ normalSnippet: phase.script.normalSnippet,
10
+ cookielessSnippet: phase.script.cookielessSnippet,
11
+ segmentCompatSnippet: phase.script.segmentCompatSnippet,
12
+ helperScriptOnly: phase.script.helperScriptOnly,
13
+ matches: phase.script.matches,
14
+ normalBundleConfigured: phase.script.normalBundleConfigured,
15
+ normalBundleSignals: phase.script.normalBundleSignals,
16
+ tagManager: phase.script.tagManager,
17
+ },
18
+ network: {
19
+ cookielessRequestCount: phase.network.cookielessRequestCount,
20
+ normalRequestCount: phase.network.normalRequestCount,
21
+ // cookielessRequestSeenFirst is deliberately withheld from the model.
22
+ // Cookieless is the fallback bundle, not the first mover, so the order
23
+ // the two modes fired in carries no verdict — but a field named like
24
+ // that invites one. What matters is whether both fired at all, which
25
+ // the two counts above already say.
26
+ dreamdataRequestCounts: phase.network.dreamdataRequestCounts,
27
+ trackingHosts: phase.network.trackingHosts,
28
+ observedWriteKeys: phase.network.observedWriteKeys,
29
+ // The `name@version` builds the requests named themselves. One install
30
+ // ships every bundle it has at ONE version, so two versions here are
31
+ // two copies of the script running side by side — each firing its own
32
+ // page() on load, with its own messageId, doubling the page view.
33
+ observedLibraries: phase.network.observedLibraries,
34
+ // True when this phase answered the banner and reloaded, so these
35
+ // requests cover TWO loads of the entry URL — unanswered, then decided.
36
+ // Two page events for that URL are the consent upgrade, not a duplicate
37
+ // loader; page-view-counted-once leaves such a page view unjudged,
38
+ // unless the events name two different builds — which two loads of one
39
+ // install cannot produce.
40
+ spansConsentAnswer: phase.network.spansConsentAnswer,
41
+ requests: phase.network.requests.map((r) => ({
42
+ endpointName: r.endpointName,
43
+ method: r.method,
44
+ status: r.status,
45
+ mode: r.mode,
46
+ identity: r.identity,
47
+ url: r.url,
48
+ // The event's own id, not the request's: two requests sharing one
49
+ // means the SAME event was delivered twice, which no single healthy
50
+ // instance does. It is the cheapest way to see a second bundle
51
+ // replaying the buffer the first one already sent.
52
+ messageId: extractMessageId(r.payload),
53
+ })),
54
+ },
55
+ globals: phase.globals,
56
+ thirdPartySegment: phase.thirdPartySegment,
57
+ cookies: phase.cookies,
58
+ consent: {
59
+ ...phase.consent,
60
+ // `raw` is dropped, as it always has been — it is the whole gtag
61
+ // argument object and nothing readable comes of handing it over. The
62
+ // typed fields beside it are what used to be locked inside it: the verb
63
+ // separates the site's configured default from the visitor's decision,
64
+ // and `region` is what explains a log that reads "granted" on a page
65
+ // Google resolved to "denied".
66
+ dataLayerConsentPushes: phase.consent.dataLayerConsentPushes.map((push) => ({
67
+ verb: push.verb,
68
+ analyticsStorage: push.analyticsStorage,
69
+ signals: push.signals,
70
+ region: push.region,
71
+ waitForUpdate: push.waitForUpdate,
72
+ timestamp: push.timestamp,
73
+ })),
74
+ },
75
+ consentAcceptance: phase.consentAcceptance,
76
+ secondPage: phase.secondPage === null
77
+ ? null
78
+ : {
79
+ attempted: phase.secondPage.attempted,
80
+ skippedReason: phase.secondPage.skippedReason,
81
+ toUrl: phase.secondPage.toUrl,
82
+ linkText: phase.secondPage.linkText,
83
+ kind: phase.secondPage.kind,
84
+ urlChanged: phase.secondPage.urlChanged,
85
+ historyEvents: phase.secondPage.historyEvents.map((event) => event.kind),
86
+ network: {
87
+ cookielessRequestCount: phase.secondPage.network.cookielessRequestCount,
88
+ normalRequestCount: phase.secondPage.network.normalRequestCount,
89
+ dreamdataRequestCounts: phase.secondPage.network.dreamdataRequestCounts,
90
+ requests: phase.secondPage.network.requests.map((r) => ({
91
+ endpointName: r.endpointName,
92
+ status: r.status,
93
+ mode: r.mode,
94
+ identity: r.identity,
95
+ })),
96
+ },
97
+ cookies: phase.secondPage.cookies,
98
+ },
99
+ // Same filter buildConsoleCheck applies, for the same reason: a message
100
+ // that names Dreamdata only inside a CSP policy it prints is not about
101
+ // this install, and showing it to the model invites a debugging-signals
102
+ // failure the deterministic layer will not back (idtdna.com).
103
+ console: {
104
+ errors: phase.console.errors.filter(mentionsDreamdata),
105
+ warnings: phase.console.warnings.filter(mentionsDreamdata),
106
+ },
107
+ collectionErrors: phase.collectionErrors,
108
+ };
109
+ }
110
+ /**
111
+ * The one thing the model is asked for beyond the checklist, and only when
112
+ * there is something to ask about.
113
+ *
114
+ * Kept in the USER prompt rather than the system prompt so SYSTEM_PROMPT stays
115
+ * byte-stable — the replay harness accounts for its size, and a prompt that
116
+ * changes shape per run makes that accounting meaningless. Naming the specific
117
+ * checks is what keeps the ask narrow: without them the model reliably
118
+ * volunteers a second opinion on findings that are already decided.
119
+ */
120
+ function unexplainedChecksAsk(analysis) {
121
+ const unexplained = analysis.diagnosis.unexplainedChecks;
122
+ if (unexplained.length === 0)
123
+ return undefined;
124
+ return (`These checks are failing and no catalogued root cause accounts for them: ${unexplained.join(", ")}. ` +
125
+ "Return an \"unexplainedObservation\": ONE plain sentence on what their evidence shows and why the catalogue may not cover it. " +
126
+ "No URL, no link, no documentation path, no proposed fix. Omit the key if you have nothing to add.");
127
+ }
128
+ export function buildPromptPayload(evidence, analysis) {
129
+ return JSON.stringify({
130
+ unexplainedChecksAsk: unexplainedChecksAsk(analysis),
131
+ url: evidence.url,
132
+ timestamp: evidence.timestamp,
133
+ browser: evidence.browser,
134
+ // What the same URL serves a plain HTTP GET, with no browser involved.
135
+ // Executes nothing, so it answers no behavioural question — its whole
136
+ // job is to say whether the browser was shown this site's page at all.
137
+ // deployment.pageReadability is the resolved form of that comparison.
138
+ httpProbe: evidence.httpProbe,
139
+ deployment: analysis,
140
+ phases: evidence.phases.map(compactPhase),
141
+ }, null, 2);
142
+ }
143
+ //# sourceMappingURL=evidence-view.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"evidence-view.js","sourceRoot":"","sources":["../../src/ai/evidence-view.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAG1D,SAAS,YAAY,CAAC,KAAqB;IACzC,OAAO;QACL,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,MAAM,EAAE;YACN,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,OAAO;YAC7B,QAAQ,EAAE,KAAK,CAAC,MAAM,CAAC,QAAQ;YAC/B,aAAa,EAAE,KAAK,CAAC,MAAM,CAAC,aAAa;YACzC,iBAAiB,EAAE,KAAK,CAAC,MAAM,CAAC,iBAAiB;YACjD,oBAAoB,EAAE,KAAK,CAAC,MAAM,CAAC,oBAAoB;YACvD,gBAAgB,EAAE,KAAK,CAAC,MAAM,CAAC,gBAAgB;YAC/C,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,OAAO;YAC7B,sBAAsB,EAAE,KAAK,CAAC,MAAM,CAAC,sBAAsB;YAC3D,mBAAmB,EAAE,KAAK,CAAC,MAAM,CAAC,mBAAmB;YACrD,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,UAAU;SACpC;QACD,OAAO,EAAE;YACP,sBAAsB,EAAE,KAAK,CAAC,OAAO,CAAC,sBAAsB;YAC5D,kBAAkB,EAAE,KAAK,CAAC,OAAO,CAAC,kBAAkB;YACpD,sEAAsE;YACtE,uEAAuE;YACvE,qEAAqE;YACrE,qEAAqE;YACrE,oCAAoC;YACpC,sBAAsB,EAAE,KAAK,CAAC,OAAO,CAAC,sBAAsB;YAC5D,aAAa,EAAE,KAAK,CAAC,OAAO,CAAC,aAAa;YAC1C,iBAAiB,EAAE,KAAK,CAAC,OAAO,CAAC,iBAAiB;YAClD,uEAAuE;YACvE,qEAAqE;YACrE,sEAAsE;YACtE,kEAAkE;YAClE,iBAAiB,EAAE,KAAK,CAAC,OAAO,CAAC,iBAAiB;YAClD,kEAAkE;YAClE,wEAAwE;YACxE,wEAAwE;YACxE,mEAAmE;YACnE,uEAAuE;YACvE,0BAA0B;YAC1B,kBAAkB,EAAE,KAAK,CAAC,OAAO,CAAC,kBAAkB;YACpD,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBAC3C,YAAY,EAAE,CAAC,CAAC,YAAY;gBAC5B,MAAM,EAAE,CAAC,CAAC,MAAM;gBAChB,MAAM,EAAE,CAAC,CAAC,MAAM;gBAChB,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,QAAQ,EAAE,CAAC,CAAC,QAAQ;gBACpB,GAAG,EAAE,CAAC,CAAC,GAAG;gBACV,kEAAkE;gBAClE,oEAAoE;gBACpE,+DAA+D;gBAC/D,mDAAmD;gBACnD,SAAS,EAAE,gBAAgB,CAAC,CAAC,CAAC,OAAO,CAAC;aACvC,CAAC,CAAC;SACJ;QACD,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,iBAAiB,EAAE,KAAK,CAAC,iBAAiB;QAC1C,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,OAAO,EAAE;YACP,GAAG,KAAK,CAAC,OAAO;YAChB,iEAAiE;YACjE,qEAAqE;YACrE,wEAAwE;YACxE,uEAAuE;YACvE,qEAAqE;YACrE,+BAA+B;YAC/B,sBAAsB,EAAE,KAAK,CAAC,OAAO,CAAC,sBAAsB,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;gBAC1E,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,gBAAgB,EAAE,IAAI,CAAC,gBAAgB;gBACvC,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,aAAa,EAAE,IAAI,CAAC,aAAa;gBACjC,SAAS,EAAE,IAAI,CAAC,SAAS;aAC1B,CAAC,CAAC;SACJ;QACD,iBAAiB,EAAE,KAAK,CAAC,iBAAiB;QAC1C,UAAU,EACR,KAAK,CAAC,UAAU,KAAK,IAAI;YACvB,CAAC,CAAC,IAAI;YACN,CAAC,CAAC;gBACE,SAAS,EAAE,KAAK,CAAC,UAAU,CAAC,SAAS;gBACrC,aAAa,EAAE,KAAK,CAAC,UAAU,CAAC,aAAa;gBAC7C,KAAK,EAAE,KAAK,CAAC,UAAU,CAAC,KAAK;gBAC7B,QAAQ,EAAE,KAAK,CAAC,UAAU,CAAC,QAAQ;gBACnC,IAAI,EAAE,KAAK,CAAC,UAAU,CAAC,IAAI;gBAC3B,UAAU,EAAE,KAAK,CAAC,UAAU,CAAC,UAAU;gBACvC,aAAa,EAAE,KAAK,CAAC,UAAU,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC;gBACxE,OAAO,EAAE;oBACP,sBAAsB,EAAE,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,sBAAsB;oBACvE,kBAAkB,EAAE,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,kBAAkB;oBAC/D,sBAAsB,EAAE,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,sBAAsB;oBACvE,QAAQ,EAAE,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;wBACtD,YAAY,EAAE,CAAC,CAAC,YAAY;wBAC5B,MAAM,EAAE,CAAC,CAAC,MAAM;wBAChB,IAAI,EAAE,CAAC,CAAC,IAAI;wBACZ,QAAQ,EAAE,CAAC,CAAC,QAAQ;qBACrB,CAAC,CAAC;iBACJ;gBACD,OAAO,EAAE,KAAK,CAAC,UAAU,CAAC,OAAO;aAClC;QACP,wEAAwE;QACxE,uEAAuE;QACvE,wEAAwE;QACxE,8DAA8D;QAC9D,OAAO,EAAE;YACP,MAAM,EAAE,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,iBAAiB,CAAC;YACtD,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,iBAAiB,CAAC;SAC3D;QACD,gBAAgB,EAAE,KAAK,CAAC,gBAAgB;KACzC,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,oBAAoB,CAAC,QAA4B;IACxD,MAAM,WAAW,GAAG,QAAQ,CAAC,SAAS,CAAC,iBAAiB,CAAC;IACzD,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAC/C,OAAO,CACL,4EAA4E,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;QACtG,gIAAgI;QAChI,mGAAmG,CACpG,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,QAA4B,EAAE,QAA4B;IAC3F,OAAO,IAAI,CAAC,SAAS,CACnB;QACE,oBAAoB,EAAE,oBAAoB,CAAC,QAAQ,CAAC;QACpD,GAAG,EAAE,QAAQ,CAAC,GAAG;QACjB,SAAS,EAAE,QAAQ,CAAC,SAAS;QAC7B,OAAO,EAAE,QAAQ,CAAC,OAAO;QACzB,uEAAuE;QACvE,sEAAsE;QACtE,uEAAuE;QACvE,sEAAsE;QACtE,SAAS,EAAE,QAAQ,CAAC,SAAS;QAC7B,UAAU,EAAE,QAAQ;QACpB,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC;KAC1C,EACD,IAAI,EACJ,CAAC,CACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,58 @@
1
+ import { spawn } from "node:child_process";
2
+ import { AIError } from "../utils/errors.js";
3
+ import { parseCustomCliCmd, resolveCliPresetTemplate } from "./cli-presets.js";
4
+ function resolveTemplate(config) {
5
+ if (config.cliCmd) {
6
+ const parsed = parseCustomCliCmd(config.cliCmd);
7
+ return {
8
+ command: parsed.command,
9
+ args: parsed.args.map((a) => a.replace("{model}", config.model)),
10
+ promptVia: parsed.promptVia,
11
+ };
12
+ }
13
+ if (config.cliPreset) {
14
+ const preset = resolveCliPresetTemplate(config.cliPreset, config.model);
15
+ if (!preset) {
16
+ throw new AIError(`Unknown CLI preset "${config.cliPreset}". Pass --cli-cmd with a custom command template instead.`);
17
+ }
18
+ return preset;
19
+ }
20
+ throw new AIError("--backend local-cli requires either --cli-preset or --cli-cmd.");
21
+ }
22
+ export async function callLocalCli(config, prompt) {
23
+ const template = resolveTemplate(config);
24
+ const args = template.args.map((a) => (a === "{prompt}" ? prompt : a));
25
+ return new Promise((resolve, reject) => {
26
+ const child = spawn(template.command, args, { stdio: ["pipe", "pipe", "pipe"] });
27
+ let stdout = "";
28
+ let stderr = "";
29
+ child.stdout.on("data", (chunk) => (stdout += chunk.toString()));
30
+ child.stderr.on("data", (chunk) => (stderr += chunk.toString()));
31
+ child.on("error", (err) => {
32
+ reject(new AIError(`Failed to run CLI "${template.command}": ${err.message}`, err));
33
+ });
34
+ child.on("close", (code) => {
35
+ if (code !== 0) {
36
+ reject(new AIError(`CLI "${template.command}" exited with code ${code}: ${stderr.slice(0, 500)}`));
37
+ return;
38
+ }
39
+ // An exit code of 0 is not proof of an answer. The Claude CLI reports a
40
+ // model it will not serve on stdout and still exits 0, so the refusal
41
+ // used to travel on as the model's reply and surface as an unparseable
42
+ // verdict inside an otherwise complete report. A reply carrying no JSON
43
+ // object at all is a failure of the CLI, not a judgement, and is worth
44
+ // saying so here where the raw text is still in hand.
45
+ if (!stdout.includes("{")) {
46
+ reject(new AIError(`CLI "${template.command}" exited 0 without returning a verdict. ` +
47
+ `Output: ${(stdout.trim() || stderr.trim() || "(empty)").slice(0, 500)}`));
48
+ return;
49
+ }
50
+ resolve(stdout);
51
+ });
52
+ if (template.promptVia === "stdin") {
53
+ child.stdin.write(prompt);
54
+ }
55
+ child.stdin.end();
56
+ });
57
+ }
58
+ //# sourceMappingURL=local-cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-cli.js","sourceRoot":"","sources":["../../src/ai/local-cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,78 @@
1
+ import OpenAI from "openai";
2
+ import { AIError } from "../utils/errors.js";
3
+ /**
4
+ * Bun's fetch closes a connection that has been idle for 5 minutes
5
+ * (BUN_CONFIG_HTTP_IDLE_TIMEOUT), and the timer only resets on bytes. A
6
+ * non-streaming completion sends none until it is finished, so any model
7
+ * slower than that dies on the socket regardless of what timeout the SDK was
8
+ * given — which is how a 30-minute deadline produced three five-minute
9
+ * attempts. Disabling the idle timer leaves the SDK's own timeout as the one
10
+ * deadline, which is the one the caller set.
11
+ *
12
+ * `timeout` is Bun-specific; Node's fetch ignores the unknown property, so the
13
+ * built CLI behaves the same on either runtime.
14
+ */
15
+ const fetchWithoutIdleTimeout = (input, init) => fetch(input, { ...init, timeout: false });
16
+ /**
17
+ * The SDK's APIConnectionTimeoutError never sets `.name`, so it reads as a
18
+ * plain "Error" — checking the name matched the underlying DOMException a
19
+ * socket abort carries, and missed the SDK's own timeout entirely, which is
20
+ * the one case the message below exists for. The class is the reliable test.
21
+ */
22
+ function isTimeout(err) {
23
+ if (err instanceof OpenAI.APIConnectionTimeoutError)
24
+ return true;
25
+ const causeName = (err?.cause)?.name;
26
+ return causeName === "TimeoutError";
27
+ }
28
+ export async function callOpenAICompatible(config, systemPrompt, userPrompt) {
29
+ const client = new OpenAI({
30
+ baseURL: config.baseUrl,
31
+ apiKey: config.apiKey || "not-required",
32
+ ...(config.timeoutMs === undefined ? {} : { timeout: config.timeoutMs }),
33
+ // One retry, not the SDK's two. A retry earns its keep against a hosted
34
+ // endpoint's 429 or a dropped connection; against a local model every
35
+ // attempt costs another full inference, and retrying a timeout three
36
+ // times turns a 30-minute deadline into a 90-minute one.
37
+ maxRetries: 1,
38
+ fetch: fetchWithoutIdleTimeout,
39
+ });
40
+ try {
41
+ const completion = await client.chat.completions.create({
42
+ model: config.model,
43
+ response_format: { type: "json_object" },
44
+ // Scoring a checklist against fixed evidence is not a task with a
45
+ // creative range: the same evidence should produce the same verdicts.
46
+ // Left unset, Ollama samples at 0.8, and two replays of one archive
47
+ // disagreed on two of seven items — which makes the golden-set
48
+ // comparison unable to tell a model regression from a dice roll. The
49
+ // seed pins what temperature 0 alone does not on every runtime.
50
+ temperature: 0,
51
+ seed: 0,
52
+ messages: [
53
+ { role: "system", content: systemPrompt },
54
+ { role: "user", content: userPrompt },
55
+ ],
56
+ });
57
+ const content = completion.choices[0]?.message?.content;
58
+ if (!content) {
59
+ throw new AIError("Model returned an empty response.");
60
+ }
61
+ return content;
62
+ }
63
+ catch (err) {
64
+ if (err instanceof AIError)
65
+ throw err;
66
+ // A client-side abort is not "the endpoint failed" — the model may well
67
+ // still be generating — and the fix is a number the caller controls, so
68
+ // say that rather than handing back an SDK stack trace.
69
+ if (isTimeout(err)) {
70
+ const waited = config.timeoutMs === undefined ? "the client default" : `${Math.round(config.timeoutMs / 1000)}s`;
71
+ throw new AIError(`${config.model} at ${config.baseUrl} did not answer within ${waited}. A local model that does not fit in ` +
72
+ `RAM spills onto the CPU and can take far longer than this for one run — check \`ollama ps\` for a ` +
73
+ `CPU/GPU split, and either raise --ai-timeout (AI_TIMEOUT_MS) or use a model that fits.`, err);
74
+ }
75
+ throw new AIError(`OpenAI-compatible request to ${config.baseUrl} failed: ${err.message}`, err);
76
+ }
77
+ }
78
+ //# sourceMappingURL=openai-compatible.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai-compatible.js","sourceRoot":"","sources":["../../src/ai/openai-compatible.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAG7C;;;;;;;;;;;GAWG;AACH,MAAM,uBAAuB,GAAG,CAAC,KAA6B,EAAE,IAAkB,EAAqB,EAAE,CACvG,KAAK,CAAC,KAAK,EAAE,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,EAAwC,CAAC,CAAC;AAElF;;;;;GAKG;AACH,SAAS,SAAS,CAAC,GAAY;IAC7B,IAAI,GAAG,YAAY,MAAM,CAAC,yBAAyB;QAAE,OAAO,IAAI,CAAC;IACjE,MAAM,SAAS,GAAG,CAAE,GAA4C,EAAE,KAAK,CAAC,EAAE,IAAI,CAAC;IAC/E,OAAO,SAAS,KAAK,cAAc,CAAC;AACtC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,MAAuB,EACvB,YAAoB,EACpB,UAAkB;IAElB,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC;QACxB,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,cAAc;QACvC,GAAG,CAAC,MAAM,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC;QACxE,wEAAwE;QACxE,sEAAsE;QACtE,qEAAqE;QACrE,yDAAyD;QACzD,UAAU,EAAE,CAAC;QACb,KAAK,EAAE,uBAAuB;KAC/B,CAAC,CAAC;IAEH,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC;YACtD,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,eAAe,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE;YACxC,kEAAkE;YAClE,sEAAsE;YACtE,oEAAoE;YACpE,+DAA+D;YAC/D,qEAAqE;YACrE,gEAAgE;YAChE,WAAW,EAAE,CAAC;YACd,IAAI,EAAE,CAAC;YACP,QAAQ,EAAE;gBACR,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE;gBACzC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE;aACtC;SACF,CAAC,CAAC;QAEH,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC;QACxD,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,IAAI,OAAO,CAAC,mCAAmC,CAAC,CAAC;QACzD,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,OAAO;YAAE,MAAM,GAAG,CAAC;QACtC,wEAAwE;QACxE,wEAAwE;QACxE,wDAAwD;QACxD,IAAI,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC;YACnB,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC;YACjH,MAAM,IAAI,OAAO,CACf,GAAG,MAAM,CAAC,KAAK,OAAO,MAAM,CAAC,OAAO,0BAA0B,MAAM,uCAAuC;gBACzG,oGAAoG;gBACpG,wFAAwF,EAC1F,GAAG,CACJ,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,OAAO,CAAC,gCAAgC,MAAM,CAAC,OAAO,YAAa,GAAa,CAAC,OAAO,EAAE,EAAE,GAAG,CAAC,CAAC;IAC7G,CAAC;AACH,CAAC"}
@@ -0,0 +1,105 @@
1
+ import { CORRECTNESS_CHECKLIST } from "./checklist.js";
2
+ /**
3
+ * The model's note on an unexplained check, or nothing.
4
+ *
5
+ * Drops the WHOLE field rather than scrubbing a URL out of it: a note that
6
+ * tried to link ignored the one constraint it was given, and half of such a
7
+ * note is worse than none — it would read as vetted prose while having
8
+ * already demonstrated it was not following the contract. The length and
9
+ * newline limits keep it to the one sentence it is meant to be, so it cannot
10
+ * become a second remediation section nobody asked for.
11
+ *
12
+ * Exported so the replay harness's fence-regression check can ask THIS
13
+ * function rather than re-stating its rules: a second copy of the pattern
14
+ * drifted from this one the moment the bare-domain clause was added here, and
15
+ * a check that is weaker than the fence it guards reports no regression.
16
+ */
17
+ export function sanitizeObservation(value) {
18
+ if (typeof value !== "string")
19
+ return undefined;
20
+ const text = value.trim();
21
+ if (text.length === 0 || text.length > 400)
22
+ return undefined;
23
+ if (/[\r\n]/.test(text))
24
+ return undefined;
25
+ if (/https?:\/\/|www\.|\bdreamdata\.io\b|\]\(/i.test(text))
26
+ return undefined;
27
+ return text;
28
+ }
29
+ function normalizePassed(value) {
30
+ if (value === true || value === false)
31
+ return value;
32
+ if (value === "not_applicable" || value === "n/a" || value === "na")
33
+ return "not_applicable";
34
+ return "inconclusive";
35
+ }
36
+ function extractJsonBlock(text) {
37
+ const fencedMatch = /```json\s*([\s\S]*?)```/i.exec(text) ?? /```\s*([\s\S]*?)```/.exec(text);
38
+ if (fencedMatch)
39
+ return fencedMatch[1];
40
+ const braceStart = text.indexOf("{");
41
+ const braceEnd = text.lastIndexOf("}");
42
+ if (braceStart >= 0 && braceEnd > braceStart)
43
+ return text.slice(braceStart, braceEnd + 1);
44
+ return null;
45
+ }
46
+ function buildInconclusiveVerdicts(rawResponse) {
47
+ return CORRECTNESS_CHECKLIST.map((item) => ({
48
+ id: item.id,
49
+ title: item.title,
50
+ passed: "inconclusive",
51
+ evidenceQuote: "",
52
+ feedback: `Could not parse a structured verdict from the model response. Raw response: ${rawResponse.slice(0, 500)}`,
53
+ }));
54
+ }
55
+ export function parseAIResponse(rawResponse) {
56
+ const jsonBlock = extractJsonBlock(rawResponse);
57
+ if (!jsonBlock) {
58
+ return {
59
+ verdicts: buildInconclusiveVerdicts(rawResponse),
60
+ overallPass: false,
61
+ remediationAdvice: "The AI response was not structured JSON; manual review of the raw output is recommended.",
62
+ rawResponse,
63
+ parseFailed: true,
64
+ };
65
+ }
66
+ let parsed;
67
+ try {
68
+ parsed = JSON.parse(jsonBlock);
69
+ }
70
+ catch {
71
+ return {
72
+ verdicts: buildInconclusiveVerdicts(rawResponse),
73
+ overallPass: false,
74
+ remediationAdvice: "The AI response contained malformed JSON; manual review of the raw output is recommended.",
75
+ rawResponse,
76
+ parseFailed: true,
77
+ };
78
+ }
79
+ const verdictsById = new Map((parsed.verdicts ?? []).map((v) => [v.id, v]));
80
+ const verdicts = CORRECTNESS_CHECKLIST.map((item) => {
81
+ const raw = verdictsById.get(item.id);
82
+ return {
83
+ id: item.id,
84
+ title: item.title,
85
+ passed: normalizePassed(raw?.passed),
86
+ evidenceQuote: typeof raw?.evidenceQuote === "string" ? raw.evidenceQuote : "",
87
+ feedback: typeof raw?.feedback === "string" ? raw.feedback : "No feedback returned for this item.",
88
+ };
89
+ });
90
+ // Derived, not taken from the model. The exit code rides on this, so it has
91
+ // to follow deterministically from the per-item verdicts: a "not_applicable"
92
+ // or "inconclusive" item must not sink the run (a cookieless-only install
93
+ // legitimately has several), while an all-inconclusive run is not a pass
94
+ // either — nothing was actually confirmed.
95
+ const overallPass = verdicts.some((v) => v.passed === true) && verdicts.every((v) => v.passed !== false);
96
+ return {
97
+ verdicts,
98
+ overallPass,
99
+ remediationAdvice: typeof parsed.remediationAdvice === "string" ? parsed.remediationAdvice : "",
100
+ rawResponse,
101
+ parseFailed: false,
102
+ unexplainedObservation: sanitizeObservation(parsed.unexplainedObservation),
103
+ };
104
+ }
105
+ //# sourceMappingURL=response-parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"response-parser.js","sourceRoot":"","sources":["../../src/ai/response-parser.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/ai/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,225 @@
1
+ /**
2
+ * The one question a customer asks about consent that no existing check
3
+ * answers: *is Google Consent Mode v2 correctly enabled on my site?*
4
+ *
5
+ * It is a separate module and a pure function of `phase.consent` alone — no
6
+ * network, no cookies, no script, no tag manager — so it is a leaf that
7
+ * deployment.ts and diagnosis/rules.ts can both read, and so its decision
8
+ * table can be walked by a unit test without constructing a whole run.
9
+ *
10
+ * THREE INVARIANTS, each verified against live gtag.js, hold this file
11
+ * together. Every one of them is a rule that a previous version of this
12
+ * codebase (and of the tracking snippet the CLI verifies) got wrong:
13
+ *
14
+ * 1. `ics.active === true` is the ONLY proof Consent Mode is installed.
15
+ * `ics.entries` carrying signal keys proves nothing — gtag/GTM creates
16
+ * them itself as soon as a container loads (verified live: a page with no
17
+ * Consent Mode at all reported four of them), so a site whose CMP never
18
+ * calls gtag('consent', …) still has a readable analytics_storage state.
19
+ *
20
+ * 2. Resolution is always `update ?? default`, and `default` is ALREADY
21
+ * region-resolved by gtag.js. This module therefore never re-resolves a
22
+ * region itself; the push log is read only to EXPLAIN a resolved state
23
+ * that a raw reader would have misread (see F4).
24
+ *
25
+ * 3. `usedUpdate === false` is what separates "correctly installed, this
26
+ * visitor has not answered yet" from "not installed at all". Never infer
27
+ * the latter from `entries.analytics_storage.update === undefined`: on
28
+ * most page loads, of most correctly configured sites, that field is
29
+ * absent because nobody has clicked anything yet.
30
+ *
31
+ * Scope boundary: this asks whether the MECHANISM is installed, complete and
32
+ * correctly ordered. Whether an unanswered visitor is actually covered is a
33
+ * different question, asked by the cookieless-covers-unconsented check — a
34
+ * site can legitimately pass here while every unconsented visit goes
35
+ * untracked, because the standard bundle's loader correctly declines to
36
+ * initialise and no cookieless bundle was installed behind it.
37
+ */
38
+ import { consentModeVersionFromKeys } from "../evidence/consent.js";
39
+ /**
40
+ * Fixed emission order, so the same evidence always produces the same
41
+ * sequence of causes. Roughly severity: not installed, then incomplete, then
42
+ * mis-ordered, then misleading, then timing, then the benign one.
43
+ */
44
+ const CAUSE_ORDER = [
45
+ "no-consent-mode",
46
+ "v1-only",
47
+ "set-late",
48
+ "region-shadowed-default",
49
+ "wait-period-timed-out",
50
+ "not-yet-answered",
51
+ ];
52
+ /** True when at least one phase saw a Google tag, whatever it did with it. */
53
+ export function sawGoogleTag(phases) {
54
+ return phases.some((phase) => phase.consent.googleTagDataExists);
55
+ }
56
+ /**
57
+ * F4 — a global default and a region-scoped default disagree.
58
+ *
59
+ * The shape: a CMP declares `analytics_storage: granted` globally (for
60
+ * visitors it does not have to ask), then `denied` for a list of regions.
61
+ * gtag.js resolves it correctly, so nothing leaks; what breaks is every
62
+ * consumer that reads the raw push log, which sees the global `granted` and
63
+ * takes it for a decision. That includes Dreamdata's own GTM snippet, which
64
+ * is why this is worth reporting even though the site's end state is right.
65
+ *
66
+ * Requires the disagreement AND a region scope: two plain global defaults
67
+ * that disagree are a CMP correcting itself, and the later one simply wins
68
+ * for everyone.
69
+ */
70
+ function regionShadowedDefault(phase) {
71
+ const defaults = phase.consent.dataLayerConsentPushes.filter((push) => push.verb === "default" && push.analyticsStorage !== undefined);
72
+ if (defaults.length < 2)
73
+ return false;
74
+ const values = new Set(defaults.map((push) => push.analyticsStorage));
75
+ if (values.size < 2)
76
+ return false;
77
+ return defaults.some((push) => push.region !== null && push.region !== undefined);
78
+ }
79
+ /** true from either the settled read or the pre-decision one — see IcsSnapshot. */
80
+ function setLate(ics, before) {
81
+ return ics.wasSetLate === true || before?.wasSetLate === true;
82
+ }
83
+ /**
84
+ * Whether every flag this verdict needs read back as null — a gtag.js build
85
+ * that exposes an `ics` object and none of its diagnostics. The verdict has
86
+ * nothing to stand on, and says so rather than guessing.
87
+ */
88
+ function flagsUnreadable(ics) {
89
+ return ics.active === null && ics.usedDefault === null && ics.usedUpdate === null;
90
+ }
91
+ export function judgePhaseConsentMode(phase) {
92
+ const consent = phase.consent;
93
+ const ics = consent.ics;
94
+ const name = phase.phase;
95
+ const base = `${name}.consent.googleTagDataExists: ${consent.googleTagDataExists}`;
96
+ // Row 0 — evidence predating this check, or a read that threw in the page.
97
+ if (ics === null || ics === undefined) {
98
+ return {
99
+ phase: name,
100
+ outcome: "unreadable",
101
+ version: consent.googleTagDataExists ? "unknown" : "none",
102
+ causes: [],
103
+ evidence: `${base}, ${name}.consent.ics: null`,
104
+ };
105
+ }
106
+ const version = consentModeVersionFromKeys(ics.entryKeys);
107
+ const keys = `${name}.consent.ics.entryKeys: [${ics.entryKeys.join(", ")}]`;
108
+ // Row 1 — no Google tag at all. Nothing to hold to Consent Mode: this is a
109
+ // not_applicable, never a failure. A site can be perfectly compliant
110
+ // without gtag.js on it.
111
+ if (!ics.present && !consent.googleTagDataExists) {
112
+ return { phase: name, outcome: "unreadable", version: "none", causes: [], evidence: `${base}, ics absent` };
113
+ }
114
+ if (flagsUnreadable(ics)) {
115
+ return {
116
+ phase: name,
117
+ outcome: "unreadable",
118
+ version: "unknown",
119
+ causes: [],
120
+ evidence: `${base}, ${keys}, every ics diagnostic flag read as null`,
121
+ };
122
+ }
123
+ // Row 2 — F1. The entries exist because a container created them; nothing
124
+ // ever called gtag('consent', …). This is the expensive case: the site
125
+ // looks like it has Consent Mode to anything reading entries.
126
+ if (ics.active !== true) {
127
+ return {
128
+ phase: name,
129
+ outcome: "not-installed",
130
+ version,
131
+ causes: ["no-consent-mode"],
132
+ evidence: `${name}.consent.ics.active: ${ics.active}, ${keys}`,
133
+ };
134
+ }
135
+ // Row 3 — F2. Reported before the ordering and timing faults because it is
136
+ // a gap in what the site declares at all, not in how it behaves.
137
+ if (version === "v1") {
138
+ return {
139
+ phase: name,
140
+ outcome: "misconfigured",
141
+ version,
142
+ causes: ["v1-only"],
143
+ evidence: `${keys} (ad_user_data/ad_personalization missing)`,
144
+ };
145
+ }
146
+ const causes = [];
147
+ let outcome = "enabled";
148
+ // Row 4 — F6. The one fault here that is unambiguously the customer's, and
149
+ // the one nothing else in the run observes.
150
+ if (setLate(ics, consent.icsBeforeDecision)) {
151
+ causes.push("set-late");
152
+ outcome = "misconfigured";
153
+ }
154
+ // Row 5 — F4.
155
+ if (regionShadowedDefault(phase)) {
156
+ causes.push("region-shadowed-default");
157
+ outcome = "misconfigured";
158
+ }
159
+ // Row 6 / 6b — F5. A visitor who never answers legitimately lets the window
160
+ // close, so in the clean phase this is descriptive. On a phase where the
161
+ // banner WAS answered it means the answer arrived after the window, which
162
+ // is a real fault.
163
+ if (ics.waitPeriodTimedOut === true) {
164
+ causes.push("wait-period-timed-out");
165
+ if (name !== "clean")
166
+ outcome = "misconfigured";
167
+ }
168
+ // Rows 7 and 8 — F3 versus a carried decision. Only reached when nothing
169
+ // above downgraded the outcome, so a misconfiguration is never relabelled
170
+ // as "just unanswered".
171
+ if (ics.usedUpdate !== true) {
172
+ causes.push("not-yet-answered");
173
+ if (outcome === "enabled")
174
+ outcome = "enabled-not-yet-answered";
175
+ }
176
+ return {
177
+ phase: name,
178
+ outcome,
179
+ version,
180
+ causes: orderCauses(causes),
181
+ evidence: `${name}.consent.ics.active: true, ${keys}, ` +
182
+ `usedDefault: ${ics.usedDefault}, usedUpdate: ${ics.usedUpdate}, ` +
183
+ `wasSetLate: ${ics.wasSetLate}, waitPeriodTimedOut: ${ics.waitPeriodTimedOut}`,
184
+ };
185
+ }
186
+ function orderCauses(causes) {
187
+ const present = new Set(causes);
188
+ return CAUSE_ORDER.filter((id) => present.has(id));
189
+ }
190
+ export function judgeConsentModeV2(phases) {
191
+ const perPhase = phases.map(judgePhaseConsentMode);
192
+ const readable = perPhase.filter((entry) => entry.outcome !== "unreadable");
193
+ const causes = orderCauses(perPhase.flatMap((entry) => entry.causes));
194
+ // A v1 reading anywhere wins: it is a statement about what the container
195
+ // declares, which does not change between phases, and the phase that saw
196
+ // fewer keys is the one that caught the container mid-load.
197
+ const version = readable.some((entry) => entry.version === "v1")
198
+ ? "v1"
199
+ : readable.some((entry) => entry.version === "v2")
200
+ ? "v2"
201
+ : perPhase.some((entry) => entry.version === "unknown")
202
+ ? "unknown"
203
+ : "none";
204
+ const outcome = readable.length === 0
205
+ ? "unreadable"
206
+ : readable.every((entry) => entry.outcome === "not-installed")
207
+ ? "not-installed"
208
+ : readable.some((entry) => entry.outcome === "misconfigured")
209
+ ? "misconfigured"
210
+ : // One phase seeing an update carry is enough: it is positive proof
211
+ // the mechanism works, and it is normally the grant phase that
212
+ // provides it. The other phases reading "not yet answered" is
213
+ // exactly what they are supposed to read.
214
+ readable.some((entry) => entry.outcome === "enabled")
215
+ ? "enabled"
216
+ : "enabled-not-yet-answered";
217
+ return {
218
+ outcome,
219
+ version,
220
+ causes,
221
+ perPhase,
222
+ evidence: perPhase.map((entry) => entry.evidence).join(" | "),
223
+ };
224
+ }
225
+ //# sourceMappingURL=consent-mode.js.map