Merge remote-tracking branch 'origin/master' into worktree-i18n-update-workflow
# Conflicts: # .agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.i18n.yaml # .agents/skills/dsh-translate-docs/SKILL.md # docs/i18n/README.i18n.yaml
This commit is contained in:
827 files changed
+25749
-9136
No files matched your search
@@ -8,15 +8,18 @@ import { resolve, sep } from 'node:path'
|
||||
|
||||
export const agentNoteRoot = resolve(import.meta.dirname, '../.agents/notes')
|
||||
|
||||
/** The closed set of Agent Note lifecycles (top-level folders under .agents/notes/). */
|
||||
const LIFECYCLES = ['proposed', 'implemented', 'rejected'] as const
|
||||
/** The closed set of active Agent Note lifecycles (top-level folders under .agents/notes/). */
|
||||
const AGENT_NOTE_LIFECYCLES = ['proposed', 'implemented', 'rejected'] as const
|
||||
|
||||
/**
|
||||
* The closed set of Agent Note classes (nested folder under each lifecycle). Adding a
|
||||
* class is a deliberate act: extend this list AND the README's Classification
|
||||
* section. The gate rejects any folder not listed here.
|
||||
*/
|
||||
const CLASSES = ['feature', 'bug-fix', 'simplification', 'architecture', 'process', 'testing'] as const
|
||||
export const AGENT_NOTE_CLASSES = ['feature', 'bug-fix', 'simplification', 'architecture', 'process', 'testing'] as const
|
||||
|
||||
/** Historical implemented notes live outside the active lifecycle tree. */
|
||||
const AGENT_NOTE_ARCHIVE = 'archived'
|
||||
|
||||
/** Non-Agent Note Markdown allowed to sit directly at a lifecycle root. */
|
||||
const ROOT_ALLOWLIST = new Set(['AGENTS.md', 'CLAUDE.md'])
|
||||
@@ -45,11 +48,13 @@ export function walkAgentNoteTree(): { notes: AgentNote[]; errors: string[] } {
|
||||
errors.push('structure: INDEX.md — centralized Agent Note indexes are forbidden; browse the lifecycle/class tree or search the repository')
|
||||
continue
|
||||
}
|
||||
if (entry.isDirectory() && !(LIFECYCLES as readonly string[]).includes(entry.name)) {
|
||||
errors.push(`structure: ${entry.name}/ — unknown lifecycle folder (allowed: ${LIFECYCLES.join(', ')})`)
|
||||
if (entry.isDirectory()
|
||||
&& entry.name !== AGENT_NOTE_ARCHIVE
|
||||
&& !(AGENT_NOTE_LIFECYCLES as readonly string[]).includes(entry.name)) {
|
||||
errors.push(`structure: ${entry.name}/ — unknown lifecycle folder (allowed: ${AGENT_NOTE_LIFECYCLES.join(', ')}, plus ${AGENT_NOTE_ARCHIVE}/)`)
|
||||
}
|
||||
}
|
||||
for (const lifecycle of LIFECYCLES) {
|
||||
for (const lifecycle of AGENT_NOTE_LIFECYCLES) {
|
||||
for (const match of globSync(`${lifecycle}/**/*.md`, { cwd: agentNoteRoot }).map(path => path.split(sep).join('/')).sort()) {
|
||||
const segs = match.split('/')
|
||||
// Allowlisted file directly at the lifecycle root (e.g. implemented/AGENTS.md).
|
||||
@@ -63,8 +68,8 @@ export function walkAgentNoteTree(): { notes: AgentNote[]; errors: string[] } {
|
||||
errors.push(`structure: ${match} — expected {lifecycle}/{class}/file.md (got depth ${segs.length})`)
|
||||
continue
|
||||
}
|
||||
if (!(CLASSES as readonly string[]).includes(cls)) {
|
||||
errors.push(`structure: ${match} — unknown class folder "${cls}" (allowed: ${CLASSES.join(', ')})`)
|
||||
if (!(AGENT_NOTE_CLASSES as readonly string[]).includes(cls)) {
|
||||
errors.push(`structure: ${match} — unknown class folder "${cls}" (allowed: ${AGENT_NOTE_CLASSES.join(', ')})`)
|
||||
continue
|
||||
}
|
||||
if (!/^\d{4}-\d{2}-\d{2}-.+\.md$/.test(base)) {
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
extendArchiveManifest,
|
||||
gitBlobHash,
|
||||
parseArchiveManifest,
|
||||
renderArchiveManifest,
|
||||
validateArchiveArtifacts,
|
||||
validateArchiveManifestExtension,
|
||||
type ArchiveManifest,
|
||||
} from './archived-agent-notes.ts'
|
||||
import { isArchivedAgentNotePath } from './repo-files.ts'
|
||||
|
||||
function fixture(): Map<string, Buffer> {
|
||||
const base = '2026-07-26-example'
|
||||
const source = Buffer.from(`# Agent Note: Example\n\nStatus: implemented\nArchived: 2026-07-26\n\nEnglish | [中文](${base}.zh.md)\n\n## Problem\n\nExample.\n`)
|
||||
const zh = Buffer.from(`# Agent Note: 示例\n\nStatus: implemented\nArchived: 2026-07-26\n\n[English](${base}.md) | 中文\n\n## 问题\n\n示例。\n`)
|
||||
const meta = Buffer.from(`${base}.md: ${gitBlobHash(source)}\n${base}.zh.md: ${gitBlobHash(zh)}\n`)
|
||||
return new Map([
|
||||
[`process/${base}.md`, source],
|
||||
[`process/${base}.zh.md`, zh],
|
||||
[`process/${base}.i18n.yaml`, meta],
|
||||
])
|
||||
}
|
||||
|
||||
describe('archived Agent Notes', () => {
|
||||
it('recognizes archived paths with POSIX and Windows separators', () => {
|
||||
expect(isArchivedAgentNotePath('.agents/notes/archived/process/example.md')).toBe(true)
|
||||
expect(isArchivedAgentNotePath('.agents\\notes\\archived\\process\\example.md')).toBe(true)
|
||||
expect(isArchivedAgentNotePath('.agents/notes/implemented/process/example.md')).toBe(false)
|
||||
})
|
||||
|
||||
it('accepts one complete implemented triplet with matching archive metadata', () => {
|
||||
expect(validateArchiveArtifacts(fixture())).toEqual([])
|
||||
})
|
||||
|
||||
it('rejects incomplete triplets and invalid archive headers', () => {
|
||||
const artifacts = fixture()
|
||||
artifacts.delete('process/2026-07-26-example.i18n.yaml')
|
||||
artifacts.set(
|
||||
'process/2026-07-26-example.md',
|
||||
Buffer.from('# Agent Note: Example\n\nStatus: proposed\nArchived: yesterday\n'),
|
||||
)
|
||||
expect(validateArchiveArtifacts(artifacts).join('\n')).toMatch(/incomplete archived triplet/)
|
||||
})
|
||||
|
||||
it('extends the manifest without permitting a sealed change or removal', () => {
|
||||
const artifacts = fixture()
|
||||
const empty: ArchiveManifest = { version: 1, files: {} }
|
||||
const first = extendArchiveManifest(empty, artifacts)
|
||||
expect(first.errors).toEqual([])
|
||||
expect(first.added).toHaveLength(3)
|
||||
|
||||
const sealed: ArchiveManifest = { version: 1, files: first.files }
|
||||
const changed = new Map(artifacts)
|
||||
changed.set('process/2026-07-26-example.md', Buffer.from('changed'))
|
||||
expect(extendArchiveManifest(sealed, changed).errors).toEqual([
|
||||
'process/2026-07-26-example.md: sealed content hash changed',
|
||||
])
|
||||
changed.delete('process/2026-07-26-example.zh.md')
|
||||
expect(extendArchiveManifest(sealed, changed).errors).toContain(
|
||||
'process/2026-07-26-example.zh.md: sealed artifact is missing',
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects replacing manifest seals alongside changed archive content', () => {
|
||||
const artifacts = fixture()
|
||||
const initial = extendArchiveManifest({ version: 1, files: {} }, artifacts)
|
||||
const baseline: ArchiveManifest = { version: 1, files: initial.files }
|
||||
const path = 'process/2026-07-26-example.md'
|
||||
const changedArtifacts = new Map(artifacts)
|
||||
changedArtifacts.set(path, Buffer.from('changed'))
|
||||
const replacement = extendArchiveManifest({ version: 1, files: {} }, changedArtifacts)
|
||||
const current: ArchiveManifest = { version: 1, files: replacement.files }
|
||||
|
||||
expect(extendArchiveManifest(current, changedArtifacts).errors).toEqual([])
|
||||
expect(validateArchiveManifestExtension(baseline, current)).toEqual([
|
||||
`${path}: sealed manifest hash changed`,
|
||||
])
|
||||
const removed: ArchiveManifest = {
|
||||
version: 1,
|
||||
files: Object.fromEntries(Object.entries(current.files).filter(([candidate]) => candidate !== path)),
|
||||
}
|
||||
expect(validateArchiveManifestExtension(baseline, removed)).toContain(
|
||||
`${path}: sealed manifest entry is missing`,
|
||||
)
|
||||
})
|
||||
|
||||
it('round-trips the deterministic manifest schema', () => {
|
||||
const content = renderArchiveManifest({ 'process/z.md': `sha256:${'a'.repeat(64)}` })
|
||||
expect(parseArchiveManifest(content)).toEqual({
|
||||
version: 1,
|
||||
files: { 'process/z.md': `sha256:${'a'.repeat(64)}` },
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,190 @@
|
||||
/** Pure archive-format, triplet, and immutable-manifest helpers. */
|
||||
|
||||
import { createHash } from 'node:crypto'
|
||||
import { basename } from 'node:path'
|
||||
import { AGENT_NOTE_CLASSES } from './agent-note-tree.ts'
|
||||
|
||||
/** Versioned shape of the frozen-content manifest. */
|
||||
export interface ArchiveManifest {
|
||||
version: 1
|
||||
files: Readonly<Record<string, string>>
|
||||
}
|
||||
|
||||
/** Hash one archived artifact independently of the repository's Git object format. */
|
||||
function archiveContentHash(content: Buffer): string {
|
||||
return `sha256:${createHash('sha256').update(content).digest('hex')}`
|
||||
}
|
||||
|
||||
/** Compute the SHA-1 Git blob id used by bilingual consistency sidecars. */
|
||||
export function gitBlobHash(content: Buffer): string {
|
||||
const hash = createHash('sha1')
|
||||
hash.update(`blob ${content.byteLength}\0`)
|
||||
hash.update(content)
|
||||
return hash.digest('hex')
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
/** Parse the archive manifest and reject fields or hashes outside its closed schema. */
|
||||
export function parseArchiveManifest(content: string): ArchiveManifest {
|
||||
const value: unknown = JSON.parse(content)
|
||||
if (!isRecord(value)) throw new Error('expected a JSON object')
|
||||
const fields = Object.keys(value).sort()
|
||||
if (fields.join(',') !== 'files,version') throw new Error('expected exactly the fields `version` and `files`')
|
||||
if (value.version !== 1) throw new Error('unsupported manifest version (expected 1)')
|
||||
if (!isRecord(value.files)) throw new Error('`files` must be an object')
|
||||
const files: Record<string, string> = {}
|
||||
for (const [path, hash] of Object.entries(value.files)) {
|
||||
if (typeof hash !== 'string' || !/^sha256:[0-9a-f]{64}$/.test(hash)) {
|
||||
throw new Error(`invalid content hash for ${path}`)
|
||||
}
|
||||
files[path] = hash
|
||||
}
|
||||
return { version: 1, files }
|
||||
}
|
||||
|
||||
/** Render the archive manifest with deterministic path ordering. */
|
||||
export function renderArchiveManifest(files: Readonly<Record<string, string>>): string {
|
||||
return `${JSON.stringify({
|
||||
version: 1,
|
||||
files: Object.fromEntries(Object.entries(files).sort(([left], [right]) => left.localeCompare(right))),
|
||||
}, null, 2)}\n`
|
||||
}
|
||||
|
||||
/** Reject changes or removals of entries sealed by a prior manifest. */
|
||||
export function validateArchiveManifestExtension(
|
||||
baseline: ArchiveManifest,
|
||||
current: ArchiveManifest,
|
||||
): string[] {
|
||||
const errors: string[] = []
|
||||
for (const [path, expected] of Object.entries(baseline.files)) {
|
||||
const actual = current.files[path]
|
||||
if (actual === undefined) errors.push(`${path}: sealed manifest entry is missing`)
|
||||
else if (actual !== expected) errors.push(`${path}: sealed manifest hash changed`)
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
function validDate(value: string): boolean {
|
||||
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value)
|
||||
if (match === null) return false
|
||||
const year = Number(match[1])
|
||||
const month = Number(match[2])
|
||||
const day = Number(match[3])
|
||||
const date = new Date(Date.UTC(year, month - 1, day))
|
||||
return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day
|
||||
}
|
||||
|
||||
interface Triplet {
|
||||
source?: Buffer
|
||||
zh?: Buffer
|
||||
meta?: Buffer
|
||||
}
|
||||
|
||||
function pairMeta(content: string): Map<string, string> | undefined {
|
||||
const entries = new Map<string, string>()
|
||||
for (const line of content.split('\n')) {
|
||||
if (line === '' || line.startsWith('#')) continue
|
||||
const match = /^([^:#]+\.md): ([0-9a-f]{40})$/.exec(line)
|
||||
if (match?.[1] === undefined || match[2] === undefined) return undefined
|
||||
entries.set(match[1], match[2])
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
function validateHeader(path: string, content: Buffer, sourceBase: string, chinese: boolean): string[] {
|
||||
const errors: string[] = []
|
||||
const lines = content.toString('utf8').split('\n')
|
||||
if (!/^# Agent Note: \S/.test(lines[0] ?? '')) errors.push(`${path}: line 1 must be \`# Agent Note: <title>\``)
|
||||
if (lines[1] !== '') errors.push(`${path}: line 2 must be blank`)
|
||||
if (lines[2] !== 'Status: implemented') errors.push(`${path}: line 3 must be \`Status: implemented\``)
|
||||
const archived = /^Archived: (\d{4}-\d{2}-\d{2})$/.exec(lines[3] ?? '')?.[1]
|
||||
if (archived === undefined || !validDate(archived)) {
|
||||
errors.push(`${path}: line 4 must be \`Archived: YYYY-MM-DD\` with a valid date`)
|
||||
} else if (archived < sourceBase.slice(0, 10)) {
|
||||
errors.push(`${path}: archive date ${archived} predates the note filename`)
|
||||
}
|
||||
if (lines[4] !== '') errors.push(`${path}: line 5 must be blank`)
|
||||
const switcher = chinese
|
||||
? `[English](${sourceBase}.md) | 中文`
|
||||
: `English | [中文](${sourceBase}.zh.md)`
|
||||
if (lines[5] !== switcher) errors.push(`${path}: line 6 must be ${JSON.stringify(switcher)}`)
|
||||
return errors
|
||||
}
|
||||
|
||||
/** Validate the closed kind tree, implemented/archive headers, and complete bilingual triplets. */
|
||||
export function validateArchiveArtifacts(artifacts: ReadonlyMap<string, Buffer>): string[] {
|
||||
const errors: string[] = []
|
||||
const triplets = new Map<string, Triplet>()
|
||||
for (const [path, content] of artifacts) {
|
||||
const match = /^([^/]+)\/(\d{4}-\d{2}-\d{2}-.+?)(\.zh\.md|\.i18n\.yaml|\.md)$/.exec(path)
|
||||
if (match?.[1] === undefined || match[2] === undefined || match[3] === undefined) {
|
||||
errors.push(`${path}: expected {kind}/yyyy-mm-dd-topic.{md,zh.md,i18n.yaml}`)
|
||||
continue
|
||||
}
|
||||
if (!(AGENT_NOTE_CLASSES as readonly string[]).includes(match[1])) {
|
||||
errors.push(`${path}: unknown Agent Note kind ${JSON.stringify(match[1])}`)
|
||||
continue
|
||||
}
|
||||
const key = `${match[1]}/${match[2]}`
|
||||
const triplet = triplets.get(key) ?? {}
|
||||
if (match[3] === '.md') triplet.source = content
|
||||
else if (match[3] === '.zh.md') triplet.zh = content
|
||||
else triplet.meta = content
|
||||
triplets.set(key, triplet)
|
||||
}
|
||||
|
||||
for (const [key, triplet] of [...triplets].sort(([left], [right]) => left.localeCompare(right))) {
|
||||
const sourcePath = `${key}.md`
|
||||
const zhPath = `${key}.zh.md`
|
||||
const metaPath = `${key}.i18n.yaml`
|
||||
const { source, zh, meta } = triplet
|
||||
const missing = [
|
||||
source === undefined ? sourcePath : undefined,
|
||||
zh === undefined ? zhPath : undefined,
|
||||
meta === undefined ? metaPath : undefined,
|
||||
].filter((path): path is string => path !== undefined)
|
||||
if (source === undefined || zh === undefined || meta === undefined) {
|
||||
errors.push(`${key}: incomplete archived triplet; missing ${missing.join(', ')}`)
|
||||
continue
|
||||
}
|
||||
const sourceBase = basename(key)
|
||||
errors.push(...validateHeader(sourcePath, source, sourceBase, false))
|
||||
errors.push(...validateHeader(zhPath, zh, sourceBase, true))
|
||||
const sourceDate = /^Archived: (\d{4}-\d{2}-\d{2})$/m.exec(source.toString('utf8'))?.[1]
|
||||
const zhDate = /^Archived: (\d{4}-\d{2}-\d{2})$/m.exec(zh.toString('utf8'))?.[1]
|
||||
if (sourceDate !== undefined && zhDate !== undefined && sourceDate !== zhDate) {
|
||||
errors.push(`${key}: English and Chinese archive dates differ (${sourceDate} vs ${zhDate})`)
|
||||
}
|
||||
const pair = pairMeta(meta.toString('utf8'))
|
||||
if (pair === undefined || pair.size !== 2
|
||||
|| pair.get(`${sourceBase}.md`) !== gitBlobHash(source)
|
||||
|| pair.get(`${sourceBase}.zh.md`) !== gitBlobHash(zh)) {
|
||||
errors.push(`${metaPath}: consistency record must contain the current Git blob hashes of both archived sides`)
|
||||
}
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
/** Preserve every sealed path/hash and append hashes for newly archived artifacts. */
|
||||
export function extendArchiveManifest(
|
||||
existing: ArchiveManifest,
|
||||
artifacts: ReadonlyMap<string, Buffer>,
|
||||
): { files: Record<string, string>; added: string[]; errors: string[] } {
|
||||
const errors: string[] = []
|
||||
const files: Record<string, string> = { ...existing.files }
|
||||
for (const [path, expected] of Object.entries(existing.files)) {
|
||||
const content = artifacts.get(path)
|
||||
if (content === undefined) errors.push(`${path}: sealed artifact is missing`)
|
||||
else if (archiveContentHash(content) !== expected) errors.push(`${path}: sealed content hash changed`)
|
||||
}
|
||||
const added: string[] = []
|
||||
for (const [path, content] of [...artifacts].sort(([left], [right]) => left.localeCompare(right))) {
|
||||
if (files[path] !== undefined) continue
|
||||
files[path] = archiveContentHash(content)
|
||||
added.push(path)
|
||||
}
|
||||
return { files, added, errors }
|
||||
}
|
||||
@@ -6,6 +6,6 @@
|
||||
"docs/defensive-patterns.md": 550,
|
||||
"docs/testing.md": 1100,
|
||||
"examples/AGENTS.md": 310,
|
||||
"packages/AGENTS.md": 660,
|
||||
"packages/AGENTS.md": 675,
|
||||
"packages/README.md": 835
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import ts from 'typescript'
|
||||
import { builtDeclarationPath } from './doc-typecheck-paths.ts'
|
||||
import { extractFences } from './md-fences.ts'
|
||||
import { partitionPairedMarkdownDerivatives } from './paired-markdown-derivatives.ts'
|
||||
import { isArchivedAgentNotePath } from './repo-files.ts'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
|
||||
@@ -204,7 +205,9 @@ const markdownGlobs = ['README.md', '.agents/notes/**/*.md', 'docs/**/*.md', 'pa
|
||||
|
||||
const files: string[] = []
|
||||
for (const pattern of markdownGlobs) {
|
||||
for (const match of globSync(pattern, { cwd: root })) files.push(resolve(root, match))
|
||||
for (const match of globSync(pattern, { cwd: root })) {
|
||||
if (!isArchivedAgentNotePath(match)) files.push(resolve(root, match))
|
||||
}
|
||||
}
|
||||
files.sort()
|
||||
|
||||
|
||||
@@ -165,6 +165,7 @@ export const LINK_MAP: Record<string, string> = {
|
||||
TaskSnapshot: 'tasks.md',
|
||||
TaskStart: 'tasks.md',
|
||||
TokenMeasurement: 'token-meter.md',
|
||||
CodeDispatchLog: 'tools.md',
|
||||
PostToolDecision: 'tools.md',
|
||||
PreToolDecision: 'tools.md',
|
||||
ToolDefinition: 'tools.md',
|
||||
@@ -206,6 +207,10 @@ const FOUNDATION_TYPE_NAMES = new Set([
|
||||
/** Project types deliberately documented outside the core-data catalog. */
|
||||
const TYPE_LINK_EXEMPTIONS: Readonly<Record<string, string>> = {
|
||||
AgentFactory: 'agent creation seam is owned by packages/core/agent/README.md',
|
||||
BeginCommandRequest: 'event-local request contract is owned by packages/client/ui-slash/src/types.ts',
|
||||
InsertReferenceRequest: 'event-local request contract is owned by packages/client/ui-slash/src/types.ts',
|
||||
ConsumeTokenRequest: 'event-local request contract is owned by packages/client/ui-slash/src/types.ts',
|
||||
InsertTextRequest: 'event-local request contract is owned by packages/client/ui-slash/src/types.ts',
|
||||
AgentHandle: 'agent ownership handle is owned by packages/core/agent/README.md',
|
||||
BashEnvContributor: 'service-local extension type is owned by packages/bash/tool-bash/src/index.ts',
|
||||
BashEnvVariableInfo: 'service-local metadata type is owned by packages/bash/tool-bash/src/index.ts',
|
||||
@@ -388,7 +393,7 @@ export function collectEvents(scanRoot: string = root): EventEntry[] {
|
||||
const where = `event '${name}' (${src})`
|
||||
checkTypeLinks(where, member, sf, typeLinkViolations)
|
||||
if (!mode) {
|
||||
violations.push(`${where} is missing an @mode tag. Add '@mode emit|waterfall|parallel|serial' to its JSDoc (see AGENTS.md).`)
|
||||
violations.push(`${where} is missing an @mode tag. Add '@mode emit|waterfall|parallel|serial|bail' to its JSDoc (see AGENTS.md).`)
|
||||
}
|
||||
// Conclusive structural check: a trailing `next: () => …` parameter is a
|
||||
// waterfall. (emit vs parallel vs serial is not structurally
|
||||
@@ -579,7 +584,7 @@ export function renderEvents(events: EventEntry[]): string {
|
||||
'',
|
||||
'The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns, grouped by scope. The **inherited tier** at the end is the cordis-core + loader/hmr/timer event surface a plugin also sees — pinned vendor source, summarized tersely. The event-dispatch methods themselves are generated in the [Cordis core Events API](core/events.md).',
|
||||
'',
|
||||
'Dispatch modes: **emit** (fire-and-forget), **waterfall** (each listener gets `next()` and may transform or veto — see [waterfall semantics](../cordis-primer.md#cordis-waterfall-semantics)), **parallel** (awaited fan-out; all listeners run), **serial** (awaited in registration order until one returns a bail value — anything other than `null`, `false`, or `undefined`).',
|
||||
'Dispatch modes: **emit** (fire-and-forget), **waterfall** (each listener gets `next()` and may transform or veto — see [waterfall semantics](../cordis-primer.md#cordis-waterfall-semantics)), **parallel** (awaited fan-out; all listeners run), **serial** (awaited in registration order until one returns a bail value — anything other than `null`, `false`, or `undefined`), **bail** (synchronous in-order dispatch until one listener returns a bail value; the scoped input-mutation events use it for an applied/not-applied answer).',
|
||||
'',
|
||||
]
|
||||
const scopes = [...new Set(events.map(e => e.scope))].sort()
|
||||
|
||||
@@ -365,9 +365,10 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
key: 'tasks',
|
||||
pkg: 'tasks',
|
||||
title: 'Background task registry',
|
||||
mode: 'core',
|
||||
mode: 'seam',
|
||||
implementations: ['tasks-local'],
|
||||
consumers: ['tool-bash', 'tool-pty', 'tool-subagent', 'tool-tasks'],
|
||||
note: 'Producers (background bash, PTY sends, and subagent delegations) register running work; tool-tasks is the model-facing control surface that reads, lists, and kills it.',
|
||||
note: 'Producers (background bash, PTY sends, and subagent delegations) register running work; tool-tasks is the model-facing control surface that reads, lists, and kills it; tasks-local is the process-local registry.',
|
||||
},
|
||||
{
|
||||
key: 'web',
|
||||
@@ -916,8 +917,13 @@ function renderEventRelations(pkgs: Pkg[]): string {
|
||||
lines.push(`| \`${event.name}\` | \`${event.mode}\` | ${sourceLink(event.source)} | ${relationPackages(relation.dispatchers, pkgsByShort)} | ${listenerPackages(relation.listeners, pkgsByShort)} |`)
|
||||
}
|
||||
// Every declared event needs a dispatcher: zero means dead vocabulary or an
|
||||
// unrecognized semantic dispatch shape. Listener-free extension points remain valid.
|
||||
// unrecognized semantic dispatch shape. Listener-free extension points remain
|
||||
// valid. Client-declared events are exempt: the relation scan seeds the HOST
|
||||
// aggregate program only (host+client cannot share one program — the cordis
|
||||
// Context merges collide), so client dispatch sites are structurally
|
||||
// invisible here; their rows stay in the table for the declarations' sake.
|
||||
const undispatched = [...events]
|
||||
.filter(event => !event.source.startsWith('packages/client/'))
|
||||
.filter(event => (relations.get(event.name)?.dispatchers.size ?? 0) === 0)
|
||||
.map(event => event.name)
|
||||
.sort()
|
||||
@@ -1128,7 +1134,7 @@ function renderIndex(docs: GraphDoc[]): string {
|
||||
...generatedHeader('Documentation Graph Index'),
|
||||
'These diagrams are the relationship layer above the generated catalogs. Use them to navigate package topology, capability seams, event flow, model-facing tools, app composition, and runtime lifecycle paths. Exact signatures and type shapes still live in the generated [events](cordis-catalog/events.md) / [services](cordis-catalog/services.md) catalogs, [tool-catalog.md](tool-catalog.md), and [core-data-structures/](core-data-structures/core.md).',
|
||||
'',
|
||||
'The process decision behind this index is recorded in [the documentation graph Agent Note](../.agents/notes/implemented/process/2026-07-03-documentation-graph-atlas.md).',
|
||||
'The process decision behind this index is recorded in [the documentation graph Agent Note](../.agents/notes/archived/process/2026-07-03-documentation-graph-atlas.md).',
|
||||
'',
|
||||
'| Graph | Mode |',
|
||||
'| --- | --- |',
|
||||
|
||||
@@ -29,7 +29,7 @@ import SubagentService from '@deepseek-ai/dsh-subagent'
|
||||
import type { SubagentProvider } from '@deepseek-ai/dsh-subagent'
|
||||
import SkillService from '@deepseek-ai/dsh-skill'
|
||||
import * as SkillLocal from '@deepseek-ai/dsh-skill-local'
|
||||
import TaskService from '@deepseek-ai/dsh-tasks'
|
||||
import LocalTaskService from '@deepseek-ai/dsh-tasks-local'
|
||||
import * as ToolAskUser from '@deepseek-ai/dsh-tool-ask-user'
|
||||
import * as ToolBash from '@deepseek-ai/dsh-tool-bash'
|
||||
import * as ToolCordis from '@deepseek-ai/dsh-tool-cordis'
|
||||
@@ -355,7 +355,7 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
requires: ['ctx.tools', 'ctx.tasks', 'ctx.systemPrompt'],
|
||||
writes: ['tool/call', 'tool/result', 'user/message via agent.inject() for background completion notices'],
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(TaskService)
|
||||
await ctx.plugin(LocalTaskService)
|
||||
await ctx.plugin(ToolTasks)
|
||||
},
|
||||
note:
|
||||
|
||||
+2
-2
@@ -19,7 +19,7 @@ export function rawJsDoc(text: string, node: ts.Node): string {
|
||||
}
|
||||
|
||||
/** A dispatch mode, rendered as the badge after an event name in the catalog. */
|
||||
export type Mode = 'emit' | 'waterfall' | 'parallel' | 'serial'
|
||||
export type Mode = 'emit' | 'waterfall' | 'parallel' | 'serial' | 'bail'
|
||||
|
||||
/**
|
||||
* Parse a raw JSDoc block into description prose and an optional `@mode`. Prose
|
||||
@@ -59,7 +59,7 @@ export function parseJsDoc(raw: string): { doc: string; mode: Mode | null; hasMo
|
||||
}
|
||||
for (const line of inner) {
|
||||
const tagLine = line.trimStart()
|
||||
const m = /^@mode\s+(emit|waterfall|parallel|serial)\s*$/.exec(tagLine)
|
||||
const m = /^@mode\s+(emit|waterfall|parallel|serial|bail)\s*$/.exec(tagLine)
|
||||
if (m) { mode = m[1] as Mode; hasMode = true; flushPara(); inTags = true; continue }
|
||||
if (/^@mode\b/.test(tagLine)) { hasMode = true; flushPara(); inTags = true; continue }
|
||||
if (tagLine.startsWith('@')) { flushPara(); inTags = true; continue }
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Temporary branch-convergence command for canonical packed session fixtures.
|
||||
*
|
||||
* @see ../.agents/notes/proposed/process/2026-07-26-remove-packed-session-fixture-migrator.md
|
||||
*/
|
||||
|
||||
import { writeFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { inspectSessionFixtureLayouts } from './session-fixture-layout.ts'
|
||||
|
||||
if (process.argv.length > 2) throw new Error('migrate:packed-session-fixtures takes no arguments')
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const fixtures = inspectSessionFixtureLayouts(root)
|
||||
const changed = fixtures.filter(fixture => fixture.source !== fixture.canonical)
|
||||
for (const fixture of changed) {
|
||||
writeFileSync(resolve(root, fixture.path), fixture.canonical)
|
||||
console.log(fixture.path)
|
||||
}
|
||||
console.log(`packed session fixtures: ${changed.length} rewritten, ${fixtures.length} inspected`)
|
||||
@@ -21,6 +21,11 @@ export interface ReferenceViolation {
|
||||
ref: string
|
||||
}
|
||||
|
||||
/** Whether a repository path is frozen Agent Note history, not evolving source prose. */
|
||||
export function isArchivedAgentNotePath(path: string): boolean {
|
||||
return path.replaceAll('\\', '/').startsWith('.agents/notes/archived/')
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand repository-relative globs and deduplicate symlinked files.
|
||||
* @param root - absolute repository root.
|
||||
|
||||
@@ -454,6 +454,7 @@ function docSyncLeafGates(options: {
|
||||
pnpmScript('mermaid', 'verify-mermaid'),
|
||||
pnpmScript('agent-note-classification', 'verify-agent-note-classification', { label: 'agent note classification' }),
|
||||
pnpmScript('agent-note-format', 'verify-agent-note-format', { label: 'agent note format' }),
|
||||
pnpmScript('archived-agent-notes', 'verify-archived-agent-notes', { label: 'archived agent notes' }),
|
||||
pnpmScript('type-equivalence', 'verify-type-equiv', { label: 'type equivalence' }),
|
||||
pnpmScript('translation-prompt', 'verify-translation-prompt', { label: 'translation prompt' }),
|
||||
pnpmScript('translation-pairing', 'verify-translation-pairing', { label: 'translation pairing' }),
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/** Repository-wide canonical-layout check for committed session fixtures. */
|
||||
|
||||
import { resolve } from 'node:path'
|
||||
import { expect, it } from 'vitest'
|
||||
import { inspectSessionFixtureLayouts } from './session-fixture-layout.ts'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
|
||||
it('keeps every session-format JSONL fixture in canonical packed layout', () => {
|
||||
const nonCanonical = inspectSessionFixtureLayouts(root)
|
||||
.filter(fixture => fixture.source !== fixture.canonical)
|
||||
.map(fixture => fixture.path)
|
||||
expect(
|
||||
nonCanonical,
|
||||
'Run `pnpm run migrate:packed-session-fixtures` and commit the mechanical fixture rewrite.',
|
||||
).toEqual([])
|
||||
})
|
||||
@@ -0,0 +1,57 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { decodeStorageRecord, type SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import { canonicalSessionFixture } from './session-fixture-layout.ts'
|
||||
|
||||
const HEADER = ' {"type":"session","version":0,"id":"fixture","createdAt":1,"delegationDepth":0} '
|
||||
|
||||
function chunkRun(): SessionEvent[] {
|
||||
return Array.from({ length: 4 }, (_, index) => ({
|
||||
type: 'assistant/chunk',
|
||||
seq: index,
|
||||
time: 10 + index,
|
||||
data: {
|
||||
turn: 1,
|
||||
step: 1,
|
||||
chunk: { type: 'text-delta', index: 0, text: `part-${index}` },
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
function unpackedFixture(): string {
|
||||
return [HEADER, ...chunkRun().map(event => JSON.stringify(event)), ''].join('\n')
|
||||
}
|
||||
|
||||
function decodedBody(content: string): SessionEvent[] {
|
||||
return content.trimEnd().split('\n').slice(1)
|
||||
.flatMap(line => decodeStorageRecord(JSON.parse(line) as unknown))
|
||||
}
|
||||
|
||||
describe('canonicalSessionFixture', () => {
|
||||
it('preserves the header line and packs an unpacked event run losslessly', () => {
|
||||
const canonical = canonicalSessionFixture(unpackedFixture(), 'fixture.jsonl')
|
||||
expect(canonical).toBeDefined()
|
||||
expect(canonical?.split('\n')[0]).toBe(HEADER)
|
||||
expect(JSON.parse(canonical?.split('\n')[1] ?? '{}')).toMatchObject({ type: 'text-chunks' })
|
||||
expect(decodedBody(canonical ?? '')).toStrictEqual(chunkRun())
|
||||
})
|
||||
|
||||
it('ignores JSONL whose first record is not a session header', () => {
|
||||
expect(canonicalSessionFixture('{"type":"session_event"}\n{"value":1}\n')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('is idempotent for an already packed fixture', () => {
|
||||
const packed = canonicalSessionFixture(unpackedFixture())
|
||||
expect(packed).toBeDefined()
|
||||
expect(canonicalSessionFixture(packed ?? '')).toBe(packed)
|
||||
})
|
||||
|
||||
it('fails loud on malformed records after a session header', () => {
|
||||
expect(() => canonicalSessionFixture(`${HEADER}\n{not-json}\n`, 'broken.jsonl'))
|
||||
.toThrow(/broken\.jsonl:2: invalid JSON/)
|
||||
})
|
||||
|
||||
it('labels malformed packed rows with the fixture path and line', () => {
|
||||
expect(() => canonicalSessionFixture(`${HEADER}\n{"type":"text-chunks"}\n`, 'broken.jsonl'))
|
||||
.toThrow(/broken\.jsonl:2: invalid session storage record: malformed text-chunks storage row/)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,128 @@
|
||||
/** Canonical packed-row layout helpers for repository session fixtures. */
|
||||
|
||||
import { deepStrictEqual } from 'node:assert'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { decodeStorageRecord, packChunkRuns, type SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
|
||||
/** One repository session fixture and its canonical packed representation. */
|
||||
export interface SessionFixtureLayout {
|
||||
/** Repository-relative path with `/` separators. */
|
||||
path: string
|
||||
/** Current fixture bytes decoded as UTF-8. */
|
||||
source: string
|
||||
/** Canonical packed fixture bytes. */
|
||||
canonical: string
|
||||
}
|
||||
|
||||
interface RecordLine {
|
||||
line: number
|
||||
text: string
|
||||
}
|
||||
|
||||
function recordLines(content: string): RecordLine[] {
|
||||
return content.split(/\r?\n/).flatMap((text, index) => (
|
||||
text.trim().length === 0 ? [] : [{ line: index + 1, text }]
|
||||
))
|
||||
}
|
||||
|
||||
function parseRecord(line: RecordLine, label: string): unknown {
|
||||
try {
|
||||
return JSON.parse(line.text) as unknown
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : String(error)
|
||||
throw new Error(`${label}:${line.line}: invalid JSON: ${detail}`, { cause: error })
|
||||
}
|
||||
}
|
||||
|
||||
function isSessionHeader(value: unknown): boolean {
|
||||
return value !== null && typeof value === 'object' && (value as { type?: unknown }).type === 'session'
|
||||
}
|
||||
|
||||
function decodeBody(lines: readonly RecordLine[], label: string): SessionEvent[] {
|
||||
return lines.flatMap((line) => {
|
||||
const record = parseRecord(line, label)
|
||||
try {
|
||||
return decodeStorageRecord(record)
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : String(error)
|
||||
throw new Error(`${label}:${line.line}: invalid session storage record: ${detail}`, { cause: error })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function renderFixture(headerLine: string, events: readonly SessionEvent[]): string {
|
||||
return [
|
||||
headerLine,
|
||||
...packChunkRuns(events).map(record => JSON.stringify(record)),
|
||||
'',
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Canonicalize one JSONL document when its first record is a session header.
|
||||
* The header line remains byte-identical; body records decode to logical events
|
||||
* and re-encode with {@link packChunkRuns}. Non-session JSONL returns undefined.
|
||||
*
|
||||
* @param content - JSONL source text.
|
||||
* @param label - path-like diagnostic label.
|
||||
* @returns Canonical text for a session fixture, otherwise undefined.
|
||||
*/
|
||||
export function canonicalSessionFixture(content: string, label = '<session-fixture>'): string | undefined {
|
||||
const lines = recordLines(content)
|
||||
const header = lines[0]
|
||||
if (header === undefined) return undefined
|
||||
|
||||
let headerValue: unknown
|
||||
try {
|
||||
headerValue = JSON.parse(header.text) as unknown
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
if (!isSessionHeader(headerValue)) return undefined
|
||||
|
||||
const events = decodeBody(lines.slice(1), label)
|
||||
const canonical = renderFixture(header.text, events)
|
||||
const canonicalLines = recordLines(canonical)
|
||||
const decoded = decodeBody(canonicalLines.slice(1), label)
|
||||
try {
|
||||
deepStrictEqual(decoded, events)
|
||||
} catch (error) {
|
||||
throw new Error(`${label}: packed rewrite changed the decoded event stream`, { cause: error })
|
||||
}
|
||||
if (renderFixture(header.text, decoded) !== canonical) {
|
||||
throw new Error(`${label}: packed rewrite is not idempotent`)
|
||||
}
|
||||
return canonical
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover tracked and unignored untracked JSONL files through Git.
|
||||
*
|
||||
* @param root - repository root.
|
||||
* @returns Stable repository-relative paths.
|
||||
*/
|
||||
function discoverJsonlFiles(root: string): string[] {
|
||||
return execFileSync(
|
||||
'git',
|
||||
['ls-files', '-z', '--cached', '--others', '--exclude-standard', '--', '*.jsonl'],
|
||||
{ cwd: root, encoding: 'utf8' },
|
||||
).split('\0')
|
||||
.filter(path => path.length > 0 && existsSync(resolve(root, path)))
|
||||
.sort()
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspect every repository JSONL whose first record is a session header.
|
||||
*
|
||||
* @param root - repository root.
|
||||
* @returns Session fixtures with current and canonical text.
|
||||
*/
|
||||
export function inspectSessionFixtureLayouts(root: string): SessionFixtureLayout[] {
|
||||
return discoverJsonlFiles(root).flatMap((path) => {
|
||||
const source = readFileSync(resolve(root, path), 'utf8')
|
||||
const canonical = canonicalSessionFixture(source, path)
|
||||
return canonical === undefined ? [] : [{ path, source, canonical }]
|
||||
})
|
||||
}
|
||||
File diff suppressed because one or more lines are too long.
@@ -34,6 +34,7 @@ const NON_SOURCE_DIRECTORIES = new Set([
|
||||
|
||||
/** Glob traversal exclusions corresponding to the non-source path predicate. */
|
||||
export const TRANSLATION_SCOPE_GLOB_EXCLUDES = [
|
||||
'.agents/notes/archived/**',
|
||||
'**/node_modules/**',
|
||||
'**/lib/**',
|
||||
'**/.pnpm-store/**',
|
||||
@@ -67,7 +68,8 @@ function isTranslationSourceExcluded(file: string): boolean {
|
||||
|
||||
/** Whether one discovered Markdown or sidecar path belongs to the bilingual source corpus. */
|
||||
export function isTranslationScopeFile(file: string): boolean {
|
||||
return !isTranslationSourceExcluded(file) && (README_ARTIFACT.test(file)
|
||||
return !file.startsWith('.agents/notes/archived/')
|
||||
&& !isTranslationSourceExcluded(file) && (README_ARTIFACT.test(file)
|
||||
|| file.startsWith('.agents/notes/')
|
||||
|| file.startsWith('docs/')
|
||||
|| file.startsWith('python/'))
|
||||
|
||||
@@ -609,6 +609,11 @@
|
||||
"symbol": "ToolExecutionMode",
|
||||
"source": "packages/core/tools/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/tools.md",
|
||||
"symbol": "CodeDispatchLog",
|
||||
"source": "packages/core/tools/src/index.ts"
|
||||
},
|
||||
{
|
||||
"doc": "docs/core-data-structures/tools.md",
|
||||
"symbol": "ToolRunContext",
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
/** Verify and append-seal the frozen Agent Note archive. */
|
||||
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { AGENT_NOTE_CLASSES, agentNoteRoot } from './agent-note-tree.ts'
|
||||
import {
|
||||
extendArchiveManifest,
|
||||
parseArchiveManifest,
|
||||
renderArchiveManifest,
|
||||
validateArchiveArtifacts,
|
||||
validateArchiveManifestExtension,
|
||||
type ArchiveManifest,
|
||||
} from './archived-agent-notes.ts'
|
||||
|
||||
const args = process.argv.slice(2)
|
||||
const writeMode = args.length === 1 && args[0] === '--write'
|
||||
if (args.length > 0 && !writeMode) {
|
||||
console.error('verify-archived-agent-notes: usage: tsx scripts/verify-archived-agent-notes.ts [--write]')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const archiveRoot = resolve(agentNoteRoot, 'archived')
|
||||
const manifestPath = resolve(archiveRoot, 'manifest.json')
|
||||
const repoRoot = resolve(agentNoteRoot, '../..')
|
||||
const manifestRepoPath = '.agents/notes/archived/manifest.json'
|
||||
const errors: string[] = []
|
||||
const allowedRootFiles = new Set(['AGENTS.md', 'manifest.json'])
|
||||
const kinds = new Set<string>()
|
||||
|
||||
if (!existsSync(resolve(archiveRoot, 'AGENTS.md'))) errors.push('archived/AGENTS.md is required')
|
||||
const artifacts = new Map<string, Buffer>()
|
||||
for (const entry of readdirSync(archiveRoot, { withFileTypes: true })) {
|
||||
if (entry.isFile()) {
|
||||
if (!allowedRootFiles.has(entry.name)) errors.push(`archived/${entry.name}: unexpected root file`)
|
||||
continue
|
||||
}
|
||||
if (!entry.isDirectory()) {
|
||||
errors.push(`archived/${entry.name}: only regular files and kind directories are allowed`)
|
||||
continue
|
||||
}
|
||||
if (!(AGENT_NOTE_CLASSES as readonly string[]).includes(entry.name)) {
|
||||
errors.push(`archived/${entry.name}/: unknown Agent Note kind`)
|
||||
continue
|
||||
}
|
||||
kinds.add(entry.name)
|
||||
for (const child of readdirSync(resolve(archiveRoot, entry.name), { withFileTypes: true })) {
|
||||
const rel = `${entry.name}/${child.name}`
|
||||
if (!child.isFile()) {
|
||||
errors.push(`${rel}: archived kind directories contain regular files only`)
|
||||
continue
|
||||
}
|
||||
artifacts.set(rel, readFileSync(resolve(archiveRoot, rel)))
|
||||
}
|
||||
}
|
||||
for (const kind of AGENT_NOTE_CLASSES) {
|
||||
if (!kinds.has(kind)) errors.push(`archived/${kind}/: required kind directory is missing`)
|
||||
}
|
||||
errors.push(...validateArchiveArtifacts(artifacts))
|
||||
|
||||
function runGit(args: string[]): string {
|
||||
const result = spawnSync('git', args, { cwd: repoRoot, encoding: 'utf8' })
|
||||
if (result.error !== undefined) throw result.error
|
||||
if (result.status !== 0) throw new Error(result.stderr.trim() || `git exited with status ${result.status}`)
|
||||
return result.stdout
|
||||
}
|
||||
|
||||
function readBaselineManifest(ref: string): ArchiveManifest {
|
||||
runGit(['cat-file', '-e', `${ref}^{commit}`])
|
||||
const manifestEntry = runGit(['ls-tree', '--name-only', ref, '--', manifestRepoPath]).trim()
|
||||
if (manifestEntry === '') return { version: 1, files: {} }
|
||||
return parseArchiveManifest(runGit(['show', `${ref}:${manifestRepoPath}`]))
|
||||
}
|
||||
|
||||
let manifest: ArchiveManifest = { version: 1, files: {} }
|
||||
if (existsSync(manifestPath)) {
|
||||
try {
|
||||
manifest = parseArchiveManifest(readFileSync(manifestPath, 'utf8'))
|
||||
} catch (error: unknown) {
|
||||
errors.push(`archived/manifest.json: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
} else if (!writeMode) {
|
||||
errors.push('archived/manifest.json is required; seal new artifacts with `pnpm run verify-archived-agent-notes --write`')
|
||||
}
|
||||
|
||||
// CI supplies its trusted pre-change commit; local writes compare with committed HEAD.
|
||||
const baselineRef = process.env.DSH_ARCHIVE_BASE_REF ?? 'HEAD'
|
||||
try {
|
||||
const baseline = readBaselineManifest(baselineRef)
|
||||
errors.push(...validateArchiveManifestExtension(baseline, manifest))
|
||||
} catch (error: unknown) {
|
||||
errors.push(`archived/manifest.json: cannot read baseline ${JSON.stringify(baselineRef)}: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
|
||||
const extended = extendArchiveManifest(manifest, artifacts)
|
||||
errors.push(...extended.errors)
|
||||
if (!writeMode) {
|
||||
for (const path of extended.added) errors.push(`${path}: archived artifact is not sealed in manifest.json`)
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error('verify-archived-agent-notes: archive contract violated:')
|
||||
for (const error of errors) console.error(` ${error}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (writeMode) {
|
||||
const rendered = renderArchiveManifest(extended.files)
|
||||
if (!existsSync(manifestPath) || readFileSync(manifestPath, 'utf8') !== rendered) {
|
||||
writeFileSync(manifestPath, rendered)
|
||||
}
|
||||
console.log(`verify-archived-agent-notes: sealed ${extended.added.length} new artifact(s); existing seals unchanged.`)
|
||||
} else {
|
||||
console.log(`verify-archived-agent-notes: ${artifacts.size} frozen artifact(s) checked across ${kinds.size} kind(s).`)
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import { existsSync, readFileSync } from 'node:fs'
|
||||
import { dirname, relative, resolve } from 'node:path'
|
||||
import type { Nodes } from 'mdast'
|
||||
import { parseMarkdown, visitMarkdown } from './markdown.ts'
|
||||
import { uniqueRepoFiles } from './repo-files.ts'
|
||||
import { isArchivedAgentNotePath, uniqueRepoFiles } from './repo-files.ts'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
|
||||
@@ -95,7 +95,8 @@ function findViolations(absPath: string): Violation[] {
|
||||
return out
|
||||
}
|
||||
|
||||
const files = uniqueRepoFiles(root, PATTERNS)
|
||||
// Archived notes remain valid link targets, but their historical outbound links are frozen.
|
||||
const files = uniqueRepoFiles(root, PATTERNS, isArchivedAgentNotePath)
|
||||
const all = files.flatMap(file => findViolations(file.abs))
|
||||
const checked = files.length
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { readFileSync } from 'node:fs'
|
||||
import { relative, resolve } from 'node:path'
|
||||
import type { Nodes } from 'mdast'
|
||||
import { parseMarkdown, visitMarkdown } from './markdown.ts'
|
||||
import { uniqueRepoFiles } from './repo-files.ts'
|
||||
import { isArchivedAgentNotePath, uniqueRepoFiles } from './repo-files.ts'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
|
||||
@@ -69,7 +69,7 @@ function findViolations(absPath: string): Violation[] {
|
||||
return out
|
||||
}
|
||||
|
||||
const files = uniqueRepoFiles(root, PATTERNS)
|
||||
const files = uniqueRepoFiles(root, PATTERNS, isArchivedAgentNotePath)
|
||||
const all = files.flatMap(file => findViolations(file.abs))
|
||||
const checked = files.length
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { gfmFromMarkdown } from 'mdast-util-gfm'
|
||||
import { gfm } from 'micromark-extension-gfm'
|
||||
import { JSDOM } from 'jsdom'
|
||||
import type { Nodes } from 'mdast'
|
||||
import { isArchivedAgentNotePath } from './repo-files.ts'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
|
||||
@@ -65,6 +66,7 @@ const seen = new Set<string>()
|
||||
let checkedFiles = 0
|
||||
for (const pattern of PATTERNS) {
|
||||
for (const match of globSync(pattern, { cwd: root })) {
|
||||
if (isArchivedAgentNotePath(match)) continue
|
||||
const real = realpathSync(resolve(root, match))
|
||||
if (seen.has(real)) continue
|
||||
seen.add(real)
|
||||
|
||||
@@ -7,7 +7,12 @@
|
||||
|
||||
import { existsSync, readdirSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { findReferenceViolations, uniqueRepoFiles, type ReferenceViolation as Violation } from './repo-files.ts'
|
||||
import {
|
||||
findReferenceViolations,
|
||||
isArchivedAgentNotePath,
|
||||
uniqueRepoFiles,
|
||||
type ReferenceViolation as Violation,
|
||||
} from './repo-files.ts'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
|
||||
@@ -26,7 +31,7 @@ const PATTERNS = [
|
||||
|
||||
/** Paths excluded from the scan: built output and vendored upstream source. */
|
||||
const isExcluded = (p: string): boolean =>
|
||||
p.includes('/lib/') || p.endsWith('.d.ts') || p.startsWith('vendor/')
|
||||
isArchivedAgentNotePath(p) || p.includes('/lib/') || p.endsWith('.d.ts') || p.startsWith('vendor/')
|
||||
|
||||
/**
|
||||
* Directory names of every real package, `packages/<group>/<pkg>`. A broken
|
||||
|
||||
@@ -55,6 +55,8 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'packages/client/ui-layout': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
|
||||
'packages/client/ui-sidebar': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
|
||||
'packages/client/ui-conversation': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
|
||||
'packages/client/ui-slash': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
|
||||
'packages/client/ui-command': { kind: 'indirect', reason: 'The dispatch paths trigger the host command.execute RPC; each command handler\'s host package owns any model-visible effect.' },
|
||||
'packages/client/ui-question': { kind: 'indirect', reason: 'The package mounts dsh-tool-ask-user; that tool owns the model-visible schema and answer rendering.' },
|
||||
'packages/client/ui-trajectory': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
|
||||
'packages/client/ui-workspace': { kind: 'none', reason: 'Browser-side UI plugin layer; registers no model surface.' },
|
||||
@@ -96,6 +98,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'packages/support/llm-mock-server': { kind: 'none', reason: 'The test server substitutes provider wire behavior without invoking a real model.' },
|
||||
'packages/support/llm-replay': { kind: 'none', reason: 'The keyless adapter invokes no provider model.' },
|
||||
'packages/tasks/tasks': { kind: 'indirect', reason: 'Producer and control-surface plugins own all model rendering over the task registry.' },
|
||||
'packages/tasks/tasks-local': { kind: 'indirect', reason: 'The registry backend delegates model rendering to producer plugins and dsh-tool-tasks.' },
|
||||
'packages/examples/acp-demo': { kind: 'indirect', reason: 'The app bundle delegates request composition to dsh-agent-spine-demo and dsh-acp.' },
|
||||
'packages/ui/app-boot': { kind: 'indirect', reason: 'Only the loaded plugin tree contributes model context.' },
|
||||
'packages/examples/jsonrpc-demo': { kind: 'indirect', reason: 'Only the externally configured plugin tree contributes model context.' },
|
||||
|
||||
@@ -12,6 +12,7 @@ import { globSync, readFileSync, existsSync } from 'node:fs'
|
||||
import { resolve, sep } from 'node:path'
|
||||
import ts from 'typescript'
|
||||
import { partitionPairedMarkdownDerivatives } from './paired-markdown-derivatives.ts'
|
||||
import { isArchivedAgentNotePath } from './repo-files.ts'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
|
||||
@@ -223,7 +224,10 @@ const keyOf = (x: { doc: string; symbol: string; projection?: 'public-api' }): s
|
||||
// as an orphan rather than silently skipped.
|
||||
const docSet = new Set<string>()
|
||||
for (const pattern of MARKDOWN_GLOBS) {
|
||||
for (const match of globSync(pattern, { cwd: root })) docSet.add(match.split(sep).join('/'))
|
||||
for (const match of globSync(pattern, { cwd: root })) {
|
||||
const normalized = match.split(sep).join('/')
|
||||
if (!isArchivedAgentNotePath(normalized)) docSet.add(normalized)
|
||||
}
|
||||
}
|
||||
const extractedBlocks: EquivBlock[] = [...docSet].sort().flatMap(extractEquivBlocks)
|
||||
const { primary: blocks, derivatives } = partitionPairedMarkdownDerivatives(
|
||||
|
||||
Reference in New Issue
Block a user