@holmes-lab/holmes-kit 0.2.0 → 0.3.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 +35 -0
- package/dist/.build-id +1 -1
- package/dist/holmes/assoc/assoc-arm.d.ts +94 -0
- package/dist/holmes/assoc/assoc-arm.js +187 -0
- package/dist/holmes/assoc/explore.d.ts +21 -0
- package/dist/holmes/assoc/explore.js +160 -0
- package/dist/holmes/assoc/impact-baseline.d.ts +13 -0
- package/dist/holmes/assoc/impact-baseline.js +48 -0
- package/dist/holmes/assoc/ppr.d.ts +64 -0
- package/dist/holmes/assoc/ppr.js +110 -0
- package/dist/holmes/cli/agents.d.ts +24 -11
- package/dist/holmes/cli/agents.js +93 -17
- package/dist/holmes/cli/codex-toml.d.ts +26 -0
- package/dist/holmes/cli/codex-toml.js +282 -0
- package/dist/holmes/cli/doctor.d.ts +56 -0
- package/dist/holmes/cli/doctor.js +348 -18
- package/dist/holmes/cli/index.js +16 -2
- package/dist/holmes/cli/init.js +78 -0
- package/dist/holmes/cli/interactive-prompt.js +4 -4
- package/dist/holmes/cli/mcp-launcher.d.ts +2 -2
- package/dist/holmes/cli/semantic-key.d.ts +19 -0
- package/dist/holmes/cli/semantic-key.js +93 -0
- package/dist/holmes/config/config.d.ts +9 -0
- package/dist/holmes/config/config.js +8 -1
- package/dist/holmes/cpg/language-capability.d.ts +65 -0
- package/dist/holmes/cpg/language-capability.js +145 -0
- package/dist/holmes/cpg/language-parser-walk.js +179 -34
- package/dist/holmes/cpg/language-parser.d.ts +1 -1
- package/dist/holmes/governance/ledger-rechain.d.ts +12 -0
- package/dist/holmes/governance/ledger-rechain.js +17 -2
- package/dist/holmes/governance/provenance-ledger.js +21 -0
- package/dist/holmes/guardrail/blind-spots.js +12 -1
- package/dist/holmes/guardrail/impact-gate.d.ts +77 -0
- package/dist/holmes/guardrail/impact-gate.js +263 -0
- package/dist/holmes/guardrail/write-target.d.ts +38 -1
- package/dist/holmes/guardrail/write-target.js +48 -4
- package/dist/holmes/hooks/adapters/antigravity.js +12 -1
- package/dist/holmes/hooks/corrupt-state-run.d.ts +33 -0
- package/dist/holmes/hooks/corrupt-state-run.js +16 -0
- package/dist/holmes/hooks/pre-tool-use.d.ts +26 -0
- package/dist/holmes/hooks/pre-tool-use.js +203 -11
- package/dist/holmes/hooks/stop.d.ts +65 -0
- package/dist/holmes/hooks/stop.js +200 -2
- package/dist/holmes/mcp/handlers.d.ts +88 -0
- package/dist/holmes/mcp/handlers.js +597 -16
- package/dist/holmes/mcp/maintenance-analyze.d.ts +435 -0
- package/dist/holmes/mcp/maintenance-analyze.js +994 -0
- package/dist/holmes/mcp/maintenance-evidence.d.ts +140 -0
- package/dist/holmes/mcp/maintenance-evidence.js +253 -0
- package/dist/holmes/mcp/tool-schemas.js +71 -0
- package/dist/holmes/project/root.js +3 -1
- package/dist/holmes/review/baseline-arm.d.ts +37 -0
- package/dist/holmes/review/baseline-arm.js +51 -0
- package/dist/holmes/review/captured-stdin-guard.d.ts +8 -0
- package/dist/holmes/review/captured-stdin-guard.js +48 -0
- package/dist/holmes/review/coherence-verify.d.ts +31 -0
- package/dist/holmes/review/coherence-verify.js +144 -0
- package/dist/holmes/review/commit-text.d.ts +50 -0
- package/dist/holmes/review/commit-text.js +76 -0
- package/dist/holmes/review/confidence-calibration.d.ts +39 -0
- package/dist/holmes/review/confidence-calibration.js +39 -0
- package/dist/holmes/review/content-baseline.d.ts +38 -0
- package/dist/holmes/review/content-baseline.js +103 -0
- package/dist/holmes/review/content-verify.d.ts +20 -0
- package/dist/holmes/review/content-verify.js +73 -0
- package/dist/holmes/review/dense-retrieval.d.ts +66 -0
- package/dist/holmes/review/dense-retrieval.js +97 -0
- package/dist/holmes/review/edge-quality.d.ts +44 -0
- package/dist/holmes/review/edge-quality.js +117 -0
- package/dist/holmes/review/evaluation-metrics.d.ts +138 -0
- package/dist/holmes/review/evaluation-metrics.js +175 -0
- package/dist/holmes/review/graph-verifier.d.ts +34 -0
- package/dist/holmes/review/graph-verifier.js +62 -0
- package/dist/holmes/review/hop-ablation.d.ts +100 -0
- package/dist/holmes/review/hop-ablation.js +89 -0
- package/dist/holmes/review/manual-baseline.d.ts +209 -0
- package/dist/holmes/review/manual-baseline.js +2846 -0
- package/dist/holmes/review/oracle-gap.d.ts +32 -0
- package/dist/holmes/review/oracle-gap.js +102 -0
- package/dist/holmes/review/point-in-time-replay.d.ts +41 -0
- package/dist/holmes/review/point-in-time-replay.js +161 -0
- package/dist/holmes/review/rank-diagnosis.d.ts +43 -0
- package/dist/holmes/review/rank-diagnosis.js +163 -0
- package/dist/holmes/review/replay-calibration.d.ts +62 -0
- package/dist/holmes/review/replay-calibration.js +83 -0
- package/dist/holmes/review/replay-corpus.d.ts +135 -0
- package/dist/holmes/review/replay-corpus.js +210 -0
- package/dist/holmes/review/run-replay.d.ts +260 -0
- package/dist/holmes/review/run-replay.js +729 -0
- package/dist/holmes/review/semantic-arm.d.ts +271 -0
- package/dist/holmes/review/semantic-arm.js +717 -0
- package/dist/holmes/review/semantic-retrieval.d.ts +55 -0
- package/dist/holmes/review/semantic-retrieval.js +156 -0
- package/dist/holmes/review/spec-layer-stats.d.ts +38 -0
- package/dist/holmes/review/spec-layer-stats.js +52 -0
- package/dist/holmes/review/temporal-prior.d.ts +33 -0
- package/dist/holmes/review/temporal-prior.js +53 -0
- package/dist/holmes/review/test-runner.d.ts +15 -0
- package/dist/holmes/review/test-runner.js +41 -4
- package/dist/holmes/review/test-selection-breadth.d.ts +75 -0
- package/dist/holmes/review/test-selection-breadth.js +57 -0
- package/dist/holmes/review/traceability-benchmark.d.ts +81 -0
- package/dist/holmes/review/traceability-benchmark.js +135 -0
- package/dist/holmes/review/union-verify.d.ts +12 -0
- package/dist/holmes/review/union-verify.js +70 -0
- package/dist/holmes/rtm/graph-store.d.ts +51 -0
- package/dist/holmes/rtm/graph-store.js +122 -0
- package/dist/holmes/rtm/incremental.d.ts +25 -1
- package/dist/holmes/rtm/incremental.js +18 -1
- package/dist/holmes/rtm/localize.d.ts +28 -0
- package/dist/holmes/rtm/localize.js +272 -14
- package/dist/holmes/rtm/rtm-builder.d.ts +118 -3
- package/dist/holmes/rtm/rtm-builder.js +265 -28
- package/dist/holmes/rtm/rtm-graph.d.ts +117 -2
- package/dist/holmes/rtm/rtm-graph.js +194 -31
- package/dist/holmes/rtm/taint-benchmark.d.ts +97 -0
- package/dist/holmes/rtm/taint-benchmark.js +141 -0
- package/dist/holmes/rtm/test-scope.js +24 -1
- package/dist/holmes/semantic/credentials.d.ts +15 -0
- package/dist/holmes/semantic/credentials.js +134 -0
- package/dist/holmes/semantic/embedder.d.ts +44 -0
- package/dist/holmes/semantic/embedder.js +185 -0
- package/dist/holmes/semantic/hit-rerank.d.ts +4 -0
- package/dist/holmes/semantic/hit-rerank.js +38 -0
- package/dist/holmes/semantic/tier.d.ts +37 -0
- package/dist/holmes/semantic/tier.js +54 -0
- package/dist/holmes/semantic/vector-cache.d.ts +11 -0
- package/dist/holmes/semantic/vector-cache.js +91 -0
- package/dist/holmes/spec/acceptance-quality.d.ts +81 -0
- package/dist/holmes/spec/acceptance-quality.js +169 -0
- package/dist/holmes/spec/validator.js +33 -1
- package/dist/holmes/spec/yaml-scalar.d.ts +1 -0
- package/dist/holmes/spec/yaml-scalar.js +43 -0
- package/package.json +1 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @implements A-SPEC-467
|
|
3
|
+
/**
|
|
4
|
+
* Personalized PageRank over the RTM/CPG edge set — the associative-recall primitive.
|
|
5
|
+
*
|
|
6
|
+
* Why this exists: the shipped retrieval primitives sit at two extremes. `issue_localize` stops at
|
|
7
|
+
* one spec hop (its own comment defers anything further), and `rtm_impact` returns an UNGRADED
|
|
8
|
+
* transitive closure — measured on the replay corpus, 84% of a commit's co-changed files are
|
|
9
|
+
* reachable through that closure while the analysis names 11% of them: the bottleneck is selection
|
|
10
|
+
* within reach, not reach. What is missing between the two is graded multi-hop mass —
|
|
11
|
+
* Score(1-hop) > Score(2-hop) > … > 0 — which is exactly what a damped random walk provides.
|
|
12
|
+
*
|
|
13
|
+
* Eleven graph/semantic mechanisms were measured and rejected on this corpus before this one
|
|
14
|
+
* (binary k-hop expansion, caller closure, co-change retrieval, dense embeddings, …). PPR is
|
|
15
|
+
* structurally different from all of them: it produces a RANKING over nodes rather than an
|
|
16
|
+
* admission set, and the geometric damping suppresses the crowd problem that sank the closure.
|
|
17
|
+
* That difference is the reason this module exists; whether it PAYS is decided by the replay arm
|
|
18
|
+
* (rerank and admit modes scored separately, three corpora), never assumed here.
|
|
19
|
+
*
|
|
20
|
+
* Deterministic, pure, no I/O. PPR mass never enters a gate decision — association proposes,
|
|
21
|
+
* the deterministic layers judge.
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.buildAdjacency = buildAdjacency;
|
|
25
|
+
exports.personalizedPageRank = personalizedPageRank;
|
|
26
|
+
/** Build the weighted, direction-aware, hub-damped transition structure once per graph. */
|
|
27
|
+
function buildAdjacency(edges, profile) {
|
|
28
|
+
// Degree first (undirected, unweighted): the hub penalty divides by how CONNECTED the target is,
|
|
29
|
+
// not by how heavily this particular rel weighs it.
|
|
30
|
+
const degree = new Map();
|
|
31
|
+
const kept = [];
|
|
32
|
+
for (const e of edges) {
|
|
33
|
+
const w = profile.rels[e.rel];
|
|
34
|
+
if (w === undefined || (w.fwd <= 0 && w.back <= 0))
|
|
35
|
+
continue;
|
|
36
|
+
kept.push({ src: e.src, dst: e.dst, fwd: w.fwd, back: w.back });
|
|
37
|
+
degree.set(e.src, (degree.get(e.src) ?? 0) + 1);
|
|
38
|
+
degree.set(e.dst, (degree.get(e.dst) ?? 0) + 1);
|
|
39
|
+
}
|
|
40
|
+
const damp = (node) => profile.hubExponent > 0 ? Math.pow(degree.get(node) ?? 1, profile.hubExponent) : 1;
|
|
41
|
+
const raw = new Map();
|
|
42
|
+
const nodes = new Set();
|
|
43
|
+
const add = (from, to, w) => {
|
|
44
|
+
if (w <= 0)
|
|
45
|
+
return;
|
|
46
|
+
(raw.get(from) ?? raw.set(from, []).get(from)).push({ to, w: w / damp(to) });
|
|
47
|
+
};
|
|
48
|
+
for (const e of kept) {
|
|
49
|
+
nodes.add(e.src);
|
|
50
|
+
nodes.add(e.dst);
|
|
51
|
+
add(e.src, e.dst, e.fwd);
|
|
52
|
+
add(e.dst, e.src, e.back);
|
|
53
|
+
}
|
|
54
|
+
// Normalize out-mass per node so the walk is a probability distribution.
|
|
55
|
+
for (const [, list] of raw) {
|
|
56
|
+
const total = list.reduce((a, b) => a + b.w, 0);
|
|
57
|
+
for (const o of list)
|
|
58
|
+
o.w /= total;
|
|
59
|
+
}
|
|
60
|
+
return { out: raw, nodes };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Power iteration: v' = d·(Wᵀv + danglingMass·s) + (1−d)·s.
|
|
64
|
+
*
|
|
65
|
+
* Dangling mass refluxes to the SEEDS, not uniformly — a node with no out-edges returns the walk
|
|
66
|
+
* to the question rather than to the whole graph, and this is also what keeps a disconnected
|
|
67
|
+
* component at exactly zero. The vector always sums to 1 (asserted by test): a distribution that
|
|
68
|
+
* silently leaks mass would make cross-config comparisons meaningless.
|
|
69
|
+
*/
|
|
70
|
+
function personalizedPageRank(adj, seeds, opts) {
|
|
71
|
+
const { d } = opts;
|
|
72
|
+
const maxIter = opts.maxIter ?? 10;
|
|
73
|
+
const eps = opts.eps ?? 1e-4;
|
|
74
|
+
const seedTotal = [...seeds.values()].reduce((a, b) => a + b, 0);
|
|
75
|
+
if (seedTotal <= 0)
|
|
76
|
+
return new Map();
|
|
77
|
+
const s = new Map();
|
|
78
|
+
for (const [k, v] of seeds)
|
|
79
|
+
if (v > 0)
|
|
80
|
+
s.set(k, v / seedTotal);
|
|
81
|
+
let v = new Map(s);
|
|
82
|
+
for (let i = 0; i < maxIter; i++) {
|
|
83
|
+
// walk = Wᵀv + dangling·s (dangling mass returns to the seeds), then v' = d·walk + (1−d)·s.
|
|
84
|
+
const walk = new Map();
|
|
85
|
+
let dangling = 0;
|
|
86
|
+
for (const [node, mass] of v) {
|
|
87
|
+
const out = adj.out.get(node);
|
|
88
|
+
if (out === undefined || out.length === 0) {
|
|
89
|
+
dangling += mass;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
for (const o of out)
|
|
93
|
+
walk.set(o.to, (walk.get(o.to) ?? 0) + mass * o.w);
|
|
94
|
+
}
|
|
95
|
+
const next = new Map();
|
|
96
|
+
for (const [k, sv] of s)
|
|
97
|
+
walk.set(k, (walk.get(k) ?? 0) + dangling * sv);
|
|
98
|
+
for (const [k, m] of walk)
|
|
99
|
+
next.set(k, d * m);
|
|
100
|
+
for (const [k, sv] of s)
|
|
101
|
+
next.set(k, (next.get(k) ?? 0) + (1 - d) * sv);
|
|
102
|
+
let l1 = 0;
|
|
103
|
+
for (const k of new Set([...v.keys(), ...next.keys()]))
|
|
104
|
+
l1 += Math.abs((v.get(k) ?? 0) - (next.get(k) ?? 0));
|
|
105
|
+
v = next;
|
|
106
|
+
if (eps > 0 && l1 < eps)
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
return v;
|
|
110
|
+
}
|
|
@@ -13,19 +13,32 @@ export type Agent = (typeof AGENTS)[number];
|
|
|
13
13
|
* 그 하네스에서 게이트가 **집행되는가**.
|
|
14
14
|
*
|
|
15
15
|
* - `claude` — PreToolUse/Stop 훅. 이 저장소가 처음부터 배선해 온 하네스.
|
|
16
|
-
* - `antigravity` —
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* `
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
16
|
+
* - `antigravity` — **true**, 실기 프로브로 확인(2026-08-30, `agy -p` 비대화 실행). Codex 에서
|
|
17
|
+
* 무너진 세 단계를 여기서는 전부 통과한다: `.agents/hooks.json` 이 적재되고(훅이 41회 호출됨),
|
|
18
|
+
* 페이로드가 어댑터의 `TOOL_MAP` 과 정확히 맞으며(`write_to_file`/`TargetFile`/`CodeContent`),
|
|
19
|
+
* 미승인 쓰기에 `deny` 를 내자 **파일이 생성되지 않았다.** Stop 훅의 헌법 감사와 루프 가드도
|
|
20
|
+
* 같은 실행에서 발화했다. 이 값의 근거는 이제 규약 문서가 아니라 실측이다.
|
|
21
|
+
* - `codex` — **false**, 실측(codex-cli 0.151.0, 2026-08-30, A-SPEC-442). Codex 는 훅을 집행할
|
|
22
|
+
* 수 있다: 벤더 규약대로 배선한 플러그인을 설치하면 PreToolUse 가 발화하고, exit 2 를 내면
|
|
23
|
+
* `echo BLOCKED > blocked.txt` 가 실제로 실행되지 않는다(프로브 4회 중 3·4번째).
|
|
24
|
+
*
|
|
25
|
+
* 그런데 Codex 는 플러그인을 **설치된 마켓플레이스 스냅샷**에서만 적재한다. 프로젝트 폴더에
|
|
26
|
+
* 매니페스트를 놓아도, 그것을 마켓플레이스로 등록해 두어도, 사용자가 `codex plugin add` 를
|
|
27
|
+
* 실행하기 전에는 훅이 발화하지 않는다(프로브 1·2번째). 설치는 사용자의 행위이고 doctor 는
|
|
28
|
+
* 오프라인이므로, init 이 끝난 시점에 이 값이 참이라고 말할 근거가 없다.
|
|
29
|
+
*
|
|
30
|
+
* A-SPEC-441 은 이 값을 true 로 올렸다. 그 결과 `AGENTS_MD(true)` 가 집행되지 않는 하네스에
|
|
31
|
+
* "Gates are **enforced**" 라고 적었다 — 이 파일의 머리말이 하지 말라고 적어 둔 바로 그것이다.
|
|
27
32
|
*/
|
|
28
33
|
export declare const HARNESS_ENFORCES: Record<Agent, boolean>;
|
|
34
|
+
/**
|
|
35
|
+
* @implements A-SPEC-442
|
|
36
|
+
* codex 플러그인이 저장소 안에서 사는 자리. 저장소 루트에 `plugins/` 를 만들지 않으려고
|
|
37
|
+
* 중첩 경로를 쓴다 — 마켓플레이스의 `source.path` 가 `./` 로 시작하는 상대경로이기만 하면
|
|
38
|
+
* 중첩이 허용된다는 것은 실측으로 확인했다.
|
|
39
|
+
*/
|
|
40
|
+
export declare const CODEX_PLUGIN_DIR: string;
|
|
41
|
+
export declare const CODEX_MARKETPLACE = "holmes-kit-local";
|
|
29
42
|
export interface AgentWiringOptions {
|
|
30
43
|
target: string;
|
|
31
44
|
packageRoot: string;
|
|
@@ -33,9 +33,10 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.HARNESS_ENFORCES = exports.AGENTS = void 0;
|
|
36
|
+
exports.CODEX_MARKETPLACE = exports.CODEX_PLUGIN_DIR = exports.HARNESS_ENFORCES = exports.AGENTS = void 0;
|
|
37
37
|
exports.agentFiles = agentFiles;
|
|
38
38
|
exports.agentLinks = agentLinks;
|
|
39
|
+
// @implements A-SPEC-442
|
|
39
40
|
// @implements A-SPEC-193
|
|
40
41
|
const path = __importStar(require("node:path"));
|
|
41
42
|
const mcp_launcher_1 = require("./mcp-launcher");
|
|
@@ -52,23 +53,36 @@ exports.AGENTS = ['claude', 'antigravity', 'codex'];
|
|
|
52
53
|
* 그 하네스에서 게이트가 **집행되는가**.
|
|
53
54
|
*
|
|
54
55
|
* - `claude` — PreToolUse/Stop 훅. 이 저장소가 처음부터 배선해 온 하네스.
|
|
55
|
-
* - `antigravity` —
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* `
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
56
|
+
* - `antigravity` — **true**, 실기 프로브로 확인(2026-08-30, `agy -p` 비대화 실행). Codex 에서
|
|
57
|
+
* 무너진 세 단계를 여기서는 전부 통과한다: `.agents/hooks.json` 이 적재되고(훅이 41회 호출됨),
|
|
58
|
+
* 페이로드가 어댑터의 `TOOL_MAP` 과 정확히 맞으며(`write_to_file`/`TargetFile`/`CodeContent`),
|
|
59
|
+
* 미승인 쓰기에 `deny` 를 내자 **파일이 생성되지 않았다.** Stop 훅의 헌법 감사와 루프 가드도
|
|
60
|
+
* 같은 실행에서 발화했다. 이 값의 근거는 이제 규약 문서가 아니라 실측이다.
|
|
61
|
+
* - `codex` — **false**, 실측(codex-cli 0.151.0, 2026-08-30, A-SPEC-442). Codex 는 훅을 집행할
|
|
62
|
+
* 수 있다: 벤더 규약대로 배선한 플러그인을 설치하면 PreToolUse 가 발화하고, exit 2 를 내면
|
|
63
|
+
* `echo BLOCKED > blocked.txt` 가 실제로 실행되지 않는다(프로브 4회 중 3·4번째).
|
|
64
|
+
*
|
|
65
|
+
* 그런데 Codex 는 플러그인을 **설치된 마켓플레이스 스냅샷**에서만 적재한다. 프로젝트 폴더에
|
|
66
|
+
* 매니페스트를 놓아도, 그것을 마켓플레이스로 등록해 두어도, 사용자가 `codex plugin add` 를
|
|
67
|
+
* 실행하기 전에는 훅이 발화하지 않는다(프로브 1·2번째). 설치는 사용자의 행위이고 doctor 는
|
|
68
|
+
* 오프라인이므로, init 이 끝난 시점에 이 값이 참이라고 말할 근거가 없다.
|
|
69
|
+
*
|
|
70
|
+
* A-SPEC-441 은 이 값을 true 로 올렸다. 그 결과 `AGENTS_MD(true)` 가 집행되지 않는 하네스에
|
|
71
|
+
* "Gates are **enforced**" 라고 적었다 — 이 파일의 머리말이 하지 말라고 적어 둔 바로 그것이다.
|
|
66
72
|
*/
|
|
67
73
|
exports.HARNESS_ENFORCES = {
|
|
68
74
|
claude: true,
|
|
69
75
|
antigravity: true,
|
|
70
76
|
codex: false,
|
|
71
77
|
};
|
|
78
|
+
/**
|
|
79
|
+
* @implements A-SPEC-442
|
|
80
|
+
* codex 플러그인이 저장소 안에서 사는 자리. 저장소 루트에 `plugins/` 를 만들지 않으려고
|
|
81
|
+
* 중첩 경로를 쓴다 — 마켓플레이스의 `source.path` 가 `./` 로 시작하는 상대경로이기만 하면
|
|
82
|
+
* 중첩이 허용된다는 것은 실측으로 확인했다.
|
|
83
|
+
*/
|
|
84
|
+
exports.CODEX_PLUGIN_DIR = path.join('.holmes', 'codex-plugin', 'holmes-kit');
|
|
85
|
+
exports.CODEX_MARKETPLACE = 'holmes-kit-local';
|
|
72
86
|
const q = (p) => p;
|
|
73
87
|
// @implements A-SPEC-251.1 — same launch contract as the Claude wiring: npx-pin for installed
|
|
74
88
|
// packageRoots, node for source checkouts. Shared via mcpEntryForInstall so all three harnesses
|
|
@@ -99,6 +113,17 @@ const mcpConfig = (packageRoot, specsDir, launcher) => {
|
|
|
99
113
|
// Antigravity gate was silently off. (The MCP config above is different: it passes `args` as an
|
|
100
114
|
// array, where quoting would put literal quote characters into the path.)
|
|
101
115
|
const hookCommand = (packageRoot, script) => `node "${path.join(packageRoot, 'bin', script)}"`;
|
|
116
|
+
/**
|
|
117
|
+
* @implements A-SPEC-442
|
|
118
|
+
* Codex 훅이 부를 것: **Claude 와 같은 정책 커널**.
|
|
119
|
+
*
|
|
120
|
+
* Codex 의 PreToolUse 출력 스키마는 `hookSpecificOutput.permissionDecision`(allow|deny|ask)이며,
|
|
121
|
+
* 이것은 Claude 규약과 같은 모양이다(codex 0.151.0 바이너리의 JSON 스키마에서 확인). 커널은
|
|
122
|
+
* 이미 그 형식을 낸다. 반면 antigravity 어댑터는 `toolCall.name` 을 찾고 `{"decision":"deny"}` 를
|
|
123
|
+
* exit 0 으로 낸다 — 실기에서 Codex 는 그것을 이해하지 못하고 `PreToolUse Failed` 로 처리하며
|
|
124
|
+
* **차단하지 않는다**(실측). 적재되는 훅과 말이 통하는 훅은 다른 문제이고, 둘 다 틀려 있었다.
|
|
125
|
+
*/
|
|
126
|
+
const kernelCommand = (packageRoot, script) => `node "${path.join(packageRoot, 'dist', 'holmes', 'hooks', script)}"`;
|
|
102
127
|
const hooksJson = (packageRoot) => `${JSON.stringify({
|
|
103
128
|
'holmes-kit': {
|
|
104
129
|
PreToolUse: [{
|
|
@@ -137,11 +162,12 @@ This repository operates under **No Spec, No Code** governance. An approved spec
|
|
|
137
162
|
|
|
138
163
|
1. Run \`spec_slice_init\` or \`spec_create\` to author REQ -> H-SPEC -> A-SPEC.
|
|
139
164
|
2. Approve specs via \`spec_slice_approve\` or \`spec_approve\`.
|
|
140
|
-
3. **
|
|
141
|
-
4.
|
|
142
|
-
5.
|
|
165
|
+
3. **Before editing source**, run \`maintenance_analyze({ root, request, persist: true })\` and keep the digest it returns — that persisted analysis is what the pre-edit evidence gate checks.
|
|
166
|
+
4. **Write tests first** and verify failure (RED stage).
|
|
167
|
+
5. Approve the mirroring T-SPEC (\`A-SPEC-188\` -> \`T-SPEC-188\`).
|
|
168
|
+
6. Implement source code (GREEN stage).
|
|
143
169
|
|
|
144
|
-
Use \`spec_next\` to check the next required governance step. Use \`phase_check\` to verify action permissions ahead of execution.
|
|
170
|
+
Use \`spec_next\` to check the next required governance step. Use \`phase_check\` to verify action permissions ahead of execution. Use \`impact_gate_check\` to ask what the pre-edit evidence gate would say about an edit — it is advisory and blocks nothing, so it answers the same way in every harness.
|
|
145
171
|
|
|
146
172
|
## Enforcement in this Harness
|
|
147
173
|
|
|
@@ -169,8 +195,58 @@ function agentFiles(agent, opts) {
|
|
|
169
195
|
{ path: path.join(target, 'AGENTS.md'), content: AGENTS_MD(exports.HARNESS_ENFORCES.antigravity) },
|
|
170
196
|
];
|
|
171
197
|
case 'codex':
|
|
198
|
+
// @implements A-SPEC-442 (was A-SPEC-266, A-SPEC-441) — Codex plugin, at the layout Codex reads:
|
|
199
|
+
// 1. the repository becomes a LOCAL MARKETPLACE (`.agents/plugins/marketplace.json`),
|
|
200
|
+
// because Codex loads plugins only from installed marketplace snapshots;
|
|
201
|
+
// 2. the plugin manifest's `hooks` names `./hooks.json` at the PLUGIN ROOT — the vendor's
|
|
202
|
+
// plugin reference (embedded in the codex binary) defines it that way, and the
|
|
203
|
+
// `./hooks/hooks.json` A-SPEC-441 emitted was measured never to load.
|
|
204
|
+
// The MCP wiring stays in `.codex/config.toml` (merged by init.ts).
|
|
205
|
+
// Wiring is not installation: the user still runs `codex plugin marketplace add` and
|
|
206
|
+
// `codex plugin add`, which is why HARNESS_ENFORCES.codex is false.
|
|
172
207
|
return [
|
|
173
|
-
{
|
|
208
|
+
{
|
|
209
|
+
path: path.join(target, '.agents', 'plugins', 'marketplace.json'),
|
|
210
|
+
content: `${JSON.stringify({
|
|
211
|
+
name: exports.CODEX_MARKETPLACE,
|
|
212
|
+
interface: { displayName: 'Holmes-Kit (local)' },
|
|
213
|
+
plugins: [{
|
|
214
|
+
name: 'holmes-kit',
|
|
215
|
+
source: { source: 'local', path: `./${exports.CODEX_PLUGIN_DIR.split(path.sep).join('/')}` },
|
|
216
|
+
policy: { installation: 'AVAILABLE', authentication: 'ON_INSTALL' },
|
|
217
|
+
category: 'Developer Tools',
|
|
218
|
+
}],
|
|
219
|
+
}, null, 2)}\n`,
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
path: path.join(target, exports.CODEX_PLUGIN_DIR, '.codex-plugin', 'plugin.json'),
|
|
223
|
+
content: `${JSON.stringify({
|
|
224
|
+
name: 'holmes-kit',
|
|
225
|
+
version: '0.2.1',
|
|
226
|
+
description: 'Holmes-Kit deterministic governance plugin for Codex',
|
|
227
|
+
hooks: './hooks.json',
|
|
228
|
+
}, null, 2)}\n`,
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
path: path.join(target, exports.CODEX_PLUGIN_DIR, 'hooks.json'),
|
|
232
|
+
content: `${JSON.stringify({
|
|
233
|
+
hooks: {
|
|
234
|
+
PreToolUse: [{
|
|
235
|
+
matcher: '.*',
|
|
236
|
+
hooks: [{
|
|
237
|
+
type: 'command',
|
|
238
|
+
command: kernelCommand(packageRoot, 'pre-tool-use.js'),
|
|
239
|
+
timeout: 30,
|
|
240
|
+
}],
|
|
241
|
+
}],
|
|
242
|
+
Stop: [{
|
|
243
|
+
type: 'command',
|
|
244
|
+
command: kernelCommand(packageRoot, 'stop.js'),
|
|
245
|
+
timeout: 30,
|
|
246
|
+
}],
|
|
247
|
+
},
|
|
248
|
+
}, null, 2)}\n`,
|
|
249
|
+
},
|
|
174
250
|
{ path: path.join(target, 'AGENTS.md'), content: AGENTS_MD(exports.HARNESS_ENFORCES.codex) },
|
|
175
251
|
];
|
|
176
252
|
default:
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { McpServerEntry } from './mcp-launcher';
|
|
2
|
+
/** The one table holmes-kit owns in a Codex config.toml. */
|
|
3
|
+
export declare const CODEX_TABLE = "mcp_servers.holmes-kit";
|
|
4
|
+
/**
|
|
5
|
+
* Serialize the `[mcp_servers.holmes-kit]` table. `env` is an inline table so the whole entry is ONE
|
|
6
|
+
* contiguous region (no `[mcp_servers.holmes-kit.env]` child header) — that keeps the merge boundary
|
|
7
|
+
* unambiguous: our region runs from the header to the next table header. Ends with a trailing newline.
|
|
8
|
+
*/
|
|
9
|
+
export declare function codexMcpBlock(entry: McpServerEntry, specsDir: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Merge `block` (a full `codexMcpBlock` output) into `existing`. When `existing` is null/empty the
|
|
12
|
+
* block stands alone. When our table is already present its region is REPLACED (no duplicate); when
|
|
13
|
+
* absent the block is appended after a blank-line separator. Every other line is preserved verbatim.
|
|
14
|
+
*/
|
|
15
|
+
export declare function mergeCodexToml(existing: string | null, block: string): string;
|
|
16
|
+
/** Strip our region from `existing`, preserving everything else. Absent → returned unchanged. */
|
|
17
|
+
export declare function removeCodexToml(existing: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Read `{command,args}` from our table in a config.toml — for doctor's drift check. Parses only the
|
|
20
|
+
* shape `codexMcpBlock` writes; anything it cannot read returns null (doctor then WARNs rather than
|
|
21
|
+
* translating an unreadable wiring into a pass).
|
|
22
|
+
*/
|
|
23
|
+
export declare function readCodexHolmesEntry(raw: string): {
|
|
24
|
+
command: string;
|
|
25
|
+
args: string[];
|
|
26
|
+
} | null;
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CODEX_TABLE = void 0;
|
|
4
|
+
exports.codexMcpBlock = codexMcpBlock;
|
|
5
|
+
exports.mergeCodexToml = mergeCodexToml;
|
|
6
|
+
exports.removeCodexToml = removeCodexToml;
|
|
7
|
+
exports.readCodexHolmesEntry = readCodexHolmesEntry;
|
|
8
|
+
/** The one table holmes-kit owns in a Codex config.toml. */
|
|
9
|
+
exports.CODEX_TABLE = 'mcp_servers.holmes-kit';
|
|
10
|
+
const HEADER = `[${exports.CODEX_TABLE}]`;
|
|
11
|
+
/** TOML basic-string escape — backslash and quote, the named whitespace escapes, then any remaining
|
|
12
|
+
* C0 control character as \uXXXX. Built by code point so no literal control byte lives in the source. */
|
|
13
|
+
function tomlStr(s) {
|
|
14
|
+
let out = '';
|
|
15
|
+
for (const ch of s) {
|
|
16
|
+
const code = ch.codePointAt(0);
|
|
17
|
+
if (ch === '\\')
|
|
18
|
+
out += '\\\\';
|
|
19
|
+
else if (ch === '"')
|
|
20
|
+
out += '\\"';
|
|
21
|
+
else if (ch === '\n')
|
|
22
|
+
out += '\\n';
|
|
23
|
+
else if (ch === '\r')
|
|
24
|
+
out += '\\r';
|
|
25
|
+
else if (ch === '\t')
|
|
26
|
+
out += '\\t';
|
|
27
|
+
else if (code < 0x20 || code === 0x7f)
|
|
28
|
+
out += `\\u${code.toString(16).padStart(4, '0')}`;
|
|
29
|
+
else
|
|
30
|
+
out += ch;
|
|
31
|
+
}
|
|
32
|
+
return `"${out}"`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Serialize the `[mcp_servers.holmes-kit]` table. `env` is an inline table so the whole entry is ONE
|
|
36
|
+
* contiguous region (no `[mcp_servers.holmes-kit.env]` child header) — that keeps the merge boundary
|
|
37
|
+
* unambiguous: our region runs from the header to the next table header. Ends with a trailing newline.
|
|
38
|
+
*/
|
|
39
|
+
function codexMcpBlock(entry, specsDir) {
|
|
40
|
+
const args = entry.args.map(tomlStr).join(', ');
|
|
41
|
+
return [
|
|
42
|
+
HEADER,
|
|
43
|
+
`command = ${tomlStr(entry.command)}`,
|
|
44
|
+
`args = [${args}]`,
|
|
45
|
+
`env = { HOLMES_SPECS = ${tomlStr(specsDir)} }`,
|
|
46
|
+
'',
|
|
47
|
+
].join('\n');
|
|
48
|
+
}
|
|
49
|
+
/** Advance the string/bracket state across one line's characters (comments end the line). */
|
|
50
|
+
function advance(line, st) {
|
|
51
|
+
let { ml, depth } = st;
|
|
52
|
+
let i = 0;
|
|
53
|
+
while (i < line.length) {
|
|
54
|
+
if (ml === '"""') {
|
|
55
|
+
if (line.startsWith('"""', i)) {
|
|
56
|
+
ml = null;
|
|
57
|
+
i += 3;
|
|
58
|
+
}
|
|
59
|
+
else if (line[i] === '\\') {
|
|
60
|
+
i += 2;
|
|
61
|
+
}
|
|
62
|
+
else
|
|
63
|
+
i += 1;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (ml === "'''") { // literal: no escapes
|
|
67
|
+
if (line.startsWith("'''", i)) {
|
|
68
|
+
ml = null;
|
|
69
|
+
i += 3;
|
|
70
|
+
}
|
|
71
|
+
else
|
|
72
|
+
i += 1;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (line.startsWith('"""', i)) {
|
|
76
|
+
ml = '"""';
|
|
77
|
+
i += 3;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (line.startsWith("'''", i)) {
|
|
81
|
+
ml = "'''";
|
|
82
|
+
i += 3;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const c = line[i];
|
|
86
|
+
if (c === '#')
|
|
87
|
+
break; // comment runs to end of line
|
|
88
|
+
if (c === '"') {
|
|
89
|
+
i += 1;
|
|
90
|
+
while (i < line.length && line[i] !== '"') {
|
|
91
|
+
if (line[i] === '\\')
|
|
92
|
+
i += 1;
|
|
93
|
+
i += 1;
|
|
94
|
+
}
|
|
95
|
+
i += 1;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (c === "'") {
|
|
99
|
+
i += 1;
|
|
100
|
+
while (i < line.length && line[i] !== "'")
|
|
101
|
+
i += 1;
|
|
102
|
+
i += 1;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (c === '[' || c === '{') {
|
|
106
|
+
depth += 1;
|
|
107
|
+
i += 1;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (c === ']' || c === '}') {
|
|
111
|
+
depth = Math.max(0, depth - 1);
|
|
112
|
+
i += 1;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
i += 1;
|
|
116
|
+
}
|
|
117
|
+
return { ml, depth };
|
|
118
|
+
}
|
|
119
|
+
/** State at the START of each line (index-aligned to `lines`). */
|
|
120
|
+
function lineStartStates(lines) {
|
|
121
|
+
const states = [];
|
|
122
|
+
let st = { ml: null, depth: 0 };
|
|
123
|
+
for (const line of lines) {
|
|
124
|
+
states.push(st);
|
|
125
|
+
st = advance(line, st);
|
|
126
|
+
}
|
|
127
|
+
return states;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The dotted key path of a table-header line, quotes stripped and each segment trimmed — so
|
|
131
|
+
* `[mcp_servers.holmes-kit]`, `[mcp_servers."holmes-kit"]`, and `[ mcp_servers . holmes-kit ]` all
|
|
132
|
+
* yield ['mcp_servers','holmes-kit'] (TOML treats them as the SAME table; missing an alias would
|
|
133
|
+
* append a duplicate table and invalidate the whole file — round-1 quoted-key, round-2 whitespace).
|
|
134
|
+
* Returns null when the line is not a table header.
|
|
135
|
+
*/
|
|
136
|
+
function tableKeyPath(line) {
|
|
137
|
+
const m = line.match(/^\s*\[\[?([^\]]*)\]\]?\s*(#.*)?$/);
|
|
138
|
+
if (!m)
|
|
139
|
+
return null;
|
|
140
|
+
const inner = m[1];
|
|
141
|
+
const segs = [];
|
|
142
|
+
let cur = '';
|
|
143
|
+
let q = null;
|
|
144
|
+
for (let i = 0; i < inner.length; i++) {
|
|
145
|
+
const c = inner[i];
|
|
146
|
+
if (q) {
|
|
147
|
+
if (c === q)
|
|
148
|
+
q = null;
|
|
149
|
+
else
|
|
150
|
+
cur += c;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (c === '"' || c === "'") {
|
|
154
|
+
q = c;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (c === '.') {
|
|
158
|
+
segs.push(cur.trim());
|
|
159
|
+
cur = '';
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
cur += c;
|
|
163
|
+
}
|
|
164
|
+
segs.push(cur.trim());
|
|
165
|
+
if (q !== null)
|
|
166
|
+
return null; // unbalanced quote — not a clean header
|
|
167
|
+
return segs;
|
|
168
|
+
}
|
|
169
|
+
const isOurTable = (kp) => kp.length === 2 && kp[0] === 'mcp_servers' && kp[1] === 'holmes-kit';
|
|
170
|
+
const isOurPrefix = (kp) => kp.length >= 2 && kp[0] === 'mcp_servers' && kp[1] === 'holmes-kit';
|
|
171
|
+
/**
|
|
172
|
+
* Locate our region as a [start, end) line-index pair, or null if absent. `start` is our table's
|
|
173
|
+
* header line; `end` is the first later line that opens a DIFFERENT top-level table (exclusive), or
|
|
174
|
+
* lines.length. Only real headers (outside strings, bracket-depth 0) are considered — AND the end
|
|
175
|
+
* boundary closes on ANY top-level header line, even one `tableKeyPath` cannot normalize (e.g. a
|
|
176
|
+
* quoted key containing `]`). That fail-safe is the point: an unrecognized header is never OURS, so
|
|
177
|
+
* treating it as a boundary preserves the foreign table rather than swallowing it (round-3 finding).
|
|
178
|
+
*/
|
|
179
|
+
function ourRegion(lines) {
|
|
180
|
+
const states = lineStartStates(lines);
|
|
181
|
+
const isTopLevelHeaderLine = (i) => states[i].ml === null && states[i].depth === 0 && /^\s*\[/.test(lines[i]);
|
|
182
|
+
const keyAt = (i) => (isTopLevelHeaderLine(i) ? tableKeyPath(lines[i]) : null);
|
|
183
|
+
let start = -1;
|
|
184
|
+
for (let i = 0; i < lines.length; i++) {
|
|
185
|
+
const kp = keyAt(i);
|
|
186
|
+
if (kp && isOurTable(kp)) {
|
|
187
|
+
start = i;
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (start === -1)
|
|
192
|
+
return null;
|
|
193
|
+
let end = lines.length;
|
|
194
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
195
|
+
if (!isTopLevelHeaderLine(i))
|
|
196
|
+
continue;
|
|
197
|
+
const kp = tableKeyPath(lines[i]);
|
|
198
|
+
if (!(kp && isOurPrefix(kp))) {
|
|
199
|
+
end = i;
|
|
200
|
+
break;
|
|
201
|
+
} // any non-our (incl. unparseable) header closes us
|
|
202
|
+
}
|
|
203
|
+
return { start, end };
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Merge `block` (a full `codexMcpBlock` output) into `existing`. When `existing` is null/empty the
|
|
207
|
+
* block stands alone. When our table is already present its region is REPLACED (no duplicate); when
|
|
208
|
+
* absent the block is appended after a blank-line separator. Every other line is preserved verbatim.
|
|
209
|
+
*/
|
|
210
|
+
function mergeCodexToml(existing, block) {
|
|
211
|
+
if (existing == null || existing.trim() === '')
|
|
212
|
+
return block;
|
|
213
|
+
const lines = existing.split('\n');
|
|
214
|
+
const region = ourRegion(lines);
|
|
215
|
+
const blockLines = block.replace(/\n$/, '').split('\n');
|
|
216
|
+
if (region) {
|
|
217
|
+
const after = lines.slice(region.end);
|
|
218
|
+
const merged = [...lines.slice(0, region.start), ...blockLines, ...after];
|
|
219
|
+
return merged.join('\n').replace(/\n*$/, '\n');
|
|
220
|
+
}
|
|
221
|
+
// Append: exactly one blank line between the user's content and our block.
|
|
222
|
+
const base = existing.replace(/\n*$/, '');
|
|
223
|
+
return `${base}\n\n${block.replace(/\n*$/, '')}\n`;
|
|
224
|
+
}
|
|
225
|
+
/** Strip our region from `existing`, preserving everything else. Absent → returned unchanged. */
|
|
226
|
+
function removeCodexToml(existing) {
|
|
227
|
+
const lines = existing.split('\n');
|
|
228
|
+
const region = ourRegion(lines);
|
|
229
|
+
if (!region)
|
|
230
|
+
return existing;
|
|
231
|
+
const before = lines.slice(0, region.start);
|
|
232
|
+
const after = lines.slice(region.end);
|
|
233
|
+
// Drop a trailing blank line left dangling between `before` and `after` so removal is clean.
|
|
234
|
+
while (before.length > 0 && before[before.length - 1].trim() === '')
|
|
235
|
+
before.pop();
|
|
236
|
+
const merged = [...before, ...after];
|
|
237
|
+
const joined = merged.join('\n');
|
|
238
|
+
if (joined.trim() === '')
|
|
239
|
+
return '';
|
|
240
|
+
return joined.replace(/\n*$/, '\n');
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Read `{command,args}` from our table in a config.toml — for doctor's drift check. Parses only the
|
|
244
|
+
* shape `codexMcpBlock` writes; anything it cannot read returns null (doctor then WARNs rather than
|
|
245
|
+
* translating an unreadable wiring into a pass).
|
|
246
|
+
*/
|
|
247
|
+
function readCodexHolmesEntry(raw) {
|
|
248
|
+
const lines = raw.split('\n');
|
|
249
|
+
const region = ourRegion(lines);
|
|
250
|
+
if (!region)
|
|
251
|
+
return null;
|
|
252
|
+
let command = null;
|
|
253
|
+
let args = null;
|
|
254
|
+
for (let i = region.start + 1; i < region.end; i++) {
|
|
255
|
+
const cmd = lines[i].match(/^\s*command\s*=\s*"((?:[^"\\]|\\.)*)"\s*(#.*)?$/);
|
|
256
|
+
if (cmd)
|
|
257
|
+
command = unescapeToml(cmd[1]);
|
|
258
|
+
const arr = lines[i].match(/^\s*args\s*=\s*\[(.*)\]\s*(#.*)?$/);
|
|
259
|
+
if (arr)
|
|
260
|
+
args = parseTomlStringArray(arr[1]);
|
|
261
|
+
}
|
|
262
|
+
if (command === null || args === null)
|
|
263
|
+
return null;
|
|
264
|
+
return { command, args };
|
|
265
|
+
}
|
|
266
|
+
function unescapeToml(s) {
|
|
267
|
+
return s.replace(/\\(u[0-9a-fA-F]{4}|.)/g, (_, e) => {
|
|
268
|
+
if (e[0] === 'u')
|
|
269
|
+
return String.fromCharCode(parseInt(e.slice(1), 16));
|
|
270
|
+
const map = { n: '\n', r: '\r', t: '\t', '"': '"', '\\': '\\' };
|
|
271
|
+
return map[e] ?? e;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
/** Parse a TOML inline array of basic strings: `"a", "b"` → ['a','b']. Non-conforming → []. */
|
|
275
|
+
function parseTomlStringArray(inner) {
|
|
276
|
+
const out = [];
|
|
277
|
+
const re = /"((?:[^"\\]|\\.)*)"/g;
|
|
278
|
+
let mm;
|
|
279
|
+
while ((mm = re.exec(inner)) !== null)
|
|
280
|
+
out.push(unescapeToml(mm[1]));
|
|
281
|
+
return out;
|
|
282
|
+
}
|