docs: make bilingual pairing universal
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -1,216 +1,4 @@
|
||||
{
|
||||
"requiredClasses": [
|
||||
"non-readme",
|
||||
"readme"
|
||||
],
|
||||
"requiredSince": "2026-07-14",
|
||||
"required": [
|
||||
".agents/notes/README.md",
|
||||
".agents/notes/implemented/architecture/2026-06-11-content-block-vocabulary.md",
|
||||
".agents/notes/implemented/architecture/2026-06-11-dev-invariants-over-deep-readonly.md",
|
||||
".agents/notes/implemented/architecture/2026-06-11-event-sourced-sessions.md",
|
||||
".agents/notes/implemented/architecture/2026-06-11-microkernel-event-taxonomy.md",
|
||||
".agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.md",
|
||||
".agents/notes/implemented/architecture/2026-06-11-structured-error-taxonomy.md",
|
||||
".agents/notes/implemented/architecture/2026-06-11-tool-schemas-in-prompt-assembly.md",
|
||||
".agents/notes/implemented/architecture/2026-06-13-capability-seams.md",
|
||||
".agents/notes/implemented/architecture/2026-06-13-twin-llm-adapters.md",
|
||||
".agents/notes/implemented/architecture/2026-06-14-session-persistence.md",
|
||||
".agents/notes/implemented/architecture/2026-06-15-turn-enclosure-invariant.md",
|
||||
".agents/notes/implemented/architecture/2026-06-17-filesystem-capability-seam.md",
|
||||
".agents/notes/implemented/architecture/2026-06-18-agent-lifecycle-and-ownership-seams.md",
|
||||
".agents/notes/implemented/architecture/2026-06-18-session-surface.md",
|
||||
".agents/notes/implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md",
|
||||
".agents/notes/implemented/architecture/2026-06-20-branded-ids.md",
|
||||
".agents/notes/implemented/architecture/2026-06-20-extract-example-app-packages.md",
|
||||
".agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md",
|
||||
".agents/notes/implemented/architecture/2026-06-20-package-hierarchy.md",
|
||||
".agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md",
|
||||
".agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md",
|
||||
".agents/notes/implemented/architecture/2026-06-26-file-context-as-event-gate.md",
|
||||
".agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md",
|
||||
".agents/notes/implemented/architecture/2026-06-30-event-domain-semantics.md",
|
||||
".agents/notes/implemented/architecture/2026-07-02-fs-per-session-cwd.md",
|
||||
".agents/notes/implemented/architecture/2026-07-02-result-time-applied-hunk-diffs.md",
|
||||
".agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md",
|
||||
".agents/notes/implemented/architecture/2026-07-03-filesystem-directory-listing-seam.md",
|
||||
".agents/notes/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md",
|
||||
".agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md",
|
||||
".agents/notes/implemented/architecture/2026-07-05-subagent-provider-lifecycle-events.md",
|
||||
".agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md",
|
||||
".agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.md",
|
||||
".agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md",
|
||||
".agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md",
|
||||
".agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md",
|
||||
".agents/notes/implemented/architecture/2026-07-16-explicit-turn-cancellation.md",
|
||||
".agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md",
|
||||
".agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md",
|
||||
".agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md",
|
||||
".agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.md",
|
||||
".agents/notes/implemented/feature/2026-06-14-acp-multi-session.md",
|
||||
".agents/notes/implemented/feature/2026-06-15-code-mode.md",
|
||||
".agents/notes/implemented/feature/2026-06-17-filesystem-tool-schemas.md",
|
||||
".agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md",
|
||||
".agents/notes/implemented/feature/2026-06-21-subagent-capability-seam.md",
|
||||
".agents/notes/implemented/feature/2026-06-22-acp-subagent-backend.md",
|
||||
".agents/notes/implemented/feature/2026-06-25-ask-user-question.md",
|
||||
".agents/notes/implemented/feature/2026-06-29-todo-write-tool.md",
|
||||
".agents/notes/implemented/feature/2026-06-30-hook-bridges.md",
|
||||
".agents/notes/implemented/feature/2026-06-30-hook-protocol-lib.md",
|
||||
".agents/notes/implemented/feature/2026-06-30-interception-seams.md",
|
||||
".agents/notes/implemented/feature/2026-06-30-session-store-fork-api.md",
|
||||
".agents/notes/implemented/feature/2026-06-30-subagent-observe-enrich.md",
|
||||
".agents/notes/implemented/feature/2026-07-05-dynamic-workflows.md",
|
||||
".agents/notes/implemented/feature/2026-07-05-skill-system.md",
|
||||
".agents/notes/implemented/feature/2026-07-06-approval-seam.md",
|
||||
".agents/notes/implemented/feature/2026-07-06-explicit-tool-order.md",
|
||||
".agents/notes/implemented/feature/2026-07-06-sandbox.md",
|
||||
".agents/notes/implemented/feature/2026-07-07-mcp-client-plugin.md",
|
||||
".agents/notes/implemented/feature/2026-07-07-session-prefix.md",
|
||||
".agents/notes/implemented/feature/2026-07-08-repeat-tool-guard.md",
|
||||
".agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md",
|
||||
".agents/notes/implemented/feature/2026-07-10-session-query-service.md",
|
||||
".agents/notes/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md",
|
||||
".agents/notes/implemented/process/2026-06-11-doc-sync-enforcement.md",
|
||||
".agents/notes/implemented/process/2026-06-11-quality-gates.md",
|
||||
".agents/notes/implemented/process/2026-06-11-tsdown-over-dumble.md",
|
||||
".agents/notes/implemented/process/2026-06-11-vendor-cordis-as-source.md",
|
||||
".agents/notes/implemented/process/2026-06-16-pnpm-over-yarn.md",
|
||||
".agents/notes/implemented/process/2026-06-17-ts-build-config.md",
|
||||
".agents/notes/implemented/process/2026-06-18-markdown-cross-link-lint.md",
|
||||
".agents/notes/implemented/process/2026-06-20-agent-note-classification.md",
|
||||
".agents/notes/implemented/process/2026-06-20-core-data-structures-catalog.md",
|
||||
".agents/notes/implemented/process/2026-06-20-generated-cordis-catalog.md",
|
||||
".agents/notes/implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.md",
|
||||
".agents/notes/implemented/process/2026-07-02-tool-schema-catalog.md",
|
||||
".agents/notes/implemented/process/2026-07-03-documentation-graph-atlas.md",
|
||||
".agents/notes/implemented/process/2026-07-04-cordis-jsdoc-completeness-gate.md",
|
||||
".agents/notes/implemented/process/2026-07-04-doc-tiers-and-budgets.md",
|
||||
".agents/notes/implemented/process/2026-07-04-persistence-log-catalog.md",
|
||||
".agents/notes/implemented/process/2026-07-05-uniform-agent-note-format.md",
|
||||
".agents/notes/implemented/process/2026-07-06-export-surface-jsdoc-gate.md",
|
||||
".agents/notes/implemented/process/2026-07-06-generated-config-catalog.md",
|
||||
".agents/notes/implemented/process/2026-07-06-node-engine-floor.md",
|
||||
".agents/notes/implemented/process/2026-07-06-parallel-pre-push-gates.md",
|
||||
".agents/notes/implemented/process/2026-07-10-readme-known-limitations-gate.md",
|
||||
".agents/notes/implemented/process/2026-07-12-package-model-experience-contract.md",
|
||||
".agents/notes/implemented/process/2026-07-19-web-styling-system.md",
|
||||
".agents/notes/implemented/simplification/2026-06-19-drop-mutable-session-summary.md",
|
||||
".agents/notes/implemented/simplification/2026-06-20-collapse-trace-only-session-events.md",
|
||||
".agents/notes/implemented/simplification/2026-06-20-drop-unconsumed-llm-adapter-change-event.md",
|
||||
".agents/notes/implemented/simplification/2026-06-20-drop-unconsumed-llm-assembled-surfaces.md",
|
||||
".agents/notes/implemented/simplification/2026-06-20-prune-dead-seam-methods.md",
|
||||
".agents/notes/implemented/simplification/2026-06-20-public-agent-stop-surface.md",
|
||||
".agents/notes/implemented/simplification/2026-06-20-remove-agent-boundary-mirror-events.md",
|
||||
".agents/notes/implemented/simplification/2026-06-20-unify-agent-and-session-id.md",
|
||||
".agents/notes/implemented/simplification/2026-06-26-fsspec-style-fs-seam.md",
|
||||
".agents/notes/implemented/simplification/2026-07-02-remove-stream-chunk-mirror.md",
|
||||
".agents/notes/implemented/simplification/2026-07-04-drop-image-content-block.md",
|
||||
".agents/notes/implemented/simplification/2026-07-04-drop-inert-request-knobs.md",
|
||||
".agents/notes/implemented/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md",
|
||||
".agents/notes/implemented/simplification/2026-07-04-prune-producerless-vocabulary-variants.md",
|
||||
".agents/notes/implemented/simplification/2026-07-04-prune-write-only-fs-surface.md",
|
||||
".agents/notes/implemented/simplification/2026-07-04-remove-agent-steering-mirror.md",
|
||||
".agents/notes/implemented/simplification/2026-07-04-share-app-bin-boot-glue.md",
|
||||
".agents/notes/implemented/simplification/2026-07-04-tighten-hook-protocol-contract.md",
|
||||
".agents/notes/implemented/simplification/2026-07-04-trim-acp-bridge-unreachable-surface.md",
|
||||
".agents/notes/implemented/simplification/2026-07-12-drop-unconsumed-skill-provider-events.md",
|
||||
".agents/notes/implemented/simplification/2026-07-12-prune-unused-web-seam-fields.md",
|
||||
".agents/notes/implemented/simplification/2026-07-12-simplify-session-log-representation.md",
|
||||
".agents/notes/implemented/testing/2026-06-11-property-based-testing.md",
|
||||
".agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md",
|
||||
".agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.md",
|
||||
".agents/notes/implemented/testing/2026-06-20-remove-redundant-snapshot-log-expected-output.md",
|
||||
".agents/notes/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.md",
|
||||
".agents/notes/implemented/testing/2026-06-22-fork-snapshot-scenarios.md",
|
||||
".agents/notes/implemented/testing/2026-06-22-subagent-snapshot-replay.md",
|
||||
".agents/notes/implemented/testing/2026-07-04-hook-snapshot-matrix.md",
|
||||
".agents/notes/implemented/testing/2026-07-04-single-source-acp-replay-config.md",
|
||||
".agents/notes/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md",
|
||||
".agents/notes/implemented/testing/2026-07-08-shared-acp-snapshot-package.md",
|
||||
".agents/notes/proposed/architecture/2026-06-16-typed-event-schemas.md",
|
||||
".agents/notes/proposed/feature/2026-06-30-pre-tool-input-rewrite.md",
|
||||
".agents/notes/proposed/feature/2026-07-07-claude-code-and-codex-subagent-backends.md",
|
||||
".agents/notes/proposed/feature/2026-07-08-interactive-side-sessions.md",
|
||||
".agents/notes/implemented/feature/2026-07-10-sqlite-session-query-provider.md",
|
||||
".agents/notes/rejected/feature/2026-07-13-stream-workflow-progress-through-tool-calls.md",
|
||||
".agents/notes/proposed/process/2026-06-11-api-extractor-reports.md",
|
||||
".agents/notes/proposed/process/2026-06-11-architectural-conformance.md",
|
||||
".agents/notes/proposed/process/2026-06-11-supply-chain-and-vendor-drift.md",
|
||||
".agents/notes/proposed/process/2026-06-20-discover-package-inventory.md",
|
||||
".agents/notes/proposed/simplification/2026-07-04-prune-dead-core-spine-surface.md",
|
||||
".agents/notes/proposed/testing/2026-06-11-deterministic-and-stress-testing.md",
|
||||
".agents/notes/proposed/testing/2026-06-11-mutation-testing.md",
|
||||
".agents/notes/rejected/architecture/2026-06-11-immutable-public-surfaces.md",
|
||||
".agents/notes/rejected/architecture/2026-06-20-providerless-example-base.md",
|
||||
".agents/notes/rejected/process/2026-07-04-generate-agent-note-index-tables.md",
|
||||
".agents/notes/rejected/simplification/2026-06-20-assembled-assistant-messages-only.md",
|
||||
".agents/notes/rejected/simplification/2026-06-20-drop-acp-session-load.md",
|
||||
".agents/notes/rejected/simplification/2026-06-20-drop-acp-terminal-meta.md",
|
||||
".agents/notes/rejected/simplification/2026-06-20-drop-bash-output-spill-files.md",
|
||||
".agents/notes/rejected/simplification/2026-06-20-drop-durable-step-boundaries.md",
|
||||
".agents/notes/rejected/simplification/2026-06-20-drop-unused-session-lineage.md",
|
||||
".agents/notes/rejected/simplification/2026-06-20-fold-session-persistence-interface.md",
|
||||
".agents/notes/rejected/simplification/2026-06-20-generic-tool-rendering.md",
|
||||
".agents/notes/rejected/simplification/2026-06-20-retire-mid-turn-steering.md",
|
||||
".agents/notes/rejected/simplification/2026-06-20-single-session-acp-bridge.md",
|
||||
".agents/notes/rejected/simplification/2026-06-20-truncate-interrupted-turns.md",
|
||||
".agents/notes/rejected/simplification/2026-07-04-prune-unimplemented-subagent-vocabulary.md",
|
||||
".agents/notes/rejected/simplification/2026-07-12-collapse-workflow-to-foreground-core.md",
|
||||
".agents/notes/rejected/simplification/2026-07-12-prune-unused-skill-registry-surface.md",
|
||||
"README.md",
|
||||
"docs/architecture.md",
|
||||
"docs/cookbook/adding-a-package.md",
|
||||
"docs/cookbook/adding-a-tool.md",
|
||||
"docs/cookbook/adding-a-vendored-package.md",
|
||||
"docs/cookbook/adding-an-llm-adapter.md",
|
||||
"docs/cookbook/extension-cookbook.md",
|
||||
"docs/cookbook/responding-to-pr-review-on-a-stack.md",
|
||||
"docs/cordis-primer.md",
|
||||
"docs/core-data-structures/approval.md",
|
||||
"docs/core-data-structures/bash.md",
|
||||
"docs/core-data-structures/code-runtime.md",
|
||||
"docs/core-data-structures/compaction.md",
|
||||
"docs/core-data-structures/core.md",
|
||||
"docs/core-data-structures/filesystem.md",
|
||||
"docs/core-data-structures/llm-streaming.md",
|
||||
"docs/core-data-structures/persistence.md",
|
||||
"docs/core-data-structures/sandbox.md",
|
||||
"docs/core-data-structures/scope.md",
|
||||
"docs/core-data-structures/session-query.md",
|
||||
"docs/core-data-structures/session.md",
|
||||
"docs/core-data-structures/skills.md",
|
||||
"docs/core-data-structures/subagent.md",
|
||||
"docs/core-data-structures/system-prompt.md",
|
||||
"docs/core-data-structures/tools.md",
|
||||
"docs/core-data-structures/user-interaction.md",
|
||||
"docs/core-data-structures/web.md",
|
||||
"docs/core-data-structures/workflow.md",
|
||||
"docs/defensive-patterns.md",
|
||||
"docs/development.md",
|
||||
"docs/glossary.md",
|
||||
"docs/i18n/README.md",
|
||||
"docs/i18n/translation-rules.md",
|
||||
"docs/postmortem/0001-acp-default-export-drops-inject.md",
|
||||
"docs/postmortem/0002-js-expression-disabled-filesystem-tools.md",
|
||||
"docs/postmortem/README.md",
|
||||
"docs/testing.md",
|
||||
"docs/user/develop/basic/config.md",
|
||||
"docs/user/develop/basic/index.md",
|
||||
"docs/user/develop/basic/tool.md",
|
||||
"docs/user/develop/framework/events.md",
|
||||
"docs/user/develop/framework/index.md",
|
||||
"docs/user/develop/framework/service.md",
|
||||
"docs/user/develop/practice/index.md",
|
||||
"docs/user/develop/practice/llm-adapter.md",
|
||||
"docs/user/guide/config.md",
|
||||
"docs/user/guide/index.md",
|
||||
"docs/user/guide/quickstart.md",
|
||||
"docs/user/index.md",
|
||||
"python/README.md",
|
||||
"python/sdk-runtime/README.md",
|
||||
"python/sdk/README.md"
|
||||
],
|
||||
"excluded": [
|
||||
".agents/notes/AGENTS.md",
|
||||
".agents/notes/implemented/AGENTS.md",
|
||||
@@ -228,7 +16,6 @@
|
||||
"docs/module-graph.md",
|
||||
"docs/persistence-catalog.md",
|
||||
"docs/tool-catalog.md",
|
||||
"docs/tool-execution-pipeline.md",
|
||||
"python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/"
|
||||
"docs/tool-execution-pipeline.md"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
/** Regression tests for the bilingual cutoff and structural signature. */
|
||||
/** Regression tests for the bilingual corpus scope and structural signature. */
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
datedDocumentDate,
|
||||
isIsoDate,
|
||||
isTranslationScopeFile,
|
||||
parseTranslationMarkdown,
|
||||
parseTranslationPairingManifest,
|
||||
requiresPairByDate,
|
||||
requiresTranslationPair,
|
||||
translationDocumentClass,
|
||||
translationStructureDiff,
|
||||
translationStructureSignature,
|
||||
} from './translation-pairing.ts'
|
||||
@@ -19,78 +14,30 @@ function signature(markdown: string) {
|
||||
}
|
||||
|
||||
describe('translation pairing manifest', () => {
|
||||
it('accepts a real ISO cutoff and string-array fields', () => {
|
||||
it('accepts an exclusions-only manifest', () => {
|
||||
expect(parseTranslationPairingManifest(JSON.stringify({
|
||||
requiredSince: '2026-07-14',
|
||||
required: ['README.md'],
|
||||
requiredClasses: ['non-readme'],
|
||||
excluded: ['docs/generated/'],
|
||||
}))).toEqual({
|
||||
requiredSince: '2026-07-14',
|
||||
required: ['README.md'],
|
||||
requiredClasses: ['non-readme'],
|
||||
excluded: ['docs/generated/'],
|
||||
})
|
||||
})
|
||||
|
||||
it.each(['2026-7-14', '2026-02-29', '2026-13-01', 'not-a-date'])('rejects invalid cutoff %s', (cutoff) => {
|
||||
expect(isIsoDate(cutoff)).toBe(false)
|
||||
it.each([
|
||||
['required', ['packages/README.md']],
|
||||
['requiredClasses', ['readme']],
|
||||
['requiredSince', '2026-07-14'],
|
||||
] as const)('rejects obsolete policy field %s instead of accepting an inert requirement', (field, value) => {
|
||||
expect(() => parseTranslationPairingManifest(JSON.stringify({
|
||||
requiredSince: cutoff,
|
||||
required: [],
|
||||
requiredClasses: [],
|
||||
excluded: [],
|
||||
}))).toThrow('requiredSince must be a valid YYYY-MM-DD date')
|
||||
[field]: value,
|
||||
}))).toThrow(`unsupported field(s): ${field}; every in-scope document is required`)
|
||||
})
|
||||
|
||||
it('rejects non-string manifest arrays', () => {
|
||||
it('rejects a missing or non-string exclusion list', () => {
|
||||
expect(() => parseTranslationPairingManifest('{}')).toThrow('excluded must be an array of strings')
|
||||
expect(() => parseTranslationPairingManifest(JSON.stringify({
|
||||
requiredSince: '2026-07-14',
|
||||
required: [42],
|
||||
requiredClasses: [],
|
||||
excluded: [],
|
||||
}))).toThrow('required must be an array of strings')
|
||||
})
|
||||
|
||||
it('rejects unknown and duplicate document classes', () => {
|
||||
const manifest = (requiredClasses: string[]) => JSON.stringify({
|
||||
requiredSince: '2026-07-14',
|
||||
required: [],
|
||||
requiredClasses,
|
||||
excluded: [],
|
||||
})
|
||||
expect(() => parseTranslationPairingManifest(manifest(['guide']))).toThrow('requiredClasses must contain only')
|
||||
expect(() => parseTranslationPairingManifest(manifest(['readme', 'readme']))).toThrow('requiredClasses must not contain duplicates')
|
||||
})
|
||||
})
|
||||
|
||||
describe('document-class pairing frontier', () => {
|
||||
const manifest = parseTranslationPairingManifest(JSON.stringify({
|
||||
requiredSince: '2026-07-14',
|
||||
required: ['docs/legacy/README.md'],
|
||||
requiredClasses: ['non-readme'],
|
||||
excluded: [],
|
||||
}))
|
||||
|
||||
it('classifies README basenames case-insensitively', () => {
|
||||
expect(translationDocumentClass('packages/core/README.md')).toBe('readme')
|
||||
expect(translationDocumentClass('missions/readme.md')).toBe('readme')
|
||||
expect(translationDocumentClass('docs/readme-guide.md')).toBe('non-readme')
|
||||
})
|
||||
|
||||
it('requires every non-README while retaining explicit README entries', () => {
|
||||
expect(requiresTranslationPair('docs/guide.md', manifest)).toBe(true)
|
||||
expect(requiresTranslationPair('docs/legacy/README.md', manifest)).toBe(true)
|
||||
expect(requiresTranslationPair('docs/new/README.md', manifest)).toBe(false)
|
||||
})
|
||||
|
||||
it('requires both document classes after the README frontier closes', () => {
|
||||
const closed = parseTranslationPairingManifest(JSON.stringify({
|
||||
...manifest,
|
||||
requiredClasses: ['non-readme', 'readme'],
|
||||
}))
|
||||
expect(requiresTranslationPair('docs/guide.md', closed)).toBe(true)
|
||||
expect(requiresTranslationPair('future/subtree/README.md', closed)).toBe(true)
|
||||
excluded: [42],
|
||||
}))).toThrow('excluded must be an array of strings')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -124,22 +71,6 @@ describe('translation scope discovery', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('date-based pairing frontier', () => {
|
||||
const cutoff = '2026-07-14'
|
||||
|
||||
it('enforces the cutoff day and every later day, but not the preceding day', () => {
|
||||
expect(requiresPairByDate('.agents/notes/2026-07-13-before.md', cutoff)).toBe(false)
|
||||
expect(requiresPairByDate('.agents/notes/2026-07-14-at-cutoff.md', cutoff)).toBe(true)
|
||||
expect(requiresPairByDate('.agents/notes/2026-07-15-after.md', cutoff)).toBe(true)
|
||||
})
|
||||
|
||||
it('matches only a date at the start of the basename', () => {
|
||||
expect(datedDocumentDate('.agents/notes/2026-07-14-proposal.md')).toBe('2026-07-14')
|
||||
expect(datedDocumentDate('docs/release-notes-2026-07-14-alpha.md')).toBeUndefined()
|
||||
expect(requiresPairByDate('docs/release-notes-2026-07-14-alpha.md', cutoff)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('translation structural signature', () => {
|
||||
it('accepts matching list kinds, starts, and item counts', () => {
|
||||
const source = signature('3. One\n4. Two\n\n- A\n- B\n')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Pure parsing and structural helpers for the bilingual-document pairing
|
||||
* gate. Kept separate from the CLI so cutoff and signature behavior can be
|
||||
* regression-tested without reading or mutating the repository tree.
|
||||
* gate. Kept separate from the CLI so corpus discovery and signature behavior
|
||||
* can be regression-tested without reading or mutating the repository tree.
|
||||
*/
|
||||
|
||||
import { fromMarkdown } from 'mdast-util-from-markdown'
|
||||
@@ -11,21 +11,10 @@ import type { Nodes } from 'mdast'
|
||||
|
||||
/** Validated shape of `scripts/translation-pairing.manifest.json`. */
|
||||
export interface TranslationPairingManifest {
|
||||
required: string[]
|
||||
/** Document classes whose complete in-scope population must be paired. */
|
||||
requiredClasses: TranslationDocumentClass[]
|
||||
/** Source documents exempt from pairing because they are generated, instructional, or bilingual by construction. */
|
||||
excluded: string[]
|
||||
/** Date-named documents on or after this day must merge bilingual. */
|
||||
requiredSince: string
|
||||
}
|
||||
|
||||
/** Stable classes used to close one translation rollout without enumerating files. */
|
||||
export type TranslationDocumentClass = 'readme' | 'non-readme'
|
||||
|
||||
const TRANSLATION_DOCUMENT_CLASSES: TranslationDocumentClass[] = ['readme', 'non-readme']
|
||||
|
||||
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/
|
||||
const DATED_DOCUMENT = /(?:^|\/)(\d{4}-\d{2}-\d{2})-[^/]*\.md$/
|
||||
const README_ARTIFACT = /(?:^|\/)readme(?:\.md|\.zh\.md|\.i18n\.yaml)$/i
|
||||
const NON_SOURCE_DIRECTORIES = new Set([
|
||||
'node_modules',
|
||||
@@ -84,39 +73,19 @@ export function isTranslationScopeFile(file: string): boolean {
|
||||
|| file.startsWith('python/'))
|
||||
}
|
||||
|
||||
/** Whether a string names one real calendar day in canonical ISO form. */
|
||||
export function isIsoDate(value: string): boolean {
|
||||
if (!ISO_DATE.test(value)) return false
|
||||
const date = new Date(`${value}T00:00:00.000Z`)
|
||||
return !Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === value
|
||||
}
|
||||
|
||||
/** Read one manifest string-array field or fail before enforcement starts. */
|
||||
function stringArrayField(record: Record<string, unknown>, field: 'required' | 'excluded'): string[] {
|
||||
const value = record[field]
|
||||
/** Read the manifest exclusion list or fail before enforcement starts. */
|
||||
function excludedField(record: Record<string, unknown>): string[] {
|
||||
const value = record.excluded
|
||||
if (!Array.isArray(value)) {
|
||||
throw new Error(`translation-pairing.manifest.json: ${field} must be an array of strings`)
|
||||
throw new Error('translation-pairing.manifest.json: excluded must be an array of strings')
|
||||
}
|
||||
const entries: unknown[] = value
|
||||
if (!entries.every((entry): entry is string => typeof entry === 'string')) {
|
||||
throw new Error(`translation-pairing.manifest.json: ${field} must be an array of strings`)
|
||||
throw new Error('translation-pairing.manifest.json: excluded must be an array of strings')
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
/** Read and validate the manifest's closed document-class set. */
|
||||
function requiredClassesField(record: Record<string, unknown>): TranslationDocumentClass[] {
|
||||
const value = record.requiredClasses
|
||||
if (!Array.isArray(value) || !value.every((entry): entry is TranslationDocumentClass =>
|
||||
typeof entry === 'string' && TRANSLATION_DOCUMENT_CLASSES.includes(entry as TranslationDocumentClass))) {
|
||||
throw new Error('translation-pairing.manifest.json: requiredClasses must contain only "readme" and "non-readme"')
|
||||
}
|
||||
if (new Set(value).size !== value.length) {
|
||||
throw new Error('translation-pairing.manifest.json: requiredClasses must not contain duplicates')
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
/** Parse and validate the checked-in bilingual manifest. */
|
||||
export function parseTranslationPairingManifest(content: string): TranslationPairingManifest {
|
||||
const value: unknown = JSON.parse(content)
|
||||
@@ -124,39 +93,11 @@ export function parseTranslationPairingManifest(content: string): TranslationPai
|
||||
throw new Error('translation-pairing.manifest.json: expected an object')
|
||||
}
|
||||
const record = value as Record<string, unknown>
|
||||
const requiredSince = record.requiredSince
|
||||
if (typeof requiredSince !== 'string' || !isIsoDate(requiredSince)) {
|
||||
throw new Error(`translation-pairing.manifest.json: requiredSince must be a valid YYYY-MM-DD date; got ${JSON.stringify(requiredSince)}`)
|
||||
const unsupported = Object.keys(record).filter(field => field !== 'excluded')
|
||||
if (unsupported.length > 0) {
|
||||
throw new Error(`translation-pairing.manifest.json: unsupported field(s): ${unsupported.join(', ')}; every in-scope document is required`)
|
||||
}
|
||||
return {
|
||||
required: stringArrayField(record, 'required'),
|
||||
requiredClasses: requiredClassesField(record),
|
||||
excluded: stringArrayField(record, 'excluded'),
|
||||
requiredSince,
|
||||
}
|
||||
}
|
||||
|
||||
/** Classify a Markdown source by whether its basename is README, case-insensitively. */
|
||||
export function translationDocumentClass(file: string): TranslationDocumentClass {
|
||||
return /(?:^|\/)readme\.md$/i.test(file) ? 'readme' : 'non-readme'
|
||||
}
|
||||
|
||||
/** Whether the manifest requires this in-scope source to have a complete pair. */
|
||||
export function requiresTranslationPair(file: string, manifest: TranslationPairingManifest): boolean {
|
||||
return manifest.required.includes(file)
|
||||
|| manifest.requiredClasses.includes(translationDocumentClass(file))
|
||||
|| requiresPairByDate(file, manifest.requiredSince)
|
||||
}
|
||||
|
||||
/** Return the leading date of a `yyyy-mm-dd-*.md` basename, if present. */
|
||||
export function datedDocumentDate(file: string): string | undefined {
|
||||
return DATED_DOCUMENT.exec(file)?.[1]
|
||||
}
|
||||
|
||||
/** Whether a date-named document falls on or after the pairing cutoff. */
|
||||
export function requiresPairByDate(file: string, requiredSince: string): boolean {
|
||||
const date = datedDocumentDate(file)
|
||||
return date !== undefined && date >= requiredSince
|
||||
return { excluded: excludedField(record) }
|
||||
}
|
||||
|
||||
/** The structural surface compared between the two sides of a pair. */
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
/**
|
||||
* Enforce complete English/Chinese pairs, matching structure, and recorded git
|
||||
* blob hashes under the bilingual manifest. Required files and date-named docs
|
||||
* at or after `requiredSince`, plus every source in a required document class,
|
||||
* must be paired; excluded docs may have neither a counterpart nor sidecar.
|
||||
* blob hashes for every in-scope document. The manifest contains only explicit
|
||||
* exclusions, which may have neither a counterpart nor a sidecar.
|
||||
* `--list` reports state and `--write` records both sides after human review.
|
||||
* Translation quality remains a review responsibility.
|
||||
* See `docs/i18n/README.md` for the owning contract.
|
||||
@@ -16,9 +15,7 @@ import {
|
||||
parseTranslationMarkdown,
|
||||
parseTranslationPairingManifest,
|
||||
isTranslationScopeFile,
|
||||
requiresTranslationPair,
|
||||
TRANSLATION_SCOPE_GLOB_EXCLUDES,
|
||||
translationDocumentClass,
|
||||
translationStructureDiff,
|
||||
translationStructureSignature,
|
||||
} from './translation-pairing.ts'
|
||||
@@ -119,26 +116,17 @@ if (writeMode) {
|
||||
const errors: string[] = []
|
||||
const state = new Map<string, 'ok' | 'out-of-sync' | 'missing'>()
|
||||
|
||||
// 1. Explicit manifest entries name existing source documents.
|
||||
for (const req of manifest.required) {
|
||||
if (!existsSync(join(root, req))) {
|
||||
errors.push(`${req}: listed in translation-pairing.manifest.json \`required\` but the file does not exist`)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Every source selected explicitly, by document class, or by the dated-document
|
||||
// cutoff merges bilingual. Class enforcement closes a rollout for future files too.
|
||||
// 1. Every discovered, non-excluded source merges bilingual.
|
||||
for (const source of sources) {
|
||||
if (isExcluded(source)) continue
|
||||
if (!requiresTranslationPair(source, manifest)) continue
|
||||
const { zh } = pairPaths(source)
|
||||
if (!existsSync(join(root, zh))) {
|
||||
errors.push(`${source}: required to merge bilingual as a ${translationDocumentClass(source)} document (docs/i18n/README.md); add the counterpart and record the pair`)
|
||||
errors.push(`${source}: in-scope documentation must merge bilingual (docs/i18n/README.md); add the counterpart and record the pair`)
|
||||
state.set(source, 'missing')
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Every pair that exists at all is complete and consistent. Anchor on the
|
||||
// 2. Every pair that exists at all is complete and consistent. Anchor on the
|
||||
// union of .zh.md files and .i18n.yaml records so a half-deleted pair is
|
||||
// caught from either remnant.
|
||||
const pairAnchors = new Set<string>()
|
||||
@@ -198,7 +186,7 @@ for (const source of [...pairAnchors].sort()) {
|
||||
if (!state.has(source)) state.set(source, 'ok')
|
||||
}
|
||||
|
||||
// Complete the state map for --list: any in-scope, non-excluded document with no pair yet is backlog.
|
||||
// Complete the state map for --list: any in-scope, non-excluded document with no pair is missing.
|
||||
for (const source of sources) {
|
||||
if (!isExcluded(source) && !state.has(source)) state.set(source, 'missing')
|
||||
}
|
||||
@@ -207,9 +195,7 @@ if (listMode) {
|
||||
const order = { 'out-of-sync': 0, missing: 1, ok: 2 } as const
|
||||
const rows = [...state.entries()].sort((a, b) => order[a[1]] - order[b[1]] || a[0].localeCompare(b[0]))
|
||||
for (const [file, status] of rows) {
|
||||
const required = requiresTranslationPair(file, manifest)
|
||||
const tag = required ? ` (required ${translationDocumentClass(file)})` : ' (backlog)'
|
||||
console.log(`${status.padEnd(11)} ${file}${status === 'missing' ? tag : ''}`)
|
||||
console.log(`${status.padEnd(11)} ${file}${status === 'missing' ? ' (required)' : ''}`)
|
||||
}
|
||||
const counts = { 'ok': 0, 'out-of-sync': 0, 'missing': 0 }
|
||||
for (const status of state.values()) counts[status]++
|
||||
@@ -218,7 +204,7 @@ if (listMode) {
|
||||
}
|
||||
|
||||
if (errors.length === 0) {
|
||||
console.log(`verify-translation-pairing: ${pairAnchors.size} pair(s) checked against ${manifest.required.length} explicit requirements and required classes [${manifest.requiredClasses.join(', ')}], all consistent.`)
|
||||
console.log(`verify-translation-pairing: ${pairAnchors.size} pair(s) checked across all in-scope documentation, all consistent.`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user