@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
|
@@ -20,18 +20,34 @@
|
|
|
20
20
|
// - Batch read / write / merge helpers
|
|
21
21
|
// - Pollution-key defence for JSON parse
|
|
22
22
|
//
|
|
23
|
-
// Request shape (JSON
|
|
23
|
+
// Request shape (JSON, conforms to the open subagent.dispatch.v1 envelope
|
|
24
|
+
// with skill-specific extensions). What makeRequest() emits today:
|
|
24
25
|
// {
|
|
26
|
+
// kind: "subagent.dispatch.v1" (the wire-format literal)
|
|
25
27
|
// request_id: string, unique per batch
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
// | "draft_frontmatter" | "rebuild_plan_review"
|
|
29
|
-
// | "human_fix_item"
|
|
28
|
+
// role: "wiki-tier2-<tier2_kind>" (host maps to its native
|
|
29
|
+
// sub-agent type)
|
|
30
30
|
// prompt: natural-language question the sub-agent answers
|
|
31
31
|
// inputs: minimal per-kind inputs (frontmatter blobs, etc.)
|
|
32
|
+
// effort: "heavy" | "balanced" | "light" (provider-neutral hint)
|
|
32
33
|
// response_schema: JSON shape the sub-agent must return
|
|
33
|
-
//
|
|
34
|
-
//
|
|
34
|
+
// tier2_kind: "merge_decision" | "nest_decision" | "cluster_name"
|
|
35
|
+
// | "propose_structure" | "draft_frontmatter"
|
|
36
|
+
// | "rebuild_plan_review" | "human_fix_item"
|
|
37
|
+
// (skill extension: the per-Tier-2-request kind, which
|
|
38
|
+
// the wiki-runner routes on)
|
|
39
|
+
// model: optional explicit model override; host prefers this
|
|
40
|
+
// when set, else maps `effort` to its own lineup
|
|
41
|
+
//
|
|
42
|
+
// Deprecated aliases kept for one release (emitted with the exact pre-v1
|
|
43
|
+
// per-kind values so existing wiki-runner consumers stay byte-compatible):
|
|
44
|
+
// model_hint → preserved per-kind legacy model hint
|
|
45
|
+
// effort_hint → preserved per-kind legacy effort hint
|
|
46
|
+
//
|
|
47
|
+
// Legacy envelopes written by a PREVIOUS release (where top-level `kind`
|
|
48
|
+
// WAS the Tier 2 kind and there was no `tier2_kind`/`role`) are still
|
|
49
|
+
// accepted on read: validateRequest and tier2KindOf both tolerate that
|
|
50
|
+
// shape so in-flight pending files resume across the upgrade.
|
|
35
51
|
// }
|
|
36
52
|
//
|
|
37
53
|
// Response shape (JSON):
|
|
@@ -57,30 +73,39 @@ import { dirname, join } from "node:path";
|
|
|
57
73
|
|
|
58
74
|
export const TIER2_EXIT_CODE = 7;
|
|
59
75
|
|
|
60
|
-
// The default
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
76
|
+
// The default effort matrix from guide/tiered-ai.md. Each request
|
|
77
|
+
// kind maps to an effort hint that the host harness translates to a
|
|
78
|
+
// model from its own lineup. These are hints, not mandates — the
|
|
79
|
+
// wiki-runner may override per-session by setting an explicit
|
|
80
|
+
// `model` on the request.
|
|
81
|
+
//
|
|
82
|
+
// Effort enum (provider-neutral):
|
|
83
|
+
// "heavy" — prior `opus` + high; deepest reasoning task
|
|
84
|
+
// "balanced" — prior `sonnet`/`opus` + medium; structural judgement
|
|
85
|
+
// "light" — prior `sonnet`/`haiku` + low; quick decisions
|
|
64
86
|
export const TIER2_DEFAULTS = Object.freeze({
|
|
65
87
|
merge_decision: {
|
|
66
|
-
|
|
67
|
-
|
|
88
|
+
effort: "light",
|
|
89
|
+
legacy_model_hint: "sonnet",
|
|
90
|
+
legacy_effort_hint: "low",
|
|
68
91
|
response_schema: {
|
|
69
92
|
decision: "same|different|undecidable",
|
|
70
93
|
reason: "string",
|
|
71
94
|
},
|
|
72
95
|
},
|
|
73
96
|
nest_decision: {
|
|
74
|
-
|
|
75
|
-
|
|
97
|
+
effort: "balanced",
|
|
98
|
+
legacy_model_hint: "sonnet",
|
|
99
|
+
legacy_effort_hint: "medium",
|
|
76
100
|
response_schema: {
|
|
77
101
|
decision: "nest|keep_flat|undecidable",
|
|
78
102
|
reason: "string",
|
|
79
103
|
},
|
|
80
104
|
},
|
|
81
105
|
cluster_name: {
|
|
82
|
-
|
|
83
|
-
|
|
106
|
+
effort: "light",
|
|
107
|
+
legacy_model_hint: "sonnet",
|
|
108
|
+
legacy_effort_hint: "low",
|
|
84
109
|
response_schema: {
|
|
85
110
|
slug: "kebab-case-slug",
|
|
86
111
|
purpose: "string",
|
|
@@ -92,12 +117,13 @@ export const TIER2_DEFAULTS = Object.freeze({
|
|
|
92
117
|
// ids) plus the leaves that should remain as siblings. This is
|
|
93
118
|
// the "Tier 2 gets first dibs" escalation and fires BEFORE the
|
|
94
119
|
// math-based cluster detector on every non-already-nested
|
|
95
|
-
// directory.
|
|
96
|
-
//
|
|
97
|
-
//
|
|
120
|
+
// directory. balanced effort because the task is a structural
|
|
121
|
+
// judgement call over many inputs that benefits from a strong
|
|
122
|
+
// reasoning model.
|
|
98
123
|
propose_structure: {
|
|
99
|
-
|
|
100
|
-
|
|
124
|
+
effort: "balanced",
|
|
125
|
+
legacy_model_hint: "opus",
|
|
126
|
+
legacy_effort_hint: "medium",
|
|
101
127
|
response_schema: {
|
|
102
128
|
subcategories: "array of { slug, purpose, members[] }",
|
|
103
129
|
siblings: "array of leaf ids",
|
|
@@ -105,8 +131,9 @@ export const TIER2_DEFAULTS = Object.freeze({
|
|
|
105
131
|
},
|
|
106
132
|
},
|
|
107
133
|
draft_frontmatter: {
|
|
108
|
-
|
|
109
|
-
|
|
134
|
+
effort: "balanced",
|
|
135
|
+
legacy_model_hint: "sonnet",
|
|
136
|
+
legacy_effort_hint: "medium",
|
|
110
137
|
response_schema: {
|
|
111
138
|
focus: "string",
|
|
112
139
|
covers: "array of strings",
|
|
@@ -114,8 +141,9 @@ export const TIER2_DEFAULTS = Object.freeze({
|
|
|
114
141
|
},
|
|
115
142
|
},
|
|
116
143
|
rebuild_plan_review: {
|
|
117
|
-
|
|
118
|
-
|
|
144
|
+
effort: "heavy",
|
|
145
|
+
legacy_model_hint: "opus",
|
|
146
|
+
legacy_effort_hint: "high",
|
|
119
147
|
response_schema: {
|
|
120
148
|
approve: "boolean",
|
|
121
149
|
drop: "array of iteration ids",
|
|
@@ -123,8 +151,9 @@ export const TIER2_DEFAULTS = Object.freeze({
|
|
|
123
151
|
},
|
|
124
152
|
},
|
|
125
153
|
human_fix_item: {
|
|
126
|
-
|
|
127
|
-
|
|
154
|
+
effort: "light",
|
|
155
|
+
legacy_model_hint: "sonnet",
|
|
156
|
+
legacy_effort_hint: "low",
|
|
128
157
|
response_schema: {
|
|
129
158
|
action: "string",
|
|
130
159
|
rationale: "string",
|
|
@@ -132,6 +161,20 @@ export const TIER2_DEFAULTS = Object.freeze({
|
|
|
132
161
|
},
|
|
133
162
|
});
|
|
134
163
|
|
|
164
|
+
// The three valid provider-neutral effort values. Any other value
|
|
165
|
+
// is rejected by makeRequest rather than silently falling back to a
|
|
166
|
+
// legacy alias.
|
|
167
|
+
const VALID_EFFORTS = new Set(["heavy", "balanced", "light"]);
|
|
168
|
+
|
|
169
|
+
let deprecationWarned = false;
|
|
170
|
+
function warnDeprecatedAliasOnce() {
|
|
171
|
+
if (deprecationWarned) return;
|
|
172
|
+
deprecationWarned = true;
|
|
173
|
+
process.stderr.write(
|
|
174
|
+
"[skill-llm-wiki] tier2-protocol: `model_hint` and `effort_hint` are deprecated; pass `effort` and (optional) `model` instead.\n",
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
135
178
|
export const TIER2_KINDS = Object.freeze(Object.keys(TIER2_DEFAULTS));
|
|
136
179
|
|
|
137
180
|
// Pollution keys that would leak onto Object.prototype if we
|
|
@@ -187,8 +230,26 @@ export function listBatches(wikiRoot) {
|
|
|
187
230
|
// The builder fills in defaults from TIER2_DEFAULTS and validates
|
|
188
231
|
// the shape. `inputs` is kind-specific and kept small (a few
|
|
189
232
|
// frontmatter blobs at most) so batches stay under a few KB each.
|
|
233
|
+
//
|
|
234
|
+
// Wire shape: emitted envelopes conform to the open `subagent.dispatch.v1`
|
|
235
|
+
// envelope (see https://github.com/ctxr-dev/kit/blob/main/docs/subagent-dispatch-v1.md) so any Agent
|
|
236
|
+
// Skills harness can validate them. The Tier 2 per-request kind
|
|
237
|
+
// (`merge_decision`, `propose_structure`, …) lives on `tier2_kind`, NOT on
|
|
238
|
+
// the envelope's top-level `kind` field — `kind` MUST be the literal
|
|
239
|
+
// `"subagent.dispatch.v1"`. The skill-side `role` is derived from the Tier 2
|
|
240
|
+
// kind so the harness can map to its native sub-agent type.
|
|
241
|
+
//
|
|
242
|
+
// Legacy aliases `model_hint` / `effort_hint` are emitted alongside the
|
|
243
|
+
// canonical `effort` field for one release so wiki-runners that read the
|
|
244
|
+
// old names keep working. The schema's `additionalProperties: true` allows
|
|
245
|
+
// these as a documented extension profile.
|
|
246
|
+
|
|
247
|
+
const ROLE_PREFIX = "wiki-tier2-";
|
|
190
248
|
|
|
191
|
-
export function makeRequest(
|
|
249
|
+
export function makeRequest(
|
|
250
|
+
kind,
|
|
251
|
+
{ prompt, inputs, effort, model, model_hint, effort_hint, request_id } = {},
|
|
252
|
+
) {
|
|
192
253
|
if (!TIER2_KINDS.includes(kind)) {
|
|
193
254
|
throw new Error(`tier2-protocol: unknown kind "${kind}" (valid: ${TIER2_KINDS.join(", ")})`);
|
|
194
255
|
}
|
|
@@ -203,15 +264,44 @@ export function makeRequest(kind, { prompt, inputs, model_hint, effort_hint, req
|
|
|
203
264
|
}
|
|
204
265
|
const defaults = TIER2_DEFAULTS[kind];
|
|
205
266
|
const rid = request_id ?? deriveRequestId(kind, inputs);
|
|
206
|
-
|
|
267
|
+
|
|
268
|
+
// Accept deprecated aliases (with a one-shot stderr warning) but
|
|
269
|
+
// prefer the new names when both are set.
|
|
270
|
+
if ((model_hint !== undefined || effort_hint !== undefined) && effort === undefined && model === undefined) {
|
|
271
|
+
warnDeprecatedAliasOnce();
|
|
272
|
+
}
|
|
273
|
+
const resolvedEffort = effort ?? defaults.effort;
|
|
274
|
+
if (!VALID_EFFORTS.has(resolvedEffort)) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
`tier2-protocol: invalid effort "${resolvedEffort}" (allowed: ${[...VALID_EFFORTS].join(", ")})`,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
// Required v1 fields first (`kind`, `request_id`, `role`, `prompt`,
|
|
280
|
+
// `inputs`, `effort`); skill-specific extensions follow.
|
|
281
|
+
const out = {
|
|
282
|
+
kind: "subagent.dispatch.v1",
|
|
207
283
|
request_id: rid,
|
|
208
|
-
kind,
|
|
284
|
+
role: ROLE_PREFIX + kind,
|
|
209
285
|
prompt,
|
|
210
286
|
inputs,
|
|
287
|
+
effort: resolvedEffort,
|
|
211
288
|
response_schema: defaults.response_schema,
|
|
212
|
-
|
|
213
|
-
|
|
289
|
+
// Skill-specific extension: the per-Tier-2-request kind, used by the
|
|
290
|
+
// wiki-runner to route to the right inline handler / prompt template.
|
|
291
|
+
tier2_kind: kind,
|
|
292
|
+
// Deprecated aliases retained for one release; readers should migrate to
|
|
293
|
+
// `effort` (and optional `model`). These emit the EXACT pre-v1 per-kind
|
|
294
|
+
// `model_hint`/`effort_hint` values (stored on TIER2_DEFAULTS) so the
|
|
295
|
+
// deprecation window is byte-compatible — they are NOT derived from
|
|
296
|
+
// `effort` (which would change e.g. propose_structure's model_hint).
|
|
297
|
+
// A caller-supplied alias still wins.
|
|
298
|
+
model_hint: model_hint ?? defaults.legacy_model_hint,
|
|
299
|
+
effort_hint: effort_hint ?? defaults.legacy_effort_hint,
|
|
214
300
|
};
|
|
301
|
+
if (typeof model === "string" && model.length > 0) {
|
|
302
|
+
out.model = model;
|
|
303
|
+
}
|
|
304
|
+
return out;
|
|
215
305
|
}
|
|
216
306
|
|
|
217
307
|
// Deterministic request id: sha256(kind + canonical-JSON(inputs))
|
|
@@ -248,6 +338,28 @@ function canonicalJson(value) {
|
|
|
248
338
|
|
|
249
339
|
// ── Request validation ─────────────────────────────────────────────
|
|
250
340
|
|
|
341
|
+
/**
|
|
342
|
+
* Pull the per-request Tier 2 kind off an envelope.
|
|
343
|
+
*
|
|
344
|
+
* New v1-conformant envelopes carry it on `tier2_kind` (the wire `kind` is
|
|
345
|
+
* the literal `"subagent.dispatch.v1"`). Legacy envelopes (pre-v1
|
|
346
|
+
* conformance) put it on `kind`. This helper accepts either so on-disk
|
|
347
|
+
* envelopes from a previous release continue to resolve correctly.
|
|
348
|
+
*/
|
|
349
|
+
export function tier2KindOf(req) {
|
|
350
|
+
if (!req || typeof req !== "object") return null;
|
|
351
|
+
// Only accept a `tier2_kind` that names a recognised kind; an unknown or
|
|
352
|
+
// malformed value must NOT be treated as valid downstream.
|
|
353
|
+
if (typeof req.tier2_kind === "string" && TIER2_KINDS.includes(req.tier2_kind)) {
|
|
354
|
+
return req.tier2_kind;
|
|
355
|
+
}
|
|
356
|
+
// Legacy fallback: `kind` was the per-request tier-2 kind before v1 conformance.
|
|
357
|
+
if (typeof req.kind === "string" && TIER2_KINDS.includes(req.kind)) {
|
|
358
|
+
return req.kind;
|
|
359
|
+
}
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
|
|
251
363
|
export function validateRequest(req) {
|
|
252
364
|
if (!req || typeof req !== "object") {
|
|
253
365
|
throw new Error("tier2-protocol: request must be an object");
|
|
@@ -258,8 +370,24 @@ export function validateRequest(req) {
|
|
|
258
370
|
if (typeof req.request_id !== "string" || req.request_id.length === 0) {
|
|
259
371
|
throw new Error("tier2-protocol: request.request_id must be a non-empty string");
|
|
260
372
|
}
|
|
261
|
-
|
|
262
|
-
|
|
373
|
+
// Envelope `kind` is REQUIRED. It must be the v1 wire constant
|
|
374
|
+
// "subagent.dispatch.v1" OR — for legacy pre-v1 envelopes — itself one of
|
|
375
|
+
// the Tier 2 kinds. Omitting it (even when a valid `tier2_kind` is present)
|
|
376
|
+
// is a malformed envelope: an envelope with no `kind` is neither v1 nor a
|
|
377
|
+
// recognised legacy shape, and must not be writable to a pending file.
|
|
378
|
+
if (typeof req.kind !== "string" || req.kind.length === 0) {
|
|
379
|
+
throw new Error(
|
|
380
|
+
`tier2-protocol: request.kind is required and must be "subagent.dispatch.v1" or a legacy tier-2 kind (${TIER2_KINDS.join(", ")})`,
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
if (req.kind !== "subagent.dispatch.v1" && !TIER2_KINDS.includes(req.kind)) {
|
|
384
|
+
throw new Error(
|
|
385
|
+
`tier2-protocol: request.kind must be "subagent.dispatch.v1" or a legacy tier-2 kind (${TIER2_KINDS.join(", ")}), got "${req.kind}"`,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
const t2 = tier2KindOf(req);
|
|
389
|
+
if (!t2) {
|
|
390
|
+
throw new Error(`tier2-protocol: request must declare tier2_kind (or legacy kind) from: ${TIER2_KINDS.join(", ")}`);
|
|
263
391
|
}
|
|
264
392
|
if (typeof req.prompt !== "string" || req.prompt.length === 0) {
|
|
265
393
|
throw new Error("tier2-protocol: request.prompt must be a non-empty string");
|
|
@@ -441,8 +569,12 @@ export function resolveFromFixture(fixtureMap, request) {
|
|
|
441
569
|
if (!request || typeof request.request_id !== "string") return null;
|
|
442
570
|
const specific = fixtureMap.get(request.request_id);
|
|
443
571
|
if (specific !== undefined) return specific;
|
|
444
|
-
|
|
445
|
-
|
|
572
|
+
// Wildcard lookups key on the per-Tier-2-request kind, not the v1
|
|
573
|
+
// envelope `kind` literal. Resolve via `tier2KindOf` so both new and
|
|
574
|
+
// legacy envelope shapes route correctly.
|
|
575
|
+
const t2 = tier2KindOf(request);
|
|
576
|
+
if (t2) {
|
|
577
|
+
const wildcard = fixtureMap.get(`__kind__${t2}`);
|
|
446
578
|
if (wildcard !== undefined) return wildcard;
|
|
447
579
|
}
|
|
448
580
|
return null;
|
package/scripts/lib/tiered.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// circuits the whole ladder.
|
|
11
11
|
//
|
|
12
12
|
// Three quality modes, selected via --quality-mode or the
|
|
13
|
-
// LLM_WIKI_QUALITY_MODE env var:
|
|
13
|
+
// LLM_WIKI_QUALITY_MODE env var (see resolveQualityMode):
|
|
14
14
|
//
|
|
15
15
|
// tiered-fast (default):
|
|
16
16
|
// Tier 0 → Tier 1 → Tier 2, the full ladder. Mid-band Tier 0
|
|
@@ -21,9 +21,16 @@
|
|
|
21
21
|
// obvious decisions) but anything in the Tier 0 mid-band goes
|
|
22
22
|
// straight to Tier 2, skipping Tier 1.
|
|
23
23
|
//
|
|
24
|
-
//
|
|
25
|
-
// Tier 0
|
|
26
|
-
//
|
|
24
|
+
// deterministic:
|
|
25
|
+
// Tier 0 + Tier 1 ladder, but the ladder terminates at Tier 1:
|
|
26
|
+
// mid-band Tier 0 escalates to Tier 1 (as in tiered-fast), but
|
|
27
|
+
// mid-band Tier 1 is resolved by a deterministic threshold
|
|
28
|
+
// (`TIER1_DETERMINISTIC_THRESHOLD`) instead of escalating to
|
|
29
|
+
// Tier 2. No LLM/sub-agent is ever consulted — every decision
|
|
30
|
+
// is produced from TF-IDF + MiniLM cosine alone, so repeated
|
|
31
|
+
// runs on the same inputs are byte-reproducible. This is the
|
|
32
|
+
// mode the clustering pipeline pairs with algorithmic HAC +
|
|
33
|
+
// auto-slug to produce deterministic wiki builds end-to-end.
|
|
27
34
|
//
|
|
28
35
|
// Tier 2 escalation contract: the skill's CLI runs under Node with
|
|
29
36
|
// no access to Claude Code's `Agent` tool, so it cannot spawn
|
|
@@ -64,11 +71,27 @@ import {
|
|
|
64
71
|
export const QUALITY_MODES = Object.freeze([
|
|
65
72
|
"tiered-fast",
|
|
66
73
|
"claude-first",
|
|
67
|
-
"
|
|
74
|
+
"deterministic",
|
|
68
75
|
]);
|
|
69
76
|
|
|
70
77
|
export const DEFAULT_QUALITY_MODE = "tiered-fast";
|
|
71
78
|
|
|
79
|
+
// Deterministic-mode split point for resolving mid-band Tier 1
|
|
80
|
+
// similarities. Derived as the midpoint of the Tier 1 mid-band so
|
|
81
|
+
// future tuning of the decisive-same / decisive-different thresholds
|
|
82
|
+
// propagates here without a separate code-change — no drift between
|
|
83
|
+
// "where the ladder says 'escalate'" and "where deterministic mode
|
|
84
|
+
// says 'same vs different'". Any pair whose Tier 1 cosine sits
|
|
85
|
+
// strictly above this is routed to "same"; anything at-or-below is
|
|
86
|
+
// routed to "different". In this mode there is no mid-band
|
|
87
|
+
// "undecidable" / pending-Tier-2 outcome — Tier 1 always produces a
|
|
88
|
+
// concrete branch without an LLM in the loop. (Note: Tier 0 can still
|
|
89
|
+
// produce an "undecidable" result on insufficient-text inputs — two
|
|
90
|
+
// empty frontmatters — independent of quality mode; that predates
|
|
91
|
+
// deterministic mode and is by design.)
|
|
92
|
+
export const TIER1_DETERMINISTIC_THRESHOLD =
|
|
93
|
+
(TIER1_DECISIVE_SAME + TIER1_DECISIVE_DIFFERENT) / 2;
|
|
94
|
+
|
|
72
95
|
export function resolveQualityMode(flags = {}) {
|
|
73
96
|
const fromFlag = flags.quality_mode;
|
|
74
97
|
const fromEnv = process.env.LLM_WIKI_QUALITY_MODE;
|
|
@@ -244,18 +267,6 @@ export async function decide(
|
|
|
244
267
|
}
|
|
245
268
|
|
|
246
269
|
// Mid-band Tier 0 → escalate. Behaviour depends on quality mode.
|
|
247
|
-
if (qualityMode === "tier0-only") {
|
|
248
|
-
const result = {
|
|
249
|
-
tier: 0,
|
|
250
|
-
similarity: t0.similarity,
|
|
251
|
-
decision: "undecidable",
|
|
252
|
-
confidence_band: t0.confidence_band,
|
|
253
|
-
reason: "tier0-only quality mode — mid-band left unresolved",
|
|
254
|
-
};
|
|
255
|
-
finaliseDecision(result, { a, b, hashA, hashB, wikiRoot, opId, operator, writeLog, writeCache });
|
|
256
|
-
return result;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
270
|
if (qualityMode === "claude-first") {
|
|
260
271
|
// Skip Tier 1 entirely, go straight to Tier 2.
|
|
261
272
|
return await escalateToTier2(
|
|
@@ -308,7 +319,20 @@ export async function decide(
|
|
|
308
319
|
finaliseDecision(result, { a, b, hashA, hashB, wikiRoot, opId, operator, writeLog, writeCache });
|
|
309
320
|
return result;
|
|
310
321
|
}
|
|
311
|
-
// Mid-band Tier 1
|
|
322
|
+
// Mid-band Tier 1. Branch on quality mode: deterministic resolves
|
|
323
|
+
// algorithmically, tiered-fast escalates to Tier 2.
|
|
324
|
+
if (qualityMode === "deterministic") {
|
|
325
|
+
const decision = sim > TIER1_DETERMINISTIC_THRESHOLD ? "same" : "different";
|
|
326
|
+
const result = {
|
|
327
|
+
tier: 1,
|
|
328
|
+
similarity: sim,
|
|
329
|
+
decision,
|
|
330
|
+
confidence_band: "deterministic-mid-band",
|
|
331
|
+
reason: `deterministic mode: sim ${sim.toFixed(3)} ${decision === "same" ? ">" : "≤"} ${TIER1_DETERMINISTIC_THRESHOLD}`,
|
|
332
|
+
};
|
|
333
|
+
finaliseDecision(result, { a, b, hashA, hashB, wikiRoot, opId, operator, writeLog, writeCache });
|
|
334
|
+
return result;
|
|
335
|
+
}
|
|
312
336
|
return await escalateToTier2(
|
|
313
337
|
a, b, hashA, hashB, wikiRoot, opId, operator,
|
|
314
338
|
sim, "tier1 mid-band", writeLog, writeCache,
|
package/scripts/lib/validate.mjs
CHANGED
|
@@ -133,6 +133,28 @@ export function validateWiki(wikiRoot) {
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
|
+
|
|
137
|
+
// LEAF-AT-WIKI-ROOT — the wiki root must hold only `index.md`
|
|
138
|
+
// plus subdirectories. Any `.md` file at the wiki root other
|
|
139
|
+
// than `index.md` itself violates the invariant, regardless of
|
|
140
|
+
// what its frontmatter `type:` claims. Keying off path rather
|
|
141
|
+
// than `data.type` catches the edge case of a hand-authored
|
|
142
|
+
// `foo.md` at root declared as `type: index` — it's still a
|
|
143
|
+
// loose root file the navigational model forbids. The rule is
|
|
144
|
+
// navigational: Claude reading `<root>/index.md` and following
|
|
145
|
+
// its `entries[]` should reach every leaf via a
|
|
146
|
+
// semantically-named category; loose root files bypass that
|
|
147
|
+
// mental model and bloat the top-level index.
|
|
148
|
+
const absDir = dirname(e.absolute);
|
|
149
|
+
const absName = basename(e.absolute);
|
|
150
|
+
if (absDir === wikiRoot && absName !== "index.md") {
|
|
151
|
+
push(
|
|
152
|
+
"error",
|
|
153
|
+
"LEAF-AT-WIKI-ROOT",
|
|
154
|
+
e.absolute,
|
|
155
|
+
`non-index markdown file at wiki root — must live in a subcategory (run 'fix' to contain)`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
136
158
|
}
|
|
137
159
|
|
|
138
160
|
return findings;
|