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.
Files changed (104) hide show
  1. instructware_tools-0.1.0/.gitignore +17 -0
  2. instructware_tools-0.1.0/LICENSE +21 -0
  3. instructware_tools-0.1.0/PKG-INFO +54 -0
  4. instructware_tools-0.1.0/README.md +40 -0
  5. instructware_tools-0.1.0/iwp_build/README.md +132 -0
  6. instructware_tools-0.1.0/iwp_build/__init__.py +1 -0
  7. instructware_tools-0.1.0/iwp_build/__main__.py +4 -0
  8. instructware_tools-0.1.0/iwp_build/cli.py +289 -0
  9. instructware_tools-0.1.0/iwp_build/tests/__init__.py +1 -0
  10. instructware_tools-0.1.0/iwp_build/tests/e2e/__init__.py +1 -0
  11. instructware_tools-0.1.0/iwp_build/tests/e2e/test_bootstrap_no_baseline_no_links.py +76 -0
  12. instructware_tools-0.1.0/iwp_build/tests/e2e/test_bootstrap_official_schema.py +49 -0
  13. instructware_tools-0.1.0/iwp_build/tests/e2e/test_feature_add_node.py +114 -0
  14. instructware_tools-0.1.0/iwp_build/tests/e2e/test_feature_delete_node.py +99 -0
  15. instructware_tools-0.1.0/iwp_build/tests/e2e/test_feature_modify_node.py +100 -0
  16. instructware_tools-0.1.0/iwp_build/tests/test_build_commit.py +113 -0
  17. instructware_tools-0.1.0/iwp_build/tests/test_e2e_flow.py +8 -0
  18. instructware_tools-0.1.0/iwp_build/tests/test_e2e_suite.py +24 -0
  19. instructware_tools-0.1.0/iwp_build/tests/test_watch.py +54 -0
  20. instructware_tools-0.1.0/iwp_build/watch.py +188 -0
  21. instructware_tools-0.1.0/iwp_lint/ARCHITECTURE.md +300 -0
  22. instructware_tools-0.1.0/iwp_lint/README.md +236 -0
  23. instructware_tools-0.1.0/iwp_lint/__init__.py +4 -0
  24. instructware_tools-0.1.0/iwp_lint/__main__.py +4 -0
  25. instructware_tools-0.1.0/iwp_lint/_bundled_schema/__init__.py +1 -0
  26. instructware_tools-0.1.0/iwp_lint/_bundled_schema/iwp-schema.v1.json +573 -0
  27. instructware_tools-0.1.0/iwp_lint/api.py +119 -0
  28. instructware_tools-0.1.0/iwp_lint/cli.py +265 -0
  29. instructware_tools-0.1.0/iwp_lint/comment_scanner.py +104 -0
  30. instructware_tools-0.1.0/iwp_lint/config.py +138 -0
  31. instructware_tools-0.1.0/iwp_lint/core/__init__.py +0 -0
  32. instructware_tools-0.1.0/iwp_lint/core/engine.py +375 -0
  33. instructware_tools-0.1.0/iwp_lint/core/errors.py +15 -0
  34. instructware_tools-0.1.0/iwp_lint/core/models.py +47 -0
  35. instructware_tools-0.1.0/iwp_lint/core/node_catalog.py +799 -0
  36. instructware_tools-0.1.0/iwp_lint/diff_resolver.py +21 -0
  37. instructware_tools-0.1.0/iwp_lint/engine.py +366 -0
  38. instructware_tools-0.1.0/iwp_lint/errors.py +15 -0
  39. instructware_tools-0.1.0/iwp_lint/md_parser.py +179 -0
  40. instructware_tools-0.1.0/iwp_lint/models.py +47 -0
  41. instructware_tools-0.1.0/iwp_lint/parsers/__init__.py +0 -0
  42. instructware_tools-0.1.0/iwp_lint/parsers/comment_scanner.py +104 -0
  43. instructware_tools-0.1.0/iwp_lint/parsers/markdown_outline.py +33 -0
  44. instructware_tools-0.1.0/iwp_lint/parsers/md_parser.py +192 -0
  45. instructware_tools-0.1.0/iwp_lint/parsers/node_registry.py +193 -0
  46. instructware_tools-0.1.0/iwp_lint/schema/__init__.py +0 -0
  47. instructware_tools-0.1.0/iwp_lint/schema/schema_loader.py +65 -0
  48. instructware_tools-0.1.0/iwp_lint/schema/schema_models.py +45 -0
  49. instructware_tools-0.1.0/iwp_lint/schema/schema_semantics.py +45 -0
  50. instructware_tools-0.1.0/iwp_lint/schema/schema_validator.py +178 -0
  51. instructware_tools-0.1.0/iwp_lint/schema_loader.py +63 -0
  52. instructware_tools-0.1.0/iwp_lint/schema_models.py +45 -0
  53. instructware_tools-0.1.0/iwp_lint/schema_semantics.py +45 -0
  54. instructware_tools-0.1.0/iwp_lint/schema_validator.py +178 -0
  55. instructware_tools-0.1.0/iwp_lint/tests/e2e/__init__.py +1 -0
  56. instructware_tools-0.1.0/iwp_lint/tests/e2e/test_code_only_and_compiled.py +75 -0
  57. instructware_tools-0.1.0/iwp_lint/tests/e2e/test_deleted_node.py +35 -0
  58. instructware_tools-0.1.0/iwp_lint/tests/e2e/test_i18n_and_schema.py +61 -0
  59. instructware_tools-0.1.0/iwp_lint/tests/test_e2e_flow.py +8 -0
  60. instructware_tools-0.1.0/iwp_lint/tests/test_e2e_suite.py +17 -0
  61. instructware_tools-0.1.0/iwp_lint/tests/test_regression.py +768 -0
  62. instructware_tools-0.1.0/iwp_lint/vcs/__init__.py +0 -0
  63. instructware_tools-0.1.0/iwp_lint/vcs/diff_resolver.py +127 -0
  64. instructware_tools-0.1.0/iwp_lint/vcs/snapshot_store.py +180 -0
  65. instructware_tools-0.1.0/iwp_lint/vcs/task_store.py +116 -0
  66. instructware_tools-0.1.0/pyproject.toml +66 -0
  67. instructware_tools-0.1.0/schema/iwp-schema.v1.json +573 -0
  68. instructware_tools-0.1.0/test/README.md +40 -0
  69. instructware_tools-0.1.0/test/__init__.py +0 -0
  70. instructware_tools-0.1.0/test/bootstrap_first_build/.iwp-lint.yaml +27 -0
  71. instructware_tools-0.1.0/test/bootstrap_first_build/InstructWare.iw/architecture.md +4 -0
  72. instructware_tools-0.1.0/test/bootstrap_first_build/_ir/src/iwp_links.ts +0 -0
  73. instructware_tools-0.1.0/test/bootstrap_first_build/expected/README.md +1 -0
  74. instructware_tools-0.1.0/test/bootstrap_no_baseline_no_links/.iwp-lint.yaml +27 -0
  75. instructware_tools-0.1.0/test/bootstrap_no_baseline_no_links/InstructWare.iw/architecture.md +5 -0
  76. instructware_tools-0.1.0/test/bootstrap_no_baseline_no_links/_ir/src/iwp_links.ts +1 -0
  77. instructware_tools-0.1.0/test/bootstrap_no_baseline_no_links/expected/README.md +1 -0
  78. instructware_tools-0.1.0/test/code_only_change/.iwp-lint.yaml +27 -0
  79. instructware_tools-0.1.0/test/code_only_change/InstructWare.iw/architecture.md +4 -0
  80. instructware_tools-0.1.0/test/code_only_change/_ir/src/iwp_links.ts +0 -0
  81. instructware_tools-0.1.0/test/code_only_change/expected/README.md +1 -0
  82. instructware_tools-0.1.0/test/compiled_stale_or_missing/.iwp-lint.yaml +27 -0
  83. instructware_tools-0.1.0/test/compiled_stale_or_missing/InstructWare.iw/architecture.md +4 -0
  84. instructware_tools-0.1.0/test/compiled_stale_or_missing/_ir/src/iwp_links.ts +0 -0
  85. instructware_tools-0.1.0/test/compiled_stale_or_missing/expected/README.md +1 -0
  86. instructware_tools-0.1.0/test/feature_add_node/.iwp-lint.yaml +27 -0
  87. instructware_tools-0.1.0/test/feature_add_node/InstructWare.iw/architecture.md +4 -0
  88. instructware_tools-0.1.0/test/feature_add_node/_ir/src/iwp_links.ts +0 -0
  89. instructware_tools-0.1.0/test/feature_add_node/expected/README.md +1 -0
  90. instructware_tools-0.1.0/test/feature_delete_node/.iwp-lint.yaml +27 -0
  91. instructware_tools-0.1.0/test/feature_delete_node/InstructWare.iw/architecture.md +5 -0
  92. instructware_tools-0.1.0/test/feature_delete_node/_ir/src/iwp_links.ts +0 -0
  93. instructware_tools-0.1.0/test/feature_delete_node/expected/README.md +1 -0
  94. instructware_tools-0.1.0/test/feature_modify_node/.iwp-lint.yaml +27 -0
  95. instructware_tools-0.1.0/test/feature_modify_node/InstructWare.iw/architecture.md +4 -0
  96. instructware_tools-0.1.0/test/feature_modify_node/_ir/src/iwp_links.ts +0 -0
  97. instructware_tools-0.1.0/test/feature_modify_node/expected/README.md +1 -0
  98. instructware_tools-0.1.0/test/helpers.py +222 -0
  99. instructware_tools-0.1.0/test/i18n_zh_en/.iwp-lint.yaml +27 -0
  100. instructware_tools-0.1.0/test/i18n_zh_en/InstructWare.iw/views/pages/home.md +4 -0
  101. instructware_tools-0.1.0/test/i18n_zh_en/_ir/src/iwp_links.ts +0 -0
  102. instructware_tools-0.1.0/test/i18n_zh_en/expected/README.md +1 -0
  103. instructware_tools-0.1.0/test/schema/test-schema.i18n.min.json +46 -0
  104. instructware_tools-0.1.0/test/schema/test-schema.min.json +52 -0
@@ -0,0 +1,17 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.so
4
+
5
+ .venv/
6
+ .uv-cache/
7
+ .ruff_cache/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .tmp_iwp_e2e_tests/
11
+
12
+ dist/
13
+ build/
14
+ *.egg-info/
15
+ uv.lock
16
+
17
+ .DS_Store
@@ -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,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -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
+ )