146
147
Manual runs default to `run_mode=normal` and `source_refresh_policy=reuse-same-day`. Normal mode is fail-closed: it may publish only after the existing analysis and freshness gates pass, and same-day successful source artifacts are reused instead of scraping again. Missing, failed, stale, wrong-week, or wrong-window sources are refreshed.
148
149
-Use explicit modes for safer or destructive intent:
149
+##### Rerun mode reference
150
151
-- `dry-run`: build candidate artifacts only; never commit, deploy, notify, or publish a release.
152
-- `candidate-only`: run crawl/analysis and upload candidates; promotion is blocked by the manifest.
153
-- `restore`: requires `rebuild_week=YYYY-WNN`; hydrates artifacts from `publish` for audited restore/regeneration.
154
-- `force-replace`: explicit replacement intent, but gates still must pass before promotion.
155
-- `source_refresh_policy=force-refresh`: explicitly bypass same-day source reuse and refresh sources.
151
+All rerun modes are validated before any publishing side effects:
152
157
-Invalid combinations (for example `rebuild_week` without `run_mode=restore`, `publish_release` with `dry-run`, or `restore` with `force-refresh`) fail before publish content can be modified.
153
+| Mode | Crawl | Promotion | Intent | Use case |
154
+|------|-------|-----------|--------|----------|
155
+| `normal` (default) | ✓ Fresh | ✓ Guarded gates | Produce fresh analysis, publish if gates pass | Standard weekly run |
156
+| `dry-run` | ✓ Fresh | ✗ Never | Build candidates only for inspection | Test analysis quality, verify gates, debug analysis |
157
+| `candidate-only` | ✓ Fresh | ✗ Manifest blocks | Run crawl/analysis but hold for manual approval | Staged analysis, manual promotion workflow |
158
+| `restore` | ✗ Hydrate | ✓ Guarded gates | Regenerate prior week from published artifacts | Restore/audit trail, regenerate HTML/feeds |
159
+| `force-replace` | ✓ Fresh | ✓ Guarded gates | Explicit replacement run, gates still enforce | Planned content refresh, operator override |
160
+
161
+##### Source refresh policies
162
+
163
+Control how same-day artifacts are handled during reruns:
164
+
165
+| Policy | Behavior | Use case |
166
+|--------|----------|----------|
167
+| `reuse-same-day` (default) | Reuse eligible same-day raw artifacts; refresh missing/stale/failed sources | Safe rerun without redundant API calls |
168
+| `refresh-missing-stale` | Like reuse-same-day but also refresh sources with missing or stale status | Partial refresh, correct specific source issues |
169
+| `force-refresh` | Refresh all sources regardless of prior status | Force all new data, ignore cache |
170
+
171
+Same-day artifact reuse is safe by design:
172
+- Only successfully crawled artifacts are eligible for reuse
173
+- Missing, failed, stale (>24 hours old), or wrong-week sources are always refreshed
174
+- Source status (reused/refreshed/missing/failed/stale) is recorded in the publish manifest for audit trail
175
+
176
+Invalid combinations fail immediately with clear error messages:
177
+- `rebuild_week` without `run_mode=restore`
178
+- `run_mode=restore` with `source_refresh_policy=force-refresh`
179
+- `publish_release=true` with `dry-run` or `candidate-only`
180
181
### Option C: Run individual stages locally
182
228
229
Output: `public/` directory ready for GitHub Pages.
230
231
+## Understanding Source Artifacts and Reuse
232
+
233
+### Source artifact tracking
234
+
235
+When SquadScope crawls, it records detailed information about each source artifact:
236
+
237
+- **Status:** One of `reused`, `refreshed`, `missing`, `failed`, or `stale`
238
+- **Artifact checksum:** SHA256 of successful crawls for integrity verification
239
+- **Timestamp:** When the artifact was produced or reused
240
+- **Code checksum:** Hash of the crawler code that produced it (detects version drift)
241
+
242
+This metadata is stored in the **publish manifest** (`data/candidates/YYYY-WNN/RUN_ID/publish-manifest.json`) for every run, creating an auditable trail of:
243
+- Which sources were fetched vs. reused
244
+- Why sources were refreshed (missing, failed, stale, code drift)
245
+- Provenance of every analysis artifact
246
+
247
+### Examining source status
248
+
249
+After a run completes, check the publish manifest to see which sources were reused or refreshed:
250
+
251
+```bash
252
+# Find the latest manifest for week 2026-W21
253
+find data/candidates/2026-W21 -name publish-manifest.json | sort -V | tail -1 | xargs cat | jq '.source_artifacts'
254
+```
255
+
256
+Output shows:
257
+```json
258
+{
259
+ "source_artifacts": [
260
+ {
261
+ "role": "raw_github",
262
+ "path": "data/candidates/2026-W21/github-crawl.json",
263
+ "exists": true,
264
+ "size_bytes": 45823,
265
+ "sha256": "686085ace216e10d36837a91471e28a334b2fc3d93cc1085b8d5d0e7616891bf",
266
+ "same_day_reuse": {
267
+ "status": "reused",
268
+ "source": "default"
269
+ },
270
+ "freshness": {
271
+ "status": "fresh",
272
+ "reasons": []
273
+ },
274
+ "provenance": {
275
+ "generated_at": "2026-05-20T10:15:00Z",
276
+ "sha256": "686085ace216e10d36837a91471e28a334b2fc3d93cc1085b8d5d0e7616891bf"
277
+ }
278
+ },
279
+ {
280
+ "role": "external_news",
281
+ "path": "data/candidates/2026-W21/news-articles.json",
282
+ "exists": true,
283
+ "size_bytes": 28941,
284
+ "sha256": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1",
285
+ "same_day_reuse": {
286
+ "status": "not_reused",
287
+ "source": "refresh_policy"
288
+ },
289
+ "freshness": {
290
+ "status": "stale",
291
+ "reasons": ["source_refresh_policy=refresh-missing-stale"]
292
+ },
293
+ "provenance": {
294
+ "generated_at": "2026-05-21T08:30:00Z",
295
+ "sha256": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1"
296
+ }
297
+ }
298
+ ]
299
+}
300
+```
301
+
302
+### Safe rerun scenario
303
+
304
+**Scenario:** You rerun Monday's analysis on Tuesday morning (same week) to fix a quality gate failure.
305
+
306
+**Expected behavior with `source_refresh_policy=reuse-same-day` (default):**
307
+1. Monday's successful GitHub crawl is reused (1 API call saved)
308
+2. Any failed or missing sources from Monday are refreshed
309
+3. Analysis gates run on fresh analysis only
310
+4. If gates pass, publish replaces Monday's article
311
+5. If gates fail, the publish manifest blocks the promotion and preserves Monday's good article
312
+
313
+**This is safe because:**
314
+- Only Monday's *successful* artifacts are reused
315
+- Any source that failed on Monday is fetched fresh
316
+- The manifest explicitly records what was reused
317
+- Quality gates prevent bad analysis from being published
318
+- Good prior analysis is preserved if the retry fails
319
+
320
+### When to use each policy
321
+
322
+**Use `reuse-same-day` (default):**
323
+- Standard reruns within the same day
324
+- Debugging analysis issues
325
+- Retrying quality gates after minor fixes
326
+
327
+**Use `refresh-missing-stale`:**
328
+- Some sources failed and you've fixed the crawler
329
+- You want to update stale sources without fully refreshing
330
+
331
+**Use `force-refresh`:**
332
+- You suspect source data is corrupted or needs manual validation
333
+- You're testing source updates
334
+- Policy: Always use explicit intent for full refresh
335
+
336
+## Safe Restore from Backup
337
+
338
+### Understanding backups
339
+
340
+Before any weekly article or analysis is replaced in the `publish` branch, an immutable backup is created at:
341
+
342
+```
343
+data/backups/YYYY-WNN/RUN_ID/[analysis|content]/manifest.json
344
+```
345
+
346
+Backups include:
347
+- The exact prior content being replaced (e.g., `content/weekly/2026/W21.md`)
348
+- The prior analysis file (e.g., `data/analyzed/2026-W21-summary.md`)
349
+- SHA256 checksums of all backed-up files
350
+- Timestamp and run context
351
+
352
+### When backups are created
353
+
354
+A backup is automatically created when:
355
+1. A new analysis is about to replace a prior week's analysis, OR
356
+2. A new content page is about to replace a prior week's HTML page
357
+
358
+Backups are immutable—they cannot be modified or deleted by subsequent runs.
359
+
360
+### Viewing available backups
361
+
362
+```bash
363
+# List all available backups for week 2026-W21
364
+find data/backups/2026-W21 -name manifest.json | sort -V
365
+
366
+# Inspect a backup manifest
367
+cat data/backups/2026-W21/RUN_ID/content/manifest.json | jq .
368
+```
369
+
370
+Backup manifest shows:
371
+```json
372
+{
373
+ "schema_version": "publish_backup_v1",
374
+ "week": "2026-W21",
375
+ "run_id": 12345678,
376
+ "timestamp": "2026-05-20T10:15:00Z",
377
+ "backed_up_artifacts": [
378
+ {
379
+ "path": "data/analyzed/2026-W21-summary.md",
380
+ "sha256": "abc123...",
381
+ "exists_before_replacement": true
382
+ },
383
+ {
384
+ "path": "content/weekly/2026/W21.md",
385
+ "sha256": "def456...",
386
+ "exists_before_replacement": true
387
+ }
388
+ ]
389
+}
390
+```
391
+
392
+### Restore a prior week from backup
393
+
394
+To restore a prior week (e.g., restore 2026-W21 to a known-good state):
395
+
396
+1. **Identify the backup manifest** you want to restore:
397
+ ```bash
398
+ # List backups for week 2026-W21, sorted by timestamp
399
+ find data/backups/2026-W21 -name manifest.json | sort -V
400
+ ```
401
+
402
+2. **Trigger the restore workflow:**
403
+ ```bash
404
+ gh workflow run restore-publish-backup.yml \
405
+ -R YOUR_USERNAME/SquadScope \
406
+ -f "backup_manifest=data/backups/2026-W21/RUN_ID/content/manifest.json"
407
+ ```
408
+
409
+ Or through the UI:
410
+ - Go to **Actions → Restore publish backup**
411
+ - Click **Run workflow**
412
+ - Paste the backup manifest path (e.g., `data/backups/2026-W21/12345678/content/manifest.json`)
413
+ - Click **Run workflow**
414
+
415
+3. **Restore will:**
416
+ - Check out the `publish` branch
417
+ - Validate the backup manifest integrity
418
+ - Restore all backed-up files to their pre-replacement state
419
+ - Commit with message: `restore: publish backup {manifest_path}`
420
+ - Force-push to `publish` with lease safety guards
421
+
422
+4. **Monitor the restore:**
423
+ ```bash
424
+ gh run view --log -R YOUR_USERNAME/SquadScope
425
+ ```
426
+
427
+### Restore operation guarantees
428
+
429
+- **Immutable:** Backup manifests are never modified after creation
430
+- **Atomic:** Restore applies all backed-up files or fails with no partial changes
431
+- **Lease-guarded:** Force-push uses `--force-with-lease` to detect concurrent modifications
432
+- **Audited:** Restore commit message includes the backup manifest path for traceability
433
+- **Non-destructive:** Restoring does not delete new backups created since the restore date
434
+
435
+**After a restore:**
436
+- The `publish` branch is reverted to the state before that run
437
+- Previous good analysis remains published
438
+- The restore itself appears in git history for audit trail
439
+
440
+## No-AI Fallback Policy
441
+
442
+### Why no-AI is not a replacement strategy
443
+
444
+SquadScope includes a no-AI fallback analysis script (`scripts/analyze_fallback.py`) that can generate a basic summary using heuristics when Copilot is unavailable. However, **no-AI output is explicitly not a replacement for Copilot analysis** and has specific constraints:
445
+
446
+**No-AI fallback is used only when:**
447
+1. Copilot CLI fails with a non-auth error (after retries), OR
448
+2. Copilot encounters a non-recoverable error (e.g., context too large), OR
449
+3. Copilot is completely inaccessible
450
+
451
+**No-AI output characteristics:**
452
+- Lower quality_score (typically 40–50 vs. 60+ for Copilot)
453
+- Simple heuristic categorization (no editorial synthesis)
454
+- May have incomplete signal/noise/gaps sections
455
+- Preserved as a "rejected candidate" artifact
456
+- **Does not publish without explicit operator override**
457
+
458
+### Publish manifest promotion policy
459
+
460
+When Copilot fails and no-AI fallback is generated:
461
+1. The no-AI candidate is created and stored in `data/candidates/YYYY-WNN/RUN_ID/`
462
+2. The publish manifest records `analysis_source: "no-ai"` and `quality_validation: "failed"`
463
+3. **The promotion guard blocks publication** regardless of whether gates pass
464
+4. The prior week's good analysis remains published (safe default)
465
+
466
+To inspect a rejected no-AI candidate:
467
+
468
+```bash
469
+# List rejected candidates for week 2026-W21
470
+find data/candidates/2026-W21 -name 'candidate-no-ai-attempt-*.md'
471
+
472
+# Review the no-AI candidate and its gate report
473
+cat data/candidates/2026-W21/RUN_ID/diagnostics/candidate-no-ai-attempt-0.md
474
+cat data/candidates/2026-W21/RUN_ID/diagnostics/gate-no-ai-attempt-0.json | jq '.validation_failures'
475
+```
476
+
477
+### Copilot access failures
478
+
479
+If Copilot CLI fails with an authentication or access error:
480
+- The workflow **fails immediately without attempting no-AI fallback**
481
+- An issue is created (or updated) to notify the operator to renew `COPILOT_GH_TOKEN`
482
+- The failure report is available at `data/candidates/YYYY-WNN/RUN_ID/diagnostics/copilot-cli-failure-*.json`
483
+
484
+This ensures that **transient Copilot issues do not silently degrade to no-AI analysis.**
485
+
486
+### Copilot retries
487
+
488
+If Copilot produces output that doesn't pass the quality gate, the workflow automatically retries up to 3 times:
489
+- Each retry includes focused diagnostics from the prior gate failure
490
+- If all retries fail, no-AI fallback is attempted as a last resort
491
+- Each retry and its diagnostics are recorded for audit trail
492
+
493
+## Rejected Candidate Diagnostics
494
+
495
+When analysis fails to pass quality gates, detailed diagnostics are recorded for investigation:
496
+
497
+### Candidate directory structure
498
+
499
+```
500
+data/candidates/YYYY-WNN/RUN_ID/
501
+ ├── YYYY-WNN-summary.md # Candidate analysis (if produced)
502
+ ├── YYYY-WNN-content.md # Generated HTML candidate (if produced)
503
+ ├── publish-manifest.json # Eligibility and provenance
504
+ └── diagnostics/
505
+ ├── analysis-preflight.json # Pre-analysis context budget check
506
+ ├── analysis-preflight.md # Preflight diagnostic report
507
+ ├── copilot-cli-attempt-N.log # Raw Copilot CLI stderr/stdout
508
+ ├── gate-copilot-cli-attempt-N.json # Quality gate failure details
509
+ ├── candidate-copilot-cli-attempt-N.md # Candidate snapshot
510
+ ├── candidate-no-ai-attempt-0.md # No-AI fallback (if used)
511
+ └── gate-no-ai-attempt-0.json # No-AI gate report
512
+```
513
+
514
+### Examining a failed gate report
515
+
516
+```bash
517
+# View gate failure for attempt 0
518
+cat data/candidates/2026-W21/RUN_ID/diagnostics/gate-copilot-cli-attempt-0.json | jq '{
519
+ passed: .passed,
520
+ gates: .gates,
521
+ errors_before_repair: .errors_before_repair,
522
+ repair_actions: .repair_actions,
523
+ failure_class: .failure_class
524
+}'
525
+```
526
+
527
+Gate report output:
528
+```json
529
+{
530
+ "passed": false,
531
+ "gates": {
532
+ "structural_schema": {
533
+ "passed": false,
534
+ "errors": [
535
+ "Signal section is empty or malformed",
536
+ "Signal section must contain at least 3 significant claims"
537
+ ]
538
+ },
539
+ "editorial_quality": {
540
+ "passed": false,
541
+ "errors": [
542
+ "Noise section has fewer than 3 spurious claims"
543
+ ]
544
+ },
545
+ "ai_provenance": {
546
+ "passed": true,
547
+ "errors": []
548
+ },
549
+ "evidence_citation": {
550
+ "passed": true,
551
+ "errors": []
552
+ }
553
+ },
554
+ "errors_before_repair": [
555
+ "Signal section is empty or malformed",
556
+ "Signal section must contain at least 3 significant claims",
557
+ "Noise section has fewer than 3 spurious claims"
558
+ ],
559
+ "repair_actions": [
560
+ "Expanded Signal section with 3 significant claims from trending repositories",
561
+ "Added 3 spurious/false claims to Noise section"
562
+ ],
563
+ "errors_after_repair": [],
564
+ "failure_class": "passed"
565
+}
566
+```
567
+
568
+### Quality gate specifics
569
+
570
+The publish manifest records:
571
+- `quality_score`: 0–100, where 60+ is publishable
572
+- `quality_source`: "copilot-cli", "no-ai", etc.
573
+- `validation_status`: "passed" or "failed"
574
+- `validation_failures`: Array of specific failures with repair suggestions
575
+
576
+Failed gates block promotion but don't prevent the candidate from being stored for audit trail.
577
+
578
+## Map/Reduce Analysis Status (Dry-Run Only)
579
+
580
+### Why map/reduce remains experimental
581
+
582
+SquadScope's analysis pipeline is currently single-pass for production use. Map/reduce analysis—dividing evidence into smaller chunks, analyzing each independently, then combining results—is **only available as a dry-run experimental feature** and cannot publish. This decision was made after evidence from live runs and is documented in `docs/PRD-matrix-crawl-map-reduce-analysis.md`.
583
+
584
+### Current limitations preventing map/reduce publication
585
+
586
+1. **Analysis specification mismatch:** Map/reduce mapper outputs would create intermediate artifacts not conforming to `docs/analysis-spec.md`
587
+2. **Citation preservation:** Combining mapper outputs risks losing original citations and creating false attribution
588
+3. **Claim deduplication:** Reducer must reliably dedupe claims across mappers; no production-grade deduplication exists yet
589
+4. **Token accounting:** Final combined analysis may exceed token budgets; mechanism for bounded reduction is unproven
590
+5. **Quality gate compliance:** Existing gates expect single-pass analysis structure; map/reduce will need new gates
591
+
592
+### QA gates required before map/reduce can publish (#258)
593
+
594
+Before map/reduce analysis can be enabled for production promotion, all of these QA gates must pass:
595
+
596
+- [ ] **Mapper-reducer contract testing:** Mappers and reducers in sandboxed runs must produce deterministic, validated outputs
597
+- [ ] **Citation preservation testing:** Full analysis -> map/reduce roundtrip must preserve or improve citation count/accuracy
598
+- [ ] **Claim deduplication testing:** Reducer must reliably identify and merge duplicate claims across mappers
599
+- [ ] **Token budget compliance:** End-to-end analysis must stay within token limits; no truncation or quality regression
600
+- [ ] **Spec compliance testing:** Generated analysis must pass all existing `analysis_gate.py` checks without modification
601
+- [ ] **Human editorial review:** Blind A/B comparison of single-pass vs. map/reduce outputs from 4+ weeks of real data
602
+- [ ] **Fallback behavior:** Ensure Copilot retries and no-AI fallback work correctly with map/reduce logic
603
+
604
+### Testing map/reduce in dry-run mode
605
+
606
+To test map/reduce without risk of publication:
607
+
608
+```bash
609
+gh workflow run crawl-and-publish.yml \
610
+ -R YOUR_USERNAME/SquadScope \
611
+ -f "run_mode=dry-run"
612
+```
613
+
614
+Dry-run mode ensures:
615
+- Analysis is generated but never promoted
616
+- No HTML is published
617
+- Candidates are stored for inspection
618
+- You can review results before any live traffic sees them
619
+
620
+### Expected timeline
621
+
622
+Map/reduce publication will be enabled in a future phase after:
623
+- All QA gates (#258) are implemented and passing
624
+- Human review confirms output quality meets or exceeds single-pass
625
+- Operator documentation is completed
626
+- Rollback procedures are tested
627
+
628
## Monitoring the Cron Schedule
629
630
### View recent runs