mirror of
https://gh-proxy.org/https://github.com/klinsmaya/workdsh.git
synced 2026-11-09 09:57:05 +08:00
Merge pull request #361 from anywhere-labs/architecture/desktop-architecture-gates
ci: add bilingual and dependency direction gates
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# Bilingual-pair consistency record: the git blob hash of each side as of the last
|
||||
# confirmed-consistent state. Both languages carry equal authority. Update both files
|
||||
# and re-record their hashes after editing either side.
|
||||
2026-08-16-desktop-logging.md: 8e1df3e52fcc58688fac242a8688b1d18816edad
|
||||
2026-08-16-desktop-logging.zh.md: 97346bb31546427cf9e98e9039c6ad6ba33663a5
|
||||
2026-08-16-desktop-logging.md: f97287a0b0ed06a08b31aeb0da303c3a23ea5310
|
||||
2026-08-16-desktop-logging.zh.md: f1a3b3717c5c59b151768ae4415abeb8b2f1f904
|
||||
|
||||
@@ -41,6 +41,10 @@ jobs:
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node --test scripts/bilingual-docs.test.mjs
|
||||
node scripts/verify-bilingual-docs.mjs
|
||||
node --test scripts/market-dependency-direction.test.mjs
|
||||
node scripts/verify-market-dependency-direction.mjs
|
||||
if [[ "$EVENT_NAME" == "workflow_dispatch" || -z "$BASE_SHA" || "$BASE_SHA" =~ ^0+$ ]]; then
|
||||
product=true
|
||||
else
|
||||
|
||||
+5
-1
@@ -58,7 +58,11 @@
|
||||
"build": "yarn workspace dsh-community-market build && yarn workspace dsh-plugin-desktop build",
|
||||
"typecheck": "yarn workspace dsh-plugin-desktop typecheck && yarn workspace dsh-community-market typecheck",
|
||||
"test": "yarn workspace dsh-plugin-desktop test && yarn workspace dsh-community-market test",
|
||||
"check:layout": "node scripts/verify-layout.mjs",
|
||||
"test:bilingual-docs": "node --test scripts/bilingual-docs.test.mjs",
|
||||
"check:bilingual-docs": "yarn test:bilingual-docs && node scripts/verify-bilingual-docs.mjs",
|
||||
"test:architecture-gates": "node --test scripts/market-dependency-direction.test.mjs",
|
||||
"check:architecture": "yarn test:architecture-gates && node scripts/verify-market-dependency-direction.mjs",
|
||||
"check:layout": "yarn check:bilingual-docs && yarn check:architecture && node scripts/verify-layout.mjs",
|
||||
"check": "yarn check:layout && yarn workspace dsh-community-fabric check && yarn workspace dsh-community-market check && yarn workspace dsh-plugin-desktop check",
|
||||
"dev": "yarn workspace dsh-community-market build && yarn workspace dsh-plugin-desktop dev",
|
||||
"start": "yarn workspace dsh-plugin-desktop start",
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { basename, dirname, isAbsolute, relative, resolve } from 'node:path'
|
||||
|
||||
const HASH_PATTERN = /^[0-9a-f]{40}$/u
|
||||
const DOCUMENT_PATTERN = /\.mdx?$/u
|
||||
|
||||
function normalizePath(path) {
|
||||
return path.replaceAll('\\', '/')
|
||||
}
|
||||
|
||||
function runGit(root, args) {
|
||||
return execFileSync('git', args, {
|
||||
cwd: root,
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
})
|
||||
}
|
||||
|
||||
export function parseBilingualRecord(recordPath, content) {
|
||||
const entries = []
|
||||
for (const [index, line] of content.split(/\r?\n/u).entries()) {
|
||||
const trimmed = line.trim()
|
||||
if (trimmed.length === 0 || trimmed.startsWith('#')) continue
|
||||
const match = /^([^:]+):\s*([0-9a-f]{40})$/u.exec(trimmed)
|
||||
if (match === null) {
|
||||
throw new Error(`${recordPath}:${String(index + 1)} is not a filename and Git blob hash`)
|
||||
}
|
||||
const filename = match[1]?.trim() ?? ''
|
||||
const hash = match[2] ?? ''
|
||||
if (filename !== basename(filename) || isAbsolute(filename) || !DOCUMENT_PATTERN.test(filename)) {
|
||||
throw new Error(`${recordPath}:${String(index + 1)} must name one Markdown file beside the record`)
|
||||
}
|
||||
if (!HASH_PATTERN.test(hash)) {
|
||||
throw new Error(`${recordPath}:${String(index + 1)} has an invalid Git blob hash`)
|
||||
}
|
||||
entries.push({ filename, hash })
|
||||
}
|
||||
if (entries.length !== 2) {
|
||||
throw new Error(`${recordPath} must contain exactly two bilingual document records`)
|
||||
}
|
||||
if (entries[0]?.filename === entries[1]?.filename) {
|
||||
throw new Error(`${recordPath} must reference two distinct bilingual documents`)
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
export function verifyBilingualRecords({ root, recordPaths, readText, hashDocument }) {
|
||||
let documentCount = 0
|
||||
const violations = []
|
||||
for (const recordPath of recordPaths) {
|
||||
const normalizedRecord = normalizePath(recordPath)
|
||||
const recordDirectory = dirname(resolve(root, normalizedRecord))
|
||||
let entries
|
||||
try {
|
||||
entries = parseBilingualRecord(normalizedRecord, readText(resolve(root, normalizedRecord)))
|
||||
} catch (cause) {
|
||||
violations.push(cause instanceof Error ? cause.message : String(cause))
|
||||
continue
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const documentPath = resolve(recordDirectory, entry.filename)
|
||||
const relativeDocument = normalizePath(relative(root, documentPath))
|
||||
if (relativeDocument.startsWith('../') || isAbsolute(relativeDocument)) {
|
||||
violations.push(`${normalizedRecord} references a document outside the repository`)
|
||||
continue
|
||||
}
|
||||
try {
|
||||
readText(documentPath)
|
||||
const actual = hashDocument(relativeDocument)
|
||||
if (actual !== entry.hash) {
|
||||
violations.push(`${normalizedRecord} is stale for ${relativeDocument}: expected ${actual}, recorded ${entry.hash}`)
|
||||
}
|
||||
} catch (cause) {
|
||||
violations.push(`${normalizedRecord} cannot verify ${relativeDocument}: ${cause instanceof Error ? cause.message : String(cause)}`)
|
||||
}
|
||||
documentCount += 1
|
||||
}
|
||||
}
|
||||
if (violations.length > 0) {
|
||||
throw new Error(`bilingual documentation gate failed:\n- ${violations.join('\n- ')}`)
|
||||
}
|
||||
return { recordCount: recordPaths.length, documentCount }
|
||||
}
|
||||
|
||||
export function verifyTrackedBilingualRecords(root) {
|
||||
const recordPaths = runGit(root, ['ls-files', '-z', '--', '*.i18n.yaml'])
|
||||
.split('\0')
|
||||
.filter(Boolean)
|
||||
.map(normalizePath)
|
||||
return verifyBilingualRecords({
|
||||
root,
|
||||
recordPaths,
|
||||
readText: path => readFileSync(path, 'utf8'),
|
||||
hashDocument: relativePath => runGit(root, [
|
||||
'hash-object',
|
||||
`--path=${relativePath}`,
|
||||
'--',
|
||||
relativePath,
|
||||
]).trim(),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { test } from 'node:test'
|
||||
import { resolve } from 'node:path'
|
||||
import {
|
||||
parseBilingualRecord,
|
||||
verifyBilingualRecords,
|
||||
} from './bilingual-docs.mjs'
|
||||
|
||||
test('parses exactly two Markdown blob records', () => {
|
||||
assert.deepEqual(parseBilingualRecord('docs/topic.i18n.yaml', [
|
||||
'# Both languages carry equal authority.',
|
||||
`topic.md: ${'a'.repeat(40)}`,
|
||||
`topic.zh.md: ${'b'.repeat(40)}`,
|
||||
'',
|
||||
].join('\n')), [
|
||||
{ filename: 'topic.md', hash: 'a'.repeat(40) },
|
||||
{ filename: 'topic.zh.md', hash: 'b'.repeat(40) },
|
||||
])
|
||||
})
|
||||
|
||||
test('rejects malformed, incomplete, and escaping bilingual records', () => {
|
||||
assert.throws(
|
||||
() => parseBilingualRecord('docs/topic.i18n.yaml', `topic.md: ${'a'.repeat(40)}\n`),
|
||||
/exactly two/u,
|
||||
)
|
||||
assert.throws(
|
||||
() => parseBilingualRecord('docs/topic.i18n.yaml', `topic.md = ${'a'.repeat(40)}\ntopic.zh.md: ${'b'.repeat(40)}\n`),
|
||||
/not a filename/u,
|
||||
)
|
||||
assert.throws(
|
||||
() => parseBilingualRecord('docs/topic.i18n.yaml', `../topic.md: ${'a'.repeat(40)}\ntopic.zh.md: ${'b'.repeat(40)}\n`),
|
||||
/beside the record/u,
|
||||
)
|
||||
})
|
||||
|
||||
test('verifies every recorded document hash', () => {
|
||||
const root = resolve('repository')
|
||||
const recordPath = 'docs/topic.i18n.yaml'
|
||||
const record = `topic.md: ${'a'.repeat(40)}\ntopic.zh.md: ${'b'.repeat(40)}\n`
|
||||
const reads = new Map([
|
||||
[resolve(root, recordPath), record],
|
||||
[resolve(root, 'docs/topic.md'), 'English'],
|
||||
[resolve(root, 'docs/topic.zh.md'), 'Chinese'],
|
||||
])
|
||||
const result = verifyBilingualRecords({
|
||||
root,
|
||||
recordPaths: [recordPath],
|
||||
readText: path => {
|
||||
const value = reads.get(path)
|
||||
if (value === undefined) throw new Error(`missing ${path}`)
|
||||
return value
|
||||
},
|
||||
hashDocument: path => path.endsWith('.zh.md') ? 'b'.repeat(40) : 'a'.repeat(40),
|
||||
})
|
||||
assert.deepEqual(result, { recordCount: 1, documentCount: 2 })
|
||||
assert.throws(() => verifyBilingualRecords({
|
||||
root,
|
||||
recordPaths: [recordPath],
|
||||
readText: path => reads.get(path) ?? '',
|
||||
hashDocument: () => 'c'.repeat(40),
|
||||
}), /is stale/u)
|
||||
})
|
||||
|
||||
test('reports malformed records and missing documents in one failure', () => {
|
||||
const root = resolve('repository')
|
||||
const records = new Map([
|
||||
[resolve(root, 'docs/broken.i18n.yaml'), 'not a record\n'],
|
||||
[resolve(root, 'docs/missing.i18n.yaml'), [
|
||||
`missing.md: ${'a'.repeat(40)}`,
|
||||
`missing.zh.md: ${'b'.repeat(40)}`,
|
||||
'',
|
||||
].join('\n')],
|
||||
])
|
||||
|
||||
assert.throws(() => verifyBilingualRecords({
|
||||
root,
|
||||
recordPaths: ['docs/broken.i18n.yaml', 'docs/missing.i18n.yaml'],
|
||||
readText: path => {
|
||||
const value = records.get(path)
|
||||
if (value === undefined) throw new Error(`missing ${path}`)
|
||||
return value
|
||||
},
|
||||
hashDocument: () => 'a'.repeat(40),
|
||||
}), /broken\.i18n\.yaml:1[\s\S]*cannot verify docs\/missing\.md[\s\S]*cannot verify docs\/missing\.zh\.md/u)
|
||||
})
|
||||
@@ -0,0 +1,41 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const IMPORT_SPECIFIER = /(?:\bfrom\s*|\bimport\s*\(\s*|\brequire\s*\(\s*|\bimport\s*)['"]([^'"]+)['"]/gu
|
||||
const DESKTOP_MODULE = /(?:^|\/)dsh-plugin-desktop(?:\/|$)/u
|
||||
|
||||
function normalizePath(path) {
|
||||
return path.replaceAll('\\', '/')
|
||||
}
|
||||
|
||||
export function verifyMarketDependencyDirection(files) {
|
||||
const violations = []
|
||||
for (const [path, source] of files) {
|
||||
for (const match of source.matchAll(IMPORT_SPECIFIER)) {
|
||||
const specifier = match[1] ?? ''
|
||||
if (DESKTOP_MODULE.test(normalizePath(specifier))) {
|
||||
violations.push(normalizePath(path))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (violations.length > 0) {
|
||||
throw new Error(`Market source must not import Desktop implementation: ${violations.join(', ')}`)
|
||||
}
|
||||
return { fileCount: files.length }
|
||||
}
|
||||
|
||||
export function verifyTrackedMarketDependencyDirection(root) {
|
||||
const paths = execFileSync('git', ['ls-files', '-z', '--', 'dsh-community-market/src'], {
|
||||
cwd: root,
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
})
|
||||
.split('\0')
|
||||
.filter(path => /\.[cm]?[jt]sx?$/u.test(path))
|
||||
.map(normalizePath)
|
||||
return verifyMarketDependencyDirection(
|
||||
paths.map(path => [path, readFileSync(resolve(root, path), 'utf8')]),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { test } from 'node:test'
|
||||
import { verifyMarketDependencyDirection } from './market-dependency-direction.mjs'
|
||||
|
||||
test('allows product-name data in Market source', () => {
|
||||
assert.deepEqual(verifyMarketDependencyDirection([
|
||||
['dsh-community-market/src/install.ts', "const blocked = new Set(['dsh-plugin-desktop'])\n"],
|
||||
]), { fileCount: 1 })
|
||||
})
|
||||
|
||||
test('rejects Market imports of Desktop implementation', () => {
|
||||
assert.throws(() => verifyMarketDependencyDirection([
|
||||
['dsh-community-market/src/index.ts', "import type { DesktopRuntime } from 'dsh-plugin-desktop/runtime'\n"],
|
||||
]), /must not import Desktop implementation/u)
|
||||
assert.throws(() => verifyMarketDependencyDirection([
|
||||
['dsh-community-market/src/index.ts', "const desktop = await import('dsh-plugin-desktop')\n"],
|
||||
]), /must not import Desktop implementation/u)
|
||||
assert.throws(() => verifyMarketDependencyDirection([
|
||||
['dsh-community-market/src/index.ts', "export { runtime } from '../../dsh-plugin-desktop/src/runtime.js'\n"],
|
||||
]), /must not import Desktop implementation/u)
|
||||
})
|
||||
@@ -0,0 +1,9 @@
|
||||
import { resolve } from 'node:path'
|
||||
import { verifyTrackedBilingualRecords } from './bilingual-docs.mjs'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const result = verifyTrackedBilingualRecords(root)
|
||||
|
||||
process.stdout.write(
|
||||
`verify-bilingual-docs: ${String(result.recordCount)} records and ${String(result.documentCount)} documents are consistent\n`,
|
||||
)
|
||||
@@ -1,6 +1,6 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { existsSync, lstatSync, readFileSync, readlinkSync } from 'node:fs'
|
||||
import { basename, resolve } from 'node:path'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const readJson = path => JSON.parse(readFileSync(resolve(root, path), 'utf8'))
|
||||
@@ -17,10 +17,6 @@ const plugin = readJson('dsh-plugin-desktop/package.json')
|
||||
const fabric = readJson('dsh-community-fabric/package.json')
|
||||
const market = readJson('dsh-community-market/package.json')
|
||||
const upstreamPackage = readJson('deepseek-harness/package.json')
|
||||
const noteDirectory = '.agents/notes/implemented/process'
|
||||
const noteName = '2026-08-15-pinned-upstream-and-isolated-yarn-workspace'
|
||||
const notePaths = [`${noteDirectory}/${noteName}.md`, `${noteDirectory}/${noteName}.zh.md`]
|
||||
const noteRecordPath = `${noteDirectory}/${noteName}.i18n.yaml`
|
||||
|
||||
if (workspace.packageManager !== '[email protected]') {
|
||||
fail('the product workspace must pin [email protected]')
|
||||
@@ -113,24 +109,4 @@ for (const name of Object.keys(plugin.dependencies).filter(name => name === '@de
|
||||
}
|
||||
}
|
||||
|
||||
const noteRecord = readFileSync(resolve(root, noteRecordPath), 'utf8')
|
||||
for (const notePath of notePaths) {
|
||||
// Hash the committed blob, not the working tree: checkout line endings
|
||||
// differ per host, while HEAD:<path> is identical everywhere.
|
||||
const expected = run('git', ['rev-parse', `HEAD:${notePath}`])
|
||||
const recordLine = `${basename(notePath)}: ${expected}`
|
||||
if (!noteRecord.split(/\r?\n/u).includes(recordLine)) {
|
||||
fail(`${noteRecordPath} is stale for ${notePath}`)
|
||||
}
|
||||
}
|
||||
|
||||
const readmeRecord = readFileSync(resolve(root, 'README.i18n.yaml'), 'utf8')
|
||||
for (const readmeName of ['README.md', 'README.en.md']) {
|
||||
const expected = run('git', ['rev-parse', `HEAD:${readmeName}`])
|
||||
const recordLine = `${readmeName}: ${expected}`
|
||||
if (!readmeRecord.split(/\r?\n/u).includes(recordLine)) {
|
||||
fail(`README.i18n.yaml is stale for ${readmeName}`)
|
||||
}
|
||||
}
|
||||
|
||||
process.stdout.write(`verify-layout: Yarn workspace and upstream ${upstream.commit.slice(0, 10)} are consistent\n`)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { resolve } from 'node:path'
|
||||
import { verifyTrackedMarketDependencyDirection } from './market-dependency-direction.mjs'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const result = verifyTrackedMarketDependencyDirection(root)
|
||||
|
||||
process.stdout.write(
|
||||
`verify-market-dependency-direction: ${String(result.fileCount)} Market source files are independent of Desktop implementation\n`,
|
||||
)
|
||||
Reference in New Issue
Block a user