chore(rules): refresh the vendored rules-swift to v2.14.0
This commit is contained in:
parent
ca65a334f7
commit
a078788ec9
@ -1 +1 @@
|
|||||||
1.1.2
|
2.14.0
|
||||||
|
|||||||
@ -27,7 +27,9 @@ intended behavior until the repo actually conforms.
|
|||||||
|
|
||||||
Rules corpus last read: 2026-06-30 (pinned snapshot `97dc70f`).
|
Rules corpus last read: 2026-06-30 (pinned snapshot `97dc70f`).
|
||||||
|
|
||||||
Rules corpus file count: 68.
|
Rules corpus file count: 88.
|
||||||
|
|
||||||
|
Rules corpus refreshed to rules-swift v2.14.0 (e624c8c) on 2026-11-02. New since v1.1.2: 21 rule files, listed in the tables below as NOT yet audited against this repository (a refresh of the pinned copy, not a conformance claim). The acceptance commands of both rules were run on `Sources` (372 Swift files searched) on 2026-11-02 and printed nothing. The rules' app-level tests (every pose, the fold on the simulator, the pane UI test) belong to an app and are not run and not claimed here. Changed since v1.1.2: `.github/pull_request_template.md`, `CHANGELOG.md`, `README.md`, `code-style.md`, `concurrency.md`, `core/README.md`, `core/brainstorming.md`, `core/file-naming.md`, `core/folder-grouping.md`, `core/writing-plans.md`, `cross-platform.md`, `dependency-injection.md`, `documentation.md`, `exp/README.md`, `exp/app-target.md`, `exp/build-performance.md`, `exp/common-mistakes.md`, `exp/critical-rules.md`, `exp/dependency-management.md`, `exp/implementation-patterns.md`, `exp/migration.md`, `exp/package-swift.md`, `exp/testing.md`, `exp/verification.md`, `exp/when-to-create.md`, `framework-policy.md`, `linux-server.md`, `namespacing.md`, `package-architecture.md`, `package-import-contract.md`, `package-structure.md`, `parsing-rules.md`, `scripts/check-namespacing.sh`, `scripts/leak-gate.sh`, `shared-protocols.md`, `testing.md`, `ui/appkit-views.md`, `ui/components.md`, `ui/flowspec.md`, `ui/pom.md`, `ui/swiftui-views.md`, `ui/uikit-views.md`, `ui/view-models.md` (see the canon's CHANGELOG); none adds or alters an acceptance command this repository runs. The files new since the last full read are listed and not yet audited against this repository.
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Every Markdown or shell file in the pinned rules-swift corpus MUST appear as a
|
Every Markdown or shell file in the pinned rules-swift corpus MUST appear as a
|
||||||
@ -113,6 +115,27 @@ be silently omitted.
|
|||||||
| `ui/uikit-views.md` | Vendored, pinned |
|
| `ui/uikit-views.md` | Vendored, pinned |
|
||||||
| `ui/view-models.md` | Vendored, pinned |
|
| `ui/view-models.md` | Vendored, pinned |
|
||||||
| `validation-rules.md` | Vendored, pinned |
|
| `validation-rules.md` | Vendored, pinned |
|
||||||
|
| `accessibility.md` | Listed (new since v1.1.2; NOT yet audited against this repository) |
|
||||||
|
| `app-intents.md` | Listed (new since v1.1.2; NOT yet audited against this repository) |
|
||||||
|
| `app-logging.md` | Listed (new since v1.1.2; NOT yet audited against this repository) |
|
||||||
|
| `conventions.md` | Listed (new since v1.1.2; NOT yet audited against this repository) |
|
||||||
|
| `crash-and-state-reporting.md` | Listed (new since v1.1.2; NOT yet audited against this repository) |
|
||||||
|
| `help.md` | Listed (new since v1.1.2; NOT yet audited against this repository) |
|
||||||
|
| `iphone-duo.md` | Listed (acceptance commands run on Sources, all empty; app-level tests not run, no app here) |
|
||||||
|
| `scripts/check-changelog-touched.sh` | Tracked; canon self-audit tooling, not applicable to this consumer repo. |
|
||||||
|
| `scripts/check-core-sync.sh` | Tracked; canon self-audit tooling, not applicable to this consumer repo. |
|
||||||
|
| `scripts/check-file-names.sh` | Tracked; canon self-audit tooling, not applicable to this consumer repo. |
|
||||||
|
| `scripts/check-issue-body-staleness.sh` | Tracked; canon self-audit tooling, not applicable to this consumer repo. |
|
||||||
|
| `scripts/check-links.sh` | Tracked; canon self-audit tooling, not applicable to this consumer repo. |
|
||||||
|
| `scripts/check-repo.sh` | Tracked; canon self-audit tooling, not applicable to this consumer repo. |
|
||||||
|
| `scripts/check-rule-structure.sh` | Tracked; canon self-audit tooling, not applicable to this consumer repo. |
|
||||||
|
| `scripts/check-version-bumped.sh` | Tracked; canon self-audit tooling, not applicable to this consumer repo. |
|
||||||
|
| `scripts/sync-priority-label.sh` | Tracked; canon self-audit tooling, not applicable to this consumer repo. |
|
||||||
|
| `scripts/sync-rules.sh` | Tracked; canon self-audit tooling, not applicable to this consumer repo. |
|
||||||
|
| `scripts/test-gates.sh` | Tracked; canon self-audit tooling, not applicable to this consumer repo. |
|
||||||
|
| `ui/appuikit.md` | Listed (new since v1.1.2; NOT yet audited against this repository) |
|
||||||
|
| `ui/pane-layout.md` | Listed (acceptance commands run on Sources, all empty; app-level tests not run, no app here) |
|
||||||
|
| `versioning.md` | Listed (new since v1.1.2; NOT yet audited against this repository) |
|
||||||
|
|
||||||
## Rule Matrix
|
## Rule Matrix
|
||||||
|
|
||||||
|
|||||||
@ -1,68 +1,88 @@
|
|||||||
13ffe73c313954b379e91f15746f4f7c1b621f062710a2d64c8b3fd6d3ed8aeb .github/pull_request_template.md
|
8916c56c0ef1493dff7493072210f17a4fc49787fbec202421f4aebeb9237fc6 .github/pull_request_template.md
|
||||||
|
e1f63d81103ae2f95754359922e1ea574f2fa1a16168b463f030457203623065 accessibility.md
|
||||||
|
1699841d7dee0e4084b340edc6889a0c6c6441d04b6574ea153c555fea48f5c8 app-intents.md
|
||||||
|
fa5b776f0fc2e6e4ded57ffa22a0c6663b989ef7ae3b4168e357d9be4e05af49 app-logging.md
|
||||||
2f2b00ede2a0a0b8ea3ae5d40528b9ddb65f9855e676d2516f6782b413125cbe business-rules-and-constraints.md
|
2f2b00ede2a0a0b8ea3ae5d40528b9ddb65f9855e676d2516f6782b413125cbe business-rules-and-constraints.md
|
||||||
4d45adc8beff3e6ac00231f43b523ada84f8d9d992db4b27f5011fe3d32ca993 CHANGELOG.md
|
f8df7cacde967370ff19b99410b519a853ecb3989f6dbf81f7de822b2f2e0c09 CHANGELOG.md
|
||||||
a0fa0338cbf60c819c0643dcf81ac36445497e28d964b22b943bf68fb3f44c10 code-style.md
|
a2b3128c1d5fe0325c610ac2152cf0c6e9d5a98c242d00e111a4d6be06d72dea code-style.md
|
||||||
2a76443b5671254deb77d715e7903e26dc2e0cb723bf7d4092c5678787d7cdf9 concurrency.md
|
d8c6a2d38403f2a59441111fc1fefc06806cd68dab677b934bc682c4833f53ed concurrency.md
|
||||||
16e0b09c64b19aff717eaa49ee0a22393d7b35d54829ec8ff9fa618c51efe9fb CONTRIBUTING.md
|
16e0b09c64b19aff717eaa49ee0a22393d7b35d54829ec8ff9fa618c51efe9fb CONTRIBUTING.md
|
||||||
4596e07dcaa390a213d9e5e56c25c28181aaccc0afa3d814fc8d4c5935ffc931 CONVENTIONS.md
|
beaea240b77a525019f53849d63e52ac8261fbf0b5aed1d6dcbbda1dec457063 conventions.md
|
||||||
f9049c8df726b33b6adf01724a8f13a5af2c2d9cef456dd7d742c3f5937f22f5 core/brainstorming.md
|
6aa03c4acdb44ff0b33d6607df811c46ed858f7e97073584bc7691bcb95120ca core/brainstorming.md
|
||||||
de956b34d58a19b6852bf916ee528b43756169ade1b76478c4fd0b364e1a806a core/commits.md
|
de956b34d58a19b6852bf916ee528b43756169ade1b76478c4fd0b364e1a806a core/commits.md
|
||||||
d0bc7467ec6674d47ef1dd59b6ac62b9ba2a3f5eaaf4c5e452f814ea4192acd6 core/file-naming.md
|
1be192e9fbdf798e591a8d21ac3818ccdb3d1be6627908587134deff459e09fb core/file-naming.md
|
||||||
9a58f0cb17d3a566fa27c8466a9e440950d2884c9d35e5df4cffd992009b1e24 core/first-principles-analysis.md
|
9a58f0cb17d3a566fa27c8466a9e440950d2884c9d35e5df4cffd992009b1e24 core/first-principles-analysis.md
|
||||||
91cb75a203d84ef9aa767979a7196395f9f3bbc9f20224a85b28c1ad849f954f core/folder-grouping.md
|
7d5a5dbb559a9daa195bdaf3770d0114dcb1648545c50018ed0bc240aaceb7e5 core/folder-grouping.md
|
||||||
71dcb7a8df43a9cf4e801efe015a6c4adb71023ee0bb0badbecee199497745af core/git-discipline.md
|
71dcb7a8df43a9cf4e801efe015a6c4adb71023ee0bb0badbecee199497745af core/git-discipline.md
|
||||||
c5062717b1bbfd9f8e6e363526d4e77ec313a968453aaa2cae099ce828e1349b core/no-shortcuts-first-principles.md
|
c5062717b1bbfd9f8e6e363526d4e77ec313a968453aaa2cae099ce828e1349b core/no-shortcuts-first-principles.md
|
||||||
48315f7839d1747e932c6a106b5859f0aab2a095fba1fb08640ee614a7706c0a core/oracle-first.md
|
48315f7839d1747e932c6a106b5859f0aab2a095fba1fb08640ee614a7706c0a core/oracle-first.md
|
||||||
7319934dd32b6a0c6744a980c1d95267ef1996ad8d8a4e8a973664033d497eb3 core/proof-discipline.md
|
7319934dd32b6a0c6744a980c1d95267ef1996ad8d8a4e8a973664033d497eb3 core/proof-discipline.md
|
||||||
632c3fabcd46a2ff789e841d9112b06c5a3de1ca2154fbeaa797042750e62d0a core/README.md
|
7bf502ecc7e0787c2f7235774207ed8aa925d2f7e2547ff3dad7ad788df2001d core/README.md
|
||||||
6b76e9bfa52169190da54224f9a2018fa00ba34f8dee75fa1c718010de434f06 core/round-trip-transformation.md
|
6b76e9bfa52169190da54224f9a2018fa00ba34f8dee75fa1c718010de434f06 core/round-trip-transformation.md
|
||||||
98a85b72d52bb179ed0f9712dd9131114be8a8550e62dedcf9715e4bb6eac61f core/rules.md
|
98a85b72d52bb179ed0f9712dd9131114be8a8550e62dedcf9715e4bb6eac61f core/rules.md
|
||||||
7237ffe85b06b210f6a7d6154496958ba78906169132be9e63574f627284c529 core/self-improve.md
|
7237ffe85b06b210f6a7d6154496958ba78906169132be9e63574f627284c529 core/self-improve.md
|
||||||
e7e4616c100f864a8bf65f371f3e90359835eac60b952e44dbde5016f5c7d1ff core/systematic-debugging.md
|
e7e4616c100f864a8bf65f371f3e90359835eac60b952e44dbde5016f5c7d1ff core/systematic-debugging.md
|
||||||
dfbd528fdf9ad898149c44731bc6f871cfea34e358e60d503a5b88addccd1284 core/testing-discipline.md
|
dfbd528fdf9ad898149c44731bc6f871cfea34e358e60d503a5b88addccd1284 core/testing-discipline.md
|
||||||
5444efca6c70596c7dde95106ffcdd4246872734f0a5cfe11ffba464db8d02a7 core/verification.md
|
5444efca6c70596c7dde95106ffcdd4246872734f0a5cfe11ffba464db8d02a7 core/verification.md
|
||||||
f8ee59a8313278e0507f562965d6e71a4a11aa744eade89103c23d5bdac15f02 core/writing-plans.md
|
5e1322636d77b7cf5099dd70e8406947e7a28383d23d364ba07d65339d3d80d9 core/writing-plans.md
|
||||||
ca424e5cf141c82ea86be9a4c07275ff84f7408c581aa702f354cdb11754d6a7 cross-platform.md
|
ce4cc4c7c8264fdfdd82155f751881debccfa7e290e0fdd427c753d5ecc6a964 crash-and-state-reporting.md
|
||||||
d6cb6a5d66b49e4da46b8f4eece8ef93abe4758d733e18ae058cd27eda714b07 dependency-injection.md
|
7d21b77a71e374ddc7a44584f5c8280fc1a9b68c5371216b355507b05456a864 cross-platform.md
|
||||||
|
051e121156086a16fdcae804b67b061059d13784511435aeb6572e0c26f46118 dependency-injection.md
|
||||||
8bfc0e52532d86779689d079822d88648727f2f3f667ab5a183a669f876de31e documentation-search.md
|
8bfc0e52532d86779689d079822d88648727f2f3f667ab5a183a669f876de31e documentation-search.md
|
||||||
efa2cabce23f0ed77875cc5bbca61df5b1426a6835599b6b34167103ce738085 documentation.md
|
b4108dd1ac24f70f17e9f97da3518309541bbdbb905a4d65a75e7cea6b727a8c documentation.md
|
||||||
75bef7008f232274d06cadd1a09d6493243857737e879829b498f25453eac50b domain-first.md
|
75bef7008f232274d06cadd1a09d6493243857737e879829b498f25453eac50b domain-first.md
|
||||||
4b7ece49fac6eac1cce269615eddfc64e3aa05f14deac821aeef86b244870f25 exp/app-target.md
|
fa7e3b2f9b5fa13a52b4239b4a96fcc0e277c0011c65345285907fdf8b9b1cab exp/app-target.md
|
||||||
ffe5f8830b24e2547a6e843a1b1da928d2cb484dd4f31d35a131609a3ccc42a4 exp/build-performance.md
|
ae26db85121fab89f2f2a0cf72a3a7476cb881c73d960dc68b43cf3bb8bd70eb exp/build-performance.md
|
||||||
ee5572c1d50d0d977374352d7dbfb1f09b55ec2e57bd3712443553f4444e455e exp/common-mistakes.md
|
a243e8073e5b629419775306a6b8bfa1fc745549a547d211275950a91ab9a647 exp/common-mistakes.md
|
||||||
dffe98c4b476542c1fa2abdf37e3fe6898ef86d4aa782fc3b961b1166cb85360 exp/critical-rules.md
|
389f7847110ac2ad6bb9fa962bd1f89c6d3e9b77ec42f8df698e23511cbaae8a exp/critical-rules.md
|
||||||
2ec3b9f0bd9013b12aa77e4cfdbb5e058d6a900488ebcb28b231a74aa900c7b4 exp/dependency-management.md
|
e5d103b4ac516624cea4abd4f4371fcee72cf113c5b45f07e83cd350656dd42b exp/dependency-management.md
|
||||||
18bb0b25733540837c10b57d2de59212664a595fc8b6811a6b8cb2aa3edf5864 exp/implementation-patterns.md
|
4999fe1f3898116bbe49c73f8f3111445ff95c3a9f14acbc15c7782829fb0899 exp/implementation-patterns.md
|
||||||
f1f02a9d94d0ae2ae935288a0ffa0226d28f338ef0491efdaf8420262199a47d exp/migration.md
|
39ce08c38ed0d7f42f35ad73d5cb95c3923f14097f053a996d5e53e443a1a078 exp/migration.md
|
||||||
52302f2f5791d38f7e8370bb72a711c17684f6a917b99011b1198e2d0af76810 exp/package-swift.md
|
d487e3d0a81d62454e1b03867671bea318d86c301b76b7f4b26ccb0e4f7864c3 exp/package-swift.md
|
||||||
1e8d08a75ff7f1669866bfbd7faa11c955c5ae5625f0af536a68ba8b3b06c679 exp/README.md
|
e050c1fd4b0dcfb7b0ed96207caaca25d59502e742f12f5fefd13232de5a6361 exp/README.md
|
||||||
6854b78671b25ad8bcac568f5021be4b712b9503843eb44adf8952e388ce4ddf exp/testing.md
|
0e415915dfb0100f3f333d467756c11b3791cacda6b7330c44940770d52bcb5a exp/testing.md
|
||||||
7dec307eb969dcbac7c0c28ea7a9d57f11353f676a0422a4898660be2cefc0ad exp/verification.md
|
71ff9c5f18ed9b700cc3da78f6ed7fdf9ccb448a1f60484be80e6f566c1b9d81 exp/verification.md
|
||||||
f7f182deb386f03e955f7bdeb3ea7dd583e0b1c1a482267d0432801bc89595ec exp/when-to-create.md
|
603eefbd9e6a5457e656aa380779b4cd95e8e8527d1a4093653ff0013d829279 exp/when-to-create.md
|
||||||
8c9bb26f56cb849c6164778ae09df434ba5ea71b35192b62fe2df4f72e8317f9 formatting-and-linting.md
|
8c9bb26f56cb849c6164778ae09df434ba5ea71b35192b62fe2df4f72e8317f9 formatting-and-linting.md
|
||||||
e0579e48bec9fc1670efa0f596c2458207b19b701b6bfd725b8647a30090863a framework-policy.md
|
47f9712ed168278d77ffa4e4424d1045594c2f820a9e2f5e08b854ba50cfd432 framework-policy.md
|
||||||
25eb598a7c75cd2631dd72cb81afe98cacab3f844ca7f14d38f9eb0abd5d2c43 linux-server.md
|
6c99b83cf4baa6e8b958acb5c49303bf4b6e5aea7da6a5b4eff860dfcf104bbf help.md
|
||||||
f6459775422fda6d9359e9914b88a08af7673e1e02ce0c970dd97a01660d60a0 namespacing.md
|
ebd2c9ff8e9eda5feace8ac1d76c33feaad763300513e16ef19b7c49dde03b0d iphone-duo.md
|
||||||
|
34fd0d58b6e0f5e0230c8f3beaff83699381c9ffcc232a34e25fa9f2c116096d linux-server.md
|
||||||
|
625b6512d0ddba604eca910ed6885bbe0a6b48ea9bffe296250caec3559dc161 namespacing.md
|
||||||
4f9db39aa365e26f2a18c54229e9abcd53fef6153566510453f4a2dcebf07f7d openapi-generated.md
|
4f9db39aa365e26f2a18c54229e9abcd53fef6153566510453f4a2dcebf07f7d openapi-generated.md
|
||||||
685999f2b40c2edffc348103fdbddf022cc3fbb8eb11fc3530945c75514d8c0b package-architecture.md
|
c729b183d73829c237a648f3b322e04d14746ab3f9dffe648861388d72ee2cdd package-architecture.md
|
||||||
4a287a84c891f82827d1923fae946fcdaba8feae4d31c0f698fad0e73ac82c1a package-import-contract.md
|
aba4015700dd48486dcf8e660bdd7bc78bcd1d80b5d81b765acadc2475e0fc2d package-import-contract.md
|
||||||
15173db2a547db6e4db53a4c0abe4eee259a0c4cc11ef609bc9acff89993d9cb package-structure.md
|
e7354bbc90e894f9582893bf36cca9c9ed26eb19d745f34e85abfd2f51fbfd15 package-structure.md
|
||||||
7b32c5386455160991779162917a51ae4f7a62765f05dd812f36bcd2b5d6ee19 parsing-rules.md
|
b9565e7ed15902263ee28e5c96dbe88d022f3dacf17efee83b78f898ec8f7664 parsing-rules.md
|
||||||
91d16b50114366f98e9ceb40be51176d1ee85485f5bd88e0ba454798345ba418 README.md
|
fc07ad34930ae96a2355079a9f8ce5a74e443116df64f073f84638c818937f13 README.md
|
||||||
a4e1973f58b815099c2acd28c673d00df5a93ce5cfc935f2b2ebaf150b00db43 scripts/check-namespacing.sh
|
c48dda3b5b33cec0efb28e3187b3c6f51f4db72320bf47f050f3faaec9bd715f scripts/check-changelog-touched.sh
|
||||||
c77663d648a4d7fc67012e009a36ce9d16fb0f4b4f5cc5a251f6e9433947177b scripts/leak-gate.sh
|
6920d89f257db816155a4a35a03e2daf25caff745469a0d4e9c804a1ebbfbf49 scripts/check-core-sync.sh
|
||||||
7d06771b0d9be1d5d2a73bec1d50a6d94be2799e8046be5645af638155d3edee shared-protocols.md
|
0b0a9efda01ce373336d13c11de330e42e88d5dc80636bb14e41c980778d2c2b scripts/check-file-names.sh
|
||||||
62a3886ca75e4dd8aabec389b1c56317e5fff1ebde58f1b65bbbf6cfe6e09bcd testing.md
|
a36b6b568e3b4ec9cfa3c0c7e1823099fd732a9504f92c61ace0ea741d654a50 scripts/check-issue-body-staleness.sh
|
||||||
99038cc15eeb9988a31ee4478bd83c804e7dedc99a7eb22686508e2b0ec312ea ui/appkit-views.md
|
d15a1902384a3c0bdac89154a08d80a6b86c5680a64b5d89218d8c398f65de57 scripts/check-links.sh
|
||||||
|
beaed57969cebd7abbfe12b878d338b3ee8c4e7fb75399b163d04dea0a4c6ad6 scripts/check-namespacing.sh
|
||||||
|
e5000514ebd8b97b0be57823efe5a3fb4dab30289519c6e54d66783f28addd40 scripts/check-repo.sh
|
||||||
|
a76da192689247c0a4944e0220c20e0f8bca4d2577f8be2462ee6063b12fddb2 scripts/check-rule-structure.sh
|
||||||
|
86cfc139ce4c0a845d1a996f6606c01614cf98e08111faa5333b9cf610a6fb4e scripts/check-version-bumped.sh
|
||||||
|
2e62d749c8eec14009b1c5e718dacbaa2fdfc1eab351eb3c15b3ecdc18d209cc scripts/leak-gate.sh
|
||||||
|
6fae67ceb69b1895b818b388915180ed0e171100f9c0a0d3ef7ea3bff7e2e9b1 scripts/sync-priority-label.sh
|
||||||
|
6a22662c62dcfd1be4f66f294c52dc483de399ac66ef352d221c3c76e66378ae scripts/sync-rules.sh
|
||||||
|
f8db2fcf92903906b94c354561acafec5471a1fc287e80121df764e95dd768ad scripts/test-gates.sh
|
||||||
|
7c366ba961e4e59738d2d305255674e95dfa2ffd84ceb5617769cdb066a9d5bc shared-protocols.md
|
||||||
|
f8650fa329e4200ce59d2f3f0529782c8dc1e803f6f059d68d8a4f3d77a3aa26 testing.md
|
||||||
|
9263a0b13d45b6afd81a5a998f3fe289f5337de0a9299e5107ddf1300dad785d ui/appkit-views.md
|
||||||
|
2a342f76ca103f78ea861aa186287ec7307b22d6fe57d07d0f4a45a6d547bf6f ui/appuikit.md
|
||||||
91dceefc09e8082b09f891ff8675728a92cb34a178f105644e7052a4c3329671 ui/colors.md
|
91dceefc09e8082b09f891ff8675728a92cb34a178f105644e7052a4c3329671 ui/colors.md
|
||||||
ca2d5d6a70eef00fefa56acbb7ba8df75d5bab7d6dd44a1bae85f89adc482827 ui/components.md
|
40c62ac4e53b53f107322a3e1ae354c96239e9964322862161cbe3d68bdcb33b ui/components.md
|
||||||
6545df89b59a22be605cabf1e0e0bef97dc30331baabee7779ac8d5f2e1bc496 ui/flowspec.md
|
e15732abfa2b6e194ba8d32c1340772a496ab8e1ed8f267669ccb054fb94b0a9 ui/flowspec.md
|
||||||
fafddaa4d87e8f92e430e5aa494bbb049c50f6834a1024bc8cf52faf257605f4 ui/fonts.md
|
fafddaa4d87e8f92e430e5aa494bbb049c50f6834a1024bc8cf52faf257605f4 ui/fonts.md
|
||||||
bb2442a17edcc39404388f4f2202a146f3e396dcca8da437621518db639d0fc0 ui/native-per-platform.md
|
bb2442a17edcc39404388f4f2202a146f3e396dcca8da437621518db639d0fc0 ui/native-per-platform.md
|
||||||
755d1fc079aab90dede44e45ca9aff7bcbf60e9daef1dc0487e0fe1f1bf27108 ui/pom.md
|
406c9b3f45e0570348c45211c093a52d95a0167ebd206f176d3f6acf1150c15d ui/pane-layout.md
|
||||||
|
f21dbb435707ddba348a5c3a208e9630312a5cc7e5185018c7f05b7b54eb8a17 ui/pom.md
|
||||||
bd3eefcede20de3098b4acb781b53122e185faa129553f394d5cb7504f4a34b0 ui/pre-ui-layer.md
|
bd3eefcede20de3098b4acb781b53122e185faa129553f394d5cb7504f4a34b0 ui/pre-ui-layer.md
|
||||||
a29bd9531f6aac15f5e56633abf09aa057ce0a45908b20063b83cc5a1ca854c3 ui/swiftui-views.md
|
7b464292670fdd911840b10c4ee3fee8dfa1c686d86dedab9bf88ad071974718 ui/swiftui-views.md
|
||||||
7c83f83e0e5672f877984a3637e88a2cf82f8e8019202676ce64a78b74b95208 ui/three-renderers.md
|
7c83f83e0e5672f877984a3637e88a2cf82f8e8019202676ce64a78b74b95208 ui/three-renderers.md
|
||||||
20f5610e20dff5ff1833ceab06186d31db915eeee43f1d6393e70fbab4d72041 ui/uikit-views.md
|
514980eed5a2679c4e3812c1d1f416ec066b5cb21d5c59c10876cd56a89ce1b1 ui/uikit-views.md
|
||||||
edc82febe0138dad789541d3b05f9ef16da5dc907479304b8cc8b900ac1e7fd1 ui/view-models.md
|
c36bc1688231d634fd6b0102baee9f86b04d2e38981d5fd0a4909a85176df9ad ui/view-models.md
|
||||||
35d3ac448344544b8b73b6c0be81e7d5afdce1220122127a7b2cb9547ae5b19d validation-rules.md
|
35d3ac448344544b8b73b6c0be81e7d5afdce1220122127a7b2cb9547ae5b19d validation-rules.md
|
||||||
|
36fd78d30ba7baa8ff7af633a34038c9746478b91a7f4c522f1925c9ec5720ec versioning.md
|
||||||
|
|||||||
71
third_party/rules-swift/.githooks/commit-msg
vendored
71
third_party/rules-swift/.githooks/commit-msg
vendored
@ -1,17 +1,76 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# commit-msg hook: reject AI/tool attribution and em dashes in the message.
|
# commit-msg hook: enforce the repository's commit format and prose rules.
|
||||||
# Install per clone (hooks are not cloned): git config core.hooksPath .githooks
|
# Install per clone (hooks are not cloned): git config core.hooksPath .githooks
|
||||||
# See core/commits.md and core/git-discipline.md.
|
# See core/commits.md and core/git-discipline.md.
|
||||||
|
|
||||||
msg_file="$1"
|
set -eu
|
||||||
|
|
||||||
if grep -qiE 'co-authored-by:[[:space:]]*(claude|cursor|copilot|codex|gpt|gemini|antigravity)|generated (with|by) (claude|cursor|copilot|codex|gpt|gemini|antigravity)|generated with .*code|powered by|robot' "$msg_file"; then
|
msg_file="${1:?usage: commit-msg <message-file>}"
|
||||||
echo "commit-msg: remove tool attribution (co-author trailer, 'generated with', 'powered by', robot emoji)." >&2
|
clean=$(mktemp "${TMPDIR:-/tmp}/rules-swift-commit-message.XXXXXX")
|
||||||
|
trap 'rm -f "$clean"' EXIT HUP INT TERM
|
||||||
|
|
||||||
|
# Ignore Git's comment lines and everything after the scissors marker.
|
||||||
|
awk '
|
||||||
|
/^# ------------------------ >8 ------------------------$/ { exit }
|
||||||
|
!/^#/ { print }
|
||||||
|
' "$msg_file" > "$clean"
|
||||||
|
|
||||||
|
subject=$(sed -n '1p' "$clean")
|
||||||
|
|
||||||
|
if [ -z "$subject" ]; then
|
||||||
|
echo "commit-msg: subject must not be empty." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if grep -q "$(printf '\xe2\x80\x94')" "$msg_file"; then
|
if [ "${#subject}" -gt 72 ]; then
|
||||||
echo "commit-msg: no em dashes in commit messages; use commas, colons, or restructure." >&2
|
echo "commit-msg: subject exceeds 72 characters (${#subject})." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! printf '%s\n' "$subject" | grep -qE '^(feat|fix|docs|style|refactor|perf|test|build|ci|chore)(\([a-z0-9._-]+\))?!?: [^ ].*[^.]$'; then
|
||||||
|
echo "commit-msg: use <type>(<scope>): imperative description, with an allowed Conventional Commit type and no trailing period." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$subject" in
|
||||||
|
*'!:'*)
|
||||||
|
if ! grep -qE '^BREAKING CHANGE: .+' "$clean"; then
|
||||||
|
echo "commit-msg: a breaking subject requires a BREAKING CHANGE footer with a migration explanation." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
if grep -q '^BREAKING CHANGE:' "$clean"; then
|
||||||
|
echo "commit-msg: a BREAKING CHANGE footer requires ! in the subject." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if grep -qiE '(claude|anthropic|cursor|copilot|codex|openai|chatgpt|gpt-[0-9a-z.-]*|gemini|antigravity|(^|[^[:alnum:]])AI([^[:alnum:]]|$)|LLM|assistant|machine-generated|co-pilot|pair-programmer|robot)' "$clean"; then
|
||||||
|
echo "commit-msg: Rule 5.1 forbids tool or automated-author attribution." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -q "$(printf '\342\200\224')" "$clean"; then
|
||||||
|
echo "commit-msg: Rule 5.2 forbids em dashes; use commas, colons, or restructure." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -q "$(printf '\360\237\244\226')" "$clean" \
|
||||||
|
|| grep -q "$(printf '\342\234\250')" "$clean"; then
|
||||||
|
echo "commit-msg: Rule 5.1 forbids tool-signature emoji." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -q "$(printf '\342\200\213')" "$clean" \
|
||||||
|
|| grep -q "$(printf '\357\273\277')" "$clean"; then
|
||||||
|
echo "commit-msg: Rule 5.1 forbids hidden watermark characters." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if awk 'length($0) > 72 { found = 1 } END { exit found ? 0 : 1 }' "$clean"; then
|
||||||
|
echo "commit-msg: body and footer lines must be wrapped at 72 characters." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
12
third_party/rules-swift/.githooks/pre-commit
vendored
12
third_party/rules-swift/.githooks/pre-commit
vendored
@ -12,6 +12,16 @@ sh "$(dirname "$0")/../scripts/leak-gate.sh"
|
|||||||
# Namespacing gate (one non-private top-level type per file) runs whole-tree on every
|
# Namespacing gate (one non-private top-level type per file) runs whole-tree on every
|
||||||
# commit, matching code-style.md. Cheap, and vacuous in a repo with no Swift sources.
|
# commit, matching code-style.md. Cheap, and vacuous in a repo with no Swift sources.
|
||||||
sh "$(dirname "$0")/../scripts/check-namespacing.sh"
|
sh "$(dirname "$0")/../scripts/check-namespacing.sh"
|
||||||
|
sh "$(dirname "$0")/../scripts/check-file-names.sh"
|
||||||
|
sh "$(dirname "$0")/../scripts/check-links.sh"
|
||||||
|
sh "$(dirname "$0")/../scripts/check-rule-structure.sh"
|
||||||
|
sh "$(dirname "$0")/../scripts/check-core-sync.sh"
|
||||||
|
sh "$(dirname "$0")/../scripts/test-gates.sh"
|
||||||
|
sh "$(dirname "$0")/../scripts/check-changelog-touched.sh"
|
||||||
|
|
||||||
|
# Version-bump gate: any rule change must bump VERSION (the freshness marker downstream
|
||||||
|
# repos compare against). Cheap, whole staged set. See scripts/check-version-bumped.sh.
|
||||||
|
sh "$(dirname "$0")/../scripts/check-version-bumped.sh"
|
||||||
|
|
||||||
# Lint only the Swift this commit touches. A commit with no Swift files (docs, config,
|
# Lint only the Swift this commit touches. A commit with no Swift files (docs, config,
|
||||||
# this rules repo) has nothing to format or lint, so stop here.
|
# this rules repo) has nothing to format or lint, so stop here.
|
||||||
@ -24,10 +34,12 @@ if command -v swiftformat >/dev/null 2>&1; then
|
|||||||
printf '%s\n' "$swift_files" | xargs swiftformat --lint --config .swiftformat
|
printf '%s\n' "$swift_files" | xargs swiftformat --lint --config .swiftformat
|
||||||
else
|
else
|
||||||
echo "pre-commit: swiftformat not found; install it so formatting is enforced locally." >&2
|
echo "pre-commit: swiftformat not found; install it so formatting is enforced locally." >&2
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if command -v swiftlint >/dev/null 2>&1; then
|
if command -v swiftlint >/dev/null 2>&1; then
|
||||||
printf '%s\n' "$swift_files" | xargs swiftlint lint --strict --config .swiftlint.yml
|
printf '%s\n' "$swift_files" | xargs swiftlint lint --strict --config .swiftlint.yml
|
||||||
else
|
else
|
||||||
echo "pre-commit: swiftlint not found; install it so linting is enforced locally." >&2
|
echo "pre-commit: swiftlint not found; install it so linting is enforced locally." >&2
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Contributing guide
|
- name: Contributing guide
|
||||||
url: https://github.com/mihaelamj/rules-swift/blob/main/CONTRIBUTING.md
|
url: https://codeberg.org/Mihaela/rules-swift/src/branch/main/CONTRIBUTING.md
|
||||||
about: How a change is proposed, where it belongs, and the house conventions this repo follows.
|
about: How a change is proposed, where it belongs, and the house conventions this repo follows.
|
||||||
|
|||||||
27
third_party/rules-swift/.github/labels.json
vendored
Normal file
27
third_party/rules-swift/.github/labels.json
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "bug",
|
||||||
|
"color": "ff3b30",
|
||||||
|
"description": "Something ships incorrectly"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "enhancement",
|
||||||
|
"color": "007aff",
|
||||||
|
"description": "New rule, refactor, or design proposal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "epic",
|
||||||
|
"color": "af52de",
|
||||||
|
"description": "Aggregation parent for related issues"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "priority: high",
|
||||||
|
"color": "ff3b30",
|
||||||
|
"description": "Critical, release-gating, or actively blocking"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "good first issue",
|
||||||
|
"color": "34c759",
|
||||||
|
"description": "Suitable for a new contributor"
|
||||||
|
}
|
||||||
|
]
|
||||||
@ -15,4 +15,4 @@ Closes #
|
|||||||
- [ ] `CHANGELOG.md` updated if production source changed; otherwise `[no-changelog]` is justified (Rule 3.2).
|
- [ ] `CHANGELOG.md` updated if production source changed; otherwise `[no-changelog]` is justified (Rule 3.2).
|
||||||
- [ ] Self-critic pass done: I read my own diff as a reviewer and fixed what it surfaced (Rule 3.3).
|
- [ ] Self-critic pass done: I read my own diff as a reviewer and fixed what it surfaced (Rule 3.3).
|
||||||
- [ ] No AI/tool attribution and no em dashes in any committed text (Rules 5.1, 5.2).
|
- [ ] No AI/tool attribution and no em dashes in any committed text (Rules 5.1, 5.2).
|
||||||
- [ ] Gates pass locally: `sh scripts/leak-gate.sh` and `sh scripts/check-namespacing.sh` are green.
|
- [ ] Complete local gate passes: `scripts/check-repo.sh` is green.
|
||||||
|
|||||||
15
third_party/rules-swift/.github/workflows/issue-body-staleness.yml
vendored
Normal file
15
third_party/rules-swift/.github/workflows/issue-body-staleness.yml
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
name: issue-body-staleness
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "17 4 * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: read
|
||||||
|
jobs:
|
||||||
|
issue-body-staleness:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Audit open issue bodies
|
||||||
|
run: sh scripts/check-issue-body-staleness.sh
|
||||||
@ -11,19 +11,8 @@ jobs:
|
|||||||
apply-priority-label:
|
apply-priority-label:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@v7
|
- uses: actions/checkout@v4
|
||||||
with:
|
- name: Synchronize the priority label
|
||||||
script: |
|
env:
|
||||||
const body = context.payload.issue.body || "";
|
FORGEJO_TOKEN: ${{ forgejo.token }}
|
||||||
// The form renders the dropdown under a "### Priority" heading; read the next non-empty line.
|
run: sh scripts/sync-priority-label.sh
|
||||||
const m = body.match(/^###\s+Priority\s*$\n+([^\n]+)/m);
|
|
||||||
const value = m ? m[1].trim().toLowerCase() : "";
|
|
||||||
const label = "priority: high";
|
|
||||||
const has = (context.payload.issue.labels || []).some(l => l.name === label);
|
|
||||||
const owner = context.repo.owner, repo = context.repo.repo;
|
|
||||||
const issue_number = context.payload.issue.number;
|
|
||||||
if (value === "high" && !has) {
|
|
||||||
await github.rest.issues.addLabels({ owner, repo, issue_number, labels: [label] });
|
|
||||||
} else if (value !== "high" && has) {
|
|
||||||
await github.rest.issues.removeLabel({ owner, repo, issue_number, name: label }).catch(() => {});
|
|
||||||
}
|
|
||||||
|
|||||||
@ -9,7 +9,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Run the public-safe leak gate
|
with:
|
||||||
run: sh scripts/leak-gate.sh
|
fetch-depth: 0 # diff-based changelog and VERSION gates need history
|
||||||
- name: Run the namespacing gate
|
- name: Run the complete repository gate
|
||||||
run: sh scripts/check-namespacing.sh
|
run: |
|
||||||
|
base="${{ github.event.pull_request.base.sha }}"
|
||||||
|
[ -n "$base" ] || base="${{ github.event.before }}"
|
||||||
|
case "$base" in ""|0000000000000000000000000000000000000000) base=HEAD~1 ;; esac
|
||||||
|
sh scripts/check-repo.sh "$base"
|
||||||
|
|||||||
245
third_party/rules-swift/CHANGELOG.md
vendored
245
third_party/rules-swift/CHANGELOG.md
vendored
@ -4,7 +4,250 @@ All notable changes to this rule set are documented here. This project follows S
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
A conformance pass bringing the repository into agreement with its own rules.
|
## [2.14.0] - 2026-11-02
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `concurrency.md`: where a synchronous API must guard shared state (a protocol requirement, a synchronous engine), the rule now names `Synchronization.Mutex` (SE-0433) beside `actor`: it is `Sendable` without `@unchecked`, and the state is reachable only inside `withLock`. The gate's escape-hatch and `NSLock` checks are unchanged.
|
||||||
|
|
||||||
|
## [2.13.0] - 2026-10-31
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `ui/flowspec.md`: the verb list now includes `ambient` and `rotate`, the engine is described with `AsyncStepRegistry` and `AsyncScenarioRunner` for an async pre-UI door, and a new DO says to load hand-edited scenarios with `ScenarioLoader.loadValidated`.
|
||||||
|
- `ui/flowspec.md`, `ui/pom.md`, `README.md`: the FlowSpec link points at its Codeberg home, `https://codeberg.org/Mihaela/FlowSpec`, instead of GitHub, where it no longer lives.
|
||||||
|
|
||||||
|
## [2.12.0] - 2026-10-28
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `ui/pane-layout.md`: a section on one set of panes on a Mac, an iPad and an iPhone with SuperSplit, ten rules: one view per pane role with many containers (a phone gets the same views as pages, not a split); the arrangement is a value that names the saved layout; a pane's share and its least size are different limits and the least wins; the Mac hides the problem by being wide, so test every pane at its least width; the divider is the edge of a pane and its frame in the accessibility tree is what a test asserts against; the card's clip loses what does not fit, so pair it with a test and a picture; maximize is the way to give one pane the whole area; a platform view needs an owner that lives as long as the pane; chrome as glass is a background; and three levels of test (the pure layout, the real window's frames, a picture of each arrangement).
|
||||||
|
|
||||||
|
## [2.11.0] - 2026-10-28
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `ui/pane-layout.md`: eight rules from an iPad and an iPhone Duo with a real editor, each a defect that shipped: the split sizes a pane and the content does not; an optional part is decided by a measurement (a tip in `ViewThatFits` widened a 240 point pane to 296); a header fits the narrowest pane; columns are decided by the room the parts need and not by a fixed 1000 points (a 13 inch iPad upright is 1032); a layout is saved for each way the device is held; the whole area for the document; a platform view is updated by its inputs and keeps its controller; and a picture of each arrangement is a deliverable. With two acceptance commands.
|
||||||
|
- `iphone-duo.md`: a section of what was observed on the simulator, kept apart from what Apple documents: a vertical toolbar over a vertical tab bar on the closed display, a fold that rebuilt the navigation and lost the screen, and the tools that work (Device Hub only; screenshots are black unless closed).
|
||||||
|
|
||||||
|
## [2.10.0] - 2026-10-28
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `iphone-duo.md`: availability is now stated from the SDK's own Swift interface and headers, with two facts the Apple pages do not carry (the macOS SDK of Xcode 27.1 is 27.0 and lacks the 27.1 symbols; members of a type listed for a platform can be unavailable there, as `horizontalOnly`, `verticalPreferred`, `ToolbarOverflowMenu` and the compression behaviors are on macOS), the per-API versions of the bar APIs are corrected (`visibilityPriority` iOS 27.0 and macOS 26.1, `ToolbarOverflowMenu` iOS 27.0), and a new rule says the newer APIs are reached through one layer that checks the SDK and the OS separately and gives each a fallback (the `Forwards` package).
|
||||||
|
|
||||||
|
## [2.9.0] - 2026-10-28
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `iphone-duo.md`: the foldable iPhone, from Apple's own pages (the Human Interface Guidelines page, the
|
||||||
|
documentation of Preparing your app for iPhone Duo, ArrangementView and reserved regions, five Tech Talks
|
||||||
|
and the WWDC26 UIKit session; none is in the Cupertino corpus yet). The app is resized by a fold while it
|
||||||
|
runs, so: the size is read on every change from the view and never from the screen or the launch; layout
|
||||||
|
is decided by size classes and the space, not by idiom, orientation or `UIScreen.main`; state lives above
|
||||||
|
anything a size class swaps; reserved regions, arrangement views and the hinge are iOS 27.1 and used in
|
||||||
|
their places (regions and arrangements for layout, the hinge for effects); the system's bars carry the
|
||||||
|
controls; safe areas are not symmetric; a second window is a request that can fail; and every pose and
|
||||||
|
every change between poses is tested. States what Apple has not documented (the transition, state across
|
||||||
|
a fold, scene behavior on a fold, sizes), and that the claim of support is made only after the transition
|
||||||
|
test has been run.
|
||||||
|
|
||||||
|
## [2.8.0] - 2026-10-28
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `accessibility.md`: the Mac is audited too, and what it found is in the rules (a divider with no role is a slider on the Mac and an adjustable action on iOS; a list, an outline and a placeholder-only text field are named; a ruler beside text is hidden). A finding is checked against the pixels of the picture before it is listed, and one the pixels contradict is recorded as a false positive with its measured ratio. The ledger may name element kinds, a finding with no element is let pass only by an entry with no label, and the macOS SDK's missing audit types are named by their raw values. Rule 14 now has a built answer (pages instead of columns at accessibility sizes, decided by a tested pure function) and says for which screens it was looked at.
|
||||||
|
|
||||||
|
## [2.7.0] - 2026-10-27
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `accessibility.md`: text through text styles (Dynamic Type) with the app's own zoom applied as a shift of it,
|
||||||
|
every control named by what it does and never by its symbol's default name, 28 point tap targets, an
|
||||||
|
alternative for every gesture (named actions, adjustable elements), the name and actions on the element a
|
||||||
|
person reaches and not on a container, contrast as arithmetic (4.5 to 1, Light, Dark, Increase Contrast),
|
||||||
|
announcements, Reduce Motion, layer-drawn UI exposed element by element, and a UI framework's core that
|
||||||
|
describes while each surface must declare that it realizes it. The audit
|
||||||
|
(`performAccessibilityAudit`) runs in UI tests and fails for a finding that is not on a ledger of known
|
||||||
|
findings, each with its reason. Written from the Cupertino corpus (the Human Interface Guidelines pages
|
||||||
|
Accessibility and VoiceOver, Performing accessibility testing, the audit's documentation) and from what the
|
||||||
|
audit measured on an iPad app: 300 findings on five screens, 118 of them hit areas, 69 Dynamic Type, 59
|
||||||
|
contrast. States what is not established: the layout at the largest accessibility size, VoiceOver on a
|
||||||
|
device, and the audit on the Mac.
|
||||||
|
|
||||||
|
## [2.6.0] - 2026-10-27
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `help.md`: an app's help is written once, in one JSON file, and every platform's help is made from it. The
|
||||||
|
Mac carries an Apple Help Book (the two Info.plist keys, the book's structure and indexes, generated and
|
||||||
|
checked against the file) and leaves the Help menu to the system; an iPad and an iPhone have an in-app
|
||||||
|
help sheet in a Help menu of the toolbar and in the menu bar of an iPad, and the version in the Settings
|
||||||
|
app through a Settings bundle; tips (TipKit) point at the first thing to do and go when it is done. Each
|
||||||
|
platform is told in its own words. Ships a build-checked acceptance list and tests; what was observed on a
|
||||||
|
built app is stated as observed, and the Help Book format, which the Cupertino corpus does not hold, is
|
||||||
|
marked as such.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `versioning.md` rule 14: the stamp phase names the built `Info.plist` as an input file, so that it runs
|
||||||
|
after the system writes the plist; observed on a clean build, where the stamp was lost without it.
|
||||||
|
|
||||||
|
## [2.5.0] - 2026-10-18
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `app-logging.md`: every shipped app has one given recorder, entries that are facts and never the
|
||||||
|
person's content, every intent and state change logged, a Developer menu in Debug and Beta builds
|
||||||
|
only, and a report (log window, snapshot, optional content) made on the person's request and
|
||||||
|
sent only by their choice. Ships a build-checked acceptance list and tests.
|
||||||
|
- `crash-and-state-reporting.md`: MetricKit and StateReporting used behind `canImport` and
|
||||||
|
availability, the system's crash reports as a second door, one crash seen through both doors
|
||||||
|
kept once, the call stack never in the log, states joined to the log. Claims about delivery and
|
||||||
|
the return of states are stated at the strength they were shown.
|
||||||
|
- `versioning.md` rule 14: the stamp in an Xcode app target is a post-build script phase.
|
||||||
|
|
||||||
|
## [2.4.0] - 2026-10-17
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `versioning.md`: the version is written once, in a `VERSION` file, and read as Semantic
|
||||||
|
Versioning 2.0.0 (a value outside the grammar stops the build); the store's short version is
|
||||||
|
the core only; the build number is the first-parent commit count of the release branch, which
|
||||||
|
grows by one per commit, never restarts, and names one commit; both reach the code through a
|
||||||
|
generated file from a build-tool plugin whose command names its inputs (`VERSION` and the
|
||||||
|
git `HEAD` files); a shallow clone or a missing history is refused, never defaulted to `1`;
|
||||||
|
CI fetches full history; release history is append-only; a store build comes from the release
|
||||||
|
branch with a clean tree; a second upload of one commit needs its own number (the dotted
|
||||||
|
form is stated as not established until one upload orders it). Each rule names its basis as
|
||||||
|
documented, observed, or design. Ships six acceptance commands and the tests, including a
|
||||||
|
regeneration check in a throwaway clone.
|
||||||
|
|
||||||
|
## [2.3.0] - 2026-10-14
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `app-intents.md`: App Intents live in one adapter target; an intent in a package is
|
||||||
|
declared by an `AppIntentsPackage` that each app and extension includes; dependencies are
|
||||||
|
registered, not passed; every string a person sees is a localized resource that names the
|
||||||
|
package bundle and is handed to the system unresolved, so Siri and Shortcuts resolve it in
|
||||||
|
their own language; an interpolated sentence keeps its plural forms (one and other in
|
||||||
|
English and German, one, few and other in Croatian); a sentence is one key, not translated
|
||||||
|
fragments. Each rule names its basis as documented or observed. Ships five acceptance
|
||||||
|
commands, a test recipe for the languages (set `locale` on a copy of the resource, because
|
||||||
|
`String(localized:bundle:locale:)` reads the process's language), and the statement that a
|
||||||
|
unit test does not prove Shortcuts lists the intent in each language.
|
||||||
|
|
||||||
|
## [2.2.0] - 2026-10-14
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `ui/appuikit.md`: imperative AppKit and UIKit renderers, features, and app shells import
|
||||||
|
the cross-framework layer and name its `AppUI*` types, never an `NS` or `UI` type or a
|
||||||
|
direct SDK import; a missing alias or helper is added to the layer first. Ships an
|
||||||
|
acceptance check (two greps to promote to a self-tested gate). `appkit-views.md` and
|
||||||
|
`uikit-views.md` point to it.
|
||||||
|
|
||||||
|
## [2.1.1] - 2026-10-11
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- `exp/common-mistakes.md`: the test-target pairing command filtered `swift package
|
||||||
|
describe` output on the target type `regular`, which that output never uses (it says
|
||||||
|
`library`), so the command checked no library target and passed vacuously. It now selects
|
||||||
|
`library` and `executable`, and the rule says how to confirm the command selects
|
||||||
|
something.
|
||||||
|
|
||||||
|
## [2.1.0] - 2026-10-11
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `testing.md`: an exception to "mocks live in the test target" for test-support
|
||||||
|
targets that several test targets share, because SwiftPM does not let a test target
|
||||||
|
depend on another. It names four conditions (declared in a checked-in table, not
|
||||||
|
published, not shipped except by a named allowlist, mechanically checked) and ships a
|
||||||
|
runnable acceptance check.
|
||||||
|
|
||||||
|
## [2.0.0] - 2026-07-11
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- A complete local repository gate covering leak, namespacing, filenames,
|
||||||
|
relative links, rule structure, changelog, version bumps, and fault injection.
|
||||||
|
- A checked-in five-label tracker manifest plus scheduled issue-body and label-drift
|
||||||
|
auditing against Codeberg.
|
||||||
|
- An exact vendored core license, allowing the upstream README and every changed
|
||||||
|
core rule to remain byte-identical after synchronization.
|
||||||
|
- A byte-for-byte core-sync gate with a planted divergent-vendor negative control.
|
||||||
|
- Negative controls for commit-message rejection, breaking-change notation,
|
||||||
|
missing Markdown links, leaked prose, namespacing violations, missing rule proof
|
||||||
|
sections, missing changelog/version bumps, stale issue bodies, label drift, and
|
||||||
|
consumer freshness.
|
||||||
|
- Acceptance checks for the remaining binding parsing, namespacing, planning,
|
||||||
|
design, dependency-injection, migration, and package-testing rules.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Breaking:** Renamed the non-conventional `CONVENTIONS.md` overview to
|
||||||
|
`conventions.md`; inbound repository links now use the lowercase path.
|
||||||
|
- **Breaking:** Reconciled the target graph, DI seams, namespace exception,
|
||||||
|
protocol-creation threshold, test-framework scope, and dependency direction
|
||||||
|
into one non-contradictory architecture. Consumers must review these rules
|
||||||
|
before updating their freshness marker.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Made forbidden-byte matching portable across POSIX shells so Linux `sh` does
|
||||||
|
not treat hexadecimal escape text inside the gates as a leak.
|
||||||
|
- Replaced the unavailable hosted priority-label action with a repository-owned
|
||||||
|
Forgejo API script and deterministic add, remove, and no-op fixtures.
|
||||||
|
- Renamed the consumer workflow from conformance to freshness so its name matches
|
||||||
|
its actual proof: it compares the shared-rules version marker and does not claim
|
||||||
|
semantic conformance.
|
||||||
|
- Strengthened the commit hook to enforce Conventional Commit types, the 72-column
|
||||||
|
limit, automated-author attribution prohibitions, and the em-dash rule.
|
||||||
|
- Made missing SwiftFormat or SwiftLint fail instead of silently weakening a commit
|
||||||
|
that contains Swift files.
|
||||||
|
- Repaired stale repository-relative links and narrowed the shared-path exception
|
||||||
|
in the folder-grouping rule so it no longer contradicts the default mandate.
|
||||||
|
- Scoped the Apple framework policy to Apple client/UI targets so it no longer
|
||||||
|
contradicts the Swift Linux, Windows, WASM, and server rules.
|
||||||
|
- Normalized ExtremePackaging terminology and examples to the repository's
|
||||||
|
one-package/many-targets architecture, removing impossible nested manifests.
|
||||||
|
- Classified the repository's `VERSION` marker as a conventional root filename and
|
||||||
|
repaired the file-naming rule's non-Conventional commit example.
|
||||||
|
- Removed the dead GitHub workflow badge and documented the Forgejo fallback and
|
||||||
|
local source of verification evidence.
|
||||||
|
- Moved the shell-only consumer freshness job from an unproven `macos` label to
|
||||||
|
the repository's demonstrated `ubuntu-latest` Forgejo runner label.
|
||||||
|
- Flattened the sole consumer workflow template from the one-file `templates/`
|
||||||
|
directory to `rules-freshness.yml` at the repository root.
|
||||||
|
|
||||||
|
## [1.1.2]
|
||||||
|
|
||||||
|
- **scripts/leak-gate.sh (.build exclusion):** all three scans and the file count now
|
||||||
|
exclude `.build/` directories. Fetched dependency checkouts carry their own vendor
|
||||||
|
names, absolute-path examples, and em dashes in documentation the repo does not own
|
||||||
|
and git does not track; scanning them made the gate permanently red on any consumer
|
||||||
|
with resolved dependencies, which trains readers to ignore it. Found by a rule-canon
|
||||||
|
audit on 2026-07-04 where the gate failed consumers solely on `.build/checkouts/`
|
||||||
|
markdown.
|
||||||
|
|
||||||
|
## [1.1.1]
|
||||||
|
|
||||||
|
- **scripts/leak-gate.sh (charter-file exception):** the vendor-name scan now excludes the
|
||||||
|
two agent charter files at the repository ROOT, where the filename is the tool's
|
||||||
|
addressing interface (function, not attribution). Root only; the same name in any nested
|
||||||
|
file still fails, and the exception is fault-injection tested downstream. A permanently
|
||||||
|
red gate trains readers to ignore it, which is worse than no gate.
|
||||||
|
|
||||||
|
## [1.1.0]
|
||||||
|
|
||||||
|
- **documentation.md Rule 0 (DocC scope):** DocC is packages-only (never apps), and only
|
||||||
|
for packages that are public OR heavily used. Non-qualifying packages skip the DocC
|
||||||
|
catalog entirely. Narrows the former "every project" framing.
|
||||||
|
|
||||||
|
A conformance pass brought the repository closer to agreement with its own rules.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
|||||||
20
third_party/rules-swift/CONVENTIONS.md
vendored
20
third_party/rules-swift/CONVENTIONS.md
vendored
@ -28,9 +28,10 @@ tooling.
|
|||||||
1. **Optimal over fast.** Respect existing code and idioms. Clarify ambiguity
|
1. **Optimal over fast.** Respect existing code and idioms. Clarify ambiguity
|
||||||
before coding rather than assuming requirements. When a real trade-off exists,
|
before coding rather than assuming requirements. When a real trade-off exists,
|
||||||
surface two or three options instead of guessing.
|
surface two or three options instead of guessing.
|
||||||
2. **Progressive architecture.** Start with the simplest thing that works. Add a
|
2. **Progressive architecture.** Start with the simplest thing that works. A
|
||||||
protocol only when a second concrete consumer exists. Generalise only when a
|
protocol is earned by a real cross-target seam or by multiple implementations;
|
||||||
pattern has actually emerged. Do not pre-abstract.
|
do not wrap a lone same-target concrete type preemptively. Generalise only when
|
||||||
|
a pattern has actually emerged.
|
||||||
3. **Make impossible states unrepresentable.** Use exhaustive enums with
|
3. **Make impossible states unrepresentable.** Use exhaustive enums with
|
||||||
associated values. Never force-unwrap (`!`, `try!`) in shipping code. Errors
|
associated values. Never force-unwrap (`!`, `try!`) in shipping code. Errors
|
||||||
carry both a human-readable reason and an actionable recovery path.
|
carry both a human-readable reason and an actionable recovery path.
|
||||||
@ -55,9 +56,11 @@ tooling.
|
|||||||
|
|
||||||
## Namespacing and file layout
|
## Namespacing and file layout
|
||||||
|
|
||||||
- **Every public type lives under an `enum`/`struct` namespace that mirrors its
|
- **Every public implementation type lives under an `enum`/`struct` namespace
|
||||||
folder.** No public type at file scope. Reading `Module.Sub.Leaf` should tell you
|
that mirrors its folder.** The only top-level public contracts are protocols
|
||||||
where the type lives and what it does.
|
and supporting value/null/preview types in the foundation-only
|
||||||
|
`SharedProtocols` target, where the module itself is the namespace. Reading
|
||||||
|
`Module.Sub.Leaf` should tell you where every other type lives and what it does.
|
||||||
- **Namespace anchors are caseless `enum`.** Use `struct` only when the type is
|
- **Namespace anchors are caseless `enum`.** Use `struct` only when the type is
|
||||||
also a value. Never `class` for a namespace. Shared mutable state is an `actor`
|
also a value. Never `class` for a namespace. Shared mutable state is an `actor`
|
||||||
or an injected value, never a `class`.
|
or an injected value, never a `class`.
|
||||||
@ -92,7 +95,10 @@ Tiledown builds and runs on Apple platforms and Linux, with Windows where explic
|
|||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
- Use the **Swift Testing** framework: `@Test`, `@Suite`, `#expect`. Not XCTest.
|
- Use the **Swift Testing** framework for unit and integration tests: `@Test`,
|
||||||
|
`@Suite`, `#expect`. Use XCTest only where the platform harness requires it,
|
||||||
|
notably XCUITest, or while migrating an existing suite that cannot move in one
|
||||||
|
change.
|
||||||
- One behaviour per test. Descriptive names. Deterministic data. No live network
|
- One behaviour per test. Descriptive names. Deterministic data. No live network
|
||||||
or filesystem dependence; inject test doubles.
|
or filesystem dependence; inject test doubles.
|
||||||
- One test target per source target, named `<SourceTarget>Tests`.
|
- One test target per source target, named `<SourceTarget>Tests`.
|
||||||
|
|||||||
93
third_party/rules-swift/README.md
vendored
93
third_party/rules-swift/README.md
vendored
@ -1,6 +1,5 @@
|
|||||||
# Swift coding rules (canonical)
|
# Swift coding rules (canonical)
|
||||||
|
|
||||||
[](https://github.com/mihaelamj/rules-swift/actions/workflows/leak-gate.yml)
|
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
|
|
||||||
The canonical, scrubbed Swift coding rules. This is the **Swift domain**: the
|
The canonical, scrubbed Swift coding rules. This is the **Swift domain**: the
|
||||||
@ -8,12 +7,38 @@ language- and platform-specific craft of writing Swift well. It sits on top of a
|
|||||||
vendored **core spine** of cross-cutting engineering discipline (see
|
vendored **core spine** of cross-cutting engineering discipline (see
|
||||||
[`core/`](core/)).
|
[`core/`](core/)).
|
||||||
|
|
||||||
Each file here is one Swift rule area. `CONVENTIONS.md` is the short overview; this
|
Each file here is one Swift rule area. [conventions.md](conventions.md) is the short overview; this
|
||||||
repository is the full set, with the cross-cutting discipline vendored under
|
repository is the full set, with the cross-cutting discipline vendored under
|
||||||
[`core/`](core/). Examples use a sample tile-based static site generator named
|
[`core/`](core/). Examples use a sample tile-based static site generator named
|
||||||
Tiledown (the `TileKit` library plus the `tile-down` executable); replace the
|
Tiledown (the `TileKit` library plus the `tile-down` executable); replace the
|
||||||
example names with your project's when you adopt these.
|
example names with your project's when you adopt these.
|
||||||
|
|
||||||
|
## Repository verification
|
||||||
|
|
||||||
|
Run the complete local gate before proposing a change:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
scripts/check-repo.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The gate requires `git`, a POSIX shell, Perl, and `jq`; the live issue-body audit
|
||||||
|
also requires `curl`. The core-sync check uses a sibling
|
||||||
|
`rules-engineering-discipline` clone when present and otherwise fetches the public
|
||||||
|
upstream. A missing verifier is a failure, not a skipped check.
|
||||||
|
|
||||||
|
The Codeberg origin uses Forgejo. Forgejo reads `.github/workflows` as a fallback
|
||||||
|
when `.forgejo/workflows` is absent, but a workflow file alone does not prove that
|
||||||
|
Actions are enabled or that a matching runner is online. The local gate output is
|
||||||
|
therefore the authoritative evidence for this repository.
|
||||||
|
|
||||||
|
These gates describe the current tree and enforce future commits. The published
|
||||||
|
history contains legacy commit subjects that predate the current hook; preserving
|
||||||
|
public commit identities takes precedence over rewriting that history.
|
||||||
|
|
||||||
|
The scheduled tracker gate runs `scripts/check-issue-body-staleness.sh` against
|
||||||
|
open Codeberg issues and compares repository labels with `.github/labels.json`.
|
||||||
|
Run it manually when auditing tracker state between schedules.
|
||||||
|
|
||||||
## The core spine
|
## The core spine
|
||||||
|
|
||||||
The cross-cutting engineering discipline that governs *how* work is done and
|
The cross-cutting engineering discipline that governs *how* work is done and
|
||||||
@ -70,6 +95,37 @@ domain that enforces them.
|
|||||||
behind a protocol seam.
|
behind a protocol seam.
|
||||||
- [linux-server.md](linux-server.md) - server-side operational rules for the
|
- [linux-server.md](linux-server.md) - server-side operational rules for the
|
||||||
`serve` command and any networking.
|
`serve` command and any networking.
|
||||||
|
- [app-intents.md](app-intents.md) - MANDATORY for Siri, Shortcuts, and Spotlight
|
||||||
|
actions: one adapter target, intents in a package declared by `AppIntentsPackage`,
|
||||||
|
dependencies registered not passed, every word a localized resource the system
|
||||||
|
resolves late, plural forms per language, and how to test the languages.
|
||||||
|
- [versioning.md](versioning.md) - MANDATORY for anything that ships a version: one
|
||||||
|
`VERSION` file read as Semantic Versioning 2.0.0, the store's short version is the
|
||||||
|
core only, the build number is the first-parent commit count of the release branch,
|
||||||
|
generated by a build-tool plugin that names its inputs, no silent fallback, full
|
||||||
|
history in CI, append-only release history.
|
||||||
|
- [app-logging.md](app-logging.md) - MANDATORY for every shipped app: one given recorder,
|
||||||
|
entries that are facts and never the person's content, every intent and state change
|
||||||
|
logged, a Developer menu in Debug and Beta only, and a report the person chooses to send.
|
||||||
|
- [crash-and-state-reporting.md](crash-and-state-reporting.md) - MANDATORY with the log:
|
||||||
|
MetricKit and StateReporting behind `canImport` and availability, the system's own crash
|
||||||
|
reports as a second door, one crash seen twice is one, states joined to the log.
|
||||||
|
- [help.md](help.md) - MANDATORY for every app that ships to people: one file of words,
|
||||||
|
a Help Book on the Mac that the system makes the Help menu from, an in-app help sheet and
|
||||||
|
a page in the Settings app on an iPad and an iPhone, tips (TipKit) at the first thing to
|
||||||
|
do, each platform in its own words, and About with the version the build stamped.
|
||||||
|
- [accessibility.md](accessibility.md) - MANDATORY for every app and UI framework: text through
|
||||||
|
text styles with the app's zoom as a shift of Dynamic Type, every control named by what it
|
||||||
|
does, 28 point targets, every gesture with an alternative action or adjustable element,
|
||||||
|
contrast as arithmetic, Reduce Motion, announcements, layer-drawn UI exposed as elements, a
|
||||||
|
core that describes and surfaces that must declare they realize it, and an audit that fails
|
||||||
|
the build against a ledger of known findings.
|
||||||
|
- [iphone-duo.md](iphone-duo.md) - MANDATORY for every iPhone app and UI framework: the foldable
|
||||||
|
iPhone has two displays and many poses and resizes the app while it runs; layout from size
|
||||||
|
classes and the space, never the screen, idiom or orientation; the size read on every change;
|
||||||
|
state held above anything a size class swaps; reserved regions, arrangement views and the
|
||||||
|
hinge (iOS 27.1) in their places; the system's vertical bars; asymmetric safe areas; scenes;
|
||||||
|
a test across the changes between poses; and a stated list of what Apple has not documented.
|
||||||
- [testing.md](testing.md) - Swift Testing, `@Test` / `#expect`, test isolation.
|
- [testing.md](testing.md) - Swift Testing, `@Test` / `#expect`, test isolation.
|
||||||
- [formatting-and-linting.md](formatting-and-linting.md) - SwiftFormat and
|
- [formatting-and-linting.md](formatting-and-linting.md) - SwiftFormat and
|
||||||
SwiftLint, enforced by a pre-commit hook and again in CI.
|
SwiftLint, enforced by a pre-commit hook and again in CI.
|
||||||
@ -78,7 +134,8 @@ domain that enforces them.
|
|||||||
authoritative documentation index, not from memory: query it with keywords and
|
authoritative documentation index, not from memory: query it with keywords and
|
||||||
read the results for Apple-API facts; it is a search engine, not an agent.
|
read the results for Apple-API facts; it is a search engine, not an agent.
|
||||||
- [framework-policy.md](framework-policy.md) - stay inside Apple's own SDKs and the
|
- [framework-policy.md](framework-policy.md) - stay inside Apple's own SDKs and the
|
||||||
Swift-native ecosystem; Linux falls back to C or C++ only.
|
Swift-native ecosystem when selecting an Apple client/UI framework; portable
|
||||||
|
libraries and servers follow the cross-platform rules.
|
||||||
|
|
||||||
## Packages
|
## Packages
|
||||||
|
|
||||||
@ -96,30 +153,32 @@ domain that enforces them.
|
|||||||
## ExtremePackaging
|
## ExtremePackaging
|
||||||
|
|
||||||
The granular packaging doctrine lives under [`exp/`](exp/): a monorepo split into
|
The granular packaging doctrine lives under [`exp/`](exp/): a monorepo split into
|
||||||
many single-responsibility SPM packages with explicit, unidirectional dependencies,
|
many single-responsibility SwiftPM targets in one active package, with explicit,
|
||||||
for isolated compilation, parallel builds, and per-package testability. The two
|
unidirectional dependencies for isolated compilation, parallel builds, and
|
||||||
|
per-target testability. The two
|
||||||
core files are loaded on every Swift session; the rest load on demand.
|
core files are loaded on every Swift session; the rest load on demand.
|
||||||
|
|
||||||
- [exp/critical-rules.md](exp/critical-rules.md) - core: the five rules that gate
|
- [exp/critical-rules.md](exp/critical-rules.md) - core: the five rules that gate
|
||||||
every package decision (single responsibility, explicit deps, granularity,
|
every target decision (single responsibility, explicit deps, granularity,
|
||||||
naming, layer architecture).
|
naming, layer architecture).
|
||||||
- [exp/when-to-create.md](exp/when-to-create.md) - core: the decision tree and
|
- [exp/when-to-create.md](exp/when-to-create.md) - core: the decision tree and
|
||||||
DO/DON'T list for when a new package is warranted.
|
DO/DON'T list for when a new target is warranted.
|
||||||
- [exp/implementation-patterns.md](exp/implementation-patterns.md) - patterns for
|
- [exp/implementation-patterns.md](exp/implementation-patterns.md) - patterns for
|
||||||
foundation, feature, middleware, API, component, and font packages, and for
|
foundation, feature, middleware, API, component, and font targets, and for
|
||||||
aggregators.
|
aggregators.
|
||||||
- [exp/dependency-management.md](exp/dependency-management.md) - cross-package
|
- [exp/dependency-management.md](exp/dependency-management.md) - cross-target and
|
||||||
dependencies, platform-conditional code, avoiding cycles.
|
external-package dependencies, platform-conditional code, avoiding cycles.
|
||||||
- [exp/package-swift.md](exp/package-swift.md) - the closure-with-local-variables
|
- [exp/package-swift.md](exp/package-swift.md) - the closure-with-local-variables
|
||||||
pattern for the `deps`, `allProducts`, and `targets` arrays.
|
pattern for the `deps`, `allProducts`, and `targets` arrays.
|
||||||
- [exp/common-mistakes.md](exp/common-mistakes.md) - anti-patterns paired with the
|
- [exp/common-mistakes.md](exp/common-mistakes.md) - anti-patterns paired with the
|
||||||
canonical fix, for code review of `Package.swift` changes.
|
canonical fix, for code review of `Package.swift` changes.
|
||||||
- [exp/app-target.md](exp/app-target.md) - adding or modifying an app target
|
- [exp/app-target.md](exp/app-target.md) - adding or modifying an app target
|
||||||
(SwiftUI, UIKit, AppKit entry points).
|
(SwiftUI, UIKit, AppKit entry points).
|
||||||
- [exp/testing.md](exp/testing.md) - the per-package test target and cross-package
|
- [exp/testing.md](exp/testing.md) - the per-source-target test target and
|
||||||
test doubles (architecture angle; full rules in [testing.md](testing.md)).
|
cross-target test doubles (architecture angle; full rules in
|
||||||
- [exp/migration.md](exp/migration.md) - extracting code into a new package or
|
[testing.md](testing.md)).
|
||||||
splitting a large one.
|
- [exp/migration.md](exp/migration.md) - extracting code into a new target or
|
||||||
|
splitting a large target.
|
||||||
- [exp/build-performance.md](exp/build-performance.md) - the build-time rationale
|
- [exp/build-performance.md](exp/build-performance.md) - the build-time rationale
|
||||||
behind the granularity.
|
behind the granularity.
|
||||||
- [exp/verification.md](exp/verification.md) - the final checklist before an
|
- [exp/verification.md](exp/verification.md) - the final checklist before an
|
||||||
@ -141,14 +200,16 @@ The UI rules live under [`ui/`](ui/). Any code with UI keeps the renderer thin o
|
|||||||
- [ui/native-per-platform.md](ui/native-per-platform.md) - MANDATORY: optimize for the user via genuinely native UI per platform; per-device divergence is welcome; never trade the native experience for developer or test convenience or a write-once lowest-common-denominator. Governs and is superior to three-renderers.
|
- [ui/native-per-platform.md](ui/native-per-platform.md) - MANDATORY: optimize for the user via genuinely native UI per platform; per-device divergence is welcome; never trade the native experience for developer or test convenience or a write-once lowest-common-denominator. Governs and is superior to three-renderers.
|
||||||
- [ui/three-renderers.md](ui/three-renderers.md) - build the same UI in all three Apple SDKs (SwiftUI, UIKit, AppKit) over the one model: the architecture's payoff and a comparison instrument for which framework behaves best in a scenario. Subordinate to native-per-platform: applies only where the same UI is also the best native UI.
|
- [ui/three-renderers.md](ui/three-renderers.md) - build the same UI in all three Apple SDKs (SwiftUI, UIKit, AppKit) over the one model: the architecture's payoff and a comparison instrument for which framework behaves best in a scenario. Subordinate to native-per-platform: applies only where the same UI is also the best native UI.
|
||||||
- [ui/swiftui-views.md](ui/swiftui-views.md) - the SwiftUI renderer: view architecture and identity.
|
- [ui/swiftui-views.md](ui/swiftui-views.md) - the SwiftUI renderer: view architecture and identity.
|
||||||
|
- [ui/pane-layout.md](ui/pane-layout.md) - MANDATORY for apps that lay a document out in panes, columns or pages: the split sizes a pane and the content does not (contain and clip), optional parts decided by a measurement, a header that fits the narrowest pane, columns by the room the parts need (not a fixed width), a saved layout for each way the device is held, the whole area for the document, a platform view brought up to date by its inputs and keeping its controller, and a picture of each arrangement.
|
||||||
|
- [ui/appuikit.md](ui/appuikit.md) - MANDATORY for AppKit and UIKit code: import `AppUIKit` and name its `AppUI*` types, never an `NS` or `UI` type or a direct SDK import; add a missing alias to the layer first.
|
||||||
- [ui/uikit-views.md](ui/uikit-views.md) - the UIKit renderer: `UIViewController`/`UIView` adapters, diffable data sources, re-pull on the change signal.
|
- [ui/uikit-views.md](ui/uikit-views.md) - the UIKit renderer: `UIViewController`/`UIView` adapters, diffable data sources, re-pull on the change signal.
|
||||||
- [ui/appkit-views.md](ui/appkit-views.md) - the AppKit renderer: `NSViewController`/`NSView` adapters, `withObservationTracking`, programmatic menus.
|
- [ui/appkit-views.md](ui/appkit-views.md) - the AppKit renderer: `NSViewController`/`NSView` adapters, `withObservationTracking`, programmatic menus.
|
||||||
- [ui/view-models.md](ui/view-models.md) - view-model responsibilities and patterns.
|
- [ui/view-models.md](ui/view-models.md) - view-model responsibilities and patterns.
|
||||||
- [ui/components.md](ui/components.md) - the component system.
|
- [ui/components.md](ui/components.md) - the component system.
|
||||||
- [ui/colors.md](ui/colors.md) - the color system.
|
- [ui/colors.md](ui/colors.md) - the color system.
|
||||||
- [ui/fonts.md](ui/fonts.md) - font registration in SPM packages.
|
- [ui/fonts.md](ui/fonts.md) - font registration in SwiftPM targets.
|
||||||
- [ui/pom.md](ui/pom.md) - Page Object Model for UI tests: screens behind page objects, identifiers from one shared source.
|
- [ui/pom.md](ui/pom.md) - Page Object Model for UI tests: screens behind page objects, identifiers from one shared source.
|
||||||
- [ui/flowspec.md](ui/flowspec.md) - declarative UI scenarios driven by [FlowSpec](https://github.com/mihaelamj/FlowSpec): one flow, many renderers, replayed headless and on-device.
|
- [ui/flowspec.md](ui/flowspec.md) - declarative UI scenarios driven by [FlowSpec](https://codeberg.org/Mihaela/FlowSpec): one flow, many renderers, replayed headless and on-device.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
1
third_party/rules-swift/VERSION
vendored
Normal file
1
third_party/rules-swift/VERSION
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
2.14.0
|
||||||
73
third_party/rules-swift/accessibility.md
vendored
Normal file
73
third_party/rules-swift/accessibility.md
vendored
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# Accessibility: text that scales, controls that are named and large enough, every gesture with an alternative, and an audit that fails the build
|
||||||
|
|
||||||
|
**Status: MANDATORY for every app and every UI framework.** An app is used by people who see large text, no screen, little contrast, one finger, a switch, a voice or a keyboard. The app is built for them from the first view and checked by a test that fails, not by a look at the end. A UI framework (a split view, a tree, an editor) has a platform-neutral core and a surface for each renderer; the core says what is said and what can be done, and every surface is required to do it.
|
||||||
|
|
||||||
|
Each rule below carries its basis: **documented** (Apple documentation or the Human Interface Guidelines, named so it can be read), **observed** (found by building and running it, with the number), or **design** (a choice, with the reason). Everything marked documented was read in the Cupertino corpus: the Human Interface Guidelines pages Accessibility and VoiceOver, the article Performing accessibility testing, the documentation of `XCUIApplication.performAccessibilityAudit(for:_:)` and `XCUIAccessibilityAuditType`, and the SwiftUI accessibility modifiers.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
- **Accessibility added at the end is a list of findings.** The first audit of an iPad app that had been built carefully, in every other respect, reported 300 findings on five screens (small tap targets, text that did not follow the system's size, secondary text that was too faint, controls named by the default name of their symbol). Each was cheap when the view was written and expensive afterwards.
|
||||||
|
- **A name that is wrong is worse than no name.** The buttons that expand and collapse every row of an outline were announced as "Enter Full Screen" and "Exit Full Screen", the default descriptions of the arrow symbols they drew. A person using VoiceOver was told to do something the button did not do.
|
||||||
|
- **A gesture with no alternative is a locked door.** Dragging a divider to resize panes and dragging a header to rearrange them cannot be done with VoiceOver, Switch Control or the keyboard. Where a gesture is the only way, the feature is not there for those people.
|
||||||
|
- **A framework that leaves it to each surface gets it right once.** A split-view framework with a SwiftUI surface, a layer surface and an AppKit/UIKit surface had each surface decide its own names and sizes, and the surfaces disagreed (a 16 point control on two, a 28 point divider on one). The words and the sizes belong in the core, once, with a test.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
1. **Text follows the system's text size, through text styles.** Every piece of text in a view is a text style (`.body`, `.footnote`, `.caption` and the others) or a font made with `relativeTo:`, never `.font(.system(size:))` alone. The Human Interface Guidelines say to support larger text sizes, at least to 200 percent, and to use the system's Dynamic Type; the default and the least sizes are 17 and 11 points on iOS and 13 and 10 on macOS. A size that is not a text style may be used where the system has no text size setting (macOS), in a branch that says so with a comment `a11y: fixed size` on the same line (acceptance check 1). *Basis: documented (HIG, Accessibility; Typography); observed (an iPad app whose fonts were `.system(size:)`, even with `@ScaledMetric` for the size, was reported by the audit for 69 elements as "Dynamic Type font sizes are partially unsupported", though its text did grow at the largest accessibility size; moving the fonts to text styles left 5).*
|
||||||
|
2. **An app's own zoom composes with Dynamic Type, never replaces it.** An app that lets the person zoom its text (Command-plus) applies the zoom on a touch screen as a number of steps of Dynamic Type, added to the system's size (`DynamicTypeSize.allCases` is ordered, so the shift is index arithmetic, clamped at both ends), applied once at the outermost window (an environment flag stops an inner window shifting it again). On the Mac, which has no Dynamic Type, the zoom multiplies the point size. Metrics that are not text (icon sizes, heights, spacing) use `@ScaledMetric(relativeTo:)`, so they grow with the text. *Basis: documented (SwiftUI `ScaledMetric`, `DynamicTypeSize`, `dynamicTypeSize(_:)`); design (the zoom as a shift, so that one setting does not fight the other).*
|
||||||
|
3. **Every control has a name that says what it does.** A control with no text on it (an icon button) is built as `Button("Close All", systemImage: "...") { }` with `.labelStyle(.iconOnly)`, or has `.accessibilityLabel`. The name starts with a verb, does not say "button", and is the same name on every surface that shows the control (a core owns it, rule 11). A symbol's default description is never the name. A hint says what the control does when that is not clear from the name. A list or an outline is named too: on the Mac a SwiftUI `List` is an `NSOutlineView`, and the audit reported each as an element with no description until it had `.accessibilityLabel("Files")` or `("Outline")`; a text field whose only text is a placeholder has no label either, and gets one. A column drawn beside text (a ruler of line numbers) is not an element of its own and is hidden. *Basis: documented (HIG, VoiceOver: alternative labels for all key elements; Voice Control users say the name to tap the control); observed (the "Enter Full Screen" case above; on the Mac, two outlines, a search field and a ruler reported with no description).*
|
||||||
|
4. **A tap target is at least 28 by 28 points on iOS and 20 by 20 on macOS, whatever is drawn in it.** The glyph may be 16 points; the target is the rule. A modifier gives an icon the target without changing the drawing (`.frame(minWidth: 28, minHeight: 28).contentShape(Rectangle())`). A bar a finger drags (a divider) is hit across 28 points on a touch screen and 16 with a pointer. The default size of a control on iOS is 44 by 44, and the padding around a bezeled element is about 12 points. *Basis: documented (HIG, Accessibility: control sizes, iOS default 44 by 44 and least 28 by 28, macOS 28 and 20); observed (118 "Hit area is too small" findings on five screens: 16 point header controls, 16 point disclosure chevrons, and 16 by 15 point icon buttons; none after the targets were raised).*
|
||||||
|
5. **Every gesture has an alternative that needs no gesture.** A drag to rearrange is a set of named actions on the element (`.accessibilityActions`, a custom action in UIKit, `NSAccessibilityCustomAction` in AppKit): "Swap with Source", "Maximize pane", "Close pane". A drag to resize is an adjustable element: `.accessibilityAdjustableAction` in SwiftUI, `accessibilityIncrement()` and `accessibilityDecrement()` in UIKit, `accessibilityPerformIncrement()` in AppKit, each moving the value by a fixed step and the value read as a percentage. On the Mac a bare element with an adjustable action has no role (the audit reports "Unknown role"), so there the divider is represented as a slider (`accessibilityRepresentation` with a `Slider` stepped by the same step); on iOS the representation made the audit report a slider with nothing drawn, so iOS keeps the adjustable action. The actions and the steps are defined once in the core and tested there. *Basis: documented (HIG, Accessibility: provide alternatives to gestures; VoiceOver; SwiftUI `accessibilityAdjustableAction`, `accessibilityActions`, `accessibilityRepresentation`); observed (the Mac audit's "Unknown role" for the bare element, and the iOS findings with no element for the slider).*
|
||||||
|
6. **Put the name and the actions on the element a person reaches, not on a container that holds controls.** A pane is reached by its title bar: one element named by the title, valued by its position and state ("2 of 3, focused"), with the actions of rule 5. Wrapping the whole pane in `.accessibilityElement(children: .contain)` with actions made the audit count every control and every line of text inside it as a small tap target (93 more findings on one app), and took the description of the pane's content from the host that owns it. Decoration (a symbol beside a title, a large number in an empty pane, a status symbol beside a word that says the same) is hidden with `.accessibilityHidden(true)`. *Basis: observed; documented for hiding decoration (HIG, VoiceOver: hide decorative elements, describe meaningful images).*
|
||||||
|
7. **Contrast is at least 4.5 to 1 for text up to 17 points, and 3 to 1 for text of 18 points or bold, in Light, in Dark and with Increase Contrast.** Secondary text is not the system's `.secondary` on a tinted or translucent surface: it is the primary color at a strength that keeps the ratio on the lightest and darkest panes the app draws, and stronger when Increase Contrast is on (a `ShapeStyle` that resolves from the environment). A state shown by a color (good, wrong, to watch) has a darker variant in Light that keeps the ratio, and is never shown by color alone: it has a symbol and a word. Contrast is a number and is tested as one: the WCAG formula over the colors' own components, against the extreme backgrounds, in Light and Dark. *Basis: documented (HIG, Accessibility: color and contrast; Dark Mode and Increase Contrast; not color alone); observed (the audit reported 59 contrast findings, mostly secondary text; a text color of 72 percent primary, 88 with Increase Contrast, computed at 4.5 to 1 or better on backgrounds from white to a mid gray in Light and from black to a dark gray in Dark, removed all but a few).*
|
||||||
|
8. **A change a person cannot see is announced.** When the layout changes (a pane is maximized, restored or closed), the app posts a short sentence (`AccessibilityNotification.Announcement` in SwiftUI, `UIAccessibility.post(notification: .announcement, argument:)` in UIKit, `NSAccessibility.post(element:notification:userInfo:)` with `.announcementRequested` in AppKit). *Basis: documented (HIG, VoiceOver: announce changes; SwiftUI `AccessibilityNotification`).*
|
||||||
|
9. **Reduce Motion turns springs and slides into a snap.** A view that animates its layout reads the setting (`accessibilityReduceMotion` in SwiftUI, `UIAccessibility.isReduceMotionEnabled`, `NSWorkspace.accessibilityDisplayShouldReduceMotion`) and does not animate while it is on; a host that chose no animation keeps it off whatever the system says. *Basis: documented (HIG, Accessibility: Reduce Motion; the settings list of Performing accessibility testing).*
|
||||||
|
10. **UI drawn on layers is exposed element by element.** A surface that draws panes, dividers and glyphs on `CALayer`s has no views to expose, so it builds accessibility elements (`UIAccessibilityElement` with the frame in container space, `NSAccessibilityElement` with the frame in parent space) from the same descriptions after every layout change, with activate, increment and decrement wired to the same operations as a tap and a drag. *Basis: documented (UIKit and AppKit accessibility element APIs); observed (the elements' labels and frames are read back in a headless test).*
|
||||||
|
11. **The core describes, the surfaces realize, and the protocol makes a surface say so.** A UI framework with a platform-neutral core puts in the core, with no platform framework: the words of every control (label and hint), the minimum target size, the description of each element (label, value, actions), the step of an adjustable element, and the announcement sentences. It defines a protocol each surface must adopt that declares which duties it fulfils (an option set: element per pane, control labels, adjustable dividers, actions, announcements, reduced motion), and a test per surface requires the full set, so a new surface cannot be added without saying, and a duty cannot be dropped without a failing test. Platform setters that differ between AppKit and UIKit (label, value, hint, custom actions, announcement, Reduce Motion) live once in the cross-platform layer (AppUIKit, `ui/appuikit.md`). *Basis: design; documented for the underlying APIs.*
|
||||||
|
12. **The audit is a UI test that fails.** Every screen and state of an app is audited in a UI test with `app.performAccessibilityAudit()` (iOS 17, macOS 14, Xcode 16.3), at least: the first screen, a document open, a selection made, each sheet, each tab of a phone, portrait and landscape, and Dark. The handler lets a finding pass only if it is on a ledger of known findings kept in the test target, each entry with the audit type, the element's label, the screens it is allowed on and the reason, and the reason says when the cause is not found. A finding not on the ledger fails the test, so nothing new gets in, and a fix is the deletion of its line. An entry may also name the kinds of element it covers (a disabled container, the Touch Bar the system adds on the Mac), and a finding with no element is let pass only by an entry with no label. A disabled control is let pass for contrast (inactive components are exempt in WCAG 1.4.3). Each audited screen is kept as a picture, and **a finding is checked against the picture before it is believed or listed**: measure the pixels (the text color against its background, with the WCAG formula) and record a finding the pixels contradict as a false positive with the measured ratio (the audit reported "Contrast failed" for titles that measure 13.9 to 1 and labels that measure 17.1 to 1). The audit enlarges the text while it checks; an app whose layout adapts at accessibility sizes (rule 14) changes while it is audited, so findings arrive without an element, and those are listed on their own, narrowly, with the reason. The macOS SDK has no member for the text-clipped and Dynamic Type checks, so a ledger shared with the Mac names them by their raw values (131072 and 65536, observed). The Mac app is audited too (`performAccessibilityAudit` is available from macOS 14); it found, on a window that had passed on the iPad, the divider with no role, two outlines and a search field with no description, and a ruler. *Basis: documented (`performAccessibilityAudit`; Performing accessibility testing: automate with audits in UI tests); design (the ledger); observed (the Mac findings and the pixel measurements).*
|
||||||
|
13. **What the audit cannot do is done by hand and said so.** VoiceOver, Voice Control, Switch Control and Assistive Access need a physical device; the Simulator does not run them. The audit is run at the default text size and in Dark; it does not read a screen as VoiceOver does and does not judge whether a name is a good one. An app that has not been used with VoiceOver on a device says "audited, not used with VoiceOver", never "accessible". *Basis: documented (Performing accessibility testing: the settings and the tools that need a device).*
|
||||||
|
14. **Layout at accessibility text sizes is part of the work.** At the largest accessibility size (AX5) a window of several panes cannot show them side by side. The arrangement changes (one pane at a time, a list instead of a row, a scroll view), and no information is carried only by a string that is cut off. A title that is cut is wrapped or reached another way. The decision is a pure function of the width and the text size (`usesPages(isCompactWidth:textSize:)`) and is tested over every size. *Basis: documented (HIG, Accessibility: support larger text; avoid truncation); observed (an iPad app at AX XXXL showed pane titles cut to one letter and the outline's rows to "r..." in the side-by-side arrangement; after the window took the phone's arrangement at accessibility sizes, the iPad showed the file list as one full-width page with readable text; looked at for the first screen only).*
|
||||||
|
|
||||||
|
## How to test it
|
||||||
|
|
||||||
|
- **The descriptions in the core.** Build a layout of three panes and require: each visible pane described once in reading order with its position and its focus; its actions are maximize or restore, close (only when the host allows closing) and a swap with each other pane; performing a swap action exchanges the two panes; a maximized pane is the only one described and has a restore action and no swaps; each divider's label names its two sides and its value is the percentage; an increment moves it by one step and a decrement back; a step never leaves 0 to 1 for any start; a side holding several panes is named by its count; every control role has a label, a hint, and a minimum target of at least 28.
|
||||||
|
- **Each surface.** Require that its protocol value is the full set; read back from the surface what the core described: labels and values of the elements (SwiftUI: through the model; layer view: the elements' labels, frames and handlers; native views: the labels and values on the views), that incrementing a divider element changes the ratio, that activating the close element closes the pane, that the close target is at least 28 and fits the header. Run on macOS and on an iOS simulator, because the AppKit and UIKit branches are different code (a branch that was never compiled for iOS hid a build failure).
|
||||||
|
- **The cross-platform setters.** Read back what was set (label, value, hint), the names and order of custom actions, that each runs its handler and that setting none clears them. Note: an `NSTextField`'s accessibility value is its text and ignores a value set on it, so state that must be spoken goes into the label there.
|
||||||
|
- **Dynamic Type.** The shift is tested as a pure function: zero changes nothing, steps move along the ordered sizes, a shift past either end stops at the end, up and down by the same steps returns the size. The zoom's step offset is tested from the normal size to both ends with no gap.
|
||||||
|
- **Contrast as arithmetic.** Compute the WCAG ratio from the style's own numbers against the lightest and darkest backgrounds, in Light and Dark, and with Increase Contrast; require 4.5.
|
||||||
|
- **Reduce Motion.** Switch the model's setting and require that animation is off and restored, and that a host's choice of no animation wins.
|
||||||
|
- **The audit.** The UI tests of the iPad (landscape, portrait, Dark) and iPhone (each page, each sheet) run it with the ledger.
|
||||||
|
- **By eye, at the largest size.** Set the simulator's text size (`xcrun simctl ui <device> content_size accessibility-extra-extra-extra-large`), launch, and look; set it back (`content_size large`) when done.
|
||||||
|
|
||||||
|
## Acceptance check
|
||||||
|
|
||||||
|
Run from the repository root. `SOURCES` are the Swift sources of the views, `UITESTS` the UI test folders. Every command must print nothing, except where it says otherwise:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 1. no fixed point size of a font without the marker that says why (the Mac has no Dynamic Type)
|
||||||
|
grep -rnE 'font\(\.system\(size:|\.system\(size:' "$SOURCES" --include='*.swift' | grep -v 'a11y: fixed size'
|
||||||
|
|
||||||
|
# 2. no icon-only button named by its symbol: a Button whose label is just an Image must carry accessibilityLabel in the lines after it (or be a Button(title, systemImage:))
|
||||||
|
find "$SOURCES" -name '*.swift' -exec perl -0ne 'while (/(Button *\{[^}]*\} *label: *\{ *Image\(systemName[^\n]*\n(?:[^\n]*\n){0,6})/g) { print "$ARGV: $1\n" unless $1 =~ /accessibilityLabel/ }' {} +
|
||||||
|
|
||||||
|
# 3. every app target that has UI tests has an audit in them (this must print the files that have one)
|
||||||
|
grep -rl 'performAccessibilityAudit' "$UITESTS" --include='*.swift'
|
||||||
|
|
||||||
|
# 4. the audit's handler is never a bare "return true": a ledger decides
|
||||||
|
grep -rnE 'performAccessibilityAudit *\{[^}]*return true *\}' "$UITESTS" --include='*.swift'
|
||||||
|
|
||||||
|
# 5. the core of a UI framework imports no platform framework and declares the duties (this must print the protocol)
|
||||||
|
grep -rn 'protocol .*AccessibilityRendering' "$CORE" --include='*.swift'
|
||||||
|
grep -rnE '^import (SwiftUI|UIKit|AppKit)' "$CORE" --include='*.swift'
|
||||||
|
|
||||||
|
# 6. no color given to text from the bright system palette without a contrast test beside it
|
||||||
|
grep -rnE 'foregroundStyle\(\.(red|orange|green|yellow)\)|foregroundStyle\(Color\.(red|orange|green|yellow)\)' "$SOURCES" --include='*.swift'
|
||||||
|
|
||||||
|
# 7. the test suites pass on macOS and on an iOS simulator (the two build different branches)
|
||||||
|
swift test
|
||||||
|
xcodebuild test -scheme "<package scheme>" -destination 'platform=iOS Simulator,id=<id>'
|
||||||
|
```
|
||||||
|
|
||||||
|
Promote commands 1, 2, 4 and 6 to a build-failing script with a self-test (a fixture that breaks each must fail it; a fixture that obeys all must pass it), and the tests above to the project's test suite. The check passes when the commands print as stated, the tests pass on both platforms, the audit tests pass with their ledger, and the ledger's entries each have a reason. The claim "accessible" is made only with rule 13 stated: audited at the default size and in Dark (the Mac in its own appearance), not audited at the largest accessibility size, and not used with VoiceOver on a device, until each has been done. Rule 14 is established only for the screens that have been looked at at AX5 (the first screen of the iPad app); the others are not.
|
||||||
|
|
||||||
|
Cross-references: `help.md` (the help and About are screens the audit covers; the spoken form of the version), `ui/appuikit.md` (the cross-platform setters for label, value, hint, actions, announcement and Reduce Motion), `ui/three-renderers.md` (the three surfaces a core serves, each required to fulfil the duties), `ui/swiftui-views.md` and `ui/uikit-views.md` (the views the rules apply to), `testing.md` and `core/testing-discipline.md` (the tests), `core/oracle-first.md` (the core's descriptions and the contrast arithmetic are the answer keys the surfaces are held to), `core/proof-discipline.md` (the ledger is the named frontier: each entry is a measured finding with its cause found or not), `core/no-shortcuts-first-principles.md` (a finding is fixed or listed with its reason, never silenced).
|
||||||
64
third_party/rules-swift/app-intents.md
vendored
Normal file
64
third_party/rules-swift/app-intents.md
vendored
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
# App Intents: one adapter, intents declared by package, every word a late-resolved localized resource
|
||||||
|
|
||||||
|
**Status: MANDATORY for every project that exposes actions to Siri, Shortcuts, Spotlight, or the Action button.** App Intents code lives in one adapter target and nothing else imports the framework. Each intent is thin: it builds one engine command from its parameters, asks an injected door, and reads the outcome. An intent that lives in a Swift package is declared by an `AppIntentsPackage`. Every string a person sees or hears is a localized resource that the system resolves late, never a `String` made inside `perform()`.
|
||||||
|
|
||||||
|
Each rule below carries its basis: **documented** (Apple documentation, named so it can be read) or **observed** (found by running the toolchain; it names the version, and it is re-checked when the toolchain changes).
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
- **The system, not the app, owns the moment of use.** The system creates an intent with no arguments, fills its parameters, and runs `perform()` in a process that may be an extension while the app is not running. Anything the intent needs must therefore be registered, not passed, and anything it says must still be translatable by a process that has other language preferences than the one that wrote it.
|
||||||
|
- **The framework is an adapter, not a layer.** App Intents is a way for the system to reach the app. The engine does not know it exists; the same command a button produces is the command an intent produces, so a test of the command covers both.
|
||||||
|
- **A package is not an app.** The compiler extracts intent metadata when it builds an app or extension target. Source that sits in a package is found only when the app or extension says it includes that package.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
1. **One adapter target.** `import AppIntents` appears in exactly one target, the adapter, and in the composition roots that register it. The engine, the models, and the protocols they share never import it. *Basis: project architecture (`package-architecture.md`, `package-import-contract.md`); the framework itself does not require it.*
|
||||||
|
2. **Each intent is the twin of one engine command.** The intent holds no logic of its own beyond turning parameters into a command (rejecting a value that is not possible, and telling the person) and turning the outcome into words. A test pairs every case of the engine's command enumeration with a twin or with a written classification (exposed, planned, internal, ineligible), with no `default:` in the switch, so a new command cannot be forgotten silently. *Basis: Oracle First (`core/oracle-first.md`): every case handled or classified.*
|
||||||
|
3. **Created without arguments; dependencies are registered.** The system instantiates an intent with `init()`, so an intent takes its collaborators from `@Dependency`, registered once with `AppDependencyManager` by the composition root of every process that can run an intent (the app and each extension). The dependency is one `Sendable` value that holds the door to the engine and nothing the intent could mutate. *Basis: documented (`AppIntent`, `AppDependencyManager`, `AppDependency`).*
|
||||||
|
4. **A test never resolves the dependency through the framework.** `perform()` forwards to an internal `perform(using:)` that takes the door as a parameter, and every test calls that one. *Basis: observed (Xcode 27, Swift 6.4): `@Dependency` outside the intent flow traps, so a test cannot read it.*
|
||||||
|
5. **An intent in a package is declared by a package type.** The adapter target declares a type that conforms to `AppIntentsPackage`; each app and each extension that uses the adapter declares its own `AppIntentsPackage` whose `includedPackages` lists it. A package whose intents are not included is silently absent from Shortcuts. *Basis: documented (`AppIntentsPackage`).*
|
||||||
|
6. **Every word is a localized resource.** The title, the description, each parameter title, each display representation of an enum or entity, each dialog, each error, each parameter summary, and each App Shortcut phrase and short title is localized, in the languages the product ships. English is the source language. *Basis: documented (`Adding parameters to an app intent`: "Always provide localized strings for app intents, App Shortcuts, and their parameters").*
|
||||||
|
7. **The bundle is named.** `LocalizedStringResource` and `String(localized:)` look in the main bundle unless told otherwise, and the main bundle is not the package. Every resource the adapter writes names the adapter's bundle (`.atURL(Bundle.module.bundleURL)`), through one helper, so no call site can forget. *Basis: documented (`LocalizedStringResource`, `#bundle` proposal: code in a framework or package must specify the bundle).*
|
||||||
|
8. **Late resolution.** What the system shows or speaks is handed over as a `LocalizedStringResource`, not as a `String`: `IntentDialog(_:)` and `IntentDialog(full:supporting:)` take resources, and an error conforms to `CustomLocalizedStringResourceConvertible`. The system resolves it in the language of the surface that shows it, which can differ from the app's. `IntentDialog(stringLiteral:)` of a string built in `perform()` is a violation. *Basis: documented (`LocalizedStringResource`: "App Intents ... late resolution ... the Siri UI to potentially use different localization preferences").*
|
||||||
|
9. **A sentence with interpolation keeps its plural forms.** Build the resource from a `String.LocalizationValue` and the bundle with `LocalizedStringResource(_:table:locale:bundle:comment:)`; the key is the format with its placeholders (`%lld new messages.`), and the catalog holds one plural variation per form of each language: one and other in English and German, one, few and other in Croatian. Never write "item(s)" and never decide a singular in Swift. *Basis: documented (initializer); the key derivation and the plural forms **observed**.*
|
||||||
|
10. **A sentence is one key.** A clause that is sometimes present is its own whole sentence with its own key, chosen in Swift, and not a translated fragment appended to another: word order, agreement, and punctuation differ between languages. Where one sentence holds several numbers, the translation may read them as a label and a number, or use substitutions, so that no noun has to agree with a count it is not next to. *Basis: localization practice for string catalogs; the plural substitutions **observed**.*
|
||||||
|
11. **Limits are checked once, in the adapter, and told.** A value outside what a command may take is a failure the person reads, never a value the engine is left to refuse. Constants an attribute needs (a range) must be literals in the attribute; keep one source and a test that the literal equals it. *Basis: observed for the literal requirement; the rest is the thin-intent rule.*
|
||||||
|
12. **A restricted choice is an `AppEnum` or an entity with a query.** An enum is `String`-backed and `RawRepresentable`; a finite list is never a free `String` parameter. An entity names only what a person already sees and carries no content of a message. *Basis: documented (`AppEnum`, `AppEntity`).*
|
||||||
|
13. **`perform()` is concurrency-safe by construction.** The system runs it on an arbitrary queue and an `AppIntent` is `Sendable`; work that touches UI is marked `@MainActor`, and the intent holds no mutable state. *Basis: documented (`Creating your first app intent`, `AppIntent`).*
|
||||||
|
14. **Phrases name the app.** Each `AppShortcutsProvider` phrase contains `\(.applicationName)`, and the phrases are localized in the file Apple's sample uses for them (`AppShortcuts.strings`, in the app bundle). *Basis: documented (`AppShortcutsProvider` sample); where the file lives for a package is **not** established and is checked on a device.*
|
||||||
|
15. **Metadata is proved where it is made.** A passing unit test proves the words and the command, not that Shortcuts lists the intent in each language. The proof is a build of the app or extension, then the steps of the Apple guide (Shortcuts, Apps, the app's icon, run), repeated with the device set to each shipped language. Until that is done, the claim is "tested at the reading level, not seen in Shortcuts". *Basis: documented (`Creating your first app intent`: the compiler generates the metadata when an app that contains intents is built).*
|
||||||
|
|
||||||
|
## How to test the words
|
||||||
|
|
||||||
|
- **Resolve in a language by setting `locale` on a copy of the resource** (`var r = resource; r.locale = Locale(identifier: "hr"); String(localized: r)`). That is the documented way and it picks the language. `String(localized:bundle:locale:)` does **not**: the `locale` there formats numbers while the process's languages choose the strings (observed), so a test that uses it reads the machine's language.
|
||||||
|
- **Key sets agree.** A test reads the catalog from disk and the sources, and fails when a sentence in the sources has no entry or an entry is asked for by no source. The scan accepts both `LocalizedStringResource(` and `.init(` and a literal wrapped over lines, since the formatter changes the layout.
|
||||||
|
- **Each key is complete.** Every key has every shipped language, every localization is non-empty, and the arguments a translation reads are exactly the key's (`%1$lld` positions and plural substitutions counted).
|
||||||
|
- **Plural forms are those of the language.** Check the set of categories per language, and spell out expected text for counts that fall in each Croatian category (0, 1, 2, 4, 5, 11, 12, 21, 22, 25, 101), since a wrong category reads as correct English to a reviewer who does not read the language.
|
||||||
|
- **The language asked for is the one read.** Assert that a language the catalog lacks reads as the source language, and that a locale with a region override reads the language of its tag, so a test passes on any machine.
|
||||||
|
- **Mutations.** Mutate the language choice, a plural form of the catalog, and the point where a reading ignores its locale, and require the tests to fail (`core/testing-discipline.md`).
|
||||||
|
|
||||||
|
## Acceptance check
|
||||||
|
|
||||||
|
Run from the repository root, with `ADAPTER` the adapter target's source directory and `SOURCES` the directory of all package sources. Every command must print nothing, except check 2, which must print at least one line:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 1. the framework is imported only by the adapter
|
||||||
|
grep -rlE '^[[:space:]]*import AppIntents' "$SOURCES" --include='*.swift' | grep -v "^$ADAPTER/"
|
||||||
|
|
||||||
|
# 2. the adapter declares an AppIntentsPackage (this one must print a line)
|
||||||
|
grep -rnE ':[[:space:]]*AppIntentsPackage' "$ADAPTER" --include='*.swift'
|
||||||
|
|
||||||
|
# 3. no title, description, or parameter title is a bare literal (each must name the bundle)
|
||||||
|
grep -rnE 'static (let|var) (title|description)[^=]*=[[:space:]]*(IntentDescription\()?"' "$ADAPTER" --include='*.swift'
|
||||||
|
grep -rnE '@Parameter\(title:[[:space:]]*"' "$ADAPTER" --include='*.swift'
|
||||||
|
|
||||||
|
# 4. no dialog is made from a String
|
||||||
|
grep -rnE 'IntentDialog\(stringLiteral:' "$ADAPTER" --include='*.swift'
|
||||||
|
|
||||||
|
# 5. no sentence is resolved to a String inside the adapter's intents
|
||||||
|
grep -rnE 'String\(localized:' "$ADAPTER" --include='*Intent.swift'
|
||||||
|
```
|
||||||
|
|
||||||
|
Promote the five commands to a build-failing script with a self-test (a fixture that breaks each one must fail it; a fixture that obeys all must pass it), and the key-set, completeness, and plural tests above to the project's test suite. The check passes when the commands print as stated, the tests pass, and rule 15 is either done or stated as not done.
|
||||||
|
|
||||||
|
Cross-references: `package-architecture.md` and `package-import-contract.md` (one adapter, one import), `dependency-injection.md` (the door is injected, nothing is a singleton), `testing.md` and `core/testing-discipline.md` (tests and mutations), `concurrency.md` (`Sendable` and `@MainActor`), `core/oracle-first.md` (every command is a twin or classified), `core/proof-discipline.md` (the basis of each claim).
|
||||||
59
third_party/rules-swift/app-logging.md
vendored
Normal file
59
third_party/rules-swift/app-logging.md
vendored
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# App logging: every app writes a structured log, and can turn a moment of it into a report
|
||||||
|
|
||||||
|
**Status: MANDATORY for every app that ships.** An app records what it does as structured entries (names, numbers and categories, never what the person is working on), through one recorder given to it, and has a Developer menu that turns the log around a moment into a report the person may send. The menu exists in a Debug build and a Beta build and not in a Release build.
|
||||||
|
|
||||||
|
Each rule carries its basis: **documented** (Apple documentation, named), **observed** (found by running it), or **design** (a choice, with the reason).
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
- **A bug that cannot be reproduced can still be read.** A log of intents and state changes, with the version of the build, says what the app did before the moment someone marked.
|
||||||
|
- **A log that holds the person's work is a leak.** The log is kept on disk and put in reports; what the person typed or pasted must never be in it.
|
||||||
|
- **A report that is not the person's choice is surveillance.** Nothing is sent by the app; the person reads the message and presses Send.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
1. **One recorder, given.** An app makes its recorder once, at its composition root, and hands it to what logs. No global logger, no singleton (`dependency-injection.md`). A test run gets a recorder that writes nowhere. *Basis: design.*
|
||||||
|
2. **Entries are facts, not content.** An entry has a category, a name, a level, an outcome and fields of numbers, booleans and short names. A field never holds text the person wrote, pasted, or opened; a failure's reason is cut to its first line and a bounded length, and what it quotes between quotes is replaced. A fingerprint (a hash) of the content may stand for it. *Basis: design; the reason quoted text is cut is that a compiler or parser message quotes its input.*
|
||||||
|
3. **Every intent and every change of state is an entry.** What a person does (selection, edit, toggle, command) and what the app then is (loading, done, failed) are written at the one place that makes the change, so the log is complete without each view remembering to log. *Basis: design.*
|
||||||
|
4. **Spans for work that takes time.** Work with a start and an end writes both with its duration and its outcome, and a failure says why. *Basis: design.*
|
||||||
|
5. **The log names the build.** The first entry of a run carries the version of the build in the form `versioning.md` rule 8 gives, so a report says which build it came from. *Basis: design.*
|
||||||
|
6. **It can be switched off by the person, and is on by default.** A defaults key (`-<App>Logging NO`) turns it off. *Basis: design.*
|
||||||
|
7. **The Developer menu is gated in the one file that starts the app.** `#if DEBUG || BETA` sets a flag in the app target's entry file and hands it to the coordinator; package code never reads the compile flag. The project has a `Beta` configuration (Release settings and the `BETA` condition) and the scheme archives with it. A Release build must not contain the menu. *Basis: design; verify it on a built app, not on the code (see the check).*
|
||||||
|
8. **A report is made on the person's request, for any reason.** The menu item is "Send a Report…", not "Report a crash": a slow moment, a wrong picture or a bug is as good a reason. It takes a note, offers what else may go in, and makes a zip. *Basis: design.*
|
||||||
|
9. **A report holds the log around the mark and facts of the window.** The log window is a bounded count of entries before the mark and all after it; the snapshot is counts, switches and fingerprints. Whatever else (the pasted source, the layers of the app being read, the log being read) is **content**: off by default, named to the person in the app's words, and in the report only when chosen. A content file cannot take a name the report keeps for itself or leave the report's folder. *Basis: design.*
|
||||||
|
10. **The report is kept, and is mailed only on the person's choice.** The zip is kept beside the log (the newest twenty), so it can be sent later. "Open Email" opens a new message in the person's mail app with the zip attached and the note in the body; without a mail account the file is offered for sharing. The app sends nothing. *Basis: documented (Apple, the system's mail compose and share interfaces) for the means; design for the choice.*
|
||||||
|
11. **The viewer is the same format.** The tool that reads logs and reports reads this log, refuses a report that holds a link or a path that leaves its folder, and shows the marks. An older viewer refusing a newer category is stated, so viewer and apps ship from one release. *Basis: observed (an older viewer refused the `bug` category).*
|
||||||
|
12. **Every app, not most.** Each app of a repository has the log, the menu and the report, or its absence is a named row in the repository's audit with the reason (a test fixture app is one). *Basis: design.*
|
||||||
|
|
||||||
|
## How to test it
|
||||||
|
|
||||||
|
- **The log is held to the model.** Drive the model through every intent and require an entry for each, and no field of any entry equal to, or containing, the content the test fed it.
|
||||||
|
- **The window of a report against its closed form.** For a log of known length and a mark at a known place, the report holds exactly the entries before and after that the rule states.
|
||||||
|
- **A whole report, from the mark to the zip, read back.** Unpack it with the viewer's reader and require the mark, the snapshot, the README, and the content only when asked for.
|
||||||
|
- **The menu in a built app.** Build Debug, Beta and Release; open the app and read its menu bar through the accessibility interface of that one process. Debug and Beta have the Developer menu; Release does not.
|
||||||
|
- **The questions with a stand-in for the screen.** A scripted asker answers each question in turn so the order and the refusals (no mail account, a cancelled sheet) are tested without a window.
|
||||||
|
|
||||||
|
## Acceptance check
|
||||||
|
|
||||||
|
Run from the repository root. `SOURCES` is the directory of package sources, `APPS` the app targets. Each command must print nothing unless it says it must print a line:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 1. the compile flag is read only in the app targets, never in package code
|
||||||
|
grep -rnE '#if.*(DEBUG|BETA)' "$SOURCES" --include='*.swift' | grep -v -E 'Tests/|#if (DEBUG|BETA).*(canImport|os\()'
|
||||||
|
|
||||||
|
# 2. every app entry file gates the menu (each app must print a line)
|
||||||
|
for f in $(grep -rlE '@main' "$APPS" --include='*.swift'); do grep -Ln 'DEBUG || BETA' "$f"; done
|
||||||
|
|
||||||
|
# 3. a Beta configuration exists in each project (must print a line per project)
|
||||||
|
grep -n 'BETA' "$APPS"/*/project.yml
|
||||||
|
|
||||||
|
# 4. no singleton recorder
|
||||||
|
grep -rnE 'static (let|var) (shared|default|current)[^=]*(Recorder|Logger)' "$SOURCES" --include='*.swift'
|
||||||
|
|
||||||
|
# 5. the Release build has no Developer menu (prints the menu titles of the built app; "Developer" must not appear)
|
||||||
|
# build -configuration Release, launch, then read the menu bar of that process through the accessibility interface
|
||||||
|
```
|
||||||
|
|
||||||
|
Command 5 is run on a built app, once per release, and its result is recorded in the release notes. Promote 1 to 4 to a build-failing script with a self-test.
|
||||||
|
|
||||||
|
Cross-references: `versioning.md` (the version the log names), `crash-and-state-reporting.md` (the crashes and states the log also holds), `dependency-injection.md` (the recorder is given), `testing.md`, `core/oracle-first.md` (the window's closed form), `core/proof-discipline.md` (the basis of each claim), `ui/appuikit.md` (the questions and the mail go through AppUIKit).
|
||||||
14
third_party/rules-swift/code-style.md
vendored
14
third_party/rules-swift/code-style.md
vendored
@ -8,13 +8,23 @@ For the narrower question "should the anchor be `enum`, `struct`, or `class`?",
|
|||||||
|
|
||||||
## Namespacing discipline (mandatory)
|
## Namespacing discipline (mandatory)
|
||||||
|
|
||||||
**Every public type lives under a struct or enum namespace that mirrors its folder on disk.** No public type stays at file scope. The qualified name carries module + folder + role; reading `Module.Sub.Leaf` should be enough to know where the type lives and what it does.
|
**Every public implementation type lives under a struct or enum namespace that
|
||||||
|
mirrors its folder on disk.** The named exception is the foundation-only
|
||||||
|
`SharedProtocols` target: its cross-target protocols, small contract values, and
|
||||||
|
null/preview conformers stay at file scope, because the module is their namespace.
|
||||||
|
The qualified name otherwise carries module + folder + role; reading
|
||||||
|
`Module.Sub.Leaf` should be enough to know where the type lives and what it does.
|
||||||
|
|
||||||
This applies to every Swift project. The TileDown-style root + sub-namespace pattern below is one specific instance of this discipline.
|
This applies to every Swift project. The TileDown-style root + sub-namespace pattern below is one specific instance of this discipline.
|
||||||
|
|
||||||
### Core rules
|
### Core rules
|
||||||
|
|
||||||
1. **No exceptions.** Every `public struct / enum / class / actor / protocol` declared at file scope MUST be wrapped in an `extension <Namespace> { ... }` block (or declared inside a nested type). Free-standing top-level public types are not allowed.
|
1. **One named exception.** Outside `SharedProtocols`, every `public struct / enum
|
||||||
|
/ class / actor / protocol` declared at file scope MUST be wrapped in an
|
||||||
|
`extension <Namespace> { ... }` block (or declared inside a nested type).
|
||||||
|
`SharedProtocols` keeps its contract protocols, supporting value types, and
|
||||||
|
null/preview conformers top-level; concrete live implementations never use
|
||||||
|
this exception.
|
||||||
|
|
||||||
2. **Mirror the folder.** When a folder `Sources/Foo/Bar/` holds a type `X`, the type lives at `Foo.Bar.X`. The namespace tree is a literal reflection of the file system tree.
|
2. **Mirror the folder.** When a folder `Sources/Foo/Bar/` holds a type `X`, the type lives at `Foo.Bar.X`. The namespace tree is a literal reflection of the file system tree.
|
||||||
|
|
||||||
|
|||||||
4
third_party/rules-swift/concurrency.md
vendored
4
third_party/rules-swift/concurrency.md
vendored
@ -33,7 +33,7 @@ Strict concurrency is achievable without region-based isolation, complex task hi
|
|||||||
// URLSession is thread-safe; this wrapper is stateless.
|
// URLSession is thread-safe; this wrapper is stateless.
|
||||||
struct ContentFetcher: @unchecked Sendable { ... }
|
struct ContentFetcher: @unchecked Sendable { ... }
|
||||||
```
|
```
|
||||||
- Never combine `@unchecked Sendable` with hand-rolled `NSLock` / `DispatchQueue` in new code. If you need locks, use `actor`.
|
- Never combine `@unchecked Sendable` with hand-rolled `NSLock` / `DispatchQueue` in new code. If you need locks, use `actor`. Where a synchronous API must guard shared state (a protocol requirement, a synchronous engine that cannot be `async`), use `Synchronization.Mutex` (SE-0433): `Mutex<State>` is `Sendable` without `@unchecked`, the state is reachable only inside `withLock`, and the critical section stays visible. Keep the work inside `withLock` short and never call back into the same lock from it.
|
||||||
|
|
||||||
**Why (SE-0302, Swift 5.5).** `Sendable` was introduced because passing arbitrary references across isolation boundaries makes data-race safety impossible to verify locally. The protocol turns *"is this safe to send across a concurrency domain?"* into a property the compiler can check by induction on stored properties. Implicit conformance for value types is the design intent: most structs should be `Sendable` without ceremony, because most value types compose `Sendable` parts. Explicit annotation on public types is for API clarity, not for the compiler.
|
**Why (SE-0302, Swift 5.5).** `Sendable` was introduced because passing arbitrary references across isolation boundaries makes data-race safety impossible to verify locally. The protocol turns *"is this safe to send across a concurrency domain?"* into a property the compiler can check by induction on stored properties. Implicit conformance for value types is the design intent: most structs should be `Sendable` without ceremony, because most value types compose `Sendable` parts. Explicit annotation on public types is for API clarity, not for the compiler.
|
||||||
|
|
||||||
@ -204,7 +204,7 @@ When to **not** use `AsyncStream`: single-callback progress reporting. A `@Senda
|
|||||||
Avoid in new Swift 6 code:
|
Avoid in new Swift 6 code:
|
||||||
|
|
||||||
- `DispatchQueue.main.async { ... }` → use `Task { @MainActor in ... }` or `await MainActor.run { ... }`.
|
- `DispatchQueue.main.async { ... }` → use `Task { @MainActor in ... }` or `await MainActor.run { ... }`.
|
||||||
- `NSLock` / `pthread_mutex` + `@unchecked Sendable` → use `actor`.
|
- `NSLock` / `pthread_mutex` + `@unchecked Sendable` → use `actor`; where the API must stay synchronous, use `Synchronization.Mutex`.
|
||||||
- `withCheckedContinuation` / `withUnsafeContinuation` → only when wrapping legacy callback APIs.
|
- `withCheckedContinuation` / `withUnsafeContinuation` → only when wrapping legacy callback APIs.
|
||||||
- `@MainActor` smeared across non-UI types to silence warnings → fix the design.
|
- `@MainActor` smeared across non-UI types to silence warnings → fix the design.
|
||||||
- `nonisolated(unsafe)` to bypass a warning without thinking about the race → fix the design.
|
- `nonisolated(unsafe)` to bypass a warning without thinking about the race → fix the design.
|
||||||
|
|||||||
35
third_party/rules-swift/core/LICENSE
vendored
Normal file
35
third_party/rules-swift/core/LICENSE
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
Copyright (c) 2026 Mihaela Mihaljevic.
|
||||||
|
|
||||||
|
This repository is dual-licensed by content type.
|
||||||
|
|
||||||
|
PROSE (the rule documents, all .md files)
|
||||||
|
Licensed under the Creative Commons Attribution 4.0 International License
|
||||||
|
(CC BY 4.0). You are free to share and adapt the material, including
|
||||||
|
commercially, provided you give appropriate credit. Full text:
|
||||||
|
https://creativecommons.org/licenses/by/4.0/legalcode
|
||||||
|
SPDX-License-Identifier: CC-BY-4.0
|
||||||
|
|
||||||
|
CODE (any scripts, hooks, or sample configuration)
|
||||||
|
Licensed under the MIT License, reproduced below.
|
||||||
|
SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
2
third_party/rules-swift/core/README.md
vendored
2
third_party/rules-swift/core/README.md
vendored
@ -31,4 +31,4 @@ Point your agent (or yourself) at this directory as always-loaded context. The e
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Prose under CC BY 4.0, any shipped code under MIT. See `LICENSE.md`.
|
Prose under CC BY 4.0, any shipped code under MIT. See [LICENSE](LICENSE).
|
||||||
|
|||||||
@ -107,3 +107,12 @@ A single confirmation is enough; no full ceremony required.
|
|||||||
- Burying the design in a long preamble before the actual question.
|
- Burying the design in a long preamble before the actual question.
|
||||||
- Designing in isolation without reading the existing patterns first.
|
- Designing in isolation without reading the existing patterns first.
|
||||||
- Treating silence as approval.
|
- Treating silence as approval.
|
||||||
|
|
||||||
|
## Acceptance check
|
||||||
|
|
||||||
|
For a non-trivial change, the conversation or tracked design artifact MUST show,
|
||||||
|
before the first source edit: the goal, affected files, public surface, tests, and
|
||||||
|
principal risks, followed by explicit approval. Compare the timestamp or message
|
||||||
|
order with the first implementation edit. If implementation exposed a material
|
||||||
|
scope change, the record MUST also show a revised design and fresh approval before
|
||||||
|
work continued.
|
||||||
|
|||||||
20
third_party/rules-swift/core/file-naming.md
vendored
20
third_party/rules-swift/core/file-naming.md
vendored
@ -7,7 +7,7 @@
|
|||||||
This rule governs committed **documents and assets**: scanned documents, photos, exports, dated records, and the other non-code files a repository accumulates. Two classes are out of scope and are never flagged by the audit below, because a different authority already names them:
|
This rule governs committed **documents and assets**: scanned documents, photos, exports, dated records, and the other non-code files a repository accumulates. Two classes are out of scope and are never flagged by the audit below, because a different authority already names them:
|
||||||
|
|
||||||
- **Source code.** A source file follows its language's own naming convention. A Swift type file is `PascalCase`, named for the single type it holds, and a framework-style internal type may carry a leading underscore (`_ConditionalContent.swift`); a Go file is lowercase; and so on. That language convention is the authority for those files, and the code-style rule of each language domain owns it. This rule does not override it, so source trees are excluded from the audit.
|
- **Source code.** A source file follows its language's own naming convention. A Swift type file is `PascalCase`, named for the single type it holds, and a framework-style internal type may carry a leading underscore (`_ConditionalContent.swift`); a Go file is lowercase; and so on. That language convention is the authority for those files, and the code-style rule of each language domain owns it. This rule does not override it, so source trees are excluded from the audit.
|
||||||
- **Conventional and tool-mandated names.** The uppercase names a tool or ecosystem expects verbatim: the repository-root files `LICENSE`, `CHANGELOG`, `CONTRIBUTING`, `CODEOWNERS`, `NOTICE`, `AUTHORS`, and `README`; the platform infrastructure under `.github/` (`ISSUE_TEMPLATE/`, `PULL_REQUEST_TEMPLATE`, `FUNDING.yml`); and the build and package manifests a toolchain loads by an exact name (`Package.swift`, `Package.resolved`, `Package@swift-5.9.swift`, `Dockerfile`, `Makefile`, `Gemfile`, `Rakefile`, `Podfile`, `Cartfile`, `Fastfile`). Renaming any of them would break the tool that looks for them.
|
- **Conventional and tool-mandated names.** The uppercase names a tool or ecosystem expects verbatim: the repository-root files `LICENSE`, `CHANGELOG`, `CONTRIBUTING`, `CODEOWNERS`, `NOTICE`, `AUTHORS`, `README`, and `VERSION`; the platform infrastructure under `.github/` (`ISSUE_TEMPLATE/`, `PULL_REQUEST_TEMPLATE`, `FUNDING.yml`); and the build and package manifests a toolchain loads by an exact name (`Package.swift`, `Package.resolved`, `Package@swift-5.9.swift`, `Dockerfile`, `Makefile`, `Gemfile`, `Rakefile`, `Podfile`, `Cartfile`, `Fastfile`). Renaming any of them would break the tool that looks for them.
|
||||||
|
|
||||||
## General rules
|
## General rules
|
||||||
|
|
||||||
@ -66,7 +66,7 @@ When renaming, use `git mv` to preserve history:
|
|||||||
git mv "Old File Name.pdf" "old-file-name.pdf"
|
git mv "Old File Name.pdf" "old-file-name.pdf"
|
||||||
```
|
```
|
||||||
|
|
||||||
Commit: `rename: normalize filenames`
|
Commit: `chore(files): normalize filenames`
|
||||||
|
|
||||||
## Audit (before any processing)
|
## Audit (before any processing)
|
||||||
|
|
||||||
@ -76,27 +76,29 @@ Point `<repo>` at the document and asset directories the rule governs, or add a
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Files with spaces
|
# Files with spaces
|
||||||
find <repo> -name "* *" -not -path "*/.git/*" -not -name "*.md"
|
find <repo> -name "* *" -not -path "*/.git/*"
|
||||||
|
|
||||||
# Files with uppercase (documents and assets only; source files and the
|
# Files with uppercase (documents and assets only; source files and the
|
||||||
# conventional or tool-mandated names of the Scope section are out of scope)
|
# conventional or tool-mandated names of the Scope section are out of scope)
|
||||||
find <repo> -regex ".*/[^/]*[A-Z][^/]*" -not -path "*/.git/*" -not -path "*/.github/*" \
|
find <repo> -regex ".*/[^/]*[A-Z][^/]*" -not -path "*/.git/*" -not -path "*/.github/*" \
|
||||||
-not -name "*.md" \
|
-not -name "README*" -not -name "LICENSE*" -not -name "CHANGELOG*" \
|
||||||
-not -name "README*" -not -name "LICENSE" -not -name "CHANGELOG" \
|
-not -name "CONTRIBUTING*" -not -name "CODEOWNERS" -not -name "NOTICE" \
|
||||||
-not -name "CONTRIBUTING" -not -name "CODEOWNERS" -not -name "NOTICE" \
|
-not -name "AUTHORS" -not -name "VERSION" \
|
||||||
-not -name "AUTHORS" \
|
|
||||||
-not -name "Package.swift" -not -name "Package.resolved" -not -name "Package@*.swift" \
|
-not -name "Package.swift" -not -name "Package.resolved" -not -name "Package@*.swift" \
|
||||||
-not -name "Dockerfile" -not -name "Makefile" -not -name "Gemfile" \
|
-not -name "Dockerfile" -not -name "Makefile" -not -name "Gemfile" \
|
||||||
-not -name "Rakefile" -not -name "Podfile" -not -name "Cartfile" -not -name "Fastfile"
|
-not -name "Rakefile" -not -name "Podfile" -not -name "Cartfile" -not -name "Fastfile"
|
||||||
|
|
||||||
# Files with underscores (excluding .git and the tool-mandated .github tree)
|
# Files with underscores (excluding .git and the tool-mandated .github tree)
|
||||||
find <repo> -name "*_*" -not -path "*/.git/*" -not -path "*/.github/*" -not -name "*.md"
|
find <repo> -name "*_*" -not -path "*/.git/*" -not -path "*/.github/*"
|
||||||
|
|
||||||
# HEIC files
|
# HEIC files
|
||||||
find <repo> \( -name "*.heic" -o -name "*.HEIC" \) -not -path "*/.git/*"
|
find <repo> \( -name "*.heic" -o -name "*.HEIC" \) -not -path "*/.git/*"
|
||||||
```
|
```
|
||||||
|
|
||||||
Run over the documents and assets in scope, a clean repo returns nothing from the first three queries: the `.md`, `README`, and conventional-root-file exclusions cover the in-scope exceptions, and the source-tree exclusion keeps the audit off code, which the language's own code-style rule governs (Scope).
|
Run over the documents and assets in scope, a clean repo returns nothing from the
|
||||||
|
first three queries: the `README` and conventional-root-file exclusions cover the
|
||||||
|
in-scope exceptions, and the source-tree exclusion keeps the audit off code, which
|
||||||
|
the language's own code-style rule governs (Scope).
|
||||||
|
|
||||||
## Skip list
|
## Skip list
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@ This is a cross-cutting layout rule. It is most concrete in package managers whe
|
|||||||
|
|
||||||
## One folder per buildable unit is the mandate, not just the default
|
## One folder per buildable unit is the mandate, not just the default
|
||||||
|
|
||||||
For a package manager whose default is one folder per module (target, package, crate), the standing rule is: **every buildable unit maps to exactly one top-level source folder, and that folder is the unit's home** (`Sources/<Unit>/`, `Tests/<Unit>Tests/`). No single `path:` pointing many units into a shared parent, no disjoint `sources:` / `exclude:` slicing to carve multiple units out of one folder. Listing the source root must enumerate every unit; the filesystem boundary IS the unit boundary.
|
For a package manager whose default is one folder per module (target, package, crate), the standing default is: **every buildable unit maps to exactly one top-level source folder, and that folder is the unit's home** (`Sources/<Unit>/`, `Tests/<Unit>Tests/`). Do not point many units into a shared parent or partition them with disjoint `sources:` / `exclude:` lists unless one of the two documented earned exceptions below applies and the exception is justified in the manifest. Listing the source root should enumerate every unit; the filesystem boundary is normally the unit boundary.
|
||||||
|
|
||||||
This supersedes the older "family-parent folder" idea for the unit-grouping case. The file-grouping rules below (grouping FILES *within* a single unit's folder) still apply unchanged. The two earned-exception patterns (same-kind single-file clusters; sub-units sharing an umbrella) are discouraged for new code; flatten unless there is a compelling, documented reason.
|
This supersedes the older "family-parent folder" idea for the unit-grouping case. The file-grouping rules below (grouping FILES *within* a single unit's folder) still apply unchanged. The two earned-exception patterns (same-kind single-file clusters; sub-units sharing an umbrella) are discouraged for new code; flatten unless there is a compelling, documented reason.
|
||||||
|
|
||||||
@ -100,7 +100,7 @@ When reviewing an existing tree:
|
|||||||
|
|
||||||
## How this composes with a package manager (Swift / SPM specifics)
|
## How this composes with a package manager (Swift / SPM specifics)
|
||||||
|
|
||||||
This rule conflicts with SPM's default `Sources/<Target>/<files>` convention when you have many single-file targets, because SPM expects one folder per target. Resolve by giving each affected target an explicit `path:` and `sources:` declaration in the manifest. Multiple targets MAY share the same `path:` as long as each declares a disjoint `sources:` list:
|
For an existing or explicitly approved same-kind single-file cluster, this rule conflicts with SPM's default `Sources/<Target>/<files>` convention because SPM expects one folder per target. Apply the earned exception by giving each affected target an explicit `path:` and `sources:` declaration in the manifest. Multiple targets MAY share the same `path:` only inside that documented exception and only when each declares a disjoint `sources:` list:
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
let importersSrc = "Sources/Importers"
|
let importersSrc = "Sources/Importers"
|
||||||
|
|||||||
@ -94,3 +94,12 @@ Default: `docs/plans/YYYY-MM-DD-<feature-slug>.md` in the repo where the work ha
|
|||||||
- Plans without a file map (you cannot verify scope without one).
|
- Plans without a file map (you cannot verify scope without one).
|
||||||
- Treating the design and the plan as the same thing (the design is "what and why"; the plan is "in what order, with what proofs").
|
- Treating the design and the plan as the same thing (the design is "what and why"; the plan is "in what order, with what proofs").
|
||||||
- Bundling unrelated cleanup into a feature plan (separate plans per concern).
|
- Bundling unrelated cleanup into a feature plan (separate plans per concern).
|
||||||
|
|
||||||
|
## Acceptance check
|
||||||
|
|
||||||
|
A plan conforms when it was written before implementation, names the exact files
|
||||||
|
in scope, orders tasks by dependency, gives every task a runnable verification
|
||||||
|
command and commit message, records constraints and rollback risks, and defines
|
||||||
|
completion with the full relevant gates. At handoff, diff the plan against the
|
||||||
|
implemented file list and task state; any silent scope expansion or stale task is a
|
||||||
|
failure until the plan is updated and re-approved where required.
|
||||||
|
|||||||
53
third_party/rules-swift/crash-and-state-reporting.md
vendored
Normal file
53
third_party/rules-swift/crash-and-state-reporting.md
vendored
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# Crash and state reporting: MetricKit and StateReporting, joined to the app's own log
|
||||||
|
|
||||||
|
**Status: MANDATORY for every app that has the log of `app-logging.md`.** The app asks the system for what it knows about its crashes and hangs (MetricKit), tells the system what state it is in (StateReporting), and brings both into its own log and its reports, so a crash is read next to what the person did before it.
|
||||||
|
|
||||||
|
Each rule carries its basis: **documented**, **observed**, or **design**. Where a claim about Apple's new frameworks was read from the documentation and not run, it says so.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
- **A crash takes the log's last seconds with it.** The system's report has the stack and the process; the app's log has the intents. Neither is enough alone.
|
||||||
|
- **The system can say which state the app was in.** A crash that says "while reading a report in the log viewer" is triaged in seconds; one that says nothing is not.
|
||||||
|
- **The system delivers when it chooses.** An app cannot count on a crash arriving soon, so a second, local door is needed.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
1. **Use the system's frameworks where they exist, guarded.** MetricKit's `MetricManager` and its diagnostic reports, and the `StateReporting` framework, are used behind `#if canImport(...)` and `#available(macOS 27, iOS 27, macCatalyst 27, *)`; the deployment target stays where the app's rule sets it, and nothing happens before those releases. Building them needs the SDK that has them. *Basis: documented (Apple, MetricKit and StateReporting, 2026); the deployment guard is design.*
|
||||||
|
2. **MetricKit is delivered on a device and when the system chooses.** Subscribe at launch, read the crash diagnostics of an earlier run, and treat the first delivery as late. On the Simulator, and in the test of one Mac in this repository's history, nothing arrived within three minutes. *Basis: observed (no delivery within three minutes on a Mac) and documented (delivery is the system's); a claim that MetricKit "reports crashes promptly" is **not established**.*
|
||||||
|
3. **A second door on macOS: the system's crash reports for the app.** On a Mac, the crash reports in `~/Library/Logs/DiagnosticReports` (`.ips`) for the app are read at launch, within a bounded age (three days here), the newest alone prompting. *Basis: observed.*
|
||||||
|
4. **One crash seen through both doors is one crash.** Merge by process id within a window of minutes (five here). The merge is tested, including that a report older than the age bound is not read. *Basis: design; a regression that dropped the age check was caught by this test.*
|
||||||
|
5. **A kept crash is data for the next report, never for the log's fields.** The crash is stored beside the log (the newest twenty). The log gets `crash.diagnosed` with the signal, the reason and the version, and never the call stack. The system's report, which holds stacks and paths that can name the person's account, goes only into a report the person chooses to send, and a crash a report has taken is not put in the next. *Basis: design.*
|
||||||
|
6. **The prompt is the person's.** At launch, in a Debug or Beta build, a recent crash opens the report flow with the note "The app crashed recently." for the person to add to or leave. It never sends. *Basis: design.*
|
||||||
|
7. **The app reports its state.** One reporter per app (`AppStateReporter`) tells the system a label and facts of the app: which screen, which example, which target, whether a report is open. Never a fact of what the person works on. Where a state is a tab or a mode, the change is reported at the one place that changes it. *Basis: documented (StateReporting reports app state to the system); design for the facts allowed.*
|
||||||
|
8. **The domain is told to MetricKit, and the states come back.** The domain of the reporter is given when the crash watch starts, so the system's report names the states active just before the crash; the kept crash copies them to the log as `states`. *Basis: documented that diagnostic reports carry environment states; the end-to-end return on a real crash of an app built with the new SDK is **not established** here until witnessed.*
|
||||||
|
9. **Witness it on a real crash.** The doors are held by tests on fixtures, and by one real crash of a built app: crash it, relaunch it, and require `crash.diagnosed` in the log and the crash in the next report. *Basis: design.*
|
||||||
|
|
||||||
|
## How to test it
|
||||||
|
|
||||||
|
- **Fixtures for the `.ips` reader.** A report of the app, a report of another app, one older than the bound, one cut short, and one in the future; each is read or refused as the rule says.
|
||||||
|
- **The merge.** The same crash through both doors gives one record; two crashes of two processes give two.
|
||||||
|
- **The store.** The newest twenty are kept; a crash taken by a report is marked and not offered again.
|
||||||
|
- **The log never holds the stack.** Feed a crash with a stack and require no field of `crash.diagnosed` to contain a frame.
|
||||||
|
- **A real crash.** Crash a built Debug app, relaunch, and read the log and the report.
|
||||||
|
|
||||||
|
## Acceptance check
|
||||||
|
|
||||||
|
Run from the repository root; `SOURCES` is the directory of package sources. Each command must print nothing unless it says it must print a line:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 1. the new frameworks are only imported behind canImport
|
||||||
|
grep -rnE '^[[:space:]]*import (MetricKit|StateReporting)' "$SOURCES" --include='*.swift' | while IFS=: read -r f n _; do sed -n "$((n-1))p" "$f" | grep -q 'canImport' || echo "$f:$n"; done
|
||||||
|
|
||||||
|
# 2. every use is behind an availability check
|
||||||
|
grep -rnE 'MetricManager|StateReporter' "$SOURCES" --include='*.swift' | grep -v -E 'available|canImport|///|//'
|
||||||
|
|
||||||
|
# 3. the crash record has no field for a call stack in the log (must print nothing)
|
||||||
|
grep -rnE 'callStack|stackTrace|frames' "$SOURCES"/*Logging* "$SOURCES"/BugReporting --include='*.swift' | grep -v -E 'Tests/'
|
||||||
|
|
||||||
|
# 4. each app starts the crash watch (each app must print a line)
|
||||||
|
grep -rn 'watchForCrashes' "$SOURCES" --include='*.swift'
|
||||||
|
```
|
||||||
|
|
||||||
|
Command 4 is read against the list of apps; a missing app is a named row, as in `app-logging.md` rule 12.
|
||||||
|
|
||||||
|
Cross-references: `app-logging.md` (the log and the report these join), `cross-platform.md` (the availability and `canImport` patterns), `testing.md`, `core/proof-discipline.md` (rules 2 and 8 are stated at the strength they were shown), `core/testing-discipline.md`.
|
||||||
16
third_party/rules-swift/cross-platform.md
vendored
16
third_party/rules-swift/cross-platform.md
vendored
@ -216,13 +216,19 @@ public struct TileLogger {
|
|||||||
|
|
||||||
Using `canImport(os)` rather than `os(macOS) || os(iOS) || os(visionOS) || os(tvOS) || os(watchOS)` is the gold-standard pattern. Future Apple platforms inherit `os` automatically; explicit platform lists rot.
|
Using `canImport(os)` rather than `os(macOS) || os(iOS) || os(visionOS) || os(tvOS) || os(watchOS)` is the gold-standard pattern. Future Apple platforms inherit `os` automatically; explicit platform lists rot.
|
||||||
|
|
||||||
### Pattern 6: SwiftUI app target per platform, shared feature package
|
### Pattern 6: SwiftUI app target per platform, shared feature target
|
||||||
|
|
||||||
Separate Xcode projects for iOS and macOS apps, same SwiftUI feature package providing the root view. Keep the platform-specific app shells thin; put the shared UI in one feature package.
|
Separate Xcode projects for iOS and macOS apps, with the same SwiftUI feature
|
||||||
|
target providing the root view. Keep the platform-specific app shells thin; put
|
||||||
|
the shared UI in one feature target.
|
||||||
|
|
||||||
### Pattern 7: UIKit (iOS) and AppKit (macOS) app shells, shared feature package
|
### Pattern 7: UIKit (iOS) and AppKit (macOS) app shells, shared feature target
|
||||||
|
|
||||||
When the app target must be UIKit (iOS scene delegate) or AppKit (NSWindowController), with the feature package providing platform-specific factory functions (`createMainWindow(for:)` / `createWindowController()`). Cross-platform abstractions for the shared types (`PlatformView`, `PlatformViewController`) live in a native-UI package.
|
When the app target must be UIKit (iOS scene delegate) or AppKit
|
||||||
|
(NSWindowController), the feature target provides platform-specific factory
|
||||||
|
functions (`createMainWindow(for:)` / `createWindowController()`). Cross-platform
|
||||||
|
abstractions for shared types (`PlatformView`, `PlatformViewController`) live in a
|
||||||
|
native-UI target.
|
||||||
|
|
||||||
### Pattern 13: Forward-compat shim for unreleased Apple APIs
|
### Pattern 13: Forward-compat shim for unreleased Apple APIs
|
||||||
|
|
||||||
@ -511,7 +517,7 @@ The `#else` arm is reserved for platforms you genuinely do not run on; every pla
|
|||||||
|
|
||||||
## Cross-references
|
## Cross-references
|
||||||
|
|
||||||
- `docs/rules/linux-server.md` is the server-specific operational layer (AsyncHTTPClient, swift-log, graceful shutdown, Docker). Load both together for a server repo.
|
- `linux-server.md` is the server-specific operational layer (AsyncHTTPClient, swift-log, graceful shutdown, Docker). Load both together for a server repo.
|
||||||
- `concurrency.md` is platform-agnostic; Sendable + async/await behave identically on Apple and Linux.
|
- `concurrency.md` is platform-agnostic; Sendable + async/await behave identically on Apple and Linux.
|
||||||
|
|
||||||
## Authoritative references
|
## Authoritative references
|
||||||
|
|||||||
47
third_party/rules-swift/dependency-injection.md
vendored
47
third_party/rules-swift/dependency-injection.md
vendored
@ -40,11 +40,13 @@ public struct TileRenderer {
|
|||||||
- Code that crosses a module boundary depends on a **named protocol**, not on a concrete type imported from another module.
|
- Code that crosses a module boundary depends on a **named protocol**, not on a concrete type imported from another module.
|
||||||
- The composition root (the executable target / app entry point) is the only place that holds concretes from multiple modules and wires them together.
|
- The composition root (the executable target / app entry point) is the only place that holds concretes from multiple modules and wires them together.
|
||||||
|
|
||||||
If and when Tiledown grows into multiple SPM packages:
|
In Tiledown's multi-target package:
|
||||||
|
|
||||||
- A feature/producer package may **not** `import` another feature/producer package.
|
- A feature/producer target may **not** `import` another feature/producer target.
|
||||||
- Cross-package seams are **named protocols**, either declared in a small foundation-only `*Models` package or inlined per package.
|
- Cross-target seams are **named protocols** declared in the foundation-only
|
||||||
- The composition root remains the only place that imports concretes from several packages.
|
`SharedProtocols` target.
|
||||||
|
- The composition root remains the only place that imports concretes from several
|
||||||
|
producer targets.
|
||||||
|
|
||||||
## 4. No closure typealiases at cross-module seams.
|
## 4. No closure typealiases at cross-module seams.
|
||||||
|
|
||||||
@ -67,13 +69,14 @@ Reasons: a named protocol is discoverable (you can find all conformers), it forc
|
|||||||
- This does **not** ban closures as method parameters or property values. `onProgress: (@Sendable (Progress) -> Void)?` is fine.
|
- This does **not** ban closures as method parameters or property values. `onProgress: (@Sendable (Progress) -> Void)?` is fine.
|
||||||
- It **does** ban closure-typed *named* cross-module contracts.
|
- It **does** ban closure-typed *named* cross-module contracts.
|
||||||
|
|
||||||
## 5. Every module lifts out cleanly.
|
## 5. Every target lifts out cleanly.
|
||||||
|
|
||||||
If and when Tiledown is split into multiple packages, each package should lift out of the repo without modification:
|
Each target should lift out of the repo into a minimal package without source
|
||||||
|
modification:
|
||||||
|
|
||||||
- If a package needs a dependency to function, that dependency is **named in its public API**.
|
- If a target needs a dependency to function, that dependency is **named in its public API**.
|
||||||
- Hidden dependencies (ambient singletons, reach-into-shared-state, undeclared transitive imports) are violations even if they currently build.
|
- Hidden dependencies (ambient singletons, reach-into-shared-state, undeclared transitive imports) are violations even if they currently build.
|
||||||
- A mechanical lift-out check copies the package plus its declared transitive dependencies to a temp directory, generates a minimal `Package.swift`, and runs `swift build`. Green means it lifts out.
|
- A mechanical lift-out check copies the target plus its declared transitive dependencies to a temp directory, generates a minimal `Package.swift`, and runs `swift build`. Green means it lifts out.
|
||||||
|
|
||||||
## 6. Composition root: binary name stays clean.
|
## 6. Composition root: binary name stays clean.
|
||||||
|
|
||||||
@ -97,15 +100,18 @@ The `*Impl` namespace says "this file is where we wire concretes." Composition r
|
|||||||
|
|
||||||
If Tiledown grows into multiple packages, keep a checked-in `docs/package-import-contract.md`: one row per target listing target name, allowed imports, and current state. Anything outside the allowed column is a **violation** and must be fixed in the same change. Back it with a CI script that greps `^import` per target and fails on disallowed imports.
|
If Tiledown grows into multiple packages, keep a checked-in `docs/package-import-contract.md`: one row per target listing target name, allowed imports, and current state. Anything outside the allowed column is a **violation** and must be fixed in the same change. Back it with a CI script that greps `^import` per target and fails on disallowed imports.
|
||||||
|
|
||||||
## 8. Foundation-only is the end state for producer packages.
|
## 8. Foundation-tier imports are the end state for producer targets.
|
||||||
|
|
||||||
The end goal for a multi-package split: producer packages import **only external primitives** (Foundation, system frameworks, vetted third-party SSG dependencies). Zero internal package imports. Each producer declares its own protocols inline. The composition root grows to bridge between per-package protocols.
|
Producer targets import only external primitives (Foundation, system frameworks,
|
||||||
|
vetted third-party dependencies) plus foundation-only protocol/value seam targets.
|
||||||
|
They never import another producer's concrete target. A shared named contract stays
|
||||||
|
in its seam target so producers and live writers conform to one interface; the
|
||||||
|
composition root supplies the concretes.
|
||||||
|
|
||||||
Until then, a producer package may import:
|
A producer target may import:
|
||||||
|
|
||||||
- Foundation primitives and system frameworks
|
- Foundation primitives and system frameworks
|
||||||
- Its own `*Models` companion and other foundation-only `*Models` protocol-seam targets
|
- The foundation-only `SharedProtocols` target
|
||||||
- A shared value-type layer, if one exists
|
|
||||||
|
|
||||||
Sequence the work: settle the singleton and static-state removal first, then pursue foundation-only producers as a separate effort. Do not fold the two together.
|
Sequence the work: settle the singleton and static-state removal first, then pursue foundation-only producers as a separate effort. Do not fold the two together.
|
||||||
|
|
||||||
@ -141,4 +147,17 @@ Before adding `import X` to a producer module:
|
|||||||
|
|
||||||
1. Is `X` external (Foundation, system framework, a vetted third-party dependency)? Allowed.
|
1. Is `X` external (Foundation, system framework, a vetted third-party dependency)? Allowed.
|
||||||
2. Is the target an executable target / composition root? Allowed.
|
2. Is the target an executable target / composition root? Allowed.
|
||||||
3. Otherwise: **STOP.** Surface the situation. The target state is foundation-only.
|
3. Is `X` a foundation-only protocol/value seam target allowed by the checked-in
|
||||||
|
import contract? Allowed.
|
||||||
|
4. Otherwise: **STOP.** Surface the situation. Producer-to-producer concrete
|
||||||
|
imports are forbidden.
|
||||||
|
|
||||||
|
## Acceptance check
|
||||||
|
|
||||||
|
For every changed producer target: grep finds no reachable `static let shared`, no
|
||||||
|
service-locator accessor, and no closure typealias used as a named cross-target
|
||||||
|
contract; every external collaborator appears in an initializer; every internal
|
||||||
|
import is allowed by the checked-in package import contract; and the target's
|
||||||
|
physical lift-out check builds independently. Run the full build and test suite and
|
||||||
|
cite their counts. A green monorepo build without the import audit and lift-out
|
||||||
|
proof is not sufficient.
|
||||||
|
|||||||
19
third_party/rules-swift/documentation.md
vendored
19
third_party/rules-swift/documentation.md
vendored
@ -2,11 +2,19 @@
|
|||||||
|
|
||||||
How a TileKit-style library organizes its documentation: a single DocC catalog that is the source of truth and moves with the code in every PR.
|
How a TileKit-style library organizes its documentation: a single DocC catalog that is the source of truth and moves with the code in every PR.
|
||||||
|
|
||||||
Every project's documentation lives in a **DocC catalog** inside the repo as its own SPM target. There is no freeform `Docs/` folder. The catalog is the single source of truth. Every PR that touches code must update the matching catalog article in the same PR.
|
A **qualifying package's** documentation lives in a **DocC catalog** inside the repo as its own SPM target (see Rule 0 for what qualifies). There is no freeform `Docs/` folder. The catalog is the single source of truth. Every PR that touches code must update the matching catalog article in the same PR.
|
||||||
|
|
||||||
## Core rules
|
## Core rules
|
||||||
|
|
||||||
### Rule 1: Documentation lives in a DocC catalog
|
### Rule 0: Where DocC applies (scope)
|
||||||
|
|
||||||
|
DocC is deliberately NOT universal. Applying it everywhere wastes build time and hosting on surfaces no one reads.
|
||||||
|
|
||||||
|
- **Packages only, never apps.** A DocC catalog belongs to a reusable SPM package. App targets (the shipped product shells) do NOT get a `{Product}Documentation` target or a docs-publishing workflow; their behavior is documented by their code, tests, and the packages they compose.
|
||||||
|
- **Not every package, only public OR heavily-used ones.** A package qualifies for DocC when it is (a) **public** (published / consumable outside this workspace), or (b) **heavily used** internally (a foundation package many other packages depend on). A private, single-consumer, or experimental package does NOT get a DocC catalog; inline `///` comments and its tests are its documentation.
|
||||||
|
- If a package does not qualify, Rules 1-14 below simply do not apply to it. Skip the DocC target entirely rather than shipping an empty or unread catalog.
|
||||||
|
|
||||||
|
### Rule 1: Documentation lives in a DocC catalog (for qualifying packages)
|
||||||
|
|
||||||
Organize repo-level documentation as a Swift-DocC catalog in a dedicated SPM target.
|
Organize repo-level documentation as a Swift-DocC catalog in a dedicated SPM target.
|
||||||
|
|
||||||
@ -239,10 +247,13 @@ Rule 8 covers catalog articles; this rule covers the `///` doc comments attached
|
|||||||
|
|
||||||
**Weighting (how much doc is enough):**
|
**Weighting (how much doc is enough):**
|
||||||
- Types & entry points: one-paragraph abstract + non-obvious-behaviour notes. Always.
|
- Types & entry points: one-paragraph abstract + non-obvious-behaviour notes. Always.
|
||||||
- Widely-referenced methods (3 or more caller files outside the declaring package): one-line summary + `- Parameter` / `- Returns` / `- Throws` where non-trivial.
|
- Widely-referenced methods (3 or more caller files outside the declaring target): one-line summary + `- Parameter` / `- Returns` / `- Throws` where non-trivial.
|
||||||
- Leaf / single-caller methods: one-line summary only.
|
- Leaf / single-caller methods: one-line summary only.
|
||||||
|
|
||||||
A weighted audit (undocumented public decls times cross-package usage) should be run before every major release to catch drift. Items referenced in many files but lacking docs are the highest-value gaps; items referenced nowhere outside their file are near-zero priority.
|
A weighted audit (undocumented public declarations times cross-target usage)
|
||||||
|
should run before every major release to catch drift. Items referenced in many
|
||||||
|
files but lacking docs are the highest-value gaps; items referenced nowhere
|
||||||
|
outside their file are near-zero priority.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
4
third_party/rules-swift/exp/README.md
vendored
4
third_party/rules-swift/exp/README.md
vendored
@ -18,12 +18,12 @@ These two files are loaded in every Swift session. Together they cover the rule
|
|||||||
| File | When to load |
|
| File | When to load |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `implementation-patterns.md` | Adding a foundation/feature/middleware/API/component/font package; building an aggregator |
|
| `implementation-patterns.md` | Adding a foundation/feature/middleware/API/component/font package; building an aggregator |
|
||||||
| `dependency-management.md` | Adding a cross-package dependency; platform-conditional code (`#if os()`); circular-dep concerns |
|
| `dependency-management.md` | Adding a cross-target or external-package dependency; platform-conditional code (`#if os()`); circular-dep concerns |
|
||||||
| `package-swift.md` | Editing `Package.swift` (`deps`, `allProducts`, `targets` arrays) |
|
| `package-swift.md` | Editing `Package.swift` (`deps`, `allProducts`, `targets` arrays) |
|
||||||
| `common-mistakes.md` | Code review or self-review of `Package.swift` changes |
|
| `common-mistakes.md` | Code review or self-review of `Package.swift` changes |
|
||||||
| `app-target.md` | Adding or modifying an app target (SwiftUI, UIKit, AppKit entry points) |
|
| `app-target.md` | Adding or modifying an app target (SwiftUI, UIKit, AppKit entry points) |
|
||||||
| `testing.md` | Wiring or restructuring a package's test target (architecture angle; full testing rules in top-level `testing.md`) |
|
| `testing.md` | Wiring or restructuring a package's test target (architecture angle; full testing rules in top-level `testing.md`) |
|
||||||
| `migration.md` | Extracting code into a new package or splitting a large one |
|
| `migration.md` | Extracting code into a new target or splitting a large one |
|
||||||
| `build-performance.md` | Rationale; read once for context, not normally needed during work |
|
| `build-performance.md` | Rationale; read once for context, not normally needed during work |
|
||||||
| `verification.md` | Final checklist before considering an ExtremePackaging change done |
|
| `verification.md` | Final checklist before considering an ExtremePackaging change done |
|
||||||
|
|
||||||
|
|||||||
51
third_party/rules-swift/exp/app-target.md
vendored
51
third_party/rules-swift/exp/app-target.md
vendored
@ -8,20 +8,20 @@ Three patterns for the app entry point: SwiftUI (minimal App shell), UIKit (Scen
|
|||||||
|
|
||||||
### Pattern 1: SwiftUI Apps (Minimal App Shell)
|
### Pattern 1: SwiftUI Apps (Minimal App Shell)
|
||||||
|
|
||||||
SwiftUI app targets MUST be minimal shells that import a feature package and display a view.
|
SwiftUI app targets MUST be minimal shells that import a feature target's module and display a view.
|
||||||
|
|
||||||
#### iOS App Target (iosApp or iOSApp)
|
#### iOS App Target (iosApp or iOSApp)
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
// Apps/iosApp/iosApp/iosAppApp.swift
|
// Apps/iosApp/iosApp/iosAppApp.swift
|
||||||
import AppFeature // ← Import the feature package
|
import AppFeature // ← Import the feature target's module
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct iosAppApp: App {
|
struct iosAppApp: App {
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
AppView() // ← Use the view from the feature package
|
AppView() // ← Use the view from the feature target
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -31,7 +31,7 @@ struct iosAppApp: App {
|
|||||||
|
|
||||||
```swift
|
```swift
|
||||||
// Apps/macApp/macApp/macAppApp.swift
|
// Apps/macApp/macApp/macAppApp.swift
|
||||||
import AppFeature // ← Same feature package as iOS
|
import AppFeature // ← Same feature target as iOS
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
@main
|
@main
|
||||||
@ -59,9 +59,9 @@ Apps/
|
|||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
1. App targets contain only the `@main` struct, `import AppFeature`, and `AppView()`.
|
1. App targets contain only the `@main` struct, `import AppFeature`, and `AppView()`.
|
||||||
2. All UI logic lives in the feature package (e.g. `AppFeature`).
|
2. All UI logic lives in the feature target (e.g. `AppFeature`).
|
||||||
3. Both iOS and macOS targets use the same feature package.
|
3. Both iOS and macOS app targets use the same feature target.
|
||||||
4. The feature package exports `AppView` (a SwiftUI `View`).
|
4. The feature target exports `AppView` (a SwiftUI `View`).
|
||||||
5. Font registration (if needed) goes in `init()` before body
|
5. Font registration (if needed) goes in `init()` before body
|
||||||
|
|
||||||
#### Font Registration in SwiftUI App
|
#### Font Registration in SwiftUI App
|
||||||
@ -69,7 +69,7 @@ Rules:
|
|||||||
```swift
|
```swift
|
||||||
// Apps/iosApp/iosApp/iosAppApp.swift
|
// Apps/iosApp/iosApp/iosAppApp.swift
|
||||||
import AppFeature
|
import AppFeature
|
||||||
import AppFont // ← Import font package
|
import AppFont // ← Import font target's module
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
@main
|
@main
|
||||||
@ -90,7 +90,7 @@ struct iosAppApp: App {
|
|||||||
|
|
||||||
### Pattern 2: UIKit Apps (iOS - SceneDelegate Pattern)
|
### Pattern 2: UIKit Apps (iOS - SceneDelegate Pattern)
|
||||||
|
|
||||||
UIKit app targets MUST delegate to a feature package for window creation.
|
UIKit app targets MUST delegate to a feature target for window creation.
|
||||||
|
|
||||||
**Reference layout:** `Apps/App/UIKitApp/`
|
**Reference layout:** `Apps/App/UIKitApp/`
|
||||||
|
|
||||||
@ -126,7 +126,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
|
|
||||||
```swift
|
```swift
|
||||||
// Apps/UIKitApp/SceneDelegate.swift
|
// Apps/UIKitApp/SceneDelegate.swift
|
||||||
import CanvasFeature // ← Import the feature package (NOT "Canvas")
|
import CanvasFeature // ← Import the feature target's module (NOT "Canvas")
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@ -156,15 +156,15 @@ Apps/
|
|||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
1. AppDelegate handles only app lifecycle (minimal code).
|
1. AppDelegate handles only app lifecycle (minimal code).
|
||||||
2. SceneDelegate imports the feature package.
|
2. SceneDelegate imports the feature target's module.
|
||||||
3. Feature package provides `createMainWindow(for:)` factory function.
|
3. The feature target provides `createMainWindow(for:)` factory function.
|
||||||
4. Feature package name ends with `Feature` (e.g. `CanvasFeature`, not `Canvas`).
|
4. The feature target name ends with `Feature` (e.g. `CanvasFeature`, not `Canvas`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Pattern 3: AppKit Apps (macOS - NSApplicationDelegate Pattern)
|
### Pattern 3: AppKit Apps (macOS - NSApplicationDelegate Pattern)
|
||||||
|
|
||||||
AppKit app targets MUST delegate to a feature package, with menu code in a SEPARATE file.
|
AppKit app targets MUST delegate to a feature target, with menu code in a SEPARATE file.
|
||||||
|
|
||||||
**Reference layout:** `Apps/App/AppKitApp/`
|
**Reference layout:** `Apps/App/AppKitApp/`
|
||||||
|
|
||||||
@ -185,7 +185,7 @@ _ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
|
|||||||
|
|
||||||
```swift
|
```swift
|
||||||
// Apps/AppKitApp/AppDelegate.swift
|
// Apps/AppKitApp/AppDelegate.swift
|
||||||
import CanvasFeature // ← Import the feature package (NOT "Canvas")
|
import CanvasFeature // ← Import the feature target's module (NOT "Canvas")
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@ -304,14 +304,14 @@ Rules:
|
|||||||
1. AppKit uses `main.swift` + `AppDelegate.swift` (no `@main` on AppDelegate).
|
1. AppKit uses `main.swift` + `AppDelegate.swift` (no `@main` on AppDelegate).
|
||||||
2. Menu code lives in a separate file (`MainMenu.swift`).
|
2. Menu code lives in a separate file (`MainMenu.swift`).
|
||||||
3. Menu code is an extension of `AppDelegate`.
|
3. Menu code is an extension of `AppDelegate`.
|
||||||
4. Feature package provides `createWindowController()` factory function.
|
4. The feature target provides `createWindowController()` factory function.
|
||||||
5. Feature package name MUST end with `Feature` (e.g., `CanvasFeature`, NOT `Canvas`)
|
5. The feature target name MUST end with `Feature` (e.g., `CanvasFeature`, NOT `Canvas`)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Feature Package Exports
|
### Feature Target Exports
|
||||||
|
|
||||||
The feature package (e.g., `AppFeature`, `CanvasFeature`) MUST export:
|
The feature target (e.g., `AppFeature`, `CanvasFeature`) MUST export:
|
||||||
|
|
||||||
#### For SwiftUI
|
#### For SwiftUI
|
||||||
|
|
||||||
@ -368,13 +368,12 @@ public enum CanvasFeature {
|
|||||||
|
|
||||||
### App Target Checklist
|
### App Target Checklist
|
||||||
|
|
||||||
- [ ] SwiftUI: App file has ONLY `import FeaturePackage` + `FeatureView()`
|
- [ ] SwiftUI: App file has ONLY the feature-module import plus `FeatureView()`
|
||||||
- [ ] SwiftUI: Both iOS and macOS use the SAME feature package
|
- [ ] SwiftUI: Both iOS and macOS use the SAME feature target
|
||||||
- [ ] UIKit: SceneDelegate imports feature package and uses factory
|
- [ ] UIKit: SceneDelegate imports the feature module and uses its factory
|
||||||
- [ ] AppKit: AppDelegate imports feature package and uses factory
|
- [ ] AppKit: AppDelegate imports the feature module and uses its factory
|
||||||
- [ ] AppKit: Menu code is in SEPARATE `MainMenu.swift` file
|
- [ ] AppKit: Menu code is in SEPARATE `MainMenu.swift` file
|
||||||
- [ ] AppKit: `main.swift` exists with `NSApplicationMain`
|
- [ ] AppKit: `main.swift` exists with `NSApplicationMain`
|
||||||
- [ ] Feature package name ends with `Feature` (NOT raw name like `Canvas`)
|
- [ ] Feature target name ends with `Feature` (NOT raw name like `Canvas`)
|
||||||
- [ ] Feature package exports appropriate factory functions
|
- [ ] Feature target exports appropriate factory functions
|
||||||
- [ ] Font registration (if needed) happens in app init BEFORE UI
|
- [ ] Font registration (if needed) happens in app init BEFORE UI
|
||||||
|
|
||||||
|
|||||||
19
third_party/rules-swift/exp/build-performance.md
vendored
19
third_party/rules-swift/exp/build-performance.md
vendored
@ -24,12 +24,12 @@ SPM builds in parallel:
|
|||||||
[SharedModels, AppFont] → [ApiClient, AppTheme, SharedViews] → [AuthFeature, AppFeature] → [iosApp]
|
[SharedModels, AppFont] → [ApiClient, AppTheme, SharedViews] → [AuthFeature, AppFeature] → [iosApp]
|
||||||
```
|
```
|
||||||
|
|
||||||
SPM automatically parallelizes independent package builds.
|
SPM automatically parallelizes independent target builds.
|
||||||
|
|
||||||
### CI/CD Optimization
|
### CI/CD Optimization
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# A CI runner can cache per-package
|
# A CI runner can cache per layer or product
|
||||||
stages:
|
stages:
|
||||||
- build-foundation
|
- build-foundation
|
||||||
- build-infrastructure
|
- build-infrastructure
|
||||||
@ -46,24 +46,23 @@ build-foundation:
|
|||||||
## Acceptance check
|
## Acceptance check
|
||||||
|
|
||||||
This is a rationale doc, not a binding rule, so it ships an observable checklist a reviewer applies to
|
This is a rationale doc, not a binding rule, so it ships an observable checklist a reviewer applies to
|
||||||
any change that touches the package graph, to confirm the build-time benefits above were preserved.
|
any change that touches the target graph, to confirm the build-time benefits above were preserved.
|
||||||
Each item is phrased so two reviewers agree:
|
Each item is phrased so two reviewers agree:
|
||||||
|
|
||||||
- **Granularity preserved.** The change does not merge two single-responsibility packages into one, so
|
- **Granularity preserved.** The change does not merge two single-responsibility targets into one, so
|
||||||
a touch to one concern still rebuilds only that package and its dependents, not a wider blast radius.
|
a touch to one concern still rebuilds only that target and its dependents, not a wider blast radius.
|
||||||
FAIL if files from two distinct responsibilities are folded into a single target.
|
FAIL if files from two distinct responsibilities are folded into a single target.
|
||||||
- **No new wide-fan-in edge.** The change adds no dependency that makes a low-level, frequently-edited
|
- **No new wide-fan-in edge.** The change adds no dependency that makes a low-level, frequently-edited
|
||||||
package gain many new dependents (which would force a large rebuild on every edit to it). FAIL if a
|
target gain many new dependents (which would force a large rebuild on every edit to it). FAIL if a
|
||||||
foundation package gains a new upward dependency, or a widely-depended-on package starts depending on
|
foundation target gains a new upward dependency, or a widely-depended-on target starts depending on
|
||||||
something volatile.
|
something volatile.
|
||||||
- **Parallelism intact.** Independent packages stay independent: the change introduces no edge that
|
- **Parallelism intact.** Independent targets stay independent: the change introduces no edge that
|
||||||
serialises two previously-parallel build branches without a real reason. PASS when the dependency
|
serialises two previously-parallel build branches without a real reason. PASS when the dependency
|
||||||
graph remains a DAG whose independent branches SPM can still build concurrently.
|
graph remains a DAG whose independent branches SPM can still build concurrently.
|
||||||
- **Per-package CI caching still valid.** The change keeps the foundation, infrastructure, features,
|
- **Layer/product CI caching still valid.** The change keeps the foundation, infrastructure, features,
|
||||||
apps layering, so a CI runner can still cache and skip a layer that did not change. FAIL if a
|
apps layering, so a CI runner can still cache and skip a layer that did not change. FAIL if a
|
||||||
back-edge (a lower layer depending on a higher one) defeats per-layer caching.
|
back-edge (a lower layer depending on a higher one) defeats per-layer caching.
|
||||||
|
|
||||||
A change that collapses granularity or adds a back-edge fails this checklist even if the build still
|
A change that collapses granularity or adds a back-edge fails this checklist even if the build still
|
||||||
succeeds, because it forfeits the incremental, parallel, and cache-friendly build this document exists
|
succeeds, because it forfeits the incremental, parallel, and cache-friendly build this document exists
|
||||||
to protect.
|
to protect.
|
||||||
|
|
||||||
|
|||||||
50
third_party/rules-swift/exp/common-mistakes.md
vendored
50
third_party/rules-swift/exp/common-mistakes.md
vendored
@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
Anti-patterns paired with the canonical fix. Mini-DOs are inline; full canonical forms live in `package-swift.md` (closure-with-locals), `testing.md` (test-target placement), and `implementation-patterns.md` (Pattern 6 for fonts).
|
Anti-patterns paired with the canonical fix. Mini-DOs are inline; full canonical forms live in `package-swift.md` (closure-with-locals), `testing.md` (test-target placement), and `implementation-patterns.md` (Pattern 6 for fonts).
|
||||||
|
|
||||||
### ❌ DON'T: Create God Packages
|
### ❌ DON'T: Create God Targets
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
// WRONG: "Shared" package with everything
|
// WRONG: "Shared" target with everything
|
||||||
Packages/Sources/Shared/
|
Packages/Sources/Shared/
|
||||||
├── Models/
|
├── Models/
|
||||||
├── Views/
|
├── Views/
|
||||||
@ -14,7 +14,7 @@ Packages/Sources/Shared/
|
|||||||
└── Utilities/
|
└── Utilities/
|
||||||
```
|
```
|
||||||
|
|
||||||
✅ **DO:** Split into focused packages
|
✅ **DO:** Split into focused targets
|
||||||
```
|
```
|
||||||
Packages/Sources/
|
Packages/Sources/
|
||||||
├── SharedModels/
|
├── SharedModels/
|
||||||
@ -96,7 +96,11 @@ Full forms (with platform splits, helper extensions, every dep / product / targe
|
|||||||
|
|
||||||
### ❌ DON'T: Skip Test Targets
|
### ❌ DON'T: Skip Test Targets
|
||||||
|
|
||||||
Every package MUST ship with a matching `Target.testTarget(name: "<name>Tests", dependencies: ["<name>"])`, grouped as a pair with the source target in a named sub-array. See `testing.md > TEST TARGET PLACEMENT` for the canonical form, folder layout, and the rule that mocks live in the test target rather than the source package.
|
Every source target MUST ship with a matching `Target.testTarget(name:
|
||||||
|
"<name>Tests", dependencies: ["<name>"])`, grouped as a pair with the source
|
||||||
|
target in a named sub-array. See `testing.md > TEST TARGET PLACEMENT` for the
|
||||||
|
canonical form, folder layout, and the rule that mocks live in the test target
|
||||||
|
rather than the source target.
|
||||||
|
|
||||||
### ❌ DON'T: Transitive Dependencies
|
### ❌ DON'T: Transitive Dependencies
|
||||||
|
|
||||||
@ -133,18 +137,18 @@ let authFeatureTarget = Target.target(
|
|||||||
name: "AuthFeature",
|
name: "AuthFeature",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"SharedModels",
|
"SharedModels",
|
||||||
"AppCoordination", // ✅ Protocol package
|
"AppCoordination", // ✅ Protocol target
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
### ❌ DON'T: Use Info.plist for Font Registration in Packages
|
### ❌ DON'T: Use Info.plist for Font Registration in SwiftPM Targets
|
||||||
|
|
||||||
Three anti-patterns, all covered by `implementation-patterns.md > Pattern 6 (Font/Resource Package)`:
|
Three anti-patterns, all covered by `implementation-patterns.md > Pattern 6 (Font/Resource Target)`:
|
||||||
|
|
||||||
- `UIAppFonts` / `ATSApplicationFontsPath` in Info.plist: doesn't work in SPM packages
|
- `UIAppFonts` / `ATSApplicationFontsPath` in Info.plist: doesn't work for SwiftPM target resources
|
||||||
- `.copy("Fonts")` in `Package.swift` resources: breaks `Bundle.module` lookup
|
- `.copy("Fonts")` in `Package.swift` resources: breaks `Bundle.module` lookup
|
||||||
- `Bundle.main.urls(...)` inside the package: wrong bundle for SPM resources
|
- `Bundle.main.urls(...)` inside the target: wrong bundle for SwiftPM resources
|
||||||
|
|
||||||
DO: `.process("Fonts")` in resources, `CTFontManagerRegisterFontsForURL` against `Bundle.module`, called from app init before any UI renders. Full implementation in `implementation-patterns.md > Pattern 6`.
|
DO: `.process("Fonts")` in resources, `CTFontManagerRegisterFontsForURL` against `Bundle.module`, called from app init before any UI renders. Full implementation in `implementation-patterns.md > Pattern 6`.
|
||||||
|
|
||||||
@ -155,12 +159,14 @@ Each anti-pattern above is mechanically checkable; the fixes are conforming when
|
|||||||
- **No inline top-level arrays.** The three top-level manifest arrays use the closure-with-local-variables form, never a bare array literal:
|
- **No inline top-level arrays.** The three top-level manifest arrays use the closure-with-local-variables form, never a bare array literal:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
grep -nE 'let (deps|allProducts|targets)[^=]*=[[:space:]]*\[' Packages/Package.swift Package.swift
|
MANIFEST=Package.swift
|
||||||
|
[ -f "$MANIFEST" ] || MANIFEST=Packages/Package.swift
|
||||||
|
grep -nE 'let (deps|allProducts|targets)[^=]*=[[:space:]]*\[' "$MANIFEST"
|
||||||
```
|
```
|
||||||
|
|
||||||
Output MUST be empty (each should read `let deps: [...] = {`), per the closure-with-locals fix.
|
Output MUST be empty (each should read `let deps: [...] = {`), per the closure-with-locals fix.
|
||||||
|
|
||||||
- **No Info.plist font registration in packages.** Packages register fonts via CoreText, never the plist keys or `.copy("Fonts")`:
|
- **No Info.plist font registration in SwiftPM targets.** Targets register fonts via CoreText, never the plist keys or `.copy("Fonts")`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
grep -rnE 'UIAppFonts|ATSApplicationFontsPath|\.copy\("Fonts"\)' Packages/
|
grep -rnE 'UIAppFonts|ATSApplicationFontsPath|\.copy\("Fonts"\)' Packages/
|
||||||
@ -171,10 +177,26 @@ grep -rnE 'UIAppFonts|ATSApplicationFontsPath|\.copy\("Fonts"\)' Packages/
|
|||||||
- **Every source target has a matching test target.** For each `Target.target(name: "X")` there is a `Target.testTarget(name: "XTests")`. List the names and confirm no source target lacks its pair:
|
- **Every source target has a matching test target.** For each `Target.target(name: "X")` there is a `Target.testTarget(name: "XTests")`. List the names and confirm no source target lacks its pair:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
grep -oE 'Target\.(target|testTarget)\(name: "[^"]+"' Packages/Package.swift
|
PACKAGE_ROOT=.
|
||||||
|
[ -f Package.swift ] || PACKAGE_ROOT=Packages
|
||||||
|
swift package --package-path "$PACKAGE_ROOT" describe --type json | jq -r '
|
||||||
|
.targets as $targets
|
||||||
|
| [$targets[] | select(.type == "test") | .name] as $tests
|
||||||
|
| $targets[]
|
||||||
|
| select(.type == "library" or .type == "executable")
|
||||||
|
| . as $source
|
||||||
|
| select(($tests | index($source.name + "Tests")) == null)
|
||||||
|
| .name
|
||||||
|
'
|
||||||
```
|
```
|
||||||
|
|
||||||
Every non-test target name MUST have a corresponding `<name>Tests`.
|
Output MUST be empty. Every source target name has a corresponding
|
||||||
|
`<name>Tests` target.
|
||||||
|
|
||||||
- **Observable checklist** for the structural anti-patterns no grep settles, each a clear pass/fail: no "God package" mixing unrelated concerns (Models, Views, Networking, Database under one target); no circular dependency between two packages (the graph is a DAG); no feature target lists another feature target as a dependency (coordination goes through a parent/protocol package); and every cross-package symbol use has an explicit `import` of the package that declares it, never a reliance on a transitive import.
|
`swift package describe` names a library target `library`; the value `regular` belongs to
|
||||||
|
`dump-package` and never occurs in this output, so a filter on it checks no library at all
|
||||||
|
and passes whatever the package holds. To prove the command is not vacuous, count what it
|
||||||
|
selects: the number of `library` and `executable` targets MUST equal the number of source
|
||||||
|
targets the manifest declares.
|
||||||
|
|
||||||
|
- **Observable checklist** for the structural anti-patterns no grep settles, each a clear pass/fail: no "god target" mixing unrelated concerns (Models, Views, Networking, Database under one target); no circular dependency between two targets (the graph is a DAG); no feature target lists another feature target as a dependency (coordination goes through a parent/protocol target); and every cross-target symbol use has an explicit `import` of the module that declares it, never a reliance on a transitive import.
|
||||||
|
|||||||
49
third_party/rules-swift/exp/critical-rules.md
vendored
49
third_party/rules-swift/exp/critical-rules.md
vendored
@ -1,13 +1,17 @@
|
|||||||
# Critical Rules
|
# Critical Rules
|
||||||
|
|
||||||
Always-loaded core. Five rules that gate every Swift package decision under ExtremePackaging.
|
Always-loaded core. Five rules that gate every internal SwiftPM target decision
|
||||||
|
under ExtremePackaging.
|
||||||
|
|
||||||
Follow the "ExtremePackaging" architecture pattern: a monorepo with maximum granular modularization into distinct SPM packages. Each package represents a single cohesive responsibility with explicit dependencies. This enables isolated compilation, parallel builds, clear dependency graphs, and superior testability.
|
Follow the "ExtremePackaging" architecture pattern: one active Swift package with
|
||||||
|
maximum granular modularization into distinct targets. Each target represents a
|
||||||
|
single cohesive responsibility with explicit dependencies. This enables isolated
|
||||||
|
compilation, parallel builds, clear dependency graphs, and superior testability.
|
||||||
|
|
||||||
### Rule 1: Single Responsibility per Package
|
### Rule 1: Single Responsibility per Target
|
||||||
|
|
||||||
Create packages with one clear purpose:
|
Create targets with one clear purpose:
|
||||||
- A single, well-defined responsibility per package
|
- A single, well-defined responsibility per target
|
||||||
- Don't mix concerns (UI + networking, models + API client)
|
- Don't mix concerns (UI + networking, models + API client)
|
||||||
- Name communicates the purpose
|
- Name communicates the purpose
|
||||||
- Independently buildable and testable
|
- Independently buildable and testable
|
||||||
@ -17,15 +21,15 @@ Create packages with one clear purpose:
|
|||||||
Declare dependencies explicitly in `Package.swift`:
|
Declare dependencies explicitly in `Package.swift`:
|
||||||
- List all dependencies in the package manifest
|
- List all dependencies in the package manifest
|
||||||
- No implicit/transitive dependencies
|
- No implicit/transitive dependencies
|
||||||
- Minimise cross-package dependencies
|
- Minimise cross-target dependencies
|
||||||
- Prefer unidirectional dependency flow
|
- Prefer unidirectional dependency flow
|
||||||
|
|
||||||
### Rule 3: Package Granularity
|
### Rule 3: Target Granularity
|
||||||
|
|
||||||
Prefer smaller, focused packages over larger ones:
|
Prefer smaller, focused targets over larger ones:
|
||||||
- Single-file packages are acceptable when the unit is genuinely standalone (typography, color foundation, a single protocol, a single transport).
|
- Single-file targets are acceptable when the unit is genuinely standalone (typography, color foundation, a single protocol, a single transport).
|
||||||
- Separate by role, not by topical bundling: foundation primitives, infrastructure, protocols, middleware, services, per-feature or per-CLI-verb operation packages, front-door binaries.
|
- Separate by role, not by topical bundling: foundation primitives, infrastructure, protocols, middleware, services, per-feature or per-CLI-verb operation targets, front-door binaries.
|
||||||
- Each cohesive responsibility (a feature, a CLI verb, a middleware, a transport, a service) gets its own package.
|
- Each cohesive responsibility (a feature, a CLI verb, a middleware, a transport, a service) gets its own target.
|
||||||
|
|
||||||
### Rule 4: Naming Conventions
|
### Rule 4: Naming Conventions
|
||||||
|
|
||||||
@ -33,15 +37,18 @@ Follow a consistent naming scheme. Patterns that apply across project shapes:
|
|||||||
|
|
||||||
- **Shared foundation:** `Shared*` for cross-target value-types, models, utilities, configuration (e.g. `SharedModels`, `SharedUtils`, `SharedConfiguration`).
|
- **Shared foundation:** `Shared*` for cross-target value-types, models, utilities, configuration (e.g. `SharedModels`, `SharedUtils`, `SharedConfiguration`).
|
||||||
- **Core infrastructure:** `Core*` or descriptive single-purpose names for the foundation domain (protocols, parsers, transports, indexers).
|
- **Core infrastructure:** `Core*` or descriptive single-purpose names for the foundation domain (protocols, parsers, transports, indexers).
|
||||||
- **Per-feature or per-verb operation packages:** one package per user-facing flow (UI app) or one per CLI/MCP verb (CLI/MCP app). Name after the responsibility, not after a role suffix (`Distribution`, `Indexer`, `Ingest` in a CLI; the feature's own name in a UI app).
|
- **Per-feature or per-verb operation targets:** one target per user-facing flow (UI app) or one per CLI/MCP verb (CLI/MCP app). Name after the responsibility, not after a role suffix (`Distribution`, `Indexer`, `Ingest` in a CLI; the feature's own name in a UI app).
|
||||||
- **Service packages:** `*Service` or a `Services` aggregator for cross-layer read/write services consumed by multiple front-doors.
|
- **Service targets:** `*Service` or a `Services` aggregator for cross-layer read/write services consumed by multiple front-doors.
|
||||||
- **Aggregators:** umbrella packages only when the umbrella adds real value (preview hosts, all-features aggregators for app composition); don't add one by default.
|
- **Aggregators:** umbrella targets only when the umbrella adds real value (preview hosts, all-features aggregators for app composition); don't add one by default.
|
||||||
|
|
||||||
Project-specific conventions (UI component packages, API client/server splits, design-system packages, hot-reload infrastructure) live in the project's own `AGENTS.md` or rule files, not in this always-loaded universal rule.
|
Project-specific conventions (UI component packages, API client/server splits, design-system packages, hot-reload infrastructure) live in the project's own `AGENTS.md` or rule files, not in this always-loaded universal rule.
|
||||||
|
|
||||||
### Rule 5: Layer Architecture
|
### Rule 5: Layer Architecture
|
||||||
|
|
||||||
Organize packages into clear architectural layers with unidirectional dependency flow (bottom → top). The number and naming of layers varies per project shape; the constants are:
|
Organize targets into clear architectural layers ordered bottom to top. Imports
|
||||||
|
and manifest dependencies point from higher-level consumers to lower-level
|
||||||
|
providers. The number and naming of layers varies per project shape; the constants
|
||||||
|
are:
|
||||||
|
|
||||||
- **Foundation** (bottom): shared value-types, models, primitive utilities, logging.
|
- **Foundation** (bottom): shared value-types, models, primitive utilities, logging.
|
||||||
- **Infrastructure**: protocols, persistence, networking, file I/O, parsers, transports.
|
- **Infrastructure**: protocols, persistence, networking, file I/O, parsers, transports.
|
||||||
@ -49,11 +56,15 @@ Organize packages into clear architectural layers with unidirectional dependency
|
|||||||
- **Presentation** (UI projects only): component packages, design-system packages, screens.
|
- **Presentation** (UI projects only): component packages, design-system packages, screens.
|
||||||
- **Front-door** (top): binary targets, apps, CLIs, MCP servers, preview hosts, test harnesses.
|
- **Front-door** (top): binary targets, apps, CLIs, MCP servers, preview hosts, test harnesses.
|
||||||
|
|
||||||
Every dependency edge points upward. No back-edges. For the active project's actual layer instantiation, read the active manifest: root `Package.swift` in a package repo, `Packages/Package.swift` in a project monorepo.
|
A lower layer never imports or declares a dependency on a higher layer. Build order
|
||||||
|
and dependent propagation travel upward, but the declared `depends on` relation
|
||||||
|
points downward. For the active project's actual layer instantiation, read the
|
||||||
|
active manifest: root `Package.swift` in a package repo,
|
||||||
|
`Packages/Package.swift` in a project monorepo.
|
||||||
|
|
||||||
## Acceptance check
|
## Acceptance check
|
||||||
|
|
||||||
The five rules are conforming when each is shown to hold for the active package graph:
|
The five rules are conforming when each is shown to hold for the active target graph:
|
||||||
|
|
||||||
- **The graph is acyclic and resolves.** SPM rejects a dependency cycle at resolution, so a clean resolve proves Rule 2's "no circular dependencies" floor:
|
- **The graph is acyclic and resolves.** SPM rejects a dependency cycle at resolution, so a clean resolve proves Rule 2's "no circular dependencies" floor:
|
||||||
|
|
||||||
@ -63,6 +74,6 @@ swift package resolve && swift build
|
|||||||
|
|
||||||
Both MUST exit 0. A cycle makes resolution fail.
|
Both MUST exit 0. A cycle makes resolution fail.
|
||||||
|
|
||||||
- **Dependencies are declared, not transitive.** Spot-check that every package a target's sources `import` appears in that target's `dependencies:` list (Rule 2). No target relies on a symbol reaching it only through another package's import.
|
- **Dependencies are declared, not transitive.** Spot-check that every module a target's sources `import` appears in that target's `dependencies:` list (Rule 2). No target relies on a symbol reaching it only through another target's import.
|
||||||
|
|
||||||
- **Observable checklist** for the rules SPM does not enforce, each a clear pass/fail: every package has one cohesive responsibility, not a grab-bag of unrelated concerns (Rule 1); a package is split rather than grown when a second responsibility appears, and single-file packages are allowed when genuinely standalone (Rule 3); package names follow the scheme (`Shared*` foundation, `Core*` or descriptive infrastructure, per-feature/per-verb operation packages, `*Service` services, umbrella aggregators only when they add value) (Rule 4); and every dependency edge points upward through the foundation, infrastructure, domain, presentation, front-door layering with no back-edge (Rule 5). A back-edge passes `swift build` but fails this checklist.
|
- **Observable checklist** for the rules SPM does not enforce, each a clear pass/fail: every target has one cohesive responsibility, not a grab-bag of unrelated concerns (Rule 1); a target is split rather than grown when a second responsibility appears, and single-file targets are allowed when genuinely standalone (Rule 3); target names follow the scheme (`Shared*` foundation, `Core*` or descriptive infrastructure, per-feature/per-verb operation targets, `*Service` services, umbrella aggregators only when they add value) (Rule 4); and every import or manifest dependency goes from a higher-level consumer to a lower-level provider, never the reverse (Rule 5). A back-edge passes `swift build` but fails this checklist.
|
||||||
|
|||||||
@ -18,7 +18,9 @@ Features (AuthFeature, AppFeature, etc.)
|
|||||||
Apps (iosApp, macApp)
|
Apps (iosApp, macApp)
|
||||||
```
|
```
|
||||||
|
|
||||||
Dependencies only flow downward. NEVER import from a higher layer.
|
Imports and manifest dependencies point downward, from a higher-level consumer to
|
||||||
|
a lower-level provider. NEVER import from a higher layer. Build order and dependent
|
||||||
|
propagation run upward and are not the dependency direction described here.
|
||||||
|
|
||||||
### Circular Dependency Prevention
|
### Circular Dependency Prevention
|
||||||
|
|
||||||
@ -31,7 +33,7 @@ Dependencies only flow downward. NEVER import from a higher layer.
|
|||||||
|
|
||||||
✅ **DO:**
|
✅ **DO:**
|
||||||
```swift
|
```swift
|
||||||
// Extract shared code to new package
|
// Extract shared code to a new target
|
||||||
// Both features depend on: SharedAuthModels
|
// Both features depend on: SharedAuthModels
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -163,7 +165,7 @@ let targets: [Target] = {
|
|||||||
- ✅ UI targets only compile on Apple platforms
|
- ✅ UI targets only compile on Apple platforms
|
||||||
- ✅ Clear separation between backend and frontend code
|
- ✅ Clear separation between backend and frontend code
|
||||||
- ✅ Prevents accidental dependencies on Apple frameworks in server code
|
- ✅ Prevents accidental dependencies on Apple frameworks in server code
|
||||||
- ✅ Faster CI builds (Linux can skip UI packages)
|
- ✅ Faster CI builds (Linux can skip UI targets)
|
||||||
|
|
||||||
**When to Use:**
|
**When to Use:**
|
||||||
- UI components, SwiftUI views → `#if os(iOS) || os(macOS)`
|
- UI components, SwiftUI views → `#if os(iOS) || os(macOS)`
|
||||||
@ -180,7 +182,9 @@ Conforms when:
|
|||||||
- **Manifest platform detection uses `#if os()`, never `#if canImport`.** This is the named SPM landmine: `#if canImport(UIKit)` silently breaks the manifest on Linux CI.
|
- **Manifest platform detection uses `#if os()`, never `#if canImport`.** This is the named SPM landmine: `#if canImport(UIKit)` silently breaks the manifest on Linux CI.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
grep -n '#if canImport' Packages/Package.swift Package.swift
|
MANIFEST=Package.swift
|
||||||
|
[ -f "$MANIFEST" ] || MANIFEST=Packages/Package.swift
|
||||||
|
grep -n '#if canImport' "$MANIFEST"
|
||||||
```
|
```
|
||||||
|
|
||||||
Output MUST be empty; platform splits in the manifest use `#if os(iOS) || os(macOS)`.
|
Output MUST be empty; platform splits in the manifest use `#if os(iOS) || os(macOS)`.
|
||||||
@ -193,5 +197,14 @@ swift package resolve && swift build
|
|||||||
|
|
||||||
Both MUST exit 0.
|
Both MUST exit 0.
|
||||||
|
|
||||||
- **Observable checklist** for the flow rules, each a clear pass/fail: dependencies flow only downward through the foundation, design-system, infrastructure, components, features, apps layering, with no import from a higher layer; when two packages would depend on each other, the shared code is extracted to a new package rather than creating a cycle; Apple-only products and targets sit behind `#if os(iOS) || os(macOS)` while shared models, API contracts, and server code stay unconditional (base); and a platform-specific dependency is attached with `condition: .when(platforms:)` rather than duplicated. A server target that builds on Linux CI without Apple SDKs is the observable signal that the separation holds.
|
- **Observable checklist** for the flow rules, each a clear pass/fail: imports and
|
||||||
|
manifest dependencies go only from apps toward features, components,
|
||||||
|
infrastructure, design system, and foundation, with no lower layer importing a
|
||||||
|
higher one; when two targets would depend on each other, the shared code is
|
||||||
|
extracted to a new lower-level target rather than creating a cycle; Apple-only
|
||||||
|
products and targets sit behind `#if os(iOS) || os(macOS)` while shared models,
|
||||||
|
API contracts, and server code stay unconditional (base); and a
|
||||||
|
platform-specific external package dependency is attached with
|
||||||
|
`condition: .when(platforms:)` rather than duplicated. A server target that
|
||||||
|
builds on Linux CI without Apple SDKs is the observable signal that the
|
||||||
|
separation holds.
|
||||||
|
|||||||
@ -1,18 +1,19 @@
|
|||||||
# Implementation Patterns
|
# Implementation Patterns
|
||||||
|
|
||||||
Six worked patterns for the package layers used in the canonical Swift monorepo. Foundation, Feature, Middleware, API, Component, Font/Resource.
|
Six worked patterns for the target layers used in the canonical Swift monorepo.
|
||||||
|
Foundation, Feature, Middleware, API, Component, Font/Resource.
|
||||||
|
|
||||||
Cross-referenced from elsewhere:
|
Cross-referenced from elsewhere:
|
||||||
- Aggregator pattern: see Pattern 5 (Component Layer Architecture) > AllComponents Package, the canonical example
|
- Aggregator pattern: see Pattern 5 (Component Layer Architecture) > AllComponents target, the canonical example
|
||||||
- Font registration anti-patterns: see Pattern 6 (Font/Resource Package) > Why This Pattern
|
- Font registration anti-patterns: see Pattern 6 (Font/Resource target) > Why This Pattern
|
||||||
|
|
||||||
### Pattern 1: Foundation Package (No Dependencies)
|
### Pattern 1: Foundation Target (No Dependencies)
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
// Packages/Sources/SharedModels/Package.swift (excerpt)
|
// Packages/Package.swift (SharedModels excerpt)
|
||||||
let sharedModelsTarget = Target.target(
|
let sharedModelsTarget = Target.target(
|
||||||
name: "SharedModels",
|
name: "SharedModels",
|
||||||
dependencies: [] // RULE: Foundation packages have ZERO dependencies
|
dependencies: [] // RULE: Foundation targets have ZERO dependencies
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -33,12 +34,12 @@ public struct User: Identifiable, Codable, Sendable {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Foundation packages are pure Swift, no dependencies, highly reusable.
|
Foundation targets are pure Swift, have no dependencies, and are highly reusable.
|
||||||
|
|
||||||
### Pattern 2: Feature Package (Depends on Foundation + Infrastructure)
|
### Pattern 2: Feature Target (Depends on Foundation + Infrastructure)
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
// Packages/Sources/AuthFeature/Package.swift (excerpt)
|
// Packages/Package.swift (AuthFeature excerpt)
|
||||||
let authFeatureTarget = Target.target(
|
let authFeatureTarget = Target.target(
|
||||||
name: "AuthFeature",
|
name: "AuthFeature",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
@ -80,10 +81,10 @@ public struct LoginView: View {
|
|||||||
|
|
||||||
Features depend on foundations and infrastructure, never on other features (except parent/child relationships).
|
Features depend on foundations and infrastructure, never on other features (except parent/child relationships).
|
||||||
|
|
||||||
### Pattern 3: Middleware Package (Single Purpose Infrastructure)
|
### Pattern 3: Middleware Target (Single Purpose Infrastructure)
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
// Packages/Sources/OpenAPICachingMiddleware/Package.swift (excerpt)
|
// Packages/Package.swift (OpenAPICachingMiddleware excerpt)
|
||||||
let apiCachingTarget = Target.target(
|
let apiCachingTarget = Target.target(
|
||||||
name: "OpenAPICachingMiddleware",
|
name: "OpenAPICachingMiddleware",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
@ -92,12 +93,12 @@ let apiCachingTarget = Target.target(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
Middleware packages are highly focused, single-purpose, minimal dependencies.
|
Middleware targets are highly focused, single-purpose, and have minimal dependencies.
|
||||||
|
|
||||||
### Pattern 4: API Layer Separation
|
### Pattern 4: API Layer Separation
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
// RULE: Separate packages for each API concern
|
// RULE: Separate targets for each API concern
|
||||||
|
|
||||||
// ApiShared: OpenAPI spec + generated DTOs
|
// ApiShared: OpenAPI spec + generated DTOs
|
||||||
let apiSharedTarget = Target.target(
|
let apiSharedTarget = Target.target(
|
||||||
@ -137,11 +138,11 @@ let apiServerAppTarget = Target.executableTarget(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
API layer has 4 packages: Shared (contract), Client, Server, ServerApp (executable).
|
API layer has four targets: Shared (contract), Client, Server, and ServerApp (executable).
|
||||||
|
|
||||||
### Pattern 5: Component Layer Architecture
|
### Pattern 5: Component Layer Architecture
|
||||||
|
|
||||||
The component layer has a strict 3-package hierarchy. Start with the Components package first.
|
The component layer has a strict three-target hierarchy. Start with the Components target first.
|
||||||
|
|
||||||
#### Component System Architecture
|
#### Component System Architecture
|
||||||
|
|
||||||
@ -153,7 +154,7 @@ SharedComponents (hot reload)
|
|||||||
AppComponents (app-specific production components)
|
AppComponents (app-specific production components)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 1. Components Package (CORE - comes FIRST)
|
#### 1. Components Target (CORE - comes FIRST)
|
||||||
|
|
||||||
**Purpose:** Core component system infrastructure
|
**Purpose:** Core component system infrastructure
|
||||||
|
|
||||||
@ -201,7 +202,7 @@ Packages/Sources/Components/
|
|||||||
└── components.json
|
└── components.json
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 2. SharedComponents Package (Hot Reload Infrastructure)
|
#### 2. SharedComponents Target (Hot Reload Infrastructure)
|
||||||
|
|
||||||
**Purpose:** Hot reload and development-time infrastructure
|
**Purpose:** Hot reload and development-time infrastructure
|
||||||
|
|
||||||
@ -222,7 +223,7 @@ let sharedComponentsTarget = Target.target(
|
|||||||
|
|
||||||
**Purpose:** Enables hot reload of components during development
|
**Purpose:** Enables hot reload of components during development
|
||||||
|
|
||||||
#### 3. AppComponents Package (App-Specific Components)
|
#### 3. AppComponents Target (App-Specific Components)
|
||||||
|
|
||||||
**Purpose:** Production app-specific components
|
**Purpose:** Production app-specific components
|
||||||
|
|
||||||
@ -262,11 +263,16 @@ Packages/Sources/AppComponents/
|
|||||||
└── AppComponentsRegistration.swift
|
└── AppComponentsRegistration.swift
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 4. AllComponents Package (Aggregator - Optional)
|
#### 4. AllComponents Target (Aggregator - Optional)
|
||||||
|
|
||||||
**Purpose:** Umbrella package for convenient imports.
|
**Purpose:** Umbrella target for convenient imports.
|
||||||
|
|
||||||
**This is also the canonical example of the general aggregator pattern.** An aggregator package re-exports its dependencies via `@_exported import`, so a consumer who writes `import YourAggregator` gets everything the aggregator depends on. The same shape works for any umbrella package, not just AllComponents (e.g., a hypothetical `AllFeatures` for a preview app would depend on every `*Feature` package and `@_exported import` each one).
|
**This is also the canonical example of the general aggregator pattern.** An
|
||||||
|
aggregator target re-exports its target dependencies via `@_exported import`, so
|
||||||
|
a consumer who writes `import YourAggregator` gets every module the aggregator
|
||||||
|
depends on. The same shape works for any umbrella target, not just AllComponents
|
||||||
|
(for example, a hypothetical `AllFeatures` preview target would depend on every
|
||||||
|
`*Feature` target and `@_exported import` each one).
|
||||||
|
|
||||||
**Dependencies:**
|
**Dependencies:**
|
||||||
- Components
|
- Components
|
||||||
@ -294,13 +300,13 @@ Use only in preview/demo apps and component libraries, NEVER in production featu
|
|||||||
#### Component Layer Rules
|
#### Component Layer Rules
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
1. Create the Components package first (core infrastructure).
|
1. Create the Components target first (core infrastructure).
|
||||||
2. SharedComponents depends only on hot reload tools (Inject, KZFileWatchers).
|
2. SharedComponents depends only on hot reload tools (Inject, KZFileWatchers).
|
||||||
3. AppComponents depends on Components + AppColors + AppFont
|
3. AppComponents depends on Components + AppColors + AppFont
|
||||||
4. NEVER skip the Components package - it contains the core system
|
4. NEVER skip the Components target - it contains the core system
|
||||||
5. Component configuration goes in `components.json`
|
5. Component configuration goes in `components.json`
|
||||||
6. All components must conform to the protocol defined in Components
|
6. All components must conform to the protocol defined in Components
|
||||||
7. Registration happens via ComponentRegistry from Components package
|
7. Registration happens via ComponentRegistry from the Components target
|
||||||
|
|
||||||
**Order of Creation:**
|
**Order of Creation:**
|
||||||
1. Components (AnyComponent, ComponentRegistry, etc.)
|
1. Components (AnyComponent, ComponentRegistry, etc.)
|
||||||
@ -308,7 +314,7 @@ Rules:
|
|||||||
3. AppComponents (app-specific components)
|
3. AppComponents (app-specific components)
|
||||||
4. AllComponents (aggregator - optional)
|
4. AllComponents (aggregator - optional)
|
||||||
|
|
||||||
### Pattern 6: Font/Resource Package
|
### Pattern 6: Font/Resource Target
|
||||||
|
|
||||||
Register fonts using CoreText, not via Info.plist. Resources use `.process()` in Package.swift.
|
Register fonts using CoreText, not via Info.plist. Resources use `.process()` in Package.swift.
|
||||||
|
|
||||||
@ -342,7 +348,7 @@ import AppKit
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
public enum FontRegistration {
|
public enum FontRegistration {
|
||||||
/// Register custom fonts from the AppFont package
|
/// Register custom fonts from the AppFont target
|
||||||
public static func registerFonts() {
|
public static func registerFonts() {
|
||||||
// Get all resource URLs and filter for .otf files
|
// Get all resource URLs and filter for .otf files
|
||||||
guard let resourceURLs = Bundle.module.urls(forResourcesWithExtension: nil, subdirectory: nil) else {
|
guard let resourceURLs = Bundle.module.urls(forResourcesWithExtension: nil, subdirectory: nil) else {
|
||||||
@ -374,7 +380,7 @@ public enum FontRegistration {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Package Directory Structure
|
#### Target Directory Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
Packages/Sources/AppFont/
|
Packages/Sources/AppFont/
|
||||||
@ -411,7 +417,7 @@ struct iosAppApp: App {
|
|||||||
#### Why This Pattern?
|
#### Why This Pattern?
|
||||||
|
|
||||||
**Benefits:**
|
**Benefits:**
|
||||||
- ✅ Works in SPM packages (Info.plist approach doesn't work in packages)
|
- ✅ Works in SwiftPM targets (the Info.plist approach does not)
|
||||||
- ✅ Explicit font registration with error reporting
|
- ✅ Explicit font registration with error reporting
|
||||||
- ✅ Cross-platform (iOS + macOS) with `#if canImport()` conditionals
|
- ✅ Cross-platform (iOS + macOS) with `#if canImport()` conditionals
|
||||||
- ✅ Uses `Bundle.module` (SPM automatic bundle)
|
- ✅ Uses `Bundle.module` (SPM automatic bundle)
|
||||||
@ -423,22 +429,22 @@ struct iosAppApp: App {
|
|||||||
- `.copy()` → Resources copied verbatim, may not work with `Bundle.module`
|
- `.copy()` → Resources copied verbatim, may not work with `Bundle.module`
|
||||||
|
|
||||||
**Why CoreText not Info.plist:**
|
**Why CoreText not Info.plist:**
|
||||||
- Info.plist font registration only works in app bundles, NOT in SPM packages
|
- Info.plist font registration only works in app bundles, NOT in SwiftPM library targets
|
||||||
- CoreText registration works anywhere (packages, frameworks, apps)
|
- CoreText registration works anywhere (SwiftPM targets, frameworks, apps)
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
1. Use `CTFontManagerRegisterFontsForURL` for font registration.
|
1. Use `CTFontManagerRegisterFontsForURL` for font registration.
|
||||||
2. Use `Bundle.module` in SPM packages, not `Bundle.main`.
|
2. Use `Bundle.module` in SwiftPM targets, not `Bundle.main`.
|
||||||
3. ALWAYS use `.process()` for font resources in Package.swift
|
3. ALWAYS use `.process()` for font resources in Package.swift
|
||||||
4. ALWAYS use `#if canImport(UIKit)` / `#if canImport(AppKit)` for platform imports
|
4. ALWAYS use `#if canImport(UIKit)` / `#if canImport(AppKit)` for platform imports
|
||||||
5. ALWAYS call `FontRegistration.registerFonts()` in app init BEFORE any UI renders
|
5. ALWAYS call `FontRegistration.registerFonts()` in app init BEFORE any UI renders
|
||||||
6. NEVER use Info.plist `UIAppFonts` / `ATSApplicationFontsPath` in packages
|
6. NEVER use Info.plist `UIAppFonts` / `ATSApplicationFontsPath` in SwiftPM targets
|
||||||
|
|
||||||
## Acceptance check
|
## Acceptance check
|
||||||
|
|
||||||
The patterns are conforming when their checkable invariants hold:
|
The patterns are conforming when their checkable invariants hold:
|
||||||
|
|
||||||
- **Foundation and core-infrastructure packages carry zero dependencies.** The `SharedModels` (Pattern 1) and `Components` (Pattern 5) targets declare `dependencies: []`. Confirm by reading those target definitions in `Packages/Package.swift`; a non-empty list is a violation.
|
- **Foundation and core-infrastructure targets carry zero dependencies.** The `SharedModels` (Pattern 1) and `Components` (Pattern 5) targets declare `dependencies: []`. Confirm by reading those target definitions in `Packages/Package.swift`; a non-empty list is a violation.
|
||||||
|
|
||||||
- **Fonts register via CoreText against `Bundle.module`, never Info.plist or `Bundle.main`** (Pattern 6):
|
- **Fonts register via CoreText against `Bundle.module`, never Info.plist or `Bundle.main`** (Pattern 6):
|
||||||
|
|
||||||
@ -455,7 +461,6 @@ grep -rn 'CTFontManagerRegisterFontsForURL' Packages/Sources/AppFont/
|
|||||||
grep -rln '@_exported import' Packages/Sources/
|
grep -rln '@_exported import' Packages/Sources/
|
||||||
```
|
```
|
||||||
|
|
||||||
Each hit MUST be an aggregator package used only off the production path.
|
Each hit MUST be an aggregator target used only off the production path.
|
||||||
|
|
||||||
- **Observable checklist** for the structural shape, each a clear pass/fail: the API layer keeps its four-package split (Shared contract, Client, Server, ServerApp executable, Pattern 4); features depend on foundations and infrastructure but never on sibling features except a declared parent/child (Pattern 2); middleware packages stay single-purpose with minimal dependencies (Pattern 3); and the component layer is created in order Components, SharedComponents, AppComponents, AllComponents with the Components package never skipped (Pattern 5).
|
|
||||||
|
|
||||||
|
- **Observable checklist** for the structural shape, each a clear pass/fail: the API layer keeps its four-target split (Shared contract, Client, Server, ServerApp executable, Pattern 4); features depend on foundations and infrastructure but never on sibling features except a declared parent/child (Pattern 2); middleware targets stay single-purpose with minimal dependencies (Pattern 3); and the component layer is created in order Components, SharedComponents, AppComponents, AllComponents with the Components target never skipped (Pattern 5).
|
||||||
|
|||||||
19
third_party/rules-swift/exp/migration.md
vendored
19
third_party/rules-swift/exp/migration.md
vendored
@ -1,11 +1,11 @@
|
|||||||
# Migration Patterns
|
# Migration Patterns
|
||||||
|
|
||||||
Extracting code into a new package; splitting a large package.
|
Extracting code into a new target; splitting a large target.
|
||||||
|
|
||||||
### Extracting Code to New Package
|
### Extracting Code to New Target
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Create new package structure
|
# 1. Create new target structure
|
||||||
mkdir -p Sources/NewPackage
|
mkdir -p Sources/NewPackage
|
||||||
mkdir -p Tests/NewPackageTests
|
mkdir -p Tests/NewPackageTests
|
||||||
|
|
||||||
@ -29,14 +29,23 @@ swift test --filter NewPackageTests
|
|||||||
### Splitting Large Package
|
### Splitting Large Package
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
// Before: Large "Features" package
|
// Before: Large "Features" target
|
||||||
Features/
|
Features/
|
||||||
├── Auth/
|
├── Auth/
|
||||||
├── Profile/
|
├── Profile/
|
||||||
└── Settings/
|
└── Settings/
|
||||||
|
|
||||||
// After: Separate feature packages
|
// After: Separate feature targets
|
||||||
AuthFeature/
|
AuthFeature/
|
||||||
ProfileFeature/
|
ProfileFeature/
|
||||||
SettingsFeature/
|
SettingsFeature/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Acceptance check
|
||||||
|
|
||||||
|
A target extraction or split conforms when the manifest declares every new source
|
||||||
|
and matching test target, old imports and paths grep to zero, dependency edges
|
||||||
|
remain unidirectional, each new target passes its portability or lift-out check,
|
||||||
|
and both `swift build` and the full `swift test` suite pass. Run the repository's
|
||||||
|
format, lint, package-purity, and documentation gates before calling the migration
|
||||||
|
complete.
|
||||||
|
|||||||
5
third_party/rules-swift/exp/package-swift.md
vendored
5
third_party/rules-swift/exp/package-swift.md
vendored
@ -231,7 +231,9 @@ Conforms when the manifest's three top-level arrays are built the closure-with-l
|
|||||||
- **Each array opens a closure, not a bracket.** `deps`, `allProducts`, and `targets` read `let <name>: [...] = {` and return their grouped concatenation:
|
- **Each array opens a closure, not a bracket.** `deps`, `allProducts`, and `targets` read `let <name>: [...] = {` and return their grouped concatenation:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
grep -nE 'let (deps|allProducts|targets)[^=]*=[[:space:]]*\[' Packages/Package.swift
|
MANIFEST=Package.swift
|
||||||
|
[ -f "$MANIFEST" ] || MANIFEST=Packages/Package.swift
|
||||||
|
grep -nE 'let (deps|allProducts|targets)[^=]*=[[:space:]]*\[' "$MANIFEST"
|
||||||
```
|
```
|
||||||
|
|
||||||
Output MUST be empty. A hit means an array was written as an inline `[...]` literal, the anti-pattern this file replaces.
|
Output MUST be empty. A hit means an array was written as an inline `[...]` literal, the anti-pattern this file replaces.
|
||||||
@ -243,4 +245,3 @@ grep -nE 'let (deps|allProducts|targets)[^=]*=[[:space:]]*\[' Packages/Package.s
|
|||||||
## See also
|
## See also
|
||||||
|
|
||||||
- [`core/folder-grouping.md`](../core/folder-grouping.md): when many single-file targets share a semantic kind (e.g. 27 importers, 5 renderers), they can colocate under a single parent folder using `path:` + disjoint `sources:` declarations. A `flatImporter(_:in:dependencies:)` helper is the worked example: 26 single-file importer targets share `Sources/Import/Importers/<cluster>/` paths while remaining independent SPM targets. The closure-with-local-variables pattern above still applies; the helper just returns a `Target.target(...)` with the path/sources baked in.
|
- [`core/folder-grouping.md`](../core/folder-grouping.md): when many single-file targets share a semantic kind (e.g. 27 importers, 5 renderers), they can colocate under a single parent folder using `path:` + disjoint `sources:` declarations. A `flatImporter(_:in:dependencies:)` helper is the worked example: 26 single-file importer targets share `Sources/Import/Importers/<cluster>/` paths while remaining independent SPM targets. The closure-with-local-variables pattern above still applies; the helper just returns a `Target.target(...)` with the path/sources baked in.
|
||||||
|
|
||||||
|
|||||||
24
third_party/rules-swift/exp/testing.md
vendored
24
third_party/rules-swift/exp/testing.md
vendored
@ -1,10 +1,14 @@
|
|||||||
# Testing Patterns (Architecture Angle)
|
# Testing Patterns (Architecture Angle)
|
||||||
|
|
||||||
ExtremePackaging-specific testing concerns: per-package test target, cross-package test doubles. Full Swift Testing rules live in `testing.md` (the top-level rule). Both files state the per-package-test-target rule, by deliberate cross-context dual-homing.
|
ExtremePackaging-specific testing concerns: per-source-target test targets and
|
||||||
|
cross-target test doubles. Full Swift Testing rules live in `testing.md` (the
|
||||||
|
top-level rule). Both files state the source-target/test-target pairing rule by
|
||||||
|
deliberate cross-context dual-homing.
|
||||||
|
|
||||||
Full Swift Testing rules live in `testing.md`. Two ExtremePackaging-specific points:
|
Full Swift Testing rules live in `testing.md`. Two ExtremePackaging-specific points:
|
||||||
|
|
||||||
**Per-package test target.** Every source target ships with a matching test target. Run a single package in isolation:
|
**Per-source-target test target.** Every source target ships with a matching test
|
||||||
|
target. Run a single target's tests in isolation:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd <active-package-root>
|
cd <active-package-root>
|
||||||
@ -13,7 +17,13 @@ swift test --filter <PackageName>Tests
|
|||||||
|
|
||||||
Folder layout: `Sources/<Package>/` and `Tests/<Package>Tests/` in package repos; `Packages/Sources/<Package>/` and `Packages/Tests/<Package>Tests/` in project monorepos.
|
Folder layout: `Sources/<Package>/` and `Tests/<Package>Tests/` in package repos; `Packages/Sources/<Package>/` and `Packages/Tests/<Package>Tests/` in project monorepos.
|
||||||
|
|
||||||
**Cross-package test doubles.** Mocks live in the test target, not the source package. Public protocols can be defined in the source package; their mock implementations stay in `Tests/<Package>Tests/Mocks/` or `Packages/Tests/<Package>Tests/Mocks/`, matching repository shape. Never publish mocks from `Sources/` (they leak into production binaries).
|
**Cross-target test doubles.** Mocks live in the test target, not the source
|
||||||
|
target. Public protocols can be defined in the source target; their mock
|
||||||
|
implementations stay in `Tests/<Target>Tests/Mocks/` or
|
||||||
|
`Packages/Tests/<Target>Tests/Mocks/`, matching repository shape. Never publish
|
||||||
|
mocks from `Sources/` (they leak into production binaries), except in a declared
|
||||||
|
test-support target that the gate checks (`testing.md`, section "Exception: test-support
|
||||||
|
targets").
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
// Sources/ApiClient/APIClientProtocol.swift
|
// Sources/ApiClient/APIClientProtocol.swift
|
||||||
@ -24,3 +34,11 @@ public protocol APIClientProtocol {
|
|||||||
// Tests/ApiClientTests/Mocks/MockAPIClient.swift
|
// Tests/ApiClientTests/Mocks/MockAPIClient.swift
|
||||||
public struct MockAPIClient: APIClientProtocol { /* ... */ }
|
public struct MockAPIClient: APIClientProtocol { /* ... */ }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Acceptance check
|
||||||
|
|
||||||
|
Compare every source target in `Package.swift` with the test targets: each source
|
||||||
|
target MUST have exactly one `<SourceTarget>Tests` partner that depends on it. Grep
|
||||||
|
production `Sources/` for test-double names and confirm mocks live only under the
|
||||||
|
matching test target. Run each new target's filtered tests once, then run the full
|
||||||
|
suite and cite the total count.
|
||||||
|
|||||||
11
third_party/rules-swift/exp/verification.md
vendored
11
third_party/rules-swift/exp/verification.md
vendored
@ -3,12 +3,13 @@
|
|||||||
Final checklist before considering an ExtremePackaging change done.
|
Final checklist before considering an ExtremePackaging change done.
|
||||||
|
|
||||||
When applying these rules, always:
|
When applying these rules, always:
|
||||||
1. Check current package structure matches documented 20-package layout
|
1. Check the current target structure matches the active manifest
|
||||||
- Foundation: SharedModels, AppColors, AppFont
|
- Foundation: SharedModels, AppColors, AppFont
|
||||||
- Design System: AppTheme
|
- Design System: AppTheme
|
||||||
2. Verify new packages follow naming conventions
|
2. Verify new targets follow naming conventions
|
||||||
3. Ensure dependencies flow unidirectionally (Foundation → Infrastructure → Features → Apps)
|
3. Ensure imports and declared dependencies flow from Apps to Features to
|
||||||
4. Create test targets for all new packages
|
Infrastructure to Foundation, never from a lower layer to a higher one
|
||||||
|
4. Create test targets for all new source targets
|
||||||
5. Update Package.swift using closure-with-local-variables pattern:
|
5. Update Package.swift using closure-with-local-variables pattern:
|
||||||
- Define `deps`, `allProducts`, and `targets` using `let variable: [Type] = { ... }()` pattern
|
- Define `deps`, `allProducts`, and `targets` using `let variable: [Type] = { ... }()` pattern
|
||||||
- NEVER use inline array definitions
|
- NEVER use inline array definitions
|
||||||
@ -23,7 +24,7 @@ When applying these rules, always:
|
|||||||
- Grouped targets (modelTargets, apiTargets, uiTargets)
|
- Grouped targets (modelTargets, apiTargets, uiTargets)
|
||||||
8. Check platform separation in `Package.swift` uses `#if os()`, not `#if canImport()`. (Inside Swift source files, `#if canImport(UIKit)` / `#if canImport(AppKit)` is the correct idiom; the manifest is the only place `canImport` is forbidden.)
|
8. Check platform separation in `Package.swift` uses `#if os()`, not `#if canImport()`. (Inside Swift source files, `#if canImport(UIKit)` / `#if canImport(AppKit)` is the correct idiom; the manifest is the only place `canImport` is forbidden.)
|
||||||
9. Verify `.when(platforms:)` used for platform-specific dependencies within targets
|
9. Verify `.when(platforms:)` used for platform-specific dependencies within targets
|
||||||
10. For font/resource packages:
|
10. For font/resource targets:
|
||||||
- Confirm `.process()` used for resources (NOT `.copy()`)
|
- Confirm `.process()` used for resources (NOT `.copy()`)
|
||||||
- Verify `Bundle.module` used (NOT `Bundle.main`)
|
- Verify `Bundle.module` used (NOT `Bundle.main`)
|
||||||
- Check CoreText registration implemented with error handling
|
- Check CoreText registration implemented with error handling
|
||||||
|
|||||||
49
third_party/rules-swift/exp/when-to-create.md
vendored
49
third_party/rules-swift/exp/when-to-create.md
vendored
@ -1,8 +1,8 @@
|
|||||||
# When to Create a New Package
|
# When to Create a New Target
|
||||||
|
|
||||||
Always-loaded core. Use the decision tree first. The DO/DON'T list and the checklist below cover the corner cases.
|
Always-loaded core. Use the decision tree first. The DO/DON'T list and the checklist below cover the corner cases.
|
||||||
|
|
||||||
## PACKAGE CREATION DECISION TREE
|
## TARGET CREATION DECISION TREE
|
||||||
|
|
||||||
```
|
```
|
||||||
Need to add new code?
|
Need to add new code?
|
||||||
@ -19,28 +19,28 @@ Need to add new code?
|
|||||||
│ └─ View helper/modifier? → Add to SharedViews
|
│ └─ View helper/modifier? → Add to SharedViews
|
||||||
│
|
│
|
||||||
├─ Is it a complete user-facing feature?
|
├─ Is it a complete user-facing feature?
|
||||||
│ └─ YES → Create new *Feature package
|
│ └─ YES → Create new *Feature target
|
||||||
│ Example: ProfileFeature, SettingsFeature, PaymentFeature
|
│ Example: ProfileFeature, SettingsFeature, PaymentFeature
|
||||||
│
|
│
|
||||||
├─ Is it API/networking related?
|
├─ Is it API/networking related?
|
||||||
│ ├─ Client-side? → Add to ApiClient (or create new client package)
|
│ ├─ Client-side? → Add to ApiClient (or create new client target)
|
||||||
│ ├─ Server-side? → Add to ApiServer
|
│ ├─ Server-side? → Add to ApiServer
|
||||||
│ ├─ Shared DTOs? → Add to ApiShared (OpenAPI generated)
|
│ ├─ Shared DTOs? → Add to ApiShared (OpenAPI generated)
|
||||||
│ └─ Middleware? → Create new *Middleware package
|
│ └─ Middleware? → Create new *Middleware target
|
||||||
│
|
│
|
||||||
├─ Is it shared infrastructure?
|
├─ Is it shared infrastructure?
|
||||||
│ └─ YES → Create new Shared* package
|
│ └─ YES → Create new Shared* target
|
||||||
│ Example: SharedUtilities, SharedNetworking
|
│ Example: SharedUtilities, SharedNetworking
|
||||||
│
|
│
|
||||||
└─ Still unsure?
|
└─ Still unsure?
|
||||||
└─ Ask: "Could this be reused in isolation?"
|
└─ Ask: "Could this be reused in isolation?"
|
||||||
├─ YES → Create new package
|
├─ YES → Create new target
|
||||||
└─ NO → Add to most specific existing package
|
└─ NO → Add to most specific existing target
|
||||||
```
|
```
|
||||||
|
|
||||||
## WHEN TO CREATE A NEW PACKAGE
|
## WHEN TO CREATE A NEW TARGET
|
||||||
|
|
||||||
### ✅ DO Create New Package When:
|
### ✅ DO Create New Target When:
|
||||||
|
|
||||||
1. **New Feature Module**
|
1. **New Feature Module**
|
||||||
- Complete user-facing feature (login, profile, payments)
|
- Complete user-facing feature (login, profile, payments)
|
||||||
@ -65,17 +65,17 @@ Need to add new code?
|
|||||||
- Expensive compilation (generated code)
|
- Expensive compilation (generated code)
|
||||||
- Example: `ApiShared` (OpenAPI generated)
|
- Example: `ApiShared` (OpenAPI generated)
|
||||||
|
|
||||||
### ❌ DON'T Create New Package When:
|
### ❌ DON'T Create New Target When:
|
||||||
|
|
||||||
1. **Single Use Case**
|
1. **Single Use Case**
|
||||||
- Only used by one feature
|
- Only used by one feature
|
||||||
- Tightly coupled to specific screen
|
- Tightly coupled to specific screen
|
||||||
- → Add to that feature's package
|
- → Add to that feature's target
|
||||||
|
|
||||||
2. **Trivial Helper**
|
2. **Trivial Helper**
|
||||||
- 1-2 small functions
|
- 1-2 small functions
|
||||||
- No external dependencies
|
- No external dependencies
|
||||||
- → Add to existing utility package
|
- → Add to existing utility target
|
||||||
|
|
||||||
3. **Temporary Code**
|
3. **Temporary Code**
|
||||||
- Proof of concept
|
- Proof of concept
|
||||||
@ -84,24 +84,24 @@ Need to add new code?
|
|||||||
|
|
||||||
## DECISION CHECKLIST
|
## DECISION CHECKLIST
|
||||||
|
|
||||||
### Before Creating a New Package
|
### Before Creating a New Target
|
||||||
|
|
||||||
- [ ] Package has single, clear responsibility
|
- [ ] Target has single, clear responsibility
|
||||||
- [ ] Package name follows conventions (*Feature, Shared*, Api*, *Components)
|
- [ ] Target name follows conventions (*Feature, Shared*, Api*, *Components)
|
||||||
- [ ] Dependencies are minimal and explicit
|
- [ ] Dependencies are minimal and explicit
|
||||||
- [ ] No circular dependencies introduced
|
- [ ] No circular dependencies introduced
|
||||||
- [ ] Can be built and tested in isolation
|
- [ ] Can be built and tested in isolation
|
||||||
- [ ] Fits into architectural layer (Foundation/Infrastructure/Feature/App)
|
- [ ] Fits into architectural layer (Foundation/Infrastructure/Feature/App)
|
||||||
- [ ] Test target created alongside source target
|
- [ ] Test target created alongside source target
|
||||||
- [ ] Product registered in Package.swift products array
|
- [ ] Product registered in Package.swift products array
|
||||||
- [ ] Dependencies only flow upward (no higher-layer dependencies)
|
- [ ] Imports and declared dependencies point only to lower layers (no higher-layer dependencies)
|
||||||
|
|
||||||
### Before Adding to Existing Package
|
### Before Adding to Existing Target
|
||||||
|
|
||||||
- [ ] New code shares responsibility with existing code
|
- [ ] New code shares responsibility with the existing target
|
||||||
- [ ] No better-suited package exists
|
- [ ] No better-suited target exists
|
||||||
- [ ] Not creating a "God package" with mixed concerns
|
- [ ] Not creating a "god target" with mixed concerns
|
||||||
- [ ] Won't introduce unwanted dependencies to package consumers
|
- [ ] Won't introduce unwanted dependencies to target consumers
|
||||||
|
|
||||||
### Before Modifying Package.swift
|
### Before Modifying Package.swift
|
||||||
|
|
||||||
@ -119,7 +119,7 @@ Need to add new code?
|
|||||||
- [ ] Used trailing commas in all arrays
|
- [ ] Used trailing commas in all arrays
|
||||||
- [ ] Applied `.when(platforms:)` for platform-specific dependencies within targets
|
- [ ] Applied `.when(platforms:)` for platform-specific dependencies within targets
|
||||||
|
|
||||||
### Before Adding Font/Resource Package
|
### Before Adding Font/Resource Target
|
||||||
|
|
||||||
- [ ] Used `.process()` for resources, NEVER `.copy()`
|
- [ ] Used `.process()` for resources, NEVER `.copy()`
|
||||||
- [ ] Created `FontRegistration.swift` with CoreText registration
|
- [ ] Created `FontRegistration.swift` with CoreText registration
|
||||||
@ -130,6 +130,5 @@ Need to add new code?
|
|||||||
- [ ] Added console logging for registration success/failure
|
- [ ] Added console logging for registration success/failure
|
||||||
- [ ] Called `FontRegistration.registerFonts()` in app init BEFORE UI renders
|
- [ ] Called `FontRegistration.registerFonts()` in app init BEFORE UI renders
|
||||||
- [ ] NEVER used Info.plist font registration (`UIAppFonts`, `ATSApplicationFontsPath`)
|
- [ ] NEVER used Info.plist font registration (`UIAppFonts`, `ATSApplicationFontsPath`)
|
||||||
- [ ] Package has zero dependencies (fonts are Foundation layer)
|
- [ ] Target has zero dependencies (fonts are Foundation layer)
|
||||||
- [ ] Resources organized in dedicated subdirectory (e.g., `Fonts/`)
|
- [ ] Resources organized in dedicated subdirectory (e.g., `Fonts/`)
|
||||||
|
|
||||||
|
|||||||
57
third_party/rules-swift/framework-policy.md
vendored
57
third_party/rules-swift/framework-policy.md
vendored
@ -1,11 +1,18 @@
|
|||||||
# Framework Policy (MANDATORY, ZERO TOLERANCE)
|
# Apple UI Framework Policy (MANDATORY IN APPLE CLIENT TARGETS)
|
||||||
|
|
||||||
This codebase targets Apple platforms exclusively. Every suggestion, comparison,
|
**Status: MANDATORY when selecting frameworks for an Apple client or UI target.**
|
||||||
recommendation, citation, and dependency stays inside Apple's own SDKs (SwiftUI,
|
Every UI-framework suggestion, comparison, recommendation, citation, and
|
||||||
UIKit, AppKit, Foundation, and the rest of Apple's frameworks) and the
|
dependency for that target stays inside Apple's own SDKs (SwiftUI, UIKit, AppKit,
|
||||||
Swift-native ecosystem. This is not a default to be argued out of. It is a hard
|
Foundation, and the rest of Apple's frameworks) and the Swift-native ecosystem.
|
||||||
boundary that holds in code, comments, documentation, commit messages, reviews,
|
This is a hard boundary in code, comments, documentation, commit messages,
|
||||||
and conversation alike.
|
reviews, and conversation about the Apple presentation tier.
|
||||||
|
|
||||||
|
This policy does not govern framework-free domain or presentation models,
|
||||||
|
cross-platform libraries and CLIs, or Linux server targets. Those surfaces follow
|
||||||
|
[cross-platform.md](cross-platform.md) and, for production servers,
|
||||||
|
[linux-server.md](linux-server.md). Portability of
|
||||||
|
non-UI Swift code is compatible with this policy; a foreign UI toolkit in an
|
||||||
|
Apple client is not.
|
||||||
|
|
||||||
## The mandate
|
## The mandate
|
||||||
|
|
||||||
@ -19,9 +26,10 @@ and conversation alike.
|
|||||||
toolkit without exception. The prohibition is stated as a category on purpose:
|
toolkit without exception. The prohibition is stated as a category on purpose:
|
||||||
do not enumerate the banned stacks, do not name them at all. "I only cited it as
|
do not enumerate the banned stacks, do not name them at all. "I only cited it as
|
||||||
neutral corroboration" is not a defence.
|
neutral corroboration" is not a defence.
|
||||||
- **The only exception is Linux.** When the target is genuinely Linux, use native
|
- **Keep platform-specific choices in their platform target.** Apple UI targets
|
||||||
**C or C++** only. No other language, runtime, or framework is acceptable on
|
use Apple SDKs. Linux server and cross-platform targets use the Swift-native
|
||||||
Linux.
|
choices defined by their own rules and do not weaken the Apple presentation
|
||||||
|
boundary.
|
||||||
|
|
||||||
## Concept versus rival framework: the exact line
|
## Concept versus rival framework: the exact line
|
||||||
|
|
||||||
@ -39,17 +47,24 @@ for the banned stack just because it is the canonical illustration.
|
|||||||
|
|
||||||
## Why this exists
|
## Why this exists
|
||||||
|
|
||||||
A single non-Apple reference, even an offhand one, reframes the codebase as
|
A non-Apple UI-framework reference in an Apple client, even an offhand one,
|
||||||
platform-agnostic and invites drift in dependencies, comparisons, and reviewer
|
invites drift in dependencies, comparisons, and reviewer expectations. Holding
|
||||||
expectations. Holding the boundary at the level of what we are even willing to
|
the presentation boundary keeps Apple UI decisions anchored to Apple's frameworks
|
||||||
name keeps every decision anchored to Apple's frameworks and the Swift-native
|
without misclassifying a portable domain model or a Swift server as a violation.
|
||||||
ecosystem, which is the whole point of the project.
|
|
||||||
|
|
||||||
## Acceptance check
|
## Acceptance check
|
||||||
|
|
||||||
A change conforms when: (1) no non-Apple UI or cross-platform stack is named in
|
An Apple-client or UI change conforms when: (1) no non-Apple UI or cross-platform
|
||||||
code, comments, docs, or commit text; (2) every new dependency is an Apple SDK or
|
stack is named in that target's code, comments, docs, or commit text; (2) every
|
||||||
a Swift-native library; (3) any non-Apple-targeted comparison or "what others do"
|
new presentation dependency is an Apple SDK or a Swift-native library; and (3)
|
||||||
aside is absent; and (4) the only non-Swift code present targets Linux and is
|
any rival-framework comparison or "what others do" aside is absent. Cross-platform
|
||||||
native C or C++. A change that names a rival stack anywhere, even as corroboration,
|
domain code and Swift Linux/server code are audited under their companion rules,
|
||||||
fails this rule.
|
not this acceptance check.
|
||||||
|
|
||||||
|
## Companion rules
|
||||||
|
|
||||||
|
- [cross-platform.md](cross-platform.md): portability boundaries for libraries, CLIs, and split
|
||||||
|
Apple-client/Linux-server repositories.
|
||||||
|
- [linux-server.md](linux-server.md): operational choices for production Swift on Linux.
|
||||||
|
- [ui/native-per-platform.md](ui/native-per-platform.md): native presentation principles for each supported
|
||||||
|
client platform.
|
||||||
|
|||||||
75
third_party/rules-swift/help.md
vendored
Normal file
75
third_party/rules-swift/help.md
vendored
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
# Help: one file of words, a Help Book on the Mac, in-app help and a Settings page on an iPad and an iPhone, tips where the work is
|
||||||
|
|
||||||
|
**Status: MANDATORY for every app that ships to people.** An app's help is written once, in one file, and every platform's help is made from that file: a Help Book on the Mac, an in-app help sheet and a page in the Settings app on an iPad and an iPhone, and a few tips (TipKit) at the places a person first needs them. Each platform says it in its own words, because a Mac is clicked and a phone is touched. The About page says the name and the version the build stamped (`versioning.md`).
|
||||||
|
|
||||||
|
Each rule below carries its basis: **documented** (Apple documentation or the Human Interface Guidelines, named so it can be read), **observed** (found by building and running it), or **design** (a choice, with the reason). Where the Apple document is not in the Cupertino corpus the basis says so, and the claim rests on what was observed.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
- **Help kept in several places says different things.** A Help Book, an in-app list and a web page written by hand disagree within a release. One file has nothing to disagree with.
|
||||||
|
- **A platform's help in another platform's words is wrong for the reader.** The Human Interface Guidelines say help must not tell people to click a button on an iPhone or tap a menu item on a Mac. A reader who is told to hold Command on a phone stops reading.
|
||||||
|
- **Each platform has its own place for help, and people look there.** A Mac has a Help menu with a search field; an iPad with a keyboard has the same menu bar; a phone has no menu bar, so help sits in the app and the version in the Settings app. Putting help where the platform puts it is what makes it found.
|
||||||
|
- **A tip is for the moment of need, a page is for looking up.** The two do different work and neither replaces the other.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
1. **One source of the words: a JSON file in the package that owns the editor.** `Resources/Help.json` holds the topics (`id`, `title`, `symbol`, and either `text` for every platform or `mac` and `touch` for a topic the platforms say differently) and the keys of the Mac. The file is a resource of the package; the in-app help decodes it, and the Help Book is made from it by a script. No topic is written as a string in Swift. A damaged file is refused when it is decoded, and the in-app help says so; it never shows an empty help. *Basis: design.*
|
||||||
|
2. **Each platform is told in its own words.** The words are chosen for the platform the help is read on: the Mac's help says click, Command, the File menu and a window; a touch screen's says tap, touch, the list of files and the Files app. A touch screen's help has no list of keyboard keys. *Basis: documented (Human Interface Guidelines, Offering help: "don't write copy that tells people to click a button on an iPhone or tap a menu item on a Mac").*
|
||||||
|
3. **The Mac carries an Apple Help Book, and the system makes the Help menu from it.** The app's `Info.plist` names the book with `CFBundleHelpBookFolder` (the folder, `<App>.help`) and `CFBundleHelpBookName` (the book's own bundle identifier). The book is a folder in the app's resources. An app that carries a book does not replace the Help menu: a SwiftUI `CommandGroup(replacing: .help)` is only for an app that has none, and the check for the keys decides which. The system's menu has the search field and the item that opens the help viewer. *Basis: observed (macOS 26, SwiftUI: with the two keys set and no replacing group, the Help menu showed "<App> Help", the item opened the system's help window with its search field and the book's access page; with a replacing group the system's window did not open). The Help Book format is described in Apple's Help Programming Guide, which is not in the Cupertino corpus.*
|
||||||
|
4. **The Help Book has the structure the viewer reads.** `<App>.help/Contents/Info.plist` has `CFBundleIdentifier` (equal to `CFBundleHelpBookName` of the app), `CFBundleName`, `CFBundlePackageType` `BNDL`, `CFBundleSignature` `hbwr`, `HPDBookType` `3`, `HPDBookTitle`, `HPDBookAccessPath` (the access page), `HPDBookIndexPath`, `HPDBookCSIndexPath` and `HPDBookKBProduct`. `Contents/Resources/<lang>.lproj/` holds the access page, a page for each topic in `pgs/`, the shared style in `shrd/`, and the two search indexes. Each page has `<meta name="AppleTitle">`, `description`, `keywords` and `robots` `anchors`. The indexes are made by `hiutil -Caf <name>.helpindex <lang>.lproj` and `hiutil -I corespotlight -Caf <name>.cshelpindex <lang>.lproj`. The style follows the system's colors and appearance (`color-scheme: light dark`). *Basis: observed (the book built this way opened in the system's help window, with its topics, its pages, its search field, and in dark appearance).*
|
||||||
|
5. **The Help Book is generated from the file, and committed.** One script writes the book from `Help.json`; `--check` regenerates it into a throwaway folder and compares it with the committed book, leaving out the two index files, which hold times. A build needs nothing outside its repository, and a change of the words that is not carried into the book fails the gate. *Basis: design.*
|
||||||
|
6. **The book's own files are named in lowercase, as `file-naming.md` asks.** The names are free in the book's `Info.plist`, so the access page and the indexes are `<app>.html`, `<app>.helpindex` and `<app>.cshelpindex`. The names a tool expects verbatim (`Info.plist`, and `Root.plist` of a Settings bundle) are exempt in the file-name gate, by name. *Basis: design; documented for the exempt names (Apple, Settings bundle: the main file is `Root.plist`).*
|
||||||
|
7. **An iPad and an iPhone have no Help Book; their help is an in-app page, in the places the platform has.** The page is the list of the same topics, in the touch words, in a sheet with a Done button. It is opened from a Help menu of its own in the toolbar (not from inside View), and, on an iPad, from the menu bar as well: About in the app menu and Help in the Help menu, which a keyboard reveals. A phone has no menu bar, so the toolbar is its place. *Basis: documented for the menu bar (Human Interface Guidelines, The menu bar: menu bar menus on iPad are similar to those on Mac and appear in the same order); design for the sheet and the toolbar.*
|
||||||
|
8. **About says the name and the version, in the app menu of the Mac.** It shows the display name, the version and the build number as the build stamped them into the `Info.plist` (`versioning.md`), and the richer form for bug reports, small. It never holds a version written by hand. *Basis: design; documented for the place (the Mac's app menu, Human Interface Guidelines, The menu bar).*
|
||||||
|
9. **The Settings app shows the version, from a Settings bundle.** An iPad and an iPhone app carry `Settings.bundle` with `Root.plist`: a group "About" and read-only title rows (`PSTitleValueSpecifier`, `Key` the defaults key) for the version and the build. The Settings app reads the rows from the app's defaults, so the app writes the two values to its defaults each time it starts, from the stamped `Info.plist`. The strings file is named by `StringsTable` (lowercase, `root`). *Basis: documented (Apple, Building a Settings bundle for your app: the title element "displays a read-only version of the setting's name and its currently configured value", read from the app's defaults database); observed (the values were in the app's defaults after a launch).*
|
||||||
|
10. **Tips are for the first thing to do, and go when it is done.** A tip is a TipKit `Tip` with a rule that stops it once its feature has been used (a `@Parameter` set by the feature), shown inline where the thing is (`TipView`) and never over the work. TipKit is configured once, at launch, and a failure to configure is logged with its reason, not swallowed: without its store, tips are not shown, and nothing else changes. A tip never holds what the help does not also say. *Basis: documented (TipKit, iOS 17 and macOS 14; Human Interface Guidelines, Offering help: help is related to the task people are doing and easy to dismiss).*
|
||||||
|
11. **The help is words, not pictures of the interface, until the interface is stable.** A picture of a screen is a second source that is not generated. A topic names the control and says where it is. *Basis: design.*
|
||||||
|
12. **Language.** The help is in one language until a second is shipped; then each language has its own `<lang>.lproj` in the Help Book and its own text in the file, and the check of rule 5 covers all of them. A claim that the help is localized is made only with the files for the language present. *Basis: design; not established here (no second language has been shipped).*
|
||||||
|
|
||||||
|
## How to test it
|
||||||
|
|
||||||
|
- **The file against its rules.** Decode the bundled file in a test: topics and keys are present; every `id` and `title` is unique and not empty; every topic says something on the Mac and on a touch screen; a damaged file (empty, wrong shape, not JSON) is refused with an error and never read as empty help.
|
||||||
|
- **The words against the platforms.** Join the Mac's words and the touch words of all topics and require that the Mac's contain none of `tap`, `touch`, `files app`, `open in`, and the touch words none of `click`, `command `, `file menu`. Run it on the Mac and on a simulator, so each platform is checked in its own build. Require one anchor phrase each (`command and click`, `tap it`) so that a test of absence cannot pass on an empty text.
|
||||||
|
- **The check for a book, as a function of the plist.** Require `true` for both keys set and not empty and `false` for each of: only one key, an empty value, none.
|
||||||
|
- **The book against the file.** Run the script's `--check`; it must say the book is current. Change a word in the file and require that it fails and names the difference.
|
||||||
|
- **The Mac's menu in a built app.** Build and open the app; read its menu bar through the accessibility interface of that one process (`System Events`, the process, `menu "Help"`): the item "<App> Help" is there; click it and require that the system's help window opens (it belongs to the system's help viewer, whose process is not the app's), and look at the window: the access page, its topics, the search field. Never scan the windows of other processes.
|
||||||
|
- **The sheets on a simulator.** A UI test opens the Help menu of the toolbar, reads the About page (the version matches `^Version \d+\.\d+\.\d+ \(\d+\)$` and the richer form contains `+`), closes it with Done, opens the help and finds a topic.
|
||||||
|
- **The tip.** A UI test finds the tip, performs the thing it points at, and requires that the tip is gone.
|
||||||
|
- **The Settings page.** After a launch the app's defaults hold the version and build keys; the built app contains `Settings.bundle`.
|
||||||
|
|
||||||
|
## Acceptance check
|
||||||
|
|
||||||
|
Run from the repository root. `HELP_JSON` is the file, `BOOK` the committed book, `APP` a built app (Mac) and `IOSAPP` a built app of an iPad or iPhone target. Every command must print nothing, except where it says it must print a line:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 1. the file is valid JSON and every topic has an id, a title, a symbol, and a text for each platform
|
||||||
|
python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); [sys.exit("bad topic %s" % t.get("id")) for t in d["topics"] if not (t.get("id") and t.get("title") and t.get("symbol") and (t.get("text") or (t.get("mac") and t.get("touch"))))]' "$HELP_JSON"
|
||||||
|
|
||||||
|
# 2. no Swift source holds a topic as a string (the words live in the file only)
|
||||||
|
grep -rnE 'Topic\(\s*title:' "$SOURCES" --include='*.swift'
|
||||||
|
|
||||||
|
# 3. the committed Help Book is the one the file makes (prints "help book is current")
|
||||||
|
sh scripts/build-help-book.sh --check
|
||||||
|
|
||||||
|
# 4. the book has its plist, its pages and both indexes
|
||||||
|
test -s "$BOOK/Contents/Info.plist" && test -s "$BOOK"/Contents/Resources/*.lproj/*.helpindex && test -s "$BOOK"/Contents/Resources/*.lproj/*.cshelpindex || echo "book is incomplete"
|
||||||
|
|
||||||
|
# 5. the book's files are lowercase (the C locale, or a UTF-8 locale reads [A-Z] as both cases)
|
||||||
|
LC_ALL=C find "$BOOK/Contents/Resources" -type f -name '*[A-Z]*' -print
|
||||||
|
|
||||||
|
# 6. the built Mac app names its book and carries it (this must print two lines and the folder)
|
||||||
|
plutil -extract CFBundleHelpBookFolder raw "$APP/Contents/Info.plist"; plutil -extract CFBundleHelpBookName raw "$APP/Contents/Info.plist"; ls -d "$APP"/Contents/Resources/*.help
|
||||||
|
|
||||||
|
# 7. the book's own identifier is the name the app gives (the two lines must be equal)
|
||||||
|
plutil -extract CFBundleHelpBookName raw "$APP/Contents/Info.plist"; plutil -extract CFBundleIdentifier raw "$APP"/Contents/Resources/*.help/Contents/Info.plist
|
||||||
|
|
||||||
|
# 8. an iPad or iPhone app carries a Settings bundle with a Root.plist
|
||||||
|
test -s "$IOSAPP/Settings.bundle/Root.plist" || echo "no Settings bundle"
|
||||||
|
|
||||||
|
# 9. help words are not hard-coded for one platform in the sheets: no 'click' in the touch words of the file
|
||||||
|
python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); [print(t["id"]) for t in d["topics"] if "click" in (t.get("touch") or t.get("text") or "").lower()]' "$HELP_JSON"
|
||||||
|
```
|
||||||
|
|
||||||
|
Promote commands 1 to 5 and 9 to a build-failing script with a self-test (a fixture that breaks each must fail it; a fixture that obeys all must pass it), and the tests above to the project's test suite. The check passes when the commands print as stated, the tests pass, the Mac's help window has been opened from the menu of a built app, and rule 12 is stated as not established until a second language ships.
|
||||||
|
|
||||||
|
Cross-references: `versioning.md` (the version the About page and the Settings page show, stamped into the `Info.plist`), `core/file-naming.md` (the lowercase names of the book's files), `ui/appuikit.md` and `package-structure.md` (the help view is in the presentation package, the words in its resources), `testing.md` and `core/testing-discipline.md` (the tests), `core/oracle-first.md` (the file is the answer key for the book and the in-app help), `core/proof-discipline.md` (the basis of each claim), `app-logging.md` (a tip that cannot start is logged).
|
||||||
101
third_party/rules-swift/iphone-duo.md
vendored
Normal file
101
third_party/rules-swift/iphone-duo.md
vendored
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
# iPhone Duo: two displays, many poses, and the transitions between them
|
||||||
|
|
||||||
|
**Status: MANDATORY for every iPhone app and for every UI framework that runs on an iPhone.** iPhone Duo is Apple's foldable iPhone (announced September 2026). It has an outer display (5.4 inch) and an inner display (7.6 inch), a hinge between them, and many ways to hold it: closed, open, partly folded like a book, standing on a table, on its edges. An app on it is resized by the person, at any moment, while it runs: folding or unfolding the device changes the space, the size classes and the place of the controls under the app's feet. The app is built so that a resize is a re-layout of the same app, never a restart of it, and is tested in each pose and across the changes between poses.
|
||||||
|
|
||||||
|
Each rule carries its basis: **documented** (an Apple page, named with its address, read on 2026-10-28), **observed** (found by running it, with the build), or **design** (a choice, with the reason). None of these pages is in the Cupertino corpus yet. The claims below rest on the pages themselves, read in full, and where a page is silent the rule says so (see "What Apple has not documented"). Availability is stated as the SDK states it (the Swift interface of the iOS 27.1 SDK and the UIKit headers, read on 2026-10-28): the arrangement, reserved-region, hinge and vertical-toolbar APIs are `anyAppleOS 27.1`, and the full-screen experience needs **Xcode 27.1**. Two facts the pages do not carry: **the macOS SDK shipped with Xcode 27.1 is 27.0 and does not contain those symbols**, and **a type listed for a platform is not a promise that every member of it is**: `ToolbarItemAxisBehavior.horizontalOnly` and `.verticalPreferred`, `ToolbarOverflowMenu` and the `prefers...` cases of `ToolbarVerticalCompressionBehavior` are marked unavailable on macOS.
|
||||||
|
|
||||||
|
Sources (Apple): the Human Interface Guidelines page *Designing for iPhone Duo* (`developer.apple.com/design/human-interface-guidelines/designing-for-iphone-duo`); the documentation page *Preparing your app for iPhone Duo* (`developer.apple.com/documentation/technologyoverviews/preparing-your-app-for-iphone-duo`) and its page of three steps (`developer.apple.com/iphone-duo/prepare/`); the documentation of `ArrangementView` and of `GeometryProxy.reservedRegions(kind:options:layoutDirectionBehavior:)`; the Tech Talks *Prepare your app for iPhone Duo* (111461), *Raise the bar with iPhone Duo* (111462), *Strike a pose with adaptive layouts on iPhone Duo* (111463), *Leverage multiple displays and scenes on iPhone Duo* (111464), *Design for iPhone Duo* (111466), and the WWDC26 session *Modernize your UIKit app* (278); the announcement (`apple.com/newsroom/2026/09/apple-unveils-iphone-duo/`).
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
- **The size is no longer the device's.** An app on iPhone Duo is one size on the outer display and another on the inner, and another again in Split View, partly folded, or rotated. Code that reads the size once, or decides the layout from the device, orientation or screen, is wrong the first time the person folds the phone.
|
||||||
|
- **A fold is a transition the app lives through.** Apple says that "as iPhone Duo is opened, closed, partially folded, and rotated, content transitions between displays and adjusts for rotated positions", and that "apps resize when the device folds or unfolds". What the person was doing (a selection, a scroll position, a half-typed field, an unsaved edit, the page they were on) is still there on the other side, or the app is broken.
|
||||||
|
- **The system does most of it, for an app that lets it.** Standard containers (split view, tab view, navigation stack) and standard bars adapt to every pose, and keep controls away from the fold. A hand-built layout has to do all of it and usually does not.
|
||||||
|
- **The part Apple has not written down is the part most likely to bite.** The pages say little about what happens to state, scenes and animation across a fold. An app that relies on a behavior nobody documented has a bug that only shows on the device.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
1. **Build with the latest SDK, or the app is letterboxed.** Without a rebuild the app runs but the open display shows the usual iPhone size centered, and the closed display leaves the status bar and camera to its right. With the iOS 27 SDK it fills most of the inner display; with the **iOS 27.1 SDK (Xcode 27.1)** it is edge to edge and the toolbars and tab bars lay out vertically. The page of the documentation says that an app built with Xcode 26 or earlier does not extend under the status bar and camera. *Basis: documented (Preparing your app for iPhone Duo; the three steps; Tech Talk 111461).*
|
||||||
|
2. **Read the size on every change, from the view, never from the screen or the launch.** A fold resizes the app in the middle of a session, so a size read at launch or in `viewIsAppearing` is stale. Use `view.bounds` or `window.bounds`; in SwiftUI `GeometryReader` or `onGeometryChange`; lay out in `layoutSubviews` or `viewDidLayoutSubviews`, and use `viewWillTransition(to:with:)` for work that must run on a change of size. In a window-scene delegate, `windowScene(_:didUpdateEffectiveGeometry:)` reports the scene's geometry changing and `windowScene.effectiveGeometry.coordinateSpace.bounds` is the space. Do not cache a size. *Basis: documented (the three steps: "reading size once at launch or in `viewIsAppearing` is not enough"; WWDC26 278).*
|
||||||
|
3. **Layout is decided by size classes and the space, not by the device.** Outer display: compact width (portrait regular height, landscape compact height). Inner display: regular width and regular height. A phone app can have any combination of size classes, so `horizontalSizeClass == .regular` does not mean an iPad. Do not use `userInterfaceIdiom`, a model, a pixel size (`bounds.height == 844`), `UIDevice.current.orientation`, `statusBarOrientation`, `interfaceOrientation` or `windowScene.effectiveGeometry.interfaceOrientation` to lay out. Decide wide or tall by comparing the width and the height of the space. The inner display does not honor the orientations an app declares; they are a preference the system may ignore. Do not design a layout for each pose: design two size classes, compact and regular, and let one layout grow into the other. *Basis: documented (HIG: Device poses; the three steps; Tech Talk 111461 and 111466).*
|
||||||
|
4. **No `UIScreen.main`.** On a two-display device it is ambiguous and "will be deprecated in a future release". Get a screen from the window scene (`view.window?.windowScene?.screen`), never store one, make a window with `UIWindow(windowScene:)`, and take the scale from `traitCollection.displayScale` (UIKit) or `@Environment(\.displayScale)` (SwiftUI). The scene lifecycle (`UISceneDelegate`) is required when building with the latest SDKs: without it the app no longer launches. *Basis: documented (the three steps; Tech Talk 111461; WWDC26 278).*
|
||||||
|
5. **A fold changes the size class, so state does not live in anything the size class replaces.** Selection, scroll position, text, unsaved edits and the page the person is on are held by the model that is above the layout, and the layout is a pure function of the size classes (and the space). A hand-written `if compact { A } else { B }` (or two view controllers swapped by a size-class check) is a different view with a different identity on each side of the fold, and whatever `A` held in its own `@State` is gone when `B` appears. Prefer the system's adaptive containers, which keep one structure across the change: a split view (`NavigationSplitView`, `UISplitViewController`) shows two panes on the inner display and collapses to one on the outer, and tab views and navigation stacks adapt in every pose. Where a swap is unavoidable, the state is written to the model before it and read back after, and a test proves it (How to test it). Keep the information hierarchy the same on both displays and add a level on the inner one if it suits the content (the Mail example of the Human Interface Guidelines: a list or a message when closed, both side by side when open). *Basis: documented for "keep functionality and the state of elements the same between displays", "maintain the same functionality across device poses", the split view behavior, and the Mail example (HIG: Best practices; Split views); design for the rest (what a size-class swap does to a view's own state is a property of SwiftUI and UIKit, not a statement of the page).*
|
||||||
|
6. **Move only what has to move when the device folds.** "Avoid extreme layout changes as people fold the device. Move only what's necessary to keep elements visible and easy to tap." Small adjustments, not a different arrangement. Keep controls in the same relative positions in every pose, so that nobody has to learn where actions went. Content that scrolls does not have to avoid the fold; interactive elements do, as far as they can. *Basis: documented (HIG: Reserved regions; Vertical controls; Tech Talk 111466).*
|
||||||
|
7. **Reserved regions: ask for them, do not guess them (iOS 27.1).** A reserved region is "an area of your view that an entity reserves, such as the device hardware". There are two kinds. `occlusion`: something covers your content (the Dynamic Island, a camera, window controls): place content outside it. `division`: content splits into separate regions, as at the fold of the hinge: arrange content on both sides. A region has a frame, margins, an active state and an identifier. In SwiftUI call `GeometryProxy.reservedRegions(kind:options:layoutDirectionBehavior:)` on the proxy of a `GeometryReader`; in UIKit call `UIView.reservedRegions(kind:options:)`. By default only the active regions come back; pass `includeInactive` for the others (an inactive fold has zero width, and is still useful for deciding, for instance, on an even number of grid columns). Frames are in the view's coordinate space, measured from the top-leading corner, and are mirrored for right-to-left languages unless `layoutDirectionBehavior` is `fixed`: compute from (0, 0) and add `bounds.origin` only when placing a view. The fold's division region is active only while the device is folded. The inner camera is an occlusion region that is active only while the camera is. Framework views, alerts, menus, sheets and split views already adjust; custom layouts do it themselves, for their highest-priority controls first. The API is **iOS 27.1**: gate it with `#available(iOS 27.1, *)` and lay out without it before. *Basis: documented (GeometryProxy.reservedRegions; Tech Talk 111463; HIG: Reserved regions).*
|
||||||
|
8. **An arrangement view for two related views, in its place (iOS 27.1).** `ArrangementView` (SwiftUI) and `UIArrangementViewController` (UIKit) lay out a primary and a secondary view by size class, orientation and reserved regions. **Split**: next to each other when wider than tall, one above the other when taller than wide; closed in landscape it shows only the primary view. **Overlay**: stacked on a flat display, and next to each other on a folded one (secondary leading and primary trailing in landscape; secondary on top and primary below in portrait). `arrangementViewStyle(.split.axes(...))` or `.overlay` chooses, and `overlayArrangementZIndex` (SwiftUI) or `state(for:)` (UIKit) lets a view follow the change between stacked and side by side. A layout already built with `HStack` or `VStack` maps to split, one built with `ZStack` to overlay. An arrangement view is not put inside a scroll view, a list, a grid, a navigation split view or another container that lays out views, and it does not hold a navigation split view or a tab view: it lays out content and has no navigation, so navigation is placed around it. *Basis: documented (ArrangementView documentation; Tech Talk 111463; HIG: Arrangement views).*
|
||||||
|
9. **The hinge is for effects, regions are for layout.** `onHingeChange` (SwiftUI) and `UIHingeInteraction` (UIKit) report the status (closed, partly open, fully open) and the angle continuously, and the hinge is optional (`context.hinge` is nil on a device without one). Use them for live interactions and effects; use the arrangement and reserved-region APIs for layout. A state driven by the hinge is reset when the hinge is not in the state that drives it. *Basis: documented (Tech Talk 111464).*
|
||||||
|
10. **Bars: use the system's bars, say what each item is, and let the system place it.** Toolbars, tab bars and navigation controls move to the side on the outer display and in landscape on the inner display, and stay horizontal on the inner display in portrait. Only bars the navigation containers provide are considered: a toolbar on a `NavigationStack` or `NavigationSplitView` (SwiftUI), or items on a view controller in a `UINavigationController` or `UITabBarController` (UIKit); a hand-built `UIToolbar`, `UINavigationBar` or `UITabBar` is left horizontal. Every item has a symbol and a title (the symbol is used vertically, the title in the overflow menu); an item with only a title or a custom view stays horizontal. Primary navigation (Back, Close) is first and prominent actions (Done) next: `ToolbarItemPlacement.cancellationAction` and `.topBarPinnedTrailing`, or `leadingItemGroups` and `pinnedTrailingGroup`. `axisBehavior` (27.1; its `.horizontalOnly` and `.verticalPreferred` are iOS only), `visibilityPriority` (iOS 27.0, macOS 26.1), `ToolbarOverflowMenu` (iOS 27.0, not macOS) or `additionalOverflowItems`, and `toolbarVerticalCompressionBehavior` (27.1) decide the rest; a custom view reads `toolbarVerticalEdge` (SwiftUI) or `verticalBarEdge` (UIKit) to know that its bar is vertical, and fits a fixed width. In general the default placement is not overridden (`toolbarVerticalBehavior(.disabled)` or `preferredVerticalBarBehavior` is for a single-page layout that needs the width or a sheet with one button), and no spacing is added by hand. In a split view only the detail column's bars go vertical; on the outer display a sheet's bar is vertical, and on the inner a centered sheet's stays horizontal. *Basis: documented (Preparing your app; Tech Talk 111462; HIG: Vertical controls).*
|
||||||
|
11. **Safe areas are not symmetric.** Controls sit on one edge, and in Split View each app's controls are on its outer edge, so the left inset and the right inset differ. Never compute `bounds.width - insets.left * 2`; use `bounds.inset(by: safeAreaInsets)`. Foreground content that is interactive stays inside the safe area; a full-bleed background may extend past it. Layout margins are asymmetric too. Use `ConcentricRectangle` (SwiftUI) or `UICornerConfiguration` (UIKit) to match the corners of the screen. *Basis: documented (Tech Talk 111461; HIG: Dynamic layouts).*
|
||||||
|
12. **Scenes: a second window is a request that can fail.** iPhone Duo is the first iPhone to support more than one scene of an app. New windows cannot be created from the outer display, only the inner, so a request for one is handled as something that can fail, and `UIWindowSceneActivationAction` hides itself when it is not available. Scene accessories (content on another display) are enabled by default, can be toggled, and their availability changes at run time: observe it (`onAvailabilityChange`) and disable what depends on it. The camera capture accessory (`CameraCaptureAccessory`, registered with `sceneAccessory` on the camera's view) puts content on the outer display while the app is full screen on the inner display with an active camera session. *Basis: documented (Tech Talk 111464).*
|
||||||
|
13. **A camera app follows the camera by the direction it faces.** When the device is opened, closed or rotated, the display and the camera in use may change and a camera may face the opposite way. Choose a camera by the direction it faces, not by its place, and handle the change. *Basis: documented (Preparing your app: Improve your app's camera handling; Choosing a camera by the direction it faces).*
|
||||||
|
14. **Games fill the screen in every pose.** A game may lock to portrait or landscape (`UIRequiresFullScreen` is honored on iPhone in resizable environments from iOS 27) and still fills the screen as the pose changes; it keeps the text and control sizes as steady as it can, prefers changing the aspect ratio to letterboxing, and puts artwork in the padding where letterboxing cannot be avoided. *Basis: documented (HIG: Best practices; WWDC26 278).*
|
||||||
|
15. **Test every pose, and the changes between them.** Use the iPhone Duo simulator in Device Hub (Xcode 27.1), whose controls open, close, rotate and fold the device; the resizable iOS simulator in Device Hub and iPhone Mirroring on macOS 27 for the extremes of size in both directions; and the App Resizability skill of Xcode 27.1 (its output is reviewed: Apple says it misses some issues). Then do what the pages do not: a UI test that changes the pose while the app is in a state (a selection, a scrolled list, a typed field, an unsaved edit, a sheet open, a page chosen) and requires that the state is the same after the change and back, that no control is lost, and that every control is still reachable; run in each pose and with the largest text size (`accessibility.md`). *Basis: documented for the tools (Preparing your app; Tech Talk 111461); design for the transition test.*
|
||||||
|
16. **The newer APIs are reached through one layer that detects the SDK and the OS, and gives each a fallback.** A call site does not carry an `if #available`: it calls a forward (`view.forwards.glassEffect(...)`, `proxy.forwards.reservedRegions(kind:)`), which holds the availability check, the fallback and the mapping in one tested function, and is removed from a call site when the deployment target reaches the OS. Two checks are needed and are not the same: whether the **SDK** being built against has the symbols (`#if canImport(SwiftUICore, _version: ...)`, because a macOS SDK that lacks them fails to compile an `#available`-guarded call) and whether the **OS** being run on has them (`#available`). The threshold is kept honest by a script that, for each installed SDK, checks that the symbols are present exactly when the version is at least the threshold. A fallback is the closest honest behavior (an empty list of regions, the flat-display layout of an arrangement, a bordered button for a glass one), never a pretence of the new API. The shared implementation is `Forwards` (`codeberg.org/Mihaela/Forwards`): `ForwardsCore` (pure values and the arrangement layout), `ForwardsSwiftUI` (iOS and macOS), `ForwardsUIKit` (iOS); the cross-framework AppKit and UIKit layer (`ui/appuikit.md`) delegates to it and answers with nothing on AppKit. Mac Catalyst is not a target. *Basis: observed (the macOS 27.0 SDK without the symbols, the module versions of each SDK, the members marked unavailable on macOS; the fallbacks tested on macOS, on an iOS 27.0 simulator and on the iPhone Duo simulator of iOS 27.1); design for the layering.*
|
||||||
|
17. **Do not rely on what Apple has not documented.** See the next section. An app that needs one of those behaviors observes it on the simulator and on a device, records it as observed with the OS build, and does not state it as a rule until Apple does. *Basis: design.*
|
||||||
|
|
||||||
|
## What Apple has not documented
|
||||||
|
|
||||||
|
On 2026-10-28, in the pages named above, read in full:
|
||||||
|
|
||||||
|
- **The transition itself.** The pages say content "transitions between displays" and that the app is resized, and describe the system's own effects (the wallpaper reacts to the hinge angle and zooms as the device opens; sheets, alerts, menus and toolbar buttons move away from the fold; a picture-in-picture video pins to the top and the app resizes with it). They give no duration, no curve, no intermediate frames, and no way to take part in the animation other than the layout APIs.
|
||||||
|
- **State across a fold.** No page says whether views, view controllers, scenes or `@State` survive, are recreated or are suspended when the device folds or unfolds, or what the person's state should be written to. The Human Interface Guidelines say to keep "the state of elements the same between displays"; they do not say how the system helps.
|
||||||
|
- **Scene lifecycle on a fold.** The pages name scene geometry updates and the scene lifecycle requirement, but not what a fold does to a scene (a new scene, the same scene, a new screen for it), nor `windowScene.screen` changes.
|
||||||
|
- **Detecting a pose change.** There is no named event for "the device was folded" other than a change of size class and trait, a change of scene geometry, a change of a reserved region, and the hinge API.
|
||||||
|
- **Sizes.** No page gives the points, the pixels or the scale of either display, or the exact aspect ratio (only "both displays share the same aspect ratio"), or the safe-area values.
|
||||||
|
- **State restoration**, **Split View on an iPhone versus an iPad**, and **the keys to opt in to multiple scenes** are not described.
|
||||||
|
|
||||||
|
These are the questions the transition test of rule 15 exists to answer, for the app, on the build in hand.
|
||||||
|
|
||||||
|
## What was observed on the simulator
|
||||||
|
|
||||||
|
On 2026-10-28, on the iPhone Duo simulator (iOS 27.1, Xcode 27.1), with a real app (a YAML and JSON editor with an outline, a source view and an inspector), poses driven from Device Hub (closed, open, half-folded, closed again, open again). These are observations of that build, not Apple's documentation (rule 17):
|
||||||
|
|
||||||
|
- **A vertical toolbar and a tab bar overlapped.** On the closed (outer) display, which is compact width, the system put a navigation toolbar down the side as a vertical bar; a `TabView` inside the same `NavigationStack` also drew its tab bar vertically; the two sat on the same coordinates, so the tab buttons were covered by the toolbar buttons. Setting `toolbarVerticalBehavior(.disabled)` on that screen (through the layer of rule 16) gave a horizontal tab bar at the bottom and a horizontal toolbar at the top with a More overflow. Found by dumping the accessibility tree, which gave the frames; a screenshot of a closed device does not show it.
|
||||||
|
- **A fold rebuilt the navigation and lost the screen.** Folding open to closed made the app swap its columns for pages, and the pages (a `NavigationStack`) were made again with an empty path: the person landed on the list of files with the document still open in the model. The stack now starts on the document when one is open. State above the swap (the open document, the search text, the chosen node) survived every change of pose in the run; state held in the swapped views (the chosen tab) did not.
|
||||||
|
- **Tools.** The pose can be set only from Device Hub, by clicking its pose buttons; `simctl`, XCUITest and AppleScript cannot. `simctl io screenshot` and the screenshot of a UI test are black unless the device is closed, in any other pose; a capture of the Device Hub window works in every pose. The first tap on a tab right after a pose change did not always switch the page; the test taps again and records how many taps it took.
|
||||||
|
|
||||||
|
## How to test it
|
||||||
|
|
||||||
|
- **Pure functions first.** The decision between arrangements (pages or columns, one pane or two) is a function of the size classes and the space, not of the device; test it over every combination of the two size classes and over widths from the narrowest to the widest, with equal results for equal inputs, and with no state in the function.
|
||||||
|
- **The state across the change.** In the simulator, put the app in a state, change the pose with the Device Hub controls, and read the state back: the selected row, the scroll offset, the text of a field and its caret, an unsaved edit, the open sheet, the chosen tab. Do it closed to open and open to closed, open to partly folded and back, rotated and back, and into and out of Split View. Record which of them survive on the build in hand (rule 17).
|
||||||
|
- **Every control, every pose.** Walk the accessibility tree in each pose and require that each control exists, is hittable, is not under the fold, and keeps its label (`accessibility.md`), and that the audit passes in each.
|
||||||
|
- **The sources of wrong size.** The acceptance check below finds the screen, idiom, orientation and symmetric-inset habits mechanically.
|
||||||
|
- **Availability.** Build for the oldest deployment target the app supports and require that the 27.1 APIs are behind `#available`.
|
||||||
|
- **The text size.** Run each pose at the largest accessibility text size, where the arrangement changes (`accessibility.md`, rule 14).
|
||||||
|
|
||||||
|
## Acceptance check
|
||||||
|
|
||||||
|
Run from the repository root. `SOURCES` are the Swift sources of the app and its UI packages. Every command must print nothing, except where it says otherwise:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 1. no main screen
|
||||||
|
grep -rnE 'UIScreen\.main' "$SOURCES" --include='*.swift'
|
||||||
|
|
||||||
|
# 2. no layout by idiom or by a device size
|
||||||
|
grep -rnE 'userInterfaceIdiom|UIDevice\.current\.userInterfaceIdiom|bounds\.(width|height) *== *[0-9]' "$SOURCES" --include='*.swift'
|
||||||
|
|
||||||
|
# 3. no layout by orientation
|
||||||
|
grep -rnE 'UIDevice\.current\.orientation|statusBarOrientation|\.interfaceOrientation|isLandscape|isPortrait' "$SOURCES" --include='*.swift'
|
||||||
|
|
||||||
|
# 4. no window made from the main screen
|
||||||
|
grep -rnE 'UIWindow\(frame:' "$SOURCES" --include='*.swift'
|
||||||
|
|
||||||
|
# 5. no symmetric inset arithmetic
|
||||||
|
grep -rnE 'safeAreaInsets\.(left|right|leading|trailing) *\* *2' "$SOURCES" --include='*.swift'
|
||||||
|
|
||||||
|
# 6. a regular width is not taken for an iPad
|
||||||
|
grep -rnE 'horizontalSizeClass *== *\.regular.*(iPad|pad)|(iPad|pad).*horizontalSizeClass *== *\.regular' "$SOURCES" --include='*.swift'
|
||||||
|
|
||||||
|
# 7. a hand-built bar is not a bar the system places
|
||||||
|
grep -rnE '\bUIToolbar\(|\bUINavigationBar\(|\bUITabBar\(' "$SOURCES" --include='*.swift'
|
||||||
|
|
||||||
|
# 8. the 27.1 APIs are used only in a file that gates them (this must print nothing)
|
||||||
|
for f in $(grep -rlE 'ArrangementView|UIArrangementViewController|reservedRegions|onHingeChange|UIHingeInteraction' "$SOURCES" --include='*.swift'); do grep -q 'iOS 27\.1' "$f" || echo "$f: not gated"; done
|
||||||
|
|
||||||
|
# 9. the scene lifecycle is adopted (this must print the delegate)
|
||||||
|
grep -rnE 'UIWindowSceneDelegate|UISceneDelegate|WindowGroup|@main' "$SOURCES" --include='*.swift' | head -3
|
||||||
|
```
|
||||||
|
|
||||||
|
Promote commands 1 to 8 to a build-failing script with a self-test (a fixture that breaks each must fail it; a fixture that obeys all must pass it), and the tests above to the project's test suite. The check passes when the commands print as stated, the tests pass in each pose, and the transition test has been run on the simulator and its result recorded as observed with the build. Until that run, say "built with the 27.1 SDK, laid out by size classes, not yet tested across a fold", never "supports iPhone Duo". Rule 17 stands: the section "What Apple has not documented" is the open frontier, and each item leaves it only when Apple documents it or the app has observed it.
|
||||||
|
|
||||||
|
Cross-references: `accessibility.md` (the text size and the audit in each pose; the arrangement at accessibility sizes), `ui/swiftui-views.md` and `ui/uikit-views.md` (the views the rules apply to), `ui/three-renderers.md` (the same layout decision on every surface), `cross-platform.md` (the platform conditions around 27.1 APIs), `testing.md` and `core/testing-discipline.md` (the tests), `core/oracle-first.md` (the decision function is the answer key the surfaces are held to), `core/proof-discipline.md` (documented, observed and not established are kept apart), `core/no-shortcuts-first-principles.md` (a behavior that is undocumented is observed, not assumed).
|
||||||
9
third_party/rules-swift/linux-server.md
vendored
9
third_party/rules-swift/linux-server.md
vendored
@ -4,7 +4,7 @@ Operational rules for Tiledown code that runs on Linux in production: HTTP clien
|
|||||||
|
|
||||||
Load on demand. Triggers: Linux, server, Vapor, Hummingbird, AsyncHTTPClient, swift-log, swift-crypto, FluentSQLite, FluentPostgres, NIO, SIGTERM, graceful shutdown, Docker, `swift:6.2-jammy`, `swift:6.2-noble`, Bundle.module, FoundationNetworking on a server, isatty, BoringSSL.
|
Load on demand. Triggers: Linux, server, Vapor, Hummingbird, AsyncHTTPClient, swift-log, swift-crypto, FluentSQLite, FluentPostgres, NIO, SIGTERM, graceful shutdown, Docker, `swift:6.2-jammy`, `swift:6.2-noble`, Bundle.module, FoundationNetworking on a server, isatty, BoringSSL.
|
||||||
|
|
||||||
If your target runs on Linux in production (server, CLI binary, container workload), this file is the operational rule. The higher-level topology rule and the `#if canImport(...)` patterns live in `docs/rules/cross-platform.md`; this file is the server-specific operational layer.
|
If your target runs on Linux in production (server, CLI binary, container workload), this file is the operational rule. The higher-level topology rule and the `#if canImport(...)` patterns live in `cross-platform.md`; this file is the server-specific operational layer.
|
||||||
|
|
||||||
## When this rule applies
|
## When this rule applies
|
||||||
|
|
||||||
@ -291,7 +291,10 @@ import Testing
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Mixed XCTest / Swift Testing in the same target is fine; both runners coexist. See `concurrency.md` for the broader rule.
|
XCTest and Swift Testing can coexist in the same target during an incremental
|
||||||
|
migration, because both runners are supported. New non-UI tests use Swift Testing;
|
||||||
|
retain XCTest only when a required harness has no Swift Testing equivalent. See
|
||||||
|
`concurrency.md` for the broader rule.
|
||||||
|
|
||||||
References: ST-0009, ST-0016, ST-0017, ST-0022 (Swift Testing parity across Apple, Linux, Windows).
|
References: ST-0009, ST-0016, ST-0017, ST-0022 (Swift Testing parity across Apple, Linux, Windows).
|
||||||
|
|
||||||
@ -394,7 +397,7 @@ grep -rnE 'swift:6\.0-(jammy|noble|bookworm|alpine)' .github/workflows/
|
|||||||
|
|
||||||
## 13. Cross-references
|
## 13. Cross-references
|
||||||
|
|
||||||
- **`docs/rules/cross-platform.md`** is the higher-level topology rule and covers UI patterns + library patterns. This file is the server-specific operational layer; load both together for a server repo.
|
- **`cross-platform.md`** is the higher-level topology rule and covers UI patterns + library patterns. This file is the server-specific operational layer; load both together for a server repo.
|
||||||
- **`concurrency.md`** is platform-agnostic; Sendable + async/await behave identically on Apple and Linux.
|
- **`concurrency.md`** is platform-agnostic; Sendable + async/await behave identically on Apple and Linux.
|
||||||
|
|
||||||
## 14. Authoritative references
|
## 14. Authoritative references
|
||||||
|
|||||||
15
third_party/rules-swift/namespacing.md
vendored
15
third_party/rules-swift/namespacing.md
vendored
@ -83,7 +83,11 @@ extension Logging {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The namespace is still `enum Logging`. The protocol is a nested type. For cross-target seams, the protocol lives in a foundation-only `*Models` target (cross-package coupling via protocols, not concrete imports). The composition root wires the concrete conformer.
|
The namespace is still `enum Logging`. The protocol is a nested type. The one
|
||||||
|
exception is the foundation-only `SharedProtocols` target: its cross-target
|
||||||
|
protocols, supporting values, and null/preview conformers stay top-level, with the
|
||||||
|
module itself providing the namespace. The composition root wires live concrete
|
||||||
|
conformers elsewhere.
|
||||||
|
|
||||||
## Anti-patterns
|
## Anti-patterns
|
||||||
|
|
||||||
@ -242,3 +246,12 @@ Build and tests verify the safe cases: anything that was constructing the empty
|
|||||||
## Cross-references
|
## Cross-references
|
||||||
|
|
||||||
- [code-style.md](code-style.md) covers the broader namespacing discipline (folder mirroring, file-naming convention, anchor file placement, one-type-per-file, reserved names, namespace-vs-module collisions). Read this file first for the anchor-type choice, then code-style.md for the operational details.
|
- [code-style.md](code-style.md) covers the broader namespacing discipline (folder mirroring, file-naming convention, anchor file placement, one-type-per-file, reserved names, namespace-vs-module collisions). Read this file first for the anchor-type choice, then code-style.md for the operational details.
|
||||||
|
|
||||||
|
## Acceptance check
|
||||||
|
|
||||||
|
Run `scripts/check-namespacing.sh`; it MUST exit zero. Then inspect every namespace
|
||||||
|
anchor changed by the work: a stateless, uninhabited anchor is a caseless `enum`; a
|
||||||
|
`struct` carries value state; a `class` has reference identity; shared mutable state
|
||||||
|
is an `actor` or an injected collaborator. Grep for construction sites of converted
|
||||||
|
anchors and run the affected build and tests so an accidentally instantiable former
|
||||||
|
namespace cannot survive silently.
|
||||||
|
|||||||
24
third_party/rules-swift/package-architecture.md
vendored
24
third_party/rules-swift/package-architecture.md
vendored
@ -36,7 +36,7 @@ Declare dependencies explicitly in `Package.swift`:
|
|||||||
|
|
||||||
- List every dependency in the manifest.
|
- List every dependency in the manifest.
|
||||||
- No reliance on implicit or transitive dependencies.
|
- No reliance on implicit or transitive dependencies.
|
||||||
- Minimize cross-package dependencies.
|
- Minimize cross-target dependencies.
|
||||||
- Prefer unidirectional dependency flow.
|
- Prefer unidirectional dependency flow.
|
||||||
|
|
||||||
### Rule 3: Target granularity
|
### Rule 3: Target granularity
|
||||||
@ -62,15 +62,21 @@ targets around it rather than renaming it.
|
|||||||
|
|
||||||
### Rule 5: Layer architecture (unidirectional)
|
### Rule 5: Layer architecture (unidirectional)
|
||||||
|
|
||||||
Organize targets into layers with dependency flow strictly bottom to top. The exact number of layers varies per project; the constants are:
|
Organize targets into layers ordered from foundation at the bottom to front doors
|
||||||
|
at the top. Imports and manifest dependencies point only from a higher-level
|
||||||
|
consumer to a lower-level provider. The exact number of layers varies per project;
|
||||||
|
the constants are:
|
||||||
|
|
||||||
- **Foundation** (bottom): shared value types, models, primitive utilities, logging.
|
- **Foundation** (bottom): shared value types, models, primitive utilities, logging.
|
||||||
- **Infrastructure:** protocols, persistence, networking, file I/O, parsers, transports.
|
- **Infrastructure:** protocols, persistence, networking, file I/O, parsers, transports.
|
||||||
- **Domain:** services, business logic, per-feature or per-verb operation packages.
|
- **Domain:** services, business logic, per-feature or per-verb operation targets.
|
||||||
- **Presentation** (UI projects only): component packages, design-system packages, screens.
|
- **Presentation** (UI projects only): component targets, design-system targets, screens.
|
||||||
- **Front-door** (top): binary targets, apps, CLIs, preview hosts, test harnesses.
|
- **Front-door** (top): binary targets, apps, CLIs, preview hosts, test harnesses.
|
||||||
|
|
||||||
Every dependency edge points upward. No back-edges. The active layer instantiation lives in the repository's active `Package.swift`.
|
A lower layer MUST NOT import or declare a dependency on a higher layer; that is a
|
||||||
|
back-edge. Build order and dependent propagation naturally travel from the lower
|
||||||
|
provider upward, but the declared `depends on` relation points downward. The
|
||||||
|
active layer instantiation lives in the repository's active `Package.swift`.
|
||||||
|
|
||||||
## When to create a new target
|
## When to create a new target
|
||||||
|
|
||||||
@ -122,7 +128,7 @@ Before creating a new target:
|
|||||||
- [ ] Fits a single architectural layer
|
- [ ] Fits a single architectural layer
|
||||||
- [ ] Test target created alongside the source target
|
- [ ] Test target created alongside the source target
|
||||||
- [ ] Product registered in the `Package.swift` products array when the target is public or executable
|
- [ ] Product registered in the `Package.swift` products array when the target is public or executable
|
||||||
- [ ] Dependencies only flow upward
|
- [ ] Imports and declared dependencies point only to the same or a lower layer
|
||||||
|
|
||||||
Before adding to an existing target:
|
Before adding to an existing target:
|
||||||
|
|
||||||
@ -141,12 +147,12 @@ Before modifying `Package.swift`:
|
|||||||
- [ ] Trailing commas in all arrays
|
- [ ] Trailing commas in all arrays
|
||||||
- [ ] `.when(platforms:)` applied for platform-specific dependencies within targets
|
- [ ] `.when(platforms:)` applied for platform-specific dependencies within targets
|
||||||
|
|
||||||
If you add a font or resource package:
|
If you add a font or resource target:
|
||||||
|
|
||||||
- [ ] Used `.process()` for resources, never `.copy()`
|
- [ ] Used `.process()` for resources, never `.copy()`
|
||||||
- [ ] Accessed resources via `Bundle.module`, never `Bundle.main`
|
- [ ] Accessed resources via `Bundle.module`, never `Bundle.main`
|
||||||
- [ ] Used `#if canImport(UIKit)` / `#if canImport(AppKit)` for platform imports
|
- [ ] Used `#if canImport(UIKit)` / `#if canImport(AppKit)` for platform imports
|
||||||
- [ ] The package has zero dependencies (resources are a foundation-layer concern)
|
- [ ] The target has zero dependencies (resources are a foundation-layer concern)
|
||||||
|
|
||||||
## Example layout (illustrative, not normative)
|
## Example layout (illustrative, not normative)
|
||||||
|
|
||||||
@ -173,5 +179,5 @@ Sources/ # or Packages/Sources/ in a project monorepo
|
|||||||
|
|
||||||
- [package-structure.md](package-structure.md): repository layout and the `Package.swift` manifest shape
|
- [package-structure.md](package-structure.md): repository layout and the `Package.swift` manifest shape
|
||||||
- [package-import-contract.md](package-import-contract.md): what each target may import
|
- [package-import-contract.md](package-import-contract.md): what each target may import
|
||||||
- [shared-protocols.md](shared-protocols.md): the cross-target protocol-seam package
|
- [shared-protocols.md](shared-protocols.md): the cross-target protocol-seam target
|
||||||
- [dependency-injection.md](dependency-injection.md): the DI principles these layers serve
|
- [dependency-injection.md](dependency-injection.md): the DI principles these layers serve
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# Per-Package Import Contract
|
# Per-Target Import Contract
|
||||||
|
|
||||||
The rule for what each SPM target may `import`. Tiledown is a monorepo from day one with many targets in one package, and the engine (`TileKit`) and CLI (`tile-down`) are already two targets, so this contract applies now and governs every target added to the manifest.
|
The rule for what each SPM target may `import`. Tiledown is a monorepo from day one with many targets in one package, and the engine (`TileKit`) and CLI (`tile-down`) are already two targets, so this contract applies now and governs every target added to the manifest.
|
||||||
|
|
||||||
@ -33,22 +33,29 @@ Before adding an `import X` line to a producer target, ask:
|
|||||||
1. Is `X` external (Foundation, a system framework, ArgumentParser, Testing, a vetted third-party dependency)? Allowed.
|
1. Is `X` external (Foundation, a system framework, ArgumentParser, Testing, a vetted third-party dependency)? Allowed.
|
||||||
2. Is the target an `executableTarget` / composition root (for example the `tile-down` CLI)? Allowed; the composition root wires the universe.
|
2. Is the target an `executableTarget` / composition root (for example the `tile-down` CLI)? Allowed; the composition root wires the universe.
|
||||||
3. Is `X` a foundation-tier target that is foundation-only by construction (shared constants, value types, read-only diagnostics)? Allowed.
|
3. Is `X` a foundation-tier target that is foundation-only by construction (shared constants, value types, read-only diagnostics)? Allowed.
|
||||||
4. Is `X` a protocol-seam package (foundation-only by contract, carrying only protocols and value types)? Allowed.
|
4. Is `X` the `SharedProtocols` seam target (foundation-only by contract, carrying only protocols and value types)? Allowed.
|
||||||
5. Otherwise: **STOP.** Surface the situation; do not proceed. Importing another producer's concrete target is forbidden. Route the dependency through a protocol seam; the composition root supplies the concrete via injection.
|
5. Otherwise: **STOP.** Surface the situation; do not proceed. Importing another producer's concrete target is forbidden. Route the dependency through a protocol seam; the composition root supplies the concrete via injection.
|
||||||
|
|
||||||
If you proceed past the stop rule, you have coupled two producers, which is exactly the coupling this rule exists to prevent. Every package should keep full autonomy, every dependency injectable, so any package can be pulled out of the monorepo at any time.
|
If you proceed past the stop rule, you have coupled two producers, which is exactly
|
||||||
|
the coupling this rule exists to prevent. Every target keeps full autonomy and
|
||||||
|
every dependency is injectable, so the target can be lifted into a standalone
|
||||||
|
package at any time.
|
||||||
|
|
||||||
## The rules
|
## The rules
|
||||||
|
|
||||||
1. **External primitives are always allowed.** `Foundation`, `os`, `OSLog`, `Combine`, system frameworks, `ArgumentParser`, `Testing`, `XCTest`, and vetted third-party SPM products are ambient and never count as a violation.
|
1. **Declared external primitives are allowed when they build on the target's
|
||||||
2. **A producer target may import its own protocol-seam companion.** That is the foundation-only protocol and value-type seam the producer publishes.
|
supported platforms.** Foundation, system frameworks, and vetted third-party
|
||||||
3. **A producer target may import other protocol-seam targets.** They are foundation-only by construction; importing one carries no behavioural coupling.
|
products still appear in the target's explicit dependency/platform contract;
|
||||||
4. **A producer target may not import another producer (feature) target.** Cross-feature coupling goes through protocols defined in a seam package; the concrete is supplied at the composition root.
|
test frameworks belong in test targets, not production producers.
|
||||||
5. **A producer target may not depend on a concrete writer.** Consumers hold the protocol type from the seam package; the binary supplies the live implementation.
|
2. **A producer target may import `SharedProtocols`.** It is foundation-only by
|
||||||
6. **Read-only foundation infrastructure can be imported widely** (embedded resources, read-only diagnostics). Treat it like Foundation.
|
construction and carries the named cross-target contracts and their supporting
|
||||||
7. **Composition roots can import anything.** The CLI and any other app/binary wire the live concretes and pass them down.
|
value types.
|
||||||
8. **No singletons reachable from producer code.** No `static let shared = X()`, no static config accessors. Pass the value as a parameter. The composition root constructs once and threads it down.
|
3. **A producer target may not import another producer (feature) target.** Cross-feature coupling goes through protocols defined in `SharedProtocols`; the concrete is supplied at the composition root.
|
||||||
9. **A "this singleton is sanctioned" argument is not a get-out.** Even where a singleton would be technically acceptable, the project rule is strict dependency injection.
|
4. **A producer target may not depend on a concrete writer.** Consumers hold the protocol type from `SharedProtocols`; the binary supplies the live implementation.
|
||||||
|
5. **Read-only foundation infrastructure can be imported widely** (embedded resources, read-only diagnostics). Treat it like Foundation.
|
||||||
|
6. **Composition roots can import anything.** The CLI and any other app/binary wire the live concretes and pass them down.
|
||||||
|
7. **No singletons reachable from producer code.** No `static let shared = X()`, no static config accessors. Pass the value as a parameter. The composition root constructs once and threads it down.
|
||||||
|
8. **A "this singleton is sanctioned" argument is not a get-out.** Even where a singleton would be technically acceptable, the project rule is strict dependency injection.
|
||||||
|
|
||||||
## What this rule does not say
|
## What this rule does not say
|
||||||
|
|
||||||
@ -73,10 +80,13 @@ Two checks back the contract:
|
|||||||
- A source-import audit: grep `^import` lines per target and fail if a consumer imports a forbidden producer.
|
- A source-import audit: grep `^import` lines per target and fail if a consumer imports a forbidden producer.
|
||||||
- A portability check: copy a target plus its declared transitive deps into a temporary directory, generate a minimal `Package.swift`, and run `swift build`. Green means the target genuinely lifts standalone with only its declared deps. Sweep across every producer target in CI.
|
- A portability check: copy a target plus its declared transitive deps into a temporary directory, generate a minimal `Package.swift`, and run `swift build`. Green means the target genuinely lifts standalone with only its declared deps. Sweep across every producer target in CI.
|
||||||
|
|
||||||
## When auditing the packages
|
## When auditing the targets
|
||||||
|
|
||||||
1. List every target.
|
1. List every target.
|
||||||
2. For each, run `grep -h '^import ' Sources/<Target>/**/*.swift | sort -u` to see actual imports.
|
2. For each, run
|
||||||
|
`find Sources/<Target> -name '*.swift' -exec grep -h '^import ' {} + | sort -u`
|
||||||
|
(or use `Packages/Sources/<Target>` in a project monorepo) to see actual
|
||||||
|
imports.
|
||||||
3. Compare against the contract. Producer importing another producer is a violation. Producer importing a concrete writer is a violation. A singleton reachable inside a producer is a violation.
|
3. Compare against the contract. Producer importing another producer is a violation. Producer importing a concrete writer is a violation. A singleton reachable inside a producer is a violation.
|
||||||
4. Open an issue or PR for each violation, smallest first.
|
4. Open an issue or PR for each violation, smallest first.
|
||||||
|
|
||||||
@ -114,5 +124,5 @@ This does not ban closures as method parameters or property values; `onProgress:
|
|||||||
|
|
||||||
- [dependency-injection.md](dependency-injection.md): underlying DI principles
|
- [dependency-injection.md](dependency-injection.md): underlying DI principles
|
||||||
- [shared-protocols.md](shared-protocols.md): where the protocol seams live
|
- [shared-protocols.md](shared-protocols.md): where the protocol seams live
|
||||||
- [package-architecture.md](package-architecture.md): package layers and granularity
|
- [package-architecture.md](package-architecture.md): target layers and granularity
|
||||||
- [package-structure.md](package-structure.md): repository and manifest layout
|
- [package-structure.md](package-structure.md): repository and manifest layout
|
||||||
|
|||||||
6
third_party/rules-swift/package-structure.md
vendored
6
third_party/rules-swift/package-structure.md
vendored
@ -111,7 +111,7 @@ For any UI you add, do not use Interface Builder artifacts:
|
|||||||
|
|
||||||
Keep views and view controllers in package targets, not in app targets:
|
Keep views and view controllers in package targets, not in app targets:
|
||||||
|
|
||||||
- SwiftUI views in feature packages.
|
- SwiftUI views in feature targets.
|
||||||
- UIKit/AppKit views in a dedicated UI package.
|
- UIKit/AppKit views in a dedicated UI package.
|
||||||
- App targets contain ONLY entry points (`AppDelegate`, `SceneDelegate`, `@main`).
|
- App targets contain ONLY entry points (`AppDelegate`, `SceneDelegate`, `@main`).
|
||||||
|
|
||||||
@ -339,6 +339,6 @@ Before changing repo structure:
|
|||||||
|
|
||||||
## Related rules
|
## Related rules
|
||||||
|
|
||||||
- [package-architecture.md](package-architecture.md): single-responsibility packages, layers, and the when-to-create decision tree
|
- [package-architecture.md](package-architecture.md): single-responsibility targets, layers, and the when-to-create decision tree
|
||||||
- [package-import-contract.md](package-import-contract.md): what each target may import
|
- [package-import-contract.md](package-import-contract.md): what each target may import
|
||||||
- [shared-protocols.md](shared-protocols.md): the cross-target protocol-seam package
|
- [shared-protocols.md](shared-protocols.md): the cross-target protocol-seam target
|
||||||
|
|||||||
16
third_party/rules-swift/parsing-rules.md
vendored
16
third_party/rules-swift/parsing-rules.md
vendored
@ -13,9 +13,12 @@ Key files to study:
|
|||||||
|
|
||||||
## Core Principles
|
## Core Principles
|
||||||
|
|
||||||
### 1. Protocol-First Design
|
### 1. Protocol Before Shared Implementations
|
||||||
|
|
||||||
Define shared behavior in protocols before concrete types:
|
When two formats/providers share behavior, or when the parser contract crosses a
|
||||||
|
target boundary, define the shared contract before its concrete implementations.
|
||||||
|
Do not introduce a protocol around one same-target parser with no boundary or
|
||||||
|
second implementation; that is unearned abstraction.
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
/// All parsed items share these properties
|
/// All parsed items share these properties
|
||||||
@ -253,3 +256,12 @@ func decodeText(from data: Data) -> String? {
|
|||||||
- Create tightly-coupled parsers
|
- Create tightly-coupled parsers
|
||||||
- Skip the validation layer
|
- Skip the validation layer
|
||||||
- Use force unwrapping in parsers
|
- Use force unwrapping in parsers
|
||||||
|
|
||||||
|
## Acceptance check
|
||||||
|
|
||||||
|
A parser conforms when parsing and validation are separate entry points; malformed,
|
||||||
|
empty, boundary, alternate-encoding, and unknown-field fixtures exercise both
|
||||||
|
success and failure paths; no production parser uses force unwraps or silently
|
||||||
|
drops input; and the parser test suite passes with its case count cited. For a
|
||||||
|
bidirectional format, also run the corpus and awkward-value round trips required by
|
||||||
|
`core/round-trip-transformation.md`.
|
||||||
|
|||||||
24
third_party/rules-swift/rules-freshness.yml
vendored
Normal file
24
third_party/rules-swift/rules-freshness.yml
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Managed by rules-swift (scripts/sync-rules.sh). Verifies only that this repo's
|
||||||
|
# freshness marker matches the current shared-rules version. It does not claim
|
||||||
|
# semantic rule conformance; each consumer owns and runs its project-specific gates.
|
||||||
|
name: rules-freshness
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
jobs:
|
||||||
|
rules:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Shared-rules freshness marker
|
||||||
|
run: |
|
||||||
|
BASE=https://codeberg.org/Mihaela/rules-swift/raw/branch/main
|
||||||
|
canon=$(curl -fsSL "$BASE/VERSION" | tr -d '[:space:]')
|
||||||
|
mine=$(tr -d '[:space:]' < .rules-version 2>/dev/null || echo MISSING)
|
||||||
|
if [ "$mine" != "$canon" ]; then
|
||||||
|
echo "STALE: shared-rules baseline is v$canon, this repo tracks v$mine."
|
||||||
|
echo "Review the baseline diff, fold it into this repo's rules, then set .rules-version to $canon."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "rules baseline FRESH at v$canon (repo-specific rules preserved, not checked)"
|
||||||
39
third_party/rules-swift/scripts/check-changelog-touched.sh
vendored
Normal file
39
third_party/rules-swift/scripts/check-changelog-touched.sh
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Require CHANGELOG.md whenever the vendored rule baseline changes.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT=${RULES_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
if [ "${1:-}" = "--working-tree" ]; then
|
||||||
|
changed=$(
|
||||||
|
{
|
||||||
|
git diff HEAD --name-only
|
||||||
|
git ls-files --others --exclude-standard
|
||||||
|
} | sort -u
|
||||||
|
)
|
||||||
|
elif [ "${1:-}" = "" ]; then
|
||||||
|
changed=$(git diff --cached --name-only)
|
||||||
|
else
|
||||||
|
changed=$(git diff --name-only "$1" HEAD)
|
||||||
|
fi
|
||||||
|
|
||||||
|
rules=$(printf '%s\n' "$changed" \
|
||||||
|
| grep -E '(^|/)[^/]+\.md$|^core/|^exp/|^\.swiftformat$|^\.swiftlint\.yml$|^\.pre-commit-config\.yaml$|^rules-freshness\.yml$|^scripts/sync-rules\.sh$' \
|
||||||
|
| grep -vxE 'CHANGELOG\.md|CONTRIBUTING\.md|README\.md' \
|
||||||
|
|| true)
|
||||||
|
|
||||||
|
if [ -z "$rules" ]; then
|
||||||
|
echo "changelog gate: OK (no rule files changed)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if printf '%s\n' "$changed" | grep -qx 'CHANGELOG.md'; then
|
||||||
|
echo "changelog gate: OK (rule changes documented)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "changelog gate: FAILED" >&2
|
||||||
|
echo " These rule files changed without CHANGELOG.md:" >&2
|
||||||
|
printf ' %s\n' $rules >&2
|
||||||
|
exit 1
|
||||||
63
third_party/rules-swift/scripts/check-core-sync.sh
vendored
Normal file
63
third_party/rules-swift/scripts/check-core-sync.sh
vendored
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Require core/ to match the engineering-discipline source byte-for-byte.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT=${RULES_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}
|
||||||
|
TARGET=${CORE_TARGET_DIR:-$ROOT/core}
|
||||||
|
SOURCE=${CORE_SOURCE_DIR:-}
|
||||||
|
TMP=$(mktemp -d "${TMPDIR:-/tmp}/rules-swift-core-sync.XXXXXX")
|
||||||
|
trap 'rm -rf "$TMP"' EXIT HUP INT TERM
|
||||||
|
|
||||||
|
if [ -z "$SOURCE" ]; then
|
||||||
|
sibling=$(cd "$ROOT/.." 2>/dev/null && pwd)/rules-engineering-discipline
|
||||||
|
if [ -d "$sibling/.git" ]; then
|
||||||
|
SOURCE=$sibling
|
||||||
|
else
|
||||||
|
command -v git >/dev/null 2>&1 || {
|
||||||
|
echo "core sync gate: git is required to fetch the upstream source" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
git clone -q --depth 1 \
|
||||||
|
https://codeberg.org/Mihaela/rules-engineering-discipline.git \
|
||||||
|
"$TMP/source"
|
||||||
|
SOURCE=$TMP/source
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -d "$SOURCE" ] || { echo "core sync gate: source not found: $SOURCE" >&2; exit 1; }
|
||||||
|
[ -d "$TARGET" ] || { echo "core sync gate: target not found: $TARGET" >&2; exit 1; }
|
||||||
|
|
||||||
|
source_manifest=$TMP/source-files
|
||||||
|
target_manifest=$TMP/target-files
|
||||||
|
: > "$source_manifest"
|
||||||
|
: > "$target_manifest"
|
||||||
|
|
||||||
|
for file in "$SOURCE"/*.md "$SOURCE"/LICENSE; do
|
||||||
|
[ -f "$file" ] || continue
|
||||||
|
name=${file##*/}
|
||||||
|
case "$name" in CHANGELOG.md|CONTRIBUTING.md) continue ;; esac
|
||||||
|
echo "$name" >> "$source_manifest"
|
||||||
|
done
|
||||||
|
for file in "$TARGET"/*.md "$TARGET"/LICENSE; do
|
||||||
|
[ -f "$file" ] || continue
|
||||||
|
echo "${file##*/}" >> "$target_manifest"
|
||||||
|
done
|
||||||
|
sort -o "$source_manifest" "$source_manifest"
|
||||||
|
sort -o "$target_manifest" "$target_manifest"
|
||||||
|
|
||||||
|
if ! diff -u "$source_manifest" "$target_manifest"; then
|
||||||
|
echo "CORE SYNC FAIL (vendored file set differs from upstream)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
fail=0
|
||||||
|
while IFS= read -r name; do
|
||||||
|
if ! cmp -s "$SOURCE/$name" "$TARGET/$name"; then
|
||||||
|
echo "CORE SYNC FAIL: $name differs" >&2
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
done < "$source_manifest"
|
||||||
|
|
||||||
|
[ "$fail" = 0 ] || exit 1
|
||||||
|
count=$(wc -l < "$source_manifest" | tr -d ' ')
|
||||||
|
echo "core sync gate clean ($count vendored files)"
|
||||||
68
third_party/rules-swift/scripts/check-file-names.sh
vendored
Normal file
68
third_party/rules-swift/scripts/check-file-names.sh
vendored
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Enforce the document/asset naming contract from core/file-naming.md.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
# Force the C locale for every glob/case bracket expression below. Under a
|
||||||
|
# UTF-8 locale, /bin/sh on macOS (Apple's frozen bash 3.2) collates [A-Z]
|
||||||
|
# against locale rules rather than strict ASCII order, so *[A-Z]*) can match
|
||||||
|
# an all-lowercase name like "namespacing.md". The C locale is the only one
|
||||||
|
# where [A-Z] means exactly the 26 uppercase ASCII letters.
|
||||||
|
export LC_ALL=C
|
||||||
|
|
||||||
|
ROOT=${RULES_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}
|
||||||
|
RESULT=$(mktemp "${TMPDIR:-/tmp}/rules-swift-file-names.XXXXXX")
|
||||||
|
trap 'rm -f "$RESULT"' EXIT HUP INT TERM
|
||||||
|
|
||||||
|
find "$ROOT" -mindepth 1 \
|
||||||
|
-not -path '*/.git' -not -path '*/.git/*' \
|
||||||
|
-not -path '*/.build' -not -path '*/.build/*' \
|
||||||
|
-print | while IFS= read -r path; do
|
||||||
|
relative=${path#"$ROOT"/}
|
||||||
|
name=${path##*/}
|
||||||
|
|
||||||
|
case "$relative" in
|
||||||
|
Sources/*|*/Sources/*|Tests/*|*/Tests/*|src/*|*/src/*)
|
||||||
|
continue
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$name" in
|
||||||
|
*' '*|*'('*|*')'*|*'['*|*']'*|*'&'*|*.)
|
||||||
|
echo "$relative: invalid separator or special character" >> "$RESULT"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if LC_ALL=C printf '%s' "$name" | grep -q '[^ -~]'; then
|
||||||
|
echo "$relative: non-ASCII name" >> "$RESULT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$relative" in
|
||||||
|
.github|.github/*)
|
||||||
|
continue
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
case "$name" in
|
||||||
|
README*|LICENSE|CHANGELOG*|CONTRIBUTING*|CODEOWNERS|NOTICE|AUTHORS|VERSION|Package.swift|Package.resolved|Package@*.swift|Dockerfile|Makefile|Gemfile|Rakefile|Podfile|Cartfile|Fastfile)
|
||||||
|
continue
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$name" in
|
||||||
|
*[A-Z]*) echo "$relative: uppercase name" >> "$RESULT" ;;
|
||||||
|
esac
|
||||||
|
case "$name" in
|
||||||
|
*_*) echo "$relative: underscore separator" >> "$RESULT" ;;
|
||||||
|
esac
|
||||||
|
case "$name" in
|
||||||
|
*.heic|*.HEIC) echo "$relative: convert HEIC to JPEG" >> "$RESULT" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
findings=$(cat "$RESULT")
|
||||||
|
if [ -n "$findings" ]; then
|
||||||
|
echo "FILE NAME FAIL:" >&2
|
||||||
|
echo "$findings" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "file-name gate clean"
|
||||||
134
third_party/rules-swift/scripts/check-issue-body-staleness.sh
vendored
Normal file
134
third_party/rules-swift/scripts/check-issue-body-staleness.sh
vendored
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Audit open Codeberg issues for the repository's tracker-discipline invariants.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT=$(cd "$(dirname "$0")/.." && pwd)
|
||||||
|
API=${ISSUES_API:-https://codeberg.org/api/v1/repos/Mihaela/rules-swift/issues?state=open&limit=50}
|
||||||
|
LABELS_API=${LABELS_API:-https://codeberg.org/api/v1/repos/Mihaela/rules-swift/labels?limit=100}
|
||||||
|
TMP=
|
||||||
|
LABEL_TMP=
|
||||||
|
PHANTOM=$(mktemp "${TMPDIR:-/tmp}/rules-swift-phantom.XXXXXX")
|
||||||
|
trap 'rm -f "$TMP" "$LABEL_TMP" "$PHANTOM"' EXIT HUP INT TERM
|
||||||
|
|
||||||
|
if ! command -v jq >/dev/null 2>&1; then
|
||||||
|
echo "issue-body gate: jq is required" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if { [ -z "${ISSUES_JSON:-}" ] || [ -z "${LABELS_JSON:-}" ]; } \
|
||||||
|
&& ! command -v curl >/dev/null 2>&1; then
|
||||||
|
echo "issue-body gate: curl is required for live tracker data" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "${ISSUES_JSON:-}" ]; then
|
||||||
|
data=$ISSUES_JSON
|
||||||
|
else
|
||||||
|
TMP=$(mktemp "${TMPDIR:-/tmp}/rules-swift-issues.XXXXXX")
|
||||||
|
curl -fsSL "$API" > "$TMP"
|
||||||
|
data=$TMP
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "${LABELS_JSON:-}" ]; then
|
||||||
|
labels=$LABELS_JSON
|
||||||
|
else
|
||||||
|
LABEL_TMP=$(mktemp "${TMPDIR:-/tmp}/rules-swift-labels.XXXXXX")
|
||||||
|
curl -fsSL "$LABELS_API" > "$LABEL_TMP"
|
||||||
|
labels=$LABEL_TMP
|
||||||
|
fi
|
||||||
|
|
||||||
|
fail=0
|
||||||
|
count=$(jq 'length' "$data")
|
||||||
|
label_count=$(jq 'length' "$labels")
|
||||||
|
|
||||||
|
label_drift=$(jq -nr \
|
||||||
|
--slurpfile expected "$ROOT/.github/labels.json" \
|
||||||
|
--slurpfile actual "$labels" '
|
||||||
|
$expected[0][] as $want
|
||||||
|
| ($actual[0] | map(select(.name == $want.name)) | first) as $got
|
||||||
|
| select($got == null or (($got.color | ascii_downcase) != $want.color))
|
||||||
|
| if $got == null then
|
||||||
|
"missing: \($want.name)"
|
||||||
|
else
|
||||||
|
"wrong color: \($want.name) expected=\($want.color) actual=\($got.color)"
|
||||||
|
end
|
||||||
|
')
|
||||||
|
if [ -n "$label_drift" ]; then
|
||||||
|
echo "ISSUE BODY FAIL (repository label drift):" >&2
|
||||||
|
echo "$label_drift" >&2
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
missing_status=$(jq -r '
|
||||||
|
.[]
|
||||||
|
| (.body // "") as $body
|
||||||
|
| ($body | [scan("(?m)^##+ .+$")][0] // "") as $first_heading
|
||||||
|
| select((($first_heading | test("^##+ Status")) and
|
||||||
|
($body | test("(?m)^##+ Status.*\\n+20[0-9]{2}-[0-9]{2}-[0-9]{2}:"))) | not)
|
||||||
|
| .number
|
||||||
|
' "$data")
|
||||||
|
if [ -n "$missing_status" ]; then
|
||||||
|
echo "ISSUE BODY FAIL (missing dated Status block): $missing_status" >&2
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
line_anchors=$(jq -r '
|
||||||
|
.[]
|
||||||
|
| select((.body // "") | test("#[Ll][0-9]+|[A-Za-z0-9._/-]+:[0-9]+"))
|
||||||
|
| .number
|
||||||
|
' "$data")
|
||||||
|
if [ -n "$line_anchors" ]; then
|
||||||
|
echo "ISSUE BODY FAIL (line-number anchor): $line_anchors" >&2
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
stale_refs=$(jq -r '
|
||||||
|
. as $issues
|
||||||
|
| [$issues[].number] as $open
|
||||||
|
| $issues[] as $issue
|
||||||
|
| (($issue.body // "")
|
||||||
|
| scan("(?i)(?:blocked on|pending in|depends on|after|gated on|awaits|waiting on)[^\\n#]*#[0-9]+")
|
||||||
|
| capture("#(?<number>[0-9]+)").number
|
||||||
|
| tonumber) as $reference
|
||||||
|
| select(($open | index($reference)) == null)
|
||||||
|
| "\($issue.number) -> #\($reference)"
|
||||||
|
' "$data")
|
||||||
|
if [ -n "$stale_refs" ]; then
|
||||||
|
echo "ISSUE BODY FAIL (blocker reference is not open):" >&2
|
||||||
|
echo "$stale_refs" >&2
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
priority_drift=$(jq -r '
|
||||||
|
.[]
|
||||||
|
| ((.body // "") | test("(?mi)^##+ Priority[ \\t]*\\r?\\n+high[ \\t]*$")) as $high
|
||||||
|
| ([.labels[]?.name] | index("priority: high") != null) as $labelled
|
||||||
|
| select($high != $labelled)
|
||||||
|
| .number
|
||||||
|
' "$data")
|
||||||
|
if [ -n "$priority_drift" ]; then
|
||||||
|
echo "ISSUE BODY FAIL (priority body/label drift): $priority_drift" >&2
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
jq -r '.[] as $issue | (($issue.body // "") | scan("`[^`]+`") | gsub("^`|`$"; "")) as $path | [$issue.number, $path] | @tsv' "$data" \
|
||||||
|
| while IFS="$(printf '\t')" read -r number path; do
|
||||||
|
case "$path" in
|
||||||
|
*' '*|*'<'*|*'>'*|*'('*|*')'*|http:*|https:*|'' ) continue ;;
|
||||||
|
esac
|
||||||
|
case "$path" in
|
||||||
|
*/*|*.md|*.swift|*.yml|*.yaml|*.json|*.sh|*.txt|*.toml|*.mmd|*.png|*.jpg|*.jpeg|*.pdf|LICENSE|VERSION)
|
||||||
|
[ -e "$ROOT/$path" ] || printf '%s\t%s\n' "$number" "$path"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done > "$PHANTOM"
|
||||||
|
|
||||||
|
phantoms=$(cat "$PHANTOM")
|
||||||
|
if [ -n "$phantoms" ]; then
|
||||||
|
echo "ISSUE BODY FAIL (phantom backtick path):" >&2
|
||||||
|
echo "$phantoms" >&2
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ "$fail" = 0 ] || exit 1
|
||||||
|
echo "issue-body gate clean ($count open items, $label_count labels)"
|
||||||
50
third_party/rules-swift/scripts/check-links.sh
vendored
Normal file
50
third_party/rules-swift/scripts/check-links.sh
vendored
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Validate repository-relative Markdown links that name a path or file.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT=${RULES_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}
|
||||||
|
|
||||||
|
if ! command -v perl >/dev/null 2>&1; then
|
||||||
|
echo "link gate: perl is required" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
find "$ROOT" -type f -name '*.md' -not -path '*/.git/*' -not -path '*/.build/*' -print0 \
|
||||||
|
| xargs -0 perl -MFile::Basename=dirname -MFile::Spec -e '
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
my @broken;
|
||||||
|
|
||||||
|
sub check_target {
|
||||||
|
my ($file, $target) = @_;
|
||||||
|
$target =~ s/\s+"[^"]*"$//;
|
||||||
|
$target =~ s/^<|>$//g;
|
||||||
|
return if $target eq q{} || $target =~ m{^(?:#|/|[A-Za-z][A-Za-z0-9+.-]*:)};
|
||||||
|
$target =~ s/[#?].*$//;
|
||||||
|
$target =~ s/%20/ /g;
|
||||||
|
my $path = File::Spec->rel2abs($target, dirname($file));
|
||||||
|
push @broken, "$file -> $target" unless -e $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
for my $file (@ARGV) {
|
||||||
|
open my $handle, "<", $file or die "$file: $!";
|
||||||
|
local $/;
|
||||||
|
my $text = <$handle>;
|
||||||
|
# Examples are not navigable links. Remove fenced and inline code before
|
||||||
|
# interpreting Markdown link syntax.
|
||||||
|
$text =~ s{^[ \t]*(```+|~~~+).*?^[ \t]*\1[ \t]*$}{}gms;
|
||||||
|
$text =~ s{(`+)[^\n]*?\1}{}g;
|
||||||
|
while ($text =~ /\[[^\]]*\]\(([^)]+)\)/g) {
|
||||||
|
check_target($file, $1);
|
||||||
|
}
|
||||||
|
while ($text =~ /^\s{0,3}\[[^\]]+\]:\s*(\S+)/gm) {
|
||||||
|
check_target($file, $1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (@broken) {
|
||||||
|
print "LINK FAIL (missing repository-relative targets):\n", join("\n", @broken), "\n";
|
||||||
|
exit 1;
|
||||||
|
}
|
||||||
|
'
|
||||||
|
|
||||||
|
echo "link gate clean"
|
||||||
@ -6,7 +6,7 @@
|
|||||||
# mark helper types private/fileprivate so they drop out, or split the file.
|
# mark helper types private/fileprivate so they drop out, or split the file.
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
ROOT=$(cd "$(dirname "$0")/.." && pwd)
|
ROOT=${RULES_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}
|
||||||
SCAN="$ROOT/Packages/Sources"
|
SCAN="$ROOT/Packages/Sources"
|
||||||
[ -d "$SCAN" ] || SCAN="$ROOT"
|
[ -d "$SCAN" ] || SCAN="$ROOT"
|
||||||
|
|
||||||
|
|||||||
18
third_party/rules-swift/scripts/check-repo.sh
vendored
Normal file
18
third_party/rules-swift/scripts/check-repo.sh
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Complete local verification for this documentation-and-shell repository.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT=$(cd "$(dirname "$0")/.." && pwd)
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
scripts/leak-gate.sh
|
||||||
|
scripts/check-namespacing.sh
|
||||||
|
scripts/check-file-names.sh
|
||||||
|
scripts/check-links.sh
|
||||||
|
scripts/check-rule-structure.sh
|
||||||
|
scripts/check-core-sync.sh
|
||||||
|
scripts/test-gates.sh
|
||||||
|
scripts/check-changelog-touched.sh "${1:---working-tree}"
|
||||||
|
scripts/check-version-bumped.sh "${1:---working-tree}"
|
||||||
|
|
||||||
|
echo "repository gate clean"
|
||||||
35
third_party/rules-swift/scripts/check-rule-structure.sh
vendored
Normal file
35
third_party/rules-swift/scripts/check-rule-structure.sh
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Enforce the core/rules.md state -> apply -> prove spine for binding rule files.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT=${RULES_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}
|
||||||
|
fail=0
|
||||||
|
RESULT=$(mktemp "${TMPDIR:-/tmp}/rules-swift-structure.XXXXXX")
|
||||||
|
trap 'rm -f "$RESULT"' EXIT HUP INT TERM
|
||||||
|
|
||||||
|
find "$ROOT" -type f -name '*.md' \
|
||||||
|
-not -path '*/.git/*' \
|
||||||
|
-not -path '*/.build/*' \
|
||||||
|
-not -path '*/.github/*' \
|
||||||
|
-print | while IFS= read -r file; do
|
||||||
|
relative=${file#"$ROOT"/}
|
||||||
|
case "$relative" in
|
||||||
|
README.md|CHANGELOG.md|CONTRIBUTING.md|conventions.md|core/README.md|exp/README.md)
|
||||||
|
continue
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if ! grep -qiE '^(#{1,4} (Acceptance check|Verification|.*checklist|How to self-audit|Mechanical enforcement|CI guard|CI enforcement|CI requirements|Audit)|Acceptance check:)' "$file"; then
|
||||||
|
echo "$relative"
|
||||||
|
fi
|
||||||
|
done > "$RESULT"
|
||||||
|
|
||||||
|
missing=$(cat "$RESULT")
|
||||||
|
if [ -n "$missing" ]; then
|
||||||
|
echo "RULE STRUCTURE FAIL (no acceptance or verification section):" >&2
|
||||||
|
echo "$missing" >&2
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ "$fail" = 0 ] || exit 1
|
||||||
|
echo "rule structure gate clean"
|
||||||
51
third_party/rules-swift/scripts/check-version-bumped.sh
vendored
Normal file
51
third_party/rules-swift/scripts/check-version-bumped.sh
vendored
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# VERSION-bump gate for the rules repo.
|
||||||
|
#
|
||||||
|
# If any rule-defining file changes, VERSION must change in the same commit, so
|
||||||
|
# repos that vendor these rules can tell they are stale by comparing their
|
||||||
|
# recorded VERSION against this one. VERSION is the single freshness marker; bump
|
||||||
|
# it every time you change a rule. See VERSION and formatting-and-linting.md.
|
||||||
|
#
|
||||||
|
# Runs on every commit (.githooks/pre-commit, over STAGED changes) and in CI
|
||||||
|
# (.github/workflows/leak-gate.yml, over the diff against a base ref passed as $1).
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT=${RULES_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
if [ "${1:-}" = "--working-tree" ]; then
|
||||||
|
changed=$(
|
||||||
|
{
|
||||||
|
git diff HEAD --name-only
|
||||||
|
git ls-files --others --exclude-standard
|
||||||
|
} | sort -u
|
||||||
|
)
|
||||||
|
elif [ "${1:-}" = "" ]; then
|
||||||
|
changed=$(git diff --cached --name-only) # local pre-commit: staged files
|
||||||
|
else
|
||||||
|
changed=$(git diff --name-only "$1" HEAD) # CI: base..HEAD
|
||||||
|
fi
|
||||||
|
|
||||||
|
# A "rules change" is what downstream repos adopt: enforced configs, rule docs,
|
||||||
|
# and the managed freshness/sync contract. The repo's other CI machinery,
|
||||||
|
# .githooks/, .github/, and bookkeeping files are not downstream artifacts.
|
||||||
|
rules=$(printf '%s\n' "$changed" \
|
||||||
|
| grep -E '(^|/)[^/]+\.md$|^core/|^exp/|^\.swiftformat$|^\.swiftlint\.yml$|^\.pre-commit-config\.yaml$|^rules-freshness\.yml$|^scripts/sync-rules\.sh$' \
|
||||||
|
| grep -vxE 'CHANGELOG\.md|CONTRIBUTING\.md|README\.md' \
|
||||||
|
|| true)
|
||||||
|
|
||||||
|
if [ -z "$rules" ]; then
|
||||||
|
echo "version-bump gate: OK (no rule files changed)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if printf '%s\n' "$changed" | grep -qxE 'VERSION'; then
|
||||||
|
echo "version-bump gate: OK (rules changed, VERSION -> $(cat VERSION))"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "version-bump gate: FAILED" >&2
|
||||||
|
echo " These rule files changed but VERSION ($(cat VERSION)) was not bumped:" >&2
|
||||||
|
printf ' %s\n' $rules >&2
|
||||||
|
echo " Edit VERSION in this same change so vendoring repos detect the update." >&2
|
||||||
|
exit 1
|
||||||
33
third_party/rules-swift/scripts/leak-gate.sh
vendored
33
third_party/rules-swift/scripts/leak-gate.sh
vendored
@ -7,22 +7,39 @@
|
|||||||
# names here would itself be the leak.
|
# names here would itself be the leak.
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
ROOT=$(cd "$(dirname "$0")/.." && pwd)
|
ROOT=${RULES_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}
|
||||||
fail=0
|
fail=0
|
||||||
|
|
||||||
# AI/tool vendor names are allowed ONLY in the attribution rules that discuss them.
|
# AI/tool vendor names are allowed ONLY in the attribution rules that discuss them,
|
||||||
|
# and in the agent charter files at the repository ROOT (CLAUDE.md / AGENTS.md): there
|
||||||
|
# the filename is the tool's addressing interface, function, not attribution. Root
|
||||||
|
# only; the same name in any nested file still fails.
|
||||||
VEND='\bclaude\b|anthropic|\bopenai\b|chatgpt|\bcopilot\b|\bgemini\b|antigravity'
|
VEND='\bclaude\b|anthropic|\bopenai\b|chatgpt|\bcopilot\b|\bgemini\b|antigravity'
|
||||||
v=$(grep -rniE "$VEND" "$ROOT" --include='*.md' 2>/dev/null | grep -vE '/\.git/' \
|
v=$(grep -rniIE "$VEND" "$ROOT" 2>/dev/null | grep -vE '/\.git/|/\.build/' \
|
||||||
| grep -vE '(^|[./])(commits|git-discipline)\.md:' || true)
|
| grep -vE '(^|[./])(commits|git-discipline)\.md:' \
|
||||||
|
| grep -vE '/(\.githooks/commit-msg|scripts/(leak-gate|test-gates)\.sh):' \
|
||||||
|
| grep -vE "^$ROOT/(CLAUDE|AGENTS)\.md:" || true)
|
||||||
[ -n "$v" ] && { echo "LEAK GATE FAIL (vendor name outside attribution rules):"; echo "$v" | head; fail=1; }
|
[ -n "$v" ] && { echo "LEAK GATE FAIL (vendor name outside attribution rules):"; echo "$v" | head; fail=1; }
|
||||||
|
|
||||||
# Machine-absolute paths leak a local filesystem layout.
|
# Machine-absolute paths leak a local filesystem layout.
|
||||||
p=$(grep -rnE '/Users/|/Volumes/' "$ROOT" --include='*.md' 2>/dev/null | grep -vE '/\.git/' || true)
|
p=$(grep -rniIE '/Users/|/Volumes/' "$ROOT" 2>/dev/null | grep -vE '/\.git/|/\.build/|/scripts/leak-gate\.sh:' || true)
|
||||||
[ -n "$p" ] && { echo "LEAK GATE FAIL (machine-absolute path):"; echo "$p" | head; fail=1; }
|
[ -n "$p" ] && { echo "LEAK GATE FAIL (machine-absolute path):"; echo "$p" | head; fail=1; }
|
||||||
|
|
||||||
# No em dashes in any tracked markdown.
|
# No em dashes in any repository text file.
|
||||||
e=$(grep -rlF "$(printf '\xe2\x80\x94')" "$ROOT" --include='*.md' 2>/dev/null | grep -vE '/\.git/' || true)
|
e=$(grep -rlIF "$(printf '\342\200\224')" "$ROOT" 2>/dev/null | grep -vE '/\.git/|/\.build/' || true)
|
||||||
[ -n "$e" ] && { echo "LEAK GATE FAIL (em dash):"; echo "$e"; fail=1; }
|
[ -n "$e" ] && { echo "LEAK GATE FAIL (em dash):"; echo "$e"; fail=1; }
|
||||||
|
|
||||||
|
# Signature emoji and invisible watermark characters are allowed only where the
|
||||||
|
# attribution rules name the forbidden byte patterns.
|
||||||
|
s=$(
|
||||||
|
{
|
||||||
|
grep -rlIF "$(printf '\360\237\244\226')" "$ROOT" 2>/dev/null
|
||||||
|
grep -rlIF "$(printf '\342\234\250')" "$ROOT" 2>/dev/null
|
||||||
|
grep -rlIF "$(printf '\342\200\213')" "$ROOT" 2>/dev/null
|
||||||
|
grep -rlIF "$(printf '\357\273\277')" "$ROOT" 2>/dev/null
|
||||||
|
} | sort -u | grep -vE '/\.git/|/\.build/|/(core/)?(commits|git-discipline)\.md$' || true
|
||||||
|
)
|
||||||
|
[ -n "$s" ] && { echo "LEAK GATE FAIL (signature emoji or hidden watermark):"; echo "$s"; fail=1; }
|
||||||
|
|
||||||
[ "$fail" = 0 ] || { echo "LEAK GATE FAILED for $ROOT" >&2; exit 1; }
|
[ "$fail" = 0 ] || { echo "LEAK GATE FAILED for $ROOT" >&2; exit 1; }
|
||||||
echo "leak gate clean ($(find "$ROOT" -name '*.md' -not -path '*/.git/*' | wc -l | tr -d ' ') md files)"
|
echo "leak gate clean ($(find "$ROOT" -type f -not -path '*/.git/*' -not -path '*/.build/*' | wc -l | tr -d ' ') repository files)"
|
||||||
|
|||||||
66
third_party/rules-swift/scripts/sync-priority-label.sh
vendored
Normal file
66
third_party/rules-swift/scripts/sync-priority-label.sh
vendored
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Synchronize priority: high with the issue body's Priority field on Forgejo.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
EVENT_PATH=${FORGEJO_EVENT_PATH:?FORGEJO_EVENT_PATH is required}
|
||||||
|
LABEL='priority: high'
|
||||||
|
|
||||||
|
value=$(
|
||||||
|
jq -r '.issue.body // ""' "$EVENT_PATH" | awk '
|
||||||
|
{
|
||||||
|
line = tolower($0)
|
||||||
|
gsub(/^[[:space:]]+|[[:space:]]+$/, "", line)
|
||||||
|
}
|
||||||
|
line ~ /^##+[[:space:]]+priority$/ { in_priority = 1; next }
|
||||||
|
in_priority && line ~ /^##+/ { exit }
|
||||||
|
in_priority && line != "" { print line; exit }
|
||||||
|
'
|
||||||
|
)
|
||||||
|
has=$(jq -r --arg label "$LABEL" 'any(.issue.labels[]?; .name == $label)' "$EVENT_PATH")
|
||||||
|
|
||||||
|
action=unchanged
|
||||||
|
if [ "$value" = high ] && [ "$has" = false ]; then
|
||||||
|
action=add
|
||||||
|
elif [ "$value" != high ] && [ "$has" = true ]; then
|
||||||
|
label_id=$(jq -r --arg label "$LABEL" \
|
||||||
|
'.issue.labels[] | select(.name == $label) | .id' "$EVENT_PATH")
|
||||||
|
[ -n "$label_id" ] || {
|
||||||
|
echo "priority label sync: labelled event has no label id" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
action="remove:$label_id"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${PRIORITY_LABEL_DRY_RUN:-0}" = 1 ]; then
|
||||||
|
echo "$action"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$action" in
|
||||||
|
unchanged)
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
API=${FORGEJO_API_URL:?FORGEJO_API_URL is required}
|
||||||
|
REPOSITORY=${FORGEJO_REPOSITORY:?FORGEJO_REPOSITORY is required}
|
||||||
|
TOKEN=${FORGEJO_TOKEN:?FORGEJO_TOKEN is required}
|
||||||
|
number=$(jq -r '.issue.number' "$EVENT_PATH")
|
||||||
|
endpoint="$API/repos/$REPOSITORY/issues/$number/labels"
|
||||||
|
|
||||||
|
case "$action" in
|
||||||
|
add)
|
||||||
|
payload=$(jq -cn --arg label "$LABEL" '{labels: [$label]}')
|
||||||
|
curl -fsS -X POST \
|
||||||
|
-H "Authorization: token $TOKEN" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
--data "$payload" \
|
||||||
|
"$endpoint" >/dev/null
|
||||||
|
;;
|
||||||
|
remove:*)
|
||||||
|
label_id=${action#remove:}
|
||||||
|
curl -fsS -X DELETE \
|
||||||
|
-H "Authorization: token $TOKEN" \
|
||||||
|
"$endpoint/$label_id" >/dev/null
|
||||||
|
;;
|
||||||
|
esac
|
||||||
45
third_party/rules-swift/scripts/sync-rules.sh
vendored
Normal file
45
third_party/rules-swift/scripts/sync-rules.sh
vendored
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Align a consumer repo to the shared-rules freshness marker: stamp .rules-version
|
||||||
|
# and install the freshness workflow. Baseline configs are SEEDED only if the repo
|
||||||
|
# has none; existing configs are repo-specific (baseline + project rules) and are
|
||||||
|
# never overwritten.
|
||||||
|
#
|
||||||
|
# Usage: scripts/sync-rules.sh <consumer-repo-path> [workflow-dir]
|
||||||
|
# The workflow dir is auto-detected from the repo's origin remote:
|
||||||
|
# codeberg.org (Forgejo) -> .forgejo/workflows | Gitea (default) -> .gitea/workflows
|
||||||
|
# Pass an explicit second arg to override.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
RULES=$(cd "$(dirname "$0")/.." && pwd)
|
||||||
|
TARGET="${1:?usage: sync-rules.sh <consumer-repo-path> [workflow-dir]}"
|
||||||
|
[ -d "$TARGET/.git" ] || { echo "not a git repo: $TARGET" >&2; exit 1; }
|
||||||
|
|
||||||
|
VER=$(tr -d '[:space:]' < "$RULES/VERSION")
|
||||||
|
|
||||||
|
WFDIR="${2:-}"
|
||||||
|
if [ -z "$WFDIR" ]; then
|
||||||
|
remote=$(git -C "$TARGET" remote get-url origin 2>/dev/null || echo "")
|
||||||
|
case "$remote" in
|
||||||
|
*codeberg.org*) WFDIR=".forgejo/workflows" ;; # Forgejo reads .forgejo
|
||||||
|
*) WFDIR=".gitea/workflows" ;; # Gitea reads .gitea
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
for f in .swiftformat .swiftlint.yml; do
|
||||||
|
if [ -f "$TARGET/$f" ]; then
|
||||||
|
echo " kept existing $f (repo-specific)"
|
||||||
|
else
|
||||||
|
cp "$RULES/$f" "$TARGET/$f"
|
||||||
|
echo " seeded $f from baseline (was absent)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
printf '%s\n' "$VER" > "$TARGET/.rules-version"
|
||||||
|
mkdir -p "$TARGET/$WFDIR"
|
||||||
|
legacy="$TARGET/$WFDIR/rules-conformance.yml"
|
||||||
|
if [ -f "$legacy" ] && grep -q '^# Managed by rules-swift' "$legacy"; then
|
||||||
|
rm -f "$legacy"
|
||||||
|
echo " removed legacy $WFDIR/rules-conformance.yml (it proved freshness only)"
|
||||||
|
fi
|
||||||
|
cp "$RULES/rules-freshness.yml" "$TARGET/$WFDIR/rules-freshness.yml"
|
||||||
|
echo " stamped .rules-version=$VER, installed $WFDIR/rules-freshness.yml"
|
||||||
|
echo "done: $TARGET now tracks shared-rules freshness marker v$VER"
|
||||||
212
third_party/rules-swift/scripts/test-gates.sh
vendored
Normal file
212
third_party/rules-swift/scripts/test-gates.sh
vendored
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Negative controls for the repository gates. A gate not seen red is unproven.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT=$(cd "$(dirname "$0")/.." && pwd)
|
||||||
|
TMP=$(mktemp -d "${TMPDIR:-/tmp}/rules-swift-gates.XXXXXX")
|
||||||
|
trap 'rm -rf "$TMP"' EXIT HUP INT TERM
|
||||||
|
|
||||||
|
if grep -Eq '\\x[[:xdigit:]]{2}' \
|
||||||
|
"$ROOT/scripts/leak-gate.sh" "$ROOT/.githooks/commit-msg"; then
|
||||||
|
echo "gate test failed: byte matchers must use portable octal escapes" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$TMP/priority-label"
|
||||||
|
printf '%s\n' '{"issue":{"number":1,"body":"## Priority\n\nhigh","labels":[]}}' \
|
||||||
|
> "$TMP/priority-label/add.json"
|
||||||
|
printf '%s\n' '{"issue":{"number":1,"body":"### Priority\n\nnormal","labels":[{"id":7,"name":"priority: high"}]}}' \
|
||||||
|
> "$TMP/priority-label/remove.json"
|
||||||
|
printf '%s\n' '{"issue":{"number":1,"body":"### Priority\n\nhigh","labels":[{"id":7,"name":"priority: high"}]}}' \
|
||||||
|
> "$TMP/priority-label/unchanged.json"
|
||||||
|
test "$(FORGEJO_EVENT_PATH="$TMP/priority-label/add.json" PRIORITY_LABEL_DRY_RUN=1 \
|
||||||
|
"$ROOT/scripts/sync-priority-label.sh")" = add
|
||||||
|
test "$(FORGEJO_EVENT_PATH="$TMP/priority-label/remove.json" PRIORITY_LABEL_DRY_RUN=1 \
|
||||||
|
"$ROOT/scripts/sync-priority-label.sh")" = remove:7
|
||||||
|
test "$(FORGEJO_EVENT_PATH="$TMP/priority-label/unchanged.json" PRIORITY_LABEL_DRY_RUN=1 \
|
||||||
|
"$ROOT/scripts/sync-priority-label.sh")" = unchanged
|
||||||
|
|
||||||
|
expect_hook() {
|
||||||
|
expected=$1
|
||||||
|
label=$2
|
||||||
|
content=$3
|
||||||
|
file="$TMP/message"
|
||||||
|
printf '%b\n' "$content" > "$file"
|
||||||
|
if "$ROOT/.githooks/commit-msg" "$file" >/dev/null 2>&1; then
|
||||||
|
actual=0
|
||||||
|
else
|
||||||
|
actual=1
|
||||||
|
fi
|
||||||
|
if [ "$actual" -ne "$expected" ]; then
|
||||||
|
echo "gate test failed: $label expected failure=$expected, got failure=$actual" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
expect_hook 0 "valid Conventional Commit" 'docs(readme): clarify setup'
|
||||||
|
expect_hook 1 "invalid type" 'banana: invalid type'
|
||||||
|
expect_hook 1 "long subject" 'docs(readme): explain the complete repository verification contract in excessive detail'
|
||||||
|
expect_hook 1 "automated co-author" 'docs(readme): clarify setup\n\nCo-Authored-By: Codex'
|
||||||
|
expect_hook 1 "assisted attribution" 'docs(readme): clarify setup\n\nAssisted-by: ChatGPT'
|
||||||
|
expect_hook 1 "em dash" 'docs(readme): clarify setup \342\200\224 now'
|
||||||
|
expect_hook 1 "trailing period" 'docs(readme): clarify setup.'
|
||||||
|
expect_hook 1 "breaking footer absent" 'feat(api)!: change response format'
|
||||||
|
expect_hook 0 "breaking change complete" 'feat(api)!: change response format\n\nBREAKING CHANGE: migrate callers to the new response value'
|
||||||
|
expect_hook 1 "breaking marker absent" 'feat(api): change response format\n\nBREAKING CHANGE: migrate callers to the new response value'
|
||||||
|
expect_hook 1 "long body line" 'docs(readme): clarify setup\n\nThis explanation is deliberately longer than seventy-two characters so wrapping enforcement is proven.'
|
||||||
|
expect_hook 0 "comment text ignored" 'docs(readme): clarify setup\n# Co-Authored-By: Codex \342\200\224 ignored comment'
|
||||||
|
expect_hook 1 "signature emoji" 'docs(readme): clarify setup \360\237\244\226'
|
||||||
|
expect_hook 1 "hidden watermark" 'docs(readme): clarify setup\342\200\213'
|
||||||
|
|
||||||
|
printf 'docs(test): planted trailer\n\nCo-Authored-By: Codex\n' > "$TMP/message"
|
||||||
|
if "$ROOT/.githooks/commit-msg" "$TMP/message" > "$TMP/hook-output" 2>&1; then
|
||||||
|
echo "gate test failed: planted attribution trailer passed" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
grep -q 'Rule 5.1' "$TMP/hook-output"
|
||||||
|
|
||||||
|
mkdir -p "$TMP/broken-links"
|
||||||
|
printf '[missing](missing-file)\n\n[reference]: absent/reference.md\n' > "$TMP/broken-links/README.md"
|
||||||
|
if RULES_ROOT="$TMP/broken-links" "$ROOT/scripts/check-links.sh" >/dev/null 2>&1; then
|
||||||
|
echo "gate test failed: link gate accepted missing inline and reference targets" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$TMP/leak"
|
||||||
|
printf 'bad \342\200\224 dash\n' > "$TMP/leak/note.md"
|
||||||
|
if RULES_ROOT="$TMP/leak" "$ROOT/scripts/leak-gate.sh" >/dev/null 2>&1; then
|
||||||
|
echo "gate test failed: leak gate accepted an em dash" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$TMP/signature-leak"
|
||||||
|
printf 'bad \360\237\244\226 signature\n' > "$TMP/signature-leak/note.txt"
|
||||||
|
if RULES_ROOT="$TMP/signature-leak" "$ROOT/scripts/leak-gate.sh" >/dev/null 2>&1; then
|
||||||
|
echo "gate test failed: leak gate accepted a signature emoji" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
mkdir -p "$TMP/signature-rule"
|
||||||
|
printf 'forbidden pattern: \360\237\244\226\n' > "$TMP/signature-rule/commits.md"
|
||||||
|
RULES_ROOT="$TMP/signature-rule" "$ROOT/scripts/leak-gate.sh" >/dev/null
|
||||||
|
|
||||||
|
mkdir -p "$TMP/namespacing"
|
||||||
|
printf 'public struct One {}\npublic struct Two {}\n' > "$TMP/namespacing/Two.swift"
|
||||||
|
if RULES_ROOT="$TMP/namespacing" "$ROOT/scripts/check-namespacing.sh" >/dev/null 2>&1; then
|
||||||
|
echo "gate test failed: namespacing gate accepted two top-level types" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$TMP/file-names"
|
||||||
|
printf 'bad name\n' > "$TMP/file-names/Bad_Name.txt"
|
||||||
|
printf 'bad name\n' > "$TMP/file-names/bad name.txt"
|
||||||
|
printf 'bad name\n' > "$TMP/file-names/bad&name.txt"
|
||||||
|
non_ascii=$(printf 'bad-\305\276.txt')
|
||||||
|
printf 'bad name\n' > "$TMP/file-names/$non_ascii"
|
||||||
|
if RULES_ROOT="$TMP/file-names" "$ROOT/scripts/check-file-names.sh" > "$TMP/file-name-output" 2>&1; then
|
||||||
|
echo "gate test failed: file-name gate accepted invalid fixture names" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
grep -q 'uppercase name' "$TMP/file-name-output"
|
||||||
|
grep -q 'underscore separator' "$TMP/file-name-output"
|
||||||
|
grep -q 'invalid separator or special character' "$TMP/file-name-output"
|
||||||
|
grep -q 'non-ASCII name' "$TMP/file-name-output"
|
||||||
|
|
||||||
|
# Positive control: a known-clean fixture must pass. Without this, a gate that
|
||||||
|
# flags everything (the 2026-07-12 locale bug: bash 3.2's case bracket ranges
|
||||||
|
# collate [A-Z] against the ambient UTF-8 locale, matching all-lowercase names
|
||||||
|
# too) still satisfies every must-fail expectation above and ships silently.
|
||||||
|
mkdir -p "$TMP/file-names-clean"
|
||||||
|
printf 'ok\n' > "$TMP/file-names-clean/clean-name.md"
|
||||||
|
printf 'ok\n' > "$TMP/file-names-clean/README.md"
|
||||||
|
if ! RULES_ROOT="$TMP/file-names-clean" "$ROOT/scripts/check-file-names.sh" >/dev/null 2>&1; then
|
||||||
|
echo "gate test failed: file-name gate rejected a clean fixture" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$TMP/rule-structure"
|
||||||
|
printf '# Binding Rule\n\nMUST do the thing.\n' > "$TMP/rule-structure/example.md"
|
||||||
|
if RULES_ROOT="$TMP/rule-structure" "$ROOT/scripts/check-rule-structure.sh" >/dev/null 2>&1; then
|
||||||
|
echo "gate test failed: rule-structure gate accepted a binding rule with no proof section" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$TMP/core-source" "$TMP/core-target"
|
||||||
|
printf '# Core Rule\n' > "$TMP/core-source/example.md"
|
||||||
|
printf '# Core Rule\n' > "$TMP/core-target/example.md"
|
||||||
|
CORE_SOURCE_DIR="$TMP/core-source" CORE_TARGET_DIR="$TMP/core-target" \
|
||||||
|
"$ROOT/scripts/check-core-sync.sh" >/dev/null
|
||||||
|
printf '\nDrift.\n' >> "$TMP/core-target/example.md"
|
||||||
|
if CORE_SOURCE_DIR="$TMP/core-source" CORE_TARGET_DIR="$TMP/core-target" \
|
||||||
|
"$ROOT/scripts/check-core-sync.sh" >/dev/null 2>&1; then
|
||||||
|
echo "gate test failed: core-sync gate accepted a divergent vendored file" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$TMP/version"
|
||||||
|
git -C "$TMP/version" init -q
|
||||||
|
printf '1.0.0\n' > "$TMP/version/VERSION"
|
||||||
|
printf '# Changelog\n' > "$TMP/version/CHANGELOG.md"
|
||||||
|
printf '# Rule\n' > "$TMP/version/example.md"
|
||||||
|
printf 'name: freshness\n' > "$TMP/version/rules-freshness.yml"
|
||||||
|
git -C "$TMP/version" add VERSION CHANGELOG.md example.md rules-freshness.yml
|
||||||
|
git -C "$TMP/version" -c user.name=Gate -c user.email=gate@example.invalid commit -qm 'docs(rule): seed fixture'
|
||||||
|
printf '\nChanged.\n' >> "$TMP/version/example.md"
|
||||||
|
if RULES_ROOT="$TMP/version" "$ROOT/scripts/check-version-bumped.sh" --working-tree >/dev/null 2>&1; then
|
||||||
|
echo "gate test failed: version gate accepted a rule edit without VERSION" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
printf '1.0.1\n' > "$TMP/version/VERSION"
|
||||||
|
RULES_ROOT="$TMP/version" "$ROOT/scripts/check-version-bumped.sh" --working-tree >/dev/null
|
||||||
|
if RULES_ROOT="$TMP/version" "$ROOT/scripts/check-changelog-touched.sh" --working-tree >/dev/null 2>&1; then
|
||||||
|
echo "gate test failed: changelog gate accepted a rule edit without CHANGELOG.md" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
printf '\n- Changed the rule.\n' >> "$TMP/version/CHANGELOG.md"
|
||||||
|
RULES_ROOT="$TMP/version" "$ROOT/scripts/check-changelog-touched.sh" --working-tree >/dev/null
|
||||||
|
git -C "$TMP/version" add VERSION CHANGELOG.md example.md
|
||||||
|
git -C "$TMP/version" -c user.name=Gate -c user.email=gate@example.invalid commit -qm 'docs(rule): change fixture'
|
||||||
|
printf 'on: push\n' >> "$TMP/version/rules-freshness.yml"
|
||||||
|
if RULES_ROOT="$TMP/version" "$ROOT/scripts/check-version-bumped.sh" --working-tree >/dev/null 2>&1; then
|
||||||
|
echo "gate test failed: version gate ignored the managed freshness workflow" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if RULES_ROOT="$TMP/version" "$ROOT/scripts/check-changelog-touched.sh" --working-tree >/dev/null 2>&1; then
|
||||||
|
echo "gate test failed: changelog gate ignored the managed freshness workflow" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' '[{"number":1,"body":"### Priority\n\nhigh\n\nBlocked on #2. See `missing/file.md` at README.md:10","labels":[]}]' > "$TMP/issues.json"
|
||||||
|
if ISSUES_JSON="$TMP/issues.json" LABELS_JSON="$ROOT/.github/labels.json" "$ROOT/scripts/check-issue-body-staleness.sh" > "$TMP/issues-output" 2>&1; then
|
||||||
|
echo "gate test failed: issue-body gate accepted stale fixture data" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
grep -q 'missing dated Status' "$TMP/issues-output"
|
||||||
|
grep -q 'line-number anchor' "$TMP/issues-output"
|
||||||
|
grep -q 'priority body/label drift' "$TMP/issues-output"
|
||||||
|
grep -q 'phantom backtick path' "$TMP/issues-output"
|
||||||
|
grep -q 'blocker reference is not open' "$TMP/issues-output"
|
||||||
|
printf '%s\n' '[{"number":1,"body":"### Status (YYYY-MM-DD)\n\n2026-07-11: open.\n\n### Priority\n\nnormal","labels":[]}]' > "$TMP/issues.json"
|
||||||
|
ISSUES_JSON="$TMP/issues.json" LABELS_JSON="$ROOT/.github/labels.json" "$ROOT/scripts/check-issue-body-staleness.sh" >/dev/null
|
||||||
|
|
||||||
|
printf '%s\n' '[]' > "$TMP/labels.json"
|
||||||
|
if ISSUES_JSON="$TMP/issues.json" LABELS_JSON="$TMP/labels.json" "$ROOT/scripts/check-issue-body-staleness.sh" > "$TMP/labels-output" 2>&1; then
|
||||||
|
echo "gate test failed: tracker gate accepted a missing repository label set" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
grep -q 'repository label drift' "$TMP/labels-output"
|
||||||
|
|
||||||
|
mkdir -p "$TMP/consumer"
|
||||||
|
git -C "$TMP/consumer" init -q
|
||||||
|
git -C "$TMP/consumer" remote add origin https://codeberg.org/example/consumer.git
|
||||||
|
mkdir -p "$TMP/consumer/.forgejo/workflows"
|
||||||
|
printf '# Managed by rules-swift legacy workflow\n' > "$TMP/consumer/.forgejo/workflows/rules-conformance.yml"
|
||||||
|
"$ROOT/scripts/sync-rules.sh" "$TMP/consumer" >/dev/null
|
||||||
|
test -f "$TMP/consumer/.forgejo/workflows/rules-freshness.yml"
|
||||||
|
test ! -e "$TMP/consumer/.forgejo/workflows/rules-conformance.yml"
|
||||||
|
grep -q 'does not claim' "$TMP/consumer/.forgejo/workflows/rules-freshness.yml"
|
||||||
|
grep -q 'runs-on: ubuntu-latest' "$TMP/consumer/.forgejo/workflows/rules-freshness.yml"
|
||||||
|
printf '# Consumer-owned semantic checks\n' > "$TMP/consumer/.forgejo/workflows/rules-conformance.yml"
|
||||||
|
"$ROOT/scripts/sync-rules.sh" "$TMP/consumer" >/dev/null
|
||||||
|
grep -q 'Consumer-owned semantic checks' "$TMP/consumer/.forgejo/workflows/rules-conformance.yml"
|
||||||
|
|
||||||
|
echo "gate fault-injection tests clean (14 message cases; priority label, link, leak, namespacing, file-name, rule-structure, core-sync, version, changelog, issue-body negatives; freshness migration)"
|
||||||
57
third_party/rules-swift/shared-protocols.md
vendored
57
third_party/rules-swift/shared-protocols.md
vendored
@ -8,7 +8,9 @@ Companion to [dependency-injection.md](dependency-injection.md) and [package-imp
|
|||||||
|
|
||||||
Across the package's targets, every internal target falls into one of two categories:
|
Across the package's targets, every internal target falls into one of two categories:
|
||||||
|
|
||||||
1. **Producer / feature targets** import only `SharedProtocols` plus external dependencies (Foundation, SwiftUI, Combine, third-party SPM). Their SPM `dependencies:` list is `["SharedProtocols", ...external products]` and nothing else internal.
|
1. **Producer / feature targets** import only `SharedProtocols` plus external
|
||||||
|
dependencies available on their supported platforms. Their SPM `dependencies:`
|
||||||
|
list is `["SharedProtocols", ...external products]` and nothing else internal.
|
||||||
2. **Composition root and live-writer targets** (the CLI, networking, storage, environment, and similar glue) import `SharedProtocols` plus the external framework they bridge. Their `dependencies:` list is `["SharedProtocols", ...the framework being wrapped]`. These are the only places where protocol-to-concrete glue exists.
|
2. **Composition root and live-writer targets** (the CLI, networking, storage, environment, and similar glue) import `SharedProtocols` plus the external framework they bridge. Their `dependencies:` list is `["SharedProtocols", ...the framework being wrapped]`. These are the only places where protocol-to-concrete glue exists.
|
||||||
|
|
||||||
There is no third category.
|
There is no third category.
|
||||||
@ -16,7 +18,12 @@ There is no third category.
|
|||||||
## Hard rules
|
## Hard rules
|
||||||
|
|
||||||
1. **`SharedProtocols` has zero internal dependencies.** Its SPM `dependencies:` list is `[]`.
|
1. **`SharedProtocols` has zero internal dependencies.** Its SPM `dependencies:` list is `[]`.
|
||||||
2. **Allowed imports inside `SharedProtocols`:** any external framework (`Foundation`, `SwiftUI`, `Combine`, a logging library, etc.). **Forbidden:** any internal package.
|
2. **Allowed imports inside `SharedProtocols`:** Swift standard-library types and
|
||||||
|
the portable Foundation modules supplied by the toolchain. **Forbidden:** UI
|
||||||
|
frameworks, third-party package products, and every internal target. The
|
||||||
|
target's package-dependency list remains `[]`; a contract that would expose an
|
||||||
|
external or renderer type instead defines a neutral value or stays in its
|
||||||
|
adapter target.
|
||||||
3. **Declarations are top-level.** `public protocol Recording`, not nested under a namespace anchor like `extension SharedProtocols { protocol Recording }`.
|
3. **Declarations are top-level.** `public protocol Recording`, not nested under a namespace anchor like `extension SharedProtocols { protocol Recording }`.
|
||||||
4. **Naming follows the domain noun form.** `Recording`, `FileSystem`, `HTTPRequester`. The protocol is the noun; the conformer is `LiveRecording`. You do not have to suffix the protocol with `-able` or `-ing`.
|
4. **Naming follows the domain noun form.** `Recording`, `FileSystem`, `HTTPRequester`. The protocol is the noun; the conformer is `LiveRecording`. You do not have to suffix the protocol with `-able` or `-ing`.
|
||||||
5. **One concept per file**, filename equal to the type name. Group related files in subfolders (`Coordinators/`, `Providers/`) when there are three or more related declarations.
|
5. **One concept per file**, filename equal to the type name. Group related files in subfolders (`Coordinators/`, `Providers/`) when there are three or more related declarations.
|
||||||
@ -27,7 +34,10 @@ There is no third category.
|
|||||||
- **Protocol seams:** every protocol that exists to decouple a producer from a concrete (`Recording`, `HTTPRequester`, `FileSystem`, `UUIDProvider`, `DateProvider`, and so on).
|
- **Protocol seams:** every protocol that exists to decouple a producer from a concrete (`Recording`, `HTTPRequester`, `FileSystem`, `UUIDProvider`, `DateProvider`, and so on).
|
||||||
- **Coordinator protocols** for navigation/flow when the codebase uses them.
|
- **Coordinator protocols** for navigation/flow when the codebase uses them.
|
||||||
- **Value types** that travel across targets: enums (`HTTPMethod`, `LogLevel`), small structs (`HTTPResponse`, a domain model).
|
- **Value types** that travel across targets: enums (`HTTPMethod`, `LogLevel`), small structs (`HTTPResponse`, a domain model).
|
||||||
- **Null implementations** (`NullRecording`, `NullHTTPRequester`) so producers can default to a no-op without importing a live writer.
|
- **Null implementations** (`NullRecording`, `NullHTTPRequester`) that composition
|
||||||
|
roots, previews, and tests may inject explicitly when no behavior is intended.
|
||||||
|
Producers still receive the collaborator through `init`; they do not select a
|
||||||
|
hidden default.
|
||||||
- **DEBUG-only `Preview_*_Impl`** no-op conformances for SwiftUI previews and tests.
|
- **DEBUG-only `Preview_*_Impl`** no-op conformances for SwiftUI previews and tests.
|
||||||
|
|
||||||
## What does NOT go in `SharedProtocols`
|
## What does NOT go in `SharedProtocols`
|
||||||
@ -37,17 +47,20 @@ There is no third category.
|
|||||||
|
|
||||||
## Why
|
## Why
|
||||||
|
|
||||||
Cross-package coupling through protocols (rather than concrete imports) only works if the protocol is reachable without dragging in the rest of the producer's universe. If a protocol lives in producer package `Logging`, then any consumer must `import Logging` to see it. Multiply that across every seam type and a producer ends up importing many internal packages. `SharedProtocols` collapses that to one import.
|
Cross-target coupling through protocols (rather than concrete imports) only works
|
||||||
|
if the protocol is reachable without dragging in the rest of the producer's
|
||||||
|
universe. If a protocol lives in producer target `Logging`, then any consumer must
|
||||||
|
`import Logging` to see it. Multiply that across every seam type and a producer
|
||||||
|
ends up importing many internal modules. `SharedProtocols` collapses that to one
|
||||||
|
import.
|
||||||
|
|
||||||
Live writers import `SharedProtocols` for the protocol and the external framework for the concrete, never an intermediate package.
|
Live writers import `SharedProtocols` for the protocol and the external framework
|
||||||
|
for the concrete, never an intermediate internal target.
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
// Sources/SharedProtocols/Coordinators/AppCoordinator.swift
|
// Sources/SharedProtocols/Coordinators/AppCoordinator.swift
|
||||||
import SwiftUI
|
|
||||||
import Combine
|
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public protocol AppCoordinator: AnyObject {
|
public protocol AppCoordinator: AnyObject {
|
||||||
func showWelcome()
|
func showWelcome()
|
||||||
@ -58,8 +71,6 @@ public protocol AppCoordinator: AnyObject {
|
|||||||
|
|
||||||
```swift
|
```swift
|
||||||
// Sources/SharedProtocols/Coordinators/PreviewHelpers.swift
|
// Sources/SharedProtocols/Coordinators/PreviewHelpers.swift
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
public final class Preview_AppCoordinator_Impl: AppCoordinator {
|
public final class Preview_AppCoordinator_Impl: AppCoordinator {
|
||||||
public init() {}
|
public init() {}
|
||||||
@ -148,26 +159,28 @@ Before adding a new `import` to a producer Swift file:
|
|||||||
## CI guard
|
## CI guard
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# SharedProtocols must have no internal-package imports.
|
# SharedProtocols imports only portable toolchain Foundation modules.
|
||||||
# External imports (Foundation, SwiftUI, Combine, ...) are fine.
|
|
||||||
INTERNAL_TARGETS=$(swift package describe --type json | jq -r '.targets[].name')
|
|
||||||
SHARED_PROTOCOLS_PATH="Sources/SharedProtocols"
|
SHARED_PROTOCOLS_PATH="Sources/SharedProtocols"
|
||||||
[ -d "$SHARED_PROTOCOLS_PATH" ] || SHARED_PROTOCOLS_PATH="Packages/Sources/SharedProtocols"
|
[ -d "$SHARED_PROTOCOLS_PATH" ] || SHARED_PROTOCOLS_PATH="Packages/Sources/SharedProtocols"
|
||||||
for ln in $(grep -rn '^import ' "$SHARED_PROTOCOLS_PATH"/); do
|
grep -rn '^import ' "$SHARED_PROTOCOLS_PATH"/ | while IFS= read -r hit; do
|
||||||
mod=$(echo "$ln" | awk '{print $2}')
|
import_line=${hit#*:*:}
|
||||||
if echo "$INTERNAL_TARGETS" | grep -qx "$mod"; then
|
mod=$(printf '%s\n' "$import_line" | awk '{print $2}')
|
||||||
echo "FAIL: SharedProtocols imports internal target $mod ($ln)"
|
case "$mod" in
|
||||||
exit 1
|
Foundation|FoundationEssentials|FoundationInternationalization) ;;
|
||||||
fi
|
*)
|
||||||
|
echo "FAIL: SharedProtocols imports non-toolchain module $mod ($hit)"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
done
|
done
|
||||||
```
|
```
|
||||||
|
|
||||||
## Sequencing
|
## Sequencing
|
||||||
|
|
||||||
If you migrate existing scattered protocols into `SharedProtocols`, do it as one coordinated arc, not a per-package vote:
|
If you migrate existing scattered protocols into `SharedProtocols`, do it as one coordinated arc, not a per-target vote:
|
||||||
|
|
||||||
1. Move every cross-target protocol and value type into `SharedProtocols`.
|
1. Move every cross-target protocol and value type into `SharedProtocols`.
|
||||||
2. Update every internal target's deps: producers gain `["SharedProtocols"]` and drop intermediate packages; live writers gain `["SharedProtocols", <external framework>]`.
|
2. Update every internal target's deps: producers gain `["SharedProtocols"]` and drop intermediate targets; live writers gain `["SharedProtocols", <external framework>]`.
|
||||||
3. Update every old `import` to `import SharedProtocols`.
|
3. Update every old `import` to `import SharedProtocols`.
|
||||||
4. Delete the now-empty intermediate source directories and target declarations.
|
4. Delete the now-empty intermediate source directories and target declarations.
|
||||||
5. Build and test green; commit.
|
5. Build and test green; commit.
|
||||||
@ -176,5 +189,5 @@ If you migrate existing scattered protocols into `SharedProtocols`, do it as one
|
|||||||
|
|
||||||
- [dependency-injection.md](dependency-injection.md): DI principles the seams serve
|
- [dependency-injection.md](dependency-injection.md): DI principles the seams serve
|
||||||
- [package-import-contract.md](package-import-contract.md): what each target may import
|
- [package-import-contract.md](package-import-contract.md): what each target may import
|
||||||
- [package-architecture.md](package-architecture.md): package layers and granularity
|
- [package-architecture.md](package-architecture.md): target layers and granularity
|
||||||
- [package-structure.md](package-structure.md): repository and manifest layout
|
- [package-structure.md](package-structure.md): repository and manifest layout
|
||||||
|
|||||||
45
third_party/rules-swift/testing.md
vendored
45
third_party/rules-swift/testing.md
vendored
@ -595,6 +595,51 @@ public struct MockFileClient: FileClientProtocol {
|
|||||||
|
|
||||||
The protocol MAY be public (so the mock can implement it). The mock MUST live in the test target.
|
The protocol MAY be public (so the mock can implement it). The mock MUST live in the test target.
|
||||||
|
|
||||||
|
#### Exception: test-support targets
|
||||||
|
|
||||||
|
SwiftPM does not let one test target depend on another, so a double or a fixture that
|
||||||
|
several test targets share cannot live in any one of them. A project MAY keep such
|
||||||
|
shared code in a **test-support target** under `Packages/Sources/`. A test-support
|
||||||
|
target conforms only when all four hold:
|
||||||
|
|
||||||
|
1. **Declared.** It is listed, with the reason it exists, in a checked-in table
|
||||||
|
(`docs/test-support-targets.md`). A target that is not in the table is not a
|
||||||
|
test-support target, and the mocks rule above applies to it.
|
||||||
|
2. **Not published.** No `.library` and no `.executable` product exposes it.
|
||||||
|
3. **Not shipped, except by allowlist.** No target of a shipped product depends on it,
|
||||||
|
except the dependency edges the table names, each with a reason. The usual reason
|
||||||
|
is a demo or diagnostic executable that runs against an in-memory implementation.
|
||||||
|
4. **Mechanically checked.** The project's gate reads the resolved package
|
||||||
|
(`swift package dump-package`) and fails on a published support target, on an edge
|
||||||
|
that is not in the allowlist, and on a support target missing from the table.
|
||||||
|
|
||||||
|
A real implementation that happens to run in memory and that an app ships (an in-memory
|
||||||
|
store behind a seam, chosen by the composition root) is not a test double. It is an
|
||||||
|
adapter, it is subject to the import contract and not to this rule, and it is named for
|
||||||
|
what it is and not `Mock` or `Fake`.
|
||||||
|
|
||||||
|
Acceptance check, with `SUPPORT` set to the names in the table:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SUPPORT='["TestSupport","ConversationFixtures","CloudFake"]'
|
||||||
|
swift package --package-path Packages dump-package | jq -r --argjson s "$SUPPORT" '
|
||||||
|
(.products[] | select(any(.targets[]; . as $t | $s | index($t) != null)) | "published: " + .name),
|
||||||
|
(.targets[] | select(.type == "executable" or .type == "regular")
|
||||||
|
| select(.name as $n | $s | index($n) == null) | . as $x
|
||||||
|
| .dependencies[]? | (.target // .byName)[0]
|
||||||
|
| select(. as $d | $s | index($d) != null) | "\($x.name) -> \(.)")'
|
||||||
|
```
|
||||||
|
|
||||||
|
No line may begin with `published:`. Every `A -> B` line MUST appear in the table's
|
||||||
|
allowlist with a reason, and the allowlist MUST NOT name an edge that the command does
|
||||||
|
not print.
|
||||||
|
|
||||||
|
WRONG: `Sources/FileClientMock/` exposed as a library product, so any consumer can link
|
||||||
|
a mock into a release build.
|
||||||
|
|
||||||
|
RIGHT: `Sources/TestSupport/` listed in the table, absent from `products:`, and
|
||||||
|
depended on only by test targets and the one named diagnostic executable.
|
||||||
|
|
||||||
### Running a single target's tests
|
### Running a single target's tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
2
third_party/rules-swift/ui/appkit-views.md
vendored
2
third_party/rules-swift/ui/appkit-views.md
vendored
@ -7,6 +7,8 @@ user action as `perform(_ intent:)`, and re-renders on the model's change signal
|
|||||||
observed through `withObservationTracking`. It holds ZERO business logic and owns
|
observed through `withObservationTracking`. It holds ZERO business logic and owns
|
||||||
ZERO state that is not view-local.
|
ZERO state that is not view-local.
|
||||||
|
|
||||||
|
> **Naming.** In a codebase that uses the cross-framework layer, the platform type names in the examples below are written as their `AppUI*` aliases and the module imported is `AppUIKit`; see `appuikit.md` (MANDATORY).
|
||||||
|
|
||||||
## Core rules
|
## Core rules
|
||||||
|
|
||||||
1. The renderer renders Surface state only: no business logic, no API calls, no domain decisions.
|
1. The renderer renders Surface state only: no business logic, no API calls, no domain decisions.
|
||||||
|
|||||||
37
third_party/rules-swift/ui/appuikit.md
vendored
Normal file
37
third_party/rules-swift/ui/appuikit.md
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# AppUIKit: imperative AppKit and UIKit code goes through the cross-framework layer
|
||||||
|
|
||||||
|
**Status: MANDATORY for every AppKit or UIKit renderer, feature, and app shell, in every app.** Imperative AppKit and UIKit code imports `AppUIKit` and names its `AppUI*` types. It never types an `NS` or `UI` UI type of its own and never imports `AppKit` or `UIKit` directly. When the layer lacks an alias or a helper the code needs, the component is added to the layer first, then used.
|
||||||
|
|
||||||
|
AppUIKit is the sibling package that is the cross-framework namespace for imperative AppKit and UIKit code: one `AppUI*` alias for each type both frameworks have (`AppUIView`, `AppUIViewController`, `AppUITableView`, `AppUISplitViewController`, `AppUIToolbar`, `AppUIApplicationDelegate`, ...), a few semantic helpers (colors, fonts, the current graphics context, accessibility identifiers, SF Symbols), and a re-export of the platform framework and Core Graphics, so `import AppUIKit` alone reaches both. It is a naming layer, not a UI framework.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
- **One name, one place.** A type reference reads the same on macOS and iOS, so shared logic (a status line, a persona control, a tool palette, a layer row) is written once, and the `#if canImport` noise stays inside the layer instead of at every call site.
|
||||||
|
- **A missing piece is fixed once.** An alias or helper that one app lacks is added to the layer and every app gains it; the alternative is each app typing the platform name and drifting.
|
||||||
|
- **It does not change what is native.** This rule governs how types are *named*, not what the UI looks like. `native-per-platform.md` still decides the shape of the interface per device: a menu bar and split columns on the Mac, one-handed bars on the iPhone. The renderer for each SDK is still its own, over the same framework-free model (`pre-ui-layer.md`). SwiftUI code does not use the layer.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
1. **Import.** An AppKit or UIKit renderer, feature, or shell target imports `AppUIKit`. A direct `import AppKit` or `import UIKit` is a violation (the layer re-exports them).
|
||||||
|
2. **Names.** In those targets no `NS`-prefixed or `UI`-prefixed UI type is written. Use the alias: `AppUIView`, not `NSView` or `UIView`; `AppUIMenu`, not `NSMenu` or `UIMenu`; `AppUIApplicationDelegate`, not `NSApplicationDelegate` or `UIApplicationDelegate`. Use the layer's helper where one exists: `AppUIKit.currentCGContext()` for the drawing context, `AppUIImage.appUISymbol(_:description:)` for an SF Symbol, `setAXIdentifier(_:)` for an accessibility identifier.
|
||||||
|
3. **Foundation names are not UI types.** `NSObject`, `NSString`, `NSNumber`, `NSError`, `NSLock`, `NSNotification`, `NSData`, `NSURL`, `NSDate`, and the like are Foundation and are fine. A project keeps this list short and checked in with its gate.
|
||||||
|
4. **A missing component is added, not worked around.** If the layer has no alias or helper for what the code needs, add it to the layer (a one-line alias, with its test and changelog entry, on both frameworks where both have the type; on the one that has it where only one does), publish it, and then use it. Do not type the platform name "just this once", and do not write a private alias in the app.
|
||||||
|
5. **Where the two frameworks genuinely differ, the code differs.** A split view controller, a toolbar, a table's data source, and event handling are not the same API in AppKit and UIKit; the alias names the type, the code around it is platform code in its own target. The layer does not paper over that and this rule does not ask it to.
|
||||||
|
6. **Out of scope.** The strings of a property list or a project manifest (`UIApplicationSceneManifest`, `UISceneConfigurations`, and so on) are keys, not types, and stay as they are. The rule applies to Swift source of AppKit and UIKit targets, not to the layer itself, which is where the platform names live.
|
||||||
|
|
||||||
|
## Acceptance check
|
||||||
|
|
||||||
|
A gate, not a review. Run from the repository root, with `TARGETS` the directories of the AppKit and UIKit renderer, feature, and shell targets; both commands must print nothing:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 1. no direct import of the platform framework
|
||||||
|
grep -rnE '^[[:space:]]*(@preconcurrency[[:space:]]+)?import (AppKit|UIKit)$' $TARGETS --include='*.swift'
|
||||||
|
|
||||||
|
# 2. no NS or UI type named (the tokens only; the framework name itself, Foundation names, and the module's own UIKit*/AppKit* names excepted)
|
||||||
|
grep -rnoE '\b(NS|UI)[A-Z][A-Za-z0-9]*' $TARGETS --include='*.swift' \
|
||||||
|
| grep -vE ':(NSObject|NSObjectProtocol|NSString|NSNumber|NSError|NSLock|NSNotification|NSData|NSURL|NSDate|UIKit[A-Za-z0-9]*|AppKit[A-Za-z0-9]*)$'
|
||||||
|
```
|
||||||
|
|
||||||
|
A project promotes the second command to a build-failing script with a self-test (a fixture that names `UIView` after `import AppUIKit` must fail it; a fixture that names only `AppUIView` and `NSObject` must pass it), the same way `pre-ui-layer.md` requires the UI-import boundary to be mechanical. The check passes only when both are empty and every alias the code uses resolves to a type on both frameworks it is used on.
|
||||||
|
|
||||||
|
Cross-references: `pre-ui-layer.md` (the renderer is the only code that imports a UI framework; this rule says how it names the SDK), `native-per-platform.md` (what the interface is; this rule is only how it is spelled), `appkit-views.md`, `uikit-views.md`.
|
||||||
18
third_party/rules-swift/ui/components.md
vendored
18
third_party/rules-swift/ui/components.md
vendored
@ -4,13 +4,13 @@ How a Tiledown-style native editor builds its UI from reusable, registry-driven
|
|||||||
|
|
||||||
## One component per file
|
## One component per file
|
||||||
|
|
||||||
- Each UI component lives in its own component package, NEVER in feature packages or screens.
|
- Each UI component lives in its own component target, NEVER in feature targets or screens.
|
||||||
- ONE component per file; the file name equals the component name.
|
- ONE component per file; the file name equals the component name.
|
||||||
- Every component conforms to the `Component` protocol from the `Components` package.
|
- Every component conforms to the `Component` protocol from the `Components` target.
|
||||||
|
|
||||||
## Core rules
|
## Core rules
|
||||||
|
|
||||||
### Rule 1: Three-package hierarchy
|
### Rule 1: Three-target hierarchy
|
||||||
|
|
||||||
Use this strict layering:
|
Use this strict layering:
|
||||||
|
|
||||||
@ -24,9 +24,9 @@ AppComponents (app-specific - depends on Components + theme + font packages)
|
|||||||
AllComponents (optional aggregator for previews - depends on Components + AppComponents)
|
AllComponents (optional aggregator for previews - depends on Components + AppComponents)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Rule 2: Components package foundation
|
### Rule 2: Components target foundation
|
||||||
|
|
||||||
Create the `Components` package FIRST with the core infrastructure:
|
Create the `Components` target FIRST with the core infrastructure:
|
||||||
|
|
||||||
- MUST contain: the `Component` protocol, `AnyComponent`, `ComponentsRegistry`, `ComponentFactory`.
|
- MUST contain: the `Component` protocol, `AnyComponent`, `ComponentsRegistry`, `ComponentFactory`.
|
||||||
- MUST contain: bundle management, registration interfaces, and the component-list model/view.
|
- MUST contain: bundle management, registration interfaces, and the component-list model/view.
|
||||||
@ -38,11 +38,11 @@ Create the `Components` package FIRST with the core infrastructure:
|
|||||||
Depend ONLY on hot-reload tooling:
|
Depend ONLY on hot-reload tooling:
|
||||||
|
|
||||||
- A hot-reload injection library and a file-watching library.
|
- A hot-reload injection library and a file-watching library.
|
||||||
- NEVER depend on UI packages (theme, font). Mixing them in breaks the hot-reload layer's independence from the UI foundation.
|
- NEVER depend on UI targets (theme, font). Mixing them in breaks the hot-reload layer's independence from the UI foundation.
|
||||||
|
|
||||||
### Rule 4: AppComponents dependencies
|
### Rule 4: AppComponents dependencies
|
||||||
|
|
||||||
Depend on the foundation packages:
|
Depend on the foundation targets:
|
||||||
|
|
||||||
- `Components` (core system)
|
- `Components` (core system)
|
||||||
- The theme package (colors, styles)
|
- The theme package (colors, styles)
|
||||||
@ -52,8 +52,8 @@ Depend on the foundation packages:
|
|||||||
|
|
||||||
Register components via the registry:
|
Register components via the registry:
|
||||||
|
|
||||||
- All components conform to the `Component` protocol from the `Components` package.
|
- All components conform to the `Component` protocol from the `Components` target.
|
||||||
- Registration happens in a per-package `*ComponentsRegistration.swift`.
|
- Registration happens in a per-target `*ComponentsRegistration.swift`.
|
||||||
- Register external components through the shared registry.
|
- Register external components through the shared registry.
|
||||||
|
|
||||||
## The Component protocol
|
## The Component protocol
|
||||||
|
|||||||
7
third_party/rules-swift/ui/flowspec.md
vendored
7
third_party/rules-swift/ui/flowspec.md
vendored
@ -1,13 +1,13 @@
|
|||||||
# FlowSpec (declarative UI scenarios)
|
# FlowSpec (declarative UI scenarios)
|
||||||
|
|
||||||
**Status: RECOMMENDED, and the expected form when a UI ships in more than one renderer.** A UI flow is described once, as a declarative scenario, and replayed against every UI implementation. The scenario is the single source of truth for what a flow does; the implementations cannot drift in their behavior because they all run the same steps. The engine is the public package [FlowSpec](https://github.com/mihaelamj/FlowSpec).
|
**Status: RECOMMENDED, and the expected form when a UI ships in more than one renderer.** A UI flow is described once, as a declarative scenario, and replayed against every UI implementation. The scenario is the single source of truth for what a flow does; the implementations cannot drift in their behavior because they all run the same steps. The engine is the public package [FlowSpec](https://codeberg.org/Mihaela/FlowSpec).
|
||||||
|
|
||||||
## The model
|
## The model
|
||||||
|
|
||||||
- A **scenario** is a JSON file: `{ id, title, actor?, preconditions[], tags[], steps[] }`.
|
- A **scenario** is a JSON file: `{ id, title, actor?, preconditions[], tags[], steps[] }`.
|
||||||
- A **step** is `{ verb, target, arg? }`. Its key is `"verb:target"`.
|
- A **step** is `{ verb, target, arg? }`. Its key is `"verb:target"`.
|
||||||
- A **verb** comes from a small closed vocabulary (`open`, `tap`, `type`, `swipe`, `wait`, `assert`, `request`). The vocabulary is deliberately tiny so a scenario reads like a specification, not a script; adding a verb is a coordinated change across the schema, the enum, and every registry.
|
- A **verb** comes from a small closed vocabulary (`open`, `tap`, `type`, `swipe`, `wait`, `assert`, `request`, `ambient`, `rotate`). `ambient` injects an automatic, no-user event (a timer, a notification, an agent's progress) at its true position in the ordered run; `rotate` turns the device to `portrait` or `landscape`. The vocabulary is deliberately tiny so a scenario reads like a specification, not a script; adding a verb is a coordinated change across the schema, the enum, and every registry.
|
||||||
- A **step registry** is the seam: one per UI, a `@MainActor` type with a single `execute(_ step: Step) throws` that turns a step into a real action against that UI. The registry throws on an unknown `verb:target`, so a scenario can never reference machinery that is not wired up. FlowSpec ships a default identifier-driven XCUITest registry in its `FlowSpecXCUI` module (see `pom.md` for the page-object pattern it drives); the headless registry that drives the model layer you write yourself. The core `FlowSpec` engine stays `Foundation`-only.
|
- A **step registry** is the seam: one per UI, a `@MainActor` type with a single `execute(_ step: Step) throws` that turns a step into a real action against that UI. The registry throws on an unknown `verb:target`, so a scenario can never reference machinery that is not wired up. FlowSpec ships a default identifier-driven XCUITest registry in its `FlowSpecXCUI` module (see `pom.md` for the page-object pattern it drives); the headless registry that drives the model layer you write yourself. When the thing a step drives is asynchronous (a pre-UI engine whose one door is `async`), the registry is an `AsyncStepRegistry` run by `AsyncScenarioRunner`, so the replay awaits the engine's real concurrency instead of reducing it to a blocking call. The core `FlowSpec` engine stays `Foundation`-only.
|
||||||
- The **runner** walks a scenario's steps in order, stops on the first failure, and reports the scenario id and step index, so a log points at the exact step.
|
- The **runner** walks a scenario's steps in order, stops on the first failure, and reports the scenario id and step index, so a log points at the exact step.
|
||||||
|
|
||||||
## Why and when
|
## Why and when
|
||||||
@ -22,6 +22,7 @@
|
|||||||
- Drive the on-device registry through page objects (`pom.md`), not raw queries.
|
- Drive the on-device registry through page objects (`pom.md`), not raw queries.
|
||||||
- Replay the same scenario corpus headless against the model layer; do not fork a separate headless test set.
|
- Replay the same scenario corpus headless against the model layer; do not fork a separate headless test set.
|
||||||
- Let the runner stop at the first failure and report the scenario id and step index.
|
- Let the runner stop at the first failure and report the scenario id and step index.
|
||||||
|
- Load a hand-edited scenario with `ScenarioLoader.loadValidated`, which also rejects an inconsistent one (no steps, a `swipe` with no direction, a `wait` with a non-positive timeout, a `rotate` to no orientation) with every finding named by its rule code; `load` only decodes.
|
||||||
- Throw `unknownStep` on an unwired `verb:target` so a scenario cannot reference missing machinery.
|
- Throw `unknownStep` on an unwired `verb:target` so a scenario cannot reference missing machinery.
|
||||||
|
|
||||||
## DON'T
|
## DON'T
|
||||||
|
|||||||
53
third_party/rules-swift/ui/pane-layout.md
vendored
Normal file
53
third_party/rules-swift/ui/pane-layout.md
vendored
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# Pane layout and adaptive columns
|
||||||
|
|
||||||
|
**Status: MANDATORY for every app that lays a document out in panes, columns or pages.** Learned on an iPad and an iPhone Duo with a real editor, and each rule below is a defect that was shipped and found by looking at the screen.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
A split gives each pane a size; the content of a pane then asks for one too. When the two disagree the content wins, silently: the pane's content is wider than its pane, a divider is drawn over a search field, rows are cut. A window that counts as wide by a fixed number lays three columns out in a portrait iPad and leaves the document a hundred points. A model that changes under a view is not always seen by the view. None of this shows in a unit test of the model; it shows in a picture of the real window.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
1. **The split sizes a pane; the content does not.** Lay the content of a pane over a view that takes whatever room it is offered (`Color.clear.overlay(alignment: .topLeading) { content }` then `.clipped()`), so that a part that needs more than the pane has is cut at the edge and cannot widen the pane. Do not rely on the content to be small.
|
||||||
|
2. **Optional parts are decided by a measurement, not by a container that is meant to drop them.** A tip put in `ViewThatFits` with a `minWidth` was found to make a 240 point pane's content 296 points wide even when it showed nothing. Measure the pane (`onGeometryChange`) and put the optional part in only when the pane is wide enough for it.
|
||||||
|
3. **A header fits the narrowest pane it will have.** Put the controls that are always there in one row and the ones that come and go in a second row that exists only when needed; give a field a short prompt. A UI test fails when any control is past the pane's edge (the divider beside the pane is the edge).
|
||||||
|
4. **Columns are decided by the room the parts need, not by a fixed width.** The window is wide enough for the sidebar, the document and the inspector beside it when its width exceeds the sum of their widths and the document's least width. Put the sum in one pure function and test it with the real window widths of Apple's devices (a 13 inch iPad upright is 1032 points: a flat 1000 counted it as wide). Anything narrower gives the window to the document and puts the inspector in a popover.
|
||||||
|
5. **A layout is saved for the way the device is held.** Key the saved layout by the arrangement and make the arrangement include the orientation; a comment that says two keys are separate is not a test, so test it.
|
||||||
|
6. **A way to give the document the whole area.** A control in the toolbar that hides the outline and the panes beside it, and shows them again, is a toggle that looks pressed while they are shown; the pane's own maximize control and the toolbar control stay in step.
|
||||||
|
7. **A view of a platform view is brought up to date by a change of its inputs, and keeps its controller.** SwiftUI's update of a representable was not called when a document was opened on an iPad, and a coordinator was made more than once for one view, so the update went to a controller whose view was not the one on screen. Read what the view shows where the parent makes it (so the parent's change makes a new value), and keep the controller in an object the view owns, so the view that is shown and the controller that is updated are the same.
|
||||||
|
8. **Look at the screen, in both orientations and at the narrowest size, before saying it is done.** A picture of each arrangement is a deliverable of the change (`accessibility.md`, `iphone-duo.md`).
|
||||||
|
|
||||||
|
## One set of panes on a Mac, an iPad and an iPhone (SuperSplit)
|
||||||
|
|
||||||
|
Learned by running the same editor on all three with SuperSplit for the split. The panes are one set of views; what differs is the container, and the container is chosen by the room, not by the device.
|
||||||
|
|
||||||
|
1. **One view per pane role, many containers.** Write each pane once, by role (`EditorPane(role: .outline)`), and let the container decide how roles are shown: all six side by side in a Mac window; the outline, the source and the diagnostics in a split on an iPad, with the inspector beside it or in a popover; a page of a `TabView` each on a phone, where SuperSplit is not used at all. A phone does not get a split of tiny panes: pages are the same views without the split. Never fork a pane per device.
|
||||||
|
2. **The arrangement is a value, and it names the saved layout.** `desktop`, `tablet`, `upright` and `short` (little height) are cases of one enum; the container is made again (`.id(arrangement)`) when the arrangement changes, and each arrangement keeps its own saved layout, per device, per kind of document. A layout laid out for a wide window is not what suits a tall one, and a fold or a rotation must not apply one to the other. Test that the keys differ.
|
||||||
|
3. **A pane's share and its least size are two different limits, and the least must win.** The outline is a share of the width (30 per cent on a tablet); on an iPad with a sidebar and an inspector that share came out below what the outline needs. Give the pane a least size, and have the layout clamp the share to it when the container shrinks (`setMinimumSize` and a ratio clamped at layout time), with an exhaustive test over container widths that the least holds. Whatever the share, the content must fit the least (rule 3 above).
|
||||||
|
4. **The Mac hides the problem by being wide.** A pane that fits in a wide Mac window is squeezed on an iPad that shares its width with a sidebar and an inspector. Test every pane at its least width, on every platform, not at the size the app first opens at.
|
||||||
|
5. **The divider is the edge of a pane, and the accessibility tree tells where it is.** A divider is a bar about 28 points wide centered on the boundary between two panes, drawn over both. Its frame in the accessibility tree is the line a UI test uses to assert that no control of a pane is past its edge (`Divider between Outline and 2 panes`); on the Mac it is a slider, on iOS adjustable. Read frames from the tree; a picture shows that something is wrong, the frames say what.
|
||||||
|
6. **A pane's content is cut at the card, so what is cut is not seen.** The pane card clips its body: a control that does not fit is silently lost, which is the worst failure. Pair the clip with a test that fails on any control past the edge, and with a picture of each arrangement.
|
||||||
|
7. **Maximize is the way to give one pane the whole area.** SuperSplit maximizes a pane over the layout; a toolbar toggle for "hide the outline" is maximize of the source, kept in step with the pane's own control, saved with the arrangement, and applied when the arrangement is made.
|
||||||
|
8. **Platform views inside a pane need an owner that lives as long as the pane.** A pane's content is made by a closure inside the split, and SwiftUI made the coordinator of a text view more than once for one pane, so a change of the model went to a controller whose view was not on the screen. Keep the controller in an object the pane's view owns, and read the model where the pane builds the view.
|
||||||
|
9. **Chrome as glass is a background.** The header strip and the round controls take the glass look as a background behind the title and the glyphs (through a layer that falls back, such as Forwards), so that what is drawn on them is drawn the same where a view is rendered off the screen and in pixel tests.
|
||||||
|
10. **Test it at three levels, and keep them apart.** The pure layout (positions of panes and dividers for a list of container sizes, probed from the model), the real window (frames from the accessibility tree, in each orientation and at the largest text size), and a picture of each arrangement looked at by a person. The first would have found the squeezed outline, the second the header under the divider, and only the third the squeezed source pane.
|
||||||
|
|
||||||
|
## How to find what widens a pane
|
||||||
|
|
||||||
|
Switch the parts off one at a time at run time (an environment variable passed to the app through the test's launch environment, not the runner's) and measure the frame of the content, the pane and the divider from the accessibility tree: with the header and the list both off the content was still too wide, which named the tip. Never judge by what a view looks like; read the frames.
|
||||||
|
|
||||||
|
## Acceptance check
|
||||||
|
|
||||||
|
Run from the repository root; `SOURCES` are the Swift sources of the app and its UI packages. Both must print nothing:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 1. no window counted as wide by a fixed width
|
||||||
|
grep -rnE '(windowWidth|size\.width|width) *> *(9|1[0-9])[0-9]{2}\b' "$SOURCES" --include='*.swift'
|
||||||
|
|
||||||
|
# 2. no optional part left to ViewThatFits with a least width
|
||||||
|
grep -rn -A4 'ViewThatFits' "$SOURCES" --include='*.swift' | grep -E 'minWidth'
|
||||||
|
```
|
||||||
|
|
||||||
|
The change passes when those print nothing, the unit tests of the width rule and of the saved layouts for each arrangement pass, and the UI test that no control of a pane is past its edge passes in each orientation.
|
||||||
|
|
||||||
|
Cross-references: `iphone-duo.md` (the same size decisions on a foldable), `accessibility.md` (the text size changes the arrangement), `ui/swiftui-views.md` and `ui/uikit-views.md`, `ui/three-renderers.md`, `core/proof-discipline.md` (what was observed and what was assumed), `core/no-shortcuts-first-principles.md` (a clip hides the symptom: the cause is named and tested).
|
||||||
2
third_party/rules-swift/ui/pom.md
vendored
2
third_party/rules-swift/ui/pom.md
vendored
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
1. **A base page owns the mechanics.** A single shared base class is constructed with the `XCUIApplication` injected (`init(app:)`) and holds every reusable interaction: waiting for an element to appear or disappear, tapping, asserting existence, scrolling to an element, taking a screenshot. Every method threads `file: StaticString = #filePath, line: UInt = #line` so a failure blames the test's call site, not the base page. Methods are `open` so a page can override a wait without re-implementing it.
|
1. **A base page owns the mechanics.** A single shared base class is constructed with the `XCUIApplication` injected (`init(app:)`) and holds every reusable interaction: waiting for an element to appear or disappear, tapping, asserting existence, scrolling to an element, taking a screenshot. Every method threads `file: StaticString = #filePath, line: UInt = #line` so a failure blames the test's call site, not the base page. Methods are `open` so a page can override a wait without re-implementing it.
|
||||||
|
|
||||||
A default identifier-driven XCUITest registry ships in `FlowSpecXCUI`, part of the [FlowSpec](https://github.com/mihaelamj/FlowSpec) package, so an app whose views set accessibility identifiers can run scenarios with no custom registry code; the page objects themselves are per-app. See `flowspec.md`.
|
A default identifier-driven XCUITest registry ships in `FlowSpecXCUI`, part of the [FlowSpec](https://codeberg.org/Mihaela/FlowSpec) package, so an app whose views set accessibility identifiers can run scenarios with no custom registry code; the page objects themselves are per-app. See `flowspec.md`.
|
||||||
|
|
||||||
2. **One page object per screen.** A concrete page subclasses the base class, file name equals page name. Its locators are computed `XCUIElement` properties that query the app by a shared identifier constant and the typed element kind (`app.buttons[ID.Catalog.search]`, `app.staticTexts[ID.Item.title]`), never a raw string. Its methods split into verifications (return `Self`) and actions (return the next page).
|
2. **One page object per screen.** A concrete page subclasses the base class, file name equals page name. Its locators are computed `XCUIElement` properties that query the app by a shared identifier constant and the typed element kind (`app.buttons[ID.Catalog.search]`, `app.staticTexts[ID.Item.title]`), never a raw string. Its methods split into verifications (return `Self`) and actions (return the next page).
|
||||||
|
|
||||||
|
|||||||
2
third_party/rules-swift/ui/swiftui-views.md
vendored
2
third_party/rules-swift/ui/swiftui-views.md
vendored
@ -122,6 +122,8 @@ struct CardStyle: ViewModifier {
|
|||||||
|
|
||||||
### Accessibility
|
### Accessibility
|
||||||
|
|
||||||
|
The full rule is [accessibility.md](../accessibility.md): text styles and Dynamic Type, names that say what a control does, 28 point targets, an alternative for every gesture, contrast, Reduce Motion, and the audit that fails the build. What follows is the minimum for one element.
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
TextField("Name", text: $name)
|
TextField("Name", text: $name)
|
||||||
.accessibilityLabel("Your name")
|
.accessibilityLabel("Your name")
|
||||||
|
|||||||
4
third_party/rules-swift/ui/uikit-views.md
vendored
4
third_party/rules-swift/ui/uikit-views.md
vendored
@ -6,6 +6,8 @@ How a Tiledown-style native editor writes its UIKit renderer: `UIViewController`
|
|||||||
action as `perform(_ intent:)`, and re-renders on the model's change signal. It
|
action as `perform(_ intent:)`, and re-renders on the model's change signal. It
|
||||||
holds ZERO business logic and owns ZERO state that is not view-local.
|
holds ZERO business logic and owns ZERO state that is not view-local.
|
||||||
|
|
||||||
|
> **Naming.** In a codebase that uses the cross-framework layer, the platform type names in the examples below are written as their `AppUI*` aliases and the module imported is `AppUIKit`; see `appuikit.md` (MANDATORY).
|
||||||
|
|
||||||
## Core rules
|
## Core rules
|
||||||
|
|
||||||
1. The renderer renders Surface state only: no business logic, no API calls, no domain decisions.
|
1. The renderer renders Surface state only: no business logic, no API calls, no domain decisions.
|
||||||
@ -124,6 +126,8 @@ private func apply(_ dirty: Set<SurfaceArea>) {
|
|||||||
|
|
||||||
### Accessibility
|
### Accessibility
|
||||||
|
|
||||||
|
The full rule is [accessibility.md](../accessibility.md): text styles and Dynamic Type, names that say what a control does, 28 point targets, an alternative for every gesture, contrast, Reduce Motion, and the audit that fails the build. What follows is the minimum for one element.
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
reloadButton.accessibilityLabel = "Reload tiles"
|
reloadButton.accessibilityLabel = "Reload tiles"
|
||||||
reloadButton.accessibilityHint = "Fetches the latest tiles"
|
reloadButton.accessibilityHint = "Fetches the latest tiles"
|
||||||
|
|||||||
2
third_party/rules-swift/ui/view-models.md
vendored
2
third_party/rules-swift/ui/view-models.md
vendored
@ -28,7 +28,7 @@ ViewModel = Coordinator (NOT a business-logic container)
|
|||||||
|
|
||||||
### Rule 3: Dependency injection
|
### Rule 3: Dependency injection
|
||||||
|
|
||||||
- MUST inject collaborators (see docs/rules/dependency-injection.md)
|
- MUST inject collaborators (see [dependency-injection.md](../dependency-injection.md))
|
||||||
- SHOULD prefer struct-based dependencies over protocols where practical
|
- SHOULD prefer struct-based dependencies over protocols where practical
|
||||||
- NO global singletons
|
- NO global singletons
|
||||||
|
|
||||||
|
|||||||
65
third_party/rules-swift/versioning.md
vendored
Normal file
65
third_party/rules-swift/versioning.md
vendored
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
# Versioning: one VERSION file, a semantic version, a build number that is the commit count
|
||||||
|
|
||||||
|
**Status: MANDATORY for every app and service that ships a version number.** The version is written once, in a `VERSION` file, and is a Semantic Versioning 2.0.0 value. The build number is the number of commits on the first-parent chain of the release branch at build time. Both reach the code through a generated file, never through a constant that someone edits. The App Store fields take only what Apple documents for them, and everything richer stays in our own strings.
|
||||||
|
|
||||||
|
Each rule below carries its basis: **documented** (Apple documentation or the Semantic Versioning specification, named so it can be read), **observed** (found by running the toolchain), or **design** (a choice, with the reason).
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
- **A version kept in several places drifts.** A hand-edited Swift constant, a project setting of `1.0`, and a store listing will disagree within a sprint. One file has nothing to disagree with.
|
||||||
|
- **The store fields are narrow.** The short version takes three integers and the build takes one to three; neither takes a pre-release tag or a hash. A scheme that wants those keeps them in logs, bug reports, and headers, where they cost nothing.
|
||||||
|
- **A build number that names a commit is a pointer.** A tester reports build 42; `git rev-list --first-parent --reverse main | sed -n 42p` returns the commit. A timestamp or a counter in a CI service cannot be walked back that way from a clone.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
1. **One source: the `VERSION` file at the repository root.** It holds one line, a version in the form `major.minor.patch` with an optional `-pre.release`. No other file states a version number by hand: no Swift constant, no project setting, no plist. *Basis: design.*
|
||||||
|
2. **It is parsed by the grammar, not by habit.** A value that is not Semantic Versioning 2.0.0 (a leading `v`, a missing part, a leading zero, an empty identifier, a number too large for an `Int`) has no value, and the build stops with a message that names the file and the text. `VERSION` carries no build metadata; the build adds its own. *Basis: documented (semver.org, 2.0.0, sections 2, 9 and 10).*
|
||||||
|
3. **The store's short version is the core and nothing else.** `CFBundleShortVersionString` is `major.minor.patch`: digits and periods, three integers. A pre-release or build metadata cannot go in it. *Basis: documented (Apple, `CFBundleShortVersionString`: "The required format is three period-separated integers ... can only contain numeric characters (0-9) and periods").*
|
||||||
|
4. **The build number is the first-parent commit count of the release branch.** `git rev-list --first-parent --count HEAD`. On the release branch it grows by exactly one per commit and every value names one commit, which a plain `--count` does not guarantee once a merge brings in side commits. It never restarts when the version changes. *Basis: documented for the need (Apple, `CFBundleVersion` is "one to three period-separated integers"; and Xcode Cloud: "Mac apps must follow different build number requirements ... its build number must continuously increase, even across app versions"); design for the first-parent choice.*
|
||||||
|
5. **It is generated at build time, by a build-tool plugin, into a file the code reads.** The generator reads `VERSION` and git and writes a Swift file of constants; the version type in the code returns them. The plugin's command names its inputs: `VERSION`, and the git files that move when `HEAD` does (`.git/HEAD` and its reflog, `.git/logs/HEAD`, which every commit, merge, reset and checkout appends to). Name only inputs that exist; SwiftPM treats a missing input as an error. *Basis: observed (SwiftPM 6.x, macOS): in a clone with the inputs named, one new commit changed the generated build number from 43 to 44 and the commit hash with it.*
|
||||||
|
6. **No silent default.** A generator that cannot count commits stops. A shallow clone counts too few and is refused (`git rev-parse --is-shallow-repository`); a work tree with no repository is refused. A build that has no git history on purpose (a container image built from a copy of the sources) is given the facts in two named variables (`APP_BUILD_NUMBER`, a positive integer, and `APP_COMMIT`, a short hash), and a build with neither stops. Never fall back to `1`. *Basis: design. A shallow clone has fewer commits than the branch, so its count is smaller than the true one and can fall below a build already uploaded; a fallback to `1` is the same failure with a constant.*
|
||||||
|
7. **CI clones the full history.** Every job that builds a version fetches all commits (`fetch-depth: 0` in GitHub Actions, `GIT_DEPTH: 0` in GitLab). *Basis: the generator refuses anything else (rule 6).*
|
||||||
|
8. **The richer form is for our own strings.** `semanticVersion` is `major.minor.patch+build.commit` for a release and `major.minor.patch-pre.release.build+commit` for a pre-release, with `.dirty` appended to the metadata when tracked files had uncommitted changes. For a pre-release the count is a numeric identifier, which semantic versioning orders by value (`beta.9` before `beta.42`); metadata is ignored in precedence, so on a release it can name a build but not order it. It is used in logs, bug reports, and request headers, and nowhere a store field is. *Basis: documented (semver.org, section 11, precedence); design for where it is used.*
|
||||||
|
9. **The release branch's history is append-only.** A rebase, a force-push, or a re-import of that branch can lower the count below a build already uploaded, and a Mac app cannot recover from a lower build number. Whatever syncs commits into the branch merges them. *Basis: documented (the Mac rule in rule 4); design for the consequence.*
|
||||||
|
10. **A store build comes from the release branch, from a clean tree.** The release lane refuses any other branch and any uncommitted change to a tracked file, so the number leads back to one commit. *Basis: design.*
|
||||||
|
11. **A second upload of the same commit needs its own build number.** A commit count numbers commits, not uploads; App Store Connect requires each version and build pair to be unique. The dotted form (`42`, then `42.1`) is available because Apple allows up to three integers, but how App Store Connect orders `42.1` against `42` is **not established** here and is tried with one real upload before it is relied on. *Basis: documented for the uniqueness (Xcode Cloud, "unique combination of CFBundleShortVersionString and CFBundleVersion"); the ordering is **not established**.*
|
||||||
|
12. **The apps take the settings from the same place.** The release lane passes `MARKETING_VERSION` and `CURRENT_PROJECT_VERSION` to the build from one script that reads `VERSION` and git, so a project file's own `1.0` and `1` are never what ships. A plain run from the IDE shows the project file's values; that is a known limit, stated, not hidden. *Basis: design.*
|
||||||
|
13. **Bumping is one command that edits one file.** It validates the text by the grammar of rule 2 before writing, and it writes nothing else. *Basis: design.*
|
||||||
|
14. **In an Xcode app target the stamp is a post-build script phase.** Where the build is an Xcode project and not a package plugin, one script reads `VERSION` and the first-parent count (refusing a shallow clone, rule 6) and writes `CFBundleShortVersionString` and `CFBundleVersion` into the built product's `Info.plist`; user-script sandboxing is switched off for that target so the script can read git. The project's own `MARKETING_VERSION` stays a placeholder. The phase names the built `Info.plist` as an input file (`$(TARGET_BUILD_DIR)/$(INFOPLIST_PATH)`), so that the stamp runs after the system has written the plist: without it, a clean build wrote the plist after the stamp and the app reported no version. *Basis: observed (the stamped values read back from the built app's plist; on a clean build of an iPad app the stamp ran before `ProcessInfoPlistFile` and was lost, and after the input file was named it ran after it and was kept); design for the choice.*
|
||||||
|
|
||||||
|
## How to test it
|
||||||
|
|
||||||
|
- **The parser against the specification.** Accept the specification's own valid examples and read each back as written; refuse each of its invalid forms (leading zeros, empty identifiers, missing parts, a leading `v`, whitespace, a number past `Int`); check the precedence chain `1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0` in every pair; check that `1.9.0 < 1.10.0` (numbers, not text); check that metadata takes no part in precedence but is part of equality.
|
||||||
|
- **The version type against the file.** A test reads `VERSION` and compares the core and the pre-release with the type's; it asks git for the first-parent count and requires the built number to be positive and no more than it.
|
||||||
|
- **The rule that builds the richer form, apart from the generated numbers.** Take it as a function of its inputs and hold it to its closed form: the release shape, the pre-release shape, the ordering of two builds of one beta, a pre-release below its release, the dirty mark, and that the text parses back to an equal value.
|
||||||
|
- **The generator's refusals.** Run the built tool against a shallow clone, a bad `VERSION`, a `VERSION` with metadata, a missing `VERSION`, a tree with no git and no variables, and a bad `APP_BUILD_NUMBER`; each must exit non-zero with a message that names the cause. Run it with both variables and no git; it must succeed.
|
||||||
|
- **Regeneration.** In a throwaway clone, build, make an empty commit, build again, and require the generated number to rise by one. This is the test that the plugin's inputs are named.
|
||||||
|
|
||||||
|
## Acceptance check
|
||||||
|
|
||||||
|
Run from the repository root. `VERSION` is the file; `SOURCES` is the directory of package sources. Every command must print nothing, except where it says it must print a line:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 1. VERSION is one line and is a semantic version without build metadata
|
||||||
|
grep -Ex '(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?' VERSION | grep -c . | grep -vx 1
|
||||||
|
|
||||||
|
# 2. no Swift source states a version number by hand (a constant named major, minor, patch, sprint or build with an integer literal)
|
||||||
|
grep -rnE 'static (let|var) (major|minor|patch|sprint|build(Number)?)[^=]*=[[:space:]]*[0-9]+' "$SOURCES" --include='*.swift' | grep -v 'Generated'
|
||||||
|
|
||||||
|
# 3. the generator never falls back to a made-up count
|
||||||
|
grep -rnE 'return 1$' "$SOURCES"/*Generator* --include='*.swift'
|
||||||
|
|
||||||
|
# 4. the plugin names its inputs (this one must print a line)
|
||||||
|
grep -rn 'inputFiles' "$SOURCES"/../Plugins --include='*.swift'
|
||||||
|
|
||||||
|
# 5. CI fetches the full history (each of these must print a line where the file exists)
|
||||||
|
grep -n 'fetch-depth: 0' .github/workflows/*.yml 2>/dev/null
|
||||||
|
grep -n 'GIT_DEPTH: 0' .gitlab-ci.yml 2>/dev/null
|
||||||
|
|
||||||
|
# 6. the release build number equals the commit count of the commit built (prints the count; compare it to the build)
|
||||||
|
git rev-list --first-parent --count HEAD
|
||||||
|
```
|
||||||
|
|
||||||
|
Promote commands 1 to 3 to a build-failing script with a self-test (a fixture that breaks each must fail it; a fixture that obeys all must pass it), and the tests above to the project's test suite. The check passes when the commands print as stated, the tests pass, and rule 11 is either tried with an upload or stated as not tried.
|
||||||
|
|
||||||
|
Cross-references: `package-structure.md` (the plugin and its generator are their own targets), `dependency-injection.md` (the version type is read, never reached through a singleton), `testing.md` and `core/testing-discipline.md` (the tests and the regeneration check), `core/oracle-first.md` (the specification's examples are the answer key for the parser), `core/proof-discipline.md` (the basis of each claim), `core/git-discipline.md` (append-only history on the release branch).
|
||||||
Loading…
Reference in New Issue
Block a user