@iceinvein/agent-skills 0.1.39 → 0.2.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.
- package/README.md +18 -2
- package/dist/cli/index.js +105 -28
- package/package.json +1 -1
- package/skills/index.json +14 -2
- package/skills/magpie/README.md +2 -1
- package/skills/magpie/SKILL.md +138 -45
- package/skills/magpie/bin/magpie.ts +43 -0
- package/skills/magpie/fixtures/example-pr/brief.json +18 -0
- package/skills/magpie/fixtures/fake-gh-nodiff.sh +38 -0
- package/skills/magpie/fixtures/fake-gh.sh +14 -0
- package/skills/magpie/package.json +1 -1
- package/skills/magpie/references/peer-review.md +7 -2
- package/skills/magpie/references/scout.md +90 -0
- package/skills/magpie/references/specialists.md +117 -6
- package/skills/magpie/scripts/__tests__/cli.test.ts +101 -1
- package/skills/magpie/scripts/__tests__/dedupe-cmd.test.ts +187 -0
- package/skills/magpie/scripts/__tests__/diff-chunks.test.ts +51 -0
- package/skills/magpie/scripts/__tests__/filter-diff-preservation.test.ts +54 -0
- package/skills/magpie/scripts/__tests__/findings-files.test.ts +35 -0
- package/skills/magpie/scripts/__tests__/gh.test.ts +90 -0
- package/skills/magpie/scripts/__tests__/git-diff.test.ts +83 -0
- package/skills/magpie/scripts/__tests__/helpers/git-fixture.ts +47 -0
- package/skills/magpie/scripts/__tests__/path-filter.test.ts +27 -0
- package/skills/magpie/scripts/__tests__/preview-cmd.test.ts +16 -0
- package/skills/magpie/scripts/__tests__/refresh.test.ts +45 -0
- package/skills/magpie/scripts/__tests__/render-cmd.test.ts +173 -1
- package/skills/magpie/scripts/__tests__/render-findings.test.ts +151 -1
- package/skills/magpie/scripts/__tests__/render-progress.test.ts +42 -0
- package/skills/magpie/scripts/__tests__/setup-cmd.test.ts +83 -1
- package/skills/magpie/scripts/__tests__/shard.test.ts +165 -0
- package/skills/magpie/scripts/__tests__/skill-lint.test.ts +207 -7
- package/skills/magpie/scripts/__tests__/types.test.ts +47 -0
- package/skills/magpie/scripts/dedupe-cmd.ts +58 -3
- package/skills/magpie/scripts/diff-chunks.ts +28 -0
- package/skills/magpie/scripts/findings-files.ts +32 -0
- package/skills/magpie/scripts/gh.ts +68 -14
- package/skills/magpie/scripts/git-diff.ts +111 -0
- package/skills/magpie/scripts/path-filter.ts +9 -5
- package/skills/magpie/scripts/preview-cmd.ts +11 -1
- package/skills/magpie/scripts/refresh.ts +32 -3
- package/skills/magpie/scripts/render-cmd.ts +35 -11
- package/skills/magpie/scripts/render-findings.ts +78 -2
- package/skills/magpie/scripts/render-progress.ts +6 -1
- package/skills/magpie/scripts/setup-cmd.ts +38 -1
- package/skills/magpie/scripts/shard.ts +171 -0
- package/skills/magpie/scripts/status-cmd.ts +7 -4
- package/skills/magpie/scripts/types.ts +50 -0
- package/skills/magpie/skill.json +2 -2
- package/skills/magpie/templates/styles.css +66 -0
- package/skills/migrate/README.md +194 -0
- package/skills/migrate/SKILL.md +197 -0
- package/skills/migrate/bin/migrate +15 -0
- package/skills/migrate/bin/migrate.ts +309 -0
- package/skills/migrate/biome.json +35 -0
- package/skills/migrate/bun.lock +24 -0
- package/skills/migrate/docs/architecture.md +294 -0
- package/skills/migrate/docs/reference.md +590 -0
- package/skills/migrate/fixtures/tiny-express/GROUND-TRUTH.md +39 -0
- package/skills/migrate/fixtures/tiny-express/app.js +29 -0
- package/skills/migrate/fixtures/tiny-express/cron.js +6 -0
- package/skills/migrate/fixtures/tiny-express/reports/daily-users.json +6 -0
- package/skills/migrate/fixtures/tiny-express/schema.sql +12 -0
- package/skills/migrate/fixtures/tiny-express/settings.json +4 -0
- package/skills/migrate/fixtures/tiny-express/views/users.html +9 -0
- package/skills/migrate/fixtures/tiny-webforms/Controllers/UsersController.cs +68 -0
- package/skills/migrate/fixtures/tiny-webforms/Default.aspx +7 -0
- package/skills/migrate/fixtures/tiny-webforms/Default.aspx.cs +14 -0
- package/skills/migrate/fixtures/tiny-webforms/GROUND-TRUTH.md +50 -0
- package/skills/migrate/fixtures/tiny-webforms/Integrations/BillingClient.cs +16 -0
- package/skills/migrate/fixtures/tiny-webforms/Jobs/NightlyDigestJob.cs +33 -0
- package/skills/migrate/fixtures/tiny-webforms/Reports/DailyUsers.rdl +11 -0
- package/skills/migrate/fixtures/tiny-webforms/Schema.sql +12 -0
- package/skills/migrate/fixtures/tiny-webforms/Site.master +16 -0
- package/skills/migrate/fixtures/tiny-webforms/Users.aspx +8 -0
- package/skills/migrate/fixtures/tiny-webforms/Users.aspx.cs +14 -0
- package/skills/migrate/fixtures/tiny-webforms/web.config +10 -0
- package/skills/migrate/install.sh +68 -0
- package/skills/migrate/package.json +17 -0
- package/skills/migrate/references/phases/enumerate.md +291 -0
- package/skills/migrate/references/phases/extract.md +652 -0
- package/skills/migrate/references/phases/parity.md +275 -0
- package/skills/migrate/references/phases/probe.md +135 -0
- package/skills/migrate/references/phases/queue.md +242 -0
- package/skills/migrate/references/phases/seam.md +416 -0
- package/skills/migrate/references/recipes/README.md +116 -0
- package/skills/migrate/references/recipes/aspnet.md +287 -0
- package/skills/migrate/references/run-ops.md +280 -0
- package/skills/migrate/scripts/__tests__/census.test.ts +775 -0
- package/skills/migrate/scripts/__tests__/check.test.ts +458 -0
- package/skills/migrate/scripts/__tests__/citations.test.ts +156 -0
- package/skills/migrate/scripts/__tests__/cli.test.ts +183 -0
- package/skills/migrate/scripts/__tests__/concurrency.test.ts +164 -0
- package/skills/migrate/scripts/__tests__/config.test.ts +112 -0
- package/skills/migrate/scripts/__tests__/e2e-express.test.ts +1093 -0
- package/skills/migrate/scripts/__tests__/e2e-webforms.test.ts +1276 -0
- package/skills/migrate/scripts/__tests__/e2e.test.ts +320 -0
- package/skills/migrate/scripts/__tests__/ids.test.ts +38 -0
- package/skills/migrate/scripts/__tests__/import.test.ts +155 -0
- package/skills/migrate/scripts/__tests__/init.test.ts +192 -0
- package/skills/migrate/scripts/__tests__/leaks.test.ts +176 -0
- package/skills/migrate/scripts/__tests__/lock.test.ts +183 -0
- package/skills/migrate/scripts/__tests__/paths.test.ts +129 -0
- package/skills/migrate/scripts/__tests__/phase-cmd.test.ts +151 -0
- package/skills/migrate/scripts/__tests__/phases.test.ts +70 -0
- package/skills/migrate/scripts/__tests__/queue.test.ts +475 -0
- package/skills/migrate/scripts/__tests__/report.test.ts +150 -0
- package/skills/migrate/scripts/__tests__/run-state.test.ts +136 -0
- package/skills/migrate/scripts/__tests__/status-reset.test.ts +318 -0
- package/skills/migrate/scripts/__tests__/store.test.ts +132 -0
- package/skills/migrate/scripts/__tests__/validate.test.ts +54 -0
- package/skills/migrate/scripts/census-cmd.ts +109 -0
- package/skills/migrate/scripts/census.ts +342 -0
- package/skills/migrate/scripts/check-cmd.ts +24 -0
- package/skills/migrate/scripts/check.ts +376 -0
- package/skills/migrate/scripts/citations.ts +92 -0
- package/skills/migrate/scripts/config.ts +237 -0
- package/skills/migrate/scripts/ids.ts +31 -0
- package/skills/migrate/scripts/import-cmd.ts +141 -0
- package/skills/migrate/scripts/init-cmd.ts +118 -0
- package/skills/migrate/scripts/leaks.ts +184 -0
- package/skills/migrate/scripts/lock.ts +188 -0
- package/skills/migrate/scripts/paths.ts +103 -0
- package/skills/migrate/scripts/phase-cmd.ts +63 -0
- package/skills/migrate/scripts/phases.ts +113 -0
- package/skills/migrate/scripts/queue-cmd.ts +98 -0
- package/skills/migrate/scripts/queue.ts +258 -0
- package/skills/migrate/scripts/report-cmd.ts +47 -0
- package/skills/migrate/scripts/report.ts +131 -0
- package/skills/migrate/scripts/reset-cmd.ts +120 -0
- package/skills/migrate/scripts/status-cmd.ts +52 -0
- package/skills/migrate/scripts/store.ts +159 -0
- package/skills/migrate/scripts/types.ts +137 -0
- package/skills/migrate/scripts/validate.ts +221 -0
- package/skills/migrate/skill.json +33 -0
- package/skills/migrate/templates/config.toml +27 -0
- package/skills/migrate/templates/queue-item.md +17 -0
- package/skills/migrate/tsconfig.json +18 -0
- package/skills/migrate/uninstall.sh +31 -0
- package/skills/sluice/SKILL.md +82 -0
- package/skills/sluice/references/deep-channel.md +94 -0
- package/skills/sluice/references/finish.md +35 -0
- package/skills/sluice/references/intent.md +29 -0
- package/skills/sluice/references/review.md +42 -0
- package/skills/sluice/references/root-cause.md +38 -0
- package/skills/sluice/references/show-or-say.md +36 -0
- package/skills/sluice/references/test-first.md +35 -0
- package/skills/sluice/references/verify.md +26 -0
- package/skills/sluice/skill.json +32 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { runCheck } from './check.ts'
|
|
2
|
+
import { loadConfig } from './config.ts'
|
|
3
|
+
import { storePaths } from './paths.ts'
|
|
4
|
+
import { loadPhases, PHASES } from './phases.ts'
|
|
5
|
+
import { loadQueue } from './queue.ts'
|
|
6
|
+
import { readRows } from './store.ts'
|
|
7
|
+
import type { Element, Requirement } from './types.ts'
|
|
8
|
+
|
|
9
|
+
export async function runStatus(opts: { root: string }): Promise<number> {
|
|
10
|
+
const cfg = await loadConfig(opts.root)
|
|
11
|
+
const p = storePaths(opts.root)
|
|
12
|
+
const phases = await loadPhases(opts.root)
|
|
13
|
+
const elements = await readRows<Element>(p.elements)
|
|
14
|
+
const requirements = await readRows<Requirement>(p.requirements)
|
|
15
|
+
const { items, errors } = await loadQueue(p.queueDir)
|
|
16
|
+
// A malformed queue item on disk must not be invisible here: `check`
|
|
17
|
+
// flags it and exits 1, `queue list` flags it and exits 1, and status
|
|
18
|
+
// reporting the same store at exit 0 with no mention of it would read as
|
|
19
|
+
// "nothing else to look at" when there is.
|
|
20
|
+
for (const e of errors) process.stderr.write(`status: ${e}\n`)
|
|
21
|
+
const open = items.filter((i) => i.status === 'open')
|
|
22
|
+
|
|
23
|
+
process.stdout.write(`source: ${cfg.source.path} (${cfg.source.stack}, ${cfg.source.basis})\n`)
|
|
24
|
+
process.stdout.write(`scope: ${cfg.source.scope}\n\n`)
|
|
25
|
+
|
|
26
|
+
for (const phase of PHASES) {
|
|
27
|
+
const state = phases[phase]
|
|
28
|
+
const batches = state.batches.length > 0 ? ` (${state.batches.length} batch(es))` : ''
|
|
29
|
+
process.stdout.write(` ${phase.padEnd(11)} ${state.status}${batches}\n`)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const { summary } = await runCheck({ root: opts.root })
|
|
33
|
+
process.stdout.write(`\n${summary}\n`)
|
|
34
|
+
process.stdout.write(`${requirements.length} requirement(s), ${elements.length} element(s)\n`)
|
|
35
|
+
process.stdout.write(`${open.length} open queue item(s) of ${items.length}\n`)
|
|
36
|
+
|
|
37
|
+
const active = PHASES.find((ph) => phases[ph].status !== 'done')
|
|
38
|
+
if (active) {
|
|
39
|
+
const last = phases[active].batches.at(-1)
|
|
40
|
+
const from = last ? `last batch ${last.id} (${last.count} row(s))` : 'no batches yet'
|
|
41
|
+
process.stdout.write(`\nresume: ${active}, ${from}\n`)
|
|
42
|
+
if (phases[active].pending.length > 0) {
|
|
43
|
+
process.stdout.write(`pending: ${phases[active].pending.join(', ')}\n`)
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
process.stdout.write('\nresume: all phases done\n')
|
|
47
|
+
}
|
|
48
|
+
// Same class and exit code as `queue list` for the identical condition: a
|
|
49
|
+
// content failure on an otherwise well-formed request, not a usage error
|
|
50
|
+
// (2) and not success (0) while part of the queue could not be read.
|
|
51
|
+
return errors.length > 0 ? 1 : 0
|
|
52
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { readFile, rename, unlink, writeFile } from 'node:fs/promises'
|
|
4
|
+
import { assertNotUnderSource } from './paths.ts'
|
|
5
|
+
|
|
6
|
+
// Reads one JSON file and parses it, or throws a clean diagnostic naming the
|
|
7
|
+
// path and the reason (missing file, or content that isn't valid JSON)
|
|
8
|
+
// instead of letting readFile/JSON.parse's raw error escape. Shared by every
|
|
9
|
+
// command that takes a single JSON file as an argument, so each one only has
|
|
10
|
+
// to decide what its own exit code means, not how to report a bad file.
|
|
11
|
+
export async function readJsonFile(path: string): Promise<unknown> {
|
|
12
|
+
let text: string
|
|
13
|
+
try {
|
|
14
|
+
text = await readFile(path, 'utf8')
|
|
15
|
+
} catch {
|
|
16
|
+
throw new Error(`${path}: not found`)
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(text)
|
|
20
|
+
} catch {
|
|
21
|
+
throw new Error(`${path}: not valid JSON`)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Reads one markdown file as text, or throws a clean diagnostic naming the
|
|
26
|
+
// path first (missing file, a directory where a file was expected, or an
|
|
27
|
+
// unreadable file) instead of letting readFile's raw error escape. Queue
|
|
28
|
+
// items are markdown, not JSON, so this is readJsonFile's sibling: same
|
|
29
|
+
// message convention, no JSON parsing.
|
|
30
|
+
export async function readTextFile(path: string): Promise<string> {
|
|
31
|
+
try {
|
|
32
|
+
return await readFile(path, 'utf8')
|
|
33
|
+
} catch (e) {
|
|
34
|
+
const code = (e as { code?: string }).code
|
|
35
|
+
if (code === 'EISDIR') throw new Error(`${path}: is a directory, not a file`)
|
|
36
|
+
if (code === 'EACCES') throw new Error(`${path}: permission denied`)
|
|
37
|
+
throw new Error(`${path}: not found`)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Parses each line without asserting a type onto it, and keeps the 1-based
|
|
42
|
+
// line number each value came from. readRows below is the common case (a
|
|
43
|
+
// caller that trusts its own writer), but a gate checking a file that could
|
|
44
|
+
// have been hand-edited needs both: the unknown value, so it can run real
|
|
45
|
+
// validation instead of a cast that only satisfies the compiler; and the
|
|
46
|
+
// line number, so a violation can point at something the reader can find in
|
|
47
|
+
// the file rather than just naming the record's own fields.
|
|
48
|
+
export async function readRawRows(path: string): Promise<{ line: number; raw: unknown }[]> {
|
|
49
|
+
if (!existsSync(path)) return []
|
|
50
|
+
const text = await readFile(path, 'utf8')
|
|
51
|
+
const rows: { line: number; raw: unknown }[] = []
|
|
52
|
+
const lines = text.split('\n')
|
|
53
|
+
for (let i = 0; i < lines.length; i++) {
|
|
54
|
+
const line = lines[i]?.trim()
|
|
55
|
+
if (!line) continue
|
|
56
|
+
try {
|
|
57
|
+
rows.push({ line: i + 1, raw: JSON.parse(line) })
|
|
58
|
+
} catch {
|
|
59
|
+
throw new Error(`${path}: malformed JSON at line ${i + 1}`)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return rows
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function readRows<T>(path: string): Promise<T[]> {
|
|
66
|
+
return (await readRawRows(path)).map((r) => r.raw as T)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Temp-plus-rename so a crash mid-write cannot truncate the store. The temp
|
|
70
|
+
// file is a sibling, which keeps the rename on one filesystem and therefore
|
|
71
|
+
// atomic. Cleanup on failure ignores unlink errors to let the original error
|
|
72
|
+
// propagate unchanged.
|
|
73
|
+
export async function writeAtomically(
|
|
74
|
+
path: string,
|
|
75
|
+
content: string,
|
|
76
|
+
sourcePath: string,
|
|
77
|
+
): Promise<void> {
|
|
78
|
+
await assertNotUnderSource(path, sourcePath)
|
|
79
|
+
const random = randomBytes(8).toString('hex')
|
|
80
|
+
const tmp = `${path}.${random}.tmp`
|
|
81
|
+
try {
|
|
82
|
+
await writeFile(tmp, content)
|
|
83
|
+
await rename(tmp, path)
|
|
84
|
+
} finally {
|
|
85
|
+
try {
|
|
86
|
+
await unlink(tmp)
|
|
87
|
+
} catch {
|
|
88
|
+
// Ignore cleanup errors, let original error propagate
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Temp-plus-rename so a crash mid-write cannot truncate the store. The temp
|
|
94
|
+
// file is a sibling, which keeps the rename on one filesystem and therefore
|
|
95
|
+
// atomic.
|
|
96
|
+
export async function writeRows<T>(path: string, rows: T[], sourcePath: string): Promise<void> {
|
|
97
|
+
const body = rows.map((r) => JSON.stringify(r)).join('\n')
|
|
98
|
+
const text = rows.length > 0 ? `${body}\n` : ''
|
|
99
|
+
await writeAtomically(path, text, sourcePath)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function stableStringify(obj: unknown): string {
|
|
103
|
+
if (obj === null) return 'null'
|
|
104
|
+
if (typeof obj !== 'object') return JSON.stringify(obj)
|
|
105
|
+
if (Array.isArray(obj)) {
|
|
106
|
+
return `[${obj.map(stableStringify).join(',')}]`
|
|
107
|
+
}
|
|
108
|
+
const pairs: string[] = []
|
|
109
|
+
const keys = Object.keys(obj as Record<string, unknown>).sort()
|
|
110
|
+
for (const key of keys) {
|
|
111
|
+
const val = (obj as Record<string, unknown>)[key]
|
|
112
|
+
pairs.push(`${JSON.stringify(key)}:${stableStringify(val)}`)
|
|
113
|
+
}
|
|
114
|
+
return `{${pairs.join(',')}}`
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function upsertRows<T extends { id: string }>(
|
|
118
|
+
existing: T[],
|
|
119
|
+
incoming: T[],
|
|
120
|
+
): { rows: T[]; added: number; updated: number } {
|
|
121
|
+
const snapshot = new Map<string, string>()
|
|
122
|
+
for (let i = 0; i < existing.length; i++) {
|
|
123
|
+
const row = existing[i]
|
|
124
|
+
if (row) snapshot.set(row.id, stableStringify(row))
|
|
125
|
+
}
|
|
126
|
+
const rows = [...existing]
|
|
127
|
+
const index = new Map<string, number>()
|
|
128
|
+
for (let i = 0; i < rows.length; i++) {
|
|
129
|
+
const row = rows[i]
|
|
130
|
+
if (row) index.set(row.id, i)
|
|
131
|
+
}
|
|
132
|
+
for (const row of incoming) {
|
|
133
|
+
const at = index.get(row.id)
|
|
134
|
+
if (at === undefined) {
|
|
135
|
+
index.set(row.id, rows.length)
|
|
136
|
+
rows.push(row)
|
|
137
|
+
} else {
|
|
138
|
+
rows[at] = row
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
let added = 0
|
|
142
|
+
let updated = 0
|
|
143
|
+
const finalIndex = new Map<string, string>()
|
|
144
|
+
for (let i = 0; i < rows.length; i++) {
|
|
145
|
+
const row = rows[i]
|
|
146
|
+
if (row) {
|
|
147
|
+
const id = row.id
|
|
148
|
+
const newStr = stableStringify(row)
|
|
149
|
+
finalIndex.set(id, newStr)
|
|
150
|
+
const oldStr = snapshot.get(id)
|
|
151
|
+
if (oldStr === undefined) {
|
|
152
|
+
added++
|
|
153
|
+
} else if (oldStr !== newStr) {
|
|
154
|
+
updated++
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { rows, added, updated }
|
|
159
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
export type Lens = 'code' | 'nav' | 'docs' | 'runtime'
|
|
2
|
+
|
|
3
|
+
export type Ref =
|
|
4
|
+
| { kind: 'src'; path: string; lines?: [number, number] }
|
|
5
|
+
| { kind: 'ledger'; id: string }
|
|
6
|
+
| { kind: 'doc'; path: string; note?: string }
|
|
7
|
+
| { kind: 'observed'; host: string; path: string; behavior: string }
|
|
8
|
+
|
|
9
|
+
export type Disposition =
|
|
10
|
+
| { kind: 'unaccounted' }
|
|
11
|
+
| { kind: 'mapped'; fr: string }
|
|
12
|
+
| { kind: 'out-of-scope'; queue: string }
|
|
13
|
+
|
|
14
|
+
export type Element = {
|
|
15
|
+
id: string
|
|
16
|
+
surface: string
|
|
17
|
+
element: string
|
|
18
|
+
found_by: Lens[]
|
|
19
|
+
disposition: Disposition
|
|
20
|
+
refs: Ref[]
|
|
21
|
+
lens: Lens
|
|
22
|
+
batch: string
|
|
23
|
+
notes: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type Confidence =
|
|
27
|
+
| { kind: 'confirmed' }
|
|
28
|
+
| { kind: 'inferred' }
|
|
29
|
+
| { kind: 'queued'; queue: string }
|
|
30
|
+
|
|
31
|
+
export type Parity =
|
|
32
|
+
| { kind: 'golden-master'; ref: string }
|
|
33
|
+
| { kind: 'differential'; ref: string }
|
|
34
|
+
| { kind: 'rubric'; level: 'high' }
|
|
35
|
+
| { kind: 'rubric'; level: 'moderate' | 'low' | 'unknown'; queue: string }
|
|
36
|
+
|
|
37
|
+
export type Requirement = {
|
|
38
|
+
id: string
|
|
39
|
+
cap: string
|
|
40
|
+
requirement: string
|
|
41
|
+
actors: string
|
|
42
|
+
objects: string
|
|
43
|
+
rules: string
|
|
44
|
+
origin: 'intended' | 'accidental-candidate'
|
|
45
|
+
confidence: Confidence
|
|
46
|
+
citations: Ref[]
|
|
47
|
+
parity: Parity | null
|
|
48
|
+
batch: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type Capability = {
|
|
52
|
+
slug: string
|
|
53
|
+
title: string
|
|
54
|
+
ns: string
|
|
55
|
+
elements: string[]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type Delta = {
|
|
59
|
+
id: string
|
|
60
|
+
scope: string
|
|
61
|
+
rationale: string
|
|
62
|
+
parity_exclusion: string
|
|
63
|
+
validation: string
|
|
64
|
+
owner_signed: string | null
|
|
65
|
+
batch: string
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type Skipped = { element: string; reason: string }
|
|
69
|
+
|
|
70
|
+
// A direction records both how many findings it produced and how that count
|
|
71
|
+
// was produced. The evidence is free text because some directions are
|
|
72
|
+
// judgment walks rather than commands, so the gate is presence, not
|
|
73
|
+
// executability; the field exists so a reviewer can retrace a count, and so a
|
|
74
|
+
// later re-run verb has somewhere to read the command from.
|
|
75
|
+
export type Direction = { count: number; evidence: string }
|
|
76
|
+
|
|
77
|
+
export type Census =
|
|
78
|
+
| {
|
|
79
|
+
kind: 'lens'
|
|
80
|
+
surface: string
|
|
81
|
+
phase: string
|
|
82
|
+
directions: Record<string, Direction>
|
|
83
|
+
total: number
|
|
84
|
+
in_ledger: number
|
|
85
|
+
added: number
|
|
86
|
+
skipped: Skipped[]
|
|
87
|
+
queued: string[]
|
|
88
|
+
batch: string
|
|
89
|
+
}
|
|
90
|
+
| {
|
|
91
|
+
kind: 'attribute'
|
|
92
|
+
surface: string
|
|
93
|
+
subject: string
|
|
94
|
+
phase: string
|
|
95
|
+
directions: Record<string, Direction>
|
|
96
|
+
total: number
|
|
97
|
+
behavioral: number
|
|
98
|
+
explained: number
|
|
99
|
+
queued: string[]
|
|
100
|
+
batch: string
|
|
101
|
+
}
|
|
102
|
+
| {
|
|
103
|
+
kind: 'rule-sweep'
|
|
104
|
+
subject: string
|
|
105
|
+
phase: string
|
|
106
|
+
probes: number
|
|
107
|
+
found: number
|
|
108
|
+
as_requirements: number
|
|
109
|
+
queued: string[]
|
|
110
|
+
batch: string
|
|
111
|
+
}
|
|
112
|
+
| {
|
|
113
|
+
kind: 'closer'
|
|
114
|
+
closer: string
|
|
115
|
+
phase: string
|
|
116
|
+
checked: number
|
|
117
|
+
findings: number
|
|
118
|
+
fixed: number
|
|
119
|
+
queued: string[]
|
|
120
|
+
batch: string
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export type Severity = 'critical' | 'moderate' | 'minor'
|
|
124
|
+
|
|
125
|
+
export type QueueItem = {
|
|
126
|
+
id: string
|
|
127
|
+
severity: Severity
|
|
128
|
+
status: 'open' | 'adjudicated'
|
|
129
|
+
ruling?: string
|
|
130
|
+
adjudicated?: string
|
|
131
|
+
evidence: string
|
|
132
|
+
options: string
|
|
133
|
+
recommendation: string
|
|
134
|
+
path: string
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export type Violation = { gate: string; message: string }
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import type { Config } from './config.ts'
|
|
2
|
+
import { validateElementId } from './ids.ts'
|
|
3
|
+
import type {
|
|
4
|
+
Confidence,
|
|
5
|
+
Delta,
|
|
6
|
+
Disposition,
|
|
7
|
+
Element,
|
|
8
|
+
Lens,
|
|
9
|
+
Parity,
|
|
10
|
+
Ref,
|
|
11
|
+
Requirement,
|
|
12
|
+
} from './types.ts'
|
|
13
|
+
|
|
14
|
+
export type Validated<T> = { ok: true; value: T } | { ok: false; errors: string[] }
|
|
15
|
+
|
|
16
|
+
const LENSES: Lens[] = ['code', 'nav', 'docs', 'runtime']
|
|
17
|
+
|
|
18
|
+
function str(v: unknown): v is string {
|
|
19
|
+
return typeof v === 'string'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// A row must be a JSON object. null, arrays and primitives all fail this, and
|
|
23
|
+
// must be rejected here rather than left to crash on the first property read:
|
|
24
|
+
// `null.id` throws, but a normal validation error is what every validator in
|
|
25
|
+
// this file (and census.ts) expects to return instead.
|
|
26
|
+
export function isRecord(v: unknown): v is Record<string, unknown> {
|
|
27
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function checkRef(ref: unknown, where: string, errors: string[]): void {
|
|
31
|
+
const r = ref as Record<string, unknown>
|
|
32
|
+
const kind = r?.kind
|
|
33
|
+
if (kind === 'src') {
|
|
34
|
+
if (!str(r.path)) errors.push(`${where}: src ref needs a path`)
|
|
35
|
+
if (r.lines !== undefined) {
|
|
36
|
+
const lines = r.lines as unknown[]
|
|
37
|
+
const ok =
|
|
38
|
+
Array.isArray(lines) &&
|
|
39
|
+
lines.length === 2 &&
|
|
40
|
+
typeof lines[0] === 'number' &&
|
|
41
|
+
typeof lines[1] === 'number' &&
|
|
42
|
+
(lines[0] as number) <= (lines[1] as number)
|
|
43
|
+
if (!ok) errors.push(`${where}: src ref lines must be [start, end] with start <= end`)
|
|
44
|
+
}
|
|
45
|
+
} else if (kind === 'ledger') {
|
|
46
|
+
if (!str(r.id)) errors.push(`${where}: ledger ref needs an id`)
|
|
47
|
+
} else if (kind === 'doc') {
|
|
48
|
+
if (!str(r.path)) errors.push(`${where}: doc ref needs a path`)
|
|
49
|
+
} else if (kind === 'observed') {
|
|
50
|
+
if (!str(r.host) || !str(r.path) || !str(r.behavior)) {
|
|
51
|
+
errors.push(`${where}: observed ref needs host, path and behavior`)
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
errors.push(`${where}: unknown ref kind ${String(kind)}`)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function validateElement(row: unknown, cfg: Config): Validated<Element> {
|
|
59
|
+
if (!isRecord(row)) return { ok: false, errors: ['element: row must be a JSON object'] }
|
|
60
|
+
const errors: string[] = []
|
|
61
|
+
const r = row
|
|
62
|
+
const id = str(r.id) ? r.id : ''
|
|
63
|
+
const surface = str(r.surface) ? r.surface : ''
|
|
64
|
+
const where = `element ${id || '<no id>'}`
|
|
65
|
+
|
|
66
|
+
if (!id) errors.push('element: missing id')
|
|
67
|
+
if (!surface) errors.push(`${where}: missing surface`)
|
|
68
|
+
else if (!cfg.surfaces.includes(surface)) {
|
|
69
|
+
errors.push(
|
|
70
|
+
`${where}: surface ${surface} is not in the declared set [${cfg.surfaces.join(', ')}]`,
|
|
71
|
+
)
|
|
72
|
+
} else if (id) {
|
|
73
|
+
const idError = validateElementId(id, surface, cfg.surfaceSingular)
|
|
74
|
+
if (idError) errors.push(`${where}: ${idError}`)
|
|
75
|
+
}
|
|
76
|
+
if (!str(r.element) || r.element.length === 0) errors.push(`${where}: missing element text`)
|
|
77
|
+
|
|
78
|
+
const foundBy = r.found_by as unknown[]
|
|
79
|
+
if (!Array.isArray(foundBy) || foundBy.length === 0) {
|
|
80
|
+
errors.push(`${where}: found_by must list at least one lens`)
|
|
81
|
+
} else {
|
|
82
|
+
for (const l of foundBy) {
|
|
83
|
+
if (!LENSES.includes(l as Lens))
|
|
84
|
+
errors.push(`${where}: unknown lens ${String(l)} in found_by`)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (!LENSES.includes(r.lens as Lens)) errors.push(`${where}: unknown lens ${String(r.lens)}`)
|
|
88
|
+
|
|
89
|
+
const d = r.disposition as Record<string, unknown> | undefined
|
|
90
|
+
const dk = d?.kind
|
|
91
|
+
if (dk === 'unaccounted') {
|
|
92
|
+
// nothing further
|
|
93
|
+
} else if (dk === 'mapped') {
|
|
94
|
+
if (!str(d?.fr)) errors.push(`${where}: mapped disposition needs an fr id`)
|
|
95
|
+
} else if (dk === 'out-of-scope') {
|
|
96
|
+
if (!str(d?.queue)) errors.push(`${where}: out-of-scope disposition needs a queue id`)
|
|
97
|
+
} else {
|
|
98
|
+
errors.push(`${where}: unknown disposition kind ${String(dk)}`)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const refs = (r.refs ?? []) as unknown[]
|
|
102
|
+
if (!Array.isArray(refs)) errors.push(`${where}: refs must be an array`)
|
|
103
|
+
else
|
|
104
|
+
refs.forEach((ref, i) => {
|
|
105
|
+
checkRef(ref, `${where} refs[${i}]`, errors)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
if (errors.length > 0) return { ok: false, errors }
|
|
109
|
+
return {
|
|
110
|
+
ok: true,
|
|
111
|
+
value: {
|
|
112
|
+
id,
|
|
113
|
+
surface,
|
|
114
|
+
element: r.element as string,
|
|
115
|
+
found_by: foundBy as Lens[],
|
|
116
|
+
disposition: r.disposition as Disposition,
|
|
117
|
+
refs: refs as Ref[],
|
|
118
|
+
lens: r.lens as Lens,
|
|
119
|
+
batch: '',
|
|
120
|
+
notes: str(r.notes) ? r.notes : '',
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function validateRequirement(row: unknown, _cfg: Config): Validated<Requirement> {
|
|
126
|
+
if (!isRecord(row)) return { ok: false, errors: ['requirement: row must be a JSON object'] }
|
|
127
|
+
const errors: string[] = []
|
|
128
|
+
const r = row
|
|
129
|
+
const id = str(r.id) ? r.id : ''
|
|
130
|
+
const where = `requirement ${id || '<no id>'}`
|
|
131
|
+
|
|
132
|
+
if (!id) errors.push('requirement: missing id')
|
|
133
|
+
if (!str(r.cap) || r.cap.length === 0) errors.push(`${where}: missing cap`)
|
|
134
|
+
if (!str(r.requirement) || r.requirement.length === 0)
|
|
135
|
+
errors.push(`${where}: missing requirement text`)
|
|
136
|
+
if (r.origin !== 'intended' && r.origin !== 'accidental-candidate') {
|
|
137
|
+
errors.push(`${where}: origin must be intended or accidental-candidate`)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const c = r.confidence as Record<string, unknown> | undefined
|
|
141
|
+
if (c?.kind === 'queued') {
|
|
142
|
+
if (!str(c.queue)) errors.push(`${where}: queued confidence needs a queue id`)
|
|
143
|
+
} else if (c?.kind !== 'confirmed' && c?.kind !== 'inferred') {
|
|
144
|
+
errors.push(`${where}: unknown confidence kind ${String(c?.kind)}`)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const citations = (r.citations ?? []) as unknown[]
|
|
148
|
+
if (!Array.isArray(citations) || citations.length === 0) {
|
|
149
|
+
errors.push(`${where}: at least one citation is required`)
|
|
150
|
+
} else {
|
|
151
|
+
citations.forEach((ref, i) => {
|
|
152
|
+
checkRef(ref, `${where} citations[${i}]`, errors)
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const p = r.parity as Record<string, unknown> | null | undefined
|
|
157
|
+
if (p !== null && p !== undefined) {
|
|
158
|
+
if (p.kind === 'golden-master' || p.kind === 'differential') {
|
|
159
|
+
if (!str(p.ref)) errors.push(`${where}: ${String(p.kind)} parity needs a ref`)
|
|
160
|
+
} else if (p.kind === 'rubric') {
|
|
161
|
+
const level = p.level
|
|
162
|
+
if (level === 'high') {
|
|
163
|
+
// no queue id needed
|
|
164
|
+
} else if (level === 'moderate' || level === 'low' || level === 'unknown') {
|
|
165
|
+
if (!str(p.queue)) errors.push(`${where}: rubric:${level} parity needs a queue id`)
|
|
166
|
+
} else {
|
|
167
|
+
errors.push(`${where}: unknown rubric level ${String(level)}`)
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
errors.push(`${where}: unknown parity kind ${String(p.kind)}`)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (errors.length > 0) return { ok: false, errors }
|
|
175
|
+
return {
|
|
176
|
+
ok: true,
|
|
177
|
+
value: {
|
|
178
|
+
id,
|
|
179
|
+
cap: r.cap as string,
|
|
180
|
+
requirement: r.requirement as string,
|
|
181
|
+
actors: str(r.actors) ? r.actors : '-',
|
|
182
|
+
objects: str(r.objects) ? r.objects : '-',
|
|
183
|
+
rules: str(r.rules) ? r.rules : '-',
|
|
184
|
+
origin: r.origin as Requirement['origin'],
|
|
185
|
+
confidence: r.confidence as Confidence,
|
|
186
|
+
citations: citations as Ref[],
|
|
187
|
+
parity: (r.parity ?? null) as Parity | null,
|
|
188
|
+
batch: '',
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function validateDelta(row: unknown, _cfg: Config): Validated<Delta> {
|
|
194
|
+
if (!isRecord(row)) return { ok: false, errors: ['delta: row must be a JSON object'] }
|
|
195
|
+
const errors: string[] = []
|
|
196
|
+
const r = row
|
|
197
|
+
const id = str(r.id) ? r.id : ''
|
|
198
|
+
const where = `delta ${id || '<no id>'}`
|
|
199
|
+
if (!id.startsWith('delta-')) errors.push(`${where}: id must start with delta-`)
|
|
200
|
+
for (const key of ['scope', 'rationale', 'parity_exclusion', 'validation']) {
|
|
201
|
+
const v = r[key]
|
|
202
|
+
if (!str(v) || v.length === 0) errors.push(`${where}: missing ${key}`)
|
|
203
|
+
}
|
|
204
|
+
const signed = r.owner_signed
|
|
205
|
+
if (signed !== null && signed !== undefined && !str(signed)) {
|
|
206
|
+
errors.push(`${where}: owner_signed must be a date string or null`)
|
|
207
|
+
}
|
|
208
|
+
if (errors.length > 0) return { ok: false, errors }
|
|
209
|
+
return {
|
|
210
|
+
ok: true,
|
|
211
|
+
value: {
|
|
212
|
+
id,
|
|
213
|
+
scope: r.scope as string,
|
|
214
|
+
rationale: r.rationale as string,
|
|
215
|
+
parity_exclusion: r.parity_exclusion as string,
|
|
216
|
+
validation: r.validation as string,
|
|
217
|
+
owner_signed: (signed ?? null) as string | null,
|
|
218
|
+
batch: '',
|
|
219
|
+
},
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "migrate",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Source-agnostic legacy migration mapping. Walks a legacy codebase through probe, enumerate, seam, extract, parity, and queue, building an auditable requirements ledger with mandatory citations and a `migrate check` gate in place of self-reported completeness. Use when the user asks to migrate, re-specify, replatform, or map a legacy system onto a new stack, or to resume, check, or report on a mapping run already under way.",
|
|
5
|
+
"author": "iceinvein",
|
|
6
|
+
"type": "prompt",
|
|
7
|
+
"tools": ["claude"],
|
|
8
|
+
"files": { "prompt": "SKILL.md" },
|
|
9
|
+
"bundle": {
|
|
10
|
+
"include": [
|
|
11
|
+
"bin",
|
|
12
|
+
"references",
|
|
13
|
+
"scripts",
|
|
14
|
+
"templates",
|
|
15
|
+
"fixtures/tiny-express",
|
|
16
|
+
"fixtures/tiny-webforms",
|
|
17
|
+
"install.sh",
|
|
18
|
+
"uninstall.sh",
|
|
19
|
+
"package.json",
|
|
20
|
+
"README.md",
|
|
21
|
+
"docs/reference.md"
|
|
22
|
+
],
|
|
23
|
+
"exclude": ["scripts/__tests__/"]
|
|
24
|
+
},
|
|
25
|
+
"install": {
|
|
26
|
+
"claude": {
|
|
27
|
+
"prompt": ".claude/skills/migrate/SKILL.md",
|
|
28
|
+
"bundleRoot": ".claude/skills/migrate",
|
|
29
|
+
"postinstall": "install.sh",
|
|
30
|
+
"postremove": "uninstall.sh"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[source]
|
|
2
|
+
path = "{{SOURCE_PATH}}"
|
|
3
|
+
scope = "{{SCOPE}}"
|
|
4
|
+
stack = "{{SOURCE_STACK}}"
|
|
5
|
+
vcs = "{{VCS}}"
|
|
6
|
+
basis = "{{BASIS}}"
|
|
7
|
+
|
|
8
|
+
[target]
|
|
9
|
+
name = "{{TARGET_NAME}}"
|
|
10
|
+
stack = "{{TARGET_STACK}}"
|
|
11
|
+
parity_test_path = "tests/parity/{capability}/{fr_slug}.test.ts"
|
|
12
|
+
|
|
13
|
+
[target.layout]
|
|
14
|
+
|
|
15
|
+
[target.commands]
|
|
16
|
+
test = "echo 'wire the target test command'"
|
|
17
|
+
lint = "echo 'wire the target lint command'"
|
|
18
|
+
build = "echo 'wire the target build command'"
|
|
19
|
+
|
|
20
|
+
[surfaces]
|
|
21
|
+
types = ["routes", "tables", "jobs", "reports", "screens", "integrations", "workflows", "settings"]
|
|
22
|
+
|
|
23
|
+
[closers]
|
|
24
|
+
set = ["cross-capability-workflow", "scope-injection", "read-write-symmetry"]
|
|
25
|
+
|
|
26
|
+
[handoff]
|
|
27
|
+
adapter = "markdown"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noUncheckedIndexedAccess": true,
|
|
8
|
+
"noImplicitOverride": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"types": ["bun"],
|
|
14
|
+
"lib": ["ESNext"],
|
|
15
|
+
"jsx": "preserve"
|
|
16
|
+
},
|
|
17
|
+
"include": ["bin/**/*.ts", "scripts/**/*.ts"]
|
|
18
|
+
}
|