@t275005746/gse 0.1.1 → 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 (86) hide show
  1. package/.gse/releases/public-github-release-v0.1.2.md +49 -0
  2. package/.gse/releases/public-registry-publication-npm.md +49 -49
  3. package/.gse/state.json +148 -26
  4. package/CHANGELOG.md +52 -8
  5. package/README.md +13 -6
  6. package/README.zh-CN.md +13 -6
  7. package/SKILL.md +32 -12
  8. package/assets/templates/dispatch-packet.md +22 -14
  9. package/assets/templates/evidence.md +11 -5
  10. package/assets/templates/role-fallback-packet.md +49 -0
  11. package/package.json +18 -10
  12. package/references/agent-roles.md +41 -20
  13. package/references/commands.md +75 -45
  14. package/references/drift-audit.md +2 -1
  15. package/references/evidence-taxonomy.md +46 -9
  16. package/references/file-ownership.md +17 -13
  17. package/references/final-form-roadmap.md +203 -0
  18. package/references/final-readiness.md +3 -3
  19. package/references/host-adapters.md +38 -10
  20. package/references/learning-system.md +24 -8
  21. package/references/maintenance-cadence.md +51 -0
  22. package/references/project-guards.md +52 -0
  23. package/references/quality-gates.md +18 -11
  24. package/references/role-dispatch-fallback.md +54 -0
  25. package/references/tool-adapters.md +13 -3
  26. package/scripts/audit-close-gate-hardening.mjs +188 -0
  27. package/scripts/audit-close-gate.mjs +219 -33
  28. package/scripts/audit-command-execution.mjs +30 -17
  29. package/scripts/audit-commands.mjs +7 -5
  30. package/scripts/audit-completion-plan-drill.mjs +271 -0
  31. package/scripts/audit-completion-readiness.mjs +9 -6
  32. package/scripts/audit-continue-preflight.mjs +318 -0
  33. package/scripts/audit-evidence-levels.mjs +217 -0
  34. package/scripts/audit-evidence-review-queue.mjs +206 -0
  35. package/scripts/audit-final-acceptance-packet.mjs +9 -9
  36. package/scripts/audit-final-form-progress-report.mjs +19 -18
  37. package/scripts/audit-final-form-roadmap.mjs +157 -0
  38. package/scripts/audit-final-form-stale-copy.mjs +2 -2
  39. package/scripts/audit-final-readiness.mjs +3 -3
  40. package/scripts/audit-fresh-session-readiness.mjs +1 -1
  41. package/scripts/audit-host-capabilities.mjs +237 -0
  42. package/scripts/audit-installed-sync.mjs +203 -0
  43. package/scripts/audit-learning-drift.mjs +292 -0
  44. package/scripts/audit-learning-promotion.mjs +351 -0
  45. package/scripts/audit-learning-system.mjs +24 -14
  46. package/scripts/audit-local-final-form-completion.mjs +11 -10
  47. package/scripts/audit-maintenance-cadence.mjs +139 -0
  48. package/scripts/audit-maintenance-snapshot.mjs +181 -0
  49. package/scripts/audit-npm-package-metadata.mjs +8 -2
  50. package/scripts/audit-npm-publish-dry-run.mjs +10 -4
  51. package/scripts/audit-npm-tarball-install.mjs +9 -3
  52. package/scripts/audit-owner-external-gate-kit.mjs +12 -11
  53. package/scripts/audit-project-guards.mjs +189 -0
  54. package/scripts/audit-project.mjs +14 -11
  55. package/scripts/audit-public-acceptance-handoff.mjs +10 -10
  56. package/scripts/audit-public-acceptance-readiness.mjs +6 -6
  57. package/scripts/audit-public-release-checklist.mjs +17 -15
  58. package/scripts/audit-release-bundle.mjs +13 -11
  59. package/scripts/audit-release-owner-action-plan-drill.mjs +5 -4
  60. package/scripts/audit-release-owner-action-plan.mjs +10 -10
  61. package/scripts/audit-release-status-manifest.mjs +4 -4
  62. package/scripts/audit-roadmap-consistency.mjs +11 -7
  63. package/scripts/audit-role-dispatch-fallback.mjs +212 -0
  64. package/scripts/audit-session-sync.mjs +127 -0
  65. package/scripts/audit-state-freshness.mjs +6 -5
  66. package/scripts/audit-state-repair.mjs +360 -0
  67. package/scripts/audit-target-hardening-drills.mjs +518 -0
  68. package/scripts/audit-tool-fallback-policy.mjs +180 -0
  69. package/scripts/audit-ui-browser-evidence-policy.mjs +191 -0
  70. package/scripts/audit-validation-profiles.mjs +6 -4
  71. package/scripts/backfill-evidence-levels.mjs +98 -0
  72. package/scripts/check-encoding.mjs +72 -0
  73. package/scripts/generate-continue-packet.mjs +1487 -0
  74. package/scripts/generate-final-acceptance-packet.mjs +23 -8
  75. package/scripts/generate-final-form-progress-report.mjs +25 -23
  76. package/scripts/generate-host-runtime-evidence-handoff.mjs +23 -16
  77. package/scripts/generate-maintenance-snapshot.mjs +216 -0
  78. package/scripts/generate-public-acceptance-handoff.mjs +26 -14
  79. package/scripts/generate-release-owner-action-plan.mjs +4 -3
  80. package/scripts/generate-release-status-manifest.mjs +4 -3
  81. package/scripts/init-project.mjs +144 -63
  82. package/scripts/record-learning.mjs +37 -8
  83. package/scripts/record-session-sync.mjs +87 -0
  84. package/scripts/run-gse-command.mjs +79 -47
  85. package/scripts/run-validation-profile.mjs +24 -5
  86. package/scripts/validate-gse.mjs +262 -9
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+
5
+ const args = process.argv.slice(2)
6
+
7
+ function readArg(name, fallback = null) {
8
+ const index = args.indexOf(name)
9
+ if (index === -1) return fallback
10
+ return args[index + 1] ?? fallback
11
+ }
12
+
13
+ function readArgs(name) {
14
+ const values = []
15
+ for (let index = 0; index < args.length; index += 1) {
16
+ if (args[index] === name && args[index + 1]) values.push(args[index + 1])
17
+ }
18
+ return values
19
+ }
20
+
21
+ const root = path.resolve(readArg('--root', path.join(import.meta.dirname, '..')))
22
+ const jsonOnly = args.includes('--json')
23
+ const requireInstalled = args.includes('--require-installed')
24
+ const requiredThreads = readArgs('--require-thread')
25
+ const recordPath = path.join(root, '.gse', 'session-sync.jsonl')
26
+
27
+ function check(id, label, ok, evidence, risk = '') {
28
+ return { id, label, status: ok ? 'passed' : 'failed', evidence, risk }
29
+ }
30
+
31
+ function parseJsonl(filePath) {
32
+ if (!fs.existsSync(filePath)) return { records: [], errors: [] }
33
+ const lines = fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '').split(/\r?\n/)
34
+ const records = []
35
+ const errors = []
36
+ lines.forEach((line, index) => {
37
+ if (!line.trim()) return
38
+ try {
39
+ records.push(JSON.parse(line))
40
+ } catch (error) {
41
+ errors.push({ line: index + 1, error: error.message })
42
+ }
43
+ })
44
+ return { records, errors }
45
+ }
46
+
47
+ function validRecord(record) {
48
+ const validStatuses = new Set(['sent', 'installed-sync', 'skipped', 'failed', 'archived', 'unavailable'])
49
+ return record?.schemaVersion === 1 &&
50
+ typeof record.recordedAt === 'string' &&
51
+ validStatuses.has(record.status) &&
52
+ typeof record.method === 'string' &&
53
+ record.method.length > 0 &&
54
+ typeof record.evidence === 'string' &&
55
+ record.evidence.length > 0
56
+ }
57
+
58
+ const { records, errors } = parseJsonl(recordPath)
59
+ const invalidRecords = records.filter((record) => !validRecord(record))
60
+ const installedRecords = records.filter((record) => record.status === 'installed-sync')
61
+ const threadResults = requiredThreads.map((threadId) => {
62
+ const matches = records.filter((record) => record.threadId === threadId)
63
+ const latest = matches.at(-1) ?? null
64
+ const accepted = latest && ['sent', 'archived', 'unavailable', 'failed', 'skipped'].includes(latest.status)
65
+ return { threadId, latest, accepted }
66
+ })
67
+
68
+ const checks = [
69
+ check(
70
+ 'SS01',
71
+ 'session sync record file is optional but parseable when present',
72
+ errors.length === 0,
73
+ fs.existsSync(recordPath) ? recordPath : 'no .gse/session-sync.jsonl yet',
74
+ errors.map((item) => `line ${item.line}: ${item.error}`).join('; '),
75
+ ),
76
+ check(
77
+ 'SS02',
78
+ 'all session sync records follow schema',
79
+ invalidRecords.length === 0,
80
+ `records=${records.length}, invalid=${invalidRecords.length}`,
81
+ ),
82
+ requireInstalled
83
+ ? check(
84
+ 'SS03',
85
+ 'installed-copy sync/parity evidence is recorded',
86
+ installedRecords.length > 0,
87
+ installedRecords.at(-1)?.evidence ?? 'missing installed-sync record',
88
+ )
89
+ : check('SS03', 'installed-copy sync record is optional unless required', true, 'no --require-installed supplied'),
90
+ check(
91
+ 'SS04',
92
+ 'required thread sync outcomes are recorded honestly',
93
+ threadResults.every((item) => item.accepted),
94
+ threadResults.map((item) => `${item.threadId}:${item.latest?.status ?? 'missing'}`).join(', ') || 'no --require-thread supplied',
95
+ 'Allowed outcomes are sent, archived, unavailable, failed, or skipped; adoption must not be inferred from the record alone.',
96
+ ),
97
+ ]
98
+
99
+ const passed = checks.filter((item) => item.status === 'passed').length
100
+ const failed = checks.length - passed
101
+ const report = {
102
+ root,
103
+ recordPath,
104
+ generatedAt: new Date().toISOString(),
105
+ summary: { status: failed === 0 ? 'passed' : 'failed', passed, failed, total: checks.length },
106
+ workflows: {
107
+ sessionSyncRecords: failed === 0 ? 'verified' : 'failed',
108
+ installedSyncRecorded: installedRecords.length > 0 ? 'recorded' : 'not-recorded',
109
+ requiredThreadCount: requiredThreads.length,
110
+ },
111
+ records: {
112
+ total: records.length,
113
+ installedSync: installedRecords.length,
114
+ requiredThreads: threadResults,
115
+ },
116
+ limits: [
117
+ 'This audit verifies sync records and honest outcomes; it does not prove a target session adopted the new capability.',
118
+ 'Use --require-thread for owner-requested active sessions and --require-installed for installed-copy capability sync closure.',
119
+ 'Archived or unavailable sessions should be recorded as such instead of treated as successful syncs.',
120
+ ],
121
+ checks,
122
+ }
123
+
124
+ if (jsonOnly) console.log(JSON.stringify(report, null, 2))
125
+ else console.log(JSON.stringify(report, null, 2))
126
+
127
+ if (failed > 0) process.exit(1)
@@ -48,11 +48,12 @@ const doctor = parseJson(doctorRun.stdout)
48
48
  const pendingAreas = (doctor?.pendingGates ?? []).map((gate) => gate.area).sort()
49
49
  const stateSearch = [
50
50
  state?.currentSlice?.id,
51
- state?.currentSlice?.outcome,
52
- state?.currentSlice?.status,
53
- state?.currentSlice?.nextAction,
54
- ...(state?.residualRisks ?? []),
55
- ].join('\n')
51
+ state?.currentSlice?.outcome,
52
+ state?.currentSlice?.status,
53
+ state?.currentSlice?.nextAction,
54
+ ...(state?.nextChecks ?? []),
55
+ ...(state?.residualRisks ?? []),
56
+ ].join('\n')
56
57
 
57
58
  const staleResolvedGatePhrases = [
58
59
  'authorize GitHub workflow scope',
@@ -0,0 +1,360 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ const args = process.argv.slice(2)
8
+
9
+ function readArg(name, fallback = null) {
10
+ const index = args.indexOf(name)
11
+ if (index === -1) return fallback
12
+ return args[index + 1] ?? fallback
13
+ }
14
+
15
+ const root = path.resolve(readArg('--root', path.join(import.meta.dirname, '..')))
16
+ const targetArg = readArg('--target')
17
+ const jsonOnly = args.includes('--json')
18
+ const write = args.includes('--write') || args.includes('--execute')
19
+ const selfTest = args.includes('--self-test') || !targetArg
20
+ const maxActiveRisks = Number(readArg('--max-active-risks', '6'))
21
+ const maxRiskLength = Number(readArg('--max-risk-length', '260'))
22
+
23
+ function readText(filePath) {
24
+ return fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '') : ''
25
+ }
26
+
27
+ function readJson(filePath) {
28
+ if (!fs.existsSync(filePath)) return { exists: false, ok: false, data: null, error: 'missing' }
29
+ try {
30
+ return { exists: true, ok: true, data: JSON.parse(readText(filePath)), error: '' }
31
+ } catch (error) {
32
+ return { exists: true, ok: false, data: null, error: error.message }
33
+ }
34
+ }
35
+
36
+ function readJsonl(filePath) {
37
+ if (!fs.existsSync(filePath)) return { exists: false, ok: false, records: [], error: 'missing', lines: [] }
38
+ const lines = readText(filePath)
39
+ .split(/\r?\n/)
40
+ .map((line) => line.trim())
41
+ .filter(Boolean)
42
+ const records = []
43
+ for (const [index, line] of lines.entries()) {
44
+ try {
45
+ records.push(JSON.parse(line))
46
+ } catch (error) {
47
+ return { exists: true, ok: false, records, error: `line ${index + 1}: ${error.message}`, lines }
48
+ }
49
+ }
50
+ return { exists: true, ok: true, records, error: '', lines }
51
+ }
52
+
53
+ function backupFile(filePath) {
54
+ if (!fs.existsSync(filePath)) return null
55
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-')
56
+ const backupDir = path.join(path.dirname(filePath), 'backups')
57
+ fs.mkdirSync(backupDir, { recursive: true })
58
+ const backupPath = path.join(backupDir, `${stamp}-${path.basename(filePath)}.bak`)
59
+ fs.copyFileSync(filePath, backupPath)
60
+ return backupPath
61
+ }
62
+
63
+ function relative(target, fullPath) {
64
+ return path.relative(target, fullPath).replace(/\\/g, '/')
65
+ }
66
+
67
+ function repairAction(id, severity, targetPath, problem, command, options = {}) {
68
+ return {
69
+ id,
70
+ severity,
71
+ targetPath,
72
+ problem,
73
+ command,
74
+ writeSupported: Boolean(options.writeSupported),
75
+ safeToAutoRepair: Boolean(options.safeToAutoRepair),
76
+ }
77
+ }
78
+
79
+ function compactRisks(state, limit) {
80
+ const residualRisks = Array.isArray(state.residualRisks) ? state.residualRisks : []
81
+ const keep = residualRisks.slice(0, limit)
82
+ const overflow = residualRisks.slice(limit)
83
+ const archive = Array.isArray(state.riskArchive) ? state.riskArchive : []
84
+ return {
85
+ ...state,
86
+ residualRisks: keep,
87
+ riskArchive: [
88
+ ...archive,
89
+ ...overflow.map((risk) => ({
90
+ archivedAt: new Date().toISOString().slice(0, 10),
91
+ risk,
92
+ resolution: 'Archived by GSE state repair to keep active residual risks compact.',
93
+ })),
94
+ ],
95
+ }
96
+ }
97
+
98
+ function latestEvidenceFileExists(target, latest) {
99
+ return latest?.evidenceFile ? fs.existsSync(path.join(target, latest.evidenceFile)) : false
100
+ }
101
+
102
+ export function auditStateRepair(target, options = {}) {
103
+ const resolvedTarget = path.resolve(target)
104
+ const writeChanges = Boolean(options.write)
105
+ const riskLimit = Number(options.maxActiveRisks ?? maxActiveRisks)
106
+ const riskLengthLimit = Number(options.maxRiskLength ?? maxRiskLength)
107
+ const gseDir = path.join(resolvedTarget, '.gse')
108
+ const statePath = path.join(gseDir, 'state.json')
109
+ const evidenceIndexPath = path.join(gseDir, 'evidence', 'index.jsonl')
110
+ const stateResult = readJson(statePath)
111
+ const evidenceIndex = readJsonl(evidenceIndexPath)
112
+ const state = stateResult.ok ? stateResult.data : null
113
+ const latestEvidence = evidenceIndex.records.at(-1) ?? null
114
+ const actions = []
115
+ const writes = []
116
+
117
+ if (!fs.existsSync(gseDir)) {
118
+ actions.push(repairAction(
119
+ 'SR01',
120
+ 'hard',
121
+ '.gse',
122
+ 'Missing .gse directory.',
123
+ 'node <gse-skill>/scripts/init-project.mjs --target <project-root> --mode auto --json',
124
+ ))
125
+ }
126
+
127
+ if (!stateResult.ok) {
128
+ actions.push(repairAction(
129
+ 'SR02',
130
+ 'hard',
131
+ '.gse/state.json',
132
+ stateResult.exists ? `Invalid JSON: ${stateResult.error}` : 'Missing state.json.',
133
+ stateResult.exists
134
+ ? 'Back up .gse/state.json, inspect the JSON error, then run node <gse-skill>/scripts/update-project-state.mjs --target <project-root> --force --json only if rebuilding from project docs is acceptable.'
135
+ : 'node <gse-skill>/scripts/update-project-state.mjs --target <project-root> --json',
136
+ ))
137
+ }
138
+
139
+ if (!evidenceIndex.ok) {
140
+ actions.push(repairAction(
141
+ 'SR03',
142
+ 'hard',
143
+ '.gse/evidence/index.jsonl',
144
+ evidenceIndex.exists ? `Invalid JSONL: ${evidenceIndex.error}` : 'Missing evidence index.',
145
+ evidenceIndex.exists
146
+ ? 'Back up .gse/evidence/index.jsonl, fix the reported line, and rerun /gse continue before implementation.'
147
+ : 'node <gse-skill>/scripts/update-project-state.mjs --target <project-root> --json',
148
+ ))
149
+ }
150
+
151
+ if (state) {
152
+ const residualRisks = Array.isArray(state.residualRisks) ? state.residualRisks : []
153
+ const longRisks = residualRisks.filter((risk) => String(risk).length > riskLengthLimit)
154
+ if (!Array.isArray(state.residualRisks)) {
155
+ actions.push(repairAction(
156
+ 'SR04',
157
+ 'warning',
158
+ '.gse/state.json',
159
+ 'residualRisks is missing or is not an array.',
160
+ 'Set residualRisks to an explicit array; use [] only when there are no known residual risks.',
161
+ ))
162
+ } else if (residualRisks.length > riskLimit) {
163
+ actions.push(repairAction(
164
+ 'SR05',
165
+ 'warning',
166
+ '.gse/state.json',
167
+ `${residualRisks.length} active residual risks exceed compact limit ${riskLimit}.`,
168
+ 'node <gse-skill>/scripts/run-gse-command.mjs --target <project-root> --command "/gse repair" --execute --json',
169
+ { writeSupported: true, safeToAutoRepair: true },
170
+ ))
171
+ if (writeChanges) {
172
+ const backupPath = backupFile(statePath)
173
+ const repairedState = compactRisks(state, riskLimit)
174
+ fs.writeFileSync(statePath, JSON.stringify(repairedState, null, 2) + '\n', 'utf8')
175
+ writes.push({
176
+ action: 'compact-residual-risks',
177
+ targetPath: '.gse/state.json',
178
+ backupPath: backupPath ? relative(resolvedTarget, backupPath) : null,
179
+ kept: repairedState.residualRisks.length,
180
+ archived: residualRisks.length - repairedState.residualRisks.length,
181
+ })
182
+ }
183
+ }
184
+ if (longRisks.length) {
185
+ actions.push(repairAction(
186
+ 'SR06',
187
+ 'warning',
188
+ '.gse/state.json',
189
+ `${longRisks.length} residual risk(s) exceed ${riskLengthLimit} characters.`,
190
+ 'Shorten active risks to current decision-useful summaries and move detail into riskArchive or the evidence log.',
191
+ ))
192
+ }
193
+ }
194
+
195
+ if (state && evidenceIndex.ok && evidenceIndex.records.length > 0) {
196
+ if (state.lastEvidence && latestEvidence?.evidenceFile && state.lastEvidence !== latestEvidence.evidenceFile) {
197
+ actions.push(repairAction(
198
+ 'SR07',
199
+ 'warning',
200
+ '.gse/state.json',
201
+ `lastEvidence points to ${state.lastEvidence}, but latest evidence index record points to ${latestEvidence.evidenceFile}.`,
202
+ 'Update lastEvidence to the latest evidence file after confirming the newest record belongs to the current project state.',
203
+ ))
204
+ }
205
+ if (latestEvidence?.evidenceFile && !latestEvidenceFileExists(resolvedTarget, latestEvidence)) {
206
+ actions.push(repairAction(
207
+ 'SR08',
208
+ 'hard',
209
+ '.gse/evidence/index.jsonl',
210
+ `Latest evidence file is missing: ${latestEvidence.evidenceFile}.`,
211
+ 'Restore the referenced evidence file or append a new evidence record that points to an existing file.',
212
+ ))
213
+ }
214
+ if (
215
+ state.currentSlice?.nextAction &&
216
+ latestEvidence?.nextAction &&
217
+ state.currentSlice.nextAction !== latestEvidence.nextAction
218
+ ) {
219
+ actions.push(repairAction(
220
+ 'SR09',
221
+ 'warning',
222
+ '.gse/state.json',
223
+ 'currentSlice.nextAction differs from the latest evidence nextAction.',
224
+ 'Confirm whether state or evidence is newer; update only the stale side before selecting an implementation slice.',
225
+ ))
226
+ }
227
+ }
228
+
229
+ const hardActions = actions.filter((item) => item.severity === 'hard')
230
+ const warningActions = actions.filter((item) => item.severity === 'warning')
231
+ return {
232
+ root,
233
+ target: resolvedTarget,
234
+ generatedAt: new Date().toISOString(),
235
+ write: writeChanges,
236
+ summary: {
237
+ status: hardActions.length ? 'repair-required' : warningActions.length ? 'repair-advised' : 'clean',
238
+ hard: hardActions.length,
239
+ warnings: warningActions.length,
240
+ actions: actions.length,
241
+ writes: writes.length,
242
+ },
243
+ state: {
244
+ exists: stateResult.exists,
245
+ valid: stateResult.ok,
246
+ currentSliceId: state?.currentSlice?.id ?? null,
247
+ nextAction: state?.currentSlice?.nextAction ?? null,
248
+ residualRisks: Array.isArray(state?.residualRisks) ? state.residualRisks.length : null,
249
+ riskArchive: Array.isArray(state?.riskArchive) ? state.riskArchive.length : null,
250
+ },
251
+ evidenceIndex: {
252
+ exists: evidenceIndex.exists,
253
+ valid: evidenceIndex.ok,
254
+ records: evidenceIndex.records.length,
255
+ latestEvidenceFile: latestEvidence?.evidenceFile ?? null,
256
+ latestNextAction: latestEvidence?.nextAction ?? null,
257
+ },
258
+ repairActions: actions,
259
+ writes,
260
+ limits: [
261
+ 'Default mode is diagnostic; it does not modify project files.',
262
+ 'Automatic writes are limited to reversible residual-risk compaction with a backup.',
263
+ 'Invalid JSON/JSONL is not guessed or overwritten; repair the reported file or rebuild state from project docs deliberately.',
264
+ ],
265
+ }
266
+ }
267
+
268
+ function createFixture(kind) {
269
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), `gse-state-repair-${kind}-`))
270
+ fs.mkdirSync(path.join(dir, '.gse', 'evidence'), { recursive: true })
271
+ fs.writeFileSync(path.join(dir, '.gse', 'README.md'), '# GSE\n', 'utf8')
272
+ const state = {
273
+ schemaVersion: 1,
274
+ projectName: 'repair-fixture',
275
+ mode: 'standard',
276
+ canonicalPlan: '',
277
+ phase: 'execute',
278
+ currentSlice: {
279
+ id: 'repair-fixture',
280
+ outcome: 'Repair fixture.',
281
+ status: 'verified',
282
+ nextAction: kind === 'stale' ? 'Stale state action.' : 'Run next repair fixture.',
283
+ },
284
+ lastEvidence: '.gse/evidence/2026-07-08.md',
285
+ residualRisks: kind === 'overlong'
286
+ ? Array.from({ length: 9 }, (_, index) => `Fixture risk ${index + 1}.`)
287
+ : ['Fixture risk.'],
288
+ riskArchive: [],
289
+ }
290
+ if (kind === 'bad-state') fs.writeFileSync(path.join(dir, '.gse', 'state.json'), '{ bad json\n', 'utf8')
291
+ else fs.writeFileSync(path.join(dir, '.gse', 'state.json'), JSON.stringify(state, null, 2) + '\n', 'utf8')
292
+ const record = {
293
+ date: '2026-07-08',
294
+ recordType: 'slice',
295
+ status: 'verified',
296
+ evidenceLevel: 'verified-unit',
297
+ requiredEvidenceLevel: 'verified-unit',
298
+ summary: 'Repair fixture evidence.',
299
+ evidenceFile: '.gse/evidence/2026-07-08.md',
300
+ commands: ['fixture'],
301
+ nextAction: 'Run next repair fixture.',
302
+ }
303
+ fs.writeFileSync(path.join(dir, '.gse', 'evidence', '2026-07-08.md'), '# Evidence\n', 'utf8')
304
+ fs.writeFileSync(
305
+ path.join(dir, '.gse', 'evidence', 'index.jsonl'),
306
+ kind === 'bad-jsonl' ? '{"date":"2026-07-08"\n' : JSON.stringify(record) + '\n',
307
+ 'utf8',
308
+ )
309
+ return dir
310
+ }
311
+
312
+ function runSelfTest() {
313
+ const clean = createFixture('clean')
314
+ const badState = createFixture('bad-state')
315
+ const badJsonl = createFixture('bad-jsonl')
316
+ const overlong = createFixture('overlong')
317
+ const stale = createFixture('stale')
318
+ const writeFixture = createFixture('overlong')
319
+ const reports = {
320
+ clean: auditStateRepair(clean),
321
+ badState: auditStateRepair(badState),
322
+ badJsonl: auditStateRepair(badJsonl),
323
+ overlong: auditStateRepair(overlong),
324
+ stale: auditStateRepair(stale),
325
+ write: auditStateRepair(writeFixture, { write: true }),
326
+ }
327
+ const checks = [
328
+ { id: 'SR-T01', status: reports.clean.summary.status === 'clean' ? 'passed' : 'failed', evidence: reports.clean.summary.status },
329
+ { id: 'SR-T02', status: reports.badState.repairActions.some((item) => item.id === 'SR02' && item.severity === 'hard') ? 'passed' : 'failed', evidence: 'bad state fixture' },
330
+ { id: 'SR-T03', status: reports.badJsonl.repairActions.some((item) => item.id === 'SR03' && item.severity === 'hard') ? 'passed' : 'failed', evidence: 'bad JSONL fixture' },
331
+ { id: 'SR-T04', status: reports.overlong.repairActions.some((item) => item.id === 'SR05' && item.safeToAutoRepair) ? 'passed' : 'failed', evidence: 'overlong risk fixture' },
332
+ { id: 'SR-T05', status: reports.stale.repairActions.some((item) => item.id === 'SR09') ? 'passed' : 'failed', evidence: 'stale nextAction fixture' },
333
+ {
334
+ id: 'SR-T06',
335
+ status: reports.write.writes.some((item) => item.action === 'compact-residual-risks' && String(item.backupPath).startsWith('.gse/backups/')) && reports.write.state.residualRisks === 9 ? 'passed' : 'failed',
336
+ evidence: 'write report keeps pre-write count and records .gse backup',
337
+ },
338
+ ]
339
+ const passed = checks.filter((item) => item.status === 'passed').length
340
+ const failed = checks.length - passed
341
+ for (const dir of [clean, badState, badJsonl, overlong, stale, writeFixture]) fs.rmSync(dir, { recursive: true, force: true })
342
+ return {
343
+ root,
344
+ generatedAt: new Date().toISOString(),
345
+ summary: { status: failed === 0 ? 'passed' : 'failed', passed, failed, total: checks.length },
346
+ workflows: { stateRepairPath: failed === 0 ? 'verified' : 'failed' },
347
+ checks,
348
+ }
349
+ }
350
+
351
+ const isCli = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])
352
+
353
+ if (isCli) {
354
+ const report = selfTest ? runSelfTest() : auditStateRepair(targetArg, { write })
355
+
356
+ if (jsonOnly) console.log(JSON.stringify(report, null, 2))
357
+ else console.log(JSON.stringify(report, null, 2))
358
+
359
+ if (report.summary.status === 'failed' || report.summary.status === 'repair-required') process.exit(1)
360
+ }