Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,37 @@
All notable changes to **basou** are recorded here. The project follows
[Semantic Versioning](https://semver.org/spec/v2.0.0.html) starting with v0.1.0.

## Unreleased

### Added

- The generated instruction files now localize their content from the
declaration, completing the i18n pass that v0.33.0 started for the views. A
repo's preset block (`basou project preset` / `derive` / `retrofit`) follows
the **repo's own** declared `language`: `ja` renders the Japanese content
byte-identical to previous releases; `en`, `en+ja`, or an undeclared language
render English. The workspace view's generated AGENTS.md block and the
anchor's create-only starter follow the **anchor entry's** language,
mirroring the views' "the workspace speaks the anchor's language" rule. All
instruction-file strings moved into the single `@basou/core` locale table
(`presetStrings` / `resolveRepoContentLanguage` /
`resolveAnchorContentLanguage`, all exported), shrinking the language-lint
E-5 allowlist from three files to that one module.

### Changed

- **Default instruction-file language is now English.** A repo that declares no
`language` (or declares `en` / `en+ja`) previously received Japanese preset
blocks, view blocks, and anchor starters; it now receives English. To keep
the Japanese content, declare `language: ja` on the repo entry (for its
preset block) or on the anchor entry (for the view block and anchor starter)
in `.basou/manifest.yaml`. Blocks committed by earlier releases surface as an
ordinary preset `update` plan on the next `basou project preset` / `derive`
run when their language resolution changed (`basou project check` inspects
wiring structure, not generated-region content, so it does not detect this).
Rendered prose is presentation, not contract (see
`docs/spec/compatibility.md`).

## 0.33.0 — 2026-07-13

### Added
Expand Down
13 changes: 12 additions & 1 deletion docs/spec/generated-markdown.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ All four share the same surface convention: an English document title, a
`> Generated at <iso>` line, and localized `##` section headings (see the view
language rule below).

## §10.0 View language ("the workspace speaks the anchor's language")
## §10.0 Generated-content language ("the workspace speaks the anchor's language")

The tool-generated chrome of all four views — section headings, labels, and
verdict prose — is localized per workspace, resolved from the manifest roster:
Expand All @@ -43,6 +43,17 @@ language is a project declaration (the manifest), not a caller preference.
The rendered prose itself (headings, wording) is presentation, not contract —
see `compatibility.md`. The templates below show the English chrome.

### Instruction files (preset block, view block, anchor starter)

The same declaration-driven rule extends to the GENERATED INSTRUCTION FILES
(`basou project preset` / `derive` / `retrofit` / anchor seeding), with one
refinement: a repo's preset block lives inside that repo's own instruction
file, so its content language follows the **repo's own** declared `language`
(`ja` renders Japanese; `en`, `en+ja`, or an undeclared language render
English — silently, like the views). Workspace-level instruction artifacts —
the view's generated AGENTS.md block and the anchor's create-only starter —
follow the **anchor entry's** language, mirroring the views' rule.

## §10.1 Living-artifact policy (handoff.md, decisions.md)

- **Generated**: basou produces them by reading events.jsonl.
Expand Down
23 changes: 14 additions & 9 deletions packages/cli/src/commands/project.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2303,8 +2303,8 @@ describe("basou project preset", () => {
const body = await readFile(canonicalPath("pub"), "utf8");
expect(body).toContain(GENERATED_START);
expect(body).toContain(GENERATED_END);
expect(body).toContain("ソース可視性: private");
expect(body).toContain("web(デプロイ) — 公開 / en+ja");
expect(body).toContain("Source visibility: private");
expect(body).toContain("web (deployed) — public / en+ja");
});

it("--apply updates only the marker region, preserving hand-authored content", async () => {
Expand Down Expand Up @@ -2417,7 +2417,7 @@ describe("basou project preset", () => {
viewApplied: false,
} satisfies ProjectPresetResult);
expect(out).toContain("Preset blocks to generate");
expect(out).toContain("ソース可視性: public");
expect(out).toContain("Source visibility: public");
expect(out).toContain("agents/pub/AGENTS.md");
});

Expand Down Expand Up @@ -3620,9 +3620,10 @@ describe("basou project derive", () => {
expect(gi).toContain("AGENTS.md");

// anchor seed: the planning master's own root AGENTS.md was created (a
// create-only starter, absent before the run).
// create-only starter, absent before the run; the anchor declares no
// language, so it renders the English starter).
const anchorDoc = await readFile(join(anchor(), "AGENTS.md"), "utf8");
expect(anchorDoc).toContain("planning master(anchor)");
expect(anchorDoc).toContain("planning master (anchor)");

// anchor spokes: with its AGENTS.md now seeded, the anchor's OWN CLAUDE.md /
// Copilot spokes are wired to it (self-style), like every other repo — never
Expand Down Expand Up @@ -4311,14 +4312,18 @@ describe("basou project derive: anchor seed (doRunProjectSeedAnchor)", () => {
await doRunProjectSeedAnchor({ apply: true }, { cwd: host() });
const content = await readFile(anchorDoc(), "utf8");
expect(content).toContain("# AGENTS.md (host)");
expect(content).toContain("**Acme の planning master(anchor)**");
// The anchor declares en+ja, which resolves to the English starter (one
// content language; en is the shared floor).
expect(content).toContain("**planning master (anchor) of Acme**");
// No manifest-derived roster snapshot table (it would drift in a frozen file).
expect(content).not.toContain("| repo | 可視性 | 言語 | 指示書 |");
expect(content).not.toContain("| repo | visibility | language | instructions |");
// Pointers exclude the anchor itself and include the view (the live-roster source).
expect(content).toContain("- pub/AGENTS.md");
expect(content).not.toContain("- host/AGENTS.md");
expect(content).toContain("- view/AGENTS.md(workspace view");
expect(content).toContain("最新の repo 構成(roster)はここを正とする");
expect(content).toContain("- view/AGENTS.md (the workspace view");
expect(content).toContain(
"the authoritative, up-to-date repo roster (the live roster) lives there",
);
expect(logs.join("\n")).toContain("Seeded the anchor's own `AGENTS.md`");
});

Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,11 @@ export {
} from "./lib/path-sanitizer.js";
export type { SourceRootScope } from "./lib/source-root-scope.js";
export { AGENT_INFRA_DIRS, classifyFilesBySourceRoot } from "./lib/source-root-scope.js";
export type { ViewLanguage, ViewStrings } from "./lib/view-strings.js";
export type { PresetStrings, ViewLanguage, ViewStrings } from "./lib/view-strings.js";
export {
presetStrings,
resolveAnchorContentLanguage,
resolveRepoContentLanguage,
resolveViewLanguage,
resolveViewLanguageFromPaths,
viewStrings,
Expand Down
86 changes: 85 additions & 1 deletion packages/core/src/lib/view-strings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest";
import type { Manifest } from "../schemas/manifest.schema.js";
import { type BasouPaths, ensureBasouDirectory } from "../storage/basou-dir.js";
import { createManifest, writeManifest } from "../storage/manifest.js";
import { resolveViewLanguage, resolveViewLanguageFromPaths, viewStrings } from "./view-strings.js";
import {
presetStrings,
resolveAnchorContentLanguage,
resolveRepoContentLanguage,
resolveViewLanguage,
resolveViewLanguageFromPaths,
viewStrings,
} from "./view-strings.js";

type RepoEntry = NonNullable<Manifest["repos"]>[number];

Expand Down Expand Up @@ -162,3 +169,80 @@ describe("viewStrings", () => {
}
});
});

describe("resolveRepoContentLanguage", () => {
it("resolves ja only from an explicit ja declaration", () => {
expect(resolveRepoContentLanguage("ja")).toBe("ja");
expect(resolveRepoContentLanguage("en")).toBe("en");
expect(resolveRepoContentLanguage("en+ja")).toBe("en");
expect(resolveRepoContentLanguage(undefined)).toBe("en");
});
});

describe("resolveAnchorContentLanguage", () => {
it("follows the anchor-flagged entry's language", () => {
expect(
resolveAnchorContentLanguage([{ anchor: true, language: "ja" }, { language: "en" }]),
).toBe("ja");
expect(
resolveAnchorContentLanguage([{ anchor: true, language: "en+ja" }, { language: "ja" }]),
).toBe("en");
});

it("defaults to en with no anchor entry or no anchor language", () => {
expect(resolveAnchorContentLanguage([])).toBe("en");
expect(resolveAnchorContentLanguage([{ language: "ja" }])).toBe("en");
expect(resolveAnchorContentLanguage([{ anchor: true }])).toBe("en");
});

it("first-wins when more than one entry carries the anchor flag (declared order)", () => {
expect(
resolveAnchorContentLanguage([
{ anchor: true, language: "ja" },
{ anchor: true, language: "en" },
]),
).toBe("ja");
expect(resolveAnchorContentLanguage([{ anchor: true }, { anchor: true, language: "ja" }])).toBe(
"en",
);
});
});

describe("presetStrings", () => {
// Same rationale as the viewStrings sweep: exercise every parameterized entry
// in BOTH languages so a table entry that throws or renders empty cannot ship,
// and the label maps cover every enum branch.
it("every parameterized entry renders a non-empty string in both languages", () => {
for (const lang of ["en", "ja"] as const) {
const t = presetStrings(lang);
const rendered: string[] = [
t.repoBlock.visibilityLabel("public"),
t.repoBlock.visibilityLabel("private"),
t.repoBlock.visibilityLabel("future-public"),
t.repoBlock.visibilityLabel(undefined),
t.repoBlock.sourceLanguageLabel("en"),
t.repoBlock.sourceLanguageLabel("ja"),
t.repoBlock.sourceLanguageLabel("en+ja"),
t.repoBlock.sourceLanguageLabel(undefined),
t.repoBlock.publishKindLabel("web"),
t.repoBlock.publishKindLabel("npm"),
t.repoBlock.publishVisibilityLabel("public"),
t.repoBlock.publishVisibilityLabel("private"),
t.repoBlock.publishVisibilityLabel("future-public"),
t.repoBlock.publishVisibilityLabel(undefined),
t.repoBlock.contentLanguageLabel("en+ja"),
t.repoBlock.contentLanguageLabel(undefined),
t.viewBlock.selfNote("ws"),
t.viewBlock.aggregates(0),
t.viewBlock.aggregates(3),
t.anchorStarter.identityLine("Acme"),
t.anchorStarter.viewPointerLine("acme-workspace"),
...t.anchorStarter.policyTodo,
];
for (const line of rendered) {
expect(typeof line).toBe("string");
expect(line.length).toBeGreaterThan(0);
}
}
});
});
Loading