@indigoai-us/hq-cli 5.288.0 → 5.290.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/CHANGELOG.md +22 -0
- package/assets/bot-workers/setup/skills/explain-hq.md +35 -0
- package/assets/bot-workers/setup/worker.yaml +25 -23
- package/assets/scaffold/core/scripts/handoff-finalize.sh +670 -0
- package/assets/scaffold/core/scripts/handoff-open-steps.sh +81 -0
- package/assets/scaffold/core/scripts/handoff-post.sh +187 -0
- package/assets/scaffold/core/scripts/qmd-reindex-bg.sh +40 -0
- package/dist/command-catalog.generated.d.ts +51 -0
- package/dist/command-catalog.generated.js +63 -0
- package/dist/commands/core-handoff.d.ts +22 -0
- package/dist/commands/core-handoff.js +1240 -0
- package/dist/commands/core-ontology.d.ts +6 -1
- package/dist/commands/core-ontology.js +42 -7
- package/dist/commands/core.js +8 -1
- package/dist/commands/lanes.js +157 -5
- package/dist/lib/bot/run.js +92 -2
- package/dist/lib/bot/setup-explain.d.ts +68 -0
- package/dist/lib/bot/setup-explain.js +141 -0
- package/dist/lib/checkpoint-ontology/entity-file.d.ts +21 -0
- package/dist/lib/checkpoint-ontology/entity-file.js +100 -0
- package/dist/lib/checkpoint-ontology/merge-copies.d.ts +34 -2
- package/dist/lib/checkpoint-ontology/merge-copies.js +541 -27
- package/dist/lib/lanes/store.d.ts +4 -2
- package/dist/lib/lanes/store.js +37 -15
- package/dist/lib/lanes/types.d.ts +6 -2
- package/dist/lib/lanes/types.js +1 -1
- package/dist/utils/self-update.d.ts +3 -0
- package/dist/utils/self-update.js +232 -1
- package/package.json +1 -1
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The setup bot's instant "Explain HQ first" answer.
|
|
3
|
+
*
|
|
4
|
+
* The setup bot's first question is whether to explain HQ or jump straight in,
|
|
5
|
+
* with "Explain HQ first" as a suggested reply. The explanation is fixed text,
|
|
6
|
+
* so a click on that button does not need a model turn: the runner sends the
|
|
7
|
+
* text from the setup worker's `skills/explain-hq.md` at once, and the bot's
|
|
8
|
+
* next turn starts with a note saying the explanation already went out.
|
|
9
|
+
*
|
|
10
|
+
* The text has one home, the worker file. The model reads the same file when
|
|
11
|
+
* someone asks for the explanation in their own words, so the two paths never
|
|
12
|
+
* drift. Anything unexpected (file missing or malformed, the question not
|
|
13
|
+
* pending, any other message) falls back to the normal model turn.
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from "node:fs";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
/** The suggested reply that triggers the instant answer. Exact match only. */
|
|
18
|
+
export const EXPLAIN_HQ_SUGGESTION = "Explain HQ first";
|
|
19
|
+
/** Where the explanation lives, relative to the setup worker folder. */
|
|
20
|
+
export const EXPLAIN_HQ_FILE = path.join("skills", "explain-hq.md");
|
|
21
|
+
/** The setup bot's progress note in its memory folder (setup worker.yaml). */
|
|
22
|
+
export const SETUP_PROGRESS_FILE = "setup-progress.md";
|
|
23
|
+
/** Runner-owned state, in the bot's own folder (never the model's memory). */
|
|
24
|
+
export const SETUP_INTRO_STATE_FILE = "setup-intro.json";
|
|
25
|
+
const EXPLANATION_HEADING = /^##[ \t]+The explanation[ \t]*$/m;
|
|
26
|
+
const CLOSING_HEADING = /^##[ \t]+After the explanation, when the app sends it[ \t]*$/m;
|
|
27
|
+
/**
|
|
28
|
+
* Put in front of the next message from the person, so the model knows the
|
|
29
|
+
* explanation went out without it. It never reaches the person.
|
|
30
|
+
*/
|
|
31
|
+
export const SETUP_EXPLAINED_NOTE = `[Note from the HQ app: the person clicked "${EXPLAIN_HQ_SUGGESTION}", and the app already sent them the explanation ` +
|
|
32
|
+
`from skills/explain-hq.md word for word, ending by asking whether to start the setup. The explain-or-jump question is answered. ` +
|
|
33
|
+
"Write `Intro: explained` in your progress note, do not explain HQ again, and carry on with the first unfinished setup step. " +
|
|
34
|
+
"Their reply to that message follows.]";
|
|
35
|
+
/** The text between a heading and the next `## ` heading (or the end). */
|
|
36
|
+
function sectionAfter(body, heading) {
|
|
37
|
+
const match = heading.exec(body);
|
|
38
|
+
if (!match)
|
|
39
|
+
return null;
|
|
40
|
+
const rest = body.slice(match.index + match[0].length);
|
|
41
|
+
const next = /^##[ \t]/m.exec(rest);
|
|
42
|
+
return (next ? rest.slice(0, next.index) : rest).trim();
|
|
43
|
+
}
|
|
44
|
+
/** True when the closing carries exactly one well-formed suggestions block. */
|
|
45
|
+
function hasSuggestionsBlock(closing) {
|
|
46
|
+
const fence = /^```hq-block\n(.+)\n```$/m.exec(closing);
|
|
47
|
+
if (!fence)
|
|
48
|
+
return false;
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(fence[1]);
|
|
51
|
+
const block = parsed.blocks?.[0];
|
|
52
|
+
return (parsed.v === 1 &&
|
|
53
|
+
block?.kind === "suggestions" &&
|
|
54
|
+
Array.isArray(block.items) &&
|
|
55
|
+
block.items.length >= 2 &&
|
|
56
|
+
block.items.every((item) => typeof item === "string" && item.trim().length > 0));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Parse the worker file. Null when it is not the shape the app can send. */
|
|
63
|
+
export function parseSetupExplainer(body) {
|
|
64
|
+
const text = body.replace(/\r\n/g, "\n");
|
|
65
|
+
const explanation = sectionAfter(text, EXPLANATION_HEADING);
|
|
66
|
+
const closing = sectionAfter(text, CLOSING_HEADING);
|
|
67
|
+
if (!explanation || !closing)
|
|
68
|
+
return null;
|
|
69
|
+
// The closing must end with the block (nothing after it) so the app shows the buttons.
|
|
70
|
+
if (!closing.endsWith("```") || !hasSuggestionsBlock(closing))
|
|
71
|
+
return null;
|
|
72
|
+
return { explanation, closing, reply: `${explanation}\n\n${closing}` };
|
|
73
|
+
}
|
|
74
|
+
/** Read the explanation from the setup worker folder the bot runs. */
|
|
75
|
+
export function loadSetupExplainer(hqRoot, workerDir) {
|
|
76
|
+
try {
|
|
77
|
+
return parseSetupExplainer(fs.readFileSync(path.resolve(hqRoot, workerDir, EXPLAIN_HQ_FILE), "utf8"));
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** What the model's progress note says about the opening question. */
|
|
84
|
+
export function readSetupProgressIntro(memoryDir) {
|
|
85
|
+
let note;
|
|
86
|
+
try {
|
|
87
|
+
note = fs.readFileSync(path.join(memoryDir, SETUP_PROGRESS_FILE), "utf8");
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
if (/^Status:[ \t]*finished/im.test(note))
|
|
93
|
+
return "finished";
|
|
94
|
+
if (/^Intro:[ \t]*(explained|skipped)\b/im.test(note))
|
|
95
|
+
return "answered";
|
|
96
|
+
if (/^Intro:[ \t]*asked\b/im.test(note))
|
|
97
|
+
return "asked";
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
export function readSetupIntroState(dir) {
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(dir, SETUP_INTRO_STATE_FILE), "utf8"));
|
|
103
|
+
return parsed && parsed.v === 1 ? parsed : { v: 1 };
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return { v: 1 };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export function patchSetupIntroState(dir, patch) {
|
|
110
|
+
const next = { ...readSetupIntroState(dir), ...patch, v: 1 };
|
|
111
|
+
for (const key of Object.keys(next)) {
|
|
112
|
+
if (next[key] === undefined)
|
|
113
|
+
delete next[key];
|
|
114
|
+
}
|
|
115
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
116
|
+
const dest = path.join(dir, SETUP_INTRO_STATE_FILE);
|
|
117
|
+
const tmp = `${dest}.tmp.${process.pid}`;
|
|
118
|
+
fs.writeFileSync(tmp, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
|
|
119
|
+
fs.renameSync(tmp, dest);
|
|
120
|
+
return next;
|
|
121
|
+
}
|
|
122
|
+
/** A bot reply that asks the explain-or-jump question (the words or the button). */
|
|
123
|
+
export function replyOffersExplain(reply) {
|
|
124
|
+
return reply.includes(`"${EXPLAIN_HQ_SUGGESTION}"`) || /explain HQ to you/i.test(reply);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The instant answer is due only when ALL of these hold: the message is exactly
|
|
128
|
+
* the button text with nothing attached, the question is pending (the bot's
|
|
129
|
+
* last reply offered it, or its note says it asked), and the explanation has
|
|
130
|
+
* not gone out yet (neither by the app nor, per the note, by the model).
|
|
131
|
+
*/
|
|
132
|
+
export function setupExplainDue(input) {
|
|
133
|
+
if (input.text !== EXPLAIN_HQ_SUGGESTION || input.attachments > 0)
|
|
134
|
+
return false;
|
|
135
|
+
if (input.state.explainedAt)
|
|
136
|
+
return false;
|
|
137
|
+
if (input.progress === "answered" || input.progress === "finished")
|
|
138
|
+
return false;
|
|
139
|
+
return Boolean(input.state.offeredAt) || input.progress === "asked";
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=setup-explain.js.map
|
|
@@ -66,6 +66,27 @@ export type PatchResult = {
|
|
|
66
66
|
* change, the file is returned unchanged so a rerun writes nothing.
|
|
67
67
|
*/
|
|
68
68
|
export declare function patchEntityFile(content: string, patch: EntityPatch): PatchResult;
|
|
69
|
+
/** A namespaced record in a source system, as the cloud schema's `external_refs` holds it. */
|
|
70
|
+
export interface ExternalRef {
|
|
71
|
+
system: string;
|
|
72
|
+
namespace: string;
|
|
73
|
+
objectType?: string;
|
|
74
|
+
recordId: string;
|
|
75
|
+
}
|
|
76
|
+
export interface IdentityPatch {
|
|
77
|
+
externalIds: Record<string, string>;
|
|
78
|
+
webDomains: string[];
|
|
79
|
+
/** When set, the whole `external_refs` list; when absent, the key is left alone. */
|
|
80
|
+
externalRefs?: ExternalRef[];
|
|
81
|
+
lastUpdated: string;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Set `external_ids` and `web_domains` on an existing, valid entity file.
|
|
85
|
+
* A key whose value is already equal is left as it is; an absent key is
|
|
86
|
+
* appended at the end of the front matter. Nothing else changes except
|
|
87
|
+
* `last_updated`. When neither key changes the file is returned unchanged.
|
|
88
|
+
*/
|
|
89
|
+
export declare function setIdentityKeys(content: string, patch: IdentityPatch): PatchResult;
|
|
69
90
|
export interface SignalBackRef {
|
|
70
91
|
signalType: string;
|
|
71
92
|
preview: string;
|
|
@@ -206,6 +206,106 @@ function checkPatched(original, next, aliases, signalCount, lastUpdated) {
|
|
|
206
206
|
return "patch_last_updated";
|
|
207
207
|
return null;
|
|
208
208
|
}
|
|
209
|
+
function renderExternalIds(ids) {
|
|
210
|
+
const entries = Object.entries(ids);
|
|
211
|
+
if (entries.length === 0)
|
|
212
|
+
return "external_ids: {}";
|
|
213
|
+
return ["external_ids:", ...entries.map(([key, value]) => ` ${key}: ${quote(value)}`)].join("\n");
|
|
214
|
+
}
|
|
215
|
+
function renderExternalRefs(refs) {
|
|
216
|
+
if (refs.length === 0)
|
|
217
|
+
return "external_refs: []";
|
|
218
|
+
const lines = ["external_refs:"];
|
|
219
|
+
for (const ref of refs) {
|
|
220
|
+
lines.push(` - system: ${quote(ref.system)}`, ` namespace: ${quote(ref.namespace)}`);
|
|
221
|
+
if (ref.objectType !== undefined)
|
|
222
|
+
lines.push(` objectType: ${quote(ref.objectType)}`);
|
|
223
|
+
lines.push(` recordId: ${quote(ref.recordId)}`);
|
|
224
|
+
}
|
|
225
|
+
return lines.join("\n");
|
|
226
|
+
}
|
|
227
|
+
function renderWebDomains(domains) {
|
|
228
|
+
if (domains.length === 0)
|
|
229
|
+
return "web_domains: []";
|
|
230
|
+
return ["web_domains:", ...domains.map((domain) => ` - ${quote(domain)}`)].join("\n");
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Set `external_ids` and `web_domains` on an existing, valid entity file.
|
|
234
|
+
* A key whose value is already equal is left as it is; an absent key is
|
|
235
|
+
* appended at the end of the front matter. Nothing else changes except
|
|
236
|
+
* `last_updated`. When neither key changes the file is returned unchanged.
|
|
237
|
+
*/
|
|
238
|
+
export function setIdentityKeys(content, patch) {
|
|
239
|
+
const match = FRONTMATTER_RE.exec(content);
|
|
240
|
+
const parsed = parseEntityFile(content);
|
|
241
|
+
if (!match || !parsed)
|
|
242
|
+
return { ok: false, reason: "unparseable" };
|
|
243
|
+
const before = validateEntityFrontmatter(parsed.frontmatter);
|
|
244
|
+
if (typeof before === "string")
|
|
245
|
+
return { ok: false, reason: `invalid_existing:${before}` };
|
|
246
|
+
for (const key of Object.keys(patch.externalIds)) {
|
|
247
|
+
if (!/^[A-Za-z0-9_-]+$/.test(key))
|
|
248
|
+
return { ok: false, reason: "external_ids_key" };
|
|
249
|
+
}
|
|
250
|
+
const currentIds = (parsed.frontmatter.external_ids ?? {});
|
|
251
|
+
const currentDomains = (parsed.frontmatter.web_domains ?? []);
|
|
252
|
+
const currentRefs = parsed.frontmatter.external_refs ?? [];
|
|
253
|
+
const idsChanged = !isDeepStrictEqual(currentIds, patch.externalIds);
|
|
254
|
+
const domainsChanged = !isDeepStrictEqual(currentDomains, patch.webDomains);
|
|
255
|
+
const refsChanged = patch.externalRefs !== undefined && !isDeepStrictEqual(currentRefs, patch.externalRefs);
|
|
256
|
+
if (!idsChanged && !domainsChanged && !refsChanged)
|
|
257
|
+
return { ok: true, content, changed: false };
|
|
258
|
+
const newline = content.includes("\r\n") ? "\r\n" : "\n";
|
|
259
|
+
let lines = match[1].split(/\r?\n/);
|
|
260
|
+
if (idsChanged) {
|
|
261
|
+
const rendered = renderExternalIds(patch.externalIds);
|
|
262
|
+
lines = replaceTopLevelKey(lines, "external_ids", rendered) ?? [...lines, ...rendered.split("\n")];
|
|
263
|
+
}
|
|
264
|
+
if (domainsChanged) {
|
|
265
|
+
const rendered = renderWebDomains(patch.webDomains);
|
|
266
|
+
lines = replaceTopLevelKey(lines, "web_domains", rendered) ?? [...lines, ...rendered.split("\n")];
|
|
267
|
+
}
|
|
268
|
+
if (refsChanged) {
|
|
269
|
+
const rendered = renderExternalRefs(patch.externalRefs);
|
|
270
|
+
lines = replaceTopLevelKey(lines, "external_refs", rendered) ?? [...lines, ...rendered.split("\n")];
|
|
271
|
+
}
|
|
272
|
+
const stamped = replaceTopLevelKey(lines, "last_updated", `last_updated: ${patch.lastUpdated}`);
|
|
273
|
+
if (!stamped)
|
|
274
|
+
return { ok: false, reason: "missing_owned_key" };
|
|
275
|
+
lines = stamped;
|
|
276
|
+
const head = content.slice(0, content.indexOf(match[1]));
|
|
277
|
+
const tail = content.slice(content.indexOf(match[1]) + match[1].length);
|
|
278
|
+
const next = `${head}${lines.join(newline)}${tail}`;
|
|
279
|
+
const reparsed = parseEntityFile(next);
|
|
280
|
+
if (!reparsed)
|
|
281
|
+
return { ok: false, reason: "patch_unparseable" };
|
|
282
|
+
if (typeof validateEntityFrontmatter(reparsed.frontmatter) === "string")
|
|
283
|
+
return { ok: false, reason: "patch_invalid" };
|
|
284
|
+
const keys = new Set([...Object.keys(parsed.frontmatter), ...Object.keys(reparsed.frontmatter)]);
|
|
285
|
+
for (const key of keys) {
|
|
286
|
+
if (key === "external_ids" || key === "web_domains" || key === "last_updated")
|
|
287
|
+
continue;
|
|
288
|
+
if (key === "external_refs" && patch.externalRefs !== undefined)
|
|
289
|
+
continue;
|
|
290
|
+
if (!isDeepStrictEqual(parsed.frontmatter[key], reparsed.frontmatter[key])) {
|
|
291
|
+
return { ok: false, reason: `patch_changed_key:${key}` };
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (!isDeepStrictEqual(reparsed.frontmatter.external_ids ?? {}, patch.externalIds)) {
|
|
295
|
+
return { ok: false, reason: "patch_external_ids" };
|
|
296
|
+
}
|
|
297
|
+
if (!isDeepStrictEqual(reparsed.frontmatter.web_domains ?? [], patch.webDomains)) {
|
|
298
|
+
return { ok: false, reason: "patch_web_domains" };
|
|
299
|
+
}
|
|
300
|
+
if (refsChanged && !isDeepStrictEqual(reparsed.frontmatter.external_refs ?? [], patch.externalRefs)) {
|
|
301
|
+
return { ok: false, reason: "patch_external_refs" };
|
|
302
|
+
}
|
|
303
|
+
if (reparsed.frontmatter.last_updated !== patch.lastUpdated)
|
|
304
|
+
return { ok: false, reason: "patch_last_updated" };
|
|
305
|
+
if (reparsed.body !== parsed.body)
|
|
306
|
+
return { ok: false, reason: "patch_body" };
|
|
307
|
+
return { ok: true, content: next, changed: true };
|
|
308
|
+
}
|
|
209
309
|
/** hq-pro `previewSignalContent`: first 80 characters, one line. */
|
|
210
310
|
export function previewSignalContent(canonicalContent) {
|
|
211
311
|
const collapsed = canonicalContent.replace(/\s+/g, " ").trim();
|
|
@@ -26,7 +26,11 @@
|
|
|
26
26
|
* merge cannot rewrite (a copy, a knowledge file, a signal) is held.
|
|
27
27
|
*
|
|
28
28
|
* A signal whose `entity_refs` names a copy holds that copy: signals are
|
|
29
|
-
* server-owned, so the ref cannot be re-pointed from here.
|
|
29
|
+
* server-owned, so the ref cannot be re-pointed from here. With `rewriteRefs`
|
|
30
|
+
* (flag `checkpoint.ontology.merge-rewrite-refs`) signal refs, invoice and
|
|
31
|
+
* bill links, source and knowledge links are re-pointed at the survivor, the
|
|
32
|
+
* survivor receives the copy's `external_ids` and `web_domains`, and only a
|
|
33
|
+
* link from another copy or an external id conflict holds a pair.
|
|
30
34
|
*
|
|
31
35
|
* With `pairKind: "variants"` (flag `checkpoint.ontology.merge-variants`) the
|
|
32
36
|
* pairs are two-file duplicate groups whose canonical names differ only in
|
|
@@ -42,6 +46,12 @@ export declare const MERGE_COPIES_FLAG = "checkpoint.ontology.merge-copies";
|
|
|
42
46
|
* names differ only in spacing, case or punctuation (CO-65).
|
|
43
47
|
*/
|
|
44
48
|
export declare const MERGE_VARIANTS_FLAG = "checkpoint.ontology.merge-variants";
|
|
49
|
+
/**
|
|
50
|
+
* `merge-copies --rewrite-refs`: re-point signal `entity_refs`, invoice and
|
|
51
|
+
* bill links, source and knowledge links from a copy to its survivor, and
|
|
52
|
+
* carry the copy's `external_ids` and `web_domains` to the survivor.
|
|
53
|
+
*/
|
|
54
|
+
export declare const MERGE_REWRITE_REFS_FLAG = "checkpoint.ontology.merge-rewrite-refs";
|
|
45
55
|
export type CopySkipReason = "no_original" | "original_invalid" | "identity_differs" | "not_in_duplicate_group" | "group_size" | "type_differs" | "names_differ" | "numbered_copy" | "not_listed";
|
|
46
56
|
export type PairKind = "numbered" | "variants";
|
|
47
57
|
export interface CopySkip {
|
|
@@ -55,10 +65,22 @@ export interface CopyPair {
|
|
|
55
65
|
/** Company-relative path of the original it merges into. */
|
|
56
66
|
original: string;
|
|
57
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* How to settle one provider's `external_ids` conflict: `primary` stays in
|
|
70
|
+
* `external_ids`, and the pair's other id moves to `external_refs` under
|
|
71
|
+
* `namespace` (the provider account or workspace) and `object_type`.
|
|
72
|
+
*/
|
|
73
|
+
export interface ExternalIdResolution {
|
|
74
|
+
primary: string;
|
|
75
|
+
namespace: string;
|
|
76
|
+
object_type?: string;
|
|
77
|
+
}
|
|
58
78
|
/** One entry of a `--only` list: an exact copy -> original pair. */
|
|
59
79
|
export interface ListedPair {
|
|
60
80
|
copy: string;
|
|
61
81
|
original: string;
|
|
82
|
+
/** Provider -> resolution, applied only when `rewriteRefs` is on and the ids conflict. */
|
|
83
|
+
resolve_external_ids?: Record<string, ExternalIdResolution>;
|
|
62
84
|
}
|
|
63
85
|
export interface CopyPlan extends CopyPair {
|
|
64
86
|
add_aliases: string[];
|
|
@@ -75,8 +97,14 @@ export interface CopyPlan extends CopyPair {
|
|
|
75
97
|
/** Signal files whose `entity_refs` name the copy. */
|
|
76
98
|
signal_refs: string[];
|
|
77
99
|
/** Why the copy cannot be merged or deleted from here. */
|
|
78
|
-
held?: "signal_refs" | "linked_from_unwritable" | "merge_invalid";
|
|
100
|
+
held?: "signal_refs" | "linked_from_unwritable" | "merge_invalid" | "external_id_conflict";
|
|
79
101
|
detail?: string;
|
|
102
|
+
/** Keys added to the survivor. Set only when `rewriteRefs` is on. */
|
|
103
|
+
add_external_ids?: string[];
|
|
104
|
+
/** Domains added to the survivor. Set only when `rewriteRefs` is on. */
|
|
105
|
+
add_web_domains?: string[];
|
|
106
|
+
/** `system:namespace:recordId` refs added to the survivor's `external_refs`. Set only when `rewriteRefs` is on. */
|
|
107
|
+
add_external_refs?: string[];
|
|
80
108
|
/** Body lines of the copy the merged original lacks; a copy with any is never deleted. */
|
|
81
109
|
unmerged_body_lines: number;
|
|
82
110
|
}
|
|
@@ -105,6 +133,8 @@ export interface MergeCopiesReport {
|
|
|
105
133
|
}[];
|
|
106
134
|
/** Listed entries the finder did not produce. Absent unless `--only` was set. */
|
|
107
135
|
only_unmatched?: ListedPair[];
|
|
136
|
+
/** Signal, source and knowledge files re-pointed at a survivor. Absent unless `rewriteRefs` was set. */
|
|
137
|
+
rewritten?: string[];
|
|
108
138
|
/** Entity-file links whose target file does not exist. */
|
|
109
139
|
broken_links_before: number;
|
|
110
140
|
broken_links_after: number;
|
|
@@ -123,6 +153,8 @@ export interface MergeCopiesOptions {
|
|
|
123
153
|
limit?: number;
|
|
124
154
|
/** Handle only these exact pairs; every other found pair is skipped as `not_listed`. */
|
|
125
155
|
only?: ListedPair[];
|
|
156
|
+
/** Re-point refs and links held today, and carry ids and domains (flag merge-rewrite-refs). */
|
|
157
|
+
rewriteRefs?: boolean;
|
|
126
158
|
now: Date;
|
|
127
159
|
deleteVaultKey?: DeleteVaultKey;
|
|
128
160
|
}
|