All 176 files in the four accumulating directories now use a zero-padded 4-digit counter prefix that reflects creation order (`NNNN-slug.md`). The counter is assigned per directory in strict git-log creation order; ties broken alphabetically by original name. The old `YYYY-MM-DD-` prefix on docs/specs/ and docs/plans/ files is dropped — the date is recoverable from git log and the counter carries the ordering. A file's counter is stable for the life of the file: never reassigned, never reused, never compacted. Deleted files retire their counter; subsequent files do not fill the gap. This is the property that lets cross-references stay literal — refs use the full filename including the counter (`design/contracts/0007-honesty-rule.md`) so they grep cleanly and resolve directly without a glob step. 313 cross-references updated across .md/.rs/.toml/.c/.json files (test pins, include_str! paths, design-INDEX entries, baseline notes, runtime C comments, inter-contract markdown links incl. bare basename and `../models/foo.md` forms). CLAUDE.md gets a new "File-naming convention" section spelling out the rule and rationale. skills/brainstorm/SKILL.md and skills/planner/SKILL.md updated so new spec/plan creation produces counter-prefixed names from the start. The full test suite (cargo test --workspace) passes.
28 KiB
pd.1 — core API split (load_modules_with + build_workspace) — Implementation Plan
Parent spec:
docs/specs/0028-prelude-decouple.mdFor agentic workers: REQUIRED SUB-SKILL: use
skills/implementto run this plan. Steps use- [ ]checkboxes for tracking.
Goal: Introduce two new public fns in crates/ailang-core/src/workspace.rs
(load_modules_with + build_workspace), thread implicit_imports: &[&str]
through validate_canonical_type_names, and replace the hardcoded "prelude"
fallbacks in check_class_ref and check_type_con_name with the parameterised
walk. load_workspace_with survives pd.1 as a thin compatibility shim composed
of the new fns + the still-in-place prelude inject; pd.2 rewires surface to use
the new API directly and removes the shim.
Architecture: core's existing load_workspace_with<F> is the single block
that today does DFS + reserved-name + inject + 3-stage validation. pd.1 carves
out load_modules_with (DFS only) and build_workspace (validation +
registry) as new public fns; the surviving load_workspace_with becomes a
3-line composition load_modules_with → reserved-name + inject → build_workspace.
core's src/ literal-"prelude" strings drop from 8 to 4 (the inject step's
two and WorkspaceLoadError::ReservedModuleName's string-arg site stay
because the shim still holds them; the four diagnostic-fallback occurrences
on lines 1084 / 1086 / 1148 / 1150 are removed by the threading). Surface
and downstream tests are unchanged in pd.1.
Tech Stack: ailang-core (workspace.rs only). No surface, codegen, check,
or CLI changes.
Files this plan creates or modifies:
- Modify:
crates/ailang-core/src/workspace.rs:413-528— extract DFS pre-amble intoload_modules_with(lines 462-491), extract validation pipeline intobuild_workspace(lines 506-528), refitload_workspace_withas a 3-line shim.load_prelude(425-428) andPRELUDE_JSON(417-419) stay in pd.1 (still consumed by the shim's inject step); they leave in pd.2. - Modify:
crates/ailang-core/src/workspace.rs:925-1097— addimplicit_imports: &[&str]parameter tovalidate_canonical_type_names(signature at line 938-940); thread tocheck_class_ref(line 1041-1046) andcheck_type_con_name(line 1101-1106); replace literal-"prelude"fallback blocks at lines 1084-1091 and 1148-1155 with the iter form. - Modify:
crates/ailang-core/src/workspace.rs:437-439— retirepub fn load_workspace. Zero production callers; the rustdoc example at lines 22-31 referencing it gets rewritten to use the shim. - Modify:
crates/ailang-core/src/workspace.rs:1-32— module rustdoc. Update the example block (lines 22-31) and the "single entry point" wording (line 8) to reflect the shim shape. - Modify:
crates/ailang-core/src/workspace.rs#[cfg(test)] mod testsblock (1583-2749) — direct callers ofvalidate_canonical_type_namesadd&["prelude"]as second arg (~12 tests). Direct callers ofload_workspace,load_workspace_with,build_registryare unchanged (the shim + the unchanged validators preserve the public signature for the latter two; the legacyload_workspacewrapper has no in-tree test caller per recon). - Test:
crates/ailang-core/src/workspace.rsmod tests — three new#[test]fns to pin the new API: (a)load_modules_withreturns modules WITHOUT the prelude (proves it is loader-only), (b)build_workspaceaccepts a pre-assembled modules-map and runs the three-stage validation, (c) the shim compositionload_workspace_withproduces an output byte-identical to today (regression pin against the refactor).
Task 1: Add pub fn load_modules_with (DFS extraction)
Files:
-
Modify:
crates/ailang-core/src/workspace.rs— add new fn between currentload_workspace_withand the prelude-related helpers (anywhere in lines 430-530 is fine; suggest just aboveload_workspace_with). -
Step 1: Write the failing test in the
#[cfg(test)] mod testsblock at the end ofworkspace.rs.
#[test]
fn load_modules_with_returns_modules_without_prelude() {
let dir = tempfile::tempdir().unwrap();
write_module(dir.path(), "main", &[]);
let entry = dir.path().join("main.ail.json");
let (entry_name, root_dir, modules) =
load_modules_with(&entry, load_one).expect("load");
assert_eq!(entry_name, "main");
assert_eq!(root_dir, dir.path().to_path_buf());
assert!(
!modules.contains_key("prelude"),
"load_modules_with must not auto-inject prelude (that is build_workspace's caller's job)"
);
assert!(modules.contains_key("main"));
}
- Step 2: Run test to verify it fails
Run: cargo test -p ailang-core load_modules_with_returns_modules_without_prelude
Expected: FAIL with "cannot find function load_modules_with in this scope"
- Step 3: Add the new fn
Place this above load_workspace_with (anywhere in the 430-530 range works;
keep PRELUDE_JSON and load_prelude adjacent to load_workspace_with for
locality):
/// pd.1: extract the DFS pre-amble of `load_workspace_with` into a public
/// loader-only fn. Returns `(entry_name, root_dir, modules)` with NO prelude
/// injected and NO validation run. The caller is responsible for injecting
/// any implicit modules (e.g. prelude) and then handing the result to
/// `build_workspace` for validation + registry construction.
///
/// Surface composes this with a prelude-injection step in pd.2; pd.1 keeps
/// the shim `load_workspace_with` callable so surface is unchanged.
pub fn load_modules_with<F>(
entry_path: &Path,
loader: F,
) -> Result<(String, PathBuf, BTreeMap<String, Module>), WorkspaceLoadError>
where
F: Fn(&Path) -> Result<Module, WorkspaceLoadError> + Copy,
{
let entry_path = entry_path.to_path_buf();
let root_dir = entry_path
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."));
let entry_module = loader(&entry_path)?;
let expected_entry_name = module_name_from_path(&entry_path);
if entry_module.name != expected_entry_name {
return Err(WorkspaceLoadError::ModuleNameMismatch {
name_in_file: entry_module.name.clone(),
name_from_path: expected_entry_name,
});
}
let entry_name = entry_module.name.clone();
let mut modules: BTreeMap<String, Module> = BTreeMap::new();
let mut visiting: Vec<String> = Vec::new();
let mut visiting_set: HashSet<String> = HashSet::new();
visit(
entry_module,
&root_dir,
&mut modules,
&mut visiting,
&mut visiting_set,
loader,
)?;
Ok((entry_name, root_dir, modules))
}
- Step 4: Run test to verify it passes
Run: cargo test -p ailang-core load_modules_with_returns_modules_without_prelude
Expected: PASS.
- Step 5: Run full core test suite to verify no regression
Run: cargo test -p ailang-core
Expected: all existing tests pass (the new fn is additive; nothing else
calls it yet).
Task 2: Add pub fn build_workspace (validation pipeline extraction)
Files:
-
Modify:
crates/ailang-core/src/workspace.rs— add new fn next toload_modules_with. Theimplicit_imports: &[&str]parameter is declared onbuild_workspaceAND threaded into the call tovalidate_canonical_type_names(which gets the parameter in Task 3). -
Step 1: Write the failing test
Place in the #[cfg(test)] mod tests block:
#[test]
fn build_workspace_accepts_assembled_modules_and_runs_validation() {
let dir = tempfile::tempdir().unwrap();
write_module(dir.path(), "main", &[]);
let entry = dir.path().join("main.ail.json");
let (entry_name, root_dir, mut modules) =
load_modules_with(&entry, load_one).expect("load");
// Caller-side prelude inject (mirrors what surface will do in pd.2).
let prelude = load_prelude();
modules.insert("prelude".to_string(), prelude);
let ws = build_workspace(entry_name, root_dir, modules, &["prelude"])
.expect("build");
assert_eq!(ws.entry, "main");
assert!(ws.modules.contains_key("main"));
assert!(ws.modules.contains_key("prelude"));
}
- Step 2: Run test to verify it fails
Run: cargo test -p ailang-core build_workspace_accepts_assembled_modules_and_runs_validation
Expected: FAIL with "cannot find function build_workspace in this scope".
- Step 3: Add the new fn
Place adjacent to load_modules_with:
/// pd.1: extract the validation + registry-construction tail of
/// `load_workspace_with` into a public fn that operates on a pre-assembled
/// modules map. The caller (typically `surface::load_workspace`, post-pd.2)
/// is responsible for injecting any implicit modules (e.g. prelude) into
/// `modules` BEFORE calling this fn, AND for naming those implicit modules
/// in `implicit_imports` so the diagnostic helpers (`check_class_ref`,
/// `check_type_con_name`) include them as fallback candidates.
///
/// Runs the three-stage pipeline:
/// 1. `validate_canonical_type_names` (with `implicit_imports`)
/// 2. `validate_classdefs` (no `implicit_imports` — does not consume it)
/// 3. `build_registry`
pub fn build_workspace(
entry_name: String,
root_dir: PathBuf,
modules: BTreeMap<String, Module>,
implicit_imports: &[&str],
) -> Result<Workspace, WorkspaceLoadError> {
validate_canonical_type_names(&modules, implicit_imports)?;
validate_classdefs(&modules)?;
let registry = build_registry(&modules)?;
Ok(Workspace {
entry: entry_name,
modules,
root_dir,
registry,
})
}
- Step 4: Run test to verify it fails (with a different error)
Run: cargo test -p ailang-core build_workspace_accepts_assembled_modules_and_runs_validation
Expected: FAIL with "this function takes 1 argument but 2 arguments were
supplied" (because validate_canonical_type_names still has the old
single-arg signature — Task 3 adds the parameter). The test compiles
through build_workspace but fails at the inner call site.
If the failure mode is different (e.g. trait bound), inspect — the
compile-error message must point at validate_canonical_type_names's
arity, not at anything else.
- Step 5: Verify Task 2 is wired (deferring green to Task 3)
This task ends RED-on-purpose. Task 3 lands the parameter on
validate_canonical_type_names and turns Task 2's test green at the
end of Task 3.
Task 3: Thread implicit_imports: &[&str] through validate_canonical_type_names, check_class_ref, check_type_con_name
Files:
-
Modify:
crates/ailang-core/src/workspace.rs:925-1097— three signatures- ~6 internal call sites in
validate_canonical_type_names+ two fallback-iter rewrites in the diagnostic helpers.
- ~6 internal call sites in
-
Step 1: Write the failing test for the new threading semantics
Place in #[cfg(test)] mod tests. The test exercises the parameterisation
end-to-end: with implicit_imports = &["prelude"] the fallback fires
(today's behaviour); with implicit_imports = &[] the same fixture
returns BareCrossModuleClassRef with NO prelude.X candidate.
#[test]
fn implicit_imports_arg_drives_prelude_fallback_in_diagnostics() {
// Build a 2-module workspace where module "main" has a bare class-ref
// "Eq" that resolves only via prelude.
let mut modules = BTreeMap::new();
modules.insert("main".to_string(), module_with_bare_classref("main", "Eq"));
modules.insert(
"prelude".to_string(),
module_with_class_def("prelude", "Eq", "a"),
);
// With implicit_imports = &["prelude"] — prelude.Eq appears in candidates.
let err_with = validate_canonical_type_names(&modules, &["prelude"]).unwrap_err();
match err_with {
WorkspaceLoadError::BareCrossModuleClassRef { ref candidates, .. } => {
assert!(
candidates.iter().any(|c| c == "prelude.Eq"),
"expected prelude.Eq in candidates, got {:?}", candidates
);
}
other => panic!("expected BareCrossModuleClassRef, got {other:?}"),
}
// With implicit_imports = &[] — prelude.Eq does NOT appear.
let err_without = validate_canonical_type_names(&modules, &[]).unwrap_err();
match err_without {
WorkspaceLoadError::BareCrossModuleClassRef { ref candidates, .. } => {
assert!(
!candidates.iter().any(|c| c == "prelude.Eq"),
"expected NO prelude.Eq in candidates, got {:?}", candidates
);
}
other => panic!("expected BareCrossModuleClassRef, got {other:?}"),
}
}
The helpers module_with_bare_classref and module_with_class_def may
need to be added if no in-tree analogue exists. Keep them in the same
mod tests block. Suggest:
fn module_with_bare_classref(name: &str, class_ref: &str) -> Module {
Module {
name: name.to_string(),
imports: vec![],
defs: vec![Def::Instance(InstanceDef {
class: class_ref.to_string(),
ty: Type::Con { name: format!("{name}.Foo"), args: vec![] },
methods: vec![],
doc: None,
})],
}
}
fn module_with_class_def(name: &str, class_name: &str, param: &str) -> Module {
Module {
name: name.to_string(),
imports: vec![],
defs: vec![Def::Class(ClassDef {
name: class_name.to_string(),
param: param.to_string(),
superclasses: vec![],
methods: vec![],
doc: None,
})],
}
}
If similar helpers already exist in the test block, use them and skip the helper additions.
- Step 2: Run test to verify it fails
Run: cargo test -p ailang-core implicit_imports_arg_drives_prelude_fallback_in_diagnostics
Expected: FAIL with "this function takes 1 argument but 2 arguments were
supplied" — validate_canonical_type_names still has the old single-arg
signature.
- Step 3: Add the parameter to
validate_canonical_type_names
Edit the signature at line 938-940. Old:
pub(crate) fn validate_canonical_type_names(
modules: &BTreeMap<String, Module>,
) -> Result<(), WorkspaceLoadError> {
New:
pub(crate) fn validate_canonical_type_names(
modules: &BTreeMap<String, Module>,
implicit_imports: &[&str],
) -> Result<(), WorkspaceLoadError> {
- Step 4: Add the parameter to
check_class_refandcheck_type_con_name
check_class_ref (signature at lines 1041-1046). Old:
fn check_class_ref(
class_ref: &str,
owning_module: &str,
local_classes: &BTreeMap<String, BTreeSet<String>>,
import_names: &[&str],
) -> Result<(), WorkspaceLoadError> {
New:
fn check_class_ref(
class_ref: &str,
owning_module: &str,
local_classes: &BTreeMap<String, BTreeSet<String>>,
import_names: &[&str],
implicit_imports: &[&str],
) -> Result<(), WorkspaceLoadError> {
check_type_con_name (signature at lines 1101-1106). Old:
fn check_type_con_name(
name: &str,
owning_module: &str,
local_types: &BTreeMap<String, BTreeSet<String>>,
import_names: &[&str],
) -> Result<(), WorkspaceLoadError> {
New:
fn check_type_con_name(
name: &str,
owning_module: &str,
local_types: &BTreeMap<String, BTreeSet<String>>,
import_names: &[&str],
implicit_imports: &[&str],
) -> Result<(), WorkspaceLoadError> {
- Step 5: Replace the literal-
"prelude"fallback incheck_class_ref
At lines 1084-1091, replace:
if !import_names.contains(&"prelude")
&& local_classes
.get("prelude")
.map(|s| s.contains(class_ref))
.unwrap_or(false)
{
candidates.push(format!("prelude.{class_ref}"));
}
with:
for implicit in implicit_imports {
if import_names.contains(implicit) {
continue; // already added above
}
if local_classes
.get(*implicit)
.map(|s| s.contains(class_ref))
.unwrap_or(false)
{
candidates.push(format!("{implicit}.{class_ref}"));
}
}
- Step 6: Replace the literal-
"prelude"fallback incheck_type_con_name
At lines 1148-1155, replace:
if !import_names.contains(&"prelude")
&& local_types
.get("prelude")
.map(|s| s.contains(name))
.unwrap_or(false)
{
candidates.push(format!("prelude.{name}"));
}
with:
for implicit in implicit_imports {
if import_names.contains(implicit) {
continue; // already added above
}
if local_types
.get(*implicit)
.map(|s| s.contains(name))
.unwrap_or(false)
{
candidates.push(format!("{implicit}.{name}"));
}
}
- Step 7: Update the call sites of
check_class_refandcheck_type_con_nameinsidevalidate_canonical_type_names
Six internal call sites per recon (lines 981, 988, 998, 1002, 1007, 1015).
Each gets implicit_imports forwarded as the new last arg. The exact
shape of each call site varies — the change is mechanical: add
implicit_imports as the trailing positional arg.
Example pattern (the actual lines may differ slightly; replicate for all six call sites):
Old:
check_type_con_name(name, owning, local_types, &import_names)?;
New:
check_type_con_name(name, owning, local_types, &import_names, implicit_imports)?;
- Step 8: Run the new test from Step 1
Run: cargo test -p ailang-core implicit_imports_arg_drives_prelude_fallback_in_diagnostics
Expected: PASS — both the with-prelude and without-prelude branches.
- Step 9: Run Task 2's test
Run: cargo test -p ailang-core build_workspace_accepts_assembled_modules_and_runs_validation
Expected: PASS — the inner validate_canonical_type_names(&modules, implicit_imports) call now compiles.
- Step 10: Migrate existing direct callers of
validate_canonical_type_names
Recon listed ~12 tests in the mod tests block that call
validate_canonical_type_names(&modules) directly (lines 1981, 2000,
2036, 2076, 2108, 2143, 2179, 2584, 2625, 2678, 2720; plus any siblings
in 2393-2483 not enumerated). Each gets a mechanical rewrite:
Old:
validate_canonical_type_names(&modules)
New:
validate_canonical_type_names(&modules, &["prelude"])
The &["prelude"] matches today's hardcoded behaviour; assertions on
prelude.X candidate strings continue to pass.
- Step 11: Run the full core test suite
Run: cargo test -p ailang-core
Expected: all tests pass. If a missed call site breaks compilation, find it,
add &["prelude"], re-run.
Task 4: Refit load_workspace_with as a thin compatibility shim
Files:
-
Modify:
crates/ailang-core/src/workspace.rs:441-529— replace the body ofload_workspace_withwith a 3-line composition. The doc-comment updates to reflect the shim role. -
Step 1: Write the regression-pin test for the shim
The shim must produce a Workspace byte-identical to what today's
load_workspace_with produces for the same inputs. Pin one
representative case in the #[cfg(test)] mod tests block:
#[test]
fn load_workspace_with_shim_preserves_today_semantics() {
let dir = tempfile::tempdir().unwrap();
write_module(dir.path(), "main", &[]);
let entry = dir.path().join("main.ail.json");
let ws = load_workspace_with(&entry, load_one).expect("load");
assert_eq!(ws.entry, "main");
assert!(ws.modules.contains_key("main"));
assert!(
ws.modules.contains_key("prelude"),
"shim must still inject prelude"
);
// Module hash stability: the prelude module hash must equal what
// today's load_prelude() produces.
assert_eq!(
module_hash(&ws.modules["prelude"]),
module_hash(&load_prelude()),
);
}
- Step 2: Run test to verify it passes against today's implementation
Run: cargo test -p ailang-core load_workspace_with_shim_preserves_today_semantics
Expected: PASS — this is the baseline before the shim refit, asserting the
test is correctly written against today's behaviour.
- Step 3: Refit
load_workspace_with
Replace the body at lines 461-528 (everything inside the fn after the signature). Old shape (~70 lines): the inline DFS + inject + validation block. New shape:
pub fn load_workspace_with<F>(
entry_path: &Path,
loader: F,
) -> Result<Workspace, WorkspaceLoadError>
where
F: Fn(&Path) -> Result<Module, WorkspaceLoadError> + Copy,
{
// pd.1: thin compatibility shim composed of the new
// `load_modules_with` + `build_workspace` plus the prelude inject.
// pd.2 will rewire `surface::load_workspace` to call the new fns
// directly and remove this shim.
let (entry_name, root_dir, mut modules) = load_modules_with(entry_path, loader)?;
// Iter 23.1 (preserved through the shim): the prelude module is
// implicitly part of every workspace. A user module of the same name
// would shadow; reject explicitly.
if modules.contains_key("prelude") {
return Err(WorkspaceLoadError::ReservedModuleName {
name: "prelude".to_string(),
});
}
modules.insert("prelude".to_string(), load_prelude());
build_workspace(entry_name, root_dir, modules, &["prelude"])
}
The doc-comment above the fn (lines 441-454) updates to reflect the shim role:
/// pd.1: thin compatibility shim composed of `load_modules_with` +
/// `build_workspace` plus the prelude inject. Preserves today's
/// `load_workspace_with` semantics so surface (`crates/ailang-surface/src/loader.rs`)
/// stays unchanged in pd.1. pd.2 will rewire surface to call
/// `load_modules_with` and `build_workspace` directly and retire this
/// shim along with the embedded `PRELUDE_JSON` constant.
///
/// Algorithm: DFS over `imports` via `load_modules_with`, then inject
/// the prelude (rejecting collision with a user module of the same
/// name), then validate + build the typeclass registry via
/// `build_workspace`.
- Step 4: Run the regression-pin test
Run: cargo test -p ailang-core load_workspace_with_shim_preserves_today_semantics
Expected: PASS — the shim produces the same Workspace.
- Step 5: Run the full core test suite
Run: cargo test -p ailang-core
Expected: all tests pass. The shim is a pure refactor; every existing
test that called load_workspace_with continues to see the same
behaviour.
Task 5: Retire pub fn load_workspace (the JSON-only legacy wrapper)
Files:
-
Modify:
crates/ailang-core/src/workspace.rs:437-439— delete the fn. -
Modify:
crates/ailang-core/src/workspace.rs:1-32— update the module-level rustdoc. -
Modify:
crates/ailang-core/src/lib.rs— remove the re-export ofload_workspaceif present. -
Step 1: Verify no in-tree caller
Run: git grep -n 'ailang_core::load_workspace\b' -- '*.rs' '*.toml' | grep -v 'load_workspace_with'
Expected: no production hits. Doc-comment hits in crates/ailang-core/src/workspace.rs:1-32
and other //! / /// references are acceptable; they'll be cleaned up
in Step 4.
If a real caller appears, STOP and report — the spec assumed zero callers per the grounding-check report.
- Step 2: Delete the fn
Remove lines 437-439:
pub fn load_workspace(entry_path: &Path) -> Result<Workspace, WorkspaceLoadError> {
load_workspace_with(entry_path, load_one)
}
- Step 3: Remove the re-export from
lib.rs
Inspect crates/ailang-core/src/lib.rs. If it contains
pub use workspace::load_workspace; or similar, delete that line. If
the re-export is pub use workspace::*;, no edit needed; if it names
load_workspace explicitly, delete the entry.
- Step 4: Update the module rustdoc
crates/ailang-core/src/workspace.rs:1-32 — the example block calls
ailang_core::load_workspace(...). Rewrite the example to use the
shim:
Old (approximate — verify with the file):
//! ```no_run
//! use ailang_core::load_workspace;
//! let ws = load_workspace(...).unwrap();
//! ```
New:
//! ```no_run
//! // pd.2 will move the public entry point to ailang-surface;
//! // pd.1 keeps `load_workspace_with` as the single core entry point.
//! use ailang_core::workspace::{load_workspace_with, load_one};
//! let ws = load_workspace_with(/* path */&std::path::PathBuf::from("examples/foo.ail.json"), load_one).unwrap();
//! ```
The "single entry point" wording at line 8 (if present) updates correspondingly.
- Step 5: Run cargo check + tests
Run: cargo check -p ailang-core && cargo test -p ailang-core
Expected: clean compile, all tests green. The doc-test in the rustdoc
example may or may not run depending on the no_run attribute; if it
runs and fails, mark it no_run or ignore.
Task 6: Full workspace test suite + bench regressions
Files: none directly — verification step.
- Step 1: Run the full workspace test suite
Run: cargo test --workspace
Expected: all tests green. ailang-surface, ailang-check, ailang-codegen,
and ail tests are unaffected because the shim preserves the public
behaviour.
If a test fails, the most likely cause is a missed call site for
validate_canonical_type_names outside the mod tests block (Task 3
Step 10). Find it via git grep -n 'validate_canonical_type_names('
and migrate.
- Step 2: Run the regression baselines
Run: python bench/check.py && python bench/compile_check.py && python bench/cross_lang.py
Expected: all three exit code 0. None of pd.1's changes alter compiled
binary output or check semantics; baselines should be unchanged.
- Step 3: Confirm core's
src/literal-"prelude"count dropped from 8 to 4
Run: git grep -c '"prelude"' crates/ailang-core/src/workspace.rs
Expected: count drops from the pre-pd.1 baseline (8) to 4. The remaining
4 are: line 498 (if modules.contains_key("prelude") in the shim),
line 500 (name: "prelude".to_string() in the shim's err return), line
504 (modules.insert("prelude".to_string(), load_prelude()) in the
shim), and any survivor in test code. The 4 diagnostic-fallback
occurrences (1084 / 1086 / 1148 / 1150) MUST be gone.
If any of the four diagnostic occurrences survives, Task 3 Steps 5-6 were incomplete — find and remove.
- Step 4: Confirm pd.1 acceptance criterion
Per the spec (§Acceptance criteria, items 4 + 5 are pd.1's portion; items 1, 2, 3, 6, 7 belong to pd.2/pd.3):
cargo test --workspacegreen ✓ (Step 1)bench/check.py,bench/compile_check.py,bench/cross_lang.pygreen at unchanged baselines ✓ (Step 2)
The remaining acceptance items (delete prelude.ail.json, zero
include_str!-of-prelude.ail.json, zero literal-"prelude" in
core/src/, carve_out_inventory update, form-a §C4 (b) update)
land in pd.2 + pd.3.
Out of scope for pd.1
The following spec items are deferred to pd.2 / pd.3 and MUST NOT be touched in pd.1:
- Adding
PRELUDE_AIL: &str = include_str!("../../../examples/prelude.ail")orparse_preludetocrates/ailang-surface/src/loader.rs(pd.2). - Rewriting
surface::load_workspaceto call the new core fns directly (pd.2) — surface stays callingload_workspace_withvia the shim in pd.1. - Removing the prelude embed (
PRELUDE_JSONconst +load_preludefn) from core (pd.2 — once surface owns the embed, the shim can drop them). - The cross-form-identity preflight test (pd.2 — gates the embed swap).
- Deleting
examples/prelude.ail.json(pd.3). - Deleting the defensive include in
crates/ail/src/main.rs:472-484(pd.3). - Updating
crates/ailang-core/tests/carve_out_inventory.rs(pd.3). - Updating
docs/specs/0025-form-a-default-authoring.md§C4 (b) (pd.3).
A pd.1 implementer who finds themselves about to edit any of the above files should STOP and re-read the parent spec's iteration sketch — the work belongs to a later iter.