driftseal 2.1.0 → 3.1.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/README.md +42 -18
- package/README.zh-CN.md +34 -14
- package/benchmark/recent-log.js +181 -0
- package/bin/driftseal.js +561 -532
- package/lib/outcome-fold.js +377 -0
- package/lib/outcome-index-sqlite.js +802 -0
- package/lib/sqlite-runtime.js +71 -0
- package/package.json +7 -2
- package/skills/use-driftseal/SKILL.md +6 -5
- package/test/package-smoke.js +150 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const MINIMUM_NODE = Object.freeze({ major: 22, minor: 13, display: '22.13.0' });
|
|
4
|
+
|
|
5
|
+
class SqliteUnavailableError extends Error {
|
|
6
|
+
constructor(message, cause) {
|
|
7
|
+
super(message, cause ? { cause } : undefined);
|
|
8
|
+
this.name = 'SqliteUnavailableError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function assertSupportedNode(version = process.versions.node) {
|
|
13
|
+
const [major, minor] = String(version)
|
|
14
|
+
.split('.')
|
|
15
|
+
.map((part) => Number.parseInt(part, 10));
|
|
16
|
+
if (major > MINIMUM_NODE.major || (major === MINIMUM_NODE.major && minor >= MINIMUM_NODE.minor)) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
throw new Error(`DriftSeal requires Node.js ${MINIMUM_NODE.display} or newer; found ${version}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function warningType(args) {
|
|
23
|
+
const first = args[0];
|
|
24
|
+
if (typeof first === 'string') return first;
|
|
25
|
+
if (first && typeof first === 'object') return first.type || first.name || null;
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function warningMessage(warning) {
|
|
30
|
+
if (warning instanceof Error) return warning.message;
|
|
31
|
+
return String(warning);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function loadNodeSqlite() {
|
|
35
|
+
assertSupportedNode();
|
|
36
|
+
if (process.env._DRIFTSEAL_TEST_DISABLE_SQLITE === '1') {
|
|
37
|
+
throw new SqliteUnavailableError('node:sqlite is unavailable in this runtime');
|
|
38
|
+
}
|
|
39
|
+
const originalEmitWarning = process.emitWarning;
|
|
40
|
+
process.emitWarning = function filteredEmitWarning(warning, ...args) {
|
|
41
|
+
if (
|
|
42
|
+
warningType(args) === 'ExperimentalWarning' &&
|
|
43
|
+
/\bSQLite\b/i.test(warningMessage(warning))
|
|
44
|
+
) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
return Reflect.apply(originalEmitWarning, this, [warning, ...args]);
|
|
48
|
+
};
|
|
49
|
+
try {
|
|
50
|
+
return require('node:sqlite');
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error instanceof SqliteUnavailableError) throw error;
|
|
53
|
+
throw new SqliteUnavailableError('node:sqlite is unavailable in this runtime', error);
|
|
54
|
+
} finally {
|
|
55
|
+
process.emitWarning = originalEmitWarning;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let databaseSync;
|
|
60
|
+
|
|
61
|
+
function getDatabaseSync() {
|
|
62
|
+
if (!databaseSync) ({ DatabaseSync: databaseSync } = loadNodeSqlite());
|
|
63
|
+
return databaseSync;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = {
|
|
67
|
+
MINIMUM_NODE,
|
|
68
|
+
SqliteUnavailableError,
|
|
69
|
+
assertSupportedNode,
|
|
70
|
+
getDatabaseSync,
|
|
71
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "driftseal",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "Seal outcomes, verification, and decisions into an auditable workflow for agentic coding",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"driftseal",
|
|
@@ -34,16 +34,21 @@
|
|
|
34
34
|
"files": [
|
|
35
35
|
"index.js",
|
|
36
36
|
"bin",
|
|
37
|
+
"lib",
|
|
38
|
+
"benchmark",
|
|
37
39
|
"skills",
|
|
40
|
+
"test/package-smoke.js",
|
|
38
41
|
"README.md",
|
|
39
42
|
"README.zh-CN.md",
|
|
40
43
|
"LICENSE"
|
|
41
44
|
],
|
|
42
45
|
"scripts": {
|
|
46
|
+
"benchmark:recent-log": "node benchmark/recent-log.js",
|
|
47
|
+
"test:package": "node test/package-smoke.js",
|
|
43
48
|
"test": "node --test test/*.test.js"
|
|
44
49
|
},
|
|
45
50
|
"engines": {
|
|
46
|
-
"node": ">=
|
|
51
|
+
"node": ">=22.13.0"
|
|
47
52
|
},
|
|
48
53
|
"dependencies": {
|
|
49
54
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
@@ -47,13 +47,14 @@ If `status` or `log` fails with a duplicate id or with `multiple outcomes in
|
|
|
47
47
|
progress`, or the outcome log has conflict markers, run `driftseal absorb`
|
|
48
48
|
instead of editing `.seal/outcomes/events.jsonl`. When both sides still have an
|
|
49
49
|
open outcome, add `--abandon-theirs` or `--abandon-ours`; this works whether your
|
|
50
|
-
open outcome sits in the log or is parked
|
|
50
|
+
open outcome sits in the log or is parked beside the WAL. `driftseal init` also
|
|
51
51
|
configures the local git merge driver; clones need `init` again for that driver.
|
|
52
52
|
|
|
53
|
-
In a Git
|
|
54
|
-
can run with an outcome still in
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
In a default Git-repository seal, `begin` parks beside the WAL and does not dirty
|
|
54
|
+
the tracked outcome log, so `git merge` can run with an outcome still in
|
|
55
|
+
progress. Custom `$DRIFTSEAL_HOME` seals write that open outcome directly to
|
|
56
|
+
`events.jsonl`. `end` writes the closed record to `.seal/outcomes/events.jsonl`;
|
|
57
|
+
if it is interrupted, the outcome stays open there and `end` can be run again.
|
|
57
58
|
|
|
58
59
|
For a v1 repository, do not reinterpret or delete the old logs directly. Use
|
|
59
60
|
`driftseal migrate v1-to-v2 inspect`, prepare a reviewed grouping plan, apply
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const { execFileSync, spawnSync } = require('node:child_process');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
|
|
9
|
+
const root = path.resolve(__dirname, '..');
|
|
10
|
+
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'driftseal-package-smoke-'));
|
|
11
|
+
const packDirectory = path.join(temporary, 'pack');
|
|
12
|
+
const consumer = path.join(temporary, 'consumer');
|
|
13
|
+
|
|
14
|
+
function npmCli() {
|
|
15
|
+
const npmJs = process.env.npm_execpath;
|
|
16
|
+
assert.equal(typeof npmJs, 'string', 'package smoke expects to run under npm (npm_execpath)');
|
|
17
|
+
assert.notEqual(npmJs, '', 'package smoke expects to run under npm (npm_execpath)');
|
|
18
|
+
assert.match(path.basename(npmJs), /\.js$/i, 'npm_execpath must be the JavaScript CLI');
|
|
19
|
+
assert.equal(fs.existsSync(npmJs), true, `npm_execpath exists: ${npmJs}`);
|
|
20
|
+
return npmJs;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function run(command, args, options = {}) {
|
|
24
|
+
return execFileSync(command, args, {
|
|
25
|
+
cwd: root,
|
|
26
|
+
encoding: 'utf8',
|
|
27
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
28
|
+
...options,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function runNpm(args, options = {}) {
|
|
33
|
+
return run(process.execPath, [npmCli(), ...args], options);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function leftoverRebuildFiles(outcomeDir) {
|
|
37
|
+
return fs
|
|
38
|
+
.readdirSync(outcomeDir)
|
|
39
|
+
.filter(
|
|
40
|
+
(name) =>
|
|
41
|
+
name.startsWith('..outcome-index.sqlite.') && name.endsWith('.tmp')
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function assertValidRebuiltIndex(indexFile, outcomeDir, runtime) {
|
|
46
|
+
assert.equal(fs.existsSync(indexFile), true, 'rebuilt SQLite index exists');
|
|
47
|
+
const raw = fs.readFileSync(indexFile);
|
|
48
|
+
assert.notEqual(raw.toString('utf8'), 'corrupt package smoke index');
|
|
49
|
+
assert.equal(raw.subarray(0, 16).toString(), 'SQLite format 3\0');
|
|
50
|
+
|
|
51
|
+
const leftovers = leftoverRebuildFiles(outcomeDir);
|
|
52
|
+
assert.deepEqual(leftovers, [], `temporary rebuild files remain: ${leftovers.join(', ')}`);
|
|
53
|
+
|
|
54
|
+
const DatabaseSync = runtime.getDatabaseSync();
|
|
55
|
+
const db = new DatabaseSync(indexFile, { readOnly: true });
|
|
56
|
+
try {
|
|
57
|
+
assert.equal(
|
|
58
|
+
Number(db.prepare('PRAGMA user_version').get().user_version),
|
|
59
|
+
runtime.INDEX_SCHEMA_VERSION
|
|
60
|
+
);
|
|
61
|
+
assert.equal(db.prepare('PRAGMA quick_check').get().quick_check, 'ok');
|
|
62
|
+
const outcomes = db
|
|
63
|
+
.prepare('SELECT id, ordinal, lane, status FROM outcomes ORDER BY ordinal')
|
|
64
|
+
.all();
|
|
65
|
+
assert.equal(outcomes.length, 1);
|
|
66
|
+
assert.equal(typeof outcomes[0].id, 'string');
|
|
67
|
+
assert.notEqual(outcomes[0].id, '');
|
|
68
|
+
assert.equal(Number(outcomes[0].ordinal), 0);
|
|
69
|
+
assert.equal(outcomes[0].lane, 'main');
|
|
70
|
+
assert.equal(outcomes[0].status, 'abandoned');
|
|
71
|
+
} finally {
|
|
72
|
+
db.close();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
fs.mkdirSync(packDirectory);
|
|
78
|
+
fs.mkdirSync(consumer);
|
|
79
|
+
fs.writeFileSync(
|
|
80
|
+
path.join(consumer, 'package.json'),
|
|
81
|
+
`${JSON.stringify({ name: 'driftseal-package-smoke', private: true }, null, 2)}\n`
|
|
82
|
+
);
|
|
83
|
+
const packed = JSON.parse(
|
|
84
|
+
runNpm(['pack', '--json', '--pack-destination', packDirectory])
|
|
85
|
+
);
|
|
86
|
+
assert.equal(packed.length, 1);
|
|
87
|
+
const tarball = path.join(packDirectory, packed[0].filename);
|
|
88
|
+
runNpm(['install', '--ignore-scripts', '--no-audit', '--no-fund', tarball], {
|
|
89
|
+
cwd: consumer,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const installed = path.join(consumer, 'node_modules', 'driftseal');
|
|
93
|
+
for (const file of [
|
|
94
|
+
'bin/driftseal.js',
|
|
95
|
+
'bin/driftseal-mcp.js',
|
|
96
|
+
'lib/outcome-fold.js',
|
|
97
|
+
'lib/outcome-index-sqlite.js',
|
|
98
|
+
'lib/sqlite-runtime.js',
|
|
99
|
+
'benchmark/recent-log.js',
|
|
100
|
+
'test/package-smoke.js',
|
|
101
|
+
]) {
|
|
102
|
+
assert.equal(fs.existsSync(path.join(installed, file)), true, `${file} is packaged`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const sandbox = path.join(temporary, 'sandbox');
|
|
106
|
+
const home = path.join(sandbox, '.seal');
|
|
107
|
+
const outcomeDir = path.join(home, 'outcomes');
|
|
108
|
+
const indexFile = path.join(outcomeDir, '.outcome-index.sqlite');
|
|
109
|
+
fs.mkdirSync(sandbox);
|
|
110
|
+
const env = {
|
|
111
|
+
...process.env,
|
|
112
|
+
DRIFTSEAL_HOME: home,
|
|
113
|
+
DRIFTSEAL_DECISION_HOME: path.join(home, 'madr'),
|
|
114
|
+
};
|
|
115
|
+
const cli = path.join(installed, 'bin', 'driftseal.js');
|
|
116
|
+
run(process.execPath, [cli, 'begin', 'packaged sqlite smoke'], {
|
|
117
|
+
cwd: sandbox,
|
|
118
|
+
env,
|
|
119
|
+
});
|
|
120
|
+
run(
|
|
121
|
+
process.execPath,
|
|
122
|
+
[cli, 'end', '--status', 'abandoned', '--note', 'packaged smoke'],
|
|
123
|
+
{ cwd: sandbox, env }
|
|
124
|
+
);
|
|
125
|
+
const logged = spawnSync(process.execPath, [cli, 'log', '--last', '1'], {
|
|
126
|
+
cwd: sandbox,
|
|
127
|
+
env,
|
|
128
|
+
encoding: 'utf8',
|
|
129
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
130
|
+
});
|
|
131
|
+
assert.equal(logged.status, 0, logged.stderr);
|
|
132
|
+
assert.match(logged.stdout, /packaged sqlite smoke/);
|
|
133
|
+
assert.doesNotMatch(logged.stderr, /SQLite is an experimental feature/);
|
|
134
|
+
assert.equal(fs.existsSync(indexFile), true);
|
|
135
|
+
fs.writeFileSync(indexFile, 'corrupt package smoke index');
|
|
136
|
+
const rebuilt = run(process.execPath, [cli, 'log', '--last', '1'], {
|
|
137
|
+
cwd: sandbox,
|
|
138
|
+
env,
|
|
139
|
+
});
|
|
140
|
+
assert.match(rebuilt, /packaged sqlite smoke/);
|
|
141
|
+
assertValidRebuiltIndex(indexFile, outcomeDir, {
|
|
142
|
+
INDEX_SCHEMA_VERSION: require(path.join(installed, 'lib', 'outcome-index-sqlite.js'))
|
|
143
|
+
.INDEX_SCHEMA_VERSION,
|
|
144
|
+
getDatabaseSync: require(path.join(installed, 'lib', 'sqlite-runtime.js'))
|
|
145
|
+
.getDatabaseSync,
|
|
146
|
+
});
|
|
147
|
+
process.stdout.write(`package smoke passed: ${packed[0].filename}\n`);
|
|
148
|
+
} finally {
|
|
149
|
+
fs.rmSync(temporary, { recursive: true, force: true });
|
|
150
|
+
}
|