@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/action.yml +1 -1
  3. package/dist/cli/program.js +2461 -274
  4. package/dist/cli/program.js.map +1 -1
  5. package/dist/config/index.d.ts +1 -1
  6. package/dist/config/index.js +77 -4
  7. package/dist/config/index.js.map +1 -1
  8. package/dist/{index-DAeq_OIi.d.ts → index-Di7PkJuf.d.ts} +209 -8
  9. package/dist/index.d.ts +1942 -4
  10. package/dist/index.js +2190 -193
  11. package/dist/index.js.map +1 -1
  12. package/docs/PRD-doc-bridge-knowledge-engine.md +338 -0
  13. package/docs/knowledge-engine-runbook.md +44 -0
  14. package/ecosystem-claims.json +19 -19
  15. package/ecosystem-upstream.json +2 -2
  16. package/ecosystem.json +117 -109
  17. package/mcpb/manifest.json +25 -1
  18. package/package.json +2 -2
  19. package/skills/doc-bridge-handoff/scripts/resolve-handoff.mjs +1 -1
  20. package/src/agents/registry-adapter.ts +97 -0
  21. package/src/cli/program.ts +285 -3
  22. package/src/config/defaults.ts +14 -1
  23. package/src/config/schema.ts +69 -0
  24. package/src/conformance/ecosystem-contract.ts +6 -3
  25. package/src/discovery/documentation.ts +320 -0
  26. package/src/discovery/repository.ts +514 -0
  27. package/src/fixes/proposals.ts +165 -0
  28. package/src/index-builder/content-hash.ts +9 -2
  29. package/src/index-builder/human-adapters/plain-markdown.ts +17 -1
  30. package/src/index-builder/llms-txt.ts +22 -2
  31. package/src/index-builder/scan-corpus.ts +17 -1
  32. package/src/index.ts +99 -2
  33. package/src/mcp/server.ts +178 -8
  34. package/src/reconciliation/reconcile.ts +227 -0
  35. package/src/report/html.ts +74 -0
  36. package/src/rules/engine.ts +180 -0
  37. package/src/safety/repository.ts +84 -0
  38. package/src/schemas/knowledge.ts +315 -0
  39. package/src/validate.ts +24 -0
  40. package/src/version.ts +1 -1
  41. package/src/workflow/engine.ts +238 -0
  42. 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
@@ -12,4 +12,5 @@ export default defineConfig({
12
12
  clean: true,
13
13
  splitting: false,
14
14
  target: 'node22',
15
- })
15
+ external: ['typescript'],
16
+ })