Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 196 additions & 0 deletions docs/campaigns/2026-05-26/01-triage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
<!-- SPDX-License-Identifier: MPL-2.0 -->
<!-- SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) -->

# 01-triage — algorithmic spec

> Historical campaign snapshot from 2026-05-26. The original `01-triage.ts`
> (183 lines, Deno TypeScript) is removed per the standards#239 estate
> TypeScript → AffineScript migration. This document preserves the
> algorithmic spec so a future AffineScript port can land cleanly when the
> stdlib gaps surfaced below (feeding standards#242 STEP 3) close.

## Purpose

Reads per-repo `AssailReport` JSONs, classifies each weak-point finding into
one of six buckets, groups the actionable buckets by `(repo, file_dir,
category)`, and emits a JSON summary describing the PR-candidate plan.

## Inputs

- `<per-repo-dir>`: directory of `<repo>.json` files (one per repo), each
an `AssailReport` produced by `panic-attack` assail.
- `<plan.json>`: output path for the summary.

## Domain types

```text
Severity := Low | Medium | High | Critical

WeakPoint :=
{ category: PA-code or alias
, file: optional source path
, line: optional line number
, severity: Severity
, description: free text
, suppressed: bool
}

AssailReport :=
{ schema_version: semver
, program_path: path
, language: identifier
, weak_points: [WeakPoint]
, suppressed_count?: int
}

PrCandidate := WeakPoint + repo + bucket + canonical category code
```

## Static policy tables

### Proof-file extensions
`.lean, .agda, .lagda, .v, .idr, .idr2, .fst, .fsti, .thy, .spthy, .smt2, .tla`

### Categories with reliable automated fixes (Critical / High only)
`PA001/UnsafeCode, PA006/PanicPath, PA022/CryptoMisuse`

### Categories requiring human judgement (always → issue, never auto-fix)
`PA023/SupplyChain, PA024/InputBoundary, PA025/MutationGap, PA021/ProofDrift`

### Parked proof debts (skip wholesale)
- `ephapax / formal/Semantics.v:3327` (preservation, deferred per the
ephapax-preservation-closure-plan).
- `betlang / *` where description contains `substTop_preserves_typing`
(discharge recipe in PR#27 body).

### Category alias → canonical PA-code mapping
Recognises both `PA\d{3}` codes (passed through) and the enum-name
aliases `UnsafeCode → PA001, PanicPath → PA006, CommandInjection → PA003,
UnsafeDeserialization → PA004, AtomExhaustion → PA005, UnsafeFFI → PA007,
PathTraversal → PA008, HardcodedSecret → PA009, ProofDrift → PA021,
CryptoMisuse → PA022, SupplyChain → PA023, InputBoundary → PA024,
MutationGap → PA025`. Unknown values pass through unchanged.

## Classification algorithm

For each `(repo, wp)`:

1. `wp.suppressed` → `skip-suppressed`.
2. severity not in `{Critical, High}` → `skip-unknown-cat` (out of scope this
wave; the bucket name is historical and load-bearing in summaries).
3. `(repo, wp)` matches a parked-debt row → `skip-known`.
4. `wp.file` contains `.claude/worktrees/` or `/_wt-` → `skip-known` (main
checkout is the source of truth, not worktree branches).
5. `wp.file` extension in proof-file set → `proof-draft`.
6. Canonical category code in `issue_only` set → `issue`.
7. Canonical category code in `autofix_ok` set → `autofix`.
8. Otherwise → `issue` (default conservative — needs human eye).

## PR grouping

Group all non-skip candidates by the key
`<repo>::<file_dir>::<category>`. `file_dir` is the parent directory of
`wp.file` (joined by `/`), so all findings under the same directory and
category collapse into one PR.

## Output summary shape

```jsonc
{
"generated_at": "<UTC ISO8601>",
"per_repo_scanned": <int>,
"total_candidates": <int>,
"by_bucket": { "<bucket>": <int>, ... },
"by_repo": { "<repo>": <non-skip count>, ... }, // only repos with >0
"pr_groups": [
{
"key": "<repo>::<file_dir>::<category>",
"repo": "<repo>",
"file_dir": "<file_dir>",
"category": "<PA-code>",
"bucket": "<bucket>",
"finding_count": <int>,
"severities": ["<Severity>", ...],
"examples": [<PrCandidate>, ...] // up to 3
}
]
}
```

## Side-channel logs (stderr)

```
triage complete: <N> candidates, <G> PR groups → <plan.json>
buckets: { "<bucket>": <int>, ... }
```

## Stdlib gaps surfaced for the AffineScript port (feeds standards#242 STEP 3)

This script depends on the following Deno / TS-stdlib surfaces that have no
direct equivalent in `stdlib/Deno.affine` or `stdlib/json.affine` today.
Each gap is a candidate fill-in for STEP 3 (stdlib enrichment) before the
port can land cleanly.

1. **`Set<T>` membership** — no native AS surface. Workaround: `[String]`
with linear-scan `containsString`. Acceptable for the ≤25-entry policy
lists this script uses; not a general-purpose Set.

2. **`Map<String, [T]>` group-by** — no native AS surface. Workaround:
`[(String, [PrCandidate])]` association list. O(n²) but n is small
here. A real Map binding would be a broader stdlib win.

3. **Async generators (`async function* walk(...)`)** — no AS surface.
Workaround: collapse to eager `[String]` list collection via
`Deno::walkRecursive` (already in `Deno.affine`).

4. **`JSON.parse` returning a typed sum** — `Deno::jsonParse` returns the
opaque `Deno.Json`, with field access via discrete `jsonGet*` externs.
Reading nested `AssailReport.weak_points[i].category` is many extern
calls. A typed-decoder generator (`json.affine` style) is the cleaner
path. The `json.affine` v0.3 work (mentioned in RSR-stack status memo)
would close this.

5. **Regex object construction** — TS uses `new RegExp(pat).test(s)` and
inline `/^PA\d{3}/.test(cat)`. `Deno::regexMatch(s, pat)` covers the
call shape but does not expose a constructed regex value — fine for
this script.

6. **`new Date().toISOString()`** — needed for `generated_at` and the
per-PrCandidate `lastProbe` analogue. `Deno.affine` has `dateNow`
(returns ms) but no ISO8601 formatter. Would need `dateNowIso` extern.

7. **`Object.fromEntries(...)` for dynamic object construction** — needed
for the `by_bucket` / `by_repo` summary objects. `Deno.affine` exposes
`jsonNull` / `jsonStringify` but no `jsonObjectFromPairs` builder
extern. Workaround: build the JSON string manually.

8. **Optional chaining `wp.file?.endsWith(...)`** — explicit Option/match
on the AS side (`wp.file != "" && Deno::endsWith(...)`). Working as
intended; just verbose.

9. **`async / await` on synchronous-effect FS calls** — `Deno.affine`
externs are all sync (per the `Deno.affine` header comment); the port
loses the async machinery, which is the right call for the Deno-ESM
backend.

10. **Spread / rest patterns in destructuring** — TS uses
`[...new Set(candidates.map(c => c.repo))].sort()` etc. Each call
site needs an explicit `unique_sort_string` helper in AS.

## Reference: original implementation

Removed in this PR — see git history for the full TS source. The original
referenced these external surfaces:

- `https://deno.land/std@0.224.0/fs/walk.ts`
- `https://deno.land/std@0.224.0/path/mod.ts`
- `Deno.{args, readTextFile, writeTextFile, exit}`
- Built-ins: `Set`, `Map`, `Date`, `Object`, `JSON`, `RegExp`, `console`

## Migration status

- 🟡 Spec preserved (this document) — 2026-05-30.
- 🔴 `.affine` implementation pending stdlib fill-in per gaps 1–4, 6, 7
above. Tracking: standards#242 STEP 3.
- 🟢 `01-triage.ts` removed (campaign artefact; logic deferred to future
panic-attack runs once the AffineScript port lands).
183 changes: 0 additions & 183 deletions docs/campaigns/2026-05-26/01-triage.ts

This file was deleted.

Loading