sneakoscope 4.8.4 → 4.8.6

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 (81) hide show
  1. package/README.md +3 -3
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/crates/sks-core/src/main.rs +1 -1
  5. package/dist/bin/sks.js +1 -1
  6. package/dist/cli/command-registry.js +1 -0
  7. package/dist/core/agents/agent-central-ledger.js +10 -5
  8. package/dist/core/agents/agent-orchestrator.js +22 -1
  9. package/dist/core/agents/agent-runner-codex-exec.js +24 -0
  10. package/dist/core/agents/fast-mode-policy.js +3 -4
  11. package/dist/core/agents/native-cli-session-proof.js +1 -1
  12. package/dist/core/agents/native-cli-worker.js +73 -113
  13. package/dist/core/agents/worker-pane-communication-contract.js +2 -3
  14. package/dist/core/codex/codex-cli-syntax-builder.js +2 -0
  15. package/dist/core/codex-app/codex-init-deep.js +10 -3
  16. package/dist/core/codex-control/codex-reliability-shield.js +36 -17
  17. package/dist/core/codex-control/codex-sdk-adapter.js +2 -2
  18. package/dist/core/codex-control/codex-sdk-config-policy.js +4 -0
  19. package/dist/core/codex-control/codex-task-runner.js +23 -11
  20. package/dist/core/codex-native/core-skill-manifest.js +2 -1
  21. package/dist/core/commands/gates-command.js +58 -0
  22. package/dist/core/commands/release-command.js +5 -5
  23. package/dist/core/commands/stop-gate-command.js +1 -1
  24. package/dist/core/fsx.js +1 -1
  25. package/dist/core/hooks-runtime/team-digest.js +86 -0
  26. package/dist/core/hooks-runtime.js +1 -82
  27. package/dist/core/init/skills.js +395 -0
  28. package/dist/core/init.js +2 -376
  29. package/dist/core/lean-engineering-policy.js +2 -1
  30. package/dist/core/naruto/naruto-active-pool.js +36 -7
  31. package/dist/core/naruto/naruto-real-worker-child.js +67 -11
  32. package/dist/core/permission-gates.js +45 -19
  33. package/dist/core/pipeline-internals/runtime-gates.js +26 -45
  34. package/dist/core/ppt/artifacts.js +19 -0
  35. package/dist/core/ppt/html.js +96 -0
  36. package/dist/core/ppt/style-tokens.js +248 -0
  37. package/dist/core/ppt.js +6 -352
  38. package/dist/core/qa-loop/{qa-contract-v2.js → qa-contract.js} +1 -1
  39. package/dist/core/qa-loop/{qa-gate-v2.js → qa-gate.js} +1 -1
  40. package/dist/core/qa-loop/qa-runtime-artifacts.js +2 -2
  41. package/dist/core/qa-loop.js +11 -4
  42. package/dist/core/recallpulse/policy.js +226 -0
  43. package/dist/core/recallpulse.js +2 -225
  44. package/dist/core/release/release-gate-dag.js +6 -2
  45. package/dist/core/research/mock-result.js +406 -0
  46. package/dist/core/research/prompt.js +15 -0
  47. package/dist/core/research.js +2 -402
  48. package/dist/core/routes/constants.js +48 -0
  49. package/dist/core/routes/design-policy.js +37 -0
  50. package/dist/core/routes/evidence.js +77 -0
  51. package/dist/core/routes/ppt-policy.js +26 -0
  52. package/dist/core/routes.js +8 -179
  53. package/dist/core/safety/mutation-ledger.js +2 -0
  54. package/dist/core/safety/ssot-guard.js +2 -2
  55. package/dist/core/stop-gate/stop-gate-check.js +10 -17
  56. package/dist/core/version.js +1 -1
  57. package/dist/core/zellij/zellij-slot-pane-renderer.js +15 -8
  58. package/dist/core/zellij/zellij-slot-telemetry.js +28 -12
  59. package/dist/scripts/check-pipeline-budget.js +1 -7
  60. package/dist/scripts/check-pipeline-runtime.js +1 -8
  61. package/dist/scripts/codex-control-all-pipelines-check.js +5 -14
  62. package/dist/scripts/codex-sdk-all-pipelines-check.js +5 -9
  63. package/dist/scripts/fast-codex-service-tier-proof-check.js +2 -2
  64. package/dist/scripts/gate-policy-audit-check.js +70 -0
  65. package/dist/scripts/official-docs-compat-report.js +2 -2
  66. package/dist/scripts/product-design-plugin-routing-check.js +10 -5
  67. package/dist/scripts/qa-loop-surface-router-check.js +2 -2
  68. package/dist/scripts/release-check-dynamic-execute.js +32 -35
  69. package/dist/scripts/release-check-dynamic.js +19 -28
  70. package/dist/scripts/release-check-stamp.js +2 -2
  71. package/dist/scripts/release-dag-full-coverage-check.js +57 -394
  72. package/dist/scripts/release-dynamic-performance-check.js +18 -19
  73. package/dist/scripts/release-dynamic-presets-check.js +2 -2
  74. package/dist/scripts/release-gate-dag-runner.js +3 -1
  75. package/dist/scripts/release-gate-existence-audit.js +59 -91
  76. package/dist/scripts/release-gate-planner.js +43 -23
  77. package/dist/scripts/release-metadata-1-19-check.js +100 -12
  78. package/dist/scripts/worker-pane-communication-contract-check.js +1 -2
  79. package/dist/scripts/zellij-slot-pane-renderer-check.js +3 -2
  80. package/package.json +13 -965
  81. package/dist/core/pipeline-runtime.js +0 -2
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import fs from 'node:fs/promises';
3
2
  import path from 'node:path';
4
- import { ensureDir, nowIso, readJson, writeJsonAtomic } from '../fsx.js';
3
+ import { appendJsonlBounded, ensureDir, nowIso, readJson, writeJsonAtomic } from '../fsx.js';
5
4
  import { runCodexTask } from '../codex-control/codex-control-plane.js';
6
5
  import { CODEX_AGENT_WORKER_RESULT_SCHEMA_ID, codexAgentWorkerResultSchema } from '../codex-control/schemas/agent-worker-result.schema.js';
7
6
  async function main() {
@@ -12,12 +11,17 @@ async function main() {
12
11
  if (!intake?.result_path || !intake?.heartbeat_path || !intake?.item?.id) {
13
12
  throw new Error('naruto worker intake is invalid');
14
13
  }
15
- await fs.appendFile(intake.heartbeat_path, `${JSON.stringify({
14
+ const startedAt = Date.now();
15
+ const maxRuntimeMs = normalizeMaxRuntimeMs(intake.max_runtime_ms);
16
+ let finished = false;
17
+ const heartbeatTimer = startDeadmanHeartbeat({ intake, startedAt, maxRuntimeMs, finished: () => finished });
18
+ await appendJsonlBounded(intake.heartbeat_path, {
16
19
  schema: 'sks.naruto-actual-worker-heartbeat.v1',
17
20
  ts: nowIso(),
18
21
  item_id: intake.item.id,
19
- status: 'running'
20
- })}\n`);
22
+ status: 'running',
23
+ progress: null
24
+ }, 2 * 1024 * 1024);
21
25
  if (intake.backend === 'fake')
22
26
  process.env.SKS_CODEX_SDK_FAKE = '1';
23
27
  const controlRoot = path.join(path.dirname(intake.result_path), 'codex-control');
@@ -76,14 +80,19 @@ async function main() {
76
80
  changed_files: Array.isArray(workerResult?.changed_files) ? workerResult.changed_files : [],
77
81
  blockers
78
82
  });
79
- await fs.appendFile(intake.heartbeat_path, `${JSON.stringify({
83
+ finished = true;
84
+ clearInterval(heartbeatTimer);
85
+ await appendJsonlBounded(intake.heartbeat_path, {
80
86
  schema: 'sks.naruto-actual-worker-heartbeat.v1',
81
87
  ts: nowIso(),
82
88
  item_id: intake.item.id,
83
- status: blockers.length ? 'blocked' : 'done'
84
- })}\n`);
89
+ status: blockers.length ? 'blocked' : 'done',
90
+ progress: { done: 1, total: 1 }
91
+ }, 2 * 1024 * 1024);
85
92
  }
86
93
  catch (err) {
94
+ finished = true;
95
+ clearInterval(heartbeatTimer);
87
96
  await writeJsonAtomic(intake.result_path, {
88
97
  schema: 'sks.naruto-actual-worker-result.v1',
89
98
  ok: false,
@@ -94,15 +103,62 @@ async function main() {
94
103
  worktree_path: intake.worktree_path,
95
104
  blockers: [`naruto_actual_worker_control_plane_exception:${err?.message || String(err)}`]
96
105
  });
97
- await fs.appendFile(intake.heartbeat_path, `${JSON.stringify({
106
+ await appendJsonlBounded(intake.heartbeat_path, {
98
107
  schema: 'sks.naruto-actual-worker-heartbeat.v1',
99
108
  ts: nowIso(),
100
109
  item_id: intake.item.id,
101
- status: 'blocked'
102
- })}\n`);
110
+ status: 'blocked',
111
+ progress: null
112
+ }, 2 * 1024 * 1024);
103
113
  throw err;
104
114
  }
105
115
  }
116
+ function startDeadmanHeartbeat(input) {
117
+ const parsed = Number(process.env.SKS_ZELLIJ_WORKER_PROGRESS_MS || 10000);
118
+ const intervalMs = Math.max(1000, Number.isFinite(parsed) ? Math.floor(parsed) : 10000);
119
+ return setInterval(async () => {
120
+ if (input.finished())
121
+ return;
122
+ const elapsedMs = Date.now() - input.startedAt;
123
+ if (elapsedMs > input.maxRuntimeMs) {
124
+ await writeJsonAtomic(input.intake.result_path, {
125
+ schema: 'sks.naruto-actual-worker-result.v1',
126
+ ok: false,
127
+ generated_at: nowIso(),
128
+ item_id: input.intake.item.id,
129
+ placement: input.intake.placement,
130
+ backend: input.intake.backend,
131
+ worktree_path: input.intake.worktree_path,
132
+ status: 'timed_out',
133
+ blockers: ['naruto_worker_hard_timeout']
134
+ }).catch(() => undefined);
135
+ await appendJsonlBounded(input.intake.heartbeat_path, {
136
+ schema: 'sks.naruto-actual-worker-heartbeat.v1',
137
+ ts: nowIso(),
138
+ item_id: input.intake.item.id,
139
+ status: 'blocked',
140
+ elapsed_ms: elapsedMs,
141
+ progress: null
142
+ }, 2 * 1024 * 1024).catch(() => undefined);
143
+ process.exit(124);
144
+ return;
145
+ }
146
+ await appendJsonlBounded(input.intake.heartbeat_path, {
147
+ schema: 'sks.naruto-actual-worker-heartbeat.v1',
148
+ ts: nowIso(),
149
+ item_id: input.intake.item.id,
150
+ status: 'running',
151
+ elapsed_ms: elapsedMs,
152
+ progress: null
153
+ }, 2 * 1024 * 1024).catch(() => undefined);
154
+ }, intervalMs);
155
+ }
156
+ function normalizeMaxRuntimeMs(value) {
157
+ const parsed = Number(value);
158
+ if (Number.isFinite(parsed) && parsed > 0)
159
+ return Math.max(1000, Math.min(Math.floor(parsed), 24 * 60 * 60 * 1000));
160
+ return 10 * 60 * 1000;
161
+ }
106
162
  function backendPreference(value) {
107
163
  const backend = String(value || '');
108
164
  if (backend === 'ollama' || backend === 'local-llm')
@@ -86,29 +86,55 @@ export function madSksCatastrophicDbReasons(cls = {}) {
86
86
  blocked.push('dangerous_project_or_branch_management');
87
87
  return [...new Set(blocked)];
88
88
  }
89
+ export function evaluatePermission({ action, scope, profile, classification, active = false, env = process.env } = {}) {
90
+ const selectedProfile = normalizePermissionProfile(profile, env);
91
+ const actionId = String(action || scope || '').trim();
92
+ const blocked = new Set((selectedProfile.blocked || []).map(String));
93
+ const allowed = new Set((selectedProfile.allowed || []).map(String));
94
+ const catastrophic = madSksCatastrophicDbReasons(classification || {});
95
+ const blockedCategories = [
96
+ ...(actionId && blocked.has(actionId) ? [actionId] : []),
97
+ ...catastrophic
98
+ ];
99
+ if (!active) {
100
+ return { matched: false, active: false, allowed: false, action: 'ignore', reasons: ['permission_profile_inactive'], blocked_categories: [], profile: permissionGateSummary(selectedProfile) };
101
+ }
102
+ if (blockedCategories.length) {
103
+ return { matched: true, active: true, allowed: false, action: 'block', reasons: ['permission_profile_blocked_action'], blocked_categories: [...new Set(blockedCategories)], profile: permissionGateSummary(selectedProfile) };
104
+ }
105
+ if (actionId && allowed.size > 0 && !allowed.has(actionId)) {
106
+ return { matched: true, active: true, allowed: false, action: 'block', reasons: ['permission_profile_action_not_allowed'], blocked_categories: [actionId], profile: permissionGateSummary(selectedProfile) };
107
+ }
108
+ return { matched: true, active: true, allowed: true, action: 'allow', reasons: ['permission_profile_scoped_allow'], blocked_categories: [], profile: permissionGateSummary(selectedProfile) };
109
+ }
89
110
  export function evaluateMadSksPermissionGate({ classification, active = false } = {}) {
90
111
  const cls = classification || { level: 'none', reasons: [] };
91
- if (!active || !['write', 'destructive'].includes(cls.level))
92
- return { matched: false, active: Boolean(active), profile: permissionGateSummary() };
93
- const catastrophic = madSksCatastrophicDbReasons(cls);
94
- if (catastrophic.length) {
95
- return {
96
- matched: true,
97
- active: true,
98
- allowed: false,
99
- action: 'block',
100
- reasons: ['mad_sks_catastrophic_db_operation_blocked'],
101
- blocked_categories: catastrophic,
102
- profile: permissionGateSummary()
103
- };
112
+ if (!active || !['write', 'destructive'].includes(cls.level)) {
113
+ return { matched: false, active: Boolean(active), allowed: false, action: 'ignore', reasons: [], blocked_categories: [], profile: permissionGateSummary() };
104
114
  }
115
+ const decision = evaluatePermission({ action: null, profile: MAD_SKS_PERMISSION_PROFILE, classification: cls, active: true });
105
116
  return {
106
- matched: true,
107
- active: true,
108
- allowed: true,
109
- action: 'allow',
110
- reasons: ['mad_sks_scoped_live_full_access_active'],
111
- profile: permissionGateSummary()
117
+ ...decision,
118
+ reasons: decision.allowed ? ['mad_sks_scoped_live_full_access_active'] : ['mad_sks_catastrophic_db_operation_blocked']
112
119
  };
113
120
  }
121
+ function normalizePermissionProfile(profile, env = process.env) {
122
+ if (profile?.allowed && profile?.blocked)
123
+ return profile;
124
+ const configured = loadConfiguredPermissionProfiles(env);
125
+ const id = typeof profile === 'string' ? profile : profile?.id;
126
+ return configured.find((row) => row.id === id) || MAD_SKS_PERMISSION_PROFILE;
127
+ }
128
+ function loadConfiguredPermissionProfiles(env = process.env) {
129
+ const raw = String(env.SKS_PERMISSION_PROFILES_JSON || '').trim();
130
+ if (!raw)
131
+ return [];
132
+ try {
133
+ const parsed = JSON.parse(raw);
134
+ return Array.isArray(parsed?.profiles) ? parsed.profiles : Array.isArray(parsed) ? parsed : [];
135
+ }
136
+ catch {
137
+ return [];
138
+ }
139
+ }
114
140
  //# sourceMappingURL=permission-gates.js.map
@@ -1,4 +1,3 @@
1
- import fsp from 'node:fs/promises';
2
1
  import path from 'node:path';
3
2
  import { appendJsonl, exists, nowIso, readJson, readText, writeJsonAtomic } from '../fsx.js';
4
3
  import { containsUserQuestion, noQuestionContinuationReason } from '../no-question-guard.js';
@@ -72,42 +71,9 @@ async function staleReflectionReasons(root, state = {}, gate = {}) {
72
71
  const id = state?.mission_id;
73
72
  if (!id)
74
73
  return [];
75
- const dir = missionDir(root, id);
76
- const missing = [];
77
- for (const file of gateFilesForState(state).filter((file) => file && !['none', 'honest_mode'].includes(file))) {
78
- if (await fileUpdatedAfter(path.join(dir, file), created))
79
- missing.push(`${file}:updated_after_reflection`);
80
- }
81
- const transcript = await readText(path.join(dir, 'team-transcript.jsonl'), '');
82
- const newerWorkEvent = transcript
83
- .split(/\n/)
84
- .filter(Boolean)
85
- .map((line) => {
86
- try {
87
- return JSON.parse(line);
88
- }
89
- catch {
90
- return null;
91
- }
92
- })
93
- .find((event) => {
94
- const ts = Date.parse(event?.ts || '');
95
- if (!Number.isFinite(ts) || ts <= created)
96
- return false;
97
- return !/^(REFLECTION|HONEST|TEAM_CLEANUP)$/i.test(String(event?.phase || ''));
98
- });
99
- if (newerWorkEvent)
100
- missing.push('team-transcript.jsonl:work_after_reflection');
101
- return missing;
102
- }
103
- async function fileUpdatedAfter(file, timeMs) {
104
- try {
105
- const stat = await fsp.stat(file);
106
- return stat.mtimeMs > timeMs + 1000;
107
- }
108
- catch {
109
- return false;
110
- }
74
+ if (state.reflection_invalidation_required !== true)
75
+ return [];
76
+ return ['reflection_invalidation_required'];
111
77
  }
112
78
  function reflectionStopReason(state = {}, status = {}) {
113
79
  const id = state?.mission_id || 'latest';
@@ -227,7 +193,7 @@ export async function evaluateStop(root, state, payload, opts = {}) {
227
193
  if (gate.ok) {
228
194
  const reflection = await reflectionGateStatus(root, state);
229
195
  if (!reflection.ok)
230
- return complianceBlock(root, state, reflectionStopReason(state, reflection), { gate: 'reflection', missing: reflection.missing });
196
+ await appendHonestModeNote(root, state, `reflection stale: ${(reflection.missing || []).join(', ')}`);
231
197
  return { continue: true };
232
198
  }
233
199
  const missing = gate.missing?.length ? ` Missing gate fields: ${gate.missing.join(', ')}.` : '';
@@ -250,7 +216,7 @@ export async function evaluateStop(root, state, payload, opts = {}) {
250
216
  return { continue: true, systemMessage: `SKS: canonical stop-gate passed at ${stopCheck.gate_path}` };
251
217
  }
252
218
  else if (stopCheck.action === 'hard_blocked') {
253
- return { continue: true, systemMessage: `SKS: ${stopCheck.feedback}` };
219
+ return { decision: 'block', reason: stopCheck.feedback, action: 'hard_blocked', gate: stopCheck.gate_path };
254
220
  }
255
221
  else {
256
222
  const missing = stopCheck.diagnostics.missing_fields?.length ? ` Missing gate fields: ${stopCheck.diagnostics.missing_fields.join(', ')}.` : '';
@@ -304,7 +270,7 @@ async function complianceBlock(root, state = {}, reason = '', detail = {}) {
304
270
  const normalized = normalizeComplianceReason(reason);
305
271
  const previous = await readJson(guardPath, {});
306
272
  const count = previous.normalized_reason === normalized ? Number(previous.repeat_count || 0) + 1 : 1;
307
- const limit = complianceLoopLimit();
273
+ const limit = complianceLoopLimit(detail.gate || state.stop_gate || '');
308
274
  const record = {
309
275
  schema_version: 1,
310
276
  updated_at: nowIso(),
@@ -323,7 +289,9 @@ async function complianceBlock(root, state = {}, reason = '', detail = {}) {
323
289
  if (!record.tripped)
324
290
  return { decision: 'block', reason, gate: detail.gate || state.stop_gate || null, missing: Array.isArray(detail.missing) ? detail.missing : [] };
325
291
  await writeJsonAtomic(path.join(dir, HARD_BLOCKER_ARTIFACT), {
326
- passed: true,
292
+ schema: 'sks.hard-blocker.v1',
293
+ passed: false,
294
+ status: 'hard_blocked',
327
295
  created_at: nowIso(),
328
296
  reason: 'compliance_loop_guard_tripped',
329
297
  route: record.route,
@@ -337,10 +305,11 @@ async function complianceBlock(root, state = {}, reason = '', detail = {}) {
337
305
  ]
338
306
  });
339
307
  await appendJsonl(path.join(dir, 'events.jsonl'), { ts: nowIso(), type: 'pipeline.compliance_loop_guard.tripped', gate: record.gate, repeat_count: count, limit });
340
- return null;
308
+ return { decision: 'escalate', reason, gate: detail.gate || state.stop_gate || null, repeat_count: count, message: '동일 사유가 반복됩니다. 사용자 개입이 필요합니다.' };
341
309
  }
342
- function complianceLoopLimit() {
343
- const raw = Number.parseInt(process.env.SKS_COMPLIANCE_LOOP_LIMIT || '', 10);
310
+ function complianceLoopLimit(gate = '') {
311
+ const gateKey = String(gate || '').toUpperCase().replace(/[^A-Z0-9]+/g, '_').replace(/^_+|_+$/g, '');
312
+ const raw = Number.parseInt(String((gateKey && process.env[`SKS_COMPLIANCE_LOOP_LIMIT_${gateKey}`]) || process.env.SKS_COMPLIANCE_LOOP_LIMIT || ''), 10);
344
313
  if (!Number.isFinite(raw))
345
314
  return DEFAULT_COMPLIANCE_LOOP_LIMIT;
346
315
  return Math.max(1, Math.min(20, raw));
@@ -349,7 +318,6 @@ function normalizeComplianceReason(reason = '') {
349
318
  return String(reason || '')
350
319
  .replace(/\bM-\d{8}-\d{6}-[a-z0-9]+\b/gi, 'M-*')
351
320
  .replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z/g, 'TIMESTAMP')
352
- .replace(/\d+/g, 'N')
353
321
  .replace(/\s+/g, ' ')
354
322
  .trim()
355
323
  .slice(0, 1200);
@@ -386,8 +354,21 @@ async function passedHardBlocker(root, state) {
386
354
  const blocker = await readJson(path.join(missionDir(root, state.mission_id), file), null);
387
355
  if (!blocker)
388
356
  return { ok: false };
357
+ if (String(blocker.status || '') === 'hard_blocked')
358
+ return { ok: false, file, missing: ['hard_blocked'] };
389
359
  return { ok: blocker.passed === true && String(blocker.reason || '').trim() && Array.isArray(blocker.evidence) && blocker.evidence.length > 0, file };
390
360
  }
361
+ async function appendHonestModeNote(root, state = {}, message) {
362
+ if (!state?.mission_id)
363
+ return;
364
+ const dir = missionDir(root, state.mission_id);
365
+ await appendJsonl(path.join(dir, 'honest-mode-notes.jsonl'), {
366
+ ts: nowIso(),
367
+ type: 'honest_mode_note',
368
+ route: state.route_command || state.route || state.mode || null,
369
+ message
370
+ });
371
+ }
391
372
  function missingRequiredGateFields(file, state, gate = {}) {
392
373
  const mode = String(state?.mode || '').toUpperCase();
393
374
  if (file === 'team-gate.json' || mode === 'TEAM') {
@@ -0,0 +1,19 @@
1
+ export const PPT_AUDIENCE_STRATEGY_ARTIFACT = 'ppt-audience-strategy.json';
2
+ export const PPT_GATE_ARTIFACT = 'ppt-gate.json';
3
+ export const PPT_SOURCE_LEDGER_ARTIFACT = 'ppt-source-ledger.json';
4
+ export const PPT_FACT_LEDGER_ARTIFACT = 'ppt-fact-ledger.json';
5
+ export const PPT_STORYBOARD_ARTIFACT = 'ppt-storyboard.json';
6
+ export const PPT_STYLE_TOKENS_ARTIFACT = 'ppt-style-tokens.json';
7
+ export const PPT_IMAGE_ASSET_LEDGER_ARTIFACT = 'ppt-image-asset-ledger.json';
8
+ export const PPT_ASSET_DIR = 'assets';
9
+ export const PPT_REVIEW_POLICY_ARTIFACT = 'ppt-review-policy.json';
10
+ export const PPT_REVIEW_LEDGER_ARTIFACT = 'ppt-review-ledger.json';
11
+ export const PPT_ITERATION_REPORT_ARTIFACT = 'ppt-iteration-report.json';
12
+ export const PPT_SOURCE_HTML_DIR = 'source-html';
13
+ export const PPT_HTML_ARTIFACT = `${PPT_SOURCE_HTML_DIR}/artifact.html`;
14
+ export const PPT_PDF_ARTIFACT = 'artifact.pdf';
15
+ export const PPT_RENDER_REPORT_ARTIFACT = 'ppt-render-report.json';
16
+ export const PPT_CLEANUP_REPORT_ARTIFACT = 'ppt-cleanup-report.json';
17
+ export const PPT_PARALLEL_REPORT_ARTIFACT = 'ppt-parallel-report.json';
18
+ export const PPT_TEMP_DIR = 'ppt-tmp';
19
+ //# sourceMappingURL=artifacts.js.map
@@ -0,0 +1,96 @@
1
+ function escapeHtml(value) {
2
+ return String(value ?? '')
3
+ .replace(/&/g, '&amp;')
4
+ .replace(/</g, '&lt;')
5
+ .replace(/>/g, '&gt;')
6
+ .replace(/"/g, '&quot;')
7
+ .replace(/'/g, '&#39;');
8
+ }
9
+ function jsonScript(value) {
10
+ return JSON.stringify(value).replace(/</g, '\\u003c');
11
+ }
12
+ export function buildPptHtml({ contract = {}, audience, sourceLedger, factLedger, imageAssetLedger, reviewPolicy, storyboard, styleTokens }) {
13
+ const title = escapeHtml(storyboard.title);
14
+ const referenceName = escapeHtml(styleTokens.design_policy?.design_reference_selection?.primary?.name || 'selected design reference');
15
+ const audienceRaw = escapeHtml(audience?.audience_profile?.raw || 'Audience context');
16
+ const stpRaw = escapeHtml(audience?.stp?.raw || 'STP context');
17
+ const decisionRaw = escapeHtml(audience?.decision_context?.raw || storyboard.thesis || '');
18
+ const surfaceRule = styleTokens.layout?.treatment === 'shadow_as_border_minimal_depth'
19
+ ? `box-shadow: 0 0 0 1px ${styleTokens.color.rule}; border: 0;`
20
+ : `border: 1px solid ${styleTokens.color.rule}; box-shadow: none;`;
21
+ const css = `@page { size: 16in 9in; margin: 0; }
22
+ * { box-sizing: border-box; }
23
+ body { margin: 0; background: ${styleTokens.color.bg}; color: ${styleTokens.color.text}; font-family: ${styleTokens.typography.font_stack}; }
24
+ .page { width: 100vw; min-height: 100vh; page-break-after: always; padding: 64px 88px 54px; display: grid; grid-template-rows: auto 1fr auto; gap: 34px; }
25
+ .topline { display: grid; grid-template-columns: 1fr auto; align-items: end; border-bottom: 1px solid ${styleTokens.color.rule}; padding-bottom: 14px; }
26
+ .kicker { color: ${styleTokens.color.primary}; font-size: ${styleTokens.typography.caption_px}px; font-weight: 600; letter-spacing: 0; text-transform: uppercase; }
27
+ .reference { color: ${styleTokens.color.muted}; font-size: ${styleTokens.typography.caption_px}px; letter-spacing: 0; }
28
+ .content { display: grid; grid-template-columns: minmax(0, 6fr) minmax(320px, 4fr); gap: 58px; align-items: center; }
29
+ h1 { margin: 0; font-size: ${styleTokens.typography.display_px}px; line-height: 1.08; letter-spacing: 0; max-width: 1040px; font-weight: 600; }
30
+ p { margin: 0; color: ${styleTokens.color.muted}; font-size: ${styleTokens.typography.body_px}px; line-height: ${styleTokens.typography.line_height}; max-width: 920px; }
31
+ .claim { display: grid; gap: 26px; }
32
+ .evidence { ${surfaceRule} border-radius: ${styleTokens.layout.radius_px}px; background: ${styleTokens.color.surface}; display: grid; }
33
+ .image-asset { padding: 12px; border-bottom: 1px solid ${styleTokens.color.rule}; }
34
+ .image-asset img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover; border-radius: ${styleTokens.layout.radius_px}px; }
35
+ .evidence-row { padding: 22px 24px; border-bottom: 1px solid ${styleTokens.color.rule}; }
36
+ .evidence-row:last-child { border-bottom: 0; }
37
+ .label { color: ${styleTokens.color.primary}; font-size: ${styleTokens.typography.caption_px}px; font-weight: 600; letter-spacing: 0; text-transform: uppercase; margin-bottom: 8px; }
38
+ .value { color: ${styleTokens.color.text}; font-size: 20px; line-height: 1.42; }
39
+ .source { display: grid; grid-template-columns: 1fr auto; gap: 24px; color: ${styleTokens.color.muted}; font-size: ${styleTokens.typography.caption_px}px; border-top: 1px solid ${styleTokens.color.rule}; padding-top: 14px; }
40
+ .accent { width: 64px; height: 3px; background: ${styleTokens.color.accent}; }`;
41
+ const generatedAssets = (imageAssetLedger?.assets || []).filter((asset) => asset.status === 'generated' && asset.html_src);
42
+ const pages = storyboard.pages.map((page) => {
43
+ const asset = generatedAssets.find((candidate) => Number(candidate.slide) === Number(page.number));
44
+ return `<section class="page">
45
+ <header class="topline">
46
+ <div class="kicker">${escapeHtml(page.kind)} / ${page.number}</div>
47
+ <div class="reference">${referenceName}</div>
48
+ </header>
49
+ <main class="content">
50
+ <div class="claim">
51
+ <div class="accent"></div>
52
+ <h1>${escapeHtml(page.claim)}</h1>
53
+ <p>${escapeHtml(page.support)}</p>
54
+ </div>
55
+ <aside class="evidence" aria-label="decision evidence">
56
+ ${asset ? `<div class="image-asset"><img src="${escapeHtml(asset.html_src)}" alt="${escapeHtml(asset.role || 'generated presentation visual')}"></div>` : ''}
57
+ <div class="evidence-row">
58
+ <div class="label">Audience</div>
59
+ <div class="value">${audienceRaw}</div>
60
+ </div>
61
+ <div class="evidence-row">
62
+ <div class="label">STP</div>
63
+ <div class="value">${stpRaw}</div>
64
+ </div>
65
+ <div class="evidence-row">
66
+ <div class="label">Decision</div>
67
+ <div class="value">${decisionRaw}</div>
68
+ </div>
69
+ </aside>
70
+ </main>
71
+ <div class="source">
72
+ <span>Sources: ${escapeHtml((page.source_ids || []).join(', ') || 'none')}</span>
73
+ <span>${escapeHtml(styleTokens.layout?.composition || 'presentation-grid')}</span>
74
+ </div>
75
+ </section>`;
76
+ }).join('\n');
77
+ return `<!doctype html>
78
+ <html lang="${styleTokens.typography.language}">
79
+ <head>
80
+ <meta charset="utf-8">
81
+ <meta name="viewport" content="width=device-width, initial-scale=1">
82
+ <title>${title}</title>
83
+ <style>${css}</style>
84
+ </head>
85
+ <body>
86
+ ${pages}
87
+ <script type="application/json" id="ppt-audience-strategy">${jsonScript(audience)}</script>
88
+ <script type="application/json" id="ppt-source-ledger">${jsonScript(sourceLedger)}</script>
89
+ <script type="application/json" id="ppt-fact-ledger">${jsonScript(factLedger || null)}</script>
90
+ <script type="application/json" id="ppt-image-asset-ledger">${jsonScript(imageAssetLedger || null)}</script>
91
+ <script type="application/json" id="ppt-review-policy">${jsonScript(reviewPolicy || null)}</script>
92
+ </body>
93
+ </html>
94
+ `;
95
+ }
96
+ //# sourceMappingURL=html.js.map