@codyswann/lisa 2.146.0 → 2.147.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.
Files changed (63) hide show
  1. package/package.json +1 -1
  2. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  3. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  4. package/plugins/lisa-agy/plugin.json +1 -1
  5. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  6. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  7. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  8. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  9. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  10. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  11. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  12. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  13. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  14. package/plugins/lisa-expo-agy/plugin.json +1 -1
  15. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  16. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  17. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  19. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  20. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  24. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  25. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  29. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  30. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  34. package/plugins/lisa-rails-agy/plugin.json +1 -1
  35. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  39. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  40. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  44. package/plugins/lisa-wiki/schema/lisa-wiki-config.schema.json +46 -2
  45. package/plugins/lisa-wiki/scripts/lint-wiki.mjs +137 -0
  46. package/plugins/lisa-wiki/scripts/validate-config.mjs +89 -0
  47. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  48. package/plugins/lisa-wiki-agy/schema/lisa-wiki-config.schema.json +46 -2
  49. package/plugins/lisa-wiki-agy/scripts/lint-wiki.mjs +137 -0
  50. package/plugins/lisa-wiki-agy/scripts/validate-config.mjs +89 -0
  51. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-wiki-copilot/schema/lisa-wiki-config.schema.json +46 -2
  53. package/plugins/lisa-wiki-copilot/scripts/lint-wiki.mjs +137 -0
  54. package/plugins/lisa-wiki-copilot/scripts/validate-config.mjs +89 -0
  55. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-wiki-cursor/schema/lisa-wiki-config.schema.json +46 -2
  57. package/plugins/lisa-wiki-cursor/scripts/lint-wiki.mjs +137 -0
  58. package/plugins/lisa-wiki-cursor/scripts/validate-config.mjs +89 -0
  59. package/plugins/src/wiki/schema/lisa-wiki-config.schema.json +46 -2
  60. package/plugins/src/wiki/scripts/lint-wiki.mjs +137 -0
  61. package/plugins/src/wiki/scripts/validate-config.mjs +89 -0
  62. package/scripts/install-claude-plugins.sh +31 -0
  63. package/all/copy-overwrite/.safety-net.json +0 -25
@@ -22,6 +22,17 @@ const RETENTION = [
22
22
  "external-pointer-only",
23
23
  ];
24
24
  const SENSITIVITY = ["public", "internal", "confidential", "restricted"];
25
+ const REDACTION_ENTITIES = [
26
+ "api_key",
27
+ "bank_account",
28
+ "credit_card",
29
+ "oauth_token",
30
+ "password",
31
+ "private_key",
32
+ "routing_number",
33
+ "ssn",
34
+ ];
35
+ const REDACTION_SCANNERS = ["builtin", "gitleaks", "presidio"];
25
36
  const SOURCE_LAYOUT = ["by-system", "by-category"];
26
37
  const README_MODE = ["rich", "stub", "preserve"];
27
38
 
@@ -49,6 +60,14 @@ function checkType(value, type, label) {
49
60
  );
50
61
  }
51
62
  }
63
+ function checkKnownKeys(object, allowed, label) {
64
+ if (!isObject(object)) return;
65
+ for (const key of Object.keys(object)) {
66
+ if (!allowed.includes(key)) {
67
+ err(`${label}.${key}: unknown field`);
68
+ }
69
+ }
70
+ }
52
71
 
53
72
  if (!fs.existsSync(configPath)) {
54
73
  console.error(`✗ config not found: ${configPath}`);
@@ -123,8 +142,78 @@ if (config.readme !== undefined) {
123
142
  if (config.sensitivity !== undefined) {
124
143
  if (!isObject(config.sensitivity)) err("sensitivity: must be an object");
125
144
  else {
145
+ checkKnownKeys(
146
+ config.sensitivity,
147
+ ["enabled", "default", "redaction"],
148
+ "sensitivity"
149
+ );
126
150
  checkType(config.sensitivity.enabled, "boolean", "sensitivity.enabled");
127
151
  checkEnum(config.sensitivity.default, SENSITIVITY, "sensitivity.default");
152
+ if (config.sensitivity.redaction !== undefined) {
153
+ if (!isObject(config.sensitivity.redaction)) {
154
+ err("sensitivity.redaction: must be an object");
155
+ } else {
156
+ const redaction = config.sensitivity.redaction;
157
+ checkKnownKeys(
158
+ redaction,
159
+ [
160
+ "enabled",
161
+ "scanners",
162
+ "failClosed",
163
+ "requireReview",
164
+ "allowedEntities",
165
+ "blockedEntities",
166
+ ],
167
+ "sensitivity.redaction"
168
+ );
169
+ checkType(
170
+ redaction.enabled,
171
+ "boolean",
172
+ "sensitivity.redaction.enabled"
173
+ );
174
+ checkType(
175
+ redaction.failClosed,
176
+ "boolean",
177
+ "sensitivity.redaction.failClosed"
178
+ );
179
+ checkType(
180
+ redaction.requireReview,
181
+ "boolean",
182
+ "sensitivity.redaction.requireReview"
183
+ );
184
+ if (
185
+ redaction.scanners !== undefined &&
186
+ !(isStringArray(redaction.scanners) && redaction.scanners.length > 0)
187
+ ) {
188
+ err(
189
+ "sensitivity.redaction.scanners: must be a non-empty array of strings"
190
+ );
191
+ }
192
+ const scanners = isStringArray(redaction.scanners)
193
+ ? redaction.scanners
194
+ : [];
195
+ for (const scanner of scanners) {
196
+ checkEnum(
197
+ scanner,
198
+ REDACTION_SCANNERS,
199
+ "sensitivity.redaction.scanners[]"
200
+ );
201
+ }
202
+ for (const key of ["allowedEntities", "blockedEntities"]) {
203
+ if (redaction[key] !== undefined && !isStringArray(redaction[key])) {
204
+ err(`sensitivity.redaction.${key}: must be an array of strings`);
205
+ }
206
+ const entities = isStringArray(redaction[key]) ? redaction[key] : [];
207
+ for (const entity of entities) {
208
+ checkEnum(
209
+ entity,
210
+ REDACTION_ENTITIES,
211
+ `sensitivity.redaction.${key}[]`
212
+ );
213
+ }
214
+ }
215
+ }
216
+ }
128
217
  }
129
218
  }
130
219
  if (config.documentation !== undefined) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.146.0",
3
+ "version": "2.147.0",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -35,10 +35,54 @@
35
35
  },
36
36
  "sensitivity": {
37
37
  "type": "object",
38
- "additionalProperties": true,
38
+ "additionalProperties": false,
39
39
  "properties": {
40
40
  "enabled": { "type": "boolean" },
41
- "default": { "enum": ["public", "internal", "confidential", "restricted"] }
41
+ "default": { "enum": ["public", "internal", "confidential", "restricted"] },
42
+ "redaction": {
43
+ "type": "object",
44
+ "additionalProperties": false,
45
+ "properties": {
46
+ "enabled": { "type": "boolean" },
47
+ "scanners": {
48
+ "type": "array",
49
+ "items": { "enum": ["builtin", "gitleaks", "presidio"] },
50
+ "minItems": 1
51
+ },
52
+ "failClosed": { "type": "boolean" },
53
+ "requireReview": { "type": "boolean" },
54
+ "allowedEntities": {
55
+ "type": "array",
56
+ "items": {
57
+ "enum": [
58
+ "api_key",
59
+ "bank_account",
60
+ "credit_card",
61
+ "oauth_token",
62
+ "password",
63
+ "private_key",
64
+ "routing_number",
65
+ "ssn"
66
+ ]
67
+ }
68
+ },
69
+ "blockedEntities": {
70
+ "type": "array",
71
+ "items": {
72
+ "enum": [
73
+ "api_key",
74
+ "bank_account",
75
+ "credit_card",
76
+ "oauth_token",
77
+ "password",
78
+ "private_key",
79
+ "routing_number",
80
+ "ssn"
81
+ ]
82
+ }
83
+ }
84
+ }
85
+ }
42
86
  }
43
87
  },
44
88
  "sourceRetention": { "enum": ["raw-ok", "sanitized-note-only", "metadata-only", "external-pointer-only"] },
@@ -15,6 +15,7 @@ import { fileURLToPath } from "node:url";
15
15
  import {
16
16
  loadConfig,
17
17
  loadStructure,
18
+ readJsonSafe,
18
19
  pluginRootFrom,
19
20
  walkFiles,
20
21
  parseFrontmatter,
@@ -60,6 +61,14 @@ const rel = p => path.relative(process.cwd(), p);
60
61
  const wrel = p => path.relative(wikiRoot, p);
61
62
  const categories = config?.categories ?? structure.categoryDirs?.default ?? [];
62
63
  const frontmatterRequired = config?.frontmatter !== false;
64
+ const configPath = path.resolve(
65
+ opt("--config") ?? "wiki/lisa-wiki.config.json"
66
+ );
67
+ const localConfigPath = path.join(
68
+ path.dirname(configPath),
69
+ "lisa-wiki.config.local.json"
70
+ );
71
+ const localConfig = readJsonSafe(localConfigPath);
63
72
 
64
73
  const allMd = walkFiles(wikiRoot, { ext: ".md" });
65
74
  const allFiles = walkFiles(wikiRoot);
@@ -71,6 +80,134 @@ const isUnder = (p, dir) => {
71
80
  const isSynthesisPage = p => categories.some(c => isUnder(p, c));
72
81
  const isSourceNote = p => isUnder(p, "sources");
73
82
 
83
+ // --- 0. redaction policy readiness ----------------------------------------
84
+ const availableRedactionScanners = new Set(["builtin"]);
85
+ const committedRedaction = config?.sensitivity?.redaction;
86
+ const localRedaction = localConfig?.sensitivity?.redaction;
87
+ const addUnsafeLocalOverride = message =>
88
+ report.add(
89
+ "redaction-policy",
90
+ "unsafe-local-override",
91
+ "FAIL",
92
+ message,
93
+ path.relative(wikiRoot, localConfigPath)
94
+ );
95
+ const isArraySubset = (candidate, baseline) =>
96
+ candidate.every(value => baseline.includes(value));
97
+ if (committedRedaction?.enabled === true) {
98
+ const scanners = Array.isArray(committedRedaction.scanners)
99
+ ? committedRedaction.scanners
100
+ : [];
101
+ const missingScanners = scanners.filter(
102
+ scanner => !availableRedactionScanners.has(scanner)
103
+ );
104
+ if (scanners.length === 0) {
105
+ report.add(
106
+ "redaction-policy",
107
+ "scanner-missing",
108
+ committedRedaction.failClosed === false ? "WARN" : "FAIL",
109
+ "redaction is enabled but no scanner is selected"
110
+ );
111
+ } else if (missingScanners.length > 0) {
112
+ report.add(
113
+ "redaction-policy",
114
+ "scanner-unavailable",
115
+ committedRedaction.failClosed === false ? "WARN" : "FAIL",
116
+ `redaction scanner unavailable: ${missingScanners.join(", ")}`
117
+ );
118
+ } else {
119
+ report.add(
120
+ "redaction-policy",
121
+ "scanner-available",
122
+ "PASS",
123
+ `redaction scanner available: ${scanners.join(", ")}`
124
+ );
125
+ }
126
+ if (committedRedaction.failClosed !== true) {
127
+ report.add(
128
+ "redaction-policy",
129
+ "fail-closed",
130
+ "WARN",
131
+ "redaction is enabled without failClosed=true"
132
+ );
133
+ }
134
+ if (committedRedaction.requireReview !== true) {
135
+ report.add(
136
+ "redaction-policy",
137
+ "review-required",
138
+ "WARN",
139
+ "redaction is enabled without requireReview=true"
140
+ );
141
+ }
142
+ if (localRedaction?.enabled === false) {
143
+ addUnsafeLocalOverride("local config disables committed redaction policy");
144
+ }
145
+ if (
146
+ committedRedaction.failClosed === true &&
147
+ localRedaction?.failClosed === false
148
+ ) {
149
+ addUnsafeLocalOverride(
150
+ "local config disables committed redaction fail-closed policy"
151
+ );
152
+ }
153
+ if (
154
+ committedRedaction.requireReview === true &&
155
+ localRedaction?.requireReview === false
156
+ ) {
157
+ addUnsafeLocalOverride(
158
+ "local config disables committed redaction review requirement"
159
+ );
160
+ }
161
+ if (localRedaction && Array.isArray(localRedaction.scanners)) {
162
+ const localScanners = localRedaction.scanners;
163
+ const unavailableLocalScanners = localScanners.filter(
164
+ scanner => !availableRedactionScanners.has(scanner)
165
+ );
166
+ const removedScanners = scanners.filter(
167
+ scanner => !localScanners.includes(scanner)
168
+ );
169
+ if (scanners.length > 0 && localScanners.length === 0) {
170
+ addUnsafeLocalOverride(
171
+ "local config removes committed redaction scanners"
172
+ );
173
+ } else if (unavailableLocalScanners.length > 0) {
174
+ addUnsafeLocalOverride(
175
+ `local config selects unavailable redaction scanner: ${unavailableLocalScanners.join(", ")}`
176
+ );
177
+ } else if (removedScanners.length > 0) {
178
+ addUnsafeLocalOverride(
179
+ `local config removes committed redaction scanner: ${removedScanners.join(", ")}`
180
+ );
181
+ }
182
+ }
183
+ if (
184
+ localRedaction &&
185
+ Array.isArray(committedRedaction.allowedEntities) &&
186
+ Array.isArray(localRedaction.allowedEntities) &&
187
+ !isArraySubset(
188
+ localRedaction.allowedEntities,
189
+ committedRedaction.allowedEntities
190
+ )
191
+ ) {
192
+ addUnsafeLocalOverride(
193
+ "local config expands committed redaction allowed entities"
194
+ );
195
+ }
196
+ if (
197
+ localRedaction &&
198
+ Array.isArray(committedRedaction.blockedEntities) &&
199
+ Array.isArray(localRedaction.blockedEntities) &&
200
+ !isArraySubset(
201
+ committedRedaction.blockedEntities,
202
+ localRedaction.blockedEntities
203
+ )
204
+ ) {
205
+ addUnsafeLocalOverride(
206
+ "local config removes committed redaction blocked entities"
207
+ );
208
+ }
209
+ }
210
+
74
211
  // --- A. structure conformance ---------------------------------------------
75
212
  for (const f of structure.requiredFiles ?? []) {
76
213
  report.add(
@@ -22,6 +22,17 @@ const RETENTION = [
22
22
  "external-pointer-only",
23
23
  ];
24
24
  const SENSITIVITY = ["public", "internal", "confidential", "restricted"];
25
+ const REDACTION_ENTITIES = [
26
+ "api_key",
27
+ "bank_account",
28
+ "credit_card",
29
+ "oauth_token",
30
+ "password",
31
+ "private_key",
32
+ "routing_number",
33
+ "ssn",
34
+ ];
35
+ const REDACTION_SCANNERS = ["builtin", "gitleaks", "presidio"];
25
36
  const SOURCE_LAYOUT = ["by-system", "by-category"];
26
37
  const README_MODE = ["rich", "stub", "preserve"];
27
38
 
@@ -49,6 +60,14 @@ function checkType(value, type, label) {
49
60
  );
50
61
  }
51
62
  }
63
+ function checkKnownKeys(object, allowed, label) {
64
+ if (!isObject(object)) return;
65
+ for (const key of Object.keys(object)) {
66
+ if (!allowed.includes(key)) {
67
+ err(`${label}.${key}: unknown field`);
68
+ }
69
+ }
70
+ }
52
71
 
53
72
  if (!fs.existsSync(configPath)) {
54
73
  console.error(`✗ config not found: ${configPath}`);
@@ -123,8 +142,78 @@ if (config.readme !== undefined) {
123
142
  if (config.sensitivity !== undefined) {
124
143
  if (!isObject(config.sensitivity)) err("sensitivity: must be an object");
125
144
  else {
145
+ checkKnownKeys(
146
+ config.sensitivity,
147
+ ["enabled", "default", "redaction"],
148
+ "sensitivity"
149
+ );
126
150
  checkType(config.sensitivity.enabled, "boolean", "sensitivity.enabled");
127
151
  checkEnum(config.sensitivity.default, SENSITIVITY, "sensitivity.default");
152
+ if (config.sensitivity.redaction !== undefined) {
153
+ if (!isObject(config.sensitivity.redaction)) {
154
+ err("sensitivity.redaction: must be an object");
155
+ } else {
156
+ const redaction = config.sensitivity.redaction;
157
+ checkKnownKeys(
158
+ redaction,
159
+ [
160
+ "enabled",
161
+ "scanners",
162
+ "failClosed",
163
+ "requireReview",
164
+ "allowedEntities",
165
+ "blockedEntities",
166
+ ],
167
+ "sensitivity.redaction"
168
+ );
169
+ checkType(
170
+ redaction.enabled,
171
+ "boolean",
172
+ "sensitivity.redaction.enabled"
173
+ );
174
+ checkType(
175
+ redaction.failClosed,
176
+ "boolean",
177
+ "sensitivity.redaction.failClosed"
178
+ );
179
+ checkType(
180
+ redaction.requireReview,
181
+ "boolean",
182
+ "sensitivity.redaction.requireReview"
183
+ );
184
+ if (
185
+ redaction.scanners !== undefined &&
186
+ !(isStringArray(redaction.scanners) && redaction.scanners.length > 0)
187
+ ) {
188
+ err(
189
+ "sensitivity.redaction.scanners: must be a non-empty array of strings"
190
+ );
191
+ }
192
+ const scanners = isStringArray(redaction.scanners)
193
+ ? redaction.scanners
194
+ : [];
195
+ for (const scanner of scanners) {
196
+ checkEnum(
197
+ scanner,
198
+ REDACTION_SCANNERS,
199
+ "sensitivity.redaction.scanners[]"
200
+ );
201
+ }
202
+ for (const key of ["allowedEntities", "blockedEntities"]) {
203
+ if (redaction[key] !== undefined && !isStringArray(redaction[key])) {
204
+ err(`sensitivity.redaction.${key}: must be an array of strings`);
205
+ }
206
+ const entities = isStringArray(redaction[key]) ? redaction[key] : [];
207
+ for (const entity of entities) {
208
+ checkEnum(
209
+ entity,
210
+ REDACTION_ENTITIES,
211
+ `sensitivity.redaction.${key}[]`
212
+ );
213
+ }
214
+ }
215
+ }
216
+ }
128
217
  }
129
218
  }
130
219
  if (config.documentation !== undefined) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "2.146.0",
3
+ "version": "2.147.0",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -35,10 +35,54 @@
35
35
  },
36
36
  "sensitivity": {
37
37
  "type": "object",
38
- "additionalProperties": true,
38
+ "additionalProperties": false,
39
39
  "properties": {
40
40
  "enabled": { "type": "boolean" },
41
- "default": { "enum": ["public", "internal", "confidential", "restricted"] }
41
+ "default": { "enum": ["public", "internal", "confidential", "restricted"] },
42
+ "redaction": {
43
+ "type": "object",
44
+ "additionalProperties": false,
45
+ "properties": {
46
+ "enabled": { "type": "boolean" },
47
+ "scanners": {
48
+ "type": "array",
49
+ "items": { "enum": ["builtin", "gitleaks", "presidio"] },
50
+ "minItems": 1
51
+ },
52
+ "failClosed": { "type": "boolean" },
53
+ "requireReview": { "type": "boolean" },
54
+ "allowedEntities": {
55
+ "type": "array",
56
+ "items": {
57
+ "enum": [
58
+ "api_key",
59
+ "bank_account",
60
+ "credit_card",
61
+ "oauth_token",
62
+ "password",
63
+ "private_key",
64
+ "routing_number",
65
+ "ssn"
66
+ ]
67
+ }
68
+ },
69
+ "blockedEntities": {
70
+ "type": "array",
71
+ "items": {
72
+ "enum": [
73
+ "api_key",
74
+ "bank_account",
75
+ "credit_card",
76
+ "oauth_token",
77
+ "password",
78
+ "private_key",
79
+ "routing_number",
80
+ "ssn"
81
+ ]
82
+ }
83
+ }
84
+ }
85
+ }
42
86
  }
43
87
  },
44
88
  "sourceRetention": { "enum": ["raw-ok", "sanitized-note-only", "metadata-only", "external-pointer-only"] },
@@ -15,6 +15,7 @@ import { fileURLToPath } from "node:url";
15
15
  import {
16
16
  loadConfig,
17
17
  loadStructure,
18
+ readJsonSafe,
18
19
  pluginRootFrom,
19
20
  walkFiles,
20
21
  parseFrontmatter,
@@ -60,6 +61,14 @@ const rel = p => path.relative(process.cwd(), p);
60
61
  const wrel = p => path.relative(wikiRoot, p);
61
62
  const categories = config?.categories ?? structure.categoryDirs?.default ?? [];
62
63
  const frontmatterRequired = config?.frontmatter !== false;
64
+ const configPath = path.resolve(
65
+ opt("--config") ?? "wiki/lisa-wiki.config.json"
66
+ );
67
+ const localConfigPath = path.join(
68
+ path.dirname(configPath),
69
+ "lisa-wiki.config.local.json"
70
+ );
71
+ const localConfig = readJsonSafe(localConfigPath);
63
72
 
64
73
  const allMd = walkFiles(wikiRoot, { ext: ".md" });
65
74
  const allFiles = walkFiles(wikiRoot);
@@ -71,6 +80,134 @@ const isUnder = (p, dir) => {
71
80
  const isSynthesisPage = p => categories.some(c => isUnder(p, c));
72
81
  const isSourceNote = p => isUnder(p, "sources");
73
82
 
83
+ // --- 0. redaction policy readiness ----------------------------------------
84
+ const availableRedactionScanners = new Set(["builtin"]);
85
+ const committedRedaction = config?.sensitivity?.redaction;
86
+ const localRedaction = localConfig?.sensitivity?.redaction;
87
+ const addUnsafeLocalOverride = message =>
88
+ report.add(
89
+ "redaction-policy",
90
+ "unsafe-local-override",
91
+ "FAIL",
92
+ message,
93
+ path.relative(wikiRoot, localConfigPath)
94
+ );
95
+ const isArraySubset = (candidate, baseline) =>
96
+ candidate.every(value => baseline.includes(value));
97
+ if (committedRedaction?.enabled === true) {
98
+ const scanners = Array.isArray(committedRedaction.scanners)
99
+ ? committedRedaction.scanners
100
+ : [];
101
+ const missingScanners = scanners.filter(
102
+ scanner => !availableRedactionScanners.has(scanner)
103
+ );
104
+ if (scanners.length === 0) {
105
+ report.add(
106
+ "redaction-policy",
107
+ "scanner-missing",
108
+ committedRedaction.failClosed === false ? "WARN" : "FAIL",
109
+ "redaction is enabled but no scanner is selected"
110
+ );
111
+ } else if (missingScanners.length > 0) {
112
+ report.add(
113
+ "redaction-policy",
114
+ "scanner-unavailable",
115
+ committedRedaction.failClosed === false ? "WARN" : "FAIL",
116
+ `redaction scanner unavailable: ${missingScanners.join(", ")}`
117
+ );
118
+ } else {
119
+ report.add(
120
+ "redaction-policy",
121
+ "scanner-available",
122
+ "PASS",
123
+ `redaction scanner available: ${scanners.join(", ")}`
124
+ );
125
+ }
126
+ if (committedRedaction.failClosed !== true) {
127
+ report.add(
128
+ "redaction-policy",
129
+ "fail-closed",
130
+ "WARN",
131
+ "redaction is enabled without failClosed=true"
132
+ );
133
+ }
134
+ if (committedRedaction.requireReview !== true) {
135
+ report.add(
136
+ "redaction-policy",
137
+ "review-required",
138
+ "WARN",
139
+ "redaction is enabled without requireReview=true"
140
+ );
141
+ }
142
+ if (localRedaction?.enabled === false) {
143
+ addUnsafeLocalOverride("local config disables committed redaction policy");
144
+ }
145
+ if (
146
+ committedRedaction.failClosed === true &&
147
+ localRedaction?.failClosed === false
148
+ ) {
149
+ addUnsafeLocalOverride(
150
+ "local config disables committed redaction fail-closed policy"
151
+ );
152
+ }
153
+ if (
154
+ committedRedaction.requireReview === true &&
155
+ localRedaction?.requireReview === false
156
+ ) {
157
+ addUnsafeLocalOverride(
158
+ "local config disables committed redaction review requirement"
159
+ );
160
+ }
161
+ if (localRedaction && Array.isArray(localRedaction.scanners)) {
162
+ const localScanners = localRedaction.scanners;
163
+ const unavailableLocalScanners = localScanners.filter(
164
+ scanner => !availableRedactionScanners.has(scanner)
165
+ );
166
+ const removedScanners = scanners.filter(
167
+ scanner => !localScanners.includes(scanner)
168
+ );
169
+ if (scanners.length > 0 && localScanners.length === 0) {
170
+ addUnsafeLocalOverride(
171
+ "local config removes committed redaction scanners"
172
+ );
173
+ } else if (unavailableLocalScanners.length > 0) {
174
+ addUnsafeLocalOverride(
175
+ `local config selects unavailable redaction scanner: ${unavailableLocalScanners.join(", ")}`
176
+ );
177
+ } else if (removedScanners.length > 0) {
178
+ addUnsafeLocalOverride(
179
+ `local config removes committed redaction scanner: ${removedScanners.join(", ")}`
180
+ );
181
+ }
182
+ }
183
+ if (
184
+ localRedaction &&
185
+ Array.isArray(committedRedaction.allowedEntities) &&
186
+ Array.isArray(localRedaction.allowedEntities) &&
187
+ !isArraySubset(
188
+ localRedaction.allowedEntities,
189
+ committedRedaction.allowedEntities
190
+ )
191
+ ) {
192
+ addUnsafeLocalOverride(
193
+ "local config expands committed redaction allowed entities"
194
+ );
195
+ }
196
+ if (
197
+ localRedaction &&
198
+ Array.isArray(committedRedaction.blockedEntities) &&
199
+ Array.isArray(localRedaction.blockedEntities) &&
200
+ !isArraySubset(
201
+ committedRedaction.blockedEntities,
202
+ localRedaction.blockedEntities
203
+ )
204
+ ) {
205
+ addUnsafeLocalOverride(
206
+ "local config removes committed redaction blocked entities"
207
+ );
208
+ }
209
+ }
210
+
74
211
  // --- A. structure conformance ---------------------------------------------
75
212
  for (const f of structure.requiredFiles ?? []) {
76
213
  report.add(