From a5b7b11525e6a97b5d91214f60ebc4e4bd416266 Mon Sep 17 00:00:00 2001 From: Frank O'Hara Date: Sun, 9 Aug 2026 09:23:58 -0600 Subject: [PATCH] fix(add): report the task ID the index will actually store `lash add "Ship v0.7.0 release notes" -l docs` printed `ship-v0-7-0-release-notes` while the index stored `ship-v070-release-notes-docs`. Anything that copied the printed ID (`lash show`, `lash complete`, `@depends-on`) failed, and a `@depends-on` written against it dangled, which the dangling-reference check then rejected. Two derivations existed and disagreed three ways. `v0.7.0` became `v0-7-0` in the emitter (punctuation to separator) and `v070` in the parser (punctuation deleted). The parser slugged the raw checkbox title, so an inline `#docs` was folded into the identity as a `-docs` suffix. And only the parser truncated at 40 characters. `lash_types::task::synthesize_task_id` is now the one derivation, called by both. It strips inline labels before slugging, because a task's identity should not change when someone tags it, and it truncates, so both sides agree on long titles. That still leaves the numeric suffix the parser appends to break a collision, which the emitter cannot know about. So the creation service re-reads the file it just wrote and reports the ID the parser assigned to the task at that line. If the re-read fails the emitter's ID is kept rather than failing a write that already succeeded. `parse_inline_labels` and the new label stripping share one predicate, `label::is_inline_label`, so a title loses exactly the words that parsing turns into labels. Behaviour change on upgrade: synthesized IDs for existing tasks change where a title held an inline label or punctuation inside a word. `implement-authentication-security` becomes `implement-authentication` and `design-requestresponse-schemas` becomes `design-request-response-schemas`. A `@depends-on` written against an old synthesized ID needs updating. Explicit `@id:` values are untouched. Four CLI snapshots are updated to match; every changed line is one of those two cases. Tests: four integration tests covering all three discrepancies in one title, truncation of a long title, the numeric collision suffix, and the reported ID resolving as a `--depends-on` target. All four fail without the fix. --- crates/lash-cli/tests/add_command_test.rs | 141 ++++++++++++++++++ .../regression_tests__list_all_tasks.snap | 64 ++++---- .../regression_tests__list_backend_label.snap | 24 +-- .../regression_tests__list_open_status.snap | 54 +++---- .../regression_tests__show_tasks_file.snap | 6 +- crates/lash-core/src/creation/emitter.rs | 17 +-- crates/lash-core/src/creation/service.rs | 30 +++- crates/lash-core/src/parser/builder.rs | 37 +---- crates/lash-types/src/label.rs | 50 +++++-- crates/lash-types/src/task.rs | 57 +++++++ lash.index.md | 17 ++- 11 files changed, 361 insertions(+), 136 deletions(-) diff --git a/crates/lash-cli/tests/add_command_test.rs b/crates/lash-cli/tests/add_command_test.rs index 3cd25a7..3210e0f 100644 --- a/crates/lash-cli/tests/add_command_test.rs +++ b/crates/lash-cli/tests/add_command_test.rs @@ -794,3 +794,144 @@ Prose that must stay under its heading. "task escaped the Tasks section:\n{content}" ); } + +// --------------------------------------------------------------------- +// The reported task ID must be the indexed one +// +// `lash add` printed an ID derived by the emitter while the index stored +// one derived by the parser, and the two disagreed three ways: `v0.7.0` +// slugged to `v0-7-0` in one and `v070` in the other, the parser folded +// inline labels into the ID, and only the parser truncated at 40 +// characters. Anything that copied the printed ID (`lash show`, +// `@depends-on`) failed. +// --------------------------------------------------------------------- + +/// The ID out of `Created task ...`. +fn reported_id(stdout: &str) -> String { + stdout + .lines() + .find_map(|line| line.split("Created task ").nth(1)) + .and_then(|rest| rest.split_whitespace().next()) + .unwrap_or_else(|| panic!("no created-task line in:\n{stdout}")) + .to_string() +} + +#[test] +fn test_add_reports_the_id_the_index_stores() { + let project = project_with_tasks_file(); + index(&project); + + // Every discrepancy from the original report in one title: a dotted + // version number, an inline label, and enough characters to truncate. + let stdout = run_lash_command() + .arg("--root") + .arg(project.path()) + .arg("add") + .arg("Ship v0.7.0 release notes") + .arg("--file") + .arg("tasks.md") + .arg("--label") + .arg("docs") + .assert() + .success() + .get_output() + .stdout + .clone(); + let id = reported_id(&String::from_utf8(stdout).unwrap()); + + assert_eq!(id, "ship-v0-7-0-release-notes"); + + index(&project); + run_lash_command() + .arg("--root") + .arg(project.path()) + .arg("show") + .arg(format!("tasks#{id}")) + .assert() + .success() + .stdout(predicate::str::contains("Ship v0.7.0 release notes")); +} + +#[test] +fn test_add_reports_the_truncated_id_for_a_long_title() { + let project = project_with_tasks_file(); + index(&project); + + let stdout = run_lash_command() + .arg("--root") + .arg(project.path()) + .arg("add") + .arg("Coverage fail fast when the coverage command fails and its output predates the run") + .arg("--file") + .arg("tasks.md") + .assert() + .success() + .get_output() + .stdout + .clone(); + let id = reported_id(&String::from_utf8(stdout).unwrap()); + + assert_eq!( + id.chars().count(), + 40, + "reported ID was not truncated: {id}" + ); + + index(&project); + run_lash_command() + .arg("--root") + .arg(project.path()) + .arg("show") + .arg(format!("tasks#{id}")) + .assert() + .success(); +} + +#[test] +fn test_add_reports_the_numeric_suffix_used_to_break_a_collision() { + // Only the parser knows about the suffix, so this is the case the + // emitter could not get right on its own. + let project = project_with_tasks_file(); + index(&project); + + add_task(&project, "Repeated title", "tasks.md"); + let stdout = add_task(&project, "Repeated title", "tasks.md"); + let id = reported_id(&stdout); + + assert_eq!(id, "repeated-title-2"); + + index(&project); + run_lash_command() + .arg("--root") + .arg(project.path()) + .arg("show") + .arg("tasks#repeated-title-2") + .assert() + .success(); +} + +#[test] +fn test_add_reported_id_resolves_as_a_dependency_target() { + // The consequence that made this more than an ergonomic problem: a + // `@depends-on` written against the reported ID used to dangle, and the + // dangling-reference check rejects it. + let project = project_with_tasks_file(); + index(&project); + + let stdout = add_task(&project, "Ship v0.7.0 release notes", "tasks.md"); + let id = reported_id(&stdout); + index(&project); + + run_lash_command() + .arg("--root") + .arg(project.path()) + .arg("add") + .arg("Follow-up task") + .arg("--file") + .arg("tasks.md") + .arg("--depends-on") + .arg(&id) + .assert() + .success() + .stderr(predicate::str::contains("Warning").not()); +} diff --git a/crates/lash-cli/tests/snapshots/regression_tests__list_all_tasks.snap b/crates/lash-cli/tests/snapshots/regression_tests__list_all_tasks.snap index 6448fe4..bf44b4d 100644 --- a/crates/lash-cli/tests/snapshots/regression_tests__list_all_tasks.snap +++ b/crates/lash-cli/tests/snapshots/regression_tests__list_all_tasks.snap @@ -9,16 +9,16 @@ expression: normalized ├── ○ api.md (backend.api) │ ├── [x] Design REST API (backend.api#design-rest-api) │ │ ├── [x] Define endpoints (backend.api#define-endpoints) - │ │ └── [x] Design request/response schemas (backend.api#design-requestresponse-schemas) - │ ├── [ ] Implement authentication #security (backend.api#implement-authentication-security) #security + │ │ └── [x] Design request/response schemas (backend.api#design-request-response-schemas) + │ ├── [ ] Implement authentication #security (backend.api#implement-authentication) #security │ │ ├── [x] JWT token generation (backend.api#jwt-token-generation) - │ │ ├── [ ] OAuth2 integration #p1 (backend.api#oauth2-integration-p1) #p1 + │ │ ├── [ ] OAuth2 integration #p1 (backend.api#oauth2-integration) #p1 │ │ └── [ ] Session management (backend.api#session-management) - │ ├── [ ] Implement CRUD endpoints #core (backend.api#implement-crud-endpoints-core) #core + │ ├── [ ] Implement CRUD endpoints #core (backend.api#implement-crud-endpoints) #core │ │ ├── [x] User endpoints (backend.api#user-endpoints) │ │ ├── [ ] Task endpoints (backend.api#task-endpoints) │ │ └── [ ] Project endpoints (backend.api#project-endpoints) - │ └── [ ] Add API documentation #docs (backend.api#add-api-documentation-docs) #docs + │ └── [ ] Add API documentation #docs (backend.api#add-api-documentation) #docs │ ├── [ ] OpenAPI spec (backend.api#openapi-spec) │ └── [ ] Example requests (backend.api#example-requests) ├── ✓ database.md (backend.database) @@ -27,15 +27,15 @@ expression: normalized │ │ ├── [x] Tasks table (backend.database#tasks-table) │ │ ├── [x] Projects table (backend.database#projects-table) │ │ └── [x] Relationships (backend.database#relationships) - │ ├── [x] Set up migrations #infrastructure (backend.database#set-up-migrations-infrastructure) #infrastructure + │ ├── [x] Set up migrations #infrastructure (backend.database#set-up-migrations) #infrastructure │ │ ├── [x] Migration framework (backend.database#migration-framework) │ │ ├── [x] Initial schema migration (backend.database#initial-schema-migration) │ │ └── [x] Seed data (backend.database#seed-data) - │ ├── [x] Add database layer #core (backend.database#add-database-layer-core) #core + │ ├── [x] Add database layer #core (backend.database#add-database-layer) #core │ │ ├── [x] Connection pooling (backend.database#connection-pooling) │ │ ├── [x] Query builders (backend.database#query-builders) │ │ └── [x] Transaction support (backend.database#transaction-support) - │ └── [x] Performance optimization #performance (backend.database#performance-optimization-performance) #performance + │ └── [x] Performance optimization #performance (backend.database#performance-optimization) #performance │ ├── [x] Add indexes (backend.database#add-indexes) │ └── [x] Query optimization (backend.database#query-optimization) └── ○ tests.md (backend.tests) @@ -43,48 +43,48 @@ expression: normalized │ ├── [x] Test database (backend.tests#test-database) │ ├── [x] Test fixtures (backend.tests#test-fixtures) │ └── [x] Helper functions (backend.tests#helper-functions) - ├── [ ] Unit tests #unit-testing (backend.tests#unit-tests-unit-testing) #unit-testing + ├── [ ] Unit tests #unit-testing (backend.tests#unit-tests) #unit-testing │ ├── [x] Model tests (backend.tests#model-tests) │ ├── [ ] Service tests (backend.tests#service-tests) │ └── [ ] Utility tests (backend.tests#utility-tests) - ├── [ ] Integration tests #integration-testing (backend.tests#integration-tests-integration-testing) #integration-testing + ├── [ ] Integration tests #integration-testing (backend.tests#integration-tests) #integration-testing │ ├── [x] API endpoint tests (backend.tests#api-endpoint-tests) │ ├── [ ] Database integration tests (backend.tests#database-integration-tests) │ └── [ ] Auth flow tests (backend.tests#auth-flow-tests) - └── [ ] Performance tests #performance (backend.tests#performance-tests-performance) #performance + └── [ ] Performance tests #performance (backend.tests#performance-tests) #performance ├── [ ] Load testing (backend.tests#load-testing) └── [ ] Stress testing (backend.tests#stress-testing) └── docs/ ├── ○ api-docs.md (docs.api) - │ ├── [ ] API reference documentation #docs (docs.api#api-reference-documentation-docs) #docs + │ ├── [ ] API reference documentation #docs (docs.api#api-reference-documentation) #docs │ │ ├── [ ] Authentication endpoints (docs.api#authentication-endpoints) │ │ ├── [ ] User endpoints (docs.api#user-endpoints) │ │ ├── [ ] Task endpoints (docs.api#task-endpoints) │ │ └── [ ] Project endpoints (docs.api#project-endpoints) - │ ├── [ ] API guide #guides (docs.api#api-guide-guides) #guides + │ ├── [ ] API guide #guides (docs.api#api-guide) #guides │ │ ├── [ ] Getting started (docs.api#getting-started) │ │ ├── [ ] Authentication guide (docs.api#authentication-guide) │ │ ├── [ ] Rate limiting (docs.api#rate-limiting) │ │ └── [ ] Error handling (docs.api#error-handling) - │ └── [ ] Code examples #examples (docs.api#code-examples-examples) #examples + │ └── [ ] Code examples #examples (docs.api#code-examples) #examples │ ├── [ ] cURL examples (docs.api#curl-examples) │ ├── [ ] JavaScript examples (docs.api#javascript-examples) │ └── [ ] Python examples (docs.api#python-examples) └── ○ user-guide.md (docs.user-guide) - ├── [ ] Getting started guide #guides (docs.user-guide#getting-started-guide-guides) #guides + ├── [ ] Getting started guide #guides (docs.user-guide#getting-started-guide) #guides │ ├── [ ] Installation (docs.user-guide#installation) │ ├── [ ] Quick start (docs.user-guide#quick-start) │ └── [ ] Basic concepts (docs.user-guide#basic-concepts) - ├── [ ] Feature documentation #features (docs.user-guide#feature-documentation-features) #features + ├── [ ] Feature documentation #features (docs.user-guide#feature-documentation) #features │ ├── [ ] Task management (docs.user-guide#task-management) │ ├── [ ] Project organization (docs.user-guide#project-organization) │ ├── [ ] Collaboration (docs.user-guide#collaboration) │ └── [ ] Search and filtering (docs.user-guide#search-and-filtering) - ├── [ ] Tutorial #tutorials (docs.user-guide#tutorial-tutorials) #tutorials + ├── [ ] Tutorial #tutorials (docs.user-guide#tutorial) #tutorials │ ├── [ ] First project (docs.user-guide#first-project) │ ├── [ ] Team workflow (docs.user-guide#team-workflow) │ └── [ ] Advanced features (docs.user-guide#advanced-features) - └── [ ] FAQ #support (docs.user-guide#faq-support) #support + └── [ ] FAQ #support (docs.user-guide#faq) #support ├── [ ] Common questions (docs.user-guide#common-questions) ├── [ ] Troubleshooting (docs.user-guide#troubleshooting) └── [ ] Best practices (docs.user-guide#best-practices) @@ -94,18 +94,18 @@ expression: normalized │ │ ├── [x] Choose framework (frontend.components#choose-framework) │ │ ├── [x] Configure build (frontend.components#configure-build) │ │ └── [x] Set up storybook (frontend.components#set-up-storybook) - │ ├── [ ] Implement base components #components (frontend.components#implement-base-components-components) #components + │ ├── [ ] Implement base components #components (frontend.components#implement-base-components) #components │ │ ├── [x] Button (frontend.components#button) │ │ ├── [x] Input (frontend.components#input) │ │ ├── [x] Card (frontend.components#card) │ │ ├── [ ] Modal (frontend.components#modal) │ │ └── [ ] Dropdown (frontend.components#dropdown) - │ ├── [ ] Implement feature components #features (frontend.components#implement-feature-components-features) #features + │ ├── [ ] Implement feature components #features (frontend.components#implement-feature-components) #features │ │ ├── [ ] Login form (frontend.components#login-form) │ │ ├── [ ] Task list (frontend.components#task-list) │ │ ├── [ ] Task detail (frontend.components#task-detail) │ │ └── [ ] Project dashboard (frontend.components#project-dashboard) - │ └── [ ] Add component tests #testing (frontend.components#add-component-tests-testing) #testing + │ └── [ ] Add component tests #testing (frontend.components#add-component-tests) #testing │ ├── [x] Unit tests (frontend.components#unit-tests) │ └── [ ] Visual regression tests (frontend.components#visual-regression-tests) ├── ○ state.md (frontend.state) @@ -113,17 +113,17 @@ expression: normalized │ │ ├── [x] Choose library (frontend.state#choose-library) │ │ ├── [x] Configure store (frontend.state#configure-store) │ │ └── [x] Define state shape (frontend.state#define-state-shape) - │ ├── [ ] Implement state slices #state (frontend.state#implement-state-slices-state) #state + │ ├── [ ] Implement state slices #state (frontend.state#implement-state-slices) #state │ │ ├── [x] Auth state (frontend.state#auth-state) │ │ ├── [ ] Tasks state (frontend.state#tasks-state) │ │ ├── [ ] Projects state (frontend.state#projects-state) │ │ └── [ ] UI state (frontend.state#ui-state) - │ ├── [ ] Add API integration #api (frontend.state#add-api-integration-api) #api + │ ├── [ ] Add API integration #api (frontend.state#add-api-integration) #api │ │ ├── [x] API client (frontend.state#api-client) │ │ ├── [ ] Data fetching (frontend.state#data-fetching) │ │ ├── [ ] Caching strategy (frontend.state#caching-strategy) │ │ └── [ ] Error handling (frontend.state#error-handling) - │ └── [ ] Add state tests #testing (frontend.state#add-state-tests-testing) #testing + │ └── [ ] Add state tests #testing (frontend.state#add-state-tests) #testing │ ├── [ ] Reducer tests (frontend.state#reducer-tests) │ ├── [ ] Action tests (frontend.state#action-tests) │ └── [ ] Selector tests (frontend.state#selector-tests) @@ -132,34 +132,34 @@ expression: normalized │ ├── [x] Test runner (frontend.tests#test-runner) │ ├── [x] Testing library (frontend.tests#testing-library) │ └── [x] Mock API (frontend.tests#mock-api) - ├── [ ] Component tests #component-testing (frontend.tests#component-tests-component-testing) #component-testing + ├── [ ] Component tests #component-testing (frontend.tests#component-tests) #component-testing │ ├── [x] Base component tests (frontend.tests#base-component-tests) │ ├── [ ] Feature component tests (frontend.tests#feature-component-tests) │ └── [ ] Integration tests (frontend.tests#integration-tests) - ├── [ ] E2E tests #e2e-testing (frontend.tests#e2e-tests-e2e-testing) #e2e-testing + ├── [ ] E2E tests #e2e-testing (frontend.tests#e2e-tests) #e2e-testing │ ├── [ ] User flows (frontend.tests#user-flows) │ ├── [ ] Critical paths (frontend.tests#critical-paths) │ └── [ ] Error scenarios (frontend.tests#error-scenarios) - └── [ ] Visual tests #visual-testing (frontend.tests#visual-tests-visual-testing) #visual-testing + └── [ ] Visual tests #visual-testing (frontend.tests#visual-tests) #visual-testing ├── [ ] Snapshot tests (frontend.tests#snapshot-tests) └── [ ] Visual regression (frontend.tests#visual-regression) └── ○ lash.index.md (medium-project) ├── [x] Project initialization (medium-project#project-initialization) │ ├── [x] Set up repository (medium-project#set-up-repository) │ ├── [x] Configure build system (medium-project#configure-build-system) - │ └── [x] Set up CI/CD (medium-project#set-up-cicd) - ├── [ ] Backend development #backend (medium-project#backend-development-backend) #backend + │ └── [x] Set up CI/CD (medium-project#set-up-ci-cd) + ├── [ ] Backend development #backend (medium-project#backend-development) #backend │ ├── [x] Database schema (medium-project#database-schema) │ ├── [ ] API implementation (medium-project#api-implementation) │ └── [ ] Backend tests (medium-project#backend-tests) - ├── [ ] Frontend development #frontend (medium-project#frontend-development-frontend) #frontend + ├── [ ] Frontend development #frontend (medium-project#frontend-development) #frontend │ ├── [ ] UI components (medium-project#ui-components) │ ├── [ ] State management (medium-project#state-management) │ └── [ ] Frontend tests (medium-project#frontend-tests) - ├── [ ] Documentation #docs (medium-project#documentation-docs) #docs + ├── [ ] Documentation #docs (medium-project#documentation) #docs │ ├── [ ] API docs (medium-project#api-docs) │ └── [ ] User guide (medium-project#user-guide) - └── [ ] Deployment #ops (medium-project#deployment-ops) #ops + └── [ ] Deployment #ops (medium-project#deployment) #ops ├── [ ] Staging environment (medium-project#staging-environment) └── [ ] Production deployment (medium-project#production-deployment) diff --git a/crates/lash-cli/tests/snapshots/regression_tests__list_backend_label.snap b/crates/lash-cli/tests/snapshots/regression_tests__list_backend_label.snap index f7ecae2..41fae36 100644 --- a/crates/lash-cli/tests/snapshots/regression_tests__list_backend_label.snap +++ b/crates/lash-cli/tests/snapshots/regression_tests__list_backend_label.snap @@ -9,16 +9,16 @@ expression: normalized ├── ○ api.md (backend.api) │ ├── [x] Design REST API (backend.api#design-rest-api) │ │ ├── [x] Define endpoints (backend.api#define-endpoints) - │ │ └── [x] Design request/response schemas (backend.api#design-requestresponse-schemas) - │ ├── [ ] Implement authentication #security (backend.api#implement-authentication-security) #security + │ │ └── [x] Design request/response schemas (backend.api#design-request-response-schemas) + │ ├── [ ] Implement authentication #security (backend.api#implement-authentication) #security │ │ ├── [x] JWT token generation (backend.api#jwt-token-generation) - │ │ ├── [ ] OAuth2 integration #p1 (backend.api#oauth2-integration-p1) #p1 + │ │ ├── [ ] OAuth2 integration #p1 (backend.api#oauth2-integration) #p1 │ │ └── [ ] Session management (backend.api#session-management) - │ ├── [ ] Implement CRUD endpoints #core (backend.api#implement-crud-endpoints-core) #core + │ ├── [ ] Implement CRUD endpoints #core (backend.api#implement-crud-endpoints) #core │ │ ├── [x] User endpoints (backend.api#user-endpoints) │ │ ├── [ ] Task endpoints (backend.api#task-endpoints) │ │ └── [ ] Project endpoints (backend.api#project-endpoints) - │ └── [ ] Add API documentation #docs (backend.api#add-api-documentation-docs) #docs + │ └── [ ] Add API documentation #docs (backend.api#add-api-documentation) #docs │ ├── [ ] OpenAPI spec (backend.api#openapi-spec) │ └── [ ] Example requests (backend.api#example-requests) ├── ✓ database.md (backend.database) @@ -27,15 +27,15 @@ expression: normalized │ │ ├── [x] Tasks table (backend.database#tasks-table) │ │ ├── [x] Projects table (backend.database#projects-table) │ │ └── [x] Relationships (backend.database#relationships) - │ ├── [x] Set up migrations #infrastructure (backend.database#set-up-migrations-infrastructure) #infrastructure + │ ├── [x] Set up migrations #infrastructure (backend.database#set-up-migrations) #infrastructure │ │ ├── [x] Migration framework (backend.database#migration-framework) │ │ ├── [x] Initial schema migration (backend.database#initial-schema-migration) │ │ └── [x] Seed data (backend.database#seed-data) - │ ├── [x] Add database layer #core (backend.database#add-database-layer-core) #core + │ ├── [x] Add database layer #core (backend.database#add-database-layer) #core │ │ ├── [x] Connection pooling (backend.database#connection-pooling) │ │ ├── [x] Query builders (backend.database#query-builders) │ │ └── [x] Transaction support (backend.database#transaction-support) - │ └── [x] Performance optimization #performance (backend.database#performance-optimization-performance) #performance + │ └── [x] Performance optimization #performance (backend.database#performance-optimization) #performance │ ├── [x] Add indexes (backend.database#add-indexes) │ └── [x] Query optimization (backend.database#query-optimization) └── ○ tests.md (backend.tests) @@ -43,18 +43,18 @@ expression: normalized │ ├── [x] Test database (backend.tests#test-database) │ ├── [x] Test fixtures (backend.tests#test-fixtures) │ └── [x] Helper functions (backend.tests#helper-functions) - ├── [ ] Unit tests #unit-testing (backend.tests#unit-tests-unit-testing) #unit-testing + ├── [ ] Unit tests #unit-testing (backend.tests#unit-tests) #unit-testing │ ├── [x] Model tests (backend.tests#model-tests) │ ├── [ ] Service tests (backend.tests#service-tests) │ └── [ ] Utility tests (backend.tests#utility-tests) - ├── [ ] Integration tests #integration-testing (backend.tests#integration-tests-integration-testing) #integration-testing + ├── [ ] Integration tests #integration-testing (backend.tests#integration-tests) #integration-testing │ ├── [x] API endpoint tests (backend.tests#api-endpoint-tests) │ ├── [ ] Database integration tests (backend.tests#database-integration-tests) │ └── [ ] Auth flow tests (backend.tests#auth-flow-tests) - └── [ ] Performance tests #performance (backend.tests#performance-tests-performance) #performance + └── [ ] Performance tests #performance (backend.tests#performance-tests) #performance ├── [ ] Load testing (backend.tests#load-testing) └── [ ] Stress testing (backend.tests#stress-testing) └── ○ lash.index.md (medium-project) - └── [ ] Backend development #backend (medium-project#backend-development-backend) #backend + └── [ ] Backend development #backend (medium-project#backend-development) #backend Total: 4 file(s), 46 task(s) diff --git a/crates/lash-cli/tests/snapshots/regression_tests__list_open_status.snap b/crates/lash-cli/tests/snapshots/regression_tests__list_open_status.snap index 7dc4ea4..8a53c8c 100644 --- a/crates/lash-cli/tests/snapshots/regression_tests__list_open_status.snap +++ b/crates/lash-cli/tests/snapshots/regression_tests__list_open_status.snap @@ -7,107 +7,107 @@ expression: normalized ┗┛┗┻┛┛┗ └── backend/ ├── ○ api.md (backend.api) - │ ├── [ ] Implement authentication #security (backend.api#implement-authentication-security) #security - │ │ ├── [ ] OAuth2 integration #p1 (backend.api#oauth2-integration-p1) #p1 + │ ├── [ ] Implement authentication #security (backend.api#implement-authentication) #security + │ │ ├── [ ] OAuth2 integration #p1 (backend.api#oauth2-integration) #p1 │ │ └── [ ] Session management (backend.api#session-management) - │ ├── [ ] Implement CRUD endpoints #core (backend.api#implement-crud-endpoints-core) #core + │ ├── [ ] Implement CRUD endpoints #core (backend.api#implement-crud-endpoints) #core │ │ ├── [ ] Task endpoints (backend.api#task-endpoints) │ │ └── [ ] Project endpoints (backend.api#project-endpoints) - │ └── [ ] Add API documentation #docs (backend.api#add-api-documentation-docs) #docs + │ └── [ ] Add API documentation #docs (backend.api#add-api-documentation) #docs │ ├── [ ] OpenAPI spec (backend.api#openapi-spec) │ └── [ ] Example requests (backend.api#example-requests) └── ○ tests.md (backend.tests) - ├── [ ] Unit tests #unit-testing (backend.tests#unit-tests-unit-testing) #unit-testing + ├── [ ] Unit tests #unit-testing (backend.tests#unit-tests) #unit-testing │ ├── [ ] Service tests (backend.tests#service-tests) │ └── [ ] Utility tests (backend.tests#utility-tests) - ├── [ ] Integration tests #integration-testing (backend.tests#integration-tests-integration-testing) #integration-testing + ├── [ ] Integration tests #integration-testing (backend.tests#integration-tests) #integration-testing │ ├── [ ] Database integration tests (backend.tests#database-integration-tests) │ └── [ ] Auth flow tests (backend.tests#auth-flow-tests) - └── [ ] Performance tests #performance (backend.tests#performance-tests-performance) #performance + └── [ ] Performance tests #performance (backend.tests#performance-tests) #performance ├── [ ] Load testing (backend.tests#load-testing) └── [ ] Stress testing (backend.tests#stress-testing) └── docs/ ├── ○ api-docs.md (docs.api) - │ ├── [ ] API reference documentation #docs (docs.api#api-reference-documentation-docs) #docs + │ ├── [ ] API reference documentation #docs (docs.api#api-reference-documentation) #docs │ │ ├── [ ] Authentication endpoints (docs.api#authentication-endpoints) │ │ ├── [ ] User endpoints (docs.api#user-endpoints) │ │ ├── [ ] Task endpoints (docs.api#task-endpoints) │ │ └── [ ] Project endpoints (docs.api#project-endpoints) - │ ├── [ ] API guide #guides (docs.api#api-guide-guides) #guides + │ ├── [ ] API guide #guides (docs.api#api-guide) #guides │ │ ├── [ ] Getting started (docs.api#getting-started) │ │ ├── [ ] Authentication guide (docs.api#authentication-guide) │ │ ├── [ ] Rate limiting (docs.api#rate-limiting) │ │ └── [ ] Error handling (docs.api#error-handling) - │ └── [ ] Code examples #examples (docs.api#code-examples-examples) #examples + │ └── [ ] Code examples #examples (docs.api#code-examples) #examples │ ├── [ ] cURL examples (docs.api#curl-examples) │ ├── [ ] JavaScript examples (docs.api#javascript-examples) │ └── [ ] Python examples (docs.api#python-examples) └── ○ user-guide.md (docs.user-guide) - ├── [ ] Getting started guide #guides (docs.user-guide#getting-started-guide-guides) #guides + ├── [ ] Getting started guide #guides (docs.user-guide#getting-started-guide) #guides │ ├── [ ] Installation (docs.user-guide#installation) │ ├── [ ] Quick start (docs.user-guide#quick-start) │ └── [ ] Basic concepts (docs.user-guide#basic-concepts) - ├── [ ] Feature documentation #features (docs.user-guide#feature-documentation-features) #features + ├── [ ] Feature documentation #features (docs.user-guide#feature-documentation) #features │ ├── [ ] Task management (docs.user-guide#task-management) │ ├── [ ] Project organization (docs.user-guide#project-organization) │ ├── [ ] Collaboration (docs.user-guide#collaboration) │ └── [ ] Search and filtering (docs.user-guide#search-and-filtering) - ├── [ ] Tutorial #tutorials (docs.user-guide#tutorial-tutorials) #tutorials + ├── [ ] Tutorial #tutorials (docs.user-guide#tutorial) #tutorials │ ├── [ ] First project (docs.user-guide#first-project) │ ├── [ ] Team workflow (docs.user-guide#team-workflow) │ └── [ ] Advanced features (docs.user-guide#advanced-features) - └── [ ] FAQ #support (docs.user-guide#faq-support) #support + └── [ ] FAQ #support (docs.user-guide#faq) #support ├── [ ] Common questions (docs.user-guide#common-questions) ├── [ ] Troubleshooting (docs.user-guide#troubleshooting) └── [ ] Best practices (docs.user-guide#best-practices) └── frontend/ ├── ○ components.md (frontend.components) - │ ├── [ ] Implement base components #components (frontend.components#implement-base-components-components) #components + │ ├── [ ] Implement base components #components (frontend.components#implement-base-components) #components │ │ ├── [ ] Modal (frontend.components#modal) │ │ └── [ ] Dropdown (frontend.components#dropdown) - │ ├── [ ] Implement feature components #features (frontend.components#implement-feature-components-features) #features + │ ├── [ ] Implement feature components #features (frontend.components#implement-feature-components) #features │ │ ├── [ ] Login form (frontend.components#login-form) │ │ ├── [ ] Task list (frontend.components#task-list) │ │ ├── [ ] Task detail (frontend.components#task-detail) │ │ └── [ ] Project dashboard (frontend.components#project-dashboard) - │ └── [ ] Add component tests #testing (frontend.components#add-component-tests-testing) #testing + │ └── [ ] Add component tests #testing (frontend.components#add-component-tests) #testing │ └── [ ] Visual regression tests (frontend.components#visual-regression-tests) ├── ○ state.md (frontend.state) - │ ├── [ ] Implement state slices #state (frontend.state#implement-state-slices-state) #state + │ ├── [ ] Implement state slices #state (frontend.state#implement-state-slices) #state │ │ ├── [ ] Tasks state (frontend.state#tasks-state) │ │ ├── [ ] Projects state (frontend.state#projects-state) │ │ └── [ ] UI state (frontend.state#ui-state) - │ ├── [ ] Add API integration #api (frontend.state#add-api-integration-api) #api + │ ├── [ ] Add API integration #api (frontend.state#add-api-integration) #api │ │ ├── [ ] Data fetching (frontend.state#data-fetching) │ │ ├── [ ] Caching strategy (frontend.state#caching-strategy) │ │ └── [ ] Error handling (frontend.state#error-handling) - │ └── [ ] Add state tests #testing (frontend.state#add-state-tests-testing) #testing + │ └── [ ] Add state tests #testing (frontend.state#add-state-tests) #testing │ ├── [ ] Reducer tests (frontend.state#reducer-tests) │ ├── [ ] Action tests (frontend.state#action-tests) │ └── [ ] Selector tests (frontend.state#selector-tests) └── ○ tests.md (frontend.tests) - ├── [ ] Component tests #component-testing (frontend.tests#component-tests-component-testing) #component-testing + ├── [ ] Component tests #component-testing (frontend.tests#component-tests) #component-testing │ ├── [ ] Feature component tests (frontend.tests#feature-component-tests) │ └── [ ] Integration tests (frontend.tests#integration-tests) - ├── [ ] E2E tests #e2e-testing (frontend.tests#e2e-tests-e2e-testing) #e2e-testing + ├── [ ] E2E tests #e2e-testing (frontend.tests#e2e-tests) #e2e-testing │ ├── [ ] User flows (frontend.tests#user-flows) │ ├── [ ] Critical paths (frontend.tests#critical-paths) │ └── [ ] Error scenarios (frontend.tests#error-scenarios) - └── [ ] Visual tests #visual-testing (frontend.tests#visual-tests-visual-testing) #visual-testing + └── [ ] Visual tests #visual-testing (frontend.tests#visual-tests) #visual-testing ├── [ ] Snapshot tests (frontend.tests#snapshot-tests) └── [ ] Visual regression (frontend.tests#visual-regression) └── ○ lash.index.md (medium-project) - ├── [ ] Backend development #backend (medium-project#backend-development-backend) #backend + ├── [ ] Backend development #backend (medium-project#backend-development) #backend │ ├── [ ] API implementation (medium-project#api-implementation) │ └── [ ] Backend tests (medium-project#backend-tests) - ├── [ ] Frontend development #frontend (medium-project#frontend-development-frontend) #frontend + ├── [ ] Frontend development #frontend (medium-project#frontend-development) #frontend │ ├── [ ] UI components (medium-project#ui-components) │ ├── [ ] State management (medium-project#state-management) │ └── [ ] Frontend tests (medium-project#frontend-tests) - ├── [ ] Documentation #docs (medium-project#documentation-docs) #docs + ├── [ ] Documentation #docs (medium-project#documentation) #docs │ ├── [ ] API docs (medium-project#api-docs) │ └── [ ] User guide (medium-project#user-guide) - └── [ ] Deployment #ops (medium-project#deployment-ops) #ops + └── [ ] Deployment #ops (medium-project#deployment) #ops ├── [ ] Staging environment (medium-project#staging-environment) └── [ ] Production deployment (medium-project#production-deployment) diff --git a/crates/lash-cli/tests/snapshots/regression_tests__show_tasks_file.snap b/crates/lash-cli/tests/snapshots/regression_tests__show_tasks_file.snap index 41065c7..fdfb686 100644 --- a/crates/lash-cli/tests/snapshots/regression_tests__show_tasks_file.snap +++ b/crates/lash-cli/tests/snapshots/regression_tests__show_tasks_file.snap @@ -15,7 +15,7 @@ Tasks (7): [ ] Feature development (dev-tasks#feature-development) ├── [x] Design API (dev-tasks#design-api) ├── [ ] API documentation (dev-tasks#api-documentation) - └── [ ] Implement endpoints #backend (dev-tasks#implement-endpoints-backend) - [ ] Documentation #docs (dev-tasks#documentation-docs) + └── [ ] Implement endpoints #backend (dev-tasks#implement-endpoints) + [ ] Documentation #docs (dev-tasks#documentation) ├── [ ] User guide (dev-tasks#user-guide) - └── [ ] Write tests #testing (dev-tasks#write-tests-testing) + └── [ ] Write tests #testing (dev-tasks#write-tests) diff --git a/crates/lash-core/src/creation/emitter.rs b/crates/lash-core/src/creation/emitter.rs index 7785d76..c951389 100644 --- a/crates/lash-core/src/creation/emitter.rs +++ b/crates/lash-core/src/creation/emitter.rs @@ -523,7 +523,10 @@ impl MarkdownEmitter { /// Synthesize a task ID from the task title /// - /// Converts the title to a slug-like format (lowercase, hyphens for spaces). + /// Thin wrapper over [`lash_types::task::synthesize_task_id`], which is + /// also what the parser uses when a task carries no `@id:`. Sharing it is + /// the point: an ID reported by `lash add` has to be the ID the index + /// stores, or `lash show` and `@depends-on` cannot use it. /// /// # Examples /// @@ -537,17 +540,7 @@ impl MarkdownEmitter { /// ``` #[must_use] pub fn synthesize_id(title: &str) -> String { - title - .to_lowercase() - .chars() - .map(|c| if c.is_alphanumeric() { c } else { '-' }) - .collect::() - // Remove leading/trailing hyphens and collapse multiple hyphens - .trim_matches('-') - .split('-') - .filter(|s| !s.is_empty()) - .collect::>() - .join("-") + lash_types::task::synthesize_task_id(title) } /// Synthesize a file ID from the file path diff --git a/crates/lash-core/src/creation/service.rs b/crates/lash-core/src/creation/service.rs index 203f6ad..9d7c3a9 100644 --- a/crates/lash-core/src/creation/service.rs +++ b/crates/lash-core/src/creation/service.rs @@ -9,7 +9,7 @@ use lash_types::config::LashConfig; use lash_types::creation::{FileTarget, TaskCreationRequest, TaskCreationResult}; use lash_types::creation_errors::TaskCreationError; use lash_types::file::TaskFile; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use super::emitter::MarkdownEmitter; use super::placement::PlacementResolver; @@ -120,11 +120,37 @@ impl TaskCreationService { let placement = PlacementResolver::resolve(&ctx, request).map_err(|e| vec![e])?; // Step 4: Emit to markdown - let result = MarkdownEmitter::emit(request, &ctx, &placement).map_err(|e| vec![e])?; + let mut result = MarkdownEmitter::emit(request, &ctx, &placement).map_err(|e| vec![e])?; + + // Step 5: Report the ID the parser gives the task, not the one the + // emitter guessed. The two agree on the slug now that both go through + // `synthesize_task_id`, but only the parser can apply the numeric + // suffix it uses to break a collision, and it is the parser's answer + // that ends up in the index and in `lash show`. + if let Some(parsed_id) = + Self::parsed_id_at(&result.file_path, result.line_number, &self.config) + { + result.task_id = parsed_id; + } Ok(result) } + /// The ID the parser assigns to the task written at `line_number` + /// + /// Returns `None` if the file cannot be re-read or holds no task on that + /// line, in which case the caller keeps the emitter's ID rather than + /// failing a write that already succeeded. + fn parsed_id_at(path: &Path, line_number: usize, config: &LashConfig) -> Option { + parse_file(path, config) + .ok()? + .tasks + .tasks() + .iter() + .find(|task| task.line_number == line_number) + .map(|task| task.id.clone()) + } + /// Load and parse the target file if it exists /// /// Handles different file target types and returns the parsed file content diff --git a/crates/lash-core/src/parser/builder.rs b/crates/lash-core/src/parser/builder.rs index 4c6ce38..c783edd 100644 --- a/crates/lash-core/src/parser/builder.rs +++ b/crates/lash-core/src/parser/builder.rs @@ -570,40 +570,15 @@ impl TaskTreeBuilder { /// /// Returns a unique ID for the task. fn generate_synthetic_id(&mut self, title: &str, index: usize) -> String { - // Create slug from title - let slug = title - .to_lowercase() - .chars() - .map(|c| { - if c.is_alphanumeric() || c == '-' { - c - } else if c.is_whitespace() { - '-' - } else { - '\0' // Mark for removal - } - }) - .filter(|&c| c != '\0') - .collect::(); - - // Truncate to reasonable length (40 chars) - let truncated = if slug.chars().count() > 40 { - slug.chars().take(40).collect::() - } else { - slug - }; - - // Clean up: remove leading/trailing hyphens and collapse multiple hyphens - let mut cleaned = truncated.trim_matches('-').to_string(); - while cleaned.contains("--") { - cleaned = cleaned.replace("--", "-"); - } + // Shared with `lash add`, so the ID reported on creation is the ID + // stored on the next index. + let slug = lash_types::task::synthesize_task_id(title); - // Use numeric fallback if slug is empty - let base_id = if cleaned.is_empty() { + // Use numeric fallback if the title slugs to nothing + let base_id = if slug.is_empty() { format!("task-{index}") } else { - cleaned + slug }; // Check if ID is already used; if so, add numeric suffix diff --git a/crates/lash-types/src/label.rs b/crates/lash-types/src/label.rs index 1367ea8..d37238e 100644 --- a/crates/lash-types/src/label.rs +++ b/crates/lash-types/src/label.rs @@ -135,23 +135,41 @@ pub fn is_valid_label(s: &str) -> bool { /// ``` #[must_use] pub fn parse_inline_labels(text: &str) -> Vec