@indigoai-us/hq-cli 5.291.0 → 5.292.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +27 -8
  2. package/assets/scaffold/core/scripts/handoff-finalize.sh +18 -0
  3. package/assets/scaffold/core/scripts/handoff-post.sh +71 -6
  4. package/assets/scaffold/core/scripts/hq-delegate-bundle.sh +307 -0
  5. package/assets/scaffold/core/scripts/hq-delegate-grant.sh +226 -0
  6. package/assets/scaffold/core/scripts/hq-delegate-publish.sh +91 -0
  7. package/assets/scaffold/core/scripts/hq-delegate-send.sh +259 -0
  8. package/assets/scaffold/core/scripts/hq-delegate-verify.sh +213 -0
  9. package/assets/scaffold/core/scripts/hq-job-notify.sh +389 -0
  10. package/assets/scaffold/core/scripts/hq-job-probe.sh +835 -0
  11. package/assets/scaffold/core/scripts/hq-job-remediate.sh +359 -0
  12. package/assets/scaffold/core/scripts/hq-job-run.sh +1025 -0
  13. package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +830 -0
  14. package/dist/command-catalog.generated.d.ts +106 -1
  15. package/dist/command-catalog.generated.js +132 -1
  16. package/dist/commands/core-delegate-w1b.d.ts +8 -0
  17. package/dist/commands/core-delegate-w1b.js +1049 -0
  18. package/dist/commands/core-delegate.d.ts +28 -0
  19. package/dist/commands/core-delegate.js +15 -13
  20. package/dist/commands/core-handoff.js +152 -23
  21. package/dist/commands/core-jobs-native.d.ts +15 -0
  22. package/dist/commands/core-jobs-native.js +709 -2
  23. package/dist/commands/core-ontology.d.ts +22 -4
  24. package/dist/commands/core-ontology.js +90 -8
  25. package/dist/commands/core.js +10 -0
  26. package/dist/lib/checkpoint-ontology/drop-aliases.d.ts +41 -0
  27. package/dist/lib/checkpoint-ontology/drop-aliases.js +76 -0
  28. package/dist/lib/checkpoint-ontology/entity-file.d.ts +6 -3
  29. package/dist/lib/checkpoint-ontology/entity-file.js +12 -6
  30. package/dist/lib/checkpoint-ontology/merge-copies.d.ts +27 -8
  31. package/dist/lib/checkpoint-ontology/merge-copies.js +64 -97
  32. package/dist/lib/daemon/flag.d.ts +2 -0
  33. package/dist/lib/daemon/flag.js +2 -0
  34. package/dist/utils/scaffold-sibling-references.js +4 -4
  35. package/dist/utils/self-update.d.ts +6 -0
  36. package/dist/utils/self-update.js +28 -6
  37. package/dist/utils/staged-install.d.ts +34 -0
  38. package/dist/utils/staged-install.js +296 -0
  39. package/package.json +1 -1
  40. package/scripts/mesh-daemon-if-stale.mjs +4 -0
@@ -9,7 +9,8 @@
9
9
  */
10
10
  import { Command } from "commander";
11
11
  import { type FlagReader } from "../lib/checkpoint-ontology/plan.js";
12
- import { type DeleteVaultKey, type ListedPair } from "../lib/checkpoint-ontology/merge-copies.js";
12
+ import { type AliasDrop } from "../lib/checkpoint-ontology/drop-aliases.js";
13
+ import { type DeleteVaultKey, type ListedPair, type MergeCopiesReport } from "../lib/checkpoint-ontology/merge-copies.js";
13
14
  export declare const ONTOLOGY_PAYLOAD_FILE = "payload.json";
14
15
  /** The hq-flags read, scoped to the payload company. */
15
16
  export declare const registryFlagReader: FlagReader;
@@ -31,12 +32,15 @@ export interface MergeCopiesCommandOptions {
31
32
  apply?: boolean;
32
33
  delete?: boolean;
33
34
  variants?: boolean;
35
+ /** Merge exactly the `--only` pairs, whatever their names (flag merge-listed-pairs). */
36
+ listed?: boolean;
34
37
  limit?: number;
35
38
  only?: string;
36
39
  /** Also re-point refs and links held today (flag merge-rewrite-refs). */
37
40
  rewriteRefs?: boolean;
38
41
  json?: boolean;
39
42
  }
43
+ export declare function mergeCopiesSummary(report: MergeCopiesReport): string;
40
44
  /** Reads a `--only` file: a JSON array of `{ copy, original }` company-relative paths. */
41
45
  export declare function readListedPairs(file: string): ListedPair[];
42
46
  /**
@@ -47,10 +51,24 @@ export declare function readListedPairs(file: string): ListedPair[];
47
51
  * With `--variants`, merges two-file duplicate groups whose names differ only
48
52
  * in case, spacing or punctuation instead, gated by
49
53
  * `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.
54
+ * `checkpoint.ontology.merge-rewrite-refs`) also re-points invoice and
55
+ * knowledge links to the survivor and carries external ids and web domains;
56
+ * pairs named by signals or sources stay held. `--listed` (flag
57
+ * `checkpoint.ontology.merge-listed-pairs`) merges exactly the `--only` pairs,
58
+ * which a person reviewed, instead of the pairs a finder produces.
53
59
  */
54
60
  export declare function runOntologyMergeCopies(hqRoot: string, slug: string | undefined, options: MergeCopiesCommandOptions, deps?: OntologyCommandDeps): Promise<string>;
61
+ /** Reads a `drop-aliases --only` file: a JSON array of `{ entity, alias }`. */
62
+ export declare function readAliasDrops(file: string): AliasDrop[];
63
+ /**
64
+ * `hq core ontology drop-aliases`: remove the reviewed `{ entity, alias }`
65
+ * pairs in `--only` from entity files (`--apply`); without it only reports.
66
+ * Gated by `checkpoint.ontology.drop-aliases` and `checkpoint.ontology.multi-writer`.
67
+ */
68
+ export declare function runOntologyDropAliases(hqRoot: string, slug: string | undefined, options: {
69
+ only?: string;
70
+ apply?: boolean;
71
+ json?: boolean;
72
+ }, deps?: OntologyCommandDeps): Promise<string>;
55
73
  export declare function registerCoreOntologyCommand(core: Command): void;
56
74
  //# sourceMappingURL=core-ontology.d.ts.map
@@ -17,7 +17,8 @@ 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_REWRITE_REFS_FLAG, MERGE_VARIANTS_FLAG, mergeCopies, } from "../lib/checkpoint-ontology/merge-copies.js";
20
+ import { DROP_ALIASES_FLAG, dropAliases } from "../lib/checkpoint-ontology/drop-aliases.js";
21
+ import { MERGE_COPIES_FLAG, MERGE_LISTED_PAIRS_FLAG, MERGE_REWRITE_REFS_FLAG, MERGE_VARIANTS_FLAG, mergeCopies, } from "../lib/checkpoint-ontology/merge-copies.js";
21
22
  import { callDeleteEndpoint } from "./files.js";
22
23
  import { resolveVaultCredential } from "../utils/resolve-vault-credential.js";
23
24
  import { registerCoreOntologyNativeCommands } from "./core-ontology-native.js";
@@ -142,7 +143,7 @@ async function companyVaultDeleter(companyUid) {
142
143
  return (await callDeleteEndpoint({ token, companyUid, prefix: key, dryRun: false })).deleted;
143
144
  };
144
145
  }
145
- function mergeCopiesSummary(report) {
146
+ export function mergeCopiesSummary(report) {
146
147
  const held = report.plans.filter((plan) => plan.held).length;
147
148
  const lines = [
148
149
  `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`,
@@ -150,7 +151,7 @@ function mergeCopiesSummary(report) {
150
151
  ` duplicate groups ${report.duplicate_groups_before} -> ${report.duplicate_groups_after}; broken entity links ${report.broken_links_before} -> ${report.broken_links_after}`,
151
152
  ];
152
153
  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
+ lines.push(` held ${plan.copy}: ${plan.held}${plan.held === "external_id_conflict" || plan.held === "server_owned_refs" ? ` (${plan.detail})` : ""}`);
154
155
  }
155
156
  for (const error of report.errors)
156
157
  lines.push(` error ${error.path}: ${error.error}`);
@@ -216,9 +217,11 @@ function readResolution(copy, raw) {
216
217
  * With `--variants`, merges two-file duplicate groups whose names differ only
217
218
  * in case, spacing or punctuation instead, gated by
218
219
  * `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.
220
+ * `checkpoint.ontology.merge-rewrite-refs`) also re-points invoice and
221
+ * knowledge links to the survivor and carries external ids and web domains;
222
+ * pairs named by signals or sources stay held. `--listed` (flag
223
+ * `checkpoint.ontology.merge-listed-pairs`) merges exactly the `--only` pairs,
224
+ * which a person reviewed, instead of the pairs a finder produces.
222
225
  */
223
226
  export async function runOntologyMergeCopies(hqRoot, slug, options, deps = {}) {
224
227
  if (options.apply && options.delete) {
@@ -227,13 +230,20 @@ export async function runOntologyMergeCopies(hqRoot, slug, options, deps = {}) {
227
230
  if (options.limit !== undefined && (!Number.isInteger(options.limit) || options.limit < 1)) {
228
231
  throw new OntologyUsageError("ontology: --limit must be a positive whole number");
229
232
  }
233
+ if (options.listed && options.variants) {
234
+ throw new OntologyUsageError("ontology: use either --listed or --variants");
235
+ }
236
+ if (options.listed && options.only === undefined) {
237
+ throw new OntologyUsageError("ontology: --listed needs --only <file>");
238
+ }
230
239
  const only = options.only !== undefined ? readListedPairs(options.only) : undefined;
231
240
  const resolution = resolveOntologyCompany(hqRoot, slug);
232
241
  if (!resolution.ok)
233
242
  throw new OntologyUsageError(`ontology: company rejected: ${resolution.reason}`);
234
243
  const company = resolution.company;
235
244
  const readFlag = deps.readFlag ?? registryFlagReader;
236
- for (const flag of [options.variants ? MERGE_VARIANTS_FLAG : MERGE_COPIES_FLAG, MULTI_WRITER_FLAG, ...(options.rewriteRefs ? [MERGE_REWRITE_REFS_FLAG] : [])]) {
245
+ const kindFlag = options.listed ? MERGE_LISTED_PAIRS_FLAG : options.variants ? MERGE_VARIANTS_FLAG : MERGE_COPIES_FLAG;
246
+ for (const flag of [kindFlag, MULTI_WRITER_FLAG, ...(options.rewriteRefs ? [MERGE_REWRITE_REFS_FLAG] : [])]) {
237
247
  if (!(await readFlag(flag, company))) {
238
248
  return options.json ? JSON.stringify({ company: company.slug, skipped: `${flag} off` }) : `ontology merge-copies: skipped (${flag} is off)`;
239
249
  }
@@ -251,6 +261,7 @@ export async function runOntologyMergeCopies(hqRoot, slug, options, deps = {}) {
251
261
  mode,
252
262
  now: deps.now?.() ?? new Date(),
253
263
  ...(options.variants ? { pairKind: "variants" } : {}),
264
+ ...(options.listed ? { pairKind: "listed" } : {}),
254
265
  ...(options.limit !== undefined ? { limit: options.limit } : {}),
255
266
  ...(only ? { only } : {}),
256
267
  ...(options.rewriteRefs ? { rewriteRefs: true } : {}),
@@ -258,6 +269,59 @@ export async function runOntologyMergeCopies(hqRoot, slug, options, deps = {}) {
258
269
  });
259
270
  return options.json ? JSON.stringify(report, null, 2) : mergeCopiesSummary(report);
260
271
  }
272
+ /** Reads a `drop-aliases --only` file: a JSON array of `{ entity, alias }`. */
273
+ export function readAliasDrops(file) {
274
+ let parsed;
275
+ try {
276
+ parsed = JSON.parse(fs.readFileSync(file, "utf8"));
277
+ }
278
+ catch (error) {
279
+ throw new OntologyUsageError(`ontology: --only file ${file} is not readable JSON: ${error.message}`);
280
+ }
281
+ const usage = "ontology: --only must be a non-empty JSON array of { entity, alias } strings";
282
+ if (!Array.isArray(parsed) || parsed.length === 0)
283
+ throw new OntologyUsageError(usage);
284
+ return parsed.map((item) => {
285
+ const record = item;
286
+ if (!record || typeof record.entity !== "string" || typeof record.alias !== "string" || record.alias === "") {
287
+ throw new OntologyUsageError(usage);
288
+ }
289
+ return { entity: record.entity, alias: record.alias };
290
+ });
291
+ }
292
+ /**
293
+ * `hq core ontology drop-aliases`: remove the reviewed `{ entity, alias }`
294
+ * pairs in `--only` from entity files (`--apply`); without it only reports.
295
+ * Gated by `checkpoint.ontology.drop-aliases` and `checkpoint.ontology.multi-writer`.
296
+ */
297
+ export async function runOntologyDropAliases(hqRoot, slug, options, deps = {}) {
298
+ if (options.only === undefined)
299
+ throw new OntologyUsageError("ontology: drop-aliases needs --only <file>");
300
+ const drops = readAliasDrops(options.only);
301
+ const resolution = resolveOntologyCompany(hqRoot, slug);
302
+ if (!resolution.ok)
303
+ throw new OntologyUsageError(`ontology: company rejected: ${resolution.reason}`);
304
+ const company = resolution.company;
305
+ const readFlag = deps.readFlag ?? registryFlagReader;
306
+ for (const flag of [DROP_ALIASES_FLAG, MULTI_WRITER_FLAG]) {
307
+ if (!(await readFlag(flag, company))) {
308
+ return options.json ? JSON.stringify({ company: company.slug, skipped: `${flag} off` }) : `ontology drop-aliases: skipped (${flag} is off)`;
309
+ }
310
+ }
311
+ const report = dropAliases({ hqRoot, company, mode: options.apply ? "apply" : "dry_run", drops, now: deps.now?.() ?? new Date() });
312
+ if (options.json)
313
+ return JSON.stringify(report, null, 2);
314
+ const planned = Object.values(report.planned).reduce((sum, list) => sum + list.length, 0);
315
+ const lines = [
316
+ `ontology drop-aliases (${report.mode.replace("_", " ")}) in ${report.company}: ${planned} alias(es) on ${Object.keys(report.planned).length} entities, written ${report.written.length}, skipped ${report.skipped.length}, errors ${report.errors.length}`,
317
+ ` duplicate groups ${report.duplicate_groups_before} -> ${report.duplicate_groups_after}`,
318
+ ];
319
+ for (const skip of report.skipped)
320
+ lines.push(` skipped ${skip.entity} "${skip.alias}": ${skip.reason}${skip.detail ? ` (${skip.detail})` : ""}`);
321
+ for (const error of report.errors)
322
+ lines.push(` error ${error.path}: ${error.error}`);
323
+ return lines.join("\n");
324
+ }
261
325
  function fail(error) {
262
326
  if (error instanceof OntologyUsageError || error instanceof ApplyError || error instanceof WriteBoundsError) {
263
327
  process.stderr.write(`${error.message}\n`);
@@ -306,9 +370,10 @@ export function registerCoreOntologyCommand(core) {
306
370
  .option("--apply", "merge each copy into its original and re-point links (writes entity files)")
307
371
  .option("--delete", "delete fully merged copies from the vault by exact key (after sync pushed the merge)")
308
372
  .option("--variants", "merge two-file duplicate groups whose names differ only in case, spacing or punctuation (checkpoint.ontology.merge-variants)")
373
+ .option("--listed", "merge exactly the pairs in the --only file, whatever their names (checkpoint.ontology.merge-listed-pairs)")
309
374
  .option("--limit <n>", "handle at most this many copies", (value) => Number(value))
310
375
  .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)")
376
+ .option("--rewrite-refs", "also re-point invoice and knowledge links to the survivor and carry external ids and web domains; pairs named by signals or sources stay held (checkpoint.ontology.merge-rewrite-refs)")
312
377
  .option("--json", "print JSON")
313
378
  .option("--hq-root <path>", "HQ installation to operate on")
314
379
  .action(async (options) => {
@@ -320,6 +385,23 @@ export function registerCoreOntologyCommand(core) {
320
385
  }
321
386
  });
322
387
  describeCoreCommand(mergeCopies, { execution: "native", root: "live" });
388
+ const dropAliasesCommand = ontology
389
+ .command("drop-aliases")
390
+ .description("Remove reviewed wrong aliases from entity files (dry run by default)")
391
+ .requiredOption("--company <slug>", "company to change")
392
+ .option("--only <file>", "JSON array of { entity, alias } to remove; entity is a company-relative path")
393
+ .option("--apply", "write the entity files")
394
+ .option("--json", "print JSON")
395
+ .option("--hq-root <path>", "HQ installation to operate on")
396
+ .action(async (options) => {
397
+ try {
398
+ process.stdout.write(`${await runOntologyDropAliases(rootFor(options), options.company, options)}\n`);
399
+ }
400
+ catch (error) {
401
+ fail(error);
402
+ }
403
+ });
404
+ describeCoreCommand(dropAliasesCommand, { execution: "native", root: "live" });
323
405
  const apply = ontology
324
406
  .command("apply")
325
407
  .description("Validate the run's entity candidates and write them to the payload company")
@@ -214,6 +214,11 @@ export const SCAFFOLD_COMMANDS = [
214
214
  export const SCAFFOLD_ONLY_ASSETS = [
215
215
  { asset: "core/scripts/jobs-schedule-parse.sh", root: "live", summary: "Rollback companion for native jobs schedule-parse" },
216
216
  { asset: "core/scripts/jobs-validate.sh", root: "live", summary: "Rollback companion for native jobs validate" },
217
+ { asset: "core/scripts/outpost-jobs-reconcile.sh", root: "live", summary: "Rollback companion for native jobs reconcile" },
218
+ { asset: "core/scripts/hq-job-probe.sh", root: "live", summary: "Scheduled jobs readiness probe used by the reconcile rollback path" },
219
+ { asset: "core/scripts/hq-job-run.sh", root: "live", summary: "Scheduled jobs runner used by generated systemd units" },
220
+ { asset: "core/scripts/hq-job-notify.sh", root: "live", summary: "Scheduled jobs failure notification helper" },
221
+ { asset: "core/scripts/hq-job-remediate.sh", root: "live", summary: "Scheduled jobs bounded remediation helper" },
217
222
  // Shared shell libs sourced by the two relocated scripts (and hooks). Never
218
223
  // dispatched on their own (so no `hq core` command), but they MUST ship in the
219
224
  // tarball, or the bundled hq-session / checkpoint-stop-gate die at `source`
@@ -235,6 +240,11 @@ export const SCAFFOLD_ONLY_ASSETS = [
235
240
  { asset: "core/scripts/hq-delegate-repo.sh", root: "live", summary: "Rollback shell for native delegate repo" },
236
241
  { asset: "core/scripts/hq-delegate-secrets.sh", root: "live", summary: "Rollback shell for native delegate secrets" },
237
242
  { asset: "core/scripts/hq-delegate-transfer.sh", root: "live", summary: "Rollback shell for native delegate transfer" },
243
+ { asset: "core/scripts/hq-delegate-bundle.sh", root: "live", summary: "Rollback shell for native delegate bundle" },
244
+ { asset: "core/scripts/hq-delegate-grant.sh", root: "live", summary: "Rollback shell for native delegate grant" },
245
+ { asset: "core/scripts/hq-delegate-publish.sh", root: "live", summary: "Rollback shell for native delegate publish" },
246
+ { asset: "core/scripts/hq-delegate-send.sh", root: "live", summary: "Rollback shell for native delegate send" },
247
+ { asset: "core/scripts/hq-delegate-verify.sh", root: "live", summary: "Rollback shell for native delegate verify" },
238
248
  { asset: "core/scripts/lib/secret-patterns.sh", root: "live", summary: "Secret-pattern library sourced by the delegate rollback shells" },
239
249
  // Retained rollback companions for the gated native ontology commands. These
240
250
  // are loose HQ-tree assets, but `hq core ontology` dispatches natively.
@@ -0,0 +1,41 @@
1
+ /**
2
+ * `hq core ontology drop-aliases`: remove reviewed wrong aliases from entity
3
+ * files (CO-74). An alias that names a different entity joins two unrelated
4
+ * entities into one duplicate group; a person lists each `{ entity, alias }`
5
+ * to remove, and nothing else changes. The dry run writes nothing; `apply`
6
+ * writes the entity files through the multi-writer outbox so sync pushes them.
7
+ */
8
+ import { type OntologyCompany } from "./company.js";
9
+ export declare const DROP_ALIASES_FLAG = "checkpoint.ontology.drop-aliases";
10
+ /** One reviewed removal: `entity` is a company-relative entity path. */
11
+ export interface AliasDrop {
12
+ entity: string;
13
+ alias: string;
14
+ }
15
+ export type AliasDropSkipReason = "no_entity" | "type_not_writable" | "alias_absent" | "patch_failed";
16
+ export interface AliasDropReport {
17
+ company: string;
18
+ mode: "dry_run" | "apply";
19
+ duplicate_groups_before: number;
20
+ duplicate_groups_after: number;
21
+ /** Entity -> aliases removed (dry run: would be removed). */
22
+ planned: Record<string, string[]>;
23
+ written: string[];
24
+ skipped: Array<AliasDrop & {
25
+ reason: AliasDropSkipReason;
26
+ detail?: string;
27
+ }>;
28
+ errors: Array<{
29
+ path: string;
30
+ error: string;
31
+ }>;
32
+ }
33
+ export interface DropAliasesOptions {
34
+ hqRoot: string;
35
+ company: OntologyCompany;
36
+ mode: "dry_run" | "apply";
37
+ drops: AliasDrop[];
38
+ now: Date;
39
+ }
40
+ export declare function dropAliases(options: DropAliasesOptions): AliasDropReport;
41
+ //# sourceMappingURL=drop-aliases.d.ts.map
@@ -0,0 +1,76 @@
1
+ /**
2
+ * `hq core ontology drop-aliases`: remove reviewed wrong aliases from entity
3
+ * files (CO-74). An alias that names a different entity joins two unrelated
4
+ * entities into one duplicate group; a person lists each `{ entity, alias }`
5
+ * to remove, and nothing else changes. The dry run writes nothing; `apply`
6
+ * writes the entity files through the multi-writer outbox so sync pushes them.
7
+ */
8
+ import * as fs from "node:fs";
9
+ import * as path from "node:path";
10
+ import { entitiesRoot, safeWriteFile } from "./company.js";
11
+ import { patchEntityFile } from "./entity-file.js";
12
+ import { isWritableEntityType } from "./identity.js";
13
+ import { findDuplicateGroups, recordOutbox } from "./multi-writer.js";
14
+ import { buildEntityIndex } from "./tree.js";
15
+ export const DROP_ALIASES_FLAG = "checkpoint.ontology.drop-aliases";
16
+ export function dropAliases(options) {
17
+ const { hqRoot, company, mode, drops, now } = options;
18
+ const index = buildEntityIndex(company.dir);
19
+ const report = {
20
+ company: company.slug,
21
+ mode,
22
+ duplicate_groups_before: findDuplicateGroups(index).length,
23
+ duplicate_groups_after: 0,
24
+ planned: {},
25
+ written: [],
26
+ skipped: [],
27
+ errors: [],
28
+ };
29
+ const byEntity = new Map();
30
+ for (const drop of drops) {
31
+ const entity = index.byRel.get(drop.entity);
32
+ if (!entity) {
33
+ report.skipped.push({ ...drop, reason: "no_entity" });
34
+ }
35
+ else if (!isWritableEntityType(entity.type)) {
36
+ report.skipped.push({ ...drop, reason: "type_not_writable" });
37
+ }
38
+ else if (!entity.aliases.includes(drop.alias)) {
39
+ report.skipped.push({ ...drop, reason: "alias_absent" });
40
+ }
41
+ else {
42
+ byEntity.set(drop.entity, [...(byEntity.get(drop.entity) ?? []), drop]);
43
+ }
44
+ }
45
+ const writes = new Map();
46
+ for (const [rel, list] of [...byEntity].sort(([a], [b]) => a.localeCompare(b))) {
47
+ const content = fs.readFileSync(path.join(company.dir, rel), "utf8");
48
+ const patched = patchEntityFile(content, { removeAliases: list.map((drop) => drop.alias), lastUpdated: now.toISOString() });
49
+ if (!patched.ok) {
50
+ for (const drop of list)
51
+ report.skipped.push({ ...drop, reason: "patch_failed", detail: patched.reason });
52
+ continue;
53
+ }
54
+ report.planned[rel] = [...new Set(list.map((drop) => drop.alias))];
55
+ writes.set(rel, patched.content);
56
+ }
57
+ if (mode === "apply") {
58
+ for (const [rel, content] of writes) {
59
+ try {
60
+ // Outbox first: sync pushes an entity file only when its bytes match.
61
+ recordOutbox(hqRoot, company, [{ rel, content }], now);
62
+ safeWriteFile(hqRoot, [entitiesRoot(company)], path.join(company.dir, rel), content);
63
+ report.written.push(rel);
64
+ }
65
+ catch (error) {
66
+ report.errors.push({ path: rel, error: error.name });
67
+ }
68
+ }
69
+ report.duplicate_groups_after = findDuplicateGroups(buildEntityIndex(company.dir)).length;
70
+ }
71
+ else {
72
+ report.duplicate_groups_after = report.duplicate_groups_before;
73
+ }
74
+ return report;
75
+ }
76
+ //# sourceMappingURL=drop-aliases.js.map
@@ -47,6 +47,8 @@ export declare function renderNewEntity(entity: NewEntity): string;
47
47
  export declare const OWNED_KEYS: ReadonlySet<string>;
48
48
  export interface EntityPatch {
49
49
  addAliases?: string[];
50
+ /** Exact alias strings to remove (reviewed wrong aliases). Applied before `addAliases`. */
51
+ removeAliases?: string[];
50
52
  signalCount?: number;
51
53
  lastUpdated: string;
52
54
  /** The caller changed the body, so `last_updated` moves even with no key change. */
@@ -61,9 +63,10 @@ export type PatchResult = {
61
63
  reason: string;
62
64
  };
63
65
  /**
64
- * Patch an existing, valid entity file. Aliases are only appended, and
65
- * `signal_count` never goes down. When nothing but `last_updated` would
66
- * change, the file is returned unchanged so a rerun writes nothing.
66
+ * Patch an existing, valid entity file. Aliases are appended, and removed
67
+ * only when `removeAliases` names them exactly; `signal_count` never goes
68
+ * down. When nothing but `last_updated` would change, the file is returned
69
+ * unchanged so a rerun writes nothing.
67
70
  */
68
71
  export declare function patchEntityFile(content: string, patch: EntityPatch): PatchResult;
69
72
  /** A namespaced record in a source system, as the cloud schema's `external_refs` holds it. */
@@ -144,9 +144,10 @@ function appendAliases(lines, all, added) {
144
144
  return [...lines.slice(0, end), ...added.map((alias) => `${indent}- ${quote(alias)}`), ...lines.slice(end)];
145
145
  }
146
146
  /**
147
- * Patch an existing, valid entity file. Aliases are only appended, and
148
- * `signal_count` never goes down. When nothing but `last_updated` would
149
- * change, the file is returned unchanged so a rerun writes nothing.
147
+ * Patch an existing, valid entity file. Aliases are appended, and removed
148
+ * only when `removeAliases` names them exactly; `signal_count` never goes
149
+ * down. When nothing but `last_updated` would change, the file is returned
150
+ * unchanged so a rerun writes nothing.
150
151
  */
151
152
  export function patchEntityFile(content, patch) {
152
153
  const match = FRONTMATTER_RE.exec(content);
@@ -156,19 +157,24 @@ export function patchEntityFile(content, patch) {
156
157
  const before = validateEntityFrontmatter(parsed.frontmatter);
157
158
  if (typeof before === "string")
158
159
  return { ok: false, reason: `invalid_existing:${before}` };
159
- const aliases = [...before.aliases];
160
+ const remove = new Set(patch.removeAliases ?? []);
161
+ const kept = before.aliases.filter((alias) => !remove.has(alias));
162
+ const aliases = [...kept];
160
163
  for (const alias of patch.addAliases ?? []) {
161
164
  if (!aliases.includes(alias))
162
165
  aliases.push(alias);
163
166
  }
164
167
  const signalCount = Math.max(before.signal_count, patch.signalCount ?? 0);
165
- const aliasesChanged = aliases.length !== before.aliases.length;
168
+ const removed = kept.length !== before.aliases.length;
169
+ const aliasesChanged = removed || aliases.length !== before.aliases.length;
166
170
  const countChanged = signalCount !== before.signal_count;
167
171
  if (!aliasesChanged && !countChanged && !patch.bodyChanged)
168
172
  return { ok: true, content, changed: false };
169
173
  const newline = content.includes("\r\n") ? "\r\n" : "\n";
170
174
  let lines = match[1].split(/\r?\n/);
171
- if (aliasesChanged)
175
+ if (removed)
176
+ lines = replaceTopLevelKey(lines, "aliases", renderAliases(aliases));
177
+ else if (aliasesChanged)
172
178
  lines = appendAliases(lines, aliases, aliases.slice(before.aliases.length));
173
179
  if (lines && countChanged)
174
180
  lines = replaceTopLevelKey(lines, "signal_count", `signal_count: ${signalCount}`);
@@ -27,16 +27,25 @@
27
27
  *
28
28
  * A signal whose `entity_refs` names a copy holds that copy: signals are
29
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
+ * (flag `checkpoint.ontology.merge-rewrite-refs`) invoice and bill links and
31
+ * knowledge links are re-pointed at the survivor and the survivor receives
32
+ * the copy's `external_ids` and `web_domains`. A signal ref or a link from a
33
+ * source still holds the pair (`server_owned_refs`): the sync client cannot
34
+ * push those files. A link from another copy or an external id conflict also
35
+ * holds a pair.
34
36
  *
35
37
  * With `pairKind: "variants"` (flag `checkpoint.ontology.merge-variants`) the
36
38
  * pairs are two-file duplicate groups whose canonical names differ only in
37
39
  * case, spacing or punctuation; the survivor is the spaced name, then the
38
40
  * higher `signal_count`, then path order. The copy's canonical name becomes
39
41
  * an alias of the survivor. Merge and delete work as for numbered copies.
42
+ *
43
+ * With `pairKind: "listed"` (flag `checkpoint.ontology.merge-listed-pairs`)
44
+ * the pairs are exactly the `only` entries, reviewed by a person: no finder
45
+ * runs, so the names may differ. Each entry must name two entities of one
46
+ * type; a copy listed twice, or listed both as a copy and as an original, is
47
+ * skipped as `listed_conflict`. Holds, merge and delete work as for numbered
48
+ * copies.
40
49
  */
41
50
  import { type OntologyCompany } from "./company.js";
42
51
  import { type EntityIndex } from "./tree.js";
@@ -46,14 +55,16 @@ export declare const MERGE_COPIES_FLAG = "checkpoint.ontology.merge-copies";
46
55
  * names differ only in spacing, case or punctuation (CO-65).
47
56
  */
48
57
  export declare const MERGE_VARIANTS_FLAG = "checkpoint.ontology.merge-variants";
58
+ /** `merge-copies --listed`: merge exactly the reviewed `--only` pairs (CO-74). */
59
+ export declare const MERGE_LISTED_PAIRS_FLAG = "checkpoint.ontology.merge-listed-pairs";
49
60
  /**
50
61
  * `merge-copies --rewrite-refs`: re-point signal `entity_refs`, invoice and
51
62
  * bill links, source and knowledge links from a copy to its survivor, and
52
63
  * carry the copy's `external_ids` and `web_domains` to the survivor.
53
64
  */
54
65
  export declare const MERGE_REWRITE_REFS_FLAG = "checkpoint.ontology.merge-rewrite-refs";
55
- export type CopySkipReason = "no_original" | "original_invalid" | "identity_differs" | "not_in_duplicate_group" | "group_size" | "type_differs" | "names_differ" | "numbered_copy" | "not_listed";
56
- export type PairKind = "numbered" | "variants";
66
+ export type CopySkipReason = "no_original" | "original_invalid" | "identity_differs" | "not_in_duplicate_group" | "group_size" | "type_differs" | "names_differ" | "numbered_copy" | "not_listed" | "listed_conflict";
67
+ export type PairKind = "numbered" | "variants" | "listed";
57
68
  export interface CopySkip {
58
69
  copy: string;
59
70
  reason: CopySkipReason;
@@ -97,7 +108,7 @@ export interface CopyPlan extends CopyPair {
97
108
  /** Signal files whose `entity_refs` name the copy. */
98
109
  signal_refs: string[];
99
110
  /** Why the copy cannot be merged or deleted from here. */
100
- held?: "signal_refs" | "linked_from_unwritable" | "merge_invalid" | "external_id_conflict";
111
+ held?: "signal_refs" | "linked_from_unwritable" | "merge_invalid" | "external_id_conflict" | "server_owned_refs";
101
112
  detail?: string;
102
113
  /** Keys added to the survivor. Set only when `rewriteRefs` is on. */
103
114
  add_external_ids?: string[];
@@ -133,7 +144,7 @@ export interface MergeCopiesReport {
133
144
  }[];
134
145
  /** Listed entries the finder did not produce. Absent unless `--only` was set. */
135
146
  only_unmatched?: ListedPair[];
136
- /** Signal, source and knowledge files re-pointed at a survivor. Absent unless `rewriteRefs` was set. */
147
+ /** Knowledge files re-pointed at a survivor. Absent unless `rewriteRefs` was set. */
137
148
  rewritten?: string[];
138
149
  /** Entity-file links whose target file does not exist. */
139
150
  broken_links_before: number;
@@ -171,6 +182,14 @@ export declare function findVariantPairs(index: EntityIndex): {
171
182
  pairs: CopyPair[];
172
183
  skipped: CopySkip[];
173
184
  };
185
+ /**
186
+ * The reviewed pairs themselves, for `pairKind: "listed"`. An entry is kept
187
+ * when both files are indexed entities of one writable type. Read-only.
188
+ */
189
+ export declare function findListedPairs(index: EntityIndex, only: ListedPair[]): {
190
+ pairs: CopyPair[];
191
+ skipped: CopySkip[];
192
+ };
174
193
  /**
175
194
  * Plan, merge or delete numbered copies for one company. The dry run writes
176
195
  * nothing; merge writes entity files only; delete removes vault keys and