@indigoai-us/hq-cli 5.289.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 +18 -1
- 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/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 +7 -3
- package/dist/utils/self-update.d.ts +3 -0
- package/dist/utils/self-update.js +232 -1
- package/package.json +1 -1
|
@@ -33,6 +33,8 @@ export interface MergeCopiesCommandOptions {
|
|
|
33
33
|
variants?: boolean;
|
|
34
34
|
limit?: number;
|
|
35
35
|
only?: string;
|
|
36
|
+
/** Also re-point refs and links held today (flag merge-rewrite-refs). */
|
|
37
|
+
rewriteRefs?: boolean;
|
|
36
38
|
json?: boolean;
|
|
37
39
|
}
|
|
38
40
|
/** Reads a `--only` file: a JSON array of `{ copy, original }` company-relative paths. */
|
|
@@ -44,7 +46,10 @@ export declare function readListedPairs(file: string): ListedPair[];
|
|
|
44
46
|
* `checkpoint.ontology.merge-copies` and `checkpoint.ontology.multi-writer`.
|
|
45
47
|
* With `--variants`, merges two-file duplicate groups whose names differ only
|
|
46
48
|
* in case, spacing or punctuation instead, gated by
|
|
47
|
-
* `checkpoint.ontology.merge-variants`.
|
|
49
|
+
* `checkpoint.ontology.merge-variants`. `--rewrite-refs` (flag
|
|
50
|
+
* `checkpoint.ontology.merge-rewrite-refs`) also re-points signal refs,
|
|
51
|
+
* invoice and bill links, source and knowledge links, and carries external
|
|
52
|
+
* ids and web domains.
|
|
48
53
|
*/
|
|
49
54
|
export declare function runOntologyMergeCopies(hqRoot: string, slug: string | undefined, options: MergeCopiesCommandOptions, deps?: OntologyCommandDeps): Promise<string>;
|
|
50
55
|
export declare function registerCoreOntologyCommand(core: Command): void;
|
|
@@ -17,7 +17,7 @@ import { applyOntologyRun, ApplyError } from "../lib/checkpoint-ontology/apply.j
|
|
|
17
17
|
import { resolveOntologyCompany, WriteBoundsError } from "../lib/checkpoint-ontology/company.js";
|
|
18
18
|
import { findDuplicateGroups, summarizeMergeBack, } from "../lib/checkpoint-ontology/multi-writer.js";
|
|
19
19
|
import { buildEntityIndex } from "../lib/checkpoint-ontology/tree.js";
|
|
20
|
-
import { MERGE_COPIES_FLAG, MERGE_VARIANTS_FLAG, mergeCopies, } from "../lib/checkpoint-ontology/merge-copies.js";
|
|
20
|
+
import { MERGE_COPIES_FLAG, MERGE_REWRITE_REFS_FLAG, MERGE_VARIANTS_FLAG, mergeCopies, } from "../lib/checkpoint-ontology/merge-copies.js";
|
|
21
21
|
import { callDeleteEndpoint } from "./files.js";
|
|
22
22
|
import { resolveVaultCredential } from "../utils/resolve-vault-credential.js";
|
|
23
23
|
import { registerCoreOntologyNativeCommands } from "./core-ontology-native.js";
|
|
@@ -146,11 +146,12 @@ function mergeCopiesSummary(report) {
|
|
|
146
146
|
const held = report.plans.filter((plan) => plan.held).length;
|
|
147
147
|
const lines = [
|
|
148
148
|
`ontology merge-copies (${report.mode.replace("_", " ")}) in ${report.company} (${report.pair_kind}): ${report.plans.length} of ${report.copies_found} copies, ${held} held, ${report.skipped.length} skipped`,
|
|
149
|
-
` merged ${report.merged.length}, relinked ${report.relinked.length}, deleted ${report.deleted.length}, not ready ${report.not_ready.length}, errors ${report.errors.length}`,
|
|
149
|
+
` merged ${report.merged.length}, relinked ${report.relinked.length}${report.rewritten !== undefined ? `, rewritten ${report.rewritten.length}` : ""}, deleted ${report.deleted.length}, not ready ${report.not_ready.length}, errors ${report.errors.length}`,
|
|
150
150
|
` duplicate groups ${report.duplicate_groups_before} -> ${report.duplicate_groups_after}; broken entity links ${report.broken_links_before} -> ${report.broken_links_after}`,
|
|
151
151
|
];
|
|
152
|
-
for (const plan of report.plans.filter((entry) => entry.held))
|
|
153
|
-
lines.push(` held ${plan.copy}: ${plan.held}`);
|
|
152
|
+
for (const plan of report.plans.filter((entry) => entry.held)) {
|
|
153
|
+
lines.push(` held ${plan.copy}: ${plan.held}${plan.held === "external_id_conflict" ? ` (${plan.detail})` : ""}`);
|
|
154
|
+
}
|
|
154
155
|
for (const error of report.errors)
|
|
155
156
|
lines.push(` error ${error.path}: ${error.error}`);
|
|
156
157
|
for (const entry of report.only_unmatched ?? [])
|
|
@@ -176,7 +177,36 @@ export function readListedPairs(file) {
|
|
|
176
177
|
})) {
|
|
177
178
|
throw new OntologyUsageError("ontology: --only must be a non-empty JSON array of { copy, original } paths");
|
|
178
179
|
}
|
|
179
|
-
return parsed.map((entry) =>
|
|
180
|
+
return parsed.map((entry) => {
|
|
181
|
+
if (entry.resolve_external_ids === undefined)
|
|
182
|
+
return { copy: entry.copy, original: entry.original };
|
|
183
|
+
return { copy: entry.copy, original: entry.original, resolve_external_ids: readResolution(entry.copy, entry.resolve_external_ids) };
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
/** `{ provider: { primary, namespace, object_type? } }`, every value a non-empty string. */
|
|
187
|
+
function readResolution(copy, raw) {
|
|
188
|
+
const usage = `ontology: --only entry ${copy}: resolve_external_ids must map each provider to { primary, namespace, object_type? } strings`;
|
|
189
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw) || Object.keys(raw).length === 0)
|
|
190
|
+
throw new OntologyUsageError(usage);
|
|
191
|
+
const out = {};
|
|
192
|
+
for (const [provider, value] of Object.entries(raw)) {
|
|
193
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
194
|
+
throw new OntologyUsageError(usage);
|
|
195
|
+
const record = value;
|
|
196
|
+
const nonEmpty = (field) => typeof field === "string" && field.length > 0;
|
|
197
|
+
if (!Object.keys(record).every((key) => key === "primary" || key === "namespace" || key === "object_type") ||
|
|
198
|
+
!nonEmpty(record.primary) ||
|
|
199
|
+
!nonEmpty(record.namespace) ||
|
|
200
|
+
(record.object_type !== undefined && !nonEmpty(record.object_type))) {
|
|
201
|
+
throw new OntologyUsageError(usage);
|
|
202
|
+
}
|
|
203
|
+
out[provider] = {
|
|
204
|
+
primary: record.primary,
|
|
205
|
+
namespace: record.namespace,
|
|
206
|
+
...(record.object_type !== undefined ? { object_type: record.object_type } : {}),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
180
210
|
}
|
|
181
211
|
/**
|
|
182
212
|
* `hq core ontology merge-copies`: merge `<slug> <n>.md` copies into their
|
|
@@ -185,7 +215,10 @@ export function readListedPairs(file) {
|
|
|
185
215
|
* `checkpoint.ontology.merge-copies` and `checkpoint.ontology.multi-writer`.
|
|
186
216
|
* With `--variants`, merges two-file duplicate groups whose names differ only
|
|
187
217
|
* in case, spacing or punctuation instead, gated by
|
|
188
|
-
* `checkpoint.ontology.merge-variants`.
|
|
218
|
+
* `checkpoint.ontology.merge-variants`. `--rewrite-refs` (flag
|
|
219
|
+
* `checkpoint.ontology.merge-rewrite-refs`) also re-points signal refs,
|
|
220
|
+
* invoice and bill links, source and knowledge links, and carries external
|
|
221
|
+
* ids and web domains.
|
|
189
222
|
*/
|
|
190
223
|
export async function runOntologyMergeCopies(hqRoot, slug, options, deps = {}) {
|
|
191
224
|
if (options.apply && options.delete) {
|
|
@@ -200,7 +233,7 @@ export async function runOntologyMergeCopies(hqRoot, slug, options, deps = {}) {
|
|
|
200
233
|
throw new OntologyUsageError(`ontology: company rejected: ${resolution.reason}`);
|
|
201
234
|
const company = resolution.company;
|
|
202
235
|
const readFlag = deps.readFlag ?? registryFlagReader;
|
|
203
|
-
for (const flag of [options.variants ? MERGE_VARIANTS_FLAG : MERGE_COPIES_FLAG, MULTI_WRITER_FLAG]) {
|
|
236
|
+
for (const flag of [options.variants ? MERGE_VARIANTS_FLAG : MERGE_COPIES_FLAG, MULTI_WRITER_FLAG, ...(options.rewriteRefs ? [MERGE_REWRITE_REFS_FLAG] : [])]) {
|
|
204
237
|
if (!(await readFlag(flag, company))) {
|
|
205
238
|
return options.json ? JSON.stringify({ company: company.slug, skipped: `${flag} off` }) : `ontology merge-copies: skipped (${flag} is off)`;
|
|
206
239
|
}
|
|
@@ -220,6 +253,7 @@ export async function runOntologyMergeCopies(hqRoot, slug, options, deps = {}) {
|
|
|
220
253
|
...(options.variants ? { pairKind: "variants" } : {}),
|
|
221
254
|
...(options.limit !== undefined ? { limit: options.limit } : {}),
|
|
222
255
|
...(only ? { only } : {}),
|
|
256
|
+
...(options.rewriteRefs ? { rewriteRefs: true } : {}),
|
|
223
257
|
...(deleteVaultKey ? { deleteVaultKey } : {}),
|
|
224
258
|
});
|
|
225
259
|
return options.json ? JSON.stringify(report, null, 2) : mergeCopiesSummary(report);
|
|
@@ -274,6 +308,7 @@ export function registerCoreOntologyCommand(core) {
|
|
|
274
308
|
.option("--variants", "merge two-file duplicate groups whose names differ only in case, spacing or punctuation (checkpoint.ontology.merge-variants)")
|
|
275
309
|
.option("--limit <n>", "handle at most this many copies", (value) => Number(value))
|
|
276
310
|
.option("--only <file>", "handle only the copy -> original pairs listed in this JSON file")
|
|
311
|
+
.option("--rewrite-refs", "also re-point signal refs, invoice, source and knowledge links to the survivor and carry external ids and web domains (checkpoint.ontology.merge-rewrite-refs)")
|
|
277
312
|
.option("--json", "print JSON")
|
|
278
313
|
.option("--hq-root <path>", "HQ installation to operate on")
|
|
279
314
|
.action(async (options) => {
|
package/dist/commands/core.js
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
import { Option } from "commander";
|
|
32
32
|
import { registerCoreCheckpointCommand } from "./core-checkpoint.js";
|
|
33
33
|
import { registerCoreDelegateCommand } from "./core-delegate.js";
|
|
34
|
+
import { registerCoreHandoffCommand } from "./core-handoff.js";
|
|
34
35
|
import { registerCoreOntologyCommand } from "./core-ontology.js";
|
|
35
36
|
import { registerCoreKnowledgePrefsCommand } from "./core-knowledge-prefs.js";
|
|
36
37
|
import { registerCorePolicyCommand } from "./core-policy.js";
|
|
@@ -241,7 +242,7 @@ export const SCAFFOLD_ONLY_ASSETS = [
|
|
|
241
242
|
{ asset: "core/scripts/ontology-source-worklist.sh", root: "live", summary: "Rollback companion for native ontology source worklist" },
|
|
242
243
|
{ asset: "core/scripts/source-yaml-validate.sh", root: "cwd", summary: "Rollback companion for native source YAML validation" },
|
|
243
244
|
// Shared by native knowledge-prefs and the ontology candidate shell fallback.
|
|
244
|
-
{ asset: "core/scripts/knowledge-prefs.sh", root: "live", summary: "
|
|
245
|
+
{ asset: "core/scripts/knowledge-prefs.sh", root: "live", summary: "Sibling dependency for ontology candidate rollback companion" },
|
|
245
246
|
// Rollback companions for the default-off native knowledge and policy routes.
|
|
246
247
|
// policy-retire also runs the scaffold validator in-process when native, but
|
|
247
248
|
// its bundled shell fallback needs both helper assets at these exact paths.
|
|
@@ -250,6 +251,11 @@ export const SCAFFOLD_ONLY_ASSETS = [
|
|
|
250
251
|
{ asset: "core/scripts/read-policy-frontmatter.sh", root: "cwd", summary: "Rollback companion for native policy frontmatter" },
|
|
251
252
|
{ asset: "core/scripts/eval-trigger.sh", root: "live", summary: "Trigger grammar helper used by the policy validator companion" },
|
|
252
253
|
{ asset: ".claude/hooks/validate-policy-frontmatter.sh", root: "live", summary: "Policy validator companion used by policy-retire rollback" },
|
|
254
|
+
// Wave 2a handoff shells remain bundled as the default-off rollback path.
|
|
255
|
+
{ asset: "core/scripts/handoff-finalize.sh", root: "live", summary: "Rollback shell for native handoff finalize" },
|
|
256
|
+
{ asset: "core/scripts/handoff-post.sh", root: "live", summary: "Rollback shell for native handoff post" },
|
|
257
|
+
{ asset: "core/scripts/handoff-open-steps.sh", root: "live", summary: "Rollback shell for native handoff open-steps" },
|
|
258
|
+
{ asset: "core/scripts/qmd-reindex-bg.sh", root: "live", summary: "Rollback handoff reindex forwarder" },
|
|
253
259
|
...NATIVE_UTILITY_COMMANDS.map(({ asset, root, summary }) => ({
|
|
254
260
|
asset,
|
|
255
261
|
root,
|
|
@@ -326,6 +332,7 @@ export function registerCoreCommands(program) {
|
|
|
326
332
|
registerCoreOntologyCommand(core);
|
|
327
333
|
registerCoreCheckpointGateSettingCommand(core);
|
|
328
334
|
registerCoreDelegateCommand(core);
|
|
335
|
+
registerCoreHandoffCommand(core);
|
|
329
336
|
registerCoreKnowledgePrefsCommand(core);
|
|
330
337
|
registerCorePolicyCommand(core);
|
|
331
338
|
// Wave 2 handoff/worktree commands retain their byte-identical shell assets
|
|
@@ -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
|
}
|