@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.
Files changed (148) hide show
  1. package/README.md +18 -2
  2. package/dist/cli/index.js +105 -28
  3. package/package.json +1 -1
  4. package/skills/index.json +14 -2
  5. package/skills/magpie/README.md +2 -1
  6. package/skills/magpie/SKILL.md +138 -45
  7. package/skills/magpie/bin/magpie.ts +43 -0
  8. package/skills/magpie/fixtures/example-pr/brief.json +18 -0
  9. package/skills/magpie/fixtures/fake-gh-nodiff.sh +38 -0
  10. package/skills/magpie/fixtures/fake-gh.sh +14 -0
  11. package/skills/magpie/package.json +1 -1
  12. package/skills/magpie/references/peer-review.md +7 -2
  13. package/skills/magpie/references/scout.md +90 -0
  14. package/skills/magpie/references/specialists.md +117 -6
  15. package/skills/magpie/scripts/__tests__/cli.test.ts +101 -1
  16. package/skills/magpie/scripts/__tests__/dedupe-cmd.test.ts +187 -0
  17. package/skills/magpie/scripts/__tests__/diff-chunks.test.ts +51 -0
  18. package/skills/magpie/scripts/__tests__/filter-diff-preservation.test.ts +54 -0
  19. package/skills/magpie/scripts/__tests__/findings-files.test.ts +35 -0
  20. package/skills/magpie/scripts/__tests__/gh.test.ts +90 -0
  21. package/skills/magpie/scripts/__tests__/git-diff.test.ts +83 -0
  22. package/skills/magpie/scripts/__tests__/helpers/git-fixture.ts +47 -0
  23. package/skills/magpie/scripts/__tests__/path-filter.test.ts +27 -0
  24. package/skills/magpie/scripts/__tests__/preview-cmd.test.ts +16 -0
  25. package/skills/magpie/scripts/__tests__/refresh.test.ts +45 -0
  26. package/skills/magpie/scripts/__tests__/render-cmd.test.ts +173 -1
  27. package/skills/magpie/scripts/__tests__/render-findings.test.ts +151 -1
  28. package/skills/magpie/scripts/__tests__/render-progress.test.ts +42 -0
  29. package/skills/magpie/scripts/__tests__/setup-cmd.test.ts +83 -1
  30. package/skills/magpie/scripts/__tests__/shard.test.ts +165 -0
  31. package/skills/magpie/scripts/__tests__/skill-lint.test.ts +207 -7
  32. package/skills/magpie/scripts/__tests__/types.test.ts +47 -0
  33. package/skills/magpie/scripts/dedupe-cmd.ts +58 -3
  34. package/skills/magpie/scripts/diff-chunks.ts +28 -0
  35. package/skills/magpie/scripts/findings-files.ts +32 -0
  36. package/skills/magpie/scripts/gh.ts +68 -14
  37. package/skills/magpie/scripts/git-diff.ts +111 -0
  38. package/skills/magpie/scripts/path-filter.ts +9 -5
  39. package/skills/magpie/scripts/preview-cmd.ts +11 -1
  40. package/skills/magpie/scripts/refresh.ts +32 -3
  41. package/skills/magpie/scripts/render-cmd.ts +35 -11
  42. package/skills/magpie/scripts/render-findings.ts +78 -2
  43. package/skills/magpie/scripts/render-progress.ts +6 -1
  44. package/skills/magpie/scripts/setup-cmd.ts +38 -1
  45. package/skills/magpie/scripts/shard.ts +171 -0
  46. package/skills/magpie/scripts/status-cmd.ts +7 -4
  47. package/skills/magpie/scripts/types.ts +50 -0
  48. package/skills/magpie/skill.json +2 -2
  49. package/skills/magpie/templates/styles.css +66 -0
  50. package/skills/migrate/README.md +194 -0
  51. package/skills/migrate/SKILL.md +197 -0
  52. package/skills/migrate/bin/migrate +15 -0
  53. package/skills/migrate/bin/migrate.ts +309 -0
  54. package/skills/migrate/biome.json +35 -0
  55. package/skills/migrate/bun.lock +24 -0
  56. package/skills/migrate/docs/architecture.md +294 -0
  57. package/skills/migrate/docs/reference.md +590 -0
  58. package/skills/migrate/fixtures/tiny-express/GROUND-TRUTH.md +39 -0
  59. package/skills/migrate/fixtures/tiny-express/app.js +29 -0
  60. package/skills/migrate/fixtures/tiny-express/cron.js +6 -0
  61. package/skills/migrate/fixtures/tiny-express/reports/daily-users.json +6 -0
  62. package/skills/migrate/fixtures/tiny-express/schema.sql +12 -0
  63. package/skills/migrate/fixtures/tiny-express/settings.json +4 -0
  64. package/skills/migrate/fixtures/tiny-express/views/users.html +9 -0
  65. package/skills/migrate/fixtures/tiny-webforms/Controllers/UsersController.cs +68 -0
  66. package/skills/migrate/fixtures/tiny-webforms/Default.aspx +7 -0
  67. package/skills/migrate/fixtures/tiny-webforms/Default.aspx.cs +14 -0
  68. package/skills/migrate/fixtures/tiny-webforms/GROUND-TRUTH.md +50 -0
  69. package/skills/migrate/fixtures/tiny-webforms/Integrations/BillingClient.cs +16 -0
  70. package/skills/migrate/fixtures/tiny-webforms/Jobs/NightlyDigestJob.cs +33 -0
  71. package/skills/migrate/fixtures/tiny-webforms/Reports/DailyUsers.rdl +11 -0
  72. package/skills/migrate/fixtures/tiny-webforms/Schema.sql +12 -0
  73. package/skills/migrate/fixtures/tiny-webforms/Site.master +16 -0
  74. package/skills/migrate/fixtures/tiny-webforms/Users.aspx +8 -0
  75. package/skills/migrate/fixtures/tiny-webforms/Users.aspx.cs +14 -0
  76. package/skills/migrate/fixtures/tiny-webforms/web.config +10 -0
  77. package/skills/migrate/install.sh +68 -0
  78. package/skills/migrate/package.json +17 -0
  79. package/skills/migrate/references/phases/enumerate.md +291 -0
  80. package/skills/migrate/references/phases/extract.md +652 -0
  81. package/skills/migrate/references/phases/parity.md +275 -0
  82. package/skills/migrate/references/phases/probe.md +135 -0
  83. package/skills/migrate/references/phases/queue.md +242 -0
  84. package/skills/migrate/references/phases/seam.md +416 -0
  85. package/skills/migrate/references/recipes/README.md +116 -0
  86. package/skills/migrate/references/recipes/aspnet.md +287 -0
  87. package/skills/migrate/references/run-ops.md +280 -0
  88. package/skills/migrate/scripts/__tests__/census.test.ts +775 -0
  89. package/skills/migrate/scripts/__tests__/check.test.ts +458 -0
  90. package/skills/migrate/scripts/__tests__/citations.test.ts +156 -0
  91. package/skills/migrate/scripts/__tests__/cli.test.ts +183 -0
  92. package/skills/migrate/scripts/__tests__/concurrency.test.ts +164 -0
  93. package/skills/migrate/scripts/__tests__/config.test.ts +112 -0
  94. package/skills/migrate/scripts/__tests__/e2e-express.test.ts +1093 -0
  95. package/skills/migrate/scripts/__tests__/e2e-webforms.test.ts +1276 -0
  96. package/skills/migrate/scripts/__tests__/e2e.test.ts +320 -0
  97. package/skills/migrate/scripts/__tests__/ids.test.ts +38 -0
  98. package/skills/migrate/scripts/__tests__/import.test.ts +155 -0
  99. package/skills/migrate/scripts/__tests__/init.test.ts +192 -0
  100. package/skills/migrate/scripts/__tests__/leaks.test.ts +176 -0
  101. package/skills/migrate/scripts/__tests__/lock.test.ts +183 -0
  102. package/skills/migrate/scripts/__tests__/paths.test.ts +129 -0
  103. package/skills/migrate/scripts/__tests__/phase-cmd.test.ts +151 -0
  104. package/skills/migrate/scripts/__tests__/phases.test.ts +70 -0
  105. package/skills/migrate/scripts/__tests__/queue.test.ts +475 -0
  106. package/skills/migrate/scripts/__tests__/report.test.ts +150 -0
  107. package/skills/migrate/scripts/__tests__/run-state.test.ts +136 -0
  108. package/skills/migrate/scripts/__tests__/status-reset.test.ts +318 -0
  109. package/skills/migrate/scripts/__tests__/store.test.ts +132 -0
  110. package/skills/migrate/scripts/__tests__/validate.test.ts +54 -0
  111. package/skills/migrate/scripts/census-cmd.ts +109 -0
  112. package/skills/migrate/scripts/census.ts +342 -0
  113. package/skills/migrate/scripts/check-cmd.ts +24 -0
  114. package/skills/migrate/scripts/check.ts +376 -0
  115. package/skills/migrate/scripts/citations.ts +92 -0
  116. package/skills/migrate/scripts/config.ts +237 -0
  117. package/skills/migrate/scripts/ids.ts +31 -0
  118. package/skills/migrate/scripts/import-cmd.ts +141 -0
  119. package/skills/migrate/scripts/init-cmd.ts +118 -0
  120. package/skills/migrate/scripts/leaks.ts +184 -0
  121. package/skills/migrate/scripts/lock.ts +188 -0
  122. package/skills/migrate/scripts/paths.ts +103 -0
  123. package/skills/migrate/scripts/phase-cmd.ts +63 -0
  124. package/skills/migrate/scripts/phases.ts +113 -0
  125. package/skills/migrate/scripts/queue-cmd.ts +98 -0
  126. package/skills/migrate/scripts/queue.ts +258 -0
  127. package/skills/migrate/scripts/report-cmd.ts +47 -0
  128. package/skills/migrate/scripts/report.ts +131 -0
  129. package/skills/migrate/scripts/reset-cmd.ts +120 -0
  130. package/skills/migrate/scripts/status-cmd.ts +52 -0
  131. package/skills/migrate/scripts/store.ts +159 -0
  132. package/skills/migrate/scripts/types.ts +137 -0
  133. package/skills/migrate/scripts/validate.ts +221 -0
  134. package/skills/migrate/skill.json +33 -0
  135. package/skills/migrate/templates/config.toml +27 -0
  136. package/skills/migrate/templates/queue-item.md +17 -0
  137. package/skills/migrate/tsconfig.json +18 -0
  138. package/skills/migrate/uninstall.sh +31 -0
  139. package/skills/sluice/SKILL.md +82 -0
  140. package/skills/sluice/references/deep-channel.md +94 -0
  141. package/skills/sluice/references/finish.md +35 -0
  142. package/skills/sluice/references/intent.md +29 -0
  143. package/skills/sluice/references/review.md +42 -0
  144. package/skills/sluice/references/root-cause.md +38 -0
  145. package/skills/sluice/references/show-or-say.md +36 -0
  146. package/skills/sluice/references/test-first.md +35 -0
  147. package/skills/sluice/references/verify.md +26 -0
  148. 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,17 @@
1
+ ---
2
+ id: q-{{SLUG}}
3
+ severity: {{SEVERITY}}
4
+ status: open
5
+ ---
6
+
7
+ ## Evidence
8
+
9
+ {{EVIDENCE}}
10
+
11
+ ## Options
12
+
13
+ {{OPTIONS}}
14
+
15
+ ## Recommendation
16
+
17
+ {{RECOMMENDATION}}
@@ -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
+ }