akm-cli 0.9.0-beta.5 → 0.9.0-beta.51
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 +711 -0
- package/README.md +12 -4
- package/dist/akm +38 -0
- package/dist/akm-migrate-storage +38 -0
- 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/quick.json +1 -1
- 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 +6 -2
- 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 +38 -10
- package/dist/cli/parse-args.js +46 -1
- package/dist/cli/shared.js +28 -0
- package/dist/cli.js +27 -11
- package/dist/commands/agent/agent-dispatch.js +2 -2
- package/dist/commands/agent/agent-support.js +0 -7
- package/dist/commands/agent/contribute-cli.js +17 -4
- package/dist/commands/config-cli.js +18 -2
- package/dist/commands/env/child-env.js +47 -0
- package/dist/commands/env/env-cli.js +33 -26
- package/dist/commands/env/secret-cli.js +36 -22
- package/dist/commands/feedback-cli.js +15 -6
- package/dist/commands/graph/graph-cli.js +5 -13
- package/dist/commands/graph/graph.js +76 -72
- package/dist/commands/health/checks.js +49 -1
- package/dist/commands/health/html-report.js +422 -80
- package/dist/commands/health.js +386 -9
- package/dist/commands/improve/calibration.js +161 -0
- package/dist/commands/improve/consolidate/chunking.js +141 -0
- package/dist/commands/improve/consolidate/eligibility.js +81 -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 +635 -660
- package/dist/commands/improve/dedup.js +482 -0
- package/dist/commands/improve/distill.js +159 -69
- package/dist/commands/improve/eligibility.js +434 -0
- package/dist/commands/improve/encoding-salience.js +205 -0
- package/dist/commands/improve/extract-cli.js +124 -2
- package/dist/commands/improve/extract-prompt.js +39 -2
- package/dist/commands/improve/extract-watch.js +140 -0
- package/dist/commands/improve/extract.js +389 -40
- package/dist/commands/improve/feedback-valence.js +54 -0
- package/dist/commands/improve/homeostatic.js +467 -0
- package/dist/commands/improve/improve-auto-accept.js +138 -7
- package/dist/commands/improve/improve-cli.js +36 -61
- package/dist/commands/improve/improve-profiles.js +14 -0
- package/dist/commands/improve/improve-result-file.js +14 -25
- package/dist/commands/improve/improve-session.js +58 -0
- package/dist/commands/improve/improve.js +485 -2498
- package/dist/commands/improve/locks.js +154 -0
- package/dist/commands/improve/loop-stages.js +1083 -0
- package/dist/commands/improve/memory/memory-contradiction-detect.js +23 -28
- package/dist/commands/improve/outcome-loop.js +256 -0
- package/dist/commands/improve/preparation.js +1966 -0
- package/dist/commands/improve/proactive-maintenance.js +115 -0
- package/dist/commands/improve/procedural.js +418 -0
- package/dist/commands/improve/recombine.js +850 -0
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/improve/reflect.js +183 -40
- package/dist/commands/improve/salience.js +438 -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/proposal/drain-policies.js +5 -0
- package/dist/commands/proposal/drain.js +43 -50
- package/dist/commands/proposal/proposal-cli.js +21 -31
- package/dist/commands/proposal/proposal.js +5 -0
- package/dist/commands/proposal/propose.js +7 -2
- package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
- package/dist/commands/proposal/validators/proposals.js +189 -63
- package/dist/commands/read/curate.js +414 -94
- package/dist/commands/read/knowledge.js +6 -3
- package/dist/commands/read/search-cli.js +9 -4
- package/dist/commands/read/search.js +10 -6
- package/dist/commands/read/show.js +86 -7
- package/dist/commands/sources/init.js +49 -17
- package/dist/commands/sources/installed-stashes.js +11 -3
- package/dist/commands/sources/schema-repair.js +43 -45
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/source-add.js +7 -3
- package/dist/commands/sources/stash-cli.js +28 -40
- package/dist/commands/sources/stash-skeleton.js +23 -8
- package/dist/commands/tasks/tasks-cli.js +19 -27
- package/dist/commands/tasks/tasks.js +39 -11
- package/dist/commands/wiki-cli.js +21 -35
- package/dist/core/asset/asset-registry.js +3 -1
- package/dist/core/asset/asset-spec.js +18 -2
- package/dist/core/asset/frontmatter.js +166 -167
- package/dist/core/asset/markdown.js +8 -0
- package/dist/core/authoring-rules.js +92 -0
- package/dist/core/common.js +0 -5
- package/dist/core/config/config-migration.js +12 -11
- package/dist/core/config/config-schema.js +340 -56
- package/dist/core/config/config-types.js +3 -3
- package/dist/core/config/config.js +28 -7
- package/dist/core/events.js +3 -7
- package/dist/core/improve-types.js +11 -8
- package/dist/core/logs-db.js +10 -66
- package/dist/core/parse.js +36 -16
- package/dist/core/paths.js +3 -0
- 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 +714 -0
- package/dist/core/state-db.js +525 -474
- package/dist/indexer/db/db.js +439 -247
- package/dist/indexer/db/graph-db.js +129 -86
- package/dist/indexer/ensure-index.js +152 -17
- package/dist/indexer/graph/graph-boost.js +51 -41
- package/dist/indexer/graph/graph-extraction.js +218 -4
- package/dist/indexer/index-writer-lock.js +99 -0
- package/dist/indexer/indexer.js +123 -221
- package/dist/indexer/passes/dir-staleness.js +114 -0
- package/dist/indexer/passes/memory-inference.js +13 -5
- package/dist/indexer/passes/staleness-detect.js +2 -5
- package/dist/indexer/search/db-search.js +19 -6
- package/dist/indexer/search/ranking-contributors.js +22 -0
- package/dist/indexer/search/ranking.js +4 -0
- package/dist/indexer/search/search-source.js +17 -18
- package/dist/indexer/search/semantic-status.js +4 -0
- package/dist/indexer/walk/matchers.js +9 -0
- package/dist/integrations/agent/config.js +6 -53
- package/dist/integrations/agent/index.js +2 -18
- package/dist/integrations/agent/prompts.js +75 -9
- package/dist/integrations/agent/runner-dispatch.js +59 -0
- package/dist/integrations/harnesses/claude/session-log.js +11 -1
- package/dist/integrations/harnesses/index.js +2 -3
- 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 +0 -2
- package/dist/integrations/session-logs/index.js +16 -0
- package/dist/llm/client.js +45 -15
- package/dist/llm/embedder.js +42 -3
- package/dist/llm/embedders/deterministic.js +66 -0
- package/dist/llm/embedders/local.js +66 -2
- package/dist/llm/feature-gate.js +8 -4
- 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 +74 -2
- package/dist/output/shapes/curate.js +14 -2
- package/dist/output/shapes/passthrough.js +0 -1
- package/dist/output/text/helpers.js +16 -1
- package/dist/registry/providers/skills-sh.js +21 -147
- package/dist/registry/providers/static-index.js +15 -157
- package/dist/registry/resolve.js +22 -9
- package/dist/runtime.js +25 -1
- package/dist/scripts/migrate-storage.js +2617 -1961
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +759 -510
- package/dist/setup/setup.js +29 -8
- 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/tar-utils.js +16 -8
- 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/registry-cache.js +92 -0
- package/dist/storage/sqlite-pragmas.js +146 -0
- package/dist/tasks/backends/cron.js +1 -1
- 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 +5 -13
- package/dist/text-import-hook.mjs +0 -0
- package/dist/wiki/wiki.js +37 -0
- package/dist/workflows/db.js +3 -4
- package/dist/workflows/runtime/runs.js +1 -117
- package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/data-and-telemetry.md +3 -2
- package/docs/migration/release-notes/0.9.0.md +39 -0
- package/package.json +13 -11
- package/dist/commands/db-cli.js +0 -23
- package/dist/indexer/db/db-backup.js +0 -376
|
@@ -0,0 +1,231 @@
|
|
|
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
|
+
// ── LLM-output sanitization ─────────────────────────────────────────────────
|
|
5
|
+
//
|
|
6
|
+
// Pure string/frontmatter transforms that defend against three classes of LLM
|
|
7
|
+
// defect observed across hundreds of consolidate proposals:
|
|
8
|
+
//
|
|
9
|
+
// 1. Code-fence leakage: the entire merged asset is wrapped in
|
|
10
|
+
// ```markdown … ``` (or ```yaml … ```) despite the prompt forbidding
|
|
11
|
+
// fences. The post-processor used to pass this through verbatim, so the
|
|
12
|
+
// first character of the asset content became a backtick rather than
|
|
13
|
+
// `---`, defeating the frontmatter parser.
|
|
14
|
+
// 2. YAML quote-escaping bugs: descriptions like `'"Specialty intro...:`
|
|
15
|
+
// with unbalanced quotes that break the YAML reader. The post-processor
|
|
16
|
+
// historically passed the LLM's raw scalar straight into a manually
|
|
17
|
+
// assembled `description: <raw>` line.
|
|
18
|
+
// 3. Truncated descriptions hitting token cutoffs — the model's max_tokens
|
|
19
|
+
// runs out mid-sentence, leaving things like
|
|
20
|
+
// `description: "Tables in narrow column containers need max-width:100% +"`
|
|
21
|
+
// with no closing context.
|
|
22
|
+
//
|
|
23
|
+
// `sanitizeMergedContent` and `validateProposalFrontmatter` defend against
|
|
24
|
+
// all three at the point where LLM output is consumed.
|
|
25
|
+
import { parse as yamlParse } from "yaml";
|
|
26
|
+
import { assembleAssetFromString, serializeFrontmatter } from "../../../core/asset/asset-serialize.js";
|
|
27
|
+
import { parseFrontmatter } from "../../../core/asset/frontmatter.js";
|
|
28
|
+
/**
|
|
29
|
+
* Attempt to recover a frontmatter block that is missing its closing `---`.
|
|
30
|
+
*
|
|
31
|
+
* Scans lines after the opening `---` for the first blank line or the first
|
|
32
|
+
* line that cannot be a YAML scalar (i.e. not a key-value, indented
|
|
33
|
+
* continuation, comment, or list item). Injects `---` before that line so
|
|
34
|
+
* the normal parser can proceed.
|
|
35
|
+
*
|
|
36
|
+
* Returns the patched string on success, or `null` if the structure is too
|
|
37
|
+
* ambiguous to recover safely (e.g. no opening `---`, or no body content
|
|
38
|
+
* found after the frontmatter key-value lines).
|
|
39
|
+
*/
|
|
40
|
+
function recoverMalformedFrontmatter(raw) {
|
|
41
|
+
if (!raw.startsWith("---"))
|
|
42
|
+
return null;
|
|
43
|
+
const lines = raw.split(/\r?\n/);
|
|
44
|
+
// Skip the opening `---` line (index 0).
|
|
45
|
+
let insertAt = -1;
|
|
46
|
+
for (let i = 1; i < lines.length; i++) {
|
|
47
|
+
const line = lines[i];
|
|
48
|
+
// A blank line marks the end of the frontmatter block in many YAML variants.
|
|
49
|
+
if (line.trim() === "") {
|
|
50
|
+
insertAt = i;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
// A line that is clearly body content: doesn't look like a YAML key, an
|
|
54
|
+
// indented continuation, a comment, or a sequence item.
|
|
55
|
+
const isYaml = /^\w[\w-]*\s*:/.test(line) || // key: value
|
|
56
|
+
/^\s+\S/.test(line) || // indented continuation / nested
|
|
57
|
+
/^\s*#/.test(line) || // YAML comment
|
|
58
|
+
/^\s*-\s/.test(line); // sequence item
|
|
59
|
+
if (!isYaml) {
|
|
60
|
+
insertAt = i;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (insertAt < 0)
|
|
65
|
+
return null;
|
|
66
|
+
const result = [...lines.slice(0, insertAt), "---", ...lines.slice(insertAt)].join("\n");
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Outer-fence stripper specific to consolidate. Unlike the shared
|
|
71
|
+
* `stripMarkdownFences` helper (which only handles markdown fences), this
|
|
72
|
+
* variant additionally recognises `yaml` and bare-language fences and refuses
|
|
73
|
+
* to strip an unbalanced fence — i.e. a leading ``` with no trailing ``` is
|
|
74
|
+
* treated as a malformed response, not partially sanitized.
|
|
75
|
+
*
|
|
76
|
+
* Returns `null` when only one half of a fence pair is present (caller
|
|
77
|
+
* should reject the response entirely).
|
|
78
|
+
*/
|
|
79
|
+
export function stripOuterCodeFence(raw) {
|
|
80
|
+
const trimmed = raw.trim();
|
|
81
|
+
const leading = trimmed.match(/^```(?:markdown|md|yaml|yml)?\s*\r?\n/i);
|
|
82
|
+
const trailing = trimmed.match(/\r?\n```\s*$/);
|
|
83
|
+
if (!leading && !trailing)
|
|
84
|
+
return { content: trimmed, stripped: false };
|
|
85
|
+
if (!leading || !trailing)
|
|
86
|
+
return null; // unbalanced — refuse
|
|
87
|
+
const inner = trimmed.slice(leading[0].length, trimmed.length - trailing[0].length).trim();
|
|
88
|
+
return { content: inner, stripped: true };
|
|
89
|
+
}
|
|
90
|
+
export function sanitizeMergedContent(raw) {
|
|
91
|
+
// Step 1: Strip outer code fence.
|
|
92
|
+
// Recovery path: if only the leading fence is present, strip it and continue
|
|
93
|
+
// provided the inner content starts with `---`. Trailing-only fences are NOT
|
|
94
|
+
// recovered — a trailing ``` is more likely a body code block than a forgotten
|
|
95
|
+
// wrapper, so recovering would silently corrupt the body.
|
|
96
|
+
let body;
|
|
97
|
+
{
|
|
98
|
+
const fenceResult = stripOuterCodeFence(raw);
|
|
99
|
+
if (fenceResult) {
|
|
100
|
+
body = fenceResult.content;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
const trimmed = raw.trim();
|
|
104
|
+
const leadingMatch = trimmed.match(/^```(?:markdown|md|yaml|yml)?\s*\r?\n([\s\S]*)$/i);
|
|
105
|
+
const inner = leadingMatch ? leadingMatch[1].trim() : null;
|
|
106
|
+
if (!inner?.startsWith("---")) {
|
|
107
|
+
return { ok: false, reason: "UNBALANCED_CODE_FENCE" };
|
|
108
|
+
}
|
|
109
|
+
body = inner;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Strip <think> blocks (some local models still emit them despite system prompts).
|
|
113
|
+
body = body.replace(/<think>[\s\S]*?<\/think>/gi, "").trim();
|
|
114
|
+
// Step 2: Verify frontmatter sentinel.
|
|
115
|
+
// Recovery path: LLM sometimes emits 1-2 lines of preamble (e.g. "Here is the
|
|
116
|
+
// merged content:") before the `---`. Accept if `---` appears within 300 chars.
|
|
117
|
+
// Beyond that it's more likely a body section divider, not a frontmatter start.
|
|
118
|
+
if (!body.startsWith("---")) {
|
|
119
|
+
const nlIdx = body.indexOf("\n---");
|
|
120
|
+
if (nlIdx >= 0 && nlIdx < 300) {
|
|
121
|
+
body = body.slice(nlIdx + 1);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
return { ok: false, reason: "MISSING_FRONTMATTER_SENTINEL" };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Extract frontmatter block.
|
|
128
|
+
// Recovery path: LLM sometimes omits the closing `---` delimiter. Detect this
|
|
129
|
+
// by scanning lines after the opening `---` for the first blank line or the
|
|
130
|
+
// first line that isn't a YAML key-value pair, then inject `---` there.
|
|
131
|
+
let match = body.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r\n|\r|\n|$)([\s\S]*)$/);
|
|
132
|
+
if (!match) {
|
|
133
|
+
const recovered = recoverMalformedFrontmatter(body);
|
|
134
|
+
if (recovered) {
|
|
135
|
+
match = recovered.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r\n|\r|\n|$)([\s\S]*)$/);
|
|
136
|
+
}
|
|
137
|
+
if (!match) {
|
|
138
|
+
return { ok: false, reason: "MALFORMED_FRONTMATTER_BLOCK" };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Re-parse via the yaml library so any quote-escaping mistakes either get
|
|
142
|
+
// normalised or surface as a parse error we can reject.
|
|
143
|
+
// Recovery: if the strict yaml library fails, fall back to the lenient
|
|
144
|
+
// hand-rolled parseFrontmatter parser, which tolerates common LLM YAML
|
|
145
|
+
// quirks (unescaped special chars, bare scalars, etc.). If it recovers
|
|
146
|
+
// at least one key, proceed — serializeFrontmatter below will re-serialize
|
|
147
|
+
// cleanly. Only reject if both parsers fail to extract any data.
|
|
148
|
+
let parsedFm;
|
|
149
|
+
try {
|
|
150
|
+
parsedFm = yamlParse(match[1]);
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
const fallback = parseFrontmatter(`---\n${match[1]}\n---\n${match[2]}`);
|
|
154
|
+
if (fallback.frontmatter !== null && Object.keys(fallback.data).length > 0) {
|
|
155
|
+
parsedFm = fallback.data;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
return { ok: false, reason: `INVALID_YAML: ${e instanceof Error ? e.message : String(e)}` };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (parsedFm === null || typeof parsedFm !== "object" || Array.isArray(parsedFm)) {
|
|
162
|
+
return { ok: false, reason: "FRONTMATTER_NOT_OBJECT" };
|
|
163
|
+
}
|
|
164
|
+
const fm = parsedFm;
|
|
165
|
+
// Normalise placeholder leaks like `updated: today`, `updated: {today: null}`,
|
|
166
|
+
// `updated: now`, etc. The consolidate prompt instructs the LLM not to emit
|
|
167
|
+
// these, but small models still do. Replace any such leak with today's ISO
|
|
168
|
+
// date OR drop the field if we can't safely normalise it.
|
|
169
|
+
normalizeUpdatedField(fm);
|
|
170
|
+
// Re-serialise via yaml.stringify to fix any quoting quirks.
|
|
171
|
+
let serialized;
|
|
172
|
+
try {
|
|
173
|
+
serialized = serializeFrontmatter(fm);
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
return { ok: false, reason: `YAML_STRINGIFY_FAILED: ${e instanceof Error ? e.message : String(e)}` };
|
|
177
|
+
}
|
|
178
|
+
const cleaned = assembleAssetFromString(serialized, match[2]);
|
|
179
|
+
return { ok: true, result: { content: cleaned, frontmatter: fm } };
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Mutate `fm.updated` in place to normalise placeholder leaks emitted by the
|
|
183
|
+
* LLM. The consolidate prompt forbids these, but small models still produce
|
|
184
|
+
* literal `today` / `{today: null}` / `now` values.
|
|
185
|
+
*
|
|
186
|
+
* Rules:
|
|
187
|
+
* - A real ISO-style date string (YYYY-MM-DD, optionally with time) stays as-is.
|
|
188
|
+
* - A Date object (some YAML parsers materialise dates) is converted to its
|
|
189
|
+
* ISO yyyy-mm-dd form.
|
|
190
|
+
* - A placeholder string ("today", "now", "{today}", "${today}", template
|
|
191
|
+
* variables) is replaced with today's ISO date.
|
|
192
|
+
* - A map/object (e.g. `{today: null}`) is replaced with today's ISO date.
|
|
193
|
+
* - `null`, empty string, missing → left alone (no field added; reviewers
|
|
194
|
+
* should not silently gain metadata they didn't write).
|
|
195
|
+
*
|
|
196
|
+
* Exported for unit testing.
|
|
197
|
+
*/
|
|
198
|
+
export function normalizeUpdatedField(fm) {
|
|
199
|
+
if (!("updated" in fm))
|
|
200
|
+
return;
|
|
201
|
+
const v = fm.updated;
|
|
202
|
+
if (v === null || v === undefined || v === "")
|
|
203
|
+
return;
|
|
204
|
+
const todayIso = new Date().toISOString().slice(0, 10);
|
|
205
|
+
if (v instanceof Date) {
|
|
206
|
+
fm.updated = v.toISOString().slice(0, 10);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (typeof v === "string") {
|
|
210
|
+
const trimmed = v.trim().toLowerCase();
|
|
211
|
+
if (/^\d{4}-\d{2}-\d{2}/.test(v.trim()))
|
|
212
|
+
return; // already a real date
|
|
213
|
+
if (trimmed === "today" ||
|
|
214
|
+
trimmed === "now" ||
|
|
215
|
+
trimmed === "{today}" ||
|
|
216
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: matches the literal user-typed placeholder text "${today}" so we can normalize it to today's ISO date
|
|
217
|
+
trimmed === "${today}" ||
|
|
218
|
+
trimmed === "{{today}}" ||
|
|
219
|
+
/^\{?\s*today\s*\}?$/.test(trimmed)) {
|
|
220
|
+
fm.updated = todayIso;
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
// Unknown string format — leave alone so it's visible in the diff.
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (typeof v === "object") {
|
|
227
|
+
// Maps like `{today: null}`, `{now: null}` — clearly a template leak.
|
|
228
|
+
fm.updated = todayIso;
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
}
|