akm-cli 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +66 -0
- package/dist/{cli.js → src/cli.js} +712 -34
- package/dist/{commands → src/commands}/config-cli.js +47 -4
- package/dist/src/commands/distill.js +283 -0
- package/dist/src/commands/events.js +108 -0
- package/dist/src/commands/history.js +191 -0
- package/dist/{commands → src/commands}/installed-stashes.js +1 -1
- package/dist/src/commands/proposal.js +119 -0
- package/dist/src/commands/propose.js +171 -0
- package/dist/src/commands/reflect.js +193 -0
- package/dist/{commands → src/commands}/registry-search.js +71 -7
- package/dist/{commands → src/commands}/remember.js +12 -0
- package/dist/{commands → src/commands}/search.js +104 -4
- package/dist/{commands → src/commands}/self-update.js +4 -3
- package/dist/{commands → src/commands}/show.js +73 -0
- package/dist/{commands → src/commands}/source-add.js +5 -1
- package/dist/{commands → src/commands}/source-manage.js +7 -1
- package/dist/{core → src/core}/asset-ref.js +5 -5
- package/dist/{core → src/core}/asset-spec.js +12 -0
- package/dist/{core → src/core}/common.js +1 -1
- package/dist/{core → src/core}/config.js +203 -121
- package/dist/{core → src/core}/errors.js +4 -0
- package/dist/src/core/events.js +239 -0
- package/dist/src/core/lesson-lint.js +86 -0
- package/dist/src/core/proposals.js +406 -0
- package/dist/src/core/warn.js +72 -0
- package/dist/{core → src/core}/write-source.js +80 -5
- package/dist/{indexer → src/indexer}/db-search.js +114 -24
- package/dist/{indexer → src/indexer}/db.js +76 -23
- package/dist/{indexer → src/indexer}/file-context.js +0 -3
- package/dist/src/indexer/graph-boost.js +179 -0
- package/dist/src/indexer/graph-extraction.js +212 -0
- package/dist/{indexer → src/indexer}/indexer.js +88 -7
- package/dist/{indexer → src/indexer}/matchers.js +1 -1
- package/dist/src/indexer/memory-inference.js +263 -0
- package/dist/{indexer → src/indexer}/metadata.js +111 -3
- package/dist/{indexer → src/indexer}/search-source.js +4 -2
- package/dist/src/integrations/agent/config.js +292 -0
- package/dist/src/integrations/agent/detect.js +94 -0
- package/dist/src/integrations/agent/index.js +17 -0
- package/dist/src/integrations/agent/profiles.js +65 -0
- package/dist/src/integrations/agent/prompts.js +167 -0
- package/dist/src/integrations/agent/spawn.js +272 -0
- package/dist/{integrations → src/integrations}/github.js +9 -3
- package/dist/{integrations → src/integrations}/lockfile.js +0 -26
- package/dist/{llm → src/llm}/client.js +33 -2
- package/dist/{llm → src/llm}/embedders/remote.js +37 -3
- package/dist/src/llm/feature-gate.js +108 -0
- package/dist/src/llm/graph-extract.js +107 -0
- package/dist/src/llm/index-passes.js +35 -0
- package/dist/src/llm/memory-infer.js +86 -0
- package/dist/{output → src/output}/cli-hints.js +15 -2
- package/dist/{output → src/output}/renderers.js +63 -2
- package/dist/src/output/shapes.js +523 -0
- package/dist/src/output/text.js +1116 -0
- package/dist/{registry → src/registry}/build-index.js +19 -8
- package/dist/{registry → src/registry}/factory.js +0 -8
- package/dist/{registry → src/registry}/providers/static-index.js +6 -3
- package/dist/{registry → src/registry}/resolve.js +68 -2
- package/dist/{setup → src/setup}/setup.js +52 -5
- package/dist/{sources → src/sources}/providers/git.js +7 -15
- package/dist/{wiki → src/wiki}/wiki.js +54 -6
- package/dist/{workflows → src/workflows}/runs.js +37 -3
- package/dist/tests/add-website-source.test.js +119 -0
- package/dist/tests/agent/agent-config-loader.test.js +70 -0
- package/dist/tests/agent/agent-config.test.js +221 -0
- package/dist/tests/agent/agent-detect.test.js +100 -0
- package/dist/tests/agent/agent-spawn.test.js +234 -0
- package/dist/tests/agent-output.test.js +186 -0
- package/dist/tests/architecture/agent-no-llm-sdk-guard.test.js +103 -0
- package/dist/tests/architecture/agent-spawn-seam.test.js +193 -0
- package/dist/tests/architecture/llm-stateless-seam.test.js +112 -0
- package/dist/tests/asset-ref.test.js +192 -0
- package/dist/tests/asset-registry.test.js +103 -0
- package/dist/tests/asset-spec.test.js +241 -0
- package/dist/tests/bench/attribution.test.js +996 -0
- package/dist/tests/bench/cleanup-sigint.test.js +83 -0
- package/dist/tests/bench/cleanup.js +234 -0
- package/dist/tests/bench/cleanup.test.js +166 -0
- package/dist/tests/bench/cli.js +1018 -0
- package/dist/tests/bench/cli.test.js +445 -0
- package/dist/tests/bench/compare.test.js +556 -0
- package/dist/tests/bench/corpus.js +317 -0
- package/dist/tests/bench/corpus.test.js +258 -0
- package/dist/tests/bench/doctor.js +525 -0
- package/dist/tests/bench/driver.js +401 -0
- package/dist/tests/bench/driver.test.js +584 -0
- package/dist/tests/bench/environment.js +233 -0
- package/dist/tests/bench/environment.test.js +199 -0
- package/dist/tests/bench/evolve-metrics.js +179 -0
- package/dist/tests/bench/evolve-metrics.test.js +187 -0
- package/dist/tests/bench/evolve.js +647 -0
- package/dist/tests/bench/evolve.test.js +624 -0
- package/dist/tests/bench/failure-modes.test.js +349 -0
- package/dist/tests/bench/feedback-integrity.test.js +457 -0
- package/dist/tests/bench/leakage.test.js +228 -0
- package/dist/tests/bench/learning-curve.test.js +134 -0
- package/dist/tests/bench/metrics.js +2395 -0
- package/dist/tests/bench/metrics.test.js +1150 -0
- package/dist/tests/bench/no-os-tmpdir-invariant.test.js +43 -0
- package/dist/tests/bench/opencode-config.js +194 -0
- package/dist/tests/bench/opencode-config.test.js +370 -0
- package/dist/tests/bench/report.js +1885 -0
- package/dist/tests/bench/report.test.js +1038 -0
- package/dist/tests/bench/run-config.js +355 -0
- package/dist/tests/bench/run-config.test.js +298 -0
- package/dist/tests/bench/run-curate-test.js +32 -0
- package/dist/tests/bench/run-failing-tasks.js +56 -0
- package/dist/tests/bench/run-full-bench.js +51 -0
- package/dist/tests/bench/run-items36-targeted.js +69 -0
- package/dist/tests/bench/run-nano-quick.js +42 -0
- package/dist/tests/bench/run-waveg-targeted.js +62 -0
- package/dist/tests/bench/runner.js +699 -0
- package/dist/tests/bench/runner.test.js +958 -0
- package/dist/tests/bench/search-bridge.test.js +331 -0
- package/dist/tests/bench/tmp.js +131 -0
- package/dist/tests/bench/trajectory.js +116 -0
- package/dist/tests/bench/trajectory.test.js +127 -0
- package/dist/tests/bench/verifier.js +114 -0
- package/dist/tests/bench/verifier.test.js +118 -0
- package/dist/tests/bench/workflow-evaluator.js +557 -0
- package/dist/tests/bench/workflow-evaluator.test.js +421 -0
- package/dist/tests/bench/workflow-spec.js +345 -0
- package/dist/tests/bench/workflow-spec.test.js +363 -0
- package/dist/tests/bench/workflow-trace.js +472 -0
- package/dist/tests/bench/workflow-trace.test.js +254 -0
- package/dist/tests/benchmark-search-quality.js +536 -0
- package/dist/tests/benchmark-suite.js +1441 -0
- package/dist/tests/capture-cli.test.js +112 -0
- package/dist/tests/cli-errors.test.js +204 -0
- package/dist/tests/commands/events.test.js +370 -0
- package/dist/tests/commands/history.test.js +418 -0
- package/dist/tests/commands/import.test.js +103 -0
- package/dist/tests/commands/proposal-cli.test.js +209 -0
- package/dist/tests/commands/reflect-propose-cli.test.js +333 -0
- package/dist/tests/commands/remember.test.js +97 -0
- package/dist/tests/commands/scope-flags.test.js +300 -0
- package/dist/tests/commands/search.test.js +537 -0
- package/dist/tests/commands/show-indexer-parity.test.js +117 -0
- package/dist/tests/commands/show.test.js +294 -0
- package/dist/tests/common.test.js +266 -0
- package/dist/tests/completions.test.js +142 -0
- package/dist/tests/config-cli.test.js +193 -0
- package/dist/tests/config-llm-features.test.js +139 -0
- package/dist/tests/config.test.js +569 -0
- package/dist/tests/contracts/migration-baseline.test.js +43 -0
- package/dist/tests/contracts/reflect-propose-envelope.test.js +139 -0
- package/dist/tests/contracts/spec-helpers.js +46 -0
- package/dist/tests/contracts/v1-spec-section-11-proposal-queue.test.js +228 -0
- package/dist/tests/contracts/v1-spec-section-12-agent-config.test.js +56 -0
- package/dist/tests/contracts/v1-spec-section-13-lesson-type.test.js +34 -0
- package/dist/tests/contracts/v1-spec-section-14-llm-features.test.js +94 -0
- package/dist/tests/contracts/v1-spec-section-4-1-asset-types.test.js +39 -0
- package/dist/tests/contracts/v1-spec-section-4-2-quality-rules.test.js +44 -0
- package/dist/tests/contracts/v1-spec-section-5-configuration.test.js +47 -0
- package/dist/tests/contracts/v1-spec-section-6-orchestration.test.js +40 -0
- package/dist/tests/contracts/v1-spec-section-7-module-layout.test.js +58 -0
- package/dist/tests/contracts/v1-spec-section-8-extension-points.test.js +34 -0
- package/dist/tests/contracts/v1-spec-section-9-4-cli-surface.test.js +75 -0
- package/dist/tests/contracts/v1-spec-section-9-7-llm-agent-boundary.test.js +36 -0
- package/dist/tests/core/write-source.test.js +366 -0
- package/dist/tests/curate-command.test.js +87 -0
- package/dist/tests/db-scoring.test.js +201 -0
- package/dist/tests/db.test.js +654 -0
- package/dist/tests/distill-cli-flag.test.js +208 -0
- package/dist/tests/distill.test.js +515 -0
- package/dist/tests/docker-install.test.js +120 -0
- package/dist/tests/e2e.test.js +1419 -0
- package/dist/tests/embedder.test.js +340 -0
- package/dist/tests/embedding-model-config.test.js +379 -0
- package/dist/tests/feedback-command.test.js +172 -0
- package/dist/tests/file-context.test.js +552 -0
- package/dist/tests/fixtures/scripts/git/summarize-diff.js +9 -0
- package/dist/tests/fixtures/scripts/lint/eslint-check.js +7 -0
- package/dist/tests/fixtures/stashes/load.js +166 -0
- package/dist/tests/fixtures/stashes/load.test.js +97 -0
- package/dist/tests/fixtures/stashes/ranking-baseline/scripts/mem0-search.js +12 -0
- package/dist/tests/frontmatter.test.js +190 -0
- package/dist/tests/fts-field-weighting.test.js +254 -0
- package/dist/tests/fuzzy-search.test.js +230 -0
- package/dist/tests/git-provider-clone.test.js +45 -0
- package/dist/tests/github.test.js +161 -0
- package/dist/tests/graph-boost-ranking.test.js +305 -0
- package/dist/tests/graph-extraction.test.js +282 -0
- package/dist/tests/helpers/usage-events.js +8 -0
- package/dist/tests/index-pass-llm.test.js +161 -0
- package/dist/tests/indexer.test.js +570 -0
- package/dist/tests/info-command.test.js +166 -0
- package/dist/tests/init.test.js +69 -0
- package/dist/tests/install-script.test.js +246 -0
- package/dist/tests/integration/agent-real-profile.test.js +94 -0
- package/dist/tests/issue-36-repro.test.js +304 -0
- package/dist/tests/issues-191-194.test.js +160 -0
- package/dist/tests/lesson-lint.test.js +111 -0
- package/dist/tests/llm-client.test.js +115 -0
- package/dist/tests/llm-feature-gate.test.js +151 -0
- package/dist/tests/llm.test.js +139 -0
- package/dist/tests/lockfile.test.js +216 -0
- package/dist/tests/manifest.test.js +205 -0
- package/dist/tests/markdown.test.js +126 -0
- package/dist/tests/matchers-unit.test.js +189 -0
- package/dist/tests/memory-inference.test.js +299 -0
- package/dist/tests/merge-scoring.test.js +136 -0
- package/dist/tests/metadata.test.js +313 -0
- package/dist/tests/migration-help.test.js +89 -0
- package/dist/tests/origin-resolve.test.js +124 -0
- package/dist/tests/output-baseline.test.js +218 -0
- package/dist/tests/output-shapes-unit.test.js +478 -0
- package/dist/tests/parallel-search.test.js +272 -0
- package/dist/tests/parameter-metadata.test.js +365 -0
- package/dist/tests/paths.test.js +177 -0
- package/dist/tests/progressive-disclosure.test.js +280 -0
- package/dist/tests/proposals.test.js +279 -0
- package/dist/tests/proposed-quality.test.js +271 -0
- package/dist/tests/provider-registry.test.js +32 -0
- package/dist/tests/ranking-regression.test.js +548 -0
- package/dist/tests/reflect-propose.test.js +455 -0
- package/dist/tests/registry-build-index.test.js +394 -0
- package/dist/tests/registry-cli.test.js +290 -0
- package/dist/tests/registry-index-v2.test.js +430 -0
- package/dist/tests/registry-install.test.js +728 -0
- package/dist/tests/registry-providers/parity.test.js +189 -0
- package/dist/tests/registry-providers/skills-sh.test.js +309 -0
- package/dist/tests/registry-providers/static-index.test.js +238 -0
- package/dist/tests/registry-resolve.test.js +126 -0
- package/dist/tests/registry-search.test.js +923 -0
- package/dist/tests/remember-frontmatter.test.js +378 -0
- package/dist/tests/remember-unit.test.js +123 -0
- package/dist/tests/ripgrep-install.test.js +251 -0
- package/dist/tests/ripgrep-resolve.test.js +108 -0
- package/dist/tests/ripgrep.test.js +163 -0
- package/dist/tests/save-command.test.js +94 -0
- package/dist/tests/save-trust-qa-fixes.test.js +270 -0
- package/dist/tests/scoring-pipeline.test.js +648 -0
- package/dist/tests/search-include-proposed-cli.test.js +118 -0
- package/dist/tests/self-update.test.js +442 -0
- package/dist/tests/semantic-search-e2e.test.js +512 -0
- package/dist/tests/semantic-status.test.js +471 -0
- package/dist/tests/setup-run.integration.js +877 -0
- package/dist/tests/setup-wizard.test.js +198 -0
- package/dist/tests/setup.test.js +131 -0
- package/dist/tests/source-add.test.js +11 -0
- package/dist/tests/source-clone.test.js +254 -0
- package/dist/tests/source-manage.test.js +366 -0
- package/dist/tests/source-providers/filesystem.test.js +82 -0
- package/dist/tests/source-providers/git.test.js +252 -0
- package/dist/tests/source-providers/website.test.js +128 -0
- package/dist/tests/source-qa-fixes.test.js +286 -0
- package/dist/tests/source-registry.test.js +350 -0
- package/dist/tests/source-resolve.test.js +100 -0
- package/dist/tests/source-source.test.js +281 -0
- package/dist/tests/source.test.js +533 -0
- package/dist/tests/tar-utils-scan.test.js +73 -0
- package/dist/tests/toggle-components.test.js +73 -0
- package/dist/tests/usage-telemetry.test.js +265 -0
- package/dist/tests/utility-scoring.test.js +558 -0
- package/dist/tests/vault-load-error.test.js +78 -0
- package/dist/tests/vault-qa-fixes.test.js +194 -0
- package/dist/tests/vault.test.js +429 -0
- package/dist/tests/vector-search.test.js +608 -0
- package/dist/tests/walker.test.js +252 -0
- package/dist/tests/wave2-cluster-bc.test.js +228 -0
- package/dist/tests/wave2-cluster-d.test.js +180 -0
- package/dist/tests/wave2-cluster-e.test.js +179 -0
- package/dist/tests/wiki-qa-fixes.test.js +270 -0
- package/dist/tests/wiki.test.js +529 -0
- package/dist/tests/workflow-cli.test.js +271 -0
- package/dist/tests/workflow-markdown.test.js +171 -0
- package/dist/tests/workflow-path-escape.test.js +132 -0
- package/dist/tests/workflow-qa-fixes.test.js +395 -0
- package/dist/tests/workflows/indexer-rejection.test.js +213 -0
- package/docs/README.md +8 -0
- package/docs/migration/release-notes/0.7.0.md +244 -0
- package/package.json +2 -2
- package/dist/core/warn.js +0 -27
- package/dist/output/shapes.js +0 -212
- package/dist/output/text.js +0 -520
- /package/dist/{commands → src/commands}/completions.js +0 -0
- /package/dist/{commands → src/commands}/curate.js +0 -0
- /package/dist/{commands → src/commands}/info.js +0 -0
- /package/dist/{commands → src/commands}/init.js +0 -0
- /package/dist/{commands → src/commands}/install-audit.js +0 -0
- /package/dist/{commands → src/commands}/migration-help.js +0 -0
- /package/dist/{commands → src/commands}/source-clone.js +0 -0
- /package/dist/{commands → src/commands}/vault.js +0 -0
- /package/dist/{core → src/core}/asset-registry.js +0 -0
- /package/dist/{core → src/core}/frontmatter.js +0 -0
- /package/dist/{core → src/core}/markdown.js +0 -0
- /package/dist/{core → src/core}/paths.js +0 -0
- /package/dist/{indexer → src/indexer}/manifest.js +0 -0
- /package/dist/{indexer → src/indexer}/search-fields.js +0 -0
- /package/dist/{indexer → src/indexer}/semantic-status.js +0 -0
- /package/dist/{indexer → src/indexer}/usage-events.js +0 -0
- /package/dist/{indexer → src/indexer}/walker.js +0 -0
- /package/dist/{llm → src/llm}/embedder.js +0 -0
- /package/dist/{llm → src/llm}/embedders/cache.js +0 -0
- /package/dist/{llm → src/llm}/embedders/local.js +0 -0
- /package/dist/{llm → src/llm}/embedders/types.js +0 -0
- /package/dist/{llm → src/llm}/metadata-enhance.js +0 -0
- /package/dist/{output → src/output}/context.js +0 -0
- /package/dist/{registry → src/registry}/create-provider-registry.js +0 -0
- /package/dist/{registry → src/registry}/origin-resolve.js +0 -0
- /package/dist/{registry → src/registry}/providers/index.js +0 -0
- /package/dist/{registry → src/registry}/providers/skills-sh.js +0 -0
- /package/dist/{registry → src/registry}/providers/types.js +0 -0
- /package/dist/{registry → src/registry}/types.js +0 -0
- /package/dist/{setup → src/setup}/detect.js +0 -0
- /package/dist/{setup → src/setup}/ripgrep-install.js +0 -0
- /package/dist/{setup → src/setup}/ripgrep-resolve.js +0 -0
- /package/dist/{setup → src/setup}/steps.js +0 -0
- /package/dist/{sources → src/sources}/include.js +0 -0
- /package/dist/{sources → src/sources}/provider-factory.js +0 -0
- /package/dist/{sources → src/sources}/provider.js +0 -0
- /package/dist/{sources → src/sources}/providers/filesystem.js +0 -0
- /package/dist/{sources → src/sources}/providers/index.js +0 -0
- /package/dist/{sources → src/sources}/providers/install-types.js +0 -0
- /package/dist/{sources → src/sources}/providers/npm.js +0 -0
- /package/dist/{sources → src/sources}/providers/provider-utils.js +0 -0
- /package/dist/{sources → src/sources}/providers/sync-from-ref.js +0 -0
- /package/dist/{sources → src/sources}/providers/tar-utils.js +0 -0
- /package/dist/{sources → src/sources}/providers/website.js +0 -0
- /package/dist/{sources → src/sources}/resolve.js +0 -0
- /package/dist/{sources → src/sources}/types.js +0 -0
- /package/dist/{templates → src/templates}/wiki-templates.js +0 -0
- /package/dist/{version.js → src/version.js} +0 -0
- /package/dist/{workflows → src/workflows}/authoring.js +0 -0
- /package/dist/{workflows → src/workflows}/cli.js +0 -0
- /package/dist/{workflows → src/workflows}/db.js +0 -0
- /package/dist/{workflows → src/workflows}/document-cache.js +0 -0
- /package/dist/{workflows → src/workflows}/parser.js +0 -0
- /package/dist/{workflows → src/workflows}/renderer.js +0 -0
- /package/dist/{workflows → src/workflows}/schema.js +0 -0
- /package/dist/{workflows → src/workflows}/validator.js +0 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append-only events stream — `events.jsonl` (#204).
|
|
3
|
+
*
|
|
4
|
+
* Every mutating CLI verb funnels through `appendEvent` so external
|
|
5
|
+
* observers (sync, replication, audit, dashboards) can react to stash
|
|
6
|
+
* changes by tailing a single file. The file is plain newline-delimited
|
|
7
|
+
* JSON; each line is a self-contained event envelope.
|
|
8
|
+
*
|
|
9
|
+
* The helper is the only thing in akm that writes to events.jsonl. It
|
|
10
|
+
* accepts injectable `now()` and `path` so tests can pin time and use a
|
|
11
|
+
* tmpdir without any global mutation.
|
|
12
|
+
*
|
|
13
|
+
* Format (each line):
|
|
14
|
+
* { "schemaVersion": 1, "id": <number>, "ts": "<ISO>",
|
|
15
|
+
* "eventType": "<verb>", "ref"?: "<asset-ref>", ... }
|
|
16
|
+
*
|
|
17
|
+
* - `id` is a monotonic integer per file. We use the file's pre-write
|
|
18
|
+
* byte length as a durable cursor for `--since` (stable across processes
|
|
19
|
+
* because every appender holds an O_APPEND write). Callers can also pass
|
|
20
|
+
* a string ISO timestamp to `--since` and we filter by `ts >= since`.
|
|
21
|
+
* - `ts` is ISO-8601 (UTC, millisecond precision).
|
|
22
|
+
*
|
|
23
|
+
* The event `id` is derived at read time (line index) — the file itself
|
|
24
|
+
* is the source of truth, so the writer never has to coordinate with a
|
|
25
|
+
* counter. Tail consumers can persist a byte offset (durable cursor).
|
|
26
|
+
*/
|
|
27
|
+
import fs from "node:fs";
|
|
28
|
+
import path from "node:path";
|
|
29
|
+
import { getCacheDir } from "./paths";
|
|
30
|
+
/**
|
|
31
|
+
* Default events.jsonl location: `<cacheDir>/events.jsonl`.
|
|
32
|
+
*
|
|
33
|
+
* Env-isolation caveat: `getCacheDir()` reads `XDG_CACHE_HOME` at the time of
|
|
34
|
+
* each call. Two cooperating processes (e.g. one writing events, one tailing)
|
|
35
|
+
* MUST inherit the same `XDG_CACHE_HOME` or they will read/write different
|
|
36
|
+
* `events.jsonl` files. This is the same env-isolation behaviour as the rest
|
|
37
|
+
* of akm — config, indexes, and caches all key off XDG paths — so set
|
|
38
|
+
* `XDG_CACHE_HOME` consistently across processes that share the events bus.
|
|
39
|
+
*/
|
|
40
|
+
export function getEventsPath() {
|
|
41
|
+
return path.join(getCacheDir(), "events.jsonl");
|
|
42
|
+
}
|
|
43
|
+
function resolvePath(ctx) {
|
|
44
|
+
return ctx?.filePath ?? getEventsPath();
|
|
45
|
+
}
|
|
46
|
+
function resolveNow(ctx) {
|
|
47
|
+
return ctx?.now ?? Date.now;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Append a single event. Best-effort: a write failure is logged once to
|
|
51
|
+
* stderr but never propagates — observability must not break mutation.
|
|
52
|
+
*
|
|
53
|
+
* The id field is intentionally omitted on write (the line index is the
|
|
54
|
+
* id; the reader assigns it). Keeping it off the wire avoids a coordination
|
|
55
|
+
* step between concurrent appenders.
|
|
56
|
+
*/
|
|
57
|
+
export function appendEvent(input, ctx) {
|
|
58
|
+
const filePath = resolvePath(ctx);
|
|
59
|
+
const now = resolveNow(ctx);
|
|
60
|
+
const ts = new Date(now()).toISOString();
|
|
61
|
+
const envelope = {
|
|
62
|
+
schemaVersion: 1,
|
|
63
|
+
ts,
|
|
64
|
+
eventType: input.eventType,
|
|
65
|
+
...(input.ref !== undefined ? { ref: input.ref } : {}),
|
|
66
|
+
...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
|
|
67
|
+
};
|
|
68
|
+
const line = `${JSON.stringify(envelope)}\n`;
|
|
69
|
+
try {
|
|
70
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
71
|
+
// O_APPEND guarantees atomic appends ≤ PIPE_BUF (4 KiB on Linux); our
|
|
72
|
+
// events are well under that ceiling, so concurrent processes can write
|
|
73
|
+
// safely without locking. `appendFileSync` opens with `'a'` which sets
|
|
74
|
+
// O_APPEND.
|
|
75
|
+
fs.appendFileSync(filePath, line, { encoding: "utf8" });
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
// Best-effort: events stream failures must not break the mutating verb.
|
|
79
|
+
// Surface once to stderr so operators can diagnose.
|
|
80
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
81
|
+
process.stderr.write(`akm: events.jsonl append failed (${message})\n`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Read all events matching the filter. Returns a `nextOffset` that callers
|
|
86
|
+
* can persist between processes for monotonic resumption — `sinceOffset`
|
|
87
|
+
* is the durable cursor referenced in the acceptance criteria.
|
|
88
|
+
*/
|
|
89
|
+
export function readEvents(options = {}, ctx) {
|
|
90
|
+
const filePath = resolvePath(ctx);
|
|
91
|
+
if (!fs.existsSync(filePath)) {
|
|
92
|
+
return { events: [], nextOffset: 0 };
|
|
93
|
+
}
|
|
94
|
+
const stat = fs.statSync(filePath);
|
|
95
|
+
const startOffset = options.sinceOffset && options.sinceOffset > 0 ? options.sinceOffset : 0;
|
|
96
|
+
if (startOffset >= stat.size) {
|
|
97
|
+
return { events: [], nextOffset: stat.size };
|
|
98
|
+
}
|
|
99
|
+
const fd = fs.openSync(filePath, "r");
|
|
100
|
+
try {
|
|
101
|
+
const length = stat.size - startOffset;
|
|
102
|
+
const buf = Buffer.alloc(length);
|
|
103
|
+
fs.readSync(fd, buf, 0, length, startOffset);
|
|
104
|
+
const text = buf.toString("utf8");
|
|
105
|
+
const events = parseEventLines(text, options, startOffset);
|
|
106
|
+
return { events, nextOffset: stat.size };
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
fs.closeSync(fd);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function parseEventLines(text, options, startOffset) {
|
|
113
|
+
// Each line that ends with \n is a complete event. A trailing partial
|
|
114
|
+
// line (no terminating \n) is ignored — the next read will pick it up
|
|
115
|
+
// once it is fully written.
|
|
116
|
+
const out = [];
|
|
117
|
+
let lineStart = 0;
|
|
118
|
+
// The envelope id is the 1-based line index across the whole file. We
|
|
119
|
+
// approximate that here as the line index from the start of the read
|
|
120
|
+
// window plus a synthetic offset — for callers using `--since`, the
|
|
121
|
+
// absolute id is less useful than the byte cursor anyway. To keep ids
|
|
122
|
+
// monotonic across reads we use absolute byte position as a stable
|
|
123
|
+
// surrogate identifier.
|
|
124
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
125
|
+
if (text.charCodeAt(i) !== 10 /* \n */)
|
|
126
|
+
continue;
|
|
127
|
+
const line = text.slice(lineStart, i);
|
|
128
|
+
const absStart = startOffset + lineStart;
|
|
129
|
+
lineStart = i + 1;
|
|
130
|
+
if (!line.trim())
|
|
131
|
+
continue;
|
|
132
|
+
let parsed;
|
|
133
|
+
try {
|
|
134
|
+
parsed = JSON.parse(line);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// Skip malformed lines — better than crashing the read pipeline.
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const envelope = {
|
|
141
|
+
schemaVersion: 1,
|
|
142
|
+
id: absStart,
|
|
143
|
+
ts: typeof parsed.ts === "string" ? parsed.ts : "",
|
|
144
|
+
eventType: typeof parsed.eventType === "string" ? parsed.eventType : "unknown",
|
|
145
|
+
...(typeof parsed.ref === "string" ? { ref: parsed.ref } : {}),
|
|
146
|
+
...(parsed.metadata !== undefined ? { metadata: parsed.metadata } : {}),
|
|
147
|
+
};
|
|
148
|
+
if (!matchesFilter(envelope, options))
|
|
149
|
+
continue;
|
|
150
|
+
out.push(envelope);
|
|
151
|
+
}
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
function matchesFilter(envelope, options) {
|
|
155
|
+
if (options.type && envelope.eventType !== options.type)
|
|
156
|
+
return false;
|
|
157
|
+
if (options.ref && envelope.ref !== options.ref)
|
|
158
|
+
return false;
|
|
159
|
+
if (options.since && envelope.ts && envelope.ts < options.since)
|
|
160
|
+
return false;
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Follow events.jsonl. Polls at `intervalMs` (default 75ms) and emits
|
|
165
|
+
* every new event to `onEvent`. Resolves when `signal` aborts, when
|
|
166
|
+
* `maxEvents` events have been observed, or when `maxDurationMs` elapses.
|
|
167
|
+
*
|
|
168
|
+
* The polling cursor is byte-offset based, so concurrent writers cannot
|
|
169
|
+
* cause skips: between two reads we always pick up everything appended
|
|
170
|
+
* since the last `nextOffset`.
|
|
171
|
+
*/
|
|
172
|
+
export async function tailEvents(options = {}, ctx) {
|
|
173
|
+
const intervalMs = options.intervalMs ?? 75;
|
|
174
|
+
const collected = [];
|
|
175
|
+
let cursor = options.sinceOffset ?? 0;
|
|
176
|
+
// Seed the cursor: if the caller passed --since (timestamp) but no
|
|
177
|
+
// sinceOffset, do an initial filtered read so they see history before
|
|
178
|
+
// we start polling. This matches the documented behaviour of `tail
|
|
179
|
+
// --since`: emit existing events that match, then follow.
|
|
180
|
+
if (options.sinceOffset === undefined) {
|
|
181
|
+
const initial = readEvents({ since: options.since, type: options.type, ref: options.ref }, ctx);
|
|
182
|
+
for (const event of initial.events) {
|
|
183
|
+
collected.push(event);
|
|
184
|
+
options.onEvent?.(event);
|
|
185
|
+
if (options.maxEvents !== undefined && collected.length >= options.maxEvents) {
|
|
186
|
+
return { events: collected, nextOffset: initial.nextOffset, reason: "maxEvents" };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
cursor = initial.nextOffset;
|
|
190
|
+
}
|
|
191
|
+
const startedAt = Date.now();
|
|
192
|
+
return new Promise((resolve) => {
|
|
193
|
+
let resolved = false;
|
|
194
|
+
let timer;
|
|
195
|
+
function finish(reason) {
|
|
196
|
+
if (resolved)
|
|
197
|
+
return;
|
|
198
|
+
resolved = true;
|
|
199
|
+
if (timer)
|
|
200
|
+
clearInterval(timer);
|
|
201
|
+
resolve({ events: collected, nextOffset: cursor, reason });
|
|
202
|
+
}
|
|
203
|
+
function tick() {
|
|
204
|
+
try {
|
|
205
|
+
const result = readEvents({ sinceOffset: cursor, type: options.type, ref: options.ref }, ctx);
|
|
206
|
+
cursor = result.nextOffset;
|
|
207
|
+
for (const event of result.events) {
|
|
208
|
+
// Apply --since filter inside the polling loop too — the cursor is
|
|
209
|
+
// byte-offset so it can hand us events the user filtered out.
|
|
210
|
+
if (options.since && event.ts && event.ts < options.since)
|
|
211
|
+
continue;
|
|
212
|
+
collected.push(event);
|
|
213
|
+
options.onEvent?.(event);
|
|
214
|
+
if (options.maxEvents !== undefined && collected.length >= options.maxEvents) {
|
|
215
|
+
finish("maxEvents");
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
// Non-fatal: stay in the loop.
|
|
222
|
+
}
|
|
223
|
+
if (options.maxDurationMs !== undefined && Date.now() - startedAt >= options.maxDurationMs) {
|
|
224
|
+
finish("maxDuration");
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (options.signal) {
|
|
228
|
+
if (options.signal.aborted) {
|
|
229
|
+
finish("signal");
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
options.signal.addEventListener("abort", () => finish("signal"), { once: true });
|
|
233
|
+
}
|
|
234
|
+
timer = setInterval(tick, intervalMs);
|
|
235
|
+
// Run one tick immediately so callers don't have to wait an interval
|
|
236
|
+
// for events written in the same tick as the tail starts.
|
|
237
|
+
tick();
|
|
238
|
+
});
|
|
239
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic frontmatter lint for `lesson` assets (v1 spec §13).
|
|
3
|
+
*
|
|
4
|
+
* The contract is fixed:
|
|
5
|
+
*
|
|
6
|
+
* - Required: `description` — a non-empty single-line string describing
|
|
7
|
+
* what the lesson teaches.
|
|
8
|
+
* - Required: `when_to_use` — a non-empty single-line string describing
|
|
9
|
+
* the trigger that should make a caller apply the lesson.
|
|
10
|
+
*
|
|
11
|
+
* Lint produces structured findings rather than throwing so callers can
|
|
12
|
+
* batch-validate (e.g. `akm proposal accept` over a queue) and surface every
|
|
13
|
+
* violation in a single pass. A strict wrapper (`assertLessonValid`) throws
|
|
14
|
+
* a `UsageError` for call sites that want a fail-fast contract — most
|
|
15
|
+
* notably the proposal-accept path described in v1 spec §13.1.
|
|
16
|
+
*
|
|
17
|
+
* The lint is intentionally side-effect free and does not import the indexer
|
|
18
|
+
* or filesystem walker; it operates on a single file path + raw string. This
|
|
19
|
+
* lets it run from any code path (CLI, proposal-accept, asset-spec tests)
|
|
20
|
+
* without dragging in the rest of the runtime.
|
|
21
|
+
*/
|
|
22
|
+
import fs from "node:fs";
|
|
23
|
+
import { UsageError } from "./errors";
|
|
24
|
+
import { parseFrontmatter } from "./frontmatter";
|
|
25
|
+
function isNonEmptyString(value) {
|
|
26
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Lint a lesson given its raw markdown source.
|
|
30
|
+
*
|
|
31
|
+
* `pathForMessages` is woven into every finding's message so callers can
|
|
32
|
+
* surface the offending file in CLI/proposal flows without having to map
|
|
33
|
+
* findings back to paths separately.
|
|
34
|
+
*/
|
|
35
|
+
export function lintLessonContent(raw, pathForMessages) {
|
|
36
|
+
const findings = [];
|
|
37
|
+
const parsed = parseFrontmatter(raw);
|
|
38
|
+
const fm = parsed.data;
|
|
39
|
+
if (!("description" in fm)) {
|
|
40
|
+
findings.push({
|
|
41
|
+
kind: "missing-description",
|
|
42
|
+
field: "description",
|
|
43
|
+
message: `Lesson at ${pathForMessages} is missing required frontmatter field \`description\`.`,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
else if (!isNonEmptyString(fm.description)) {
|
|
47
|
+
findings.push({
|
|
48
|
+
kind: "empty-description",
|
|
49
|
+
field: "description",
|
|
50
|
+
message: `Lesson at ${pathForMessages} has an empty \`description\` frontmatter field; it must be a non-empty single-line string.`,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (!("when_to_use" in fm)) {
|
|
54
|
+
findings.push({
|
|
55
|
+
kind: "missing-when_to_use",
|
|
56
|
+
field: "when_to_use",
|
|
57
|
+
message: `Lesson at ${pathForMessages} is missing required frontmatter field \`when_to_use\`.`,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
else if (!isNonEmptyString(fm.when_to_use)) {
|
|
61
|
+
findings.push({
|
|
62
|
+
kind: "empty-when_to_use",
|
|
63
|
+
field: "when_to_use",
|
|
64
|
+
message: `Lesson at ${pathForMessages} has an empty \`when_to_use\` frontmatter field; it must be a non-empty single-line string.`,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return { path: pathForMessages, findings };
|
|
68
|
+
}
|
|
69
|
+
/** Lint a lesson file on disk. Throws if the file cannot be read. */
|
|
70
|
+
export function lintLessonFile(filePath) {
|
|
71
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
72
|
+
return lintLessonContent(raw, filePath);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Strict variant: throws a `UsageError` if any finding is present. The thrown
|
|
76
|
+
* error carries the full set of findings on its message and a hint pointing
|
|
77
|
+
* at v1 spec §13. The first finding's `field` becomes the error's primary
|
|
78
|
+
* field for callers that want to highlight the first violation.
|
|
79
|
+
*/
|
|
80
|
+
export function assertLessonValid(filePath) {
|
|
81
|
+
const report = lintLessonFile(filePath);
|
|
82
|
+
if (report.findings.length === 0)
|
|
83
|
+
return;
|
|
84
|
+
const message = report.findings.map((f) => f.message).join("\n");
|
|
85
|
+
throw new UsageError(message, "MISSING_REQUIRED_ARGUMENT", "Lessons require non-empty `description` and `when_to_use` frontmatter fields. See v1 spec §13.");
|
|
86
|
+
}
|