@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.
- package/CHANGELOG.md +27 -8
- package/assets/scaffold/core/scripts/handoff-finalize.sh +18 -0
- package/assets/scaffold/core/scripts/handoff-post.sh +71 -6
- package/assets/scaffold/core/scripts/hq-delegate-bundle.sh +307 -0
- package/assets/scaffold/core/scripts/hq-delegate-grant.sh +226 -0
- package/assets/scaffold/core/scripts/hq-delegate-publish.sh +91 -0
- package/assets/scaffold/core/scripts/hq-delegate-send.sh +259 -0
- package/assets/scaffold/core/scripts/hq-delegate-verify.sh +213 -0
- package/assets/scaffold/core/scripts/hq-job-notify.sh +389 -0
- package/assets/scaffold/core/scripts/hq-job-probe.sh +835 -0
- package/assets/scaffold/core/scripts/hq-job-remediate.sh +359 -0
- package/assets/scaffold/core/scripts/hq-job-run.sh +1025 -0
- package/assets/scaffold/core/scripts/outpost-jobs-reconcile.sh +830 -0
- package/dist/command-catalog.generated.d.ts +106 -1
- package/dist/command-catalog.generated.js +132 -1
- package/dist/commands/core-delegate-w1b.d.ts +8 -0
- package/dist/commands/core-delegate-w1b.js +1049 -0
- package/dist/commands/core-delegate.d.ts +28 -0
- package/dist/commands/core-delegate.js +15 -13
- package/dist/commands/core-handoff.js +152 -23
- package/dist/commands/core-jobs-native.d.ts +15 -0
- package/dist/commands/core-jobs-native.js +709 -2
- package/dist/commands/core-ontology.d.ts +22 -4
- package/dist/commands/core-ontology.js +90 -8
- package/dist/commands/core.js +10 -0
- package/dist/lib/checkpoint-ontology/drop-aliases.d.ts +41 -0
- package/dist/lib/checkpoint-ontology/drop-aliases.js +76 -0
- package/dist/lib/checkpoint-ontology/entity-file.d.ts +6 -3
- package/dist/lib/checkpoint-ontology/entity-file.js +12 -6
- package/dist/lib/checkpoint-ontology/merge-copies.d.ts +27 -8
- package/dist/lib/checkpoint-ontology/merge-copies.js +64 -97
- package/dist/lib/daemon/flag.d.ts +2 -0
- package/dist/lib/daemon/flag.js +2 -0
- package/dist/utils/scaffold-sibling-references.js +4 -4
- package/dist/utils/self-update.d.ts +6 -0
- package/dist/utils/self-update.js +28 -6
- package/dist/utils/staged-install.d.ts +34 -0
- package/dist/utils/staged-install.js +296 -0
- package/package.json +1 -1
- 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
|
|
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
|
|
51
|
-
*
|
|
52
|
-
*
|
|
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 {
|
|
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
|
|
220
|
-
*
|
|
221
|
-
*
|
|
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
|
-
|
|
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
|
|
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")
|
package/dist/commands/core.js
CHANGED
|
@@ -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
|
|
65
|
-
* `
|
|
66
|
-
* change, the file is returned
|
|
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
|
|
148
|
-
* `
|
|
149
|
-
* change, the file is returned
|
|
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
|
|
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
|
|
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 (
|
|
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`)
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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
|
-
/**
|
|
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
|