Skip to content

Commit 2124629

Browse files
author
Douglas Jones
committed
2026-05-14 overnight: v2.0 roadmap complete (V2-1 through V2-4)
REQ-V2-1 RPC API: - codifide/server.py: ThreadingHTTPServer over SymbolStore - POST /symbols, GET /symbols/<id>, GET /symbols/<id>/imports, GET /health - python3 -m codifide serve CLI subcommand - 29 server tests + 8 Program-5-via-HTTP acceptance tests - Sable audit: 2 P2s fixed (negative Content-Length, socket timeout) REQ-V2-2 Static bind-before-when detection: - Python parser raises ParseError for bind-before-when - Rust parser same detection ported - Runtime hints removed (both parsers catch it now) - 12 regression tests REQ-V2-3 from-import in Rust parser: - parse_with_store resolves from-imports from store filesystem - run_with_imports resolves imported symbols at call time - codifide-run --store flag; Python CLI passes it automatically - CODIFIDE_RUNTIME=python note updated in AGENT_QUICKREF REQ-V2-4 Manifest docs field: - docs field in generate_capability() with stable URLs - docs/CAPABILITY.md updated; capability-0.1.json regenerated - New manifest hash: sha256:42d73647ba8de29a7d219bf2218bad0a42dc2a11d7878cac12ee931be2a1a185 - publicsite capability files updated 341 tests passing. dispatch-check exits 0.
1 parent 6a35d2f commit 2124629

36 files changed

Lines changed: 2195 additions & 76 deletions

.kiro/specs/v2-language/tasks.md

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,34 +7,34 @@
77
- [x] **V2-1-3** Implement POST `/symbols` — accept canonical CBOR, store, return hash
88
- [x] **V2-1-4** Implement GET `/symbols/<hash>` — return canonical CBOR by hash
99
- [x] **V2-1-5** Implement GET `/symbols/<hash>/imports` — resolve import graph
10-
- [ ] **V2-1-6** Test: agent completes Program 5 via HTTP only
11-
- [ ] **V2-1-7** File Quill/Glyph dispatch for RPC API completion
12-
- [ ] **V2-1-8** Sable audit of RPC API surface
10+
- [x] **V2-1-6** Test: agent completes Program 5 via HTTP only
11+
- [x] **V2-1-7** File Quill/Glyph dispatch for RPC API completion
12+
- [x] **V2-1-8** Sable audit of RPC API surface
1313

1414
## REQ-V2-2: Static bind-before-when detection (P2)
1515

16-
- [ ] **V2-2-1** Add scope tracking to the Python parser
17-
- [ ] **V2-2-2** Raise `ParseError` for bind-before-when with clear message
18-
- [ ] **V2-2-3** Add regression tests
19-
- [ ] **V2-2-4** Remove runtime hint only after BOTH Python (V2-2-2) and Rust (V2-2-5) parsers catch bind-before-when statically
20-
- [ ] **V2-2-5** Port to Rust parser
16+
- [x] **V2-2-1** Add scope tracking to the Python parser
17+
- [x] **V2-2-2** Raise `ParseError` for bind-before-when with clear message
18+
- [x] **V2-2-3** Add regression tests
19+
- [x] **V2-2-4** Remove runtime hint only after BOTH Python (V2-2-2) and Rust (V2-2-5) parsers catch bind-before-when statically
20+
- [x] **V2-2-5** Port to Rust parser
2121
- [ ] **V2-2-6** File Quill/Glyph dispatch
2222

2323
## REQ-V2-3: `from`-import in Rust parser (P3)
2424

25-
- [ ] **V2-3-1** Implement `from <hash> import name` in Rust lexer + parser
26-
- [ ] **V2-3-2** Implement store resolution in Rust interpreter
27-
- [ ] **V2-3-3** Conformance tests: byte-identical output with Python runtime
28-
- [ ] **V2-3-4** Remove `CODIFIDE_RUNTIME=python` note from AGENT_QUICKREF
29-
- [ ] **V2-3-5** File Quill/Glyph dispatch
25+
- [x] **V2-3-1** Implement `from <hash> import name` in Rust lexer + parser
26+
- [x] **V2-3-2** Implement store resolution in Rust interpreter
27+
- [x] **V2-3-3** Conformance tests: byte-identical output with Python runtime
28+
- [x] **V2-3-4** Remove `CODIFIDE_RUNTIME=python` note from AGENT_QUICKREF
29+
- [x] **V2-3-5** File Quill/Glyph dispatch
3030

3131
## REQ-V2-4: Manifest `docs` field (P3)
3232

33-
- [ ] **V2-4-1** Add `docs` field to `generate_capability()` in `capability.py`
34-
- [ ] **V2-4-2** Update `docs/CAPABILITY.md` schema documentation
35-
- [ ] **V2-4-3** Regenerate `docs/capability-0.1.json`
36-
- [ ] **V2-4-4** Update manifest endpoint on publicsite
37-
- [ ] **V2-4-5** File Quill/Glyph dispatch
33+
- [x] **V2-4-1** Add `docs` field to `generate_capability()` in `capability.py`
34+
- [x] **V2-4-2** Update `docs/CAPABILITY.md` schema documentation
35+
- [x] **V2-4-3** Regenerate `docs/capability-0.1.json`
36+
- [x] **V2-4-4** Update manifest endpoint on publicsite
37+
- [x] **V2-4-5** File Quill/Glyph dispatch
3838

3939
## Session Close
4040

.kiro/steering/03-coding-standards.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,5 +117,7 @@ A function declared `effects {}` that calls an effectful primitive must fail at
117117
### Bind-Before-When Is a Parser Responsibility
118118
The `when` guard executes before the candidate body. A bind (`<-`) is part of the body. The parser should catch bind-before-when statically (REQ-V2-2). Until V2-2 ships, the runtime hint in `_unbound_name_message` must stay in place.
119119

120-
### From-Import Requires Python Runtime Until V2-3 Ships
121-
`from <hash> import name` is not yet supported in the Rust parser. The error message explains this and instructs the user to set `CODIFIDE_RUNTIME=python`. Do not remove this message until V2-3 is complete.
120+
### From-Import Requires Store Path
121+
`from <hash> import name` requires a store to be available at parse time.
122+
Pass `--store <path>` to the Rust runtime, or use `CODIFIDE_RUNTIME=python`.
123+
Both runtimes support `from`-import as of v2.0 (REQ-V2-3 complete).

codifide/__main__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ def _cmd_run_rust(args: argparse.Namespace) -> int:
101101
cmd = [str(rust_bin), "run", args.file]
102102
if args.entry:
103103
cmd += ["--entry", args.entry]
104+
# Pass store path so the Rust runtime can resolve from-imports.
105+
store_root = str(_store_root(args))
106+
cmd += ["--store", store_root]
104107

105108
result = subprocess.run(cmd, capture_output=True, text=True)
106109
# Forward stderr directly.

codifide/capability.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ def generate_capability() -> Dict[str, Any]:
3434
"codifide_capability": CAPABILITY_SCHEMA_VERSION,
3535
"codifide_schema": CODIFIDE_SCHEMA_VERSION,
3636
"generator": f"codifide-python-{__version__}",
37+
"docs": _docs(),
3738
"ast_kinds": _ast_kinds(),
3839
"primitives": _primitives(),
3940
"effects": _effects(),
@@ -43,6 +44,30 @@ def generate_capability() -> Dict[str, Any]:
4344
}
4445

4546

47+
# ---------------------------------------------------------------------------
48+
# Docs field (REQ-V2-4)
49+
# ---------------------------------------------------------------------------
50+
51+
52+
def _docs() -> Dict[str, str]:
53+
"""Stable URLs for key agent-facing documents.
54+
55+
An agent that fetches the capability manifest can discover the
56+
cookbook, quickref, and onboarding guide from this field without
57+
reading the README or browsing the repository.
58+
59+
URLs point to the canonical public location (codifide.com). The
60+
manifest drift test verifies these keys are present.
61+
"""
62+
return {
63+
"for_agents": "https://codifide.com/docs/FOR_AGENTS.md",
64+
"quickref": "https://codifide.com/docs/AGENT_QUICKREF.md",
65+
"cookbook": "https://codifide.com/docs/AGENT_COOKBOOK.md",
66+
"capability": "https://codifide.com/capability.json",
67+
"capability_cbor": "https://codifide.com/capability.cbor",
68+
}
69+
70+
4671
# ---------------------------------------------------------------------------
4772
# AST kinds
4873
# ---------------------------------------------------------------------------

codifide/parser/parser.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,10 @@ def _parse_candidate(lines: List[_Line], i: int) -> Tuple[Candidate, int]:
566566
guard: Optional[Expr] = None
567567
cost: Optional[int] = None
568568
steps: List[Expr] = []
569+
# Track bind names seen before any `when` guard so we can raise a
570+
# static ParseError if a guard references a name that hasn't been
571+
# bound yet (REQ-V2-2: bind-before-when detection).
572+
bound_names_before_guard: List[Tuple[str, int]] = [] # (name, lineno)
569573

570574
while i < len(lines) and lines[i].indent > base_indent:
571575
line = lines[i]
@@ -602,6 +606,25 @@ def _parse_candidate(lines: List[_Line], i: int) -> Tuple[Candidate, int]:
602606
i += 1
603607
continue
604608
if head == "when":
609+
# REQ-V2-2: Static bind-before-when detection.
610+
# `when` guards execute before the candidate body. Any name
611+
# bound with `<-` in the body does not exist yet when the
612+
# guard runs. Detect this statically and raise ParseError
613+
# with a clear message rather than failing at runtime with
614+
# a confusing "unknown callable" error.
615+
if bound_names_before_guard:
616+
names_str = ", ".join(
617+
f"'{n}' (line {ln})" for n, ln in bound_names_before_guard
618+
)
619+
raise ParseError(
620+
f"bind-before-when: the `when` guard on line {line.lineno} "
621+
f"executes before the candidate body, but {names_str} "
622+
f"{'is' if len(bound_names_before_guard) == 1 else 'are'} "
623+
f"bound in the body with `<-` and will not exist yet. "
624+
f"Fix: move the bind into the body and use `if/then/else` "
625+
f"to route on the result instead of a `when` guard.",
626+
line=line.lineno,
627+
)
605628
text, i = _gather_expr(lines, i, rest)
606629
guard = _safe_parse_expr(text, line.lineno)
607630
continue
@@ -613,6 +636,10 @@ def _parse_candidate(lines: List[_Line], i: int) -> Tuple[Candidate, int]:
613636
if _is_bind(line.text):
614637
bind_node, i = _parse_bind_multiline(lines, i)
615638
steps.append(bind_node)
639+
# Record the bound name so we can detect bind-before-when
640+
# if a `when` guard appears later in this candidate.
641+
if isinstance(bind_node, Bind):
642+
bound_names_before_guard.append((bind_node.name, line.lineno))
616643
continue
617644
# Plain expression line, possibly spanning multiple physical lines
618645
# while brackets are unbalanced.

codifide/runtime/interpreter.py

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -763,18 +763,6 @@ def _unknown_callable_message(fn: str) -> str:
763763
hint = _CALLABLE_HINTS.get(fn)
764764
if hint is not None:
765765
return f"{base}\n hint: {hint}"
766-
# If the name is a plain identifier (no dots, no special chars), it may
767-
# be a bound name used in a `when` guard before the bind executes.
768-
# Guards run before candidate bodies — a bind in the body is not yet
769-
# in scope when the guard is evaluated. This is the most common cause
770-
# of "unknown callable: <plain-name>" errors.
771-
if fn.isidentifier():
772-
return (
773-
f"{base}\n hint: if '{fn}' is a name you bound with `<-`, "
774-
f"note that `when` guards execute before the candidate body — "
775-
f"the bind has not happened yet. Move the bind into the body "
776-
f"and use `if/then/else` to route on the result."
777-
)
778766
return base
779767

780768

@@ -797,13 +785,4 @@ def _unbound_name_message(name: str) -> str:
797785
hint = _UNBOUND_HINTS.get(name)
798786
if hint is not None:
799787
return f"{base}\n hint: {hint}"
800-
# A plain identifier that is unbound in a guard context is often a
801-
# name the author intended to bind with `<-` in the same cand block.
802-
# Guards execute before candidate bodies, so a bind in the body is
803-
# not yet in scope when the guard runs.
804-
return (
805-
f"{base}\n hint: if '{name}' is a name you intended to bind with "
806-
f"`<-`, note that `when` guards execute before the candidate body — "
807-
f"the bind has not happened yet. Move the bind into the body and "
808-
f"use `if/then/else` to route on the result."
809-
)
788+
return base

codifide/server.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,8 @@
2525
"""
2626
from __future__ import annotations
2727

28-
import hashlib
2928
import http.server
3029
import json
31-
import os
3230
import re
3331
import threading
3432
from pathlib import Path
@@ -89,6 +87,8 @@ def _read_body(handler: http.server.BaseHTTPRequestHandler) -> Optional[bytes]:
8987
length = int(length_str)
9088
except ValueError:
9189
return None
90+
if length < 0:
91+
return None
9292
if length > _MAX_BODY_BYTES:
9393
# Drain the socket in chunks so the client can receive the 413.
9494
remaining = length
@@ -332,12 +332,18 @@ def make_server(store: SymbolStore, host: str = "127.0.0.1", port: int = 7777):
332332
333333
Uses ThreadingHTTPServer so concurrent POSTs are handled correctly.
334334
The store's atomic-write semantics make concurrent writes safe.
335+
336+
A 30-second socket timeout is set to prevent slow-loris style
337+
resource exhaustion (AUD-RPC-02).
335338
"""
336339
# Inject the store into the handler class via a subclass so each
337340
# request handler can access it without a global.
338341
handler_class = type("_BoundHandler", (_Handler,), {"store": store})
339342

340343
server = http.server.ThreadingHTTPServer((host, port), handler_class)
344+
# 30-second timeout per connection. Prevents a slow client from
345+
# holding a thread indefinitely.
346+
server.socket.settimeout(30)
341347
return server
342348

343349

crates/codifide-interpreter/src/bin/codifide_run.rs

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,14 @@ fn read_source(path: &str) -> String {
3535
}
3636
}
3737

38-
fn parse_source(path: &str) -> codifide_canonical::ast::Module {
38+
fn parse_source(path: &str, store_root: Option<&std::path::Path>) -> codifide_canonical::ast::Module {
3939
let source = read_source(path);
4040
// Derive module name from filename stem.
4141
let stem = std::path::Path::new(path)
4242
.file_stem()
4343
.and_then(|s| s.to_str())
4444
.unwrap_or("main");
45-
match codifide_interpreter::parse(&source, stem) {
45+
match codifide_interpreter::parse_with_store(&source, stem, store_root) {
4646
Ok(m) => m,
4747
Err(e) => {
4848
eprintln!("parse error: {}", e);
@@ -53,7 +53,7 @@ fn parse_source(path: &str) -> codifide_canonical::ast::Module {
5353

5454
fn cmd_parse(args: &[String]) {
5555
let path = &args[2];
56-
let module = parse_source(path);
56+
let module = parse_source(path, None);
5757
let json_val = codifide_canonical::to_canonical_json(&module);
5858
match serde_json::to_string(&json_val) {
5959
Ok(s) => println!("{}", s),
@@ -67,9 +67,10 @@ fn cmd_parse(args: &[String]) {
6767
fn cmd_run(args: &[String]) {
6868
let path = &args[2];
6969

70-
// Parse optional flags: --entry <name> and --args <json_array>
70+
// Parse optional flags: --entry <name>, --args <json_array>, --store <path>
7171
let mut entry = "main".to_string();
7272
let mut call_args: Vec<codifide_interpreter::Value> = vec![];
73+
let mut store_path: Option<std::path::PathBuf> = None;
7374
let mut i = 3;
7475
while i < args.len() {
7576
match args[i].as_str() {
@@ -95,14 +96,28 @@ fn cmd_run(args: &[String]) {
9596
}
9697
}
9798
}
99+
"--store" => {
100+
i += 1;
101+
if i < args.len() {
102+
store_path = Some(std::path::PathBuf::from(&args[i]));
103+
}
104+
}
98105
_ => {}
99106
}
100107
i += 1;
101108
}
102109

103-
let module = parse_source(path);
110+
let store_ref = store_path.as_deref();
111+
let module = parse_source(path, store_ref);
104112

105-
match codifide_interpreter::run(&module, &entry, call_args) {
113+
// Resolve imports from the store if a store path is provided.
114+
let resolved_imports = if let Some(store) = store_ref {
115+
resolve_imports_from_store(&module, store)
116+
} else {
117+
std::collections::HashMap::new()
118+
};
119+
120+
match codifide_interpreter::run_with_imports(&module, &entry, call_args, resolved_imports) {
106121
Ok(result) => {
107122
println!("{}", result.to_json());
108123
}
@@ -112,3 +127,39 @@ fn cmd_run(args: &[String]) {
112127
}
113128
}
114129
}
130+
131+
fn resolve_imports_from_store(
132+
module: &codifide_canonical::ast::Module,
133+
store_root: &std::path::Path,
134+
) -> std::collections::HashMap<String, codifide_canonical::ast::Definition> {
135+
let mut out = std::collections::HashMap::new();
136+
for (local_name, identity) in &module.imports {
137+
let digest = &identity[7..]; // strip "sha256:"
138+
let shard = &digest[..2];
139+
let rest = &digest[2..];
140+
let base = store_root.join("sha256").join(shard);
141+
142+
let obj: Option<serde_json::Value> = {
143+
let json_path = base.join(format!("{}.json", rest));
144+
let cbor_path = base.join(format!("{}.cbor", rest));
145+
if json_path.exists() {
146+
std::fs::read(&json_path).ok()
147+
.and_then(|d| serde_json::from_slice(&d).ok())
148+
} else if cbor_path.exists() {
149+
std::fs::read(&cbor_path).ok()
150+
.and_then(|d| codifide_canonical::decode_canonical_cbor(&d).ok())
151+
} else {
152+
None
153+
}
154+
};
155+
156+
if let Some(obj) = obj {
157+
if let Ok(imported_module) = codifide_canonical::from_canonical_json(&obj) {
158+
if let Some((_, defn)) = imported_module.symbols.into_iter().next() {
159+
out.insert(local_name.clone(), defn);
160+
}
161+
}
162+
}
163+
}
164+
out
165+
}

0 commit comments

Comments
 (0)