akm-cli 0.9.0-beta.9 → 0.9.0-rc.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 +592 -0
- package/README.md +12 -4
- package/dist/akm +38 -0
- package/dist/akm-migrate-storage +38 -0
- package/dist/assets/help/help-improve.md +9 -6
- package/dist/assets/hints/cli-hints-full.md +6 -5
- package/dist/assets/profiles/default.json +9 -4
- package/dist/assets/profiles/frequent.json +1 -1
- package/dist/assets/profiles/memory-focus.json +1 -1
- package/dist/assets/profiles/proactive-maintenance.json +25 -0
- package/dist/assets/profiles/quick.json +1 -1
- package/dist/assets/profiles/recombine-only.json +21 -0
- package/dist/assets/profiles/reflect-distill.json +30 -0
- package/dist/assets/profiles/synthesize.json +15 -0
- package/dist/assets/profiles/thorough.json +1 -1
- package/dist/assets/prompts/consolidate-system.md +23 -0
- package/dist/assets/prompts/contradiction-judge.md +33 -0
- package/dist/assets/prompts/distill-knowledge-system.md +22 -0
- package/dist/assets/prompts/distill-lesson-system.md +36 -0
- package/dist/assets/prompts/extract-session.md +11 -3
- package/dist/assets/prompts/graph-extract-system.md +1 -0
- package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
- package/dist/assets/prompts/memory-infer-system.md +1 -0
- package/dist/assets/prompts/memory-infer-user.md +5 -0
- package/dist/assets/prompts/metadata-enhance-system.md +1 -0
- package/dist/assets/prompts/procedural-system.md +44 -0
- package/dist/assets/prompts/recombine-system.md +40 -0
- package/dist/assets/prompts/staleness-detect-system.md +6 -0
- package/dist/assets/prompts/validate-summary-judge.md +1 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
- package/dist/assets/templates/html/health.html +281 -111
- package/dist/assets/wiki/ingest-workflow-template.md +45 -16
- package/dist/assets/wiki/schema-template.md +4 -4
- package/dist/cli/clack.js +56 -0
- package/dist/cli/config-migrate.js +7 -1
- package/dist/cli/confirm.js +1 -1
- package/dist/cli/parse-args.js +46 -1
- package/dist/cli/shared.js +28 -0
- package/dist/cli.js +25 -21
- package/dist/commands/agent/agent-dispatch.js +3 -2
- package/dist/commands/agent/agent-support.js +0 -7
- package/dist/commands/agent/contribute-cli.js +26 -7
- package/dist/commands/config-cli.js +26 -13
- package/dist/commands/env/child-env.js +47 -0
- package/dist/commands/env/env-cli.js +220 -227
- package/dist/commands/env/env.js +14 -67
- package/dist/commands/env/secret-cli.js +140 -138
- package/dist/commands/feedback-cli.js +156 -155
- package/dist/commands/graph/graph-cli.js +5 -13
- package/dist/commands/graph/graph.js +3 -3
- package/dist/commands/health/advisories.js +151 -0
- package/dist/commands/health/checks.js +103 -16
- package/dist/commands/health/html-report.js +447 -81
- package/dist/commands/health/improve-metrics.js +771 -0
- package/dist/commands/health/llm-usage.js +65 -0
- package/dist/commands/health/md-report.js +103 -0
- package/dist/commands/health/metrics.js +278 -0
- package/dist/commands/health/stash-exposure.js +46 -0
- package/dist/commands/health/surfaces.js +216 -0
- package/dist/commands/health/task-runs.js +135 -0
- package/dist/commands/health/types.js +26 -0
- package/dist/commands/health/windows.js +195 -0
- package/dist/commands/health.js +91 -1091
- package/dist/commands/improve/anti-collapse.js +170 -0
- package/dist/commands/improve/calibration.js +161 -0
- package/dist/commands/improve/collapse-detector.js +421 -0
- package/dist/commands/improve/consolidate/chunking.js +141 -0
- package/dist/commands/improve/consolidate/eligibility.js +64 -0
- package/dist/commands/improve/consolidate/merge.js +145 -0
- package/dist/commands/improve/consolidate/sanitize.js +231 -0
- package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
- package/dist/commands/improve/consolidate.js +1295 -1277
- package/dist/commands/improve/dedup.js +482 -0
- package/dist/commands/improve/distill/content-repair.js +202 -0
- package/dist/commands/improve/distill/promote-memory.js +229 -0
- package/dist/commands/improve/distill/quality-gate.js +236 -0
- package/dist/commands/improve/distill-guards.js +127 -0
- package/dist/commands/improve/distill-promotion-policy.js +826 -167
- package/dist/commands/improve/distill.js +228 -605
- package/dist/commands/improve/eligibility.js +434 -0
- package/dist/commands/improve/encoding-salience.js +205 -0
- package/dist/commands/improve/extract-cli.js +179 -59
- package/dist/commands/improve/extract-prompt.js +54 -3
- package/dist/commands/improve/extract-watch.js +140 -0
- package/dist/commands/improve/extract.js +409 -43
- package/dist/commands/improve/feedback-valence.js +54 -0
- package/dist/commands/improve/hot-probation.js +45 -0
- package/dist/commands/improve/improve-auto-accept.js +157 -10
- package/dist/commands/improve/improve-cli.js +115 -73
- package/dist/commands/improve/improve-profiles.js +28 -8
- package/dist/commands/improve/improve-result-file.js +15 -25
- package/dist/commands/improve/improve-session.js +58 -0
- package/dist/commands/improve/improve.js +485 -2764
- package/dist/commands/improve/locks.js +154 -0
- package/dist/commands/improve/loop-stages.js +1100 -0
- package/dist/commands/improve/memory/memory-belief.js +14 -15
- package/dist/commands/improve/memory/memory-contradiction-detect.js +83 -60
- package/dist/commands/improve/memory/memory-improve.js +27 -27
- package/dist/commands/improve/outcome-loop.js +270 -0
- package/dist/commands/improve/preparation.js +2002 -0
- package/dist/commands/improve/proactive-maintenance.js +37 -35
- package/dist/commands/improve/procedural.js +398 -0
- package/dist/commands/improve/recombine.js +818 -0
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/improve/reflect.js +206 -45
- package/dist/commands/improve/salience.js +455 -0
- package/dist/commands/improve/schema-similarity-gate.js +168 -0
- package/dist/commands/improve/shared.js +51 -0
- package/dist/commands/improve/triage.js +93 -0
- package/dist/commands/lint/agent-linter.js +19 -24
- package/dist/commands/lint/base-linter.js +173 -60
- package/dist/commands/lint/command-linter.js +19 -24
- package/dist/commands/lint/env-key-rules.js +38 -1
- package/dist/commands/lint/fact-linter.js +39 -0
- package/dist/commands/lint/index.js +31 -13
- package/dist/commands/lint/memory-linter.js +1 -1
- package/dist/commands/lint/registry.js +7 -2
- package/dist/commands/lint/task-linter.js +3 -3
- package/dist/commands/lint/workflow-linter.js +26 -1
- package/dist/commands/observability-cli.js +4 -4
- package/dist/commands/proposal/drain-policies.js +13 -4
- package/dist/commands/proposal/drain.js +45 -51
- package/dist/commands/proposal/legacy-import.js +115 -0
- package/dist/commands/proposal/proposal-cli.js +24 -34
- package/dist/commands/proposal/proposal.js +2 -1
- package/dist/commands/proposal/propose.js +8 -3
- package/dist/commands/proposal/repository.js +829 -0
- package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
- package/dist/commands/proposal/validators/proposals.js +93 -895
- package/dist/commands/read/curate.js +410 -111
- package/dist/commands/read/knowledge.js +10 -3
- package/dist/commands/read/remember-cli.js +133 -138
- package/dist/commands/read/search-cli.js +15 -8
- package/dist/commands/read/search.js +22 -11
- package/dist/commands/read/show.js +106 -14
- package/dist/commands/registry-cli.js +76 -87
- package/dist/commands/remember.js +11 -12
- package/dist/commands/sources/add-cli.js +91 -95
- package/dist/commands/sources/history.js +1 -1
- package/dist/commands/sources/init.js +66 -18
- package/dist/commands/sources/installed-stashes.js +11 -3
- package/dist/commands/sources/schema-repair.js +44 -46
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/source-add.js +7 -3
- package/dist/commands/sources/sources-cli.js +3 -3
- package/dist/commands/sources/stash-cli.js +19 -39
- package/dist/commands/sources/stash-skeleton.js +57 -8
- package/dist/commands/tasks/default-tasks.js +15 -2
- package/dist/commands/tasks/tasks-cli.js +20 -29
- package/dist/commands/tasks/tasks.js +39 -11
- package/dist/commands/wiki-cli.js +23 -38
- package/dist/commands/workflow-cli.js +15 -1
- package/dist/core/asset/asset-registry.js +3 -1
- package/dist/core/asset/asset-spec.js +21 -4
- package/dist/core/asset/frontmatter.js +188 -167
- package/dist/core/asset/markdown.js +8 -0
- package/dist/core/authoring-rules.js +92 -0
- package/dist/core/common.js +4 -23
- package/dist/core/concurrent.js +10 -1
- package/dist/core/config/config-io.js +10 -1
- package/dist/core/config/config-migration.js +18 -40
- package/dist/core/config/config-schema.js +382 -62
- package/dist/core/config/config-types.js +3 -3
- package/dist/core/config/config.js +67 -22
- package/dist/core/deep-merge.js +38 -0
- package/dist/core/errors.js +1 -0
- package/dist/core/eval/rank-metrics.js +113 -0
- package/dist/core/events.js +4 -7
- package/dist/core/improve-types.js +47 -8
- package/dist/core/logs-db.js +14 -75
- package/dist/core/parse.js +36 -16
- package/dist/core/paths.js +18 -18
- package/dist/core/standards/resolve-standards-context.js +87 -0
- package/dist/core/standards/resolve-stash-standards.js +99 -0
- package/dist/core/standards/resolve-type-conventions.js +66 -0
- package/dist/core/state/migrations.js +770 -0
- package/dist/core/state-db.js +132 -1126
- package/dist/core/structured.js +69 -0
- package/dist/core/time.js +53 -0
- package/dist/core/warn.js +21 -0
- package/dist/core/write-source.js +37 -0
- package/dist/indexer/db/db.js +259 -769
- package/dist/indexer/db/entry-mapper.js +41 -0
- package/dist/indexer/db/graph-db.js +129 -86
- package/dist/indexer/db/llm-cache.js +2 -2
- package/dist/indexer/db/schema.js +516 -0
- package/dist/indexer/ensure-index.js +36 -92
- package/dist/indexer/feedback/utility-policy.js +75 -0
- package/dist/indexer/graph/graph-boost.js +51 -41
- package/dist/indexer/graph/graph-extraction.js +207 -4
- package/dist/indexer/index-writer-lock.js +18 -11
- package/dist/indexer/index-written-assets.js +105 -0
- package/dist/indexer/indexer.js +182 -204
- package/dist/indexer/passes/dir-staleness.js +114 -0
- package/dist/indexer/passes/memory-inference.js +13 -5
- package/dist/indexer/passes/metadata.js +20 -0
- package/dist/indexer/read-preflight.js +23 -0
- package/dist/indexer/search/db-search.js +89 -13
- package/dist/indexer/search/fts-query.js +51 -0
- package/dist/indexer/search/ranking-contributors.js +95 -9
- package/dist/indexer/search/ranking.js +79 -3
- package/dist/indexer/search/search-fields.js +6 -0
- package/dist/indexer/search/search-source.js +32 -21
- package/dist/indexer/search/semantic-status.js +4 -0
- package/dist/indexer/walk/matchers.js +9 -0
- package/dist/indexer/walk/walker.js +21 -13
- package/dist/integrations/agent/builders.js +39 -13
- package/dist/integrations/agent/config.js +20 -59
- package/dist/integrations/agent/detect.js +9 -0
- package/dist/integrations/agent/index.js +3 -19
- package/dist/integrations/agent/model-aliases.js +7 -2
- package/dist/integrations/agent/profiles.js +7 -1
- package/dist/integrations/agent/prompts.js +75 -9
- package/dist/integrations/agent/runner-dispatch.js +59 -0
- package/dist/integrations/agent/runner.js +13 -9
- package/dist/integrations/agent/spawn.js +69 -67
- package/dist/integrations/harnesses/claude/agent-builder.js +1 -1
- package/dist/integrations/harnesses/claude/index.js +2 -0
- package/dist/integrations/harnesses/claude/session-log.js +10 -0
- package/dist/integrations/harnesses/index.js +2 -3
- package/dist/integrations/harnesses/opencode/agent-builder.js +1 -1
- package/dist/integrations/harnesses/opencode/index.js +2 -0
- package/dist/integrations/harnesses/opencode/session-log.js +173 -3
- package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +98 -17
- package/dist/integrations/harnesses/types.js +1 -0
- package/dist/integrations/session-logs/index.js +16 -0
- package/dist/llm/call-ai.js +2 -2
- package/dist/llm/client.js +34 -11
- package/dist/llm/embedder.js +67 -4
- package/dist/llm/embedders/cache.js +3 -1
- package/dist/llm/embedders/deterministic.js +66 -0
- package/dist/llm/embedders/local.js +73 -3
- package/dist/llm/feature-gate.js +16 -15
- package/dist/llm/graph-extract.js +67 -44
- package/dist/llm/memory-infer-impl.js +138 -0
- package/dist/llm/memory-infer.js +1 -127
- package/dist/llm/metadata-enhance.js +44 -31
- package/dist/llm/structured-call.js +49 -0
- package/dist/migrate-storage-node.mjs +8 -0
- package/dist/output/context.js +5 -5
- package/dist/output/renderers.js +85 -14
- package/dist/output/shapes/curate.js +14 -2
- package/dist/output/shapes/helpers.js +0 -3
- package/dist/output/shapes/passthrough.js +2 -1
- package/dist/output/text/helpers.js +29 -1
- package/dist/output/text/workflow.js +1 -0
- package/dist/registry/providers/skills-sh.js +21 -147
- package/dist/registry/providers/static-index.js +15 -157
- package/dist/registry/resolve.js +27 -9
- package/dist/runtime.js +25 -1
- package/dist/scripts/migrate-storage.js +2661 -2369
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +883 -596
- package/dist/setup/detect.js +9 -0
- package/dist/setup/legacy-config.js +106 -0
- package/dist/setup/prompt.js +57 -0
- package/dist/setup/providers.js +14 -0
- package/dist/setup/registry-stash-loader.js +12 -0
- package/dist/setup/semantic-assets.js +124 -0
- package/dist/setup/setup.js +52 -1614
- package/dist/setup/steps/connection.js +734 -0
- package/dist/setup/steps/output.js +31 -0
- package/dist/setup/steps/platforms.js +124 -0
- package/dist/setup/steps/semantic.js +27 -0
- package/dist/setup/steps/sources.js +222 -0
- package/dist/setup/steps/stashdir.js +42 -0
- package/dist/setup/steps/tasks.js +152 -0
- package/dist/sources/include.js +6 -2
- package/dist/sources/providers/filesystem.js +0 -1
- package/dist/sources/providers/git-install.js +210 -0
- package/dist/sources/providers/git-provider.js +234 -0
- package/dist/sources/providers/git-stash.js +248 -0
- package/dist/sources/providers/git.js +10 -661
- package/dist/sources/providers/npm.js +2 -6
- package/dist/sources/providers/provider-utils.js +13 -7
- package/dist/sources/providers/sync-from-ref.js +9 -1
- package/dist/sources/providers/website.js +9 -5
- package/dist/sources/website-ingest.js +187 -29
- package/dist/sources/wiki-fetchers/registry.js +53 -0
- package/dist/sources/wiki-fetchers/youtube.js +239 -0
- package/dist/storage/database.js +45 -10
- package/dist/storage/managed-db.js +82 -0
- package/dist/storage/repositories/canaries-repository.js +107 -0
- package/dist/storage/repositories/consolidation-repository.js +38 -0
- package/dist/storage/repositories/embeddings-repository.js +72 -0
- package/dist/storage/repositories/events-repository.js +187 -0
- package/dist/storage/repositories/extract-sessions-repository.js +96 -0
- package/dist/storage/repositories/improve-runs-repository.js +146 -0
- package/dist/storage/repositories/index-db.js +14 -8
- package/dist/storage/repositories/proposals-repository.js +220 -0
- package/dist/storage/repositories/recombine-repository.js +213 -0
- package/dist/storage/repositories/registry-cache.js +93 -0
- package/dist/storage/repositories/registry-index-cache-repository.js +46 -0
- package/dist/storage/repositories/task-history-repository.js +93 -0
- package/dist/storage/sqlite-pragmas.js +146 -0
- package/dist/tasks/backends/cron.js +1 -1
- package/dist/tasks/backends/index.js +9 -0
- package/dist/tasks/backends/launchd.js +1 -1
- package/dist/tasks/backends/schtasks.js +1 -1
- package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
- package/dist/tasks/runner.js +15 -13
- package/dist/text-import-hook.mjs +0 -0
- package/dist/wiki/wiki.js +52 -11
- package/dist/workflows/cli.js +1 -0
- package/dist/workflows/db.js +3 -4
- package/dist/workflows/runtime/runs.js +43 -118
- package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/README.md +69 -18
- package/docs/data-and-telemetry.md +5 -4
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.9.0.md +39 -0
- package/package.json +10 -10
- package/dist/assets/tasks/core/update-stashes.yml +0 -4
- package/dist/commands/db-cli.js +0 -23
- package/dist/indexer/db/db-backup.js +0 -376
- package/dist/indexer/passes/staleness-detect.js +0 -488
|
@@ -40,13 +40,15 @@ import path from "node:path";
|
|
|
40
40
|
import { TYPE_DIRS } from "../../core/asset/asset-spec.js";
|
|
41
41
|
import { parseFrontmatter } from "../../core/asset/frontmatter.js";
|
|
42
42
|
import { concurrentMap } from "../../core/concurrent.js";
|
|
43
|
-
import { getIndexPassConfig, resolveBatchSize } from "../../core/config/config.js";
|
|
43
|
+
import { getIndexPassConfig, loadConfig, resolveBatchSize } from "../../core/config/config.js";
|
|
44
|
+
import { rethrowIfTestIsolationError } from "../../core/errors.js";
|
|
44
45
|
import { warn, warnVerbose } from "../../core/warn.js";
|
|
45
46
|
import { isProcessEnabled } from "../../llm/feature-gate.js";
|
|
46
47
|
import * as graphExtract from "../../llm/graph-extract.js";
|
|
47
48
|
import { resolveIndexPassLLM } from "../../llm/index-passes.js";
|
|
48
|
-
import { computeBodyHash,
|
|
49
|
-
import { loadStoredGraphSnapshot, replaceStoredGraph } from "../db/graph-db.js";
|
|
49
|
+
import { computeBodyHash, getLlmCacheEntriesByRefs, getLlmCacheEntry, upsertLlmCacheEntry, } from "../db/db.js";
|
|
50
|
+
import { drainExtractionQueue, loadStoredGraphSnapshot, replaceStoredGraph } from "../db/graph-db.js";
|
|
51
|
+
import { GRAPH_SCHEMA_VERSION } from "../db/schema.js";
|
|
50
52
|
import { walkMarkdownFiles } from "../walk/walker.js";
|
|
51
53
|
import { deduplicateGraph } from "./graph-dedup.js";
|
|
52
54
|
/** Schema version for the persisted artifact — bumps trigger a full rebuild. */
|
|
@@ -92,6 +94,12 @@ function computeGraphQualityTelemetry(consideredFiles, extractedFiles, entityCou
|
|
|
92
94
|
};
|
|
93
95
|
}
|
|
94
96
|
export const DEFAULT_GRAPH_EXTRACTION_INCLUDE_TYPES = ["memory", "knowledge"];
|
|
97
|
+
/**
|
|
98
|
+
* Max number of lazy-extraction queue rows drained per pass (#624-P3). Bounds
|
|
99
|
+
* per-run work so a large backlog is spread across runs rather than processed
|
|
100
|
+
* all at once. Generous default — the queue is normally near-empty.
|
|
101
|
+
*/
|
|
102
|
+
const GRAPH_EXTRACTION_QUEUE_DRAIN_LIMIT = 100;
|
|
95
103
|
const SUPPORTED_GRAPH_EXTRACTION_INCLUDE_TYPES = new Set([
|
|
96
104
|
"memory",
|
|
97
105
|
"knowledge",
|
|
@@ -291,8 +299,29 @@ export async function runGraphExtractionPass(ctx) {
|
|
|
291
299
|
warnVerbose("graph extraction: skipped because no primary stash source is available.");
|
|
292
300
|
return { ...EMPTY_RESULT };
|
|
293
301
|
}
|
|
302
|
+
// #624-P3: drain the lazy-extraction queue BEFORE the ranked sweep, highest
|
|
303
|
+
// priority first. Queued paths are extracted individually (per-file merge,
|
|
304
|
+
// other files untouched) so they are processed even when they fall outside
|
|
305
|
+
// the normal candidate set. Default (empty queue) is a byte-identical no-op:
|
|
306
|
+
// drainExtractionQueue returns [] and the loop body never runs.
|
|
307
|
+
if (db) {
|
|
308
|
+
const drained = drainExtractionQueue(db, primary.path, GRAPH_EXTRACTION_QUEUE_DRAIN_LIMIT);
|
|
309
|
+
for (const queued of drained) {
|
|
310
|
+
if (signal?.aborted)
|
|
311
|
+
break;
|
|
312
|
+
await extractGraphForSingleFile(db, primary.path, queued.filePath, queued.bodyHash, { config, signal });
|
|
313
|
+
}
|
|
314
|
+
}
|
|
294
315
|
const includeTypes = getGraphExtractionIncludeTypes(config);
|
|
295
|
-
|
|
316
|
+
let eligible = collectEligibleFiles(primary.path, includeTypes).filter((candidate) => !options.candidatePaths || options.candidatePaths.has(candidate.absPath));
|
|
317
|
+
// P2 (#624): when topN is set and a DB is available, rank the (already
|
|
318
|
+
// candidate-filtered) eligible set by utility_scores DESC and keep only the
|
|
319
|
+
// top-N. Default (topN unset) is byte-identical to today — no ranking query
|
|
320
|
+
// is issued and the eligible set is untouched. Ranking composes WITH the
|
|
321
|
+
// candidatePaths filter: scoped-then-ranked-then-sliced.
|
|
322
|
+
if (db && options.topN != null && options.topN >= 0) {
|
|
323
|
+
eligible = rankCandidatesByUtility(db, eligible, primary.path).slice(0, options.topN);
|
|
324
|
+
}
|
|
296
325
|
const considered = eligible.length;
|
|
297
326
|
if (considered === 0) {
|
|
298
327
|
const scoped = options.candidatePaths ? ` matching ${options.candidatePaths.size} candidate path(s)` : "";
|
|
@@ -344,6 +373,7 @@ export async function runGraphExtractionPass(ctx) {
|
|
|
344
373
|
failureCount: 0,
|
|
345
374
|
htmlErrorCount: 0,
|
|
346
375
|
retryAttempts: 0,
|
|
376
|
+
nonArrayBatchFailures: 0,
|
|
347
377
|
};
|
|
348
378
|
const canReusePreviousGraph = previousGraph.telemetry?.extractorId === extractorId;
|
|
349
379
|
const runtimeTelemetry = {
|
|
@@ -597,6 +627,7 @@ export async function runGraphExtractionPass(ctx) {
|
|
|
597
627
|
telemetry.failureCount = runtimeTelemetry.failureCount ?? 0;
|
|
598
628
|
telemetry.htmlErrorCount = runtimeTelemetry.htmlErrorCount ?? 0;
|
|
599
629
|
telemetry.retryAttempts = runtimeTelemetry.retryAttempts ?? 0;
|
|
630
|
+
telemetry.nonArrayBatchFailures = runtimeTelemetry.nonArrayBatchFailures ?? 0;
|
|
600
631
|
const qualityConsidered = mergedNodes.length;
|
|
601
632
|
const qualityExtracted = mergedNodes.filter((node) => node.status === "extracted" && node.entities.length > 0).length;
|
|
602
633
|
const quality = computeGraphQualityTelemetry(qualityConsidered, qualityExtracted, deduped.entities.length, deduped.relations.length);
|
|
@@ -628,6 +659,178 @@ export async function runGraphExtractionPass(ctx) {
|
|
|
628
659
|
warnings,
|
|
629
660
|
};
|
|
630
661
|
}
|
|
662
|
+
/**
|
|
663
|
+
* Infer the asset type (`memory`, `knowledge`, …) for a path from the stash
|
|
664
|
+
* directory segment it lives under. Returns the matching include-type, or
|
|
665
|
+
* `undefined` when the path is not under a known graph-eligible type dir.
|
|
666
|
+
*/
|
|
667
|
+
function inferGraphTypeForPath(stashRoot, absPath) {
|
|
668
|
+
const rel = path.relative(stashRoot, absPath);
|
|
669
|
+
const firstSeg = rel.split(path.sep)[0];
|
|
670
|
+
if (!firstSeg)
|
|
671
|
+
return undefined;
|
|
672
|
+
for (const type of SUPPORTED_GRAPH_EXTRACTION_INCLUDE_TYPES) {
|
|
673
|
+
if (TYPE_DIRS[type] === firstSeg)
|
|
674
|
+
return type;
|
|
675
|
+
}
|
|
676
|
+
return undefined;
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* #624-P3 — extract graph data for a SINGLE file and merge it into the stored
|
|
680
|
+
* graph WITHOUT clobbering other files' rows.
|
|
681
|
+
*
|
|
682
|
+
* Re-reads the body from disk at call time (the queued body_hash is NOT trusted
|
|
683
|
+
* blindly — the file may have been deleted or changed since enqueue) and skips
|
|
684
|
+
* silently (returns `false`) when the file is gone or empty. Resolves the LLM
|
|
685
|
+
* via {@link resolveIndexPassLLM} (model-available guard: returns `false` when
|
|
686
|
+
* no provider is configured) UNLESS `opts.llmOverride` is supplied, in which
|
|
687
|
+
* case the override is the extractor seam (used by tests and by callers that
|
|
688
|
+
* already hold a resolved model).
|
|
689
|
+
*
|
|
690
|
+
* Returns `true` when a graph row was written for the file, `false` on any
|
|
691
|
+
* skip (missing file, empty body, unknown type, no model, or extraction error).
|
|
692
|
+
*/
|
|
693
|
+
export async function extractGraphForSingleFile(db, stashRoot, filePath, bodyHash, opts) {
|
|
694
|
+
try {
|
|
695
|
+
// Re-read from disk — never trust a stale queued body.
|
|
696
|
+
let raw;
|
|
697
|
+
try {
|
|
698
|
+
raw = fs.readFileSync(filePath, "utf8");
|
|
699
|
+
}
|
|
700
|
+
catch {
|
|
701
|
+
return false; // file gone / unreadable → silent skip
|
|
702
|
+
}
|
|
703
|
+
const parsed = parseFrontmatter(raw);
|
|
704
|
+
const body = parsed.content.trim();
|
|
705
|
+
if (!body)
|
|
706
|
+
return false;
|
|
707
|
+
const type = inferGraphTypeForPath(stashRoot, filePath) ?? "memory";
|
|
708
|
+
const effectiveHash = bodyHash ?? computeBodyHash(body);
|
|
709
|
+
// Extract — via the injected seam, or the real per-asset path.
|
|
710
|
+
let extraction;
|
|
711
|
+
if (opts?.llmOverride) {
|
|
712
|
+
const out = await opts.llmOverride(body);
|
|
713
|
+
extraction = {
|
|
714
|
+
entities: out.entities,
|
|
715
|
+
relations: out.relations,
|
|
716
|
+
...(out.confidence !== undefined ? { confidence: out.confidence } : {}),
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
const config = opts?.config ?? loadConfig();
|
|
721
|
+
if (!isProcessEnabled("index", "graph_extraction", config))
|
|
722
|
+
return false;
|
|
723
|
+
const llmConfig = resolveIndexPassLLM("graph", config);
|
|
724
|
+
if (!llmConfig)
|
|
725
|
+
return false; // model-available guard
|
|
726
|
+
const result = await graphExtract.extractGraphFromBody(llmConfig, body, opts?.signal, config);
|
|
727
|
+
extraction = {
|
|
728
|
+
entities: result.entities,
|
|
729
|
+
relations: result.relations,
|
|
730
|
+
...(result.confidence !== undefined ? { confidence: result.confidence } : {}),
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
const entities = [...new Set(extraction.entities.map((e) => e.trim()).filter(Boolean))];
|
|
734
|
+
const relations = extraction.relations
|
|
735
|
+
.map((r) => ({
|
|
736
|
+
from: r.from.trim(),
|
|
737
|
+
to: r.to.trim(),
|
|
738
|
+
...(r.type ? { type: r.type.trim() } : {}),
|
|
739
|
+
...(normalizeConfidence(r.confidence) !== undefined ? { confidence: normalizeConfidence(r.confidence) } : {}),
|
|
740
|
+
}))
|
|
741
|
+
.filter((r) => r.from && r.to);
|
|
742
|
+
const node = {
|
|
743
|
+
path: filePath,
|
|
744
|
+
type,
|
|
745
|
+
bodyHash: effectiveHash,
|
|
746
|
+
entities,
|
|
747
|
+
relations,
|
|
748
|
+
...(normalizeConfidence(extraction.confidence) !== undefined
|
|
749
|
+
? { confidence: normalizeConfidence(extraction.confidence) }
|
|
750
|
+
: {}),
|
|
751
|
+
status: entities.length > 0 ? "extracted" : "empty",
|
|
752
|
+
reason: entities.length > 0 ? "none" : "no_graph_content",
|
|
753
|
+
extractionRunId: crypto.randomUUID(),
|
|
754
|
+
};
|
|
755
|
+
// Merge with the previously-stored nodes, scoping the refresh to JUST this
|
|
756
|
+
// path so other files' rows are preserved (and graph_meta counts refresh).
|
|
757
|
+
const previousGraph = loadGraphFile(stashRoot, db);
|
|
758
|
+
const candidatePaths = new Set([filePath]);
|
|
759
|
+
const mergedNodes = mergeGraphNodes(previousGraph.files, [node], candidatePaths);
|
|
760
|
+
const assetRefs = mergedNodes.map((n) => n.path);
|
|
761
|
+
const deduped = deduplicateGraph(mergedNodes.map((n) => ({ entities: n.entities, relations: n.relations })), assetRefs);
|
|
762
|
+
const qualityExtracted = mergedNodes.filter((n) => n.status === "extracted" && n.entities.length > 0).length;
|
|
763
|
+
const quality = computeGraphQualityTelemetry(mergedNodes.length, qualityExtracted, deduped.entities.length, deduped.relations.length);
|
|
764
|
+
const graph = {
|
|
765
|
+
schemaVersion: GRAPH_FILE_SCHEMA_VERSION,
|
|
766
|
+
generatedAt: new Date().toISOString(),
|
|
767
|
+
stashRoot,
|
|
768
|
+
files: mergedNodes,
|
|
769
|
+
entities: deduped.entities,
|
|
770
|
+
relations: deduped.relations,
|
|
771
|
+
quality,
|
|
772
|
+
...(previousGraph.telemetry ? { telemetry: previousGraph.telemetry } : {}),
|
|
773
|
+
};
|
|
774
|
+
return writeGraphFile(stashRoot, graph, db);
|
|
775
|
+
}
|
|
776
|
+
catch (err) {
|
|
777
|
+
rethrowIfTestIsolationError(err);
|
|
778
|
+
return false;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
// ── Eligible-file detection ─────────────────────────────────────────────────
|
|
782
|
+
/**
|
|
783
|
+
* Rank eligible graph-extraction candidates by their entry `utility_scores`,
|
|
784
|
+
* highest first, for the incremental high-signal-first sweep (P2 of #624).
|
|
785
|
+
*
|
|
786
|
+
* The join is READ-ONLY (`entries.file_path = candidate.absPath`, then
|
|
787
|
+
* `entries.id -> utility_scores.entry_id`) and does NOT re-couple the graph
|
|
788
|
+
* rows to `entries`. It reads the GLOBAL `utility_scores` table (not the
|
|
789
|
+
* per-scope `utility_scores_scoped`), so ranking is corpus-wide; `stashRoot`
|
|
790
|
+
* is accepted for call-site symmetry/future scoping but is not used to filter
|
|
791
|
+
* (the global table has no `stash_root` column).
|
|
792
|
+
*
|
|
793
|
+
* Candidates with no matching `entries` row, or an entry with no
|
|
794
|
+
* `utility_scores` row, get an effective utility of 0 (LEFT JOIN + COALESCE)
|
|
795
|
+
* and sort LAST — they are deprioritized, never dropped, so a `topN >= total`
|
|
796
|
+
* slice still includes them and they remain reachable on later runs.
|
|
797
|
+
*
|
|
798
|
+
* Ties (equal utility) break by `file_path` ASC for deterministic output.
|
|
799
|
+
* Returns a NEW array; the input is not mutated. SQLite's ~999 bound-parameter
|
|
800
|
+
* cap is respected by chunking the `IN (...)` lookup at 500.
|
|
801
|
+
*
|
|
802
|
+
* Exported for direct unit testing.
|
|
803
|
+
*/
|
|
804
|
+
export function rankCandidatesByUtility(db, candidates, _stashRoot) {
|
|
805
|
+
// Cannot rank without a DB → return the input unranked rather than throw.
|
|
806
|
+
// Keeps the DB-less code path (reuse-from-memory) working when topN is set.
|
|
807
|
+
if (!db || candidates.length === 0)
|
|
808
|
+
return candidates;
|
|
809
|
+
const utilityByPath = new Map();
|
|
810
|
+
const CHUNK = 500;
|
|
811
|
+
for (let start = 0; start < candidates.length; start += CHUNK) {
|
|
812
|
+
const chunk = candidates.slice(start, start + CHUNK);
|
|
813
|
+
const paths = chunk.map((c) => c.absPath);
|
|
814
|
+
const placeholders = paths.map(() => "?").join(", ");
|
|
815
|
+
const rows = db
|
|
816
|
+
.prepare(`SELECT e.file_path AS file_path, COALESCE(MAX(u.utility), 0) AS utility
|
|
817
|
+
FROM entries e
|
|
818
|
+
LEFT JOIN utility_scores u ON u.entry_id = e.id
|
|
819
|
+
WHERE e.file_path IN (${placeholders})
|
|
820
|
+
GROUP BY e.file_path`)
|
|
821
|
+
.all(...paths);
|
|
822
|
+
for (const row of rows) {
|
|
823
|
+
utilityByPath.set(row.file_path, row.utility ?? 0);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
return [...candidates].sort((a, b) => {
|
|
827
|
+
const ua = utilityByPath.get(a.absPath) ?? 0;
|
|
828
|
+
const ub = utilityByPath.get(b.absPath) ?? 0;
|
|
829
|
+
if (ub !== ua)
|
|
830
|
+
return ub - ua; // utility DESC
|
|
831
|
+
return a.absPath < b.absPath ? -1 : a.absPath > b.absPath ? 1 : 0; // tie-break: path ASC
|
|
832
|
+
});
|
|
833
|
+
}
|
|
631
834
|
/**
|
|
632
835
|
* Scan the primary stash for `memory:` and `knowledge:` markdown files
|
|
633
836
|
* suitable for graph extraction. The directory layout convention is the
|
|
@@ -7,6 +7,7 @@ import { probeLock, releaseLock, releaseLockIfOwned, tryAcquireLockSync } from "
|
|
|
7
7
|
import { getDbPath, getIndexWriterLockPath } from "../core/paths.js";
|
|
8
8
|
const INDEX_WRITER_LOCK_STALE_AFTER_MS = 12 * 60 * 60 * 1000;
|
|
9
9
|
const INDEX_WRITER_WAIT_MS = 100;
|
|
10
|
+
const DEFAULT_INDEX_WRITER_MAX_WAIT_MS = 10 * 60 * 1000;
|
|
10
11
|
const heldLocks = new Map();
|
|
11
12
|
function buildPayload(purpose, pid = process.pid) {
|
|
12
13
|
return JSON.stringify({
|
|
@@ -46,27 +47,26 @@ function retainHeldLock(lockPath) {
|
|
|
46
47
|
heldLocks.set(lockPath, { depth: 1, exitHandler });
|
|
47
48
|
return { lockPath, release: () => releaseHeldLock(lockPath) };
|
|
48
49
|
}
|
|
49
|
-
function detachHeldLock(lockPath) {
|
|
50
|
-
const held = heldLocks.get(lockPath);
|
|
51
|
-
if (!held)
|
|
52
|
-
return;
|
|
53
|
-
heldLocks.delete(lockPath);
|
|
54
|
-
process.off("exit", held.exitHandler);
|
|
55
|
-
}
|
|
56
50
|
export async function acquireIndexWriterLease(options) {
|
|
57
51
|
const mode = options.mode ?? "wait";
|
|
58
52
|
const lockPath = getIndexWriterLockPath();
|
|
53
|
+
const startedAt = Date.now();
|
|
54
|
+
const maxWaitMs = options.maxWaitMs ?? DEFAULT_INDEX_WRITER_MAX_WAIT_MS;
|
|
59
55
|
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
60
56
|
if (heldLocks.has(lockPath)) {
|
|
57
|
+
options.onAcquired?.({ waitedMs: 0 });
|
|
61
58
|
return retainHeldLock(lockPath);
|
|
62
59
|
}
|
|
60
|
+
let lastWaitNoticeMs = 0;
|
|
63
61
|
while (true) {
|
|
64
62
|
throwIfAborted(options.signal);
|
|
65
63
|
if (tryAcquireLockSync(lockPath, buildPayload(options.purpose))) {
|
|
64
|
+
options.onAcquired?.({ waitedMs: Date.now() - startedAt });
|
|
66
65
|
return retainHeldLock(lockPath);
|
|
67
66
|
}
|
|
68
67
|
const probe = probeLock(lockPath, { staleAfterMs: INDEX_WRITER_LOCK_STALE_AFTER_MS });
|
|
69
68
|
if (probe.state === "held" && probe.holderPid === process.pid) {
|
|
69
|
+
options.onAcquired?.({ waitedMs: Date.now() - startedAt });
|
|
70
70
|
return retainHeldLock(lockPath);
|
|
71
71
|
}
|
|
72
72
|
if (probe.state === "stale") {
|
|
@@ -75,6 +75,17 @@ export async function acquireIndexWriterLease(options) {
|
|
|
75
75
|
}
|
|
76
76
|
if (mode === "try")
|
|
77
77
|
return undefined;
|
|
78
|
+
// Held by another live process. Time out only *after* a real acquisition
|
|
79
|
+
// attempt, so a caller with maxWaitMs:0 still gets one chance at a free lock
|
|
80
|
+
// instead of throwing before it ever tries.
|
|
81
|
+
if (maxWaitMs >= 0 && Date.now() - startedAt >= maxWaitMs) {
|
|
82
|
+
throw new Error(`timed out waiting for index writer lease for ${options.purpose}`);
|
|
83
|
+
}
|
|
84
|
+
const waitedMs = Date.now() - startedAt;
|
|
85
|
+
if (waitedMs - lastWaitNoticeMs >= 15000) {
|
|
86
|
+
options.onWait?.({ waitedMs });
|
|
87
|
+
lastWaitNoticeMs = waitedMs;
|
|
88
|
+
}
|
|
78
89
|
await delay(INDEX_WRITER_WAIT_MS);
|
|
79
90
|
}
|
|
80
91
|
}
|
|
@@ -90,10 +101,6 @@ export async function withIndexWriterLease(options, run) {
|
|
|
90
101
|
lease.release();
|
|
91
102
|
}
|
|
92
103
|
}
|
|
93
|
-
export function handoffIndexWriterLeaseToPid(lease, pid, purpose) {
|
|
94
|
-
fs.writeFileSync(lease.lockPath, buildPayload(purpose, pid), "utf8");
|
|
95
|
-
detachHeldLock(lease.lockPath);
|
|
96
|
-
}
|
|
97
104
|
export function probeIndexWriterLease() {
|
|
98
105
|
return probeLock(getIndexWriterLockPath(), { staleAfterMs: INDEX_WRITER_LOCK_STALE_AFTER_MS });
|
|
99
106
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Write-path indexing: targeted single-file index updates for asset writers.
|
|
6
|
+
*
|
|
7
|
+
* The index is maintained eagerly by every first-class mutation command
|
|
8
|
+
* (`source add`, `wiki`, `workflow`, `setup` all run `akmIndex()` after
|
|
9
|
+
* writing). The memory write paths — `akm remember` / `writeMarkdownAsset`
|
|
10
|
+
* and extract's session assets — historically did not, which is why reads
|
|
11
|
+
* used to compensate with stale-triggered background reindexes (the
|
|
12
|
+
* lock-contention footgun removed alongside this module's introduction; see
|
|
13
|
+
* docs/design/read-path-reindex-contention-findings.md §7).
|
|
14
|
+
*
|
|
15
|
+
* This is NOT a general reindex. It upserts exactly the files the caller just
|
|
16
|
+
* wrote: frontmatter/metadata via the shared matcher pipeline, the `entries`
|
|
17
|
+
* row, and an incremental FTS refresh. Embeddings, index-time LLM passes,
|
|
18
|
+
* graph extraction, `builtAt`, and the per-dir walk cache are all deliberately
|
|
19
|
+
* untouched — the next full run heals them (the opportunistic-recovery
|
|
20
|
+
* strategy of docs/technical/index-consistency-adr.md).
|
|
21
|
+
*/
|
|
22
|
+
import fs from "node:fs";
|
|
23
|
+
import path from "node:path";
|
|
24
|
+
import { getDbPath } from "../core/paths.js";
|
|
25
|
+
import { warnVerbose } from "../core/warn.js";
|
|
26
|
+
import { closeDatabase, getEntryCount, openExistingDatabase, rebuildFts, upsertEntry } from "./db/db.js";
|
|
27
|
+
import { generateMetadataFlat } from "./passes/metadata.js";
|
|
28
|
+
import { buildSearchText } from "./search/search-fields.js";
|
|
29
|
+
/**
|
|
30
|
+
* Busy-timeout (ms) for write-path index upserts. A real write — unlike the
|
|
31
|
+
* 250ms telemetry inserts — but it must not hang `akm remember` for the full
|
|
32
|
+
* default 30s behind a running full reindex. When it times out, the upsert is
|
|
33
|
+
* skipped and the asset becomes searchable after that reindex instead.
|
|
34
|
+
*/
|
|
35
|
+
export const WRITE_PATH_INDEX_BUSY_TIMEOUT_MS = 5_000;
|
|
36
|
+
/**
|
|
37
|
+
* Index the given just-written asset files into the existing local index.
|
|
38
|
+
*
|
|
39
|
+
* FAIL-OPEN at every step: any error (index.db absent, empty, locked past the
|
|
40
|
+
* busy timeout, unparseable file) is reduced to a verbose-only warning and the
|
|
41
|
+
* write command succeeds untouched. The degraded outcome is exactly the
|
|
42
|
+
* pre-write-path-indexing behavior: the asset appears after the next full
|
|
43
|
+
* `akm index` / improve-cron run.
|
|
44
|
+
*
|
|
45
|
+
* An absent or empty index is skipped on purpose — bootstrap belongs to the
|
|
46
|
+
* first read (`ensureIndex`) or an explicit `akm index`, which also cover
|
|
47
|
+
* embeddings and the other passes this fast path skips.
|
|
48
|
+
*/
|
|
49
|
+
export async function indexWrittenAssets(stashDir, filePaths) {
|
|
50
|
+
try {
|
|
51
|
+
const dbPath = getDbPath();
|
|
52
|
+
if (!fs.existsSync(dbPath))
|
|
53
|
+
return;
|
|
54
|
+
// The full walk never descends into dot-directories (they hold state like
|
|
55
|
+
// `.meta/`, `.stash.json`), and `shouldIndexStashFile` relies on the walker
|
|
56
|
+
// for that — mirror it here so this fast path indexes exactly what a full
|
|
57
|
+
// run would.
|
|
58
|
+
const files = filePaths.filter((f) => {
|
|
59
|
+
if (!fs.existsSync(f))
|
|
60
|
+
return false;
|
|
61
|
+
const rel = path.relative(stashDir, f);
|
|
62
|
+
return !rel.split(/[\\/]+/).some((segment) => segment.startsWith("."));
|
|
63
|
+
});
|
|
64
|
+
if (files.length === 0)
|
|
65
|
+
return;
|
|
66
|
+
// Generate metadata BEFORE opening the DB so the write window stays
|
|
67
|
+
// short. One call per file keeps the entry↔path pairing exact.
|
|
68
|
+
const pairs = [];
|
|
69
|
+
for (const file of files) {
|
|
70
|
+
const generated = await generateMetadataFlat(stashDir, [file]);
|
|
71
|
+
const entry = generated.entries[0];
|
|
72
|
+
// Workflows carry a side-table document upsert this fast path doesn't
|
|
73
|
+
// do; no current caller writes them, but guard so one never lands
|
|
74
|
+
// half-indexed.
|
|
75
|
+
if (entry && entry.type !== "workflow")
|
|
76
|
+
pairs.push({ file, entry });
|
|
77
|
+
}
|
|
78
|
+
if (pairs.length === 0)
|
|
79
|
+
return;
|
|
80
|
+
const db = openExistingDatabase(dbPath);
|
|
81
|
+
try {
|
|
82
|
+
db.exec(`PRAGMA busy_timeout = ${WRITE_PATH_INDEX_BUSY_TIMEOUT_MS}`);
|
|
83
|
+
if (getEntryCount(db) === 0)
|
|
84
|
+
return;
|
|
85
|
+
for (const { file, entry } of pairs) {
|
|
86
|
+
const entryKey = `${stashDir}:${entry.type}:${entry.name}`;
|
|
87
|
+
let entryWithSize = entry;
|
|
88
|
+
try {
|
|
89
|
+
entryWithSize = { ...entry, fileSize: fs.statSync(file).size };
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// stat raced a delete — index without the size, like the full walk does.
|
|
93
|
+
}
|
|
94
|
+
upsertEntry(db, entryKey, path.dirname(file), file, stashDir, entryWithSize, buildSearchText(entry));
|
|
95
|
+
}
|
|
96
|
+
rebuildFts(db, { incremental: true });
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
closeDatabase(db);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
warnVerbose("Write-path index update skipped (asset appears after the next full index):", error instanceof Error ? error.message : String(error));
|
|
104
|
+
}
|
|
105
|
+
}
|