blun-king-cli 9.0.0 → 9.0.1
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/LIESMICH.txt +1 -7
- package/README.md +4 -16
- package/bin/blun.js +248 -160
- package/bin/core-bootstrap.js +47 -0
- package/bin/king.js +277 -1
- package/bin/launcher-mode.js +2 -1
- package/bin/launcher-runtime.js +221 -0
- package/bin/plugin-bootstrap.js +0 -0
- package/bin/private-paths.js +0 -0
- package/bin/update-lease.js +399 -0
- package/bin/update-notice.js +1094 -0
- package/blun.mjs +4060 -6667
- package/package.json +3 -10
- package/skills/screenshot-lesen/SKILL.md +0 -1
- package/skills/web-lesen/SKILL.md +0 -1
- package/telegram-plugin/dist/bridge.mjs +1 -21
- package/mnemo/access_routes.js +0 -692
- package/mnemo/agent_governance.js +0 -4242
- package/mnemo/agent_mail.js +0 -901
- package/mnemo/bootstrap_auto.js +0 -137
- package/mnemo/brief_coordination.js +0 -226
- package/mnemo/code_read_tools.js +0 -375
- package/mnemo/context_preview_tools.js +0 -603
- package/mnemo/embeddings.js +0 -66
- package/mnemo/external_repo_ops.js +0 -575
- package/mnemo/facts/example-project-rules.json +0 -90
- package/mnemo/facts/example.json +0 -34
- package/mnemo/identity_schema.sql +0 -139
- package/mnemo/journal_schema.js +0 -561
- package/mnemo/loop_doctor_tools.js +0 -661
- package/mnemo/mail_secret_refs.js +0 -150
- package/mnemo/mcp.js +0 -9309
- package/mnemo/memory_consolidation.js +0 -1914
- package/mnemo/memory_health_tools.js +0 -165
- package/mnemo/package.json +0 -79
- package/mnemo/protected_scope_gate.js +0 -627
- package/mnemo/resource_access_control.js +0 -684
- package/mnemo/runtime_governance.js +0 -1256
- package/mnemo/runtime_turn_gate.js +0 -862
- package/mnemo/sandbox.js +0 -143
- package/mnemo/schema.sql +0 -389
- package/mnemo/shared_utils.js +0 -763
- package/mnemo/skills/agent-auto-resume/SKILL.md +0 -56
- package/mnemo/skills/agent_hand/SKILL.md +0 -43
- package/mnemo/skills/agent_hand/run.js +0 -63
- package/mnemo/skills/book_flight/SKILL.md +0 -34
- package/mnemo/skills/external_repo_review/SKILL.md +0 -43
- package/mnemo/skills/external_repo_review/run.js +0 -73
- package/mnemo/skills/pay_invoice/SKILL.md +0 -34
- package/mnemo/team_quality_ops.js +0 -944
- package/mnemo/timeline_report_tools.js +0 -810
- package/mnemo/write_gate_risk.js +0 -80
- package/mnemo/writer_health.js +0 -152
- package/skills/doku-ingestion/SKILL.md +0 -48
- package/skills/doku-ingestion/ingest_docs.py +0 -133
package/mnemo/shared_utils.js
DELETED
|
@@ -1,763 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared pure utilities — single source of truth for helpers used
|
|
3
|
-
* by daemon.js, mcp.js, and timeline_report_tools.js.
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* const { parseMaybeJson, deepMergePlain, uniqueIntegers } = require("./shared_utils");
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
function parseMaybeJson(value, fallback) {
|
|
10
|
-
if (value == null || value === "") return fallback;
|
|
11
|
-
if (typeof value !== "string") return value;
|
|
12
|
-
try { return JSON.parse(value); } catch { return fallback === undefined ? value : fallback; }
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function uniqueIntegers(values) {
|
|
16
|
-
return Array.from(new Set((Array.isArray(values) ? values : [values])
|
|
17
|
-
.map(v => parseInt(v, 10))
|
|
18
|
-
.filter(v => Number.isInteger(v) && v > 0)));
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function deepMergePlain(base, override) {
|
|
22
|
-
const out = Object.assign({}, base || {});
|
|
23
|
-
for (const [k, v] of Object.entries(override || {})) {
|
|
24
|
-
if (v && typeof v === "object" && !Array.isArray(v) && out[k] && typeof out[k] === "object" && !Array.isArray(out[k])) {
|
|
25
|
-
out[k] = deepMergePlain(out[k], v);
|
|
26
|
-
} else {
|
|
27
|
-
out[k] = v;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return out;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Strip memory-private blocks from text before SQLite persist.
|
|
34
|
-
function stripPrivate(text) {
|
|
35
|
-
if (typeof text !== "string" || !text) return { text, hadPrivate: false };
|
|
36
|
-
const patterns = [
|
|
37
|
-
{ re: /<private\b[^>]*>[\s\S]*?<\/private>/gi, marker: "[private]" },
|
|
38
|
-
{ re: /<mnemo-private\b[^>]*>[\s\S]*?<\/mnemo-private>/gi, marker: "[private]" },
|
|
39
|
-
{ re: /<memory-private\b[^>]*>[\s\S]*?<\/memory-private>/gi, marker: "[private]" },
|
|
40
|
-
{ re: /<no-memory\b[^>]*>[\s\S]*?<\/no-memory>/gi, marker: "[no-memory]" },
|
|
41
|
-
{ re: /<nomemory\b[^>]*>[\s\S]*?<\/nomemory>/gi, marker: "[no-memory]" },
|
|
42
|
-
{ re: /\[private\][\s\S]*?\[\/private\]/gi, marker: "[private]" },
|
|
43
|
-
{ re: /\[no-memory\][\s\S]*?\[\/no-memory\]/gi, marker: "[no-memory]" },
|
|
44
|
-
{ re: /<!--\s*(?:mnemo:)?private\s*-->[\s\S]*?<!--\s*\/(?:mnemo:)?private\s*-->/gi, marker: "[private]" },
|
|
45
|
-
{ re: /<!--\s*(?:mnemo:)?no-memory\s*-->[\s\S]*?<!--\s*\/(?:mnemo:)?no-memory\s*-->/gi, marker: "[no-memory]" },
|
|
46
|
-
];
|
|
47
|
-
let out = text;
|
|
48
|
-
let hadPrivate = false;
|
|
49
|
-
for (const pattern of patterns) {
|
|
50
|
-
pattern.re.lastIndex = 0;
|
|
51
|
-
if (pattern.re.test(out)) {
|
|
52
|
-
hadPrivate = true;
|
|
53
|
-
pattern.re.lastIndex = 0;
|
|
54
|
-
out = out.replace(pattern.re, pattern.marker);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return { text: out, hadPrivate };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function parseAgentCsv(value) {
|
|
61
|
-
return String(value || "")
|
|
62
|
-
.split(",")
|
|
63
|
-
.map((s) => s.trim())
|
|
64
|
-
.filter(Boolean);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function normalizeAgentName(name) {
|
|
68
|
-
return String(name || "").trim().toLowerCase();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function jsonSafe(value, maxChars = 12000) {
|
|
72
|
-
if (value === undefined) return null;
|
|
73
|
-
try {
|
|
74
|
-
const raw = typeof value === "string" ? value : JSON.stringify(value);
|
|
75
|
-
if (!raw) return null;
|
|
76
|
-
return raw.length > maxChars ? raw.slice(0, maxChars) + "...[truncated]" : raw;
|
|
77
|
-
} catch {
|
|
78
|
-
const raw = String(value);
|
|
79
|
-
return raw.length > maxChars ? raw.slice(0, maxChars) + "...[truncated]" : raw;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function compactContent(value, maxChars = 8000) {
|
|
84
|
-
if (value == null) return null;
|
|
85
|
-
const raw = typeof value === "string" ? value : jsonSafe(value, maxChars);
|
|
86
|
-
if (!raw) return null;
|
|
87
|
-
const scrubbed = stripPrivate(raw).text || "";
|
|
88
|
-
return scrubbed.length > maxChars ? scrubbed.slice(0, maxChars) + "...[truncated]" : scrubbed;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function parseMetaJson(metaJson) {
|
|
92
|
-
try { return JSON.parse(metaJson || "{}"); } catch { return {}; }
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function isoOrNull(value) {
|
|
96
|
-
if (!value) return null;
|
|
97
|
-
let raw = String(value).trim();
|
|
98
|
-
if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) raw += "T09:00:00";
|
|
99
|
-
const d = new Date(raw);
|
|
100
|
-
return Number.isNaN(d.getTime()) ? null : d.toISOString();
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function parseBriefTitle(content) {
|
|
104
|
-
const lines = String(content || "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
105
|
-
const first = lines.find((line) => !/^#{1,6}\s*$/.test(line)) || "Brief";
|
|
106
|
-
return first.replace(/^#{1,6}\s*/, "").slice(0, 140) || "Brief";
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// --- Brief contract constants & helpers ---
|
|
110
|
-
|
|
111
|
-
const TEAM_BRIEF_ALIASES = new Set(["all", "crew", "everyone", "group", "gruppe", "team"]);
|
|
112
|
-
const BRIEF_CONTRACT_VERSION = "firm-brief-v1";
|
|
113
|
-
const BRIEF_REQUIRED_HEADINGS = ["## Title", "## Project", "## Request", "## Acceptance", "## Report Back"];
|
|
114
|
-
|
|
115
|
-
function cleanScope(scope) {
|
|
116
|
-
return String(scope || "default").toLowerCase().replace(/[^a-z0-9_-]/g, "") || "default";
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function uniqueAgentNames(names) {
|
|
120
|
-
const seen = new Set();
|
|
121
|
-
const out = [];
|
|
122
|
-
for (const name of names || []) {
|
|
123
|
-
const cleaned = String(name || "").trim();
|
|
124
|
-
const key = cleaned.toLowerCase();
|
|
125
|
-
if (!cleaned || TEAM_BRIEF_ALIASES.has(key) || seen.has(key)) continue;
|
|
126
|
-
seen.add(key);
|
|
127
|
-
out.push(key);
|
|
128
|
-
}
|
|
129
|
-
return out;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function isTeamBriefTarget(name) {
|
|
133
|
-
return TEAM_BRIEF_ALIASES.has(String(name || "").trim().toLowerCase());
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function hasCanonicalBriefShape(content) {
|
|
137
|
-
const text = String(content || "");
|
|
138
|
-
return BRIEF_REQUIRED_HEADINGS.filter((heading) => text.includes(heading)).length >= 4;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function normalizeBriefMeta(meta, extras = {}) {
|
|
142
|
-
const base = meta && typeof meta === "object" ? { ...meta } : {};
|
|
143
|
-
return {
|
|
144
|
-
...base,
|
|
145
|
-
...extras,
|
|
146
|
-
brief_contract_version: BRIEF_CONTRACT_VERSION,
|
|
147
|
-
brief_contract_required: true,
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function normalizeBriefContent(content, meta, extras = {}) {
|
|
152
|
-
const body = String(content || "").trim();
|
|
153
|
-
const normalizedMeta = normalizeBriefMeta(meta, extras);
|
|
154
|
-
if (!body) return { content: body, meta: normalizedMeta };
|
|
155
|
-
if (hasCanonicalBriefShape(body)) return { content: body, meta: normalizedMeta };
|
|
156
|
-
const project = String(normalizedMeta.project || normalizedMeta.scope || normalizedMeta.portal || "unspecified").trim() || "unspecified";
|
|
157
|
-
const constraints = []
|
|
158
|
-
.concat(Array.isArray(normalizedMeta.constraints) ? normalizedMeta.constraints : [])
|
|
159
|
-
.concat(Array.isArray(normalizedMeta.guardrails) ? normalizedMeta.guardrails : [])
|
|
160
|
-
.filter(Boolean);
|
|
161
|
-
const acceptance = []
|
|
162
|
-
.concat(Array.isArray(normalizedMeta.acceptance) ? normalizedMeta.acceptance : [])
|
|
163
|
-
.concat(Array.isArray(normalizedMeta.acceptance_criteria) ? normalizedMeta.acceptance_criteria : [])
|
|
164
|
-
.filter(Boolean);
|
|
165
|
-
const reportBack = []
|
|
166
|
-
.concat(Array.isArray(normalizedMeta.report_back) ? normalizedMeta.report_back : [])
|
|
167
|
-
.concat(["what changed", "what was checked", "what is still open"]);
|
|
168
|
-
return {
|
|
169
|
-
content: [
|
|
170
|
-
"# Brief",
|
|
171
|
-
"",
|
|
172
|
-
"## Title",
|
|
173
|
-
parseBriefTitle(body),
|
|
174
|
-
"",
|
|
175
|
-
"## Project",
|
|
176
|
-
project,
|
|
177
|
-
"",
|
|
178
|
-
"## Request",
|
|
179
|
-
body,
|
|
180
|
-
"",
|
|
181
|
-
"## Constraints",
|
|
182
|
-
...(constraints.length ? constraints.map((item) => `- ${item}`) : ["- follow project rules", "- no duplicate work", "- stay in assigned lane"]),
|
|
183
|
-
"",
|
|
184
|
-
"## Acceptance",
|
|
185
|
-
...(acceptance.length ? acceptance.map((item) => `- ${item}`) : ["- requested outcome is implemented", "- no regressions introduced", "- result is reported in the standard report area"]),
|
|
186
|
-
"",
|
|
187
|
-
"## Report Back",
|
|
188
|
-
...reportBack.map((item) => `- ${item}`),
|
|
189
|
-
].join("\n"),
|
|
190
|
-
meta: normalizedMeta,
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// --- File / media helpers ---
|
|
195
|
-
|
|
196
|
-
function baseName(p) {
|
|
197
|
-
const raw = String(p || "").split(/[\\/]/).pop() || "";
|
|
198
|
-
return raw.trim();
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function extensionName(p) {
|
|
202
|
-
const name = baseName(p);
|
|
203
|
-
const idx = name.lastIndexOf(".");
|
|
204
|
-
return idx >= 0 ? name.slice(idx + 1).toLowerCase() : "";
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function inferMediaKind(a, meta, payload, fileName, ext) {
|
|
208
|
-
const eventKind = String(a.event_kind || "").toLowerCase();
|
|
209
|
-
const hinted = String(a.media_kind || meta.media_kind || payload.media_kind || "").toLowerCase();
|
|
210
|
-
if (hinted) return hinted;
|
|
211
|
-
if (eventKind.includes("screenshot")) return "screenshot";
|
|
212
|
-
if (eventKind.includes("video")) return "video";
|
|
213
|
-
if (eventKind.includes("audio") || eventKind.includes("voice")) return "audio";
|
|
214
|
-
if (eventKind.includes("photo") || eventKind.includes("image")) return "image";
|
|
215
|
-
if (eventKind.includes("document") || eventKind.includes("pdf")) return "document";
|
|
216
|
-
if (eventKind.includes("file") || eventKind.includes("attachment")) return "file";
|
|
217
|
-
if (["png","jpg","jpeg","webp","gif","bmp"].includes(ext)) return "screenshot";
|
|
218
|
-
if (["mp4","mov","webm","mkv","avi","m4v","mpeg","mpg"].includes(ext)) return "video";
|
|
219
|
-
if (["mp3","wav","m4a","aac","ogg","oga","opus","flac"].includes(ext)) return "audio";
|
|
220
|
-
if (["pdf","doc","docx","txt","md","rtf","html","htm","csv","tsv","json","jsonl","xml","log"].includes(ext)) return "document";
|
|
221
|
-
if (fileName) return "file";
|
|
222
|
-
return "";
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function inferMediaType(ext, kind) {
|
|
226
|
-
if (kind === "screenshot" || kind === "image") return "image";
|
|
227
|
-
if (kind === "video") return "video";
|
|
228
|
-
if (kind === "audio") return "audio";
|
|
229
|
-
if (kind === "document") return "document";
|
|
230
|
-
return ext ? "file" : "";
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function uniqueStrings(list) {
|
|
234
|
-
return Array.from(new Set((Array.isArray(list) ? list : [list]).map(x => String(x || "").trim()).filter(Boolean)));
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function compactTitleText(value, max = 90) {
|
|
238
|
-
const raw = String(value || "")
|
|
239
|
-
.replace(/<[^>]+>/g, " ")
|
|
240
|
-
.replace(/[`*_#>\[\](){}]/g, " ")
|
|
241
|
-
.replace(/\s+/g, " ")
|
|
242
|
-
.trim();
|
|
243
|
-
if (!raw) return "";
|
|
244
|
-
return raw.length > max ? raw.slice(0, max - 1).trim() + "…" : raw;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function captureSourceLabel(source, channel) {
|
|
248
|
-
const s = String(source || "").toLowerCase();
|
|
249
|
-
const c = String(channel || "").toLowerCase();
|
|
250
|
-
if (s.includes("telegram") || c.includes("telegram") || c.includes("chat")) return "Chat";
|
|
251
|
-
if (s.includes("email") || c.includes("mail")) return "Email";
|
|
252
|
-
if (s.includes("browser")) return "Browser";
|
|
253
|
-
if (s.includes("brief")) return "Brief";
|
|
254
|
-
if (s.includes("manual")) return "Manual";
|
|
255
|
-
return source ? String(source).replace(/[-_]+/g, " ").replace(/\b\w/g, m => m.toUpperCase()) : "Capture";
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function formatCaptureDisplayTime(value) {
|
|
259
|
-
const d = value ? new Date(value) : new Date();
|
|
260
|
-
if (!Number.isFinite(d.getTime())) return "";
|
|
261
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
262
|
-
return `${pad(d.getDate())}.${pad(d.getMonth() + 1)}.${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function formatCaptureFileTime(value) {
|
|
266
|
-
const d = value ? new Date(value) : new Date();
|
|
267
|
-
if (!Number.isFinite(d.getTime())) return "";
|
|
268
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
269
|
-
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}-${pad(d.getHours())}-${pad(d.getMinutes())}`;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function slugFilePart(value, max = 96) {
|
|
273
|
-
const s = String(value || "")
|
|
274
|
-
.normalize("NFKD")
|
|
275
|
-
.replace(/[\u0300-\u036f]/g, "")
|
|
276
|
-
.toLowerCase()
|
|
277
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
278
|
-
.replace(/^-+|-+$/g, "")
|
|
279
|
-
.slice(0, max)
|
|
280
|
-
.replace(/-+$/g, "");
|
|
281
|
-
return s || "media";
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function buildMediaTitle(input = {}) {
|
|
285
|
-
const explicit = input.title || input.meta && input.meta.title || input.payload && input.payload.title;
|
|
286
|
-
if (explicit) return compactTitleText(explicit, 160);
|
|
287
|
-
const sourceLabel = captureSourceLabel(input.source, input.channel);
|
|
288
|
-
const stamp = formatCaptureDisplayTime(input.occurred_at);
|
|
289
|
-
const context = compactTitleText(
|
|
290
|
-
input.context_text || input.content || input.text ||
|
|
291
|
-
input.meta && (input.meta.context_text || input.meta.caption || input.meta.message_text || input.meta.notes) ||
|
|
292
|
-
input.payload && (input.payload.context_text || input.payload.caption || input.payload.message_text || input.payload.notes) ||
|
|
293
|
-
"",
|
|
294
|
-
100
|
|
295
|
-
);
|
|
296
|
-
const fallback = compactTitleText([input.project, input.media_kind, input.route, input.page_url, input.file_name].filter(Boolean).join(" "), 100);
|
|
297
|
-
return [sourceLabel, stamp, context || fallback].filter(Boolean).join(" ").trim();
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function buildCanonicalMediaFileName(input = {}) {
|
|
301
|
-
const ext = String(input.file_ext || extensionName(input.file_name || input.media_path) || "asset").toLowerCase();
|
|
302
|
-
const title = input.title || buildMediaTitle(input);
|
|
303
|
-
const stamp = formatCaptureFileTime(input.occurred_at);
|
|
304
|
-
const label = captureSourceLabel(input.source, input.channel);
|
|
305
|
-
const source = slugFilePart(label, 24);
|
|
306
|
-
const body = slugFilePart(title.replace(formatCaptureDisplayTime(input.occurred_at), "").replace(new RegExp("^" + label + "\\s*", "i"), ""), 100);
|
|
307
|
-
const base = [source, stamp, body].filter(Boolean).join("-");
|
|
308
|
-
return `${base || "media"}${ext ? "." + ext.replace(/^\./, "") : ""}`;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// --- Contract validation constants ---
|
|
312
|
-
|
|
313
|
-
const AUTH_CONTRACT_REQUIRED_FIELDS = ["status", "mode", "provider", "canonical_project", "canonical_login_url", "shared_identity_scope", "shared_accounts"];
|
|
314
|
-
const UI_CONTRACT_REQUIRED_FIELDS = [
|
|
315
|
-
"status",
|
|
316
|
-
"canonical_brand_project",
|
|
317
|
-
"canonical_header_project",
|
|
318
|
-
"canonical_button_project",
|
|
319
|
-
"canonical_font_source_project",
|
|
320
|
-
"canonical_font_display",
|
|
321
|
-
"canonical_font_body",
|
|
322
|
-
"canonical_logo_light_asset",
|
|
323
|
-
"canonical_logo_dark_asset",
|
|
324
|
-
"canonical_logo_size_rule",
|
|
325
|
-
"canonical_button_size_rule",
|
|
326
|
-
"light_mode_required",
|
|
327
|
-
"dark_mode_required",
|
|
328
|
-
"shared_ui_family",
|
|
329
|
-
"no_local_ui_interpretation"
|
|
330
|
-
];
|
|
331
|
-
|
|
332
|
-
// --- Sensitivity detectors ---
|
|
333
|
-
|
|
334
|
-
function authSensitiveTask(input) {
|
|
335
|
-
const text = [
|
|
336
|
-
input && input.task,
|
|
337
|
-
input && input.summary,
|
|
338
|
-
Array.isArray(input && input.topics) ? input.topics.join(" ") : "",
|
|
339
|
-
Array.isArray(input && input.files) ? input.files.join(" ") : "",
|
|
340
|
-
input && input.action_type
|
|
341
|
-
].filter(Boolean).join(" ");
|
|
342
|
-
return /\b(auth|login|sso|signup|signin|sign-in|session|cookie|oauth|password|reset|forgot|verify|onboarding|account)\b/i.test(text);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
function uiSensitiveTask(input) {
|
|
346
|
-
const text = [
|
|
347
|
-
input && input.task,
|
|
348
|
-
input && input.summary,
|
|
349
|
-
Array.isArray(input && input.topics) ? input.topics.join(" ") : "",
|
|
350
|
-
Array.isArray(input && input.files) ? input.files.join(" ") : "",
|
|
351
|
-
input && input.action_type
|
|
352
|
-
].filter(Boolean).join(" ");
|
|
353
|
-
return /\b(ui|frontend|header|headder|footer|menu|menue|nav|navigation|button|buttons|theme|light|dark|logo|style|design|layout|mobile|responsive|landing)\b/i.test(text);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function wizardTargetGate(input, projectRules) {
|
|
357
|
-
const rules = projectRules && !projectRules.error ? projectRules : {};
|
|
358
|
-
const requiredGates = parseMaybeJson(rules.required_gates, []) || [];
|
|
359
|
-
const deployRules = parseMaybeJson(rules.deploy_rules, {}) || {};
|
|
360
|
-
const canonicalNav = parseMaybeJson(rules.canonical_nav, {}) || {};
|
|
361
|
-
const required = Boolean(
|
|
362
|
-
deployRules.wizard_target_required
|
|
363
|
-
|| deployRules.ambiguous_wizard_task_blocks
|
|
364
|
-
|| requiredGates.includes("explicit_wizard_target")
|
|
365
|
-
|| (canonicalNav.builder_v1 && canonicalNav.builder_v2)
|
|
366
|
-
);
|
|
367
|
-
if (!required) return { required: false, status: "ok", reason: "wizard target gate not configured" };
|
|
368
|
-
|
|
369
|
-
const resourceText = Array.isArray(input && input.resources)
|
|
370
|
-
? input.resources.map((resource) => [
|
|
371
|
-
resource && resource.resource_key,
|
|
372
|
-
resource && resource.resource_kind,
|
|
373
|
-
resource && resource.label,
|
|
374
|
-
resource && resource.name
|
|
375
|
-
].filter(Boolean).join(" ")).join(" ")
|
|
376
|
-
: "";
|
|
377
|
-
const text = [
|
|
378
|
-
input && input.project,
|
|
379
|
-
input && input.task,
|
|
380
|
-
input && input.summary,
|
|
381
|
-
input && input.scope,
|
|
382
|
-
input && input.action_type,
|
|
383
|
-
input && input.tool_name,
|
|
384
|
-
Array.isArray(input && input.topics) ? input.topics.join(" ") : "",
|
|
385
|
-
Array.isArray(input && input.files) ? input.files.join(" ") : "",
|
|
386
|
-
Array.isArray(input && input.routes) ? input.routes.join(" ") : "",
|
|
387
|
-
Array.isArray(input && input.urls) ? input.urls.join(" ") : "",
|
|
388
|
-
Array.isArray(input && input.system_names) ? input.system_names.join(" ") : "",
|
|
389
|
-
resourceText
|
|
390
|
-
].filter(Boolean).join(" ").toLowerCase();
|
|
391
|
-
|
|
392
|
-
const mentionsWizard = /\bwiz(?:ard|rad)\b/.test(text)
|
|
393
|
-
|| /\bwiz(?:ard|rad)\s*[12]\b/.test(text)
|
|
394
|
-
|| /\bbuilder[_-]?v?[12]\b/.test(text)
|
|
395
|
-
|| /apps\.example(?:corp)?(?:\.ai)?:builder[_-]?v?[12]\b/.test(text)
|
|
396
|
-
|| /\/dashboard\/builder[_-]?v?[12]\b/.test(text);
|
|
397
|
-
if (!mentionsWizard) return { required: true, status: "ok", reason: "no wizard surface mentioned" };
|
|
398
|
-
|
|
399
|
-
const builder_v1 = /\bwiz(?:ard|rad)\s*1\b/.test(text)
|
|
400
|
-
|| /\bwiz(?:ard|rad)1\b/.test(text)
|
|
401
|
-
|| /\bbuilder[_-]?v?1\b/.test(text)
|
|
402
|
-
|| /apps\.example(?:corp)?(?:\.ai)?:builder[_-]?v?1\b/.test(text)
|
|
403
|
-
|| /\/dashboard\/builder[_-]?v?1\b/.test(text);
|
|
404
|
-
const builder_v2 = /\bwiz(?:ard|rad)\s*2\b/.test(text)
|
|
405
|
-
|| /\bwiz(?:ard|rad)2\b/.test(text)
|
|
406
|
-
|| /\bbuilder[_-]?v?2\b/.test(text)
|
|
407
|
-
|| /apps\.example(?:corp)?(?:\.ai)?:builder[_-]?v?2\b/.test(text)
|
|
408
|
-
|| /\/dashboard\/builder[_-]?v?2\b/.test(text);
|
|
409
|
-
|
|
410
|
-
if (builder_v1 && builder_v2) {
|
|
411
|
-
return {
|
|
412
|
-
required: true,
|
|
413
|
-
status: "block",
|
|
414
|
-
reason: "multiple wizard targets mentioned; create separate Work Orders for builder_v1 and builder_v2",
|
|
415
|
-
target: "mixed"
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
if (builder_v1 || builder_v2) {
|
|
419
|
-
return {
|
|
420
|
-
required: true,
|
|
421
|
-
status: "ok",
|
|
422
|
-
reason: "explicit wizard target present",
|
|
423
|
-
target: builder_v2 ? "apps.example:builder_v2" : "apps.example:builder_v1"
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
return {
|
|
427
|
-
required: true,
|
|
428
|
-
status: "block",
|
|
429
|
-
reason: "ambiguous wizard target; task must explicitly name apps.example:builder_v1 or apps.example:builder_v2",
|
|
430
|
-
target: null
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// --- Contract report builders ---
|
|
435
|
-
// These take an ensureTables callback so callers wire in their own schema bootstrap.
|
|
436
|
-
|
|
437
|
-
function authContractReport(tdb, project, ensureTables) {
|
|
438
|
-
if (ensureTables) ensureTables(tdb);
|
|
439
|
-
if (!project) return { error: "project required" };
|
|
440
|
-
const row = tdb.prepare("SELECT project, auth_matrix, updated_at, updated_by FROM project_rules WHERE project=?").get(project);
|
|
441
|
-
if (!row) return { error: "project_rules_missing", project, blockers: ["project rules missing"], hint: "Set auth_matrix in mem_project_rules_set before auth/login work." };
|
|
442
|
-
const contract = parseMaybeJson(row.auth_matrix, {}) || {};
|
|
443
|
-
const missing = [];
|
|
444
|
-
if (!contract || typeof contract !== "object" || Array.isArray(contract)) {
|
|
445
|
-
missing.push("auth_matrix");
|
|
446
|
-
} else {
|
|
447
|
-
for (const field of AUTH_CONTRACT_REQUIRED_FIELDS) {
|
|
448
|
-
const value = contract[field];
|
|
449
|
-
if (value === undefined || value === null || value === "" || value === "unknown") missing.push(field);
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
const canonicalProject = contract.canonical_project || project;
|
|
453
|
-
const identityScope = contract.shared_identity_scope || null;
|
|
454
|
-
const peers = [];
|
|
455
|
-
const mismatches = [];
|
|
456
|
-
const rows = tdb.prepare("SELECT project, auth_matrix FROM project_rules WHERE auth_matrix IS NOT NULL").all();
|
|
457
|
-
for (const peerRow of rows) {
|
|
458
|
-
const peer = parseMaybeJson(peerRow.auth_matrix, {}) || {};
|
|
459
|
-
if (!peer || typeof peer !== "object" || Array.isArray(peer)) continue;
|
|
460
|
-
const sameCanonical = (peer.canonical_project || peerRow.project) === canonicalProject;
|
|
461
|
-
const sameScope = identityScope && peer.shared_identity_scope && peer.shared_identity_scope === identityScope;
|
|
462
|
-
const linkedPortal = Array.isArray(contract.portals) && contract.portals.includes(peerRow.project);
|
|
463
|
-
if (!(sameCanonical || sameScope || linkedPortal || peerRow.project === project)) continue;
|
|
464
|
-
peers.push({
|
|
465
|
-
project: peerRow.project,
|
|
466
|
-
provider: peer.provider || null,
|
|
467
|
-
canonical_login_url: peer.canonical_login_url || null,
|
|
468
|
-
shared_identity_scope: peer.shared_identity_scope || null,
|
|
469
|
-
session_cookie_scope: peer.session_cookie_scope || null,
|
|
470
|
-
shared_accounts: peer.shared_accounts
|
|
471
|
-
});
|
|
472
|
-
if (peerRow.project === project) continue;
|
|
473
|
-
for (const field of ["provider", "canonical_login_url", "shared_identity_scope", "session_cookie_scope", "shared_accounts"]) {
|
|
474
|
-
const here = contract[field];
|
|
475
|
-
const there = peer[field];
|
|
476
|
-
if (here !== undefined && there !== undefined && here !== null && there !== null && JSON.stringify(here) !== JSON.stringify(there)) {
|
|
477
|
-
mismatches.push({ peer_project: peerRow.project, field, expected: here, actual: there });
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
const blockers = [];
|
|
482
|
-
if (contract.status === "unknown" || contract.status === "draft" || contract.status == null) blockers.push("auth contract status is not active");
|
|
483
|
-
if (missing.length) blockers.push("auth contract missing required fields: " + missing.join(", "));
|
|
484
|
-
if (mismatches.length) blockers.push("auth contract mismatch across linked portals: " + mismatches.map(m => `${m.peer_project}.${m.field}`).join(", "));
|
|
485
|
-
return {
|
|
486
|
-
ok: blockers.length === 0,
|
|
487
|
-
status: blockers.length ? "block" : "ok",
|
|
488
|
-
project,
|
|
489
|
-
canonical_project: canonicalProject,
|
|
490
|
-
contract,
|
|
491
|
-
missing,
|
|
492
|
-
mismatches,
|
|
493
|
-
peers,
|
|
494
|
-
blockers,
|
|
495
|
-
hint: blockers.length ? "Do not change login/SSO until the canonical auth contract matches across linked portals." : "Canonical auth contract is consistent."
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
function uiContractReport(tdb, project, ensureTables) {
|
|
500
|
-
if (ensureTables) ensureTables(tdb);
|
|
501
|
-
if (!project) return { error: "project required" };
|
|
502
|
-
const row = tdb.prepare("SELECT project, canonical_nav, design_rules, updated_at, updated_by FROM project_rules WHERE project=?").get(project);
|
|
503
|
-
if (!row) return { error: "project_rules_missing", project, blockers: ["project rules missing"], hint: "Set canonical_nav + design_rules before frontend/header/button work." };
|
|
504
|
-
const design = parseMaybeJson(row.design_rules, {}) || {};
|
|
505
|
-
const nav = parseMaybeJson(row.canonical_nav, null);
|
|
506
|
-
const missing = [];
|
|
507
|
-
if (!design || typeof design !== "object" || Array.isArray(design)) {
|
|
508
|
-
missing.push("design_rules");
|
|
509
|
-
} else {
|
|
510
|
-
for (const field of UI_CONTRACT_REQUIRED_FIELDS) {
|
|
511
|
-
const value = design[field];
|
|
512
|
-
if (value === undefined || value === null || value === "" || value === "unknown") missing.push(field);
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
const navItems = Array.isArray(nav) ? nav : [nav?.primary, nav?.items, nav?.menu, nav?.links].find(items => Array.isArray(items)) || [];
|
|
516
|
-
if (navItems.length === 0) missing.push("canonical_nav");
|
|
517
|
-
const family = design.shared_ui_family || null;
|
|
518
|
-
const peers = [];
|
|
519
|
-
const mismatches = [];
|
|
520
|
-
const rows = tdb.prepare("SELECT project, design_rules, canonical_nav FROM project_rules WHERE design_rules IS NOT NULL").all();
|
|
521
|
-
for (const peerRow of rows) {
|
|
522
|
-
const peerDesign = parseMaybeJson(peerRow.design_rules, {}) || {};
|
|
523
|
-
if (!peerDesign || typeof peerDesign !== "object" || Array.isArray(peerDesign)) continue;
|
|
524
|
-
const sameFamily = family && peerDesign.shared_ui_family === family;
|
|
525
|
-
const linkedPortal = Array.isArray(design.portals) && design.portals.includes(peerRow.project);
|
|
526
|
-
if (!(sameFamily || linkedPortal || peerRow.project === project)) continue;
|
|
527
|
-
peers.push({
|
|
528
|
-
project: peerRow.project,
|
|
529
|
-
canonical_header_project: peerDesign.canonical_header_project || null,
|
|
530
|
-
canonical_button_project: peerDesign.canonical_button_project || null,
|
|
531
|
-
canonical_brand_project: peerDesign.canonical_brand_project || null,
|
|
532
|
-
canonical_font_source_project: peerDesign.canonical_font_source_project || null,
|
|
533
|
-
canonical_font_display: peerDesign.canonical_font_display || null,
|
|
534
|
-
canonical_font_body: peerDesign.canonical_font_body || null,
|
|
535
|
-
canonical_logo_light_asset: peerDesign.canonical_logo_light_asset || null,
|
|
536
|
-
canonical_logo_dark_asset: peerDesign.canonical_logo_dark_asset || null,
|
|
537
|
-
canonical_logo_size_rule: peerDesign.canonical_logo_size_rule || null,
|
|
538
|
-
canonical_button_size_rule: peerDesign.canonical_button_size_rule || null,
|
|
539
|
-
light_mode_required: peerDesign.light_mode_required,
|
|
540
|
-
dark_mode_required: peerDesign.dark_mode_required,
|
|
541
|
-
shared_ui_family: peerDesign.shared_ui_family || null,
|
|
542
|
-
no_local_ui_interpretation: peerDesign.no_local_ui_interpretation
|
|
543
|
-
});
|
|
544
|
-
if (peerRow.project === project) continue;
|
|
545
|
-
for (const field of [
|
|
546
|
-
"canonical_brand_project",
|
|
547
|
-
"canonical_header_project",
|
|
548
|
-
"canonical_button_project",
|
|
549
|
-
"canonical_font_source_project",
|
|
550
|
-
"canonical_font_display",
|
|
551
|
-
"canonical_font_body",
|
|
552
|
-
"canonical_logo_light_asset",
|
|
553
|
-
"canonical_logo_dark_asset",
|
|
554
|
-
"canonical_logo_size_rule",
|
|
555
|
-
"canonical_button_size_rule",
|
|
556
|
-
"light_mode_required",
|
|
557
|
-
"dark_mode_required",
|
|
558
|
-
"shared_ui_family",
|
|
559
|
-
"no_local_ui_interpretation"
|
|
560
|
-
]) {
|
|
561
|
-
const here = design[field];
|
|
562
|
-
const there = peerDesign[field];
|
|
563
|
-
if (here !== undefined && there !== undefined && here !== null && there !== null && JSON.stringify(here) !== JSON.stringify(there)) {
|
|
564
|
-
mismatches.push({ peer_project: peerRow.project, field, expected: here, actual: there });
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
const blockers = [];
|
|
569
|
-
if (design.status === "unknown" || design.status === "draft" || design.status == null) blockers.push("ui contract status is not active");
|
|
570
|
-
if (missing.length) blockers.push("ui contract missing required fields: " + missing.join(", "));
|
|
571
|
-
if (mismatches.length) blockers.push("ui contract mismatch across linked portals: " + mismatches.map(m => `${m.peer_project}.${m.field}`).join(", "));
|
|
572
|
-
if (design.no_local_ui_interpretation !== true) blockers.push("ui contract must explicitly forbid local reinterpretation");
|
|
573
|
-
return {
|
|
574
|
-
ok: blockers.length === 0,
|
|
575
|
-
status: blockers.length ? "block" : "ok",
|
|
576
|
-
project,
|
|
577
|
-
contract: design,
|
|
578
|
-
nav_items_count: navItems.length,
|
|
579
|
-
missing,
|
|
580
|
-
mismatches,
|
|
581
|
-
peers,
|
|
582
|
-
blockers,
|
|
583
|
-
hint: blockers.length ? "Do not change header/buttons/theme until the canonical UI contract matches example.com across linked portals." : "Canonical UI contract is consistent."
|
|
584
|
-
};
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
// --- Reminder pure helpers ---
|
|
588
|
-
|
|
589
|
-
function normalizeReminderText(text) {
|
|
590
|
-
return String(text || "")
|
|
591
|
-
.toLowerCase()
|
|
592
|
-
.normalize("NFD")
|
|
593
|
-
.replace(/[\u0300-\u036f]/g, "")
|
|
594
|
-
.replace(/ß/g, "ss");
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
function parseReminderTime(norm) {
|
|
598
|
-
let m = norm.match(/\b(?:um\s*)?(\d{1,2})[:.](\d{2})\b/);
|
|
599
|
-
if (m) {
|
|
600
|
-
const hour = parseInt(m[1], 10);
|
|
601
|
-
const minute = parseInt(m[2], 10);
|
|
602
|
-
if (hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59) return { hour, minute, explicit: true };
|
|
603
|
-
}
|
|
604
|
-
m = norm.match(/\bum\s+(\d{1,2})(?:\s*uhr)?(?:\s*(\d{1,2}))?\b/) || norm.match(/\b(\d{1,2})\s*uhr(?:\s*(\d{1,2}))?\b/);
|
|
605
|
-
if (m) {
|
|
606
|
-
const hour = parseInt(m[1], 10);
|
|
607
|
-
const minute = m[2] != null ? parseInt(m[2], 10) : 0;
|
|
608
|
-
if (hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59) return { hour, minute, explicit: true };
|
|
609
|
-
}
|
|
610
|
-
return { hour: 9, minute: 0, explicit: false };
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
function applyReminderTime(date, time) {
|
|
614
|
-
const d = new Date(date.getTime());
|
|
615
|
-
d.setHours(time.hour, time.minute, 0, 0);
|
|
616
|
-
return d;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
function parseReminderDue(text, baseTime) {
|
|
620
|
-
const raw = String(text || "");
|
|
621
|
-
const norm = normalizeReminderText(raw);
|
|
622
|
-
const base = baseTime ? new Date(baseTime) : new Date();
|
|
623
|
-
const start = Number.isNaN(base.getTime()) ? new Date() : base;
|
|
624
|
-
const time = parseReminderTime(norm);
|
|
625
|
-
const finish = (date, dueText, precision) => ({
|
|
626
|
-
due_at: applyReminderTime(date, time).toISOString(),
|
|
627
|
-
due_text: dueText,
|
|
628
|
-
due_precision: time.explicit ? "datetime" : precision,
|
|
629
|
-
confidence: precision === "unknown" ? "low" : (precision === "week" ? "medium" : "high"),
|
|
630
|
-
});
|
|
631
|
-
let m = raw.match(/\b(\d{4}-\d{2}-\d{2})(?:[ T](\d{1,2})(?::(\d{2}))?)?\b/);
|
|
632
|
-
if (m) {
|
|
633
|
-
const d = new Date(m[1] + "T00:00:00");
|
|
634
|
-
const t = m[2] ? { hour: parseInt(m[2], 10), minute: m[3] ? parseInt(m[3], 10) : 0, explicit: true } : time;
|
|
635
|
-
return { due_at: applyReminderTime(d, t).toISOString(), due_text: m[0], due_precision: t.explicit ? "datetime" : "date", confidence: "high" };
|
|
636
|
-
}
|
|
637
|
-
m = norm.match(/\b(\d{1,2})\.(\d{1,2})(?:\.(\d{2,4}))?\b/);
|
|
638
|
-
if (m) {
|
|
639
|
-
let year = m[3] ? parseInt(m[3], 10) : start.getFullYear();
|
|
640
|
-
if (year < 100) year += 2000;
|
|
641
|
-
let d = new Date(year, parseInt(m[2], 10) - 1, parseInt(m[1], 10));
|
|
642
|
-
if (!m[3] && d.getTime() < start.getTime() - 86400000) d = new Date(year + 1, parseInt(m[2], 10) - 1, parseInt(m[1], 10));
|
|
643
|
-
if (!Number.isNaN(d.getTime())) return finish(d, m[0], "date");
|
|
644
|
-
}
|
|
645
|
-
m = norm.match(/\bin\s+(\d+)\s*(minuten?|mins?|stunden?|hours?|tage?|days?|wochen?|weeks?)\b/);
|
|
646
|
-
if (m) {
|
|
647
|
-
const n = parseInt(m[1], 10);
|
|
648
|
-
const unit = m[2];
|
|
649
|
-
const d = new Date(start.getTime());
|
|
650
|
-
if (/min/.test(unit)) d.setMinutes(d.getMinutes() + n);
|
|
651
|
-
else if (/stund|hour/.test(unit)) d.setHours(d.getHours() + n);
|
|
652
|
-
else if (/tag|day/.test(unit)) d.setDate(d.getDate() + n);
|
|
653
|
-
else d.setDate(d.getDate() + n * 7);
|
|
654
|
-
return { due_at: d.toISOString(), due_text: m[0], due_precision: "relative", confidence: "high" };
|
|
655
|
-
}
|
|
656
|
-
if (/\bubermorgen\b/.test(norm)) {
|
|
657
|
-
const d = new Date(start.getTime());
|
|
658
|
-
d.setDate(d.getDate() + 2);
|
|
659
|
-
return finish(d, "ubermorgen", "day");
|
|
660
|
-
}
|
|
661
|
-
if (/\bmorgen\b/.test(norm)) {
|
|
662
|
-
const d = new Date(start.getTime());
|
|
663
|
-
d.setDate(d.getDate() + 1);
|
|
664
|
-
return finish(d, "morgen", "day");
|
|
665
|
-
}
|
|
666
|
-
if (/\bheute\b/.test(norm)) return finish(start, "heute", "day");
|
|
667
|
-
const weekdays = [
|
|
668
|
-
{ day: 0, names: ["sonntag", "sunday"] },
|
|
669
|
-
{ day: 1, names: ["montag", "monday"] },
|
|
670
|
-
{ day: 2, names: ["dienstag", "tuesday"] },
|
|
671
|
-
{ day: 3, names: ["mittwoch", "wednesday"] },
|
|
672
|
-
{ day: 4, names: ["donnerstag", "thursday"] },
|
|
673
|
-
{ day: 5, names: ["freitag", "friday"] },
|
|
674
|
-
{ day: 6, names: ["samstag", "saturday"] },
|
|
675
|
-
];
|
|
676
|
-
for (const w of weekdays) {
|
|
677
|
-
const name = w.names.find((n) => new RegExp("\\b" + n + "\\b").test(norm));
|
|
678
|
-
if (!name) continue;
|
|
679
|
-
let delta = (w.day - start.getDay() + 7) % 7;
|
|
680
|
-
if (delta === 0 || /\b(nachste|naechste|next)\b/.test(norm)) delta += 7;
|
|
681
|
-
const d = new Date(start.getTime());
|
|
682
|
-
d.setDate(d.getDate() + delta);
|
|
683
|
-
return finish(d, name, /\bwoche|week\b/.test(norm) ? "week" : "day");
|
|
684
|
-
}
|
|
685
|
-
if (/\b(nachste|naechste|next)\s+woche\b/.test(norm)) {
|
|
686
|
-
const d = new Date(start.getTime());
|
|
687
|
-
let delta = (1 - start.getDay() + 7) % 7;
|
|
688
|
-
if (delta === 0) delta = 7;
|
|
689
|
-
d.setDate(d.getDate() + delta);
|
|
690
|
-
return finish(d, "nachste woche", "week");
|
|
691
|
-
}
|
|
692
|
-
return { due_at: null, due_text: null, due_precision: "unknown", confidence: "low" };
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
function reminderTitleFromText(text) {
|
|
696
|
-
const cleaned = String(text || "").replace(/\s+/g, " ").trim();
|
|
697
|
-
return cleaned ? cleaned.slice(0, 180) : "Reminder";
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
function reminderRow(row) {
|
|
701
|
-
if (!row) return null;
|
|
702
|
-
return Object.assign({}, row, { meta: parseMetaJson(row.meta_json) });
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
// --- Readiness / flag helpers ---
|
|
706
|
-
|
|
707
|
-
function boolFlag(value, fallback = false) {
|
|
708
|
-
if (value === undefined || value === null || value === "") return fallback;
|
|
709
|
-
if (typeof value === "boolean") return value;
|
|
710
|
-
if (typeof value === "number") return value !== 0;
|
|
711
|
-
const normalized = String(value).trim().toLowerCase();
|
|
712
|
-
if (["1", "true", "yes", "y", "on", "enabled"].includes(normalized)) return true;
|
|
713
|
-
if (["0", "false", "no", "n", "off", "disabled"].includes(normalized)) return false;
|
|
714
|
-
return fallback;
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
function isoAgeDays(isoText) {
|
|
718
|
-
const ms = Date.parse(String(isoText || ""));
|
|
719
|
-
if (!Number.isFinite(ms)) return null;
|
|
720
|
-
return Math.max(0, Math.floor((Date.now() - ms) / 86400000));
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
function freshnessFromAgeDays(ageDays, warnDays, criticalDays) {
|
|
724
|
-
if (ageDays == null) return "unknown";
|
|
725
|
-
if (ageDays >= criticalDays) return "critical";
|
|
726
|
-
if (ageDays >= warnDays) return "stale";
|
|
727
|
-
return "fresh";
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
function capabilityMatrixForDepartments(departments) {
|
|
731
|
-
const set = new Set((departments || []).map((name) => String(name || "").toLowerCase()));
|
|
732
|
-
return {
|
|
733
|
-
read: true,
|
|
734
|
-
report: true,
|
|
735
|
-
edit: set.size > 0,
|
|
736
|
-
deploy: set.has("deploy-ops") || set.has("strategy-review"),
|
|
737
|
-
billing: set.has("billing") || set.has("strategy-review"),
|
|
738
|
-
auth: set.has("backend") || set.has("strategy-review"),
|
|
739
|
-
production: set.has("deploy-ops") || set.has("strategy-review"),
|
|
740
|
-
};
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
module.exports = {
|
|
744
|
-
parseMaybeJson, deepMergePlain, uniqueIntegers,
|
|
745
|
-
stripPrivate, parseAgentCsv, normalizeAgentName,
|
|
746
|
-
jsonSafe, compactContent, parseMetaJson, isoOrNull, parseBriefTitle,
|
|
747
|
-
// Brief contract
|
|
748
|
-
TEAM_BRIEF_ALIASES, BRIEF_CONTRACT_VERSION, BRIEF_REQUIRED_HEADINGS,
|
|
749
|
-
cleanScope, uniqueAgentNames, isTeamBriefTarget,
|
|
750
|
-
hasCanonicalBriefShape, normalizeBriefMeta, normalizeBriefContent,
|
|
751
|
-
// File / media
|
|
752
|
-
baseName, extensionName, inferMediaKind, inferMediaType, uniqueStrings, compactTitleText, captureSourceLabel, formatCaptureDisplayTime, formatCaptureFileTime, slugFilePart, buildMediaTitle, buildCanonicalMediaFileName,
|
|
753
|
-
// Readiness / flags
|
|
754
|
-
boolFlag, isoAgeDays, freshnessFromAgeDays, capabilityMatrixForDepartments,
|
|
755
|
-
// Contract validation
|
|
756
|
-
AUTH_CONTRACT_REQUIRED_FIELDS, UI_CONTRACT_REQUIRED_FIELDS,
|
|
757
|
-
authSensitiveTask, uiSensitiveTask,
|
|
758
|
-
wizardTargetGate,
|
|
759
|
-
authContractReport, uiContractReport,
|
|
760
|
-
// Reminder helpers
|
|
761
|
-
normalizeReminderText, parseReminderTime, applyReminderTime,
|
|
762
|
-
parseReminderDue, reminderTitleFromText, reminderRow,
|
|
763
|
-
};
|