@tenonhq/dovetail-core 0.0.92 → 0.0.94
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/dist/appUtils.js +32 -5
- package/dist/commander.js +30 -0
- package/dist/knowledgeDiff.js +64 -0
- package/dist/knowledgeDiffCommand.js +121 -0
- package/dist/tests/knowledgeDiff.test.js +83 -0
- package/dist/tests/normalizeManifestKeys.test.js +94 -0
- package/package.json +3 -2
- package/release-manifest.json +84 -0
package/dist/appUtils.js
CHANGED
|
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.createAndAssignUpdateSet = exports.swapScope = exports.buildFiles = exports.summarizeRecord = exports.pushFiles = exports.getAppFileList = exports.groupAppFiles = exports.processMissingFiles = exports.refreshAllFiles = exports.findMissingFiles = exports.syncManifest = exports.processManifest = exports.normalizeManifestKeys = void 0;
|
|
39
|
+
exports.createAndAssignUpdateSet = exports.swapScope = exports.buildFiles = exports.summarizeRecord = exports.pushFiles = exports.getAppFileList = exports.groupAppFiles = exports.processMissingFiles = exports.refreshAllFiles = exports.findMissingFiles = exports.syncManifest = exports.processManifest = exports.normalizeManifestKeys = exports.toSafeFolderName = void 0;
|
|
40
40
|
const path_1 = __importDefault(require("path"));
|
|
41
41
|
const fs_1 = __importDefault(require("fs"));
|
|
42
42
|
const progress_1 = __importDefault(require("progress"));
|
|
@@ -171,9 +171,29 @@ const processTablesInManifest = async (tables, forceWrite, sourcePath, onRecordP
|
|
|
171
171
|
});
|
|
172
172
|
};
|
|
173
173
|
/**
|
|
174
|
-
*
|
|
174
|
+
* Returns a filesystem-safe folder name for a record.
|
|
175
|
+
*
|
|
176
|
+
* Dovetail names each record's on-disk folder after the record's display name.
|
|
177
|
+
* Windows/NTFS forbids these characters in a path component — `< > : " | ? * \ /`
|
|
178
|
+
* — and forbids a trailing space or trailing dot. A folder whose name violates
|
|
179
|
+
* this cannot be checked out on Windows (`git clone` aborts with "invalid path"),
|
|
180
|
+
* even though macOS/Linux accept it. When the display name is unsafe we fall back
|
|
181
|
+
* to the record's sys_id, which is hex and safe on every OS. The true display name
|
|
182
|
+
* is unaffected here and is still carried by the server-provided metaData.json.
|
|
183
|
+
*/
|
|
184
|
+
const toSafeFolderName = (record) => {
|
|
185
|
+
var name = record.name || "";
|
|
186
|
+
var isUnsafe = /[<>:"|?*\\/]/.test(name) || /[ .]$/.test(name) || name === "";
|
|
187
|
+
return isUnsafe ? record.sys_id : name;
|
|
188
|
+
};
|
|
189
|
+
exports.toSafeFolderName = toSafeFolderName;
|
|
190
|
+
/**
|
|
191
|
+
* Re-keys manifest records from sys_id to a filesystem-safe folder name.
|
|
175
192
|
* Some ServiceNow tables return records keyed by sys_id instead of display name.
|
|
176
|
-
* This ensures consistent naming for directories and manifest lookups
|
|
193
|
+
* This ensures consistent naming for directories and manifest lookups, and keeps
|
|
194
|
+
* the on-disk folder name (which every writer derives from `record.name`) equal to
|
|
195
|
+
* the manifest key that `dove push` looks records up by — so the key ≡ folder
|
|
196
|
+
* invariant holds even when an unsafe display name forces a sys_id fallback.
|
|
177
197
|
*/
|
|
178
198
|
const normalizeManifestKeys = (manifest) => {
|
|
179
199
|
var tables = manifest.tables || {};
|
|
@@ -186,11 +206,15 @@ const normalizeManifestKeys = (manifest) => {
|
|
|
186
206
|
for (var j = 0; j < recordKeys.length; j++) {
|
|
187
207
|
var key = recordKeys[j];
|
|
188
208
|
var record = records[key];
|
|
189
|
-
var displayKey = record
|
|
190
|
-
// Handle duplicate
|
|
209
|
+
var displayKey = (0, exports.toSafeFolderName)(record);
|
|
210
|
+
// Handle duplicate folder names by appending sys_id suffix
|
|
191
211
|
if (normalized[displayKey]) {
|
|
192
212
|
displayKey = displayKey + " (" + record.sys_id.substring(0, 8) + ")";
|
|
193
213
|
}
|
|
214
|
+
// Keep record.name === manifest key so all writers (which build the folder
|
|
215
|
+
// path from record.name) and push (which looks up by folder name) agree.
|
|
216
|
+
// The real display name lives in the server-provided metaData.json snapshot.
|
|
217
|
+
record.name = displayKey;
|
|
194
218
|
normalized[displayKey] = record;
|
|
195
219
|
}
|
|
196
220
|
tables[tableName].records = normalized;
|
|
@@ -199,6 +223,9 @@ const normalizeManifestKeys = (manifest) => {
|
|
|
199
223
|
};
|
|
200
224
|
exports.normalizeManifestKeys = normalizeManifestKeys;
|
|
201
225
|
const processManifest = async (manifest, forceWrite = false, sourcePath) => {
|
|
226
|
+
// Idempotent: guarantees filesystem-safe folder names + key ≡ record.name on
|
|
227
|
+
// every write path, including downloadCommand which does not normalize upstream.
|
|
228
|
+
(0, exports.normalizeManifestKeys)(manifest);
|
|
202
229
|
const tableCount = Object.keys(manifest.tables).length;
|
|
203
230
|
FileLogger_1.fileLogger.debug("Processing manifest: " + (manifest.scope || "legacy") + " (" + tableCount + " tables)");
|
|
204
231
|
var recordCount = countRecordsInTables(manifest.tables);
|
package/dist/commander.js
CHANGED
|
@@ -15,6 +15,7 @@ const deleteRecordCommand_1 = require("./deleteRecordCommand");
|
|
|
15
15
|
const migrateCommand_1 = require("./migrateCommand");
|
|
16
16
|
const clickupCommands_1 = require("./clickupCommands");
|
|
17
17
|
const loginCommand_1 = require("./loginCommand");
|
|
18
|
+
const knowledgeDiffCommand_1 = require("./knowledgeDiffCommand");
|
|
18
19
|
const yargs_1 = __importDefault(require("yargs"));
|
|
19
20
|
async function initCommands() {
|
|
20
21
|
const sharedOptions = {
|
|
@@ -461,6 +462,35 @@ async function initCommands() {
|
|
|
461
462
|
.demandCommand(1, "Please specify a task subcommand (e.g., clear)");
|
|
462
463
|
}, function () {
|
|
463
464
|
/* subcommands handle execution */
|
|
465
|
+
})
|
|
466
|
+
.command(["knowledge-diff"], "Report Dovetail releases this repo has not documented yet (stages pending events for /dovetail-features-sync)", function (cmdArgs) {
|
|
467
|
+
cmdArgs.options({
|
|
468
|
+
...sharedOptions,
|
|
469
|
+
manifest: {
|
|
470
|
+
type: "string",
|
|
471
|
+
describe: "Path to the Dovetail release manifest (defaults to the bundled one)",
|
|
472
|
+
},
|
|
473
|
+
config: {
|
|
474
|
+
type: "string",
|
|
475
|
+
describe: "Path to the consumer knowledge-sync config.json",
|
|
476
|
+
},
|
|
477
|
+
ledger: {
|
|
478
|
+
type: "string",
|
|
479
|
+
describe: "Path to the documented-state ledger.json",
|
|
480
|
+
},
|
|
481
|
+
out: {
|
|
482
|
+
type: "string",
|
|
483
|
+
describe: "Directory to write pending release-event files",
|
|
484
|
+
},
|
|
485
|
+
json: {
|
|
486
|
+
type: "boolean",
|
|
487
|
+
default: false,
|
|
488
|
+
describe: "Print the diff as JSON instead of writing pending files",
|
|
489
|
+
},
|
|
490
|
+
});
|
|
491
|
+
return cmdArgs;
|
|
492
|
+
}, async function (args) {
|
|
493
|
+
await (0, knowledgeDiffCommand_1.knowledgeDiffCommand)(args);
|
|
464
494
|
})
|
|
465
495
|
.help().argv;
|
|
466
496
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Pure, deterministic core for `dove knowledge-diff`. No fs, no network — unit-testable.
|
|
3
|
+
// Given a Dovetail release manifest (the publish feed) and a consumer repo's
|
|
4
|
+
// documented-state ledger + watch config, returns the release-events the
|
|
5
|
+
// consumer has not documented yet. knowledgeDiffCommand wraps this with I/O to
|
|
6
|
+
// stage pending events for the in-session /dovetail-features-sync skill.
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.semverGt = semverGt;
|
|
9
|
+
exports.eventFilename = eventFilename;
|
|
10
|
+
exports.computeDrift = computeDrift;
|
|
11
|
+
// Numeric semver compare for the x.y.z range Dovetail uses (no prerelease tags).
|
|
12
|
+
function semverGt(a, b) {
|
|
13
|
+
const pa = String(a)
|
|
14
|
+
.split(".")
|
|
15
|
+
.map(function (n) {
|
|
16
|
+
return parseInt(n, 10) || 0;
|
|
17
|
+
});
|
|
18
|
+
const pb = String(b)
|
|
19
|
+
.split(".")
|
|
20
|
+
.map(function (n) {
|
|
21
|
+
return parseInt(n, 10) || 0;
|
|
22
|
+
});
|
|
23
|
+
for (let i = 0; i < 3; i++) {
|
|
24
|
+
const da = pa[i] || 0;
|
|
25
|
+
const db = pb[i] || 0;
|
|
26
|
+
if (da > db)
|
|
27
|
+
return true;
|
|
28
|
+
if (da < db)
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
// Filesystem-safe pending filename: unscoped package name + version.
|
|
34
|
+
function eventFilename(ev) {
|
|
35
|
+
const unscoped = String(ev.package || "").replace(/^@[^/]+\//, "");
|
|
36
|
+
return unscoped + "@" + ev.version + ".json";
|
|
37
|
+
}
|
|
38
|
+
// Events newer than the ledger's documented_version, limited to watch_packages
|
|
39
|
+
// (when set) and excluding ignore_packages. Deterministically sorted so callers
|
|
40
|
+
// and tests get a stable order.
|
|
41
|
+
function computeDrift(manifest, ledger, config) {
|
|
42
|
+
const events = (manifest && manifest.events) || [];
|
|
43
|
+
const watch = (config && config.watch_packages) || [];
|
|
44
|
+
const ignore = (config && config.ignore_packages) || [];
|
|
45
|
+
const ledgerPackages = (ledger && ledger.packages) || {};
|
|
46
|
+
const drift = [];
|
|
47
|
+
for (const ev of events) {
|
|
48
|
+
if (!ev || !ev.package || !ev.version)
|
|
49
|
+
continue;
|
|
50
|
+
if (ignore.indexOf(ev.package) !== -1)
|
|
51
|
+
continue;
|
|
52
|
+
if (watch.length > 0 && watch.indexOf(ev.package) === -1)
|
|
53
|
+
continue;
|
|
54
|
+
const entry = ledgerPackages[ev.package];
|
|
55
|
+
const documented = entry && entry.documented_version;
|
|
56
|
+
if (documented && !semverGt(ev.version, documented))
|
|
57
|
+
continue;
|
|
58
|
+
drift.push(ev);
|
|
59
|
+
}
|
|
60
|
+
drift.sort(function (a, b) {
|
|
61
|
+
return (a.package + a.version).localeCompare(b.package + b.version);
|
|
62
|
+
});
|
|
63
|
+
return drift;
|
|
64
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.knowledgeDiffCommand = knowledgeDiffCommand;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const commands_1 = require("./commands");
|
|
40
|
+
const Logger_1 = require("./Logger");
|
|
41
|
+
const knowledgeDiff_1 = require("./knowledgeDiff");
|
|
42
|
+
function readJson(filePath) {
|
|
43
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
44
|
+
}
|
|
45
|
+
function resolveManifestPath(explicit) {
|
|
46
|
+
if (explicit)
|
|
47
|
+
return path.resolve(explicit);
|
|
48
|
+
// Bundled inside @tenonhq/dovetail-core at publish time (see publish-on-merge.js).
|
|
49
|
+
// __dirname is <pkg>/dist at runtime, so ".." is the installed package root.
|
|
50
|
+
return path.resolve(__dirname, "..", "release-manifest.json");
|
|
51
|
+
}
|
|
52
|
+
// `dove knowledge-diff` — pure, deterministic detector. Compares the Dovetail
|
|
53
|
+
// release manifest against this repo's documented-state ledger and stages the
|
|
54
|
+
// undocumented release-events for the in-session /dovetail-features-sync skill.
|
|
55
|
+
// No network, no LLM — safe to run at install or in CI.
|
|
56
|
+
async function knowledgeDiffCommand(args) {
|
|
57
|
+
(0, commands_1.setLogLevel)(args);
|
|
58
|
+
const cwd = process.cwd();
|
|
59
|
+
const configPath = args.config
|
|
60
|
+
? path.resolve(args.config)
|
|
61
|
+
: path.join(cwd, "context", "dovetail-releases", "config.json");
|
|
62
|
+
let config = {};
|
|
63
|
+
if (fs.existsSync(configPath)) {
|
|
64
|
+
config = readJson(configPath);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
Logger_1.logger.warn("knowledge-diff: no config at " + configPath + "; watching all packages.");
|
|
68
|
+
}
|
|
69
|
+
const manifestPath = resolveManifestPath(args.manifest);
|
|
70
|
+
if (!fs.existsSync(manifestPath)) {
|
|
71
|
+
Logger_1.logger.warn("knowledge-diff: no release manifest at " +
|
|
72
|
+
manifestPath +
|
|
73
|
+
" — nothing to diff (Dovetail publish writes it).");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const manifest = readJson(manifestPath);
|
|
77
|
+
let ledgerPath;
|
|
78
|
+
if (args.ledger) {
|
|
79
|
+
ledgerPath = path.resolve(args.ledger);
|
|
80
|
+
}
|
|
81
|
+
else if (config.ledger) {
|
|
82
|
+
ledgerPath = path.resolve(cwd, config.ledger);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
ledgerPath = path.join(cwd, "context", "dovetail-releases", "ledger.json");
|
|
86
|
+
}
|
|
87
|
+
const ledger = fs.existsSync(ledgerPath)
|
|
88
|
+
? readJson(ledgerPath)
|
|
89
|
+
: { packages: {} };
|
|
90
|
+
const drift = (0, knowledgeDiff_1.computeDrift)(manifest, ledger, config);
|
|
91
|
+
if (args.json) {
|
|
92
|
+
process.stdout.write(JSON.stringify({ count: drift.length, events: drift }, null, 2) + "\n");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (drift.length === 0) {
|
|
96
|
+
Logger_1.logger.info("knowledge-diff: no new Dovetail releases to document.");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
let outDir;
|
|
100
|
+
if (args.out) {
|
|
101
|
+
outDir = path.resolve(args.out);
|
|
102
|
+
}
|
|
103
|
+
else if (config.pending_dir) {
|
|
104
|
+
outDir = path.resolve(cwd, config.pending_dir);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
outDir = path.join(cwd, "context", "dovetail-releases", "pending");
|
|
108
|
+
}
|
|
109
|
+
if (!fs.existsSync(outDir)) {
|
|
110
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
111
|
+
}
|
|
112
|
+
for (const ev of drift) {
|
|
113
|
+
const dest = path.join(outDir, (0, knowledgeDiff_1.eventFilename)(ev));
|
|
114
|
+
fs.writeFileSync(dest, JSON.stringify(ev, null, 2) + "\n");
|
|
115
|
+
}
|
|
116
|
+
Logger_1.logger.info("knowledge-diff: wrote " +
|
|
117
|
+
drift.length +
|
|
118
|
+
" pending release-event(s) to " +
|
|
119
|
+
path.relative(cwd, outDir) +
|
|
120
|
+
". Run /dovetail-features-sync to document them.");
|
|
121
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const knowledgeDiff_1 = require("../knowledgeDiff");
|
|
4
|
+
describe("semverGt", function () {
|
|
5
|
+
it("compares x.y.z numerically (not lexically)", function () {
|
|
6
|
+
expect((0, knowledgeDiff_1.semverGt)("0.0.13", "0.0.12")).toBe(true);
|
|
7
|
+
expect((0, knowledgeDiff_1.semverGt)("0.0.12", "0.0.12")).toBe(false);
|
|
8
|
+
expect((0, knowledgeDiff_1.semverGt)("0.1.0", "0.0.99")).toBe(true);
|
|
9
|
+
expect((0, knowledgeDiff_1.semverGt)("1.0.0", "0.9.9")).toBe(true);
|
|
10
|
+
expect((0, knowledgeDiff_1.semverGt)("0.0.2", "0.0.10")).toBe(false);
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
describe("eventFilename", function () {
|
|
14
|
+
it("strips the npm scope and appends the version", function () {
|
|
15
|
+
expect((0, knowledgeDiff_1.eventFilename)({
|
|
16
|
+
event_id: "x",
|
|
17
|
+
package: "@tenonhq/dovetail-clickup",
|
|
18
|
+
version: "0.0.13",
|
|
19
|
+
prev_version: "0.0.12",
|
|
20
|
+
})).toBe("dovetail-clickup@0.0.13.json");
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
describe("computeDrift", function () {
|
|
24
|
+
const manifest = {
|
|
25
|
+
events: [
|
|
26
|
+
{
|
|
27
|
+
event_id: "dovetail-clickup@0.0.13",
|
|
28
|
+
package: "@tenonhq/dovetail-clickup",
|
|
29
|
+
version: "0.0.13",
|
|
30
|
+
prev_version: "0.0.12",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
event_id: "dovetail-core@0.0.93",
|
|
34
|
+
package: "@tenonhq/dovetail-core",
|
|
35
|
+
version: "0.0.93",
|
|
36
|
+
prev_version: "0.0.92",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
event_id: "dovetail-gmail@0.0.11",
|
|
40
|
+
package: "@tenonhq/dovetail-gmail",
|
|
41
|
+
version: "0.0.11",
|
|
42
|
+
prev_version: "0.0.10",
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
const ledger = {
|
|
47
|
+
packages: {
|
|
48
|
+
"@tenonhq/dovetail-clickup": { documented_version: "0.0.12" },
|
|
49
|
+
"@tenonhq/dovetail-core": { documented_version: "0.0.93" },
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
it("returns events newer than the ledger; undocumented packages count as drift", function () {
|
|
53
|
+
const ids = (0, knowledgeDiff_1.computeDrift)(manifest, ledger, { watch_packages: [] }).map(function (e) {
|
|
54
|
+
return e.event_id;
|
|
55
|
+
});
|
|
56
|
+
expect(ids).toContain("dovetail-clickup@0.0.13"); // 0.0.13 > 0.0.12
|
|
57
|
+
expect(ids).toContain("dovetail-gmail@0.0.11"); // not in ledger yet
|
|
58
|
+
expect(ids).not.toContain("dovetail-core@0.0.93"); // equal to documented
|
|
59
|
+
});
|
|
60
|
+
it("limits to watch_packages when the set is non-empty", function () {
|
|
61
|
+
const drift = (0, knowledgeDiff_1.computeDrift)(manifest, ledger, {
|
|
62
|
+
watch_packages: ["@tenonhq/dovetail-clickup"],
|
|
63
|
+
});
|
|
64
|
+
expect(drift.length).toBe(1);
|
|
65
|
+
expect(drift[0].package).toBe("@tenonhq/dovetail-clickup");
|
|
66
|
+
});
|
|
67
|
+
it("excludes ignore_packages", function () {
|
|
68
|
+
const ids = (0, knowledgeDiff_1.computeDrift)(manifest, ledger, {
|
|
69
|
+
watch_packages: [],
|
|
70
|
+
ignore_packages: ["@tenonhq/dovetail-gmail"],
|
|
71
|
+
}).map(function (e) {
|
|
72
|
+
return e.event_id;
|
|
73
|
+
});
|
|
74
|
+
expect(ids).not.toContain("dovetail-gmail@0.0.11");
|
|
75
|
+
expect(ids).toContain("dovetail-clickup@0.0.13");
|
|
76
|
+
});
|
|
77
|
+
it("is deterministically sorted by package+version", function () {
|
|
78
|
+
const ids = (0, knowledgeDiff_1.computeDrift)(manifest, ledger, {}).map(function (e) {
|
|
79
|
+
return e.event_id;
|
|
80
|
+
});
|
|
81
|
+
expect(ids).toEqual(["dovetail-clickup@0.0.13", "dovetail-gmail@0.0.11"]);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Tests for filesystem-safe folder naming.
|
|
3
|
+
//
|
|
4
|
+
// Dovetail names each record's folder after its ServiceNow display name. Windows
|
|
5
|
+
// forbids `< > : " | ? * \ /`, a trailing space, and a trailing dot in a path
|
|
6
|
+
// component, so a record named "Email Status Change " (trailing space) or an ACL
|
|
7
|
+
// named "x_cadso_work_task.*" cannot be checked out on Windows. toSafeFolderName
|
|
8
|
+
// falls back to the always-safe sys_id for such names; normalizeManifestKeys keeps
|
|
9
|
+
// the on-disk folder name (record.name) equal to the manifest key push looks up by.
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
const appUtils_1 = require("../appUtils");
|
|
12
|
+
const rec = (name, sys_id) => ({
|
|
13
|
+
name,
|
|
14
|
+
sys_id,
|
|
15
|
+
files: [{ name: "metaData", type: "json" }],
|
|
16
|
+
});
|
|
17
|
+
describe("toSafeFolderName", () => {
|
|
18
|
+
it("leaves a safe display name unchanged", () => {
|
|
19
|
+
expect((0, appUtils_1.toSafeFolderName)(rec("Send Email", "abc123"))).toBe("Send Email");
|
|
20
|
+
expect((0, appUtils_1.toSafeFolderName)(rec("Prevent delete - WorkGroup", "abc123"))).toBe("Prevent delete - WorkGroup");
|
|
21
|
+
});
|
|
22
|
+
it("falls back to sys_id for a trailing space", () => {
|
|
23
|
+
expect((0, appUtils_1.toSafeFolderName)(rec("Email Status Change ", "fefb8758"))).toBe("fefb8758");
|
|
24
|
+
});
|
|
25
|
+
it("falls back to sys_id for a trailing dot", () => {
|
|
26
|
+
expect((0, appUtils_1.toSafeFolderName)(rec("weird.", "sid1"))).toBe("sid1");
|
|
27
|
+
});
|
|
28
|
+
it("falls back to sys_id for each Windows-illegal character", () => {
|
|
29
|
+
const illegal = ["<", ">", ":", '"', "|", "?", "*", "\\", "/"];
|
|
30
|
+
illegal.forEach((ch) => {
|
|
31
|
+
expect((0, appUtils_1.toSafeFolderName)(rec("name" + ch + "x", "sidX"))).toBe("sidX");
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
it("treats the wildcard ACL name as unsafe", () => {
|
|
35
|
+
expect((0, appUtils_1.toSafeFolderName)(rec("x_cadso_work_task.*", "aclsid"))).toBe("aclsid");
|
|
36
|
+
});
|
|
37
|
+
it("falls back to sys_id for an empty name", () => {
|
|
38
|
+
expect((0, appUtils_1.toSafeFolderName)(rec("", "emptysid"))).toBe("emptysid");
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
describe("normalizeManifestKeys", () => {
|
|
42
|
+
const buildManifest = (records) => {
|
|
43
|
+
const byKey = {};
|
|
44
|
+
records.forEach((r) => {
|
|
45
|
+
byKey[r.sys_id] = r; // simulate a server manifest keyed by sys_id
|
|
46
|
+
});
|
|
47
|
+
return { scope: "x_cadso_test", tables: { sys_script: { records: byKey } } };
|
|
48
|
+
};
|
|
49
|
+
it("keys unsafe records by sys_id and safe records by name", () => {
|
|
50
|
+
const manifest = buildManifest([
|
|
51
|
+
rec("Send Email", "safe1"),
|
|
52
|
+
rec("Email Status Change ", "fefb8758330ede107b18bc534d5c7bfb"),
|
|
53
|
+
rec("x_cadso_work_task.*", "acl9999"),
|
|
54
|
+
]);
|
|
55
|
+
(0, appUtils_1.normalizeManifestKeys)(manifest);
|
|
56
|
+
const keys = Object.keys(manifest.tables.sys_script.records);
|
|
57
|
+
expect(keys).toContain("Send Email");
|
|
58
|
+
expect(keys).toContain("fefb8758330ede107b18bc534d5c7bfb");
|
|
59
|
+
expect(keys).toContain("acl9999");
|
|
60
|
+
expect(keys).not.toContain("Email Status Change ");
|
|
61
|
+
expect(keys).not.toContain("x_cadso_work_task.*");
|
|
62
|
+
});
|
|
63
|
+
it("keeps record.name === manifest key for every record (push invariant)", () => {
|
|
64
|
+
const manifest = buildManifest([
|
|
65
|
+
rec("Send Email", "safe1"),
|
|
66
|
+
rec("Email Status Change ", "unsafe1"),
|
|
67
|
+
rec("h:extended", "unsafe2"),
|
|
68
|
+
]);
|
|
69
|
+
(0, appUtils_1.normalizeManifestKeys)(manifest);
|
|
70
|
+
const records = manifest.tables.sys_script.records;
|
|
71
|
+
Object.keys(records).forEach((key) => {
|
|
72
|
+
expect(records[key].name).toBe(key);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
it("preserves sys_id so push can still resolve the record", () => {
|
|
76
|
+
const manifest = buildManifest([rec("Email Status Change ", "fefb8758")]);
|
|
77
|
+
(0, appUtils_1.normalizeManifestKeys)(manifest);
|
|
78
|
+
const record = manifest.tables.sys_script.records["fefb8758"];
|
|
79
|
+
expect(record).toBeDefined();
|
|
80
|
+
expect(record.sys_id).toBe("fefb8758");
|
|
81
|
+
});
|
|
82
|
+
it("disambiguates duplicate folder names with a sys_id suffix", () => {
|
|
83
|
+
const manifest = buildManifest([
|
|
84
|
+
rec("Duplicate", "1111111111111111"),
|
|
85
|
+
rec("Duplicate", "2222222222222222"),
|
|
86
|
+
]);
|
|
87
|
+
(0, appUtils_1.normalizeManifestKeys)(manifest);
|
|
88
|
+
const keys = Object.keys(manifest.tables.sys_script.records);
|
|
89
|
+
expect(keys).toContain("Duplicate");
|
|
90
|
+
expect(keys).toContain("Duplicate (22222222)");
|
|
91
|
+
// invariant still holds for the suffixed record
|
|
92
|
+
expect(manifest.tables.sys_script.records["Duplicate (22222222)"].name).toBe("Duplicate (22222222)");
|
|
93
|
+
});
|
|
94
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tenonhq/dovetail-core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.94",
|
|
4
4
|
"description": "Next-gen file syncer",
|
|
5
5
|
"license": "GPL-3.0",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -61,7 +61,8 @@
|
|
|
61
61
|
"files": [
|
|
62
62
|
"dist/**/*",
|
|
63
63
|
"skills/**/*",
|
|
64
|
-
"README.md"
|
|
64
|
+
"README.md",
|
|
65
|
+
"release-manifest.json"
|
|
65
66
|
],
|
|
66
67
|
"publishConfig": {
|
|
67
68
|
"access": "public"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "dovetail-manifest/v1",
|
|
3
|
+
"generated_at": "2026-05-28T15:54:22.498Z",
|
|
4
|
+
"events": [
|
|
5
|
+
{
|
|
6
|
+
"$schema": "dovetail-release-event/v1",
|
|
7
|
+
"event_id": "dovetail-claude-plans@0.0.19",
|
|
8
|
+
"package": "@tenonhq/dovetail-claude-plans",
|
|
9
|
+
"version": "0.0.19",
|
|
10
|
+
"prev_version": "0.0.18",
|
|
11
|
+
"published_at": "2026-05-28T15:54:22.483Z",
|
|
12
|
+
"git": {
|
|
13
|
+
"sha": "0247ef6e75093c95359413725977df07bd5bff70",
|
|
14
|
+
"tag": "dovetail-claude-plans@0.0.19"
|
|
15
|
+
},
|
|
16
|
+
"semver_bump": "patch",
|
|
17
|
+
"commits": [
|
|
18
|
+
{
|
|
19
|
+
"sha": "0247ef6e7",
|
|
20
|
+
"subject": "feat(dashboard): Prompt Lints filters + merged-PR filter for Claude Plans (#116)",
|
|
21
|
+
"pr": 116
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"sha": "5f14a3421",
|
|
25
|
+
"subject": "chore(release): @tenonhq/dovetail-claude-plans@0.0.18 [skip ci]"
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"exports_delta": null,
|
|
29
|
+
"narrative": null
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"$schema": "dovetail-release-event/v1",
|
|
33
|
+
"event_id": "dovetail-dashboard@0.0.31",
|
|
34
|
+
"package": "@tenonhq/dovetail-dashboard",
|
|
35
|
+
"version": "0.0.31",
|
|
36
|
+
"prev_version": "0.0.30",
|
|
37
|
+
"published_at": "2026-05-28T15:54:22.483Z",
|
|
38
|
+
"git": {
|
|
39
|
+
"sha": "0247ef6e75093c95359413725977df07bd5bff70",
|
|
40
|
+
"tag": "dovetail-dashboard@0.0.31"
|
|
41
|
+
},
|
|
42
|
+
"semver_bump": "patch",
|
|
43
|
+
"commits": [
|
|
44
|
+
{
|
|
45
|
+
"sha": "0247ef6e7",
|
|
46
|
+
"subject": "feat(dashboard): Prompt Lints filters + merged-PR filter for Claude Plans (#116)",
|
|
47
|
+
"pr": 116
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"sha": "c35d549c4",
|
|
51
|
+
"subject": "chore(release): @tenonhq/dovetail-dashboard@0.0.30 [skip ci]"
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"exports_delta": null,
|
|
55
|
+
"narrative": null
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"$schema": "dovetail-release-event/v1",
|
|
59
|
+
"event_id": "dovetail-core@0.0.93",
|
|
60
|
+
"package": "@tenonhq/dovetail-core",
|
|
61
|
+
"version": "0.0.93",
|
|
62
|
+
"prev_version": "0.0.92",
|
|
63
|
+
"published_at": "2026-05-28T08:23:44.072Z",
|
|
64
|
+
"git": {
|
|
65
|
+
"sha": "f2f4d29e5fbd6197fc501bc803a864ed605cd54a",
|
|
66
|
+
"tag": "dovetail-core@0.0.93"
|
|
67
|
+
},
|
|
68
|
+
"semver_bump": "patch",
|
|
69
|
+
"commits": [
|
|
70
|
+
{
|
|
71
|
+
"sha": "f2f4d29e5",
|
|
72
|
+
"subject": "feat: release manifest emit + dove knowledge-diff (downstream doc sync) (#114)",
|
|
73
|
+
"pr": 114
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"sha": "16e4c0cc2",
|
|
77
|
+
"subject": "chore(release): @tenonhq/dovetail-core@0.0.92 [skip ci]"
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
"exports_delta": null,
|
|
81
|
+
"narrative": null
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|