devflow-kit 3.6.0 → 3.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/CHANGELOG.md +438 -1
- package/README.md +15 -11
- package/dist/agents/code.md +9 -15
- package/dist/agents/design.md +1 -1
- package/dist/agents/fix.md +11 -13
- package/dist/agents/git.md +1 -1
- package/dist/agents/harden.md +1 -1
- package/dist/agents/refactor.md +7 -10
- package/dist/agents/review.md +6 -4
- package/dist/agents/skim.md +2 -2
- package/dist/agents/triage.md +2 -2
- package/dist/cli/commands/capture.js +1 -1
- package/dist/cli/commands/curation-move-io.js +102 -0
- package/dist/cli/commands/curation-move-step.js +374 -0
- package/dist/cli/commands/{learning.js → curation.js} +302 -99
- package/dist/cli/commands/gate-level-prompts.js +426 -0
- package/dist/cli/commands/gates.js +299 -0
- package/dist/cli/commands/init-seed.js +16 -5
- package/dist/cli/commands/init.js +263 -54
- package/dist/cli/commands/memory.js +20 -2
- package/dist/cli/commands/prompt-io.js +37 -0
- package/dist/cli/commands/uninstall.js +81 -9
- package/dist/cli.js +4 -2
- package/dist/commands/bug-analysis.md +25 -36
- package/dist/commands/code-review.md +77 -73
- package/dist/commands/debug.md +5 -5
- package/dist/commands/dynamic-build.md +76 -83
- package/dist/commands/dynamic-plan.md +5 -5
- package/dist/commands/dynamic-profile.md +5 -5
- package/dist/commands/dynamic-tickets.md +5 -5
- package/dist/commands/explore.md +6 -6
- package/dist/commands/implement.md +68 -102
- package/dist/commands/plan.md +19 -29
- package/dist/commands/release.md +5 -5
- package/dist/commands/research.md +6 -6
- package/dist/commands/resolve.md +29 -31
- package/dist/commands/self-review.md +5 -13
- package/dist/core/assets.js +20 -20
- package/dist/core/curate-tuning-config.js +5 -6
- package/dist/core/curation-move-notice.js +84 -0
- package/dist/core/curation-move-skip.js +36 -0
- package/dist/core/curation-move.js +35 -0
- package/dist/core/curation-queue-cleanup.js +134 -0
- package/dist/core/curation-store.js +98 -0
- package/dist/core/{learning-variants.js → curation-variants.js} +65 -41
- package/dist/core/echo-value.js +48 -0
- package/dist/core/evidence-policy.js +18 -6
- package/dist/core/feature-switch.js +95 -19
- package/dist/core/fs-atomic.js +1 -1
- package/dist/core/gate-level.js +221 -0
- package/dist/core/git.js +16 -14
- package/dist/core/ledger-root.js +82 -22
- package/dist/core/linked-path.js +4 -4
- package/dist/core/manifest-lock.js +238 -0
- package/dist/core/manifest.js +84 -24
- package/dist/core/migrations.js +1 -13
- package/dist/core/model-discovery.js +7 -1
- package/dist/core/observations.js +2 -2
- package/dist/core/plugins.js +15 -15
- package/dist/core/pool.js +26 -0
- package/dist/core/project-paths.js +103 -34
- package/dist/core/queue-drain.js +2 -2
- package/dist/core/tracked-areas.js +35 -0
- package/dist/core/tracker.js +6 -34
- package/dist/{learning-off → curation-off}/agents/code.md +9 -15
- package/dist/{learning-off → curation-off}/agents/fix.md +11 -13
- package/dist/{learning-off → curation-off}/agents/refactor.md +7 -10
- package/dist/{learning-off → curation-off}/agents/review.md +5 -3
- package/dist/{learning-off → curation-off}/agents/triage.md +1 -1
- package/dist/{learning-off → curation-off}/commands/bug-analysis.md +20 -31
- package/dist/{learning-off → curation-off}/commands/code-review.md +74 -70
- package/dist/{learning-off → curation-off}/commands/debug.md +2 -2
- package/dist/{learning-off → curation-off}/commands/dynamic-build.md +73 -80
- package/dist/{learning-off → curation-off}/commands/explore.md +2 -2
- package/dist/{learning-off → curation-off}/commands/implement.md +65 -99
- package/dist/{learning-off → curation-off}/commands/plan.md +15 -25
- package/dist/{learning-off → curation-off}/commands/resolve.md +26 -28
- package/dist/{learning-off → curation-off}/commands/self-review.md +2 -10
- package/dist/hud/components/{learning-counts.js → curation-counts.js} +36 -17
- package/dist/hud/config.js +1 -1
- package/dist/hud/index.js +6 -6
- package/dist/hud/render.js +3 -3
- package/dist/skills/git/references/pr/validate-branch.md +8 -5
- package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +20 -14
- package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +20 -20
- package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +20 -20
- package/dist/targets/claude-code/{learning-install.js → curation-install.js} +99 -67
- package/dist/targets/claude-code/installer.js +15 -15
- package/dist/targets/claude-code/language-stamp.js +5 -5
- package/dist/targets/claude-code/post-install.js +2 -2
- package/dist/targets/claude-code/tracker-servers.js +1 -1
- package/package.json +3 -2
- package/src/assets/agents/_partials/_code.mds +16 -17
- package/src/assets/agents/code.mds +4 -7
- package/src/assets/agents/curate.md +12 -14
- package/src/assets/agents/debug.mds +6 -6
- package/src/assets/agents/design.mds +18 -18
- package/src/assets/agents/fix.mds +6 -5
- package/src/assets/agents/git.mds +1 -1
- package/src/assets/agents/harden.mds +7 -7
- package/src/assets/agents/knowledge.mds +23 -23
- package/src/assets/agents/refactor.mds +2 -2
- package/src/assets/agents/research.mds +15 -15
- package/src/assets/agents/review.mds +45 -43
- package/src/assets/agents/skim.mds +11 -11
- package/src/assets/agents/synthesize.md +2 -2
- package/src/assets/agents/triage.mds +22 -22
- package/src/assets/agents/validate.md +13 -26
- package/src/assets/commands/_partials/_decisions.mds +13 -13
- package/src/assets/commands/_partials/_engine.mds +8 -8
- package/src/assets/commands/_partials/_knowledge.mds +29 -15
- package/src/assets/commands/_partials/_preamble.mds +4 -4
- package/src/assets/commands/_partials/_settings.mds +3 -3
- package/src/assets/commands/_partials/_wave.mds +17 -24
- package/src/assets/commands/bug-analysis.mds +37 -48
- package/src/assets/commands/code-review.mds +90 -86
- package/src/assets/commands/debug.mds +14 -14
- package/src/assets/commands/dynamic-build.mds +78 -78
- package/src/assets/commands/dynamic-plan.mds +22 -22
- package/src/assets/commands/dynamic-profile.mds +2 -2
- package/src/assets/commands/dynamic-tickets.mds +8 -8
- package/src/assets/commands/explore.mds +20 -20
- package/src/assets/commands/implement.mds +92 -118
- package/src/assets/commands/plan.mds +41 -51
- package/src/assets/commands/release.mds +12 -12
- package/src/assets/commands/research.mds +19 -19
- package/src/assets/commands/resolve.mds +56 -50
- package/src/assets/commands/self-review.mds +20 -20
- package/src/assets/mds/git/_ops.mds +3 -1
- package/src/assets/mds/git/_ops_pr.mds +1 -1
- package/src/assets/mds/git/_ops_steps.mds +13 -7
- package/src/assets/mds/git/_pr.mds +7 -4
- package/src/assets/mds/tracker/_common.mds +23 -12
- package/src/assets/mds/tracker/_github.mds +2 -9
- package/src/assets/mds/tracker/_jira.mds +2 -15
- package/src/assets/mds/tracker/_linear.mds +2 -15
- package/src/assets/rules/quality.md +1 -0
- package/src/assets/scripts/ci-wait.cjs +22 -4
- package/src/assets/scripts/claude-md-audit.cjs +66 -51
- package/src/assets/scripts/hooks/background-memory-update +406 -97
- package/src/assets/scripts/hooks/capture-prompt +12 -12
- package/src/assets/scripts/hooks/capture-question +11 -11
- package/src/assets/scripts/hooks/capture-turn +12 -12
- package/src/assets/scripts/hooks/{learning-lock → curation-lock} +2 -2
- package/src/assets/scripts/hooks/curation-move +293 -0
- package/src/assets/scripts/hooks/ensure-devflow-init +45 -13
- package/src/assets/scripts/hooks/ensure-proxy +4 -4
- package/src/assets/scripts/hooks/ensure-root-gitignore +4 -4
- package/src/assets/scripts/hooks/git-marker +81 -0
- package/src/assets/scripts/hooks/json-helper.cjs +145 -51
- package/src/assets/scripts/hooks/json-parse +19 -4
- package/src/assets/scripts/hooks/lib/curation-move.cjs +1582 -0
- package/src/assets/scripts/hooks/lib/{learning-store.cjs → curation-store.cjs} +242 -118
- package/src/assets/scripts/hooks/lib/decisions-format.cjs +3 -3
- package/src/assets/scripts/hooks/lib/mkdir-lock.cjs +24 -7
- package/src/assets/scripts/hooks/lib/project-paths.cjs +132 -39
- package/src/assets/scripts/hooks/lib/render-decisions.cjs +34 -22
- package/src/assets/scripts/hooks/lib/tracked-areas.cjs +259 -0
- package/src/assets/scripts/hooks/memory-permission +358 -0
- package/src/assets/scripts/hooks/memory-worker +1 -1
- package/src/assets/scripts/hooks/pre-compact-memory +6 -16
- package/src/assets/scripts/hooks/queue-append +228 -103
- package/src/assets/scripts/hooks/resolve-project-root +2 -2
- package/src/assets/scripts/hooks/session-start-context +145 -71
- package/src/assets/scripts/hooks/session-start-memory +27 -46
- package/src/assets/scripts/lib/project-config.cjs +31 -9
- package/src/assets/scripts/resolve-base.cjs +1043 -0
- package/src/assets/scripts/resolve-settings.cjs +238 -46
- package/src/assets/skills/agent-references/SKILL.md +9 -2
- package/src/assets/skills/agent-references/references/commands/code-review-levels.md +19 -0
- package/src/assets/skills/agent-references/references/commands/implement-levels.md +27 -0
- package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +6 -4
- package/src/assets/skills/agent-references/references/commands/plan-levels.md +40 -0
- package/src/assets/skills/agent-references/references/commands/resolve-levels.md +24 -0
- package/src/assets/skills/agent-references/references/synthesize/planning.md +9 -22
- package/src/assets/skills/agent-references/references/synthesize/review.md +11 -11
- package/src/assets/skills/apply-decisions/SKILL.md +1 -1
- package/src/assets/skills/docs-framework/SKILL.md +7 -7
- package/src/assets/skills/docs-framework/references/patterns.md +3 -4
- package/src/assets/skills/quality-gates/SKILL.md +1 -1
- package/src/assets/skills/worktree-support/references/roots.md +2 -2
- package/templates/lint-pack/.lint-pack.conf +86 -0
- package/templates/lint-pack/README.md +190 -0
- package/templates/lint-pack/lint-pack.sh +905 -0
- package/dist/core/learning-queue-cleanup.js +0 -27
- package/dist/core/learning-store.js +0 -61
- /package/dist/{learning-off → curation-off}/agents/debug.md +0 -0
- /package/dist/{learning-off → curation-off}/agents/design.md +0 -0
- /package/dist/{learning-off → curation-off}/agents/harden.md +0 -0
- /package/dist/{learning-off → curation-off}/agents/knowledge.md +0 -0
- /package/dist/{learning-off → curation-off}/agents/research.md +0 -0
- /package/dist/{learning-off → curation-off}/agents/skim.md +0 -0
- /package/dist/{learning-off → curation-off}/commands/dynamic-plan.md +0 -0
- /package/dist/{learning-off → curation-off}/commands/dynamic-profile.md +0 -0
- /package/dist/{learning-off → curation-off}/commands/dynamic-tickets.md +0 -0
- /package/dist/{learning-off → curation-off}/commands/release.md +0 -0
- /package/dist/{learning-off → curation-off}/commands/research.md +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
// src/assets/scripts/hooks/lib/
|
|
1
|
+
// src/assets/scripts/hooks/lib/curation-store.cjs
|
|
2
2
|
//
|
|
3
|
-
// The v2
|
|
3
|
+
// The v2 curation store: the one place the curation plumbing reads, validates,
|
|
4
4
|
// projects and writes the decisions log, the ledger and their side files.
|
|
5
5
|
//
|
|
6
6
|
// DESIGN: plumbing only. No function here prints or calls process.exit. Work that
|
|
7
7
|
// can fail on its input or on the lock returns a Result — { ok: true, value } or
|
|
8
8
|
// { ok: false, error: { kind, message } } — and its caller prints it: json-helper.cjs
|
|
9
|
-
// for the ops, or the `devflow
|
|
9
|
+
// for the ops, or the `devflow curation` CLI through src/core/curation-store.ts. A throw
|
|
10
10
|
// means a broken invariant or an I/O failure, never an expected outcome; a lock
|
|
11
11
|
// held by withDecisionsLock is released on every path, the throw included.
|
|
12
12
|
//
|
|
@@ -14,39 +14,47 @@
|
|
|
14
14
|
// decisions-format.cjs or render-decisions.cjs at load time: both require it, for
|
|
15
15
|
// the status list and the one-line and inactive-note text they share with `list`.
|
|
16
16
|
//
|
|
17
|
-
//
|
|
17
|
+
// The one-time move of the old ledger folder to its new name is not here: it lives in
|
|
18
|
+
// curation-move.cjs, which requires this module. The store holds only the guard that
|
|
19
|
+
// keeps its writers off an unfinished move (withDecisionsLock reads the move journal),
|
|
20
|
+
// so a store write loads no move code.
|
|
21
|
+
//
|
|
22
|
+
// Files under <root>/.devflow/curation/:
|
|
18
23
|
// decisions-log.jsonl observation rows — the content authority
|
|
19
24
|
// decisions-ledger.jsonl anchored rows — projections of log rows
|
|
20
25
|
// decisions-log.archive.jsonl rotated-out observation rows (D-ROTATE-UNREFERENCED)
|
|
21
26
|
// decisions-history.jsonl prior content versions (D-CONTENT-HISTORY)
|
|
22
27
|
// *.rejected.jsonl quarantined malformed lines (D-QUARANTINE-MALFORMED)
|
|
23
28
|
// *.pre-v2.jsonl one-time copies of the v1 files (D-V1-BACKUP-ONCE)
|
|
24
|
-
// .decisions.lock/ the one
|
|
29
|
+
// .decisions.lock/ the one curation lock (D-ONE-LEARNING-LOCK)
|
|
25
30
|
// .pending-turns.jsonl the queue the capture hooks append to
|
|
26
31
|
// .pending-turns.processing the claimed batch, and .pending-turns.owner its
|
|
27
32
|
// owner's token (D-OWNED-CLAIM)
|
|
28
33
|
//
|
|
29
34
|
// TS COUNTERPARTS: src/core/observations.ts mirrors the status lists (D201), and
|
|
30
|
-
// src/core/
|
|
35
|
+
// src/core/curation-store.ts transcribes the functions the CLI calls from their
|
|
31
36
|
// JSDoc here (D-LEARNING-STORE-SEAM) — change both together.
|
|
32
37
|
|
|
33
38
|
'use strict';
|
|
34
39
|
|
|
35
40
|
const crypto = require('crypto');
|
|
36
41
|
const fs = require('fs');
|
|
42
|
+
const os = require('os');
|
|
37
43
|
const path = require('path');
|
|
38
44
|
const { execFileSync } = require('child_process');
|
|
39
45
|
|
|
40
46
|
const {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
LEGACY_NAMES,
|
|
48
|
+
getCurationDir,
|
|
49
|
+
getCurationPendingTurnsPath,
|
|
50
|
+
getCurationPendingTurnsProcessingPath,
|
|
51
|
+
getCurationClaimOwnerPath,
|
|
45
52
|
getDecisionsLedgerPath,
|
|
46
53
|
getDecisionsLogPath,
|
|
47
54
|
getDecisionsArchivePath,
|
|
48
55
|
getDecisionsHistoryPath,
|
|
49
56
|
getDecisionsLockDir,
|
|
57
|
+
curationMoveJournalPath,
|
|
50
58
|
} = require('./project-paths.cjs');
|
|
51
59
|
const { acquireMkdirLock, releaseLock } = require('./mkdir-lock.cjs');
|
|
52
60
|
const { safePath } = require('./safe-path.cjs');
|
|
@@ -401,17 +409,17 @@ function readTextUnlinked(file, { maxBytes = Infinity } = {}) {
|
|
|
401
409
|
* delete them without a trace, and a reader that quarantined would make list,
|
|
402
410
|
* show and the HUD write files.
|
|
403
411
|
*
|
|
404
|
-
* D-NO-LINKED-READ: a
|
|
412
|
+
* D-NO-LINKED-READ: a curation file that is itself a symbolic link reads as
|
|
405
413
|
* missing, and nothing is read through it; the pre-v2 backup skips one the same
|
|
406
414
|
* way (ensurePreV2Backup), and release-claim reads the claim's owner file the same
|
|
407
415
|
* way, and only up to CLAIM_OWNER_MAX_BYTES (readClaimOwner). readJsonl and
|
|
408
416
|
* readClaimOwner read a regular file alone (readTextUnlinked): a directory, a FIFO
|
|
409
417
|
* or a device where the file belongs reads as missing too, and neither waits on
|
|
410
|
-
* one. Reason: a repository can commit any
|
|
418
|
+
* one. Reason: a repository can commit any curation file as a link to a file
|
|
411
419
|
* elsewhere on the machine, and a read that followed it would put that file's
|
|
412
420
|
* lines into list and show and, through a rewrite, the quarantine or a render,
|
|
413
|
-
* into the project's
|
|
414
|
-
* /dev/zero would wait forever or fill memory, holding the
|
|
421
|
+
* into the project's curation folder; a read that followed one to a FIFO or to
|
|
422
|
+
* /dev/zero would wait forever or fill memory, holding the curation lock when a
|
|
415
423
|
* writer or release-claim reads. The writers never write through a link either:
|
|
416
424
|
* a rename replaces one, and an append refuses one.
|
|
417
425
|
*
|
|
@@ -448,11 +456,16 @@ function rejectedPathFor(file) {
|
|
|
448
456
|
|
|
449
457
|
/**
|
|
450
458
|
* Append to `file`, creating it when absent. O_NOFOLLOW refuses a symlink planted
|
|
451
|
-
* at `file` (ELOOP) instead of writing through it.
|
|
459
|
+
* at `file` (ELOOP) instead of writing through it. A file this call creates gets
|
|
460
|
+
* `mode`, as an explicit open mode (the process umask can only remove bits from it).
|
|
461
|
+
*
|
|
462
|
+
* @param {string} file
|
|
463
|
+
* @param {string} content
|
|
464
|
+
* @param {number} [mode] - the mode of a created file (default 0o666, as the umask leaves it)
|
|
452
465
|
*/
|
|
453
|
-
function appendNoFollow(file, content) {
|
|
466
|
+
function appendNoFollow(file, content, mode = 0o666) {
|
|
454
467
|
const flags = fs.constants.O_WRONLY | fs.constants.O_APPEND | fs.constants.O_CREAT | (fs.constants.O_NOFOLLOW || 0);
|
|
455
|
-
const fd = fs.openSync(file, flags,
|
|
468
|
+
const fd = fs.openSync(file, flags, mode);
|
|
456
469
|
try {
|
|
457
470
|
fs.writeFileSync(fd, content);
|
|
458
471
|
} finally {
|
|
@@ -467,17 +480,18 @@ function appendNoFollow(file, content) {
|
|
|
467
480
|
*
|
|
468
481
|
* @param {string} file - the file the lines were read from
|
|
469
482
|
* @param {Array<{ line: number, text: string }>} rejected
|
|
470
|
-
* @param {{ now?: number }} [opts] - now: epoch ms (default Date.now())
|
|
483
|
+
* @param {{ now?: number, mode?: number }} [opts] - now: epoch ms (default Date.now());
|
|
484
|
+
* mode: the mode of a quarantine file this call creates (default as the umask leaves it)
|
|
471
485
|
* @returns {number} the number of records appended
|
|
472
486
|
*/
|
|
473
|
-
function quarantineRejected(file, rejected, { now = Date.now() } = {}) {
|
|
487
|
+
function quarantineRejected(file, rejected, { now = Date.now(), mode } = {}) {
|
|
474
488
|
if (rejected.length === 0) return 0;
|
|
475
489
|
const rejectedAt = new Date(now).toISOString();
|
|
476
490
|
const source = path.basename(file);
|
|
477
491
|
const content = rejected
|
|
478
492
|
.map(r => JSON.stringify({ rejected_at: rejectedAt, source, line: r.line, text: r.text }))
|
|
479
493
|
.join('\n') + '\n';
|
|
480
|
-
appendNoFollow(rejectedPathFor(file), content);
|
|
494
|
+
appendNoFollow(rejectedPathFor(file), content, mode);
|
|
481
495
|
return rejected.length;
|
|
482
496
|
}
|
|
483
497
|
|
|
@@ -545,14 +559,14 @@ function writeJsonlAtomic(file, rows) {
|
|
|
545
559
|
// ---------------------------------------------------------------------------
|
|
546
560
|
|
|
547
561
|
/**
|
|
548
|
-
* True when `<root>/.devflow/
|
|
562
|
+
* True when `<root>/.devflow/curation` exists and is a directory.
|
|
549
563
|
*
|
|
550
564
|
* @param {string} root - project root
|
|
551
565
|
* @returns {boolean}
|
|
552
566
|
*/
|
|
553
|
-
function
|
|
567
|
+
function hasCurationDir(root) {
|
|
554
568
|
try {
|
|
555
|
-
return fs.statSync(
|
|
569
|
+
return fs.statSync(getCurationDir(root)).isDirectory();
|
|
556
570
|
} catch (err) {
|
|
557
571
|
if (err && (err.code === 'ENOENT' || err.code === 'ENOTDIR')) return false;
|
|
558
572
|
throw err;
|
|
@@ -560,17 +574,17 @@ function hasLearningDir(root) {
|
|
|
560
574
|
}
|
|
561
575
|
|
|
562
576
|
/**
|
|
563
|
-
* The error Result of an op run where `<root>/.devflow/
|
|
577
|
+
* The error Result of an op run where `<root>/.devflow/curation/` is absent
|
|
564
578
|
* (D-NO-STRAY-TREE).
|
|
565
579
|
*
|
|
566
580
|
* @param {string} opName - operation name, for the message
|
|
567
581
|
* @param {string} root - project root
|
|
568
|
-
* @returns {{ ok: false, error: { kind: 'no-
|
|
582
|
+
* @returns {{ ok: false, error: { kind: 'no-curation-dir', message: string } }}
|
|
569
583
|
*/
|
|
570
|
-
function
|
|
584
|
+
function noCurationDir(opName, root) {
|
|
571
585
|
return {
|
|
572
586
|
ok: false,
|
|
573
|
-
error: { kind: 'no-
|
|
587
|
+
error: { kind: 'no-curation-dir', message: `${opName}: no .devflow/curation/ under ${root} — run from the project root` },
|
|
574
588
|
};
|
|
575
589
|
}
|
|
576
590
|
|
|
@@ -585,22 +599,22 @@ function isSymbolicLink(file) {
|
|
|
585
599
|
}
|
|
586
600
|
|
|
587
601
|
/**
|
|
588
|
-
* The first of `<root>/.devflow` and `<root>/.devflow/
|
|
602
|
+
* The first of `<root>/.devflow` and `<root>/.devflow/curation` that is itself a
|
|
589
603
|
* symbolic link, or null when neither is (D-NO-LINKED-TREE).
|
|
590
604
|
*
|
|
591
605
|
* @param {string} root - project root
|
|
592
606
|
* @returns {string|null}
|
|
593
607
|
*/
|
|
594
|
-
function
|
|
595
|
-
const
|
|
596
|
-
for (const dir of [path.dirname(
|
|
608
|
+
function linkedCurationFolder(root) {
|
|
609
|
+
const curationDir = getCurationDir(root);
|
|
610
|
+
for (const dir of [path.dirname(curationDir), curationDir]) {
|
|
597
611
|
if (isSymbolicLink(dir)) return dir;
|
|
598
612
|
}
|
|
599
613
|
return null;
|
|
600
614
|
}
|
|
601
615
|
|
|
602
616
|
/**
|
|
603
|
-
* The error Result of an op run where `.devflow` or `.devflow/
|
|
617
|
+
* The error Result of an op run where `.devflow` or `.devflow/curation` under its
|
|
604
618
|
* root is a symbolic link (D-NO-LINKED-TREE).
|
|
605
619
|
*
|
|
606
620
|
* @param {string} opName - operation name, for the message
|
|
@@ -614,6 +628,82 @@ function linkedFolder(opName, dir) {
|
|
|
614
628
|
};
|
|
615
629
|
}
|
|
616
630
|
|
|
631
|
+
/** The Stats of `file` without following a link, or null when nothing is there. */
|
|
632
|
+
function lstatOrNull(file) {
|
|
633
|
+
try {
|
|
634
|
+
return fs.lstatSync(file);
|
|
635
|
+
} catch (err) {
|
|
636
|
+
if (err && (err.code === 'ENOENT' || err.code === 'ENOTDIR')) return null;
|
|
637
|
+
throw err;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** The physical path of `target`, or its resolved path when it does not exist yet. */
|
|
642
|
+
function physicalPath(target) {
|
|
643
|
+
try {
|
|
644
|
+
return fs.realpathSync(target);
|
|
645
|
+
} catch (err) {
|
|
646
|
+
if (err && (err.code === 'ENOENT' || err.code === 'ENOTDIR')) return path.resolve(target);
|
|
647
|
+
throw err;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** The home directory: `HOME`, else the passwd entry, else null. */
|
|
652
|
+
function homeDirOrNull() {
|
|
653
|
+
if (process.env.HOME) return process.env.HOME;
|
|
654
|
+
try {
|
|
655
|
+
return os.homedir() || null;
|
|
656
|
+
} catch {
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Where a root's move journal is, resolved the way moveLedger resolves its machine
|
|
663
|
+
* directory (`$HOME/.devflow`) and the root's key (the root's physical path); null when
|
|
664
|
+
* no home directory is known.
|
|
665
|
+
*/
|
|
666
|
+
function moveJournalPathOrNull(root) {
|
|
667
|
+
const home = homeDirOrNull();
|
|
668
|
+
if (home === null) return null;
|
|
669
|
+
return curationMoveJournalPath(path.join(home, '.devflow'), physicalPath(path.resolve(root)));
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Remove a machine-side journal whose old folder is gone: it has no set left to
|
|
674
|
+
* finish. Only a regular file is removed, never a link or any other entry, and nothing
|
|
675
|
+
* is followed. It never throws: a journal that cannot be removed (EACCES, EPERM, EROFS
|
|
676
|
+
* on the machine directory) stays, and the writer goes on, because the journal holds
|
|
677
|
+
* no writer once its old folder is gone. A later write tries again.
|
|
678
|
+
*
|
|
679
|
+
* @param {string} journal
|
|
680
|
+
*/
|
|
681
|
+
function dropStaleMoveJournal(journal) {
|
|
682
|
+
try {
|
|
683
|
+
const stat = lstatOrNull(journal);
|
|
684
|
+
if (stat === null || !stat.isFile()) return;
|
|
685
|
+
removeIfPresent(journal);
|
|
686
|
+
} catch {
|
|
687
|
+
// Best effort only: see the note above.
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Whether the journal at `journal` holds store writers off: it exists and the old
|
|
693
|
+
* folder it belongs to still exists. A journal whose old folder is gone is stale and is
|
|
694
|
+
* dropped (dropStaleMoveJournal), and the writer goes on.
|
|
695
|
+
*
|
|
696
|
+
* @param {string} journal
|
|
697
|
+
* @param {string} oldFolder
|
|
698
|
+
* @returns {boolean}
|
|
699
|
+
*/
|
|
700
|
+
function moveJournalHoldsWriters(journal, oldFolder) {
|
|
701
|
+
if (lstatOrNull(journal) === null) return false;
|
|
702
|
+
if (lstatOrNull(oldFolder) !== null) return true;
|
|
703
|
+
dropStaleMoveJournal(journal);
|
|
704
|
+
return false;
|
|
705
|
+
}
|
|
706
|
+
|
|
617
707
|
/** True for a Result: `{ ok: true, … }` or `{ ok: false, error: { … } }`. */
|
|
618
708
|
function isResult(value) {
|
|
619
709
|
return isPlainObject(value) && (value.ok === true || (value.ok === false && isPlainObject(value.error)));
|
|
@@ -624,20 +714,30 @@ function isResult(value) {
|
|
|
624
714
|
* The lock is released on every path, a throw from `fn` included; the throw then
|
|
625
715
|
* reaches the caller.
|
|
626
716
|
*
|
|
627
|
-
* D-ONE-LEARNING-LOCK:
|
|
717
|
+
* D-ONE-LEARNING-LOCK: curation writers take one lock, `.decisions.lock`, through
|
|
628
718
|
* this wrapper — every write to the log, the ledger, their side files and the
|
|
629
719
|
* rendered files happens under it. Reason: two writers under two locks can each
|
|
630
720
|
* read one file, change it and rename their copy over it, and the second rename
|
|
631
721
|
* silently discards the first one's change.
|
|
632
722
|
*
|
|
633
|
-
* D-NO-STRAY-TREE: a
|
|
634
|
-
* `.devflow/
|
|
723
|
+
* D-NO-STRAY-TREE: a curation writer refuses with `no-curation-dir` when
|
|
724
|
+
* `.devflow/curation/` is absent under its root, and the store never creates that
|
|
635
725
|
* directory or its parent — the lock directory is the only thing made inside it.
|
|
636
|
-
* Reason: a writer run from the wrong directory would otherwise create a
|
|
726
|
+
* Reason: a writer run from the wrong directory would otherwise create a curation
|
|
637
727
|
* tree there and write a ledger that no session ever reads.
|
|
638
728
|
*
|
|
639
|
-
*
|
|
640
|
-
*
|
|
729
|
+
* An unfinished ledger move (a move journal in the machine directory, next to the move
|
|
730
|
+
* lock, see moveLedger in curation-move.cjs) makes every writer answer `busy` without taking the lock, so no
|
|
731
|
+
* write lands between two renames of the move's ledger set. The journal is checked
|
|
732
|
+
* again once the lock is held: a move can start, and stop part way through the set,
|
|
733
|
+
* while the writer waits for the lock. Only the machine-side file counts, and only
|
|
734
|
+
* while the old folder it belongs to exists (moveJournalHoldsWriters): a file in the
|
|
735
|
+
* repository's old folder is repository content and is never read. With no home
|
|
736
|
+
* directory no move can have started (moveLedger refuses without a machine directory),
|
|
737
|
+
* so there is no journal to find and the writer goes on.
|
|
738
|
+
*
|
|
739
|
+
* D-NO-LINKED-TREE: a curation writer refuses with `not-a-directory`, changing
|
|
740
|
+
* nothing, when `.devflow` or `.devflow/curation` under its root is a symbolic
|
|
641
741
|
* link. Reason: a repository can commit either one as a link to a folder elsewhere
|
|
642
742
|
* on the machine, and a writer that followed it would take its lock there and
|
|
643
743
|
* rewrite, quarantine, archive, render or delete files in whatever folder the link
|
|
@@ -649,25 +749,29 @@ function isResult(value) {
|
|
|
649
749
|
* @param {() => { ok: boolean }} fn - the locked body; it must return a Result
|
|
650
750
|
* @param {{ timeoutMs?: number, staleMs?: number }} [opts]
|
|
651
751
|
* @returns {{ ok: true, value?: unknown } | { ok: false, error: { kind: string, message: string } }}
|
|
652
|
-
* fn's Result, or an error of kind `not-a-directory`, `no-
|
|
752
|
+
* fn's Result, or an error of kind `not-a-directory`, `no-curation-dir` or `busy`.
|
|
653
753
|
*/
|
|
654
754
|
function withDecisionsLock(opName, root, fn, { timeoutMs = LOCK_ACQUIRE_TIMEOUT_MS, staleMs = LOCK_STALE_MS } = {}) {
|
|
655
|
-
const linked =
|
|
755
|
+
const linked = linkedCurationFolder(root);
|
|
656
756
|
if (linked !== null) return linkedFolder(opName, linked);
|
|
657
|
-
|
|
757
|
+
const journal = moveJournalPathOrNull(root);
|
|
758
|
+
const oldFolder = path.join(root, '.devflow', LEGACY_NAMES.folder);
|
|
759
|
+
if (journal !== null && moveJournalHoldsWriters(journal, oldFolder)) return unfinishedMove(opName, journal);
|
|
760
|
+
if (!hasCurationDir(root)) return noCurationDir(opName, root);
|
|
658
761
|
const lockDir = getDecisionsLockDir(root);
|
|
659
762
|
let acquired;
|
|
660
763
|
try {
|
|
661
764
|
acquired = acquireMkdirLock(lockDir, timeoutMs, staleMs);
|
|
662
765
|
} catch (err) {
|
|
663
|
-
// The
|
|
664
|
-
if (err && err.code === 'ENOENT') return
|
|
766
|
+
// The curation directory went away between the check and the mkdir.
|
|
767
|
+
if (err && err.code === 'ENOENT') return noCurationDir(opName, root);
|
|
665
768
|
throw err;
|
|
666
769
|
}
|
|
667
770
|
if (!acquired) {
|
|
668
771
|
return { ok: false, error: { kind: 'busy', message: `${opName}: timeout acquiring lock at ${lockDir}` } };
|
|
669
772
|
}
|
|
670
773
|
try {
|
|
774
|
+
if (journal !== null && moveJournalHoldsWriters(journal, oldFolder)) return unfinishedMove(opName, journal);
|
|
671
775
|
const result = fn();
|
|
672
776
|
if (!isResult(result)) {
|
|
673
777
|
throw new TypeError(`${opName}: the locked body must return a Result ({ ok: true, value } or { ok: false, error })`);
|
|
@@ -678,6 +782,14 @@ function withDecisionsLock(opName, root, fn, { timeoutMs = LOCK_ACQUIRE_TIMEOUT_
|
|
|
678
782
|
}
|
|
679
783
|
}
|
|
680
784
|
|
|
785
|
+
/** The `busy` Result of a writer that finds an unfinished ledger move's journal. */
|
|
786
|
+
function unfinishedMove(opName, journal) {
|
|
787
|
+
return {
|
|
788
|
+
ok: false,
|
|
789
|
+
error: { kind: 'busy', message: `${opName}: an unfinished ledger move is recorded at ${journal}; it finishes at the next session start or devflow init` },
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
|
|
681
793
|
// ---------------------------------------------------------------------------
|
|
682
794
|
// State and the ledger registry
|
|
683
795
|
// ---------------------------------------------------------------------------
|
|
@@ -690,7 +802,7 @@ function withDecisionsLock(opName, root, fn, { timeoutMs = LOCK_ACQUIRE_TIMEOUT_
|
|
|
690
802
|
* @param {string} root - project root
|
|
691
803
|
* @returns {{ ledgerRows: object[], logRows: object[], rejected: { ledger: Array<{ line: number, text: string }>, log: Array<{ line: number, text: string }> } }}
|
|
692
804
|
*/
|
|
693
|
-
function
|
|
805
|
+
function readCurationState(root) {
|
|
694
806
|
const ledger = readJsonl(getDecisionsLedgerPath(root));
|
|
695
807
|
const log = readJsonl(getDecisionsLogPath(root));
|
|
696
808
|
return {
|
|
@@ -1191,7 +1303,7 @@ function ensurePreV2Backup(root, { logRows = [], ledgerRows = [] } = {}) {
|
|
|
1191
1303
|
/** Days of inactivity after which an observation no ledger row carries leaves the log (D-ROTATE-UNREFERENCED). */
|
|
1192
1304
|
const ROTATE_AGE_DAYS = 30;
|
|
1193
1305
|
|
|
1194
|
-
/** What an older install's usage telemetry left in the
|
|
1306
|
+
/** What an older install's usage telemetry left in the curation directory: a file and a lock directory. */
|
|
1195
1307
|
const USAGE_LEFTOVERS = Object.freeze(['.decisions-usage.json', '.decisions-usage.lock']);
|
|
1196
1308
|
|
|
1197
1309
|
/**
|
|
@@ -1221,7 +1333,7 @@ function lastActivityMs(row) {
|
|
|
1221
1333
|
* anchor_id copied onto a log row can say what to keep, and a dedup by id dropped
|
|
1222
1334
|
* the newer version of a row whose older version was already archived.
|
|
1223
1335
|
*
|
|
1224
|
-
* Under the
|
|
1336
|
+
* Under the curation lock it first deletes the usage leftovers. When no row is
|
|
1225
1337
|
* due it writes nothing else. When rows are due it backs up a v1 tree
|
|
1226
1338
|
* (D-V1-BACKUP-ONCE) and quarantines the log's malformed lines
|
|
1227
1339
|
* (D-QUARANTINE-MALFORMED) before it appends to the archive and rewrites the log;
|
|
@@ -1232,12 +1344,12 @@ function lastActivityMs(row) {
|
|
|
1232
1344
|
* @param {{ now?: number, timeoutMs?: number }} [opts] - now: epoch ms (default Date.now())
|
|
1233
1345
|
* @returns {{ ok: true, value: { rotated: number, appended: number } } | { ok: false, error: { kind: string, message: string } }}
|
|
1234
1346
|
* rotated: rows removed from the log; appended: rows added to the archive.
|
|
1235
|
-
* Errors are withDecisionsLock's not-a-directory, no-
|
|
1347
|
+
* Errors are withDecisionsLock's not-a-directory, no-curation-dir and busy.
|
|
1236
1348
|
*/
|
|
1237
1349
|
function rotateObservations(root, { now = Date.now(), timeoutMs } = {}) {
|
|
1238
1350
|
return withDecisionsLock('rotate-observations', root, () => {
|
|
1239
1351
|
for (const name of USAGE_LEFTOVERS) {
|
|
1240
|
-
fs.rmSync(path.join(
|
|
1352
|
+
fs.rmSync(path.join(getCurationDir(root), name), { recursive: true, force: true });
|
|
1241
1353
|
}
|
|
1242
1354
|
|
|
1243
1355
|
const logPath = getDecisionsLogPath(root);
|
|
@@ -1276,7 +1388,7 @@ function rotateObservations(root, { now = Date.now(), timeoutMs } = {}) {
|
|
|
1276
1388
|
// ---------------------------------------------------------------------------
|
|
1277
1389
|
|
|
1278
1390
|
/**
|
|
1279
|
-
* Drop the observations no entry uses — `devflow
|
|
1391
|
+
* Drop the observations no entry uses — `devflow curation --clear`.
|
|
1280
1392
|
*
|
|
1281
1393
|
* D-CLEAR-UNREFERENCED: clearing removes from the log exactly the rows no ledger
|
|
1282
1394
|
* row carries (D-LEDGER-REGISTRY), whatever their age or status, and keeps every
|
|
@@ -1286,7 +1398,7 @@ function rotateObservations(root, { now = Date.now(), timeoutMs } = {}) {
|
|
|
1286
1398
|
* row that is its content authority, and refresh then refused them all — and a
|
|
1287
1399
|
* malformed ledger line may be the one carrying a row clearing would drop.
|
|
1288
1400
|
*
|
|
1289
|
-
* It runs under the
|
|
1401
|
+
* It runs under the curation lock (D-ONE-LEARNING-LOCK). When a row is dropped it
|
|
1290
1402
|
* backs up a v1 tree (D-V1-BACKUP-ONCE) and quarantines the log's malformed lines
|
|
1291
1403
|
* (D-QUARANTINE-MALFORMED) before it rewrites the log; with nothing to drop it
|
|
1292
1404
|
* writes nothing.
|
|
@@ -1295,7 +1407,7 @@ function rotateObservations(root, { now = Date.now(), timeoutMs } = {}) {
|
|
|
1295
1407
|
* @param {{ now?: number, timeoutMs?: number }} [opts] - now: epoch ms (default Date.now())
|
|
1296
1408
|
* @returns {{ ok: true, value: { cleared: number, kept: number } } | { ok: false, error: { kind: string, message: string } }}
|
|
1297
1409
|
* cleared: rows removed from the log; kept: rows left in it. Error kinds:
|
|
1298
|
-
* ledger-malformed, and withDecisionsLock's not-a-directory, no-
|
|
1410
|
+
* ledger-malformed, and withDecisionsLock's not-a-directory, no-curation-dir and busy.
|
|
1299
1411
|
*/
|
|
1300
1412
|
function clearUnreferenced(root, { now = Date.now(), timeoutMs } = {}) {
|
|
1301
1413
|
return withDecisionsLock('clear', root, () => {
|
|
@@ -1340,11 +1452,11 @@ function removeEmptyDir(dir) {
|
|
|
1340
1452
|
}
|
|
1341
1453
|
|
|
1342
1454
|
/**
|
|
1343
|
-
* Remove every
|
|
1455
|
+
* Remove every curation file — `devflow curation --reset`: the log, the ledger
|
|
1344
1456
|
* and their side files, the rendered files, the tuning config, and the queue
|
|
1345
|
-
* with its claim and owner file — and then the
|
|
1457
|
+
* with its claim and owner file — and then the curation directory itself.
|
|
1346
1458
|
*
|
|
1347
|
-
* D-RESET-UNDER-LOCK: reset empties the
|
|
1459
|
+
* D-RESET-UNDER-LOCK: reset empties the curation directory under the curation
|
|
1348
1460
|
* lock, sparing only the lock directory, and removes the emptied directory once
|
|
1349
1461
|
* the lock is released, and only if nothing has arrived in it. Reason: the lock
|
|
1350
1462
|
* is released by its path, so removing it with the directory would let this
|
|
@@ -1352,8 +1464,8 @@ function removeEmptyDir(dir) {
|
|
|
1352
1464
|
* and what arrives once the lock is free — a captured turn, or the next writer's
|
|
1353
1465
|
* lock — belongs to the next run.
|
|
1354
1466
|
*
|
|
1355
|
-
* Like every
|
|
1356
|
-
* nothing (D-NO-STRAY-TREE), refuses a
|
|
1467
|
+
* Like every curation writer it refuses without `.devflow/curation/` and creates
|
|
1468
|
+
* nothing (D-NO-STRAY-TREE), refuses a curation directory or a `.devflow` that is a
|
|
1357
1469
|
* symbolic link and removes nothing (D-NO-LINKED-TREE), since emptying it would
|
|
1358
1470
|
* empty whatever directory the link leads to, and waits at most `timeoutMs` for the
|
|
1359
1471
|
* lock, breaking one a crashed run left behind (D-ONE-LEARNING-LOCK). A symbolic
|
|
@@ -1362,18 +1474,18 @@ function removeEmptyDir(dir) {
|
|
|
1362
1474
|
* @param {string} root - project root
|
|
1363
1475
|
* @param {{ timeoutMs?: number }} [opts]
|
|
1364
1476
|
* @returns {{ ok: true, value: { removed: number } } | { ok: false, error: { kind: string, message: string } }}
|
|
1365
|
-
* removed: the entries removed from the
|
|
1366
|
-
* withDecisionsLock's not-a-directory, no-
|
|
1477
|
+
* removed: the entries removed from the curation directory. Errors are
|
|
1478
|
+
* withDecisionsLock's not-a-directory, no-curation-dir and busy.
|
|
1367
1479
|
*/
|
|
1368
|
-
function
|
|
1369
|
-
const
|
|
1480
|
+
function resetCuration(root, { timeoutMs } = {}) {
|
|
1481
|
+
const curationDir = getCurationDir(root);
|
|
1370
1482
|
const lockName = path.basename(getDecisionsLockDir(root));
|
|
1371
1483
|
const reset = withDecisionsLock('reset', root, () => {
|
|
1372
|
-
const entries = fs.readdirSync(
|
|
1373
|
-
for (const name of entries) fs.rmSync(path.join(
|
|
1484
|
+
const entries = fs.readdirSync(curationDir).filter(name => name !== lockName);
|
|
1485
|
+
for (const name of entries) fs.rmSync(path.join(curationDir, name), { recursive: true, force: true });
|
|
1374
1486
|
return { ok: true, value: { removed: entries.length } };
|
|
1375
1487
|
}, { timeoutMs });
|
|
1376
|
-
if (reset.ok) removeEmptyDir(
|
|
1488
|
+
if (reset.ok) removeEmptyDir(curationDir);
|
|
1377
1489
|
return reset;
|
|
1378
1490
|
}
|
|
1379
1491
|
|
|
@@ -1391,7 +1503,7 @@ const CLAIM_STALE_SECS = 900;
|
|
|
1391
1503
|
/** How long a claim waits for the queue's own lock (ms): the wait of queue-append's overflow truncation. */
|
|
1392
1504
|
const QUEUE_LOCK_TIMEOUT_MS = 2000;
|
|
1393
1505
|
|
|
1394
|
-
/** Age after which the queue's own lock counts as abandoned (ms):
|
|
1506
|
+
/** Age after which the queue's own lock counts as abandoned (ms): curation-lock's threshold. */
|
|
1395
1507
|
const QUEUE_LOCK_STALE_MS = 30000;
|
|
1396
1508
|
|
|
1397
1509
|
/** A claim token: 16 lowercase hex characters. */
|
|
@@ -1462,7 +1574,7 @@ function removeIfPresent(file) {
|
|
|
1462
1574
|
* larger than CLAIM_OWNER_MAX_BYTES reads as absent (D-NO-LINKED-READ, at readJsonl).
|
|
1463
1575
|
*/
|
|
1464
1576
|
function readClaimOwner(root) {
|
|
1465
|
-
const text = readTextUnlinked(
|
|
1577
|
+
const text = readTextUnlinked(getCurationClaimOwnerPath(root), { maxBytes: CLAIM_OWNER_MAX_BYTES });
|
|
1466
1578
|
if (text === null) return null;
|
|
1467
1579
|
const token = text.trim();
|
|
1468
1580
|
return CLAIM_TOKEN_RE.test(token) ? token : null;
|
|
@@ -1473,7 +1585,7 @@ function readClaimOwner(root) {
|
|
|
1473
1585
|
* queue-append's overflow truncation takes, so a truncation can never rewrite the
|
|
1474
1586
|
* queue from rows already claimed. link(2) refuses an existing claim; on a
|
|
1475
1587
|
* filesystem without hard links a rename stands in, which is safe because the
|
|
1476
|
-
* caller found the claim path empty under the
|
|
1588
|
+
* caller found the claim path empty under the curation lock. A row a capture hook
|
|
1477
1589
|
* appends meanwhile lands in the claimed batch or in the queue formed after it,
|
|
1478
1590
|
* never in neither.
|
|
1479
1591
|
*
|
|
@@ -1516,22 +1628,22 @@ function moveQueueToClaim(queuePath, claimPath) {
|
|
|
1516
1628
|
}
|
|
1517
1629
|
|
|
1518
1630
|
/**
|
|
1519
|
-
* Claim the
|
|
1631
|
+
* Claim the curation queue for one Curate run.
|
|
1520
1632
|
*
|
|
1521
|
-
* D-OWNED-CLAIM: the
|
|
1522
|
-
* claim-queue and release-claim ops, under the
|
|
1633
|
+
* D-OWNED-CLAIM: the curation queue is claimed and released only through the
|
|
1634
|
+
* claim-queue and release-claim ops, under the curation lock. A claim moves the
|
|
1523
1635
|
* queue to .pending-turns.processing by link(2), under the queue's own lock (a
|
|
1524
1636
|
* rename stands in only where the filesystem has no hard links), sets the claim's
|
|
1525
1637
|
* mtime to now and records a fresh random token in .pending-turns.owner. A claim
|
|
1526
1638
|
* younger than CLAIM_STALE_SECS is busy to every other claimant; an older one is
|
|
1527
1639
|
* taken over with a new token, and the waiting queue is left for the next claim.
|
|
1528
1640
|
* Release deletes the claim only for the token that owns it, and every json-helper
|
|
1529
|
-
*
|
|
1641
|
+
* curation op refreshes an existing claim's mtime before it runs, without ever
|
|
1530
1642
|
* creating one. Reason: a check-then-mv claim let two runs claim at once and
|
|
1531
1643
|
* clobber a batch, mv kept the queue's old mtime so a fresh claim could look stale
|
|
1532
1644
|
* at once, and an unconditional final unlink deleted another run's claim.
|
|
1533
1645
|
*
|
|
1534
|
-
* Without .devflow/
|
|
1646
|
+
* Without .devflow/curation/ it answers none and creates nothing.
|
|
1535
1647
|
*
|
|
1536
1648
|
* @param {string} root - project root
|
|
1537
1649
|
* @param {{ now?: number, token?: string, timeoutMs?: number }} [opts]
|
|
@@ -1542,17 +1654,17 @@ function moveQueueToClaim(queuePath, claimPath) {
|
|
|
1542
1654
|
*/
|
|
1543
1655
|
function claimQueue(root, { now = Date.now(), token = newClaimToken(), timeoutMs } = {}) {
|
|
1544
1656
|
assertClaimToken('claimQueue', token);
|
|
1545
|
-
if (!
|
|
1657
|
+
if (!hasCurationDir(root)) return { ok: true, value: { state: 'none' } };
|
|
1546
1658
|
return withDecisionsLock('claim-queue', root, () => {
|
|
1547
|
-
const claimPath =
|
|
1548
|
-
const queuePath =
|
|
1659
|
+
const claimPath = getCurationPendingTurnsProcessingPath(root);
|
|
1660
|
+
const queuePath = getCurationPendingTurnsPath(root);
|
|
1549
1661
|
const at = new Date(now);
|
|
1550
1662
|
|
|
1551
1663
|
const claim = regularFileStat(claimPath);
|
|
1552
1664
|
if (claim === false) return notRegularFile('claim-queue', claimPath);
|
|
1553
1665
|
if (claim !== null) {
|
|
1554
1666
|
if (now - claim.mtimeMs < CLAIM_STALE_SECS * 1000) return { ok: true, value: { state: 'busy' } };
|
|
1555
|
-
writeFileAtomic(
|
|
1667
|
+
writeFileAtomic(getCurationClaimOwnerPath(root), `${token}\n`);
|
|
1556
1668
|
fs.utimesSync(claimPath, at, at);
|
|
1557
1669
|
return { ok: true, value: { state: 'claimed', token, takeover: true } };
|
|
1558
1670
|
}
|
|
@@ -1564,7 +1676,7 @@ function claimQueue(root, { now = Date.now(), token = newClaimToken(), timeoutMs
|
|
|
1564
1676
|
const moved = moveQueueToClaim(queuePath, claimPath);
|
|
1565
1677
|
if (moved !== 'moved') return { ok: true, value: { state: moved } };
|
|
1566
1678
|
fs.utimesSync(claimPath, at, at);
|
|
1567
|
-
writeFileAtomic(
|
|
1679
|
+
writeFileAtomic(getCurationClaimOwnerPath(root), `${token}\n`);
|
|
1568
1680
|
return { ok: true, value: { state: 'claimed', token, takeover: false } };
|
|
1569
1681
|
}, { timeoutMs });
|
|
1570
1682
|
}
|
|
@@ -1581,14 +1693,14 @@ function claimQueue(root, { now = Date.now(), token = newClaimToken(), timeoutMs
|
|
|
1581
1693
|
* @param {string} token
|
|
1582
1694
|
* @param {{ timeoutMs?: number }} [opts]
|
|
1583
1695
|
* @returns {{ ok: true, value: { state: 'released'|'not-owner'|'gone' } } | { ok: false, error: { kind: string, message: string } }}
|
|
1584
|
-
* errors include withDecisionsLock's not-a-directory, no-
|
|
1696
|
+
* errors include withDecisionsLock's not-a-directory, no-curation-dir and busy
|
|
1585
1697
|
* @throws {TypeError} when `token` is not a claim token
|
|
1586
1698
|
*/
|
|
1587
1699
|
function releaseClaim(root, token, { timeoutMs } = {}) {
|
|
1588
1700
|
assertClaimToken('releaseClaim', token);
|
|
1589
1701
|
return withDecisionsLock('release-claim', root, () => {
|
|
1590
|
-
const claimPath =
|
|
1591
|
-
const ownerPath =
|
|
1702
|
+
const claimPath = getCurationPendingTurnsProcessingPath(root);
|
|
1703
|
+
const ownerPath = getCurationClaimOwnerPath(root);
|
|
1592
1704
|
const owned = readClaimOwner(root) === token;
|
|
1593
1705
|
|
|
1594
1706
|
const claim = regularFileStat(claimPath);
|
|
@@ -1607,8 +1719,8 @@ function releaseClaim(root, token, { timeoutMs } = {}) {
|
|
|
1607
1719
|
/**
|
|
1608
1720
|
* The claim heartbeat (D-OWNED-CLAIM): set an existing claim's mtime to now. It
|
|
1609
1721
|
* never creates a claim and never follows a symlink at the claim path, and it
|
|
1610
|
-
* takes no lock — json-helper sends it before each
|
|
1611
|
-
*
|
|
1722
|
+
* takes no lock — json-helper sends it before each curation op runs. A claim in a
|
|
1723
|
+
* curation tree reached through a symbolic link is left alone (D-NO-LINKED-TREE):
|
|
1612
1724
|
* `lutimes` follows a linked folder above the claim, and the op that follows
|
|
1613
1725
|
* refuses that tree anyway.
|
|
1614
1726
|
*
|
|
@@ -1617,8 +1729,8 @@ function releaseClaim(root, token, { timeoutMs } = {}) {
|
|
|
1617
1729
|
* @returns {{ ok: true, value: { touched: boolean } } | { ok: false, error: { kind: 'heartbeat-failed', message: string } }}
|
|
1618
1730
|
*/
|
|
1619
1731
|
function touchClaim(root, { now = Date.now() } = {}) {
|
|
1620
|
-
if (
|
|
1621
|
-
const claimPath =
|
|
1732
|
+
if (linkedCurationFolder(root) !== null) return { ok: true, value: { touched: false } };
|
|
1733
|
+
const claimPath = getCurationPendingTurnsProcessingPath(root);
|
|
1622
1734
|
const at = new Date(now);
|
|
1623
1735
|
try {
|
|
1624
1736
|
fs.lutimesSync(claimPath, at, at);
|
|
@@ -1950,16 +2062,17 @@ function resolveVerifyRef(root) {
|
|
|
1950
2062
|
/**
|
|
1951
2063
|
* Render decisions.md, pitfalls.md and index.md from `ledgerRows` and write each
|
|
1952
2064
|
* atomically, the index last. The caller holds .decisions.lock
|
|
1953
|
-
* (D-ONE-LEARNING-LOCK), so the
|
|
2065
|
+
* (D-ONE-LEARNING-LOCK), so the curation directory exists. render-decisions.cjs
|
|
1954
2066
|
* requires this module at load time, so it is required here, on first use. Prints
|
|
1955
2067
|
* nothing.
|
|
1956
2068
|
*
|
|
1957
2069
|
* @param {string} root - project root
|
|
1958
2070
|
* @param {object[]} ledgerRows - every ledger row
|
|
2071
|
+
* @param {string} [outputDir] - the folder the files belong in (default: the curation folder)
|
|
1959
2072
|
*/
|
|
1960
|
-
function renderAll(root, ledgerRows) {
|
|
1961
|
-
const {
|
|
1962
|
-
for (const file of
|
|
2073
|
+
function renderAll(root, ledgerRows, outputDir = getCurationDir(root)) {
|
|
2074
|
+
const { renderCurationFiles } = require('./render-decisions.cjs');
|
|
2075
|
+
for (const file of renderCurationFiles(root, ledgerRows, outputDir)) writeFileAtomic(file.path, file.content);
|
|
1963
2076
|
}
|
|
1964
2077
|
|
|
1965
2078
|
// ---------------------------------------------------------------------------
|
|
@@ -2118,7 +2231,7 @@ function restoreFirst(id, carriers) {
|
|
|
2118
2231
|
* observations: the count the log row holds afterwards; reprojected: the
|
|
2119
2232
|
* anchors re-projected, in anchor order. Error kinds: invalid-input (with
|
|
2120
2233
|
* problems), duplicate-log-id, restore-first, cannot-reproject, and
|
|
2121
|
-
* withDecisionsLock's not-a-directory, no-
|
|
2234
|
+
* withDecisionsLock's not-a-directory, no-curation-dir and busy.
|
|
2122
2235
|
* @throws {TypeError} when `mode` is not a put mode
|
|
2123
2236
|
*/
|
|
2124
2237
|
function putObservation(root, mode, input, { now = Date.now(), timeoutMs, scopeMatches } = {}) {
|
|
@@ -2190,16 +2303,16 @@ function putUnderLock(root, mode, input, { now, scopeMatches }) {
|
|
|
2190
2303
|
* The data behind `list`, read-only: buildListing over the ledger and the log as
|
|
2191
2304
|
* they are, every glob scope checked against the files git tracks. Malformed
|
|
2192
2305
|
* lines are counted, never quarantined (D-QUARANTINE-MALFORMED). Refuses without
|
|
2193
|
-
* .devflow/
|
|
2306
|
+
* .devflow/curation/ (D-NO-STRAY-TREE).
|
|
2194
2307
|
*
|
|
2195
2308
|
* @param {string} root - project root
|
|
2196
2309
|
* @param {{ scopeMatches?: (glob: string) => boolean }} [opts] - default gitScopeMatcher(root)
|
|
2197
2310
|
* @returns {{ ok: true, value: { active: object[], inactive: object[], observations: object[], integrity: object[], malformed: { ledger: number, log: number } } }
|
|
2198
|
-
* | { ok: false, error: { kind: 'no-
|
|
2311
|
+
* | { ok: false, error: { kind: 'no-curation-dir', message: string } }}
|
|
2199
2312
|
*/
|
|
2200
2313
|
function readListing(root, { scopeMatches } = {}) {
|
|
2201
|
-
if (!
|
|
2202
|
-
const { ledgerRows, logRows, rejected } =
|
|
2314
|
+
if (!hasCurationDir(root)) return noCurationDir('list', root);
|
|
2315
|
+
const { ledgerRows, logRows, rejected } = readCurationState(root);
|
|
2203
2316
|
return {
|
|
2204
2317
|
ok: true,
|
|
2205
2318
|
value: buildListing(ledgerRows, logRows, { scopeMatches: scopeMatches || gitScopeMatcher(root), rejected }),
|
|
@@ -2280,16 +2393,16 @@ function formatListing(listing) {
|
|
|
2280
2393
|
* the log and the history as they are. When lines were skipped as malformed
|
|
2281
2394
|
* (D-QUARANTINE-MALFORMED), a found entry gains `malformed: { ledger, log }` and
|
|
2282
2395
|
* a not-found message says a skipped line may hold it. Refuses without
|
|
2283
|
-
* .devflow/
|
|
2396
|
+
* .devflow/curation/ (D-NO-STRAY-TREE).
|
|
2284
2397
|
*
|
|
2285
2398
|
* @param {string} root - project root
|
|
2286
2399
|
* @param {string} key - an anchor id or an observation id
|
|
2287
2400
|
* @returns {{ ok: true, value: { key: string, ledger: object[], log: object|null, history_versions: object[], flags: object[], malformed?: { ledger: number, log: number } } }
|
|
2288
|
-
* | { ok: false, error: { kind: 'no-
|
|
2401
|
+
* | { ok: false, error: { kind: 'no-curation-dir'|'invalid-key'|'not-found', message: string } }}
|
|
2289
2402
|
*/
|
|
2290
2403
|
function showByKey(root, key) {
|
|
2291
|
-
if (!
|
|
2292
|
-
const { ledgerRows, logRows, rejected } =
|
|
2404
|
+
if (!hasCurationDir(root)) return noCurationDir('show', root);
|
|
2405
|
+
const { ledgerRows, logRows, rejected } = readCurationState(root);
|
|
2293
2406
|
const shown = showEntry(key, ledgerRows, logRows, { historyVersions: id => historyVersions(root, id) });
|
|
2294
2407
|
const malformed = { ledger: rejected.ledger.length, log: rejected.log.length };
|
|
2295
2408
|
const skipped = malformed.ledger + malformed.log;
|
|
@@ -2320,7 +2433,7 @@ function warmScopeMatcher(ledgerRows, scopeMatches) {
|
|
|
2320
2433
|
/**
|
|
2321
2434
|
* Hand out the entries maintenance works on next, and lease them.
|
|
2322
2435
|
*
|
|
2323
|
-
* Under the
|
|
2436
|
+
* Under the curation lock it flags integrity problems and selects the due
|
|
2324
2437
|
* entries (D-DUE-ORDER), then stamps each entry it hands out with
|
|
2325
2438
|
* `last_attempt` = now: claim-due is the one writer of the field the lease
|
|
2326
2439
|
* reads. A hand-out backs up a v1 tree first (D-V1-BACKUP-ONCE) and quarantines
|
|
@@ -2336,10 +2449,10 @@ function warmScopeMatcher(ledgerRows, scopeMatches) {
|
|
|
2336
2449
|
* @returns {{ ok: true, value: { ref: { ref: 'origin/HEAD'|'HEAD', commit: string } | null, due: Array<{ anchor_id: string, reason: string, bytes: number }> } }
|
|
2337
2450
|
* | { ok: false, error: { kind: string, message: string } }}
|
|
2338
2451
|
* due is selectDue's answer; errors are withDecisionsLock's not-a-directory,
|
|
2339
|
-
* no-
|
|
2452
|
+
* no-curation-dir and busy
|
|
2340
2453
|
*/
|
|
2341
2454
|
function claimDue(root, { now = Date.now(), timeoutMs, scopeMatches } = {}) {
|
|
2342
|
-
if (!
|
|
2455
|
+
if (!hasCurationDir(root)) return noCurationDir('claim-due', root);
|
|
2343
2456
|
const matches = scopeMatches || gitScopeMatcher(root);
|
|
2344
2457
|
const ref = resolveVerifyRef(root);
|
|
2345
2458
|
warmScopeMatcher(readJsonl(getDecisionsLedgerPath(root)).rows, matches);
|
|
@@ -2413,7 +2526,7 @@ function nextAnchorFromLedger(ledgerRows, type) {
|
|
|
2413
2526
|
|
|
2414
2527
|
/**
|
|
2415
2528
|
* True when a project-relative path is one the cited-number scan never reads: the
|
|
2416
|
-
*
|
|
2529
|
+
* curation tree, where every entry cites itself, or anything under an excluded
|
|
2417
2530
|
* directory segment.
|
|
2418
2531
|
*
|
|
2419
2532
|
* @param {string} relPath - relative to the project root, either separator style
|
|
@@ -2421,7 +2534,7 @@ function nextAnchorFromLedger(ledgerRows, type) {
|
|
|
2421
2534
|
*/
|
|
2422
2535
|
function isCitedScanExcluded(relPath) {
|
|
2423
2536
|
const norm = relPath.split(path.sep).join('/');
|
|
2424
|
-
if (norm === '.devflow/
|
|
2537
|
+
if (norm === '.devflow/curation' || norm.startsWith('.devflow/curation/')) return true;
|
|
2425
2538
|
return norm.split('/').some(segment => CITED_SCAN_EXCLUDED_SEGMENTS.includes(segment));
|
|
2426
2539
|
}
|
|
2427
2540
|
|
|
@@ -2432,7 +2545,7 @@ function isCitedScanExcluded(relPath) {
|
|
|
2432
2545
|
* command a repository's config names in `core.fsmonitor` — code chosen by the
|
|
2433
2546
|
* repository this hook runs inside. The call turns it off for itself
|
|
2434
2547
|
* (`-c core.fsmonitor=false`), so the listing stays a pure read. Reason: the
|
|
2435
|
-
*
|
|
2548
|
+
* curation ops run inside any repository a session opens, and a read that ran
|
|
2436
2549
|
* the repository's command would execute it with the user's privileges.
|
|
2437
2550
|
*
|
|
2438
2551
|
* @param {string} root - project root
|
|
@@ -2512,7 +2625,7 @@ function readScannedText(file) {
|
|
|
2512
2625
|
* Every anchor the project's files cite as a whole word, each mapped to its first
|
|
2513
2626
|
* citation: the scan behind D-E4-SKIP. It reads the files git tracks under `root`
|
|
2514
2627
|
* (D-NO-FSMONITOR) or, outside a git working tree, the files a directory walk
|
|
2515
|
-
* finds — never the
|
|
2628
|
+
* finds — never the curation tree, an excluded directory, a symbolic link, a
|
|
2516
2629
|
* binary file or a file over the size cap. A file it cannot read is passed over.
|
|
2517
2630
|
* It writes nothing and takes no lock.
|
|
2518
2631
|
*
|
|
@@ -2584,10 +2697,10 @@ function mintAnchor(ledgerRows, type, citedAnchors) {
|
|
|
2584
2697
|
* Promote a v2 observation to a new ledger entry of `type` — the assign-anchor op.
|
|
2585
2698
|
*
|
|
2586
2699
|
* D-E4-SKIP: assign-anchor scans the project's tracked files once, before it takes
|
|
2587
|
-
* the
|
|
2700
|
+
* the curation lock, for every anchor they cite as a whole word, and mints the
|
|
2588
2701
|
* first number past the type's highest anchored number that no file cites; it
|
|
2589
2702
|
* reports each number it skips, skips at most E4_MAX_SKIPS of them and refuses
|
|
2590
|
-
* when the next one is cited too. The scan reads neither the
|
|
2703
|
+
* when the next one is cited too. The scan reads neither the curation tree nor
|
|
2591
2704
|
* .git or a vendored or build directory (node_modules, target, dist), nor a
|
|
2592
2705
|
* symbolic link, a binary file or a file over 5 MB. Reason: a document can cite a
|
|
2593
2706
|
* number the ledger has not minted yet, and minting over it silently binds that
|
|
@@ -2598,7 +2711,7 @@ function mintAnchor(ledgerRows, type, citedAnchors) {
|
|
|
2598
2711
|
* whatever its status (D-LEDGER-REGISTRY). The new row is its projection
|
|
2599
2712
|
* (toLedgerRowV2) with the type's active status and today as both date and
|
|
2600
2713
|
* last_verified; it is appended to the ledger and the files are re-rendered, all
|
|
2601
|
-
* under the
|
|
2714
|
+
* under the curation lock. The log is never written. Like every writer it backs
|
|
2602
2715
|
* up a v1 tree first (D-V1-BACKUP-ONCE) and quarantines the ledger's malformed
|
|
2603
2716
|
* lines before it rewrites the ledger (D-QUARANTINE-MALFORMED); a refusal writes
|
|
2604
2717
|
* nothing.
|
|
@@ -2612,13 +2725,13 @@ function mintAnchor(ledgerRows, type, citedAnchors) {
|
|
|
2612
2725
|
* | { ok: false, error: { kind: string, message: string } }}
|
|
2613
2726
|
* Error kinds: not-in-log, duplicate-log-id, already-promoted, v1-observation,
|
|
2614
2727
|
* type-mismatch, cited-numbers-exhausted, and withDecisionsLock's
|
|
2615
|
-
* not-a-directory, no-
|
|
2728
|
+
* not-a-directory, no-curation-dir and busy.
|
|
2616
2729
|
* @throws {TypeError} for a type other than decision or pitfall, or a malformed obsId
|
|
2617
2730
|
*/
|
|
2618
2731
|
function assignAnchor(root, type, obsId, { now = Date.now(), timeoutMs, citedAnchors } = {}) {
|
|
2619
2732
|
if (anchorPrefixFor(type) === null) throw new TypeError(`assignAnchor: type must be 'decision' or 'pitfall', got '${type}'`);
|
|
2620
2733
|
if (typeof obsId !== 'string' || !OBS_ID_RE.test(obsId)) throw new TypeError('assignAnchor: obsId must be an observation id');
|
|
2621
|
-
if (!
|
|
2734
|
+
if (!hasCurationDir(root)) return noCurationDir('assign-anchor', root);
|
|
2622
2735
|
const cited = citedAnchors || collectCitedAnchorIds(root);
|
|
2623
2736
|
return withDecisionsLock('assign-anchor', root, () => assignUnderLock(root, type, obsId, { now, cited }), { timeoutMs });
|
|
2624
2737
|
}
|
|
@@ -2760,7 +2873,7 @@ function recordRefreshHistory(root, plans, ledgerRows, { now }) {
|
|
|
2760
2873
|
* an inactive entry or a v1 one, and — when re-projecting — an entry with no
|
|
2761
2874
|
* observation id or whose log row is missing, doubled, v1 or of a type it cannot
|
|
2762
2875
|
* take refuses the whole batch, every refused anchor listed, nothing written. A
|
|
2763
|
-
* batch that changes no row writes nothing. Otherwise, under the
|
|
2876
|
+
* batch that changes no row writes nothing. Otherwise, under the curation lock,
|
|
2764
2877
|
* it backs up a v1 tree (D-V1-BACKUP-ONCE), quarantines the ledger's malformed
|
|
2765
2878
|
* lines (D-QUARANTINE-MALFORMED), then writes the ledger once and renders once.
|
|
2766
2879
|
*
|
|
@@ -2771,7 +2884,7 @@ function recordRefreshHistory(root, plans, ledgerRows, { now }) {
|
|
|
2771
2884
|
* @returns {{ ok: true, value: { refreshed: Array<{ anchor_id: string, state: 'verified'|'reprojected'|'unchanged' }> } }
|
|
2772
2885
|
* | { ok: false, error: { kind: string, message: string, problems?: Array<{ anchor_id: string, message: string }> } }}
|
|
2773
2886
|
* refreshed: each anchor once, in the order given. Error kinds: refused (with
|
|
2774
|
-
* problems), and withDecisionsLock's not-a-directory, no-
|
|
2887
|
+
* problems), and withDecisionsLock's not-a-directory, no-curation-dir and busy.
|
|
2775
2888
|
* @throws {TypeError} when anchorIds is empty or holds anything but anchor ids
|
|
2776
2889
|
*/
|
|
2777
2890
|
function refreshAnchors(root, anchorIds, { verified = false, now = Date.now(), timeoutMs } = {}) {
|
|
@@ -2991,7 +3104,7 @@ function quoteAtRef(root, at, quote, { verifyRef } = {}) {
|
|
|
2991
3104
|
* Each also sets retired_on to today and clears the other notes; the rest of the
|
|
2992
3105
|
* row stays as it is, so a v1 entry stays v1. An entry already inactive, an
|
|
2993
3106
|
* anchor the ledger does not hold or holds twice, and a successor absent or
|
|
2994
|
-
* inactive are refused. The input and the quote are checked before the
|
|
3107
|
+
* inactive are refused. The input and the quote are checked before the curation
|
|
2995
3108
|
* lock is taken; under it the ledger is written once and the files are rendered,
|
|
2996
3109
|
* after a v1 tree is backed up (D-V1-BACKUP-ONCE) and malformed ledger lines are
|
|
2997
3110
|
* quarantined (D-QUARANTINE-MALFORMED). A refusal writes nothing.
|
|
@@ -3008,7 +3121,7 @@ function quoteAtRef(root, at, quote, { verifyRef } = {}) {
|
|
|
3008
3121
|
* kinds: invalid-input (with problems), quoteAtRef's, not-found,
|
|
3009
3122
|
* duplicate-anchor, already-inactive, successor-not-found,
|
|
3010
3123
|
* successor-duplicate-anchor, successor-inactive, and withDecisionsLock's
|
|
3011
|
-
* not-a-directory, no-
|
|
3124
|
+
* not-a-directory, no-curation-dir and busy.
|
|
3012
3125
|
* @throws {TypeError} for a malformed anchorId or a status that is not inactive
|
|
3013
3126
|
*/
|
|
3014
3127
|
function retireAnchor(root, anchorId, status, input, { now = Date.now(), timeoutMs, verifyRef } = {}) {
|
|
@@ -3016,7 +3129,7 @@ function retireAnchor(root, anchorId, status, input, { now = Date.now(), timeout
|
|
|
3016
3129
|
if (!INACTIVE_STATUSES.includes(status)) {
|
|
3017
3130
|
throw new TypeError(`retireAnchor: status must be one of ${INACTIVE_STATUSES.join(', ')}, got '${status}'`);
|
|
3018
3131
|
}
|
|
3019
|
-
if (!
|
|
3132
|
+
if (!hasCurationDir(root)) return noCurationDir('retire-anchor', root);
|
|
3020
3133
|
const problems = retireInputProblems(anchorId, status, input);
|
|
3021
3134
|
if (problems.length > 0) return invalidInput('retire-anchor', problems);
|
|
3022
3135
|
let note;
|
|
@@ -3078,7 +3191,7 @@ function retireUnderLock(root, anchorId, status, note, { now }) {
|
|
|
3078
3191
|
* claim-due hands it out ahead of every verified entry (D-DUE-ORDER); its content
|
|
3079
3192
|
* and its date stay, and the files are re-rendered. An entry already active, an
|
|
3080
3193
|
* anchor the ledger does not hold or holds twice, and a row whose type its anchor
|
|
3081
|
-
* does not name are refused. It writes as retireAnchor does, under the
|
|
3194
|
+
* does not name are refused. It writes as retireAnchor does, under the curation
|
|
3082
3195
|
* lock; a refusal writes nothing.
|
|
3083
3196
|
*
|
|
3084
3197
|
* @param {string} root - project root
|
|
@@ -3087,7 +3200,7 @@ function retireUnderLock(root, anchorId, status, note, { now }) {
|
|
|
3087
3200
|
* @returns {{ ok: true, value: { anchor_id: string, status: 'Accepted'|'Active' } }
|
|
3088
3201
|
* | { ok: false, error: { kind: string, message: string } }}
|
|
3089
3202
|
* Error kinds: not-found, duplicate-anchor, already-active, type-mismatch, and
|
|
3090
|
-
* withDecisionsLock's not-a-directory, no-
|
|
3203
|
+
* withDecisionsLock's not-a-directory, no-curation-dir and busy.
|
|
3091
3204
|
* @throws {TypeError} for a malformed anchorId
|
|
3092
3205
|
*/
|
|
3093
3206
|
function restoreAnchor(root, anchorId, { now = Date.now(), timeoutMs } = {}) {
|
|
@@ -3148,15 +3261,16 @@ module.exports = {
|
|
|
3148
3261
|
readJsonl,
|
|
3149
3262
|
rejectedPathFor,
|
|
3150
3263
|
quarantineRejected,
|
|
3264
|
+
appendNoFollow,
|
|
3151
3265
|
readJsonlForWrite,
|
|
3152
3266
|
writeExclusive,
|
|
3153
3267
|
writeFileAtomic,
|
|
3154
3268
|
writeJsonlAtomic,
|
|
3155
3269
|
// Locking
|
|
3156
|
-
|
|
3270
|
+
hasCurationDir,
|
|
3157
3271
|
withDecisionsLock,
|
|
3158
3272
|
// State and the ledger registry
|
|
3159
|
-
|
|
3273
|
+
readCurationState,
|
|
3160
3274
|
ledgerRegistry,
|
|
3161
3275
|
// Validation
|
|
3162
3276
|
validateObservationInput,
|
|
@@ -3171,7 +3285,7 @@ module.exports = {
|
|
|
3171
3285
|
// Rotation, clearing and resetting
|
|
3172
3286
|
rotateObservations,
|
|
3173
3287
|
clearUnreferenced,
|
|
3174
|
-
|
|
3288
|
+
resetCuration,
|
|
3175
3289
|
// The queue claim
|
|
3176
3290
|
CLAIM_STALE_SECS,
|
|
3177
3291
|
CLAIM_TOKEN_RE,
|
|
@@ -3180,6 +3294,16 @@ module.exports = {
|
|
|
3180
3294
|
claimQueue,
|
|
3181
3295
|
releaseClaim,
|
|
3182
3296
|
touchClaim,
|
|
3297
|
+
// Primitives curation-move.cjs builds on
|
|
3298
|
+
QUEUE_LOCK_STALE_MS,
|
|
3299
|
+
NO_HARD_LINK_CODES,
|
|
3300
|
+
isPlainObject,
|
|
3301
|
+
readTextUnlinked,
|
|
3302
|
+
removeIfPresent,
|
|
3303
|
+
renderAll,
|
|
3304
|
+
lstatOrNull,
|
|
3305
|
+
physicalPath,
|
|
3306
|
+
homeDirOrNull,
|
|
3183
3307
|
// Integrity, listing, due selection and show
|
|
3184
3308
|
integrityFlags,
|
|
3185
3309
|
buildListing,
|