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
|
@@ -221,7 +221,18 @@ function maybeAutoMigrateConfigFile(configPath, text) {
|
|
|
221
221
|
catch {
|
|
222
222
|
return text; // Malformed JSON — let parseAndValidate surface the error.
|
|
223
223
|
}
|
|
224
|
-
|
|
224
|
+
// Downgrade protection. Skip migration when the on-disk config is NEWER than
|
|
225
|
+
// this binary (=== 1), OR when it carries a configVersion we cannot order
|
|
226
|
+
// against ours (compareConfigVersion returns undefined for an unparseable
|
|
227
|
+
// value — e.g. one written by a newer/foreign akm). Migrating such a config
|
|
228
|
+
// could strip fields a newer binary added, a cross-version data-loss path.
|
|
229
|
+
//
|
|
230
|
+
// A MISSING configVersion (absent → undefined) is a legacy pre-versioning
|
|
231
|
+
// config that MUST still migrate, so the unparseable-skip is gated on the
|
|
232
|
+
// field being PRESENT (`onDiskVersion !== undefined`).
|
|
233
|
+
const onDiskVersion = obj.configVersion;
|
|
234
|
+
const versionOrder = compareConfigVersion(onDiskVersion, CURRENT_CONFIG_VERSION);
|
|
235
|
+
if (versionOrder === 1 || (onDiskVersion !== undefined && versionOrder === undefined)) {
|
|
225
236
|
return text;
|
|
226
237
|
}
|
|
227
238
|
const { changed, result } = migrateConfigShape(obj);
|
|
@@ -249,8 +260,8 @@ function maybeAutoMigrateConfigFile(configPath, text) {
|
|
|
249
260
|
" to preview a dry-run diff: akm config migrate --dry-run --print-diff",
|
|
250
261
|
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
|
|
251
262
|
].join("\n");
|
|
252
|
-
process.stderr
|
|
253
|
-
process.stdout
|
|
263
|
+
process.stderr?.write?.(`${banner}\n`);
|
|
264
|
+
process.stdout?.write?.(`${banner}\n`);
|
|
254
265
|
}
|
|
255
266
|
catch (err) {
|
|
256
267
|
// #461: never return migrated bytes when disk write fails — that triggers
|
|
@@ -273,9 +284,14 @@ export function saveConfig(config) {
|
|
|
273
284
|
const dir = path.dirname(configPath);
|
|
274
285
|
fs.mkdirSync(dir, { recursive: true });
|
|
275
286
|
const sanitized = sanitizeConfigForWrite(config);
|
|
276
|
-
// Final validation gate before bytes hit disk.
|
|
277
|
-
//
|
|
278
|
-
//
|
|
287
|
+
// Final validation gate before bytes hit disk. Runs the FULL schema —
|
|
288
|
+
// including the cross-field superRefine guards (removed `feedbackDistillation`
|
|
289
|
+
// process key, `defaultWriteTarget` resolution, writable npm/website sources)
|
|
290
|
+
// and all type/enum/range checks — so an `akm config set` (leaf OR object
|
|
291
|
+
// form) cannot persist a guard-violating or mistyped value. NOTE: unknown
|
|
292
|
+
// keys are intentionally NOT rejected here — object schemas are `.passthrough()`
|
|
293
|
+
// so cross-version skew round-trips (see config-schema.ts header); the lenient
|
|
294
|
+
// tolerance is by design, not an oversight.
|
|
279
295
|
const parseResult = AkmConfigSchema.safeParse(sanitized);
|
|
280
296
|
if (!parseResult.success) {
|
|
281
297
|
const lines = parseResult.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
|
|
@@ -450,7 +466,12 @@ function applyRuntimeEnvApiKeys(config) {
|
|
|
450
466
|
}
|
|
451
467
|
// LLM profile keys: AKM_LLM_API_KEY for the default profile, then
|
|
452
468
|
// AKM_PROFILE_<UPPER>_API_KEY for any profile (per-profile wins).
|
|
453
|
-
|
|
469
|
+
// Resolve the default profile the SAME way the rest of the config layer does
|
|
470
|
+
// (resolveDefaultLlmProfileName), so the implicit `profiles.llm.default`
|
|
471
|
+
// fallback is honored. Keying off the raw `defaults.llm` field alone silently
|
|
472
|
+
// dropped AKM_LLM_API_KEY for configs that rely on the implicit default —
|
|
473
|
+
// the same no-op-run class as the 2026-05-23 incident.
|
|
474
|
+
const defaultProfile = resolveDefaultLlmProfileName(next);
|
|
454
475
|
if (next.profiles?.llm) {
|
|
455
476
|
const updated = { ...next.profiles.llm };
|
|
456
477
|
let changed = false;
|
package/dist/core/events.js
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
import path from "node:path";
|
|
28
28
|
import { rethrowIfTestIsolationError } from "./errors.js";
|
|
29
29
|
import { getDataDir } from "./paths.js";
|
|
30
|
-
import { insertEvent, openStateDatabase, readStateEvents } from "./state-db.js";
|
|
30
|
+
import { insertEvent, openStateDatabase, readStateEvents, withStateDb } from "./state-db.js";
|
|
31
31
|
import { error } from "./warn.js";
|
|
32
32
|
/**
|
|
33
33
|
* Legacy events.jsonl path — used only by the migration script
|
|
@@ -82,18 +82,14 @@ export function appendEvent(input, ctx) {
|
|
|
82
82
|
// Default path: open, insert, close.
|
|
83
83
|
const dbPath = resolveDbPath(ctx);
|
|
84
84
|
try {
|
|
85
|
-
|
|
86
|
-
try {
|
|
85
|
+
withStateDb((db) => {
|
|
87
86
|
insertEvent(db, {
|
|
88
87
|
eventType: input.eventType,
|
|
89
88
|
ts,
|
|
90
89
|
ref: input.ref,
|
|
91
90
|
metadata: input.metadata,
|
|
92
91
|
});
|
|
93
|
-
}
|
|
94
|
-
finally {
|
|
95
|
-
db.close();
|
|
96
|
-
}
|
|
92
|
+
}, { path: dbPath });
|
|
97
93
|
}
|
|
98
94
|
catch (err) {
|
|
99
95
|
// Never mask the bun-test isolation guard as a silent "events failed".
|
|
@@ -11,16 +11,18 @@ import { assertNever } from "./assert.js";
|
|
|
11
11
|
*
|
|
12
12
|
* Buckets:
|
|
13
13
|
* - `accepted` — a write/content-authoring action succeeded.
|
|
14
|
-
* - `
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
14
|
+
* - `skipped` — the ref was GATED OUT before any content was produced: a
|
|
15
|
+
* cooldown, a signal-delta/eligibility skip, or a distill pool-delta skip.
|
|
16
|
+
* These are NOT rejections of produced content — they are the run declining
|
|
17
|
+
* to act on a ref it had no new reason to touch, and they scale with the
|
|
18
|
+
* whole indexed-ref pool (~13k/run), so folding them into `rejected` made the
|
|
19
|
+
* "accept rate" meaningless (deep-tuning analysis 2026-06-29, finding #1).
|
|
20
|
+
* - `rejected` — the run PRODUCED a change and a content-policy guard then
|
|
21
|
+
* rejected it (`reflect-guard-rejected`). This is the genuine value-rejection
|
|
22
|
+
* signal; it is small and meaningful, no longer drowned by gated skips.
|
|
21
23
|
* - `error` — the action failed (LLM/runtime error).
|
|
22
24
|
* - `noop` — bookkeeping that is neither a write nor a rejection (memory-prune);
|
|
23
|
-
* intentionally counted in none of the
|
|
25
|
+
* intentionally counted in none of the numeric buckets.
|
|
24
26
|
*
|
|
25
27
|
* The `default: assertNever(mode)` arm makes any future union variant a
|
|
26
28
|
* compile-time error here, forcing an explicit bucket choice.
|
|
@@ -35,6 +37,7 @@ export function classifyImproveAction(mode) {
|
|
|
35
37
|
case "reflect-cooldown":
|
|
36
38
|
case "reflect-skipped":
|
|
37
39
|
case "distill-skipped":
|
|
40
|
+
return "skipped";
|
|
38
41
|
case "reflect-guard-rejected":
|
|
39
42
|
return "rejected";
|
|
40
43
|
case "reflect-failed":
|
package/dist/core/logs-db.js
CHANGED
|
@@ -16,8 +16,10 @@
|
|
|
16
16
|
* Log lines are high-volume, append-only, and freely purgeable; state.db rows
|
|
17
17
|
* (events, proposals, task_history) are durable records. Separating them keeps
|
|
18
18
|
* state.db small and lets log retention be aggressive without touching durable
|
|
19
|
-
* state.
|
|
20
|
-
*
|
|
19
|
+
* state. Callers that need to correlate a task_history row with its log lines do
|
|
20
|
+
* an application-side join on the {@link buildTaskRunId} key (e.g. `health` via
|
|
21
|
+
* {@link getLoggedRunIds}) — no SQLite ATTACH, so the split survives a future
|
|
22
|
+
* provider change.
|
|
21
23
|
*
|
|
22
24
|
* ## run_id
|
|
23
25
|
*
|
|
@@ -39,8 +41,8 @@ import fs from "node:fs";
|
|
|
39
41
|
import path from "node:path";
|
|
40
42
|
import { openDatabase } from "../storage/database.js";
|
|
41
43
|
import { runMigrations as runSqliteMigrations } from "../storage/engines/sqlite-migrations.js";
|
|
44
|
+
import { applyStandardPragmas } from "../storage/sqlite-pragmas.js";
|
|
42
45
|
import { getDataDir } from "./paths.js";
|
|
43
|
-
import { getStateDbPath } from "./state-db.js";
|
|
44
46
|
// ── Path helper ──────────────────────────────────────────────────────────────
|
|
45
47
|
/**
|
|
46
48
|
* Default path: `<dataDir>/logs.db` — alongside state.db so cooperating
|
|
@@ -75,9 +77,9 @@ export function openLogsDatabase(dbPath) {
|
|
|
75
77
|
fs.mkdirSync(dir, { recursive: true });
|
|
76
78
|
}
|
|
77
79
|
const db = openDatabase(resolvedPath);
|
|
78
|
-
// PRAGMAs must run before any DDL or DML.
|
|
79
|
-
db
|
|
80
|
-
db
|
|
80
|
+
// PRAGMAs must run before any DDL or DML. foreignKeys:false preserves this
|
|
81
|
+
// opener's historical behaviour — logs.db has never enforced foreign keys.
|
|
82
|
+
applyStandardPragmas(db, { dataDir: dir, foreignKeys: false });
|
|
81
83
|
runMigrations(db);
|
|
82
84
|
return db;
|
|
83
85
|
}
|
|
@@ -145,8 +147,8 @@ export function runMigrations(db) {
|
|
|
145
147
|
* Encode a task run's identity — the unique `(task_id, started_at)` pair from
|
|
146
148
|
* state.db `task_history` — as a single run_id string.
|
|
147
149
|
*
|
|
148
|
-
* The format MUST stay in sync with the
|
|
149
|
-
*
|
|
150
|
+
* The format MUST stay in sync with the application-side join key that callers
|
|
151
|
+
* build from a `task_history` row's `task_id` and `started_at`.
|
|
150
152
|
*/
|
|
151
153
|
export function buildTaskRunId(taskId, startedAtIso) {
|
|
152
154
|
return `${taskId}@${startedAtIso}`;
|
|
@@ -229,64 +231,6 @@ export function getLoggedRunIds(db, runIds) {
|
|
|
229
231
|
}
|
|
230
232
|
return out;
|
|
231
233
|
}
|
|
232
|
-
// ── Cross-db: ATTACH state.db ────────────────────────────────────────────────
|
|
233
|
-
/**
|
|
234
|
-
* ATTACH state.db to an open logs.db handle under the schema name `state`,
|
|
235
|
-
* enabling cross-db joins like task_history × task_logs.
|
|
236
|
-
*
|
|
237
|
-
* The state.db file must already exist (callers always open state.db first in
|
|
238
|
-
* practice); attaching a non-existent path would silently create an empty,
|
|
239
|
-
* unmigrated database file, so this throws instead.
|
|
240
|
-
*/
|
|
241
|
-
export function attachStateDatabase(db, stateDbPath) {
|
|
242
|
-
const resolved = stateDbPath ?? getStateDbPath();
|
|
243
|
-
if (!fs.existsSync(resolved)) {
|
|
244
|
-
throw new Error(`Cannot ATTACH state.db: file does not exist at ${resolved}`);
|
|
245
|
-
}
|
|
246
|
-
// prepare().run() rather than db.run(): both drivers support parameterised
|
|
247
|
-
// ATTACH through a prepared statement, and no other call site uses db.run().
|
|
248
|
-
db.prepare("ATTACH DATABASE ? AS state").run(resolved);
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* Convenience: open logs.db with state.db attached as `state`. The returned
|
|
252
|
-
* handle supports cross-db queries such as {@link queryFailedRunLogLines}.
|
|
253
|
-
* Close it like any other handle (DETACH is implicit on close).
|
|
254
|
-
*/
|
|
255
|
-
export function openLogsDatabaseWithState(logsDbPath, stateDbPath) {
|
|
256
|
-
const db = openLogsDatabase(logsDbPath);
|
|
257
|
-
try {
|
|
258
|
-
attachStateDatabase(db, stateDbPath);
|
|
259
|
-
}
|
|
260
|
-
catch (err) {
|
|
261
|
-
db.close();
|
|
262
|
-
throw err;
|
|
263
|
-
}
|
|
264
|
-
return db;
|
|
265
|
-
}
|
|
266
|
-
/**
|
|
267
|
-
* Cross-db join: every log line belonging to a FAILED task_history run whose
|
|
268
|
-
* `started_at` is `>= since` (all failed runs when omitted). Requires a handle
|
|
269
|
-
* opened via {@link openLogsDatabaseWithState}.
|
|
270
|
-
*
|
|
271
|
-
* The join key is the run_id encoding documented on {@link buildTaskRunId}:
|
|
272
|
-
* `task_logs.run_id = task_history.task_id || '@' || task_history.started_at`.
|
|
273
|
-
*/
|
|
274
|
-
export function queryFailedRunLogLines(db, options = {}) {
|
|
275
|
-
const conditions = ["th.status = 'failed'"];
|
|
276
|
-
const params = [];
|
|
277
|
-
if (options.since) {
|
|
278
|
-
conditions.push("th.started_at >= ?");
|
|
279
|
-
params.push(options.since);
|
|
280
|
-
}
|
|
281
|
-
const limit = options.limit !== undefined && options.limit >= 0 ? ` LIMIT ${Math.floor(options.limit)}` : "";
|
|
282
|
-
return db
|
|
283
|
-
.prepare(`SELECT th.task_id, l.run_id, th.started_at, th.status, l.ts, l.stream, l.level, l.line
|
|
284
|
-
FROM state.task_history th
|
|
285
|
-
JOIN task_logs l ON l.run_id = th.task_id || '@' || th.started_at
|
|
286
|
-
WHERE ${conditions.join(" AND ")}
|
|
287
|
-
ORDER BY th.started_at DESC, l.id ASC${limit}`)
|
|
288
|
-
.all(...params);
|
|
289
|
-
}
|
|
290
234
|
// ── Retention ────────────────────────────────────────────────────────────────
|
|
291
235
|
/**
|
|
292
236
|
* Delete task_logs rows older than `retentionDays` (default: 90). Mirrors
|
package/dist/core/parse.js
CHANGED
|
@@ -98,16 +98,24 @@ export function parseJsonResponse(raw) {
|
|
|
98
98
|
* balanced `{ }` or `[ ]` structure in the text and attempts to parse that
|
|
99
99
|
* substring. Returns `undefined` if no valid JSON structure is found.
|
|
100
100
|
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* returned
|
|
101
|
+
* Shape preference is controlled by {@link ParseEmbeddedJsonOptions.expect}:
|
|
102
|
+
* - `"any"` (default): non-array results are preferred — a `{…}` object found
|
|
103
|
+
* first is returned immediately; arrays (`[…]`) are a fallback.
|
|
104
|
+
* - `"array"`: only top-level arrays are returned. The direct parse is
|
|
105
|
+
* accepted only if it is an array, and the scanner returns the first
|
|
106
|
+
* balanced `[…]` while skipping `{…}` openers entirely.
|
|
104
107
|
*/
|
|
105
|
-
export function parseEmbeddedJsonResponse(raw) {
|
|
108
|
+
export function parseEmbeddedJsonResponse(raw, options) {
|
|
109
|
+
const expectArray = options?.expect === "array";
|
|
106
110
|
const direct = parseJsonResponse(raw);
|
|
107
|
-
if (direct !== undefined)
|
|
111
|
+
if (direct !== undefined && (!expectArray || Array.isArray(direct)))
|
|
108
112
|
return direct;
|
|
109
113
|
const text = escapeJsonStringControls(stripCodeFences(stripThinkBlocks(raw)));
|
|
110
114
|
let arrayFallback;
|
|
115
|
+
// Scan only *top-level* balanced structures: once a `{…}`/`[…]` is matched we
|
|
116
|
+
// jump `start` past its closing bracket rather than re-scanning its interior.
|
|
117
|
+
// This keeps array mode from salvaging an array *nested inside* a leading
|
|
118
|
+
// object (e.g. the `entities` array of a bare `{entities,relations}` object).
|
|
111
119
|
for (let start = 0; start < text.length; start++) {
|
|
112
120
|
const opener = text[start];
|
|
113
121
|
if (opener !== "{" && opener !== "[")
|
|
@@ -116,6 +124,7 @@ export function parseEmbeddedJsonResponse(raw) {
|
|
|
116
124
|
let depth = 0;
|
|
117
125
|
let inString = false;
|
|
118
126
|
let escaped = false;
|
|
127
|
+
let end = -1;
|
|
119
128
|
for (let i = start; i < text.length; i++) {
|
|
120
129
|
const ch = text[i];
|
|
121
130
|
if (inString) {
|
|
@@ -139,20 +148,31 @@ export function parseEmbeddedJsonResponse(raw) {
|
|
|
139
148
|
if (ch === closer) {
|
|
140
149
|
depth -= 1;
|
|
141
150
|
if (depth === 0) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (!Array.isArray(parsed)) {
|
|
145
|
-
return parsed;
|
|
146
|
-
}
|
|
147
|
-
arrayFallback ??= parsed;
|
|
148
|
-
break;
|
|
149
|
-
}
|
|
150
|
-
catch {
|
|
151
|
-
break;
|
|
152
|
-
}
|
|
151
|
+
end = i;
|
|
152
|
+
break;
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
|
+
if (end === -1)
|
|
157
|
+
continue; // never balanced — let the next opener try
|
|
158
|
+
try {
|
|
159
|
+
const parsed = JSON.parse(text.slice(start, end + 1));
|
|
160
|
+
if (Array.isArray(parsed)) {
|
|
161
|
+
// First valid array wins in array mode; in "any" mode it is the
|
|
162
|
+
// fallback returned only if no object is found.
|
|
163
|
+
if (expectArray)
|
|
164
|
+
return parsed;
|
|
165
|
+
arrayFallback ??= parsed;
|
|
166
|
+
}
|
|
167
|
+
else if (!expectArray) {
|
|
168
|
+
return parsed;
|
|
169
|
+
}
|
|
170
|
+
// Skip past this balanced structure so we don't descend into it.
|
|
171
|
+
start = end;
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// Malformed candidate — advance one char and try the next opener.
|
|
175
|
+
}
|
|
156
176
|
}
|
|
157
177
|
return arrayFallback;
|
|
158
178
|
}
|
package/dist/core/paths.js
CHANGED
|
@@ -215,6 +215,9 @@ export function getDataDir(env = process.env, platform = process.platform) {
|
|
|
215
215
|
export function getDbPath() {
|
|
216
216
|
return path.join(getDataDir(), "index.db");
|
|
217
217
|
}
|
|
218
|
+
export function getIndexWriterLockPath() {
|
|
219
|
+
return path.join(getDataDir(), "index.db.write.lock");
|
|
220
|
+
}
|
|
218
221
|
export function getWorkflowDbPath() {
|
|
219
222
|
return path.join(getDataDir(), "workflow.db");
|
|
220
223
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
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
|
+
* Dispatch resolver for the standards prompt seam — selects which of the two
|
|
6
|
+
* standards features fires for a given write target, mutually exclusively:
|
|
7
|
+
*
|
|
8
|
+
* - **Feature A — wiki schema**: the target is a wiki page (a ref/path under
|
|
9
|
+
* `wikis/<name>/`, NOT a `raw/` file and NOT a wiki infra file
|
|
10
|
+
* `schema.md`/`index.md`/`log.md`). Returns that wiki's `schema.md` body.
|
|
11
|
+
* - **Feature B — stash standards**: the target is any non-wiki asset.
|
|
12
|
+
* Returns the concatenated `category: convention`/`meta` fact bodies.
|
|
13
|
+
* - **Neither fires**: a wiki `raw/` file or a wiki infra file. Returns `""`.
|
|
14
|
+
*
|
|
15
|
+
* The two NEVER both fire. Both underlying readers degrade to `""` on
|
|
16
|
+
* missing/malformed input and never throw, so this resolver never throws.
|
|
17
|
+
*/
|
|
18
|
+
import { extractWikiNameFromRef, INDEX_MD, LOG_MD, loadWikiSchema, SCHEMA_MD } from "../../wiki/wiki.js";
|
|
19
|
+
import { resolveStashStandards } from "./resolve-stash-standards.js";
|
|
20
|
+
import { resolveTypeConventions, typeConventionRef } from "./resolve-type-conventions.js";
|
|
21
|
+
/** Wiki infra files that are not authored pages (relative to the wiki root). */
|
|
22
|
+
const WIKI_INFRA_BASENAMES = new Set([SCHEMA_MD, INDEX_MD, LOG_MD]);
|
|
23
|
+
/**
|
|
24
|
+
* Extract the asset type from a canonical ref (`[origin//]type:name`) without
|
|
25
|
+
* throwing. Returns `undefined` for refs that have no `type:` prefix. Kept local
|
|
26
|
+
* and lenient — the per-type resolver validates the result against
|
|
27
|
+
* `getAssetTypes()`, so a bogus prefix here simply yields no convention.
|
|
28
|
+
*/
|
|
29
|
+
function refType(ref) {
|
|
30
|
+
if (!ref)
|
|
31
|
+
return undefined;
|
|
32
|
+
const body = ref.includes("//") ? ref.slice(ref.indexOf("//") + 2) : ref;
|
|
33
|
+
const colon = body.indexOf(":");
|
|
34
|
+
if (colon <= 0)
|
|
35
|
+
return undefined;
|
|
36
|
+
return body.slice(0, colon).trim() || undefined;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the standards context for a write target identified by its asset ref.
|
|
40
|
+
*
|
|
41
|
+
* @param ref Canonical asset ref of the write target (e.g. `skill:foo`,
|
|
42
|
+
* `wiki:research/topics/x`). When undefined, the target is a
|
|
43
|
+
* non-wiki authoring flow → stash standards.
|
|
44
|
+
* @param stashRoot Stash root directory.
|
|
45
|
+
*/
|
|
46
|
+
export function resolveStandardsContext(ref, stashRoot) {
|
|
47
|
+
const wikiName = ref ? extractWikiNameFromRef(ref) : undefined;
|
|
48
|
+
if (!wikiName) {
|
|
49
|
+
// Non-wiki asset target → Feature B (general stash standards) plus the
|
|
50
|
+
// per-type SOFT conventions layer (#646), type-scoped to the write target.
|
|
51
|
+
const general = resolveStashStandards(stashRoot);
|
|
52
|
+
const type = refType(ref);
|
|
53
|
+
// A non-empty body here guarantees `type` is a `getAssetTypes()`-validated
|
|
54
|
+
// string (the resolver returns "" otherwise).
|
|
55
|
+
const typeConventions = type ? resolveTypeConventions(stashRoot, type) : "";
|
|
56
|
+
if (!typeConventions || !type)
|
|
57
|
+
return general;
|
|
58
|
+
// Soft, type-scoped guidance — clearly labeled and kept separate from the
|
|
59
|
+
// HARD (validator-enforced) rules that `authoringRulesForType` injects
|
|
60
|
+
// downstream. These facts are advice only; they never weaken the gate.
|
|
61
|
+
const softSection = [
|
|
62
|
+
`# ${typeConventionRef(type)} (soft per-type conventions — guidance, not enforced)`,
|
|
63
|
+
typeConventions,
|
|
64
|
+
].join("\n");
|
|
65
|
+
return general ? `${general}\n\n${softSection}` : softSection;
|
|
66
|
+
}
|
|
67
|
+
// Wiki target. Extract the page path after `wiki:<name>/`.
|
|
68
|
+
const prefix = `wiki:${wikiName}/`;
|
|
69
|
+
const pagePath = ref?.startsWith(prefix) ? ref.slice(prefix.length) : "";
|
|
70
|
+
// `wiki:<name>` with no page, a `raw/` file, or a wiki infra file → neither
|
|
71
|
+
// feature fires.
|
|
72
|
+
if (!pagePath)
|
|
73
|
+
return "";
|
|
74
|
+
if (pagePath === "raw" || pagePath.startsWith("raw/"))
|
|
75
|
+
return "";
|
|
76
|
+
// Infra files (`schema`/`index`/`log`) are only special at the WIKI ROOT.
|
|
77
|
+
// A nested page like `wiki:research/analysis/schema` is a genuine page and
|
|
78
|
+
// must NOT be suppressed, so only check when the page is at root depth.
|
|
79
|
+
if (!pagePath.includes("/")) {
|
|
80
|
+
// Refs drop the `.md` extension; compare against both forms defensively.
|
|
81
|
+
if (WIKI_INFRA_BASENAMES.has(pagePath) || WIKI_INFRA_BASENAMES.has(`${pagePath}.md`)) {
|
|
82
|
+
return "";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// A genuine wiki page → Feature A (that wiki's schema body).
|
|
86
|
+
return loadWikiSchema(stashRoot, wikiName).body;
|
|
87
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
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
|
+
* Resolve the stash-authoring "standards" context (Feature B of the standards
|
|
6
|
+
* plan): gather the bodies of `fact` assets whose `category` frontmatter is
|
|
7
|
+
* `convention` or `meta` so naming / tag / frontmatter conventions are surfaced
|
|
8
|
+
* to the agent when it creates or edits a non-wiki asset.
|
|
9
|
+
*
|
|
10
|
+
* Selection is by **frontmatter `category`**, never by path — flat
|
|
11
|
+
* (`facts/x.md`) and nested (`facts/conventions/x.md`) layouts resolve
|
|
12
|
+
* identically. The MVP does no parsing of fenced blocks, no rule objects, and
|
|
13
|
+
* no warnings: it concatenates the selected facts' bodies in stable enumeration
|
|
14
|
+
* order, each preceded by a one-line `# <ref>` provenance header. Returns `""`
|
|
15
|
+
* when no matching facts exist.
|
|
16
|
+
*/
|
|
17
|
+
import fs from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { parseFrontmatter } from "../asset/frontmatter.js";
|
|
20
|
+
/** `category` values that mark a fact as an authoring standard. */
|
|
21
|
+
const STANDARD_CATEGORIES = new Set(["convention", "meta"]);
|
|
22
|
+
/** Directory (under the stash root) where `fact` assets live. */
|
|
23
|
+
const FACTS_SUBDIR = "facts";
|
|
24
|
+
/**
|
|
25
|
+
* Per-type SOFT convention facts (`facts/conventions/assets/<type>.md`, #646)
|
|
26
|
+
* are surfaced **type-scoped** through `resolveTypeConventions`, so they must
|
|
27
|
+
* NOT leak into this un-type-scoped general layer (authoring a `command` must
|
|
28
|
+
* not pull the `skill` convention). Excluded by relative path (POSIX form).
|
|
29
|
+
*/
|
|
30
|
+
const TYPE_CONVENTIONS_REL = "conventions/assets/";
|
|
31
|
+
/**
|
|
32
|
+
* Recursively collect `.md` files under `dir` in stable (sorted) enumeration
|
|
33
|
+
* order. Returns absolute paths. Missing dir → `[]`.
|
|
34
|
+
*/
|
|
35
|
+
function collectMarkdownFiles(dir) {
|
|
36
|
+
let entries;
|
|
37
|
+
try {
|
|
38
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
const results = [];
|
|
44
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
45
|
+
const full = path.join(dir, entry.name);
|
|
46
|
+
if (entry.isDirectory()) {
|
|
47
|
+
results.push(...collectMarkdownFiles(full));
|
|
48
|
+
}
|
|
49
|
+
else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
50
|
+
results.push(full);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return results;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Derive a fact ref (`fact:conventions/naming`) from an absolute markdown path
|
|
57
|
+
* relative to the facts root. Mirrors the canonical-name derivation in
|
|
58
|
+
* `asset-spec.ts` (POSIX separators, `.md` stripped).
|
|
59
|
+
*/
|
|
60
|
+
function toFactRef(factsRoot, absPath) {
|
|
61
|
+
const rel = path.relative(factsRoot, absPath).split(path.sep).join("/");
|
|
62
|
+
const name = rel.endsWith(".md") ? rel.slice(0, -3) : rel;
|
|
63
|
+
return `fact:${name}`;
|
|
64
|
+
}
|
|
65
|
+
export function resolveStashStandards(stashRoot) {
|
|
66
|
+
const factsRoot = path.join(stashRoot, FACTS_SUBDIR);
|
|
67
|
+
const sections = [];
|
|
68
|
+
for (const absPath of collectMarkdownFiles(factsRoot)) {
|
|
69
|
+
// Per-type SOFT conventions are delivered type-scoped (#646); skip them
|
|
70
|
+
// here so they never leak un-type-scoped into every authoring flow.
|
|
71
|
+
const relPosix = path.relative(factsRoot, absPath).split(path.sep).join("/");
|
|
72
|
+
if (relPosix.startsWith(TYPE_CONVENTIONS_REL))
|
|
73
|
+
continue;
|
|
74
|
+
let raw;
|
|
75
|
+
try {
|
|
76
|
+
raw = fs.readFileSync(absPath, "utf8");
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
let category = "";
|
|
82
|
+
let body = "";
|
|
83
|
+
try {
|
|
84
|
+
const parsed = parseFrontmatter(raw);
|
|
85
|
+
category = typeof parsed.data.category === "string" ? parsed.data.category.trim() : "";
|
|
86
|
+
body = parsed.content;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (!STANDARD_CATEGORIES.has(category))
|
|
92
|
+
continue;
|
|
93
|
+
const trimmed = body.trim();
|
|
94
|
+
if (!trimmed)
|
|
95
|
+
continue; // skip stub facts with frontmatter but no body
|
|
96
|
+
sections.push(`# ${toFactRef(factsRoot, absPath)}\n${trimmed}`);
|
|
97
|
+
}
|
|
98
|
+
return sections.join("\n\n");
|
|
99
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
* Resolve **per-type SOFT authoring conventions** (#646) — the third and final
|
|
6
|
+
* authoring-guidance layer:
|
|
7
|
+
*
|
|
8
|
+
* 1. HARD rules (validator-rejecting, code-sourced) → `authoringRulesForType()`
|
|
9
|
+
* (`src/core/authoring-rules.ts`, #645). Never editable; the gate enforces them.
|
|
10
|
+
* 2. General stash standards (cross-type naming/tag conventions) →
|
|
11
|
+
* `resolveStashStandards()` `category: convention|meta` facts (#642).
|
|
12
|
+
* 3. **Per-type SOFT conventions** (voice, structure, length *preference* for
|
|
13
|
+
* *this* asset type) → user-editable `facts/conventions/assets/<type>.md`
|
|
14
|
+
* (THIS module). Augments the built-in `TYPE_HINTS` fallback for display.
|
|
15
|
+
*
|
|
16
|
+
* These facts are **soft only** — advice, not contract. They MUST NOT carry
|
|
17
|
+
* hard, validator-rejecting rules: a user editing or deleting one must never be
|
|
18
|
+
* able to weaken the authoring contract the gate enforces (#645 boundary).
|
|
19
|
+
*
|
|
20
|
+
* Selection is by a `getAssetTypes()`-validated basename: only
|
|
21
|
+
* `facts/conventions/assets/<known-type>.md` resolves. Read directly from disk
|
|
22
|
+
* (no index rebuild); any missing dir/file, unknown type, or read error degrades
|
|
23
|
+
* to `""` and never throws.
|
|
24
|
+
*/
|
|
25
|
+
import fs from "node:fs";
|
|
26
|
+
import path from "node:path";
|
|
27
|
+
import { getAssetTypes } from "../asset/asset-spec.js";
|
|
28
|
+
import { parseFrontmatter } from "../asset/frontmatter.js";
|
|
29
|
+
/** Sub-path (under the stash root) for per-type SOFT convention facts. */
|
|
30
|
+
export const TYPE_CONVENTIONS_SUBDIR = path.join("facts", "conventions", "assets");
|
|
31
|
+
/** The `fact:` ref prefix for a per-type convention, e.g. `fact:conventions/assets/skill`. */
|
|
32
|
+
export function typeConventionRef(type) {
|
|
33
|
+
return `fact:conventions/assets/${type}`;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Read the SOFT authoring-convention body for asset type `type`, if a stash
|
|
37
|
+
* owner has authored `facts/conventions/assets/<type>.md`.
|
|
38
|
+
*
|
|
39
|
+
* @returns the trimmed markdown body (frontmatter stripped), or `""` when the
|
|
40
|
+
* type is unknown, the file is absent, or anything goes wrong.
|
|
41
|
+
*/
|
|
42
|
+
export function resolveTypeConventions(stashRoot, type) {
|
|
43
|
+
if (!stashRoot || !type)
|
|
44
|
+
return "";
|
|
45
|
+
// Basename MUST be a known asset type — never resolve an arbitrary file.
|
|
46
|
+
if (!getAssetTypes().includes(type))
|
|
47
|
+
return "";
|
|
48
|
+
const abs = path.join(stashRoot, TYPE_CONVENTIONS_SUBDIR, `${type}.md`);
|
|
49
|
+
let raw;
|
|
50
|
+
try {
|
|
51
|
+
raw = fs.readFileSync(abs, "utf8");
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return ""; // missing dir/file or read error → degrade to empty
|
|
55
|
+
}
|
|
56
|
+
let body = "";
|
|
57
|
+
try {
|
|
58
|
+
body = parseFrontmatter(raw).content;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Malformed frontmatter: fall back to the whole file (parseFrontmatter
|
|
62
|
+
// normally returns whole content as body, but guard defensively).
|
|
63
|
+
body = raw;
|
|
64
|
+
}
|
|
65
|
+
return body.trim();
|
|
66
|
+
}
|