@fro.bot/systematic 3.14.5 → 3.15.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.
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
interface ValidatorOptions {
|
|
2
|
+
readonly argv: readonly string[];
|
|
3
|
+
readonly cwd?: string;
|
|
4
|
+
readonly outputSink?: (message: string) => void;
|
|
5
|
+
readonly errorSink?: (message: string) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function runClaudeCodeValidator(options: ValidatorOptions): number;
|
|
8
|
+
export {};
|
package/dist/cli.js
CHANGED
|
@@ -17,9 +17,9 @@ import {
|
|
|
17
17
|
} from "./index-0stf3ag0.js";
|
|
18
18
|
|
|
19
19
|
// src/cli.ts
|
|
20
|
-
import
|
|
20
|
+
import fs6 from "fs";
|
|
21
21
|
import os2 from "os";
|
|
22
|
-
import
|
|
22
|
+
import path5 from "path";
|
|
23
23
|
|
|
24
24
|
// src/lib/agent-resolver.ts
|
|
25
25
|
import fs from "fs";
|
|
@@ -1978,6 +1978,108 @@ function cleanup(agentsRoot, configOptions) {
|
|
|
1978
1978
|
return { status: "ok" };
|
|
1979
1979
|
}
|
|
1980
1980
|
|
|
1981
|
+
// src/lib/review-artifact-path.ts
|
|
1982
|
+
import fs4 from "fs";
|
|
1983
|
+
import path3 from "path";
|
|
1984
|
+
function formatReviewArtifactIssuePath(issuePath) {
|
|
1985
|
+
if (issuePath.length === 0)
|
|
1986
|
+
return "$";
|
|
1987
|
+
return issuePath.map((segment) => typeof segment === "number" ? String(segment) : segment).join(".");
|
|
1988
|
+
}
|
|
1989
|
+
function readReviewArtifact(filePath) {
|
|
1990
|
+
let content;
|
|
1991
|
+
try {
|
|
1992
|
+
content = fs4.readFileSync(filePath, "utf8");
|
|
1993
|
+
} catch {
|
|
1994
|
+
return { message: "Review artifact file could not be read", ok: false };
|
|
1995
|
+
}
|
|
1996
|
+
try {
|
|
1997
|
+
return { ok: true, value: JSON.parse(content) };
|
|
1998
|
+
} catch (error) {
|
|
1999
|
+
return {
|
|
2000
|
+
message: error instanceof SyntaxError ? "Review artifact contains malformed JSON" : "Review artifact file could not be read",
|
|
2001
|
+
ok: false
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
function isLegacyReviewArtifact(value) {
|
|
2006
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && !Object.hasOwn(value, "schema_version");
|
|
2007
|
+
}
|
|
2008
|
+
function hasParentDirectoryTraversal(input) {
|
|
2009
|
+
return input.split(/[\\/]+/).some((segment) => segment === "..");
|
|
2010
|
+
}
|
|
2011
|
+
function pathContainsSymlink(candidate) {
|
|
2012
|
+
let current = path3.parse(candidate).root;
|
|
2013
|
+
const relative2 = path3.relative(current, candidate);
|
|
2014
|
+
for (const segment of relative2.split(path3.sep)) {
|
|
2015
|
+
if (!segment)
|
|
2016
|
+
continue;
|
|
2017
|
+
current = path3.join(current, segment);
|
|
2018
|
+
try {
|
|
2019
|
+
if (fs4.lstatSync(current).isSymbolicLink())
|
|
2020
|
+
return true;
|
|
2021
|
+
} catch {
|
|
2022
|
+
return false;
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
return false;
|
|
2026
|
+
}
|
|
2027
|
+
function isWithinDirectory(candidate, directory) {
|
|
2028
|
+
const relative2 = path3.relative(directory, candidate);
|
|
2029
|
+
return relative2 !== "" && relative2 !== ".." && !relative2.startsWith(`..${path3.sep}`) && !path3.isAbsolute(relative2);
|
|
2030
|
+
}
|
|
2031
|
+
function resolveReviewArtifactPath(input, cwd) {
|
|
2032
|
+
if (hasParentDirectoryTraversal(input)) {
|
|
2033
|
+
return {
|
|
2034
|
+
message: "Review artifact path must not contain parent-directory traversal",
|
|
2035
|
+
ok: false
|
|
2036
|
+
};
|
|
2037
|
+
}
|
|
2038
|
+
const artifactRoot = path3.resolve(cwd, ".context", "systematic", "ce-review");
|
|
2039
|
+
let canonicalRoot;
|
|
2040
|
+
try {
|
|
2041
|
+
canonicalRoot = fs4.realpathSync(artifactRoot);
|
|
2042
|
+
if (!fs4.statSync(canonicalRoot).isDirectory()) {
|
|
2043
|
+
return {
|
|
2044
|
+
message: "Review artifact directory is not a directory",
|
|
2045
|
+
ok: false
|
|
2046
|
+
};
|
|
2047
|
+
}
|
|
2048
|
+
} catch {
|
|
2049
|
+
return { message: "Review artifact directory is unavailable", ok: false };
|
|
2050
|
+
}
|
|
2051
|
+
const candidate = path3.resolve(cwd, input);
|
|
2052
|
+
if (pathContainsSymlink(candidate)) {
|
|
2053
|
+
return {
|
|
2054
|
+
message: "Review artifact path must not contain symlinks",
|
|
2055
|
+
ok: false
|
|
2056
|
+
};
|
|
2057
|
+
}
|
|
2058
|
+
let canonicalTarget;
|
|
2059
|
+
try {
|
|
2060
|
+
canonicalTarget = fs4.realpathSync(candidate);
|
|
2061
|
+
} catch {
|
|
2062
|
+
return { message: "Review artifact file was not found", ok: false };
|
|
2063
|
+
}
|
|
2064
|
+
if (!isWithinDirectory(canonicalTarget, canonicalRoot)) {
|
|
2065
|
+
return {
|
|
2066
|
+
message: "Review artifact path must remain inside .context/systematic/ce-review",
|
|
2067
|
+
ok: false
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
try {
|
|
2071
|
+
if (!fs4.lstatSync(canonicalTarget).isFile()) {
|
|
2072
|
+
return {
|
|
2073
|
+
message: "Review artifact target is not a regular file",
|
|
2074
|
+
ok: false
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
} catch {
|
|
2078
|
+
return { message: "Review artifact file was not found", ok: false };
|
|
2079
|
+
}
|
|
2080
|
+
return { ok: true, path: canonicalTarget };
|
|
2081
|
+
}
|
|
2082
|
+
|
|
1981
2083
|
// src/lib/review-artifact-schema.ts
|
|
1982
2084
|
var MAX_REVIEWER_LENGTH = 64;
|
|
1983
2085
|
var MAX_RUN_ID_LENGTH = 64;
|
|
@@ -2216,8 +2318,8 @@ var ReviewArtifactSchema = exports_external.object({
|
|
|
2216
2318
|
}).strict();
|
|
2217
2319
|
|
|
2218
2320
|
// src/lib/setup.ts
|
|
2219
|
-
import
|
|
2220
|
-
import
|
|
2321
|
+
import fs5 from "fs";
|
|
2322
|
+
import path4 from "path";
|
|
2221
2323
|
var SYSTEMATIC_PACKAGE_NAME = "@fro.bot/systematic";
|
|
2222
2324
|
var PI_PACKAGE_IDENTIFIER = `npm:${SYSTEMATIC_PACKAGE_NAME}`;
|
|
2223
2325
|
var SETUP_ERROR_NAME = "SetupError";
|
|
@@ -2227,10 +2329,10 @@ function createSetupError(message) {
|
|
|
2227
2329
|
return error;
|
|
2228
2330
|
}
|
|
2229
2331
|
var DEFAULT_OPS = {
|
|
2230
|
-
writeFileSync:
|
|
2231
|
-
renameSync:
|
|
2232
|
-
unlinkSync:
|
|
2233
|
-
chmodSync:
|
|
2332
|
+
writeFileSync: fs5.writeFileSync,
|
|
2333
|
+
renameSync: fs5.renameSync,
|
|
2334
|
+
unlinkSync: fs5.unlinkSync,
|
|
2335
|
+
chmodSync: fs5.chmodSync
|
|
2234
2336
|
};
|
|
2235
2337
|
function isRecord2(value) {
|
|
2236
2338
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -2252,7 +2354,7 @@ function isPiSystematicIdentifier(identifier) {
|
|
|
2252
2354
|
}
|
|
2253
2355
|
function lstatOrNull(targetPath) {
|
|
2254
2356
|
try {
|
|
2255
|
-
return
|
|
2357
|
+
return fs5.lstatSync(targetPath);
|
|
2256
2358
|
} catch (error) {
|
|
2257
2359
|
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
2258
2360
|
return null;
|
|
@@ -2261,10 +2363,10 @@ function lstatOrNull(targetPath) {
|
|
|
2261
2363
|
}
|
|
2262
2364
|
}
|
|
2263
2365
|
function assertRealpathUnderCwd(dir, cwd) {
|
|
2264
|
-
const realDir =
|
|
2265
|
-
const realCwd =
|
|
2266
|
-
const relative2 =
|
|
2267
|
-
if (relative2.startsWith("..") ||
|
|
2366
|
+
const realDir = fs5.realpathSync(dir);
|
|
2367
|
+
const realCwd = fs5.realpathSync(cwd);
|
|
2368
|
+
const relative2 = path4.relative(realCwd, realDir);
|
|
2369
|
+
if (relative2.startsWith("..") || path4.isAbsolute(relative2)) {
|
|
2268
2370
|
throw createSetupError(`Refusing to write under ${dir}: resolved directory escapes the project root`);
|
|
2269
2371
|
}
|
|
2270
2372
|
}
|
|
@@ -2277,7 +2379,7 @@ function assertParentTrusted(parentDir, cwd) {
|
|
|
2277
2379
|
assertRealpathUnderCwd(parentDir, cwd);
|
|
2278
2380
|
return;
|
|
2279
2381
|
}
|
|
2280
|
-
|
|
2382
|
+
fs5.mkdirSync(parentDir, { recursive: true });
|
|
2281
2383
|
assertRealpathUnderCwd(parentDir, cwd);
|
|
2282
2384
|
}
|
|
2283
2385
|
var OPENCODE_TARGET_CANDIDATES = [
|
|
@@ -2287,19 +2389,19 @@ var OPENCODE_TARGET_CANDIDATES = [
|
|
|
2287
2389
|
"opencode.json"
|
|
2288
2390
|
];
|
|
2289
2391
|
function resolveOpenCodeTargetPath(cwd) {
|
|
2290
|
-
const opencodeDirStat = lstatOrNull(
|
|
2392
|
+
const opencodeDirStat = lstatOrNull(path4.join(cwd, ".opencode"));
|
|
2291
2393
|
if (opencodeDirStat?.isSymbolicLink()) {
|
|
2292
|
-
return
|
|
2394
|
+
return path4.join(cwd, OPENCODE_TARGET_CANDIDATES[0]);
|
|
2293
2395
|
}
|
|
2294
2396
|
for (const candidate of OPENCODE_TARGET_CANDIDATES) {
|
|
2295
|
-
const candidatePath =
|
|
2397
|
+
const candidatePath = path4.join(cwd, candidate);
|
|
2296
2398
|
if (lstatOrNull(candidatePath) !== null)
|
|
2297
2399
|
return candidatePath;
|
|
2298
2400
|
}
|
|
2299
|
-
return
|
|
2401
|
+
return path4.join(cwd, "opencode.jsonc");
|
|
2300
2402
|
}
|
|
2301
2403
|
var IS_WINDOWS = process.platform === "win32";
|
|
2302
|
-
var OPEN_FLAGS = IS_WINDOWS ?
|
|
2404
|
+
var OPEN_FLAGS = IS_WINDOWS ? fs5.constants.O_RDONLY : fs5.constants.O_RDONLY | fs5.constants.O_NOFOLLOW | fs5.constants.O_NONBLOCK;
|
|
2303
2405
|
function assertWindowsPreOpenTrust(targetPath) {
|
|
2304
2406
|
const preStat = lstatOrNull(targetPath);
|
|
2305
2407
|
if (preStat && (preStat.isSymbolicLink() || !preStat.isFile())) {
|
|
@@ -2311,7 +2413,7 @@ function openTrustedExisting(targetPath) {
|
|
|
2311
2413
|
assertWindowsPreOpenTrust(targetPath);
|
|
2312
2414
|
let fd;
|
|
2313
2415
|
try {
|
|
2314
|
-
fd =
|
|
2416
|
+
fd = fs5.openSync(targetPath, OPEN_FLAGS);
|
|
2315
2417
|
} catch (error) {
|
|
2316
2418
|
if (error instanceof Error && "code" in error) {
|
|
2317
2419
|
if (error.code === "ENOENT")
|
|
@@ -2323,13 +2425,13 @@ function openTrustedExisting(targetPath) {
|
|
|
2323
2425
|
throw error;
|
|
2324
2426
|
}
|
|
2325
2427
|
try {
|
|
2326
|
-
const stat =
|
|
2428
|
+
const stat = fs5.fstatSync(fd);
|
|
2327
2429
|
if (!stat.isFile()) {
|
|
2328
2430
|
throw createSetupError(`Refusing to read ${targetPath}: not a regular file`);
|
|
2329
2431
|
}
|
|
2330
|
-
return { bytes:
|
|
2432
|
+
return { bytes: fs5.readFileSync(fd), mode: stat.mode & 511 };
|
|
2331
2433
|
} finally {
|
|
2332
|
-
|
|
2434
|
+
fs5.closeSync(fd);
|
|
2333
2435
|
}
|
|
2334
2436
|
}
|
|
2335
2437
|
function atomicWrite(targetPath, content, originalBytes, mode, ops) {
|
|
@@ -2344,8 +2446,8 @@ function atomicWrite(targetPath, content, originalBytes, mode, ops) {
|
|
|
2344
2446
|
writeTempAndRename(targetPath, content, mode, preserveMode, ops);
|
|
2345
2447
|
}
|
|
2346
2448
|
function writeTempAndRename(destPath, content, mode, preserveExactMode, ops) {
|
|
2347
|
-
const parentDir =
|
|
2348
|
-
const tempPath = makeTempPath(parentDir,
|
|
2449
|
+
const parentDir = path4.dirname(destPath);
|
|
2450
|
+
const tempPath = makeTempPath(parentDir, path4.basename(destPath));
|
|
2349
2451
|
try {
|
|
2350
2452
|
ops.writeFileSync(tempPath, content, { flag: "wx", mode });
|
|
2351
2453
|
if (preserveExactMode) {
|
|
@@ -2358,10 +2460,10 @@ function writeTempAndRename(destPath, content, mode, preserveExactMode, ops) {
|
|
|
2358
2460
|
}
|
|
2359
2461
|
}
|
|
2360
2462
|
function makeTempPath(parentDir, basename2) {
|
|
2361
|
-
return
|
|
2463
|
+
return path4.join(parentDir, `.${basename2}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
2362
2464
|
}
|
|
2363
2465
|
function cleanupTemp(tempPath, ops) {
|
|
2364
|
-
if (!
|
|
2466
|
+
if (!fs5.existsSync(tempPath))
|
|
2365
2467
|
return;
|
|
2366
2468
|
try {
|
|
2367
2469
|
ops.unlinkSync(tempPath);
|
|
@@ -2395,7 +2497,7 @@ function parseOpenCodeJsonc(rawText, targetPath) {
|
|
|
2395
2497
|
}
|
|
2396
2498
|
function setupOpenCode(cwd, ops) {
|
|
2397
2499
|
const targetPath = resolveOpenCodeTargetPath(cwd);
|
|
2398
|
-
assertParentTrusted(
|
|
2500
|
+
assertParentTrusted(path4.dirname(targetPath), cwd);
|
|
2399
2501
|
const existing = openTrustedExisting(targetPath);
|
|
2400
2502
|
if (existing === null) {
|
|
2401
2503
|
const content = `${JSON.stringify({ plugin: [SYSTEMATIC_PACKAGE_NAME] }, null, 2)}
|
|
@@ -2453,8 +2555,8 @@ function assertPiTaggedEntryUsable(entry, targetPath) {
|
|
|
2453
2555
|
}
|
|
2454
2556
|
}
|
|
2455
2557
|
function setupPi(cwd, ops) {
|
|
2456
|
-
const targetPath =
|
|
2457
|
-
assertParentTrusted(
|
|
2558
|
+
const targetPath = path4.join(cwd, ".pi", "settings.json");
|
|
2559
|
+
assertParentTrusted(path4.dirname(targetPath), cwd);
|
|
2458
2560
|
const existing = openTrustedExisting(targetPath);
|
|
2459
2561
|
if (existing === null) {
|
|
2460
2562
|
const content = `${JSON.stringify({ packages: [PI_PACKAGE_IDENTIFIER] }, null, 2)}
|
|
@@ -2514,10 +2616,10 @@ function setupHarness(harness, cwd, opsOverride) {
|
|
|
2514
2616
|
// src/cli.ts
|
|
2515
2617
|
function readPackageMetadata(packageRoot) {
|
|
2516
2618
|
try {
|
|
2517
|
-
const packageJsonPath =
|
|
2518
|
-
if (!
|
|
2619
|
+
const packageJsonPath = path5.join(packageRoot, "package.json");
|
|
2620
|
+
if (!fs6.existsSync(packageJsonPath))
|
|
2519
2621
|
return {};
|
|
2520
|
-
const content =
|
|
2622
|
+
const content = fs6.readFileSync(packageJsonPath, "utf8");
|
|
2521
2623
|
const parsed = JSON.parse(content);
|
|
2522
2624
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2523
2625
|
return {};
|
|
@@ -2531,7 +2633,7 @@ function readPackageMetadata(packageRoot) {
|
|
|
2531
2633
|
return {};
|
|
2532
2634
|
}
|
|
2533
2635
|
}
|
|
2534
|
-
var PACKAGE_ROOT =
|
|
2636
|
+
var PACKAGE_ROOT = path5.resolve(import.meta.dirname, "..");
|
|
2535
2637
|
var VERSION = readPackageMetadata(PACKAGE_ROOT).version ?? "unknown";
|
|
2536
2638
|
var HELP = `
|
|
2537
2639
|
systematic - OpenCode plugin for systematic engineering workflows
|
|
@@ -2590,9 +2692,9 @@ Scope:
|
|
|
2590
2692
|
var VALIDATE_REVIEW_ARTIFACT_USAGE = "Usage: systematic validate-review-artifact <path>";
|
|
2591
2693
|
var REVIEW_ARTIFACT_SCHEMA_RELATIVE_PATH = "skills/ce-review/references/review-summary-schema.json";
|
|
2592
2694
|
function defaultCapabilityRoots() {
|
|
2593
|
-
const configDir = process.env.XDG_CONFIG_HOME ?
|
|
2695
|
+
const configDir = process.env.XDG_CONFIG_HOME ? path5.join(process.env.XDG_CONFIG_HOME, "opencode") : path5.join(os2.homedir(), ".config/opencode");
|
|
2594
2696
|
return {
|
|
2595
|
-
agentsRoot:
|
|
2697
|
+
agentsRoot: path5.join(PACKAGE_ROOT, "agents"),
|
|
2596
2698
|
configDir,
|
|
2597
2699
|
cwd: process.cwd(),
|
|
2598
2700
|
homeDir: os2.homedir(),
|
|
@@ -2691,9 +2793,9 @@ function collectConfigMetadata(roots, injected) {
|
|
|
2691
2793
|
}
|
|
2692
2794
|
function resolveCapabilityRootPath(root) {
|
|
2693
2795
|
try {
|
|
2694
|
-
return
|
|
2796
|
+
return fs6.realpathSync(root);
|
|
2695
2797
|
} catch {
|
|
2696
|
-
return
|
|
2798
|
+
return path5.resolve(root);
|
|
2697
2799
|
}
|
|
2698
2800
|
}
|
|
2699
2801
|
function capabilityRoots(roots) {
|
|
@@ -2703,7 +2805,7 @@ function capabilityRoots(roots) {
|
|
|
2703
2805
|
{ id: "package", path: resolveCapabilityRootPath(roots.packageRoot) },
|
|
2704
2806
|
{
|
|
2705
2807
|
id: "skills",
|
|
2706
|
-
path: resolveCapabilityRootPath(
|
|
2808
|
+
path: resolveCapabilityRootPath(path5.join(roots.packageRoot, "skills"))
|
|
2707
2809
|
},
|
|
2708
2810
|
{ id: "user", path: resolveCapabilityRootPath(roots.homeDir) }
|
|
2709
2811
|
];
|
|
@@ -2744,85 +2846,6 @@ function runCapabilities(options) {
|
|
|
2744
2846
|
function runCapabilitiesCli(options) {
|
|
2745
2847
|
return runCapabilities(options);
|
|
2746
2848
|
}
|
|
2747
|
-
function hasParentDirectoryTraversal(input) {
|
|
2748
|
-
return input.split(/[\\/]+/).some((segment) => segment === "..");
|
|
2749
|
-
}
|
|
2750
|
-
function pathContainsSymlink(candidate) {
|
|
2751
|
-
let current = path4.parse(candidate).root;
|
|
2752
|
-
const relative2 = path4.relative(current, candidate);
|
|
2753
|
-
for (const segment of relative2.split(path4.sep)) {
|
|
2754
|
-
if (!segment)
|
|
2755
|
-
continue;
|
|
2756
|
-
current = path4.join(current, segment);
|
|
2757
|
-
try {
|
|
2758
|
-
if (fs5.lstatSync(current).isSymbolicLink())
|
|
2759
|
-
return true;
|
|
2760
|
-
} catch {
|
|
2761
|
-
return false;
|
|
2762
|
-
}
|
|
2763
|
-
}
|
|
2764
|
-
return false;
|
|
2765
|
-
}
|
|
2766
|
-
function isWithinDirectory(candidate, directory) {
|
|
2767
|
-
const relative2 = path4.relative(directory, candidate);
|
|
2768
|
-
return relative2 !== "" && relative2 !== ".." && !relative2.startsWith(`..${path4.sep}`) && !path4.isAbsolute(relative2);
|
|
2769
|
-
}
|
|
2770
|
-
function resolveReviewArtifactPath(input, cwd) {
|
|
2771
|
-
if (hasParentDirectoryTraversal(input)) {
|
|
2772
|
-
return {
|
|
2773
|
-
message: "Review artifact path must not contain parent-directory traversal",
|
|
2774
|
-
ok: false
|
|
2775
|
-
};
|
|
2776
|
-
}
|
|
2777
|
-
const artifactRoot = path4.resolve(cwd, ".context", "systematic", "ce-review");
|
|
2778
|
-
let canonicalRoot;
|
|
2779
|
-
try {
|
|
2780
|
-
canonicalRoot = fs5.realpathSync(artifactRoot);
|
|
2781
|
-
if (!fs5.statSync(canonicalRoot).isDirectory()) {
|
|
2782
|
-
return {
|
|
2783
|
-
message: "Review artifact directory is not a directory",
|
|
2784
|
-
ok: false
|
|
2785
|
-
};
|
|
2786
|
-
}
|
|
2787
|
-
} catch {
|
|
2788
|
-
return { message: "Review artifact directory is unavailable", ok: false };
|
|
2789
|
-
}
|
|
2790
|
-
const candidate = path4.resolve(cwd, input);
|
|
2791
|
-
if (pathContainsSymlink(candidate)) {
|
|
2792
|
-
return {
|
|
2793
|
-
message: "Review artifact path must not contain symlinks",
|
|
2794
|
-
ok: false
|
|
2795
|
-
};
|
|
2796
|
-
}
|
|
2797
|
-
let canonicalTarget;
|
|
2798
|
-
try {
|
|
2799
|
-
canonicalTarget = fs5.realpathSync(candidate);
|
|
2800
|
-
} catch {
|
|
2801
|
-
return { message: "Review artifact file was not found", ok: false };
|
|
2802
|
-
}
|
|
2803
|
-
if (!isWithinDirectory(canonicalTarget, canonicalRoot)) {
|
|
2804
|
-
return {
|
|
2805
|
-
message: "Review artifact path must remain inside .context/systematic/ce-review",
|
|
2806
|
-
ok: false
|
|
2807
|
-
};
|
|
2808
|
-
}
|
|
2809
|
-
try {
|
|
2810
|
-
if (!fs5.lstatSync(canonicalTarget).isFile()) {
|
|
2811
|
-
return {
|
|
2812
|
-
message: "Review artifact target is not a regular file",
|
|
2813
|
-
ok: false
|
|
2814
|
-
};
|
|
2815
|
-
}
|
|
2816
|
-
} catch {
|
|
2817
|
-
return { message: "Review artifact file was not found", ok: false };
|
|
2818
|
-
}
|
|
2819
|
-
return { ok: true, path: canonicalTarget };
|
|
2820
|
-
}
|
|
2821
|
-
function formatReviewArtifactIssuePath(issuePath) {
|
|
2822
|
-
if (issuePath.length === 0)
|
|
2823
|
-
return "$";
|
|
2824
|
-
return issuePath.map((segment) => typeof segment === "number" ? String(segment) : segment).join(".");
|
|
2825
|
-
}
|
|
2826
2849
|
function validateReviewArtifactArgument(argv) {
|
|
2827
2850
|
const commandIndex = argv[0] === "systematic" ? 1 : 0;
|
|
2828
2851
|
if (argv[commandIndex] !== "validate-review-artifact")
|
|
@@ -2831,25 +2854,6 @@ function validateReviewArtifactArgument(argv) {
|
|
|
2831
2854
|
return;
|
|
2832
2855
|
return argv[commandIndex + 1];
|
|
2833
2856
|
}
|
|
2834
|
-
function readReviewArtifact(filePath) {
|
|
2835
|
-
let content;
|
|
2836
|
-
try {
|
|
2837
|
-
content = fs5.readFileSync(filePath, "utf8");
|
|
2838
|
-
} catch {
|
|
2839
|
-
return { message: "Review artifact file could not be read", ok: false };
|
|
2840
|
-
}
|
|
2841
|
-
try {
|
|
2842
|
-
return { ok: true, value: JSON.parse(content) };
|
|
2843
|
-
} catch (error) {
|
|
2844
|
-
return {
|
|
2845
|
-
message: error instanceof SyntaxError ? "Review artifact contains malformed JSON" : "Review artifact file could not be read",
|
|
2846
|
-
ok: false
|
|
2847
|
-
};
|
|
2848
|
-
}
|
|
2849
|
-
}
|
|
2850
|
-
function isLegacyReviewArtifact(value) {
|
|
2851
|
-
return value !== null && typeof value === "object" && !Array.isArray(value) && !Object.hasOwn(value, "schema_version");
|
|
2852
|
-
}
|
|
2853
2857
|
function runValidateReviewArtifact(options) {
|
|
2854
2858
|
const outputSink = options.outputSink ?? ((message) => console.log(message));
|
|
2855
2859
|
const errorSink = options.errorSink ?? ((message) => console.error(message));
|
|
@@ -2916,7 +2920,7 @@ function setupCommand(rest) {
|
|
|
2916
2920
|
}
|
|
2917
2921
|
}
|
|
2918
2922
|
function listItems(type) {
|
|
2919
|
-
const packageRoot =
|
|
2923
|
+
const packageRoot = path5.resolve(import.meta.dirname, "..");
|
|
2920
2924
|
const bundledDir = packageRoot;
|
|
2921
2925
|
let finder;
|
|
2922
2926
|
let subdir;
|
|
@@ -2937,7 +2941,7 @@ function listItems(type) {
|
|
|
2937
2941
|
console.error(`Unknown type: ${type}. Use: skills, agents, commands`);
|
|
2938
2942
|
process.exit(1);
|
|
2939
2943
|
}
|
|
2940
|
-
const items = finder(
|
|
2944
|
+
const items = finder(path5.join(bundledDir, subdir));
|
|
2941
2945
|
if (items.length === 0) {
|
|
2942
2946
|
console.log(`No ${type} found.`);
|
|
2943
2947
|
return;
|
|
@@ -2954,15 +2958,15 @@ function configShow() {
|
|
|
2954
2958
|
`);
|
|
2955
2959
|
console.log(` User config: ${paths.userConfig}`);
|
|
2956
2960
|
console.log(` Project config: ${paths.projectConfig}`);
|
|
2957
|
-
if (
|
|
2961
|
+
if (fs6.existsSync(paths.projectConfig)) {
|
|
2958
2962
|
console.log(`
|
|
2959
2963
|
Project configuration:`);
|
|
2960
|
-
console.log(
|
|
2964
|
+
console.log(fs6.readFileSync(paths.projectConfig, "utf-8"));
|
|
2961
2965
|
}
|
|
2962
|
-
if (
|
|
2966
|
+
if (fs6.existsSync(paths.userConfig)) {
|
|
2963
2967
|
console.log(`
|
|
2964
2968
|
User configuration:`);
|
|
2965
|
-
console.log(
|
|
2969
|
+
console.log(fs6.readFileSync(paths.userConfig, "utf-8"));
|
|
2966
2970
|
}
|
|
2967
2971
|
}
|
|
2968
2972
|
function configPath() {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type ArtifactPathResult = {
|
|
2
|
+
readonly ok: true;
|
|
3
|
+
readonly path: string;
|
|
4
|
+
} | {
|
|
5
|
+
readonly ok: false;
|
|
6
|
+
readonly message: string;
|
|
7
|
+
};
|
|
8
|
+
export type ReadArtifactResult = {
|
|
9
|
+
readonly ok: true;
|
|
10
|
+
readonly value: unknown;
|
|
11
|
+
} | {
|
|
12
|
+
readonly ok: false;
|
|
13
|
+
readonly message: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function formatReviewArtifactIssuePath(issuePath: readonly PropertyKey[]): string;
|
|
16
|
+
export declare function readReviewArtifact(filePath: string): ReadArtifactResult;
|
|
17
|
+
export declare function isLegacyReviewArtifact(value: unknown): boolean;
|
|
18
|
+
export declare function hasParentDirectoryTraversal(input: string): boolean;
|
|
19
|
+
export declare function pathContainsSymlink(candidate: string): boolean;
|
|
20
|
+
export declare function resolveReviewArtifactPath(input: string, cwd: string): ArtifactPathResult;
|
package/package.json
CHANGED
|
@@ -210,15 +210,27 @@ validating it. This ordering makes the artifact validatable at all: without
|
|
|
210
210
|
`schema_version`, the validator reports the legacy status (exit 3) rather than
|
|
211
211
|
a real validation result.
|
|
212
212
|
|
|
213
|
-
After writing `review-summary.json`, the parent
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
213
|
+
After writing `review-summary.json`, the parent resolves and runs a validator
|
|
214
|
+
in this order: the bundled `systematic-validate-review-artifact <path>` command
|
|
215
|
+
first, then the npm-installed `systematic validate-review-artifact <path>`
|
|
216
|
+
command. Both can be present at once. The bundled command ships beside the
|
|
217
|
+
prose being executed, so it is the one whose behavior matches the contract.
|
|
218
|
+
The parent runs the first command it resolves and reads its result; it does not
|
|
219
|
+
merely test whether a name is on `PATH`, because a version-manager shim can be
|
|
220
|
+
present there and fail on every invocation.
|
|
221
|
+
|
|
222
|
+
The command runs from the repository root. Containment resolves
|
|
223
|
+
`.context/systematic/ce-review` relative to the working directory. If it runs
|
|
224
|
+
elsewhere, the CLI reports that the directory is unavailable and the parent
|
|
225
|
+
records that reason. This is visible degradation rather than silent success,
|
|
226
|
+
so it is acceptable. If neither command is available, the parent records
|
|
227
|
+
`validation.status: "unavailable"` with a reason that the validator is absent.
|
|
228
|
+
If a resolved command is present but cannot be started or otherwise fails
|
|
229
|
+
before returning a validation result, the parent records the same status with
|
|
230
|
+
a distinct invocation-failure reason. Neither case is `failed`: that value is
|
|
231
|
+
reserved for a validator that ran and found that the artifact did not conform.
|
|
232
|
+
When a validator is available and the parent does not run it, that is
|
|
233
|
+
`validation.status: "not_attempted"`, also with a reason. The
|
|
222
234
|
`validation.status` values are `passed`, `failed`, `unavailable`, and
|
|
223
235
|
`not_attempted`; `validation.reason` is required for every status except
|
|
224
236
|
`passed`, where it is forbidden.
|