akm-cli 0.9.0-beta.4 → 0.9.0-beta.40
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 +626 -0
- 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/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 +22 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +22 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +24 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +22 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +25 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +21 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +21 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +23 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +22 -0
- package/dist/assets/templates/html/health.html +281 -111
- package/dist/cli.js +14 -3
- package/dist/commands/agent/contribute-cli.js +16 -3
- package/dist/commands/feedback-cli.js +15 -6
- package/dist/commands/graph/graph.js +75 -71
- package/dist/commands/health/checks.js +48 -0
- package/dist/commands/health/html-report.js +422 -80
- package/dist/commands/health.js +381 -9
- package/dist/commands/improve/calibration.js +161 -0
- package/dist/commands/improve/consolidate.js +631 -111
- package/dist/commands/improve/dedup.js +482 -0
- package/dist/commands/improve/distill.js +163 -69
- package/dist/commands/improve/encoding-salience.js +205 -0
- package/dist/commands/improve/extract-cli.js +115 -1
- package/dist/commands/improve/extract-prompt.js +39 -2
- package/dist/commands/improve/extract-watch.js +140 -0
- package/dist/commands/improve/extract.js +403 -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 +113 -6
- package/dist/commands/improve/improve-profiles.js +12 -0
- package/dist/commands/improve/improve.js +2042 -612
- package/dist/commands/improve/memory/memory-contradiction-detect.js +23 -28
- package/dist/commands/improve/outcome-loop.js +256 -0
- package/dist/commands/improve/proactive-maintenance.js +115 -0
- package/dist/commands/improve/procedural.js +418 -0
- package/dist/commands/improve/recombine.js +602 -0
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/improve/reflect.js +46 -4
- package/dist/commands/improve/related-sessions.js +120 -0
- 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 +34 -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 +17 -1
- package/dist/commands/proposal/proposal.js +5 -0
- package/dist/commands/proposal/propose.js +5 -0
- package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
- package/dist/commands/proposal/validators/proposals.js +187 -57
- package/dist/commands/read/curate.js +344 -80
- package/dist/commands/read/search-cli.js +7 -0
- package/dist/commands/read/search.js +1 -0
- package/dist/commands/read/show.js +67 -2
- package/dist/commands/sources/init.js +36 -9
- package/dist/commands/sources/installed-stashes.js +5 -1
- package/dist/commands/sources/schema-repair.js +13 -1
- package/dist/commands/sources/stash-cli.js +19 -3
- package/dist/commands/sources/stash-skeleton.js +23 -8
- package/dist/core/asset/asset-registry.js +2 -0
- package/dist/core/asset/asset-spec.js +14 -0
- package/dist/core/asset/frontmatter.js +166 -167
- package/dist/core/asset/markdown.js +8 -0
- package/dist/core/authoring-rules.js +83 -0
- package/dist/core/config/config-schema.js +274 -2
- package/dist/core/config/config.js +2 -2
- package/dist/core/logs-db.js +4 -3
- 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-db.js +691 -30
- package/dist/indexer/db/db.js +364 -38
- 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 +203 -3
- package/dist/indexer/index-writer-lock.js +99 -0
- package/dist/indexer/indexer.js +114 -111
- package/dist/indexer/passes/memory-inference.js +10 -3
- package/dist/indexer/passes/staleness-detect.js +2 -5
- package/dist/indexer/search/db-search.js +15 -4
- package/dist/indexer/search/ranking-contributors.js +22 -0
- package/dist/indexer/search/ranking.js +4 -0
- package/dist/indexer/walk/matchers.js +9 -0
- package/dist/integrations/agent/prompts.js +33 -0
- package/dist/integrations/harnesses/claude/session-log.js +11 -1
- package/dist/integrations/harnesses/opencode/session-log.js +173 -3
- package/dist/integrations/session-logs/index.js +16 -0
- package/dist/llm/client.js +23 -4
- package/dist/llm/embedder.js +27 -3
- package/dist/llm/embedders/local.js +66 -2
- package/dist/llm/feature-gate.js +8 -4
- package/dist/llm/graph-extract.js +2 -1
- package/dist/llm/memory-infer.js +4 -8
- package/dist/llm/metadata-enhance.js +9 -1
- package/dist/output/renderers.js +73 -1
- package/dist/output/shapes/curate.js +14 -2
- package/dist/output/text/helpers.js +16 -1
- package/dist/runtime.js +25 -1
- package/dist/scripts/migrate-storage.js +1378 -599
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +479 -270
- package/dist/setup/setup.js +3 -3
- package/dist/sources/providers/tar-utils.js +16 -8
- package/dist/storage/sqlite-pragmas.js +146 -0
- package/dist/wiki/wiki.js +37 -0
- package/dist/workflows/db.js +3 -4
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/data-and-telemetry.md +1 -0
- package/package.json +8 -6
|
@@ -133,6 +133,34 @@ export const ImproveProcessConfigSchema = z
|
|
|
133
133
|
// consolidation pass skips entirely (emits `pool_below_min_size`). 0 disables
|
|
134
134
|
// the guard. Only meaningful on the `consolidate` process. Default 500.
|
|
135
135
|
minPoolSize: z.number().int().min(0).optional(),
|
|
136
|
+
// Consolidate process: deterministic near-duplicate dedup pre-pass (#617).
|
|
137
|
+
// A cheap, no-LLM fast path that collapses obvious duplicates (`.derived`
|
|
138
|
+
// origin pairs + content twins) before the LLM consolidation. Default OFF
|
|
139
|
+
// — when absent the consolidate pass behaves byte-identically to today.
|
|
140
|
+
// `cosineThreshold` is a strict floor in [0, 1] (default 0.97) for the
|
|
141
|
+
// optional embedding-similarity match; exact normalized content-hash
|
|
142
|
+
// equality always collapses regardless of the threshold. Only meaningful
|
|
143
|
+
// on the `consolidate` process.
|
|
144
|
+
dedup: z
|
|
145
|
+
.object({
|
|
146
|
+
enabled: z.boolean().optional(),
|
|
147
|
+
cosineThreshold: z.number().min(0).max(1).optional(),
|
|
148
|
+
// WS-3a: maximum pool size for the O(n²) cosine-similarity twin compare.
|
|
149
|
+
// Only the first `cosineCandidateLimit` memories are cosine-compared;
|
|
150
|
+
// exact-hash matches still run over the full pool. Default 500. Raise
|
|
151
|
+
// with care — cost is O(n²).
|
|
152
|
+
cosineCandidateLimit: z.number().int().positive().optional(),
|
|
153
|
+
})
|
|
154
|
+
.strict()
|
|
155
|
+
.optional(),
|
|
156
|
+
// Consolidate process: judged-state cache (#581). When enabled, a memory
|
|
157
|
+
// whose current content hash equals its cached judged hash is SKIPPED from
|
|
158
|
+
// the LLM pool (judged-unchanged → no re-judge), letting one run sweep the
|
|
159
|
+
// whole corpus at O(changed/new) cost instead of narrowing to a recent
|
|
160
|
+
// time-window slice. Default OFF — when absent the consolidate pass behaves
|
|
161
|
+
// byte-identically to today (the incrementalSince path is unaffected). Only
|
|
162
|
+
// meaningful on the `consolidate` process.
|
|
163
|
+
judgedCache: z.object({ enabled: z.boolean().optional() }).strict().optional(),
|
|
136
164
|
qualityGate: z.object({ enabled: z.boolean().optional() }).strict().optional(),
|
|
137
165
|
contradictionDetection: z.object({ enabled: z.boolean().optional() }).strict().optional(),
|
|
138
166
|
// Extract process config (only meaningful for extract process)
|
|
@@ -144,18 +172,178 @@ export const ImproveProcessConfigSchema = z
|
|
|
144
172
|
// on the `extract` process.
|
|
145
173
|
minContentChars: z.number().int().min(0).optional(),
|
|
146
174
|
maxChunkSize: z.number().int().min(1).max(50).optional(),
|
|
175
|
+
// Consolidate process: narrow candidate pool to memories modified within
|
|
176
|
+
// this duration window plus their graph neighbours. Only meaningful on
|
|
177
|
+
// the `consolidate` process. Absent = full-pool sweep.
|
|
178
|
+
incrementalSince: z.string().optional(),
|
|
179
|
+
// Consolidate process: hard cap on memories processed per pass.
|
|
180
|
+
// Reflect/distill: max refs processed (same as profile-level `limit`).
|
|
181
|
+
limit: positiveInt.optional(),
|
|
182
|
+
// Consolidate process: graph neighbours per changed memory during
|
|
183
|
+
// incremental consolidation. Default 5. Only meaningful with incrementalSince.
|
|
184
|
+
neighborsPerChanged: z.number().int().min(1).optional(),
|
|
185
|
+
// Distill process: skip distill entirely when reflect produced zero planned refs.
|
|
186
|
+
requirePlannedRefs: z.boolean().optional(),
|
|
187
|
+
// proactiveMaintenance process (Layer 2): staleness gate + rotation cooldown
|
|
188
|
+
// in days (default 30). Only meaningful on `proactiveMaintenance`.
|
|
189
|
+
dueDays: z.number().int().min(0).optional(),
|
|
190
|
+
// proactiveMaintenance process: top-N bound per run (default 25). Alias for
|
|
191
|
+
// `limit`; `maxPerRun` wins when both are set.
|
|
192
|
+
maxPerRun: positiveInt.optional(),
|
|
193
|
+
// graphExtraction process (#624 P2): when set, rank eligible files by
|
|
194
|
+
// utility_scores DESC and process only the top-N per run (incremental
|
|
195
|
+
// high-signal-first sweep). Unset = process all eligible (current
|
|
196
|
+
// behavior). Only meaningful on `graphExtraction`.
|
|
197
|
+
topN: positiveInt.optional(),
|
|
198
|
+
// MemoryInference process: minimum pending memory count to run the pass.
|
|
199
|
+
minPendingCount: z.number().int().min(0).optional(),
|
|
147
200
|
// Extract process: minimum number of new (unseen, in-window) candidate
|
|
148
201
|
// sessions below which the extract pass skips entirely (emits an
|
|
149
202
|
// `improve_skipped` event with `reason: "below_min_new_sessions"`). 0
|
|
150
203
|
// disables the guard. Only meaningful on the `extract` process. Default 0
|
|
151
204
|
// (disabled) so existing behaviour is preserved; only opted-in profiles set it.
|
|
152
205
|
minNewSessions: z.number().int().min(0).optional(),
|
|
206
|
+
// Extract process: cap on NEW sessions processed (LLM-called) per run; the
|
|
207
|
+
// rest roll to the next run (still unseen). 0 disables. Absent = default 25.
|
|
208
|
+
maxSessionsPerRun: z.number().int().min(0).optional(),
|
|
153
209
|
// #561 — index agent sessions as a searchable `session` asset (extract
|
|
154
210
|
// process). Absent = on-when-an-LLM-is-available (fail-open when offline).
|
|
211
|
+
// COST: when on, each processed session makes a SECOND LLM call (the session
|
|
212
|
+
// summary) on top of the extraction call — i.e. ~2 LLM calls/session. Set to
|
|
213
|
+
// false to halve per-session extract cost at the price of unsearchable
|
|
214
|
+
// sessions. (Unchanged/skip sessions still cost zero — the content-hash
|
|
215
|
+
// ledger gates both calls upstream.)
|
|
155
216
|
indexSessions: z.boolean().optional(),
|
|
156
217
|
// #561 — minimum session duration in minutes for session indexing. 0
|
|
157
218
|
// disables the gate. Absent = default 5. Only meaningful on `extract`.
|
|
158
219
|
minSessionDuration: z.number().min(0).optional(),
|
|
220
|
+
// Consolidate process: fallback p90 wall-clock time per consolidation chunk
|
|
221
|
+
// in seconds, used for cold-start budget estimation when no telemetry
|
|
222
|
+
// history exists. The actual p90 is derived from observed run durations
|
|
223
|
+
// once sufficient history accumulates; this value is only used on the very
|
|
224
|
+
// first run. Default 30 s. Only meaningful on the `consolidate` process.
|
|
225
|
+
p90ChunkSecondsDefault: z.number().finite().positive().optional(),
|
|
226
|
+
// WS-3b: Homeostatic demotion (step 0a). Before any LLM merge, demote
|
|
227
|
+
// retrievalSalience for stale/low-value assets so the merge pool is bounded
|
|
228
|
+
// and high-SNR. Demotion is state.db-only (file content untouched);
|
|
229
|
+
// re-promotable on re-retrieval. Default OFF. Only meaningful on the
|
|
230
|
+
// `consolidate` process.
|
|
231
|
+
homeostaticDemotion: z
|
|
232
|
+
.object({
|
|
233
|
+
enabled: z.boolean().optional(),
|
|
234
|
+
// Minimum days since last retrieval to consider an asset stale (default 30).
|
|
235
|
+
staleDays: z.number().int().min(0).optional(),
|
|
236
|
+
// Demotion factor: multiply retrievalSalience by this when stale (default 0.5).
|
|
237
|
+
demotionFactor: z.number().min(0).max(1).optional(),
|
|
238
|
+
})
|
|
239
|
+
.strict()
|
|
240
|
+
.optional(),
|
|
241
|
+
// WS-3b: Schema-similarity gate (step 0b). At intake, if a new candidate's
|
|
242
|
+
// body embedding is within epsilon of an existing derived-layer lesson/knowledge
|
|
243
|
+
// node, mark it schema-consistent and lower its priority. Default OFF.
|
|
244
|
+
// Only meaningful on the `consolidate` and `extract` processes.
|
|
245
|
+
schemaSimilarity: z
|
|
246
|
+
.object({
|
|
247
|
+
enabled: z.boolean().optional(),
|
|
248
|
+
// Epsilon: cosine similarity threshold above which a candidate is schema-consistent
|
|
249
|
+
// (default 0.85 — looser than dedup's 0.97 since we want to catch conceptual overlap).
|
|
250
|
+
epsilon: z.number().min(0).max(1).optional(),
|
|
251
|
+
// Multiplicative factor applied to candidate confidence when schema-consistent.
|
|
252
|
+
// Default 0.5 — halves the confidence so schema-consistent candidates are less likely
|
|
253
|
+
// to pass the quality gate and create redundant stash entries.
|
|
254
|
+
confidencePenalty: z.number().min(0).max(1).optional(),
|
|
255
|
+
})
|
|
256
|
+
.strict()
|
|
257
|
+
.optional(),
|
|
258
|
+
// WS-3b: Hot-probation intake buffer (step 0c, #604). New system-generated
|
|
259
|
+
// extractions enter captureMode: hot-probation and spend ONE consolidation
|
|
260
|
+
// cycle in probation. Dedup + quality second-pass runs before promotion.
|
|
261
|
+
// Default OFF. Only meaningful on the `extract` process.
|
|
262
|
+
hotProbation: z
|
|
263
|
+
.object({
|
|
264
|
+
enabled: z.boolean().optional(),
|
|
265
|
+
})
|
|
266
|
+
.strict()
|
|
267
|
+
.optional(),
|
|
268
|
+
// WS-3b: Anti-collapse guards (step 8). Prevents the consolidation pipeline
|
|
269
|
+
// from collapsing too aggressively and losing diversity.
|
|
270
|
+
// - maxGeneration: refuse to merge two assets both above this generation (default 2).
|
|
271
|
+
// - lexicalDiversityCheck: low n-gram diversity ⇒ raise merge threshold.
|
|
272
|
+
// - randomClusterFraction: occasional random (non-similar) cluster in pool (default 0.05).
|
|
273
|
+
// Default OFF. Only meaningful on the `consolidate` process.
|
|
274
|
+
antiCollapse: z
|
|
275
|
+
.object({
|
|
276
|
+
enabled: z.boolean().optional(),
|
|
277
|
+
maxGeneration: z.number().int().min(1).optional(),
|
|
278
|
+
lexicalDiversityCheck: z.boolean().optional(),
|
|
279
|
+
randomClusterFraction: z.number().min(0).max(1).optional(),
|
|
280
|
+
})
|
|
281
|
+
.strict()
|
|
282
|
+
.optional(),
|
|
283
|
+
// WS-3b: CLS (Complementary Learning System) interleaving (step 9).
|
|
284
|
+
// distill/memoryInference prompts include embedding-retrieved existing adjacent
|
|
285
|
+
// lessons/knowledge to prevent catastrophic interference with prior generalizations.
|
|
286
|
+
// Default OFF. Only meaningful on `distill` and `memoryInference` processes.
|
|
287
|
+
cls: z
|
|
288
|
+
.object({
|
|
289
|
+
enabled: z.boolean().optional(),
|
|
290
|
+
// Number of adjacent lessons/knowledge to include in prompts (default 3).
|
|
291
|
+
adjacentCount: z.number().int().min(1).optional(),
|
|
292
|
+
})
|
|
293
|
+
.strict()
|
|
294
|
+
.optional(),
|
|
295
|
+
// WS-3b: Distill→source fidelity check (step 10). After a distill proposal,
|
|
296
|
+
// check it against its cited source memories; a contradiction flag forces
|
|
297
|
+
// human review. Default OFF. Only meaningful on `distill` process.
|
|
298
|
+
fidelityCheck: z
|
|
299
|
+
.object({
|
|
300
|
+
enabled: z.boolean().optional(),
|
|
301
|
+
})
|
|
302
|
+
.strict()
|
|
303
|
+
.optional(),
|
|
304
|
+
// #609 — recombine process: minimum related-memory cluster size before an
|
|
305
|
+
// LLM generalization call. Default 3. Only meaningful on `recombine`.
|
|
306
|
+
minClusterSize: z.number().int().min(2).optional(),
|
|
307
|
+
// #609 — recombine process: hard cap on clusters processed per run (one
|
|
308
|
+
// bounded LLM call each). Default 5. Only meaningful on `recombine`.
|
|
309
|
+
maxClustersPerRun: positiveInt.optional(),
|
|
310
|
+
// #632 — recombine process: max members a cluster may contain before it is
|
|
311
|
+
// SKIPPED (drops bland over-broad buckets). When set, largest-first ranking
|
|
312
|
+
// no longer starves tighter clusters. Default UNSET = no cap. Only
|
|
313
|
+
// meaningful on `recombine`.
|
|
314
|
+
maxClusterSize: positiveInt.optional(),
|
|
315
|
+
// #632 — recombine process: tag values that must never form a tag cluster
|
|
316
|
+
// (generic project-wide tags). Default UNSET/[]. Only meaningful on
|
|
317
|
+
// `recombine`.
|
|
318
|
+
excludeTags: z.array(z.string().min(1)).optional(),
|
|
319
|
+
// #609 — recombine process: relatedness signal used to form clusters
|
|
320
|
+
// (tags | graph | both). Clustering is by relatedness, never embedding
|
|
321
|
+
// similarity. Default "tags". Only meaningful on `recombine`.
|
|
322
|
+
relatednessSource: z.enum(["tags", "graph", "both"]).optional(),
|
|
323
|
+
// #609 — recombine process: consecutive re-inductions required before a
|
|
324
|
+
// hypothesis is promoted to a lesson. Default 2. Only meaningful on
|
|
325
|
+
// `recombine`.
|
|
326
|
+
confirmThreshold: z.number().int().min(1).optional(),
|
|
327
|
+
// #615 — procedural process: minimum number of distinct assets sharing the
|
|
328
|
+
// same successful normalized ordered-action sequence before it is compiled
|
|
329
|
+
// into a workflow proposal. Default 3. Only meaningful on `procedural`.
|
|
330
|
+
minRecurrence: z.number().int().min(2).optional(),
|
|
331
|
+
// #615 — procedural process: hard cap on workflow proposals emitted per run
|
|
332
|
+
// (one bounded LLM call each). Default 3. Only meaningful on `procedural`.
|
|
333
|
+
maxProposalsPerRun: positiveInt.optional(),
|
|
334
|
+
// #615 — procedural process: asset type a compiled sequence is emitted as.
|
|
335
|
+
// Reserved; v1 always emits "workflow". Only meaningful on `procedural`.
|
|
336
|
+
emitAs: z.enum(["workflow", "skill"]).optional(),
|
|
337
|
+
// #639 — semantic value-floor filter for the `reflect` process. When
|
|
338
|
+
// enabled, proposals classified as "low-value" by the deterministic noise
|
|
339
|
+
// gate are deferred. DEFAULT OFF (absent / { enabled: false } = pre-#639
|
|
340
|
+
// byte-identical behaviour). Only meaningful on the `reflect` process.
|
|
341
|
+
lowValueFilter: z.object({ enabled: z.boolean().optional() }).strict().optional(),
|
|
342
|
+
// #641 — procedural-aware floor for the `extract` process triage gate.
|
|
343
|
+
// When true, a session must have markers>=1 OR editCommit>=0.5 to pass, even
|
|
344
|
+
// if score>=minScore. DEFAULT OFF (absent/false = pre-#641 byte-identical).
|
|
345
|
+
// Only meaningful on the `extract` process when triage is also enabled.
|
|
346
|
+
proceduralAwareFloor: z.boolean().optional(),
|
|
159
347
|
// Triage process config (only meaningful for the `triage` process)
|
|
160
348
|
applyMode: z.enum(["queue", "promote"]).optional(),
|
|
161
349
|
policy: z.string().min(1).optional(),
|
|
@@ -181,6 +369,9 @@ const ImproveProfileProcessesSchema = z
|
|
|
181
369
|
graphExtraction: ImproveProcessConfigSchema.optional(),
|
|
182
370
|
validation: ImproveProcessConfigSchema.optional(),
|
|
183
371
|
triage: ImproveProcessConfigSchema.optional(),
|
|
372
|
+
proactiveMaintenance: ImproveProcessConfigSchema.optional(),
|
|
373
|
+
recombine: ImproveProcessConfigSchema.optional(),
|
|
374
|
+
procedural: ImproveProcessConfigSchema.optional(),
|
|
184
375
|
})
|
|
185
376
|
.passthrough()
|
|
186
377
|
.superRefine((val, ctx) => {
|
|
@@ -204,6 +395,9 @@ const ImproveProfileProcessesSchema = z
|
|
|
204
395
|
"validation",
|
|
205
396
|
"extract",
|
|
206
397
|
"triage",
|
|
398
|
+
"proactiveMaintenance",
|
|
399
|
+
"recombine",
|
|
400
|
+
"procedural",
|
|
207
401
|
]);
|
|
208
402
|
for (const k of Object.keys(raw)) {
|
|
209
403
|
if (!allowed.has(k)) {
|
|
@@ -221,6 +415,16 @@ export const ImproveProfileConfigSchema = z
|
|
|
221
415
|
processes: ImproveProfileProcessesSchema.optional(),
|
|
222
416
|
autoAccept: nonNegativeNumber.optional(),
|
|
223
417
|
limit: positiveInt.optional(),
|
|
418
|
+
// #616 — bounded multi-cycle phasing. Number of prep->loop->post-loop
|
|
419
|
+
// cycles per run. positiveInt forbids 0/negative. DEFAULT 1 => byte-identical
|
|
420
|
+
// single-pass behavior.
|
|
421
|
+
maxCycles: positiveInt.optional(),
|
|
422
|
+
// #614 — symmetric valence weighting in the eligibility sort. When true,
|
|
423
|
+
// the attention term becomes |valence| MAGNITUDE so BOTH strong positive
|
|
424
|
+
// and strong negative feedback drive attention (utility stays dominant) and
|
|
425
|
+
// strong-signed assets are routed to a fix/reinforce lane. DEFAULT OFF —
|
|
426
|
+
// false/absent preserves the legacy negative-only ranking byte-for-byte.
|
|
427
|
+
symmetricValence: z.boolean().optional(),
|
|
224
428
|
sync: z
|
|
225
429
|
.object({
|
|
226
430
|
enabled: z.boolean().optional(),
|
|
@@ -337,6 +541,7 @@ const SearchGraphBoostSchema = z
|
|
|
337
541
|
export const SearchConfigSchema = z
|
|
338
542
|
.object({
|
|
339
543
|
minScore: nonNegativeNumber.optional(),
|
|
544
|
+
defaultExcludeTypes: z.array(nonEmptyString).optional(),
|
|
340
545
|
curateRerank: z.object({ enabled: z.boolean().optional() }).strict().optional(),
|
|
341
546
|
graphBoost: SearchGraphBoostSchema.optional(),
|
|
342
547
|
})
|
|
@@ -355,10 +560,74 @@ const ImproveUtilityDecaySchema = z
|
|
|
355
560
|
feedbackStabilityBoost: z.number().finite().min(1).optional(),
|
|
356
561
|
})
|
|
357
562
|
.strict();
|
|
563
|
+
// #612 / WS-4 — auto-accept gate calibration + bounded, opt-in per-phase
|
|
564
|
+
// threshold auto-tune. DEFAULT OFF: when absent (or `autoTune: false`) no
|
|
565
|
+
// tuning occurs, so the gate behaves byte-identically to today.
|
|
566
|
+
// WS-4 adds: per-phase persistence (state.db) + auto-tune ceiling default 85.
|
|
567
|
+
const ImproveCalibrationSchema = z
|
|
568
|
+
.object({
|
|
569
|
+
/** Master switch for the bounded threshold auto-tune. Default false (parity). */
|
|
570
|
+
autoTune: z.boolean().optional(),
|
|
571
|
+
/** Lower bound (0-100) the tuned threshold may never drop below. */
|
|
572
|
+
minThreshold: z.number().int().min(0).max(100).optional(),
|
|
573
|
+
/**
|
|
574
|
+
* Upper bound (0-100) the tuned threshold may never rise above.
|
|
575
|
+
* WS-4 default: 85 (prevents gate converging to pure exploitation).
|
|
576
|
+
*/
|
|
577
|
+
maxThreshold: z.number().int().min(0).max(100).optional(),
|
|
578
|
+
/** Maximum adjustment magnitude (points) applied in one tune step. */
|
|
579
|
+
maxStep: positiveInt.optional(),
|
|
580
|
+
/** Minimum acted-on sample count required before any adjustment. */
|
|
581
|
+
minSamples: nonNegativeNumber.optional(),
|
|
582
|
+
/** Target realized accept rate in [0, 1]. Default 0.9. */
|
|
583
|
+
targetAcceptRate: z.number().finite().min(0).max(1).optional(),
|
|
584
|
+
})
|
|
585
|
+
.strict();
|
|
586
|
+
// WS-4 — exploration budget: a fixed fraction of proposals accepted per run
|
|
587
|
+
// regardless of confidence. DEFAULT OFF.
|
|
588
|
+
const ImproveExplorationSchema = z
|
|
589
|
+
.object({
|
|
590
|
+
/**
|
|
591
|
+
* Enable the exploration budget lane. Default false (parity).
|
|
592
|
+
* When true, a fraction of proposals are accepted regardless of confidence.
|
|
593
|
+
*/
|
|
594
|
+
enabled: z.boolean().optional(),
|
|
595
|
+
/**
|
|
596
|
+
* Fraction of proposals per run to accept as exploration [0, 1].
|
|
597
|
+
* Default 0.05 (5%). Clamped to [0, 1] at read time.
|
|
598
|
+
*/
|
|
599
|
+
budgetFraction: z.number().finite().min(0).max(1).optional(),
|
|
600
|
+
})
|
|
601
|
+
.strict();
|
|
602
|
+
const ImproveSalienceSchema = z
|
|
603
|
+
.object({
|
|
604
|
+
/**
|
|
605
|
+
* WS-2 Part-V gate: enable the outcome-weight term in the salience projection.
|
|
606
|
+
* Default false (parity — WS-1 weights w_e=0.30, w_r=0.70 until Part-V confirms
|
|
607
|
+
* no regression). Set to true after running scripts/akm-eval + health report.
|
|
608
|
+
*/
|
|
609
|
+
outcomeWeightEnabled: z.boolean().optional(),
|
|
610
|
+
/**
|
|
611
|
+
* Minimum encoding salience score [0, 1] for a zero-feedback asset to be
|
|
612
|
+
* admitted to the high-salience improve lane (#608).
|
|
613
|
+
* Default 0.75. Set to 1.0 to disable the lane entirely.
|
|
614
|
+
*/
|
|
615
|
+
salienceThreshold: z.number().min(0).max(1).optional(),
|
|
616
|
+
/**
|
|
617
|
+
* Per-run additive replay budget (#610). Up to this many top-salience refs are
|
|
618
|
+
* revisited even with no reactive signal and regardless of cooldown. Additive
|
|
619
|
+
* on top of --limit. Default 0 = no replay.
|
|
620
|
+
*/
|
|
621
|
+
replayBudget: z.number().int().min(0).optional(),
|
|
622
|
+
})
|
|
623
|
+
.strict();
|
|
358
624
|
export const ImproveConfigSchema = z
|
|
359
625
|
.object({
|
|
360
626
|
utilityDecay: ImproveUtilityDecaySchema.optional(),
|
|
361
627
|
eventRetentionDays: nonNegativeNumber.optional(),
|
|
628
|
+
calibration: ImproveCalibrationSchema.optional(),
|
|
629
|
+
exploration: ImproveExplorationSchema.optional(),
|
|
630
|
+
salience: ImproveSalienceSchema.optional(),
|
|
362
631
|
})
|
|
363
632
|
.strict();
|
|
364
633
|
// ── Index / per-pass ────────────────────────────────────────────────────────
|
|
@@ -372,6 +641,7 @@ const GRAPH_EXTRACTION_INCLUDE_TYPES_ALLOWED = [
|
|
|
372
641
|
"lesson",
|
|
373
642
|
"task",
|
|
374
643
|
"wiki",
|
|
644
|
+
"fact",
|
|
375
645
|
];
|
|
376
646
|
const INDEX_PASS_PROVIDER_KEYS = new Set([
|
|
377
647
|
"endpoint",
|
|
@@ -388,6 +658,7 @@ const INDEX_PASS_KNOWN_KEYS = new Set([
|
|
|
388
658
|
"graphExtractionBatchSize",
|
|
389
659
|
"graphExtractionIncludeTypes",
|
|
390
660
|
"memoryInferenceBatchSize",
|
|
661
|
+
"lazyGraphExtraction",
|
|
391
662
|
]);
|
|
392
663
|
/**
|
|
393
664
|
* Per-pass `index.<pass>` entry. Uses preprocess + manual validation so we can
|
|
@@ -414,8 +685,8 @@ export const IndexPassConfigSchema = z.preprocess((raw, ctx) => {
|
|
|
414
685
|
ctx.addIssue({
|
|
415
686
|
code: z.ZodIssueCode.custom,
|
|
416
687
|
message: `Unknown key \`${[...(ctx.path ?? []), key].join(".")}\`. Per-pass entries support \`llm\` ` +
|
|
417
|
-
"(boolean opt-out), `graphExtractionBatchSize`, `graphExtractionIncludeTypes`,
|
|
418
|
-
"`memoryInferenceBatchSize`.",
|
|
688
|
+
"(boolean opt-out), `graphExtractionBatchSize`, `graphExtractionIncludeTypes`, " +
|
|
689
|
+
"`memoryInferenceBatchSize`, and `lazyGraphExtraction`.",
|
|
419
690
|
});
|
|
420
691
|
return raw;
|
|
421
692
|
}
|
|
@@ -434,6 +705,7 @@ export const IndexPassConfigSchema = z.preprocess((raw, ctx) => {
|
|
|
434
705
|
graphExtractionBatchSize: positiveInt.optional(),
|
|
435
706
|
graphExtractionIncludeTypes: z.array(z.enum(GRAPH_EXTRACTION_INCLUDE_TYPES_ALLOWED)).nonempty().optional(),
|
|
436
707
|
memoryInferenceBatchSize: positiveInt.optional(),
|
|
708
|
+
lazyGraphExtraction: z.boolean().optional(),
|
|
437
709
|
})
|
|
438
710
|
.passthrough());
|
|
439
711
|
const MetadataEnhanceSchema = z.object({ enabled: z.boolean().optional() }).strict();
|
|
@@ -249,8 +249,8 @@ function maybeAutoMigrateConfigFile(configPath, text) {
|
|
|
249
249
|
" to preview a dry-run diff: akm config migrate --dry-run --print-diff",
|
|
250
250
|
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
|
|
251
251
|
].join("\n");
|
|
252
|
-
process.stderr
|
|
253
|
-
process.stdout
|
|
252
|
+
process.stderr?.write?.(`${banner}\n`);
|
|
253
|
+
process.stdout?.write?.(`${banner}\n`);
|
|
254
254
|
}
|
|
255
255
|
catch (err) {
|
|
256
256
|
// #461: never return migrated bytes when disk write fails — that triggers
|
package/dist/core/logs-db.js
CHANGED
|
@@ -39,6 +39,7 @@ import fs from "node:fs";
|
|
|
39
39
|
import path from "node:path";
|
|
40
40
|
import { openDatabase } from "../storage/database.js";
|
|
41
41
|
import { runMigrations as runSqliteMigrations } from "../storage/engines/sqlite-migrations.js";
|
|
42
|
+
import { applyStandardPragmas } from "../storage/sqlite-pragmas.js";
|
|
42
43
|
import { getDataDir } from "./paths.js";
|
|
43
44
|
import { getStateDbPath } from "./state-db.js";
|
|
44
45
|
// ── Path helper ──────────────────────────────────────────────────────────────
|
|
@@ -75,9 +76,9 @@ export function openLogsDatabase(dbPath) {
|
|
|
75
76
|
fs.mkdirSync(dir, { recursive: true });
|
|
76
77
|
}
|
|
77
78
|
const db = openDatabase(resolvedPath);
|
|
78
|
-
// PRAGMAs must run before any DDL or DML.
|
|
79
|
-
db
|
|
80
|
-
db
|
|
79
|
+
// PRAGMAs must run before any DDL or DML. foreignKeys:false preserves this
|
|
80
|
+
// opener's historical behaviour — logs.db has never enforced foreign keys.
|
|
81
|
+
applyStandardPragmas(db, { dataDir: dir, foreignKeys: false });
|
|
81
82
|
runMigrations(db);
|
|
82
83
|
return db;
|
|
83
84
|
}
|
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
|
+
}
|