instructware-tools 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- instructware_tools-0.1.0/.gitignore +17 -0
- instructware_tools-0.1.0/LICENSE +21 -0
- instructware_tools-0.1.0/PKG-INFO +54 -0
- instructware_tools-0.1.0/README.md +40 -0
- instructware_tools-0.1.0/iwp_build/README.md +132 -0
- instructware_tools-0.1.0/iwp_build/__init__.py +1 -0
- instructware_tools-0.1.0/iwp_build/__main__.py +4 -0
- instructware_tools-0.1.0/iwp_build/cli.py +289 -0
- instructware_tools-0.1.0/iwp_build/tests/__init__.py +1 -0
- instructware_tools-0.1.0/iwp_build/tests/e2e/__init__.py +1 -0
- instructware_tools-0.1.0/iwp_build/tests/e2e/test_bootstrap_no_baseline_no_links.py +76 -0
- instructware_tools-0.1.0/iwp_build/tests/e2e/test_bootstrap_official_schema.py +49 -0
- instructware_tools-0.1.0/iwp_build/tests/e2e/test_feature_add_node.py +114 -0
- instructware_tools-0.1.0/iwp_build/tests/e2e/test_feature_delete_node.py +99 -0
- instructware_tools-0.1.0/iwp_build/tests/e2e/test_feature_modify_node.py +100 -0
- instructware_tools-0.1.0/iwp_build/tests/test_build_commit.py +113 -0
- instructware_tools-0.1.0/iwp_build/tests/test_e2e_flow.py +8 -0
- instructware_tools-0.1.0/iwp_build/tests/test_e2e_suite.py +24 -0
- instructware_tools-0.1.0/iwp_build/tests/test_watch.py +54 -0
- instructware_tools-0.1.0/iwp_build/watch.py +188 -0
- instructware_tools-0.1.0/iwp_lint/ARCHITECTURE.md +300 -0
- instructware_tools-0.1.0/iwp_lint/README.md +236 -0
- instructware_tools-0.1.0/iwp_lint/__init__.py +4 -0
- instructware_tools-0.1.0/iwp_lint/__main__.py +4 -0
- instructware_tools-0.1.0/iwp_lint/_bundled_schema/__init__.py +1 -0
- instructware_tools-0.1.0/iwp_lint/_bundled_schema/iwp-schema.v1.json +573 -0
- instructware_tools-0.1.0/iwp_lint/api.py +119 -0
- instructware_tools-0.1.0/iwp_lint/cli.py +265 -0
- instructware_tools-0.1.0/iwp_lint/comment_scanner.py +104 -0
- instructware_tools-0.1.0/iwp_lint/config.py +138 -0
- instructware_tools-0.1.0/iwp_lint/core/__init__.py +0 -0
- instructware_tools-0.1.0/iwp_lint/core/engine.py +375 -0
- instructware_tools-0.1.0/iwp_lint/core/errors.py +15 -0
- instructware_tools-0.1.0/iwp_lint/core/models.py +47 -0
- instructware_tools-0.1.0/iwp_lint/core/node_catalog.py +799 -0
- instructware_tools-0.1.0/iwp_lint/diff_resolver.py +21 -0
- instructware_tools-0.1.0/iwp_lint/engine.py +366 -0
- instructware_tools-0.1.0/iwp_lint/errors.py +15 -0
- instructware_tools-0.1.0/iwp_lint/md_parser.py +179 -0
- instructware_tools-0.1.0/iwp_lint/models.py +47 -0
- instructware_tools-0.1.0/iwp_lint/parsers/__init__.py +0 -0
- instructware_tools-0.1.0/iwp_lint/parsers/comment_scanner.py +104 -0
- instructware_tools-0.1.0/iwp_lint/parsers/markdown_outline.py +33 -0
- instructware_tools-0.1.0/iwp_lint/parsers/md_parser.py +192 -0
- instructware_tools-0.1.0/iwp_lint/parsers/node_registry.py +193 -0
- instructware_tools-0.1.0/iwp_lint/schema/__init__.py +0 -0
- instructware_tools-0.1.0/iwp_lint/schema/schema_loader.py +65 -0
- instructware_tools-0.1.0/iwp_lint/schema/schema_models.py +45 -0
- instructware_tools-0.1.0/iwp_lint/schema/schema_semantics.py +45 -0
- instructware_tools-0.1.0/iwp_lint/schema/schema_validator.py +178 -0
- instructware_tools-0.1.0/iwp_lint/schema_loader.py +63 -0
- instructware_tools-0.1.0/iwp_lint/schema_models.py +45 -0
- instructware_tools-0.1.0/iwp_lint/schema_semantics.py +45 -0
- instructware_tools-0.1.0/iwp_lint/schema_validator.py +178 -0
- instructware_tools-0.1.0/iwp_lint/tests/e2e/__init__.py +1 -0
- instructware_tools-0.1.0/iwp_lint/tests/e2e/test_code_only_and_compiled.py +75 -0
- instructware_tools-0.1.0/iwp_lint/tests/e2e/test_deleted_node.py +35 -0
- instructware_tools-0.1.0/iwp_lint/tests/e2e/test_i18n_and_schema.py +61 -0
- instructware_tools-0.1.0/iwp_lint/tests/test_e2e_flow.py +8 -0
- instructware_tools-0.1.0/iwp_lint/tests/test_e2e_suite.py +17 -0
- instructware_tools-0.1.0/iwp_lint/tests/test_regression.py +768 -0
- instructware_tools-0.1.0/iwp_lint/vcs/__init__.py +0 -0
- instructware_tools-0.1.0/iwp_lint/vcs/diff_resolver.py +127 -0
- instructware_tools-0.1.0/iwp_lint/vcs/snapshot_store.py +180 -0
- instructware_tools-0.1.0/iwp_lint/vcs/task_store.py +116 -0
- instructware_tools-0.1.0/pyproject.toml +66 -0
- instructware_tools-0.1.0/schema/iwp-schema.v1.json +573 -0
- instructware_tools-0.1.0/test/README.md +40 -0
- instructware_tools-0.1.0/test/__init__.py +0 -0
- instructware_tools-0.1.0/test/bootstrap_first_build/.iwp-lint.yaml +27 -0
- instructware_tools-0.1.0/test/bootstrap_first_build/InstructWare.iw/architecture.md +4 -0
- instructware_tools-0.1.0/test/bootstrap_first_build/_ir/src/iwp_links.ts +0 -0
- instructware_tools-0.1.0/test/bootstrap_first_build/expected/README.md +1 -0
- instructware_tools-0.1.0/test/bootstrap_no_baseline_no_links/.iwp-lint.yaml +27 -0
- instructware_tools-0.1.0/test/bootstrap_no_baseline_no_links/InstructWare.iw/architecture.md +5 -0
- instructware_tools-0.1.0/test/bootstrap_no_baseline_no_links/_ir/src/iwp_links.ts +1 -0
- instructware_tools-0.1.0/test/bootstrap_no_baseline_no_links/expected/README.md +1 -0
- instructware_tools-0.1.0/test/code_only_change/.iwp-lint.yaml +27 -0
- instructware_tools-0.1.0/test/code_only_change/InstructWare.iw/architecture.md +4 -0
- instructware_tools-0.1.0/test/code_only_change/_ir/src/iwp_links.ts +0 -0
- instructware_tools-0.1.0/test/code_only_change/expected/README.md +1 -0
- instructware_tools-0.1.0/test/compiled_stale_or_missing/.iwp-lint.yaml +27 -0
- instructware_tools-0.1.0/test/compiled_stale_or_missing/InstructWare.iw/architecture.md +4 -0
- instructware_tools-0.1.0/test/compiled_stale_or_missing/_ir/src/iwp_links.ts +0 -0
- instructware_tools-0.1.0/test/compiled_stale_or_missing/expected/README.md +1 -0
- instructware_tools-0.1.0/test/feature_add_node/.iwp-lint.yaml +27 -0
- instructware_tools-0.1.0/test/feature_add_node/InstructWare.iw/architecture.md +4 -0
- instructware_tools-0.1.0/test/feature_add_node/_ir/src/iwp_links.ts +0 -0
- instructware_tools-0.1.0/test/feature_add_node/expected/README.md +1 -0
- instructware_tools-0.1.0/test/feature_delete_node/.iwp-lint.yaml +27 -0
- instructware_tools-0.1.0/test/feature_delete_node/InstructWare.iw/architecture.md +5 -0
- instructware_tools-0.1.0/test/feature_delete_node/_ir/src/iwp_links.ts +0 -0
- instructware_tools-0.1.0/test/feature_delete_node/expected/README.md +1 -0
- instructware_tools-0.1.0/test/feature_modify_node/.iwp-lint.yaml +27 -0
- instructware_tools-0.1.0/test/feature_modify_node/InstructWare.iw/architecture.md +4 -0
- instructware_tools-0.1.0/test/feature_modify_node/_ir/src/iwp_links.ts +0 -0
- instructware_tools-0.1.0/test/feature_modify_node/expected/README.md +1 -0
- instructware_tools-0.1.0/test/helpers.py +222 -0
- instructware_tools-0.1.0/test/i18n_zh_en/.iwp-lint.yaml +27 -0
- instructware_tools-0.1.0/test/i18n_zh_en/InstructWare.iw/views/pages/home.md +4 -0
- instructware_tools-0.1.0/test/i18n_zh_en/_ir/src/iwp_links.ts +0 -0
- instructware_tools-0.1.0/test/i18n_zh_en/expected/README.md +1 -0
- instructware_tools-0.1.0/test/schema/test-schema.i18n.min.json +46 -0
- instructware_tools-0.1.0/test/schema/test-schema.min.json +52 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 InstructWare
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: instructware-tools
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI tools for InstructWare protocol linting and incremental build workflows.
|
|
5
|
+
Project-URL: Homepage, https://github.com/InstructWare/iwp-tools
|
|
6
|
+
Project-URL: Repository, https://github.com/InstructWare/iwp-tools
|
|
7
|
+
Project-URL: Issues, https://github.com/InstructWare/iwp-tools/issues
|
|
8
|
+
Author: InstructWare Maintainers
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Requires-Dist: pyyaml>=6.0.1
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# iwp-tools
|
|
16
|
+
|
|
17
|
+
`iwp-tools` is the standalone toolkit repository for InstructWare protocol workflows.
|
|
18
|
+
|
|
19
|
+
It provides two CLI commands:
|
|
20
|
+
|
|
21
|
+
- `iwp-lint`: schema/link/coverage quality checks
|
|
22
|
+
- `iwp-build`: incremental build orchestration on top of `iwp-lint`
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pipx install instructware-tools
|
|
28
|
+
iwp-lint --help
|
|
29
|
+
iwp-build --help
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Local development
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
uv sync --group dev
|
|
36
|
+
uv run ruff check .
|
|
37
|
+
uv run ruff format --check .
|
|
38
|
+
uv run pyright iwp_lint iwp_build test
|
|
39
|
+
uv run python -m unittest iwp_lint.tests.test_regression
|
|
40
|
+
uv run python -m unittest iwp_build.tests.test_e2e_suite
|
|
41
|
+
uv run python -m unittest iwp_lint.tests.test_e2e_suite
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Build releases
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
uv build
|
|
48
|
+
uv run pyinstaller --onefile --name iwp-lint iwp_lint/__main__.py
|
|
49
|
+
uv run pyinstaller --onefile --name iwp-build iwp_build/__main__.py
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## License
|
|
53
|
+
|
|
54
|
+
This repository is licensed under MIT. See [`LICENSE`](./LICENSE).
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# iwp-tools
|
|
2
|
+
|
|
3
|
+
`iwp-tools` is the standalone toolkit repository for InstructWare protocol workflows.
|
|
4
|
+
|
|
5
|
+
It provides two CLI commands:
|
|
6
|
+
|
|
7
|
+
- `iwp-lint`: schema/link/coverage quality checks
|
|
8
|
+
- `iwp-build`: incremental build orchestration on top of `iwp-lint`
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pipx install instructware-tools
|
|
14
|
+
iwp-lint --help
|
|
15
|
+
iwp-build --help
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Local development
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
uv sync --group dev
|
|
22
|
+
uv run ruff check .
|
|
23
|
+
uv run ruff format --check .
|
|
24
|
+
uv run pyright iwp_lint iwp_build test
|
|
25
|
+
uv run python -m unittest iwp_lint.tests.test_regression
|
|
26
|
+
uv run python -m unittest iwp_build.tests.test_e2e_suite
|
|
27
|
+
uv run python -m unittest iwp_lint.tests.test_e2e_suite
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Build releases
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
uv build
|
|
34
|
+
uv run pyinstaller --onefile --name iwp-lint iwp_lint/__main__.py
|
|
35
|
+
uv run pyinstaller --onefile --name iwp-build iwp_build/__main__.py
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## License
|
|
39
|
+
|
|
40
|
+
This repository is licensed under MIT. See [`LICENSE`](./LICENSE).
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# iwp-build
|
|
2
|
+
|
|
3
|
+
`iwp-build` is the orchestrator layer for IWP workflows.
|
|
4
|
+
|
|
5
|
+
It provides the manual build checkpoint and reuses `iwp_lint` as the quality/diff engine.
|
|
6
|
+
|
|
7
|
+
## Development Setup
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
uv sync --group dev
|
|
11
|
+
uv run iwp-build --help
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Responsibilities
|
|
15
|
+
|
|
16
|
+
- orchestrate workflow entrypoints (`build`, `verify`, `watch`)
|
|
17
|
+
- provide a manual build checkpoint for intent diff and implementation gap output
|
|
18
|
+
- call `iwp_lint` library API directly (no subprocess dependency)
|
|
19
|
+
|
|
20
|
+
Non-goals:
|
|
21
|
+
|
|
22
|
+
- does not re-implement lint/schema/coverage logic
|
|
23
|
+
- does not replace agent runtime or code generation engine
|
|
24
|
+
|
|
25
|
+
## Commands
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
uv run iwp-build build --config .iwp-lint.yaml
|
|
29
|
+
uv run iwp-build build --config .iwp-lint.yaml --mode diff --json out/iwp-build.json --diff-json out/iwp-diff.json
|
|
30
|
+
uv run iwp-build verify --config .iwp-lint.yaml --run-tests
|
|
31
|
+
uv run iwp-build watch --config .iwp-lint.yaml --verify
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Backward-compatible module entry still works:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
python -m iwp_build build --config .iwp-lint.yaml
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Workflow
|
|
41
|
+
|
|
42
|
+
1. `build`: compile `.iwc`, compute intent diff (markdown delta), then compute implementation gap (link/coverage diagnostics)
|
|
43
|
+
2. agent uses the compact diff output as implementation hints and edits code
|
|
44
|
+
3. `verify`: run compiled checks, full lint gate, and optional regression tests
|
|
45
|
+
4. `watch` (optional local loop): incremental `.iwc` compile only; not a workflow checkpoint
|
|
46
|
+
|
|
47
|
+
## Integration with iwp_lint API
|
|
48
|
+
|
|
49
|
+
`iwp-build` uses these APIs from `iwp_lint/api.py`:
|
|
50
|
+
|
|
51
|
+
- `snapshot_action(...)`
|
|
52
|
+
- `run_quality_gate(...)`
|
|
53
|
+
- `compile_context(...)`
|
|
54
|
+
- `verify_compiled(...)`
|
|
55
|
+
|
|
56
|
+
This design keeps one source of truth for lint and snapshot semantics.
|
|
57
|
+
|
|
58
|
+
## Watch Mode (Hot Compile for `.iwc`)
|
|
59
|
+
|
|
60
|
+
`iwp-build watch` is a local developer loop (optional):
|
|
61
|
+
|
|
62
|
+
- starts with one full `.iwc` compile
|
|
63
|
+
- polls markdown and control files
|
|
64
|
+
- batches changes with debounce
|
|
65
|
+
- recompiles only impacted markdown sources
|
|
66
|
+
- targets `.iwc v2` dual artifacts (`.iwp/compiled/json/**` + `.iwp/compiled/md/**`)
|
|
67
|
+
- can optionally verify artifacts and run tests after each cycle
|
|
68
|
+
- does not generate workflow tasks or baseline checkpoints
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
uv run iwp-build watch --config .iwp-lint.yaml --debounce-ms 600 --verify
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Suggested CI Usage
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
uv run iwp-build build --config .iwp-lint.yaml --json out/iwp-build.json --diff-json out/iwp-diff.json
|
|
82
|
+
# agent applies changes based on out/iwp-diff.json
|
|
83
|
+
uv run iwp-build verify --config .iwp-lint.yaml --run-tests
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Output notes:
|
|
87
|
+
|
|
88
|
+
- `--json` writes the full checkpoint payload (`summary`, `compile`, `intent_diff`, `gap_report`)
|
|
89
|
+
- `--diff-json` writes a compact payload for agent implementation loops (`summary`, `intent_diff`, `gap_report.diagnostics`, `gap_report.nodes`)
|
|
90
|
+
|
|
91
|
+
## E2E Scenarios
|
|
92
|
+
|
|
93
|
+
Build e2e tests are fixture-driven and map to agent flow checkpoints:
|
|
94
|
+
|
|
95
|
+
- shared fixtures: `test/<scenario>/`
|
|
96
|
+
- e2e suite entrypoint: `iwp_build/tests/test_e2e_suite.py`
|
|
97
|
+
- compatibility wrapper: `iwp_build/tests/test_e2e_flow.py`
|
|
98
|
+
|
|
99
|
+
Covered flows:
|
|
100
|
+
|
|
101
|
+
- feature add node: build fails before link patch, then passes after `@iwp.link` update
|
|
102
|
+
- feature delete node: stale link fails verify, cleanup + rebuild restores green state
|
|
103
|
+
- feature modify node: impacted nodes detected in diff, link update required
|
|
104
|
+
- bootstrap without baseline and without links: first build fails, patch links, second build initializes baseline
|
|
105
|
+
- bootstrap first build: `--mode auto` enters `bootstrap_full` and initializes baseline
|
|
106
|
+
|
|
107
|
+
Schema profile matrix:
|
|
108
|
+
|
|
109
|
+
- every build e2e scenario runs both:
|
|
110
|
+
- `minimal` profile (shared test schema under `test/schema/`)
|
|
111
|
+
- `official` profile (`schema/iwp-schema.v1.json`)
|
|
112
|
+
- tests rewrite fixture markdown as needed per profile to keep business intent assertions stable.
|
|
113
|
+
|
|
114
|
+
Run only build e2e:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
uv run python -m unittest iwp_build.tests.test_e2e_suite
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Migration from Legacy Commands
|
|
121
|
+
|
|
122
|
+
The old task-oriented commands were removed to keep the tool surface minimal.
|
|
123
|
+
|
|
124
|
+
- `iwp-build snapshot init|update|diff` -> `iwp-build build`
|
|
125
|
+
- `iwp-build plan` -> `iwp-build build`
|
|
126
|
+
- `iwp-build apply` -> removed (no task status workflow)
|
|
127
|
+
- `iwp-build verify --id <task_id>` -> `iwp-build verify`
|
|
128
|
+
|
|
129
|
+
Design change:
|
|
130
|
+
|
|
131
|
+
- `build` is now the manual checkpoint for intent diff + implementation gap.
|
|
132
|
+
- `watch` remains optional acceleration and does not define workflow checkpoints.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
import unittest
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
|
|
9
|
+
from iwp_build.watch import run_watch
|
|
10
|
+
from iwp_lint.api import (
|
|
11
|
+
compile_context,
|
|
12
|
+
run_quality_gate,
|
|
13
|
+
snapshot_action,
|
|
14
|
+
verify_compiled,
|
|
15
|
+
)
|
|
16
|
+
from iwp_lint.config import load_config
|
|
17
|
+
from iwp_lint.core.engine import print_console_report, run_diff, run_full
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
21
|
+
parser = argparse.ArgumentParser(
|
|
22
|
+
prog="iwp-build", description="IWP incremental build orchestrator"
|
|
23
|
+
)
|
|
24
|
+
parser.add_argument("--config", help="Path to .iwp-lint.yaml or .json", default=None)
|
|
25
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
26
|
+
|
|
27
|
+
build_cmd = sub.add_parser(
|
|
28
|
+
"build", help="Compile .iwc and compute incremental implementation gap"
|
|
29
|
+
)
|
|
30
|
+
build_cmd.add_argument("--config", help="Path to .iwp-lint.yaml or .json", default=None)
|
|
31
|
+
build_cmd.add_argument(
|
|
32
|
+
"--mode",
|
|
33
|
+
choices=["auto", "diff", "full"],
|
|
34
|
+
default="auto",
|
|
35
|
+
help="Build mode: auto tries diff first and falls back to full on first run",
|
|
36
|
+
)
|
|
37
|
+
build_cmd.add_argument("--json", help="Write build summary JSON to path", default=None)
|
|
38
|
+
build_cmd.add_argument(
|
|
39
|
+
"--diff-json",
|
|
40
|
+
help="Write compact diff payload for agent consumption",
|
|
41
|
+
default=None,
|
|
42
|
+
)
|
|
43
|
+
build_cmd.set_defaults(command="build")
|
|
44
|
+
|
|
45
|
+
verify_cmd = sub.add_parser("verify", help="Run compiled check + lint gate (+ tests)")
|
|
46
|
+
verify_cmd.add_argument("--config", help="Path to .iwp-lint.yaml or .json", default=None)
|
|
47
|
+
verify_cmd.add_argument("--run-tests", action="store_true", help="Run regression tests")
|
|
48
|
+
verify_cmd.set_defaults(command="verify")
|
|
49
|
+
|
|
50
|
+
watch_cmd = sub.add_parser(
|
|
51
|
+
"watch", help="Watch markdown changes and rebuild .iwc incrementally"
|
|
52
|
+
)
|
|
53
|
+
watch_cmd.add_argument("--config", help="Path to .iwp-lint.yaml or .json", default=None)
|
|
54
|
+
watch_cmd.add_argument(
|
|
55
|
+
"--debounce-ms",
|
|
56
|
+
type=int,
|
|
57
|
+
default=600,
|
|
58
|
+
help="Debounce window for change batching",
|
|
59
|
+
)
|
|
60
|
+
watch_cmd.add_argument(
|
|
61
|
+
"--poll-ms",
|
|
62
|
+
type=int,
|
|
63
|
+
default=250,
|
|
64
|
+
help="Polling interval for file change detection",
|
|
65
|
+
)
|
|
66
|
+
watch_cmd.add_argument(
|
|
67
|
+
"--verify",
|
|
68
|
+
action="store_true",
|
|
69
|
+
help="Verify compiled artifacts after each build",
|
|
70
|
+
)
|
|
71
|
+
watch_cmd.add_argument(
|
|
72
|
+
"--run-tests",
|
|
73
|
+
action="store_true",
|
|
74
|
+
help="Run regression tests after each successful build",
|
|
75
|
+
)
|
|
76
|
+
watch_cmd.add_argument(
|
|
77
|
+
"--once",
|
|
78
|
+
action="store_true",
|
|
79
|
+
help="Run one compile cycle and exit",
|
|
80
|
+
)
|
|
81
|
+
watch_cmd.set_defaults(command="watch")
|
|
82
|
+
|
|
83
|
+
return parser
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def main(argv: list[str] | None = None) -> int:
|
|
87
|
+
parser = build_parser()
|
|
88
|
+
args = parser.parse_args(argv)
|
|
89
|
+
config = load_config(args.config)
|
|
90
|
+
|
|
91
|
+
if args.command == "build":
|
|
92
|
+
return _run_build(
|
|
93
|
+
config=config,
|
|
94
|
+
mode=args.mode,
|
|
95
|
+
json_path=args.json,
|
|
96
|
+
diff_json_path=args.diff_json,
|
|
97
|
+
)
|
|
98
|
+
if args.command == "verify":
|
|
99
|
+
return _run_verify(config, args.run_tests)
|
|
100
|
+
if args.command == "watch":
|
|
101
|
+
return run_watch(
|
|
102
|
+
config=config,
|
|
103
|
+
config_file=args.config,
|
|
104
|
+
debounce_ms=args.debounce_ms,
|
|
105
|
+
poll_ms=args.poll_ms,
|
|
106
|
+
verify=args.verify,
|
|
107
|
+
run_tests=args.run_tests,
|
|
108
|
+
once=args.once,
|
|
109
|
+
compile_fn=compile_context,
|
|
110
|
+
verify_fn=verify_compiled,
|
|
111
|
+
)
|
|
112
|
+
raise RuntimeError(f"unknown command: {args.command}")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _run_build(
|
|
116
|
+
config,
|
|
117
|
+
mode: str,
|
|
118
|
+
json_path: str | None,
|
|
119
|
+
diff_json_path: str | None = None,
|
|
120
|
+
) -> int:
|
|
121
|
+
compile_result = compile_context(config)
|
|
122
|
+
build_mode = mode
|
|
123
|
+
needs_bootstrap_init = False
|
|
124
|
+
intent = {
|
|
125
|
+
"changed_files": [],
|
|
126
|
+
"changed_md_files": [],
|
|
127
|
+
"changed_code_files": [],
|
|
128
|
+
"changed_count": 0,
|
|
129
|
+
"impacted_nodes": [],
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if mode in {"auto", "diff"}:
|
|
133
|
+
try:
|
|
134
|
+
intent = snapshot_action(config, "diff")
|
|
135
|
+
gap_report = run_diff(config, None, None)
|
|
136
|
+
build_mode = "diff"
|
|
137
|
+
except RuntimeError as exc:
|
|
138
|
+
if mode == "diff":
|
|
139
|
+
raise
|
|
140
|
+
if "baseline not found" not in str(exc):
|
|
141
|
+
raise
|
|
142
|
+
gap_report = run_full(config)
|
|
143
|
+
build_mode = "bootstrap_full"
|
|
144
|
+
needs_bootstrap_init = True
|
|
145
|
+
else:
|
|
146
|
+
gap_report = run_full(config)
|
|
147
|
+
build_mode = "full"
|
|
148
|
+
|
|
149
|
+
gap_error_count = int(gap_report["summary"]["error_count"])
|
|
150
|
+
baseline_bootstrapped = needs_bootstrap_init and gap_error_count == 0
|
|
151
|
+
summary = {
|
|
152
|
+
"build_mode": build_mode,
|
|
153
|
+
"baseline_bootstrapped": baseline_bootstrapped,
|
|
154
|
+
"compiled_count": int(compile_result.get("compiled_count", 0)),
|
|
155
|
+
"removed_count": int(compile_result.get("removed_count", 0)),
|
|
156
|
+
"changed_count": int(intent.get("changed_count", 0)),
|
|
157
|
+
"changed_md_count": _safe_len(intent.get("changed_md_files")),
|
|
158
|
+
"impacted_nodes_count": _safe_len(intent.get("impacted_nodes")),
|
|
159
|
+
"gap_error_count": gap_error_count,
|
|
160
|
+
"gap_warning_count": int(gap_report["summary"]["warning_count"]),
|
|
161
|
+
}
|
|
162
|
+
print(
|
|
163
|
+
"[iwp-build] build "
|
|
164
|
+
f"mode={summary['build_mode']} "
|
|
165
|
+
f"compiled={summary['compiled_count']} removed={summary['removed_count']} "
|
|
166
|
+
f"changed={summary['changed_count']} impacted_nodes={summary['impacted_nodes_count']} "
|
|
167
|
+
f"gap_errors={summary['gap_error_count']}"
|
|
168
|
+
)
|
|
169
|
+
print_console_report(gap_report)
|
|
170
|
+
full_payload = {
|
|
171
|
+
"summary": summary,
|
|
172
|
+
"compile": compile_result,
|
|
173
|
+
"intent_diff": intent,
|
|
174
|
+
"gap_report": gap_report,
|
|
175
|
+
}
|
|
176
|
+
written_json_path = _write_json(
|
|
177
|
+
json_path,
|
|
178
|
+
full_payload,
|
|
179
|
+
)
|
|
180
|
+
if written_json_path is not None:
|
|
181
|
+
print(
|
|
182
|
+
"[iwp-build] build json "
|
|
183
|
+
f"path={written_json_path} "
|
|
184
|
+
f"changed_md={summary['changed_md_count']} "
|
|
185
|
+
f"impacted_nodes={summary['impacted_nodes_count']} "
|
|
186
|
+
f"gap_errors={summary['gap_error_count']}"
|
|
187
|
+
)
|
|
188
|
+
written_diff_json_path = _write_json(
|
|
189
|
+
diff_json_path,
|
|
190
|
+
_to_compact_diff_payload(summary=summary, intent=intent, gap_report=gap_report),
|
|
191
|
+
)
|
|
192
|
+
if written_diff_json_path is not None:
|
|
193
|
+
print(
|
|
194
|
+
"[iwp-build] build diff json "
|
|
195
|
+
f"path={written_diff_json_path} "
|
|
196
|
+
f"changed_md={summary['changed_md_count']} "
|
|
197
|
+
f"impacted_nodes={summary['impacted_nodes_count']} "
|
|
198
|
+
f"gap_errors={summary['gap_error_count']}"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
if gap_error_count > 0:
|
|
202
|
+
print("[iwp-build] build failed; keep previous baseline unchanged")
|
|
203
|
+
return 1
|
|
204
|
+
|
|
205
|
+
# Build completion is the manual checkpoint; only commit snapshot when gap checks pass.
|
|
206
|
+
if needs_bootstrap_init:
|
|
207
|
+
snapshot_action(config, "init")
|
|
208
|
+
else:
|
|
209
|
+
snapshot_action(config, "update")
|
|
210
|
+
return 0
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _run_verify(config, run_tests: bool) -> int:
|
|
214
|
+
compiled = verify_compiled(config)
|
|
215
|
+
if not bool(compiled.get("ok", False)):
|
|
216
|
+
print(f"[iwp-build] verify compiled checked={compiled.get('checked_sources', 0)} ok=False")
|
|
217
|
+
return 1
|
|
218
|
+
|
|
219
|
+
gate = run_quality_gate(config)
|
|
220
|
+
print_console_report(gate["lint_report"])
|
|
221
|
+
if gate["lint_exit_code"] != 0:
|
|
222
|
+
return 1
|
|
223
|
+
|
|
224
|
+
if run_tests:
|
|
225
|
+
suite = unittest.defaultTestLoader.loadTestsFromName("iwp_lint.tests.test_regression")
|
|
226
|
+
result = unittest.TextTestRunner(stream=sys.stdout, verbosity=1).run(suite)
|
|
227
|
+
if not result.wasSuccessful():
|
|
228
|
+
return 1
|
|
229
|
+
|
|
230
|
+
print("[iwp-build] verify ok")
|
|
231
|
+
return 0
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _safe_len(value: object) -> int:
|
|
235
|
+
return len(value) if isinstance(value, list) else 0
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _to_compact_diff_payload(
|
|
239
|
+
*,
|
|
240
|
+
summary: dict[str, object],
|
|
241
|
+
intent: dict[str, object],
|
|
242
|
+
gap_report: dict[str, object],
|
|
243
|
+
) -> dict[str, object]:
|
|
244
|
+
raw_gap_summary = gap_report.get("summary", {})
|
|
245
|
+
gap_summary: Mapping[str, object]
|
|
246
|
+
if isinstance(raw_gap_summary, Mapping):
|
|
247
|
+
gap_summary = raw_gap_summary
|
|
248
|
+
else:
|
|
249
|
+
gap_summary = {}
|
|
250
|
+
return {
|
|
251
|
+
"summary": {
|
|
252
|
+
"build_mode": summary.get("build_mode"),
|
|
253
|
+
"changed_md_count": summary.get("changed_md_count"),
|
|
254
|
+
"impacted_nodes_count": summary.get("impacted_nodes_count"),
|
|
255
|
+
"gap_error_count": summary.get("gap_error_count"),
|
|
256
|
+
"gap_warning_count": summary.get("gap_warning_count"),
|
|
257
|
+
},
|
|
258
|
+
"intent_diff": {
|
|
259
|
+
"changed_md_files": intent.get("changed_md_files", []),
|
|
260
|
+
"impacted_nodes": intent.get("impacted_nodes", []),
|
|
261
|
+
},
|
|
262
|
+
"gap_report": {
|
|
263
|
+
"mode": gap_report.get("mode"),
|
|
264
|
+
"summary": {
|
|
265
|
+
"error_count": gap_summary.get("error_count", 0),
|
|
266
|
+
"warning_count": gap_summary.get("warning_count", 0),
|
|
267
|
+
"total_nodes_in_scope": gap_summary.get("total_nodes_in_scope", 0),
|
|
268
|
+
"total_nodes_all": gap_summary.get("total_nodes_all", 0),
|
|
269
|
+
"covered_nodes": gap_summary.get("covered_nodes", 0),
|
|
270
|
+
},
|
|
271
|
+
"diagnostics": gap_report.get("diagnostics", []),
|
|
272
|
+
"nodes": gap_report.get("nodes", []),
|
|
273
|
+
},
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _write_json(path: str | None, payload: dict[str, object]) -> str | None:
|
|
278
|
+
if not path:
|
|
279
|
+
return None
|
|
280
|
+
from pathlib import Path
|
|
281
|
+
|
|
282
|
+
out_path = Path(path)
|
|
283
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
284
|
+
out_path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
285
|
+
return out_path.as_posix()
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
if __name__ == "__main__":
|
|
289
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import unittest
|
|
4
|
+
|
|
5
|
+
from test.helpers import (
|
|
6
|
+
SCHEMA_PROFILES,
|
|
7
|
+
apply_schema_profile,
|
|
8
|
+
copy_scenario_to_workspace,
|
|
9
|
+
latest_snapshot_id,
|
|
10
|
+
read_json,
|
|
11
|
+
run_build,
|
|
12
|
+
write_architecture_markdown,
|
|
13
|
+
write_links_for_source,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BootstrapNoBaselineNoLinksBuildE2E(unittest.TestCase):
|
|
18
|
+
def _assert_ok(self, result, label: str) -> None:
|
|
19
|
+
self.assertEqual(
|
|
20
|
+
result.returncode,
|
|
21
|
+
0,
|
|
22
|
+
msg=f"{label} failed\nstdout:\n{result.stdout}\nstderr:\n{result.stderr}",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def test_bootstrap_fail_then_patch_links_then_verify(self) -> None:
|
|
26
|
+
for profile in SCHEMA_PROFILES:
|
|
27
|
+
with self.subTest(schema_profile=profile):
|
|
28
|
+
tempdir, workspace = copy_scenario_to_workspace("bootstrap_no_baseline_no_links")
|
|
29
|
+
self.addCleanup(tempdir.cleanup)
|
|
30
|
+
config_path = workspace / ".iwp-lint.yaml"
|
|
31
|
+
out_dir = workspace / "out"
|
|
32
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
apply_schema_profile(config_path, profile)
|
|
34
|
+
write_architecture_markdown(workspace, profile, ["Alpha", "Beta"])
|
|
35
|
+
|
|
36
|
+
self.assertIsNone(latest_snapshot_id(config_path))
|
|
37
|
+
fail_result = run_build(
|
|
38
|
+
[
|
|
39
|
+
"build",
|
|
40
|
+
"--config",
|
|
41
|
+
str(config_path),
|
|
42
|
+
"--mode",
|
|
43
|
+
"auto",
|
|
44
|
+
"--json",
|
|
45
|
+
str(out_dir / "build_fail.json"),
|
|
46
|
+
]
|
|
47
|
+
)
|
|
48
|
+
self.assertEqual(fail_result.returncode, 1)
|
|
49
|
+
fail_payload = read_json(out_dir / "build_fail.json")
|
|
50
|
+
self.assertEqual(fail_payload["summary"]["build_mode"], "bootstrap_full")
|
|
51
|
+
self.assertFalse(fail_payload["summary"]["baseline_bootstrapped"])
|
|
52
|
+
self.assertGreater(fail_payload["summary"]["gap_error_count"], 0)
|
|
53
|
+
self.assertIsNone(latest_snapshot_id(config_path))
|
|
54
|
+
|
|
55
|
+
write_links_for_source(workspace, "architecture.md")
|
|
56
|
+
pass_result = run_build(
|
|
57
|
+
[
|
|
58
|
+
"build",
|
|
59
|
+
"--config",
|
|
60
|
+
str(config_path),
|
|
61
|
+
"--mode",
|
|
62
|
+
"auto",
|
|
63
|
+
"--json",
|
|
64
|
+
str(out_dir / "build_pass.json"),
|
|
65
|
+
]
|
|
66
|
+
)
|
|
67
|
+
self._assert_ok(pass_result, f"bootstrap after link patch ({profile})")
|
|
68
|
+
pass_payload = read_json(out_dir / "build_pass.json")
|
|
69
|
+
self.assertEqual(pass_payload["summary"]["build_mode"], "bootstrap_full")
|
|
70
|
+
self.assertTrue(pass_payload["summary"]["baseline_bootstrapped"])
|
|
71
|
+
self.assertEqual(pass_payload["summary"]["gap_error_count"], 0)
|
|
72
|
+
self.assertIsNotNone(latest_snapshot_id(config_path))
|
|
73
|
+
self._assert_ok(
|
|
74
|
+
run_build(["verify", "--config", str(config_path)]),
|
|
75
|
+
f"verify after bootstrap ({profile})",
|
|
76
|
+
)
|