@agentskit/doc-bridge 1.4.1 → 1.5.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/CHANGELOG.md +19 -0
- package/action.yml +1 -1
- package/dist/cli/program.js +2461 -274
- package/dist/cli/program.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +77 -4
- package/dist/config/index.js.map +1 -1
- package/dist/{index-DAeq_OIi.d.ts → index-Di7PkJuf.d.ts} +209 -8
- package/dist/index.d.ts +1942 -4
- package/dist/index.js +2190 -193
- package/dist/index.js.map +1 -1
- package/docs/PRD-doc-bridge-knowledge-engine.md +338 -0
- package/docs/knowledge-engine-runbook.md +44 -0
- package/ecosystem-claims.json +19 -19
- package/ecosystem-upstream.json +2 -2
- package/ecosystem.json +117 -109
- package/mcpb/manifest.json +25 -1
- package/package.json +2 -2
- package/skills/doc-bridge-handoff/scripts/resolve-handoff.mjs +1 -1
- package/src/agents/registry-adapter.ts +97 -0
- package/src/cli/program.ts +285 -3
- package/src/config/defaults.ts +14 -1
- package/src/config/schema.ts +69 -0
- package/src/conformance/ecosystem-contract.ts +6 -3
- package/src/discovery/documentation.ts +320 -0
- package/src/discovery/repository.ts +514 -0
- package/src/fixes/proposals.ts +165 -0
- package/src/index-builder/content-hash.ts +9 -2
- package/src/index-builder/human-adapters/plain-markdown.ts +17 -1
- package/src/index-builder/llms-txt.ts +22 -2
- package/src/index-builder/scan-corpus.ts +17 -1
- package/src/index.ts +99 -2
- package/src/mcp/server.ts +178 -8
- package/src/reconciliation/reconcile.ts +227 -0
- package/src/report/html.ts +74 -0
- package/src/rules/engine.ts +180 -0
- package/src/safety/repository.ts +84 -0
- package/src/schemas/knowledge.ts +315 -0
- package/src/validate.ts +24 -0
- package/src/version.ts +1 -1
- package/src/workflow/engine.ts +238 -0
- package/tsup.config.ts +2 -1
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { join, relative, resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { contentHashForArtifactV1, sha256NormalizedV1 } from '../index-builder/content-hash.js'
|
|
5
|
+
import { WorkflowRunV1Schema, type WorkflowRunV1, type WorkflowState, type WorkflowStep } from '../schemas/knowledge.js'
|
|
6
|
+
|
|
7
|
+
export const WORKFLOW_STAGES = ['collect', 'normalize', 'reconcile', 'evaluate', 'report'] as const
|
|
8
|
+
export type WorkflowStage = (typeof WORKFLOW_STAGES)[number]
|
|
9
|
+
|
|
10
|
+
export type WorkflowStageContext = {
|
|
11
|
+
readonly root: string
|
|
12
|
+
readonly stage: WorkflowStage
|
|
13
|
+
readonly input: unknown
|
|
14
|
+
readonly previousOutput: unknown
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type WorkflowStageHandler = (context: WorkflowStageContext) => unknown
|
|
18
|
+
|
|
19
|
+
export type WorkflowOptions = {
|
|
20
|
+
readonly root: string
|
|
21
|
+
readonly stateDir?: string
|
|
22
|
+
readonly sourceRevision: string
|
|
23
|
+
readonly configurationHash: string
|
|
24
|
+
readonly toolVersion?: string
|
|
25
|
+
readonly runId?: string
|
|
26
|
+
readonly stage?: WorkflowStage | 'all'
|
|
27
|
+
readonly inputs?: Partial<Record<WorkflowStage, unknown>>
|
|
28
|
+
readonly handlers: Partial<Record<WorkflowStage, WorkflowStageHandler>>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type WorkflowExecutionResult = {
|
|
32
|
+
readonly run: WorkflowRunV1
|
|
33
|
+
readonly stateDir: string
|
|
34
|
+
readonly reusedStages: readonly WorkflowStage[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type PersistedArtifact = {
|
|
38
|
+
readonly type: 'workflow-step-artifact'
|
|
39
|
+
readonly stage: WorkflowStage
|
|
40
|
+
readonly inputHash: string
|
|
41
|
+
readonly outputHash: string
|
|
42
|
+
readonly value: unknown
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const stageState: Record<WorkflowStage, WorkflowState> = {
|
|
46
|
+
collect: 'discovering',
|
|
47
|
+
normalize: 'analyzed',
|
|
48
|
+
reconcile: 'compared',
|
|
49
|
+
evaluate: 'proposed',
|
|
50
|
+
report: 'delivered',
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const defaultStateDir = (root: string): string => join(root, '.doc-bridge', 'workflow')
|
|
54
|
+
|
|
55
|
+
const atomicWrite = (path: string, value: unknown): void => {
|
|
56
|
+
const temp = `${path}.tmp-${process.pid}-${Date.now()}`
|
|
57
|
+
writeFileSync(temp, `${JSON.stringify(value, null, 2)}\n`, 'utf8')
|
|
58
|
+
renameSync(temp, path)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const writeManifest = (stateDir: string, run: WorkflowRunV1): void => {
|
|
62
|
+
atomicWrite(join(stateDir, 'manifest.json'), run)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const appendTransition = (stateDir: string, transition: WorkflowRunV1['transitions'][number]): void => {
|
|
66
|
+
appendFileSync(join(stateDir, 'transitions.jsonl'), `${JSON.stringify(transition)}\n`, 'utf8')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const transition = (run: WorkflowRunV1, to: WorkflowState, reason?: string): WorkflowRunV1 => {
|
|
70
|
+
const item = {
|
|
71
|
+
from: run.state,
|
|
72
|
+
to,
|
|
73
|
+
at: new Date().toISOString(),
|
|
74
|
+
...(reason ? { reason } : {}),
|
|
75
|
+
}
|
|
76
|
+
return { ...run, state: to, transitions: [...run.transitions, item] }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const runId = (): string => `${Date.now()}-${process.pid}`
|
|
80
|
+
|
|
81
|
+
const stageInputHash = (options: WorkflowOptions, stage: WorkflowStage, input: unknown): string =>
|
|
82
|
+
sha256NormalizedV1({ stage, input, sourceRevision: options.sourceRevision, configurationHash: options.configurationHash, toolVersion: options.toolVersion ?? '1.0.0' })
|
|
83
|
+
|
|
84
|
+
const stageArtifactPath = (stateDir: string, stage: WorkflowStage, inputHash: string): string => join(stateDir, 'artifacts', `${stage}-${inputHash}.json`)
|
|
85
|
+
|
|
86
|
+
const readArtifact = (path: string): PersistedArtifact => JSON.parse(readFileSync(path, 'utf8')) as PersistedArtifact
|
|
87
|
+
|
|
88
|
+
const stepArtifactPath = (stateDir: string, step: WorkflowStep): string => resolve(stateDir, step.artifactRefs?.[0] ?? '')
|
|
89
|
+
|
|
90
|
+
const stepOutput = (stateDir: string, run: WorkflowRunV1, stage: WorkflowStage): unknown => {
|
|
91
|
+
const step = run.steps.find((item) => item.name === stage)
|
|
92
|
+
if (!step || step.status !== 'completed' || !step.artifactRefs?.[0]) return null
|
|
93
|
+
return readArtifact(stepArtifactPath(stateDir, step)).value
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const acquireLock = (stateDir: string): (() => void) => {
|
|
97
|
+
const lock = join(stateDir, '.lock')
|
|
98
|
+
try {
|
|
99
|
+
mkdirSync(lock)
|
|
100
|
+
} catch {
|
|
101
|
+
const ownerPath = join(lock, 'owner.json')
|
|
102
|
+
try {
|
|
103
|
+
const owner = JSON.parse(readFileSync(ownerPath, 'utf8')) as { pid?: number }
|
|
104
|
+
if (typeof owner.pid === 'number') process.kill(owner.pid, 0)
|
|
105
|
+
throw new Error(`Workflow is already running (pid ${owner.pid ?? 'unknown'}).`)
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (error instanceof Error && error.message.startsWith('Workflow is already running')) throw error
|
|
108
|
+
rmSync(lock, { recursive: true, force: true })
|
|
109
|
+
mkdirSync(lock)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
writeFileSync(join(lock, 'owner.json'), JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }), 'utf8')
|
|
113
|
+
return () => rmSync(lock, { recursive: true, force: true })
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const loadManifest = (stateDir: string): WorkflowRunV1 | undefined => {
|
|
117
|
+
const path = join(stateDir, 'manifest.json')
|
|
118
|
+
if (!existsSync(path)) return undefined
|
|
119
|
+
return WorkflowRunV1Schema.parse(JSON.parse(readFileSync(path, 'utf8')) as unknown)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const baseRun = (options: WorkflowOptions, stateDir: string, supersedes?: string): WorkflowRunV1 => {
|
|
123
|
+
const inputHash = sha256NormalizedV1({ sourceRevision: options.sourceRevision, configurationHash: options.configurationHash, toolVersion: options.toolVersion ?? '1.0.0' })
|
|
124
|
+
return WorkflowRunV1Schema.parse({
|
|
125
|
+
type: 'workflow-run',
|
|
126
|
+
schemaVersion: 1,
|
|
127
|
+
contentHash: '0'.repeat(64),
|
|
128
|
+
contentHashAlgo: 'sha256-normalized-v1',
|
|
129
|
+
project: { name: resolve(options.root).split('/').pop() ?? 'project', root: '.' },
|
|
130
|
+
sourceRevision: options.sourceRevision,
|
|
131
|
+
sourceRevisionKind: 'content',
|
|
132
|
+
configurationHash: options.configurationHash,
|
|
133
|
+
pipelineVersion: '1.0.0',
|
|
134
|
+
analyzerVersions: { workflow: options.toolVersion ?? '1.0.0' },
|
|
135
|
+
runId: options.runId ?? runId(),
|
|
136
|
+
state: 'created',
|
|
137
|
+
steps: WORKFLOW_STAGES.map((name) => ({ name, status: 'pending', inputHash })) as WorkflowStep[],
|
|
138
|
+
transitions: [{ from: null, to: 'created', at: new Date().toISOString() }],
|
|
139
|
+
artifactRefs: [relative(resolve(options.root), stateDir), ...(supersedes ? [`supersedes:${supersedes}`] : [])],
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const withHash = (run: WorkflowRunV1): WorkflowRunV1 => WorkflowRunV1Schema.parse({ ...run, contentHash: contentHashForArtifactV1(run) })
|
|
144
|
+
|
|
145
|
+
const sameInputs = (run: WorkflowRunV1, options: WorkflowOptions): boolean =>
|
|
146
|
+
run.sourceRevision === options.sourceRevision &&
|
|
147
|
+
run.configurationHash === options.configurationHash &&
|
|
148
|
+
run.analyzerVersions.workflow === (options.toolVersion ?? '1.0.0')
|
|
149
|
+
|
|
150
|
+
const selectedStages = (stage: WorkflowOptions['stage']): readonly WorkflowStage[] =>
|
|
151
|
+
stage && stage !== 'all' ? [stage] : WORKFLOW_STAGES
|
|
152
|
+
|
|
153
|
+
export const runWorkflow = (options: WorkflowOptions): WorkflowExecutionResult => {
|
|
154
|
+
const root = resolve(options.root)
|
|
155
|
+
const stateDir = resolve(root, options.stateDir ?? defaultStateDir(root))
|
|
156
|
+
mkdirSync(join(stateDir, 'artifacts'), { recursive: true })
|
|
157
|
+
const release = acquireLock(stateDir)
|
|
158
|
+
try {
|
|
159
|
+
let run = loadManifest(stateDir)
|
|
160
|
+
let supersedes: string | undefined
|
|
161
|
+
if (run && !sameInputs(run, options)) {
|
|
162
|
+
supersedes = run.runId
|
|
163
|
+
run = withHash(transition(run, 'stale', 'Source revision, configuration hash, or tool version changed.'))
|
|
164
|
+
appendTransition(stateDir, run.transitions[run.transitions.length - 1]!)
|
|
165
|
+
writeManifest(stateDir, run)
|
|
166
|
+
run = undefined
|
|
167
|
+
}
|
|
168
|
+
if (!run) {
|
|
169
|
+
run = withHash(baseRun(options, stateDir, supersedes))
|
|
170
|
+
appendTransition(stateDir, run.transitions[0]!)
|
|
171
|
+
writeManifest(stateDir, run)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!run) throw new Error('Workflow manifest was not initialized.')
|
|
175
|
+
const firstSelectedStage = selectedStages(options.stage)[0]
|
|
176
|
+
const previousStageIndex = firstSelectedStage ? WORKFLOW_STAGES.indexOf(firstSelectedStage) - 1 : -1
|
|
177
|
+
let previousOutput: unknown = previousStageIndex >= 0 ? stepOutput(stateDir, run, WORKFLOW_STAGES[previousStageIndex]!) : null
|
|
178
|
+
const reusedStages: WorkflowStage[] = []
|
|
179
|
+
for (const stage of selectedStages(options.stage)) {
|
|
180
|
+
const input = options.inputs?.[stage] ?? previousOutput
|
|
181
|
+
const inputHash = stageInputHash(options, stage, input)
|
|
182
|
+
const existing = run.steps.find((step) => step.name === stage)
|
|
183
|
+
const artifactPath = existing?.artifactRefs?.[0] ? resolve(stateDir, existing.artifactRefs[0]) : stageArtifactPath(stateDir, stage, inputHash)
|
|
184
|
+
if (existing?.status === 'completed' && existing.inputHash === inputHash && existing.outputHash && existsSync(artifactPath)) {
|
|
185
|
+
previousOutput = readArtifact(artifactPath).value
|
|
186
|
+
reusedStages.push(stage)
|
|
187
|
+
continue
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const handler = options.handlers[stage]
|
|
191
|
+
if (!handler) throw new Error(`No handler configured for workflow stage "${stage}".`)
|
|
192
|
+
run = withHash(transition(run, stageState[stage]))
|
|
193
|
+
appendTransition(stateDir, run.transitions[run.transitions.length - 1]!)
|
|
194
|
+
const runningStep: WorkflowStep = { name: stage, status: 'running', inputHash }
|
|
195
|
+
run = withHash({ ...run, steps: run.steps.map((step) => step.name === stage ? runningStep : step) })
|
|
196
|
+
writeManifest(stateDir, run)
|
|
197
|
+
try {
|
|
198
|
+
const value = handler({ root, stage, input, previousOutput })
|
|
199
|
+
const outputHash = sha256NormalizedV1(value)
|
|
200
|
+
const artifact: PersistedArtifact = { type: 'workflow-step-artifact', stage, inputHash, outputHash, value }
|
|
201
|
+
mkdirSync(join(stateDir, 'artifacts'), { recursive: true })
|
|
202
|
+
if (existsSync(artifactPath)) {
|
|
203
|
+
const existingArtifact = readArtifact(artifactPath)
|
|
204
|
+
if (existingArtifact.outputHash !== outputHash) throw new Error(`Immutable workflow artifact collision for stage "${stage}".`)
|
|
205
|
+
} else {
|
|
206
|
+
atomicWrite(artifactPath, artifact)
|
|
207
|
+
}
|
|
208
|
+
const ref = relative(stateDir, artifactPath)
|
|
209
|
+
const completedStep: WorkflowStep = { name: stage, status: 'completed', inputHash, outputHash, artifactRefs: [ref] }
|
|
210
|
+
run = withHash({ ...run, steps: run.steps.map((step) => step.name === stage ? completedStep : step) })
|
|
211
|
+
writeManifest(stateDir, run)
|
|
212
|
+
previousOutput = value
|
|
213
|
+
} catch (error) {
|
|
214
|
+
run = withHash(transition(run, 'failed', error instanceof Error ? error.message : String(error)))
|
|
215
|
+
appendTransition(stateDir, run.transitions[run.transitions.length - 1]!)
|
|
216
|
+
writeManifest(stateDir, run)
|
|
217
|
+
throw error
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (selectedStages(options.stage).every((stage) => run!.steps.find((step) => step.name === stage)?.status === 'completed')) {
|
|
222
|
+
const complete = selectedStages(options.stage).includes('report') && run.state !== 'delivered' ? withHash(transition(run, 'delivered')) : run
|
|
223
|
+
if (complete !== run) {
|
|
224
|
+
appendTransition(stateDir, complete.transitions[complete.transitions.length - 1]!)
|
|
225
|
+
run = complete
|
|
226
|
+
}
|
|
227
|
+
writeManifest(stateDir, run)
|
|
228
|
+
if (run.state === 'delivered') atomicWrite(join(stateDir, 'last-known-good.json'), { runId: run.runId, manifestHash: run.contentHash, report: run.steps.find((step) => step.name === 'report')?.artifactRefs?.[0] })
|
|
229
|
+
}
|
|
230
|
+
return { run, stateDir, reusedStages }
|
|
231
|
+
} finally {
|
|
232
|
+
release()
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export const loadWorkflowManifest = (stateDir: string): WorkflowRunV1 => WorkflowRunV1Schema.parse(JSON.parse(readFileSync(join(resolve(stateDir), 'manifest.json'), 'utf8')) as unknown)
|
|
237
|
+
|
|
238
|
+
export const loadWorkflowStepOutput = (stateDir: string, stage: WorkflowStage): unknown => stepOutput(resolve(stateDir), loadWorkflowManifest(stateDir), stage)
|
package/tsup.config.ts
CHANGED