progmune-runtime 2.0.2 → 2.1.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 (107) hide show
  1. package/.mcp.json +11 -0
  2. package/.progmune_allowlist +50 -0
  3. package/.test_report/test_report.md +87 -0
  4. package/Dockerfile +2 -10
  5. package/FAQ.md +167 -0
  6. package/demo-project/auth.ts +55 -0
  7. package/demo-project/tsconfig.json +8 -0
  8. package/dist/ab-stats.js +11 -0
  9. package/dist/acl-breakdown.js +13 -0
  10. package/dist/all-sessions.js +11 -0
  11. package/dist/antibody-stats.js +11 -0
  12. package/dist/audit.js +218 -0
  13. package/dist/benchmark-count.js +7 -0
  14. package/dist/benchmark-full.js +17 -0
  15. package/dist/benchmark-pass-rate.js +54 -0
  16. package/dist/benchmark-report.js +67 -0
  17. package/dist/benchmark-save.js +62 -0
  18. package/dist/benchmark-status.js +15 -0
  19. package/dist/branch-ledger.js +365 -0
  20. package/dist/branch-tree-count.js +14 -0
  21. package/dist/check.js +506 -0
  22. package/dist/common-fixpath.js +12 -0
  23. package/dist/constraint-types.js +12 -0
  24. package/dist/deterministic-replay.js +277 -0
  25. package/dist/emitter.js +112 -12
  26. package/dist/exec-metrics.js +11 -0
  27. package/dist/execute.js +242 -0
  28. package/dist/extract-ir.js +550 -5
  29. package/dist/failure-collector.js +143 -0
  30. package/dist/failure-corpus.js +481 -35
  31. package/dist/failure-report.js +11 -0
  32. package/dist/failures.js +11 -0
  33. package/dist/fast-path-hits.js +13 -0
  34. package/dist/feedback.js +6 -3
  35. package/dist/file-lock.js +82 -0
  36. package/dist/find-session.js +10 -0
  37. package/dist/fingerprint-list.js +15 -0
  38. package/dist/gen-history-log.js +13 -0
  39. package/dist/generate.js +2 -1
  40. package/dist/generate_500.js +4 -2
  41. package/dist/genome.js +11 -0
  42. package/dist/heatmap-data.js +11 -0
  43. package/dist/heatmap.js +11 -0
  44. package/dist/immune-reporter.js +137 -0
  45. package/dist/learned.js +11 -0
  46. package/dist/ledger-registry.js +241 -0
  47. package/dist/llm.js +43 -2
  48. package/dist/load-benchmarks.js +47 -0
  49. package/dist/main.js +2 -1
  50. package/dist/mcp-server.mjs +446 -34
  51. package/dist/memory-layer.js +51 -13
  52. package/dist/metrics.js +11 -0
  53. package/dist/obs-web.js +561 -0
  54. package/dist/p0_ssg_demo.js +255 -40
  55. package/dist/planner.js +933 -65
  56. package/dist/protocol-registry.js +105 -0
  57. package/dist/recent-session.js +12 -0
  58. package/dist/repair-proposal.js +353 -0
  59. package/dist/report.js +32 -0
  60. package/dist/runtime-invariants.js +161 -0
  61. package/dist/runtime-types.js +117 -0
  62. package/dist/search-planner.js +38 -9
  63. package/dist/semantic-snapshot.js +155 -0
  64. package/dist/semantic-trace.js +1497 -0
  65. package/dist/semantic-validator.js +3 -2
  66. package/dist/semantic_guard_test.js +2 -1
  67. package/dist/sessions.js +11 -0
  68. package/dist/ssg-validator.js +658 -20
  69. package/dist/svl-distribution.js +11 -0
  70. package/dist/terminal-status.js +11 -0
  71. package/dist/test_failure_corpus.js +3 -1
  72. package/dist/token-savings.js +11 -0
  73. package/dist/total-repairs.js +12 -0
  74. package/dist/unresolved-count.js +12 -0
  75. package/dist/valid-fingerprints.js +13 -0
  76. package/dist/validator.js +116 -60
  77. package/dist/verify-fps.js +11 -0
  78. package/dist/verify-ledgers.js +11 -0
  79. package/docs/whitepaper-style.css +77 -0
  80. package/docs/whitepaper-v2.1.md +609 -0
  81. package/docs/whitepaper-v2.2.md +1064 -0
  82. package/docs/whitepaper-v2.2.pdf +0 -0
  83. package/fly.toml +1 -1
  84. package/package.json +13 -4
  85. package/protocols.json +136 -0
  86. package/public/dashboard.html +119 -0
  87. package/readme.md +829 -0
  88. package/server/hub.js +84 -12
  89. package/test/replay-golden/sess_1780063202050_mgeld.json +9 -0
  90. package/test/replay-golden/sess_1780064032560_gocld.json +354 -0
  91. package/test/replay-golden/sess_1780064413331_s2709.json +606 -0
  92. package/test/replay-golden/sess_1780064792710_y3avo.json +614 -0
  93. package/test/replay-golden.ts +84 -0
  94. package/test_benchmark.js +165 -0
  95. package/test_comprehensive.mjs +638 -0
  96. package/test_concurrency.js +129 -0
  97. package/test_ir_robustness.js +85 -0
  98. package/test_semantic_contracts.js +269 -0
  99. package/test_ssg_stress.js +156 -0
  100. package/test_svl3.js +58 -0
  101. package/tsconfig.json +1 -1
  102. package/.env.example +0 -3
  103. package/.progmune_memory/fingerprints.json +0 -7
  104. package/.progmune_memory/opt_in.json +0 -4
  105. package/README.md +0 -118
  106. package/dist/mcp-server.js +0 -55
  107. package/immune_hub_data/2026-05-14.json +0 -50
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.main = main;
4
+ // @progmune-generated session=sess_1780343822181_r90g9 timestamp=2026-06-01T19:57:03.488Z ruleHash=9dec68bc2995e92a
5
+ // Generated with IR constraint: 394 functions, 17 protocol rules
6
+ const failure_corpus_1 = require("./failure-corpus");
7
+ function main() {
8
+ const genome = (0, failure_corpus_1.getFailureGenome)();
9
+ return genome;
10
+ }
11
+ main();
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.main = main;
4
+ // @progmune-generated session=sess_1780207842282_uihp3 timestamp=2026-05-31T06:10:44.349Z ruleHash=9dec68bc2995e92a
5
+ // Generated with IR constraint: 306 functions, 17 protocol rules
6
+ const semantic_trace_1 = require("./src/semantic-trace");
7
+ function main() {
8
+ const summary = (0, semantic_trace_1.formatImmuneResponse)({});
9
+ return summary;
10
+ }
11
+ main();
@@ -9,7 +9,9 @@ const failure_corpus_1 = require("./failure-corpus");
9
9
  constraintType: "protocol",
10
10
  actionSequence: [{ kind: "call", function: "generate_jwt" }],
11
11
  errorDetail: "非法调用:generate_jwt 要求前置状态 [AUTHENTICATED],当前状态为 [UNAUTHENTICATED]",
12
- ssgState: "UNAUTHENTICATED",
12
+ ssgState: ["UNAUTHENTICATED"],
13
+ plannerAttempt: 1,
14
+ plannerRetryTotal: 3,
13
15
  });
14
16
  // 打印当前统计
15
17
  console.log("当前失败案例总数:", (0, failure_corpus_1.getAllFailures)().length);
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.main = main;
4
+ // @progmune-generated session=sess_1780343649325_e8j9k timestamp=2026-06-01T19:54:10.484Z ruleHash=9dec68bc2995e92a
5
+ // Generated with IR constraint: 390 functions, 17 protocol rules
6
+ const failure_corpus_1 = require("./failure-corpus");
7
+ function main() {
8
+ const stats = (0, failure_corpus_1.getAntibodyStats)();
9
+ return stats;
10
+ }
11
+ main();
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.main = main;
4
+ // @progmune-generated session=sess_1780343904555_v2jom timestamp=2026-06-01T19:58:26.164Z ruleHash=9dec68bc2995e92a
5
+ // Generated with IR constraint: 397 functions, 17 protocol rules
6
+ const execute_1 = require("./execute");
7
+ function main() {
8
+ const metrics = (0, execute_1.getExecutionMetrics)();
9
+ const totalRepairCount = findIndex();
10
+ return totalRepairCount;
11
+ }
12
+ main();
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.main = main;
4
+ // @progmune-generated session=sess_1780343799715_z6pcw timestamp=2026-06-01T19:56:41.675Z ruleHash=9dec68bc2995e92a
5
+ // Generated with IR constraint: 393 functions, 17 protocol rules
6
+ const failure_corpus_1 = require("./failure-corpus");
7
+ function main() {
8
+ const sessions = (0, failure_corpus_1.getAllSessions)();
9
+ const failures = (0, failure_corpus_1.getAllFailures)();
10
+ return failures;
11
+ }
12
+ main();
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.main = main;
4
+ // @progmune-generated session=sess_1780343878763_2tpv3 timestamp=2026-06-01T19:58:00.794Z ruleHash=9dec68bc2995e92a
5
+ // Generated with IR constraint: 396 functions, 17 protocol rules
6
+ const immune_reporter_1 = require("./immune-reporter");
7
+ const ledger_registry_1 = require("./ledger-registry");
8
+ function main() {
9
+ const fps = (0, immune_reporter_1.extractFingerprints)({});
10
+ const summary = (0, ledger_registry_1.verifyAllFingerprints)("defaultStr");
11
+ return summary;
12
+ }
13
+ main();
package/dist/validator.js CHANGED
@@ -41,7 +41,8 @@ function loadIR() {
41
41
  const irPath = path.resolve(__dirname, "../ir.json");
42
42
  if (!fs.existsSync(irPath))
43
43
  return [];
44
- return JSON.parse(fs.readFileSync(irPath, "utf-8"));
44
+ const raw = JSON.parse(fs.readFileSync(irPath, "utf-8"));
45
+ return Array.isArray(raw) ? raw : (raw.functions || []);
45
46
  }
46
47
  const BUILTIN_WHITELIST = new Set([
47
48
  "console.log", "setTimeout", "setInterval", "clearTimeout",
@@ -69,128 +70,183 @@ function normalizeType(type) {
69
70
  return "any";
70
71
  return t;
71
72
  }
73
+ // ========== 重写的变量流向分析:基于声明追踪的确定性检查 ==========
72
74
  function checkVariableFlow(actions) {
73
75
  const errors = [];
74
76
  const declared = new Map();
75
- for (let i = 0; i < actions.length; i++) {
76
- const action = actions[i];
77
+ const isLiteral = (val) => {
78
+ if (typeof val !== 'string')
79
+ return true;
80
+ if (/^["'`]/.test(val) || /["'`]$/.test(val))
81
+ return true;
82
+ if (/^\d+$/.test(val))
83
+ return true;
84
+ if (val === 'true' || val === 'false' || val === 'null' || val === 'undefined')
85
+ return true;
86
+ if (/\s/.test(val) || /[^\w]/.test(val))
87
+ return true;
88
+ return false;
89
+ };
90
+ const processAction = (action) => {
77
91
  if (action.kind === "call") {
78
- for (const arg of (action.args || [])) {
79
- if (typeof arg.value === "string" && /^[a-zA-Z_]\w*$/.test(arg.value)) {
80
- if (declared.has(arg.value))
81
- continue;
82
- if (arg.value.length < 15 && arg.value[0] === arg.value[0].toLowerCase()) {
83
- continue;
84
- }
85
- }
86
- }
92
+ // call 动作的参数值来自结构化 {name, type, value},都是字面量,不做变量引用检查
87
93
  if (action.assignTo) {
88
- if (action.args?.some(a => a.value === action.assignTo)) {
89
- errors.push(`变量 '${action.assignTo}' 在动作${i}中引用自身`);
90
- }
91
94
  declared.set(action.assignTo, "any");
92
95
  }
93
96
  }
94
97
  else if (action.kind === "assign") {
95
- if (action.target) {
96
- if (typeof action.value === "string" && /^[a-zA-Z_]\w*$/.test(action.value)) {
97
- if (!declared.has(action.value) && action.value.length < 15)
98
- continue;
99
- if (!declared.has(action.value))
100
- errors.push(`赋值时引用未定义变量 '${action.value}'`);
98
+ if (typeof action.value === "string") {
99
+ const val = action.value;
100
+ if (!isLiteral(val) && /^[a-zA-Z_]\w*$/.test(val)) {
101
+ if (!declared.has(val)) {
102
+ errors.push(`变量 '${val}' 在赋值前未声明`);
103
+ }
101
104
  }
105
+ }
106
+ if (action.target) {
102
107
  declared.set(action.target, "any");
103
108
  }
104
109
  }
105
110
  else if (action.kind === "return") {
106
- if (typeof action.value === "string" && /^[a-zA-Z_]\w*$/.test(action.value)) {
107
- if (!declared.has(action.value) && !/^["']/.test(action.value) && action.value.length < 15)
108
- continue;
109
- if (!declared.has(action.value))
110
- errors.push(`返回未定义变量 '${action.value}'`);
111
+ if (typeof action.value === "string") {
112
+ const val = action.value;
113
+ if (!isLiteral(val) && /^[a-zA-Z_]\w*$/.test(val)) {
114
+ if (!declared.has(val)) {
115
+ errors.push(`返回语句引用了未声明的变量 '${val}'`);
116
+ }
117
+ }
111
118
  }
112
119
  }
113
120
  else if (action.kind === "if") {
114
- if (typeof action.condition === "string" && declared.has(action.condition))
115
- continue;
116
- if (typeof action.condition === "string" && /^(true|false)$/.test(action.condition))
117
- continue;
118
- if (typeof action.condition === "string" && action.condition.length < 10) {
119
- // 可能是表达式,放行
121
+ if (typeof action.condition === "string") {
122
+ const cond = action.condition;
123
+ if (!isLiteral(cond) && /^[a-zA-Z_]\w*$/.test(cond)) {
124
+ if (!declared.has(cond) && cond !== 'true' && cond !== 'false') {
125
+ errors.push(`条件中引用了未声明的变量 '${cond}'`);
126
+ }
127
+ }
128
+ }
129
+ for (const a of (action.thenActions || [])) {
130
+ processAction(a);
120
131
  }
121
- else {
122
- errors.push(`条件中引用了未定义的变量 '${action.condition}'`);
132
+ for (const a of (action.elseActions || [])) {
133
+ processAction(a);
123
134
  }
124
- errors.push(...checkVariableFlow(action.thenActions || []));
125
- errors.push(...checkVariableFlow(action.elseActions || []));
126
135
  }
127
136
  else if (action.kind === "for") {
128
- errors.push(...checkVariableFlow(action.bodyActions || []));
137
+ if (action.variable)
138
+ declared.set(action.variable, "any");
139
+ for (const a of (action.bodyActions || [])) {
140
+ processAction(a);
141
+ }
129
142
  }
143
+ };
144
+ for (const action of actions) {
145
+ processAction(action);
130
146
  }
131
147
  return errors;
132
148
  }
133
- function validateAction(action) {
149
+ /**
150
+ * 校验单个动作的合法性(函数存在、类型匹配、参数数量)。
151
+ * @protocol namespace=dev_pipeline pre_states=["IR_EXTRACTED"] post_states=["ACTION_VALIDATED"]
152
+ */
153
+ function validateAction(action, actionIndex) {
134
154
  const functions = loadIR();
135
155
  const errors = [];
156
+ const violations = [];
157
+ const idx = actionIndex ?? 0;
136
158
  if (!action || !["call", "if", "for", "assign", "return"].includes(action.kind)) {
137
- errors.push(`无效动作类型: '${action?.kind}'`);
138
- return { valid: false, errors };
159
+ const msg = `无效动作类型: '${action?.kind}'`;
160
+ errors.push(msg);
161
+ violations.push({ svl: 1, violatedConstraint: "symbol_existence", actionIndex: idx, description: msg });
162
+ return { valid: false, errors, violations };
139
163
  }
140
164
  if (action.kind === "call") {
141
165
  const fn = functions.find((f) => f.name === action.function);
142
166
  if (!fn) {
143
167
  if (action.function && BUILTIN_WHITELIST.has(action.function))
144
- return { valid: true, errors: [] };
145
- errors.push(`函数 '${action.function}' 不存在`);
146
- return { valid: false, errors };
168
+ return { valid: true, errors: [], violations: [] };
169
+ const msg = `函数 '${action.function}' 不存在`;
170
+ errors.push(msg);
171
+ violations.push({ svl: 1, violatedConstraint: "symbol_existence", actionIndex: idx, missingStates: [action.function], description: msg });
172
+ return { valid: false, errors, violations };
147
173
  }
148
174
  if (!action.args) {
149
- errors.push(`函数 '${action.function}' 缺少参数列表`);
150
- return { valid: false, errors };
175
+ const msg = `函数 '${action.function}' 缺少参数列表`;
176
+ errors.push(msg);
177
+ violations.push({ svl: 2, violatedConstraint: "type_mismatch", actionIndex: idx, description: msg });
178
+ return { valid: false, errors, violations };
151
179
  }
152
180
  if (action.args.length !== fn.params.length) {
153
- errors.push(`参数数量不匹配: 期望 ${fn.params.length}, 实际 ${action.args.length}`);
181
+ const msg = `参数数量不匹配: 期望 ${fn.params.length}, 实际 ${action.args.length}`;
182
+ errors.push(msg);
183
+ violations.push({ svl: 2, violatedConstraint: "type_mismatch", actionIndex: idx, description: msg });
154
184
  }
155
185
  action.args.forEach((arg, i) => {
156
186
  if (!arg) {
157
- errors.push(`函数 '${action.function}' 的第${i}个参数为空`);
187
+ const msg = `函数 '${action.function}' 的第${i}个参数为空`;
188
+ errors.push(msg);
189
+ violations.push({ svl: 2, violatedConstraint: "type_mismatch", actionIndex: idx, description: msg });
158
190
  return;
159
191
  }
160
192
  const expected = normalizeType(fn.params[i]?.type);
161
193
  const actual = normalizeType(arg.type);
162
194
  if (actual !== "any" && expected !== "any" && actual !== expected) {
163
- errors.push(`类型不匹配: 参数 '${fn.params[i].name}' 期望 ${expected}, 实际 ${actual}`);
195
+ const msg = `类型不匹配: 参数 '${fn.params[i].name}' 期望 ${expected}, 实际 ${actual}`;
196
+ errors.push(msg);
197
+ violations.push({ svl: 2, violatedConstraint: "type_mismatch", actionIndex: idx, description: msg });
164
198
  }
165
199
  });
166
200
  }
167
201
  else if (action.kind === "if") {
168
202
  if (action.thenActions) {
169
- for (const a of action.thenActions)
170
- errors.push(...validateAction(a).errors);
203
+ for (const a of action.thenActions) {
204
+ const r = validateAction(a, idx);
205
+ errors.push(...r.errors);
206
+ violations.push(...r.violations);
207
+ }
171
208
  }
172
209
  if (action.elseActions) {
173
- for (const a of action.elseActions)
174
- errors.push(...validateAction(a).errors);
210
+ for (const a of action.elseActions) {
211
+ const r = validateAction(a, idx);
212
+ errors.push(...r.errors);
213
+ violations.push(...r.violations);
214
+ }
175
215
  }
176
216
  }
177
217
  else if (action.kind === "for") {
178
218
  if (action.bodyActions) {
179
- for (const a of action.bodyActions)
180
- errors.push(...validateAction(a).errors);
219
+ for (const a of action.bodyActions) {
220
+ const r = validateAction(a, idx);
221
+ errors.push(...r.errors);
222
+ violations.push(...r.violations);
223
+ }
181
224
  }
182
225
  }
183
- return { valid: errors.length === 0, errors };
226
+ return { valid: errors.length === 0, errors, violations };
184
227
  }
228
+ /**
229
+ * 批量校验动作序列 + 变量流向分析。
230
+ * @protocol namespace=dev_pipeline pre_states=["ACTION_VALIDATED"] post_states=["SEQUENCE_VALIDATED"] invalidate=["ACTION_VALIDATED"]
231
+ */
185
232
  function validateActionSequence(actions) {
186
233
  const errors = [];
187
- for (const action of actions) {
188
- const result = validateAction(action);
189
- if (!result.valid)
234
+ const violations = [];
235
+ for (let i = 0; i < actions.length; i++) {
236
+ const result = validateAction(actions[i], i);
237
+ if (!result.valid) {
190
238
  errors.push(...result.errors);
239
+ violations.push(...result.violations);
240
+ }
191
241
  }
192
242
  if (errors.length === 0) {
193
- errors.push(...checkVariableFlow(actions));
243
+ const flowErrors = checkVariableFlow(actions);
244
+ if (flowErrors.length > 0) {
245
+ errors.push(...flowErrors);
246
+ for (const msg of flowErrors) {
247
+ violations.push({ svl: 3, violatedConstraint: "dataflow", actionIndex: 0, description: msg });
248
+ }
249
+ }
194
250
  }
195
- return { valid: errors.length === 0, errors };
251
+ return { valid: errors.length === 0, errors, violations };
196
252
  }
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.main = main;
4
+ // @progmune-generated session=sess_1780345747502_y4u91 timestamp=2026-06-01T20:29:08.854Z ruleHash=9dec68bc2995e92a
5
+ // Generated with IR constraint: 388 functions, 17 protocol rules
6
+ const ledger_registry_1 = require("./ledger-registry");
7
+ function main() {
8
+ const result = (0, ledger_registry_1.verifyAllFingerprints)("");
9
+ return result;
10
+ }
11
+ main();
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.main = main;
4
+ // @progmune-generated session=sess_1780343460763_6iyf4 timestamp=2026-06-01T19:51:02.054Z ruleHash=9dec68bc2995e92a
5
+ // Generated with IR constraint: 386 functions, 17 protocol rules
6
+ const ledger_registry_1 = require("./ledger-registry");
7
+ function main() {
8
+ const summary = (0, ledger_registry_1.verifyAllFingerprints)("defaultStr");
9
+ return summary;
10
+ }
11
+ main();
@@ -0,0 +1,77 @@
1
+ /* Progmune Runtime v2.2 Whitepaper — Clean Academic Style */
2
+ body {
3
+ font-family: 'Times New Roman', Times, 'Songti SC', 'SimSun', serif;
4
+ font-size: 12pt;
5
+ line-height: 1.8;
6
+ color: #000;
7
+ max-width: 780px;
8
+ margin: 0 auto;
9
+ padding: 2.5rem 2rem;
10
+ }
11
+
12
+ /* ── Headings ── */
13
+ h1 { font-size: 1.8rem; text-align: center; margin: 0 0 0.3rem; font-weight: bold; }
14
+ h2 { font-size: 1.3rem; margin: 2rem 0 0.8rem; border-bottom: 1px solid #000; padding-bottom: 0.2rem; }
15
+ h3 { font-size: 1.1rem; margin: 1.5rem 0 0.5rem; }
16
+ h4 { font-size: 1rem; margin: 1.2rem 0 0.4rem; }
17
+
18
+ /* ── Code ── */
19
+ code {
20
+ font-family: 'Courier New', Courier, monospace;
21
+ font-size: 0.9em;
22
+ background: #f5f5f5;
23
+ padding: 0.1em 0.3em;
24
+ }
25
+ pre {
26
+ font-family: 'Courier New', Courier, monospace;
27
+ font-size: 0.85em;
28
+ background: #f8f8f8;
29
+ border: 1px solid #ddd;
30
+ padding: 0.8rem 1rem;
31
+ overflow-x: auto;
32
+ line-height: 1.5;
33
+ }
34
+ pre code { background: none; padding: 0; border: none; }
35
+
36
+ /* ── Tables ── */
37
+ table {
38
+ width: 100%;
39
+ border-collapse: collapse;
40
+ margin: 1rem 0 1.5rem;
41
+ font-size: 0.95em;
42
+ }
43
+ thead { background: #333; color: #fff; }
44
+ th, td {
45
+ padding: 0.4rem 0.6rem;
46
+ text-align: left;
47
+ border: 1px solid #999;
48
+ }
49
+
50
+ /* ── Blockquote ── */
51
+ blockquote {
52
+ border-left: 3px solid #666;
53
+ margin: 0.8rem 0;
54
+ padding: 0.3rem 0.8rem;
55
+ background: #f9f9f9;
56
+ font-style: italic;
57
+ }
58
+
59
+ /* ── Horizontal rule ── */
60
+ hr { border: none; border-top: 1px solid #999; margin: 1.5rem 0; }
61
+
62
+ /* ── Lists ── */
63
+ ul, ol { padding-left: 1.5rem; }
64
+ li { margin: 0.2rem 0; }
65
+
66
+ /* ── Strong ── */
67
+ strong { color: #000; }
68
+
69
+ /* ── Print ── */
70
+ @media print {
71
+ body { font-size: 10.5pt; max-width: none; padding: 0.8rem; }
72
+ h1 { font-size: 1.5rem; }
73
+ h2 { font-size: 1.15rem; }
74
+ pre { font-size: 0.8em; white-space: pre-wrap; }
75
+ table { font-size: 0.85em; }
76
+ @page { margin: 2cm; }
77
+ }