codex-plugin-doctor 1.27.0 → 1.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,170 @@
1
+ import { classifySuppressionRecord, isValidSuppressionFingerprint, validateSuppressionRecord } from "./suppression-record.js";
2
+ export class SuppressionManagementError extends Error {
3
+ code;
4
+ field;
5
+ index;
6
+ indexes;
7
+ constructor(message, details) {
8
+ super(message);
9
+ this.name = "SuppressionManagementError";
10
+ this.code = details.code;
11
+ this.field = details.field;
12
+ this.index = details.index;
13
+ this.indexes = details.indexes;
14
+ }
15
+ }
16
+ function createSuppressionManagementError(message, details) {
17
+ return new SuppressionManagementError(message, details);
18
+ }
19
+ function isPlainObject(value) {
20
+ return typeof value === "object" && value !== null && !Array.isArray(value);
21
+ }
22
+ function readSuppressions(config) {
23
+ if (!("suppressions" in config)) {
24
+ return {
25
+ suppressions: [],
26
+ hasSuppressions: false
27
+ };
28
+ }
29
+ const { suppressions } = config;
30
+ if (!Array.isArray(suppressions)) {
31
+ throw createSuppressionManagementError("Doctor config suppressions must be an array when present.", {
32
+ code: "suppression_non_array"
33
+ });
34
+ }
35
+ return {
36
+ suppressions,
37
+ hasSuppressions: true
38
+ };
39
+ }
40
+ function copyConfigWithSuppressions(config, suppressions) {
41
+ return {
42
+ ...config,
43
+ suppressions
44
+ };
45
+ }
46
+ function buildManagedInvalidSuppression(index, invalidField) {
47
+ return {
48
+ index,
49
+ status: "invalid",
50
+ invalidField
51
+ };
52
+ }
53
+ function buildRemovedSuppressionSummary(suppression) {
54
+ const classified = classifySuppressionRecord(suppression, new Date(0));
55
+ if (classified.status === "invalid") {
56
+ return {
57
+ invalidField: classified.field
58
+ };
59
+ }
60
+ return {
61
+ fingerprint: classified.fingerprint,
62
+ reason: classified.reason,
63
+ expiresAt: classified.expiresAt
64
+ };
65
+ }
66
+ function findDuplicateFingerprintIndex(suppressions, fingerprint) {
67
+ return suppressions.findIndex((suppression) => isPlainObject(suppression) &&
68
+ typeof suppression.fingerprint === "string" &&
69
+ suppression.fingerprint === fingerprint);
70
+ }
71
+ function validateManagedFingerprint(fingerprint) {
72
+ if (!isValidSuppressionFingerprint(fingerprint)) {
73
+ throw createSuppressionManagementError("Suppression fingerprint is invalid.", {
74
+ code: "suppression_invalid_fingerprint"
75
+ });
76
+ }
77
+ return fingerprint;
78
+ }
79
+ function findFingerprintMatches(suppressions, fingerprint) {
80
+ return suppressions.flatMap((suppression, index) => isPlainObject(suppression) &&
81
+ typeof suppression.fingerprint === "string" &&
82
+ suppression.fingerprint === fingerprint
83
+ ? [index]
84
+ : []);
85
+ }
86
+ export function listSuppressions(config, now = new Date()) {
87
+ const { suppressions, hasSuppressions } = readSuppressions(config);
88
+ if (!hasSuppressions) {
89
+ return [];
90
+ }
91
+ return suppressions.map((suppression, index) => {
92
+ const classified = classifySuppressionRecord(suppression, now);
93
+ if (classified.status === "invalid") {
94
+ return buildManagedInvalidSuppression(index, classified.field);
95
+ }
96
+ return {
97
+ index,
98
+ status: classified.status,
99
+ fingerprint: classified.fingerprint,
100
+ reason: classified.reason,
101
+ expiresAt: classified.expiresAt
102
+ };
103
+ });
104
+ }
105
+ export function addSuppression(config, record) {
106
+ const { suppressions } = readSuppressions(config);
107
+ const validation = validateSuppressionRecord(record);
108
+ if (!validation.valid) {
109
+ throw createSuppressionManagementError(`Suppression record is invalid: ${validation.field}.`, {
110
+ code: "suppression_invalid_record",
111
+ field: validation.field
112
+ });
113
+ }
114
+ const duplicateIndex = findDuplicateFingerprintIndex(suppressions, validation.fingerprint);
115
+ if (duplicateIndex >= 0) {
116
+ throw createSuppressionManagementError(`Suppression fingerprint already exists at index ${duplicateIndex}.`, {
117
+ code: "suppression_duplicate_fingerprint",
118
+ index: duplicateIndex
119
+ });
120
+ }
121
+ const suppression = {
122
+ fingerprint: validation.fingerprint,
123
+ reason: validation.reason,
124
+ expiresAt: validation.expiresAt
125
+ };
126
+ const nextSuppressions = [...suppressions, suppression];
127
+ return {
128
+ config: copyConfigWithSuppressions(config, nextSuppressions),
129
+ index: suppressions.length,
130
+ suppression
131
+ };
132
+ }
133
+ export function removeSuppressionByIndex(config, index) {
134
+ const { suppressions } = readSuppressions(config);
135
+ if (!Number.isInteger(index) ||
136
+ index < 0 ||
137
+ index >= suppressions.length) {
138
+ throw createSuppressionManagementError("Suppression index must be an integer within range.", {
139
+ code: "suppression_invalid_index",
140
+ index
141
+ });
142
+ }
143
+ const suppression = suppressions[index];
144
+ const nextSuppressions = [
145
+ ...suppressions.slice(0, index),
146
+ ...suppressions.slice(index + 1)
147
+ ];
148
+ return {
149
+ config: copyConfigWithSuppressions(config, nextSuppressions),
150
+ index,
151
+ suppression: buildRemovedSuppressionSummary(suppression)
152
+ };
153
+ }
154
+ export function removeSuppressionByFingerprint(config, fingerprint) {
155
+ const { suppressions } = readSuppressions(config);
156
+ const validFingerprint = validateManagedFingerprint(fingerprint);
157
+ const matches = findFingerprintMatches(suppressions, validFingerprint);
158
+ if (matches.length === 0) {
159
+ throw createSuppressionManagementError("Suppression fingerprint not found.", {
160
+ code: "suppression_fingerprint_not_found"
161
+ });
162
+ }
163
+ if (matches.length > 1) {
164
+ throw createSuppressionManagementError(`Suppression fingerprint matches multiple suppressions at indexes: ${matches.join(", ")}.`, {
165
+ code: "suppression_fingerprint_ambiguous",
166
+ indexes: matches
167
+ });
168
+ }
169
+ return removeSuppressionByIndex(config, matches[0]);
170
+ }
@@ -0,0 +1,26 @@
1
+ export type SuppressionRecordField = "record" | "fingerprint" | "reason" | "expiresAt";
2
+ export interface ValidSuppressionRecord {
3
+ valid: true;
4
+ fingerprint: string;
5
+ reason: string;
6
+ expiresAt: string;
7
+ }
8
+ export interface InvalidSuppressionRecord {
9
+ valid: false;
10
+ field: SuppressionRecordField;
11
+ }
12
+ type ActiveSuppressionRecord = ValidSuppressionRecord & {
13
+ status: "active";
14
+ };
15
+ type ExpiredSuppressionRecord = ValidSuppressionRecord & {
16
+ status: "expired";
17
+ };
18
+ type InvalidClassifiedSuppressionRecord = InvalidSuppressionRecord & {
19
+ status: "invalid";
20
+ };
21
+ export type ClassifiedSuppressionRecord = ActiveSuppressionRecord | ExpiredSuppressionRecord | InvalidClassifiedSuppressionRecord;
22
+ export declare function isValidSuppressionFingerprint(value: unknown): value is string;
23
+ export declare function validateSuppressionRecord(value: unknown): ValidSuppressionRecord | InvalidSuppressionRecord;
24
+ export declare function isSuppressionExpired(expiresAt: string, now: Date): boolean;
25
+ export declare function classifySuppressionRecord(value: unknown, now: Date): ClassifiedSuppressionRecord;
26
+ export {};
@@ -0,0 +1,62 @@
1
+ function isPlainObject(value) {
2
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3
+ }
4
+ function isRealDate(value) {
5
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
6
+ if (!match) {
7
+ return false;
8
+ }
9
+ const year = Number(match[1]);
10
+ const month = Number(match[2]);
11
+ const day = Number(match[3]);
12
+ const date = new Date(Date.UTC(year, month - 1, day));
13
+ return (date.getUTCFullYear() === year &&
14
+ date.getUTCMonth() === month - 1 &&
15
+ date.getUTCDate() === day);
16
+ }
17
+ export function isValidSuppressionFingerprint(value) {
18
+ return typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
19
+ }
20
+ export function validateSuppressionRecord(value) {
21
+ if (!isPlainObject(value)) {
22
+ return { valid: false, field: "record" };
23
+ }
24
+ if (!isValidSuppressionFingerprint(value.fingerprint)) {
25
+ return { valid: false, field: "fingerprint" };
26
+ }
27
+ if (typeof value.reason !== "string" || value.reason.trim().length === 0) {
28
+ return { valid: false, field: "reason" };
29
+ }
30
+ if (typeof value.expiresAt !== "string" || !isRealDate(value.expiresAt)) {
31
+ return { valid: false, field: "expiresAt" };
32
+ }
33
+ return {
34
+ valid: true,
35
+ fingerprint: value.fingerprint,
36
+ reason: value.reason.trim(),
37
+ expiresAt: value.expiresAt
38
+ };
39
+ }
40
+ export function isSuppressionExpired(expiresAt, now) {
41
+ const expirationBoundary = Date.parse(`${expiresAt}T00:00:00.000Z`) + 86_400_000;
42
+ return now.getTime() >= expirationBoundary;
43
+ }
44
+ export function classifySuppressionRecord(value, now) {
45
+ const validation = validateSuppressionRecord(value);
46
+ if (!validation.valid) {
47
+ return {
48
+ status: "invalid",
49
+ ...validation
50
+ };
51
+ }
52
+ if (isSuppressionExpired(validation.expiresAt, now)) {
53
+ return {
54
+ status: "expired",
55
+ ...validation
56
+ };
57
+ }
58
+ return {
59
+ status: "active",
60
+ ...validation
61
+ };
62
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import type { CheckOptions, CheckResult } from "./domain/types.js";
2
2
  export { applyDoctorConfig, loadDoctorConfig, type DoctorConfig } from "./core/doctor-config.js";
3
+ export { readRawDoctorConfig, resolveDoctorConfigPath, writeRawDoctorConfig, type LoadedDoctorConfig, type RawDoctorConfig } from "./core/doctor-config-store.js";
4
+ export { classifySuppressionRecord, isValidSuppressionFingerprint, isSuppressionExpired, validateSuppressionRecord, type ClassifiedSuppressionRecord, type InvalidSuppressionRecord, type SuppressionRecordField, type ValidSuppressionRecord } from "./core/suppression-record.js";
5
+ export { addSuppression, listSuppressions, removeSuppressionByFingerprint, removeSuppressionByIndex, SuppressionManagementError, type ManagedSuppressionRecord, type SuppressionManagementErrorCode, type SuppressionMutationResult } from "./core/suppression-management.js";
3
6
  export { buildFindingFingerprint, formatFindingFingerprintLine, withFindingFingerprint, withFindingFingerprints } from "./reporting/finding-fingerprint.js";
4
7
  export { buildSecurityAudit, renderSecurityAuditJson, renderSecurityScorecard, type SecurityAudit } from "./security/security-audit.js";
5
8
  export { buildTrustScore, renderTrustScore, renderTrustScoreJson, type BuildTrustScoreOptions, type TrustScoreReport } from "./security/trust-score.js";
package/dist/index.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { validatePlugin } from "./core/validate-plugin.js";
2
2
  export { applyDoctorConfig, loadDoctorConfig } from "./core/doctor-config.js";
3
+ export { readRawDoctorConfig, resolveDoctorConfigPath, writeRawDoctorConfig } from "./core/doctor-config-store.js";
4
+ export { classifySuppressionRecord, isValidSuppressionFingerprint, isSuppressionExpired, validateSuppressionRecord } from "./core/suppression-record.js";
5
+ export { addSuppression, listSuppressions, removeSuppressionByFingerprint, removeSuppressionByIndex, SuppressionManagementError } from "./core/suppression-management.js";
3
6
  export { buildFindingFingerprint, formatFindingFingerprintLine, withFindingFingerprint, withFindingFingerprints } from "./reporting/finding-fingerprint.js";
4
7
  export { buildSecurityAudit, renderSecurityAuditJson, renderSecurityScorecard } from "./security/security-audit.js";
5
8
  export { buildTrustScore, renderTrustScore, renderTrustScoreJson } from "./security/trust-score.js";
@@ -0,0 +1,7 @@
1
+ import type { ManagedSuppressionRecord, SuppressionMutationResult } from "../core/suppression-management.js";
2
+ type SuppressionMutationCommand = "suppress.add" | "suppress.remove";
3
+ export declare function renderSuppressionList(configPath: string, suppressions: ManagedSuppressionRecord[]): string;
4
+ export declare function renderSuppressionListJson(configPath: string, suppressions: ManagedSuppressionRecord[]): string;
5
+ export declare function renderSuppressionMutation(command: SuppressionMutationCommand, configPath: string, result: SuppressionMutationResult): string;
6
+ export declare function renderSuppressionMutationJson(command: SuppressionMutationCommand, configPath: string, result: SuppressionMutationResult): string;
7
+ export {};
@@ -0,0 +1,117 @@
1
+ function isObject(value) {
2
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3
+ }
4
+ function isInvalidField(value) {
5
+ return (value === "record" ||
6
+ value === "fingerprint" ||
7
+ value === "reason" ||
8
+ value === "expiresAt");
9
+ }
10
+ function isValidSuppressionSummary(value) {
11
+ return (isObject(value) &&
12
+ !("invalidField" in value) &&
13
+ typeof value.fingerprint === "string" &&
14
+ typeof value.reason === "string" &&
15
+ value.reason.trim().length > 0 &&
16
+ typeof value.expiresAt === "string");
17
+ }
18
+ function buildListEntry(record) {
19
+ if (record.status === "invalid" && isInvalidField(record.invalidField)) {
20
+ return {
21
+ index: record.index,
22
+ status: record.status,
23
+ invalidField: record.invalidField
24
+ };
25
+ }
26
+ if ((record.status === "active" || record.status === "expired") &&
27
+ isValidSuppressionSummary(record)) {
28
+ return {
29
+ index: record.index,
30
+ status: record.status,
31
+ fingerprint: record.fingerprint,
32
+ reason: record.reason,
33
+ expiresAt: record.expiresAt
34
+ };
35
+ }
36
+ return {
37
+ index: record.index,
38
+ status: "invalid",
39
+ invalidField: "record"
40
+ };
41
+ }
42
+ function buildMutationSummary(value) {
43
+ if (isObject(value) && isInvalidField(value.invalidField)) {
44
+ return {
45
+ invalidField: value.invalidField
46
+ };
47
+ }
48
+ if (isValidSuppressionSummary(value)) {
49
+ return {
50
+ fingerprint: value.fingerprint,
51
+ reason: value.reason,
52
+ expiresAt: value.expiresAt
53
+ };
54
+ }
55
+ return { invalidField: "record" };
56
+ }
57
+ function getActionLabel(command) {
58
+ return command === "suppress.add" ? "Added" : "Removed";
59
+ }
60
+ export function renderSuppressionList(configPath, suppressions) {
61
+ const lines = [
62
+ `Config: ${configPath}`,
63
+ `Total suppressions: ${suppressions.length}`
64
+ ];
65
+ if (suppressions.length === 0) {
66
+ lines.push("", "No suppressions.");
67
+ return lines.join("\n");
68
+ }
69
+ lines.push("", "Suppressions", "------------");
70
+ for (const suppression of suppressions) {
71
+ const entry = buildListEntry(suppression);
72
+ if (entry.status === "invalid") {
73
+ lines.push(`[${entry.index}] INVALID ${entry.invalidField}`);
74
+ continue;
75
+ }
76
+ lines.push(`[${entry.index}] ${entry.status.toUpperCase()} ${entry.fingerprint}`);
77
+ lines.push(` Reason: ${entry.reason}`);
78
+ lines.push(` Expires: ${entry.expiresAt}`);
79
+ }
80
+ return lines.join("\n");
81
+ }
82
+ export function renderSuppressionListJson(configPath, suppressions) {
83
+ return JSON.stringify({
84
+ schemaVersion: "1.0.0",
85
+ kind: "doctor.suppress.list",
86
+ command: "suppress.list",
87
+ configPath,
88
+ suppressions: suppressions.map(buildListEntry)
89
+ }, null, 2);
90
+ }
91
+ export function renderSuppressionMutation(command, configPath, result) {
92
+ const summary = buildMutationSummary(result.suppression);
93
+ const lines = [
94
+ `Action: ${getActionLabel(command)}`,
95
+ `Config: ${configPath}`,
96
+ `Index: ${result.index}`
97
+ ];
98
+ lines.push("");
99
+ if ("invalidField" in summary) {
100
+ lines.push(`Invalid field: ${summary.invalidField}`);
101
+ return lines.join("\n");
102
+ }
103
+ lines.push(`Fingerprint: ${summary.fingerprint}`);
104
+ lines.push(`Reason: ${summary.reason}`);
105
+ lines.push(`Expires: ${summary.expiresAt}`);
106
+ return lines.join("\n");
107
+ }
108
+ export function renderSuppressionMutationJson(command, configPath, result) {
109
+ return JSON.stringify({
110
+ schemaVersion: "1.0.0",
111
+ kind: `doctor.${command}`,
112
+ command,
113
+ configPath,
114
+ index: result.index,
115
+ suppression: buildMutationSummary(result.suppression)
116
+ }, null, 2);
117
+ }
package/dist/run-cli.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { runCheck } from "./index.js";
2
+ import { writeRawDoctorConfig } from "./index.js";
2
3
  export interface CliIo {
3
4
  writeStdout(message: string): void;
4
5
  writeStderr(message: string): void;
@@ -13,6 +14,8 @@ export interface CliTerminalContext {
13
14
  export interface RunCliOptions {
14
15
  terminalContext?: CliTerminalContext;
15
16
  runCheckImpl?: typeof runCheck;
17
+ writeRawDoctorConfigImpl?: typeof writeRawDoctorConfig;
18
+ now?: () => Date;
16
19
  releaseAssetUploadImpl?: (args: string[]) => Promise<void>;
17
20
  resolveLatestVersion?: () => Promise<string>;
18
21
  }