akm-cli 0.8.0-rc2 → 0.8.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/{.github/CHANGELOG.md → CHANGELOG.md} +191 -3
- package/README.md +22 -6
- package/SECURITY.md +93 -0
- package/dist/cli/config-migrate.js +144 -0
- package/dist/cli/config-validate.js +39 -0
- package/dist/cli/confirm.js +73 -0
- package/dist/cli/parse-args.js +93 -3
- package/dist/cli/shared.js +129 -0
- package/dist/cli.js +2141 -1268
- package/dist/commands/add-cli.js +279 -0
- package/dist/commands/agent-dispatch.js +20 -12
- package/dist/commands/agent-support.js +11 -5
- package/dist/commands/completions.js +3 -0
- package/dist/commands/config-cli.js +129 -517
- package/dist/commands/consolidate.js +1533 -144
- package/dist/commands/curate.js +44 -3
- package/dist/commands/db-cli.js +23 -0
- package/dist/commands/distill-promotion-policy.js +5 -3
- package/dist/commands/distill.js +906 -100
- package/dist/commands/env.js +213 -0
- package/dist/commands/eval-cases.js +3 -0
- package/dist/commands/events.js +3 -0
- package/dist/commands/extract-cli.js +127 -0
- package/dist/commands/extract-prompt.js +204 -0
- package/dist/commands/extract.js +477 -0
- package/dist/commands/feedback-cli.js +331 -0
- package/dist/commands/graph.js +260 -5
- package/dist/commands/health.js +977 -51
- package/dist/commands/help/help-accept.md +6 -3
- package/dist/commands/help/help-improve.md +36 -8
- package/dist/commands/help/help-proposals.md +7 -4
- package/dist/commands/help/help-reject.md +5 -2
- package/dist/commands/history.js +51 -16
- package/dist/commands/improve-auto-accept.js +97 -0
- package/dist/commands/improve-cli.js +236 -0
- package/dist/commands/improve-profiles.js +184 -0
- package/dist/commands/improve-result-file.js +167 -0
- package/dist/commands/improve.js +1725 -332
- package/dist/commands/info.js +3 -0
- package/dist/commands/init.js +49 -1
- package/dist/commands/installed-stashes.js +6 -23
- package/dist/commands/knowledge.js +3 -0
- package/dist/commands/lint/agent-linter.js +3 -0
- package/dist/commands/lint/base-linter.js +199 -5
- package/dist/commands/lint/command-linter.js +3 -0
- package/dist/commands/lint/default-linter.js +3 -0
- package/dist/commands/lint/env-key-rules.js +154 -0
- package/dist/commands/lint/index.js +92 -3
- package/dist/commands/lint/knowledge-linter.js +3 -0
- package/dist/commands/lint/markdown-insertion.js +343 -0
- package/dist/commands/lint/memory-linter.js +3 -0
- package/dist/commands/lint/registry.js +3 -0
- package/dist/commands/lint/skill-linter.js +3 -0
- package/dist/commands/lint/task-linter.js +15 -12
- package/dist/commands/lint/types.js +3 -0
- package/dist/commands/lint/workflow-linter.js +3 -0
- package/dist/commands/lint.js +3 -0
- package/dist/commands/migration-help.js +5 -2
- package/dist/commands/proposal-drain-policies.js +128 -0
- package/dist/commands/proposal-drain.js +477 -0
- package/dist/commands/proposal.js +60 -6
- package/dist/commands/propose.js +24 -19
- package/dist/commands/reflect.js +1004 -94
- package/dist/commands/registry-cli.js +150 -0
- package/dist/commands/registry-search.js +3 -0
- package/dist/commands/remember-cli.js +257 -0
- package/dist/commands/remember.js +15 -6
- package/dist/commands/schema-repair.js +88 -15
- package/dist/commands/search.js +99 -14
- package/dist/commands/secret.js +173 -0
- package/dist/commands/self-update.js +3 -0
- package/dist/commands/show.js +32 -13
- package/dist/commands/source-add.js +7 -35
- package/dist/commands/source-clone.js +3 -0
- package/dist/commands/source-manage.js +3 -0
- package/dist/commands/tasks.js +161 -95
- package/dist/commands/url-checker.js +3 -0
- package/dist/core/action-contributors.js +3 -0
- package/dist/core/asset-ref.js +13 -2
- package/dist/core/asset-registry.js +9 -2
- package/dist/core/asset-serialize.js +88 -0
- package/dist/core/asset-spec.js +61 -5
- package/dist/core/common.js +93 -5
- package/dist/core/concurrent.js +3 -0
- package/dist/core/config-io.js +347 -0
- package/dist/core/config-migration.js +622 -0
- package/dist/core/config-schema.js +558 -0
- package/dist/core/config-sources.js +108 -0
- package/dist/core/config-types.js +4 -0
- package/dist/core/config-walker.js +337 -0
- package/dist/core/config.js +366 -1077
- package/dist/core/errors.js +42 -20
- package/dist/core/events.js +31 -25
- package/dist/core/file-lock.js +104 -0
- package/dist/core/frontmatter.js +75 -10
- package/dist/core/lesson-lint.js +3 -0
- package/dist/core/markdown.js +3 -0
- package/dist/core/memory-belief.js +62 -0
- package/dist/core/memory-contradiction-detect.js +274 -0
- package/dist/core/memory-improve.js +142 -14
- package/dist/core/parse.js +3 -0
- package/dist/core/paths.js +218 -50
- package/dist/core/proposal-quality-validators.js +380 -0
- package/dist/core/proposal-validators.js +11 -3
- package/dist/core/proposals.js +464 -5
- package/dist/core/state-db.js +349 -56
- package/dist/core/text-truncation.js +107 -0
- package/dist/core/time.js +3 -0
- package/dist/core/tty.js +59 -0
- package/dist/core/warn.js +7 -2
- package/dist/core/write-source.js +12 -0
- package/dist/indexer/db-backup.js +391 -0
- package/dist/indexer/db-search.js +136 -28
- package/dist/indexer/db.js +661 -166
- package/dist/indexer/ensure-index.js +3 -0
- package/dist/indexer/file-context.js +3 -0
- package/dist/indexer/graph-boost.js +162 -40
- package/dist/indexer/graph-db.js +241 -51
- package/dist/indexer/graph-dedup.js +3 -7
- package/dist/indexer/graph-extraction.js +242 -149
- package/dist/indexer/index-context.js +3 -9
- package/dist/indexer/indexer.js +84 -14
- package/dist/indexer/llm-cache.js +24 -19
- package/dist/indexer/manifest.js +3 -0
- package/dist/indexer/matchers.js +184 -11
- package/dist/indexer/memory-inference.js +94 -50
- package/dist/indexer/metadata-contributors.js +3 -0
- package/dist/indexer/metadata.js +110 -50
- package/dist/indexer/path-resolver.js +3 -0
- package/dist/indexer/project-context.js +192 -0
- package/dist/indexer/ranking-contributors.js +134 -7
- package/dist/indexer/ranking.js +8 -1
- package/dist/indexer/search-fields.js +5 -9
- package/dist/indexer/search-hit-enrichers.js +91 -2
- package/dist/indexer/search-source.js +20 -1
- package/dist/indexer/semantic-status.js +4 -1
- package/dist/indexer/staleness-detect.js +447 -0
- package/dist/indexer/usage-events.js +12 -9
- package/dist/indexer/walker.js +3 -0
- package/dist/integrations/agent/builders.js +135 -0
- package/dist/integrations/agent/config.js +121 -401
- package/dist/integrations/agent/detect.js +3 -0
- package/dist/integrations/agent/index.js +6 -14
- package/dist/integrations/agent/model-aliases.js +55 -0
- package/dist/integrations/agent/profiles.js +3 -0
- package/dist/integrations/agent/prompts.js +137 -8
- package/dist/integrations/agent/runner.js +208 -0
- package/dist/integrations/agent/sdk-runner.js +8 -2
- package/dist/integrations/agent/spawn.js +54 -14
- package/dist/integrations/github.js +3 -0
- package/dist/integrations/lockfile.js +22 -51
- package/dist/integrations/session-logs/index.js +4 -0
- package/dist/integrations/session-logs/inline-refs.js +35 -0
- package/dist/integrations/session-logs/pre-filter.js +152 -0
- package/dist/integrations/session-logs/providers/claude-code.js +226 -0
- package/dist/integrations/session-logs/providers/opencode.js +231 -25
- package/dist/integrations/session-logs/types.js +3 -0
- package/dist/llm/call-ai.js +14 -26
- package/dist/llm/client.js +16 -2
- package/dist/llm/embedder.js +20 -29
- package/dist/llm/embedders/cache.js +3 -7
- package/dist/llm/embedders/local.js +42 -1
- package/dist/llm/embedders/remote.js +20 -8
- package/dist/llm/embedders/types.js +3 -7
- package/dist/llm/feature-gate.js +92 -56
- package/dist/llm/graph-extract.js +401 -30
- package/dist/llm/index-passes.js +44 -29
- package/dist/llm/memory-infer.js +30 -2
- package/dist/llm/metadata-enhance.js +3 -7
- package/dist/llm/prompts/extract-session.md +80 -0
- package/dist/llm/prompts/graph-extract-user-prompt.md +24 -1
- package/dist/output/cli-hints-full.md +60 -32
- package/dist/output/cli-hints-short.md +10 -7
- package/dist/output/cli-hints.js +5 -2
- package/dist/output/context.js +60 -8
- package/dist/output/renderers.js +170 -194
- package/dist/output/shapes/curate.js +56 -0
- package/dist/output/shapes/distill.js +10 -0
- package/dist/output/shapes/env-list.js +19 -0
- package/dist/output/shapes/events.js +11 -0
- package/dist/output/shapes/helpers.js +424 -0
- package/dist/output/shapes/history.js +7 -0
- package/dist/output/shapes/passthrough.js +105 -0
- package/dist/output/shapes/proposal-accept.js +7 -0
- package/dist/output/shapes/proposal-diff.js +7 -0
- package/dist/output/shapes/proposal-list.js +7 -0
- package/dist/output/shapes/proposal-producer.js +11 -0
- package/dist/output/shapes/proposal-reject.js +7 -0
- package/dist/output/shapes/proposal-show.js +7 -0
- package/dist/output/shapes/registry-search.js +6 -0
- package/dist/output/shapes/registry.js +30 -0
- package/dist/output/shapes/search.js +6 -0
- package/dist/output/shapes/secret-list.js +19 -0
- package/dist/output/shapes/show.js +6 -0
- package/dist/output/shapes/vault-list.js +19 -0
- package/dist/output/shapes.js +51 -549
- package/dist/output/text/add.js +6 -0
- package/dist/output/text/clone.js +6 -0
- package/dist/output/text/config.js +6 -0
- package/dist/output/text/curate.js +6 -0
- package/dist/output/text/distill.js +7 -0
- package/dist/output/text/enable-disable.js +7 -0
- package/dist/output/text/events.js +10 -0
- package/dist/output/text/feedback.js +6 -0
- package/dist/output/text/helpers.js +1059 -0
- package/dist/output/text/history.js +7 -0
- package/dist/output/text/import.js +6 -0
- package/dist/output/text/index.js +6 -0
- package/dist/output/text/info.js +6 -0
- package/dist/output/text/init.js +6 -0
- package/dist/output/text/list.js +6 -0
- package/dist/output/text/proposal-producer.js +8 -0
- package/dist/output/text/proposal.js +12 -0
- package/dist/output/text/registry-commands.js +11 -0
- package/dist/output/text/registry.js +30 -0
- package/dist/output/text/remember.js +6 -0
- package/dist/output/text/remove.js +6 -0
- package/dist/output/text/save.js +6 -0
- package/dist/output/text/search.js +6 -0
- package/dist/output/text/show.js +6 -0
- package/dist/output/text/update.js +6 -0
- package/dist/output/text/upgrade.js +6 -0
- package/dist/output/text/vault.js +16 -0
- package/dist/output/text/wiki.js +15 -0
- package/dist/output/text/workflow.js +14 -0
- package/dist/output/text.js +44 -1329
- package/dist/registry/build-index.js +3 -0
- package/dist/registry/create-provider-registry.js +3 -0
- package/dist/registry/factory.js +4 -1
- package/dist/registry/origin-resolve.js +3 -0
- package/dist/registry/providers/index.js +3 -0
- package/dist/registry/providers/skills-sh.js +11 -2
- package/dist/registry/providers/static-index.js +10 -1
- package/dist/registry/providers/types.js +3 -24
- package/dist/registry/resolve.js +11 -16
- package/dist/registry/types.js +3 -0
- package/dist/scripts/migrate-storage.js +17767 -0
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
- package/dist/scripts/migrations/v16-to-v17.js +141 -0
- package/dist/setup/detect.js +3 -0
- package/dist/setup/ripgrep-install.js +3 -0
- package/dist/setup/ripgrep-resolve.js +3 -0
- package/dist/setup/setup.js +306 -67
- package/dist/setup/steps.js +3 -15
- package/dist/sources/include.js +3 -0
- package/dist/sources/provider-factory.js +3 -11
- package/dist/sources/provider.js +3 -20
- package/dist/sources/providers/filesystem.js +19 -23
- package/dist/sources/providers/git.js +171 -21
- package/dist/sources/providers/index.js +3 -0
- package/dist/sources/providers/install-types.js +3 -13
- package/dist/sources/providers/npm.js +3 -4
- package/dist/sources/providers/provider-utils.js +3 -0
- package/dist/sources/providers/sync-from-ref.js +3 -11
- package/dist/sources/providers/tar-utils.js +3 -0
- package/dist/sources/providers/website.js +18 -22
- package/dist/sources/resolve.js +3 -0
- package/dist/sources/types.js +3 -0
- package/dist/sources/website-ingest.js +3 -0
- package/dist/tasks/backends/cron.js +3 -0
- package/dist/tasks/backends/exec-utils.js +3 -0
- package/dist/tasks/backends/index.js +3 -11
- package/dist/tasks/backends/launchd.js +3 -0
- package/dist/tasks/backends/schtasks.js +3 -0
- package/dist/tasks/parser.js +51 -38
- package/dist/tasks/resolveAkmBin.js +3 -0
- package/dist/tasks/runner.js +35 -9
- package/dist/tasks/schedule.js +20 -1
- package/dist/tasks/schema.js +5 -3
- package/dist/tasks/validator.js +6 -3
- package/dist/version.js +3 -0
- package/dist/wiki/wiki-templates.js +3 -0
- package/dist/wiki/wiki.js +3 -0
- package/dist/workflows/authoring.js +3 -0
- package/dist/workflows/cli.js +3 -0
- package/dist/workflows/db.js +140 -10
- package/dist/workflows/document-cache.js +3 -10
- package/dist/workflows/parser.js +3 -0
- package/dist/workflows/renderer.js +3 -0
- package/dist/workflows/runs.js +18 -1
- package/dist/workflows/schema.js +3 -0
- package/dist/workflows/scope-key.js +3 -0
- package/dist/workflows/validator.js +5 -9
- package/docs/README.md +7 -2
- package/docs/data-and-telemetry.md +225 -0
- package/docs/migration/release-notes/0.7.5.md +2 -2
- package/docs/migration/release-notes/0.8.0.md +57 -5
- package/docs/migration/v0.7-to-v0.8.md +1378 -0
- package/package.json +28 -11
- package/.github/LICENSE +0 -374
- package/dist/commands/install-audit.js +0 -385
- package/dist/commands/vault.js +0 -310
- package/dist/indexer/match-contributors.js +0 -141
- package/dist/integrations/agent/pipeline.js +0 -39
- package/dist/integrations/agent/runners.js +0 -31
package/dist/core/state-db.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
1
4
|
/**
|
|
2
5
|
* state.db — Durable SQLite database for non-regenerable akm state.
|
|
3
6
|
*
|
|
@@ -315,6 +318,138 @@ const MIGRATIONS = [
|
|
|
315
318
|
ON task_history(status);
|
|
316
319
|
`,
|
|
317
320
|
},
|
|
321
|
+
// ── Migration 003 — improve_runs ────────────────────────────────────────────
|
|
322
|
+
//
|
|
323
|
+
// Records every `akm improve` invocation as a durable row, replacing the
|
|
324
|
+
// legacy `<stash>/.akm/runs/<runId>/improve-result.json` artifact files.
|
|
325
|
+
//
|
|
326
|
+
// The `dry_run` column is FIRST-CLASS and indexed so productivity audits can
|
|
327
|
+
// cleanly filter dry-run probes out of real-run analyses without parsing
|
|
328
|
+
// `result_json`. The dry-run/real-run artifact-trap (recorded in
|
|
329
|
+
// feedback_akm_dryrun_artifact_trap) was the specific motivating bug.
|
|
330
|
+
//
|
|
331
|
+
// Indexed (query) columns:
|
|
332
|
+
// id TEXT PK — runId (`buildImproveRunId()` output).
|
|
333
|
+
// started_at TEXT — ISO-8601; indexed for time-range queries.
|
|
334
|
+
// stash_dir TEXT — absolute stash root; multi-stash scoping.
|
|
335
|
+
// dry_run INTEGER — 0/1; indexed for productivity audits.
|
|
336
|
+
// scope_mode TEXT — "all" | "type" | "ref"; indexed via composite
|
|
337
|
+
// with stash_dir for stash-scoped scope queries.
|
|
338
|
+
//
|
|
339
|
+
// Non-indexed payload:
|
|
340
|
+
// completed_at TEXT — ISO-8601 or NULL if interrupted.
|
|
341
|
+
// profile TEXT — improve profile name (nullable).
|
|
342
|
+
// scope_value TEXT — type name or asset ref (nullable).
|
|
343
|
+
// guidance TEXT — user-provided guidance text, if any.
|
|
344
|
+
// ok INTEGER — 0/1; whether the run produced ok=true.
|
|
345
|
+
// result_json TEXT — full AkmImproveResult JSON.
|
|
346
|
+
// metrics_json TEXT — aggregate counts extracted from result, cheap
|
|
347
|
+
// to query without parsing result_json.
|
|
348
|
+
//
|
|
349
|
+
// Extensible (metadata_json) columns:
|
|
350
|
+
// metadata_json TEXT — JSON object for future improve-run fields.
|
|
351
|
+
//
|
|
352
|
+
// ADD COLUMN extension points (future migrations):
|
|
353
|
+
// ALTER TABLE improve_runs ADD COLUMN duration_ms INTEGER DEFAULT NULL;
|
|
354
|
+
// ALTER TABLE improve_runs ADD COLUMN host TEXT DEFAULT NULL;
|
|
355
|
+
//
|
|
356
|
+
// TTL: rows where started_at < NOW() - 90 days can be deleted by
|
|
357
|
+
// `purgeOldImproveRuns()`. No automatic deletion occurs here.
|
|
358
|
+
{
|
|
359
|
+
id: "003-improve-runs",
|
|
360
|
+
up: `
|
|
361
|
+
CREATE TABLE IF NOT EXISTS improve_runs (
|
|
362
|
+
id TEXT PRIMARY KEY,
|
|
363
|
+
started_at TEXT NOT NULL,
|
|
364
|
+
completed_at TEXT,
|
|
365
|
+
stash_dir TEXT NOT NULL,
|
|
366
|
+
dry_run INTEGER NOT NULL DEFAULT 0,
|
|
367
|
+
profile TEXT,
|
|
368
|
+
scope_mode TEXT NOT NULL,
|
|
369
|
+
scope_value TEXT,
|
|
370
|
+
guidance TEXT,
|
|
371
|
+
ok INTEGER NOT NULL,
|
|
372
|
+
result_json TEXT NOT NULL,
|
|
373
|
+
metrics_json TEXT,
|
|
374
|
+
metadata_json TEXT NOT NULL DEFAULT '{}'
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
-- Query patterns supported:
|
|
378
|
+
-- SELECT … WHERE started_at >= ? AND started_at <= ?
|
|
379
|
+
-- → idx_improve_runs_started
|
|
380
|
+
-- SELECT … WHERE dry_run = 0
|
|
381
|
+
-- → idx_improve_runs_dry_run (productivity audits filter trap)
|
|
382
|
+
-- SELECT … WHERE stash_dir = ? AND scope_mode = ?
|
|
383
|
+
-- → idx_improve_runs_stash_scope
|
|
384
|
+
CREATE INDEX IF NOT EXISTS idx_improve_runs_started
|
|
385
|
+
ON improve_runs(started_at);
|
|
386
|
+
CREATE INDEX IF NOT EXISTS idx_improve_runs_dry_run
|
|
387
|
+
ON improve_runs(dry_run);
|
|
388
|
+
CREATE INDEX IF NOT EXISTS idx_improve_runs_stash_scope
|
|
389
|
+
ON improve_runs(stash_dir, scope_mode);
|
|
390
|
+
`,
|
|
391
|
+
},
|
|
392
|
+
// ── Migration 004 — extract_sessions_seen ───────────────────────────────────
|
|
393
|
+
//
|
|
394
|
+
// Tracks which platform sessions the extractor has processed, so the discovery
|
|
395
|
+
// pass in `akm extract --since <window>` skips sessions whose content hasn't
|
|
396
|
+
// changed since the last successful run. Replaces the akm-plugin
|
|
397
|
+
// session-checkpoint hook's implicit "write-once" memory of what's been
|
|
398
|
+
// captured — but persistent and queryable.
|
|
399
|
+
//
|
|
400
|
+
// Indexed (query) columns:
|
|
401
|
+
// harness TEXT — harness name (claude-code, opencode, ...).
|
|
402
|
+
// session_id TEXT — platform-native session identifier.
|
|
403
|
+
// processed_at TEXT — ISO-8601 UTC; when extract last ran on this session.
|
|
404
|
+
// session_ended_at TEXT — session.endedAt at processing time. When a
|
|
405
|
+
// later listSessions reports a *newer* endedAt
|
|
406
|
+
// for the same session_id, the extractor
|
|
407
|
+
// re-processes the appended events.
|
|
408
|
+
// outcome TEXT — "candidates_queued" | "no_candidates" |
|
|
409
|
+
// "skipped" | "failed".
|
|
410
|
+
//
|
|
411
|
+
// Non-indexed columns:
|
|
412
|
+
// candidate_count INTEGER — number of candidates the LLM produced.
|
|
413
|
+
// proposal_count INTEGER — number of proposals actually queued
|
|
414
|
+
// (candidates may fail downstream validation).
|
|
415
|
+
// rationale TEXT — for "no_candidates", the LLM's explanation.
|
|
416
|
+
// source_run TEXT — sourceRun id for PROV-DM traceability.
|
|
417
|
+
// metadata_json TEXT — future-proofing (pre-filter stats, LLM
|
|
418
|
+
// model+version, prompt token count, etc.).
|
|
419
|
+
//
|
|
420
|
+
// PK: (harness, session_id) — one row per session per harness. A re-extract
|
|
421
|
+
// updates the row in place via INSERT OR REPLACE.
|
|
422
|
+
//
|
|
423
|
+
// TTL: no automatic deletion. Sessions stay tracked as long as the source
|
|
424
|
+
// session files exist on disk. Operator can `DELETE FROM extract_sessions_seen
|
|
425
|
+
// WHERE processed_at < ?` for cleanup if desired.
|
|
426
|
+
{
|
|
427
|
+
id: "004-extract-sessions-seen",
|
|
428
|
+
up: `
|
|
429
|
+
CREATE TABLE IF NOT EXISTS extract_sessions_seen (
|
|
430
|
+
harness TEXT NOT NULL,
|
|
431
|
+
session_id TEXT NOT NULL,
|
|
432
|
+
processed_at TEXT NOT NULL,
|
|
433
|
+
session_ended_at TEXT,
|
|
434
|
+
outcome TEXT NOT NULL,
|
|
435
|
+
candidate_count INTEGER NOT NULL DEFAULT 0,
|
|
436
|
+
proposal_count INTEGER NOT NULL DEFAULT 0,
|
|
437
|
+
rationale TEXT,
|
|
438
|
+
source_run TEXT,
|
|
439
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
440
|
+
PRIMARY KEY (harness, session_id)
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
-- Query patterns:
|
|
444
|
+
-- SELECT … WHERE harness = ? → idx_extract_sessions_harness
|
|
445
|
+
-- SELECT … WHERE processed_at >= ? → idx_extract_sessions_processed
|
|
446
|
+
-- SELECT … WHERE harness = ? AND session_id = ? → PK
|
|
447
|
+
CREATE INDEX IF NOT EXISTS idx_extract_sessions_harness
|
|
448
|
+
ON extract_sessions_seen(harness);
|
|
449
|
+
CREATE INDEX IF NOT EXISTS idx_extract_sessions_processed
|
|
450
|
+
ON extract_sessions_seen(processed_at);
|
|
451
|
+
`,
|
|
452
|
+
},
|
|
318
453
|
];
|
|
319
454
|
/**
|
|
320
455
|
* Create the migrations table if it does not exist. This must be called
|
|
@@ -498,12 +633,21 @@ export function readStateEvents(db, options = {}) {
|
|
|
498
633
|
/**
|
|
499
634
|
* Delete events older than `retentionDays` (default: 90). Safe to call from
|
|
500
635
|
* a maintenance cron; uses a single DELETE with an index-covered ts predicate.
|
|
636
|
+
*
|
|
637
|
+
* Returns the number of rows actually deleted so callers can emit an
|
|
638
|
+
* `events_purged` observability event. A non-positive or non-finite
|
|
639
|
+
* `retentionDays` is treated as "disabled" and returns 0 without scanning.
|
|
501
640
|
*/
|
|
502
641
|
export function purgeOldEvents(db, retentionDays = 90) {
|
|
503
642
|
if (!Number.isFinite(retentionDays) || retentionDays <= 0)
|
|
504
|
-
return;
|
|
643
|
+
return 0;
|
|
505
644
|
const cutoff = new Date(Date.now() - retentionDays * 86_400_000).toISOString();
|
|
506
|
-
db.prepare("DELETE FROM events WHERE ts < ?").run(cutoff);
|
|
645
|
+
const result = db.prepare("DELETE FROM events WHERE ts < ?").run(cutoff);
|
|
646
|
+
// bun:sqlite's run() returns { changes, lastInsertRowid }. `changes` may be
|
|
647
|
+
// a number or bigint depending on the underlying lib; coerce to number for
|
|
648
|
+
// the metadata payload.
|
|
649
|
+
const changes = result.changes ?? 0;
|
|
650
|
+
return typeof changes === "bigint" ? Number(changes) : changes;
|
|
507
651
|
}
|
|
508
652
|
// ── proposals table helpers ──────────────────────────────────────────────────
|
|
509
653
|
/**
|
|
@@ -643,27 +787,52 @@ export function queryTaskHistory(db, options = {}) {
|
|
|
643
787
|
* monotonic integer ids. Callers that persisted a byte-offset cursor must
|
|
644
788
|
* discard it after migration and use the returned `maxId` as the new cursor.
|
|
645
789
|
*
|
|
646
|
-
*
|
|
647
|
-
*
|
|
648
|
-
*
|
|
649
|
-
*
|
|
790
|
+
* **Idempotency**: each line is pre-checked against the `events` table using
|
|
791
|
+
* `(event_type, ts, ref, metadata_json)` as the duplicate key. Lines whose
|
|
792
|
+
* exact tuple is already present are skipped and reported as `skipped` in the
|
|
793
|
+
* return value. This makes the migration safe to re-run (the v0.7→v0.8
|
|
794
|
+
* migration guide recommends re-running the script as a recovery path; without
|
|
795
|
+
* this guard, every re-run would double-import the entire event log).
|
|
796
|
+
*
|
|
797
|
+
* Duplicate detection is per-import-tuple, not a table-wide UNIQUE constraint:
|
|
798
|
+
* the events table has no UNIQUE constraint at runtime so that
|
|
799
|
+
* `appendEvent` can write multiple events with the same ts (sub-millisecond
|
|
800
|
+
* bursts produce identical `(event_type, ts, ref)` triples in practice). The
|
|
801
|
+
* SELECT-first check is scoped to the import path only.
|
|
802
|
+
*
|
|
803
|
+
* The import is wrapped in a single transaction for atomicity.
|
|
650
804
|
*
|
|
651
805
|
* @param db - Open state.db connection.
|
|
652
806
|
* @param jsonlPath - Absolute path to the events.jsonl file to import.
|
|
653
|
-
* @returns Number of rows inserted
|
|
807
|
+
* @returns Number of rows inserted, the max id assigned, and the
|
|
808
|
+
* count of rows skipped because an identical event already
|
|
809
|
+
* existed in the table.
|
|
654
810
|
*/
|
|
655
811
|
export async function importEventsJsonl(db, jsonlPath) {
|
|
656
812
|
const { readFileSync, existsSync } = await import("node:fs");
|
|
657
813
|
if (!existsSync(jsonlPath)) {
|
|
658
|
-
return { imported: 0, maxId: 0 };
|
|
814
|
+
return { imported: 0, maxId: 0, skipped: 0 };
|
|
659
815
|
}
|
|
660
816
|
const text = readFileSync(jsonlPath, "utf8");
|
|
661
817
|
const lines = text.split("\n").filter((l) => l.trim().length > 0);
|
|
662
818
|
let imported = 0;
|
|
663
819
|
let maxId = 0;
|
|
664
|
-
|
|
820
|
+
let skipped = 0;
|
|
821
|
+
const insertStmt = db.prepare(`INSERT INTO events (event_type, ts, ref, metadata_json)
|
|
665
822
|
VALUES (?, ?, ?, ?)
|
|
666
823
|
RETURNING id`);
|
|
824
|
+
// Dedup pre-check: matches by the full tuple including metadata_json so an
|
|
825
|
+
// import is idempotent over identical rows but does not collide with two
|
|
826
|
+
// genuinely different events that happen to share (event_type, ts, ref).
|
|
827
|
+
//
|
|
828
|
+
// Uses IS for ref so two NULL refs compare equal (a plain `=` would treat
|
|
829
|
+
// NULL = NULL as NULL and the row would be re-inserted on every run).
|
|
830
|
+
const existsStmt = db.prepare(`SELECT 1 FROM events
|
|
831
|
+
WHERE event_type = ?
|
|
832
|
+
AND ts = ?
|
|
833
|
+
AND ref IS ?
|
|
834
|
+
AND metadata_json = ?
|
|
835
|
+
LIMIT 1`);
|
|
667
836
|
db.transaction(() => {
|
|
668
837
|
for (const line of lines) {
|
|
669
838
|
let parsed;
|
|
@@ -677,7 +846,12 @@ export async function importEventsJsonl(db, jsonlPath) {
|
|
|
677
846
|
const ts = typeof parsed.ts === "string" ? parsed.ts : new Date().toISOString();
|
|
678
847
|
const ref = typeof parsed.ref === "string" ? parsed.ref : null;
|
|
679
848
|
const metadata = parsed.metadata !== undefined && typeof parsed.metadata === "object" ? JSON.stringify(parsed.metadata) : "{}";
|
|
680
|
-
const
|
|
849
|
+
const duplicate = existsStmt.get(eventType, ts, ref, metadata);
|
|
850
|
+
if (duplicate) {
|
|
851
|
+
skipped++;
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
const result = insertStmt.get(eventType, ts, ref, metadata);
|
|
681
855
|
if (result) {
|
|
682
856
|
imported++;
|
|
683
857
|
if (result.id > maxId)
|
|
@@ -685,7 +859,170 @@ export async function importEventsJsonl(db, jsonlPath) {
|
|
|
685
859
|
}
|
|
686
860
|
}
|
|
687
861
|
})();
|
|
688
|
-
return { imported, maxId };
|
|
862
|
+
return { imported, maxId, skipped };
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Compute the cheap aggregate metrics blob from a full improve result.
|
|
866
|
+
*
|
|
867
|
+
* Pure function — no I/O. Used by {@link recordImproveRun} to populate
|
|
868
|
+
* `metrics_json`. Exposed for tests and for any future call site that wants
|
|
869
|
+
* the same aggregation logic without hitting state.db.
|
|
870
|
+
*/
|
|
871
|
+
export function computeImproveRunMetrics(result) {
|
|
872
|
+
const plannedCount = Array.isArray(result.plannedRefs) ? result.plannedRefs.length : 0;
|
|
873
|
+
const actions = Array.isArray(result.actions) ? result.actions : [];
|
|
874
|
+
const actionsCount = actions.length;
|
|
875
|
+
let acceptedCount = 0;
|
|
876
|
+
let rejectedCount = 0;
|
|
877
|
+
let autoAcceptedCount = 0;
|
|
878
|
+
let errorCount = 0;
|
|
879
|
+
for (const action of actions) {
|
|
880
|
+
switch (action.mode) {
|
|
881
|
+
case "reflect":
|
|
882
|
+
case "distill":
|
|
883
|
+
case "memory-inference":
|
|
884
|
+
case "graph-extraction":
|
|
885
|
+
acceptedCount++;
|
|
886
|
+
break;
|
|
887
|
+
case "reflect-cooldown":
|
|
888
|
+
case "reflect-skipped":
|
|
889
|
+
case "distill-skipped":
|
|
890
|
+
rejectedCount++;
|
|
891
|
+
break;
|
|
892
|
+
case "reflect-failed":
|
|
893
|
+
case "error":
|
|
894
|
+
errorCount++;
|
|
895
|
+
break;
|
|
896
|
+
case "memory-prune":
|
|
897
|
+
// Prune is bookkeeping, not "accepted" content authoring; count
|
|
898
|
+
// separately as a no-op for the audit aggregate.
|
|
899
|
+
break;
|
|
900
|
+
}
|
|
901
|
+
// Legacy: pre-gate action results may carry autoAccepted: true (reflect path).
|
|
902
|
+
const r = action.result;
|
|
903
|
+
if (r && r.autoAccepted === true)
|
|
904
|
+
autoAcceptedCount++;
|
|
905
|
+
}
|
|
906
|
+
// Add gate-promoted count from the unified PostPhaseAutoAcceptGate (all phases).
|
|
907
|
+
autoAcceptedCount += result.gateAutoAcceptedCount ?? 0;
|
|
908
|
+
return { plannedCount, actionsCount, acceptedCount, rejectedCount, autoAcceptedCount, errorCount };
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Insert a single improve-run row into `improve_runs`. Uses parameterised SQL.
|
|
912
|
+
*
|
|
913
|
+
* Idempotency: the table's PRIMARY KEY is `id`, so re-running with the same
|
|
914
|
+
* runId would error. Callers mint a fresh runId per invocation via
|
|
915
|
+
* {@link buildImproveRunId} so this is not a concern in practice — but the
|
|
916
|
+
* default behaviour is INSERT (not REPLACE) so accidental dupes surface as
|
|
917
|
+
* a SQLite constraint error rather than silently overwriting a prior record.
|
|
918
|
+
*
|
|
919
|
+
* The `metrics` parameter defaults to the output of
|
|
920
|
+
* {@link computeImproveRunMetrics} when not supplied. Pass an explicit
|
|
921
|
+
* `metrics` object to override the derivation (e.g. tests).
|
|
922
|
+
*/
|
|
923
|
+
export function recordImproveRun(db, input) {
|
|
924
|
+
const metricsObj = input.metrics ?? computeImproveRunMetrics(input.result);
|
|
925
|
+
db.prepare(`
|
|
926
|
+
INSERT INTO improve_runs
|
|
927
|
+
(id, started_at, completed_at, stash_dir, dry_run, profile,
|
|
928
|
+
scope_mode, scope_value, guidance, ok, result_json, metrics_json, metadata_json)
|
|
929
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
930
|
+
`).run(input.id, input.startedAt, input.completedAt, input.stashDir, input.dryRun ? 1 : 0, input.profile, input.scopeMode, input.scopeValue, input.guidance, input.ok ? 1 : 0, JSON.stringify(input.result), JSON.stringify(metricsObj), JSON.stringify(input.metadata ?? {}));
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Delete improve_runs rows older than `retentionDays` (default: 90). Mirrors
|
|
934
|
+
* {@link purgeOldEvents} — same default, same return shape (number of rows
|
|
935
|
+
* actually deleted), same disabled-when-non-finite semantics.
|
|
936
|
+
*
|
|
937
|
+
* Safe to call from the improve post-loop maintenance pass alongside
|
|
938
|
+
* `purgeOldEvents(db, retentionDays)`.
|
|
939
|
+
*/
|
|
940
|
+
export function purgeOldImproveRuns(db, retentionDays = 90) {
|
|
941
|
+
if (!Number.isFinite(retentionDays) || retentionDays <= 0)
|
|
942
|
+
return 0;
|
|
943
|
+
const cutoff = new Date(Date.now() - retentionDays * 86_400_000).toISOString();
|
|
944
|
+
const result = db.prepare("DELETE FROM improve_runs WHERE started_at < ?").run(cutoff);
|
|
945
|
+
const changes = result.changes ?? 0;
|
|
946
|
+
return typeof changes === "bigint" ? Number(changes) : changes;
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Record (or update) one session's extract outcome. INSERT-OR-REPLACE so the
|
|
950
|
+
* row reflects the most recent run; downstream skip-logic compares
|
|
951
|
+
* `session_ended_at` against the live session metadata to decide if anything
|
|
952
|
+
* new arrived since `processed_at`.
|
|
953
|
+
*/
|
|
954
|
+
export function upsertExtractedSession(db, input) {
|
|
955
|
+
const endedAtIso = typeof input.sessionEndedAt === "number" && Number.isFinite(input.sessionEndedAt)
|
|
956
|
+
? new Date(input.sessionEndedAt).toISOString()
|
|
957
|
+
: null;
|
|
958
|
+
db.prepare(`
|
|
959
|
+
INSERT OR REPLACE INTO extract_sessions_seen
|
|
960
|
+
(harness, session_id, processed_at, session_ended_at, outcome,
|
|
961
|
+
candidate_count, proposal_count, rationale, source_run, metadata_json)
|
|
962
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
963
|
+
`).run(input.harness, input.sessionId, input.processedAt, endedAtIso, input.outcome, input.candidateCount, input.proposalCount, input.rationale ?? null, input.sourceRun ?? null, JSON.stringify(input.metadata ?? {}));
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Fetch a single session's last extract record, or `undefined` when the
|
|
967
|
+
* session has never been processed.
|
|
968
|
+
*/
|
|
969
|
+
export function getExtractedSession(db, harness, sessionId) {
|
|
970
|
+
// bun:sqlite returns null (not undefined) when no row matches — normalize so
|
|
971
|
+
// callers can rely on `if (!row)` and `toBeUndefined()` equivalently.
|
|
972
|
+
const row = db
|
|
973
|
+
.prepare("SELECT * FROM extract_sessions_seen WHERE harness = ? AND session_id = ?")
|
|
974
|
+
.get(harness, sessionId);
|
|
975
|
+
return row ?? undefined;
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Bulk-fetch session-extract status for a list of sessionIds in one harness.
|
|
979
|
+
* Returns a Map keyed by sessionId so callers can do O(1) lookups while
|
|
980
|
+
* iterating the discovery list.
|
|
981
|
+
*/
|
|
982
|
+
export function getExtractedSessionsMap(db, harness, sessionIds) {
|
|
983
|
+
const out = new Map();
|
|
984
|
+
if (sessionIds.length === 0)
|
|
985
|
+
return out;
|
|
986
|
+
// SQLite has a ~999 param ceiling; chunk if a caller ever exceeds that.
|
|
987
|
+
const CHUNK = 500;
|
|
988
|
+
for (let i = 0; i < sessionIds.length; i += CHUNK) {
|
|
989
|
+
const chunk = sessionIds.slice(i, i + CHUNK);
|
|
990
|
+
const placeholders = chunk.map(() => "?").join(",");
|
|
991
|
+
const rows = db
|
|
992
|
+
.prepare(`SELECT * FROM extract_sessions_seen
|
|
993
|
+
WHERE harness = ? AND session_id IN (${placeholders})`)
|
|
994
|
+
.all(harness, ...chunk);
|
|
995
|
+
for (const row of rows)
|
|
996
|
+
out.set(row.session_id, row);
|
|
997
|
+
}
|
|
998
|
+
return out;
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Decide whether a session should be skipped because the extractor has
|
|
1002
|
+
* already processed it AND nothing has changed since. The "anything new since
|
|
1003
|
+
* last extract?" rule is: the live `sessionEndedAtMs` is strictly later than
|
|
1004
|
+
* the recorded `session_ended_at`. Same-or-earlier endedAt means we'd be
|
|
1005
|
+
* re-processing the exact same content for no gain.
|
|
1006
|
+
*
|
|
1007
|
+
* Returns:
|
|
1008
|
+
* - `false` — no prior row, or session has new content since last extract.
|
|
1009
|
+
* The caller should process it.
|
|
1010
|
+
* - `true` — the session was already processed and hasn't been updated.
|
|
1011
|
+
* The caller should skip.
|
|
1012
|
+
*/
|
|
1013
|
+
export function shouldSkipAlreadyExtractedSession(prior, liveSessionEndedAtMs) {
|
|
1014
|
+
if (!prior)
|
|
1015
|
+
return false;
|
|
1016
|
+
// No live timestamp → can't tell if anything's new. Be conservative and
|
|
1017
|
+
// skip — the operator can pass --force later if we add it.
|
|
1018
|
+
if (typeof liveSessionEndedAtMs !== "number" || !Number.isFinite(liveSessionEndedAtMs)) {
|
|
1019
|
+
return true;
|
|
1020
|
+
}
|
|
1021
|
+
const priorMs = prior.session_ended_at ? Date.parse(prior.session_ended_at) : Number.NaN;
|
|
1022
|
+
if (!Number.isFinite(priorMs))
|
|
1023
|
+
return false;
|
|
1024
|
+
// Re-process when there's new content; skip when the session is unchanged.
|
|
1025
|
+
return liveSessionEndedAtMs <= priorMs;
|
|
689
1026
|
}
|
|
690
1027
|
// ── registry_index_cache (goes in index.db, not state.db) ───────────────────
|
|
691
1028
|
/**
|
|
@@ -696,7 +1033,7 @@ export async function importEventsJsonl(db, jsonlPath) {
|
|
|
696
1033
|
* created with CREATE TABLE IF NOT EXISTS so it is safe to call inside
|
|
697
1034
|
* ensureSchema() or as a standalone migration.
|
|
698
1035
|
*
|
|
699
|
-
* Purpose: caches the result of resolving and fetching remote registry
|
|
1036
|
+
* Purpose: caches the result of resolving and fetching remote registry stash
|
|
700
1037
|
* indexes so `akm search` does not hit the network on every invocation.
|
|
701
1038
|
*
|
|
702
1039
|
* Indexed (query) columns:
|
|
@@ -729,47 +1066,3 @@ export const REGISTRY_INDEX_CACHE_DDL = `
|
|
|
729
1066
|
CREATE INDEX IF NOT EXISTS idx_registry_cache_fetched
|
|
730
1067
|
ON registry_index_cache(fetched_at);
|
|
731
1068
|
`;
|
|
732
|
-
/**
|
|
733
|
-
* Ensure the `registry_index_cache` table exists in the given database
|
|
734
|
-
* (intended for use with the index.db connection from openDatabase()).
|
|
735
|
-
* Idempotent — safe to call on every open.
|
|
736
|
-
*/
|
|
737
|
-
export function ensureRegistryIndexCacheSchema(db) {
|
|
738
|
-
db.exec(REGISTRY_INDEX_CACHE_DDL);
|
|
739
|
-
}
|
|
740
|
-
/**
|
|
741
|
-
* Upsert a registry index cache entry.
|
|
742
|
-
*
|
|
743
|
-
* @param db - Open index.db connection.
|
|
744
|
-
* @param registryUrl - Canonical URL of the registry.
|
|
745
|
-
* @param indexJson - Serialised registry index document.
|
|
746
|
-
* @param etag - HTTP ETag from the response (optional).
|
|
747
|
-
* @param lastModified - HTTP Last-Modified from the response (optional).
|
|
748
|
-
*/
|
|
749
|
-
export function upsertRegistryIndexCache(db, registryUrl, indexJson, etag, lastModified) {
|
|
750
|
-
db.prepare(`
|
|
751
|
-
INSERT INTO registry_index_cache (registry_url, fetched_at, etag, last_modified, index_json)
|
|
752
|
-
VALUES (?, ?, ?, ?, ?)
|
|
753
|
-
ON CONFLICT(registry_url) DO UPDATE SET
|
|
754
|
-
fetched_at = excluded.fetched_at,
|
|
755
|
-
etag = excluded.etag,
|
|
756
|
-
last_modified = excluded.last_modified,
|
|
757
|
-
index_json = excluded.index_json
|
|
758
|
-
`).run(registryUrl, new Date().toISOString(), etag ?? null, lastModified ?? null, indexJson);
|
|
759
|
-
}
|
|
760
|
-
/**
|
|
761
|
-
* Look up a cached registry index entry. Returns undefined when not found or
|
|
762
|
-
* when the entry is older than `maxAgeMs`.
|
|
763
|
-
*/
|
|
764
|
-
export function getRegistryIndexCache(db, registryUrl, maxAgeMs = 3_600_000 /* 1 hour */) {
|
|
765
|
-
const row = db
|
|
766
|
-
.prepare(`SELECT fetched_at, etag, last_modified, index_json
|
|
767
|
-
FROM registry_index_cache WHERE registry_url = ?`)
|
|
768
|
-
.get(registryUrl);
|
|
769
|
-
if (!row)
|
|
770
|
-
return undefined;
|
|
771
|
-
const fetchedAt = Date.parse(row.fetched_at);
|
|
772
|
-
if (Number.isNaN(fetchedAt) || Date.now() - fetchedAt > maxAgeMs)
|
|
773
|
-
return undefined;
|
|
774
|
-
return { indexJson: row.index_json, etag: row.etag, lastModified: row.last_modified };
|
|
775
|
-
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Shared text-truncation heuristics used by `distill` and `consolidate`.
|
|
6
|
+
*
|
|
7
|
+
* Both commands need to recognise when an LLM-produced description string was
|
|
8
|
+
* sliced mid-sentence (typically the model hit its output budget). The two
|
|
9
|
+
* implementations historically maintained overlapping-but-not-identical
|
|
10
|
+
* vocabularies of hanging-connector words, which was a maintenance trap.
|
|
11
|
+
*
|
|
12
|
+
* This module is the single source of truth. `distill` continues to layer its
|
|
13
|
+
* own section-heading regex on top (those patterns are distill-specific and
|
|
14
|
+
* intentionally stay local to that module).
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Words that, when ending a sentence, suggest the description was truncated
|
|
18
|
+
* mid-sentence. Prepositions, conjunctions, articles, and auxiliary verbs that
|
|
19
|
+
* almost always have *something* following them in well-formed prose.
|
|
20
|
+
*
|
|
21
|
+
* This is the UNION of the two prior vocabularies used by `distill` and
|
|
22
|
+
* `consolidate` — a superset of both, so behaviour is at least as strict as
|
|
23
|
+
* either previous check.
|
|
24
|
+
*
|
|
25
|
+
* Stored lowercased; callers must lower-case the last word before lookup.
|
|
26
|
+
*/
|
|
27
|
+
export const TRUNCATION_TRAILING_WORDS = new Set([
|
|
28
|
+
"a",
|
|
29
|
+
"after",
|
|
30
|
+
"an",
|
|
31
|
+
"and",
|
|
32
|
+
"are",
|
|
33
|
+
"as",
|
|
34
|
+
"at",
|
|
35
|
+
"be",
|
|
36
|
+
"been",
|
|
37
|
+
"before",
|
|
38
|
+
"being",
|
|
39
|
+
"but",
|
|
40
|
+
"by",
|
|
41
|
+
"can",
|
|
42
|
+
"could",
|
|
43
|
+
"did",
|
|
44
|
+
"do",
|
|
45
|
+
"does",
|
|
46
|
+
"for",
|
|
47
|
+
"from",
|
|
48
|
+
"had",
|
|
49
|
+
"has",
|
|
50
|
+
"have",
|
|
51
|
+
"if",
|
|
52
|
+
"in",
|
|
53
|
+
"into",
|
|
54
|
+
"is",
|
|
55
|
+
"may",
|
|
56
|
+
"might",
|
|
57
|
+
"must",
|
|
58
|
+
"of",
|
|
59
|
+
"on",
|
|
60
|
+
"onto",
|
|
61
|
+
"or",
|
|
62
|
+
"per",
|
|
63
|
+
"shall",
|
|
64
|
+
"should",
|
|
65
|
+
"so",
|
|
66
|
+
"than",
|
|
67
|
+
"that",
|
|
68
|
+
"the",
|
|
69
|
+
"to",
|
|
70
|
+
"upon",
|
|
71
|
+
"via",
|
|
72
|
+
"was",
|
|
73
|
+
"were",
|
|
74
|
+
"when",
|
|
75
|
+
"which",
|
|
76
|
+
"while",
|
|
77
|
+
"will",
|
|
78
|
+
"with",
|
|
79
|
+
"would",
|
|
80
|
+
]);
|
|
81
|
+
/**
|
|
82
|
+
* Returns a reason string when `description` looks truncated mid-sentence;
|
|
83
|
+
* returns `null` if the description appears complete.
|
|
84
|
+
*
|
|
85
|
+
* Heuristics:
|
|
86
|
+
* - Trailing `,`, `;`, `:`, or `+` (operator-style cutoff like `max-width:100% +`)
|
|
87
|
+
* - Trailing ellipsis (`...` or `…`)
|
|
88
|
+
* - Last word matches {@link TRUNCATION_TRAILING_WORDS}
|
|
89
|
+
*
|
|
90
|
+
* Does NOT detect section-heading fragments — that check is distill-specific
|
|
91
|
+
* and lives in `src/commands/distill.ts` (`HEADING_FRAGMENT_PATTERNS`).
|
|
92
|
+
*/
|
|
93
|
+
export function detectTruncatedDescription(description) {
|
|
94
|
+
const trimmed = description.trim();
|
|
95
|
+
if (trimmed.length === 0)
|
|
96
|
+
return null; // empty handled elsewhere
|
|
97
|
+
if (/[,;:+]$/.test(trimmed))
|
|
98
|
+
return "ends with trailing punctuation/operator";
|
|
99
|
+
if (/\.{3,}$/.test(trimmed) || /…$/.test(trimmed))
|
|
100
|
+
return "ends with ellipsis";
|
|
101
|
+
const lastWord = trimmed.split(/\s+/).pop() ?? "";
|
|
102
|
+
const normalized = lastWord.toLowerCase();
|
|
103
|
+
if (TRUNCATION_TRAILING_WORDS.has(normalized)) {
|
|
104
|
+
return `ends with hanging connector "${lastWord}"`;
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
package/dist/core/time.js
CHANGED
package/dist/core/tty.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
* Terminal-glyph and color gating helpers (#486).
|
|
6
|
+
*
|
|
7
|
+
* Honor the [NO_COLOR](https://no-color.org/) convention — when NO_COLOR is
|
|
8
|
+
* present in the environment (with any value, including empty), the CLI
|
|
9
|
+
* suppresses both ANSI color codes AND decorative emoji glyphs. Emoji often
|
|
10
|
+
* render as garbage in non-Unicode terminals or get logged as literal bytes
|
|
11
|
+
* when output is piped to text aggregators, so we treat them as a subset of
|
|
12
|
+
* "color" decoration.
|
|
13
|
+
*
|
|
14
|
+
* Detection is also automatic on non-TTY stderr (output piped or redirected
|
|
15
|
+
* to a file), where decorative glyphs add nothing.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Returns true when decorative glyphs and color codes should be emitted.
|
|
19
|
+
* False when NO_COLOR is set or stderr is not a TTY (unless FORCE_COLOR
|
|
20
|
+
* overrides per the de-facto Node convention).
|
|
21
|
+
*/
|
|
22
|
+
export function shouldDecorate() {
|
|
23
|
+
if (process.env.NO_COLOR !== undefined)
|
|
24
|
+
return false;
|
|
25
|
+
if (process.env.FORCE_COLOR !== undefined && process.env.FORCE_COLOR !== "" && process.env.FORCE_COLOR !== "0") {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
return process.stderr.isTTY === true;
|
|
29
|
+
}
|
|
30
|
+
// Map known decorative emoji to plain ASCII fallbacks. Anything not listed
|
|
31
|
+
// is still removed by the catch-all sweep below when decoration is off.
|
|
32
|
+
const EMOJI_FALLBACKS = [
|
|
33
|
+
[/\u{1F44B}\s*/gu, ""], // wave (with trailing space)
|
|
34
|
+
[/✓/g, "[ok]"],
|
|
35
|
+
[/✗/g, "[x]"],
|
|
36
|
+
[/✘/g, "[x]"],
|
|
37
|
+
[/⚠️?/g, "[!]"],
|
|
38
|
+
[/\u{1F4DA}\s*/gu, ""], // books
|
|
39
|
+
];
|
|
40
|
+
/**
|
|
41
|
+
* If decoration is disabled, replace known emoji with ASCII fallbacks and
|
|
42
|
+
* strip any remaining pictograph code points. If decoration is enabled,
|
|
43
|
+
* return the input unchanged.
|
|
44
|
+
*/
|
|
45
|
+
export function plainize(text) {
|
|
46
|
+
if (shouldDecorate())
|
|
47
|
+
return text;
|
|
48
|
+
let out = text;
|
|
49
|
+
for (const [pattern, repl] of EMOJI_FALLBACKS) {
|
|
50
|
+
out = out.replace(pattern, repl);
|
|
51
|
+
}
|
|
52
|
+
// Catch-all for unmapped pictographs. Future-proof when new emoji are added
|
|
53
|
+
// to call sites without updating the explicit map.
|
|
54
|
+
out = out.replace(/[\u{1F300}-\u{1FAFF}]/gu, "");
|
|
55
|
+
// Collapse runs of whitespace introduced by emoji removal, but preserve
|
|
56
|
+
// intentional leading indentation. Only collapses interior runs.
|
|
57
|
+
out = out.replace(/(\S)[ \t]{2,}/g, "$1 ");
|
|
58
|
+
return out;
|
|
59
|
+
}
|
package/dist/core/warn.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
1
4
|
/**
|
|
2
5
|
* Module-level quiet/verbose flags and optional file sink for stderr output.
|
|
3
6
|
*
|
|
@@ -81,8 +84,10 @@ function appendToLogFile(level, args) {
|
|
|
81
84
|
try {
|
|
82
85
|
fs.appendFileSync(logFilePath, `[${ts}] [${level}] ${msg}\n`);
|
|
83
86
|
}
|
|
84
|
-
catch {
|
|
85
|
-
//
|
|
87
|
+
catch (e) {
|
|
88
|
+
// Log file write failed — emit directly to stderr so the message is not lost.
|
|
89
|
+
process.stderr.write(`[akm:warn] log-file write failed (${logFilePath}): ${e}\n`);
|
|
90
|
+
process.stderr.write(`[${ts}] [${level}] ${msg}\n`);
|
|
86
91
|
}
|
|
87
92
|
}
|
|
88
93
|
/**
|