akm-cli 0.8.0-rc2 → 0.8.1
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/{.github/CHANGELOG.md → CHANGELOG.md} +238 -3
- package/README.md +22 -6
- package/SECURITY.md +93 -0
- package/dist/assets/help/help-accept.md +12 -0
- package/dist/assets/help/help-improve.md +81 -0
- package/dist/{commands → assets}/help/help-proposals.md +7 -4
- package/dist/assets/help/help-reject.md +11 -0
- package/dist/{output → assets/hints}/cli-hints-full.md +60 -32
- package/dist/{output → assets/hints}/cli-hints-short.md +10 -7
- package/dist/assets/profiles/default.json +15 -0
- package/dist/assets/profiles/graph-refresh.json +13 -0
- package/dist/assets/profiles/memory-focus.json +12 -0
- package/dist/assets/profiles/quick.json +15 -0
- package/dist/assets/profiles/thorough.json +15 -0
- package/dist/assets/prompts/extract-session.md +80 -0
- package/dist/assets/prompts/graph-extract-user-prompt.md +35 -0
- package/dist/assets/tasks/graph-refresh-weekly.yml +10 -0
- package/dist/cli/config-migrate.js +144 -0
- package/dist/cli/config-validate.js +39 -0
- package/dist/cli/confirm.js +73 -0
- package/dist/cli/parse-args.js +93 -3
- package/dist/cli/shared.js +129 -0
- package/dist/cli.js +2141 -1268
- package/dist/commands/add-cli.js +279 -0
- package/dist/commands/agent-dispatch.js +20 -12
- package/dist/commands/agent-support.js +11 -5
- package/dist/commands/completions.js +3 -0
- package/dist/commands/config-cli.js +129 -517
- package/dist/commands/consolidate.js +1557 -147
- package/dist/commands/curate.js +44 -3
- package/dist/commands/db-cli.js +23 -0
- package/dist/commands/distill-promotion-policy.js +5 -3
- package/dist/commands/distill.js +906 -100
- package/dist/commands/env.js +213 -0
- package/dist/commands/eval-cases.js +3 -0
- package/dist/commands/events.js +3 -0
- package/dist/commands/extract-cli.js +127 -0
- package/dist/commands/extract-prompt.js +217 -0
- package/dist/commands/extract.js +477 -0
- package/dist/commands/feedback-cli.js +331 -0
- package/dist/commands/graph.js +260 -5
- package/dist/commands/health.js +1042 -55
- package/dist/commands/history.js +51 -16
- package/dist/commands/improve-auto-accept.js +97 -0
- package/dist/commands/improve-cli.js +236 -0
- package/dist/commands/improve-profiles.js +138 -0
- package/dist/commands/improve-result-file.js +167 -0
- package/dist/commands/improve.js +1736 -346
- package/dist/commands/info.js +26 -28
- package/dist/commands/init.js +49 -1
- package/dist/commands/installed-stashes.js +6 -23
- package/dist/commands/knowledge.js +3 -0
- package/dist/commands/lint/agent-linter.js +3 -0
- package/dist/commands/lint/base-linter.js +199 -5
- package/dist/commands/lint/command-linter.js +3 -0
- package/dist/commands/lint/default-linter.js +3 -0
- package/dist/commands/lint/env-key-rules.js +154 -0
- package/dist/commands/lint/index.js +92 -3
- package/dist/commands/lint/knowledge-linter.js +3 -0
- package/dist/commands/lint/markdown-insertion.js +343 -0
- package/dist/commands/lint/memory-linter.js +3 -0
- package/dist/commands/lint/registry.js +3 -0
- package/dist/commands/lint/skill-linter.js +3 -0
- package/dist/commands/lint/task-linter.js +15 -12
- package/dist/commands/lint/types.js +3 -0
- package/dist/commands/lint/workflow-linter.js +3 -0
- package/dist/commands/lint.js +3 -0
- package/dist/commands/migration-help.js +5 -2
- package/dist/commands/proposal-drain-policies.js +128 -0
- package/dist/commands/proposal-drain.js +477 -0
- package/dist/commands/proposal.js +60 -6
- package/dist/commands/propose.js +24 -19
- package/dist/commands/reflect.js +1004 -94
- package/dist/commands/registry-cli.js +150 -0
- package/dist/commands/registry-search.js +3 -0
- package/dist/commands/remember-cli.js +257 -0
- package/dist/commands/remember.js +15 -6
- package/dist/commands/schema-repair.js +88 -15
- package/dist/commands/search.js +99 -14
- package/dist/commands/secret.js +173 -0
- package/dist/commands/self-update.js +3 -0
- package/dist/commands/show.js +32 -13
- package/dist/commands/source-add.js +7 -35
- package/dist/commands/source-clone.js +3 -0
- package/dist/commands/source-manage.js +3 -0
- package/dist/commands/tasks.js +161 -95
- package/dist/commands/url-checker.js +3 -0
- package/dist/core/action-contributors.js +3 -0
- package/dist/core/asset-ref.js +13 -2
- package/dist/core/asset-registry.js +9 -2
- package/dist/core/asset-serialize.js +88 -0
- package/dist/core/asset-spec.js +61 -5
- package/dist/core/common.js +93 -5
- package/dist/core/concurrent.js +3 -0
- package/dist/core/config-io.js +347 -0
- package/dist/core/config-migration.js +622 -0
- package/dist/core/config-schema.js +558 -0
- package/dist/core/config-sources.js +108 -0
- package/dist/core/config-types.js +4 -0
- package/dist/core/config-walker.js +337 -0
- package/dist/core/config.js +366 -1077
- package/dist/core/errors.js +42 -20
- package/dist/core/events.js +31 -25
- package/dist/core/file-lock.js +104 -0
- package/dist/core/frontmatter.js +75 -10
- package/dist/core/lesson-lint.js +3 -0
- package/dist/core/markdown.js +3 -0
- package/dist/core/memory-belief.js +62 -0
- package/dist/core/memory-contradiction-detect.js +274 -0
- package/dist/core/memory-improve.js +142 -14
- package/dist/core/parse.js +3 -0
- package/dist/core/paths.js +218 -50
- package/dist/core/proposal-quality-validators.js +380 -0
- package/dist/core/proposal-validators.js +11 -3
- package/dist/core/proposals.js +464 -5
- package/dist/core/state-db.js +349 -56
- package/dist/core/text-truncation.js +107 -0
- package/dist/core/time.js +3 -0
- package/dist/core/tty.js +59 -0
- package/dist/core/warn.js +7 -2
- package/dist/core/write-source.js +12 -0
- package/dist/indexer/db-backup.js +391 -0
- package/dist/indexer/db-search.js +136 -28
- package/dist/indexer/db.js +661 -166
- package/dist/indexer/ensure-index.js +3 -0
- package/dist/indexer/file-context.js +3 -0
- package/dist/indexer/graph-boost.js +162 -40
- package/dist/indexer/graph-db.js +241 -51
- package/dist/indexer/graph-dedup.js +3 -7
- package/dist/indexer/graph-extraction.js +242 -149
- package/dist/indexer/index-context.js +3 -9
- package/dist/indexer/indexer.js +86 -16
- package/dist/indexer/llm-cache.js +24 -19
- package/dist/indexer/manifest.js +3 -0
- package/dist/indexer/matchers.js +184 -11
- package/dist/indexer/memory-inference.js +94 -50
- package/dist/indexer/metadata-contributors.js +3 -0
- package/dist/indexer/metadata.js +110 -50
- package/dist/indexer/path-resolver.js +3 -0
- package/dist/indexer/project-context.js +192 -0
- package/dist/indexer/ranking-contributors.js +134 -7
- package/dist/indexer/ranking.js +8 -1
- package/dist/indexer/search-fields.js +5 -9
- package/dist/indexer/search-hit-enrichers.js +91 -2
- package/dist/indexer/search-source.js +20 -1
- package/dist/indexer/semantic-status.js +4 -1
- package/dist/indexer/staleness-detect.js +447 -0
- package/dist/indexer/usage-events.js +12 -9
- package/dist/indexer/walker.js +3 -0
- package/dist/integrations/agent/builders.js +135 -0
- package/dist/integrations/agent/config.js +121 -401
- package/dist/integrations/agent/detect.js +3 -0
- package/dist/integrations/agent/index.js +6 -14
- package/dist/integrations/agent/model-aliases.js +55 -0
- package/dist/integrations/agent/profiles.js +3 -0
- package/dist/integrations/agent/prompts.js +137 -8
- package/dist/integrations/agent/runner.js +208 -0
- package/dist/integrations/agent/sdk-runner.js +8 -2
- package/dist/integrations/agent/spawn.js +54 -14
- package/dist/integrations/github.js +3 -0
- package/dist/integrations/lockfile.js +22 -51
- package/dist/integrations/session-logs/index.js +4 -0
- package/dist/integrations/session-logs/inline-refs.js +35 -0
- package/dist/integrations/session-logs/pre-filter.js +152 -0
- package/dist/integrations/session-logs/providers/claude-code.js +226 -0
- package/dist/integrations/session-logs/providers/opencode.js +231 -25
- package/dist/integrations/session-logs/types.js +3 -0
- package/dist/llm/call-ai.js +14 -26
- package/dist/llm/client.js +16 -2
- package/dist/llm/embedder.js +20 -29
- package/dist/llm/embedders/cache.js +3 -7
- package/dist/llm/embedders/local.js +42 -1
- package/dist/llm/embedders/remote.js +20 -8
- package/dist/llm/embedders/types.js +3 -7
- package/dist/llm/feature-gate.js +92 -56
- package/dist/llm/graph-extract.js +402 -31
- package/dist/llm/index-passes.js +44 -29
- package/dist/llm/memory-infer.js +30 -2
- package/dist/llm/metadata-enhance.js +3 -7
- package/dist/output/cli-hints.js +7 -4
- package/dist/output/context.js +60 -8
- package/dist/output/renderers.js +170 -194
- package/dist/output/shapes/curate.js +56 -0
- package/dist/output/shapes/distill.js +10 -0
- package/dist/output/shapes/env-list.js +19 -0
- package/dist/output/shapes/events.js +11 -0
- package/dist/output/shapes/helpers.js +424 -0
- package/dist/output/shapes/history.js +7 -0
- package/dist/output/shapes/passthrough.js +105 -0
- package/dist/output/shapes/proposal-accept.js +7 -0
- package/dist/output/shapes/proposal-diff.js +7 -0
- package/dist/output/shapes/proposal-list.js +7 -0
- package/dist/output/shapes/proposal-producer.js +11 -0
- package/dist/output/shapes/proposal-reject.js +7 -0
- package/dist/output/shapes/proposal-show.js +7 -0
- package/dist/output/shapes/registry-search.js +6 -0
- package/dist/output/shapes/registry.js +30 -0
- package/dist/output/shapes/search.js +6 -0
- package/dist/output/shapes/secret-list.js +19 -0
- package/dist/output/shapes/show.js +6 -0
- package/dist/output/shapes/vault-list.js +19 -0
- package/dist/output/shapes.js +51 -549
- package/dist/output/text/add.js +6 -0
- package/dist/output/text/clone.js +6 -0
- package/dist/output/text/config.js +6 -0
- package/dist/output/text/curate.js +6 -0
- package/dist/output/text/distill.js +7 -0
- package/dist/output/text/enable-disable.js +7 -0
- package/dist/output/text/events.js +10 -0
- package/dist/output/text/feedback.js +6 -0
- package/dist/output/text/helpers.js +1059 -0
- package/dist/output/text/history.js +7 -0
- package/dist/output/text/import.js +6 -0
- package/dist/output/text/index.js +6 -0
- package/dist/output/text/info.js +6 -0
- package/dist/output/text/init.js +6 -0
- package/dist/output/text/list.js +6 -0
- package/dist/output/text/proposal-producer.js +8 -0
- package/dist/output/text/proposal.js +12 -0
- package/dist/output/text/registry-commands.js +11 -0
- package/dist/output/text/registry.js +30 -0
- package/dist/output/text/remember.js +6 -0
- package/dist/output/text/remove.js +6 -0
- package/dist/output/text/save.js +6 -0
- package/dist/output/text/search.js +6 -0
- package/dist/output/text/show.js +6 -0
- package/dist/output/text/update.js +6 -0
- package/dist/output/text/upgrade.js +6 -0
- package/dist/output/text/vault.js +16 -0
- package/dist/output/text/wiki.js +15 -0
- package/dist/output/text/workflow.js +14 -0
- package/dist/output/text.js +44 -1329
- package/dist/registry/build-index.js +3 -0
- package/dist/registry/create-provider-registry.js +3 -0
- package/dist/registry/factory.js +4 -1
- package/dist/registry/origin-resolve.js +3 -0
- package/dist/registry/providers/index.js +3 -0
- package/dist/registry/providers/skills-sh.js +11 -2
- package/dist/registry/providers/static-index.js +10 -1
- package/dist/registry/providers/types.js +3 -24
- package/dist/registry/resolve.js +11 -16
- package/dist/registry/types.js +3 -0
- package/dist/scripts/migrate-storage.js +17767 -0
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
- package/dist/scripts/migrations/v16-to-v17.js +141 -0
- package/dist/setup/detect.js +3 -0
- package/dist/setup/ripgrep-install.js +3 -0
- package/dist/setup/ripgrep-resolve.js +3 -0
- package/dist/setup/setup.js +306 -67
- package/dist/setup/steps.js +3 -15
- package/dist/sources/include.js +3 -0
- package/dist/sources/provider-factory.js +3 -11
- package/dist/sources/provider.js +3 -20
- package/dist/sources/providers/filesystem.js +19 -23
- package/dist/sources/providers/git.js +171 -21
- package/dist/sources/providers/index.js +3 -0
- package/dist/sources/providers/install-types.js +3 -13
- package/dist/sources/providers/npm.js +3 -4
- package/dist/sources/providers/provider-utils.js +3 -0
- package/dist/sources/providers/sync-from-ref.js +3 -11
- package/dist/sources/providers/tar-utils.js +3 -0
- package/dist/sources/providers/website.js +18 -22
- package/dist/sources/resolve.js +3 -0
- package/dist/sources/types.js +3 -0
- package/dist/sources/website-ingest.js +3 -0
- package/dist/tasks/backends/cron.js +3 -0
- package/dist/tasks/backends/exec-utils.js +3 -0
- package/dist/tasks/backends/index.js +3 -11
- package/dist/tasks/backends/launchd.js +4 -1
- package/dist/tasks/backends/schtasks.js +4 -1
- package/dist/tasks/parser.js +51 -38
- package/dist/tasks/resolveAkmBin.js +3 -0
- package/dist/tasks/runner.js +35 -9
- package/dist/tasks/schedule.js +20 -1
- package/dist/tasks/schema.js +5 -3
- package/dist/tasks/validator.js +6 -3
- package/dist/version.js +3 -0
- package/dist/wiki/wiki-templates.js +6 -3
- package/dist/wiki/wiki.js +4 -1
- package/dist/workflows/authoring.js +4 -1
- package/dist/workflows/cli.js +3 -0
- package/dist/workflows/db.js +140 -10
- package/dist/workflows/document-cache.js +3 -10
- package/dist/workflows/parser.js +3 -0
- package/dist/workflows/renderer.js +3 -0
- package/dist/workflows/runs.js +18 -1
- package/dist/workflows/schema.js +3 -0
- package/dist/workflows/scope-key.js +3 -0
- package/dist/workflows/validator.js +5 -9
- package/docs/README.md +7 -2
- package/docs/data-and-telemetry.md +225 -0
- package/docs/migration/release-notes/0.7.5.md +2 -2
- package/docs/migration/release-notes/0.8.0.md +57 -5
- package/docs/migration/v0.7-to-v0.8.md +1378 -0
- package/package.json +28 -11
- package/.github/LICENSE +0 -374
- package/dist/commands/help/help-accept.md +0 -9
- package/dist/commands/help/help-improve.md +0 -53
- package/dist/commands/help/help-reject.md +0 -8
- package/dist/commands/install-audit.js +0 -385
- package/dist/commands/vault.js +0 -310
- package/dist/indexer/match-contributors.js +0 -141
- package/dist/integrations/agent/pipeline.js +0 -39
- package/dist/integrations/agent/runners.js +0 -31
- package/dist/llm/prompts/graph-extract-user-prompt.md +0 -12
- /package/dist/{tasks → assets}/backends/launchd-template.xml +0 -0
- /package/dist/{tasks → assets}/backends/schtasks-template.xml +0 -0
- /package/dist/{commands → assets}/help/help-propose.md +0 -0
- /package/dist/{wiki → assets/wiki}/index-template.md +0 -0
- /package/dist/{wiki → assets/wiki}/ingest-workflow-template.md +0 -0
- /package/dist/{wiki → assets/wiki}/log-template.md +0 -0
- /package/dist/{wiki → assets/wiki}/schema-template.md +0 -0
- /package/dist/{workflows → assets/workflows}/workflow-template.md +0 -0
|
@@ -0,0 +1,380 @@
|
|
|
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
|
+
* Shared content-quality validators consumed by the improve pipeline
|
|
6
|
+
* (`distill`, `consolidate`, `reflect`) and by the `proposal accept` gate.
|
|
7
|
+
*
|
|
8
|
+
* ## Reflect size gate — calibrated blended formula (2026-05-22)
|
|
9
|
+
*
|
|
10
|
+
* ### Distribution baseline (n=844 reflect-eligible stash assets)
|
|
11
|
+
*
|
|
12
|
+
* min=1 p10=371 p25=778 p50=1508 p75=5456 p90=11721 p99=43463 max=298010 bytes
|
|
13
|
+
* Buckets: <500=135 (16%), 500–2000=340 (40%), 2000–8000=222 (26%), >8000=147 (17%)
|
|
14
|
+
*
|
|
15
|
+
* ### Problem with the original fixed-ratio gate
|
|
16
|
+
*
|
|
17
|
+
* - Small sources (~420 bytes, 16th pct): the 200% expansion ceiling fires at
|
|
18
|
+
* only 840 bytes proposed — one good paragraph. Hair-trigger for a terse
|
|
19
|
+
* reference note.
|
|
20
|
+
* - Large sources (~7KB, 75th pct): 200% ceiling = 14KB; reasonable, but a hard
|
|
21
|
+
* cap prevents runaway expansion from LLM hallucinations.
|
|
22
|
+
*
|
|
23
|
+
* ### Blended-bound formula
|
|
24
|
+
*
|
|
25
|
+
* Shrinkage floor (accept if proposed >= lower):
|
|
26
|
+
* lower = max(REFLECT_SHRINK_RATIO_MIN * sourceLen, REFLECT_ABSOLUTE_FLOOR_BYTES)
|
|
27
|
+
* → For tiny sources (sourceLen < 300), the absolute floor dominates so a
|
|
28
|
+
* genuinely tightened note still passes.
|
|
29
|
+
* → For large sources (>1KB), the ratio floor dominates (50% of 7KB = 3.5KB).
|
|
30
|
+
*
|
|
31
|
+
* Expansion ceiling (accept if proposed <= upper):
|
|
32
|
+
* upper = max(REFLECT_EXPAND_RATIO_MAX * sourceLen, REFLECT_ABSOLUTE_CEILING_BYTES)
|
|
33
|
+
* …but always capped at REFLECT_ABSOLUTE_MAX_BYTES.
|
|
34
|
+
* → For small sources (≤778 bytes, p25), the absolute ceiling (2000 bytes)
|
|
35
|
+
* dominates — one substantive paragraph is always acceptable.
|
|
36
|
+
* → For medium/large sources (>1KB), the ratio ceiling dominates.
|
|
37
|
+
* → Any proposal exceeding 25000 bytes is always rejected regardless of ratio.
|
|
38
|
+
*
|
|
39
|
+
* ### Constant calibration rationale
|
|
40
|
+
*
|
|
41
|
+
* REFLECT_ABSOLUTE_FLOOR_BYTES = 150
|
|
42
|
+
* Half of p10 (371) ≈ 185; we set 150 so even very aggressive condensation
|
|
43
|
+
* of a seed note is allowed down to roughly a two-sentence summary.
|
|
44
|
+
*
|
|
45
|
+
* REFLECT_ABSOLUTE_CEILING_BYTES = 2500
|
|
46
|
+
* Raised from 2000 (2026-05-22): small-source rejections at 248–281% on
|
|
47
|
+
* 900–953 byte assets were borderline false positives. 2500 gives a short
|
|
48
|
+
* lesson or command ~1.5KB of room to grow before the absolute kicks in.
|
|
49
|
+
*
|
|
50
|
+
* REFLECT_ABSOLUTE_MAX_BYTES = 25000
|
|
51
|
+
* Below p99 (43463). Catches genuine LLM runaway (whole-chapter insertions)
|
|
52
|
+
* without blocking legitimate large rewrites of large sources.
|
|
53
|
+
*
|
|
54
|
+
* REFLECT_EXPAND_RATIO_MAX = 2.5
|
|
55
|
+
* Raised from 2.0 (2026-05-22): 2× was too tight for dense short assets
|
|
56
|
+
* (lessons, commands) that have legitimate room to grow. 2.5× resolves
|
|
57
|
+
* 248% expansion on a 900-byte lesson while still catching 281%+ on ~1KB
|
|
58
|
+
* assets where the absolute ceiling takes over.
|
|
59
|
+
*/
|
|
60
|
+
// ── Reflect-size guard ───────────────────────────────────────────────────────
|
|
61
|
+
import { parseFrontmatter } from "./frontmatter";
|
|
62
|
+
import { detectTruncatedDescription, TRUNCATION_TRAILING_WORDS } from "./text-truncation";
|
|
63
|
+
// ── Description / when_to_use shape ─────────────────────────────────────────
|
|
64
|
+
export const HEADING_FRAGMENT_PATTERNS = [
|
|
65
|
+
/^for example\b/i,
|
|
66
|
+
/^to reduce\b/i,
|
|
67
|
+
/^key (pitfalls|fixes|points|takeaways|considerations|steps|notes|tips|insights|features|benefits|risks)\b/i,
|
|
68
|
+
/^example[s]?$/i,
|
|
69
|
+
/^summary$/i,
|
|
70
|
+
/^overview$/i,
|
|
71
|
+
/^introduction$/i,
|
|
72
|
+
/^takeaways$/i,
|
|
73
|
+
/^conclusion$/i,
|
|
74
|
+
/^notes?$/i,
|
|
75
|
+
/^tips?$/i,
|
|
76
|
+
];
|
|
77
|
+
export function isValidDescription(value, inputRef, options = {}) {
|
|
78
|
+
if (typeof value !== "string")
|
|
79
|
+
return { ok: false, reason: "description is not a string" };
|
|
80
|
+
const v = value.trim();
|
|
81
|
+
if (!v)
|
|
82
|
+
return { ok: false, reason: "description is empty" };
|
|
83
|
+
if (v.length < 20)
|
|
84
|
+
return { ok: false, reason: `description is too short (${v.length} chars; need ≥20)` };
|
|
85
|
+
if (v.length > 400)
|
|
86
|
+
return { ok: false, reason: `description is too long (${v.length} chars; max 400)` };
|
|
87
|
+
if (/^\s*[\d#*\->`]/.test(v))
|
|
88
|
+
return { ok: false, reason: "description starts with a digit or markdown marker" };
|
|
89
|
+
const last = v.slice(-1);
|
|
90
|
+
if (last === ":" || last === ";" || last === ",")
|
|
91
|
+
return { ok: false, reason: `description ends with truncation indicator "${last}"` };
|
|
92
|
+
const lastWordMatch = v.match(/([A-Za-z']+)[.!?]*$/);
|
|
93
|
+
if (lastWordMatch) {
|
|
94
|
+
const lastWord = lastWordMatch[1].toLowerCase();
|
|
95
|
+
if (TRUNCATION_TRAILING_WORDS.has(lastWord))
|
|
96
|
+
return { ok: false, reason: `description ends with truncation-indicator word "${lastWord}"` };
|
|
97
|
+
}
|
|
98
|
+
if (/^lesson distilled from\b/i.test(v))
|
|
99
|
+
return { ok: false, reason: "description matches the auto-repair placeholder text" };
|
|
100
|
+
for (const re of HEADING_FRAGMENT_PATTERNS) {
|
|
101
|
+
if (re.test(v))
|
|
102
|
+
return { ok: false, reason: `description looks like a section heading: "${v.slice(0, 40)}"` };
|
|
103
|
+
}
|
|
104
|
+
if (/^(def|function|async\s+def|async\s+function|class|const|let|var|export\s+function|export\s+const|export\s+default|import|public|private|protected|fn|func)\s+\S/i.test(v)) {
|
|
105
|
+
const firstWord = v.split(/\s+/)[0] ?? "";
|
|
106
|
+
return {
|
|
107
|
+
ok: false,
|
|
108
|
+
reason: `description starts with code keyword "${firstWord}" — looks like a code fragment, not prose`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const backtickCount = (v.match(/`/g) ?? []).length;
|
|
112
|
+
if (backtickCount % 2 !== 0)
|
|
113
|
+
return {
|
|
114
|
+
ok: false,
|
|
115
|
+
reason: `description has ${backtickCount} backticks (unbalanced); likely contains a malformed code fragment`,
|
|
116
|
+
};
|
|
117
|
+
if (/^when\b/i.test(v))
|
|
118
|
+
return { ok: false, reason: "description starts with 'When' — that pattern belongs in when_to_use" };
|
|
119
|
+
if (!options.skipRefTailCheck) {
|
|
120
|
+
const refTail = inputRef.split(":").pop()?.toLowerCase() ?? "";
|
|
121
|
+
if (refTail.length >= 6 && v.toLowerCase().includes(refTail) && v.length < refTail.length + 40)
|
|
122
|
+
return { ok: false, reason: "description appears to just name the input ref" };
|
|
123
|
+
}
|
|
124
|
+
return { ok: true };
|
|
125
|
+
}
|
|
126
|
+
export function isValidWhenToUse(value, inputRef) {
|
|
127
|
+
if (typeof value !== "string")
|
|
128
|
+
return { ok: false, reason: "when_to_use is not a string" };
|
|
129
|
+
const v = value.trim();
|
|
130
|
+
if (!v)
|
|
131
|
+
return { ok: false, reason: "when_to_use is empty" };
|
|
132
|
+
if (v.length < 15)
|
|
133
|
+
return { ok: false, reason: `when_to_use is too short (${v.length} chars; need ≥15)` };
|
|
134
|
+
if (v.length > 400)
|
|
135
|
+
return { ok: false, reason: `when_to_use is too long (${v.length} chars; max 400)` };
|
|
136
|
+
if (/^when working with\b/i.test(v))
|
|
137
|
+
return { ok: false, reason: "when_to_use is the circular 'When working with ...' fallback" };
|
|
138
|
+
const refTail = inputRef.split(":").pop()?.toLowerCase() ?? "";
|
|
139
|
+
if (refTail.length >= 6 && v.toLowerCase().includes(refTail) && v.length < refTail.length + 25)
|
|
140
|
+
return { ok: false, reason: "when_to_use appears to just name the input ref" };
|
|
141
|
+
return { ok: true };
|
|
142
|
+
}
|
|
143
|
+
export function detectDoubleFrontmatter(content) {
|
|
144
|
+
const fenceLines = content.split(/\r?\n/).filter((l) => /^---\s*$/.test(l));
|
|
145
|
+
if (fenceLines.length > 2)
|
|
146
|
+
return {
|
|
147
|
+
kind: "double-frontmatter-fence",
|
|
148
|
+
message: `Content contains ${fenceLines.length} \`---\` fence lines; assets with frontmatter must have exactly 2 (one open, one close).`,
|
|
149
|
+
};
|
|
150
|
+
const body = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
|
|
151
|
+
const pseudoLine = body
|
|
152
|
+
.split(/\r?\n/)
|
|
153
|
+
.find((l) => /^\s*(\*\*|__)?\s*(description|when_to_use)\s*(\*\*|__)?\s*:/i.test(l));
|
|
154
|
+
if (pseudoLine)
|
|
155
|
+
return {
|
|
156
|
+
kind: "pseudo-frontmatter-in-body",
|
|
157
|
+
message: `Body contains a pseudo-frontmatter restatement: "${pseudoLine.slice(0, 80)}". Fields belong in YAML frontmatter only.`,
|
|
158
|
+
};
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
export function validateProposalFrontmatter(fm) {
|
|
162
|
+
const desc = fm.description;
|
|
163
|
+
if (typeof desc !== "string" || desc.trim().length === 0)
|
|
164
|
+
return { ok: false, reason: "MISSING_FRONTMATTER_DESCRIPTION" };
|
|
165
|
+
const truncReason = detectTruncatedDescription(desc);
|
|
166
|
+
if (truncReason)
|
|
167
|
+
return { ok: false, reason: `TRUNCATED_DESCRIPTION (${truncReason})` };
|
|
168
|
+
return { ok: true };
|
|
169
|
+
}
|
|
170
|
+
export function hasSupersededStatus(frontmatter) {
|
|
171
|
+
const status = frontmatter?.status;
|
|
172
|
+
return typeof status === "string" && status.trim().toLowerCase() === "superseded";
|
|
173
|
+
}
|
|
174
|
+
export function hasHotCaptureMode(frontmatter) {
|
|
175
|
+
return frontmatter?.captureMode === "hot";
|
|
176
|
+
}
|
|
177
|
+
// ── Consolidate merge size gate ──────────────────────────────────────────────
|
|
178
|
+
/**
|
|
179
|
+
* Ratio lower-bound for merged body vs. the larger source body.
|
|
180
|
+
* Lower than reflect (0.5) because deduplication is expected — two memories
|
|
181
|
+
* with 80-90% overlap legitimately compress to well under 50% of the larger.
|
|
182
|
+
*/
|
|
183
|
+
export const MERGE_SHRINK_RATIO_MIN = 0.3;
|
|
184
|
+
/**
|
|
185
|
+
* Absolute floor (chars) for merged body. When sources are short (<~333 chars),
|
|
186
|
+
* `MERGE_SHRINK_RATIO_MIN × largerBodyLen` falls below this and the absolute
|
|
187
|
+
* floor dominates — prevents false positives on very terse memory pairs.
|
|
188
|
+
* Matches the existing `promote_source_too_small` floor of 100 chars.
|
|
189
|
+
*/
|
|
190
|
+
export const MERGE_ABSOLUTE_FLOOR_CHARS = 100;
|
|
191
|
+
// ── Reflect size gate ────────────────────────────────────────────────────────
|
|
192
|
+
/** Ratio lower-bound: proposed body must be at least this fraction of source. */
|
|
193
|
+
export const REFLECT_SHRINK_RATIO_MIN = 0.5;
|
|
194
|
+
/** Ratio upper-bound: proposed body must not exceed this fraction of source. */
|
|
195
|
+
export const REFLECT_EXPAND_RATIO_MAX = 2.5;
|
|
196
|
+
/**
|
|
197
|
+
* Below this byte count, ratio checks are too noisy — skip them entirely.
|
|
198
|
+
* Unchanged from the original gate.
|
|
199
|
+
*/
|
|
200
|
+
export const REFLECT_SIZE_GUARD_MIN_BYTES = 200;
|
|
201
|
+
/**
|
|
202
|
+
* Absolute shrinkage floor (bytes). Even if `ratio * sourceLen` is lower, a
|
|
203
|
+
* proposed body of at least this many bytes is always accepted on the shrinkage
|
|
204
|
+
* side. Protects against false positives when the source is small (<300 bytes).
|
|
205
|
+
*/
|
|
206
|
+
export const REFLECT_ABSOLUTE_FLOOR_BYTES = 150;
|
|
207
|
+
/**
|
|
208
|
+
* Absolute expansion ceiling (bytes). Even if `ratio * sourceLen` is lower, a
|
|
209
|
+
* proposed body up to this many bytes is always accepted on the expansion side.
|
|
210
|
+
* Protects against false positives when the source is small (≤778 bytes, p25).
|
|
211
|
+
*/
|
|
212
|
+
export const REFLECT_ABSOLUTE_CEILING_BYTES = 2500;
|
|
213
|
+
/**
|
|
214
|
+
* Hard expansion cap (bytes). Regardless of ratio, a proposed body exceeding
|
|
215
|
+
* this limit is always rejected. Guards against runaway LLM hallucinations on
|
|
216
|
+
* large sources.
|
|
217
|
+
*/
|
|
218
|
+
export const REFLECT_ABSOLUTE_MAX_BYTES = 25000;
|
|
219
|
+
/**
|
|
220
|
+
* Calibrated size check: compare proposed body length against source body
|
|
221
|
+
* length using a blended-bound formula.
|
|
222
|
+
*
|
|
223
|
+
* **Shrinkage** — accept if:
|
|
224
|
+
* `proposedLen >= max(REFLECT_SHRINK_RATIO_MIN * sourceLen, REFLECT_ABSOLUTE_FLOOR_BYTES)`
|
|
225
|
+
*
|
|
226
|
+
* **Expansion** — accept if:
|
|
227
|
+
* `proposedLen <= min(max(REFLECT_EXPAND_RATIO_MAX * sourceLen, REFLECT_ABSOLUTE_CEILING_BYTES), REFLECT_ABSOLUTE_MAX_BYTES)`
|
|
228
|
+
*
|
|
229
|
+
* Returns `{ ok: true }` when:
|
|
230
|
+
* - `sourceBody` is absent or `undefined`
|
|
231
|
+
* - source body is shorter than {@link REFLECT_SIZE_GUARD_MIN_BYTES}
|
|
232
|
+
* - the proposed length is within the blended bounds
|
|
233
|
+
*/
|
|
234
|
+
export function checkReflectSize(sourceBody, proposedBody) {
|
|
235
|
+
if (typeof sourceBody !== "string")
|
|
236
|
+
return { ok: true };
|
|
237
|
+
const sourceLen = sourceBody.trim().length;
|
|
238
|
+
if (sourceLen < REFLECT_SIZE_GUARD_MIN_BYTES)
|
|
239
|
+
return { ok: true };
|
|
240
|
+
const proposedLen = proposedBody.trim().length;
|
|
241
|
+
const ratio = proposedLen / sourceLen;
|
|
242
|
+
// Shrinkage check: lower bound = max(ratio floor, absolute floor)
|
|
243
|
+
const shrinkFloor = Math.max(REFLECT_SHRINK_RATIO_MIN * sourceLen, REFLECT_ABSOLUTE_FLOOR_BYTES);
|
|
244
|
+
if (proposedLen < shrinkFloor) {
|
|
245
|
+
return { ok: false, code: "EXCESSIVE_SHRINKAGE", ratio };
|
|
246
|
+
}
|
|
247
|
+
// Expansion check: upper bound = min(max(ratio ceiling, absolute ceiling), hard cap)
|
|
248
|
+
const expandCeiling = Math.min(Math.max(REFLECT_EXPAND_RATIO_MAX * sourceLen, REFLECT_ABSOLUTE_CEILING_BYTES), REFLECT_ABSOLUTE_MAX_BYTES);
|
|
249
|
+
if (proposedLen > expandCeiling) {
|
|
250
|
+
return { ok: false, code: "EXCESSIVE_EXPANSION", ratio };
|
|
251
|
+
}
|
|
252
|
+
return { ok: true };
|
|
253
|
+
}
|
|
254
|
+
// ── ProposalValidator entries (registered with proposal-validators.ts) ──────
|
|
255
|
+
const descriptionQualityValidator = {
|
|
256
|
+
name: "description-quality",
|
|
257
|
+
appliesTo(_proposal, ctx) {
|
|
258
|
+
return ctx.parsedRef?.type === "knowledge" || ctx.parsedRef?.type === "memory" || ctx.parsedRef?.type === "lesson";
|
|
259
|
+
},
|
|
260
|
+
validate(proposal) {
|
|
261
|
+
if (typeof proposal.payload?.content !== "string" || proposal.payload.content.trim() === "")
|
|
262
|
+
return [];
|
|
263
|
+
let fm;
|
|
264
|
+
try {
|
|
265
|
+
fm = parseFrontmatter(proposal.payload.content).data;
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return [];
|
|
269
|
+
}
|
|
270
|
+
const check = validateProposalFrontmatter(fm);
|
|
271
|
+
if (check.ok)
|
|
272
|
+
return [];
|
|
273
|
+
return [
|
|
274
|
+
{
|
|
275
|
+
kind: "invalid-description",
|
|
276
|
+
message: `Proposal ${proposal.id} (${proposal.ref}) has an invalid description: ${check.reason}.`,
|
|
277
|
+
},
|
|
278
|
+
];
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
const lessonContentQualityValidator = {
|
|
282
|
+
name: "lesson-content-quality",
|
|
283
|
+
appliesTo(_proposal, ctx) {
|
|
284
|
+
return ctx.parsedRef?.type === "lesson";
|
|
285
|
+
},
|
|
286
|
+
validate(proposal) {
|
|
287
|
+
if (typeof proposal.payload?.content !== "string")
|
|
288
|
+
return [];
|
|
289
|
+
let fm;
|
|
290
|
+
try {
|
|
291
|
+
fm = parseFrontmatter(proposal.payload.content).data;
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
return [];
|
|
295
|
+
}
|
|
296
|
+
const findings = [];
|
|
297
|
+
const descCheck = isValidDescription(fm.description, proposal.ref);
|
|
298
|
+
if (!descCheck.ok)
|
|
299
|
+
findings.push({
|
|
300
|
+
kind: "invalid-description",
|
|
301
|
+
message: `Lesson proposal ${proposal.id} (${proposal.ref}) has an invalid description: ${descCheck.reason}.`,
|
|
302
|
+
});
|
|
303
|
+
const wtuCheck = isValidWhenToUse(fm.when_to_use, proposal.ref);
|
|
304
|
+
if (!wtuCheck.ok)
|
|
305
|
+
findings.push({
|
|
306
|
+
kind: "invalid-when_to_use",
|
|
307
|
+
message: `Lesson proposal ${proposal.id} (${proposal.ref}) has an invalid when_to_use: ${wtuCheck.reason}.`,
|
|
308
|
+
});
|
|
309
|
+
if (descCheck.ok &&
|
|
310
|
+
wtuCheck.ok &&
|
|
311
|
+
typeof fm.description === "string" &&
|
|
312
|
+
typeof fm.when_to_use === "string" &&
|
|
313
|
+
fm.description.trim().toLowerCase() === fm.when_to_use.trim().toLowerCase()) {
|
|
314
|
+
findings.push({
|
|
315
|
+
kind: "description-equals-when_to_use",
|
|
316
|
+
message: `Lesson proposal ${proposal.id} (${proposal.ref}) has identical description and when_to_use.`,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
const dfm = detectDoubleFrontmatter(proposal.payload.content);
|
|
320
|
+
if (dfm)
|
|
321
|
+
findings.push({ kind: dfm.kind, message: `Lesson proposal ${proposal.id} (${proposal.ref}): ${dfm.message}` });
|
|
322
|
+
return findings;
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
const sourceNotSupersededValidator = {
|
|
326
|
+
name: "source-not-superseded",
|
|
327
|
+
appliesTo(proposal, ctx) {
|
|
328
|
+
return proposal.source === "consolidate" && !!ctx.source?.frontmatter;
|
|
329
|
+
},
|
|
330
|
+
validate(proposal, ctx) {
|
|
331
|
+
if (hasSupersededStatus(ctx.source?.frontmatter)) {
|
|
332
|
+
return [
|
|
333
|
+
{
|
|
334
|
+
kind: "source-superseded",
|
|
335
|
+
message: `Proposal ${proposal.id} (${proposal.ref}) has a source asset marked status:superseded; superseded memories are not promotable knowledge.`,
|
|
336
|
+
},
|
|
337
|
+
];
|
|
338
|
+
}
|
|
339
|
+
return [];
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
/** Strip an opening frontmatter block (`---\n…\n---`) from `content`, returning the body. */
|
|
343
|
+
function stripFrontmatterBody(content) {
|
|
344
|
+
return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
|
|
345
|
+
}
|
|
346
|
+
const reflectSizeGuardValidator = {
|
|
347
|
+
name: "reflect-size-guard",
|
|
348
|
+
appliesTo(proposal, ctx) {
|
|
349
|
+
return proposal.source === "reflect" && typeof ctx.source?.content === "string";
|
|
350
|
+
},
|
|
351
|
+
validate(proposal, ctx) {
|
|
352
|
+
const sourceBody = stripFrontmatterBody(ctx.source?.content ?? "");
|
|
353
|
+
const proposedBody = typeof proposal.payload?.content === "string" ? stripFrontmatterBody(proposal.payload.content) : "";
|
|
354
|
+
const outcome = checkReflectSize(sourceBody, proposedBody);
|
|
355
|
+
if (outcome.ok)
|
|
356
|
+
return [];
|
|
357
|
+
const pct = (outcome.ratio * 100).toFixed(0);
|
|
358
|
+
const limit = outcome.code === "EXCESSIVE_SHRINKAGE" ? "minimum 50%" : "maximum 250%";
|
|
359
|
+
const cause = outcome.code === "EXCESSIVE_SHRINKAGE"
|
|
360
|
+
? "Concrete content was likely deleted."
|
|
361
|
+
: "Speculative material was likely added.";
|
|
362
|
+
return [
|
|
363
|
+
{
|
|
364
|
+
kind: outcome.code.toLowerCase(),
|
|
365
|
+
message: `Reflect rejected: ${outcome.code} — proposed body is ${pct}% of source (${limit}) for ref ${proposal.ref}. ${cause}`,
|
|
366
|
+
},
|
|
367
|
+
];
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
/**
|
|
371
|
+
* Full set of quality validators in registration order. Appended onto
|
|
372
|
+
* {@link defaultProposalValidators} so they run inside `validateProposal` on
|
|
373
|
+
* `proposal accept` automatically.
|
|
374
|
+
*/
|
|
375
|
+
export const defaultProposalQualityValidators = [
|
|
376
|
+
descriptionQualityValidator,
|
|
377
|
+
lessonContentQualityValidator,
|
|
378
|
+
sourceNotSupersededValidator,
|
|
379
|
+
reflectSizeGuardValidator,
|
|
380
|
+
];
|
|
@@ -1,6 +1,10 @@
|
|
|
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/.
|
|
1
4
|
import { parseAssetRef } from "./asset-ref";
|
|
2
5
|
import { parseFrontmatter } from "./frontmatter";
|
|
3
6
|
import { lintLessonContent } from "./lesson-lint";
|
|
7
|
+
import { defaultProposalQualityValidators } from "./proposal-quality-validators";
|
|
4
8
|
const genericProposalValidator = {
|
|
5
9
|
name: "generic-proposal-validator",
|
|
6
10
|
appliesTo: () => true,
|
|
@@ -46,10 +50,14 @@ const lessonProposalValidator = {
|
|
|
46
50
|
}));
|
|
47
51
|
},
|
|
48
52
|
};
|
|
49
|
-
export const defaultProposalValidators = [
|
|
50
|
-
|
|
53
|
+
export const defaultProposalValidators = [
|
|
54
|
+
genericProposalValidator,
|
|
55
|
+
lessonProposalValidator,
|
|
56
|
+
...defaultProposalQualityValidators,
|
|
57
|
+
];
|
|
58
|
+
export function runProposalValidators(proposal, validators = defaultProposalValidators, initialContext = {}) {
|
|
51
59
|
const findings = [];
|
|
52
|
-
const ctx = {};
|
|
60
|
+
const ctx = { ...initialContext };
|
|
53
61
|
for (const validator of validators) {
|
|
54
62
|
if (!validator.appliesTo(proposal, ctx))
|
|
55
63
|
continue;
|