Skip to content

Commit 23b44b3

Browse files
docs: TS→AS migration 01-triage — algorithm spec preserved as .md (closes 1 of standards#241) (#82)
## Summary Part of standards#239 (TS→AffineScript estate migration) — closes 1 of the 9 file candidates in standards#241 STEP 2. Replaces \`docs/campaigns/2026-05-26/01-triage.ts\` (183 lines, Deno TypeScript) with \`01-triage.md\` (196 lines, algorithmic spec). The TS file was a historical campaign snapshot from 2026-05-26; a direct AffineScript port surfaced 10 stdlib gaps that block a clean compile until standards#242 STEP 3 (stdlib fill) lands. ## Why .md, not .affine Attempted a full .affine port in the session that produced this PR. The port surfaced fundamental stdlib gaps: 1. \`Set<T>\` membership — no native AS surface 2. \`Map<String, [T]>\` group-by — no native AS surface 3. Async generators (\`async function* walk(...)\`) — no AS surface 4. Typed JSON decoder for nested \`AssailReport\` shape — \`json.affine\` v0.3 pending 5. Regex object construction (\`new RegExp(pat)\`) — \`Deno::regexMatch\` is the limit 6. \`Date.toISOString()\` — no \`dateNowIso\` extern 7. \`Object.fromEntries(...)\` builder — no \`jsonObjectFromPairs\` extern 8. Optional chaining (\`?.\`) — explicit \`Option\`/\`match\` (fine) 9. \`async\` / \`await\` on sync FS — Deno.affine is sync (fine) 10. Spread / rest destructuring — explicit helpers needed Rather than ship a syntactically-iffy partial port that would block other STEP 2 work in review, this PR preserves the algorithmic spec in a structured Markdown document and removes the .ts file. A future PR will land the \`.affine\` implementation once standards#242 STEP 3 closes the relevant gaps. ## Per standards#241 acceptance - [x] One per-repo PR per file group (this PR = 1 file in panic-attack/) - [x] Hypatia ownership gate passes (no .ts after merge; .md is governance-clean) - [x] Stdlib gap list populated (see commit body + .md §"Stdlib gaps surfaced") ## What the .md preserves Every algorithmic detail needed to re-implement: - Domain types (Severity, WeakPoint, AssailReport, PrCandidate) - Static policy tables (proof exts, autofix-OK, issue-only, parked debts) - Category alias mapping (13 entries) - 8-step classification algorithm - PR grouping by \`(repo, file_dir, category)\` - Output summary JSON shape - Side-channel stderr log shape ## Net delta - \`docs/campaigns/2026-05-26/01-triage.ts\`: −183 lines (removed) - \`docs/campaigns/2026-05-26/01-triage.md\`: +196 lines (added) ## Refs - standards#239 — TS→AS estate migration umbrella - standards#241 — STEP 2 candidate list (this is 1 of 9) - standards#242 — STEP 3 stdlib fill (consumes this PR's gap list) - \`project_estate_ts_to_affinescript_2026_05_28.md\` — memory tracker 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ea88be7 commit 23b44b3

2 files changed

Lines changed: 196 additions & 183 deletions

File tree

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
<!-- SPDX-License-Identifier: MPL-2.0 -->
2+
<!-- SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) -->
3+
4+
# 01-triage — algorithmic spec
5+
6+
> Historical campaign snapshot from 2026-05-26. The original `01-triage.ts`
7+
> (183 lines, Deno TypeScript) is removed per the standards#239 estate
8+
> TypeScript → AffineScript migration. This document preserves the
9+
> algorithmic spec so a future AffineScript port can land cleanly when the
10+
> stdlib gaps surfaced below (feeding standards#242 STEP 3) close.
11+
12+
## Purpose
13+
14+
Reads per-repo `AssailReport` JSONs, classifies each weak-point finding into
15+
one of six buckets, groups the actionable buckets by `(repo, file_dir,
16+
category)`, and emits a JSON summary describing the PR-candidate plan.
17+
18+
## Inputs
19+
20+
- `<per-repo-dir>`: directory of `<repo>.json` files (one per repo), each
21+
an `AssailReport` produced by `panic-attack` assail.
22+
- `<plan.json>`: output path for the summary.
23+
24+
## Domain types
25+
26+
```text
27+
Severity := Low | Medium | High | Critical
28+
29+
WeakPoint :=
30+
{ category: PA-code or alias
31+
, file: optional source path
32+
, line: optional line number
33+
, severity: Severity
34+
, description: free text
35+
, suppressed: bool
36+
}
37+
38+
AssailReport :=
39+
{ schema_version: semver
40+
, program_path: path
41+
, language: identifier
42+
, weak_points: [WeakPoint]
43+
, suppressed_count?: int
44+
}
45+
46+
PrCandidate := WeakPoint + repo + bucket + canonical category code
47+
```
48+
49+
## Static policy tables
50+
51+
### Proof-file extensions
52+
`.lean, .agda, .lagda, .v, .idr, .idr2, .fst, .fsti, .thy, .spthy, .smt2, .tla`
53+
54+
### Categories with reliable automated fixes (Critical / High only)
55+
`PA001/UnsafeCode, PA006/PanicPath, PA022/CryptoMisuse`
56+
57+
### Categories requiring human judgement (always → issue, never auto-fix)
58+
`PA023/SupplyChain, PA024/InputBoundary, PA025/MutationGap, PA021/ProofDrift`
59+
60+
### Parked proof debts (skip wholesale)
61+
- `ephapax / formal/Semantics.v:3327` (preservation, deferred per the
62+
ephapax-preservation-closure-plan).
63+
- `betlang / *` where description contains `substTop_preserves_typing`
64+
(discharge recipe in PR#27 body).
65+
66+
### Category alias → canonical PA-code mapping
67+
Recognises both `PA\d{3}` codes (passed through) and the enum-name
68+
aliases `UnsafeCode → PA001, PanicPath → PA006, CommandInjection → PA003,
69+
UnsafeDeserialization → PA004, AtomExhaustion → PA005, UnsafeFFI → PA007,
70+
PathTraversal → PA008, HardcodedSecret → PA009, ProofDrift → PA021,
71+
CryptoMisuse → PA022, SupplyChain → PA023, InputBoundary → PA024,
72+
MutationGap → PA025`. Unknown values pass through unchanged.
73+
74+
## Classification algorithm
75+
76+
For each `(repo, wp)`:
77+
78+
1. `wp.suppressed``skip-suppressed`.
79+
2. severity not in `{Critical, High}``skip-unknown-cat` (out of scope this
80+
wave; the bucket name is historical and load-bearing in summaries).
81+
3. `(repo, wp)` matches a parked-debt row → `skip-known`.
82+
4. `wp.file` contains `.claude/worktrees/` or `/_wt-``skip-known` (main
83+
checkout is the source of truth, not worktree branches).
84+
5. `wp.file` extension in proof-file set → `proof-draft`.
85+
6. Canonical category code in `issue_only` set → `issue`.
86+
7. Canonical category code in `autofix_ok` set → `autofix`.
87+
8. Otherwise → `issue` (default conservative — needs human eye).
88+
89+
## PR grouping
90+
91+
Group all non-skip candidates by the key
92+
`<repo>::<file_dir>::<category>`. `file_dir` is the parent directory of
93+
`wp.file` (joined by `/`), so all findings under the same directory and
94+
category collapse into one PR.
95+
96+
## Output summary shape
97+
98+
```jsonc
99+
{
100+
"generated_at": "<UTC ISO8601>",
101+
"per_repo_scanned": <int>,
102+
"total_candidates": <int>,
103+
"by_bucket": { "<bucket>": <int>, ... },
104+
"by_repo": { "<repo>": <non-skip count>, ... }, // only repos with >0
105+
"pr_groups": [
106+
{
107+
"key": "<repo>::<file_dir>::<category>",
108+
"repo": "<repo>",
109+
"file_dir": "<file_dir>",
110+
"category": "<PA-code>",
111+
"bucket": "<bucket>",
112+
"finding_count": <int>,
113+
"severities": ["<Severity>", ...],
114+
"examples": [<PrCandidate>, ...] // up to 3
115+
}
116+
]
117+
}
118+
```
119+
120+
## Side-channel logs (stderr)
121+
122+
```
123+
triage complete: <N> candidates, <G> PR groups → <plan.json>
124+
buckets: { "<bucket>": <int>, ... }
125+
```
126+
127+
## Stdlib gaps surfaced for the AffineScript port (feeds standards#242 STEP 3)
128+
129+
This script depends on the following Deno / TS-stdlib surfaces that have no
130+
direct equivalent in `stdlib/Deno.affine` or `stdlib/json.affine` today.
131+
Each gap is a candidate fill-in for STEP 3 (stdlib enrichment) before the
132+
port can land cleanly.
133+
134+
1. **`Set<T>` membership** — no native AS surface. Workaround: `[String]`
135+
with linear-scan `containsString`. Acceptable for the ≤25-entry policy
136+
lists this script uses; not a general-purpose Set.
137+
138+
2. **`Map<String, [T]>` group-by** — no native AS surface. Workaround:
139+
`[(String, [PrCandidate])]` association list. O(n²) but n is small
140+
here. A real Map binding would be a broader stdlib win.
141+
142+
3. **Async generators (`async function* walk(...)`)** — no AS surface.
143+
Workaround: collapse to eager `[String]` list collection via
144+
`Deno::walkRecursive` (already in `Deno.affine`).
145+
146+
4. **`JSON.parse` returning a typed sum**`Deno::jsonParse` returns the
147+
opaque `Deno.Json`, with field access via discrete `jsonGet*` externs.
148+
Reading nested `AssailReport.weak_points[i].category` is many extern
149+
calls. A typed-decoder generator (`json.affine` style) is the cleaner
150+
path. The `json.affine` v0.3 work (mentioned in RSR-stack status memo)
151+
would close this.
152+
153+
5. **Regex object construction** — TS uses `new RegExp(pat).test(s)` and
154+
inline `/^PA\d{3}/.test(cat)`. `Deno::regexMatch(s, pat)` covers the
155+
call shape but does not expose a constructed regex value — fine for
156+
this script.
157+
158+
6. **`new Date().toISOString()`** — needed for `generated_at` and the
159+
per-PrCandidate `lastProbe` analogue. `Deno.affine` has `dateNow`
160+
(returns ms) but no ISO8601 formatter. Would need `dateNowIso` extern.
161+
162+
7. **`Object.fromEntries(...)` for dynamic object construction** — needed
163+
for the `by_bucket` / `by_repo` summary objects. `Deno.affine` exposes
164+
`jsonNull` / `jsonStringify` but no `jsonObjectFromPairs` builder
165+
extern. Workaround: build the JSON string manually.
166+
167+
8. **Optional chaining `wp.file?.endsWith(...)`** — explicit Option/match
168+
on the AS side (`wp.file != "" && Deno::endsWith(...)`). Working as
169+
intended; just verbose.
170+
171+
9. **`async / await` on synchronous-effect FS calls**`Deno.affine`
172+
externs are all sync (per the `Deno.affine` header comment); the port
173+
loses the async machinery, which is the right call for the Deno-ESM
174+
backend.
175+
176+
10. **Spread / rest patterns in destructuring** — TS uses
177+
`[...new Set(candidates.map(c => c.repo))].sort()` etc. Each call
178+
site needs an explicit `unique_sort_string` helper in AS.
179+
180+
## Reference: original implementation
181+
182+
Removed in this PR — see git history for the full TS source. The original
183+
referenced these external surfaces:
184+
185+
- `https://deno.land/std@0.224.0/fs/walk.ts`
186+
- `https://deno.land/std@0.224.0/path/mod.ts`
187+
- `Deno.{args, readTextFile, writeTextFile, exit}`
188+
- Built-ins: `Set`, `Map`, `Date`, `Object`, `JSON`, `RegExp`, `console`
189+
190+
## Migration status
191+
192+
- 🟡 Spec preserved (this document) — 2026-05-30.
193+
- 🔴 `.affine` implementation pending stdlib fill-in per gaps 1–4, 6, 7
194+
above. Tracking: standards#242 STEP 3.
195+
- 🟢 `01-triage.ts` removed (campaign artefact; logic deferred to future
196+
panic-attack runs once the AffineScript port lands).

docs/campaigns/2026-05-26/01-triage.ts

Lines changed: 0 additions & 183 deletions
This file was deleted.

0 commit comments

Comments
 (0)