@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.
- package/LICENSE +21 -0
- package/README.md +756 -0
- package/dist/ai/checker.js +222 -0
- package/dist/ai/checker.js.map +1 -0
- package/dist/ai/checklist.js +190 -0
- package/dist/ai/checklist.js.map +1 -0
- package/dist/ai/cli-presets.js +27 -0
- package/dist/ai/cli-presets.js.map +1 -0
- package/dist/ai/evidence-view.js +143 -0
- package/dist/ai/evidence-view.js.map +1 -0
- package/dist/ai/local-cli.js +58 -0
- package/dist/ai/local-cli.js.map +1 -0
- package/dist/ai/openai-compatible.js +78 -0
- package/dist/ai/openai-compatible.js.map +1 -0
- package/dist/ai/response-parser.js +105 -0
- package/dist/ai/response-parser.js.map +1 -0
- package/dist/ai/types.js +2 -0
- package/dist/ai/types.js.map +1 -0
- package/dist/analysis/consent-mode.js +225 -0
- package/dist/analysis/consent-mode.js.map +1 -0
- package/dist/analysis/deployment.js +2658 -0
- package/dist/analysis/deployment.js.map +1 -0
- package/dist/analysis/diagnosis/copy.en.js +303 -0
- package/dist/analysis/diagnosis/copy.en.js.map +1 -0
- package/dist/analysis/diagnosis/diagnose.js +172 -0
- package/dist/analysis/diagnosis/diagnose.js.map +1 -0
- package/dist/analysis/diagnosis/docs.js +118 -0
- package/dist/analysis/diagnosis/docs.js.map +1 -0
- package/dist/analysis/diagnosis/index.js +5 -0
- package/dist/analysis/diagnosis/index.js.map +1 -0
- package/dist/analysis/diagnosis/rules.js +656 -0
- package/dist/analysis/diagnosis/rules.js.map +1 -0
- package/dist/analysis/diagnosis/types.js +2 -0
- package/dist/analysis/diagnosis/types.js.map +1 -0
- package/dist/cli.js +344 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/env.js +100 -0
- package/dist/config/env.js.map +1 -0
- package/dist/config/preflight.js +167 -0
- package/dist/config/preflight.js.map +1 -0
- package/dist/config/user-config.js +79 -0
- package/dist/config/user-config.js.map +1 -0
- package/dist/evidence/collect.js +527 -0
- package/dist/evidence/collect.js.map +1 -0
- package/dist/evidence/consent-accept.js +377 -0
- package/dist/evidence/consent-accept.js.map +1 -0
- package/dist/evidence/consent.js +340 -0
- package/dist/evidence/consent.js.map +1 -0
- package/dist/evidence/constants.js +615 -0
- package/dist/evidence/constants.js.map +1 -0
- package/dist/evidence/globals.js +132 -0
- package/dist/evidence/globals.js.map +1 -0
- package/dist/evidence/gtm-container.js +432 -0
- package/dist/evidence/gtm-container.js.map +1 -0
- package/dist/evidence/heuristic-banner.js +107 -0
- package/dist/evidence/heuristic-banner.js.map +1 -0
- package/dist/evidence/http-probe.js +139 -0
- package/dist/evidence/http-probe.js.map +1 -0
- package/dist/evidence/init-script.js +112 -0
- package/dist/evidence/init-script.js.map +1 -0
- package/dist/evidence/network.js +365 -0
- package/dist/evidence/network.js.map +1 -0
- package/dist/evidence/preference-center.js +368 -0
- package/dist/evidence/preference-center.js.map +1 -0
- package/dist/evidence/script.js +425 -0
- package/dist/evidence/script.js.map +1 -0
- package/dist/evidence/second-page.js +332 -0
- package/dist/evidence/second-page.js.map +1 -0
- package/dist/evidence/selector-probe.js +53 -0
- package/dist/evidence/selector-probe.js.map +1 -0
- package/dist/evidence/stealth.js +21 -0
- package/dist/evidence/stealth.js.map +1 -0
- package/dist/evidence/types.js +14 -0
- package/dist/evidence/types.js.map +1 -0
- package/dist/interactive/prompt.js +31 -0
- package/dist/interactive/prompt.js.map +1 -0
- package/dist/mcp/server.js +308 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/summary.js +41 -0
- package/dist/mcp/summary.js.map +1 -0
- package/dist/report/context.js +184 -0
- package/dist/report/context.js.map +1 -0
- package/dist/report/projection.js +125 -0
- package/dist/report/projection.js.map +1 -0
- package/dist/report/quote-check.js +90 -0
- package/dist/report/quote-check.js.map +1 -0
- package/dist/report/render.js +178 -0
- package/dist/report/render.js.map +1 -0
- package/dist/report/serializers.js +124 -0
- package/dist/report/serializers.js.map +1 -0
- package/dist/report/store.js +139 -0
- package/dist/report/store.js.map +1 -0
- package/dist/run/verify.js +77 -0
- package/dist/run/verify.js.map +1 -0
- package/dist/utils/duration.js +10 -0
- package/dist/utils/duration.js.map +1 -0
- package/dist/utils/errors.js +17 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/logger.js +59 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/runTimingsLog.js +27 -0
- package/dist/utils/runTimingsLog.js.map +1 -0
- package/dist/utils/slug.js +27 -0
- package/dist/utils/slug.js.map +1 -0
- package/dist/version.js +10 -0
- package/dist/version.js.map +1 -0
- package/package.json +57 -0
- 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":"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"}
|
|
@@ -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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"}
|
package/dist/ai/types.js
ADDED
|
@@ -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
|