@outcomeeng/spx 0.5.0 → 0.5.2
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 +7 -9
- package/dist/cli.js +1531 -1265
- package/dist/cli.js.map +1 -1
- package/package.json +2 -11
package/dist/cli.js
CHANGED
|
@@ -234,7 +234,7 @@ async function runVerifyPipeline(filePath, productDir) {
|
|
|
234
234
|
return { lines: [], exitCode: 0, verdict: verdict.header?.verdict };
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
-
// src/
|
|
237
|
+
// src/commands/audit/verify.ts
|
|
238
238
|
async function runVerifyCommand(filePath, productDir, writeLine) {
|
|
239
239
|
const result = await runVerifyPipeline(filePath, productDir);
|
|
240
240
|
if (result.exitCode === 0) {
|
|
@@ -246,6 +246,8 @@ async function runVerifyCommand(filePath, productDir, writeLine) {
|
|
|
246
246
|
}
|
|
247
247
|
return result.exitCode;
|
|
248
248
|
}
|
|
249
|
+
|
|
250
|
+
// src/interfaces/cli/audit.ts
|
|
249
251
|
var auditDomain = {
|
|
250
252
|
name: "audit",
|
|
251
253
|
description: "Audit verdict verification",
|
|
@@ -272,7 +274,7 @@ async function initCommand(options = {}) {
|
|
|
272
274
|
if (!exists) {
|
|
273
275
|
await execa(
|
|
274
276
|
"claude",
|
|
275
|
-
["plugin", "marketplace", "add", "outcomeeng/
|
|
277
|
+
["plugin", "marketplace", "add", "outcomeeng/plugins"],
|
|
276
278
|
{ cwd }
|
|
277
279
|
);
|
|
278
280
|
return "\u2713 outcomeeng marketplace installed successfully\n\nRun 'claude plugin marketplace list' to view all marketplaces.";
|
|
@@ -839,7 +841,7 @@ Searched for: **/.claude/settings.local.json`;
|
|
|
839
841
|
return formatReport(result, previewOnly, globalSettingsPath, outputFile);
|
|
840
842
|
}
|
|
841
843
|
|
|
842
|
-
// src/
|
|
844
|
+
// src/interfaces/cli/claude.ts
|
|
843
845
|
function registerClaudeCommands(claudeCmd) {
|
|
844
846
|
claudeCmd.command("init").description("Initialize or update outcomeeng marketplace plugin").action(async () => {
|
|
845
847
|
try {
|
|
@@ -2729,7 +2731,7 @@ function resolveProductDir(cwd = process.cwd()) {
|
|
|
2729
2731
|
};
|
|
2730
2732
|
}
|
|
2731
2733
|
|
|
2732
|
-
// src/
|
|
2734
|
+
// src/interfaces/cli/config.ts
|
|
2733
2735
|
function buildDefaultDeps() {
|
|
2734
2736
|
return {
|
|
2735
2737
|
resolveConfig,
|
|
@@ -2778,7 +2780,7 @@ var configDomain = {
|
|
|
2778
2780
|
};
|
|
2779
2781
|
|
|
2780
2782
|
// src/commands/session/archive.ts
|
|
2781
|
-
import { mkdir,
|
|
2783
|
+
import { mkdir, rename, stat } from "fs/promises";
|
|
2782
2784
|
import { dirname as dirname2, join as join4 } from "path";
|
|
2783
2785
|
|
|
2784
2786
|
// src/domains/session/archive.ts
|
|
@@ -2892,10 +2894,12 @@ var SessionInvalidNextStepError = class extends SessionError {
|
|
|
2892
2894
|
this.name = "SessionInvalidNextStepError";
|
|
2893
2895
|
}
|
|
2894
2896
|
};
|
|
2895
|
-
var
|
|
2896
|
-
constructor(
|
|
2897
|
-
super(
|
|
2898
|
-
|
|
2897
|
+
var SessionHandoffBaseError = class extends SessionError {
|
|
2898
|
+
constructor() {
|
|
2899
|
+
super(
|
|
2900
|
+
"Cannot create a handoff session from this git work context. Run handoff from the root worktree, or from a linked worktree with a clean working tree detached at the tip of origin/<default branch>."
|
|
2901
|
+
);
|
|
2902
|
+
this.name = "SessionHandoffBaseError";
|
|
2899
2903
|
}
|
|
2900
2904
|
};
|
|
2901
2905
|
var SessionNotClaimedError = class extends SessionError {
|
|
@@ -2913,179 +2917,27 @@ var NoSessionsAvailableError = class extends SessionError {
|
|
|
2913
2917
|
this.name = "NoSessionsAvailableError";
|
|
2914
2918
|
}
|
|
2915
2919
|
};
|
|
2920
|
+
var SessionLegacyFrontmatterInputError = class extends SessionError {
|
|
2921
|
+
constructor() {
|
|
2922
|
+
super(
|
|
2923
|
+
`Invalid handoff input: stdin opens with the YAML-frontmatter delimiter \`---\`. Use the JSON-prefix wire format: a single JSON object holding caller-supplied fields followed by the body bytes verbatim. Example:
|
|
2916
2924
|
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
// src/domains/session/timestamp.ts
|
|
2921
|
-
var SESSION_ID_PATTERN = /^(\d{4})-(\d{2})-(\d{2})_(\d{2})-(\d{2})-(\d{2})$/;
|
|
2922
|
-
var SESSION_ID_SEPARATOR = "_";
|
|
2923
|
-
function generateSessionId(options = {}) {
|
|
2924
|
-
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
2925
|
-
const date = now();
|
|
2926
|
-
const year = date.getFullYear();
|
|
2927
|
-
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
2928
|
-
const day = String(date.getDate()).padStart(2, "0");
|
|
2929
|
-
const hours = String(date.getHours()).padStart(2, "0");
|
|
2930
|
-
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
2931
|
-
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
2932
|
-
return `${year}-${month}-${day}${SESSION_ID_SEPARATOR}${hours}-${minutes}-${seconds}`;
|
|
2933
|
-
}
|
|
2934
|
-
function parseSessionId(id) {
|
|
2935
|
-
const match = SESSION_ID_PATTERN.exec(id);
|
|
2936
|
-
if (!match) {
|
|
2937
|
-
return null;
|
|
2925
|
+
printf '%s\\n' '{"priority":"high","goal":"...","next_step":"..."}' '# Body' | spx session handoff`
|
|
2926
|
+
);
|
|
2927
|
+
this.name = "SessionLegacyFrontmatterInputError";
|
|
2938
2928
|
}
|
|
2939
|
-
const [, yearStr, monthStr, dayStr, hoursStr, minutesStr, secondsStr] = match;
|
|
2940
|
-
const year = parseInt(yearStr, 10);
|
|
2941
|
-
const month = parseInt(monthStr, 10) - 1;
|
|
2942
|
-
const day = parseInt(dayStr, 10);
|
|
2943
|
-
const hours = parseInt(hoursStr, 10);
|
|
2944
|
-
const minutes = parseInt(minutesStr, 10);
|
|
2945
|
-
const seconds = parseInt(secondsStr, 10);
|
|
2946
|
-
if (month < 0 || month > 11) return null;
|
|
2947
|
-
if (day < 1 || day > 31) return null;
|
|
2948
|
-
if (hours < 0 || hours > 23) return null;
|
|
2949
|
-
if (minutes < 0 || minutes > 59) return null;
|
|
2950
|
-
if (seconds < 0 || seconds > 59) return null;
|
|
2951
|
-
return new Date(year, month, day, hours, minutes, seconds);
|
|
2952
|
-
}
|
|
2953
|
-
|
|
2954
|
-
// src/domains/session/types.ts
|
|
2955
|
-
var SESSION_PRIORITY = {
|
|
2956
|
-
HIGH: "high",
|
|
2957
|
-
MEDIUM: "medium",
|
|
2958
|
-
LOW: "low"
|
|
2959
|
-
};
|
|
2960
|
-
var SESSION_STATUSES = ["todo", "doing", "archive"];
|
|
2961
|
-
var DEFAULT_LIST_STATUSES = ["doing", "todo"];
|
|
2962
|
-
var PRIORITY_ORDER = {
|
|
2963
|
-
[SESSION_PRIORITY.HIGH]: 0,
|
|
2964
|
-
[SESSION_PRIORITY.MEDIUM]: 1,
|
|
2965
|
-
[SESSION_PRIORITY.LOW]: 2
|
|
2966
|
-
};
|
|
2967
|
-
var DEFAULT_PRIORITY = SESSION_PRIORITY.MEDIUM;
|
|
2968
|
-
var SESSION_FRONT_MATTER = {
|
|
2969
|
-
PRIORITY: "priority",
|
|
2970
|
-
ID: "id",
|
|
2971
|
-
BRANCH: "branch",
|
|
2972
|
-
WORKTREE: "worktree",
|
|
2973
|
-
GOAL: "goal",
|
|
2974
|
-
NEXT_STEP: "next_step",
|
|
2975
|
-
RESULT: "result",
|
|
2976
|
-
CREATED_AT: "created_at",
|
|
2977
|
-
AGENT_SESSION_ID: "agent_session_id",
|
|
2978
|
-
SPECS: "specs",
|
|
2979
|
-
FILES: "files"
|
|
2980
|
-
};
|
|
2981
|
-
|
|
2982
|
-
// src/domains/session/list.ts
|
|
2983
|
-
var FRONT_MATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n(?:---|\.\.\.)\r?\n?/;
|
|
2984
|
-
var SESSION_PRIORITY_VALUES = Object.values(SESSION_PRIORITY);
|
|
2985
|
-
var DEFAULT_SESSION_METADATA = {
|
|
2986
|
-
priority: DEFAULT_PRIORITY,
|
|
2987
|
-
branch: "",
|
|
2988
|
-
worktree: "",
|
|
2989
|
-
goal: "",
|
|
2990
|
-
next_step: "",
|
|
2991
|
-
result: "",
|
|
2992
|
-
specs: [],
|
|
2993
|
-
files: []
|
|
2994
2929
|
};
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
files: []
|
|
3000
|
-
};
|
|
3001
|
-
}
|
|
3002
|
-
function isValidPriority(value) {
|
|
3003
|
-
return typeof value === "string" && SESSION_PRIORITY_VALUES.some((priority) => priority === value);
|
|
3004
|
-
}
|
|
3005
|
-
function parseSessionMetadata(content) {
|
|
3006
|
-
const match = FRONT_MATTER_PATTERN.exec(content);
|
|
3007
|
-
if (!match) {
|
|
3008
|
-
return defaultSessionMetadata();
|
|
3009
|
-
}
|
|
3010
|
-
try {
|
|
3011
|
-
const parsed = parseYaml2(match[1]);
|
|
3012
|
-
if (!parsed || typeof parsed !== "object") {
|
|
3013
|
-
return defaultSessionMetadata();
|
|
3014
|
-
}
|
|
3015
|
-
const rawPriority = parsed[SESSION_FRONT_MATTER.PRIORITY];
|
|
3016
|
-
const priority = isValidPriority(rawPriority) ? rawPriority : DEFAULT_PRIORITY;
|
|
3017
|
-
const metadata = {
|
|
3018
|
-
...defaultSessionMetadata(),
|
|
3019
|
-
priority
|
|
3020
|
-
};
|
|
3021
|
-
const id = parsed[SESSION_FRONT_MATTER.ID];
|
|
3022
|
-
if (typeof id === "string") metadata.id = id;
|
|
3023
|
-
const branch = parsed[SESSION_FRONT_MATTER.BRANCH];
|
|
3024
|
-
metadata.branch = typeof branch === "string" ? branch : DEFAULT_SESSION_METADATA.branch;
|
|
3025
|
-
const worktree = parsed[SESSION_FRONT_MATTER.WORKTREE];
|
|
3026
|
-
metadata.worktree = typeof worktree === "string" ? worktree : DEFAULT_SESSION_METADATA.worktree;
|
|
3027
|
-
const goal = parsed[SESSION_FRONT_MATTER.GOAL];
|
|
3028
|
-
metadata.goal = typeof goal === "string" ? goal : DEFAULT_SESSION_METADATA.goal;
|
|
3029
|
-
const nextStep = parsed[SESSION_FRONT_MATTER.NEXT_STEP];
|
|
3030
|
-
metadata.next_step = typeof nextStep === "string" ? nextStep : DEFAULT_SESSION_METADATA.next_step;
|
|
3031
|
-
const result = parsed[SESSION_FRONT_MATTER.RESULT];
|
|
3032
|
-
metadata.result = typeof result === "string" ? result : DEFAULT_SESSION_METADATA.result;
|
|
3033
|
-
const createdAt = parsed[SESSION_FRONT_MATTER.CREATED_AT];
|
|
3034
|
-
if (typeof createdAt === "string") metadata.created_at = createdAt;
|
|
3035
|
-
const agentSessionId = parsed[SESSION_FRONT_MATTER.AGENT_SESSION_ID];
|
|
3036
|
-
if (typeof agentSessionId === "string") metadata.agent_session_id = agentSessionId;
|
|
3037
|
-
const specs = parsed[SESSION_FRONT_MATTER.SPECS];
|
|
3038
|
-
if (Array.isArray(specs)) {
|
|
3039
|
-
metadata.specs = specs.filter((s) => typeof s === "string");
|
|
3040
|
-
}
|
|
3041
|
-
const files = parsed[SESSION_FRONT_MATTER.FILES];
|
|
3042
|
-
if (Array.isArray(files)) {
|
|
3043
|
-
metadata.files = files.filter((f) => typeof f === "string");
|
|
3044
|
-
}
|
|
3045
|
-
return metadata;
|
|
3046
|
-
} catch {
|
|
3047
|
-
return defaultSessionMetadata();
|
|
2930
|
+
var SessionInvalidJsonHeaderError = class extends SessionError {
|
|
2931
|
+
constructor(reason) {
|
|
2932
|
+
super(`Invalid JSON header for handoff: ${reason}`);
|
|
2933
|
+
this.name = "SessionInvalidJsonHeaderError";
|
|
3048
2934
|
}
|
|
3049
|
-
}
|
|
3050
|
-
function sortSessions(sessions) {
|
|
3051
|
-
return [...sessions].sort((a, b) => {
|
|
3052
|
-
const priorityA = PRIORITY_ORDER[a.metadata.priority];
|
|
3053
|
-
const priorityB = PRIORITY_ORDER[b.metadata.priority];
|
|
3054
|
-
if (priorityA !== priorityB) {
|
|
3055
|
-
return priorityA - priorityB;
|
|
3056
|
-
}
|
|
3057
|
-
const dateA = parseSessionId(a.id);
|
|
3058
|
-
const dateB = parseSessionId(b.id);
|
|
3059
|
-
if (!dateA && !dateB) return 0;
|
|
3060
|
-
if (!dateA) return 1;
|
|
3061
|
-
if (!dateB) return -1;
|
|
3062
|
-
return dateB.getTime() - dateA.getTime();
|
|
3063
|
-
});
|
|
3064
|
-
}
|
|
2935
|
+
};
|
|
3065
2936
|
|
|
3066
2937
|
// src/git/root.ts
|
|
3067
2938
|
import { execa as execa2 } from "execa";
|
|
3068
2939
|
import { dirname, isAbsolute, join as join3, relative as relative2, resolve as resolve3 } from "path";
|
|
3069
2940
|
|
|
3070
|
-
// src/git/errors.ts
|
|
3071
|
-
var SESSION_GIT_CONTEXT_ERROR_MESSAGE = {
|
|
3072
|
-
BRANCH_UNAVAILABLE: "Cannot create a handoff session because the current Git branch could not be detected.",
|
|
3073
|
-
EMPTY_BRANCH: "Cannot create a handoff session because Git returned an empty branch name.",
|
|
3074
|
-
DETACHED_HEAD: "Cannot create a handoff session from a detached HEAD."
|
|
3075
|
-
};
|
|
3076
|
-
var SessionGitContextError = class extends Error {
|
|
3077
|
-
constructor(message = SESSION_GIT_CONTEXT_ERROR_MESSAGE.BRANCH_UNAVAILABLE) {
|
|
3078
|
-
super(message);
|
|
3079
|
-
this.name = "SessionGitContextError";
|
|
3080
|
-
}
|
|
3081
|
-
};
|
|
3082
|
-
var SessionDetachedHeadError = class extends SessionGitContextError {
|
|
3083
|
-
constructor() {
|
|
3084
|
-
super(SESSION_GIT_CONTEXT_ERROR_MESSAGE.DETACHED_HEAD);
|
|
3085
|
-
this.name = "SessionDetachedHeadError";
|
|
3086
|
-
}
|
|
3087
|
-
};
|
|
3088
|
-
|
|
3089
2941
|
// src/config/defaults.ts
|
|
3090
2942
|
var DEFAULT_CONFIG = {
|
|
3091
2943
|
sessions: {
|
|
@@ -3131,14 +2983,22 @@ var GIT_ROOT_COMMAND = {
|
|
|
3131
2983
|
ABBREV_REF: "--abbrev-ref",
|
|
3132
2984
|
GIT_COMMON_DIR: "--git-common-dir",
|
|
3133
2985
|
HEAD: "HEAD",
|
|
3134
|
-
SHOW_TOPLEVEL: "--show-toplevel"
|
|
2986
|
+
SHOW_TOPLEVEL: "--show-toplevel",
|
|
2987
|
+
SYMBOLIC_REF: "symbolic-ref",
|
|
2988
|
+
SHORT: "--short",
|
|
2989
|
+
ORIGIN_HEAD_REF: "refs/remotes/origin/HEAD",
|
|
2990
|
+
STATUS: "status",
|
|
2991
|
+
PORCELAIN: "--porcelain",
|
|
2992
|
+
PATH_FORMAT_ABSOLUTE: "--path-format=absolute"
|
|
3135
2993
|
};
|
|
2994
|
+
var ORIGIN_REF_PREFIX = "origin/";
|
|
3136
2995
|
var GIT_SHOW_TOPLEVEL_ARGS = [
|
|
3137
2996
|
GIT_ROOT_COMMAND.REV_PARSE,
|
|
3138
2997
|
GIT_ROOT_COMMAND.SHOW_TOPLEVEL
|
|
3139
2998
|
];
|
|
3140
2999
|
var GIT_COMMON_DIR_ARGS = [
|
|
3141
3000
|
GIT_ROOT_COMMAND.REV_PARSE,
|
|
3001
|
+
GIT_ROOT_COMMAND.PATH_FORMAT_ABSOLUTE,
|
|
3142
3002
|
GIT_ROOT_COMMAND.GIT_COMMON_DIR
|
|
3143
3003
|
];
|
|
3144
3004
|
var GIT_CURRENT_BRANCH_ARGS = [
|
|
@@ -3146,6 +3006,19 @@ var GIT_CURRENT_BRANCH_ARGS = [
|
|
|
3146
3006
|
GIT_ROOT_COMMAND.ABBREV_REF,
|
|
3147
3007
|
GIT_ROOT_COMMAND.HEAD
|
|
3148
3008
|
];
|
|
3009
|
+
var GIT_HEAD_SHA_ARGS = [
|
|
3010
|
+
GIT_ROOT_COMMAND.REV_PARSE,
|
|
3011
|
+
GIT_ROOT_COMMAND.HEAD
|
|
3012
|
+
];
|
|
3013
|
+
var GIT_ORIGIN_HEAD_REF_ARGS = [
|
|
3014
|
+
GIT_ROOT_COMMAND.SYMBOLIC_REF,
|
|
3015
|
+
GIT_ROOT_COMMAND.SHORT,
|
|
3016
|
+
GIT_ROOT_COMMAND.ORIGIN_HEAD_REF
|
|
3017
|
+
];
|
|
3018
|
+
var GIT_STATUS_PORCELAIN_ARGS = [
|
|
3019
|
+
GIT_ROOT_COMMAND.STATUS,
|
|
3020
|
+
GIT_ROOT_COMMAND.PORCELAIN
|
|
3021
|
+
];
|
|
3149
3022
|
async function detectWorktreeProductRoot(cwd = process.cwd(), deps = defaultDeps) {
|
|
3150
3023
|
try {
|
|
3151
3024
|
const result = await deps.execa(
|
|
@@ -3224,36 +3097,68 @@ function computeRelativeWorktreePath(commonDir, toplevel) {
|
|
|
3224
3097
|
const worktreePath = relative2(commonDirProductRoot, toplevel);
|
|
3225
3098
|
return worktreePath === "" ? "" : worktreePath;
|
|
3226
3099
|
}
|
|
3227
|
-
async function
|
|
3228
|
-
const
|
|
3100
|
+
async function resolveDefaultBranch(cwd = process.cwd(), deps = defaultDeps) {
|
|
3101
|
+
const result = await deps.execa(
|
|
3102
|
+
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
3103
|
+
[...GIT_ORIGIN_HEAD_REF_ARGS],
|
|
3104
|
+
{ cwd, reject: false }
|
|
3105
|
+
);
|
|
3106
|
+
if (result.exitCode !== 0) return null;
|
|
3107
|
+
const ref = extractStdout(result.stdout);
|
|
3108
|
+
if (!ref.startsWith(ORIGIN_REF_PREFIX)) return null;
|
|
3109
|
+
const branch = ref.slice(ORIGIN_REF_PREFIX.length);
|
|
3110
|
+
return branch.length === 0 ? null : branch;
|
|
3111
|
+
}
|
|
3112
|
+
async function getCurrentBranch(cwd = process.cwd(), deps = defaultDeps) {
|
|
3113
|
+
const result = await deps.execa(
|
|
3229
3114
|
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
3230
3115
|
[...GIT_CURRENT_BRANCH_ARGS],
|
|
3231
3116
|
{ cwd, reject: false }
|
|
3232
3117
|
);
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3118
|
+
if (result.exitCode !== 0) return null;
|
|
3119
|
+
const branch = extractStdout(result.stdout);
|
|
3120
|
+
if (branch.length === 0 || branch === GIT_ROOT_COMMAND.HEAD) return null;
|
|
3121
|
+
return branch;
|
|
3122
|
+
}
|
|
3123
|
+
async function getHeadSha(cwd = process.cwd(), deps = defaultDeps) {
|
|
3124
|
+
const result = await deps.execa(
|
|
3125
|
+
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
3126
|
+
[...GIT_HEAD_SHA_ARGS],
|
|
3127
|
+
{ cwd, reject: false }
|
|
3128
|
+
);
|
|
3129
|
+
if (result.exitCode !== 0) return null;
|
|
3130
|
+
const sha = extractStdout(result.stdout);
|
|
3131
|
+
return sha.length === 0 ? null : sha;
|
|
3132
|
+
}
|
|
3133
|
+
async function resolveRefSha(ref, cwd = process.cwd(), deps = defaultDeps) {
|
|
3134
|
+
const result = await deps.execa(
|
|
3244
3135
|
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
3245
|
-
[
|
|
3136
|
+
[GIT_ROOT_COMMAND.REV_PARSE, ref],
|
|
3246
3137
|
{ cwd, reject: false }
|
|
3247
3138
|
);
|
|
3248
|
-
|
|
3139
|
+
if (result.exitCode !== 0) return null;
|
|
3140
|
+
const sha = extractStdout(result.stdout);
|
|
3141
|
+
return sha.length === 0 ? null : sha;
|
|
3142
|
+
}
|
|
3143
|
+
async function isWorkingTreeClean(cwd = process.cwd(), deps = defaultDeps) {
|
|
3144
|
+
const result = await deps.execa(
|
|
3249
3145
|
GIT_ROOT_COMMAND.EXECUTABLE,
|
|
3250
|
-
[...
|
|
3146
|
+
[...GIT_STATUS_PORCELAIN_ARGS],
|
|
3251
3147
|
{ cwd, reject: false }
|
|
3252
3148
|
);
|
|
3149
|
+
if (result.exitCode !== 0) return false;
|
|
3150
|
+
return extractStdout(result.stdout).length === 0;
|
|
3151
|
+
}
|
|
3152
|
+
async function isRootWorktree(cwd = process.cwd(), deps = defaultDeps) {
|
|
3153
|
+
const [toplevelResult, commonDirResult] = await Promise.all([
|
|
3154
|
+
deps.execa(GIT_ROOT_COMMAND.EXECUTABLE, [...GIT_SHOW_TOPLEVEL_ARGS], { cwd, reject: false }),
|
|
3155
|
+
deps.execa(GIT_ROOT_COMMAND.EXECUTABLE, [...GIT_COMMON_DIR_ARGS], { cwd, reject: false })
|
|
3156
|
+
]);
|
|
3157
|
+
if (toplevelResult.exitCode !== 0) return false;
|
|
3158
|
+
if (commonDirResult.exitCode !== 0) return true;
|
|
3253
3159
|
const toplevel = extractStdout(toplevelResult.stdout);
|
|
3254
3160
|
const commonDir = extractStdout(commonDirResult.stdout);
|
|
3255
|
-
|
|
3256
|
-
return { branch, worktree };
|
|
3161
|
+
return computeRelativeWorktreePath(commonDir, toplevel) === "";
|
|
3257
3162
|
}
|
|
3258
3163
|
async function resolveSessionConfig(options = {}) {
|
|
3259
3164
|
const { sessionsDir, cwd, deps } = options;
|
|
@@ -3326,11 +3231,6 @@ async function resolveArchivePaths(sessionId, config) {
|
|
|
3326
3231
|
}
|
|
3327
3232
|
async function archiveSingle(sessionId, config) {
|
|
3328
3233
|
const { source, target } = await resolveArchivePaths(sessionId, config);
|
|
3329
|
-
const content = await readFile3(source, "utf-8");
|
|
3330
|
-
const metadata = parseSessionMetadata(content);
|
|
3331
|
-
if (metadata.result.trim().length === 0) {
|
|
3332
|
-
throw new SessionInvalidResultError(sessionId);
|
|
3333
|
-
}
|
|
3334
3234
|
await mkdir(dirname2(target), { recursive: true });
|
|
3335
3235
|
await rename(source, target);
|
|
3336
3236
|
return `${SESSION_ARCHIVE_OUTPUT.ARCHIVED}: ${sessionId}
|
|
@@ -3355,63 +3255,197 @@ function resolveDeletePath(sessionId, existingPaths) {
|
|
|
3355
3255
|
|
|
3356
3256
|
// src/domains/session/show.ts
|
|
3357
3257
|
import { join as join5 } from "path";
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
doingDir: join5(sessionsBaseDir, statusDirs.doing),
|
|
3376
|
-
archiveDir: join5(sessionsBaseDir, statusDirs.archive)
|
|
3377
|
-
};
|
|
3378
|
-
var SEARCH_ORDER = [...SESSION_STATUSES];
|
|
3379
|
-
function resolveSessionPaths(id, config = DEFAULT_SESSION_CONFIG) {
|
|
3380
|
-
const filename = `${id}.md`;
|
|
3381
|
-
return [
|
|
3382
|
-
`${config.todoDir}/${filename}`,
|
|
3383
|
-
`${config.doingDir}/${filename}`,
|
|
3384
|
-
`${config.archiveDir}/${filename}`
|
|
3385
|
-
];
|
|
3258
|
+
|
|
3259
|
+
// src/domains/session/list.ts
|
|
3260
|
+
import { parse as parseYaml2 } from "yaml";
|
|
3261
|
+
|
|
3262
|
+
// src/domains/session/timestamp.ts
|
|
3263
|
+
var SESSION_ID_PATTERN = /^(\d{4})-(\d{2})-(\d{2})_(\d{2})-(\d{2})-(\d{2})$/;
|
|
3264
|
+
var SESSION_ID_SEPARATOR = "_";
|
|
3265
|
+
function generateSessionId(options = {}) {
|
|
3266
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
3267
|
+
const date = now();
|
|
3268
|
+
const year = date.getFullYear();
|
|
3269
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
3270
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
3271
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
3272
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
3273
|
+
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
3274
|
+
return `${year}-${month}-${day}${SESSION_ID_SEPARATOR}${hours}-${minutes}-${seconds}`;
|
|
3386
3275
|
}
|
|
3387
|
-
function
|
|
3388
|
-
const
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3276
|
+
function parseSessionId(id) {
|
|
3277
|
+
const match = SESSION_ID_PATTERN.exec(id);
|
|
3278
|
+
if (!match) {
|
|
3279
|
+
return null;
|
|
3280
|
+
}
|
|
3281
|
+
const [, yearStr, monthStr, dayStr, hoursStr, minutesStr, secondsStr] = match;
|
|
3282
|
+
const year = parseInt(yearStr, 10);
|
|
3283
|
+
const month = parseInt(monthStr, 10) - 1;
|
|
3284
|
+
const day = parseInt(dayStr, 10);
|
|
3285
|
+
const hours = parseInt(hoursStr, 10);
|
|
3286
|
+
const minutes = parseInt(minutesStr, 10);
|
|
3287
|
+
const seconds = parseInt(secondsStr, 10);
|
|
3288
|
+
if (month < 0 || month > 11) return null;
|
|
3289
|
+
if (day < 1 || day > 31) return null;
|
|
3290
|
+
if (hours < 0 || hours > 23) return null;
|
|
3291
|
+
if (minutes < 0 || minutes > 59) return null;
|
|
3292
|
+
if (seconds < 0 || seconds > 59) return null;
|
|
3293
|
+
return new Date(year, month, day, hours, minutes, seconds);
|
|
3404
3294
|
}
|
|
3405
3295
|
|
|
3406
|
-
// src/
|
|
3407
|
-
var
|
|
3408
|
-
|
|
3296
|
+
// src/domains/session/types.ts
|
|
3297
|
+
var SESSION_PRIORITY = {
|
|
3298
|
+
HIGH: "high",
|
|
3299
|
+
MEDIUM: "medium",
|
|
3300
|
+
LOW: "low"
|
|
3409
3301
|
};
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3302
|
+
var SESSION_STATUSES = ["todo", "doing", "archive"];
|
|
3303
|
+
var DEFAULT_LIST_STATUSES = ["doing", "todo"];
|
|
3304
|
+
var PRIORITY_ORDER = {
|
|
3305
|
+
[SESSION_PRIORITY.HIGH]: 0,
|
|
3306
|
+
[SESSION_PRIORITY.MEDIUM]: 1,
|
|
3307
|
+
[SESSION_PRIORITY.LOW]: 2
|
|
3308
|
+
};
|
|
3309
|
+
var DEFAULT_PRIORITY = SESSION_PRIORITY.MEDIUM;
|
|
3310
|
+
var SESSION_FRONT_MATTER = {
|
|
3311
|
+
PRIORITY: "priority",
|
|
3312
|
+
GIT_REF: "git_ref",
|
|
3313
|
+
GOAL: "goal",
|
|
3314
|
+
NEXT_STEP: "next_step",
|
|
3315
|
+
CREATED_AT: "created_at",
|
|
3316
|
+
AGENT_SESSION_ID: "agent_session_id",
|
|
3317
|
+
SPECS: "specs",
|
|
3318
|
+
FILES: "files"
|
|
3319
|
+
};
|
|
3320
|
+
|
|
3321
|
+
// src/domains/session/list.ts
|
|
3322
|
+
var FRONT_MATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n(?:---|\.\.\.)\r?\n?/;
|
|
3323
|
+
var SESSION_PRIORITY_VALUES = Object.values(SESSION_PRIORITY);
|
|
3324
|
+
var DEFAULT_SESSION_METADATA = {
|
|
3325
|
+
priority: DEFAULT_PRIORITY,
|
|
3326
|
+
git_ref: "",
|
|
3327
|
+
goal: "",
|
|
3328
|
+
next_step: "",
|
|
3329
|
+
specs: [],
|
|
3330
|
+
files: []
|
|
3331
|
+
};
|
|
3332
|
+
function defaultSessionMetadata() {
|
|
3333
|
+
return {
|
|
3334
|
+
...DEFAULT_SESSION_METADATA,
|
|
3335
|
+
specs: [],
|
|
3336
|
+
files: []
|
|
3337
|
+
};
|
|
3338
|
+
}
|
|
3339
|
+
function isValidPriority(value) {
|
|
3340
|
+
return typeof value === "string" && SESSION_PRIORITY_VALUES.some((priority) => priority === value);
|
|
3341
|
+
}
|
|
3342
|
+
function parseSessionMetadata(content) {
|
|
3343
|
+
const match = FRONT_MATTER_PATTERN.exec(content);
|
|
3344
|
+
if (!match) {
|
|
3345
|
+
return defaultSessionMetadata();
|
|
3346
|
+
}
|
|
3347
|
+
try {
|
|
3348
|
+
const parsed = parseYaml2(match[1]);
|
|
3349
|
+
if (!parsed || typeof parsed !== "object") {
|
|
3350
|
+
return defaultSessionMetadata();
|
|
3351
|
+
}
|
|
3352
|
+
const rawPriority = parsed[SESSION_FRONT_MATTER.PRIORITY];
|
|
3353
|
+
const priority = isValidPriority(rawPriority) ? rawPriority : DEFAULT_PRIORITY;
|
|
3354
|
+
const metadata = {
|
|
3355
|
+
...defaultSessionMetadata(),
|
|
3356
|
+
priority
|
|
3357
|
+
};
|
|
3358
|
+
const gitRef = parsed[SESSION_FRONT_MATTER.GIT_REF];
|
|
3359
|
+
metadata.git_ref = typeof gitRef === "string" ? gitRef : DEFAULT_SESSION_METADATA.git_ref;
|
|
3360
|
+
const goal = parsed[SESSION_FRONT_MATTER.GOAL];
|
|
3361
|
+
metadata.goal = typeof goal === "string" ? goal : DEFAULT_SESSION_METADATA.goal;
|
|
3362
|
+
const nextStep = parsed[SESSION_FRONT_MATTER.NEXT_STEP];
|
|
3363
|
+
metadata.next_step = typeof nextStep === "string" ? nextStep : DEFAULT_SESSION_METADATA.next_step;
|
|
3364
|
+
const createdAt = parsed[SESSION_FRONT_MATTER.CREATED_AT];
|
|
3365
|
+
if (typeof createdAt === "string") metadata.created_at = createdAt;
|
|
3366
|
+
const agentSessionId = parsed[SESSION_FRONT_MATTER.AGENT_SESSION_ID];
|
|
3367
|
+
if (typeof agentSessionId === "string") metadata.agent_session_id = agentSessionId;
|
|
3368
|
+
const specs = parsed[SESSION_FRONT_MATTER.SPECS];
|
|
3369
|
+
if (Array.isArray(specs)) {
|
|
3370
|
+
metadata.specs = specs.filter((s) => typeof s === "string");
|
|
3371
|
+
}
|
|
3372
|
+
const files = parsed[SESSION_FRONT_MATTER.FILES];
|
|
3373
|
+
if (Array.isArray(files)) {
|
|
3374
|
+
metadata.files = files.filter((f) => typeof f === "string");
|
|
3375
|
+
}
|
|
3376
|
+
return metadata;
|
|
3377
|
+
} catch {
|
|
3378
|
+
return defaultSessionMetadata();
|
|
3379
|
+
}
|
|
3380
|
+
}
|
|
3381
|
+
function sortSessions(sessions) {
|
|
3382
|
+
return [...sessions].sort((a, b) => {
|
|
3383
|
+
const priorityA = PRIORITY_ORDER[a.metadata.priority];
|
|
3384
|
+
const priorityB = PRIORITY_ORDER[b.metadata.priority];
|
|
3385
|
+
if (priorityA !== priorityB) {
|
|
3386
|
+
return priorityA - priorityB;
|
|
3387
|
+
}
|
|
3388
|
+
const dateA = parseSessionId(a.id);
|
|
3389
|
+
const dateB = parseSessionId(b.id);
|
|
3390
|
+
if (!dateA && !dateB) return 0;
|
|
3391
|
+
if (!dateA) return 1;
|
|
3392
|
+
if (!dateB) return -1;
|
|
3393
|
+
return dateB.getTime() - dateA.getTime();
|
|
3394
|
+
});
|
|
3395
|
+
}
|
|
3396
|
+
|
|
3397
|
+
// src/domains/session/show.ts
|
|
3398
|
+
var SESSION_SHOW_LABEL = {
|
|
3399
|
+
STATUS: "Status",
|
|
3400
|
+
PRIORITY: "Priority",
|
|
3401
|
+
GIT_REF: "Git ref",
|
|
3402
|
+
GOAL: "Goal",
|
|
3403
|
+
NEXT_STEP: "Next step",
|
|
3404
|
+
CREATED: "Created",
|
|
3405
|
+
AGENT_SESSION: "Agent session"
|
|
3406
|
+
};
|
|
3407
|
+
var SESSION_SHOW_SEPARATOR_CHAR = "\u2500";
|
|
3408
|
+
var SESSION_SHOW_SEPARATOR_WIDTH = 40;
|
|
3409
|
+
var { dir: sessionsBaseDir, statusDirs } = DEFAULT_CONFIG.sessions;
|
|
3410
|
+
var DEFAULT_SESSION_CONFIG = {
|
|
3411
|
+
todoDir: join5(sessionsBaseDir, statusDirs.todo),
|
|
3412
|
+
doingDir: join5(sessionsBaseDir, statusDirs.doing),
|
|
3413
|
+
archiveDir: join5(sessionsBaseDir, statusDirs.archive)
|
|
3414
|
+
};
|
|
3415
|
+
var SEARCH_ORDER = [...SESSION_STATUSES];
|
|
3416
|
+
function resolveSessionPaths(id, config = DEFAULT_SESSION_CONFIG) {
|
|
3417
|
+
const filename = `${id}.md`;
|
|
3418
|
+
return [
|
|
3419
|
+
`${config.todoDir}/${filename}`,
|
|
3420
|
+
`${config.doingDir}/${filename}`,
|
|
3421
|
+
`${config.archiveDir}/${filename}`
|
|
3422
|
+
];
|
|
3423
|
+
}
|
|
3424
|
+
function formatShowOutput(content, options) {
|
|
3425
|
+
const metadata = parseSessionMetadata(content);
|
|
3426
|
+
const headerLines = [
|
|
3427
|
+
`${SESSION_SHOW_LABEL.STATUS}: ${options.status}`,
|
|
3428
|
+
`${SESSION_SHOW_LABEL.PRIORITY}: ${metadata.priority}`,
|
|
3429
|
+
`${SESSION_SHOW_LABEL.GIT_REF}: ${metadata.git_ref}`,
|
|
3430
|
+
`${SESSION_SHOW_LABEL.GOAL}: ${metadata.goal}`,
|
|
3431
|
+
`${SESSION_SHOW_LABEL.NEXT_STEP}: ${metadata.next_step}`,
|
|
3432
|
+
`${SESSION_SHOW_LABEL.CREATED}: ${metadata.created_at ?? ""}`,
|
|
3433
|
+
`${SESSION_SHOW_LABEL.AGENT_SESSION}: ${metadata.agent_session_id ?? ""}`
|
|
3434
|
+
];
|
|
3435
|
+
const header = headerLines.join("\n");
|
|
3436
|
+
const separator = "\n" + SESSION_SHOW_SEPARATOR_CHAR.repeat(SESSION_SHOW_SEPARATOR_WIDTH) + "\n\n";
|
|
3437
|
+
return header + separator + content;
|
|
3438
|
+
}
|
|
3439
|
+
|
|
3440
|
+
// src/commands/session/delete.ts
|
|
3441
|
+
var SESSION_DELETE_OUTPUT = {
|
|
3442
|
+
DELETED: "Deleted session"
|
|
3443
|
+
};
|
|
3444
|
+
async function findExistingPaths(paths) {
|
|
3445
|
+
const existing = [];
|
|
3446
|
+
for (const path6 of paths) {
|
|
3447
|
+
try {
|
|
3448
|
+
const stats = await stat2(path6);
|
|
3415
3449
|
if (stats.isFile()) {
|
|
3416
3450
|
existing.push(path6);
|
|
3417
3451
|
}
|
|
@@ -3435,49 +3469,186 @@ async function deleteCommand(options) {
|
|
|
3435
3469
|
// src/commands/session/handoff.ts
|
|
3436
3470
|
import { mkdir as mkdir2, writeFile } from "fs/promises";
|
|
3437
3471
|
import { join as join6, resolve as resolve4 } from "path";
|
|
3472
|
+
import { stringify as stringifyYaml3 } from "yaml";
|
|
3438
3473
|
|
|
3439
3474
|
// src/domains/session/create.ts
|
|
3440
|
-
import {
|
|
3475
|
+
import { stringify as stringifyYaml2 } from "yaml";
|
|
3441
3476
|
var SESSION_FRONT_MATTER_DELIMITER = "---";
|
|
3442
3477
|
var SESSION_FRONT_MATTER_OPEN = `${SESSION_FRONT_MATTER_DELIMITER}
|
|
3443
3478
|
`;
|
|
3444
3479
|
var SESSION_FRONT_MATTER_CLOSE = `
|
|
3445
3480
|
${SESSION_FRONT_MATTER_DELIMITER}
|
|
3446
3481
|
`;
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
if (
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3482
|
+
|
|
3483
|
+
// src/domains/session/handoff-base.ts
|
|
3484
|
+
function resolveHandoffGitRef(facts) {
|
|
3485
|
+
if (facts.isRootWorktree) {
|
|
3486
|
+
if (facts.branch !== null) return facts.branch;
|
|
3487
|
+
if (facts.headSha !== null) return facts.headSha;
|
|
3488
|
+
throw new SessionHandoffBaseError();
|
|
3489
|
+
}
|
|
3490
|
+
if (facts.branch !== null) {
|
|
3491
|
+
throw new SessionHandoffBaseError();
|
|
3492
|
+
}
|
|
3493
|
+
if (!facts.isClean) {
|
|
3494
|
+
throw new SessionHandoffBaseError();
|
|
3495
|
+
}
|
|
3496
|
+
if (facts.headSha === null || facts.defaultTipSha === null || facts.headSha !== facts.defaultTipSha) {
|
|
3497
|
+
throw new SessionHandoffBaseError();
|
|
3498
|
+
}
|
|
3499
|
+
return facts.headSha;
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3502
|
+
// src/domains/session/parse-handoff-input.ts
|
|
3503
|
+
var LEGACY_FRONTMATTER_PREFIX = /^---\r?\n/;
|
|
3504
|
+
var JSON_OBJECT_OPEN_CHAR = "{";
|
|
3505
|
+
var CARRIAGE_RETURN_CHAR_CODE = "\r".charCodeAt(0);
|
|
3506
|
+
var NEWLINE_CHAR_CODE = "\n".charCodeAt(0);
|
|
3507
|
+
var BACKSLASH_CHAR_CODE = "\\".charCodeAt(0);
|
|
3508
|
+
var DOUBLE_QUOTE_CHAR_CODE = '"'.charCodeAt(0);
|
|
3509
|
+
var OPEN_BRACE_CHAR_CODE = "{".charCodeAt(0);
|
|
3510
|
+
var CLOSE_BRACE_CHAR_CODE = "}".charCodeAt(0);
|
|
3511
|
+
var UNBALANCED_HEADER_END = -1;
|
|
3512
|
+
var LF_SEPARATOR_LENGTH = 1;
|
|
3513
|
+
var CRLF_SEPARATOR_LENGTH = 2;
|
|
3514
|
+
var SESSION_PRIORITY_VALUES2 = new Set(Object.values(SESSION_PRIORITY));
|
|
3515
|
+
function parseHandoffInput(stdin) {
|
|
3516
|
+
if (LEGACY_FRONTMATTER_PREFIX.test(stdin)) {
|
|
3517
|
+
throw new SessionLegacyFrontmatterInputError();
|
|
3518
|
+
}
|
|
3519
|
+
if (!stdin.startsWith(JSON_OBJECT_OPEN_CHAR)) {
|
|
3520
|
+
throw new SessionInvalidJsonHeaderError(
|
|
3521
|
+
"stdin must begin with a JSON object opening with '{'"
|
|
3522
|
+
);
|
|
3460
3523
|
}
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3524
|
+
const headerEnd = findJsonObjectEnd(stdin);
|
|
3525
|
+
if (headerEnd === UNBALANCED_HEADER_END) {
|
|
3526
|
+
throw new SessionInvalidJsonHeaderError(
|
|
3527
|
+
"unbalanced JSON object \u2014 opening brace has no matching close"
|
|
3528
|
+
);
|
|
3529
|
+
}
|
|
3530
|
+
const headerText = stdin.slice(0, headerEnd + 1);
|
|
3531
|
+
let parsed;
|
|
3532
|
+
try {
|
|
3533
|
+
parsed = JSON.parse(headerText);
|
|
3534
|
+
} catch (err) {
|
|
3535
|
+
const detail = err instanceof Error ? err.message : "JSON.parse failed";
|
|
3536
|
+
throw new SessionInvalidJsonHeaderError(detail);
|
|
3537
|
+
}
|
|
3538
|
+
const header = validateHandoffHeader(parsed);
|
|
3539
|
+
const bodyStart = consumeOptionalBodySeparator(stdin, headerEnd + 1);
|
|
3540
|
+
return { header, body: stdin.slice(bodyStart) };
|
|
3541
|
+
}
|
|
3542
|
+
function consumeOptionalBodySeparator(stdin, bodyStart) {
|
|
3543
|
+
if (stdin.charCodeAt(bodyStart) === CARRIAGE_RETURN_CHAR_CODE && stdin.charCodeAt(bodyStart + LF_SEPARATOR_LENGTH) === NEWLINE_CHAR_CODE) {
|
|
3544
|
+
return bodyStart + CRLF_SEPARATOR_LENGTH;
|
|
3545
|
+
}
|
|
3546
|
+
if (stdin.charCodeAt(bodyStart) === NEWLINE_CHAR_CODE) {
|
|
3547
|
+
return bodyStart + LF_SEPARATOR_LENGTH;
|
|
3548
|
+
}
|
|
3549
|
+
return bodyStart;
|
|
3550
|
+
}
|
|
3551
|
+
function findJsonObjectEnd(stdin) {
|
|
3552
|
+
let depth = 0;
|
|
3553
|
+
let inString = false;
|
|
3554
|
+
for (let i = 0; i < stdin.length; i++) {
|
|
3555
|
+
const code = stdin.charCodeAt(i);
|
|
3556
|
+
if (inString) {
|
|
3557
|
+
if (code === BACKSLASH_CHAR_CODE) {
|
|
3558
|
+
i += 1;
|
|
3559
|
+
continue;
|
|
3560
|
+
}
|
|
3561
|
+
if (code === DOUBLE_QUOTE_CHAR_CODE) {
|
|
3562
|
+
inString = false;
|
|
3563
|
+
}
|
|
3564
|
+
continue;
|
|
3565
|
+
}
|
|
3566
|
+
if (code === DOUBLE_QUOTE_CHAR_CODE) {
|
|
3567
|
+
inString = true;
|
|
3568
|
+
continue;
|
|
3569
|
+
}
|
|
3570
|
+
if (code === OPEN_BRACE_CHAR_CODE) {
|
|
3571
|
+
depth += 1;
|
|
3572
|
+
continue;
|
|
3573
|
+
}
|
|
3574
|
+
if (code === CLOSE_BRACE_CHAR_CODE) {
|
|
3575
|
+
depth -= 1;
|
|
3576
|
+
if (depth === 0) return i;
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3579
|
+
return UNBALANCED_HEADER_END;
|
|
3464
3580
|
}
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
if (!content || content.trim().length === 0) {
|
|
3469
|
-
throw new SessionInvalidContentError("Session content cannot be empty");
|
|
3581
|
+
function validateHandoffHeader(parsed) {
|
|
3582
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3583
|
+
throw new SessionInvalidJsonHeaderError("header must be a JSON object");
|
|
3470
3584
|
}
|
|
3471
|
-
|
|
3585
|
+
const obj = parsed;
|
|
3586
|
+
const priority = ensurePriorityOrDefault(obj[SESSION_FRONT_MATTER.PRIORITY]);
|
|
3587
|
+
const goal = ensureStringOrEmpty(obj[SESSION_FRONT_MATTER.GOAL], SESSION_FRONT_MATTER.GOAL);
|
|
3588
|
+
const nextStep = ensureStringOrEmpty(obj[SESSION_FRONT_MATTER.NEXT_STEP], SESSION_FRONT_MATTER.NEXT_STEP);
|
|
3589
|
+
const specs = ensureStringArrayOrDefault(obj[SESSION_FRONT_MATTER.SPECS], SESSION_FRONT_MATTER.SPECS);
|
|
3590
|
+
const files = ensureStringArrayOrDefault(obj[SESSION_FRONT_MATTER.FILES], SESSION_FRONT_MATTER.FILES);
|
|
3591
|
+
return {
|
|
3592
|
+
priority,
|
|
3593
|
+
goal,
|
|
3594
|
+
next_step: nextStep,
|
|
3595
|
+
specs,
|
|
3596
|
+
files
|
|
3597
|
+
};
|
|
3472
3598
|
}
|
|
3473
|
-
function
|
|
3474
|
-
|
|
3475
|
-
if (
|
|
3476
|
-
throw new
|
|
3599
|
+
function ensurePriorityOrDefault(value) {
|
|
3600
|
+
if (value === void 0) return DEFAULT_PRIORITY;
|
|
3601
|
+
if (typeof value !== "string" || !SESSION_PRIORITY_VALUES2.has(value)) {
|
|
3602
|
+
throw new SessionInvalidJsonHeaderError(
|
|
3603
|
+
`${SESSION_FRONT_MATTER.PRIORITY} must be one of ${[...SESSION_PRIORITY_VALUES2].join(", ")}`
|
|
3604
|
+
);
|
|
3477
3605
|
}
|
|
3478
|
-
|
|
3479
|
-
|
|
3606
|
+
return value;
|
|
3607
|
+
}
|
|
3608
|
+
function ensureStringOrEmpty(value, fieldName) {
|
|
3609
|
+
if (value === void 0) return "";
|
|
3610
|
+
if (typeof value !== "string") {
|
|
3611
|
+
throw new SessionInvalidJsonHeaderError(`${fieldName} must be a string`);
|
|
3612
|
+
}
|
|
3613
|
+
return value;
|
|
3614
|
+
}
|
|
3615
|
+
function ensureStringArrayOrDefault(value, fieldName) {
|
|
3616
|
+
if (value === void 0) return [];
|
|
3617
|
+
if (!Array.isArray(value)) {
|
|
3618
|
+
throw new SessionInvalidJsonHeaderError(`${fieldName} must be an array of strings`);
|
|
3619
|
+
}
|
|
3620
|
+
for (const item of value) {
|
|
3621
|
+
if (typeof item !== "string") {
|
|
3622
|
+
throw new SessionInvalidJsonHeaderError(`${fieldName} must be an array of strings`);
|
|
3623
|
+
}
|
|
3480
3624
|
}
|
|
3625
|
+
return value;
|
|
3626
|
+
}
|
|
3627
|
+
|
|
3628
|
+
// src/commands/session/handoff.ts
|
|
3629
|
+
async function resolveSessionGitRef(cwd, deps) {
|
|
3630
|
+
const [isRoot, branch, headSha] = await Promise.all([
|
|
3631
|
+
isRootWorktree(cwd, deps),
|
|
3632
|
+
getCurrentBranch(cwd, deps),
|
|
3633
|
+
getHeadSha(cwd, deps)
|
|
3634
|
+
]);
|
|
3635
|
+
let isClean = false;
|
|
3636
|
+
let defaultTipSha = null;
|
|
3637
|
+
if (!isRoot && branch === null) {
|
|
3638
|
+
const [clean, defaultBranch] = await Promise.all([
|
|
3639
|
+
isWorkingTreeClean(cwd, deps),
|
|
3640
|
+
resolveDefaultBranch(cwd, deps)
|
|
3641
|
+
]);
|
|
3642
|
+
isClean = clean;
|
|
3643
|
+
defaultTipSha = defaultBranch === null ? null : await resolveRefSha(`${ORIGIN_REF_PREFIX}${defaultBranch}`, cwd, deps);
|
|
3644
|
+
}
|
|
3645
|
+
return resolveHandoffGitRef({
|
|
3646
|
+
isRootWorktree: isRoot,
|
|
3647
|
+
branch,
|
|
3648
|
+
headSha,
|
|
3649
|
+
isClean,
|
|
3650
|
+
defaultTipSha
|
|
3651
|
+
});
|
|
3481
3652
|
}
|
|
3482
3653
|
async function handoffCommand(options) {
|
|
3483
3654
|
const { config, warning } = await resolveSessionConfig({
|
|
@@ -3485,32 +3656,47 @@ async function handoffCommand(options) {
|
|
|
3485
3656
|
cwd: options.cwd,
|
|
3486
3657
|
deps: options.deps
|
|
3487
3658
|
});
|
|
3488
|
-
const
|
|
3489
|
-
|
|
3659
|
+
const content = options.content;
|
|
3660
|
+
if (content === void 0 || content.trim().length === 0) {
|
|
3661
|
+
throw new SessionInvalidContentError("Session content cannot be empty");
|
|
3662
|
+
}
|
|
3663
|
+
const { header, body } = parseHandoffInput(content);
|
|
3664
|
+
if (header.goal.length === 0) {
|
|
3665
|
+
throw new SessionInvalidGoalError();
|
|
3666
|
+
}
|
|
3667
|
+
if (header.next_step.length === 0) {
|
|
3668
|
+
throw new SessionInvalidNextStepError();
|
|
3669
|
+
}
|
|
3670
|
+
const gitRef = await resolveSessionGitRef(options.cwd, options.deps);
|
|
3490
3671
|
const sessionId = generateSessionId();
|
|
3491
3672
|
const agentSessionId = process.env.CLAUDE_SESSION_ID ?? process.env.CODEX_THREAD_ID;
|
|
3492
|
-
const
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3673
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3674
|
+
const frontMatterObject = {
|
|
3675
|
+
[SESSION_FRONT_MATTER.PRIORITY]: header.priority,
|
|
3676
|
+
[SESSION_FRONT_MATTER.CREATED_AT]: createdAt,
|
|
3677
|
+
[SESSION_FRONT_MATTER.GIT_REF]: gitRef,
|
|
3678
|
+
[SESSION_FRONT_MATTER.GOAL]: header.goal,
|
|
3679
|
+
[SESSION_FRONT_MATTER.NEXT_STEP]: header.next_step,
|
|
3680
|
+
[SESSION_FRONT_MATTER.SPECS]: [...header.specs],
|
|
3681
|
+
[SESSION_FRONT_MATTER.FILES]: [...header.files]
|
|
3682
|
+
};
|
|
3683
|
+
if (agentSessionId !== void 0) {
|
|
3684
|
+
frontMatterObject[SESSION_FRONT_MATTER.AGENT_SESSION_ID] = agentSessionId;
|
|
3685
|
+
}
|
|
3686
|
+
const yaml = stringifyYaml3(frontMatterObject, { defaultStringType: "QUOTE_DOUBLE" }).trimEnd();
|
|
3687
|
+
const fullContent = `${SESSION_FRONT_MATTER_OPEN}${yaml}${SESSION_FRONT_MATTER_CLOSE}${body}`;
|
|
3499
3688
|
const filename = `${sessionId}.md`;
|
|
3500
3689
|
const sessionPath = join6(config.todoDir, filename);
|
|
3501
3690
|
const absolutePath = resolve4(sessionPath);
|
|
3502
3691
|
await mkdir2(config.todoDir, { recursive: true });
|
|
3503
3692
|
await writeFile(sessionPath, fullContent, "utf-8");
|
|
3504
|
-
|
|
3505
|
-
process.stderr.write(`${warning}
|
|
3506
|
-
`);
|
|
3507
|
-
}
|
|
3508
|
-
return `Created handoff session <HANDOFF_ID>${sessionId}</HANDOFF_ID>
|
|
3693
|
+
const output = `Created handoff session <HANDOFF_ID>${sessionId}</HANDOFF_ID>
|
|
3509
3694
|
<SESSION_FILE>${absolutePath}</SESSION_FILE>`;
|
|
3695
|
+
return warning ? { output, warning } : { output };
|
|
3510
3696
|
}
|
|
3511
3697
|
|
|
3512
3698
|
// src/commands/session/list.ts
|
|
3513
|
-
import { readdir, readFile as
|
|
3699
|
+
import { readdir, readFile as readFile3 } from "fs/promises";
|
|
3514
3700
|
import { join as join7 } from "path";
|
|
3515
3701
|
var SESSION_LIST_FORMAT = {
|
|
3516
3702
|
TEXT: "text",
|
|
@@ -3526,7 +3712,7 @@ async function loadSessionsFromDir(dir, status) {
|
|
|
3526
3712
|
if (!file.endsWith(".md")) continue;
|
|
3527
3713
|
const id = file.replace(".md", "");
|
|
3528
3714
|
const filePath = join7(dir, file);
|
|
3529
|
-
const content = await
|
|
3715
|
+
const content = await readFile3(filePath, "utf-8");
|
|
3530
3716
|
const metadata = parseSessionMetadata(content);
|
|
3531
3717
|
sessions.push({
|
|
3532
3718
|
id,
|
|
@@ -3588,7 +3774,7 @@ async function listCommand(options) {
|
|
|
3588
3774
|
}
|
|
3589
3775
|
|
|
3590
3776
|
// src/commands/session/pickup.ts
|
|
3591
|
-
import { mkdir as mkdir3, readdir as readdir2, readFile as
|
|
3777
|
+
import { mkdir as mkdir3, readdir as readdir2, readFile as readFile4, rename as rename2 } from "fs/promises";
|
|
3592
3778
|
import { join as join8 } from "path";
|
|
3593
3779
|
|
|
3594
3780
|
// src/domains/session/pickup.ts
|
|
@@ -3635,7 +3821,7 @@ async function loadTodoSessions(config) {
|
|
|
3635
3821
|
if (!file.endsWith(".md")) continue;
|
|
3636
3822
|
const id = file.replace(".md", "");
|
|
3637
3823
|
const filePath = join8(config.todoDir, file);
|
|
3638
|
-
const content = await
|
|
3824
|
+
const content = await readFile4(filePath, "utf-8");
|
|
3639
3825
|
const metadata = parseSessionMetadata(content);
|
|
3640
3826
|
sessions.push({
|
|
3641
3827
|
id,
|
|
@@ -3652,21 +3838,7 @@ async function loadTodoSessions(config) {
|
|
|
3652
3838
|
throw error;
|
|
3653
3839
|
}
|
|
3654
3840
|
}
|
|
3655
|
-
async function
|
|
3656
|
-
const { config } = await resolveSessionConfig({ sessionsDir: options.sessionsDir });
|
|
3657
|
-
let sessionId;
|
|
3658
|
-
if (options.auto) {
|
|
3659
|
-
const sessions = await loadTodoSessions(config);
|
|
3660
|
-
const selected = selectBestSession(sessions);
|
|
3661
|
-
if (!selected) {
|
|
3662
|
-
throw new NoSessionsAvailableError();
|
|
3663
|
-
}
|
|
3664
|
-
sessionId = selected.id;
|
|
3665
|
-
} else if (options.sessionId) {
|
|
3666
|
-
sessionId = options.sessionId;
|
|
3667
|
-
} else {
|
|
3668
|
-
throw new Error("Either session ID or --auto flag is required");
|
|
3669
|
-
}
|
|
3841
|
+
async function pickupSingle(sessionId, config) {
|
|
3670
3842
|
const paths = buildClaimPaths(sessionId, config);
|
|
3671
3843
|
await mkdir3(config.doingDir, { recursive: true });
|
|
3672
3844
|
try {
|
|
@@ -3674,15 +3846,36 @@ async function pickupCommand(options) {
|
|
|
3674
3846
|
} catch (error) {
|
|
3675
3847
|
throw classifyClaimError(error, sessionId);
|
|
3676
3848
|
}
|
|
3677
|
-
const content = await
|
|
3849
|
+
const content = await readFile4(paths.target, "utf-8");
|
|
3678
3850
|
const output = formatShowOutput(content, { status: PICKUP_TARGET_STATUS });
|
|
3679
3851
|
return `Claimed session <PICKUP_ID>${sessionId}</PICKUP_ID>
|
|
3680
3852
|
|
|
3681
3853
|
${output}`;
|
|
3682
3854
|
}
|
|
3855
|
+
async function pickupCommand(options) {
|
|
3856
|
+
const { config } = await resolveSessionConfig({ sessionsDir: options.sessionsDir });
|
|
3857
|
+
if (options.auto) {
|
|
3858
|
+
if (options.sessionIds.length > 0) {
|
|
3859
|
+
throw new Error("Session IDs cannot be combined with --auto");
|
|
3860
|
+
}
|
|
3861
|
+
const sessions = await loadTodoSessions(config);
|
|
3862
|
+
const selected = selectBestSession(sessions);
|
|
3863
|
+
if (!selected) {
|
|
3864
|
+
throw new NoSessionsAvailableError();
|
|
3865
|
+
}
|
|
3866
|
+
return pickupSingle(selected.id, config);
|
|
3867
|
+
}
|
|
3868
|
+
if (options.sessionIds.length === 0) {
|
|
3869
|
+
throw new Error("Either session ID or --auto flag is required");
|
|
3870
|
+
}
|
|
3871
|
+
if (options.sessionIds.length === 1) {
|
|
3872
|
+
return pickupSingle(options.sessionIds[0], config);
|
|
3873
|
+
}
|
|
3874
|
+
return processBatch(options.sessionIds, (id) => pickupSingle(id, config));
|
|
3875
|
+
}
|
|
3683
3876
|
|
|
3684
3877
|
// src/commands/session/prune.ts
|
|
3685
|
-
import { readdir as readdir3, readFile as
|
|
3878
|
+
import { readdir as readdir3, readFile as readFile5, unlink as unlink2 } from "fs/promises";
|
|
3686
3879
|
import { join as join9 } from "path";
|
|
3687
3880
|
|
|
3688
3881
|
// src/domains/session/prune.ts
|
|
@@ -3733,7 +3926,7 @@ async function loadArchiveSessions(config) {
|
|
|
3733
3926
|
if (!file.endsWith(".md")) continue;
|
|
3734
3927
|
const id = file.replace(".md", "");
|
|
3735
3928
|
const filePath = join9(config.archiveDir, file);
|
|
3736
|
-
const content = await
|
|
3929
|
+
const content = await readFile5(filePath, "utf-8");
|
|
3737
3930
|
const metadata = parseSessionMetadata(content);
|
|
3738
3931
|
sessions.push({
|
|
3739
3932
|
id,
|
|
@@ -3850,7 +4043,7 @@ async function releaseCommand(options) {
|
|
|
3850
4043
|
}
|
|
3851
4044
|
|
|
3852
4045
|
// src/commands/session/show.ts
|
|
3853
|
-
import { readFile as
|
|
4046
|
+
import { readFile as readFile6, stat as stat3 } from "fs/promises";
|
|
3854
4047
|
async function findExistingPath(paths, _config) {
|
|
3855
4048
|
for (let i = 0; i < paths.length; i++) {
|
|
3856
4049
|
const filePath = paths[i];
|
|
@@ -3870,7 +4063,7 @@ async function showSingle(sessionId, config) {
|
|
|
3870
4063
|
if (!found) {
|
|
3871
4064
|
throw new SessionNotFoundError(sessionId);
|
|
3872
4065
|
}
|
|
3873
|
-
const content = await
|
|
4066
|
+
const content = await readFile6(found.path, "utf-8");
|
|
3874
4067
|
return formatShowOutput(content, { status: found.status });
|
|
3875
4068
|
}
|
|
3876
4069
|
async function showCommand2(options) {
|
|
@@ -3885,9 +4078,9 @@ Session File Format:
|
|
|
3885
4078
|
|
|
3886
4079
|
---
|
|
3887
4080
|
priority: high
|
|
4081
|
+
git_ref: main
|
|
3888
4082
|
goal: Fix the failing release check
|
|
3889
4083
|
next_step: Run the focused session tests
|
|
3890
|
-
result: Local validation passed
|
|
3891
4084
|
specs: []
|
|
3892
4085
|
files: []
|
|
3893
4086
|
---
|
|
@@ -3896,43 +4089,47 @@ Session File Format:
|
|
|
3896
4089
|
Session content...
|
|
3897
4090
|
|
|
3898
4091
|
Workflow:
|
|
3899
|
-
1. handoff - Create session (todo)
|
|
4092
|
+
1. handoff - Create session (todo) \u2014 JSON header + body on stdin
|
|
3900
4093
|
2. pickup - Claim session (todo -> doing)
|
|
3901
4094
|
3. release - Return session (doing -> todo)
|
|
3902
|
-
4. archive - Move session
|
|
4095
|
+
4. archive - Move session to archive
|
|
3903
4096
|
5. delete - Remove session permanently
|
|
3904
4097
|
`;
|
|
3905
4098
|
var HANDOFF_FRONTMATTER_HELP = `
|
|
3906
4099
|
Usage:
|
|
3907
|
-
Pipe
|
|
4100
|
+
Pipe a JSON header followed by the body bytes to stdin.
|
|
3908
4101
|
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
4102
|
+
The header is a single JSON object holding caller-supplied structured fields.
|
|
4103
|
+
A single LF or CRLF after the header is consumed as a separator. The body
|
|
4104
|
+
is the remaining bytes verbatim \u2014 no YAML, no escape rules, no ambiguity
|
|
4105
|
+
from leading characters like '#' or '---'.
|
|
4106
|
+
|
|
4107
|
+
JSON Header Fields:
|
|
4108
|
+
priority "high" | "medium" | "low" (default: medium)
|
|
4109
|
+
goal required, non-empty string
|
|
4110
|
+
next_step required, non-empty string
|
|
4111
|
+
specs optional string[], for pickup auto-injection
|
|
4112
|
+
files optional string[], for pickup auto-injection
|
|
3918
4113
|
|
|
3919
4114
|
Prefilled by the CLI:
|
|
3920
|
-
created_at,
|
|
4115
|
+
created_at, git_ref, and agent_session_id when an agent session ID is
|
|
4116
|
+
available.
|
|
3921
4117
|
|
|
3922
|
-
|
|
3923
|
-
|
|
4118
|
+
git_ref records the branch name (root worktree on a branch), the HEAD SHA
|
|
4119
|
+
(detached), or the origin/<default> tip SHA (clean detached linked worktree).
|
|
4120
|
+
Handoff is refused from any other linked-worktree state.
|
|
3924
4121
|
|
|
3925
4122
|
Output Tags (for automation):
|
|
3926
|
-
<HANDOFF_ID>session-id</HANDOFF_ID>
|
|
3927
|
-
<SESSION_FILE>/path/to/file</SESSION_FILE>
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
4123
|
+
<HANDOFF_ID>session-id</HANDOFF_ID> Session identifier
|
|
4124
|
+
<SESSION_FILE>/path/to/file</SESSION_FILE> Absolute path to created file
|
|
4125
|
+
|
|
4126
|
+
Canonical Invocation:
|
|
4127
|
+
printf '%s\\n' \\
|
|
4128
|
+
'{"priority":"high","goal":"Fix login","next_step":"Run validation","specs":[],"files":[]}' \\
|
|
4129
|
+
'# Fix login' \\
|
|
4130
|
+
'' \\
|
|
4131
|
+
'Body text \u2014 # symbols, --- delimiters, and code fences are literal.' \\
|
|
4132
|
+
| spx session handoff
|
|
3936
4133
|
`;
|
|
3937
4134
|
var PICKUP_SELECTION_HELP = `
|
|
3938
4135
|
Selection Logic (--auto):
|
|
@@ -3943,10 +4140,10 @@ Selection Logic (--auto):
|
|
|
3943
4140
|
4. Within same priority: oldest session first
|
|
3944
4141
|
|
|
3945
4142
|
Output:
|
|
3946
|
-
<PICKUP_ID>session-id</PICKUP_ID> tag for
|
|
4143
|
+
<PICKUP_ID>session-id</PICKUP_ID> tag for each claimed session
|
|
3947
4144
|
`;
|
|
3948
4145
|
|
|
3949
|
-
// src/
|
|
4146
|
+
// src/interfaces/cli/session.ts
|
|
3950
4147
|
async function readStdin() {
|
|
3951
4148
|
if (process.stdin.isTTY) {
|
|
3952
4149
|
return void 0;
|
|
@@ -3958,7 +4155,7 @@ async function readStdin() {
|
|
|
3958
4155
|
data += chunk;
|
|
3959
4156
|
});
|
|
3960
4157
|
process.stdin.on("end", () => {
|
|
3961
|
-
resolve6(data.
|
|
4158
|
+
resolve6(data.length === 0 ? void 0 : data);
|
|
3962
4159
|
});
|
|
3963
4160
|
process.stdin.on("error", () => {
|
|
3964
4161
|
resolve6(void 0);
|
|
@@ -4005,14 +4202,14 @@ function registerSessionCommands(sessionCmd) {
|
|
|
4005
4202
|
handleError(error);
|
|
4006
4203
|
}
|
|
4007
4204
|
});
|
|
4008
|
-
sessionCmd.command("pickup [
|
|
4205
|
+
sessionCmd.command("pickup [ids...]").description("Claim one or more sessions (move from todo to doing)").option("--auto", "Auto-select highest priority session").option("--sessions-dir <path>", "Custom sessions directory").addHelpText("after", PICKUP_SELECTION_HELP).action(async (ids, options) => {
|
|
4009
4206
|
try {
|
|
4010
|
-
if (
|
|
4207
|
+
if (ids.length === 0 && !options.auto) {
|
|
4011
4208
|
console.error("Error: Either session ID or --auto flag is required");
|
|
4012
4209
|
process.exit(1);
|
|
4013
4210
|
}
|
|
4014
4211
|
const output = await pickupCommand({
|
|
4015
|
-
|
|
4212
|
+
sessionIds: ids,
|
|
4016
4213
|
auto: options.auto,
|
|
4017
4214
|
sessionsDir: options.sessionsDir
|
|
4018
4215
|
});
|
|
@@ -4032,14 +4229,18 @@ function registerSessionCommands(sessionCmd) {
|
|
|
4032
4229
|
handleError(error);
|
|
4033
4230
|
}
|
|
4034
4231
|
});
|
|
4035
|
-
sessionCmd.command("handoff").description("Create a handoff session (reads
|
|
4232
|
+
sessionCmd.command("handoff").description("Create a handoff session (reads JSON header + body from stdin)").option("--sessions-dir <path>", "Custom sessions directory").addHelpText("after", HANDOFF_FRONTMATTER_HELP).action(async (options) => {
|
|
4036
4233
|
try {
|
|
4037
4234
|
const content = await readStdin();
|
|
4038
|
-
const
|
|
4235
|
+
const result = await handoffCommand({
|
|
4039
4236
|
content,
|
|
4040
4237
|
sessionsDir: options.sessionsDir
|
|
4041
4238
|
});
|
|
4042
|
-
|
|
4239
|
+
if (result.warning !== void 0) {
|
|
4240
|
+
process.stderr.write(`${result.warning}
|
|
4241
|
+
`);
|
|
4242
|
+
}
|
|
4243
|
+
console.log(result.output);
|
|
4043
4244
|
} catch (error) {
|
|
4044
4245
|
handleError(error);
|
|
4045
4246
|
}
|
|
@@ -4098,7 +4299,7 @@ var sessionDomain = {
|
|
|
4098
4299
|
};
|
|
4099
4300
|
|
|
4100
4301
|
// src/lib/spec-tree/index.ts
|
|
4101
|
-
import { readdir as readdir5, readFile as
|
|
4302
|
+
import { readdir as readdir5, readFile as readFile7 } from "fs/promises";
|
|
4102
4303
|
import { join as join10 } from "path";
|
|
4103
4304
|
var SPEC_TREE_FIELD_KEY = {
|
|
4104
4305
|
VERSION: "version",
|
|
@@ -4191,7 +4392,7 @@ function createFilesystemSpecTreeSource(options) {
|
|
|
4191
4392
|
if (ref.path === void 0) {
|
|
4192
4393
|
throw new Error("Filesystem source refs require a path");
|
|
4193
4394
|
}
|
|
4194
|
-
return
|
|
4395
|
+
return readFile7(join10(options.productDir, ref.path), SPEC_TREE_TEXT_ENCODING);
|
|
4195
4396
|
}
|
|
4196
4397
|
};
|
|
4197
4398
|
}
|
|
@@ -4690,7 +4891,7 @@ function formatNodeLabel(node) {
|
|
|
4690
4891
|
].join(STATUS_SEPARATOR);
|
|
4691
4892
|
}
|
|
4692
4893
|
|
|
4693
|
-
// src/
|
|
4894
|
+
// src/interfaces/cli/spec.ts
|
|
4694
4895
|
var SPEC_DOMAIN_CLI = {
|
|
4695
4896
|
COMMAND: "spec",
|
|
4696
4897
|
STATUS_COMMAND: "status",
|
|
@@ -4760,6 +4961,9 @@ var specDomain = {
|
|
|
4760
4961
|
}
|
|
4761
4962
|
};
|
|
4762
4963
|
|
|
4964
|
+
// src/commands/validation/markdown.ts
|
|
4965
|
+
import { relative as relative4 } from "path";
|
|
4966
|
+
|
|
4763
4967
|
// src/validation/config/path-filter.ts
|
|
4764
4968
|
import { isAbsolute as isAbsolute2, relative as relative3 } from "path";
|
|
4765
4969
|
var PATH_PREFIX_SEPARATOR = "/";
|
|
@@ -4855,28 +5059,313 @@ function applyValidationPathFilterToScope(scopeConfig, filter) {
|
|
|
4855
5059
|
};
|
|
4856
5060
|
}
|
|
4857
5061
|
|
|
4858
|
-
//
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
5062
|
+
// src/validation/steps/markdown.ts
|
|
5063
|
+
import { existsSync as existsSync2, statSync } from "fs";
|
|
5064
|
+
import { basename, dirname as dirname3, join as join11, relative as pathRelative } from "path";
|
|
5065
|
+
import { main as markdownlintMain } from "markdownlint-cli2";
|
|
5066
|
+
import relativeLinksRule from "markdownlint-rule-relative-links";
|
|
5067
|
+
var MARKDOWN_DEFAULT_DIRECTORY_NAMES = ["spx", "docs"];
|
|
5068
|
+
var MARKDOWN_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".markdown"]);
|
|
5069
|
+
var MARKDOWN_DIRECTORY_GLOB = "**/*.md";
|
|
5070
|
+
var ENABLED_RULES = {
|
|
5071
|
+
MD001: true,
|
|
5072
|
+
MD003: true,
|
|
5073
|
+
MD009: true,
|
|
5074
|
+
MD010: true,
|
|
5075
|
+
MD025: true,
|
|
5076
|
+
MD047: true
|
|
5077
|
+
};
|
|
5078
|
+
var MD024_DISABLED_DIRECTORIES = ["docs"];
|
|
5079
|
+
var MARKDOWN_CUSTOM_RULE_NAMES = relativeLinksRule.names;
|
|
5080
|
+
var MARKDOWN_VALIDATION_TARGET_KIND = {
|
|
5081
|
+
DIRECTORY: "directory",
|
|
5082
|
+
FILE: "file"
|
|
5083
|
+
};
|
|
5084
|
+
var MARKDOWN_VALIDATION_TARGET_DIAGNOSTICS = {
|
|
5085
|
+
MISSING_OR_UNRELATED_SCOPE: "not an existing directory or markdown file"
|
|
5086
|
+
};
|
|
5087
|
+
var ERROR_LINE_PATTERN = /^(.+?):(\d+)(?::\d+)?\s+(.+)$/;
|
|
5088
|
+
var defaultMarkdownValidationTargetDeps = {
|
|
5089
|
+
statSync
|
|
5090
|
+
};
|
|
5091
|
+
function buildMarkdownlintConfig(directoryName) {
|
|
5092
|
+
const md024Disabled = MD024_DISABLED_DIRECTORIES.includes(
|
|
5093
|
+
directoryName
|
|
5094
|
+
);
|
|
5095
|
+
return {
|
|
5096
|
+
default: false,
|
|
5097
|
+
...ENABLED_RULES,
|
|
5098
|
+
MD024: md024Disabled ? false : { siblings_only: true },
|
|
5099
|
+
customRules: [relativeLinksRule]
|
|
5100
|
+
};
|
|
5101
|
+
}
|
|
5102
|
+
function getDefaultDirectories(projectRoot) {
|
|
5103
|
+
return MARKDOWN_DEFAULT_DIRECTORY_NAMES.map((name) => join11(projectRoot, name)).filter((dir) => existsSync2(dir));
|
|
5104
|
+
}
|
|
5105
|
+
function resolveMarkdownValidationTarget(path6, deps = defaultMarkdownValidationTargetDeps) {
|
|
5106
|
+
if (isExistingDirectory(path6, deps)) {
|
|
5107
|
+
return { target: { kind: MARKDOWN_VALIDATION_TARGET_KIND.DIRECTORY, path: path6 } };
|
|
5108
|
+
}
|
|
5109
|
+
if (hasMarkdownExtension(path6) && isExistingFile(path6, deps)) {
|
|
5110
|
+
return { target: { kind: MARKDOWN_VALIDATION_TARGET_KIND.FILE, path: path6 } };
|
|
5111
|
+
}
|
|
5112
|
+
return {
|
|
5113
|
+
skipped: {
|
|
5114
|
+
path: path6,
|
|
5115
|
+
reason: MARKDOWN_VALIDATION_TARGET_DIAGNOSTICS.MISSING_OR_UNRELATED_SCOPE
|
|
5116
|
+
}
|
|
5117
|
+
};
|
|
5118
|
+
}
|
|
5119
|
+
function getExcludeGlobs(projectRoot) {
|
|
5120
|
+
if (projectRoot === void 0) return [];
|
|
5121
|
+
const reader = createIgnoreSourceReader(projectRoot, {
|
|
5122
|
+
ignoreSourceFilename: IGNORE_SOURCE_FILENAME_DEFAULT,
|
|
5123
|
+
specTreeRootSegment: SPEC_TREE_CONFIG.ROOT_DIRECTORY
|
|
5124
|
+
});
|
|
5125
|
+
return reader.entries().map((entry) => `${entry.segment}/**`);
|
|
5126
|
+
}
|
|
5127
|
+
var DATA_URI_PATTERN = /\bdata:/;
|
|
5128
|
+
function parseErrorLine(line) {
|
|
5129
|
+
if (DATA_URI_PATTERN.test(line)) {
|
|
5130
|
+
return null;
|
|
5131
|
+
}
|
|
5132
|
+
const match = ERROR_LINE_PATTERN.exec(line);
|
|
5133
|
+
if (!match) {
|
|
5134
|
+
return null;
|
|
5135
|
+
}
|
|
5136
|
+
const [, file, lineStr, detail] = match;
|
|
5137
|
+
return {
|
|
5138
|
+
file,
|
|
5139
|
+
line: parseInt(lineStr, 10),
|
|
5140
|
+
detail
|
|
5141
|
+
};
|
|
5142
|
+
}
|
|
5143
|
+
async function validateMarkdown(options) {
|
|
5144
|
+
const { targets, projectRoot } = options;
|
|
5145
|
+
const errors = [];
|
|
5146
|
+
const excludeGlobs = getExcludeGlobs(projectRoot);
|
|
5147
|
+
for (const target of targets) {
|
|
5148
|
+
const directory = targetDirectory(target);
|
|
5149
|
+
const dirName = markdownlintConfigDirectoryName(directory, projectRoot);
|
|
5150
|
+
const config = buildMarkdownlintConfig(dirName);
|
|
5151
|
+
const dirErrors = await validateTarget(target, config, projectRoot, excludeGlobs);
|
|
5152
|
+
errors.push(...dirErrors);
|
|
5153
|
+
}
|
|
5154
|
+
return {
|
|
5155
|
+
success: errors.length === 0,
|
|
5156
|
+
errors
|
|
5157
|
+
};
|
|
5158
|
+
}
|
|
5159
|
+
async function validateTarget(target, config, projectRoot, ignoreGlobs = []) {
|
|
5160
|
+
const errors = [];
|
|
5161
|
+
const directory = targetDirectory(target);
|
|
5162
|
+
const argv = target.kind === MARKDOWN_VALIDATION_TARGET_KIND.FILE ? [basename(target.path)] : [MARKDOWN_DIRECTORY_GLOB];
|
|
5163
|
+
const { customRules, ...markdownlintConfig } = config;
|
|
5164
|
+
const optionsOverride = {
|
|
5165
|
+
config: {
|
|
5166
|
+
...markdownlintConfig,
|
|
5167
|
+
"relative-links": projectRoot ? { root_path: projectRoot } : true
|
|
5168
|
+
},
|
|
5169
|
+
customRules,
|
|
5170
|
+
noProgress: true,
|
|
5171
|
+
noBanner: true,
|
|
5172
|
+
...ignoreGlobs.length > 0 ? { ignores: ignoreGlobs } : {}
|
|
5173
|
+
};
|
|
5174
|
+
await markdownlintMain({
|
|
5175
|
+
directory,
|
|
5176
|
+
argv,
|
|
5177
|
+
optionsOverride,
|
|
5178
|
+
noImport: true,
|
|
5179
|
+
logMessage: () => {
|
|
5180
|
+
},
|
|
5181
|
+
logError: (message) => {
|
|
5182
|
+
const parsed = parseErrorLine(message);
|
|
5183
|
+
if (parsed) {
|
|
5184
|
+
errors.push({
|
|
5185
|
+
...parsed,
|
|
5186
|
+
file: join11(directory, parsed.file)
|
|
5187
|
+
});
|
|
5188
|
+
}
|
|
5189
|
+
}
|
|
5190
|
+
});
|
|
5191
|
+
return errors;
|
|
5192
|
+
}
|
|
5193
|
+
function targetDirectory(target) {
|
|
5194
|
+
return target.kind === MARKDOWN_VALIDATION_TARGET_KIND.FILE ? dirname3(target.path) : target.path;
|
|
5195
|
+
}
|
|
5196
|
+
function hasMarkdownExtension(path6) {
|
|
5197
|
+
const lastDot = path6.lastIndexOf(".");
|
|
5198
|
+
if (lastDot < 0) return false;
|
|
5199
|
+
return MARKDOWN_FILE_EXTENSIONS.has(path6.slice(lastDot).toLowerCase());
|
|
5200
|
+
}
|
|
5201
|
+
function isExistingDirectory(path6, deps) {
|
|
5202
|
+
try {
|
|
5203
|
+
return deps.statSync(path6).isDirectory();
|
|
5204
|
+
} catch {
|
|
5205
|
+
return false;
|
|
5206
|
+
}
|
|
5207
|
+
}
|
|
5208
|
+
function isExistingFile(path6, deps) {
|
|
5209
|
+
try {
|
|
5210
|
+
return deps.statSync(path6).isFile();
|
|
5211
|
+
} catch {
|
|
5212
|
+
return false;
|
|
5213
|
+
}
|
|
5214
|
+
}
|
|
5215
|
+
function markdownlintConfigDirectoryName(directory, projectRoot) {
|
|
5216
|
+
if (projectRoot !== void 0) {
|
|
5217
|
+
const [rootSegment] = pathRelative(projectRoot, directory).split(/[\\/]/);
|
|
5218
|
+
if (MD024_DISABLED_DIRECTORIES.includes(rootSegment)) {
|
|
5219
|
+
return rootSegment;
|
|
5220
|
+
}
|
|
5221
|
+
}
|
|
5222
|
+
return basename(directory);
|
|
5223
|
+
}
|
|
5224
|
+
|
|
5225
|
+
// src/commands/validation/messages.ts
|
|
5226
|
+
var VALIDATION_STAGE_DISPLAY_NAMES = {
|
|
5227
|
+
CIRCULAR: "Circular dependencies",
|
|
5228
|
+
KNIP: "Knip",
|
|
5229
|
+
ESLINT: "ESLint",
|
|
5230
|
+
TYPESCRIPT: "TypeScript",
|
|
5231
|
+
MARKDOWN: "Markdown",
|
|
5232
|
+
LITERAL: "Literal"
|
|
5233
|
+
};
|
|
5234
|
+
var VALIDATION_SKIP_LABELS = {
|
|
5235
|
+
VERB: "Skipping",
|
|
5236
|
+
LITERAL_REASON: "skip-literal",
|
|
5237
|
+
TYPESCRIPT_ABSENT_REASON: "TypeScript not detected in project",
|
|
5238
|
+
VALIDATION_PATHS_NO_TARGETS_REASON: "validation paths matched no files",
|
|
5239
|
+
MARKDOWN_NO_SCOPE_REASON: "no markdown files in --files scope",
|
|
5240
|
+
MARKDOWN_NO_DEFAULT_DIRECTORIES_REASON: "no spx/ or docs/ directories found"
|
|
5241
|
+
};
|
|
5242
|
+
var VALIDATION_COMMAND_OUTPUT = {
|
|
5243
|
+
CIRCULAR_FOUND: `${VALIDATION_STAGE_DISPLAY_NAMES.CIRCULAR} found`,
|
|
5244
|
+
CIRCULAR_NONE_FOUND: `${VALIDATION_STAGE_DISPLAY_NAMES.CIRCULAR}: \u2713 None found`,
|
|
5245
|
+
KNIP_CONFIG_ERROR: `${VALIDATION_STAGE_DISPLAY_NAMES.KNIP}: \u2717 config error`,
|
|
5246
|
+
KNIP_DISABLED: `${VALIDATION_STAGE_DISPLAY_NAMES.KNIP}: skipped (disabled by validation.knip.enabled)`,
|
|
5247
|
+
KNIP_SUCCESS: `${VALIDATION_STAGE_DISPLAY_NAMES.KNIP}: \u2713 No unused code found`,
|
|
5248
|
+
KNIP_FAILURE: "Unused code found",
|
|
5249
|
+
ESLINT_SUCCESS: `${VALIDATION_STAGE_DISPLAY_NAMES.ESLINT}: \u2713 No errors found`,
|
|
5250
|
+
ESLINT_FAILURE: `${VALIDATION_STAGE_DISPLAY_NAMES.ESLINT} validation failed`,
|
|
5251
|
+
ESLINT_MISSING_CONFIG: "ESLint config not found: project has tsconfig.json but no eslint.config.{ts,js,mjs,cjs}",
|
|
5252
|
+
TYPESCRIPT_SUCCESS: `${VALIDATION_STAGE_DISPLAY_NAMES.TYPESCRIPT}: \u2713 No type errors`,
|
|
5253
|
+
TYPESCRIPT_FAILURE: `${VALIDATION_STAGE_DISPLAY_NAMES.TYPESCRIPT} validation failed`,
|
|
5254
|
+
MARKDOWN_NO_ISSUES: `${VALIDATION_STAGE_DISPLAY_NAMES.MARKDOWN}: No issues found`,
|
|
5255
|
+
MARKDOWN_ERROR_SUMMARY_SUFFIX: "error(s) found"
|
|
5256
|
+
};
|
|
5257
|
+
var LITERAL_SKIP_OUTPUT = `${VALIDATION_STAGE_DISPLAY_NAMES.LITERAL}: skipped (--${VALIDATION_SKIP_LABELS.LITERAL_REASON})`;
|
|
5258
|
+
var LITERAL_SKIP_JSON_OUTPUT = JSON.stringify({
|
|
5259
|
+
skipped: true,
|
|
5260
|
+
reason: VALIDATION_SKIP_LABELS.LITERAL_REASON
|
|
5261
|
+
});
|
|
5262
|
+
function formatTypeScriptAbsentSkipMessage(stageName) {
|
|
5263
|
+
return `\u23ED ${VALIDATION_SKIP_LABELS.VERB} ${stageName} (${VALIDATION_SKIP_LABELS.TYPESCRIPT_ABSENT_REASON})`;
|
|
5264
|
+
}
|
|
5265
|
+
function formatValidationPathsNoTargetsSkipMessage(stageName) {
|
|
5266
|
+
return `\u23ED ${VALIDATION_SKIP_LABELS.VERB} ${stageName} (${VALIDATION_SKIP_LABELS.VALIDATION_PATHS_NO_TARGETS_REASON})`;
|
|
5267
|
+
}
|
|
5268
|
+
|
|
5269
|
+
// src/commands/validation/markdown.ts
|
|
5270
|
+
var MARKDOWN_COMMAND_OUTPUT = {
|
|
5271
|
+
ERROR_SUMMARY_SUFFIX: VALIDATION_COMMAND_OUTPUT.MARKDOWN_ERROR_SUMMARY_SUFFIX,
|
|
5272
|
+
NO_ISSUES: VALIDATION_COMMAND_OUTPUT.MARKDOWN_NO_ISSUES,
|
|
5273
|
+
SKIPPED_FILE_SCOPE_PREFIX: "Markdown skipped file scope"
|
|
5274
|
+
};
|
|
5275
|
+
var MARKDOWN_CONFIG_ERROR_MESSAGE = `${VALIDATION_STAGE_DISPLAY_NAMES.MARKDOWN}: \u2717 config error`;
|
|
5276
|
+
async function markdownCommand(options) {
|
|
5277
|
+
const { cwd, files, quiet } = options;
|
|
5278
|
+
const startTime = Date.now();
|
|
5279
|
+
const loaded = await resolveConfig(cwd, [validationConfigDescriptor]);
|
|
5280
|
+
if (!loaded.ok) {
|
|
5281
|
+
return {
|
|
5282
|
+
exitCode: 1,
|
|
5283
|
+
output: `${MARKDOWN_CONFIG_ERROR_MESSAGE} \u2014 ${loaded.error}`,
|
|
5284
|
+
durationMs: Date.now() - startTime
|
|
5285
|
+
};
|
|
5286
|
+
}
|
|
5287
|
+
const validationConfig = loaded.value[validationConfigDescriptor.section];
|
|
5288
|
+
const pathFilter = validationPathFilterForTool(
|
|
5289
|
+
validationConfig.paths,
|
|
5290
|
+
VALIDATION_PATH_TOOL_SUBSECTIONS.MARKDOWN
|
|
5291
|
+
);
|
|
5292
|
+
const targetResolutions = files && files.length > 0 ? files.map((filePath) => resolveMarkdownValidationTarget(filePath)) : void 0;
|
|
5293
|
+
const unfilteredTargets = targetResolutions !== void 0 ? targetResolutions.map((resolution) => resolution.target).filter((target) => target !== void 0) : getDefaultDirectories(cwd).map((path6) => ({
|
|
5294
|
+
kind: MARKDOWN_VALIDATION_TARGET_KIND.DIRECTORY,
|
|
5295
|
+
path: path6
|
|
5296
|
+
}));
|
|
5297
|
+
const targets = unfilteredTargets.filter(
|
|
5298
|
+
(target) => pathPassesValidationFilter(relative4(cwd, target.path), pathFilter)
|
|
5299
|
+
);
|
|
5300
|
+
const skippedTargets = targetResolutions === void 0 ? [] : targetResolutions.map((resolution) => resolution.skipped).filter((skipped) => skipped !== void 0);
|
|
5301
|
+
const skippedOutput = quiet ? [] : skippedTargets.map(formatSkippedFileScope);
|
|
5302
|
+
if (targets.length === 0) {
|
|
5303
|
+
const reason = files && files.length > 0 ? VALIDATION_SKIP_LABELS.MARKDOWN_NO_SCOPE_REASON : VALIDATION_SKIP_LABELS.MARKDOWN_NO_DEFAULT_DIRECTORIES_REASON;
|
|
5304
|
+
const output = quiet ? "" : [
|
|
5305
|
+
...skippedOutput,
|
|
5306
|
+
`${VALIDATION_STAGE_DISPLAY_NAMES.MARKDOWN}: skipped (${reason})`
|
|
5307
|
+
].join("\n");
|
|
5308
|
+
return { exitCode: 0, output, durationMs: Date.now() - startTime };
|
|
5309
|
+
}
|
|
5310
|
+
const result = await validateMarkdown({
|
|
5311
|
+
targets,
|
|
5312
|
+
projectRoot: cwd
|
|
5313
|
+
});
|
|
5314
|
+
const durationMs = Date.now() - startTime;
|
|
5315
|
+
if (result.success) {
|
|
5316
|
+
const output = quiet ? "" : [...skippedOutput, MARKDOWN_COMMAND_OUTPUT.NO_ISSUES].join("\n");
|
|
5317
|
+
return { exitCode: 0, output, durationMs };
|
|
5318
|
+
} else {
|
|
5319
|
+
const errorLines = result.errors.map(
|
|
5320
|
+
(error) => ` ${error.file}:${error.line} ${error.detail}`
|
|
5321
|
+
);
|
|
5322
|
+
const output = [
|
|
5323
|
+
...skippedOutput,
|
|
5324
|
+
`Markdown: ${result.errors.length} ${MARKDOWN_COMMAND_OUTPUT.ERROR_SUMMARY_SUFFIX}`,
|
|
5325
|
+
...errorLines
|
|
5326
|
+
].join("\n");
|
|
5327
|
+
return { exitCode: 1, output, durationMs };
|
|
5328
|
+
}
|
|
5329
|
+
}
|
|
5330
|
+
function formatSkippedFileScope(target) {
|
|
5331
|
+
return `${MARKDOWN_COMMAND_OUTPUT.SKIPPED_FILE_SCOPE_PREFIX}: ${target.path} (${target.reason})`;
|
|
5332
|
+
}
|
|
5333
|
+
|
|
5334
|
+
// src/validation/languages/markdown.ts
|
|
5335
|
+
var MARKDOWN_LANGUAGE_NAME = "markdown";
|
|
5336
|
+
var markdownValidationLanguage = {
|
|
5337
|
+
name: MARKDOWN_LANGUAGE_NAME,
|
|
5338
|
+
stages: [
|
|
5339
|
+
{
|
|
5340
|
+
name: VALIDATION_STAGE_DISPLAY_NAMES.MARKDOWN,
|
|
5341
|
+
failsPipeline: true,
|
|
5342
|
+
run: (context) => markdownCommand({ cwd: context.cwd, files: context.files, quiet: context.quiet })
|
|
5343
|
+
}
|
|
5344
|
+
]
|
|
5345
|
+
};
|
|
5346
|
+
|
|
5347
|
+
// node_modules/.pnpm/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
|
|
5348
|
+
function createScanner(text, ignoreTrivia = false) {
|
|
5349
|
+
const len = text.length;
|
|
5350
|
+
let pos = 0, value = "", tokenOffset = 0, token = 16, lineNumber = 0, lineStartOffset = 0, tokenLineStartOffset = 0, prevTokenLineStartOffset = 0, scanError = 0;
|
|
5351
|
+
function scanHexDigits(count, exact) {
|
|
5352
|
+
let digits = 0;
|
|
5353
|
+
let value2 = 0;
|
|
5354
|
+
while (digits < count || !exact) {
|
|
5355
|
+
let ch = text.charCodeAt(pos);
|
|
5356
|
+
if (ch >= 48 && ch <= 57) {
|
|
5357
|
+
value2 = value2 * 16 + ch - 48;
|
|
5358
|
+
} else if (ch >= 65 && ch <= 70) {
|
|
5359
|
+
value2 = value2 * 16 + ch - 65 + 10;
|
|
5360
|
+
} else if (ch >= 97 && ch <= 102) {
|
|
5361
|
+
value2 = value2 * 16 + ch - 97 + 10;
|
|
5362
|
+
} else {
|
|
5363
|
+
break;
|
|
5364
|
+
}
|
|
5365
|
+
pos++;
|
|
5366
|
+
digits++;
|
|
5367
|
+
}
|
|
5368
|
+
if (digits < count) {
|
|
4880
5369
|
value2 = -1;
|
|
4881
5370
|
}
|
|
4882
5371
|
return value2;
|
|
@@ -5715,8 +6204,8 @@ var ParseErrorCode;
|
|
|
5715
6204
|
})(ParseErrorCode || (ParseErrorCode = {}));
|
|
5716
6205
|
|
|
5717
6206
|
// src/validation/config/scope.ts
|
|
5718
|
-
import { existsSync as
|
|
5719
|
-
import { isAbsolute as isAbsolute3, join as
|
|
6207
|
+
import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync2 } from "fs";
|
|
6208
|
+
import { isAbsolute as isAbsolute3, join as join12 } from "path";
|
|
5720
6209
|
var TSCONFIG_FILES = {
|
|
5721
6210
|
full: "tsconfig.json",
|
|
5722
6211
|
production: "tsconfig.production.json"
|
|
@@ -5726,11 +6215,11 @@ var GLOB_MARKER = "*";
|
|
|
5726
6215
|
var HIDDEN_PATH_PREFIX = ".";
|
|
5727
6216
|
var defaultScopeDeps = {
|
|
5728
6217
|
readFileSync: readFileSync2,
|
|
5729
|
-
existsSync:
|
|
6218
|
+
existsSync: existsSync3,
|
|
5730
6219
|
readdirSync
|
|
5731
6220
|
};
|
|
5732
6221
|
function resolveProjectPath(projectRoot, path6) {
|
|
5733
|
-
return isAbsolute3(path6) ? path6 :
|
|
6222
|
+
return isAbsolute3(path6) ? path6 : join12(projectRoot, path6);
|
|
5734
6223
|
}
|
|
5735
6224
|
function parseTypeScriptConfig(configPath, deps = defaultScopeDeps) {
|
|
5736
6225
|
try {
|
|
@@ -5774,7 +6263,7 @@ function hasTypeScriptFilesRecursive(dirPath, maxDepth = 2, deps = defaultScopeD
|
|
|
5774
6263
|
if (hasDirectTsFiles) return true;
|
|
5775
6264
|
const subdirs = items.filter((item) => item.isDirectory() && !item.name.startsWith("."));
|
|
5776
6265
|
for (const subdir of subdirs.slice(0, 5)) {
|
|
5777
|
-
if (hasTypeScriptFilesRecursive(
|
|
6266
|
+
if (hasTypeScriptFilesRecursive(join12(dirPath, subdir.name), maxDepth - 1, deps)) {
|
|
5778
6267
|
return true;
|
|
5779
6268
|
}
|
|
5780
6269
|
}
|
|
@@ -5797,7 +6286,7 @@ function getTopLevelDirectoriesWithTypeScript(config, projectRoot, deps = defaul
|
|
|
5797
6286
|
});
|
|
5798
6287
|
if (!isExcluded) {
|
|
5799
6288
|
try {
|
|
5800
|
-
const hasTypeScriptFiles = hasTypeScriptFilesRecursive(
|
|
6289
|
+
const hasTypeScriptFiles = hasTypeScriptFilesRecursive(join12(projectRoot, dir), 2, deps);
|
|
5801
6290
|
if (hasTypeScriptFiles) {
|
|
5802
6291
|
directories.add(dir);
|
|
5803
6292
|
}
|
|
@@ -5828,13 +6317,13 @@ function getLiteralTopLevelPatternDirectory(pattern) {
|
|
|
5828
6317
|
function filterActiveIncludePatterns(patterns, excludePatterns, projectRoot, deps) {
|
|
5829
6318
|
return patterns.filter((pattern) => {
|
|
5830
6319
|
const topLevelDir = getLiteralTopLevelPatternDirectory(pattern);
|
|
5831
|
-
return topLevelDir === null || deps.existsSync(
|
|
6320
|
+
return topLevelDir === null || deps.existsSync(join12(projectRoot, topLevelDir));
|
|
5832
6321
|
}).filter((pattern) => pathPassesValidationFilter(pattern, { exclude: excludePatterns }));
|
|
5833
6322
|
}
|
|
5834
6323
|
function getValidationDirectories(scope, projectRoot, deps = defaultScopeDeps) {
|
|
5835
6324
|
const config = resolveTypeScriptConfig(scope, projectRoot, deps);
|
|
5836
6325
|
const configDirectories = getTopLevelDirectoriesWithTypeScript(config, projectRoot, deps);
|
|
5837
|
-
const existingDirectories = configDirectories.filter((dir) => deps.existsSync(
|
|
6326
|
+
const existingDirectories = configDirectories.filter((dir) => deps.existsSync(join12(projectRoot, dir)));
|
|
5838
6327
|
return existingDirectories;
|
|
5839
6328
|
}
|
|
5840
6329
|
function getTypeScriptScope(scope, projectRoot, deps = defaultScopeDeps) {
|
|
@@ -5991,7 +6480,7 @@ function detectTypeScript(projectRoot, deps = defaultLanguageDetectionDeps) {
|
|
|
5991
6480
|
|
|
5992
6481
|
// src/validation/steps/circular.ts
|
|
5993
6482
|
import madge from "madge";
|
|
5994
|
-
import { join as
|
|
6483
|
+
import { join as join13 } from "path";
|
|
5995
6484
|
var CIRCULAR_DEPS_KEYS = {
|
|
5996
6485
|
MADGE: "madge"
|
|
5997
6486
|
};
|
|
@@ -6000,87 +6489,36 @@ var defaultCircularDeps = {
|
|
|
6000
6489
|
};
|
|
6001
6490
|
async function validateCircularDependencies(scope, typescriptScope, projectRoot, deps = defaultCircularDeps) {
|
|
6002
6491
|
try {
|
|
6003
|
-
const analyzeDirectories = typescriptScope.directories.map((directory) =>
|
|
6004
|
-
if (analyzeDirectories.length === 0) {
|
|
6005
|
-
return { success: true };
|
|
6006
|
-
}
|
|
6007
|
-
const tsConfigFile =
|
|
6008
|
-
const excludeRegExps = typescriptScope.excludePatterns.map((pattern) => {
|
|
6009
|
-
const cleanPattern = pattern.replace(/\/\*\*?\/\*$/, "");
|
|
6010
|
-
const escaped = cleanPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6011
|
-
return new RegExp(escaped);
|
|
6012
|
-
});
|
|
6013
|
-
const result = await deps.madge(analyzeDirectories, {
|
|
6014
|
-
baseDir: projectRoot,
|
|
6015
|
-
fileExtensions: ["ts", "tsx"],
|
|
6016
|
-
tsConfig: tsConfigFile,
|
|
6017
|
-
excludeRegExp: excludeRegExps
|
|
6018
|
-
});
|
|
6019
|
-
const circular = result.circular();
|
|
6020
|
-
if (circular.length === 0) {
|
|
6021
|
-
return { success: true };
|
|
6022
|
-
} else {
|
|
6023
|
-
return {
|
|
6024
|
-
success: false,
|
|
6025
|
-
error: `Found ${circular.length} circular dependency cycle(s)`,
|
|
6026
|
-
circularDependencies: circular
|
|
6027
|
-
};
|
|
6028
|
-
}
|
|
6029
|
-
} catch (error) {
|
|
6030
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
6031
|
-
return { success: false, error: errorMessage };
|
|
6032
|
-
}
|
|
6033
|
-
}
|
|
6034
|
-
|
|
6035
|
-
// src/commands/validation/messages.ts
|
|
6036
|
-
var VALIDATION_PIPELINE = {
|
|
6037
|
-
TOTAL_STEPS: 6
|
|
6038
|
-
};
|
|
6039
|
-
var VALIDATION_STAGE_DISPLAY_NAMES = {
|
|
6040
|
-
CIRCULAR: "Circular dependencies",
|
|
6041
|
-
KNIP: "Knip",
|
|
6042
|
-
ESLINT: "ESLint",
|
|
6043
|
-
TYPESCRIPT: "TypeScript",
|
|
6044
|
-
MARKDOWN: "Markdown",
|
|
6045
|
-
LITERAL: "Literal"
|
|
6046
|
-
};
|
|
6047
|
-
var VALIDATION_SKIP_LABELS = {
|
|
6048
|
-
VERB: "Skipping",
|
|
6049
|
-
LITERAL_REASON: "skip-literal",
|
|
6050
|
-
TYPESCRIPT_ABSENT_REASON: "TypeScript not detected in project",
|
|
6051
|
-
VALIDATION_PATHS_NO_TARGETS_REASON: "validation paths matched no files",
|
|
6052
|
-
MARKDOWN_NO_SCOPE_REASON: "no markdown files in --files scope",
|
|
6053
|
-
MARKDOWN_NO_DEFAULT_DIRECTORIES_REASON: "no spx/ or docs/ directories found"
|
|
6054
|
-
};
|
|
6055
|
-
var VALIDATION_COMMAND_OUTPUT = {
|
|
6056
|
-
CIRCULAR_FOUND: `${VALIDATION_STAGE_DISPLAY_NAMES.CIRCULAR} found`,
|
|
6057
|
-
CIRCULAR_NONE_FOUND: `${VALIDATION_STAGE_DISPLAY_NAMES.CIRCULAR}: \u2713 None found`,
|
|
6058
|
-
KNIP_CONFIG_ERROR: `${VALIDATION_STAGE_DISPLAY_NAMES.KNIP}: \u2717 config error`,
|
|
6059
|
-
KNIP_DISABLED: `${VALIDATION_STAGE_DISPLAY_NAMES.KNIP}: skipped (disabled by validation.knip.enabled)`,
|
|
6060
|
-
KNIP_SUCCESS: `${VALIDATION_STAGE_DISPLAY_NAMES.KNIP}: \u2713 No unused code found`,
|
|
6061
|
-
KNIP_FAILURE: "Unused code found",
|
|
6062
|
-
ESLINT_SUCCESS: `${VALIDATION_STAGE_DISPLAY_NAMES.ESLINT}: \u2713 No errors found`,
|
|
6063
|
-
ESLINT_FAILURE: `${VALIDATION_STAGE_DISPLAY_NAMES.ESLINT} validation failed`,
|
|
6064
|
-
ESLINT_MISSING_CONFIG: "ESLint config not found: project has tsconfig.json but no eslint.config.{ts,js,mjs,cjs}",
|
|
6065
|
-
TYPESCRIPT_SUCCESS: `${VALIDATION_STAGE_DISPLAY_NAMES.TYPESCRIPT}: \u2713 No type errors`,
|
|
6066
|
-
TYPESCRIPT_FAILURE: `${VALIDATION_STAGE_DISPLAY_NAMES.TYPESCRIPT} validation failed`,
|
|
6067
|
-
MARKDOWN_NO_ISSUES: `${VALIDATION_STAGE_DISPLAY_NAMES.MARKDOWN}: No issues found`,
|
|
6068
|
-
MARKDOWN_ERROR_SUMMARY_SUFFIX: "error(s) found"
|
|
6069
|
-
};
|
|
6070
|
-
var LITERAL_SKIP_OUTPUT = `${VALIDATION_STAGE_DISPLAY_NAMES.LITERAL}: skipped (--${VALIDATION_SKIP_LABELS.LITERAL_REASON})`;
|
|
6071
|
-
var LITERAL_SKIP_JSON_OUTPUT = JSON.stringify({
|
|
6072
|
-
skipped: true,
|
|
6073
|
-
reason: VALIDATION_SKIP_LABELS.LITERAL_REASON
|
|
6074
|
-
});
|
|
6075
|
-
var VALIDATION_STEP_LINE_PATTERN = new RegExp(
|
|
6076
|
-
String.raw`^\[(\d+)\/${VALIDATION_PIPELINE.TOTAL_STEPS}\]`,
|
|
6077
|
-
"gm"
|
|
6078
|
-
);
|
|
6079
|
-
function formatTypeScriptAbsentSkipMessage(stageName) {
|
|
6080
|
-
return `\u23ED ${VALIDATION_SKIP_LABELS.VERB} ${stageName} (${VALIDATION_SKIP_LABELS.TYPESCRIPT_ABSENT_REASON})`;
|
|
6081
|
-
}
|
|
6082
|
-
function formatValidationPathsNoTargetsSkipMessage(stageName) {
|
|
6083
|
-
return `\u23ED ${VALIDATION_SKIP_LABELS.VERB} ${stageName} (${VALIDATION_SKIP_LABELS.VALIDATION_PATHS_NO_TARGETS_REASON})`;
|
|
6492
|
+
const analyzeDirectories = typescriptScope.directories.map((directory) => join13(projectRoot, directory));
|
|
6493
|
+
if (analyzeDirectories.length === 0) {
|
|
6494
|
+
return { success: true };
|
|
6495
|
+
}
|
|
6496
|
+
const tsConfigFile = join13(projectRoot, TSCONFIG_FILES[scope]);
|
|
6497
|
+
const excludeRegExps = typescriptScope.excludePatterns.map((pattern) => {
|
|
6498
|
+
const cleanPattern = pattern.replace(/\/\*\*?\/\*$/, "");
|
|
6499
|
+
const escaped = cleanPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6500
|
+
return new RegExp(escaped);
|
|
6501
|
+
});
|
|
6502
|
+
const result = await deps.madge(analyzeDirectories, {
|
|
6503
|
+
baseDir: projectRoot,
|
|
6504
|
+
fileExtensions: ["ts", "tsx"],
|
|
6505
|
+
tsConfig: tsConfigFile,
|
|
6506
|
+
excludeRegExp: excludeRegExps
|
|
6507
|
+
});
|
|
6508
|
+
const circular = result.circular();
|
|
6509
|
+
if (circular.length === 0) {
|
|
6510
|
+
return { success: true };
|
|
6511
|
+
} else {
|
|
6512
|
+
return {
|
|
6513
|
+
success: false,
|
|
6514
|
+
error: `Found ${circular.length} circular dependency cycle(s)`,
|
|
6515
|
+
circularDependencies: circular
|
|
6516
|
+
};
|
|
6517
|
+
}
|
|
6518
|
+
} catch (error) {
|
|
6519
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
6520
|
+
return { success: false, error: errorMessage };
|
|
6521
|
+
}
|
|
6084
6522
|
}
|
|
6085
6523
|
|
|
6086
6524
|
// src/commands/validation/circular.ts
|
|
@@ -6134,36 +6572,11 @@ ${cycles}`;
|
|
|
6134
6572
|
}
|
|
6135
6573
|
}
|
|
6136
6574
|
|
|
6137
|
-
// src/commands/validation/format.ts
|
|
6138
|
-
var DURATION_THRESHOLD_MS = 1e3;
|
|
6139
|
-
var VALIDATION_SYMBOLS = {
|
|
6140
|
-
SUCCESS: "\u2713",
|
|
6141
|
-
FAILURE: "\u2717"
|
|
6142
|
-
};
|
|
6143
|
-
var VALIDATION_SUMMARY_STATUS = {
|
|
6144
|
-
PASSED: "passed",
|
|
6145
|
-
FAILED: "failed"
|
|
6146
|
-
};
|
|
6147
|
-
function formatDuration(ms) {
|
|
6148
|
-
if (ms < DURATION_THRESHOLD_MS) {
|
|
6149
|
-
return `${ms}ms`;
|
|
6150
|
-
}
|
|
6151
|
-
const seconds = ms / 1e3;
|
|
6152
|
-
return `${seconds.toFixed(1)}s`;
|
|
6153
|
-
}
|
|
6154
|
-
function formatSummary(options) {
|
|
6155
|
-
const { success, totalDurationMs } = options;
|
|
6156
|
-
const symbol = success ? VALIDATION_SYMBOLS.SUCCESS : VALIDATION_SYMBOLS.FAILURE;
|
|
6157
|
-
const status = success ? VALIDATION_SUMMARY_STATUS.PASSED : VALIDATION_SUMMARY_STATUS.FAILED;
|
|
6158
|
-
const duration = formatDuration(totalDurationMs);
|
|
6159
|
-
return `${symbol} Validation ${status} (${duration} total)`;
|
|
6160
|
-
}
|
|
6161
|
-
|
|
6162
6575
|
// src/validation/steps/knip.ts
|
|
6163
|
-
import { existsSync as
|
|
6576
|
+
import { existsSync as existsSync4 } from "fs";
|
|
6164
6577
|
import { mkdtemp, rm, writeFile as writeFile2 } from "fs/promises";
|
|
6165
6578
|
import { tmpdir } from "os";
|
|
6166
|
-
import { isAbsolute as isAbsolute4, join as
|
|
6579
|
+
import { isAbsolute as isAbsolute4, join as join14 } from "path";
|
|
6167
6580
|
|
|
6168
6581
|
// src/lib/process-lifecycle/exit-codes.ts
|
|
6169
6582
|
var SIGINT_EXIT_CODE = 130;
|
|
@@ -6317,7 +6730,7 @@ var KNIP_COMMAND_TOKENS = {
|
|
|
6317
6730
|
USE_TSCONFIG_FILES_FLAG: "--use-tsconfig-files"
|
|
6318
6731
|
};
|
|
6319
6732
|
var defaultKnipDeps = {
|
|
6320
|
-
existsSync:
|
|
6733
|
+
existsSync: existsSync4,
|
|
6321
6734
|
mkdtemp,
|
|
6322
6735
|
rm,
|
|
6323
6736
|
writeFile: writeFile2
|
|
@@ -6337,7 +6750,7 @@ async function validateKnip2(context, runner = defaultKnipProcessRunner, deps =
|
|
|
6337
6750
|
}
|
|
6338
6751
|
async function runKnipSubprocess(projectRoot, typescriptScope, runner, deps) {
|
|
6339
6752
|
const scopedTsconfig = typescriptScope.filteredByValidationPaths ? await createScopedKnipTsconfig(projectRoot, typescriptScope, deps) : void 0;
|
|
6340
|
-
const localBin =
|
|
6753
|
+
const localBin = join14(projectRoot, "node_modules", ".bin", "knip");
|
|
6341
6754
|
const binary = deps.existsSync(localBin) ? localBin : "npx";
|
|
6342
6755
|
const baseArgs = scopedTsconfig === void 0 ? [] : [
|
|
6343
6756
|
KNIP_COMMAND_TOKENS.USE_TSCONFIG_FILES_FLAG,
|
|
@@ -6392,12 +6805,12 @@ async function runKnipSubprocess(projectRoot, typescriptScope, runner, deps) {
|
|
|
6392
6805
|
});
|
|
6393
6806
|
}
|
|
6394
6807
|
async function createScopedKnipTsconfig(projectRoot, typescriptScope, deps) {
|
|
6395
|
-
const tempDir = await deps.mkdtemp(
|
|
6396
|
-
const configPath =
|
|
6397
|
-
const toProjectPathPattern = (pattern) => isAbsolute4(pattern) ? pattern :
|
|
6808
|
+
const tempDir = await deps.mkdtemp(join14(tmpdir(), "validate-knip-"));
|
|
6809
|
+
const configPath = join14(tempDir, TSCONFIG_FILES.full);
|
|
6810
|
+
const toProjectPathPattern = (pattern) => isAbsolute4(pattern) ? pattern : join14(projectRoot, pattern);
|
|
6398
6811
|
const project = typescriptScope.filePatterns.length > 0 ? typescriptScope.filePatterns : typescriptScope.directories.map((directory) => `${directory}/**/*.{js,ts,tsx}`);
|
|
6399
6812
|
const config = {
|
|
6400
|
-
extends:
|
|
6813
|
+
extends: join14(projectRoot, TSCONFIG_FILES.full),
|
|
6401
6814
|
include: project.map(toProjectPathPattern),
|
|
6402
6815
|
exclude: typescriptScope.excludePatterns.map(toProjectPathPattern)
|
|
6403
6816
|
};
|
|
@@ -6446,13 +6859,13 @@ async function knipCommand(options) {
|
|
|
6446
6859
|
}
|
|
6447
6860
|
|
|
6448
6861
|
// src/validation/steps/eslint.ts
|
|
6449
|
-
import { existsSync as
|
|
6450
|
-
import { join as
|
|
6862
|
+
import { existsSync as existsSync6 } from "fs";
|
|
6863
|
+
import { join as join16 } from "path";
|
|
6451
6864
|
|
|
6452
6865
|
// src/validation/lint-policy.ts
|
|
6453
6866
|
import { execFileSync } from "child_process";
|
|
6454
|
-
import { existsSync as
|
|
6455
|
-
import { join as
|
|
6867
|
+
import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
|
|
6868
|
+
import { join as join15 } from "path";
|
|
6456
6869
|
|
|
6457
6870
|
// src/validation/lint-policy-constants.ts
|
|
6458
6871
|
var LINT_POLICY_MANIFESTS = {
|
|
@@ -6499,17 +6912,17 @@ var DEPRECATED_SPEC_NODE_SUFFIX_PATTERN = /\.(capability|feature|story)$/;
|
|
|
6499
6912
|
var BASE_BRANCH_REFS = [LINT_POLICY_BASE_REFS.REMOTE_MAIN, LINT_POLICY_BASE_REFS.LOCAL_MAIN];
|
|
6500
6913
|
function readManifest(productDir, file, key) {
|
|
6501
6914
|
return parseLintPolicyManifest(
|
|
6502
|
-
readFileSync3(
|
|
6915
|
+
readFileSync3(join15(productDir, file), "utf-8"),
|
|
6503
6916
|
file,
|
|
6504
6917
|
key
|
|
6505
6918
|
);
|
|
6506
6919
|
}
|
|
6507
6920
|
function manifestExists(productDir, file) {
|
|
6508
|
-
return
|
|
6921
|
+
return existsSync5(join15(productDir, file));
|
|
6509
6922
|
}
|
|
6510
6923
|
function findDeprecatedSpecNodePath(productDir) {
|
|
6511
6924
|
function visit2(relativeDirectory) {
|
|
6512
|
-
const absoluteDirectory =
|
|
6925
|
+
const absoluteDirectory = join15(productDir, relativeDirectory);
|
|
6513
6926
|
for (const entry of readdirSync2(absoluteDirectory, { withFileTypes: true })) {
|
|
6514
6927
|
if (!entry.isDirectory()) {
|
|
6515
6928
|
continue;
|
|
@@ -6525,8 +6938,8 @@ function findDeprecatedSpecNodePath(productDir) {
|
|
|
6525
6938
|
}
|
|
6526
6939
|
return void 0;
|
|
6527
6940
|
}
|
|
6528
|
-
const specTreeRootPath =
|
|
6529
|
-
if (!
|
|
6941
|
+
const specTreeRootPath = join15(productDir, SPEC_TREE_ROOT);
|
|
6942
|
+
if (!existsSync5(specTreeRootPath)) {
|
|
6530
6943
|
return void 0;
|
|
6531
6944
|
}
|
|
6532
6945
|
return visit2(SPEC_TREE_ROOT);
|
|
@@ -6551,8 +6964,8 @@ function assertManifestEntries(productDir, file, entries, suffixPattern, suffixD
|
|
|
6551
6964
|
if (!suffixPattern.test(entry)) {
|
|
6552
6965
|
throw new Error(`${file} entry must ${suffixDescription}: ${entry}`);
|
|
6553
6966
|
}
|
|
6554
|
-
const absoluteEntry =
|
|
6555
|
-
if (!
|
|
6967
|
+
const absoluteEntry = join15(productDir, entry);
|
|
6968
|
+
if (!existsSync5(absoluteEntry) || !statSync2(absoluteEntry).isDirectory()) {
|
|
6556
6969
|
throw new Error(`${file} entry does not exist as a directory: ${entry}`);
|
|
6557
6970
|
}
|
|
6558
6971
|
}
|
|
@@ -6811,8 +7224,8 @@ async function validateESLint(context, runner = defaultEslintProcessRunner, outp
|
|
|
6811
7224
|
scopeConfig: context.scopeConfig
|
|
6812
7225
|
});
|
|
6813
7226
|
return new Promise((resolve6) => {
|
|
6814
|
-
const localBin =
|
|
6815
|
-
const binary =
|
|
7227
|
+
const localBin = join16(projectRoot, ...ESLINT_LOCAL_BIN_SEGMENTS);
|
|
7228
|
+
const binary = existsSync6(localBin) ? localBin : "npx";
|
|
6816
7229
|
const spawnArgs = binary === "npx" ? eslintArgs : eslintArgs.slice(1);
|
|
6817
7230
|
const eslintProcess = spawnManagedSubprocess(runner, binary, spawnArgs, {
|
|
6818
7231
|
cwd: projectRoot
|
|
@@ -6912,8 +7325,8 @@ async function lintCommand(options) {
|
|
|
6912
7325
|
}
|
|
6913
7326
|
|
|
6914
7327
|
// src/validation/literal/index.ts
|
|
6915
|
-
import { readFile as
|
|
6916
|
-
import { isAbsolute as isAbsolute5, relative as
|
|
7328
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
7329
|
+
import { isAbsolute as isAbsolute5, relative as relative6, resolve as resolve5 } from "path";
|
|
6917
7330
|
|
|
6918
7331
|
// src/lib/file-inclusion/predicates/ignore-source.ts
|
|
6919
7332
|
var IGNORE_SOURCE_LAYER = "ignore-source";
|
|
@@ -6948,7 +7361,7 @@ var ignoreSourceLayer = makeLayer(
|
|
|
6948
7361
|
|
|
6949
7362
|
// src/lib/file-inclusion/pipeline.ts
|
|
6950
7363
|
import { readdir as readdir6 } from "fs/promises";
|
|
6951
|
-
import { join as
|
|
7364
|
+
import { join as join17, relative as relative5, sep } from "path";
|
|
6952
7365
|
var EXPLICIT_OVERRIDE_LAYER = "explicit-override";
|
|
6953
7366
|
function isNodeError2(err) {
|
|
6954
7367
|
return err instanceof Error && "code" in err;
|
|
@@ -6966,11 +7379,11 @@ async function collectPaths(absoluteDir, projectRoot, result, artifactDirs) {
|
|
|
6966
7379
|
for (const entry of dirEntries) {
|
|
6967
7380
|
if (entry.isDirectory()) {
|
|
6968
7381
|
if (artifactDirs.has(entry.name)) continue;
|
|
6969
|
-
const absolutePath =
|
|
7382
|
+
const absolutePath = join17(absoluteDir, entry.name);
|
|
6970
7383
|
await collectPaths(absolutePath, projectRoot, result, artifactDirs);
|
|
6971
7384
|
} else if (entry.isFile()) {
|
|
6972
|
-
const absolutePath =
|
|
6973
|
-
const rel =
|
|
7385
|
+
const absolutePath = join17(absoluteDir, entry.name);
|
|
7386
|
+
const rel = relative5(projectRoot, absolutePath);
|
|
6974
7387
|
result.push(sep === "/" ? rel : rel.split(sep).join("/"));
|
|
6975
7388
|
}
|
|
6976
7389
|
}
|
|
@@ -7079,782 +7492,551 @@ function collectLiterals(source, filename, options) {
|
|
|
7079
7492
|
loc: true,
|
|
7080
7493
|
range: true,
|
|
7081
7494
|
comment: false,
|
|
7082
|
-
jsx: true,
|
|
7083
|
-
ecmaVersion: "latest",
|
|
7084
|
-
sourceType: "module"
|
|
7085
|
-
});
|
|
7086
|
-
const out = [];
|
|
7087
|
-
const ancestors = [];
|
|
7088
|
-
walk(ast, { filename, isTestFixtureFile: isTestLikeFile(filename) }, ancestors, options, out);
|
|
7089
|
-
return out;
|
|
7090
|
-
}
|
|
7091
|
-
function walk(node, context, ancestors, options, out) {
|
|
7092
|
-
emitLiteral(node, context, ancestors, options, out);
|
|
7093
|
-
const keys = options.visitorKeys[node.type];
|
|
7094
|
-
if (!keys) {
|
|
7095
|
-
return;
|
|
7096
|
-
}
|
|
7097
|
-
const skip = MODULE_NAMING_SKIP[node.type] ?? EMPTY_SKIP;
|
|
7098
|
-
for (const key of keys) {
|
|
7099
|
-
if (skip.has(key)) continue;
|
|
7100
|
-
const child = node[key];
|
|
7101
|
-
if (Array.isArray(child)) {
|
|
7102
|
-
for (const item of child) {
|
|
7103
|
-
if (isNode(item)) walkChild(item, node, context, ancestors, options, out);
|
|
7104
|
-
}
|
|
7105
|
-
} else if (isNode(child)) {
|
|
7106
|
-
walkChild(child, node, context, ancestors, options, out);
|
|
7107
|
-
}
|
|
7108
|
-
}
|
|
7109
|
-
}
|
|
7110
|
-
function walkChild(node, parent, context, ancestors, options, out) {
|
|
7111
|
-
ancestors.push({ node: parent });
|
|
7112
|
-
try {
|
|
7113
|
-
walk(node, context, ancestors, options, out);
|
|
7114
|
-
} finally {
|
|
7115
|
-
ancestors.pop();
|
|
7116
|
-
}
|
|
7117
|
-
}
|
|
7118
|
-
function isNode(value) {
|
|
7119
|
-
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
7120
|
-
}
|
|
7121
|
-
function emitLiteral(node, context, ancestors, options, out) {
|
|
7122
|
-
if (node.type !== LITERAL_TYPE && node.type !== TEMPLATE_ELEMENT_TYPE) {
|
|
7123
|
-
return;
|
|
7124
|
-
}
|
|
7125
|
-
if (isFixtureDataLiteral(context, ancestors)) {
|
|
7126
|
-
return;
|
|
7127
|
-
}
|
|
7128
|
-
const line = node.loc?.start?.line ?? 0;
|
|
7129
|
-
if (node.type === LITERAL_TYPE) {
|
|
7130
|
-
if (typeof node.value === "string") {
|
|
7131
|
-
if (node.value.length >= options.minStringLength) {
|
|
7132
|
-
out.push({ kind: "string", value: node.value, loc: { file: context.filename, line } });
|
|
7133
|
-
}
|
|
7134
|
-
} else if (typeof node.value === "number") {
|
|
7135
|
-
const raw = typeof node.raw === "string" ? node.raw : String(node.value);
|
|
7136
|
-
if (isMeaningfulNumber(raw, options.minNumberDigits)) {
|
|
7137
|
-
out.push({ kind: "number", value: String(node.value), loc: { file: context.filename, line } });
|
|
7138
|
-
}
|
|
7139
|
-
}
|
|
7140
|
-
return;
|
|
7141
|
-
}
|
|
7142
|
-
if (node.type === TEMPLATE_ELEMENT_TYPE) {
|
|
7143
|
-
const value = node.value;
|
|
7144
|
-
const cooked = value?.cooked ?? "";
|
|
7145
|
-
if (cooked.length >= options.minStringLength) {
|
|
7146
|
-
out.push({ kind: "string", value: cooked, loc: { file: context.filename, line } });
|
|
7147
|
-
}
|
|
7148
|
-
}
|
|
7149
|
-
}
|
|
7150
|
-
function isTestLikeFile(filename) {
|
|
7151
|
-
return filename.includes(TEST_PATH_SEGMENT) || filename.includes(WINDOWS_TEST_PATH_SEGMENT) || filename.includes(TEST_FILE_MARKER);
|
|
7152
|
-
}
|
|
7153
|
-
function isFixtureDataLiteral(context, ancestors) {
|
|
7154
|
-
if (!context.isTestFixtureFile) {
|
|
7155
|
-
return false;
|
|
7156
|
-
}
|
|
7157
|
-
return isInsideFixtureWriterArgument(ancestors) || isInsideFixtureDataVariable(ancestors);
|
|
7158
|
-
}
|
|
7159
|
-
function isInsideFixtureWriterArgument(ancestors) {
|
|
7160
|
-
for (let index = ancestors.length - 1; index >= 0; index -= 1) {
|
|
7161
|
-
const ancestor = ancestors[index];
|
|
7162
|
-
if (ancestor.node.type !== CALL_EXPRESSION_TYPE) {
|
|
7163
|
-
continue;
|
|
7164
|
-
}
|
|
7165
|
-
const callName = getCallName(ancestor.node);
|
|
7166
|
-
if (callName === void 0 || !isFixtureWriterCall(callName)) {
|
|
7167
|
-
continue;
|
|
7168
|
-
}
|
|
7169
|
-
if (hasNestedFunctionBetween(ancestors, index)) {
|
|
7170
|
-
continue;
|
|
7171
|
-
}
|
|
7172
|
-
return true;
|
|
7173
|
-
}
|
|
7174
|
-
return false;
|
|
7175
|
-
}
|
|
7176
|
-
function isFixtureWriterCall(callName) {
|
|
7177
|
-
return FIXTURE_WRITER_CALLS.has(callName);
|
|
7178
|
-
}
|
|
7179
|
-
function isInsideFixtureDataVariable(ancestors) {
|
|
7180
|
-
for (let index = ancestors.length - 1; index >= 0; index -= 1) {
|
|
7181
|
-
const ancestor = ancestors[index];
|
|
7182
|
-
if (ancestor.node.type !== VARIABLE_DECLARATOR_TYPE) {
|
|
7183
|
-
continue;
|
|
7184
|
-
}
|
|
7185
|
-
if (hasNestedFunctionBetween(ancestors, index)) {
|
|
7186
|
-
continue;
|
|
7187
|
-
}
|
|
7188
|
-
const variableName = getFixtureDataDeclaratorName(ancestor.node);
|
|
7189
|
-
if (variableName !== void 0 && isFixtureDataVariableName(variableName)) {
|
|
7190
|
-
return true;
|
|
7191
|
-
}
|
|
7192
|
-
}
|
|
7193
|
-
return false;
|
|
7194
|
-
}
|
|
7195
|
-
function hasNestedFunctionBetween(ancestors, ancestorIndex) {
|
|
7196
|
-
for (let index = ancestorIndex + 1; index < ancestors.length; index += 1) {
|
|
7197
|
-
if (FUNCTION_NODE_TYPES.has(ancestors[index].node.type)) {
|
|
7198
|
-
return true;
|
|
7199
|
-
}
|
|
7200
|
-
}
|
|
7201
|
-
return false;
|
|
7202
|
-
}
|
|
7203
|
-
function getFixtureDataDeclaratorName(node) {
|
|
7204
|
-
const bindingName = getIdentifierName(node.id);
|
|
7205
|
-
if (bindingName !== void 0) {
|
|
7206
|
-
return bindingName;
|
|
7207
|
-
}
|
|
7208
|
-
return getIdentifierName(node.init);
|
|
7209
|
-
}
|
|
7210
|
-
function isFixtureDataVariableName(variableName) {
|
|
7211
|
-
const segments = splitIdentifierName(variableName);
|
|
7212
|
-
if (segments.length === 0) {
|
|
7213
|
-
return false;
|
|
7214
|
-
}
|
|
7215
|
-
if (segments.some((segment) => FIXTURE_DATA_DIRECT_SEGMENTS.has(segment))) {
|
|
7216
|
-
return true;
|
|
7217
|
-
}
|
|
7218
|
-
if (!segments.some((segment) => FIXTURE_DATA_ROLE_SEGMENTS.has(segment))) {
|
|
7219
|
-
return false;
|
|
7220
|
-
}
|
|
7221
|
-
if (segments.every((segment) => FIXTURE_DATA_ROLE_SEGMENTS.has(segment))) {
|
|
7222
|
-
return true;
|
|
7223
|
-
}
|
|
7224
|
-
const finalSegment = segments[segments.length - 1];
|
|
7225
|
-
return finalSegment !== void 0 && FIXTURE_DATA_CONTEXT_SEGMENTS.has(finalSegment);
|
|
7226
|
-
}
|
|
7227
|
-
function splitIdentifierName(variableName) {
|
|
7228
|
-
return variableName.split("_").flatMap((part) => part.match(IDENTIFIER_SEGMENT_PATTERN) ?? []).map((segment) => segment.toLowerCase());
|
|
7229
|
-
}
|
|
7230
|
-
function getCallName(node) {
|
|
7231
|
-
const callee = node.callee;
|
|
7232
|
-
if (!isNode(callee)) {
|
|
7233
|
-
return void 0;
|
|
7234
|
-
}
|
|
7235
|
-
if (callee.type === IDENTIFIER_TYPE) {
|
|
7236
|
-
return getIdentifierName(callee);
|
|
7237
|
-
}
|
|
7238
|
-
if (callee.type !== MEMBER_EXPRESSION_TYPE) {
|
|
7239
|
-
return void 0;
|
|
7240
|
-
}
|
|
7241
|
-
const property = callee.property;
|
|
7242
|
-
if (isNode(property)) {
|
|
7243
|
-
return property.type === IDENTIFIER_TYPE ? getIdentifierName(property) : getLiteralString(property);
|
|
7244
|
-
}
|
|
7245
|
-
return void 0;
|
|
7246
|
-
}
|
|
7247
|
-
function getIdentifierName(value) {
|
|
7248
|
-
if (!isNode(value) || value.type !== IDENTIFIER_TYPE) {
|
|
7249
|
-
return void 0;
|
|
7250
|
-
}
|
|
7251
|
-
return typeof value.name === "string" ? value.name : void 0;
|
|
7252
|
-
}
|
|
7253
|
-
function getLiteralString(value) {
|
|
7254
|
-
if (!isNode(value) || value.type !== LITERAL_TYPE) {
|
|
7255
|
-
return void 0;
|
|
7256
|
-
}
|
|
7257
|
-
return typeof value.value === "string" ? value.value : void 0;
|
|
7258
|
-
}
|
|
7259
|
-
function isMeaningfulNumber(raw, minDigits) {
|
|
7260
|
-
const digits = raw.replace(/[^0-9]/g, "");
|
|
7261
|
-
return digits.length >= minDigits;
|
|
7262
|
-
}
|
|
7263
|
-
function buildIndex(occurrences) {
|
|
7264
|
-
const map = /* @__PURE__ */ new Map();
|
|
7265
|
-
for (const occ of occurrences) {
|
|
7266
|
-
const key = makeKey(occ.kind, occ.value);
|
|
7267
|
-
const existing = map.get(key);
|
|
7268
|
-
if (existing) {
|
|
7269
|
-
existing.push(occ.loc);
|
|
7270
|
-
} else {
|
|
7271
|
-
map.set(key, [occ.loc]);
|
|
7272
|
-
}
|
|
7273
|
-
}
|
|
7274
|
-
return map;
|
|
7275
|
-
}
|
|
7276
|
-
function makeKey(kind, value) {
|
|
7277
|
-
return `${kind}\0${value}`;
|
|
7278
|
-
}
|
|
7279
|
-
function splitKey(key) {
|
|
7280
|
-
const idx = key.indexOf("\0");
|
|
7281
|
-
return { kind: key.slice(0, idx), value: key.slice(idx + 1) };
|
|
7495
|
+
jsx: true,
|
|
7496
|
+
ecmaVersion: "latest",
|
|
7497
|
+
sourceType: "module"
|
|
7498
|
+
});
|
|
7499
|
+
const out = [];
|
|
7500
|
+
const ancestors = [];
|
|
7501
|
+
walk(ast, { filename, isTestFixtureFile: isTestLikeFile(filename) }, ancestors, options, out);
|
|
7502
|
+
return out;
|
|
7282
7503
|
}
|
|
7283
|
-
function
|
|
7284
|
-
|
|
7285
|
-
const
|
|
7286
|
-
|
|
7287
|
-
|
|
7288
|
-
for (const occ of occurrences) {
|
|
7289
|
-
if (input.allowlist.has(occ.value)) continue;
|
|
7290
|
-
const key = makeKey(occ.kind, occ.value);
|
|
7291
|
-
let byFile = testIndex.get(key);
|
|
7292
|
-
if (!byFile) {
|
|
7293
|
-
byFile = /* @__PURE__ */ new Map();
|
|
7294
|
-
testIndex.set(key, byFile);
|
|
7295
|
-
}
|
|
7296
|
-
const locsInFile = byFile.get(file);
|
|
7297
|
-
if (locsInFile) locsInFile.push(occ.loc);
|
|
7298
|
-
else byFile.set(file, [occ.loc]);
|
|
7299
|
-
}
|
|
7504
|
+
function walk(node, context, ancestors, options, out) {
|
|
7505
|
+
emitLiteral(node, context, ancestors, options, out);
|
|
7506
|
+
const keys = options.visitorKeys[node.type];
|
|
7507
|
+
if (!keys) {
|
|
7508
|
+
return;
|
|
7300
7509
|
}
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
const
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
srcReuse.push({
|
|
7309
|
-
test: testLoc,
|
|
7310
|
-
kind,
|
|
7311
|
-
value,
|
|
7312
|
-
src: srcLocs,
|
|
7313
|
-
remediation: REMEDIATION.IMPORT_FROM_SOURCE
|
|
7314
|
-
});
|
|
7315
|
-
}
|
|
7316
|
-
} else if (byFile.size >= 2) {
|
|
7317
|
-
for (let i = 0; i < allTestLocs.length; i += 1) {
|
|
7318
|
-
const otherTests = [...allTestLocs.slice(0, i), ...allTestLocs.slice(i + 1)];
|
|
7319
|
-
testDupe.push({
|
|
7320
|
-
test: allTestLocs[i],
|
|
7321
|
-
kind,
|
|
7322
|
-
value,
|
|
7323
|
-
otherTests,
|
|
7324
|
-
remediation: REMEDIATION.REFACTOR_TO_SOURCE_OR_GENERATOR
|
|
7325
|
-
});
|
|
7510
|
+
const skip = MODULE_NAMING_SKIP[node.type] ?? EMPTY_SKIP;
|
|
7511
|
+
for (const key of keys) {
|
|
7512
|
+
if (skip.has(key)) continue;
|
|
7513
|
+
const child = node[key];
|
|
7514
|
+
if (Array.isArray(child)) {
|
|
7515
|
+
for (const item of child) {
|
|
7516
|
+
if (isNode(item)) walkChild(item, node, context, ancestors, options, out);
|
|
7326
7517
|
}
|
|
7518
|
+
} else if (isNode(child)) {
|
|
7519
|
+
walkChild(child, node, context, ancestors, options, out);
|
|
7327
7520
|
}
|
|
7328
7521
|
}
|
|
7329
|
-
return { srcReuse, testDupe };
|
|
7330
|
-
}
|
|
7331
|
-
|
|
7332
|
-
// src/validation/literal/walker.ts
|
|
7333
|
-
var TYPESCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx"]);
|
|
7334
|
-
var DECLARATION_SUFFIX = ".d.ts";
|
|
7335
|
-
function isTypescriptSource(path6) {
|
|
7336
|
-
if (path6.endsWith(DECLARATION_SUFFIX)) return false;
|
|
7337
|
-
const ext = extensionOf(path6);
|
|
7338
|
-
return TYPESCRIPT_EXTENSIONS.has(ext);
|
|
7339
|
-
}
|
|
7340
|
-
function extensionOf(name) {
|
|
7341
|
-
const idx = name.lastIndexOf(".");
|
|
7342
|
-
return idx === -1 ? "" : name.slice(idx);
|
|
7343
7522
|
}
|
|
7344
|
-
function
|
|
7345
|
-
|
|
7523
|
+
function walkChild(node, parent, context, ancestors, options, out) {
|
|
7524
|
+
ancestors.push({ node: parent });
|
|
7525
|
+
try {
|
|
7526
|
+
walk(node, context, ancestors, options, out);
|
|
7527
|
+
} finally {
|
|
7528
|
+
ancestors.pop();
|
|
7529
|
+
}
|
|
7346
7530
|
}
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
var PATH_PREFIX_SEPARATOR2 = "/";
|
|
7350
|
-
var DEFAULT_LITERAL_COLLECT_OPTIONS = {
|
|
7351
|
-
visitorKeys: defaultVisitorKeys,
|
|
7352
|
-
minStringLength: literalConfigDescriptor.defaults.minStringLength,
|
|
7353
|
-
minNumberDigits: literalConfigDescriptor.defaults.minNumberDigits
|
|
7354
|
-
};
|
|
7355
|
-
function normalizePathPrefix2(prefix) {
|
|
7356
|
-
const posix = prefix.split(/[\\/]/g).join(PATH_PREFIX_SEPARATOR2);
|
|
7357
|
-
return posix.endsWith(PATH_PREFIX_SEPARATOR2) ? posix : `${posix}${PATH_PREFIX_SEPARATOR2}`;
|
|
7531
|
+
function isNode(value) {
|
|
7532
|
+
return typeof value === "object" && value !== null && typeof value.type === "string";
|
|
7358
7533
|
}
|
|
7359
|
-
function
|
|
7360
|
-
if (
|
|
7361
|
-
return
|
|
7534
|
+
function emitLiteral(node, context, ancestors, options, out) {
|
|
7535
|
+
if (node.type !== LITERAL_TYPE && node.type !== TEMPLATE_ELEMENT_TYPE) {
|
|
7536
|
+
return;
|
|
7362
7537
|
}
|
|
7363
|
-
|
|
7364
|
-
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
const request = input.files ? {
|
|
7378
|
-
explicit: input.files.map((f) => {
|
|
7379
|
-
const abs = isAbsolute5(f) ? f : resolve5(input.productDir, f);
|
|
7380
|
-
return relative5(input.productDir, abs).split(/[\\/]/g).join("/");
|
|
7381
|
-
})
|
|
7382
|
-
} : { walkRoot: input.productDir };
|
|
7383
|
-
const scope = await runPipeline(
|
|
7384
|
-
[artifactDirectoryLayer, hiddenPrefixLayer],
|
|
7385
|
-
input.productDir,
|
|
7386
|
-
request,
|
|
7387
|
-
DEFAULT_SCOPE_CONFIG,
|
|
7388
|
-
EMPTY_IGNORE_READER
|
|
7389
|
-
);
|
|
7390
|
-
const filtered = applyPathFilter(scope.included, input.pathConfig);
|
|
7391
|
-
const candidateFiles = filtered.filter((entry) => isTypescriptSource(entry.path)).map((entry) => resolve5(input.productDir, entry.path));
|
|
7392
|
-
const collectOptions = {
|
|
7393
|
-
visitorKeys: defaultVisitorKeys,
|
|
7394
|
-
minStringLength: config.minStringLength,
|
|
7395
|
-
minNumberDigits: config.minNumberDigits
|
|
7396
|
-
};
|
|
7397
|
-
const srcOccurrences = [];
|
|
7398
|
-
const testOccurrencesByFile = /* @__PURE__ */ new Map();
|
|
7399
|
-
const indexedOccurrencesByFile = /* @__PURE__ */ new Map();
|
|
7400
|
-
for (const abs of candidateFiles) {
|
|
7401
|
-
const rel = relative5(input.productDir, abs).split(/[\\/]/g).join("/");
|
|
7402
|
-
const content = await readSafe(abs);
|
|
7403
|
-
if (content === null) continue;
|
|
7404
|
-
const occurrences = collectLiterals(content, rel, collectOptions);
|
|
7405
|
-
indexedOccurrencesByFile.set(rel, occurrences);
|
|
7406
|
-
if (isTestFile(rel)) {
|
|
7407
|
-
testOccurrencesByFile.set(rel, occurrences);
|
|
7408
|
-
} else {
|
|
7409
|
-
srcOccurrences.push(...occurrences);
|
|
7538
|
+
if (isFixtureDataLiteral(context, ancestors)) {
|
|
7539
|
+
return;
|
|
7540
|
+
}
|
|
7541
|
+
const line = node.loc?.start?.line ?? 0;
|
|
7542
|
+
if (node.type === LITERAL_TYPE) {
|
|
7543
|
+
if (typeof node.value === "string") {
|
|
7544
|
+
if (node.value.length >= options.minStringLength) {
|
|
7545
|
+
out.push({ kind: "string", value: node.value, loc: { file: context.filename, line } });
|
|
7546
|
+
}
|
|
7547
|
+
} else if (typeof node.value === "number") {
|
|
7548
|
+
const raw = typeof node.raw === "string" ? node.raw : String(node.value);
|
|
7549
|
+
if (isMeaningfulNumber(raw, options.minNumberDigits)) {
|
|
7550
|
+
out.push({ kind: "number", value: String(node.value), loc: { file: context.filename, line } });
|
|
7551
|
+
}
|
|
7410
7552
|
}
|
|
7553
|
+
return;
|
|
7411
7554
|
}
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
});
|
|
7418
|
-
return { findings, indexedOccurrencesByFile };
|
|
7419
|
-
}
|
|
7420
|
-
async function readSafe(path6) {
|
|
7421
|
-
try {
|
|
7422
|
-
return await readFile9(path6, "utf8");
|
|
7423
|
-
} catch (err) {
|
|
7424
|
-
if (typeof err === "object" && err !== null && "code" in err) {
|
|
7425
|
-
const code = err.code;
|
|
7426
|
-
if (code === "ENOENT" || code === "EISDIR") return null;
|
|
7555
|
+
if (node.type === TEMPLATE_ELEMENT_TYPE) {
|
|
7556
|
+
const value = node.value;
|
|
7557
|
+
const cooked = value?.cooked ?? "";
|
|
7558
|
+
if (cooked.length >= options.minStringLength) {
|
|
7559
|
+
out.push({ kind: "string", value: cooked, loc: { file: context.filename, line } });
|
|
7427
7560
|
}
|
|
7428
|
-
throw err;
|
|
7429
7561
|
}
|
|
7430
7562
|
}
|
|
7431
|
-
|
|
7432
|
-
|
|
7433
|
-
var LITERAL_PROBLEM_KIND = {
|
|
7434
|
-
REUSE: "reuse",
|
|
7435
|
-
DUPE: "dupe"
|
|
7436
|
-
};
|
|
7437
|
-
var OUTPUT_MODE_NAME = {
|
|
7438
|
-
TEXT: "text",
|
|
7439
|
-
VERBOSE: "verbose",
|
|
7440
|
-
FILES_WITH_PROBLEMS: "filesWithProblems",
|
|
7441
|
-
LITERALS: "literals",
|
|
7442
|
-
JSON: "json"
|
|
7443
|
-
};
|
|
7444
|
-
var OUTPUT_MODE_NAMES = Object.values(OUTPUT_MODE_NAME);
|
|
7445
|
-
var LITERAL_EXIT_CODES = {
|
|
7446
|
-
OK: 0,
|
|
7447
|
-
FINDINGS: 1,
|
|
7448
|
-
CONFIG_ERROR: 2
|
|
7449
|
-
};
|
|
7450
|
-
var TYPESCRIPT_ABSENT_MESSAGE3 = "\u23ED Skipping Literal (TypeScript not detected in project)";
|
|
7451
|
-
var LITERAL_DISABLED_MESSAGE = "\u23ED Skipping Literal (disabled by validation.literal.enabled)";
|
|
7452
|
-
var NO_PROBLEMS_MESSAGE = "Literal: \u2713 No problems";
|
|
7453
|
-
function formatNoProblemsOfKind(kind) {
|
|
7454
|
-
return `Literal: No problems of type ${kind}`;
|
|
7563
|
+
function isTestLikeFile(filename) {
|
|
7564
|
+
return filename.includes(TEST_PATH_SEGMENT) || filename.includes(WINDOWS_TEST_PATH_SEGMENT) || filename.includes(TEST_FILE_MARKER);
|
|
7455
7565
|
}
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
if (!tsDetection.present) {
|
|
7460
|
-
return {
|
|
7461
|
-
exitCode: LITERAL_EXIT_CODES.OK,
|
|
7462
|
-
output: options.quiet ? "" : TYPESCRIPT_ABSENT_MESSAGE3,
|
|
7463
|
-
durationMs: Date.now() - start
|
|
7464
|
-
};
|
|
7465
|
-
}
|
|
7466
|
-
let resolvedEnabled;
|
|
7467
|
-
let resolvedLiteralConfig;
|
|
7468
|
-
let resolvedPathConfig;
|
|
7469
|
-
if (options.config !== void 0) {
|
|
7470
|
-
resolvedEnabled = options.enabled ?? validationConfigDescriptor.defaults.literal.enabled;
|
|
7471
|
-
resolvedLiteralConfig = options.config;
|
|
7472
|
-
resolvedPathConfig = options.pathConfig ?? validationConfigDescriptor.defaults.paths;
|
|
7473
|
-
} else {
|
|
7474
|
-
const loaded = await resolveConfig(options.cwd, [validationConfigDescriptor]);
|
|
7475
|
-
if (!loaded.ok) {
|
|
7476
|
-
return {
|
|
7477
|
-
exitCode: LITERAL_EXIT_CODES.CONFIG_ERROR,
|
|
7478
|
-
output: `Literal: \u2717 config error \u2014 ${loaded.error}`,
|
|
7479
|
-
durationMs: Date.now() - start
|
|
7480
|
-
};
|
|
7481
|
-
}
|
|
7482
|
-
const validationConfig = loaded.value[validationConfigDescriptor.section];
|
|
7483
|
-
resolvedEnabled = validationConfig.literal.enabled;
|
|
7484
|
-
resolvedLiteralConfig = validationConfig.literal.values;
|
|
7485
|
-
resolvedPathConfig = validationPathFilterForTool(
|
|
7486
|
-
validationConfig.paths,
|
|
7487
|
-
VALIDATION_PATH_TOOL_SUBSECTIONS.LITERAL
|
|
7488
|
-
);
|
|
7566
|
+
function isFixtureDataLiteral(context, ancestors) {
|
|
7567
|
+
if (!context.isTestFixtureFile) {
|
|
7568
|
+
return false;
|
|
7489
7569
|
}
|
|
7490
|
-
|
|
7491
|
-
|
|
7492
|
-
|
|
7493
|
-
|
|
7494
|
-
|
|
7495
|
-
|
|
7570
|
+
return isInsideFixtureWriterArgument(ancestors) || isInsideFixtureDataVariable(ancestors);
|
|
7571
|
+
}
|
|
7572
|
+
function isInsideFixtureWriterArgument(ancestors) {
|
|
7573
|
+
for (let index = ancestors.length - 1; index >= 0; index -= 1) {
|
|
7574
|
+
const ancestor = ancestors[index];
|
|
7575
|
+
if (ancestor.node.type !== CALL_EXPRESSION_TYPE) {
|
|
7576
|
+
continue;
|
|
7577
|
+
}
|
|
7578
|
+
const callName = getCallName(ancestor.node);
|
|
7579
|
+
if (callName === void 0 || !isFixtureWriterCall(callName)) {
|
|
7580
|
+
continue;
|
|
7581
|
+
}
|
|
7582
|
+
if (hasNestedFunctionBetween(ancestors, index)) {
|
|
7583
|
+
continue;
|
|
7584
|
+
}
|
|
7585
|
+
return true;
|
|
7496
7586
|
}
|
|
7497
|
-
|
|
7498
|
-
productDir: options.cwd,
|
|
7499
|
-
files: options.files,
|
|
7500
|
-
config: resolvedLiteralConfig,
|
|
7501
|
-
pathConfig: resolvedPathConfig
|
|
7502
|
-
});
|
|
7503
|
-
const filteredFindings = filterLiteralFindings(result.findings, options.kind);
|
|
7504
|
-
const totalProblems = countLiteralProblems(filteredFindings);
|
|
7505
|
-
const exitCode = totalProblems === 0 ? LITERAL_EXIT_CODES.OK : LITERAL_EXIT_CODES.FINDINGS;
|
|
7506
|
-
const output = options.json ? JSON.stringify(filteredFindings) : options.quiet ? "" : formatLiteralCommandOutput(filteredFindings, options);
|
|
7507
|
-
return { exitCode, output, durationMs: Date.now() - start };
|
|
7587
|
+
return false;
|
|
7508
7588
|
}
|
|
7509
|
-
function
|
|
7510
|
-
return
|
|
7511
|
-
srcReuse: kind === LITERAL_PROBLEM_KIND.DUPE ? [] : sortReuseFindings(findings.srcReuse),
|
|
7512
|
-
testDupe: kind === LITERAL_PROBLEM_KIND.REUSE ? [] : sortDupeFindings(findings.testDupe)
|
|
7513
|
-
};
|
|
7589
|
+
function isFixtureWriterCall(callName) {
|
|
7590
|
+
return FIXTURE_WRITER_CALLS.has(callName);
|
|
7514
7591
|
}
|
|
7515
|
-
function
|
|
7516
|
-
|
|
7592
|
+
function isInsideFixtureDataVariable(ancestors) {
|
|
7593
|
+
for (let index = ancestors.length - 1; index >= 0; index -= 1) {
|
|
7594
|
+
const ancestor = ancestors[index];
|
|
7595
|
+
if (ancestor.node.type !== VARIABLE_DECLARATOR_TYPE) {
|
|
7596
|
+
continue;
|
|
7597
|
+
}
|
|
7598
|
+
if (hasNestedFunctionBetween(ancestors, index)) {
|
|
7599
|
+
continue;
|
|
7600
|
+
}
|
|
7601
|
+
const variableName = getFixtureDataDeclaratorName(ancestor.node);
|
|
7602
|
+
if (variableName !== void 0 && isFixtureDataVariableName(variableName)) {
|
|
7603
|
+
return true;
|
|
7604
|
+
}
|
|
7605
|
+
}
|
|
7606
|
+
return false;
|
|
7517
7607
|
}
|
|
7518
|
-
function
|
|
7519
|
-
|
|
7520
|
-
(
|
|
7521
|
-
|
|
7608
|
+
function hasNestedFunctionBetween(ancestors, ancestorIndex) {
|
|
7609
|
+
for (let index = ancestorIndex + 1; index < ancestors.length; index += 1) {
|
|
7610
|
+
if (FUNCTION_NODE_TYPES.has(ancestors[index].node.type)) {
|
|
7611
|
+
return true;
|
|
7612
|
+
}
|
|
7613
|
+
}
|
|
7614
|
+
return false;
|
|
7522
7615
|
}
|
|
7523
|
-
function
|
|
7524
|
-
const
|
|
7525
|
-
|
|
7526
|
-
|
|
7527
|
-
|
|
7528
|
-
|
|
7529
|
-
"REUSE",
|
|
7530
|
-
findings.srcReuse.map((finding) => ({
|
|
7531
|
-
problemKind: LITERAL_PROBLEM_KIND.REUSE,
|
|
7532
|
-
literalKind: finding.kind,
|
|
7533
|
-
value: finding.value,
|
|
7534
|
-
test: finding.test,
|
|
7535
|
-
related: finding.src
|
|
7536
|
-
}))
|
|
7537
|
-
);
|
|
7538
|
-
appendVerboseSection(
|
|
7539
|
-
lines,
|
|
7540
|
-
"DUPE",
|
|
7541
|
-
findings.testDupe.map((finding) => ({
|
|
7542
|
-
problemKind: LITERAL_PROBLEM_KIND.DUPE,
|
|
7543
|
-
literalKind: finding.kind,
|
|
7544
|
-
value: finding.value,
|
|
7545
|
-
test: finding.test,
|
|
7546
|
-
related: finding.otherTests
|
|
7547
|
-
}))
|
|
7548
|
-
);
|
|
7549
|
-
return lines.join("\n");
|
|
7616
|
+
function getFixtureDataDeclaratorName(node) {
|
|
7617
|
+
const bindingName = getIdentifierName(node.id);
|
|
7618
|
+
if (bindingName !== void 0) {
|
|
7619
|
+
return bindingName;
|
|
7620
|
+
}
|
|
7621
|
+
return getIdentifierName(node.init);
|
|
7550
7622
|
}
|
|
7551
|
-
function
|
|
7552
|
-
|
|
7623
|
+
function isFixtureDataVariableName(variableName) {
|
|
7624
|
+
const segments = splitIdentifierName(variableName);
|
|
7625
|
+
if (segments.length === 0) {
|
|
7626
|
+
return false;
|
|
7627
|
+
}
|
|
7628
|
+
if (segments.some((segment) => FIXTURE_DATA_DIRECT_SEGMENTS.has(segment))) {
|
|
7629
|
+
return true;
|
|
7630
|
+
}
|
|
7631
|
+
if (!segments.some((segment) => FIXTURE_DATA_ROLE_SEGMENTS.has(segment))) {
|
|
7632
|
+
return false;
|
|
7633
|
+
}
|
|
7634
|
+
if (segments.every((segment) => FIXTURE_DATA_ROLE_SEGMENTS.has(segment))) {
|
|
7635
|
+
return true;
|
|
7636
|
+
}
|
|
7637
|
+
const finalSegment = segments[segments.length - 1];
|
|
7638
|
+
return finalSegment !== void 0 && FIXTURE_DATA_CONTEXT_SEGMENTS.has(finalSegment);
|
|
7553
7639
|
}
|
|
7554
|
-
function
|
|
7555
|
-
|
|
7556
|
-
|
|
7557
|
-
|
|
7558
|
-
|
|
7559
|
-
|
|
7560
|
-
|
|
7640
|
+
function splitIdentifierName(variableName) {
|
|
7641
|
+
return variableName.split("_").flatMap((part) => part.match(IDENTIFIER_SEGMENT_PATTERN) ?? []).map((segment) => segment.toLowerCase());
|
|
7642
|
+
}
|
|
7643
|
+
function getCallName(node) {
|
|
7644
|
+
const callee = node.callee;
|
|
7645
|
+
if (!isNode(callee)) {
|
|
7646
|
+
return void 0;
|
|
7561
7647
|
}
|
|
7562
|
-
|
|
7648
|
+
if (callee.type === IDENTIFIER_TYPE) {
|
|
7649
|
+
return getIdentifierName(callee);
|
|
7650
|
+
}
|
|
7651
|
+
if (callee.type !== MEMBER_EXPRESSION_TYPE) {
|
|
7652
|
+
return void 0;
|
|
7653
|
+
}
|
|
7654
|
+
const property = callee.property;
|
|
7655
|
+
if (isNode(property)) {
|
|
7656
|
+
return property.type === IDENTIFIER_TYPE ? getIdentifierName(property) : getLiteralString(property);
|
|
7657
|
+
}
|
|
7658
|
+
return void 0;
|
|
7563
7659
|
}
|
|
7564
|
-
function
|
|
7565
|
-
|
|
7566
|
-
|
|
7567
|
-
return formatNoProblemsOfKind(options.kind);
|
|
7660
|
+
function getIdentifierName(value) {
|
|
7661
|
+
if (!isNode(value) || value.type !== IDENTIFIER_TYPE) {
|
|
7662
|
+
return void 0;
|
|
7568
7663
|
}
|
|
7569
|
-
|
|
7570
|
-
|
|
7664
|
+
return typeof value.name === "string" ? value.name : void 0;
|
|
7665
|
+
}
|
|
7666
|
+
function getLiteralString(value) {
|
|
7667
|
+
if (!isNode(value) || value.type !== LITERAL_TYPE) {
|
|
7668
|
+
return void 0;
|
|
7571
7669
|
}
|
|
7572
|
-
|
|
7573
|
-
if (options.literals) return formatLiteralValues(findings);
|
|
7574
|
-
if (options.verbose) return formatVerboseLiteralProblems(findings);
|
|
7575
|
-
return formatDefaultLiteralProblems(findings);
|
|
7670
|
+
return typeof value.value === "string" ? value.value : void 0;
|
|
7576
7671
|
}
|
|
7577
|
-
function
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
problemKind: LITERAL_PROBLEM_KIND.REUSE,
|
|
7581
|
-
literalKind: finding.kind,
|
|
7582
|
-
value: finding.value,
|
|
7583
|
-
test: finding.test,
|
|
7584
|
-
related: finding.src
|
|
7585
|
-
})),
|
|
7586
|
-
...sortDupeFindings(findings.testDupe).map((finding) => ({
|
|
7587
|
-
problemKind: LITERAL_PROBLEM_KIND.DUPE,
|
|
7588
|
-
literalKind: finding.kind,
|
|
7589
|
-
value: finding.value,
|
|
7590
|
-
test: finding.test,
|
|
7591
|
-
related: finding.otherTests
|
|
7592
|
-
}))
|
|
7593
|
-
];
|
|
7672
|
+
function isMeaningfulNumber(raw, minDigits) {
|
|
7673
|
+
const digits = raw.replace(/[^0-9]/g, "");
|
|
7674
|
+
return digits.length >= minDigits;
|
|
7594
7675
|
}
|
|
7595
|
-
function
|
|
7596
|
-
const
|
|
7597
|
-
|
|
7598
|
-
|
|
7599
|
-
|
|
7600
|
-
|
|
7601
|
-
|
|
7602
|
-
|
|
7603
|
-
|
|
7676
|
+
function buildIndex(occurrences) {
|
|
7677
|
+
const map = /* @__PURE__ */ new Map();
|
|
7678
|
+
for (const occ of occurrences) {
|
|
7679
|
+
const key = makeKey(occ.kind, occ.value);
|
|
7680
|
+
const existing = map.get(key);
|
|
7681
|
+
if (existing) {
|
|
7682
|
+
existing.push(occ.loc);
|
|
7683
|
+
} else {
|
|
7684
|
+
map.set(key, [occ.loc]);
|
|
7604
7685
|
}
|
|
7605
|
-
lines.push(
|
|
7606
|
-
` line ${problem.test.line}: ${formatLiteralValue(problem.literalKind, problem.value)} also in ${problem.related.map(formatLoc).join(", ")}`
|
|
7607
|
-
);
|
|
7608
7686
|
}
|
|
7687
|
+
return map;
|
|
7609
7688
|
}
|
|
7610
|
-
function
|
|
7611
|
-
return
|
|
7689
|
+
function makeKey(kind, value) {
|
|
7690
|
+
return `${kind}\0${value}`;
|
|
7612
7691
|
}
|
|
7613
|
-
function
|
|
7614
|
-
|
|
7692
|
+
function splitKey(key) {
|
|
7693
|
+
const idx = key.indexOf("\0");
|
|
7694
|
+
return { kind: key.slice(0, idx), value: key.slice(idx + 1) };
|
|
7615
7695
|
}
|
|
7616
|
-
function
|
|
7617
|
-
|
|
7696
|
+
function detectReuse(input) {
|
|
7697
|
+
const srcReuse = [];
|
|
7698
|
+
const testDupe = [];
|
|
7699
|
+
const testIndex = /* @__PURE__ */ new Map();
|
|
7700
|
+
for (const [file, occurrences] of input.testOccurrencesByFile) {
|
|
7701
|
+
for (const occ of occurrences) {
|
|
7702
|
+
if (input.allowlist.has(occ.value)) continue;
|
|
7703
|
+
const key = makeKey(occ.kind, occ.value);
|
|
7704
|
+
let byFile = testIndex.get(key);
|
|
7705
|
+
if (!byFile) {
|
|
7706
|
+
byFile = /* @__PURE__ */ new Map();
|
|
7707
|
+
testIndex.set(key, byFile);
|
|
7708
|
+
}
|
|
7709
|
+
const locsInFile = byFile.get(file);
|
|
7710
|
+
if (locsInFile) locsInFile.push(occ.loc);
|
|
7711
|
+
else byFile.set(file, [occ.loc]);
|
|
7712
|
+
}
|
|
7713
|
+
}
|
|
7714
|
+
for (const [key, byFile] of testIndex) {
|
|
7715
|
+
const { kind, value } = splitKey(key);
|
|
7716
|
+
const srcLocs = input.srcIndex.get(key);
|
|
7717
|
+
const allTestLocs = [];
|
|
7718
|
+
for (const locs of byFile.values()) allTestLocs.push(...locs);
|
|
7719
|
+
if (srcLocs && srcLocs.length > 0) {
|
|
7720
|
+
for (const testLoc of allTestLocs) {
|
|
7721
|
+
srcReuse.push({
|
|
7722
|
+
test: testLoc,
|
|
7723
|
+
kind,
|
|
7724
|
+
value,
|
|
7725
|
+
src: srcLocs,
|
|
7726
|
+
remediation: REMEDIATION.IMPORT_FROM_SOURCE
|
|
7727
|
+
});
|
|
7728
|
+
}
|
|
7729
|
+
} else if (byFile.size >= 2) {
|
|
7730
|
+
for (let i = 0; i < allTestLocs.length; i += 1) {
|
|
7731
|
+
const otherTests = [...allTestLocs.slice(0, i), ...allTestLocs.slice(i + 1)];
|
|
7732
|
+
testDupe.push({
|
|
7733
|
+
test: allTestLocs[i],
|
|
7734
|
+
kind,
|
|
7735
|
+
value,
|
|
7736
|
+
otherTests,
|
|
7737
|
+
remediation: REMEDIATION.REFACTOR_TO_SOURCE_OR_GENERATOR
|
|
7738
|
+
});
|
|
7739
|
+
}
|
|
7740
|
+
}
|
|
7741
|
+
}
|
|
7742
|
+
return { srcReuse, testDupe };
|
|
7618
7743
|
}
|
|
7619
|
-
|
|
7620
|
-
|
|
7744
|
+
|
|
7745
|
+
// src/validation/literal/walker.ts
|
|
7746
|
+
var TYPESCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx"]);
|
|
7747
|
+
var DECLARATION_SUFFIX = ".d.ts";
|
|
7748
|
+
function isTypescriptSource(path6) {
|
|
7749
|
+
if (path6.endsWith(DECLARATION_SUFFIX)) return false;
|
|
7750
|
+
const ext = extensionOf(path6);
|
|
7751
|
+
return TYPESCRIPT_EXTENSIONS.has(ext);
|
|
7621
7752
|
}
|
|
7622
|
-
function
|
|
7623
|
-
|
|
7753
|
+
function extensionOf(name) {
|
|
7754
|
+
const idx = name.lastIndexOf(".");
|
|
7755
|
+
return idx === -1 ? "" : name.slice(idx);
|
|
7624
7756
|
}
|
|
7625
|
-
function
|
|
7626
|
-
return
|
|
7757
|
+
function isTestFile(relPath) {
|
|
7758
|
+
return /\.test\.tsx?$/.test(relPath);
|
|
7627
7759
|
}
|
|
7628
7760
|
|
|
7629
|
-
// src/
|
|
7630
|
-
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
|
|
7634
|
-
|
|
7635
|
-
import { main as markdownlintMain } from "markdownlint-cli2";
|
|
7636
|
-
import relativeLinksRule from "markdownlint-rule-relative-links";
|
|
7637
|
-
var MARKDOWN_DEFAULT_DIRECTORY_NAMES = ["spx", "docs"];
|
|
7638
|
-
var MARKDOWN_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".markdown"]);
|
|
7639
|
-
var MARKDOWN_DIRECTORY_GLOB = "**/*.md";
|
|
7640
|
-
var ENABLED_RULES = {
|
|
7641
|
-
MD001: true,
|
|
7642
|
-
MD003: true,
|
|
7643
|
-
MD009: true,
|
|
7644
|
-
MD010: true,
|
|
7645
|
-
MD025: true,
|
|
7646
|
-
MD047: true
|
|
7647
|
-
};
|
|
7648
|
-
var MD024_DISABLED_DIRECTORIES = ["docs"];
|
|
7649
|
-
var MARKDOWN_CUSTOM_RULE_NAMES = relativeLinksRule.names;
|
|
7650
|
-
var MARKDOWN_VALIDATION_TARGET_KIND = {
|
|
7651
|
-
DIRECTORY: "directory",
|
|
7652
|
-
FILE: "file"
|
|
7653
|
-
};
|
|
7654
|
-
var MARKDOWN_VALIDATION_TARGET_DIAGNOSTICS = {
|
|
7655
|
-
MISSING_OR_UNRELATED_SCOPE: "not an existing directory or markdown file"
|
|
7656
|
-
};
|
|
7657
|
-
var ERROR_LINE_PATTERN = /^(.+?):(\d+)(?::\d+)?\s+(.+)$/;
|
|
7658
|
-
var defaultMarkdownValidationTargetDeps = {
|
|
7659
|
-
statSync: statSync2
|
|
7761
|
+
// src/validation/literal/index.ts
|
|
7762
|
+
var PATH_PREFIX_SEPARATOR2 = "/";
|
|
7763
|
+
var DEFAULT_LITERAL_COLLECT_OPTIONS = {
|
|
7764
|
+
visitorKeys: defaultVisitorKeys,
|
|
7765
|
+
minStringLength: literalConfigDescriptor.defaults.minStringLength,
|
|
7766
|
+
minNumberDigits: literalConfigDescriptor.defaults.minNumberDigits
|
|
7660
7767
|
};
|
|
7661
|
-
function
|
|
7662
|
-
const
|
|
7663
|
-
|
|
7664
|
-
);
|
|
7665
|
-
return {
|
|
7666
|
-
default: false,
|
|
7667
|
-
...ENABLED_RULES,
|
|
7668
|
-
MD024: md024Disabled ? false : { siblings_only: true },
|
|
7669
|
-
customRules: [relativeLinksRule]
|
|
7670
|
-
};
|
|
7671
|
-
}
|
|
7672
|
-
function getDefaultDirectories(projectRoot) {
|
|
7673
|
-
return MARKDOWN_DEFAULT_DIRECTORY_NAMES.map((name) => join17(projectRoot, name)).filter((dir) => existsSync6(dir));
|
|
7768
|
+
function normalizePathPrefix2(prefix) {
|
|
7769
|
+
const posix = prefix.split(/[\\/]/g).join(PATH_PREFIX_SEPARATOR2);
|
|
7770
|
+
return posix.endsWith(PATH_PREFIX_SEPARATOR2) ? posix : `${posix}${PATH_PREFIX_SEPARATOR2}`;
|
|
7674
7771
|
}
|
|
7675
|
-
function
|
|
7676
|
-
if (
|
|
7677
|
-
return
|
|
7678
|
-
}
|
|
7679
|
-
if (hasMarkdownExtension(path6) && isExistingFile(path6, deps)) {
|
|
7680
|
-
return { target: { kind: MARKDOWN_VALIDATION_TARGET_KIND.FILE, path: path6 } };
|
|
7772
|
+
function applyPathFilter(entries, pathConfig) {
|
|
7773
|
+
if (pathConfig === void 0) {
|
|
7774
|
+
return entries;
|
|
7681
7775
|
}
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
|
|
7776
|
+
const includePrefixes = (pathConfig.include ?? []).map(normalizePathPrefix2);
|
|
7777
|
+
const excludePrefixes = (pathConfig.exclude ?? []).map(normalizePathPrefix2);
|
|
7778
|
+
return entries.filter((entry) => {
|
|
7779
|
+
if (includePrefixes.length > 0 && !includePrefixes.some((p) => entry.path.startsWith(p))) {
|
|
7780
|
+
return false;
|
|
7686
7781
|
}
|
|
7687
|
-
|
|
7688
|
-
|
|
7689
|
-
|
|
7690
|
-
|
|
7691
|
-
const reader = createIgnoreSourceReader(projectRoot, {
|
|
7692
|
-
ignoreSourceFilename: IGNORE_SOURCE_FILENAME_DEFAULT,
|
|
7693
|
-
specTreeRootSegment: SPEC_TREE_CONFIG.ROOT_DIRECTORY
|
|
7782
|
+
if (excludePrefixes.some((p) => entry.path.startsWith(p))) {
|
|
7783
|
+
return false;
|
|
7784
|
+
}
|
|
7785
|
+
return true;
|
|
7694
7786
|
});
|
|
7695
|
-
return reader.entries().map((entry) => `${entry.segment}/**`);
|
|
7696
7787
|
}
|
|
7697
|
-
|
|
7698
|
-
|
|
7699
|
-
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
7788
|
+
async function validateLiteralReuse(input) {
|
|
7789
|
+
const config = input.config ?? literalConfigDescriptor.defaults;
|
|
7790
|
+
const request = input.files ? {
|
|
7791
|
+
explicit: input.files.map((f) => {
|
|
7792
|
+
const abs = isAbsolute5(f) ? f : resolve5(input.productDir, f);
|
|
7793
|
+
return relative6(input.productDir, abs).split(/[\\/]/g).join("/");
|
|
7794
|
+
})
|
|
7795
|
+
} : { walkRoot: input.productDir };
|
|
7796
|
+
const scope = await runPipeline(
|
|
7797
|
+
[artifactDirectoryLayer, hiddenPrefixLayer],
|
|
7798
|
+
input.productDir,
|
|
7799
|
+
request,
|
|
7800
|
+
DEFAULT_SCOPE_CONFIG,
|
|
7801
|
+
EMPTY_IGNORE_READER
|
|
7802
|
+
);
|
|
7803
|
+
const filtered = applyPathFilter(scope.included, input.pathConfig);
|
|
7804
|
+
const candidateFiles = filtered.filter((entry) => isTypescriptSource(entry.path)).map((entry) => resolve5(input.productDir, entry.path));
|
|
7805
|
+
const collectOptions = {
|
|
7806
|
+
visitorKeys: defaultVisitorKeys,
|
|
7807
|
+
minStringLength: config.minStringLength,
|
|
7808
|
+
minNumberDigits: config.minNumberDigits
|
|
7711
7809
|
};
|
|
7810
|
+
const srcOccurrences = [];
|
|
7811
|
+
const testOccurrencesByFile = /* @__PURE__ */ new Map();
|
|
7812
|
+
const indexedOccurrencesByFile = /* @__PURE__ */ new Map();
|
|
7813
|
+
for (const abs of candidateFiles) {
|
|
7814
|
+
const rel = relative6(input.productDir, abs).split(/[\\/]/g).join("/");
|
|
7815
|
+
const content = await readSafe(abs);
|
|
7816
|
+
if (content === null) continue;
|
|
7817
|
+
const occurrences = collectLiterals(content, rel, collectOptions);
|
|
7818
|
+
indexedOccurrencesByFile.set(rel, occurrences);
|
|
7819
|
+
if (isTestFile(rel)) {
|
|
7820
|
+
testOccurrencesByFile.set(rel, occurrences);
|
|
7821
|
+
} else {
|
|
7822
|
+
srcOccurrences.push(...occurrences);
|
|
7823
|
+
}
|
|
7824
|
+
}
|
|
7825
|
+
const srcIndex = buildIndex(srcOccurrences);
|
|
7826
|
+
const findings = detectReuse({
|
|
7827
|
+
srcIndex,
|
|
7828
|
+
testOccurrencesByFile,
|
|
7829
|
+
allowlist: resolveAllowlist(config)
|
|
7830
|
+
});
|
|
7831
|
+
return { findings, indexedOccurrencesByFile };
|
|
7712
7832
|
}
|
|
7713
|
-
async function
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
errors.push(...dirErrors);
|
|
7833
|
+
async function readSafe(path6) {
|
|
7834
|
+
try {
|
|
7835
|
+
return await readFile8(path6, "utf8");
|
|
7836
|
+
} catch (err) {
|
|
7837
|
+
if (typeof err === "object" && err !== null && "code" in err) {
|
|
7838
|
+
const code = err.code;
|
|
7839
|
+
if (code === "ENOENT" || code === "EISDIR") return null;
|
|
7840
|
+
}
|
|
7841
|
+
throw err;
|
|
7723
7842
|
}
|
|
7724
|
-
return {
|
|
7725
|
-
success: errors.length === 0,
|
|
7726
|
-
errors
|
|
7727
|
-
};
|
|
7728
7843
|
}
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
|
|
7754
|
-
|
|
7755
|
-
|
|
7756
|
-
|
|
7757
|
-
|
|
7758
|
-
|
|
7844
|
+
|
|
7845
|
+
// src/commands/validation/literal.ts
|
|
7846
|
+
var LITERAL_PROBLEM_KIND = {
|
|
7847
|
+
REUSE: "reuse",
|
|
7848
|
+
DUPE: "dupe"
|
|
7849
|
+
};
|
|
7850
|
+
var OUTPUT_MODE_NAME = {
|
|
7851
|
+
TEXT: "text",
|
|
7852
|
+
VERBOSE: "verbose",
|
|
7853
|
+
FILES_WITH_PROBLEMS: "filesWithProblems",
|
|
7854
|
+
LITERALS: "literals",
|
|
7855
|
+
JSON: "json"
|
|
7856
|
+
};
|
|
7857
|
+
var OUTPUT_MODE_NAMES = Object.values(OUTPUT_MODE_NAME);
|
|
7858
|
+
var LITERAL_EXIT_CODES = {
|
|
7859
|
+
OK: 0,
|
|
7860
|
+
FINDINGS: 1,
|
|
7861
|
+
CONFIG_ERROR: 2
|
|
7862
|
+
};
|
|
7863
|
+
var TYPESCRIPT_ABSENT_MESSAGE3 = "\u23ED Skipping Literal (TypeScript not detected in project)";
|
|
7864
|
+
var LITERAL_DISABLED_MESSAGE = "\u23ED Skipping Literal (disabled by validation.literal.enabled)";
|
|
7865
|
+
var NO_PROBLEMS_MESSAGE = "Literal: \u2713 No problems";
|
|
7866
|
+
function formatNoProblemsOfKind(kind) {
|
|
7867
|
+
return `Literal: No problems of type ${kind}`;
|
|
7868
|
+
}
|
|
7869
|
+
async function literalCommand(options) {
|
|
7870
|
+
const start = Date.now();
|
|
7871
|
+
const tsDetection = detectTypeScript(options.cwd);
|
|
7872
|
+
if (!tsDetection.present) {
|
|
7873
|
+
return {
|
|
7874
|
+
exitCode: LITERAL_EXIT_CODES.OK,
|
|
7875
|
+
output: options.quiet ? "" : TYPESCRIPT_ABSENT_MESSAGE3,
|
|
7876
|
+
durationMs: Date.now() - start
|
|
7877
|
+
};
|
|
7878
|
+
}
|
|
7879
|
+
let resolvedEnabled;
|
|
7880
|
+
let resolvedLiteralConfig;
|
|
7881
|
+
let resolvedPathConfig;
|
|
7882
|
+
if (options.config !== void 0) {
|
|
7883
|
+
resolvedEnabled = options.enabled ?? validationConfigDescriptor.defaults.literal.enabled;
|
|
7884
|
+
resolvedLiteralConfig = options.config;
|
|
7885
|
+
resolvedPathConfig = options.pathConfig ?? validationConfigDescriptor.defaults.paths;
|
|
7886
|
+
} else {
|
|
7887
|
+
const loaded = await resolveConfig(options.cwd, [validationConfigDescriptor]);
|
|
7888
|
+
if (!loaded.ok) {
|
|
7889
|
+
return {
|
|
7890
|
+
exitCode: LITERAL_EXIT_CODES.CONFIG_ERROR,
|
|
7891
|
+
output: `Literal: \u2717 config error \u2014 ${loaded.error}`,
|
|
7892
|
+
durationMs: Date.now() - start
|
|
7893
|
+
};
|
|
7759
7894
|
}
|
|
7895
|
+
const validationConfig = loaded.value[validationConfigDescriptor.section];
|
|
7896
|
+
resolvedEnabled = validationConfig.literal.enabled;
|
|
7897
|
+
resolvedLiteralConfig = validationConfig.literal.values;
|
|
7898
|
+
resolvedPathConfig = validationPathFilterForTool(
|
|
7899
|
+
validationConfig.paths,
|
|
7900
|
+
VALIDATION_PATH_TOOL_SUBSECTIONS.LITERAL
|
|
7901
|
+
);
|
|
7902
|
+
}
|
|
7903
|
+
if (!resolvedEnabled) {
|
|
7904
|
+
return {
|
|
7905
|
+
exitCode: LITERAL_EXIT_CODES.OK,
|
|
7906
|
+
output: options.quiet ? "" : LITERAL_DISABLED_MESSAGE,
|
|
7907
|
+
durationMs: Date.now() - start
|
|
7908
|
+
};
|
|
7909
|
+
}
|
|
7910
|
+
const result = await validateLiteralReuse({
|
|
7911
|
+
productDir: options.cwd,
|
|
7912
|
+
files: options.files,
|
|
7913
|
+
config: resolvedLiteralConfig,
|
|
7914
|
+
pathConfig: resolvedPathConfig
|
|
7760
7915
|
});
|
|
7761
|
-
|
|
7916
|
+
const filteredFindings = filterLiteralFindings(result.findings, options.kind);
|
|
7917
|
+
const totalProblems = countLiteralProblems(filteredFindings);
|
|
7918
|
+
const exitCode = totalProblems === 0 ? LITERAL_EXIT_CODES.OK : LITERAL_EXIT_CODES.FINDINGS;
|
|
7919
|
+
const output = options.json ? JSON.stringify(filteredFindings) : options.quiet ? "" : formatLiteralCommandOutput(filteredFindings, options);
|
|
7920
|
+
return { exitCode, output, durationMs: Date.now() - start };
|
|
7762
7921
|
}
|
|
7763
|
-
function
|
|
7764
|
-
return
|
|
7922
|
+
function filterLiteralFindings(findings, kind) {
|
|
7923
|
+
return {
|
|
7924
|
+
srcReuse: kind === LITERAL_PROBLEM_KIND.DUPE ? [] : sortReuseFindings(findings.srcReuse),
|
|
7925
|
+
testDupe: kind === LITERAL_PROBLEM_KIND.REUSE ? [] : sortDupeFindings(findings.testDupe)
|
|
7926
|
+
};
|
|
7765
7927
|
}
|
|
7766
|
-
function
|
|
7767
|
-
|
|
7768
|
-
if (lastDot < 0) return false;
|
|
7769
|
-
return MARKDOWN_FILE_EXTENSIONS.has(path6.slice(lastDot).toLowerCase());
|
|
7928
|
+
function countLiteralProblems(findings) {
|
|
7929
|
+
return findings.srcReuse.length + findings.testDupe.length;
|
|
7770
7930
|
}
|
|
7771
|
-
function
|
|
7772
|
-
|
|
7773
|
-
|
|
7774
|
-
|
|
7775
|
-
return false;
|
|
7776
|
-
}
|
|
7931
|
+
function formatDefaultLiteralProblems(findings) {
|
|
7932
|
+
return toLiteralProblems(findings).map(
|
|
7933
|
+
(problem) => `[${problem.problemKind}] ${formatLiteralValue(problem.literalKind, problem.value)} ${formatLoc(problem.test)}`
|
|
7934
|
+
).join("\n");
|
|
7777
7935
|
}
|
|
7778
|
-
function
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
|
|
7782
|
-
|
|
7783
|
-
|
|
7936
|
+
function formatVerboseLiteralProblems(findings) {
|
|
7937
|
+
const lines = [
|
|
7938
|
+
`Literal: ${countLiteralProblems(findings)} problems (reuse: ${findings.srcReuse.length}, dupe: ${findings.testDupe.length})`
|
|
7939
|
+
];
|
|
7940
|
+
appendVerboseSection(
|
|
7941
|
+
lines,
|
|
7942
|
+
"REUSE",
|
|
7943
|
+
findings.srcReuse.map((finding) => ({
|
|
7944
|
+
problemKind: LITERAL_PROBLEM_KIND.REUSE,
|
|
7945
|
+
literalKind: finding.kind,
|
|
7946
|
+
value: finding.value,
|
|
7947
|
+
test: finding.test,
|
|
7948
|
+
related: finding.src
|
|
7949
|
+
}))
|
|
7950
|
+
);
|
|
7951
|
+
appendVerboseSection(
|
|
7952
|
+
lines,
|
|
7953
|
+
"DUPE",
|
|
7954
|
+
findings.testDupe.map((finding) => ({
|
|
7955
|
+
problemKind: LITERAL_PROBLEM_KIND.DUPE,
|
|
7956
|
+
literalKind: finding.kind,
|
|
7957
|
+
value: finding.value,
|
|
7958
|
+
test: finding.test,
|
|
7959
|
+
related: finding.otherTests
|
|
7960
|
+
}))
|
|
7961
|
+
);
|
|
7962
|
+
return lines.join("\n");
|
|
7784
7963
|
}
|
|
7785
|
-
function
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
7964
|
+
function formatFilesWithProblems(findings) {
|
|
7965
|
+
return [...new Set(toLiteralProblems(findings).map((problem) => problem.test.file))].sort().join("\n");
|
|
7966
|
+
}
|
|
7967
|
+
function formatLiteralValues(findings) {
|
|
7968
|
+
const values = /* @__PURE__ */ new Map();
|
|
7969
|
+
for (const problem of toLiteralProblems(findings)) {
|
|
7970
|
+
values.set(`${problem.literalKind}\0${problem.value}`, {
|
|
7971
|
+
kind: problem.literalKind,
|
|
7972
|
+
value: problem.value
|
|
7973
|
+
});
|
|
7791
7974
|
}
|
|
7792
|
-
return
|
|
7975
|
+
return [...values.values()].sort((left, right) => left.value.localeCompare(right.value) || left.kind.localeCompare(right.kind)).map((entry) => formatLiteralValue(entry.kind, entry.value)).join("\n");
|
|
7793
7976
|
}
|
|
7794
|
-
|
|
7795
|
-
|
|
7796
|
-
|
|
7797
|
-
|
|
7798
|
-
NO_ISSUES: VALIDATION_COMMAND_OUTPUT.MARKDOWN_NO_ISSUES,
|
|
7799
|
-
SKIPPED_FILE_SCOPE_PREFIX: "Markdown skipped file scope"
|
|
7800
|
-
};
|
|
7801
|
-
var MARKDOWN_CONFIG_ERROR_MESSAGE = `${VALIDATION_STAGE_DISPLAY_NAMES.MARKDOWN}: \u2717 config error`;
|
|
7802
|
-
async function markdownCommand(options) {
|
|
7803
|
-
const { cwd, files, quiet } = options;
|
|
7804
|
-
const startTime = Date.now();
|
|
7805
|
-
const loaded = await resolveConfig(cwd, [validationConfigDescriptor]);
|
|
7806
|
-
if (!loaded.ok) {
|
|
7807
|
-
return {
|
|
7808
|
-
exitCode: 1,
|
|
7809
|
-
output: `${MARKDOWN_CONFIG_ERROR_MESSAGE} \u2014 ${loaded.error}`,
|
|
7810
|
-
durationMs: Date.now() - startTime
|
|
7811
|
-
};
|
|
7977
|
+
function formatLiteralCommandOutput(findings, options) {
|
|
7978
|
+
const totalProblems = countLiteralProblems(findings);
|
|
7979
|
+
if (totalProblems === 0 && options.kind !== void 0) {
|
|
7980
|
+
return formatNoProblemsOfKind(options.kind);
|
|
7812
7981
|
}
|
|
7813
|
-
|
|
7814
|
-
|
|
7815
|
-
validationConfig.paths,
|
|
7816
|
-
VALIDATION_PATH_TOOL_SUBSECTIONS.MARKDOWN
|
|
7817
|
-
);
|
|
7818
|
-
const targetResolutions = files && files.length > 0 ? files.map((filePath) => resolveMarkdownValidationTarget(filePath)) : void 0;
|
|
7819
|
-
const unfilteredTargets = targetResolutions !== void 0 ? targetResolutions.map((resolution) => resolution.target).filter((target) => target !== void 0) : getDefaultDirectories(cwd).map((path6) => ({
|
|
7820
|
-
kind: MARKDOWN_VALIDATION_TARGET_KIND.DIRECTORY,
|
|
7821
|
-
path: path6
|
|
7822
|
-
}));
|
|
7823
|
-
const targets = unfilteredTargets.filter(
|
|
7824
|
-
(target) => pathPassesValidationFilter(relative6(cwd, target.path), pathFilter)
|
|
7825
|
-
);
|
|
7826
|
-
const skippedTargets = targetResolutions === void 0 ? [] : targetResolutions.map((resolution) => resolution.skipped).filter((skipped) => skipped !== void 0);
|
|
7827
|
-
const skippedOutput = quiet ? [] : skippedTargets.map(formatSkippedFileScope);
|
|
7828
|
-
if (targets.length === 0) {
|
|
7829
|
-
const reason = files && files.length > 0 ? VALIDATION_SKIP_LABELS.MARKDOWN_NO_SCOPE_REASON : VALIDATION_SKIP_LABELS.MARKDOWN_NO_DEFAULT_DIRECTORIES_REASON;
|
|
7830
|
-
const output = quiet ? "" : [
|
|
7831
|
-
...skippedOutput,
|
|
7832
|
-
`${VALIDATION_STAGE_DISPLAY_NAMES.MARKDOWN}: skipped (${reason})`
|
|
7833
|
-
].join("\n");
|
|
7834
|
-
return { exitCode: 0, output, durationMs: Date.now() - startTime };
|
|
7982
|
+
if (totalProblems === 0) {
|
|
7983
|
+
return options.filesWithProblems || options.literals || options.verbose ? "" : NO_PROBLEMS_MESSAGE;
|
|
7835
7984
|
}
|
|
7836
|
-
|
|
7837
|
-
|
|
7838
|
-
|
|
7839
|
-
|
|
7840
|
-
|
|
7841
|
-
|
|
7842
|
-
|
|
7843
|
-
|
|
7844
|
-
|
|
7845
|
-
|
|
7846
|
-
|
|
7985
|
+
if (options.filesWithProblems) return formatFilesWithProblems(findings);
|
|
7986
|
+
if (options.literals) return formatLiteralValues(findings);
|
|
7987
|
+
if (options.verbose) return formatVerboseLiteralProblems(findings);
|
|
7988
|
+
return formatDefaultLiteralProblems(findings);
|
|
7989
|
+
}
|
|
7990
|
+
function toLiteralProblems(findings) {
|
|
7991
|
+
return [
|
|
7992
|
+
...sortReuseFindings(findings.srcReuse).map((finding) => ({
|
|
7993
|
+
problemKind: LITERAL_PROBLEM_KIND.REUSE,
|
|
7994
|
+
literalKind: finding.kind,
|
|
7995
|
+
value: finding.value,
|
|
7996
|
+
test: finding.test,
|
|
7997
|
+
related: finding.src
|
|
7998
|
+
})),
|
|
7999
|
+
...sortDupeFindings(findings.testDupe).map((finding) => ({
|
|
8000
|
+
problemKind: LITERAL_PROBLEM_KIND.DUPE,
|
|
8001
|
+
literalKind: finding.kind,
|
|
8002
|
+
value: finding.value,
|
|
8003
|
+
test: finding.test,
|
|
8004
|
+
related: finding.otherTests
|
|
8005
|
+
}))
|
|
8006
|
+
];
|
|
8007
|
+
}
|
|
8008
|
+
function appendVerboseSection(lines, heading, problems) {
|
|
8009
|
+
const sortedProblems = [...problems].sort(compareLiteralProblems);
|
|
8010
|
+
if (sortedProblems.length === 0) return;
|
|
8011
|
+
lines.push(heading);
|
|
8012
|
+
let currentFile;
|
|
8013
|
+
for (const problem of sortedProblems) {
|
|
8014
|
+
if (problem.test.file !== currentFile) {
|
|
8015
|
+
lines.push(problem.test.file);
|
|
8016
|
+
currentFile = problem.test.file;
|
|
8017
|
+
}
|
|
8018
|
+
lines.push(
|
|
8019
|
+
` line ${problem.test.line}: ${formatLiteralValue(problem.literalKind, problem.value)} also in ${problem.related.map(formatLoc).join(", ")}`
|
|
7847
8020
|
);
|
|
7848
|
-
const output = [
|
|
7849
|
-
...skippedOutput,
|
|
7850
|
-
`Markdown: ${result.errors.length} ${MARKDOWN_COMMAND_OUTPUT.ERROR_SUMMARY_SUFFIX}`,
|
|
7851
|
-
...errorLines
|
|
7852
|
-
].join("\n");
|
|
7853
|
-
return { exitCode: 1, output, durationMs };
|
|
7854
8021
|
}
|
|
7855
8022
|
}
|
|
7856
|
-
function
|
|
7857
|
-
return
|
|
8023
|
+
function sortReuseFindings(findings) {
|
|
8024
|
+
return [...findings].sort(compareFindings);
|
|
8025
|
+
}
|
|
8026
|
+
function sortDupeFindings(findings) {
|
|
8027
|
+
return [...findings].sort(compareFindings);
|
|
8028
|
+
}
|
|
8029
|
+
function compareFindings(left, right) {
|
|
8030
|
+
return left.test.file.localeCompare(right.test.file) || left.test.line - right.test.line || left.kind.localeCompare(right.kind) || left.value.localeCompare(right.value);
|
|
8031
|
+
}
|
|
8032
|
+
function compareLiteralProblems(left, right) {
|
|
8033
|
+
return left.test.file.localeCompare(right.test.file) || left.test.line - right.test.line || left.literalKind.localeCompare(right.literalKind) || left.value.localeCompare(right.value);
|
|
8034
|
+
}
|
|
8035
|
+
function formatLiteralValue(kind, value) {
|
|
8036
|
+
return kind === "string" ? `"${value}"` : value;
|
|
8037
|
+
}
|
|
8038
|
+
function formatLoc(loc) {
|
|
8039
|
+
return `${loc.file}:${loc.line}`;
|
|
7858
8040
|
}
|
|
7859
8041
|
|
|
7860
8042
|
// src/validation/steps/typescript.ts
|
|
@@ -8082,42 +8264,119 @@ async function typescriptCommand(options) {
|
|
|
8082
8264
|
}
|
|
8083
8265
|
}
|
|
8084
8266
|
|
|
8267
|
+
// src/validation/languages/typescript.ts
|
|
8268
|
+
var TYPESCRIPT_LANGUAGE_NAME = "typescript";
|
|
8269
|
+
async function runLiteralStage(context) {
|
|
8270
|
+
if (context.skipLiteral) {
|
|
8271
|
+
const skipOutput = context.json ? LITERAL_SKIP_JSON_OUTPUT : LITERAL_SKIP_OUTPUT;
|
|
8272
|
+
return { exitCode: 0, output: context.quiet ? "" : skipOutput };
|
|
8273
|
+
}
|
|
8274
|
+
return literalCommand({
|
|
8275
|
+
cwd: context.cwd,
|
|
8276
|
+
files: context.files,
|
|
8277
|
+
quiet: context.quiet,
|
|
8278
|
+
json: context.json
|
|
8279
|
+
});
|
|
8280
|
+
}
|
|
8281
|
+
var typescriptValidationLanguage = {
|
|
8282
|
+
name: TYPESCRIPT_LANGUAGE_NAME,
|
|
8283
|
+
stages: [
|
|
8284
|
+
{
|
|
8285
|
+
name: VALIDATION_STAGE_DISPLAY_NAMES.CIRCULAR,
|
|
8286
|
+
failsPipeline: true,
|
|
8287
|
+
run: (context) => circularCommand({ cwd: context.cwd, quiet: context.quiet, json: context.json })
|
|
8288
|
+
},
|
|
8289
|
+
{
|
|
8290
|
+
name: VALIDATION_STAGE_DISPLAY_NAMES.KNIP,
|
|
8291
|
+
// Knip is informational: unused-code findings never fail the pipeline.
|
|
8292
|
+
failsPipeline: false,
|
|
8293
|
+
run: (context) => knipCommand({ cwd: context.cwd, quiet: context.quiet, json: context.json })
|
|
8294
|
+
},
|
|
8295
|
+
{
|
|
8296
|
+
name: VALIDATION_STAGE_DISPLAY_NAMES.ESLINT,
|
|
8297
|
+
failsPipeline: true,
|
|
8298
|
+
run: (context) => lintCommand({
|
|
8299
|
+
cwd: context.cwd,
|
|
8300
|
+
scope: context.scope,
|
|
8301
|
+
files: context.files,
|
|
8302
|
+
fix: context.fix,
|
|
8303
|
+
quiet: context.quiet,
|
|
8304
|
+
json: context.json
|
|
8305
|
+
})
|
|
8306
|
+
},
|
|
8307
|
+
{
|
|
8308
|
+
name: VALIDATION_STAGE_DISPLAY_NAMES.TYPESCRIPT,
|
|
8309
|
+
failsPipeline: true,
|
|
8310
|
+
run: (context) => typescriptCommand({
|
|
8311
|
+
cwd: context.cwd,
|
|
8312
|
+
scope: context.scope,
|
|
8313
|
+
files: context.files,
|
|
8314
|
+
quiet: context.quiet,
|
|
8315
|
+
json: context.json
|
|
8316
|
+
})
|
|
8317
|
+
},
|
|
8318
|
+
{
|
|
8319
|
+
name: VALIDATION_STAGE_DISPLAY_NAMES.LITERAL,
|
|
8320
|
+
failsPipeline: true,
|
|
8321
|
+
run: runLiteralStage
|
|
8322
|
+
}
|
|
8323
|
+
]
|
|
8324
|
+
};
|
|
8325
|
+
|
|
8326
|
+
// src/validation/registry.ts
|
|
8327
|
+
var validationRegistry = {
|
|
8328
|
+
languages: [typescriptValidationLanguage, markdownValidationLanguage]
|
|
8329
|
+
};
|
|
8330
|
+
var validationPipelineStages = validationRegistry.languages.flatMap(
|
|
8331
|
+
(language) => language.stages
|
|
8332
|
+
);
|
|
8333
|
+
var VALIDATION_PIPELINE_TOTAL_STEPS = validationPipelineStages.length;
|
|
8334
|
+
|
|
8335
|
+
// src/commands/validation/format.ts
|
|
8336
|
+
var DURATION_THRESHOLD_MS = 1e3;
|
|
8337
|
+
var VALIDATION_SYMBOLS = {
|
|
8338
|
+
SUCCESS: "\u2713",
|
|
8339
|
+
FAILURE: "\u2717"
|
|
8340
|
+
};
|
|
8341
|
+
var VALIDATION_SUMMARY_STATUS = {
|
|
8342
|
+
PASSED: "passed",
|
|
8343
|
+
FAILED: "failed"
|
|
8344
|
+
};
|
|
8345
|
+
function formatDuration(ms) {
|
|
8346
|
+
if (ms < DURATION_THRESHOLD_MS) {
|
|
8347
|
+
return `${ms}ms`;
|
|
8348
|
+
}
|
|
8349
|
+
const seconds = ms / 1e3;
|
|
8350
|
+
return `${seconds.toFixed(1)}s`;
|
|
8351
|
+
}
|
|
8352
|
+
function formatSummary(options) {
|
|
8353
|
+
const { success, totalDurationMs } = options;
|
|
8354
|
+
const symbol = success ? VALIDATION_SYMBOLS.SUCCESS : VALIDATION_SYMBOLS.FAILURE;
|
|
8355
|
+
const status = success ? VALIDATION_SUMMARY_STATUS.PASSED : VALIDATION_SUMMARY_STATUS.FAILED;
|
|
8356
|
+
const duration = formatDuration(totalDurationMs);
|
|
8357
|
+
return `${symbol} Validation ${status} (${duration} total)`;
|
|
8358
|
+
}
|
|
8359
|
+
|
|
8085
8360
|
// src/commands/validation/all.ts
|
|
8086
|
-
var TOTAL_STEPS = VALIDATION_PIPELINE.TOTAL_STEPS;
|
|
8087
8361
|
function formatStepWithTiming(stepNumber, result, quiet) {
|
|
8088
8362
|
if (quiet || !result.output) return "";
|
|
8089
8363
|
const timing = result.durationMs === void 0 ? "" : ` (${formatDuration(result.durationMs)})`;
|
|
8090
|
-
return `[${stepNumber}/${
|
|
8364
|
+
return `[${stepNumber}/${VALIDATION_PIPELINE_TOTAL_STEPS}] ${result.output}${timing}`;
|
|
8091
8365
|
}
|
|
8092
8366
|
async function allCommand(options) {
|
|
8093
8367
|
const { cwd, scope, files, fix, quiet = false, json, skipLiteral = false } = options;
|
|
8094
8368
|
const startTime = Date.now();
|
|
8095
8369
|
const outputs = [];
|
|
8096
8370
|
let hasFailure = false;
|
|
8097
|
-
const
|
|
8098
|
-
|
|
8099
|
-
|
|
8100
|
-
|
|
8101
|
-
|
|
8102
|
-
|
|
8103
|
-
|
|
8104
|
-
|
|
8105
|
-
|
|
8106
|
-
if (lintOutput) outputs.push(lintOutput);
|
|
8107
|
-
if (lintResult.exitCode !== 0) hasFailure = true;
|
|
8108
|
-
const tsResult = await typescriptCommand({ cwd, scope, files, quiet, json });
|
|
8109
|
-
const tsOutput = formatStepWithTiming(4, tsResult, quiet);
|
|
8110
|
-
if (tsOutput) outputs.push(tsOutput);
|
|
8111
|
-
if (tsResult.exitCode !== 0) hasFailure = true;
|
|
8112
|
-
const markdownResult = await markdownCommand({ cwd, files, quiet });
|
|
8113
|
-
const markdownOutput = formatStepWithTiming(5, markdownResult, quiet);
|
|
8114
|
-
if (markdownOutput) outputs.push(markdownOutput);
|
|
8115
|
-
if (markdownResult.exitCode !== 0) hasFailure = true;
|
|
8116
|
-
const literalSkipOutput = json ? LITERAL_SKIP_JSON_OUTPUT : LITERAL_SKIP_OUTPUT;
|
|
8117
|
-
const literalResult = skipLiteral ? { exitCode: 0, output: quiet ? "" : literalSkipOutput } : await literalCommand({ cwd, files, quiet, json });
|
|
8118
|
-
const literalOutput = formatStepWithTiming(6, literalResult, quiet);
|
|
8119
|
-
if (literalOutput) outputs.push(literalOutput);
|
|
8120
|
-
if (literalResult.exitCode !== 0) hasFailure = true;
|
|
8371
|
+
const context = { cwd, scope, files, fix, quiet, json, skipLiteral };
|
|
8372
|
+
let stepNumber = 0;
|
|
8373
|
+
for (const stage of validationPipelineStages) {
|
|
8374
|
+
stepNumber += 1;
|
|
8375
|
+
const result = await stage.run(context);
|
|
8376
|
+
const stepOutput = formatStepWithTiming(stepNumber, result, quiet);
|
|
8377
|
+
if (stepOutput) outputs.push(stepOutput);
|
|
8378
|
+
if (stage.failsPipeline && result.exitCode !== 0) hasFailure = true;
|
|
8379
|
+
}
|
|
8121
8380
|
const totalDurationMs = Date.now() - startTime;
|
|
8122
8381
|
if (!quiet) {
|
|
8123
8382
|
const summary = formatSummary({ success: !hasFailure, totalDurationMs });
|
|
@@ -8268,7 +8527,7 @@ function serializeWithUpdatedInclude(target, include) {
|
|
|
8268
8527
|
return serializeConfigFileSectionsWithSetIn(target, ALLOWLIST_INCLUDE_PATH, [...include]);
|
|
8269
8528
|
}
|
|
8270
8529
|
|
|
8271
|
-
// src/
|
|
8530
|
+
// src/interfaces/cli/validation.ts
|
|
8272
8531
|
var validationCliDefinition = {
|
|
8273
8532
|
domain: {
|
|
8274
8533
|
commandName: "validation",
|
|
@@ -8517,17 +8776,24 @@ var validationDomain = {
|
|
|
8517
8776
|
}
|
|
8518
8777
|
};
|
|
8519
8778
|
|
|
8779
|
+
// src/interfaces/cli/registry.ts
|
|
8780
|
+
var CLI_DOMAINS = [
|
|
8781
|
+
auditDomain,
|
|
8782
|
+
claudeDomain,
|
|
8783
|
+
configDomain,
|
|
8784
|
+
sessionDomain,
|
|
8785
|
+
specDomain,
|
|
8786
|
+
validationDomain
|
|
8787
|
+
];
|
|
8788
|
+
|
|
8520
8789
|
// src/cli.ts
|
|
8521
8790
|
installLifecycle();
|
|
8522
8791
|
var require3 = createRequire2(import.meta.url);
|
|
8523
8792
|
var { version } = require3("../package.json");
|
|
8524
8793
|
var program = new Command();
|
|
8525
8794
|
program.name("spx").description("Fast, deterministic CLI tool for spec workflow management").version(version);
|
|
8526
|
-
|
|
8527
|
-
|
|
8528
|
-
|
|
8529
|
-
sessionDomain.register(program);
|
|
8530
|
-
specDomain.register(program);
|
|
8531
|
-
validationDomain.register(program);
|
|
8795
|
+
for (const domain of CLI_DOMAINS) {
|
|
8796
|
+
domain.register(program);
|
|
8797
|
+
}
|
|
8532
8798
|
program.parse();
|
|
8533
8799
|
//# sourceMappingURL=cli.js.map
|