@ctxr/skill-llm-wiki 1.0.2 → 1.2.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 +128 -0
- package/README.md +11 -8
- package/SKILL.md +11 -11
- package/guide/cli.md +3 -2
- package/guide/correctness/safety.md +2 -2
- package/guide/layout/in-place-mode.md +1 -1
- package/guide/substrate/operators.md +1 -1
- package/guide/substrate/tiered-ai.md +6 -5
- package/guide/ux/user-intent.md +1 -1
- package/package.json +13 -4
- package/scripts/cli.mjs +92 -2
- package/scripts/lib/balance.mjs +579 -0
- package/scripts/lib/cluster-detect.mjs +482 -4
- package/scripts/lib/contract.mjs +53 -4
- package/scripts/lib/decision-log.mjs +121 -15
- package/scripts/lib/draft.mjs +127 -20
- package/scripts/lib/frontmatter.mjs +45 -9
- package/scripts/lib/heal.mjs +5 -0
- package/scripts/lib/intent.mjs +370 -4
- package/scripts/lib/join-constants.mjs +22 -0
- package/scripts/lib/join.mjs +917 -0
- package/scripts/lib/nest-applier.mjs +395 -32
- package/scripts/lib/operators.mjs +472 -38
- package/scripts/lib/orchestrator.mjs +419 -12
- package/scripts/lib/root-containment.mjs +351 -0
- package/scripts/lib/similarity-cache.mjs +115 -20
- package/scripts/lib/similarity.mjs +11 -0
- package/scripts/lib/soft-dag.mjs +726 -0
- package/scripts/lib/tier2-protocol.mjs +169 -37
- package/scripts/lib/tiered.mjs +42 -18
- package/scripts/lib/validate.mjs +22 -0
package/scripts/lib/intent.mjs
CHANGED
|
@@ -33,6 +33,16 @@
|
|
|
33
33
|
// INT-12 ambiguity reached interactive resolution in a non-TTY
|
|
34
34
|
// context (emitted by cli.mjs when NonInteractiveError fires)
|
|
35
35
|
// INT-13 unknown --quality-mode value
|
|
36
|
+
// INT-14 invalid --fanout-target value (must be a positive integer
|
|
37
|
+
// in [FANOUT_TARGET_MIN, FANOUT_TARGET_MAX])
|
|
38
|
+
// INT-14a --fanout-target used on a subcommand other than build /
|
|
39
|
+
// rebuild (balance enforcement is build/rebuild-only)
|
|
40
|
+
// INT-15 invalid --max-depth value (must be a positive integer in
|
|
41
|
+
// [MAX_DEPTH_MIN, MAX_DEPTH_MAX])
|
|
42
|
+
// INT-15a --max-depth used on a subcommand other than build /
|
|
43
|
+
// rebuild (balance enforcement is build/rebuild-only)
|
|
44
|
+
// INT-16a --soft-dag-parents used on a subcommand other than build /
|
|
45
|
+
// rebuild (soft-DAG synthesis is build/rebuild-only)
|
|
36
46
|
//
|
|
37
47
|
// Plan shape (status === "ok"):
|
|
38
48
|
// {
|
|
@@ -50,7 +60,11 @@
|
|
|
50
60
|
|
|
51
61
|
import { spawnSync } from "node:child_process";
|
|
52
62
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
53
|
-
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
63
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
64
|
+
import {
|
|
65
|
+
DEFAULT_COLLISION_POLICY,
|
|
66
|
+
VALID_COLLISION_POLICIES,
|
|
67
|
+
} from "./join-constants.mjs";
|
|
54
68
|
import {
|
|
55
69
|
defaultSiblingPath,
|
|
56
70
|
hasPrivateGit,
|
|
@@ -64,14 +78,53 @@ export const VALID_LAYOUT_MODES = Object.freeze(["sibling", "in-place", "hosted"
|
|
|
64
78
|
// rejects typos BEFORE the orchestrator runs, avoiding expensive
|
|
65
79
|
// rollbacks on a trivial flag error. Must stay in sync with
|
|
66
80
|
// tiered.mjs:QUALITY_MODES — the unit test
|
|
67
|
-
// tests/unit/intent-resolve.test.mjs
|
|
68
|
-
//
|
|
81
|
+
// tests/unit/intent-resolve.test.mjs::"VALID_QUALITY_MODES is in
|
|
82
|
+
// sync with tiered.mjs::QUALITY_MODES" pins that both lists contain
|
|
83
|
+
// the same modes in the same order so a future drift fails loud
|
|
84
|
+
// instead of silently letting one layer accept a mode the other
|
|
85
|
+
// rejects.
|
|
69
86
|
export const VALID_QUALITY_MODES = Object.freeze([
|
|
70
87
|
"tiered-fast",
|
|
71
88
|
"claude-first",
|
|
72
|
-
"
|
|
89
|
+
"deterministic",
|
|
73
90
|
]);
|
|
74
91
|
|
|
92
|
+
// Range bounds for the balance-enforcement flags (`--fanout-target`,
|
|
93
|
+
// `--max-depth`). Exported so tests and the balance module can
|
|
94
|
+
// reference them directly without re-stating the literal values.
|
|
95
|
+
//
|
|
96
|
+
// The ranges cover the band where a post-convergence rebalance pass
|
|
97
|
+
// has a meaningful effect. Fanout 1 is degenerate (every split forces
|
|
98
|
+
// single-child chains); 100+ is effectively unbounded for any real
|
|
99
|
+
// corpus (the cluster detector caps individual clusters at 8).
|
|
100
|
+
// Max depth starts at 1 — the "no nesting"/flat-layout case is out
|
|
101
|
+
// of this flag's scope; balance's purpose is to flatten OVERDEEP
|
|
102
|
+
// branches, not to undo nesting the pipeline already decided to
|
|
103
|
+
// create, and the regular NEST operator handles the no-nesting
|
|
104
|
+
// starting condition anyway. 11+ max-depth is deeper than any
|
|
105
|
+
// hand-authored corpus anyone has reported, so the flag becomes a
|
|
106
|
+
// silent no-op above that. Out-of-range values are treated as user
|
|
107
|
+
// errors at intent time so the flag is never a silent no-op at
|
|
108
|
+
// runtime.
|
|
109
|
+
export const FANOUT_TARGET_MIN = 2;
|
|
110
|
+
export const FANOUT_TARGET_MAX = 100;
|
|
111
|
+
export const MAX_DEPTH_MIN = 1;
|
|
112
|
+
export const MAX_DEPTH_MAX = 10;
|
|
113
|
+
|
|
114
|
+
// Parse + validate an integer-in-range flag value. Returns a short
|
|
115
|
+
// human-readable reason string when the value is invalid (for use in
|
|
116
|
+
// the ambiguity error), or null when valid. Factored out so INT-14
|
|
117
|
+
// and INT-15 share one implementation.
|
|
118
|
+
function invalidIntInRange(raw, min, max) {
|
|
119
|
+
if (typeof raw !== "string" || !/^\d+$/.test(raw)) {
|
|
120
|
+
return `expected a positive integer, got "${raw}"`;
|
|
121
|
+
}
|
|
122
|
+
const n = Number.parseInt(raw, 10);
|
|
123
|
+
if (n < min) return `${n} is below the minimum ${min}`;
|
|
124
|
+
if (n > max) return `${n} is above the maximum ${max}`;
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
75
128
|
export function ok(plan) {
|
|
76
129
|
return { status: "ok", plan };
|
|
77
130
|
}
|
|
@@ -265,6 +318,153 @@ export function resolveIntent(ctx) {
|
|
|
265
318
|
"--quality-mode",
|
|
266
319
|
);
|
|
267
320
|
}
|
|
321
|
+
// Validate `LLM_WIKI_QUALITY_MODE` here too, but only for
|
|
322
|
+
// subcommands that actually consult runtime quality-mode
|
|
323
|
+
// resolution (build / extend / rebuild / fix / join all call
|
|
324
|
+
// through `resolveQualityMode` in the orchestrator). Commands
|
|
325
|
+
// like `rollback`, `validate`, `init`, `heal` never touch
|
|
326
|
+
// tiered.mjs, so blocking them on a stale env var — especially
|
|
327
|
+
// `rollback`, which is a recovery path — would make a trivial
|
|
328
|
+
// shell-config typo catastrophic. Without this gate, a stale
|
|
329
|
+
// `LLM_WIKI_QUALITY_MODE=tier0-only` would lock the user out of
|
|
330
|
+
// recovering the wiki they were trying to rollback.
|
|
331
|
+
//
|
|
332
|
+
// Symmetric with the flag path — same code, same suggestions,
|
|
333
|
+
// same exit-2 ambiguous shape — for the subcommands where the
|
|
334
|
+
// env var is actually consumed.
|
|
335
|
+
const QUALITY_MODE_CONSUMERS = new Set([
|
|
336
|
+
"build",
|
|
337
|
+
"extend",
|
|
338
|
+
"rebuild",
|
|
339
|
+
"fix",
|
|
340
|
+
"join",
|
|
341
|
+
]);
|
|
342
|
+
const envQualityMode = process.env.LLM_WIKI_QUALITY_MODE;
|
|
343
|
+
if (
|
|
344
|
+
QUALITY_MODE_CONSUMERS.has(subcommand) &&
|
|
345
|
+
!f.quality_mode &&
|
|
346
|
+
envQualityMode &&
|
|
347
|
+
!VALID_QUALITY_MODES.includes(envQualityMode)
|
|
348
|
+
) {
|
|
349
|
+
return ambiguous(
|
|
350
|
+
"INT-13",
|
|
351
|
+
`unknown LLM_WIKI_QUALITY_MODE value "${envQualityMode}"`,
|
|
352
|
+
VALID_QUALITY_MODES.map((m) => ({
|
|
353
|
+
description: `use ${m} quality mode`,
|
|
354
|
+
flag: `--quality-mode ${m}`,
|
|
355
|
+
})),
|
|
356
|
+
"LLM_WIKI_QUALITY_MODE",
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
// Balance-enforcement flags are build/rebuild-only. The CLI parser
|
|
360
|
+
// accepts them globally (to produce a uniform flag table), so intent
|
|
361
|
+
// has to gate the subcommand explicitly — otherwise `fix`, `join`,
|
|
362
|
+
// `rollback` etc. would silently carry them through to the
|
|
363
|
+
// orchestrator, where the hook would fire and apply unexpected
|
|
364
|
+
// structural mutations outside the documented surface.
|
|
365
|
+
const BALANCE_FLAG_SUBCOMMANDS = new Set(["build", "rebuild"]);
|
|
366
|
+
// Subcommands that operate on an existing wiki (take a wiki path).
|
|
367
|
+
// For these, the natural remediation is `rebuild` (which walks an
|
|
368
|
+
// existing wiki). For source-operating subcommands with no wiki
|
|
369
|
+
// path, `build` is the right suggestion. `build` itself isn't in
|
|
370
|
+
// either set since it can't reach this branch.
|
|
371
|
+
const WIKI_OPERATING = new Set(["fix", "validate", "extend", "rebuild", "rollback", "join"]);
|
|
372
|
+
const suggestedSub = WIKI_OPERATING.has(subcommand) ? "rebuild" : "build";
|
|
373
|
+
if (f.fanout_target !== undefined) {
|
|
374
|
+
if (!BALANCE_FLAG_SUBCOMMANDS.has(subcommand)) {
|
|
375
|
+
return ambiguous(
|
|
376
|
+
"INT-14a",
|
|
377
|
+
`--fanout-target is only supported on build / rebuild (got "${subcommand}")`,
|
|
378
|
+
[
|
|
379
|
+
{
|
|
380
|
+
description: "drop --fanout-target",
|
|
381
|
+
flag: "(remove --fanout-target)",
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
description: `run on a ${suggestedSub} instead`,
|
|
385
|
+
flag: `${suggestedSub} ... --fanout-target <N>`,
|
|
386
|
+
},
|
|
387
|
+
],
|
|
388
|
+
"--fanout-target",
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
const bad = invalidIntInRange(
|
|
392
|
+
f.fanout_target,
|
|
393
|
+
FANOUT_TARGET_MIN,
|
|
394
|
+
FANOUT_TARGET_MAX,
|
|
395
|
+
);
|
|
396
|
+
if (bad) {
|
|
397
|
+
return ambiguous(
|
|
398
|
+
"INT-14",
|
|
399
|
+
`invalid --fanout-target "${f.fanout_target}" (${bad})`,
|
|
400
|
+
[
|
|
401
|
+
{
|
|
402
|
+
description: `use a positive integer in [${FANOUT_TARGET_MIN}, ${FANOUT_TARGET_MAX}]`,
|
|
403
|
+
flag: "--fanout-target <integer>",
|
|
404
|
+
},
|
|
405
|
+
],
|
|
406
|
+
"--fanout-target",
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (f.max_depth !== undefined) {
|
|
411
|
+
if (!BALANCE_FLAG_SUBCOMMANDS.has(subcommand)) {
|
|
412
|
+
return ambiguous(
|
|
413
|
+
"INT-15a",
|
|
414
|
+
`--max-depth is only supported on build / rebuild (got "${subcommand}")`,
|
|
415
|
+
[
|
|
416
|
+
{
|
|
417
|
+
description: "drop --max-depth",
|
|
418
|
+
flag: "(remove --max-depth)",
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
description: `run on a ${suggestedSub} instead`,
|
|
422
|
+
flag: `${suggestedSub} ... --max-depth <D>`,
|
|
423
|
+
},
|
|
424
|
+
],
|
|
425
|
+
"--max-depth",
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
const bad = invalidIntInRange(f.max_depth, MAX_DEPTH_MIN, MAX_DEPTH_MAX);
|
|
429
|
+
if (bad) {
|
|
430
|
+
return ambiguous(
|
|
431
|
+
"INT-15",
|
|
432
|
+
`invalid --max-depth "${f.max_depth}" (${bad})`,
|
|
433
|
+
[
|
|
434
|
+
{
|
|
435
|
+
description: `use a positive integer in [${MAX_DEPTH_MIN}, ${MAX_DEPTH_MAX}]`,
|
|
436
|
+
flag: "--max-depth <integer>",
|
|
437
|
+
},
|
|
438
|
+
],
|
|
439
|
+
"--max-depth",
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
// --soft-dag-parents shares balance's subcommand scope (build /
|
|
444
|
+
// rebuild only). Same defense-in-depth as INT-14a / INT-15a: the
|
|
445
|
+
// CLI parser accepts the flag globally, so intent has to gate
|
|
446
|
+
// explicitly — otherwise `fix`, `join`, `rollback` etc. would
|
|
447
|
+
// silently carry the flag through to the orchestrator and rewrite
|
|
448
|
+
// parents[] on every leaf, outside the documented surface.
|
|
449
|
+
if (f.soft_dag_parents === true) {
|
|
450
|
+
if (!BALANCE_FLAG_SUBCOMMANDS.has(subcommand)) {
|
|
451
|
+
return ambiguous(
|
|
452
|
+
"INT-16a",
|
|
453
|
+
`--soft-dag-parents is only supported on build / rebuild (got "${subcommand}")`,
|
|
454
|
+
[
|
|
455
|
+
{
|
|
456
|
+
description: "drop --soft-dag-parents",
|
|
457
|
+
flag: "(remove --soft-dag-parents)",
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
description: `run on a ${suggestedSub} instead`,
|
|
461
|
+
flag: `${suggestedSub} ... --soft-dag-parents`,
|
|
462
|
+
},
|
|
463
|
+
],
|
|
464
|
+
"--soft-dag-parents",
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
268
468
|
if (f.layout_mode === "in-place" && f.target) {
|
|
269
469
|
return ambiguous(
|
|
270
470
|
"INT-09a",
|
|
@@ -324,6 +524,172 @@ export function resolveIntent(ctx) {
|
|
|
324
524
|
});
|
|
325
525
|
}
|
|
326
526
|
|
|
527
|
+
// ─── Join: N >= 2 existing wikis into a unified output ───────────
|
|
528
|
+
//
|
|
529
|
+
// Positional shape: `join <wiki-a> <wiki-b> [<wiki-c>...]`. The
|
|
530
|
+
// `--target` flag is REQUIRED (where to write the unified output)
|
|
531
|
+
// — join is inherently hosted-mode because the output belongs
|
|
532
|
+
// neither next to source A nor source B. Each source must exist
|
|
533
|
+
// and be a skill-managed wiki (have `.llmwiki/git`). Target must
|
|
534
|
+
// not already exist (or must be empty): the CLI materialises into
|
|
535
|
+
// a fresh tree so the source wikis stay strictly read-only.
|
|
536
|
+
//
|
|
537
|
+
// `--id-collision` selects the cross-source id-collision policy
|
|
538
|
+
// (`namespace` / `merge` / `ask`); defaults to `namespace`. The
|
|
539
|
+
// value is validated here against the same canonical list
|
|
540
|
+
// scripts/lib/join.mjs exports, so a typo fails at the intent
|
|
541
|
+
// layer with structured INT-17 rather than burning a pre-op
|
|
542
|
+
// snapshot and rolling back.
|
|
543
|
+
if (subcommand === "join") {
|
|
544
|
+
if (args.length < 2) {
|
|
545
|
+
return ambiguous(
|
|
546
|
+
"INT-06",
|
|
547
|
+
`join requires at least 2 <wiki-path> positionals (got ${args.length})`,
|
|
548
|
+
[
|
|
549
|
+
{
|
|
550
|
+
description: "specify two or more source wikis",
|
|
551
|
+
flag: "join <wiki-a> <wiki-b> [<wiki-c>...]",
|
|
552
|
+
},
|
|
553
|
+
],
|
|
554
|
+
"positional wiki paths",
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
const sources = args.map((a) => absolute(cwd, a));
|
|
558
|
+
for (const s of sources) {
|
|
559
|
+
if (!existsSync(s)) {
|
|
560
|
+
return ambiguous(
|
|
561
|
+
"INT-06",
|
|
562
|
+
`join: source wiki ${s} does not exist`,
|
|
563
|
+
[
|
|
564
|
+
{
|
|
565
|
+
description: "point at an existing skill-managed wiki",
|
|
566
|
+
flag: `join <existing-wiki> <existing-wiki>`,
|
|
567
|
+
},
|
|
568
|
+
],
|
|
569
|
+
"each positional must exist",
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
if (!hasPrivateGit(s)) {
|
|
573
|
+
return ambiguous(
|
|
574
|
+
"INT-06",
|
|
575
|
+
`join: ${s} is not a skill-managed wiki (no .llmwiki/git)`,
|
|
576
|
+
[
|
|
577
|
+
{
|
|
578
|
+
description: "build each source first",
|
|
579
|
+
flag: `build ${s} --layout-mode in-place`,
|
|
580
|
+
},
|
|
581
|
+
],
|
|
582
|
+
"each source must be a managed wiki",
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
if (!f.target) {
|
|
587
|
+
return ambiguous(
|
|
588
|
+
"INT-09b",
|
|
589
|
+
`join requires --target <path> (join output is always hosted; see guide/operations/ingest/join.md)`,
|
|
590
|
+
[
|
|
591
|
+
{
|
|
592
|
+
description: "set an explicit output path",
|
|
593
|
+
flag: "--target <path/for/unified/wiki>",
|
|
594
|
+
},
|
|
595
|
+
],
|
|
596
|
+
"--target",
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
const target = absolute(cwd, f.target);
|
|
600
|
+
// Source immutability guard: --target must not equal, nest
|
|
601
|
+
// under, or contain any source wiki. Join writes to target;
|
|
602
|
+
// any path relationship between target and a source means the
|
|
603
|
+
// write would either clobber source files or place output
|
|
604
|
+
// inside the source tree (violating the documented "sources
|
|
605
|
+
// are read-only" guarantee). Detect both subpath directions
|
|
606
|
+
// with `path.relative` and reject with INT-18 before the
|
|
607
|
+
// pre-op snapshot fires. This check runs BEFORE the
|
|
608
|
+
// empty-target check — a target that equals or contains a
|
|
609
|
+
// source would otherwise be (correctly) flagged as non-empty
|
|
610
|
+
// under INT-01, masking the more specific semantic problem.
|
|
611
|
+
for (const s of sources) {
|
|
612
|
+
const relTargetFromSource = relative(s, target);
|
|
613
|
+
const relSourceFromTarget = relative(target, s);
|
|
614
|
+
const targetUnderSource =
|
|
615
|
+
relTargetFromSource === "" ||
|
|
616
|
+
(!relTargetFromSource.startsWith("..") &&
|
|
617
|
+
!isAbsolute(relTargetFromSource));
|
|
618
|
+
const sourceUnderTarget =
|
|
619
|
+
relSourceFromTarget !== "" &&
|
|
620
|
+
!relSourceFromTarget.startsWith("..") &&
|
|
621
|
+
!isAbsolute(relSourceFromTarget);
|
|
622
|
+
if (targetUnderSource || sourceUnderTarget) {
|
|
623
|
+
return ambiguous(
|
|
624
|
+
"INT-18",
|
|
625
|
+
`join: --target ${target} must not equal, nest under, or contain any source wiki (conflicts with ${s})`,
|
|
626
|
+
[
|
|
627
|
+
{
|
|
628
|
+
description:
|
|
629
|
+
"pick a target path outside every source wiki tree",
|
|
630
|
+
flag: "--target <path-outside-sources>",
|
|
631
|
+
},
|
|
632
|
+
],
|
|
633
|
+
"--target must not overlap sources",
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
if (existsSync(target)) {
|
|
638
|
+
// Allow an empty directory but nothing else — the target must
|
|
639
|
+
// be safe to materialise into without clobbering user data.
|
|
640
|
+
let entries;
|
|
641
|
+
try {
|
|
642
|
+
entries = readdirSync(target);
|
|
643
|
+
} catch {
|
|
644
|
+
entries = null;
|
|
645
|
+
}
|
|
646
|
+
if (!entries || entries.length > 0) {
|
|
647
|
+
return ambiguous(
|
|
648
|
+
"INT-01",
|
|
649
|
+
`join: --target path ${target} already exists and is not empty`,
|
|
650
|
+
[
|
|
651
|
+
{
|
|
652
|
+
description: "pick a fresh output path",
|
|
653
|
+
flag: "--target <new-path>",
|
|
654
|
+
},
|
|
655
|
+
],
|
|
656
|
+
"--target must be empty or new",
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
// Share VALID_COLLISION_POLICIES with `join.mjs` — the single
|
|
661
|
+
// canonical list. A local copy here risked silent drift if the
|
|
662
|
+
// set gained or lost a policy; importing keeps the intent
|
|
663
|
+
// validator and the runtime in lockstep.
|
|
664
|
+
const policy = f.id_collision || DEFAULT_COLLISION_POLICY;
|
|
665
|
+
if (!VALID_COLLISION_POLICIES.includes(policy)) {
|
|
666
|
+
return ambiguous(
|
|
667
|
+
"INT-17",
|
|
668
|
+
`unknown --id-collision value "${policy}"`,
|
|
669
|
+
VALID_COLLISION_POLICIES.map((p) => ({
|
|
670
|
+
description: `use ${p} policy`,
|
|
671
|
+
flag: `--id-collision ${p}`,
|
|
672
|
+
})),
|
|
673
|
+
"--id-collision",
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
return ok({
|
|
677
|
+
operation: "join",
|
|
678
|
+
layout_mode: "hosted",
|
|
679
|
+
source: null,
|
|
680
|
+
sources,
|
|
681
|
+
target,
|
|
682
|
+
is_new_wiki: true,
|
|
683
|
+
flags: {
|
|
684
|
+
accept_dirty: f.accept_dirty === true,
|
|
685
|
+
no_prompt: f.no_prompt === true,
|
|
686
|
+
json_errors: f.json_errors === true || f.json === true,
|
|
687
|
+
id_collision: policy,
|
|
688
|
+
quality_mode: f.quality_mode,
|
|
689
|
+
},
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
|
|
327
693
|
// ─── Rebuild / fix: the positional IS the wiki, not a source ──────
|
|
328
694
|
// These operations read frontmatter from an existing wiki and write
|
|
329
695
|
// back in place. There is no separate source — the wiki is both.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// join-constants.mjs — the minimal shared constants between the
|
|
2
|
+
// runtime pipeline (`join.mjs`) and the intent layer
|
|
3
|
+
// (`intent.mjs`).
|
|
4
|
+
//
|
|
5
|
+
// Kept in its own file so that importing the policy allow-list
|
|
6
|
+
// at intent time does NOT drag in the full join pipeline module
|
|
7
|
+
// (ingest + convergence + indices + validation + every transitive
|
|
8
|
+
// dependency). CLI paths that never run a join — build, rebuild,
|
|
9
|
+
// fix, validate, rollback, init, heal, where — would otherwise
|
|
10
|
+
// pay the cold-start cost of loading `join.mjs` and its
|
|
11
|
+
// dependency graph on every invocation just to resolve the
|
|
12
|
+
// `--id-collision` flag. Keeping this module dependency-light
|
|
13
|
+
// (zero imports, plain string constants) keeps every non-join
|
|
14
|
+
// CLI startup path fast.
|
|
15
|
+
|
|
16
|
+
export const VALID_COLLISION_POLICIES = Object.freeze([
|
|
17
|
+
"namespace",
|
|
18
|
+
"merge",
|
|
19
|
+
"ask",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
export const DEFAULT_COLLISION_POLICY = "namespace";
|