cb581f44b9
- project_map_reinit now regenerates only the target subtree + ancestors by default, falling back to full reinit when subtree file count exceeds reinitFullThresholdPercent (default 10%). - Add reinitFullThresholdPercent config option. - Expose fix=true on project_map_validate Pi tool for localized repair. - Update docs and runtime guidance to prefer patch / validate --fix before full reinit. - Add integration tests for smart reinit and update typebox mock.
545 lines
17 KiB
TypeScript
545 lines
17 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
import {
|
|
mkdtempSync,
|
|
writeFileSync,
|
|
mkdirSync,
|
|
rmSync,
|
|
readFileSync,
|
|
} from "fs";
|
|
import { join } from "path";
|
|
import { tmpdir } from "os";
|
|
import { initProject, reinitPath } from "../src/init.js";
|
|
import { patchFile } from "../src/patch.js";
|
|
import { validateMaps } from "../src/validate.js";
|
|
import { createMockFileClient } from "./mock-llm.js";
|
|
|
|
describe("integration", () => {
|
|
let dir: string;
|
|
|
|
beforeEach(() => {
|
|
dir = mkdtempSync(join(tmpdir(), "pi-map-int-"));
|
|
});
|
|
|
|
afterEach(() => {
|
|
rmSync(dir, { recursive: true });
|
|
});
|
|
|
|
it("init creates both .pi-map.md and .pi-map.index.md files", async () => {
|
|
mkdirSync(join(dir, "src"));
|
|
writeFileSync(join(dir, "src", "index.ts"), `export function foo() {}\n`);
|
|
const client = createMockFileClient();
|
|
await initProject(dir, { llmClient: client, verbose: false });
|
|
|
|
const map = readFileSync(join(dir, "src", ".pi-map.md"), "utf8");
|
|
const index = readFileSync(join(dir, "src", ".pi-map.index.md"), "utf8");
|
|
expect(map).toContain("# src");
|
|
expect(index).toContain("# src (index)");
|
|
expect(index).toContain("dir: src");
|
|
});
|
|
|
|
it("root artifacts contain Project Map Protocol", async () => {
|
|
writeFileSync(join(dir, "package.json"), `{}\n`);
|
|
const client = createMockFileClient();
|
|
await initProject(dir, { llmClient: client, verbose: false });
|
|
|
|
const rootMap = readFileSync(join(dir, ".pi-map.md"), "utf8");
|
|
const rootIndex = readFileSync(join(dir, ".pi-map.index.md"), "utf8");
|
|
|
|
expect(rootMap).toContain("## Project Map Protocol");
|
|
expect(rootMap).toContain("index: ./.pi-map.index.md");
|
|
expect(rootIndex).toContain("## Project Map Protocol");
|
|
expect(rootIndex).toContain("map: ./.pi-map.md");
|
|
});
|
|
|
|
it("non-root map contains index link", async () => {
|
|
mkdirSync(join(dir, "src"));
|
|
writeFileSync(join(dir, "src", "index.ts"), `export function foo() {}\n`);
|
|
const client = createMockFileClient();
|
|
await initProject(dir, { llmClient: client, verbose: false });
|
|
|
|
const map = readFileSync(join(dir, "src", ".pi-map.md"), "utf8");
|
|
expect(map).toContain("index: src/.pi-map.index.md");
|
|
expect(map).toContain("dir: src");
|
|
});
|
|
|
|
it("index contains parent and children when applicable", async () => {
|
|
mkdirSync(join(dir, "src"));
|
|
mkdirSync(join(dir, "src", "utils"));
|
|
writeFileSync(join(dir, "src", "index.ts"), `export function foo() {}\n`);
|
|
writeFileSync(
|
|
join(dir, "src", "utils", "helpers.ts"),
|
|
`export const h = 1;\n`,
|
|
);
|
|
const client = createMockFileClient();
|
|
await initProject(dir, { llmClient: client, verbose: false });
|
|
|
|
const srcIndex = readFileSync(join(dir, "src", ".pi-map.index.md"), "utf8");
|
|
expect(srcIndex).toContain("## parent");
|
|
expect(srcIndex).toContain("index: ./.pi-map.index.md");
|
|
|
|
expect(srcIndex).toContain("## children");
|
|
expect(srcIndex).toContain("- src/utils");
|
|
|
|
const utilsIndex = readFileSync(
|
|
join(dir, "src", "utils", ".pi-map.index.md"),
|
|
"utf8",
|
|
);
|
|
expect(utilsIndex).toContain("## parent");
|
|
expect(utilsIndex).toContain("index: src/.pi-map.index.md");
|
|
});
|
|
|
|
it("patch updates a file entry", async () => {
|
|
mkdirSync(join(dir, "src"));
|
|
writeFileSync(join(dir, "src", "index.ts"), `export function foo() {}\n`);
|
|
writeFileSync(join(dir, "src", "utils.ts"), `export const bar = 1;\n`);
|
|
const client = createMockFileClient();
|
|
await initProject(dir, { llmClient: client, verbose: false });
|
|
|
|
// Modify a file
|
|
writeFileSync(
|
|
join(dir, "src", "index.ts"),
|
|
`export function foo() {}\nexport function baz() {}\n`,
|
|
);
|
|
await patchFile(join(dir, "src", "index.ts"), client, dir);
|
|
|
|
const map = readFileSync(join(dir, "src", ".pi-map.md"), "utf8");
|
|
expect(map).toContain("baz");
|
|
});
|
|
|
|
it("patch with small mode refreshes ancestor indexes but preserves ancestor maps", async () => {
|
|
mkdirSync(join(dir, "src"));
|
|
writeFileSync(join(dir, "package.json"), `{}\n`);
|
|
writeFileSync(join(dir, "src", "index.ts"), `export const a = 1;\n`);
|
|
writeFileSync(join(dir, "src", "helper.ts"), `export const b = 2;\n`);
|
|
const client = createMockFileClient();
|
|
await initProject(dir, { llmClient: client, verbose: false });
|
|
|
|
writeFileSync(
|
|
join(dir, ".pi-map.md"),
|
|
`${readFileSync(join(dir, ".pi-map.md"), "utf8")}\nSENTINEL_ROOT_MAP\n`,
|
|
);
|
|
writeFileSync(
|
|
join(dir, ".pi-map.index.md"),
|
|
`${readFileSync(join(dir, ".pi-map.index.md"), "utf8")}\nSENTINEL_ROOT_INDEX\n`,
|
|
);
|
|
|
|
writeFileSync(
|
|
join(dir, "src", "index.ts"),
|
|
`export const a = 1;\nexport const c = 3;\n`,
|
|
);
|
|
await patchFile(join(dir, "src", "index.ts"), client, dir, {
|
|
rootPath: dir,
|
|
patchMode: "small",
|
|
});
|
|
|
|
const rootMap = readFileSync(join(dir, ".pi-map.md"), "utf8");
|
|
const rootIndex = readFileSync(join(dir, ".pi-map.index.md"), "utf8");
|
|
expect(rootMap).toContain("SENTINEL_ROOT_MAP");
|
|
expect(rootIndex).not.toContain("SENTINEL_ROOT_INDEX");
|
|
});
|
|
|
|
it("patch with structural mode refreshes ancestor maps and indexes", async () => {
|
|
mkdirSync(join(dir, "src"));
|
|
writeFileSync(join(dir, "package.json"), `{}\n`);
|
|
writeFileSync(join(dir, "src", "index.ts"), `export const a = 1;\n`);
|
|
writeFileSync(join(dir, "src", "helper.ts"), `export const b = 2;\n`);
|
|
const client = createMockFileClient();
|
|
await initProject(dir, { llmClient: client, verbose: false });
|
|
|
|
writeFileSync(
|
|
join(dir, ".pi-map.md"),
|
|
`${readFileSync(join(dir, ".pi-map.md"), "utf8")}\nSENTINEL_ROOT_MAP\n`,
|
|
);
|
|
writeFileSync(
|
|
join(dir, ".pi-map.index.md"),
|
|
`${readFileSync(join(dir, ".pi-map.index.md"), "utf8")}\nSENTINEL_ROOT_INDEX\n`,
|
|
);
|
|
|
|
writeFileSync(
|
|
join(dir, "src", "index.ts"),
|
|
`export const a = 1;\nexport const c = 3;\n`,
|
|
);
|
|
await patchFile(join(dir, "src", "index.ts"), client, dir, {
|
|
rootPath: dir,
|
|
patchMode: "structural",
|
|
});
|
|
|
|
const rootMap = readFileSync(join(dir, ".pi-map.md"), "utf8");
|
|
const rootIndex = readFileSync(join(dir, ".pi-map.index.md"), "utf8");
|
|
expect(rootMap).not.toContain("SENTINEL_ROOT_MAP");
|
|
expect(rootIndex).not.toContain("SENTINEL_ROOT_INDEX");
|
|
});
|
|
|
|
it("validate detects new files", async () => {
|
|
mkdirSync(join(dir, "src"));
|
|
writeFileSync(join(dir, "src", "a.ts"), `export const a = 1;\n`);
|
|
const client = createMockFileClient();
|
|
await initProject(dir, { llmClient: client, verbose: false });
|
|
|
|
// Add new file
|
|
writeFileSync(join(dir, "src", "b.ts"), `export const b = 2;\n`);
|
|
|
|
const result = await validateMaps(dir);
|
|
expect(result.clean).toBe(false);
|
|
expect(result.discrepancies.some((d) => d.type === "missing")).toBe(true);
|
|
});
|
|
|
|
it("validate detects deleted files", async () => {
|
|
mkdirSync(join(dir, "src"));
|
|
writeFileSync(join(dir, "src", "a.ts"), `export const a = 1;\n`);
|
|
writeFileSync(join(dir, "src", "b.ts"), `export const b = 2;\n`);
|
|
const client = createMockFileClient();
|
|
await initProject(dir, { llmClient: client, verbose: false });
|
|
|
|
// Delete a file
|
|
rmSync(join(dir, "src", "b.ts"));
|
|
|
|
const result = await validateMaps(dir);
|
|
expect(result.clean).toBe(false);
|
|
expect(result.discrepancies.some((d) => d.type === "orphaned")).toBe(true);
|
|
});
|
|
|
|
it("validate detects changed signatures", async () => {
|
|
mkdirSync(join(dir, "src"));
|
|
writeFileSync(join(dir, "src", "a.ts"), `export const a = 1;\n`);
|
|
const client = createMockFileClient();
|
|
await initProject(dir, { llmClient: client, verbose: false });
|
|
|
|
// Change exports
|
|
writeFileSync(
|
|
join(dir, "src", "a.ts"),
|
|
`export const a = 1;\nexport const c = 3;\n`,
|
|
);
|
|
|
|
const result = await validateMaps(dir);
|
|
expect(result.clean).toBe(false);
|
|
expect(result.discrepancies.some((d) => d.type === "stale-signature")).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it("init populates routing metadata in rich maps", async () => {
|
|
mkdirSync(join(dir, "src"));
|
|
writeFileSync(
|
|
join(dir, "src", "index.ts"),
|
|
`export function init() {}\nexport function run() {}\n`,
|
|
);
|
|
const client = createMockFileClient();
|
|
await initProject(dir, { llmClient: client, verbose: false });
|
|
|
|
const map = readFileSync(join(dir, "src", ".pi-map.md"), "utf8");
|
|
// Tags and symbols should be scaffolded or populated
|
|
expect(map).toContain("## tags");
|
|
expect(map).toContain("## symbols");
|
|
expect(map).toContain("## workflows");
|
|
});
|
|
|
|
it("leaf directory index is tiny but present", async () => {
|
|
mkdirSync(join(dir, "src"));
|
|
mkdirSync(join(dir, "src", "utils"));
|
|
writeFileSync(
|
|
join(dir, "src", "utils", "helpers.ts"),
|
|
`export const h = 1;\n`,
|
|
);
|
|
const client = createMockFileClient();
|
|
await initProject(dir, { llmClient: client, verbose: false });
|
|
|
|
const index = readFileSync(
|
|
join(dir, "src", "utils", ".pi-map.index.md"),
|
|
"utf8",
|
|
);
|
|
// Leaf index should have empty children but still follow contract
|
|
expect(index).toContain("## children");
|
|
expect(index).toContain("-");
|
|
expect(index).toContain("## parent");
|
|
expect(index).toContain("index: src/.pi-map.index.md");
|
|
});
|
|
|
|
it("non-source directories omit uncertain workflows", async () => {
|
|
mkdirSync(join(dir, "docs"));
|
|
writeFileSync(join(dir, "docs", "README.md"), `# README\n`);
|
|
const client = createMockFileClient();
|
|
await initProject(dir, { llmClient: client, verbose: false });
|
|
|
|
const map = readFileSync(join(dir, "docs", ".pi-map.md"), "utf8");
|
|
// No source files means workflows section should be empty/omitted
|
|
const workflowsMatch = map.match(/## workflows\n([^#]*)/);
|
|
if (workflowsMatch) {
|
|
expect(workflowsMatch[1].trim()).toBe("-");
|
|
}
|
|
});
|
|
|
|
it("init loads config caps and applies them to routing metadata", async () => {
|
|
mkdirSync(join(dir, "src"));
|
|
for (let i = 0; i < 20; i++) {
|
|
writeFileSync(
|
|
join(dir, "src", `file${i}.ts`),
|
|
`export function fn${i}() {}\n`,
|
|
);
|
|
}
|
|
writeFileSync(
|
|
join(dir, ".pi-project-map.json"),
|
|
JSON.stringify({ tagCap: 3, workflowHintCap: 2 }),
|
|
);
|
|
|
|
const client = createMockFileClient();
|
|
await initProject(dir, { llmClient: client, verbose: false });
|
|
|
|
const map = readFileSync(join(dir, "src", ".pi-map.md"), "utf8");
|
|
const parsedMap = (await import("../src/format.js")).parseDirectoryMap(map);
|
|
expect(parsedMap.tags.length).toBeLessThanOrEqual(3);
|
|
expect(parsedMap.symbols.length).toBeLessThanOrEqual(3);
|
|
|
|
const index = readFileSync(join(dir, "src", ".pi-map.index.md"), "utf8");
|
|
const parsedIndex = (await import("../src/format.js")).parseDirectoryIndex(
|
|
index,
|
|
);
|
|
expect(parsedIndex.workflows.length).toBeLessThanOrEqual(2);
|
|
});
|
|
|
|
it("validate hard-fails when an index is missing", async () => {
|
|
mkdirSync(join(dir, "src"));
|
|
writeFileSync(join(dir, "src", "a.ts"), `export const a = 1;\n`);
|
|
const client = createMockFileClient();
|
|
await initProject(dir, { llmClient: client, verbose: false });
|
|
|
|
rmSync(join(dir, "src", ".pi-map.index.md"));
|
|
|
|
const result = await validateMaps(dir, { verbose: false });
|
|
expect(result.clean).toBe(false);
|
|
expect(result.discrepancies.some((d) => d.type === "stale-index")).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it("validate detects broken sibling links", async () => {
|
|
mkdirSync(join(dir, "src"));
|
|
writeFileSync(join(dir, "src", "a.ts"), `export const a = 1;\n`);
|
|
const client = createMockFileClient();
|
|
await initProject(dir, { llmClient: client, verbose: false });
|
|
|
|
const mapPath = join(dir, "src", ".pi-map.md");
|
|
writeFileSync(
|
|
mapPath,
|
|
readFileSync(mapPath, "utf8").replace(
|
|
"index: src/.pi-map.index.md",
|
|
"index: src/bad-index.md",
|
|
),
|
|
);
|
|
|
|
const result = await validateMaps(dir, { verbose: false });
|
|
expect(result.clean).toBe(false);
|
|
expect(result.discrepancies.some((d) => d.type === "broken-link")).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it("validate --fix repairs the affected chain", async () => {
|
|
mkdirSync(join(dir, "src"));
|
|
writeFileSync(join(dir, "package.json"), `{}\n`);
|
|
writeFileSync(join(dir, "src", "a.ts"), `export const a = 1;\n`);
|
|
const client = createMockFileClient();
|
|
await initProject(dir, { llmClient: client, verbose: false });
|
|
|
|
rmSync(join(dir, "src", ".pi-map.index.md"));
|
|
const result = await validateMaps(dir, {
|
|
fix: true,
|
|
verbose: false,
|
|
llmClient: client,
|
|
cacheDir: dir,
|
|
patchMode: "small",
|
|
});
|
|
|
|
expect(result.clean).toBe(false);
|
|
expect(result.fixed).toBeGreaterThan(0);
|
|
expect(
|
|
readFileSync(join(dir, "src", ".pi-map.index.md"), "utf8"),
|
|
).toContain("# src (index)");
|
|
});
|
|
|
|
it("reinit on root regenerates all artifacts", async () => {
|
|
mkdirSync(join(dir, "src"));
|
|
writeFileSync(join(dir, "src", "a.ts"), `export const a = 1;\n`);
|
|
const client = createMockFileClient();
|
|
await initProject(dir, { llmClient: client, verbose: false });
|
|
|
|
writeFileSync(
|
|
join(dir, ".pi-map.md"),
|
|
readFileSync(join(dir, ".pi-map.md"), "utf8") + "\nCORRUPTED",
|
|
);
|
|
await reinitPath(dir, { llmClient: client, verbose: false });
|
|
expect(readFileSync(join(dir, ".pi-map.md"), "utf8")).not.toContain(
|
|
"CORRUPTED",
|
|
);
|
|
});
|
|
|
|
it("reinit on a small subtree regenerates subtree and ancestors but not siblings", async () => {
|
|
mkdirSync(join(dir, "src"));
|
|
mkdirSync(join(dir, "lib"));
|
|
writeFileSync(join(dir, "src", "a.ts"), `export const a = 1;\n`);
|
|
for (let i = 0; i < 10; i++) {
|
|
writeFileSync(
|
|
join(dir, "lib", `b${i}.ts`),
|
|
`export const b${i} = ${i};\n`,
|
|
);
|
|
}
|
|
writeFileSync(
|
|
join(dir, ".pi-project-map.json"),
|
|
JSON.stringify({ reinitFullThresholdPercent: 10 }),
|
|
);
|
|
|
|
const client = createMockFileClient();
|
|
await initProject(dir, { llmClient: client, verbose: false });
|
|
|
|
writeFileSync(
|
|
join(dir, "src", ".pi-map.md"),
|
|
readFileSync(join(dir, "src", ".pi-map.md"), "utf8") + "\nSUBTREE_MARKER",
|
|
);
|
|
writeFileSync(
|
|
join(dir, "lib", ".pi-map.md"),
|
|
readFileSync(join(dir, "lib", ".pi-map.md"), "utf8") + "\nSIBLING_MARKER",
|
|
);
|
|
|
|
await reinitPath(join(dir, "src"), {
|
|
llmClient: client,
|
|
verbose: false,
|
|
});
|
|
|
|
expect(readFileSync(join(dir, "src", ".pi-map.md"), "utf8")).not.toContain(
|
|
"SUBTREE_MARKER",
|
|
);
|
|
expect(readFileSync(join(dir, "lib", ".pi-map.md"), "utf8")).toContain(
|
|
"SIBLING_MARKER",
|
|
);
|
|
});
|
|
|
|
it("reinit falls back to full regeneration when subtree exceeds threshold", async () => {
|
|
mkdirSync(join(dir, "src"));
|
|
mkdirSync(join(dir, "lib"));
|
|
for (let i = 0; i < 11; i++) {
|
|
writeFileSync(
|
|
join(dir, "src", `a${i}.ts`),
|
|
`export const a${i} = ${i};\n`,
|
|
);
|
|
}
|
|
writeFileSync(join(dir, "lib", "b.ts"), `export const b = 1;\n`);
|
|
writeFileSync(
|
|
join(dir, ".pi-project-map.json"),
|
|
JSON.stringify({ reinitFullThresholdPercent: 10 }),
|
|
);
|
|
|
|
const client = createMockFileClient();
|
|
await initProject(dir, { llmClient: client, verbose: false });
|
|
|
|
writeFileSync(
|
|
join(dir, "lib", ".pi-map.md"),
|
|
readFileSync(join(dir, "lib", ".pi-map.md"), "utf8") + "\nSIBLING_MARKER",
|
|
);
|
|
|
|
await reinitPath(join(dir, "src"), {
|
|
llmClient: client,
|
|
verbose: false,
|
|
});
|
|
|
|
expect(readFileSync(join(dir, "lib", ".pi-map.md"), "utf8")).not.toContain(
|
|
"SIBLING_MARKER",
|
|
);
|
|
});
|
|
|
|
it("reinit respects a custom reinitFullThresholdPercent", async () => {
|
|
mkdirSync(join(dir, "src"));
|
|
mkdirSync(join(dir, "lib"));
|
|
writeFileSync(join(dir, "src", "a.ts"), `export const a = 1;\n`);
|
|
writeFileSync(join(dir, "lib", "b.ts"), `export const b = 1;\n`);
|
|
writeFileSync(
|
|
join(dir, ".pi-project-map.json"),
|
|
JSON.stringify({ reinitFullThresholdPercent: 50 }),
|
|
);
|
|
|
|
const client = createMockFileClient();
|
|
await initProject(dir, { llmClient: client, verbose: false });
|
|
|
|
writeFileSync(
|
|
join(dir, "lib", ".pi-map.md"),
|
|
readFileSync(join(dir, "lib", ".pi-map.md"), "utf8") + "\nSIBLING_MARKER",
|
|
);
|
|
|
|
await reinitPath(join(dir, "src"), {
|
|
llmClient: client,
|
|
verbose: false,
|
|
});
|
|
|
|
expect(readFileSync(join(dir, "lib", ".pi-map.md"), "utf8")).toContain(
|
|
"SIBLING_MARKER",
|
|
);
|
|
});
|
|
|
|
it("reinit on a small subtree regenerates ancestors up to the root", async () => {
|
|
mkdirSync(join(dir, "src"));
|
|
mkdirSync(join(dir, "lib"));
|
|
writeFileSync(join(dir, "src", "a.ts"), `export const a = 1;\n`);
|
|
for (let i = 0; i < 10; i++) {
|
|
writeFileSync(
|
|
join(dir, "lib", `b${i}.ts`),
|
|
`export const b${i} = ${i};\n`,
|
|
);
|
|
}
|
|
writeFileSync(
|
|
join(dir, ".pi-project-map.json"),
|
|
JSON.stringify({ reinitFullThresholdPercent: 10 }),
|
|
);
|
|
|
|
const client = createMockFileClient();
|
|
await initProject(dir, { llmClient: client, verbose: false });
|
|
|
|
writeFileSync(
|
|
join(dir, ".pi-map.md"),
|
|
readFileSync(join(dir, ".pi-map.md"), "utf8") + "\nROOT_MARKER",
|
|
);
|
|
writeFileSync(
|
|
join(dir, "lib", ".pi-map.md"),
|
|
readFileSync(join(dir, "lib", ".pi-map.md"), "utf8") + "\nSIBLING_MARKER",
|
|
);
|
|
|
|
await reinitPath(join(dir, "src"), {
|
|
llmClient: client,
|
|
verbose: false,
|
|
});
|
|
|
|
expect(readFileSync(join(dir, ".pi-map.md"), "utf8")).not.toContain(
|
|
"ROOT_MARKER",
|
|
);
|
|
expect(readFileSync(join(dir, "lib", ".pi-map.md"), "utf8")).toContain(
|
|
"SIBLING_MARKER",
|
|
);
|
|
});
|
|
|
|
it("reinit on a non-existent path falls back to full regeneration", async () => {
|
|
mkdirSync(join(dir, "src"));
|
|
mkdirSync(join(dir, "lib"));
|
|
writeFileSync(join(dir, "src", "a.ts"), `export const a = 1;\n`);
|
|
writeFileSync(join(dir, "lib", "b.ts"), `export const b = 1;\n`);
|
|
writeFileSync(
|
|
join(dir, ".pi-project-map.json"),
|
|
JSON.stringify({ reinitFullThresholdPercent: 10 }),
|
|
);
|
|
|
|
const client = createMockFileClient();
|
|
await initProject(dir, { llmClient: client, verbose: false });
|
|
|
|
writeFileSync(
|
|
join(dir, "lib", ".pi-map.md"),
|
|
readFileSync(join(dir, "lib", ".pi-map.md"), "utf8") + "\nSIBLING_MARKER",
|
|
);
|
|
|
|
await reinitPath(join(dir, "does-not-exist"), {
|
|
llmClient: client,
|
|
verbose: false,
|
|
});
|
|
|
|
expect(readFileSync(join(dir, "lib", ".pi-map.md"), "utf8")).not.toContain(
|
|
"SIBLING_MARKER",
|
|
);
|
|
});
|
|
});
|